cadence.py python
116 lines 4.1 KB
Raw
sha256:cf6265cea8c21d9228d90dec13ef6ec2dab5103d466db9cc4590681832de4bf8 docs(KD-STAGING): sync governance after KD-6b DONE Human 12 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 from __future__ import annotations
27
28 import argparse
29 import json
30 import logging
31 import pathlib
32 import sys
33
34 from muse.core.errors import ExitCode
35 from muse.core.repo import read_repo_id, require_repo
36 from muse.core.store import read_current_branch, resolve_commit_ref
37 from muse.plugins.midi._analysis import detect_cadences
38 from muse.plugins.midi._query import load_track, load_track_from_workdir
39
40 logger = logging.getLogger(__name__)
41
42
43
44 def _read_branch(root: pathlib.Path) -> str:
45 return read_current_branch(root)
46
47
48 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
49 """Register the cadence subcommand."""
50 parser = subparsers.add_parser("cadence", help="Detect phrase-ending cadences in a MIDI track.", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
51 parser.add_argument("track", metavar="TRACK", help="Workspace-relative path to a .mid file.")
52 parser.add_argument("--commit", "-c", metavar="REF", default=None, dest="ref", help="Analyse a historical snapshot instead of the working tree.")
53 parser.add_argument("--json", action="store_true", dest="as_json", help="Emit results as JSON.")
54 parser.set_defaults(func=run)
55
56
57 def run(args: argparse.Namespace) -> None:
58 """Detect phrase-ending cadences in a MIDI track.
59
60 ``muse cadence`` identifies authentic, deceptive, half, and plagal
61 cadences by examining chord motions at phrase boundaries (every 4 bars).
62
63 Agents can use this to:
64 - Verify that phrase structure matches an intended form.
65 - Flag compositions where phrase endings lack proper resolution.
66 - Compare cadence patterns across branches to detect structural drift.
67
68 Git cannot do this — it has no concept of musical phrase structure.
69 """
70 track: str = args.track
71 ref: str | None = args.ref
72 as_json: bool = args.as_json
73
74 root = require_repo()
75 commit_label = "working tree"
76
77 if ref is not None:
78 repo_id = read_repo_id(root)
79 branch = _read_branch(root)
80 commit = resolve_commit_ref(root, repo_id, branch, ref)
81 if commit is None:
82 print(f"❌ Commit '{ref}' not found.", file=sys.stderr)
83 raise SystemExit(ExitCode.USER_ERROR)
84 result = load_track(root, commit.commit_id, track)
85 commit_label = commit.commit_id[:8]
86 else:
87 result = load_track_from_workdir(root, track)
88
89 if result is None:
90 print(f"❌ Track '{track}' not found or not a valid MIDI file.", file=sys.stderr)
91 raise SystemExit(ExitCode.USER_ERROR)
92
93 notes, _tpb = result
94 if not notes:
95 print(f" (no notes found in '{track}')")
96 return
97
98 cadences = detect_cadences(notes)
99
100 if as_json:
101 print(json.dumps(
102 {"track": track, "commit": commit_label, "cadences": list(cadences)},
103 indent=2,
104 ))
105 return
106
107 print(f"\nCadence analysis: {track} — {commit_label}")
108 if not cadences:
109 print(" (no cadences detected — track may be too short or lack chords)")
110 return
111
112 print(f"Found {len(cadences)} cadence{'s' if len(cadences) != 1 else ''}\n")
113 print(f" {'Bar':>4} {'Type':<14} {'From':<12} {'To':<12}")
114 print(" " + "─" * 46)
115 for c in cadences:
116 print(f" {c['bar']:>4} {c['cadence_type']:<14} {c['from_chord']:<12} {c['to_chord']:<12}")
File History 1 commit
sha256:cf6265cea8c21d9228d90dec13ef6ec2dab5103d466db9cc4590681832de4bf8 docs(KD-STAGING): sync governance after KD-6b DONE Human 12 days ago