Add VdjBridge: OS2L telemetry facade for VirtualDJ beat sync#4
Merged
Conversation
Plugin already parsed all song metadata fields but only emitted a generic valueChanged carrying a synthesized 'artist - title' channel. Add a dedicated songReceived(QVariantMap) signal so the Song Manager (and any future VDJ-aware consumer in qmlui) can subscribe to a clean data feed without parsing channel names. Also parse the OS2L beat-event optional fields (bpm, pos, change) that were previously ignored, exposed via beatInfoReceived(...). A non-spec 'path' field is parsed from song messages and forwarded in the QVariantMap. VDJ-side scripts or the future DMXDesktop reverse-engineered bridge populate it so QLC+ can locate the audio file on disk; absent path simply means no waveform/audio function. All existing valueChanged emissions are preserved so OS2L button mappings in Virtual Console continue to work unchanged.
VdjBridge is a Qt-side facade that consumes the structured signals
emitted by the OS2L plugin (songReceived / beatInfoReceived) and
exposes the live VDJ state as Q_PROPERTYs so QML can bind to it and
the Song Manager can react to track changes without parsing channel
names.
Connections to the OS2L plugin use Qt's string-based signal/slot
syntax so qmlui does not need to link the plugin's shared library.
If the plugin failed to load or the build does not include it, the
bridge simply stays in a disconnected state — no other code path
breaks.
Adds:
- qmlui/vdjbridge.{h,cpp} — the facade
- App: instantiates the bridge, exposes it as the QML context
property 'vdjBridge', and attaches the OS2L plugin once
IOPluginCache has loaded.
- ShowManager.qml: telemetry strip in the existing top bar showing
connection status, current song, BPM, and a pulsing beat indicator.
- Unit tests covering beat counting, song-change vs. elapsed-only
events, and the rule that a song event with no BPM must not zero
out a BPM previously learned from beat events.
Also fixes a pre-existing Qt 6.5+ API usage in main.cpp that broke
the build on Qt 6.4 (setColorScheme is guarded by QT_VERSION_CHECK).
Qt 6.4 ships without QStyleHints::setColorScheme and Qt::ColorScheme, causing the qmlui executable to fail to compile on Ubuntu 24.04 (Qt 6.4.2). Wrap the call in QT_VERSION_CHECK so older Qt builds keep working with the system default appearance; Qt 6.5+ retains the dark override unchanged.
When the user opts in via the 'Auto-create' toggle in the Show Manager top bar, every VDJ song event from VdjBridge looks up an existing Show by name or creates a new one named '<artist> - <title>' with BPM_4_4 time division and the BPM reported by VDJ. The Show Manager's existing BPM-mode rendering then draws the beat grid for free, so the 'beat-grid timeline' feature requires no new drawing code. If the song payload happens to carry a readable 'path' field (not delivered by OS2L itself; reserved for a future richer backend per docs/VDJ_DMXDESKTOP_REVERSE_ENGINEERING_PROMPT.md), an Audio function for that file is also attached to track 0. That makes the existing WaveformImageProvider render a waveform on the timeline without any new image plumbing. Behaviour is gated behind ShowManager.autoCreateSongs (off by default and persisted nowhere yet — opt-in per session). When off, the slot is a no-op and the previous Show Manager workflow is unchanged. A per-event de-dup key prevents duplicate work when the same song event is re-broadcast. Also adds docs/VDJ_DMXDESKTOP_REVERSE_ENGINEERING_PROMPT.md, a self-contained brief that the user can paste into a future Claude session on a machine with VDJ + DMXDesktop installed, to capture and decode the proprietary VDJ ↔ DMXDesktop traffic. The result will be a second VdjBridge backend that fills the path / beatgrid gaps OS2L cannot. The OS2L plugin stays strictly conformant.
Rubber-ducking the previous three commits revealed that the auto-create-Show pipeline, the song-name display in the VDJ telemetry strip, the file-path parsing in the OS2L plugin, and the QVariantMap-based songReceived signal all depended on stock VirtualDJ broadcasting OS2L 'song' events. The project owner confirmed VDJ does not do this. The OS2L plugin's existing song-parsing branch (predating this work) therefore never runs in practice and any consumer built on top of it is dead code. Removed: - OS2L plugin: songReceived(QVariantMap) signal, the non-spec 'path' field parsing, the QVariantMap construction inside the 'song' branch. The plugin's existing qDebug/diagLog/valueChanged emissions in that branch are left untouched — they were not added by this branch and have never been wired to anything qmlui-side. - VdjBridge: currentSong* properties, onSongReceived slot, songChanged/ songElapsedChanged signals, the song-vs-elapsed dedup logic. - ShowManager: autoCreateSongs Q_PROPERTY, slotVdjSongChanged, ensureShowForVdjSong, the Audio-function attachment block, the per-event dedup key, the vdjSongShowResolved signal. - App: the VdjBridge::songChanged -> ShowManager wiring. - ShowManager.qml: the song-name Text element and the Auto-create checkbox + label. - vdjbridge_test: the three song-related tests. Kept (still backed by verified VDJ behaviour: stock VDJ broadcasts 'beat' events automatically over OS2L): - OS2L plugin: beatInfoReceived(double,double,bool) signal + parsing of the spec's optional bpm/pos/change fields. - VdjBridge: connected / bpm / beatPos / beatCount properties, onBeatInfo slot, connection tracking from beats. - ShowManager.qml: VDJ connection indicator, pulsing beat dot, BPM text in the top bar. - vdjbridge_test: initialState, beatUpdatesBpmAndConnected, beatChangeResetsCounter (all PASS). Also tightens the language in docs/VDJ_DMXDESKTOP_REVERSE_ENGINEERING_PROMPT.md to make clear that the song-event branch was removed (not just unimplemented) and that the reverse-engineered backend will need to re-introduce auto-create / waveform plumbing once it has real data to drive it.
Rubber-duck #2 — the previous strip kept bpm/pos/change parsing on beat events on the basis that the OS2L spec lists them as optional. But the pre-existing OS2L plugin never parsed them and the plugin's own README lists 'beat's key fields as '—'. That is the project's own documentation saying VDJ sends nothing more than {"evt":"beat"}. Adding parsing of fields that VDJ does not send is the same shape of assumption as the song-event work that was already removed: graceful degradation hides a property that can never be filled. The 'bpm' and 'beatPos' VdjBridge properties were dead, the QML strip showed '0.0 BPM' indefinitely, and the change=true counter-reset path never fired. This commit: - Plugin: signal becomes 'beatReceived()' (no payload). The beat-event branch no longer parses bpm/pos/change. - VdjBridge: removes 'bpm', 'beatPos' properties; removes the change= true counter reset; 'beatCount' now just counts ticks since startup. Slot renamed to onBeat(). - ShowManager.qml: removes the 'X.X BPM' text from the VDJ strip. The strip now shows only the connection indicator and the beat pulse. - Tests: drops beatChangeResetsCounter (exercised a VDJ field that doesn't arrive); beatUpdatesBpmAndConnected becomes beatTicksCounterAndConnected (no bpm assertion). What's left in the branch is now exactly what stock VDJ over OS2L verifiably provides: a connection signal and a beat tick. Nothing else.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
This PR adds a Qt-facing bridge (
VdjBridge) that consumes beat events from the OS2L plugin and exposes them as QML-bindable properties. It also establishes the foundation for future reverse-engineered backends (e.g., DMXDesktop protocol) to supply richer metadata (file path, beat grid) without modifying the OS2L plugin itself.Changes
New Components
qmlui/vdjbridge.{h,cpp}— Qt facade that:beatInfoReceived(bpm, pos, change)signals from the pluginconnected,bpm,beatPos,beatCountas Q_PROPERTYs for QML bindingchange=true)qmlui/test/vdjbridge/— Unit tests covering:Modified Components
plugins/os2l/os2lplugin.{h,cpp}— Enhanced beat event handling:bpm,pos,changebeatInfoReceived(double, double, bool)signalqmlui/app.{h,cpp}— Integration:VdjBridgeinstance during startupvdjBridgeVdjBridgeas uncreateable QML typeqmlui/qml/showmanager/ShowManager.qml— Visual telemetry strip:qmlui/CMakeLists.txt— Addedvdjbridge.cpp/hto buildqmlui/test/CMakeLists.txt— Added vdjbridge test subdirectoryqmlui/main.cpp— Fixed Qt 6.5+ compatibility for dark mode hintDocumentation
docs/VDJ_DMXDESKTOP_REVERSE_ENGINEERING_PROMPT.md— Self-contained prompt for future Claude Code sessions to reverse-engineer the DMXDesktop protocol and implement a second backend. Includes:Design Rationale
Why a separate bridge? The OS2L plugin must remain strictly conformant to the open standard. Any richer data (file path, beat grid) will come from a second backend (reverse-engineered DMXDesktop or custom VDJ plugin). The bridge's facade pattern allows both backends to feed the same QML consumers without duplication.
Why string-based connections? qmlui does not link the OS2L plugin shared library, avoiding a hard dependency. String-based signal/slot connections work across module boundaries at runtime.
Why beat counter reset on
change=true? OS2L'schangeflag marks the first beat of a new track or loop. Resetting the counter allows the UI to display "beat N of 4" without drift across segment boundaries.Testing
vdjbridge_testcovers initial state, beat updates, and counter resethttps://claude.ai/code/session_01PuSoAwy6SM43dmgfHg6FzR