Samplerbox LCD Display

As already posted, I have created a Samplerbox. Mine has a 16×2 LCD Display instead of an LED display. To use this configuration, just connect the LCD identical as the LED display. The coding needed is a library found at github to access the lcd display:
https://github.com/dotmat/Mathew-s-Git/blob/master/BeeSafe/i2c/i2clibraries/i2c_lcd_smbus.py
Just copy the library file in the same directory as the python script. The samplerbox.py needed is as follows:

#
#  SamplerBox 
#
#  author:    Joseph Ernest (twitter: @JosephErnest, mail: contact@samplerbox.org)
#  url:       http://www.samplerbox.org/
#  license:   Creative Commons ShareAlike 3.0 (http://creativecommons.org/licenses/by-sa/3.0/)
#
#  samplerbox.py: Main file
#



#########################################
##  LOCAL  
##  CONFIG
#########################################

AUDIO_DEVICE_ID = 2                     # change this number to use another soundcard
SAMPLES_DIR = "/media/"                       # The root directory containing the sample-sets. Example: "/media/" to look for samples on a USB stick / SD card
USE_SERIALPORT_MIDI = True             # Set to True to enable MIDI IN via SerialPort (e.g. RaspberryPi's GPIO UART pins)
USE_I2C_7SEGMENTDISPLAY = True         # Set to True to use a 7-segment display via I2C 
USE_BUTTONS = True                     # Set to True to use momentary buttons (connected to RaspberryPi's GPIO pins) to change preset
MAX_POLYPHONY = 80                      # This can be set higher, but 80 is a safe value


#########################################
##  IMPORT 
##  MODULES
#########################################

import wave
import time
import numpy
import os, re
import pyaudio
import threading   
from chunk import Chunk
import struct
import rtmidi_python as rtmidi
import samplerbox_audio



#########################################
##  SLIGHT MODIFICATION OF PYTHON'S WAVE MODULE
##  TO READ CUE MARKERS & LOOP MARKERS
#########################################

class waveread(wave.Wave_read):
    def initfp(self, file):
        self._convert = None
        self._soundpos = 0
        self._cue = []
        self._loops = []
        self._ieee = False
        self._file = Chunk(file, bigendian = 0)
        if self._file.getname() != 'RIFF':
            raise Error, 'file does not start with RIFF id'
        if self._file.read(4) != 'WAVE':
            raise Error, 'not a WAVE file'
        self._fmt_chunk_read = 0
        self._data_chunk = None
        while 1:
            self._data_seek_needed = 1
            try:
                chunk = Chunk(self._file, bigendian = 0)
            except EOFError:
                break
            chunkname = chunk.getname()
            if chunkname == 'fmt ':
                self._read_fmt_chunk(chunk)
                self._fmt_chunk_read = 1
            elif chunkname == 'data':
                if not self._fmt_chunk_read:
                    raise Error, 'data chunk before fmt chunk'
                self._data_chunk = chunk
                self._nframes = chunk.chunksize // self._framesize
                self._data_seek_needed = 0
            elif chunkname == 'cue ':
                numcue = struct.unpack('<i',chunk.read(4))[0]
                for i in range(numcue): 
                  id, position, datachunkid, chunkstart, blockstart, sampleoffset = struct.unpack('<iiiiii',chunk.read(24))
                  self._cue.append(sampleoffset)
            elif chunkname == 'smpl':
                manuf, prod, sampleperiod, midiunitynote, midipitchfraction, smptefmt, smpteoffs, numsampleloops, samplerdata = struct.unpack('<iiiiiiiii',chunk.read(36))
                for i in range(numsampleloops):
                   cuepointid, type, start, end, fraction, playcount = struct.unpack('<iiiiii',chunk.read(24))                     self._loops.append([start,end])              chunk.skip()         if not self._fmt_chunk_read or not self._data_chunk:             raise Error, 'fmt chunk and/or data chunk missing'     def getmarkers(self):         return self._cue              def getloops(self):         return self._loops ######################################### ##  MIXER CLASSES ######################################### class PlayingSound:     def __init__(self, sound, note):         self.sound = sound         self.pos = 0         self.fadeoutpos = 0         self.isfadeout = False         self.note = note     def fadeout(self, i):         self.isfadeout = True              def stop(self):         try: playingsounds.remove(self)          except: pass class Sound:     def __init__(self, filename, midinote, velocity):         wf = waveread(filename)         self.fname = filename         self.midinote = midinote         self.velocity = velocity         if wf.getloops():              self.loop = wf.getloops()[0][0]             self.nframes = wf.getloops()[0][1] + 2         else:             self.loop = -1             self.nframes = wf.getnframes()         self.data = self.frames2array(wf.readframes(self.nframes), wf.getsampwidth(), wf.getnchannels())         wf.close()                 def play(self, note):         snd = PlayingSound(self, note)         playingsounds.append(snd)         return snd     def frames2array(self, data, sampwidth, numchan):         if sampwidth == 2:             npdata = numpy.fromstring(data, dtype = numpy.int16)         elif sampwidth == 3:             npdata = samplerbox_audio.binary24_to_int16(data, len(data)/3)         if numchan == 1:              npdata = numpy.repeat(npdata, 2)         return npdata FADEOUTLENGTH = 30000 FADEOUT = numpy.linspace(1., 0., FADEOUTLENGTH)            # by default, float64 FADEOUT = numpy.power(FADEOUT, 6) FADEOUT = numpy.append(FADEOUT, numpy.zeros(FADEOUTLENGTH, numpy.float32)).astype(numpy.float32) SPEED = numpy.power(2, numpy.arange(0.0, 84.0)/12).astype(numpy.float32) samples = {} playingnotes = {} sustainplayingnotes = [] sustain = False playingsounds = [] globalvolume = 10 ** (-12.0/20)    #  -12dB default global volume ######################################### ##  AUDIO AND MIDI CALLBACKS ######################################### def AudioCallback(in_data, frame_count, time_info, status):     global playingsounds     rmlist = []     playingsounds = playingsounds[-MAX_POLYPHONY:]         b = samplerbox_audio.mixaudiobuffers(playingsounds, rmlist, frame_count, FADEOUT, FADEOUTLENGTH, SPEED)     for e in rmlist:         try: playingsounds.remove(e)         except: pass     b *= globalvolume     odata = (b.astype(numpy.int16)).tostring()        return (odata, pyaudio.paContinue) def MidiCallback(message, time_stamp):     global playingnotes, sustain, sustainplayingnotes     global preset     messagetype = message[0] >> 4
    messagechannel = (message[0] & 15) + 1
    note = message[1] if len(message) > 1 else None
    midinote = note
    velocity = message[2] if len(message) > 2 else None

    if messagetype == 9 and velocity == 0: messagetype = 8

    if messagetype == 9:    # Note on
        try:
          playingnotes.setdefault(midinote,[]).append(samples[midinote, velocity].play(note))
        except:
          pass

    elif messagetype == 8:  # Note off
        if midinote in playingnotes:
            for n in playingnotes[midinote]: 
                if sustain:
                    sustainplayingnotes.append(n)
                else:
                    n.fadeout(50)
            playingnotes[midinote] = []

    elif messagetype == 12: # Program change
        print 'Program change ' + str(note)
        preset = note
        LoadSamples()

    elif (messagetype == 11) and (note == 64) and (velocity < 64): # sustain pedal off         for n in sustainplayingnotes:             n.fadeout(50)         sustainplayingnotes = []                sustain = False     elif (messagetype == 11) and (note == 64) and (velocity >= 64): # sustain pedal on
        sustain = True



#########################################
##  LOAD SAMPLES
#########################################

LoadingThread = None            
LoadingInterrupt = False

def LoadSamples():
    global LoadingThread
    global LoadingInterrupt

    if LoadingThread:
        LoadingInterrupt = True
        LoadingThread.join()
        LoadingThread = None
    
    LoadingInterrupt = False
    LoadingThread = threading.Thread(target = ActuallyLoad)
    LoadingThread.daemon = True
    LoadingThread.start()

NOTES = ["c", "c#", "d", "d#", "e", "f", "f#", "g", "g#", "a", "a#", "b"]

def ActuallyLoad():    
    global preset
    global samples
    global playingsounds
    global globalvolume
    playingsounds = []
    samples = {}    
    dirname = next((f for f in os.listdir(SAMPLES_DIR) if f.startswith("%d " % preset)), None)      # or next(glob.iglob("blah*"), None)
    globalvolume = 10 ** (-12.0/20)    #  -12dB default global volume
    if dirname:
        samplename = dirname
        dirname = os.path.join(SAMPLES_DIR, dirname)
    if not dirname: 
        print 'Preset empty: %s' % preset
        display("%03d Empty" % preset,'                ')
        return
    print 'Preset loading: %s' % preset
    display("%03d Loading..." % preset,samplename)
    definitionfname = os.path.join(dirname, "%d.txt" % preset)                     # parse the sample-set definition file
    if not os.path.isfile(definitionfname): 
        definitionfname = os.path.join(dirname, "definition.txt")
    if os.path.isfile(definitionfname):
        with open(definitionfname, 'r') as definitionfile:
            for i, pattern in enumerate(definitionfile):
                try:
                    if r'%%volume' in pattern:        # %%paramaters are global parameters
                        globalvolume *= 10 ** (float(pattern.split('=')[1].strip()) / 20)
                        continue
                    defaultparams = { 'midinote': '0', 'velocity': '127', 'notename': '' }
                    if len(pattern.split(',')) > 1:
                        defaultparams.update(dict([item.split('=') for item in pattern.split(',', 1)[1].replace(' ','').replace('%', '').split(',')]))
                    pattern = pattern.split(',')[0]
                    pattern = pattern.replace("%midinote", r"(?P\d+)").replace("%velocity", r"(?P\d+)").replace("%notename", r"(?P[A-Ga-g]#?[0-9])").replace("*", r".*").strip()
                    for fname in os.listdir(dirname):
                        if LoadingInterrupt: 
                            return
                        m = re.match(pattern, fname)
                        if m:
                            info = m.groupdict()
                            midinote = int(info.get('midinote', defaultparams['midinote']))
                            velocity = int(info.get('velocity', defaultparams['velocity']))
                            notename = info.get('notename', defaultparams['notename'])
                            if notename: midinote = NOTES.index(notename[:-1].lower()) + (int(notename[-1])+2) * 12
                            samples[midinote, velocity] = Sound(os.path.join(dirname, fname), midinote, velocity)
                except:
                    print "Error in definition file, skipping line %s." % (i+1)

    else:
        for midinote in range(0, 127): 
            if LoadingInterrupt: 
                return
            file = os.path.join(dirname, "%d.wav" % midinote)
            if os.path.isfile(file):
                samples[midinote, 127] = Sound(file, midinote, 127)             

    initial_keys = set(samples.keys())
    for midinote in xrange(128):
        lastvelocity = None
        for velocity in xrange(128):
            if (midinote, velocity) not in initial_keys:
                samples[midinote, velocity] = lastvelocity
            else: 
                if not lastvelocity: 
                    for v in xrange(velocity): samples[midinote, v] = samples[midinote, velocity]
                lastvelocity = samples[midinote, velocity]
        if not lastvelocity: 
            for velocity in xrange(128): 
                try: samples[midinote, velocity] = samples[midinote-1, velocity]
                except: pass
    if len(initial_keys) > 0:
      print 'Preset loaded: ' + str(preset) 
      display("%03d" % preset,samplename)    
    else:
      print 'Preset empty: ' + str(preset) 
      display("E%03d Empty" % preset,'')    



#########################################
##  OPEN AUDIO DEVICE
#########################################

p = pyaudio.PyAudio()
stream = p.open(format = pyaudio.paInt16, channels = 2, rate = 44100, frames_per_buffer = 512, output = True, input = False, output_device_index = AUDIO_DEVICE_ID, stream_callback = AudioCallback)
print 'Opened audio: '+ p.get_device_info_by_index(AUDIO_DEVICE_ID)['name']



#########################################
##  BUTTONS THREAD (RASPBERRY PI GPIO) 
#########################################

if USE_BUTTONS:
    import RPi.GPIO as GPIO

    lastbuttontime = 0

    def Buttons():
        GPIO.setmode(GPIO.BCM)
        GPIO.setup(18, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP)    
        global preset, lastbuttontime
        while True:
          now = time.time()
          if not GPIO.input(18) and (now - lastbuttontime) > 0.2:
            lastbuttontime = now 
            preset -= 1
            if preset < 0: preset = 127             LoadSamples()           elif not GPIO.input(17)  and (now - lastbuttontime) > 0.2:
            lastbuttontime = now
            preset += 1  
            if preset > 127: preset = 0
            LoadSamples()      

          time.sleep(0.02)

    ButtonsThread = threading.Thread(target = Buttons)
    ButtonsThread.daemon = True
    ButtonsThread.start()    



#########################################
##  7-SEGMENT DISPLAY 
##  
#########################################

if USE_I2C_7SEGMENTDISPLAY:
    import i2c_lcd_smbus
    lcd = i2c_lcd_smbus.i2c_lcd(0x27,1, 2, 1, 0, 4, 5, 6, 7, 3)
    lcd.command(lcd.CMD_Display_Control | lcd.OPT_Enable_Display)
    lcd.backLightOn()

    def display(s,t):
        lcd.setPosition(1,0)
        lcd.writeString(s+"                ")
        lcd.setPosition(2, 0)
	lcd.writeString(t+"                ")
    display('Starting','Samplerbox')
    time.sleep(0.5)

else:

    def display(s):
        pass    



#########################################
##  MIDI IN via SERIAL PORT
##  
#########################################

if USE_SERIALPORT_MIDI:
    import serial

    ser = serial.Serial('/dev/ttyAMA0', baudrate=38400)       # see hack in /boot/cmline.txt : 38400 is 31250 baud for MIDI!

    def MidiSerialCallback():
        message = [0, 0, 0]
        while True:
          i = 0
          while i < 3:             data = ord(ser.read(1)) # read a byte             if data >> 7 != 0:  
              i = 0      # status byte!   this is the beginning of a midi message: http://www.midi.org/techspecs/midimessages.php
            message[i] = data
            i += 1
            if i == 2 and message[0] >> 4 == 12:  # program change: don't wait for a third byte: it has only 2 bytes
              message[2] = 0
              i = 3
          MidiCallback(message, None)

    MidiThread = threading.Thread(target = MidiSerialCallback)
    MidiThread.daemon = True
    MidiThread.start()



#########################################
##  LOAD FIRST SOUNDBANK
##  
#########################################     

preset = 0
LoadSamples()



#########################################
##  MIDI DEVICES DETECTION
##  MAIN LOOP
#########################################

midi_in = [rtmidi.MidiIn()]
previous = []
while True:
    for port in midi_in[0].ports:
        if port not in previous and 'Midi Through' not in port:
            midi_in.append(rtmidi.MidiIn())
            midi_in[-1].callback = MidiCallback        
            midi_in[-1].open_port(port)       
            print 'Opened MIDI: '+ port
    previous = midi_in[0].ports
    time.sleep(2)

2 Gedanken zu „Samplerbox LCD Display“

  1. Servus
    Habe mit Interesse den Beitrag I2C-Display für SamplerBox gelesen. Auch ich habe diese Samplerbox nachgebaut und möchte ein I2C-LCD Display anschliessen. Erlaube mir bitte die Frage: Wie hast du die neue sampler.py kopieren können? Ich hab mir das image auf der SD-Card installiert aber keine Schreibrechte ! Beim editieren mit nano im Kommandozeilenmode, kann ich wohl ändern aber nicht speichern! Bin kein Linux-Kenner und wäre für eine Hilfestellung sehr dankbar!
    Außerdem funktioniert meine Samplerbox nicht zufriedenstellend. Wenn ich einen Ton nur kurz anschlage, klingt er ewig nach. Das Klavier funktioniert auch nur holprig…..
    Könnte das daran liegen, dass ich einen normalen RaspberryPi verwende und keinen + ? Wie und worauf läuft bei dir die Box? Hoffe du sprichst deutsch.
    Mit freundlichem Gruß
    Hans-Peter Traussnigg
    (nähe Graz – Österreich)

    1. Wenn du dich per SSH auf der Samplerbox einloggst steht da in der Willkommensnachricht wie man das Filesystem ummounten muss, damit man darauf schreiben kann. Das ist dann bis zum Neustart beschreibbar. Aber vor dem Abstöpseln einen Shutdown machen, damit das Filesystem nicht kaputt geht (sicher ist sicher).
      Am besten kopierst du das File von Windows aus per winscp auf den RaspberryPi, oder bei anderen Betriebssystemen einen anderem scp (secure copy) Programm.
      Es gibt 16×2 Displays mit anderer I2c Adresse. Manche haben z.B. 0x20. Das sollte bei der Beschreibung hoffentlich stehen. Ansonsten kann man das auch herausfinden wenn das angeschlossen ist. Da kann man z.B. unter http://www.enigma14.eu/wiki/1602_LCD_on_RPi_B+ schauen wie das geht. Dann im Quelltext nach 0x27 suchen und das austauschen.

      Ich verwende einen RaspberryPi2 und bin damit voll zufrieden. Es läuft alles völlig problemlos. Auf dem alten RPi habe ich es nicht probiert. So groß sollte der Unterschied aber nicht sein. Ich könnte mir da eher Probleme mit der USB-Soundkarte vorstellen. Den Standard RPi Audioausgang brauchst du gar nicht zu probieren, das ist völlig unbrauchbar.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.