@mthines/reaper-mcp 0.14.0 → 0.15.0-beta.17.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/main.js CHANGED
@@ -2202,10 +2202,10 @@ function createServer() {
2202
2202
  }
2203
2203
 
2204
2204
  // apps/reaper-mcp-server/src/main.ts
2205
- import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
2206
- import { join as join4, dirname as dirname2 } from "node:path";
2205
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3 } from "node:fs";
2206
+ import { join as join5, dirname as dirname2 } from "node:path";
2207
2207
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2208
- import { homedir as homedir2 } from "node:os";
2208
+ import { homedir as homedir3 } from "node:os";
2209
2209
 
2210
2210
  // apps/reaper-mcp-server/src/cli.ts
2211
2211
  import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
@@ -2391,6 +2391,174 @@ function resolveAssetDirWithFallback(baseDir, buildName, sourceName) {
2391
2391
  return resolveAssetDir(baseDir, sourceName);
2392
2392
  }
2393
2393
 
2394
+ // apps/reaper-mcp-server/src/init.ts
2395
+ import { checkbox, select } from "@inquirer/prompts";
2396
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "node:fs";
2397
+ import { join as join4 } from "node:path";
2398
+ import { homedir as homedir2 } from "node:os";
2399
+ async function runInit(opts, dirResolver) {
2400
+ const isTTY = Boolean(process.stdin.isTTY);
2401
+ const headless = opts.yes || !isTTY;
2402
+ console.log("REAPER MCP \u2014 Interactive Setup\n");
2403
+ const bridgeDir = await ensureBridgeDir();
2404
+ console.log(`REAPER resource path: ${join4(bridgeDir, "..", "..")}
2405
+ `);
2406
+ let selections;
2407
+ if (headless) {
2408
+ selections = {
2409
+ bridge: true,
2410
+ skills: true,
2411
+ settings: true,
2412
+ projectConfig: opts.project,
2413
+ skillsScope: "global"
2414
+ };
2415
+ if (opts.yes) {
2416
+ console.log("Running in non-interactive mode (--yes flag).\n");
2417
+ } else {
2418
+ console.log("Non-interactive terminal detected. Running with defaults.\n");
2419
+ }
2420
+ } else {
2421
+ const components = await checkbox({
2422
+ message: "Which components would you like to install?",
2423
+ choices: [
2424
+ { name: "REAPER Bridge (Lua bridge + JSFX analyzers)", value: "bridge", checked: true },
2425
+ { name: `AI Skills & Agents (knowledge base, Claude agents, rules, skills)`, value: "skills", checked: true },
2426
+ { name: `Claude Code Settings (auto-allow ${MCP_TOOL_NAMES.length} REAPER tools)`, value: "settings", checked: true },
2427
+ { name: "Project Config (.mcp.json in current directory)", value: "projectConfig", checked: false }
2428
+ ]
2429
+ });
2430
+ let skillsScope = "global";
2431
+ if (components.includes("skills")) {
2432
+ const scopeAnswer = await select({
2433
+ message: "Install scope for AI Skills & Agents:",
2434
+ choices: [
2435
+ { name: "Global (~/.claude/) \u2014 available in all projects", value: "global" },
2436
+ { name: "Project (.claude/) \u2014 current directory only", value: "project" }
2437
+ ]
2438
+ });
2439
+ skillsScope = scopeAnswer;
2440
+ }
2441
+ selections = {
2442
+ bridge: components.includes("bridge"),
2443
+ skills: components.includes("skills"),
2444
+ settings: components.includes("settings"),
2445
+ projectConfig: components.includes("projectConfig"),
2446
+ skillsScope
2447
+ };
2448
+ }
2449
+ const __dirname2 = dirResolver();
2450
+ if (selections.bridge) {
2451
+ console.log("Installing REAPER Bridge...");
2452
+ const scriptsDir = getReaperScriptsPath();
2453
+ mkdirSync2(scriptsDir, { recursive: true });
2454
+ 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}`);
2461
+ }
2462
+ const effectsDir = join4(getReaperEffectsPath(), "reaper-mcp");
2463
+ 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);
2468
+ if (installFile(src, dest)) {
2469
+ console.log(` Installed: reaper-mcp/${jsfx}`);
2470
+ } else {
2471
+ console.log(` Not found: ${src}`);
2472
+ }
2473
+ }
2474
+ console.log("");
2475
+ }
2476
+ if (selections.skills) {
2477
+ const isGlobal = selections.skillsScope === "global";
2478
+ const baseDir = isGlobal ? join4(homedir2(), ".claude") : process.cwd();
2479
+ const claudeDir = isGlobal ? baseDir : join4(baseDir, ".claude");
2480
+ console.log(`Installing AI Skills & Agents (${selections.skillsScope})...`);
2481
+ const knowledgeSrc = resolveAssetDir(__dirname2, "knowledge");
2482
+ if (existsSync2(knowledgeSrc)) {
2483
+ const dest = join4(isGlobal ? join4(homedir2(), ".claude") : process.cwd(), "knowledge");
2484
+ const count = copyDirSync(knowledgeSrc, dest);
2485
+ console.log(` Installed knowledge base: ${count} files`);
2486
+ } else {
2487
+ console.log(" Knowledge base not found in package. Skipping.");
2488
+ }
2489
+ const rulesSrc = resolveAssetDirWithFallback(__dirname2, "claude-rules", join4(".claude", "rules"));
2490
+ if (existsSync2(rulesSrc)) {
2491
+ const dest = join4(claudeDir, "rules");
2492
+ const count = copyDirSync(rulesSrc, dest);
2493
+ console.log(` Installed Claude rules: ${count} files`);
2494
+ }
2495
+ const skillsSrc = resolveAssetDirWithFallback(__dirname2, "claude-skills", join4(".claude", "skills"));
2496
+ if (existsSync2(skillsSrc)) {
2497
+ const dest = join4(claudeDir, "skills");
2498
+ const count = copyDirSync(skillsSrc, dest);
2499
+ console.log(` Installed Claude skills: ${count} files`);
2500
+ }
2501
+ const agentsSrc = resolveAssetDirWithFallback(__dirname2, "claude-agents", join4(".claude", "agents"));
2502
+ if (existsSync2(agentsSrc)) {
2503
+ const dest = join4(claudeDir, "agents");
2504
+ const count = copyDirSync(agentsSrc, dest);
2505
+ console.log(` Installed Claude agents: ${count} files`);
2506
+ }
2507
+ console.log("");
2508
+ }
2509
+ if (selections.settings) {
2510
+ console.log("Configuring Claude Code Settings...");
2511
+ const settingsDir = join4(homedir2(), ".claude");
2512
+ const settingsPath = join4(settingsDir, "settings.json");
2513
+ const result = ensureClaudeSettings(settingsPath);
2514
+ if (result === "created") {
2515
+ console.log(` Created: ${settingsPath}`);
2516
+ } else if (result === "updated") {
2517
+ console.log(` Updated with new REAPER tools: ${settingsPath}`);
2518
+ } else {
2519
+ console.log(` Already configured: ${settingsPath}`);
2520
+ }
2521
+ console.log("");
2522
+ }
2523
+ if (selections.projectConfig) {
2524
+ console.log("Creating Project Config...");
2525
+ const mcpJsonPath = join4(process.cwd(), ".mcp.json");
2526
+ if (createMcpJson(mcpJsonPath)) {
2527
+ console.log(` Created: ${mcpJsonPath}`);
2528
+ } else {
2529
+ console.log(` Already exists: ${mcpJsonPath}`);
2530
+ }
2531
+ console.log("");
2532
+ }
2533
+ console.log("Running system check...");
2534
+ const bridgeRunning = await isBridgeRunning();
2535
+ console.log(` Lua bridge: ${bridgeRunning ? "Connected" : "Not detected (start after REAPER is open)"}`);
2536
+ const globalClaudeDir = join4(homedir2(), ".claude");
2537
+ const localAgents = existsSync2(join4(process.cwd(), ".claude", "agents"));
2538
+ const globalAgents = existsSync2(join4(globalClaudeDir, "agents"));
2539
+ const agentsExist = localAgents || globalAgents;
2540
+ console.log(` Mix agents: ${agentsExist ? "Installed" : "Not installed"}`);
2541
+ const mcpJsonExists = existsSync2(join4(process.cwd(), ".mcp.json"));
2542
+ console.log(` MCP config: ${mcpJsonExists ? ".mcp.json found" : ".mcp.json not present"}`);
2543
+ console.log("");
2544
+ console.log("Setup complete! Next steps:");
2545
+ if (selections.bridge) {
2546
+ const scriptsDir = getReaperScriptsPath();
2547
+ const luaDest = join4(scriptsDir, "mcp_bridge.lua");
2548
+ console.log(" 1. Open REAPER");
2549
+ console.log(" 2. Actions > Show action list > Load ReaScript");
2550
+ console.log(` 3. Select: ${luaDest}`);
2551
+ console.log(" 4. Run the script (it keeps running via defer loop)");
2552
+ console.log(" 5. Open Claude Code \u2014 REAPER tools + mix engineer agents are ready");
2553
+ } else {
2554
+ console.log(" 1. Open Claude Code \u2014 REAPER tools + mix engineer agents are ready");
2555
+ }
2556
+ if (agentsExist) {
2557
+ console.log('\nTry: @mix-engineer "Please gain stage my tracks"');
2558
+ console.log('Or: @mix-analyzer "Roast my mix"');
2559
+ }
2560
+ }
2561
+
2394
2562
  // apps/reaper-mcp-server/src/main.ts
2395
2563
  var __dirname = dirname2(fileURLToPath2(import.meta.url));
2396
2564
  async function setup() {
@@ -2399,23 +2567,23 @@ async function setup() {
2399
2567
  console.log(`Bridge directory: ${bridgeDir}
2400
2568
  `);
2401
2569
  const scriptsDir = getReaperScriptsPath();
2402
- mkdirSync2(scriptsDir, { recursive: true });
2570
+ mkdirSync3(scriptsDir, { recursive: true });
2403
2571
  const reaperDir = resolveAssetDir(__dirname, "reaper");
2404
- const luaSrc = join4(reaperDir, "mcp_bridge.lua");
2405
- const luaDest = join4(scriptsDir, "mcp_bridge.lua");
2572
+ const luaSrc = join5(reaperDir, "mcp_bridge.lua");
2573
+ const luaDest = join5(scriptsDir, "mcp_bridge.lua");
2406
2574
  console.log("Installing Lua bridge...");
2407
2575
  if (installFile(luaSrc, luaDest)) {
2408
2576
  console.log(` Installed: mcp_bridge.lua`);
2409
2577
  } else {
2410
2578
  console.log(` Not found: ${luaSrc}`);
2411
2579
  }
2412
- const effectsDir = join4(getReaperEffectsPath(), "reaper-mcp");
2413
- mkdirSync2(effectsDir, { recursive: true });
2580
+ const effectsDir = join5(getReaperEffectsPath(), "reaper-mcp");
2581
+ mkdirSync3(effectsDir, { recursive: true });
2414
2582
  console.log("\nInstalling JSFX analyzers...");
2415
2583
  for (const jsfx of REAPER_ASSETS) {
2416
2584
  if (jsfx === "mcp_bridge.lua") continue;
2417
- const src = join4(reaperDir, jsfx);
2418
- const dest = join4(effectsDir, jsfx);
2585
+ const src = join5(reaperDir, jsfx);
2586
+ const dest = join5(effectsDir, jsfx);
2419
2587
  if (installFile(src, dest)) {
2420
2588
  console.log(` Installed: reaper-mcp/${jsfx}`);
2421
2589
  } else {
@@ -2438,41 +2606,41 @@ async function installSkills(scope) {
2438
2606
  console.log(`REAPER MCP \u2014 Install AI Mix Engineer Skills (scope: ${scope})
2439
2607
  `);
2440
2608
  const isGlobal = scope === "global";
2441
- const baseDir = isGlobal ? join4(homedir2(), ".claude") : process.cwd();
2442
- const claudeDir = isGlobal ? baseDir : join4(baseDir, ".claude");
2609
+ const baseDir = isGlobal ? join5(homedir3(), ".claude") : process.cwd();
2610
+ const claudeDir = isGlobal ? baseDir : join5(baseDir, ".claude");
2443
2611
  const knowledgeSrc = resolveAssetDir(__dirname, "knowledge");
2444
- if (existsSync2(knowledgeSrc)) {
2445
- const dest = join4(baseDir, "knowledge");
2612
+ if (existsSync3(knowledgeSrc)) {
2613
+ const dest = join5(baseDir, "knowledge");
2446
2614
  const count = copyDirSync(knowledgeSrc, dest);
2447
2615
  console.log(`Installed knowledge base: ${count} files \u2192 ${dest}`);
2448
2616
  } else {
2449
2617
  console.log("Knowledge base not found in package. Skipping.");
2450
2618
  }
2451
- const rulesSrc = resolveAssetDirWithFallback(__dirname, "claude-rules", join4(".claude", "rules"));
2452
- if (existsSync2(rulesSrc)) {
2453
- const dest = join4(claudeDir, "rules");
2619
+ const rulesSrc = resolveAssetDirWithFallback(__dirname, "claude-rules", join5(".claude", "rules"));
2620
+ if (existsSync3(rulesSrc)) {
2621
+ const dest = join5(claudeDir, "rules");
2454
2622
  const count = copyDirSync(rulesSrc, dest);
2455
2623
  console.log(`Installed Claude rules: ${count} files \u2192 ${dest}`);
2456
2624
  } else {
2457
2625
  console.log("Claude rules not found in package. Skipping.");
2458
2626
  }
2459
- const skillsSrc = resolveAssetDirWithFallback(__dirname, "claude-skills", join4(".claude", "skills"));
2460
- if (existsSync2(skillsSrc)) {
2461
- const dest = join4(claudeDir, "skills");
2627
+ const skillsSrc = resolveAssetDirWithFallback(__dirname, "claude-skills", join5(".claude", "skills"));
2628
+ if (existsSync3(skillsSrc)) {
2629
+ const dest = join5(claudeDir, "skills");
2462
2630
  const count = copyDirSync(skillsSrc, dest);
2463
2631
  console.log(`Installed Claude skills: ${count} files \u2192 ${dest}`);
2464
2632
  } else {
2465
2633
  console.log("Claude skills not found in package. Skipping.");
2466
2634
  }
2467
- const agentsSrc = resolveAssetDirWithFallback(__dirname, "claude-agents", join4(".claude", "agents"));
2468
- if (existsSync2(agentsSrc)) {
2469
- const dest = join4(claudeDir, "agents");
2635
+ const agentsSrc = resolveAssetDirWithFallback(__dirname, "claude-agents", join5(".claude", "agents"));
2636
+ if (existsSync3(agentsSrc)) {
2637
+ const dest = join5(claudeDir, "agents");
2470
2638
  const count = copyDirSync(agentsSrc, dest);
2471
2639
  console.log(`Installed Claude agents: ${count} files \u2192 ${dest}`);
2472
2640
  } else {
2473
2641
  console.log("Claude agents not found in package. Skipping.");
2474
2642
  }
2475
- const settingsPath = join4(claudeDir, "settings.json");
2643
+ const settingsPath = join5(claudeDir, "settings.json");
2476
2644
  const result = ensureClaudeSettings(settingsPath);
2477
2645
  if (result === "created") {
2478
2646
  console.log(`Created Claude settings: ${settingsPath}`);
@@ -2482,7 +2650,7 @@ async function installSkills(scope) {
2482
2650
  console.log(`Claude settings already has all REAPER tools: ${settingsPath}`);
2483
2651
  }
2484
2652
  if (!isGlobal) {
2485
- const mcpJsonPath = join4(baseDir, ".mcp.json");
2653
+ const mcpJsonPath = join5(baseDir, ".mcp.json");
2486
2654
  if (createMcpJson(mcpJsonPath)) {
2487
2655
  console.log(`
2488
2656
  Created: ${mcpJsonPath}`);
@@ -2503,24 +2671,24 @@ async function doctor() {
2503
2671
  if (!bridgeRunning) {
2504
2672
  console.log(' \u2192 Run "npx @mthines/reaper-mcp setup" then load mcp_bridge.lua in REAPER');
2505
2673
  }
2506
- const globalClaudeDir = join4(homedir2(), ".claude");
2507
- const localAgents = existsSync2(join4(process.cwd(), ".claude", "agents"));
2508
- const globalAgents = existsSync2(join4(globalClaudeDir, "agents"));
2674
+ const globalClaudeDir = join5(homedir3(), ".claude");
2675
+ const localAgents = existsSync3(join5(process.cwd(), ".claude", "agents"));
2676
+ const globalAgents = existsSync3(join5(globalClaudeDir, "agents"));
2509
2677
  const agentsExist = localAgents || globalAgents;
2510
2678
  const agentsLocation = localAgents ? ".claude/agents/" : globalAgents ? "~/.claude/agents/" : "";
2511
2679
  console.log(`Mix agents: ${agentsExist ? `\u2713 Found (${agentsLocation})` : "\u2717 Not installed"}`);
2512
2680
  if (!agentsExist) {
2513
2681
  console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"');
2514
2682
  }
2515
- const localKnowledge = existsSync2(join4(process.cwd(), "knowledge"));
2516
- const globalKnowledge = existsSync2(join4(globalClaudeDir, "knowledge"));
2683
+ const localKnowledge = existsSync3(join5(process.cwd(), "knowledge"));
2684
+ const globalKnowledge = existsSync3(join5(globalClaudeDir, "knowledge"));
2517
2685
  const knowledgeExists = localKnowledge || globalKnowledge;
2518
2686
  const knowledgeLocation = localKnowledge ? "project" : globalKnowledge ? "~/.claude/" : "";
2519
2687
  console.log(`Knowledge base: ${knowledgeExists ? `\u2713 Found (${knowledgeLocation})` : "\u2717 Not installed"}`);
2520
2688
  if (!knowledgeExists) {
2521
2689
  console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"');
2522
2690
  }
2523
- const mcpJsonExists = existsSync2(join4(process.cwd(), ".mcp.json"));
2691
+ const mcpJsonExists = existsSync3(join5(process.cwd(), ".mcp.json"));
2524
2692
  console.log(`MCP config: ${mcpJsonExists ? "\u2713 .mcp.json found" : "\u2717 .mcp.json missing"}`);
2525
2693
  if (!mcpJsonExists) {
2526
2694
  console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills --project" to create .mcp.json');
@@ -2583,7 +2751,19 @@ async function serve() {
2583
2751
  });
2584
2752
  }
2585
2753
  var command = process.argv[2];
2754
+ var cliArgs = process.argv.slice(3);
2755
+ var hasYesFlag = cliArgs.includes("--yes") || cliArgs.includes("-y");
2756
+ var hasProjectFlag = cliArgs.includes("--project");
2586
2757
  switch (command) {
2758
+ case "init":
2759
+ runInit(
2760
+ { yes: hasYesFlag, project: hasProjectFlag },
2761
+ () => __dirname
2762
+ ).catch((err) => {
2763
+ console.error("Init failed:", err);
2764
+ process.exit(1);
2765
+ });
2766
+ break;
2587
2767
  case "setup":
2588
2768
  setup().catch((err) => {
2589
2769
  console.error("Setup failed:", err);
@@ -2623,6 +2803,9 @@ switch (command) {
2623
2803
  Usage:
2624
2804
  npx @mthines/reaper-mcp Start MCP server (stdio mode)
2625
2805
  npx @mthines/reaper-mcp serve Start MCP server (stdio mode)
2806
+ npx @mthines/reaper-mcp init Guided interactive setup (recommended for new users)
2807
+ npx @mthines/reaper-mcp init --yes Non-interactive setup (install everything with defaults)
2808
+ npx @mthines/reaper-mcp init --project Include .mcp.json in current directory
2626
2809
  npx @mthines/reaper-mcp setup Install Lua bridge + JSFX analyzers into REAPER
2627
2810
  npx @mthines/reaper-mcp install-skills Install AI knowledge + agents globally (default)
2628
2811
  npx @mthines/reaper-mcp install-skills --project Install into current project directory
@@ -2630,7 +2813,10 @@ Usage:
2630
2813
  npx @mthines/reaper-mcp doctor Check that everything is configured correctly
2631
2814
  npx @mthines/reaper-mcp status Check if Lua bridge is running in REAPER
2632
2815
 
2633
- Quick Start:
2816
+ Quick Start (interactive):
2817
+ npx @mthines/reaper-mcp init # guided setup \u2014 select components interactively
2818
+
2819
+ Quick Start (manual steps):
2634
2820
  1. npx @mthines/reaper-mcp setup # install REAPER components
2635
2821
  2. Load mcp_bridge.lua in REAPER (Actions > Load ReaScript > Run)
2636
2822
  3. npx @mthines/reaper-mcp install-skills # install AI knowledge + agents (globally)
@@ -2638,7 +2824,7 @@ Quick Start:
2638
2824
 
2639
2825
  Tip: install globally for shorter commands:
2640
2826
  npm install -g @mthines/reaper-mcp
2641
- reaper-mcp setup
2827
+ reaper-mcp init
2642
2828
  `);
2643
2829
  break;
2644
2830
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mthines/reaper-mcp",
3
- "version": "0.14.0",
3
+ "version": "0.15.0-beta.17.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",
@@ -27,6 +27,7 @@
27
27
  "reaper-mcp": "./main.js"
28
28
  },
29
29
  "dependencies": {
30
+ "@inquirer/prompts": "^8.3.2",
30
31
  "@modelcontextprotocol/sdk": "^1.12.0",
31
32
  "@opentelemetry/api": "^1.9.0",
32
33
  "@opentelemetry/auto-instrumentations-node": "^0.71.0",
@@ -584,23 +584,10 @@ function handlers.read_track_spectrum(params)
584
584
  local track = reaper.GetTrack(0, idx)
585
585
  if not track then return nil, "Track " .. idx .. " not found" end
586
586
 
587
- -- Check if MCP analyzer JSFX is already on the track
588
- local analyzer_idx = -1
589
- local fx_count = reaper.TrackFX_GetCount(track)
590
- for i = 0, fx_count - 1 do
591
- local _, name = reaper.TrackFX_GetFXName(track, i)
592
- if name and name:find(MCP_ANALYZER_FX_NAME) then
593
- analyzer_idx = i
594
- break
595
- end
596
- end
597
-
598
- -- Auto-insert if not present
599
- if analyzer_idx < 0 then
600
- analyzer_idx = reaper.TrackFX_AddByName(track, MCP_ANALYZER_FX_NAME, false, -1)
601
- if analyzer_idx < 0 then
602
- return nil, "MCP Spectrum Analyzer JSFX not found. Run 'reaper-mcp setup' to install it."
603
- end
587
+ -- Find or auto-insert analyzer (uses MCP Meters container if available)
588
+ local analyzer_idx, err = ensure_jsfx_on_track(track, MCP_ANALYZER_FX_NAME)
589
+ if not analyzer_idx then
590
+ return nil, err or "MCP Spectrum Analyzer JSFX not found. Run 'reaper-mcp setup' to install it."
604
591
  end
605
592
 
606
593
  -- Read spectrum data from gmem
@@ -1000,8 +987,25 @@ function handlers.snapshot_restore(params)
1000
987
 
1001
988
  local restored = 0
1002
989
 
1003
- if state.tracks then
1004
- for _, track_state in ipairs(state.tracks) do
990
+ -- Decode tracks array if the fallback JSON parser left it as a raw string
991
+ local tracks = state.tracks
992
+ if type(tracks) == "string" then
993
+ tracks = json_decode_array(tracks)
994
+ end
995
+
996
+ if tracks then
997
+ for _, track_state in ipairs(tracks) do
998
+ -- Decode nested arrays that may also be raw strings from fallback parser
999
+ if type(track_state.fx) == "string" then
1000
+ track_state.fx = json_decode_array(track_state.fx)
1001
+ end
1002
+ if type(track_state.sends) == "string" then
1003
+ track_state.sends = json_decode_array(track_state.sends)
1004
+ end
1005
+ if type(track_state.fxEnabled) == "string" then
1006
+ track_state.fxEnabled = json_decode_array(track_state.fxEnabled)
1007
+ end
1008
+
1005
1009
  local track = reaper.GetTrack(0, track_state.index)
1006
1010
  if track then
1007
1011
  -- Basic mixer state (all versions)
@@ -1211,10 +1215,119 @@ end
1211
1215
  local MCP_LUFS_METER_FX_NAME = "reaper-mcp/mcp_lufs_meter"
1212
1216
  local MCP_CORRELATION_METER_FX_NAME = "reaper-mcp/mcp_correlation_meter"
1213
1217
  local MCP_CREST_FACTOR_FX_NAME = "reaper-mcp/mcp_crest_factor"
1218
+ local MCP_FX_PREFIX = "reaper%-mcp/" -- Lua pattern for matching MCP JSFX names
1219
+
1220
+ -- Per-track cache of MCP container FX index to avoid rescanning on every read.
1221
+ -- Keyed by track pointer (userdata), value is container FX index or false (no container).
1222
+ -- Invalidated when the FX chain changes (checked via TrackFX_GetCount).
1223
+ local container_cache = {}
1224
+
1225
+ -- Find or create the MCP Meters container on a track.
1226
+ -- Uses REAPER 7.06+ container_item.X API. Returns container FX index, or nil
1227
+ -- if containers are not supported (REAPER < 7.06).
1228
+ local function find_or_create_mcp_container(track)
1229
+ -- Check cache first
1230
+ local cache_entry = container_cache[tostring(track)]
1231
+ if cache_entry then
1232
+ local cached_idx, cached_fx_count = cache_entry[1], cache_entry[2]
1233
+ local current_fx_count = reaper.TrackFX_GetCount(track)
1234
+ if current_fx_count == cached_fx_count then
1235
+ if cached_idx == false then return nil end -- cached negative result
1236
+ return cached_idx
1237
+ end
1238
+ -- FX count changed — invalidate cache
1239
+ container_cache[tostring(track)] = nil
1240
+ end
1241
+
1242
+ local fx_count = reaper.TrackFX_GetCount(track)
1243
+
1244
+ -- Search for existing container that holds MCP JSFX
1245
+ for i = 0, fx_count - 1 do
1246
+ local _, name = reaper.TrackFX_GetFXName(track, i)
1247
+ if name and (name:find("Container") or name:find("MCP Meters")) then
1248
+ local ok, count_str = reaper.TrackFX_GetNamedConfigParm(track, i, "container_count")
1249
+ if ok then
1250
+ local count = tonumber(count_str) or 0
1251
+ for j = 0, count - 1 do
1252
+ local _, addr_str = reaper.TrackFX_GetNamedConfigParm(track, i, "container_item." .. j)
1253
+ local addr = tonumber(addr_str)
1254
+ if addr then
1255
+ local _, fx_name = reaper.TrackFX_GetFXName(track, addr)
1256
+ if fx_name and fx_name:find(MCP_FX_PREFIX) then
1257
+ -- Found our container — cache and return
1258
+ container_cache[tostring(track)] = { i, fx_count }
1259
+ return i
1260
+ end
1261
+ end
1262
+ end
1263
+ end
1264
+ end
1265
+ end
1266
+
1267
+ -- No existing MCP container — create one at end of chain
1268
+ local idx = reaper.TrackFX_AddByName(track, "Container", false, -1)
1269
+ if idx < 0 then
1270
+ -- Container FX not available (REAPER < 7)
1271
+ container_cache[tostring(track)] = { false, fx_count }
1272
+ return nil
1273
+ end
1274
+
1275
+ -- Name the container "MCP Meters" for easy identification
1276
+ reaper.TrackFX_SetNamedConfigParm(track, idx, "renamed_name", "MCP Meters")
1277
+
1278
+ -- Verify container API works (confirms REAPER 7.06+)
1279
+ local ok = reaper.TrackFX_GetNamedConfigParm(track, idx, "container_count")
1280
+ if not ok then
1281
+ -- API not available — remove the container and fall back
1282
+ reaper.TrackFX_Delete(track, idx)
1283
+ container_cache[tostring(track)] = { false, fx_count }
1284
+ return nil
1285
+ end
1286
+
1287
+ -- Cache the new container (fx_count + 1 because we just added the container)
1288
+ container_cache[tostring(track)] = { idx, reaper.TrackFX_GetCount(track) }
1289
+ return idx
1290
+ end
1214
1291
 
1215
1292
  -- Helper: find or auto-insert a named JSFX on a track.
1216
- -- Returns the FX index (0-based) on success, or nil + error message on failure.
1293
+ -- Tries to place inside an "MCP Meters" FX Container (REAPER 7.06+).
1294
+ -- Falls back to direct insertion on older REAPER versions.
1295
+ -- Returns the FX index (possibly container-addressed) on success, or nil + error.
1217
1296
  local function ensure_jsfx_on_track(track, fx_name)
1297
+ -- Try container approach first (REAPER 7.06+)
1298
+ local container_idx = find_or_create_mcp_container(track)
1299
+ if container_idx then
1300
+ local ok, count_str = reaper.TrackFX_GetNamedConfigParm(track, container_idx, "container_count")
1301
+ local count = tonumber(count_str) or 0
1302
+
1303
+ -- Search inside container for existing JSFX
1304
+ for i = 0, count - 1 do
1305
+ local _, addr_str = reaper.TrackFX_GetNamedConfigParm(track, container_idx, "container_item." .. i)
1306
+ local addr = tonumber(addr_str)
1307
+ if addr then
1308
+ local _, name = reaper.TrackFX_GetFXName(track, addr)
1309
+ if name and name:find(fx_name, 1, true) then
1310
+ return addr -- Found inside container
1311
+ end
1312
+ end
1313
+ end
1314
+
1315
+ -- Not found — insert inside container
1316
+ -- Use legacy addressing: 0x2000000 + (1-based slot) * (top_chain_count + 1) + (1-based container pos)
1317
+ local top_count = reaper.TrackFX_GetCount(track)
1318
+ local slot = count + 1 -- 1-based, next empty slot
1319
+ local container_pos = container_idx + 1 -- 1-based
1320
+ local insert_addr = 0x2000000 + slot * (top_count + 1) + container_pos
1321
+ local new_idx = reaper.TrackFX_AddByName(track, fx_name, false, insert_addr)
1322
+ if new_idx >= 0 then
1323
+ -- Invalidate cache since FX count changed
1324
+ container_cache[tostring(track)] = nil
1325
+ return new_idx
1326
+ end
1327
+ -- Container insertion failed — fall through to direct
1328
+ end
1329
+
1330
+ -- Fallback: direct insertion (REAPER < 7.06 or container failed)
1218
1331
  local fx_count = reaper.TrackFX_GetCount(track)
1219
1332
  for i = 0, fx_count - 1 do
1220
1333
  local _, name = reaper.TrackFX_GetFXName(track, i)
@@ -1222,7 +1335,6 @@ local function ensure_jsfx_on_track(track, fx_name)
1222
1335
  return i
1223
1336
  end
1224
1337
  end
1225
- -- Auto-insert
1226
1338
  local idx = reaper.TrackFX_AddByName(track, fx_name, false, -1)
1227
1339
  if idx < 0 then
1228
1340
  return nil, fx_name .. " JSFX not found. Run 'reaper-mcp setup' to install it."
@@ -1240,11 +1352,14 @@ function handlers.read_track_lufs(params)
1240
1352
  local fx_idx, err = ensure_jsfx_on_track(track, MCP_LUFS_METER_FX_NAME)
1241
1353
  if not fx_idx then return nil, err end
1242
1354
 
1243
- -- Set the track_slot parameter (slider2, param index 1) so this instance
1244
- -- writes to a unique gmem offset and doesn't collide with other tracks
1245
- reaper.TrackFX_SetParam(track, fx_idx, 1, idx / 127)
1355
+ -- Set the track_slot parameter so this instance writes to a unique gmem offset
1356
+ local desired_slot = idx / 127
1357
+ local current_slot = reaper.TrackFX_GetParam(track, fx_idx, 1)
1358
+ if math.abs(current_slot - desired_slot) > 0.001 then
1359
+ reaper.TrackFX_SetParam(track, fx_idx, 1, desired_slot)
1360
+ end
1246
1361
 
1247
- -- Attach to the LUFS meter gmem namespace and read from track-specific offset
1362
+ -- Read from gmem (JSFX writes here from @sample)
1248
1363
  reaper.gmem_attach("MCPLufsMeter")
1249
1364
 
1250
1365
  local base = idx * 8
@@ -1277,6 +1392,7 @@ function handlers.read_track_correlation(params)
1277
1392
  local fx_idx, err = ensure_jsfx_on_track(track, MCP_CORRELATION_METER_FX_NAME)
1278
1393
  if not fx_idx then return nil, err end
1279
1394
 
1395
+ -- Read from gmem (JSFX writes here from @sample)
1280
1396
  reaper.gmem_attach("MCPCorrelationMeter")
1281
1397
 
1282
1398
  local correlation = reaper.gmem_read(0)
@@ -1303,6 +1419,7 @@ function handlers.read_track_crest(params)
1303
1419
  local fx_idx, err = ensure_jsfx_on_track(track, MCP_CREST_FACTOR_FX_NAME)
1304
1420
  if not fx_idx then return nil, err end
1305
1421
 
1422
+ -- Read from gmem (JSFX writes here from @sample)
1306
1423
  reaper.gmem_attach("MCPCrestFactor")
1307
1424
 
1308
1425
  local crest_factor = reaper.gmem_read(0)
@@ -2861,8 +2978,9 @@ function handlers.create_track_envelope(params)
2861
2978
  return nil, "Unknown envelope name: " .. params.envelopeName .. ". Use Volume, Pan, Mute, Width, or Trim Volume"
2862
2979
  end
2863
2980
  -- Get track chunk, insert envelope chunk if missing
2981
+ -- Use anchored pattern "<VOLENV\n" to avoid matching VOLENV2 (Trim Volume)
2864
2982
  local _, chunk = reaper.GetTrackStateChunk(track, "", false)
2865
- if not chunk:find(chunk_key) then
2983
+ if not chunk:find("<" .. chunk_key .. "\n") then
2866
2984
  -- Insert a minimal envelope chunk before the closing >
2867
2985
  local env_chunk = "\n<" .. chunk_key .. "\nACT 1 -1\nVIS 1 1 1\nLANEHEIGHT 0 0\nARM 0\nDEFSHAPE 0 -1 -1\n>\n"
2868
2986
  -- Use position capture to find the last ">" (closing the <TRACK block).
@@ -2874,8 +2992,8 @@ function handlers.create_track_envelope(params)
2874
2992
  reaper.SetTrackStateChunk(track, chunk, false)
2875
2993
  else
2876
2994
  -- Envelope exists in chunk but may be hidden; make it visible
2877
- chunk = chunk:gsub("(" .. chunk_key .. "[^\n]*\n)ACT 0", "%1ACT 1")
2878
- chunk = chunk:gsub("(" .. chunk_key .. "[^\n]*\n[^\n]*\n)VIS 0", "%1VIS 1")
2995
+ chunk = chunk:gsub("(<" .. chunk_key .. "[^\n]*\n)ACT 0", "%1ACT 1")
2996
+ chunk = chunk:gsub("(<" .. chunk_key .. "[^\n]*\n[^\n]*\n)VIS 0", "%1VIS 1")
2879
2997
  reaper.SetTrackStateChunk(track, chunk, false)
2880
2998
  end
2881
2999
  env = reaper.GetTrackEnvelopeByName(track, params.envelopeName)
@@ -58,7 +58,6 @@ slider1:window_ms=300<50,2000,10>Window (ms)
58
58
  gmem[3] = -150;
59
59
  );
60
60
 
61
- local(l, r, m, s);
62
61
  l = spl0;
63
62
  r = spl1;
64
63
 
@@ -66,7 +65,6 @@ slider1:window_ms=300<50,2000,10>Window (ms)
66
65
  m = (l + r) * 0.5;
67
66
  s = (l - r) * 0.5;
68
67
 
69
- local(ll, rr, lr, mm, ss);
70
68
  ll = l * l;
71
69
  rr = r * r;
72
70
  lr = l * r;
@@ -99,8 +97,6 @@ slider1:window_ms=300<50,2000,10>Window (ms)
99
97
 
100
98
  // Update gmem every 512 samples (~86 Hz at 44.1kHz)
101
99
  (buf_pos % 512) == 0 ? (
102
- local(denom, corr, mid_rms, side_rms, mid_db, side_db, width);
103
-
104
100
  // Correlation: sum(L*R) / sqrt(sum(L*L) * sum(R*R))
105
101
  denom = sqrt(sum_ll * sum_rr);
106
102
  denom > 0.0000001 ? (
@@ -49,7 +49,6 @@ slider2:peak_hold_ms=1000<100,10000,100>Peak Hold (ms)
49
49
  );
50
50
 
51
51
  // Mono mix for metering
52
- local(mono, sq, peak_abs);
53
52
  mono = (spl0 + spl1) * 0.5;
54
53
  sq = mono * mono;
55
54
  peak_abs = abs(mono);
@@ -76,8 +75,6 @@ slider2:peak_hold_ms=1000<100,10000,100>Peak Hold (ms)
76
75
 
77
76
  // Update gmem every 512 samples
78
77
  (buf_pos % 512) == 0 ? (
79
- local(rms_val, rms_db, peak_db, crest_db);
80
-
81
78
  rms_val = sqrt(sum_sq / buf_size);
82
79
 
83
80
  rms_val > 0.000001 ? (
@@ -21,29 +21,18 @@ slider2:track_slot=0<0,127,1>Track Slot
21
21
  // K-weighting filter state (two biquad stages per channel)
22
22
  // Stage 1: High-shelf pre-filter (+4 dB at 1681 Hz)
23
23
  // Stage 2: High-pass RLB filter (100 Hz, -12 dB/oct)
24
-
25
- // Biquad state: [b0, b1, b2, a1, a2, x1, x2, y1, y2] per stage per channel
26
- // We store state variables in flat arrays
27
24
  hs_x1l = hs_x2l = hs_y1l = hs_y2l = 0;
28
25
  hs_x1r = hs_x2r = hs_y1r = hs_y2r = 0;
29
26
  hp_x1l = hp_x2l = hp_y1l = hp_y2l = 0;
30
27
  hp_x1r = hp_x2r = hp_y1r = hp_y2r = 0;
31
28
 
32
- // High-shelf filter coefficients (pre-computed for 48kHz; recomputed @slider)
33
- // Using bilinear transform of analog prototype per BS.1770-4 Annex 1
29
+ // High-shelf filter coefficients (recomputed on init and sample rate change)
34
30
  hs_b0 = hs_b1 = hs_b2 = hs_a1 = hs_a2 = 0;
35
31
  hp_b0 = hp_b1 = hp_b2 = hp_a1 = hp_a2 = 0;
36
32
 
37
- // Circular buffer for loudness blocks (sliding windows)
38
- // 400ms block = momentary, 3s = short-term
39
- // BS.1770 uses 100ms overlapping blocks (75% overlap)
40
- // We approximate with per-sample accumulation into sliding windows
41
-
42
- // Ring buffer for per-sample squared weighted values
43
- // momentary: 400ms window
44
- // short-term: 3s window
45
- // We store up to 3s of samples in a circular buffer
46
- buf_size = 0; // computed at runtime from srate
33
+ // Ring buffer for per-sample squared weighted values (up to 3s)
34
+ // momentary: 400ms window, short-term: 3s window
35
+ buf_size = 0;
47
36
  buf_l = 65536; // start address for left channel ring buffer
48
37
  buf_r = 131072; // start address for right channel ring buffer
49
38
  buf_pos = 0;
@@ -63,13 +52,11 @@ slider2:track_slot=0<0,127,1>Track Slot
63
52
  // Measurement duration
64
53
  sample_count = 0;
65
54
 
66
- // Flags
67
55
  needs_init = 1;
68
56
 
69
57
  function compute_kweight_filters() (
70
58
  // High-shelf pre-filter: +4 dB shelf at f0 = 1681.974 Hz
71
59
  // From BS.1770-4, Annex 1, Table 1
72
- local(db, K, Vh, Vb, a0);
73
60
  db = 3.99984385397; // ~4 dB
74
61
  K = tan($pi * 1681.974 / srate);
75
62
  Vh = exp(db / 20 * log(10));
@@ -82,7 +69,6 @@ slider2:track_slot=0<0,127,1>Track Slot
82
69
  hs_a2 = (1 - K/0.7071067811865476 + K*K) / a0;
83
70
 
84
71
  // High-pass filter: 38.13547 Hz, Q = 0.5003270373238773
85
- local(K2, a0);
86
72
  K2 = tan($pi * 38.13547 / srate);
87
73
  a0 = K2*K2 + K2/0.5003270373238773 + 1;
88
74
  hp_b0 = 1 / a0;
@@ -93,7 +79,6 @@ slider2:track_slot=0<0,127,1>Track Slot
93
79
  );
94
80
 
95
81
  function reset_measurement() (
96
- local(base);
97
82
  base = floor(slider2 + 0.5) * 8;
98
83
  buf_pos = 0;
99
84
  momentary_sum_l = 0;
@@ -121,7 +106,7 @@ slider2:track_slot=0<0,127,1>Track Slot
121
106
  );
122
107
 
123
108
  @slider
124
- // Re-compute filter coefficients (in case sample rate has changed)
109
+ // Re-compute filter coefficients (handles sample rate changes)
125
110
  needs_init = 1;
126
111
 
127
112
  // Handle reset button
@@ -142,7 +127,6 @@ slider2:track_slot=0<0,127,1>Track Slot
142
127
  );
143
128
 
144
129
  // --- K-weighting: stage 1 (high-shelf) ---
145
- local(wl, wr, kl, kr);
146
130
  wl = spl0;
147
131
  wr = spl1;
148
132
 
@@ -155,7 +139,6 @@ slider2:track_slot=0<0,127,1>Track Slot
155
139
  hs_x2r = hs_x1r; hs_x1r = wr; hs_y2r = hs_y1r; hs_y1r = kr;
156
140
 
157
141
  // --- K-weighting: stage 2 (high-pass) ---
158
- local(fl, fr);
159
142
  fl = hp_b0 * kl + hp_b1 * hp_x1l + hp_b2 * hp_x2l - hp_a1 * hp_y1l - hp_a2 * hp_y2l;
160
143
  hp_x2l = hp_x1l; hp_x1l = kl; hp_y2l = hp_y1l; hp_y1l = fl;
161
144
 
@@ -163,13 +146,10 @@ slider2:track_slot=0<0,127,1>Track Slot
163
146
  hp_x2r = hp_x1r; hp_x1r = kr; hp_y2r = hp_y1r; hp_y1r = fr;
164
147
 
165
148
  // Squared weighted samples
166
- local(sq_l, sq_r);
167
149
  sq_l = fl * fl;
168
150
  sq_r = fr * fr;
169
151
 
170
152
  // --- Ring buffer update ---
171
- // Subtract outgoing samples from running sums
172
- local(old_l, old_r);
173
153
  old_l = buf_l[buf_pos];
174
154
  old_r = buf_r[buf_pos];
175
155
 
@@ -177,12 +157,7 @@ slider2:track_slot=0<0,127,1>Track Slot
177
157
  shortterm_sum_l -= old_l;
178
158
  shortterm_sum_r -= old_r;
179
159
 
180
- // Momentary window: 400ms = srate * 0.4 samples back
181
- // We subtract from momentary only when the sample leaving is within 400ms window
182
- // Simplified: maintain separate momentary accumulators via separate pointers
183
- // For efficiency, use a single ring buffer and recompute momentary from partial sum
184
- // Here we track momentary with a second set of pointers (400ms lag)
185
- local(mom_pos);
160
+ // Momentary window: 400ms lag pointer
186
161
  mom_pos = buf_pos - floor(srate * 0.4);
187
162
  mom_pos < 0 ? mom_pos += buf_size;
188
163
  momentary_sum_l -= buf_l[mom_pos];
@@ -203,18 +178,12 @@ slider2:track_slot=0<0,127,1>Track Slot
203
178
  buf_pos >= buf_size ? buf_pos = 0;
204
179
 
205
180
  // --- Integrated loudness (gated per BS.1770) ---
206
- // Absolute gate: only include 400ms blocks above -70 LUFS
207
- // Simplified: accumulate ungated for speed, apply gate check periodically
181
+ // Simplified: accumulate ungated, apply gate check periodically
208
182
  integrated_sum += sq_l + sq_r;
209
183
  integrated_count += 1;
210
184
 
211
185
  // --- True peak: 4x linear interpolation oversampling ---
212
- // Insert 3 interpolated samples between each real sample using linear interpolation
213
- local(prev_l, prev_r, frac, tp_l, tp_r);
214
-
215
- // We use the previous sample stored in a local state var
216
- // Stored in memory block above buf_r to avoid conflict
217
- // prev_l is at buf_r + buf_size, prev_r is at buf_r + buf_size + 1
186
+ // Previous sample stored above buf_r to avoid conflict
218
187
  prev_l = buf_r[buf_size];
219
188
  prev_r = buf_r[buf_size + 1];
220
189
 
@@ -224,7 +193,6 @@ slider2:track_slot=0<0,127,1>Track Slot
224
193
  // Check interpolated peaks at 1/4, 2/4, 3/4 offsets
225
194
  frac = 0.25;
226
195
  loop(3,
227
- local(interp_l, interp_r);
228
196
  interp_l = abs(prev_l + frac * (spl0 - prev_l));
229
197
  interp_r = abs(prev_r + frac * (spl1 - prev_r));
230
198
  interp_l > tp_l ? tp_l = interp_l;
@@ -243,9 +211,6 @@ slider2:track_slot=0<0,127,1>Track Slot
243
211
 
244
212
  // --- Update gmem every 4096 samples (approx 10x/second at 44.1kHz) ---
245
213
  (sample_count % 4096) == 0 ? (
246
- local(shortterm_mean, momentary_mean, integrated_lufs, shortterm_lufs, momentary_lufs);
247
- local(mom_samples, base);
248
-
249
214
  base = floor(slider2 + 0.5) * 8;
250
215
 
251
216
  mom_samples = floor(srate * 0.4);
@@ -255,8 +220,7 @@ slider2:track_slot=0<0,127,1>Track Slot
255
220
  shortterm_mean = (shortterm_sum_l + shortterm_sum_r) / (buf_size * 2);
256
221
  momentary_mean = (momentary_sum_l + momentary_sum_r) / (mom_samples * 2);
257
222
 
258
- // Integrated mean square (two channels: L + R sum, divided by count*2)
259
- local(integrated_mean);
223
+ // Integrated mean square (two channels)
260
224
  integrated_mean = integrated_count > 0 ? (integrated_sum / (integrated_count * 2)) : 0;
261
225
 
262
226
  // LUFS = -0.691 + 10 * log10(mean_square)
@@ -274,7 +238,6 @@ slider2:track_slot=0<0,127,1>Track Slot
274
238
 
275
239
  integrated_mean > 0 ? (
276
240
  // Apply absolute gate at -70 LUFS
277
- local(ungated_lufs);
278
241
  ungated_lufs = -0.691 + 10 * log10(integrated_mean);
279
242
  ungated_lufs > -70 ? (
280
243
  integrated_lufs = ungated_lufs;