| """Tempo map derivation from beat tracker output.""" |
|
|
| import numpy as np |
|
|
| from midmid.beat_tracker import BeatData |
|
|
|
|
| def derive_tempo_map( |
| beat_data: BeatData, change_threshold: float = 0.08, |
| ) -> list[tuple[float, float]]: |
| """Derive a tempo map from beat data. |
| |
| Returns list of (time_seconds, bpm) tuples, sorted by time. |
| """ |
| beats = beat_data.beats |
| if len(beats) < 2: |
| return [(0.0, 120.0)] |
|
|
| intervals = np.diff(beats) |
| bpms = 60.0 / intervals |
|
|
| median_bpm = np.median(bpms) |
| valid = (bpms > median_bpm * 0.6) & (bpms < median_bpm * 1.6) |
| if not np.any(valid): |
| return [(0.0, float(median_bpm))] |
|
|
| valid_bpms = bpms[valid] |
| if np.std(valid_bpms) / np.mean(valid_bpms) < change_threshold: |
| avg_bpm = float(np.mean(valid_bpms)) |
| return [(0.0, _round_bpm(avg_bpm))] |
|
|
| tempo_map = [] |
| current_bpm = float(bpms[0]) if valid[0] else float(median_bpm) |
| tempo_map.append((0.0, _round_bpm(current_bpm))) |
|
|
| window = 4 |
| for i in range(window, len(bpms) - window + 1, window): |
| chunk = bpms[i : i + window] |
| chunk_valid = chunk[(chunk > median_bpm * 0.6) & (chunk < median_bpm * 1.6)] |
| if len(chunk_valid) == 0: |
| continue |
| local_bpm = float(np.mean(chunk_valid)) |
| if abs(local_bpm - current_bpm) / current_bpm > change_threshold: |
| current_bpm = local_bpm |
| tempo_map.append((float(beats[i]), _round_bpm(current_bpm))) |
|
|
| return tempo_map |
|
|
|
|
| def get_median_bpm(beat_data: BeatData) -> float: |
| if len(beat_data.beats) < 2: |
| return 120.0 |
| intervals = np.diff(beat_data.beats) |
| bpms = 60.0 / intervals |
| return float(_round_bpm(np.median(bpms))) |
|
|
|
|
| def estimate_time_signature(beat_data: BeatData) -> int: |
| if len(beat_data.downbeats) < 2: |
| return 4 |
|
|
| beats = beat_data.beats |
| downbeats = beat_data.downbeats |
|
|
| counts = [] |
| for i in range(len(downbeats) - 1): |
| start, end = downbeats[i], downbeats[i + 1] |
| n = np.sum((beats >= start) & (beats < end)) |
| if 2 <= n <= 7: |
| counts.append(n) |
|
|
| if not counts: |
| return 4 |
|
|
| values, freq = np.unique(counts, return_counts=True) |
| return int(values[np.argmax(freq)]) |
|
|
|
|
| def _round_bpm(bpm: float) -> float: |
| return round(float(bpm), 2) |
|
|