ardour/libs/ardour/automation_control.cc

418 lines
12 KiB
C++

/*
* Copyright (C) 2007-2014 David Robillard <d@drobilla.net>
* Copyright (C) 2008-2017 Paul Davis <paul@linuxaudiosystems.com>
* Copyright (C) 2014-2019 Robin Gareus <robin@gareus.org>
* Copyright (C) 2015 Nick Mainsbridge <mainsbridge@gmail.com>
*
* 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 <math.h>
#include <iostream>
#include "pbd/memento_command.h"
#include "ardour/audioengine.h"
#include "ardour/automation_control.h"
#include "ardour/automation_watch.h"
#include "ardour/control_group.h"
#include "ardour/event_type_map.h"
#include "ardour/session.h"
#include "ardour/selection.h"
#include "ardour/value_as_string.h"
#include "pbd/i18n.h"
#ifdef COMPILER_MSVC
#include <float.h>
// C99 'isfinite()' is not available in MSVC.
#define isfinite_local(val) (bool)_finite((double)val)
#else
#define isfinite_local isfinite
#endif
using namespace std;
using namespace ARDOUR;
using namespace PBD;
AutomationControl::AutomationControl(ARDOUR::Session& session,
const Evoral::Parameter& parameter,
const ParameterDescriptor& desc,
std::shared_ptr<ARDOUR::AutomationList> list,
const string& name,
Controllable::Flag flags)
: Controllable (name.empty() ? EventTypeMap::instance().to_symbol(parameter) : name, flags)
, Evoral::Control(parameter, desc, list)
, SessionHandleRef (session)
, _desc(desc)
, _no_session(false)
{
if (_desc.toggled) {
set_flags (Controllable::Toggle);
}
std::shared_ptr<AutomationList> al = alist();
if (al) {
al->StateChanged.connect_same_thread (_state_changed_connection, boost::bind (&Session::set_dirty, &_session));
}
}
AutomationControl::~AutomationControl ()
{
if (!_no_session && !_session.deletion_in_progress ()) {
_session.selection().remove_control_by_id (id());
DropReferences (); /* EMIT SIGNAL */
}
}
void
AutomationControl::session_going_away ()
{
SessionHandleRef::session_going_away ();
DropReferences (); /* EMIT SIGNAL */
_no_session = true;
}
bool
AutomationControl::writable() const
{
std::shared_ptr<AutomationList> al = alist();
if (al) {
return al->automation_state() != Play;
}
return true;
}
double
AutomationControl::get_value() const
{
/* automation_run() will have updated the value
* (possibly ahead of time, according to latency compensation),
* and actually_set_value() will have set the user-value accordingly.
*/
return Control::get_double();
}
double
AutomationControl::get_save_value() const
{
/* save user-value, not incl masters */
return Control::get_double ();
}
void
AutomationControl::pre_realtime_queue_stuff (double val, PBD::Controllable::GroupControlDisposition gcd)
{
if (_group && _group->use_me (gcd)) {
_group->pre_realtime_queue_stuff (val);
} else {
do_pre_realtime_queue_stuff (val);
}
}
void
AutomationControl::set_value (double val, PBD::Controllable::GroupControlDisposition gcd)
{
if (!writable()) {
return;
}
if (_list && !touching () && alist()->automation_state() == Latch && _session.transport_rolling ()) {
start_touch (timepos_t (_session.transport_sample ()));
}
/* enforce strict double/boolean value mapping */
if (_desc.toggled) {
if (val != 0.0) {
val = 1.0;
}
}
if (check_rt (val, gcd)) {
/* change has been queued to take place in an RT context */
return;
}
if (_group && _group->use_me (gcd)) {
_group->set_group_value (std::dynamic_pointer_cast<AutomationControl>(shared_from_this()), val);
} else {
actually_set_value (val, gcd);
}
}
AutomationControlList
AutomationControl::grouped_controls () const
{
if (_group && _group->use_me (PBD::Controllable::UseGroup)) {
return _group->controls ();
} else {
return AutomationControlList ();
}
}
void
AutomationControl::automation_run (samplepos_t start, pframes_t nframes)
{
if (!automation_playback ()) {
return;
}
assert (_list);
bool valid = false;
double val = _list->rt_safe_eval (timepos_t (start), valid);
if (!valid) {
return;
}
if (toggled ()) {
const double thresh = .5 * (_desc.upper - _desc.lower);
set_value_unchecked (val >= thresh ? _desc.upper : _desc.lower);
} else {
set_value_unchecked (val);
}
}
/** Set the value and do the right thing based on automation state
* (e.g. record if necessary, etc.)
* @param value `user' value
*/
void
AutomationControl::actually_set_value (double value, PBD::Controllable::GroupControlDisposition gcd)
{
std::shared_ptr<AutomationList> al = alist ();
const samplepos_t pos = _session.transport_sample();
bool to_list;
/* We cannot use ::get_value() here since that is virtual, and intended
to return a scalar value that in some way reflects the state of the
control (with semantics defined by the control itself, since it's
internal state may be more complex than can be fully represented by
a single scalar).
This method's only job is to set the "user_double()" value of the
underlying Evoral::Control object, and so we should compare the new
value we're being given to the current user_double().
Unless ... we're doing automation playback, in which case the
current effective value of the control (used to determine if
anything has changed) is the one derived from the automation event
list.
*/
float old_value = Control::get_double();
if (al && al->automation_write ()) {
to_list = true;
} else {
to_list = false;
}
Control::set_double (value, timepos_t (pos), to_list);
if (old_value != (float)value) {
#if 0
AutomationType at = (AutomationType) _parameter.type();
std::cerr << "++++ Changed (" << enum_2_string (at) << ", " << enum_2_string (gcd) << ") = " << value
<< " (was " << old_value << ") @ " << this << std::endl;
#endif
Changed (true, gcd);
if (!al || !al->automation_playback ()) {
_session.set_dirty ();
}
}
}
void
AutomationControl::set_list (std::shared_ptr<Evoral::ControlList> list)
{
Control::set_list (list);
Changed (true, Controllable::NoGroup);
}
void
AutomationControl::set_automation_state (AutoState as)
{
if (flags() & NotAutomatable) {
return;
}
if (alist() && as != alist()->automation_state()) {
const double val = get_value ();
alist()->set_automation_state (as);
if (as == Write) {
AutomationWatch::instance().add_automation_watch (std::dynamic_pointer_cast<AutomationControl>(shared_from_this()));
} else if (as & (Touch | Latch)) {
if (alist()->empty()) {
if (alist()->time_domain() == Temporal::AudioTime) {
Control::set_double (val, timepos_t (_session.current_start ().samples()), true);
Control::set_double (val, timepos_t (_session.current_end ().samples()), true);
} else {
Control::set_double (val, timepos_t (_session.current_start ().beats()), true);
Control::set_double (val, timepos_t (_session.current_end ().beats()), true);
}
Changed (true, Controllable::NoGroup);
}
if (!touching()) {
AutomationWatch::instance().remove_automation_watch (std::dynamic_pointer_cast<AutomationControl>(shared_from_this()));
} else {
/* this seems unlikely, but the combination of
* a control surface and the mouse could make
* it possible to put the control into Touch
* mode *while* touching it.
*/
AutomationWatch::instance().add_automation_watch (std::dynamic_pointer_cast<AutomationControl>(shared_from_this()));
}
} else {
AutomationWatch::instance().remove_automation_watch (std::dynamic_pointer_cast<AutomationControl>(shared_from_this()));
Changed (false, Controllable::NoGroup);
}
}
}
void
AutomationControl::start_touch (timepos_t const & when)
{
if (!_list || touching ()) {
return;
}
ControlTouched (std::dynamic_pointer_cast<PBD::Controllable>(shared_from_this())); /* EMIT SIGNAL */
if (alist()->automation_state() & (Touch | Latch)) {
/* subtle. aligns the user value with the playback and
* use take actual value (incl masters).
*
* Touch + hold writes inverse curve of master-automation
* using AutomationWatch::timer ()
*/
AutomationControl::actually_set_value (get_value (), Controllable::NoGroup);
alist()->start_touch (when);
AutomationWatch::instance().add_automation_watch (std::dynamic_pointer_cast<AutomationControl>(shared_from_this()));
set_touching (true);
}
}
void
AutomationControl::stop_touch (timepos_t const & when)
{
if (!_list || !touching ()) {
return;
}
if (alist()->automation_state() == Latch && _session.transport_rolling ()) {
return;
}
if (alist()->automation_state() == Touch && _session.transport_rolling () && _desc.toggled) {
/* Toggle buttons always latch */
return;
}
set_touching (false);
if (alist()->automation_state() & (Touch | Latch)) {
alist()->stop_touch (when);
AutomationWatch::instance().remove_automation_watch (std::dynamic_pointer_cast<AutomationControl>(shared_from_this()));
}
}
void
AutomationControl::commit_transaction (bool did_write)
{
if (did_write) {
XMLNode* before = alist ()->before ();
if (before) {
_session.begin_reversible_command (string_compose (_("record %1 automation"), name ()));
_session.commit_reversible_command (alist ()->memento_command (before, &alist ()->get_state ()));
}
} else {
alist ()->clear_history ();
}
}
/* take control-value and return UI range [0..1] */
double
AutomationControl::internal_to_interface (double val, bool rotary) const
{
// XXX maybe optimize. _desc.from_interface() has
// a switch-statement depending on AutomationType.
return _desc.to_interface (val, rotary);
}
/* map GUI range [0..1] to control-value */
double
AutomationControl::interface_to_internal (double val, bool rotary) const
{
if (!isfinite_local (val)) {
assert (0);
val = 0;
}
// XXX maybe optimize. see above.
return _desc.from_interface (val, rotary);
}
std::string
AutomationControl::get_user_string () const
{
return ARDOUR::value_as_string (_desc, get_value());
}
bool
AutomationControl::push_group (std::shared_ptr<ControlGroup> cg)
{
if (_pushed_group) {
return false;
}
_pushed_group = _group;
_group = cg;
return true;
}
bool
AutomationControl::pop_group ()
{
_group = _pushed_group;
_pushed_group.reset ();
return true;
}
void
AutomationControl::set_group (std::shared_ptr<ControlGroup> cg)
{
/* this method can only be called by a ControlGroup. We do not need
to ensure consistency by calling ControlGroup::remove_control(),
since we are guaranteed that the ControlGroup will take care of that
for us.
*/
_group = cg;
}
bool
AutomationControl::check_rt (double val, Controllable::GroupControlDisposition gcd)
{
if (!_session.loading() && (flags() & Controllable::RealTime) && !AudioEngine::instance()->in_process_thread()) {
/* queue change in RT context */
_session.set_control (std::dynamic_pointer_cast<AutomationControl>(shared_from_this()), val, gcd);
return true;
}
return false;
}