@mthines/reaper-mcp 0.19.0 → 0.70.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +19 -570
  2. package/bridge-ui/mcp_bridge.html +129 -0
  3. package/bridge-ui/mcp_bridge.webmanifest +16 -0
  4. package/bridge-ui/mcp_bridge_icon_192.png +0 -0
  5. package/bridge-ui/mcp_bridge_icon_512.png +0 -0
  6. package/bridge-ui/mcp_bridge_sw.js +35 -0
  7. package/claude/agents/plugin-researcher.md +188 -0
  8. package/claude/knowledge/reference/aesthetics-scoring.md +48 -0
  9. package/{knowledge → claude/knowledge}/reference/common-mistakes.md +1 -1
  10. package/claude/knowledge/reference/lorekit-setup.md +36 -0
  11. package/claude/knowledge/reference/memory-protocol.md +377 -0
  12. package/{knowledge → claude/knowledge}/reference/metering.md +5 -5
  13. package/claude/knowledge/reference/perceived-loudness.md +182 -0
  14. package/{knowledge → claude/knowledge}/workflows/editing.md +4 -0
  15. package/{knowledge → claude/knowledge}/workflows/gain-staging.md +21 -23
  16. package/{knowledge → claude/knowledge}/workflows/learn-plugin.md +15 -48
  17. package/{knowledge → claude/knowledge}/workflows/master-bus.md +13 -1
  18. package/claude/knowledge/workflows/section-levels.md +138 -0
  19. package/{knowledge → claude/knowledge}/workflows/session-prep.md +4 -0
  20. package/claude/knowledge/workflows/transient-lighting.md +52 -0
  21. package/claude/skills/critique/SKILL.md +131 -0
  22. package/claude/skills/editing/SKILL.md +281 -0
  23. package/claude/skills/learn-plugin/SKILL.md +210 -0
  24. package/claude/skills/mastering/SKILL.md +151 -0
  25. package/claude/skills/mix-memory/SKILL.md +140 -0
  26. package/claude/skills/mixer/SKILL.md +253 -0
  27. package/claude/skills/new-extension/SKILL.md +324 -0
  28. package/main.js +7843 -1940
  29. package/package.json +17 -15
  30. package/reaper/CLAUDE.md +22 -4
  31. package/reaper/mcp_actions_lib.lua +375 -0
  32. package/reaper/mcp_analyzer.jsfx +27 -12
  33. package/reaper/mcp_arrange_lib.lua +949 -0
  34. package/reaper/mcp_bridge.lua +2321 -758
  35. package/reaper/mcp_edit_views_lib.lua +1378 -0
  36. package/reaper/mcp_envelopes_lib.lua +748 -0
  37. package/reaper/mcp_extensions_lib.lua +55 -0
  38. package/reaper/mcp_fxstate_lib.lua +144 -0
  39. package/reaper/mcp_gain_trace.jsfx +126 -0
  40. package/reaper/mcp_items_lib.lua +877 -0
  41. package/reaper/mcp_lufs_meter.jsfx +16 -13
  42. package/reaper/mcp_midi_lib.lua +555 -0
  43. package/reaper/mcp_mixing_lib.lua +1021 -0
  44. package/reaper/mcp_plan_lib.lua +801 -0
  45. package/reaper/mcp_prefs_lib.lua +127 -0
  46. package/reaper/mcp_project_lib.lua +1088 -0
  47. package/reaper/mcp_recording_lib.lua +595 -0
  48. package/reaper/mcp_render_lib.lua +492 -0
  49. package/reaper/mcp_snapshot_lib.lua +547 -120
  50. package/reaper/mcp_snapshot_next.lua +10 -4
  51. package/reaper/mcp_snapshot_prev.lua +11 -4
  52. package/reaper/mcp_snapshot_quick_save.lua +7 -32
  53. package/reaper/mcp_snapshots_ui_lib.lua +258 -0
  54. package/reaper/mcp_tracks_lib.lua +409 -0
  55. package/sidecar/analysis.py +661 -0
  56. package/sidecar/chords.py +96 -0
  57. package/sidecar/mixcheck.py +295 -0
  58. package/sidecar/requirements-chords.txt +6 -0
  59. package/sidecar/requirements-separation.txt +3 -0
  60. package/sidecar/requirements.txt +17 -0
  61. package/sidecar/separation.py +142 -0
  62. package/sidecar/server.py +622 -0
  63. package/sidecar/tests/test_analysis.py +249 -0
  64. package/sidecar/tests/test_chords.py +117 -0
  65. package/sidecar/tests/test_mixcheck.py +163 -0
  66. package/sidecar/tests/test_separation.py +73 -0
  67. package/sidecar/tests/test_server.py +464 -0
  68. package/sidecar/tests/test_verify.py +243 -0
  69. package/sidecar/tests/test_voice_render.py +268 -0
  70. package/sidecar/tests/test_voice_sketch.py +140 -0
  71. package/sidecar/verify.py +224 -0
  72. package/sidecar/voice_render.py +295 -0
  73. package/sidecar/voice_sketch.py +280 -0
  74. package/claude-agents/critique.md +0 -147
  75. package/claude-agents/editor.md +0 -184
  76. package/claude-agents/levels.md +0 -107
  77. package/claude-agents/mastering.md +0 -129
  78. package/claude-agents/mixer.md +0 -248
  79. package/claude-agents/preflight.md +0 -149
  80. package/claude-agents/producer.md +0 -159
  81. package/claude-agents/setup.md +0 -150
  82. package/claude-agents/stems.md +0 -135
  83. package/claude-rules/architecture.md +0 -39
  84. package/claude-rules/development.md +0 -54
  85. package/claude-rules/lua-bridge.md +0 -50
  86. package/claude-rules/testing.md +0 -42
  87. package/claude-skills/learn-plugin.md +0 -123
  88. package/knowledge/CLAUDE.md +0 -71
  89. package/knowledge/plugins/_template.md +0 -82
  90. package/knowledge/plugins/fabfilter/pro-c-2.md +0 -117
  91. package/knowledge/plugins/fabfilter/pro-l-2.md +0 -95
  92. package/knowledge/plugins/fabfilter/pro-q-3.md +0 -112
  93. package/knowledge/plugins/neural-dsp/helix-native.md +0 -104
  94. package/knowledge/plugins/stock-reaper/js-1175-compressor.md +0 -94
  95. package/knowledge/plugins/stock-reaper/rea-comp.md +0 -100
  96. package/knowledge/plugins/stock-reaper/rea-delay.md +0 -95
  97. package/knowledge/plugins/stock-reaper/rea-eq.md +0 -103
  98. package/knowledge/plugins/stock-reaper/rea-gate.md +0 -99
  99. package/knowledge/plugins/stock-reaper/rea-limit.md +0 -75
  100. package/knowledge/plugins/stock-reaper/rea-verb.md +0 -76
  101. package/knowledge/reference/perceived-loudness.md +0 -122
  102. package/reaper/mcp_snapshot_manager.lua +0 -1001
  103. /package/{knowledge → claude/knowledge}/genres/_template.md +0 -0
  104. /package/{knowledge → claude/knowledge}/genres/electronic.md +0 -0
  105. /package/{knowledge → claude/knowledge}/genres/hip-hop.md +0 -0
  106. /package/{knowledge → claude/knowledge}/genres/metal.md +0 -0
  107. /package/{knowledge → claude/knowledge}/genres/orchestral.md +0 -0
  108. /package/{knowledge → claude/knowledge}/genres/pop.md +0 -0
  109. /package/{knowledge → claude/knowledge}/genres/rock.md +0 -0
  110. /package/{knowledge → claude/knowledge}/reference/compression.md +0 -0
  111. /package/{knowledge → claude/knowledge}/reference/frequencies.md +0 -0
  112. /package/{knowledge → claude/knowledge}/workflows/delivery.md +0 -0
  113. /package/{knowledge → claude/knowledge}/workflows/drum-bus.md +0 -0
  114. /package/{knowledge → claude/knowledge}/workflows/low-end.md +0 -0
  115. /package/{knowledge → claude/knowledge}/workflows/stem-prep.md +0 -0
  116. /package/{knowledge → claude/knowledge}/workflows/vocal-chain.md +0 -0
@@ -0,0 +1,280 @@
1
+ """Voice sketch to monophonic melody + heuristic GM drums. Local, model-free.
2
+
3
+ YIN rejects aperiodic frames before note segmentation; it does not transcribe
4
+ speech or separate simultaneous voices. Beatbox labels are spectral guesses,
5
+ not a trained kick/snare/hat classifier. No harmony is invented.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import math
10
+ import numpy as np
11
+
12
+ import analysis
13
+
14
+ MAX_SECONDS = 120
15
+ MAX_NOTES = 10000
16
+
17
+
18
+ def _pitch_frames(mono: np.ndarray, sr: int, pitch_min: int, pitch_max: int):
19
+ """YIN cumulative mean normalized difference, 10 ms hops, 93 ms windows.
20
+
21
+ Low-pass before resampling so high-frequency beatbox noise cannot alias
22
+ into the pitch range. Work a frame at a time to bound FFT memory.
23
+ """
24
+ rate = 11025
25
+ cutoff = min(4500, sr * 0.45)
26
+ taps = np.arange(-48, 49)
27
+ kernel = 2 * cutoff / sr * np.sinc(2 * cutoff / sr * taps) * np.hanning(97)
28
+ filtered = np.convolve(mono, kernel / kernel.sum(), mode="same")
29
+ x = np.interp(np.arange(0, mono.size, sr / rate), np.arange(mono.size), filtered)
30
+ size, hop = 1024, 110
31
+ padded = np.pad(x, (size // 2, size // 2))
32
+ lo = max(2, int(rate / (440 * 2 ** ((pitch_max + 0.5 - 69) / 12))))
33
+ hi = min(size // 2, int(math.ceil(rate / (440 * 2 ** ((pitch_min - 0.5 - 69) / 12)))))
34
+ times, pitches, clarity, levels, silences = [], [], [], [], []
35
+ gate = max(1e-5, float(np.sqrt(np.mean(x * x))) * 0.08)
36
+ for pos in range(0, x.size, hop):
37
+ frame = padded[pos:pos + size].copy()
38
+ frame -= frame.mean()
39
+ rms = float(np.sqrt(np.mean(frame * frame)))
40
+ pitch, confidence = -1.0, 0.0
41
+ if rms > gate:
42
+ spec = np.fft.rfft(frame, size * 2)
43
+ ac = np.fft.irfft(spec * spec.conj(), size * 2)[:hi + 1]
44
+ sq = np.r_[0, np.cumsum(frame * frame)]
45
+ lag = np.arange(1, hi + 1)
46
+ diff = np.maximum(0, sq[size - lag] + sq[size] - sq[lag] - 2 * ac[1:])
47
+ norm = np.r_[1, diff * lag / np.maximum(np.cumsum(diff), 1e-12)]
48
+ troughs = [i for i in range(lo, hi) if norm[i] < 0.18 and norm[i] <= norm[i - 1]
49
+ and norm[i] < norm[i + 1]]
50
+ best = troughs[0] if troughs else int(lo + np.argmin(norm[lo:hi + 1]))
51
+ confidence = float(np.clip(1 - norm[best], 0, 1))
52
+ if confidence >= 0.78:
53
+ refined = float(best)
54
+ if 1 < best < hi:
55
+ a, b, c = norm[best - 1:best + 2]
56
+ denom = a - 2 * b + c
57
+ if denom > 1e-9:
58
+ refined += float(np.clip(0.5 * (a - c) / denom, -0.5, 0.5))
59
+ pitch = 69 + 12 * math.log2(rate / refined / 440)
60
+ if not pitch_min - 0.5 <= pitch <= pitch_max + 0.5:
61
+ pitch = -1.0
62
+ times.append(pos / rate)
63
+ pitches.append(pitch)
64
+ clarity.append(confidence if pitch >= 0 else 0)
65
+ levels.append(rms)
66
+ short_frame = padded[pos + size // 2 - hop // 2:pos + size // 2 + hop // 2]
67
+ silences.append(float(np.sqrt(np.mean(short_frame * short_frame))) < gate)
68
+ return np.array(times), np.array(pitches), np.array(clarity), np.array(levels), np.array(silences)
69
+
70
+
71
+ def _melody(mono: np.ndarray, sr: int, pitch_min: int, pitch_max: int, min_note: float):
72
+ times, pitches, clarity, levels, silences = _pitch_frames(mono, sr, pitch_min, pitch_max)
73
+ rounded = np.where(pitches >= 0, np.floor(pitches + 0.5), -1).astype(int)
74
+ # Median among voiced neighbours removes single-frame octave/rounding glitches,
75
+ # but never fills a truly unvoiced frame with a made-up note.
76
+ smooth = rounded.copy()
77
+ for i in range(len(rounded)):
78
+ near = rounded[max(0, i - 2):i + 3]
79
+ voiced = near[near >= 0]
80
+ if rounded[i] >= 0 and voiced.size:
81
+ smooth[i] = int(np.median(voiced))
82
+ for i in range(1, len(smooth) - 2):
83
+ if smooth[i] < 0 and smooth[i - 1] >= 0:
84
+ for gap in (1, 2):
85
+ if smooth[i + gap] == smooth[i - 1]:
86
+ smooth[i:i + gap] = smooth[i - 1]
87
+ break
88
+ # The 93 ms pitch window spans short vocal gaps; 10 ms silence evidence must
89
+ # survive dropout smoothing without treating percussion attacks as reattacks.
90
+ smooth[silences] = -1
91
+ notes = []
92
+ i = 0
93
+ top = float(np.percentile(levels, 95)) or 1
94
+ hop = 110 / 11025
95
+ while i < len(smooth):
96
+ j = i + 1
97
+ while j < len(smooth) and smooth[j] == smooth[i]:
98
+ j += 1
99
+ if smooth[i] >= 0 and (j - i) * hop >= min_note:
100
+ start = max(0, times[i] - 0.02)
101
+ end = min(mono.size / sr, times[j - 1] + hop - 0.02)
102
+ # Percussion attacks do not rearticulate a continuous voiced run.
103
+ notes.append({"time": round(float(start), 4), "duration": round(float(end - start), 4),
104
+ "pitch": int(smooth[i]), "velocity": int(np.clip(90 * levels[i:j].mean() / top, 35, 115)),
105
+ "confidence": round(float(clarity[i:j].mean()), 3)})
106
+ i = j
107
+ return notes, (times, clarity)
108
+
109
+
110
+ def _drums(mono: np.ndarray, sr: int, hits: list, voiced, mode: str):
111
+ notes = []
112
+ for hit in hits:
113
+ t = hit["time"]
114
+ seg = mono[int(t * sr):int((t + 0.06) * sr)]
115
+ if seg.size < 16:
116
+ continue
117
+ power = np.abs(np.fft.rfft(seg * np.hanning(seg.size))) ** 2
118
+ freq = np.fft.rfftfreq(seg.size, 1 / sr)
119
+ total = float(power.sum()) or 1e-12
120
+ low = float(power[freq < 180].sum() / total)
121
+ high = float(power[freq > 3500].sum() / total)
122
+ centre = float((freq * power).sum() / total)
123
+ near = (voiced[0] >= t) & (voiced[0] < t + 0.06)
124
+ periodic = float(voiced[1][near].mean()) if near.any() else 0
125
+ # Pure humming should not become a parallel drum line in auto mode.
126
+ # Pitched plosives can still count as a kick: this is intentionally heuristic.
127
+ crest = float(np.max(np.abs(seg)) / max(np.sqrt(np.mean(seg * seg)), 1e-12))
128
+ if mode == "auto" and periodic > 0.8 and high < 0.08 and crest < 3.5:
129
+ continue
130
+ if high > 0.35 or centre > 3500:
131
+ pitch, label, confidence = 42, "closed_hat", max(high, 0.5)
132
+ elif low > 0.3 and centre < 1800:
133
+ pitch, label, confidence = 36, "kick", low
134
+ else:
135
+ pitch, label, confidence = 38, "snare", 1 - max(low, high)
136
+ notes.append({"time": t, "duration": 0.06, "pitch": pitch, "label": label,
137
+ "velocity": int(np.clip(40 + 75 * hit["strength"], 1, 127)),
138
+ "confidence": round(float(confidence), 3)})
139
+ return notes
140
+
141
+
142
+ def _tempo(flux, frame_s: float, hits: list, duration: float, bpm: float | None, threshold: float):
143
+ estimate = analysis.estimate_tempo(flux, frame_s)
144
+ detected = estimate["bpm"]
145
+ if bpm is None and (detected is None or (estimate["confidence"] or 0) < 0.08):
146
+ raise ValueError("No reliable pulse found. Supply bpm, or select a longer rhythmic range (at least 4 s).")
147
+ base = float(bpm or round(detected))
148
+ origin = min((h["time"] for h in hits), default=0.0)
149
+ windows = []
150
+ if bpm is None:
151
+ for start in np.arange(origin, duration - 3.99, 4.0):
152
+ lo, hi = int(start / frame_s), int((start + 4) / frame_s)
153
+ local = analysis.estimate_tempo(flux[lo:hi], frame_s)
154
+ windows.append((float(start), local))
155
+ # The global autocorrelation can favour the longer, later section.
156
+ # Start at the opening pulse when two windows independently agree.
157
+ if len(windows) >= 2:
158
+ a, b = windows[0][1], windows[1][1]
159
+ if a["bpm"] and b["bpm"] and min(a["confidence"] or 0, b["confidence"] or 0) >= 0.2:
160
+ if abs(a["bpm"] / b["bpm"] - 1) < 0.08:
161
+ base = float(round((a["bpm"] + b["bpm"]) / 2))
162
+ segments = [{"time": origin, "beat": 0.0, "bpm": base}]
163
+ if bpm is None:
164
+ # A change must persist in two adjacent four-second windows. Fold only
165
+ # near-exact octave ambiguities: a genuine 100 -> 150 is not 100 -> 75.
166
+ pending = None
167
+ for start, local in windows:
168
+ value = local["bpm"]
169
+ if value is None or (local["confidence"] or 0) < 0.2:
170
+ pending = None
171
+ continue
172
+ current = segments[-1]["bpm"]
173
+ if abs(value / current - 2) < 0.16:
174
+ value /= 2
175
+ elif abs(value / current - 0.5) < 0.04:
176
+ value *= 2
177
+ if abs(value / current - 1) < threshold:
178
+ pending = None
179
+ continue
180
+ if pending is not None and abs(value / pending[1] - 1) < 0.08:
181
+ change_time = pending[0]
182
+ previous = segments[-1]
183
+ beat = previous["beat"] + (change_time - previous["time"]) * previous["bpm"] / 60
184
+ segments.append({"time": round(float(change_time), 4), "beat": round(beat, 6),
185
+ "bpm": float(round((pending[1] + value) / 2))})
186
+ pending = None
187
+ else:
188
+ pending = (float(start), value)
189
+ return {"bpm": base, "detectedBpm": detected, "confidence": estimate["confidence"],
190
+ "source": "manual" if bpm is not None else "estimated", "originSeconds": origin,
191
+ "segments": segments, "alternatives": [base / 2, base * 2]}
192
+
193
+
194
+ def _beat(time: float, segments: list) -> float:
195
+ segment = next((s for s in reversed(segments) if time >= s["time"]), segments[0])
196
+ return max(0, segment["beat"] + (time - segment["time"]) * segment["bpm"] / 60)
197
+
198
+
199
+ def _quantize(notes: list, segments: list, grid: float, melodic: bool):
200
+ by_slot = {}
201
+ for note in notes:
202
+ beat = round(math.floor(_beat(note["time"], segments) / grid + 0.5) * grid, 6)
203
+ end = math.floor(_beat(note["time"] + note["duration"], segments) / grid + 0.5) * grid
204
+ n = {**note, "beat": beat, "lengthBeats": round(max(grid if melodic else min(grid, 0.125), end - beat), 6)}
205
+ key = (beat, 0 if melodic else note["pitch"])
206
+ previous = by_slot.get(key)
207
+ if previous is None or note["confidence"] * note["duration"] > previous["confidence"] * previous["duration"]:
208
+ by_slot[key] = n
209
+ out = sorted(by_slot.values(), key=lambda n: (n["beat"], n["pitch"]))
210
+ if melodic:
211
+ for a, b in zip(out, out[1:]):
212
+ a["lengthBeats"] = round(min(a["lengthBeats"], b["beat"] - a["beat"]), 6)
213
+ return out
214
+
215
+
216
+ def transcribe(audio: np.ndarray, sr: int, *, mode: str = "auto", bpm: float | None = None,
217
+ grid: float = 0.25, pitch_min: int = 36, pitch_max: int = 84,
218
+ min_note_ms: float = 80, tempo_change_threshold: float = 0.2) -> dict:
219
+ """Times are relative to the selected clip; beats start at its first onset.
220
+
221
+ Returns full bounded note arrays to the Node client, which pages the agent's
222
+ response and optionally exports all notes. Four-four meter is an assumption,
223
+ never inferred. Timing confidence is not a calibrated probability.
224
+ """
225
+ duration = audio.shape[1] / sr
226
+ if not 0.5 <= duration <= MAX_SECONDS:
227
+ raise ValueError(f"Select between 0.5 and {MAX_SECONDS} seconds")
228
+ if mode not in ("auto", "melody", "drums"):
229
+ raise ValueError("mode must be auto, melody or drums")
230
+ if bpm is not None and (not math.isfinite(bpm) or not 40 <= bpm <= 240):
231
+ raise ValueError("bpm must be between 40 and 240")
232
+ if not math.isfinite(grid) or not 1 / 12 <= grid <= 1:
233
+ raise ValueError("grid must be between 1/12 and 1 beat")
234
+ if not 24 <= pitch_min < pitch_max <= 96:
235
+ raise ValueError("pitchMin must be below pitchMax, both between 24 and 96")
236
+ if not 40 <= min_note_ms <= 500 or not 0.1 <= tempo_change_threshold <= 0.5:
237
+ raise ValueError("minNoteMs must be 40..500; tempoChangeThreshold must be 0.1..0.5")
238
+ if not np.isfinite(audio).all():
239
+ raise ValueError("Audio contains non-finite samples")
240
+ mono = audio.mean(axis=0).astype(np.float32)
241
+ if float(np.max(np.abs(mono))) < 1e-5:
242
+ raise ValueError("No audible content in the selected range")
243
+ detected = analysis.transients(audio, sr, sensitivity=0.6, min_gap_ms=80)
244
+ hits = detected["transients"]
245
+ melody, voiced = ([], (np.array([]), np.array([])))
246
+ if mode != "drums":
247
+ melody, voiced = _melody(mono, sr, pitch_min, pitch_max, min_note_ms / 1000)
248
+ drums = _drums(mono, sr, hits, voiced, mode) if mode != "melody" else []
249
+ flux, _, frame_s = analysis._band_flux(mono, sr)
250
+ tempo = _tempo(flux["full"], frame_s, hits, duration, bpm, tempo_change_threshold)
251
+ # Pitch-class evidence must not change when a coarse grid merges note onsets.
252
+ profile = [0.0] * 12
253
+ for note in melody:
254
+ profile[note["pitch"] % 12] += note["duration"] * note["confidence"]
255
+ melody = _quantize(melody, tempo["segments"], grid, True)
256
+ drums = _quantize(drums, tempo["segments"], grid, False)
257
+ if not melody and not drums:
258
+ raise ValueError("No notes detected. Try a different mode, pitch range or minNoteMs.")
259
+ if len(melody) + len(drums) > MAX_NOTES:
260
+ raise ValueError(f"Too many notes (max {MAX_NOTES}); select a shorter range")
261
+ # Group exact repeated four-beat drum bars without inventing missing hits.
262
+ bars = {}
263
+ for n in drums:
264
+ bar = int(n["beat"] // 4)
265
+ bars.setdefault(bar, []).append((round(n["beat"] % 4, 6), n["pitch"]))
266
+ patterns = {}
267
+ for bar, pattern in bars.items():
268
+ patterns.setdefault(tuple(pattern), []).append(bar + 1)
269
+ warnings = ["Experimental transcription: review pitches, octaves and kick/snare/hat guesses.",
270
+ "Speech is not removed; use startTime/endTime to exclude spoken cues.",
271
+ "4/4 meter and the first onset as beat zero are assumptions, not detected downbeats.",
272
+ "Half/double tempo is ambiguous; supply bpm to choose the intended pulse."]
273
+ if bpm is None and (tempo["confidence"] or 0) < 0.25:
274
+ warnings.append("Weak tempo evidence; audition the result and consider supplying bpm.")
275
+ if not melody and mode != "drums":
276
+ warnings.append("No stable melody found in the selected pitch range.")
277
+ return {"durationSeconds": round(duration, 4), "mode": mode, "grid": grid, "tempo": tempo,
278
+ "chroma": profile, "melody": melody, "drums": drums, "warnings": warnings,
279
+ "patterns": [{"bars": bars, "hits": [{"beat": b, "pitch": p} for b, p in pattern]}
280
+ for pattern, bars in patterns.items()]}
@@ -1,147 +0,0 @@
1
- ---
2
- name: critique
3
- description: Mix analysis and critique — the "roast my mix" agent. Analyzes a REAPER session and produces a detailed report of problems and suggestions. Does NOT make changes.
4
- tools: Read, Glob, Grep
5
- mcpServers:
6
- - reaper
7
- model: sonnet
8
- permissionMode: acceptEdits
9
- ---
10
-
11
- # Mix Analyzer Agent ("Roast My Mix")
12
-
13
- You are a brutally honest mix critic with 20 years of experience. Your job is to analyze a REAPER session and produce an actionable report of everything that could be improved. You **observe and report only** — you do NOT make changes.
14
-
15
- After your report, ask the user which problems they want you to fix first (they can hand off to `@mixer` for execution).
16
-
17
- ---
18
-
19
- ## Analysis Checklist
20
-
21
- Run through ALL of these checks systematically.
22
-
23
- ### 1. Session overview
24
- ```
25
- tool: get_project_info
26
- tool: list_tracks
27
- ```
28
- Note: track count, tempo, sample rate, bus structure.
29
-
30
- ### 2. Gain staging check (perceived-loudness-aware)
31
- Start playback of the chorus/loudest section:
32
- ```
33
- tool: play
34
- ```
35
- Then read meters for ALL tracks:
36
- ```
37
- tool: read_track_meters (for each track)
38
- ```
39
- Flag standard issues:
40
- - Any track averaging below -24 dBFS or above -10 dBFS
41
- - Any track peaking at or above -3 dBFS
42
- - Mix bus peaking above -6 dBFS
43
-
44
- **Also check perceived loudness balance** (see `knowledge/reference/perceived-loudness.md`):
45
- - Bass instruments (kick, bass, 808) should read **higher** on meters than vocals/guitars (-16 to -14 dBFS) — if they're at the same level as presence-range instruments, they'll sound too quiet
46
- - Vocals/snare should read **lower** (-19 to -20 dBFS) — the 2-5 kHz content sounds louder than the meter shows
47
- - Hi-hats/cymbals at similar meter levels as bass = way too loud perceptually
48
- - Flag any mix where all tracks are at the same RMS level regardless of spectral content — this is a common amateur mistake
49
-
50
- ### 3. Frequency balance (with perceived loudness context)
51
- Read spectrum on the mix bus:
52
- ```
53
- tool: read_track_spectrum (mix bus index)
54
- ```
55
- Check for:
56
- - **Sub buildup** (20–60 Hz): excessive rumble — but remember sub energy sounds quieter than it meters; some buildup is needed for bass-heavy genres
57
- - **Low-mid mud** (200–400 Hz): cloudy, boxy sound — the ear is less sensitive here, so mud accumulates before it's noticed on meters
58
- - **Harshness** (2–5 kHz): fatiguing, piercing — the ear's peak sensitivity zone. Even modest energy here sounds loud and causes listening fatigue
59
- - **Missing air** (10–20 kHz): dull, lifeless — sensitivity drops off, but accumulated boosts across many tracks cause fatigue
60
- - **Missing presence** (1–4 kHz): vocals buried — often a masking issue (guitars/keys competing in the most sensitive range), not just a level issue
61
-
62
- **Perceived loudness note**: A "flat" spectrum on an analyzer does NOT mean a perceptually balanced mix. The ear naturally amplifies 2-5 kHz, so a truly flat spectrum will sound harsh/forward in the presence range.
63
-
64
- ### 4. Dynamics check
65
- ```
66
- tool: read_track_crest (mix bus index)
67
- ```
68
- - Crest factor < 6 dB → over-compressed, squashed
69
- - Crest factor 8–12 dB → healthy for most genres
70
- - Crest factor > 15 dB → may need dynamics control
71
-
72
- ### 5. Loudness check
73
- ```
74
- tool: read_track_lufs (mix bus index)
75
- ```
76
- Compare against genre/platform targets:
77
- - Streaming (Spotify/YouTube): -14 LUFS, -1 dBTP
78
- - Hip-Hop/EDM: -10 to -7 LUFS
79
- - Orchestral: -23 to -16 LUFS
80
-
81
- ### 6. Stereo image check
82
- ```
83
- tool: read_track_correlation (mix bus index)
84
- ```
85
- - Correlation < 0 → phase cancellation (critical problem)
86
- - Correlation 0.0–0.3 → very wide, may collapse in mono
87
- - Correlation > 0.8 → very narrow, may sound boring
88
- - Check if bass content is mono (read correlation on bass/kick tracks)
89
-
90
- ### 7. FX chain audit
91
- For each track, check `get_track_properties` to see the FX chain:
92
- - Tracks with audio but 0 FX → probably needs at least an EQ
93
- - Tracks with 10+ FX → possibly over-processed
94
- - Missing HPF on non-bass tracks → common mistake
95
-
96
- ### 8. Common problems checklist
97
- If genre knowledge is available, load it:
98
- ```
99
- Glob("knowledge/genres/*.md")
100
- Read the matching genre file
101
- ```
102
- Also load the mistakes checklist:
103
- ```
104
- Read("knowledge/reference/common-mistakes.md")
105
- ```
106
-
107
- ---
108
-
109
- ## Report Format
110
-
111
- Structure your output as:
112
-
113
- ---
114
-
115
- **Mix Analysis Report**
116
-
117
- **Session**: {project name} | {track count} tracks | {tempo} BPM | {sample rate} Hz
118
-
119
- **Overall Impression**: [2-3 sentences — honest first reaction]
120
-
121
- **Critical Issues** (fix these first):
122
- 1. **{Issue}**: {What the meters/spectrum showed} → {Recommended fix}
123
- 2. ...
124
-
125
- **Notable Issues** (fix after criticals):
126
- 1. **{Issue}**: {Evidence} → {Fix}
127
- 2. ...
128
-
129
- **Things Working Well**:
130
- - {Positive observations — always find at least one}
131
-
132
- **Recommended Next Steps**:
133
- 1. {Most impactful fix}
134
- 2. {Second priority}
135
- 3. {Third priority}
136
-
137
- **Suggested workflow**: Run `@levels` / `@mixer` / `@mastering` next.
138
-
139
- ---
140
-
141
- ## Rules
142
- - Be honest but constructive — explain WHY something is a problem
143
- - Back every claim with a measurement (dB, Hz, LUFS)
144
- - Don't just say "it's muddy" — say "200–400 Hz shows +4 dB above the average curve"
145
- - Always suggest a specific fix, not just identify the problem
146
- - Find something positive to mention — even in rough mixes
147
- - **Account for perceived loudness** — don't flag bass instruments as "too hot" just because they meter higher than vocals. Bass needs more dB to sound balanced. Conversely, flag presence-range instruments that meter the same as bass — they'll sound much louder than intended
@@ -1,184 +0,0 @@
1
- ---
2
- name: editor
3
- description: Audio editing specialist — manages crossfades, timing alignment, phase correction, head/tail cleanup, and arrangement editing. Use for "edit the tracks", "fix timing", "clean up edits", or "align the drums".
4
- tools: Read, Glob
5
- mcpServers:
6
- - reaper
7
- model: sonnet
8
- permissionMode: acceptEdits
9
- ---
10
-
11
- # Audio Editor Agent
12
-
13
- You are an audio editor for REAPER DAW. Your job is to make raw recorded performances mix-ready by managing crossfades, aligning timing, checking phase coherence, cleaning up heads/tails, and handling arrangement edits. You work between session preparation and gain staging.
14
-
15
- **Editing is invisible when done right.** The goal is that no listener ever hears an edit point, a timing drift, or a phase problem.
16
-
17
- ---
18
-
19
- ## Workflow
20
-
21
- ### Step 1: Save a safety snapshot
22
- ```
23
- tool: snapshot_save
24
- params: { name: "pre-editing", description: "State before audio editing" }
25
- ```
26
-
27
- ### Step 2: Inventory media items
28
- ```
29
- tool: list_tracks
30
- ```
31
- For each track:
32
- ```
33
- tool: list_media_items
34
- params: { trackIndex: N }
35
- ```
36
- Note: item count, positions, gaps, overlaps.
37
-
38
- ### Step 3: Phase check multi-mic recordings
39
-
40
- For related mic pairs (kick in/out, snare top/bottom, DI + amp), check phase:
41
- ```
42
- tool: read_track_correlation
43
- params: { trackIndex: N }
44
- ```
45
-
46
- If correlation is negative or very low (< 0.3):
47
-
48
- 1. Try polarity flip:
49
- ```
50
- tool: set_track_property
51
- params: { trackIndex: N, property: "phase", value: true }
52
- ```
53
-
54
- 2. If polarity flip doesn't improve correlation, time-align by nudging item position:
55
- ```
56
- tool: get_media_item_properties
57
- params: { trackIndex: N, itemIndex: 0 }
58
- ```
59
- ```
60
- tool: set_media_item_properties
61
- params: { trackIndex: N, itemIndex: 0, position: ADJUSTED_SECONDS }
62
- ```
63
-
64
- **Always measure correlation before AND after any phase correction.**
65
-
66
- ### Step 4: Crossfade management
67
-
68
- Every edit point needs a crossfade. Check items and apply appropriate lengths:
69
-
70
- | Edit Type | Crossfade Length |
71
- |-----------|-----------------|
72
- | Percussive (drums, transients) | 2-5 ms |
73
- | Sustained (vocals, pads, strings) | 10-50 ms |
74
- | Comp points (between takes) | 20-100 ms |
75
- | Section transitions | 50-200 ms |
76
-
77
- ```
78
- tool: set_media_item_properties
79
- params: { trackIndex: N, itemIndex: N, fadeInLength: 0.005, fadeOutLength: 0.005 }
80
- ```
81
-
82
- ### Step 5: Timing alignment
83
-
84
- Use stretch markers to align performances to the grid without cutting:
85
-
86
- ```
87
- tool: add_stretch_marker
88
- params: { trackIndex: N, itemIndex: N, position: POSITION, sourcePosition: SOURCE_POSITION }
89
- ```
90
-
91
- Alignment priority order:
92
- 1. **Kick drum** — rhythmic anchor, align to grid
93
- 2. **Snare** — lock to grid or slightly behind for groove
94
- 3. **Bass** — align attacks to kick hits
95
- 4. **Rhythm instruments** — align to kick/snare pattern
96
- 5. **Vocals** — nudge naturally, don't over-quantize
97
-
98
- Check existing stretch markers:
99
- ```
100
- tool: get_stretch_markers
101
- params: { trackIndex: N, itemIndex: N }
102
- ```
103
-
104
- ### Step 6: Head and tail cleanup
105
-
106
- Trim pre-roll noise from track starts:
107
- ```
108
- tool: trim_media_item
109
- params: { trackIndex: N, itemIndex: 0, trimStart: SECONDS }
110
- ```
111
-
112
- Apply fade-ins to prevent transient artifacts:
113
- ```
114
- tool: set_media_item_properties
115
- params: { trackIndex: N, itemIndex: 0, fadeInLength: 0.01 }
116
- ```
117
-
118
- Apply fade-outs to track endings:
119
- ```
120
- tool: set_media_item_properties
121
- params: { trackIndex: N, itemIndex: LAST, fadeOutLength: 0.05 }
122
- ```
123
-
124
- ### Step 7: Arrangement editing (if requested)
125
-
126
- Split at section boundaries:
127
- ```
128
- tool: split_media_item
129
- params: { trackIndex: N, itemIndex: N, position: SECONDS }
130
- ```
131
-
132
- Move sections:
133
- ```
134
- tool: move_media_item
135
- params: { trackIndex: N, itemIndex: N, newPosition: SECONDS }
136
- ```
137
-
138
- Delete unwanted sections:
139
- ```
140
- tool: delete_media_item
141
- params: { trackIndex: N, itemIndex: N }
142
- ```
143
-
144
- **Critical**: When rearranging, process ALL tracks at each boundary to keep everything in sync.
145
-
146
- ### Step 8: Save post-editing snapshot
147
- ```
148
- tool: snapshot_save
149
- params: { name: "post-editing", description: "Audio editing complete — crossfades, timing, phase, cleanup done" }
150
- ```
151
-
152
- ### Step 9: Report
153
-
154
- For each track edited:
155
- - What was done (crossfades added, timing aligned, phase corrected, trimmed)
156
- - Phase correlation before/after (for multi-mic groups)
157
- - Any issues found (tracks that need re-recording, extreme timing drift)
158
-
159
- ---
160
-
161
- ## Genre-Aware Editing
162
-
163
- The tightness of editing depends on genre:
164
-
165
- | Genre | Timing Approach |
166
- |-------|----------------|
167
- | Pop / EDM | Tight grid quantization, surgical precision |
168
- | Rock | Mostly on grid, allow slight human feel |
169
- | Metal | Very tight, especially drums and rhythm guitars |
170
- | Jazz / Blues | Preserve feel, only fix obvious mistakes |
171
- | Folk / Acoustic | Minimal editing, natural performance priority |
172
- | Hip-Hop | Drums tight, vocals can be looser |
173
-
174
- Read `knowledge/genres/{genre}.md` if genre is known.
175
-
176
- ## Rules
177
-
178
- - **Always save a snapshot first** — editing is hard to undo manually
179
- - **Edit multi-mic groups together** — never move a snare top without moving snare bottom, overheads, and rooms
180
- - **Crossfade everything** — one missed crossfade = one audible click in the final mix
181
- - **Don't over-quantize** — some genres need human feel. Ask the user about desired tightness.
182
- - **Measure, don't guess** — use `read_track_correlation` for phase decisions, not assumptions
183
- - **Flag problems you can't fix** — if timing is too far off for stretch markers (>10%), recommend re-recording
184
- - Read `knowledge/workflows/editing.md` for the detailed reference workflow