triggerbox: significantly re-design MIDI Triggers to allow for bounds editing

This commit is contained in:
Paul Davis
2024-11-24 13:25:49 -07:00
parent 7af9442098
commit c39de501a3
2 changed files with 177 additions and 72 deletions

View File

@@ -433,6 +433,9 @@ class LIBARDOUR_API Trigger : public PBD::Stateful {
static PBD::Signal<void(PBD::PropertyChange,Trigger*)> TriggerPropertyChange;
void region_property_change (PBD::PropertyChange const &);
virtual void bounds_changed (Temporal::timepos_t const & start, Temporal::timepos_t const & end) {}
protected:
struct UIRequests {
std::atomic<bool> stop;
@@ -489,6 +492,7 @@ class LIBARDOUR_API Trigger : public PBD::Stateful {
Temporal::BBT_Offset _nxt_quantization;
std::atomic<Trigger*> _pending;
std::atomic<unsigned int> last_property_generation;
PBD::ScopedConnection region_connection;
void when_stopped_during_run (BufferSet& bufs, pframes_t dest_offset);
void set_region_internal (std::shared_ptr<Region>);
@@ -669,6 +673,13 @@ class LIBARDOUR_API MIDITrigger : public Trigger {
void check_edit_swap (timepos_t const &, bool playing, BufferSet&);
RTMidiBufferBeats const & rt_midi_buffer() const { return *rt_midibuffer.load(); }
void bounds_changed (Temporal::timepos_t const & start, Temporal::timepos_t const & end);
Temporal::Beats play_start() const { return _play_start; }
Temporal::Beats play_end() const { return _play_end; }
Temporal::Beats loop_start() const { return _loop_start; }
Temporal::Beats loop_end() const { return _loop_end; }
protected:
void retrigger ();
@@ -688,15 +699,30 @@ class LIBARDOUR_API MIDITrigger : public Trigger {
PBD::ScopedConnection content_connection;
void model_contents_changed();
Temporal::Beats loop_start;
Temporal::Beats loop_end;
Temporal::Beats _play_start;
Temporal::Beats _play_end;
Temporal::Beats _loop_start;
Temporal::Beats _loop_end;
uint32_t first_event_index;
uint32_t last_event_index;
std::atomic<RTMidiBufferBeats*> rt_midibuffer;
std::atomic<RTMidiBufferBeats*> pending_rt_midibuffer;
std::atomic<RTMidiBufferBeats*> old_rt_midibuffer;
uint32_t iter;
uint32_t iter; /* index into the above RTMidiBufferBeats for current playback */
struct PendingSwap {
RTMidiBufferBeats* rt_midibuffer;
Temporal::Beats play_start;
Temporal::Beats play_end;
Temporal::Beats loop_start;
Temporal::Beats loop_end;
Temporal::Beats length;
PendingSwap() : rt_midibuffer (nullptr) {}
~PendingSwap() { delete rt_midibuffer; }
};
std::atomic<PendingSwap*> pending_swap;
std::atomic<PendingSwap*> old_pending_swap;
bool map_change;
@@ -704,6 +730,7 @@ class LIBARDOUR_API MIDITrigger : public Trigger {
void compute_and_set_length ();
void _startup (BufferSet&, pframes_t dest_offset, Temporal::BBT_Offset const &);
void setup_event_indices ();
void adjust_bounds (Temporal::Beats const & start, Temporal::Beats const & end, Temporal::Beats const & length, bool from_region);
};
class LIBARDOUR_API TriggerBoxThread

View File

@@ -720,6 +720,8 @@ Trigger::clear_region ()
void
Trigger::set_region_internal (std::shared_ptr<Region> r)
{
region_connection.disconnect ();
/* No whole file regions in the triggerbox, just like we do not allow
* them in playlists either.
*/
@@ -729,6 +731,18 @@ Trigger::set_region_internal (std::shared_ptr<Region> r)
} else {
_region = r;
}
if (_region) {
_region->PropertyChanged.connect_same_thread (region_connection, std::bind (&Trigger::region_property_change, this, _1));
}
}
void
Trigger::region_property_change (PropertyChange const & what_changed)
{
if (what_changed.contains (Properties::start) || what_changed.contains (Properties::length)) {
bounds_changed (_region->start(), _region->end());
}
}
timepos_t
@@ -2353,8 +2367,8 @@ MIDITrigger::MIDITrigger (uint32_t n, TriggerBox& b)
, first_event_index (0)
, last_event_index (0)
, rt_midibuffer (nullptr)
, pending_rt_midibuffer (nullptr)
, old_rt_midibuffer (nullptr)
, pending_swap (nullptr)
, old_pending_swap (nullptr)
, map_change (false)
{
_channel_map.assign (16, -1);
@@ -2367,43 +2381,60 @@ MIDITrigger::~MIDITrigger ()
void
MIDITrigger::check_edit_swap (timepos_t const & time, bool playing, BufferSet& bufs)
{
RTMidiBufferBeats* pending = pending_rt_midibuffer.load();
/* NOTE: this method runs synchronously with respect to the process
cycle. The trigger will not be in ::run() while we execute this.
On the other hand, another (UI) thread could be queing another
pending swap.
*/
PendingSwap* pending = pending_swap.exchange (nullptr);
RTMidiBufferBeats* old_rtmb = nullptr;
if (!pending) {
return;
}
if (playing) {
MidiBuffer& mbuf (bufs.get_midi (0));
MidiStateTracker post_edit_state;
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1/%2 noticed pending swap\n", _box.order(), index()));
/* Get the new MIDI state at the current time, and resolve any
* differences with the MIDI state held by the triggerbox we're
* in.
*/
if (pending->rt_midibuffer) {
if (playing) {
/* note: transition_beats is the time of the most recent
* transition (e.g. loop point)
*/
MidiBuffer& mbuf (bufs.get_midi (0));
MidiStateTracker post_edit_state;
Temporal::Beats p = time.beats() - transition_beats;
/* Get the new MIDI state at the current time, and resolve any
* differences with the MIDI state held by the triggerbox we're
* in.
*/
/* now determine the state at this time, which we need relative
* to the start of the loop within the data.
*/
/* note: transition_beats is the time of the most recent
* transition (e.g. loop point)
*/
pending->track_state (p - loop_start, post_edit_state);
_box.tracker->resolve_diff (post_edit_state, mbuf, time.samples());
Temporal::Beats p = time.beats() - transition_beats;
/* now determine the state at this time, which we need relative
* to the start of the loop within the data.
*/
pending->rt_midibuffer->track_state (p - _play_start, post_edit_state);
_box.tracker->resolve_diff (post_edit_state, mbuf, time.samples());
}
if (pending->rt_midibuffer) {
old_rtmb = rt_midibuffer.exchange (pending->rt_midibuffer);
}
if (iter < rt_midibuffer.load()->size()) {
/* shutdown */
}
}
old_rt_midibuffer = rt_midibuffer.exchange (pending);
if (iter < rt_midibuffer.load()->size()) {
/* shutdown */
}
adjust_bounds (pending->play_start, pending->play_end, pending->length, false);
setup_event_indices ();
pending_rt_midibuffer = nullptr;
pending->rt_midibuffer = old_rtmb;
old_pending_swap.store (pending);
}
void
@@ -2423,12 +2454,12 @@ MIDITrigger::setup_event_indices ()
last_event_index = std::numeric_limits<uint32_t>::max();
for (uint32_t n = 0; n < rt->size(); ++n) {
if ((first_event_index == 0) && ((*rt)[n].timestamp >= loop_start)) {
if ((first_event_index == 0) && ((*rt)[n].timestamp >= _play_start)) {
/* first one at or after the loop start */
first_event_index = n;
}
if ((last_event_index == std::numeric_limits<uint32_t>::max()) && ((*rt)[n].timestamp > loop_end)) {
if ((last_event_index == std::numeric_limits<uint32_t>::max()) && ((*rt)[n].timestamp > _play_end)) {
/* first one at or after the loop end */
last_event_index = n; /* exclusive end */
}
@@ -2441,6 +2472,32 @@ MIDITrigger::setup_event_indices ()
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1/%2 first index %3 last index %4 of %5\n", _box.order(), index(), first_event_index, last_event_index, rt->size()));
}
void
MIDITrigger::adjust_bounds (Temporal::Beats const & start, Temporal::Beats const & end, Temporal::Beats const & length, bool from_region)
{
if (!from_region && _region) {
_region->set_length (timecnt_t (length, timepos_t (start)));
}
_play_start = start;
_play_end = end;
/* Note that in theory we may be able to get loop start/end from the
* SMF and it could different from the data start/end
*/
_loop_start = start;
_loop_end = end;
data_length = length;
_follow_length = Temporal::BBT_Offset (0, length.get_beats(), 0);
set_length (timecnt_t (length));
DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1/%2 new bounds %3..%4 %5..%6 len %7 of %7\n", _box.order(), index(), _play_start, _play_end, _loop_start, _loop_end, length, rt_midibuffer.load()->size()));
setup_event_indices ();
}
void
MIDITrigger::arm ()
{
@@ -2467,21 +2524,17 @@ MIDITrigger::captured (SlotArmInfo& ai, BufferSet& bufs)
* been moved to rtmb.
*/
old_rt_midibuffer = rt_midibuffer.exchange (ai.midi_buf);
#warning what to do about the old RT midi buffer
// old_rt_midibuffer = rt_midibuffer.exchange (ai.midi_buf);
Temporal::TempoMap::SharedPtr tmap (Temporal::TempoMap::use());
timecnt_t dur = tmap->convert_duration (timecnt_t (ai.captured), timepos_t (ai.start_samples), Temporal::BeatTime);
loop_start = Temporal::Beats();
loop_end = dur.beats();
data_length = loop_end;
_follow_length = Temporal::BBT_Offset (0, data_length.get_beats(), 0);
set_length (timecnt_t (data_length));
adjust_bounds (Temporal::Beats(), dur.beats(), dur.beats(), false);
iter = 0;
_follow_action0 = FollowAction::Again;
setup_event_indices ();
/* Mark ai.midi_buf as null so that it is not deleted */
ai.midi_buf = nullptr;
delete &ai;
@@ -2491,8 +2544,8 @@ MIDITrigger::captured (SlotArmInfo& ai, BufferSet& bufs)
/* Meanwhile, build a new source and region from the data now in rt_midibuffer */
// std::cerr << "capture done, ask for a source of length " << loop_end.str() << std::endl;
TriggerBox::worker->request_build_source (this, timecnt_t (loop_end));
// std::cerr << "capture done, ask for a source of length " << dur.beats().str() << std::endl;
TriggerBox::worker->request_build_source (this, timecnt_t (dur.beats()));
_armed = false;
ArmChanged(); /* EMIT SIGNAL */
@@ -2980,9 +3033,15 @@ MIDITrigger::set_region_in_worker_thread_from_capture (std::shared_ptr<Region> r
set_region_internal (r);
set_name (mr->name());
/* We do not need to call adjust_bounds() here because the values were
* set (and have not changed) when ::captured() was called.
*/
_model = mr->model();
_model->ContentsChanged.connect_same_thread (content_connection, std::bind (&MIDITrigger::model_contents_changed, this));
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 ();
@@ -3015,27 +3074,15 @@ MIDITrigger::set_region_in_worker_thread (std::shared_ptr<Region> r)
_model = mr->model();
_model->ContentsChanged.connect_same_thread (content_connection, std::bind (&MIDITrigger::model_contents_changed, this));
_play_start = _region->start().beats ();
_play_end = _region->end().beats ();
_loop_start = _play_start;
_loop_end = _play_end;
data_length = _play_end - _play_start;
/* Note that in theory we may be able to get loop start/end from the
* SMF file and it could different from the data start/end
*/
model_contents_changed ();
loop_start = r->start ().beats();
loop_end = r->end ().beats();
data_length = loop_end - loop_start;
_follow_length = Temporal::BBT_Offset (0, data_length.get_beats(), 0);
set_length (timecnt_t (data_length));
RTMidiBufferBeats* rtmb = new RTMidiBufferBeats;
{
MidiModel::ReadLock lm (_model->read_lock());
_model->render (lm, *rtmb);
}
pending_rt_midibuffer = rtmb;
/* bounds will be reset when pending request is swapped in from RT thread */
estimate_midi_patches ();
@@ -3117,21 +3164,52 @@ MIDITrigger::tempo_map_changed ()
void
MIDITrigger::model_contents_changed ()
{
RTMidiBufferBeats * rtmb (new RTMidiBufferBeats);
PendingSwap* pending = new PendingSwap;
pending->rt_midibuffer = new RTMidiBufferBeats;
_model->render (_model->read_lock(), *rtmb);
pending->play_start = _play_start;
pending->play_end = _play_end;
pending->loop_start = _loop_start;
pending->loop_end = _loop_end;
pending->length = data_length;
/* Clean up a previous RT midi buffer swap */
RTMidiBufferBeats* old = old_rt_midibuffer.load();
if (old) {
delete old;
old_rt_midibuffer = nullptr;
}
_model->render (_model->read_lock(), *pending->rt_midibuffer);
/* And set it. RT thread will find this and do what needs to be done */
pending_rt_midibuffer.store (rtmb);
pending_swap.store (pending);
/* Clean up a previous RT midi buffer swap */
PendingSwap* old = old_pending_swap.exchange (nullptr);
if (old) {
delete old;
}
}
void
MIDITrigger::bounds_changed (Temporal::timepos_t const & start, Temporal::timepos_t const & end)
{
PendingSwap* pending = new PendingSwap;
pending->play_start = start.beats();
pending->play_end = end.beats();
pending->loop_start = pending->play_start;
pending->loop_end = pending->play_end;
pending->length = pending->play_end - pending->play_start;
/* And set it. RT thread will find this and do what needs to be done */
pending_swap.store (pending);
/* Clean up a previous RT midi buffer swap (if there is one) */
PendingSwap* old = old_pending_swap.exchange (nullptr);
if (old) {
delete old;
}
}
template<bool in_process_context>