From cec3db54f010c4c9afec400a1b9de4680aa6d0bb Mon Sep 17 00:00:00 2001 From: Paul Davis Date: Fri, 8 Aug 2025 17:52:25 -0600 Subject: [PATCH] refactor AudioTrigger::estimate_tempo() into ARDOUR::estimate_audio_tempo() --- libs/ardour/ardour/utils.h | 7 ++ libs/ardour/triggerbox.cc | 120 ++------------------------------- libs/ardour/utils.cc | 131 ++++++++++++++++++++++++++++++++++++- 3 files changed, 141 insertions(+), 117 deletions(-) diff --git a/libs/ardour/ardour/utils.h b/libs/ardour/ardour/utils.h index 73b3ccc0d6..dfc71a5c88 100644 --- a/libs/ardour/ardour/utils.h +++ b/libs/ardour/ardour/utils.h @@ -44,10 +44,15 @@ class XMLNode; +namespace Temporal { + class Meter; +} + namespace ARDOUR { class Route; class Track; +class Region; LIBARDOUR_API std::string legalize_for_path (const std::string& str); LIBARDOUR_API std::string legalize_for_universal_path (const std::string& str); @@ -146,6 +151,8 @@ template std::shared_ptr stripable_list_to_co return cl; } +LIBARDOUR_API bool estimate_audio_tempo (std::shared_ptr region, Sample* data, samplecnt_t data_length, samplecnt_t sample_rate, double& qpm, Temporal::Meter& meter, double& beatcount); + #if __APPLE__ LIBARDOUR_API std::string CFStringRefToStdString(CFStringRef stringRef); #endif // __APPLE__ diff --git a/libs/ardour/triggerbox.cc b/libs/ardour/triggerbox.cc index a73a091cde..1c6ccb0cc5 100644 --- a/libs/ardour/triggerbox.cc +++ b/libs/ardour/triggerbox.cc @@ -1773,123 +1773,11 @@ AudioTrigger::set_region_in_worker_thread_internal (std::shared_ptr r, b void AudioTrigger::estimate_tempo () { - using namespace Temporal; - TempoMap::SharedPtr tm (TempoMap::use()); + double beatcount; + ARDOUR::estimate_audio_tempo (_region, data[0], data.length, _box.session().sample_rate(), _estimated_tempo, _meter, beatcount); + /* initialize our follow_length to match the beatcnt ... user can later change this value to have the clip end sooner or later than its data length */ + set_follow_length(Temporal::BBT_Offset( 0, rint(beatcount), 0)); - TimelineRange range (_region->start(), _region->start() + _region->length(), 0); - SegmentDescriptor segment; - bool have_segment; - - have_segment = _region->source (0)->get_segment_descriptor (range, segment); - - if (have_segment) { - - _estimated_tempo = segment.tempo().quarter_notes_per_minute (); - _meter = segment.meter(); - DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1: tempo and meter from segment descriptor\n", index())); - - } else { - /* not a great guess, but what else can we do? */ - - TempoMetric const & metric (tm->metric_at (timepos_t (AudioTime))); - - _meter = metric.meter (); - - /* check the name to see if there's a (heuristically obvious) hint - * about the tempo. - */ - - string str = _region->name(); - string::size_type bi; - string::size_type ni; - double text_tempo = -1.; - - if (((bi = str.find (" bpm")) != string::npos) || - ((bi = str.find ("bpm")) != string::npos) || - ((bi = str.find (" BPM")) != string::npos) || - ((bi = str.find ("BPM")) != string::npos) ){ - - string sub (str.substr (0, bi)); - - if ((ni = sub.find_last_of ("0123456789.,_-")) != string::npos) { - - int nni = ni; /* ni is unsigned, nni is signed */ - - while (nni >= 0) { - if (!isdigit (sub[nni]) && - (sub[nni] != '.') && - (sub[nni] != ',')) { - break; - } - --nni; - } - - if (nni > 0) { - std::stringstream p (sub.substr (nni + 1)); - p >> text_tempo; - if (!p) { - text_tempo = -1.; - } else { - _estimated_tempo = text_tempo; - } - } - } - } - - if (text_tempo < 0) { - - breakfastquay::MiniBPM mbpm (_box.session().sample_rate()); - - _estimated_tempo = mbpm.estimateTempoOfSamples (data[0], data.length); - - //cerr << name() << "MiniBPM Estimated: " << _estimated_tempo << " bpm from " << (double) data.length / _box.session().sample_rate() << " seconds\n"; - } - } - - const double seconds = (double) data.length / _box.session().sample_rate(); - - /* now check the determined tempo and force it to a value that gives us - an integer beat/quarter count. This is a heuristic that tries to - avoid clips that slightly over- or underrun a quantization point, - resulting in small or larger gaps in output if they are repeating. - */ - - if ((_estimated_tempo != 0.)) { - /* fractional beatcnt */ - double maybe_beats = (seconds / 60.) * _estimated_tempo; - double beatcount = round (maybe_beats); - - /* the vast majority of third-party clips are 1,2,4,8, or 16-bar 'beats'. - * Given no other metadata, it makes things 'just work' if we assume 4/4 time signature, and power-of-2 bars (1,2,4,8 or 16) - * TODO: someday we could provide a widget for users who have unlabeled, un-metadata'd, clips that they *know* are 3/4 or 5/4 or 11/4 */ - { - double barcount = round (beatcount/4); - if (barcount <= 18) { /* why not 16 here? fuzzy logic allows minibpm to misjudge the clip a bit */ - for (int pwr = 0; pwr <= 4; pwr++) { - float bc = pow(2,pwr); - if (barcount <= bc) { - barcount = bc; - break; - } - } - } - beatcount = round(barcount * 4); - } - - DEBUG_RESULT (double, est, _estimated_tempo); - _estimated_tempo = beatcount / (seconds/60.); - DEBUG_TRACE (DEBUG::Triggers, string_compose ("given original estimated tempo %1, rounded beatcnt is %2 : resulting in working bpm = %3\n", est, _beatcnt, _estimated_tempo)); - - /* initialize our follow_length to match the beatcnt ... user can later change this value to have the clip end sooner or later than its data length */ - set_follow_length(Temporal::BBT_Offset( 0, rint(beatcount), 0)); - } - -#if 0 - cerr << "estimated tempo: " << _estimated_tempo << endl; - const samplecnt_t one_beat = tm->bbt_duration_at (timepos_t (AudioTime), BBT_Offset (0, 1, 0)).samples(); - cerr << "one beat in samples: " << one_beat << endl; - cerr << "rounded beatcount = " << round (beatcount) << endl; -#endif } bool diff --git a/libs/ardour/utils.cc b/libs/ardour/utils.cc index c42d400839..2bf45bd7fc 100644 --- a/libs/ardour/utils.cc +++ b/libs/ardour/utils.cc @@ -60,8 +60,14 @@ #include "pbd/strsplit.h" #include "pbd/replace_all.h" -#include "ardour/utils.h" +#include "temporal/tempo.h" + +#include "ardour/minibpm.h" +#include "ardour/region.h" #include "ardour/rc_configuration.h" +#include "ardour/segment_descriptor.h" +#include "ardour/source.h" +#include "ardour/utils.h" #include "pbd/i18n.h" @@ -793,3 +799,126 @@ ARDOUR::compute_sha1_of_file (std::string path) sha1_result_hash (&s, hash); return std::string (hash); } + +bool +ARDOUR::estimate_audio_tempo (std::shared_ptr region, Sample* data, samplecnt_t data_length, samplecnt_t sample_rate, double& qpm, Temporal::Meter& meter, double& beatcount) +{ + using namespace Temporal; + TempoMap::SharedPtr tm (TempoMap::use()); + + TimelineRange range (region->start(), region->start() + region->length(), 0); + SegmentDescriptor segment; + bool have_segment; + + have_segment = region->source (0)->get_segment_descriptor (range, segment); + + if (have_segment) { + + qpm = segment.tempo().quarter_notes_per_minute (); + meter = segment.meter(); + // DEBUG_TRACE (DEBUG::Triggers, string_compose ("%1: tempo and meter from segment descriptor\n", index())); + + } else { + /* not a great guess, but what else can we do? */ + + TempoMetric const & metric (tm->metric_at (timepos_t (AudioTime))); + + meter = metric.meter (); + + /* check the name to see if there's a (heuristically obvious) hint + * about the tempo. + */ + + string str = region->name(); + string::size_type bi; + string::size_type ni; + double text_tempo = -1.; + + if (((bi = str.find (" bpm")) != string::npos) || + ((bi = str.find ("bpm")) != string::npos) || + ((bi = str.find (" BPM")) != string::npos) || + ((bi = str.find ("BPM")) != string::npos) ){ + + string sub (str.substr (0, bi)); + + if ((ni = sub.find_last_of ("0123456789.,_-")) != string::npos) { + + int nni = ni; /* ni is unsigned, nni is signed */ + + while (nni >= 0) { + if (!isdigit (sub[nni]) && + (sub[nni] != '.') && + (sub[nni] != ',')) { + break; + } + --nni; + } + + if (nni > 0) { + std::stringstream p (sub.substr (nni + 1)); + p >> text_tempo; + if (!p) { + text_tempo = -1.; + } else { + qpm = text_tempo; + } + } + } + } + + if (text_tempo < 0) { + + breakfastquay::MiniBPM mbpm (sample_rate); + + qpm = mbpm.estimateTempoOfSamples (data, data_length); + + //cerr << name() << "MiniBPM Estimated: " << qpm << " bpm from " << (double) data.length / _box.session().sample_rate() << " seconds\n"; + } + } + + const double seconds = (double) data_length / sample_rate; + + /* now check the determined tempo and force it to a value that gives us + an integer beat/quarter count. This is a heuristic that tries to + avoid clips that slightly over- or underrun a quantization point, + resulting in small or larger gaps in output if they are repeating. + */ + + if ((qpm != 0.)) { + /* fractional beatcnt */ + double maybe_beats = (seconds / 60.) * qpm; + beatcount = round (maybe_beats); + + /* the vast majority of third-party clips are 1,2,4,8, or 16-bar 'beats'. + * Given no other metadata, it makes things 'just work' if we assume 4/4 time signature, and power-of-2 bars (1,2,4,8 or 16) + * TODO: someday we could provide a widget for users who have unlabeled, un-metadata'd, clips that they *know* are 3/4 or 5/4 or 11/4 */ + { + double barcount = round (beatcount/4); + if (barcount <= 18) { /* why not 16 here? fuzzy logic allows minibpm to misjudge the clip a bit */ + for (int pwr = 0; pwr <= 4; pwr++) { + float bc = pow(2,pwr); + if (barcount <= bc) { + barcount = bc; + break; + } + } + } + beatcount = round(barcount * 4); + } + + // DEBUG_RESULT (double, est, qpm); + qpm = beatcount / (seconds/60.); + // DEBUG_TRACE (DEBUG::Triggers, string_compose ("given original estimated tempo %1, rounded beatcnt is %2 : resulting in working bpm = %3\n", est, _beatcnt, qpm)); + + } + +#if 0 + cerr << "estimated tempo: " << qpm << endl; + const samplecnt_t one_beat = tm->bbt_duration_at (timepos_t (AudioTime), BBT_Offset (0, 1, 0)).samples(); + cerr << "one beat in samples: " << one_beat << endl; + cerr << "rounded beatcount = " << round (beatcount) << endl; +#endif + + return true; +} +