diff --git a/gtk2_ardour/ardev_common.sh.in b/gtk2_ardour/ardev_common.sh.in index 2775026b37..c3f0767846 100644 --- a/gtk2_ardour/ardev_common.sh.in +++ b/gtk2_ardour/ardev_common.sh.in @@ -23,7 +23,7 @@ export ARDOUR_DATA_PATH=$TOP/share:$TOP/build:$TOP/gtk2_ardour:$TOP/build/gtk2_a export ARDOUR_MIDIMAPS_PATH=$TOP/share/midi_maps export ARDOUR_EXPORT_FORMATS_PATH=$TOP/share/export export ARDOUR_THEMES_PATH=$TOP/gtk2_ardour/themes -export ARDOUR_BACKEND_PATH=$libs/backends/jack:$libs/backends/dummy:$libs/backends/alsa:$libs/backends/coreaudio:$libs/backends/portaudio:$libs/backends/pulseaudio +export ARDOUR_BACKEND_PATH=$libs/backends/jack:$libs/backends/dummy:$libs/backends/alsa:$libs/backends/coreaudio:$libs/backends/portaudio:$libs/backends/pulseaudio:$libs/backends/ndi export ARDOUR_TEST_PATH=$TOP/libs/ardour/test/data export PBD_TEST_PATH=$TOP/libs/pbd/test export EVORAL_TEST_PATH=$TOP/libs/evoral/test/testdata diff --git a/libs/backends/ndi/ndi_backend.cc b/libs/backends/ndi/ndi_backend.cc new file mode 100644 index 0000000000..e1efcc4a1c --- /dev/null +++ b/libs/backends/ndi/ndi_backend.cc @@ -0,0 +1,995 @@ +/* + * Copyright (C) 2021 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "pbd/compose.h" +#include "pbd/error.h" +#include "pbd/file_utils.h" +#include "pbd/pthread_utils.h" + +#include "ardour/port_manager.h" + +#include "ndi_backend.h" + +#include "pbd/i18n.h" + +using namespace ARDOUR; + +static std::string s_instance_name; + +const size_t NDIBackend::_max_buffer_size = 8192; + +#define N_CHANNELS (2) + +NDIBackend::NDIBackend (AudioEngine& e, AudioBackendInfo& info) + : AudioBackend (e, info) + , PortEngineSharedImpl (e, s_instance_name) + , _run (false) + , _active (false) + , _freewheel (false) + , _freewheeling (false) + , _last_process_start (0) + , _samplerate (48000) + , _samples_per_period (1024) + , _systemic_audio_input_latency (0) + , _systemic_audio_output_latency (0) + , _dsp_load (0) + , _processed_samples (0) +{ + _instance_name = s_instance_name; +} + +NDIBackend::~NDIBackend () +{ + clear_ports (); +} + +/* AUDIOBACKEND API */ + +std::string +NDIBackend::name () const +{ + return X_("NDI"); +} + +bool +NDIBackend::is_realtime () const +{ + return true; +} + +std::vector +NDIBackend::enumerate_devices () const +{ + std::vector devices; + devices.push_back (DeviceStatus (_("Default Playback"), true)); + return devices; +} + +std::vector +NDIBackend::available_sample_rates (const std::string&) const +{ + std::vector sr; + sr.push_back (8000.0); + sr.push_back (22050.0); + sr.push_back (24000.0); + sr.push_back (44100.0); + sr.push_back (48000.0); + sr.push_back (88200.0); + sr.push_back (96000.0); + sr.push_back (176400.0); + sr.push_back (192000.0); + return sr; +} + +std::vector +NDIBackend::available_buffer_sizes (const std::string&) const +{ + std::vector bs; + bs.push_back (64); + bs.push_back (128); + bs.push_back (256); + bs.push_back (512); + bs.push_back (1024); + bs.push_back (2048); + bs.push_back (4096); + bs.push_back (8192); + return bs; +} + +uint32_t +NDIBackend::available_input_channel_count (const std::string&) const +{ + return 0; +} + +uint32_t +NDIBackend::available_output_channel_count (const std::string&) const +{ + return N_CHANNELS; +} + +bool +NDIBackend::can_change_sample_rate_when_running () const +{ + return false; +} + +bool +NDIBackend::can_change_buffer_size_when_running () const +{ + return false; +} + +int +NDIBackend::set_device_name (const std::string& d) +{ + return 0; +} + +int +NDIBackend::set_sample_rate (float sr) +{ + if (sr <= 0) { + return -1; + } + _samplerate = sr; + engine.sample_rate_change (sr); + return 0; +} + +int +NDIBackend::set_buffer_size (uint32_t bs) +{ + if (bs <= 0 || bs > _max_buffer_size) { + return -1; + } + + _samples_per_period = bs; + + engine.buffer_size_change (bs); + return 0; +} + +int +NDIBackend::set_interleaved (bool yn) +{ + if (!yn) { + return 0; + } + return -1; +} + +int +NDIBackend::set_input_channels (uint32_t cc) +{ + return 0; +} + +int +NDIBackend::set_output_channels (uint32_t cc) +{ + return 0; +} + +int +NDIBackend::set_systemic_input_latency (uint32_t sl) +{ + return 0; // XXX +} + +int +NDIBackend::set_systemic_output_latency (uint32_t sl) +{ + return 0; // XXX +} + +/* Retrieving parameters */ +std::string +NDIBackend::device_name () const +{ + return _("Default Playback"); +} + +float +NDIBackend::sample_rate () const +{ + return _samplerate; +} + +uint32_t +NDIBackend::buffer_size () const +{ + return _samples_per_period; +} + +bool +NDIBackend::interleaved () const +{ + return false; +} + +uint32_t +NDIBackend::input_channels () const +{ + return N_CHANNELS; +} + +uint32_t +NDIBackend::output_channels () const +{ + return N_CHANNELS; +} + +uint32_t +NDIBackend::systemic_input_latency () const +{ + return _systemic_audio_input_latency; +} + +uint32_t +NDIBackend::systemic_output_latency () const +{ + return _systemic_audio_output_latency; +} + +/* MIDI */ +std::vector +NDIBackend::enumerate_midi_options () const +{ + std::vector midi_options; + midi_options.push_back (get_standard_device_name (DeviceNone)); + return midi_options; +} + +std::vector +NDIBackend::enumerate_midi_devices () const +{ + return std::vector (); +} + +int +NDIBackend::set_midi_option (const std::string& opt) +{ + return 0; +} + +std::string +NDIBackend::midi_option () const +{ + return get_standard_device_name (DeviceNone); +} + +/* External control app */ +std::string +NDIBackend::control_app_name () const +{ + return ""; +} + +void +NDIBackend::launch_control_app () +{ +} + +/* State Control */ + +static void* +pthread_process (void* arg) +{ + NDIBackend* d = static_cast (arg); + d->main_process_thread (); + pthread_exit (0); + return 0; +} + +int +NDIBackend::_start (bool /*for_latency_measurement*/) +{ + if (!_active && _run) { + PBD::error << _("NDIBackend: already active.") << endmsg; + /* recover from 'halted', reap threads */ + stop (); + } + + if (_active || _run) { + PBD::info << _("NDIBackend: already active.") << endmsg; + return BackendReinitializationError; + } + + clear_ports (); + + /* reset internal state */ + _dsp_load = 0; + _freewheeling = false; + _freewheel = false; + _last_process_start = 0; + _systemic_audio_input_latency = 0; + _systemic_audio_output_latency = 0; + + /* TODO connect to NDI-server and prepare stream */ + + /* register ports, notify port-engine */ + if (register_system_ports ()) { + PBD::error << _("NDIBackend: failed to register system ports.") << endmsg; + // close_ndi (); // XXX + return PortRegistrationError; + } + + engine.sample_rate_change (_samplerate); + engine.buffer_size_change (_samples_per_period); + + if (engine.reestablish_ports ()) { + PBD::error << _("NDIBackend: Could not re-establish ports.") << endmsg; + // close_ndi (); // XXX + return PortReconnectError; + } + + engine.reconnect_ports (); + + _run = true; + g_atomic_int_set (&_port_change_flag, 0); + + if (pbd_realtime_pthread_create (PBD_SCHED_FIFO, PBD_RT_PRI_MAIN, PBD_RT_STACKSIZE_PROC, + &_main_thread, pthread_process, this)) { + if (pbd_pthread_create (PBD_RT_STACKSIZE_PROC, &_main_thread, pthread_process, this)) { + PBD::error << _("NDIBackend: failed to create process thread.") << endmsg; + stop (); + _run = false; + return ProcessThreadStartError; + } else { + PBD::warning << _("NDIBackend: cannot acquire realtime permissions.") << endmsg; + } + } + + int timeout = 5000; + while (!_active && --timeout > 0) { + Glib::usleep (1000); + } + + if (timeout == 0 || !_active) { + PBD::error << _("NDIBackend: failed to start process thread.") << endmsg; + _run = false; + // close_ndi (); // XXX + return ProcessThreadStartError; + } + + return NoError; +} + +int +NDIBackend::stop () +{ + void* status; + if (!_run) { + return 0; + } + + _run = false; + /* TODO: STOP NDI */ + + if (pthread_join (_main_thread, &status)) { + PBD::error << _("NDIBackend: failed to terminate.") << endmsg; + return -1; + } + unregister_ports (); + + /* TODO: close NDI connection */ + + return (_active == false) ? 0 : -1; +} + +int +NDIBackend::freewheel (bool onoff) +{ + _freewheeling = onoff; + return 0; +} + +float +NDIBackend::dsp_load () const +{ + return 100.f * _dsp_load; +} + +size_t +NDIBackend::raw_buffer_size (DataType t) +{ + switch (t) { + case DataType::AUDIO: + return _samples_per_period * sizeof (Sample); + case DataType::MIDI: + return _max_buffer_size; + } + return 0; +} + +/* Process time */ +samplepos_t +NDIBackend::sample_time () +{ + return _processed_samples; +} + +samplepos_t +NDIBackend::sample_time_at_cycle_start () +{ + return _processed_samples; +} + +pframes_t +NDIBackend::samples_since_cycle_start () +{ + if (!_active || !_run || _freewheeling || _freewheel) { + return 0; + } + if (_last_process_start == 0) { + return 0; + } + + const int64_t elapsed_time_us = g_get_monotonic_time () - _last_process_start; + return std::max ((pframes_t)0, (pframes_t)rint (1e-6 * elapsed_time_us * _samplerate)); +} + +void* +NDIBackend::ndi_process_thread (void* arg) +{ + ThreadData* td = reinterpret_cast (arg); + boost::function f = td->f; + delete td; + f (); + return 0; +} + +int +NDIBackend::create_process_thread (boost::function func) +{ + pthread_t thread_id; + ThreadData* td = new ThreadData (this, func, PBD_RT_STACKSIZE_PROC); + + if (pbd_realtime_pthread_create (PBD_SCHED_FIFO, PBD_RT_PRI_PROC, PBD_RT_STACKSIZE_PROC, + &thread_id, ndi_process_thread, td)) { + if (pbd_pthread_create (PBD_RT_STACKSIZE_PROC, &thread_id, ndi_process_thread, td)) { + PBD::error << _("AudioEngine: cannot create process thread.") << endmsg; + return -1; + } + } + + _threads.push_back (thread_id); + return 0; +} + +int +NDIBackend::join_process_threads () +{ + int rv = 0; + + for (std::vector::const_iterator i = _threads.begin (); i != _threads.end (); ++i) { + void* status; + if (pthread_join (*i, &status)) { + PBD::error << _("AudioEngine: cannot terminate process thread.") << endmsg; + rv -= 1; + } + } + _threads.clear (); + return rv; +} + +bool +NDIBackend::in_process_thread () +{ + if (pthread_equal (_main_thread, pthread_self ()) != 0) { + return true; + } + + for (std::vector::const_iterator i = _threads.begin (); i != _threads.end (); ++i) { + if (pthread_equal (*i, pthread_self ()) != 0) { + return true; + } + } + return false; +} + +uint32_t +NDIBackend::process_thread_count () +{ + return _threads.size (); +} + +void +NDIBackend::update_latencies () +{ + /* trigger latency callback in RT thread (locked graph) */ + port_connect_add_remove_callback (); +} + +/* PORTENGINE API */ + +void* +NDIBackend::private_handle () const +{ + return NULL; +} + +const std::string& +NDIBackend::my_name () const +{ + return _instance_name; +} + +int +NDIBackend::register_system_ports () +{ + LatencyRange lr; + /* input/source ports */ + lr.min = lr.max = _systemic_audio_input_latency; + for (int i = 1; i <= N_CHANNELS; ++i) { + char tmp[64]; + snprintf (tmp, sizeof (tmp), "system:capture_%d", i); + BackendPortPtr p = add_port (std::string (tmp), DataType::AUDIO, static_cast (IsOutput | IsPhysical | IsTerminal)); + if (!p) { + return -1; + } + set_latency_range (p, true, lr); + _system_inputs.push_back (p); + } + + /* output/sink ports */ + lr.min = lr.max = _systemic_audio_output_latency; + for (int i = 1; i <= N_CHANNELS; ++i) { + char tmp[64]; + snprintf (tmp, sizeof (tmp), "system:playback_%d", i); + BackendPortPtr p = add_port (std::string (tmp), DataType::AUDIO, static_cast (IsInput | IsPhysical | IsTerminal)); + if (!p) { + return -1; + } + set_latency_range (p, true, lr); + //p->set_hw_port_name ("") + _system_outputs.push_back (p); + } + return 0; +} + +BackendPort* +NDIBackend::port_factory (std::string const& name, ARDOUR::DataType type, ARDOUR::PortFlags flags) +{ + BackendPort* port = 0; + + switch (type) { + case DataType::AUDIO: + port = new NDIAudioPort (*this, name, flags); + break; + case DataType::MIDI: + port = new NDIMidiPort (*this, name, flags); + break; + default: + PBD::error << string_compose (_("%1::register_port: Invalid Data Type."), _instance_name) << endmsg; + return 0; + } + + return port; +} + +/* MIDI */ +int +NDIBackend::midi_event_get (pframes_t& timestamp, size_t& size, uint8_t const** buf, void* port_buffer, uint32_t event_index) +{ + assert (buf && port_buffer); + NDIMidiBuffer& source = *static_cast (port_buffer); + if (event_index >= source.size ()) { + return -1; + } + NDIMidiEvent* const event = source[event_index].get (); + + timestamp = event->timestamp (); + size = event->size (); + *buf = event->data (); + return 0; +} + +int +NDIBackend::midi_event_put (void* port_buffer, + pframes_t timestamp, + const uint8_t* buffer, size_t size) +{ + assert (buffer && port_buffer); + NDIMidiBuffer& dst = *static_cast (port_buffer); + dst.push_back (boost::shared_ptr (new NDIMidiEvent (timestamp, buffer, size))); + return 0; +} + +uint32_t +NDIBackend::get_midi_event_count (void* port_buffer) +{ + assert (port_buffer); + return static_cast (port_buffer)->size (); +} + +void +NDIBackend::midi_clear (void* port_buffer) +{ + assert (port_buffer); + NDIMidiBuffer* buf = static_cast (port_buffer); + assert (buf); + buf->clear (); +} + +/* Monitoring */ + +bool +NDIBackend::can_monitor_input () const +{ + return false; +} + +int +NDIBackend::request_input_monitoring (PortEngine::PortHandle, bool) +{ + return -1; +} + +int +NDIBackend::ensure_input_monitoring (PortEngine::PortHandle, bool) +{ + return -1; +} + +bool +NDIBackend::monitoring_input (PortEngine::PortHandle) +{ + return false; +} + +/* Latency management */ + +void +NDIBackend::set_latency_range (PortEngine::PortHandle port_handle, bool for_playback, LatencyRange latency_range) +{ + BackendPortPtr port = boost::dynamic_pointer_cast (port_handle); + if (!valid_port (port)) { + PBD::error << _("NDIAudioPort::set_latency_range (): invalid port.") << endmsg; + } + port->set_latency_range (latency_range, for_playback); +} + +LatencyRange +NDIBackend::get_latency_range (PortEngine::PortHandle port_handle, bool for_playback) +{ + BackendPortPtr port = boost::dynamic_pointer_cast (port_handle); + LatencyRange r; + + if (!valid_port (port)) { + PBD::error << _("NDIAudioPort::get_latency_range (): invalid port.") << endmsg; + r.min = 0; + r.max = 0; + return r; + } + + r = port->latency_range (for_playback); + + if (port->is_physical () && port->is_terminal ()) { + if (port->is_input () && for_playback) { + r.min += _samples_per_period + _systemic_audio_output_latency; + r.max += _samples_per_period + _systemic_audio_output_latency; + } + if (port->is_output () && !for_playback) { + r.min += _samples_per_period + _systemic_audio_input_latency; + r.max += _samples_per_period + _systemic_audio_input_latency; + } + } + + return r; +} + +/* Getting access to the data buffer for a port */ + +void* +NDIBackend::get_buffer (PortEngine::PortHandle port_handle, pframes_t nframes) +{ + BackendPortPtr port = boost::dynamic_pointer_cast (port_handle); + + assert (port); + assert (valid_port (port)); + + return port->get_buffer (nframes); +} + +/* Engine Process */ +void* +NDIBackend::main_process_thread () +{ + AudioEngine::thread_init_callback (this); + _active = true; + _processed_samples = 0; + + manager.registration_callback (); + manager.graph_order_callback (); + + /* TODO begin NDI streaming */ + + _dsp_load_calc.reset (); + + while (_run) { + if (_freewheeling != _freewheel) { + _freewheel = _freewheeling; + engine.freewheel_callback (_freewheel); + + if (_freewheel) { + /* TODO pause NDI stream */ + } + + /* TODO flush stream (before and after freewheeling) */ + + if (!_freewheel) { + /* TODO resume NDI stream */ + _dsp_load_calc.reset (); + } + } + + if (!_freewheel) { + /* TODO: wait for NDI to provide data */ + + int64_t clock1 = g_get_monotonic_time (); + /* call engine process callback */ + _last_process_start = g_get_monotonic_time (); + if (engine.process_callback (_samples_per_period)) { + /* ERROR -- TODO: stop NDI stream */ + _active = false; + return 0; + } + + /* write back audio */ + uint32_t i = 0; + float buf[_max_buffer_size * N_CHANNELS]; + assert (_system_outputs.size () == N_CHANNELS); + + /* interleave */ + for (std::vector::const_iterator it = _system_outputs.begin (); it != _system_outputs.end (); ++it, ++i) { + BackendPortPtr port = boost::dynamic_pointer_cast (*it); + const float* src = (const float*)port->get_buffer (_samples_per_period); + for (size_t n = 0; n < _samples_per_period; ++n) { + buf[N_CHANNELS * n + i] = src[n]; + } + } + /* TODO write interlevaed data to NDI */ + + _processed_samples += _samples_per_period; + + _dsp_load_calc.set_max_time (_samplerate, _samples_per_period); + _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 (); + +#if 1 // Sleep for now + const int64_t elapsed_time = _dsp_load_calc.elapsed_time_us (); + const int64_t nominal_time = _dsp_load_calc.get_max_time_us (); + const int64_t sleepy = nominal_time - elapsed_time; + Glib::usleep (std::max ((int64_t) 100, sleepy)); +#endif + } else { + /* Freewheelin' */ + _last_process_start = 0; + if (engine.process_callback (_samples_per_period)) { + _active = false; + return 0; + } + + _dsp_load = 1.0f; + Glib::usleep (100); // don't hog cpu + } + + bool connections_changed = false; + bool ports_changed = false; + if (!pthread_mutex_trylock (&_port_callback_mutex)) { + if (g_atomic_int_compare_and_exchange (&_port_change_flag, 1, 0)) { + ports_changed = true; + } + if (!_port_connection_queue.empty ()) { + connections_changed = true; + } + while (!_port_connection_queue.empty ()) { + PortConnectData* c = _port_connection_queue.back (); + manager.connect_callback (c->a, c->b, c->c); + _port_connection_queue.pop_back (); + delete c; + } + pthread_mutex_unlock (&_port_callback_mutex); + } + if (ports_changed) { + manager.registration_callback (); + } + if (connections_changed) { + manager.graph_order_callback (); + } + if (connections_changed || ports_changed) { + update_system_port_latencies (); + engine.latency_callback (false); + engine.latency_callback (true); + } + } + + _active = false; + if (_run) { + engine.halted_callback ("NDI I/O error."); + } + return 0; +} + +/******************************************************************************/ + +static boost::shared_ptr _instance; + +static boost::shared_ptr backend_factory (AudioEngine& e); +static int instantiate (const std::string& arg1, const std::string& /* arg2 */); +static int deinstantiate (); +static bool already_configured (); +static bool available (); + +static ARDOUR::AudioBackendInfo _descriptor = { + _("NDI"), + instantiate, + deinstantiate, + backend_factory, + already_configured, + available +}; + +static boost::shared_ptr +backend_factory (AudioEngine& e) +{ + if (!_instance) { + _instance.reset (new NDIBackend (e, _descriptor)); + } + return _instance; +} + +static int +instantiate (const std::string& arg1, const std::string& /* arg2 */) +{ + s_instance_name = arg1; + return 0; +} + +static int +deinstantiate () +{ + _instance.reset (); + return 0; +} + +static bool +already_configured () +{ + return false; +} + +static bool +available () +{ + return true; +} + +extern "C" ARDOURBACKEND_API ARDOUR::AudioBackendInfo* descriptor () +{ + return &_descriptor; +} + +/******************************************************************************/ + +NDIAudioPort::NDIAudioPort (NDIBackend& b, const std::string& name, PortFlags flags) + : BackendPort (b, name, flags) +{ + memset (_buffer, 0, sizeof (_buffer)); + mlock (_buffer, sizeof (_buffer)); +} + +NDIAudioPort::~NDIAudioPort () +{ +} + +void* +NDIAudioPort::get_buffer (pframes_t n_samples) +{ + if (is_input ()) { + const std::set& connections = get_connections (); + std::set::const_iterator it = connections.begin (); + + if (it == connections.end ()) { + memset (_buffer, 0, n_samples * sizeof (Sample)); + } else { + boost::shared_ptr source = boost::dynamic_pointer_cast (*it); + assert (source && source->is_output ()); + memcpy (_buffer, source->const_buffer (), n_samples * sizeof (Sample)); + while (++it != connections.end ()) { + source = boost::dynamic_pointer_cast (*it); + assert (source && source->is_output ()); + Sample* dst = buffer (); + const Sample* src = source->const_buffer (); + for (uint32_t s = 0; s < n_samples; ++s, ++dst, ++src) { + *dst += *src; + } + } + } + } + return _buffer; +} + +NDIMidiPort::NDIMidiPort (NDIBackend& b, const std::string& name, PortFlags flags) + : BackendPort (b, name, flags) +{ + _buffer.clear (); + _buffer.reserve (256); +} + +NDIMidiPort::~NDIMidiPort () +{ +} + +struct MidiEventSorter { + bool + operator() (const boost::shared_ptr& a, const boost::shared_ptr& b) + { + return *a < *b; + } +}; + +void* NDIMidiPort::get_buffer (pframes_t /*n_samples*/) +{ + if (is_input ()) { + _buffer.clear (); + const std::set& connections = get_connections (); + for (std::set::const_iterator i = connections.begin (); + i != connections.end (); + ++i) { + const NDIMidiBuffer* src = boost::dynamic_pointer_cast (*i)->const_buffer (); + for (NDIMidiBuffer::const_iterator it = src->begin (); it != src->end (); ++it) { + _buffer.push_back (*it); + } + } + std::stable_sort (_buffer.begin (), _buffer.end (), MidiEventSorter ()); + } + return &_buffer; +} + +NDIMidiEvent::NDIMidiEvent (const pframes_t timestamp, const uint8_t* data, size_t size) + : _size (size) + , _timestamp (timestamp) +{ + if (size > 0 && size < MaxNDIMidiEventSize) { + memcpy (_data, data, size); + } +} + +NDIMidiEvent::NDIMidiEvent (const NDIMidiEvent& other) + : _size (other.size ()) + , _timestamp (other.timestamp ()) +{ + if (other.size () && other.const_data ()) { + assert (other._size < MaxNDIMidiEventSize); + memcpy (_data, other._data, other._size); + } +}; diff --git a/libs/backends/ndi/ndi_backend.h b/libs/backends/ndi/ndi_backend.h new file mode 100644 index 0000000000..4d2b70f697 --- /dev/null +++ b/libs/backends/ndi/ndi_backend.h @@ -0,0 +1,356 @@ +/* + * Copyright (C) 2019 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef __libbackend_ndi_audiobackend_h__ +#define __libbackend_ndi_audiobackend_h__ + +#include +#include +#include +#include + +#include +#include + +#include + +#include "pbd/natsort.h" + +#include "ardour/audio_backend.h" +#include "ardour/dsp_load_calculator.h" +#include "ardour/port_engine_shared.h" + +#define MaxNDIMidiEventSize (256) + +namespace ARDOUR { + +class NDIBackend; + +class NDIMidiEvent : public BackendMIDIEvent +{ +public: + NDIMidiEvent (const pframes_t timestamp, const uint8_t* data, size_t size); + NDIMidiEvent (const NDIMidiEvent& other); + + size_t size () const { return _size; } + pframes_t timestamp () const { return _timestamp; } + const uint8_t* data () const { return _data; } + const uint8_t* const_data () const { return _data; } + +private: + size_t _size; + pframes_t _timestamp; + uint8_t _data[MaxNDIMidiEventSize]; +}; + +typedef std::vector> NDIMidiBuffer; + +class NDIAudioPort : public BackendPort +{ +public: + NDIAudioPort (NDIBackend& b, const std::string&, PortFlags); + ~NDIAudioPort (); + + DataType type () const { return DataType::AUDIO; } + Sample* buffer () { return _buffer; } + const Sample* const_buffer () const { return _buffer; } + void* get_buffer (pframes_t nframes); + +private: + Sample _buffer[8192]; +}; + +class NDIMidiPort : public BackendPort +{ +public: + NDIMidiPort (NDIBackend& b, const std::string&, PortFlags); + ~NDIMidiPort (); + + DataType type () const { return DataType::MIDI; } + void* get_buffer (pframes_t nframes); + const NDIMidiBuffer* const_buffer () const { return &_buffer; } + +private: + NDIMidiBuffer _buffer; +}; + +class NDIBackend : public AudioBackend, public PortEngineSharedImpl +{ +public: + NDIBackend (AudioEngine& e, AudioBackendInfo& info); + ~NDIBackend (); + + /* AUDIOBACKEND API */ + + std::string name () const; + bool is_realtime () const; + + std::vector enumerate_devices () const; + std::vector available_sample_rates (const std::string& device) const; + std::vector available_buffer_sizes (const std::string& device) const; + uint32_t available_input_channel_count (const std::string& device) const; + uint32_t available_output_channel_count (const std::string& device) const; + + bool can_change_sample_rate_when_running () const; + bool can_change_buffer_size_when_running () const; + bool can_measure_systemic_latency () const { return false; } + + int set_device_name (const std::string&); + int set_sample_rate (float); + int set_buffer_size (uint32_t); + int set_interleaved (bool yn); + int set_input_channels (uint32_t); + int set_output_channels (uint32_t); + int set_systemic_input_latency (uint32_t); + int set_systemic_output_latency (uint32_t); + int set_systemic_midi_input_latency (std::string const, uint32_t) { return 0; } + int set_systemic_midi_output_latency (std::string const, uint32_t) { return 0; } + int reset_device () { return 0; }; + + /* Retrieving parameters */ + std::string device_name () const; + float sample_rate () const; + uint32_t buffer_size () const; + bool interleaved () const; + uint32_t input_channels () const; + uint32_t output_channels () const; + uint32_t systemic_input_latency () const; + uint32_t systemic_output_latency () const; + uint32_t systemic_midi_input_latency (std::string const) const { return 0; } + uint32_t systemic_midi_output_latency (std::string const) const { return 0; } + + /* External control app */ + std::string control_app_name () const; + void launch_control_app (); + + /* MIDI */ + std::vector enumerate_midi_options () const; + std::vector enumerate_midi_devices () const; + int set_midi_option (const std::string&); + std::string midi_option () const; + + int set_midi_device_enabled (std::string const, bool) { return 0; } + bool midi_device_enabled (std::string const) const { return true; } + bool can_set_systemic_midi_latencies () const { return false; } + + /* State Control */ +protected: + int _start (bool for_latency_measurement); + +public: + int stop (); + int freewheel (bool); + float dsp_load () const; + size_t raw_buffer_size (DataType t); + + /* Process time */ + samplepos_t sample_time (); + samplepos_t sample_time_at_cycle_start (); + pframes_t samples_since_cycle_start (); + + int create_process_thread (boost::function func); + int join_process_threads (); + bool in_process_thread (); + uint32_t process_thread_count (); + + void update_latencies (); + + /* PORTENGINE API */ + + void* private_handle () const; + const std::string& my_name () const; + + /* PortEngine API - forwarded to PortEngineSharedImpl */ + + bool port_is_physical (PortEngine::PortHandle ph) const + { + return PortEngineSharedImpl::port_is_physical (ph); + } + void get_physical_outputs (DataType type, std::vector& results) + { + PortEngineSharedImpl::get_physical_outputs (type, results); + } + void get_physical_inputs (DataType type, std::vector& results) + { + PortEngineSharedImpl::get_physical_inputs (type, results); + } + ChanCount n_physical_outputs () const + { + return PortEngineSharedImpl::n_physical_outputs (); + } + ChanCount n_physical_inputs () const + { + return PortEngineSharedImpl::n_physical_inputs (); + } + uint32_t port_name_size () const + { + return PortEngineSharedImpl::port_name_size (); + } + int set_port_name (PortEngine::PortHandle ph, const std::string& name) + { + return PortEngineSharedImpl::set_port_name (ph, name); + } + std::string get_port_name (PortEngine::PortHandle ph) const + { + return PortEngineSharedImpl::get_port_name (ph); + } + PortFlags get_port_flags (PortEngine::PortHandle ph) const + { + return PortEngineSharedImpl::get_port_flags (ph); + } + PortEngine::PortPtr get_port_by_name (std::string const& name) const + { + return PortEngineSharedImpl::get_port_by_name (name); + } + int get_port_property (PortEngine::PortHandle ph, const std::string& key, std::string& value, std::string& type) const + { + return PortEngineSharedImpl::get_port_property (ph, key, value, type); + } + int set_port_property (PortEngine::PortHandle ph, const std::string& key, const std::string& value, const std::string& type) + { + return PortEngineSharedImpl::set_port_property (ph, key, value, type); + } + int get_ports (const std::string& port_name_pattern, DataType type, PortFlags flags, std::vector& results) const + { + return PortEngineSharedImpl::get_ports (port_name_pattern, type, flags, results); + } + DataType port_data_type (PortEngine::PortHandle ph) const + { + return PortEngineSharedImpl::port_data_type (ph); + } + PortEngine::PortPtr register_port (const std::string& shortname, ARDOUR::DataType type, ARDOUR::PortFlags flags) + { + return PortEngineSharedImpl::register_port (shortname, type, flags); + } + void unregister_port (PortHandle ph) + { + if (!_run) { + return; + } + PortEngineSharedImpl::unregister_port (ph); + } + int connect (const std::string& src, const std::string& dst) + { + return PortEngineSharedImpl::connect (src, dst); + } + int disconnect (const std::string& src, const std::string& dst) + { + return PortEngineSharedImpl::disconnect (src, dst); + } + int connect (PortEngine::PortHandle ph, const std::string& other) + { + return PortEngineSharedImpl::connect (ph, other); + } + int disconnect (PortEngine::PortHandle ph, const std::string& other) + { + return PortEngineSharedImpl::disconnect (ph, other); + } + int disconnect_all (PortEngine::PortHandle ph) + { + return PortEngineSharedImpl::disconnect_all (ph); + } + bool connected (PortEngine::PortHandle ph, bool process_callback_safe) + { + return PortEngineSharedImpl::connected (ph, process_callback_safe); + } + bool connected_to (PortEngine::PortHandle ph, const std::string& other, bool process_callback_safe) + { + return PortEngineSharedImpl::connected_to (ph, other, process_callback_safe); + } + bool physically_connected (PortEngine::PortHandle ph, bool process_callback_safe) + { + return PortEngineSharedImpl::physically_connected (ph, process_callback_safe); + } + int get_connections (PortEngine::PortHandle ph, std::vector& results, bool process_callback_safe) + { + return PortEngineSharedImpl::get_connections (ph, results, process_callback_safe); + } + + /* MIDI */ + int midi_event_get (pframes_t& timestamp, size_t& size, uint8_t const** buf, void* port_buffer, uint32_t event_index); + int midi_event_put (void* port_buffer, pframes_t timestamp, const uint8_t* buffer, size_t size); + uint32_t get_midi_event_count (void* port_buffer); + void midi_clear (void* port_buffer); + + /* Monitoring */ + + bool can_monitor_input () const; + int request_input_monitoring (PortHandle, bool); + int ensure_input_monitoring (PortHandle, bool); + bool monitoring_input (PortHandle); + + /* Latency management */ + + void set_latency_range (PortHandle, bool for_playback, LatencyRange); + LatencyRange get_latency_range (PortHandle, bool for_playback); + + /* Getting access to the data buffer for a port */ + + void* get_buffer (PortHandle, pframes_t); + + void* main_process_thread (); + +private: + std::string _instance_name; + + bool _run; /* keep going or stop, ardour thread */ + bool _active; /* is running, process thread */ + bool _freewheel; + bool _freewheeling; + + uint64_t _last_process_start; + + /* audio settings */ + float _samplerate; + size_t _samples_per_period; + uint32_t _systemic_audio_input_latency; + uint32_t _systemic_audio_output_latency; + + static const size_t _max_buffer_size; + + /* processing */ + float _dsp_load; + ARDOUR::DSPLoadCalculator _dsp_load_calc; + samplecnt_t _processed_samples; + pthread_t _main_thread; + + /* process threads */ + static void* ndi_process_thread (void*); + std::vector _threads; + + struct ThreadData { + NDIBackend* engine; + boost::function f; + size_t stacksize; + + ThreadData (NDIBackend* e, boost::function fp, size_t stacksz) + : engine (e) + , f (fp) + , stacksize (stacksz) + { + } + }; + + /* port engine */ + BackendPort* port_factory (std::string const& name, ARDOUR::DataType dt, ARDOUR::PortFlags flags); + int register_system_ports (); +}; + +} // namespace ARDOUR + +#endif /* __libbackend_ndi_audiobackend_h__ */ diff --git a/libs/backends/ndi/wscript b/libs/backends/ndi/wscript new file mode 100644 index 0000000000..f574d3e275 --- /dev/null +++ b/libs/backends/ndi/wscript @@ -0,0 +1,32 @@ +#!/usr/bin/env python +from waflib.extras import autowaf as autowaf +import os +import sys +import re + +I18N_PACKAGE = 'ndi-backend' + +# Mandatory variables +top = '.' +out = 'build' + +def options(opt): + autowaf.set_options(opt) + +def configure(conf): + pass + +def build(bld): + obj = bld(features = 'cxx cxxshlib') + obj.source = [ + 'ndi_backend.cc', + ] + obj.includes = ['.'] + obj.name = 'ndi_backend' + obj.target = 'ndi_backend' + obj.use = 'libardour libpbd' + obj.uselib = 'GLIBMM XML' + obj.install_path = os.path.join(bld.env['LIBDIR'], 'backends') + obj.defines = ['PACKAGE="' + I18N_PACKAGE + '"', + 'ARDOURBACKEND_DLL_EXPORTS' + ] diff --git a/tools/linux_packaging/build b/tools/linux_packaging/build index ba412e939a..28a8a5e6ca 100755 --- a/tools/linux_packaging/build +++ b/tools/linux_packaging/build @@ -418,7 +418,7 @@ done cp $BUILD_ROOT/libs/panners/*/lib*.so* $Panners # Backends -for backend in jack alsa dummy wavesaudio pulseaudio; do +for backend in jack alsa dummy wavesaudio pulseaudio ndi; do cp $BUILD_ROOT/libs/backends/$backend/lib*.so* $Backends done diff --git a/wscript b/wscript index 6488749633..cb44ac960b 100644 --- a/wscript +++ b/wscript @@ -1344,6 +1344,9 @@ int main () { return 0; } if opts.build_tests and 'dummy' not in backends: backends += ['dummy'] + if True: + backends += ['ndi'] + conf.env['BACKENDS'] = backends conf.env['BUILD_JACKBACKEND'] = any('jack' in b for b in backends) conf.env['BUILD_ALSABACKEND'] = any('alsa' in b for b in backends) @@ -1351,6 +1354,7 @@ int main () { return 0; } conf.env['BUILD_PABACKEND'] = any('portaudio' in b for b in backends) conf.env['BUILD_CORECRAPPITA'] = any('coreaudio' in b for b in backends) conf.env['BUILD_PULSEAUDIO'] = any('pulseaudio' in b for b in backends) + conf.env['BUILD_NDIBACKEND'] = any('ndi' in b for b in backends) if backends == [''] or not ( conf.env['BUILD_JACKBACKEND'] @@ -1497,6 +1501,7 @@ const char* const ardour_config_info = "\\n\\ write_config_text('ALSA Backend', conf.env['BUILD_ALSABACKEND']) write_config_text('Dummy backend', conf.env['BUILD_DUMMYBACKEND']) write_config_text('JACK Backend', conf.env['BUILD_JACKBACKEND']) + write_config_text('NDI Backend', conf.env['BUILD_NDIBACKEND']) write_config_text('Pulseaudio Backend', conf.env['BUILD_PULSEAUDIO']) config_text.write("\\n\\\n") write_config_text('Buildstack', conf.env['DEPSTACK_REV'])