gabriel / muse public
tempo.py python
100 lines 3.7 KB
Raw
sha256:f9828efc523c2f8ccaee12ae76086a09a9a6a10d6dd20e53f0b13793f0fdcf50 docs: add symlog (#53) and reflog (#54) follow-up issue files Sonnet 4.6 19 days ago
1 """muse tempo — estimate and report the tempo of a MIDI track.
2
3 Estimates BPM from inter-onset intervals and reports the ticks-per-beat
4 metadata. For agent workflows that need to match tempo across branches or
5 verify that time-stretching operations preserved the rhythmic grid.
6
7 Usage::
8
9 muse tempo tracks/drums.mid
10 muse tempo tracks/bass.mid --commit HEAD~2
11 muse tempo tracks/melody.mid --json
12
13 Output::
14
15 Tempo analysis: tracks/drums.mid — working tree
16 Estimated BPM: 120.0
17 Ticks per beat: 480
18 Confidence: high (ioi_voting method)
19
20 Note: BPM is estimated from inter-onset intervals.
21 For authoritative BPM, embed a MIDI tempo event at tick 0.
22 """
23
24 import argparse
25 import json
26 import logging
27 import pathlib
28 import sys
29
30 from muse.core.types import short_id
31 from muse.core.errors import ExitCode
32 from muse.core.repo import require_repo
33 from muse.core.refs import read_current_branch
34 from muse.core.commits import resolve_commit_ref
35 from muse.plugins.midi._analysis import estimate_tempo
36 from muse.plugins.midi._query import load_track, load_track_from_workdir
37
38 logger = logging.getLogger(__name__)
39
40 def _read_branch(root: pathlib.Path) -> str:
41 return read_current_branch(root)
42
43 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
44 """Register the tempo subcommand."""
45 parser = subparsers.add_parser("tempo", help="Estimate the BPM of a MIDI track from inter-onset intervals.", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
46 parser.add_argument("track", metavar="TRACK", help="Workspace-relative path to a .mid file.")
47 parser.add_argument("--commit", "-c", metavar="REF", default=None, dest="ref", help="Analyse a historical snapshot instead of the working tree.")
48 parser.add_argument("--json", action="store_true", dest="as_json", help="Emit results as JSON.")
49 parser.set_defaults(func=run)
50
51 def run(args: argparse.Namespace) -> None:
52 """Estimate the BPM of a MIDI track from inter-onset intervals.
53
54 ``muse tempo`` uses IOI voting to estimate the underlying beat duration
55 and converts it to BPM. Confidence is rated high/medium/low based on
56 how consistently notes cluster around a common beat subdivision.
57
58 For agents: use this to verify that time-stretch transformations
59 produced the expected tempo, or to detect BPM drift between branches.
60 """
61 track: str = args.track
62 ref: str | None = args.ref
63 as_json: bool = args.as_json
64
65 root = require_repo()
66 commit_label = "working tree"
67
68 if ref is not None:
69 branch = _read_branch(root)
70 commit = resolve_commit_ref(root, branch, ref)
71 if commit is None:
72 print(f"❌ Commit '{ref}' not found.", file=sys.stderr)
73 raise SystemExit(ExitCode.USER_ERROR)
74 result = load_track(root, commit.commit_id, track)
75 commit_label = commit.commit_id
76 else:
77 result = load_track_from_workdir(root, track)
78
79 if result is None:
80 print(f"❌ Track '{track}' not found or not a valid MIDI file.", file=sys.stderr)
81 raise SystemExit(ExitCode.USER_ERROR)
82
83 notes, _tpb = result
84 if not notes:
85 print(f" (no notes found in '{track}')")
86 return
87
88 est = estimate_tempo(notes)
89
90 if as_json:
91 print(json.dumps({"track": track, "commit": commit_label, **est}))
92 return
93
94 print(f"\nTempo analysis: {track} — {commit_label}")
95 print(f"Estimated BPM: {est['estimated_bpm']}")
96 print(f"Ticks per beat: {est['ticks_per_beat']}")
97 print(f"Confidence: {est['confidence']} ({est['method']} method)")
98 print("")
99 print("Note: BPM is estimated from inter-onset intervals.")
100 print("For authoritative BPM, embed a MIDI tempo event at tick 0.")
File History 1 commit
sha256:f9828efc523c2f8ccaee12ae76086a09a9a6a10d6dd20e53f0b13793f0fdcf50 docs: add symlog (#53) and reflog (#54) follow-up issue files Sonnet 4.6 19 days ago