Skip to content

Super Timecode Converter v1.6

Choose a tag to compare

@fiverecords fiverecords released this 21 Mar 20:16
· 29 commits to main since this release
b3d3bf0

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.

  1. Discovery EXIT stops active connections (StageLinQInput.h): When a Denon device broadcasts DISCOVERER_EXIT_, we now stop the active DeviceConnectionThread for 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.

  2. Reconnect kills stale threads (StageLinQInput.h): Before launching a new DeviceConnectionThread in manageConnections(), 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.

  3. StateMap/BeatInfo buffer resync (StageLinQInput.h): StateMap resync now scans forward for smaa magic 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).

  4. 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.

  5. Proactive device liveness detection (StageLinQInput.h): drainMainSocket() now minimally parses Reference messages (instead of discarding raw bytes) and tracks lastDeviceRefTime. If no data arrives for kDeviceTimeoutSec (5s), the connection thread exits proactively instead of waiting for the next TCP write to fail. Added getDeviceIp() accessor and stopConnectionThreadsForIp() helper for fixes 1-2.

  6. BeatInfo clean shutdown (StageLinQInput.h): Added buildBeatInfoStop() and sending the stop frame before closing beatSocket. 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).

  7. 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).

  8. 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(), allowing manageConnections() to relaunch. Previously, the thread became a zombie: it kept sending keepalives on the main socket (keeping dev.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.

  9. 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_DEBUG logging 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.

  10. Player preference subscribed first (StageLinQInput.h): /Client/Preferences/Player is now subscribed before any deck paths. On SC6000 player 2, this path sets deckOffset=2 which 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 with deckOffset=0 (wrong deck indices) until the Player preference arrived at the end. Now the offset is set before any deck data flows.

  11. 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.

  1. LTC pitch deceleration (TimecodeEngine.h): Removed the isPlaying gate on LTC pitch (slqPlaying ? pll.pitch : 0.0). When a Denon deck pauses, the Speed StateMap path ramps toward 0 over several hundred milliseconds -- the PLL follows that ramp and LTC decelerates smoothly, matching the ProDJLink behavior. The isPlaying gate cut LTC to zero the instant Play=false arrived (which can precede the Speed ramp), causing an audible click/pop in the LTC audio stream. Now uses pll.pitch directly, same as ProDJLink line 882. The sourceActive gate (speed >= kMinEncodingPitch) already handles the fully-stopped state.

  2. Speed fallback race condition (StageLinQInput.h): Added std::atomic<bool> speedReceived to StageLinQDeckState. getActualSpeed() previously fell back to 1.0 when speed == 0.0 && isPlaying == true. This caused a race: if Speed=0.0 (pause ramp complete) arrived before Play=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.