tempo.py python
105 lines 3.8 KB
Raw
sha256:cf6265cea8c21d9228d90dec13ef6ec2dab5103d466db9cc4590681832de4bf8 docs(KD-STAGING): sync governance after KD-6b DONE Human 13 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 from __future__ import annotations
25
26 import argparse
27 import json
28 import logging
29 import pathlib
30 import sys
31
32 from muse.core.errors import ExitCode
33 from muse.core.repo import read_repo_id, require_repo
34 from muse.core.store import read_current_branch, 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
41
42 def _read_branch(root: pathlib.Path) -> str:
43 return read_current_branch(root)
44
45
46 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
47 """Register the tempo subcommand."""
48 parser = subparsers.add_parser("tempo", help="Estimate the BPM of a MIDI track from inter-onset intervals.", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
49 parser.add_argument("track", metavar="TRACK", help="Workspace-relative path to a .mid file.")
50 parser.add_argument("--commit", "-c", metavar="REF", default=None, dest="ref", help="Analyse a historical snapshot instead of the working tree.")
51 parser.add_argument("--json", action="store_true", dest="as_json", help="Emit results as JSON.")
52 parser.set_defaults(func=run)
53
54
55 def run(args: argparse.Namespace) -> None:
56 """Estimate the BPM of a MIDI track from inter-onset intervals.
57
58 ``muse tempo`` uses IOI voting to estimate the underlying beat duration
59 and converts it to BPM. Confidence is rated high/medium/low based on
60 how consistently notes cluster around a common beat subdivision.
61
62 For agents: use this to verify that time-stretch transformations
63 produced the expected tempo, or to detect BPM drift between branches.
64 """
65 track: str = args.track
66 ref: str | None = args.ref
67 as_json: bool = args.as_json
68
69 root = require_repo()
70 commit_label = "working tree"
71
72 if ref is not None:
73 repo_id = read_repo_id(root)
74 branch = _read_branch(root)
75 commit = resolve_commit_ref(root, repo_id, branch, ref)
76 if commit is None:
77 print(f"❌ Commit '{ref}' not found.", file=sys.stderr)
78 raise SystemExit(ExitCode.USER_ERROR)
79 result = load_track(root, commit.commit_id, track)
80 commit_label = commit.commit_id[:8]
81 else:
82 result = load_track_from_workdir(root, track)
83
84 if result is None:
85 print(f"❌ Track '{track}' not found or not a valid MIDI file.", file=sys.stderr)
86 raise SystemExit(ExitCode.USER_ERROR)
87
88 notes, _tpb = result
89 if not notes:
90 print(f" (no notes found in '{track}')")
91 return
92
93 est = estimate_tempo(notes)
94
95 if as_json:
96 print(json.dumps({"track": track, "commit": commit_label, **est}, indent=2))
97 return
98
99 print(f"\nTempo analysis: {track} — {commit_label}")
100 print(f"Estimated BPM: {est['estimated_bpm']}")
101 print(f"Ticks per beat: {est['ticks_per_beat']}")
102 print(f"Confidence: {est['confidence']} ({est['method']} method)")
103 print("")
104 print("Note: BPM is estimated from inter-onset intervals.")
105 print("For authoritative BPM, embed a MIDI tempo event at tick 0.")
File History 1 commit
sha256:cf6265cea8c21d9228d90dec13ef6ec2dab5103d466db9cc4590681832de4bf8 docs(KD-STAGING): sync governance after KD-6b DONE Human 13 days ago