Overhaul MIDI Clock generator

* Fix absolute time alignment, sync clock ticks on exact beats
* Fix start/cont sync to MIDI Clock **Beats** (MIDI Clock / 6)
* Send timecode from engine (not session) - vari-speed independent
* Special case MClk port, don't /resample/
* Support pre-roll, sync start.
This commit is contained in:
Robin Gareus 2020-05-30 03:45:46 +02:00
parent 29a6bfd3e7
commit ef94663d1c
Signed by: rgareus
GPG Key ID: A090BCE02CF57F04
8 changed files with 157 additions and 296 deletions

View File

@ -289,6 +289,7 @@ public:
void process (pframes_t nframes);
void send_ltc_for_cycle (samplepos_t, samplepos_t, pframes_t);
void send_mclk_for_cycle (samplepos_t, samplepos_t, pframes_t, samplecnt_t);
BufferSet& get_silent_buffers (ChanCount count = ChanCount::ZERO);
BufferSet& get_noinplace_buffers (ChanCount count = ChanCount::ZERO);

View File

@ -37,60 +37,36 @@ namespace ARDOUR
class Session;
class MidiPort;
class LIBARDOUR_API MidiClockTicker : public SessionHandlePtr, boost::noncopyable
class LIBARDOUR_API MidiClockTicker : boost::noncopyable
{
public:
MidiClockTicker ();
MidiClockTicker (Session*);
virtual ~MidiClockTicker ();
void tick (const samplepos_t& transport_samples, pframes_t nframes);
bool has_midi_port () const
{
return _midi_port != 0;
}
void set_session (Session* s);
void session_going_away ();
/// slot for the signal session::MIDIClock_PortChanged
void update_midi_clock_port ();
/// slot for the signal session::TransportStateChange
void transport_state_changed ();
/// slot for the signal session::TransportLooped
void transport_looped ();
/// slot for the signal session::Located
void session_located ();
/// pulses per quarter note (default 24)
void set_ppqn (int ppqn)
{
_ppqn = ppqn;
}
void tick (samplepos_t, samplepos_t, pframes_t, samplecnt_t);
private:
boost::shared_ptr<MidiPort> _midi_port;
int _ppqn;
double _last_tick;
bool _send_pos;
bool _send_state;
class Position;
boost::scoped_ptr<Position> _pos;
double one_ppqn_in_samples (samplepos_t transport_position);
void reset ();
double one_ppqn_in_samples (samplepos_t transport_position) const;
void send_midi_clock_event (pframes_t offset, pframes_t nframes);
void send_start_event (pframes_t offset, pframes_t nframes);
void send_continue_event (pframes_t offset, pframes_t nframes);
void send_stop_event (pframes_t offset, pframes_t nframes);
void send_position_event (uint32_t midi_clocks, pframes_t offset, pframes_t nframes);
bool _rolling;
double _next_tick;
uint32_t _beat_pos;
uint32_t _clock_cnt;
samplepos_t _transport_pos;
ARDOUR::Session* _session;
LatencyRange _mclk_out_latency;
};
} // namespace ARDOUR
// namespace
#endif /* __libardour_ticker_h__ */

View File

@ -470,6 +470,7 @@ AudioEngine::process_callback (pframes_t nframes)
Freewheel (nframes);
} else {
samplepos_t start_sample = _session->transport_sample ();
samplecnt_t pre_roll = _session->remaining_latency_preroll ();
if (Port::cycle_nframes () <= nframes) {
_session->process (Port::cycle_nframes ());
@ -495,6 +496,8 @@ AudioEngine::process_callback (pframes_t nframes)
/* send timecode for current cycle */
samplepos_t end_sample = _session->transport_sample ();
_session->send_ltc_for_cycle (start_sample, end_sample, nframes);
/* and MIDI Clock */
_session->send_mclk_for_cycle (start_sample, end_sample, nframes, pre_roll);
}
if (_freewheeling) {

View File

@ -78,7 +78,7 @@ MidiPortManager::create_ports ()
p = AudioEngine::instance()->register_output_port (DataType::MIDI, X_("MTC out"));
_mtc_output_port= boost::dynamic_pointer_cast<MidiPort> (p);
p = AudioEngine::instance()->register_output_port (DataType::MIDI, X_("MIDI Clock out"));
p = AudioEngine::instance()->register_output_port (DataType::MIDI, X_("MIDI Clock out"), false, TransportGenerator);
_midi_clock_output_port= boost::dynamic_pointer_cast<MidiPort> (p);
}

View File

@ -7064,6 +7064,12 @@ Session::maybe_update_tempo_from_midiclock_tempo (float bpm)
}
}
void
Session::send_mclk_for_cycle (samplepos_t start_sample, samplepos_t end_sample, pframes_t n_samples, samplecnt_t pre_roll)
{
midi_clock->tick (start_sample, end_sample, n_samples, pre_roll);
}
void
Session::set_had_destructive_tracks (bool yn)
{

View File

@ -147,12 +147,7 @@ Session::process (pframes_t nframes)
*/
try {
if (!_silent && !_engine.freewheeling() && Config->get_send_midi_clock() && (transport_speed() == 1.0f || transport_speed() == 0.0f) && midi_clock->has_midi_port()) {
midi_clock->tick (transport_at_start, nframes);
}
_scene_changer->run (transport_at_start, transport_at_start + nframes);
} catch (...) {
/* don't bother with a message */
}

View File

@ -277,8 +277,7 @@ Session::post_engine_init ()
/* MidiClock requires a tempo map */
delete midi_clock;
midi_clock = new MidiClockTicker ();
midi_clock->set_session (this);
midi_clock = new MidiClockTicker (this);
/* crossfades require sample rate knowledge */

View File

@ -40,290 +40,179 @@
using namespace ARDOUR;
using namespace PBD;
/** MIDI Clock Position tracking */
class MidiClockTicker::Position : public Timecode::BBT_Time
MidiClockTicker::MidiClockTicker (Session* s)
{
public:
Position ()
: speed (0.0f)
, sample (0)
, midi_beats (0)
{
}
~Position () {}
/** Sync timing information taken from the given Session
* @return True if timings differed
*/
bool sync (Session* s)
{
bool changed = false;
double sp = s->transport_speed ();
samplecnt_t fr = s->transport_sample ();
if (speed != sp) {
speed = sp;
changed = true;
}
if (sample != fr) {
sample = fr;
changed = true;
}
/* Midi beats and clocks always gets updated for now */
s->bbt_time (this->sample, *this);
const TempoMap& tempo = s->tempo_map ();
const Meter& meter = tempo.meter_at_sample (sample);
const double divisions = meter.divisions_per_bar ();
const double divisor = meter.note_divisor ();
const double qnote_scale = divisor * 0.25f;
double mb;
/** Midi Beats in terms of Song Position Pointer is equivalent to total
* sixteenth notes at 'time'
*/
mb = (((bars - 1) * divisions) + beats - 1);
mb += (double)ticks / (double)Position::ticks_per_beat * qnote_scale;
mb *= 16.0f / divisor;
if (mb != midi_beats) {
midi_beats = mb;
midi_clocks = midi_beats * 6.0f;
changed = true;
}
return changed;
}
double speed;
samplecnt_t sample;
double midi_beats;
double midi_clocks;
void print (std::ostream& s)
{
s << "samples: " << sample << " midi beats: " << midi_beats << " speed: " << speed;
}
};
MidiClockTicker::MidiClockTicker ()
: _ppqn (24)
, _last_tick (0.0)
, _send_pos (false)
, _send_state (false)
{
_pos.reset (new Position ());
_session = s;
_midi_port = s->midi_clock_output_port ();
reset ();
}
MidiClockTicker::~MidiClockTicker ()
{
_pos.reset (0);
}
void
MidiClockTicker::set_session (Session* s)
MidiClockTicker::reset ()
{
SessionHandlePtr::set_session (s);
if (_session) {
_session->TransportStateChange.connect_same_thread (_session_connections, boost::bind (&MidiClockTicker::transport_state_changed, this));
_session->TransportLooped.connect_same_thread (_session_connections, boost::bind (&MidiClockTicker::transport_looped, this));
_session->Located.connect_same_thread (_session_connections, boost::bind (&MidiClockTicker::session_located, this));
update_midi_clock_port ();
_pos->sync (_session);
}
_rolling = false;
_next_tick = 0;
_beat_pos = 0;
_clock_cnt = 0;
_transport_pos = -1;
}
void
MidiClockTicker::session_located ()
MidiClockTicker::tick (samplepos_t start_sample, samplepos_t end_sample, pframes_t n_samples, samplecnt_t pre_roll)
{
DEBUG_TRACE (DEBUG::MidiClock, string_compose ("Session Located: %1, speed: %2\n", _session->transport_sample (), _session->transport_speed ()));
/* silence buffer */
_midi_port->cycle_start (n_samples);
if (!_session || !_pos->sync (_session)) {
return;
double speed = (end_sample - start_sample) / (double)n_samples;
if (!Config->get_send_midi_clock () /*|| !TransportMasterManager::instance().current()*/) {
if (_rolling) {
send_stop_event (0, n_samples);
}
reset ();
goto out;
}
_last_tick = _pos->sample;
// TODO: cache
_midi_port->get_connected_latency_range (_mclk_out_latency, true);
if (!Config->get_send_midi_clock ()) {
return;
if (speed == 0 && start_sample == 0 && end_sample == 0) {
/* test if pre-roll is active, special-case
* "start at zero"
*/
if (pre_roll > 0 && pre_roll >= _mclk_out_latency.max && pre_roll < _mclk_out_latency.max + n_samples) {
assert (!_rolling);
pframes_t pos = pre_roll - _mclk_out_latency.max;
_next_tick = one_ppqn_in_samples (0) - _mclk_out_latency.max;
DEBUG_TRACE (DEBUG::MidiClock, string_compose (
"Preroll Start offset: %1 (port latency: %2, remaining preroll: %3) next %4\n",
pos, _mclk_out_latency.max, pre_roll, _next_tick));
_rolling = true;
_beat_pos = 0;
_clock_cnt = 1;
_transport_pos = 0;
send_start_event (pos, n_samples);
send_midi_clock_event (pos, n_samples);
}
/* Handle case _mclk_out_latency.max > one_ppqn_in_samples (0)
* may need to send more than one clock */
if (pre_roll > 0 && _next_tick < 0) {
assert (_rolling);
while (_next_tick < 0 && pre_roll >= -_next_tick && pre_roll < n_samples - _next_tick) {
pframes_t pos = pre_roll + _next_tick;
_next_tick += one_ppqn_in_samples (llrint (0));
DEBUG_TRACE (DEBUG::MidiClock, string_compose (
"Preroll Tick offset: %1 (port latency: %2, remaining preroll: %3) next %4\n",
pos, _mclk_out_latency.max, pre_roll, _next_tick));
send_midi_clock_event (pos, n_samples);
if (++_clock_cnt == 6) {
_clock_cnt = 0;
++_beat_pos;
}
}
}
if (pre_roll > 0) {
goto out;
}
}
_send_pos = true;
}
void
MidiClockTicker::session_going_away ()
{
SessionHandlePtr::session_going_away ();
_midi_port.reset ();
}
void
MidiClockTicker::update_midi_clock_port ()
{
_midi_port = _session->midi_clock_output_port ();
}
void
MidiClockTicker::transport_state_changed ()
{
if (_session->exporting ()) {
/* no midi clock during export, for now */
return;
if (speed != 1.0) {
if (_rolling) {
DEBUG_TRACE (DEBUG::MidiClock, string_compose ("Speed != 1 - Stop @ cycle: 1 .. %2 (- preroll %3) nsamples: %4\n",
start_sample, end_sample, end_sample, pre_roll, n_samples));
send_stop_event (0, n_samples);
}
reset ();
goto out;
}
if (!_session->engine ().running ()) {
/* Engine stopped, we can't do anything */
return;
/* test for discontinuity */
if (start_sample != _transport_pos) {
if (_rolling) {
DEBUG_TRACE (DEBUG::MidiClock, string_compose ("Discontinuty start_sample: %1 ticker-pos: %2\n", start_sample, _transport_pos));
send_stop_event (0, n_samples);
}
_rolling = false;
_transport_pos = -1;
}
if (!_pos->sync (_session)) {
return;
}
if (!_rolling) {
if (_transport_pos < 0 || _next_tick < start_sample) {
/* get the next downbeat */
uint32_t beat_pos;
samplepos_t clk_pos;
DEBUG_TRACE (DEBUG::MidiClock,
string_compose ("Transport state change @ %4, speed: %1 position: %2 play loop: %3\n",
_pos->speed, _pos->sample, _session->get_play_loop (), _pos->sample));
_session->tempo_map ().midi_clock_beat_at_of_after (start_sample + _mclk_out_latency.max, clk_pos, beat_pos);
_last_tick = _pos->sample;
_beat_pos = beat_pos;
_next_tick = clk_pos - _mclk_out_latency.max;
_transport_pos = end_sample;
}
if (!Config->get_send_midi_clock ()) {
return;
}
if (_next_tick >= start_sample && _next_tick < end_sample) {
DEBUG_TRACE (DEBUG::MidiClock, string_compose ("Start rolling at %1 beat-pos: %2\n", _next_tick, _beat_pos));
_send_state = true;
}
_rolling = true;
_clock_cnt = 0;
void
MidiClockTicker::transport_looped ()
{
Location* loop_location = _session->locations ()->auto_loop_location ();
assert (loop_location);
DEBUG_TRACE (DEBUG::MidiClock,
string_compose ("Transport looped, position: %1, loop start: %2, loop end: %3, play loop: %4\n",
_session->transport_sample (), loop_location->start (), loop_location->end (), _session->get_play_loop ()));
// adjust _last_tick, so that the next MIDI clock message is sent
// in due time (and the tick interval is still constant)
samplecnt_t elapsed_since_last_tick = loop_location->end () - _last_tick;
if (loop_location->start () > elapsed_since_last_tick) {
_last_tick = loop_location->start () - elapsed_since_last_tick;
} else {
_last_tick = 0;
}
}
void
MidiClockTicker::tick (const samplepos_t& transport_sample, pframes_t nframes)
{
if (!Config->get_send_midi_clock () || _session == 0 || _midi_port == 0) {
return;
}
if (_send_pos) {
if (_pos->speed == 0.0f) {
send_position_event (llrint (_pos->midi_beats), 0, nframes);
} else if (_pos->speed == 1.0f) {
send_stop_event (0, nframes);
if (_pos->sample == 0) {
send_start_event (0, nframes);
if (_beat_pos == 0 && _next_tick == 0 && start_sample == 0) {
send_start_event (0, n_samples);
} else {
send_position_event (llrint (_pos->midi_beats), 0, nframes);
send_continue_event (0, nframes);
send_position_event (_beat_pos, 0, n_samples); // consider sending this early
send_continue_event (_next_tick - start_sample, n_samples);
}
} else {
/* Varispeed not supported */
goto out;
}
_send_pos = false;
}
if (_send_state) {
if (_pos->speed == 1.0f) {
if (_session->get_play_loop ()) {
assert (_session->locations ()->auto_loop_location ());
assert (_rolling);
if (_pos->sample == _session->locations ()->auto_loop_location ()->start ()) {
send_start_event (0, nframes);
} else {
send_continue_event (0, nframes);
}
} else if (_pos->sample == 0) {
send_start_event (0, nframes);
} else {
send_continue_event (0, nframes);
}
// send_midi_clock_event (0);
} else if (_pos->speed == 0.0f) {
send_stop_event (0, nframes);
send_position_event (llrint (_pos->midi_beats), 0, nframes);
while (_next_tick >= start_sample && _next_tick < end_sample) {
DEBUG_TRACE (DEBUG::MidiClock, string_compose ("Tick @ %1 cycle: %2 .. %3 nsamples: %4, ticker-pos: %5\n",
_next_tick, start_sample, end_sample, n_samples, _transport_pos));
send_midi_clock_event (_next_tick - start_sample, n_samples);
if (++_clock_cnt == 6) {
_clock_cnt = 0;
++_beat_pos;
}
_send_state = false;
_next_tick += one_ppqn_in_samples (llrint (_next_tick));
}
if (_session->transport_speed () != 1.0f) {
/* no varispeed support and nothing to do after this if stopped */
return;
}
_transport_pos = end_sample;
const samplepos_t end = _pos->sample + nframes;
double iter = _last_tick;
while (true) {
double clock_delta = one_ppqn_in_samples (llrint (iter));
double next_tick = iter + clock_delta;
sampleoffset_t next_tick_offset = llrint (next_tick) - end;
DEBUG_TRACE (DEBUG::MidiClock,
string_compose ("Tick: iter: %1, last tick time: %2, next tick time: %3, offset: %4, cycle length: %5\n",
iter, _last_tick, next_tick, next_tick_offset, nframes));
if (next_tick_offset >= nframes) {
break;
}
if (next_tick_offset >= 0) {
send_midi_clock_event (next_tick_offset, nframes);
}
iter = next_tick;
}
_last_tick = iter;
_pos->sample = end;
out:
_midi_port->flush_buffers (n_samples);
_midi_port->cycle_end (n_samples);
}
double
MidiClockTicker::one_ppqn_in_samples (samplepos_t transport_position)
MidiClockTicker::one_ppqn_in_samples (samplepos_t transport_position) const
{
const double samples_per_quarter_note = _session->tempo_map ().samples_per_quarter_note_at (transport_position, _session->nominal_sample_rate ());
return samples_per_quarter_note / double(_ppqn);
return samples_per_quarter_note / 24.0;
}
void
MidiClockTicker::send_midi_clock_event (pframes_t offset, pframes_t nframes)
{
if (!_midi_port) {
return;
}
assert (_midi_port);
static uint8_t msg = MIDI_CMD_COMMON_CLOCK;
@ -336,51 +225,43 @@ MidiClockTicker::send_midi_clock_event (pframes_t offset, pframes_t nframes)
void
MidiClockTicker::send_start_event (pframes_t offset, pframes_t nframes)
{
if (!_midi_port) {
return;
}
assert (_midi_port);
static uint8_t msg = { MIDI_CMD_COMMON_START };
MidiBuffer& mb (_midi_port->get_midi_buffer (nframes));
mb.push_back (offset, 1, &msg);
DEBUG_TRACE (DEBUG::MidiClock, string_compose ("Start %1\n", _last_tick));
DEBUG_TRACE (DEBUG::MidiClock, string_compose ("Start %1\n", _next_tick));
}
void
MidiClockTicker::send_continue_event (pframes_t offset, pframes_t nframes)
{
if (!_midi_port) {
return;
}
assert (_midi_port);
static uint8_t msg = { MIDI_CMD_COMMON_CONTINUE };
MidiBuffer& mb (_midi_port->get_midi_buffer (nframes));
mb.push_back (offset, 1, &msg);
DEBUG_TRACE (DEBUG::MidiClock, string_compose ("Continue %1\n", _last_tick));
DEBUG_TRACE (DEBUG::MidiClock, string_compose ("Continue %1\n", _next_tick));
}
void
MidiClockTicker::send_stop_event (pframes_t offset, pframes_t nframes)
{
if (!_midi_port) {
return;
}
assert (_midi_port);
static uint8_t msg = MIDI_CMD_COMMON_STOP;
MidiBuffer& mb (_midi_port->get_midi_buffer (nframes));
mb.push_back (offset, 1, &msg);
DEBUG_TRACE (DEBUG::MidiClock, string_compose ("Stop %1\n", _last_tick));
DEBUG_TRACE (DEBUG::MidiClock, string_compose ("Stop %1\n", _next_tick));
}
void
MidiClockTicker::send_position_event (uint32_t midi_beats, pframes_t offset, pframes_t nframes)
{
if (!_midi_port) {
return;
}
assert (_midi_port);
/* can only use 14bits worth */
if (midi_beats > 0x3fff) {