File size: 3,161 Bytes
b2e4883
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#!/usr/bin/env python3
"""Feasibility spike: can we detect the tempo curve + onsets on a real DI track?

Runs librosa onset detection + frame-wise tempo on the DI WAV and compares
against the notated tempo from the MIDI time map (if given).
"""
import argparse
import sys

import librosa
import numpy as np


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("wav", help="DI track WAV path")
    ap.add_argument("--sr", type=int, default=22050, help="resample target")
    args = ap.parse_args()

    print(f"Loading {args.wav} ...")
    y, sr = librosa.load(args.wav, sr=args.sr, mono=True)
    dur = len(y) / sr
    print(f"  duration {dur:.1f}s, sr {sr}, samples {len(y)}")

    # --- 1. Onset strength envelope ---
    print("\n[onset] detecting onset strength ...")
    onset_env = librosa.onset.onset_strength(y=y, sr=sr)
    onsets = librosa.onset.onset_detect(onset_envelope=onset_env, sr=sr,
                                        units="time", backtrack=True)
    print(f"  {len(onsets)} onsets")
    # onset rate (hits/sec) -> coarse BPM indicator
    if len(onsets) > 1:
        inter = np.diff(onsets)
        inter = inter[inter > 0.08]  # ignore sub-80ms jitter
        if len(inter):
            rate = 60.0 / np.median(inter)
            print(f"  median inter-onset {np.median(inter):.3f}s -> ~{rate:.1f} BPM")

    # --- 2. Frame-wise tempo curve ---
    print("\n[tempo] frame-wise tempo curve ...")
    tempo_out = librosa.feature.tempo(
        onset_envelope=onset_env, sr=sr, aggregate=None,
    )
    # some librosa builds return a scalar when aggregate=None; wrap if needed
    if np.isscalar(tempo_out) or (np.ndim(tempo_out) == 0):
        tempo_curve = np.full(onset_env.shape[-1], float(tempo_out))
    else:
        tempo_curve = np.asarray(tempo_out).reshape(-1)
    hop = librosa.get_hop_length() if hasattr(librosa, "get_hop_length") else 512
    frame_dur = hop / sr
    n = len(tempo_curve)
    # Robust per-window median in 4s windows
    win = max(1, int(4.0 / frame_dur))
    print(f"  {n} frames, frame {frame_dur:.2f}s")
    t = 0.0
    i = 0
    print("\n  time(s)  ~BPM(median 4s window)")
    seen = []
    while i < n:
        w = tempo_curve[i:i + win]
        med = float(np.median(w[np.isfinite(w)])) if np.any(np.isfinite(w)) else float("nan")
        t = i * frame_dur
        seen.append((t, med))
        print(f"  {t:7.1f}   {med:6.1f}")
        i += win
    # distinct tempo regimes
    uniq = sorted(set(round(m) for _, m in seen if np.isfinite(m)))
    print("\n  distinct ~tempo values:", uniq)

    # --- 3. Beat tracking (librosa default) ---
    print("\n[beat] beat_track ...")
    tempo_est, beats = librosa.beat.beat_track(onset_envelope=onset_env, sr=sr)
    if np.ndim(tempo_est):
        tempo_est = float(np.median(np.asarray(tempo_est)))
    beat_times = librosa.frames_to_time(beats, sr=sr)
    print(f"  est BPM {float(tempo_est):.1f}, {len(beat_times)} beats")
    if len(beat_times) > 1:
        ii = np.diff(beat_times); ii = ii[ii > 0]
        print(f"  median beat interval {np.median(ii):.3f}s -> ~{60/np.median(ii):.1f} BPM")


if __name__ == "__main__":
    main()