> Status: superseded by docs/ideas/dmx/ (the full v0.6 plan with two parallel-track handoffs). This file is kept for the original architecture sketch + risk register; the actionable plan lives in the dmx/ folder.
What the user wants
Quoting:
> We did a lot of DMX stuff with another program (saintpetejackboy/TagTable, private repo) — it has nice DMX input that works across all kinds of hardware, with viable defaults and ample configuration. It can also do "patterns" of lights and a "beat pad" interface for triggering different lighting sequences with their own timing/clocks, and the ability to have just one play at a time, or multiple, synchronized to the same clock or not (only one will still play with a priority system, but multiple can be going at once so if one turns off, another can resume, and they can loop and stuff). It also has built-in presets for different kinds of DMX light effects.
>
> Would it be feasible to add DMX nodes to all of this? If so, we could technically be pressing a key on an external device that triggers Resolume + lights, and even it could feed back in so that Resolume could then also trigger DMX.
End goal: "USB button → Resolume clip + DMX cue, and a Resolume parameter → a DMX channel" — same node graph, same UX.
Verdict
Yes, feasible. The hard parts (DMX device I/O, fixture profiles, sequences) are all well-trodden ground. The XOSC architecture is already a router; adding two more node kinds (dmx-out, dmx-in) and a sequence runner is a natural extension.
But:
- It's the largest single feature on the roadmap (probably 10–15 working days in earnest).
- TagTable's "beat pad / sequence / priority" concept is a separate engine, not just a node kind — it deserves its own surface in XOSC's UI rather than living inside a single node's spec.
- The user-experience win is large enough that we should plan it as a v0.6 or v0.7 milestone with its own designed surface, not a shoehorn.
What we can borrow from TagTable (saintpetejackboy/TagTable)
Repo is private and the working agent doesn't have current access to inspect it directly. From the user's description:
- Hardware support across multiple DMX adapters — Enttec Open DMX, Enttec USB Pro / Pro Mk2, ESP-DMX boxes, Art-Net over Ethernet. Implies a backend-abstraction layer rather than calling one driver directly.
- Pattern engine with a beat clock, polyphony controlled by priority (one wins) or simultaneous (mix), per-pattern timing/clock, looping behaviour.
- Built-in fixture presets — RGB, RGBA, RGBAW, dimmer, moving-head, etc. Likely a JSON profile per fixture type (channel layout + names + ranges).
src/ for:
1. The DMX backend abstraction — list every concrete transmitter it implements; we'll pick one or two for v0.6 and stub the rest.
2. The fixture-profile JSON schema. Reuse the schema verbatim if at all reasonable — interop with TagTable presets is a nice user perk.
3. The sequence/pattern engine. We don't need to copy code (Node.js vs whatever TagTable runs), but the semantics — priority queue, exclusive vs additive, beat sync — should match user expectations.
Hard requirements before any code
- USB-DMX hardware ID: which adapters does the user own / target users own? Drives the backend list.
- DMX universes: one or many simultaneously? Drives the data-model for
dmx-out. - Art-Net (network DMX) in or out? Different driver.
- Beat clock source: internal tap-tempo, MIDI clock, OSC
/clock? All three doable, pick by ranking. - Frame rate target: standard DMX is ~44 Hz max; we don't transmit faster than that.
Architecture proposal
Layered
┌───────────────────────────────────────────────────────────┐
│ Renderer │
│ - DmxOutNode / DmxInNode (graph-level) │
│ - SequenceLauncher (a pad-grid surface, separate panel) │
│ - FixtureLibrary (modal, edit profiles) │
│ - Inspector for dmx-out: pick universe + fixture + │
│ channel(s) + scale │
└───────────────────────────────────┬───────────────────────┘
│ IPC
┌───────────────────────────────────┴───────────────────────┐
│ Main process │
│ - DMX engine │
│ • universes: Map<universeId, FrameBuffer512> │
│ • drivers: Map<driverId, Transmitter> │
│ • frame loop: 44 Hz, transmits each universe via its │
│ bound driver │
│ • mixer: writes from {graph nodes, sequence runner, │
│ art-net inbound} merged with priority rules │
│ - Sequence runner │
│ • patterns: timeline of channel writes vs. time │
│ • beat clock, schedulers │
└───────────────────────────────────────────────────────────┘
New node kinds
| Kind | Direction | Spec fields | Notes |
|---|---|---|---|
| dmx-out | sink | universeId, fixtureProfileId or channelMask, valueMode (raw / 0..1), curve | Discrete inputs fire a snapshot of the fixture's "active" pose; continuous inputs map to a chosen channel. |
| dmx-in | source | source kind (art-net / serial-driver), universeId, channel filter (single / range) | Channel-update events flow onto inputBus same as resolume-in. |
Both reuse the applyTransform already in store.ts.
The Sequence Launcher (separate surface, not a node)
The "beat pad" is fundamentally a different surface from the node graph. A dot on the canvas does not represent "play pattern X at beat 3." Two options:
Option A — separate panel. A new top-level view ("Sequences") with a grid of pads. Each pad references a pattern (channel-vs-time data) + playback policy (priority slot, looping, exclusive/additive). Pad triggers come from the inputBus (so a kbd key or MIDI note can fire them), and pad state can drive the inputBus too (so a pattern's beat can fire OSC packets).
This keeps the node graph clean and is closer to TagTable's UX.
Option B — sequence node. A sequence node with internal pattern data and "trigger" / "stop" handles. Multiple sequence nodes participate in a shared priority queue at runtime.
Recommendation: A. The sequence engine has too much state (active pads, beat clock, mix rules) to belong in one node spec. The pads can still appear as graph endpoints — i.e. each pad is reachable by the input bus — but the configuration UI is a dedicated panel.
Priority + exclusivity (the TagTable model)
Each pattern declares:
slot: string— exclusivity group. Two patterns in the same slot can't play at the same time; the higher-priority one wins, the loser pauses or stops.priority: number— within a slot, higher wins.mix: "replace" | "max" | "add"— when multiple patterns from different slots write to the same channel.clock: "internal" | "shared:<id>"— patterns sharing a clock id move in lockstep; "internal" gives each pattern its own.loop: bool,bars: number,bpm: number?— beat math.
The DMX engine's mixer composes the active pattern set into the per-universe frame buffer at 44 Hz. Patterns whose slot is owned by someone higher-priority sit in a paused state and resume if the higher-priority one stops.
Resolume ↔ DMX the user wants
> A key triggers Resolume + lights, and Resolume can also trigger DMX.
With node graph + sequences in place:
[kbd: F13] ──► [resolume-out: trigger /clip]
└──► [sequence-pad: "Cue 1"] (lights)
[resolume-in: /opacity] ──► [dmx-out: dimmer-channel]
That's exactly what the user described. Both directions work because XOSC is already a bidirectional dispatcher.
What's not feasible without serious effort
- DMX over MIDI clock with sub-millisecond jitter requirements. Node.js scheduling at 44 Hz is fine (~22 ms slot), but tight phase-locked control of multiple universes is a real-time problem; not ours.
- Plug-and-play with every DMX adapter on the market. We'll ship 2–3 drivers and users with exotic hardware will have to wait or contribute.
- Beat detection from audio. Tap-tempo and external MIDI/OSC clock are easy. Listening to audio and inferring BPM is its own multi-week project.
Risk register
| Risk | Likelihood | Mitigation |
|---|---|---|
| serialport native module breaks Electron build on user's Windows machine | M | We already use serialport for serial input; same toolchain works. |
| 44 Hz frame loop in the renderer suffers GC pauses on long sessions | L | Keep the loop in main; renderer only sends mutation messages. |
| Fixture profile schema diverges from TagTable | M | Adopt TagTable's schema verbatim where possible; document any extensions. |
| Sequence engine becomes its own product | H | Scope it down ruthlessly: V1 = 8 pads, 1 slot, no mix rules. Iterate. |
| User wants Art-Net out (network DMX) before USB DMX | L | Easy reorder; backend abstraction supports both. |
What I'd ship as "DMX v1" inside XOSC v0.6
The smallest version that's still useful for the user's described event use:
1. One USB DMX driver (Enttec USB Pro Mk2 — most common).
2. One universe.
3. dmx-out node with channel + value (no fixture profiles yet).
4. Continuous-input → channel mapping via the existing transform pipeline.
5. No sequence launcher — just direct graph wiring.
That's XL total — 6–8 days. Adding fixtures + 2nd driver is +M. Adding sequence launcher is the multi-week chunk.
When to start
Don't. Until the user re-prioritises, this is parked. The v0.5 backlog has 6–10 weeks of work that benefits everybody, not just users with USB DMX dongles.
Pre-DMX prep (do as part of v0.5)
When we land items 1–4 of v0.5-backlog.md (lineage + echo guard + osc-in + logic nodes), the routing engine becomes the right shape for DMX without further refactor. Specifically:
- Multi-hop dispatch lets a sequence pad fire a logic node fire a DMX channel fire an OSC packet.
- Lineage cap prevents catastrophic loops the moment DMX inbound is added.
- Edge dispatch modes (sequential / delay) cover "DMX cue, wait 50 ms, OSC packet."