Prototype using additional ALSA devices (w/resampling).
authorRobin Gareus <robin@gareus.org>
Mon, 18 Sep 2017 14:13:47 +0000 (16:13 +0200)
committerRobin Gareus <robin@gareus.org>
Mon, 18 Sep 2017 14:13:55 +0000 (16:13 +0200)
libs/backends/alsa/alsa_audiobackend.cc
libs/backends/alsa/alsa_audiobackend.h
libs/backends/alsa/alsa_slave.cc [new file with mode: 0644]
libs/backends/alsa/alsa_slave.h [new file with mode: 0644]
libs/backends/alsa/wscript

index 93ec8dead84d8e36b2dc6fb1c108cb1abaaf23d0..5abb4715026a6612c2e89022a120d971a3bb9f80 100644 (file)
 
 #include <glibmm.h>
 
+#include <boost/foreach.hpp>
+#include <boost/tokenizer.hpp>
+
 #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<char> sep (";");
+               boost::tokenizer<boost::char_separator<char> > 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<AlsaPort*>::const_iterator it = (*s)->inputs.begin (); it != (*s)->inputs.end (); ++it) {
+                                               unregister_port (*it);
+                                       }
+                                       for (std::vector<AlsaPort*>::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<AlsaPort*>::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<AlsaPort*>::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<AlsaPort*>::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<PortFlags>(IsOutput | IsPhysical | IsTerminal));
+               if (!p) goto errout;
+               AlsaPort *ap = static_cast<AlsaPort*>(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<PortFlags>(IsInput | IsPhysical | IsTerminal));
+               if (!p) goto errout;
+               AlsaPort *ap = static_cast<AlsaPort*>(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<AlsaPort*>::const_iterator it = inputs.begin (); it != inputs.end (); ++it) {
+               (*it)->set_latency_range (lr, false);
+        }
+
+       lr.min = lr.max = play;
+       for (std::vector<AlsaPort*>::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 */
+}
 
 /******************************************************************************/
 
index e90bec5df9daeffa2dd2deae25c77581a0b940bf..ad3bb419493d1539a62c4b50253faf32e9e7879b 100644 (file)
@@ -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<pthread_t> _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<AlsaPort *> inputs;
+                               std::vector<AlsaPort *> outputs;
+
+                               PBD::Signal0<void> UpdateLatency;
+                               PBD::ScopedConnection latency_connection;
+
+                       protected:
+                               void update_latencies (uint32_t, uint32_t);
+
+                       private:
+                               PBD::ScopedConnection _halted_connection;
+                               void halted ();
+               };
+
+               typedef std::vector<AudioSlave*> 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 (file)
index 0000000..e1061cc
--- /dev/null
@@ -0,0 +1,523 @@
+/*
+ * Copyright (C) 2017 Robin Gareus <robin@gareus.org>
+ *
+ * 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 <cmath>
+#include <glibmm.h>
+
+#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<AlsaAudioSlave*> (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<float>::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<float>::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<float>::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<float>::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 (file)
index 0000000..28cd20a
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2017 Robin Gareus <robin@gareus.org>
+ *
+ * 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 <pthread.h>
+
+#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<void> 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<float> _rb_capture;
+       PBD::RingBuffer<float> _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__ */
index 465260d2652311adc528d51deedb25af98cfb869..d7a15c02fa442f6c694fb91255ef29cd7e586797 100644 (file)
@@ -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'