gabriel / muse public
tension.py python
111 lines 4.0 KB
Raw
sha256:be3641f35bdbcc094677776a77b9aa6a5dab891f8fab201dc162d03c2bab5aea fix(read): strip position:null from structured_delta ops in… Sonnet 4.6 patch 24 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 import argparse
26 import json
27 import logging
28 import pathlib
29 import sys
30
31 from muse.core.types import short_id
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 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 def _read_branch(root: pathlib.Path) -> str:
44 return read_current_branch(root)
45
46 def _tension_bar(tension: float) -> str:
47 blocks = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"]
48 level = int(tension * (_BAR_WIDTH - 1))
49 block = blocks[min(int(tension * 7), 7)]
50 return block * level
51
52 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
53 """Register the tension subcommand."""
54 parser = subparsers.add_parser("tension", help="Show the harmonic tension arc of a MIDI track bar by bar.", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
55 parser.add_argument("track", metavar="TRACK", help="Workspace-relative path to a .mid file.")
56 parser.add_argument("--commit", "-c", metavar="REF", default=None, dest="ref", help="Analyse a historical snapshot instead of the working tree.")
57 parser.add_argument("--json", action="store_true", dest="as_json", help="Emit results as JSON.")
58 parser.set_defaults(func=run)
59
60 def run(args: argparse.Namespace) -> None:
61 """Show the harmonic tension arc of a MIDI track bar by bar.
62
63 ``muse tension`` uses interval dissonance weights to score each bar's
64 harmonic complexity. A well-structured composition typically builds
65 tension toward phrase climaxes and resolves it at cadence points.
66
67 Agents can use this as an automated quality gate: if tension is flat or
68 unresolved at expected cadence points, the composition needs revision.
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 branch = _read_branch(root)
79 commit = resolve_commit_ref(root, branch, ref)
80 if commit is None:
81 print(f"❌ Commit '{ref}' not found.", file=sys.stderr)
82 raise SystemExit(ExitCode.USER_ERROR)
83 result = load_track(root, commit.commit_id, track)
84 commit_label = commit.commit_id
85 else:
86 result = load_track_from_workdir(root, track)
87
88 if result is None:
89 print(f"❌ Track '{track}' not found or not a valid MIDI file.", file=sys.stderr)
90 raise SystemExit(ExitCode.USER_ERROR)
91
92 notes, _tpb = result
93 if not notes:
94 print(f" (no notes found in '{track}')")
95 return
96
97 bars = compute_tension(notes)
98
99 if as_json:
100 print(json.dumps(
101 {"track": track, "commit": commit_label, "bars": list(bars)},
102 ))
103 return
104
105 print(f"\nHarmonic tension: {track} — {commit_label}\n")
106 for b in bars:
107 bar_str = _tension_bar(b["tension"])
108 print(
109 f" bar {b['bar']:>3} {bar_str:<{_BAR_WIDTH}}"
110 f" {b['tension']:.3f} {b['label']}"
111 )
File History 3 commits
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 23 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 29 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 29 days ago