Receiving OSC and MIDI (responders)
Everything so far has the client sending — building OSC or MIDI and pushing it to the server. Responders add the other direction: the client receives OSC and MIDI from any application, matches each message, and dispatches it to a callback. That callback can do anything, including emit OSC or MIDI onward — so the client becomes a hub: a MIDI keyboard or a controller app on one side, the Clausters server (or another program) on the other.
The two responder classes mirror SuperCollider's OSCFunc and MIDIFunc:
OscFunc(func, path)firesfuncon incoming OSC messages whose address matchespath.MidiFunc(func, midi_msg)firesfuncon incoming MIDI of a given type ("note_on",["note_on", "note_off"], …).
Both are enabled the moment you create them, and you call free() (or disable()) when done.
Receivers: the transport under a responder
A responder registers with a receiver — the object that owns the listening socket (or MIDI port) and a background thread that decodes and demuxes incoming messages:
OscReceiverbinds a UDP socket and decodes each datagram (bundles unwrapped) through the same single decode door the server uses.MidiReceiveropens a virtual MIDI input port (through theclausters-midicrate'slivefeature) that other apps and devices route into, and parses each message into a dict.
You can pass a receiver explicitly, or let the responder use a lazily-created module default. The default OSC receiver binds an ephemeral port; the default MIDI receiver opens a virtual input port the first time a MidiFunc needs it. They are the one bit of process-wide state in this layer, in the spirit of main.default_clock — explicit receivers are always available when you want full control (a fixed port, a shared clock).
from clausters.base import OscReceiver
from clausters.responders import OscFunc
# A receiver on a fixed port external apps can target.
recv = OscReceiver(port=57121).start()
@OscFunc # not how you'd normally call it -- see the decorator form below
def _(): ...
OSC: matching and the callback
An OscFunc callback is called func(msg, time, src):
msgis the message as a list —[addr, arg1, arg2, …].timeis the bundle's Unix time, orNonefor an immediate or bare message.srcis the(host, port)of the sender.
You can narrow what matches with src=(host, port) (respond only to one sender) and arg_template (match arguments by position — a literal compares equal, a callable is a predicate, None matches anything):
from clausters.base import OscReceiver
from clausters.responders import OscFunc, oscfunc
recv = OscReceiver(port=57121).start()
# Relay an incoming /note <midinote> <dur> to the server as a synth.
def play(msg, time, src):
freq = 440.0 * 2 ** ((msg[1] - 69) / 12)
server.synth("default", {"freq": freq})
OscFunc(play, "/note", recv=recv)
# The decorator form reads the same and returns the OscFunc.
@oscfunc("/ctl", arg_template=[7, None], recv=recv)
def on_cc(msg, time, src):
print("controller 7 ->", msg[2])
OscFunc(...).one_shot() frees the responder after its first match, for a one-time wait.
MIDI: message dicts
A MidiReceiver decodes raw channel-voice bytes into a dict with a type and the type's fields — {"type": "note_on", "channel": 0, "note": 60, "velocity": 100}, {"type": "control_change", "channel": 0, "control": 7, "value": 127}, and so on (pitchwheel carries a single 14-bit pitch). A MidiFunc callback is called func(message, src) with that dict and the port name, and you match on the type, an optional chan, and an arg_template over the fields:
from clausters.base import MidiReceiver
from clausters.responders import MidiFunc
recv = MidiReceiver(port="clausters-in").start()
voices = {}
def note_on(m, src):
if m["velocity"] == 0:
return note_off(m, src)
freq = 440.0 * 2 ** ((m["note"] - 69) / 12)
voices[m["note"]] = server.synth("default", {"freq": freq, "amp": m["velocity"] / 127 * 0.3})
def note_off(m, src):
synth = voices.pop(m["note"], None)
if synth is not None:
server.free(synth)
MidiFunc(note_on, "note_on", recv=recv)
MidiFunc(note_off, "note_off", recv=recv)
That virtual input port is a loose cable until you wire a MIDI source into it (pw-link, qpwgraph, or aconnect). This is the client-side mirror of the server's own direct MIDI input: a Clausters server can be played by MIDI it receives itself, or by a client that listens to MIDI and forwards /s_new. Both coexist.
Threading: the golden rule
A responder's callback runs on the receiver's background thread (or, if the receiver was given a clock, on the clock thread). The golden rule holds: never block that thread. Keep callbacks quick. To sequence in response to an incoming message — play a phrase, not a single note — schedule a routine on a clock, which returns immediately:
from clausters.base import Routine
def start_phrase(msg, time, src):
def phrase():
for degree in (0, 2, 4):
Event(instrument="default", degree=degree).play(server)
yield 0.25
clock.play(Routine(phrase)) # non-blocking; the clock thread runs it
OscFunc(start_phrase, "/go", recv=recv)
Reacting to the shared transport
Setting the server's shared transport (/transport, the beat grid several clients phase-align on — see Timing models) pushes the new grid to every client registered with /notify. A responder on /transport.reply re-aligns this client the moment a conductor changes the tempo or origin, with no polling. Register /notify from the receiver's own socket so the push lands where the responder listens:
recv = OscReceiver(port=57121).start()
recv.send(server.target.addr(), "/notify", 1) # subscribe on this socket
def on_transport(msg, time, src):
origin, tempo, defined = msg[1], msg[2], msg[3]
if defined:
clock.join_transport(server) # adopt the new grid
OscFunc(on_transport, "/transport.reply", recv=recv)
Examples
osc_responder.py— the client as an OSC hub: relay incoming/noteto the server, and re-align on a/transport.replypush. Runs against a live server, self-feeding a few messages to demonstrate.midi_responder.py— aMidiFuncturning a MIDI keyboard into synths on the server.
See Examples.