From 77cefd7721fff0e545af90529780faf3e840ee29 Mon Sep 17 00:00:00 2001 From: Paul Davis Date: Sat, 11 Mar 2023 20:42:57 -0700 Subject: [PATCH] new tempo mapping drag objects (API and implementation) --- gtk2_ardour/editor.h | 6 +- gtk2_ardour/editor_canvas.cc | 1 + gtk2_ardour/editor_drag.cc | 327 +++++++++++++++++++++++++++++------ gtk2_ardour/editor_drag.h | 76 +++++++- gtk2_ardour/editor_mouse.cc | 155 +++++++++++++++-- 5 files changed, 490 insertions(+), 75 deletions(-) diff --git a/gtk2_ardour/editor.h b/gtk2_ardour/editor.h index 2e6f6cac08..072287793c 100644 --- a/gtk2_ardour/editor.h +++ b/gtk2_ardour/editor.h @@ -2523,6 +2523,8 @@ private: void remove_gap_marker_callback (Temporal::timepos_t at, Temporal::timecnt_t distance); + void choose_mapping_drag (ArdourCanvas::Item*, GdkEvent*); + template Temporal::TimeDomain drag_time_domain (T* thing_with_time_domain) { return thing_with_time_domain ? thing_with_time_domain->time_domain() : Temporal::AudioTime; @@ -2538,7 +2540,9 @@ private: friend class RegionDrag; friend class RegionMoveDrag; friend class TrimDrag; - friend class MappingDrag; + friend class MappingTwistDrag; + friend class MappingLinearDrag; + friend class MappingStretchDrag; friend class MeterMarkerDrag; friend class BBTMarkerDrag; friend class TempoMarkerDrag; diff --git a/gtk2_ardour/editor_canvas.cc b/gtk2_ardour/editor_canvas.cc index 2e09ddf9a6..303fd9402b 100644 --- a/gtk2_ardour/editor_canvas.cc +++ b/gtk2_ardour/editor_canvas.cc @@ -261,6 +261,7 @@ Editor::initialize_canvas () tempo_bar->Event.connect (sigc::bind (sigc::mem_fun (*this, &Editor::canvas_ruler_bar_event), tempo_bar, TempoBarItem, "tempo bar")); mapping_bar->Event.connect (sigc::bind (sigc::mem_fun (*this, &Editor::canvas_ruler_bar_event), mapping_bar, MappingBarItem, "mapping bar")); + mapping_cursor->Event.connect (sigc::bind (sigc::mem_fun (*this, &Editor::canvas_ruler_bar_event), mapping_cursor, MappingCursorItem, "mapping cursor")); meter_bar->Event.connect (sigc::bind (sigc::mem_fun (*this, &Editor::canvas_ruler_bar_event), meter_bar, MeterBarItem, "meter bar")); marker_bar->Event.connect (sigc::bind (sigc::mem_fun (*this, &Editor::canvas_ruler_bar_event), marker_bar, MarkerBarItem, "marker bar")); cd_marker_bar->Event.connect (sigc::bind (sigc::mem_fun (*this, &Editor::canvas_ruler_bar_event), cd_marker_bar, CdMarkerBarItem, "cd marker bar")); diff --git a/gtk2_ardour/editor_drag.cc b/gtk2_ardour/editor_drag.cc index 10c9ee0a40..9894844dcf 100644 --- a/gtk2_ardour/editor_drag.cc +++ b/gtk2_ardour/editor_drag.cc @@ -3331,7 +3331,6 @@ TempoMarkerDrag::motion (GdkEvent* event, bool first_move) } } - void TempoMarkerDrag::finished (GdkEvent* event, bool movement_occurred) { @@ -3475,32 +3474,34 @@ BBTMarkerDrag::aborted (bool moved) } } -MappingDrag::MappingDrag (Editor* e, ArdourCanvas::Item* i) +/******************************************************************************/ + +MappingLinearDrag::MappingLinearDrag (Editor* e, ArdourCanvas::Item* i, Temporal::TempoMap::WritableSharedPtr& wmap) : Drag (e, i, Temporal::BeatTime) , _tempo (0) + , _grab_bpm (0) + , map (wmap) , _before_state (0) , _drag_valid (true) { - DEBUG_TRACE (DEBUG::Drags, "New MappingDrag\n"); + DEBUG_TRACE (DEBUG::Drags, "New MappingLinearDrag\n"); } void -MappingDrag::start_grab (GdkEvent* event, Gdk::Cursor* cursor) +MappingLinearDrag::start_grab (GdkEvent* event, Gdk::Cursor* cursor) { - map = _editor->begin_tempo_mapping (); - Drag::start_grab (event, cursor); _tempo = const_cast (&map->metric_at (raw_grab_time().beats()).tempo()); + _grab_bpm = _tempo->note_types_per_minute(); if (adjusted_current_time (event, false) <= _tempo->time()) { + std::cerr << "too early for " << *_tempo << std::endl; _drag_valid = false; return; } - _editor->tempo_curve_selected (_tempo, true); - ostringstream sstr; if (_tempo->continuing()) { TempoPoint const * prev = map->previous_tempo (*_tempo); @@ -3514,12 +3515,12 @@ MappingDrag::start_grab (GdkEvent* event, Gdk::Cursor* cursor) } void -MappingDrag::setup_pointer_offset () +MappingLinearDrag::setup_pointer_offset () { /* get current state */ _before_state = &map->get_state(); - _grab_qn = max (Beats(), raw_grab_time().beats()); + Beats grab_qn = max (Beats(), raw_grab_time().beats()); uint32_t divisions = _editor->get_grid_beat_divisions (_editor->grid_type()); @@ -3527,52 +3528,33 @@ MappingDrag::setup_pointer_offset () divisions = 4; } - _grab_qn = _grab_qn.round_to_subdivision (divisions, Temporal::RoundDownAlways); - _pointer_offset = timepos_t (_grab_qn).distance (raw_grab_time()); + grab_qn = grab_qn.round_to_subdivision (divisions, Temporal::RoundDownAlways); + _pointer_offset = timepos_t (grab_qn).distance (raw_grab_time()); } void -MappingDrag::motion (GdkEvent* event, bool first_move) +MappingLinearDrag::motion (GdkEvent* event, bool first_move) { if (!_drag_valid) { return; } if (first_move) { - _editor->begin_reversible_command (_("stretch tempo")); + _editor->begin_reversible_command (_("map tempo")); } - timepos_t pf; - - if (_editor->grid_musical()) { - pf = adjusted_current_time (event, false); - } else { - pf = adjusted_current_time (event); - } - - if (ArdourKeyboard::modifier_state_equals (event->button.state, Keyboard::PrimaryModifier)) { - /* adjust previous tempo to match pointer sample */ - map->stretch_tempo (_tempo, timepos_t (_grab_qn).samples(), pf.samples(), _grab_qn, pf.beats()); - _editor->mid_tempo_change (Editor::BBTChanged); - } - - ostringstream sstr; - if (_tempo->continuing()) { - TempoPoint const * prev = map->previous_tempo (*_tempo); - if (prev) { - _editor->tempo_curve_selected (prev, true); - sstr << "end: " << fixed << setprecision(3) << prev->end_note_types_per_minute() << "\n"; - } - } - sstr << "start: " << fixed << setprecision(3) << _tempo->note_types_per_minute(); - show_verbose_cursor_text (sstr.str()); + double new_bpm = std::max (1.5, _grab_bpm - ((current_pointer_x() - grab_x()) / 5.0)); + stringstream strs; + Temporal::Tempo new_tempo (new_bpm, _tempo->note_type()); + map->change_tempo (*_tempo, new_tempo); + _editor->mid_tempo_change (Editor::MappingChanged); } void -MappingDrag::finished (GdkEvent* event, bool movement_occurred) +MappingLinearDrag::finished (GdkEvent* event, bool movement_occurred) { if (!_drag_valid) { - _editor->abort_tempo_map_edit (); + _editor->abort_tempo_mapping (); return; } @@ -3580,21 +3562,12 @@ MappingDrag::finished (GdkEvent* event, bool movement_occurred) /* click, no drag */ - _editor->abort_tempo_map_edit (); + _editor->abort_tempo_mapping (); + _editor->session()->request_locate (grab_sample(), false, _was_rolling ? MustRoll : RollIfAppropriate); return; } else { - _editor->tempo_curve_selected (_tempo, false); - - if (_tempo->continuing()) { - - TempoPoint const * prev_tempo = map->previous_tempo (*_tempo); - - if (prev_tempo) { - _editor->tempo_curve_selected (prev_tempo, false); - } - } } XMLNode &after = map->get_state(); @@ -3612,7 +3585,253 @@ MappingDrag::finished (GdkEvent* event, bool movement_occurred) } void -MappingDrag::aborted (bool moved) +MappingLinearDrag::aborted (bool moved) +{ + _editor->abort_tempo_mapping (); +} + +/******************************************************************************/ + +MappingStretchDrag::MappingStretchDrag (Editor* e, ArdourCanvas::Item* i, Temporal::TempoMap::WritableSharedPtr& wmap) + : Drag (e, i, Temporal::BeatTime) + , _tempo (0) + , map (wmap) + , _before_state (0) + , _drag_valid (true) +{ + DEBUG_TRACE (DEBUG::Drags, "New MappingStretchDrag\n"); + +} + +void +MappingStretchDrag::start_grab (GdkEvent* event, Gdk::Cursor* cursor) +{ + Drag::start_grab (event, cursor); + + _tempo = const_cast (&map->metric_at (raw_grab_time().beats()).tempo()); + + if (adjusted_current_time (event, false) <= _tempo->time()) { + std::cerr << "too early for " << *_tempo << std::endl; + _drag_valid = false; + return; + } + + ostringstream sstr; + if (_tempo->continuing()) { + TempoPoint const * prev = map->previous_tempo (*_tempo); + if (prev) { + sstr << "end: " << fixed << setprecision(3) << prev->end_note_types_per_minute() << "\n"; + } + } + + sstr << "start: " << fixed << setprecision(3) << _tempo->note_types_per_minute(); + show_verbose_cursor_text (sstr.str()); +} + +void +MappingStretchDrag::setup_pointer_offset () +{ + /* get current state */ + _before_state = &map->get_state(); + + _grab_qn = max (Beats(), raw_grab_time().beats()); + + uint32_t divisions = _editor->get_grid_beat_divisions (_editor->grid_type()); + + if (divisions == 0) { + divisions = 4; + } + + _grab_qn = _grab_qn.round_to_subdivision (divisions, Temporal::RoundDownAlways); + _pointer_offset = timepos_t (_grab_qn).distance (raw_grab_time()); +} + +void +MappingStretchDrag::motion (GdkEvent* event, bool first_move) +{ + if (!_drag_valid) { + return; + } + + if (first_move) { + _editor->begin_reversible_command (_("map tempo w/stretch")); + } + + timepos_t pf; + + if (_editor->grid_musical()) { + pf = adjusted_current_time (event, false); + } else { + pf = adjusted_current_time (event); + } + + map->stretch_tempo (_tempo, timepos_t (_grab_qn).samples(), pf.samples(), _grab_qn, pf.beats()); + _editor->mapping_cursor->set_position (Duple (_editor->time_to_pixel_unrounded (pf), _editor->mapping_cursor->position().y)); + _editor->mid_tempo_change (Editor::MappingChanged); +} + +void +MappingStretchDrag::finished (GdkEvent* event, bool movement_occurred) +{ + if (!movement_occurred) { + + /* click, no drag */ + + _editor->abort_tempo_mapping (); + _editor->session()->request_locate (grab_sample(), false, _was_rolling ? MustRoll : RollIfAppropriate); + return; + } + + if (!_drag_valid) { + _editor->abort_tempo_mapping (); + return; + } + + XMLNode &after = map->get_state(); + + _editor->session()->add_command (new Temporal::TempoCommand (_("move BBT point"), _before_state, &after)); + _editor->commit_reversible_command (); + + /* 2nd argument means "update tempo map display after the new map is + * installed. We need to do this because the code above has not + * actually changed anything about how tempo is displayed, it simply + * modified the map. + */ + + _editor->commit_tempo_mapping (map); +} + +void +MappingStretchDrag::aborted (bool moved) +{ + _editor->abort_tempo_mapping (); +} + +/******************************************************************************/ + +MappingTwistDrag::MappingTwistDrag (Editor* e, ArdourCanvas::Item* i, Temporal::TempoMap::WritableSharedPtr& wmap, + TempoPoint& prv, + TempoPoint& fcus, + TempoPoint& nxt) + : Drag (e, i, Temporal::BeatTime) + , prev (prv) + , focus (fcus) + , next (nxt) + , map (wmap) + , direction (0.) + , delta (0.) + , _before_state (0) + , _drag_valid (true) +{ + DEBUG_TRACE (DEBUG::Drags, "New MappingTwistDrag\n"); + initial_npm = focus.note_types_per_minute (); +} + +void +MappingTwistDrag::start_grab (GdkEvent* event, Gdk::Cursor* cursor) +{ + Drag::start_grab (event, cursor); +} + +void +MappingTwistDrag::setup_pointer_offset () +{ + /* get current state */ + _before_state = &map->get_state(); + + Beats grab_qn = max (Beats(), raw_grab_time().beats()); + + uint32_t divisions = _editor->get_grid_beat_divisions (_editor->grid_type()); + + if (divisions == 0) { + divisions = 4; + } + + grab_qn = grab_qn.round_to_subdivision (divisions, Temporal::RoundDownAlways); + _pointer_offset = timepos_t (grab_qn).distance (raw_grab_time()); +} + +void +MappingTwistDrag::motion (GdkEvent* event, bool first_move) +{ + if (first_move) { + _editor->begin_reversible_command (_("map tempo w/twist")); + } + + samplepos_t mouse_pos; + + if (_editor->grid_musical()) { + mouse_pos = adjusted_current_time (event, false).samples(); + } else { + mouse_pos = adjusted_current_time (event).samples(); + } + + double pixels_moved = _drags->current_pointer_x() - last_pointer_x(); + + if (_drags->current_pointer_x() < last_pointer_x()) { + if (direction < 0.) { + direction = 1.; + initial_npm += delta; + delta = 0.; + } + } else { + if (direction >= 0.) { + direction = -1.; + initial_npm += delta; + delta = 0.; + } + } + + if (_drags->current_pointer_time() >= timepos_t::from_superclock (next.sclock())) { + return; + } + if (_drags->current_pointer_time() <= timepos_t::from_superclock (prev.sclock())) { + return; + } + + /* XXX needs to scale somehow with zoom level */ + + delta += 0.75 * (last_pointer_x() - _drags->current_pointer_x()); + + map->twist_tempi (prev, focus, next, initial_npm + delta); + + // _editor->mapping_cursor->set_position (Duple (_editor->sample_to_pixel_unrounded (mouse_pos), _editor->mapping_cursor->position().y)); + _editor->mid_tempo_change (Editor::MappingChanged); +} + +void +MappingTwistDrag::finished (GdkEvent* event, bool movement_occurred) +{ + if (!movement_occurred) { + + /* click, no drag */ + + _editor->abort_tempo_mapping (); + _editor->session()->request_locate (grab_sample(), false, _was_rolling ? MustRoll : RollIfAppropriate); + return; + } + + if (!_drag_valid) { + _editor->abort_tempo_mapping (); + return; + } + + XMLNode &after = map->get_state(); + + _editor->session()->add_command (new Temporal::TempoCommand (_("move BBT point"), _before_state, &after)); + _editor->commit_reversible_command (); + + /* 2nd argument means "update tempo map display after the new map is + * installed. We need to do this because the code above has not + * actually changed anything about how tempo is displayed, it simply + * modified the map. + */ + + _editor->commit_tempo_mapping (map); +} + +void +MappingTwistDrag::aborted (bool moved) { _editor->abort_tempo_mapping (); } @@ -3634,8 +3853,6 @@ TempoTwistDrag::start_grab (GdkEvent* event, Gdk::Cursor* cursor) { Drag::start_grab (event, cursor); - map = _editor->begin_tempo_mapping (); - /* Get the tempo point that starts this section */ _tempo = const_cast (&map->tempo_at (raw_grab_time())); @@ -3685,7 +3902,7 @@ TempoTwistDrag::motion (GdkEvent* event, bool first_move) } /* adjust this and the next tempi to match pointer sample */ - map->twist_tempi (_tempo, adjusted_time (grab_time(), 0, false).samples(), mouse_pos); + // map->twist_tempi (_tempo, adjusted_time (grab_time(), 0, false).samples(), mouse_pos); ostringstream sstr; sstr << "start: " << fixed << setprecision(3) << _tempo->note_types_per_minute() << "\n"; diff --git a/gtk2_ardour/editor_drag.h b/gtk2_ardour/editor_drag.h index f483c9f00f..b3cb0950e4 100644 --- a/gtk2_ardour/editor_drag.h +++ b/gtk2_ardour/editor_drag.h @@ -913,12 +913,10 @@ private: XMLNode* _before_state; }; - -/** BBT Ruler drag */ -class MappingDrag : public Drag +class MappingLinearDrag : public Drag { public: - MappingDrag (Editor *, ArdourCanvas::Item *); + MappingLinearDrag (Editor *, ArdourCanvas::Item *, Temporal::TempoMap::WritableSharedPtr&); void start_grab (GdkEvent *, Gdk::Cursor* c = 0); void motion (GdkEvent *, bool); @@ -936,14 +934,82 @@ public: void setup_pointer_offset (); private: - Temporal::Beats _grab_qn; Temporal::TempoPoint* _tempo; + double _grab_bpm; Temporal::TempoMap::WritableSharedPtr map; XMLNode* _before_state; bool _drag_valid; }; +class MappingStretchDrag : public Drag +{ +public: + MappingStretchDrag (Editor *, ArdourCanvas::Item *, Temporal::TempoMap::WritableSharedPtr&); + + void start_grab (GdkEvent *, Gdk::Cursor* c = 0); + void motion (GdkEvent *, bool); + void finished (GdkEvent *, bool); + void aborted (bool); + + bool allow_vertical_autoscroll () const { + return false; + } + + bool y_movement_matters () const { + return false; + } + + void setup_pointer_offset (); + +private: + Temporal::TempoPoint* _tempo; + Temporal::TempoMap::WritableSharedPtr map; + Temporal::Beats _grab_qn; + + XMLNode* _before_state; + bool _drag_valid; +}; + +class MappingTwistDrag : public Drag +{ +public: + MappingTwistDrag (Editor *, ArdourCanvas::Item *, Temporal::TempoMap::WritableSharedPtr&, + Temporal::TempoPoint& prev, + Temporal::TempoPoint& focus, + Temporal::TempoPoint& next); + + void start_grab (GdkEvent *, Gdk::Cursor* c = 0); + void motion (GdkEvent *, bool); + void finished (GdkEvent *, bool); + void aborted (bool); + + bool allow_vertical_autoscroll () const { + return false; + } + + bool y_movement_matters () const { + return false; + } + + void setup_pointer_offset (); + +private: + Temporal::TempoPoint& prev; + Temporal::TempoPoint& focus; + Temporal::TempoPoint& next; + double _grab_bpm; + Temporal::TempoMap::WritableSharedPtr map; + + double direction; + double delta; + double initial_npm; + + XMLNode* _before_state; + bool _drag_valid; +}; + + /** tempo curve twist drag */ class TempoTwistDrag : public Drag { diff --git a/gtk2_ardour/editor_mouse.cc b/gtk2_ardour/editor_mouse.cc index 15074dd26e..63a639c3e4 100644 --- a/gtk2_ardour/editor_mouse.cc +++ b/gtk2_ardour/editor_mouse.cc @@ -817,14 +817,8 @@ Editor::button_press_handler_1 (ArdourCanvas::Item* item, GdkEvent* event, ItemT break; case MappingBarItem: - if (!Keyboard::modifier_state_equals (event->button.state, Keyboard::PrimaryModifier) - && !ArdourKeyboard::indicates_constraint (event->button.state)) { - _drags->set (new CursorDrag (this, *_playhead_cursor, false), event); - } else if (Keyboard::modifier_state_equals (event->button.state, Keyboard::PrimaryModifier|Keyboard::TertiaryModifier)) { - _drags->set (new TempoTwistDrag (this, item), event); - } else if (Keyboard::modifier_state_contains (event->button.state, Keyboard::PrimaryModifier)) { - _drags->set (new MappingDrag (this, item), event); - } + case MappingCursorItem: + choose_mapping_drag (item, event); return true; case TempoBarItem: @@ -1698,6 +1692,7 @@ Editor::button_release_handler (ArdourCanvas::Item* item, GdkEvent* event, ItemT case CdMarkerBarItem: case TempoBarItem: case MappingBarItem: + case MappingCursorItem: case TempoCurveItem: case MeterBarItem: case VideoBarItem: @@ -1823,7 +1818,9 @@ Editor::button_release_handler (ArdourCanvas::Item* item, GdkEvent* event, ItemT mouse_add_new_marker (where, Location::IsCueMarker); } return true; + case MappingBarItem: + case MappingCursorItem: return true; case TempoBarItem: @@ -1987,8 +1984,13 @@ Editor::enter_handler (ArdourCanvas::Item* item, GdkEvent* event, ItemType item_ choose_canvas_cursor_on_entry (item_type); switch (item_type) { + case MappingCursorItem: + /* nothing to do ??? */ + break; + case MappingBarItem: mapping_cursor->show (); + mapping_cursor->raise_to_top (); break; case ControlPointItem: @@ -2136,6 +2138,10 @@ Editor::leave_handler (ArdourCanvas::Item* item, GdkEvent*, ItemType item_type) } switch (item_type) { + case MappingCursorItem: + /* ignore */ + break; + case MappingBarItem: mapping_cursor->hide (); break; @@ -2324,9 +2330,11 @@ Editor::motion_handler (ArdourCanvas::Item* item, GdkEvent* event, bool from_aut //drags change the snapped_cursor location, because we are snapping the thing being dragged, not the actual mouse cursor return _drags->motion_handler (event, from_autoscroll); } else { + bool ignored; bool peaks_visible = false; samplepos_t where; + if (mouse_sample (where, ignored)) { /* display peaks */ @@ -2338,14 +2346,48 @@ Editor::motion_handler (ArdourCanvas::Item* item, GdkEvent* event, bool from_aut } } - /* the snapped_cursor shows where an operation (like Split) is going to occur */ timepos_t t (where); - snap_to_with_modifier (t, event); - set_snapped_cursor_position (t); + bool move_snapped_cursor = true; - if (item == mapping_bar) { - double const new_pos = sample_to_pixel_unrounded (where); - mapping_cursor->set_center (ArdourCanvas::Duple (new_pos, mapping_cursor->center().y)); + if (item == mapping_bar || item == mapping_cursor) { + + /* Snap to the nearest beat, and figure out how + * many pixels from the pointer cursor that is. + */ + + timepos_t snapped = _snap_to_bbt (t, RoundNearest, SnapToGrid_Unscaled, GridTypeBeat); + const double unsnapped_pos = time_to_pixel_unrounded (t); + const double snapped_pos = time_to_pixel_unrounded (snapped); + + if (std::abs (snapped_pos - unsnapped_pos) < 10 * UIConfiguration::instance().get_ui_scale()) { + + /* Close to a beat, so snap the mapping + * cursor *and* the snapped cursor to + * the beat. + */ + + mapping_cursor->show (); + mapping_cursor->raise_to_top (); + + mapping_cursor->set_position (ArdourCanvas::Duple (snapped_pos, mapping_cursor->position().y)); + set_snapped_cursor_position (snapped); + + move_snapped_cursor = false; + + } else { + + /* Not close to a beat, hide the + * mapping cursor, then move the + * snapped cursor as normal. + */ + + mapping_cursor->hide (); + } + } + + if (move_snapped_cursor) { + snap_to_with_modifier (t, event); + set_snapped_cursor_position (t); } } @@ -2901,3 +2943,88 @@ Editor::get_pointer_position (double& x, double& y) const _track_canvas->get_pointer (px, py); _track_canvas->window_to_canvas (px, py, x, y); } + +void +Editor::choose_mapping_drag (ArdourCanvas::Item* item, GdkEvent* event) +{ + if (item != mapping_cursor && item != mapping_bar) { + return; + } + + Temporal::TempoMap::WritableSharedPtr map = begin_tempo_mapping (); + + if (item == mapping_bar) { + /* Drag on the bar, not the cursor: just adjust tempo up or + * down. + */ + _drags->set (new MappingLinearDrag (this, item, map), event); + std::cerr << ":Linear\n"; + return; + } + + /* Decide between a tempo twist drag, which we do if the + * pointer is between two tempo markers, and a tempo stretch + * drag, which we do if the pointer is after the last tempo + * marker before the end of the map or a BBT Marker. + */ + + timepos_t pointer_time (canvas_event_sample (event, nullptr, nullptr)); + Temporal::TempoPoint& tempo = const_cast(map->tempo_at (pointer_time)); + + TempoPoint* after = const_cast (map->next_tempo (tempo)); + + if (!after || dynamic_cast(after)) { + /* This is the final tempo, or the next one is a BBT marker. + * No twisting, just stretch this one. + */ + std::cerr << "stretch!\n"; + _drags->set (new MappingStretchDrag (this, item, map), event); + std::cerr << ":Stretch\n"; + return; + } + + std::cerr << "Pointer time: " << pointer_time << std::endl; + + BBT_Argument bbt = map->bbt_at (pointer_time); + std::cerr << " bbt " << bbt << std::endl; + bbt = BBT_Argument (bbt.reference(), bbt.round_to_beat ()); + std::cerr << "rounded to " << bbt << " vs. " << tempo.bbt() << std::endl; + + TempoPoint* before; + TempoPoint* focus; + + if (tempo.bbt() < bbt) { + + /* Add a new tempo marker at the nearest beat point + (essentially the snapped grab point for the drag), so that + it becomes the middle one of three used by the twist tempo + operation. + */ + + before = const_cast (&tempo); + Tempo copied_no_ramp (map->tempo_at (bbt)); + TempoPoint& added = const_cast (map->set_tempo (copied_no_ramp, bbt)); + focus = &added; + std::cerr << "Focus will be " << *focus << std::endl; + reset_tempo_marks (); + + } else { + + before = const_cast (map->previous_tempo (tempo)); + + if (!before) { + return; + } + + focus = &tempo; + } + + std::cerr << "Prev: " << *before << std::endl; + std::cerr << "Focus: " << *focus << std::endl; + std::cerr << "Next: " << *after << std::endl; + + std::cerr << "focus says next at " << focus->superclock_at (after->beats()) << " vs. " << after->sclock() << std::endl; + + _drags->set (new MappingTwistDrag (this, item, map, *before, *focus, *after), event); + std::cerr << ":Twist\n"; +}