@@ -268,20 +268,27 @@ namespace {
268268
269269// Normalize onsets by standard deviation
270270ArrayXr normalize_onsets (const ArrayXr& onsets) {
271- Real mean = onsets.mean ();
272- Real std = std::sqrt ((onsets - mean).square ().mean ());
273- if (std < util::tiny<Real>()) {
274- return onsets;
271+ if (onsets.size () <= 1 ) {
272+ return onsets / util::tiny<Real>();
275273 }
276- return onsets / std;
274+
275+ Real mean = onsets.mean ();
276+ Real variance = (onsets - mean).square ().sum () /
277+ static_cast <Real>(onsets.size () - 1 );
278+ Real std = std::sqrt (variance);
279+ return onsets / (std + util::tiny<Real>());
280+ }
281+
282+ int round_to_nearest_even (Real value) {
283+ return static_cast <int >(std::nearbyint (value));
277284}
278285
279286// Compute local score with Gaussian weighting
280287ArrayXr beat_local_score (const ArrayXr& onset_envelope, Real frames_per_beat) {
281288 Eigen::Index N = onset_envelope.size ();
282289 ArrayXr localscore (N);
283290
284- int fpb = static_cast < int >( std::round ( frames_per_beat) );
291+ int fpb = round_to_nearest_even ( frames_per_beat);
285292 int window_size = 2 * fpb + 1 ;
286293
287294 // Create Gaussian window
@@ -294,11 +301,15 @@ ArrayXr beat_local_score(const ArrayXr& onset_envelope, Real frames_per_beat) {
294301 // Same-mode convolution
295302 for (Eigen::Index i = 0 ; i < N; ++i) {
296303 Real sum = 0.0 ;
297- for (int k = 0 ; k < window_size; ++k) {
304+ Eigen::Index half_window = window_size / 2 ;
305+ Eigen::Index k_start = std::max<Eigen::Index>(
306+ 0 , i + half_window - N + 1 );
307+ Eigen::Index k_stop = std::min<Eigen::Index>(
308+ i + half_window + 1 , window_size);
309+
310+ for (Eigen::Index k = k_start; k < k_stop; ++k) {
298311 Eigen::Index j = i + window_size / 2 - k;
299- if (j >= 0 && j < N) {
300- sum += window (k) * onset_envelope (j);
301- }
312+ sum += window (k) * onset_envelope (j);
302313 }
303314 localscore (i) = sum;
304315 }
@@ -320,19 +331,21 @@ std::pair<std::vector<int>, ArrayXr> beat_track_dp(
320331 Real score_thresh = 0.01 * localscore.maxCoeff ();
321332 bool first_beat = true ;
322333
334+ backlink[0 ] = -1 ;
323335 cumscore (0 ) = localscore (0 );
324336
325- int fpb = static_cast <int >(std::round (frames_per_beat));
337+ int fpb = round_to_nearest_even (frames_per_beat);
338+ int first_lag = round_to_nearest_even (static_cast <Real>(fpb) / 2.0 );
326339
327- for (Eigen::Index i = 1 ; i < N; ++i) {
340+ for (Eigen::Index i = 0 ; i < N; ++i) {
328341 Real best_score = -std::numeric_limits<Real>::infinity ();
329342 int beat_location = -1 ;
330343
331344 // Search over possible predecessors
332- Eigen::Index search_start = std::max ( Eigen::Index ( 0 ), i - 2 * fpb);
333- Eigen::Index search_end = std::max ( Eigen::Index ( 0 ), i - fpb / 2 );
334-
335- for (Eigen::Index loc = search_start; loc < search_end; ++loc) {
345+ for ( Eigen::Index loc = i - first_lag; loc >= i - 2 * fpb; --loc) {
346+ if (loc < 0 ) {
347+ break ;
348+ }
336349 Real penalty = std::log (static_cast <Real>(i - loc)) - std::log (frames_per_beat);
337350 Real score = cumscore (loc) - tightness * penalty * penalty;
338351 if (score > best_score) {
@@ -358,25 +371,52 @@ std::pair<std::vector<int>, ArrayXr> beat_track_dp(
358371 return {backlink, cumscore};
359372}
360373
361- // Backtrack from the best ending point
362- std::vector<bool > dp_backtrack (const std::vector<int >& backlink, const ArrayXr& cumscore) {
374+ Real median (std::vector<Real> values) {
375+ if (values.empty ()) {
376+ return 0.0 ;
377+ }
378+
379+ std::sort (values.begin (), values.end ());
380+ size_t mid = values.size () / 2 ;
381+ if (values.size () % 2 == 1 ) {
382+ return values[mid];
383+ }
384+
385+ return 0.5 * (values[mid - 1 ] + values[mid]);
386+ }
387+
388+ Eigen::Index last_beat (const ArrayXr& cumscore) {
363389 Eigen::Index N = cumscore.size ();
364- std::vector<bool > beats (N, false );
390+ auto localmax = util::localmax (cumscore);
391+
392+ std::vector<Real> local_scores;
393+ local_scores.reserve (static_cast <size_t >(N));
394+ for (Eigen::Index i = 0 ; i < N; ++i) {
395+ if (localmax (i)) {
396+ local_scores.push_back (cumscore (i));
397+ }
398+ }
365399
366- // Find the last beat (max cumscore in the last portion)
367- Eigen::Index search_start = std::max (Eigen::Index (0 ), N - N / 4 );
368- Eigen::Index tail = search_start;
369- Real max_score = cumscore (search_start);
400+ Real threshold = 0.5 * median (local_scores);
370401
371- for ( Eigen::Index i = search_start + 1 ; i < N; ++i) {
372- if ( cumscore (i) > max_score ) {
373- max_score = cumscore (i);
402+ Eigen::Index tail = N - 1 ;
403+ for (Eigen::Index i = N - 1 ; i >= 0 ; --i ) {
404+ if ( localmax (i) && cumscore (i) >= threshold) {
374405 tail = i;
406+ break ;
375407 }
376408 }
377409
410+ return tail;
411+ }
412+
413+ // Backtrack from the best ending point
414+ std::vector<bool > dp_backtrack (const std::vector<int >& backlink, const ArrayXr& cumscore) {
415+ Eigen::Index N = cumscore.size ();
416+ std::vector<bool > beats (N, false );
417+
378418 // Backtrack
379- Eigen::Index idx = tail ;
419+ Eigen::Index idx = last_beat (cumscore) ;
380420 while (idx >= 0 ) {
381421 beats[idx] = true ;
382422 idx = backlink[idx];
@@ -393,7 +433,7 @@ std::vector<bool> trim_beats(const ArrayXr& localscore, const std::vector<bool>&
393433 return trimmed;
394434 }
395435
396- // Compute threshold based on beat onsets
436+ // Compute the smoothed beat-onset envelope threshold.
397437 std::vector<Real> beat_scores;
398438 for (size_t i = 0 ; i < beats.size (); ++i) {
399439 if (beats[i]) {
@@ -405,34 +445,43 @@ std::vector<bool> trim_beats(const ArrayXr& localscore, const std::vector<bool>&
405445 return trimmed;
406446 }
407447
408- // RMS of beat scores
409- Real rms = 0 ;
410- for (Real s : beat_scores) {
411- rms += s * s;
448+ std::vector<Real> window = {0.0 , 0.5 , 1.0 , 0.5 , 0.0 };
449+ std::vector<Real> smooth_boe (beat_scores.size () + window.size () - 1 , 0.0 );
450+ for (size_t i = 0 ; i < beat_scores.size (); ++i) {
451+ for (size_t j = 0 ; j < window.size (); ++j) {
452+ smooth_boe[i + j] += beat_scores[i] * window[j];
453+ }
412454 }
413- rms = std::sqrt (rms / beat_scores.size ());
414- Real threshold = 0.5 * rms;
415455
416- // Trim leading weak beats
417- for (size_t i = 0 ; i < trimmed.size (); ++i) {
418- if (trimmed[i]) {
419- if (localscore (i) <= threshold) {
420- trimmed[i] = false ;
421- } else {
422- break ;
423- }
424- }
456+ size_t start = window.size () / 2 ;
457+ size_t stop = std::min (
458+ smooth_boe.size (),
459+ static_cast <size_t >(localscore.size ()) + window.size () / 2 );
460+
461+ Real mean_square = 0.0 ;
462+ size_t smooth_count = 0 ;
463+ for (size_t i = start; i < stop; ++i) {
464+ mean_square += smooth_boe[i] * smooth_boe[i];
465+ ++smooth_count;
425466 }
426467
427- // Trim trailing weak beats
428- for (int i = static_cast <int >(trimmed.size ()) - 1 ; i >= 0 ; --i) {
429- if (trimmed[i]) {
430- if (localscore (i) <= threshold) {
431- trimmed[i] = false ;
432- } else {
433- break ;
434- }
435- }
468+ Real threshold = 0.0 ;
469+ if (trim && smooth_count > 0 ) {
470+ threshold = 0.5 * std::sqrt (mean_square / static_cast <Real>(smooth_count));
471+ }
472+
473+ // Match librosa.beat.__trim_beats: the threshold is computed from selected
474+ // beat scores, but edge suppression scans frame-local scores.
475+ Eigen::Index n = 0 ;
476+ while (n < localscore.size () && localscore (n) <= threshold) {
477+ trimmed[static_cast <size_t >(n)] = false ;
478+ ++n;
479+ }
480+
481+ n = localscore.size () - 1 ;
482+ while (n >= 0 && localscore (n) <= threshold) {
483+ trimmed[static_cast <size_t >(n)] = false ;
484+ --n;
436485 }
437486
438487 return trimmed;
@@ -469,7 +518,7 @@ std::pair<Real, std::vector<Eigen::Index>> beat_track(
469518
470519 // Convert BPM to frames per beat
471520 Real frame_rate = sr / hop_length;
472- Real frames_per_beat = std::round (frame_rate * 60.0 / bpm_val);
521+ Real frames_per_beat = std::nearbyint (frame_rate * 60.0 / bpm_val);
473522
474523 // Normalize onsets
475524 ArrayXr normalized = normalize_onsets (onset_envelope);
@@ -519,7 +568,8 @@ std::pair<Real, std::vector<Eigen::Index>> beat_track_audio(
519568 BeatUnits units) {
520569
521570 // Compute onset envelope
522- ArrayXr envelope = onset::onset_strength (y, sr, 2048 , hop_length);
571+ ArrayXr envelope = onset::onset_strength (y, sr, 2048 , hop_length, 1 , 1 ,
572+ false , true , AggregateFunc::Median);
523573
524574 return beat_track (envelope, sr, hop_length, start_bpm, tightness, trim, bpm_opt, units);
525575}
0 commit comments