* MIDI control lanes: Set Interpolationtype according to Parameter
[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 <cassert>
21 #include <utility>
22
23 #include <gtkmm.h>
24
25 #include <gtkmm2ext/gtk_ui.h>
26
27 #include <ardour/midi_playlist.h>
28 #include <ardour/midi_region.h>
29 #include <ardour/midi_source.h>
30 #include <ardour/midi_diskstream.h>
31 #include <ardour/midi_track.h>
32 #include <ardour/smf_source.h>
33 #include <ardour/region_factory.h>
34
35 #include "midi_streamview.h"
36 #include "region_view.h"
37 #include "midi_region_view.h"
38 #include "midi_time_axis.h"
39 #include "canvas-simplerect.h"
40 #include "region_selection.h"
41 #include "selection.h"
42 #include "public_editor.h"
43 #include "ardour_ui.h"
44 #include "rgb_macros.h"
45 #include "gui_thread.h"
46 #include "utils.h"
47 #include "simplerect.h"
48 #include "lineset.h"
49
50 using namespace std;
51 using namespace ARDOUR;
52 using namespace PBD;
53 using namespace Editing;
54
55 MidiStreamView::MidiStreamView (MidiTimeAxisView& tv)
56         : StreamView (tv)
57         , note_range_adjustment(0.0f, 0.0f, 0.0f)
58         , _range_dirty(false)
59         , _range_sum_cache(-1.0)
60         , _lowest_note(60)
61         , _highest_note(71)
62         , _data_note_min(60)
63         , _data_note_max(71)
64 {
65         if (tv.is_track())
66                 stream_base_color = ARDOUR_UI::config()->canvasvar_MidiTrackBase.get();
67         else
68                 stream_base_color = ARDOUR_UI::config()->canvasvar_MidiBusBase.get();
69
70         use_rec_regions = tv.editor().show_waveforms_recording ();
71
72         /* use a group dedicated to MIDI underlays. Audio underlays are not in this group. */
73         midi_underlay_group = new ArdourCanvas::Group (*canvas_group);
74         midi_underlay_group->lower_to_bottom();
75
76         /* put the note lines in the timeaxisview's group, so it 
77            can be put below ghost regions from MIDI underlays*/
78         _note_lines = new ArdourCanvas::LineSet(*canvas_group, ArdourCanvas::LineSet::Horizontal);
79
80         _note_lines->property_x1() = 0;
81         _note_lines->property_y1() = 0;
82         _note_lines->property_x2() = trackview().editor().frame_to_pixel (max_frames);
83         _note_lines->property_y2() = 0;
84
85         _note_lines->signal_event().connect (bind (mem_fun (_trackview.editor(), &PublicEditor::canvas_stream_view_event), _note_lines, &_trackview));
86         _note_lines->lower_to_bottom();
87
88         ColorsChanged.connect(mem_fun(*this, &MidiStreamView::draw_note_lines));
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 (mem_fun (*this, &MidiStreamView::note_range_adjustment_changed));
94 }
95
96 MidiStreamView::~MidiStreamView ()
97 {
98 }
99
100 static void
101 veto_note_range(uint8_t& min, uint8_t& max)
102 {
103         /* Legal notes, thanks */
104         if (max > 127)
105                 max = 127;
106         if (min > 127)
107                 min = 127;
108         
109         /* Always display at least one octave in [0, 127] */
110         if (max == 127) {
111                 if (min > (127 - 11)) {
112                         min = 127 - 11;
113                 }
114         } else if (max < min + 11) {
115                 uint8_t d = 11 - (max - min);
116                 if (max + d/2 > 127) {
117                         min -= d;
118                 } else {
119                         min -= d / 2;
120                         max += d / 2;
121                 }
122         }
123         assert(max - min >= 11);
124         assert(max < 127);
125         assert(min < 127);
126 }
127
128 RegionView*
129 MidiStreamView::add_region_view_internal (boost::shared_ptr<Region> r, bool wfd, bool recording)
130 {
131         boost::shared_ptr<MidiRegion> region = boost::dynamic_pointer_cast<MidiRegion> (r);
132
133         if (region == 0) {
134                 return NULL;
135         }
136
137         MidiRegionView *region_view;
138         list<RegionView *>::iterator i;
139
140         for (i = region_views.begin(); i != region_views.end(); ++i) {
141                 if ((*i)->region() == r) {
142                         
143                         /* great. we already have a MidiRegionView for this Region. use it again. */
144
145                         (*i)->set_valid (true);
146                         
147                         display_region(dynamic_cast<MidiRegionView*>(*i), wfd);
148
149                         return NULL;
150                 }
151         }
152         
153         region_view = new MidiRegionView (canvas_group, _trackview, region, 
154                         _samples_per_unit, region_color);
155                 
156         region_view->init (region_color, false);
157         region_views.push_front (region_view);
158                         
159         /* display events and find note range */
160         display_region(region_view, wfd);
161
162         /* catch regionview going away */
163         region->GoingAway.connect (bind (mem_fun (*this, &MidiStreamView::remove_region_view), region));
164         
165         RegionViewAdded (region_view);
166
167         return region_view;
168 }
169
170 void
171 MidiStreamView::display_region(MidiRegionView* region_view, bool load_model)
172 {
173         if ( ! region_view)
174                 return;
175                         
176         region_view->enable_display(true);
177
178         boost::shared_ptr<MidiSource> source(region_view->midi_region()->midi_source(0));
179
180         if (load_model) {
181                 source->load_model();
182         }
183
184         _range_dirty = update_data_note_range(
185                         source->model()->lowest_note(),
186                         source->model()->highest_note());
187
188         // Display region contents
189         region_view->display_model(source->model());
190 }
191
192 void
193 MidiStreamView::display_diskstream (boost::shared_ptr<Diskstream> ds)
194 {
195         StreamView::display_diskstream(ds);
196         draw_note_lines();
197         NoteRangeChanged();
198 }
199                         
200 bool
201 MidiStreamView::update_data_note_range(uint8_t min, uint8_t max)
202 {
203         bool dirty = false;
204         if (min < _data_note_min) {
205                 _data_note_min = min;
206                 dirty = true;
207         }
208         if (max > _data_note_max) {
209                 _data_note_max = max;
210                 dirty = true;
211         }
212         return dirty;
213 }
214
215 // FIXME: code duplication with AudioStreamView
216 void
217 MidiStreamView::redisplay_diskstream ()
218 {
219         list<RegionView *>::iterator i, tmp;
220
221         _range_dirty = false;
222         _data_note_min = 127;
223         _data_note_max = 0;
224
225         for (i = region_views.begin(); i != region_views.end(); ++i) {
226                 (*i)->set_valid (false);
227                 (*i)->enable_display (false);
228                 
229                 // Load model if it isn't already, to get note range
230                 MidiRegionView* mrv = dynamic_cast<MidiRegionView*>(*i);
231                 if (mrv) {
232                         mrv->midi_region()->midi_source(0)->load_model();
233                         _range_dirty = update_data_note_range(
234                                         mrv->midi_region()->model()->lowest_note(),
235                                         mrv->midi_region()->model()->highest_note());
236                 }
237         }
238
239         // No notes, use default range
240         if (!_range_dirty) {
241                 _data_note_min = 60;
242                 _data_note_max = 71;
243         }
244         
245         bool range_changed = false;
246
247         // Extend visible range to show newly recorded data, if necessary
248         if (_data_note_min < _lowest_note) {
249                 _lowest_note = _data_note_min;
250                 range_changed = true;
251         }
252         if (_data_note_max > _highest_note) {
253                 _highest_note = _data_note_max;
254                 range_changed = true;
255         }
256         
257         veto_note_range(_lowest_note, _highest_note);
258         
259         if (_trackview.is_midi_track()) {
260                 _trackview.get_diskstream()->playlist()->foreach_region (
261                                 static_cast<StreamView*>(this), &StreamView::add_region_view);
262         }
263
264         RegionViewList copy;
265         
266         /* Place regions */
267         for (i = region_views.begin(); i != region_views.end(); ) {
268                 tmp = i;
269                 tmp++;
270
271                 if (!(*i)->is_valid()) {
272                         delete *i;
273                         region_views.erase (i);
274                         i = tmp;
275                         continue;
276                 } else {
277                         (*i)->enable_display(true);
278                         (*i)->set_height(height); // apply note range
279                 }
280                 
281                 /* Sort regionviews by layer so that when we call region_layered ()
282                    the canvas layering works out (in non-stacked mode). */
283
284                 if (copy.size() == 0) {
285                         copy.push_front((*i));
286                         i = tmp;
287                         continue;
288                 }
289
290                 RegionViewList::iterator k = copy.begin();
291                 RegionViewList::iterator l = copy.end();
292                 l--;
293
294                 if ((*i)->region()->layer() <= (*k)->region()->layer()) {
295                         copy.push_front((*i));
296                         i = tmp;
297                         continue;
298                 } else if ((*i)->region()->layer() >= (*l)->region()->layer()) {
299                         copy.push_back((*i));
300                         i = tmp;
301                         continue;
302                 }
303
304                 for (RegionViewList::iterator j = copy.begin(); j != copy.end(); ++j) {
305                         if ((*j)->region()->layer() >= (*i)->region()->layer()) {
306                                 copy.insert(j, (*i));
307                                 break;
308                         }
309                 }
310
311                 i = tmp;
312         }
313         
314         /* Fix canvas layering */
315         for (RegionViewList::iterator j = copy.begin(); j != copy.end(); ++j) {
316                 region_layered (*j);
317         }
318         
319         /* Update note range and re-draw note lines if necessary */
320         apply_note_range(_lowest_note, _highest_note);
321         NoteRangeChanged();
322 }
323
324
325 void
326 MidiStreamView::update_contents_height ()
327 {
328         StreamView::update_contents_height();
329         _note_lines->property_y2() = height;
330         draw_note_lines();
331 }
332         
333 void
334 MidiStreamView::draw_note_lines()
335 {
336         double y;
337         double prev_y = contents_height();
338         uint32_t color;
339
340         _note_lines->clear();
341
342         for (int i = lowest_note(); i <= highest_note(); ++i) {
343                 y = floor(note_to_y(i));
344                 
345                 _note_lines->add_line(prev_y, 1.0, ARDOUR_UI::config()->canvasvar_PianoRollBlackOutline.get());
346
347                 switch (i % 12) {
348                 case 1:
349                 case 3:
350                 case 6:
351                 case 8:
352                 case 10:
353                         color = ARDOUR_UI::config()->canvasvar_PianoRollBlack.get();
354                         break;
355                 default:
356                         color = ARDOUR_UI::config()->canvasvar_PianoRollWhite.get();
357                         break;
358                 }
359
360                 if (i == highest_note()) {
361                         _note_lines->add_line(y, prev_y - y, color);
362                 } else {
363                         _note_lines->add_line(y + 1.0, prev_y - y - 1.0, color);
364                 }
365
366                 prev_y = y;
367         }
368 }
369
370 void
371 MidiStreamView::set_note_range(VisibleNoteRange r)
372 {
373         if (r == FullRange) {
374                 _lowest_note = 0;
375                 _highest_note = 127;
376         } else {
377                 _lowest_note = _data_note_min;
378                 _highest_note = _data_note_max;
379         }
380
381         apply_note_range(_lowest_note, _highest_note);
382 }
383
384 void
385 MidiStreamView::apply_note_range(uint8_t lowest, uint8_t highest)
386 {
387         _highest_note = highest;
388         _lowest_note = lowest;
389         note_range_adjustment.set_page_size(_highest_note - _lowest_note);
390         note_range_adjustment.set_value(_lowest_note);
391         draw_note_lines();
392         
393         for (list<RegionView*>::iterator i = region_views.begin(); i != region_views.end(); ++i) {
394                 ((MidiRegionView*)(*i))->apply_note_range(lowest, highest);
395         }
396
397         NoteRangeChanged();
398 }
399
400 void 
401 MidiStreamView::update_note_range(uint8_t note_num)
402 {
403         assert(note_num <= 127);
404         _data_note_min = min(_data_note_min, note_num);
405         _data_note_max = max(_data_note_max, note_num);
406 }
407         
408 void
409 MidiStreamView::setup_rec_box ()
410 {
411         // cerr << _trackview.name() << " streamview SRB\n";
412
413         if (_trackview.session().transport_rolling()) {
414
415                 if (!rec_active && 
416                     _trackview.session().record_status() == Session::Recording && 
417                     _trackview.get_diskstream()->record_enabled()) {
418
419                         if (use_rec_regions && rec_regions.size() == rec_rects.size()) {
420
421                                 /* add a new region, but don't bother if they set use_rec_regions mid-record */
422
423                                 MidiRegion::SourceList sources;
424                                 
425                                 for (list<sigc::connection>::iterator prc = rec_data_ready_connections.begin(); prc != rec_data_ready_connections.end(); ++prc) {
426                                         (*prc).disconnect();
427                                 }
428                                 rec_data_ready_connections.clear();
429
430                                 // FIXME
431                                 boost::shared_ptr<MidiDiskstream> mds = boost::dynamic_pointer_cast<MidiDiskstream>(_trackview.get_diskstream());
432                                 assert(mds);
433
434                                 sources.push_back(mds->write_source());
435                                 
436                                 rec_data_ready_connections.push_back (mds->write_source()->ViewDataRangeReady.connect (bind (mem_fun (*this, &MidiStreamView::rec_data_range_ready), boost::weak_ptr<Source>(mds->write_source())))); 
437
438                                 // handle multi
439                                 
440                                 jack_nframes_t start = 0;
441                                 if (rec_regions.size() > 0) {
442                                         start = rec_regions.back().first->position() + _trackview.get_diskstream()->get_captured_frames(rec_regions.size()-1);
443                                 }
444                                 
445                                 boost::shared_ptr<MidiRegion> region (boost::dynamic_pointer_cast<MidiRegion>
446                                         (RegionFactory::create (sources, start, 1 , "", 0, (Region::Flag)(Region::DefaultFlags | Region::DoNotSaveState), false)));
447                                 assert(region);
448                                 region->set_position (_trackview.session().transport_frame(), this);
449                                 rec_regions.push_back (make_pair(region, (RegionView*)0));
450                                 
451                                 // rec regions are destroyed in setup_rec_box
452
453                                 /* we add the region later */
454                         }
455                         
456                         /* start a new rec box */
457
458                         boost::shared_ptr<MidiTrack> mt = _trackview.midi_track(); /* we know what it is already */
459                         boost::shared_ptr<MidiDiskstream> ds = mt->midi_diskstream();
460                         jack_nframes_t frame_pos = ds->current_capture_start ();
461                         gdouble xstart = _trackview.editor().frame_to_pixel (frame_pos);
462                         gdouble xend;
463                         uint32_t fill_color;
464
465                         assert(_trackview.midi_track()->mode() == Normal);
466                         
467                         xend = xstart;
468                         fill_color = ARDOUR_UI::config()->canvasvar_RecordingRect.get();
469                         
470                         ArdourCanvas::SimpleRect * rec_rect = new Gnome::Canvas::SimpleRect (*canvas_group);
471                         rec_rect->property_x1() = xstart;
472                         rec_rect->property_y1() = 1.0;
473                         rec_rect->property_x2() = xend;
474                         rec_rect->property_y2() = (double) _trackview.current_height() - 1;
475                         rec_rect->property_outline_color_rgba() = ARDOUR_UI::config()->canvasvar_RecordingRect.get();
476                         rec_rect->property_fill_color_rgba() = fill_color;
477                         rec_rect->lower_to_bottom();
478                         
479                         RecBoxInfo recbox;
480                         recbox.rectangle = rec_rect;
481                         recbox.start = _trackview.session().transport_frame();
482                         recbox.length = 0;
483                         
484                         rec_rects.push_back (recbox);
485                         
486                         screen_update_connection.disconnect();
487                         screen_update_connection = ARDOUR_UI::instance()->SuperRapidScreenUpdate.connect (mem_fun (*this, &MidiStreamView::update_rec_box));    
488                         rec_updating = true;
489                         rec_active = true;
490
491                 } else if (rec_active &&
492                            (_trackview.session().record_status() != Session::Recording ||
493                             !_trackview.get_diskstream()->record_enabled())) {
494
495                         screen_update_connection.disconnect();
496                         rec_active = false;
497                         rec_updating = false;
498
499                 }
500                 
501         } else {
502
503                 // cerr << "\tNOT rolling, rec_rects = " << rec_rects.size() << " rec_regions = " << rec_regions.size() << endl;
504
505                 if (!rec_rects.empty() || !rec_regions.empty()) {
506
507                         /* disconnect rapid update */
508                         screen_update_connection.disconnect();
509
510                         for (list<sigc::connection>::iterator prc = rec_data_ready_connections.begin(); prc != rec_data_ready_connections.end(); ++prc) {
511                                 (*prc).disconnect();
512                         }
513                         rec_data_ready_connections.clear();
514
515                         rec_updating = false;
516                         rec_active = false;
517                         
518                         /* remove temp regions */
519                         
520                         for (list<pair<boost::shared_ptr<Region>,RegionView*> >::iterator iter = rec_regions.begin(); iter != rec_regions.end();) {
521                                 list<pair<boost::shared_ptr<Region>,RegionView*> >::iterator tmp;
522                                 
523                                 tmp = iter;
524                                 ++tmp;
525
526                                 (*iter).first->drop_references ();
527
528                                 iter = tmp;
529                         }
530                         
531                         rec_regions.clear();
532
533                         // cerr << "\tclear " << rec_rects.size() << " rec rects\n";
534
535                         /* transport stopped, clear boxes */
536                         for (vector<RecBoxInfo>::iterator iter=rec_rects.begin(); iter != rec_rects.end(); ++iter) {
537                                 RecBoxInfo &rect = (*iter);
538                                 delete rect.rectangle;
539                         }
540                         
541                         rec_rects.clear();
542                         
543                 }
544         }
545 }
546
547 void
548 MidiStreamView::update_rec_regions (boost::shared_ptr<MidiModel> data, nframes_t start, nframes_t dur)
549 {
550         ENSURE_GUI_THREAD (bind (mem_fun (*this, &MidiStreamView::update_rec_regions), data, start, dur));
551
552         if (use_rec_regions) {
553
554                 uint32_t n = 0;
555                 bool     update_range = false;
556
557                 for (list<pair<boost::shared_ptr<Region>,RegionView*> >::iterator iter = rec_regions.begin(); iter != rec_regions.end(); n++) {
558
559                         list<pair<boost::shared_ptr<Region>,RegionView*> >::iterator tmp;
560
561                         tmp = iter;
562                         ++tmp;
563                         
564                         boost::shared_ptr<MidiRegion> region = boost::dynamic_pointer_cast<MidiRegion>(iter->first);
565                         if (!region || !iter->second) {
566                                 iter = tmp;
567                                 continue;
568                         }
569                         
570                         if (!canvas_item_visible (rec_rects[n].rectangle)) {
571                                 /* rect already hidden, this region is done */
572                                 iter = tmp;
573                                 continue;
574                         }
575                         
576                         nframes_t origlen = region->length();
577                         
578                         if (region == rec_regions.back().first && rec_active) {
579
580                                 if (start >= region->midi_source(0)->timeline_position()) {
581                                 
582                                         nframes_t nlen = start + dur - region->position();
583
584                                         if (nlen != region->length()) {
585                                         
586                                                 region->freeze ();
587                                                 region->set_position (_trackview.get_diskstream()->get_capture_start_frame(n), this);
588                                                 region->set_length (start + dur - region->position(), this);
589                                                 region->thaw ("updated");
590                                                 
591                                                 if (origlen == 1) {
592                                                         /* our special initial length */
593                                                         iter->second = add_region_view_internal (region, false);
594                                                         ((MidiRegionView*)iter->second)->begin_write();
595                                                 }
596
597                                                 /* also update rect */
598                                                 ArdourCanvas::SimpleRect * rect = rec_rects[n].rectangle;
599                                                 gdouble xend = _trackview.editor().frame_to_pixel (region->position() + region->length());
600                                                 rect->property_x2() = xend;
601
602                                                 /* draw events */
603                                                 MidiRegionView* mrv = (MidiRegionView*)iter->second;
604                                                 for (size_t i=0; i < data->n_notes(); ++i) {
605
606                                                         // FIXME: slooooooooow!
607
608                                                         const boost::shared_ptr<Evoral::Note> note = data->note_at(i);
609                                                         
610                                                         if (note->length() > 0 && note->end_time() + region->position() > start)
611                                                                 mrv->resolve_note(note->note(), note->end_time());
612
613                                                         if (note->time() + region->position() < start)
614                                                                 continue;
615
616                                                         if (note->time() + region->position() > start + dur)
617                                                                 break;
618
619                                                         mrv->add_note(note);
620
621                                                         if (note->note() < _lowest_note) {
622                                                                 _lowest_note = note->note();
623                                                                 update_range = true;
624                                                         } else if (note->note() > _highest_note) {
625                                                                 _highest_note = note->note();
626                                                                 update_range = true;
627                                                         }
628                                                 }
629                                                 
630                                                 mrv->extend_active_notes();
631                                         }
632                                 }
633
634                         } else {
635                                 
636                                 nframes_t nlen = _trackview.get_diskstream()->get_captured_frames(n);
637
638                                 if (nlen != region->length()) {
639
640                                         if (region->source(0)->length() >= region->position() + nlen) {
641
642                                                 region->freeze ();
643                                                 region->set_position (_trackview.get_diskstream()->get_capture_start_frame(n), this);
644                                                 region->set_length (nlen, this);
645                                                 region->thaw ("updated");
646                                                 
647                                                 if (origlen == 1) {
648                                                         /* our special initial length */
649                                                         iter->second = add_region_view_internal (region, false);
650                                                 }
651                                                 
652                                                 /* also hide rect */
653                                                 ArdourCanvas::Item * rect = rec_rects[n].rectangle;
654                                                 rect->hide();
655
656                                         }
657                                 }
658                         }
659
660                         iter = tmp;
661                 }
662
663                 if (update_range)
664                         update_contents_height();
665         }
666 }
667
668 void
669 MidiStreamView::rec_data_range_ready (jack_nframes_t start, jack_nframes_t dur, boost::weak_ptr<Source> weak_src)
670 {
671         // this is called from the butler thread for now
672         
673         ENSURE_GUI_THREAD(bind (mem_fun (*this, &MidiStreamView::rec_data_range_ready), start, dur, weak_src));
674         
675         boost::shared_ptr<SMFSource> src (boost::dynamic_pointer_cast<SMFSource>(weak_src.lock()));
676         
677         this->update_rec_regions (src->model(), start, dur);
678 }
679
680 void
681 MidiStreamView::color_handler ()
682 {
683         //case cMidiTrackBase:
684         if (_trackview.is_midi_track()) {
685                 //canvas_rect->property_fill_color_rgba() = ARDOUR_UI::config()->canvasvar_MidiTrackBase.get();
686         } 
687
688         //case cMidiBusBase:
689         if (!_trackview.is_midi_track()) {
690                 //canvas_rect->property_fill_color_rgba() = ARDOUR_UI::config()->canvasvar_MidiBusBase.get();;
691         }
692 }
693
694 void
695 MidiStreamView::note_range_adjustment_changed()
696 {
697         double sum = note_range_adjustment.get_value() + note_range_adjustment.get_page_size();
698         int lowest = (int) floor(note_range_adjustment.get_value());
699         int highest;
700
701         if (sum == _range_sum_cache) {
702                 //cerr << "cached" << endl;
703                 highest = (int) floor(sum);
704         } else {
705                 //cerr << "recalc" << endl;
706                 highest = lowest + (int) floor(note_range_adjustment.get_page_size());
707                 _range_sum_cache = sum;
708         }
709
710         if (lowest == _lowest_note && highest == _highest_note) {
711                 return;
712         }
713
714         //cerr << "note range adjustment changed: " << lowest << " " << highest << endl;
715         //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;
716
717         _lowest_note = lowest;
718         _highest_note = highest;
719         apply_note_range(lowest, highest);
720 }
721