Feature request: OSC as a first-class event and action

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.

Thanks for your thorough feature request. Life is a bit rough right now, but I will take a closer look at this when things settle down.