Super Timecode Converter v1.6
Denon StageLinQ Integration
v1.6.0 adds native support for Denon Engine OS hardware via the StageLinQ protocol. STC now connects to Denon DJ equipment (SC5000, SC6000, Prime 4/2/Go, X1800, X1850) alongside Pioneer CDJ/DJM hardware, with the same timecode generation, track mapping, mixer forwarding, and show control capabilities.
Important: The Denon StageLinQ implementation is preliminary. It has been built entirely from open-source protocol references (chrisle/StageLinq, icedream/go-stagelinq, Jaxc/PyStageLinQ) and verified byte-by-byte against their source code, but has not yet been tested with real Denon hardware. The protocol frames, service handshake, StateMap subscriptions, BeatInfo parsing, and FileTransfer database download all match the reference implementations exactly, but real-world device behavior may differ.
If you have access to Denon Prime hardware and would like to help test, please try it and report your results on the GitHub issues page. Debug builds include logging that will help diagnose any issues. Bug reports with Wireshark captures are especially valuable.
Protocol implementation (StageLinQInput.h, 2401 lines)
Complete StageLinQ protocol in C++17/JUCE, based on three MIT-licensed open-source references: chrisle/StageLinq (TypeScript), icedream/go-stagelinq (Go), and Jaxc/PyStageLinQ (Python).
- Discovery: UDP broadcast on port 51337 with "airD" magic, per-interface broadcast for Windows compatibility, periodic re-announcement (1s), exit notification on shutdown
- Service negotiation: TCP connect to discovered devices, service request/response for StateMap, BeatInfo and FileTransfer ports, reference keepalives (250ms)
- StateMap: 45/45 deck paths (100% coverage of all 3 references) + 26/26 global paths + 10 mixer paths, "smaa" framed TCP with UTF-16BE paths and JSON values
- BeatInfo: Real-time binary stream with per-deck beat position, total beats, BPM, and timeline
- Multi-device: Automatic deck offset from /Client/Preferences/Player -- SC6000 Player 2 maps to STC Decks 3-4
StateMap deck paths (45 per deck):
Play, PlayState, PlayStatePath, CurrentBPM, Speed, SpeedState, SpeedNeutral, SpeedRange, SpeedOffsetUp, SpeedOffsetDown, SyncMode, ExternalMixerVolume, ExternalScratchWheelTouch, Pads/View, Track/ArtistName, Track/SongName, Track/TrackName, Track/TrackLength, Track/SongLoaded, Track/SongAnalyzed, Track/CurrentBPM, Track/CurrentKeyIndex, Track/KeyLock, Track/CuePosition, Track/SampleRate, Track/TrackNetworkPath, Track/TrackUri, Track/TrackData, Track/TrackBytes, Track/TrackWasPlayed, Track/Bleep, Track/SoundSwitchGuid, Track/PlayPauseLEDState, Track/CurrentLoopInPosition, Track/CurrentLoopOutPosition, Track/CurrentLoopSizeInBeats, Track/LoopEnableState, Track/Loop/QuickLoop1-8, DeckIsMaster
StateMap global paths (26):
DeckCount, Master/MasterTempo, Sync/Network/MasterStatus, LayerA, LayerB, Player, PlayerJogColorA/B, SyncMode, PlayerColor1-4 with A/B variants (12), CurrentDevice, HasSDCardConnected, HasUsbDeviceConnected, ActiveDeck, ViewLayerB
StateMap mixer paths (10):
CH1-4 fader positions, CrossfaderPosition, ChannelAssignment1-4, NumberOfChannels
Database client (StageLinQDbClient.h, 1373 lines)
FileTransfer protocol client + SQLite database reader for Denon Engine Library metadata, artwork, waveform, and performance data.
- FileTransfer protocol: "fltx" framed TCP, source enumeration, file stat, chunked download with transfer IDs
- Database download: Database2/m.db (v2) with fallback to m.db (v1), matching chrisle/StageLinq behavior
- SQLite amalgamation: sqlite3.h + sqlite3.c (v3.47.2, public domain) compiled directly into the project
- Track query: By path (local tracks) or by URI (streaming tracks from Beatsource/Tidal/SoundCloud), matching chrisle DbConnection.ts
- Schema-aware: v2 albumArtId vs v1 idAlbumArt column names, automatic fallback
- NULL-safe: All sqlite3_column_text calls wrapped in null check (Engine DJ databases frequently have NULL metadata fields)
- Network path parsing: net://uuid/source/Engine Library/Music/path format, identical algorithm to chrisle/StageLinq trackPath.ts
BLOB decoders (from libdjinterop format documentation):
| BLOB | Compression | Content |
|---|---|---|
| overviewWaveFormData | zlib | 3-band waveform (low/mid/high), reordered to Pioneer display format at decode time |
| quickCues | zlib | Cue points with label, sample offset, ARGB color + main cue |
| loops | raw (not compressed) | Saved loops with label, start/end sample offsets, ARGB color |
| beatData | zlib | Beat grid markers with sample offset, beat number |
| AlbumArt.albumArt | raw | JPEG/PNG image data |
| Track.key | integer 0-23 or text | Musical key (converted via musicalKeyToString) |
StageLinQ View (StageLinQView.h, 961 lines)
External 30Hz deck view window with full Denon visualization.
Per-deck panel (4 decks):
- Artwork from Engine Library database
- 3-band waveform (overview) with playhead cursor
- Cue markers and saved loop regions (from database BLOBs)
- Live active loop overlay (from StateMap, green, real-time)
- Track info: artist, title, musical key (live from StateMap, updates with key shift)
- Key lock badge [KL]
- BPM with effective multiplier
- Pitch percentage
- Beat-in-bar indicator (4 dots)
- Status badges: LOOP (with beat size), REV (bleep/reverse), JOG (jog wheel touch)
- SMPTE timecode display
- TrackMap offset timecode
- Channel fader bar
- Engine assignment and TrackMap status
Mixer section:
- Crossfader visualization with A/B labels and thumb indicator
Layout:
- Toggle between 4x1 (horizontal row, for docking below Resolume etc.) and 2x2 grid
- Window bounds and layout state persisted in AppSettings
Mixer forward (OSC / MIDI / ArtNet)
StageLinQ mixer data is forwarded to external systems via the same OSC/MIDI/ArtNet buttons as Pioneer.
- MixerMap Denon mode: Separate editable mixer map (slq_mixermap.json) with CH1-4 faders + crossfader
- OSC: Configurable addresses (default /mixer/ch1..4, /mixer/crossfader), 0.0-1.0 float
- MIDI CC: Configurable CC numbers (default CC 1-5), 0-127
- MIDI Note: Configurable note numbers for grandMA2/MA3 executor faders
- ArtNet DMX: Configurable DMX channels (default 1-5), 0-255
- Dedup: Values only sent on change, DMX re-send at 100ms for node timeout prevention
- Editor: MixerMap editor opens Pioneer or Denon map based on active input, DJM model filter hidden for Denon
Inline display (MainComponent)
When StageLinQ is active, the main display shows:
- Track info (artist -- title) in Denon green accent
- BPM with multiplier, pitch percentage, device model
- Artwork from Engine Library database
- 3-band waveform with playhead cursor (from database)
- Mixer fader bars (Unicode block characters, same style as Pioneer DJM display)
- Player combo: DECK 1-4 (no PLAYER 5-6 or XF-A/XF-B which are Pioneer-specific)
- MixerMap button in Denon green (switches to Pioneer blue when ProDJLink is active)
TimecodeEngine integration
- InputSource::StageLinQ with complete tick loop: PLL, interpolation, track change detection, TrackMap lookup, MIDI Clock, Ableton Link, OSC BPM forward, mixer forward
- forwardStageLinQMixer() reads from Denon MixerMap entries
- DMX buffer reset on input switch (prevents stale Pioneer data leaking into StageLinQ session)
- Dedup arrays for both Pioneer (lastSentMixer[128]) and StageLinQ (lastSentSlqMixer[5])
Other changes
- Tab centering: Engine tabs centered relative to full window width
- Unknown path logger: Wrapped in #if JUCE_DEBUG for zero overhead in Release builds
- MixerMap dual-mode: MixerMapMode::Pioneer / MixerMapMode::Denon with separate defaults and persistence files
- Restore path fix: Engine restore loop now wires all shared pointers (StageLinQ, DbServer, SlqMixerMap were missing)
Network robustness fixes (pre-hardware audit)
Eleven fixes to StageLinQ network handling, identified by cross-referencing chrisle/StageLinq v3 changelog, go-stagelinq, PyStageLinQ protocol docs, and MarByteBeep discussion threads against our implementation. All are pure logic improvements that do not require hardware to verify. Fixes 6-7 add clean protocol shutdown and protocol constraint documentation. Fixes 8-9 address connection lifecycle and debugging instrumentation. Fixes 10-11 correct subscription ordering and error handling.
-
Discovery EXIT stops active connections (StageLinQInput.h): When a Denon device broadcasts
DISCOVERER_EXIT_, we now stop the activeDeviceConnectionThreadfor that IP before erasing the device entry. Previously, the thread would keep running on dead sockets until I/O error, and if the device re-announced quickly, two threads could run for the same IP producing duplicate data. -
Reconnect kills stale threads (StageLinQInput.h): Before launching a new
DeviceConnectionThreadinmanageConnections(), we now scan for and signal any existing thread for the same IP to exit. Prevents duplicate threads when a device does rapid EXIT + re-announce faster than thread teardown. -
StateMap/BeatInfo buffer resync (StageLinQInput.h): StateMap resync now scans forward for
smaamagic bytes instead of blind 4-byte skip (which could burn CPU on a corrupted stream). BeatInfo resync discards the entire buffer instead of byte-skipping (beat data is real-time, frame loss is harmless, and BeatInfo has no magic bytes for re-alignment). -
FileTransfer Disconnect handling (StageLinQDbClient.h): The FileTransfer disconnect response (message ID 0x9) now closes the FT socket and resets the pointer, per chrisle changelog: "Handle Shutdown msg (0x9) from FileTransfer svc". Previously the message was silently ignored, leaving a dead socket that subsequent reads would fail on.
-
Proactive device liveness detection (StageLinQInput.h):
drainMainSocket()now minimally parses Reference messages (instead of discarding raw bytes) and trackslastDeviceRefTime. If no data arrives forkDeviceTimeoutSec(5s), the connection thread exits proactively instead of waiting for the next TCP write to fail. AddedgetDeviceIp()accessor andstopConnectionThreadsForIp()helper for fixes 1-2. -
BeatInfo clean shutdown (StageLinQInput.h): Added
buildBeatInfoStop()and sending the stop frame before closingbeatSocket. Previously the socket was closed directly (TCP RST), which is "rude" -- the device only learns we left when the TCP connection dies. Now we send the proper stop sequence first, matching the protocol's start/stop symmetry (confirmed from MarByteBeep/honusz discussion). -
Protocol documentation (StageLinQInput.h): Added comments documenting: (a) the token MSB constraint from PyStageLinQ (if bit 7 of byte[0] is set, device silently ignores service requests -- our known-good token is safe); (b) link-local 169.254.x.x behavior for direct-cable setups (Prime Go defaults to link-local when no DHCP is available, PC needs manual IP in same range).
-
Dead service socket detection (StageLinQInput.h): If StateMap or BeatInfo TCP socket dies during Phase 4 (e.g., network glitch), the connection thread now exits the read loop and calls
markDisconnected(), allowingmanageConnections()to relaunch. Previously, the thread became a zombie: it kept sending keepalives on the main socket (keepingdev.connected = true) while no deck data flowed, permanently blocking reconnection. Both chrisle and go-stagelinq tear down the entire connection when any service socket fails. -
Main socket drain desync prevention + debug logging (StageLinQInput.h):
drainMainSocket()now clears the entire buffer on unknown message IDs instead of 4-byte skip. Prevents desync when the device sends a ServiceAnnounce (ID=0x0, variable-length frame) during Phase 4 -- this can happen if the user inserts a USB stick and the device re-announces FileTransfer. Added#if JUCE_DEBUGlogging in all three service parsers: unknown smaa subtypes in StateMap, non-smaa blocks on the StateMap connection, and unknown BeatInfo message types. These logs will be critical during the first hardware testing session to discover any protocol messages not covered by the open-source references. -
Player preference subscribed first (StageLinQInput.h):
/Client/Preferences/Playeris now subscribed before any deck paths. On SC6000 player 2, this path setsdeckOffset=2which maps device-local Deck1/Deck2 to STC decks 3-4. Previously, all 180 deck path subscriptions were sent first, and the device would reply with state values that were processed withdeckOffset=0(wrong deck indices) until the Player preference arrived at the end. Now the offset is set before any deck data flows. -
Incomplete database download returns empty (StageLinQDbClient.h): If the FileTransfer download is incomplete (network timeout, device disconnect),
downloadFile()now returns an empty vector instead of the truncated data. A partial SQLite file is always corrupt and will fail to open. The previous code returned the partial data with a hopeful comment that was incorrect.
Additionally: DeviceConnectionThread creation changed from raw new to std::make_unique for exception safety (if vector::push_back throws during reallocation, the thread object is still owned and destroyed). Removed dead && i >= 4 condition in StateMap resync (loop starts at i = 4 so the check was always true).
Cross-platform cleanup (all source files)
Replaced all non-ASCII characters in comments across 7 pre-existing source files (73 lines total). No BOM present in any file -- MSVC without /utf-8 interprets these as system-codepage bytes, which can trigger C4819 warnings or compilation failures on non-Western locales.
| Character | Replacement | Files affected |
|---|---|---|
| -> (arrow) | -> | ProDJLinkInput.h, TimecodeCore.h |
| -- (em dash) | -- | ProDJLinkInput.h, TimecodeEngine.h |
| [OK]/[FAIL] (emoji) | [OK]/[FAIL] | ProDJLinkInput.h |
| x (multiply) | x | ProDJLinkInput.h, MtcInput.h |
| +/- (plus-minus) | +/- | TimecodeCore.h, MtcInput.h |
| <-> (bidi arrow) | <-> | TimecodeCore.h |
| Sec. (section) | Sec. | ArtnetOutput.h |
| ~ (approx) | ~ | LtcInput.h |
| 1/4 (fraction) | 1/4 | MtcInput.h |
| (dot) (bullet) | (dot) | CustomLookAndFeel.h |
All 32 source files now contain only printable ASCII (0x00-0x7F).
Timecode engine parity fixes (StageLinQ vs ProDJLink)
Two fixes identified by comparing how TimecodeEngine consumes StageLinQ data vs the battle-tested ProDJLink path.
-
LTC pitch deceleration (TimecodeEngine.h): Removed the
isPlayinggate on LTC pitch (slqPlaying ? pll.pitch : 0.0). When a Denon deck pauses, theSpeedStateMap path ramps toward 0 over several hundred milliseconds -- the PLL follows that ramp and LTC decelerates smoothly, matching the ProDJLink behavior. TheisPlayinggate cut LTC to zero the instantPlay=falsearrived (which can precede the Speed ramp), causing an audible click/pop in the LTC audio stream. Now usespll.pitchdirectly, same as ProDJLink line 882. ThesourceActivegate (speed >= kMinEncodingPitch) already handles the fully-stopped state. -
Speed fallback race condition (StageLinQInput.h): Added
std::atomic<bool> speedReceivedtoStageLinQDeckState.getActualSpeed()previously fell back to 1.0 whenspeed == 0.0 && isPlaying == true. This caused a race: ifSpeed=0.0(pause ramp complete) arrived beforePlay=false(two separate StateMap paths, delivery order not guaranteed), the fallback returned 1.0 -- telling the PLL the deck was at full speed when actually stopped. The PLL would jump forward incorrectly. Now the fallback only activates when Speed has never sent a value (!speedReceived), covering the case where firmware doesn't emit the Speed path at all. Once Speed sends any value (including 0.0), it is trusted.
Show Lock (MainComponent, AppSettings)
New "SHOW LOCK" button in the tab bar (right edge) prevents accidental changes during live shows. Similar to grandMA Session Lock or QLab Show Mode.
When locked (button shows "LOCKED" in red):
Blocked: input source selection, FPS buttons, FPS convert toggle, player/interface/device combos (audio, MIDI, ArtNet), add/rename/delete engines, TrackMap editor, backup/restore, output frame offsets (MTC, ArtNet, LTC), OSC BPM forward, OSC mixer forward, MIDI mixer forward, MIDI clock, ArtNet mixer forward, ArtNet triggers, Ableton Link, MIDI/ArtNet trigger device and address config. Attempting a blocked action flashes the lock button bright red for 300ms as visual feedback. ComboBox and slider selections are automatically reverted to their engine state.
Unlocked (operational during shows): output enable/disable toggles (emergency silencing), gain sliders, BPM multiplier, freewheel controls, view windows.
Lock: single click. Unlock: click + confirm dialog ("Unlock Show Mode?") to prevent accidental unlock. Close app while locked: confirmation dialog warns that all timecode outputs will stop.
State persisted in settings.json (showModeLocked field); survives app restart.
New files
| File | Lines | Description |
|---|---|---|
| StageLinQInput.h | 2649 | Complete StageLinQ protocol (discovery, StateMap 45+26+10 paths, BeatInfo, multi-device) |
| StageLinQDbClient.h | 1390 | FileTransfer protocol + SQLite database + artwork + waveform + cues/loops/beatgrid decoders |
| StageLinQView.h | 961 | External deck view window with artwork, waveform, cues, live loops, key, badges |
| sqlite3.h | -- | SQLite amalgamation v3.47.2 header (public domain, vendored) |
| sqlite3.c | -- | SQLite amalgamation v3.47.2 implementation (public domain, vendored) |
| RELEASE_NOTES_v1.6.0.md | -- | This file |
Modified files
| File | Lines | Changes |
|---|---|---|
| TimecodeEngine.h | 2374 | StageLinQ tick loop, mixer forward, dedup arrays, start/stop, LTC pitch fix |
| MainComponent.h | 536 | StageLinQ + DbClient + SlqMixerMap members, Show Lock |
| MainComponent.cpp | 5052 | Button, lifecycle, display, artwork/waveform feed, mixer bars, tab centering, Show Lock |
| MixerMap.h | 363 | Dual-mode (Pioneer/Denon), separate persistence, buildDenonDefaults() |
| MixerMapEditor.h | 527 | Hide DJM model filter for Denon mode, correct export filename |
| AppSettings.h | 930 | SLQ view bounds, layout persistence, Show Lock persistence |
| Main.cpp | 121 | Version 1.5.3 -> 1.6.0, Show Lock close confirmation |
| README.md | -- | StageLinQ section, sqlite3.c in CMake, SQLITE_THREADSAFE=1 |
Build requirements
- Add sqlite3.h and sqlite3.c to the project (Projucer or CMake)
- Add SQLITE_THREADSAFE=1 to Preprocessor Definitions
- Optionally add _CRT_SECURE_NO_WARNINGS to suppress sqlite3.c Windows deprecation warnings
- Add sqlite3.c linguist-vendored and sqlite3.h linguist-vendored to .gitattributes
Known limitations (pending hardware testing)
- No hardware testing yet: All protocol work is based on open-source references. Extensive logging (Debug builds) and unknown-path capture is included for hardware verification.
- Network path format unverified: parseNetworkPathToDbPath matches chrisle/StageLinq exactly but needs real TrackNetworkPath values from hardware to confirm.
- BLOB decoders unverified: Waveform, cue, loop, and beatgrid decoders follow libdjinterop documentation but need real Engine DJ database BLOBs to verify.
- BeatInfo timeline units: Assumed milliseconds per reference implementations.
- No XF-A/XF-B: Denon does not expose crossfader channel assignment via StateMap.
- No EQ/effects mixer data: Only faders + crossfader are available from the 3 reference implementations. More paths may be discovered with hardware via the unknown path logger.
- macOS firewall: Same ad-hoc signing limitation as ProDJLink -- Application Firewall may prompt or block.