gabriel / muse public
cadence.py python
109 lines 4.0 KB
Raw
sha256:144495ae90498f9d51aeac4bf7f9ee9e9502d14474745a856b96921f7349c0ce next round of fixing tests Human 103 days ago
1 """muse cadence — cadence detection for a MIDI track.
2
3 Identifies phrase endings (authentic, deceptive, half, plagal cadences) by
4 examining chord motions at bar boundaries. Agents composing or reviewing
5 multi-section music need automated cadence detection to enforce correct
6 phrase structure without listening to audio.
7
8 Usage::
9
10 muse cadence tracks/chords.mid
11 muse cadence tracks/piano.mid --commit HEAD~1
12 muse cadence tracks/strings.mid --json
13
14 Output::
15
16 Cadence analysis: tracks/chords.mid — working tree
17 Found 3 cadences
18
19 Bar Type From To
20 ──────────────────────────────────────
21 5 authentic Gdom7 Cmaj
22 9 half Cmaj Gdom7
23 13 authentic Ddom7 Gmaj
24 """
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 require_repo
34 from muse.core.refs import read_current_branch
35 from muse.core.commits import resolve_commit_ref
36 from muse.plugins.midi._analysis import detect_cadences
37 from muse.plugins.midi._query import load_track, load_track_from_workdir
38
39 logger = logging.getLogger(__name__)
40
41 def _read_branch(root: pathlib.Path) -> str:
42 return read_current_branch(root)
43
44 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
45 """Register the cadence subcommand."""
46 parser = subparsers.add_parser("cadence", help="Detect phrase-ending cadences in a MIDI track.", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
47 parser.add_argument("track", metavar="TRACK", help="Workspace-relative path to a .mid file.")
48 parser.add_argument("--commit", "-c", metavar="REF", default=None, dest="ref", help="Analyse a historical snapshot instead of the working tree.")
49 parser.add_argument("--json", action="store_true", dest="as_json", help="Emit results as JSON.")
50 parser.set_defaults(func=run)
51
52 def run(args: argparse.Namespace) -> None:
53 """Detect phrase-ending cadences in a MIDI track.
54
55 ``muse cadence`` identifies authentic, deceptive, half, and plagal
56 cadences by examining chord motions at phrase boundaries (every 4 bars).
57
58 Agents can use this to:
59 - Verify that phrase structure matches an intended form.
60 - Flag compositions where phrase endings lack proper resolution.
61 - Compare cadence patterns across branches to detect structural drift.
62
63 Git cannot do this — it has no concept of musical phrase structure.
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 branch = _read_branch(root)
74 commit = resolve_commit_ref(root, branch, ref)
75 if commit is None:
76 print(f"❌ Commit '{ref}' not found.", file=sys.stderr)
77 raise SystemExit(ExitCode.USER_ERROR)
78 result = load_track(root, commit.commit_id, track)
79 commit_label = commit.commit_id
80 else:
81 result = load_track_from_workdir(root, track)
82
83 if result is None:
84 print(f"❌ Track '{track}' not found or not a valid MIDI file.", file=sys.stderr)
85 raise SystemExit(ExitCode.USER_ERROR)
86
87 notes, _tpb = result
88 if not notes:
89 print(f" (no notes found in '{track}')")
90 return
91
92 cadences = detect_cadences(notes)
93
94 if as_json:
95 print(json.dumps(
96 {"track": track, "commit": commit_label, "cadences": list(cadences)},
97 ))
98 return
99
100 print(f"\nCadence analysis: {track} — {commit_label}")
101 if not cadences:
102 print(" (no cadences detected — track may be too short or lack chords)")
103 return
104
105 print(f"Found {len(cadences)} cadence{'s' if len(cadences) != 1 else ''}\n")
106 print(f" {'Bar':>4} {'Type':<14} {'From':<12} {'To':<12}")
107 print(f" {'─' * 46}")
108 for c in cadences:
109 print(f" {c['bar']:>4} {c['cadence_type']:<14} {c['from_chord']:<12} {c['to_chord']:<12}")
File History 1 commit
sha256:144495ae90498f9d51aeac4bf7f9ee9e9502d14474745a856b96921f7349c0ce next round of fixing tests Human 103 days ago