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:
parent
7fb3c3e137
commit
927788a0b0
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -396,6 +396,7 @@ def build(bld):
|
|||
'libaudiographer',
|
||||
'libtemporal',
|
||||
'liblua',
|
||||
'zita-resampler',
|
||||
]
|
||||
if bld.env['build_target'] != 'mingw':
|
||||
obj.uselib += ['DL']
|
||||
|
|
Loading…
Reference in New Issue