475 lines
13 KiB
C++
475 lines
13 KiB
C++
/*
|
|
* Copyright (C) 2023 Paul Davis <paul@linuxaudiosystems.com>
|
|
* Copyright (C) 2023 Robin Gareus <robin@gareus.org>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*/
|
|
|
|
#include "pbd/unwind.h"
|
|
|
|
#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<MuteMaster> mm)
|
|
: Processor (s, _("Surround"), Temporal::TimeDomainProvider (Temporal::AudioTime))
|
|
, _surround_id (s.next_surround_send_id ())
|
|
, _current_gain (GAIN_COEFF_ZERO)
|
|
, _has_state (false)
|
|
, _ignore_enable_change (false)
|
|
, _mute_master (mm)
|
|
|
|
{
|
|
_send_delay.reset (new DelayLine (_session, "Send-" + name ()));
|
|
_thru_delay.reset (new DelayLine (_session, "Thru-" + name ()));
|
|
|
|
std::shared_ptr<AutomationList> gl (new AutomationList (Evoral::Parameter (SurroundSendLevel), *this));
|
|
_gain_control = std::shared_ptr<GainControl> (new GainControl (_session, Evoral::Parameter (SurroundSendLevel), 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);
|
|
|
|
_send_enable_control = std::shared_ptr<AutomationControl> (new AutomationControl (_session, BusSendEnable, ParameterDescriptor(BusSendEnable)));
|
|
_send_enable_control->Changed.connect_same_thread (*this, boost::bind (&SurroundSend::send_enable_changed, this));
|
|
_send_enable_control->clear_flag (PBD::Controllable::RealTime);
|
|
|
|
ActiveChanged.connect_same_thread (*this, boost::bind (&SurroundSend::proc_active_changed, this));
|
|
|
|
InternalSend::CycleStart.connect_same_thread (*this, boost::bind (&SurroundSend::cycle_start, this, _1));
|
|
}
|
|
|
|
SurroundSend::~SurroundSend ()
|
|
{
|
|
_send_enable_control->drop_references ();
|
|
}
|
|
|
|
std::shared_ptr<SurroundPannable>
|
|
SurroundSend::pannable (size_t chn) const
|
|
{
|
|
return _pannable[chn];
|
|
}
|
|
|
|
std::shared_ptr<SurroundPannable> 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<uint32_t> (2, _configured_input.n_audio ());
|
|
#endif
|
|
return _configured_input.n_audio ();
|
|
}
|
|
|
|
void
|
|
SurroundSend::add_pannable ()
|
|
{
|
|
std::shared_ptr<SurroundPannable> p = std::shared_ptr<SurroundPannable> (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);
|
|
|
|
for (uint32_t i = 0; i < _pannable.size (); ++i) {
|
|
_pannable[i]->sync_auto_state_with (p);
|
|
p->sync_auto_state_with (_pannable[i]);
|
|
}
|
|
|
|
_pannable.push_back (p);
|
|
|
|
_change_connections.drop_connections ();
|
|
for (auto const& c: _controls) {
|
|
std::shared_ptr<AutomationControl> ac = std::dynamic_pointer_cast<AutomationControl>(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<uint32_t> (2, n_audio);
|
|
#endif
|
|
|
|
if (_configured) {
|
|
changed = n_audio != n_pannables ();
|
|
}
|
|
|
|
while (_pannable.size () < n_audio) {
|
|
add_pannable ();
|
|
}
|
|
|
|
if (changed) {
|
|
for (uint32_t i = 0; i < n_audio; ++i) {
|
|
_pannable[i]->foreach_pan_control ([](std::shared_ptr<AutomationControl> ac) { ac->clear_flag (PBD::Controllable::HiddenControl); });
|
|
}
|
|
for (uint32_t i = n_audio; i < _pannable.size (); ++i) {
|
|
_pannable[i]->foreach_pan_control ([](std::shared_ptr<AutomationControl> ac) { ac->set_flag (PBD::Controllable::HiddenControl); });
|
|
}
|
|
}
|
|
|
|
#ifdef MIXBUS
|
|
/* Link visibility - currently only for Mixbus which has a custom UI, and at most stereo */
|
|
for (uint32_t i = 0; i < _pannable.size (); ++i) {
|
|
_pannable[i]->foreach_pan_control ([](std::shared_ptr<AutomationControl> ac) { ac->clear_visually_linked_control (); });
|
|
}
|
|
/* first link local controls */
|
|
for (uint32_t i = 0; i < n_audio; ++i) {
|
|
_pannable[i]->setup_visual_links ();
|
|
}
|
|
for (uint32_t i = 0; i < n_audio; ++i) {
|
|
for (uint32_t j = 0; j < n_audio; ++j) {
|
|
if (i == j) {
|
|
continue;
|
|
}
|
|
_pannable[i]->sync_visual_link_to (_pannable[j]);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
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<Route*> (_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 (param.id() >= n_pannables ()) {
|
|
return X_("hidden");
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
void
|
|
SurroundSend::send_enable_changed ()
|
|
{
|
|
if (_ignore_enable_change) {
|
|
return;
|
|
}
|
|
PBD::Unwinder<bool> uw (_ignore_enable_change, true);
|
|
if (_send_enable_control->get_value () > 0) {
|
|
activate ();
|
|
} else {
|
|
deactivate ();
|
|
}
|
|
}
|
|
|
|
void
|
|
SurroundSend::proc_active_changed ()
|
|
{
|
|
if (_ignore_enable_change) {
|
|
return;
|
|
}
|
|
PBD::Unwinder<bool> uw (_ignore_enable_change, true);
|
|
_send_enable_control->set_value (_pending_active ? 1 : 0, PBD::Controllable::UseGroup);
|
|
}
|
|
|
|
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;
|
|
}
|