Make some musical operations on music-locked regions operate in beats.
authornick_m <mainsbridge@gmail.com>
Mon, 13 Jun 2016 17:21:52 +0000 (03:21 +1000)
committernick_m <mainsbridge@gmail.com>
Sat, 9 Jul 2016 16:18:36 +0000 (02:18 +1000)
- use exact beats to determine frame position.
- see comments in tempo.cc for more.
- this hasn't been done for split yet, but dragging and
  trimming are supported.

13 files changed:
gtk2_ardour/editor_drag.cc
gtk2_ardour/editor_drag.h
gtk2_ardour/midi_time_axis.cc
gtk2_ardour/midi_time_axis.h
gtk2_ardour/region_view.cc
gtk2_ardour/region_view.h
gtk2_ardour/step_editor.cc
libs/ardour/ardour/midi_region.h
libs/ardour/ardour/region.h
libs/ardour/ardour/tempo.h
libs/ardour/midi_region.cc
libs/ardour/region.cc
libs/ardour/tempo.cc

index c5e72fb778940939276f9e52afa6a18a5a01d380..d87388e888a96cabeff3dd86f010399578b21999 100644 (file)
@@ -510,7 +510,7 @@ Drag::show_verbose_cursor_text (string const & text)
 }
 
 boost::shared_ptr<Region>
-Drag::add_midi_region (MidiTimeAxisView* view, bool commit)
+Drag::add_midi_region (MidiTimeAxisView* view, bool commit, const int32_t& sub_num)
 {
        if (_editor->session()) {
                const TempoMap& map (_editor->session()->tempo_map());
@@ -519,7 +519,7 @@ Drag::add_midi_region (MidiTimeAxisView* view, bool commit)
                   might be wrong.
                */
                framecnt_t len = map.frame_at_beat (map.beat_at_frame (pos) + 1.0) - pos;
-               return view->add_region (grab_frame(), len, commit);
+               return view->add_region (grab_frame(), len, commit, sub_num);
        }
 
        return boost::shared_ptr<Region>();
@@ -1257,7 +1257,7 @@ RegionMoveDrag::motion (GdkEvent* event, bool first_move)
 
                        const boost::shared_ptr<const Region> original = rv->region();
                        boost::shared_ptr<Region> region_copy = RegionFactory::create (original, true);
-                       region_copy->set_position (original->position());
+                       region_copy->set_position (original->position(), _editor->get_grid_music_divisions (event->button.state));
                        /* need to set this so that the drop zone code can work. This doesn't
                           actually put the region into the playlist, but just sets a weak pointer
                           to it.
@@ -1364,7 +1364,8 @@ RegionMoveDrag::finished (GdkEvent* ev, bool movement_occurred)
                finished_copy (
                        changed_position,
                        changed_tracks,
-                       drag_delta
+                       drag_delta,
+                       ev->button.state
                        );
 
        } else {
@@ -1372,7 +1373,8 @@ RegionMoveDrag::finished (GdkEvent* ev, bool movement_occurred)
                finished_no_copy (
                        changed_position,
                        changed_tracks,
-                       drag_delta
+                       drag_delta,
+                       ev->button.state
                        );
 
        }
@@ -1419,7 +1421,7 @@ RegionMoveDrag::create_destination_time_axis (boost::shared_ptr<Region> region,
 }
 
 void
-RegionMoveDrag::finished_copy (bool const changed_position, bool const /*changed_tracks*/, framecnt_t const drag_delta)
+RegionMoveDrag::finished_copy (bool const changed_position, bool const /*changed_tracks*/, framecnt_t const drag_delta, int32_t const ev_state)
 {
        RegionSelection new_views;
        PlaylistSet modified_playlists;
@@ -1510,7 +1512,8 @@ void
 RegionMoveDrag::finished_no_copy (
        bool const changed_position,
        bool const changed_tracks,
-       framecnt_t const drag_delta
+       framecnt_t const drag_delta,
+       int32_t const ev_state
        )
 {
        RegionSelection new_views;
@@ -1631,7 +1634,7 @@ RegionMoveDrag::finished_no_copy (
                                playlist->freeze ();
                        }
 
-                       rv->region()->set_position (where);
+                       rv->region()->set_position (where, _editor->get_grid_music_divisions (ev_state));
                        _editor->session()->add_command (new StatefulDiffCommand (rv->region()));
                }
 
@@ -1876,7 +1879,7 @@ RegionInsertDrag::RegionInsertDrag (Editor* e, boost::shared_ptr<Region> r, Rout
 }
 
 void
-RegionInsertDrag::finished (GdkEvent *, bool)
+RegionInsertDrag::finished (GdkEvent * event, bool)
 {
        int pos = _views.front().time_axis_view;
        assert(pos >= 0 && pos < (int)_time_axis_views.size());
@@ -2303,7 +2306,7 @@ RegionCreateDrag::motion (GdkEvent* event, bool first_move)
 {
        if (first_move) {
                _editor->begin_reversible_command (_("create region"));
-               _region = add_midi_region (_view, false);
+               _region = add_midi_region (_view, false, _editor->get_grid_music_divisions (event->button.state));
                _view->playlist()->freeze ();
        } else {
                if (_region) {
@@ -2326,10 +2329,10 @@ RegionCreateDrag::motion (GdkEvent* event, bool first_move)
 }
 
 void
-RegionCreateDrag::finished (GdkEvent*, bool movement_occurred)
+RegionCreateDrag::finished (GdkEvent* event, bool movement_occurred)
 {
        if (!movement_occurred) {
-               add_midi_region (_view, true);
+               add_midi_region (_view, true, _editor->get_grid_music_divisions (event->button.state));
        } else {
                _view->playlist()->thaw ();
                _editor->commit_reversible_command();
@@ -2911,7 +2914,9 @@ TrimDrag::motion (GdkEvent* event, bool first_move)
        switch (_operation) {
        case StartTrim:
                for (list<DraggingView>::iterator i = _views.begin(); i != _views.end(); ++i) {
-                       bool changed = i->view->trim_front (i->initial_position + dt, non_overlap_trim);
+                       bool changed = i->view->trim_front (i->initial_position + dt, non_overlap_trim
+                                                           , _editor->get_grid_music_divisions (event->button.state));
+
                        if (changed && _preserve_fade_anchor) {
                                AudioRegionView* arv = dynamic_cast<AudioRegionView*> (i->view);
                                if (arv) {
@@ -3379,8 +3384,7 @@ TempoMarkerDrag::aborted (bool moved)
        if (moved) {
                TempoMap& map (_editor->session()->tempo_map());
                map.set_state (*before_state, Stateful::current_state_version);
-               // delete the dummy marker we used for visual representation while moving.
-               // a new visual marker will show up automatically.
+               // delete the dummy (hidden) marker we used for events while moving.
                delete _marker;
        }
 }
@@ -4757,7 +4761,7 @@ RubberbandSelectDrag::finished (GdkEvent* event, bool movement_occurred)
                        /* MIDI track */
                        if (_editor->selection->empty() && _editor->mouse_mode == MouseDraw) {
                                /* nothing selected */
-                               add_midi_region (mtv, true);
+                               add_midi_region (mtv, true, _editor->get_grid_music_divisions(event->button.state));
                                do_deselect = false;
                        }
                }
index 196bd3712379b65d8eb07758d22a01fbfd691ba1..d9ac49cfd40e9d73b498d0aaf53e0cfd90861f06 100644 (file)
@@ -248,7 +248,7 @@ protected:
        /* sets snap delta from unsnapped pos */
        void setup_snap_delta (framepos_t pos);
 
-       boost::shared_ptr<ARDOUR::Region> add_midi_region (MidiTimeAxisView*, bool commit);
+       boost::shared_ptr<ARDOUR::Region> add_midi_region (MidiTimeAxisView*, bool commit, const int32_t& sub_num);
 
        void show_verbose_cursor_time (framepos_t);
        void show_verbose_cursor_duration (framepos_t, framepos_t, double xoffset = 0);
@@ -407,13 +407,15 @@ private:
        void finished_no_copy (
                bool const,
                bool const,
-               ARDOUR::framecnt_t const
+               ARDOUR::framecnt_t const,
+               int32_t const ev_state
                );
 
        void finished_copy (
                bool const,
                bool const,
-               ARDOUR::framecnt_t const
+               ARDOUR::framecnt_t const,
+               int32_t const ev_state
                );
 
        RegionView* insert_region_into_playlist (
index 9f68aa5e55e0cc5ff006324346150f2285da4641..074f2a115b4248eee99d8375343c7c4bb81084c2 100644 (file)
@@ -1508,7 +1508,7 @@ MidiTimeAxisView::automation_child_menu_item (Evoral::Parameter param)
 }
 
 boost::shared_ptr<MidiRegion>
-MidiTimeAxisView::add_region (framepos_t pos, framecnt_t length, bool commit)
+MidiTimeAxisView::add_region (framepos_t pos, framecnt_t length, bool commit, const int32_t& sub_num)
 {
        Editor* real_editor = dynamic_cast<Editor*> (&_editor);
        if (commit) {
@@ -1526,7 +1526,7 @@ MidiTimeAxisView::add_region (framepos_t pos, framecnt_t length, bool commit)
        plist.add (ARDOUR::Properties::name, PBD::basename_nosuffix(src->name()));
 
        boost::shared_ptr<Region> region = (RegionFactory::create (src, plist));
-
+       region->set_position (pos, sub_num);
        playlist()->add_region (region, pos);
        _session->add_command (new StatefulDiffCommand (playlist()));
 
index 6fe77dc8ac091f46804b2f2cad687f168c7381ff..b8e60660b407b6d8ebab46aa2534480638689952 100644 (file)
@@ -82,7 +82,7 @@ public:
 
        void set_height (uint32_t, TrackHeightMode m = OnlySelf);
 
-       boost::shared_ptr<ARDOUR::MidiRegion> add_region (ARDOUR::framepos_t, ARDOUR::framecnt_t, bool);
+       boost::shared_ptr<ARDOUR::MidiRegion> add_region (ARDOUR::framepos_t, ARDOUR::framecnt_t, bool, const int32_t& sub_num);
 
        void show_all_automation (bool apply_to_selection = false);
        void show_existing_automation (bool apply_to_selection = false);
index a62d000abc74c24e9f33637f3d75ba2718b62560..b9648bbbc3868751d0cd6275395c881d78ee882b 100644 (file)
@@ -819,7 +819,7 @@ RegionView::update_coverage_frames (LayerDisplay d)
 }
 
 bool
-RegionView::trim_front (framepos_t new_bound, bool no_overlap)
+RegionView::trim_front (framepos_t new_bound, bool no_overlap, const int32_t& sub_num)
 {
        if (_region->locked()) {
                return false;
@@ -836,7 +836,7 @@ RegionView::trim_front (framepos_t new_bound, bool no_overlap)
                return false;
        }
 
-       _region->trim_front (speed_bound);
+       _region->trim_front (speed_bound, sub_num);
 
        if (no_overlap) {
                // Get the next region on the left of this region and shrink/expand it.
index f17e37a72da4810dab3036818e497a6887b5f76d..43608c31d14c4ae25b7447b534727055809b7df5 100644 (file)
@@ -102,7 +102,7 @@ class RegionView : public TimeAxisViewItem
        /** Called when a front trim is about to begin */
        virtual void trim_front_starting () {}
 
-       bool trim_front (framepos_t, bool);
+       bool trim_front (framepos_t, bool, const int32_t& sub_num);
 
        /** Called when a start trim has finished */
        virtual void trim_front_ending () {}
index 8afc0ed8261cbe176cb2fb1d15ecf47972a2b517..3ea3bb4ccd6cd99427a07c8a18edc11152adcf15 100644 (file)
@@ -122,7 +122,7 @@ StepEditor::prepare_step_edit_region ()
                framecnt_t next_bar_pos = _mtv.session()->tempo_map().frame_at_beat (next_bar_in_beats);
                framecnt_t len = next_bar_pos - step_edit_insert_position;
 
-               step_edit_region = _mtv.add_region (step_edit_insert_position, len, true);
+               step_edit_region = _mtv.add_region (step_edit_insert_position, len, true, _editor.get_grid_music_divisions (0));
 
                RegionView* rv = _mtv.midi_view()->find_view (step_edit_region);
                step_edit_region_view = dynamic_cast<MidiRegionView*>(rv);
index b4557ed1dd828fcc0d3911b188c97dff701f79b8..3a097c907e37cfa6ed70e70e2e51f16e6f0b7d25 100644 (file)
@@ -133,8 +133,8 @@ class LIBARDOUR_API MidiRegion : public Region
 
        void set_position_internal (framepos_t pos, bool allow_bbt_recompute);
        void set_length_internal (framecnt_t len);
-       void set_start_internal (framecnt_t);
-       void trim_to_internal (framepos_t position, framecnt_t length);
+       void set_start_internal (framecnt_t, const int32_t& sub_num);
+       void trim_to_internal (framepos_t position, framecnt_t length, const int32_t& sub_num);
        void update_length_beats ();
 
        void model_changed ();
index 776a8a79669b76134ff71b221110f1a272b7b3a1..e0dd159ce5939d146d49e62495ba6c77e3d02e69 100644 (file)
@@ -207,7 +207,7 @@ class LIBARDOUR_API Region
 
        void set_length (framecnt_t);
        void set_start (framepos_t);
-       void set_position (framepos_t);
+       void set_position (framepos_t, int32_t sub_num = 0);
        void set_initial_position (framepos_t);
        void special_set_position (framepos_t);
        virtual void update_after_tempo_map_change (bool send_change = true);
@@ -216,15 +216,15 @@ class LIBARDOUR_API Region
        bool at_natural_position () const;
        void move_to_natural_position ();
 
-       void move_start (frameoffset_t distance);
-       void trim_front (framepos_t new_position);
-       void trim_end (framepos_t new_position);
-       void trim_to (framepos_t position, framecnt_t length);
+       void move_start (frameoffset_t distance, const int32_t& sub_num = 0);
+       void trim_front (framepos_t new_position, const int32_t& sub_num = 0);
+       void trim_end (framepos_t new_position, const int32_t& sub_num = 0);
+       void trim_to (framepos_t position, framecnt_t length, const int32_t& sub_num = 0);
 
        virtual void fade_range (framepos_t, framepos_t) {}
 
-       void cut_front (framepos_t new_position);
-       void cut_end (framepos_t new_position);
+       void cut_front (framepos_t new_position, const int32_t& sub_num = 0);
+       void cut_end (framepos_t new_position, const int32_t& sub_num = 0);
 
        void set_layer (layer_t l); /* ONLY Playlist can call this */
        void raise ();
@@ -358,7 +358,7 @@ class LIBARDOUR_API Region
        void post_set (const PBD::PropertyChange&);
        virtual void set_position_internal (framepos_t pos, bool allow_bbt_recompute);
        virtual void set_length_internal (framecnt_t);
-       virtual void set_start_internal (framecnt_t);
+       virtual void set_start_internal (framecnt_t, const int32_t& sub_num = 0);
        bool verify_start_and_length (framepos_t, framecnt_t&);
        void first_edit ();
 
@@ -396,9 +396,9 @@ class LIBARDOUR_API Region
   private:
        void mid_thaw (const PBD::PropertyChange&);
 
-       virtual void trim_to_internal (framepos_t position, framecnt_t length);
-       void modify_front (framepos_t new_position, bool reset_fade);
-       void modify_end (framepos_t new_position, bool reset_fade);
+       virtual void trim_to_internal (framepos_t position, framecnt_t length, const int32_t& sub_num);
+       void modify_front (framepos_t new_position, bool reset_fade, const int32_t& sub_num);
+       void modify_end (framepos_t new_position, bool reset_fade, const int32_t& sub_num);
 
        void maybe_uncopy ();
 
index 33d9fb937d6af3aa78396ca787cfe95f6ec75deb..1bf4c47d7a29e8998b6a37d2afe8ee8241330cc0 100644 (file)
@@ -450,6 +450,8 @@ class LIBARDOUR_API TempoMap : public PBD::StatefulDestructible
        bool gui_change_tempo (TempoSection*, const Tempo& bpm);
        void gui_dilate_tempo (TempoSection* tempo, const framepos_t& frame, const framepos_t& end_frame, const double& pulse);
 
+       double exact_beat_at_frame (const framepos_t& frame, const int32_t& sub_num);
+
        std::pair<double, framepos_t> predict_tempo_position (TempoSection* section, const Timecode::BBT_Time& bbt);
        bool can_solve_bbt (TempoSection* section, const Timecode::BBT_Time& bbt);
 
@@ -493,6 +495,8 @@ private:
        bool solve_map_frame (Metrics& metrics, MeterSection* section, const framepos_t& frame);
        bool solve_map_bbt (Metrics& metrics, MeterSection* section, const Timecode::BBT_Time& bbt);
 
+       double exact_beat_at_frame_locked (const Metrics& metrics, const framepos_t& frame, const int32_t& sub_num);
+
        friend class ::BBTTest;
        friend class ::FrameposPlusBeatsTest;
        friend class ::TempoTest;
index 81bfc2aa2345914cb982058b0c1a7e687cbeaea1..26a8509aca61a4c7199ea5d2726c0b9cbb0f9f1e 100644 (file)
@@ -458,14 +458,17 @@ MidiRegion::fix_negative_start ()
 }
 
 void
-MidiRegion::set_start_internal (framecnt_t s)
+MidiRegion::set_start_internal (framecnt_t s, const int32_t& sub_num)
 {
-       Region::set_start_internal (s);
-       set_start_beats_from_start_frames ();
+       Region::set_start_internal (s, sub_num);
+
+       if (position_lock_style() == AudioTime) {
+               set_start_beats_from_start_frames ();
+       }
 }
 
 void
-MidiRegion::trim_to_internal (framepos_t position, framecnt_t length)
+MidiRegion::trim_to_internal (framepos_t position, framecnt_t length, const int32_t& sub_num)
 {
        framepos_t new_start;
 
@@ -476,7 +479,7 @@ MidiRegion::trim_to_internal (framepos_t position, framecnt_t length)
        PropertyChange what_changed;
 
        /* beat has not been set by set_position_internal */
-       const double beat_delta = _session.tempo_map().beat_at_frame (position) - beat();
+       const double beat_delta = _session.tempo_map().exact_beat_at_frame (position, sub_num) - beat();
 
        /* Set position before length, otherwise for MIDI regions this bad thing happens:
         * 1. we call set_length_internal; length in beats is computed using the region's current
@@ -504,7 +507,7 @@ MidiRegion::trim_to_internal (framepos_t position, framecnt_t length)
                _start_beats = Evoral::Beats (new_start_beat);
                what_changed.add (Properties::start_beats);
 
-               set_start_internal (new_start);
+               set_start_internal (new_start, sub_num);
                what_changed.add (Properties::start);
        }
 
index ba18cbc62db3d440efe873c3f558e3780d3c2824..5fb5b30014add2474a2f8ea6919c7c720f36688a 100644 (file)
@@ -570,13 +570,19 @@ Region::update_after_tempo_map_change (bool send)
 }
 
 void
-Region::set_position (framepos_t pos)
+Region::set_position (framepos_t pos, int32_t sub_num)
 {
        if (!can_move()) {
                return;
        }
 
-       set_position_internal (pos, true);
+       if (sub_num == 0) {
+               set_position_internal (pos, true);
+       } else {
+               double beat = _session.tempo_map().exact_beat_at_frame (pos, sub_num);
+               _beat = beat;
+               set_position_internal (pos, false);
+       }
 
        /* do this even if the position is the same. this helps out
           a GUI that has moved its representation already.
@@ -738,7 +744,7 @@ Region::set_start (framepos_t pos)
 }
 
 void
-Region::move_start (frameoffset_t distance)
+Region::move_start (frameoffset_t distance, const int32_t& sub_num)
 {
        if (locked() || position_locked() || video_locked()) {
                return;
@@ -774,7 +780,7 @@ Region::move_start (frameoffset_t distance)
                return;
        }
 
-       set_start_internal (new_start);
+       set_start_internal (new_start, sub_num);
 
        _whole_file = false;
        first_edit ();
@@ -783,25 +789,25 @@ Region::move_start (frameoffset_t distance)
 }
 
 void
-Region::trim_front (framepos_t new_position)
+Region::trim_front (framepos_t new_position, const int32_t& sub_num)
 {
-       modify_front (new_position, false);
+       modify_front (new_position, false, sub_num);
 }
 
 void
-Region::cut_front (framepos_t new_position)
+Region::cut_front (framepos_t new_position, const int32_t& sub_num)
 {
-       modify_front (new_position, true);
+       modify_front (new_position, true, sub_num);
 }
 
 void
-Region::cut_end (framepos_t new_endpoint)
+Region::cut_end (framepos_t new_endpoint, const int32_t& sub_num)
 {
-       modify_end (new_endpoint, true);
+       modify_end (new_endpoint, true, sub_num);
 }
 
 void
-Region::modify_front (framepos_t new_position, bool reset_fade)
+Region::modify_front (framepos_t new_position, bool reset_fade, const int32_t& sub_num)
 {
        if (locked()) {
                return;
@@ -831,7 +837,7 @@ Region::modify_front (framepos_t new_position, bool reset_fade)
                        newlen = _length + (_position - new_position);
                }
 
-               trim_to_internal (new_position, newlen);
+               trim_to_internal (new_position, newlen, sub_num);
 
                if (reset_fade) {
                        _right_of_split = true;
@@ -846,14 +852,14 @@ Region::modify_front (framepos_t new_position, bool reset_fade)
 }
 
 void
-Region::modify_end (framepos_t new_endpoint, bool reset_fade)
+Region::modify_end (framepos_t new_endpoint, bool reset_fade, const int32_t& sub_num)
 {
        if (locked()) {
                return;
        }
 
        if (new_endpoint > _position) {
-               trim_to_internal (_position, new_endpoint - _position);
+               trim_to_internal (_position, new_endpoint - _position, sub_num);
                if (reset_fade) {
                        _left_of_split = true;
                }
@@ -868,19 +874,19 @@ Region::modify_end (framepos_t new_endpoint, bool reset_fade)
  */
 
 void
-Region::trim_end (framepos_t new_endpoint)
+Region::trim_end (framepos_t new_endpoint, const int32_t& sub_num)
 {
-       modify_end (new_endpoint, false);
+       modify_end (new_endpoint, false, sub_num);
 }
 
 void
-Region::trim_to (framepos_t position, framecnt_t length)
+Region::trim_to (framepos_t position, framecnt_t length, const int32_t& sub_num)
 {
        if (locked()) {
                return;
        }
 
-       trim_to_internal (position, length);
+       trim_to_internal (position, length, sub_num);
 
        if (!property_changes_suspended()) {
                recompute_at_start ();
@@ -889,7 +895,7 @@ Region::trim_to (framepos_t position, framecnt_t length)
 }
 
 void
-Region::trim_to_internal (framepos_t position, framecnt_t length)
+Region::trim_to_internal (framepos_t position, framecnt_t length, const int32_t& sub_num)
 {
        framepos_t new_start;
 
@@ -926,7 +932,7 @@ Region::trim_to_internal (framepos_t position, framecnt_t length)
        PropertyChange what_changed;
 
        if (_start != new_start) {
-               set_start_internal (new_start);
+               set_start_internal (new_start, sub_num);
                what_changed.add (Properties::start);
        }
 
@@ -1845,7 +1851,7 @@ Region::post_set (const PropertyChange& pc)
 }
 
 void
-Region::set_start_internal (framecnt_t s)
+Region::set_start_internal (framecnt_t s, const int32_t& sub_num)
 {
        _start = s;
 }
index c5c7e2c52fa90ad158c22b4126f4c0473acade76..5df06f7745295f18621644cfa91cbe409d7260a8 100644 (file)
@@ -586,12 +586,16 @@ MeterSection::get_state() const
 /*
   Tempo Map Overview
 
+  The Shaggs - Things I Wonder
+  https://www.youtube.com/watch?v=9wQK6zMJOoQ
+
   Tempo is the rate of the musical pulse.
   Meters divide the pulses into measures and beats.
 
   TempoSections - provide pulses in the form of beats_per_minute() and note_type() where note_type is the division of a whole pulse,
   and beats_per_minute is the number of note_types in one minute (unlike what its name suggests).
-  Note that Tempo::beats_per_minute() has nothing to do with musical beats.
+  Note that Tempo::beats_per_minute() has nothing to do with musical beats. It has been left that way because
+  a shorter one hasn't been found yet (pulse_divisions_per_minute()?).
 
   MeterSecions - divide pulses into measures (via divisions_per_bar) and beats (via note_divisor).
 
@@ -629,6 +633,43 @@ MeterSection::get_state() const
   Because ramped MusicTime and AudioTime tempos can interact with each other,
   reordering is frequent. Care must be taken to keep _metrics in a solved state.
   Solved means ordered by frame or pulse with frame-accurate precision (see check_solved()).
+
+  Music and Audio
+
+  Music and audio-locked objects may seem interchangeable on the surface, but when translating
+  between audio samples and beats, keep in mind that a sample is only a quantised approximation
+  of the actual time (in minutes) of a beat.
+  Thus if a gui user points to the frame occupying the start of a music-locked object on 1|3|0, it does not
+  mean that this frame is the actual location in time of 1|3|0.
+
+  You cannot use a frame measurement to determine beat distance except under special circumstances
+  (e.g. where the user has requested that a beat lie on a SMPTE frame or if the tempo is known to be constant over the duration).
+
+  This means is that a user operating on a musical grid must supply the desired beat position and/or current beat quantization in order for the
+  sample space the user is operating at to be translated correctly to the object.
+
+  The current approach is to interpret the supplied frame using the grid division the user has currently selected.
+  If the user has no musical grid set, they are actually operating in sample space (even SMPTE frames are rounded to audio frame), so
+  the supplied audio frame is interpreted as the desired musical location (beat_at_frame()).
+
+  tldr: Beat, being a function of time, has nothing to do with sample rate, but time quantization can get in the way of precision.
+
+  When frame_at_beat() is called, the position calculation is performed in pulses and minutes.
+  The result is rounded to audio frames.
+  When beat_at_frame() is called, the frame is converted to minutes, with no rounding performed on the result.
+
+  So :
+  frame_at_beat (beat_at_frame (frame)) == frame
+  but :
+  beat_at_frame (frame_at_beat (beat)) != beat due to the time quantization of frame_at_beat().
+
+  Doing the second one will result in a beat distance error of up to 0.5 audio samples.
+  So instead work in pulses and/or beats and only use beat position to caclulate frame position (e.g. after tempo change).
+  For audio-locked objects, use frame position to calculate beat position.
+
+  The above pointless example would then do:
+  beat_at_pulse (pulse_at_beat (beat)) to avoid rounding.
+
 */
 struct MetricSectionSorter {
     bool operator() (const MetricSection* a, const MetricSection* b) {
@@ -2545,25 +2586,9 @@ TempoMap::gui_move_tempo (TempoSection* ts, const framepos_t& frame, const int&
                        /* if we're snapping to a musical grid, set the pulse exactly instead of via the supplied frame. */
                        Glib::Threads::RWLock::WriterLock lm (lock);
                        TempoSection* tempo_copy = copy_metrics_and_point (_metrics, future_map, ts);
-                       double beat = beat_at_frame_locked (future_map, frame);
-
-                       if (sub_num > 1) {
-                               beat = floor (beat) + (floor (((beat - floor (beat)) * (double) sub_num) + 0.5) / sub_num);
-                       } else if (sub_num == 1) {
-                               /* snap to beat */
-                               beat = floor (beat + 0.5);
-                       }
-
+                       const double beat = exact_beat_at_frame_locked (future_map, frame, sub_num);
                        double pulse = pulse_at_beat_locked (future_map, beat);
 
-                       if (sub_num == -1) {
-                               /* snap to  bar */
-                               BBT_Time bbt = bbt_at_beat_locked (future_map, beat);
-                               bbt.beats = 1;
-                               bbt.ticks = 0;
-                               pulse = pulse_at_bbt_locked (future_map, bbt);
-                       }
-
                        if (solve_map_pulse (future_map, tempo_copy, pulse)) {
                                solve_map_pulse (_metrics, ts, pulse);
                                recompute_meters (_metrics);
@@ -2811,6 +2836,33 @@ TempoMap::gui_dilate_tempo (TempoSection* ts, const framepos_t& frame, const fra
        MetricPositionChanged (); // Emit Signal
 }
 
+double
+TempoMap::exact_beat_at_frame (const framepos_t& frame, const int32_t& sub_num)
+{
+       Glib::Threads::RWLock::ReaderLock lm (lock);
+
+       return exact_beat_at_frame_locked (_metrics, frame, sub_num);
+}
+
+double
+TempoMap::exact_beat_at_frame_locked (const Metrics& metrics, const framepos_t& frame, const int32_t& sub_num)
+{
+       double beat = beat_at_frame_locked (metrics, frame);
+       if (sub_num > 1) {
+               beat = floor (beat) + (floor (((beat - floor (beat)) * (double) sub_num) + 0.5) / sub_num);
+       } else if (sub_num == 1) {
+               /* snap to beat */
+               beat = floor (beat + 0.5);
+       } else if (sub_num == -1) {
+               /* snap to  bar */
+               Timecode::BBT_Time bbt = bbt_at_beat_locked (metrics, beat);
+               bbt.beats = 1;
+               bbt.ticks = 0;
+               beat = beat_at_bbt_locked (metrics, bbt);
+       }
+       return beat;
+}
+
 framecnt_t
 TempoMap::bbt_duration_at (framepos_t pos, const BBT_Time& bbt, int dir)
 {