@mthines/reaper-mcp 0.19.0-beta.20.2 → 0.19.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/main.js +634 -97
- package/package.json +1 -1
- package/reaper/mcp_bridge.lua +186 -1
package/main.js
CHANGED
|
@@ -27,10 +27,10 @@ var SCOPE_NAME = "reaper-mcp-server";
|
|
|
27
27
|
function readServiceVersion() {
|
|
28
28
|
try {
|
|
29
29
|
const require2 = createRequire(import.meta.url);
|
|
30
|
-
const
|
|
30
|
+
const __dirname3 = dirname(fileURLToPath(import.meta.url));
|
|
31
31
|
const pkgPaths = [
|
|
32
|
-
join(
|
|
33
|
-
join(
|
|
32
|
+
join(__dirname3, "..", "package.json"),
|
|
33
|
+
join(__dirname3, "package.json")
|
|
34
34
|
];
|
|
35
35
|
for (const p of pkgPaths) {
|
|
36
36
|
try {
|
|
@@ -2232,6 +2232,288 @@ function registerCategoryTools(server) {
|
|
|
2232
2232
|
);
|
|
2233
2233
|
}
|
|
2234
2234
|
|
|
2235
|
+
// apps/reaper-mcp-server/src/tools/aesthetics.ts
|
|
2236
|
+
import { z as z17 } from "zod/v4";
|
|
2237
|
+
import { unlink as unlink2 } from "node:fs/promises";
|
|
2238
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2239
|
+
|
|
2240
|
+
// apps/reaper-mcp-server/src/sidecar.ts
|
|
2241
|
+
import { spawn } from "node:child_process";
|
|
2242
|
+
import { existsSync } from "node:fs";
|
|
2243
|
+
import { join as join3 } from "node:path";
|
|
2244
|
+
import { homedir as homedir2, platform as platform2 } from "node:os";
|
|
2245
|
+
var VENV_BIN = platform2() === "win32" ? "Scripts" : "bin";
|
|
2246
|
+
var VENV_PYTHON_NAME = platform2() === "win32" ? "python.exe" : "python";
|
|
2247
|
+
var SIDECAR_VENV_PATH = join3(homedir2(), ".reaper-mcp", "sidecar-venv");
|
|
2248
|
+
var SIDECAR_SCRIPT_PATH = join3(homedir2(), ".reaper-mcp", "sidecar", "server.py");
|
|
2249
|
+
var VENV_PYTHON = join3(SIDECAR_VENV_PATH, VENV_BIN, VENV_PYTHON_NAME);
|
|
2250
|
+
var _rawTimeout = Number(process.env["REAPER_MCP_SIDECAR_TIMEOUT_MS"]);
|
|
2251
|
+
var ANALYZE_TIMEOUT_MS = Number.isFinite(_rawTimeout) && _rawTimeout > 0 ? _rawTimeout : 6e4;
|
|
2252
|
+
var SidecarClientImpl = class {
|
|
2253
|
+
process = null;
|
|
2254
|
+
pending = /* @__PURE__ */ new Map();
|
|
2255
|
+
nextId = 1;
|
|
2256
|
+
lineBuffer = "";
|
|
2257
|
+
restarting = false;
|
|
2258
|
+
restartAttempted = false;
|
|
2259
|
+
/** Cached spawn promise — prevents double-spawn on concurrent cold-start calls. */
|
|
2260
|
+
spawnPromise = null;
|
|
2261
|
+
isAvailable() {
|
|
2262
|
+
return existsSync(SIDECAR_SCRIPT_PATH) && existsSync(VENV_PYTHON);
|
|
2263
|
+
}
|
|
2264
|
+
async analyze(wavPath, startTime, endTime) {
|
|
2265
|
+
if (!this.isAvailable()) {
|
|
2266
|
+
throw new Error(
|
|
2267
|
+
"Audio understanding sidecar not installed. Run: node dist/apps/reaper-mcp-server/main.js setup-sidecar"
|
|
2268
|
+
);
|
|
2269
|
+
}
|
|
2270
|
+
await this.ensureRunning();
|
|
2271
|
+
return new Promise((resolve, reject) => {
|
|
2272
|
+
const id = this.nextId++;
|
|
2273
|
+
const request = {
|
|
2274
|
+
jsonrpc: "2.0",
|
|
2275
|
+
id,
|
|
2276
|
+
method: "analyze",
|
|
2277
|
+
params: { path: wavPath, startTime, endTime }
|
|
2278
|
+
};
|
|
2279
|
+
const timer = setTimeout(() => {
|
|
2280
|
+
if (this.pending.delete(id)) {
|
|
2281
|
+
process.stderr.write(
|
|
2282
|
+
`[reaper-mcp] Sidecar analyze() timed out after ${ANALYZE_TIMEOUT_MS}ms \u2014 killing process
|
|
2283
|
+
`
|
|
2284
|
+
);
|
|
2285
|
+
if (this.process) {
|
|
2286
|
+
this.process.kill("SIGKILL");
|
|
2287
|
+
this.process = null;
|
|
2288
|
+
}
|
|
2289
|
+
reject(new Error(
|
|
2290
|
+
`Sidecar timeout after ${ANALYZE_TIMEOUT_MS}ms. Override with REAPER_MCP_SIDECAR_TIMEOUT_MS env var.`
|
|
2291
|
+
));
|
|
2292
|
+
}
|
|
2293
|
+
}, ANALYZE_TIMEOUT_MS);
|
|
2294
|
+
this.pending.set(id, {
|
|
2295
|
+
resolve: (r) => {
|
|
2296
|
+
clearTimeout(timer);
|
|
2297
|
+
resolve(r);
|
|
2298
|
+
},
|
|
2299
|
+
reject: (e) => {
|
|
2300
|
+
clearTimeout(timer);
|
|
2301
|
+
reject(e);
|
|
2302
|
+
}
|
|
2303
|
+
});
|
|
2304
|
+
const line = JSON.stringify(request) + "\n";
|
|
2305
|
+
try {
|
|
2306
|
+
const proc = this.process;
|
|
2307
|
+
if (!proc || !proc.stdin) {
|
|
2308
|
+
throw new Error("Sidecar process not running");
|
|
2309
|
+
}
|
|
2310
|
+
proc.stdin.write(line);
|
|
2311
|
+
} catch (err) {
|
|
2312
|
+
clearTimeout(timer);
|
|
2313
|
+
this.pending.delete(id);
|
|
2314
|
+
reject(new Error(`Failed to write to sidecar stdin: ${err instanceof Error ? err.message : String(err)}`));
|
|
2315
|
+
}
|
|
2316
|
+
});
|
|
2317
|
+
}
|
|
2318
|
+
shutdown() {
|
|
2319
|
+
if (this.process) {
|
|
2320
|
+
this.process.kill("SIGTERM");
|
|
2321
|
+
this.process = null;
|
|
2322
|
+
}
|
|
2323
|
+
for (const [id, { reject }] of this.pending.entries()) {
|
|
2324
|
+
reject(new Error("Sidecar shut down"));
|
|
2325
|
+
this.pending.delete(id);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
// -------------------------------------------------------------------------
|
|
2329
|
+
// Private
|
|
2330
|
+
// -------------------------------------------------------------------------
|
|
2331
|
+
async ensureRunning() {
|
|
2332
|
+
if (this.process && !this.process.killed) return;
|
|
2333
|
+
if (!this.spawnPromise) {
|
|
2334
|
+
this.spawnPromise = this.spawn().finally(() => {
|
|
2335
|
+
this.spawnPromise = null;
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2338
|
+
await this.spawnPromise;
|
|
2339
|
+
}
|
|
2340
|
+
async spawn() {
|
|
2341
|
+
const child = spawn(VENV_PYTHON, [SIDECAR_SCRIPT_PATH], {
|
|
2342
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2343
|
+
});
|
|
2344
|
+
if (child.stdout) {
|
|
2345
|
+
child.stdout.setEncoding("utf8");
|
|
2346
|
+
child.stdout.on("data", (chunk) => this.onStdout(chunk));
|
|
2347
|
+
}
|
|
2348
|
+
if (child.stderr) {
|
|
2349
|
+
child.stderr.setEncoding("utf8");
|
|
2350
|
+
child.stderr.on("data", (data) => {
|
|
2351
|
+
process.stderr.write(`[reaper-mcp-sidecar] ${data}`);
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
2354
|
+
child.on("exit", (code) => {
|
|
2355
|
+
process.stderr.write(`[reaper-mcp] Sidecar process exited (code ${code})
|
|
2356
|
+
`);
|
|
2357
|
+
this.process = null;
|
|
2358
|
+
this.handleProcessDeath();
|
|
2359
|
+
});
|
|
2360
|
+
child.on("error", (err) => {
|
|
2361
|
+
process.stderr.write(`[reaper-mcp] Sidecar spawn error: ${err.message}
|
|
2362
|
+
`);
|
|
2363
|
+
this.process = null;
|
|
2364
|
+
this.handleProcessDeath();
|
|
2365
|
+
});
|
|
2366
|
+
this.process = child;
|
|
2367
|
+
this.lineBuffer = "";
|
|
2368
|
+
this.restarting = false;
|
|
2369
|
+
this.restartAttempted = false;
|
|
2370
|
+
}
|
|
2371
|
+
onStdout(chunk) {
|
|
2372
|
+
this.lineBuffer += chunk;
|
|
2373
|
+
let newlineIdx;
|
|
2374
|
+
while ((newlineIdx = this.lineBuffer.indexOf("\n")) !== -1) {
|
|
2375
|
+
const line = this.lineBuffer.slice(0, newlineIdx).trim();
|
|
2376
|
+
this.lineBuffer = this.lineBuffer.slice(newlineIdx + 1);
|
|
2377
|
+
if (line) this.onResponse(line);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
onResponse(line) {
|
|
2381
|
+
let response;
|
|
2382
|
+
try {
|
|
2383
|
+
response = JSON.parse(line);
|
|
2384
|
+
} catch {
|
|
2385
|
+
process.stderr.write(`[reaper-mcp] Sidecar returned malformed JSON: ${line}
|
|
2386
|
+
`);
|
|
2387
|
+
for (const [id, { reject }] of this.pending.entries()) {
|
|
2388
|
+
reject(new Error(`Sidecar returned malformed JSON: ${line}`));
|
|
2389
|
+
this.pending.delete(id);
|
|
2390
|
+
}
|
|
2391
|
+
return;
|
|
2392
|
+
}
|
|
2393
|
+
const pending = this.pending.get(response.id);
|
|
2394
|
+
if (!pending) {
|
|
2395
|
+
process.stderr.write(`[reaper-mcp] Sidecar response for unknown id ${response.id}
|
|
2396
|
+
`);
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
this.pending.delete(response.id);
|
|
2400
|
+
if (response.error) {
|
|
2401
|
+
pending.reject(new Error(response.error.message));
|
|
2402
|
+
} else if (response.result) {
|
|
2403
|
+
pending.resolve(response.result);
|
|
2404
|
+
} else {
|
|
2405
|
+
pending.reject(new Error("Sidecar response missing both result and error fields"));
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
handleProcessDeath() {
|
|
2409
|
+
if (this.restarting || this.pending.size === 0) {
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
if (!this.restartAttempted) {
|
|
2413
|
+
this.restarting = true;
|
|
2414
|
+
this.restartAttempted = true;
|
|
2415
|
+
process.stderr.write("[reaper-mcp] Sidecar crashed; attempting one restart...\n");
|
|
2416
|
+
this.spawn().then(() => {
|
|
2417
|
+
for (const [id, { reject }] of this.pending.entries()) {
|
|
2418
|
+
reject(new Error("Sidecar restarted; please retry the operation"));
|
|
2419
|
+
this.pending.delete(id);
|
|
2420
|
+
}
|
|
2421
|
+
}).catch((err) => {
|
|
2422
|
+
for (const [id, { reject }] of this.pending.entries()) {
|
|
2423
|
+
reject(new Error(`Sidecar restart failed: ${err.message}. Run: node dist/apps/reaper-mcp-server/main.js setup-sidecar`));
|
|
2424
|
+
this.pending.delete(id);
|
|
2425
|
+
}
|
|
2426
|
+
});
|
|
2427
|
+
} else {
|
|
2428
|
+
for (const [id, { reject }] of this.pending.entries()) {
|
|
2429
|
+
reject(new Error("Python sidecar unavailable after restart attempt. Run: node dist/apps/reaper-mcp-server/main.js setup-sidecar"));
|
|
2430
|
+
this.pending.delete(id);
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
};
|
|
2435
|
+
var _singleton = null;
|
|
2436
|
+
function getSidecarClient() {
|
|
2437
|
+
if (!_singleton) {
|
|
2438
|
+
_singleton = new SidecarClientImpl();
|
|
2439
|
+
}
|
|
2440
|
+
return _singleton;
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
// apps/reaper-mcp-server/src/tools/aesthetics.ts
|
|
2444
|
+
var SIDECAR_NOT_INSTALLED_MSG = "Audio understanding sidecar not installed. Run: node dist/apps/reaper-mcp-server/main.js setup-sidecar";
|
|
2445
|
+
function registerAestheticsTools(server) {
|
|
2446
|
+
server.tool(
|
|
2447
|
+
"analyze_track_aesthetics",
|
|
2448
|
+
"Analyze the perceptual aesthetic quality of a track's audio using Meta's Audiobox Aesthetics model (CC-BY 4.0). Bounces the track to a temp WAV file (post-FX, post-fader) and runs 4-axis perceptual quality scoring: Production Quality (PQ), Production Complexity (PC), Content Enjoyment (CE), Content Usefulness (CU). Returns scores 0-10. Requires the Python sidecar (run: node dist/apps/reaper-mcp-server/main.js setup-sidecar).",
|
|
2449
|
+
{
|
|
2450
|
+
trackIndex: z17.coerce.number().int().min(0).describe("Zero-based track index"),
|
|
2451
|
+
startTime: z17.coerce.number().min(0).optional().describe("Start time in seconds (default: 0 or use REAPER time selection)"),
|
|
2452
|
+
endTime: z17.coerce.number().min(0).optional().describe("End time in seconds (default: startTime + durationSeconds)"),
|
|
2453
|
+
durationSeconds: z17.coerce.number().min(0.5).max(30).optional().default(5).describe("Duration to analyze in seconds (default 5, max 30). Ignored if endTime is provided.")
|
|
2454
|
+
},
|
|
2455
|
+
async ({ trackIndex, startTime, endTime, durationSeconds }) => {
|
|
2456
|
+
const sidecar = getSidecarClient();
|
|
2457
|
+
if (!sidecar.isAvailable()) {
|
|
2458
|
+
return {
|
|
2459
|
+
content: [{ type: "text", text: SIDECAR_NOT_INSTALLED_MSG }],
|
|
2460
|
+
isError: true
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
const resolvedStart = startTime ?? 0;
|
|
2464
|
+
const resolvedEnd = endTime ?? resolvedStart + (durationSeconds ?? 5);
|
|
2465
|
+
if (resolvedEnd <= resolvedStart) {
|
|
2466
|
+
return {
|
|
2467
|
+
content: [{ type: "text", text: "endTime must be greater than startTime" }],
|
|
2468
|
+
isError: true
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
const commandId = randomUUID2();
|
|
2472
|
+
const renderRes = await sendCommand("render_track_to_wav", {
|
|
2473
|
+
trackIndex,
|
|
2474
|
+
startTime: resolvedStart,
|
|
2475
|
+
endTime: resolvedEnd,
|
|
2476
|
+
commandId
|
|
2477
|
+
});
|
|
2478
|
+
if (!renderRes.success) {
|
|
2479
|
+
return {
|
|
2480
|
+
content: [{ type: "text", text: `Render failed: ${renderRes.error}` }],
|
|
2481
|
+
isError: true
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
const renderData = renderRes.data;
|
|
2485
|
+
const wavPath = renderData.wavPath;
|
|
2486
|
+
try {
|
|
2487
|
+
const scores = await sidecar.analyze(wavPath, resolvedStart, resolvedEnd);
|
|
2488
|
+
const result = {
|
|
2489
|
+
trackIndex,
|
|
2490
|
+
trackName: renderData.trackName,
|
|
2491
|
+
productionQuality: scores.PQ,
|
|
2492
|
+
productionComplexity: scores.PC,
|
|
2493
|
+
contentEnjoyment: scores.CE,
|
|
2494
|
+
contentUsefulness: scores.CU,
|
|
2495
|
+
startTime: resolvedStart,
|
|
2496
|
+
endTime: resolvedEnd,
|
|
2497
|
+
durationSeconds: resolvedEnd - resolvedStart,
|
|
2498
|
+
modelVersion: scores.modelVersion
|
|
2499
|
+
};
|
|
2500
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
2501
|
+
} catch (err) {
|
|
2502
|
+
return {
|
|
2503
|
+
content: [{
|
|
2504
|
+
type: "text",
|
|
2505
|
+
text: err instanceof Error ? err.message : String(err)
|
|
2506
|
+
}],
|
|
2507
|
+
isError: true
|
|
2508
|
+
};
|
|
2509
|
+
} finally {
|
|
2510
|
+
await unlink2(wavPath).catch(() => {
|
|
2511
|
+
});
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
);
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2235
2517
|
// apps/reaper-mcp-server/src/server.ts
|
|
2236
2518
|
function instrumentToolHandlers(server) {
|
|
2237
2519
|
const originalTool = server.tool.bind(server);
|
|
@@ -2291,40 +2573,43 @@ function createServer() {
|
|
|
2291
2573
|
registerEnvelopeTools(server);
|
|
2292
2574
|
registerBatchTools(server);
|
|
2293
2575
|
registerCategoryTools(server);
|
|
2576
|
+
registerAestheticsTools(server);
|
|
2294
2577
|
return server;
|
|
2295
2578
|
}
|
|
2296
2579
|
|
|
2297
2580
|
// apps/reaper-mcp-server/src/main.ts
|
|
2298
|
-
import { existsSync as
|
|
2299
|
-
import { join as
|
|
2300
|
-
import { fileURLToPath as
|
|
2301
|
-
import { homedir as
|
|
2581
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4 } from "node:fs";
|
|
2582
|
+
import { join as join7, dirname as dirname3 } from "node:path";
|
|
2583
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2584
|
+
import { homedir as homedir5, platform as platform4 } from "node:os";
|
|
2585
|
+
import { exec as execCb } from "node:child_process";
|
|
2586
|
+
import { promisify as promisifyUtil } from "node:util";
|
|
2302
2587
|
|
|
2303
2588
|
// apps/reaper-mcp-server/src/cli.ts
|
|
2304
|
-
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2305
|
-
import { join as
|
|
2589
|
+
import { copyFileSync, existsSync as existsSync2, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2590
|
+
import { join as join4 } from "node:path";
|
|
2306
2591
|
function resolveAssetDir(baseDir, name) {
|
|
2307
|
-
const sibling =
|
|
2308
|
-
if (
|
|
2309
|
-
const parent =
|
|
2310
|
-
if (
|
|
2592
|
+
const sibling = join4(baseDir, name);
|
|
2593
|
+
if (existsSync2(sibling)) return sibling;
|
|
2594
|
+
const parent = join4(baseDir, "..", name);
|
|
2595
|
+
if (existsSync2(parent)) return parent;
|
|
2311
2596
|
let dir = baseDir;
|
|
2312
2597
|
for (let i = 0; i < 5; i++) {
|
|
2313
|
-
const candidate =
|
|
2314
|
-
if (
|
|
2315
|
-
const up =
|
|
2598
|
+
const candidate = join4(dir, name);
|
|
2599
|
+
if (existsSync2(candidate)) return candidate;
|
|
2600
|
+
const up = join4(dir, "..");
|
|
2316
2601
|
if (up === dir) break;
|
|
2317
2602
|
dir = up;
|
|
2318
2603
|
}
|
|
2319
2604
|
return sibling;
|
|
2320
2605
|
}
|
|
2321
2606
|
function copyDirSync(src, dest) {
|
|
2322
|
-
if (!
|
|
2607
|
+
if (!existsSync2(src)) return 0;
|
|
2323
2608
|
mkdirSync(dest, { recursive: true });
|
|
2324
2609
|
let count = 0;
|
|
2325
2610
|
for (const entry of readdirSync(src)) {
|
|
2326
|
-
const srcPath =
|
|
2327
|
-
const destPath =
|
|
2611
|
+
const srcPath = join4(src, entry);
|
|
2612
|
+
const destPath = join4(dest, entry);
|
|
2328
2613
|
if (statSync(srcPath).isDirectory()) {
|
|
2329
2614
|
count += copyDirSync(srcPath, destPath);
|
|
2330
2615
|
} else {
|
|
@@ -2335,14 +2620,14 @@ function copyDirSync(src, dest) {
|
|
|
2335
2620
|
return count;
|
|
2336
2621
|
}
|
|
2337
2622
|
function installFile(src, dest) {
|
|
2338
|
-
if (
|
|
2623
|
+
if (existsSync2(src)) {
|
|
2339
2624
|
copyFileSync(src, dest);
|
|
2340
2625
|
return true;
|
|
2341
2626
|
}
|
|
2342
2627
|
return false;
|
|
2343
2628
|
}
|
|
2344
2629
|
function createMcpJson(targetPath) {
|
|
2345
|
-
if (
|
|
2630
|
+
if (existsSync2(targetPath)) return false;
|
|
2346
2631
|
const config = JSON.stringify({
|
|
2347
2632
|
mcpServers: {
|
|
2348
2633
|
reaper: {
|
|
@@ -2469,12 +2754,16 @@ var MCP_TOOL_NAMES = [
|
|
|
2469
2754
|
// progressive discovery
|
|
2470
2755
|
"list_tool_categories",
|
|
2471
2756
|
"enable_tool_category",
|
|
2472
|
-
"disable_tool_category"
|
|
2757
|
+
"disable_tool_category",
|
|
2758
|
+
// semantic audio analysis (requires Python sidecar — opt-in)
|
|
2759
|
+
"analyze_track_aesthetics"
|
|
2760
|
+
// Note: 'render_track_to_wav' is an internal Lua bridge command, NOT a public MCP tool.
|
|
2761
|
+
// It must NOT be listed here — MCP_TOOL_NAMES drives the Claude Code allowlist.
|
|
2473
2762
|
];
|
|
2474
2763
|
function ensureClaudeSettings(settingsPath) {
|
|
2475
2764
|
const allowList = MCP_TOOL_NAMES.map((t) => `mcp__reaper__${t}`);
|
|
2476
|
-
if (!
|
|
2477
|
-
mkdirSync(
|
|
2765
|
+
if (!existsSync2(settingsPath)) {
|
|
2766
|
+
mkdirSync(join4(settingsPath, ".."), { recursive: true });
|
|
2478
2767
|
const config = { permissions: { allow: allowList } };
|
|
2479
2768
|
writeFileSync(settingsPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2480
2769
|
return "created";
|
|
@@ -2491,21 +2780,21 @@ function ensureClaudeSettings(settingsPath) {
|
|
|
2491
2780
|
}
|
|
2492
2781
|
function resolveAssetDirWithFallback(baseDir, buildName, sourceName) {
|
|
2493
2782
|
const resolved = resolveAssetDir(baseDir, buildName);
|
|
2494
|
-
if (
|
|
2783
|
+
if (existsSync2(resolved)) return resolved;
|
|
2495
2784
|
return resolveAssetDir(baseDir, sourceName);
|
|
2496
2785
|
}
|
|
2497
2786
|
|
|
2498
2787
|
// apps/reaper-mcp-server/src/init.ts
|
|
2499
2788
|
import { checkbox, select } from "@inquirer/prompts";
|
|
2500
|
-
import { existsSync as
|
|
2501
|
-
import { join as
|
|
2502
|
-
import { homedir as
|
|
2789
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "node:fs";
|
|
2790
|
+
import { join as join5 } from "node:path";
|
|
2791
|
+
import { homedir as homedir3 } from "node:os";
|
|
2503
2792
|
async function runInit(opts, dirResolver) {
|
|
2504
2793
|
const isTTY = Boolean(process.stdin.isTTY);
|
|
2505
2794
|
const headless = opts.yes || !isTTY;
|
|
2506
2795
|
console.log("REAPER MCP \u2014 Interactive Setup\n");
|
|
2507
2796
|
const bridgeDir = await ensureBridgeDir();
|
|
2508
|
-
console.log(`REAPER resource path: ${
|
|
2797
|
+
console.log(`REAPER resource path: ${join5(bridgeDir, "..", "..")}
|
|
2509
2798
|
`);
|
|
2510
2799
|
let selections;
|
|
2511
2800
|
if (headless) {
|
|
@@ -2550,27 +2839,27 @@ async function runInit(opts, dirResolver) {
|
|
|
2550
2839
|
skillsScope
|
|
2551
2840
|
};
|
|
2552
2841
|
}
|
|
2553
|
-
const
|
|
2842
|
+
const __dirname3 = dirResolver();
|
|
2554
2843
|
if (selections.bridge) {
|
|
2555
2844
|
console.log("Installing REAPER Bridge...");
|
|
2556
2845
|
const scriptsDir = getReaperScriptsPath();
|
|
2557
2846
|
mkdirSync2(scriptsDir, { recursive: true });
|
|
2558
|
-
const reaperDir = resolveAssetDir(
|
|
2847
|
+
const reaperDir = resolveAssetDir(__dirname3, "reaper");
|
|
2559
2848
|
for (const luaFile of ["mcp_bridge.lua", "mcp_snapshot_manager.lua"]) {
|
|
2560
|
-
const src =
|
|
2561
|
-
const dest =
|
|
2849
|
+
const src = join5(reaperDir, luaFile);
|
|
2850
|
+
const dest = join5(scriptsDir, luaFile);
|
|
2562
2851
|
if (installFile(src, dest)) {
|
|
2563
2852
|
console.log(` Installed: ${luaFile}`);
|
|
2564
2853
|
} else {
|
|
2565
2854
|
console.log(` Not found: ${src}`);
|
|
2566
2855
|
}
|
|
2567
2856
|
}
|
|
2568
|
-
const effectsDir =
|
|
2857
|
+
const effectsDir = join5(getReaperEffectsPath(), "reaper-mcp");
|
|
2569
2858
|
mkdirSync2(effectsDir, { recursive: true });
|
|
2570
2859
|
for (const asset of REAPER_ASSETS) {
|
|
2571
2860
|
if (asset.endsWith(".lua")) continue;
|
|
2572
|
-
const src =
|
|
2573
|
-
const dest =
|
|
2861
|
+
const src = join5(reaperDir, asset);
|
|
2862
|
+
const dest = join5(effectsDir, asset);
|
|
2574
2863
|
if (installFile(src, dest)) {
|
|
2575
2864
|
console.log(` Installed: reaper-mcp/${asset}`);
|
|
2576
2865
|
} else {
|
|
@@ -2581,32 +2870,32 @@ async function runInit(opts, dirResolver) {
|
|
|
2581
2870
|
}
|
|
2582
2871
|
if (selections.skills) {
|
|
2583
2872
|
const isGlobal = selections.skillsScope === "global";
|
|
2584
|
-
const baseDir = isGlobal ?
|
|
2585
|
-
const claudeDir = isGlobal ? baseDir :
|
|
2873
|
+
const baseDir = isGlobal ? join5(homedir3(), ".claude") : process.cwd();
|
|
2874
|
+
const claudeDir = isGlobal ? baseDir : join5(baseDir, ".claude");
|
|
2586
2875
|
console.log(`Installing AI Skills & Agents (${selections.skillsScope})...`);
|
|
2587
|
-
const knowledgeSrc = resolveAssetDir(
|
|
2588
|
-
if (
|
|
2589
|
-
const dest =
|
|
2876
|
+
const knowledgeSrc = resolveAssetDir(__dirname3, "knowledge");
|
|
2877
|
+
if (existsSync3(knowledgeSrc)) {
|
|
2878
|
+
const dest = join5(isGlobal ? join5(homedir3(), ".claude") : process.cwd(), "knowledge");
|
|
2590
2879
|
const count = copyDirSync(knowledgeSrc, dest);
|
|
2591
2880
|
console.log(` Installed knowledge base: ${count} files`);
|
|
2592
2881
|
} else {
|
|
2593
2882
|
console.log(" Knowledge base not found in package. Skipping.");
|
|
2594
2883
|
}
|
|
2595
|
-
const rulesSrc = resolveAssetDirWithFallback(
|
|
2596
|
-
if (
|
|
2597
|
-
const dest =
|
|
2884
|
+
const rulesSrc = resolveAssetDirWithFallback(__dirname3, "claude-rules", join5(".claude", "rules"));
|
|
2885
|
+
if (existsSync3(rulesSrc)) {
|
|
2886
|
+
const dest = join5(claudeDir, "rules");
|
|
2598
2887
|
const count = copyDirSync(rulesSrc, dest);
|
|
2599
2888
|
console.log(` Installed Claude rules: ${count} files`);
|
|
2600
2889
|
}
|
|
2601
|
-
const skillsSrc = resolveAssetDirWithFallback(
|
|
2602
|
-
if (
|
|
2603
|
-
const dest =
|
|
2890
|
+
const skillsSrc = resolveAssetDirWithFallback(__dirname3, "claude-skills", join5(".claude", "skills"));
|
|
2891
|
+
if (existsSync3(skillsSrc)) {
|
|
2892
|
+
const dest = join5(claudeDir, "skills");
|
|
2604
2893
|
const count = copyDirSync(skillsSrc, dest);
|
|
2605
2894
|
console.log(` Installed Claude skills: ${count} files`);
|
|
2606
2895
|
}
|
|
2607
|
-
const agentsSrc = resolveAssetDirWithFallback(
|
|
2608
|
-
if (
|
|
2609
|
-
const dest =
|
|
2896
|
+
const agentsSrc = resolveAssetDirWithFallback(__dirname3, "claude-agents", join5(".claude", "agents"));
|
|
2897
|
+
if (existsSync3(agentsSrc)) {
|
|
2898
|
+
const dest = join5(claudeDir, "agents");
|
|
2610
2899
|
const count = copyDirSync(agentsSrc, dest);
|
|
2611
2900
|
console.log(` Installed Claude agents: ${count} files`);
|
|
2612
2901
|
}
|
|
@@ -2614,8 +2903,8 @@ async function runInit(opts, dirResolver) {
|
|
|
2614
2903
|
}
|
|
2615
2904
|
if (selections.settings) {
|
|
2616
2905
|
console.log("Configuring Claude Code Settings...");
|
|
2617
|
-
const settingsDir =
|
|
2618
|
-
const settingsPath =
|
|
2906
|
+
const settingsDir = join5(homedir3(), ".claude");
|
|
2907
|
+
const settingsPath = join5(settingsDir, "settings.json");
|
|
2619
2908
|
const result = ensureClaudeSettings(settingsPath);
|
|
2620
2909
|
if (result === "created") {
|
|
2621
2910
|
console.log(` Created: ${settingsPath}`);
|
|
@@ -2628,7 +2917,7 @@ async function runInit(opts, dirResolver) {
|
|
|
2628
2917
|
}
|
|
2629
2918
|
if (selections.projectConfig) {
|
|
2630
2919
|
console.log("Creating Project Config...");
|
|
2631
|
-
const mcpJsonPath =
|
|
2920
|
+
const mcpJsonPath = join5(process.cwd(), ".mcp.json");
|
|
2632
2921
|
if (createMcpJson(mcpJsonPath)) {
|
|
2633
2922
|
console.log(` Created: ${mcpJsonPath}`);
|
|
2634
2923
|
} else {
|
|
@@ -2639,18 +2928,18 @@ async function runInit(opts, dirResolver) {
|
|
|
2639
2928
|
console.log("Running system check...");
|
|
2640
2929
|
const bridgeRunning = await isBridgeRunning();
|
|
2641
2930
|
console.log(` Lua bridge: ${bridgeRunning ? "Connected" : "Not detected (start after REAPER is open)"}`);
|
|
2642
|
-
const globalClaudeDir =
|
|
2643
|
-
const localAgents =
|
|
2644
|
-
const globalAgents =
|
|
2931
|
+
const globalClaudeDir = join5(homedir3(), ".claude");
|
|
2932
|
+
const localAgents = existsSync3(join5(process.cwd(), ".claude", "agents"));
|
|
2933
|
+
const globalAgents = existsSync3(join5(globalClaudeDir, "agents"));
|
|
2645
2934
|
const agentsExist = localAgents || globalAgents;
|
|
2646
2935
|
console.log(` Mix agents: ${agentsExist ? "Installed" : "Not installed"}`);
|
|
2647
|
-
const mcpJsonExists =
|
|
2936
|
+
const mcpJsonExists = existsSync3(join5(process.cwd(), ".mcp.json"));
|
|
2648
2937
|
console.log(` MCP config: ${mcpJsonExists ? ".mcp.json found" : ".mcp.json not present"}`);
|
|
2649
2938
|
console.log("");
|
|
2650
2939
|
console.log("Setup complete! Next steps:");
|
|
2651
2940
|
if (selections.bridge) {
|
|
2652
2941
|
const scriptsDir = getReaperScriptsPath();
|
|
2653
|
-
const luaDest =
|
|
2942
|
+
const luaDest = join5(scriptsDir, "mcp_bridge.lua");
|
|
2654
2943
|
console.log(" 1. Open REAPER");
|
|
2655
2944
|
console.log(" 2. Actions > Show action list > Load ReaScript");
|
|
2656
2945
|
console.log(` 3. Select: ${luaDest}`);
|
|
@@ -2665,33 +2954,206 @@ async function runInit(opts, dirResolver) {
|
|
|
2665
2954
|
}
|
|
2666
2955
|
}
|
|
2667
2956
|
|
|
2668
|
-
// apps/reaper-mcp-server/src/
|
|
2957
|
+
// apps/reaper-mcp-server/src/setup-sidecar.ts
|
|
2958
|
+
import { exec as execImpl } from "node:child_process";
|
|
2959
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, copyFileSync as copyFileSync2, rmSync } from "node:fs";
|
|
2960
|
+
import { join as join6, dirname as dirname2 } from "node:path";
|
|
2961
|
+
import { homedir as homedir4, platform as platform3 } from "node:os";
|
|
2962
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2963
|
+
var VENV_BIN2 = platform3() === "win32" ? "Scripts" : "bin";
|
|
2964
|
+
var VENV_PYTHON_NAME2 = platform3() === "win32" ? "python.exe" : "python";
|
|
2965
|
+
var VENV_PIP_NAME = platform3() === "win32" ? "pip.exe" : "pip";
|
|
2966
|
+
function exec(cmd) {
|
|
2967
|
+
return new Promise((resolve, reject) => {
|
|
2968
|
+
execImpl(cmd, (err, stdout, stderr) => {
|
|
2969
|
+
if (err) reject(err);
|
|
2970
|
+
else resolve({ stdout, stderr });
|
|
2971
|
+
});
|
|
2972
|
+
});
|
|
2973
|
+
}
|
|
2669
2974
|
var __dirname = dirname2(fileURLToPath2(import.meta.url));
|
|
2975
|
+
var SIDECAR_VENV_PATH2 = join6(homedir4(), ".reaper-mcp", "sidecar-venv");
|
|
2976
|
+
var SIDECAR_DIR = join6(homedir4(), ".reaper-mcp", "sidecar");
|
|
2977
|
+
var SIDECAR_SCRIPT_DEST = join6(SIDECAR_DIR, "server.py");
|
|
2978
|
+
var MODEL_ID = "facebook/audiobox-aesthetics";
|
|
2979
|
+
var MIN_PYTHON_MAJOR = 3;
|
|
2980
|
+
var MIN_PYTHON_MINOR = 10;
|
|
2981
|
+
function getPythonBin() {
|
|
2982
|
+
return process.env["PYTHON_BIN"] ?? "python3";
|
|
2983
|
+
}
|
|
2984
|
+
function parsePythonVersion(output) {
|
|
2985
|
+
const match = output.match(/Python\s+(\d+)\.(\d+)\.(\d+)/i);
|
|
2986
|
+
if (!match) return null;
|
|
2987
|
+
return [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10)];
|
|
2988
|
+
}
|
|
2989
|
+
async function checkPython(pythonBin) {
|
|
2990
|
+
let stdout;
|
|
2991
|
+
try {
|
|
2992
|
+
const result = await exec(`"${pythonBin}" --version`);
|
|
2993
|
+
stdout = (result.stdout + result.stderr).trim();
|
|
2994
|
+
} catch {
|
|
2995
|
+
const notFound = `Python interpreter not found: ${pythonBin}
|
|
2996
|
+
Install Python 3.10+ from https://python.org or set the PYTHON_BIN environment variable.
|
|
2997
|
+
Example: PYTHON_BIN=/usr/local/bin/python3.11 node dist/apps/reaper-mcp-server/main.js setup-sidecar`;
|
|
2998
|
+
throw new Error(notFound);
|
|
2999
|
+
}
|
|
3000
|
+
const version = parsePythonVersion(stdout);
|
|
3001
|
+
if (!version) {
|
|
3002
|
+
throw new Error(`Could not parse Python version from: "${stdout}". Is PYTHON_BIN set correctly?`);
|
|
3003
|
+
}
|
|
3004
|
+
const [major, minor, patch] = version;
|
|
3005
|
+
if (major < MIN_PYTHON_MAJOR || major === MIN_PYTHON_MAJOR && minor < MIN_PYTHON_MINOR) {
|
|
3006
|
+
throw new Error(
|
|
3007
|
+
`Python ${MIN_PYTHON_MAJOR}.${MIN_PYTHON_MINOR}+ required. Found: ${major}.${minor}.${patch} at ${pythonBin}.
|
|
3008
|
+
Install Python 3.10+ from https://python.org or set PYTHON_BIN to a newer interpreter.`
|
|
3009
|
+
);
|
|
3010
|
+
}
|
|
3011
|
+
return `${major}.${minor}.${patch}`;
|
|
3012
|
+
}
|
|
3013
|
+
async function createVenv(pythonBin) {
|
|
3014
|
+
mkdirSync3(join6(homedir4(), ".reaper-mcp"), { recursive: true });
|
|
3015
|
+
await exec(`"${pythonBin}" -m venv --clear "${SIDECAR_VENV_PATH2}"`);
|
|
3016
|
+
}
|
|
3017
|
+
function cleanupBrokenVenv() {
|
|
3018
|
+
try {
|
|
3019
|
+
if (existsSync4(SIDECAR_VENV_PATH2)) {
|
|
3020
|
+
rmSync(SIDECAR_VENV_PATH2, { recursive: true, force: true });
|
|
3021
|
+
}
|
|
3022
|
+
} catch {
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
function resolveRequirementsTxt(baseDir) {
|
|
3026
|
+
const sidecarDir = resolveAssetDir(baseDir, "sidecar");
|
|
3027
|
+
return join6(sidecarDir, "requirements.txt");
|
|
3028
|
+
}
|
|
3029
|
+
async function installDeps(requirementsPath) {
|
|
3030
|
+
const pip = join6(SIDECAR_VENV_PATH2, VENV_BIN2, VENV_PIP_NAME);
|
|
3031
|
+
await exec(`"${pip}" install --upgrade pip`);
|
|
3032
|
+
const { stdout, stderr } = await exec(`"${pip}" install -r "${requirementsPath}"`);
|
|
3033
|
+
if (stdout) process.stdout.write(stdout);
|
|
3034
|
+
if (stderr) process.stderr.write(stderr);
|
|
3035
|
+
}
|
|
3036
|
+
async function downloadModelWeights() {
|
|
3037
|
+
const python = join6(SIDECAR_VENV_PATH2, VENV_BIN2, VENV_PYTHON_NAME2);
|
|
3038
|
+
const script = [
|
|
3039
|
+
"from huggingface_hub import snapshot_download",
|
|
3040
|
+
`print("Downloading ${MODEL_ID} weights to ~/.cache/huggingface/hub/ ...")`,
|
|
3041
|
+
`snapshot_download("${MODEL_ID}")`,
|
|
3042
|
+
`print("Download complete.")`
|
|
3043
|
+
].join("; ");
|
|
3044
|
+
const { stdout, stderr } = await exec(`"${python}" -c "${script}"`);
|
|
3045
|
+
if (stdout) process.stdout.write(stdout);
|
|
3046
|
+
if (stderr) process.stderr.write(stderr);
|
|
3047
|
+
}
|
|
3048
|
+
function copySidecarScript(baseDir) {
|
|
3049
|
+
const sidecarDir = resolveAssetDir(baseDir, "sidecar");
|
|
3050
|
+
const src = join6(sidecarDir, "server.py");
|
|
3051
|
+
if (!existsSync4(src)) {
|
|
3052
|
+
throw new Error(
|
|
3053
|
+
`sidecar/server.py not found at: ${src}
|
|
3054
|
+
Run \`pnpm nx build reaper-mcp-server\` first.`
|
|
3055
|
+
);
|
|
3056
|
+
}
|
|
3057
|
+
mkdirSync3(SIDECAR_DIR, { recursive: true });
|
|
3058
|
+
copyFileSync2(src, SIDECAR_SCRIPT_DEST);
|
|
3059
|
+
}
|
|
3060
|
+
async function setupSidecar() {
|
|
3061
|
+
console.log("REAPER MCP \u2014 Setup Python Sidecar\n");
|
|
3062
|
+
console.log("This installs the opt-in audio AI subsystem for perceptual analysis.");
|
|
3063
|
+
console.log("Requires Python 3.10+ and an internet connection (~831 MB download).\n");
|
|
3064
|
+
const pythonBin = getPythonBin();
|
|
3065
|
+
console.log(`Checking Python interpreter: ${pythonBin}`);
|
|
3066
|
+
let version;
|
|
3067
|
+
try {
|
|
3068
|
+
version = await checkPython(pythonBin);
|
|
3069
|
+
} catch (err) {
|
|
3070
|
+
console.error(`
|
|
3071
|
+
Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
3072
|
+
process.exit(1);
|
|
3073
|
+
}
|
|
3074
|
+
console.log(` Using Python interpreter: ${pythonBin} (${version})`);
|
|
3075
|
+
console.log(`
|
|
3076
|
+
Creating virtual environment at: ${SIDECAR_VENV_PATH2}`);
|
|
3077
|
+
try {
|
|
3078
|
+
await createVenv(pythonBin);
|
|
3079
|
+
console.log(" Virtual environment created.");
|
|
3080
|
+
} catch (err) {
|
|
3081
|
+
console.error(`
|
|
3082
|
+
Failed to create venv: ${err instanceof Error ? err.message : String(err)}`);
|
|
3083
|
+
process.exit(1);
|
|
3084
|
+
}
|
|
3085
|
+
const requirementsPath = resolveRequirementsTxt(__dirname);
|
|
3086
|
+
console.log(`
|
|
3087
|
+
Installing Python dependencies from: ${requirementsPath}`);
|
|
3088
|
+
if (!existsSync4(requirementsPath)) {
|
|
3089
|
+
console.error(`
|
|
3090
|
+
Error: requirements.txt not found at: ${requirementsPath}`);
|
|
3091
|
+
console.error("Run `pnpm nx build reaper-mcp-server` first.");
|
|
3092
|
+
process.exit(1);
|
|
3093
|
+
}
|
|
3094
|
+
try {
|
|
3095
|
+
await installDeps(requirementsPath);
|
|
3096
|
+
console.log(" Dependencies installed.");
|
|
3097
|
+
} catch (err) {
|
|
3098
|
+
console.error(`
|
|
3099
|
+
Failed to install dependencies: ${err instanceof Error ? err.message : String(err)}`);
|
|
3100
|
+
cleanupBrokenVenv();
|
|
3101
|
+
console.error("Removed partial venv so the next setup-sidecar run starts clean.");
|
|
3102
|
+
process.exit(1);
|
|
3103
|
+
}
|
|
3104
|
+
console.log("\nPre-downloading Audiobox Aesthetics model weights (~831 MB)...");
|
|
3105
|
+
console.log("This may take several minutes depending on your internet connection.");
|
|
3106
|
+
try {
|
|
3107
|
+
await downloadModelWeights();
|
|
3108
|
+
} catch (err) {
|
|
3109
|
+
console.error(`
|
|
3110
|
+
Model download failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3111
|
+
console.error("Check your internet connection and run setup-sidecar again.");
|
|
3112
|
+
process.exit(1);
|
|
3113
|
+
}
|
|
3114
|
+
console.log(`
|
|
3115
|
+
Installing sidecar script to: ${SIDECAR_SCRIPT_DEST}`);
|
|
3116
|
+
try {
|
|
3117
|
+
copySidecarScript(__dirname);
|
|
3118
|
+
console.log(" Sidecar script installed.");
|
|
3119
|
+
} catch (err) {
|
|
3120
|
+
console.error(`
|
|
3121
|
+
Failed to install sidecar script: ${err instanceof Error ? err.message : String(err)}`);
|
|
3122
|
+
process.exit(1);
|
|
3123
|
+
}
|
|
3124
|
+
console.log("\nSidecar setup complete!\n");
|
|
3125
|
+
console.log("The analyze_track_aesthetics tool is now available.");
|
|
3126
|
+
console.log("Run `node dist/apps/reaper-mcp-server/main.js doctor` to verify.\n");
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
// apps/reaper-mcp-server/src/main.ts
|
|
3130
|
+
var execAsync = promisifyUtil(execCb);
|
|
3131
|
+
var __dirname2 = dirname3(fileURLToPath3(import.meta.url));
|
|
2670
3132
|
async function setup() {
|
|
2671
3133
|
console.log("REAPER MCP Server \u2014 Setup\n");
|
|
2672
3134
|
const bridgeDir = await ensureBridgeDir();
|
|
2673
3135
|
console.log(`Bridge directory: ${bridgeDir}
|
|
2674
3136
|
`);
|
|
2675
3137
|
const scriptsDir = getReaperScriptsPath();
|
|
2676
|
-
|
|
2677
|
-
const reaperDir = resolveAssetDir(
|
|
3138
|
+
mkdirSync4(scriptsDir, { recursive: true });
|
|
3139
|
+
const reaperDir = resolveAssetDir(__dirname2, "reaper");
|
|
2678
3140
|
console.log("Installing Lua scripts...");
|
|
2679
3141
|
for (const luaFile of ["mcp_bridge.lua", "mcp_snapshot_manager.lua"]) {
|
|
2680
|
-
const src =
|
|
2681
|
-
const dest =
|
|
3142
|
+
const src = join7(reaperDir, luaFile);
|
|
3143
|
+
const dest = join7(scriptsDir, luaFile);
|
|
2682
3144
|
if (installFile(src, dest)) {
|
|
2683
3145
|
console.log(` Installed: ${luaFile}`);
|
|
2684
3146
|
} else {
|
|
2685
3147
|
console.log(` Not found: ${src}`);
|
|
2686
3148
|
}
|
|
2687
3149
|
}
|
|
2688
|
-
const effectsDir =
|
|
2689
|
-
|
|
3150
|
+
const effectsDir = join7(getReaperEffectsPath(), "reaper-mcp");
|
|
3151
|
+
mkdirSync4(effectsDir, { recursive: true });
|
|
2690
3152
|
console.log("\nInstalling JSFX analyzers...");
|
|
2691
3153
|
for (const asset of REAPER_ASSETS) {
|
|
2692
3154
|
if (asset.endsWith(".lua")) continue;
|
|
2693
|
-
const src =
|
|
2694
|
-
const dest =
|
|
3155
|
+
const src = join7(reaperDir, asset);
|
|
3156
|
+
const dest = join7(effectsDir, asset);
|
|
2695
3157
|
if (installFile(src, dest)) {
|
|
2696
3158
|
console.log(` Installed: reaper-mcp/${asset}`);
|
|
2697
3159
|
} else {
|
|
@@ -2702,7 +3164,7 @@ async function setup() {
|
|
|
2702
3164
|
console.log("Next steps:");
|
|
2703
3165
|
console.log(" 1. Open REAPER");
|
|
2704
3166
|
console.log(" 2. Actions > Show action list > Load ReaScript");
|
|
2705
|
-
console.log(` 3. Select: ${
|
|
3167
|
+
console.log(` 3. Select: ${join7(scriptsDir, "mcp_bridge.lua")}`);
|
|
2706
3168
|
console.log(" 4. Run the script (it will keep running in background via defer loop)");
|
|
2707
3169
|
console.log(" 5. Add reaper-mcp to your Claude Code config (see: npx @mthines/reaper-mcp doctor)");
|
|
2708
3170
|
}
|
|
@@ -2714,41 +3176,41 @@ async function installSkills(scope) {
|
|
|
2714
3176
|
console.log(`REAPER MCP \u2014 Install AI Mix Engineer Skills (scope: ${scope})
|
|
2715
3177
|
`);
|
|
2716
3178
|
const isGlobal = scope === "global";
|
|
2717
|
-
const baseDir = isGlobal ?
|
|
2718
|
-
const claudeDir = isGlobal ? baseDir :
|
|
2719
|
-
const knowledgeSrc = resolveAssetDir(
|
|
2720
|
-
if (
|
|
2721
|
-
const dest =
|
|
3179
|
+
const baseDir = isGlobal ? join7(homedir5(), ".claude") : process.cwd();
|
|
3180
|
+
const claudeDir = isGlobal ? baseDir : join7(baseDir, ".claude");
|
|
3181
|
+
const knowledgeSrc = resolveAssetDir(__dirname2, "knowledge");
|
|
3182
|
+
if (existsSync5(knowledgeSrc)) {
|
|
3183
|
+
const dest = join7(baseDir, "knowledge");
|
|
2722
3184
|
const count = copyDirSync(knowledgeSrc, dest);
|
|
2723
3185
|
console.log(`Installed knowledge base: ${count} files \u2192 ${dest}`);
|
|
2724
3186
|
} else {
|
|
2725
3187
|
console.log("Knowledge base not found in package. Skipping.");
|
|
2726
3188
|
}
|
|
2727
|
-
const rulesSrc = resolveAssetDirWithFallback(
|
|
2728
|
-
if (
|
|
2729
|
-
const dest =
|
|
3189
|
+
const rulesSrc = resolveAssetDirWithFallback(__dirname2, "claude-rules", join7(".claude", "rules"));
|
|
3190
|
+
if (existsSync5(rulesSrc)) {
|
|
3191
|
+
const dest = join7(claudeDir, "rules");
|
|
2730
3192
|
const count = copyDirSync(rulesSrc, dest);
|
|
2731
3193
|
console.log(`Installed Claude rules: ${count} files \u2192 ${dest}`);
|
|
2732
3194
|
} else {
|
|
2733
3195
|
console.log("Claude rules not found in package. Skipping.");
|
|
2734
3196
|
}
|
|
2735
|
-
const skillsSrc = resolveAssetDirWithFallback(
|
|
2736
|
-
if (
|
|
2737
|
-
const dest =
|
|
3197
|
+
const skillsSrc = resolveAssetDirWithFallback(__dirname2, "claude-skills", join7(".claude", "skills"));
|
|
3198
|
+
if (existsSync5(skillsSrc)) {
|
|
3199
|
+
const dest = join7(claudeDir, "skills");
|
|
2738
3200
|
const count = copyDirSync(skillsSrc, dest);
|
|
2739
3201
|
console.log(`Installed Claude skills: ${count} files \u2192 ${dest}`);
|
|
2740
3202
|
} else {
|
|
2741
3203
|
console.log("Claude skills not found in package. Skipping.");
|
|
2742
3204
|
}
|
|
2743
|
-
const agentsSrc = resolveAssetDirWithFallback(
|
|
2744
|
-
if (
|
|
2745
|
-
const dest =
|
|
3205
|
+
const agentsSrc = resolveAssetDirWithFallback(__dirname2, "claude-agents", join7(".claude", "agents"));
|
|
3206
|
+
if (existsSync5(agentsSrc)) {
|
|
3207
|
+
const dest = join7(claudeDir, "agents");
|
|
2746
3208
|
const count = copyDirSync(agentsSrc, dest);
|
|
2747
3209
|
console.log(`Installed Claude agents: ${count} files \u2192 ${dest}`);
|
|
2748
3210
|
} else {
|
|
2749
3211
|
console.log("Claude agents not found in package. Skipping.");
|
|
2750
3212
|
}
|
|
2751
|
-
const settingsPath =
|
|
3213
|
+
const settingsPath = join7(claudeDir, "settings.json");
|
|
2752
3214
|
const result = ensureClaudeSettings(settingsPath);
|
|
2753
3215
|
if (result === "created") {
|
|
2754
3216
|
console.log(`Created Claude settings: ${settingsPath}`);
|
|
@@ -2758,7 +3220,7 @@ async function installSkills(scope) {
|
|
|
2758
3220
|
console.log(`Claude settings already has all REAPER tools: ${settingsPath}`);
|
|
2759
3221
|
}
|
|
2760
3222
|
if (!isGlobal) {
|
|
2761
|
-
const mcpJsonPath =
|
|
3223
|
+
const mcpJsonPath = join7(baseDir, ".mcp.json");
|
|
2762
3224
|
if (createMcpJson(mcpJsonPath)) {
|
|
2763
3225
|
console.log(`
|
|
2764
3226
|
Created: ${mcpJsonPath}`);
|
|
@@ -2779,31 +3241,92 @@ async function doctor() {
|
|
|
2779
3241
|
if (!bridgeRunning) {
|
|
2780
3242
|
console.log(' \u2192 Run "npx @mthines/reaper-mcp setup" then load mcp_bridge.lua in REAPER');
|
|
2781
3243
|
}
|
|
2782
|
-
const globalClaudeDir =
|
|
2783
|
-
const localAgents =
|
|
2784
|
-
const globalAgents =
|
|
3244
|
+
const globalClaudeDir = join7(homedir5(), ".claude");
|
|
3245
|
+
const localAgents = existsSync5(join7(process.cwd(), ".claude", "agents"));
|
|
3246
|
+
const globalAgents = existsSync5(join7(globalClaudeDir, "agents"));
|
|
2785
3247
|
const agentsExist = localAgents || globalAgents;
|
|
2786
3248
|
const agentsLocation = localAgents ? ".claude/agents/" : globalAgents ? "~/.claude/agents/" : "";
|
|
2787
3249
|
console.log(`Mix agents: ${agentsExist ? `\u2713 Found (${agentsLocation})` : "\u2717 Not installed"}`);
|
|
2788
3250
|
if (!agentsExist) {
|
|
2789
3251
|
console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"');
|
|
2790
3252
|
}
|
|
2791
|
-
const localKnowledge =
|
|
2792
|
-
const globalKnowledge =
|
|
3253
|
+
const localKnowledge = existsSync5(join7(process.cwd(), "knowledge"));
|
|
3254
|
+
const globalKnowledge = existsSync5(join7(globalClaudeDir, "knowledge"));
|
|
2793
3255
|
const knowledgeExists = localKnowledge || globalKnowledge;
|
|
2794
3256
|
const knowledgeLocation = localKnowledge ? "project" : globalKnowledge ? "~/.claude/" : "";
|
|
2795
3257
|
console.log(`Knowledge base: ${knowledgeExists ? `\u2713 Found (${knowledgeLocation})` : "\u2717 Not installed"}`);
|
|
2796
3258
|
if (!knowledgeExists) {
|
|
2797
3259
|
console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"');
|
|
2798
3260
|
}
|
|
2799
|
-
const mcpJsonExists =
|
|
3261
|
+
const mcpJsonExists = existsSync5(join7(process.cwd(), ".mcp.json"));
|
|
2800
3262
|
console.log(`MCP config: ${mcpJsonExists ? "\u2713 .mcp.json found" : "\u2717 .mcp.json missing"}`);
|
|
2801
3263
|
if (!mcpJsonExists) {
|
|
2802
3264
|
console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills --project" to create .mcp.json');
|
|
2803
3265
|
}
|
|
2804
3266
|
console.log('\nTo check SWS Extensions, start REAPER and use the "list_available_fx" tool.');
|
|
2805
3267
|
console.log("SWS provides enhanced plugin discovery and snapshot support.\n");
|
|
2806
|
-
|
|
3268
|
+
console.log("Python Sidecar (opt-in, for analyze_track_aesthetics):");
|
|
3269
|
+
const sidecarVenvPath = join7(homedir5(), ".reaper-mcp", "sidecar-venv");
|
|
3270
|
+
const venvBin = platform4() === "win32" ? "Scripts" : "bin";
|
|
3271
|
+
const venvPythonName = platform4() === "win32" ? "python.exe" : "python";
|
|
3272
|
+
const venvPython = join7(sidecarVenvPath, venvBin, venvPythonName);
|
|
3273
|
+
const hfCachePath = join7(homedir5(), ".cache", "huggingface", "hub");
|
|
3274
|
+
let pythonOk = false;
|
|
3275
|
+
let pythonDetail = "not found";
|
|
3276
|
+
try {
|
|
3277
|
+
const pythonBin = process.env["PYTHON_BIN"] ?? "python3";
|
|
3278
|
+
const { stdout, stderr } = await execAsync(`"${pythonBin}" --version`);
|
|
3279
|
+
const versionStr = (stdout + stderr).trim();
|
|
3280
|
+
const match = versionStr.match(/Python\s+(\d+)\.(\d+)/i);
|
|
3281
|
+
if (match) {
|
|
3282
|
+
const major = parseInt(match[1], 10);
|
|
3283
|
+
const minor = parseInt(match[2], 10);
|
|
3284
|
+
pythonOk = major > 3 || major === 3 && minor >= 10;
|
|
3285
|
+
pythonDetail = versionStr;
|
|
3286
|
+
}
|
|
3287
|
+
} catch {
|
|
3288
|
+
pythonDetail = "not found";
|
|
3289
|
+
}
|
|
3290
|
+
console.log(` Python \u2265 3.10: ${pythonOk ? `\u2713 ${pythonDetail}` : `\u2717 ${pythonDetail}`}`);
|
|
3291
|
+
if (!pythonOk) {
|
|
3292
|
+
console.log(" \u2192 Install Python 3.10+ from https://python.org or set PYTHON_BIN");
|
|
3293
|
+
}
|
|
3294
|
+
const venvExists = existsSync5(sidecarVenvPath);
|
|
3295
|
+
console.log(` Venv: ${venvExists ? `\u2713 ${sidecarVenvPath}` : "\u2717 Not installed"}`);
|
|
3296
|
+
if (!venvExists) {
|
|
3297
|
+
console.log(" \u2192 Run: node dist/apps/reaper-mcp-server/main.js setup-sidecar");
|
|
3298
|
+
}
|
|
3299
|
+
let depsOk = false;
|
|
3300
|
+
if (venvExists && existsSync5(venvPython)) {
|
|
3301
|
+
try {
|
|
3302
|
+
await execAsync(`"${venvPython}" -c "import audiobox_aesthetics"`);
|
|
3303
|
+
depsOk = true;
|
|
3304
|
+
} catch {
|
|
3305
|
+
depsOk = false;
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
console.log(` Dependencies: ${depsOk ? "\u2713 audiobox-aesthetics importable" : "\u2717 Not installed"}`);
|
|
3309
|
+
if (!depsOk && venvExists) {
|
|
3310
|
+
console.log(" \u2192 Run: node dist/apps/reaper-mcp-server/main.js setup-sidecar");
|
|
3311
|
+
}
|
|
3312
|
+
const weightsPath = join7(hfCachePath, "models--facebook--audiobox-aesthetics");
|
|
3313
|
+
const weightsExist = existsSync5(weightsPath);
|
|
3314
|
+
console.log(` Model weights: ${weightsExist ? `\u2713 ${weightsPath}` : "\u2717 Not downloaded"}`);
|
|
3315
|
+
if (!weightsExist) {
|
|
3316
|
+
console.log(" \u2192 Run: node dist/apps/reaper-mcp-server/main.js setup-sidecar");
|
|
3317
|
+
}
|
|
3318
|
+
const sidecarReady = venvExists && depsOk && weightsExist;
|
|
3319
|
+
const sidecarOptedIn = venvExists || weightsExist;
|
|
3320
|
+
const sidecarBroken = sidecarOptedIn && !sidecarReady;
|
|
3321
|
+
if (!sidecarOptedIn) {
|
|
3322
|
+
console.log("\n Sidecar: not installed (opt-in). Run setup-sidecar to enable audio AI tools.");
|
|
3323
|
+
} else if (sidecarBroken) {
|
|
3324
|
+
console.log("\n Sidecar: PARTIALLY INSTALLED \u2014 run setup-sidecar to repair.");
|
|
3325
|
+
} else {
|
|
3326
|
+
console.log("\n Sidecar: fully installed and ready.");
|
|
3327
|
+
}
|
|
3328
|
+
console.log("");
|
|
3329
|
+
process.exit(bridgeRunning && knowledgeExists && mcpJsonExists && !sidecarBroken ? 0 : 1);
|
|
2807
3330
|
}
|
|
2808
3331
|
async function serve() {
|
|
2809
3332
|
const log = (...args) => console.error("[reaper-mcp]", ...args);
|
|
@@ -2812,7 +3335,7 @@ async function serve() {
|
|
|
2812
3335
|
startDiagnosticsPoller();
|
|
2813
3336
|
startEventsPoller();
|
|
2814
3337
|
log("Starting REAPER MCP Server...");
|
|
2815
|
-
log(`Entry: ${
|
|
3338
|
+
log(`Entry: ${fileURLToPath3(import.meta.url)}`);
|
|
2816
3339
|
const tracer = getTracer();
|
|
2817
3340
|
await tracer.startActiveSpan("mcp.server.startup", { kind: SpanKind3.INTERNAL }, async (startupSpan) => {
|
|
2818
3341
|
try {
|
|
@@ -2866,7 +3389,7 @@ switch (command) {
|
|
|
2866
3389
|
case "init":
|
|
2867
3390
|
runInit(
|
|
2868
3391
|
{ yes: hasYesFlag, project: hasProjectFlag },
|
|
2869
|
-
() =>
|
|
3392
|
+
() => __dirname2
|
|
2870
3393
|
).catch((err) => {
|
|
2871
3394
|
console.error("Init failed:", err);
|
|
2872
3395
|
process.exit(1);
|
|
@@ -2890,6 +3413,12 @@ switch (command) {
|
|
|
2890
3413
|
process.exit(1);
|
|
2891
3414
|
});
|
|
2892
3415
|
break;
|
|
3416
|
+
case "setup-sidecar":
|
|
3417
|
+
setupSidecar().catch((err) => {
|
|
3418
|
+
console.error("Sidecar setup failed:", err);
|
|
3419
|
+
process.exit(1);
|
|
3420
|
+
});
|
|
3421
|
+
break;
|
|
2893
3422
|
case "status": {
|
|
2894
3423
|
(async () => {
|
|
2895
3424
|
const running = await isBridgeRunning();
|
|
@@ -2915,6 +3444,7 @@ Usage:
|
|
|
2915
3444
|
npx @mthines/reaper-mcp init --yes Non-interactive setup (install everything with defaults)
|
|
2916
3445
|
npx @mthines/reaper-mcp init --project Include .mcp.json in current directory
|
|
2917
3446
|
npx @mthines/reaper-mcp setup Install Lua bridge + JSFX analyzers into REAPER
|
|
3447
|
+
npx @mthines/reaper-mcp setup-sidecar Install Python sidecar for perceptual audio analysis (opt-in, ~831 MB)
|
|
2918
3448
|
npx @mthines/reaper-mcp install-skills Install AI knowledge + agents globally (default)
|
|
2919
3449
|
npx @mthines/reaper-mcp install-skills --project Install into current project directory
|
|
2920
3450
|
npx @mthines/reaper-mcp install-skills --global Install into ~/.claude/ (default)
|
|
@@ -2936,24 +3466,31 @@ Tip: install globally for shorter commands:
|
|
|
2936
3466
|
`);
|
|
2937
3467
|
break;
|
|
2938
3468
|
}
|
|
3469
|
+
function shutdownAll() {
|
|
3470
|
+
stopPollers();
|
|
3471
|
+
try {
|
|
3472
|
+
getSidecarClient().shutdown();
|
|
3473
|
+
} catch {
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
2939
3476
|
process.on("SIGINT", () => {
|
|
2940
3477
|
console.error("[reaper-mcp] Interrupted");
|
|
2941
|
-
|
|
3478
|
+
shutdownAll();
|
|
2942
3479
|
shutdownTelemetry().finally(() => process.exit(0));
|
|
2943
3480
|
});
|
|
2944
3481
|
process.on("SIGTERM", () => {
|
|
2945
3482
|
console.error("[reaper-mcp] Terminated");
|
|
2946
|
-
|
|
3483
|
+
shutdownAll();
|
|
2947
3484
|
shutdownTelemetry().finally(() => process.exit(0));
|
|
2948
3485
|
});
|
|
2949
3486
|
process.on("uncaughtException", (err) => {
|
|
2950
3487
|
console.error("[reaper-mcp] Uncaught exception:", err);
|
|
2951
|
-
|
|
3488
|
+
shutdownAll();
|
|
2952
3489
|
shutdownTelemetry().finally(() => process.exit(1));
|
|
2953
3490
|
});
|
|
2954
3491
|
process.on("unhandledRejection", (reason) => {
|
|
2955
3492
|
console.error("[reaper-mcp] Unhandled rejection:", reason);
|
|
2956
|
-
|
|
3493
|
+
shutdownAll();
|
|
2957
3494
|
shutdownTelemetry().finally(() => process.exit(1));
|
|
2958
3495
|
});
|
|
2959
3496
|
//# sourceMappingURL=main.js.map
|
package/package.json
CHANGED
package/reaper/mcp_bridge.lua
CHANGED
|
@@ -1533,7 +1533,11 @@ local MCP_LUFS_METER_FX_NAME = "reaper-mcp/mcp_lufs_meter"
|
|
|
1533
1533
|
local MCP_CORRELATION_METER_FX_NAME = "reaper-mcp/mcp_correlation_meter"
|
|
1534
1534
|
local MCP_CREST_FACTOR_FX_NAME = "reaper-mcp/mcp_crest_factor"
|
|
1535
1535
|
local MCP_MIDI_EMITTER_FX_NAME = "reaper-mcp/mcp_midi_emitter"
|
|
1536
|
-
|
|
1536
|
+
-- Lua pattern matching the JSFX `desc:` line — that's what TrackFX_GetFXName
|
|
1537
|
+
-- returns, not the on-disk path. Every MCP JSFX starts its desc with "MCP ".
|
|
1538
|
+
-- (The outer container is renamed to "MCP Meters", but this prefix is only used
|
|
1539
|
+
-- to identify the JSFX *inside* a container, so collisions are not possible.)
|
|
1540
|
+
local MCP_FX_PREFIX = "MCP "
|
|
1537
1541
|
|
|
1538
1542
|
-- Per-track cache of MCP container FX index to avoid rescanning on every read.
|
|
1539
1543
|
-- Keyed by track pointer (userdata), value is container FX index or false (no container).
|
|
@@ -3612,6 +3616,187 @@ function handlers.send_midi_note(params)
|
|
|
3612
3616
|
return { sent = true, timestampMs = math.floor(reaper.time_precise() * 1000) }
|
|
3613
3617
|
end
|
|
3614
3618
|
|
|
3619
|
+
-- =============================================================================
|
|
3620
|
+
-- render_track_to_wav handler
|
|
3621
|
+
--
|
|
3622
|
+
-- Renders a target track's output (post-FX, post-fader) to a temp WAV file.
|
|
3623
|
+
-- Saves and restores all render settings and solo states around the render.
|
|
3624
|
+
-- Blocks the defer loop during render (~0.5-1s) — this is by design.
|
|
3625
|
+
--
|
|
3626
|
+
-- Params:
|
|
3627
|
+
-- trackIndex (number) — zero-based track index
|
|
3628
|
+
-- startTime (number) — render start, seconds from project start
|
|
3629
|
+
-- endTime (number) — render end, seconds from project start
|
|
3630
|
+
-- commandId (string) — used to generate a unique temp filename
|
|
3631
|
+
--
|
|
3632
|
+
-- Returns:
|
|
3633
|
+
-- { trackIndex, trackName, wavPath, durationSeconds, sampleRate, channelCount }
|
|
3634
|
+
-- =============================================================================
|
|
3635
|
+
|
|
3636
|
+
function handlers.render_track_to_wav(params)
|
|
3637
|
+
local track_index = params.trackIndex
|
|
3638
|
+
local start_time = params.startTime
|
|
3639
|
+
local end_time = params.endTime
|
|
3640
|
+
local command_id = params.commandId or tostring(os.time())
|
|
3641
|
+
|
|
3642
|
+
-- Validate inputs
|
|
3643
|
+
if track_index == nil then
|
|
3644
|
+
return nil, "Missing required param: trackIndex"
|
|
3645
|
+
end
|
|
3646
|
+
if start_time == nil or end_time == nil then
|
|
3647
|
+
return nil, "Missing required params: startTime, endTime"
|
|
3648
|
+
end
|
|
3649
|
+
if end_time <= start_time then
|
|
3650
|
+
return nil, "endTime must be greater than startTime"
|
|
3651
|
+
end
|
|
3652
|
+
|
|
3653
|
+
-- Get track
|
|
3654
|
+
local track = reaper.GetTrack(0, track_index)
|
|
3655
|
+
if not track then
|
|
3656
|
+
return nil, "Track " .. tostring(track_index) .. " not found"
|
|
3657
|
+
end
|
|
3658
|
+
local track_name = ({reaper.GetSetMediaTrackInfo_String(track, "P_NAME", "", false)})[2] or ""
|
|
3659
|
+
if track_name == "" then track_name = "Track " .. (track_index + 1) end
|
|
3660
|
+
|
|
3661
|
+
-- Render target is always 44100 Hz (WAV for sidecar inference).
|
|
3662
|
+
-- The actual project sample rate is irrelevant here; Audiobox resamples internally.
|
|
3663
|
+
local sample_rate = 44100
|
|
3664
|
+
|
|
3665
|
+
-- Build unique temp file path
|
|
3666
|
+
-- Use GetTempPath() if available (REAPER 6.29+), fall back to os.tmpname pattern
|
|
3667
|
+
local temp_dir
|
|
3668
|
+
if reaper.GetTempPath then
|
|
3669
|
+
temp_dir = reaper.GetTempPath()
|
|
3670
|
+
else
|
|
3671
|
+
-- Fallback: derive temp dir from os.tmpname()
|
|
3672
|
+
local t = os.tmpname()
|
|
3673
|
+
temp_dir = t:match("^(.+)[/\\][^/\\]+$") or "/tmp"
|
|
3674
|
+
os.remove(t)
|
|
3675
|
+
end
|
|
3676
|
+
|
|
3677
|
+
-- Ensure temp dir ends with separator
|
|
3678
|
+
if temp_dir:sub(-1) ~= "/" and temp_dir:sub(-1) ~= "\\" then
|
|
3679
|
+
temp_dir = temp_dir .. "/"
|
|
3680
|
+
end
|
|
3681
|
+
|
|
3682
|
+
local wav_filename = "mcp_aes_" .. command_id:gsub("-", ""):sub(1, 16) .. ".wav"
|
|
3683
|
+
local wav_path = temp_dir .. wav_filename
|
|
3684
|
+
|
|
3685
|
+
-- -------------------------------------------------------------------------
|
|
3686
|
+
-- Save current render settings
|
|
3687
|
+
-- -------------------------------------------------------------------------
|
|
3688
|
+
local saved_file = ({reaper.GetSetProjectInfo_String(0, "RENDER_FILE", "", false)})[2] or ""
|
|
3689
|
+
local saved_pattern = ({reaper.GetSetProjectInfo_String(0, "RENDER_PATTERN", "", false)})[2] or ""
|
|
3690
|
+
local saved_format = ({reaper.GetSetProjectInfo_String(0, "RENDER_FORMAT", "", false)})[2] or ""
|
|
3691
|
+
local saved_bounds = reaper.GetSetProjectInfo(0, "RENDER_BOUNDSFLAG", 0, false)
|
|
3692
|
+
local saved_settings = reaper.GetSetProjectInfo(0, "RENDER_SETTINGS", 0, false)
|
|
3693
|
+
local saved_start = reaper.GetSetProjectInfo(0, "RENDER_STARTPOS", 0, false)
|
|
3694
|
+
local saved_end = reaper.GetSetProjectInfo(0, "RENDER_ENDPOS", 0, false)
|
|
3695
|
+
local saved_srate = reaper.GetSetProjectInfo(0, "RENDER_SRATE", 0, false)
|
|
3696
|
+
local saved_channels = reaper.GetSetProjectInfo(0, "RENDER_CHANNELS", 0, false)
|
|
3697
|
+
|
|
3698
|
+
-- Save solo states for all tracks
|
|
3699
|
+
local track_count = reaper.CountTracks(0)
|
|
3700
|
+
local saved_solo_states = {}
|
|
3701
|
+
for i = 0, track_count - 1 do
|
|
3702
|
+
local t = reaper.GetTrack(0, i)
|
|
3703
|
+
saved_solo_states[i] = reaper.GetMediaTrackInfo_Value(t, "I_SOLO")
|
|
3704
|
+
end
|
|
3705
|
+
|
|
3706
|
+
-- -------------------------------------------------------------------------
|
|
3707
|
+
-- Apply render settings
|
|
3708
|
+
-- -------------------------------------------------------------------------
|
|
3709
|
+
local ok, restore_err = pcall(function()
|
|
3710
|
+
-- Set output: render to temp dir with unique filename (no extension in pattern)
|
|
3711
|
+
reaper.GetSetProjectInfo_String(0, "RENDER_FILE", temp_dir, true)
|
|
3712
|
+
reaper.GetSetProjectInfo_String(0, "RENDER_PATTERN", wav_filename:match("^(.+)%.wav$") or wav_filename, true)
|
|
3713
|
+
|
|
3714
|
+
-- Set render bounds: 0 = custom time range
|
|
3715
|
+
reaper.GetSetProjectInfo(0, "RENDER_BOUNDSFLAG", 0, true)
|
|
3716
|
+
reaper.GetSetProjectInfo(0, "RENDER_STARTPOS", start_time, true)
|
|
3717
|
+
reaper.GetSetProjectInfo(0, "RENDER_ENDPOS", end_time, true)
|
|
3718
|
+
|
|
3719
|
+
-- Set render mode: 2 = stems (selected tracks via master mix / solo)
|
|
3720
|
+
reaper.GetSetProjectInfo(0, "RENDER_SETTINGS", 2, true)
|
|
3721
|
+
|
|
3722
|
+
-- Force WAV at 44100 Hz stereo so the sidecar always gets a known format,
|
|
3723
|
+
-- independent of whatever the user last rendered. Restored in finally.
|
|
3724
|
+
-- "evaw" is the little-endian "wave" fourcc REAPER uses for WAV format.
|
|
3725
|
+
reaper.GetSetProjectInfo_String(0, "RENDER_FORMAT", "evaw", true)
|
|
3726
|
+
reaper.GetSetProjectInfo(0, "RENDER_SRATE", sample_rate, true)
|
|
3727
|
+
reaper.GetSetProjectInfo(0, "RENDER_CHANNELS", 2, true)
|
|
3728
|
+
|
|
3729
|
+
-- Unsolo all tracks, then solo only the target track
|
|
3730
|
+
for i = 0, track_count - 1 do
|
|
3731
|
+
local t = reaper.GetTrack(0, i)
|
|
3732
|
+
reaper.SetMediaTrackInfo_Value(t, "I_SOLO", 0)
|
|
3733
|
+
end
|
|
3734
|
+
reaper.SetMediaTrackInfo_Value(track, "I_SOLO", 1)
|
|
3735
|
+
|
|
3736
|
+
-- Trigger render: action 42230 = "Render project, using the most recent render settings, auto-close render dialog"
|
|
3737
|
+
-- This call is synchronous — it blocks the defer loop until rendering completes.
|
|
3738
|
+
reaper.Main_OnCommand(42230, 0)
|
|
3739
|
+
end)
|
|
3740
|
+
|
|
3741
|
+
-- -------------------------------------------------------------------------
|
|
3742
|
+
-- Restore all settings and solo states (always, even on error)
|
|
3743
|
+
-- -------------------------------------------------------------------------
|
|
3744
|
+
local restore_ok, restore_err2 = pcall(function()
|
|
3745
|
+
reaper.GetSetProjectInfo_String(0, "RENDER_FILE", saved_file, true)
|
|
3746
|
+
reaper.GetSetProjectInfo_String(0, "RENDER_PATTERN", saved_pattern, true)
|
|
3747
|
+
reaper.GetSetProjectInfo_String(0, "RENDER_FORMAT", saved_format, true)
|
|
3748
|
+
reaper.GetSetProjectInfo(0, "RENDER_BOUNDSFLAG", saved_bounds, true)
|
|
3749
|
+
reaper.GetSetProjectInfo(0, "RENDER_SETTINGS", saved_settings, true)
|
|
3750
|
+
reaper.GetSetProjectInfo(0, "RENDER_STARTPOS", saved_start, true)
|
|
3751
|
+
reaper.GetSetProjectInfo(0, "RENDER_ENDPOS", saved_end, true)
|
|
3752
|
+
reaper.GetSetProjectInfo(0, "RENDER_SRATE", saved_srate, true)
|
|
3753
|
+
reaper.GetSetProjectInfo(0, "RENDER_CHANNELS", saved_channels, true)
|
|
3754
|
+
for i = 0, track_count - 1 do
|
|
3755
|
+
local t = reaper.GetTrack(0, i)
|
|
3756
|
+
reaper.SetMediaTrackInfo_Value(t, "I_SOLO", saved_solo_states[i] or 0)
|
|
3757
|
+
end
|
|
3758
|
+
end)
|
|
3759
|
+
|
|
3760
|
+
if not restore_ok then
|
|
3761
|
+
-- Log but don't fail — the render may have succeeded
|
|
3762
|
+
reaper.ShowConsoleMsg("[reaper-mcp] Warning: failed to restore render settings: " .. tostring(restore_err2) .. "\n")
|
|
3763
|
+
end
|
|
3764
|
+
|
|
3765
|
+
if not ok then
|
|
3766
|
+
return nil, "Render failed: " .. tostring(restore_err)
|
|
3767
|
+
end
|
|
3768
|
+
|
|
3769
|
+
-- Verify the file was actually created.
|
|
3770
|
+
-- RENDER_PATTERN was set without the .wav extension so REAPER appends it;
|
|
3771
|
+
-- wav_path already has the full expected name including the extension.
|
|
3772
|
+
local f = io.open(wav_path, "rb")
|
|
3773
|
+
if not f then
|
|
3774
|
+
return nil, "Render produced no output file at: " .. wav_path
|
|
3775
|
+
end
|
|
3776
|
+
-- Verify the rendered file is actually a WAV. RENDER_FORMAT was set to "evaw"
|
|
3777
|
+
-- (REAPER's WAV fourcc) above; if REAPER silently ignored that value the file
|
|
3778
|
+
-- could be any codec the user last rendered (MP3/FLAC/etc.). Check the RIFF
|
|
3779
|
+
-- magic bytes so we fail loudly here rather than crashing in the Python sidecar
|
|
3780
|
+
-- with a confusing "Cannot read audio file" error.
|
|
3781
|
+
local magic = f:read(4)
|
|
3782
|
+
f:close()
|
|
3783
|
+
if magic ~= "RIFF" then
|
|
3784
|
+
os.remove(wav_path)
|
|
3785
|
+
return nil, "Render produced a non-WAV file (RENDER_FORMAT may not have been applied). " ..
|
|
3786
|
+
"First 4 bytes: " .. (magic and string.format("%q", magic) or "<empty>")
|
|
3787
|
+
end
|
|
3788
|
+
|
|
3789
|
+
local duration = end_time - start_time
|
|
3790
|
+
return {
|
|
3791
|
+
trackIndex = track_index,
|
|
3792
|
+
trackName = track_name,
|
|
3793
|
+
wavPath = wav_path,
|
|
3794
|
+
durationSeconds = duration,
|
|
3795
|
+
sampleRate = sample_rate,
|
|
3796
|
+
channelCount = 2,
|
|
3797
|
+
}
|
|
3798
|
+
end
|
|
3799
|
+
|
|
3615
3800
|
-- =============================================================================
|
|
3616
3801
|
-- Bridge diagnostics handler
|
|
3617
3802
|
-- =============================================================================
|