Hi,
I drive a live keyboard rig from a Stream Deck XL and a Stream Deck+ through this plugin: three keyboard zones feeding Ableton, breath controller, a pedalboard, and a fallback path for when the DAW dies mid-song. About 140 scripts. I have also published a small Stream Deck+ layout built on the plugin — streamdeck-midi-note-display — so the scripting language is something I use daily rather than occasionally.
This is a request for OSC support inside the plugin, and an attempt to show that it would fit the existing language almost without adding anything to it.
Why I am asking: the path I have to take today
To tell Ableton anything, my deck currently goes:
Stream Deck → MIDI → a Python bridge → OSC → AbletonOSC → Live
└─ exists only to change transport
Bome MIDI Translator also sits in that rig, but it earns its place: it splits the keyboard into zones, which is real work. The bridge is different — I wrote it (oscmidi-bridge, MIT), it works well, and it does nothing but carry the same intent across a transport boundary. It is a process that has to be launched, watched and kept alive before a gig for no musical reason at all.
The concrete thing that hurts: a CC carries 0-127
This is the case that pushed me to write this post, because no amount of configuration solves it.
In Ableton, the instrument a keyboard zone plays is chosen by routing a track to a chain, and chains are addressed by their index in a flat list. That list counts a container as one slot and a leaf as sixteen, so the numbering climbs fast and irregularly. My 28 destinations land here:
Organ 3 Winds 116 <- the last one a CC can reach
Piano 19 Piccolo 264
Rhodes 35 Strings 313
… Violin 461
Vibanet 99 Brass 639
Sax 803
Tromb 819
Twenty of the twenty-eight are past 127. The eight that fit are the keyboards; every wind, string, synth and brass sound in the rig is out of reach of a Control Change. And no encoding trick recovers them, because the spacing is irregular — no multiplier, no offset, it depends on how each rack happens to be nested.
What I had to build instead: the deck sends a CC carrying a position in the list (0-27), and the bridge holds a lookup table turning it into the real index. The table is generated from the Live set so it cannot drift, which is one more moving part to keep right. All of that to work around seven bits.
In OSC, the same thing is one message with an integer argument. No table, no generator, nothing to keep in sync.
The proposal: two additions, no new grammar
What makes this worth asking is that the language already has exactly the right shape. It has events, actions, and typed literals inside expressions. OSC needs one new family in each of the first two, and nothing at all in the third.
1 — An outgoing action
Today an action names a transport and its parameters:
{cc:16,52,#@Idx#} send CC 52 on channel 16
{pb:2,8192} pitch bend
{@P1Cur:#@z1v#} set a variable
The OSC one would read the same way — an address, then arguments:
{osc:/live/song/start_playing}
{osc:/live/song/set/tempo,120.0}
{osc:/live/track/set/output_routing_channel,0,#@Idx#}
Argument typing is the only genuinely new question, and the language already answers it. Numbers and strings are distinguished in expressions today (#"Z1 : "&@l_s1#), so the same convention carries over:
12 integer
12.0 float the decimal point forces it
"Main" string
That distinction is not cosmetic for OSC: AbletonOSC refuses a float where it wants a track index, and refuses an integer where it wants a tempo. I hit both. The rule above is what my bridge uses, and it has been enough for every address I have needed.
2 — An incoming event
Today an event is a pattern that binds values into @e_ variables:
[(cc:1,21,*){@P1Cur:#@e_ccvalue#}]
[(noteon:2,*,1-127){…}]
[(@P1Route:0-127){…}] a variable changing is already an event
The OSC one would match an address and bind its arguments:
[(osc:/live/song/get/tempo,*){@Bpm:#@e_oscarg1#}]
[(osc:/live/view/get/selected_scene,*){@Scene:#@e_oscarg1#}]
@e_oscarg1, @e_oscarg2 … alongside the existing @e_ccvalue, @e_note, @e_tics. Same idea, same naming, nothing to learn.
3 — One line of configuration
A target host and port, declared once, the way MIDI ports already are:
{osc.target:127.0.0.1:11000}
{osc.listen:11001}
4 — And the same thing in JavaScript
Scripts can also be written in JavaScript, where the natural shape is a method rather than an action. Mirroring io.launch(), something like:
io.osc.send("/live/track/set/output_routing_channel", 0, idx); // idx from a variable
io.osc.on("/live/song/get/tempo", (bpm) => { … });
The value of the JS side specifically is that the address becomes computable. In my case the three keyboard zones differ only by a track number, so one expression covers all three instead of three near-identical scripts. (@jordikt made a closely related argument for io.run() recently, and the pattern of “give the script one primitive and let it compose” seems to be where the language is already heading.)
Why this is small, and why it is not just for Ableton
No new grammar. [(event){action}] is untouched. OSC is one more event family and one more action family inside a structure the plugin already has.
No new concepts for the user. Anyone who can write {cc:16,52,…} can write {osc:/address,…}. The learning curve is an address instead of a channel and a number.
It carries what MIDI structurally cannot. Integers beyond 127, floats, and — the one I would value most — strings. Today, to show a track name on a key, the plugin has to read it from a file that something else wrote. An OSC reply carries it directly.
And it is not an Ableton feature. OSC is spoken by Reaper, QLab, Resolume, TouchDesigner, Open Stage Control, Behringer’s X-Air and M32 desks, and most lighting software. A single addition opens all of them, which is a different proposition from adding support for one DAW.
What I can offer
I have been living in this problem for a while and would be glad to help rather than just ask:
- oscmidi-bridge — the bridge I use, MIT. Its configuration language solved the same design questions (argument typing, address matching, feedback throttling, what to do when a value exceeds 7 bits), so it may be useful as prior art even if none of the code is.
- bome-project-tools — tooling and format notes for the other half of my rig.
- I am happy to test a beta against a real four-hop setup and report precisely, including the unglamorous parts: reconnection after the target restarts, what happens when nothing is listening, and message rate under a VU-meter feed. Those are the three that cost me the most time in the bridge.
Thanks for a plugin solid enough that my whole rig depends on it — the request only exists because everything else already works.