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.
#ifndef __ardour_audio_port_h__
#define __ardour_audio_port_h__
+#include "zita-resampler/vmresampler.h"
+
#include "ardour/port.h"
#include "ardour/audio_buffer.h"
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
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 ();
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;
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
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; }
samplecnt_t _remaining_latency_preroll;
// varispeed playback
+ double _engine_speed;
double _transport_speed;
double _default_transport_speed;
double _last_transport_speed;
#include <cassert>
+#include "pbd/malign.h"
#include "pbd/stacktrace.h"
#include "ardour/audio_buffer.h"
, _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;
}
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;
+ }
}
}
_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
{
/* 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;
}
assert (_port_handle);
return (Sample *) port_engine.get_buffer (_port_handle, _cycle_nframes);
}
-
-
-
-
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);
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);
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) {
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)
<< " + " << _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];
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;
}
}
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;
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
}
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
(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);
}
}
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",
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",
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
{
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);
+}
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);
}
, _slave (0)
, _silent (false)
, _remaining_latency_preroll (0)
+ , _engine_speed (1.0)
, _transport_speed (0)
, _default_transport_speed (1.0)
, _last_transport_speed (0)
'libaudiographer',
'libtemporal',
'liblua',
+ 'zita-resampler',
]
if bld.env['build_target'] != 'mingw':
obj.uselib += ['DL']