gabriel / muse public
cadence.py python
116 lines 4.1 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 132 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._types import short_id
35 from muse.core.errors import ExitCode
36 from muse.core.repo import read_repo_id, require_repo
37 from muse.core.store import read_current_branch, resolve_commit_ref
38 from muse.plugins.midi._analysis import detect_cadences
39 from muse.plugins.midi._query import load_track, load_track_from_workdir
40
41 logger = logging.getLogger(__name__)
42
43
44
45 def _read_branch(root: pathlib.Path) -> str:
46 return read_current_branch(root)
47
48
49 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
50 """Register the cadence subcommand."""
51 parser = subparsers.add_parser("cadence", help="Detect phrase-ending cadences in a MIDI track.", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
52 parser.add_argument("track", metavar="TRACK", help="Workspace-relative path to a .mid file.")
53 parser.add_argument("--commit", "-c", metavar="REF", default=None, dest="ref", help="Analyse a historical snapshot instead of the working tree.")
54 parser.add_argument("--json", action="store_true", dest="as_json", help="Emit results as JSON.")
55 parser.set_defaults(func=run)
56
57
58 def run(args: argparse.Namespace) -> None:
59 """Detect phrase-ending cadences in a MIDI track.
60
61 ``muse cadence`` identifies authentic, deceptive, half, and plagal
62 cadences by examining chord motions at phrase boundaries (every 4 bars).
63
64 Agents can use this to:
65 - Verify that phrase structure matches an intended form.
66 - Flag compositions where phrase endings lack proper resolution.
67 - Compare cadence patterns across branches to detect structural drift.
68
69 Git cannot do this — it has no concept of musical phrase structure.
70 """
71 track: str = args.track
72 ref: str | None = args.ref
73 as_json: bool = args.as_json
74
75 root = require_repo()
76 commit_label = "working tree"
77
78 if ref is not None:
79 repo_id = read_repo_id(root)
80 branch = _read_branch(root)
81 commit = resolve_commit_ref(root, repo_id, branch, ref)
82 if commit is None:
83 print(f"❌ Commit '{ref}' not found.", file=sys.stderr)
84 raise SystemExit(ExitCode.USER_ERROR)
85 result = load_track(root, commit.commit_id, track)
86 commit_label = short_id(commit.commit_id)
87 else:
88 result = load_track_from_workdir(root, track)
89
90 if result is None:
91 print(f"❌ Track '{track}' not found or not a valid MIDI file.", file=sys.stderr)
92 raise SystemExit(ExitCode.USER_ERROR)
93
94 notes, _tpb = result
95 if not notes:
96 print(f" (no notes found in '{track}')")
97 return
98
99 cadences = detect_cadences(notes)
100
101 if as_json:
102 print(json.dumps(
103 {"track": track, "commit": commit_label, "cadences": list(cadences)},
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(f" {'─' * 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 3 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 132 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 138 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 141 days ago