some initial bits of work on canvas allocation
[ardour.git] / gtk2_ardour / midi_streamview.cc
1 /*
2     Copyright (C) 2001-2007 Paul Davis
3
4     This program is free software; you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation; either version 2 of the License, or
7     (at your option) any later version.
8
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13
14     You should have received a copy of the GNU General Public License
15     along with this program; if not, write to the Free Software
16     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17 */
18
19 #include <cmath>
20 #include <utility>
21
22 #include <gtkmm.h>
23
24 #include <gtkmm2ext/gtk_ui.h>
25
26 #include "canvas/line_set.h"
27 #include "canvas/rectangle.h"
28
29 #include "ardour/midi_region.h"
30 #include "ardour/midi_source.h"
31 #include "ardour/midi_track.h"
32 #include "ardour/operations.h"
33 #include "ardour/region_factory.h"
34 #include "ardour/session.h"
35 #include "ardour/smf_source.h"
36
37 #include "gui_thread.h"
38 #include "midi_region_view.h"
39 #include "midi_streamview.h"
40 #include "midi_time_axis.h"
41 #include "midi_util.h"
42 #include "public_editor.h"
43 #include "region_selection.h"
44 #include "region_view.h"
45 #include "rgb_macros.h"
46 #include "selection.h"
47 #include "ui_config.h"
48 #include "utils.h"
49
50 #include "pbd/i18n.h"
51
52 using namespace std;
53 using namespace ARDOUR;
54 using namespace ARDOUR_UI_UTILS;
55 using namespace PBD;
56 using namespace Editing;
57
58 MidiStreamView::MidiStreamView (MidiTimeAxisView& tv)
59         : StreamView (tv)
60         , note_range_adjustment(0.0f, 0.0f, 0.0f)
61         , _range_dirty(false)
62         , _range_sum_cache(-1.0)
63         , _lowest_note(60)
64         , _highest_note(71)
65         , _data_note_min(60)
66         , _data_note_max(71)
67         , _note_lines (0)
68         , _updates_suspended (false)
69 {
70         /* use a group dedicated to MIDI underlays. Audio underlays are not in this group. */
71         midi_underlay_group = new ArdourCanvas::Container (_canvas_group);
72         midi_underlay_group->lower_to_bottom();
73
74         /* put the note lines in the timeaxisview's group, so it
75            can be put below ghost regions from MIDI underlays
76         */
77         _note_lines = new ArdourCanvas::LineSet (_canvas_group, ArdourCanvas::LineSet::Horizontal);
78
79         _note_lines->Event.connect(
80                 sigc::bind(sigc::mem_fun(_trackview.editor(),
81                                          &PublicEditor::canvas_stream_view_event),
82                            _note_lines, &_trackview));
83
84         _note_lines->lower_to_bottom();
85
86         color_handler ();
87
88         UIConfiguration::instance().ColorsChanged.connect(sigc::mem_fun(*this, &MidiStreamView::color_handler));
89
90         note_range_adjustment.set_page_size(_highest_note - _lowest_note);
91         note_range_adjustment.set_value(_lowest_note);
92
93         note_range_adjustment.signal_value_changed().connect(
94                 sigc::mem_fun(*this, &MidiStreamView::note_range_adjustment_changed));
95 }
96
97 MidiStreamView::~MidiStreamView ()
98 {
99 }
100
101 RegionView*
102 MidiStreamView::create_region_view (boost::shared_ptr<Region> r, bool /*wfd*/, bool recording)
103 {
104         boost::shared_ptr<MidiRegion> region = boost::dynamic_pointer_cast<MidiRegion> (r);
105
106         if (region == 0) {
107                 return 0;
108         }
109
110         RegionView* region_view = NULL;
111         if (recording) {
112                 region_view = new MidiRegionView (
113                         _canvas_group, _trackview, region,
114                         _samples_per_pixel, region_color, recording,
115                         TimeAxisViewItem::Visibility(TimeAxisViewItem::ShowFrame));
116         } else {
117                 region_view = new MidiRegionView (_canvas_group, _trackview, region,
118                                                   _samples_per_pixel, region_color);
119         }
120
121         region_view->init (false);
122
123         return region_view;
124 }
125
126 RegionView*
127 MidiStreamView::add_region_view_internal (boost::shared_ptr<Region> r, bool wait_for_data, bool recording)
128 {
129         boost::shared_ptr<MidiRegion> region = boost::dynamic_pointer_cast<MidiRegion> (r);
130
131         if (!region) {
132                 return 0;
133         }
134
135         for (list<RegionView*>::iterator i = region_views.begin(); i != region_views.end(); ++i) {
136                 if ((*i)->region() == r) {
137
138                         /* great. we already have a MidiRegionView for this Region. use it again. */
139
140                         (*i)->set_valid (true);
141
142                         display_region(dynamic_cast<MidiRegionView*>(*i), wait_for_data);
143
144                         return 0;
145                 }
146         }
147
148         MidiRegionView* region_view = dynamic_cast<MidiRegionView*> (create_region_view (r, wait_for_data, recording));
149         if (region_view == 0) {
150                 return 0;
151         }
152
153         region_views.push_front (region_view);
154
155         /* display events and find note range */
156         display_region (region_view, wait_for_data);
157
158         /* fit note range if we are importing */
159         if (_trackview.session()->operation_in_progress (Operations::insert_file)) {
160                 set_note_range (ContentsRange);
161         }
162
163         /* catch regionview going away */
164         boost::weak_ptr<Region> wr (region); // make this explicit
165         region->DropReferences.connect (*this, invalidator (*this), boost::bind (&MidiStreamView::remove_region_view, this, wr), gui_context());
166
167         RegionViewAdded (region_view);
168
169         return region_view;
170 }
171
172 void
173 MidiStreamView::display_region(MidiRegionView* region_view, bool load_model)
174 {
175         if (!region_view) {
176                 return;
177         }
178
179         region_view->enable_display (true);
180         region_view->set_height (child_height());
181
182         boost::shared_ptr<MidiSource> source(region_view->midi_region()->midi_source(0));
183         if (!source) {
184                 error << _("attempt to display MIDI region with no source") << endmsg;
185                 return;
186         }
187
188         if (load_model) {
189                 Glib::Threads::Mutex::Lock lm(source->mutex());
190                 source->load_model(lm);
191         }
192
193         if (!source->model()) {
194                 error << _("attempt to display MIDI region with no model") << endmsg;
195                 return;
196         }
197
198         _range_dirty = update_data_note_range(
199                 source->model()->lowest_note(),
200                 source->model()->highest_note());
201
202         // Display region contents
203         region_view->display_model(source->model());
204 }
205
206
207 void
208 MidiStreamView::display_track (boost::shared_ptr<Track> tr)
209 {
210         StreamView::display_track (tr);
211
212         draw_note_lines();
213
214         NoteRangeChanged();
215 }
216
217 void
218 MidiStreamView::update_contents_metrics(boost::shared_ptr<Region> r)
219 {
220         boost::shared_ptr<MidiRegion> mr = boost::dynamic_pointer_cast<MidiRegion>(r);
221         if (mr) {
222                 Glib::Threads::Mutex::Lock lm(mr->midi_source(0)->mutex());
223                 mr->midi_source(0)->load_model(lm);
224                 _range_dirty = update_data_note_range(
225                         mr->model()->lowest_note(),
226                         mr->model()->highest_note());
227         }
228 }
229
230 bool
231 MidiStreamView::update_data_note_range(uint8_t min, uint8_t max)
232 {
233         bool dirty = false;
234         if (min < _data_note_min) {
235                 _data_note_min = min;
236                 dirty = true;
237         }
238         if (max > _data_note_max) {
239                 _data_note_max = max;
240                 dirty = true;
241         }
242         return dirty;
243 }
244
245 void
246 MidiStreamView::set_layer_display (LayerDisplay d)
247 {
248         if (d != Overlaid) {
249                 return;
250         }
251
252         StreamView::set_layer_display (d);
253 }
254
255 void
256 MidiStreamView::redisplay_track ()
257 {
258         if (!_trackview.is_midi_track()) {
259                 return;
260         }
261
262         list<RegionView*>::iterator i;
263
264         // Load models if necessary, and find note range of all our contents
265         _range_dirty = false;
266         _data_note_min = 127;
267         _data_note_max = 0;
268         _trackview.track()->playlist()->foreach_region(
269                 sigc::mem_fun (*this, &StreamView::update_contents_metrics));
270
271         // No notes, use default range
272         if (!_range_dirty) {
273                 _data_note_min = 60;
274                 _data_note_max = 71;
275         }
276
277         // Flag region views as invalid and disable drawing
278         for (i = region_views.begin(); i != region_views.end(); ++i) {
279                 (*i)->set_valid(false);
280                 (*i)->enable_display(false);
281         }
282
283         // Add and display region views, and flag them as valid
284         _trackview.track()->playlist()->foreach_region(
285                 sigc::hide_return (sigc::mem_fun (*this, &StreamView::add_region_view)));
286
287         // Stack regions by layer, and remove invalid regions
288         layer_regions();
289
290         // Update note range (not regions which are correct) and draw note lines
291         apply_note_range(_lowest_note, _highest_note, false);
292 }
293
294
295 void
296 MidiStreamView::update_contents_height ()
297 {
298         StreamView::update_contents_height();
299
300         _note_lines->set_extent (ArdourCanvas::COORD_MAX);
301
302         apply_note_range (lowest_note(), highest_note(), true);
303 }
304
305 void
306 MidiStreamView::draw_note_lines()
307 {
308         if (!_note_lines || _updates_suspended) {
309                 return;
310         }
311
312         double y;
313         double prev_y = .5;
314         uint32_t color;
315
316         _note_lines->clear();
317
318         if (child_height() < 140 || note_height() < 3) {
319                 /* track is too small for note lines, or there are too many */
320                 return;
321         }
322
323         /* do this is order of highest ... lowest since that matches the
324          * coordinate system in which y=0 is at the top
325          */
326
327         for (int i = highest_note() + 1; i >= lowest_note(); --i) {
328
329                 y = floor(note_to_y (i)) + .5;
330
331                 /* this is the line actually corresponding to the division
332                  * between notes
333                  */
334
335                 if (i <= highest_note()) {
336                         _note_lines->add (y, 1.0, UIConfiguration::instance().color ("piano roll black outline"));
337                 }
338
339                 /* now add a thicker line/bar which covers the entire vertical
340                  * height of this note.
341                  */
342
343                 switch (i % 12) {
344                 case 1:
345                 case 3:
346                 case 6:
347                 case 8:
348                 case 10:
349                         color = UIConfiguration::instance().color_mod ("piano roll black", "piano roll black");
350                         break;
351                 default:
352                         color = UIConfiguration::instance().color_mod ("piano roll white", "piano roll white");
353                         break;
354                 }
355
356                 double h = y - prev_y;
357                 double mid = y + (h/2.0);
358
359                 if (height > 1.0) { // XXX ? should that not be h >= 1 ?
360                         _note_lines->add (mid, h, color);
361                 }
362
363                 prev_y = y;
364         }
365 }
366
367 void
368 MidiStreamView::set_note_range(VisibleNoteRange r)
369 {
370         if (r == FullRange) {
371                 _lowest_note = 0;
372                 _highest_note = 127;
373         } else {
374                 _lowest_note = _data_note_min;
375                 _highest_note = _data_note_max;
376         }
377
378         apply_note_range(_lowest_note, _highest_note, true);
379 }
380
381 void
382 MidiStreamView::apply_note_range(uint8_t lowest, uint8_t highest, bool to_region_views)
383 {
384         _highest_note = highest;
385         _lowest_note = lowest;
386
387         int const max_note_height = 20;  // This should probably be based on text size...
388         int const range = _highest_note - _lowest_note;
389         int const pixels_per_note = floor (child_height () / range);
390
391         /* do not grow note height beyond 10 pixels */
392         if (pixels_per_note > max_note_height) {
393
394                 int const available_note_range = floor (child_height() / max_note_height);
395                 int additional_notes = available_note_range - range;
396
397                 /* distribute additional notes to higher and lower ranges, clamp at 0 and 127 */
398                 for (int i = 0; i < additional_notes; i++){
399
400                         if (i % 2 && _highest_note < 127){
401                                 _highest_note++;
402                         }
403                         else if (i % 2) {
404                                 _lowest_note--;
405                         }
406                         else if (_lowest_note > 0){
407                                 _lowest_note--;
408                         }
409                         else {
410                                 _highest_note++;
411                         }
412                 }
413         }
414
415         note_range_adjustment.set_page_size(_highest_note - _lowest_note);
416         note_range_adjustment.set_value(_lowest_note);
417
418         draw_note_lines();
419
420         if (to_region_views) {
421                 apply_note_range_to_regions ();
422         }
423
424         NoteRangeChanged();
425 }
426
427 void
428 MidiStreamView::apply_note_range_to_regions ()
429 {
430         if (!_updates_suspended) {
431                 for (list<RegionView*>::iterator i = region_views.begin(); i != region_views.end(); ++i) {
432                         ((MidiRegionView*)(*i))->apply_note_range(_lowest_note, _highest_note);
433                 }
434         }
435 }
436
437 void
438 MidiStreamView::update_note_range(uint8_t note_num)
439 {
440         _data_note_min = min(_data_note_min, note_num);
441         _data_note_max = max(_data_note_max, note_num);
442 }
443
444 void
445 MidiStreamView::setup_rec_box ()
446 {
447         // cerr << _trackview.name() << " streamview SRB\n";
448
449         if (_trackview.session()->transport_rolling()) {
450
451                 if (!rec_active &&
452                     _trackview.session()->record_status() == Session::Recording &&
453                     _trackview.track()->rec_enable_control()->get_value()) {
454
455                         if (UIConfiguration::instance().get_show_waveforms_while_recording() && rec_regions.size() == rec_rects.size()) {
456
457                                 /* add a new region, but don't bother if they set show-waveforms-while-recording mid-record */
458
459                                 MidiRegion::SourceList sources;
460
461                                 rec_data_ready_connections.drop_connections ();
462
463                                 sources.push_back (_trackview.midi_track()->write_source());
464
465                                 // handle multi
466
467                                 framepos_t start = 0;
468                                 if (rec_regions.size() > 0) {
469                                         start = rec_regions.back().first->start()
470                                                 + _trackview.track()->get_captured_frames(rec_regions.size()-1);
471                                 }
472
473                                 if (!rec_regions.empty()) {
474                                         MidiRegionView* mrv = dynamic_cast<MidiRegionView*> (rec_regions.back().second);
475                                         mrv->end_write ();
476                                 }
477
478                                 PropertyList plist;
479
480                                 plist.add (ARDOUR::Properties::start, start);
481                                 plist.add (ARDOUR::Properties::length, 1);
482                                 /* Just above we're setting this nascent region's length to 1.  I think this
483                                    is so that the RegionView gets created with a non-zero width, as apparently
484                                    creating a RegionView with a zero width causes it never to be displayed
485                                    (there is a warning in TimeAxisViewItem::init about this).  However, we
486                                    must also set length_beats to something non-zero, otherwise the frame length
487                                    of 1 causes length_beats to be set to some small quantity << 1.  Then
488                                    when the position is set up below, this length_beats is used to recompute
489                                    length using BeatsFramesConverter::to, which is slightly innacurate for small
490                                    beats values because it converts floating point beats to bars, beats and
491                                    integer ticks.  The upshot of which being that length gets set back to 0,
492                                    meaning no region view is ever seen, meaning no MIDI notes during record (#3820).
493                                 */
494                                 plist.add (ARDOUR::Properties::length_beats, 1);
495                                 plist.add (ARDOUR::Properties::name, string());
496                                 plist.add (ARDOUR::Properties::layer, 0);
497
498                                 boost::shared_ptr<MidiRegion> region (boost::dynamic_pointer_cast<MidiRegion>
499                                                                       (RegionFactory::create (sources, plist, false)));
500                                 if (region) {
501                                         region->set_start (_trackview.track()->current_capture_start()
502                                                            - _trackview.track()->get_capture_start_frame (0));
503                                         region->set_position (_trackview.session()->transport_frame());
504
505                                         RegionView* rv = add_region_view_internal (region, false, true);
506                                         MidiRegionView* mrv = dynamic_cast<MidiRegionView*> (rv);
507                                         mrv->begin_write ();
508
509                                         /* rec region will be destroyed in setup_rec_box */
510                                         rec_regions.push_back (make_pair (region, rv));
511
512                                         /* we add the region later */
513                                         setup_new_rec_layer_time (region);
514                                 } else {
515                                         error << _("failed to create MIDI region") << endmsg;
516                                 }
517                         }
518
519                         /* start a new rec box */
520
521                         create_rec_box(_trackview.midi_track()->current_capture_start(), 0);
522
523                 } else if (rec_active &&
524                            (_trackview.session()->record_status() != Session::Recording ||
525                             !_trackview.track()->rec_enable_control()->get_value())) {
526                         screen_update_connection.disconnect();
527                         rec_active = false;
528                         rec_updating = false;
529                 }
530
531         } else {
532
533                 // cerr << "\tNOT rolling, rec_rects = " << rec_rects.size() << " rec_regions = " << rec_regions.size() << endl;
534
535                 if (!rec_rects.empty() || !rec_regions.empty()) {
536
537                         /* disconnect rapid update */
538                         screen_update_connection.disconnect();
539                         rec_data_ready_connections.drop_connections ();
540                         rec_updating = false;
541                         rec_active = false;
542
543                         /* remove temp regions */
544
545                         for (list<pair<boost::shared_ptr<Region>,RegionView*> >::iterator iter = rec_regions.begin(); iter != rec_regions.end();) {
546                                 list<pair<boost::shared_ptr<Region>,RegionView*> >::iterator tmp;
547
548                                 tmp = iter;
549                                 ++tmp;
550
551                                 (*iter).first->drop_references ();
552
553                                 iter = tmp;
554                         }
555
556                         rec_regions.clear();
557
558                         // cerr << "\tclear " << rec_rects.size() << " rec rects\n";
559
560                         /* transport stopped, clear boxes */
561                         for (vector<RecBoxInfo>::iterator iter=rec_rects.begin(); iter != rec_rects.end(); ++iter) {
562                                 RecBoxInfo &rect = (*iter);
563                                 delete rect.rectangle;
564                         }
565
566                         rec_rects.clear();
567
568                 }
569         }
570 }
571
572 void
573 MidiStreamView::color_handler ()
574 {
575         draw_note_lines ();
576
577         if (_trackview.is_midi_track()) {
578                 canvas_rect->set_fill_color (UIConfiguration::instance().color_mod ("midi track base", "midi track base"));
579         } else {
580                 canvas_rect->set_fill_color (UIConfiguration::instance().color ("midi bus base"));
581         }
582 }
583
584 void
585 MidiStreamView::note_range_adjustment_changed()
586 {
587         double sum = note_range_adjustment.get_value() + note_range_adjustment.get_page_size();
588         int lowest = (int) floor(note_range_adjustment.get_value());
589         int highest;
590
591         if (sum == _range_sum_cache) {
592                 //cerr << "cached" << endl;
593                 highest = (int) floor(sum);
594         } else {
595                 //cerr << "recalc" << endl;
596                 highest = lowest + (int) floor(note_range_adjustment.get_page_size());
597                 _range_sum_cache = sum;
598         }
599
600         if (lowest == _lowest_note && highest == _highest_note) {
601                 return;
602         }
603
604         //cerr << "note range adjustment changed: " << lowest << " " << highest << endl;
605         //cerr << "  val=" << v_zoom_adjustment.get_value() << " page=" << v_zoom_adjustment.get_page_size() << " sum=" << v_zoom_adjustment.get_value() + v_zoom_adjustment.get_page_size() << endl;
606
607         _lowest_note = lowest;
608         _highest_note = highest;
609         apply_note_range(lowest, highest, true);
610 }
611
612 void
613 MidiStreamView::update_rec_box ()
614 {
615         StreamView::update_rec_box ();
616
617         if (rec_regions.empty()) {
618                 return;
619         }
620
621         /* Update the region being recorded to reflect where we currently are */
622         boost::shared_ptr<ARDOUR::Region> region = rec_regions.back().first;
623         region->set_length (_trackview.track()->current_capture_end () - _trackview.track()->current_capture_start(), 0);
624
625         MidiRegionView* mrv = dynamic_cast<MidiRegionView*> (rec_regions.back().second);
626         mrv->extend_active_notes ();
627 }
628
629 uint8_t
630 MidiStreamView::y_to_note (double y) const
631 {
632         int const n = ((contents_height() - y) / contents_height() * (double)contents_note_range())
633                 + lowest_note();
634
635         if (n < 0) {
636                 return 0;
637         } else if (n > 127) {
638                 return 127;
639         }
640
641         /* min due to rounding and/or off-by-one errors */
642         return min ((uint8_t) n, highest_note());
643 }
644
645 /** Suspend updates to the regions' note ranges and our
646  *  note lines until resume_updates() is called.
647  */
648 void
649 MidiStreamView::suspend_updates ()
650 {
651         _updates_suspended = true;
652 }
653
654 /** Resume updates to region note ranges and note lines,
655  *  and update them now.
656  */
657 void
658 MidiStreamView::resume_updates ()
659 {
660         _updates_suspended = false;
661
662         draw_note_lines ();
663         apply_note_range_to_regions ();
664
665         _canvas_group->redraw ();
666 }
667
668 struct RegionPositionSorter {
669         bool operator() (RegionView* a, RegionView* b) {
670                 return a->region()->position() < b->region()->position();
671         }
672 };
673
674 bool
675 MidiStreamView::paste (ARDOUR::framepos_t pos, const Selection& selection, PasteContext& ctx, const int32_t sub_num)
676 {
677         /* Paste into the first region which starts on or before pos.  Only called when
678            using an internal editing tool. */
679
680         if (region_views.empty()) {
681                 return false;
682         }
683
684         region_views.sort (RegionView::PositionOrder());
685
686         list<RegionView*>::const_iterator prev = region_views.begin ();
687
688         for (list<RegionView*>::const_iterator i = region_views.begin(); i != region_views.end(); ++i) {
689                 if ((*i)->region()->position() > pos) {
690                         break;
691                 }
692                 prev = i;
693         }
694
695         boost::shared_ptr<Region> r = (*prev)->region ();
696
697         /* If *prev doesn't cover pos, it's no good */
698         if (r->position() > pos || ((r->position() + r->length()) < pos)) {
699                 return false;
700         }
701
702         MidiRegionView* mrv = dynamic_cast<MidiRegionView*> (*prev);
703         return mrv ? mrv->paste(pos, selection, ctx, sub_num) : false;
704 }