diff --git a/libs/backends/alsa/alsa_audiobackend.cc b/libs/backends/alsa/alsa_audiobackend.cc index 93ec8dead8..5abb471502 100644 --- a/libs/backends/alsa/alsa_audiobackend.cc +++ b/libs/backends/alsa/alsa_audiobackend.cc @@ -23,12 +23,17 @@ #include +#include +#include + #include "alsa_audiobackend.h" #include "pbd/compose.h" +#include "pbd/convert.h" #include "pbd/error.h" #include "pbd/file_utils.h" #include "pbd/pthread_utils.h" + #include "ardour/filesystem_paths.h" #include "ardour/port_manager.h" #include "ardouralsautil/devicelist.h" @@ -668,6 +673,11 @@ AlsaAudioBackend::set_midi_device_enabled (std::string const device, bool enable nfo->enabled = enable; if (_run && prev_enabled != enable) { + // XXX actually we should not change system-ports while running, + // because iterators in main_process_thread will become invalid. + // + // Luckily the engine dialog does not call this while the engine is running, + // This code is currently not used. if (enable) { // add ports for the given device register_system_midi_ports(device); @@ -940,6 +950,34 @@ AlsaAudioBackend::_start (bool for_latency_measurement) return ProcessThreadStartError; } +#if 1 + if (NULL != getenv ("ALSAEXT")) { + boost::char_separator sep (";"); + boost::tokenizer > devs (std::string(getenv ("ALSAEXT")), sep); + BOOST_FOREACH (const std::string& tmp, devs) { + std::string dev (tmp); + std::string::size_type n = dev.find ('@'); + unsigned int sr = _samplerate; + unsigned int spp = _samples_per_period; + unsigned int duplex = 3; // TODO parse 1: play, 2: capt, 3:both + if (n != std::string::npos) { + std::string opt (dev.substr (n + 1)); + sr = PBD::atoi (opt); + dev = dev.substr (0, n); + std::string::size_type n = opt.find ('/'); + if (n != std::string::npos) { + spp = PBD::atoi (opt.substr (n + 1)); + } + } + if (add_slave (dev.c_str(), sr, spp, duplex)) { + PBD::info << string_compose (_("ALSA slave '%1' added"), dev) << endmsg; + } else { + PBD::error << string_compose (_("ALSA failed to add '%1' as slave"), dev) << endmsg; + } + } + } +#endif + return NoError; } @@ -970,6 +1008,12 @@ AlsaAudioBackend::stop () delete m; } + while (!_slaves.empty ()) { + AudioSlave* s = _slaves.back (); + _slaves.pop_back (); + delete s; + } + unregister_ports(); delete _pcmi; _pcmi = 0; _midi_ins = _midi_outs = 0; @@ -1819,8 +1863,14 @@ AlsaAudioBackend::main_process_thread () { AudioEngine::thread_init_callback (this); _active = true; + bool reset_dll = true; + int last_n_periods = 0; _processed_samples = 0; + double dll_dt = (double) _samples_per_period / (double) _samplerate; + double dll_w1 = 2 * M_PI * 0.1 * dll_dt; + double dll_w2 = dll_w1 * dll_w1; + uint64_t clock1; _pcmi->pcm_start (); int no_proc_errors = 0; @@ -1829,18 +1879,70 @@ AlsaAudioBackend::main_process_thread () manager.registration_callback(); manager.graph_order_callback(); + const double sr_norm = 1e-6 * (double) _samplerate / (double)_samples_per_period; + while (_run) { long nr; bool xrun = false; + bool drain_slaves = false; if (_freewheeling != _freewheel) { _freewheel = _freewheeling; engine.freewheel_callback (_freewheel); + for (AudioSlaves::iterator s = _slaves.begin (); s != _slaves.end (); ++s) { + (*s)->freewheel (_freewheel); + } + if (!_freewheel) { + _pcmi->pcm_stop (); + _pcmi->pcm_start (); + drain_slaves = true; + } } if (!_freewheel) { nr = _pcmi->pcm_wait (); + /* update DLL */ + uint64_t clock0 = g_get_monotonic_time(); + if (reset_dll || last_n_periods != 1) { + reset_dll = false; + drain_slaves = true; + dll_dt = 1e6 * (double) _samples_per_period / (double)_samplerate; + _t0 = clock0; + _t1 = clock0 + dll_dt; + } else { + const double er = clock0 - _t1; + _t0 = _t1; + _t1 = _t1 + dll_w1 * er + dll_dt; + dll_dt += dll_w2 * er; + } + + for (AudioSlaves::iterator s = _slaves.begin (); s != _slaves.end (); ++s) { + if ((*s)->dead) { + continue; + } + if ((*s)->halt) { + /* slave died, unregister its ports (not rt-safe, but no matter) */ + PBD::error << _("ALSA Slave device halted") << endmsg; + for (std::vector::const_iterator it = (*s)->inputs.begin (); it != (*s)->inputs.end (); ++it) { + unregister_port (*it); + } + for (std::vector::const_iterator it = (*s)->outputs.begin (); it != (*s)->outputs.end (); ++it) { + unregister_port (*it); + } + (*s)->inputs.clear (); + (*s)->outputs.clear (); + (*s)->active = false; + (*s)->dead = true; + continue; + } + (*s)->active = (*s)->running () && (*s)->state () >= 0; + if (!(*s)->active) { + continue; + } + (*s)->cycle_start (_t0, (_t1 - _t0) * sr_norm, drain_slaves); + } + if (_pcmi->state () > 0) { ++no_proc_errors; xrun = true; @@ -1858,6 +1960,7 @@ AlsaAudioBackend::main_process_thread () break; } + last_n_periods = 0; while (nr >= (long)_samples_per_period && _freewheeling == _freewheel) { uint32_t i = 0; clock1 = g_get_monotonic_time(); @@ -1869,6 +1972,16 @@ AlsaAudioBackend::main_process_thread () } _pcmi->capt_done (_samples_per_period); + for (AudioSlaves::iterator s = _slaves.begin (); s != _slaves.end (); ++s) { + if (!(*s)->active) { + continue; + } + i = 0; + for (std::vector::const_iterator it = (*s)->inputs.begin (); it != (*s)->inputs.end (); ++it, ++i) { + (*s)->capt_chan (i, (float*)((*it)->get_buffer(_samples_per_period)), _samples_per_period); + } + } + /* de-queue incoming midi*/ i = 0; for (std::vector::const_iterator it = _system_midi_in.begin (); it != _system_midi_in.end (); ++it, ++i) { @@ -1924,6 +2037,18 @@ AlsaAudioBackend::main_process_thread () _pcmi->clear_chan (i, _samples_per_period); } _pcmi->play_done (_samples_per_period); + + for (AudioSlaves::iterator s = _slaves.begin (); s != _slaves.end (); ++s) { + if (!(*s)->active) { + continue; + } + i = 0; + for (std::vector::const_iterator it = (*s)->outputs.begin (); it != (*s)->outputs.end (); ++it, ++i) { + (*s)->play_chan (i, (float*)((*it)->get_buffer(_samples_per_period)), _samples_per_period); + } + (*s)->cycle_end (); + } + nr -= _samples_per_period; _processed_samples += _samples_per_period; @@ -1931,10 +2056,12 @@ AlsaAudioBackend::main_process_thread () _dsp_load_calc.set_start_timestamp_us (clock1); _dsp_load_calc.set_stop_timestamp_us (g_get_monotonic_time()); _dsp_load = _dsp_load_calc.get_dsp_load (); + ++last_n_periods; } if (xrun && (_pcmi->capt_xrun() > 0 || _pcmi->play_xrun() > 0)) { engine.Xrun (); + reset_dll = true; #if 0 fprintf(stderr, "ALSA x-run read: %.2f ms, write: %.2f ms\n", _pcmi->capt_xrun() * 1000.0, _pcmi->play_xrun() * 1000.0); @@ -1980,6 +2107,7 @@ AlsaAudioBackend::main_process_thread () } _dsp_load = 1.0; + reset_dll = true; Glib::usleep (100); // don't hog cpu } @@ -2021,6 +2149,119 @@ AlsaAudioBackend::main_process_thread () return 0; } +/******************************************************************************/ + +bool +AlsaAudioBackend::add_slave (const char* device, + unsigned int slave_rate, + unsigned int slave_spp, + unsigned int duplex) +{ + AudioSlave* s = new AudioSlave (device, duplex, + _samplerate, _samples_per_period, + slave_rate, slave_spp, 2); + + if (s->state ()) { + // TODO parse error status + PBD::error << string_compose (_("Failed to create slave device '%1' error %2\n"), device, s->state ()) << endmsg; + goto errout; + } + + for (uint32_t i = 0, n = 1; i < s->ncapt (); ++i) { + char tmp[64]; + do { + snprintf(tmp, sizeof(tmp), "extern:capture_%d", n); + if (find_port (tmp)) { + ++n; + } else { + break; + } + } while (1); + PortHandle p = add_port(std::string(tmp), DataType::AUDIO, static_cast(IsOutput | IsPhysical | IsTerminal)); + if (!p) goto errout; + AlsaPort *ap = static_cast(p); + s->inputs.push_back (ap); + } + + for (uint32_t i = 0, n = 1; i < s->nplay (); ++i) { + char tmp[64]; + do { + snprintf(tmp, sizeof(tmp), "extern:playback_%d", n); + if (find_port (tmp)) { + ++n; + } else { + break; + } + } while (1); + PortHandle p = add_port(std::string(tmp), DataType::AUDIO, static_cast(IsInput | IsPhysical | IsTerminal)); + if (!p) goto errout; + AlsaPort *ap = static_cast(p); + s->outputs.push_back (ap); + } + + if (!s->start ()) { + PBD::error << string_compose (_("Failed to start slave device '%1'\n"), device) << endmsg; + goto errout; + } + s->UpdateLatency.connect_same_thread (s->latency_connection, boost::bind (&AlsaAudioBackend::update_latencies, this)); + _slaves.push_back (s); + return true; + +errout: + delete s; // releases device + return false; +} + +AlsaAudioBackend::AudioSlave::AudioSlave ( + const char* device, + unsigned int duplex, + unsigned int master_rate, + unsigned int master_samples_per_period, + unsigned int slave_rate, + unsigned int slave_samples_per_period, + unsigned int periods_per_cycle) + : AlsaDeviceReservation (device) + , AlsaAudioSlave ( + (duplex & 1) ? device : NULL /* playback */, + (duplex & 2) ? device : NULL /* capture */, + master_rate, master_samples_per_period, + slave_rate, slave_samples_per_period, periods_per_cycle) + , active (false) + , halt (false) + , dead (false) +{ + Halted.connect_same_thread (_halted_connection, boost::bind (&AudioSlave::halted, this)); +} + +AlsaAudioBackend::AudioSlave::~AudioSlave () +{ + stop (); +} + +void +AlsaAudioBackend::AudioSlave::halted () +{ + // Note: Halted() is emitted from the Slave's process thread. + release_device (); + halt = true; +} + +void +AlsaAudioBackend::AudioSlave::update_latencies (uint32_t play, uint32_t capt) +{ + LatencyRange lr; + lr.min = lr.max = (capt); + for (std::vector::const_iterator it = inputs.begin (); it != inputs.end (); ++it) { + (*it)->set_latency_range (lr, false); + } + + lr.min = lr.max = play; + for (std::vector::const_iterator it = outputs.begin (); it != outputs.end (); ++it) { + (*it)->set_latency_range (lr, true); + } + printf (" ----- SLAVE LATENCY play=%d capt=%d\n", play, capt); // XXX DEBUG + UpdateLatency (); /* EMIT SIGNAL */ +} /******************************************************************************/ diff --git a/libs/backends/alsa/alsa_audiobackend.h b/libs/backends/alsa/alsa_audiobackend.h index e90bec5df9..ad3bb41949 100644 --- a/libs/backends/alsa/alsa_audiobackend.h +++ b/libs/backends/alsa/alsa_audiobackend.h @@ -41,6 +41,7 @@ #include "zita-alsa-pcmi.h" #include "alsa_rawmidi.h" #include "alsa_sequencer.h" +#include "alsa_slave.h" namespace ARDOUR { @@ -397,6 +398,9 @@ class AlsaAudioBackend : public AudioBackend { framecnt_t _processed_samples; pthread_t _main_thread; + /* DLL, track main process callback timing */ + double _t0, _t1; + /* process threads */ static void* alsa_process_thread (void *); std::vector _threads; @@ -480,6 +484,46 @@ class AlsaAudioBackend : public AudioBackend { void update_systemic_audio_latencies (); void update_systemic_midi_latencies (); + /* additional re-sampled I/O */ + bool add_slave (const char* slave_device, + unsigned int slave_rate, + unsigned int slave_spp, + unsigned int duplex = 3); + + class AudioSlave : public AlsaDeviceReservation, public AlsaAudioSlave { + public: + AudioSlave ( + const char* device, + unsigned int duplex, + unsigned int master_rate, + unsigned int master_samples_per_period, + unsigned int slave_rate, + unsigned int slave_samples_per_period, + unsigned int periods_per_cycle); + + ~AudioSlave (); + + bool active; // set in sync with process-cb + bool halt; + bool dead; + + std::vector inputs; + std::vector outputs; + + PBD::Signal0 UpdateLatency; + PBD::ScopedConnection latency_connection; + + protected: + void update_latencies (uint32_t, uint32_t); + + private: + PBD::ScopedConnection _halted_connection; + void halted (); + }; + + typedef std::vector AudioSlaves; + AudioSlaves _slaves; + }; // class AlsaAudioBackend } // namespace diff --git a/libs/backends/alsa/alsa_slave.cc b/libs/backends/alsa/alsa_slave.cc new file mode 100644 index 0000000000..e1061ccc09 --- /dev/null +++ b/libs/backends/alsa/alsa_slave.cc @@ -0,0 +1,523 @@ +/* + * Copyright (C) 2017 Robin Gareus + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + + +#include +#include + +#include "pbd/compose.h" +#include "pbd/error.h" +#include "pbd/pthread_utils.h" + +#include "alsa_slave.h" + +#include "pbd/i18n.h" + +using namespace ARDOUR; + +AlsaAudioSlave::AlsaAudioSlave ( + const char *play_name, + const char *capt_name, + unsigned int master_rate, + unsigned int master_samples_per_period, + unsigned int slave_rate, + unsigned int slave_samples_per_period, + unsigned int periods_per_cycle) + : _pcmi (play_name, capt_name, 0, slave_rate, slave_samples_per_period, periods_per_cycle, 2, /* Alsa_pcmi::DEBUG_ALL */ 0) + , _run (false) + , _active (false) + , _samples_since_dll_reset (0) + , _ratio (1.0) + , _slave_speed (1.0) + , _draining (1) + , _rb_capture (4 * /* AlsaAudioBackend::_max_buffer_size */ 8192 * _pcmi.ncapt ()) + , _rb_playback (4 * /* AlsaAudioBackend::_max_buffer_size */ 8192 * _pcmi.nplay ()) + , _samples_per_period (master_samples_per_period) + , _capt_buff (0) + , _play_buff (0) + , _src_buff (0) +{ + if (0 != _pcmi.state()) { + return; + } + + /* from alsa-slave to master */ + _ratio = (double) master_rate / (double) _pcmi.fsamp(); + +#ifndef NDEBUG + fprintf (stdout, " --[[ ALSA Slave %s/%s ratio: %.4f\n", play_name, capt_name, _ratio); + _pcmi.printinfo (); + fprintf (stdout, " --]]\n"); +#endif + + _src_capt.setup (_ratio, _pcmi.ncapt (), /*quality*/ 32); // save capture to master + _src_play.setup (1.0 / _ratio, _pcmi.nplay (), /*quality*/ 32); // master to slave play + + _src_capt.set_rrfilt (100); + _src_play.set_rrfilt (100); + + _capt_buff = (float*) malloc (sizeof(float) * _pcmi.ncapt () * _samples_per_period); + _play_buff = (float*) malloc (sizeof(float) * _pcmi.nplay () * _samples_per_period); + _src_buff = (float*) malloc (sizeof(float) * std::max (_pcmi.nplay (), _pcmi.ncapt ())); +} + +AlsaAudioSlave::~AlsaAudioSlave () +{ + stop (); + free (_capt_buff); + free (_play_buff); + free (_src_buff); +} + +void +AlsaAudioSlave::reset_resampler (ArdourZita::VResampler& src) +{ + src.reset (); + src.inp_count = src.inpsize () - 1; + src.out_count = 200000; + src.process (); +} + +bool +AlsaAudioSlave::start () +{ + if (_run) { + return false; + } + + _run = true; + if (pbd_realtime_pthread_create (PBD_SCHED_FIFO, -20, 100000, + &_thread, _process_thread, this)) + { + if (pthread_create (&_thread, NULL, _process_thread, this)) { + _run = false; + PBD::error << _("AlsaAudioBackend: failed to create slave process thread.") << endmsg; + return false; + } + } + + int timeout = 5000; + while (!_active && --timeout > 0) { Glib::usleep (1000); } + + if (timeout == 0 || !_active) { + _run = false; + PBD::error << _("AlsaAudioBackend: failed to start slave process thread.") << endmsg; + return false; + } + + return true; +} + +void +AlsaAudioSlave::stop () +{ + void *status; + if (!_run) { + return; + } + + _run = false; + if (pthread_join (_thread, &status)) { + PBD::error << _("AlsaAudioBackend: slave failed to terminate properly.") << endmsg; + } + _pcmi.pcm_stop (); +} + +void* +AlsaAudioSlave::_process_thread (void* arg) +{ + AlsaAudioSlave* aas = static_cast (arg); + return aas->process_thread (); +} + +void* +AlsaAudioSlave::process_thread () +{ + _active = true; + + bool reset_dll = true; + int last_n_periods = 0; + int no_proc_errors = 0; + const int bailout = 2 * _pcmi.fsamp () / _pcmi.fsize (); + + double dll_dt = (double) _pcmi.fsize () / (double)_pcmi.fsamp (); + double dll_w1 = 2 * M_PI * 0.1 * dll_dt; + double dll_w2 = dll_w1 * dll_w1; + + const double sr_norm = 1e-6 * (double) _pcmi.fsamp () / (double) _pcmi.fsize (); + + _pcmi.pcm_start (); + + while (_run) { + bool xrun = false; + long nr = _pcmi.pcm_wait (); + + /* update DLL */ + uint64_t clock0 = g_get_monotonic_time(); + + if (reset_dll || last_n_periods != 1) { + reset_dll = false; + dll_dt = 1e6 * (double) _pcmi.fsize () / (double) _pcmi.fsamp(); + _t0 = clock0; + _t1 = clock0 + dll_dt; + _samples_since_dll_reset = 0; + } else { + const double er = clock0 - _t1; + _t0 = _t1; + _t1 = _t1 + dll_w1 * er + dll_dt; + dll_dt += dll_w2 * er; + _samples_since_dll_reset += _pcmi.fsize (); + } + + _slave_speed = (_t1 - _t0) * sr_norm; // XXX atomic + + if (_pcmi.state () > 0) { + ++no_proc_errors; + xrun = true; + } + + if (_pcmi.state () < 0) { + PBD::error << _("AlsaAudioBackend: Slave I/O error.") << endmsg; + break; + } + + if (no_proc_errors > bailout) { + PBD::error << _("AlsaAudioBackend: Slave terminated due to continuous x-runs.") << endmsg; + break; + } + + const size_t spp = _pcmi.fsize (); + const bool drain = g_atomic_int_get (&_draining); + last_n_periods = 0; + + while (nr >= (long)spp) { + no_proc_errors = 0; + + _pcmi.capt_init (spp); + if (drain) { + /* do nothing */ + } else if (_rb_capture.write_space () >= _pcmi.ncapt () * spp) { +#if 0 // failsafe: write interleave sample by sample + for (uint32_t s = 0; s < spp; ++s) { + for (uint32_t c = 0; c < _pcmi.ncapt (); ++c) { + float d; + _pcmi.capt_chan (c, &d, 1); + _rb_capture.write (&d, 1); + } + } +#else + unsigned int nchn = _pcmi.ncapt (); + PBD::RingBuffer::rw_vector vec; + _rb_capture.get_write_vector (&vec); + if (vec.len[0] >= nchn * spp) { + for (uint32_t c = 0; c < nchn; ++c) { + _pcmi.capt_chan (c, vec.buf[0] + c, spp, nchn); + } + } else { + uint32_t c; + /* first copy continuous area */ + uint32_t k = vec.len[0] / nchn; + for (c = 0; c < nchn; ++c) { + _pcmi.capt_chan (c, vec.buf[0] + c, k, nchn); + } + + /* possible samples at end of first buffer chunk, + * incomplete audio-frame */ + uint32_t s = vec.len[0] - k * nchn; + assert (s < nchn); + + for (c = 0; c < s; ++c) { + _pcmi.capt_chan (c, vec.buf[0] + k * nchn + c, 1, nchn); + } + /* cont'd audio-frame at second ringbuffer chunk */ + for (; c < nchn; ++c) { + _pcmi.capt_chan (c, vec.buf[1] + c - s, spp - k, nchn); + } + /* remaining data in 2nd area */ + for (c = 0; c < s; ++c) { + _pcmi.capt_chan (c, vec.buf[1] + c + nchn - s, spp - k, nchn); + } + } + _rb_capture.increment_write_idx (spp * nchn); +#endif + } else { + g_atomic_int_set(&_draining, 1); + } + _pcmi.capt_done (spp); + + + if (drain) { + _rb_playback.increment_read_idx (_rb_playback.read_space ()); + } + + _pcmi.play_init (spp); + if (_rb_playback.read_space () >= _pcmi.nplay () * spp) { +#if 0 // failsafe: read sample by sample de-interleave + for (uint32_t s = 0; s < spp; ++s) { + for (uint32_t c = 0; c < _pcmi.nplay (); ++c) { + float d; + _rb_playback.read (&d, 1); + _pcmi.play_chan (c, (const float*)&d, 1); + } + } +#else + unsigned int nchn = _pcmi.nplay (); + PBD::RingBuffer::rw_vector vec; + _rb_playback.get_read_vector (&vec); + if (vec.len[0] >= nchn * spp) { + for (uint32_t c = 0; c < nchn; ++c) { + _pcmi.play_chan (c, vec.buf[0] + c, spp, nchn); + } + } else { + uint32_t c; + uint32_t k = vec.len[0] / nchn; + for (c = 0; c < nchn; ++c) { + _pcmi.play_chan (c, vec.buf[0] + c, k, nchn); + } + + uint32_t s = vec.len[0] - k * nchn; + assert (s < nchn); + + for (c = 0; c < s; ++c) { + _pcmi.play_chan (c, vec.buf[0] + k * nchn + c, 1, nchn); + } + + for (; c < nchn; ++c) { + _pcmi.play_chan (c, vec.buf[1] + c - s, spp - k, nchn); + } + for (c = 0; c < s; ++c) { + _pcmi.play_chan (c, vec.buf[1] + c + nchn - s, spp - k, nchn); + } + } + _rb_playback.increment_read_idx (spp * nchn); +#endif + } else { + if (!drain) { + printf ("Slave Process: Playback Buffer Underflow, have %u want %lu\n", _rb_playback.read_space (), _pcmi.nplay () * spp); // XXX DEBUG + _play_latency += spp * _ratio; + update_latencies (_play_latency, _capt_latency); + } + /* silence outputs */ + for (uint32_t c = 0; c < _pcmi.nplay (); ++c) { + _pcmi.clear_chan (c, spp); + } + } + _pcmi.play_done (spp); + + nr -= spp; + ++last_n_periods; + } + + if (xrun && (_pcmi.capt_xrun() > 0 || _pcmi.play_xrun() > 0)) { + reset_dll = true; + _samples_since_dll_reset = 0; + g_atomic_int_set(&_draining, 1); + } + } + + _pcmi.pcm_stop (); + _active = false; + + if (_run) { + Halted (); /* Emit Signal */ + } + return 0; +} + +void +AlsaAudioSlave::cycle_start (double tme, double mst_speed, bool drain) +{ + //printf ("SRC %f / %f = %f\n", mst_speed, _slave_speed, mst_speed / _slave_speed); + //printf ("DRIFT (mst) %11.1f - (slv) %11.1f = %.1f us = %.1f spl\n", tme, _t0, tme - _t0, (tme - _t0) * _pcmi.fsamp () * 1e-6); + //printf ("Slave capt: %u play: %u\n", _rb_capture.read_space (), _rb_playback.read_space ()); + + // TODO LPF filter ratios, atomic _slave_speed + const double slave_speed = _slave_speed; + + _src_capt.set_rratio (mst_speed / slave_speed); + _src_play.set_rratio (slave_speed / mst_speed); + + memset (_capt_buff, 0, sizeof(float) * _pcmi.ncapt () * _samples_per_period); + + if (drain) { + g_atomic_int_set(&_draining, 1); + return; + } + + if (g_atomic_int_get (&_draining)) { + _rb_capture.increment_read_idx (_rb_capture.read_space()); + return; + } + + /* resample slave capture data from ringbuffer */ + unsigned int nchn = _pcmi.ncapt (); + _src_capt.out_count = _samples_per_period; + _src_capt.out_data = _capt_buff; + + /* estimate required samples */ + const double rratio = _ratio * mst_speed / slave_speed; + if (_rb_capture.read_space() < ceil (nchn * _samples_per_period / rratio)) { + printf ("--- UNDERFLOW --- have %u want %.1f\n", _rb_capture.read_space(), ceil (nchn * _samples_per_period / rratio)); // XXX DEBUG + _capt_latency += _samples_per_period; + update_latencies (_play_latency, _capt_latency); + return; + } + + bool underflow = false; + while (_src_capt.out_count && _active) { + if (_rb_capture.read_space() < nchn) { + underflow = true; + break; + } + unsigned int n; + PBD::RingBuffer::rw_vector vec; + _rb_capture.get_read_vector (&vec); + if (vec.len[0] < nchn) { + _rb_capture.read (_src_buff, nchn); + _src_capt.inp_count = 1; + _src_capt.inp_data = _src_buff; + _src_capt.process (); + } else { + _src_capt.inp_count = n = vec.len[0] / nchn; + _src_capt.inp_data = vec.buf[0]; + _src_capt.process (); + n -= _src_capt.inp_count; + _rb_capture.increment_read_idx (n * _pcmi.ncapt ()); + } + } + + if (underflow) { + std::cerr << "ALSA Slave: Capture Ringbuffer Underflow\n"; // XXX + g_atomic_int_set(&_draining, 1); + } + + if (!_active || underflow) { + memset (_capt_buff, 0, sizeof(float) * _pcmi.ncapt () * _samples_per_period); + } + + memset (_play_buff, 0, sizeof(float) * _pcmi.nplay () * _samples_per_period); +} + +void +AlsaAudioSlave::cycle_end () +{ + bool drain_done = false; + bool overflow = false; + + if (g_atomic_int_get (&_draining)) { + if (_rb_capture.read_space() == 0 && _rb_playback.read_space() == 0 && _samples_since_dll_reset > _pcmi.fsamp ()) { + reset_resampler (_src_capt); + reset_resampler (_src_play); + memset (_src_buff, 0, sizeof (float) * _pcmi.nplay()); + /* prefill ringbuffers, resampler variance */ + for (int i = 0; i < 16; ++i) { + _rb_playback.write (_src_buff, _pcmi.nplay()); + } + memset (_src_buff, 0, sizeof (float) * _pcmi.ncapt()); + // It's safe to write here, process-thread NO-OPs while draining. + for (int i = 0; i < 16; ++i) { + _rb_capture.write (_src_buff, _pcmi.ncapt()); + } + _capt_latency = 16; + _play_latency = 16 + _ratio * _pcmi.fsize () * (_pcmi.play_nfrag () - 1); + update_latencies (_play_latency, _capt_latency); + drain_done = true; + } else { + return; + } + } + + /* resample collected playback data into ringbuffer */ + unsigned int nchn = _pcmi.nplay (); + _src_play.inp_count = _samples_per_period; + _src_play.inp_data = _play_buff; + + while (_src_play.inp_count && _active) { + unsigned int n; + PBD::RingBuffer::rw_vector vec; + _rb_playback.get_write_vector (&vec); + if (vec.len[0] < nchn) { + _src_play.out_count = 1; + _src_play.out_data = _src_buff; + _src_play.process (); + if (_rb_playback.write_space() < nchn) { + overflow = true; + break; + } else if (_src_play.out_count == 0) { + _rb_playback.write (_src_buff, nchn); + } + } else { + _src_play.out_count = n = vec.len[0] / nchn; + _src_play.out_data = vec.buf[0]; + _src_play.process (); + n -= _src_play.out_count; + if (_rb_playback.write_space() < n * nchn) { + overflow = true; + break; + } + _rb_playback.increment_write_idx (n * nchn); + } + } + + if (overflow) { + std::cerr << "ALSA Slave: Playback Ringbuffer Overflow\n"; // XXX + g_atomic_int_set(&_draining, 1); + return; + } + if (drain_done) { + g_atomic_int_set(&_draining, 0); + } +} + +void +AlsaAudioSlave::freewheel (bool onoff) +{ + if (onoff) { + g_atomic_int_set(&_draining, 1); + } +} + +/* master read slave's capture. + * resampled at cycle_start, before master can call this + */ +uint32_t +AlsaAudioSlave::capt_chan (uint32_t chn, float* dst, uint32_t n_samples) +{ + uint32_t nchn = _pcmi.ncapt (); + assert (chn < nchn && n_samples == _samples_per_period); + float* src = &_capt_buff[chn]; + for (uint32_t s = 0; s < n_samples; ++s) { + dst[s] = src[s * nchn]; + } + return n_samples; +} + +/* write from master to slave output, + * resampled at cycle_end, after master called this. + */ +uint32_t +AlsaAudioSlave::play_chan (uint32_t chn, float* src, uint32_t n_samples) +{ + uint32_t nchn = _pcmi.nplay (); + assert (chn < nchn && n_samples == _samples_per_period); + float* dst = &_play_buff[chn]; + for (uint32_t s = 0; s < n_samples; ++s) { + dst[s * nchn] = src[s]; + } + return n_samples; +} diff --git a/libs/backends/alsa/alsa_slave.h b/libs/backends/alsa/alsa_slave.h new file mode 100644 index 0000000000..28cd20af06 --- /dev/null +++ b/libs/backends/alsa/alsa_slave.h @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2017 Robin Gareus + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#ifndef __libbackend_alsa_slave_h__ +#define __libbackend_alsa_slave_h__ + +#include + +#include "pbd/ringbuffer.h" +#include "zita-resampler/vresampler.h" +#include "zita-alsa-pcmi.h" + +namespace ARDOUR { + +class AlsaAudioSlave +{ +public: + AlsaAudioSlave ( + const char *play_name, + const char *capt_name, + unsigned int master_rate, + unsigned int master_samples_per_period, + unsigned int slave_rate, + unsigned int slave_samples_per_period, + unsigned int periods_per_cycle); + + virtual ~AlsaAudioSlave (); + + bool start (); + void stop (); + + void cycle_start (double, double, bool); + void cycle_end (); + + uint32_t capt_chan (uint32_t chn, float* dst, uint32_t n_samples); + uint32_t play_chan (uint32_t chn, float* src, uint32_t n_samples); + + bool running () const { return _active; } + void freewheel (bool); + + int state (void) const { return _pcmi.state (); } + uint32_t nplay (void) const { return _pcmi.nplay (); } + uint32_t ncapt (void) const { return _pcmi.ncapt (); } + + PBD::Signal0 Halted; + +protected: + virtual void update_latencies (uint32_t, uint32_t) = 0; + +private: + Alsa_pcmi _pcmi; + + static void* _process_thread (void *); + void* process_thread (); + pthread_t _thread; + + bool _run; /* keep going or stop, ardour thread */ + bool _active; /* is running, process thread */ + + /* DLL, track slave process callback */ + double _t0, _t1; + uint64_t _samples_since_dll_reset; + + double _ratio; + uint32_t _capt_latency; + double _play_latency; + + volatile double _slave_speed; + volatile gint _draining; + + PBD::RingBuffer _rb_capture; + PBD::RingBuffer _rb_playback; + + size_t _samples_per_period; // master + + float* _capt_buff; + float* _play_buff; + float* _src_buff; + + ArdourZita::VResampler _src_capt; + ArdourZita::VResampler _src_play; + + static void reset_resampler (ArdourZita::VResampler&); + +}; // class AlsaAudioSlave + +} // namespace +#endif /* __libbackend_alsa_slave_h__ */ diff --git a/libs/backends/alsa/wscript b/libs/backends/alsa/wscript index 465260d265..d7a15c02fa 100644 --- a/libs/backends/alsa/wscript +++ b/libs/backends/alsa/wscript @@ -23,13 +23,14 @@ def build(bld): 'alsa_midi.cc', 'alsa_rawmidi.cc', 'alsa_sequencer.cc', + 'alsa_slave.cc', 'zita-alsa-pcmi.cc', ] obj.includes = ['.'] obj.name = 'alsa_audiobackend' obj.target = 'alsa_audiobackend' - obj.use = 'libardour libpbd ardouralsautil' - obj.uselib = 'ALSA GLIBMM XML' + obj.use = 'libardour libpbd ardouralsautil zita-resampler' + obj.uselib = 'ALSA GLIBMM XML LIBZRESAMPLER' obj.install_path = os.path.join(bld.env['LIBDIR'], 'backends') obj.defines = ['PACKAGE="' + I18N_PACKAGE + '"', 'ARDOURBACKEND_DLL_EXPORTS'