@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 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 78 REAPER tools)
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 (67) ──→ controls REAPER in real-time
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 78 REAPER tools (no permission prompts)
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 (67)
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 78 REAPER tools. If you need to set this up manually, add to your project's `.claude/settings.json` (or `~/.claude/settings.json` for global):
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 (67 tools, esbuild bundle)
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
@@ -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 67 REAPER tools via the `reaper` MCP server:
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 all parameters of an FX plugin with current values and ranges",
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("get_fx_parameters", { trackIndex, fxIndex });
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 luaSrc = join4(reaperDir, "mcp_bridge.lua");
2456
- const luaDest = join4(scriptsDir, "mcp_bridge.lua");
2457
- if (installFile(luaSrc, luaDest)) {
2458
- console.log(" Installed: mcp_bridge.lua");
2459
- } else {
2460
- console.log(` Not found: ${luaSrc}`);
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 jsfx of REAPER_ASSETS) {
2465
- if (jsfx === "mcp_bridge.lua") continue;
2466
- const src = join4(reaperDir, jsfx);
2467
- const dest = join4(effectsDir, jsfx);
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/${jsfx}`);
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
- const luaSrc = join5(reaperDir, "mcp_bridge.lua");
2573
- const luaDest = join5(scriptsDir, "mcp_bridge.lua");
2574
- console.log("Installing Lua bridge...");
2575
- if (installFile(luaSrc, luaDest)) {
2576
- console.log(` Installed: mcp_bridge.lua`);
2577
- } else {
2578
- console.log(` Not found: ${luaSrc}`);
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 jsfx of REAPER_ASSETS) {
2584
- if (jsfx === "mcp_bridge.lua") continue;
2585
- const src = join5(reaperDir, jsfx);
2586
- const dest = join5(effectsDir, jsfx);
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/${jsfx}`);
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: ${luaDest}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mthines/reaper-mcp",
3
- "version": "0.15.0",
3
+ "version": "0.17.0-beta.18.1",
4
4
  "type": "module",
5
5
  "description": "MCP server for controlling REAPER DAW — real-time mixing, FX control, and frequency analysis for AI agents",
6
6
  "license": "MIT",
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 bridge
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"
@@ -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
- parameters[#parameters + 1] = {
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
- parameters = parameters,
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
  -- =============================================================================