@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.
- package/README.md +19 -570
- package/bridge-ui/mcp_bridge.html +129 -0
- package/bridge-ui/mcp_bridge.webmanifest +16 -0
- package/bridge-ui/mcp_bridge_icon_192.png +0 -0
- package/bridge-ui/mcp_bridge_icon_512.png +0 -0
- package/bridge-ui/mcp_bridge_sw.js +35 -0
- package/claude/agents/plugin-researcher.md +188 -0
- package/claude/knowledge/reference/aesthetics-scoring.md +48 -0
- package/{knowledge → claude/knowledge}/reference/common-mistakes.md +1 -1
- package/claude/knowledge/reference/lorekit-setup.md +36 -0
- package/claude/knowledge/reference/memory-protocol.md +377 -0
- package/{knowledge → claude/knowledge}/reference/metering.md +5 -5
- package/claude/knowledge/reference/perceived-loudness.md +182 -0
- package/{knowledge → claude/knowledge}/workflows/editing.md +4 -0
- package/{knowledge → claude/knowledge}/workflows/gain-staging.md +21 -23
- package/{knowledge → claude/knowledge}/workflows/learn-plugin.md +15 -48
- package/{knowledge → claude/knowledge}/workflows/master-bus.md +13 -1
- package/claude/knowledge/workflows/section-levels.md +138 -0
- package/{knowledge → claude/knowledge}/workflows/session-prep.md +4 -0
- package/claude/knowledge/workflows/transient-lighting.md +52 -0
- package/claude/skills/critique/SKILL.md +131 -0
- package/claude/skills/editing/SKILL.md +281 -0
- package/claude/skills/learn-plugin/SKILL.md +210 -0
- package/claude/skills/mastering/SKILL.md +151 -0
- package/claude/skills/mix-memory/SKILL.md +140 -0
- package/claude/skills/mixer/SKILL.md +253 -0
- package/claude/skills/new-extension/SKILL.md +324 -0
- package/main.js +7843 -1940
- package/package.json +17 -15
- package/reaper/CLAUDE.md +22 -4
- package/reaper/mcp_actions_lib.lua +375 -0
- package/reaper/mcp_analyzer.jsfx +27 -12
- package/reaper/mcp_arrange_lib.lua +949 -0
- package/reaper/mcp_bridge.lua +2321 -758
- package/reaper/mcp_edit_views_lib.lua +1378 -0
- package/reaper/mcp_envelopes_lib.lua +748 -0
- package/reaper/mcp_extensions_lib.lua +55 -0
- package/reaper/mcp_fxstate_lib.lua +144 -0
- package/reaper/mcp_gain_trace.jsfx +126 -0
- package/reaper/mcp_items_lib.lua +877 -0
- package/reaper/mcp_lufs_meter.jsfx +16 -13
- package/reaper/mcp_midi_lib.lua +555 -0
- package/reaper/mcp_mixing_lib.lua +1021 -0
- package/reaper/mcp_plan_lib.lua +801 -0
- package/reaper/mcp_prefs_lib.lua +127 -0
- package/reaper/mcp_project_lib.lua +1088 -0
- package/reaper/mcp_recording_lib.lua +595 -0
- package/reaper/mcp_render_lib.lua +492 -0
- package/reaper/mcp_snapshot_lib.lua +547 -120
- package/reaper/mcp_snapshot_next.lua +10 -4
- package/reaper/mcp_snapshot_prev.lua +11 -4
- package/reaper/mcp_snapshot_quick_save.lua +7 -32
- package/reaper/mcp_snapshots_ui_lib.lua +258 -0
- package/reaper/mcp_tracks_lib.lua +409 -0
- package/sidecar/analysis.py +661 -0
- package/sidecar/chords.py +96 -0
- package/sidecar/mixcheck.py +295 -0
- package/sidecar/requirements-chords.txt +6 -0
- package/sidecar/requirements-separation.txt +3 -0
- package/sidecar/requirements.txt +17 -0
- package/sidecar/separation.py +142 -0
- package/sidecar/server.py +622 -0
- package/sidecar/tests/test_analysis.py +249 -0
- package/sidecar/tests/test_chords.py +117 -0
- package/sidecar/tests/test_mixcheck.py +163 -0
- package/sidecar/tests/test_separation.py +73 -0
- package/sidecar/tests/test_server.py +464 -0
- package/sidecar/tests/test_verify.py +243 -0
- package/sidecar/tests/test_voice_render.py +268 -0
- package/sidecar/tests/test_voice_sketch.py +140 -0
- package/sidecar/verify.py +224 -0
- package/sidecar/voice_render.py +295 -0
- package/sidecar/voice_sketch.py +280 -0
- package/claude-agents/critique.md +0 -147
- package/claude-agents/editor.md +0 -184
- package/claude-agents/levels.md +0 -107
- package/claude-agents/mastering.md +0 -129
- package/claude-agents/mixer.md +0 -248
- package/claude-agents/preflight.md +0 -149
- package/claude-agents/producer.md +0 -159
- package/claude-agents/setup.md +0 -150
- package/claude-agents/stems.md +0 -135
- package/claude-rules/architecture.md +0 -39
- package/claude-rules/development.md +0 -54
- package/claude-rules/lua-bridge.md +0 -50
- package/claude-rules/testing.md +0 -42
- package/claude-skills/learn-plugin.md +0 -123
- package/knowledge/CLAUDE.md +0 -71
- package/knowledge/plugins/_template.md +0 -82
- package/knowledge/plugins/fabfilter/pro-c-2.md +0 -117
- package/knowledge/plugins/fabfilter/pro-l-2.md +0 -95
- package/knowledge/plugins/fabfilter/pro-q-3.md +0 -112
- package/knowledge/plugins/neural-dsp/helix-native.md +0 -104
- package/knowledge/plugins/stock-reaper/js-1175-compressor.md +0 -94
- package/knowledge/plugins/stock-reaper/rea-comp.md +0 -100
- package/knowledge/plugins/stock-reaper/rea-delay.md +0 -95
- package/knowledge/plugins/stock-reaper/rea-eq.md +0 -103
- package/knowledge/plugins/stock-reaper/rea-gate.md +0 -99
- package/knowledge/plugins/stock-reaper/rea-limit.md +0 -75
- package/knowledge/plugins/stock-reaper/rea-verb.md +0 -76
- package/knowledge/reference/perceived-loudness.md +0 -122
- package/reaper/mcp_snapshot_manager.lua +0 -1001
- /package/{knowledge → claude/knowledge}/genres/_template.md +0 -0
- /package/{knowledge → claude/knowledge}/genres/electronic.md +0 -0
- /package/{knowledge → claude/knowledge}/genres/hip-hop.md +0 -0
- /package/{knowledge → claude/knowledge}/genres/metal.md +0 -0
- /package/{knowledge → claude/knowledge}/genres/orchestral.md +0 -0
- /package/{knowledge → claude/knowledge}/genres/pop.md +0 -0
- /package/{knowledge → claude/knowledge}/genres/rock.md +0 -0
- /package/{knowledge → claude/knowledge}/reference/compression.md +0 -0
- /package/{knowledge → claude/knowledge}/reference/frequencies.md +0 -0
- /package/{knowledge → claude/knowledge}/workflows/delivery.md +0 -0
- /package/{knowledge → claude/knowledge}/workflows/drum-bus.md +0 -0
- /package/{knowledge → claude/knowledge}/workflows/low-end.md +0 -0
- /package/{knowledge → claude/knowledge}/workflows/stem-prep.md +0 -0
- /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
|
package/claude-agents/editor.md
DELETED
|
@@ -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
|