Move vari-speed into backend (resample ports)

Previously Ardour used a /local/ per track vari-speed mechanism.
Now that the disk-reader is a latency-compensated processor, the speed
of each disk-reader would need to be maintained locally, offset by each
disk-reader's output latency. Furthermore each disk-reader may
produce a different number of samples, depending on its global alignment.

This commit introduces port-data resampling directly at the engine-level:
Up/down-sample all input ports at the beginning, and down/up-sample output
port-data using the inverse ratio at the end of the session's process
cycle.
The session itself is unaware of the speed-change, and only needs to
handle transport speeds {-1, 0, +1}.

This also allows for aligned cue-monitoring and vari-speed recording,
and also pitch-shifts synthesized MIDI along.
This commit is contained in:
Robin Gareus 2017-10-29 18:30:18 +01:00
parent 7fb3c3e137
commit 927788a0b0
10 changed files with 185 additions and 27 deletions

View File

@ -21,6 +21,8 @@
#ifndef __ardour_audio_port_h__
#define __ardour_audio_port_h__
#include "zita-resampler/vmresampler.h"
#include "ardour/port.h"
#include "ardour/audio_buffer.h"
@ -49,12 +51,14 @@ class LIBARDOUR_API AudioPort : public Port
friend class PortManager;
AudioPort (std::string const &, PortFlags);
/* special access for PortManager only (hah, C++) */
Sample* engine_get_whole_audio_buffer ();
/* special access for PortManager only (hah, C++) */
Sample* engine_get_whole_audio_buffer ();
private:
AudioBuffer* _buffer;
bool _buf_valid;
AudioBuffer* _buffer;
ArdourZita::VMResampler _src;
Sample* _data;
bool _buf_valid;
};
} // namespace ARDOUR

View File

@ -106,7 +106,7 @@ public:
void set_private_latency_range (LatencyRange& range, bool playback);
const LatencyRange& private_latency_range (bool playback) const;
void set_public_latency_range (LatencyRange& range, bool playback) const;
void set_public_latency_range (LatencyRange const& range, bool playback) const;
LatencyRange public_latency_range (bool playback) const;
virtual void reset ();
@ -121,15 +121,16 @@ public:
virtual void realtime_locate () {}
bool physically_connected () const;
bool externally_connected () const;
PBD::Signal1<void,bool> MonitorInputChanged;
static PBD::Signal2<void,boost::shared_ptr<Port>,boost::shared_ptr<Port> > PostDisconnect;
static PBD::Signal0<void> PortDrop;
static PBD::Signal0<void> PortSignalDrop;
static void set_cycle_samplecnt (pframes_t n) {
_cycle_nframes = n;
}
static void set_speed_ratio (double s);
static void set_cycle_samplecnt (pframes_t n);
static samplecnt_t port_offset() { return _global_port_buffer_offset; }
static void set_global_port_buffer_offset (pframes_t off) {
_global_port_buffer_offset = off;
@ -145,21 +146,26 @@ public:
static std::string state_node_name;
static pframes_t cycle_nframes () { return _cycle_nframes; }
protected:
Port (std::string const &, DataType, PortFlags);
PortEngine::PortHandle _port_handle;
PortEngine::PortHandle _port_handle;
static bool _connecting_blocked;
static pframes_t _global_port_buffer_offset; /* access only from process() tree */
static bool _connecting_blocked;
static pframes_t _cycle_nframes; /* access only from process() tree */
samplecnt_t _port_buffer_offset; /* access only from process() tree */
static pframes_t _global_port_buffer_offset; /* access only from process() tree */
samplecnt_t _port_buffer_offset; /* access only from process() tree */
LatencyRange _private_playback_latency;
LatencyRange _private_capture_latency;
static double _speed_ratio;
static const uint32_t _resampler_quality; /* also latency of the resampler */
private:
std::string _name; ///< port short name
PortFlags _flags; ///< flags

View File

@ -728,6 +728,12 @@ public:
bool synced_to_mtc () const { return config.get_external_sync() && Config->get_sync_source() == MTC && g_atomic_int_get (const_cast<gint*>(&_mtc_active)); }
bool synced_to_ltc () const { return config.get_external_sync() && Config->get_sync_source() == LTC && g_atomic_int_get (const_cast<gint*>(&_ltc_active)); }
double engine_speed() const { return _engine_speed; }
double actual_speed() const {
if (_transport_speed > 0) return _engine_speed;
if (_transport_speed < 0) return - _engine_speed;
return 0;
}
double transport_speed() const { return _count_in_samples > 0 ? 0. : _transport_speed; }
bool transport_stopped() const { return _transport_speed == 0.0; }
bool transport_rolling() const { return _transport_speed != 0.0 && _count_in_samples == 0 && _remaining_latency_preroll == 0; }
@ -1255,6 +1261,7 @@ private:
samplecnt_t _remaining_latency_preroll;
// varispeed playback
double _engine_speed;
double _transport_speed;
double _default_transport_speed;
double _last_transport_speed;

View File

@ -18,6 +18,7 @@
#include <cassert>
#include "pbd/malign.h"
#include "pbd/stacktrace.h"
#include "ardour/audio_buffer.h"
@ -36,10 +37,14 @@ AudioPort::AudioPort (const std::string& name, PortFlags flags)
, _buffer (new AudioBuffer (0))
{
assert (name.find_first_of (':') == string::npos);
cache_aligned_malloc ((void**) &_data, sizeof (Sample) * 8192);
_src.setup (_resampler_quality);
_src.set_rrfilt (10);
}
AudioPort::~AudioPort ()
{
cache_aligned_free (_data);
delete _buffer;
}
@ -47,11 +52,32 @@ void
AudioPort::cycle_start (pframes_t nframes)
{
/* caller must hold process lock */
Port::cycle_start (nframes);
Port::cycle_start (nframes);
if (sends_output()) {
_buffer->prepare ();
} else if (!externally_connected ()) {
/* ardour internal port, just silence input, don't resample */
// TODO reset resampler only once
_src.reset ();
memset (_data, 0, _cycle_nframes * sizeof (float));
} else {
_src.inp_data = (float*)port_engine.get_buffer (_port_handle, nframes);
_src.inp_count = nframes;
_src.out_count = _cycle_nframes;
_src.set_rratio (_cycle_nframes / (double)nframes);
_src.out_data = _data;
_src.process ();
#ifndef NDEBUG
if (_src.inp_count != 0 || _src.out_count != 0) {
printf ("AudioPort::cycle_start x-flow: %d/%d\n", _src.inp_count, _src.out_count);
}
#endif
while (_src.out_count > 0) {
*_src.out_data = _src.out_data[-1];
++_src.out_data;
--_src.out_count;
}
}
}
@ -66,6 +92,33 @@ AudioPort::cycle_end (pframes_t nframes)
_buffer->silence (nframes);
}
}
if (sends_output() && _port_handle) {
if (!externally_connected ()) {
/* ardour internal port, data goes nowhere, skip resampling */
// TODO reset resampler only once
_src.reset ();
return;
}
_src.inp_count = _cycle_nframes;
_src.out_count = nframes;
_src.set_rratio (nframes / (double)_cycle_nframes);
_src.inp_data = _data;
_src.out_data = (float*)port_engine.get_buffer (_port_handle, nframes);
_src.process ();
#ifndef NDEBUG
if (_src.inp_count != 0 || _src.out_count != 0) {
printf ("AudioPort::cycle_end x-flow: %d/%d\n", _src.inp_count, _src.out_count);
}
#endif
while (_src.out_count > 0) {
*_src.out_data = _src.out_data[-1];
++_src.out_data;
--_src.out_count;
}
}
}
void
@ -78,8 +131,12 @@ AudioPort::get_audio_buffer (pframes_t nframes)
{
/* caller must hold process lock */
assert (_port_handle);
_buffer->set_data ((Sample *) port_engine.get_buffer (_port_handle, _cycle_nframes) +
_global_port_buffer_offset + _port_buffer_offset, nframes);
if (!externally_connected ()) {
_buffer->set_data ((Sample *) port_engine.get_buffer (_port_handle, _cycle_nframes) +
_global_port_buffer_offset + _port_buffer_offset, nframes);
} else {
_buffer->set_data (&_data[_global_port_buffer_offset + _port_buffer_offset], nframes);
}
return *_buffer;
}
@ -90,7 +147,3 @@ AudioPort::engine_get_whole_audio_buffer ()
assert (_port_handle);
return (Sample *) port_engine.get_buffer (_port_handle, _cycle_nframes);
}

View File

@ -190,6 +190,7 @@ int
AudioEngine::process_callback (pframes_t nframes)
{
Glib::Threads::Mutex::Lock tm (_process_lock, Glib::Threads::TRY_LOCK);
Port::set_speed_ratio (1.0);
PT_TIMING_REF;
PT_TIMING_CHECK (1);
@ -357,6 +358,14 @@ AudioEngine::process_callback (pframes_t nframes)
return 0;
}
if (!_freewheeling || Freewheel.empty()) {
// run a list of slaves here
// - multiple slaves (ow_many_dsp_threads() in paralell)
// - session can pick one (ask for position & speed)
// - GUI can display all
Port::set_speed_ratio (_session->engine_speed ());
}
/* tell all relevant objects that we're starting a new cycle */
InternalSend::CycleStart (nframes);
@ -373,7 +382,19 @@ AudioEngine::process_callback (pframes_t nframes)
if (_freewheeling && !Freewheel.empty()) {
Freewheel (nframes);
} else {
_session->process (nframes);
if (Port::cycle_nframes () <= nframes) {
_session->process (Port::cycle_nframes ());
} else {
pframes_t remain = Port::cycle_nframes ();
while (remain > 0) {
pframes_t nf = std::min (remain, nframes);
_session->process (nf);
remain -= nf;
if (remain > 0) {
split_cycle (nf);
}
}
}
}
if (_freewheeling) {

View File

@ -138,9 +138,15 @@ MidiPort::get_midi_buffer (pframes_t nframes)
continue;
}
timestamp = floor (timestamp * _speed_ratio);
/* check that the event is in the acceptable time range */
if ((timestamp < (_global_port_buffer_offset + _port_buffer_offset)) ||
(timestamp >= (_global_port_buffer_offset + _port_buffer_offset + nframes))) {
// XXX this is normal after a split cycles:
// The engine buffer contains the data for the complete cycle, but
// only the part after _global_port_buffer_offset is needed.
#ifndef NDEBUG
cerr << "Dropping incoming MIDI at time " << timestamp << "; offset="
<< _global_port_buffer_offset << " limit="
<< (_global_port_buffer_offset + _port_buffer_offset + nframes)
@ -148,9 +154,14 @@ MidiPort::get_midi_buffer (pframes_t nframes)
<< " + " << _port_buffer_offset
<< " + " << nframes
<< ")\n";
#endif
continue;
}
/* adjust timestamp to match current cycle */
timestamp -= _global_port_buffer_offset + _port_buffer_offset;
assert (timestamp >= 0 && timestamp < nframes);
if ((buf[0] & 0xF0) == 0x90 && buf[2] == 0) {
/* normalize note on with velocity 0 to proper note off */
uint8_t ev[3];
@ -196,19 +207,20 @@ MidiPort::resolve_notes (void* port_buffer, MidiBuffer::TimeType when)
for (uint8_t channel = 0; channel <= 0xF; channel++) {
uint8_t ev[3] = { ((uint8_t) (MIDI_CMD_CONTROL | channel)), MIDI_CTL_SUSTAIN, 0 };
pframes_t tme = floor (when / _speed_ratio);
/* we need to send all notes off AND turn the
* sustain/damper pedal off to handle synths
* that prioritize sustain over AllNotesOff
*/
if (port_engine.midi_event_put (port_buffer, when, ev, 3) != 0) {
if (port_engine.midi_event_put (port_buffer, tme, ev, 3) != 0) {
cerr << "failed to deliver sustain-zero on channel " << (int)channel << " on port " << name() << endl;
}
ev[1] = MIDI_CTL_ALL_NOTES_OFF;
if (port_engine.midi_event_put (port_buffer, when, ev, 3) != 0) {
if (port_engine.midi_event_put (port_buffer, tme, ev, 3) != 0) {
cerr << "failed to deliver ALL NOTES OFF on channel " << (int)channel << " on port " << name() << endl;
}
}
@ -279,7 +291,8 @@ MidiPort::flush_buffers (pframes_t nframes)
assert (ev.time() < (nframes + _global_port_buffer_offset));
if (ev.time() >= _global_port_buffer_offset) {
if (port_engine.midi_event_put (port_buffer, (pframes_t) ev.time() + _port_buffer_offset, ev.buffer(), ev.size()) != 0) {
pframes_t tme = floor ((ev.time() + _port_buffer_offset) / _speed_ratio);
if (port_engine.midi_event_put (port_buffer, tme, ev.buffer(), ev.size()) != 0) {
cerr << "write failed, dropped event, time "
<< ev.time() << " + " << _port_buffer_offset
<< " > " << _global_port_buffer_offset << endl;

View File

@ -43,7 +43,9 @@ PBD::Signal0<void> Port::PortSignalDrop;
bool Port::_connecting_blocked = false;
pframes_t Port::_global_port_buffer_offset = 0;
pframes_t Port::_cycle_nframes = 0;
double Port::_speed_ratio = 1.0;
std::string Port::state_node_name = X_("Port");
const uint32_t Port::_resampler_quality = 12;
/* a handy define to shorten what would otherwise be a needlessly verbose
* repeated phrase
@ -350,7 +352,7 @@ Port::increment_port_buffer_offset (pframes_t nframes)
}
void
Port::set_public_latency_range (LatencyRange& range, bool playback) const
Port::set_public_latency_range (LatencyRange const& range, bool playback) const
{
/* this sets the visible latency that the rest of the port system
sees. because we do latency compensation, all (most) of our visible
@ -363,7 +365,16 @@ Port::set_public_latency_range (LatencyRange& range, bool playback) const
(playback ? "PLAYBACK" : "CAPTURE")));;
if (_port_handle) {
port_engine.set_latency_range (_port_handle, playback, range);
LatencyRange r (range);
if (externally_connected ()) {
#if 0
r.min *= _speed_ratio;
r.max *= _speed_ratio;
#endif
r.min += (_resampler_quality - 1);
r.max += (_resampler_quality - 1);
}
port_engine.set_latency_range (_port_handle, playback, r);
}
}
@ -419,6 +430,14 @@ Port::public_latency_range (bool /*playback*/) const
if (_port_handle) {
r = port_engine.get_latency_range (_port_handle, sends_output() ? true : false);
if (externally_connected ()) {
#if 0
r.min /= _speed_ratio;
r.max /= _speed_ratio;
#endif
r.min += (_resampler_quality - 1);
r.max += (_resampler_quality - 1);
}
DEBUG_TRACE (DEBUG::Latency, string_compose (
"GET PORT %1: %4 PUBLIC latency range %2 .. %3\n",
@ -458,6 +477,14 @@ Port::get_connected_latency_range (LatencyRange& range, bool playback) const
if (remote_port) {
lr = port_engine.get_latency_range (remote_port, playback);
if (externally_connected ()) {
#if 0
lr.min /= _speed_ratio;
lr.max /= _speed_ratio;
#endif
lr.min += (_resampler_quality - 1);
lr.max += (_resampler_quality - 1);
}
DEBUG_TRACE (DEBUG::Latency, string_compose (
"\t%1 <-> %2 : latter has latency range %3 .. %4\n",
@ -564,6 +591,19 @@ Port::physically_connected () const
return port_engine.physically_connected (_port_handle);
}
bool
Port::externally_connected () const
{
if (!_port_handle) {
return false;
}
// TODO: When used with JACK, check if this port
// is connected to any non-ardour ports.
return port_engine.physically_connected (_port_handle);
}
XMLNode&
Port::get_state () const
{
@ -621,3 +661,14 @@ Port::set_state (const XMLNode& node, int)
return 0;
}
/*static*/ void
Port::set_speed_ratio (double s) {
/* see VMResampler::set_rratio() for min/max range */
_speed_ratio = std::min (16.0, std::max (0.5, s));
}
/*static*/ void
Port::set_cycle_samplecnt (pframes_t n) {
_cycle_nframes = floor (n * _speed_ratio);
}

View File

@ -749,10 +749,11 @@ void
PortManager::cycle_start (pframes_t nframes)
{
Port::set_global_port_buffer_offset (0);
Port::set_cycle_samplecnt (nframes);
Port::set_cycle_samplecnt (nframes);
_cycle_ports = ports.reader ();
// TODO parallelize
for (Ports::iterator p = _cycle_ports->begin(); p != _cycle_ports->end(); ++p) {
p->second->cycle_start (nframes);
}

View File

@ -187,6 +187,7 @@ Session::Session (AudioEngine &eng,
, _slave (0)
, _silent (false)
, _remaining_latency_preroll (0)
, _engine_speed (1.0)
, _transport_speed (0)
, _default_transport_speed (1.0)
, _last_transport_speed (0)

View File

@ -396,6 +396,7 @@ def build(bld):
'libaudiographer',
'libtemporal',
'liblua',
'zita-resampler',
]
if bld.env['build_target'] != 'mingw':
obj.uselib += ['DL']