wireless midi <-> osc bridge using an esp8266-01. this circuit is extremely cheap to build. schematics, arduino code and examples for supercollider below.

i'm using the great Arduino MIDI Library that allows for both sending and receiving a multitude of midi messages including sysex, system realtime and time code messages. my arduino code just converts all these to/from osc and send or broadcast them over wifi network.

note: sending midi over wifi udp is generally a bad idea. there will be delays, glitches and even lost messages (hanging notes). this is specially problematic for midi time code (sync) messages. that said, in many situations this is ok and in my tests with simple note on/off messages + bend and control, things seem to work just fine.


the circuit takes in 5v and then the regulator steps this down to 3.3v. notice the huge 220uF capacitor that's needed to provide power for the esp8266 during its infamous current draw spikes.


supercollider example code...


OSCFunc.trace(true, true);

n= NetAddr("f0mid.local", 18120);  //ip of esp8266
n.sendMsg(\ip, 192, 168, 1, 99);  //receiver ip (laptop - by default this is x.x.x.255 (broadcast))
n.sendMsg(\port, 57120);  //set receiver port (by default this is 57120)

n.sendMsg(\thru, 0);  //off
n.sendMsg(\thru, 1);  //full (default)
n.sendMsg(\thru, 2);  //same channel
n.sendMsg(\thru, 3);  //different channel

n.sendMsg(\noteOn, 66, 127, 1);  //(note, velo, chan)
n.sendMsg(\noteOff, 66, 0, 1);  //(note, velo, chan)
n.sendMsg(\afterTouchPoly, 50, 60, 3);  //poly pressure (note, press, chan)
n.sendMsg(\controlChange, 1, 64, 3);  //(num, val, chan)
n.sendMsg(\programChange, 10, 4);  //(num, chan)  note the -1 offset
n.sendMsg(\afterTouchChannel, 40, 2);  //(press, chan)
n.sendMsg(\pitchBend, -8000, 1);  //(bend, chan)  -8192 - 8191
n.sendMsg(\sysEx, 240, 14, 5, 0, 5, 247);  //(sysex) - 240 a b c d e ... 247

var clock= 0xf8;  //248
var start= 0xfa;  //250
var continue= 0xfb;  //251
var stop= 0xfc;  //252{
        n.sendMsg(\realTime, start);{
                n.sendMsg(\realTime, clock);
        n.sendMsg(\realTime, stop);
        n.sendMsg(\realTime, continue);{
                n.sendMsg(\realTime, clock);
        n.sendMsg(\realTime, stop);
n.sendMsg(\realTime, 0xfe);  //active sensing
n.sendMsg(\realTime, 0xff);  //system reset

n.sendMsg(\songPosition, 100);
n.sendMsg(\songSelect, 3);

n.sendMsg(\beginNrpn, 10, 3);  //(number, channel)
n.sendMsg(\nrpnDecrement, 40, 3);  //(amount, channel)
n.sendMsg(\nrpnIncrement, 30, 3);  //(amount, channel)
n.sendMsg(\endNrpn, 3);  //(channel)

n.sendMsg(\beginRpn, 10, 4);  //(number, channel)
n.sendMsg(\rpnDecrement, 40, 4);  //(amount, channel)
n.sendMsg(\rpnIncrement, 30, 4);  //(amount, channel)
n.sendMsg(\endRpn, 4);  //(channel)

//--simple midi synth example
s.latency= 0.02;
        var busBend= Bus.control(s);
        var busCF= Bus.control(s);
        var busRQ= Bus.control(s);
        var busVol= Bus.control(s);
        var busPan= Bus.control(s);
        busBend.value= 0;
        busCF.value= 1000;
        busRQ.value= 0.5;
        busVol.value= 0.5;
        busPan.value= 0;
        SynthDef(\note, {|freq= 400, amp= 0.5, gate= 1, busBend, busCF, busRQ, busVol, busPan|
                var env=, 1, 0.85, 0.1), gate, amp, doneAction:2);
                var bend=;
                var cf=;
                var rq=;
                var vol=;
                var pan=;
                var src=, cf, rq);
      ,*env, pan, vol));
        d= ();
        OSCdef(\f0mid, {|msg|
                        \activeSensing, {},
                        \noteOn, {
                      [2]).set(\gate, 0);
                                d.put(msg[2], Synth(\note, [
                                        \freq, msg[2],
                                        \amp, msg[3].lincurve(0, 127, 0, 0.75, 4),
                                        \busBend, busBend,
                                        \busCF, busCF,
                                        \busRQ, busRQ,
                                        \busVol, busVol,
                                        \busPan, busPan
                        \noteOff, {
                      [2]).set(\gate, 0);
                                d.put(msg[2], nil);
                        \pitchBend, {
                                busBend.value= msg[2]/8192;
                        \controlChange, {
                                        1, {
                                                busCF.value= msg[3].linexp(0, 127, 400, 4000);
                                        7, {
                                                busVol.value= msg[3].lincurve(0, 127, 0, 1, 0);
                                        10, {
                                                busPan.value= msg[3].linlin(0, 127, -1, 1);
                                        74, {
                                                busRQ.value= msg[3].linlin(0, 127, 2, 0.1);
                                        {("todo control: "+msg).postln}
                        {("todo command: "+msg).postln}
        }, \f0mid);

//mtc - receive
var a= MIDISMPTEAssembler({|time, format, dropFrame, srcID|
        [time, format, dropFrame, srcID].postln;
OSCdef(\f0mid, {|msg, time, addr|
        var chan, valu;
        if(msg[1]==\mtcQF, {
                chan= msg[2].rightShift(4);  //nibble high
                valu= msg[2].bitAnd(15);  //nibble low
                if(chan==7, {
                        valu= switch(valu,
                                6, {valu= 96},  //30fps
                                4, {valu= 64},  //30fps drop
                                2, {valu= 32},  //25fps
                                0, {valu= 0}     //24fps
                a.value(addr.addr.bitAnd(255), chan, valu);
}, \f0mid);

//mtc - send (kind of works - wslib quark required)
var startSec= 0;
var t= Main.elapsedTime-startSec;
var a= SMPTE(0, 30);{
        var chan= 0, valu= 0;{
                        0, {valu= a.frames.asInteger.bitAnd(15)},
                        1, {valu= a.frames.asInteger.rightShift(4)},
                        2, {valu= a.seconds.asInteger.bitAnd(15)},
                        3, {valu= a.seconds.asInteger.rightShift(4)},
                        4, {valu= a.minutes.asInteger.bitAnd(15)},
                        5, {valu= a.minutes.asInteger.rightShift(4)},
                        6, {valu= a.hours.asInteger.bitAnd(15)},
                        7, {
                                valu= a.hours.asInteger.bitAnd(1).rightShift(4);
                                        30, {valu= valu.bitOr(6)},  //30fps
                                        //30fps drop not supported
                                        25, {valu= valu.bitOr(2)},  //25fps
                                        //24, {valu= valu.bitOr(0)}     //24fps
                n.sendMsg(\mtcQF, chan.leftShift(4)+valu.bitAnd(15));
                chan= chan+1;
                if(chan==8, {
                        chan= 0;

update 180421: added soft access-point option as well as osc commands for setting the receiver ip and port
update 181211: removed soft access-point and simplified wifi setup with the wifimanager library

Package icon f0mid_firmware.zip3.92 KB

rpi audio codec

here's how to set up the proto WM8731 based audio codec module from MikroElektronika and use it with supercollider on a raspberry pi 3.

power off the rpi and connect the proto board to the rpi with jump wires like this...

proto               raspberry
-----                -----
sck           ->    rpi 12
miso         ->    rpi 38
mosi         ->    rpi 40
adcl+dacl  ->    rpi 35  //both proto pins go to the same rpi pin
sda           ->    rpi 3
scl            ->    rpi 5
3.3v         ->    rpi 1
gnd          ->    rpi 6

see pinout diagram for help with the rpi gpio numbering.

power on the rpi and open a terminal and type...

sudo nano /boot/config.txt

find and uncomment the first line and add the second...


press ctrl+o to save and ctrl+x to exit.

sudo reboot

again open a terminal and type...


first press F5 to show all controls, then...
* enable item 'Mic' (space)
* set item 'Mic Boost' to 100 (up arrow key)
* enable item 'Playback Deemphasis' (m key)
* disable item 'ADC High Pass Filter' (m key)
* set item 'Input Mux' to Mic (arrow keys)
* enable item 'Output Mixer HiFi' (m key)


now you should be able to start jackd with for example...

jackd -P75 -dalsa -dhw:0 -r48000 -p256 -n2

and get decent in/out sound at 5.3ms jack latency.

solar powered supercollider

here's how to run supercollider on power coming from the sun...

the main component is a raspberry pi zero with wifi that at startup creates a wireless access point, starts jackd+supercollider and launches a default sound patch.
to play around with the system and change the default sound log on to the access point with a laptop and start livecoding supercollider via the terminal or use the standard scide via vnc. one can for example also set up a couple of osc responders and let friends log on with their phones to control sounds.



the connections are pretty straightforward...

solarpanel -> dcdc converter -> battery -> rpi0 -> soundcard -> amplifier -> speaker(s)

the dcdc converter is taking the higher voltage coming out of the solar panel (~6v) and turns it into a stable 5v. this is then either charging the battery, or directly powering the raspberry pi. note that the amplifier also needs 5v and here i have taken that from pins 4 and 6 on the pi.

the powerbank battery is optional and can be omitted but then the solar panel will have to stay in the sun at all times - else the system will turn off or reboot when the power from the panel drops. the battery acts like a reservoir for when clouds are passing by but not only that - it also lets the system be used for a couple of hours in the evening.

material/modules needed:

* rpi zero w
* 8gb micro sd card
* 5v usb powerbank (best if it can charge and output power at the same time)
* 6v 6watt solar panel ( )
* dc-dc converter ( )
* usb sound adapter
* pam8403 stereo amplifier module
* two full range speakers
* wooden board, double adhesive tape + various cables and screws

download raspbian jessie (here jessie desktop and burn it onto the sd card with etcher.

do the usual setup (change default password, activate ssh), optionally activate vnc and then install supercolliderStandaloneRPI1.

to set up a wifi access point do the following (basically the same as this)...

* sudo apt-get install dnsmasq hostapd
* sudo systemctl stop dnsmasq
* sudo systemctl stop hostapd
* sudo nano /etc/dhcpcd.conf  #and add...
        denyinterfaces wlan0
* sudo nano /etc/network/interfaces  #and make sure wlan0 looks like...
        allow-hotplug wlan0
        iface wlan0 inet static
* sudo service dhcpcd restart
* sudo ifdown wlan0
* sudo ifup wlan0
* sudo nano /etc/dnsmasq.conf  #and add the following...
* sudo nano /etc/hostapd/hostapd.conf  #and add the following...
* sudo nano /etc/default/hostapd  #and change to the following...
* sudo service hostapd start
* sudo service dnsmasq start

last change the file mycode.scd and add this default sound (tweet0340)...

        play{a=SinOscFB;Mix(AllpassN[12,8])+3/24,0,Dseq([0,8,5,1,5,4,5]*round(c*18),inf))+60),c*2)/4)}// #SuperCollider

if it is distorting try lowering the volume in alsamixer.


after many years i finally got around to rebuild one of these boxes.

so this old soviet made device is now a wireless controller that send out osc. there are in total 34 buttons, 16 knobs and an additional rgb status led. it automatically connects via wifi to max or supercollider and run on 5v (usb powerbank).

kicad schematics, arduino firmware, supercollider classes and maxmsp abstractions attached below.


the inside is quite a mess. i use an atmega168 together with six 4051 multiplexers to read all the inputs. the wifi module is an esp8266-01.


update 190124: v1.1 fixed a small but breaking bug in the atmega168 code

Package icon udssrKontroll_1.1.zip116.32 KB

supercollider firmata 3

reading digital inputs from an arduino with the SCFirmata is a little bit more complicated than needed.

here an example that reads 6 analog and 6 digital at the same time.

NOTE: use resistors (10K) to pull up or pull down the digital inputs. (i couldn't figure out how to activate the built in pullups.)

d= SerialPort.devices[0]; // or d= "/dev/tty.usbserial-A1001NeZ" - edit number (or string) to match your arduino
f= FirmataDevice(d);//if it works it should post 'Protocol version: 2.5' after a few seconds

~analog= [0, 1, 2, 3, 4, 5];  //A0-A5
~digital= [2, 3, 4, 5, 6, 12];  //some digital input pins
s.latency= 0.05;
        var freqsArr= 0!~analog.size;
        var ampsArr= 0!~digital.size;
        Ndef(\snd3, {\, 0.05), 0, \}).play;{|x|
                f.reportAnalogPin(x, true);      //start reading analog pins
        f.analogPinAction= {|num, val|
                //[num, val].postln;
                freqsArr.put(~analog.indexOf(num), val);
                Ndef(\snd3).setn(\freqs, freqsArr);
                f.setPinMode(x, \INPUT);
        f.reportDigitalPort(0, true);
        f.reportDigitalPort(1, true);
        f.digitalPortAction= {|port, mask|
                var dig;
                //[port, mask, mask.asBinaryString].postln;
                dig= ~digital.collect{|x| ((mask<<(port*8))&(1<<x)==(1<<x)).binaryValue};
                Ndef(\snd3).set(\amps, dig.postln);

        f.reportAnalogPin(i, false);     //stop reading A0-Anum
f.reportDigitalPort(0, false);
f.reportDigitalPort(1, false);

previous articles...

supercollider firmata 2

+2 years ago i put up a simple example of how to use firmata with arduino and supercollider here. that code still work but it only show how to read a single analog input on the arduino.

here is how one can read both A0 and A1 and map those to synth parameters in supercollider...

//how to read pins A0 and A1 with SCFirmata...
//tested with Arduino1.8.0 and SC3.8.0
//first in Arduino IDE:
//  * select File / Examples / Firmata / StandardFirmata
//  * upload this example to an arduino
//then in SC install the SCFirmata classes
//  * download zip file
//  * extract files and put them in your sc application support directory
//  * recompile sc

d= SerialPort.devices[0]; // or d= "/dev/tty.usbserial-A1001NeZ" - edit number (or string) to match your arduino
f= FirmataDevice(d);//if it works it should post 'Protocol version: 2.5' after a few seconds


Ndef(\snd, {|freq1= 400, freq2= 500, amp= 0.5|[freq1, freq2].lag(0.08), 0, amp.lag(0.08)).tanh}).play;
f.reportAnalogPin(0, true);      //start reading A0
f.reportAnalogPin(1, true);      //start reading A1
f.analogPinAction= {|num, val|
        [num, val].postln;
                0, {
                        Ndef(\snd).set(\freq1, val.linexp(0, 1023, 400, 800)); //A0 mapped to freq1
                1, {
                        Ndef(\snd).set(\freq2, val.linexp(0, 1023, 400, 800)); //A1 mapped to freq2

f.reportAnalogPin(0, false);     //stop reading A0
f.reportAnalogPin(1, false);     //stop reading A1

and to read all six analog inputs (A0-A5) one can do...

d= SerialPort.devices[0]; // or d= "/dev/tty.usbserial-A1001NeZ" - edit number (or string) to match your arduino
f= FirmataDevice(d);//if it works it should post 'Protocol version: 2.5' after a few seconds

~numberOfAna= 6;  //number of analog inputs (here A0-A5)

var freqsArr= 0!~numberOfAna;
Ndef(\snd2, {|amp= 0.5|\, 0.05), 0, amp.lag(0.08)).tanh)}).play;{|i|
        f.reportAnalogPin(i, true);      //start reading A0-Anum
f.analogPinAction= {|num, val|
        [num, val].postln;
        freqsArr.put(num, val);
        Ndef(\snd2).setn(\freqs, freqsArr);

        f.reportAnalogPin(i, false);     //stop reading A0-Anum


this board is using an old raspberry pi 1 to control the speed of computer fans. the electronics are pretty simple (see attached schematics below): it takes 7-36V input power, has twelve mosfets for pwm control and finally a dc/dc converter to power the rpi.
it was built for controlling pc cooling fans but can also drive other types of dc motors, lightbulbs or solenoids.
the off button is there to safely power down the raspberry pi.

the trick with this though is that the system can be livecoded over wifi using supercollider, maxmsp or any other osc capable program. so when you start the board the rpi sets up a wireless access point and starts a python script that accepts incoming opensoundcontrol messages. at startup the rpi1 will also start supercollider and load a file (dragspelFans.scd) that is meant to contain whatever code you'd like to run as default. this file you later overwrite with your own sc code that you've developed/livecoded using your laptop.


below are step-by-step instructions on how i set this up plus the relevant python and supercollider code. it should work on all rpi models but here the rpi1 or rpi0 is assumed.

* download and install raspbian-stretch-lite onto a 2gb sd card
* to enable ssh create an empty file on the sd card. call it ssh. (this terminal command touch /Volumes/boot/ssh will do it on osx or just create an empty textfile and save it without any file extension)
* connect your rpi to your home router via ethernet and type the following in terminal on your laptop:
* ssh-keygen -R raspberrypi.local
* ssh pi@raspberrypi.local #default password is raspberry
* sudo raspi-config #change password to _____, set memory split to 16 under advanced, change hostname to fans under network, update, finish and reboot (sudo reboot)
* ssh pi@fans.local #log in again from your laptop
* sudo apt-get update
* sudo apt-get upgrade
* sudo apt-get dist-upgrade

this section will install osc and gpio libraries for python and also set up the python script to automatically start at system boot.
* sudo apt-get install python-liblo pigpio python-pigpio
* sudo crontab -e #and add the following line at the end (use ctrl+o and ctrl+x to save and exit):

@reboot /usr/bin/pigpiod -s 5 && /usr/bin/python /home/pi/

* nano ~/ #and copy&paste in the following:

#pwm control for 12 fans/motors/leds

#NOTE: make sure to run this in terminal first...
# sudo pigpiod -s 5

import sys
from os import system
from time import sleep
import pigpio
from liblo import *

inport= 9999  #for osc commands to this python script
pinoff= 2  #bcm numbering
pins= [3, 4, 14, 15, 17, 18, 27, 22, 23, 24, 10, 9]  #bcm numbering - more can be added here
target= ('', 57120)  #for osc to sclang
hz= 800  #pwm frequency in hz - note may need to adapt -s option in sudo pigpio -s 5 above
range= 100  #duty cycle range 0 to 100

pi= pigpio.pi()
pi.set_mode(pinoff, pigpio.INPUT)  #no internal pullup needed
for pin in pins:
  pi.set_mode(pin, pigpio.OUTPUT)
  pi.set_PWM_frequency(pin, hz)
  pi.set_PWM_range(pin, range)
  pi.set_PWM_dutycycle(pin, 0)

class MyServer(ServerThread):
        def __init__(self):
                ServerThread.__init__(self, inport)
        @make_method('/pwms', 'i'*len(pins))
        def pwms_callback(self, path, args):
                #print args  #debug
                i= 0
                for pin in pins:
                        pi.set_PWM_dutycycle(pin, min(max(0, args[i]), range))
                        i= i+1
        @make_method('/shutdown', '')
        def shutdown_callback(self, path, args):
                stop('sudo halt -p')  #turn off rpi
        @make_method('/reboot', '')
        def reboot_callback(self, path, args):
                stop('sudo reboot')  #reboot rpi
        @make_method('/start', '')
        def start_callback(self, path, args):
                send(target, '/start', 1)  #start default program in supercollider
        @make_method('/stop', '')
        def stop_callback(self, path, args):
                send(target, '/stop', 0)  #stop default program in supercollider
                for pin in pins:  #and also set all pwm to 0
                        pi.set_PWM_dutycycle(pin, 0)
        @make_method(None, None)
        def fallback(self, path, args):
                print 'received unknown message "%s"' % path

def stop(cmd):
        system('killall pigpiod sclang')

        server= MyServer()
except ServerError, err:
        print str(err)

def main():
        while True:
                        print 'shutting down...'
                        stop('sudo halt -p')

if __name__ == '__main__':
        except KeyboardInterrupt:

again use ctrl+o and ctrl+x to save and exit. now sudo reboot and then try to send osc commands to the rpi. here's how to send some test osc messages from your laptop to the rpi using supercollider...

n= NetAddr("fans.local", 9999);
n.sendMsg(\pwms, *[50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])  //the number of integers should match the number of pins and range in your python code (here 12 pins, 0-100)
n.sendMsg(\pwms, *[25, 50, 75, 0, 0, 0, 0, 0, 0, 0, 0, 0])  //first pin 25%, second %50 third 75%, rest 0
n.sendMsg(\pwms, *[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])  //all off

you can also try to connect pin bcm2 to ground. that should now act like an off button and turn off the rpi in a safe way.

this section is optional. only install supercollider if you want to run your rpi as a standalone installation or something similar. so if you plan to always remote control the system you can skip over this step.
note: this is for rpi0&rpi1, for rpi2&rpi3 change all references to supercolliderStandaloneRPI1 below to supercolliderStandaloneRPI2
see for more details (this page also show how to install jackd if you need audio from your rpi).
* sudo apt-get install libqt5webkit5 libqt5sensors5 libqt5positioning5 libfftw3-bin libcwiid1 git libasound2-dev libsamplerate0-dev libsndfile1-dev libreadline-dev xvfb libjack-jackd2-0
* cd ~
* git clone --depth 1
* mkdir -p ~/.config/SuperCollider
* cp supercolliderStandaloneRPI1/sc_ide_conf_temp.yaml ~/.config/SuperCollider/sc_ide_conf.yaml
* cd supercolliderStandaloneRPI1
* nano #and change the script to look like this:

./sclang -a -l sclang.yaml ../dragspelFans.scd

* nano share/user/startup.scd #and add the following two lines:

OSCFunc({"/home/pi/dragspelFans.scd".load}, \start).permanent= true;
OSCFunc({}, \stop).permanent= true;

* mkdir share/user/Extensions
* nano share/user/Extensions/ #and copy&paste in the following:

//f.olofsson2016-2018 - for controlling 12ch computer fan switch board
DragspelFans {
        var <rpi, num, vals, lastv, <>debug;
        *new {|debug= false, rpi, num= 12|
                ^, rpi, num);
        initDragspelFans {|d, r, n|
                num= n;
                if(r.notNil, {
                        rpi= r;
                }, {
                                rpi= NetAddr("fans.local", 9999);
                        } {|err|
                                "could not connect to rpi.\n make sure you are connected to the wifi network 'dragspel'.".warn;
                                rpi= NetAddr("", 9999);  //temp just for testing
                debug= d;
                vals= 0!num;

        setAll {|val= 100|  //val should be 0 to 100
                vals= val!num;
        clearAll {
                vals= 0!num;
        val {|index, val|  //index should be 0-11, val 0-100
                vals= vals.put(index, val);
        arr {|arr|  //arr should be 12 numbers in an array
                vals= arr;

        shutdown {
        reboot {
        start {
        stop {

        prSend {|v|
                if(debug, {
                v= vals.clip(0, 100).round.asInteger;
                if(v!=lastv, {  //filter out repeats
                        lastv= v;
                        rpi.sendMsg(\pwms, *v);  //send to

* nano ~/dragspelFans.scd #and copy&paste in the following:

//demo autostart script - put your own standalone code in here
Event.addEventType(\fans, {d.val(~index, ~val)});
Pbind(\type, \fans, \dur, 0.5, \index, Pseq([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], inf), \val, Pwhite(0, 100, inf)).play;

* crontab -e #and add the following to the end (note no sudo here this time)

@reboot cd /home/pi/supercolliderStandaloneRPI1 && xvfb-run ./

now sudo reboot and supercollider should automatically start the code in dragspelFans.scd. it'll take a while so give it a minute or two.

to test it more run the following supercollider code on your laptop...

n= NetAddr("fans.local", 9999);
n.sendMsg(\stop);  //first stop the dragspelFans.scd script
n.sendMsg(\pwms, *[25, 50, 75, 0, 0, 0, 0, 0, 0, 0, 0, 0]);  //set pwm manually

//install the class on your laptop sc and also try the following example code

a= DragspelFans(true);  //might take a moment or two
CmdPeriod.doOnce({a.clearAll});  //optional

//version0 - all on or off
a.setAll(50)  //set all to some value 0-100

//version1 - using an array
a.arr([0, 0, 100, 0, 0, 100, 0, 0, 100, 0, 0, 100])  //turn on some
a.arr([0, 100, 0, 0, 100, 0, 0, 100, 0, 0, 100, 0])  //turn some other fans
a.arr([30, 0, 0, 40, 100, 0, 40, 0, 0, 80, 0, 0])  //a few slower

//version2 - set index to value
a.val(9, 100);
a.val(9, 0);
a.val(11, 100);
a.val(11, 0);
a.val(11, 60);
a.val(11, 0);

//fade in each fan in order
                        a.val(j, i);
                        a.val(j, 99-i);

//using patterns
Event.addEventType(\fans, {a.val(~index, ~val)});
Pdef(\test, Pbind(\type, \fans, \dur, 0.125, \index, Pseq([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].scramble, inf), \val, Pwhite(0, 100, inf))).play;

a.start;  //start the dragspelFans.scd script on the rpi again

or test it using the attached maxmsp patch.


//--wifi softap
this section is optional. it will set up a wifi access point served from the rpi. basically taken from
* sudo apt-get install dnsmasq hostapd
* sudo systemctl stop dnsmasq
* sudo systemctl stop hostapd
* sudo nano /etc/dhcpcd.conf #and add the following to the end:

interface wlan0
    static ip_address=

* sudo service dhcpcd restart
* sudo nano /etc/dnsmasq.conf #and add the following two lines to the bottom:


* sudo nano /etc/hostapd/hostapd.conf #and add the following (remember to set passphrase 8-64 characters):


* sudo nano /etc/default/hostapd #and change one line to the following:


* sudo systemctl start hostapd
* sudo systemctl start dnsmasq

now sudo reboot on the rpi, log on to dragspel wifi network from your laptop and try to send some osc commands.

update 180225: major rewrite to use pigpio instead of RPi.GPIO and also raspbian stretch instead of jessie. pwm works much better.


next week in Bucharest we'll be setting up the subjective frequency transducer for the third time. i described the sound/vibration generating part of this system before but didn't write much about how the controllers work.

so for each sound channel (i.e. each bass transducer) there's a wireless controller that enables the audience to set their preferred frequency. technically it's done with a rotary encoder, a esp8266 wifi module, a mega168 and a big 7-segment lcd. the circuit runs off two AAA batteries.

when someone touches the rotary encoder, the circuit wakes up and starts sending osc messages to a laptop running supercollider. supercollider receives the values, starts playing an oscillator and sends the sound to the corresponding audio channel. when done, sc fades out the oscillator and sends an off message to the circuit and the controller goes back to sleep mode.

i spent quite some time optimising the microcontroller (atmega168) code. it was hard to both reduce power consumption and still being able to quickly wake up and react on user input as well as on incoming osc messages. it's a common problem with battery powered radio devices.

also getting the esp8266 to handle osc messages was a pain. here and here are some more info and simplified versions of that.

in the end, the code for talking to these circuits in supercollider looked like this:

//sc example: sending. turn off circuit 3 and set it back to initial frequency
~encode= {|id, on, hi, lo| (id&255<<24)|(on&255<<16)|(hi&255<<8)|(lo&255)};
~encode.value(3, 0, 0, ~initFreq);
//sc example: receiving. decoding data from the four esp8266
OSCdef(\sti, {|msg, time, addr|
        var id= msg[1]>>24;
        var onoff= (msg[1]>>16)&255;
        var freq= (msg[1]&65280)+(msg[1]&255);
        [\id, id, \onoff, onoff, \freq, freq].post;
}, \sti);

the microcontroller code could still be improved. i'd like it to wake up on both wdt and uart. at the moment the circuit is drawing 22mA average in off+idle state, and 33mA average with display set to '20' which is okey but not optimal. and when sending osc you get current spikes of a few hundred milliamps but there's no way around that.

//f.olofsson 2015-2016

#define ID 3
#define FREQ 0 //start frequency
#define FREQ_MIN 0
#define FREQ_MAX 9999
#define WLAN_ADDR "" //laptop static ip
#define WLAN_PORT 1112
String tag = "/tap"; //incomming osc addy

#include <avr/sleep.h>
#include <avr/power.h>
#include <avr/wdt.h>

#include <Encoder.h>

Encoder myEnc(3, 2);
float freq = FREQ;  //starting frequency
int freqLast = -999;

byte state = 0;
int enc = 0;
byte dig = 0;
byte cnt = 0;
boolean resp;

uint8_t buf[16];  //osc message

void setup() {
  pinMode(2, INPUT_PULLUP);  //encoder a
  pinMode(3, INPUT_PULLUP);  //encoder b
  pinMode(4, INPUT_PULLUP);  //encoder button
  DDRB = B11111111;  //segments
  DDRC = B00001111;  //digits selector

  //--set up wifi
  resp = Serial.find("ready\r\n");
  resp = Serial.find("OK\r\n");
  do {
    resp = Serial.find("OK\r\n");
  } while (!resp);
  resp = Serial.find("OK\r\n");
  Serial.print("\",57120,");  //supercollider default port
  resp = Serial.find("OK\r\n");

  //--osc message
  buf[0] = 47;   // /
  buf[1] = 115;  // s
  buf[2] = 116;  // t
  buf[3] = 105;  // i
  buf[4] = 0;
  buf[5] = 0;
  buf[6] = 0;
  buf[7] = 0;
  buf[8] = 44;   // ,
  buf[9] = 105;  // i
  buf[10] = 0;
  buf[11] = 0;
  buf[12] = ID;  // a high   (id)
  buf[13] = state; // a low  (onoff)
  buf[14] = 0;   // b high   (freq hi)
  buf[15] = 0;   // b low    (freq lo)

  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1 = 0;
  OCR1A = 32768;  //62.5Hz display updaterate
  TCCR1B |= (1 << WGM12);
  TCCR1B |= (1 << CS10);  //prescaler divide by 1
  TIMSK1 |= (1 << OCIE1A);

  MCUSR &= ~(1 << WDRF);
  WDTCSR |= (1 << WDCE) | (1 << WDE);
  WDTCSR = 1 << WDP0 | 1 << WDP1;

volatile int f_wdt = 1; //watchdog wakeup
ISR(WDT_vect) {
  if (f_wdt == 0) {
    f_wdt = 1;
void enterSleep(void) {

ISR(TIMER1_COMPA_vect) {  //update display periodically
  if (state == 2) {

void sendOsc() {
  buf[13] = state;
  buf[14] = int(freq) >> 8;
  buf[15] = int(freq) & 255;
  Serial.write(buf, sizeof(buf));
  resp = Serial.find("OK\r\n");

void loop() {
  dig = 1 - ((PIND >> 4) & 1);  //encoder momentary button
  switch (state) {
    case 2:  //running (display on)
      enc =;
      if (enc != 0) {
        float incStep = enc / 2.0;
        freq = max(FREQ_MIN, min(FREQ_MAX, freq + incStep));
        if (int(freq) != freqLast) {
          freqLast = int(freq);
      if (dig == 1) {  //TODO: or timeout here?
        state = 3;
    case 0:  //sleeping (display off)
      f_wdt = 0;
      enc =;
      if ((dig == 1) || (enc != 0)) {
        state = 1;
        freq = FREQ; //reset
    case 3:  //turning off when button released
      if (dig == 0) {
        state = 0;
    case 1:  //turning on when button released
      if ((dig == 0) || (enc != 0)) {
        state = 2;

  //--receive osc
  while (Serial.available()) {
    String abc = Serial.readStringUntil('\n');
    if (abc.startsWith("+IPD,4,16:" + tag)) {
      //if(abc[22]==ID) { //optional filter by device ID
      if (abc[23] == 0) {
        state = 0;
      } else {
        state = 2;
      freq = (abc[24] << 8) + abc[25];
void displayClear() {
  PORTC = B00001111;
  PORTB = B00000000;
void progressDot(byte index) {
  setChr(255, true);
void displayFreq() {
  int val = freq; //cuts off fraction
  switch (cnt) {
    case 0:
      if (val > 999) {
        setChr((val % 10000) / 1000, false);
      } else {
        setChr(255, false);
      cnt = 1;
    case 1:
      if (val > 99) {
        setChr((val % 1000) / 100, false);
      } else {
        setChr(255, false);
      cnt = 2;
    case 2:
      if (val > 9) {
        setChr((val % 100) / 10, false);
      } else {
        setChr(255, false);
      cnt = 3;
    case 3:
      setChr(val % 10, false);
      cnt = 0;

void selDig(byte index) {
  switch (index) {
    case 1:
      PORTC = B00001110;
    case 2:
      PORTC = B00001101;
    case 3:
      PORTC = B00001011;
    case 4:
      PORTC = B00000111;

void setChr(byte chr, bool dot) {
  switch (chr) {
    case 255:  //clear
      PORTB = B00000000;
    case 0:
      PORTB = B11111100;
    case 1:
      PORTB = B01100000;
    case 2:
      PORTB = B11011010;
    case 3:
      PORTB = B11110010;
    case 4:
      PORTB = B01100110;
    case 5:
      PORTB = B10110110;
    case 6:
      PORTB = B10111110;
    case 7:
      PORTB = B11100000;
    case 8:
      PORTB = B11111110;
    case 9:
      PORTB = B11100110;
        case 10:  //A
        case 11:  //B
        case 12:  //C
        case 13:  //D
        case 14:  //E
        case 15:  //F
        case 16:  //G
        case 17:  //H

  if (dot) {
    PORTB |= B00000001;


Subscribe to RSS - supercollider