1019 lines
36 KiB
C++
1019 lines
36 KiB
C++
/*
|
|
* Copyright (C) 2021 Paul Davis <paul@linuxaudiosystems.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*/
|
|
|
|
#ifndef __ardour_triggerbox_h__
|
|
#define __ardour_triggerbox_h__
|
|
|
|
#include <pthread.h>
|
|
|
|
#include <atomic>
|
|
#include <map>
|
|
#include <vector>
|
|
#include <string>
|
|
#include <exception>
|
|
|
|
#include <glibmm/threads.h>
|
|
|
|
#include "pbd/crossthread.h"
|
|
#include "pbd/pcg_rand.h"
|
|
#include "pbd/pool.h"
|
|
#include "pbd/properties.h"
|
|
#include "pbd/ringbuffer.h"
|
|
#include "pbd/stateful.h"
|
|
|
|
#include "midi++/types.h"
|
|
|
|
#include "temporal/beats.h"
|
|
#include "temporal/bbt_time.h"
|
|
#include "temporal/tempo.h"
|
|
|
|
#include "evoral/PatchChange.h"
|
|
#include "evoral/SMF.h"
|
|
|
|
#include "ardour/midi_model.h"
|
|
#include "ardour/midi_state_tracker.h"
|
|
#include "ardour/processor.h"
|
|
#include "ardour/segment_descriptor.h"
|
|
#include "ardour/types.h"
|
|
#include "ardour/types_convert.h"
|
|
|
|
#include "ardour/libardour_visibility.h"
|
|
|
|
class XMLNode;
|
|
|
|
namespace RubberBand {
|
|
class RubberBandStretcher;
|
|
}
|
|
|
|
namespace MIDI {
|
|
class Parser;
|
|
}
|
|
|
|
namespace ARDOUR {
|
|
|
|
class Session;
|
|
class AudioRegion;
|
|
class MidiRegion;
|
|
class TriggerBox;
|
|
class SideChain;
|
|
class MidiPort;
|
|
|
|
typedef uint32_t color_t;
|
|
|
|
LIBARDOUR_API std::string cue_marker_name (int32_t);
|
|
|
|
class Trigger;
|
|
|
|
typedef std::shared_ptr<Trigger> TriggerPtr;
|
|
|
|
class LIBARDOUR_API Trigger : public PBD::Stateful {
|
|
public:
|
|
enum State {
|
|
/* This is the initial state for a Trigger, and means that it is not
|
|
* doing anything at all
|
|
*/
|
|
Stopped,
|
|
/* A Trigger in this state has been chosen by its parent TriggerBox
|
|
* (e.g. because of a bang() call that put it in the queue), a Trigger in
|
|
* this state is waiting for the next occurence of its quantization to
|
|
* occur before transitioning to Running
|
|
*/
|
|
WaitingToStart,
|
|
/* a Trigger in this state is going to deliver data during calls
|
|
* to its ::run() method.
|
|
*/
|
|
Running,
|
|
/* a Trigger in this state was running, has been re-triggered e.g. by a
|
|
* ::bang() call with LaunchStyle set to Repeat, and is waiting for the
|
|
* next occurence of its quantization to occur before transitioning
|
|
* back to Running.
|
|
*/
|
|
WaitingForRetrigger,
|
|
/* a Trigger in this state is delivering data during calls to ::run(), but
|
|
* is waiting for the next occurence of its quantization to occur when it will
|
|
* transition to Stopping and then Stopped.
|
|
*/
|
|
WaitingToStop,
|
|
/* a Trigger in this state is delivering data during calls to ::run(), but
|
|
* is waiting for the next occurence of another Trigger's quantization to occur when it will
|
|
* transition to Stopping and then Stopped (and be followed by
|
|
* the other Trigger.
|
|
*/
|
|
WaitingToSwitch,
|
|
/* a Trigger in this state was Running but noticed that it should stop
|
|
* during the current call to ::run(). By the end of that call, it will
|
|
* have transitioned to Stopped.
|
|
*/
|
|
Stopping,
|
|
};
|
|
|
|
enum LaunchStyle {
|
|
OneShot, /* mouse down/NoteOn starts; mouse up/NoteOff ignored */
|
|
ReTrigger, /* mouse down/NoteOn starts or retriggers; mouse up/NoteOff */
|
|
Gate, /* runs till mouse up/note off then to next quantization */
|
|
Toggle, /* runs till next mouse down/NoteOn */
|
|
Repeat, /* plays only quantization extent until mouse up/note off */
|
|
};
|
|
|
|
enum StretchMode { /* currently mapped to the matching RubberBand::RubberBandStretcher::Option */
|
|
Crisp,
|
|
Mixed,
|
|
Smooth,
|
|
};
|
|
|
|
Trigger (uint32_t index, TriggerBox&);
|
|
virtual ~Trigger() {}
|
|
|
|
static void make_property_quarks ();
|
|
|
|
protected:
|
|
/* properties controllable by the user */
|
|
|
|
PBD::Property<LaunchStyle> _launch_style;
|
|
PBD::Property<FollowAction> _follow_action0;
|
|
PBD::Property<FollowAction> _follow_action1;
|
|
PBD::Property<int> _follow_action_probability; /* 1 .. 100 */
|
|
PBD::Property<uint32_t> _follow_count;
|
|
PBD::Property<Temporal::BBT_Offset> _quantization;
|
|
PBD::Property<Temporal::BBT_Offset> _follow_length;
|
|
PBD::Property<bool> _use_follow_length;
|
|
PBD::Property<bool> _legato;
|
|
PBD::Property<gain_t> _gain;
|
|
PBD::Property<float> _velocity_effect;
|
|
PBD::Property<bool> _stretchable;
|
|
PBD::Property<bool> _cue_isolated;
|
|
PBD::Property<bool> _allow_patch_changes;
|
|
PBD::Property<StretchMode> _stretch_mode;
|
|
|
|
/* Properties that are not CAS-updated at retrigger */
|
|
|
|
PBD::Property<std::string> _name;
|
|
PBD::Property<color_t> _color;
|
|
|
|
public:
|
|
/* this is positioned here so that we can easily keep it in sync
|
|
with the properties list above.
|
|
*/
|
|
struct UIState {
|
|
std::atomic<unsigned int> generation; /* used for CAS */
|
|
|
|
LaunchStyle launch_style = OneShot;
|
|
FollowAction follow_action0 = FollowAction (FollowAction::Again);
|
|
FollowAction follow_action1 = FollowAction (FollowAction::Stop);
|
|
int follow_action_probability = 0;
|
|
uint32_t follow_count = 1;
|
|
Temporal::BBT_Offset quantization = Temporal::BBT_Offset (1, 0, 0);
|
|
Temporal::BBT_Offset follow_length = Temporal::BBT_Offset (1, 0, 0);
|
|
bool use_follow_length = false;
|
|
bool legato = false;
|
|
gain_t gain = 1.0;
|
|
float velocity_effect = 0;
|
|
bool stretchable = true;
|
|
bool cue_isolated = false;
|
|
bool allow_patch_changes = true;
|
|
StretchMode stretch_mode = Trigger::Crisp;
|
|
|
|
Evoral::SMF::UsedChannels used_channels = Evoral::SMF::UsedChannels();
|
|
Evoral::PatchChange<MidiBuffer::TimeType> patch_change[16];
|
|
|
|
std::string name = "";
|
|
color_t color = 0xBEBEBEFF;
|
|
double tempo = 0; //unset
|
|
|
|
UIState() : generation (0) {}
|
|
|
|
UIState& operator= (UIState const & other) {
|
|
|
|
/* we do not copy generation */
|
|
|
|
generation = 0;
|
|
|
|
launch_style = other.launch_style;
|
|
follow_action0 = other.follow_action0;
|
|
follow_action1 = other.follow_action1;
|
|
follow_action_probability = other.follow_action_probability;
|
|
follow_count = other.follow_count;
|
|
quantization = other.quantization;
|
|
follow_length = other.follow_length;
|
|
use_follow_length = other.use_follow_length;
|
|
legato = other.legato;
|
|
gain = other.gain;
|
|
velocity_effect = other.velocity_effect;
|
|
stretchable = other.stretchable;
|
|
cue_isolated = other.cue_isolated;
|
|
allow_patch_changes = other.allow_patch_changes;
|
|
stretch_mode = other.stretch_mode;
|
|
used_channels = other.used_channels;
|
|
|
|
for (int i = 0; i<16; i++) {
|
|
if (other.patch_change[i].is_set()) {
|
|
patch_change[i] = other.patch_change[i];
|
|
}
|
|
}
|
|
|
|
name = other.name;
|
|
color = other.color;
|
|
tempo = other.tempo;
|
|
|
|
return *this;
|
|
}
|
|
};
|
|
|
|
#define TRIGGERBOX_PROPERTY_DECL(name,type) void set_ ## name (type); type name () const;
|
|
#define TRIGGERBOX_PROPERTY_DECL_CONST_REF(name,type) void set_ ## name (type const &); type name () const
|
|
|
|
TRIGGERBOX_PROPERTY_DECL (launch_style, LaunchStyle);
|
|
TRIGGERBOX_PROPERTY_DECL_CONST_REF (follow_action0, FollowAction);
|
|
TRIGGERBOX_PROPERTY_DECL_CONST_REF (follow_action1, FollowAction);
|
|
TRIGGERBOX_PROPERTY_DECL (follow_action_probability, int);
|
|
TRIGGERBOX_PROPERTY_DECL (follow_count, uint32_t);
|
|
TRIGGERBOX_PROPERTY_DECL_CONST_REF (quantization, Temporal::BBT_Offset);
|
|
TRIGGERBOX_PROPERTY_DECL_CONST_REF (follow_length, Temporal::BBT_Offset);
|
|
TRIGGERBOX_PROPERTY_DECL (use_follow_length, bool);
|
|
TRIGGERBOX_PROPERTY_DECL (legato, bool);
|
|
TRIGGERBOX_PROPERTY_DECL (velocity_effect, float);
|
|
TRIGGERBOX_PROPERTY_DECL (stretchable, bool);
|
|
TRIGGERBOX_PROPERTY_DECL (cue_isolated, bool);
|
|
TRIGGERBOX_PROPERTY_DECL (allow_patch_changes, bool);
|
|
TRIGGERBOX_PROPERTY_DECL (gain, gain_t);
|
|
TRIGGERBOX_PROPERTY_DECL (stretch_mode, StretchMode);
|
|
TRIGGERBOX_PROPERTY_DECL (color, color_t);
|
|
TRIGGERBOX_PROPERTY_DECL_CONST_REF (name, std::string);
|
|
|
|
#undef TRIGGERBOX_PROPERTY_DECL
|
|
#undef TRIGGERBOX_PROPERTY_DECL_CONST_REF
|
|
|
|
/* Calling ::bang() will cause this Trigger to be placed in its owning
|
|
TriggerBox's queue.
|
|
*/
|
|
void bang (float velocity = 1.0f);
|
|
|
|
/* Calling ::unbang() is equivalent to a mouse-up or note-off
|
|
... it MIGHT cause a clip to stop, but more likely has no effect, depending on the slot's launch-style.
|
|
*/
|
|
void unbang ();
|
|
|
|
/* Calling ::request_stop() to stop a Trigger at the earliest possible
|
|
* opportunity, rather than at the next quantization point.
|
|
*/
|
|
void request_stop ();
|
|
|
|
/* Call ::stop_quantized() to stop a Trigger at the next quantization point.
|
|
*/
|
|
void stop_quantized ();
|
|
|
|
virtual void tempo_map_changed() {}
|
|
|
|
virtual pframes_t run (BufferSet&, samplepos_t start_sample, samplepos_t end_sample,
|
|
Temporal::Beats const & start, Temporal::Beats const & end,
|
|
pframes_t nframes, pframes_t offset, double bpm, pframes_t& quantize_offset) = 0;
|
|
virtual void set_start (timepos_t const &) = 0;
|
|
virtual void set_end (timepos_t const &) = 0;
|
|
virtual void set_length (timecnt_t const &) = 0;
|
|
virtual void reload (BufferSet&, void*) = 0;
|
|
virtual void io_change () {}
|
|
virtual void set_legato_offset (timepos_t const & offset) = 0;
|
|
|
|
timepos_t current_pos() const;
|
|
double position_as_fraction() const;
|
|
|
|
Temporal::BBT_Argument compute_start (Temporal::TempoMap::SharedPtr const &, samplepos_t start, samplepos_t end, Temporal::BBT_Offset const & q, samplepos_t& start_samples, bool& will_start);
|
|
virtual timepos_t compute_end (Temporal::TempoMap::SharedPtr const &, Temporal::BBT_Time const &, samplepos_t, Temporal::Beats &) = 0;
|
|
virtual void start_and_roll_to (samplepos_t start, samplepos_t position, uint32_t cnt) = 0;
|
|
|
|
/* because follow actions involve probability is it easier to code the will-not-follow case */
|
|
|
|
bool will_not_follow() const;
|
|
bool will_follow() const { return !will_not_follow(); }
|
|
|
|
/* assumes that this is currently playing but does not enforce it */
|
|
bool cue_launched() const { return _cue_launched; }
|
|
|
|
virtual bool probably_oneshot () const = 0;
|
|
|
|
virtual timepos_t start_offset () const = 0; /* offset from start of data */
|
|
virtual timepos_t current_length() const = 0; /* offset from start() */
|
|
virtual timepos_t natural_length() const = 0; /* offset from start() */
|
|
|
|
void process_state_requests (BufferSet& bufs, pframes_t dest_offset);
|
|
|
|
bool active() const { return _state >= Running; }
|
|
State state() const { return _state; }
|
|
|
|
virtual bool has_data() const = 0;
|
|
|
|
void set_region (std::shared_ptr<Region>, bool use_thread = true);
|
|
void clear_region ();
|
|
virtual int set_region_in_worker_thread (std::shared_ptr<Region>) = 0;
|
|
std::shared_ptr<Region> region() const { return _region; }
|
|
|
|
uint32_t index() const { return _index; }
|
|
|
|
/* Managed by TriggerBox, these record the time that the trigger is
|
|
* scheduled to start or stop at. Computed in
|
|
* Trigger::maybe_compute_next_transition().
|
|
*/
|
|
samplepos_t transition_samples;
|
|
Temporal::Beats transition_beats;
|
|
Temporal::BBT_Time _transition_bbt;
|
|
|
|
XMLNode& get_state () const;
|
|
int set_state (const XMLNode&, int version);
|
|
|
|
void maybe_compute_next_transition (samplepos_t start_sample, Temporal::Beats const & start, Temporal::Beats const & end, pframes_t& nframes, pframes_t& dest_offset);
|
|
|
|
|
|
bool compute_quantized_transition (samplepos_t start_sample, Temporal::Beats const & start, Temporal::Beats const & end,
|
|
Temporal::BBT_Argument& t_bbt, Temporal::Beats& t_beats, samplepos_t& t_samples,
|
|
Temporal::TempoMap::SharedPtr const & tmap, Temporal::BBT_Offset const & q);
|
|
|
|
pframes_t compute_next_transition (samplepos_t start_sample, Temporal::Beats const & start, Temporal::Beats const & end, pframes_t nframes,
|
|
Temporal::BBT_Argument& t_bbt, Temporal::Beats& t_beats, samplepos_t& t_samples,
|
|
Temporal::TempoMap::SharedPtr const & tmap);
|
|
|
|
|
|
template<typename TriggerType>
|
|
void 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, pframes_t&), uint32_t cnt);
|
|
void set_next_trigger (int n);
|
|
int next_trigger() const { return _next_trigger; }
|
|
|
|
/* any non-zero value will work for the default argument, and means
|
|
"use your own launch quantization". BBT_Offset (0, 0, 0) means what
|
|
it says: start immediately
|
|
*/
|
|
void startup (BufferSet&, pframes_t dest_offset, Temporal::BBT_Offset const & start_quantization = Temporal::BBT_Offset (9, 3,0));
|
|
void startup_from_ffwd (BufferSet&, uint32_t loop_cnt);
|
|
void shutdown_from_fwd ();
|
|
virtual void shutdown (BufferSet& bufs, pframes_t dest_offset);
|
|
virtual void jump_start ();
|
|
virtual void jump_stop (BufferSet& bufs, pframes_t dest_offset);
|
|
void begin_stop (bool explicit_stop = false);
|
|
void begin_switch (TriggerPtr);
|
|
|
|
bool explicitly_stopped() const { return _explicitly_stopped; }
|
|
|
|
uint32_t loop_count() const { return _loop_cnt; }
|
|
|
|
void set_ui (void*);
|
|
void* ui () const { return _ui; }
|
|
|
|
TriggerBox& box() const { return _box; }
|
|
|
|
double estimated_tempo() const { return _estimated_tempo; }
|
|
|
|
/* the following functions deal with audio- or midi-specific SegmentDescriptor properties, provided as virtuals so we don't have to do lots of dynamic_casting */
|
|
/* segment_tempo is currently a no-op for MIDI, but may be implemented later */
|
|
virtual double segment_tempo() const = 0;
|
|
virtual void set_segment_tempo (double t) = 0;
|
|
|
|
/* used_channels is a no-op for audio */
|
|
virtual Evoral::SMF::UsedChannels used_channels() const { return Evoral::SMF::UsedChannels(); }
|
|
virtual void set_used_channels (Evoral::SMF::UsedChannels) {}
|
|
|
|
/* patch changes are a no-op for audio */
|
|
virtual void set_patch_change (Evoral::PatchChange<MidiBuffer::TimeType> const &) {}
|
|
virtual Evoral::PatchChange<MidiBuffer::TimeType> const patch_change (uint8_t) const { return Evoral::PatchChange<MidiBuffer::TimeType>(); }
|
|
virtual void unset_patch_change (uint8_t channel) {}
|
|
virtual void unset_all_patch_changes () {}
|
|
virtual bool patch_change_set (uint8_t channel) const { return false; }
|
|
|
|
virtual void setup_stretcher () = 0;
|
|
|
|
Temporal::Meter meter() const { return _meter; }
|
|
|
|
void set_velocity_gain (gain_t g) {_pending_velocity_gain=g;}
|
|
|
|
void set_pending (Trigger*);
|
|
Trigger* swap_pending (Trigger*);
|
|
|
|
void update_properties ();
|
|
|
|
static Trigger * const MagicClearPointerValue;
|
|
|
|
virtual SegmentDescriptor get_segment_descriptor () const = 0;
|
|
|
|
static void request_trigger_delete (Trigger* t);
|
|
|
|
/* these operations are provided to get/set all the "user visible" trigger properties at once */
|
|
/* examples: drag+dropping from slot to slot, or "Range->Bounce to Slot", where a single operation sets many */
|
|
void get_ui_state (UIState &state) const;
|
|
void set_ui_state (UIState &state);
|
|
|
|
static PBD::Signal2<void,PBD::PropertyChange,Trigger*> TriggerPropertyChange;
|
|
|
|
protected:
|
|
struct UIRequests {
|
|
std::atomic<bool> stop;
|
|
UIRequests() : stop (false) {}
|
|
};
|
|
|
|
std::shared_ptr<Region> _region;
|
|
samplecnt_t process_index;
|
|
samplepos_t final_processed_sample; /* where we stop playing, in process time, compare with process_index */
|
|
UIState ui_state;
|
|
TriggerBox& _box;
|
|
UIRequests _requests;
|
|
State _state;
|
|
bool _playout;
|
|
std::atomic<int> _bang;
|
|
std::atomic<int> _unbang;
|
|
uint32_t _index;
|
|
int _next_trigger;
|
|
uint32_t _loop_cnt; /* how many times in a row has this played */
|
|
void* _ui;
|
|
bool _explicitly_stopped;
|
|
gain_t _pending_velocity_gain;
|
|
gain_t _velocity_gain;
|
|
bool _cue_launched;
|
|
|
|
/* these are only used by midi triggers but the ui_state API needs them */
|
|
Evoral::SMF::UsedChannels _used_channels;
|
|
Evoral::PatchChange<MidiBuffer::TimeType> _patch_change[16];
|
|
std::vector<int> _channel_map;
|
|
|
|
void copy_to_ui_state ();
|
|
|
|
|
|
/* computed from data */
|
|
|
|
double _estimated_tempo; //TODO: this should come from the MIDI file
|
|
double _segment_tempo; //TODO: this will likely get stored in the SegmentDescriptor for audio triggers
|
|
|
|
/* basic process is :
|
|
1) when a file is loaded, we infer its bpm either by minibpm's estimate, a flag in the filename, metadata (TBD) or other means
|
|
2) we assume the clip must have an integer number of beats in it (simplest case is a one-bar loop with 4 beats in it)
|
|
3) ...so we round to the nearest beat length, and set the tempo to *exactly* fit the sample-length into the assumed beat-length
|
|
4) the user may recognize a problem: "this was a 3/4 beat, which was rounded to 4 beats but it should have been 3"
|
|
5) if the user changes the beat-length, then the tempo is recalculated for use during stretching
|
|
6) someday, we will also allow the sample start and length to be adjusted in a trimmer, and that will also adjust the tempo
|
|
7) in all cases the user should be in final control; but our "internal" value for stretching are just sample-start and BPM, end of story
|
|
*/
|
|
double _beatcnt;
|
|
Temporal::Meter _meter;
|
|
|
|
samplepos_t expected_end_sample;
|
|
Temporal::BBT_Offset _start_quantization;
|
|
Temporal::BBT_Offset _nxt_quantization;
|
|
std::atomic<Trigger*> _pending;
|
|
std::atomic<unsigned int> last_property_generation;
|
|
|
|
void when_stopped_during_run (BufferSet& bufs, pframes_t dest_offset);
|
|
void set_region_internal (std::shared_ptr<Region>);
|
|
virtual void retrigger();
|
|
virtual void _startup (BufferSet&, pframes_t dest_offset, Temporal::BBT_Offset const &);
|
|
|
|
bool internal_use_follow_length() const;
|
|
void send_property_change (PBD::PropertyChange pc);
|
|
};
|
|
|
|
class LIBARDOUR_API AudioTrigger : public Trigger {
|
|
public:
|
|
AudioTrigger (uint32_t index, TriggerBox&);
|
|
~AudioTrigger ();
|
|
|
|
template<bool actually_run> pframes_t 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, pframes_t& quantize_offset);
|
|
|
|
pframes_t 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, pframes_t& quantize_offset) {
|
|
return audio_run<true> (bufs, start_sample, end_sample, start, end, nframes, dest_offset, bpm, quantize_offset);
|
|
}
|
|
|
|
bool has_data() const { return data.length > 0; }
|
|
uint32_t n_channels() const { return data.size(); }
|
|
|
|
StretchMode stretch_mode() const { return _stretch_mode; }
|
|
void set_stretch_mode (StretchMode);
|
|
|
|
double segment_tempo() const { return _segment_tempo; }
|
|
void set_segment_tempo (double t);
|
|
|
|
double segment_beatcnt () { return _beatcnt; }
|
|
void set_segment_beatcnt (double count);
|
|
|
|
void set_start (timepos_t const &);
|
|
void set_end (timepos_t const &);
|
|
void set_legato_offset (timepos_t const &);
|
|
void set_length (timecnt_t const &);
|
|
timepos_t start_offset () const; /* offset from start of data */
|
|
timepos_t current_length() const; /* offset from start of data */
|
|
timepos_t natural_length() const; /* offset from start of data */
|
|
void reload (BufferSet&, void*);
|
|
void io_change ();
|
|
bool probably_oneshot () const;
|
|
|
|
int set_region_in_worker_thread (std::shared_ptr<Region>);
|
|
void jump_start ();
|
|
void jump_stop (BufferSet& bufs, pframes_t dest_offset);
|
|
|
|
XMLNode& get_state () const;
|
|
int set_state (const XMLNode&, int version);
|
|
|
|
RubberBand::RubberBandStretcher* stretcher() { return (_stretcher); }
|
|
|
|
SegmentDescriptor get_segment_descriptor () const;
|
|
timepos_t compute_end (Temporal::TempoMap::SharedPtr const &, Temporal::BBT_Time const &, samplepos_t, Temporal::Beats &);
|
|
void start_and_roll_to (samplepos_t start, samplepos_t position, uint32_t cnt);
|
|
|
|
bool stretching () const;
|
|
|
|
protected:
|
|
void retrigger ();
|
|
|
|
private:
|
|
struct Data : std::vector<Sample*> {
|
|
samplecnt_t length;
|
|
|
|
Data () : length (0) {}
|
|
};
|
|
|
|
Data data;
|
|
RubberBand::RubberBandStretcher* _stretcher;
|
|
samplepos_t _start_offset;
|
|
|
|
|
|
/* computed during run */
|
|
|
|
samplecnt_t read_index;
|
|
samplepos_t last_readable_sample; /* where the data runs out, relative to the start of the data, compare with read_index */
|
|
samplepos_t _legato_offset;
|
|
samplecnt_t retrieved;
|
|
samplecnt_t got_stretcher_padding;
|
|
samplecnt_t to_pad;
|
|
samplecnt_t to_drop;
|
|
|
|
virtual void setup_stretcher ();
|
|
|
|
void drop_data ();
|
|
int load_data (std::shared_ptr<AudioRegion>);
|
|
int load_data (BufferSet const &);
|
|
void estimate_tempo ();
|
|
void reset_stretcher ();
|
|
void _startup (BufferSet&, pframes_t dest_offset, Temporal::BBT_Offset const &);
|
|
};
|
|
|
|
|
|
class LIBARDOUR_API MIDITrigger : public Trigger {
|
|
public:
|
|
MIDITrigger (uint32_t index, TriggerBox&);
|
|
~MIDITrigger ();
|
|
|
|
template<bool actually_run> pframes_t midi_run (BufferSet&, samplepos_t start_sample, samplepos_t end_sample,
|
|
Temporal::Beats const & start_beats, Temporal::Beats const & end_beats, pframes_t nframes, pframes_t offset, double bpm, pframes_t& quantize_offset);
|
|
|
|
pframes_t 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, pframes_t& quantize_offset) {
|
|
return midi_run<true> (bufs, start_sample, end_sample, start, end, nframes, dest_offset, bpm, quantize_offset);
|
|
}
|
|
|
|
bool has_data() const { return (bool) model; }
|
|
|
|
void set_start (timepos_t const &);
|
|
void set_end (timepos_t const &);
|
|
void set_legato_offset (timepos_t const &);
|
|
void set_length (timecnt_t const &);
|
|
timepos_t start_offset () const;
|
|
timepos_t end() const; /* offset from start of data */
|
|
timepos_t current_length() const; /* offset from start of data */
|
|
timepos_t natural_length() const; /* offset from start of data */
|
|
void reload (BufferSet&, void*);
|
|
bool probably_oneshot () const;
|
|
|
|
void tempo_map_changed();
|
|
void estimate_midi_patches ();
|
|
|
|
int set_region_in_worker_thread (std::shared_ptr<Region>);
|
|
void jump_start ();
|
|
void shutdown (BufferSet& bufs, pframes_t dest_offset);
|
|
void jump_stop (BufferSet& bufs, pframes_t dest_offset);
|
|
|
|
XMLNode& get_state () const;
|
|
int set_state (const XMLNode&, int version);
|
|
|
|
SegmentDescriptor get_segment_descriptor () const;
|
|
timepos_t compute_end (Temporal::TempoMap::SharedPtr const &, Temporal::BBT_Time const &, samplepos_t, Temporal::Beats &);
|
|
void start_and_roll_to (samplepos_t start, samplepos_t position, uint32_t cnt);
|
|
|
|
void set_patch_change (Evoral::PatchChange<MidiBuffer::TimeType> const &);
|
|
Evoral::PatchChange<MidiBuffer::TimeType> const patch_change (uint8_t) const;
|
|
void unset_patch_change (uint8_t channel);
|
|
void unset_all_patch_changes ();
|
|
bool patch_change_set (uint8_t channel) const;
|
|
|
|
/* It's possible that a portion of a midi file would use a subset of the total channels used, so store that info in the segment descriptor */
|
|
Evoral::SMF::UsedChannels used_channels() const { return ui_state.used_channels; }
|
|
void set_used_channels (Evoral::SMF::UsedChannels);
|
|
|
|
/* theoretically, MIDI files can have a dedicated tempo outside the session tempo map (*un-stretched*) but this is currently unimplemented */
|
|
/* boilerplate tempo functions are provided here so we don't have to do constant dynamic_cast checks to use the tempo+stretch APIs */
|
|
virtual double segment_tempo() const {return 120.0;}
|
|
virtual void set_segment_tempo (double t) {}
|
|
virtual void setup_stretcher () {}
|
|
|
|
void set_channel_map (int channel, int target);
|
|
void unset_channel_map (int channel);
|
|
int channel_map (int channel);
|
|
std::vector<int> const & channel_map() const { return _channel_map; }
|
|
|
|
protected:
|
|
void retrigger ();
|
|
|
|
private:
|
|
PBD::ID data_source;
|
|
PBD::ScopedConnection content_connection;
|
|
|
|
Temporal::Beats final_beat;
|
|
|
|
Temporal::DoubleableBeats data_length; /* using timestamps from data */
|
|
Temporal::DoubleableBeats last_event_beats;
|
|
samplepos_t last_event_samples;
|
|
|
|
Temporal::BBT_Offset _start_offset;
|
|
Temporal::BBT_Offset _legato_offset;
|
|
|
|
std::shared_ptr<MidiModel> model;
|
|
MidiModel::const_iterator iter;
|
|
bool map_change;
|
|
|
|
int load_data (std::shared_ptr<MidiRegion>);
|
|
int load_data (BufferSet const &);
|
|
void compute_and_set_length ();
|
|
void _startup (BufferSet&, pframes_t dest_offset, Temporal::BBT_Offset const &);
|
|
};
|
|
|
|
class LIBARDOUR_API TriggerBoxThread
|
|
{
|
|
public:
|
|
TriggerBoxThread ();
|
|
~TriggerBoxThread();
|
|
|
|
static void init_request_pool() { Request::init_pool(); }
|
|
|
|
void set_region (TriggerBox&, uint32_t slot, std::shared_ptr<Region>);
|
|
void request_delete_trigger (Trigger* t);
|
|
|
|
void summon();
|
|
void stop();
|
|
void wait_until_finished();
|
|
|
|
private:
|
|
static void* _thread_work(void *arg);
|
|
void* thread_work();
|
|
|
|
enum RequestType {
|
|
Quit,
|
|
SetRegion,
|
|
DeleteTrigger
|
|
};
|
|
|
|
struct Request {
|
|
|
|
Request (RequestType t) : type (t) {}
|
|
|
|
RequestType type;
|
|
/* for set region */
|
|
TriggerBox* box;
|
|
uint32_t slot;
|
|
std::shared_ptr<Region> region;
|
|
/* for DeleteTrigger */
|
|
Trigger* trigger;
|
|
|
|
void* operator new (size_t);
|
|
void operator delete (void* ptr, size_t);
|
|
|
|
static PBD::MultiAllocSingleReleasePool* pool;
|
|
static void init_pool ();
|
|
};
|
|
|
|
pthread_t thread;
|
|
PBD::RingBuffer<Request*> requests;
|
|
|
|
CrossThreadChannel _xthread;
|
|
void queue_request (Request*);
|
|
void delete_trigger (Trigger*);
|
|
};
|
|
|
|
struct CueRecord {
|
|
int32_t cue_number;
|
|
samplepos_t when;
|
|
|
|
CueRecord (int32_t cn, samplepos_t t): cue_number (cn), when (t) {}
|
|
CueRecord () : cue_number (0), when (0) {}
|
|
|
|
static const int32_t stop_all = INT32_MAX;
|
|
};
|
|
|
|
typedef PBD::RingBuffer<CueRecord> CueRecords;
|
|
|
|
class LIBARDOUR_API TriggerBox : public Processor
|
|
{
|
|
public:
|
|
|
|
#ifdef MIXBUS
|
|
static const int32_t default_triggers_per_box = 8;
|
|
#else
|
|
static const int32_t default_triggers_per_box = 16;
|
|
#endif
|
|
|
|
TriggerBox (Session&, DataType dt);
|
|
~TriggerBox ();
|
|
|
|
static CueRecords cue_records;
|
|
static bool cue_recording () { return _cue_recording; }
|
|
static void set_cue_recording (bool yn);
|
|
static PBD::Signal0<void> CueRecordingChanged;
|
|
|
|
void run (BufferSet& bufs, samplepos_t start_sample, samplepos_t end_sample, double speed, pframes_t nframes, bool result_required);
|
|
bool can_support_io_configuration (const ChanCount& in, ChanCount& out);
|
|
bool configure_io (ChanCount in, ChanCount out);
|
|
|
|
bool empty() const { return _active_slots == 0; }
|
|
PBD::Signal0<void> EmptyStatusChanged;
|
|
|
|
int32_t order() const { return _order; }
|
|
void set_order(int32_t n);
|
|
|
|
typedef std::vector<TriggerPtr> Triggers;
|
|
|
|
TriggerPtr trigger (Triggers::size_type);
|
|
|
|
void bang_trigger_at (Triggers::size_type row, float velocity = 1.0f);
|
|
void unbang_trigger_at (Triggers::size_type row);
|
|
|
|
void add_trigger (TriggerPtr);
|
|
|
|
void fast_forward (CueEvents const &, samplepos_t transport_postiion);
|
|
bool fast_forwarding() const { return _fast_forwarding; }
|
|
|
|
void set_pending (uint32_t slot, Trigger*);
|
|
|
|
XMLNode& get_state () const;
|
|
int set_state (const XMLNode&, int version);
|
|
|
|
void deep_sources (std::set<std::shared_ptr<Source>>&);
|
|
void used_regions (std::set<std::shared_ptr<Region>>&);
|
|
|
|
void set_from_path (uint32_t slot, std::string const & path);
|
|
void set_from_selection (uint32_t slot, std::shared_ptr<Region>);
|
|
|
|
DataType data_type() const { return _data_type; }
|
|
|
|
void stop_all_immediately ();
|
|
void stop_all_quantized ();
|
|
|
|
TriggerPtr currently_playing() const { return _currently_playing; }
|
|
|
|
TriggerPtr trigger_by_id (PBD::ID);
|
|
|
|
void clear_all_triggers ();
|
|
void clear_cue (int cue);
|
|
void set_all_follow_action (ARDOUR::FollowAction const &, uint32_t n=0);
|
|
void set_all_launch_style (ARDOUR::Trigger::LaunchStyle);
|
|
void set_all_quantization (Temporal::BBT_Offset const&);
|
|
void set_all_probability (int zero_to_a_hundred);
|
|
|
|
/* Returns a negative value is there is no active Trigger, or a value between 0
|
|
* and 1.0 if there is, corresponding to the value of position_as_fraction() for
|
|
* the active Trigger.
|
|
*/
|
|
double position_as_fraction() const;
|
|
|
|
void queue_explict (uint32_t);
|
|
TriggerPtr get_next_trigger ();
|
|
TriggerPtr peek_next_trigger ();
|
|
|
|
void request_reload (int32_t slot, void*);
|
|
void set_region (uint32_t slot, std::shared_ptr<Region> region);
|
|
|
|
void non_realtime_transport_stop (samplepos_t now, bool flush);
|
|
void non_realtime_locate (samplepos_t now);
|
|
void realtime_handle_transport_stopped ();
|
|
|
|
void enqueue_trigger_state_for_region (std::shared_ptr<Region>, std::shared_ptr<Trigger::UIState>);
|
|
|
|
void tempo_map_changed ();
|
|
|
|
/* valid only within the ::run() call tree */
|
|
int32_t active_scene() const { return _active_scene; }
|
|
|
|
PBD::Signal1<void,uint32_t> TriggerSwapped;
|
|
|
|
enum TriggerMidiMapMode {
|
|
AbletonPush,
|
|
SequentialNote,
|
|
ByMidiChannel,
|
|
Custom,
|
|
};
|
|
|
|
/* This is null for TriggerBoxen constructed with DataType::AUDIO */
|
|
MidiStateTracker* tracker;
|
|
|
|
static bool lookup_custom_midi_binding (std::vector<uint8_t> const &, int& x, int& y);
|
|
static void add_custom_midi_binding (std::vector<uint8_t> const &, int x, int y);
|
|
static void remove_custom_midi_binding (int x, int y);
|
|
static void clear_custom_midi_bindings ();
|
|
static int save_custom_midi_bindings (std::string const & path);
|
|
static int load_custom_midi_bindings (XMLNode const &);
|
|
static XMLNode* get_custom_midi_binding_state ();
|
|
|
|
void begin_midi_learn (int index);
|
|
void midi_unlearn (int index);
|
|
void stop_midi_learn ();
|
|
|
|
static Temporal::BBT_Offset assumed_trigger_duration () { return _assumed_trigger_duration; }
|
|
static void set_assumed_trigger_duration (Temporal::BBT_Offset const &);
|
|
|
|
static TriggerMidiMapMode midi_map_mode () { return _midi_map_mode; }
|
|
static void set_midi_map_mode (TriggerMidiMapMode m);
|
|
|
|
static int first_midi_note() { return _first_midi_note; }
|
|
static void set_first_midi_note (int n);
|
|
|
|
static void init ();
|
|
static void static_init (Session&);
|
|
static void begin_process_cycle ();
|
|
|
|
static TriggerBoxThread* worker;
|
|
|
|
static void start_transport_stop (Session&);
|
|
|
|
static PBD::PropertyChange all_trigger_props();
|
|
|
|
void send_property_change (PBD::PropertyChange pc);
|
|
static PBD::Signal2<void,PBD::PropertyChange,int> TriggerBoxPropertyChange;
|
|
|
|
void dump (std::ostream &) const;
|
|
|
|
private:
|
|
struct Requests {
|
|
std::atomic<bool> stop_all;
|
|
|
|
Requests () : stop_all (false) {}
|
|
};
|
|
|
|
static Temporal::BBT_Offset _assumed_trigger_duration;
|
|
|
|
DataType _data_type;
|
|
int32_t _order;
|
|
mutable Glib::Threads::RWLock trigger_lock; /* protects all_triggers */
|
|
Triggers all_triggers;
|
|
|
|
typedef std::vector<Trigger*> PendingTriggers;
|
|
PendingTriggers pending;
|
|
|
|
PBD::RingBuffer<uint32_t> explicit_queue; /* user queued triggers */
|
|
TriggerPtr _currently_playing;
|
|
Requests _requests;
|
|
bool _stop_all;
|
|
int32_t _active_scene;
|
|
int32_t _active_slots;
|
|
bool _locate_armed;
|
|
bool _cancel_locate_armed;
|
|
bool _fast_forwarding;
|
|
|
|
PBD::PCGRand _pcg;
|
|
|
|
/* These four are accessed (read/write) only from process() context */
|
|
|
|
void drop_triggers ();
|
|
void process_ui_trigger_requests ();
|
|
void process_midi_trigger_requests (BufferSet&);
|
|
int determine_next_trigger (uint32_t n);
|
|
void stop_all ();
|
|
|
|
void maybe_swap_pending (uint32_t);
|
|
|
|
void parameter_changed (std::string const &);
|
|
static void static_parameter_changed (std::string const &);
|
|
|
|
static int _first_midi_note;
|
|
static TriggerMidiMapMode _midi_map_mode;
|
|
|
|
struct Request {
|
|
enum Type {
|
|
Use,
|
|
Reload,
|
|
};
|
|
|
|
Type type;
|
|
|
|
/* cannot use a union here because we need Request to have a
|
|
* "trivial" constructor.
|
|
*/
|
|
|
|
TriggerPtr trigger;
|
|
void* ptr;
|
|
int32_t slot;
|
|
|
|
Request (Type t) : type (t) {}
|
|
|
|
static PBD::MultiAllocSingleReleasePool* pool;
|
|
static void init_pool();
|
|
|
|
void* operator new (size_t);
|
|
void operator delete (void* ptr, size_t);
|
|
};
|
|
|
|
typedef PBD::RingBuffer<Request*> RequestBuffer;
|
|
RequestBuffer requests;
|
|
|
|
void process_requests (BufferSet&);
|
|
void process_request (BufferSet&, Request*);
|
|
|
|
void reload (BufferSet& bufs, int32_t slot, void* ptr);
|
|
|
|
void cancel_locate_armed ();
|
|
void fast_forward_nothing_to_do ();
|
|
int handle_stopped_trigger (BufferSet& bufs, pframes_t dest_offset);
|
|
|
|
PBD::ScopedConnection stop_all_connection;
|
|
|
|
typedef std::map<std::vector<uint8_t>,std::pair<int,int> > CustomMidiMap;
|
|
static CustomMidiMap _custom_midi_map;
|
|
|
|
static void midi_input_handler (MIDI::Parser&, MIDI::byte*, size_t, samplecnt_t);
|
|
static std::shared_ptr<MIDI::Parser> input_parser;
|
|
static PBD::ScopedConnection midi_input_connection;
|
|
static void input_port_check ();
|
|
static PBD::ScopedConnectionList static_connections;
|
|
static std::shared_ptr<MidiPort> current_input;
|
|
|
|
static bool _learning;
|
|
static std::pair<int,int> learning_for;
|
|
static PBD::Signal0<void> TriggerMIDILearned;
|
|
|
|
static void init_pool();
|
|
|
|
static std::atomic<int> active_trigger_boxes;
|
|
static std::atomic<bool> _cue_recording;
|
|
static bool roll_requested;
|
|
static void maybe_request_roll (Session&);
|
|
};
|
|
|
|
class TriggerReference
|
|
{
|
|
public:
|
|
TriggerReference () : box (0), slot (0) {}
|
|
TriggerReference (ARDOUR::TriggerBox& b, uint32_t s) : box (&b), slot (s) {}
|
|
|
|
std::shared_ptr<ARDOUR::Trigger> trigger() const { assert (box); return box->trigger (slot); }
|
|
|
|
ARDOUR::TriggerBox* box;
|
|
uint32_t slot;
|
|
};
|
|
|
|
namespace Properties {
|
|
LIBARDOUR_API extern PBD::PropertyDescriptor<bool> running;
|
|
LIBARDOUR_API extern PBD::PropertyDescriptor<bool> legato;
|
|
LIBARDOUR_API extern PBD::PropertyDescriptor<bool> use_follow_length;
|
|
LIBARDOUR_API extern PBD::PropertyDescriptor<Temporal::BBT_Offset> quantization;
|
|
LIBARDOUR_API extern PBD::PropertyDescriptor<Temporal::BBT_Offset> follow_length;
|
|
LIBARDOUR_API extern PBD::PropertyDescriptor<Trigger::LaunchStyle> launch_style;
|
|
LIBARDOUR_API extern PBD::PropertyDescriptor<FollowAction> follow_action0;
|
|
LIBARDOUR_API extern PBD::PropertyDescriptor<FollowAction> follow_action1;
|
|
LIBARDOUR_API extern PBD::PropertyDescriptor<Trigger::StretchMode> stretch_mode;
|
|
LIBARDOUR_API extern PBD::PropertyDescriptor<uint32_t> follow_count;
|
|
LIBARDOUR_API extern PBD::PropertyDescriptor<int> follow_action_probability;
|
|
LIBARDOUR_API extern PBD::PropertyDescriptor<float> velocity_effect;
|
|
LIBARDOUR_API extern PBD::PropertyDescriptor<gain_t> gain;
|
|
LIBARDOUR_API extern PBD::PropertyDescriptor<uint32_t> currently_playing;
|
|
LIBARDOUR_API extern PBD::PropertyDescriptor<uint32_t> queued;
|
|
LIBARDOUR_API extern PBD::PropertyDescriptor<bool> stretchable;
|
|
LIBARDOUR_API extern PBD::PropertyDescriptor<bool> cue_isolated;
|
|
LIBARDOUR_API extern PBD::PropertyDescriptor<bool> allow_patch_changes;
|
|
LIBARDOUR_API extern PBD::PropertyDescriptor<bool> patch_change; /* type not important */
|
|
LIBARDOUR_API extern PBD::PropertyDescriptor<bool> channel_map; /* type not important */
|
|
LIBARDOUR_API extern PBD::PropertyDescriptor<bool> used_channels; /* type not important */
|
|
|
|
LIBARDOUR_API extern PBD::PropertyDescriptor<bool> tempo_meter; /* only used to transmit changes, not storage */
|
|
}
|
|
|
|
|
|
} // namespace ARDOUR
|
|
|
|
namespace PBD {
|
|
DEFINE_ENUM_CONVERT(ARDOUR::FollowAction::Type);
|
|
DEFINE_ENUM_CONVERT(ARDOUR::Trigger::LaunchStyle);
|
|
DEFINE_ENUM_CONVERT(ARDOUR::Trigger::StretchMode);
|
|
} /* namespace PBD */
|
|
|
|
|
|
#endif /* __ardour_triggerbox_h__ */
|