Somewhat unbelievably, macOS defines size_t as unsigned long, which the compiler there believes is distinct from unsigned long long even then they have the same bitwidth and signedness. We don't have any string converters for unsigned long, only uint64_t (unsigned long long), so various things break.
1475 lines
32 KiB
C++
1475 lines
32 KiB
C++
#include <iostream>
|
|
#include <cstdlib>
|
|
|
|
#include <glibmm.h>
|
|
|
|
#include <rubberband/RubberBandStretcher.h>
|
|
|
|
#include "pbd/basename.h"
|
|
#include "pbd/compose.h"
|
|
#include "pbd/failed_constructor.h"
|
|
#include "pbd/types_convert.h"
|
|
|
|
#include "temporal/tempo.h"
|
|
|
|
#include "ardour/audioregion.h"
|
|
#include "ardour/audio_buffer.h"
|
|
#include "ardour/debug.h"
|
|
#include "ardour/midi_buffer.h"
|
|
#include "ardour/region_factory.h"
|
|
#include "ardour/session.h"
|
|
#include "ardour/session_object.h"
|
|
#include "ardour/source_factory.h"
|
|
#include "ardour/sndfilesource.h"
|
|
#include "ardour/triggerbox.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;
|
|
}
|
|
}
|
|
|
|
Trigger::Trigger (uint64_t n, TriggerBox& b)
|
|
: _box (b)
|
|
, _state (Stopped)
|
|
, _requested_state (None)
|
|
, _bang (0)
|
|
, _unbang (0)
|
|
, _index (n)
|
|
, _launch_style (Toggle)
|
|
, _follow_action { NextTrigger, Stop }
|
|
, _follow_action_probability (100)
|
|
, _quantization (Temporal::BBT_Offset (0, 1, 0))
|
|
, _legato (true)
|
|
, _ui (0)
|
|
{
|
|
}
|
|
|
|
void
|
|
Trigger::set_name (std::string const & str)
|
|
{
|
|
_name = str;
|
|
}
|
|
|
|
void
|
|
Trigger::set_ui (void* p)
|
|
{
|
|
_ui = p;
|
|
}
|
|
|
|
void
|
|
Trigger::bang ()
|
|
{
|
|
_bang.fetch_add (1);
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("bang on %1\n", _index));
|
|
}
|
|
|
|
void
|
|
Trigger::unbang ()
|
|
{
|
|
_unbang.fetch_add (1);
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("un-bang on %1\n", _index));
|
|
}
|
|
|
|
void
|
|
Trigger::set_follow_action (FollowAction f, uint64_t n)
|
|
{
|
|
assert (n < 2);
|
|
_follow_action[n] = f;
|
|
}
|
|
|
|
void
|
|
Trigger::set_launch_style (LaunchStyle l)
|
|
{
|
|
_launch_style = l;
|
|
|
|
set_usable_length ();
|
|
}
|
|
|
|
XMLNode&
|
|
Trigger::get_state (void)
|
|
{
|
|
XMLNode* node = new XMLNode (X_("Trigger"));
|
|
|
|
node->set_property (X_("legato"), _legato);
|
|
node->set_property (X_("launch-style"), enum_2_string (_launch_style));
|
|
node->set_property (X_("follow-action-0"), enum_2_string (_follow_action[0]));
|
|
node->set_property (X_("follow-action-1"), enum_2_string (_follow_action[1]));
|
|
node->set_property (X_("quantization"), _quantization);
|
|
node->set_property (X_("name"), _name);
|
|
node->set_property (X_("index"), _index);
|
|
|
|
if (_region) {
|
|
node->set_property (X_("region"), _region->id());
|
|
}
|
|
|
|
return *node;
|
|
}
|
|
|
|
int
|
|
Trigger::set_state (const XMLNode& node, int version)
|
|
{
|
|
node.get_property (X_("legato"), _legato);
|
|
node.get_property (X_("launch-style"), _launch_style);
|
|
node.get_property (X_("follow-action-0"), _follow_action[0]);
|
|
node.get_property (X_("follow-action-1"), _follow_action[1]);
|
|
node.get_property (X_("quantization"), _quantization);
|
|
node.get_property (X_("name"), _name);
|
|
node.get_property (X_("index"), _index);
|
|
|
|
PBD::ID rid;
|
|
|
|
node.get_property (X_("region"), rid);
|
|
|
|
boost::shared_ptr<Region> r = RegionFactory::region_by_id (rid);
|
|
|
|
if (r) {
|
|
set_region (r);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
Trigger::set_legato (bool yn)
|
|
{
|
|
_legato = yn;
|
|
PropertyChanged (Properties::legato);
|
|
}
|
|
|
|
void
|
|
Trigger::set_follow_action_probability (int n)
|
|
{
|
|
n = std::min (100, n);
|
|
n = std::max (0, n);
|
|
|
|
_follow_action_probability = n;
|
|
}
|
|
|
|
void
|
|
Trigger::set_quantization (Temporal::BBT_Offset const & q)
|
|
{
|
|
_quantization = q;
|
|
set_usable_length ();
|
|
}
|
|
|
|
void
|
|
Trigger::set_region_internal (boost::shared_ptr<Region> r)
|
|
{
|
|
_region = r;
|
|
}
|
|
|
|
Temporal::BBT_Offset
|
|
Trigger::quantization () const
|
|
{
|
|
return _quantization;
|
|
}
|
|
|
|
void
|
|
Trigger::stop (int next)
|
|
{
|
|
request_state (Stopped);
|
|
}
|
|
|
|
void
|
|
Trigger::request_state (State s)
|
|
{
|
|
_requested_state.store (s);
|
|
}
|
|
|
|
void
|
|
Trigger::startup()
|
|
{
|
|
_state = WaitingToStart;
|
|
PropertyChanged (ARDOUR::Properties::running);
|
|
}
|
|
|
|
void
|
|
Trigger::jump_start()
|
|
{
|
|
/* this is used when we start a new trigger in legato mode. We do not
|
|
wait for quantization.
|
|
*/
|
|
_state = Running;
|
|
PropertyChanged (ARDOUR::Properties::running);
|
|
}
|
|
|
|
void
|
|
Trigger::jump_stop()
|
|
{
|
|
/* this is used when we start a new trigger in legato mode. We do not
|
|
wait for quantization.
|
|
*/
|
|
_state = Stopped;
|
|
PropertyChanged (ARDOUR::Properties::running);
|
|
}
|
|
|
|
void
|
|
Trigger::process_state_requests ()
|
|
{
|
|
State new_state = _requested_state.exchange (None);
|
|
|
|
if (new_state != None && new_state != _state) {
|
|
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 requested state %2\n", index(), enum_2_string (new_state)));
|
|
|
|
switch (new_state) {
|
|
case Stopped:
|
|
if (_state != WaitingToStop) {
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 %2 => %3\n", index(), enum_2_string (_state), enum_2_string (WaitingToStop)));
|
|
_state = WaitingToStop;
|
|
PropertyChanged (ARDOUR::Properties::running);
|
|
}
|
|
break;
|
|
case Running:
|
|
_box.queue_explict (this);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* 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 None:
|
|
abort ();
|
|
break;
|
|
|
|
case Running:
|
|
switch (launch_style()) {
|
|
case OneShot:
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 oneshot %2 => %3\n", index(), enum_2_string (Running), enum_2_string (WaitingForRetrigger)));
|
|
_state = WaitingForRetrigger;
|
|
PropertyChanged (ARDOUR::Properties::running);
|
|
break;
|
|
case Gate:
|
|
case Toggle:
|
|
case Repeat:
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 %2 gate/toggle/repeat => %3\n", index(), enum_2_string (Running), enum_2_string (WaitingToStop)));
|
|
_state = WaitingToStop;
|
|
_box.clear_implicit ();
|
|
PropertyChanged (ARDOUR::Properties::running);
|
|
}
|
|
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 (this);
|
|
break;
|
|
|
|
case WaitingToStart:
|
|
case WaitingToStop:
|
|
case WaitingForRetrigger:
|
|
case Stopping:
|
|
break;
|
|
}
|
|
}
|
|
|
|
while ((x = _unbang.load ())) {
|
|
|
|
_unbang.fetch_sub (1);
|
|
|
|
if (_launch_style == Gate || _launch_style == Repeat) {
|
|
switch (_state) {
|
|
case Running:
|
|
_state = WaitingToStop;
|
|
PropertyChanged (ARDOUR::Properties::running);
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 unbanged, now in WaitingToStop\n", index()));
|
|
break;
|
|
default:
|
|
/* didn't even get started */
|
|
_state = Stopped;
|
|
PropertyChanged (ARDOUR::Properties::running);
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 unbanged, never started, now stopped\n", index()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Trigger::RunType
|
|
Trigger::maybe_compute_next_transition (Temporal::Beats const & start, Temporal::Beats const & end)
|
|
{
|
|
/* In these states, we are not waiting for a transition */
|
|
|
|
switch (_state) {
|
|
case Stopped:
|
|
return RunNone;
|
|
case Running:
|
|
return RunAll;
|
|
case Stopping:
|
|
return RunAll;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
timepos_t ev_time (Temporal::BeatTime);
|
|
|
|
if (_quantization.bars == 0) {
|
|
ev_time = timepos_t (start.snap_to (Temporal::Beats (_quantization.beats, _quantization.ticks)));
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 quantized with %5 start at %2, sb %3 eb %4\n", index(), ev_time.beats(), start, end, _quantization));
|
|
} else {
|
|
/* XXX not yet handled */
|
|
}
|
|
|
|
if (ev_time.beats() >= start && ev_time < end) {
|
|
|
|
bang_samples = ev_time.samples();
|
|
bang_beats = ev_time.beats ();
|
|
|
|
if (_state == WaitingToStop) {
|
|
_state = Stopping;
|
|
PropertyChanged (ARDOUR::Properties::running);
|
|
return RunEnd;
|
|
} else if (_state == WaitingToStart) {
|
|
retrigger ();
|
|
_state = Running;
|
|
_box.prepare_next (_index);
|
|
PropertyChanged (ARDOUR::Properties::running);
|
|
return RunStart;
|
|
} else if (_state == WaitingForRetrigger) {
|
|
retrigger ();
|
|
_state = Running;
|
|
_box.prepare_next (_index);
|
|
PropertyChanged (ARDOUR::Properties::running);
|
|
return RunAll;
|
|
}
|
|
} else {
|
|
if (_state == WaitingForRetrigger || _state == WaitingToStop) {
|
|
/* retrigger time has not been reached, just continue
|
|
to play normally until then.
|
|
*/
|
|
return RunAll;
|
|
}
|
|
}
|
|
|
|
return RunNone;
|
|
}
|
|
|
|
/*--------------------*/
|
|
|
|
AudioTrigger::AudioTrigger (uint64_t n, TriggerBox& b)
|
|
: Trigger (n, b)
|
|
, data (0)
|
|
, read_index (0)
|
|
, data_length (0)
|
|
, _start_offset (0)
|
|
, _legato_offset (0)
|
|
, usable_length (0)
|
|
, last_sample (0)
|
|
{
|
|
}
|
|
|
|
AudioTrigger::~AudioTrigger ()
|
|
{
|
|
for (std::vector<Sample*>::iterator d = data.begin(); d != data.end(); ++d) {
|
|
delete *d;
|
|
}
|
|
}
|
|
|
|
void
|
|
AudioTrigger::startup ()
|
|
{
|
|
Trigger::startup ();
|
|
retrigger ();
|
|
}
|
|
|
|
void
|
|
AudioTrigger::jump_start ()
|
|
{
|
|
Trigger::jump_start ();
|
|
retrigger ();
|
|
}
|
|
|
|
void
|
|
AudioTrigger::jump_stop ()
|
|
{
|
|
Trigger::jump_stop ();
|
|
retrigger ();
|
|
}
|
|
|
|
XMLNode&
|
|
AudioTrigger::get_state (void)
|
|
{
|
|
XMLNode& node (Trigger::get_state());
|
|
|
|
node.set_property (X_("start"), timepos_t (_start_offset));
|
|
node.set_property (X_("length"), timepos_t (usable_length));
|
|
|
|
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();
|
|
|
|
node.get_property (X_("length"), t);
|
|
usable_length = t.samples();
|
|
last_sample = _start_offset + usable_length;
|
|
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
AudioTrigger::set_start (timepos_t const & s)
|
|
{
|
|
_start_offset = s.samples ();
|
|
}
|
|
|
|
void
|
|
AudioTrigger::set_end (timepos_t const & e)
|
|
{
|
|
set_length (timepos_t (e.samples() - _start_offset));
|
|
}
|
|
|
|
void
|
|
AudioTrigger::set_legato_offset (timepos_t const & offset)
|
|
{
|
|
_legato_offset = offset.samples();
|
|
}
|
|
|
|
timepos_t
|
|
AudioTrigger::current_pos() const
|
|
{
|
|
return timepos_t (read_index);
|
|
}
|
|
|
|
timepos_t
|
|
AudioTrigger::end() const
|
|
{
|
|
return timepos_t (_start_offset + usable_length);
|
|
}
|
|
|
|
void
|
|
AudioTrigger::set_length (timepos_t const & newlen)
|
|
{
|
|
using namespace RubberBand;
|
|
using namespace Temporal;
|
|
|
|
if (!_region) {
|
|
return;
|
|
}
|
|
|
|
boost::shared_ptr<AudioRegion> ar (boost::dynamic_pointer_cast<AudioRegion> (_region));
|
|
|
|
/* load raw data */
|
|
|
|
load_data (ar);
|
|
|
|
if (newlen == timepos_t (_region->length_samples())) {
|
|
/* no stretch required */
|
|
return;
|
|
}
|
|
|
|
/* offline stretch */
|
|
|
|
/* study */
|
|
|
|
const uint32_t nchans = ar->n_channels();
|
|
|
|
RubberBandStretcher::Options options = RubberBandStretcher::Option (RubberBandStretcher::OptionProcessOffline|RubberBandStretcher::OptionStretchPrecise);
|
|
RubberBandStretcher stretcher (_box.session().sample_rate(), nchans, options, 1.0, 1.0);
|
|
|
|
/* Compute stretch ratio */
|
|
|
|
double new_ratio;
|
|
|
|
if (newlen.time_domain() == AudioTime) {
|
|
new_ratio = (double) newlen.samples() / data_length;
|
|
} else {
|
|
/* XXX what to use for position ??? */
|
|
timecnt_t l (newlen, timepos_t (AudioTime));
|
|
const timecnt_t dur = TempoMap::use()->convert_duration (l, timepos_t (0), AudioTime);
|
|
new_ratio = (double) dur.samples() / data_length;
|
|
}
|
|
|
|
stretcher.setTimeRatio (new_ratio);
|
|
|
|
const samplecnt_t expected_length = ceil (data_length * new_ratio) + 16; /* extra space for safety */
|
|
std::vector<Sample*> stretched;
|
|
|
|
for (uint32_t n = 0; n < nchans; ++n) {
|
|
stretched.push_back (new Sample[expected_length]);
|
|
}
|
|
|
|
/* RB expects array-of-ptr-to-Sample, so set one up */
|
|
|
|
std::vector<Sample*> raw(nchans);
|
|
std::vector<Sample*> results(nchans);
|
|
|
|
/* study, then process */
|
|
|
|
const samplecnt_t block_size = 16384;
|
|
samplecnt_t read = 0;
|
|
|
|
stretcher.setDebugLevel (0);
|
|
stretcher.setMaxProcessSize (block_size);
|
|
stretcher.setExpectedInputDuration (data_length);
|
|
|
|
while (read < data_length) {
|
|
|
|
for (uint32_t n = 0; n < nchans; ++n) {
|
|
raw[n] = data[n] + read;
|
|
}
|
|
|
|
samplecnt_t to_read = std::min (block_size, data_length - read);
|
|
read += to_read;
|
|
|
|
stretcher.study (&raw[0], to_read, (read >= data_length));
|
|
}
|
|
|
|
read = 0;
|
|
|
|
samplecnt_t processed = 0;
|
|
samplecnt_t avail;
|
|
|
|
while (read < data_length) {
|
|
|
|
for (uint32_t n = 0; n < nchans; ++n) {
|
|
raw[n] = data[n] + read;
|
|
}
|
|
|
|
samplecnt_t to_read = std::min (block_size, data_length - read);
|
|
read += to_read;
|
|
|
|
stretcher.process (&raw[0], to_read, (read >= data_length));
|
|
|
|
while ((avail = stretcher.available()) > 0) {
|
|
|
|
for (uint32_t n = 0; n < nchans; ++n) {
|
|
results[n] = stretched[n] + processed;
|
|
}
|
|
|
|
processed += stretcher.retrieve (&results[0], avail);
|
|
}
|
|
}
|
|
|
|
/* collect final chunk of data, possible delayed by thread activity in stretcher */
|
|
|
|
while ((avail = stretcher.available()) >= 0) {
|
|
|
|
if (avail == 0) {
|
|
Glib::usleep (10000);
|
|
continue;
|
|
}
|
|
|
|
for (uint32_t n = 0; n < nchans; ++n) {
|
|
results[n] = stretched[n] + processed;
|
|
}
|
|
|
|
processed += stretcher.retrieve (&results[0], avail);
|
|
}
|
|
|
|
/* allocate new data buffers */
|
|
|
|
drop_data ();
|
|
data = stretched;
|
|
data_length = processed;
|
|
if (!usable_length || usable_length > data_length) {
|
|
usable_length = data_length;
|
|
last_sample = _start_offset + usable_length;
|
|
}
|
|
}
|
|
|
|
void
|
|
AudioTrigger::set_usable_length ()
|
|
{
|
|
if (!_region) {
|
|
return;
|
|
}
|
|
|
|
switch (_launch_style) {
|
|
case Repeat:
|
|
break;
|
|
default:
|
|
usable_length = data_length;
|
|
last_sample = _start_offset + usable_length;
|
|
return;
|
|
}
|
|
|
|
if (_quantization == Temporal::BBT_Offset ()) {
|
|
usable_length = data_length;
|
|
last_sample = _start_offset + usable_length;
|
|
return;
|
|
}
|
|
|
|
/* XXX MUST HANDLE BAR-LEVEL QUANTIZATION */
|
|
|
|
timecnt_t len (Temporal::Beats (_quantization.beats, _quantization.ticks), timepos_t (Temporal::Beats()));
|
|
usable_length = len.samples();
|
|
last_sample = _start_offset + usable_length;
|
|
}
|
|
|
|
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 (boost::shared_ptr<Region> r)
|
|
{
|
|
boost::shared_ptr<AudioRegion> ar = boost::dynamic_pointer_cast<AudioRegion> (r);
|
|
|
|
if (!ar) {
|
|
return -1;
|
|
}
|
|
|
|
set_region_internal (r);
|
|
|
|
/* this will load data, but won't stretch it for now */
|
|
|
|
set_length (timepos_t::from_superclock (r->length ().magnitude()));
|
|
|
|
PropertyChanged (ARDOUR::Properties::name);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
AudioTrigger::drop_data ()
|
|
{
|
|
for (uint32_t n = 0; n < data.size(); ++n) {
|
|
delete [] data[n];
|
|
}
|
|
data.clear ();
|
|
}
|
|
|
|
int
|
|
AudioTrigger::load_data (boost::shared_ptr<AudioRegion> ar)
|
|
{
|
|
const uint32_t nchans = ar->n_channels();
|
|
|
|
data_length = ar->length_samples();
|
|
|
|
/* if usable length was already set, only adjust it if it is too large */
|
|
if (!usable_length || usable_length > data_length) {
|
|
usable_length = data_length;
|
|
last_sample = _start_offset + usable_length;
|
|
}
|
|
|
|
drop_data ();
|
|
|
|
try {
|
|
for (uint32_t n = 0; n < nchans; ++n) {
|
|
data.push_back (new Sample[data_length]);
|
|
ar->read (data[n], 0, data_length, n);
|
|
}
|
|
|
|
set_name (ar->name());
|
|
|
|
} catch (...) {
|
|
drop_data ();
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
AudioTrigger::retrigger ()
|
|
{
|
|
read_index = _start_offset + _legato_offset;
|
|
_legato_offset = 0; /* used one time only */
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 retriggered to %2\n", _index, read_index));
|
|
}
|
|
|
|
int
|
|
AudioTrigger::run (BufferSet& bufs, pframes_t nframes, pframes_t dest_offset, bool first)
|
|
{
|
|
boost::shared_ptr<AudioRegion> ar = boost::dynamic_pointer_cast<AudioRegion>(_region);
|
|
const bool long_enough_to_fade = (nframes >= 64);
|
|
|
|
assert (ar);
|
|
assert (active());
|
|
|
|
while (nframes) {
|
|
|
|
pframes_t this_read = (pframes_t) std::min ((samplecnt_t) nframes, (last_sample - read_index));
|
|
|
|
for (uint64_t chn = 0; chn < ar->n_channels(); ++chn) {
|
|
|
|
uint64_t channel = chn % data.size();
|
|
Sample* src = data[channel] + read_index;
|
|
AudioBuffer& buf (bufs.get_audio (chn));
|
|
|
|
|
|
if (first) {
|
|
buf.read_from (src, this_read, dest_offset);
|
|
} else {
|
|
buf.accumulate_from (src, this_read, dest_offset);
|
|
}
|
|
}
|
|
|
|
read_index += this_read;
|
|
|
|
if (read_index >= last_sample) {
|
|
|
|
/* We reached the end */
|
|
|
|
if ((_launch_style == Repeat) || (_box.peek_next_trigger() == this)) { /* self repeat */
|
|
nframes -= this_read;
|
|
dest_offset += this_read;
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 reached end, but set to loop, so retrigger\n", index()));
|
|
retrigger ();
|
|
/* and go around again */
|
|
continue;
|
|
|
|
} else {
|
|
|
|
if (this_read < nframes) {
|
|
|
|
for (uint64_t chn = 0; chn < ar->n_channels(); ++chn) {
|
|
uint64_t channel = chn % data.size();
|
|
AudioBuffer& buf (bufs.get_audio (channel));
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 short fill, ri %2 vs ls %3, do silent fill\n", index(), read_index, last_sample));
|
|
buf.silence (nframes - this_read, dest_offset + this_read);
|
|
}
|
|
}
|
|
_state = Stopped;
|
|
PropertyChanged (ARDOUR::Properties::running);
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 reached end, now stopped\n", index()));
|
|
break;
|
|
}
|
|
}
|
|
|
|
nframes -= this_read;
|
|
}
|
|
|
|
if (_state == Stopping && long_enough_to_fade) {
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 was stopping, now stopped\n", index()));
|
|
_state = Stopped;
|
|
PropertyChanged (ARDOUR::Properties::running);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**************/
|
|
|
|
void
|
|
Trigger::make_property_quarks ()
|
|
{
|
|
Properties::muted.property_id = g_quark_from_static_string (X_("running"));
|
|
DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for running = %1\n", Properties::running.property_id));
|
|
}
|
|
|
|
const uint64_t TriggerBox::default_triggers_per_box = 8;
|
|
|
|
TriggerBox::TriggerBox (Session& s, DataType dt)
|
|
: Processor (s, _("TriggerBox"), Temporal::BeatTime)
|
|
, _bang_queue (1024)
|
|
, _unbang_queue (1024)
|
|
, _data_type (dt)
|
|
, explicit_queue (64)
|
|
, implicit_queue (64)
|
|
, currently_playing (0)
|
|
, _stop_all (false)
|
|
{
|
|
|
|
/* default number of possible triggers. call ::add_trigger() to increase */
|
|
|
|
if (_data_type == DataType::AUDIO) {
|
|
for (uint64_t n = 0; n < default_triggers_per_box; ++n) {
|
|
all_triggers.push_back (new AudioTrigger (n, *this));
|
|
}
|
|
}
|
|
|
|
midi_trigger_map.insert (midi_trigger_map.end(), std::make_pair (uint8_t (60), 0));
|
|
midi_trigger_map.insert (midi_trigger_map.end(), std::make_pair (uint8_t (61), 1));
|
|
midi_trigger_map.insert (midi_trigger_map.end(), std::make_pair (uint8_t (62), 2));
|
|
midi_trigger_map.insert (midi_trigger_map.end(), std::make_pair (uint8_t (63), 3));
|
|
midi_trigger_map.insert (midi_trigger_map.end(), std::make_pair (uint8_t (64), 4));
|
|
midi_trigger_map.insert (midi_trigger_map.end(), std::make_pair (uint8_t (65), 5));
|
|
midi_trigger_map.insert (midi_trigger_map.end(), std::make_pair (uint8_t (66), 6));
|
|
midi_trigger_map.insert (midi_trigger_map.end(), std::make_pair (uint8_t (67), 7));
|
|
midi_trigger_map.insert (midi_trigger_map.end(), std::make_pair (uint8_t (68), 8));
|
|
midi_trigger_map.insert (midi_trigger_map.end(), std::make_pair (uint8_t (69), 9));
|
|
}
|
|
|
|
void
|
|
TriggerBox::clear_implicit ()
|
|
{
|
|
implicit_queue.reset ();
|
|
}
|
|
|
|
void
|
|
TriggerBox::queue_explict (Trigger* t)
|
|
{
|
|
assert (t);
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("explicit queue %1\n", t->index()));
|
|
explicit_queue.write (&t, 1);
|
|
implicit_queue.reset ();
|
|
|
|
if (currently_playing) {
|
|
currently_playing->unbang ();
|
|
}
|
|
}
|
|
|
|
void
|
|
TriggerBox::queue_implicit (Trigger* t)
|
|
{
|
|
assert (t);
|
|
|
|
if (explicit_queue.read_space() == 0) {
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("implicit queue %1\n", t->index()));
|
|
implicit_queue.write (&t, 1);
|
|
}
|
|
}
|
|
|
|
Trigger*
|
|
TriggerBox::peek_next_trigger ()
|
|
{
|
|
/* allows us to check if there's a next trigger queued, without
|
|
* actually reading it from either of the queues.
|
|
*/
|
|
|
|
RingBuffer<Trigger*>::rw_vector rwv;
|
|
|
|
explicit_queue.get_read_vector (&rwv);
|
|
if (rwv.len[0] > 0) {
|
|
return *(rwv.buf[0]);
|
|
}
|
|
|
|
implicit_queue.get_read_vector (&rwv);
|
|
if (rwv.len[0] > 0) {
|
|
return *(rwv.buf[0]);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
Trigger*
|
|
TriggerBox::get_next_trigger ()
|
|
{
|
|
Trigger* r;
|
|
|
|
if (explicit_queue.read (&r, 1) == 1) {
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("next trigger from explicit queue = %1\n", r->index()));
|
|
return r;
|
|
}
|
|
|
|
if (implicit_queue.read (&r, 1) == 1) {
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("next trigger from implicit queue = %1\n", r->index()));
|
|
return r;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
TriggerBox::set_from_path (uint64_t slot, std::string const & path)
|
|
{
|
|
assert (slot < all_triggers.size());
|
|
|
|
try {
|
|
SoundFileInfo info;
|
|
string errmsg;
|
|
|
|
if (!SndFileSource::get_soundfile_info (path, info, errmsg)) {
|
|
error << string_compose (_("Cannot get info from audio file %1 (%2)"), path, errmsg) << endmsg;
|
|
return -1;
|
|
}
|
|
|
|
SourceList src_list;
|
|
|
|
for (uint16_t n = 0; n < info.channels; ++n) {
|
|
boost::shared_ptr<Source> source (SourceFactory::createExternal (DataType::AUDIO, _session, path, n, Source::Flag (0), true));
|
|
if (!source) {
|
|
error << string_compose (_("Cannot create source from %1"), path) << endmsg;
|
|
src_list.clear ();
|
|
return -1;
|
|
}
|
|
src_list.push_back (source);
|
|
}
|
|
|
|
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);
|
|
|
|
/* XXX catch region going away */
|
|
|
|
} catch (std::exception& e) {
|
|
cerr << "loading sample from " << path << " failed: " << e.what() << endl;
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
TriggerBox::~TriggerBox ()
|
|
{
|
|
drop_triggers ();
|
|
}
|
|
|
|
void
|
|
TriggerBox::request_stop_all ()
|
|
{
|
|
_stop_all = true;
|
|
}
|
|
|
|
void
|
|
TriggerBox::stop_all ()
|
|
{
|
|
/* XXX needs to be done with mutex or via thread-safe queue */
|
|
|
|
for (uint64_t n = 0; n < all_triggers.size(); ++n) {
|
|
all_triggers[n]->stop (-1);
|
|
}
|
|
|
|
implicit_queue.reset ();
|
|
explicit_queue.reset ();
|
|
}
|
|
|
|
void
|
|
TriggerBox::drop_triggers ()
|
|
{
|
|
Glib::Threads::RWLock::WriterLock lm (trigger_lock);
|
|
|
|
for (Triggers::iterator t = all_triggers.begin(); t != all_triggers.end(); ++t) {
|
|
if (*t) {
|
|
delete *t;
|
|
(*t) = 0;
|
|
}
|
|
}
|
|
|
|
all_triggers.clear ();
|
|
}
|
|
|
|
Trigger*
|
|
TriggerBox::trigger (Triggers::size_type n)
|
|
{
|
|
Glib::Threads::RWLock::ReaderLock lm (trigger_lock);
|
|
|
|
if (n >= all_triggers.size()) {
|
|
return 0;
|
|
}
|
|
|
|
return all_triggers[n];
|
|
}
|
|
|
|
bool
|
|
TriggerBox::can_support_io_configuration (const ChanCount& in, ChanCount& out)
|
|
{
|
|
if (in.get(DataType::MIDI) < 1) {
|
|
return false;
|
|
}
|
|
|
|
out = ChanCount::max (out, ChanCount (DataType::AUDIO, 2));
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
TriggerBox::configure_io (ChanCount in, ChanCount out)
|
|
{
|
|
return Processor::configure_io (in, out);
|
|
}
|
|
|
|
void
|
|
TriggerBox::add_trigger (Trigger* trigger)
|
|
{
|
|
Glib::Threads::RWLock::WriterLock lm (trigger_lock);
|
|
all_triggers.push_back (trigger);
|
|
}
|
|
|
|
void
|
|
TriggerBox::process_midi_trigger_requests (BufferSet& bufs)
|
|
{
|
|
/* check MIDI port input buffers for triggers */
|
|
|
|
for (BufferSet::midi_iterator mi = bufs.midi_begin(); mi != bufs.midi_end(); ++mi) {
|
|
MidiBuffer& mb (*mi);
|
|
|
|
for (MidiBuffer::iterator ev = mb.begin(); ev != mb.end(); ++ev) {
|
|
|
|
if (!(*ev).is_note()) {
|
|
continue;
|
|
}
|
|
|
|
MidiTriggerMap::iterator mt = midi_trigger_map.find ((*ev).note());
|
|
Trigger* t = 0;
|
|
|
|
if (mt != midi_trigger_map.end()) {
|
|
|
|
assert (mt->second < all_triggers.size());
|
|
|
|
t = all_triggers[mt->second];
|
|
|
|
if (!t) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if ((*ev).is_note_on()) {
|
|
|
|
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)
|
|
{
|
|
if (start_sample < 0) {
|
|
/* we can't do anything under these conditions (related to
|
|
latency compensation
|
|
*/
|
|
return;
|
|
}
|
|
|
|
process_midi_trigger_requests (bufs);
|
|
|
|
|
|
/* now let each trigger handle any state changes */
|
|
|
|
std::vector<uint64_t> to_run;
|
|
|
|
for (uint64_t n = 0; n < all_triggers.size(); ++n) {
|
|
all_triggers[n]->process_state_requests ();
|
|
}
|
|
|
|
Trigger* nxt = 0;
|
|
|
|
if (!currently_playing) {
|
|
if ((currently_playing = get_next_trigger ()) != 0) {
|
|
currently_playing->startup ();
|
|
}
|
|
}
|
|
|
|
if (!currently_playing) {
|
|
return;
|
|
}
|
|
|
|
/* transport must be active for triggers */
|
|
|
|
if (!_session.transport_state_rolling()) {
|
|
_session.start_transport_from_processor ();
|
|
}
|
|
|
|
timepos_t start (start_sample);
|
|
timepos_t end (end_sample);
|
|
Temporal::Beats start_beats (start.beats());
|
|
Temporal::Beats end_beats (end.beats());
|
|
Temporal::TempoMap::SharedPtr tmap (Temporal::TempoMap::use());
|
|
uint64_t max_chans = 0;
|
|
bool first = false;
|
|
|
|
/* see if there's another trigger explicitly queued that has legato set. */
|
|
|
|
RingBuffer<Trigger*>::rw_vector rwv;
|
|
explicit_queue.get_read_vector (&rwv);
|
|
|
|
if (rwv.len[0] > 0) {
|
|
|
|
/* actually fetch it (guaranteed to pull from the explicit queue */
|
|
|
|
nxt = get_next_trigger ();
|
|
|
|
/* if user triggered same clip, with legato set, then there is
|
|
* nothing to do
|
|
*/
|
|
|
|
if (nxt != currently_playing) {
|
|
|
|
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.
|
|
*/
|
|
|
|
nxt->set_legato_offset (currently_playing->current_pos());
|
|
nxt->jump_start ();
|
|
currently_playing->jump_stop ();
|
|
prepare_next (nxt->index());
|
|
/* and switch */
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 => %2 switched to in legato mode\n", currently_playing->index(), nxt->index()));
|
|
currently_playing = nxt;
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_stop_all) {
|
|
stop_all ();
|
|
_stop_all = false;
|
|
}
|
|
|
|
while (currently_playing) {
|
|
|
|
assert (currently_playing->state() >= Trigger::WaitingToStart);
|
|
|
|
Trigger::RunType rt;
|
|
|
|
switch (currently_playing->state()) {
|
|
case Trigger::WaitingToStop:
|
|
case Trigger::WaitingToStart:
|
|
case Trigger::WaitingForRetrigger:
|
|
rt = currently_playing->maybe_compute_next_transition (start_beats, end_beats);
|
|
break;
|
|
default:
|
|
rt = Trigger::RunAll;
|
|
}
|
|
|
|
if (rt == Trigger::RunNone) {
|
|
/* nothing to do at this time, still waiting to start */
|
|
return;
|
|
}
|
|
|
|
boost::shared_ptr<Region> r = currently_playing->region();
|
|
|
|
sampleoffset_t dest_offset;
|
|
pframes_t trigger_samples;
|
|
|
|
const bool was_waiting_to_start = (currently_playing->state() == Trigger::WaitingToStart);
|
|
|
|
if (rt == Trigger::RunEnd) {
|
|
|
|
/* trigger will reach it's end somewhere within this
|
|
* process cycle, so compute the number of samples it
|
|
* should generate.
|
|
*/
|
|
|
|
trigger_samples = nframes - (currently_playing->bang_samples - start_sample);
|
|
dest_offset = 0;
|
|
|
|
} else if (rt == Trigger::RunStart) {
|
|
|
|
/* 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.
|
|
*/
|
|
|
|
dest_offset = std::max (samplepos_t (0), currently_playing->bang_samples - start_sample);
|
|
trigger_samples = nframes - dest_offset;
|
|
|
|
} else if (rt == Trigger::RunAll) {
|
|
|
|
/* trigger is just running normally, and will fill
|
|
* buffers entirely.
|
|
*/
|
|
|
|
dest_offset = 0;
|
|
trigger_samples = nframes;
|
|
|
|
} else {
|
|
/* NOTREACHED */
|
|
}
|
|
|
|
if (was_waiting_to_start) {
|
|
determine_next_trigger (currently_playing->index());
|
|
}
|
|
|
|
AudioTrigger* at = dynamic_cast<AudioTrigger*> (currently_playing);
|
|
|
|
if (at) {
|
|
|
|
boost::shared_ptr<AudioRegion> ar = boost::dynamic_pointer_cast<AudioRegion> (r);
|
|
const uint64_t nchans = ar->n_channels ();
|
|
|
|
max_chans = std::max (max_chans, nchans);
|
|
|
|
at->run (bufs, trigger_samples, dest_offset, first);
|
|
|
|
first = false;
|
|
|
|
} else {
|
|
|
|
/* XXX MIDI triggers to be implemented */
|
|
|
|
}
|
|
|
|
if (currently_playing->state() == Trigger::Stopped) {
|
|
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 did stop\n", currently_playing->index()));
|
|
|
|
Trigger* nxt = get_next_trigger ();
|
|
|
|
if (nxt) {
|
|
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1 switching to %2\n", currently_playing->index(), nxt->index()));
|
|
if (nxt->legato()) {
|
|
nxt->set_legato_offset (currently_playing->current_pos());
|
|
}
|
|
/* start it up */
|
|
nxt->startup();
|
|
currently_playing = nxt;
|
|
|
|
} else {
|
|
currently_playing = 0;
|
|
}
|
|
|
|
} else {
|
|
/* done */
|
|
break;
|
|
}
|
|
}
|
|
|
|
ChanCount cc (DataType::AUDIO, max_chans);
|
|
cc.set_midi (bufs.count().n_midi());
|
|
bufs.set_count (cc);
|
|
}
|
|
|
|
void
|
|
TriggerBox::prepare_next (uint64_t current)
|
|
{
|
|
int nxt = determine_next_trigger (current);
|
|
|
|
DEBUG_TRACE (DEBUG::Triggers, string_compose ("nxt for %1 = %2\n", current, nxt));
|
|
|
|
if (nxt >= 0) {
|
|
queue_implicit (all_triggers[nxt]);
|
|
}
|
|
}
|
|
|
|
int
|
|
TriggerBox::determine_next_trigger (uint64_t current)
|
|
{
|
|
uint64_t n;
|
|
uint64_t runnable = 0;
|
|
|
|
/* count number of triggers that can actually be run (i.e. they have a region) */
|
|
|
|
for (uint64_t n = 0; n < all_triggers.size(); ++n) {
|
|
if (all_triggers[n]->region()) {
|
|
runnable++;
|
|
}
|
|
}
|
|
|
|
/* decide which of the two follow actions we're going to use (based on
|
|
* random number and the probability setting)
|
|
*/
|
|
|
|
int which_follow_action;
|
|
int r = _pcg.rand (100); // 0 .. 99
|
|
|
|
if (r <= all_triggers[current]->follow_action_probability()) {
|
|
which_follow_action = 0;
|
|
} else {
|
|
which_follow_action = 1;
|
|
}
|
|
|
|
/* first switch: deal with the "special" cases where we either do
|
|
* nothing or just repeat the current trigger
|
|
*/
|
|
|
|
switch (all_triggers[current]->follow_action (which_follow_action)) {
|
|
|
|
case Trigger::Stop:
|
|
return -1;
|
|
|
|
case Trigger::QueuedTrigger:
|
|
/* XXX implement me */
|
|
return -1;
|
|
default:
|
|
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 (all_triggers[current]->follow_action (which_follow_action)) {
|
|
|
|
case Trigger::Again:
|
|
return current;
|
|
|
|
case Trigger::NextTrigger:
|
|
n = current;
|
|
while (true) {
|
|
++n;
|
|
|
|
if (n >= all_triggers.size()) {
|
|
n = 0;
|
|
}
|
|
|
|
if (n == current) {
|
|
cerr << "outa here\n";
|
|
break;
|
|
}
|
|
|
|
if (all_triggers[n]->region() && !all_triggers[n]->active()) {
|
|
return n;
|
|
}
|
|
}
|
|
break;
|
|
case Trigger::PrevTrigger:
|
|
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 Trigger::FirstTrigger:
|
|
for (n = 0; n < all_triggers.size(); ++n) {
|
|
if (all_triggers[n]->region() && !all_triggers[n]->active ()) {
|
|
return n;
|
|
}
|
|
}
|
|
break;
|
|
case Trigger::LastTrigger:
|
|
for (int i = all_triggers.size() - 1; i >= 0; --i) {
|
|
if (all_triggers[i]->region() && !all_triggers[i]->active ()) {
|
|
return i;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case Trigger::AnyTrigger:
|
|
while (true) {
|
|
n = _pcg.rand (all_triggers.size());
|
|
if (!all_triggers[n]->region()) {
|
|
continue;
|
|
}
|
|
if (all_triggers[n]->active()) {
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
return n;
|
|
|
|
|
|
case Trigger::OtherTrigger:
|
|
while (true) {
|
|
n = _pcg.rand (all_triggers.size());
|
|
if ((uint64_t) n == current) {
|
|
continue;
|
|
}
|
|
if (!all_triggers[n]->region()) {
|
|
continue;
|
|
}
|
|
if (all_triggers[n]->active()) {
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
return n;
|
|
|
|
|
|
/* NOTREACHED */
|
|
case Trigger::Stop:
|
|
case Trigger::QueuedTrigger:
|
|
break;
|
|
|
|
}
|
|
|
|
return current;
|
|
}
|
|
|
|
XMLNode&
|
|
TriggerBox::get_state (void)
|
|
{
|
|
XMLNode& node (Processor::get_state ());
|
|
|
|
node.set_property (X_("type"), X_("triggerbox"));
|
|
node.set_property (X_("data-type"), _data_type.to_string());
|
|
|
|
XMLNode* trigger_child (new XMLNode (X_("Triggers")));
|
|
|
|
{
|
|
Glib::Threads::RWLock::ReaderLock lm (trigger_lock);
|
|
for (Triggers::iterator t = all_triggers.begin(); t != all_triggers.end(); ++t) {
|
|
trigger_child->add_child_nocopy ((*t)->get_state());
|
|
}
|
|
}
|
|
|
|
|
|
node.add_child_nocopy (*trigger_child);
|
|
|
|
return node;
|
|
}
|
|
|
|
int
|
|
TriggerBox::set_state (const XMLNode& node, int version)
|
|
{
|
|
node.get_property (X_("data-type"), _data_type);
|
|
|
|
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) {
|
|
Trigger* trig;
|
|
|
|
if (_data_type == DataType::AUDIO) {
|
|
trig = new AudioTrigger (all_triggers.size(), *this);
|
|
all_triggers.push_back (trig);
|
|
trig->set_state (**t, version);
|
|
} else {
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|