@mthines/reaper-mcp 0.15.0-beta.17.2 → 0.16.0
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 +64 -50
- package/main.js +30 -3
- package/package.json +1 -1
- package/reaper/mcp_bridge.lua +301 -4
package/README.md
CHANGED
|
@@ -12,18 +12,20 @@ AI-powered mixing for REAPER DAW. An MCP server that gives AI agents (Claude Cod
|
|
|
12
12
|
## Quick Start
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
#
|
|
16
|
-
npx @mthines/reaper-mcp
|
|
17
|
-
|
|
18
|
-
# 2. In REAPER: Actions > Load ReaScript > select mcp_bridge.lua > Run
|
|
19
|
-
|
|
20
|
-
# 3. Install AI mix knowledge (globally by default, or --project for local)
|
|
21
|
-
npx @mthines/reaper-mcp install-skills
|
|
15
|
+
# Interactive guided setup (recommended)
|
|
16
|
+
npx @mthines/reaper-mcp init
|
|
22
17
|
|
|
23
|
-
#
|
|
18
|
+
# Or non-interactive — install everything with defaults
|
|
19
|
+
npx @mthines/reaper-mcp init --yes
|
|
24
20
|
```
|
|
25
21
|
|
|
26
|
-
|
|
22
|
+
The `init` wizard walks you through:
|
|
23
|
+
1. Installing the REAPER bridge (Lua + JSFX analyzers)
|
|
24
|
+
2. Installing AI mix knowledge and agents
|
|
25
|
+
3. Configuring Claude Code settings (auto-allows all 78 REAPER tools)
|
|
26
|
+
4. Optionally creating a project-local `.mcp.json`
|
|
27
|
+
|
|
28
|
+
Then load `mcp_bridge.lua` in REAPER and open Claude Code — you're ready to mix.
|
|
27
29
|
|
|
28
30
|
## What it does
|
|
29
31
|
|
|
@@ -70,21 +72,56 @@ REAPER's scripting environment is sandboxed — no sockets, no HTTP. The Lua bri
|
|
|
70
72
|
- [Node.js](https://nodejs.org/) 20+
|
|
71
73
|
- [SWS Extensions](https://www.sws-extension.org/) (recommended — enables plugin discovery and enhanced features)
|
|
72
74
|
|
|
73
|
-
###
|
|
75
|
+
### Option A: Interactive Setup (recommended)
|
|
74
76
|
|
|
75
77
|
```bash
|
|
78
|
+
npx @mthines/reaper-mcp init
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The wizard guides you through selecting which components to install:
|
|
82
|
+
- **REAPER Bridge** — Lua bridge + JSFX analyzers (copied to your REAPER resource folder)
|
|
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)
|
|
85
|
+
- **Project Config** — `.mcp.json` for the current directory (opt-in)
|
|
86
|
+
|
|
87
|
+
For CI/automation, use `--yes` to skip prompts and install everything with defaults:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npx @mthines/reaper-mcp init --yes # bridge + global skills + settings
|
|
91
|
+
npx @mthines/reaper-mcp init --yes --project # also creates .mcp.json in current directory
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Option B: Manual Steps
|
|
95
|
+
|
|
96
|
+
If you prefer to run each step individually:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# 1. Install REAPER components (Lua bridge + JSFX analyzers)
|
|
76
100
|
npx @mthines/reaper-mcp setup
|
|
101
|
+
|
|
102
|
+
# 2. Install AI mix knowledge (globally by default, or --project for local)
|
|
103
|
+
npx @mthines/reaper-mcp install-skills
|
|
104
|
+
npx @mthines/reaper-mcp install-skills --project # project-local alternative
|
|
77
105
|
```
|
|
78
106
|
|
|
79
|
-
|
|
107
|
+
**What gets installed:**
|
|
80
108
|
|
|
109
|
+
The `setup` command copies into your REAPER resource folder:
|
|
81
110
|
- `mcp_bridge.lua` — persistent Lua bridge script
|
|
82
111
|
- `mcp_analyzer.jsfx` — FFT spectrum analyzer
|
|
83
112
|
- `mcp_lufs_meter.jsfx` — ITU-R BS.1770 LUFS meter
|
|
84
113
|
- `mcp_correlation_meter.jsfx` — stereo correlation analyzer
|
|
85
114
|
- `mcp_crest_factor.jsfx` — dynamics/crest factor meter
|
|
86
115
|
|
|
87
|
-
|
|
116
|
+
The `install-skills` command installs to `~/.claude/` (global, default) or `.claude/` (project):
|
|
117
|
+
- `agents/` — mix engineer subagents (`@mix-engineer`, `@gain-stage`, `@mix-analyzer`, `@master`)
|
|
118
|
+
- `rules/` — architecture and development rules
|
|
119
|
+
- `skills/` — skills like `/learn-plugin`
|
|
120
|
+
- `knowledge/` — plugin knowledge, genre rules, workflows, reference data
|
|
121
|
+
|
|
122
|
+
### Start the Lua Bridge in REAPER
|
|
123
|
+
|
|
124
|
+
After setup (either option), load the bridge in REAPER:
|
|
88
125
|
|
|
89
126
|
1. Open REAPER
|
|
90
127
|
2. **Actions > Show action list > Load ReaScript**
|
|
@@ -93,30 +130,7 @@ This copies into your REAPER resource folder:
|
|
|
93
130
|
|
|
94
131
|
You should see in REAPER's console: `MCP Bridge: Started`
|
|
95
132
|
|
|
96
|
-
###
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
# Install globally (default) — available from any directory
|
|
100
|
-
npx @mthines/reaper-mcp install-skills
|
|
101
|
-
|
|
102
|
-
# Or install into a specific project
|
|
103
|
-
cd your-music-project
|
|
104
|
-
npx @mthines/reaper-mcp install-skills --project
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
**Global install** (`--global`, default) installs to `~/.claude/`:
|
|
108
|
-
|
|
109
|
-
- `~/.claude/agents/` — mix engineer subagents (`@mix-engineer`, `@gain-stage`, `@mix-analyzer`, `@master`)
|
|
110
|
-
- `~/.claude/rules/` — architecture and development rules
|
|
111
|
-
- `~/.claude/skills/` — skills like `/learn-plugin`
|
|
112
|
-
- `~/.claude/knowledge/` — plugin knowledge, genre rules, workflows, reference data
|
|
113
|
-
|
|
114
|
-
**Project install** (`--project`) installs to your current directory:
|
|
115
|
-
|
|
116
|
-
- `.claude/agents/`, `.claude/rules/`, `.claude/skills/`, `knowledge/` — same as above, scoped to the project
|
|
117
|
-
- `.mcp.json` — MCP server configuration for Claude Code
|
|
118
|
-
|
|
119
|
-
### Step 4: Verify
|
|
133
|
+
### Verify
|
|
120
134
|
|
|
121
135
|
```bash
|
|
122
136
|
npx @mthines/reaper-mcp doctor
|
|
@@ -245,7 +259,7 @@ Checks that the bridge is connected, knowledge is installed, and MCP config exis
|
|
|
245
259
|
|
|
246
260
|
## Using the Mix Agents
|
|
247
261
|
|
|
248
|
-
Once you've run `setup`
|
|
262
|
+
Once you've run `init` (or `setup` + `install-skills`), open Claude Code. Four specialized mix agents are available:
|
|
249
263
|
|
|
250
264
|
### Available Agents
|
|
251
265
|
|
|
@@ -359,9 +373,7 @@ Processing decisions adapt to the genre:
|
|
|
359
373
|
|
|
360
374
|
## Autonomous Mode (Allow All Tools)
|
|
361
375
|
|
|
362
|
-
By default Claude Code asks permission for each MCP tool call.
|
|
363
|
-
|
|
364
|
-
Add to your project's `.claude/settings.json` (or `~/.claude/settings.json` for global):
|
|
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):
|
|
365
377
|
|
|
366
378
|
```json
|
|
367
379
|
{
|
|
@@ -443,21 +455,23 @@ The format is `mcp__reaper__{tool_name}`. Once added, Claude Code will run these
|
|
|
443
455
|
|
|
444
456
|
## CLI Commands
|
|
445
457
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
npx @mthines/reaper-mcp
|
|
449
|
-
npx @mthines/reaper-mcp setup
|
|
450
|
-
npx @mthines/reaper-mcp
|
|
451
|
-
npx @mthines/reaper-mcp
|
|
452
|
-
npx @mthines/reaper-mcp
|
|
453
|
-
npx @mthines/reaper-mcp
|
|
454
|
-
|
|
458
|
+
| Command | Description |
|
|
459
|
+
|---------|-------------|
|
|
460
|
+
| `npx @mthines/reaper-mcp init` | Interactive guided setup (recommended for new users) |
|
|
461
|
+
| `npx @mthines/reaper-mcp init --yes` | Non-interactive setup — install everything with defaults |
|
|
462
|
+
| `npx @mthines/reaper-mcp init --yes --project` | Non-interactive setup + create `.mcp.json` in current directory |
|
|
463
|
+
| `npx @mthines/reaper-mcp serve` | Start MCP server in stdio mode (default when no command given) |
|
|
464
|
+
| `npx @mthines/reaper-mcp setup` | Install Lua bridge + JSFX into REAPER |
|
|
465
|
+
| `npx @mthines/reaper-mcp install-skills` | Install AI knowledge + agents globally |
|
|
466
|
+
| `npx @mthines/reaper-mcp install-skills --project` | Install into current project directory |
|
|
467
|
+
| `npx @mthines/reaper-mcp doctor` | Verify everything is configured correctly |
|
|
468
|
+
| `npx @mthines/reaper-mcp status` | Check bridge connection |
|
|
455
469
|
|
|
456
470
|
Or install globally for shorter commands:
|
|
457
471
|
|
|
458
472
|
```bash
|
|
459
473
|
npm install -g @mthines/reaper-mcp
|
|
460
|
-
reaper-mcp
|
|
474
|
+
reaper-mcp init
|
|
461
475
|
```
|
|
462
476
|
|
|
463
477
|
## Claude Code Integration
|
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
|
}
|
|
@@ -1960,7 +1986,7 @@ var TOOL_CATEGORIES = {
|
|
|
1960
1986
|
fx: {
|
|
1961
1987
|
name: "fx",
|
|
1962
1988
|
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"]
|
|
1989
|
+
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
1990
|
},
|
|
1965
1991
|
transport: {
|
|
1966
1992
|
name: "transport",
|
|
@@ -2279,6 +2305,7 @@ var MCP_TOOL_NAMES = [
|
|
|
2279
2305
|
"add_fx",
|
|
2280
2306
|
"remove_fx",
|
|
2281
2307
|
"get_fx_parameters",
|
|
2308
|
+
"analyze_fx",
|
|
2282
2309
|
"set_fx_parameter",
|
|
2283
2310
|
// discovery
|
|
2284
2311
|
"list_available_fx",
|
package/package.json
CHANGED
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
|
|