diff --git a/libs/ardour/ardour/rc_configuration_vars.h b/libs/ardour/ardour/rc_configuration_vars.h index 9d138b0f9c..e3b7a1b5f1 100644 --- a/libs/ardour/ardour/rc_configuration_vars.h +++ b/libs/ardour/ardour/rc_configuration_vars.h @@ -134,7 +134,6 @@ CONFIG_VARIABLE (ListenPosition, listen_position, "listen-position", AfterFaderL CONFIG_VARIABLE (PFLPosition, pfl_position, "pfl-position", PFLFromAfterProcessors) CONFIG_VARIABLE (AFLPosition, afl_position, "afl-position", AFLFromAfterProcessors) CONFIG_VARIABLE (bool, use_monitor_bus, "use-monitor-bus", false) -CONFIG_VARIABLE (bool, use_surround_master, "use-surround-master", false) CONFIG_VARIABLE (bool, solo_control_is_listen_control, "solo-control-is-listen-control", false) CONFIG_VARIABLE (bool, exclusive_solo, "exclusive-solo", false) diff --git a/libs/ardour/ardour/route.h b/libs/ardour/ardour/route.h index aad8930af4..e25e744a28 100644 --- a/libs/ardour/ardour/route.h +++ b/libs/ardour/ardour/route.h @@ -97,6 +97,8 @@ class SoloIsolateControl; class PhaseControl; class MonitorControl; class TriggerBox; +class SurroundReturn; +class SurroundSend; class LIBARDOUR_API Route : public Stripable, public GraphNode, @@ -141,6 +143,8 @@ public: static void set_name_in_state (XMLNode &, const std::string &); std::shared_ptr monitoring_control() const { return _monitoring_control; } + std::shared_ptr surround_send() const { return _surround_send; } + std::shared_ptr surround_return() const { return _surround_return; } MonitorState monitoring_state () const; virtual MonitorState get_input_monitoring_state (bool recording, bool talkback) const { return MonitoringSilence; } @@ -183,7 +187,7 @@ public: void push_solo_upstream (int32_t delta); void push_solo_isolate_upstream (int32_t delta); bool can_solo () const { - return !(is_master() || is_monitor() || is_auditioner() || is_foldbackbus()); + return !(is_singleton() || is_auditioner() || is_foldbackbus()); } bool is_safe () const { return _solo_safe_control->get_value(); @@ -192,6 +196,7 @@ public: return can_solo() || is_foldbackbus (); } void enable_monitor_send (); + void enable_surround_send (); void set_denormal_protection (bool yn); bool denormal_protection() const; @@ -260,7 +265,11 @@ public: std::shared_ptr automation_control_recurse (PBD::ID const & id) const; - void automatables (PBD::ControllableSet&) const; + void automatables (PBD::ControllableSet&) const; + + void queue_surround_processors_changed () { + _pending_surround_send.store (1); + } /* special processors */ @@ -433,6 +442,7 @@ public: int add_aux_send (std::shared_ptr, std::shared_ptr); int add_foldback_send (std::shared_ptr, bool post_fader); void remove_monitor_send (); + void remove_surround_send (); /** * return true if this route feeds the first argument directly, via @@ -669,6 +679,8 @@ protected: std::shared_ptr _beatbox; #endif std::shared_ptr _monitoring_control; + std::shared_ptr _surround_send; + std::shared_ptr _surround_return; DiskIOPoint _disk_io_point; @@ -676,13 +688,15 @@ protected: EmitNone = 0x00, EmitMeterChanged = 0x01, EmitMeterVisibilityChange = 0x02, - EmitRtProcessorChange = 0x04 + EmitRtProcessorChange = 0x04, + EmitSendReturnChange = 0x08 }; - ProcessorList _pending_processor_order; - std::atomic _pending_process_reorder; // atomic - std::atomic _pending_listen_change; // atomic - std::atomic _pending_signals; // atomic + ProcessorList _pending_processor_order; + std::atomic _pending_process_reorder; + std::atomic _pending_listen_change; + std::atomic _pending_surround_send; + std::atomic _pending_signals; MeterPoint _meter_point; MeterPoint _pending_meter_point; diff --git a/libs/ardour/ardour/session.h b/libs/ardour/ardour/session.h index 8ef6ea3e6f..1f76f15b2a 100644 --- a/libs/ardour/ardour/session.h +++ b/libs/ardour/ardour/session.h @@ -986,6 +986,7 @@ public: PBD::Signal0 IsolatedChanged; PBD::Signal0 MonitorChanged; PBD::Signal0 MonitorBusAddedOrRemoved; + PBD::Signal0 SurroundMasterAddedOrRemoved; PBD::Signal0 session_routes_reconnected; @@ -998,6 +999,7 @@ public: std::shared_ptr monitor_out() const { return _monitor_out; } std::shared_ptr master_out() const { return _master_out; } std::shared_ptr master_volume () const; + std::shared_ptr surround_master() const { return _surround_master; } PresentationInfo::order_t master_order_key () const { return _master_out ? _master_out->presentation_info ().order () : -1; } bool ensure_stripable_sort_order (); @@ -1024,18 +1026,24 @@ public: } uint32_t next_send_id(); + uint32_t next_surround_send_id(); uint32_t next_aux_send_id(); uint32_t next_return_id(); uint32_t next_insert_id(); void mark_send_id (uint32_t); + void mark_surround_send_id (uint32_t); void mark_aux_send_id (uint32_t); void mark_return_id (uint32_t); void mark_insert_id (uint32_t); void unmark_send_id (uint32_t); + void unmark_surround_send_id (uint32_t); void unmark_aux_send_id (uint32_t); void unmark_return_id (uint32_t); void unmark_insert_id (uint32_t); + bool vapor_barrier (); + bool vapor_export_barrier (); + /* s/w "RAID" management */ boost::optional available_capture_duration(); @@ -1485,6 +1493,12 @@ private: void add_monitor_section (); void remove_monitor_section (); + void add_surround_master (); + void remove_surround_master (); + + boost::optional _vapor_available; + boost::optional _vapor_exportable; + void update_latency (bool playback); void set_owned_port_public_latency (bool playback); bool update_route_latency (bool reverse, bool apply_to_delayline, bool* delayline_update_needed); @@ -2060,6 +2074,7 @@ private: /* INSERT AND SEND MANAGEMENT */ boost::dynamic_bitset send_bitset; + boost::dynamic_bitset surround_send_bitset; boost::dynamic_bitset aux_send_bitset; boost::dynamic_bitset return_bitset; boost::dynamic_bitset insert_bitset; @@ -2198,13 +2213,16 @@ private: std::shared_ptr _master_out; std::shared_ptr _monitor_out; + std::shared_ptr _surround_master; friend class PortManager; void auto_connect_master_bus (); void auto_connect_monitor_bus (); + void auto_connect_surround_master (); void auto_connect_io (std::shared_ptr); void setup_route_monitor_sends (bool enable, bool need_process_lock); + void setup_route_surround_sends (bool enable, bool need_process_lock); int find_all_sources (std::string path, std::set& result); int find_all_sources_across_snapshots (std::set& result, bool exclude_this_snapshot); diff --git a/libs/ardour/ardour/session_configuration_vars.h b/libs/ardour/ardour/session_configuration_vars.h index 2a48fbb264..f28dbb776e 100644 --- a/libs/ardour/ardour/session_configuration_vars.h +++ b/libs/ardour/ardour/session_configuration_vars.h @@ -65,6 +65,7 @@ CONFIG_VARIABLE (std::string, timecode_generator_offset, "timecode-generator-off CONFIG_VARIABLE (bool, midi_copy_is_fork, "midi-copy-is-fork", true) CONFIG_VARIABLE (bool, tracks_follow_session_time, "tracks-follow-session-time", false) CONFIG_VARIABLE (bool, realtime_export, "realtime-export", false) +CONFIG_VARIABLE (bool, use_surround_master, "use-surround-master", false) /* Video-settings are saved with the session and belong to the session. * headless ardour could remote control xjadeo for example. diff --git a/libs/ardour/ardour/surround_pannable.h b/libs/ardour/ardour/surround_pannable.h new file mode 100644 index 0000000000..660e7a01b4 --- /dev/null +++ b/libs/ardour/ardour/surround_pannable.h @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2023 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 _ardour_surround_pannable_h_ +#define _ardour_surround_pannable_h_ + +#include "pbd/stateful.h" + +#include "ardour/automatable.h" +#include "ardour/automation_control.h" +#include "ardour/session_handle.h" + +namespace ARDOUR +{ + +class LIBARDOUR_API SurroundControllable : public AutomationControl +{ +public: + SurroundControllable (Session&, Evoral::Parameter, Temporal::TimeDomainProvider const&); + std::string get_user_string () const; + +private: +}; + +class LIBARDOUR_API SurroundPannable : public Automatable, public PBD::Stateful, public SessionHandleRef +{ +public: + SurroundPannable (Session& s, uint32_t chn, Temporal::TimeDomainProvider const &); + ~SurroundPannable (); + + std::shared_ptr pan_pos_x; + std::shared_ptr pan_pos_y; + std::shared_ptr pan_pos_z; + std::shared_ptr pan_size; + std::shared_ptr pan_snap; + std::shared_ptr binaural_render_mode; + + void set_automation_state (AutoState); + AutoState automation_state() const { return _auto_state; } + PBD::Signal1 automation_state_changed; + + bool automation_playback() const { + return (_auto_state & Play) || ((_auto_state & (Touch | Latch)) && !touching()); + } + + bool touching() const; + + XMLNode& get_state () const; + int set_state (const XMLNode&, int version); + +protected: + void control_auto_state_changed (AutoState); + virtual XMLNode& state () const; + + AutoState _auto_state; + uint32_t _responding_to_control_auto_state_change; + +private: + void value_changed (); +}; + +} + +#endif diff --git a/libs/ardour/ardour/surround_return.h b/libs/ardour/ardour/surround_return.h new file mode 100644 index 0000000000..f5535ba29e --- /dev/null +++ b/libs/ardour/ardour/surround_return.h @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2023 Paul Davis + * Copyright (C) 2023 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 __ardour_surround_return_h__ +#define __ardour_surround_return_h__ + +#ifdef HAVE_LV2_1_18_6 +#include +#include +#else +#include +#include +#endif + +#include "ardour/chan_mapping.h" +#include "ardour/lufs_meter.h" +#include "ardour/processor.h" + +namespace ARDOUR +{ +class Session; +class SurroundSend; +class SurroundPannable; +class LV2Plugin; + +class LIBARDOUR_API SurroundReturn : public Processor +{ +public: + SurroundReturn (Session&); + virtual ~SurroundReturn (); + + bool can_support_io_configuration (const ChanCount& in, ChanCount& out); + void run (BufferSet& bufs, samplepos_t start_sample, samplepos_t end_sample, double speed, pframes_t nframes, bool); + int set_block_size (pframes_t); + void flush (); + void set_playback_offset (samplecnt_t cnt); + bool display_to_user () const { return false; } + + void setup_export (std::string const&, samplepos_t, samplepos_t); + void finalize_export (); + + std::shared_ptr surround_processor () const { + return _surround_processor; + } + + /* a value <= -200 indicates that no data is available */ + float integrated_loudness () const; + float max_dbtp () const; + + samplecnt_t signal_latency () const; + +protected: + XMLNode& state () const; + +private: + static const size_t max_object_id = 128; // happens to be the same as a constant in a well known surround system + static const size_t num_pan_parameters = 5; // X, Y, Z, Size, Snap + + void forge_int_msg (uint32_t obj_id, uint32_t key, int val, uint32_t key2 = 0, int val2 = 0); + void maybe_send_metadata (size_t id, pframes_t frame, pan_t const v[num_pan_parameters]); + void evaluate (size_t id, std::shared_ptr const&, timepos_t const& , pframes_t); + + std::shared_ptr _surround_processor; + + LUFSMeter _lufs_meter; + + LV2_Atom_Forge _forge; + uint8_t _atom_buf[8192]; + pan_t _current_value[max_object_id][num_pan_parameters]; + int _current_render_mode[max_object_id]; + size_t _current_n_objects; + BufferSet _surround_bufs; + ChanMapping _in_map; + ChanMapping _out_map; + bool _exporting; + samplepos_t _export_start; + samplepos_t _export_end; + bool _rolling; + std::atomic _flush; +}; + +} // namespace ARDOUR + +#endif /* __ardour_surround_return_h__ */ diff --git a/libs/ardour/ardour/surround_send.h b/libs/ardour/ardour/surround_send.h new file mode 100644 index 0000000000..9539f109ca --- /dev/null +++ b/libs/ardour/ardour/surround_send.h @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2023 Paul Davis + * Copyright (C) 2023 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 __ardour_surround_send_h__ +#define __ardour_surround_send_h__ + +#include "ardour/processor.h" +#include "ardour/send.h" + +namespace ARDOUR +{ +class Amp; +class SurroundPannable; +class MuteMaster; +class GainControl; + +class LIBARDOUR_API SurroundSend : public Processor, public LatentSend +{ +public: + SurroundSend (Session&, std::shared_ptr); + virtual ~SurroundSend (); + + /* methods for the UI to access SurroundSend controls */ + std::shared_ptr gain_control () const { return _gain_control; } + std::shared_ptr pannable (size_t chn = 0) const; + + uint32_t n_pannables () const; + + PBD::Signal0 NPannablesChanged; + PBD::Signal0 PanChanged; + + /* Route/processor interface */ + bool can_support_io_configuration (const ChanCount& in, ChanCount& out) { return in == out; } + bool configure_io (ChanCount in, ChanCount out); + int set_block_size (pframes_t); + void run (BufferSet& bufs, samplepos_t start_sample, samplepos_t end_sample, double speed, pframes_t nframes, bool); + bool display_to_user() const; + bool does_routing() const { return true; } + + std::string describe_parameter(Evoral::Parameter param); + + /* Latent Send */ + void set_delay_in (samplecnt_t); + void set_delay_out (samplecnt_t, size_t bus = 0); + void update_delaylines (bool rt_ok); + samplecnt_t get_delay_in () const { return _delay_in; } + samplecnt_t get_delay_out () const { return _delay_out; } + samplecnt_t signal_latency () const; + + /* These may only be called by a SurroundReturn (to which we are attached) from within its ::run() * method */ + BufferSet const& bufs () const { return _mixbufs; } + + std::shared_ptr const& pan_param (size_t chn, timepos_t& s, timepos_t& e) const; + +protected: + int set_state (const XMLNode&, int version); + XMLNode& state () const; + +private: + void ensure_mixbufs (); + gain_t target_gain () const; + void cycle_start (pframes_t); + void add_pannable (); + + BufferSet _mixbufs; + int32_t _surround_id; + timepos_t _cycle_start; + timepos_t _cycle_end; + gain_t _current_gain; + bool _has_state; + + std::vector> _pannable; + + std::shared_ptr _gain_control; + std::shared_ptr _amp; + std::shared_ptr _mute_master; + std::shared_ptr _send_delay; + std::shared_ptr _thru_delay; + + PBD::ScopedConnectionList _change_connections; +}; + +} // namespace ARDOUR + +#endif /* __ardour_surround_send_h__ */ diff --git a/libs/ardour/automatable.cc b/libs/ardour/automatable.cc index 3d5bab25a7..8529d4e0bb 100644 --- a/libs/ardour/automatable.cc +++ b/libs/ardour/automatable.cc @@ -46,6 +46,7 @@ #include "ardour/plugin_insert.h" #include "ardour/record_enable_control.h" #include "ardour/session.h" +#include "ardour/surround_pannable.h" #include "ardour/uri_map.h" #include "ardour/value_as_string.h" @@ -589,6 +590,9 @@ Automatable::control_factory(const Evoral::Parameter& param) control = new GainControl(_a_session, param); } else if (param.type() == BusSendLevel) { control = new GainControl(_a_session, param); + } else if (param.type() == PanSurroundX || param.type() == PanSurroundY || param.type() == PanSurroundZ || param.type() == PanSurroundSize || param.type() == PanSurroundSnap || param.type() == BinauralRenderMode) { + assert (0); + control = new SurroundControllable (_a_session, param.type(), *this); } else if (param.type() == PanAzimuthAutomation || param.type() == PanWidthAutomation || param.type() == PanElevationAutomation) { Pannable* pannable = dynamic_cast(this); if (pannable) { diff --git a/libs/ardour/export_handler.cc b/libs/ardour/export_handler.cc index 2ccda2aaa8..c07a765fe0 100644 --- a/libs/ardour/export_handler.cc +++ b/libs/ardour/export_handler.cc @@ -40,6 +40,7 @@ #include "ardour/export_format_specification.h" #include "ardour/export_filename.h" #include "ardour/soundcloud_upload.h" +#include "ardour/surround_return.h" #include "ardour/system_exec.h" #include "pbd/openuri.h" #include "pbd/basename.h" @@ -227,6 +228,11 @@ ExportHandler::start_timespan () post_processing = false; session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1)); process_position = current_timespan->get_start(); + + if (!region_export && !current_timespan->vapor ().empty () && session.surround_master ()) { + session.surround_master ()->surround_return ()->setup_export (current_timespan->vapor (), current_timespan->get_start (), current_timespan->get_end ()); + } + // TODO check if it's a RegionExport.. set flag to skip process_without_events() return session.start_audio_export (process_position, realtime, region_export); } @@ -377,6 +383,10 @@ ExportHandler::start_timespan_bg (void* eh) void ExportHandler::finish_timespan () { + if (/*!region_export &&*/ !current_timespan->vapor ().empty () && session.surround_master ()) { + session.surround_master ()->surround_return ()->finalize_export (); + } + graph_builder->get_analysis_results (export_status->result_map); /* work-around: split-channel will produce several files diff --git a/libs/ardour/globals.cc b/libs/ardour/globals.cc index 7b4266d262..632eb00f51 100644 --- a/libs/ardour/globals.cc +++ b/libs/ardour/globals.cc @@ -748,6 +748,7 @@ ARDOUR::init (bool try_optimization, const char* localedir, bool with_gui) reserved_io_names[_("Monitor")] = true; reserved_io_names[_("Master")] = true; + reserved_io_names[_("Surround")] = true; reserved_io_names[X_("auditioner")] = true; // auditioner.cc Track (s, "auditioner",...) reserved_io_names[X_("x-virtual-keyboard")] = false; reserved_io_names[X_("MIDI Tracer 1")] = false; diff --git a/libs/ardour/route.cc b/libs/ardour/route.cc index 3cea82fdff..6b1e9ff630 100644 --- a/libs/ardour/route.cc +++ b/libs/ardour/route.cc @@ -95,6 +95,8 @@ #include "ardour/session.h" #include "ardour/solo_control.h" #include "ardour/solo_isolate_control.h" +#include "ardour/surround_return.h" +#include "ardour/surround_send.h" #include "ardour/triggerbox.h" #include "ardour/types_convert.h" #include "ardour/unknown_processor.h" @@ -143,6 +145,7 @@ Route::Route (Session& sess, string name, PresentationInfo::Flag flag, DataType _pending_process_reorder.store (0); _pending_listen_change.store (0); + _pending_surround_send.store (0); _pending_signals.store (0); } @@ -214,9 +217,12 @@ Route::init () _amp->set_owner (this); _polarity.reset (new PolarityProcessor (_session, _phase_control)); - _polarity->activate(); _polarity->set_owner (this); + if (!is_surround_master ()) { + _polarity->activate(); + } + if (is_monitor ()) { _amp->set_display_name (_("Monitor")); } @@ -230,7 +236,9 @@ Route::init () _trim.reset (new Amp (_session, X_("Trim"), _trim_control, false)); _trim->set_display_to_user (false); - if (dynamic_cast(this)) { + if (is_surround_master ()) { + _trim->deactivate (); + } else if (dynamic_cast(this)) { /* we can't do this in the AudioTrack's constructor * because _trim does not exit then */ @@ -286,6 +294,17 @@ Route::init () _monitor_control.reset (new MonitorProcessor (_session)); _monitor_control->activate (); } + + if (is_surround_master ()) { + _meter_point = _pending_meter_point = MeterPreFader; + _surround_return.reset (new SurroundReturn (_session)); + _surround_return->activate (); + panner_shell()->set_bypassed (true); + + _monitor_control.reset (new MonitorProcessor (_session)); + _monitor_control->activate (); + } + if (_presentation_info.flags() & PresentationInfo::FoldbackBus) { panner_shell()->select_panner_by_uri ("http://ardour.org/plugin/panner_balance"); } @@ -1431,7 +1450,7 @@ Route::clear_processors (Placement p) bool Route::is_internal_processor (std::shared_ptr p) const { - if (p == _amp || p == _meter || p == _main_outs || p == _delayline || p == _trim || p == _polarity || (_volume && p == _volume) || (_triggerbox && p == _triggerbox)) { + if (p == _amp || p == _meter || p == _main_outs || p == _delayline || p == _trim || p == _polarity || (_volume && p == _volume) || (_triggerbox && p == _triggerbox) || (_surround_return && p == _surround_return) || (_surround_send && p == _surround_send)) { return true; } return false; @@ -3174,7 +3193,7 @@ Route::set_processor_state (const XMLNode& node, int version) must_configure = true; } _intreturn->set_state (**niter, version); - } else if (is_monitor() && prop->value() == "monitor") { + } else if ((is_monitor() || is_surround_master ()) && prop->value() == "monitor") { if (!_monitor_control) { _monitor_control.reset (new MonitorProcessor (_session)); must_configure = true; @@ -3310,6 +3329,10 @@ Route::set_processor_state (XMLNode const& node, int version, XMLProperty const* send->output()->changed.connect_same_thread (*send, boost::bind (&Route::output_change_handler, this, _1, _2)); } + } else if (prop->value() == "sursend") { + _surround_send.reset (new SurroundSend (_session, _mute_master)); + _surround_send->set_owner (this); + processor = _surround_send; } else { warning << string_compose(_("unknown Processor type \"%1\"; ignored"), prop->value()) << endmsg; return false; @@ -3437,6 +3460,7 @@ Route::enable_monitor_send () /* master never sends to monitor section via the normal mechanism */ assert (!is_master ()); assert (!is_monitor ()); + assert (!is_surround_master ()); /* make sure we have one */ if (!_monitor_send) { @@ -3617,6 +3641,14 @@ Route::direct_feeds_according_to_reality (std::shared_ptr node, bool* Glib::Threads::RWLock::ReaderLock lm (_processor_lock); + /* our surround send always feeds the surround master */ + if (other->is_surround_master () && _surround_send) { + if (via_send_only) { + *via_send_only = true; + } + return true; + } + for (ProcessorList::iterator r = _processors.begin(); r != _processors.end(); ++r) { std::shared_ptr iop = std::dynamic_pointer_cast(*r); @@ -4141,6 +4173,15 @@ Route::apply_processor_changes_rt () */ update_signal_latency (true); } + + if (_pending_surround_send.load ()) { + Glib::Threads::RWLock::WriterLock pwl (_processor_lock, Glib::Threads::TRY_LOCK); + if (pwl.locked()) { + _pending_surround_send.store (0); + emissions |= EmitSendReturnChange; + } + } + if (emissions != 0) { _pending_signals.store (emissions); return true; @@ -4164,6 +4205,9 @@ Route::emit_pending_signals () if (sig & EmitRtProcessorChange) { processors_changed (RouteProcessorChange (RouteProcessorChange::RealTimeChange)); /* EMIT SIGNAL */ } + if (sig & EmitSendReturnChange) { + processors_changed (RouteProcessorChange (RouteProcessorChange::SendReturnChange, false)); /* EMIT SIGNAL */ + } /* this would be a job for the butler. * Conceptually we should not take processe/processor locks here. @@ -4372,6 +4416,9 @@ Route::update_signal_latency (bool apply_to_delayline, bool* delayline_update_ne if (std::shared_ptr rtn = std::dynamic_pointer_cast (*i)) { rtn->set_playback_offset (0); } + if (std::shared_ptr rtn = std::dynamic_pointer_cast (*i)) { + rtn->set_playback_offset (0); + } // TODO sidechain inputs?! } return 0; @@ -4451,7 +4498,7 @@ Route::update_signal_latency (bool apply_to_delayline, bool* delayline_update_ne *delayline_update_needed = true; } } - } else if (!apply_to_delayline && std::dynamic_pointer_cast (*i)) { + } else if (!apply_to_delayline && (std::dynamic_pointer_cast (*i) || std::dynamic_pointer_cast (*i))) { /* InternalReturn::set_playback_offset() calls set_delay_out(), requires process lock */ const samplecnt_t poff = _signal_latency + _output_latency; if (delayline_update_needed && (*i)->playback_offset () != poff) { @@ -5181,7 +5228,11 @@ Route::setup_invisible_processors () /* find visible processors */ for (ProcessorList::iterator i = _processors.begin(); i != _processors.end(); ++i) { - std::shared_ptr auxsnd = std::dynamic_pointer_cast ((*i)); + std::shared_ptr auxsnd = std::dynamic_pointer_cast (*i); + + if (std::dynamic_pointer_cast (*i)) { + continue; + } #ifdef HAVE_BEATBOX /* XXX temporary hack while we decide on visibility */ @@ -5216,6 +5267,11 @@ Route::setup_invisible_processors () new_processors.insert (amp, _meter); } + /* SURROUND SEND */ + if (_surround_send) { + new_processors.push_back (_surround_send); + } + /* MAIN OUTS */ assert (_main_outs); @@ -5318,6 +5374,15 @@ Route::setup_invisible_processors () new_processors.push_front (_intreturn); } + /* SURROUND RETURN */ + if (_surround_return) { + assert (_surround_return && is_surround_master ()); + new_processors.push_front (_monitor_control); + + assert (!_surround_return->display_to_user ()); + new_processors.push_front (_surround_return); + } + /* DISK READER & WRITER (for Track objects) */ if (_disk_reader || _disk_writer) { @@ -5371,8 +5436,8 @@ Route::setup_invisible_processors () } } - /* Polarity Invert (always present) */ - if (_polarity) { + /* Polarity Invert */ + if (_polarity->active ()) { ProcessorList::iterator reader_pos = find (new_processors.begin(), new_processors.end(), _disk_reader); ProcessorList::iterator polarity_pos; if (reader_pos != new_processors.end()) { @@ -6354,3 +6419,49 @@ Route::tempo_map_changed () _triggerbox->tempo_map_changed (); } } + +void +Route::enable_surround_send () +{ + if (is_main_bus ()) { + /* no surround sends for you */ + return; + } + + /* Caller must hold process lock */ + assert (!AudioEngine::instance()->process_lock().trylock()); + + /* make sure we have one */ + if (!_surround_send) { + _surround_send.reset (new SurroundSend (_session, _mute_master)); + _surround_send->set_owner (this); + _surround_send->activate (); + } + + Glib::Threads::RWLock::WriterLock lm (_processor_lock); + configure_processors_unlocked (0, &lm); + /* We cannot emit `processors_changed` while holing the `process lock` + * This can lead to deadlock in ARDOUR::Session::route_processors_changed + */ + _pending_surround_send.store (1); +} + +void +Route::remove_surround_send () +{ + /* Caller must hold process lock */ + assert (!AudioEngine::instance()->process_lock().trylock()); + + if (!_surround_send) { + return; + } + + _surround_send.reset (); + + Glib::Threads::RWLock::WriterLock lm (_processor_lock); + configure_processors_unlocked (0, &lm); + /* We cannot emit `processors_changed` while holing the `process lock` + * This can lead to deadlock in ARDOUR::Session::route_processors_changed + */ + _pending_surround_send.store (1); +} diff --git a/libs/ardour/session.cc b/libs/ardour/session.cc index fc5a9d1bb9..8c6e9c707f 100644 --- a/libs/ardour/session.cc +++ b/libs/ardour/session.cc @@ -86,6 +86,7 @@ #include "ardour/graph.h" #include "ardour/io_plug.h" #include "ardour/luabindings.h" +#include "ardour/lv2_plugin.h" #include "ardour/midiport_manager.h" #include "ardour/scene_changer.h" #include "ardour/midi_patch_manager.h" @@ -122,6 +123,7 @@ #include "ardour/solo_isolate_control.h" #include "ardour/source_factory.h" #include "ardour/speakers.h" +#include "ardour/surround_return.h" #include "ardour/tempo.h" #include "ardour/ticker.h" #include "ardour/transport_fsm.h" @@ -785,6 +787,7 @@ Session::destroy () _master_out.reset (); _monitor_out.reset (); + _surround_master.reset (); { RCUWriter writer (routes); @@ -1375,6 +1378,199 @@ Session::reset_monitor_section () setup_route_monitor_sends (true, false); } +void +Session::remove_surround_master () +{ + if (!_surround_master) { + return; + } + + /* allow deletion when session is unloaded */ + if (!_engine.running() && !deletion_in_progress ()) { + error << _("Cannot remove monitor section while the engine is offline.") << endmsg; + return; + } + + /* if we are auditioning, cancel it ... this is a workaround + to a problem (auditioning does not execute the process graph, + which is needed to remove routes when using >1 core for processing) + */ + cancel_audition (); + + if (!deletion_in_progress ()) { + setup_route_surround_sends (false, true); + _engine.monitor_port().clear_ports (true); + } + + remove_route (_surround_master); + if (deletion_in_progress ()) { + return; + } + + SurroundMasterAddedOrRemoved (); /* EMIT SIGNAL */ +} + +bool +Session::vapor_barrier () +{ +#if !(defined (LV2_EXTENDED) && defined (HAVE_LV2_1_10_0)) + return false; +#endif + if (_vapor_available.has_value ()) { + return _vapor_available.value (); + } + + bool ok = false; + bool ex = false; + + if (nominal_sample_rate () == 48000 || nominal_sample_rate () == 96000) { + std::shared_ptr p; + + if (_surround_master) { + p = _surround_master->surround_return ()->surround_processor (); + } else { + PluginManager& mgr (PluginManager::instance ()); + for (auto const& i : mgr.lv2_plugin_info ()) { + if ("urn:ardour:a-vapor" != i->unique_id) { + continue; + } + p = std::dynamic_pointer_cast (i->load (*this)); + break; + } + } + if (p) { + ok = true; + ex = p->can_export (); + } + } + + _vapor_exportable = ex; + _vapor_available = ok; + + return ok; +} + +bool +Session::vapor_export_barrier () +{ +#if !(defined (LV2_EXTENDED) && defined (HAVE_LV2_1_10_0)) + return false; +#endif + if (!_vapor_exportable.has_value ()) { + vapor_barrier (); + } + assert (_vapor_exportable.has_value ()); + return _vapor_exportable.value (); +} + +void +Session::add_surround_master () +{ + RouteList rl; + + if (_surround_master) { + return; + } + + if (!_engine.running()) { + error << _("Cannot create surround master while the engine is offline.") << endmsg; + return; + } + + if (!vapor_barrier()) { + error << _("Some surround sound systems require a sample-rate of 48kHz or 96kHz.") << endmsg; + return; + } + + std::shared_ptr r (new Route (*this, _("Surround"), PresentationInfo::SurroundMaster, DataType::AUDIO)); + + if (r->init ()) { + return; + } + + BOOST_MARK_ROUTE(r); + + try { + Glib::Threads::Mutex::Lock lm (AudioEngine::instance()->process_lock ()); + r->input()->ensure_io (ChanCount (), false, this); + r->output()->ensure_io (ChanCount (DataType::AUDIO, 16), false, this); + } catch (...) { + error << _("Cannot create surround master. 'Surround' Port name is not unique.") << endmsg; + return; + } + + rl.push_back (r); + add_routes (rl, false, false, 0); + + assert (_surround_master); + + auto_connect_surround_master (); + + /* Hold process lock while doing this so that we don't hear bits and + * pieces of audio as we work on each route. + */ + + setup_route_surround_sends (true, true); + + SurroundMasterAddedOrRemoved (); /* EMIT SIGNAL */ +} + +void +Session::auto_connect_surround_master () +{ + /* compare to auto_connect_io */ + vector outputs; + _engine.get_physical_outputs (DataType::AUDIO, outputs); + + std::shared_ptr io = _surround_master->output (); + uint32_t limit = io->n_ports ().n_audio (); + + Glib::Threads::Mutex::Lock lm (AudioEngine::instance()->process_lock ()); + /* connect binaural outputs, port 12, 13 */ + for (uint32_t n = 12, p = 0; n < limit && outputs.size () > p; ++n, ++p) { + std::shared_ptr ap = io->audio (n); + + if (io->connect (ap, outputs[p], this)) { + error << string_compose (_("cannot connect %1 output %2 to %3"), io->name(), n, outputs[p]) << endmsg; + break; + } + } + lm.release (); + + /* Mute non-surround path */ + if (_monitor_out) { + _monitor_out->monitor_control ()->set_mono (true); + } else if (_master_out) { + _master_out->mute_control ()->set_value (true, PBD::Controllable::NoGroup); + } + +} + +void +Session::setup_route_surround_sends (bool enable, bool need_process_lock) +{ + Glib::Threads::Mutex::Lock lx (AudioEngine::instance()->process_lock (), Glib::Threads::NOT_LOCK); + if (need_process_lock) { + /* Hold process lock while doing this so that we don't hear bits and + * pieces of audio as we work on each route. + */ + lx.acquire(); + } + + std::shared_ptr rl = routes.reader (); + ProcessorChangeBlocker pcb (this, false /* XXX */); + + for (auto const& x : *rl) { + if (x->can_monitor ()) { + if (enable) { + x->enable_surround_send (); + } else { + x->remove_surround_send (); + } + } + } +} + int Session::add_master_bus (ChanCount const& count) { @@ -3222,6 +3418,11 @@ Session::new_route_from_template (uint32_t how_many, PresentationInfo::order_t i (*x)->remove_monitor_send (); } } + if (_surround_master) { + (*x)->enable_surround_send(); + } else { + (*x)->remove_surround_send(); + } /* reconnect ports using information from state */ for (auto const& wio : (*x)->all_inputs ()) { std::shared_ptr io = wio.lock(); @@ -3339,6 +3540,10 @@ Session::add_routes_inner (RouteList& new_routes, bool input_auto_connect, bool _monitor_out = r; } + if (r->is_surround_master()) { + _surround_master = r; + } + std::shared_ptr tr = std::dynamic_pointer_cast (r); if (tr) { tr->PlaylistChanged.connect_same_thread (*this, boost::bind (&Session::track_playlist_changed, this, std::weak_ptr (tr))); @@ -3406,6 +3611,13 @@ Session::add_routes_inner (RouteList& new_routes, bool input_auto_connect, bool } } + if (_surround_master && !loading()) { + Glib::Threads::Mutex::Lock lm (_engine.process_lock()); + for (auto & r : new_routes) { + r->enable_surround_send (); + } + } + reassign_track_numbers (); } @@ -3579,6 +3791,10 @@ Session::remove_routes (std::shared_ptr routes_to_remove) _monitor_out.reset (); } + if (*iter == _surround_master) { + _surround_master.reset (); + } + // We need to disconnect the route's inputs and outputs (*iter)->input()->disconnect (0); @@ -5677,6 +5893,26 @@ Session::next_send_id () } } +uint32_t +Session::next_surround_send_id () +{ + /* this doesn't really loop forever. just think about it */ + + while (true) { + for (boost::dynamic_bitset::size_type n = 1; n < surround_send_bitset.size(); ++n) { + if (!surround_send_bitset[n]) { + surround_send_bitset[n] = true; + return n; + + } + } + + /* none available, so resize and try again */ + + surround_send_bitset.resize (surround_send_bitset.size() + 16, false); + } +} + uint32_t Session::next_aux_send_id () { @@ -5741,6 +5977,18 @@ Session::mark_aux_send_id (uint32_t id) aux_send_bitset[id] = true; } +void +Session::mark_surround_send_id (uint32_t id) +{ + if (id >= surround_send_bitset.size()) { + surround_send_bitset.resize (id+16, false); + } + if (surround_send_bitset[id]) { + warning << string_compose (_("surround send ID %1 appears to be in use already"), id) << endmsg; + } + surround_send_bitset[id] = true; +} + void Session::mark_return_id (uint32_t id) { @@ -5787,6 +6035,17 @@ Session::unmark_aux_send_id (uint32_t id) } } +void +Session::unmark_surround_send_id (uint32_t id) +{ + if (deletion_in_progress ()) { + return; + } + if (id < surround_send_bitset.size()) { + surround_send_bitset[id] = false; + } +} + void Session::unmark_return_id (uint32_t id) { diff --git a/libs/ardour/session_state.cc b/libs/ardour/session_state.cc index 10ca7e1fa1..4e1de120f6 100644 --- a/libs/ardour/session_state.cc +++ b/libs/ardour/session_state.cc @@ -657,8 +657,9 @@ Session::create (const string& session_template, BusProfile const * bus_profile, return rv; } - if (Config->get_use_monitor_bus()) + if (Config->get_use_monitor_bus()) { add_monitor_section (); + } } } @@ -4582,6 +4583,35 @@ Session::config_changed (std::string p, bool ours) remove_monitor_section (); } } + } else if (p == "use-surround-master") { + /* NB. This is always called when constructing a session, + * after restoring session state (if any), + * via post_engine_init() -> Config->map_parameters() + */ + bool want_sm = config.get_use_surround_master(); + bool have_sm = _surround_master ? true : false; + if (loading ()) { + /* When loading an existing session, the config "use-surround-master" + * is ignored. Instead the sesion-state (xml) will have added the + * "surround-master" and restored its state (and connections) + * if the session has a surround master.. + * Update the config to reflect this. + */ + if (want_sm != have_sm) { + config.set_use_surround_master (have_sm); + } + SurroundMasterAddedOrRemoved (); /* EMIT SIGNAL */ + } else { + /* Otherwise, Config::set_use_surround_master() does + * control the the presence of the monitor-section + * (new sessions, user initiated change) + */ + if (want_sm && !have_sm) { + add_surround_master (); + } else if (!want_sm && have_sm) { + remove_surround_master (); + } + } } else if (p == "loop-fade-choice") { last_loopend = 0; /* force locate to refill buffers with new loop boundary data */ auto_loop_changed (_locations->auto_loop_location()); diff --git a/libs/ardour/surround_pannable.cc b/libs/ardour/surround_pannable.cc new file mode 100644 index 0000000000..fe2545f007 --- /dev/null +++ b/libs/ardour/surround_pannable.cc @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2023 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 "pbd/error.h" + +#include "ardour/automation_list.h" +#include "ardour/surround_pannable.h" +#include "ardour/session.h" +#include "ardour/value_as_string.h" + +#include "pbd/i18n.h" + +using namespace std; +using namespace PBD; +using namespace ARDOUR; + +SurroundControllable::SurroundControllable (Session& s, Evoral::Parameter param, Temporal::TimeDomainProvider const& tdp) + : AutomationControl (s, + param, + ParameterDescriptor(param), + std::shared_ptr(new AutomationList(param, tdp))) +{ +} + +std::string +SurroundControllable::get_user_string () const +{ + float v = get_value (); + char buf[32]; + switch (parameter ().type ()) { + case PanSurroundX: + if (v == 0.5) { + return _("Center"); + } + snprintf(buf, sizeof(buf), "L%3d R%3d", (int)rint (100.0 * (1.0 - v)), (int)rint (100.0 * v)); + break; + case PanSurroundY: + snprintf(buf, sizeof(buf), "F%3d B%3d", (int)rint (100.0 * (1.0 - v)), (int)rint (100.0 * v)); + break; + case PanSurroundSize: + snprintf(buf, sizeof(buf), "%.0f%%", 100.f * v); + break; + default: + return value_as_string (desc(), v); + } + return buf; +} + +SurroundPannable::SurroundPannable (Session& s, uint32_t chn, Temporal::TimeDomainProvider const & tdp) + : Automatable (s, tdp) + , SessionHandleRef (s) + , pan_pos_x (new SurroundControllable (s, Evoral::Parameter (PanSurroundX, 0, chn), tdp)) + , pan_pos_y (new SurroundControllable (s, Evoral::Parameter (PanSurroundY, 0, chn), tdp)) + , pan_pos_z (new SurroundControllable (s, Evoral::Parameter (PanSurroundZ, 0, chn), tdp)) + , pan_size (new SurroundControllable (s, Evoral::Parameter (PanSurroundSize, 0, chn), tdp)) + , pan_snap (new SurroundControllable (s, Evoral::Parameter (PanSurroundSnap, 0, chn), tdp)) + , binaural_render_mode (new SurroundControllable (s, Evoral::Parameter (BinauralRenderMode, 0, chn), tdp)) + , _auto_state (Off) + , _responding_to_control_auto_state_change (0) +{ + binaural_render_mode->set_flag (Controllable::NotAutomatable); + + add_control (pan_pos_x); + add_control (pan_pos_y); + add_control (pan_pos_z); + add_control (pan_size); + add_control (pan_snap); + add_control (binaural_render_mode); // not automatable + + /* all controls change state together */ + pan_pos_x->alist()->automation_state_changed.connect_same_thread (*this, boost::bind (&SurroundPannable::control_auto_state_changed, this, _1)); + pan_pos_y->alist()->automation_state_changed.connect_same_thread (*this, boost::bind (&SurroundPannable::control_auto_state_changed, this, _1)); + pan_pos_z->alist()->automation_state_changed.connect_same_thread (*this, boost::bind (&SurroundPannable::control_auto_state_changed, this, _1)); + pan_size->alist()->automation_state_changed.connect_same_thread (*this, boost::bind (&SurroundPannable::control_auto_state_changed, this, _1)); + pan_snap->alist()->automation_state_changed.connect_same_thread (*this, boost::bind (&SurroundPannable::control_auto_state_changed, this, _1)); + + pan_pos_x->Changed.connect_same_thread (*this, boost::bind (&SurroundPannable::value_changed, this)); + pan_pos_y->Changed.connect_same_thread (*this, boost::bind (&SurroundPannable::value_changed, this)); + pan_pos_z->Changed.connect_same_thread (*this, boost::bind (&SurroundPannable::value_changed, this)); + pan_size->Changed.connect_same_thread (*this, boost::bind (&SurroundPannable::value_changed, this)); + pan_snap->Changed.connect_same_thread (*this, boost::bind (&SurroundPannable::value_changed, this)); +} + +SurroundPannable::~SurroundPannable () +{ +} + +void +SurroundPannable::control_auto_state_changed (AutoState new_state) +{ + if (_responding_to_control_auto_state_change) { + return; + } + + _responding_to_control_auto_state_change++; + + pan_pos_x->set_automation_state (new_state); + pan_pos_y->set_automation_state (new_state); + pan_pos_z->set_automation_state (new_state); + pan_size->set_automation_state (new_state); + pan_snap->set_automation_state (new_state); + + _responding_to_control_auto_state_change--; + + _auto_state = new_state; + automation_state_changed (new_state); /* EMIT SIGNAL */ +} + +void +SurroundPannable::value_changed () +{ + _session.set_dirty (); +} + +void +SurroundPannable::set_automation_state (AutoState state) +{ + if (state == _auto_state) { + return; + } + _auto_state = state; + + const Controls& c (controls()); + + for (Controls::const_iterator ci = c.begin(); ci != c.end(); ++ci) { + std::shared_ptr ac = std::dynamic_pointer_cast(ci->second); + if (ac) { + ac->alist()->set_automation_state (state); + } + } + + _session.set_dirty (); + automation_state_changed (_auto_state); /* EMIT SIGNAL */ +} + +bool +SurroundPannable::touching () const +{ + const Controls& c (controls()); + + for (auto const& i : c) { + std::shared_ptr ac = std::dynamic_pointer_cast(i.second); + if (ac && ac->touching ()) { + return true; + } + } + return false; +} + +XMLNode& +SurroundPannable::get_state () const +{ + return state (); +} + +XMLNode& +SurroundPannable::state () const +{ + XMLNode* node = new XMLNode (X_("SurroundPannable")); + node->set_property ("channel", pan_pos_x->parameter ().id ()); + + node->add_child_nocopy (pan_pos_x->get_state()); + node->add_child_nocopy (pan_pos_y->get_state()); + node->add_child_nocopy (pan_pos_z->get_state()); + node->add_child_nocopy (pan_size->get_state()); + node->add_child_nocopy (pan_snap->get_state()); + node->add_child_nocopy (binaural_render_mode->get_state()); + + return *node; +} + +int +SurroundPannable::set_state (const XMLNode& root, int version) +{ + if (root.name() != X_("SurroundPannable")) { + return -1; + } + + const XMLNodeList& nlist (root.children()); + XMLNodeConstIterator niter; + + for (niter = nlist.begin(); niter != nlist.end(); ++niter) { + if ((*niter)->name() != Controllable::xml_node_name) { + continue; + } + std::string control_name; + + if (!(*niter)->get_property (X_("name"), control_name)) { + continue; + } + + if (control_name == pan_pos_x->name()) { + pan_pos_x->set_state (**niter, version); + } else if (control_name == pan_pos_y->name()) { + pan_pos_y->set_state (**niter, version); + } else if (control_name == pan_pos_z->name()) { + pan_pos_z->set_state (**niter, version); + } else if (control_name == pan_size->name()) { + pan_size->set_state (**niter, version); + } else if (control_name == pan_snap->name()) { + pan_snap->set_state (**niter, version); + } else if (control_name == binaural_render_mode->name()) { + binaural_render_mode->set_state (**niter, version); + } + } + + return 0; +} diff --git a/libs/ardour/surround_return.cc b/libs/ardour/surround_return.cc new file mode 100644 index 0000000000..a5236de18f --- /dev/null +++ b/libs/ardour/surround_return.cc @@ -0,0 +1,380 @@ +/* + * Copyright (C) 2023 Robin Gareus + * Copyright (C) 2023 Paul Davis + * + * 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 "ardour/surround_return.h" +#include "ardour/audio_buffer.h" +#include "ardour/lv2_plugin.h" +#include "ardour/route.h" +#include "ardour/session.h" +#include "ardour/surround_pannable.h" +#include "ardour/surround_send.h" +#include "ardour/uri_map.h" +#include "pbd/i18n.h" + +using namespace ARDOUR; + +SurroundReturn::SurroundReturn (Session& s) + : Processor (s, _("SurrReturn"), Temporal::TimeDomainProvider (Temporal::AudioTime)) + , _lufs_meter (s.nominal_sample_rate (), 5) + , _current_n_objects (max_object_id) + , _in_map (ChanCount (DataType::AUDIO, 128)) + , _out_map (ChanCount (DataType::AUDIO, 14 + 6 /* Loudness Meter */)) + , _exporting (false) + , _export_start (0) + , _export_end (0) +{ +#if !(defined(LV2_EXTENDED) && defined(HAVE_LV2_1_10_0)) + throw failed_constructor (); +#endif + + _surround_processor = std::dynamic_pointer_cast (find_plugin (_session, "urn:ardour:a-vapor", ARDOUR::LV2)); + + if (!_surround_processor) { + throw ProcessorException (_("Required Atmos/Vapor Processor not found.")); + } + + _flush.store (0); + _surround_processor->activate (); + _surround_bufs.ensure_buffers (DataType::AUDIO, 128, s.get_block_size ()); + _surround_bufs.set_count (ChanCount (DataType::AUDIO, 128)); + + lv2_atom_forge_init (&_forge, URIMap::instance ().urid_map ()); + + for (size_t i = 0; i < max_object_id; ++i) { + _current_render_mode[i] = -1; + for (size_t p = 0; p < num_pan_parameters; ++p) { + _current_value[i][p] = -1111; /* some invalid data that forces an update */ + } + } +} + +SurroundReturn::~SurroundReturn () +{ +} + +int +SurroundReturn::set_block_size (pframes_t nframes) +{ + _surround_bufs.ensure_buffers (DataType::AUDIO, 128, nframes); + _surround_processor->set_block_size (nframes); + return 0; +} + +samplecnt_t +SurroundReturn::signal_latency () const +{ + return _surround_processor->signal_latency (); +} + +void +SurroundReturn::flush () +{ + _flush.store (1); +} + +void +SurroundReturn::run (BufferSet& bufs, samplepos_t start_sample, samplepos_t end_sample, double speed, pframes_t nframes, bool) +{ + if (!check_active ()) { + return; + } + + int canderef (1); + if (_flush.compare_exchange_strong (canderef, 0)) { + _surround_processor->flush (); + } + + bufs.set_count (_configured_output); + _surround_bufs.silence (nframes, 0); + + RouteList rl = *_session.get_routes (); // XXX this allocates memory + rl.sort (Stripable::Sorter (true)); + + size_t id = 10; // First 10 IDs are reseved for bed mixes + + for (auto const& r : rl) { + std::shared_ptr ss; + if (!r->active ()) { + continue; + } + if (!(ss = r->surround_send ()) || !ss->active ()) { + continue; + } + + timepos_t start, end; + + for (uint32_t s = 0; s < ss->bufs ().count ().n_audio () && id < max_object_id; ++s, ++id) { + + std::shared_ptr const& p (ss->pan_param (s, start, end)); + AutoState const as = p->automation_state (); + bool const automated = (as & Play) || ((as & (Touch | Latch)) && !p->touching ()); + + AudioBuffer& dst_ab (_surround_bufs.get_audio (id)); + AudioBuffer const& src_ab (ss->bufs ().get_audio (s)); + if (id > 9) { + /* object */ + dst_ab.read_from (src_ab, nframes); + if (!automated || start_sample >= end_sample) { + pan_t const v[num_pan_parameters] = + { + (pan_t)p->pan_pos_x->get_value (), + (pan_t)p->pan_pos_y->get_value (), + (pan_t)p->pan_pos_z->get_value (), + (pan_t)p->pan_size->get_value (), + (pan_t)p->pan_snap->get_value () + }; + maybe_send_metadata (id, 0, v); + } else { + /* Evaluate Automation + * + * Note, exclusive end: range = [start_sample, end_sample[ + * nframes == end_sample - start_sample + * IOW: end_sample == next cycle's start_sample; + */ + if (nframes < 2) { + evaluate (id, p, timepos_t (start_sample), 0); + } else { + timepos_t start (start_sample); + timepos_t end (end_sample - 1); + while (true) { + Evoral::ControlEvent next_event (timepos_t (Temporal::AudioTime), 0.0f); + if (!p->find_next_event (start, end, next_event)) { + break; + } + samplecnt_t pos = std::min (timepos_t (start_sample).distance (next_event.when).samples(), (samplecnt_t) nframes - 1); + evaluate (id, p, next_event.when, pos); + start = next_event.when; + } + /* end */ + evaluate (id, p, end, nframes - 1); + } + } + /* configure near/mid/far - not sample-accurate */ + int const brm = p->binaural_render_mode->get_value (); + if (brm!= _current_render_mode[id]) { + _current_render_mode[id] = brm; +#if defined(LV2_EXTENDED) && defined(HAVE_LV2_1_10_0) + URIMap::URIDs const& urids = URIMap::instance ().urids; + forge_int_msg (urids.surr_Settings, urids.surr_Channel, id, urids.surr_BinauralRenderMode, brm); +#endif + } + + } else { + /* bed mix */ + dst_ab.merge_from (src_ab, nframes); + } + + } + + if (id >= max_object_id) { + break; + } + } + + if (_current_n_objects != id) { + _current_n_objects = id; +#if defined(LV2_EXTENDED) && defined(HAVE_LV2_1_10_0) + URIMap::URIDs const& urids = URIMap::instance ().urids; + forge_int_msg (urids.surr_Settings, urids.surr_ChannelCount, _current_n_objects); +#endif + } + + uint32_t meter_nframes = nframes; + uint32_t meter_offset = 0; + + if (_exporting && _export_start >= start_sample && _export_start < end_sample && start_sample != end_sample) { + _lufs_meter.reset (); + meter_offset = _export_start - start_sample; + meter_nframes -= meter_offset; +#if defined(LV2_EXTENDED) && defined(HAVE_LV2_1_10_0) + //std::cout << "SURR START EXPORT " << start_sample << " <= " << _export_start << " < " << end_sample << "\n"; + URIMap::URIDs const& urids = URIMap::instance ().urids; + forge_int_msg (urids.surr_ExportStart, urids.time_frame, _export_start - start_sample); +#endif + } + + if (_exporting && _export_end >= start_sample && _export_end < end_sample) { + meter_nframes = _export_end - start_sample; +#if defined(LV2_EXTENDED) && defined(HAVE_LV2_1_10_0) + //std::cout << "SURR START EXPORT " << start_sample << " <= " << _export_end << " < " << end_sample << "\n"; + URIMap::URIDs const& urids = URIMap::instance ().urids; + forge_int_msg (urids.surr_ExportStop, urids.time_frame, _export_end - start_sample); +#endif + } + + _surround_processor->connect_and_run (_surround_bufs, start_sample, end_sample, speed, _in_map, _out_map, nframes, 0); + + BufferSet::iterator i = _surround_bufs.begin (DataType::AUDIO); + for (BufferSet::iterator o = bufs.begin (DataType::AUDIO); o != bufs.end (DataType::AUDIO); ++i, ++o) { + o->read_from (*i, nframes); + } + + if (_exporting) { + _rolling = true; + } else if (_rolling && start_sample == end_sample) { + _rolling = false; + } else if (!_rolling && start_sample != end_sample) { + _rolling = true; + _lufs_meter.reset (); + } + + float const* data[5] = { + _surround_bufs.get_audio (14).data (meter_offset), + _surround_bufs.get_audio (15).data (meter_offset), + _surround_bufs.get_audio (16).data (meter_offset), + _surround_bufs.get_audio (18).data (meter_offset), + _surround_bufs.get_audio (19).data (meter_offset) + }; + + if (_rolling && (!_exporting || _export_end >= end_sample)) { + _lufs_meter.run (data, meter_nframes); + } +} + +void +SurroundReturn::forge_int_msg (uint32_t obj_id, uint32_t key, int val, uint32_t key2, int val2) +{ + URIMap::URIDs const& urids = URIMap::instance ().urids; + LV2_Atom_Forge_Frame frame; + lv2_atom_forge_set_buffer (&_forge, _atom_buf, sizeof(_atom_buf)); + lv2_atom_forge_frame_time (&_forge, 0); + LV2_Atom* msg = (LV2_Atom*)lv2_atom_forge_object (&_forge, &frame, 1, obj_id); + lv2_atom_forge_key (&_forge, key); + lv2_atom_forge_int (&_forge, val); + if (key2 > 0) { + lv2_atom_forge_key (&_forge, key2); + lv2_atom_forge_int (&_forge, val2); + } + lv2_atom_forge_pop (&_forge, &frame); + _surround_processor->write_from_ui (0, urids.atom_eventTransfer, lv2_atom_total_size (msg), (const uint8_t*)msg); +} + +void +SurroundReturn::maybe_send_metadata (size_t id, pframes_t sample, pan_t const v[num_pan_parameters]) +{ + bool changed = false; + for (size_t i = 0; i < num_pan_parameters; ++i) { + if (_current_value[id][i] != v[i]) { + changed = true; + } + _current_value[id][i] = v[i]; + } + if (!changed) { + return; + } + URIMap::URIDs const& urids = URIMap::instance ().urids; + +#if defined(LV2_EXTENDED) && defined(HAVE_LV2_1_10_0) + LV2_Atom_Forge_Frame frame; + lv2_atom_forge_set_buffer (&_forge, _atom_buf, sizeof(_atom_buf)); + lv2_atom_forge_frame_time (&_forge, 0); + LV2_Atom* msg = (LV2_Atom*)lv2_atom_forge_object (&_forge, &frame, 1, urids.surr_MetaData); + lv2_atom_forge_key (&_forge, urids.time_frame); + lv2_atom_forge_int (&_forge, sample); + lv2_atom_forge_key (&_forge, urids.surr_Channel); + lv2_atom_forge_int (&_forge, id); + lv2_atom_forge_key (&_forge, urids.surr_PosX); + lv2_atom_forge_float (&_forge, v[0]); + lv2_atom_forge_key (&_forge, urids.surr_PosY); + lv2_atom_forge_float (&_forge, v[1]); + lv2_atom_forge_key (&_forge, urids.surr_PosZ); + lv2_atom_forge_float (&_forge, v[2]); + lv2_atom_forge_key (&_forge, urids.surr_Size); + lv2_atom_forge_float (&_forge, v[3]); + lv2_atom_forge_key (&_forge, urids.surr_Snap); + lv2_atom_forge_bool (&_forge, v[4]> 0 ? true : false); + lv2_atom_forge_pop (&_forge, &frame); + + _surround_processor->write_from_ui (0, urids.atom_eventTransfer, lv2_atom_total_size (msg), (const uint8_t*)msg); +#endif +} + +void +SurroundReturn::evaluate (size_t id, std::shared_ptr const& p, timepos_t const& when, pframes_t sample) +{ + bool ok[num_pan_parameters]; + pan_t const v[num_pan_parameters] = + { + (pan_t)p->pan_pos_x->list()->rt_safe_eval (when, ok[0]), + (pan_t)p->pan_pos_y->list()->rt_safe_eval (when, ok[1]), + (pan_t)p->pan_pos_z->list()->rt_safe_eval (when, ok[2]), + (pan_t)p->pan_size->list()->rt_safe_eval (when, ok[3]), + (pan_t)p->pan_snap->list()->rt_safe_eval (when, ok[4]) + }; + if (ok[0] && ok[1] && ok[2] && ok[3] && ok[4]) { + maybe_send_metadata (id, sample, v); + } +} + +bool +SurroundReturn::can_support_io_configuration (const ChanCount& in, ChanCount& out) +{ + out = ChanCount (DataType::AUDIO, 14); // 7.1.4 + binaural + return in.n_total () == 0; +} + +void +SurroundReturn::set_playback_offset (samplecnt_t cnt) +{ + Processor::set_playback_offset (cnt); + std::shared_ptr rl (_session.get_routes ()); + for (auto const& r : *rl) { + std::shared_ptr ss = r->surround_send (); + if (ss) { + ss->set_delay_out (cnt); + } + } +} + +void +SurroundReturn::setup_export (std::string const& fn, samplepos_t ss, samplepos_t es) +{ + if (0 == _surround_processor->setup_export (fn.c_str())) { + _exporting = true; + _export_start = ss - effective_latency (); + _export_end = es - effective_latency (); + } +} + +void +SurroundReturn::finalize_export () +{ + _surround_processor->finalize_export (); + _exporting = false; + _export_start = _export_end = 0; +} + +float +SurroundReturn::integrated_loudness () const +{ + return _lufs_meter.integrated_loudness (); +} + +float +SurroundReturn::max_dbtp () const +{ + return _lufs_meter.dbtp (); +} + +XMLNode& +SurroundReturn::state () const +{ + XMLNode* node = new XMLNode (X_("SurroundReturn")); + return *node; +} diff --git a/libs/ardour/surround_send.cc b/libs/ardour/surround_send.cc new file mode 100644 index 0000000000..7b3d6f57c3 --- /dev/null +++ b/libs/ardour/surround_send.cc @@ -0,0 +1,402 @@ +/* + * Copyright (C) 2023 Paul Davis + * Copyright (C) 2023 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 "ardour/surround_send.h" +#include "ardour/amp.h" +#include "ardour/audioengine.h" +#include "ardour/buffer.h" +#include "ardour/delayline.h" +#include "ardour/gain_control.h" +#include "ardour/internal_send.h" +#include "ardour/surround_pannable.h" +#include "ardour/route.h" +#include "ardour/session.h" + +#include "pbd/i18n.h" + +using namespace ARDOUR; + +SurroundSend::SurroundSend (Session& s, std::shared_ptr mm) + : Processor (s, _("Surround"), Temporal::TimeDomainProvider (Temporal::AudioTime)) + , _surround_id (s.next_surround_send_id ()) + , _current_gain (GAIN_COEFF_ZERO) + , _has_state (false) + , _mute_master (mm) + +{ + _send_delay.reset (new DelayLine (_session, "Send-" + name ())); + _thru_delay.reset (new DelayLine (_session, "Thru-" + name ())); + + std::shared_ptr gl (new AutomationList (Evoral::Parameter (BusSendLevel), *this)); + _gain_control = std::shared_ptr (new GainControl (_session, Evoral::Parameter (BusSendLevel), gl)); + _amp.reset (new Amp (_session, _("Surround"), _gain_control, false)); + _amp->activate (); + + _gain_control->set_flag (PBD::Controllable::InlineControl); + //_gain_control->set_value (GAIN_COEFF_ZERO, PBD::Controllable::NoGroup); + + add_control (_gain_control); + + InternalSend::CycleStart.connect_same_thread (*this, boost::bind (&SurroundSend::cycle_start, this, _1)); +} + +SurroundSend::~SurroundSend () +{ +} + +std::shared_ptr +SurroundSend::pannable (size_t chn) const +{ + return _pannable[chn]; +} + +std::shared_ptr const& +SurroundSend::pan_param (size_t chn, timepos_t& s, timepos_t& e) const +{ + s = _cycle_start; + e = _cycle_end; + return _pannable[chn]; +} + + +gain_t +SurroundSend::target_gain () const +{ + return _mute_master->mute_gain_at (MuteMaster::SurroundSend); +} + +void +SurroundSend::run (BufferSet& bufs, samplepos_t start_sample, samplepos_t end_sample, double speed, pframes_t nframes, bool) +{ + automation_run (start_sample, nframes); + + if (!check_active ()) { + _mixbufs.silence (nframes, 0); + return; + } + + /* Copy inputs to mixbufs, since (a) we may need to adjust gain (b) the + * contents need to be available for the Surround return (later) + */ + + BufferSet::iterator o = _mixbufs.begin (DataType::AUDIO); + BufferSet::iterator i = bufs.begin (DataType::AUDIO); + + for (; i != bufs.end (DataType::AUDIO) && o != _mixbufs.end (DataType::AUDIO); ++i, ++o) { + o->read_from (*i, nframes); + } + + /* main gain control: * mute & bypass/enable */ + gain_t tgain = target_gain (); + + if (tgain != _current_gain) { + /* target gain has changed, fade in/out */ + _current_gain = Amp::apply_gain (_mixbufs, _session.nominal_sample_rate (), nframes, _current_gain, tgain); + } else if (tgain == GAIN_COEFF_ZERO) { + /* we were quiet last time, and we're still supposed to be quiet. */ + Amp::apply_simple_gain (_mixbufs, nframes, GAIN_COEFF_ZERO); + return; + } else if (tgain != GAIN_COEFF_UNITY) { + /* target gain has not changed, but is not zero or unity */ + Amp::apply_simple_gain (_mixbufs, nframes, tgain); + } + + /* apply fader gain automation */ + _amp->set_gain_automation_buffer (_session.send_gain_automation_buffer ()); + _amp->setup_gain_automation (start_sample, end_sample, nframes); + _amp->run (_mixbufs, start_sample, end_sample, speed, nframes, true); + + _send_delay->run (_mixbufs, start_sample, end_sample, speed, nframes, true); + + for (uint32_t chn = 0; chn < n_pannables (); ++ chn) { + _pannable[chn]->automation_run (start_sample, nframes); + } + + _cycle_start = timepos_t (start_sample); + _cycle_end = timepos_t (end_sample); + + _thru_delay->run (bufs, start_sample, end_sample, speed, nframes, true); +} + +void +SurroundSend::set_delay_in (samplecnt_t delay) +{ + if (_delay_in == delay) { + return; + } + _delay_in = delay; + update_delaylines (false); +} + +void +SurroundSend::set_delay_out (samplecnt_t delay, size_t /*bus*/) +{ + if (_delay_out == delay) { + return; + } + _delay_out = delay; + update_delaylines (true); +} + +void +SurroundSend::update_delaylines (bool rt_ok) +{ + if (!rt_ok && AudioEngine::instance ()->running () && AudioEngine::instance ()->in_process_thread ()) { + if (_delay_out > _delay_in) { + if (_send_delay->delay () != 0 || _thru_delay->delay () != _delay_out - _delay_in) { + QueueUpdate (); /* EMIT SIGNAL */ + } + } else { + if (_thru_delay->delay () != 0 || _send_delay->delay () != _delay_in - _delay_out) { + QueueUpdate (); /* EMIT SIGNAL */ + } + } + return; + } + + bool changed; + if (_delay_out > _delay_in) { + changed = _thru_delay->set_delay (_delay_out - _delay_in); + _send_delay->set_delay (0); + } else { + changed = _thru_delay->set_delay (0); + _send_delay->set_delay (_delay_in - _delay_out); + } + + if (changed && !AudioEngine::instance ()->in_process_thread ()) { + ChangedLatency (); /* EMIT SIGNAL */ + } +} + +samplecnt_t +SurroundSend::signal_latency () const +{ + if (!_pending_active) { + return 0; + } + if (_delay_out > _delay_in) { + return _delay_out - _delay_in; + } + return 0; +} + +bool +SurroundSend::display_to_user() const +{ +#ifdef MIXBUS + return false; +#endif + return true; +} + +uint32_t +SurroundSend::n_pannables () const +{ + /* do not use _pannable.size(), + * if we would do so, state of removed pannables would be saved. + */ +#ifdef MIXBUS + return std::min (2, _configured_input.n_audio ()); +#endif + return _configured_input.n_audio (); +} + +void +SurroundSend::add_pannable () +{ + std::shared_ptr p = std::shared_ptr (new SurroundPannable (_session, _pannable.size (), Temporal::TimeDomainProvider (Temporal::AudioTime))); + + add_control (p->pan_pos_x); + add_control (p->pan_pos_y); + add_control (p->pan_pos_z); + add_control (p->pan_size); + add_control (p->pan_snap); + add_control (p->binaural_render_mode); + _pannable.push_back (p); + + _change_connections.drop_connections (); + for (auto const& c: _controls) { + std::shared_ptr ac = std::dynamic_pointer_cast(c.second); + ac->Changed.connect_same_thread (_change_connections, [this](bool, PBD::Controllable::GroupControlDisposition) { PanChanged (); /* EMIT SIGNAL*/}); + } +} + +bool +SurroundSend::configure_io (ChanCount in, ChanCount out) +{ + bool changed = false; + uint32_t n_audio = in.n_audio (); + +#ifdef MIXBUS + n_audio = std::min (2, n_audio); +#endif + + if (_configured) { + changed = n_audio != n_pannables (); + } + + while (_pannable.size () < n_audio) { + add_pannable (); + } + + if (!_configured && !_has_state) { + switch (n_audio) { + case 2: + _pannable[0]->pan_pos_x->set_value (0.0, PBD::Controllable::NoGroup); + _pannable[1]->pan_pos_x->set_value (1.0, PBD::Controllable::NoGroup); + break; + case 3: + _pannable[0]->pan_pos_x->set_value (0.0, PBD::Controllable::NoGroup); + _pannable[1]->pan_pos_x->set_value (1.0, PBD::Controllable::NoGroup); + _pannable[2]->pan_pos_x->set_value (0.5, PBD::Controllable::NoGroup); + break; + case 5: + _pannable[0]->pan_pos_x->set_value (0.0, PBD::Controllable::NoGroup); + _pannable[1]->pan_pos_x->set_value (1.0, PBD::Controllable::NoGroup); + _pannable[2]->pan_pos_x->set_value (0.5, PBD::Controllable::NoGroup); + _pannable[3]->pan_pos_x->set_value (0.0, PBD::Controllable::NoGroup); + _pannable[4]->pan_pos_x->set_value (1.0, PBD::Controllable::NoGroup); + _pannable[3]->pan_pos_y->set_value (1.0, PBD::Controllable::NoGroup); + _pannable[4]->pan_pos_y->set_value (1.0, PBD::Controllable::NoGroup); + break; + default: + break; + } + } + + ChanCount ca (DataType::AUDIO, n_audio); + _amp->configure_io (ca, ca); + + if (!_send_delay->configure_io (ca, ca)) { + return false; + } + if (!_thru_delay->configure_io (in, out)) { + return false; + } + + if (_configured && changed) { + /* We cannot emit `processors_changed` while holing the `process lock` */ + dynamic_cast (_owner)->queue_surround_processors_changed (); /* EMIT SIGNAL */ + } + + Processor::configure_io (in, out); /* may EMIT SIGNAL ConfigurationChanged */ + + set_block_size (_session.get_block_size ()); + + if (changed) { + NPannablesChanged (); /* EMIT SIGNAL */ + } + return true; +} + +void +SurroundSend::ensure_mixbufs () +{ + _mixbufs.ensure_buffers (DataType::AUDIO, n_pannables (), _session.get_block_size ()); +} + +int +SurroundSend::set_block_size (pframes_t) +{ + ensure_mixbufs (); + return 0; +} + +void +SurroundSend::cycle_start (pframes_t /*nframes*/) +{ + for (BufferSet::audio_iterator b = _mixbufs.audio_begin (); b != _mixbufs.audio_end (); ++b) { + b->prepare (); + } +} + +std::string +SurroundSend::describe_parameter (Evoral::Parameter param) +{ + if (n_pannables () < 2) { + /* Use default names */ + return Automatable::describe_parameter (param); + } + + std::string prefix; + if (n_pannables () == 2) { + prefix = string_compose ("[%1]", param.id() == 0 ? S_("Panner|L") : S_("Panner|R")); + } else { + prefix = string_compose ("[%1]", 1 + param.id()); + } + + if (param.type() == PanSurroundX) { + return string_compose("%1 %2", prefix, _("Left/Right")); + } else if (param.type() == PanSurroundY) { + return string_compose("%1 %2", prefix, _("Front/Back")); + } else if (param.type() == PanSurroundZ) { + return string_compose("%1 %2", prefix, _("Elevation")); + } else if (param.type() == PanSurroundSize) { + return string_compose("%1 %2", prefix, _("Object Size")); + } else if (param.type() == PanSurroundSnap) { + return string_compose("%1 %2", prefix, _("Snap to Speaker")); + } else if (param.type() == BinauralRenderMode) { + return string_compose("%1 %2", prefix, _("Binaural Render mode")); + } + + return Automatable::describe_parameter (param); +} + +int +SurroundSend::set_state (const XMLNode& node, int version) +{ + XMLNode* gainnode = node.child (PBD::Controllable::xml_node_name.c_str()); + _gain_control->set_state (*gainnode, version); + + uint32_t npan; + if (!node.get_property("n-pannables", npan)) { + return -1; + } + + while (_pannable.size () < npan) { + add_pannable (); + } + + XMLNodeList pans = node.children (X_("SurroundPannable")); + for (auto const& c: pans) { + uint32_t chn; + if (!c->get_property("channel", chn)) { + continue; + } + _pannable[chn]->set_state (*c, version); + } + + _has_state = true; + + return Processor::set_state (node, version); +} + +XMLNode& +SurroundSend::state () const +{ + XMLNode& node (Processor::state ()); + node.set_property ("type", "sursend"); + node.set_property ("n-pannables", n_pannables ()); + + node.add_child_nocopy (_gain_control->get_state()); + for (uint32_t chn = 0; chn < n_pannables (); ++ chn) { + node.add_child_nocopy (_pannable[chn]->get_state ()); + } + return node; +} diff --git a/libs/ardour/wscript b/libs/ardour/wscript index f002c2ea0f..0d0b7bdca1 100644 --- a/libs/ardour/wscript +++ b/libs/ardour/wscript @@ -249,6 +249,9 @@ libardour_sources = [ 'stripable.cc', # 'step_sequencer.cc', 'strip_silence.cc', + 'surround_pannable.cc', + 'surround_return.cc', + 'surround_send.cc', 'system_exec.cc', 'revision.cc', 'rt_midibuffer.cc',