-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
MIDI Clock Input #702
MIDI Clock Input #702
Changes from all commits
5d826c9
e46cceb
5518af1
098c92e
02cd418
a9df9a9
b1f93bd
26e4ecd
afdd57b
96936f4
f9ff1ca
8286af1
421edb7
4857f4a
e1ebbb5
eb405df
5cf310b
d038c61
c4edd54
8ea12bf
8530c02
2a0e34a
e6a281a
b20d51b
c3aad6c
66b6c54
1b2a11c
c9be401
9bffb9f
98f313d
e3468de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we consider the first clock tick after start a bar? I think yes. We should drop a note about it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes! Here it is:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please drop a comment about that. |
||
} | ||
|
||
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<qint64>(beat_length)); | ||
m_smoothedBeatTime = m_lastBeatTime + beat_duration; | ||
} else { | ||
m_smoothedBeatTime = timestamp; | ||
} | ||
m_lastBeatTime = timestamp; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this thick a bar? Is there any rule inside the Midi standard to know which clock signal is really the bar? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
what? The midi standard does not communicate measures. Just 24 ticks per quarter. Note that the standard says "quarter", and in music, a "quarter note" does not imply 4/4 time. I'm ok changing the word to "beat" but "PPQ" -- Pulses Per Quarter, is the industry standard term and I think we should stay with the standard: https://www.google.com/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#q=midi+ppq There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, I just wonder if there is a quasi standard for measures since all connected devices have to map their measures to the midi clock ticks. According to https://en.wikipedia.org/wiki/Tempo Quaters per minute is the same as beats per minute. Quarter would be a new term in the Mixxx syncing code so I prefer to stick at "beat". There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes the wording issue worse:
But since we do not use the Midi beats, we can Ignore it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is midi code so I will to stick to midi teminology |
||
|
||
// 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); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This mean value calculation kills all abrupt tempo changes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not really. Midi is not precise enough to handle truly abrupt changes, and midi gear often gets out of sync when the tempo changes quickly, so that behavior is expected. Just google "midi sync problems" sometime. It's an old, imprecise standard. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for you explanations. Can you add that as comment? |
||
} | ||
|
||
// 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should introduce toDoubleMins() |
||
|
||
// 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<double>(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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can return a valid value. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can make a guess, but by the API I've written this situation is invalid and indicates a bad call upstream. |
||
} | ||
if (bpm == 0.0) { | ||
return 0.0; | ||
} | ||
// Get seconds per beat. | ||
const double beat_length = 60.0 / bpm; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. bpm can be 0 |
||
// 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); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
#ifndef MIDISOURCECLOCK_H | ||
#define MIDISOURCECLOCK_H | ||
|
||
#include <QList> | ||
#include <QMutex> | ||
|
||
#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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this can be constexpr as well. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we are not using constexpr thanks to windows |
||
|
||
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This requires locking or atomic access. |
||
} | ||
|
||
// Return the exact recorded time of the last beat pulse. | ||
mixxx::Duration lastBeatTime() const { | ||
QMutexLocker lock(&m_mutex); | ||
return m_lastBeatTime; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This requires locking or atomic access. |
||
} | ||
|
||
// 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to be complete, we need to support MIDI_CONTINUE
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
supporting midi_continue is going to be tricky because right now the midisourceclock handles bpm calculation, but beat fraction information is handled by the calling code. It will take some work to figure out how to freeze the data, then thaw it, because all the timing is based on an absolute clock. For now this will be a todo.
Since this code won't be exposed in Mixxx, I think it's safe to have it be somewhat feature incomplete.