diff --git a/CMakeLists.txt b/CMakeLists.txt index d6fe06a49814..7cd87cda49f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -468,6 +468,8 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/controllers/learningutils.cpp src/controllers/midi/legacymidicontrollermapping.cpp src/controllers/midi/legacymidicontrollermappingfilehandler.cpp + src/controllers/midi/midibeatclock.cpp + src/controllers/midi/midibeatclockreceiver.cpp src/controllers/midi/midicontroller.cpp src/controllers/midi/midienumerator.cpp src/controllers/midi/midimessage.cpp @@ -585,6 +587,7 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/engine/sidechain/networkinputstreamworker.cpp src/engine/sidechain/networkoutputstreamworker.cpp src/engine/sync/basesyncablelistener.cpp + src/engine/sync/controllersyncable.cpp src/engine/sync/enginesync.cpp src/engine/sync/internalclock.cpp src/engine/sync/synccontrol.cpp @@ -1493,6 +1496,7 @@ add_executable(mixxx-test src/test/metadatatest.cpp src/test/metaknob_link_test.cpp src/test/midicontrollertest.cpp + src/test/midibeatclockreceivertest.cpp src/test/mixxxtest.cpp src/test/movinginterquartilemean_test.cpp src/test/nativeeffects_test.cpp diff --git a/src/controllers/bulk/bulkcontroller.cpp b/src/controllers/bulk/bulkcontroller.cpp index 242b59f9a2d4..36f4c2afe18b 100644 --- a/src/controllers/bulk/bulkcontroller.cpp +++ b/src/controllers/bulk/bulkcontroller.cpp @@ -65,10 +65,12 @@ static QString get_string(libusb_device_handle *handle, u_int8_t id) { } BulkController::BulkController( + const QString& group, libusb_context* context, libusb_device_handle* handle, struct libusb_device_descriptor* desc) - : m_context(context), + : Controller(group), + m_context(context), m_phandle(handle), in_epaddr(0), out_epaddr(0) { diff --git a/src/controllers/bulk/bulkcontroller.h b/src/controllers/bulk/bulkcontroller.h index 4bf6f0ea740e..1154014ca6f0 100644 --- a/src/controllers/bulk/bulkcontroller.h +++ b/src/controllers/bulk/bulkcontroller.h @@ -37,6 +37,7 @@ class BulkController : public Controller { Q_OBJECT public: BulkController( + const QString& group, libusb_context* context, libusb_device_handle* handle, struct libusb_device_descriptor* desc); diff --git a/src/controllers/bulk/bulkenumerator.cpp b/src/controllers/bulk/bulkenumerator.cpp index d6afabefd2f0..3a713c327e59 100644 --- a/src/controllers/bulk/bulkenumerator.cpp +++ b/src/controllers/bulk/bulkenumerator.cpp @@ -36,6 +36,7 @@ QList BulkEnumerator::queryDevices() { ssize_t i = 0; int err = 0; + int index = 1; for (i = 0; i < cnt; i++) { libusb_device *device = list[i]; struct libusb_device_descriptor desc; @@ -49,8 +50,13 @@ QList BulkEnumerator::queryDevices() { continue; } + // Generate a group for this controller + QString group = QStringLiteral("[BulkController") + + QString::number(index) + QChar(']'); + index++; + BulkController* currentDevice = - new BulkController(m_context, handle, &desc); + new BulkController(group, m_context, handle, &desc); m_devices.push_back(currentDevice); } } diff --git a/src/controllers/controller.cpp b/src/controllers/controller.cpp index c48daf2fa140..0216725bdd58 100644 --- a/src/controllers/controller.cpp +++ b/src/controllers/controller.cpp @@ -8,8 +8,9 @@ #include "moc_controller.cpp" #include "util/screensaver.h" -Controller::Controller() - : m_pScriptEngineLegacy(nullptr), +Controller::Controller(const QString& group) + : m_group(group), + m_pScriptEngineLegacy(nullptr), m_bIsOutputDevice(false), m_bIsInputDevice(false), m_bIsOpen(false), diff --git a/src/controllers/controller.h b/src/controllers/controller.h index 6d764bb4a9c0..82b5147885da 100644 --- a/src/controllers/controller.h +++ b/src/controllers/controller.h @@ -2,11 +2,13 @@ #include #include +#include #include "controllers/controllermappinginfo.h" #include "controllers/legacycontrollermapping.h" #include "controllers/legacycontrollermappingfilehandler.h" #include "controllers/scripting/legacy/controllerscriptenginelegacy.h" +#include "engine/sync/controllersyncable.h" #include "util/duration.h" class ControllerJSProxy; @@ -17,7 +19,7 @@ class ControllerJSProxy; class Controller : public QObject { Q_OBJECT public: - explicit Controller(); + explicit Controller(const QString& group); ~Controller() override; // Subclass should call close() at minimum. /// The object that is exposed to the JS scripts as the "controller" object. @@ -50,6 +52,10 @@ class Controller : public QObject { inline const QString& getCategory() const { return m_sDeviceCategory; } + const QString& getGroup() const { + return m_group; + } + virtual bool isMappable() const = 0; inline bool isLearning() const { return m_bLearning; @@ -112,6 +118,10 @@ class Controller : public QObject { // polling/processing but before closing the device. virtual void stopEngine(); + virtual std::shared_ptr syncable() const { + return nullptr; + } + // To be called when receiving events void triggerActivity(); @@ -150,6 +160,9 @@ class Controller : public QObject { } private: + /// Group name for control objects + const QString m_group; + ControllerScriptEngineLegacy* m_pScriptEngineLegacy; // Verbose and unique device name suitable for display. diff --git a/src/controllers/controllermanager.cpp b/src/controllers/controllermanager.cpp index 48472ae69f4e..aa42f5a5b82e 100644 --- a/src/controllers/controllermanager.cpp +++ b/src/controllers/controllermanager.cpp @@ -6,6 +6,8 @@ #include "controllers/controllerlearningeventfilter.h" #include "controllers/defs_controllers.h" #include "controllers/midi/portmidienumerator.h" +#include "engine/enginemaster.h" +#include "engine/sync/enginesync.h" #include "moc_controllermanager.cpp" #include "util/cmdlineargs.h" #include "util/time.h" @@ -78,9 +80,10 @@ bool controllerCompare(Controller *a,Controller *b) { return a->getName() < b->getName(); } -ControllerManager::ControllerManager(UserSettingsPointer pConfig) +ControllerManager::ControllerManager(UserSettingsPointer pConfig, EngineMaster* pMixingEngine) : QObject(), m_pConfig(pConfig), + m_pMixingEngine(pMixingEngine), // WARNING: Do not parent m_pControllerLearningEventFilter to // ControllerManager because the CM is moved to its own thread and runs // its own event loop. @@ -192,12 +195,31 @@ void ControllerManager::updateControllerList() { locker.relock(); if (newDeviceList != m_controllers) { + m_pMixingEngine->getEngineSync()->setControllerSyncables({}); m_controllers = newDeviceList; + + QList> controllerSyncables; + for (const auto* pController : qAsConst(m_controllers)) { + const std::shared_ptr pSyncable = pController->syncable(); + if (pSyncable != nullptr) { + connect(pSyncable.get(), + &ControllerSyncable::syncModeRequested, + this, + &ControllerManager::slotControllerRequestSyncMode); + controllerSyncables.append(pSyncable); + } + } + m_pMixingEngine->getEngineSync()->setControllerSyncables(controllerSyncables); locker.unlock(); emit devicesChanged(); } } +void ControllerManager::slotControllerRequestSyncMode(SyncMode mode) { + ControllerSyncable* pSyncable = qobject_cast(QObject::sender()); + m_pMixingEngine->getEngineSync()->requestSyncMode(pSyncable, mode); +} + QList ControllerManager::getControllers() const { QMutexLocker locker(&m_mutex); return m_controllers; diff --git a/src/controllers/controllermanager.h b/src/controllers/controllermanager.h index 7cb776afb57f..922bda7da5ac 100644 --- a/src/controllers/controllermanager.h +++ b/src/controllers/controllermanager.h @@ -12,6 +12,7 @@ // Forward declaration(s) class Controller; class ControllerLearningEventFilter; +class EngineMaster; /// Function to sort controllers by name bool controllerCompare(Controller *a, Controller *b); @@ -20,7 +21,7 @@ bool controllerCompare(Controller *a, Controller *b); class ControllerManager : public QObject { Q_OBJECT public: - ControllerManager(UserSettingsPointer pConfig); + ControllerManager(UserSettingsPointer pConfig, EngineMaster* pMixingEngine); virtual ~ControllerManager(); static const mixxx::Duration kPollInterval; @@ -71,8 +72,11 @@ class ControllerManager : public QObject { void stopPolling(); void maybeStartOrStopPolling(); + void slotControllerRequestSyncMode(SyncMode mode); + private: UserSettingsPointer m_pConfig; + EngineMaster* m_pMixingEngine; ControllerLearningEventFilter* m_pControllerLearningEventFilter; QTimer m_pollTimer; mutable QMutex m_mutex; diff --git a/src/controllers/hid/hidcontroller.cpp b/src/controllers/hid/hidcontroller.cpp index 9126db1bac83..7433a4735630 100644 --- a/src/controllers/hid/hidcontroller.cpp +++ b/src/controllers/hid/hidcontroller.cpp @@ -16,8 +16,10 @@ constexpr int kMaxHidErrorMessageSize = 512; } // namespace HidController::HidController( + const QString& group, mixxx::hid::DeviceInfo&& deviceInfo) - : m_deviceInfo(std::move(deviceInfo)), + : Controller(group), + m_deviceInfo(std::move(deviceInfo)), m_pHidDevice(nullptr), m_pollingBufferIndex(0) { setDeviceCategory(mixxx::hid::DeviceCategory::guessFromDeviceInfo(m_deviceInfo)); diff --git a/src/controllers/hid/hidcontroller.h b/src/controllers/hid/hidcontroller.h index f84463302b26..4b2f619dccb4 100644 --- a/src/controllers/hid/hidcontroller.h +++ b/src/controllers/hid/hidcontroller.h @@ -10,6 +10,7 @@ class HidController final : public Controller { Q_OBJECT public: explicit HidController( + const QString& group, mixxx::hid::DeviceInfo&& deviceInfo); ~HidController() override; diff --git a/src/controllers/hid/hidenumerator.cpp b/src/controllers/hid/hidenumerator.cpp index 68c0f70a453e..59116cf0d9cb 100644 --- a/src/controllers/hid/hidenumerator.cpp +++ b/src/controllers/hid/hidenumerator.cpp @@ -65,6 +65,7 @@ HidEnumerator::~HidEnumerator() { QList HidEnumerator::queryDevices() { qInfo() << "Scanning USB HID devices"; + int index = 1; hid_device_info* device_info_list = hid_enumerate(0x0, 0x0); for (const auto* device_info = device_info_list; device_info; @@ -85,8 +86,13 @@ QList HidEnumerator::queryDevices() { continue; } - HidController* newDevice = new HidController(std::move(deviceInfo)); + // Generate a group for this controller + QString group = QStringLiteral("[HidController") + + QString::number(index) + QChar(']'); + + HidController* newDevice = new HidController(group, std::move(deviceInfo)); m_devices.push_back(newDevice); + index++; } hid_free_enumeration(device_info_list); diff --git a/src/controllers/midi/hss1394controller.cpp b/src/controllers/midi/hss1394controller.cpp index d73e20621243..d0e07ec8f3b3 100644 --- a/src/controllers/midi/hss1394controller.cpp +++ b/src/controllers/midi/hss1394controller.cpp @@ -60,10 +60,11 @@ void DeviceChannelListener::Reconnected() { } Hss1394Controller::Hss1394Controller( + const QString& group, const hss1394::TNodeInfo& deviceInfo, int deviceIndex, UserSettingsPointer pConfig) - : MidiController(), + : MidiController(group), m_deviceInfo(deviceInfo), m_iDeviceIndex(deviceIndex) { // Note: We prepend the input stream's index to the device's name to prevent diff --git a/src/controllers/midi/hss1394controller.h b/src/controllers/midi/hss1394controller.h index 1f1cbf57e552..9de49b3d65e3 100644 --- a/src/controllers/midi/hss1394controller.h +++ b/src/controllers/midi/hss1394controller.h @@ -38,6 +38,7 @@ class Hss1394Controller : public MidiController { Q_OBJECT public: Hss1394Controller( + const QString& group, const hss1394::TNodeInfo& deviceInfo, int deviceIndex, UserSettingsPointer pConfig); diff --git a/src/controllers/midi/hss1394enumerator.cpp b/src/controllers/midi/hss1394enumerator.cpp index 76a120c9701c..e922c405d3d4 100644 --- a/src/controllers/midi/hss1394enumerator.cpp +++ b/src/controllers/midi/hss1394enumerator.cpp @@ -28,6 +28,7 @@ QList Hss1394Enumerator::queryDevices() { hss1394::uint uNodes = Node::Instance()->GetNodeCount(); qDebug() << " Nodes detected:" << uNodes; + int index = 1; for(hss1394::uint i=0; i<40; i++) { TNodeInfo tNodeInfo; bool bInstalled; @@ -40,8 +41,14 @@ QList Hss1394Enumerator::queryDevices() { QString("%1").arg(tNodeInfo.uGUID.mu32Low, 0, 16), QString("%1").arg(tNodeInfo.uProtocolVersion, 0, 16)); qDebug() << " " << message; + + // Generate a group for this controller + QString group = QStringLiteral("[Hss1374Controller") + + QString::number(index) + QChar(']'); + index++; + Hss1394Controller* currentDevice = new Hss1394Controller( - tNodeInfo, i, m_pConfig); + group, tNodeInfo, i, m_pConfig); m_devices.push_back(currentDevice); } } diff --git a/src/controllers/midi/midibeatclock.cpp b/src/controllers/midi/midibeatclock.cpp new file mode 100644 index 000000000000..7d4c8ec63576 --- /dev/null +++ b/src/controllers/midi/midibeatclock.cpp @@ -0,0 +1,43 @@ +#include "controllers/midi/midibeatclock.h" + +namespace mixxx { + +MidiBeatClock::MidiBeatClock(const QString& group) + : ControllerSyncable(group) { + // Pick a wide range (1 to 200) and allow out of bounds sets. This lets you + // map a soft-takeover MIDI knob to the master BPM. This also creates bpm_up + // and bpm_down controls. + // bpm_up / bpm_down steps by 1 + // bpm_up_small / bpm_down_small steps by 0.1 + m_pClockBpm.reset(new ControlObject(ConfigKey(m_group, "bpm"))); + m_pClockBpm->setReadOnly(); + // The relative position between two beats in the range 0.0 ... 1.0 + m_pClockBeatDistance.reset(new ControlObject(ConfigKey(m_group, "beat_distance"))); + m_pClockBeatDistance->setReadOnly(); +} + +void MidiBeatClock::receive(unsigned char status, Duration timestamp) { + MidiBeatClockReceiver::receive(status, timestamp); + m_pClockBpm->setAndConfirm(bpm().getValue()); + m_pClockBeatDistance->setAndConfirm(beatDistance()); +} + +void MidiBeatClock::setMasterBeatDistance(double beatDistance) { + Q_UNUSED(beatDistance); +}; + +void MidiBeatClock::setMasterBpm(double bpm) { + Q_UNUSED(bpm); +}; + +void MidiBeatClock::setMasterParams(double beatDistance, double baseBpm, double bpm) { + Q_UNUSED(beatDistance); + Q_UNUSED(baseBpm); + Q_UNUSED(bpm); +}; + +void MidiBeatClock::setInstantaneousBpm(double bpm) { + Q_UNUSED(bpm); +}; + +} // namespace mixxx diff --git a/src/controllers/midi/midibeatclock.h b/src/controllers/midi/midibeatclock.h new file mode 100644 index 000000000000..88b9ea853556 --- /dev/null +++ b/src/controllers/midi/midibeatclock.h @@ -0,0 +1,70 @@ +#include + +#include "control/controlobject.h" +#include "controllers/midi/midibeatclockreceiver.h" +#include "engine/sync/controllersyncable.h" + +namespace mixxx { + +class MidiBeatClock : public ControllerSyncable, public MidiBeatClockReceiver { + public: + MidiBeatClock(const QString& group); + + void receive(unsigned char status, Duration timestamp); + + /// Notify a Syncable that it is now the only currently-playing syncable. + void notifyOnlyPlayingSyncable() override{}; + + /// Notify a Syncable that they should sync phase. + void requestSync() override{}; + + /// Only relevant for player Syncables. + bool isPlaying() const override { + return MidiBeatClockReceiver::isPlaying(); + }; + + /// Gets the current speed of the syncable in bpm (bpm * rate slider), doesn't + /// include scratch or FF/REW values. + double getBpm() const override { + return bpm().getValue(); + }; + + /// Gets the speed of the syncable if it was playing at 1.0 rate. + double getBaseBpm() const override { + return bpm().getValue(); + }; + + /// Gets the beat distance as a fraction from 0 to 1 + double getBeatDistance() const override { + return beatDistance(); + }; + + /// The following functions are used to tell syncables about the state of the + /// current Sync Master. + /// Must never result in a call to + /// SyncableListener::notifyBeatDistanceChanged or signal loops could occur. + void setMasterBeatDistance(double beatDistance) override; + + /// Must never result in a call to SyncableListener::notifyBpmChanged or + /// signal loops could occur. + void setMasterBpm(double bpm) override; + + /// Combines the above three calls into one, since they are often set + /// simultaneously. Avoids redundant recalculation that would occur by + /// using the three calls separately. + void setMasterParams(double beatDistance, double baseBpm, double bpm) override; + + /// Must never result in a call to + /// SyncableListener::notifyInstantaneousBpmChanged or signal loops could + /// occur. + void setInstantaneousBpm(double bpm) override; + + private slots: + void slotSyncLeaderEnabledChangeRequest(double value); + + private: + QScopedPointer m_pClockBpm; + QScopedPointer m_pClockBeatDistance; +}; + +} // namespace mixxx diff --git a/src/controllers/midi/midibeatclockreceiver.cpp b/src/controllers/midi/midibeatclockreceiver.cpp new file mode 100644 index 000000000000..a73da978f2f4 --- /dev/null +++ b/src/controllers/midi/midibeatclockreceiver.cpp @@ -0,0 +1,68 @@ +#include "controllers/midi/midibeatclockreceiver.h" + +#include "controllers/midi/midimessage.h" + +namespace mixxx { + +MidiBeatClockReceiver::MidiBeatClockReceiver() + : m_bpm(Bpm(Bpm::kValueUndefined)), + m_isPlaying(false), + m_clockTickIndex(0) { +} + +// static +bool MidiBeatClockReceiver::canReceiveMidiStatus(unsigned char status) { + switch (status) { + case MidiOpCode::MIDI_START: + case MidiOpCode::MIDI_STOP: + case MidiOpCode::MIDI_TIMING_CLK: + return true; + default: + return false; + } +} + +void MidiBeatClockReceiver::receive(unsigned char status, Duration timestamp) { + switch (status) { + case MidiOpCode::MIDI_START: + m_clockTickIndex.store(0); + m_isPlaying.store(true); + break; + case MidiOpCode::MIDI_STOP: + m_isPlaying.store(false); + break; + case MidiOpCode::MIDI_TIMING_CLK: { + const int index = m_clockTickIndex.load(); + if (m_lastTimestamp != Duration::empty() && timestamp != Duration::empty()) { + m_intervalRingBuffer[m_clockTickIndex] = timestamp - m_lastTimestamp; + + int numValues = 0; + Duration sumIntervals; + for (int i = 0; i < kPulsesPerQuarterNote; i++) { + if (m_intervalRingBuffer[i] != Duration::empty()) { + sumIntervals += m_intervalRingBuffer[i]; + numValues++; + } + } + + if (numValues > 0) { + const Bpm bpm = Bpm(1000000000.0 / + (sumIntervals.toDoubleNanos() / numValues) / + kPulsesPerQuarterNote * 60.0); + m_bpm.store(bpm); + } + } + m_clockTickIndex.store((index + 1) % kPulsesPerQuarterNote); + m_lastTimestamp = timestamp; + break; + } + default: + DEBUG_ASSERT(!"Unhandled message type"); + } +}; + +double MidiBeatClockReceiver::beatDistance() const { + return 1.0 - (static_cast(m_clockTickIndex) / kPulsesPerQuarterNote); +} + +} // namespace mixxx diff --git a/src/controllers/midi/midibeatclockreceiver.h b/src/controllers/midi/midibeatclockreceiver.h new file mode 100644 index 000000000000..b1d80c667188 --- /dev/null +++ b/src/controllers/midi/midibeatclockreceiver.h @@ -0,0 +1,35 @@ +#include + +#include "track/bpm.h" +#include "util/duration.h" + +namespace mixxx { + +class MidiBeatClockReceiver { + public: + static constexpr int kPulsesPerQuarterNote = 24; + + MidiBeatClockReceiver(); + static bool canReceiveMidiStatus(unsigned char status); + void receive(unsigned char status, Duration timestamp); + + bool isPlaying() const { + return m_isPlaying; + } + + Bpm bpm() const { + return m_bpm; + } + + double beatDistance() const; + + private: + std::atomic m_bpm; + std::atomic m_isPlaying; + std::atomic m_clockTickIndex; + + Duration m_lastTimestamp; + Duration m_intervalRingBuffer[kPulsesPerQuarterNote]; +}; + +} // namespace mixxx diff --git a/src/controllers/midi/midicontroller.cpp b/src/controllers/midi/midicontroller.cpp index 4c8b601f0deb..aacd052df9f5 100644 --- a/src/controllers/midi/midicontroller.cpp +++ b/src/controllers/midi/midicontroller.cpp @@ -11,8 +11,9 @@ #include "util/math.h" #include "util/screensaver.h" -MidiController::MidiController() - : Controller() { +MidiController::MidiController(const QString& group) + : Controller(group), + m_pBeatClock(std::make_shared(group)) { setDeviceCategory(tr("MIDI Controller")); } @@ -214,13 +215,17 @@ void MidiController::receivedShortMessage(unsigned char status, unsigned char channel = MidiUtils::channelFromStatus(status); unsigned char opCode = MidiUtils::opCodeFromStatus(status); + if (m_pBeatClock->canReceiveMidiStatus(status)) { + m_pBeatClock->receive(status, timestamp); + } + // Ignore MIDI beat clock messages (0xF8) until we have proper MIDI sync in // Mixxx. These messages are not suitable to use in JS anyway, as they are // sent at 24 ppqn (i.e. one message every 20.83 ms for a 120 BPM track) // and require real-time code. Currently, they are only spam on the // console, inhibit the screen saver unintentionally, could potentially // slow down Mixxx or interfere with the learning wizard. - if (status == 0xF8) { + if (status == MidiOpCode::MIDI_TIMING_CLK) { return; } diff --git a/src/controllers/midi/midicontroller.h b/src/controllers/midi/midicontroller.h index e7b3a8f06a69..811401d52de8 100644 --- a/src/controllers/midi/midicontroller.h +++ b/src/controllers/midi/midicontroller.h @@ -3,6 +3,7 @@ #include "controllers/controller.h" #include "controllers/midi/legacymidicontrollermapping.h" #include "controllers/midi/legacymidicontrollermappingfilehandler.h" +#include "controllers/midi/midibeatclock.h" #include "controllers/midi/midimessage.h" #include "controllers/midi/midioutputhandler.h" #include "controllers/softtakeover.h" @@ -18,7 +19,7 @@ class DlgControllerLearning; class MidiController : public Controller { Q_OBJECT public: - explicit MidiController(); + explicit MidiController(const QString& group); ~MidiController() override; ControllerJSProxy* jsProxy() override; @@ -37,6 +38,10 @@ class MidiController : public Controller { bool matchMapping(const MappingInfo& mapping) override; + std::shared_ptr syncable() const override { + return m_pBeatClock; + } + signals: void messageReceived(unsigned char status, unsigned char control, unsigned char value); @@ -92,6 +97,7 @@ class MidiController : public Controller { std::shared_ptr m_pMapping; SoftTakeoverCtrl m_st; QList > m_fourteen_bit_queued_mappings; + std::shared_ptr m_pBeatClock; // So it can access sendShortMsg() friend class MidiOutputHandler; diff --git a/src/controllers/midi/portmidicontroller.cpp b/src/controllers/midi/portmidicontroller.cpp index 04215af1082b..403731b7158a 100644 --- a/src/controllers/midi/portmidicontroller.cpp +++ b/src/controllers/midi/portmidicontroller.cpp @@ -4,11 +4,12 @@ #include "controllers/midi/midiutils.h" #include "moc_portmidicontroller.cpp" -PortMidiController::PortMidiController(const PmDeviceInfo* inputDeviceInfo, +PortMidiController::PortMidiController(const QString& group, + const PmDeviceInfo* inputDeviceInfo, const PmDeviceInfo* outputDeviceInfo, int inputDeviceIndex, int outputDeviceIndex) - : MidiController(), m_cReceiveMsg_index(0), m_bInSysex(false) { + : MidiController(group), m_cReceiveMsg_index(0), m_bInSysex(false) { for (unsigned int k = 0; k < MIXXX_PORTMIDI_BUFFER_LEN; ++k) { // Can be shortened to `m_midiBuffer[k] = {}` with C++11. m_midiBuffer[k].message = 0; diff --git a/src/controllers/midi/portmidicontroller.h b/src/controllers/midi/portmidicontroller.h index 25012eb1ac99..9cc33968947b 100644 --- a/src/controllers/midi/portmidicontroller.h +++ b/src/controllers/midi/portmidicontroller.h @@ -49,7 +49,8 @@ class PortMidiController : public MidiController { Q_OBJECT public: - PortMidiController(const PmDeviceInfo* inputDeviceInfo, + PortMidiController(const QString& group, + const PmDeviceInfo* inputDeviceInfo, const PmDeviceInfo* outputDeviceInfo, int inputDeviceIndex, int outputDeviceIndex); diff --git a/src/controllers/midi/portmidienumerator.cpp b/src/controllers/midi/portmidienumerator.cpp index 4dbe4bf02998..4c8afe093aec 100644 --- a/src/controllers/midi/portmidienumerator.cpp +++ b/src/controllers/midi/portmidienumerator.cpp @@ -224,6 +224,7 @@ QList PortMidiEnumerator::queryDevices() { } // Search for input devices and pair them with output devices if applicable + int index = 1; for (int i = 0; i < iNumDevices; i++) { const PmDeviceInfo* pDeviceInfo = Pm_GetDeviceInfo(i); VERIFY_OR_DEBUG_ASSERT(pDeviceInfo) { @@ -264,13 +265,19 @@ QList PortMidiEnumerator::queryDevices() { } } + // Generate a group for this controller + QString group = QStringLiteral("[PortMidiController") + + QString::number(index) + QChar(']'); + index++; + // So at this point, we either have an input-only MIDI device // (outputDeviceInfo == NULL) or we've found a matching output MIDI // device (outputDeviceInfo != NULL). //.... so create our (aggregate) MIDI device! PortMidiController* currentDevice = - new PortMidiController(inputDeviceInfo, + new PortMidiController(group, + inputDeviceInfo, outputDeviceInfo, inputDevIndex, outputDevIndex); diff --git a/src/coreservices.cpp b/src/coreservices.cpp index 5eae68f0278e..f1f5a074b2d9 100644 --- a/src/coreservices.cpp +++ b/src/coreservices.cpp @@ -309,7 +309,7 @@ void CoreServices::initialize(QApplication* pApp) { // but do not set up controllers until the end of the application startup // (long) qDebug() << "Creating ControllerManager"; - m_pControllerManager = std::make_shared(pConfig); + m_pControllerManager = std::make_shared(pConfig, m_pEngine.get()); // Inhibit the screensaver if the option is set. (Do it before creating the preferences dialog) int inhibit = pConfig->getValue(ConfigKey("[Config]", "InhibitScreensaver"), -1); diff --git a/src/engine/sync/basesyncablelistener.cpp b/src/engine/sync/basesyncablelistener.cpp index e2c309c6bc4e..bde0ced5b3a0 100644 --- a/src/engine/sync/basesyncablelistener.cpp +++ b/src/engine/sync/basesyncablelistener.cpp @@ -60,6 +60,11 @@ bool BaseSyncableListener::syncDeckExists() const { return true; } } + for (const auto& pSyncable : qAsConst(m_controllerSyncables)) { + if (pSyncable->isSynchronized() && pSyncable->getBaseBpm() > 0) { + return true; + } + } return false; } @@ -96,6 +101,13 @@ void BaseSyncableListener::setMasterBpm(Syncable* pSource, double bpm) { } pSyncable->setMasterBpm(bpm); } + for (const auto& pSyncable : qAsConst(m_controllerSyncables)) { + if (pSyncable.get() == pSource || + !pSyncable->isSynchronized()) { + continue; + } + pSyncable->setMasterBpm(bpm); + } } void BaseSyncableListener::setMasterInstantaneousBpm(Syncable* pSource, double bpm) { @@ -109,6 +121,13 @@ void BaseSyncableListener::setMasterInstantaneousBpm(Syncable* pSource, double b } pSyncable->setInstantaneousBpm(bpm); } + for (const auto& pSyncable : qAsConst(m_controllerSyncables)) { + if (pSyncable.get() == pSource || + !pSyncable->isSynchronized()) { + continue; + } + pSyncable->setInstantaneousBpm(bpm); + } } void BaseSyncableListener::setMasterBeatDistance(Syncable* pSource, double beat_distance) { @@ -122,6 +141,13 @@ void BaseSyncableListener::setMasterBeatDistance(Syncable* pSource, double beat_ } pSyncable->setMasterBeatDistance(beat_distance); } + for (const auto& pSyncable : qAsConst(m_controllerSyncables)) { + if (pSyncable.get() == pSource || + !pSyncable->isSynchronized()) { + continue; + } + pSyncable->setMasterBeatDistance(beat_distance); + } } void BaseSyncableListener::setMasterParams(Syncable* pSource, double beat_distance, @@ -137,6 +163,13 @@ void BaseSyncableListener::setMasterParams(Syncable* pSource, double beat_distan } pSyncable->setMasterParams(beat_distance, base_bpm, bpm); } + for (const auto& pSyncable : qAsConst(m_controllerSyncables)) { + if (pSyncable.get() == pSource || + !pSyncable->isSynchronized()) { + continue; + } + pSyncable->setMasterParams(beat_distance, base_bpm, bpm); + } } void BaseSyncableListener::checkUniquePlayingSyncable() { @@ -155,6 +188,19 @@ void BaseSyncableListener::checkUniquePlayingSyncable() { ++playing_sync_decks; } } + for (const auto& pSyncable : qAsConst(m_controllerSyncables)) { + if (!pSyncable->isSynchronized()) { + continue; + } + + if (pSyncable->isPlaying()) { + if (playing_sync_decks > 0) { + return; + } + unique_syncable = pSyncable.get(); + ++playing_sync_decks; + } + } if (playing_sync_decks == 1) { unique_syncable->notifyOnlyPlayingSyncable(); } diff --git a/src/engine/sync/basesyncablelistener.h b/src/engine/sync/basesyncablelistener.h index ca3376ca83f8..2e371658839d 100644 --- a/src/engine/sync/basesyncablelistener.h +++ b/src/engine/sync/basesyncablelistener.h @@ -1,5 +1,8 @@ #pragma once +#include +#include + #include "engine/sync/syncable.h" #include "preferences/usersettings.h" @@ -21,6 +24,10 @@ class BaseSyncableListener : public SyncableListener { void onCallbackStart(int sampleRate, int bufferSize); void onCallbackEnd(int sampleRate, int bufferSize); + void setControllerSyncables(const QList>& syncables) { + m_controllerSyncables = syncables; + } + // Only for testing. Do not use. Syncable* getSyncableForGroup(const QString& group); Syncable* getMasterSyncable() override { @@ -69,4 +76,5 @@ class BaseSyncableListener : public SyncableListener { // The list of all Syncables registered with BaseSyncableListener via // addSyncableDeck. QList m_syncables; + QList> m_controllerSyncables; }; diff --git a/src/engine/sync/controllersyncable.cpp b/src/engine/sync/controllersyncable.cpp new file mode 100644 index 000000000000..982d0a839839 --- /dev/null +++ b/src/engine/sync/controllersyncable.cpp @@ -0,0 +1,17 @@ +#include "engine/sync/controllersyncable.h" + +ControllerSyncable::ControllerSyncable(const QString& group) + : Syncable(), + m_group(group), + m_syncMode(SYNC_INVALID) { + m_pSyncLeaderEnabled.reset(new ControlPushButton(ConfigKey(m_group, "sync_master"))); + m_pSyncLeaderEnabled->setButtonMode(ControlPushButton::TOGGLE); + m_pSyncLeaderEnabled->connectValueChangeRequest(this, + &ControllerSyncable::slotSyncLeaderEnabledChangeRequest, + Qt::DirectConnection); +} + +void ControllerSyncable::slotSyncLeaderEnabledChangeRequest(double value) { + SyncMode mode = syncModeFromDouble(value); + emit syncModeRequested(mode); +} diff --git a/src/engine/sync/controllersyncable.h b/src/engine/sync/controllersyncable.h new file mode 100644 index 000000000000..c6f8b936e78e --- /dev/null +++ b/src/engine/sync/controllersyncable.h @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include "control/controlpushbutton.h" +#include "engine/sync/syncable.h" + +class ControllerSyncable : public QObject, public Syncable { + Q_OBJECT + + public: + ControllerSyncable(const QString& group); + + const QString& getGroup() const override { + return m_group; + }; + + EngineChannel* getChannel() const override { + return nullptr; + } + + /// Notify a Syncable that their mode has changed. The Syncable must record + /// this mode and return the latest mode in response to getMode(). + void setSyncMode(SyncMode mode) override { + m_syncMode = mode; + m_pSyncLeaderEnabled->setAndConfirm(isMaster(mode)); + } + + /// Must NEVER return a mode that was not set directly via + /// notifySyncModeChanged. + SyncMode getSyncMode() const override { + return m_syncMode; + } + + signals: + void syncModeRequested(SyncMode mode); + + protected slots: + void slotSyncLeaderEnabledChangeRequest(double value); + + protected: + QString m_group; + SyncMode m_syncMode; + + QScopedPointer m_pSyncLeaderEnabled; +}; diff --git a/src/engine/sync/enginesync.cpp b/src/engine/sync/enginesync.cpp index bf27fbd1a725..2711a9c08117 100644 --- a/src/engine/sync/enginesync.cpp +++ b/src/engine/sync/enginesync.cpp @@ -56,6 +56,29 @@ Syncable* EngineSync::pickMaster(Syncable* enabling_syncable) { stopped_deck_count++; } } + for (const auto& pSyncable : qAsConst(m_controllerSyncables)) { + if (pSyncable->getBaseBpm() <= 0.0) { + continue; + } + + if (pSyncable.get() != enabling_syncable) { + if (!pSyncable->isSynchronized()) { + continue; + } + } + + if (pSyncable->isPlaying()) { + if (playing_deck_count == 0) { + first_playing_deck = pSyncable.get(); + } + playing_deck_count++; + } else { + if (stopped_deck_count == 0) { + first_stopped_deck = pSyncable.get(); + } + stopped_deck_count++; + } + } if (playing_deck_count == 1) { return first_playing_deck; @@ -147,6 +170,28 @@ Syncable* EngineSync::findBpmMatchTarget(Syncable* requester) { } } + for (const auto& pOtherSyncable : qAsConst(m_controllerSyncables)) { + if (pOtherSyncable.get() == requester) { + continue; + } + if (pOtherSyncable->getBaseBpm() == 0.0) { + continue; + } + + // If the other deck is playing we stop looking immediately. Otherwise continue looking + // for a playing deck with bpm > 0.0. + if (pOtherSyncable->isPlaying()) { + return pOtherSyncable.get(); + } + + // The target is not playing. If this is the first one we have seen, + // record it. If we never find a playing target, we'll return + // this one as a fallback. + if (!pStoppedTarget && !requester->isPlaying()) { + pStoppedTarget = pOtherSyncable.get(); + } + } + return pStoppedTarget; } @@ -470,5 +515,17 @@ bool EngineSync::otherSyncedPlaying(const QString& group) { othersInSync = true; } } + for (const auto& theSyncable : qAsConst(m_controllerSyncables)) { + bool isSynchonized = theSyncable->isSynchronized(); + if (theSyncable->getGroup() == group) { + if (!isSynchonized) { + return false; + } + continue; + } + if (theSyncable->isPlaying() && isSynchonized) { + othersInSync = true; + } + } return othersInSync; } diff --git a/src/test/controller_mapping_validation_test.cpp b/src/test/controller_mapping_validation_test.cpp index 3c1b4925a037..0063444277cf 100644 --- a/src/test/controller_mapping_validation_test.cpp +++ b/src/test/controller_mapping_validation_test.cpp @@ -27,7 +27,8 @@ void FakeControllerJSProxy::sendShortMsg(unsigned char status, } FakeController::FakeController() - : m_bMidiMapping(false), + : Controller("[FakeController]"), + m_bMidiMapping(false), m_bHidMapping(false) { startEngine(); getScriptEngine()->setTesting(true); diff --git a/src/test/midibeatclockreceivertest.cpp b/src/test/midibeatclockreceivertest.cpp new file mode 100644 index 000000000000..611d8ad38bba --- /dev/null +++ b/src/test/midibeatclockreceivertest.cpp @@ -0,0 +1,67 @@ +#include + +#include "controllers/midi/midibeatclockreceiver.h" +#include "controllers/midi/midimessage.h" +#include "util/duration.h" + +class MidiBeatClockReceiverTest : public testing::Test {}; + +TEST_F(MidiBeatClockReceiverTest, BpmDetection) { + mixxx::MidiBeatClockReceiver beatClockReceiver; + + constexpr auto kQuarterNotesPerSecond = 100 * 24 / 60; + constexpr auto kClockIntervalNanos = + mixxx::Duration::fromNanos(1000000000 / kQuarterNotesPerSecond); + + for (int i = 0; i < 24; i++) { + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * i); + } + + EXPECT_DOUBLE_EQ(100.0, beatClockReceiver.bpm().getValue()); +} + +TEST_F(MidiBeatClockReceiverTest, PhaseDetection) { + mixxx::MidiBeatClockReceiver beatClockReceiver; + EXPECT_DOUBLE_EQ(1.0, beatClockReceiver.beatDistance()); + + constexpr auto kClockIntervalNanos = mixxx::Duration::fromNanos(25000000); + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * 1); + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * 2); + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * 3); + EXPECT_DOUBLE_EQ(0.875, beatClockReceiver.beatDistance()); + + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * 4); + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * 5); + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * 6); + EXPECT_DOUBLE_EQ(0.75, beatClockReceiver.beatDistance()); + + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * 7); + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * 8); + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * 9); + EXPECT_DOUBLE_EQ(0.625, beatClockReceiver.beatDistance()); + + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * 10); + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * 11); + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * 12); + EXPECT_DOUBLE_EQ(0.5, beatClockReceiver.beatDistance()); + + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * 13); + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * 14); + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * 15); + EXPECT_DOUBLE_EQ(0.375, beatClockReceiver.beatDistance()); + + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * 16); + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * 17); + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * 18); + EXPECT_DOUBLE_EQ(0.25, beatClockReceiver.beatDistance()); + + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * 19); + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * 20); + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * 21); + EXPECT_DOUBLE_EQ(0.125, beatClockReceiver.beatDistance()); + + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * 22); + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * 23); + beatClockReceiver.receive(MidiOpCode::MIDI_TIMING_CLK, kClockIntervalNanos * 24); + EXPECT_DOUBLE_EQ(1.0, beatClockReceiver.beatDistance()); +} diff --git a/src/test/midicontrollertest.cpp b/src/test/midicontrollertest.cpp index b8c7b477649a..8939f2d34fa0 100644 --- a/src/test/midicontrollertest.cpp +++ b/src/test/midicontrollertest.cpp @@ -12,7 +12,9 @@ class MockMidiController : public MidiController { public: - explicit MockMidiController(): MidiController() {} + explicit MockMidiController(const QString& group) + : MidiController(group) { + } ~MockMidiController() override { } MOCK_METHOD0(open, int()); @@ -27,7 +29,7 @@ class MockMidiController : public MidiController { class MidiControllerTest : public MixxxTest { protected: void SetUp() override { - m_pController.reset(new MockMidiController()); + m_pController.reset(new MockMidiController("[MockMidiController]")); m_pMapping = std::make_shared(); } diff --git a/src/test/portmidicontroller_test.cpp b/src/test/portmidicontroller_test.cpp index 244cff2f5316..dcb0c3f0fa24 100644 --- a/src/test/portmidicontroller_test.cpp +++ b/src/test/portmidicontroller_test.cpp @@ -15,11 +15,15 @@ using ::testing::SetArrayArgument; class MockPortMidiController : public PortMidiController { public: - MockPortMidiController(const PmDeviceInfo* inputDeviceInfo, + MockPortMidiController( + const QString& group, + const PmDeviceInfo* inputDeviceInfo, const PmDeviceInfo* outputDeviceInfo, int inputDeviceIndex, int outputDeviceIndex) - : PortMidiController(inputDeviceInfo, + : PortMidiController( + group, + inputDeviceInfo, outputDeviceInfo, inputDeviceIndex, outputDeviceIndex) { @@ -78,7 +82,11 @@ class PortMidiControllerTest : public MixxxTest { m_outputDeviceInfo.opened = 0; m_pController.reset(new MockPortMidiController( - &m_inputDeviceInfo, &m_outputDeviceInfo, 0, 0)); + "[MockPortMidiController]", + &m_inputDeviceInfo, + &m_outputDeviceInfo, + 0, + 0)); m_pController->setPortMidiInputDevice(m_mockInput); m_pController->setPortMidiOutputDevice(m_mockOutput); }