f0mid
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 (see API) 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 especially problematic for MIDI time code (sync) messages. That said, in many situations this is ok and in my tests with a 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...
//http://fortyseveneffects.github.io/arduino_midi_library/a00041.html
//http://fortyseveneffects.github.io/arduino_midi_library/a00043.html
OSCFunc.trace(true, true);
OSCFunc.trace(false);
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
//realtime
(
var clock= 0xf8; //248
var start= 0xfa; //250
var continue= 0xfb; //251
var stop= 0xfc; //252
Routine.run({
n.sendMsg(\realTime, start);
100.do{
n.sendMsg(\realTime, clock);
0.02.wait;
};
n.sendMsg(\realTime, stop);
1.wait;
n.sendMsg(\realTime, continue);
100.do{
n.sendMsg(\realTime, clock);
0.02.wait;
};
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(\tuneRequest);
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;
s.waitForBoot{
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= EnvGen.ar(Env.adsr(0.01, 1, 0.85, 0.1), gate, amp, doneAction:2);
var bend= In.kr(busBend).lag(0.01);
var cf= In.kr(busCF).lag(0.01);
var rq= In.kr(busRQ).lag(0.01);
var vol= In.kr(busVol).lag(0.01);
var pan= In.kr(busPan).lag(0.01);
var src= BLowPass4.ar(VarSaw.ar((freq+bend).midicps), cf, rq);
OffsetOut.ar(0, Pan2.ar(src*env, pan, vol));
}).add;
d= ();
OSCdef(\f0mid, {|msg|
switch(msg[1],
\activeSensing, {},
\noteOn, {
d.at(msg[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, {
d.at(msg[2]).set(\gate, 0);
d.put(msg[2], nil);
},
\pitchBend, {
busBend.value= msg[2]/8192;
},
\controlChange, {
switch(msg[2],
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);
CmdPeriod.doOnce({
busBend.free;
busCF.free;
busRQ.free;
busVol.free;
busPan.free;
});
};
)
//mtc - receive
(
var a= MIDISMPTEAssembler({|time, format, dropFrame, srcID|
[time, format, dropFrame, srcID].postln;
//time.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);
Routine.run({
var chan= 0, valu= 0;
inf.do{
a.newSeconds(Main.elapsedTime-t);
switch(chan,
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);
switch(a.fps,
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;
});
(1/(a.fps*4)).wait;
};
});
)
Updates:
- 180421: added soft access-point option, as well as OSC commands for setting the receiver IP and port
- 181211: removed soft access-point and simplified WiFi setup with the WiFiManager library
Attachments: | |
---|---|
f0mid_firmware.zip |