I have designed a prototype app with the following classes:
Ticker
class (discussed here) that has an assignable callback that will be executed every tick (specified by_tickInterval
) on a separate thread.SongPosition
class that basically represents a position in a song.BeatClock
class that assigns a callback to_ticker
which is supposed to update_songpos
and here I have a problem...
Here is what Ticker
class implementation looks like:
#include <cstdint>
#include <functional>
#include <chrono>
#include <thread>
#include <future>
#include <mutex>
class Ticker {
public:
/* TYPES: */
// Tick interval type in nanoseconds
typedef std::chrono::nanoseconds tick_interval_type;
// OnTick callback type
typedef std::function<void()> on_tick_type;
/* CONSTANTS: */
// Default ticker interval equal to 120 BPM
static const tick_interval_type kDefaultTickerInterval;
/* INIT: */
// Constructs object
explicit Ticker (tick_interval_type tickInterval)
: _onTick ()
, _tickInterval (tickInterval)
, _running (false) {}
// Destructs object
~Ticker () {}
/* PROPERTIES: */
// Sets tick interval to a new value
void setTickInterval (tick_interval_type tickInterval) {
_tickIntervalMutex.lock();
_tickInterval = tickInterval;
_tickIntervalMutex.unlock();
}
// Sets on-tick callback to a new value
void setOnTick (on_tick_type onTick) { _onTick = onTick; }
/* PUBLIC METHODS: */
// Starts the ticker
void start () {
if (_running) return;
_running = true;
std::thread run( &Ticker::loop, this );
run.detach();
}
// Stops the ticker
void stop () { _running = false; }
private:
/* FIELDS: */
// OnTick callback, called every tick
on_tick_type _onTick;
// Tick interval between every tick
tick_interval_type _tickInterval;
// Ticker running flag
volatile bool _running;
// Tick interval mutex for thread safety
std::mutex _tickIntervalMutex;
/* PRIVATE METHODS: */
// Inner loop executed on a separate thread to produce ticks
void loop () {
tick_interval_type ti;
while (_running) {
std::thread run (_onTick);
run.detach();
_tickIntervalMutex.lock();
ti = _tickInterval;
_tickIntervalMutex.unlock();
std::this_thread::sleep_for( ti );
}
}
};
// Default ticker interval equal to 120 BPM
const Ticker::tick_interval_type Ticker::kDefaultTickerInterval (625000000UL / 120UL);
Here is what SongPosition
class implementation looks like:
#include <cstdint>
class SongPosition {
public:
/* INIT: */
// Constructs song position instance: Ticks | Beats | Measures
SongPosition (uint8_t ticks = 0u, uint8_t beats = 0u, uint16_t measures = 0u) {
_ticks = ticks;
_beats = beats;
_measures = measures;
}
~SongPosition () {}
/* PROPERTIES:: */
// Counts from 0..95
uint8_t ticks () const { return _ticks; }
// Counts the quarter notes 0..3
uint8_t beats () const { return _beats; }
// Counts the measures 0..65535
uint16_t measures () const { return _measures; }
// Current meter location, sixteens-notes counter
uint16_t meter () const { return (_beats << 2) | (_measures << 4); }
/* METHODS: */
// Resets meter counters
void reset () {
_ticks = 0u;
_beats = 0u;
_measures = 0u;
}
// Resets ticks counter
void resetTicks () { _ticks = 0u; }
// Resets beats counter
void resetBeats () { _beats = 0u; }
// Increments ticks counter
void incrementTicks () { ++_ticks; }
// Increments beats counter
void incrementBeats () { ++_beats; }
// Increments measures counter
void incrementMeasures () { ++_measures; }
// Decrements measures counter
void decrementMeasures () { --_measures; }
bool operator== (const SongPosition& other) const {
if (_ticks == other.ticks()
&& _beats == other.beats()
&& _measures == other.measures()) return true;
else return false;
}
bool operator!= (const SongPosition& other) const {
return !(*this == other);
}
private:
/* FIELDS: */
// Counts from 0..95
uint8_t _ticks;
// Counts the quarter notes 0..3
uint8_t _beats;
// Counts the measures 0..65535
uint16_t _measures;
};
inline bool operator< (const SongPosition& lhs, const SongPosition& rhs) {
if (lhs.measures() < rhs.measures()) return true;
else if (lhs.measures() == rhs.measures()) {
if (lhs.beats() < rhs.beats()) return true;
else if (lhs.beats() == rhs.beats()) {
if (lhs.ticks() < rhs.ticks()) return true;
else return false;
}
else return false;
}
else return false;
}
inline bool operator> (const SongPosition& lhs, const SongPosition& rhs) {
return rhs < lhs;
}
inline bool operator<= (const SongPosition& lhs, const SongPosition& rhs) {
return !(lhs > rhs);
}
inline bool operator>= (const SongPosition& lhs, const SongPosition& rhs) {
return !(lhs < rhs);
}
And finally here is what BeatClock
class implementation looks like (the problem one):
#include <cstdint>
#include "SongPosition.h"
#include "Ticker.h"
class BeatClock {
public:
/* INNER CLASSES: */
// Inner state of beat clock
enum class State : uint8_t {
kStopped,
kRunning,
kPaused
};
/* CONSTANTS: */
// Clock pre-start delay to give a slave time to prepare (1ms)
static const std::chrono::milliseconds kPreStartDelay;
/* CONSTRUCTORS/DISTRUCTORS: */
// Constructs object
BeatClock ()
: _bpm (120UL)
, _ticker (Ticker::kDefaultTickerInterval)
, _songpos ()
, _precount () {
_ticker.setOnTick( [&] {
_tickerMutex.lock();
_songpos.incrementTicks();
if (_songpos.ticks() == 96U) { // next beat...
_songpos.resetTicks();
_songpos.incrementBeats();
if (_songpos.beats() == 4U) { // next measure...
_songpos.resetBeats();
_songpos.incrementMeasures();
}
}
if (_precount == 0U) {
_precount = 4U;
printf( "Send - RealTimeClock: %2i | %1i | %05i",
_songpos.ticks(), _songpos.beats(), _songpos.ticks() );
}
++_precount;
_tickerMutex.unlock();
} );
_state = State::kStopped;
}
// Destructs object
~BeatClock () {}
/* PROPERTIES: */
// Gets current BPM value
uint64_t bpm () const { return _bpm; }
// Sets current BPM to a new value
void setBpm (uint64_t bpm) {
_bpm = bpm;
Ticker::tick_interval_type span (625000000UL / _bpm);
_ticker.setTickInterval( span );
}
/* PUBLIC METHODS: */
// Starts if currently stopped / Pauses if currently running / Resumes if currently paused
void start () {
switch (_state)
{
case State::kStopped: // start...
_songpos.reset();
this->sendMeter();
printf( "Send - RealTimeStart" );
std::this_thread::sleep_for( kPreStartDelay );
_state = State::kRunning;
_ticker.start();
break;
case State::kRunning: // pause...
_ticker.stop();
_state = State::kPaused;
break;
case State::kPaused: // continue...
printf( "Send - RealTimeContinue" );
_state = State::kRunning;
_ticker.start();
break;
}
}
// Stops if currently running or paused / Resets if currently stopped
void stop () {
switch (_state)
{
case State::kStopped: // reset...
_songpos.reset();
this->sendMeter();
printf( "Send - RealTimeStop" );
break;
case State::kRunning:
case State::kPaused: // stop...
printf( "Send - RealTimeStop" );
_ticker.stop();
_precount = 0U;
_state = State::kStopped;
break;
}
}
// Increments measure and resets sub-counters
void forward () {
if (_state == State::kRunning) _ticker.stop();
_songpos.resetTicks();
_songpos.resetBeats();
_songpos.incrementMeasures();
_precount = 0U;
this->sendMeter();
if (_state == State::kRunning) _ticker.start();
}
// Decrements measure and resets sub-counters
void rewind () {
if (_state == State::kRunning) _ticker.stop();
_songpos.resetTicks();
_songpos.resetBeats();
if (_songpos.measures() > 0U) _songpos.decrementMeasures();
_precount = 0U;
this->sendMeter();
if (_state == State::kRunning) _ticker.start();
}
private:
/* FIELDS: */
// Current BPM value
uint64_t _bpm;
// Used for generating tick events.
Ticker _ticker;
// Current song position
SongPosition _songpos;
// Clock precount to scale from 96 to 24 ppqn (0..3)
uint8_t _precount;
// Transport state
State _state;
// Ticker mutex for thread safety
std::mutex _tickerMutex;
/* PRIVATE METHODS: */
// Sends Song-Position MIDI message
void sendMeter () {
printf( "Send - SongPosition: %i", _songpos.meter() );
}
};
// Clock pre-start delay to give a slave time to prepare (1ms)
const std::chrono::milliseconds BeatClock::kPreStartDelay (1);
_onTick
callback is not working as expected, updating _songpos
randomly (because of data-races?)
Can you please advice how to update _songpos
in controllable way from a callback (which is running on a separate thread)?
Here is what I'm expecting to be logged (that is 24 ticks per beat and 4 beats per measure):
Send - SongPosition: 0
Send - RealTimeStart // begin...
Send - RealTimeClock: 2 | 0 | 00000
Send - RealTimeClock: 5 | 0 | 00000
Send - RealTimeClock: 9 | 0 | 00000
Send - RealTimeClock: 13 | 0 | 00000
Send - RealTimeClock: 17 | 0 | 00000
Send - RealTimeClock: 21 | 0 | 00000
Send - RealTimeClock: 25 | 0 | 00000
Send - RealTimeClock: 29 | 0 | 00000
Send - RealTimeClock: 33 | 0 | 00000
Send - RealTimeClock: 37 | 0 | 00000
Send - RealTimeClock: 41 | 0 | 00000
Send - RealTimeClock: 45 | 0 | 00000
Send - RealTimeClock: 49 | 0 | 00000
Send - RealTimeClock: 53 | 0 | 00000
Send - RealTimeClock: 57 | 0 | 00000
Send - RealTimeClock: 61 | 0 | 00000
Send - RealTimeClock: 65 | 0 | 00000
Send - RealTimeClock: 69 | 0 | 00000
Send - RealTimeClock: 73 | 0 | 00000
Send - RealTimeClock: 77 | 0 | 00000
Send - RealTimeClock: 81 | 0 | 00000
Send - RealTimeClock: 85 | 0 | 00000
Send - RealTimeClock: 89 | 0 | 00000
Send - RealTimeClock: 93 | 0 | 00000
Send - RealTimeClock: 1 | 1 | 00000 // next beat..
Send - RealTimeClock: 5 | 1 | 00000
Send - RealTimeClock: 9 | 1 | 00000
Send - RealTimeClock: 13 | 1 | 00000
Send - RealTimeClock: 17 | 1 | 00000
Send - RealTimeClock: 21 | 1 | 00000
Send - RealTimeClock: 25 | 1 | 00000
Send - RealTimeClock: 29 | 1 | 00000
Send - RealTimeClock: 33 | 1 | 00000
Send - RealTimeClock: 37 | 1 | 00000
Send - RealTimeClock: 41 | 1 | 00000
Send - RealTimeClock: 45 | 1 | 00000
Send - RealTimeClock: 49 | 1 | 00000
Send - RealTimeClock: 53 | 1 | 00000
Send - RealTimeClock: 57 | 1 | 00000
Send - RealTimeClock: 61 | 1 | 00000
Send - RealTimeClock: 65 | 1 | 00000
Send - RealTimeClock: 69 | 1 | 00000
Send - RealTimeClock: 73 | 1 | 00000
Send - RealTimeClock: 77 | 1 | 00000
Send - RealTimeClock: 81 | 1 | 00000
Send - RealTimeClock: 85 | 1 | 00000
Send - RealTimeClock: 89 | 1 | 00000
Send - RealTimeClock: 93 | 1 | 00000
Send - RealTimeClock: 1 | 2 | 00000 // next beat..
Send - RealTimeClock: 5 | 2 | 00000
Send - RealTimeClock: 9 | 2 | 00000
Send - RealTimeClock: 13 | 2 | 00000
Send - RealTimeClock: 17 | 2 | 00000
Send - RealTimeClock: 21 | 2 | 00000
Send - RealTimeClock: 25 | 2 | 00000
Send - RealTimeClock: 29 | 2 | 00000
Send - RealTimeClock: 33 | 2 | 00000
Send - RealTimeClock: 37 | 2 | 00000
Send - RealTimeClock: 41 | 2 | 00000
Send - RealTimeClock: 45 | 2 | 00000
Send - RealTimeClock: 49 | 2 | 00000
Send - RealTimeClock: 53 | 2 | 00000
Send - RealTimeClock: 57 | 2 | 00000
Send - RealTimeClock: 61 | 2 | 00000
Send - RealTimeClock: 65 | 2 | 00000
Send - RealTimeClock: 69 | 2 | 00000
Send - RealTimeClock: 73 | 2 | 00000
Send - RealTimeClock: 77 | 2 | 00000
Send - RealTimeClock: 81 | 2 | 00000
Send - RealTimeClock: 85 | 2 | 00000
Send - RealTimeClock: 89 | 2 | 00000
Send - RealTimeClock: 93 | 2 | 00000
Send - RealTimeClock: 1 | 3 | 00000 // next beat..
Send - RealTimeClock: 5 | 3 | 00000
Send - RealTimeClock: 9 | 3 | 00000
Send - RealTimeClock: 13 | 3 | 00000
Send - RealTimeClock: 17 | 3 | 00000
Send - RealTimeClock: 21 | 3 | 00000
Send - RealTimeClock: 25 | 3 | 00000
Send - RealTimeClock: 29 | 3 | 00000
Send - RealTimeClock: 33 | 3 | 00000
Send - RealTimeClock: 37 | 3 | 00000
Send - RealTimeClock: 41 | 3 | 00000
Send - RealTimeClock: 45 | 3 | 00000
Send - RealTimeClock: 49 | 3 | 00000
Send - RealTimeClock: 53 | 3 | 00000
Send - RealTimeClock: 57 | 3 | 00000
Send - RealTimeClock: 61 | 3 | 00000
Send - RealTimeClock: 65 | 3 | 00000
Send - RealTimeClock: 69 | 3 | 00000
Send - RealTimeClock: 73 | 3 | 00000
Send - RealTimeClock: 77 | 3 | 00000
Send - RealTimeClock: 81 | 3 | 00000
Send - RealTimeClock: 85 | 3 | 00000
Send - RealTimeClock: 89 | 3 | 00000
Send - RealTimeClock: 93 | 3 | 00000
Send - RealTimeClock: 1 | 0 | 00001 // next measure and once again...
Send - RealTimeStop // enough. :)
Thank you very much!