gabriel / muse public

density.py file-level

at sha256:6 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 πŸ’₯ blast risk
sha256:6 chore(timeline): remove unused RationalRate import in entity.py · · Jul 10, 2026
1 """muse density β€” note density analysis per bar for a MIDI track.
2
3 Shows how many notes per beat fall in each bar, revealing texture changes:
4 sparse verses, dense choruses, quiet codas. A swarm of agents editing
5 different sections can use this to reason about arrangement density without
6 audio playback.
7
8 Usage::
9
10 muse density tracks/piano.mid
11 muse density tracks/melody.mid --commit HEAD~4
12 muse density tracks/rhythm.mid --json
13
14 Output::
15
16 Note density: tracks/piano.mid β€” working tree
17 Bars: 16 Β· Peak: bar 9 (4.25 notes/beat) Β· Avg: 2.1
18
19 bar 1 β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 2.00 notes/beat ( 8 notes)
20 bar 2 β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ 3.50 notes/beat (14 notes)
21 bar 3 β–ˆβ–ˆβ–ˆβ–ˆ 1.00 notes/beat ( 4 notes)
22 ...
23 """
24
25 import argparse
26 import json
27 import logging
28 import pathlib
29 import sys
30
31 from muse.core.errors import ExitCode
32 from muse.core.repo import require_repo
33 from muse.core.refs import read_current_branch
34 from muse.core.commits import resolve_commit_ref
35 from muse.plugins.midi._analysis import analyze_density
36 from muse.plugins.midi._query import load_track, load_track_from_workdir
37
38 logger = logging.getLogger(__name__)
39
40 _BAR_WIDTH = 32
41
42 def _read_branch(root: pathlib.Path) -> str:
43 return read_current_branch(root)
44
45 def register(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
46 """Register the density subcommand."""
47 parser = subparsers.add_parser("density", help="Show note density (notes per beat) per bar of a MIDI track.", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
48 parser.add_argument("track", metavar="TRACK", help="Workspace-relative path to a .mid file.")
49 parser.add_argument("--commit", "-c", metavar="REF", default=None, dest="ref", help="Analyse a historical snapshot instead of the working tree.")
50 parser.add_argument("--json", action="store_true", dest="as_json", help="Emit results as JSON.")
51 parser.set_defaults(func=run)
52
53 def run(args: argparse.Namespace) -> None:
54 """Show note density (notes per beat) per bar of a MIDI track.
55
56 ``muse density`` reveals the textural arc of a composition: which bars are
57 dense, which are sparse. Agents orchestrating multi-part arrangements use
58 this to avoid over-crowding any single section, and to verify that section
59 transitions (verse β†’ chorus) are properly contrast-shaped.
60
61 Git cannot do this. Muse stores notes as structured data, so density is
62 computable at any historical snapshot with no manual inspection.
63 """
64 track: str = args.track
65 ref: str | None = args.ref
66 as_json: bool = args.as_json
67
68 root = require_repo()
69 commit_label = "working tree"
70
71 if ref is not None:
72 branch = _read_branch(root)
73 commit = resolve_commit_ref(root, branch, ref)
74 if commit is None:
75 print(f"❌ Commit '{ref}' not found.", file=sys.stderr)
76 raise SystemExit(ExitCode.USER_ERROR)
77 result = load_track(root, commit.commit_id, track)
78 commit_label = commit.commit_id
79 else:
80 result = load_track_from_workdir(root, track)
81
82 if result is None:
83 print(f"❌ Track '{track}' not found or not a valid MIDI file.", file=sys.stderr)
84 raise SystemExit(ExitCode.USER_ERROR)
85
86 notes, _tpb = result
87 if not notes:
88 print(f" (no notes found in '{track}')")
89 return
90
91 bars = analyze_density(notes)
92
93 if as_json:
94 print(json.dumps(
95 {"track": track, "commit": commit_label, "bars": list(bars)},
96 ))
97 return
98
99 if not bars:
100 print(" (no bars detected)")
101 return
102
103 peak_bar = max(bars, key=lambda b: b["notes_per_beat"])
104 avg_npb = sum(b["notes_per_beat"] for b in bars) / len(bars)
105
106 print(f"\nNote density: {track} β€” {commit_label}")
107 print(
108 f"Bars: {len(bars)} Β· "
109 f"Peak: bar {peak_bar['bar']} ({peak_bar['notes_per_beat']} notes/beat) Β· "
110 f"Avg: {avg_npb:.1f}"
111 )
112 print("")
113
114 max_npb = max(b["notes_per_beat"] for b in bars) or 1.0
115 for b in bars:
116 fill = int(b["notes_per_beat"] / max_npb * _BAR_WIDTH)
117 print(
118 f" bar {b['bar']:>3} {'β–ˆ' * fill:<{_BAR_WIDTH}}"
119 f" {b['notes_per_beat']:>5.2f} notes/beat"
120 f" ({b['note_count']:>3} notes)"
121 )