← All docs
Ideas

DMX integration — feasibility study

> 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).
When the work begins, read TagTable's 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."
Doing those properly now means DMX later is mostly node-kind work, not router rewrite.