@mthines/reaper-mcp 0.15.0 → 0.17.0-beta.18.1
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 +11 -7
- package/claude-agents/mixer.md +2 -1
- package/main.js +75 -28
- package/package.json +1 -1
- package/reaper/CLAUDE.md +1 -0
- package/reaper/install.sh +3 -1
- package/reaper/mcp_bridge.lua +321 -4
- package/reaper/mcp_snapshot_manager.lua +997 -0
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ npx @mthines/reaper-mcp init --yes
|
|
|
22
22
|
The `init` wizard walks you through:
|
|
23
23
|
1. Installing the REAPER bridge (Lua + JSFX analyzers)
|
|
24
24
|
2. Installing AI mix knowledge and agents
|
|
25
|
-
3. Configuring Claude Code settings (auto-allows all
|
|
25
|
+
3. Configuring Claude Code settings (auto-allows all 80 REAPER tools)
|
|
26
26
|
4. Optionally creating a project-local `.mcp.json`
|
|
27
27
|
|
|
28
28
|
Then load `mcp_bridge.lua` in REAPER and open Claude Code — you're ready to mix.
|
|
@@ -31,7 +31,7 @@ Then load `mcp_bridge.lua` in REAPER and open Claude Code — you're ready to mi
|
|
|
31
31
|
|
|
32
32
|
```
|
|
33
33
|
Claude Code
|
|
34
|
-
├── MCP Tools (
|
|
34
|
+
├── MCP Tools (80) ──→ controls REAPER in real-time
|
|
35
35
|
│ ├── Track management (list, get/set properties, arm, phase, input)
|
|
36
36
|
│ ├── FX management (add/remove, get/set parameters, enable/offline, presets)
|
|
37
37
|
│ ├── Transport (play, stop, record, cursor position)
|
|
@@ -43,7 +43,7 @@ Claude Code
|
|
|
43
43
|
│ ├── MIDI editing (14 tools: notes, CC, items, analysis, batch ops)
|
|
44
44
|
│ ├── Media items (11 tools: properties, split, move, trim, stretch)
|
|
45
45
|
│ ├── Plugin discovery (list installed FX, search, presets)
|
|
46
|
-
│ ├── Snapshots (save/restore mixer state for A/B comparison)
|
|
46
|
+
│ ├── Snapshots (save/restore/delete mixer state for A/B comparison)
|
|
47
47
|
│ └── Routing (sends, receives, bus structure)
|
|
48
48
|
│
|
|
49
49
|
└── Mix Engineer Knowledge ──→ knows HOW to use those tools
|
|
@@ -81,7 +81,7 @@ npx @mthines/reaper-mcp init
|
|
|
81
81
|
The wizard guides you through selecting which components to install:
|
|
82
82
|
- **REAPER Bridge** — Lua bridge + JSFX analyzers (copied to your REAPER resource folder)
|
|
83
83
|
- **AI Skills & Agents** — knowledge base, mix agents, rules, skills (global or project-local)
|
|
84
|
-
- **Claude Code Settings** — auto-allows all
|
|
84
|
+
- **Claude Code Settings** — auto-allows all 80 REAPER tools (no permission prompts)
|
|
85
85
|
- **Project Config** — `.mcp.json` for the current directory (opt-in)
|
|
86
86
|
|
|
87
87
|
For CI/automation, use `--yes` to skip prompts and install everything with defaults:
|
|
@@ -108,6 +108,7 @@ npx @mthines/reaper-mcp install-skills --project # project-local alternative
|
|
|
108
108
|
|
|
109
109
|
The `setup` command copies into your REAPER resource folder:
|
|
110
110
|
- `mcp_bridge.lua` — persistent Lua bridge script
|
|
111
|
+
- `mcp_snapshot_manager.lua` — snapshot manager GUI (save/restore/delete from REAPER)
|
|
111
112
|
- `mcp_analyzer.jsfx` — FFT spectrum analyzer
|
|
112
113
|
- `mcp_lufs_meter.jsfx` — ITU-R BS.1770 LUFS meter
|
|
113
114
|
- `mcp_correlation_meter.jsfx` — stereo correlation analyzer
|
|
@@ -138,7 +139,7 @@ npx @mthines/reaper-mcp doctor
|
|
|
138
139
|
|
|
139
140
|
Checks that the bridge is connected, knowledge is installed, and MCP config exists.
|
|
140
141
|
|
|
141
|
-
## MCP Tools (
|
|
142
|
+
## MCP Tools (80)
|
|
142
143
|
|
|
143
144
|
### Project & Tracks
|
|
144
145
|
|
|
@@ -250,6 +251,7 @@ Checks that the bridge is connected, knowledge is installed, and MCP config exis
|
|
|
250
251
|
| `snapshot_save` | Save current mixer state (volumes, pans, FX, mutes) |
|
|
251
252
|
| `snapshot_restore` | Restore a saved snapshot |
|
|
252
253
|
| `snapshot_list` | List all saved snapshots |
|
|
254
|
+
| `snapshot_delete` | Delete a saved snapshot by name |
|
|
253
255
|
|
|
254
256
|
### Routing
|
|
255
257
|
|
|
@@ -373,7 +375,7 @@ Processing decisions adapt to the genre:
|
|
|
373
375
|
|
|
374
376
|
## Autonomous Mode (Allow All Tools)
|
|
375
377
|
|
|
376
|
-
By default Claude Code asks permission for each MCP tool call. The `init` command (and `install-skills`) automatically configures `settings.json` to allow all
|
|
378
|
+
By default Claude Code asks permission for each MCP tool call. The `init` command (and `install-skills`) automatically configures `settings.json` to allow all 80 REAPER tools. If you need to set this up manually, add to your project's `.claude/settings.json` (or `~/.claude/settings.json` for global):
|
|
377
379
|
|
|
378
380
|
```json
|
|
379
381
|
{
|
|
@@ -404,6 +406,7 @@ By default Claude Code asks permission for each MCP tool call. The `init` comman
|
|
|
404
406
|
"mcp__reaper__snapshot_save",
|
|
405
407
|
"mcp__reaper__snapshot_restore",
|
|
406
408
|
"mcp__reaper__snapshot_list",
|
|
409
|
+
"mcp__reaper__snapshot_delete",
|
|
407
410
|
"mcp__reaper__get_track_routing",
|
|
408
411
|
"mcp__reaper__set_fx_enabled",
|
|
409
412
|
"mcp__reaper__set_fx_offline",
|
|
@@ -525,7 +528,7 @@ Platform defaults:
|
|
|
525
528
|
```
|
|
526
529
|
reaper-mcp/
|
|
527
530
|
├── apps/
|
|
528
|
-
│ ├── reaper-mcp-server/ # MCP server (
|
|
531
|
+
│ ├── reaper-mcp-server/ # MCP server (80 tools, esbuild bundle)
|
|
529
532
|
│ └── reaper-mix-agent/ # AI mix agent (knowledge loader, plugin resolver)
|
|
530
533
|
├── libs/protocol/ # Shared TypeScript types
|
|
531
534
|
├── knowledge/ # AI mix engineer knowledge base
|
|
@@ -535,6 +538,7 @@ reaper-mcp/
|
|
|
535
538
|
│ └── reference/ # Frequency, compression, metering, perceived loudness cheat sheets
|
|
536
539
|
├── reaper/ # Files installed into REAPER
|
|
537
540
|
│ ├── mcp_bridge.lua # Persistent Lua bridge
|
|
541
|
+
│ ├── mcp_snapshot_manager.lua # Snapshot manager GUI (save/restore/delete)
|
|
538
542
|
│ ├── mcp_analyzer.jsfx # FFT spectrum analyzer
|
|
539
543
|
│ ├── mcp_lufs_meter.jsfx # LUFS meter (BS.1770)
|
|
540
544
|
│ ├── mcp_correlation_meter.jsfx # Stereo correlation meter
|
package/claude-agents/mixer.md
CHANGED
|
@@ -28,7 +28,7 @@ You are a professional mix engineer with 20 years of experience working inside R
|
|
|
28
28
|
|
|
29
29
|
## Available MCP Tools
|
|
30
30
|
|
|
31
|
-
You have access to
|
|
31
|
+
You have access to 80 REAPER tools via the `reaper` MCP server:
|
|
32
32
|
|
|
33
33
|
### Session Info
|
|
34
34
|
- `get_project_info` — project name, tempo, time sig, sample rate, transport
|
|
@@ -70,6 +70,7 @@ You have access to 67 REAPER tools via the `reaper` MCP server:
|
|
|
70
70
|
- `snapshot_save` — save mixer state
|
|
71
71
|
- `snapshot_restore` — restore saved state
|
|
72
72
|
- `snapshot_list` — list all snapshots
|
|
73
|
+
- `snapshot_delete` — delete a saved snapshot
|
|
73
74
|
|
|
74
75
|
### Markers & Regions
|
|
75
76
|
- `list_markers` — all project markers (index, name, position, color)
|
package/main.js
CHANGED
|
@@ -599,13 +599,39 @@ function registerFxTools(server) {
|
|
|
599
599
|
);
|
|
600
600
|
server.tool(
|
|
601
601
|
"get_fx_parameters",
|
|
602
|
-
"List
|
|
602
|
+
"List parameters of an FX plugin with current values and ranges. Supports filtering by name pattern, changed-only filter, and pagination. For a quick summary of active EQ bands or compressor settings, use analyze_fx instead.",
|
|
603
|
+
{
|
|
604
|
+
trackIndex: z2.coerce.number().int().min(0).describe("Zero-based track index"),
|
|
605
|
+
fxIndex: z2.coerce.number().int().min(0).describe("Zero-based FX index in the chain"),
|
|
606
|
+
namePattern: z2.string().optional().describe('Filter params by name (case-insensitive substring match, e.g. "Gain", "Band 1")'),
|
|
607
|
+
changedOnly: z2.boolean().optional().describe("Only return params that appear non-default (value differs from minimum or formatted value suggests activity)"),
|
|
608
|
+
offset: z2.coerce.number().int().min(0).optional().describe("Skip first N matching params (default 0)"),
|
|
609
|
+
limit: z2.coerce.number().int().min(1).optional().describe("Max params to return (default all)")
|
|
610
|
+
},
|
|
611
|
+
async ({ trackIndex, fxIndex, namePattern, changedOnly, offset, limit }) => {
|
|
612
|
+
const res = await sendCommand("get_fx_parameters", {
|
|
613
|
+
trackIndex,
|
|
614
|
+
fxIndex,
|
|
615
|
+
namePattern,
|
|
616
|
+
changedOnly,
|
|
617
|
+
offset,
|
|
618
|
+
limit
|
|
619
|
+
});
|
|
620
|
+
if (!res.success) {
|
|
621
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
622
|
+
}
|
|
623
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
624
|
+
}
|
|
625
|
+
);
|
|
626
|
+
server.tool(
|
|
627
|
+
"analyze_fx",
|
|
628
|
+
"Analyze an FX plugin and return a compact summary: FX name, preset, and notable (non-default) parameters. For EQ plugins, detects active bands with frequency/gain/Q/shape. For compressors, extracts threshold/ratio/attack/release/makeup. Use this before get_fx_parameters to understand what a plugin is doing without reading 500+ parameters.",
|
|
603
629
|
{
|
|
604
630
|
trackIndex: z2.coerce.number().int().min(0).describe("Zero-based track index"),
|
|
605
631
|
fxIndex: z2.coerce.number().int().min(0).describe("Zero-based FX index in the chain")
|
|
606
632
|
},
|
|
607
633
|
async ({ trackIndex, fxIndex }) => {
|
|
608
|
-
const res = await sendCommand("
|
|
634
|
+
const res = await sendCommand("analyze_fx", { trackIndex, fxIndex });
|
|
609
635
|
if (!res.success) {
|
|
610
636
|
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
611
637
|
}
|
|
@@ -881,6 +907,20 @@ function registerSnapshotTools(server) {
|
|
|
881
907
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
882
908
|
}
|
|
883
909
|
);
|
|
910
|
+
server.tool(
|
|
911
|
+
"snapshot_delete",
|
|
912
|
+
"Delete a named mixer snapshot. Removes the snapshot file from .reaper-mcp/snapshots/ alongside the project file (or global bridge dir for unsaved projects). This action is permanent and cannot be undone.",
|
|
913
|
+
{
|
|
914
|
+
name: z7.string().min(1).describe("Name of the snapshot to delete")
|
|
915
|
+
},
|
|
916
|
+
async ({ name }) => {
|
|
917
|
+
const res = await sendCommand("snapshot_delete", { name });
|
|
918
|
+
if (!res.success) {
|
|
919
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
920
|
+
}
|
|
921
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
922
|
+
}
|
|
923
|
+
);
|
|
884
924
|
}
|
|
885
925
|
|
|
886
926
|
// apps/reaper-mcp-server/src/tools/routing.ts
|
|
@@ -1960,7 +2000,7 @@ var TOOL_CATEGORIES = {
|
|
|
1960
2000
|
fx: {
|
|
1961
2001
|
name: "fx",
|
|
1962
2002
|
description: "FX chain management: add, remove, inspect, and set parameters. Includes batch setup of an entire FX chain and batch parameter updates across multiple plugins.",
|
|
1963
|
-
tools: ["add_fx", "remove_fx", "get_fx_parameters", "set_fx_parameter", "set_fx_enabled", "set_fx_offline", "setup_fx_chain", "set_multiple_fx_parameters"]
|
|
2003
|
+
tools: ["add_fx", "remove_fx", "get_fx_parameters", "analyze_fx", "set_fx_parameter", "set_fx_enabled", "set_fx_offline", "setup_fx_chain", "set_multiple_fx_parameters"]
|
|
1964
2004
|
},
|
|
1965
2005
|
transport: {
|
|
1966
2006
|
name: "transport",
|
|
@@ -2266,7 +2306,8 @@ var REAPER_ASSETS = [
|
|
|
2266
2306
|
"mcp_analyzer.jsfx",
|
|
2267
2307
|
"mcp_lufs_meter.jsfx",
|
|
2268
2308
|
"mcp_correlation_meter.jsfx",
|
|
2269
|
-
"mcp_crest_factor.jsfx"
|
|
2309
|
+
"mcp_crest_factor.jsfx",
|
|
2310
|
+
"mcp_snapshot_manager.lua"
|
|
2270
2311
|
];
|
|
2271
2312
|
var MCP_TOOL_NAMES = [
|
|
2272
2313
|
// project
|
|
@@ -2279,6 +2320,7 @@ var MCP_TOOL_NAMES = [
|
|
|
2279
2320
|
"add_fx",
|
|
2280
2321
|
"remove_fx",
|
|
2281
2322
|
"get_fx_parameters",
|
|
2323
|
+
"analyze_fx",
|
|
2282
2324
|
"set_fx_parameter",
|
|
2283
2325
|
// discovery
|
|
2284
2326
|
"list_available_fx",
|
|
@@ -2303,6 +2345,7 @@ var MCP_TOOL_NAMES = [
|
|
|
2303
2345
|
"snapshot_save",
|
|
2304
2346
|
"snapshot_restore",
|
|
2305
2347
|
"snapshot_list",
|
|
2348
|
+
"snapshot_delete",
|
|
2306
2349
|
// routing
|
|
2307
2350
|
"get_track_routing",
|
|
2308
2351
|
// midi
|
|
@@ -2452,21 +2495,23 @@ async function runInit(opts, dirResolver) {
|
|
|
2452
2495
|
const scriptsDir = getReaperScriptsPath();
|
|
2453
2496
|
mkdirSync2(scriptsDir, { recursive: true });
|
|
2454
2497
|
const reaperDir = resolveAssetDir(__dirname2, "reaper");
|
|
2455
|
-
const
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2498
|
+
for (const luaFile of ["mcp_bridge.lua", "mcp_snapshot_manager.lua"]) {
|
|
2499
|
+
const src = join4(reaperDir, luaFile);
|
|
2500
|
+
const dest = join4(scriptsDir, luaFile);
|
|
2501
|
+
if (installFile(src, dest)) {
|
|
2502
|
+
console.log(` Installed: ${luaFile}`);
|
|
2503
|
+
} else {
|
|
2504
|
+
console.log(` Not found: ${src}`);
|
|
2505
|
+
}
|
|
2461
2506
|
}
|
|
2462
2507
|
const effectsDir = join4(getReaperEffectsPath(), "reaper-mcp");
|
|
2463
2508
|
mkdirSync2(effectsDir, { recursive: true });
|
|
2464
|
-
for (const
|
|
2465
|
-
if (
|
|
2466
|
-
const src = join4(reaperDir,
|
|
2467
|
-
const dest = join4(effectsDir,
|
|
2509
|
+
for (const asset of REAPER_ASSETS) {
|
|
2510
|
+
if (asset.endsWith(".lua")) continue;
|
|
2511
|
+
const src = join4(reaperDir, asset);
|
|
2512
|
+
const dest = join4(effectsDir, asset);
|
|
2468
2513
|
if (installFile(src, dest)) {
|
|
2469
|
-
console.log(` Installed: reaper-mcp/${
|
|
2514
|
+
console.log(` Installed: reaper-mcp/${asset}`);
|
|
2470
2515
|
} else {
|
|
2471
2516
|
console.log(` Not found: ${src}`);
|
|
2472
2517
|
}
|
|
@@ -2569,23 +2614,25 @@ async function setup() {
|
|
|
2569
2614
|
const scriptsDir = getReaperScriptsPath();
|
|
2570
2615
|
mkdirSync3(scriptsDir, { recursive: true });
|
|
2571
2616
|
const reaperDir = resolveAssetDir(__dirname, "reaper");
|
|
2572
|
-
|
|
2573
|
-
const
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2617
|
+
console.log("Installing Lua scripts...");
|
|
2618
|
+
for (const luaFile of ["mcp_bridge.lua", "mcp_snapshot_manager.lua"]) {
|
|
2619
|
+
const src = join5(reaperDir, luaFile);
|
|
2620
|
+
const dest = join5(scriptsDir, luaFile);
|
|
2621
|
+
if (installFile(src, dest)) {
|
|
2622
|
+
console.log(` Installed: ${luaFile}`);
|
|
2623
|
+
} else {
|
|
2624
|
+
console.log(` Not found: ${src}`);
|
|
2625
|
+
}
|
|
2579
2626
|
}
|
|
2580
2627
|
const effectsDir = join5(getReaperEffectsPath(), "reaper-mcp");
|
|
2581
2628
|
mkdirSync3(effectsDir, { recursive: true });
|
|
2582
2629
|
console.log("\nInstalling JSFX analyzers...");
|
|
2583
|
-
for (const
|
|
2584
|
-
if (
|
|
2585
|
-
const src = join5(reaperDir,
|
|
2586
|
-
const dest = join5(effectsDir,
|
|
2630
|
+
for (const asset of REAPER_ASSETS) {
|
|
2631
|
+
if (asset.endsWith(".lua")) continue;
|
|
2632
|
+
const src = join5(reaperDir, asset);
|
|
2633
|
+
const dest = join5(effectsDir, asset);
|
|
2587
2634
|
if (installFile(src, dest)) {
|
|
2588
|
-
console.log(` Installed: reaper-mcp/${
|
|
2635
|
+
console.log(` Installed: reaper-mcp/${asset}`);
|
|
2589
2636
|
} else {
|
|
2590
2637
|
console.log(` Not found: ${src}`);
|
|
2591
2638
|
}
|
|
@@ -2594,7 +2641,7 @@ async function setup() {
|
|
|
2594
2641
|
console.log("Next steps:");
|
|
2595
2642
|
console.log(" 1. Open REAPER");
|
|
2596
2643
|
console.log(" 2. Actions > Show action list > Load ReaScript");
|
|
2597
|
-
console.log(` 3. Select: ${
|
|
2644
|
+
console.log(` 3. Select: ${join5(scriptsDir, "mcp_bridge.lua")}`);
|
|
2598
2645
|
console.log(" 4. Run the script (it will keep running in background via defer loop)");
|
|
2599
2646
|
console.log(" 5. Add reaper-mcp to your Claude Code config (see: npx @mthines/reaper-mcp doctor)");
|
|
2600
2647
|
}
|
package/package.json
CHANGED
package/reaper/CLAUDE.md
CHANGED
|
@@ -7,6 +7,7 @@ Files installed INTO the REAPER DAW by the `setup` command. These run inside REA
|
|
|
7
7
|
| File | Language | Purpose |
|
|
8
8
|
|------|----------|---------|
|
|
9
9
|
| `mcp_bridge.lua` | Lua | Persistent bridge: polls for JSON commands, executes ReaScript API, writes responses |
|
|
10
|
+
| `mcp_snapshot_manager.lua` | Lua | Standalone snapshot manager GUI (gfx-based, save/restore/delete snapshots) |
|
|
10
11
|
| `mcp_analyzer.jsfx` | JSFX/EEL2 | Real-time FFT spectrum analyzer, writes to gmem[] |
|
|
11
12
|
| `mcp_lufs_meter.jsfx` | JSFX/EEL2 | LUFS loudness metering |
|
|
12
13
|
| `mcp_correlation_meter.jsfx` | JSFX/EEL2 | Stereo correlation and width analysis |
|
package/reaper/install.sh
CHANGED
|
@@ -23,11 +23,13 @@ fi
|
|
|
23
23
|
|
|
24
24
|
echo "REAPER resource path: $REAPER_PATH"
|
|
25
25
|
|
|
26
|
-
# Install Lua
|
|
26
|
+
# Install Lua scripts
|
|
27
27
|
SCRIPTS_DIR="$REAPER_PATH/Scripts"
|
|
28
28
|
mkdir -p "$SCRIPTS_DIR"
|
|
29
29
|
cp "$SCRIPT_DIR/mcp_bridge.lua" "$SCRIPTS_DIR/mcp_bridge.lua"
|
|
30
30
|
echo "Installed: $SCRIPTS_DIR/mcp_bridge.lua"
|
|
31
|
+
cp "$SCRIPT_DIR/mcp_snapshot_manager.lua" "$SCRIPTS_DIR/mcp_snapshot_manager.lua"
|
|
32
|
+
echo "Installed: $SCRIPTS_DIR/mcp_snapshot_manager.lua"
|
|
31
33
|
|
|
32
34
|
# Install JSFX analyzers
|
|
33
35
|
EFFECTS_DIR="$REAPER_PATH/Effects/reaper-mcp"
|
package/reaper/mcp_bridge.lua
CHANGED
|
@@ -297,6 +297,155 @@ local function from_db(db)
|
|
|
297
297
|
return 10 ^ (db / 20)
|
|
298
298
|
end
|
|
299
299
|
|
|
300
|
+
-- =============================================================================
|
|
301
|
+
-- FX analysis helpers
|
|
302
|
+
-- =============================================================================
|
|
303
|
+
|
|
304
|
+
-- Heuristic: detect if a parameter appears to be non-default.
|
|
305
|
+
-- REAPER has no TrackFX_GetParamDefault API, so we use multiple signals.
|
|
306
|
+
-- Intentionally permissive — better to include false positives than miss changes.
|
|
307
|
+
local function is_param_non_default(val, min_val, max_val, formatted)
|
|
308
|
+
local range = max_val - min_val
|
|
309
|
+
if range == 0 then return false end
|
|
310
|
+
|
|
311
|
+
-- Check formatted value for common "off/default" indicators
|
|
312
|
+
local lower = formatted:lower()
|
|
313
|
+
if lower == "off" or lower == "none" or lower == "unused"
|
|
314
|
+
or lower == "0.0 db" or lower == "0.00 db"
|
|
315
|
+
or lower == "0 %" or lower == "0.0 %" or lower == "0.00 %" then
|
|
316
|
+
return false
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
-- Value at exact minimum is often default for "disabled" params
|
|
320
|
+
if val == min_val then return false end
|
|
321
|
+
|
|
322
|
+
return true
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
-- Detect EQ band structure by finding groups of Freq/Gain/Q params per band.
|
|
326
|
+
-- Works for ReaEQ, FabFilter Pro-Q, and most parametric EQs.
|
|
327
|
+
local function analyze_eq_bands(all_params, param_count)
|
|
328
|
+
local bands = {}
|
|
329
|
+
|
|
330
|
+
for p = 0, param_count - 1 do
|
|
331
|
+
local par = all_params[p]
|
|
332
|
+
local name = par.name
|
|
333
|
+
local name_lower = name:lower()
|
|
334
|
+
|
|
335
|
+
-- Extract band number from patterns like "Band 1 Frequency", "1 Frequency"
|
|
336
|
+
local band_num = name:match("[Bb]and%s*(%d+)") or name:match("^(%d+)%s")
|
|
337
|
+
if band_num then
|
|
338
|
+
band_num = tonumber(band_num)
|
|
339
|
+
if not bands[band_num] then
|
|
340
|
+
bands[band_num] = { bandIndex = band_num - 1, paramIndices = {} }
|
|
341
|
+
end
|
|
342
|
+
local b = bands[band_num]
|
|
343
|
+
b.paramIndices[#b.paramIndices + 1] = p
|
|
344
|
+
|
|
345
|
+
if name_lower:find("freq") and not name_lower:find("side") then
|
|
346
|
+
b.frequency = par.formattedValue
|
|
347
|
+
b.freq_val = par.value
|
|
348
|
+
elseif name_lower:find("gain") and not name_lower:find("side")
|
|
349
|
+
and not name_lower:find("dynamic") then
|
|
350
|
+
b.gain = par.formattedValue
|
|
351
|
+
b.gain_val = par.value
|
|
352
|
+
b.gain_min = par.minValue
|
|
353
|
+
b.gain_max = par.maxValue
|
|
354
|
+
elseif name_lower:match("^%d+ q$") or name_lower:match("band %d+ q$")
|
|
355
|
+
or name_lower:find("bandwidth") then
|
|
356
|
+
b.q = par.formattedValue
|
|
357
|
+
elseif name_lower:find("type") or name_lower:find("shape") then
|
|
358
|
+
b.shape = par.formattedValue
|
|
359
|
+
elseif name_lower:find("enabled") or name_lower:find("active")
|
|
360
|
+
or name_lower:find("used") then
|
|
361
|
+
b.enabled_val = par.value
|
|
362
|
+
b.enabled_fmt = par.formattedValue
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
-- Filter to active/used bands only
|
|
368
|
+
local active_bands = {}
|
|
369
|
+
local sorted_keys = {}
|
|
370
|
+
for k, _ in pairs(bands) do sorted_keys[#sorted_keys + 1] = k end
|
|
371
|
+
table.sort(sorted_keys)
|
|
372
|
+
|
|
373
|
+
for _, k in ipairs(sorted_keys) do
|
|
374
|
+
local b = bands[k]
|
|
375
|
+
local is_active = true
|
|
376
|
+
|
|
377
|
+
-- Check explicit enabled/used flag
|
|
378
|
+
if b.enabled_val ~= nil then
|
|
379
|
+
if b.enabled_fmt then
|
|
380
|
+
local lower = b.enabled_fmt:lower()
|
|
381
|
+
if lower == "unused" or lower == "off" or lower == "disabled" then
|
|
382
|
+
is_active = false
|
|
383
|
+
else
|
|
384
|
+
is_active = true
|
|
385
|
+
end
|
|
386
|
+
else
|
|
387
|
+
is_active = b.enabled_val > 0.5
|
|
388
|
+
end
|
|
389
|
+
elseif b.gain_val ~= nil and b.gain_min ~= nil then
|
|
390
|
+
-- No enabled flag: check if gain is at midpoint (likely zero/default)
|
|
391
|
+
local gain_range = b.gain_max - b.gain_min
|
|
392
|
+
if gain_range > 0 then
|
|
393
|
+
local gain_norm = (b.gain_val - b.gain_min) / gain_range
|
|
394
|
+
is_active = math.abs(gain_norm - 0.5) > 0.01
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
if is_active then
|
|
399
|
+
active_bands[#active_bands + 1] = {
|
|
400
|
+
bandIndex = b.bandIndex,
|
|
401
|
+
enabled = true,
|
|
402
|
+
frequency = b.frequency or "?",
|
|
403
|
+
gain = b.gain or "?",
|
|
404
|
+
q = b.q or "?",
|
|
405
|
+
shape = b.shape or "?",
|
|
406
|
+
paramIndices = b.paramIndices,
|
|
407
|
+
}
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
return active_bands
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
-- Extract key compressor parameters by name matching.
|
|
415
|
+
local function analyze_compressor(all_params, param_count)
|
|
416
|
+
local settings = {}
|
|
417
|
+
local patterns = {
|
|
418
|
+
{ key = "threshold", pats = { "threshold" } },
|
|
419
|
+
{ key = "ratio", pats = { "ratio" } },
|
|
420
|
+
{ key = "attack", pats = { "attack" } },
|
|
421
|
+
{ key = "release", pats = { "release" } },
|
|
422
|
+
{ key = "makeup", pats = { "makeup", "make%-up", "make up", "output gain" } },
|
|
423
|
+
{ key = "knee", pats = { "knee" } },
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
for p = 0, param_count - 1 do
|
|
427
|
+
local par = all_params[p]
|
|
428
|
+
local name_lower = par.name:lower()
|
|
429
|
+
for _, pat in ipairs(patterns) do
|
|
430
|
+
if not settings[pat.key] then
|
|
431
|
+
for _, pattern in ipairs(pat.pats) do
|
|
432
|
+
if name_lower:find(pattern) then
|
|
433
|
+
settings[pat.key] = {
|
|
434
|
+
index = par.index,
|
|
435
|
+
name = par.name,
|
|
436
|
+
value = par.value,
|
|
437
|
+
formattedValue = par.formattedValue,
|
|
438
|
+
}
|
|
439
|
+
break
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
return settings
|
|
447
|
+
end
|
|
448
|
+
|
|
300
449
|
-- =============================================================================
|
|
301
450
|
-- Command handlers
|
|
302
451
|
-- =============================================================================
|
|
@@ -508,16 +657,108 @@ function handlers.get_fx_parameters(params)
|
|
|
508
657
|
if not track then return nil, "Track " .. idx .. " not found" end
|
|
509
658
|
|
|
510
659
|
local param_count = reaper.TrackFX_GetNumParams(track, fx_idx)
|
|
511
|
-
local parameters = {}
|
|
512
660
|
|
|
661
|
+
-- Get FX name for context
|
|
662
|
+
local _, fx_name = reaper.TrackFX_GetFXName(track, fx_idx)
|
|
663
|
+
|
|
664
|
+
-- Optional filters
|
|
665
|
+
local name_pattern = params.namePattern
|
|
666
|
+
if name_pattern then name_pattern = name_pattern:lower() end
|
|
667
|
+
local changed_only = params.changedOnly
|
|
668
|
+
|
|
669
|
+
-- Collect matching params
|
|
670
|
+
local matched = {}
|
|
513
671
|
for p = 0, param_count - 1 do
|
|
514
672
|
local _, pname = reaper.TrackFX_GetParamName(track, fx_idx, p)
|
|
515
673
|
local val, min_val, max_val = reaper.TrackFX_GetParam(track, fx_idx, p)
|
|
516
674
|
local _, formatted = reaper.TrackFX_GetFormattedParamValue(track, fx_idx, p)
|
|
517
675
|
|
|
518
|
-
|
|
676
|
+
local include = true
|
|
677
|
+
|
|
678
|
+
-- Name filter: case-insensitive substring
|
|
679
|
+
if name_pattern and not pname:lower():find(name_pattern, 1, true) then
|
|
680
|
+
include = false
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
-- Changed-only filter
|
|
684
|
+
if include and changed_only then
|
|
685
|
+
include = is_param_non_default(val, min_val, max_val, formatted or "")
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
if include then
|
|
689
|
+
matched[#matched + 1] = {
|
|
690
|
+
index = p,
|
|
691
|
+
name = pname,
|
|
692
|
+
value = val,
|
|
693
|
+
formattedValue = formatted or "",
|
|
694
|
+
minValue = min_val,
|
|
695
|
+
maxValue = max_val,
|
|
696
|
+
}
|
|
697
|
+
end
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
-- Pagination (applied after filtering)
|
|
701
|
+
local req_offset = params.offset or 0
|
|
702
|
+
local total_matched = #matched
|
|
703
|
+
local start_idx = math.min(req_offset + 1, total_matched + 1)
|
|
704
|
+
local end_idx = total_matched
|
|
705
|
+
if params.limit then
|
|
706
|
+
end_idx = math.min(start_idx + params.limit - 1, total_matched)
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
local result_params = {}
|
|
710
|
+
for i = start_idx, end_idx do
|
|
711
|
+
result_params[#result_params + 1] = matched[i]
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
return {
|
|
715
|
+
trackIndex = idx,
|
|
716
|
+
fxIndex = fx_idx,
|
|
717
|
+
fxName = fx_name or "",
|
|
718
|
+
parameterCount = param_count,
|
|
719
|
+
matchedCount = total_matched,
|
|
720
|
+
returned = #result_params,
|
|
721
|
+
offset = req_offset,
|
|
722
|
+
hasMore = end_idx < total_matched,
|
|
723
|
+
parameters = result_params,
|
|
724
|
+
}
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
function handlers.analyze_fx(params)
|
|
728
|
+
local idx = params.trackIndex
|
|
729
|
+
local fx_idx = params.fxIndex
|
|
730
|
+
if not idx or not fx_idx then
|
|
731
|
+
return nil, "trackIndex and fxIndex required"
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
local track = reaper.GetTrack(0, idx)
|
|
735
|
+
if not track then return nil, "Track " .. idx .. " not found" end
|
|
736
|
+
|
|
737
|
+
local param_count = reaper.TrackFX_GetNumParams(track, fx_idx)
|
|
738
|
+
if param_count == 0 then return nil, "FX " .. fx_idx .. " not found or has no parameters" end
|
|
739
|
+
|
|
740
|
+
local _, fx_name = reaper.TrackFX_GetFXName(track, fx_idx)
|
|
741
|
+
local _, preset_name = reaper.TrackFX_GetPreset(track, fx_idx)
|
|
742
|
+
|
|
743
|
+
-- Detect plugin type from name
|
|
744
|
+
local fx_lower = (fx_name or ""):lower()
|
|
745
|
+
local plugin_type = "generic"
|
|
746
|
+
if fx_lower:find("eq") or fx_lower:find("pro%-q") or fx_lower:find("equaliz") then
|
|
747
|
+
plugin_type = "eq"
|
|
748
|
+
elseif fx_lower:find("comp") or fx_lower:find("pro%-c") or fx_lower:find("1176")
|
|
749
|
+
or fx_lower:find("la%-2a") or fx_lower:find("limiter") or fx_lower:find("pro%-l") then
|
|
750
|
+
plugin_type = "compressor"
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
-- Read all params once
|
|
754
|
+
local all_params = {}
|
|
755
|
+
for p = 0, param_count - 1 do
|
|
756
|
+
local _, pname = reaper.TrackFX_GetParamName(track, fx_idx, p)
|
|
757
|
+
local val, min_val, max_val = reaper.TrackFX_GetParam(track, fx_idx, p)
|
|
758
|
+
local _, formatted = reaper.TrackFX_GetFormattedParamValue(track, fx_idx, p)
|
|
759
|
+
all_params[p] = {
|
|
519
760
|
index = p,
|
|
520
|
-
name = pname,
|
|
761
|
+
name = pname or "",
|
|
521
762
|
value = val,
|
|
522
763
|
formattedValue = formatted or "",
|
|
523
764
|
minValue = min_val,
|
|
@@ -525,11 +766,67 @@ function handlers.get_fx_parameters(params)
|
|
|
525
766
|
}
|
|
526
767
|
end
|
|
527
768
|
|
|
769
|
+
local notable = {}
|
|
770
|
+
local eq_bands = nil
|
|
771
|
+
local comp_settings = nil
|
|
772
|
+
|
|
773
|
+
if plugin_type == "eq" then
|
|
774
|
+
eq_bands = analyze_eq_bands(all_params, param_count)
|
|
775
|
+
-- Notable params = non-band params that are non-default
|
|
776
|
+
for p = 0, param_count - 1 do
|
|
777
|
+
local par = all_params[p]
|
|
778
|
+
local name_lower = par.name:lower()
|
|
779
|
+
if not name_lower:find("band") and not name_lower:match("^%d+ ")
|
|
780
|
+
and is_param_non_default(par.value, par.minValue, par.maxValue, par.formattedValue) then
|
|
781
|
+
notable[#notable + 1] = {
|
|
782
|
+
index = par.index, name = par.name,
|
|
783
|
+
value = par.value, formattedValue = par.formattedValue,
|
|
784
|
+
}
|
|
785
|
+
end
|
|
786
|
+
end
|
|
787
|
+
elseif plugin_type == "compressor" then
|
|
788
|
+
comp_settings = analyze_compressor(all_params, param_count)
|
|
789
|
+
-- Notable = all non-default, non-core-compressor params
|
|
790
|
+
local core_names = {}
|
|
791
|
+
if comp_settings then
|
|
792
|
+
for _, v in pairs(comp_settings) do
|
|
793
|
+
if v and v.index then core_names[v.index] = true end
|
|
794
|
+
end
|
|
795
|
+
end
|
|
796
|
+
for p = 0, param_count - 1 do
|
|
797
|
+
local par = all_params[p]
|
|
798
|
+
if not core_names[p]
|
|
799
|
+
and is_param_non_default(par.value, par.minValue, par.maxValue, par.formattedValue) then
|
|
800
|
+
notable[#notable + 1] = {
|
|
801
|
+
index = par.index, name = par.name,
|
|
802
|
+
value = par.value, formattedValue = par.formattedValue,
|
|
803
|
+
}
|
|
804
|
+
end
|
|
805
|
+
end
|
|
806
|
+
else
|
|
807
|
+
-- Generic: return all non-default params
|
|
808
|
+
for p = 0, param_count - 1 do
|
|
809
|
+
local par = all_params[p]
|
|
810
|
+
if is_param_non_default(par.value, par.minValue, par.maxValue, par.formattedValue) then
|
|
811
|
+
notable[#notable + 1] = {
|
|
812
|
+
index = par.index, name = par.name,
|
|
813
|
+
value = par.value, formattedValue = par.formattedValue,
|
|
814
|
+
}
|
|
815
|
+
end
|
|
816
|
+
end
|
|
817
|
+
end
|
|
818
|
+
|
|
528
819
|
return {
|
|
529
820
|
trackIndex = idx,
|
|
530
821
|
fxIndex = fx_idx,
|
|
822
|
+
fxName = fx_name or "",
|
|
823
|
+
presetName = preset_name or "",
|
|
531
824
|
parameterCount = param_count,
|
|
532
|
-
|
|
825
|
+
notableParamCount = #notable,
|
|
826
|
+
pluginType = plugin_type,
|
|
827
|
+
eqBands = eq_bands,
|
|
828
|
+
compressorSettings = comp_settings,
|
|
829
|
+
notableParams = notable,
|
|
533
830
|
}
|
|
534
831
|
end
|
|
535
832
|
|
|
@@ -1129,6 +1426,26 @@ function handlers.snapshot_list(params)
|
|
|
1129
1426
|
return { snapshots = snapshots, total = #snapshots, storageLocation = get_snapshot_storage_location() }
|
|
1130
1427
|
end
|
|
1131
1428
|
|
|
1429
|
+
function handlers.snapshot_delete(params)
|
|
1430
|
+
local name = params.name
|
|
1431
|
+
if not name or name == "" then
|
|
1432
|
+
return nil, "name required"
|
|
1433
|
+
end
|
|
1434
|
+
|
|
1435
|
+
local path = snapshot_path(name)
|
|
1436
|
+
local content = read_file(path)
|
|
1437
|
+
if not content then
|
|
1438
|
+
return nil, "Snapshot not found: " .. name
|
|
1439
|
+
end
|
|
1440
|
+
|
|
1441
|
+
local ok, err = os.remove(path)
|
|
1442
|
+
if not ok then
|
|
1443
|
+
return nil, "Failed to delete snapshot: " .. (err or "unknown error")
|
|
1444
|
+
end
|
|
1445
|
+
|
|
1446
|
+
return { name = name, deleted = true }
|
|
1447
|
+
end
|
|
1448
|
+
|
|
1132
1449
|
-- =============================================================================
|
|
1133
1450
|
-- Routing handler
|
|
1134
1451
|
-- =============================================================================
|