ardour/libs/ardour/triggerbox.cc

3466 lines
91 KiB
C++
Raw Normal View History

#include <algorithm>
#include <iostream>
#include <cstdlib>
#include <sstream>
#include <boost/make_shared.hpp>
#include <glibmm.h>
#include <rubberband/RubberBandStretcher.h>
2021-07-28 00:39:42 -04:00
#include "pbd/basename.h"
#include "pbd/compose.h"
#include "pbd/failed_constructor.h"
#include "pbd/pthread_utils.h"
2021-08-31 18:46:19 -04:00
#include "pbd/types_convert.h"
#include "temporal/tempo.h"
#include "ardour/audioengine.h"
#include "ardour/audioregion.h"
#include "ardour/audio_buffer.h"
#include "ardour/debug.h"
#include "ardour/import_status.h"
#include "ardour/midi_buffer.h"
#include "ardour/midi_model.h"
#include "ardour/midi_region.h"
#include "ardour/minibpm.h"
#include "ardour/port.h"
#include "ardour/region_factory.h"
#include "ardour/session.h"
#include "ardour/session_object.h"
#include "ardour/sidechain.h"
#include "ardour/source_factory.h"
#include "ardour/smf_source.h"
#include "ardour/sndfilesource.h"
#include "ardour/triggerbox.h"
#include "ardour/types_convert.h"
#include "pbd/i18n.h"
using namespace PBD;
using namespace ARDOUR;
using std::string;
using std::cerr;
using std::endl;
2021-08-07 18:20:36 -04:00
namespace ARDOUR {
namespace Properties {
PBD::PropertyDescriptor<bool> running;
PBD::PropertyDescriptor<bool> passthru;
PBD::PropertyDescriptor<bool> legato;
PBD::PropertyDescriptor<bool> use_follow_length;
PBD::PropertyDescriptor<Temporal::BBT_Offset> quantization;
PBD::PropertyDescriptor<Temporal::BBT_Offset> follow_length;
2021-10-01 20:39:06 -04:00
PBD::PropertyDescriptor<Trigger::LaunchStyle> launch_style;
PBD::PropertyDescriptor<ARDOUR::FollowAction> follow_action0;
PBD::PropertyDescriptor<ARDOUR::FollowAction> follow_action1;
PBD::PropertyDescriptor<uint32_t> currently_playing;
PBD::PropertyDescriptor<uint32_t> follow_count;
2021-12-06 12:27:59 -05:00
PBD::PropertyDescriptor<int> follow_action_probability;
PBD::PropertyDescriptor<float> velocity_effect;
PBD::PropertyDescriptor<gain_t> gain;
PBD::PropertyDescriptor<bool> stretchable;
PBD::PropertyDescriptor<bool> isolated;
PBD::PropertyDescriptor<Trigger::StretchMode> stretch_mode;
PBD::PropertyDescriptor<bool> tempo_meter; /* only to transmit updates, not storage */
2021-08-07 18:20:36 -04:00
}
}
FollowAction::FollowAction (std::string const & str)
{
std::string::size_type colon = str.find_first_of (':');
if (colon == std::string::npos) {
throw failed_constructor ();
}
type = FollowAction::Type (string_2_enum (str.substr (0, colon), type));
/* We use the ulong representation of the bitset because the string
version is absurd.
*/
unsigned long ul;
std::stringstream ss (str.substr (colon+1));
ss >> ul;
if (!ss) {
throw failed_constructor();
}
targets = Targets (ul);
}
std::string
FollowAction::to_string () const
{
/* We use the ulong representation of the bitset because the string
version is absurd.
*/
return string_compose ("%1:%2", enum_2_string (type), targets.to_ulong());
}
Trigger * const Trigger::MagicClearPointerValue = (Trigger*) 0xfeedface;
Trigger::Trigger (uint32_t n, TriggerBox& b)
: _box (b)
, _state (Stopped)
, _bang (0)
, _unbang (0)
, _index (n)
, _loop_cnt (0)
, _ui (0)
, _explicitly_stopped (false)
, _pending_velocity_gain (1.0)
, _velocity_gain (1.0)
, _launch_style (Properties::launch_style, OneShot)
, _follow_action0 (Properties::follow_action0, FollowAction (FollowAction::Again))
, _follow_action1 (Properties::follow_action1, FollowAction (FollowAction::Stop))
, _follow_action_probability (Properties::follow_action_probability, 0)
, _follow_count (Properties::follow_count, 1)
, _quantization (Properties::quantization, Temporal::BBT_Offset (1, 0, 0))
, _follow_length (Properties::follow_length, Temporal::BBT_Offset (1, 0, 0))
, _use_follow_length (Properties::use_follow_length, false)
, _legato (Properties::legato, false)
, _name (Properties::name, "")
, _gain (Properties::gain, 1.0)
, _midi_velocity_effect (Properties::velocity_effect, 0.)
, _stretchable (Properties::stretchable, true)
, _isolated (Properties::isolated, false)
, _color (Properties::color, 0xBEBEBEFF)
, _stretch_mode (Properties::stretch_mode, Trigger::Crisp)
, cue_launched (false)
, _estimated_tempo (0.)
, _segment_tempo (0.)
2022-01-14 16:35:51 -05:00
, _barcnt (0.)
, _meter (4, 4)
, expected_end_sample (0)
, _pending ((Trigger*) 0)
2021-08-07 18:20:36 -04:00
{
add_property (_launch_style);
add_property (_follow_action0);
add_property (_follow_action1);
add_property (_follow_action_probability);
add_property (_follow_count);
add_property (_quantization);
add_property (_follow_length);
add_property (_use_follow_length);
add_property (_legato);
add_property (_name);
add_property (_gain);
add_property (_midi_velocity_effect);
add_property (_stretchable);
add_property (_isolated);
add_property (_color);
add_property (_stretch_mode);
}
void
Trigger::request_trigger_delete (Trigger* t)
{
TriggerBox::worker->request_delete_trigger (t);
}
void
Trigger::set_pending (Trigger* t)
{
Trigger* old = _pending.exchange (t);
if (old && old != MagicClearPointerValue) {
/* new pending trigger set before existing pending trigger was used */
delete old;
}
}
Trigger*
Trigger::swap_pending (Trigger* t)
{
return _pending.exchange (t);
}
bool
Trigger::will_not_follow () const
{
return (_follow_action0.val().type == FollowAction::None && _follow_action_probability == 0) ||
(_follow_action0.val().type == FollowAction::None && _follow_action1.val().type == FollowAction::None);
2021-08-07 18:20:36 -04:00
}
void
Trigger::set_name (std::string const & str)
{
2022-01-16 11:56:45 -05:00
if (_name == str) {
return;
}
_name = str;
PropertyChanged (Properties::name);
_box.session().set_dirty();
}
void
Trigger::set_scene_isolated (bool i)
{
2022-01-16 11:56:45 -05:00
if (_isolated == i) {
return;
}
_isolated = i;
PropertyChanged (ARDOUR::Properties::isolated);
_box.session().set_dirty();
}
void
Trigger::set_color (color_t c)
{
2022-01-16 11:56:45 -05:00
if (_color == c) {
return;
}
_color = c;
PropertyChanged (ARDOUR::Properties::color);
_box.session().set_dirty();
}
void
Trigger::set_stretchable (bool s)
{
2022-01-16 11:56:45 -05:00
if (_stretchable == s) {
return;
}
_stretchable = s;
PropertyChanged (ARDOUR::Properties::stretchable);
_box.session().set_dirty();
}
void
Trigger::set_ui (void* p)
{
_ui = p;
}
void
Trigger::bang ()
{
if (!_region) {
return;
}
_bang.fetch_add (1);
2021-08-30 18:31:35 -04:00
DEBUG_TRACE (DEBUG::Triggers, string_compose ("bang on %1\n", _index));
}
void
Trigger::unbang ()
{
if (!_region) {
return;
}
_unbang.fetch_add (1);
2021-08-30 18:31:35 -04:00
DEBUG_TRACE (DEBUG::Triggers, string_compose ("un-bang on %1\n", _index));
}
void
Trigger::set_gain (gain_t g)
{
if (_gain == g) {
return;
}
_gain = g;
PropertyChanged (Properties::gain);
_box.session().set_dirty();
}
void
Trigger::set_midi_velocity_effect (float mve)
{
if (_midi_velocity_effect == mve) {
return;
}
_midi_velocity_effect = std::min (1.f, std::max (0.f, mve));
PropertyChanged (Properties::velocity_effect);
_box.session().set_dirty();
}
2021-10-07 00:40:37 -04:00
void
Trigger::set_follow_count (uint32_t n)
{
if (_follow_count == n) {
return;
}
2021-10-07 00:40:37 -04:00
_follow_count = n;
PropertyChanged (Properties::follow_count);
_box.session().set_dirty();
2021-10-07 00:40:37 -04:00
}
void
Trigger::set_follow_action (FollowAction f, uint32_t n)
{
2021-08-12 00:54:13 -04:00
assert (n < 2);
2021-10-01 20:39:06 -04:00
if (n == 0) {
if (_follow_action0 == f) {
return;
}
_follow_action0 = f;
2021-10-01 20:39:06 -04:00
PropertyChanged (Properties::follow_action0);
} else {
if (_follow_action1 == f) {
return;
}
_follow_action1 = f;
2021-10-01 20:39:06 -04:00
PropertyChanged (Properties::follow_action1);
}
_box.session().set_dirty();
}
Trigger::LaunchStyle
Trigger::launch_style () const
{
if (cue_launched) {
return OneShot;
}
return _launch_style;
}
void
Trigger::set_launch_style (LaunchStyle l)
{
if (_launch_style == l) {
return;
}
_launch_style = l;
2021-10-01 20:39:06 -04:00
PropertyChanged (Properties::launch_style);
_box.session().set_dirty();
}
XMLNode&
Trigger::get_state (void)
{
XMLNode* node = new XMLNode (X_("Trigger"));
2021-08-31 18:46:19 -04:00
for (OwnedPropertyList::iterator i = _properties->begin(); i != _properties->end(); ++i) {
i->second->get_value (*node);
}
2021-08-31 18:46:19 -04:00
node->set_property (X_("index"), _index);
node->set_property (X_("estimated-tempo"), _estimated_tempo);
node->set_property (X_("segment-tempo"), _segment_tempo);
node->set_property (X_("barcnt"), _barcnt);
2021-08-31 18:46:19 -04:00
if (_region) {
node->set_property (X_("region"), _region->id());
}
return *node;
}
int
Trigger::set_state (const XMLNode& node, int version)
{
/* Set region first since set_region_in_worker_thread() will set some
values that may/will need to be overridden by XML
*/
PBD::ID rid;
node.get_property (X_("region"), rid);
boost::shared_ptr<Region> r = RegionFactory::region_by_id (rid);
if (r) {
set_region (r, false); //TODO: this results in a call to estimate_tempo() which should be avoided if bpm is already known
}
node.get_property (X_("estimated-tempo"), _estimated_tempo); //TODO: for now: if we know the bpm, overwrite the value that estimate_tempo() found
node.get_property (X_("segment-tempo"), _segment_tempo);
node.get_property (X_("barcnt"), _barcnt);
node.get_property (X_("index"), _index);
set_values (node);
return 0;
}
2021-08-12 01:09:36 -04:00
void
Trigger::set_follow_length (Temporal::BBT_Offset const & bbo)
{
2022-01-16 11:56:45 -05:00
if (_use_follow_length == bbo) {
return;
}
_follow_length = bbo;
2022-01-16 11:56:45 -05:00
PropertyChanged (Properties::use_follow_length);
_box.session().set_dirty();
}
void
Trigger::set_use_follow_length (bool ufl)
{
if (_use_follow_length == ufl) {
return;
}
_use_follow_length = ufl;
PropertyChanged (Properties::use_follow_length);
_box.session().set_dirty();
}
bool
Trigger::internal_use_follow_length () const
{
return (_follow_action0.val().type != FollowAction::None) && _use_follow_length;
}
void
Trigger::set_legato (bool yn)
{
2022-01-16 11:56:45 -05:00
if (_legato == yn) {
return;
}
_legato = yn;
PropertyChanged (Properties::legato);
_box.session().set_dirty();
}
2021-08-12 01:09:36 -04:00
void
Trigger::set_follow_action_probability (int n)
{
if (_follow_action_probability == n) {
return;
}
2021-08-12 01:09:36 -04:00
n = std::min (100, n);
n = std::max (0, n);
_follow_action_probability = n;
PropertyChanged (Properties::follow_action_probability);
_box.session().set_dirty();
2021-08-12 01:09:36 -04:00
}
void
Trigger::set_quantization (Temporal::BBT_Offset const & q)
{
if (_quantization == q) {
return;
}
_quantization = q;
PropertyChanged (Properties::quantization);
_box.session().set_dirty();
}
void
Trigger::set_region (boost::shared_ptr<Region> r, bool use_thread)
{
/* Called from (G)UI thread */
if (!r) {
/* clear operation, no need to talk to the worker thread */
set_pending (Trigger::MagicClearPointerValue);
2021-12-21 20:13:50 -05:00
request_stop ();
} else if (use_thread) {
/* load data, do analysis in another thread */
TriggerBox::worker->set_region (_box, index(), r);
} else {
set_region_in_worker_thread (r);
}
}
void
Trigger::clear_region ()
{
/* Called from RT process thread */
_region.reset ();
set_name("");
}
void
Trigger::set_region_internal (boost::shared_ptr<Region> r)
{
_region = r;
}
Temporal::BBT_Offset
Trigger::quantization () const
{
return _quantization;
}
void
Trigger::request_stop ()
{
_requests.stop = true;
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 asked to stop\n", name()));
}
void
Trigger::startup (Temporal::BBT_Offset const & start_quantization)
{
/* This is just a non-virtual wrapper with a default parameter that calls _startup() */
_startup (start_quantization);
}
void
Trigger::_startup (Temporal::BBT_Offset const & start_quantization)
{
_state = WaitingToStart;
_loop_cnt = 0;
_velocity_gain = _pending_velocity_gain;
_explicitly_stopped = false;
if (start_quantization == Temporal::BBT_Offset()) {
/* negative quantization means "do not quantize */
_start_quantization = Temporal::BBT_Offset (-1, 0, 0);
} else {
_start_quantization = _quantization;
}
retrigger ();
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 starts up\n", name()));
PropertyChanged (ARDOUR::Properties::running);
}
void
Trigger::shutdown (BufferSet& bufs, pframes_t dest_offset)
{
_state = Stopped;
cue_launched = false;
_pending_velocity_gain = _velocity_gain = 1.0;
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 shuts down\n", name()));
PropertyChanged (ARDOUR::Properties::running);
}
void
Trigger::jump_start()
{
/* this is used when we start a new trigger in legato mode. We do not
wait for quantization.
*/
_state = Running;
/* XXX set expected_end_sample */
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 requested state %2\n", index(), enum_2_string (_state)));
PropertyChanged (ARDOUR::Properties::running);
}
void
Trigger::jump_stop (BufferSet& bufs, pframes_t dest_offset)
{
/* this is used when we start a new trigger in legato mode. We do not
wait for quantization.
*/
shutdown (bufs, dest_offset);
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 requested state %2\n", index(), enum_2_string (_state)));
PropertyChanged (ARDOUR::Properties::running);
}
void
Trigger::begin_stop (bool explicit_stop)
{
/* this is used when we start a tell a currently playing trigger to
stop, but wait for quantization first.
*/
_state = WaitingToStop;
_explicitly_stopped = explicit_stop;
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 requested state %2\n", index(), enum_2_string (_state)));
PropertyChanged (ARDOUR::Properties::running);
}
void
Trigger::process_state_requests (BufferSet& bufs, pframes_t dest_offset)
{
bool stop = _requests.stop.exchange (false);
if (stop) {
/* This is for an immediate stop, not a quantized one */
if (_state != Stopped) {
shutdown (bufs, dest_offset);
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 immediate stop implemented\n", name()));
}
/* Don't process bang/unbang requests since we're stopping */
_bang = 0;
_unbang = 0;
return;
}
/* now check bangs/unbangs */
int x;
while ((x = _bang.load ())) {
_bang.fetch_sub (1);
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 handling bang with state = %2\n", index(), enum_2_string (_state)));
switch (_state) {
case Running:
case Playout:
switch (launch_style()) {
case OneShot:
/* do nothing, just let it keep playing */
break;
case ReTrigger:
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 oneshot %2 => %3\n", index(), enum_2_string (Running), enum_2_string (WaitingForRetrigger)));
_state = WaitingForRetrigger;
PropertyChanged (ARDOUR::Properties::running);
break;
case Gate:
case Toggle:
case Repeat:
if (_box.active_scene() >= 0) {
std::cerr << "should not happen, cue launching but launch_style() said " << enum_2_string (launch_style()) << std::endl;
} else {
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 %2 gate/toggle/repeat => %3\n", index(), enum_2_string (Running), enum_2_string (WaitingToStop)));
begin_stop (true);
}
}
break;
case Stopped:
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 %2 stopped => %3\n", index(), enum_2_string (Stopped), enum_2_string (WaitingToStart)));
_box.queue_explict (index());
cue_launched = (_box.active_scene() >= 0);
std::cerr << index() << " aka " << name() << " launched via cue ? " << cue_launched << std::endl;
break;
case WaitingToStart:
case WaitingToStop:
case WaitingForRetrigger:
case Stopping:
break;
}
}
while ((x = _unbang.load ())) {
_unbang.fetch_sub (1);
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 unbanged\n", index()));
switch (_state) {
case Running:
case Playout:
begin_stop (true);
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 unbanged, now in WaitingToStop\n", index()));
break;
case Stopped:
case Stopping: /* theoretically not possible */
case WaitingToStop:
case WaitingForRetrigger:
/* do nothing */
break;
case WaitingToStart:
/* didn't even get started */
shutdown (bufs, dest_offset);
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 unbanged, never started, now stopped\n", index()));
}
}
}
void
Trigger::maybe_compute_next_transition (samplepos_t start_sample, Temporal::Beats const & start, Temporal::Beats const & end, pframes_t& nframes, pframes_t& dest_offset, bool passthru)
{
using namespace Temporal;
/* This should never be called by a stopped trigger */
assert (_state != Stopped);
/* In these states, we are not waiting for a transition */
if (_state == Running || _state == Stopping || _state == Playout) {
/* will cover everything */
return;
}
timepos_t transition_time (BeatTime);
TempoMap::SharedPtr tmap (TempoMap::use());
Temporal::BBT_Time transition_bbt;
pframes_t extra_offset = 0;
BBT_Offset q (_start_quantization);
/* Clips don't stop on their own quantize; in Live they stop on the Global Quantize setting; we will choose 1 bar (Live's default) for now */
#warning when Global Quantize is implemented, use that instead of '1 bar' here
if (_state == WaitingToStop) {
q = BBT_Offset(1,0,0);
}
/* XXX need to use global grid here is quantization == zero */
/* Given the value of @param start, determine, based on the
* quantization, the next time for a transition.
*/
if (q < Temporal::BBT_Offset (0, 0, 0)) {
/* negative quantization == do not quantize */
transition_samples = start_sample;
transition_beats = start;
transition_time = timepos_t (start);
transition_bbt = tmap->bbt_at (transition_beats);
} else if (q.bars == 0) {
Temporal::Beats transition_beats = start.round_up_to_multiple (Temporal::Beats (q.beats, q.ticks));
transition_bbt = tmap->bbt_at (transition_beats);
transition_time = timepos_t (transition_beats);
} else {
transition_bbt = tmap->bbt_at (timepos_t (start));
transition_bbt = transition_bbt.round_up_to_bar ();
/* bars are 1-based; 'every 4 bars' means 'on bar 1, 5, 9, ...' */
transition_bbt.bars = 1 + ((transition_bbt.bars-1) / q.bars * q.bars);
transition_time = timepos_t (tmap->quarters_at (transition_bbt));
}
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 quantized with %5 transition at %2, sb %3 eb %4\n", index(), transition_time.beats(), start, end, q));
/* See if this time falls within the range of time given to us */
if (transition_time.beats() < start || transition_time > end) {
/* retrigger time has not been reached, just continue
to play normally until then.
*/
return;
}
/* transition time has arrived! let's figure out what're doing:
* stopping, starting, retriggering
*/
transition_samples = transition_time.samples();
transition_beats = transition_time.beats ();
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 in range, should start/stop at %2 aka %3\n", index(), transition_samples, transition_beats));
switch (_state) {
case WaitingToStop:
_state = Stopping;
PropertyChanged (ARDOUR::Properties::running);
/* trigger will reach it's end somewhere within this
* process cycle, so compute the number of samples it
* should generate.
*/
nframes = transition_samples - start_sample;
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 will stop somewhere in the middle of run(), specifically at %2 (%3) vs expected end at %4\n", name(), transition_time, transition_time.beats(), expected_end_sample));
/* offset within the buffer(s) for output remains
unchanged, since we will write from the first
location corresponding to start
*/
break;
case WaitingToStart:
retrigger ();
_state = Running;
set_expected_end_sample (tmap, transition_bbt, transition_samples);
PropertyChanged (ARDOUR::Properties::running);
/* trigger will start somewhere within this process
* cycle. Compute the sample offset where any audio
* should end up, and the number of samples it should generate.
*/
extra_offset = std::max (samplepos_t (0), transition_samples - start_sample);
nframes -= extra_offset;
dest_offset += extra_offset;
if (!passthru) {
/* XXX need to silence start of buffers up to dest_offset */
}
break;
case WaitingForRetrigger:
retrigger ();
_state = Running;
set_expected_end_sample (tmap, transition_bbt, transition_samples);
PropertyChanged (ARDOUR::Properties::running);
/* trigger is just running normally, and will fill
* buffers entirely.
*/
break;
default:
fatal << string_compose (_("programming error: %1"), "impossible trigger state in ::maybe_compute_next_transition()") << endmsg;
abort();
}
return;
}
void
Trigger::when_stopped_during_run (BufferSet& bufs, pframes_t dest_offset)
{
if (_state == Stopped || _state == Stopping) {
if ((_state == Stopped) && !_explicitly_stopped && (launch_style() == Trigger::Gate || launch_style() == Trigger::Repeat)) {
jump_start ();
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 was stopped, repeat/gate ret\n", index()));
} else {
if ((launch_style() != Repeat) && (launch_style() != Gate) && (_loop_cnt == _follow_count)) {
/* have played the specified number of times, we're done */
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 loop cnt %2 satisfied, now stopped\n", index(), _follow_count));
shutdown (bufs, dest_offset);
} else if (_state == Stopping) {
/* did not reach the end of the data. Presumably
* another trigger was explicitly queued, and we
* stopped
*/
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 not at end, but ow stopped\n", index()));
shutdown (bufs, dest_offset);
} else {
/* reached the end, but we haven't done that enough
* times yet for a follow action/stop to take
* effect. Time to get played again.
*/
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 was stopping, now waiting to retrigger, loop cnt %2 fc %3\n", index(), _loop_cnt, _follow_count));
/* we will "restart" at the beginning of the
next iteration of the trigger.
*/
_state = WaitingToStart;
retrigger ();
PropertyChanged (ARDOUR::Properties::running);
}
}
}
}
/*--------------------*/
AudioTrigger::AudioTrigger (uint32_t n, TriggerBox& b)
: Trigger (n, b)
, _stretcher (0)
, _start_offset (0)
, last_sample (0)
, read_index (0)
, process_index (0)
, _legato_offset (0)
, retrieved (0)
, got_stretcher_padding (false)
, to_pad (0)
, to_drop (0)
{
}
AudioTrigger::~AudioTrigger ()
{
drop_data ();
delete _stretcher;
}
void
AudioTrigger::set_stretch_mode (Trigger::StretchMode sm)
{
if (_stretch_mode == sm) {
return;
}
_stretch_mode = sm;
PropertyChanged (Properties::stretch_mode);
_box.session().set_dirty();
}
void
AudioTrigger::set_segment_tempo (double t)
{
if (_segment_tempo != t) {
_segment_tempo = t; //TODO : this data will likely get stored in the SegmentDescriptor, not the trigger itself
PropertyChanged (ARDOUR::Properties::tempo_meter);
_box.session().set_dirty();
}
}
void
AudioTrigger::set_segment_meter (Temporal::Meter const &m)
{
if (_meter != m) {
_meter = m;
//given a meter from the user, tempo is assumed constant and we re-calc barcnt internally
const double seconds = (double) data.length / _box.session().sample_rate();
const double quarters = (seconds / 60.) * _segment_tempo;
_barcnt = quarters / _meter.divisions_per_bar();
PropertyChanged (ARDOUR::Properties::tempo_meter);
_box.session().set_dirty();
}
}
void
AudioTrigger::set_segment_barcnt (double count)
{
if (_barcnt != count) {
_barcnt = count;
//given a barcnt from the user, meter is assumed constant and we re-calc tempo internally
const double seconds = (double) data.length / _box.session().sample_rate();
const double quarters = _barcnt * _meter.divisions_per_bar(); //TODO: this assumes note_value=quarter
_estimated_tempo = quarters / (seconds/60.0);
set_segment_tempo(_estimated_tempo);
}
}
bool
AudioTrigger::stretching() const
{
return (_segment_tempo != .0) && _stretchable;
}
2021-12-12 13:20:58 -05:00
SegmentDescriptor
AudioTrigger::get_segment_descriptor () const
{
SegmentDescriptor sd;
sd.set_tempo (Temporal::Tempo (_segment_tempo, 4));
2021-12-12 13:20:58 -05:00
return sd;
}
void
AudioTrigger::_startup (Temporal::BBT_Offset const & start_quantization)
{
Trigger::_startup (start_quantization);
}
void
AudioTrigger::jump_start ()
{
Trigger::jump_start ();
retrigger ();
}
void
AudioTrigger::jump_stop (BufferSet& bufs, pframes_t dest_offset)
{
Trigger::jump_stop (bufs, dest_offset);
retrigger ();
}
double
AudioTrigger::position_as_fraction () const
{
if (!active()) {
return 0.0;
}
return process_index / (double) (expected_end_sample - transition_samples);
}
2021-08-31 18:46:19 -04:00
XMLNode&
AudioTrigger::get_state (void)
{
XMLNode& node (Trigger::get_state());
node.set_property (X_("start"), timepos_t (_start_offset));
return node;
}
int
AudioTrigger::set_state (const XMLNode& node, int version)
2021-08-31 18:46:19 -04:00
{
timepos_t t;
if (!Trigger::set_state (node, version)) {
return -1;
}
node.get_property (X_("start"), t);
_start_offset = t.samples();
2021-08-31 18:46:19 -04:00
return 0;
}
void
AudioTrigger::set_start (timepos_t const & s)
{
_start_offset = s.samples ();
}
void
AudioTrigger::set_end (timepos_t const & e)
{
assert (!data.empty());
set_length (timecnt_t (e.samples() - _start_offset, timepos_t (_start_offset)));
}
void
AudioTrigger::set_legato_offset (timepos_t const & offset)
{
_legato_offset = offset.samples();
}
timepos_t
AudioTrigger::start_offset () const
{
return timepos_t (_start_offset);
}
timepos_t
AudioTrigger::current_pos() const
{
return timepos_t (process_index);
}
void
AudioTrigger::set_expected_end_sample (Temporal::TempoMap::SharedPtr const & tmap, Temporal::BBT_Time const & transition_bbt, samplepos_t transition_sample)
{
/* Our task here is to set:
expected_end_sample: the sample position where the data for the clip should run out (taking stretch into account)
last_sample: the sample in the data where we stop reading
final_sample: the sample where the trigger stops and the follow action if any takes effect
Things that affect these values:
data.length : how many samples there are in the data (AudioTime / samples)
_follow_length : the (user specified) time after the start of the trigger when the follow action should take effect
_use_follow_length : whether to use the follow_length value, or the clip's natural length
_barcnt : the expected duration of the trigger, based on analysis of its tempo, or user-set
*/
samplepos_t end_by_follow_length = tmap->sample_at (tmap->bbt_walk(transition_bbt, _follow_length));
samplepos_t end_by_barcnt = tmap->sample_at (tmap->bbt_walk(transition_bbt, Temporal::BBT_Offset (round (_barcnt), 0, 0)));
samplepos_t end_by_data_length = transition_sample + data.length;
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 @ %2 / %3 / %4 ends: FL %5 (from %6) BC %7 DL %8\n",
index(), transition_sample, transition_beats, transition_bbt,
end_by_follow_length, _follow_length, end_by_barcnt, end_by_data_length));
if (stretching()) {
if (internal_use_follow_length()) {
expected_end_sample = std::min (end_by_follow_length, end_by_barcnt);
} else {
expected_end_sample = end_by_barcnt;
}
} else {
if (internal_use_follow_length()) {
expected_end_sample = std::min (end_by_follow_length, end_by_data_length);
} else {
expected_end_sample = end_by_data_length;
}
}
if (internal_use_follow_length()) {
final_sample = end_by_follow_length - transition_sample;
} else {
final_sample = expected_end_sample - transition_sample;
}
samplecnt_t usable_length;
if (internal_use_follow_length() && (end_by_follow_length < end_by_data_length)) {
usable_length = tmap->sample_at (tmap->bbt_walk (Temporal::BBT_Time (), _follow_length));
} else {
usable_length = data.length;
}
/* called from set_expected_end_sample() when we know the time (audio &
* musical time domains when we start starting. Our job here is to
* define the last_sample we can use as data.
*/
Temporal::BBT_Offset q (_quantization);
if (launch_style() != Repeat || (q == Temporal::BBT_Offset())) {
last_sample = _start_offset + usable_length;
} else {
/* This is for Repeat mode only deliberately ignore the _follow_length
* here, because we'll be playing just the quantization distance no
* matter what.
*/
/* XXX MUST HANDLE BAR-LEVEL QUANTIZATION */
timecnt_t len (Temporal::Beats (q.beats, q.ticks), timepos_t (Temporal::Beats()));
last_sample = _start_offset + len.samples();
}
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1: final sample %2 vs ees %3 ls %4\n", index(), final_sample, expected_end_sample, last_sample));
}
void
AudioTrigger::set_length (timecnt_t const & newlen)
{
/* XXX what? */
}
timepos_t
AudioTrigger::current_length() const
{
if (_region) {
return timepos_t (data.length);
}
return timepos_t (Temporal::BeatTime);
}
timepos_t
AudioTrigger::natural_length() const
{
if (_region) {
return timepos_t::from_superclock (_region->length().magnitude());
}
return timepos_t (Temporal::BeatTime);
}
int
AudioTrigger::set_region_in_worker_thread (boost::shared_ptr<Region> r)
{
assert (!active());
boost::shared_ptr<AudioRegion> ar = boost::dynamic_pointer_cast<AudioRegion> (r);
if (r && !ar) {
return -1;
}
set_region_internal (r);
if (!r) {
/* unset */
return 0;
}
load_data (ar);
estimate_tempo (); //TODO: should first check if we already know this info from xml
_segment_tempo = _estimated_tempo;
setup_stretcher ();
/* Given what we know about the tempo and duration, set the defaults
* for the trigger properties.
*/
if (_segment_tempo == 0.) {
_stretchable = false;
_quantization = Temporal::BBT_Offset (-1, 0, 0);
_follow_action0 = FollowAction (FollowAction::None);
} else {
if (probably_oneshot()) {
/* short trigger, treat as a one shot */
_stretchable = false;
_follow_action0 = FollowAction (FollowAction::None);
_quantization = Temporal::BBT_Offset (-1, 0, 0);
} else {
_stretchable = true;
_quantization = Temporal::BBT_Offset (1, 0, 0);
_follow_action0 = FollowAction (FollowAction::Again);
}
}
_follow_action_probability = 0; /* 100% left */
PropertyChanged (ARDOUR::Properties::name);
return 0;
}
void
AudioTrigger::estimate_tempo ()
{
using namespace Temporal;
TempoMap::SharedPtr tm (TempoMap::use());
TimelineRange range (_region->start(), _region->start() + _region->length(), 0);
SegmentDescriptor segment;
bool have_segment;
have_segment = _region->source (0)->get_segment_descriptor (range, segment);
if (have_segment) {
_estimated_tempo = segment.tempo().quarter_notes_per_minute ();
_meter = segment.meter();
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1: tempo and meter from segment descriptor\n", index()));
} else {
/* not a great guess, but what else can we do? */
TempoMetric const & metric (tm->metric_at (timepos_t (AudioTime)));
_meter = metric.meter ();
/* check the name to see if there's a (heuristically obvious) hint
* about the tempo.
*/
string str = _region->name();
string::size_type bi;
string::size_type ni;
double text_tempo = -1.;
std::cerr << "Determine tempo for " << name() << std::endl;
if (((bi = str.find ("bpm")) != string::npos) ||
((bi = str.find ("BPM")) != string::npos)) {
string sub (str.substr (0, bi));
if ((ni = sub.find_last_of ("0123456789.,_-")) != string::npos) {
int nni = ni; /* ni is unsigned, nni is signed */
while (nni >= 0) {
if (!isdigit (sub[nni]) &&
(sub[nni] != '.') &&
(sub[nni] != ',')) {
break;
}
--nni;
}
if (nni > 0) {
std::stringstream p (sub.substr (nni + 1));
p >> text_tempo;
if (!p) {
text_tempo = -1.;
} else {
_estimated_tempo = text_tempo;
std::cerr << "from filename, tempo = " << _estimated_tempo << std::endl;
}
}
}
}
/* We don't have too many good choices here. Triggers can fire at any
* time, so there's no special place on the tempo map that we can use
* to get the meter from and thus compute an estimated bar count for
* this region. Our solution for now: just use the first meter.
*/
if (text_tempo < 0) {
breakfastquay::MiniBPM mbpm (_box.session().sample_rate());
mbpm.setBPMRange (metric.tempo().quarter_notes_per_minute () * 0.75, metric.tempo().quarter_notes_per_minute() * 1.5);
_estimated_tempo = mbpm.estimateTempoOfSamples (data[0], data.length);
if (_estimated_tempo == 0.0) {
/* no estimated tempo, just return since we'll use it as-is */
std::cerr << "Could not determine tempo for " << name() << std::endl;
return;
}
cerr << name() << " Estimated bpm " << _estimated_tempo << " from " << (double) data.length / _box.session().sample_rate() << " seconds\n";
}
if (!have_segment) {
segment.set_extent (_region->start_sample(), _region->length_samples());
}
segment.set_tempo (Temporal::Tempo (_estimated_tempo, 4));
for (auto & src : _region->sources()) {
src->set_segment_descriptor (segment);
}
}
const double seconds = (double) data.length / _box.session().sample_rate();
const double quarters = (seconds / 60.) * _estimated_tempo;
/* initialize bar count to match the file length ... user can later change this value */
_barcnt = quarters / _meter.divisions_per_bar();
/* initialize our follow_length to match the barcnt ... user can later change this value */
_follow_length = (Temporal::BBT_Offset( rint(_barcnt), 0, 0));
/* now check the determined tempo and force it to a value that gives us
an integer bar/quarter count. This is a heuristic that tries to
avoid clips that slightly over- or underrun a quantization point,
resulting in small or larger gaps in output if they are repeating.
*/
if ((_estimated_tempo != 0.) && (rint (_barcnt) != _barcnt)) {
/* fractional barcnt */
int intquarters = round (quarters);
double at = _estimated_tempo;
_estimated_tempo = intquarters / (seconds/60.);
DEBUG_TRACE (DEBUG::Triggers, string_compose ("adjusted barcnt of %1 and q = %2 to %3, old %4 new at = %5 seconds was %6\n", _barcnt, quarters, intquarters, at, _estimated_tempo, seconds));
}
/* use initial tempo in map (assumed for now to be the only one */
const samplecnt_t one_bar = tm->bbt_duration_at (timepos_t (AudioTime), BBT_Offset (1, 0, 0)).samples();
cerr << "estimated tempo: " << _estimated_tempo << endl;
cerr << "one bar in samples: " << one_bar << endl;
cerr << "barcnt = " << round (_barcnt) << endl;
}
bool
AudioTrigger::probably_oneshot () const
{
assert (_segment_tempo != 0.);
if ((data.length < (_box.session().sample_rate()/2)) ||
/* XXX use Meter here, not 4.0 */
((_barcnt < 1) && (data.length < (4.0 * ((_box.session().sample_rate() * 60) / _segment_tempo))))) {
std::cerr << "looks like a one-shot\n";
return true;
}
return false;
}
void
AudioTrigger::io_change ()
{
if (_stretcher) {
setup_stretcher ();
}
}
/* This exists so that we can play with the value easily. Currently, 1024 seems as good as any */
static const samplecnt_t rb_blocksize = 1024;
void
AudioTrigger::setup_stretcher ()
{
using namespace RubberBand;
using namespace Temporal;
if (!_region) {
return;
}
boost::shared_ptr<AudioRegion> ar (boost::dynamic_pointer_cast<AudioRegion> (_region));
const uint32_t nchans = std::min (_box.input_streams().n_audio(), ar->n_channels());
//map our internal enum to a rubberband option
RubberBandStretcher::Option ro;
switch (_stretch_mode) {
case Trigger::Crisp : ro = RubberBandStretcher::OptionTransientsCrisp; break;
case Trigger::Mixed : ro = RubberBandStretcher::OptionTransientsMixed; break;
case Trigger::Smooth : ro = RubberBandStretcher::OptionTransientsSmooth; break;
}
RubberBandStretcher::Options options = RubberBandStretcher::Option (RubberBandStretcher::OptionProcessRealTime |
ro);
delete _stretcher;
_stretcher = new RubberBandStretcher (_box.session().sample_rate(), nchans, options, 1.0, 1.0);
_stretcher->setMaxProcessSize (rb_blocksize);
}
void
AudioTrigger::drop_data ()
{
for (auto& d : data) {
delete [] d;
}
data.clear ();
}
int
AudioTrigger::load_data (boost::shared_ptr<AudioRegion> ar)
{
const uint32_t nchans = ar->n_channels();
data.length = ar->length_samples();
drop_data ();
try {
for (uint32_t n = 0; n < nchans; ++n) {
data.push_back (new Sample[data.length]);
ar->read (data[n], 0, data.length, n);
}
set_name (ar->name());
} catch (...) {
drop_data ();
return -1;
}
return 0;
}
void
AudioTrigger::retrigger ()
{
read_index = _start_offset + _legato_offset;
process_index = 0;
retrieved = 0;
_legato_offset = 0; /* used one time only */
_stretcher->reset ();
got_stretcher_padding = false;
to_pad = 0;
to_drop = 0;
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 retriggered to %2\n", _index, read_index));
}
pframes_t
AudioTrigger::run (BufferSet& bufs, samplepos_t start_sample, samplepos_t end_sample, Temporal::Beats const & start, Temporal::Beats const & end,
pframes_t nframes, pframes_t dest_offset, bool passthru, double bpm, bool /* can_clear */)
{
boost::shared_ptr<AudioRegion> ar = boost::dynamic_pointer_cast<AudioRegion>(_region);
/* We do not modify the I/O of our parent route, so we process only min (bufs.n_audio(),region.channels()) */
const uint32_t nchans = std::min (bufs.count().n_audio(), ar->n_channels());
int avail = 0;
BufferSet& scratch (_box.session().get_scratch_buffers (ChanCount (DataType::AUDIO, nchans)));
std::vector<Sample*> bufp(nchans);
const bool do_stretch = stretching();
2021-08-07 20:53:49 -04:00
/* see if we're going to start or stop or retrigger in this run() call */
maybe_compute_next_transition (start_sample, start, end, nframes, dest_offset, passthru);
const pframes_t orig_nframes = nframes;
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 after checking for transition, state = %2, will stretch %3, nf will be %4\n", name(), enum_2_string (_state), do_stretch, nframes));
switch (_state) {
case Stopped:
case WaitingForRetrigger:
case WaitingToStart:
/* did everything we could do */
return nframes;
case Running:
case Playout:
case WaitingToStop:
case Stopping:
/* stuff to do */
break;
}
/* We use session scratch buffers for both padding the start of the
* input to RubberBand, and to hold the output. Because of this dual
* purpose, we use a generic variable name ('bufp') to refer to them.
*/
for (uint32_t chn = 0; chn < bufs.count().n_audio(); ++chn) {
bufp[chn] = scratch.get_audio (chn).data();
}
/* tell the stretcher what we are doing for this ::run() call */
if (do_stretch && _state != Playout) {
const double stretch = _segment_tempo / bpm;
_stretcher->setTimeRatio (stretch);
DEBUG_TRACE (DEBUG::Triggers, string_compose ("clip tempo %1 bpm %2 ratio %3%4\n", _segment_tempo, bpm, std::setprecision (6), stretch));
if ((avail = _stretcher->available()) < 0) {
error << _("Could not configure rubberband stretcher") << endmsg;
return 0;
}
/* We are using Rubberband in realtime mode, but this mdoe of
* operation has some issues. The first is that it will
* generate a certain number of samples of output at the start
* that are not based on the input, due to processing latency.
*
* In this context, we don't care about this output, because we
* have all the data available from the outset, and we can just
* wait until this "latency" period is over. So we will feed
* an initial chunk of data to the stretcher, and then throw
* away the corresponding data on the output.
*
* This code is modelled on the code for rubberband(1), part of
* the rubberband software.
*/
if (!got_stretcher_padding) {
to_pad = _stretcher->getLatency();
to_drop = to_pad;
got_stretcher_padding = true;
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 requires %2 padding %3\n", name(), to_pad));
}
while (to_pad > 0) {
const samplecnt_t limit = std::min ((samplecnt_t) scratch.get_audio (0).capacity(), to_pad);
for (uint32_t chn = 0; chn < nchans; ++chn) {
for (samplecnt_t n = 0; n < limit; ++n) {
bufp[chn][n] = 0.f;
}
}
_stretcher->process (&bufp[0], limit, false);
to_pad -= limit;
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 padded %2 left %3\n", name(), limit, to_pad));
}
}
while (nframes && (_state != Playout)) {
pframes_t to_stretcher;
pframes_t from_stretcher;
if (do_stretch) {
if (read_index < last_sample) {
/* still have data to push into the stretcher */
to_stretcher = (pframes_t) std::min (samplecnt_t (rb_blocksize), (last_sample - read_index));
const bool at_end = (to_stretcher < rb_blocksize);
while ((pframes_t) avail < nframes && (read_index < last_sample)) {
/* keep feeding the stretcher in chunks of "to_stretcher",
* until there's nframes of data available, or we reach
* the end of the region
*/
std::vector<Sample*> in(nchans);
for (uint32_t chn = 0; chn < nchans; ++chn) {
in[chn] = data[chn] + read_index;
}
/* Note: RubberBandStretcher's process() and retrieve() API's accepts Sample**
* as their first argument. This code may appear to only be processing the first
* channel, but actually processes them all in one pass.
*/
_stretcher->process (&in[0], to_stretcher, at_end);
read_index += to_stretcher;
avail = _stretcher->available ();
if (to_drop && avail) {
samplecnt_t this_drop = std::min (std::min ((samplecnt_t) avail, to_drop), (samplecnt_t) scratch.get_audio (0).capacity());
_stretcher->retrieve (&bufp[0], this_drop);
to_drop -= this_drop;
avail = _stretcher->available ();
}
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 process %2 at-end %3 avail %4 of %5\n", name(), to_stretcher, at_end, avail, nframes));
}
/* we've fed the stretcher enough data to have
* (at least) nframes of output available.
*/
from_stretcher = nframes;
} else {
/* finished delivering data to stretcher, but may have not yet retrieved it all */
avail = _stretcher->available ();
from_stretcher = (pframes_t) std::min ((pframes_t) nframes, (pframes_t) avail);
}
/* fetch the stretch */
retrieved += _stretcher->retrieve (&bufp[0], from_stretcher);
if (read_index >= last_sample) {
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 no more data to deliver to stretcher, but retrieved %2 to put current end at %3 vs %4 / %5 pi %6\n",
index(), retrieved, transition_samples + retrieved, expected_end_sample, final_sample, process_index));
if (transition_samples + retrieved > expected_end_sample) {
/* final pull from stretched data into output buffers */
from_stretcher = std::min ((samplecnt_t) from_stretcher, expected_end_sample - process_index);
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 total retrieved data %2 exceeds theoretical size %3, truncate from_stretcher to %4\n",
index(), retrieved, expected_end_sample - transition_samples, from_stretcher));
if (from_stretcher == 0) {
if (process_index < final_sample) {
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 reached (EX) end, entering playout mode to cover %2 .. %3\n", index(), process_index, final_sample));
_state = Playout;
} else {
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 reached (EX) end, now stopped, retrieved %2, avail %3 pi %4 vs fs %5\n", index(), retrieved, avail, process_index, final_sample));
_state = Stopped;
_loop_cnt++;
}
break;
}
}
}
} else {
/* no stretch */
from_stretcher = (pframes_t) std::min ((samplecnt_t) nframes, (last_sample - read_index));
}
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 ready with %2 ri %3 ls %4, will write %5\n", name(), avail, read_index, last_sample, from_stretcher));
/* deliver to buffers */
for (uint32_t chn = 0; chn < bufs.count().n_audio(); ++chn) {
uint32_t channel = chn % data.size();
2021-12-17 11:50:50 -05:00
AudioBuffer& buf (bufs.get_audio (chn));
Sample* src = do_stretch ? bufp[channel] : (data[channel] + read_index);
gain_t gain = _velocity_gain * _gain; //incorporate the gain from velocity_effect
if (!passthru) {
buf.read_from (src, from_stretcher, dest_offset);
if (gain != 1.0f) {
buf.apply_gain (gain, from_stretcher);
}
} else {
if (gain != 1.0f) {
buf.accumulate_with_gain_from (src, from_stretcher, gain, dest_offset);
} else {
2021-12-17 11:50:50 -05:00
buf.accumulate_from (src, from_stretcher, dest_offset);
}
}
}
process_index += from_stretcher;
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 pi grew by %2 to %3\n", index(), from_stretcher, process_index));
/* Move read_index, in the case that we are not using a
* stretcher
*/
if (!do_stretch) {
read_index += from_stretcher;
}
nframes -= from_stretcher;
avail = _stretcher->available ();
dest_offset += from_stretcher;
2021-10-07 00:40:37 -04:00
if (read_index >= last_sample && (!do_stretch || avail <= 0)) {
if (process_index < final_sample) {
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 reached end, entering playout mode to cover %2 .. %3\n", index(), process_index, final_sample));
_state = Playout;
} else {
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 reached end, now stopped, retrieved %2, avail %3\n", index(), retrieved, avail));
_state = Stopped;
_loop_cnt++;
}
break;
}
}
pframes_t covered_frames = orig_nframes - nframes;
if (_state == Playout) {
if (nframes != orig_nframes) {
/* we've already taken dest_offset into account, it plays no
role in a "playout" during the same ::run() call
*/
dest_offset = 0;
}
const pframes_t remaining_frames_for_run= orig_nframes - covered_frames;
const pframes_t remaining_frames_till_final = final_sample - process_index;
const pframes_t to_fill = std::min (remaining_frames_till_final, remaining_frames_for_run);
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 playout mode, remaining in run %2 till final %3 @ %5 ts %7 vs pi @ %6 to fill %4\n",
index(), remaining_frames_for_run, remaining_frames_till_final, to_fill, final_sample, process_index, transition_samples));
if (remaining_frames_till_final != 0) {
if (!passthru) {
for (uint32_t chn = 0; chn < bufs.count().n_audio(); ++chn) {
AudioBuffer& buf (bufs.get_audio (chn));
buf.silence (to_fill, covered_frames + dest_offset);
}
}
process_index += to_fill;
covered_frames += to_fill;
if (process_index < final_sample) {
/* more playout to be done */
return covered_frames;
}
}
_state = Stopped;
_loop_cnt++;
}
if (_state == Stopped || _state == Stopping) {
/* note: neither argument is used in the audio case */
when_stopped_during_run (bufs, dest_offset);
}
return covered_frames;
}
void
AudioTrigger::reload (BufferSet&, void*)
{
}
/*--------------------*/
MIDITrigger::MIDITrigger (uint32_t n, TriggerBox& b)
: Trigger (n, b)
, data_length (Temporal::Beats())
, last_event_beats (Temporal::Beats())
, _start_offset (0, 0, 0)
, _legato_offset (0, 0, 0)
{
}
MIDITrigger::~MIDITrigger ()
{
}
bool
MIDITrigger::probably_oneshot () const
{
/* XXX fix for short chord stabs */
return false;
}
void
MIDITrigger::set_expected_end_sample (Temporal::TempoMap::SharedPtr const & tmap, Temporal::BBT_Time const & transition_bbt, samplepos_t)
{
Temporal::Beats end_by_follow_length = tmap->quarters_at (tmap->bbt_walk (transition_bbt, _follow_length));
Temporal::Beats end_by_data_length = transition_beats + data_length;
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 ends: FL %2 DL %3 tbbt %4 fl %5\n", index(), end_by_follow_length, end_by_data_length, transition_bbt, _follow_length));
Temporal::Beats usable_length;
if (internal_use_follow_length() && (end_by_follow_length < end_by_data_length)) {
usable_length = tmap->quarters_at (tmap->bbt_walk (transition_bbt, _follow_length)) - transition_beats;
} else {
usable_length = data_length;
}
Temporal::BBT_Offset q (_quantization);
if (launch_style() != Repeat || (q == Temporal::BBT_Offset())) {
if (internal_use_follow_length()) {
final_beat = end_by_follow_length;
} else {
final_beat = end_by_data_length;
}
} else {
/* XXX MUST HANDLE BAR-LEVEL QUANTIZATION */
timecnt_t len (Temporal::Beats (q.beats, q.ticks), timepos_t (Temporal::Beats()));
final_beat = len.beats ();
}
}
2021-12-12 13:20:58 -05:00
SegmentDescriptor
MIDITrigger::get_segment_descriptor () const
{
SegmentDescriptor sd;
boost::shared_ptr<MidiRegion> mr = boost::dynamic_pointer_cast<MidiRegion> (_region);
assert (mr);
sd.set_extent (Temporal::Beats(), mr->length().beats());
/* we don't really have tempo information for MIDI yet */
sd.set_tempo (Temporal::Tempo (120, 4));
return sd;
}
void
MIDITrigger::_startup (Temporal::BBT_Offset const & start_quantization)
{
Trigger::_startup (start_quantization);
}
void
MIDITrigger::jump_start ()
{
Trigger::jump_start ();
retrigger ();
}
void
MIDITrigger::shutdown (BufferSet& bufs, pframes_t dest_offset)
{
Trigger::shutdown (bufs, dest_offset);
MidiBuffer& mb (bufs.get_midi (0));
tracker.resolve_notes (mb, dest_offset);
}
void
MIDITrigger::jump_stop (BufferSet& bufs, pframes_t dest_offset)
{
Trigger::jump_stop (bufs, dest_offset);
MidiBuffer& mb (bufs.get_midi (0));
tracker.resolve_notes (mb, dest_offset);
retrigger ();
}
double
MIDITrigger::position_as_fraction () const
{
if (!active()) {
return 0.0;
}
Temporal::DoubleableBeats db (last_event_beats);
double dl = db.to_double ();
double dr = data_length.to_double ();
2021-10-29 18:22:55 -04:00
return dl / dr;
}
XMLNode&
MIDITrigger::get_state (void)
{
XMLNode& node (Trigger::get_state());
node.set_property (X_("start"), start_offset());
return node;
}
int
MIDITrigger::set_state (const XMLNode& node, int version)
{
timepos_t t;
if (!Trigger::set_state (node, version)) {
return -1;
}
node.get_property (X_("start"), t);
Temporal::Beats b (t.beats());
/* XXX need to deal with bar offsets */
_start_offset = Temporal::BBT_Offset (0, b.get_beats(), b.get_ticks());
return 0;
}
void
MIDITrigger::set_start (timepos_t const & s)
{
/* XXX need to handle bar offsets */
Temporal::Beats b (s.beats());
_start_offset = Temporal::BBT_Offset (0, b.get_beats(), b.get_ticks());
}
void
MIDITrigger::set_end (timepos_t const & e)
{
/* XXX need to handle bar offsets */
set_length (timecnt_t (e.beats() - Temporal::Beats (_start_offset.beats, _start_offset.ticks), start_offset()));
}
void
MIDITrigger::set_legato_offset (timepos_t const & offset)
{
/* XXX need to handle bar offsets */
Temporal::Beats b (offset.beats());
_legato_offset = Temporal::BBT_Offset (0, b.get_beats(), b.get_ticks());
}
timepos_t
MIDITrigger::start_offset () const
{
/* XXX single meter assumption */
Temporal::Meter const &m = Temporal::TempoMap::use()->meter_at (Temporal::Beats (0, 0));
return timepos_t (m.to_quarters (_start_offset));
}
timepos_t
MIDITrigger::current_pos() const
{
return timepos_t (last_event_beats);
}
void
MIDITrigger::set_length (timecnt_t const & newlen)
{
}
timepos_t
MIDITrigger::current_length() const
{
if (_region) {
return timepos_t (data_length);
}
return timepos_t (Temporal::BeatTime);
}
timepos_t
MIDITrigger::natural_length() const
{
if (_region) {
return timepos_t::from_ticks (_region->length().magnitude());
}
return timepos_t (Temporal::BeatTime);
}
int
MIDITrigger::set_region_in_worker_thread (boost::shared_ptr<Region> r)
{
boost::shared_ptr<MidiRegion> mr = boost::dynamic_pointer_cast<MidiRegion> (r);
if (!mr) {
return -1;
}
set_region_internal (r);
set_name (mr->name());
data_length = mr->length().beats();
set_length (mr->length());
model = mr->model ();
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 loaded midi region, span is %2\n", name(), data_length));
PropertyChanged (ARDOUR::Properties::name);
return 0;
}
void
MIDITrigger::retrigger ()
{
/* XXX need to deal with bar offsets */
// const Temporal::BBT_Offset o = _start_offset + _legato_offset;
iter = model->begin();
_legato_offset = Temporal::BBT_Offset ();
last_event_beats = Temporal::Beats();
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 retriggered to %2, ts = %3\n", _index, iter->time(), transition_beats));
}
void
MIDITrigger::reload (BufferSet&, void*)
{
}
pframes_t
MIDITrigger::run (BufferSet& bufs, samplepos_t start_sample, samplepos_t end_sample,
Temporal::Beats const & start_beats, Temporal::Beats const & end_beats,
pframes_t nframes, pframes_t dest_offset, bool passthru, double bpm, bool can_clear)
{
MidiBuffer& mb (bufs.get_midi (0));
typedef Evoral::Event<MidiModel::TimeType> MidiEvent;
const timepos_t region_start_time = _region->start();
const Temporal::Beats region_start = region_start_time.beats();
Temporal::TempoMap::SharedPtr tmap (Temporal::TempoMap::use());
samplepos_t last_event_samples = max_samplepos;
/* see if we're going to start or stop or retrigger in this run() call */
maybe_compute_next_transition (start_sample, start_beats, end_beats, nframes, dest_offset, passthru);
const pframes_t orig_nframes = nframes;
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 after checking for transition, state = %2\n", name(), enum_2_string (_state)));
switch (_state) {
case Stopped:
case WaitingForRetrigger:
case WaitingToStart:
return nframes;
case Running:
case Playout:
case WaitingToStop:
case Stopping:
break;
}
if (!passthru && can_clear) {
/* XXX MidiBuffer::silence() doesn't work correctly - it does
the same thing as MidiBuffer::clear(). It needs to be fixed
so that we can just clear out events in the range we're
processing. Until that is done, @param can_clear controls
whether or not we can clear the whole buffer, and is set
true for the first ::run() call per process cycle.
*/
mb.clear ();
}
Temporal::Beats last_event_timeline_beats;
while (iter != model->end() && _state != Playout) {
MidiEvent const & event (*iter);
/* Event times are in beats, relative to start of source
* file. We need to convert to region-relative time, and then
* a session timeline time, which is defined by the time at
* which we last transitioned (in this case, to being active)
*/
const Temporal::Beats maybe_last_event_timeline_beats = transition_beats + (event.time() - region_start);
// std::cerr << "considering " << event << " with sb " << start_beats << " eb " << end_beats << " tb " << maybe_last_event_timeline_beats << " fb " << final_beat << endl;
if (maybe_last_event_timeline_beats >= final_beat) {
/* do this to "fake" having reached the end */
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 tlrr %2 >- fb %3, so at end\n", index(), maybe_last_event_timeline_beats, final_beat));
iter = model->end();
break;
}
/* Now get samples */
const samplepos_t timeline_samples = tmap->sample_at (maybe_last_event_timeline_beats);
if (timeline_samples >= end_sample) {
break;
}
2021-10-29 18:22:55 -04:00
/* Now we have to convert to a position within the buffer we
* are writing to.
*
2021-12-29 11:37:43 -05:00
* start_sample has already been been adjusted to reflect a
* previous Trigger's processing during this run cycle, so we
* can ignore dest_offset (which is necessary for audio
* triggers where the data is a continuous data stream, but not
* required here).
*/
2021-10-29 18:22:55 -04:00
samplepos_t buffer_samples = timeline_samples - start_sample;
2021-10-29 18:22:55 -04:00
const Evoral::Event<MidiBuffer::TimeType> ev (Evoral::MIDI_EVENT, buffer_samples, event.size(), const_cast<uint8_t*>(event.buffer()), false);
DEBUG_TRACE (DEBUG::Triggers, string_compose ("given et %1 TS %7 rs %8 ts %2 bs %3 ss %4 do %5, inserting %6\n", maybe_last_event_timeline_beats, timeline_samples, buffer_samples, start_sample, dest_offset, ev, transition_beats, region_start));
mb.insert_event (ev);
tracker.track (event.buffer());
last_event_beats = event.time();
last_event_timeline_beats = maybe_last_event_timeline_beats;
last_event_samples = timeline_samples;
++iter;
}
if (_state == Stopping) {
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 was stopping, now stopped\n", index()));
tracker.resolve_notes (mb, nframes-1);
}
if (iter == model->end()) {
/* We reached the end */
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 reached end, leb %2 les %3 fb %4 dl %5\n", index(), last_event_timeline_beats, last_event_samples, final_beat, data_length));
if (last_event_timeline_beats < final_beat) {
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 entering playout because ... leb %2 < fb %3\n", index(), last_event_timeline_beats, final_beat));
if (_state != Playout) {
_state = Playout;
}
if (_state == Playout) {
if (final_beat > end_beats) {
/* not finished with playout yet, all frames covered */
nframes = 0;
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 not done with playout, all frames covered\n", index()));
} else {
/* finishing up playout */
samplepos_t final_sample = tmap->sample_at (timepos_t (final_beat));
nframes = orig_nframes - (final_sample - start_sample);
_loop_cnt++;
_state = Stopped;
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 playout done, nf = %2 fb %3 fs %4 %5\n", index(), nframes, final_beat, final_sample, start_sample));
}
}
} else {
_loop_cnt++;
_state = Stopped;
/* the time we processed spans from start to the last event */
if (last_event_samples != max_samplepos) {
nframes = (last_event_samples - start_sample);
} else {
/* all frames covered */
nframes = 0;
}
}
} else {
/* we didn't reach the end of the MIDI data, ergo we covered
the entire timespan passed into us.
*/
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 did not reach end, nframes left at %2\n", index(), nframes));
nframes = 0;
}
const samplecnt_t covered_frames = orig_nframes - nframes;
if (_state == Stopped || _state == Stopping) {
when_stopped_during_run (bufs, dest_offset + orig_nframes - 1);
}
return covered_frames;
}
/**************/
void
Trigger::make_property_quarks ()
{
Properties::running.property_id = g_quark_from_static_string (X_("running"));
DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for running = %1\n", Properties::running.property_id));
Properties::passthru.property_id = g_quark_from_static_string (X_("passthru"));
DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for passthru = %1\n", Properties::passthru.property_id));
Properties::follow_count.property_id = g_quark_from_static_string (X_("follow-count"));
DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for follow_count = %1\n", Properties::follow_count.property_id));
Properties::use_follow_length.property_id = g_quark_from_static_string (X_("use-follow-length"));
DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for use_follow_length = %1\n", Properties::use_follow_length.property_id));
Properties::follow_length.property_id = g_quark_from_static_string (X_("follow-length"));
DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for follow_length = %1\n", Properties::follow_length.property_id));
Properties::legato.property_id = g_quark_from_static_string (X_("legato"));
DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for legato = %1\n", Properties::legato.property_id));
Properties::velocity_effect.property_id = g_quark_from_static_string (X_("velocity-effect"));
DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for velocity_effect = %1\n", Properties::velocity_effect.property_id));
Properties::follow_action_probability.property_id = g_quark_from_static_string (X_("follow-action-probability"));
DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for follow_action_probability = %1\n", Properties::follow_action_probability.property_id));
Properties::quantization.property_id = g_quark_from_static_string (X_("quantization"));
DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for quantization = %1\n", Properties::quantization.property_id));
2021-10-01 20:39:06 -04:00
Properties::launch_style.property_id = g_quark_from_static_string (X_("launch-style"));
DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for quantization = %1\n", Properties::launch_style.property_id));
Properties::follow_action0.property_id = g_quark_from_static_string (X_("follow-action-0"));
DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for follow-action-0 = %1\n", Properties::follow_action0.property_id));
Properties::follow_action1.property_id = g_quark_from_static_string (X_("follow-action-1"));
DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for follow-action-1 = %1\n", Properties::follow_action1.property_id));
Properties::gain.property_id = g_quark_from_static_string (X_("gain"));
DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for gain = %1\n", Properties::gain.property_id));
Properties::stretchable.property_id = g_quark_from_static_string (X_("stretchable"));
DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for stretchable = %1\n", Properties::stretchable.property_id));
Properties::isolated.property_id = g_quark_from_static_string (X_("isolated"));
DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for isolated = %1\n", Properties::isolated.property_id));
Properties::stretch_mode.property_id = g_quark_from_static_string (X_("stretch_mode"));
DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for stretch_mode = %1\n", Properties::stretch_mode.property_id));
}
Temporal::BBT_Offset TriggerBox::_assumed_trigger_duration (4, 0, 0);
//TriggerBox::TriggerMidiMapMode TriggerBox::_midi_map_mode (TriggerBox::AbletonPush);
TriggerBox::TriggerMidiMapMode TriggerBox::_midi_map_mode (TriggerBox::SequentialNote);
int TriggerBox::_first_midi_note = 60;
std::atomic<int> TriggerBox::active_trigger_boxes (0);
TriggerBoxThread* TriggerBox::worker = 0;
CueRecords TriggerBox::cue_records (256);
std::atomic<bool> TriggerBox::_cue_recording (false);
PBD::Signal0<void> TriggerBox::CueRecordingChanged;
void
TriggerBox::init ()
{
worker = new TriggerBoxThread;
TriggerBoxThread::init_request_pool ();
init_pool ();
}
TriggerBox::TriggerBox (Session& s, DataType dt)
: Processor (s, _("TriggerBox"), Temporal::BeatTime)
, _data_type (dt)
, _order (-1)
, explicit_queue (64)
, _currently_playing (0)
, _stop_all (false)
, _pass_thru (false)
, _active_scene (-1)
, requests (1024)
{
set_display_to_user (false);
/* default number of possible triggers. call ::add_trigger() to increase */
if (_data_type == DataType::AUDIO) {
for (uint32_t n = 0; n < default_triggers_per_box; ++n) {
all_triggers.push_back (boost::make_shared<AudioTrigger> (n, *this));
}
} else {
for (uint32_t n = 0; n < default_triggers_per_box; ++n) {
all_triggers.push_back (boost::make_shared<MIDITrigger> (n, *this));
}
}
while (pending.size() < all_triggers.size()) {
pending.push_back (std::atomic<Trigger*>(0));
}
Config->ParameterChanged.connect_same_thread (*this, boost::bind (&TriggerBox::parameter_changed, this, _1));
}
void
TriggerBox::set_cue_recording (bool yn)
{
if (yn != _cue_recording) {
_cue_recording = yn;
CueRecordingChanged ();
}
}
void
TriggerBox::set_region (uint32_t slot, boost::shared_ptr<Region> region)
{
/* This is called from our worker thread */
Trigger* t;
switch (_data_type) {
case DataType::AUDIO:
t = new AudioTrigger (slot, *this);
break;
case DataType::MIDI:
t = new MIDITrigger (slot, *this);
break;
default:
return;
}
t->set_region_in_worker_thread (region);
/* XXX what happens if pending is already set? */
set_pending (slot, t);
}
void
TriggerBox::set_pending (uint32_t slot, Trigger* t)
{
all_triggers[slot]->set_pending (t);
}
void
TriggerBox::maybe_swap_pending (uint32_t slot)
{
/* This is called synchronously with process() (i.e. in an RT process
thread) and so it is impossible for any Triggers in this TriggerBox
to be invoked while this executes.
*/
Trigger* p = 0;
p = all_triggers[slot]->swap_pending (p);
if (p) {
if (p == Trigger::MagicClearPointerValue) {
all_triggers[slot]->clear_region ();
} else {
/* Note use of a custom delete function. We cannot
delete the old trigger from the RT context where the
trigger swap will happen, so we will ask the trigger
helper thread to take care of it.
*/
all_triggers[slot].reset (p, Trigger::request_trigger_delete);
TriggerSwapped (slot); /* EMIT SIGNAL */
}
}
}
void
TriggerBox::set_order (int32_t n)
{
_order = n;
}
void
TriggerBox::queue_explict (uint32_t n)
{
assert (n < all_triggers.size());
explicit_queue.write (&n, 1);
DEBUG_TRACE (DEBUG::Triggers, string_compose ("explicit queue %1, EQ = %2\n", n, explicit_queue.read_space()));
if (_currently_playing) {
_currently_playing->unbang ();
}
}
TriggerPtr
TriggerBox::get_next_trigger ()
{
uint32_t n;
if (explicit_queue.read (&n, 1) == 1) {
TriggerPtr r = trigger (n);
DEBUG_TRACE (DEBUG::Triggers, string_compose ("next trigger from explicit queue = %1\n", r->index()));
return r;
}
return 0;
}
void
TriggerBox::set_from_selection (uint32_t slot, boost::shared_ptr<Region> region)
{
DEBUG_TRACE (DEBUG::Triggers, string_compose ("load %1 into %2\n", region->name(), slot));
if (slot >= all_triggers.size()) {
return;
}
all_triggers[slot]->set_region (region);
}
void
TriggerBox::set_from_path (uint32_t slot, std::string const & path)
{
if (slot >= all_triggers.size()) {
return;
}
const DataType source_type = SMFSource::safe_midi_file_extension (path) ? DataType::MIDI : DataType::AUDIO;
if (source_type != _data_type) {
error << string_compose (_("Cannot use %1 files in %2 slots"),
((source_type == DataType::MIDI) ? "MIDI" : "audio"),
((source_type == DataType::MIDI) ? "audio" : "MIDI")) << endmsg;
return;
}
try {
ImportStatus status;
status.total = 1;
status.quality = SrcBest;
status.freeze = false;
status.paths.push_back (path);
status.replace_existing_source = false;
status.split_midi_channels = false;
status.midi_track_name_source = ARDOUR::SMFTrackNumber;
_session.import_files (status);
if (status.cancel) {
error << string_compose (_("Cannot create source from %1"), path) << endmsg;
return;
}
if (status.sources.empty()) {
error << string_compose (_("Could not create source from %1"), path) << endmsg;
return;
}
SourceList src_list;
for (auto& src : status.sources) {
src_list.push_back (src);
}
PropertyList plist;
plist.add (Properties::start, 0);
plist.add (Properties::length, src_list.front()->length ());
plist.add (Properties::name, basename_nosuffix (path));
plist.add (Properties::layer, 0);
plist.add (Properties::layering_index, 0);
boost::shared_ptr<Region> the_region (RegionFactory::create (src_list, plist, true));
all_triggers[slot]->set_region (the_region);
} catch (std::exception& e) {
cerr << "loading sample from " << path << " failed: " << e.what() << endl;
return;
}
}
TriggerBox::~TriggerBox ()
{
}
void
TriggerBox::stop_all_immediately ()
{
_requests.stop_all = true;
}
void
TriggerBox::clear_all_triggers ()
{
for (uint64_t n = 0; n < all_triggers.size(); ++n) {
all_triggers[n]->set_region (boost::shared_ptr<Region>());
}
}
void
TriggerBox::set_all_launch_style (ARDOUR::Trigger::LaunchStyle ls)
{
for (uint64_t n = 0; n < all_triggers.size(); ++n) {
all_triggers[n]->set_launch_style (ls);
}
}
void
TriggerBox::set_all_follow_action (ARDOUR::FollowAction const & fa, uint32_t fa_n)
{
for (uint64_t n = 0; n < all_triggers.size(); ++n) {
all_triggers[n]->set_follow_action (fa, fa_n);
}
}
void
TriggerBox::set_all_probability (int zero_to_hundred)
{
for (uint64_t n = 0; n < all_triggers.size(); ++n) {
all_triggers[n]->set_follow_action_probability (zero_to_hundred);
}
}
void
TriggerBox::set_all_quantization (Temporal::BBT_Offset const& q)
{
for (uint64_t n = 0; n < all_triggers.size(); ++n) {
all_triggers[n]->set_quantization (q);
}
}
void
TriggerBox::stop_all ()
{
/* Stops all triggers as soon as possible */
/* XXX needs to be done with mutex or via thread-safe queue */
DEBUG_TRACE (DEBUG::Triggers, "stop-all request received\n");
for (uint32_t n = 0; n < all_triggers.size(); ++n) {
all_triggers[n]->request_stop ();
}
_stop_all = true;
explicit_queue.reset ();
}
void
TriggerBox::stop_all_quantized ()
{
for (uint32_t n = 0; n < all_triggers.size(); ++n) {
all_triggers[n]->unbang ();
}
}
void
TriggerBox::drop_triggers ()
{
Glib::Threads::RWLock::WriterLock lm (trigger_lock);
all_triggers.clear ();
}
TriggerPtr
TriggerBox::trigger (Triggers::size_type n)
{
Glib::Threads::RWLock::ReaderLock lm (trigger_lock);
if (n >= all_triggers.size()) {
return 0;
}
return all_triggers[n];
}
void
TriggerBox::add_midi_sidechain ()
{
assert (owner());
if (!_sidechain) {
_sidechain.reset (new SideChain (_session, string_compose ("%1/%2", owner()->name(), name ())));
_sidechain->activate ();
_sidechain->input()->add_port ("", owner(), DataType::MIDI); // add a port, don't connect.
boost::shared_ptr<Port> p = _sidechain->input()->nth (0);
if (p) {
p->connect (Config->get_default_trigger_input_port());
} else {
error << _("Could not create port for trigger side-chain") << endmsg;
}
}
}
void
TriggerBox::update_sidechain_name ()
{
if (!_sidechain) {
return;
}
assert (owner());
_sidechain->set_name (string_compose ("%1/%2", owner()->name(), name ()));
}
bool
TriggerBox::can_support_io_configuration (const ChanCount& in, ChanCount& out)
{
/* if this is an audio trigger, let it be known that we have at least 1 audio output.
*/
if (_data_type == DataType::AUDIO) {
out.set_audio (std::max (in.n_audio(), 1U));
}
/* if this is a MIDI trigger, let it be known that we have at least 1 MIDI output.
*/
if (_data_type == DataType::MIDI) {
out.set_midi (std::max (in.n_midi(), 1U));
}
return true;
}
bool
TriggerBox::configure_io (ChanCount in, ChanCount out)
{
if (_sidechain) {
_sidechain->configure_io (in, out);
}
bool ret = Processor::configure_io (in, out);
if (ret) {
for (uint32_t n = 0; n < all_triggers.size(); ++n) {
all_triggers[n]->io_change ();
}
}
return ret;
}
void
TriggerBox::add_trigger (TriggerPtr trigger)
{
Glib::Threads::RWLock::WriterLock lm (trigger_lock);
all_triggers.push_back (trigger);
}
void
TriggerBox::set_midi_map_mode (TriggerMidiMapMode m)
{
_midi_map_mode = m;
}
void
TriggerBox::set_first_midi_note (int n)
{
_first_midi_note = n;
}
int
TriggerBox::note_to_trigger (int midi_note, int channel)
{
const int column = _order;
int first_note;
int top;
switch (_midi_map_mode) {
case AbletonPush:
/* the top row of pads generate MIDI note 92, 93, 94 and so on.
Each lower row generates notes 8 below the one above it.
*/
top = 92 + column;
for (int row = 0; row < 8; ++row) {
if (midi_note == top - (row * 8)) {
return row;
}
}
return -1;
break;
case SequentialNote:
first_note = _first_midi_note + (column * all_triggers.size());
return midi_note - first_note; /* direct access to row */
case ByMidiChannel:
first_note = 3;
break;
default:
break;
}
return midi_note;
}
void
TriggerBox::process_midi_trigger_requests (BufferSet& bufs)
{
/* check MIDI port input buffers for triggers */
for (BufferSet::midi_iterator mi = bufs.midi_begin(); mi != bufs.midi_end(); ++mi) {
MidiBuffer& mb (*mi);
for (MidiBuffer::iterator ev = mb.begin(); ev != mb.end(); ++ev) {
if (!(*ev).is_note()) {
continue;
}
int trigger_number = note_to_trigger ((*ev).note(), (*ev).channel());
DEBUG_TRACE (DEBUG::Triggers, string_compose ("note %1 received on %2, translated to trigger num %3\n", (int) (*ev).note(), (int) (*ev).channel(), trigger_number));
if (trigger_number < 0) {
/* not for us */
continue;
}
if (trigger_number >= (int) all_triggers.size()) {
continue;
}
TriggerPtr t = all_triggers[trigger_number];
if (!t) {
continue;
}
if ((*ev).is_note_on()) {
if (t->midi_velocity_effect() != 0.0) {
/* if MVE is zero, MIDI velocity has no
impact on gain. If it is small, it
has a small effect on gain. As it
approaches 1.0, it has full control
over the trigger gain.
*/
t->set_velocity_gain (1.0 - (t->midi_velocity_effect() * (*ev).velocity() / 127.f));
}
t->bang ();
} else if ((*ev).is_note_off()) {
t->unbang ();
}
}
}
}
void
TriggerBox::set_pass_thru (bool yn)
{
_requests.pass_thru = yn;
PropertyChanged (Properties::passthru);
2022-01-16 11:56:45 -05:00
session().set_dirty();
}
void
TriggerBox::run (BufferSet& bufs, samplepos_t start_sample, samplepos_t end_sample, double speed, pframes_t nframes, bool result_required)
{
/* XXX a test to check if we have no usable slots would be good
here. if so, we can just return.
*/
/* STEP ONE: are we actually active? */
if (!check_active()) {
return;
2021-12-03 07:52:51 -05:00
}
2021-10-11 10:50:58 -04:00
#ifndef NDEBUG
{
Temporal::TempoMap::SharedPtr __tmap (Temporal::TempoMap::use());
const Temporal::Beats __start_beats (timepos_t (start_sample).beats());
const Temporal::Beats __end_beats (timepos_t (end_sample).beats());
const double __bpm = __tmap->quarters_per_minute_at (timepos_t (__start_beats));
DEBUG_TRACE (DEBUG::Triggers, string_compose ("**** Triggerbox::run() for %6, ss %1 es %2 sb %3 eb %4 bpm %5\n", start_sample, end_sample, __start_beats, __end_beats, __bpm, order()));
}
#endif
_pass_thru = _requests.pass_thru.load ();
bool allstop = _requests.stop_all.exchange (false);
/* STEP TWO: if latency compensation tells us that we haven't really
* started yet, do nothing, because we can't make sense of a negative
* start sample time w.r.t the tempo map.
*/
if (start_sample < 0) {
return;
}
const pframes_t orig_nframes = nframes;
/* STEP THREE: triggers in audio tracks need a MIDI sidechain to be
* able to receive inbound MIDI for triggering etc. This needs to run
* before anything else, since we may need data just received to launch
* a trigger (or stop it)
*/
if (_sidechain) {
_sidechain->run (bufs, start_sample, end_sample, speed, nframes, true);
}
if (!_cue_recording) {
int32_t cue_bang = _session.first_cue_within (start_sample, end_sample);
if (cue_bang >= 0) {
std::cerr << " CUE BANG " << cue_bang << std::endl;
_active_scene = cue_bang;
}
}
/* STEP SIX: if at this point there is an active cue, make it trigger
* our corresponding slot
*/
if (_active_scene >= 0) {
DEBUG_TRACE (DEBUG::Triggers, string_compose ("tb noticed active scene %1\n", _active_scene));
if (_active_scene < (int32_t) all_triggers.size()) {
if (!all_triggers[_active_scene]->scene_isolated()) {
all_triggers[_active_scene]->bang ();
}
}
}
/* STEP FOUR: handle any incoming requests from the GUI or other
* non-MIDI UIs
*/
process_requests (bufs);
/* STEP FIVE: handle any incoming MIDI requests
*/
process_midi_trigger_requests (bufs);
/* STEP SEVEN: let each slot process any individual state requests
*/
std::vector<uint32_t> to_run;
for (uint32_t n = 0; n < all_triggers.size(); ++n) {
all_triggers[n]->process_state_requests (bufs, nframes - 1);
}
/* cue handling is over at this point, reset _active_scene to reflect this */
_active_scene = -1;
if (_currently_playing && _currently_playing->state() == Trigger::Stopped) {
_currently_playing = 0;
}
for (uint32_t n = 0; n < all_triggers.size(); ++n) {
if (all_triggers[n] != _currently_playing) {
maybe_swap_pending (n);
}
}
/* STEP EIGHT: if there is no active slot, see if there any queued up
*/
if (!_currently_playing && !allstop) {
if ((_currently_playing = get_next_trigger()) != 0) {
maybe_swap_pending (_currently_playing->index());
_currently_playing->startup ();
PropertyChanged (Properties::currently_playing);
active_trigger_boxes.fetch_add (1);
}
}
/* STEP NINE: if we've been told to stop all slots, do so
*/
if (allstop) {
stop_all ();
}
/* STEP TEN: nothing to do?
*/
if (!_currently_playing) {
DEBUG_TRACE (DEBUG::Triggers, "nothing currently playing 1, reset stop_all to false\n");
_stop_all = false;
return;
}
/* transport must be active for triggers */
if (!_session.transport_state_rolling() && !allstop) {
_session.start_transport_from_trigger ();
}
/* now get the information we need related to the tempo map and the
* timeline
*/
const Temporal::Beats end_beats (timepos_t (end_sample).beats());
Temporal::TempoMap::SharedPtr tmap (Temporal::TempoMap::use());
uint32_t max_chans = 0;
TriggerPtr nxt;
pframes_t dest_offset = 0;
bool can_clear = true;
while (nframes) {
/* start can move if we have to switch triggers in mid-process cycle */
const Temporal::Beats start_beats (timepos_t (start_sample).beats());
const double bpm = tmap->quarters_per_minute_at (timepos_t (start_beats));
DEBUG_TRACE (DEBUG::Triggers, string_compose ("nf loop, ss %1 es %2 sb %3 eb %4 bpm %5\n", start_sample, end_sample, start_beats, end_beats, bpm));
/* see if there's another trigger explicitly queued */
RingBuffer<uint32_t>::rw_vector rwv;
explicit_queue.get_read_vector (&rwv);
if (rwv.len[0] > 0) {
DEBUG_TRACE (DEBUG::Triggers, string_compose ("explicit queue rvec %1 + %2\n", rwv.len[0], rwv.len[1]));
/* peek at it without dequeing it */
uint32_t n = *(rwv.buf[0]);
nxt = trigger (n);
/* if user triggered same clip, that will have been handled as
* it processed bang requests. Nothing to do here otherwise.
*/
if (nxt != _currently_playing) {
/* user has triggered a different slot than the currently waiting-to-play or playing slot */
if (nxt->legato()) {
/* We want to start this trigger immediately, without
* waiting for quantization points, and it should start
* playing at the same internal offset as the current
* trigger.
*/
explicit_queue.increment_read_idx (1); /* consume the entry we peeked at */
nxt->set_legato_offset (_currently_playing->current_pos());
/* starting up next trigger, check for pending */
maybe_swap_pending (n);
nxt = trigger (n);
nxt->jump_start ();
_currently_playing->jump_stop (bufs, dest_offset);
/* and switch */
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 => %2 switched to in legato mode\n", _currently_playing->index(), nxt->index()));
_currently_playing = nxt;
can_clear = true;
PropertyChanged (Properties::currently_playing);
} else {
/* no legato-switch */
if (_currently_playing->state() == Trigger::Stopped) {
explicit_queue.increment_read_idx (1); /* consume the entry we peeked at */
/* starting up next trigger, check for pending */
maybe_swap_pending (n);
nxt = trigger (n);
nxt->startup ();
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 was finished, started %2\n", _currently_playing->index(), nxt->index()));
_currently_playing = nxt;
/* since we just switched, can_clear needs to be true again so that MIDITriggers can do the right
thing for !passthru
*/
can_clear = true;
PropertyChanged (Properties::currently_playing);
} else if (_currently_playing->state() != Trigger::WaitingToStop) {
/* but just begin stoppingthe currently playing slot */
_currently_playing->begin_stop ();
DEBUG_TRACE (DEBUG::Triggers, string_compose ("start stop for %1 before switching to %2\n", _currently_playing->index(), nxt->index()));
}
}
}
}
DEBUG_TRACE (DEBUG::Triggers, string_compose ("currently playing: %1, state now %2 stop all ? %3\n", _currently_playing->name(), enum_2_string (_currently_playing->state()), _stop_all));
/* if we're not in the process of stopping all active triggers,
* but the current one has stopped, decide which (if any)
* trigger to play next.
*/
if (_currently_playing->state() == Trigger::Stopped) {
if (!_stop_all && !_currently_playing->explicitly_stopped()) {
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 has stopped, need next...\n", _currently_playing->name()));
if (_currently_playing->will_follow()) {
int n = determine_next_trigger (_currently_playing->index());
Temporal::BBT_Offset start_quantization;
std::cerr << "dnt = " << n << endl;
if (n < 0) {
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 finished, no next trigger\n", _currently_playing->name()));
_currently_playing = 0;
PropertyChanged (Properties::currently_playing);
break; /* no triggers to come next, break out of nframes loop */
}
if ((int) _currently_playing->index() == n) {
start_quantization = Temporal::BBT_Offset ();
DEBUG_TRACE (DEBUG::Triggers, string_compose ("switching to next trigger %1, will use start immediately \n", all_triggers[n]->name()));
} else {
DEBUG_TRACE (DEBUG::Triggers, string_compose ("switching to next trigger %1\n", all_triggers[n]->name()));
}
_currently_playing = all_triggers[n];
_currently_playing->startup (start_quantization);
can_clear = true;
PropertyChanged (Properties::currently_playing);
} else {
_currently_playing = 0;
PropertyChanged (Properties::currently_playing);
DEBUG_TRACE (DEBUG::Triggers, "currently playing was stopped, but stop_all was set, leaving nf loop\n");
/* leave nframes loop */
break;
}
} else {
_currently_playing = 0;
PropertyChanged (Properties::currently_playing);
DEBUG_TRACE (DEBUG::Triggers, "currently playing was stopped, but stop_all was set, leaving nf loop\n");
/* leave nframes loop */
break;
}
}
pframes_t frames_covered;
boost::shared_ptr<AudioRegion> ar = boost::dynamic_pointer_cast<AudioRegion> (_currently_playing->region());
if (ar) {
max_chans = std::max (ar->n_channels(), max_chans);
}
frames_covered = _currently_playing->run (bufs, start_sample, end_sample, start_beats, end_beats, nframes, dest_offset, _pass_thru, bpm, can_clear);
nframes -= frames_covered;
start_sample += frames_covered;
dest_offset += frames_covered;
can_clear = false;
DEBUG_TRACE (DEBUG::Triggers, string_compose ("trig %1 ran, covered %2 state now %3 nframes now %4\n",
_currently_playing->name(), frames_covered, enum_2_string (_currently_playing->state()), nframes));
}
if (nframes && !_pass_thru) {
/* didn't cover the entire nframes worth of the buffer, and not
* doing pass thru, so silence whatever is left.
*/
for (uint32_t chn = 0; chn < bufs.count().n_audio(); ++chn) {
AudioBuffer& buf (bufs.get_audio (chn));
buf.silence (nframes, (orig_nframes - nframes));
}
}
if (!_currently_playing) {
DEBUG_TRACE (DEBUG::Triggers, "nothing currently playing 2, reset stop_all to false\n");
_stop_all = false;
}
/* audio buffer (channel) count determined by max of input and
* _currently_playing's channel count (if it was audio).
*/
ChanCount cc (DataType::AUDIO, max_chans);
/* MIDI buffer count not changed */
cc.set_midi (bufs.count().n_midi());
bufs.set_count (cc);
}
int
TriggerBox::determine_next_trigger (uint32_t current)
{
uint32_t n;
uint32_t runnable = 0;
std::vector<int32_t> possible_targets;
possible_targets.reserve (default_triggers_per_box);
/* count number of triggers that can actually be run (i.e. they have a region) */
for (uint32_t n = 0; n < all_triggers.size(); ++n) {
if (all_triggers[n]->region()) {
runnable++;
}
}
if (runnable == 0 || !all_triggers[current]->region()) {
return -1;
}
if (all_triggers[current]->follow_action (0).type == FollowAction::None) {
/* when left follow action is disabled, no follow action */
return -1;
}
/* decide which of the two follow actions we're going to use (based on
* random number and the probability setting)
*/
2021-08-15 10:04:08 -04:00
int r = _pcg.rand (100); // 0 .. 99
FollowAction fa;
if (r >= all_triggers[current]->follow_action_probability()) {
fa = all_triggers[current]->follow_action (0);
} else {
fa = all_triggers[current]->follow_action (1);
}
/* first switch: deal with the "special" cases where we either do
* nothing or just repeat the current trigger
*/
DEBUG_TRACE (DEBUG::Triggers, string_compose ("choose next trigger using follow action %1 given prob %2 and rnd %3\n", fa.to_string(), all_triggers[current]->follow_action_probability(), r));
if (fa.type == FollowAction::Stop) {
return -1;
}
if (runnable == 1) {
/* there's only 1 runnable trigger, so the "next" one
is the same as the current one.
*/
return current;
}
/* second switch: handle the "real" follow actions */
switch (fa.type) {
case FollowAction::None:
return -1;
2021-08-12 00:54:13 -04:00
case FollowAction::Again:
return current;
2021-08-12 00:54:13 -04:00
case FollowAction::ForwardTrigger:
n = current;
while (true) {
++n;
if (n >= all_triggers.size()) {
cerr << "loop with n = " << n << " of " << all_triggers.size() << endl;
n = 0;
}
if (n == current) {
cerr << "outa here\n";
break;
}
if (all_triggers[n]->region() && !all_triggers[n]->active()) {
return n;
}
}
break;
case FollowAction::ReverseTrigger:
n = current;
while (true) {
if (n == 0) {
n = all_triggers.size() - 1;
} else {
n -= 1;
}
if (n == current) {
break;
}
if (all_triggers[n]->region() && !all_triggers[n]->active ()) {
return n;
}
}
break;
case FollowAction::FirstTrigger:
for (n = 0; n < all_triggers.size(); ++n) {
if (all_triggers[n]->region() && !all_triggers[n]->active ()) {
return n;
}
}
break;
case FollowAction::LastTrigger:
2021-08-11 20:14:37 -04:00
for (int i = all_triggers.size() - 1; i >= 0; --i) {
if (all_triggers[i]->region() && !all_triggers[i]->active ()) {
return i;
}
}
break;
case FollowAction::JumpTrigger:
for (std::size_t n = 0; n < default_triggers_per_box; ++n) {
if (fa.targets.test (n) && all_triggers[n]->region()) {
possible_targets.push_back (n);
}
}
if (possible_targets.empty()) {
return 1;
}
return possible_targets[_pcg.rand (possible_targets.size())];
/* NOTREACHED */
case FollowAction::Stop:
break;
}
return current;
}
XMLNode&
TriggerBox::get_state (void)
{
2021-08-31 18:46:19 -04:00
XMLNode& node (Processor::get_state ());
node.set_property (X_("type"), X_("triggerbox"));
2021-08-31 18:46:19 -04:00
node.set_property (X_("data-type"), _data_type.to_string());
node.set_property (X_("order"), _order);
2021-08-31 18:46:19 -04:00
XMLNode* trigger_child (new XMLNode (X_("Triggers")));
{
Glib::Threads::RWLock::ReaderLock lm (trigger_lock);
for (Triggers::iterator t = all_triggers.begin(); t != all_triggers.end(); ++t) {
trigger_child->add_child_nocopy ((*t)->get_state());
}
}
node.add_child_nocopy (*trigger_child);
if (_sidechain) {
node.add_child_nocopy (_sidechain->get_state ());
}
2021-08-31 18:46:19 -04:00
return node;
}
int
TriggerBox::set_state (const XMLNode& node, int version)
{
Processor::set_state (node, version);
node.get_property (X_("data-type"), _data_type);
node.get_property (X_("order"), _order);
XMLNode* tnode (node.child (X_("Triggers")));
assert (tnode);
XMLNodeList const & tchildren (tnode->children());
drop_triggers ();
{
Glib::Threads::RWLock::WriterLock lm (trigger_lock);
for (XMLNodeList::const_iterator t = tchildren.begin(); t != tchildren.end(); ++t) {
TriggerPtr trig;
/* Note use of a custom delete function. We cannot
delete the old trigger from the RT context where the
trigger swap will happen, so we will ask the trigger
helper thread to take care of it.
*/
if (_data_type == DataType::AUDIO) {
trig.reset (new AudioTrigger (all_triggers.size(), *this), Trigger::request_trigger_delete);
all_triggers.push_back (trig);
trig->set_state (**t, version);
2021-10-29 18:22:55 -04:00
} else if (_data_type == DataType::MIDI) {
trig.reset (new MIDITrigger (all_triggers.size(), *this), Trigger::request_trigger_delete);
2021-10-29 18:22:55 -04:00
all_triggers.push_back (trig);
trig->set_state (**t, version);
}
}
}
/* sidechain is a Processor (IO) */
XMLNode* scnode = node.child (Processor::state_node_name.c_str ());
if (scnode) {
add_midi_sidechain ();
assert (_sidechain);
_sidechain->set_state (*scnode, version);
}
return 0;
}
void
TriggerBox::parameter_changed (std::string const & param)
{
if (param == X_("default-trigger-input-port")) {
reconnect_to_default ();
}
}
void
TriggerBox::reconnect_to_default ()
{
if (!_sidechain) {
return;
}
_sidechain->input()->nth (0)->disconnect_all ();
_sidechain->input()->nth (0)->connect (Config->get_default_trigger_input_port());
}
MultiAllocSingleReleasePool* TriggerBox::Request::pool;
void
TriggerBox::init_pool ()
{
/* "indirection" is because the Request struct is private, and so
nobody else can call its ::init_pool() static method.
*/
Request::init_pool ();
}
void
TriggerBox::Request::init_pool ()
{
pool = new MultiAllocSingleReleasePool (X_("TriggerBoxRequests"), sizeof (TriggerBox::Request), 1024);
}
void*
TriggerBox::Request::operator new (size_t)
{
return pool->alloc();
}
void
TriggerBox::Request::operator delete (void *ptr, size_t /*size*/)
{
return pool->release (ptr);
}
void
TriggerBox::request_reload (int32_t slot, void* ptr)
{
Request* r = new Request (Request::Reload);
r->slot = slot;
r->ptr = ptr;
requests.write (&r, 1);
}
void
TriggerBox::process_requests (BufferSet& bufs)
{
Request* r;
while (requests.read (&r, 1) == 1) {
process_request (bufs, r);
}
}
void
TriggerBox::process_request (BufferSet& bufs, Request* req)
{
switch (req->type) {
case Request::Use:
std::cerr << "Use for " << req->slot << std::endl;
break;
case Request::Reload:
std::cerr << "Reload for " << req->slot << std::endl;
reload (bufs, req->slot, req->ptr);
break;
}
delete req; /* back to the pool, RT-safe */
}
void
TriggerBox::reload (BufferSet& bufs, int32_t slot, void* ptr)
{
if (slot >= (int32_t) all_triggers.size()) {
return;
}
std::cerr << "reload slot " << slot << std::endl;
all_triggers[slot]->reload (bufs, ptr);
}
double
TriggerBox::position_as_fraction () const
{
TriggerPtr cp = _currently_playing;
if (!cp) {
return -1;
}
return cp->position_as_fraction ();
}
/* Thread */
MultiAllocSingleReleasePool* TriggerBoxThread::Request::pool = 0;
TriggerBoxThread::TriggerBoxThread ()
: requests (1024)
, _xthread (true)
{
if (pthread_create_and_store ("triggerbox thread", &thread, _thread_work, this)) {
error << _("Session: could not create triggerbox thread") << endmsg;
throw failed_constructor ();
}
}
TriggerBoxThread::~TriggerBoxThread()
{
void* status;
char msg = (char) Quit;
_xthread.deliver (msg);
pthread_join (thread, &status);
}
void *
TriggerBoxThread::_thread_work (void* arg)
{
SessionEvent::create_per_thread_pool ("tbthread events", 4096);
pthread_set_name (X_("tbthread"));
return ((TriggerBoxThread *) arg)->thread_work ();
}
void *
TriggerBoxThread::thread_work ()
{
2021-12-22 09:42:27 -05:00
pthread_set_name (X_("Trigger Worker"));
while (true) {
char msg;
if (_xthread.receive (msg, true) >= 0) {
if (msg == (char) Quit) {
return (void *) 0;
abort(); /*NOTREACHED*/
}
Temporal::TempoMap::fetch ();
Request* req;
while (requests.read (&req, 1) == 1) {
switch (req->type) {
case SetRegion:
req->box->set_region (req->slot, req->region);
break;
case DeleteTrigger:
delete_trigger (req->trigger);
break;
default:
break;
}
delete req; /* back to pool */
}
}
}
return (void *) 0;
}
void
TriggerBoxThread::queue_request (Request* req)
{
char c = req->type;
/* Quit is handled by simply delivering the request type (1 byte), with
* no payload in the FIFO. See ::thread_work() above.
*/
if (req->type != Quit) {
if (requests.write (&req, 1) != 1) {
return;
}
}
_xthread.deliver (c);
}
void*
TriggerBoxThread::Request::operator new (size_t)
{
return pool->alloc ();
}
void
TriggerBoxThread::Request::operator delete (void* ptr, size_t)
{
pool->release (ptr);
}
void
TriggerBoxThread::Request::init_pool ()
{
pool = new MultiAllocSingleReleasePool (X_("TriggerBoxThreadRequests"), sizeof (TriggerBoxThread::Request), 1024);
}
void
TriggerBoxThread::set_region (TriggerBox& box, uint32_t slot, boost::shared_ptr<Region> r)
{
TriggerBoxThread::Request* req = new TriggerBoxThread::Request (TriggerBoxThread::SetRegion);
req->box = &box;
req->slot = slot;
req->region = r;
queue_request (req);
}
void
TriggerBoxThread::request_delete_trigger (Trigger* t)
{
TriggerBoxThread::Request* req = new TriggerBoxThread::Request (DeleteTrigger);
req->trigger = t;
queue_request (req);
}
void
TriggerBoxThread::delete_trigger (Trigger* t)
{
delete t;
}