tempo editing updates
authornick_m <mainsbridge@gmail.com>
Sat, 4 Mar 2017 18:21:56 +0000 (05:21 +1100)
committernick_m <mainsbridge@gmail.com>
Sat, 4 Mar 2017 18:21:56 +0000 (05:21 +1100)
- a tempo marker may now be set to always continue (clamped)
  this means that the end tempo of the previous section will
  track the start tempo during tempo ops.
  it mimics the behaviour in 5.8, with the gui indicating
  the curves to be changed.

gtk2_ardour/editor.h
gtk2_ardour/editor_drag.cc
gtk2_ardour/editor_drag.h
gtk2_ardour/editor_markers.cc
gtk2_ardour/editor_tempodisplay.cc
libs/ardour/ardour/tempo.h
libs/ardour/tempo.cc

index ec781e2df602bd578153c8347b144eccfb653e7e..917dacb53e2d7bd1080ddc162ae1bc42327350ee 100644 (file)
@@ -1688,6 +1688,7 @@ class Editor : public PublicEditor, public PBD::ScopedConnectionList, public ARD
        void marker_menu_rename ();
        void rename_marker (ArdourMarker *marker);
        void toggle_marker_lock_style ();
+       void toggle_tempo_clamped ();
        void toggle_tempo_type ();
        void continue_previous_tempo ();
        void ramp_to_next_tempo ();
index cd60f721bf6d3cb36e1c544623f638d9741a243f..1538457f7ebb6347eeab71227ca145b6f90541b5 100644 (file)
@@ -3364,7 +3364,7 @@ TempoMarkerDrag::TempoMarkerDrag (Editor* e, ArdourCanvas::Item* i, bool c)
        , _copy (c)
        , _grab_bpm (120.0, 4.0)
        , _grab_qn (0.0)
-       , before_state (0)
+       , _before_state (0)
 {
        DEBUG_TRACE (DEBUG::Drags, "New TempoMarkerDrag\n");
 
@@ -3423,7 +3423,7 @@ TempoMarkerDrag::motion (GdkEvent* event, bool first_move)
                _marker->hide();
 
                /* get current state */
-               before_state = &map.get_state();
+               _before_state = &map.get_state();
 
                if (!_copy) {
                        _editor->begin_reversible_command (_("move tempo mark"));
@@ -3451,7 +3451,7 @@ TempoMarkerDrag::motion (GdkEvent* event, bool first_move)
        if (ArdourKeyboard::indicates_constraint (event->button.state) && ArdourKeyboard::indicates_copy (event->button.state)) {
                double new_bpm = max (1.5, _grab_bpm.end_note_types_per_minute() + ((grab_y() - min (-1.0, current_pointer_y())) / 5.0));
                stringstream strs;
-               _editor->session()->tempo_map().gui_change_tempo (_real_section, Tempo (new_bpm, _real_section->note_type()), true);
+               _editor->session()->tempo_map().gui_change_tempo (_real_section, Tempo (_real_section->note_types_per_minute(), _real_section->note_type(), new_bpm));
                strs << "end:" << fixed << setprecision(3) << new_bpm;
                show_verbose_cursor_text (strs.str());
 
@@ -3459,7 +3459,7 @@ TempoMarkerDrag::motion (GdkEvent* event, bool first_move)
                /* use vertical movement to alter tempo .. should be log */
                double new_bpm = max (1.5, _grab_bpm.note_types_per_minute() + ((grab_y() - min (-1.0, current_pointer_y())) / 5.0));
                stringstream strs;
-               _editor->session()->tempo_map().gui_change_tempo (_real_section, Tempo (new_bpm, _real_section->note_type()), false);
+               _editor->session()->tempo_map().gui_change_tempo (_real_section, Tempo (new_bpm, _real_section->note_type(), _real_section->end_note_types_per_minute()));
                strs << "start:" << fixed << setprecision(3) << new_bpm;
                show_verbose_cursor_text (strs.str());
 
@@ -3501,7 +3501,7 @@ TempoMarkerDrag::finished (GdkEvent* event, bool movement_occurred)
        TempoMap& map (_editor->session()->tempo_map());
 
        XMLNode &after = map.get_state();
-       _editor->session()->add_command (new MementoCommand<TempoMap>(map, before_state, &after));
+       _editor->session()->add_command (new MementoCommand<TempoMap>(map, _before_state, &after));
        _editor->commit_reversible_command ();
 
        // delete the dummy marker we used for visual representation while moving.
@@ -3515,7 +3515,7 @@ TempoMarkerDrag::aborted (bool moved)
        _marker->set_position (_marker->tempo().frame());
        if (moved) {
                TempoMap& map (_editor->session()->tempo_map());
-               map.set_state (*before_state, Stateful::current_state_version);
+               map.set_state (*_before_state, Stateful::current_state_version);
                // delete the dummy (hidden) marker we used for events while moving.
                delete _marker;
        }
@@ -3525,7 +3525,7 @@ BBTRulerDrag::BBTRulerDrag (Editor* e, ArdourCanvas::Item* i)
        : Drag (e, i)
        , _grab_qn (0.0)
        , _tempo (0)
-       , before_state (0)
+       , _before_state (0)
 {
        DEBUG_TRACE (DEBUG::Drags, "New BBTRulerDrag\n");
 
@@ -3540,6 +3540,14 @@ BBTRulerDrag::start_grab (GdkEvent* event, Gdk::Cursor* cursor)
        _editor->tempo_curve_selected (_tempo, true);
 
        ostringstream sstr;
+       if (_tempo->clamped()) {
+               TempoSection* prev = map.previous_tempo_section (_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());
 }
@@ -3548,6 +3556,9 @@ void
 BBTRulerDrag::setup_pointer_frame_offset ()
 {
        TempoMap& map (_editor->session()->tempo_map());
+       /* get current state */
+       _before_state = &map.get_state();
+
        const double beat_at_frame = max (0.0, map.beat_at_frame (raw_grab_frame()));
        const uint32_t divisions = _editor->get_grid_beat_divisions (0);
        double beat = 0.0;
@@ -3574,8 +3585,6 @@ BBTRulerDrag::motion (GdkEvent* event, bool first_move)
        TempoMap& map (_editor->session()->tempo_map());
 
        if (first_move) {
-               /* get current state */
-               before_state = &map.get_state();
                _editor->begin_reversible_command (_("stretch tempo"));
        }
 
@@ -3591,7 +3600,15 @@ BBTRulerDrag::motion (GdkEvent* event, bool first_move)
                /* adjust previous tempo to match pointer frame */
                _editor->session()->tempo_map().gui_stretch_tempo (_tempo, map.frame_at_quarter_note (_grab_qn), pf);
        }
+
        ostringstream sstr;
+       if (_tempo->clamped()) {
+               TempoSection* prev = map.previous_tempo_section (_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());
 }
@@ -3606,16 +3623,23 @@ BBTRulerDrag::finished (GdkEvent* event, bool movement_occurred)
        TempoMap& map (_editor->session()->tempo_map());
 
        XMLNode &after = map.get_state();
-       _editor->session()->add_command(new MementoCommand<TempoMap>(map, before_state, &after));
+       _editor->session()->add_command(new MementoCommand<TempoMap>(map, _before_state, &after));
        _editor->commit_reversible_command ();
        _editor->tempo_curve_selected (_tempo, false);
+
+       if (_tempo->clamped()) {
+               TempoSection* prev_tempo = map.previous_tempo_section (_tempo);
+               if (prev_tempo) {
+                       _editor->tempo_curve_selected (prev_tempo, false);
+               }
+       }
 }
 
 void
 BBTRulerDrag::aborted (bool moved)
 {
        if (moved) {
-               _editor->session()->tempo_map().set_state (*before_state, Stateful::current_state_version);
+               _editor->session()->tempo_map().set_state (*_before_state, Stateful::current_state_version);
        }
 }
 
@@ -3626,7 +3650,7 @@ TempoTwistDrag::TempoTwistDrag (Editor* e, ArdourCanvas::Item* i)
        , _tempo (0)
        , _next_tempo (0)
        , _drag_valid (true)
-       , before_state (0)
+       , _before_state (0)
 {
        DEBUG_TRACE (DEBUG::Drags, "New TempoTwistDrag\n");
 
@@ -3637,6 +3661,8 @@ TempoTwistDrag::start_grab (GdkEvent* event, Gdk::Cursor* cursor)
 {
        Drag::start_grab (event, cursor);
        TempoMap& map (_editor->session()->tempo_map());
+       /* get current state */
+       _before_state = &map.get_state();
        _tempo = const_cast<TempoSection*> (&map.tempo_section_at_frame (raw_grab_frame()));
 
        _next_tempo = map.next_tempo_section (_tempo);
@@ -3697,8 +3723,6 @@ TempoTwistDrag::motion (GdkEvent* event, bool first_move)
        TempoMap& map (_editor->session()->tempo_map());
 
        if (first_move) {
-               /* get current state */
-               before_state = &map.get_state();
                _editor->begin_reversible_command (_("twist tempo"));
        }
 
@@ -3734,7 +3758,7 @@ TempoTwistDrag::finished (GdkEvent* event, bool movement_occurred)
        _editor->tempo_curve_selected (_next_tempo, false);
 
        XMLNode &after = map.get_state();
-       _editor->session()->add_command(new MementoCommand<TempoMap>(map, before_state, &after));
+       _editor->session()->add_command(new MementoCommand<TempoMap>(map, _before_state, &after));
        _editor->commit_reversible_command ();
 }
 
@@ -3742,7 +3766,7 @@ void
 TempoTwistDrag::aborted (bool moved)
 {
        if (moved) {
-               _editor->session()->tempo_map().set_state (*before_state, Stateful::current_state_version);
+               _editor->session()->tempo_map().set_state (*_before_state, Stateful::current_state_version);
        }
 }
 
@@ -3750,22 +3774,37 @@ TempoEndDrag::TempoEndDrag (Editor* e, ArdourCanvas::Item* i)
        : Drag (e, i)
        , _grab_qn (0.0)
        , _tempo (0)
-       , before_state (0)
+       , _before_state (0)
 {
        DEBUG_TRACE (DEBUG::Drags, "New TempoEndDrag\n");
-
+       TempoMarker* marker = reinterpret_cast<TempoMarker*> (_item->get_data ("marker"));
+       _tempo = &marker->tempo();
+       _grab_qn = _tempo->pulse() * 4.0;
 }
 
 void
 TempoEndDrag::start_grab (GdkEvent* event, Gdk::Cursor* cursor)
 {
        Drag::start_grab (event, cursor);
-       TempoMap& map (_editor->session()->tempo_map());
-       _tempo = const_cast<TempoSection*> (&map.tempo_section_at_frame (raw_grab_frame()));
-       _editor->tempo_curve_selected (&map.tempo_section_at_frame (_tempo->frame() - 1), true);
+       TempoMap& tmap (_editor->session()->tempo_map());
+
+       /* get current state */
+       _before_state = &tmap.get_state();
+
 
        ostringstream sstr;
-       sstr << "end: " << fixed << setprecision(3) << map.tempo_section_at_frame (_tempo->frame() - 1).end_note_types_per_minute() << "\n";
+
+       TempoSection* prev = 0;
+       if ((prev = tmap.previous_tempo_section (_tempo)) != 0) {
+               _editor->tempo_curve_selected (tmap.previous_tempo_section (_tempo), true);
+               sstr << "end: " << fixed << setprecision(3) << tmap.tempo_section_at_frame (_tempo->frame() - 1).end_note_types_per_minute() << "\n";
+       }
+
+       if (_tempo->clamped()) {
+               _editor->tempo_curve_selected (_tempo, true);
+               sstr << "start: " << fixed << setprecision(3) << _tempo->note_types_per_minute();
+       }
+
        show_verbose_cursor_text (sstr.str());
 }
 
@@ -3773,21 +3812,6 @@ void
 TempoEndDrag::setup_pointer_frame_offset ()
 {
        TempoMap& map (_editor->session()->tempo_map());
-       const double beat_at_frame = max (0.0, map.beat_at_frame (raw_grab_frame()));
-       const uint32_t divisions = _editor->get_grid_beat_divisions (0);
-       double beat = 0.0;
-
-       if (divisions > 0) {
-               beat = floor (beat_at_frame) + (floor (((beat_at_frame - floor (beat_at_frame)) * divisions)) / divisions);
-       } else {
-               /* while it makes some sense for the user to determine the division to 'grab',
-                  grabbing a bar often leads to confusing results wrt the actual tempo section being altered
-                  and the result over steep tempo curves. Use sixteenths.
-               */
-               beat = floor (beat_at_frame) + (floor (((beat_at_frame - floor (beat_at_frame)) * 4)) / 4);
-       }
-
-       _grab_qn = map.quarter_note_at_beat (beat);
 
        _pointer_frame_offset = raw_grab_frame() - map.frame_at_quarter_note (_grab_qn);
 
@@ -3799,8 +3823,6 @@ TempoEndDrag::motion (GdkEvent* event, bool first_move)
        TempoMap& map (_editor->session()->tempo_map());
 
        if (first_move) {
-               /* get current state */
-               before_state = &map.get_state();
                _editor->begin_reversible_command (_("stretch end tempo"));
        }
 
@@ -3810,7 +3832,12 @@ TempoEndDrag::motion (GdkEvent* event, bool first_move)
        map.gui_stretch_tempo_end (&map.tempo_section_at_frame (_tempo->frame() - 1), map.frame_at_quarter_note (_grab_qn), pf);
 
        ostringstream sstr;
-       sstr << "end: " << fixed << setprecision(3) << map.tempo_section_at_frame (_tempo->frame() - 1).end_note_types_per_minute();
+       sstr << "end: " << fixed << setprecision(3) << map.tempo_section_at_frame (_tempo->frame() - 1).end_note_types_per_minute() << "\n";
+
+       if (_tempo->clamped()) {
+               sstr << "start: " << fixed << setprecision(3) << _tempo->note_types_per_minute();
+       }
+
        show_verbose_cursor_text (sstr.str());
 }
 
@@ -3821,19 +3848,28 @@ TempoEndDrag::finished (GdkEvent* event, bool movement_occurred)
                return;
        }
 
-       TempoMap& map (_editor->session()->tempo_map());
+       TempoMap& tmap (_editor->session()->tempo_map());
 
-       XMLNode &after = map.get_state();
-       _editor->session()->add_command(new MementoCommand<TempoMap>(map, before_state, &after));
+       XMLNode &after = tmap.get_state();
+       _editor->session()->add_command(new MementoCommand<TempoMap>(tmap, _before_state, &after));
        _editor->commit_reversible_command ();
-       _editor->tempo_curve_selected (&map.tempo_section_at_frame (_tempo->frame() - 1), false);
+
+       TempoSection* prev = 0;
+       if ((prev = tmap.previous_tempo_section (_tempo)) != 0) {
+               _editor->tempo_curve_selected (prev, false);
+       }
+
+       if (_tempo->clamped()) {
+               _editor->tempo_curve_selected (_tempo, false);
+
+       }
 }
 
 void
 TempoEndDrag::aborted (bool moved)
 {
        if (moved) {
-               _editor->session()->tempo_map().set_state (*before_state, Stateful::current_state_version);
+               _editor->session()->tempo_map().set_state (*_before_state, Stateful::current_state_version);
        }
 }
 
index d41ca2708fca56c9d7bfb53cc8a19473c8372645..84356f46368ccaa9191bc719c10931702cf5be6b 100644 (file)
@@ -794,7 +794,7 @@ private:
        bool _movable;
        ARDOUR::Tempo _grab_bpm;
        double _grab_qn;
-       XMLNode* before_state;
+       XMLNode* _before_state;
 };
 
 /** BBT Ruler drag */
@@ -821,7 +821,7 @@ public:
 private:
        double _grab_qn;
        ARDOUR::TempoSection* _tempo;
-       XMLNode* before_state;
+       XMLNode* _before_state;
 };
 
 /** tempo curve twist drag */
@@ -851,7 +851,7 @@ private:
        ARDOUR::TempoSection* _tempo;
        ARDOUR::TempoSection* _next_tempo;
        bool _drag_valid;
-       XMLNode* before_state;
+       XMLNode* _before_state;
 };
 
 
@@ -879,7 +879,7 @@ public:
 private:
        double _grab_qn;
        ARDOUR::TempoSection* _tempo;
-       XMLNode* before_state;
+       XMLNode* _before_state;
 };
 
 /** Drag of the playhead cursor */
index 66911fc34c42ea60561e5b605644c90a5b6ef6b3..40942e15694b810e1a1f04c5d422aadedae3cd54 100644 (file)
@@ -991,8 +991,17 @@ Editor::build_tempo_marker_menu (TempoMarker* loc, bool can_remove)
        MenuList& items = tempo_marker_menu->items();
        tempo_marker_menu->set_name ("ArdourContextMenu");
 
-       if (loc->tempo().type() == TempoSection::Ramp) {
-               items.push_back (MenuElem (_("Set Constant"), sigc::mem_fun(*this, &Editor::toggle_tempo_type)));
+       if (!loc->tempo().initial()) {
+               if (loc->tempo().clamped()) {
+                       items.push_back (MenuElem (_("Unlock Continue"), sigc::mem_fun(*this, &Editor::toggle_tempo_clamped)));
+               } else {
+                       items.push_back (MenuElem (_("Lock Continue"), sigc::mem_fun(*this, &Editor::toggle_tempo_clamped)));
+               }
+
+               TempoSection* prev_ts = _session->tempo_map().previous_tempo_section (&loc->tempo());
+               if (prev_ts && prev_ts->end_note_types_per_minute() != loc->tempo().note_types_per_minute()) {
+                       items.push_back (MenuElem (_("Continue"), sigc::mem_fun(*this, &Editor::continue_previous_tempo)));
+               }
        }
 
        TempoSection* next_ts = _session->tempo_map().next_tempo_section (&loc->tempo());
@@ -1000,9 +1009,8 @@ Editor::build_tempo_marker_menu (TempoMarker* loc, bool can_remove)
                items.push_back (MenuElem (_("Ramp to Next"), sigc::mem_fun(*this, &Editor::ramp_to_next_tempo)));
        }
 
-       TempoSection* prev_ts = _session->tempo_map().previous_tempo_section (&loc->tempo());
-       if (prev_ts && prev_ts->end_note_types_per_minute() != loc->tempo().note_types_per_minute()) {
-               items.push_back (MenuElem (_("Continue"), sigc::mem_fun(*this, &Editor::continue_previous_tempo)));
+       if (loc->tempo().type() == TempoSection::Ramp) {
+               items.push_back (MenuElem (_("Set Constant"), sigc::mem_fun(*this, &Editor::toggle_tempo_type)));
        }
 
        if (loc->tempo().position_lock_style() == AudioTime && can_remove) {
@@ -1464,6 +1472,34 @@ Editor::toggle_tempo_type ()
                commit_reversible_command ();
        }
 }
+/* clamped locks the previous section end tempo to the start tempo */
+void
+Editor::toggle_tempo_clamped ()
+{
+       TempoMarker* tm;
+       MeterMarker* mm;
+       dynamic_cast_marker_object (marker_menu_item->get_data ("marker"), &mm, &tm);
+
+       if (tm) {
+               begin_reversible_command (_("Clamp Tempo"));
+               XMLNode &before = _session->tempo_map().get_state();
+
+               TempoSection* tsp = &tm->tempo();
+               TempoSection* prev = _session->tempo_map().previous_tempo_section (tsp);
+
+               if (prev) {
+                       /* set to the end tempo of the previous section */
+                       Tempo new_tempo (prev->end_note_types_per_minute(), prev->note_type(), tsp->end_note_types_per_minute());
+                       _session->tempo_map().gui_change_tempo (tsp, new_tempo);
+               }
+
+               tsp->set_clamped (!tsp->clamped());
+
+               XMLNode &after = _session->tempo_map().get_state();
+               _session->add_command(new MementoCommand<TempoMap>(_session->tempo_map(), &before, &after));
+               commit_reversible_command ();
+       }
+}
 
 void
 Editor::continue_previous_tempo ()
index 752373db6efee5910d97fae63631a7a87b3edeb5..7f34e193985c89b4b09300405707fe4e2b4902ef 100644 (file)
@@ -316,6 +316,10 @@ Editor::redisplay_tempo (bool immediate_redraw)
 void
 Editor::tempo_curve_selected (TempoSection* ts, bool yn)
 {
+       if (ts == 0) {
+               return;
+       }
+
        for (Curves::iterator x = tempo_curves.begin(); x != tempo_curves.end(); ++x) {
                if (&(*x)->tempo() == ts) {
                        if (yn) {
index b1c2dc6428e3adfea75c62ff5934871d6692e01a..dcf68d7f162d149f56b9f77377c87e04cb6e1689 100644 (file)
@@ -201,7 +201,7 @@ class LIBARDOUR_API TempoSection : public MetricSection, public Tempo {
        };
 
        TempoSection (const double& pulse, const double& minute, Tempo tempo, PositionLockStyle pls, framecnt_t sr)
-               : MetricSection (pulse, minute, pls, true, sr), Tempo (tempo), _c (0.0), _active (true), _locked_to_meter (false)  {}
+               : MetricSection (pulse, minute, pls, true, sr), Tempo (tempo), _c (0.0), _active (true), _locked_to_meter (false), _clamped (false)  {}
 
        TempoSection (const XMLNode&, const framecnt_t sample_rate);
 
@@ -220,6 +220,9 @@ class LIBARDOUR_API TempoSection : public MetricSection, public Tempo {
        bool locked_to_meter ()  const { return _locked_to_meter; }
        void set_locked_to_meter (bool yn) { _locked_to_meter = yn; }
 
+       bool clamped ()  const { return _clamped; }
+       void set_clamped (bool yn) { _clamped = yn; }
+
        Tempo tempo_at_minute (const double& minute) const;
        double minute_at_ntpm (const double& ntpm, const double& pulse) const;
 
@@ -264,9 +267,11 @@ class LIBARDOUR_API TempoSection : public MetricSection, public Tempo {
           this enables us to keep the tempo change at the same relative
           position within the bar if/when the meter changes.
        */
+
        double _c;
        bool _active;
        bool _locked_to_meter;
+       bool _clamped;
        Timecode::BBT_Time _legacy_bbt;
        bool _legacy_end;
 };
@@ -501,7 +506,7 @@ class LIBARDOUR_API TempoMap : public PBD::StatefulDestructible
 
        void gui_set_tempo_position (TempoSection*, const framepos_t& frame, const int& sub_num);
        void gui_set_meter_position (MeterSection*, const framepos_t& frame);
-       bool gui_change_tempo (TempoSection*, const Tempo& bpm, bool change_end);
+       bool gui_change_tempo (TempoSection*, const Tempo& bpm);
        void gui_stretch_tempo (TempoSection* tempo, const framepos_t frame, const framepos_t end_frame);
        void gui_stretch_tempo_end (TempoSection* tempo, const framepos_t frame, const framepos_t end_frame);
        bool gui_twist_tempi (TempoSection* first, const Tempo& bpm, const framepos_t frame, const framepos_t end_frame);
@@ -515,6 +520,9 @@ class LIBARDOUR_API TempoMap : public PBD::StatefulDestructible
 
 private:
 
+       TempoSection* previous_tempo_section_locked (const Metrics& metrics, TempoSection*) const;
+       TempoSection* next_tempo_section_locked (const Metrics& metrics, TempoSection*) const;
+
        double beat_at_minute_locked (const Metrics& metrics, const double& minute) const;
        double minute_at_beat_locked (const Metrics& metrics, const double& beat) const;
 
index e11d28bd980946fbc1a36b0d9510e518aa21e6aa..bb61fb70127a368dfd57d13a68226ffc9adddd6d 100644 (file)
@@ -90,6 +90,7 @@ TempoSection::TempoSection (const XMLNode& node, framecnt_t sample_rate)
        , _c (0.0)
        , _active (true)
        , _locked_to_meter (false)
+       , _clamped (false)
        , _legacy_end (false)
 {
        XMLProperty const * prop;
@@ -147,6 +148,13 @@ TempoSection::TempoSection (const XMLNode& node, framecnt_t sample_rate)
                }
        }
 
+       if ((prop = node.property ("clamped")) == 0) {
+               warning << _("TempoSection XML node has no \"clamped\" property") << endmsg;
+               set_clamped (false);
+       } else {
+               set_clamped (string_is_affirmative (prop->value()));
+       }
+
        /* XX replace old end-beats-per-minute name with note-types-per-minute */
        if ((prop = node.property ("end-beats-per-minute")) != 0) {
                if (sscanf (prop->value().c_str(), "%lf", &_end_note_types_per_minute) != 1 || _end_note_types_per_minute < 0.0) {
@@ -223,6 +231,8 @@ TempoSection::get_state() const
        root->add_property ("beats-per-minute", buf);
        snprintf (buf, sizeof (buf), "%lf", _note_type);
        root->add_property ("note-type", buf);
+       snprintf (buf, sizeof (buf), "%s", _clamped?"yes":"no");
+       root->add_property ("clamped", buf);
        snprintf (buf, sizeof (buf), "%lf", _end_note_types_per_minute);
        root->add_property ("end-beats-per-minute", buf);
        snprintf (buf, sizeof (buf), "%s", !initial()?"yes":"no");
@@ -1119,7 +1129,8 @@ TempoMap::replace_tempo (TempoSection& ts, const Tempo& tempo, const double& pul
                return;
        }
 
-       const bool locked_to_meter = ts.locked_to_meter();
+       bool const locked_to_meter = ts.locked_to_meter();
+       bool const ts_clamped = ts.clamped();
        TempoSection* new_ts = 0;
 
        {
@@ -1135,6 +1146,7 @@ TempoMap::replace_tempo (TempoSection& ts, const Tempo& tempo, const double& pul
                        } else {
                                remove_tempo_locked (ts);
                                new_ts = add_tempo_locked (tempo, pulse, minute_at_frame (frame), pls, true, locked_to_meter);
+                               new_ts->set_clamped (ts_clamped);
 
                                if (new_ts && new_ts->type() == TempoSection::Constant) {
                                        new_ts->set_end_note_types_per_minute (new_ts->note_types_per_minute());
@@ -1162,6 +1174,7 @@ TempoMap::replace_tempo (TempoSection& ts, const Tempo& tempo, const double& pul
                        first.set_minute (minute_at_frame (frame));
                        first.set_position_lock_style (AudioTime);
                        first.set_locked_to_meter (true);
+                       first.set_clamped (ts_clamped);
                        {
                                /* cannot move the first tempo section */
                                *static_cast<Tempo*>(&first) = tempo;
@@ -3381,7 +3394,7 @@ TempoMap::gui_set_meter_position (MeterSection* ms, const framepos_t& frame)
 }
 
 bool
-TempoMap::gui_change_tempo (TempoSection* ts, const Tempo& bpm, bool change_end)
+TempoMap::gui_change_tempo (TempoSection* ts, const Tempo& bpm)
 {
        Metrics future_map;
        bool can_solve = false;
@@ -3389,27 +3402,39 @@ TempoMap::gui_change_tempo (TempoSection* ts, const Tempo& bpm, bool change_end)
                Glib::Threads::RWLock::WriterLock lm (lock);
                TempoSection* tempo_copy = copy_metrics_and_point (_metrics, future_map, ts);
 
-               if (change_end && tempo_copy->type() == TempoSection::Constant) {
+               if (tempo_copy->type() == TempoSection::Constant) {
                        tempo_copy->set_end_note_types_per_minute (bpm.note_types_per_minute());
                        tempo_copy->set_note_types_per_minute (bpm.note_types_per_minute());
-               } else if (change_end) {
-                       tempo_copy->set_end_note_types_per_minute (bpm.note_types_per_minute());
                } else {
                        tempo_copy->set_note_types_per_minute (bpm.note_types_per_minute());
+                       tempo_copy->set_end_note_types_per_minute (bpm.end_note_types_per_minute());
+               }
+
+               if (ts->clamped()) {
+                       TempoSection* prev = 0;
+                       if ((prev = previous_tempo_section_locked (future_map, tempo_copy)) != 0) {
+                               prev->set_end_note_types_per_minute (tempo_copy->note_types_per_minute());
+                       }
                }
 
                recompute_tempi (future_map);
 
                if (check_solved (future_map)) {
-                       if (change_end && ts->type() == TempoSection::Constant) {
+                       if (ts->type() == TempoSection::Constant) {
                                ts->set_end_note_types_per_minute (bpm.note_types_per_minute());
                                ts->set_note_types_per_minute (bpm.note_types_per_minute());
-                       } else if (change_end) {
-                               ts->set_end_note_types_per_minute (bpm.note_types_per_minute());
                        } else {
+                               ts->set_end_note_types_per_minute (bpm.end_note_types_per_minute());
                                ts->set_note_types_per_minute (bpm.note_types_per_minute());
                        }
 
+                       if (ts->clamped()) {
+                               TempoSection* prev = 0;
+                               if ((prev = previous_tempo_section_locked (_metrics, ts)) != 0) {
+                                       prev->set_end_note_types_per_minute (ts->note_types_per_minute());
+                               }
+                       }
+
                        recompute_map (_metrics);
                        can_solve = true;
                }
@@ -3457,18 +3482,36 @@ TempoMap::gui_stretch_tempo (TempoSection* ts, const framepos_t frame, const fra
                framepos_t const min_dframe = 2;
 
                double new_bpm;
+               if (prev_t->clamped()) {
+                       TempoSection* next_t = next_tempo_section_locked (future_map, prev_t);
+                       TempoSection* prev_to_prev_t = previous_tempo_section_locked (future_map, prev_t);
+                       /* the change in frames is the result of changing the slope of at most 2 previous tempo sections.
+                          constant to constant is straightforward, as the tempo prev to prev_t has constant slope.
+                       */
+                       double contribution = 0.0;
+                       if (next_t && prev_to_prev_t && prev_to_prev_t->type() == TempoSection::Ramp) {
+                               contribution = (prev_t->frame() - prev_to_prev_t->frame()) / (double) (next_t->frame() - prev_to_prev_t->frame());
+                       }
+                       framepos_t const fr_off = (end_frame - frame);
+                       const frameoffset_t prev_t_frame_contribution = fr_off - (contribution * (double) fr_off);
 
-               if (frame > prev_t->frame() + min_dframe && end_frame > prev_t->frame() + min_dframe) {
-
-                       new_bpm = prev_t->note_types_per_minute() * ((frame - prev_t->frame())
-                                                                                  / (double) (end_frame - prev_t->frame()));
+                       if (frame > prev_to_prev_t->frame() + min_dframe && (frame + prev_t_frame_contribution) > prev_to_prev_t->frame() + min_dframe) {
+                               new_bpm = prev_t->note_types_per_minute() * ((frame - prev_to_prev_t->frame())
+                                                                                    / (double) ((frame + prev_t_frame_contribution) - prev_to_prev_t->frame()));
+                       } else {
+                               new_bpm = prev_t->note_types_per_minute();
+                       }
                } else {
-                       new_bpm = prev_t->note_types_per_minute();
-               }
+                       if (frame > prev_t->frame() + min_dframe && end_frame > prev_t->frame() + min_dframe) {
 
-               std::cout << "new bpm : " << new_bpm << std::endl;
-               new_bpm = min (new_bpm, (double) 1000.0);
+                               new_bpm = prev_t->note_types_per_minute() * ((frame - prev_t->frame())
+                                                                            / (double) (end_frame - prev_t->frame()));
+                       } else {
+                               new_bpm = prev_t->note_types_per_minute();
+                       }
 
+                       new_bpm = min (new_bpm, (double) 1000.0);
+               }
                /* don't clamp and proceed here.
                   testing has revealed that this can go negative,
                   which is an entirely different thing to just being too low.
@@ -3485,6 +3528,13 @@ TempoMap::gui_stretch_tempo (TempoSection* ts, const framepos_t frame, const fra
                        prev_t->set_note_types_per_minute (new_bpm);
                }
 
+               if (prev_t->clamped()) {
+                       TempoSection* prev = 0;
+                       if ((prev = previous_tempo_section_locked (future_map, prev_t)) != 0) {
+                               prev->set_end_note_types_per_minute (prev_t->note_types_per_minute());
+                       }
+               }
+
                recompute_tempi (future_map);
                recompute_meters (future_map);
 
@@ -3495,12 +3545,17 @@ TempoMap::gui_stretch_tempo (TempoSection* ts, const framepos_t frame, const fra
                                ts->set_end_note_types_per_minute (new_bpm);
                                ts->set_note_types_per_minute (new_bpm);
                        }
+                       if (ts->clamped()) {
+                               TempoSection* prev = 0;
+                               if ((prev = previous_tempo_section_locked (_metrics, ts)) != 0) {
+                                       prev->set_end_note_types_per_minute (ts->note_types_per_minute());
+                               }
+                       }
                        recompute_tempi (_metrics);
                        recompute_meters (_metrics);
                }
        }
 
-       MetricPositionChanged (PropertyChange ()); // Emit Signal
 
 out:
        Metrics::const_iterator d = future_map.begin();
@@ -3508,6 +3563,8 @@ out:
                delete (*d);
                ++d;
        }
+       MetricPositionChanged (PropertyChange ()); // Emit Signal
+
 
 }
 void
@@ -3532,31 +3589,17 @@ TempoMap::gui_stretch_tempo_end (TempoSection* ts, const framepos_t frame, const
 
                TempoSection* prev_t = copy_metrics_and_point (_metrics, future_map, ts);
 
-/*
-               TempoSection* next_t = 0;
-               for (Metrics::const_iterator i = future_map.begin(); i != future_map.end(); ++i) {
-                       if ((*i)->is_tempo() && (*i)->minute() >  prev_t->minute()) {
-                               next_t = static_cast<TempoSection*> (*i);
-                               break;
-                       }
-               }
-
-               if (!next_t) {
-                       return;
-               }
-*/
                if (!prev_t) {
                        return;
                }
 
-
                /* minimum allowed measurement distance in frames */
                framepos_t const min_dframe = 2;
                double new_bpm;
 
                if (frame > prev_t->frame() + min_dframe && end_frame > prev_t->frame() + min_dframe) {
-                       new_bpm = prev_t->end_note_types_per_minute() * ((frame - prev_t->frame())
-                                                                                / (double) (end_frame - prev_t->frame()));
+                       new_bpm = prev_t->end_note_types_per_minute() * ((prev_t->frame() - frame)
+                                                                                / (double) (prev_t->frame() - end_frame));
                } else {
                        new_bpm = prev_t->end_note_types_per_minute();
                }
@@ -3569,18 +3612,31 @@ TempoMap::gui_stretch_tempo_end (TempoSection* ts, const framepos_t frame, const
 
                prev_t->set_end_note_types_per_minute (new_bpm);
 
+               TempoSection* next = 0;
+               if ((next = next_tempo_section_locked (future_map, prev_t)) != 0) {
+                       if (next->clamped()) {
+                               next->set_note_types_per_minute (prev_t->end_note_types_per_minute());
+                       }
+               }
+
                recompute_tempi (future_map);
                recompute_meters (future_map);
 
                if (check_solved (future_map)) {
                        ts->set_end_note_types_per_minute (new_bpm);
 
+                       TempoSection* true_next = 0;
+                       if ((true_next = next_tempo_section_locked (_metrics, ts)) != 0) {
+                               if (true_next->clamped()) {
+                                       true_next->set_note_types_per_minute (ts->end_note_types_per_minute());
+                               }
+                       }
+
                        recompute_tempi (_metrics);
                        recompute_meters (_metrics);
                }
        }
 
-       MetricPositionChanged (PropertyChange ()); // Emit Signal
 
 out:
        Metrics::const_iterator d = future_map.begin();
@@ -3589,7 +3645,9 @@ out:
                ++d;
        }
 
+       MetricPositionChanged (PropertyChange ()); // Emit Signal
 }
+
 bool
 TempoMap::gui_twist_tempi (TempoSection* ts, const Tempo& bpm, const framepos_t frame, const framepos_t end_frame)
 {
@@ -3638,7 +3696,6 @@ TempoMap::gui_twist_tempi (TempoSection* ts, const Tempo& bpm, const framepos_t
                }
 
                if (!next_to_next_t) {
-                       std::cout << "no next to next t" << std::endl;
                        return false;
                }
 
@@ -3651,10 +3708,8 @@ TempoMap::gui_twist_tempi (TempoSection* ts, const Tempo& bpm, const framepos_t
                const frameoffset_t tempo_copy_frame_contribution = fr_off - (prev_contribution * (double) fr_off);
 
 
-               framepos_t old_tc_pos = tempo_copy->frame();
-               framepos_t old_next_pos = next_t->frame();
+               framepos_t old_tc_minute = tempo_copy->minute();
                double old_next_minute = next_t->minute();
-               framepos_t old_next_to_next_pos = next_to_next_t->frame();
                double old_next_to_next_minute = next_to_next_t->minute();
 
                double new_bpm;
@@ -3682,6 +3737,7 @@ TempoMap::gui_twist_tempi (TempoSection* ts, const Tempo& bpm, const framepos_t
                if (tempo_copy->type() == TempoSection::Constant) {
                        tempo_copy->set_end_note_types_per_minute (new_bpm);
                }
+
                recompute_tempi (future_map);
 
                if (check_solved (future_map)) {
@@ -3689,11 +3745,14 @@ TempoMap::gui_twist_tempi (TempoSection* ts, const Tempo& bpm, const framepos_t
                        if (!next_t) {
                                return false;
                        }
+
                        ts->set_note_types_per_minute (new_bpm);
                        if (ts->type() == TempoSection::Constant) {
                                ts->set_end_note_types_per_minute (new_bpm);
                        }
+
                        recompute_map (_metrics);
+
                        can_solve = true;
                }
 
@@ -3701,7 +3760,7 @@ TempoMap::gui_twist_tempi (TempoSection* ts, const Tempo& bpm, const framepos_t
                        if (frame > tempo_copy->frame() + min_dframe && end_frame > tempo_copy->frame() + min_dframe) {
 
                                new_next_bpm = next_t->note_types_per_minute() * ((next_to_next_t->minute() - old_next_minute)
-                                                                                       / (double) ((old_next_to_next_minute) - old_next_minute));
+                                                                                 / (double) ((old_next_to_next_minute) - old_next_minute));
 
                        } else {
                                new_next_bpm = next_t->note_types_per_minute();
@@ -3730,20 +3789,22 @@ TempoMap::gui_twist_tempi (TempoSection* ts, const Tempo& bpm, const framepos_t
                        double copy_frame_ratio = 1.0;
 
                        if (next_to_next_t) {
-                               next_frame_ratio =  (next_to_next_t->frame() - old_next_pos) / (double) (old_next_to_next_pos -  old_next_pos);
+                               next_frame_ratio = (next_to_next_t->minute() - old_next_minute) / (old_next_to_next_minute -  old_next_minute);
 
-                               copy_frame_ratio =   ((old_tc_pos - next_t->frame()) / (double) (old_tc_pos - old_next_pos));
-
-                       } else {
-                               //next_frame_ratio = (((next_to_next_pos - fr_off) - next_t->frame()) / (double) ((next_to_next_pos) - next_t->frame()));
-                               //next_pulse_ratio = (start_pulse / end_pulse);
+                               copy_frame_ratio = ((old_tc_minute - next_t->minute()) / (double) (old_tc_minute - old_next_minute));
                        }
 
                        new_next_bpm = next_t->note_types_per_minute() * next_frame_ratio;
-                       new_copy_end_bpm =  tempo_copy->end_note_types_per_minute() * copy_frame_ratio;
+                       new_copy_end_bpm = tempo_copy->end_note_types_per_minute() * copy_frame_ratio;
 
-                       next_t->set_note_types_per_minute (new_next_bpm);
                        tempo_copy->set_end_note_types_per_minute (new_copy_end_bpm);
+
+                       if (next_t->clamped()) {
+                               next_t->set_note_types_per_minute (new_copy_end_bpm);
+                       } else {
+                               next_t->set_note_types_per_minute (new_next_bpm);
+                       }
+
                        recompute_tempi (future_map);
 
                        if (check_solved (future_map)) {
@@ -3757,7 +3818,13 @@ TempoMap::gui_twist_tempi (TempoSection* ts, const Tempo& bpm, const framepos_t
                                if (!next_t) {
                                        return false;
                                }
-                               next_t->set_note_types_per_minute (new_next_bpm);
+
+                               if (next_t->clamped()) {
+                                       next_t->set_note_types_per_minute (new_copy_end_bpm);
+                               } else {
+                                       next_t->set_note_types_per_minute (new_next_bpm);
+                               }
+
                                ts->set_end_note_types_per_minute (new_copy_end_bpm);
                                recompute_map (_metrics);
                                can_solve = true;
@@ -3770,9 +3837,8 @@ TempoMap::gui_twist_tempi (TempoSection* ts, const Tempo& bpm, const framepos_t
                delete (*d);
                ++d;
        }
-       if (can_solve) {
-               MetricPositionChanged (PropertyChange ()); // Emit Signal
-       }
+
+       MetricPositionChanged (PropertyChange ()); // Emit Signal
 
        return can_solve;
 }
@@ -4287,16 +4353,23 @@ TempoMap::tempo_section_at_beat_locked (const Metrics& metrics, const double& be
 
 TempoSection*
 TempoMap::previous_tempo_section (TempoSection* ts) const
+{
+       Glib::Threads::RWLock::ReaderLock lm (lock);
+
+       return previous_tempo_section_locked (_metrics, ts);
+
+}
+
+TempoSection*
+TempoMap::previous_tempo_section_locked (const Metrics& metrics, TempoSection* ts) const
 {
        if (!ts) {
                return 0;
        }
 
-       Glib::Threads::RWLock::ReaderLock lm (lock);
-
        TempoSection* prev = 0;
 
-       for (Metrics::const_iterator i = _metrics.begin(); i != _metrics.end(); ++i) {
+       for (Metrics::const_iterator i = metrics.begin(); i != metrics.end(); ++i) {
 
                if ((*i)->is_tempo()) {
                        TempoSection* t = static_cast<TempoSection*> (*i);
@@ -4324,16 +4397,22 @@ TempoMap::previous_tempo_section (TempoSection* ts) const
 
 TempoSection*
 TempoMap::next_tempo_section (TempoSection* ts) const
+{
+       Glib::Threads::RWLock::ReaderLock lm (lock);
+
+       return next_tempo_section_locked (_metrics, ts);
+}
+
+TempoSection*
+TempoMap::next_tempo_section_locked (const Metrics& metrics, TempoSection* ts) const
 {
        if (!ts) {
                return 0;
        }
 
-       Glib::Threads::RWLock::ReaderLock lm (lock);
-
        TempoSection* prev = 0;
 
-       for (Metrics::const_iterator i = _metrics.begin(); i != _metrics.end(); ++i) {
+       for (Metrics::const_iterator i = metrics.begin(); i != metrics.end(); ++i) {
 
                if ((*i)->is_tempo()) {
                        TempoSection* t = static_cast<TempoSection*> (*i);