@mthines/reaper-mcp 0.14.0 → 0.14.2-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 +219 -33
- package/package.json +2 -1
- package/reaper/mcp_bridge.lua +146 -28
- package/reaper/mcp_correlation_meter.jsfx +0 -4
- package/reaper/mcp_crest_factor.jsfx +0 -3
- package/reaper/mcp_lufs_meter.jsfx +9 -46
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
|
|
2206
|
-
import { join as
|
|
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
|
|
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
|
-
|
|
2570
|
+
mkdirSync3(scriptsDir, { recursive: true });
|
|
2403
2571
|
const reaperDir = resolveAssetDir(__dirname, "reaper");
|
|
2404
|
-
const luaSrc =
|
|
2405
|
-
const luaDest =
|
|
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 =
|
|
2413
|
-
|
|
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 =
|
|
2418
|
-
const dest =
|
|
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 ?
|
|
2442
|
-
const claudeDir = isGlobal ? baseDir :
|
|
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 (
|
|
2445
|
-
const dest =
|
|
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",
|
|
2452
|
-
if (
|
|
2453
|
-
const dest =
|
|
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",
|
|
2460
|
-
if (
|
|
2461
|
-
const dest =
|
|
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",
|
|
2468
|
-
if (
|
|
2469
|
-
const dest =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2507
|
-
const localAgents =
|
|
2508
|
-
const globalAgents =
|
|
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 =
|
|
2516
|
-
const globalKnowledge =
|
|
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 =
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.14.2-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",
|
package/reaper/mcp_bridge.lua
CHANGED
|
@@ -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
|
-
--
|
|
588
|
-
local analyzer_idx =
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
1004
|
-
|
|
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
|
-
--
|
|
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
|
|
1244
|
-
|
|
1245
|
-
reaper.
|
|
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
|
-
--
|
|
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 (
|
|
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
|
-
//
|
|
38
|
-
// 400ms
|
|
39
|
-
|
|
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 (
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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;
|