4505 lines
127 KiB
C++
4505 lines
127 KiB
C++
#include <algorithm>
|
|
#include <iostream>
|
|
#include <cstdlib>
|
|
#include <memory>
|
|
#include <sstream>
|
|
|
|
#include <boost/make_shared.hpp>
|
|
|
|
#include <glibmm.h>
|
|
|
|
#include <rubberband/RubberBandStretcher.h>
|
|
|
|
#include "pbd/basename.h"
|
|
#include "pbd/compose.h"
|
|
#include "pbd/failed_constructor.h"
|
|
#include "pbd/pthread_utils.h"
|
|
#include "pbd/types_convert.h"
|
|
#include "pbd/unwind.h"
|
|
|
|
#include "temporal/tempo.h"
|
|
|
|
#include "ardour/auditioner.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;
|
|
|
|
namespace ARDOUR {
|
|
namespace Properties {
|
|
PBD::PropertyDescriptor<bool> running;
|
|
PBD::PropertyDescriptor<bool> legato;
|
|
PBD::PropertyDescriptor<bool> use_follow_length;
|
|
PBD::PropertyDescriptor<Temporal::BBT_Offset> quantization;
|
|
PBD::PropertyDescriptor<Temporal::BBT_Offset> follow_length;
|
|
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;
|
|
PBD::PropertyDescriptor<int> follow_action_probability;
|
|
PBD::PropertyDescriptor<float> velocity_effect;
|
|
PBD::PropertyDescriptor<gain_t> gain;
|
|
PBD::PropertyDescriptor<bool> stretchable;
|
|
PBD::PropertyDescriptor<bool> cue_isolated;
|
|
PBD::PropertyDescriptor<bool> allow_patch_changes;
|
|
PBD::PropertyDescriptor<Trigger::StretchMode> stretch_mode;
|
|
PBD::PropertyDescriptor<bool> tempo_meter; /* only to transmit updates, not storage */
|
|
PBD::PropertyDescriptor<bool> patch_change; /* only to transmit updates, not storage */
|
|
PBD::PropertyDescriptor<bool> channel_map; /* only to transmit updates, not storage */
|
|
PBD::PropertyDescriptor<bool> used_channels; /* only to transmit updates, not storage */
|
|
}
|
|
}
|
|
|
|
PropertyChange
|
|
TriggerBox::all_trigger_props()
|
|
{
|
|
PropertyChange all;
|
|
all.add(Properties::name);
|
|
all.add(Properties::color);
|
|
all.add(Properties::legato);
|
|
all.add(Properties::use_follow_length);
|
|
all.add(Properties::quantization);
|
|
all.add(Properties::follow_length);
|
|
all.add(Properties::launch_style);
|
|
all.add(Properties::follow_action0);
|
|
all.add(Properties::follow_action1);
|
|
all.add(Properties::follow_action_probability);
|
|
all.add(Properties::velocity_effect);
|
|
all.add(Properties::gain);
|
|
all.add(Properties::stretchable);
|
|
all.add(Properties::cue_isolated);
|
|
all.add(Properties::allow_patch_changes);
|
|
all.add(Properties::stretch_mode);
|
|
all.add(Properties::tempo_meter);
|
|
all.add(Properties::stretchable);
|
|
all.add(Properties::patch_change);
|
|
all.add(Properties::channel_map);
|
|
all.add(Properties::used_channels);
|
|
|
|
return all;
|
|
}
|
|
|
|
std::string
|
|
ARDOUR::cue_marker_name (int32_t index)
|
|
{
|
|
/* this somewhat weird code structure is intended to allow for easy and
|
|
* correct translation.
|
|
*/
|
|
|
|
using std::string;
|
|
|
|
if (index == INT32_MAX) {
|
|
/* this is a reasonable "stop" icon */
|
|
return string (X_("\u25a1"));
|
|
}
|
|
|
|
switch (index) {
|
|
case 0: return string (_("A"));
|
|
case 1: return string (_("B"));
|
|
case 2: return string (_("C"));
|
|
case 3: return string (_("D"));
|
|
case 4: return string (_("E"));
|
|
case 5: return string (_("F"));
|
|
case 6: return string (_("G"));
|
|
case 7: return string (_("H"));
|
|
case 8: return string (_("I"));
|
|
case 9: return string (_("J"));
|
|
case 10: return string (_("K"));
|
|
case 11: return string (_("L"));
|
|
case 12: return string (_("M"));
|
|
case 13: return string (_("N"));
|
|
case 14: return string (_("O"));
|
|
case 15: return string (_("P"));
|
|
case 16: return string (_("Q"));
|
|
case 17: return string (_("R"));
|
|
case 18: return string (_("S"));
|
|
case 19: return string (_("T"));
|
|
case 20: return string (_("U"));
|
|
case 21: return string (_("V"));
|
|
case 22: return string (_("W"));
|
|
case 23: return string (_("X"));
|
|
case 24: return string (_("Y"));
|
|
case 25: return string (_("Z"));
|
|
}
|
|
|
|
return string();
|
|
}
|
|
|
|
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)
|
|
: _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)
|
|
, _gain (Properties::gain, 1.0)
|
|
, _velocity_effect (Properties::velocity_effect, 0.)
|
|
, _stretchable (Properties::stretchable, true)
|
|
, _cue_isolated (Properties::cue_isolated, false)
|
|
, _allow_patch_changes (Properties::allow_patch_changes, true)
|
|
, _stretch_mode (Properties::stretch_mode, Trigger::Crisp)
|
|
, _name (Properties::name, "")
|
|
, _color (Properties::color, 0xBEBEBEFF)
|
|
, process_index (0)
|
|
, final_processed_sample (0)
|
|
, _box (b)
|
|
, _state (Stopped)
|
|
, _playout (false)
|
|
, _bang (0)
|
|
, _unbang (0)
|
|
, _index (n)
|
|
, _loop_cnt (0)
|
|
, _ui (0)
|
|
, _explicitly_stopped (false)
|
|
, _pending_velocity_gain (1.0)
|
|
, _velocity_gain (1.0)
|
|
, _cue_launched (false)
|
|
, _used_channels (Evoral::SMF::UsedChannels())
|
|
, _estimated_tempo (0.)
|
|
, _segment_tempo (0.)
|
|
, _beatcnt (0.)
|
|
, _meter (4, 4)
|
|
, expected_end_sample (0)
|
|
, _pending ((Trigger*) 0)
|
|
, last_property_generation (0)
|
|
{
|
|
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 (_velocity_effect);
|
|
add_property (_stretchable);
|
|
add_property (_allow_patch_changes);
|
|
add_property (_cue_isolated);
|
|
add_property (_color);
|
|
add_property (_stretch_mode);
|
|
|
|
copy_to_ui_state ();
|
|
}
|
|
|
|
void
|
|
Trigger::request_trigger_delete (Trigger* t)
|
|
{
|
|
TriggerBox::worker->request_delete_trigger (t);
|
|
}
|
|
|
|
void
|
|
Trigger::get_ui_state (Trigger::UIState &state) const
|
|
{
|
|
/* this is used for operations like d&d when we want to query the current state */
|
|
/* you can't return ui_state here because that struct is used to queue properties that are being input *to* the trigger */
|
|
/* TODO: rename our member variable ui_state to _queued_ui_state or similar @paul ? */
|
|
state.launch_style = _launch_style;
|
|
state.follow_action0 = _follow_action0;
|
|
state.follow_action1 = _follow_action1;
|
|
state.follow_action_probability = _follow_action_probability;
|
|
state.follow_count = _follow_count;
|
|
state.quantization = _quantization;
|
|
state.follow_length = _follow_length;
|
|
state.use_follow_length = _use_follow_length;
|
|
state.legato = _legato;
|
|
state.gain = _gain;
|
|
state.velocity_effect = _velocity_effect;
|
|
state.stretchable = _stretchable;
|
|
state.allow_patch_changes = _allow_patch_changes;
|
|
state.cue_isolated = _cue_isolated;
|
|
state.stretch_mode = _stretch_mode;
|
|
|
|
state.name = _name;
|
|
state.color = _color;
|
|
|
|
state.used_channels = used_channels();
|
|
for (int i = 0; i<16; i++) {
|
|
if (_patch_change[i].is_set()) {
|
|
state.patch_change[i] = _patch_change[i];
|
|
}
|
|
}
|
|
|
|
/* tempo is currently not a property */
|
|
state.tempo = segment_tempo();
|
|
}
|
|
|
|
void
|
|
Trigger::set_ui_state (Trigger::UIState &state)
|
|
{
|
|
ui_state = state;
|
|
|
|
/* increment ui_state generation so vals will get loaded when the trigger stops */
|
|
unsigned int g = ui_state.generation.load();
|
|
while (!ui_state.generation.compare_exchange_strong (g, g+1));
|
|
|
|
/* tempo is currently outside the scope of ui_state */
|
|
if (state.tempo > 0) {
|
|
set_segment_tempo(state.tempo);
|
|
}
|
|
}
|
|
|
|
void
|
|
Trigger::update_properties ()
|
|
{
|
|
/* Don't update unless there is evidence of a change */
|
|
|
|
unsigned int g;
|
|
|
|
while ((g = ui_state.generation.load()) != last_property_generation) {
|
|
|
|
StretchMode old_stretch = _stretch_mode;
|
|
|
|
_launch_style = ui_state.launch_style;
|
|
_follow_action0 = ui_state.follow_action0;
|
|
_follow_action1 = ui_state.follow_action1;
|
|
_follow_action_probability = ui_state.follow_action_probability;
|
|
_follow_count = ui_state.follow_count;
|
|
_quantization = ui_state.quantization;
|
|
_follow_length = ui_state.follow_length;
|
|
_use_follow_length = ui_state.use_follow_length;
|
|
_legato = ui_state.legato;
|
|
_gain = ui_state.gain;
|
|
_velocity_effect = ui_state.velocity_effect;
|
|
_stretchable = ui_state.stretchable;
|
|
_allow_patch_changes = ui_state.allow_patch_changes;
|
|
_cue_isolated = ui_state.cue_isolated;
|
|
_stretch_mode = ui_state.stretch_mode;
|
|
_color = ui_state.color;
|
|
|
|
/* @paul: is this safe to do here ?*/
|
|
/* the UI only allows changing stretch_mode when the clip is stopped,
|
|
* and you can't d+d or create a new clip while it's playing, so I think it's OK */
|
|
if (_stretch_mode != old_stretch) {
|
|
setup_stretcher ();
|
|
}
|
|
|
|
/* during construction of a new trigger, the ui_state.name is initialized and queued
|
|
* ...but in the interim, we have likely been assigned a name in a separate thread (importing the region)
|
|
* ...so don't overwrite our name if ui_state.name is empty
|
|
*/
|
|
if (ui_state.name != "" ) {
|
|
_name = ui_state.name;
|
|
}
|
|
|
|
_used_channels = ui_state.used_channels;
|
|
|
|
for (int chan = 0; chan<16; chan++) {
|
|
if (ui_state.patch_change[chan].is_set()) {
|
|
_patch_change[chan] = ui_state.patch_change[chan];
|
|
}
|
|
}
|
|
|
|
last_property_generation = g;
|
|
}
|
|
|
|
/* we get here when we were able to copy the entire set of properties
|
|
* without the ui_state.generation value changing during the copy, or
|
|
* when no update appeared to be required.
|
|
*/
|
|
}
|
|
|
|
void
|
|
Trigger::copy_to_ui_state ()
|
|
{
|
|
/* usable only at object creation */
|
|
|
|
ui_state.launch_style = _launch_style;
|
|
ui_state.follow_action0 = _follow_action0;
|
|
ui_state.follow_action1 = _follow_action1;
|
|
ui_state.follow_action_probability = _follow_action_probability;
|
|
ui_state.follow_count = _follow_count;
|
|
ui_state.quantization = _quantization;
|
|
ui_state.follow_length = _follow_length;
|
|
ui_state.use_follow_length = _use_follow_length;
|
|
ui_state.legato = _legato;
|
|
ui_state.gain = _gain;
|
|
ui_state.velocity_effect = _velocity_effect;
|
|
ui_state.stretchable = _stretchable;
|
|
ui_state.cue_isolated = _cue_isolated;
|
|
ui_state.allow_patch_changes = _allow_patch_changes;
|
|
ui_state.stretch_mode = _stretch_mode;
|
|
ui_state.name = _name;
|
|
ui_state.color = _color;
|
|
|
|
ui_state.used_channels = _used_channels;
|
|
for (int i = 0; i<16; i++) {
|
|
if (_patch_change[i].is_set()) {
|
|
ui_state.patch_change[i] = _patch_change[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
Trigger::send_property_change (PropertyChange pc)
|
|
{
|
|
if (_box.fast_forwarding()) {
|
|
return;
|
|
}
|
|
|
|
PropertyChanged (pc);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
#define TRIGGER_UI_SET(name,type) \
|
|
void \
|
|
Trigger::set_ ## name (type val) \
|
|
{ \
|
|
unsigned int g = ui_state.generation.load(); \
|
|
do { ui_state.name = val; } while (!ui_state.generation.compare_exchange_strong (g, g+1)); \
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("trigger %1 property& cas-set: %2 gen %3\n", index(), _ ## name.property_name(), ui_state.generation.load())); \
|
|
send_property_change (Properties::name); /* EMIT SIGNAL */ \
|
|
_box.session().set_dirty (); \
|
|
} \
|
|
type \
|
|
Trigger::name () const \
|
|
{ \
|
|
unsigned int g = ui_state.generation.load (); \
|
|
type val; \
|
|
\
|
|
do { val = ui_state.name; } while (ui_state.generation.load () != g); \
|
|
\
|
|
return val; \
|
|
}
|
|
|
|
#define TRIGGER_UI_SET_CONST_REF(name,type) \
|
|
void \
|
|
Trigger::set_ ## name (type const & val) \
|
|
{ \
|
|
unsigned int g = ui_state.generation.load(); \
|
|
do { ui_state.name = val; } while (!ui_state.generation.compare_exchange_strong (g, g+1)); \
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("trigger %1 property& cas-set: %2 gen %3\n", index(), _ ## name.property_name(), ui_state.generation.load())); \
|
|
send_property_change (Properties::name); /* EMIT SIGNAL */ \
|
|
_box.session().set_dirty (); \
|
|
} \
|
|
type \
|
|
Trigger::name () const \
|
|
{ \
|
|
unsigned int g = ui_state.generation.load (); \
|
|
type val; \
|
|
\
|
|
do { val = ui_state.name; } while (ui_state.generation.load () != g); \
|
|
\
|
|
return val; \
|
|
}
|
|
|
|
/* these params are central to the triggerbox behavior and must only be applied at ::retrigger() via ::update_properties() */
|
|
TRIGGER_UI_SET (cue_isolated,bool)
|
|
TRIGGER_UI_SET (stretchable, bool)
|
|
TRIGGER_UI_SET (velocity_effect, float)
|
|
TRIGGER_UI_SET (follow_count, uint32_t)
|
|
TRIGGER_UI_SET_CONST_REF (follow_action0, FollowAction)
|
|
TRIGGER_UI_SET_CONST_REF (follow_action1, FollowAction)
|
|
TRIGGER_UI_SET (launch_style, Trigger::LaunchStyle)
|
|
TRIGGER_UI_SET_CONST_REF (follow_length, Temporal::BBT_Offset)
|
|
TRIGGER_UI_SET (use_follow_length, bool)
|
|
TRIGGER_UI_SET (legato, bool)
|
|
TRIGGER_UI_SET (follow_action_probability, int)
|
|
TRIGGER_UI_SET_CONST_REF (quantization, Temporal::BBT_Offset)
|
|
|
|
#define TRIGGER_DIRECT_SET(name,type) \
|
|
void \
|
|
Trigger::set_ ## name (type val) \
|
|
{ \
|
|
if (_ ## name == val) { return; } \
|
|
_ ## name = val; \
|
|
ui_state.name = val; \
|
|
unsigned int g = ui_state.generation.load(); \
|
|
do { ui_state.name = val; } while (!ui_state.generation.compare_exchange_strong (g, g+1)); \
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("trigger %1 property& cas-set: %2 gen %3\n", index(), _ ## name.property_name(), ui_state.generation.load())); \
|
|
send_property_change (Properties::name); /* EMIT SIGNAL */ \
|
|
_box.session().set_dirty (); \
|
|
} \
|
|
type \
|
|
Trigger::name () const \
|
|
{ \
|
|
return _ ## name; \
|
|
}
|
|
|
|
#define TRIGGER_DIRECT_SET_CONST_REF(name,type) \
|
|
void \
|
|
Trigger::set_ ## name (type const & val) \
|
|
{ \
|
|
if (_ ## name == val) { return; } \
|
|
_ ## name = val; \
|
|
ui_state.name = val; \
|
|
unsigned int g = ui_state.generation.load(); \
|
|
do { ui_state.name = val; } while (!ui_state.generation.compare_exchange_strong (g, g+1)); \
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("trigger %1 property& cas-set: %2 gen %3\n", index(), _ ## name.property_name(), ui_state.generation.load())); \
|
|
send_property_change (Properties::name); /* EMIT SIGNAL */ \
|
|
_box.session().set_dirty (); \
|
|
} \
|
|
type \
|
|
Trigger::name () const \
|
|
{ \
|
|
return _ ## name; \
|
|
}
|
|
|
|
/* these params can take effect outside the scope of ::retrigger
|
|
* BUT they still need to set the ui_state variables as well as the associated member variable
|
|
* otherwise an incoming ui_state change will overwrite your changes
|
|
* */
|
|
TRIGGER_DIRECT_SET_CONST_REF (name, std::string)
|
|
TRIGGER_DIRECT_SET (color, color_t)
|
|
TRIGGER_DIRECT_SET (gain, gain_t)
|
|
TRIGGER_DIRECT_SET (allow_patch_changes, bool)
|
|
/* patch_change[] is implemented manually but it needs to operate the same as above */
|
|
|
|
void
|
|
Trigger::set_ui (void* p)
|
|
{
|
|
_ui = p;
|
|
}
|
|
|
|
void
|
|
Trigger::bang ()
|
|
{
|
|
if (!_region) {
|
|
return;
|
|
}
|
|
_bang.fetch_add (1);
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("bang on %1\n", _index));
|
|
}
|
|
|
|
void
|
|
Trigger::unbang ()
|
|
{
|
|
if (!_region) {
|
|
return;
|
|
}
|
|
_unbang.fetch_add (1);
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("un-bang on %1\n", _index));
|
|
}
|
|
|
|
XMLNode&
|
|
Trigger::get_state () const
|
|
{
|
|
XMLNode* node = new XMLNode (X_("Trigger"));
|
|
|
|
/* XXX possible locking problems here if trigger is active, because
|
|
* properties could be overwritten
|
|
*/
|
|
|
|
for (OwnedPropertyList::iterator i = _properties->begin(); i != _properties->end(); ++i) {
|
|
i->second->get_value (*node);
|
|
}
|
|
|
|
node->set_property (X_("index"), _index);
|
|
|
|
node->set_property (X_("segment-tempo"), _segment_tempo);
|
|
|
|
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); //this results in a call to estimate_tempo()
|
|
}
|
|
|
|
double tempo;
|
|
if (node.get_property (X_("segment-tempo"), tempo)) {
|
|
/* this is the user-selected tempo which overrides estimated_tempo */
|
|
set_segment_tempo(tempo);
|
|
}
|
|
|
|
node.get_property (X_("index"), _index);
|
|
set_values (node);
|
|
|
|
return 0;
|
|
}
|
|
|
|
bool
|
|
Trigger::internal_use_follow_length () const
|
|
{
|
|
return (_follow_action0.val().type != FollowAction::None) && _use_follow_length;
|
|
}
|
|
|
|
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);
|
|
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;
|
|
}
|
|
|
|
timepos_t
|
|
Trigger::current_pos() const
|
|
{
|
|
return timepos_t (process_index);
|
|
}
|
|
|
|
double
|
|
Trigger::position_as_fraction () const
|
|
{
|
|
if (!active()) {
|
|
return 0.0;
|
|
}
|
|
|
|
return process_index / (double) final_processed_sample;
|
|
}
|
|
|
|
void
|
|
Trigger::retrigger ()
|
|
{
|
|
process_index = 0;
|
|
_playout = false;
|
|
}
|
|
|
|
void
|
|
Trigger::request_stop ()
|
|
{
|
|
_requests.stop = true;
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 asked to stop\n", name()));
|
|
}
|
|
|
|
void
|
|
Trigger::startup (BufferSet& bufs, pframes_t dest_offset, Temporal::BBT_Offset const & start_quantization)
|
|
{
|
|
/* This is just a non-virtual wrapper with a default parameter that calls _startup() */
|
|
_startup (bufs, dest_offset, start_quantization);
|
|
}
|
|
|
|
void
|
|
Trigger::_startup (BufferSet& bufs, pframes_t dest_offset, Temporal::BBT_Offset const & start_quantization)
|
|
{
|
|
_state = WaitingToStart;
|
|
_playout = false;
|
|
_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()));
|
|
send_property_change (ARDOUR::Properties::running);
|
|
}
|
|
|
|
void
|
|
Trigger::shutdown_from_fwd ()
|
|
{
|
|
_state = Stopped;
|
|
_playout = false;
|
|
_loop_cnt = 0;
|
|
_cue_launched = false;
|
|
_pending_velocity_gain = _velocity_gain = 1.0;
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 shuts down\n", name()));
|
|
send_property_change (ARDOUR::Properties::running);
|
|
}
|
|
|
|
void
|
|
Trigger::shutdown (BufferSet& /*bufs*/, pframes_t /*dest_offset*/)
|
|
{
|
|
shutdown_from_fwd ();
|
|
}
|
|
|
|
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 jump_start() requested state %2\n", index(), enum_2_string (_state)));
|
|
send_property_change (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 jump_stop() requested state %2\n", index(), enum_2_string (_state)));
|
|
send_property_change (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 begin_stop() requested state %2\n", index(), enum_2_string (_state)));
|
|
send_property_change (ARDOUR::Properties::running);
|
|
}
|
|
|
|
void
|
|
Trigger::begin_switch (TriggerPtr nxt)
|
|
{
|
|
/* this is used when we start a tell a currently playing trigger to
|
|
stop, but wait for quantization first.
|
|
*/
|
|
_state = WaitingToSwitch;
|
|
_nxt_quantization = nxt->_quantization;
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 begin_switch() requested state %2\n", index(), enum_2_string (_state)));
|
|
send_property_change (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:
|
|
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;
|
|
send_property_change (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);
|
|
break;
|
|
|
|
case WaitingToStart:
|
|
case WaitingToStop:
|
|
case WaitingToSwitch:
|
|
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:
|
|
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 WaitingToSwitch:
|
|
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()));
|
|
}
|
|
}
|
|
}
|
|
|
|
Temporal::BBT_Time
|
|
Trigger::compute_start (Temporal::TempoMap::SharedPtr const & tmap, samplepos_t start, samplepos_t end, Temporal::BBT_Offset const & q, samplepos_t& start_samples, bool& will_start)
|
|
{
|
|
Temporal::Beats start_beats (tmap->quarters_at (timepos_t (start)));
|
|
Temporal::Beats end_beats (tmap->quarters_at (timepos_t (end)));
|
|
|
|
Temporal::BBT_Time t_bbt;
|
|
Temporal::Beats t_beats;
|
|
|
|
if (!compute_quantized_transition (start, start_beats, end_beats, t_bbt, t_beats, start_samples, tmap, q)) {
|
|
will_start = false;
|
|
return Temporal::BBT_Time ();
|
|
}
|
|
|
|
will_start = true;
|
|
return t_bbt;
|
|
}
|
|
|
|
bool
|
|
Trigger::compute_quantized_transition (samplepos_t start_sample, Temporal::Beats const & start_beats, Temporal::Beats const & end_beats,
|
|
Temporal::BBT_Time& t_bbt, Temporal::Beats& t_beats, samplepos_t& t_samples,
|
|
Temporal::TempoMap::SharedPtr const & tmap, Temporal::BBT_Offset const & q)
|
|
{
|
|
/* 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.
|
|
*/
|
|
|
|
Temporal::BBT_Time possible_bbt;
|
|
Temporal::Beats possible_beats;
|
|
samplepos_t possible_samples;
|
|
|
|
if (q < Temporal::BBT_Offset (0, 0, 0)) {
|
|
/* negative quantization == do not quantize */
|
|
|
|
possible_samples = start_sample;
|
|
possible_beats = start_beats;
|
|
possible_bbt = tmap->bbt_at (possible_beats);
|
|
|
|
} else if (q.bars == 0) {
|
|
|
|
possible_beats = start_beats.round_up_to_multiple (Temporal::Beats (q.beats, q.ticks));
|
|
possible_bbt = tmap->bbt_at (possible_beats);
|
|
possible_samples = tmap->sample_at (possible_beats);
|
|
|
|
} else {
|
|
|
|
possible_bbt = tmap->bbt_at (timepos_t (start_beats));
|
|
possible_bbt = possible_bbt.round_up_to_bar ();
|
|
/* bars are 1-based; 'every 4 bars' means 'on bar 1, 5, 9, ...' */
|
|
possible_bbt.bars = 1 + ((possible_bbt.bars-1) / q.bars * q.bars);
|
|
possible_beats = tmap->quarters_at (possible_bbt);
|
|
possible_samples = tmap->sample_at (possible_bbt);
|
|
|
|
}
|
|
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%6/%1 quantized with %5 transition at %2, sb %3 eb %4\n", index(), possible_samples, start_beats, end_beats, q, _box.order()));
|
|
|
|
/* See if this time falls within the range of time given to us */
|
|
|
|
if (possible_beats < start_beats || possible_beats > end_beats) {
|
|
/* transition time not reached */
|
|
return false;
|
|
}
|
|
|
|
t_bbt = possible_bbt;
|
|
t_beats = possible_beats;
|
|
t_samples = possible_samples;
|
|
|
|
return true;
|
|
}
|
|
|
|
pframes_t
|
|
Trigger::compute_next_transition (samplepos_t start_sample, Temporal::Beats const & start, Temporal::Beats const & end, pframes_t nframes,
|
|
Temporal::BBT_Time& t_bbt, Temporal::Beats& t_beats, samplepos_t& t_samples,
|
|
Temporal::TempoMap::SharedPtr const & tmap)
|
|
{
|
|
using namespace Temporal;
|
|
|
|
/* In these states, we are not waiting for a transition */
|
|
|
|
if (_state == Stopped || _state == Running || _state == Stopping) {
|
|
/* no transition */
|
|
return 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);
|
|
|
|
} else if (_state == WaitingToSwitch) {
|
|
|
|
q = _nxt_quantization;
|
|
|
|
}
|
|
|
|
if (!compute_quantized_transition (start_sample, start, end, t_bbt, t_beats, t_samples, tmap, q)) {
|
|
/* no transition */
|
|
return 0;
|
|
}
|
|
|
|
switch (_state) {
|
|
case WaitingToStop:
|
|
case WaitingToSwitch:
|
|
nframes = t_samples - start_sample;
|
|
break;
|
|
|
|
case WaitingToStart:
|
|
nframes -= std::max (samplepos_t (0), t_samples - start_sample);
|
|
break;
|
|
|
|
case WaitingForRetrigger:
|
|
break;
|
|
|
|
default:
|
|
fatal << string_compose (_("programming error: %1 %2 %3"), "impossible trigger state (", enum_2_string (_state), ") in ::adjust_nframes()") << endmsg;
|
|
abort();
|
|
}
|
|
|
|
return nframes;
|
|
}
|
|
|
|
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)
|
|
{
|
|
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)) {
|
|
/* will cover everything */
|
|
return;
|
|
}
|
|
|
|
Temporal::BBT_Time transition_bbt;
|
|
TempoMap::SharedPtr tmap (TempoMap::use());
|
|
|
|
if (!compute_next_transition (start_sample, start, end, nframes, transition_bbt, transition_beats, transition_samples, tmap)) {
|
|
return;
|
|
}
|
|
|
|
pframes_t extra_offset = 0;
|
|
Temporal::Beats elen_ignored;
|
|
|
|
/* transition time has arrived! let's figure out what're doing:
|
|
* stopping, starting, retriggering
|
|
*/
|
|
|
|
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:
|
|
case WaitingToSwitch:
|
|
_state = Stopping;
|
|
send_property_change (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/%2 will stop somewhere in the middle of run(), specifically at %3 (%4) vs expected end at %5\n", index(), name(), transition_beats, transition_samples, 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;
|
|
(void) compute_end (tmap, transition_bbt, transition_samples, elen_ignored);
|
|
send_property_change (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;
|
|
|
|
/* XXX need to silence start of buffers up to dest_offset */
|
|
break;
|
|
|
|
case WaitingForRetrigger:
|
|
retrigger ();
|
|
_state = Running;
|
|
(void) compute_end (tmap, transition_bbt, transition_samples, elen_ignored);
|
|
send_property_change (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 ();
|
|
send_property_change (ARDOUR::Properties::running);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
template<typename TriggerType>
|
|
void
|
|
Trigger::start_and_roll_to (samplepos_t start_pos, samplepos_t end_position, TriggerType& trigger,
|
|
pframes_t (TriggerType::*run_method) (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, double bpm))
|
|
{
|
|
const pframes_t block_size = AudioEngine::instance()->samples_per_cycle ();
|
|
BufferSet bufs;
|
|
|
|
/* no need to allocate any space for BufferSet because we call
|
|
audio_run<false>() which is guaranteed to never use the buffers.
|
|
|
|
AudioTrigger::_startup() also does not use BufferSet (MIDITrigger
|
|
does, and we use virtual functions so the argument list is the same
|
|
for both, even though only the MIDI case needs the BufferSet).
|
|
*/
|
|
|
|
startup (bufs, 0, _quantization);
|
|
_cue_launched = true;
|
|
|
|
samplepos_t pos = start_pos;
|
|
Temporal::TempoMap::SharedPtr tmap (Temporal::TempoMap::use());
|
|
|
|
while (pos < end_position) {
|
|
pframes_t nframes = std::min (block_size, (pframes_t) (end_position - pos));
|
|
Temporal::Beats start_beats = tmap->quarters_at (timepos_t (pos));
|
|
Temporal::Beats end_beats = tmap->quarters_at (timepos_t (pos+nframes));
|
|
const double bpm = tmap->quarters_per_minute_at (timepos_t (start_beats));
|
|
|
|
pframes_t n = (trigger.*run_method) (bufs, pos, pos+nframes, start_beats, end_beats, nframes, 0, bpm);
|
|
|
|
/* We could have reached the end. Check and restart, because
|
|
* TriggerBox::fast_forward() already determined that we are
|
|
* the active trigger at @param end_position
|
|
*/
|
|
|
|
if (_state == Stopped) {
|
|
retrigger ();
|
|
_state = WaitingToStart;
|
|
_cue_launched = true;
|
|
}
|
|
|
|
pos += n;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/*--------------------*/
|
|
|
|
AudioTrigger::AudioTrigger (uint32_t n, TriggerBox& b)
|
|
: Trigger (n, b)
|
|
, _stretcher (0)
|
|
, _start_offset (0)
|
|
, read_index (0)
|
|
, last_readable_sample (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;
|
|
send_property_change (Properties::stretch_mode);
|
|
_box.session().set_dirty();
|
|
}
|
|
|
|
void
|
|
AudioTrigger::set_segment_tempo (double t)
|
|
{
|
|
if (!_region) {
|
|
_segment_tempo = 0;
|
|
return;
|
|
}
|
|
|
|
if (t<=0.) {
|
|
/*special case: we're told the file has no defined tempo.
|
|
* this can happen from crazy user input (0 beat length or somesuch), or if estimate_tempo() fails entirely
|
|
* in either case, we need to make a sensible _beatcnt, and that means we need a tempo */
|
|
const double seconds = (double) data.length / _box.session().sample_rate();
|
|
double beats = ceil(4. * 120. * (seconds/60.0)); //how many (rounded up) 16th-notes would this be at 120bpm?
|
|
beats /= 4.; //convert to quarter notes
|
|
t = beats / (seconds/60); /* our operating tempo. note that _estimated_tempo probably retains the 0bpm */
|
|
}
|
|
|
|
if (_segment_tempo != t) {
|
|
|
|
_segment_tempo = t;
|
|
|
|
/*beatcnt is a derived property from segment tempo and the file's length*/
|
|
const double seconds = (double) data.length / _box.session().sample_rate();
|
|
_beatcnt = _segment_tempo * (seconds/60.0);
|
|
|
|
/*initialize follow_length to match the length of the clip */
|
|
_follow_length = Temporal::BBT_Offset (0, _beatcnt, 0);
|
|
|
|
send_property_change (ARDOUR::Properties::tempo_meter);
|
|
_box.session().set_dirty();
|
|
}
|
|
|
|
/* TODO: once we have a Region Trimmer, this could get more complicated:
|
|
* this segment might overlap another SD (Coverage==Internal|Start|End)
|
|
* in which case we might be setting both SDs, or not. TBD*/
|
|
if (_region) {
|
|
SegmentDescriptor segment = get_segment_descriptor();
|
|
for (auto & src : _region->sources()) {
|
|
src->set_segment_descriptor (segment);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
AudioTrigger::set_segment_beatcnt (double count)
|
|
{
|
|
//given a beatcnt from the user, we use the data length to re-calc tempo internally
|
|
// ... TODO: provide a graphical trimmer to give the user control of data.length by dragging the start and end of the sample.
|
|
const double seconds = (double) data.length / _box.session().sample_rate();
|
|
double tempo = count / (seconds/60.0);
|
|
|
|
set_segment_tempo(tempo);
|
|
}
|
|
|
|
bool
|
|
AudioTrigger::stretching() const
|
|
{
|
|
return (_segment_tempo != .0) && _stretchable;
|
|
}
|
|
|
|
SegmentDescriptor
|
|
AudioTrigger::get_segment_descriptor () const
|
|
{
|
|
SegmentDescriptor sd;
|
|
|
|
sd.set_extent (_region->start_sample(), _region->length_samples());
|
|
sd.set_tempo (Temporal::Tempo (_segment_tempo, 4));
|
|
|
|
return sd;
|
|
}
|
|
|
|
void
|
|
AudioTrigger::_startup (BufferSet& bufs, pframes_t dest_offset, Temporal::BBT_Offset const & start_quantization)
|
|
{
|
|
Trigger::_startup (bufs, dest_offset, 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 ();
|
|
}
|
|
|
|
XMLNode&
|
|
AudioTrigger::get_state () const
|
|
{
|
|
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)
|
|
{
|
|
timepos_t t;
|
|
|
|
if (Trigger::set_state (node, version)) {
|
|
return -1;
|
|
}
|
|
|
|
node.get_property (X_("start"), t);
|
|
_start_offset = t.samples();
|
|
|
|
/* we've changed our internal values; we need to update our queued UIState or they will be lost when UIState is applied */
|
|
copy_to_ui_state ();
|
|
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
AudioTrigger::set_start (timepos_t const & s)
|
|
{
|
|
/* XXX better minimum size needed */
|
|
_start_offset = std::max (samplepos_t (4096), 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);
|
|
}
|
|
|
|
void
|
|
AudioTrigger::start_and_roll_to (samplepos_t start_pos, samplepos_t end_position)
|
|
{
|
|
Trigger::start_and_roll_to<AudioTrigger> (start_pos, end_position, *this, &AudioTrigger::audio_run<false>);
|
|
}
|
|
|
|
timepos_t
|
|
AudioTrigger::compute_end (Temporal::TempoMap::SharedPtr const & tmap, Temporal::BBT_Time const & transition_bbt, samplepos_t transition_sample, Temporal::Beats & effective_length)
|
|
{
|
|
/* Our task here is to set:
|
|
|
|
expected_end_sample: (TIMELINE!) the sample position where the data for the clip should run out (taking stretch into account)
|
|
last_readable_sample: the sample in the data where we stop reading
|
|
final_processed_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
|
|
_beatcnt : the expected duration of the trigger, based on analysis of its tempo .. can be overridden by the user later
|
|
*/
|
|
|
|
samplepos_t end_by_follow_length = tmap->sample_at (tmap->bbt_walk(transition_bbt, _follow_length));
|
|
samplepos_t end_by_data_length = transition_sample + (data.length - _start_offset);
|
|
/* this could still blow up if the data is less than 1 tick long, but
|
|
we should handle that elsewhere.
|
|
*/
|
|
const Temporal::Beats bc (Temporal::Beats::from_double (_beatcnt));
|
|
samplepos_t end_by_beatcnt = tmap->sample_at (tmap->bbt_walk(transition_bbt, Temporal::BBT_Offset (0, bc.get_beats(), bc.get_ticks())));
|
|
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 SO %9 @ %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_beatcnt, end_by_data_length, _start_offset));
|
|
|
|
if (stretching()) {
|
|
if (internal_use_follow_length()) {
|
|
expected_end_sample = std::min (end_by_follow_length, end_by_beatcnt);
|
|
} else {
|
|
expected_end_sample = end_by_beatcnt;
|
|
}
|
|
} 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_processed_sample = end_by_follow_length - transition_sample;
|
|
} else {
|
|
final_processed_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 = end_by_follow_length - transition_samples;
|
|
} else {
|
|
usable_length = (data.length - _start_offset);
|
|
}
|
|
|
|
/* called from compute_end() when we know the time (audio &
|
|
* musical time domains when we start starting. Our job here is to
|
|
* define the last_readable_sample we can use as data.
|
|
*/
|
|
|
|
Temporal::BBT_Offset q (_quantization);
|
|
|
|
if (launch_style() != Repeat || (q == Temporal::BBT_Offset())) {
|
|
|
|
last_readable_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_readable_sample = _start_offset + len.samples();
|
|
}
|
|
|
|
effective_length = tmap->quarters_at_sample (transition_sample + final_processed_sample) - tmap->quarters_at_sample (transition_sample);
|
|
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1: final sample %2 vs ees %3 ls %4\n", index(), final_processed_sample, expected_end_sample, last_readable_sample));
|
|
|
|
return timepos_t (expected_end_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 (); /* NOTE: if this is an existing clip (D+D copy) then it will likely have a SD tempo, and that short-circuits minibpm for us */
|
|
|
|
/* given an initial tempo guess, we need to set our operating tempo and beat_cnt value.
|
|
* this may be reset momentarily with user-settings (UIState) from a d+d operation */
|
|
set_segment_tempo(_estimated_tempo);
|
|
|
|
setup_stretcher ();
|
|
|
|
/* Given what we know about the tempo and duration, set the defaults
|
|
* for the trigger properties.
|
|
*/
|
|
|
|
if (_estimated_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 */
|
|
|
|
/* we've changed our internal values; we need to update our queued UIState or they will be lost when UIState is applied */
|
|
copy_to_ui_state ();
|
|
|
|
send_property_change (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.;
|
|
|
|
if (((bi = str.find (" bpm")) != string::npos) ||
|
|
((bi = str.find ("bpm")) != string::npos) ||
|
|
((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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (text_tempo < 0) {
|
|
|
|
breakfastquay::MiniBPM mbpm (_box.session().sample_rate());
|
|
|
|
_estimated_tempo = mbpm.estimateTempoOfSamples (data[0], data.length);
|
|
|
|
//cerr << name() << "MiniBPM Estimated: " << _estimated_tempo << " bpm from " << (double) data.length / _box.session().sample_rate() << " seconds\n";
|
|
}
|
|
}
|
|
|
|
const double seconds = (double) data.length / _box.session().sample_rate();
|
|
|
|
/* now check the determined tempo and force it to a value that gives us
|
|
an integer beat/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.)) {
|
|
/* fractional beatcnt */
|
|
double maybe_beats = (seconds / 60.) * _estimated_tempo;
|
|
double beatcount = round (maybe_beats);
|
|
|
|
/* the vast majority of third-party clips are 1,2,4,8, or 16-bar 'beats'.
|
|
* Given no other metadata, it makes things 'just work' if we assume 4/4 time signature, and power-of-2 bars (1,2,4,8 or 16)
|
|
* TODO: someday we could provide a widget for users who have unlabeled, un-metadata'd, clips that they *know* are 3/4 or 5/4 or 11/4 */
|
|
{
|
|
double barcount = round (beatcount/4);
|
|
if (barcount <= 18) { /* why not 16 here? fuzzy logic allows minibpm to misjudge the clip a bit */
|
|
for (int pwr = 0; pwr <= 4; pwr++) {
|
|
float bc = pow(2,pwr);
|
|
if (barcount <= bc) {
|
|
barcount = bc;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
beatcount = round(barcount * 4);
|
|
}
|
|
|
|
double est = _estimated_tempo;
|
|
_estimated_tempo = beatcount / (seconds/60.);
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("given original estimated tempo %1, rounded beatcnt is %2 : resulting in working bpm = %3\n", est, _beatcnt, _estimated_tempo));
|
|
|
|
/* initialize our follow_length to match the beatcnt ... user can later change this value to have the clip end sooner or later than its data length */
|
|
set_follow_length(Temporal::BBT_Offset( 0, rint(beatcount), 0));
|
|
}
|
|
|
|
#if 0
|
|
cerr << "estimated tempo: " << _estimated_tempo << endl;
|
|
const samplecnt_t one_beat = tm->bbt_duration_at (timepos_t (AudioTime), BBT_Offset (0, 1, 0)).samples();
|
|
cerr << "one beat in samples: " << one_beat << endl;
|
|
cerr << "rounded beatcount = " << round (beatcount) << endl;
|
|
#endif
|
|
}
|
|
|
|
bool
|
|
AudioTrigger::probably_oneshot () const
|
|
{
|
|
assert (_segment_tempo != 0.);
|
|
|
|
if ((data.length < (_box.session().sample_rate()/2)) || //less than 1/2 second
|
|
(_segment_tempo > 140) || //minibpm thinks this is really fast
|
|
(_segment_tempo < 60)) { //minibpm thinks this is really slow
|
|
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::reset_stretcher ()
|
|
{
|
|
_stretcher->reset ();
|
|
got_stretcher_padding = false;
|
|
to_pad = 0;
|
|
to_drop = 0;
|
|
}
|
|
|
|
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 ()
|
|
{
|
|
Trigger::retrigger ();
|
|
|
|
update_properties ();
|
|
reset_stretcher ();
|
|
|
|
read_index = _start_offset + _legato_offset;
|
|
retrieved = 0;
|
|
_legato_offset = 0; /* used one time only */
|
|
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 retriggered to %2\n", _index, read_index));
|
|
}
|
|
|
|
template<bool in_process_context>
|
|
pframes_t
|
|
AudioTrigger::audio_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, double bpm)
|
|
{
|
|
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 = (in_process_context ? std::min (bufs.count().n_audio(), ar->n_channels()) : ar->n_channels());
|
|
int avail = 0;
|
|
BufferSet* scratch;
|
|
std::unique_ptr<BufferSet> scratchp;
|
|
std::vector<Sample*> bufp(nchans);
|
|
const bool do_stretch = stretching() && _segment_tempo > 1;
|
|
|
|
/* 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);
|
|
const pframes_t orig_nframes = nframes;
|
|
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1/%2 after checking for transition, state = %3, will stretch %4, nf will be %5 of %6\n", index(), 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 WaitingToStop:
|
|
case WaitingToSwitch:
|
|
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.
|
|
*/
|
|
|
|
if (in_process_context) {
|
|
scratch = &(_box.session().get_scratch_buffers (ChanCount (DataType::AUDIO, nchans)));
|
|
} else {
|
|
scratchp.reset (new BufferSet ());
|
|
scratchp->ensure_buffers (DataType::AUDIO, nchans, nframes);
|
|
/* have to set up scratch as a raw ptr so that the in_process_context
|
|
and !in_process_context case can use the same code syntax
|
|
*/
|
|
scratch = scratchp.get();
|
|
}
|
|
|
|
for (uint32_t chn = 0; chn < nchans; ++chn) {
|
|
bufp[chn] = scratch->get_audio (chn).data();
|
|
}
|
|
|
|
/* tell the stretcher what we are doing for this ::run() call */
|
|
|
|
if (do_stretch && !_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) {
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1/%2 stretcher->available() returned %3 - not configured!\n", index(), name(), avail));
|
|
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) {
|
|
memset (bufp[chn], 0, sizeof (Sample) * limit);
|
|
}
|
|
|
|
_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 && !_playout) {
|
|
|
|
pframes_t to_stretcher;
|
|
pframes_t from_stretcher;
|
|
|
|
if (do_stretch) {
|
|
|
|
if (read_index < last_readable_sample) {
|
|
|
|
/* still have data to push into the stretcher */
|
|
|
|
to_stretcher = (pframes_t) std::min (samplecnt_t (rb_blocksize), (last_readable_sample - read_index));
|
|
const bool at_end = (to_stretcher < rb_blocksize);
|
|
|
|
while ((pframes_t) avail < nframes && (read_index < last_readable_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;
|
|
// cerr << "FS#1 from nframes = " << from_stretcher << endl;
|
|
} 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);
|
|
// cerr << "FS#X from avail " << avail << " nf " << nframes << " = " << from_stretcher << endl;
|
|
}
|
|
|
|
/* fetch the stretch */
|
|
|
|
retrieved += _stretcher->retrieve (&bufp[0], from_stretcher);
|
|
|
|
if (read_index >= last_readable_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_processed_sample, process_index));
|
|
|
|
if (transition_samples + retrieved > expected_end_sample) {
|
|
/* final pull from stretched data into output buffers */
|
|
// cerr << "FS#2 from ees " << final_processed_sample << " - " << process_index << " & " << from_stretcher;
|
|
from_stretcher = std::min ((samplecnt_t) from_stretcher, final_processed_sample - process_index);
|
|
// cerr << " => " << from_stretcher << endl;
|
|
|
|
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_processed_sample) {
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 reached (EX) end, entering playout mode to cover %2 .. %3\n", index(), process_index, final_processed_sample));
|
|
_playout = true;
|
|
} else {
|
|
_state = Stopped;
|
|
_loop_cnt++;
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 reached (EX) end, now stopped, retrieved %2, avail %3 pi %4 vs fs %5 LC now %6\n", index(), retrieved, avail, process_index, final_processed_sample, _loop_cnt));
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
} else {
|
|
/* no stretch */
|
|
from_stretcher = (pframes_t) std::min ((samplecnt_t) nframes, (last_readable_sample - read_index));
|
|
// cerr << "FS#3 from lrs " << last_readable_sample << " - " << read_index << " = " << from_stretcher << endl;
|
|
|
|
}
|
|
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 ready with %2 ri %3 ls %4, will write %5\n", name(), avail, read_index, last_readable_sample, from_stretcher));
|
|
|
|
/* deliver to buffers */
|
|
|
|
if (in_process_context) { /* constexpr, will be handled at compile time */
|
|
|
|
for (uint32_t chn = 0; chn < bufs.count().n_audio(); ++chn) {
|
|
|
|
uint32_t channel = chn % data.size();
|
|
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 (gain != 1.0f) {
|
|
buf.accumulate_with_gain_from (src, from_stretcher, gain, dest_offset);
|
|
} else {
|
|
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;
|
|
|
|
if (read_index >= last_readable_sample && (!do_stretch || avail <= 0)) {
|
|
|
|
if (process_index < final_processed_sample) {
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 reached end, entering playout mode to cover %2 .. %3\n", index(), process_index, final_processed_sample));
|
|
_playout = true;
|
|
} else {
|
|
_state = Stopped;
|
|
_loop_cnt++;
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 reached end, now stopped, retrieved %2, avail %3 LC now %4\n", index(), retrieved, avail, _loop_cnt));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
pframes_t covered_frames = orig_nframes - nframes;
|
|
|
|
if (_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_processed_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_processed_sample, process_index, transition_samples));
|
|
|
|
if (remaining_frames_till_final != 0) {
|
|
|
|
process_index += to_fill;
|
|
covered_frames += to_fill;
|
|
|
|
if (process_index < final_processed_sample) {
|
|
/* more playout to be done */
|
|
return covered_frames;
|
|
}
|
|
}
|
|
|
|
_state = Stopped;
|
|
_loop_cnt++;
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 playout finished, LC now %4\n", index(), _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)
|
|
{
|
|
_channel_map.assign (16, -1);
|
|
}
|
|
|
|
MIDITrigger::~MIDITrigger ()
|
|
{
|
|
}
|
|
|
|
void
|
|
MIDITrigger::set_used_channels (Evoral::SMF::UsedChannels used)
|
|
{
|
|
if (ui_state.used_channels != used) {
|
|
|
|
/* increment ui_state generation so vals will get loaded when the trigger stops */
|
|
unsigned int g = ui_state.generation.load();
|
|
while (!ui_state.generation.compare_exchange_strong (g, g+1));
|
|
|
|
ui_state.used_channels = used;
|
|
|
|
send_property_change (ARDOUR::Properties::used_channels);
|
|
_box.session().set_dirty();
|
|
}
|
|
}
|
|
|
|
void
|
|
MIDITrigger::set_channel_map (int channel, int target)
|
|
{
|
|
if (channel < 0 || channel >= 16) {
|
|
return;
|
|
}
|
|
|
|
if (target < 0 || target >= 16) {
|
|
return;
|
|
}
|
|
|
|
if (_channel_map[channel] != target) {
|
|
_channel_map[channel] = target;
|
|
send_property_change (Properties::channel_map);
|
|
}
|
|
}
|
|
|
|
void
|
|
MIDITrigger::unset_channel_map (int channel)
|
|
{
|
|
if (channel < 0 || channel >= 16) {
|
|
return;
|
|
}
|
|
|
|
if (_channel_map[channel] >= 0) {
|
|
_channel_map[channel] = -1;
|
|
send_property_change (Properties::channel_map);
|
|
}
|
|
}
|
|
|
|
int
|
|
MIDITrigger::channel_map (int channel)
|
|
{
|
|
if (channel < 0 || channel >= 16) {
|
|
return -1;
|
|
}
|
|
return _channel_map[channel];
|
|
}
|
|
|
|
void
|
|
MIDITrigger::set_patch_change (Evoral::PatchChange<MidiBuffer::TimeType> const & pc)
|
|
{
|
|
/* this must recreate the behavior of TRIGGER_SET, but it requires special handling because its an array */
|
|
/* specifically, we need to make sure and set the ui_state as well as the internal property, so the triggerbox won't overwrite these changes when it loads the trigger state */
|
|
assert (pc.is_set());
|
|
|
|
ui_state.patch_change[pc.channel()] = pc;
|
|
|
|
/* increment ui_state generation so vals will get loaded when the trigger stops */
|
|
unsigned int g = ui_state.generation.load();
|
|
while (!ui_state.generation.compare_exchange_strong (g, g+1));
|
|
|
|
send_property_change (Properties::patch_change);
|
|
}
|
|
|
|
void
|
|
MIDITrigger::unset_all_patch_changes ()
|
|
{
|
|
/* this must recreate the behavior of TRIGGER_SET, but it requires special handling because its an array */
|
|
/* specifically, we need to make sure and set the ui_state as well as the internal property, so the triggerbox won't overwrite these changes when it loads the trigger state */
|
|
for (uint8_t chn = 0; chn < 16; ++chn) {
|
|
if (ui_state.patch_change[chn].is_set ()) {
|
|
ui_state.patch_change[chn].unset ();
|
|
}
|
|
}
|
|
|
|
/* increment ui_state generation so vals will get loaded when the trigger stops */
|
|
unsigned int g = ui_state.generation.load();
|
|
while (!ui_state.generation.compare_exchange_strong (g, g+1));
|
|
|
|
send_property_change (Properties::patch_change);
|
|
}
|
|
|
|
void
|
|
MIDITrigger::unset_patch_change (uint8_t channel)
|
|
{
|
|
/* this must recreate the behavior of TRIGGER_SET_DIRECT, but it requires special handling because its an array */
|
|
/* specifically, we need to make sure and set the ui_state as well as the internal property, so the triggerbox won't overwrite these changes when it loads the trigger state */
|
|
assert (channel < 16);
|
|
|
|
/* increment ui_state generation so vals will get loaded when the trigger stops */
|
|
unsigned int g = ui_state.generation.load();
|
|
while (!ui_state.generation.compare_exchange_strong (g, g+1));
|
|
|
|
if (ui_state.patch_change[channel].is_set()) {
|
|
ui_state.patch_change[channel].unset ();
|
|
}
|
|
|
|
send_property_change (Properties::patch_change);
|
|
}
|
|
|
|
bool
|
|
MIDITrigger::patch_change_set (uint8_t channel) const
|
|
{
|
|
assert (channel < 16);
|
|
return ui_state.patch_change[channel].is_set();
|
|
}
|
|
|
|
Evoral::PatchChange<MidiBuffer::TimeType> const
|
|
MIDITrigger::patch_change (uint8_t channel) const
|
|
{
|
|
Evoral::PatchChange<MidiBuffer::TimeType> ret;
|
|
|
|
assert (channel < 16);
|
|
if (ui_state.patch_change[channel].is_set()) {
|
|
ret = ui_state.patch_change[channel];
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
bool
|
|
MIDITrigger::probably_oneshot () const
|
|
{
|
|
/* XXX fix for short chord stabs */
|
|
return false;
|
|
}
|
|
|
|
void
|
|
MIDITrigger::start_and_roll_to (samplepos_t start_pos, samplepos_t end_position)
|
|
{
|
|
Trigger::start_and_roll_to (start_pos, end_position, *this, &MIDITrigger::midi_run<false>);
|
|
}
|
|
|
|
timepos_t
|
|
MIDITrigger::compute_end (Temporal::TempoMap::SharedPtr const & tmap, Temporal::BBT_Time const & transition_bbt, samplepos_t, Temporal::Beats & effective_length)
|
|
{
|
|
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::BBT_Offset q (_quantization);
|
|
|
|
if (launch_style() != Repeat || (q == Temporal::BBT_Offset())) {
|
|
|
|
if (internal_use_follow_length()) {
|
|
final_beat = end_by_follow_length;
|
|
effective_length = tmap->bbtwalk_to_quarters (transition_bbt, _follow_length);
|
|
} else {
|
|
final_beat = end_by_data_length;
|
|
effective_length = tmap->bbtwalk_to_quarters (transition_bbt, Temporal::BBT_Offset (0, data_length.get_beats(), data_length.get_ticks()));
|
|
}
|
|
|
|
} else {
|
|
|
|
/* XXX MUST HANDLE BAR-LEVEL QUANTIZATION */
|
|
|
|
timecnt_t len (Temporal::Beats (q.beats, q.ticks), timepos_t (Temporal::Beats()));
|
|
final_beat = len.beats ();
|
|
}
|
|
|
|
timepos_t e (final_beat);
|
|
|
|
final_processed_sample = e.samples() - transition_samples;
|
|
|
|
return e;
|
|
}
|
|
|
|
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 (BufferSet& bufs, pframes_t dest_offset, Temporal::BBT_Offset const & start_quantization)
|
|
{
|
|
Trigger::_startup (bufs, dest_offset, start_quantization);
|
|
|
|
MidiBuffer* mb = 0;
|
|
|
|
if (bufs.count().n_midi() != 0) {
|
|
mb = &bufs.get_midi (0);
|
|
}
|
|
|
|
/* Possibly inject patch changes, if set */
|
|
|
|
for (int chn = 0; chn < 16; ++chn) {
|
|
if (_used_channels.test(chn) && allow_patch_changes() && _patch_change[chn].is_set()) {
|
|
_patch_change[chn].set_time (dest_offset);
|
|
DEBUG_TRACE (DEBUG::MidiTriggers, string_compose ("Injecting patch change c:%1 b:%2 p:%3\n", (uint32_t) _patch_change[chn].channel(), (uint32_t) _patch_change[chn].bank(), (uint32_t) _patch_change[chn].program()));
|
|
for (int msg = 0; msg < _patch_change[chn].messages(); ++msg) {
|
|
if (mb) {
|
|
mb->insert_event (_patch_change[chn].message (msg));
|
|
_box.tracker->track (_patch_change[chn].message (msg).buffer());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MIDITrigger::jump_start ()
|
|
{
|
|
Trigger::jump_start ();
|
|
retrigger ();
|
|
}
|
|
|
|
void
|
|
MIDITrigger::shutdown (BufferSet& bufs, pframes_t dest_offset)
|
|
{
|
|
Trigger::shutdown (bufs, dest_offset);
|
|
|
|
if (bufs.count().n_midi()) {
|
|
MidiBuffer& mb (bufs.get_midi (0));
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 shutdown, resolve notes @ %2\n", index(), dest_offset));
|
|
_box.tracker->resolve_notes (mb, dest_offset);
|
|
}
|
|
|
|
_box.tracker->reset ();
|
|
}
|
|
|
|
void
|
|
MIDITrigger::jump_stop (BufferSet& bufs, pframes_t dest_offset)
|
|
{
|
|
Trigger::jump_stop (bufs, dest_offset);
|
|
|
|
MidiBuffer& mb (bufs.get_midi (0));
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 jump stop, resolve notes @ %2\n", index(), dest_offset));
|
|
_box.tracker->resolve_notes (mb, dest_offset);
|
|
|
|
retrigger ();
|
|
}
|
|
|
|
XMLNode&
|
|
MIDITrigger::get_state () const
|
|
{
|
|
XMLNode& node (Trigger::get_state());
|
|
|
|
node.set_property (X_("start"), start_offset());
|
|
|
|
std::string uchan = string_compose ("%1", _used_channels.to_ulong());
|
|
node.set_property (X_("used-channels"), uchan);
|
|
|
|
XMLNode* patches_node = 0;
|
|
|
|
for (int chn = 0; chn < 16; ++chn) {
|
|
if (_patch_change[chn].is_set()) {
|
|
if (!patches_node) {
|
|
patches_node = new XMLNode (X_("PatchChanges"));
|
|
}
|
|
XMLNode* patch_node = new XMLNode (X_("PatchChange"));
|
|
patch_node->set_property (X_("channel"), _patch_change[chn].channel());
|
|
patch_node->set_property (X_("bank"), _patch_change[chn].bank());
|
|
patch_node->set_property (X_("program"), _patch_change[chn].program());
|
|
|
|
patches_node->add_child_nocopy (*patch_node);
|
|
}
|
|
}
|
|
|
|
if (patches_node) {
|
|
node.add_child_nocopy (*patches_node);
|
|
}
|
|
|
|
std::string cmstr;
|
|
|
|
for (int chn = 0; chn < 16; ++chn) {
|
|
char buf[4];
|
|
|
|
if (chn > 0) {
|
|
cmstr += ',';
|
|
}
|
|
|
|
snprintf (buf, sizeof (buf), "%d", _channel_map[chn]);
|
|
cmstr += buf;
|
|
}
|
|
|
|
node.set_property (X_("channel-map"), cmstr);
|
|
|
|
return node;
|
|
}
|
|
|
|
int
|
|
MIDITrigger::set_state (const XMLNode& node, int version)
|
|
{
|
|
timepos_t t;
|
|
|
|
if (Trigger::set_state (node, version)) {
|
|
return -1;
|
|
}
|
|
|
|
std::string uchan;
|
|
if (node.get_property (X_("used-channels"), uchan)) {
|
|
} else {
|
|
unsigned long ul;
|
|
std::stringstream ss (uchan);
|
|
ss >> ul;
|
|
if (!ss) {
|
|
return -1;
|
|
}
|
|
set_used_channels( Evoral::SMF::UsedChannels(ul) );
|
|
}
|
|
|
|
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());
|
|
|
|
XMLNode* patches_node = node.child (X_("PatchChanges"));
|
|
|
|
if (patches_node) {
|
|
XMLNodeList const & children = patches_node->children();
|
|
for (XMLNodeList::const_iterator i = children.begin(); i != children.end(); ++i) {
|
|
if ((*i)->name() == X_("PatchChange")) {
|
|
int c, p, b;
|
|
if ((*i)->get_property (X_("channel"), c) &&
|
|
(*i)->get_property (X_("program"), p) &&
|
|
(*i)->get_property (X_("bank"), b)) {
|
|
_patch_change[c] = Evoral::PatchChange<MidiBuffer::TimeType> (0, c, p, b);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string cmstr;
|
|
|
|
if (node.get_property (X_("channel-map"), cmstr)) {
|
|
std::stringstream ss (cmstr);
|
|
char comma;
|
|
for (int chn = 0; chn < 16; ++chn) {
|
|
ss >> _channel_map[chn];
|
|
if (!ss) {
|
|
break;
|
|
}
|
|
ss >> comma;
|
|
if (!ss) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* we've changed our internal values; we need to update our queued UIState or they will be lost when UIState is applied */
|
|
copy_to_ui_state ();
|
|
|
|
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));
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
void
|
|
MIDITrigger::estimate_midi_patches ()
|
|
{
|
|
/* first, initialize all our slot's patches to GM defaults, to make playback deterministic */
|
|
for (uint8_t chan = 0; chan < 16; ++chan) {
|
|
_patch_change[chan].set_channel(chan);
|
|
_patch_change[chan].set_bank( chan == 9 ? 120 : 0 );
|
|
_patch_change[chan].set_program( 0 );
|
|
}
|
|
|
|
boost::shared_ptr<SMFSource> smfs = boost::dynamic_pointer_cast<SMFSource> (_region->source(0));
|
|
if (smfs) {
|
|
/* second, apply any patches that the Auditioner has in its memory
|
|
* ...this handles the case where the user chose patches for a file that itself lacked patch-settings
|
|
* (it's possible that the user didn't audition the actual file they dragged in, but this is still the best starting-point we have)
|
|
* */
|
|
boost::shared_ptr<ARDOUR::Auditioner> aud = _box.session().the_auditioner();
|
|
if (aud) {
|
|
for (uint8_t chan = 0; chan < 16; ++chan) {
|
|
if (aud->patch_change (chan).is_set()) {
|
|
_patch_change[chan] = aud->patch_change (chan);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* thirdly, apply the patches from the file itself (if it has any) */
|
|
boost::shared_ptr<MidiModel> model = smfs->model();
|
|
for (MidiModel::PatchChanges::const_iterator i = model->patch_changes().begin(); i != model->patch_changes().end(); ++i) {
|
|
if ((*i)->is_set()) {
|
|
int chan = (*i)->channel(); /* behavior is undefined for SMF's with multiple patch changes. I'm not sure that we care */
|
|
_patch_change[chan].set_channel ((*i)->channel());
|
|
_patch_change[chan].set_bank((*i)->bank());
|
|
_patch_change[chan].set_program((*i)->program());
|
|
}
|
|
}
|
|
|
|
/* finally, store the used_channels so the UI can show patches only for those chans actually used */
|
|
DEBUG_TRACE (DEBUG::MidiTriggers, string_compose ("%1 estimate_midi_patches(), using channels %2\n", name(), smfs->used_channels().to_string().c_str()));
|
|
_used_channels = smfs->used_channels();
|
|
}
|
|
|
|
//we've changed some of our internal values; the calling code must call copy_to_ui_state ... ::set_region_in_worker_thread does it
|
|
|
|
}
|
|
|
|
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();
|
|
_follow_length = Temporal::BBT_Offset (0, data_length.get_beats(), 0);
|
|
set_length (mr->length());
|
|
model = mr->model ();
|
|
|
|
estimate_midi_patches ();
|
|
|
|
/* we've changed some of our internal values; we need to update our queued UIState or they will be lost when UIState is applied */
|
|
copy_to_ui_state ();
|
|
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 loaded midi region, span is %2\n", name(), data_length));
|
|
|
|
send_property_change (ARDOUR::Properties::name);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
MIDITrigger::retrigger ()
|
|
{
|
|
Trigger::retrigger ();
|
|
|
|
update_properties ();
|
|
|
|
/* 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*)
|
|
{
|
|
}
|
|
|
|
template<bool in_process_context>
|
|
pframes_t
|
|
MIDITrigger::midi_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, double bpm)
|
|
{
|
|
MidiBuffer* mb (in_process_context? &bufs.get_midi (0) : 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 */
|
|
pframes_t ignore_computed_dest_offset = 0;
|
|
maybe_compute_next_transition (start_sample, start_beats, end_beats, nframes, ignore_computed_dest_offset);
|
|
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 WaitingToStop:
|
|
case WaitingToSwitch:
|
|
case Stopping:
|
|
break;
|
|
}
|
|
|
|
Temporal::Beats last_event_timeline_beats = final_beat; /* will indicate "done" if there is nothing to do */
|
|
|
|
while (iter != model->end() && !_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);
|
|
|
|
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 with %4\n", index(), maybe_last_event_timeline_beats, final_beat, event));
|
|
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;
|
|
}
|
|
|
|
if (in_process_context) { /* compile-time const expr */
|
|
|
|
/* Now we have to convert to a position within the buffer we
|
|
* are writing to.
|
|
*
|
|
* (timeline_samples - start_sample) gives us the
|
|
* sample offset from the start of our run() call. But
|
|
* since we may be executing after another trigger in
|
|
* the same process() cycle, we must take dest_offset
|
|
* into account to get an actual buffer position.
|
|
*/
|
|
|
|
samplepos_t buffer_samples = (timeline_samples - start_sample) + dest_offset;
|
|
|
|
Evoral::Event<MidiBuffer::TimeType> ev (Evoral::MIDI_EVENT, buffer_samples, event.size(), const_cast<uint8_t*>(event.buffer()), false);
|
|
|
|
if (_gain != 1.0f && ev.is_note()) {
|
|
ev.scale_velocity (_gain);
|
|
}
|
|
|
|
int chn = ev.channel();
|
|
|
|
if (_channel_map[ev.channel()] > 0) {
|
|
ev.set_channel (_channel_map[chn]);
|
|
}
|
|
|
|
if (ev.is_pgm_change() || (ev.is_cc() && ((ev.cc_number() == MIDI_CTL_LSB_BANK) || (ev.cc_number() == MIDI_CTL_MSB_BANK)))) {
|
|
if (!allow_patch_changes ()) {
|
|
/* do not send ANY patch or bank messages, just skip them */
|
|
DEBUG_TRACE (DEBUG::MidiTriggers, string_compose ("Ignoring patch change on chn:%1\n", (uint32_t) _patch_change[chn].channel()));
|
|
++iter;
|
|
continue;
|
|
} else if ( _patch_change[chn].is_set() ) {
|
|
/* from this context we don't know if a pgm message in the midi buffer is from the file or from triggerbox */
|
|
/* so when a bank or pgm message is recognized, just replace it with the desired patch */
|
|
DEBUG_TRACE (DEBUG::MidiTriggers, string_compose ("Replacing patch change c:%1 b:%2 p:%3\n", (uint32_t) _patch_change[chn].channel(), (uint32_t) _patch_change[chn].bank(), (uint32_t) _patch_change[chn].program()));
|
|
if (ev.is_cc() && (ev.cc_number() == MIDI_CTL_MSB_BANK)) {
|
|
ev.set_cc_value(_patch_change[chn].bank_msb());
|
|
} else if (ev.is_cc() && (ev.cc_number() == MIDI_CTL_LSB_BANK)) {
|
|
ev.set_cc_value(_patch_change[chn].bank_lsb());
|
|
} else if (ev.is_pgm_change()) {
|
|
ev.set_pgm_number(_patch_change[chn].program());
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
_box.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 (in_process_context && _state == Stopping) { /* first clause is a compile-time constexpr */
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 was stopping, now stopped, resolving notes @ %2\n", index(), nframes-1));
|
|
_box.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));
|
|
|
|
/* "final_beat" is an inclusive end of the trigger, not
|
|
* exclusive, so we must use <= here. That is, any last event
|
|
* (remember, iter == model->end() here, so we have already read
|
|
* through the entire MIDI model) that is up to AND INCLUDING
|
|
* final_beat counts as "haven't reached the end".
|
|
*/
|
|
|
|
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));
|
|
|
|
_playout = true;
|
|
|
|
if (final_beat > end_beats) {
|
|
/* no more events to come before final_beat,
|
|
* and that is beyond the end of this ::run()
|
|
* call. Not finished with playout yet, but
|
|
* 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_processed_sample = tmap->sample_at (timepos_t (final_beat));
|
|
nframes = orig_nframes - (final_processed_sample - start_sample);
|
|
_loop_cnt++;
|
|
_state = Stopped;
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 playout done, nf = %2 fb %3 fs %4 %5 LC %6\n", index(), nframes, final_beat, final_processed_sample, start_sample, _loop_cnt));
|
|
}
|
|
|
|
} else {
|
|
|
|
samplepos_t final_processed_sample = tmap->sample_at (timepos_t (final_beat));
|
|
nframes = orig_nframes - (final_processed_sample - start_sample);
|
|
_loop_cnt++;
|
|
_state = Stopped;
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 reached final event, now stopped, nf = %2 fb %3 fs %4 %5 LC %6\n", index(), nframes, final_beat, final_processed_sample, start_sample, _loop_cnt));
|
|
}
|
|
|
|
} 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, next event is %3\n", index(), nframes, *iter));
|
|
nframes = 0;
|
|
}
|
|
|
|
const samplecnt_t covered_frames = orig_nframes - nframes;
|
|
|
|
if (_state == Stopped || _state == Stopping) {
|
|
when_stopped_during_run (bufs, dest_offset + covered_frames);
|
|
}
|
|
|
|
process_index += covered_frames;
|
|
|
|
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::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));
|
|
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::cue_isolated.property_id = g_quark_from_static_string (X_("cue_isolated"));
|
|
DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for cue_isolated = %1\n", Properties::cue_isolated.property_id));
|
|
Properties::allow_patch_changes.property_id = g_quark_from_static_string (X_("allow_patch_changes"));
|
|
DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for allow_patch_changes = %1\n", Properties::allow_patch_changes.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));
|
|
Properties::patch_change.property_id = g_quark_from_static_string (X_("patch_change"));
|
|
DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for patch_change = %1\n", Properties::patch_change.property_id));
|
|
Properties::channel_map.property_id = g_quark_from_static_string (X_("channel_map"));
|
|
DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for channel_map = %1\n", Properties::channel_map.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;
|
|
|
|
typedef std::map <boost::shared_ptr<Region>, boost::shared_ptr<Trigger::UIState>> RegionStateMap;
|
|
RegionStateMap enqueued_state_map;
|
|
|
|
void
|
|
TriggerBox::init ()
|
|
{
|
|
worker = new TriggerBoxThread;
|
|
TriggerBoxThread::init_request_pool ();
|
|
init_pool ();
|
|
}
|
|
|
|
TriggerBox::TriggerBox (Session& s, DataType dt)
|
|
: Processor (s, _("TriggerBox"), Temporal::BeatTime)
|
|
, tracker (dt == DataType::MIDI ? new MidiStateTracker : 0)
|
|
, _data_type (dt)
|
|
, _order (-1)
|
|
, explicit_queue (64)
|
|
, _currently_playing (0)
|
|
, _stop_all (false)
|
|
, _active_scene (-1)
|
|
, _active_slots (0)
|
|
, _locate_armed (false)
|
|
, _cancel_locate_armed (false)
|
|
, _fast_forwarding (false)
|
|
|
|
, 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));
|
|
_session.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::parameter_changed (std::string const & param)
|
|
{
|
|
if (param == X_("default-trigger-input-port")) {
|
|
|
|
reconnect_to_default ();
|
|
|
|
} else if (param == "cue-behavior") {
|
|
const bool follow = (_session.config.get_cue_behavior() & FollowCues);
|
|
if (!follow) {
|
|
cancel_locate_armed ();
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
TriggerBox::cancel_locate_armed ()
|
|
{
|
|
_cancel_locate_armed = true;
|
|
}
|
|
|
|
void
|
|
TriggerBox::fast_forward (CueEvents const & cues, samplepos_t transport_position)
|
|
{
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1: ffwd to %2\n", order(), transport_position));
|
|
|
|
if (!(_session.config.get_cue_behavior() & FollowCues)) {
|
|
/* do absolutely nothing */
|
|
return;
|
|
}
|
|
|
|
if (cues.empty() || (cues.front().time > transport_position)) {
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1: nothing to be done, cp = %2\n", order(), _currently_playing));
|
|
cancel_locate_armed ();
|
|
if (tracker) {
|
|
tracker->reset ();
|
|
}
|
|
return;
|
|
}
|
|
|
|
PBD::Unwinder<bool> uw (_fast_forwarding, true);
|
|
|
|
using namespace Temporal;
|
|
TempoMap::SharedPtr tmap (TempoMap::use());
|
|
|
|
CueEvents::const_iterator c = cues.begin();
|
|
samplepos_t pos = c->time;
|
|
TriggerPtr prev;
|
|
Temporal::BBT_Time start_bbt;
|
|
samplepos_t start_samples;
|
|
Temporal::Beats elen;
|
|
|
|
while (pos < transport_position && c != cues.end() && c->time < transport_position) {
|
|
|
|
CueEvents::const_iterator nxt_cue = c; ++nxt_cue;
|
|
|
|
if (c->cue == INT32_MAX) {
|
|
/* "stop all cues" marker encountered. This ends the
|
|
duration of whatever slot might have been running
|
|
when we hit the cue.
|
|
*/
|
|
prev.reset ();
|
|
c = nxt_cue;
|
|
continue;
|
|
}
|
|
|
|
TriggerPtr trig (all_triggers[c->cue]);
|
|
|
|
if (trig->cue_isolated()) {
|
|
c = nxt_cue;
|
|
pos = c->time;
|
|
continue;
|
|
}
|
|
|
|
if (!trig->region()) {
|
|
/* the cue-identified slot is empty for this
|
|
triggerbox. This effectively ends the duration of
|
|
whatever slot might have been running when we hit
|
|
the cue.
|
|
*/
|
|
prev.reset ();
|
|
c = nxt_cue;
|
|
pos = c->time;
|
|
continue;
|
|
}
|
|
|
|
samplepos_t limit;
|
|
|
|
if (nxt_cue == cues.end()) {
|
|
limit = transport_position;
|
|
} else {
|
|
limit = nxt_cue->time;
|
|
}
|
|
|
|
bool will_start = true;
|
|
|
|
start_bbt = trig->compute_start (tmap, pos, limit, trig->quantization(), start_samples, will_start);
|
|
|
|
if (!will_start) {
|
|
/* trigger will not start between this cue and the next */
|
|
c = nxt_cue;
|
|
pos = limit;
|
|
continue;
|
|
}
|
|
|
|
/* we now consider this trigger to be running. Let's see when
|
|
* it ends...
|
|
*/
|
|
|
|
samplepos_t trig_ends_at = trig->compute_end (tmap, start_bbt, start_samples, elen).samples();
|
|
|
|
if (nxt_cue != cues.end() && trig_ends_at >= nxt_cue->time) {
|
|
/* trigger will be interrupted by next cue .
|
|
*
|
|
*/
|
|
trig_ends_at = tmap->sample_at (tmap->bbt_at (timepos_t (nxt_cue->time)).round_up_to_bar ());
|
|
}
|
|
|
|
if (trig_ends_at >= transport_position) {
|
|
prev = trig;
|
|
/* we're done. prev now indicates the trigger that
|
|
would have started most recently before the
|
|
transport position.
|
|
*/
|
|
break;
|
|
}
|
|
|
|
int dnt = determine_next_trigger (trig->index());
|
|
|
|
if (dnt < 0) {
|
|
/* no trigger follows the current one. Back to
|
|
looking for another cue.
|
|
*/
|
|
c = nxt_cue;
|
|
continue;
|
|
}
|
|
|
|
prev = trig;
|
|
pos = trig_ends_at;
|
|
trig = all_triggers[dnt];
|
|
c = nxt_cue;
|
|
}
|
|
|
|
if (pos >= transport_position || !prev) {
|
|
/* nothing to do */
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1: no trigger to be rolled (%2 >= %3, prev = %4)\n", order(), pos, transport_position, prev));
|
|
_currently_playing = 0;
|
|
_locate_armed = false;
|
|
if (tracker) {
|
|
tracker->reset ();
|
|
}
|
|
return;
|
|
}
|
|
|
|
/* prev now points to a trigger that would start before
|
|
* transport_position and would still be running at
|
|
* transport_position. We need to run it in a special mode that ensures
|
|
* that
|
|
*
|
|
* 1) for MIDI, we know the state at transport position
|
|
* 2) for audio, the stretcher is in the correct state
|
|
*/
|
|
|
|
|
|
/* find the closest start (retrigger) position for this trigger */
|
|
|
|
if (start_samples < transport_position) {
|
|
samplepos_t s = start_samples;
|
|
BBT_Time ns = start_bbt;
|
|
|
|
do {
|
|
start_samples = s;
|
|
ns = tmap->bbt_walk (ns, BBT_Offset (0, elen.get_beats(), elen.get_ticks()));
|
|
s = tmap->sample_at (ns);
|
|
} while (s < transport_position);
|
|
}
|
|
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1: roll trigger %2 from %3 to %4\n", order(), prev->index(), start_samples, transport_position));
|
|
prev->start_and_roll_to (start_samples, transport_position);
|
|
|
|
_currently_playing = prev;
|
|
_locate_armed = true;
|
|
/* currently playing is now ready to keep running at transport position
|
|
*
|
|
* Note that a MIDITrigger will have set a flag so that when we call
|
|
* ::run() again, it will dump its current MIDI state before anything
|
|
* else.
|
|
*/
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/* set_region_in_worker_thread estimates a tempo, and makes some guesses about whether a clip is a one-shot or looping*/
|
|
t->set_region_in_worker_thread (region);
|
|
|
|
/* if we are the target of a drag&drop from another Trigger Slot, we need the name, color and other properties to carry over with the region */
|
|
RegionStateMap::iterator rs;
|
|
if ((rs = enqueued_state_map.find (region)) != enqueued_state_map.end()) {
|
|
Trigger::UIState copy; copy = *(rs->second);
|
|
t->set_ui_state(*(rs->second));
|
|
enqueued_state_map.erase(rs);
|
|
}
|
|
|
|
//* always preserve the launch-style and cue_isolate status. It's likely to be right, but if it's wrong the user can "see" it's wrong anyway */
|
|
t->set_launch_style(all_triggers[slot]->launch_style());
|
|
t->set_cue_isolated(all_triggers[slot]->cue_isolated());
|
|
|
|
//* if the existing slot seems to be part of a FA 'arrangement', preserve the settings */
|
|
if (all_triggers[slot]->follow_action0().is_arrangement()) {
|
|
t->set_follow_action0(all_triggers[slot]->follow_action0());
|
|
t->set_follow_action1(all_triggers[slot]->follow_action1());
|
|
t->set_follow_action_probability(all_triggers[slot]->follow_action_probability());
|
|
t->set_quantization(all_triggers[slot]->quantization());
|
|
//color ?
|
|
|
|
t->set_follow_count(all_triggers[slot]->follow_count());
|
|
t->set_follow_length(all_triggers[slot]->follow_length());
|
|
t->set_use_follow_length(all_triggers[slot]->use_follow_length());
|
|
}
|
|
|
|
/* 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;
|
|
bool empty_changed = false;
|
|
|
|
p = all_triggers[slot]->swap_pending (p);
|
|
|
|
if (p) {
|
|
|
|
if (p == Trigger::MagicClearPointerValue) {
|
|
if (all_triggers[slot]->region()) {
|
|
if (_active_slots) {
|
|
_active_slots--;
|
|
}
|
|
if (_active_slots == 0) {
|
|
empty_changed = true;
|
|
}
|
|
}
|
|
all_triggers[slot]->clear_region ();
|
|
} else {
|
|
if (!all_triggers[slot]->region()) {
|
|
if (_active_slots == 0) {
|
|
empty_changed = true;
|
|
}
|
|
_active_slots++;
|
|
}
|
|
/* 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 */
|
|
}
|
|
}
|
|
|
|
if (empty_changed) {
|
|
EmptyStatusChanged (); /* 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;
|
|
}
|
|
|
|
TriggerPtr
|
|
TriggerBox::trigger_by_id (PBD::ID check)
|
|
{
|
|
for (uint64_t n = 0; n < all_triggers.size(); ++n) {
|
|
if (trigger (n)->id() == check) {
|
|
return trigger (n);
|
|
}
|
|
}
|
|
return TriggerPtr();
|
|
}
|
|
|
|
void
|
|
TriggerBox::enqueue_trigger_state_for_region (boost::shared_ptr<Region> region, boost::shared_ptr<Trigger::UIState> state)
|
|
{
|
|
enqueued_state_map.insert (std::make_pair(region, state));
|
|
}
|
|
|
|
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) {
|
|
if (fa_n == 0) {
|
|
all_triggers[n]->set_follow_action0 (fa);
|
|
} else {
|
|
all_triggers[n]->set_follow_action1 (fa);
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
if (!Config->get_default_trigger_input_port().empty ()) {
|
|
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 + ChanCount (DataType::MIDI, 1));
|
|
}
|
|
|
|
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 buffer for triggers. This is always the last
|
|
* MIDI buffer of the BufferSet
|
|
*/
|
|
|
|
MidiBuffer& mb (bufs.get_midi (bufs.count().n_midi() - 1 /* due to zero-based index*/));
|
|
|
|
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->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->velocity_effect() * (*ev).velocity() / 127.f));
|
|
}
|
|
t->bang ();
|
|
|
|
} else if ((*ev).is_note_off()) {
|
|
|
|
t->unbang ();
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (_session.transport_locating()) {
|
|
/* nothing to do here at all. We do not run triggers while
|
|
locate is still taking place.
|
|
*/
|
|
return;
|
|
}
|
|
|
|
#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 nf %7\n", start_sample, end_sample, __start_beats, __end_beats, __bpm, order(), nframes));
|
|
}
|
|
#endif
|
|
|
|
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;
|
|
}
|
|
|
|
/* 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);
|
|
}
|
|
|
|
bool was_recorded;
|
|
int32_t cue_bang = _session.first_cue_within (start_sample, end_sample, was_recorded);
|
|
|
|
if (!_cue_recording || !was_recorded) {
|
|
|
|
if (cue_bang == INT32_MAX) {
|
|
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 sees STOP ALL!\n", order()));
|
|
|
|
/* reached a "stop all cue-launched cues from playing"
|
|
* marker.The stop is quantized, not immediate.
|
|
*/
|
|
|
|
if (_currently_playing) {
|
|
_currently_playing->unbang ();
|
|
}
|
|
|
|
_locate_armed = 0;
|
|
|
|
} else if (cue_bang >= 0) {
|
|
_active_scene = cue_bang;
|
|
_locate_armed = 0;
|
|
}
|
|
}
|
|
|
|
/* 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]->cue_isolated()) {
|
|
if (all_triggers[_active_scene]->region()) {
|
|
all_triggers[_active_scene]->bang ();
|
|
} else {
|
|
stop_all_quantized (); //empty slot, this should work as a Stop for the running clips
|
|
//TODO: can we set a flag so the UI reports that we are stopping?
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 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 (auto & trig : all_triggers) {
|
|
trig->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 (bufs, 0);
|
|
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 ();
|
|
}
|
|
|
|
if (_locate_armed && _cancel_locate_armed) {
|
|
if (_currently_playing) {
|
|
_currently_playing->shutdown (bufs, 0);
|
|
_currently_playing = 0;
|
|
PropertyChanged (Properties::currently_playing);
|
|
}
|
|
|
|
_cancel_locate_armed = false;
|
|
|
|
} else if (!_locate_armed) {
|
|
|
|
_cancel_locate_armed = false;
|
|
}
|
|
|
|
/* 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;
|
|
|
|
/* nobody is active, but we should catch up on changes
|
|
* requested by the UI
|
|
*/
|
|
|
|
for (auto & trig : all_triggers) {
|
|
trig->update_properties();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
/* some trigger is active, but the others should catch up on changes
|
|
* requested by the UI
|
|
*/
|
|
|
|
for (auto & trig : all_triggers) {
|
|
if (trig != _currently_playing) {
|
|
trig->update_properties();
|
|
}
|
|
}
|
|
|
|
/* transport must be active for triggers */
|
|
|
|
if (!_locate_armed) {
|
|
if (!_session.transport_state_rolling() && !allstop) {
|
|
_session.start_transport_from_trigger ();
|
|
}
|
|
} else {
|
|
|
|
/* _locate_armed is true, so _currently_playing has been
|
|
fast-forwarded to our position, and is ready to
|
|
play. However, for MIDI triggers, we may need to dump a
|
|
bunch of state into our BufferSet to ensure that the state
|
|
of things matches the way it would have been had we actually
|
|
played the trigger/slot from the start.
|
|
*/
|
|
|
|
if (_session.transport_state_rolling()) {
|
|
if (tracker && bufs.count().n_midi()) {
|
|
tracker->flush (bufs.get_midi (0), 0, true);
|
|
}
|
|
_locate_armed = false;
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* 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;
|
|
|
|
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;
|
|
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 (bufs, dest_offset);
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 was finished, started %2\n", _currently_playing->index(), nxt->index()));
|
|
_currently_playing = nxt;
|
|
PropertyChanged (Properties::currently_playing);
|
|
|
|
} else if (_currently_playing->state() != Trigger::WaitingToSwitch) {
|
|
|
|
/* Notice that this condition
|
|
* leaves the next trigger to
|
|
* run in the queue.
|
|
*/
|
|
|
|
/* but just begin stoppingthe currently playing slot */
|
|
_currently_playing->begin_switch (nxt);
|
|
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;
|
|
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 (bufs, dest_offset, start_quantization);
|
|
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, bpm);
|
|
|
|
nframes -= frames_covered;
|
|
start_sample += frames_covered;
|
|
dest_offset += frames_covered;
|
|
|
|
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 (!_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_action0 ().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)
|
|
*/
|
|
|
|
int r = _pcg.rand (100); // 0 .. 99
|
|
FollowAction fa;
|
|
|
|
if (r >= all_triggers[current]->follow_action_probability()) {
|
|
fa = all_triggers[current]->follow_action0 ();
|
|
} else {
|
|
fa = all_triggers[current]->follow_action1 ();
|
|
}
|
|
|
|
/* 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;
|
|
|
|
case FollowAction::Again:
|
|
return current;
|
|
|
|
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:
|
|
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 () const
|
|
{
|
|
XMLNode& node (Processor::get_state ());
|
|
|
|
node.set_property (X_("type"), X_("triggerbox"));
|
|
node.set_property (X_("data-type"), _data_type.to_string());
|
|
node.set_property (X_("order"), _order);
|
|
XMLNode* trigger_child (new XMLNode (X_("Triggers")));
|
|
|
|
{
|
|
Glib::Threads::RWLock::ReaderLock lm (trigger_lock);
|
|
for (auto const & t : all_triggers) {
|
|
trigger_child->add_child_nocopy (t->get_state());
|
|
}
|
|
}
|
|
|
|
node.add_child_nocopy (*trigger_child);
|
|
|
|
if (_sidechain) {
|
|
node.add_child_nocopy (_sidechain->get_state ());
|
|
}
|
|
|
|
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);
|
|
} else if (_data_type == DataType::MIDI) {
|
|
trig.reset (new MIDITrigger (all_triggers.size(), *this), Trigger::request_trigger_delete);
|
|
all_triggers.push_back (trig);
|
|
trig->set_state (**t, version);
|
|
}
|
|
if (trig->region ()) {
|
|
_active_slots++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* sidechain is a Processor (IO) */
|
|
XMLNode* scnode = node.child (Processor::state_node_name.c_str ());
|
|
if (scnode) {
|
|
add_midi_sidechain ();
|
|
assert (_sidechain);
|
|
if (!regenerate_xml_or_string_ids ()) {
|
|
_sidechain->set_state (*scnode, version);
|
|
} else {
|
|
update_sidechain_name ();
|
|
}
|
|
}
|
|
|
|
/* Since _active_slots may have changed, we could consider sending
|
|
* EmptyStatusChanged, but for now we don't consider ::set_state() to
|
|
* be used except at session load.
|
|
*/
|
|
|
|
return 0;
|
|
}
|
|
|
|
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:
|
|
break;
|
|
case Request::Reload:
|
|
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;
|
|
}
|
|
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 ();
|
|
}
|
|
|
|
void
|
|
TriggerBox::realtime_handle_transport_stopped ()
|
|
{
|
|
Processor::realtime_handle_transport_stopped ();
|
|
stop_all ();
|
|
_currently_playing = 0;
|
|
}
|
|
|
|
void
|
|
TriggerBox::non_realtime_transport_stop (samplepos_t now, bool /*flush*/)
|
|
{
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 (%3): non-realtime stop at %2 (lat-adjusted to %4\n", order(), now, this, now + playback_offset()));
|
|
|
|
for (auto & t : all_triggers) {
|
|
t->shutdown_from_fwd ();
|
|
}
|
|
|
|
if (now) {
|
|
now += playback_offset();
|
|
}
|
|
|
|
fast_forward (_session.cue_events(), now);
|
|
}
|
|
|
|
void
|
|
TriggerBox::non_realtime_locate (samplepos_t now)
|
|
{
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 (%3): non-realtime locate at %2 (lat-adjusted to %4\n", order(), now, this, now + playback_offset()));
|
|
|
|
for (auto & t : all_triggers) {
|
|
t->shutdown_from_fwd ();
|
|
}
|
|
|
|
if (now) {
|
|
now += playback_offset();
|
|
}
|
|
|
|
fast_forward (_session.cue_events(), now);
|
|
}
|
|
|
|
void
|
|
TriggerBox::dump (std::ostream & ostr) const
|
|
{
|
|
ostr << "TriggerBox " << order() << std::endl;
|
|
for (auto const & t : all_triggers) {
|
|
ostr << "\tTrigger " << t->index() << " state " << enum_2_string (t->state()) << std::endl;
|
|
}
|
|
}
|
|
|
|
/* 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 ()
|
|
{
|
|
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;
|
|
}
|