From a8871ec4de6a34435b5638f1256947e63b07184c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 17 May 2026 22:41:06 +0000 Subject: [PATCH 1/6] OS2L: emit structured signals for song and beat events 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. --- plugins/os2l/os2lplugin.cpp | 32 ++++++++++++++++++++++++++++++-- plugins/os2l/os2lplugin.h | 16 ++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/plugins/os2l/os2lplugin.cpp b/plugins/os2l/os2lplugin.cpp index 6c7373382a..5995d99755 100644 --- a/plugins/os2l/os2lplugin.cpp +++ b/plugins/os2l/os2lplugin.cpp @@ -455,9 +455,15 @@ void OS2LPlugin::slotProcessTCPPackets() else if (event == "beat") { // "beat" event — BPM synchronization. - qDebug() << "[OS2L] Beat message received"; - diagLog("message", "beat"); + // OS2L spec optional fields: bpm, pos, change. + double bpm = jsonObj.value("bpm").toDouble(); + double pos = jsonObj.value("pos").toDouble(); + bool change = jsonObj.value("change").toBool(); + qDebug() << "[OS2L] Beat bpm=" << bpm << "pos=" << pos << "change=" << change; + diagLog("message", QString("beat: bpm=%1 pos=%2 change=%3") + .arg(bpm).arg(pos).arg(change ? "true" : "false")); emit valueChanged(m_inputUniverse, 0, 8341, 255, "beat"); + emit beatInfoReceived(bpm, pos, change); } else if (event == "song") { @@ -474,6 +480,11 @@ void OS2LPlugin::slotProcessTCPPackets() double duration = jsonObj.value("duration").toDouble(); QString remix = jsonObj.value("remix").toString(); int deck = jsonObj.value("deck").toInt(); + // Extension field used by the Song Manager: absolute path to the + // audio file on disk. Not in the OS2L spec; VDJ-side scripts or + // a custom bridge populate it. Empty when absent — Song Manager + // then skips the Audio function / waveform pieces. + QString path = jsonObj.value("path").toString(); qDebug() << "[OS2L] ==================== SONG METADATA ===================="; if (!songName.isEmpty()) qDebug() << "[OS2L] Song Name:" << songName; @@ -488,6 +499,7 @@ void OS2LPlugin::slotProcessTCPPackets() if (elapsed > 0) qDebug() << "[OS2L] Elapsed:" << elapsed << "seconds"; if (duration > 0) qDebug() << "[OS2L] Duration:" << duration << "seconds"; if (deck > 0) qDebug() << "[OS2L] Deck:" << deck; + if (!path.isEmpty()) qDebug() << "[OS2L] Path:" << path; qDebug() << "[OS2L] ======================================================"; // Build a rich diagnostic string for the ring buffer @@ -507,6 +519,22 @@ void OS2LPlugin::slotProcessTCPPackets() QString songId = QString("%1 - %2").arg(artist, songName); emit valueChanged(m_inputUniverse, 0, getHash(songId), 255, songId); } + + QVariantMap songMap; + songMap.insert("name", songName); + songMap.insert("artist", artist); + songMap.insert("album", album); + songMap.insert("genre", genre); + songMap.insert("year", year); + songMap.insert("remix", remix); + songMap.insert("status", status); + songMap.insert("bpm", bpm); + songMap.insert("key", key); + songMap.insert("elapsed", elapsed); + songMap.insert("duration", duration); + songMap.insert("deck", deck); + songMap.insert("path", path); + emit songReceived(songMap); } else { diff --git a/plugins/os2l/os2lplugin.h b/plugins/os2l/os2lplugin.h index 33d32a9cdb..455fdb558c 100644 --- a/plugins/os2l/os2lplugin.h +++ b/plugins/os2l/os2lplugin.h @@ -25,6 +25,7 @@ #include #include #include +#include #include "qlcioplugin.h" @@ -94,6 +95,21 @@ class OS2LPlugin final : public QLCIOPlugin quint32 universe() const; bool bonjourEnabled() const; +signals: + /** Emitted when a structured OS2L "song" event is received. + * Carries every parsed metadata field (see README "Song Metadata Fields") + * plus an optional "path" field for the audio file on disk (used by the + * Song Manager to render a waveform and an Audio function). + * Field keys: name, artist, album, genre, year, remix, status, + * bpm, key, elapsed, duration, deck, path. */ + void songReceived(QVariantMap song); + + /** Emitted on every OS2L "beat" event with the optional fields parsed. + * bpm: current beats-per-minute (0 if not provided); + * pos: position in beats since track start (0 if not provided); + * change: true on the first beat of a new track/loop (false otherwise). */ + void beatInfoReceived(double bpm, double pos, bool change); + protected: bool enableTCPServer(bool enable); quint16 getHash(QString channel); From f44cee7738d0ccaf19a6f6728c418cdd588b094c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 17 May 2026 22:51:21 +0000 Subject: [PATCH 2/6] Add VdjBridge: surface OS2L song/beat data in qmlui MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- qmlui/CMakeLists.txt | 1 + qmlui/app.cpp | 22 ++++ qmlui/app.h | 2 + qmlui/qml/showmanager/ShowManager.qml | 86 ++++++++++++++++ qmlui/test/CMakeLists.txt | 1 + qmlui/test/vdjbridge/CMakeLists.txt | 19 ++++ qmlui/test/vdjbridge/vdjbridge_test.cpp | 117 +++++++++++++++++++++ qmlui/test/vdjbridge/vdjbridge_test.h | 24 +++++ qmlui/vdjbridge.cpp | 130 ++++++++++++++++++++++++ qmlui/vdjbridge.h | 122 ++++++++++++++++++++++ 10 files changed, 524 insertions(+) create mode 100644 qmlui/test/vdjbridge/CMakeLists.txt create mode 100644 qmlui/test/vdjbridge/vdjbridge_test.cpp create mode 100644 qmlui/test/vdjbridge/vdjbridge_test.h create mode 100644 qmlui/vdjbridge.cpp create mode 100644 qmlui/vdjbridge.h diff --git a/qmlui/CMakeLists.txt b/qmlui/CMakeLists.txt index 6c3a42c02a..feb1bf2813 100644 --- a/qmlui/CMakeLists.txt +++ b/qmlui/CMakeLists.txt @@ -62,6 +62,7 @@ set(SRC_FILES treemodel.cpp treemodel.h treemodelitem.cpp treemodelitem.h uimanager.cpp uimanager.h + vdjbridge.cpp vdjbridge.h videoeditor.cpp videoeditor.h videoprovider.cpp videoprovider.h waveformimageprovider.cpp waveformimageprovider.h diff --git a/qmlui/app.cpp b/qmlui/app.cpp index a1c049f7f7..fa14593a12 100644 --- a/qmlui/app.cpp +++ b/qmlui/app.cpp @@ -60,9 +60,12 @@ #include "tardis.h" #include "networkmanager.h" #include "flowconsole.h" +#include "vdjbridge.h" #include "qlcfixturedefcache.h" #include "audioplugincache.h" +#include "ioplugincache.h" +#include "qlcioplugin.h" #include "rgbscriptscache.h" #include "qlcconfig.h" #include "qlcfile.h" @@ -90,6 +93,7 @@ App::App() , m_networkManager(nullptr) , m_uiManager(nullptr) , m_flowConsole(nullptr) + , m_vdjBridge(nullptr) , m_doc(nullptr) , m_docLoaded(false) , m_printItem(nullptr) @@ -199,6 +203,11 @@ void App::startup() m_flowConsole = new FlowConsole(this, m_doc); + // VDJ telemetry bridge. Plugins are not yet loaded here (initDoc runs + // later from startup()); the actual OS2L plugin lookup happens there. + m_vdjBridge = new VdjBridge(this); + rootContext()->setContextProperty("vdjBridge", m_vdjBridge); + m_contextManager->registerContext(m_virtualConsole); m_contextManager->registerContext(m_flowConsole); m_contextManager->registerContext(m_simpleDesk); @@ -210,6 +219,7 @@ void App::startup() qmlRegisterUncreatableType("org.qlcplus.classes", 1, 0, "ShowManager", "Can't create a ShowManager!"); qmlRegisterUncreatableType("org.qlcplus.classes", 1, 0, "NetworkManager", "Can't create a NetworkManager!"); qmlRegisterUncreatableType("org.qlcplus.classes", 1, 0, "SimpleDesk", "Can't create a SimpleDesk!"); + qmlRegisterUncreatableType("org.qlcplus.classes", 1, 0, "VdjBridge", "Use the vdjBridge context property"); // Start up in non-modified state m_doc->resetModified(); @@ -580,6 +590,18 @@ void App::initDoc() m_doc->ioPluginCache()->load(IOPluginCache::systemPluginDirectory()); #endif + // Attach the VDJ telemetry bridge to the OS2L plugin if it loaded. + // Plugin may be absent (build without OS2L, or load failure) — bridge + // simply stays in disconnected state in that case. + if (m_vdjBridge != nullptr) + { + QLCIOPlugin *os2l = m_doc->ioPluginCache()->plugin("OS2L"); + if (os2l != nullptr) + m_vdjBridge->attachOS2LPlugin(os2l); + else + qDebug() << "[VdjBridge] OS2L plugin not available"; + } + /* Load audio decoder plugins * This doesn't use a AudioPluginCache::systemPluginDirectory() cause * otherwise the qlcconfig.h creation should have been moved into the diff --git a/qmlui/app.h b/qmlui/app.h index 9cc585a528..a0710c5835 100644 --- a/qmlui/app.h +++ b/qmlui/app.h @@ -47,6 +47,7 @@ class VideoProvider; class FixtureEditor; class FlowConsole; class Tardis; +class VdjBridge; class QMouseEvent; #define SETTINGS_LANGUAGE "ui/language" @@ -246,6 +247,7 @@ protected slots: UiManager *m_uiManager; Tardis *m_tardis; FlowConsole *m_flowConsole; + VdjBridge *m_vdjBridge; /********************************************************************* * Doc diff --git a/qmlui/qml/showmanager/ShowManager.qml b/qmlui/qml/showmanager/ShowManager.qml index f172dcede0..e370b1ffeb 100644 --- a/qmlui/qml/showmanager/ShowManager.qml +++ b/qmlui/qml/showmanager/ShowManager.qml @@ -373,6 +373,92 @@ Rectangle Layout.fillWidth: true } + // VDJ telemetry strip — populated by VdjBridge from the OS2L plugin. + // Visible at all times so the user can see "not connected"; lights up + // and shows current song / BPM / beat when VDJ is streaming. + Rectangle + { + id: vdjStrip + Layout.preferredHeight: parent.height - 8 + Layout.preferredWidth: vdjRow.implicitWidth + 16 + Layout.alignment: Qt.AlignVCenter + color: "#1a1a1a" + border.color: vdjBridge.connected ? "#2ecc71" : "#444" + border.width: 1 + radius: 4 + + // Pulses on each beat — provides a quick visual sanity check + // that beat events are actually arriving from VDJ. + Rectangle + { + id: beatPulse + anchors.left: parent.left + anchors.leftMargin: 6 + anchors.verticalCenter: parent.verticalCenter + width: 10 + height: 10 + radius: 5 + color: vdjBridge.connected ? "#2ecc71" : "#555" + opacity: 0.4 + Behavior on opacity { NumberAnimation { duration: 120 } } + } + + Connections + { + target: vdjBridge + function onBeatChanged() + { + beatPulse.opacity = 1.0 + beatPulseFade.restart() + } + } + + Timer + { + id: beatPulseFade + interval: 90 + onTriggered: beatPulse.opacity = 0.4 + } + + RowLayout + { + id: vdjRow + anchors.fill: parent + anchors.leftMargin: 22 + anchors.rightMargin: 6 + spacing: 10 + + RobotoText + { + label: vdjBridge.connected ? qsTr("VDJ") : qsTr("VDJ —") + fontSize: UISettings.textSizeDefault - 2 + labelColor: vdjBridge.connected ? "#2ecc71" : "#888" + } + + Text + { + visible: vdjBridge.currentSongName !== "" + text: vdjBridge.currentArtist !== "" + ? vdjBridge.currentArtist + " — " + vdjBridge.currentSongName + : vdjBridge.currentSongName + font.family: UISettings.robotoFontName + font.pixelSize: UISettings.textSizeDefault - 2 + color: "#ddd" + elide: Text.ElideRight + Layout.maximumWidth: 280 + Layout.alignment: Qt.AlignVCenter + } + + RobotoText + { + visible: vdjBridge.bpm > 0 + label: vdjBridge.bpm.toFixed(1) + " BPM" + fontSize: UISettings.textSizeDefault - 2 + labelColor: "#9be" + } + } + } + RobotoText { label: qsTr("Markers") diff --git a/qmlui/test/CMakeLists.txt b/qmlui/test/CMakeLists.txt index 80bf5bde0c..cc735ff01f 100644 --- a/qmlui/test/CMakeLists.txt +++ b/qmlui/test/CMakeLists.txt @@ -1,3 +1,4 @@ project(test) add_subdirectory(palettegenerator) +add_subdirectory(vdjbridge) diff --git a/qmlui/test/vdjbridge/CMakeLists.txt b/qmlui/test/vdjbridge/CMakeLists.txt new file mode 100644 index 0000000000..f917117467 --- /dev/null +++ b/qmlui/test/vdjbridge/CMakeLists.txt @@ -0,0 +1,19 @@ +set(module_name "vdjbridge_test") + +add_executable(${module_name} WIN32 + ${module_name}.cpp ${module_name}.h + ../../vdjbridge.cpp ../../vdjbridge.h +) + +target_include_directories(${module_name} PRIVATE + ../../../engine/src + ../../../plugins/interfaces + ../.. +) + +target_link_libraries(${module_name} PRIVATE + Qt${QT_MAJOR_VERSION}::Core + Qt${QT_MAJOR_VERSION}::Gui + Qt${QT_MAJOR_VERSION}::Test + qlcplusengine +) diff --git a/qmlui/test/vdjbridge/vdjbridge_test.cpp b/qmlui/test/vdjbridge/vdjbridge_test.cpp new file mode 100644 index 0000000000..31398bdcfb --- /dev/null +++ b/qmlui/test/vdjbridge/vdjbridge_test.cpp @@ -0,0 +1,117 @@ +/* + Q Light Controller Plus - Unit test + vdjbridge_test.cpp +*/ + +#include +#include + +#include "vdjbridge_test.h" +#include "vdjbridge.h" + +void VdjBridge_Test::initialState() +{ + VdjBridge b; + QCOMPARE(b.connected(), false); + QCOMPARE(b.bpm(), 0.0); + QCOMPARE(b.beatPos(), 0.0); + QCOMPARE(b.beatCount(), 0); + QVERIFY(b.currentSongName().isEmpty()); + QVERIFY(b.currentSongPath().isEmpty()); +} + +void VdjBridge_Test::beatUpdatesBpmAndConnected() +{ + VdjBridge b; + QSignalSpy connectedSpy(&b, &VdjBridge::connectedChanged); + QSignalSpy beatSpy(&b, &VdjBridge::beatChanged); + + b.onBeatInfo(128.0, 4.0, false); + + QCOMPARE(b.connected(), true); + QCOMPARE(b.bpm(), 128.0); + QCOMPARE(b.beatPos(), 4.0); + QCOMPARE(b.beatCount(), 1); + QCOMPARE(connectedSpy.count(), 1); + QCOMPARE(beatSpy.count(), 1); + + b.onBeatInfo(128.0, 5.0, false); + QCOMPARE(b.beatCount(), 2); + // connected stays true — no extra connectedChanged emission + QCOMPARE(connectedSpy.count(), 1); +} + +void VdjBridge_Test::beatChangeResetsCounter() +{ + VdjBridge b; + b.onBeatInfo(120.0, 1.0, false); + b.onBeatInfo(120.0, 2.0, false); + b.onBeatInfo(120.0, 3.0, false); + QCOMPARE(b.beatCount(), 3); + + // change=true marks a new segment — counter resets, then this beat counts as 1 + b.onBeatInfo(120.0, 1.0, true); + QCOMPARE(b.beatCount(), 1); +} + +void VdjBridge_Test::songChangeEmitsSongChanged() +{ + VdjBridge b; + QSignalSpy songSpy(&b, &VdjBridge::songChanged); + QSignalSpy elapsedSpy(&b, &VdjBridge::songElapsedChanged); + + QVariantMap song; + song["name"] = "Strobe"; + song["artist"] = "Deadmau5"; + song["bpm"] = 128.0; + song["path"] = "/music/strobe.mp3"; + + b.onSongReceived(song); + + QCOMPARE(songSpy.count(), 1); + QCOMPARE(elapsedSpy.count(), 0); + QCOMPARE(b.currentSongName(), QStringLiteral("Strobe")); + QCOMPARE(b.currentArtist(), QStringLiteral("Deadmau5")); + QCOMPARE(b.currentSongPath(), QStringLiteral("/music/strobe.mp3")); + QCOMPARE(b.bpm(), 128.0); +} + +void VdjBridge_Test::sameSongElapsedEmitsElapsedOnly() +{ + VdjBridge b; + QVariantMap song; + song["name"] = "Strobe"; + song["artist"] = "Deadmau5"; + song["bpm"] = 128.0; + song["elapsed"] = 5.0; + b.onSongReceived(song); + + QSignalSpy songSpy(&b, &VdjBridge::songChanged); + QSignalSpy elapsedSpy(&b, &VdjBridge::songElapsedChanged); + + // Same track, advanced elapsed + song["elapsed"] = 12.0; + b.onSongReceived(song); + + QCOMPARE(songSpy.count(), 0); + QCOMPARE(elapsedSpy.count(), 1); + QCOMPARE(b.currentElapsed(), 12.0); +} + +void VdjBridge_Test::songWithoutBpmDoesNotZeroBpm() +{ + VdjBridge b; + // Establish a BPM via a beat event + b.onBeatInfo(140.0, 0.0, false); + QCOMPARE(b.bpm(), 140.0); + + // Now a song event without bpm (or with 0) — must not clobber it + QVariantMap song; + song["name"] = "Track A"; + song["bpm"] = 0.0; + b.onSongReceived(song); + + QCOMPARE(b.bpm(), 140.0); +} + +QTEST_MAIN(VdjBridge_Test) diff --git a/qmlui/test/vdjbridge/vdjbridge_test.h b/qmlui/test/vdjbridge/vdjbridge_test.h new file mode 100644 index 0000000000..627f986297 --- /dev/null +++ b/qmlui/test/vdjbridge/vdjbridge_test.h @@ -0,0 +1,24 @@ +/* + Q Light Controller Plus - Unit test + vdjbridge_test.h +*/ + +#ifndef VDJBRIDGE_TEST_H +#define VDJBRIDGE_TEST_H + +#include + +class VdjBridge_Test : public QObject +{ + Q_OBJECT + +private slots: + void initialState(); + void beatUpdatesBpmAndConnected(); + void beatChangeResetsCounter(); + void songChangeEmitsSongChanged(); + void sameSongElapsedEmitsElapsedOnly(); + void songWithoutBpmDoesNotZeroBpm(); +}; + +#endif diff --git a/qmlui/vdjbridge.cpp b/qmlui/vdjbridge.cpp new file mode 100644 index 0000000000..8de97d5824 --- /dev/null +++ b/qmlui/vdjbridge.cpp @@ -0,0 +1,130 @@ +/* + Q Light Controller Plus + vdjbridge.cpp + + Copyright (c) Massimo Callegari + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include "vdjbridge.h" +#include "qlcioplugin.h" + +#include + +VdjBridge::VdjBridge(QObject *parent) + : QObject(parent) +{ +} + +void VdjBridge::attachOS2LPlugin(QLCIOPlugin *plugin) +{ + if (m_plugin == plugin) + return; + + if (!m_plugin.isNull()) + m_plugin->disconnect(this); + + m_plugin = plugin; + if (m_plugin.isNull()) + { + if (m_connected) + { + m_connected = false; + emit connectedChanged(); + } + return; + } + + // String-based connections so qmlui does not need to link the plugin + // shared library. Signatures must match exactly what the plugin emits. + connect(m_plugin.data(), SIGNAL(songReceived(QVariantMap)), + this, SLOT(onSongReceived(QVariantMap))); + connect(m_plugin.data(), SIGNAL(beatInfoReceived(double,double,bool)), + this, SLOT(onBeatInfo(double,double,bool))); + connect(m_plugin.data(), SIGNAL(connectionStatusChanged(quint32,quint32)), + this, SLOT(refreshConnectionStatus())); + + refreshConnectionStatus(); +} + +void VdjBridge::refreshConnectionStatus() +{ + bool now = false; + if (!m_plugin.isNull()) + now = (m_plugin->connectionStatus(0) == QLCIOPlugin::Connected); + + if (now != m_connected) + { + m_connected = now; + emit connectedChanged(); + } +} + +void VdjBridge::onBeatInfo(double bpm, double pos, bool change) +{ + if (bpm > 0.0) + m_bpm = bpm; + m_beatPos = pos; + // OS2L "change=true" marks the first beat of a new track/loop; reset the + // running counter so the UI can display "beat N of 4" against the current + // segment without drifting. + if (change) + m_beatCount = 0; + ++m_beatCount; + + // The first beat we receive is the most reliable evidence VDJ is actually + // streaming to us. connectionStatusChanged covers the TCP handshake but + // not every transport (e.g. dropped + re-routed Bonjour entries). + if (!m_connected) + { + m_connected = true; + emit connectedChanged(); + } + + emit beatChanged(); +} + +void VdjBridge::onSongReceived(QVariantMap song) +{ + // Detect whether this is a real song change (vs. just an elapsed-time + // update for the same track) so the Song Manager doesn't re-run its + // create/lookup pipeline on every status-only event. + const QString oldName = currentSongName(); + const QString oldPath = currentSongPath(); + const QString newName = song.value("name").toString(); + const QString newPath = song.value("path").toString(); + + const bool sameTrack = (!oldName.isEmpty() && oldName == newName) || + (!oldPath.isEmpty() && oldPath == newPath); + + m_currentSong = song; + + if (sameTrack) + { + emit songElapsedChanged(); + } + else + { + if (song.value("bpm").toDouble() > 0.0) + m_bpm = song.value("bpm").toDouble(); + emit songChanged(); + emit beatChanged(); + } + + if (!m_connected) + { + m_connected = true; + emit connectedChanged(); + } +} diff --git a/qmlui/vdjbridge.h b/qmlui/vdjbridge.h new file mode 100644 index 0000000000..42090672e6 --- /dev/null +++ b/qmlui/vdjbridge.h @@ -0,0 +1,122 @@ +/* + Q Light Controller Plus + vdjbridge.h + + Copyright (c) Massimo Callegari + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0.txt + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#ifndef VDJBRIDGE_H +#define VDJBRIDGE_H + +#include +#include +#include + +class QLCIOPlugin; + +/** + * Qt-facing facade for the OS2L plugin's structured event stream. + * + * The OS2L plugin (a shared library loaded via IOPluginCache) emits + * songReceived(QVariantMap) and beatInfoReceived(double,double,bool) signals. + * VdjBridge is the qmlui-side QObject that consumes those signals and exposes + * the live VDJ state (current song, BPM, beat position, connection status) + * as Q_PROPERTYs that QML can bind to and that the Song Manager can react to. + * + * Connections from the plugin are made by App using Qt's string-based + * signal/slot syntax so qmlui does not need to link against the plugin .so. + * + * The bridge is intentionally agnostic about which VDJ-side bridge produced + * the events: a second backend (e.g. a future reverse-engineered DMXDesktop + * protocol) can feed the same slots without any change to consumers. + */ +class VdjBridge final : public QObject +{ + Q_OBJECT + + Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged) + Q_PROPERTY(double bpm READ bpm NOTIFY beatChanged) + Q_PROPERTY(double beatPos READ beatPos NOTIFY beatChanged) + Q_PROPERTY(int beatCount READ beatCount NOTIFY beatChanged) + Q_PROPERTY(QVariantMap currentSong READ currentSong NOTIFY songChanged) + Q_PROPERTY(QString currentSongName READ currentSongName NOTIFY songChanged) + Q_PROPERTY(QString currentArtist READ currentArtist NOTIFY songChanged) + Q_PROPERTY(QString currentSongPath READ currentSongPath NOTIFY songChanged) + Q_PROPERTY(double currentBpm READ currentBpm NOTIFY songChanged) + Q_PROPERTY(double currentDuration READ currentDuration NOTIFY songChanged) + Q_PROPERTY(double currentElapsed READ currentElapsed NOTIFY songElapsedChanged) + Q_PROPERTY(QString currentStatus READ currentStatus NOTIFY songChanged) + +public: + explicit VdjBridge(QObject *parent = nullptr); + + /** + * Bind to an OS2L plugin instance and start consuming its events. + * The plugin pointer may be null (no OS2L plugin available); the bridge + * stays in a disconnected state. Subsequent calls replace any prior binding. + */ + void attachOS2LPlugin(QLCIOPlugin *plugin); + + bool connected() const { return m_connected; } + double bpm() const { return m_bpm; } + double beatPos() const { return m_beatPos; } + int beatCount() const { return m_beatCount; } + QVariantMap currentSong() const { return m_currentSong; } + QString currentSongName() const { return m_currentSong.value("name").toString(); } + QString currentArtist() const { return m_currentSong.value("artist").toString(); } + QString currentSongPath() const { return m_currentSong.value("path").toString(); } + double currentBpm() const { return m_currentSong.value("bpm").toDouble(); } + double currentDuration() const { return m_currentSong.value("duration").toDouble(); } + double currentElapsed() const { return m_currentSong.value("elapsed").toDouble(); } + QString currentStatus() const { return m_currentSong.value("status").toString(); } + +public slots: + /** Connected to OS2LPlugin::songReceived (string-based). */ + void onSongReceived(QVariantMap song); + + /** Connected to OS2LPlugin::beatInfoReceived (string-based). */ + void onBeatInfo(double bpm, double pos, bool change); + + /** Connected to QLCIOPlugin::connectionStatusChanged. + * Re-queries the plugin and updates the connected property. */ + void refreshConnectionStatus(); + +signals: + void connectedChanged(); + void beatChanged(); + + /** + * Emitted on every received song event. The Song Manager listens to this + * to look up or create a Show for the track. + */ + void songChanged(); + + /** Separate from songChanged because elapsed often advances without a full + * song-event re-broadcast — avoids re-running show creation logic. */ + void songElapsedChanged(); + +private: + /** Held as a base-class pointer so qmlui does not need to link the + * OS2L plugin's shared library. Becomes null if the plugin is unloaded. */ + QPointer m_plugin; + + bool m_connected = false; + double m_bpm = 0.0; + double m_beatPos = 0.0; + int m_beatCount = 0; + QVariantMap m_currentSong; +}; + +#endif // VDJBRIDGE_H From b172b324bdec1cc3ac36bdb95ddbb1bae6d6c9f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 17 May 2026 22:56:46 +0000 Subject: [PATCH 3/6] main: guard setColorScheme behind Qt 6.5+ 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. --- qmlui/main.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qmlui/main.cpp b/qmlui/main.cpp index 8295e0bb38..c63ab5a2a2 100644 --- a/qmlui/main.cpp +++ b/qmlui/main.cpp @@ -58,7 +58,9 @@ int main(int argc, char *argv[]) QApplication app(argc, argv); /* Force dark mode so the macOS title bar and window chrome match the dark UI */ +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) QGuiApplication::styleHints()->setColorScheme(Qt::ColorScheme::Dark); +#endif // Since Qt6, the default rendering backend is Rhi. // QLC+ doesn't support it yet so OpenGL have to be forced. From 158aec99a778daa2d70c3fbb895ffc6679ce39c9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 17 May 2026 22:57:00 +0000 Subject: [PATCH 4/6] ShowManager: auto-create a Show per VDJ song event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ' - ' 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. --- ...J_DMXDESKTOP_REVERSE_ENGINEERING_PROMPT.md | 174 ++++++++++++++++ qmlui/app.cpp | 5 + qmlui/qml/showmanager/ShowManager.qml | 20 ++ qmlui/showmanager.cpp | 185 ++++++++++++++++++ qmlui/showmanager.h | 37 ++++ 5 files changed, 421 insertions(+) create mode 100644 docs/VDJ_DMXDESKTOP_REVERSE_ENGINEERING_PROMPT.md diff --git a/docs/VDJ_DMXDESKTOP_REVERSE_ENGINEERING_PROMPT.md b/docs/VDJ_DMXDESKTOP_REVERSE_ENGINEERING_PROMPT.md new file mode 100644 index 0000000000..ee3952ff1f --- /dev/null +++ b/docs/VDJ_DMXDESKTOP_REVERSE_ENGINEERING_PROMPT.md @@ -0,0 +1,174 @@ +# VDJ ↔ QLC+ Telemetry Gap-Fill: Research Prompt + +This file is a **self-contained prompt** for a future Claude Code session that +runs on your machine (with VirtualDJ and DMXDesktop actually installed and +reachable). Open this repo in Claude Code, then paste the prompt below into a +new session. + +--- + +## Background (what's already done) + +This branch (`claude/vdj-song-manager-poc-SqKCZ`) added a VDJ telemetry +pipeline based on the **OS2L** protocol (the open standard QLC+ already speaks +via `plugins/os2l/`). The pipeline is: + +``` +VirtualDJ ──OS2L (JSON over TCP, port 9996)──► plugins/os2l/os2lplugin.cpp + │ + ▼ + songReceived(QVariantMap) + beatInfoReceived(bpm,pos,change) + │ + ▼ + qmlui/vdjbridge.cpp (Qt facade) + │ + ▼ + ShowManager.slotVdjSongChanged + ShowManager.qml telemetry strip +``` + +When `autoCreateSongs` is on, every song event looks up or creates a Show +named `"Artist - Title"`, with `BPM_4_4` time division and the BPM from VDJ. +The Show Manager timeline then draws the beat grid automatically. + +## The gap + +The OS2L standard is **deliberately minimal** — it carries: + +- `evt:"beat"` with optional `bpm`, `pos`, `change` +- `evt:"btn"`, `evt:"cmd"` +- `evt:"song"` with metadata: `name`, `artist`, `album`, `genre`, `year`, + `remix`, `status`, `bpm`, `key`, `elapsed`, `duration`, `deck` + +It **does not** carry: + +- The absolute **file path** of the playing audio (needed so QLC+ can render + a real waveform on the Show timeline via the existing + `WaveformImageProvider`, and so a Song Manager can deep-link to the file). +- The full **beat grid** (an array of beat times) — only individual `beat` + events arrive as they happen, which is fine for live sync but not for + pre-planning cues on a timeline ahead of playback. +- Loops, hot cues, deck-internal markers. + +**Plan from the user:** DMXDesktop (the lighting app from VDJ's own ecosystem) +talks to VirtualDJ over a richer, non-OS2L protocol that exposes the file +path and probably more. We want to reverse-engineer that protocol and add a +second backend that feeds the same `VdjBridge` facade — so consumers +(`ShowManager`, the QML telemetry strip, future MCP tools) don't change. + +## Prerequisites on your machine + +- VirtualDJ Pro 2024+ installed +- DMXDesktop installed and configured to talk to VDJ +- Wireshark, tcpdump, or equivalent +- A few audio tracks loaded into VDJ +- This repo built (`cd build && cmake --build . --target qlcplus-qml -j8`) + +--- + +## ===== Prompt to paste into a new Claude Code session ===== + +> I need help reverse-engineering the network protocol DMXDesktop uses to talk +> to VirtualDJ, so I can add a second backend to my QLC+ `VdjBridge` that +> fills in the gaps OS2L doesn't cover — specifically the **audio file path** +> of the playing track and, if possible, the **full beat grid**. +> +> Read `docs/VDJ_DMXDESKTOP_REVERSE_ENGINEERING_PROMPT.md` for full background. +> The existing OS2L-based pipeline lives in: +> +> - `plugins/os2l/os2lplugin.{h,cpp}` — emits `songReceived(QVariantMap)` +> and `beatInfoReceived(double,double,bool)`. The QVariantMap already has +> a forward-compatible `path` key that's currently always empty when fed +> by OS2L. +> - `qmlui/vdjbridge.{h,cpp}` — the Qt facade. Add a second backend here, +> not in the OS2L plugin. +> - `qmlui/showmanager.cpp` (`ensureShowForVdjSong`) — already creates an +> `Audio` function and attaches it to a Show track when `path` is a +> readable file. So as soon as a backend populates `path`, the waveform +> appears for free on the timeline. +> +> ### Step 1 — Capture +> +> 1. Identify what DMXDesktop listens on (port / interface). Likely +> candidates: a TCP/UDP port on localhost, a websocket, or a named pipe. +> Start with `lsof -i -P -n | grep -i dmxdesktop` (mac) or +> `netstat -anp | findstr dmxdesktop` (windows). +> 2. Start a Wireshark capture on `lo`/loopback filtered to the DMXDesktop +> process, or use `tcpdump -i lo0 -A -s 0 port <port>` to a pcap file. +> 3. In VDJ: load a track on deck 1, start playing, scrub, change BPM, +> activate a hot cue, set a loop, switch decks, change track. Each +> distinct action becomes a labelled section of your capture. +> 4. Save the pcap as `vdj-dmxdesktop.pcap` somewhere I can read. +> +> ### Step 2 — Decode +> +> Read `vdj-dmxdesktop.pcap` with tshark or scapy. Most likely the wire +> format is one of: JSON-over-TCP (line-delimited), length-prefixed binary +> structs, MessagePack, or a WebSocket text frame stream. For each message +> type observed, produce a struct-style description: name, fields, types, +> typical values, frequency. Pay special attention to: +> +> - A message that arrives once per track load — should contain the file +> path or at least a stable track id we can resolve via VDJ's database +> (`VirtualDJ Database v6.xml` / `database.xml`). +> - A message containing a beat-time array or a CBG (computed beat grid). +> - Any handshake / hello / version-negotiation frames. +> +> Compare each message to the OS2L spec — anything OS2L can already deliver +> should be ignored (we already get it). +> +> ### Step 3 — Specify +> +> Write a markdown spec at `docs/DMXDESKTOP_PROTOCOL.md` containing: +> +> - Transport details (port, framing, handshake). +> - One section per message type with example payloads. +> - A mapping table: which DMXDesktop fields fill which `VdjBridge` slots +> (specifically `path`, `bpm`, `pos`, `elapsed`, `duration`, `name`, +> `artist`, plus any new fields like `beatgrid`, `loops`, `cues`). +> - Open questions (anything not figured out yet). +> +> Stop and show me the spec before writing C++ code — I want to sanity-check +> before we commit to a backend implementation. +> +> ### Step 4 — Implement the second backend +> +> Add a sibling to the OS2L plugin: probably a new `plugins/vdjbridge/` +> plugin that implements `QLCIOPlugin` and emits the same +> `songReceived(QVariantMap)` / `beatInfoReceived(...)` signals, with the +> QVariantMap fully populated (including `path` and, if available, a +> `beatgrid` array). Then in `qmlui/app.cpp` look it up the same way the +> OS2L plugin is looked up and call +> `m_vdjBridge->attachOS2LPlugin(plugin)` — rename the method to +> `attachVdjPlugin` if both can be present at once (priority: richer +> backend wins). Add a unit test that pipes a captured frame through +> the parser and asserts `songReceived` carries the expected map. +> +> ### Step 5 — Beat grid in Song Manager +> +> Once the QVariantMap has a `beatgrid` array (list of beat times in ms), +> extend `qmlui/qml/showmanager/ShowManager.qml` to overlay tick marks at +> those positions on the timeline (in addition to the BPM-derived grid that +> already renders). Add a "snap to beat" toggle. +> +> ### Constraints +> +> - **Do not** change `plugins/os2l/` to carry new fields beyond OS2L. The +> user wants OS2L to stay strictly conformant. +> - Keep everything additive: existing Shows / playback / VC behaviour +> must not change. +> - One commit per step. Build (`cmake --build build --target qlcplus-qml`) +> and run `vdjbridge_test` between commits. + +## Notes for future-me + +- If DMXDesktop turns out to use proprietary obfuscation that isn't worth + reverse-engineering, the fallback is to install a **VirtualDJ custom + plugin** (VDJ exposes a C++ SDK) that publishes the missing fields as an + OS2L-compatible side-channel. That's a lot more work than a backend + decoder but is a known-good escape hatch. +- The forward-compatible `path` key in the OS2L plugin's `songReceived` + map (`plugins/os2l/os2lplugin.cpp`) is already in place. A custom VDJ + script that injects a non-standard `"path"` field into its OS2L + messages will Just Work today — no QLC+ changes needed. diff --git a/qmlui/app.cpp b/qmlui/app.cpp index fa14593a12..ff1e96d888 100644 --- a/qmlui/app.cpp +++ b/qmlui/app.cpp @@ -207,6 +207,11 @@ void App::startup() // later from startup()); the actual OS2L plugin lookup happens there. m_vdjBridge = new VdjBridge(this); rootContext()->setContextProperty("vdjBridge", m_vdjBridge); + // Auto-create pipeline is gated by ShowManager::autoCreateSongs (off by + // default); the connection is always present so toggling it on at runtime + // takes effect on the next song event without re-wiring. + connect(m_vdjBridge, &VdjBridge::songChanged, + m_showManager, &ShowManager::slotVdjSongChanged); m_contextManager->registerContext(m_virtualConsole); m_contextManager->registerContext(m_flowConsole); diff --git a/qmlui/qml/showmanager/ShowManager.qml b/qmlui/qml/showmanager/ShowManager.qml index e370b1ffeb..a8a0a4fd30 100644 --- a/qmlui/qml/showmanager/ShowManager.qml +++ b/qmlui/qml/showmanager/ShowManager.qml @@ -456,6 +456,26 @@ Rectangle fontSize: UISettings.textSizeDefault - 2 labelColor: "#9be" } + + // Opt-in switch for VDJ-driven auto-create. Off by default + // so users with existing Show Manager workflows see no + // behaviour change until they ask for it. + CustomCheckBox + { + id: autoCreateToggle + Layout.preferredWidth: 18 + Layout.preferredHeight: 18 + Layout.alignment: Qt.AlignVCenter + checked: showManager.autoCreateSongs + onClicked: showManager.autoCreateSongs = checked + } + RobotoText + { + label: qsTr("Auto-create") + fontSize: UISettings.textSizeDefault - 2 + labelColor: showManager.autoCreateSongs ? "#fc6" : "#888" + tooltipText: qsTr("When on, every VDJ song event looks up or creates a Show with the track's name and BPM.") + } } } diff --git a/qmlui/showmanager.cpp b/qmlui/showmanager.cpp index 070262fef5..3bcdb46851 100644 --- a/qmlui/showmanager.cpp +++ b/qmlui/showmanager.cpp @@ -18,14 +18,17 @@ */ #include <QQmlContext> +#include <QFileInfo> #include "waveformimageprovider.h" #include "showmanager.h" +#include "showfunction.h" #include "sequence.h" #include "tardis.h" #include "chaser.h" #include "track.h" #include "show.h" +#include "audio.h" #include "doc.h" #include "app.h" @@ -1138,3 +1141,185 @@ void ShowManager::pasteFromClipboard() QVariantList() << func->id()); } } + +/********************************************************************* + * VDJ Song Manager integration + * + * Connected from App once the VdjBridge exists. The auto-create + * pipeline is gated on autoCreateSongs (default off) so projects that + * don't want VDJ-driven show management see exactly the previous + * behaviour. + * + * Per OS2L spec the song event delivers metadata (name, artist, bpm, + * elapsed, duration, ...) but NOT the audio file path. We treat the + * 'path' field as optional: when populated by a future richer bridge + * (e.g. a DMXDesktop-protocol backend) we additionally attach an + * Audio function — which gives the user a waveform on the timeline + * for free via the existing WaveformImageProvider. Without a path we + * still create a BPM-locked Show, which renders the beat grid. + *********************************************************************/ + +void ShowManager::setAutoCreateSongs(bool enable) +{ + if (enable == m_autoCreateSongs) + return; + m_autoCreateSongs = enable; + // Reset the de-dup key so toggling on after a song was already + // received still creates/opens the show on the next event. + m_lastAutoCreatedSongKey.clear(); + emit autoCreateSongsChanged(); +} + +static QString vdjSongKey(const QVariantMap &song) +{ + const QString path = song.value("path").toString(); + if (!path.isEmpty()) + return QStringLiteral("path:") + path; + const QString artist = song.value("artist").toString(); + const QString name = song.value("name").toString(); + if (!artist.isEmpty()) + return QStringLiteral("name:") + artist + QStringLiteral(" - ") + name; + return QStringLiteral("name:") + name; +} + +static QString vdjShowName(const QVariantMap &song) +{ + const QString artist = song.value("artist").toString(); + const QString name = song.value("name").toString(); + if (!artist.isEmpty() && !name.isEmpty()) + return artist + QStringLiteral(" - ") + name; + if (!name.isEmpty()) + return name; + return ShowManager::tr("Unknown song"); +} + +void ShowManager::slotVdjSongChanged() +{ + if (!m_autoCreateSongs) + return; + QObject *bridge = sender(); + if (bridge == nullptr) + return; + const QVariant v = bridge->property("currentSong"); + if (!v.isValid()) + return; + ensureShowForVdjSong(v.toMap()); +} + +int ShowManager::ensureShowForVdjSong(QVariantMap song) +{ + const QString name = song.value("name").toString(); + if (name.isEmpty()) + return Function::invalidId(); + + const QString key = vdjSongKey(song); + if (key == m_lastAutoCreatedSongKey && m_currentShow != nullptr) + return m_currentShow->id(); + + const QString showName = vdjShowName(song); + const int bpm = qRound(song.value("bpm").toDouble()); + + // Look up by name first — re-using existing Shows is the whole point of + // making VDJ-driven creation idempotent across sessions. + Show *show = nullptr; + for (Function *f : m_doc->functionsByType(Function::ShowType)) + { + if (f->name() == showName) + { + show = qobject_cast<Show *>(f); + break; + } + } + + bool created = false; + if (show == nullptr) + { + show = new Show(m_doc); + show->setName(showName); + if (bpm > 0) + show->setTimeDivision(Show::BPM_4_4, bpm); + if (m_doc->addFunction(show) == false) + { + qWarning() << "[ShowManager] Failed to add auto-created show for" << showName; + delete show; + return Function::invalidId(); + } + created = true; + Tardis::instance()->enqueueAction( + Tardis::FunctionCreate, show->id(), QVariant(), + Tardis::instance()->actionToByteArray(Tardis::FunctionCreate, show->id())); + } + else if (bpm > 0 && show->timeDivisionBPM() != bpm) + { + // Existing show — only refresh BPM if VDJ has a value we'd otherwise + // miss. Don't downgrade the user's choice of time division (Time vs BPM). + show->setTimeDivisionBPM(bpm); + } + + // Optional: if VDJ supplied an audio file path that's actually readable, + // attach an Audio function to track 0 so the timeline draws a waveform. + // Stock OS2L never carries a path; only a future richer bridge does. + const QString path = song.value("path").toString(); + if (!path.isEmpty()) + { + QFileInfo fi(path); + if (fi.exists() && fi.isReadable()) + { + bool alreadyAttached = false; + for (Track *t : show->tracks()) + { + for (ShowFunction *sf : t->showFunctions()) + { + Function *f = m_doc->function(sf->functionID()); + Audio *a = qobject_cast<Audio *>(f); + if (a && a->getSourceFileName() == path) + { + alreadyAttached = true; + break; + } + } + if (alreadyAttached) break; + } + + if (!alreadyAttached) + { + Audio *audio = new Audio(m_doc); + audio->setName(fi.fileName()); + audio->setSourceFileName(path); + if (m_doc->addFunction(audio)) + { + Track *track = nullptr; + if (show->tracks().isEmpty()) + { + track = new Track(Function::invalidId(), show); + track->setName(tr("Audio")); + show->addTrack(track); + } + else + { + track = show->tracks().first(); + } + ShowFunction *sf = track->createShowFunction(audio->id()); + audio->setTempoType(Function::Time); + sf->setStartTime(0); + sf->setDuration(audio->totalDuration() ? audio->totalDuration() + : qRound(song.value("duration").toDouble() * 1000.0)); + sf->setColor(ShowFunction::defaultColor(Function::AudioType)); + } + else + { + delete audio; + } + } + } + else + { + qDebug() << "[ShowManager] VDJ song path not readable:" << path; + } + } + + setCurrentShowID(show->id()); + m_lastAutoCreatedSongKey = key; + emit vdjSongShowResolved(show->id(), created); + return show->id(); +} diff --git a/qmlui/showmanager.h b/qmlui/showmanager.h index 2c562b9a91..6f63c98580 100644 --- a/qmlui/showmanager.h +++ b/qmlui/showmanager.h @@ -55,6 +55,10 @@ class ShowManager final : public PreviewContext Q_PROPERTY(bool isPaused READ isPaused NOTIFY isPausedChanged) Q_PROPERTY(int showDuration READ showDuration NOTIFY showDurationChanged) + /** Master switch for the VDJ-driven auto-create-show pipeline. + * Off by default so existing workflows are unaffected. */ + Q_PROPERTY(bool autoCreateSongs READ autoCreateSongs WRITE setAutoCreateSongs NOTIFY autoCreateSongsChanged) + Q_PROPERTY(Show::TimeDivision timeDivision READ timeDivision WRITE setTimeDivision NOTIFY timeDivisionChanged) Q_PROPERTY(int beatsDivision READ beatsDivision NOTIFY beatsDivisionChanged) Q_PROPERTY(float timeScale READ timeScale WRITE setTimeScale NOTIFY timeScaleChanged) @@ -127,6 +131,22 @@ class ShowManager final : public PreviewContext /** Flag that indicates if the Show playback is currently paused */ bool isPaused() const; + /** Master switch for the VDJ song-driven auto-create pipeline. */ + bool autoCreateSongs() const { return m_autoCreateSongs; } + void setAutoCreateSongs(bool enable); + + /** Look up or create a Show for the given VDJ song event payload. + * The map must follow the VdjBridge schema (name, artist, bpm, path, + * duration, ...). The Show becomes the current Show in the manager. + * Returns the resolved Show's function id, or Function::invalidId() + * on failure. Safe to call from any signal handler. */ + Q_INVOKABLE int ensureShowForVdjSong(QVariantMap song); + +public slots: + /** Connected to VdjBridge::songChanged from App. Gated by + * autoCreateSongs so existing workflows are unaffected when off. */ + void slotVdjSongChanged(); + signals: void currentShowIDChanged(int currentShowID); void isEditingChanged(); @@ -137,6 +157,12 @@ class ShowManager final : public PreviewContext void isPlayingChanged(bool playing); void isPausedChanged(bool paused); void showDurationChanged(int showDuration); + void autoCreateSongsChanged(); + + /** Emitted whenever a VDJ song event resulted in a Show being + * resolved (created or reused). Carries the Function id of the + * Show so listeners (e.g. tests, MCP, future Songs panel) can react. */ + void vdjSongShowResolved(int showId, bool created); private: void setPlaybackState(bool playing, bool paused); @@ -151,6 +177,17 @@ class ShowManager final : public PreviewContext /** A reference to the Show Function being edited */ Show *m_currentShow; + /** Master switch for the VDJ-driven auto-create pipeline (gated off + * by default). When false, slotVdjSongChanged is a no-op. */ + bool m_autoCreateSongs = false; + + /** Last song name we acted on, used to suppress duplicate auto-create + * invocations when VdjBridge re-emits songChanged for the same track + * (which shouldn't happen given the elapsed-only fast path, but we + * guard here defensively because creating shows on every beat would + * be a bad surprise). */ + QString m_lastAutoCreatedSongKey; + /** Flag that indicates if a Function should be stretched * when the corresponding Show Item duration changes */ bool m_stretchFunctions; From 86a2b3306ff8cde9334ed7f7bbe87333e8054d84 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Mon, 18 May 2026 05:10:41 +0000 Subject: [PATCH 5/6] Strip song-event-dependent code: VDJ does not broadcast song events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ...J_DMXDESKTOP_REVERSE_ENGINEERING_PROMPT.md | 65 +++--- plugins/os2l/os2lplugin.cpp | 22 --- plugins/os2l/os2lplugin.h | 14 +- qmlui/app.cpp | 5 - qmlui/qml/showmanager/ShowManager.qml | 34 ---- qmlui/showmanager.cpp | 185 ------------------ qmlui/showmanager.h | 37 ---- qmlui/test/vdjbridge/vdjbridge_test.cpp | 62 ------ qmlui/test/vdjbridge/vdjbridge_test.h | 3 - qmlui/vdjbridge.cpp | 36 ---- qmlui/vdjbridge.h | 53 ++--- 11 files changed, 49 insertions(+), 467 deletions(-) diff --git a/docs/VDJ_DMXDESKTOP_REVERSE_ENGINEERING_PROMPT.md b/docs/VDJ_DMXDESKTOP_REVERSE_ENGINEERING_PROMPT.md index ee3952ff1f..378bd8549b 100644 --- a/docs/VDJ_DMXDESKTOP_REVERSE_ENGINEERING_PROMPT.md +++ b/docs/VDJ_DMXDESKTOP_REVERSE_ENGINEERING_PROMPT.md @@ -28,27 +28,28 @@ VirtualDJ ──OS2L (JSON over TCP, port 9996)──► plugins/os2l/os2lplugin ShowManager.qml telemetry strip ``` -When `autoCreateSongs` is on, every song event looks up or creates a Show -named `"Artist - Title"`, with `BPM_4_4` time division and the BPM from VDJ. -The Show Manager timeline then draws the beat grid automatically. +The QML telemetry strip shows: connection status, current BPM, and a +beat-pulse indicator. That's all that is wired today. ## The gap -The OS2L standard is **deliberately minimal** — it carries: - -- `evt:"beat"` with optional `bpm`, `pos`, `change` -- `evt:"btn"`, `evt:"cmd"` -- `evt:"song"` with metadata: `name`, `artist`, `album`, `genre`, `year`, - `remix`, `status`, `bpm`, `key`, `elapsed`, `duration`, `deck` - -It **does not** carry: - -- The absolute **file path** of the playing audio (needed so QLC+ can render - a real waveform on the Show timeline via the existing - `WaveformImageProvider`, and so a Song Manager can deep-link to the file). -- The full **beat grid** (an array of beat times) — only individual `beat` - events arrive as they happen, which is fine for live sync but not for - pre-planning cues on a timeline ahead of playback. +Stock VirtualDJ + OS2L only broadcasts `evt:"beat"` (with optional +`bpm`, `pos`, `change`). `evt:"btn"` and `evt:"cmd"` exist in the spec +but fire only when the user has written a corresponding VDJ script +calling `os2l_button` / `os2l_cmd`. `evt:"song"` is documented by some +sources but **VirtualDJ does not actually broadcast it** — confirmed +by the project owner. The OS2L plugin's `song`-parsing code path +therefore never runs in practice and we have removed any qmlui-side +consumers that depended on it. + +This means OS2L gives us beats and nothing more. It **does not** give +us: + +- Anything that identifies the current track (title, artist, file path, + database id). +- The full **beat grid** (an array of beat times) — only individual + `beat` events arrive as they happen, which is fine for live sync but + not for pre-planning cues on a timeline ahead of playback. - Loops, hot cues, deck-internal markers. **Plan from the user:** DMXDesktop (the lighting app from VDJ's own ecosystem) @@ -77,16 +78,18 @@ second backend that feeds the same `VdjBridge` facade — so consumers > Read `docs/VDJ_DMXDESKTOP_REVERSE_ENGINEERING_PROMPT.md` for full background. > The existing OS2L-based pipeline lives in: > -> - `plugins/os2l/os2lplugin.{h,cpp}` — emits `songReceived(QVariantMap)` -> and `beatInfoReceived(double,double,bool)`. The QVariantMap already has -> a forward-compatible `path` key that's currently always empty when fed -> by OS2L. +> - `plugins/os2l/os2lplugin.{h,cpp}` — emits +> `beatInfoReceived(double,double,bool)`. Do NOT extend this plugin +> with non-OS2L data; OS2L stays strictly conformant. > - `qmlui/vdjbridge.{h,cpp}` — the Qt facade. Add a second backend here, -> not in the OS2L plugin. -> - `qmlui/showmanager.cpp` (`ensureShowForVdjSong`) — already creates an -> `Audio` function and attaches it to a Show track when `path` is a -> readable file. So as soon as a backend populates `path`, the waveform -> appears for free on the timeline. +> not in the OS2L plugin. The facade currently exposes only beat / +> BPM / connection state; you will add song / path / beatgrid +> properties as the new backend provides them. +> - `qmlui/showmanager.cpp` — does NOT yet contain any auto-create logic. +> That work was removed because it depended on VDJ-broadcasted song +> events that don't actually exist. Once a new backend supplies song +> metadata + file path, the auto-create-show / attach-Audio-function +> logic needs to be (re-)added here. > > ### Step 1 — Capture > @@ -165,10 +168,6 @@ second backend that feeds the same `VdjBridge` facade — so consumers - If DMXDesktop turns out to use proprietary obfuscation that isn't worth reverse-engineering, the fallback is to install a **VirtualDJ custom - plugin** (VDJ exposes a C++ SDK) that publishes the missing fields as an - OS2L-compatible side-channel. That's a lot more work than a backend + plugin** (VDJ exposes a C++ SDK) that publishes the missing fields over + a side channel of our own design. That's a lot more work than a backend decoder but is a known-good escape hatch. -- The forward-compatible `path` key in the OS2L plugin's `songReceived` - map (`plugins/os2l/os2lplugin.cpp`) is already in place. A custom VDJ - script that injects a non-standard `"path"` field into its OS2L - messages will Just Work today — no QLC+ changes needed. diff --git a/plugins/os2l/os2lplugin.cpp b/plugins/os2l/os2lplugin.cpp index 5995d99755..db2a3dda52 100644 --- a/plugins/os2l/os2lplugin.cpp +++ b/plugins/os2l/os2lplugin.cpp @@ -480,11 +480,6 @@ void OS2LPlugin::slotProcessTCPPackets() double duration = jsonObj.value("duration").toDouble(); QString remix = jsonObj.value("remix").toString(); int deck = jsonObj.value("deck").toInt(); - // Extension field used by the Song Manager: absolute path to the - // audio file on disk. Not in the OS2L spec; VDJ-side scripts or - // a custom bridge populate it. Empty when absent — Song Manager - // then skips the Audio function / waveform pieces. - QString path = jsonObj.value("path").toString(); qDebug() << "[OS2L] ==================== SONG METADATA ===================="; if (!songName.isEmpty()) qDebug() << "[OS2L] Song Name:" << songName; @@ -499,7 +494,6 @@ void OS2LPlugin::slotProcessTCPPackets() if (elapsed > 0) qDebug() << "[OS2L] Elapsed:" << elapsed << "seconds"; if (duration > 0) qDebug() << "[OS2L] Duration:" << duration << "seconds"; if (deck > 0) qDebug() << "[OS2L] Deck:" << deck; - if (!path.isEmpty()) qDebug() << "[OS2L] Path:" << path; qDebug() << "[OS2L] ======================================================"; // Build a rich diagnostic string for the ring buffer @@ -519,22 +513,6 @@ void OS2LPlugin::slotProcessTCPPackets() QString songId = QString("%1 - %2").arg(artist, songName); emit valueChanged(m_inputUniverse, 0, getHash(songId), 255, songId); } - - QVariantMap songMap; - songMap.insert("name", songName); - songMap.insert("artist", artist); - songMap.insert("album", album); - songMap.insert("genre", genre); - songMap.insert("year", year); - songMap.insert("remix", remix); - songMap.insert("status", status); - songMap.insert("bpm", bpm); - songMap.insert("key", key); - songMap.insert("elapsed", elapsed); - songMap.insert("duration", duration); - songMap.insert("deck", deck); - songMap.insert("path", path); - emit songReceived(songMap); } else { diff --git a/plugins/os2l/os2lplugin.h b/plugins/os2l/os2lplugin.h index 455fdb558c..c424783ce5 100644 --- a/plugins/os2l/os2lplugin.h +++ b/plugins/os2l/os2lplugin.h @@ -25,7 +25,6 @@ #include <QJsonObject> #include <QJsonArray> #include <QMutex> -#include <QVariantMap> #include "qlcioplugin.h" @@ -96,18 +95,13 @@ class OS2LPlugin final : public QLCIOPlugin bool bonjourEnabled() const; signals: - /** Emitted when a structured OS2L "song" event is received. - * Carries every parsed metadata field (see README "Song Metadata Fields") - * plus an optional "path" field for the audio file on disk (used by the - * Song Manager to render a waveform and an Audio function). - * Field keys: name, artist, album, genre, year, remix, status, - * bpm, key, elapsed, duration, deck, path. */ - void songReceived(QVariantMap song); - /** Emitted on every OS2L "beat" event with the optional fields parsed. * bpm: current beats-per-minute (0 if not provided); * pos: position in beats since track start (0 if not provided); - * change: true on the first beat of a new track/loop (false otherwise). */ + * change: true on the first beat of a new track/loop (false otherwise). + * + * Stock VirtualDJ broadcasts beat events automatically when OS2L is + * enabled — no user-side scripting required. */ void beatInfoReceived(double bpm, double pos, bool change); protected: diff --git a/qmlui/app.cpp b/qmlui/app.cpp index ff1e96d888..fa14593a12 100644 --- a/qmlui/app.cpp +++ b/qmlui/app.cpp @@ -207,11 +207,6 @@ void App::startup() // later from startup()); the actual OS2L plugin lookup happens there. m_vdjBridge = new VdjBridge(this); rootContext()->setContextProperty("vdjBridge", m_vdjBridge); - // Auto-create pipeline is gated by ShowManager::autoCreateSongs (off by - // default); the connection is always present so toggling it on at runtime - // takes effect on the next song event without re-wiring. - connect(m_vdjBridge, &VdjBridge::songChanged, - m_showManager, &ShowManager::slotVdjSongChanged); m_contextManager->registerContext(m_virtualConsole); m_contextManager->registerContext(m_flowConsole); diff --git a/qmlui/qml/showmanager/ShowManager.qml b/qmlui/qml/showmanager/ShowManager.qml index a8a0a4fd30..7db49ef387 100644 --- a/qmlui/qml/showmanager/ShowManager.qml +++ b/qmlui/qml/showmanager/ShowManager.qml @@ -435,20 +435,6 @@ Rectangle labelColor: vdjBridge.connected ? "#2ecc71" : "#888" } - Text - { - visible: vdjBridge.currentSongName !== "" - text: vdjBridge.currentArtist !== "" - ? vdjBridge.currentArtist + " — " + vdjBridge.currentSongName - : vdjBridge.currentSongName - font.family: UISettings.robotoFontName - font.pixelSize: UISettings.textSizeDefault - 2 - color: "#ddd" - elide: Text.ElideRight - Layout.maximumWidth: 280 - Layout.alignment: Qt.AlignVCenter - } - RobotoText { visible: vdjBridge.bpm > 0 @@ -456,26 +442,6 @@ Rectangle fontSize: UISettings.textSizeDefault - 2 labelColor: "#9be" } - - // Opt-in switch for VDJ-driven auto-create. Off by default - // so users with existing Show Manager workflows see no - // behaviour change until they ask for it. - CustomCheckBox - { - id: autoCreateToggle - Layout.preferredWidth: 18 - Layout.preferredHeight: 18 - Layout.alignment: Qt.AlignVCenter - checked: showManager.autoCreateSongs - onClicked: showManager.autoCreateSongs = checked - } - RobotoText - { - label: qsTr("Auto-create") - fontSize: UISettings.textSizeDefault - 2 - labelColor: showManager.autoCreateSongs ? "#fc6" : "#888" - tooltipText: qsTr("When on, every VDJ song event looks up or creates a Show with the track's name and BPM.") - } } } diff --git a/qmlui/showmanager.cpp b/qmlui/showmanager.cpp index 3bcdb46851..070262fef5 100644 --- a/qmlui/showmanager.cpp +++ b/qmlui/showmanager.cpp @@ -18,17 +18,14 @@ */ #include <QQmlContext> -#include <QFileInfo> #include "waveformimageprovider.h" #include "showmanager.h" -#include "showfunction.h" #include "sequence.h" #include "tardis.h" #include "chaser.h" #include "track.h" #include "show.h" -#include "audio.h" #include "doc.h" #include "app.h" @@ -1141,185 +1138,3 @@ void ShowManager::pasteFromClipboard() QVariantList() << func->id()); } } - -/********************************************************************* - * VDJ Song Manager integration - * - * Connected from App once the VdjBridge exists. The auto-create - * pipeline is gated on autoCreateSongs (default off) so projects that - * don't want VDJ-driven show management see exactly the previous - * behaviour. - * - * Per OS2L spec the song event delivers metadata (name, artist, bpm, - * elapsed, duration, ...) but NOT the audio file path. We treat the - * 'path' field as optional: when populated by a future richer bridge - * (e.g. a DMXDesktop-protocol backend) we additionally attach an - * Audio function — which gives the user a waveform on the timeline - * for free via the existing WaveformImageProvider. Without a path we - * still create a BPM-locked Show, which renders the beat grid. - *********************************************************************/ - -void ShowManager::setAutoCreateSongs(bool enable) -{ - if (enable == m_autoCreateSongs) - return; - m_autoCreateSongs = enable; - // Reset the de-dup key so toggling on after a song was already - // received still creates/opens the show on the next event. - m_lastAutoCreatedSongKey.clear(); - emit autoCreateSongsChanged(); -} - -static QString vdjSongKey(const QVariantMap &song) -{ - const QString path = song.value("path").toString(); - if (!path.isEmpty()) - return QStringLiteral("path:") + path; - const QString artist = song.value("artist").toString(); - const QString name = song.value("name").toString(); - if (!artist.isEmpty()) - return QStringLiteral("name:") + artist + QStringLiteral(" - ") + name; - return QStringLiteral("name:") + name; -} - -static QString vdjShowName(const QVariantMap &song) -{ - const QString artist = song.value("artist").toString(); - const QString name = song.value("name").toString(); - if (!artist.isEmpty() && !name.isEmpty()) - return artist + QStringLiteral(" - ") + name; - if (!name.isEmpty()) - return name; - return ShowManager::tr("Unknown song"); -} - -void ShowManager::slotVdjSongChanged() -{ - if (!m_autoCreateSongs) - return; - QObject *bridge = sender(); - if (bridge == nullptr) - return; - const QVariant v = bridge->property("currentSong"); - if (!v.isValid()) - return; - ensureShowForVdjSong(v.toMap()); -} - -int ShowManager::ensureShowForVdjSong(QVariantMap song) -{ - const QString name = song.value("name").toString(); - if (name.isEmpty()) - return Function::invalidId(); - - const QString key = vdjSongKey(song); - if (key == m_lastAutoCreatedSongKey && m_currentShow != nullptr) - return m_currentShow->id(); - - const QString showName = vdjShowName(song); - const int bpm = qRound(song.value("bpm").toDouble()); - - // Look up by name first — re-using existing Shows is the whole point of - // making VDJ-driven creation idempotent across sessions. - Show *show = nullptr; - for (Function *f : m_doc->functionsByType(Function::ShowType)) - { - if (f->name() == showName) - { - show = qobject_cast<Show *>(f); - break; - } - } - - bool created = false; - if (show == nullptr) - { - show = new Show(m_doc); - show->setName(showName); - if (bpm > 0) - show->setTimeDivision(Show::BPM_4_4, bpm); - if (m_doc->addFunction(show) == false) - { - qWarning() << "[ShowManager] Failed to add auto-created show for" << showName; - delete show; - return Function::invalidId(); - } - created = true; - Tardis::instance()->enqueueAction( - Tardis::FunctionCreate, show->id(), QVariant(), - Tardis::instance()->actionToByteArray(Tardis::FunctionCreate, show->id())); - } - else if (bpm > 0 && show->timeDivisionBPM() != bpm) - { - // Existing show — only refresh BPM if VDJ has a value we'd otherwise - // miss. Don't downgrade the user's choice of time division (Time vs BPM). - show->setTimeDivisionBPM(bpm); - } - - // Optional: if VDJ supplied an audio file path that's actually readable, - // attach an Audio function to track 0 so the timeline draws a waveform. - // Stock OS2L never carries a path; only a future richer bridge does. - const QString path = song.value("path").toString(); - if (!path.isEmpty()) - { - QFileInfo fi(path); - if (fi.exists() && fi.isReadable()) - { - bool alreadyAttached = false; - for (Track *t : show->tracks()) - { - for (ShowFunction *sf : t->showFunctions()) - { - Function *f = m_doc->function(sf->functionID()); - Audio *a = qobject_cast<Audio *>(f); - if (a && a->getSourceFileName() == path) - { - alreadyAttached = true; - break; - } - } - if (alreadyAttached) break; - } - - if (!alreadyAttached) - { - Audio *audio = new Audio(m_doc); - audio->setName(fi.fileName()); - audio->setSourceFileName(path); - if (m_doc->addFunction(audio)) - { - Track *track = nullptr; - if (show->tracks().isEmpty()) - { - track = new Track(Function::invalidId(), show); - track->setName(tr("Audio")); - show->addTrack(track); - } - else - { - track = show->tracks().first(); - } - ShowFunction *sf = track->createShowFunction(audio->id()); - audio->setTempoType(Function::Time); - sf->setStartTime(0); - sf->setDuration(audio->totalDuration() ? audio->totalDuration() - : qRound(song.value("duration").toDouble() * 1000.0)); - sf->setColor(ShowFunction::defaultColor(Function::AudioType)); - } - else - { - delete audio; - } - } - } - else - { - qDebug() << "[ShowManager] VDJ song path not readable:" << path; - } - } - - setCurrentShowID(show->id()); - m_lastAutoCreatedSongKey = key; - emit vdjSongShowResolved(show->id(), created); - return show->id(); -} diff --git a/qmlui/showmanager.h b/qmlui/showmanager.h index 6f63c98580..2c562b9a91 100644 --- a/qmlui/showmanager.h +++ b/qmlui/showmanager.h @@ -55,10 +55,6 @@ class ShowManager final : public PreviewContext Q_PROPERTY(bool isPaused READ isPaused NOTIFY isPausedChanged) Q_PROPERTY(int showDuration READ showDuration NOTIFY showDurationChanged) - /** Master switch for the VDJ-driven auto-create-show pipeline. - * Off by default so existing workflows are unaffected. */ - Q_PROPERTY(bool autoCreateSongs READ autoCreateSongs WRITE setAutoCreateSongs NOTIFY autoCreateSongsChanged) - Q_PROPERTY(Show::TimeDivision timeDivision READ timeDivision WRITE setTimeDivision NOTIFY timeDivisionChanged) Q_PROPERTY(int beatsDivision READ beatsDivision NOTIFY beatsDivisionChanged) Q_PROPERTY(float timeScale READ timeScale WRITE setTimeScale NOTIFY timeScaleChanged) @@ -131,22 +127,6 @@ class ShowManager final : public PreviewContext /** Flag that indicates if the Show playback is currently paused */ bool isPaused() const; - /** Master switch for the VDJ song-driven auto-create pipeline. */ - bool autoCreateSongs() const { return m_autoCreateSongs; } - void setAutoCreateSongs(bool enable); - - /** Look up or create a Show for the given VDJ song event payload. - * The map must follow the VdjBridge schema (name, artist, bpm, path, - * duration, ...). The Show becomes the current Show in the manager. - * Returns the resolved Show's function id, or Function::invalidId() - * on failure. Safe to call from any signal handler. */ - Q_INVOKABLE int ensureShowForVdjSong(QVariantMap song); - -public slots: - /** Connected to VdjBridge::songChanged from App. Gated by - * autoCreateSongs so existing workflows are unaffected when off. */ - void slotVdjSongChanged(); - signals: void currentShowIDChanged(int currentShowID); void isEditingChanged(); @@ -157,12 +137,6 @@ public slots: void isPlayingChanged(bool playing); void isPausedChanged(bool paused); void showDurationChanged(int showDuration); - void autoCreateSongsChanged(); - - /** Emitted whenever a VDJ song event resulted in a Show being - * resolved (created or reused). Carries the Function id of the - * Show so listeners (e.g. tests, MCP, future Songs panel) can react. */ - void vdjSongShowResolved(int showId, bool created); private: void setPlaybackState(bool playing, bool paused); @@ -177,17 +151,6 @@ public slots: /** A reference to the Show Function being edited */ Show *m_currentShow; - /** Master switch for the VDJ-driven auto-create pipeline (gated off - * by default). When false, slotVdjSongChanged is a no-op. */ - bool m_autoCreateSongs = false; - - /** Last song name we acted on, used to suppress duplicate auto-create - * invocations when VdjBridge re-emits songChanged for the same track - * (which shouldn't happen given the elapsed-only fast path, but we - * guard here defensively because creating shows on every beat would - * be a bad surprise). */ - QString m_lastAutoCreatedSongKey; - /** Flag that indicates if a Function should be stretched * when the corresponding Show Item duration changes */ bool m_stretchFunctions; diff --git a/qmlui/test/vdjbridge/vdjbridge_test.cpp b/qmlui/test/vdjbridge/vdjbridge_test.cpp index 31398bdcfb..6caa051781 100644 --- a/qmlui/test/vdjbridge/vdjbridge_test.cpp +++ b/qmlui/test/vdjbridge/vdjbridge_test.cpp @@ -16,8 +16,6 @@ void VdjBridge_Test::initialState() QCOMPARE(b.bpm(), 0.0); QCOMPARE(b.beatPos(), 0.0); QCOMPARE(b.beatCount(), 0); - QVERIFY(b.currentSongName().isEmpty()); - QVERIFY(b.currentSongPath().isEmpty()); } void VdjBridge_Test::beatUpdatesBpmAndConnected() @@ -54,64 +52,4 @@ void VdjBridge_Test::beatChangeResetsCounter() QCOMPARE(b.beatCount(), 1); } -void VdjBridge_Test::songChangeEmitsSongChanged() -{ - VdjBridge b; - QSignalSpy songSpy(&b, &VdjBridge::songChanged); - QSignalSpy elapsedSpy(&b, &VdjBridge::songElapsedChanged); - - QVariantMap song; - song["name"] = "Strobe"; - song["artist"] = "Deadmau5"; - song["bpm"] = 128.0; - song["path"] = "/music/strobe.mp3"; - - b.onSongReceived(song); - - QCOMPARE(songSpy.count(), 1); - QCOMPARE(elapsedSpy.count(), 0); - QCOMPARE(b.currentSongName(), QStringLiteral("Strobe")); - QCOMPARE(b.currentArtist(), QStringLiteral("Deadmau5")); - QCOMPARE(b.currentSongPath(), QStringLiteral("/music/strobe.mp3")); - QCOMPARE(b.bpm(), 128.0); -} - -void VdjBridge_Test::sameSongElapsedEmitsElapsedOnly() -{ - VdjBridge b; - QVariantMap song; - song["name"] = "Strobe"; - song["artist"] = "Deadmau5"; - song["bpm"] = 128.0; - song["elapsed"] = 5.0; - b.onSongReceived(song); - - QSignalSpy songSpy(&b, &VdjBridge::songChanged); - QSignalSpy elapsedSpy(&b, &VdjBridge::songElapsedChanged); - - // Same track, advanced elapsed - song["elapsed"] = 12.0; - b.onSongReceived(song); - - QCOMPARE(songSpy.count(), 0); - QCOMPARE(elapsedSpy.count(), 1); - QCOMPARE(b.currentElapsed(), 12.0); -} - -void VdjBridge_Test::songWithoutBpmDoesNotZeroBpm() -{ - VdjBridge b; - // Establish a BPM via a beat event - b.onBeatInfo(140.0, 0.0, false); - QCOMPARE(b.bpm(), 140.0); - - // Now a song event without bpm (or with 0) — must not clobber it - QVariantMap song; - song["name"] = "Track A"; - song["bpm"] = 0.0; - b.onSongReceived(song); - - QCOMPARE(b.bpm(), 140.0); -} - QTEST_MAIN(VdjBridge_Test) diff --git a/qmlui/test/vdjbridge/vdjbridge_test.h b/qmlui/test/vdjbridge/vdjbridge_test.h index 627f986297..053603c24d 100644 --- a/qmlui/test/vdjbridge/vdjbridge_test.h +++ b/qmlui/test/vdjbridge/vdjbridge_test.h @@ -16,9 +16,6 @@ private slots: void initialState(); void beatUpdatesBpmAndConnected(); void beatChangeResetsCounter(); - void songChangeEmitsSongChanged(); - void sameSongElapsedEmitsElapsedOnly(); - void songWithoutBpmDoesNotZeroBpm(); }; #endif diff --git a/qmlui/vdjbridge.cpp b/qmlui/vdjbridge.cpp index 8de97d5824..6564060398 100644 --- a/qmlui/vdjbridge.cpp +++ b/qmlui/vdjbridge.cpp @@ -48,8 +48,6 @@ void VdjBridge::attachOS2LPlugin(QLCIOPlugin *plugin) // String-based connections so qmlui does not need to link the plugin // shared library. Signatures must match exactly what the plugin emits. - connect(m_plugin.data(), SIGNAL(songReceived(QVariantMap)), - this, SLOT(onSongReceived(QVariantMap))); connect(m_plugin.data(), SIGNAL(beatInfoReceived(double,double,bool)), this, SLOT(onBeatInfo(double,double,bool))); connect(m_plugin.data(), SIGNAL(connectionStatusChanged(quint32,quint32)), @@ -94,37 +92,3 @@ void VdjBridge::onBeatInfo(double bpm, double pos, bool change) emit beatChanged(); } - -void VdjBridge::onSongReceived(QVariantMap song) -{ - // Detect whether this is a real song change (vs. just an elapsed-time - // update for the same track) so the Song Manager doesn't re-run its - // create/lookup pipeline on every status-only event. - const QString oldName = currentSongName(); - const QString oldPath = currentSongPath(); - const QString newName = song.value("name").toString(); - const QString newPath = song.value("path").toString(); - - const bool sameTrack = (!oldName.isEmpty() && oldName == newName) || - (!oldPath.isEmpty() && oldPath == newPath); - - m_currentSong = song; - - if (sameTrack) - { - emit songElapsedChanged(); - } - else - { - if (song.value("bpm").toDouble() > 0.0) - m_bpm = song.value("bpm").toDouble(); - emit songChanged(); - emit beatChanged(); - } - - if (!m_connected) - { - m_connected = true; - emit connectedChanged(); - } -} diff --git a/qmlui/vdjbridge.h b/qmlui/vdjbridge.h index 42090672e6..ed420ac8cf 100644 --- a/qmlui/vdjbridge.h +++ b/qmlui/vdjbridge.h @@ -22,25 +22,28 @@ #include <QObject> #include <QPointer> -#include <QVariantMap> class QLCIOPlugin; /** - * Qt-facing facade for the OS2L plugin's structured event stream. + * Qt-facing facade for the OS2L plugin's beat event stream. * * The OS2L plugin (a shared library loaded via IOPluginCache) emits - * songReceived(QVariantMap) and beatInfoReceived(double,double,bool) signals. - * VdjBridge is the qmlui-side QObject that consumes those signals and exposes - * the live VDJ state (current song, BPM, beat position, connection status) - * as Q_PROPERTYs that QML can bind to and that the Song Manager can react to. + * beatInfoReceived(double,double,bool) for every received OS2L beat + * event. VdjBridge is the qmlui-side QObject that consumes those signals + * and exposes the live VDJ beat / BPM / connection state as Q_PROPERTYs + * that QML can bind to. + * + * The bridge currently covers ONLY what stock VirtualDJ broadcasts over + * OS2L without any user-side scripting: continuous `evt:beat` messages + * with optional bpm/pos/change fields. Song-event handling and any + * file-path / waveform features are deliberately out of scope until a + * second backend (e.g. a reverse-engineered DMXDesktop protocol) can + * actually supply that data — see + * docs/VDJ_DMXDESKTOP_REVERSE_ENGINEERING_PROMPT.md. * * Connections from the plugin are made by App using Qt's string-based * signal/slot syntax so qmlui does not need to link against the plugin .so. - * - * The bridge is intentionally agnostic about which VDJ-side bridge produced - * the events: a second backend (e.g. a future reverse-engineered DMXDesktop - * protocol) can feed the same slots without any change to consumers. */ class VdjBridge final : public QObject { @@ -50,14 +53,6 @@ class VdjBridge final : public QObject Q_PROPERTY(double bpm READ bpm NOTIFY beatChanged) Q_PROPERTY(double beatPos READ beatPos NOTIFY beatChanged) Q_PROPERTY(int beatCount READ beatCount NOTIFY beatChanged) - Q_PROPERTY(QVariantMap currentSong READ currentSong NOTIFY songChanged) - Q_PROPERTY(QString currentSongName READ currentSongName NOTIFY songChanged) - Q_PROPERTY(QString currentArtist READ currentArtist NOTIFY songChanged) - Q_PROPERTY(QString currentSongPath READ currentSongPath NOTIFY songChanged) - Q_PROPERTY(double currentBpm READ currentBpm NOTIFY songChanged) - Q_PROPERTY(double currentDuration READ currentDuration NOTIFY songChanged) - Q_PROPERTY(double currentElapsed READ currentElapsed NOTIFY songElapsedChanged) - Q_PROPERTY(QString currentStatus READ currentStatus NOTIFY songChanged) public: explicit VdjBridge(QObject *parent = nullptr); @@ -73,19 +68,8 @@ class VdjBridge final : public QObject double bpm() const { return m_bpm; } double beatPos() const { return m_beatPos; } int beatCount() const { return m_beatCount; } - QVariantMap currentSong() const { return m_currentSong; } - QString currentSongName() const { return m_currentSong.value("name").toString(); } - QString currentArtist() const { return m_currentSong.value("artist").toString(); } - QString currentSongPath() const { return m_currentSong.value("path").toString(); } - double currentBpm() const { return m_currentSong.value("bpm").toDouble(); } - double currentDuration() const { return m_currentSong.value("duration").toDouble(); } - double currentElapsed() const { return m_currentSong.value("elapsed").toDouble(); } - QString currentStatus() const { return m_currentSong.value("status").toString(); } public slots: - /** Connected to OS2LPlugin::songReceived (string-based). */ - void onSongReceived(QVariantMap song); - /** Connected to OS2LPlugin::beatInfoReceived (string-based). */ void onBeatInfo(double bpm, double pos, bool change); @@ -97,16 +81,6 @@ public slots: void connectedChanged(); void beatChanged(); - /** - * Emitted on every received song event. The Song Manager listens to this - * to look up or create a Show for the track. - */ - void songChanged(); - - /** Separate from songChanged because elapsed often advances without a full - * song-event re-broadcast — avoids re-running show creation logic. */ - void songElapsedChanged(); - private: /** Held as a base-class pointer so qmlui does not need to link the * OS2L plugin's shared library. Becomes null if the plugin is unloaded. */ @@ -116,7 +90,6 @@ public slots: double m_bpm = 0.0; double m_beatPos = 0.0; int m_beatCount = 0; - QVariantMap m_currentSong; }; #endif // VDJBRIDGE_H From 2fcab29f010fde48d70f8b8b481530dd2789b061 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Mon, 18 May 2026 05:35:27 +0000 Subject: [PATCH 6/6] Strip assumed beat-event fields: VDJ sends bare {evt:beat} MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- plugins/os2l/os2lplugin.cpp | 15 +++++------ plugins/os2l/os2lplugin.h | 14 +++++----- qmlui/qml/showmanager/ShowManager.qml | 10 +------ qmlui/test/vdjbridge/vdjbridge_test.cpp | 25 +++--------------- qmlui/test/vdjbridge/vdjbridge_test.h | 3 +-- qmlui/vdjbridge.cpp | 16 +++-------- qmlui/vdjbridge.h | 35 +++++++++---------------- 7 files changed, 34 insertions(+), 84 deletions(-) diff --git a/plugins/os2l/os2lplugin.cpp b/plugins/os2l/os2lplugin.cpp index db2a3dda52..048e279901 100644 --- a/plugins/os2l/os2lplugin.cpp +++ b/plugins/os2l/os2lplugin.cpp @@ -454,16 +454,13 @@ void OS2LPlugin::slotProcessTCPPackets() } else if (event == "beat") { - // "beat" event — BPM synchronization. - // OS2L spec optional fields: bpm, pos, change. - double bpm = jsonObj.value("bpm").toDouble(); - double pos = jsonObj.value("pos").toDouble(); - bool change = jsonObj.value("change").toBool(); - qDebug() << "[OS2L] Beat bpm=" << bpm << "pos=" << pos << "change=" << change; - diagLog("message", QString("beat: bpm=%1 pos=%2 change=%3") - .arg(bpm).arg(pos).arg(change ? "true" : "false")); + // "beat" event — BPM synchronization. Stock VDJ sends a bare + // `{"evt":"beat"}`; the spec allows bpm/pos/change but VDJ + // does not include them, so we do not parse them. + qDebug() << "[OS2L] Beat message received"; + diagLog("message", "beat"); emit valueChanged(m_inputUniverse, 0, 8341, 255, "beat"); - emit beatInfoReceived(bpm, pos, change); + emit beatReceived(); } else if (event == "song") { diff --git a/plugins/os2l/os2lplugin.h b/plugins/os2l/os2lplugin.h index c424783ce5..92d7f8d0ba 100644 --- a/plugins/os2l/os2lplugin.h +++ b/plugins/os2l/os2lplugin.h @@ -95,14 +95,12 @@ class OS2LPlugin final : public QLCIOPlugin bool bonjourEnabled() const; signals: - /** Emitted on every OS2L "beat" event with the optional fields parsed. - * bpm: current beats-per-minute (0 if not provided); - * pos: position in beats since track start (0 if not provided); - * change: true on the first beat of a new track/loop (false otherwise). - * - * Stock VirtualDJ broadcasts beat events automatically when OS2L is - * enabled — no user-side scripting required. */ - void beatInfoReceived(double bpm, double pos, bool change); + /** Emitted on every OS2L "beat" event. Carries no payload because + * stock VirtualDJ broadcasts a bare `{"evt":"beat"}` — the optional + * bpm/pos/change fields in the OS2L spec are permitted but VDJ + * does not include them. Consumers that need BPM must derive it + * from inter-arrival times themselves. */ + void beatReceived(); protected: bool enableTCPServer(bool enable); diff --git a/qmlui/qml/showmanager/ShowManager.qml b/qmlui/qml/showmanager/ShowManager.qml index 7db49ef387..6306f3a542 100644 --- a/qmlui/qml/showmanager/ShowManager.qml +++ b/qmlui/qml/showmanager/ShowManager.qml @@ -406,7 +406,7 @@ Rectangle Connections { target: vdjBridge - function onBeatChanged() + function onBeatReceived() { beatPulse.opacity = 1.0 beatPulseFade.restart() @@ -434,14 +434,6 @@ Rectangle fontSize: UISettings.textSizeDefault - 2 labelColor: vdjBridge.connected ? "#2ecc71" : "#888" } - - RobotoText - { - visible: vdjBridge.bpm > 0 - label: vdjBridge.bpm.toFixed(1) + " BPM" - fontSize: UISettings.textSizeDefault - 2 - labelColor: "#9be" - } } } diff --git a/qmlui/test/vdjbridge/vdjbridge_test.cpp b/qmlui/test/vdjbridge/vdjbridge_test.cpp index 6caa051781..2049052043 100644 --- a/qmlui/test/vdjbridge/vdjbridge_test.cpp +++ b/qmlui/test/vdjbridge/vdjbridge_test.cpp @@ -13,43 +13,26 @@ void VdjBridge_Test::initialState() { VdjBridge b; QCOMPARE(b.connected(), false); - QCOMPARE(b.bpm(), 0.0); - QCOMPARE(b.beatPos(), 0.0); QCOMPARE(b.beatCount(), 0); } -void VdjBridge_Test::beatUpdatesBpmAndConnected() +void VdjBridge_Test::beatTicksCounterAndConnected() { VdjBridge b; QSignalSpy connectedSpy(&b, &VdjBridge::connectedChanged); - QSignalSpy beatSpy(&b, &VdjBridge::beatChanged); + QSignalSpy beatSpy(&b, &VdjBridge::beatReceived); - b.onBeatInfo(128.0, 4.0, false); + b.onBeat(); QCOMPARE(b.connected(), true); - QCOMPARE(b.bpm(), 128.0); - QCOMPARE(b.beatPos(), 4.0); QCOMPARE(b.beatCount(), 1); QCOMPARE(connectedSpy.count(), 1); QCOMPARE(beatSpy.count(), 1); - b.onBeatInfo(128.0, 5.0, false); + b.onBeat(); QCOMPARE(b.beatCount(), 2); // connected stays true — no extra connectedChanged emission QCOMPARE(connectedSpy.count(), 1); } -void VdjBridge_Test::beatChangeResetsCounter() -{ - VdjBridge b; - b.onBeatInfo(120.0, 1.0, false); - b.onBeatInfo(120.0, 2.0, false); - b.onBeatInfo(120.0, 3.0, false); - QCOMPARE(b.beatCount(), 3); - - // change=true marks a new segment — counter resets, then this beat counts as 1 - b.onBeatInfo(120.0, 1.0, true); - QCOMPARE(b.beatCount(), 1); -} - QTEST_MAIN(VdjBridge_Test) diff --git a/qmlui/test/vdjbridge/vdjbridge_test.h b/qmlui/test/vdjbridge/vdjbridge_test.h index 053603c24d..0c5ee8e27c 100644 --- a/qmlui/test/vdjbridge/vdjbridge_test.h +++ b/qmlui/test/vdjbridge/vdjbridge_test.h @@ -14,8 +14,7 @@ class VdjBridge_Test : public QObject private slots: void initialState(); - void beatUpdatesBpmAndConnected(); - void beatChangeResetsCounter(); + void beatTicksCounterAndConnected(); }; #endif diff --git a/qmlui/vdjbridge.cpp b/qmlui/vdjbridge.cpp index 6564060398..7830a796fe 100644 --- a/qmlui/vdjbridge.cpp +++ b/qmlui/vdjbridge.cpp @@ -48,8 +48,8 @@ void VdjBridge::attachOS2LPlugin(QLCIOPlugin *plugin) // String-based connections so qmlui does not need to link the plugin // shared library. Signatures must match exactly what the plugin emits. - connect(m_plugin.data(), SIGNAL(beatInfoReceived(double,double,bool)), - this, SLOT(onBeatInfo(double,double,bool))); + connect(m_plugin.data(), SIGNAL(beatReceived()), + this, SLOT(onBeat())); connect(m_plugin.data(), SIGNAL(connectionStatusChanged(quint32,quint32)), this, SLOT(refreshConnectionStatus())); @@ -69,16 +69,8 @@ void VdjBridge::refreshConnectionStatus() } } -void VdjBridge::onBeatInfo(double bpm, double pos, bool change) +void VdjBridge::onBeat() { - if (bpm > 0.0) - m_bpm = bpm; - m_beatPos = pos; - // OS2L "change=true" marks the first beat of a new track/loop; reset the - // running counter so the UI can display "beat N of 4" against the current - // segment without drifting. - if (change) - m_beatCount = 0; ++m_beatCount; // The first beat we receive is the most reliable evidence VDJ is actually @@ -90,5 +82,5 @@ void VdjBridge::onBeatInfo(double bpm, double pos, bool change) emit connectedChanged(); } - emit beatChanged(); + emit beatReceived(); } diff --git a/qmlui/vdjbridge.h b/qmlui/vdjbridge.h index ed420ac8cf..25f97d08e2 100644 --- a/qmlui/vdjbridge.h +++ b/qmlui/vdjbridge.h @@ -26,21 +26,16 @@ class QLCIOPlugin; /** - * Qt-facing facade for the OS2L plugin's beat event stream. + * Qt-facing facade for the OS2L plugin's beat tick stream. * - * The OS2L plugin (a shared library loaded via IOPluginCache) emits - * beatInfoReceived(double,double,bool) for every received OS2L beat - * event. VdjBridge is the qmlui-side QObject that consumes those signals - * and exposes the live VDJ beat / BPM / connection state as Q_PROPERTYs - * that QML can bind to. + * Exposes ONLY what stock VirtualDJ verifiably broadcasts via OS2L: + * - A bare `evt:"beat"` message at the beat rate (no payload). + * - TCP connection state from the OS2L plugin. * - * The bridge currently covers ONLY what stock VirtualDJ broadcasts over - * OS2L without any user-side scripting: continuous `evt:beat` messages - * with optional bpm/pos/change fields. Song-event handling and any - * file-path / waveform features are deliberately out of scope until a - * second backend (e.g. a reverse-engineered DMXDesktop protocol) can - * actually supply that data — see - * docs/VDJ_DMXDESKTOP_REVERSE_ENGINEERING_PROMPT.md. + * The bridge does NOT expose BPM, beat position, song info, or any + * other field, because VDJ does not send them. Computing BPM from + * inter-arrival times is possible but is a measurement task that + * belongs elsewhere — not in this passive facade. * * Connections from the plugin are made by App using Qt's string-based * signal/slot syntax so qmlui does not need to link against the plugin .so. @@ -50,9 +45,7 @@ class VdjBridge final : public QObject Q_OBJECT Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged) - Q_PROPERTY(double bpm READ bpm NOTIFY beatChanged) - Q_PROPERTY(double beatPos READ beatPos NOTIFY beatChanged) - Q_PROPERTY(int beatCount READ beatCount NOTIFY beatChanged) + Q_PROPERTY(int beatCount READ beatCount NOTIFY beatReceived) public: explicit VdjBridge(QObject *parent = nullptr); @@ -65,13 +58,11 @@ class VdjBridge final : public QObject void attachOS2LPlugin(QLCIOPlugin *plugin); bool connected() const { return m_connected; } - double bpm() const { return m_bpm; } - double beatPos() const { return m_beatPos; } int beatCount() const { return m_beatCount; } public slots: - /** Connected to OS2LPlugin::beatInfoReceived (string-based). */ - void onBeatInfo(double bpm, double pos, bool change); + /** Connected to OS2LPlugin::beatReceived (string-based). */ + void onBeat(); /** Connected to QLCIOPlugin::connectionStatusChanged. * Re-queries the plugin and updates the connected property. */ @@ -79,7 +70,7 @@ public slots: signals: void connectedChanged(); - void beatChanged(); + void beatReceived(); private: /** Held as a base-class pointer so qmlui does not need to link the @@ -87,8 +78,6 @@ public slots: QPointer<QLCIOPlugin> m_plugin; bool m_connected = false; - double m_bpm = 0.0; - double m_beatPos = 0.0; int m_beatCount = 0; };