MidiView: fix display of out-of-range live (recording) notes

This involves better handling of note range changes, and also a renaming of
member variables to better reflect their function.

It adds _finished_live_notes to hold notes that were recorded live but have
ended, since these also need to be updated as zooming or note range changes
This commit is contained in:
Paul Davis 2025-11-17 11:07:13 -07:00
parent 68c69fb89d
commit a0302bd7fc
3 changed files with 129 additions and 74 deletions

View file

@ -434,7 +434,7 @@ MidiStreamView::update_rec_box ()
region->set_length (timecnt_t (_trackview.track()->current_capture_end () - _trackview.track()->current_capture_start())); region->set_length (timecnt_t (_trackview.track()->current_capture_end () - _trackview.track()->current_capture_start()));
MidiRegionView* mrv = dynamic_cast<MidiRegionView*> (rec_regions.back().second); MidiRegionView* mrv = dynamic_cast<MidiRegionView*> (rec_regions.back().second);
mrv->extend_active_notes (); mrv->extend_unfinished_live_notes ();
} }

View file

@ -114,8 +114,8 @@ MidiView::MidiView (std::shared_ptr<MidiTrack> mt,
uint32_t basic_color) uint32_t basic_color)
: _editing_context (ec) : _editing_context (ec)
, _midi_context (bg) , _midi_context (bg)
, _active_notes (nullptr) , _unfinished_live_notes (nullptr)
, active_note_end (std::numeric_limits<Temporal::Beats>::max()) , live_note_end (std::numeric_limits<Temporal::Beats>::max())
, _note_group (new ArdourCanvas::Container (&parent)) , _note_group (new ArdourCanvas::Container (&parent))
, _note_diff_command (nullptr) , _note_diff_command (nullptr)
, _ghost_note (nullptr) , _ghost_note (nullptr)
@ -153,8 +153,8 @@ MidiView::MidiView (MidiView const & other)
, _editing_context (other.editing_context()) , _editing_context (other.editing_context())
, _midi_context (other.midi_context()) , _midi_context (other.midi_context())
, _midi_region (other.midi_region()) , _midi_region (other.midi_region())
, _active_notes (nullptr) , _unfinished_live_notes (nullptr)
, active_note_end (std::numeric_limits<Temporal::Beats>::max()) , live_note_end (std::numeric_limits<Temporal::Beats>::max())
, _note_group (new ArdourCanvas::Container (other._note_group->parent())) , _note_group (new ArdourCanvas::Container (other._note_group->parent()))
, _note_diff_command (0) , _note_diff_command (0)
, _ghost_note (nullptr) , _ghost_note (nullptr)
@ -1162,7 +1162,7 @@ MidiView::model_changed()
EC_LOCAL_TEMPO_SCOPE_ARG (_editing_context); EC_LOCAL_TEMPO_SCOPE_ARG (_editing_context);
if (_active_notes) { if (_unfinished_live_notes) {
// Currently recording // Currently recording
const samplecnt_t zoom = _editing_context.get_current_zoom(); const samplecnt_t zoom = _editing_context.get_current_zoom();
if (zoom != _last_display_zoom) { if (zoom != _last_display_zoom) {
@ -1325,7 +1325,7 @@ MidiView::view_changed()
return; return;
} }
if (_active_notes) { if (_unfinished_live_notes) {
/* Recording */ /* Recording */
@ -1599,7 +1599,7 @@ MidiView::~MidiView ()
delete _list_editor; delete _list_editor;
if (_active_notes) { if (_unfinished_live_notes) {
end_write(); end_write();
} }
_entered_note = 0; _entered_note = 0;
@ -1680,25 +1680,33 @@ MidiView::begin_write()
XXX this should not happen. XXX this should not happen.
*/ */
if (_active_notes) { if (_unfinished_live_notes) {
for (auto n = 0; n < 128; ++n) { for (auto n = 0; n < 128; ++n) {
if (_active_notes[n]) { if (_unfinished_live_notes[n]) {
auto iter = _events.find (_active_notes[n]->note()); auto iter = _events.find (_unfinished_live_notes[n]->note());
if (iter != _events.end()) { if (iter != _events.end()) {
_events.erase (iter); _events.erase (iter);
} }
} }
} }
delete [] _active_notes; delete [] _unfinished_live_notes;
}
for (auto & n : _finished_live_notes) {
auto iter = _events.find (n->note());
if (iter != _events.end()) {
_events.erase (iter);
}
} }
/* reallocate */ /* reallocate */
_active_notes = new Note*[128]; _unfinished_live_notes = new Note*[128];
for (unsigned i = 0; i < 128; ++i) { for (size_t i = 0; i < 128; ++i) {
_active_notes[i] = nullptr; _unfinished_live_notes[i] = nullptr;
} }
active_note_end = timecnt_t (Temporal::BeatTime); _finished_live_notes.clear ();
live_note_end = timecnt_t (Temporal::BeatTime);
} }
@ -1711,42 +1719,43 @@ MidiView::end_write()
owned by _events. Just delete the container used for active owned by _events. Just delete the container used for active
notes only. notes only.
*/ */
delete [] _active_notes; delete [] _unfinished_live_notes;
_active_notes = nullptr; _unfinished_live_notes = nullptr;
_finished_live_notes.clear ();
_marked_for_selection.clear(); _marked_for_selection.clear();
_marked_for_velocity.clear(); _marked_for_velocity.clear();
active_note_end = std::numeric_limits<Temporal::Beats>::max(); live_note_end = std::numeric_limits<Temporal::Beats>::max();
} }
/** Extend active notes to rightmost edge of region (if length is changed) /** Extend active notes to rightmost edge of region (if length is changed)
*/ */
void void
MidiView::extend_active_notes() MidiView::extend_unfinished_live_notes()
{ {
if (!_midi_region) { if (!_midi_region) {
return; return;
} }
extend_active_notes (_midi_region->length()); extend_unfinished_live_notes (_midi_region->length());
} }
void void
MidiView::extend_active_notes (timecnt_t const & duration) MidiView::extend_unfinished_live_notes (timecnt_t const & duration)
{ {
if (!_midi_region) { if (!_midi_region) {
return; return;
} }
if (!_active_notes) { if (!_unfinished_live_notes) {
return; return;
} }
double x1 = _editing_context.duration_to_pixels (duration); double x1 = _editing_context.duration_to_pixels (duration);
for (int i = 0; i < 128; ++i) { for (int i = 0; i < 128; ++i) {
if (_active_notes[i]) { if (_unfinished_live_notes[i]) {
_active_notes[i]->set_x1 (x1); _unfinished_live_notes[i]->set_x1 (x1);
} }
} }
} }
@ -1854,8 +1863,8 @@ MidiView::update_sustained (Note* ev)
ev->set_velocity (note->velocity()/127.0); ev->set_velocity (note->velocity()/127.0);
if (note->end_time() == std::numeric_limits<Temporal::Beats>::max()) { if (note->end_time() == std::numeric_limits<Temporal::Beats>::max()) {
if (_active_notes && note->note() < 128) { if (_unfinished_live_notes && note->note() < 128) {
Note* const old_nb = _active_notes[note->note()]; Note* const old_nb = _unfinished_live_notes[note->note()];
if (old_nb && (old_nb != ev)) { if (old_nb && (old_nb != ev)) {
/* There is an active note on this key, so we have a stuck /* There is an active note on this key, so we have a stuck
note. Finish the old rectangle here. note. Finish the old rectangle here.
@ -1866,7 +1875,7 @@ MidiView::update_sustained (Note* ev)
/* XXX we now leak old_nb if it was set since there are /* XXX we now leak old_nb if it was set since there are
* no other references to it, plus it will remain on-screen * no other references to it, plus it will remain on-screen
*/ */
_active_notes[note->note()] = ev; _unfinished_live_notes[note->note()] = ev;
} }
/* outline all but right edge */ /* outline all but right edge */
ev->set_outline_what (ArdourCanvas::Rectangle::What ( ev->set_outline_what (ArdourCanvas::Rectangle::What (
@ -1878,6 +1887,14 @@ MidiView::update_sustained (Note* ev)
ev->set_outline_all (); ev->set_outline_all ();
} }
bool visible ((y0 >= 0) && (y1 <= _midi_context.contents_height()));
if (!visible) {
ev->hide ();
} else {
ev->show ();
}
color_note (ev, note->channel()); color_note (ev, note->channel());
ev->set_ignore_events (!note_editable (ev)); ev->set_ignore_events (!note_editable (ev));
} }
@ -1916,7 +1933,7 @@ MidiView::clip_capture_update_sustained (Note *ev, double& x0, double& x1, doubl
/* normal note */ /* normal note */
timepos_t ane = active_note_end.end(); timepos_t ane = live_note_end.end();
if (ane.beats() != std::numeric_limits<Temporal::Beats>::max()) { if (ane.beats() != std::numeric_limits<Temporal::Beats>::max()) {
if (note_end > ane) { if (note_end > ane) {
@ -1930,7 +1947,7 @@ MidiView::clip_capture_update_sustained (Note *ev, double& x0, double& x1, doubl
/* nascent note currently being recorded, noteOff has not yet arrived */ /* nascent note currently being recorded, noteOff has not yet arrived */
x1 = x0 + std::max (1., _editing_context.duration_to_pixels (note_start.distance (active_note_end.end()))); x1 = x0 + std::max (1., _editing_context.duration_to_pixels (note_start.distance (live_note_end.end())));
} }
y1 = y0 + std::max (1., note_height() - 1.); y1 = y0 + std::max (1., note_height() - 1.);
@ -4707,15 +4724,15 @@ MidiView::clip_data_recorded (samplecnt_t total_duration)
return; return;
} }
if (!_active_notes) { if (!_unfinished_live_notes) {
/* left over idle callback, but we're not recording */ /* left over idle callback, but we're not recording */
return; return;
} }
if (_active_notes) { if (_unfinished_live_notes) {
for (int n = 0; n < 128; ++n) { for (int n = 0; n < 128; ++n) {
if (_active_notes[n]) { if (_unfinished_live_notes[n]) {
update_sustained (_active_notes[n]); update_sustained (_unfinished_live_notes[n]);
} }
} }
} }
@ -4724,6 +4741,7 @@ MidiView::clip_data_recorded (samplecnt_t total_duration)
assert (tb); assert (tb);
std::shared_ptr<MidiBuffer> buf = tb->get_gui_feed_buffer(); std::shared_ptr<MidiBuffer> buf = tb->get_gui_feed_buffer();
bool note_range_changed = false;
for (MidiBuffer::iterator i = buf->begin(); i != buf->end(); ++i) { for (MidiBuffer::iterator i = buf->begin(); i != buf->end(); ++i) {
const Evoral::Event<MidiBuffer::TimeType>& ev = *i; const Evoral::Event<MidiBuffer::TimeType>& ev = *i;
@ -4747,36 +4765,53 @@ MidiView::clip_data_recorded (samplecnt_t total_duration)
std::shared_ptr<NoteType> note (new NoteType (ev.channel(), time_beats, std::numeric_limits<Temporal::Beats>::max() - time_beats, ev.note(), ev.velocity())); std::shared_ptr<NoteType> note (new NoteType (ev.channel(), time_beats, std::numeric_limits<Temporal::Beats>::max() - time_beats, ev.note(), ev.velocity()));
NoteBase* nb = add_note (note, true); if (ev.note() < _midi_context.lowest_note()) {
_midi_context.update_data_note_range (ev.note(), _midi_context.highest_note());
note_range_changed |= set_note_range (ev.note(), _midi_context.highest_note());
} else if (ev.note() > _midi_context.highest_note()) {
_midi_context.update_data_note_range (_midi_context.lowest_note(), ev.note());
note_range_changed |= set_note_range (_midi_context.lowest_note(), ev.note());
}
const int y = _midi_context.note_to_y (note->note());
bool visible = (y >= 0) && (y <= _midi_context.contents_height());
NoteBase* nb = add_note (note, visible);
nb->item()->set_fill_color (UIConfiguration::instance().color ("recording note")); nb->item()->set_fill_color (UIConfiguration::instance().color ("recording note"));
nb->item()->set_outline_color (UIConfiguration::instance().color ("recording note")); nb->item()->set_outline_color (UIConfiguration::instance().color ("recording note"));
/* fix up our note range */
if (ev.note() < _midi_context.lowest_note()) {
set_note_range (ev.note(), _midi_context.highest_note());
} else if (ev.note() > _midi_context.highest_note()) {
set_note_range (_midi_context.lowest_note(), ev.note());
}
} else if (ev.type() == MIDI_CMD_NOTE_OFF) { } else if (ev.type() == MIDI_CMD_NOTE_OFF) {
// XXX WAS resolve_note (ev.note (), time_beats); // XXX WAS resolve_note (ev.note (), time_beats);
uint8_t note = ev.note (); uint8_t note = ev.note ();
Temporal::Beats end_time = time_beats; Temporal::Beats end_time = time_beats;
if (_active_notes && _active_notes[note]) { if (_unfinished_live_notes && _unfinished_live_notes[note]) {
/* Set note length so update_note() works. Note this is a local note /* Set note length so update_note() works. Note this is a local note
for recording, not from a model, so we can safely mess with it. */ for recording, not from a model, so we can safely mess with it. */
_active_notes[note]->note()->set_length (end_time - _active_notes[note]->note()->time()); _unfinished_live_notes[note]->note()->set_length (end_time - _unfinished_live_notes[note]->note()->time());
_active_notes[note]->set_x1 (_editing_context.sample_to_pixel (timepos_t (ev.time ()).samples())); _unfinished_live_notes[note]->set_x1 (_editing_context.sample_to_pixel (timepos_t (ev.time ()).samples()));
_active_notes[note]->set_outline_all (); _unfinished_live_notes[note]->set_outline_all ();
_active_notes[note] = nullptr; _unfinished_live_notes[note] = nullptr;
} }
} }
} }
active_note_end = timecnt_t (total_duration); if (note_range_changed) {
assert (_unfinished_live_notes);
for (size_t n = 0; n < 128; ++n) {
if (_unfinished_live_notes[n]) {
update_note (_unfinished_live_notes[n]);
}
}
for (auto & n : _finished_live_notes) {
update_note (n);
}
}
live_note_end = timecnt_t (total_duration);
} }
/** Called when a diskstream on our track has received some data. Update the view, if applicable. /** Called when a diskstream on our track has received some data. Update the view, if applicable.
@ -4793,7 +4828,7 @@ MidiView::data_recorded (std::weak_ptr<MidiSource> w)
return; return;
} }
if (!_active_notes) { if (!_unfinished_live_notes) {
/* we aren't actively being recorded to */ /* we aren't actively being recorded to */
return; return;
} }
@ -4807,6 +4842,7 @@ MidiView::data_recorded (std::weak_ptr<MidiSource> w)
std::shared_ptr<MidiBuffer> buf = _midi_track->get_gui_feed_buffer (); std::shared_ptr<MidiBuffer> buf = _midi_track->get_gui_feed_buffer ();
samplepos_t back = max_samplepos; samplepos_t back = max_samplepos;
bool note_range_changed = false;
for (MidiBuffer::iterator i = buf->begin(); i != buf->end(); ++i) { for (MidiBuffer::iterator i = buf->begin(); i != buf->end(); ++i) {
const Evoral::Event<MidiBuffer::TimeType>& ev = *i; const Evoral::Event<MidiBuffer::TimeType>& ev = *i;
@ -4833,27 +4869,32 @@ MidiView::data_recorded (std::weak_ptr<MidiSource> w)
assert (note->end_time() == std::numeric_limits<Temporal::Beats>::max()); assert (note->end_time() == std::numeric_limits<Temporal::Beats>::max());
NoteBase* nb = add_note (note, true); if (ev.note() < _midi_context.lowest_note()) {
_midi_context.update_data_note_range (ev.note(), _midi_context.highest_note());
note_range_changed |= set_note_range (ev.note(), _midi_context.highest_note());
} else if (ev.note() > _midi_context.highest_note()) {
_midi_context.update_data_note_range (_midi_context.lowest_note(), ev.note());
note_range_changed |= set_note_range (_midi_context.lowest_note(), ev.note());
}
const int y = _midi_context.note_to_y (note->note());
bool visible = (y >= 0) && (y <= _midi_context.contents_height());
NoteBase* nb = add_note (note, visible);
nb->item()->set_fill_color (UIConfiguration::instance().color ("recording note")); nb->item()->set_fill_color (UIConfiguration::instance().color ("recording note"));
nb->item()->set_outline_color (UIConfiguration::instance().color ("recording note")); nb->item()->set_outline_color (UIConfiguration::instance().color ("recording note"));
/* fix up our note range */
if (ev.note() < _midi_context.lowest_note()) {
set_note_range (ev.note(), _midi_context.highest_note());
} else if (ev.note() > _midi_context.highest_note()) {
set_note_range (_midi_context.lowest_note(), ev.note());
}
} else if (ev.type() == MIDI_CMD_NOTE_OFF) { } else if (ev.type() == MIDI_CMD_NOTE_OFF) {
// XXX WAS resolve_note (ev.note (), time_beats); // XXX WAS resolve_note (ev.note (), time_beats);
uint8_t note = ev.note (); uint8_t note = ev.note ();
Temporal::Beats end_time = time_beats; Temporal::Beats end_time = time_beats;
if (_active_notes && _active_notes[note]) { if (_unfinished_live_notes && _unfinished_live_notes[note]) {
/* Set note length so update_note() works. Note this is a local note /* Set note length so update_note() works. Note this is a local note
for recording, not from a model, so we can safely mess with it. */ for recording, not from a model, so we can safely mess with it. */
_active_notes[note]->note()->set_length (end_time - _active_notes[note]->note()->time()); _unfinished_live_notes[note]->note()->set_length (end_time - _unfinished_live_notes[note]->note()->time());
/* End time is relative to the source being recorded. */ /* End time is relative to the source being recorded. */
@ -4863,15 +4904,28 @@ MidiView::data_recorded (std::weak_ptr<MidiSource> w)
// - and then take the samples() value of that and convert it to pixels // - and then take the samples() value of that and convert it to pixels
// //
// Much simpler to just use ev.time() which is already the absolute position (in sample-time) // Much simpler to just use ev.time() which is already the absolute position (in sample-time)
_active_notes[note]->set_x1 (_editing_context.sample_to_pixel ((src->time_since_capture_start (timepos_t (ev.time ()))).samples())); _unfinished_live_notes[note]->set_x1 (_editing_context.sample_to_pixel ((src->time_since_capture_start (timepos_t (ev.time ()))).samples()));
_active_notes[note]->set_outline_all (); _unfinished_live_notes[note]->set_outline_all ();
_active_notes[note] = nullptr; _finished_live_notes.push_back (_unfinished_live_notes[note]);
_unfinished_live_notes[note] = nullptr;
} }
} }
back = ev.time (); back = ev.time ();
} }
if (note_range_changed) {
assert (_unfinished_live_notes);
for (size_t n = 0; n < 128; ++n) {
if (_unfinished_live_notes[n]) {
update_note (_unfinished_live_notes[n]);
}
}
for (auto & n : _finished_live_notes) {
update_note (n);
}
}
_midi_context.record_layer_check (_midi_region, back); _midi_context.record_layer_check (_midi_region, back);
} }
@ -5297,7 +5351,7 @@ struct NoteExtentInfo
void void
MidiView::join_notes_on_channel (int chn) MidiView::join_notes_on_channel (int chn)
{ {
NoteExtentInfo ninfo[127]; NoteExtentInfo ninfo[128];
for (auto & s : _selection) { for (auto & s : _selection) {
@ -5329,7 +5383,7 @@ MidiView::join_notes_on_channel (int chn)
} }
} }
for (size_t n = 0; n < 127; ++n) { for (size_t n = 0; n < 128; ++n) {
NoteExtentInfo& ni (ninfo[n]); NoteExtentInfo& ni (ninfo[n]);
@ -5445,16 +5499,16 @@ MidiView::height() const
return _midi_context.height(); return _midi_context.height();
} }
void bool
MidiView::set_note_range (uint8_t low, uint8_t high) MidiView::set_note_range (uint8_t low, uint8_t high)
{ {
_midi_context.apply_note_range (low, high, true); return _midi_context.apply_note_range (low, high, true);
} }
void bool
MidiView::maybe_set_note_range (uint8_t low, uint8_t high) MidiView::maybe_set_note_range (uint8_t low, uint8_t high)
{ {
_midi_context.maybe_apply_note_range (low, high, true); return _midi_context.maybe_apply_note_range (low, high, true);
} }
void void

View file

@ -183,8 +183,8 @@ class MidiView : public virtual sigc::trackable, public LineMerger
void begin_write (); void begin_write ();
void end_write (); void end_write ();
void extend_active_notes (); void extend_unfinished_live_notes ();
void extend_active_notes (Temporal::timecnt_t const &); void extend_unfinished_live_notes (Temporal::timecnt_t const &);
virtual void begin_drag_edit (std::string const & why); virtual void begin_drag_edit (std::string const & why);
void end_drag_edit (); void end_drag_edit ();
@ -308,8 +308,8 @@ class MidiView : public virtual sigc::trackable, public LineMerger
void show_list_editor (); void show_list_editor ();
void set_note_range (uint8_t low, uint8_t high); bool set_note_range (uint8_t low, uint8_t high);
void maybe_set_note_range (uint8_t low, uint8_t high); bool maybe_set_note_range (uint8_t low, uint8_t high);
virtual void set_visibility_note_range (MidiViewBackground::VisibleNoteRange, bool); virtual void set_visibility_note_range (MidiViewBackground::VisibleNoteRange, bool);
typedef std::set<NoteBase*> Selection; typedef std::set<NoteBase*> Selection;
@ -513,8 +513,9 @@ class MidiView : public virtual sigc::trackable, public LineMerger
CopyDragEvents _copy_drag_events; CopyDragEvents _copy_drag_events;
PatchChanges _patch_changes; PatchChanges _patch_changes;
SysExes _sys_exes; SysExes _sys_exes;
Note** _active_notes; Note** _unfinished_live_notes;
Temporal::timecnt_t active_note_end; std::vector<Note*> _finished_live_notes;
Temporal::timecnt_t live_note_end;
ArdourCanvas::Container* _note_group; ArdourCanvas::Container* _note_group;
ARDOUR::MidiModel::NoteDiffCommand* _note_diff_command; ARDOUR::MidiModel::NoteDiffCommand* _note_diff_command;
NoteBase* _ghost_note; NoteBase* _ghost_note;