gabriel / muse public
cadence.py python
109 lines 4.0 KB
Raw
sha256:37af4ab271a64f11d7bf14b4871ebaaf5dd68fb0247521007c6b034d35aa01be Merge branch 'fix/hub-user-read-update-path' into dev Human 9 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 11 commits
sha256:37af4ab271a64f11d7bf14b4871ebaaf5dd68fb0247521007c6b034d35aa01be Merge branch 'fix/hub-user-read-update-path' into dev Human 9 days ago
sha256:b7be56ec091919a612cffe7f3c8b600209d5155517443fdac0e16954c8c7d81f Merge 'task/git-export-ignored-file-deletion' into 'dev' — … Human 12 days ago
sha256:8de4334a98c945aace420969d389ad678aa926d4ab4e886b2ac4c4241cb3bf2b revert: keep pyproject.toml in canonical PEP 440 form Sonnet 4.6 patch 15 days ago
sha256:50bb615c573aa4b928fca75b60f82ba2a659910066507cec6a95c412ae22ccb9 docs: add issue docs for push have-negotiation bug (#55) an… Sonnet 4.6 18 days ago
sha256:d90d175cded68aae1d4ffcf4858917854195d0cd8ce1fe73cee4dbc02541cb74 chore: trigger push to surface null-OID paths Sonnet 4.6 24 days ago
sha256:e452ad9a6ace6ccc6d875a35e06caf9da5576a970c1c36133b69a891ce5fefa8 chore: prebuild timing test Sonnet 4.6 34 days ago
sha256:0008ab6695e3e064b3e236b24fd19e538fef6a588eb0d211622f4466d919c0b1 merge: pull staging/dev — advance to 0.2.0rc12 Sonnet 4.6 patch 36 days ago
sha256:9c33d61749fff814c5226d5386aa2af7064c2c02788594a25fdd709358132eea fix: _PROPOSAL_PREFIX_RESOLVE_LIMIT 200 → 100 to match hub … Sonnet 4.6 47 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 50 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 56 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 57 days ago