diff --git a/CMakeLists.txt b/CMakeLists.txt index 48d4d7a70b5a..82d9d80c15e3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -429,6 +429,7 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/controllers/midi/midienumerator.cpp src/controllers/midi/midimessage.cpp src/controllers/midi/midioutputhandler.cpp + src/controllers/midi/midisourceclock.cpp src/controllers/midi/midiutils.cpp src/controllers/midi/portmidicontroller.cpp src/controllers/midi/portmidienumerator.cpp @@ -544,6 +545,7 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/engine/sync/enginesync.cpp src/engine/sync/internalclock.cpp src/engine/sync/synccontrol.cpp + src/engine/sync/midimaster.cpp src/errordialoghandler.cpp src/library/analysisfeature.cpp src/library/analysislibrarytablemodel.cpp diff --git a/src/controllers/controllermanager.cpp b/src/controllers/controllermanager.cpp index e93cecc4b732..3a004ad6ad65 100644 --- a/src/controllers/controllermanager.cpp +++ b/src/controllers/controllermanager.cpp @@ -144,7 +144,7 @@ void ControllerManager::slotInitialize() { // Instantiate all enumerators. Enumerators can take a long time to // construct since they interact with host MIDI APIs. - m_enumerators.append(new PortMidiEnumerator()); + m_enumerators.append(new PortMidiEnumerator(m_pConfig)); #ifdef __HSS1394__ m_enumerators.append(new Hss1394Enumerator(m_pConfig)); #endif diff --git a/src/controllers/midi/midicontroller.cpp b/src/controllers/midi/midicontroller.cpp index 76f2fe7a9248..61dce0f860ed 100644 --- a/src/controllers/midi/midicontroller.cpp +++ b/src/controllers/midi/midicontroller.cpp @@ -1,6 +1,7 @@ #include "controllers/midi/midicontroller.h" #include "control/controlobject.h" +#include "control/controlproxy.h" #include "controllers/controllerdebug.h" #include "controllers/defs_controllers.h" #include "controllers/midi/midiutils.h" @@ -11,8 +12,13 @@ #include "util/math.h" #include "util/screensaver.h" -MidiController::MidiController() - : Controller() { +MidiController::MidiController(UserSettingsPointer config) + : Controller(), m_pConfig(config) { + m_pClockBpm = new ControlProxy("[MidiSourceClock]", "bpm", this); + m_pClockLastBeat = new ControlProxy("[MidiSourceClock]", + "last_beat_time", + this); + m_pClockRunning = new ControlProxy("[MidiSourceClock]", "run", this); setDeviceCategory(tr("MIDI Controller")); } @@ -20,6 +26,9 @@ MidiController::~MidiController() { destroyOutputHandlers(); // Don't close the device here. Sub-classes should close the device in their // destructors. + delete m_pClockRunning; + delete m_pClockLastBeat; + delete m_pClockBpm; } ControllerJSProxy* MidiController::jsProxy() { @@ -206,18 +215,25 @@ void MidiController::receivedShortMessage(unsigned char status, unsigned char channel = MidiUtils::channelFromStatus(status); unsigned char opCode = MidiUtils::opCodeFromStatus(status); - // 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) { + controllerDebug(MidiUtils::formatMidiMessage( + getName(), status, control, value, channel, opCode, timestamp)); + + // If MidiSourceClock handles the message, record the updated values and + // no further action is needed. Note that the clock code is active even if + // this device is not master, so if the user changes masters all of the data + // is ready to be used instantly. + if (m_midiSourceClock.handleMessage(status, timestamp)) { + // TODO(owen): Use preferences to determine if we are clock master and only + // set values if so. + // TODO(owen): Enable midi clock handling when UI is finished. + if (false) { + m_pClockBpm->set(m_midiSourceClock.bpm()); + m_pClockLastBeat->set(m_midiSourceClock.smoothedBeatTime().toDoubleNanos()); + m_pClockRunning->set(static_cast(m_midiSourceClock.running())); + } return; } - controllerDebug(MidiUtils::formatMidiMessage(getName(), status, control, value, - channel, opCode, timestamp)); MidiKey mappingKey(status, control); triggerActivity(); @@ -413,7 +429,7 @@ double MidiController::computeValue( newmidivalue = newmidivalue - 128.; } //Apply sensitivity to signed value. FIXME - // if(sensitivity > 0) + // if(sensitivity > 0) // _newmidivalue = _newmidivalue * ((double)sensitivity / 50.); //Apply new value to current value. newmidivalue = prevmidivalue + newmidivalue; diff --git a/src/controllers/midi/midicontroller.h b/src/controllers/midi/midicontroller.h index e72755f4f9ab..c9a638491b6f 100644 --- a/src/controllers/midi/midicontroller.h +++ b/src/controllers/midi/midicontroller.h @@ -5,8 +5,10 @@ #include "controllers/midi/legacymidicontrollermappingfilehandler.h" #include "controllers/midi/midimessage.h" #include "controllers/midi/midioutputhandler.h" +#include "controllers/midi/midisourceclock.h" #include "controllers/softtakeover.h" +class ControlProxy; class DlgControllerLearning; /// MIDI Controller base class @@ -15,10 +17,11 @@ class DlgControllerLearning; /// It must be inherited by a class that implements it on some API. /// /// Note that the subclass' destructor should call close() at a minimum. + class MidiController : public Controller { Q_OBJECT public: - explicit MidiController(); + explicit MidiController(UserSettingsPointer config); ~MidiController() override; ControllerJSProxy* jsProxy() override; @@ -107,6 +110,13 @@ class MidiController : public Controller { LegacyMidiControllerMapping m_mapping; SoftTakeoverCtrl m_st; QList > m_fourteen_bit_queued_mappings; + UserSettingsPointer m_pConfig; + MidiSourceClock m_midiSourceClock; + + // Slaves are cleaned up by QT. + ControlProxy* m_pClockBpm; + ControlProxy* m_pClockLastBeat; + ControlProxy* m_pClockRunning; // So it can access sendShortMsg() friend class MidiOutputHandler; diff --git a/src/controllers/midi/midisourceclock.cpp b/src/controllers/midi/midisourceclock.cpp new file mode 100644 index 000000000000..b6b4c4876fd9 --- /dev/null +++ b/src/controllers/midi/midisourceclock.cpp @@ -0,0 +1,140 @@ +#include "controllers/midi/midisourceclock.h" + +#include "controllers/midi/midimessage.h" +#include "util/math.h" + +bool MidiSourceClock::handleMessage(unsigned char status, + const mixxx::Duration& timestamp) { + // TODO(owen): We need to support MIDI_CONTINUE. + switch (status) { + case MIDI_START: + start(); + return true; + case MIDI_STOP: + stop(); + return true; + case MIDI_TIMING_CLK: + pulse(timestamp); + return true; + default: + return false; + } +} + +void MidiSourceClock::start() { + // Treating MIDI_START as the first downbeat is standard practice: + // http://www.blitter.com/%7Erusstopia/MIDI/%7Ejglatt/tech/midispec/seq.htm + m_bRunning = true; + m_iFilled = 0; + m_iRingBufferPos = 0; +} + +void MidiSourceClock::stop() { + m_bRunning = false; +} + +void MidiSourceClock::pulse(const mixxx::Duration& timestamp) { + // Update the ring buffer and calculate new bpm. Update the last beat time + // if we are on a beat. + + if (!m_bRunning) { + qDebug() + << "MidiSourceClock: Got clock pulse but not started, starting now."; + start(); + } + + // Ringbuffer filling. + // TODO(owen): We should have a ringbuffer convenience class. + m_pulseRingBuffer[m_iRingBufferPos] = timestamp; + m_iRingBufferPos = (m_iRingBufferPos + 1) % kRingBufferSize; + if (m_iFilled < kRingBufferSize) { + ++m_iFilled; + } + + // If this pulse is a beat mark, record it, even if we have very few samples. + if (m_iRingBufferPos % kPulsesPerQuarter == 0) { + QMutexLocker lock(&m_mutex); + if (m_dBpm != 0.0 && m_lastBeatTime.toIntegerNanos() != 0) { + // Calculate the smoothed last beat time from the current bpm + // and the actual last beat time. By not using the last smoothed + // time we prevent drift. + const double beat_length = 60.0 * 1e9 / m_dBpm; + const auto beat_duration = mixxx::Duration::fromNanos(static_cast(beat_length)); + m_smoothedBeatTime = m_lastBeatTime + beat_duration; + } else { + m_smoothedBeatTime = timestamp; + } + m_lastBeatTime = timestamp; + } + + // Figure out the bpm if we have enough samples. + if (m_iFilled > 2) { + mixxx::Duration earlyPulseTime; + if (m_iFilled < kRingBufferSize) { + earlyPulseTime = m_pulseRingBuffer[0]; + } else { + // In a filled ring buffer, the earliest pulse is the next one that + // will get filled. + earlyPulseTime = m_pulseRingBuffer[m_iRingBufferPos]; + } + QMutexLocker lock(&m_mutex); + m_dBpm = calcBpm(earlyPulseTime, timestamp, m_iFilled); + } +} + +// static +double MidiSourceClock::calcBpm(const mixxx::Duration& early_pulse, + const mixxx::Duration& late_pulse, + int pulse_count) { + // Get the elapsed time between the latest pulse and the earliest pulse + // and divide by the number of pulses in the buffer to get bpm. Midi + // clock information is by nature imprecise, and issues such as drift and + // inability to adapt to abrupt tempo changes are well known. We can not + // expect to wring more precision out of an imprecise standard. + + // If we have too few samples, we can't calculate a bpm, so return 0.0. + VERIFY_OR_DEBUG_ASSERT(pulse_count >= 2) { + qWarning() << "MidiSourceClock::calcBpm called with too few pulses"; + return 0.0; + } + + VERIFY_OR_DEBUG_ASSERT(late_pulse >= early_pulse) { + qWarning() << "MidiSourceClock asked to calculate beat fraction but " + << "late_pulse < early_pulse:" << late_pulse << early_pulse; + return 0.0; + } + + const mixxx::Duration elapsed = late_pulse - early_pulse; + const double elapsed_mins = elapsed.toDoubleSeconds() / 60.0; + + // We subtract one since two time values denote a single span of time -- + // so a filled value of 3 indicates 2 pulse periods, etc. + const double bpm = static_cast(pulse_count - 1) / kPulsesPerQuarter / elapsed_mins; + + if (bpm < kMinMidiBpm || bpm > kMaxMidiBpm) { + qWarning() << "MidiSourceClock bpm out of range, returning 0:" << bpm; + return 0; + } + return bpm; +} + +// static +double MidiSourceClock::beatFraction(const mixxx::Duration& last_beat, + const mixxx::Duration& now, + const double bpm) { + VERIFY_OR_DEBUG_ASSERT(now >= last_beat) { + qWarning() << "MidiSourceClock asked to calculate beat fraction but " + << "now < last_beat:" << now << last_beat; + return 0.0; + } + if (bpm == 0.0) { + return 0.0; + } + // Get seconds per beat. + const double beat_length = 60.0 / bpm; + // seconds / secondsperbeat = fraction of beat. + const mixxx::Duration beat_duration = now - last_beat; + const double beat_percent = beat_duration.toDoubleSeconds() / beat_length; + // Ensure values are < 1.0. + return beat_percent - floor(beat_percent); +} diff --git a/src/controllers/midi/midisourceclock.h b/src/controllers/midi/midisourceclock.h new file mode 100644 index 000000000000..614550cb4080 --- /dev/null +++ b/src/controllers/midi/midisourceclock.h @@ -0,0 +1,112 @@ +#ifndef MIDISOURCECLOCK_H +#define MIDISOURCECLOCK_H + +#include +#include + +#include "util/duration.h" + +// MidiSourceClock is not thread-safe, but is thread compatible using ControlObjects. +// The MIDI thread will make calls into MidiSourceClock and then update two Control +// Objects with the current reported BPM and last beat time. The engine thread +// can use those values to call a static function to calculate beat fraction +// at any time in the future. Time values are in nanoseconds for compatibility +// with Time::elapsed(). + +// TODO(owen): MidiSourceClock needs to support MIDI_CONTINUE. This is tricky +// because all of our times are absolute and beatFraction information is not +// stored in this class. Probably the solution is to move beatFraction into +// this class and move away from storing absolute timestamps in the ringbuffer. +class MidiSourceClock { +public: + // The number of midi pulses per quarter note (1 beat in 4/4 time). + static constexpr int kPulsesPerQuarter = 24; + // Minimum allowable calculated bpm. The bpm can still be reported as 0.0 + // if there is no incoming data or if there is a problem with the + // calculation. + static constexpr double kMinMidiBpm = 10.0; + static constexpr double kMaxMidiBpm = 300.0; + +private: + // Some of the tests use the ring buffer size, so keep those test in sync + // with this constant. + static const int kRingBufferSize = kPulsesPerQuarter * 4; + +public: + MidiSourceClock() {} + + // Handle an incoming midi status. Return true if handled. + bool handleMessage(unsigned char status, + const mixxx::Duration& timestamp); + + // Signals MIDI Start Sequence. The MidiSourceClock will reset its beat + // fraction to 0, but the bpm value will be seeded with the last recorded + // value. + void start(); + + // Signals MIDI Stop Sequence. The MidiSourceClock will stop updating its beat + // precentage. Subsequent calls to beatFraction will return valid results + // based on the last recorded beat time and last reported bpm. + void stop(); + + // Signals MIDI Timing Clock. The timing between pulses will be used to + // determine bpm. kPulsesPerQuarter pulses = 1 beat. + void pulse(const mixxx::Duration& timestamp); + + // Return the current BPM. Values are significant to 5 decimal places. + double bpm() const { + QMutexLocker lock(&m_mutex); + return m_dBpm; + } + + // Return the exact recorded time of the last beat pulse. + mixxx::Duration lastBeatTime() const { + QMutexLocker lock(&m_mutex); + return m_lastBeatTime; + } + + // Return a smoothed beat time interpolated from received data. + mixxx::Duration smoothedBeatTime() const { + QMutexLocker lock(&m_mutex); + return m_smoothedBeatTime; + } + + // Calculate instantaneous beat fraction based on provided values. If + // the beat fraction is >= 1.0, the integer value will be sliced off until + // the result is between 0 <= x < 1.0. Can be called from any thread + // since it's static. + static double beatFraction(const mixxx::Duration& last_beat, + const mixxx::Duration& now, + const double bpm); + + // Returns true if the clock is running. A master sync listener should + // always call this to make sure that the beatfraction and bpm are + // valid. + bool running() const { + return m_bRunning; + } + +private: + // Calculate the bpm based on the pulse times and counts. Returns values + // between the min and max allowable bpm, or 0.0 for error conditions. + static double calcBpm(const mixxx::Duration& early_pulse, + const mixxx::Duration& late_pulse, + int pulse_count); + + bool m_bRunning = false; + // It's a hack to say 124 all over the source, but it provides a sane + // baseline in case the midi device is already running when Mixxx starts up. + double m_dBpm = 124.0; + // Reported time of the last beat + mixxx::Duration m_lastBeatTime; + // De-jittered time of the last beat + mixxx::Duration m_smoothedBeatTime; + // Mutex for accessing bpm and last beat time for thread safety. + mutable QMutex m_mutex; + + mixxx::Duration m_pulseRingBuffer[kRingBufferSize]; + int m_iRingBufferPos = 0; + int m_iFilled = 0; +}; + +#endif // MIDISOURCECLOCK_H diff --git a/src/controllers/midi/midiutils.cpp b/src/controllers/midi/midiutils.cpp index 991b48a1abd6..a34212ee7f41 100644 --- a/src/controllers/midi/midiutils.cpp +++ b/src/controllers/midi/midiutils.cpp @@ -113,6 +113,15 @@ QString MidiUtils::formatMidiMessage(const QString& controllerName, QString::number((status & 255)>>4, 16).toUpper(), QString::number(control, 16).toUpper().rightJustified(2,'0'), QString::number(value, 16).toUpper().rightJustified(2,'0')); + case MIDI_START: + return QString("MIDI status 0x%1: Start Sequence") + .arg(QString::number(status, 16).toUpper()); + case MIDI_TIMING_CLK: + return QString("MIDI status 0x%1: Timing Clock") + .arg(QString::number(status, 16).toUpper()); + case MIDI_STOP: + return QString("MIDI status 0x%1: Stop Sequence") + .arg(QString::number(status, 16).toUpper()); default: return QString("%1: %2 status 0x%3") .arg(controllerName, msg2, diff --git a/src/controllers/midi/portmidicontroller.cpp b/src/controllers/midi/portmidicontroller.cpp index d7eea579aa07..135b853e9260 100644 --- a/src/controllers/midi/portmidicontroller.cpp +++ b/src/controllers/midi/portmidicontroller.cpp @@ -4,11 +4,14 @@ #include "controllers/midi/midiutils.h" #include "moc_portmidicontroller.cpp" -PortMidiController::PortMidiController(const PmDeviceInfo* inputDeviceInfo, +PortMidiController::PortMidiController(UserSettingsPointer config, + const PmDeviceInfo* inputDeviceInfo, const PmDeviceInfo* outputDeviceInfo, int inputDeviceIndex, int outputDeviceIndex) - : MidiController(), m_cReceiveMsg_index(0), m_bInSysex(false) { + : MidiController(config), + 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..6a0591b3a4d7 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(UserSettingsPointer config, + 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..771019f57050 100644 --- a/src/controllers/midi/portmidienumerator.cpp +++ b/src/controllers/midi/portmidienumerator.cpp @@ -26,7 +26,8 @@ bool recognizeDevice(const PmDeviceInfo& deviceInfo) { } // namespace -PortMidiEnumerator::PortMidiEnumerator() { +PortMidiEnumerator::PortMidiEnumerator(UserSettingsPointer config) + : m_pConfig(config) { PmError err = Pm_Initialize(); // Based on reading the source, it's not possible for this to fail. if (err != pmNoError) { @@ -270,7 +271,8 @@ QList PortMidiEnumerator::queryDevices() { //.... so create our (aggregate) MIDI device! PortMidiController* currentDevice = - new PortMidiController(inputDeviceInfo, + new PortMidiController(m_pConfig, + inputDeviceInfo, outputDeviceInfo, inputDevIndex, outputDevIndex); diff --git a/src/controllers/midi/portmidienumerator.h b/src/controllers/midi/portmidienumerator.h index 21548c90dd3a..652d177ca5e3 100644 --- a/src/controllers/midi/portmidienumerator.h +++ b/src/controllers/midi/portmidienumerator.h @@ -6,13 +6,14 @@ class PortMidiEnumerator : public MidiEnumerator { Q_OBJECT public: - PortMidiEnumerator(); + explicit PortMidiEnumerator(UserSettingsPointer config); ~PortMidiEnumerator() override; QList queryDevices() override; private: QList m_devices; + UserSettingsPointer m_pConfig; }; // For testing. diff --git a/src/engine/sync/basesyncablelistener.cpp b/src/engine/sync/basesyncablelistener.cpp index e2c309c6bc4e..a05a3707e3ad 100644 --- a/src/engine/sync/basesyncablelistener.cpp +++ b/src/engine/sync/basesyncablelistener.cpp @@ -3,16 +3,19 @@ #include #include "engine/sync/internalclock.h" +#include "engine/sync/midimaster.h" namespace { const QString kInternalClockGroup = QStringLiteral("[InternalClock]"); +const QString kMidiMasterClockGroup = QStringLiteral("[MidiSourceClock]"); } // anonymous namespace BaseSyncableListener::BaseSyncableListener(UserSettingsPointer pConfig) : m_pConfig(pConfig), m_pInternalClock(new InternalClock(kInternalClockGroup, this)), + m_pMidiSourceClock(new MidiMasterClock(kMidiMasterClockGroup, this)), m_pMasterSyncable(nullptr) { qRegisterMetaType("SyncMode"); m_pInternalClock->setMasterBpm(124.0); @@ -22,6 +25,7 @@ BaseSyncableListener::~BaseSyncableListener() { // We use the slider value because that is never set to 0.0. m_pConfig->set(ConfigKey("[InternalClock]", "bpm"), ConfigValue( m_pInternalClock->getBpm())); + delete m_pMidiSourceClock; delete m_pInternalClock; } @@ -35,9 +39,11 @@ void BaseSyncableListener::addSyncableDeck(Syncable* pSyncable) { void BaseSyncableListener::onCallbackStart(int sampleRate, int bufferSize) { m_pInternalClock->onCallbackStart(sampleRate, bufferSize); + m_pMidiSourceClock->onCallbackStart(sampleRate, bufferSize); } void BaseSyncableListener::onCallbackEnd(int sampleRate, int bufferSize) { + m_pMidiSourceClock->onCallbackEnd(sampleRate, bufferSize); m_pInternalClock->onCallbackEnd(sampleRate, bufferSize); } diff --git a/src/engine/sync/basesyncablelistener.h b/src/engine/sync/basesyncablelistener.h index ca3376ca83f8..bdc56b8153d0 100644 --- a/src/engine/sync/basesyncablelistener.h +++ b/src/engine/sync/basesyncablelistener.h @@ -3,8 +3,9 @@ #include "engine/sync/syncable.h" #include "preferences/usersettings.h" -class InternalClock; class EngineChannel; +class InternalClock; +class MidiMasterClock; /// BaseSyncableListener is a SyncableListener used by EngineSync. /// It provides some foundational functionality for distributing @@ -64,6 +65,8 @@ class BaseSyncableListener : public SyncableListener { UserSettingsPointer m_pConfig; // The InternalClock syncable. InternalClock* m_pInternalClock; + // Midi master clock. + MidiMasterClock* m_pMidiSourceClock; // The current Syncable that is the master. Syncable* m_pMasterSyncable; // The list of all Syncables registered with BaseSyncableListener via diff --git a/src/engine/sync/midimaster.cpp b/src/engine/sync/midimaster.cpp new file mode 100644 index 000000000000..4549b0aaea12 --- /dev/null +++ b/src/engine/sync/midimaster.cpp @@ -0,0 +1,141 @@ +#include "engine/sync/midimaster.h" + +#include + +#include "control/control.h" +#include "control/controllinpotmeter.h" +#include "control/controlpushbutton.h" +#include "controllers/midi/midisourceclock.h" +#include "engine/sync/enginesync.h" +#include "preferences/configobject.h" +#include "util/time.h" + +MidiMasterClock::MidiMasterClock( + const QString& group, SyncableListener* pEngineSync) + : m_group(group), + m_pEngineSync(pEngineSync), + m_pSyncMasterEnabled(std::make_unique( + ConfigKey(group, "sync_master"))), + m_pMidiSourceClockBpm( + std::make_unique(ConfigKey(group, "bpm"))), + m_pMidiSourceClockLastBeatTime(std::make_unique( + ConfigKey(group, "last_beat_time"))), + m_pMidiSourceClockBeatDistance(std::make_unique( + ConfigKey(group, "beat_distance"))), + m_pMidiSourceClockRunning( + std::make_unique(ConfigKey(group, "run"))), + m_pMidiSourceClockSyncAdjust(std::make_unique( + ConfigKey(group, "sync_adjust"), + -.5, + .5, + 0.1, + 0.01, + /*allow oob*/ true)), + m_mode(SYNC_NONE) { + m_pSyncMasterEnabled->setButtonMode(ControlPushButton::TOGGLE); + m_pSyncMasterEnabled->connectValueChangeRequest( + this, &MidiMasterClock::slotSyncMasterEnabledChangeRequest, Qt::DirectConnection); +} + +MidiMasterClock::~MidiMasterClock(){}; + +void MidiMasterClock::notifySyncModeChanged(SyncMode mode) { + // Syncable has absolutely no say in the matter. This is what EngineSync + // requires. Bypass confirmation by using setAndConfirm. + m_mode = mode; + m_pSyncMasterEnabled->setAndConfirm(mode == SYNC_MASTER_SOFT); +} + +void MidiMasterClock::notifyOnlyPlayingSyncable() { + // No action necessary. +} + +void MidiMasterClock::requestSync() { + // TODO: Implement this? +} + +void MidiMasterClock::requestSyncPhase() { + // TODO: maybe tell MidiSourceClock to reset which tick is the beat tick?? + // but really, it's a read-only clock. +} + +bool MidiMasterClock::isPlaying() const { + // midi running / not running state + return m_pMidiSourceClockRunning->get(); +} + +void MidiMasterClock::slotSyncMasterEnabledChangeRequest(double state) { + bool currentlyMaster = getSyncMode() == SYNC_MASTER_SOFT; + + if (state > 0.0) { + if (currentlyMaster) { + // Already master. + return; + } + m_pEngineSync->requestSyncMode(this, SYNC_MASTER_SOFT); + } else { + // TODO: midi follower (clock out?) + m_pEngineSync->requestSyncMode(this, SYNC_NONE); + } +} + +double MidiMasterClock::getBeatDistance() const { + const mixxx::Duration last_beat = mixxx::Duration::fromNanos( + static_cast(m_pMidiSourceClockLastBeatTime->get())); + double raw_percent = MidiSourceClock::beatFraction( + last_beat, mixxx::Time::elapsed(), m_pMidiSourceClockBpm->get()); + raw_percent += m_pMidiSourceClockSyncAdjust->get(); + // Fix beat loop-around. + return raw_percent - floor(raw_percent); +} + +void MidiMasterClock::setMasterBeatDistance(double beatDistance) { + // Midi master is read-only. + Q_UNUSED(beatDistance); +} + +double MidiMasterClock::getBaseBpm() const { + return m_pMidiSourceClockBpm->get(); +} + +void MidiMasterClock::setMasterBaseBpm(double bpm) { + // Midi master is read-only. + Q_UNUSED(bpm) +} + +double MidiMasterClock::getBpm() const { + return m_pMidiSourceClockBpm->get(); +} + +void MidiMasterClock::setMasterBpm(double bpm) { + // Midi master is read-only. + Q_UNUSED(bpm); +} + +void MidiMasterClock::setInstantaneousBpm(double bpm) { + // Midi master is read-only. + Q_UNUSED(bpm); +} + +void MidiMasterClock::setMasterParams(double beatDistance, double baseBpm, double bpm) { + // Midi master is read-only. + Q_UNUSED(beatDistance); + Q_UNUSED(baseBpm); + Q_UNUSED(bpm); +} + +void MidiMasterClock::onCallbackStart(int sampleRate, int bufferSize) { + Q_UNUSED(sampleRate) + Q_UNUSED(bufferSize) + double bpm = getBpm(); + m_pEngineSync->notifyInstantaneousBpmChanged(this, bpm); + m_pEngineSync->notifyBpmChanged(this, bpm); +} + +void MidiMasterClock::onCallbackEnd(int sampleRate, int bufferSize) { + Q_UNUSED(sampleRate) + Q_UNUSED(bufferSize) + double beat_distance = getBeatDistance(); + m_pMidiSourceClockBeatDistance->set(beat_distance); + m_pEngineSync->notifyBeatDistanceChanged(this, beat_distance); +} diff --git a/src/engine/sync/midimaster.h b/src/engine/sync/midimaster.h new file mode 100644 index 000000000000..ce68307c2713 --- /dev/null +++ b/src/engine/sync/midimaster.h @@ -0,0 +1,82 @@ +// MidiMasterClock provides a sync master from external midi clock input. +// It reads control objects set by midicontroller.cpp, which uses MidiSourceClock.cpp. +// It does not provide midi clock output. + +#ifndef MIDIMASTER_H +#define MIDIMASTER_H + +#include +#include +#include + +#include "engine/channels/enginechannel.h" +#include "engine/sync/clock.h" +#include "engine/sync/syncable.h" + +class ControlLinPotmeter; +class ControlObject; +class ControlPushButton; +class EngineSync; + +class MidiMasterClock : public QObject, public Clock, public Syncable { + Q_OBJECT + public: + MidiMasterClock(const QString& group, SyncableListener* pEngineSync); + virtual ~MidiMasterClock(); + + const QString& getGroup() const { + return m_group; + } + EngineChannel* getChannel() const { + return nullptr; + } + + void notifySyncModeChanged(SyncMode mode); + void notifyOnlyPlayingSyncable(); + void requestSync(); + void requestSyncPhase(); + void setSyncMode(SyncMode mode) { + // TODO: Does this implementation suffice? + m_mode = mode; + } + SyncMode getSyncMode() const { + return m_mode; + } + + bool isPlaying() const; + + double getBeatDistance() const; + void setMasterBeatDistance(double beatDistance); + + double getBaseBpm() const; + void setMasterBaseBpm(double); + void setMasterBpm(double bpm); + double getBpm() const; + void setInstantaneousBpm(double bpm); + void setMasterParams(double beatDistance, double baseBpm, double bpm); + + void onCallbackStart(int sampleRate, int bufferSize); + void onCallbackEnd(int sampleRate, int bufferSize); + + private slots: + void slotSyncMasterEnabledChangeRequest(double state); + + private: + QString m_group; + SyncableListener* m_pEngineSync; + std::unique_ptr m_pSyncMasterEnabled; + + std::unique_ptr m_pMidiSourceClockBpm; + std::unique_ptr m_pMidiSourceClockLastBeatTime; + std::unique_ptr m_pMidiSourceClockBeatDistance; + // Indicates if the midi clock is active or stopped. + std::unique_ptr m_pMidiSourceClockRunning; + // Since there may be differences in latency between other midi devices and + // Mixxx, allow for manual adjustment of the beat percentage value. + std::unique_ptr m_pMidiSourceClockSyncAdjust; + + SyncMode m_mode; + double m_dOldBpm; +}; + +#endif // MIDIMASTER_H diff --git a/src/test/enginesynctest.cpp b/src/test/enginesynctest.cpp index 6da40ee9f54d..34c2f03bfbf5 100644 --- a/src/test/enginesynctest.cpp +++ b/src/test/enginesynctest.cpp @@ -60,10 +60,10 @@ class EngineSyncTest : public MockedEngineBackendTest { } void assertSyncOff(const QString& group) { - if (group == m_sInternalClockGroup) { + if (group == m_sInternalClockGroup || group == m_sMidiSourceClockGroup) { ASSERT_EQ(0, ControlObject::getControl( - ConfigKey(m_sInternalClockGroup, "sync_master")) + ConfigKey(group, "sync_master")) ->get()); } else { ASSERT_EQ(SYNC_NONE, @@ -354,6 +354,29 @@ TEST_F(EngineSyncTest, InternalMasterSetFollowerSliderMoves) { ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get()); } +TEST_F(EngineSyncTest, MidiSourceClockMasterSetSlaveSliderMoves) { + // TODO: refactor this with the test above -- but we have to be careful to + // reset state between passes. + // If midi clock is master, and we turn on a slave, the slider should move. + auto pButtonMasterSyncMidi = std::make_unique( + m_sMidiSourceClockGroup, "sync_master"); + pButtonMasterSyncMidi->slotSet(1); + auto pMasterSyncSlider = std::make_unique(m_sMidiSourceClockGroup, "bpm"); + pMasterSyncSlider->set(100.0); + + // Set the file bpm of channel 1 to 160bpm. + auto pFileBpm1 = std::make_unique(m_sGroup1, "file_bpm"); + pFileBpm1->set(80.0); + + auto pButtonMasterSync1 = std::make_unique(m_sGroup1, "sync_mode"); + pButtonMasterSync1->slotSet(SYNC_FOLLOWER); + ProcessBuffer(); + + EXPECT_DOUBLE_EQ(getRateSliderValue(1.25), + ControlObject::getControl(ConfigKey(m_sGroup1, "rate"))->get()); + EXPECT_DOUBLE_EQ(100.0, ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get()); +} + TEST_F(EngineSyncTest, AnySyncDeckSliderStays) { // If there exists a sync deck, even if it's not playing, don't change the // master BPM if a new deck enables sync. @@ -1052,6 +1075,25 @@ TEST_F(EngineSyncTest, EnableOneDeckSliderUpdates) { ->get()); } +TEST_F(EngineSyncTest, EnableMidiSliderUpdates) { + // If we enable midi to be master, the internal slider should immediately update. + auto pButtonMasterSyncMidi = std::make_unique( + m_sMidiSourceClockGroup, "sync_master"); + ControlObject::getControl(ConfigKey(m_sMidiSourceClockGroup, "bpm"))->set(132.0); + + // Set midi to sync master. + pButtonMasterSyncMidi->slotSet(1.0); + ProcessBuffer(); + + // Midi should still be master. + ASSERT_TRUE(isExplicitMaster(m_sMidiSourceClockGroup)); + + // Internal clock rate should be set. + EXPECT_DOUBLE_EQ(132.0, + ControlObject::getControl(ConfigKey(m_sInternalClockGroup, "bpm")) + ->get()); +} + TEST_F(EngineSyncTest, SyncToNonSyncDeck) { // If deck 1 is playing, and deck 2 presses sync, deck 2 should sync to deck 1 even if // deck 1 is not a sync deck. @@ -2090,6 +2132,38 @@ TEST_F(EngineSyncTest, MasterBpmNeverZero) { ControlObject::getControl(ConfigKey(m_sInternalClockGroup, "bpm"))->get()); } +TEST_F(EngineSyncTest, MidiRateChangeMovesSlider) { + // If the midi rate changes, the rate sliders should change on followers. + auto pButtonMasterSyncMidi = std::make_unique( + m_sMidiSourceClockGroup, "sync_master"); + pButtonMasterSyncMidi->slotSet(1); + auto pMidiSlider = std::make_unique(m_sMidiSourceClockGroup, "bpm"); + pMidiSlider->set(100.0); + + auto pFileBpm1 = std::make_unique(m_sMidiSourceClockGroup, "file_bpm"); + pFileBpm1->set(80.0); + + auto pButtonMasterSync1 = std::make_unique(m_sMidiSourceClockGroup, "sync_mode"); + pButtonMasterSync1->slotSet(SYNC_FOLLOWER); + ProcessBuffer(); + + EXPECT_DOUBLE_EQ(getRateSliderValue(1.25), + ControlObject::getControl(ConfigKey(m_sGroup1, "rate"))->get()); + EXPECT_DOUBLE_EQ(100.0, ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get()); + + pMidiSlider->set(120.0); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(getRateSliderValue(1.5), + ControlObject::getControl(ConfigKey(m_sGroup1, "rate"))->get()); + EXPECT_DOUBLE_EQ(120.0, ControlObject::getControl(ConfigKey(m_sGroup1, "bpm"))->get()); + + // Setting the deck slider shouldn't work + ControlObject::getControl(ConfigKey(m_sGroup1, "rate"))->set(getRateSliderValue(1.0)); + ProcessBuffer(); + EXPECT_DOUBLE_EQ(getRateSliderValue(1.5), + ControlObject::getControl(ConfigKey(m_sGroup1, "rate"))->get()); +} + TEST_F(EngineSyncTest, ZeroBpmNaturalRate) { // If a track has a zero bpm and a bad beatgrid, make sure the rate // doesn't end up something crazy when sync is enabled.. diff --git a/src/test/midiclocktest.cpp b/src/test/midiclocktest.cpp new file mode 100644 index 000000000000..b5f203c0b2e6 --- /dev/null +++ b/src/test/midiclocktest.cpp @@ -0,0 +1,104 @@ +#include + +#include + +#include "controllers/midi/midisourceclock.h" +#include "test/mixxxtest.h" + +class MidiSourceClockTest : public MixxxTest { + protected: + virtual void SetUp() { + m_pMidiSourceClock.reset(new MidiSourceClock()); + } + QScopedPointer m_pMidiSourceClock; +}; + +TEST_F(MidiSourceClockTest, SimpleTest) { + // Simple test for pulsing at a very steady rate and then getting the bpm. + + m_pMidiSourceClock->start(); + + // Nanos per pulse for 124 bpm midi pulses: + // 60secs per min / 124bpm / kPulsesPerQuarter ppb * 1e9 nps + // ppb = pulses per beat + // nps = nanos per second + const double nanos_per_pulse = + 60.0 / 124 / MidiSourceClock::kPulsesPerQuarter * 1e9; + // This test should end before the ringbuffer is exhausted and not on + // a beat. + mixxx::Duration now; + for (double t = 0; t < MidiSourceClock::kPulsesPerQuarter * 2.5; ++t) { + now = mixxx::Duration::fromNanos(t * nanos_per_pulse); + m_pMidiSourceClock->pulse(now); + } + + EXPECT_FLOAT_EQ(124.0, m_pMidiSourceClock->bpm()); + // position 60 is 12 pulses after 48, so 1/2 beat. + EXPECT_FLOAT_EQ(0.5, m_pMidiSourceClock->beatFraction( + m_pMidiSourceClock->lastBeatTime(), now, + m_pMidiSourceClock->bpm())); + // No jitter, so no difference in smoothed result. + EXPECT_FLOAT_EQ(0.5, m_pMidiSourceClock->beatFraction( + m_pMidiSourceClock->smoothedBeatTime(), now, + m_pMidiSourceClock->bpm())); +} + +TEST_F(MidiSourceClockTest, RingBufferTest) { + m_pMidiSourceClock->start(); + + // Nanos per pulse for 124 bpm midi pulses. + const double nanos_per_pulse = + 60.0 / 124 / MidiSourceClock::kPulsesPerQuarter * 1e9; + // This test should exhaust the ringbuffer at least once, and end on + // a beat. Any multiple of pulses per quarter is a beat. + mixxx::Duration now; + for (double t = 0; t < MidiSourceClock::kPulsesPerQuarter * 6; ++t) { + now = mixxx::Duration::fromNanos(t * nanos_per_pulse); + m_pMidiSourceClock->pulse(now); + } + + EXPECT_FLOAT_EQ(124.0, m_pMidiSourceClock->bpm()); + EXPECT_FLOAT_EQ(0.0, m_pMidiSourceClock->beatFraction( + m_pMidiSourceClock->lastBeatTime(), now, + m_pMidiSourceClock->bpm())); + // Rounding error should not be very significant. + EXPECT_LT(fabs(0.0 - m_pMidiSourceClock->beatFraction( + m_pMidiSourceClock->smoothedBeatTime(), now, + m_pMidiSourceClock->bpm())), 1e-7); +} + +TEST_F(MidiSourceClockTest, JitterBufferTest) { + // smoothed downbeat and bpm should not drift very much when there is + // jitter in the signal. + m_pMidiSourceClock->start(); + const double nanos_per_pulse = + 60.0 / 124 / MidiSourceClock::kPulsesPerQuarter * 1e9; + mixxx::Duration now; + for (double t = 0; t < MidiSourceClock::kPulsesPerQuarter * 10000; ++t) { + now = mixxx::Duration::fromNanos(t * nanos_per_pulse); + // Test with up to 10ms error. + const auto error = mixxx::Duration::fromMillis(qrand() % 10 - 5); + //const mixxx::Duration error; + m_pMidiSourceClock->pulse(now + error); + } + + // Values will not be exact because of the jitter -- but they should + // be fairly close. This test is inherently flaky because we use random + // numbers, but these tolerances are large enough that repeating the test + // 10000 times results in no failures. + EXPECT_LT(fabs(124.0 - m_pMidiSourceClock->bpm()), 1.0); + + // Make sure now is well ahead of the last beat time (could be less due + // to the jitter.). The .5 ensures we're at half a beat. + now += mixxx::Duration::fromNanos( + 10.5 * MidiSourceClock::kPulsesPerQuarter * nanos_per_pulse); + + const double frac_last = m_pMidiSourceClock->beatFraction( + m_pMidiSourceClock->lastBeatTime(), now, + m_pMidiSourceClock->bpm()); + const double frac_smoothed = m_pMidiSourceClock->beatFraction( + m_pMidiSourceClock->smoothedBeatTime(), now, + m_pMidiSourceClock->bpm()); + EXPECT_LT(fabs(0.5 - frac_last), 0.1); + EXPECT_LT(fabs(0.5 - frac_smoothed), 0.1); +} diff --git a/src/test/midicontrollertest.cpp b/src/test/midicontrollertest.cpp index 41b9e9cb7449..dbd5221c2b7b 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(UserSettingsPointer config) + : MidiController(config) { + } ~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(m_pConfig)); } void addMapping(const MidiInputMapping& mapping) { diff --git a/src/test/portmidicontroller_test.cpp b/src/test/portmidicontroller_test.cpp index b443d52fac22..6520a849589e 100644 --- a/src/test/portmidicontroller_test.cpp +++ b/src/test/portmidicontroller_test.cpp @@ -19,7 +19,8 @@ class MockPortMidiController : public PortMidiController { const PmDeviceInfo* outputDeviceInfo, int inputDeviceIndex, int outputDeviceIndex) - : PortMidiController(inputDeviceInfo, + : PortMidiController(UserSettingsPointer(), + inputDeviceInfo, outputDeviceInfo, inputDeviceIndex, outputDeviceIndex) { diff --git a/src/test/signalpathtest.cpp b/src/test/signalpathtest.cpp index ed08f9e518c1..512aad4311f9 100644 --- a/src/test/signalpathtest.cpp +++ b/src/test/signalpathtest.cpp @@ -2,6 +2,7 @@ const QString BaseSignalPathTest::m_sMasterGroup = QStringLiteral("[Master]"); const QString BaseSignalPathTest::m_sInternalClockGroup = QStringLiteral("[InternalClock]"); +const QString BaseSignalPathTest::m_sMidiSourceClockGroup = QStringLiteral("[MidiSourceClock]"); // these names need to match PlayerManager::groupForDeck and friends const QString BaseSignalPathTest::m_sGroup1 = QStringLiteral("[Channel1]"); const QString BaseSignalPathTest::m_sGroup2 = QStringLiteral("[Channel2]"); diff --git a/src/test/signalpathtest.h b/src/test/signalpathtest.h index bbb057b630d1..f69f83353e54 100644 --- a/src/test/signalpathtest.h +++ b/src/test/signalpathtest.h @@ -232,6 +232,7 @@ class BaseSignalPathTest : public MixxxTest { static const QString m_sMasterGroup; static const QString m_sInternalClockGroup; + static const QString m_sMidiSourceClockGroup; static const QString m_sGroup1; static const QString m_sGroup2; static const QString m_sGroup3; diff --git a/src/util/duration.h b/src/util/duration.h index 4e48beabeebf..c47c1d028d18 100644 --- a/src/util/duration.h +++ b/src/util/duration.h @@ -264,6 +264,9 @@ class Duration : public DurationBase { } private: + /// If your code is trying to access this constructor, your code was probably + /// written for the old API. Most likely you want to use + /// Duration::fromNanos(val) instead. explicit constexpr Duration(qint64 durationNanos) : DurationBase(durationNanos) { }