tension.py python
118 lines 4.1 KB
Raw
sha256:cf6265cea8c21d9228d90dec13ef6ec2dab5103d466db9cc4590681832de4bf8 docs(KD-STAGING): sync governance after KD-6b DONE Human 13 days ago
1 """muse tension — harmonic tension curve for a MIDI track.
2
3 Scores each bar's dissonance level from 0 (perfectly consonant) to 1
4 (maximally tense). Agents composing multi-part music or reviewing agent-
5 generated harmony use this to verify that tension builds toward climaxes and
6 resolves at cadences — an impossible analysis in Git's binary-blob world.
7
8 Usage::
9
10 muse tension tracks/chords.mid
11 muse tension tracks/piano.mid --commit HEAD~2
12 muse tension tracks/strings.mid --json
13
14 Output::
15
16 Harmonic tension: tracks/chords.mid — working tree
17
18 bar 1 ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ 0.05 consonant
19 bar 2 ████████ 0.41 mild
20 bar 3 ████████████████ 0.72 tense
21 bar 4 ████ 0.21 mild
22 ...
23 """
24
25 from __future__ import annotations
26
27 import argparse
28 import json
29 import logging
30 import pathlib
31 import sys
32
33 from muse.core.errors import ExitCode
34 from muse.core.repo import read_repo_id, require_repo
35 from muse.core.store import read_current_branch, resolve_commit_ref
36 from muse.plugins.midi._analysis import compute_tension
37 from muse.plugins.midi._query import load_track, load_track_from_workdir
38
39 logger = logging.getLogger(__name__)
40
41 _BAR_WIDTH = 20
42
43
44
45 def _read_branch(root: pathlib.Path) -> str:
46 return read_current_branch(root)
47
48
49 def _tension_bar(tension: float) -> str:
50 blocks = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"]
51 level = int(tension * (_BAR_WIDTH - 1))
52 block = blocks[min(int(tension * 7), 7)]
53 return block * level
54
55
56 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
57 """Register the tension subcommand."""
58 parser = subparsers.add_parser("tension", help="Show the harmonic tension arc of a MIDI track bar by bar.", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
59 parser.add_argument("track", metavar="TRACK", help="Workspace-relative path to a .mid file.")
60 parser.add_argument("--commit", "-c", metavar="REF", default=None, dest="ref", help="Analyse a historical snapshot instead of the working tree.")
61 parser.add_argument("--json", action="store_true", dest="as_json", help="Emit results as JSON.")
62 parser.set_defaults(func=run)
63
64
65 def run(args: argparse.Namespace) -> None:
66 """Show the harmonic tension arc of a MIDI track bar by bar.
67
68 ``muse tension`` uses interval dissonance weights to score each bar's
69 harmonic complexity. A well-structured composition typically builds
70 tension toward phrase climaxes and resolves it at cadence points.
71
72 Agents can use this as an automated quality gate: if tension is flat or
73 unresolved at expected cadence points, the composition needs revision.
74 """
75 track: str = args.track
76 ref: str | None = args.ref
77 as_json: bool = args.as_json
78
79 root = require_repo()
80 commit_label = "working tree"
81
82 if ref is not None:
83 repo_id = read_repo_id(root)
84 branch = _read_branch(root)
85 commit = resolve_commit_ref(root, repo_id, branch, ref)
86 if commit is None:
87 print(f"❌ Commit '{ref}' not found.", file=sys.stderr)
88 raise SystemExit(ExitCode.USER_ERROR)
89 result = load_track(root, commit.commit_id, track)
90 commit_label = commit.commit_id[:8]
91 else:
92 result = load_track_from_workdir(root, track)
93
94 if result is None:
95 print(f"❌ Track '{track}' not found or not a valid MIDI file.", file=sys.stderr)
96 raise SystemExit(ExitCode.USER_ERROR)
97
98 notes, _tpb = result
99 if not notes:
100 print(f" (no notes found in '{track}')")
101 return
102
103 bars = compute_tension(notes)
104
105 if as_json:
106 print(json.dumps(
107 {"track": track, "commit": commit_label, "bars": list(bars)},
108 indent=2,
109 ))
110 return
111
112 print(f"\nHarmonic tension: {track} — {commit_label}\n")
113 for b in bars:
114 bar_str = _tension_bar(b["tension"])
115 print(
116 f" bar {b['bar']:>3} {bar_str:<{_BAR_WIDTH}}"
117 f" {b['tension']:.3f} {b['label']}"
118 )
File History 1 commit
sha256:cf6265cea8c21d9228d90dec13ef6ec2dab5103d466db9cc4590681832de4bf8 docs(KD-STAGING): sync governance after KD-6b DONE Human 13 days ago