Event pipeline
This page describes the full data path from a CDJ’s UDP broadcasts to a fired cue action.
Overview
Section titled “Overview”CDJ/XDJ hardware │ Pro DJ Link UDP (port 50000–50002) ▼┌─────────────────────┐│ Rust UDP listener │ Parses Pioneer binary protocol, emits typed packets└────────┬────────────┘ │ Tauri events → frontend ┌───▼─────────────┐ │ ProDjLinkAdapter │ JS adapter receives discovery + telemetry events └───┬─────────────┘ │ ┌──────▼──────────────┐ ┌──────────────────┐ │ DeckRegistry │────►│ Svelte stores │ Reactive UI state │ PlayerTelemetry │ └──────────────────┘ └──────┬──────────────┘ │ DjEvents (track-loaded, track-play, timeline-update, on-air …) ┌───▼───────────┐ │ CueEngine │ Matches events → assignments → cues → actions └───┬───────────┘ │ ┌──────▼──────────────────────────────────────────────┐ │ Output integrations │ │ MidiOutput · TimecodeStreamer · ExternalTargets │ │ InternalActions · MidiClockManager │ └─────────────────────────────────────────────────────┘1. UDP listener (Rust)
Section titled “1. UDP listener (Rust)”The Rust backend opens a raw UDP socket and listens for Pro DJ Link packets on all active network interfaces. It:
- Decodes Pioneer’s binary wire format (CDJ status, keep-alives, beat packets, on-air packets)
- Emits
discoveryevents when a new device is seen or its identity changes - Emits
telemetryevents at the CDJ’s broadcast rate (~8 Hz for status, ~4 Hz for beat) - Maintains a virtual CDJ presence (
vCDJ) so that NXS2+ decks share metadata over the link
The listener is epoch-gated — if the user switches network interface, a new epoch flushes stale state.
2. ProDjLinkAdapter (TypeScript)
Section titled “2. ProDjLinkAdapter (TypeScript)”The JS adapter subscribes to Tauri events and routes them to registered handlers. In offline mode, a MockProDjLinkAdapter replaces the network adapter with events driven by HTML5 audio position.
Packet types emitted:
| Event | When |
|---|---|
discovery | New deck found, or deck identity/on-air changes |
telemetry | Per-frame state: transport, BPM, track ID, waveform, loop markers |
3. Deck registry
Section titled “3. Deck registry”The deck registry maintains the authoritative list of known decks in memory. It:
- Upserts decks from discovery packets
- Marks decks stale if no packet has arrived in 4 000 ms
- Preserves the last-known on-air state through a stale cycle
The registry feeds the djStore Svelte store, which drives all UI.
4. Player telemetry
Section titled “4. Player telemetry”The telemetry reducer processes each telemetry packet and:
- Updates
PlayerStateindjStore(transport, BPM, elapsed/remaining seconds, waveform) - Derives
DjEvents from state transitions:
| State transition | Event emitted |
|---|---|
| No track → track present | track-loaded |
| Stopped/paused → playing | track-play |
| Track present → no track | track-unloaded |
| Off-air → on-air | deck-on-air |
| On-air → off-air | deck-off-air |
| Every telemetry tick while playing | timeline-update |
Events are published on the event bus — a simple typed pub/sub shared across features.
5. Track repository
Section titled “5. Track repository”When a track ID arrives in telemetry, the track repository creates or updates a Track record with title, artist, BPM, and waveform data. Tracks are stored in djStore and persist across sessions.
6. Assignment service
Section titled “6. Assignment service”The assignment service maps (list_id, track_key) → TrackAssignment. When a track-loaded event arrives, the service either returns the existing assignment or creates a new empty one.
7. Cue engine
Section titled “7. Cue engine”The cue engine is the heart of Helm DJ. For every DjEvent it:
- Looks up the assignment for the loaded track
- Filters cues to those matching the event’s trigger type
- For each matching cue, checks:
- Is automation armed? (
session.runtime.automation_armed) - Is panic engaged? (
session.runtime.panic_engaged) - Is this deck’s override on? (
session.runtime.deck_overrides[deck_id]) - Does the cue’s
player_routingfilter pass? - Is
live_onlysatisfied? (if set, deck must be on-air) - Has the debounce window passed?
- For
timelinetriggers: is the playhead within the trigger window?
- Is automation armed? (
- Fires or skips the cue, writing an audit entry either way
Timeline events are rate-limited in logging (one diagnostic log per deck per 2 s) to avoid burying the signal under 30 Hz noise.
Debounce
Section titled “Debounce”Each cue has a debounce_ms field (default: 500 ms). A cue cannot re-fire within this window for the same deck. This prevents double-fires on momentary state bounces (e.g. a CDJ blipping its play state during a loop).
Audit log
Section titled “Audit log”Every cue evaluation writes a TriggerAuditEntry:
outcome: 'fired' | 'skipped' | 'blocked'reason: string (on skip/block)The audit log is shown in the Cue History panel in the UI.
8. Output integrations
Section titled “8. Output integrations”Once the cue engine decides to fire a cue, it dispatches to the matching integration:
| Action type | Integration | Transport |
|---|---|---|
midi | MidiOutput | Rust MIDI via midir |
midi with fade_seconds | MidiOutput | CC value ramp at ~30 Hz |
command | ExternalTargets | Shell subprocess via Tauri |
internal | InternalActions | Direct JS/Rust command |
marker | (logged only) | Audit log + history panel |
MIDI per-deck overrides: before dispatching a MIDI action, the engine resolves any *-flagged fields from per_deck[player_number]. The player number used is the display player number (what the user sees in the UI), not the raw CDJ-reported number.
Timecode & MIDI Clock pipeline
Section titled “Timecode & MIDI Clock pipeline”These run independently of the cue engine:
TimecodeStreamerwatches the selected source deck’s transport state and callsTimecodeOutput(Rust) to start/stop/reposition MTC or LTC streams.MidiClockManagertracks the source deck’s BPM and sends 24 PPQ MIDI Clock ticks (plus Start/Stop iffollow_transportis on).
Both are gated by GLOBAL_PANIC in Rust — when panic is engaged, LTC writes silence and MTC/clock freeze.