Paul Davis
4abbabdcf9
commit fdbae82077db53add90df7448a06869dac89acc6 Author: Paul Davis <paul@linuxaudiosystems.com> Date: Wed Mar 27 21:45:28 2013 -0400 mammoth changes in basic signal flow, total redesign of MIDI channel filtering and more. commit 59343a8283698e02bc0f622313b29e98f449e4c8 Author: Paul Davis <paul@linuxaudiosystems.com> Date: Wed Mar 27 01:58:53 2013 -0400 initial working version after changes to MIDI channel filtering. may affect metering input too. testing not yet finished this commit merges many deep changes in ardour's internal architecture, combined with a total redesign of how MIDI channel filtering works. data in a track used to flow from JACK port buffers to diskstream's ringbuffers and was then copied from the ringbuffers into a BufferSet for use during Route::process_output_buffers(). The butler thread would handle the movement of data between the ringbuffers and disk. with this commit, data now flows from JACK port buffers into the BufferSet used for Route processing, and is copied from the BufferSet into the diskstream's ringbuffers (the butler thread continues to handle interactions with disk as usual). this change allowed a dramatic consolidation of code and simplification of most aspects of Track/Route::roll() and Track/Route::no_roll(). in particular, see Route::fill_buffers_with_input() which now concisely describes how we move data from JACK port buffers into the BufferSet for all Route types (including Tracks). this work was initially motivated by changing MIDI channel filtering so that we can process capture and playback independently. there is now a very clean pathway for this - see MidiTrack::roll() (NOTE: This needs implementing in the no-roll case too - a TODO item). the channel selector for MIDI tracks has been moved out of the track header and is now accessible via the context menu. more work is likely here, to make it (more) obvious to the user when filtering is going on.
3888 lines
98 KiB
C++
3888 lines
98 KiB
C++
/*
|
|
Copyright (C) 2001-2011 Paul Davis
|
|
Author: David Robillard
|
|
|
|
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., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
*/
|
|
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
#include <ostream>
|
|
|
|
#include <gtkmm.h>
|
|
|
|
#include "gtkmm2ext/gtk_ui.h"
|
|
|
|
#include <sigc++/signal.h>
|
|
|
|
#include "pbd/memento_command.h"
|
|
#include "pbd/stateful_diff_command.h"
|
|
|
|
#include "ardour/midi_model.h"
|
|
#include "ardour/midi_patch_manager.h"
|
|
#include "ardour/midi_region.h"
|
|
#include "ardour/midi_source.h"
|
|
#include "ardour/midi_track.h"
|
|
#include "ardour/session.h"
|
|
|
|
#include "evoral/Parameter.hpp"
|
|
#include "evoral/MIDIParameters.hpp"
|
|
#include "evoral/MIDIEvent.hpp"
|
|
#include "evoral/Control.hpp"
|
|
#include "evoral/midi_util.h"
|
|
|
|
#include "automation_region_view.h"
|
|
#include "automation_time_axis.h"
|
|
#include "canvas-hit.h"
|
|
#include "canvas-note.h"
|
|
#include "canvas_patch_change.h"
|
|
#include "canvas-sysex.h"
|
|
#include "debug.h"
|
|
#include "editor.h"
|
|
#include "editor_drag.h"
|
|
#include "ghostregion.h"
|
|
#include "gui_thread.h"
|
|
#include "keyboard.h"
|
|
#include "midi_channel_dialog.h"
|
|
#include "midi_cut_buffer.h"
|
|
#include "midi_list_editor.h"
|
|
#include "midi_region_view.h"
|
|
#include "midi_streamview.h"
|
|
#include "midi_time_axis.h"
|
|
#include "midi_util.h"
|
|
#include "midi_velocity_dialog.h"
|
|
#include "mouse_cursors.h"
|
|
#include "note_player.h"
|
|
#include "public_editor.h"
|
|
#include "route_time_axis.h"
|
|
#include "rgb_macros.h"
|
|
#include "selection.h"
|
|
#include "simpleline.h"
|
|
#include "streamview.h"
|
|
#include "utils.h"
|
|
#include "patch_change_dialog.h"
|
|
#include "verbose_cursor.h"
|
|
|
|
#include "i18n.h"
|
|
|
|
using namespace ARDOUR;
|
|
using namespace PBD;
|
|
using namespace Editing;
|
|
using namespace ArdourCanvas;
|
|
using Gtkmm2ext::Keyboard;
|
|
|
|
PBD::Signal1<void, MidiRegionView *> MidiRegionView::SelectionCleared;
|
|
|
|
#define MIDI_BP_ZERO ((Config->get_first_midi_bank_is_zero())?0:1)
|
|
|
|
MidiRegionView::MidiRegionView (ArdourCanvas::Group *parent, RouteTimeAxisView &tv,
|
|
boost::shared_ptr<MidiRegion> r, double spu, Gdk::Color const & basic_color)
|
|
: RegionView (parent, tv, r, spu, basic_color)
|
|
, _current_range_min(0)
|
|
, _current_range_max(0)
|
|
, _active_notes(0)
|
|
, _note_group(new ArdourCanvas::Group(*group))
|
|
, _note_diff_command (0)
|
|
, _ghost_note(0)
|
|
, _step_edit_cursor (0)
|
|
, _step_edit_cursor_width (1.0)
|
|
, _step_edit_cursor_position (0.0)
|
|
, _channel_selection_scoped_note (0)
|
|
, _temporary_note_group (0)
|
|
, _mouse_state(None)
|
|
, _pressed_button(0)
|
|
, _sort_needed (true)
|
|
, _optimization_iterator (_events.end())
|
|
, _list_editor (0)
|
|
, _no_sound_notes (false)
|
|
, _last_event_x (0)
|
|
, _last_event_y (0)
|
|
, pre_enter_cursor (0)
|
|
, pre_press_cursor (0)
|
|
, _note_player (0)
|
|
{
|
|
_note_group->raise_to_top();
|
|
PublicEditor::DropDownKeys.connect (sigc::mem_fun (*this, &MidiRegionView::drop_down_keys));
|
|
|
|
Config->ParameterChanged.connect (*this, invalidator (*this), boost::bind (&MidiRegionView::parameter_changed, this, _1), gui_context());
|
|
connect_to_diskstream ();
|
|
|
|
SelectionCleared.connect (_selection_cleared_connection, invalidator (*this), boost::bind (&MidiRegionView::selection_cleared, this, _1), gui_context ());
|
|
}
|
|
|
|
MidiRegionView::MidiRegionView (ArdourCanvas::Group *parent, RouteTimeAxisView &tv,
|
|
boost::shared_ptr<MidiRegion> r, double spu, Gdk::Color& basic_color,
|
|
TimeAxisViewItem::Visibility visibility)
|
|
: RegionView (parent, tv, r, spu, basic_color, false, visibility)
|
|
, _current_range_min(0)
|
|
, _current_range_max(0)
|
|
, _active_notes(0)
|
|
, _note_group(new ArdourCanvas::Group(*parent))
|
|
, _note_diff_command (0)
|
|
, _ghost_note(0)
|
|
, _step_edit_cursor (0)
|
|
, _step_edit_cursor_width (1.0)
|
|
, _step_edit_cursor_position (0.0)
|
|
, _channel_selection_scoped_note (0)
|
|
, _temporary_note_group (0)
|
|
, _mouse_state(None)
|
|
, _pressed_button(0)
|
|
, _sort_needed (true)
|
|
, _optimization_iterator (_events.end())
|
|
, _list_editor (0)
|
|
, _no_sound_notes (false)
|
|
, _last_event_x (0)
|
|
, _last_event_y (0)
|
|
, pre_enter_cursor (0)
|
|
, pre_press_cursor (0)
|
|
, _note_player (0)
|
|
{
|
|
_note_group->raise_to_top();
|
|
PublicEditor::DropDownKeys.connect (sigc::mem_fun (*this, &MidiRegionView::drop_down_keys));
|
|
|
|
connect_to_diskstream ();
|
|
|
|
SelectionCleared.connect (_selection_cleared_connection, invalidator (*this), boost::bind (&MidiRegionView::selection_cleared, this, _1), gui_context ());
|
|
}
|
|
|
|
void
|
|
MidiRegionView::parameter_changed (std::string const & p)
|
|
{
|
|
if (p == "diplay-first-midi-bank-as-zero") {
|
|
if (_enable_display) {
|
|
redisplay_model();
|
|
}
|
|
}
|
|
}
|
|
|
|
MidiRegionView::MidiRegionView (const MidiRegionView& other)
|
|
: sigc::trackable(other)
|
|
, RegionView (other)
|
|
, _current_range_min(0)
|
|
, _current_range_max(0)
|
|
, _active_notes(0)
|
|
, _note_group(new ArdourCanvas::Group(*get_canvas_group()))
|
|
, _note_diff_command (0)
|
|
, _ghost_note(0)
|
|
, _step_edit_cursor (0)
|
|
, _step_edit_cursor_width (1.0)
|
|
, _step_edit_cursor_position (0.0)
|
|
, _channel_selection_scoped_note (0)
|
|
, _temporary_note_group (0)
|
|
, _mouse_state(None)
|
|
, _pressed_button(0)
|
|
, _sort_needed (true)
|
|
, _optimization_iterator (_events.end())
|
|
, _list_editor (0)
|
|
, _no_sound_notes (false)
|
|
, _last_event_x (0)
|
|
, _last_event_y (0)
|
|
, pre_enter_cursor (0)
|
|
, pre_press_cursor (0)
|
|
, _note_player (0)
|
|
{
|
|
Gdk::Color c;
|
|
int r,g,b,a;
|
|
|
|
UINT_TO_RGBA (other.fill_color, &r, &g, &b, &a);
|
|
c.set_rgb_p (r/255.0, g/255.0, b/255.0);
|
|
|
|
init (c, false);
|
|
}
|
|
|
|
MidiRegionView::MidiRegionView (const MidiRegionView& other, boost::shared_ptr<MidiRegion> region)
|
|
: RegionView (other, boost::shared_ptr<Region> (region))
|
|
, _current_range_min(0)
|
|
, _current_range_max(0)
|
|
, _active_notes(0)
|
|
, _note_group(new ArdourCanvas::Group(*get_canvas_group()))
|
|
, _note_diff_command (0)
|
|
, _ghost_note(0)
|
|
, _step_edit_cursor (0)
|
|
, _step_edit_cursor_width (1.0)
|
|
, _step_edit_cursor_position (0.0)
|
|
, _channel_selection_scoped_note (0)
|
|
, _temporary_note_group (0)
|
|
, _mouse_state(None)
|
|
, _pressed_button(0)
|
|
, _sort_needed (true)
|
|
, _optimization_iterator (_events.end())
|
|
, _list_editor (0)
|
|
, _no_sound_notes (false)
|
|
, _last_event_x (0)
|
|
, _last_event_y (0)
|
|
, pre_enter_cursor (0)
|
|
, pre_press_cursor (0)
|
|
, _note_player (0)
|
|
{
|
|
Gdk::Color c;
|
|
int r,g,b,a;
|
|
|
|
UINT_TO_RGBA (other.fill_color, &r, &g, &b, &a);
|
|
c.set_rgb_p (r/255.0, g/255.0, b/255.0);
|
|
|
|
init (c, true);
|
|
}
|
|
|
|
void
|
|
MidiRegionView::init (Gdk::Color const & basic_color, bool wfd)
|
|
{
|
|
PublicEditor::DropDownKeys.connect (sigc::mem_fun (*this, &MidiRegionView::drop_down_keys));
|
|
|
|
CanvasNoteEvent::CanvasNoteEventDeleted.connect (note_delete_connection, MISSING_INVALIDATOR,
|
|
boost::bind (&MidiRegionView::maybe_remove_deleted_note_from_selection, this, _1),
|
|
gui_context());
|
|
|
|
if (wfd) {
|
|
midi_region()->midi_source(0)->load_model();
|
|
}
|
|
|
|
_model = midi_region()->midi_source(0)->model();
|
|
_enable_display = false;
|
|
|
|
RegionView::init (basic_color, false);
|
|
|
|
compute_colors (basic_color);
|
|
|
|
set_height (trackview.current_height());
|
|
|
|
region_muted ();
|
|
region_sync_changed ();
|
|
region_resized (ARDOUR::bounds_change);
|
|
region_locked ();
|
|
|
|
set_colors ();
|
|
|
|
_enable_display = true;
|
|
if (_model) {
|
|
if (wfd) {
|
|
display_model (_model);
|
|
}
|
|
}
|
|
|
|
reset_width_dependent_items (_pixel_width);
|
|
|
|
group->raise_to_top();
|
|
group->signal_event().connect (sigc::mem_fun (this, &MidiRegionView::canvas_event), false);
|
|
|
|
|
|
midi_view()->midi_track()->PlaybackChannelModeChanged.connect (_channel_mode_changed_connection, invalidator (*this),
|
|
boost::bind (&MidiRegionView::midi_channel_mode_changed, this),
|
|
gui_context ());
|
|
|
|
instrument_info().Changed.connect (_instrument_changed_connection, invalidator (*this),
|
|
boost::bind (&MidiRegionView::instrument_settings_changed, this), gui_context());
|
|
|
|
trackview.editor().SnapChanged.connect(snap_changed_connection, invalidator(*this),
|
|
boost::bind (&MidiRegionView::snap_changed, this),
|
|
gui_context());
|
|
|
|
Config->ParameterChanged.connect (*this, invalidator (*this), boost::bind (&MidiRegionView::parameter_changed, this, _1), gui_context());
|
|
connect_to_diskstream ();
|
|
|
|
SelectionCleared.connect (_selection_cleared_connection, invalidator (*this), boost::bind (&MidiRegionView::selection_cleared, this, _1), gui_context ());
|
|
}
|
|
|
|
InstrumentInfo&
|
|
MidiRegionView::instrument_info () const
|
|
{
|
|
RouteUI* route_ui = dynamic_cast<RouteUI*> (&trackview);
|
|
return route_ui->route()->instrument_info();
|
|
}
|
|
|
|
const boost::shared_ptr<ARDOUR::MidiRegion>
|
|
MidiRegionView::midi_region() const
|
|
{
|
|
return boost::dynamic_pointer_cast<ARDOUR::MidiRegion>(_region);
|
|
}
|
|
|
|
void
|
|
MidiRegionView::connect_to_diskstream ()
|
|
{
|
|
midi_view()->midi_track()->DataRecorded.connect(
|
|
*this, invalidator(*this),
|
|
boost::bind (&MidiRegionView::data_recorded, this, _1),
|
|
gui_context());
|
|
}
|
|
|
|
bool
|
|
MidiRegionView::canvas_event(GdkEvent* ev)
|
|
{
|
|
bool r;
|
|
|
|
switch (ev->type) {
|
|
case GDK_ENTER_NOTIFY:
|
|
case GDK_LEAVE_NOTIFY:
|
|
_last_event_x = ev->crossing.x;
|
|
_last_event_y = ev->crossing.y;
|
|
break;
|
|
case GDK_MOTION_NOTIFY:
|
|
_last_event_x = ev->motion.x;
|
|
_last_event_y = ev->motion.y;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (ev->type == GDK_2BUTTON_PRESS) {
|
|
// cannot use double-click to exit internal mode if single-click is being used
|
|
MouseMode m = trackview.editor().current_mouse_mode();
|
|
|
|
if ((m != MouseObject || !Keyboard::modifier_state_contains (ev->button.state, Keyboard::insert_note_modifier())) && (m != MouseDraw)) {
|
|
return trackview.editor().toggle_internal_editing_from_double_click (ev);
|
|
}
|
|
}
|
|
|
|
if ((!trackview.editor().internal_editing() && trackview.editor().current_mouse_mode() != MouseGain) ||
|
|
(trackview.editor().current_mouse_mode() == MouseTimeFX) ||
|
|
(trackview.editor().current_mouse_mode() == MouseZoom)) {
|
|
// handle non-draw modes elsewhere
|
|
return false;
|
|
}
|
|
|
|
switch (ev->type) {
|
|
case GDK_SCROLL:
|
|
return scroll (&ev->scroll);
|
|
|
|
case GDK_KEY_PRESS:
|
|
return key_press (&ev->key);
|
|
|
|
case GDK_KEY_RELEASE:
|
|
return key_release (&ev->key);
|
|
|
|
case GDK_BUTTON_PRESS:
|
|
return button_press (&ev->button);
|
|
|
|
case GDK_BUTTON_RELEASE:
|
|
r = button_release (&ev->button);
|
|
delete _note_player;
|
|
_note_player = 0;
|
|
return r;
|
|
|
|
case GDK_ENTER_NOTIFY:
|
|
return enter_notify (&ev->crossing);
|
|
|
|
case GDK_LEAVE_NOTIFY:
|
|
return leave_notify (&ev->crossing);
|
|
|
|
case GDK_MOTION_NOTIFY:
|
|
return motion (&ev->motion);
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void
|
|
MidiRegionView::remove_ghost_note ()
|
|
{
|
|
delete _ghost_note;
|
|
_ghost_note = 0;
|
|
}
|
|
|
|
bool
|
|
MidiRegionView::enter_notify (GdkEventCrossing* ev)
|
|
{
|
|
trackview.editor().MouseModeChanged.connect (
|
|
_mouse_mode_connection, invalidator (*this), boost::bind (&MidiRegionView::mouse_mode_changed, this), gui_context ()
|
|
);
|
|
|
|
if (trackview.editor().current_mouse_mode() == MouseDraw && _mouse_state != AddDragging) {
|
|
create_ghost_note (ev->x, ev->y);
|
|
}
|
|
|
|
if (!trackview.editor().internal_editing()) {
|
|
Keyboard::magic_widget_drop_focus();
|
|
} else {
|
|
Keyboard::magic_widget_grab_focus();
|
|
group->grab_focus();
|
|
}
|
|
|
|
// if current operation is non-operational in a midi region, change the cursor to so indicate
|
|
if (trackview.editor().current_mouse_mode() == MouseGain) {
|
|
Editor* editor = dynamic_cast<Editor *> (&trackview.editor());
|
|
pre_enter_cursor = editor->get_canvas_cursor();
|
|
editor->set_canvas_cursor(editor->cursors()->timebar);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
MidiRegionView::leave_notify (GdkEventCrossing*)
|
|
{
|
|
_mouse_mode_connection.disconnect ();
|
|
|
|
trackview.editor().verbose_cursor()->hide ();
|
|
remove_ghost_note ();
|
|
|
|
if (pre_enter_cursor) {
|
|
Editor* editor = dynamic_cast<Editor *> (&trackview.editor());
|
|
editor->set_canvas_cursor(pre_enter_cursor);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void
|
|
MidiRegionView::mouse_mode_changed ()
|
|
{
|
|
if (trackview.editor().current_mouse_mode() == MouseDraw && trackview.editor().internal_editing()) {
|
|
create_ghost_note (_last_event_x, _last_event_y);
|
|
} else {
|
|
remove_ghost_note ();
|
|
trackview.editor().verbose_cursor()->hide ();
|
|
}
|
|
|
|
if (!trackview.editor().internal_editing()) {
|
|
Keyboard::magic_widget_drop_focus();
|
|
} else {
|
|
Keyboard::magic_widget_grab_focus();
|
|
group->grab_focus();
|
|
}
|
|
}
|
|
|
|
bool
|
|
MidiRegionView::button_press (GdkEventButton* ev)
|
|
{
|
|
if (ev->button != 1) {
|
|
return false;
|
|
}
|
|
|
|
Editor* editor = dynamic_cast<Editor *> (&trackview.editor());
|
|
MouseMode m = editor->current_mouse_mode();
|
|
|
|
if (m == MouseObject && Keyboard::modifier_state_contains (ev->state, Keyboard::insert_note_modifier())) {
|
|
pre_press_cursor = editor->get_canvas_cursor ();
|
|
editor->set_canvas_cursor (editor->cursors()->midi_pencil);
|
|
}
|
|
|
|
if (_mouse_state != SelectTouchDragging) {
|
|
|
|
_pressed_button = ev->button;
|
|
_mouse_state = Pressed;
|
|
|
|
return true;
|
|
}
|
|
|
|
_pressed_button = ev->button;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
MidiRegionView::button_release (GdkEventButton* ev)
|
|
{
|
|
double event_x, event_y;
|
|
|
|
if (ev->button != 1) {
|
|
return false;
|
|
}
|
|
|
|
event_x = ev->x;
|
|
event_y = ev->y;
|
|
|
|
group->w2i(event_x, event_y);
|
|
group->ungrab(ev->time);
|
|
|
|
PublicEditor& editor = trackview.editor ();
|
|
|
|
if (pre_press_cursor) {
|
|
dynamic_cast<Editor*>(&editor)->set_canvas_cursor (pre_press_cursor, false);
|
|
pre_press_cursor = 0;
|
|
}
|
|
|
|
switch (_mouse_state) {
|
|
case Pressed: // Clicked
|
|
|
|
switch (editor.current_mouse_mode()) {
|
|
case MouseRange:
|
|
/* no motion occured - simple click */
|
|
clear_selection ();
|
|
break;
|
|
|
|
case MouseObject:
|
|
case MouseTimeFX:
|
|
{
|
|
clear_selection();
|
|
|
|
if (Keyboard::is_insert_note_event(ev)) {
|
|
|
|
double event_x, event_y;
|
|
|
|
event_x = ev->x;
|
|
event_y = ev->y;
|
|
group->w2i(event_x, event_y);
|
|
|
|
bool success;
|
|
Evoral::MusicalTime beats = editor.get_grid_type_as_beats (success, editor.pixel_to_frame (event_x));
|
|
|
|
if (!success) {
|
|
beats = 1;
|
|
}
|
|
|
|
/* Shorten the length by 1 tick so that we can add a new note at the next
|
|
grid snap without it overlapping this one.
|
|
*/
|
|
beats -= 1.0 / Timecode::BBT_Time::ticks_per_beat;
|
|
|
|
create_note_at (editor.pixel_to_frame (event_x), event_y, beats, true);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case MouseDraw:
|
|
{
|
|
bool success;
|
|
Evoral::MusicalTime beats = editor.get_grid_type_as_beats (success, editor.pixel_to_frame (event_x));
|
|
|
|
if (!success) {
|
|
beats = 1;
|
|
}
|
|
|
|
/* Shorten the length by 1 tick so that we can add a new note at the next
|
|
grid snap without it overlapping this one.
|
|
*/
|
|
beats -= 1.0 / Timecode::BBT_Time::ticks_per_beat;
|
|
|
|
create_note_at (editor.pixel_to_frame (event_x), event_y, beats, true);
|
|
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
_mouse_state = None;
|
|
break;
|
|
|
|
case SelectRectDragging:
|
|
case AddDragging:
|
|
editor.drags()->end_grab ((GdkEvent *) ev);
|
|
_mouse_state = None;
|
|
create_ghost_note (ev->x, ev->y);
|
|
break;
|
|
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
MidiRegionView::motion (GdkEventMotion* ev)
|
|
{
|
|
PublicEditor& editor = trackview.editor ();
|
|
|
|
if (!_ghost_note && editor.current_mouse_mode() == MouseObject &&
|
|
Keyboard::modifier_state_contains (ev->state, Keyboard::insert_note_modifier()) &&
|
|
_mouse_state != AddDragging) {
|
|
|
|
create_ghost_note (ev->x, ev->y);
|
|
|
|
} else if (_ghost_note && editor.current_mouse_mode() == MouseObject &&
|
|
Keyboard::modifier_state_contains (ev->state, Keyboard::insert_note_modifier())) {
|
|
|
|
update_ghost_note (ev->x, ev->y);
|
|
|
|
} else if (_ghost_note && editor.current_mouse_mode() == MouseObject) {
|
|
|
|
remove_ghost_note ();
|
|
editor.verbose_cursor()->hide ();
|
|
|
|
} else if (_ghost_note && editor.current_mouse_mode() == MouseDraw) {
|
|
|
|
update_ghost_note (ev->x, ev->y);
|
|
}
|
|
|
|
/* any motion immediately hides velocity text that may have been visible */
|
|
|
|
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
|
|
(*i)->hide_velocity ();
|
|
}
|
|
|
|
switch (_mouse_state) {
|
|
case Pressed:
|
|
|
|
if (_pressed_button == 1) {
|
|
|
|
MouseMode m = editor.current_mouse_mode();
|
|
|
|
if (m == MouseDraw || (m == MouseObject && Keyboard::modifier_state_contains (ev->state, Keyboard::insert_note_modifier()))) {
|
|
|
|
editor.drags()->set (new NoteCreateDrag (dynamic_cast<Editor *> (&editor), group, this), (GdkEvent *) ev);
|
|
_mouse_state = AddDragging;
|
|
remove_ghost_note ();
|
|
editor.verbose_cursor()->hide ();
|
|
return true;
|
|
} else if (m == MouseObject) {
|
|
editor.drags()->set (new MidiRubberbandSelectDrag (dynamic_cast<Editor *> (&editor), this), (GdkEvent *) ev);
|
|
clear_selection ();
|
|
_mouse_state = SelectRectDragging;
|
|
return true;
|
|
} else if (m == MouseRange) {
|
|
editor.drags()->set (new MidiVerticalSelectDrag (dynamic_cast<Editor *> (&editor), this), (GdkEvent *) ev);
|
|
_mouse_state = SelectVerticalDragging;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
|
|
case SelectRectDragging:
|
|
case SelectVerticalDragging:
|
|
case AddDragging:
|
|
editor.drags()->motion_handler ((GdkEvent *) ev, false);
|
|
break;
|
|
|
|
case SelectTouchDragging:
|
|
return false;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
bool
|
|
MidiRegionView::scroll (GdkEventScroll* ev)
|
|
{
|
|
if (_selection.empty()) {
|
|
return false;
|
|
}
|
|
|
|
if (Keyboard::modifier_state_equals (ev->state, Keyboard::PrimaryModifier)) {
|
|
/* XXX: bit of a hack; allow PrimaryModifier scroll through so that
|
|
it still works for zoom.
|
|
*/
|
|
return false;
|
|
}
|
|
|
|
trackview.editor().verbose_cursor()->hide ();
|
|
|
|
bool fine = !Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier);
|
|
bool together = Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier);
|
|
|
|
if (ev->direction == GDK_SCROLL_UP) {
|
|
change_velocities (true, fine, false, together);
|
|
} else if (ev->direction == GDK_SCROLL_DOWN) {
|
|
change_velocities (false, fine, false, together);
|
|
} else {
|
|
/* left, right: we don't use them */
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
MidiRegionView::key_press (GdkEventKey* ev)
|
|
{
|
|
/* since GTK bindings are generally activated on press, and since
|
|
detectable auto-repeat is the name of the game and only sends
|
|
repeated presses, carry out key actions at key press, not release.
|
|
*/
|
|
|
|
bool unmodified = Keyboard::no_modifier_keys_pressed (ev);
|
|
|
|
if (unmodified && (ev->keyval == GDK_Alt_L || ev->keyval == GDK_Alt_R)) {
|
|
_mouse_state = SelectTouchDragging;
|
|
return true;
|
|
|
|
} else if (ev->keyval == GDK_Escape && unmodified) {
|
|
clear_selection();
|
|
_mouse_state = None;
|
|
|
|
} else if (unmodified && (ev->keyval == GDK_comma || ev->keyval == GDK_period)) {
|
|
|
|
bool start = (ev->keyval == GDK_comma);
|
|
bool end = (ev->keyval == GDK_period);
|
|
bool shorter = Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier);
|
|
bool fine = Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier);
|
|
|
|
change_note_lengths (fine, shorter, 0.0, start, end);
|
|
|
|
return true;
|
|
|
|
} else if ((ev->keyval == GDK_BackSpace || ev->keyval == GDK_Delete) && unmodified) {
|
|
|
|
if (_selection.empty()) {
|
|
return false;
|
|
}
|
|
|
|
delete_selection();
|
|
return true;
|
|
|
|
} else if (ev->keyval == GDK_Tab) {
|
|
|
|
if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
|
|
goto_previous_note (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier));
|
|
} else {
|
|
goto_next_note (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier));
|
|
}
|
|
return true;
|
|
|
|
} else if (ev->keyval == GDK_ISO_Left_Tab) {
|
|
|
|
/* Shift-TAB generates ISO Left Tab, for some reason */
|
|
|
|
if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
|
|
goto_previous_note (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier));
|
|
} else {
|
|
goto_next_note (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier));
|
|
}
|
|
return true;
|
|
|
|
|
|
|
|
} else if (ev->keyval == GDK_Up) {
|
|
|
|
bool allow_smush = Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier);
|
|
bool fine = !Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier);
|
|
bool together = Keyboard::modifier_state_contains (ev->state, Keyboard::Level4Modifier);
|
|
|
|
if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
|
|
change_velocities (true, fine, allow_smush, together);
|
|
} else {
|
|
transpose (true, fine, allow_smush);
|
|
}
|
|
return true;
|
|
|
|
} else if (ev->keyval == GDK_Down) {
|
|
|
|
bool allow_smush = Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier);
|
|
bool fine = !Keyboard::modifier_state_contains (ev->state, Keyboard::SecondaryModifier);
|
|
bool together = Keyboard::modifier_state_contains (ev->state, Keyboard::Level4Modifier);
|
|
|
|
if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
|
|
change_velocities (false, fine, allow_smush, together);
|
|
} else {
|
|
transpose (false, fine, allow_smush);
|
|
}
|
|
return true;
|
|
|
|
} else if (ev->keyval == GDK_Left && unmodified) {
|
|
|
|
nudge_notes (false);
|
|
return true;
|
|
|
|
} else if (ev->keyval == GDK_Right && unmodified) {
|
|
|
|
nudge_notes (true);
|
|
return true;
|
|
|
|
} else if (ev->keyval == GDK_c && unmodified) {
|
|
channel_edit ();
|
|
return true;
|
|
|
|
} else if (ev->keyval == GDK_v && unmodified) {
|
|
velocity_edit ();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool
|
|
MidiRegionView::key_release (GdkEventKey* ev)
|
|
{
|
|
if ((_mouse_state == SelectTouchDragging) && (ev->keyval == GDK_Alt_L || ev->keyval == GDK_Alt_R)) {
|
|
_mouse_state = None;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void
|
|
MidiRegionView::channel_edit ()
|
|
{
|
|
if (_selection.empty()) {
|
|
return;
|
|
}
|
|
|
|
/* pick a note somewhat at random (since Selection is a set<>) to
|
|
* provide the "current" channel for the dialog.
|
|
*/
|
|
|
|
uint8_t current_channel = (*_selection.begin())->note()->channel ();
|
|
MidiChannelDialog channel_dialog (current_channel);
|
|
int ret = channel_dialog.run ();
|
|
|
|
switch (ret) {
|
|
case Gtk::RESPONSE_OK:
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
uint8_t new_channel = channel_dialog.active_channel ();
|
|
|
|
start_note_diff_command (_("channel edit"));
|
|
|
|
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ) {
|
|
Selection::iterator next = i;
|
|
++next;
|
|
change_note_channel (*i, new_channel);
|
|
i = next;
|
|
}
|
|
|
|
apply_diff ();
|
|
}
|
|
|
|
void
|
|
MidiRegionView::velocity_edit ()
|
|
{
|
|
if (_selection.empty()) {
|
|
return;
|
|
}
|
|
|
|
/* pick a note somewhat at random (since Selection is a set<>) to
|
|
* provide the "current" velocity for the dialog.
|
|
*/
|
|
|
|
uint8_t current_velocity = (*_selection.begin())->note()->velocity ();
|
|
MidiVelocityDialog velocity_dialog (current_velocity);
|
|
int ret = velocity_dialog.run ();
|
|
|
|
switch (ret) {
|
|
case Gtk::RESPONSE_OK:
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
uint8_t new_velocity = velocity_dialog.velocity ();
|
|
|
|
start_note_diff_command (_("velocity edit"));
|
|
|
|
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ) {
|
|
Selection::iterator next = i;
|
|
++next;
|
|
change_note_velocity (*i, new_velocity, false);
|
|
i = next;
|
|
}
|
|
|
|
apply_diff ();
|
|
}
|
|
|
|
void
|
|
MidiRegionView::show_list_editor ()
|
|
{
|
|
if (!_list_editor) {
|
|
_list_editor = new MidiListEditor (trackview.session(), midi_region(), midi_view()->midi_track());
|
|
}
|
|
_list_editor->present ();
|
|
}
|
|
|
|
/** Add a note to the model, and the view, at a canvas (click) coordinate.
|
|
* \param t time in frames relative to the position of the region
|
|
* \param y vertical position in pixels
|
|
* \param length duration of the note in beats
|
|
* \param snap_t true to snap t to the grid, otherwise false.
|
|
*/
|
|
void
|
|
MidiRegionView::create_note_at (framepos_t t, double y, double length, bool snap_t)
|
|
{
|
|
if (length < 2 * DBL_EPSILON) {
|
|
return;
|
|
}
|
|
|
|
MidiTimeAxisView* const mtv = dynamic_cast<MidiTimeAxisView*>(&trackview);
|
|
MidiStreamView* const view = mtv->midi_view();
|
|
|
|
const double note = view->y_to_note(y);
|
|
|
|
// Start of note in frames relative to region start
|
|
if (snap_t) {
|
|
framecnt_t grid_frames;
|
|
t = snap_frame_to_grid_underneath (t, grid_frames);
|
|
}
|
|
|
|
const boost::shared_ptr<NoteType> new_note (
|
|
new NoteType (mtv->get_channel_for_add (),
|
|
region_frames_to_region_beats(t + _region->start()),
|
|
length,
|
|
(uint8_t)note, 0x40));
|
|
|
|
if (_model->contains (new_note)) {
|
|
return;
|
|
}
|
|
|
|
view->update_note_range(new_note->note());
|
|
|
|
MidiModel::NoteDiffCommand* cmd = _model->new_note_diff_command(_("add note"));
|
|
cmd->add (new_note);
|
|
_model->apply_command(*trackview.session(), cmd);
|
|
|
|
play_midi_note (new_note);
|
|
}
|
|
|
|
void
|
|
MidiRegionView::clear_events (bool with_selection_signal)
|
|
{
|
|
clear_selection (with_selection_signal);
|
|
|
|
MidiGhostRegion* gr;
|
|
for (std::vector<GhostRegion*>::iterator g = ghosts.begin(); g != ghosts.end(); ++g) {
|
|
if ((gr = dynamic_cast<MidiGhostRegion*>(*g)) != 0) {
|
|
gr->clear_events();
|
|
}
|
|
}
|
|
|
|
for (Events::iterator i = _events.begin(); i != _events.end(); ++i) {
|
|
delete *i;
|
|
}
|
|
|
|
_events.clear();
|
|
_patch_changes.clear();
|
|
_sys_exes.clear();
|
|
_optimization_iterator = _events.end();
|
|
}
|
|
|
|
void
|
|
MidiRegionView::display_model(boost::shared_ptr<MidiModel> model)
|
|
{
|
|
_model = model;
|
|
|
|
content_connection.disconnect ();
|
|
_model->ContentsChanged.connect (content_connection, invalidator (*this), boost::bind (&MidiRegionView::redisplay_model, this), gui_context());
|
|
|
|
clear_events ();
|
|
|
|
if (_enable_display) {
|
|
redisplay_model();
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::start_note_diff_command (string name)
|
|
{
|
|
if (!_note_diff_command) {
|
|
_note_diff_command = _model->new_note_diff_command (name);
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::note_diff_add_note (const boost::shared_ptr<NoteType> note, bool selected, bool show_velocity)
|
|
{
|
|
if (_note_diff_command) {
|
|
_note_diff_command->add (note);
|
|
}
|
|
if (selected) {
|
|
_marked_for_selection.insert(note);
|
|
}
|
|
if (show_velocity) {
|
|
_marked_for_velocity.insert(note);
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::note_diff_remove_note (ArdourCanvas::CanvasNoteEvent* ev)
|
|
{
|
|
if (_note_diff_command && ev->note()) {
|
|
_note_diff_command->remove(ev->note());
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::note_diff_add_change (ArdourCanvas::CanvasNoteEvent* ev,
|
|
MidiModel::NoteDiffCommand::Property property,
|
|
uint8_t val)
|
|
{
|
|
if (_note_diff_command) {
|
|
_note_diff_command->change (ev->note(), property, val);
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::note_diff_add_change (ArdourCanvas::CanvasNoteEvent* ev,
|
|
MidiModel::NoteDiffCommand::Property property,
|
|
Evoral::MusicalTime val)
|
|
{
|
|
if (_note_diff_command) {
|
|
_note_diff_command->change (ev->note(), property, val);
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::apply_diff (bool as_subcommand)
|
|
{
|
|
bool add_or_remove;
|
|
|
|
if (!_note_diff_command) {
|
|
return;
|
|
}
|
|
|
|
if ((add_or_remove = _note_diff_command->adds_or_removes())) {
|
|
// Mark all selected notes for selection when model reloads
|
|
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
|
|
_marked_for_selection.insert((*i)->note());
|
|
}
|
|
}
|
|
|
|
if (as_subcommand) {
|
|
_model->apply_command_as_subcommand (*trackview.session(), _note_diff_command);
|
|
} else {
|
|
_model->apply_command (*trackview.session(), _note_diff_command);
|
|
}
|
|
|
|
_note_diff_command = 0;
|
|
midi_view()->midi_track()->playlist_modified();
|
|
|
|
if (add_or_remove) {
|
|
_marked_for_selection.clear();
|
|
}
|
|
|
|
_marked_for_velocity.clear();
|
|
}
|
|
|
|
void
|
|
MidiRegionView::abort_command()
|
|
{
|
|
delete _note_diff_command;
|
|
_note_diff_command = 0;
|
|
clear_selection();
|
|
}
|
|
|
|
CanvasNoteEvent*
|
|
MidiRegionView::find_canvas_note (boost::shared_ptr<NoteType> note)
|
|
{
|
|
if (_optimization_iterator != _events.end()) {
|
|
++_optimization_iterator;
|
|
}
|
|
|
|
if (_optimization_iterator != _events.end() && (*_optimization_iterator)->note() == note) {
|
|
return *_optimization_iterator;
|
|
}
|
|
|
|
for (_optimization_iterator = _events.begin(); _optimization_iterator != _events.end(); ++_optimization_iterator) {
|
|
if ((*_optimization_iterator)->note() == note) {
|
|
return *_optimization_iterator;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
MidiRegionView::get_events (Events& e, Evoral::Sequence<Evoral::MusicalTime>::NoteOperator op, uint8_t val, int chan_mask)
|
|
{
|
|
MidiModel::Notes notes;
|
|
_model->get_notes (notes, op, val, chan_mask);
|
|
|
|
for (MidiModel::Notes::iterator n = notes.begin(); n != notes.end(); ++n) {
|
|
CanvasNoteEvent* cne = find_canvas_note (*n);
|
|
if (cne) {
|
|
e.push_back (cne);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::redisplay_model()
|
|
{
|
|
// Don't redisplay the model if we're currently recording and displaying that
|
|
if (_active_notes) {
|
|
return;
|
|
}
|
|
|
|
if (!_model) {
|
|
return;
|
|
}
|
|
|
|
for (Events::iterator i = _events.begin(); i != _events.end(); ++i) {
|
|
(*i)->invalidate ();
|
|
}
|
|
|
|
MidiModel::ReadLock lock(_model->read_lock());
|
|
|
|
MidiModel::Notes& notes (_model->notes());
|
|
_optimization_iterator = _events.begin();
|
|
|
|
for (MidiModel::Notes::iterator n = notes.begin(); n != notes.end(); ++n) {
|
|
|
|
boost::shared_ptr<NoteType> note (*n);
|
|
CanvasNoteEvent* cne;
|
|
bool visible;
|
|
|
|
if (note_in_region_range (note, visible)) {
|
|
|
|
if ((cne = find_canvas_note (note)) != 0) {
|
|
|
|
cne->validate ();
|
|
|
|
CanvasNote* cn;
|
|
CanvasHit* ch;
|
|
|
|
if ((cn = dynamic_cast<CanvasNote*>(cne)) != 0) {
|
|
update_note (cn);
|
|
} else if ((ch = dynamic_cast<CanvasHit*>(cne)) != 0) {
|
|
update_hit (ch);
|
|
}
|
|
|
|
if (visible) {
|
|
cne->show ();
|
|
} else {
|
|
cne->hide ();
|
|
}
|
|
|
|
} else {
|
|
|
|
add_note (note, visible);
|
|
}
|
|
|
|
} else {
|
|
|
|
if ((cne = find_canvas_note (note)) != 0) {
|
|
cne->validate ();
|
|
cne->hide ();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/* remove note items that are no longer valid */
|
|
|
|
for (Events::iterator i = _events.begin(); i != _events.end(); ) {
|
|
if (!(*i)->valid ()) {
|
|
|
|
for (vector<GhostRegion*>::iterator j = ghosts.begin(); j != ghosts.end(); ++j) {
|
|
MidiGhostRegion* gr = dynamic_cast<MidiGhostRegion*> (*j);
|
|
if (gr) {
|
|
gr->remove_note (*i);
|
|
}
|
|
}
|
|
|
|
delete *i;
|
|
i = _events.erase (i);
|
|
|
|
} else {
|
|
++i;
|
|
}
|
|
}
|
|
|
|
_patch_changes.clear();
|
|
_sys_exes.clear();
|
|
|
|
display_sysexes();
|
|
display_patch_changes ();
|
|
|
|
_marked_for_selection.clear ();
|
|
_marked_for_velocity.clear ();
|
|
|
|
/* we may have caused _events to contain things out of order (e.g. if a note
|
|
moved earlier or later). we don't generally need them in time order, but
|
|
make a note that a sort is required for those cases that require it.
|
|
*/
|
|
|
|
_sort_needed = true;
|
|
}
|
|
|
|
void
|
|
MidiRegionView::display_patch_changes ()
|
|
{
|
|
MidiTimeAxisView* const mtv = dynamic_cast<MidiTimeAxisView*>(&trackview);
|
|
uint16_t chn_mask = mtv->midi_track()->get_playback_channel_mask();
|
|
|
|
for (uint8_t i = 0; i < 16; ++i) {
|
|
display_patch_changes_on_channel (i, chn_mask & (1 << i));
|
|
}
|
|
}
|
|
|
|
/** @param active_channel true to display patch changes fully, false to display
|
|
* them `greyed-out' (as on an inactive channel)
|
|
*/
|
|
void
|
|
MidiRegionView::display_patch_changes_on_channel (uint8_t channel, bool active_channel)
|
|
{
|
|
for (MidiModel::PatchChanges::const_iterator i = _model->patch_changes().begin(); i != _model->patch_changes().end(); ++i) {
|
|
|
|
if ((*i)->channel() != channel) {
|
|
continue;
|
|
}
|
|
|
|
const string patch_name = instrument_info().get_patch_name ((*i)->bank(), (*i)->program(), channel);
|
|
add_canvas_patch_change (*i, patch_name, active_channel);
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::display_sysexes()
|
|
{
|
|
bool have_periodic_system_messages = false;
|
|
bool display_periodic_messages = true;
|
|
|
|
if (!Config->get_never_display_periodic_midi()) {
|
|
|
|
for (MidiModel::SysExes::const_iterator i = _model->sysexes().begin(); i != _model->sysexes().end(); ++i) {
|
|
const boost::shared_ptr<const Evoral::MIDIEvent<Evoral::MusicalTime> > mev =
|
|
boost::static_pointer_cast<const Evoral::MIDIEvent<Evoral::MusicalTime> > (*i);
|
|
|
|
if (mev) {
|
|
if (mev->is_spp() || mev->is_mtc_quarter() || mev->is_mtc_full()) {
|
|
have_periodic_system_messages = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (have_periodic_system_messages) {
|
|
double zoom = trackview.editor().get_current_zoom (); // frames per pixel
|
|
|
|
/* get an approximate value for the number of samples per video frame */
|
|
|
|
double video_frame = trackview.session()->frame_rate() * (1.0/30);
|
|
|
|
/* if we are zoomed out beyond than the cutoff (i.e. more
|
|
* frames per pixel than frames per 4 video frames), don't
|
|
* show periodic sysex messages.
|
|
*/
|
|
|
|
if (zoom > (video_frame*4)) {
|
|
display_periodic_messages = false;
|
|
}
|
|
}
|
|
} else {
|
|
display_periodic_messages = false;
|
|
}
|
|
|
|
for (MidiModel::SysExes::const_iterator i = _model->sysexes().begin(); i != _model->sysexes().end(); ++i) {
|
|
|
|
const boost::shared_ptr<const Evoral::MIDIEvent<Evoral::MusicalTime> > mev =
|
|
boost::static_pointer_cast<const Evoral::MIDIEvent<Evoral::MusicalTime> > (*i);
|
|
|
|
Evoral::MusicalTime time = (*i)->time();
|
|
|
|
if (mev) {
|
|
if (mev->is_spp() || mev->is_mtc_quarter() || mev->is_mtc_full()) {
|
|
if (!display_periodic_messages) {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
ostringstream str;
|
|
str << hex;
|
|
for (uint32_t b = 0; b < (*i)->size(); ++b) {
|
|
str << int((*i)->buffer()[b]);
|
|
if (b != (*i)->size() -1) {
|
|
str << " ";
|
|
}
|
|
}
|
|
string text = str.str();
|
|
|
|
const double x = trackview.editor().frame_to_pixel(source_beats_to_region_frames(time));
|
|
|
|
double height = midi_stream_view()->contents_height();
|
|
|
|
boost::shared_ptr<CanvasSysEx> sysex = boost::shared_ptr<CanvasSysEx>(
|
|
new CanvasSysEx(*this, *_note_group, text, height, x, 1.0, (*i)));
|
|
|
|
// Show unless message is beyond the region bounds
|
|
if (time - _region->start() >= _region->length() || time < _region->start()) {
|
|
sysex->hide();
|
|
} else {
|
|
sysex->show();
|
|
}
|
|
|
|
_sys_exes.push_back(sysex);
|
|
}
|
|
}
|
|
|
|
MidiRegionView::~MidiRegionView ()
|
|
{
|
|
in_destructor = true;
|
|
|
|
trackview.editor().verbose_cursor()->hide ();
|
|
|
|
note_delete_connection.disconnect ();
|
|
|
|
delete _list_editor;
|
|
|
|
RegionViewGoingAway (this); /* EMIT_SIGNAL */
|
|
|
|
if (_active_notes) {
|
|
end_write();
|
|
}
|
|
|
|
_selection_cleared_connection.disconnect ();
|
|
|
|
_selection.clear();
|
|
clear_events (false);
|
|
|
|
delete _note_group;
|
|
delete _note_diff_command;
|
|
delete _step_edit_cursor;
|
|
delete _temporary_note_group;
|
|
}
|
|
|
|
void
|
|
MidiRegionView::region_resized (const PropertyChange& what_changed)
|
|
{
|
|
RegionView::region_resized(what_changed);
|
|
|
|
if (what_changed.contains (ARDOUR::Properties::position)) {
|
|
set_duration(_region->length(), 0);
|
|
if (_enable_display) {
|
|
redisplay_model();
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::reset_width_dependent_items (double pixel_width)
|
|
{
|
|
RegionView::reset_width_dependent_items(pixel_width);
|
|
|
|
if (_enable_display) {
|
|
redisplay_model();
|
|
}
|
|
|
|
for (PatchChanges::iterator x = _patch_changes.begin(); x != _patch_changes.end(); ++x) {
|
|
if ((*x)->width() >= _pixel_width) {
|
|
(*x)->hide();
|
|
} else {
|
|
(*x)->show();
|
|
}
|
|
}
|
|
|
|
move_step_edit_cursor (_step_edit_cursor_position);
|
|
set_step_edit_cursor_width (_step_edit_cursor_width);
|
|
}
|
|
|
|
void
|
|
MidiRegionView::set_height (double height)
|
|
{
|
|
static const double FUDGE = 2.0;
|
|
const double old_height = _height;
|
|
RegionView::set_height(height);
|
|
_height = height - FUDGE;
|
|
|
|
apply_note_range(midi_stream_view()->lowest_note(),
|
|
midi_stream_view()->highest_note(),
|
|
height != old_height + FUDGE);
|
|
|
|
if (name_pixbuf) {
|
|
name_pixbuf->raise_to_top();
|
|
}
|
|
|
|
for (PatchChanges::iterator x = _patch_changes.begin(); x != _patch_changes.end(); ++x) {
|
|
(*x)->set_height (midi_stream_view()->contents_height());
|
|
}
|
|
|
|
if (_step_edit_cursor) {
|
|
_step_edit_cursor->property_y2() = midi_stream_view()->contents_height();
|
|
}
|
|
}
|
|
|
|
|
|
/** Apply the current note range from the stream view
|
|
* by repositioning/hiding notes as necessary
|
|
*/
|
|
void
|
|
MidiRegionView::apply_note_range (uint8_t min, uint8_t max, bool force)
|
|
{
|
|
if (!_enable_display) {
|
|
return;
|
|
}
|
|
|
|
if (!force && _current_range_min == min && _current_range_max == max) {
|
|
return;
|
|
}
|
|
|
|
_current_range_min = min;
|
|
_current_range_max = max;
|
|
|
|
for (Events::const_iterator i = _events.begin(); i != _events.end(); ++i) {
|
|
CanvasNoteEvent* event = *i;
|
|
boost::shared_ptr<NoteType> note (event->note());
|
|
|
|
if (note->note() < _current_range_min ||
|
|
note->note() > _current_range_max) {
|
|
event->hide();
|
|
} else {
|
|
event->show();
|
|
}
|
|
|
|
if (CanvasNote* cnote = dynamic_cast<CanvasNote*>(event)) {
|
|
|
|
const double y1 = midi_stream_view()->note_to_y(note->note());
|
|
const double y2 = y1 + floor(midi_stream_view()->note_height());
|
|
|
|
cnote->property_y1() = y1;
|
|
cnote->property_y2() = y2;
|
|
|
|
} else if (CanvasHit* chit = dynamic_cast<CanvasHit*>(event)) {
|
|
|
|
const double diamond_size = update_hit (chit);
|
|
|
|
chit->set_height (diamond_size);
|
|
}
|
|
}
|
|
}
|
|
|
|
GhostRegion*
|
|
MidiRegionView::add_ghost (TimeAxisView& tv)
|
|
{
|
|
CanvasNote* note;
|
|
|
|
double unit_position = _region->position () / samples_per_unit;
|
|
MidiTimeAxisView* mtv = dynamic_cast<MidiTimeAxisView*>(&tv);
|
|
MidiGhostRegion* ghost;
|
|
|
|
if (mtv && mtv->midi_view()) {
|
|
/* if ghost is inserted into midi track, use a dedicated midi ghost canvas group
|
|
to allow having midi notes on top of note lines and waveforms.
|
|
*/
|
|
ghost = new MidiGhostRegion (*mtv->midi_view(), trackview, unit_position);
|
|
} else {
|
|
ghost = new MidiGhostRegion (tv, trackview, unit_position);
|
|
}
|
|
|
|
for (Events::iterator i = _events.begin(); i != _events.end(); ++i) {
|
|
if ((note = dynamic_cast<CanvasNote*>(*i)) != 0) {
|
|
ghost->add_note(note);
|
|
}
|
|
}
|
|
|
|
ghost->set_height ();
|
|
ghost->set_duration (_region->length() / samples_per_unit);
|
|
ghosts.push_back (ghost);
|
|
|
|
GhostRegion::CatchDeletion.connect (*this, invalidator (*this), boost::bind (&RegionView::remove_ghost, this, _1), gui_context());
|
|
|
|
return ghost;
|
|
}
|
|
|
|
|
|
/** Begin tracking note state for successive calls to add_event
|
|
*/
|
|
void
|
|
MidiRegionView::begin_write()
|
|
{
|
|
if (_active_notes) {
|
|
delete[] _active_notes;
|
|
}
|
|
_active_notes = new CanvasNote*[128];
|
|
for (unsigned i = 0; i < 128; ++i) {
|
|
_active_notes[i] = 0;
|
|
}
|
|
}
|
|
|
|
|
|
/** Destroy note state for add_event
|
|
*/
|
|
void
|
|
MidiRegionView::end_write()
|
|
{
|
|
delete[] _active_notes;
|
|
_active_notes = 0;
|
|
_marked_for_selection.clear();
|
|
_marked_for_velocity.clear();
|
|
}
|
|
|
|
|
|
/** Resolve an active MIDI note (while recording).
|
|
*/
|
|
void
|
|
MidiRegionView::resolve_note(uint8_t note, double end_time)
|
|
{
|
|
if (midi_view()->note_mode() != Sustained) {
|
|
return;
|
|
}
|
|
|
|
if (_active_notes && _active_notes[note]) {
|
|
|
|
/* XXX is end_time really region-centric? I think so, because
|
|
this is a new region that we're recording, so source zero is
|
|
the same as region zero
|
|
*/
|
|
const framepos_t end_time_frames = region_beats_to_region_frames(end_time);
|
|
|
|
_active_notes[note]->property_x2() = trackview.editor().frame_to_pixel(end_time_frames);
|
|
_active_notes[note]->property_outline_what() = (guint32) 0xF; // all edges
|
|
_active_notes[note] = 0;
|
|
}
|
|
}
|
|
|
|
|
|
/** Extend active notes to rightmost edge of region (if length is changed)
|
|
*/
|
|
void
|
|
MidiRegionView::extend_active_notes()
|
|
{
|
|
if (!_active_notes) {
|
|
return;
|
|
}
|
|
|
|
for (unsigned i=0; i < 128; ++i) {
|
|
if (_active_notes[i]) {
|
|
_active_notes[i]->property_x2() = trackview.editor().frame_to_pixel(_region->length());
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
MidiRegionView::play_midi_note(boost::shared_ptr<NoteType> note)
|
|
{
|
|
if (_no_sound_notes || !Config->get_sound_midi_notes()) {
|
|
return;
|
|
}
|
|
|
|
RouteUI* route_ui = dynamic_cast<RouteUI*> (&trackview);
|
|
|
|
if (!route_ui || !route_ui->midi_track()) {
|
|
return;
|
|
}
|
|
|
|
NotePlayer* np = new NotePlayer (route_ui->midi_track ());
|
|
np->add (note);
|
|
np->play ();
|
|
|
|
/* NotePlayer deletes itself */
|
|
}
|
|
|
|
void
|
|
MidiRegionView::start_playing_midi_note(boost::shared_ptr<NoteType> note)
|
|
{
|
|
if (_no_sound_notes || !Config->get_sound_midi_notes()) {
|
|
return;
|
|
}
|
|
|
|
RouteUI* route_ui = dynamic_cast<RouteUI*> (&trackview);
|
|
|
|
if (!route_ui || !route_ui->midi_track()) {
|
|
return;
|
|
}
|
|
|
|
delete _note_player;
|
|
_note_player = new NotePlayer (route_ui->midi_track ());
|
|
_note_player->add (note);
|
|
_note_player->on ();
|
|
}
|
|
|
|
void
|
|
MidiRegionView::start_playing_midi_chord (vector<boost::shared_ptr<NoteType> > notes)
|
|
{
|
|
if (_no_sound_notes || !Config->get_sound_midi_notes()) {
|
|
return;
|
|
}
|
|
|
|
RouteUI* route_ui = dynamic_cast<RouteUI*> (&trackview);
|
|
|
|
if (!route_ui || !route_ui->midi_track()) {
|
|
return;
|
|
}
|
|
|
|
delete _note_player;
|
|
_note_player = new NotePlayer (route_ui->midi_track());
|
|
|
|
for (vector<boost::shared_ptr<NoteType> >::iterator n = notes.begin(); n != notes.end(); ++n) {
|
|
_note_player->add (*n);
|
|
}
|
|
|
|
_note_player->on ();
|
|
}
|
|
|
|
|
|
bool
|
|
MidiRegionView::note_in_region_range (const boost::shared_ptr<NoteType> note, bool& visible) const
|
|
{
|
|
const framepos_t note_start_frames = source_beats_to_region_frames (note->time());
|
|
bool outside = (note_start_frames < 0) || (note_start_frames > _region->last_frame());
|
|
|
|
visible = (note->note() >= midi_stream_view()->lowest_note()) &&
|
|
(note->note() <= midi_stream_view()->highest_note());
|
|
|
|
return !outside;
|
|
}
|
|
|
|
/** Update a canvas note's size from its model note.
|
|
* @param ev Canvas note to update.
|
|
* @param update_ghost_regions true to update the note in any ghost regions that we have, otherwise false.
|
|
*/
|
|
void
|
|
MidiRegionView::update_note (CanvasNote* ev, bool update_ghost_regions)
|
|
{
|
|
boost::shared_ptr<NoteType> note = ev->note();
|
|
const double x = trackview.editor().frame_to_pixel (source_beats_to_region_frames (note->time()));
|
|
const double y1 = midi_stream_view()->note_to_y(note->note());
|
|
|
|
ev->property_x1() = x;
|
|
ev->property_y1() = y1;
|
|
|
|
/* trim note display to not overlap the end of its region */
|
|
|
|
if (note->length() > 0) {
|
|
const framepos_t note_end_frames = min (source_beats_to_region_frames (note->end_time()), _region->length());
|
|
ev->property_x2() = trackview.editor().frame_to_pixel (note_end_frames);
|
|
} else {
|
|
ev->property_x2() = trackview.editor().frame_to_pixel (_region->length());
|
|
}
|
|
|
|
ev->property_y2() = y1 + floor(midi_stream_view()->note_height());
|
|
|
|
if (note->length() == 0) {
|
|
if (_active_notes && note->note() < 128) {
|
|
// If this note is already active there's a stuck note,
|
|
// finish the old note rectangle
|
|
if (_active_notes[note->note()]) {
|
|
CanvasNote* const old_rect = _active_notes[note->note()];
|
|
boost::shared_ptr<NoteType> old_note = old_rect->note();
|
|
old_rect->property_x2() = x;
|
|
old_rect->property_outline_what() = (guint32) 0xF;
|
|
}
|
|
_active_notes[note->note()] = ev;
|
|
}
|
|
/* outline all but right edge */
|
|
ev->property_outline_what() = (guint32) (0x1 & 0x4 & 0x8);
|
|
} else {
|
|
/* outline all edges */
|
|
ev->property_outline_what() = (guint32) 0xF;
|
|
}
|
|
|
|
if (update_ghost_regions) {
|
|
for (std::vector<GhostRegion*>::iterator i = ghosts.begin(); i != ghosts.end(); ++i) {
|
|
MidiGhostRegion* gr = dynamic_cast<MidiGhostRegion*> (*i);
|
|
if (gr) {
|
|
gr->update_note (ev);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
double
|
|
MidiRegionView::update_hit (CanvasHit* ev)
|
|
{
|
|
boost::shared_ptr<NoteType> note = ev->note();
|
|
|
|
const framepos_t note_start_frames = source_beats_to_region_frames(note->time());
|
|
const double x = trackview.editor().frame_to_pixel(note_start_frames);
|
|
const double diamond_size = midi_stream_view()->note_height() / 2.0;
|
|
const double y = midi_stream_view()->note_to_y(note->note()) + ((diamond_size-2) / 4.0);
|
|
|
|
ev->move_to (x, y);
|
|
|
|
return diamond_size;
|
|
}
|
|
|
|
/** Add a MIDI note to the view (with length).
|
|
*
|
|
* If in sustained mode, notes with length 0 will be considered active
|
|
* notes, and resolve_note should be called when the corresponding note off
|
|
* event arrives, to properly display the note.
|
|
*/
|
|
void
|
|
MidiRegionView::add_note(const boost::shared_ptr<NoteType> note, bool visible)
|
|
{
|
|
CanvasNoteEvent* event = 0;
|
|
|
|
//ArdourCanvas::Group* const group = (ArdourCanvas::Group*) get_canvas_group();
|
|
|
|
if (midi_view()->note_mode() == Sustained) {
|
|
|
|
CanvasNote* ev_rect = new CanvasNote(*this, *_note_group, note);
|
|
|
|
update_note (ev_rect);
|
|
|
|
event = ev_rect;
|
|
|
|
MidiGhostRegion* gr;
|
|
|
|
for (std::vector<GhostRegion*>::iterator g = ghosts.begin(); g != ghosts.end(); ++g) {
|
|
if ((gr = dynamic_cast<MidiGhostRegion*>(*g)) != 0) {
|
|
gr->add_note(ev_rect);
|
|
}
|
|
}
|
|
|
|
} else if (midi_view()->note_mode() == Percussive) {
|
|
|
|
const double diamond_size = midi_stream_view()->note_height() / 2.0;
|
|
|
|
CanvasHit* ev_diamond = new CanvasHit (*this, *_note_group, diamond_size, note);
|
|
|
|
update_hit (ev_diamond);
|
|
|
|
event = ev_diamond;
|
|
|
|
} else {
|
|
event = 0;
|
|
}
|
|
|
|
if (event) {
|
|
if (_marked_for_selection.find(note) != _marked_for_selection.end()) {
|
|
note_selected(event, true);
|
|
}
|
|
|
|
if (_marked_for_velocity.find(note) != _marked_for_velocity.end()) {
|
|
event->show_velocity();
|
|
}
|
|
|
|
event->on_channel_selection_change (get_selected_channels());
|
|
_events.push_back(event);
|
|
|
|
if (visible) {
|
|
event->show();
|
|
} else {
|
|
event->hide ();
|
|
}
|
|
}
|
|
|
|
MidiTimeAxisView* const mtv = dynamic_cast<MidiTimeAxisView*>(&trackview);
|
|
MidiStreamView* const view = mtv->midi_view();
|
|
|
|
view->update_note_range (note->note());
|
|
}
|
|
|
|
void
|
|
MidiRegionView::step_add_note (uint8_t channel, uint8_t number, uint8_t velocity,
|
|
Evoral::MusicalTime pos, Evoral::MusicalTime len)
|
|
{
|
|
boost::shared_ptr<NoteType> new_note (new NoteType (channel, pos, len, number, velocity));
|
|
|
|
/* potentially extend region to hold new note */
|
|
|
|
framepos_t end_frame = source_beats_to_absolute_frames (new_note->end_time());
|
|
framepos_t region_end = _region->last_frame();
|
|
|
|
if (end_frame > region_end) {
|
|
_region->set_length (end_frame - _region->position());
|
|
}
|
|
|
|
MidiTimeAxisView* const mtv = dynamic_cast<MidiTimeAxisView*>(&trackview);
|
|
MidiStreamView* const view = mtv->midi_view();
|
|
|
|
view->update_note_range(new_note->note());
|
|
|
|
_marked_for_selection.clear ();
|
|
clear_selection ();
|
|
|
|
start_note_diff_command (_("step add"));
|
|
note_diff_add_note (new_note, true, false);
|
|
apply_diff();
|
|
|
|
// last_step_edit_note = new_note;
|
|
}
|
|
|
|
void
|
|
MidiRegionView::step_sustain (Evoral::MusicalTime beats)
|
|
{
|
|
change_note_lengths (false, false, beats, false, true);
|
|
}
|
|
|
|
/** Add a new patch change flag to the canvas.
|
|
* @param patch the patch change to add
|
|
* @param the text to display in the flag
|
|
* @param active_channel true to display the flag as on an active channel, false to grey it out for an inactive channel.
|
|
*/
|
|
void
|
|
MidiRegionView::add_canvas_patch_change (MidiModel::PatchChangePtr patch, const string& displaytext, bool active_channel)
|
|
{
|
|
framecnt_t region_frames = source_beats_to_region_frames (patch->time());
|
|
const double x = trackview.editor().frame_to_pixel (region_frames);
|
|
|
|
double const height = midi_stream_view()->contents_height();
|
|
|
|
boost::shared_ptr<CanvasPatchChange> patch_change = boost::shared_ptr<CanvasPatchChange>(
|
|
new CanvasPatchChange(*this, *group,
|
|
displaytext,
|
|
height,
|
|
x, 1.0,
|
|
instrument_info(),
|
|
patch,
|
|
active_channel));
|
|
|
|
if (patch_change->width() < _pixel_width) {
|
|
// Show unless patch change is beyond the region bounds
|
|
if (region_frames < 0 || region_frames >= _region->length()) {
|
|
patch_change->hide();
|
|
} else {
|
|
patch_change->show();
|
|
}
|
|
} else {
|
|
patch_change->hide ();
|
|
}
|
|
|
|
_patch_changes.push_back (patch_change);
|
|
}
|
|
|
|
MIDI::Name::PatchPrimaryKey
|
|
MidiRegionView::patch_change_to_patch_key (MidiModel::PatchChangePtr p)
|
|
{
|
|
return MIDI::Name::PatchPrimaryKey (p->program(), p->bank());
|
|
}
|
|
|
|
/// Return true iff @p pc applies to the given time on the given channel.
|
|
static bool
|
|
patch_applies (const ARDOUR::MidiModel::constPatchChangePtr pc, double time, uint8_t channel)
|
|
{
|
|
return pc->time() <= time && pc->channel() == channel;
|
|
}
|
|
|
|
void
|
|
MidiRegionView::get_patch_key_at (double time, uint8_t channel, MIDI::Name::PatchPrimaryKey& key) const
|
|
{
|
|
// The earliest event not before time
|
|
MidiModel::PatchChanges::iterator i = _model->patch_change_lower_bound (time);
|
|
|
|
// Go backwards until we find the latest PC for this channel, or the start
|
|
while (i != _model->patch_changes().begin() &&
|
|
(i == _model->patch_changes().end() ||
|
|
!patch_applies(*i, time, channel))) {
|
|
--i;
|
|
}
|
|
|
|
if (i != _model->patch_changes().end() && patch_applies(*i, time, channel)) {
|
|
key.bank_number = (*i)->bank();
|
|
key.program_number = (*i)->program ();
|
|
} else {
|
|
key.bank_number = key.program_number = 0;
|
|
}
|
|
|
|
if (!key.is_sane()) {
|
|
error << string_compose(_("insane MIDI patch key %1:%2"),
|
|
key.bank_number, key.program_number) << endmsg;
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::change_patch_change (CanvasPatchChange& pc, const MIDI::Name::PatchPrimaryKey& new_patch)
|
|
{
|
|
MidiModel::PatchChangeDiffCommand* c = _model->new_patch_change_diff_command (_("alter patch change"));
|
|
|
|
if (pc.patch()->program() != new_patch.program_number) {
|
|
c->change_program (pc.patch (), new_patch.program_number);
|
|
}
|
|
|
|
int const new_bank = new_patch.bank_number;
|
|
if (pc.patch()->bank() != new_bank) {
|
|
c->change_bank (pc.patch (), new_bank);
|
|
}
|
|
|
|
_model->apply_command (*trackview.session(), c);
|
|
|
|
_patch_changes.clear ();
|
|
display_patch_changes ();
|
|
}
|
|
|
|
void
|
|
MidiRegionView::change_patch_change (MidiModel::PatchChangePtr old_change, const Evoral::PatchChange<Evoral::MusicalTime> & new_change)
|
|
{
|
|
MidiModel::PatchChangeDiffCommand* c = _model->new_patch_change_diff_command (_("alter patch change"));
|
|
|
|
if (old_change->time() != new_change.time()) {
|
|
c->change_time (old_change, new_change.time());
|
|
}
|
|
|
|
if (old_change->channel() != new_change.channel()) {
|
|
c->change_channel (old_change, new_change.channel());
|
|
}
|
|
|
|
if (old_change->program() != new_change.program()) {
|
|
c->change_program (old_change, new_change.program());
|
|
}
|
|
|
|
if (old_change->bank() != new_change.bank()) {
|
|
c->change_bank (old_change, new_change.bank());
|
|
}
|
|
|
|
_model->apply_command (*trackview.session(), c);
|
|
|
|
_patch_changes.clear ();
|
|
display_patch_changes ();
|
|
}
|
|
|
|
/** Add a patch change to the region.
|
|
* @param t Time in frames relative to region position
|
|
* @param patch Patch to add; time and channel are ignored (time is converted from t, and channel comes from
|
|
* MidiTimeAxisView::get_channel_for_add())
|
|
*/
|
|
void
|
|
MidiRegionView::add_patch_change (framecnt_t t, Evoral::PatchChange<Evoral::MusicalTime> const & patch)
|
|
{
|
|
MidiTimeAxisView* const mtv = dynamic_cast<MidiTimeAxisView*>(&trackview);
|
|
|
|
MidiModel::PatchChangeDiffCommand* c = _model->new_patch_change_diff_command (_("add patch change"));
|
|
c->add (MidiModel::PatchChangePtr (
|
|
new Evoral::PatchChange<Evoral::MusicalTime> (
|
|
absolute_frames_to_source_beats (_region->position() + t),
|
|
mtv->get_channel_for_add(), patch.program(), patch.bank()
|
|
)
|
|
)
|
|
);
|
|
|
|
_model->apply_command (*trackview.session(), c);
|
|
|
|
_patch_changes.clear ();
|
|
display_patch_changes ();
|
|
}
|
|
|
|
void
|
|
MidiRegionView::move_patch_change (CanvasPatchChange& pc, Evoral::MusicalTime t)
|
|
{
|
|
MidiModel::PatchChangeDiffCommand* c = _model->new_patch_change_diff_command (_("move patch change"));
|
|
c->change_time (pc.patch (), t);
|
|
_model->apply_command (*trackview.session(), c);
|
|
|
|
_patch_changes.clear ();
|
|
display_patch_changes ();
|
|
}
|
|
|
|
void
|
|
MidiRegionView::delete_patch_change (CanvasPatchChange* pc)
|
|
{
|
|
MidiModel::PatchChangeDiffCommand* c = _model->new_patch_change_diff_command (_("delete patch change"));
|
|
c->remove (pc->patch ());
|
|
_model->apply_command (*trackview.session(), c);
|
|
|
|
_patch_changes.clear ();
|
|
display_patch_changes ();
|
|
}
|
|
|
|
void
|
|
MidiRegionView::previous_patch (CanvasPatchChange& patch)
|
|
{
|
|
if (patch.patch()->program() < 127) {
|
|
MIDI::Name::PatchPrimaryKey key = patch_change_to_patch_key (patch.patch());
|
|
key.program_number++;
|
|
change_patch_change (patch, key);
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::next_patch (CanvasPatchChange& patch)
|
|
{
|
|
if (patch.patch()->program() > 0) {
|
|
MIDI::Name::PatchPrimaryKey key = patch_change_to_patch_key (patch.patch());
|
|
key.program_number--;
|
|
change_patch_change (patch, key);
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::next_bank (CanvasPatchChange& patch)
|
|
{
|
|
if (patch.patch()->program() < 127) {
|
|
MIDI::Name::PatchPrimaryKey key = patch_change_to_patch_key (patch.patch());
|
|
if (key.bank_number > 0) {
|
|
key.bank_number--;
|
|
change_patch_change (patch, key);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::previous_bank (CanvasPatchChange& patch)
|
|
{
|
|
if (patch.patch()->program() > 0) {
|
|
MIDI::Name::PatchPrimaryKey key = patch_change_to_patch_key (patch.patch());
|
|
if (key.bank_number < 127) {
|
|
key.bank_number++;
|
|
change_patch_change (patch, key);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::maybe_remove_deleted_note_from_selection (CanvasNoteEvent* cne)
|
|
{
|
|
if (_selection.empty()) {
|
|
return;
|
|
}
|
|
|
|
_selection.erase (cne);
|
|
}
|
|
|
|
void
|
|
MidiRegionView::delete_selection()
|
|
{
|
|
if (_selection.empty()) {
|
|
return;
|
|
}
|
|
|
|
start_note_diff_command (_("delete selection"));
|
|
|
|
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
|
|
if ((*i)->selected()) {
|
|
_note_diff_command->remove((*i)->note());
|
|
}
|
|
}
|
|
|
|
_selection.clear();
|
|
|
|
apply_diff ();
|
|
}
|
|
|
|
void
|
|
MidiRegionView::delete_note (boost::shared_ptr<NoteType> n)
|
|
{
|
|
start_note_diff_command (_("delete note"));
|
|
_note_diff_command->remove (n);
|
|
apply_diff ();
|
|
|
|
trackview.editor().verbose_cursor()->hide ();
|
|
}
|
|
|
|
void
|
|
MidiRegionView::clear_selection_except (ArdourCanvas::CanvasNoteEvent* ev, bool signal)
|
|
{
|
|
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ) {
|
|
if ((*i) != ev) {
|
|
Selection::iterator tmp = i;
|
|
++tmp;
|
|
|
|
(*i)->set_selected (false);
|
|
(*i)->hide_velocity ();
|
|
_selection.erase (i);
|
|
|
|
i = tmp;
|
|
} else {
|
|
++i;
|
|
}
|
|
}
|
|
|
|
/* this does not change the status of this regionview w.r.t the editor
|
|
selection.
|
|
*/
|
|
|
|
if (signal) {
|
|
SelectionCleared (this); /* EMIT SIGNAL */
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::unique_select(ArdourCanvas::CanvasNoteEvent* ev)
|
|
{
|
|
clear_selection_except (ev);
|
|
|
|
/* don't bother with checking to see if we should remove this
|
|
regionview from the editor selection, since we're about to add
|
|
another note, and thus put/keep this regionview in the editor
|
|
selection anyway.
|
|
*/
|
|
|
|
if (!ev->selected()) {
|
|
add_to_selection (ev);
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::select_all_notes ()
|
|
{
|
|
clear_selection ();
|
|
|
|
for (Events::iterator i = _events.begin(); i != _events.end(); ++i) {
|
|
add_to_selection (*i);
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::select_range (framepos_t start, framepos_t end)
|
|
{
|
|
clear_selection ();
|
|
|
|
for (Events::iterator i = _events.begin(); i != _events.end(); ++i) {
|
|
framepos_t t = source_beats_to_absolute_frames((*i)->note()->time());
|
|
if (t >= start && t <= end) {
|
|
add_to_selection (*i);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::invert_selection ()
|
|
{
|
|
for (Events::iterator i = _events.begin(); i != _events.end(); ++i) {
|
|
if ((*i)->selected()) {
|
|
remove_from_selection(*i);
|
|
} else {
|
|
add_to_selection (*i);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::select_matching_notes (uint8_t notenum, uint16_t channel_mask, bool add, bool extend)
|
|
{
|
|
uint8_t low_note = 127;
|
|
uint8_t high_note = 0;
|
|
MidiModel::Notes& notes (_model->notes());
|
|
_optimization_iterator = _events.begin();
|
|
|
|
if (!add) {
|
|
clear_selection ();
|
|
}
|
|
|
|
if (extend && _selection.empty()) {
|
|
extend = false;
|
|
}
|
|
|
|
if (extend) {
|
|
|
|
/* scan existing selection to get note range */
|
|
|
|
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
|
|
if ((*i)->note()->note() < low_note) {
|
|
low_note = (*i)->note()->note();
|
|
}
|
|
if ((*i)->note()->note() > high_note) {
|
|
high_note = (*i)->note()->note();
|
|
}
|
|
}
|
|
|
|
low_note = min (low_note, notenum);
|
|
high_note = max (high_note, notenum);
|
|
}
|
|
|
|
_no_sound_notes = true;
|
|
|
|
for (MidiModel::Notes::iterator n = notes.begin(); n != notes.end(); ++n) {
|
|
|
|
boost::shared_ptr<NoteType> note (*n);
|
|
CanvasNoteEvent* cne;
|
|
bool select = false;
|
|
|
|
if (((1 << note->channel()) & channel_mask) != 0) {
|
|
if (extend) {
|
|
if ((note->note() >= low_note && note->note() <= high_note)) {
|
|
select = true;
|
|
}
|
|
} else if (note->note() == notenum) {
|
|
select = true;
|
|
}
|
|
}
|
|
|
|
if (select) {
|
|
if ((cne = find_canvas_note (note)) != 0) {
|
|
// extend is false because we've taken care of it,
|
|
// since it extends by time range, not pitch.
|
|
note_selected (cne, add, false);
|
|
}
|
|
}
|
|
|
|
add = true; // we need to add all remaining matching notes, even if the passed in value was false (for "set")
|
|
|
|
}
|
|
|
|
_no_sound_notes = false;
|
|
}
|
|
|
|
void
|
|
MidiRegionView::toggle_matching_notes (uint8_t notenum, uint16_t channel_mask)
|
|
{
|
|
MidiModel::Notes& notes (_model->notes());
|
|
_optimization_iterator = _events.begin();
|
|
|
|
for (MidiModel::Notes::iterator n = notes.begin(); n != notes.end(); ++n) {
|
|
|
|
boost::shared_ptr<NoteType> note (*n);
|
|
CanvasNoteEvent* cne;
|
|
|
|
if (note->note() == notenum && (((0x0001 << note->channel()) & channel_mask) != 0)) {
|
|
if ((cne = find_canvas_note (note)) != 0) {
|
|
if (cne->selected()) {
|
|
note_deselected (cne);
|
|
} else {
|
|
note_selected (cne, true, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::note_selected (ArdourCanvas::CanvasNoteEvent* ev, bool add, bool extend)
|
|
{
|
|
if (!add) {
|
|
clear_selection_except (ev);
|
|
if (!_selection.empty()) {
|
|
PublicEditor& editor (trackview.editor());
|
|
editor.get_selection().add (this);
|
|
}
|
|
}
|
|
|
|
if (!extend) {
|
|
|
|
if (!ev->selected()) {
|
|
add_to_selection (ev);
|
|
}
|
|
|
|
} else {
|
|
/* find end of latest note selected, select all between that and the start of "ev" */
|
|
|
|
Evoral::MusicalTime earliest = Evoral::MaxMusicalTime;
|
|
Evoral::MusicalTime latest = 0;
|
|
|
|
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
|
|
if ((*i)->note()->end_time() > latest) {
|
|
latest = (*i)->note()->end_time();
|
|
}
|
|
if ((*i)->note()->time() < earliest) {
|
|
earliest = (*i)->note()->time();
|
|
}
|
|
}
|
|
|
|
if (ev->note()->end_time() > latest) {
|
|
latest = ev->note()->end_time();
|
|
}
|
|
|
|
if (ev->note()->time() < earliest) {
|
|
earliest = ev->note()->time();
|
|
}
|
|
|
|
for (Events::iterator i = _events.begin(); i != _events.end(); ++i) {
|
|
|
|
/* find notes entirely within OR spanning the earliest..latest range */
|
|
|
|
if (((*i)->note()->time() >= earliest && (*i)->note()->end_time() <= latest) ||
|
|
((*i)->note()->time() <= earliest && (*i)->note()->end_time() >= latest)) {
|
|
add_to_selection (*i);
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::note_deselected(ArdourCanvas::CanvasNoteEvent* ev)
|
|
{
|
|
remove_from_selection (ev);
|
|
}
|
|
|
|
void
|
|
MidiRegionView::update_drag_selection(double x1, double x2, double y1, double y2, bool extend)
|
|
{
|
|
// TODO: Make this faster by storing the last updated selection rect, and only
|
|
// adjusting things that are in the area that appears/disappeared.
|
|
// We probably need a tree to be able to find events in O(log(n)) time.
|
|
|
|
for (Events::iterator i = _events.begin(); i != _events.end(); ++i) {
|
|
if ((*i)->x1() < x2 && (*i)->x2() > x1 && (*i)->y1() < y2 && (*i)->y2() > y1) {
|
|
// Rectangles intersect
|
|
if (!(*i)->selected()) {
|
|
add_to_selection (*i);
|
|
}
|
|
} else if ((*i)->selected() && !extend) {
|
|
// Rectangles do not intersect
|
|
remove_from_selection (*i);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::update_vertical_drag_selection (double y1, double y2, bool extend)
|
|
{
|
|
if (y1 > y2) {
|
|
swap (y1, y2);
|
|
}
|
|
|
|
// TODO: Make this faster by storing the last updated selection rect, and only
|
|
// adjusting things that are in the area that appears/disappeared.
|
|
// We probably need a tree to be able to find events in O(log(n)) time.
|
|
|
|
for (Events::iterator i = _events.begin(); i != _events.end(); ++i) {
|
|
if (((*i)->y1() >= y1 && (*i)->y1() <= y2)) {
|
|
// within y- (note-) range
|
|
if (!(*i)->selected()) {
|
|
add_to_selection (*i);
|
|
}
|
|
} else if ((*i)->selected() && !extend) {
|
|
remove_from_selection (*i);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::remove_from_selection (CanvasNoteEvent* ev)
|
|
{
|
|
Selection::iterator i = _selection.find (ev);
|
|
|
|
if (i != _selection.end()) {
|
|
_selection.erase (i);
|
|
}
|
|
|
|
ev->set_selected (false);
|
|
ev->hide_velocity ();
|
|
|
|
if (_selection.empty()) {
|
|
PublicEditor& editor (trackview.editor());
|
|
editor.get_selection().remove (this);
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::add_to_selection (CanvasNoteEvent* ev)
|
|
{
|
|
bool add_mrv_selection = false;
|
|
|
|
if (_selection.empty()) {
|
|
add_mrv_selection = true;
|
|
}
|
|
|
|
if (_selection.insert (ev).second) {
|
|
ev->set_selected (true);
|
|
start_playing_midi_note ((ev)->note());
|
|
}
|
|
|
|
if (add_mrv_selection) {
|
|
PublicEditor& editor (trackview.editor());
|
|
editor.get_selection().add (this);
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::move_selection(double dx, double dy, double cumulative_dy)
|
|
{
|
|
typedef vector<boost::shared_ptr<NoteType> > PossibleChord;
|
|
PossibleChord to_play;
|
|
Evoral::MusicalTime earliest = Evoral::MaxMusicalTime;
|
|
|
|
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
|
|
if ((*i)->note()->time() < earliest) {
|
|
earliest = (*i)->note()->time();
|
|
}
|
|
}
|
|
|
|
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
|
|
if (Evoral::musical_time_equal ((*i)->note()->time(), earliest)) {
|
|
to_play.push_back ((*i)->note());
|
|
}
|
|
(*i)->move_event(dx, dy);
|
|
}
|
|
|
|
if (dy && !_selection.empty() && !_no_sound_notes && Config->get_sound_midi_notes()) {
|
|
|
|
if (to_play.size() > 1) {
|
|
|
|
PossibleChord shifted;
|
|
|
|
for (PossibleChord::iterator n = to_play.begin(); n != to_play.end(); ++n) {
|
|
boost::shared_ptr<NoteType> moved_note (new NoteType (**n));
|
|
moved_note->set_note (moved_note->note() + cumulative_dy);
|
|
shifted.push_back (moved_note);
|
|
}
|
|
|
|
start_playing_midi_chord (shifted);
|
|
|
|
} else if (!to_play.empty()) {
|
|
|
|
boost::shared_ptr<NoteType> moved_note (new NoteType (*to_play.front()));
|
|
moved_note->set_note (moved_note->note() + cumulative_dy);
|
|
start_playing_midi_note (moved_note);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::note_dropped(CanvasNoteEvent *, frameoffset_t dt, int8_t dnote)
|
|
{
|
|
uint8_t lowest_note_in_selection = 127;
|
|
uint8_t highest_note_in_selection = 0;
|
|
uint8_t highest_note_difference = 0;
|
|
|
|
// find highest and lowest notes first
|
|
|
|
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
|
|
uint8_t pitch = (*i)->note()->note();
|
|
lowest_note_in_selection = std::min(lowest_note_in_selection, pitch);
|
|
highest_note_in_selection = std::max(highest_note_in_selection, pitch);
|
|
}
|
|
|
|
/*
|
|
cerr << "dnote: " << (int) dnote << endl;
|
|
cerr << "lowest note (streamview): " << int(midi_stream_view()->lowest_note())
|
|
<< " highest note (streamview): " << int(midi_stream_view()->highest_note()) << endl;
|
|
cerr << "lowest note (selection): " << int(lowest_note_in_selection) << " highest note(selection): "
|
|
<< int(highest_note_in_selection) << endl;
|
|
cerr << "selection size: " << _selection.size() << endl;
|
|
cerr << "Highest note in selection: " << (int) highest_note_in_selection << endl;
|
|
*/
|
|
|
|
// Make sure the note pitch does not exceed the MIDI standard range
|
|
if (highest_note_in_selection + dnote > 127) {
|
|
highest_note_difference = highest_note_in_selection - 127;
|
|
}
|
|
|
|
start_note_diff_command (_("move notes"));
|
|
|
|
for (Selection::iterator i = _selection.begin(); i != _selection.end() ; ++i) {
|
|
|
|
framepos_t new_frames = source_beats_to_absolute_frames ((*i)->note()->time()) + dt;
|
|
Evoral::MusicalTime new_time = absolute_frames_to_source_beats (new_frames);
|
|
|
|
if (new_time < 0) {
|
|
continue;
|
|
}
|
|
|
|
note_diff_add_change (*i, MidiModel::NoteDiffCommand::StartTime, new_time);
|
|
|
|
uint8_t original_pitch = (*i)->note()->note();
|
|
uint8_t new_pitch = original_pitch + dnote - highest_note_difference;
|
|
|
|
// keep notes in standard midi range
|
|
clamp_to_0_127(new_pitch);
|
|
|
|
lowest_note_in_selection = std::min(lowest_note_in_selection, new_pitch);
|
|
highest_note_in_selection = std::max(highest_note_in_selection, new_pitch);
|
|
|
|
note_diff_add_change (*i, MidiModel::NoteDiffCommand::NoteNumber, new_pitch);
|
|
}
|
|
|
|
apply_diff();
|
|
|
|
// care about notes being moved beyond the upper/lower bounds on the canvas
|
|
if (lowest_note_in_selection < midi_stream_view()->lowest_note() ||
|
|
highest_note_in_selection > midi_stream_view()->highest_note()) {
|
|
midi_stream_view()->set_note_range(MidiStreamView::ContentsRange);
|
|
}
|
|
}
|
|
|
|
/** @param x Pixel relative to the region position.
|
|
* @return Snapped frame relative to the region position.
|
|
*/
|
|
framepos_t
|
|
MidiRegionView::snap_pixel_to_frame(double x)
|
|
{
|
|
PublicEditor& editor (trackview.editor());
|
|
return snap_frame_to_frame (editor.pixel_to_frame (x));
|
|
}
|
|
|
|
/** @param x Pixel relative to the region position.
|
|
* @return Snapped pixel relative to the region position.
|
|
*/
|
|
double
|
|
MidiRegionView::snap_to_pixel(double x)
|
|
{
|
|
return (double) trackview.editor().frame_to_pixel(snap_pixel_to_frame(x));
|
|
}
|
|
|
|
double
|
|
MidiRegionView::get_position_pixels()
|
|
{
|
|
framepos_t region_frame = get_position();
|
|
return trackview.editor().frame_to_pixel(region_frame);
|
|
}
|
|
|
|
double
|
|
MidiRegionView::get_end_position_pixels()
|
|
{
|
|
framepos_t frame = get_position() + get_duration ();
|
|
return trackview.editor().frame_to_pixel(frame);
|
|
}
|
|
|
|
framepos_t
|
|
MidiRegionView::source_beats_to_absolute_frames(double beats) const
|
|
{
|
|
/* the time converter will return the frame corresponding to `beats'
|
|
relative to the start of the source. The start of the source
|
|
is an implied position given by region->position - region->start
|
|
*/
|
|
const framepos_t source_start = _region->position() - _region->start();
|
|
return source_start + _source_relative_time_converter.to (beats);
|
|
}
|
|
|
|
double
|
|
MidiRegionView::absolute_frames_to_source_beats(framepos_t frames) const
|
|
{
|
|
/* the `frames' argument needs to be converted into a frame count
|
|
relative to the start of the source before being passed in to the
|
|
converter.
|
|
*/
|
|
const framepos_t source_start = _region->position() - _region->start();
|
|
return _source_relative_time_converter.from (frames - source_start);
|
|
}
|
|
|
|
framepos_t
|
|
MidiRegionView::region_beats_to_region_frames(double beats) const
|
|
{
|
|
return _region_relative_time_converter.to(beats);
|
|
}
|
|
|
|
double
|
|
MidiRegionView::region_frames_to_region_beats(framepos_t frames) const
|
|
{
|
|
return _region_relative_time_converter.from(frames);
|
|
}
|
|
|
|
void
|
|
MidiRegionView::begin_resizing (bool /*at_front*/)
|
|
{
|
|
_resize_data.clear();
|
|
|
|
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
|
|
CanvasNote *note = dynamic_cast<CanvasNote *> (*i);
|
|
|
|
// only insert CanvasNotes into the map
|
|
if (note) {
|
|
NoteResizeData *resize_data = new NoteResizeData();
|
|
resize_data->canvas_note = note;
|
|
|
|
// create a new SimpleRect from the note which will be the resize preview
|
|
SimpleRect *resize_rect = new SimpleRect(
|
|
*_note_group, note->x1(), note->y1(), note->x2(), note->y2());
|
|
|
|
// calculate the colors: get the color settings
|
|
uint32_t fill_color = UINT_RGBA_CHANGE_A(
|
|
ARDOUR_UI::config()->canvasvar_MidiNoteSelected.get(),
|
|
128);
|
|
|
|
// make the resize preview notes more transparent and bright
|
|
fill_color = UINT_INTERPOLATE(fill_color, 0xFFFFFF40, 0.5);
|
|
|
|
// calculate color based on note velocity
|
|
resize_rect->property_fill_color_rgba() = UINT_INTERPOLATE(
|
|
CanvasNoteEvent::meter_style_fill_color(note->note()->velocity(), note->selected()),
|
|
fill_color,
|
|
0.85);
|
|
|
|
resize_rect->property_outline_color_rgba() = CanvasNoteEvent::calculate_outline(
|
|
ARDOUR_UI::config()->canvasvar_MidiNoteSelected.get());
|
|
|
|
resize_data->resize_rect = resize_rect;
|
|
_resize_data.push_back(resize_data);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Update resizing notes while user drags.
|
|
* @param primary `primary' note for the drag; ie the one that is used as the reference in non-relative mode.
|
|
* @param at_front which end of the note (true == note on, false == note off)
|
|
* @param delta_x change in mouse position since the start of the drag
|
|
* @param relative true if relative resizing is taking place, false if absolute resizing. This only makes
|
|
* a difference when multiple notes are being resized; in relative mode, each note's length is changed by the
|
|
* amount of the drag. In non-relative mode, all selected notes are set to have the same start or end point
|
|
* as the \a primary note.
|
|
*/
|
|
void
|
|
MidiRegionView::update_resizing (ArdourCanvas::CanvasNoteEvent* primary, bool at_front, double delta_x, bool relative)
|
|
{
|
|
bool cursor_set = false;
|
|
|
|
for (std::vector<NoteResizeData *>::iterator i = _resize_data.begin(); i != _resize_data.end(); ++i) {
|
|
SimpleRect* resize_rect = (*i)->resize_rect;
|
|
CanvasNote* canvas_note = (*i)->canvas_note;
|
|
double current_x;
|
|
|
|
if (at_front) {
|
|
if (relative) {
|
|
current_x = canvas_note->x1() + delta_x;
|
|
} else {
|
|
current_x = primary->x1() + delta_x;
|
|
}
|
|
} else {
|
|
if (relative) {
|
|
current_x = canvas_note->x2() + delta_x;
|
|
} else {
|
|
current_x = primary->x2() + delta_x;
|
|
}
|
|
}
|
|
|
|
if (at_front) {
|
|
resize_rect->property_x1() = snap_to_pixel(current_x);
|
|
resize_rect->property_x2() = canvas_note->x2();
|
|
} else {
|
|
resize_rect->property_x2() = snap_to_pixel(current_x);
|
|
resize_rect->property_x1() = canvas_note->x1();
|
|
}
|
|
|
|
if (!cursor_set) {
|
|
double beats;
|
|
|
|
beats = snap_pixel_to_frame (current_x);
|
|
beats = region_frames_to_region_beats (beats);
|
|
|
|
double len;
|
|
|
|
if (at_front) {
|
|
if (beats < canvas_note->note()->end_time()) {
|
|
len = canvas_note->note()->time() - beats;
|
|
len += canvas_note->note()->length();
|
|
} else {
|
|
len = 0;
|
|
}
|
|
} else {
|
|
if (beats >= canvas_note->note()->time()) {
|
|
len = beats - canvas_note->note()->time();
|
|
} else {
|
|
len = 0;
|
|
}
|
|
}
|
|
|
|
char buf[16];
|
|
snprintf (buf, sizeof (buf), "%.3g beats", len);
|
|
show_verbose_cursor (buf, 0, 0);
|
|
|
|
cursor_set = true;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
|
|
/** Finish resizing notes when the user releases the mouse button.
|
|
* Parameters the same as for \a update_resizing().
|
|
*/
|
|
void
|
|
MidiRegionView::commit_resizing (ArdourCanvas::CanvasNoteEvent* primary, bool at_front, double delta_x, bool relative)
|
|
{
|
|
start_note_diff_command (_("resize notes"));
|
|
|
|
for (std::vector<NoteResizeData *>::iterator i = _resize_data.begin(); i != _resize_data.end(); ++i) {
|
|
CanvasNote* canvas_note = (*i)->canvas_note;
|
|
SimpleRect* resize_rect = (*i)->resize_rect;
|
|
|
|
/* Get the new x position for this resize, which is in pixels relative
|
|
* to the region position.
|
|
*/
|
|
|
|
double current_x;
|
|
|
|
if (at_front) {
|
|
if (relative) {
|
|
current_x = canvas_note->x1() + delta_x;
|
|
} else {
|
|
current_x = primary->x1() + delta_x;
|
|
}
|
|
} else {
|
|
if (relative) {
|
|
current_x = canvas_note->x2() + delta_x;
|
|
} else {
|
|
current_x = primary->x2() + delta_x;
|
|
}
|
|
}
|
|
|
|
/* Convert that to a frame within the source */
|
|
current_x = snap_pixel_to_frame (current_x) + _region->start ();
|
|
|
|
/* and then to beats */
|
|
current_x = region_frames_to_region_beats (current_x);
|
|
|
|
if (at_front && current_x < canvas_note->note()->end_time()) {
|
|
note_diff_add_change (canvas_note, MidiModel::NoteDiffCommand::StartTime, current_x);
|
|
|
|
double len = canvas_note->note()->time() - current_x;
|
|
len += canvas_note->note()->length();
|
|
|
|
if (len > 0) {
|
|
/* XXX convert to beats */
|
|
note_diff_add_change (canvas_note, MidiModel::NoteDiffCommand::Length, len);
|
|
}
|
|
}
|
|
|
|
if (!at_front) {
|
|
double len = current_x - canvas_note->note()->time();
|
|
|
|
if (len > 0) {
|
|
/* XXX convert to beats */
|
|
note_diff_add_change (canvas_note, MidiModel::NoteDiffCommand::Length, len);
|
|
}
|
|
}
|
|
|
|
delete resize_rect;
|
|
delete (*i);
|
|
}
|
|
|
|
_resize_data.clear();
|
|
apply_diff();
|
|
}
|
|
|
|
void
|
|
MidiRegionView::abort_resizing ()
|
|
{
|
|
for (std::vector<NoteResizeData *>::iterator i = _resize_data.begin(); i != _resize_data.end(); ++i) {
|
|
delete (*i)->resize_rect;
|
|
delete *i;
|
|
}
|
|
|
|
_resize_data.clear ();
|
|
}
|
|
|
|
void
|
|
MidiRegionView::change_note_velocity(CanvasNoteEvent* event, int8_t velocity, bool relative)
|
|
{
|
|
uint8_t new_velocity;
|
|
|
|
if (relative) {
|
|
new_velocity = event->note()->velocity() + velocity;
|
|
clamp_to_0_127(new_velocity);
|
|
} else {
|
|
new_velocity = velocity;
|
|
}
|
|
|
|
event->set_selected (event->selected()); // change color
|
|
|
|
note_diff_add_change (event, MidiModel::NoteDiffCommand::Velocity, new_velocity);
|
|
}
|
|
|
|
void
|
|
MidiRegionView::change_note_note (CanvasNoteEvent* event, int8_t note, bool relative)
|
|
{
|
|
uint8_t new_note;
|
|
|
|
if (relative) {
|
|
new_note = event->note()->note() + note;
|
|
} else {
|
|
new_note = note;
|
|
}
|
|
|
|
clamp_to_0_127 (new_note);
|
|
note_diff_add_change (event, MidiModel::NoteDiffCommand::NoteNumber, new_note);
|
|
}
|
|
|
|
void
|
|
MidiRegionView::trim_note (CanvasNoteEvent* event, Evoral::MusicalTime front_delta, Evoral::MusicalTime end_delta)
|
|
{
|
|
bool change_start = false;
|
|
bool change_length = false;
|
|
Evoral::MusicalTime new_start = 0;
|
|
Evoral::MusicalTime new_length = 0;
|
|
|
|
/* NOTE: the semantics of the two delta arguments are slightly subtle:
|
|
|
|
front_delta: if positive - move the start of the note later in time (shortening it)
|
|
if negative - move the start of the note earlier in time (lengthening it)
|
|
|
|
end_delta: if positive - move the end of the note later in time (lengthening it)
|
|
if negative - move the end of the note earlier in time (shortening it)
|
|
*/
|
|
|
|
if (front_delta) {
|
|
if (front_delta < 0) {
|
|
|
|
if (event->note()->time() < -front_delta) {
|
|
new_start = 0;
|
|
} else {
|
|
new_start = event->note()->time() + front_delta; // moves earlier
|
|
}
|
|
|
|
/* start moved toward zero, so move the end point out to where it used to be.
|
|
Note that front_delta is negative, so this increases the length.
|
|
*/
|
|
|
|
new_length = event->note()->length() - front_delta;
|
|
change_start = true;
|
|
change_length = true;
|
|
|
|
} else {
|
|
|
|
Evoral::MusicalTime new_pos = event->note()->time() + front_delta;
|
|
|
|
if (new_pos < event->note()->end_time()) {
|
|
new_start = event->note()->time() + front_delta;
|
|
/* start moved toward the end, so move the end point back to where it used to be */
|
|
new_length = event->note()->length() - front_delta;
|
|
change_start = true;
|
|
change_length = true;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if (end_delta) {
|
|
bool can_change = true;
|
|
if (end_delta < 0) {
|
|
if (event->note()->length() < -end_delta) {
|
|
can_change = false;
|
|
}
|
|
}
|
|
|
|
if (can_change) {
|
|
new_length = event->note()->length() + end_delta;
|
|
change_length = true;
|
|
}
|
|
}
|
|
|
|
if (change_start) {
|
|
note_diff_add_change (event, MidiModel::NoteDiffCommand::StartTime, new_start);
|
|
}
|
|
|
|
if (change_length) {
|
|
note_diff_add_change (event, MidiModel::NoteDiffCommand::Length, new_length);
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::change_note_channel (CanvasNoteEvent* event, int8_t chn, bool relative)
|
|
{
|
|
uint8_t new_channel;
|
|
|
|
if (relative) {
|
|
if (chn < 0.0) {
|
|
if (event->note()->channel() < -chn) {
|
|
new_channel = 0;
|
|
} else {
|
|
new_channel = event->note()->channel() + chn;
|
|
}
|
|
} else {
|
|
new_channel = event->note()->channel() + chn;
|
|
}
|
|
} else {
|
|
new_channel = (uint8_t) chn;
|
|
}
|
|
|
|
note_diff_add_change (event, MidiModel::NoteDiffCommand::Channel, new_channel);
|
|
}
|
|
|
|
void
|
|
MidiRegionView::change_note_time (CanvasNoteEvent* event, Evoral::MusicalTime delta, bool relative)
|
|
{
|
|
Evoral::MusicalTime new_time;
|
|
|
|
if (relative) {
|
|
if (delta < 0.0) {
|
|
if (event->note()->time() < -delta) {
|
|
new_time = 0;
|
|
} else {
|
|
new_time = event->note()->time() + delta;
|
|
}
|
|
} else {
|
|
new_time = event->note()->time() + delta;
|
|
}
|
|
} else {
|
|
new_time = delta;
|
|
}
|
|
|
|
note_diff_add_change (event, MidiModel::NoteDiffCommand::StartTime, new_time);
|
|
}
|
|
|
|
void
|
|
MidiRegionView::change_note_length (CanvasNoteEvent* event, Evoral::MusicalTime t)
|
|
{
|
|
note_diff_add_change (event, MidiModel::NoteDiffCommand::Length, t);
|
|
}
|
|
|
|
void
|
|
MidiRegionView::change_velocities (bool up, bool fine, bool allow_smush, bool all_together)
|
|
{
|
|
int8_t delta;
|
|
int8_t value = 0;
|
|
|
|
if (_selection.empty()) {
|
|
return;
|
|
}
|
|
|
|
if (fine) {
|
|
delta = 1;
|
|
} else {
|
|
delta = 10;
|
|
}
|
|
|
|
if (!up) {
|
|
delta = -delta;
|
|
}
|
|
|
|
if (!allow_smush) {
|
|
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
|
|
if ((*i)->note()->velocity() < -delta || (*i)->note()->velocity() + delta > 127) {
|
|
goto cursor_label;
|
|
}
|
|
}
|
|
}
|
|
|
|
start_note_diff_command (_("change velocities"));
|
|
|
|
for (Selection::iterator i = _selection.begin(); i != _selection.end();) {
|
|
Selection::iterator next = i;
|
|
++next;
|
|
|
|
if (all_together) {
|
|
if (i == _selection.begin()) {
|
|
change_note_velocity (*i, delta, true);
|
|
value = (*i)->note()->velocity() + delta;
|
|
} else {
|
|
change_note_velocity (*i, value, false);
|
|
}
|
|
|
|
} else {
|
|
change_note_velocity (*i, delta, true);
|
|
}
|
|
|
|
i = next;
|
|
}
|
|
|
|
apply_diff();
|
|
|
|
cursor_label:
|
|
if (!_selection.empty()) {
|
|
char buf[24];
|
|
snprintf (buf, sizeof (buf), "Vel %d",
|
|
(int) (*_selection.begin())->note()->velocity());
|
|
show_verbose_cursor (buf, 10, 10);
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
MidiRegionView::transpose (bool up, bool fine, bool allow_smush)
|
|
{
|
|
if (_selection.empty()) {
|
|
return;
|
|
}
|
|
|
|
int8_t delta;
|
|
|
|
if (fine) {
|
|
delta = 1;
|
|
} else {
|
|
delta = 12;
|
|
}
|
|
|
|
if (!up) {
|
|
delta = -delta;
|
|
}
|
|
|
|
if (!allow_smush) {
|
|
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
|
|
if (!up) {
|
|
if ((int8_t) (*i)->note()->note() + delta <= 0) {
|
|
return;
|
|
}
|
|
} else {
|
|
if ((int8_t) (*i)->note()->note() + delta > 127) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
start_note_diff_command (_("transpose"));
|
|
|
|
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ) {
|
|
Selection::iterator next = i;
|
|
++next;
|
|
change_note_note (*i, delta, true);
|
|
i = next;
|
|
}
|
|
|
|
apply_diff ();
|
|
}
|
|
|
|
void
|
|
MidiRegionView::change_note_lengths (bool fine, bool shorter, Evoral::MusicalTime delta, bool start, bool end)
|
|
{
|
|
if (delta == 0.0) {
|
|
if (fine) {
|
|
delta = 1.0/128.0;
|
|
} else {
|
|
/* grab the current grid distance */
|
|
bool success;
|
|
delta = trackview.editor().get_grid_type_as_beats (success, _region->position());
|
|
if (!success) {
|
|
/* XXX cannot get grid type as beats ... should always be possible ... FIX ME */
|
|
error << string_compose (_("programming error: %1"), "Grid type not available as beats - TO BE FIXED") << endmsg;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (shorter) {
|
|
delta = -delta;
|
|
}
|
|
|
|
start_note_diff_command (_("change note lengths"));
|
|
|
|
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ) {
|
|
Selection::iterator next = i;
|
|
++next;
|
|
|
|
/* note the negation of the delta for start */
|
|
|
|
trim_note (*i, (start ? -delta : 0), (end ? delta : 0));
|
|
i = next;
|
|
}
|
|
|
|
apply_diff ();
|
|
|
|
}
|
|
|
|
void
|
|
MidiRegionView::nudge_notes (bool forward)
|
|
{
|
|
if (_selection.empty()) {
|
|
return;
|
|
}
|
|
|
|
/* pick a note as the point along the timeline to get the nudge distance.
|
|
its not necessarily the earliest note, so we may want to pull the notes out
|
|
into a vector and sort before using the first one.
|
|
*/
|
|
|
|
framepos_t ref_point = source_beats_to_absolute_frames ((*(_selection.begin()))->note()->time());
|
|
framepos_t unused;
|
|
framecnt_t distance;
|
|
|
|
if (trackview.editor().snap_mode() == Editing::SnapOff) {
|
|
|
|
/* grid is off - use nudge distance */
|
|
|
|
distance = trackview.editor().get_nudge_distance (ref_point, unused);
|
|
|
|
} else {
|
|
|
|
/* use grid */
|
|
|
|
framepos_t next_pos = ref_point;
|
|
|
|
if (forward) {
|
|
if (max_framepos - 1 < next_pos) {
|
|
next_pos += 1;
|
|
}
|
|
} else {
|
|
if (next_pos == 0) {
|
|
return;
|
|
}
|
|
next_pos -= 1;
|
|
}
|
|
|
|
trackview.editor().snap_to (next_pos, (forward ? 1 : -1), false);
|
|
distance = ref_point - next_pos;
|
|
}
|
|
|
|
if (distance == 0) {
|
|
return;
|
|
}
|
|
|
|
Evoral::MusicalTime delta = region_frames_to_region_beats (fabs (distance));
|
|
|
|
if (!forward) {
|
|
delta = -delta;
|
|
}
|
|
|
|
start_note_diff_command (_("nudge"));
|
|
|
|
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ) {
|
|
Selection::iterator next = i;
|
|
++next;
|
|
change_note_time (*i, delta, true);
|
|
i = next;
|
|
}
|
|
|
|
apply_diff ();
|
|
}
|
|
|
|
void
|
|
MidiRegionView::change_channel(uint8_t channel)
|
|
{
|
|
start_note_diff_command(_("change channel"));
|
|
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
|
|
note_diff_add_change (*i, MidiModel::NoteDiffCommand::Channel, channel);
|
|
}
|
|
|
|
apply_diff();
|
|
}
|
|
|
|
|
|
void
|
|
MidiRegionView::note_entered(ArdourCanvas::CanvasNoteEvent* ev)
|
|
{
|
|
Editor* editor = dynamic_cast<Editor*>(&trackview.editor());
|
|
|
|
pre_enter_cursor = editor->get_canvas_cursor ();
|
|
|
|
if (_mouse_state == SelectTouchDragging) {
|
|
note_selected (ev, true);
|
|
}
|
|
|
|
show_verbose_cursor (ev->note ());
|
|
}
|
|
|
|
void
|
|
MidiRegionView::note_left (ArdourCanvas::CanvasNoteEvent*)
|
|
{
|
|
Editor* editor = dynamic_cast<Editor*>(&trackview.editor());
|
|
|
|
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
|
|
(*i)->hide_velocity ();
|
|
}
|
|
|
|
editor->verbose_cursor()->hide ();
|
|
|
|
if (pre_enter_cursor) {
|
|
editor->set_canvas_cursor (pre_enter_cursor);
|
|
pre_enter_cursor = 0;
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::patch_entered (ArdourCanvas::CanvasPatchChange* p)
|
|
{
|
|
ostringstream s;
|
|
/* XXX should get patch name if we can */
|
|
s << _("Bank ") << (p->patch()->bank() + MIDI_BP_ZERO) << '\n'
|
|
<< _("Program ") << ((int) p->patch()->program()) + MIDI_BP_ZERO << '\n'
|
|
<< _("Channel ") << ((int) p->patch()->channel() + 1);
|
|
show_verbose_cursor (s.str(), 10, 20);
|
|
p->grab_focus();
|
|
}
|
|
|
|
void
|
|
MidiRegionView::patch_left (ArdourCanvas::CanvasPatchChange *)
|
|
{
|
|
trackview.editor().verbose_cursor()->hide ();
|
|
/* focus will transfer back via the enter-notify event sent to this
|
|
* midi region view.
|
|
*/
|
|
}
|
|
|
|
void
|
|
MidiRegionView::sysex_entered (ArdourCanvas::CanvasSysEx* p)
|
|
{
|
|
ostringstream s;
|
|
s << p->text();
|
|
show_verbose_cursor (s.str(), 10, 20);
|
|
p->grab_focus();
|
|
}
|
|
|
|
void
|
|
MidiRegionView::sysex_left (ArdourCanvas::CanvasSysEx *)
|
|
{
|
|
trackview.editor().verbose_cursor()->hide ();
|
|
/* focus will transfer back via the enter-notify event sent to this
|
|
* midi region view.
|
|
*/
|
|
}
|
|
|
|
void
|
|
MidiRegionView::note_mouse_position (float x_fraction, float /*y_fraction*/, bool can_set_cursor)
|
|
{
|
|
Editor* editor = dynamic_cast<Editor*>(&trackview.editor());
|
|
Editing::MouseMode mm = editor->current_mouse_mode();
|
|
bool trimmable = (mm == MouseObject || mm == MouseTimeFX || mm == MouseDraw);
|
|
|
|
if (trimmable && x_fraction > 0.0 && x_fraction < 0.2) {
|
|
editor->set_canvas_cursor (editor->cursors()->left_side_trim);
|
|
} else if (trimmable && x_fraction >= 0.8 && x_fraction < 1.0) {
|
|
editor->set_canvas_cursor (editor->cursors()->right_side_trim);
|
|
} else {
|
|
if (pre_enter_cursor && can_set_cursor) {
|
|
editor->set_canvas_cursor (pre_enter_cursor);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::set_frame_color()
|
|
{
|
|
uint32_t f;
|
|
|
|
TimeAxisViewItem::set_frame_color ();
|
|
|
|
if (!frame) {
|
|
return;
|
|
}
|
|
|
|
if (_selected) {
|
|
f = ARDOUR_UI::config()->canvasvar_SelectedFrameBase.get();
|
|
} else if (high_enough_for_name) {
|
|
f= ARDOUR_UI::config()->canvasvar_MidiFrameBase.get();
|
|
} else {
|
|
f = fill_color;
|
|
}
|
|
|
|
if (!rect_visible) {
|
|
f = UINT_RGBA_CHANGE_A (f, 0);
|
|
}
|
|
|
|
frame->property_fill_color_rgba() = f;
|
|
}
|
|
|
|
void
|
|
MidiRegionView::midi_channel_mode_changed ()
|
|
{
|
|
MidiTimeAxisView* const mtv = dynamic_cast<MidiTimeAxisView*>(&trackview);
|
|
uint16_t mask = mtv->midi_track()->get_playback_channel_mask();
|
|
ChannelMode mode = mtv->midi_track()->get_playback_channel_mode ();
|
|
|
|
if (mode == ForceChannel) {
|
|
mask = 0xFFFF; // Show all notes as active (below)
|
|
}
|
|
|
|
// Update notes for selection
|
|
for (Events::iterator i = _events.begin(); i != _events.end(); ++i) {
|
|
(*i)->on_channel_selection_change (mask);
|
|
}
|
|
|
|
_patch_changes.clear ();
|
|
display_patch_changes ();
|
|
}
|
|
|
|
void
|
|
MidiRegionView::instrument_settings_changed ()
|
|
{
|
|
redisplay_model();
|
|
}
|
|
|
|
void
|
|
MidiRegionView::cut_copy_clear (Editing::CutCopyOp op)
|
|
{
|
|
if (_selection.empty()) {
|
|
return;
|
|
}
|
|
|
|
PublicEditor& editor (trackview.editor());
|
|
|
|
switch (op) {
|
|
case Delete:
|
|
/* XXX what to do ? */
|
|
break;
|
|
case Cut:
|
|
case Copy:
|
|
editor.get_cut_buffer().add (selection_as_cut_buffer());
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (op != Copy) {
|
|
|
|
start_note_diff_command();
|
|
|
|
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
|
|
switch (op) {
|
|
case Copy:
|
|
break;
|
|
case Delete:
|
|
case Cut:
|
|
case Clear:
|
|
note_diff_remove_note (*i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
apply_diff();
|
|
}
|
|
}
|
|
|
|
MidiCutBuffer*
|
|
MidiRegionView::selection_as_cut_buffer () const
|
|
{
|
|
Notes notes;
|
|
|
|
for (Selection::iterator i = _selection.begin(); i != _selection.end(); ++i) {
|
|
NoteType* n = (*i)->note().get();
|
|
notes.insert (boost::shared_ptr<NoteType> (new NoteType (*n)));
|
|
}
|
|
|
|
MidiCutBuffer* cb = new MidiCutBuffer (trackview.session());
|
|
cb->set (notes);
|
|
|
|
return cb;
|
|
}
|
|
|
|
/** This method handles undo */
|
|
void
|
|
MidiRegionView::paste (framepos_t pos, float times, const MidiCutBuffer& mcb)
|
|
{
|
|
if (mcb.empty()) {
|
|
return;
|
|
}
|
|
|
|
DEBUG_TRACE (DEBUG::CutNPaste, string_compose ("MIDI paste @ %1 times %2\n", pos, times));
|
|
|
|
trackview.session()->begin_reversible_command (_("paste"));
|
|
|
|
start_note_diff_command (_("paste"));
|
|
|
|
Evoral::MusicalTime beat_delta;
|
|
Evoral::MusicalTime paste_pos_beats;
|
|
Evoral::MusicalTime duration;
|
|
Evoral::MusicalTime end_point = 0;
|
|
|
|
duration = (*mcb.notes().rbegin())->end_time() - (*mcb.notes().begin())->time();
|
|
paste_pos_beats = absolute_frames_to_source_beats (pos);
|
|
beat_delta = (*mcb.notes().begin())->time() - paste_pos_beats;
|
|
paste_pos_beats = 0;
|
|
|
|
DEBUG_TRACE (DEBUG::CutNPaste, string_compose ("Paste data spans from %1 to %2 (%3) ; paste pos beats = %4 (based on %5 - %6 ; beat delta = %7\n",
|
|
(*mcb.notes().begin())->time(),
|
|
(*mcb.notes().rbegin())->end_time(),
|
|
duration, pos, _region->position(),
|
|
paste_pos_beats, beat_delta));
|
|
|
|
clear_selection ();
|
|
|
|
for (int n = 0; n < (int) times; ++n) {
|
|
|
|
for (Notes::const_iterator i = mcb.notes().begin(); i != mcb.notes().end(); ++i) {
|
|
|
|
boost::shared_ptr<NoteType> copied_note (new NoteType (*((*i).get())));
|
|
copied_note->set_time (paste_pos_beats + copied_note->time() - beat_delta);
|
|
|
|
/* make all newly added notes selected */
|
|
|
|
note_diff_add_note (copied_note, true);
|
|
end_point = copied_note->end_time();
|
|
}
|
|
|
|
paste_pos_beats += duration;
|
|
}
|
|
|
|
/* if we pasted past the current end of the region, extend the region */
|
|
|
|
framepos_t end_frame = source_beats_to_absolute_frames (end_point);
|
|
framepos_t region_end = _region->position() + _region->length() - 1;
|
|
|
|
if (end_frame > region_end) {
|
|
|
|
DEBUG_TRACE (DEBUG::CutNPaste, string_compose ("Paste extended region from %1 to %2\n", region_end, end_frame));
|
|
|
|
_region->clear_changes ();
|
|
_region->set_length (end_frame - _region->position());
|
|
trackview.session()->add_command (new StatefulDiffCommand (_region));
|
|
}
|
|
|
|
apply_diff (true);
|
|
|
|
trackview.session()->commit_reversible_command ();
|
|
}
|
|
|
|
struct EventNoteTimeEarlyFirstComparator {
|
|
bool operator() (CanvasNoteEvent* a, CanvasNoteEvent* b) {
|
|
return a->note()->time() < b->note()->time();
|
|
}
|
|
};
|
|
|
|
void
|
|
MidiRegionView::time_sort_events ()
|
|
{
|
|
if (!_sort_needed) {
|
|
return;
|
|
}
|
|
|
|
EventNoteTimeEarlyFirstComparator cmp;
|
|
_events.sort (cmp);
|
|
|
|
_sort_needed = false;
|
|
}
|
|
|
|
void
|
|
MidiRegionView::goto_next_note (bool add_to_selection)
|
|
{
|
|
bool use_next = false;
|
|
|
|
if (_events.back()->selected()) {
|
|
return;
|
|
}
|
|
|
|
time_sort_events ();
|
|
|
|
MidiTimeAxisView* const mtv = dynamic_cast<MidiTimeAxisView*>(&trackview);
|
|
uint16_t const channel_mask = mtv->midi_track()->get_playback_channel_mask();
|
|
|
|
for (Events::iterator i = _events.begin(); i != _events.end(); ++i) {
|
|
if ((*i)->selected()) {
|
|
use_next = true;
|
|
continue;
|
|
} else if (use_next) {
|
|
if (channel_mask & (1 << (*i)->note()->channel())) {
|
|
if (!add_to_selection) {
|
|
unique_select (*i);
|
|
} else {
|
|
note_selected (*i, true, false);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* use the first one */
|
|
|
|
if (!_events.empty() && (channel_mask & (1 << _events.front()->note()->channel ()))) {
|
|
unique_select (_events.front());
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::goto_previous_note (bool add_to_selection)
|
|
{
|
|
bool use_next = false;
|
|
|
|
if (_events.front()->selected()) {
|
|
return;
|
|
}
|
|
|
|
time_sort_events ();
|
|
|
|
MidiTimeAxisView* const mtv = dynamic_cast<MidiTimeAxisView*>(&trackview);
|
|
uint16_t const channel_mask = mtv->midi_track()->get_playback_channel_mask ();
|
|
|
|
for (Events::reverse_iterator i = _events.rbegin(); i != _events.rend(); ++i) {
|
|
if ((*i)->selected()) {
|
|
use_next = true;
|
|
continue;
|
|
} else if (use_next) {
|
|
if (channel_mask & (1 << (*i)->note()->channel())) {
|
|
if (!add_to_selection) {
|
|
unique_select (*i);
|
|
} else {
|
|
note_selected (*i, true, false);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* use the last one */
|
|
|
|
if (!_events.empty() && (channel_mask & (1 << (*_events.rbegin())->note()->channel ()))) {
|
|
unique_select (*(_events.rbegin()));
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::selection_as_notelist (Notes& selected, bool allow_all_if_none_selected)
|
|
{
|
|
bool had_selected = false;
|
|
|
|
time_sort_events ();
|
|
|
|
for (Events::iterator i = _events.begin(); i != _events.end(); ++i) {
|
|
if ((*i)->selected()) {
|
|
selected.insert ((*i)->note());
|
|
had_selected = true;
|
|
}
|
|
}
|
|
|
|
if (allow_all_if_none_selected && !had_selected) {
|
|
for (Events::iterator i = _events.begin(); i != _events.end(); ++i) {
|
|
selected.insert ((*i)->note());
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::update_ghost_note (double x, double y)
|
|
{
|
|
MidiTimeAxisView* const mtv = dynamic_cast<MidiTimeAxisView*>(&trackview);
|
|
|
|
_last_ghost_x = x;
|
|
_last_ghost_y = y;
|
|
|
|
_note_group->w2i (x, y);
|
|
|
|
PublicEditor& editor = trackview.editor ();
|
|
|
|
framepos_t const unsnapped_frame = editor.pixel_to_frame (x);
|
|
framecnt_t grid_frames;
|
|
framepos_t const f = snap_frame_to_grid_underneath (unsnapped_frame, grid_frames);
|
|
|
|
/* use region_frames... because we are converting a delta within the region
|
|
*/
|
|
|
|
bool success;
|
|
double length = editor.get_grid_type_as_beats (success, unsnapped_frame);
|
|
|
|
if (!success) {
|
|
length = 1;
|
|
}
|
|
|
|
/* note that this sets the time of the ghost note in beats relative to
|
|
the start of the source; that is how all note times are stored.
|
|
*/
|
|
_ghost_note->note()->set_time (absolute_frames_to_source_beats (f + _region->position ()));
|
|
_ghost_note->note()->set_length (length);
|
|
_ghost_note->note()->set_note (midi_stream_view()->y_to_note (y));
|
|
_ghost_note->note()->set_channel (mtv->get_channel_for_add ());
|
|
|
|
/* the ghost note does not appear in ghost regions, so pass false in here */
|
|
update_note (_ghost_note, false);
|
|
|
|
show_verbose_cursor (_ghost_note->note ());
|
|
}
|
|
|
|
void
|
|
MidiRegionView::create_ghost_note (double x, double y)
|
|
{
|
|
remove_ghost_note ();
|
|
|
|
boost::shared_ptr<NoteType> g (new NoteType);
|
|
_ghost_note = new NoEventCanvasNote (*this, *_note_group, g);
|
|
_ghost_note->property_outline_color_rgba() = 0x000000aa;
|
|
update_ghost_note (x, y);
|
|
_ghost_note->show ();
|
|
|
|
_last_ghost_x = x;
|
|
_last_ghost_y = y;
|
|
|
|
show_verbose_cursor (_ghost_note->note ());
|
|
}
|
|
|
|
void
|
|
MidiRegionView::snap_changed ()
|
|
{
|
|
if (!_ghost_note) {
|
|
return;
|
|
}
|
|
|
|
create_ghost_note (_last_ghost_x, _last_ghost_y);
|
|
}
|
|
|
|
void
|
|
MidiRegionView::drop_down_keys ()
|
|
{
|
|
_mouse_state = None;
|
|
}
|
|
|
|
void
|
|
MidiRegionView::maybe_select_by_position (GdkEventButton* ev, double /*x*/, double y)
|
|
{
|
|
double note = midi_stream_view()->y_to_note(y);
|
|
Events e;
|
|
MidiTimeAxisView* const mtv = dynamic_cast<MidiTimeAxisView*>(&trackview);
|
|
|
|
uint16_t chn_mask = mtv->midi_track()->get_playback_channel_mask();
|
|
|
|
if (Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) {
|
|
get_events (e, Evoral::Sequence<Evoral::MusicalTime>::PitchGreaterThanOrEqual, (uint8_t) floor (note), chn_mask);
|
|
} else if (Keyboard::modifier_state_equals (ev->state, Keyboard::PrimaryModifier)) {
|
|
get_events (e, Evoral::Sequence<Evoral::MusicalTime>::PitchLessThanOrEqual, (uint8_t) floor (note), chn_mask);
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
bool add_mrv_selection = false;
|
|
|
|
if (_selection.empty()) {
|
|
add_mrv_selection = true;
|
|
}
|
|
|
|
for (Events::iterator i = e.begin(); i != e.end(); ++i) {
|
|
if (_selection.insert (*i).second) {
|
|
(*i)->set_selected (true);
|
|
}
|
|
}
|
|
|
|
if (add_mrv_selection) {
|
|
PublicEditor& editor (trackview.editor());
|
|
editor.get_selection().add (this);
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::color_handler ()
|
|
{
|
|
RegionView::color_handler ();
|
|
|
|
for (Events::iterator i = _events.begin(); i != _events.end(); ++i) {
|
|
(*i)->set_selected ((*i)->selected()); // will change color
|
|
}
|
|
|
|
/* XXX probably more to do here */
|
|
}
|
|
|
|
void
|
|
MidiRegionView::enable_display (bool yn)
|
|
{
|
|
RegionView::enable_display (yn);
|
|
if (yn) {
|
|
redisplay_model ();
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::show_step_edit_cursor (Evoral::MusicalTime pos)
|
|
{
|
|
if (_step_edit_cursor == 0) {
|
|
ArdourCanvas::Group* const group = (ArdourCanvas::Group*)get_canvas_group();
|
|
|
|
_step_edit_cursor = new ArdourCanvas::SimpleRect (*group);
|
|
_step_edit_cursor->property_y1() = 0;
|
|
_step_edit_cursor->property_y2() = midi_stream_view()->contents_height();
|
|
_step_edit_cursor->property_fill_color_rgba() = RGBA_TO_UINT (45,0,0,90);
|
|
_step_edit_cursor->property_outline_color_rgba() = RGBA_TO_UINT (85,0,0,90);
|
|
}
|
|
|
|
move_step_edit_cursor (pos);
|
|
_step_edit_cursor->show ();
|
|
}
|
|
|
|
void
|
|
MidiRegionView::move_step_edit_cursor (Evoral::MusicalTime pos)
|
|
{
|
|
_step_edit_cursor_position = pos;
|
|
|
|
if (_step_edit_cursor) {
|
|
double pixel = trackview.editor().frame_to_pixel (region_beats_to_region_frames (pos));
|
|
_step_edit_cursor->property_x1() = pixel;
|
|
set_step_edit_cursor_width (_step_edit_cursor_width);
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::hide_step_edit_cursor ()
|
|
{
|
|
if (_step_edit_cursor) {
|
|
_step_edit_cursor->hide ();
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::set_step_edit_cursor_width (Evoral::MusicalTime beats)
|
|
{
|
|
_step_edit_cursor_width = beats;
|
|
|
|
if (_step_edit_cursor) {
|
|
_step_edit_cursor->property_x2() = _step_edit_cursor->property_x1() + trackview.editor().frame_to_pixel (region_beats_to_region_frames (beats));
|
|
}
|
|
}
|
|
|
|
/** Called when a diskstream on our track has received some data. Update the view, if applicable.
|
|
* @param w Source that the data will end up in.
|
|
*/
|
|
void
|
|
MidiRegionView::data_recorded (boost::weak_ptr<MidiSource> w)
|
|
{
|
|
if (!_active_notes) {
|
|
/* we aren't actively being recorded to */
|
|
return;
|
|
}
|
|
|
|
boost::shared_ptr<MidiSource> src = w.lock ();
|
|
if (!src || src != midi_region()->midi_source()) {
|
|
/* recorded data was not destined for our source */
|
|
return;
|
|
}
|
|
|
|
MidiTimeAxisView* mtv = dynamic_cast<MidiTimeAxisView*> (&trackview);
|
|
|
|
boost::shared_ptr<MidiBuffer> buf = mtv->midi_track()->get_gui_feed_buffer ();
|
|
|
|
BeatsFramesConverter converter (trackview.session()->tempo_map(), mtv->midi_track()->get_capture_start_frame (0));
|
|
|
|
framepos_t back = max_framepos;
|
|
|
|
for (MidiBuffer::iterator i = buf->begin(); i != buf->end(); ++i) {
|
|
Evoral::MIDIEvent<MidiBuffer::TimeType> const ev (*i, false);
|
|
|
|
if (ev.is_channel_event()) {
|
|
if (get_channel_mode() == FilterChannels) {
|
|
if (((uint16_t(1) << ev.channel()) & get_selected_channels()) == 0) {
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ev.time() is in session frames, so (ev.time() - converter.origin_b()) is
|
|
frames from the start of the source, and so time_beats is in terms of the
|
|
source.
|
|
*/
|
|
|
|
Evoral::MusicalTime const time_beats = converter.from (ev.time () - converter.origin_b ());
|
|
|
|
if (ev.type() == MIDI_CMD_NOTE_ON) {
|
|
boost::shared_ptr<NoteType> note (
|
|
new NoteType (ev.channel(), time_beats, 0, ev.note(), ev.velocity()));
|
|
|
|
add_note (note, true);
|
|
|
|
/* fix up our note range */
|
|
if (ev.note() < _current_range_min) {
|
|
midi_stream_view()->apply_note_range (ev.note(), _current_range_max, true);
|
|
} else if (ev.note() > _current_range_max) {
|
|
midi_stream_view()->apply_note_range (_current_range_min, ev.note(), true);
|
|
}
|
|
|
|
} else if (ev.type() == MIDI_CMD_NOTE_OFF) {
|
|
resolve_note (ev.note (), time_beats);
|
|
}
|
|
|
|
back = ev.time ();
|
|
}
|
|
|
|
midi_stream_view()->check_record_layers (region(), back);
|
|
}
|
|
|
|
void
|
|
MidiRegionView::trim_front_starting ()
|
|
{
|
|
/* Reparent the note group to the region view's parent, so that it doesn't change
|
|
when the region view is trimmed.
|
|
*/
|
|
_temporary_note_group = new ArdourCanvas::Group (*group->property_parent ());
|
|
_temporary_note_group->move (group->property_x(), group->property_y());
|
|
_note_group->reparent (*_temporary_note_group);
|
|
}
|
|
|
|
void
|
|
MidiRegionView::trim_front_ending ()
|
|
{
|
|
_note_group->reparent (*group);
|
|
delete _temporary_note_group;
|
|
_temporary_note_group = 0;
|
|
|
|
if (_region->start() < 0) {
|
|
/* Trim drag made start time -ve; fix this */
|
|
midi_region()->fix_negative_start ();
|
|
}
|
|
}
|
|
|
|
void
|
|
MidiRegionView::edit_patch_change (ArdourCanvas::CanvasPatchChange* pc)
|
|
{
|
|
PatchChangeDialog d (&_source_relative_time_converter, trackview.session(), *pc->patch (), instrument_info(), Gtk::Stock::APPLY, true);
|
|
|
|
d.set_position (Gtk::WIN_POS_MOUSE);
|
|
|
|
int response = d.run();
|
|
|
|
switch (response) {
|
|
case Gtk::RESPONSE_ACCEPT:
|
|
break;
|
|
case Gtk::RESPONSE_REJECT:
|
|
delete_patch_change (pc);
|
|
return;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
change_patch_change (pc->patch(), d.patch ());
|
|
}
|
|
|
|
void
|
|
MidiRegionView::delete_sysex (CanvasSysEx* sysex)
|
|
{
|
|
MidiModel::SysExDiffCommand* c = _model->new_sysex_diff_command (_("delete sysex"));
|
|
c->remove (sysex->sysex());
|
|
_model->apply_command (*trackview.session(), c);
|
|
|
|
_sys_exes.clear ();
|
|
display_sysexes();
|
|
}
|
|
|
|
void
|
|
MidiRegionView::show_verbose_cursor (boost::shared_ptr<NoteType> n) const
|
|
{
|
|
using namespace MIDI::Name;
|
|
|
|
std::string name;
|
|
|
|
MidiTimeAxisView* const mtv = dynamic_cast<MidiTimeAxisView*>(&trackview);
|
|
if (mtv) {
|
|
boost::shared_ptr<MasterDeviceNames> device_names(mtv->get_device_names());
|
|
if (device_names) {
|
|
MIDI::Name::PatchPrimaryKey patch_key;
|
|
get_patch_key_at(n->time(), n->channel(), patch_key);
|
|
name = device_names->note_name(mtv->gui_property(X_("midnam-custom-device-mode")),
|
|
n->channel(),
|
|
patch_key.bank_number,
|
|
patch_key.program_number,
|
|
n->note());
|
|
}
|
|
}
|
|
|
|
char buf[128];
|
|
snprintf (buf, sizeof (buf), "%d %s\nCh %d Vel %d",
|
|
(int) n->note (),
|
|
name.empty() ? Evoral::midi_note_name (n->note()).c_str() : name.c_str(),
|
|
(int) n->channel() + 1,
|
|
(int) n->velocity());
|
|
|
|
show_verbose_cursor(buf, 10, 20);
|
|
}
|
|
|
|
void
|
|
MidiRegionView::show_verbose_cursor (string const & text, double xoffset, double yoffset) const
|
|
{
|
|
double wx, wy;
|
|
|
|
trackview.editor().get_pointer_position (wx, wy);
|
|
|
|
wx += xoffset;
|
|
wy += yoffset;
|
|
|
|
/* Flip the cursor above the mouse pointer if it would overlap the bottom of the canvas */
|
|
|
|
double x1, y1, x2, y2;
|
|
trackview.editor().verbose_cursor()->canvas_item()->get_bounds (x1, y1, x2, y2);
|
|
|
|
if ((wy + y2 - y1) > trackview.editor().canvas_height()) {
|
|
wy -= (y2 - y1) + 2 * yoffset;
|
|
}
|
|
|
|
trackview.editor().verbose_cursor()->set (text, wx, wy);
|
|
trackview.editor().verbose_cursor()->show ();
|
|
}
|
|
|
|
/** @param p A session framepos.
|
|
* @param grid_frames Filled in with the number of frames that a grid interval is at p.
|
|
* @return p snapped to the grid subdivision underneath it.
|
|
*/
|
|
framepos_t
|
|
MidiRegionView::snap_frame_to_grid_underneath (framepos_t p, framecnt_t& grid_frames) const
|
|
{
|
|
PublicEditor& editor = trackview.editor ();
|
|
|
|
bool success;
|
|
Evoral::MusicalTime grid_beats = editor.get_grid_type_as_beats (success, p);
|
|
|
|
if (!success) {
|
|
grid_beats = 1;
|
|
}
|
|
|
|
grid_frames = region_beats_to_region_frames (grid_beats);
|
|
|
|
/* Hack so that we always snap to the note that we are over, instead of snapping
|
|
to the next one if we're more than halfway through the one we're over.
|
|
*/
|
|
if (editor.snap_mode() == SnapNormal && p >= grid_frames / 2) {
|
|
p -= grid_frames / 2;
|
|
}
|
|
|
|
return snap_frame_to_frame (p);
|
|
}
|
|
|
|
/** Called when the selection has been cleared in any MidiRegionView.
|
|
* @param rv MidiRegionView that the selection was cleared in.
|
|
*/
|
|
void
|
|
MidiRegionView::selection_cleared (MidiRegionView* rv)
|
|
{
|
|
if (rv == this) {
|
|
return;
|
|
}
|
|
|
|
/* Clear our selection in sympathy; but don't signal the fact */
|
|
clear_selection (false);
|
|
}
|
|
|
|
void
|
|
MidiRegionView::note_button_release ()
|
|
{
|
|
delete _note_player;
|
|
_note_player = 0;
|
|
}
|
|
|
|
ChannelMode
|
|
MidiRegionView::get_channel_mode () const
|
|
{
|
|
RouteTimeAxisView* rtav = dynamic_cast<RouteTimeAxisView*> (&trackview);
|
|
return rtav->midi_track()->get_playback_channel_mode();
|
|
}
|
|
|
|
uint16_t
|
|
MidiRegionView::get_selected_channels () const
|
|
{
|
|
RouteTimeAxisView* rtav = dynamic_cast<RouteTimeAxisView*> (&trackview);
|
|
return rtav->midi_track()->get_playback_channel_mask();
|
|
}
|
|
|