Bastelkiste: Pro Mini

Hier mal eine kurze Vorstellung des Arduino Pro Mini. Das ist im Prinzip ein normales Arduino-Board nur nicht für Anfänger – daher ‚Pro‘ und sehr klein – daher ‚Mini‘. Aussehen tut das Board so:

ProMini

Das sieht groß aus, ist aber wie am Titelbild zu sehen viel viel kleiner als ein Arduino Uno und kostet auch nur ca. 10 Euro. Beim China Versender eures Vertrauens bekommt man den auch schon mal für 3 Euro. Wenn ihr also mit eurem Uno genug herumgesteckt habt, und das Projekt jetzt so bleiben soll, kann man den Lötkolben anheizen und alles zusammenlöten.
– Aber Moment zuerst gibt es noch was zu beachten:

  • Es gibt eine 3,3V/8Mhz und eine 5V/16Mhz Version. Die Spannung ist dabei gar nicht so interessant. Alle AVR Mega Microcontroller von den Arduino Boards vertragen zwischen 1.8 und 5.5 Volt Spannung. Bei geringerer Spannung läuft der Prozessor nur mit niedrigerer Frequenz stabil. Das Datenblatt meint dazu:
    • 0-4Mhz:1.8V
    • 0-8Mhz:2.7V
    • 0-16Mhz:4.5V

Wenn man den Pro Mini also direkt ohne den Spannungswandler mit Strom versorgt. Das geht über den Pin unten rechts bei dem RAW steht. Dann kann man ihn bis 8Mhz auch mit einer Li-Ion Batterie versorgen, die ja nur 3,7V hat. Für noch extremere Batterie Szenarien siehe Hier oder Hier

  • Es gibt keinen USB Anschluss. Man kann ihn also nur mit zusätzlicher Hardware programmieren. Hier kann man sehen wie das geht.

Beim Programmieren muss man aufpassen dass man in der Arduino Entwicklungsumgebung das richtige Board auswählt. Also auch 8 oder 16Mhz. Ansonsten funktionieren viele Libraries die korrektes Timing brauchen nicht. Manche Libraries sind aber auch generell mit 8Mhz nicht lauffähig. Die sehr gute FastLED Library habe ich nicht zum laufen bekommen. Ich habe dafür aber die light_WS2812 library gefunden.

Fazit

Ein sehr schönes Board das einen kompletten Arduino Uno ersetzt und gerade in Bezug auf Preis und Größe punkten kann. Ich werde euch von meinen Projekten berichten.

 

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)