> Proposal for v0.5 backlog item #3.
Why we need it
Today XOSC has two source kinds:
input— local device (kbd / MIDI / gamepad / serial).resolume-in— Resolume's WebSocket parameter feedback.
1. Stream Deck via Companion sending OSC into XOSC.
2. Reaper / Ableton / OSC-capable DAW sending track/clip state.
3. TouchOSC / TouchDesigner / Lemur custom layouts sending control values.
4. A second XOSC instance on another laptop sending OSC across LAN.
5. Resolume's OSC out (different from its WS) — some users prefer OSC over WS.
Shape
A new node kind:
export interface OscInSpec {
port: number; // local UDP port to bind
address: string; // OSC address pattern. Supports wildcards ( and ?).
// Optional bind interface; default 0.0.0.0.
bindHost?: string;
}
export interface OscInNodeData {
id: string;
kind: "osc-in";
label: string;
color: string;
spec: OscInSpec;
// The OSC argument we treat as the value for continuous routing.
// Default: index 0, only if it's i/f.
valueArgIndex?: number;
transform?: InputTransform;
position: { x: number; y: number };
}
Source-handle on the right; emits onto inputBus when a matching packet arrives.
Main process
A single UDP socket per (bindHost, port) is shared across all osc-in nodes targeting it. The bridge module owns the sockets; it parses every datagram with node-osc's parser, then dispatches to renderer via a new osc:incoming IPC channel:
interface OscIncomingEvent {
bindHost: string;
port: number;
remote: { address: string; port: number };
address: string;
args: { type: string; value: number | string | boolean | null }[];
ts: number;
}
Renderer's useOscInputs.ts (new) subscribes once, looks up matching osc-in nodes by (port, address-pattern-match), and emits on the inputBus with a synthetic descriptor.
Files
- New:
electron/osc-listener.ts(UDP listener, address-pattern matcher). - New IPC handlers in
electron/main.tsregistration:osc-in:open,osc-in:close, plus the broadcast event. - Update
electron/preload.ts+src/types/api.ts. - New:
src/features/inputs/useOscInputs.ts(renderer-side reconciler — opens listeners for any present osc-in node, closes when removed). - New node + inspector:
src/features/mapping/OscInNode.tsx,src/features/inspector/OscInInspector.tsx. src/store/store.ts— addOscInNodeDatato AnyNode; addaddOscInNodeaction; extendisSourceNode.src/components/Topbar.tsx— new "OSC In" button.src/features/mapping/GraphCanvas.tsx— register node type.
Address pattern matching
OSC has its own pattern syntax (, ?, [], {}). Implement a simple matcher; we don't need to support the full grammar in v1, but * and ? cover most users. Library option: osc npm package has a matcher; or we hand-roll one. Hand-rolling is ~30 LoC.
Continuous-value detection
If the matched packet's first i/f argument exists, set value = arg0Float (clamped 0..1; if the arg is outside 0..1, the node's transform.scale is the user's hook to remap). If no numeric arg, treat as discrete pulse (no value on the event).
This mirrors the heuristic the OSC out side already uses — symmetric.
Echo-suppression interplay
Once we have osc-in, the same loop hazard from architecture-limits.md Loop 1 applies to OSC. If a user wires osc-in listening on /x to an OSC out sending to /x on the same target, and that target is ourselves… infinite loop.
The lineage-cap from v0.5 backlog #1 catches it. We can also implement an opt-in echo guard on osc-in (same shape as the resolume-in echo guard).
Air-gap considerations
Opening a UDP listener on 0.0.0.0 exposes XOSC to the LAN. We should:
- Default
bindHostto127.0.0.1, so the listener is loopback-only by default. - Surface the bind host in the inspector with a clear "loopback only" / "LAN visible" indicator.
- Document the LAN-visible mode in the main troubleshooting doc.
Default port
Pick 9000 as the default. 8000 and 7000 are common but reserved for OSC out traffic; XOSC defaults its OSC-out node to 7000. Using 9000 for inbound avoids confusion.
Estimate
M (≤half day). Most of the work is plumbing; the UDP listener is ~50 LoC.