Skip to content
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

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5d826c9
MidiClock: a class that listens to incoming midi clock messages
ywwg Jun 14, 2015
e46cceb
Enable -std=c++11 support.
rryan Jun 4, 2015
5518af1
MidiClock: Actually commit the source files
ywwg Jun 14, 2015
098c92e
Midi Clock master works
ywwg Jun 15, 2015
02cd418
MidiClock: remove atomic / threading stuff since it's not needed..
ywwg Jun 15, 2015
a9df9a9
MidiClock: Add indication that midi is running or not.
ywwg Jun 15, 2015
b1f93bd
MidiClock: Add a sync tweak button to adjust for midi offset
ywwg Jun 16, 2015
26e4ecd
Merge branch '1.12' into midi-master
ywwg Jun 16, 2015
afdd57b
MidiClock: Fix midi not updating follower rate sliders
ywwg Jun 18, 2015
96936f4
LateNight: Fix effect unit getting too big when fourth unit is populated
ywwg Jun 21, 2015
f9ff1ca
move midi sync to below right decks
ywwg Jun 21, 2015
8286af1
MidiClock: Always update bpm on every callback
ywwg Jun 21, 2015
421edb7
Merge branch 'master' into midi-master
ywwg Sep 1, 2015
4857f4a
MidiMaster: Cleanup, clamp bpm calculation to a range
ywwg Sep 2, 2015
e1ebbb5
MidiMaster: Rename MidiClock to MidiSourceClock
ywwg Sep 2, 2015
eb405df
Merge branch 'master' into midi-master
ywwg Sep 2, 2015
5cf310b
Merge branch 'master' into midi-master
ywwg Nov 22, 2015
d038c61
Merge branch 'master' into midi-master
ywwg Nov 22, 2015
c4edd54
MidiMaster: Address notes
ywwg Nov 22, 2015
8ea12bf
MidiMaster: Propagate ConfigObject down to midicontroller where it's …
ywwg Nov 22, 2015
8530c02
Merge branch 'master' into midi-master
ywwg Jan 30, 2016
2a0e34a
Merge branch 'master' into midi-master
ywwg Feb 22, 2016
e6a281a
Midi Master: Update with latest trunk API changes
ywwg Feb 28, 2016
b20d51b
Merge branch 'master' into midi-master
ywwg Feb 28, 2016
c3aad6c
Midi Master: Use external timestamps
ywwg Mar 4, 2016
66b6c54
Midi Master: Fix tests, which were so tolerant they couldn't fail
ywwg Mar 4, 2016
1b2a11c
Midi Master: Disable setting of midi master until UI is done
ywwg Mar 5, 2016
c9be401
Merge branch 'master' into midi-master
ywwg May 4, 2016
9bffb9f
Merge branch 'master' into midi-master
ywwg Nov 19, 2016
98f313d
Merge branch 'midi-master' of https://github.com/ywwg/mixxx into midi…
Holzhaus Feb 6, 2021
e3468de
Merge pull request #11 from Holzhaus/midi-clock-input
ywwg Feb 7, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build/depends.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,7 @@ def sources(self, build):
"engine/sync/enginesync.cpp",
"engine/sync/synccontrol.cpp",
"engine/sync/internalclock.cpp",
"engine/sync/midimaster.cpp",

"engine/engineworker.cpp",
"engine/engineworkerscheduler.cpp",
Expand Down Expand Up @@ -671,6 +672,7 @@ def sources(self, build):
"controllers/midi/midicontrollerpresetfilehandler.cpp",
"controllers/midi/midienumerator.cpp",
"controllers/midi/midioutputhandler.cpp",
"controllers/midi/midisourceclock.cpp",
"controllers/softtakeover.cpp",

"main.cpp",
Expand Down
92 changes: 82 additions & 10 deletions res/skins/LateNight/decks_right.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,88 @@
</Connection>
</WidgetGroup>
<WidgetGroup>
<ObjectName>NoBorder</ObjectName>
<Layout>horizontal</Layout>
<SizePolicy>me,me</SizePolicy>
<MinimumSize>0,0</MinimumSize>
<MaximumSize>,70</MaximumSize>
<Children/>
<Connection>
<ConfigKey>[PreviewDeck],show_previewdeck</ConfigKey>
<BindProperty>visible</BindProperty>
</Connection>
<Layout>vertical</Layout>
<SizePolicy>me,min</SizePolicy>
<MaximumSize>,50</MaximumSize>
<MinimumSize>100,50</MinimumSize>
<Children>
<WidgetGroup>
<ObjectName>PreviewDeck</ObjectName>
<Layout>horizontal</Layout>
<SizePolicy>min, min</SizePolicy>
<MinimumSize>100,40</MinimumSize>
<MaximumSize>,50</MaximumSize>
<Children>
<WidgetGroup>
<ObjectName>MasterSync</ObjectName>
<SizePolicy>f,me</SizePolicy>
<Size>50,</Size>
<Layout>vertical</Layout>
<Children>
<PushButton>
<TooltipId>sync_master</TooltipId>
<Style></Style>
<NumberStates>2</NumberStates>
<State>
<Number>0</Number>
<Pressed>btn_mastersync_master_on.png</Pressed>
<Unpressed>btn_mastersync_master_off.png</Unpressed>
</State>
<State>
<Number>1</Number>
<Pressed>btn_mastersync_master_on.png</Pressed>
<Unpressed>btn_mastersync_master_on.png</Unpressed>
</State>
<Pos>4,4</Pos>
<Connection>
<ConfigKey>[MidiSourceClock],sync_master</ConfigKey>
<ButtonState>LeftButton</ButtonState>
</Connection>
</PushButton>
<NumberBpm>
<TooltipId>visual_bpm</TooltipId>
<Style>QLabel { font: bold 18px/21px Lucida Grande, Lucida Sans
Unicode, Arial, Verdana, sans-serif;
background-color:
transparent; color: #EECE33; text-align: left;padding-left:
1px;
}
</Style>
<Channel>1</Channel>
<NumberOfDigits>2</NumberOfDigits>
<Connection>
<ConfigKey>[MidiSourceClock],bpm</ConfigKey>
</Connection>
</NumberBpm>
</Children>
</WidgetGroup>
<PushButton>
<ObjectName>GuiToggleButton</ObjectName>
<NumberStates>2</NumberStates>
<Size>40f,20f</Size>
<State>
<Number>0</Number>
<Text>RUN</Text>
</State>
<State>
<Number>1</Number>
<Text>RUN</Text>
</State>
<Connection>
<ConfigKey>[MidiSourceClock],play</ConfigKey>
<ButtonState>NoButton</ButtonState>
</Connection>
</PushButton>
<Template src="skin:knob_sized.xml">
<SetVariable name="width">24</SetVariable>
<SetVariable name="height">20</SetVariable>
<SetVariable name="group">[MidiSourceClock]</SetVariable>
<SetVariable name="control">sync_adjust</SetVariable>
<SetVariable name="label">offset</SetVariable>
</Template>
</Children>
</WidgetGroup>
</Children>
</WidgetGroup>
<WidgetGroup>
<Layout>horizontal</Layout>
Expand Down
4 changes: 2 additions & 2 deletions res/skins/LateNight/mixer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
</State>
<Pos>4,4</Pos>
<Connection>
<ConfigKey>[InternalClock],sync_master</ConfigKey>
<ConfigKey>[MidiSourceClock],sync_master</ConfigKey>
<ButtonState>LeftButton</ButtonState>
</Connection>
</PushButton>
Expand All @@ -54,7 +54,7 @@
<Channel>1</Channel>
<NumberOfDigits>2</NumberOfDigits>
<Connection>
<ConfigKey>[InternalClock],bpm</ConfigKey>
<ConfigKey>[MidiSourceClock],bpm</ConfigKey>
</Connection>
</NumberBpm>
</Children>
Expand Down
25 changes: 24 additions & 1 deletion src/controllers/midi/midicontroller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@
#include "controllers/midi/midiutils.h"
#include "controllers/defs_controllers.h"
#include "controlobject.h"
#include "controlobjectslave.h"
#include "errordialoghandler.h"
#include "playermanager.h"
#include "util/math.h"

MidiController::MidiController()
: Controller() {
: Controller(), m_midiSourceClock(&m_mixxxClock) {
setDeviceCategory(tr("MIDI Controller"));
m_pClockBpm.reset(new ControlObjectSlave("[MidiSourceClock]", "bpm"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why you do not use the constructor in the init list?

m_pClockLastBeat.reset(
new ControlObjectSlave("[MidiSourceClock]", "last_beat_time"));
m_pClockRunning.reset(new ControlObjectSlave("[MidiSourceClock]", "play"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just parent the ControlObjectSlave to "this". Qt does the clean up.

}

MidiController::~MidiController() {
Expand Down Expand Up @@ -193,6 +198,15 @@ QString formatMidiMessage(unsigned char status, unsigned char control, unsigned
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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MIDI_TIMING_CLK: What's that? What about "MIDI_TICK"?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO we should unify the wording towards the Midi standard and replace all "pulse" and "tick" with just
"clock"
https://en.wikipedia.org/wiki/MIDI_beat_clock

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the name of the message in the midi spec: https://www.cs.cf.ac.uk/Dave/Multimedia/node158.html "Timing Clock: F8". Note that midi has more than one timing mechanism, so it's important to stay close to the spec instead of inventing our own terminology. https://www.google.com/search?client=ubuntu&channel=fs&q=midi+timing+clock+f8&ie=utf-8&oe=utf-8

return QString("MIDI status 0x%1: Clock Tick")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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("MIDI status 0x%1")
.arg(QString::number(status, 16).toUpper());
Expand Down Expand Up @@ -245,6 +259,15 @@ void MidiController::receive(unsigned char status, unsigned char control,
qDebug() << formatMidiMessage(status, control, value, channel, opCode);
}

// If MidiSourceClock handles the message, record the updated values and
// no further action is needed.
if (m_midiSourceClock.handleMessage(status)) {
m_pClockBpm->set(m_midiSourceClock.bpm());
m_pClockLastBeat->set(m_midiSourceClock.lastBeatTime());
m_pClockRunning->set(static_cast<double>(m_midiSourceClock.running()));
return;
}

MidiKey mappingKey(status, control);

if (isLearning()) {
Expand Down
8 changes: 8 additions & 0 deletions src/controllers/midi/midicontroller.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@
#define MIDICONTROLLER_H

#include "controllers/controller.h"
#include "controllers/midi/midisourceclock.h"
#include "controllers/midi/midicontrollerpreset.h"
#include "controllers/midi/midicontrollerpresetfilehandler.h"
#include "controllers/midi/midimessage.h"
#include "controllers/midi/midioutputhandler.h"
#include "controllers/softtakeover.h"

class ControlObjectSlave;

class MidiController : public Controller {
Q_OBJECT
public:
Expand Down Expand Up @@ -102,6 +105,11 @@ class MidiController : public Controller {
MidiControllerPreset m_preset;
SoftTakeoverCtrl m_st;
QList<QPair<MidiInputMapping, unsigned char> > m_fourteen_bit_queued_mappings;
MixxxClock m_mixxxClock;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a member?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clocksource class takes a clock as an argument by pointer, so some object needs to own the original clock. Since the MixxxClock is extremely lightweight, it's not a problem to just have it in every midicontroller instance. I could move the clock up the stack farther, but that doesn't seem to help. Alternatively, I could move the mock/clock code into time.h and have an alternative constructor for clocksource that doesn't take an argument and instead asks time.h for a static clock source object

MidiSourceClock m_midiSourceClock;
QScopedPointer<ControlObjectSlave> m_pClockBpm;
QScopedPointer<ControlObjectSlave> m_pClockLastBeat;
QScopedPointer<ControlObjectSlave> m_pClockRunning;

// So it can access sendShortMsg()
friend class MidiOutputHandler;
Expand Down
117 changes: 117 additions & 0 deletions src/controllers/midi/midisourceclock.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#include "controllers/midi/midimessage.h"
#include "controllers/midi/midisourceclock.h"
#include "util/math.h"

bool MidiSourceClock::handleMessage(unsigned char status) {
switch (status) {
case MIDI_START:
Copy link
Member

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

Copy link
Member Author

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.

start();
return true;
case MIDI_STOP:
stop();
return true;
case MIDI_TIMING_CLK:
tick();
return true;
default:
return false;
}
}

void MidiSourceClock::start() {
m_bRunning = true;
m_iFilled = 0;
m_iRingBufferPos = 0;
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! Here it is:
http://www.blitter.com/~russtopia/MIDI/~jglatt/tech/midispec/seq.htm

In other words, the MIDI Start puts the slave in "play mode", and the receipt of that first MIDI Clock marks the initial downbeat of the song (ie, MIDI Beat 0).

Copy link
Member

Choose a reason for hiding this comment

The 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::tick() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least the Interface should be prepared to receive a time stamp.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be a different implementation, so it would be done in a separate class, and we could update the API at that time with minimal refactoring. No need to engage in speculative coding.

// 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 tick but not started, starting now.";
start();
}

// Ringbuffer filling.
// TODO(owen): We should have a ringbuffer convenience class.
const qint64 lastTickTime = m_pClock->now();
m_iTickRingBuffer[m_iRingBufferPos] = lastTickTime;
m_iRingBufferPos = (m_iRingBufferPos + 1) % kRingBufferSize;
if (m_iFilled < kRingBufferSize) {
++m_iFilled;
}

// If this tick is a beat mark, record it, even if we have very few samples.
if (m_iRingBufferPos % kPulsesPerQuarter == 0) {
m_iLastBeatTime = lastTickTime;
}
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this thick a bar?

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The midi standard does not communicate measures.

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".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes the wording issue worse:

MIDI Beat is a 16th note

But since we do not use the Midi beats, we can Ignore it.

Copy link
Member Author

Choose a reason for hiding this comment

The 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) {
qint64 earlyTickTime = 0;
if (m_iFilled < kRingBufferSize) {
earlyTickTime = m_iTickRingBuffer[0];
} else {
// In a filled ring buffer, the earliest tick is the next one that
// will get filled.
earlyTickTime = m_iTickRingBuffer[m_iRingBufferPos];
}
m_dBpm = calcBpm(earlyTickTime, lastTickTime, m_iFilled);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mean value calculation kills all abrupt tempo changes.
Can we include a plausibility check to detect such changes?

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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(qint64 early_tick, qint64 late_tick,
int tick_count) {
// Get the elapsed time between the latest tick and the earliest tick
// and divide by the number of ticks in the buffer to get bpm.

// If we have too few samples, we can't calculate a bpm, so return 0.0.
DEBUG_ASSERT_AND_HANDLE(tick_count >= 2) {
qWarning() << "MidiSourceClock::calcBpm called with too few ticks";
return 0.0;
}

DEBUG_ASSERT_AND_HANDLE(late_tick >= early_tick) {
qWarning() << "MidiSourceClock asked to calculate beat percentage but "
<< "late_tick < early_tick:" << late_tick << early_tick;
return 0.0;
}

const double elapsed_mins = static_cast<double>(late_tick - early_tick)
/ (60.0 * 1e9);

// We subtract one since two time values denote a single span of time --
// so a filled value of 3 indicates 2 tick periods, etc.
const double bpm = static_cast<double>(tick_count - 1) / kPulsesPerQuarter
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A Pentium can handle Mixed type calculations. No need for a cast.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both numberator and denominator are ints, so cast is required I think. Only last divisor is double.

/ elapsed_mins;

return math_clamp(bpm, kMinMidiBpm, kMaxMidiBpm);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clamping is always wrong here. Can we return just 0 and let the calling code decide?

}

// static
double MidiSourceClock::beatPercentage(const qint64 last_beat, const qint64 now,
const double bpm) {
DEBUG_ASSERT_AND_HANDLE(now >= last_beat) {
qWarning() << "MidiSourceClock asked to calculate beat percentage but "
<< "now < last_beat:" << now << last_beat;
return 0.0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can return a valid value.

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

}
// Get seconds per beat.
const double beat_length = 60.0 / bpm;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bpm can be 0

// seconds / secondsperbeat = percentage of beat.
const double beat_percent = static_cast<double>(now - last_beat) / 1e9
/ beat_length;
// Ensure values are < 1.0.
return beat_percent - floor(beat_percent);
}

double MidiSourceClock::beatPercentage() const {
return beatPercentage(m_iLastBeatTime, m_pClock->now(), m_dBpm);
}

Loading