ardour/gtk2_ardour/ghostregion.cc

495 lines
13 KiB
C++
Raw Normal View History

/*
* Copyright (C) 2005-2018 Paul Davis <paul@linuxaudiosystems.com>
* Copyright (C) 2005 Karsten Wiese <fzuuzf@googlemail.com>
* Copyright (C) 2005 Taybin Rutkin <taybin@taybin.com>
* Copyright (C) 2007 Doug McLain <doug@nostar.net>
* Copyright (C) 2008-2014 David Robillard <d@drobilla.net>
* Copyright (C) 2009-2011 Carl Hetherington <carl@carlh.net>
* Copyright (C) 2014-2019 Robin Gareus <robin@gareus.org>
* Copyright (C) 2015 Tim Mayberry <mojofunk@gmail.com>
* Copyright (C) 2016-2017 Nick Mainsbridge <mainsbridge@gmail.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.
*/
#include "evoral/Note.h"
#include "ardour/parameter_descriptor.h"
#include "canvas/container.h"
#include "canvas/polygon.h"
#include "canvas/rectangle.h"
#include "canvas/debug.h"
#include "waveview/wave_view.h"
#include "automation_time_axis.h"
#include "ghostregion.h"
#include "midi_streamview.h"
#include "midi_time_axis.h"
Remove GhostRegion::CatchDeletion signal to reduce session close times Currently when a GhostRegion is deleted by its "parent" RegionView it emits the static GhostRegion::CatchDeletion signal which is connected to the RegionView::remove_ghost method of every RegionView instance. With a static GhostRegion::CatchDeletion signal a particular test session causes 31 Million calls of RegionView::remove_ghost on Session deletion and the session takes 70 seconds to close with a debug build. The lifetime of a ghost region is tied to both the TimeAxisView(TAV) and RegionView(RV) in that when a RegionView is deleted all GhostRegion instances associated with the RegionView should be deleted or when a TimeAxisView is deleted all ghost regions that are contained in the view should be deleted. This means that there needs to be notification between GhostRegion and both classes. Instead of using a signal for this as we know there are only two listeners and GhostRegion already holds a reference to the TimeAxisView, also take a reference to the parent RegionView in the GhostRegion constructor and use it to notify the RegionView when GhostRegion destructor is called so it can drop any references it holds. Using a direct function call in the GhostRegion destructor to notify the TimeAxisView and RegionView "parents" brings the unload/close time down for the test session from 70 seconds to 4.5 seconds. The GhostRegion also references canvas items that are added to the TimeAxisView canvas group or at least a canvas group that it manages. So when the TimeAxisView is destroyed and the canvas group that is the parent of those items is destroyed, the GhostRegion's canvas items will also be deleted/destroyed by the parent canvas item/group. This means the GhostRegions must be destroyed when the TimeAxisView they are contained in is destroyed or there will be dangling references to canvas items that have already been deleted and trying to delete them again will be bad.
2015-10-22 07:56:02 -04:00
#include "region_view.h"
#include "midi_region_view.h"
#include "rgb_macros.h"
#include "note.h"
#include "hit.h"
#include "ui_config.h"
using namespace std;
using namespace Editing;
using namespace ARDOUR;
using ArdourCanvas::Duple;
GhostRegion::GhostRegion (RegionView& rv,
ArdourCanvas::Container* parent,
TimeAxisView& tv,
TimeAxisView& source_tv,
double initial_pos)
: parent_rv (rv)
, trackview (tv)
, source_trackview (source_tv)
, base_rect (0)
{
group = new ArdourCanvas::Container (parent);
CANVAS_DEBUG_NAME (group, "ghost region group");
group->set_position (ArdourCanvas::Duple (initial_pos, 0));
if (is_automation_ghost()) {
base_rect = new ArdourCanvas::Rectangle (group);
CANVAS_DEBUG_NAME (base_rect, "ghost region rect");
base_rect->set_x0 (0);
base_rect->set_y0 (1.0);
base_rect->set_y1 (trackview.current_height());
base_rect->set_outline (false);
}
GhostRegion::set_colors();
/* the parent group of a ghostregion is a dedicated group for ghosts,
so the new ghost would want to get to the top of that group*/
group->raise_to_top ();
}
GhostRegion::~GhostRegion ()
{
Remove GhostRegion::CatchDeletion signal to reduce session close times Currently when a GhostRegion is deleted by its "parent" RegionView it emits the static GhostRegion::CatchDeletion signal which is connected to the RegionView::remove_ghost method of every RegionView instance. With a static GhostRegion::CatchDeletion signal a particular test session causes 31 Million calls of RegionView::remove_ghost on Session deletion and the session takes 70 seconds to close with a debug build. The lifetime of a ghost region is tied to both the TimeAxisView(TAV) and RegionView(RV) in that when a RegionView is deleted all GhostRegion instances associated with the RegionView should be deleted or when a TimeAxisView is deleted all ghost regions that are contained in the view should be deleted. This means that there needs to be notification between GhostRegion and both classes. Instead of using a signal for this as we know there are only two listeners and GhostRegion already holds a reference to the TimeAxisView, also take a reference to the parent RegionView in the GhostRegion constructor and use it to notify the RegionView when GhostRegion destructor is called so it can drop any references it holds. Using a direct function call in the GhostRegion destructor to notify the TimeAxisView and RegionView "parents" brings the unload/close time down for the test session from 70 seconds to 4.5 seconds. The GhostRegion also references canvas items that are added to the TimeAxisView canvas group or at least a canvas group that it manages. So when the TimeAxisView is destroyed and the canvas group that is the parent of those items is destroyed, the GhostRegion's canvas items will also be deleted/destroyed by the parent canvas item/group. This means the GhostRegions must be destroyed when the TimeAxisView they are contained in is destroyed or there will be dangling references to canvas items that have already been deleted and trying to delete them again will be bad.
2015-10-22 07:56:02 -04:00
parent_rv.remove_ghost(this);
trackview.erase_ghost(this);
delete base_rect;
delete group;
}
void
GhostRegion::set_duration (double units)
{
if (base_rect) {
base_rect->set_x1 (units);
}
}
void
GhostRegion::set_height ()
{
if (base_rect) {
base_rect->set_y1 (trackview.current_height());
}
}
void
GhostRegion::set_colors ()
{
if (is_automation_ghost()) {
base_rect->set_fill_color (UIConfiguration::instance().color_mod ("ghost track base", "ghost track base"));
}
}
guint
GhostRegion::source_track_color(unsigned char alpha)
{
Gdk::Color color = source_trackview.color();
return RGBA_TO_UINT (color.get_red() / 256, color.get_green() / 256, color.get_blue() / 256, alpha);
}
bool
GhostRegion::is_automation_ghost()
{
return (dynamic_cast<AutomationTimeAxisView*>(&trackview)) != 0;
}
AudioGhostRegion::AudioGhostRegion (RegionView& rv,
TimeAxisView& tv,
TimeAxisView& source_tv,
double initial_unit_pos)
: GhostRegion(rv, tv.ghost_group(), tv, source_tv, initial_unit_pos)
{
}
2013-04-04 18:45:27 -04:00
void
AudioGhostRegion::set_samples_per_pixel (double fpp)
{
for (vector<ArdourWaveView::WaveView*>::iterator i = waves.begin(); i != waves.end(); ++i) {
(*i)->set_samples_per_pixel (fpp);
}
}
void
AudioGhostRegion::set_height ()
{
vector<ArdourWaveView::WaveView*>::iterator i;
uint32_t n;
GhostRegion::set_height();
double const ht = ((trackview.current_height()) / (double) waves.size());
for (n = 0, i = waves.begin(); i != waves.end(); ++i, ++n) {
(*i)->set_height (ht);
(*i)->set_y_position (n * ht);
}
}
void
AudioGhostRegion::set_colors ()
{
GhostRegion::set_colors();
guint fill_color;
if (is_automation_ghost()) {
fill_color = UIConfiguration::instance().color_mod("ghost track wave fill", "region alpha");
2023-01-13 15:20:21 -05:00
} else {
fill_color = source_track_color(200);
}
for (uint32_t n=0; n < waves.size(); ++n) {
waves[n]->set_outline_color (UIConfiguration::instance().color_mod ("ghost track wave", "region alpha"));
waves[n]->set_fill_color (fill_color);
waves[n]->set_clip_color (UIConfiguration::instance().color ("ghost track wave clip"));
waves[n]->set_zero_color (UIConfiguration::instance().color ("ghost track zero line"));
}
}
/** The general constructor; called when the destination timeaxisview doesn't have
* a midistreamview.
*
Remove GhostRegion::CatchDeletion signal to reduce session close times Currently when a GhostRegion is deleted by its "parent" RegionView it emits the static GhostRegion::CatchDeletion signal which is connected to the RegionView::remove_ghost method of every RegionView instance. With a static GhostRegion::CatchDeletion signal a particular test session causes 31 Million calls of RegionView::remove_ghost on Session deletion and the session takes 70 seconds to close with a debug build. The lifetime of a ghost region is tied to both the TimeAxisView(TAV) and RegionView(RV) in that when a RegionView is deleted all GhostRegion instances associated with the RegionView should be deleted or when a TimeAxisView is deleted all ghost regions that are contained in the view should be deleted. This means that there needs to be notification between GhostRegion and both classes. Instead of using a signal for this as we know there are only two listeners and GhostRegion already holds a reference to the TimeAxisView, also take a reference to the parent RegionView in the GhostRegion constructor and use it to notify the RegionView when GhostRegion destructor is called so it can drop any references it holds. Using a direct function call in the GhostRegion destructor to notify the TimeAxisView and RegionView "parents" brings the unload/close time down for the test session from 70 seconds to 4.5 seconds. The GhostRegion also references canvas items that are added to the TimeAxisView canvas group or at least a canvas group that it manages. So when the TimeAxisView is destroyed and the canvas group that is the parent of those items is destroyed, the GhostRegion's canvas items will also be deleted/destroyed by the parent canvas item/group. This means the GhostRegions must be destroyed when the TimeAxisView they are contained in is destroyed or there will be dangling references to canvas items that have already been deleted and trying to delete them again will be bad.
2015-10-22 07:56:02 -04:00
* @param rv The parent RegionView that is being ghosted.
* @param tv TimeAxisView that this ghost region is on.
* @param source_tv TimeAxisView that we are the ghost for.
*/
MidiGhostRegion::MidiGhostRegion(MidiRegionView& rv,
Remove GhostRegion::CatchDeletion signal to reduce session close times Currently when a GhostRegion is deleted by its "parent" RegionView it emits the static GhostRegion::CatchDeletion signal which is connected to the RegionView::remove_ghost method of every RegionView instance. With a static GhostRegion::CatchDeletion signal a particular test session causes 31 Million calls of RegionView::remove_ghost on Session deletion and the session takes 70 seconds to close with a debug build. The lifetime of a ghost region is tied to both the TimeAxisView(TAV) and RegionView(RV) in that when a RegionView is deleted all GhostRegion instances associated with the RegionView should be deleted or when a TimeAxisView is deleted all ghost regions that are contained in the view should be deleted. This means that there needs to be notification between GhostRegion and both classes. Instead of using a signal for this as we know there are only two listeners and GhostRegion already holds a reference to the TimeAxisView, also take a reference to the parent RegionView in the GhostRegion constructor and use it to notify the RegionView when GhostRegion destructor is called so it can drop any references it holds. Using a direct function call in the GhostRegion destructor to notify the TimeAxisView and RegionView "parents" brings the unload/close time down for the test session from 70 seconds to 4.5 seconds. The GhostRegion also references canvas items that are added to the TimeAxisView canvas group or at least a canvas group that it manages. So when the TimeAxisView is destroyed and the canvas group that is the parent of those items is destroyed, the GhostRegion's canvas items will also be deleted/destroyed by the parent canvas item/group. This means the GhostRegions must be destroyed when the TimeAxisView they are contained in is destroyed or there will be dangling references to canvas items that have already been deleted and trying to delete them again will be bad.
2015-10-22 07:56:02 -04:00
TimeAxisView& tv,
TimeAxisView& source_tv,
double initial_unit_pos)
: GhostRegion(rv, tv.ghost_group(), tv, source_tv, initial_unit_pos)
, _note_group (new ArdourCanvas::Container (group))
, parent_mrv (rv)
, _optimization_iterator(events.end())
{
_outline = UIConfiguration::instance().color ("ghost track midi outline");
if (base_rect) {
base_rect->lower_to_bottom();
}
CANVAS_DEBUG_NAME (_note_group, "midi ghost note group");
}
MidiGhostRegion::~MidiGhostRegion()
{
clear_events ();
2016-12-23 10:14:43 -05:00
delete _note_group;
}
2023-01-10 23:41:47 -05:00
MidiGhostRegion::GhostEvent::GhostEvent (NoteBase* e, ArdourCanvas::Container* g, ArdourCanvas::Item* i)
: event (e)
2023-01-10 23:41:47 -05:00
, item (i)
, is_hit (false)
{
velocity_while_editing = event->note()->velocity();
2023-01-10 23:41:47 -05:00
}
2023-01-10 23:41:47 -05:00
MidiGhostRegion::GhostEvent::GhostEvent (NoteBase* e, ArdourCanvas::Container* g)
: event (e)
{
if (dynamic_cast<Note*>(e)) {
2023-06-20 13:28:59 -04:00
item = new ArdourCanvas::Rectangle (g, ArdourCanvas::Rect(e->x0(), e->y0(), e->x1(), e->y1()));
is_hit = false;
} else {
Hit* hit = dynamic_cast<Hit*>(e);
if (!hit) {
return;
}
ArdourCanvas::Polygon* poly = new ArdourCanvas::Polygon(g);
poly->set(Hit::points(e->y1() - e->y0()));
poly->set_position(hit->position());
item = poly;
is_hit = true;
}
velocity_while_editing = event->note()->velocity();
CANVAS_DEBUG_NAME (item, "ghost note item");
}
2013-04-04 18:45:27 -04:00
MidiGhostRegion::GhostEvent::~GhostEvent ()
{
/* event is not ours to delete */
delete item;
}
void
MidiGhostRegion::set_samples_per_pixel (double /*spu*/)
{
}
/** @return MidiStreamView that we are providing a ghost for */
MidiStreamView*
MidiGhostRegion::midi_view ()
{
StreamView* sv = source_trackview.view ();
assert (sv);
MidiStreamView* msv = dynamic_cast<MidiStreamView*> (sv);
assert (msv);
return msv;
}
void
MidiGhostRegion::set_height ()
{
GhostRegion::set_height();
update_contents_height ();
}
void
MidiGhostRegion::set_colors()
{
GhostRegion::set_colors();
_outline = UIConfiguration::instance().color ("ghost track midi outline");
for (EventList::iterator it = events.begin(); it != events.end(); ++it) {
it->second->item->set_fill_color (UIConfiguration::instance().color_mod((*it).second->event->base_color(), "ghost track midi fill"));
it->second->item->set_outline_color (_outline);
}
}
static double
note_height(TimeAxisView& trackview, MidiStreamView* mv)
{
const double tv_height = trackview.current_height();
const double note_range = mv->contents_note_range();
return std::max(1.0, floor(tv_height / note_range - 1.0));
}
static double
note_y(TimeAxisView& trackview, MidiStreamView* mv, uint8_t note_num)
{
const double tv_height = trackview.current_height();
const double note_range = mv->contents_note_range();
const double s = tv_height / note_range;
return tv_height - (note_num + 1 - mv->lowest_note()) * s;
}
void
MidiGhostRegion::update_contents_height ()
{
MidiStreamView* mv = midi_view();
if (!mv) {
return;
}
double const h = note_height(trackview, mv);
for (EventList::iterator it = events.begin(); it != events.end(); ++it) {
uint8_t const note_num = it->second->event->note()->note();
double const y = note_y(trackview, mv, note_num);
if (!it->second->is_hit) {
_tmp_rect = static_cast<ArdourCanvas::Rectangle*>(it->second->item);
_tmp_rect->set (ArdourCanvas::Rect (_tmp_rect->x0(), y, _tmp_rect->x1(), y + h));
} else {
_tmp_poly = static_cast<ArdourCanvas::Polygon*>(it->second->item);
ArdourCanvas::Duple position = _tmp_poly->position();
position.y = y;
_tmp_poly->set_position(position);
_tmp_poly->set(Hit::points(h));
}
}
}
void
2013-04-04 18:45:27 -04:00
MidiGhostRegion::add_note (NoteBase* n)
{
GhostEvent* event = new GhostEvent (n, _note_group);
events.insert (make_pair (n->note(), event));
_optimization_iterator = events.end();
event->item->set_fill_color (UIConfiguration::instance().color_mod(n->base_color(), "ghost track midi fill"));
event->item->set_outline_color (_outline);
MidiStreamView* mv = midi_view();
if (mv) {
if (!n->item()->visible()) {
event->item->hide();
} else {
2017-01-26 08:58:09 -05:00
uint8_t const note_num = n->note()->note();
double const h = note_height(trackview, mv);
double const y = note_y(trackview, mv, note_num);
if (!event->is_hit) {
_tmp_rect = static_cast<ArdourCanvas::Rectangle*>(event->item);
_tmp_rect->set (ArdourCanvas::Rect (_tmp_rect->x0(), y, _tmp_rect->x1(), y + h));
} else {
_tmp_poly = static_cast<ArdourCanvas::Polygon*>(event->item);
Duple position = _tmp_poly->position();
position.y = y;
_tmp_poly->set_position(position);
_tmp_poly->set(Hit::points(h));
}
}
}
}
void
MidiGhostRegion::clear_events()
{
_note_group->clear (true);
events.clear ();
_optimization_iterator = events.end();
}
/** Update the positions of our representation of a note.
* @param ev The GhostEvent from the parent MidiRegionView.
*/
void
MidiGhostRegion::update_note (GhostEvent* ev)
{
MidiStreamView* mv = midi_view();
if (!mv) {
return;
}
_tmp_rect = static_cast<ArdourCanvas::Rectangle*>(ev->item);
uint8_t const note_num = ev->event->note()->note();
double const y = note_y(trackview, mv, note_num);
double const h = note_height(trackview, mv);
_tmp_rect->set (ArdourCanvas::Rect (ev->event->x0(), y, ev->event->x1(), y + h));
}
/** Update the positions of our representation of a parent's hit.
* @param ev The GhostEvent from the parent MidiRegionView.
*/
void
MidiGhostRegion::update_hit (GhostEvent* ev)
{
MidiStreamView* mv = midi_view();
if (!mv) {
return;
}
_tmp_poly = static_cast<ArdourCanvas::Polygon*>(ev->item);
2017-01-26 08:58:09 -05:00
uint8_t const note_num = ev->event->note()->note();
double const h = note_height(trackview, mv);
double const y = note_y(trackview, mv, note_num);
2017-01-26 08:58:09 -05:00
ArdourCanvas::Duple ppos = ev->item->position();
ArdourCanvas::Duple gpos = _tmp_poly->position();
gpos.x = ppos.x;
gpos.y = y;
_tmp_poly->set_position(gpos);
_tmp_poly->set(Hit::points(h));
}
void
2013-04-04 18:45:27 -04:00
MidiGhostRegion::remove_note (NoteBase* note)
{
EventList::iterator f = events.find (note->note());
if (f == events.end()) {
return;
}
delete f->second;
events.erase (f);
_optimization_iterator = events.end ();
}
void
MidiGhostRegion::view_changed ()
{
model_changed();
}
void
MidiGhostRegion::model_changed ()
{
/* we rely on the parent MRV having removed notes not in the model */
for (EventList::iterator i = events.begin(); i != events.end(); ) {
std::shared_ptr<NoteType> note = i->first;
GhostEvent* cne = i->second;
const bool visible = (note->note() >= parent_mrv._current_range_min) &&
(note->note() <= parent_mrv._current_range_max);
if (visible) {
if (cne->is_hit) {
update_hit (cne);
} else {
update_note (cne);
}
cne->item->show ();
} else {
cne->item->hide ();
}
++i;
}
}
/** Given a note in our parent region (ie the actual MidiRegionView), find our
* representation of it.
* @return Our Event, or 0 if not found.
*/
2013-04-04 18:45:27 -04:00
MidiGhostRegion::GhostEvent *
MidiGhostRegion::find_event (std::shared_ptr<NoteType> parent)
{
/* we are using _optimization_iterator to speed up the common case where a caller
is going through our notes in order.
*/
if (_optimization_iterator != events.end()) {
++_optimization_iterator;
if (_optimization_iterator != events.end() && _optimization_iterator->first == parent) {
return _optimization_iterator->second;
}
}
_optimization_iterator = events.find (parent);
if (_optimization_iterator != events.end()) {
return _optimization_iterator->second;
}
return 0;
}