@mthines/reaper-mcp 0.18.0 → 0.19.0-beta.19.2
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 +181 -0
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 {
|
|
@@ -2179,6 +2179,288 @@ function registerCategoryTools(server) {
|
|
|
2179
2179
|
);
|
|
2180
2180
|
}
|
|
2181
2181
|
|
|
2182
|
+
// apps/reaper-mcp-server/src/tools/aesthetics.ts
|
|
2183
|
+
import { z as z17 } from "zod/v4";
|
|
2184
|
+
import { unlink as unlink2 } from "node:fs/promises";
|
|
2185
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2186
|
+
|
|
2187
|
+
// apps/reaper-mcp-server/src/sidecar.ts
|
|
2188
|
+
import { spawn } from "node:child_process";
|
|
2189
|
+
import { existsSync } from "node:fs";
|
|
2190
|
+
import { join as join3 } from "node:path";
|
|
2191
|
+
import { homedir as homedir2, platform as platform2 } from "node:os";
|
|
2192
|
+
var VENV_BIN = platform2() === "win32" ? "Scripts" : "bin";
|
|
2193
|
+
var VENV_PYTHON_NAME = platform2() === "win32" ? "python.exe" : "python";
|
|
2194
|
+
var SIDECAR_VENV_PATH = join3(homedir2(), ".reaper-mcp", "sidecar-venv");
|
|
2195
|
+
var SIDECAR_SCRIPT_PATH = join3(homedir2(), ".reaper-mcp", "sidecar", "server.py");
|
|
2196
|
+
var VENV_PYTHON = join3(SIDECAR_VENV_PATH, VENV_BIN, VENV_PYTHON_NAME);
|
|
2197
|
+
var _rawTimeout = Number(process.env["REAPER_MCP_SIDECAR_TIMEOUT_MS"]);
|
|
2198
|
+
var ANALYZE_TIMEOUT_MS = Number.isFinite(_rawTimeout) && _rawTimeout > 0 ? _rawTimeout : 6e4;
|
|
2199
|
+
var SidecarClientImpl = class {
|
|
2200
|
+
process = null;
|
|
2201
|
+
pending = /* @__PURE__ */ new Map();
|
|
2202
|
+
nextId = 1;
|
|
2203
|
+
lineBuffer = "";
|
|
2204
|
+
restarting = false;
|
|
2205
|
+
restartAttempted = false;
|
|
2206
|
+
/** Cached spawn promise — prevents double-spawn on concurrent cold-start calls. */
|
|
2207
|
+
spawnPromise = null;
|
|
2208
|
+
isAvailable() {
|
|
2209
|
+
return existsSync(SIDECAR_SCRIPT_PATH) && existsSync(VENV_PYTHON);
|
|
2210
|
+
}
|
|
2211
|
+
async analyze(wavPath, startTime, endTime) {
|
|
2212
|
+
if (!this.isAvailable()) {
|
|
2213
|
+
throw new Error(
|
|
2214
|
+
"Audio understanding sidecar not installed. Run: node dist/apps/reaper-mcp-server/main.js setup-sidecar"
|
|
2215
|
+
);
|
|
2216
|
+
}
|
|
2217
|
+
await this.ensureRunning();
|
|
2218
|
+
return new Promise((resolve, reject) => {
|
|
2219
|
+
const id = this.nextId++;
|
|
2220
|
+
const request = {
|
|
2221
|
+
jsonrpc: "2.0",
|
|
2222
|
+
id,
|
|
2223
|
+
method: "analyze",
|
|
2224
|
+
params: { path: wavPath, startTime, endTime }
|
|
2225
|
+
};
|
|
2226
|
+
const timer = setTimeout(() => {
|
|
2227
|
+
if (this.pending.delete(id)) {
|
|
2228
|
+
process.stderr.write(
|
|
2229
|
+
`[reaper-mcp] Sidecar analyze() timed out after ${ANALYZE_TIMEOUT_MS}ms \u2014 killing process
|
|
2230
|
+
`
|
|
2231
|
+
);
|
|
2232
|
+
if (this.process) {
|
|
2233
|
+
this.process.kill("SIGKILL");
|
|
2234
|
+
this.process = null;
|
|
2235
|
+
}
|
|
2236
|
+
reject(new Error(
|
|
2237
|
+
`Sidecar timeout after ${ANALYZE_TIMEOUT_MS}ms. Override with REAPER_MCP_SIDECAR_TIMEOUT_MS env var.`
|
|
2238
|
+
));
|
|
2239
|
+
}
|
|
2240
|
+
}, ANALYZE_TIMEOUT_MS);
|
|
2241
|
+
this.pending.set(id, {
|
|
2242
|
+
resolve: (r) => {
|
|
2243
|
+
clearTimeout(timer);
|
|
2244
|
+
resolve(r);
|
|
2245
|
+
},
|
|
2246
|
+
reject: (e) => {
|
|
2247
|
+
clearTimeout(timer);
|
|
2248
|
+
reject(e);
|
|
2249
|
+
}
|
|
2250
|
+
});
|
|
2251
|
+
const line = JSON.stringify(request) + "\n";
|
|
2252
|
+
try {
|
|
2253
|
+
const proc = this.process;
|
|
2254
|
+
if (!proc || !proc.stdin) {
|
|
2255
|
+
throw new Error("Sidecar process not running");
|
|
2256
|
+
}
|
|
2257
|
+
proc.stdin.write(line);
|
|
2258
|
+
} catch (err) {
|
|
2259
|
+
clearTimeout(timer);
|
|
2260
|
+
this.pending.delete(id);
|
|
2261
|
+
reject(new Error(`Failed to write to sidecar stdin: ${err instanceof Error ? err.message : String(err)}`));
|
|
2262
|
+
}
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
shutdown() {
|
|
2266
|
+
if (this.process) {
|
|
2267
|
+
this.process.kill("SIGTERM");
|
|
2268
|
+
this.process = null;
|
|
2269
|
+
}
|
|
2270
|
+
for (const [id, { reject }] of this.pending.entries()) {
|
|
2271
|
+
reject(new Error("Sidecar shut down"));
|
|
2272
|
+
this.pending.delete(id);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
// -------------------------------------------------------------------------
|
|
2276
|
+
// Private
|
|
2277
|
+
// -------------------------------------------------------------------------
|
|
2278
|
+
async ensureRunning() {
|
|
2279
|
+
if (this.process && !this.process.killed) return;
|
|
2280
|
+
if (!this.spawnPromise) {
|
|
2281
|
+
this.spawnPromise = this.spawn().finally(() => {
|
|
2282
|
+
this.spawnPromise = null;
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
await this.spawnPromise;
|
|
2286
|
+
}
|
|
2287
|
+
async spawn() {
|
|
2288
|
+
const child = spawn(VENV_PYTHON, [SIDECAR_SCRIPT_PATH], {
|
|
2289
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2290
|
+
});
|
|
2291
|
+
if (child.stdout) {
|
|
2292
|
+
child.stdout.setEncoding("utf8");
|
|
2293
|
+
child.stdout.on("data", (chunk) => this.onStdout(chunk));
|
|
2294
|
+
}
|
|
2295
|
+
if (child.stderr) {
|
|
2296
|
+
child.stderr.setEncoding("utf8");
|
|
2297
|
+
child.stderr.on("data", (data) => {
|
|
2298
|
+
process.stderr.write(`[reaper-mcp-sidecar] ${data}`);
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
2301
|
+
child.on("exit", (code) => {
|
|
2302
|
+
process.stderr.write(`[reaper-mcp] Sidecar process exited (code ${code})
|
|
2303
|
+
`);
|
|
2304
|
+
this.process = null;
|
|
2305
|
+
this.handleProcessDeath();
|
|
2306
|
+
});
|
|
2307
|
+
child.on("error", (err) => {
|
|
2308
|
+
process.stderr.write(`[reaper-mcp] Sidecar spawn error: ${err.message}
|
|
2309
|
+
`);
|
|
2310
|
+
this.process = null;
|
|
2311
|
+
this.handleProcessDeath();
|
|
2312
|
+
});
|
|
2313
|
+
this.process = child;
|
|
2314
|
+
this.lineBuffer = "";
|
|
2315
|
+
this.restarting = false;
|
|
2316
|
+
this.restartAttempted = false;
|
|
2317
|
+
}
|
|
2318
|
+
onStdout(chunk) {
|
|
2319
|
+
this.lineBuffer += chunk;
|
|
2320
|
+
let newlineIdx;
|
|
2321
|
+
while ((newlineIdx = this.lineBuffer.indexOf("\n")) !== -1) {
|
|
2322
|
+
const line = this.lineBuffer.slice(0, newlineIdx).trim();
|
|
2323
|
+
this.lineBuffer = this.lineBuffer.slice(newlineIdx + 1);
|
|
2324
|
+
if (line) this.onResponse(line);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
onResponse(line) {
|
|
2328
|
+
let response;
|
|
2329
|
+
try {
|
|
2330
|
+
response = JSON.parse(line);
|
|
2331
|
+
} catch {
|
|
2332
|
+
process.stderr.write(`[reaper-mcp] Sidecar returned malformed JSON: ${line}
|
|
2333
|
+
`);
|
|
2334
|
+
for (const [id, { reject }] of this.pending.entries()) {
|
|
2335
|
+
reject(new Error(`Sidecar returned malformed JSON: ${line}`));
|
|
2336
|
+
this.pending.delete(id);
|
|
2337
|
+
}
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
2340
|
+
const pending = this.pending.get(response.id);
|
|
2341
|
+
if (!pending) {
|
|
2342
|
+
process.stderr.write(`[reaper-mcp] Sidecar response for unknown id ${response.id}
|
|
2343
|
+
`);
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
this.pending.delete(response.id);
|
|
2347
|
+
if (response.error) {
|
|
2348
|
+
pending.reject(new Error(response.error.message));
|
|
2349
|
+
} else if (response.result) {
|
|
2350
|
+
pending.resolve(response.result);
|
|
2351
|
+
} else {
|
|
2352
|
+
pending.reject(new Error("Sidecar response missing both result and error fields"));
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
handleProcessDeath() {
|
|
2356
|
+
if (this.restarting || this.pending.size === 0) {
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
if (!this.restartAttempted) {
|
|
2360
|
+
this.restarting = true;
|
|
2361
|
+
this.restartAttempted = true;
|
|
2362
|
+
process.stderr.write("[reaper-mcp] Sidecar crashed; attempting one restart...\n");
|
|
2363
|
+
this.spawn().then(() => {
|
|
2364
|
+
for (const [id, { reject }] of this.pending.entries()) {
|
|
2365
|
+
reject(new Error("Sidecar restarted; please retry the operation"));
|
|
2366
|
+
this.pending.delete(id);
|
|
2367
|
+
}
|
|
2368
|
+
}).catch((err) => {
|
|
2369
|
+
for (const [id, { reject }] of this.pending.entries()) {
|
|
2370
|
+
reject(new Error(`Sidecar restart failed: ${err.message}. Run: node dist/apps/reaper-mcp-server/main.js setup-sidecar`));
|
|
2371
|
+
this.pending.delete(id);
|
|
2372
|
+
}
|
|
2373
|
+
});
|
|
2374
|
+
} else {
|
|
2375
|
+
for (const [id, { reject }] of this.pending.entries()) {
|
|
2376
|
+
reject(new Error("Python sidecar unavailable after restart attempt. Run: node dist/apps/reaper-mcp-server/main.js setup-sidecar"));
|
|
2377
|
+
this.pending.delete(id);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
};
|
|
2382
|
+
var _singleton = null;
|
|
2383
|
+
function getSidecarClient() {
|
|
2384
|
+
if (!_singleton) {
|
|
2385
|
+
_singleton = new SidecarClientImpl();
|
|
2386
|
+
}
|
|
2387
|
+
return _singleton;
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
// apps/reaper-mcp-server/src/tools/aesthetics.ts
|
|
2391
|
+
var SIDECAR_NOT_INSTALLED_MSG = "Audio understanding sidecar not installed. Run: node dist/apps/reaper-mcp-server/main.js setup-sidecar";
|
|
2392
|
+
function registerAestheticsTools(server) {
|
|
2393
|
+
server.tool(
|
|
2394
|
+
"analyze_track_aesthetics",
|
|
2395
|
+
"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).",
|
|
2396
|
+
{
|
|
2397
|
+
trackIndex: z17.coerce.number().int().min(0).describe("Zero-based track index"),
|
|
2398
|
+
startTime: z17.coerce.number().min(0).optional().describe("Start time in seconds (default: 0 or use REAPER time selection)"),
|
|
2399
|
+
endTime: z17.coerce.number().min(0).optional().describe("End time in seconds (default: startTime + durationSeconds)"),
|
|
2400
|
+
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.")
|
|
2401
|
+
},
|
|
2402
|
+
async ({ trackIndex, startTime, endTime, durationSeconds }) => {
|
|
2403
|
+
const sidecar = getSidecarClient();
|
|
2404
|
+
if (!sidecar.isAvailable()) {
|
|
2405
|
+
return {
|
|
2406
|
+
content: [{ type: "text", text: SIDECAR_NOT_INSTALLED_MSG }],
|
|
2407
|
+
isError: true
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
const resolvedStart = startTime ?? 0;
|
|
2411
|
+
const resolvedEnd = endTime ?? resolvedStart + (durationSeconds ?? 5);
|
|
2412
|
+
if (resolvedEnd <= resolvedStart) {
|
|
2413
|
+
return {
|
|
2414
|
+
content: [{ type: "text", text: "endTime must be greater than startTime" }],
|
|
2415
|
+
isError: true
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
const commandId = randomUUID2();
|
|
2419
|
+
const renderRes = await sendCommand("render_track_to_wav", {
|
|
2420
|
+
trackIndex,
|
|
2421
|
+
startTime: resolvedStart,
|
|
2422
|
+
endTime: resolvedEnd,
|
|
2423
|
+
commandId
|
|
2424
|
+
});
|
|
2425
|
+
if (!renderRes.success) {
|
|
2426
|
+
return {
|
|
2427
|
+
content: [{ type: "text", text: `Render failed: ${renderRes.error}` }],
|
|
2428
|
+
isError: true
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
const renderData = renderRes.data;
|
|
2432
|
+
const wavPath = renderData.wavPath;
|
|
2433
|
+
try {
|
|
2434
|
+
const scores = await sidecar.analyze(wavPath, resolvedStart, resolvedEnd);
|
|
2435
|
+
const result = {
|
|
2436
|
+
trackIndex,
|
|
2437
|
+
trackName: renderData.trackName,
|
|
2438
|
+
productionQuality: scores.PQ,
|
|
2439
|
+
productionComplexity: scores.PC,
|
|
2440
|
+
contentEnjoyment: scores.CE,
|
|
2441
|
+
contentUsefulness: scores.CU,
|
|
2442
|
+
startTime: resolvedStart,
|
|
2443
|
+
endTime: resolvedEnd,
|
|
2444
|
+
durationSeconds: resolvedEnd - resolvedStart,
|
|
2445
|
+
modelVersion: scores.modelVersion
|
|
2446
|
+
};
|
|
2447
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
2448
|
+
} catch (err) {
|
|
2449
|
+
return {
|
|
2450
|
+
content: [{
|
|
2451
|
+
type: "text",
|
|
2452
|
+
text: err instanceof Error ? err.message : String(err)
|
|
2453
|
+
}],
|
|
2454
|
+
isError: true
|
|
2455
|
+
};
|
|
2456
|
+
} finally {
|
|
2457
|
+
await unlink2(wavPath).catch(() => {
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
);
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2182
2464
|
// apps/reaper-mcp-server/src/server.ts
|
|
2183
2465
|
function instrumentToolHandlers(server) {
|
|
2184
2466
|
const originalTool = server.tool.bind(server);
|
|
@@ -2238,40 +2520,43 @@ function createServer() {
|
|
|
2238
2520
|
registerEnvelopeTools(server);
|
|
2239
2521
|
registerBatchTools(server);
|
|
2240
2522
|
registerCategoryTools(server);
|
|
2523
|
+
registerAestheticsTools(server);
|
|
2241
2524
|
return server;
|
|
2242
2525
|
}
|
|
2243
2526
|
|
|
2244
2527
|
// apps/reaper-mcp-server/src/main.ts
|
|
2245
|
-
import { existsSync as
|
|
2246
|
-
import { join as
|
|
2247
|
-
import { fileURLToPath as
|
|
2248
|
-
import { homedir as
|
|
2528
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4 } from "node:fs";
|
|
2529
|
+
import { join as join7, dirname as dirname3 } from "node:path";
|
|
2530
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2531
|
+
import { homedir as homedir5, platform as platform4 } from "node:os";
|
|
2532
|
+
import { exec as execCb } from "node:child_process";
|
|
2533
|
+
import { promisify as promisifyUtil } from "node:util";
|
|
2249
2534
|
|
|
2250
2535
|
// apps/reaper-mcp-server/src/cli.ts
|
|
2251
|
-
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2252
|
-
import { join as
|
|
2536
|
+
import { copyFileSync, existsSync as existsSync2, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2537
|
+
import { join as join4 } from "node:path";
|
|
2253
2538
|
function resolveAssetDir(baseDir, name) {
|
|
2254
|
-
const sibling =
|
|
2255
|
-
if (
|
|
2256
|
-
const parent =
|
|
2257
|
-
if (
|
|
2539
|
+
const sibling = join4(baseDir, name);
|
|
2540
|
+
if (existsSync2(sibling)) return sibling;
|
|
2541
|
+
const parent = join4(baseDir, "..", name);
|
|
2542
|
+
if (existsSync2(parent)) return parent;
|
|
2258
2543
|
let dir = baseDir;
|
|
2259
2544
|
for (let i = 0; i < 5; i++) {
|
|
2260
|
-
const candidate =
|
|
2261
|
-
if (
|
|
2262
|
-
const up =
|
|
2545
|
+
const candidate = join4(dir, name);
|
|
2546
|
+
if (existsSync2(candidate)) return candidate;
|
|
2547
|
+
const up = join4(dir, "..");
|
|
2263
2548
|
if (up === dir) break;
|
|
2264
2549
|
dir = up;
|
|
2265
2550
|
}
|
|
2266
2551
|
return sibling;
|
|
2267
2552
|
}
|
|
2268
2553
|
function copyDirSync(src, dest) {
|
|
2269
|
-
if (!
|
|
2554
|
+
if (!existsSync2(src)) return 0;
|
|
2270
2555
|
mkdirSync(dest, { recursive: true });
|
|
2271
2556
|
let count = 0;
|
|
2272
2557
|
for (const entry of readdirSync(src)) {
|
|
2273
|
-
const srcPath =
|
|
2274
|
-
const destPath =
|
|
2558
|
+
const srcPath = join4(src, entry);
|
|
2559
|
+
const destPath = join4(dest, entry);
|
|
2275
2560
|
if (statSync(srcPath).isDirectory()) {
|
|
2276
2561
|
count += copyDirSync(srcPath, destPath);
|
|
2277
2562
|
} else {
|
|
@@ -2282,14 +2567,14 @@ function copyDirSync(src, dest) {
|
|
|
2282
2567
|
return count;
|
|
2283
2568
|
}
|
|
2284
2569
|
function installFile(src, dest) {
|
|
2285
|
-
if (
|
|
2570
|
+
if (existsSync2(src)) {
|
|
2286
2571
|
copyFileSync(src, dest);
|
|
2287
2572
|
return true;
|
|
2288
2573
|
}
|
|
2289
2574
|
return false;
|
|
2290
2575
|
}
|
|
2291
2576
|
function createMcpJson(targetPath) {
|
|
2292
|
-
if (
|
|
2577
|
+
if (existsSync2(targetPath)) return false;
|
|
2293
2578
|
const config = JSON.stringify({
|
|
2294
2579
|
mcpServers: {
|
|
2295
2580
|
reaper: {
|
|
@@ -2412,12 +2697,16 @@ var MCP_TOOL_NAMES = [
|
|
|
2412
2697
|
// progressive discovery
|
|
2413
2698
|
"list_tool_categories",
|
|
2414
2699
|
"enable_tool_category",
|
|
2415
|
-
"disable_tool_category"
|
|
2700
|
+
"disable_tool_category",
|
|
2701
|
+
// semantic audio analysis (requires Python sidecar — opt-in)
|
|
2702
|
+
"analyze_track_aesthetics"
|
|
2703
|
+
// Note: 'render_track_to_wav' is an internal Lua bridge command, NOT a public MCP tool.
|
|
2704
|
+
// It must NOT be listed here — MCP_TOOL_NAMES drives the Claude Code allowlist.
|
|
2416
2705
|
];
|
|
2417
2706
|
function ensureClaudeSettings(settingsPath) {
|
|
2418
2707
|
const allowList = MCP_TOOL_NAMES.map((t) => `mcp__reaper__${t}`);
|
|
2419
|
-
if (!
|
|
2420
|
-
mkdirSync(
|
|
2708
|
+
if (!existsSync2(settingsPath)) {
|
|
2709
|
+
mkdirSync(join4(settingsPath, ".."), { recursive: true });
|
|
2421
2710
|
const config = { permissions: { allow: allowList } };
|
|
2422
2711
|
writeFileSync(settingsPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2423
2712
|
return "created";
|
|
@@ -2434,21 +2723,21 @@ function ensureClaudeSettings(settingsPath) {
|
|
|
2434
2723
|
}
|
|
2435
2724
|
function resolveAssetDirWithFallback(baseDir, buildName, sourceName) {
|
|
2436
2725
|
const resolved = resolveAssetDir(baseDir, buildName);
|
|
2437
|
-
if (
|
|
2726
|
+
if (existsSync2(resolved)) return resolved;
|
|
2438
2727
|
return resolveAssetDir(baseDir, sourceName);
|
|
2439
2728
|
}
|
|
2440
2729
|
|
|
2441
2730
|
// apps/reaper-mcp-server/src/init.ts
|
|
2442
2731
|
import { checkbox, select } from "@inquirer/prompts";
|
|
2443
|
-
import { existsSync as
|
|
2444
|
-
import { join as
|
|
2445
|
-
import { homedir as
|
|
2732
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "node:fs";
|
|
2733
|
+
import { join as join5 } from "node:path";
|
|
2734
|
+
import { homedir as homedir3 } from "node:os";
|
|
2446
2735
|
async function runInit(opts, dirResolver) {
|
|
2447
2736
|
const isTTY = Boolean(process.stdin.isTTY);
|
|
2448
2737
|
const headless = opts.yes || !isTTY;
|
|
2449
2738
|
console.log("REAPER MCP \u2014 Interactive Setup\n");
|
|
2450
2739
|
const bridgeDir = await ensureBridgeDir();
|
|
2451
|
-
console.log(`REAPER resource path: ${
|
|
2740
|
+
console.log(`REAPER resource path: ${join5(bridgeDir, "..", "..")}
|
|
2452
2741
|
`);
|
|
2453
2742
|
let selections;
|
|
2454
2743
|
if (headless) {
|
|
@@ -2493,27 +2782,27 @@ async function runInit(opts, dirResolver) {
|
|
|
2493
2782
|
skillsScope
|
|
2494
2783
|
};
|
|
2495
2784
|
}
|
|
2496
|
-
const
|
|
2785
|
+
const __dirname3 = dirResolver();
|
|
2497
2786
|
if (selections.bridge) {
|
|
2498
2787
|
console.log("Installing REAPER Bridge...");
|
|
2499
2788
|
const scriptsDir = getReaperScriptsPath();
|
|
2500
2789
|
mkdirSync2(scriptsDir, { recursive: true });
|
|
2501
|
-
const reaperDir = resolveAssetDir(
|
|
2790
|
+
const reaperDir = resolveAssetDir(__dirname3, "reaper");
|
|
2502
2791
|
for (const luaFile of ["mcp_bridge.lua", "mcp_snapshot_manager.lua"]) {
|
|
2503
|
-
const src =
|
|
2504
|
-
const dest =
|
|
2792
|
+
const src = join5(reaperDir, luaFile);
|
|
2793
|
+
const dest = join5(scriptsDir, luaFile);
|
|
2505
2794
|
if (installFile(src, dest)) {
|
|
2506
2795
|
console.log(` Installed: ${luaFile}`);
|
|
2507
2796
|
} else {
|
|
2508
2797
|
console.log(` Not found: ${src}`);
|
|
2509
2798
|
}
|
|
2510
2799
|
}
|
|
2511
|
-
const effectsDir =
|
|
2800
|
+
const effectsDir = join5(getReaperEffectsPath(), "reaper-mcp");
|
|
2512
2801
|
mkdirSync2(effectsDir, { recursive: true });
|
|
2513
2802
|
for (const asset of REAPER_ASSETS) {
|
|
2514
2803
|
if (asset.endsWith(".lua")) continue;
|
|
2515
|
-
const src =
|
|
2516
|
-
const dest =
|
|
2804
|
+
const src = join5(reaperDir, asset);
|
|
2805
|
+
const dest = join5(effectsDir, asset);
|
|
2517
2806
|
if (installFile(src, dest)) {
|
|
2518
2807
|
console.log(` Installed: reaper-mcp/${asset}`);
|
|
2519
2808
|
} else {
|
|
@@ -2524,32 +2813,32 @@ async function runInit(opts, dirResolver) {
|
|
|
2524
2813
|
}
|
|
2525
2814
|
if (selections.skills) {
|
|
2526
2815
|
const isGlobal = selections.skillsScope === "global";
|
|
2527
|
-
const baseDir = isGlobal ?
|
|
2528
|
-
const claudeDir = isGlobal ? baseDir :
|
|
2816
|
+
const baseDir = isGlobal ? join5(homedir3(), ".claude") : process.cwd();
|
|
2817
|
+
const claudeDir = isGlobal ? baseDir : join5(baseDir, ".claude");
|
|
2529
2818
|
console.log(`Installing AI Skills & Agents (${selections.skillsScope})...`);
|
|
2530
|
-
const knowledgeSrc = resolveAssetDir(
|
|
2531
|
-
if (
|
|
2532
|
-
const dest =
|
|
2819
|
+
const knowledgeSrc = resolveAssetDir(__dirname3, "knowledge");
|
|
2820
|
+
if (existsSync3(knowledgeSrc)) {
|
|
2821
|
+
const dest = join5(isGlobal ? join5(homedir3(), ".claude") : process.cwd(), "knowledge");
|
|
2533
2822
|
const count = copyDirSync(knowledgeSrc, dest);
|
|
2534
2823
|
console.log(` Installed knowledge base: ${count} files`);
|
|
2535
2824
|
} else {
|
|
2536
2825
|
console.log(" Knowledge base not found in package. Skipping.");
|
|
2537
2826
|
}
|
|
2538
|
-
const rulesSrc = resolveAssetDirWithFallback(
|
|
2539
|
-
if (
|
|
2540
|
-
const dest =
|
|
2827
|
+
const rulesSrc = resolveAssetDirWithFallback(__dirname3, "claude-rules", join5(".claude", "rules"));
|
|
2828
|
+
if (existsSync3(rulesSrc)) {
|
|
2829
|
+
const dest = join5(claudeDir, "rules");
|
|
2541
2830
|
const count = copyDirSync(rulesSrc, dest);
|
|
2542
2831
|
console.log(` Installed Claude rules: ${count} files`);
|
|
2543
2832
|
}
|
|
2544
|
-
const skillsSrc = resolveAssetDirWithFallback(
|
|
2545
|
-
if (
|
|
2546
|
-
const dest =
|
|
2833
|
+
const skillsSrc = resolveAssetDirWithFallback(__dirname3, "claude-skills", join5(".claude", "skills"));
|
|
2834
|
+
if (existsSync3(skillsSrc)) {
|
|
2835
|
+
const dest = join5(claudeDir, "skills");
|
|
2547
2836
|
const count = copyDirSync(skillsSrc, dest);
|
|
2548
2837
|
console.log(` Installed Claude skills: ${count} files`);
|
|
2549
2838
|
}
|
|
2550
|
-
const agentsSrc = resolveAssetDirWithFallback(
|
|
2551
|
-
if (
|
|
2552
|
-
const dest =
|
|
2839
|
+
const agentsSrc = resolveAssetDirWithFallback(__dirname3, "claude-agents", join5(".claude", "agents"));
|
|
2840
|
+
if (existsSync3(agentsSrc)) {
|
|
2841
|
+
const dest = join5(claudeDir, "agents");
|
|
2553
2842
|
const count = copyDirSync(agentsSrc, dest);
|
|
2554
2843
|
console.log(` Installed Claude agents: ${count} files`);
|
|
2555
2844
|
}
|
|
@@ -2557,8 +2846,8 @@ async function runInit(opts, dirResolver) {
|
|
|
2557
2846
|
}
|
|
2558
2847
|
if (selections.settings) {
|
|
2559
2848
|
console.log("Configuring Claude Code Settings...");
|
|
2560
|
-
const settingsDir =
|
|
2561
|
-
const settingsPath =
|
|
2849
|
+
const settingsDir = join5(homedir3(), ".claude");
|
|
2850
|
+
const settingsPath = join5(settingsDir, "settings.json");
|
|
2562
2851
|
const result = ensureClaudeSettings(settingsPath);
|
|
2563
2852
|
if (result === "created") {
|
|
2564
2853
|
console.log(` Created: ${settingsPath}`);
|
|
@@ -2571,7 +2860,7 @@ async function runInit(opts, dirResolver) {
|
|
|
2571
2860
|
}
|
|
2572
2861
|
if (selections.projectConfig) {
|
|
2573
2862
|
console.log("Creating Project Config...");
|
|
2574
|
-
const mcpJsonPath =
|
|
2863
|
+
const mcpJsonPath = join5(process.cwd(), ".mcp.json");
|
|
2575
2864
|
if (createMcpJson(mcpJsonPath)) {
|
|
2576
2865
|
console.log(` Created: ${mcpJsonPath}`);
|
|
2577
2866
|
} else {
|
|
@@ -2582,18 +2871,18 @@ async function runInit(opts, dirResolver) {
|
|
|
2582
2871
|
console.log("Running system check...");
|
|
2583
2872
|
const bridgeRunning = await isBridgeRunning();
|
|
2584
2873
|
console.log(` Lua bridge: ${bridgeRunning ? "Connected" : "Not detected (start after REAPER is open)"}`);
|
|
2585
|
-
const globalClaudeDir =
|
|
2586
|
-
const localAgents =
|
|
2587
|
-
const globalAgents =
|
|
2874
|
+
const globalClaudeDir = join5(homedir3(), ".claude");
|
|
2875
|
+
const localAgents = existsSync3(join5(process.cwd(), ".claude", "agents"));
|
|
2876
|
+
const globalAgents = existsSync3(join5(globalClaudeDir, "agents"));
|
|
2588
2877
|
const agentsExist = localAgents || globalAgents;
|
|
2589
2878
|
console.log(` Mix agents: ${agentsExist ? "Installed" : "Not installed"}`);
|
|
2590
|
-
const mcpJsonExists =
|
|
2879
|
+
const mcpJsonExists = existsSync3(join5(process.cwd(), ".mcp.json"));
|
|
2591
2880
|
console.log(` MCP config: ${mcpJsonExists ? ".mcp.json found" : ".mcp.json not present"}`);
|
|
2592
2881
|
console.log("");
|
|
2593
2882
|
console.log("Setup complete! Next steps:");
|
|
2594
2883
|
if (selections.bridge) {
|
|
2595
2884
|
const scriptsDir = getReaperScriptsPath();
|
|
2596
|
-
const luaDest =
|
|
2885
|
+
const luaDest = join5(scriptsDir, "mcp_bridge.lua");
|
|
2597
2886
|
console.log(" 1. Open REAPER");
|
|
2598
2887
|
console.log(" 2. Actions > Show action list > Load ReaScript");
|
|
2599
2888
|
console.log(` 3. Select: ${luaDest}`);
|
|
@@ -2608,33 +2897,206 @@ async function runInit(opts, dirResolver) {
|
|
|
2608
2897
|
}
|
|
2609
2898
|
}
|
|
2610
2899
|
|
|
2611
|
-
// apps/reaper-mcp-server/src/
|
|
2900
|
+
// apps/reaper-mcp-server/src/setup-sidecar.ts
|
|
2901
|
+
import { exec as execImpl } from "node:child_process";
|
|
2902
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, copyFileSync as copyFileSync2, rmSync } from "node:fs";
|
|
2903
|
+
import { join as join6, dirname as dirname2 } from "node:path";
|
|
2904
|
+
import { homedir as homedir4, platform as platform3 } from "node:os";
|
|
2905
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2906
|
+
var VENV_BIN2 = platform3() === "win32" ? "Scripts" : "bin";
|
|
2907
|
+
var VENV_PYTHON_NAME2 = platform3() === "win32" ? "python.exe" : "python";
|
|
2908
|
+
var VENV_PIP_NAME = platform3() === "win32" ? "pip.exe" : "pip";
|
|
2909
|
+
function exec(cmd) {
|
|
2910
|
+
return new Promise((resolve, reject) => {
|
|
2911
|
+
execImpl(cmd, (err, stdout, stderr) => {
|
|
2912
|
+
if (err) reject(err);
|
|
2913
|
+
else resolve({ stdout, stderr });
|
|
2914
|
+
});
|
|
2915
|
+
});
|
|
2916
|
+
}
|
|
2612
2917
|
var __dirname = dirname2(fileURLToPath2(import.meta.url));
|
|
2918
|
+
var SIDECAR_VENV_PATH2 = join6(homedir4(), ".reaper-mcp", "sidecar-venv");
|
|
2919
|
+
var SIDECAR_DIR = join6(homedir4(), ".reaper-mcp", "sidecar");
|
|
2920
|
+
var SIDECAR_SCRIPT_DEST = join6(SIDECAR_DIR, "server.py");
|
|
2921
|
+
var MODEL_ID = "facebook/audiobox-aesthetics";
|
|
2922
|
+
var MIN_PYTHON_MAJOR = 3;
|
|
2923
|
+
var MIN_PYTHON_MINOR = 10;
|
|
2924
|
+
function getPythonBin() {
|
|
2925
|
+
return process.env["PYTHON_BIN"] ?? "python3";
|
|
2926
|
+
}
|
|
2927
|
+
function parsePythonVersion(output) {
|
|
2928
|
+
const match = output.match(/Python\s+(\d+)\.(\d+)\.(\d+)/i);
|
|
2929
|
+
if (!match) return null;
|
|
2930
|
+
return [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10)];
|
|
2931
|
+
}
|
|
2932
|
+
async function checkPython(pythonBin) {
|
|
2933
|
+
let stdout;
|
|
2934
|
+
try {
|
|
2935
|
+
const result = await exec(`"${pythonBin}" --version`);
|
|
2936
|
+
stdout = (result.stdout + result.stderr).trim();
|
|
2937
|
+
} catch {
|
|
2938
|
+
const notFound = `Python interpreter not found: ${pythonBin}
|
|
2939
|
+
Install Python 3.10+ from https://python.org or set the PYTHON_BIN environment variable.
|
|
2940
|
+
Example: PYTHON_BIN=/usr/local/bin/python3.11 node dist/apps/reaper-mcp-server/main.js setup-sidecar`;
|
|
2941
|
+
throw new Error(notFound);
|
|
2942
|
+
}
|
|
2943
|
+
const version = parsePythonVersion(stdout);
|
|
2944
|
+
if (!version) {
|
|
2945
|
+
throw new Error(`Could not parse Python version from: "${stdout}". Is PYTHON_BIN set correctly?`);
|
|
2946
|
+
}
|
|
2947
|
+
const [major, minor, patch] = version;
|
|
2948
|
+
if (major < MIN_PYTHON_MAJOR || major === MIN_PYTHON_MAJOR && minor < MIN_PYTHON_MINOR) {
|
|
2949
|
+
throw new Error(
|
|
2950
|
+
`Python ${MIN_PYTHON_MAJOR}.${MIN_PYTHON_MINOR}+ required. Found: ${major}.${minor}.${patch} at ${pythonBin}.
|
|
2951
|
+
Install Python 3.10+ from https://python.org or set PYTHON_BIN to a newer interpreter.`
|
|
2952
|
+
);
|
|
2953
|
+
}
|
|
2954
|
+
return `${major}.${minor}.${patch}`;
|
|
2955
|
+
}
|
|
2956
|
+
async function createVenv(pythonBin) {
|
|
2957
|
+
mkdirSync3(join6(homedir4(), ".reaper-mcp"), { recursive: true });
|
|
2958
|
+
await exec(`"${pythonBin}" -m venv --clear "${SIDECAR_VENV_PATH2}"`);
|
|
2959
|
+
}
|
|
2960
|
+
function cleanupBrokenVenv() {
|
|
2961
|
+
try {
|
|
2962
|
+
if (existsSync4(SIDECAR_VENV_PATH2)) {
|
|
2963
|
+
rmSync(SIDECAR_VENV_PATH2, { recursive: true, force: true });
|
|
2964
|
+
}
|
|
2965
|
+
} catch {
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
function resolveRequirementsTxt(baseDir) {
|
|
2969
|
+
const sidecarDir = resolveAssetDir(baseDir, "sidecar");
|
|
2970
|
+
return join6(sidecarDir, "requirements.txt");
|
|
2971
|
+
}
|
|
2972
|
+
async function installDeps(requirementsPath) {
|
|
2973
|
+
const pip = join6(SIDECAR_VENV_PATH2, VENV_BIN2, VENV_PIP_NAME);
|
|
2974
|
+
await exec(`"${pip}" install --upgrade pip`);
|
|
2975
|
+
const { stdout, stderr } = await exec(`"${pip}" install -r "${requirementsPath}"`);
|
|
2976
|
+
if (stdout) process.stdout.write(stdout);
|
|
2977
|
+
if (stderr) process.stderr.write(stderr);
|
|
2978
|
+
}
|
|
2979
|
+
async function downloadModelWeights() {
|
|
2980
|
+
const python = join6(SIDECAR_VENV_PATH2, VENV_BIN2, VENV_PYTHON_NAME2);
|
|
2981
|
+
const script = [
|
|
2982
|
+
"from huggingface_hub import snapshot_download",
|
|
2983
|
+
`print("Downloading ${MODEL_ID} weights to ~/.cache/huggingface/hub/ ...")`,
|
|
2984
|
+
`snapshot_download("${MODEL_ID}")`,
|
|
2985
|
+
`print("Download complete.")`
|
|
2986
|
+
].join("; ");
|
|
2987
|
+
const { stdout, stderr } = await exec(`"${python}" -c "${script}"`);
|
|
2988
|
+
if (stdout) process.stdout.write(stdout);
|
|
2989
|
+
if (stderr) process.stderr.write(stderr);
|
|
2990
|
+
}
|
|
2991
|
+
function copySidecarScript(baseDir) {
|
|
2992
|
+
const sidecarDir = resolveAssetDir(baseDir, "sidecar");
|
|
2993
|
+
const src = join6(sidecarDir, "server.py");
|
|
2994
|
+
if (!existsSync4(src)) {
|
|
2995
|
+
throw new Error(
|
|
2996
|
+
`sidecar/server.py not found at: ${src}
|
|
2997
|
+
Run \`pnpm nx build reaper-mcp-server\` first.`
|
|
2998
|
+
);
|
|
2999
|
+
}
|
|
3000
|
+
mkdirSync3(SIDECAR_DIR, { recursive: true });
|
|
3001
|
+
copyFileSync2(src, SIDECAR_SCRIPT_DEST);
|
|
3002
|
+
}
|
|
3003
|
+
async function setupSidecar() {
|
|
3004
|
+
console.log("REAPER MCP \u2014 Setup Python Sidecar\n");
|
|
3005
|
+
console.log("This installs the opt-in audio AI subsystem for perceptual analysis.");
|
|
3006
|
+
console.log("Requires Python 3.10+ and an internet connection (~831 MB download).\n");
|
|
3007
|
+
const pythonBin = getPythonBin();
|
|
3008
|
+
console.log(`Checking Python interpreter: ${pythonBin}`);
|
|
3009
|
+
let version;
|
|
3010
|
+
try {
|
|
3011
|
+
version = await checkPython(pythonBin);
|
|
3012
|
+
} catch (err) {
|
|
3013
|
+
console.error(`
|
|
3014
|
+
Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
3015
|
+
process.exit(1);
|
|
3016
|
+
}
|
|
3017
|
+
console.log(` Using Python interpreter: ${pythonBin} (${version})`);
|
|
3018
|
+
console.log(`
|
|
3019
|
+
Creating virtual environment at: ${SIDECAR_VENV_PATH2}`);
|
|
3020
|
+
try {
|
|
3021
|
+
await createVenv(pythonBin);
|
|
3022
|
+
console.log(" Virtual environment created.");
|
|
3023
|
+
} catch (err) {
|
|
3024
|
+
console.error(`
|
|
3025
|
+
Failed to create venv: ${err instanceof Error ? err.message : String(err)}`);
|
|
3026
|
+
process.exit(1);
|
|
3027
|
+
}
|
|
3028
|
+
const requirementsPath = resolveRequirementsTxt(__dirname);
|
|
3029
|
+
console.log(`
|
|
3030
|
+
Installing Python dependencies from: ${requirementsPath}`);
|
|
3031
|
+
if (!existsSync4(requirementsPath)) {
|
|
3032
|
+
console.error(`
|
|
3033
|
+
Error: requirements.txt not found at: ${requirementsPath}`);
|
|
3034
|
+
console.error("Run `pnpm nx build reaper-mcp-server` first.");
|
|
3035
|
+
process.exit(1);
|
|
3036
|
+
}
|
|
3037
|
+
try {
|
|
3038
|
+
await installDeps(requirementsPath);
|
|
3039
|
+
console.log(" Dependencies installed.");
|
|
3040
|
+
} catch (err) {
|
|
3041
|
+
console.error(`
|
|
3042
|
+
Failed to install dependencies: ${err instanceof Error ? err.message : String(err)}`);
|
|
3043
|
+
cleanupBrokenVenv();
|
|
3044
|
+
console.error("Removed partial venv so the next setup-sidecar run starts clean.");
|
|
3045
|
+
process.exit(1);
|
|
3046
|
+
}
|
|
3047
|
+
console.log("\nPre-downloading Audiobox Aesthetics model weights (~831 MB)...");
|
|
3048
|
+
console.log("This may take several minutes depending on your internet connection.");
|
|
3049
|
+
try {
|
|
3050
|
+
await downloadModelWeights();
|
|
3051
|
+
} catch (err) {
|
|
3052
|
+
console.error(`
|
|
3053
|
+
Model download failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3054
|
+
console.error("Check your internet connection and run setup-sidecar again.");
|
|
3055
|
+
process.exit(1);
|
|
3056
|
+
}
|
|
3057
|
+
console.log(`
|
|
3058
|
+
Installing sidecar script to: ${SIDECAR_SCRIPT_DEST}`);
|
|
3059
|
+
try {
|
|
3060
|
+
copySidecarScript(__dirname);
|
|
3061
|
+
console.log(" Sidecar script installed.");
|
|
3062
|
+
} catch (err) {
|
|
3063
|
+
console.error(`
|
|
3064
|
+
Failed to install sidecar script: ${err instanceof Error ? err.message : String(err)}`);
|
|
3065
|
+
process.exit(1);
|
|
3066
|
+
}
|
|
3067
|
+
console.log("\nSidecar setup complete!\n");
|
|
3068
|
+
console.log("The analyze_track_aesthetics tool is now available.");
|
|
3069
|
+
console.log("Run `node dist/apps/reaper-mcp-server/main.js doctor` to verify.\n");
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
// apps/reaper-mcp-server/src/main.ts
|
|
3073
|
+
var execAsync = promisifyUtil(execCb);
|
|
3074
|
+
var __dirname2 = dirname3(fileURLToPath3(import.meta.url));
|
|
2613
3075
|
async function setup() {
|
|
2614
3076
|
console.log("REAPER MCP Server \u2014 Setup\n");
|
|
2615
3077
|
const bridgeDir = await ensureBridgeDir();
|
|
2616
3078
|
console.log(`Bridge directory: ${bridgeDir}
|
|
2617
3079
|
`);
|
|
2618
3080
|
const scriptsDir = getReaperScriptsPath();
|
|
2619
|
-
|
|
2620
|
-
const reaperDir = resolveAssetDir(
|
|
3081
|
+
mkdirSync4(scriptsDir, { recursive: true });
|
|
3082
|
+
const reaperDir = resolveAssetDir(__dirname2, "reaper");
|
|
2621
3083
|
console.log("Installing Lua scripts...");
|
|
2622
3084
|
for (const luaFile of ["mcp_bridge.lua", "mcp_snapshot_manager.lua"]) {
|
|
2623
|
-
const src =
|
|
2624
|
-
const dest =
|
|
3085
|
+
const src = join7(reaperDir, luaFile);
|
|
3086
|
+
const dest = join7(scriptsDir, luaFile);
|
|
2625
3087
|
if (installFile(src, dest)) {
|
|
2626
3088
|
console.log(` Installed: ${luaFile}`);
|
|
2627
3089
|
} else {
|
|
2628
3090
|
console.log(` Not found: ${src}`);
|
|
2629
3091
|
}
|
|
2630
3092
|
}
|
|
2631
|
-
const effectsDir =
|
|
2632
|
-
|
|
3093
|
+
const effectsDir = join7(getReaperEffectsPath(), "reaper-mcp");
|
|
3094
|
+
mkdirSync4(effectsDir, { recursive: true });
|
|
2633
3095
|
console.log("\nInstalling JSFX analyzers...");
|
|
2634
3096
|
for (const asset of REAPER_ASSETS) {
|
|
2635
3097
|
if (asset.endsWith(".lua")) continue;
|
|
2636
|
-
const src =
|
|
2637
|
-
const dest =
|
|
3098
|
+
const src = join7(reaperDir, asset);
|
|
3099
|
+
const dest = join7(effectsDir, asset);
|
|
2638
3100
|
if (installFile(src, dest)) {
|
|
2639
3101
|
console.log(` Installed: reaper-mcp/${asset}`);
|
|
2640
3102
|
} else {
|
|
@@ -2645,7 +3107,7 @@ async function setup() {
|
|
|
2645
3107
|
console.log("Next steps:");
|
|
2646
3108
|
console.log(" 1. Open REAPER");
|
|
2647
3109
|
console.log(" 2. Actions > Show action list > Load ReaScript");
|
|
2648
|
-
console.log(` 3. Select: ${
|
|
3110
|
+
console.log(` 3. Select: ${join7(scriptsDir, "mcp_bridge.lua")}`);
|
|
2649
3111
|
console.log(" 4. Run the script (it will keep running in background via defer loop)");
|
|
2650
3112
|
console.log(" 5. Add reaper-mcp to your Claude Code config (see: npx @mthines/reaper-mcp doctor)");
|
|
2651
3113
|
}
|
|
@@ -2657,41 +3119,41 @@ async function installSkills(scope) {
|
|
|
2657
3119
|
console.log(`REAPER MCP \u2014 Install AI Mix Engineer Skills (scope: ${scope})
|
|
2658
3120
|
`);
|
|
2659
3121
|
const isGlobal = scope === "global";
|
|
2660
|
-
const baseDir = isGlobal ?
|
|
2661
|
-
const claudeDir = isGlobal ? baseDir :
|
|
2662
|
-
const knowledgeSrc = resolveAssetDir(
|
|
2663
|
-
if (
|
|
2664
|
-
const dest =
|
|
3122
|
+
const baseDir = isGlobal ? join7(homedir5(), ".claude") : process.cwd();
|
|
3123
|
+
const claudeDir = isGlobal ? baseDir : join7(baseDir, ".claude");
|
|
3124
|
+
const knowledgeSrc = resolveAssetDir(__dirname2, "knowledge");
|
|
3125
|
+
if (existsSync5(knowledgeSrc)) {
|
|
3126
|
+
const dest = join7(baseDir, "knowledge");
|
|
2665
3127
|
const count = copyDirSync(knowledgeSrc, dest);
|
|
2666
3128
|
console.log(`Installed knowledge base: ${count} files \u2192 ${dest}`);
|
|
2667
3129
|
} else {
|
|
2668
3130
|
console.log("Knowledge base not found in package. Skipping.");
|
|
2669
3131
|
}
|
|
2670
|
-
const rulesSrc = resolveAssetDirWithFallback(
|
|
2671
|
-
if (
|
|
2672
|
-
const dest =
|
|
3132
|
+
const rulesSrc = resolveAssetDirWithFallback(__dirname2, "claude-rules", join7(".claude", "rules"));
|
|
3133
|
+
if (existsSync5(rulesSrc)) {
|
|
3134
|
+
const dest = join7(claudeDir, "rules");
|
|
2673
3135
|
const count = copyDirSync(rulesSrc, dest);
|
|
2674
3136
|
console.log(`Installed Claude rules: ${count} files \u2192 ${dest}`);
|
|
2675
3137
|
} else {
|
|
2676
3138
|
console.log("Claude rules not found in package. Skipping.");
|
|
2677
3139
|
}
|
|
2678
|
-
const skillsSrc = resolveAssetDirWithFallback(
|
|
2679
|
-
if (
|
|
2680
|
-
const dest =
|
|
3140
|
+
const skillsSrc = resolveAssetDirWithFallback(__dirname2, "claude-skills", join7(".claude", "skills"));
|
|
3141
|
+
if (existsSync5(skillsSrc)) {
|
|
3142
|
+
const dest = join7(claudeDir, "skills");
|
|
2681
3143
|
const count = copyDirSync(skillsSrc, dest);
|
|
2682
3144
|
console.log(`Installed Claude skills: ${count} files \u2192 ${dest}`);
|
|
2683
3145
|
} else {
|
|
2684
3146
|
console.log("Claude skills not found in package. Skipping.");
|
|
2685
3147
|
}
|
|
2686
|
-
const agentsSrc = resolveAssetDirWithFallback(
|
|
2687
|
-
if (
|
|
2688
|
-
const dest =
|
|
3148
|
+
const agentsSrc = resolveAssetDirWithFallback(__dirname2, "claude-agents", join7(".claude", "agents"));
|
|
3149
|
+
if (existsSync5(agentsSrc)) {
|
|
3150
|
+
const dest = join7(claudeDir, "agents");
|
|
2689
3151
|
const count = copyDirSync(agentsSrc, dest);
|
|
2690
3152
|
console.log(`Installed Claude agents: ${count} files \u2192 ${dest}`);
|
|
2691
3153
|
} else {
|
|
2692
3154
|
console.log("Claude agents not found in package. Skipping.");
|
|
2693
3155
|
}
|
|
2694
|
-
const settingsPath =
|
|
3156
|
+
const settingsPath = join7(claudeDir, "settings.json");
|
|
2695
3157
|
const result = ensureClaudeSettings(settingsPath);
|
|
2696
3158
|
if (result === "created") {
|
|
2697
3159
|
console.log(`Created Claude settings: ${settingsPath}`);
|
|
@@ -2701,7 +3163,7 @@ async function installSkills(scope) {
|
|
|
2701
3163
|
console.log(`Claude settings already has all REAPER tools: ${settingsPath}`);
|
|
2702
3164
|
}
|
|
2703
3165
|
if (!isGlobal) {
|
|
2704
|
-
const mcpJsonPath =
|
|
3166
|
+
const mcpJsonPath = join7(baseDir, ".mcp.json");
|
|
2705
3167
|
if (createMcpJson(mcpJsonPath)) {
|
|
2706
3168
|
console.log(`
|
|
2707
3169
|
Created: ${mcpJsonPath}`);
|
|
@@ -2722,31 +3184,92 @@ async function doctor() {
|
|
|
2722
3184
|
if (!bridgeRunning) {
|
|
2723
3185
|
console.log(' \u2192 Run "npx @mthines/reaper-mcp setup" then load mcp_bridge.lua in REAPER');
|
|
2724
3186
|
}
|
|
2725
|
-
const globalClaudeDir =
|
|
2726
|
-
const localAgents =
|
|
2727
|
-
const globalAgents =
|
|
3187
|
+
const globalClaudeDir = join7(homedir5(), ".claude");
|
|
3188
|
+
const localAgents = existsSync5(join7(process.cwd(), ".claude", "agents"));
|
|
3189
|
+
const globalAgents = existsSync5(join7(globalClaudeDir, "agents"));
|
|
2728
3190
|
const agentsExist = localAgents || globalAgents;
|
|
2729
3191
|
const agentsLocation = localAgents ? ".claude/agents/" : globalAgents ? "~/.claude/agents/" : "";
|
|
2730
3192
|
console.log(`Mix agents: ${agentsExist ? `\u2713 Found (${agentsLocation})` : "\u2717 Not installed"}`);
|
|
2731
3193
|
if (!agentsExist) {
|
|
2732
3194
|
console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"');
|
|
2733
3195
|
}
|
|
2734
|
-
const localKnowledge =
|
|
2735
|
-
const globalKnowledge =
|
|
3196
|
+
const localKnowledge = existsSync5(join7(process.cwd(), "knowledge"));
|
|
3197
|
+
const globalKnowledge = existsSync5(join7(globalClaudeDir, "knowledge"));
|
|
2736
3198
|
const knowledgeExists = localKnowledge || globalKnowledge;
|
|
2737
3199
|
const knowledgeLocation = localKnowledge ? "project" : globalKnowledge ? "~/.claude/" : "";
|
|
2738
3200
|
console.log(`Knowledge base: ${knowledgeExists ? `\u2713 Found (${knowledgeLocation})` : "\u2717 Not installed"}`);
|
|
2739
3201
|
if (!knowledgeExists) {
|
|
2740
3202
|
console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"');
|
|
2741
3203
|
}
|
|
2742
|
-
const mcpJsonExists =
|
|
3204
|
+
const mcpJsonExists = existsSync5(join7(process.cwd(), ".mcp.json"));
|
|
2743
3205
|
console.log(`MCP config: ${mcpJsonExists ? "\u2713 .mcp.json found" : "\u2717 .mcp.json missing"}`);
|
|
2744
3206
|
if (!mcpJsonExists) {
|
|
2745
3207
|
console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills --project" to create .mcp.json');
|
|
2746
3208
|
}
|
|
2747
3209
|
console.log('\nTo check SWS Extensions, start REAPER and use the "list_available_fx" tool.');
|
|
2748
3210
|
console.log("SWS provides enhanced plugin discovery and snapshot support.\n");
|
|
2749
|
-
|
|
3211
|
+
console.log("Python Sidecar (opt-in, for analyze_track_aesthetics):");
|
|
3212
|
+
const sidecarVenvPath = join7(homedir5(), ".reaper-mcp", "sidecar-venv");
|
|
3213
|
+
const venvBin = platform4() === "win32" ? "Scripts" : "bin";
|
|
3214
|
+
const venvPythonName = platform4() === "win32" ? "python.exe" : "python";
|
|
3215
|
+
const venvPython = join7(sidecarVenvPath, venvBin, venvPythonName);
|
|
3216
|
+
const hfCachePath = join7(homedir5(), ".cache", "huggingface", "hub");
|
|
3217
|
+
let pythonOk = false;
|
|
3218
|
+
let pythonDetail = "not found";
|
|
3219
|
+
try {
|
|
3220
|
+
const pythonBin = process.env["PYTHON_BIN"] ?? "python3";
|
|
3221
|
+
const { stdout, stderr } = await execAsync(`"${pythonBin}" --version`);
|
|
3222
|
+
const versionStr = (stdout + stderr).trim();
|
|
3223
|
+
const match = versionStr.match(/Python\s+(\d+)\.(\d+)/i);
|
|
3224
|
+
if (match) {
|
|
3225
|
+
const major = parseInt(match[1], 10);
|
|
3226
|
+
const minor = parseInt(match[2], 10);
|
|
3227
|
+
pythonOk = major > 3 || major === 3 && minor >= 10;
|
|
3228
|
+
pythonDetail = versionStr;
|
|
3229
|
+
}
|
|
3230
|
+
} catch {
|
|
3231
|
+
pythonDetail = "not found";
|
|
3232
|
+
}
|
|
3233
|
+
console.log(` Python \u2265 3.10: ${pythonOk ? `\u2713 ${pythonDetail}` : `\u2717 ${pythonDetail}`}`);
|
|
3234
|
+
if (!pythonOk) {
|
|
3235
|
+
console.log(" \u2192 Install Python 3.10+ from https://python.org or set PYTHON_BIN");
|
|
3236
|
+
}
|
|
3237
|
+
const venvExists = existsSync5(sidecarVenvPath);
|
|
3238
|
+
console.log(` Venv: ${venvExists ? `\u2713 ${sidecarVenvPath}` : "\u2717 Not installed"}`);
|
|
3239
|
+
if (!venvExists) {
|
|
3240
|
+
console.log(" \u2192 Run: node dist/apps/reaper-mcp-server/main.js setup-sidecar");
|
|
3241
|
+
}
|
|
3242
|
+
let depsOk = false;
|
|
3243
|
+
if (venvExists && existsSync5(venvPython)) {
|
|
3244
|
+
try {
|
|
3245
|
+
await execAsync(`"${venvPython}" -c "import audiobox_aesthetics"`);
|
|
3246
|
+
depsOk = true;
|
|
3247
|
+
} catch {
|
|
3248
|
+
depsOk = false;
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
console.log(` Dependencies: ${depsOk ? "\u2713 audiobox-aesthetics importable" : "\u2717 Not installed"}`);
|
|
3252
|
+
if (!depsOk && venvExists) {
|
|
3253
|
+
console.log(" \u2192 Run: node dist/apps/reaper-mcp-server/main.js setup-sidecar");
|
|
3254
|
+
}
|
|
3255
|
+
const weightsPath = join7(hfCachePath, "models--facebook--audiobox-aesthetics");
|
|
3256
|
+
const weightsExist = existsSync5(weightsPath);
|
|
3257
|
+
console.log(` Model weights: ${weightsExist ? `\u2713 ${weightsPath}` : "\u2717 Not downloaded"}`);
|
|
3258
|
+
if (!weightsExist) {
|
|
3259
|
+
console.log(" \u2192 Run: node dist/apps/reaper-mcp-server/main.js setup-sidecar");
|
|
3260
|
+
}
|
|
3261
|
+
const sidecarReady = venvExists && depsOk && weightsExist;
|
|
3262
|
+
const sidecarOptedIn = venvExists || weightsExist;
|
|
3263
|
+
const sidecarBroken = sidecarOptedIn && !sidecarReady;
|
|
3264
|
+
if (!sidecarOptedIn) {
|
|
3265
|
+
console.log("\n Sidecar: not installed (opt-in). Run setup-sidecar to enable audio AI tools.");
|
|
3266
|
+
} else if (sidecarBroken) {
|
|
3267
|
+
console.log("\n Sidecar: PARTIALLY INSTALLED \u2014 run setup-sidecar to repair.");
|
|
3268
|
+
} else {
|
|
3269
|
+
console.log("\n Sidecar: fully installed and ready.");
|
|
3270
|
+
}
|
|
3271
|
+
console.log("");
|
|
3272
|
+
process.exit(bridgeRunning && knowledgeExists && mcpJsonExists && !sidecarBroken ? 0 : 1);
|
|
2750
3273
|
}
|
|
2751
3274
|
async function serve() {
|
|
2752
3275
|
const log = (...args) => console.error("[reaper-mcp]", ...args);
|
|
@@ -2755,7 +3278,7 @@ async function serve() {
|
|
|
2755
3278
|
startDiagnosticsPoller();
|
|
2756
3279
|
startEventsPoller();
|
|
2757
3280
|
log("Starting REAPER MCP Server...");
|
|
2758
|
-
log(`Entry: ${
|
|
3281
|
+
log(`Entry: ${fileURLToPath3(import.meta.url)}`);
|
|
2759
3282
|
const tracer = getTracer();
|
|
2760
3283
|
await tracer.startActiveSpan("mcp.server.startup", { kind: SpanKind3.INTERNAL }, async (startupSpan) => {
|
|
2761
3284
|
try {
|
|
@@ -2809,7 +3332,7 @@ switch (command) {
|
|
|
2809
3332
|
case "init":
|
|
2810
3333
|
runInit(
|
|
2811
3334
|
{ yes: hasYesFlag, project: hasProjectFlag },
|
|
2812
|
-
() =>
|
|
3335
|
+
() => __dirname2
|
|
2813
3336
|
).catch((err) => {
|
|
2814
3337
|
console.error("Init failed:", err);
|
|
2815
3338
|
process.exit(1);
|
|
@@ -2833,6 +3356,12 @@ switch (command) {
|
|
|
2833
3356
|
process.exit(1);
|
|
2834
3357
|
});
|
|
2835
3358
|
break;
|
|
3359
|
+
case "setup-sidecar":
|
|
3360
|
+
setupSidecar().catch((err) => {
|
|
3361
|
+
console.error("Sidecar setup failed:", err);
|
|
3362
|
+
process.exit(1);
|
|
3363
|
+
});
|
|
3364
|
+
break;
|
|
2836
3365
|
case "status": {
|
|
2837
3366
|
(async () => {
|
|
2838
3367
|
const running = await isBridgeRunning();
|
|
@@ -2858,6 +3387,7 @@ Usage:
|
|
|
2858
3387
|
npx @mthines/reaper-mcp init --yes Non-interactive setup (install everything with defaults)
|
|
2859
3388
|
npx @mthines/reaper-mcp init --project Include .mcp.json in current directory
|
|
2860
3389
|
npx @mthines/reaper-mcp setup Install Lua bridge + JSFX analyzers into REAPER
|
|
3390
|
+
npx @mthines/reaper-mcp setup-sidecar Install Python sidecar for perceptual audio analysis (opt-in, ~831 MB)
|
|
2861
3391
|
npx @mthines/reaper-mcp install-skills Install AI knowledge + agents globally (default)
|
|
2862
3392
|
npx @mthines/reaper-mcp install-skills --project Install into current project directory
|
|
2863
3393
|
npx @mthines/reaper-mcp install-skills --global Install into ~/.claude/ (default)
|
|
@@ -2879,24 +3409,31 @@ Tip: install globally for shorter commands:
|
|
|
2879
3409
|
`);
|
|
2880
3410
|
break;
|
|
2881
3411
|
}
|
|
3412
|
+
function shutdownAll() {
|
|
3413
|
+
stopPollers();
|
|
3414
|
+
try {
|
|
3415
|
+
getSidecarClient().shutdown();
|
|
3416
|
+
} catch {
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
2882
3419
|
process.on("SIGINT", () => {
|
|
2883
3420
|
console.error("[reaper-mcp] Interrupted");
|
|
2884
|
-
|
|
3421
|
+
shutdownAll();
|
|
2885
3422
|
shutdownTelemetry().finally(() => process.exit(0));
|
|
2886
3423
|
});
|
|
2887
3424
|
process.on("SIGTERM", () => {
|
|
2888
3425
|
console.error("[reaper-mcp] Terminated");
|
|
2889
|
-
|
|
3426
|
+
shutdownAll();
|
|
2890
3427
|
shutdownTelemetry().finally(() => process.exit(0));
|
|
2891
3428
|
});
|
|
2892
3429
|
process.on("uncaughtException", (err) => {
|
|
2893
3430
|
console.error("[reaper-mcp] Uncaught exception:", err);
|
|
2894
|
-
|
|
3431
|
+
shutdownAll();
|
|
2895
3432
|
shutdownTelemetry().finally(() => process.exit(1));
|
|
2896
3433
|
});
|
|
2897
3434
|
process.on("unhandledRejection", (reason) => {
|
|
2898
3435
|
console.error("[reaper-mcp] Unhandled rejection:", reason);
|
|
2899
|
-
|
|
3436
|
+
shutdownAll();
|
|
2900
3437
|
shutdownTelemetry().finally(() => process.exit(1));
|
|
2901
3438
|
});
|
|
2902
3439
|
//# sourceMappingURL=main.js.map
|
package/package.json
CHANGED
package/reaper/mcp_bridge.lua
CHANGED
|
@@ -3499,6 +3499,187 @@ function handlers.insert_envelope_points(params)
|
|
|
3499
3499
|
}
|
|
3500
3500
|
end
|
|
3501
3501
|
|
|
3502
|
+
-- =============================================================================
|
|
3503
|
+
-- render_track_to_wav handler
|
|
3504
|
+
--
|
|
3505
|
+
-- Renders a target track's output (post-FX, post-fader) to a temp WAV file.
|
|
3506
|
+
-- Saves and restores all render settings and solo states around the render.
|
|
3507
|
+
-- Blocks the defer loop during render (~0.5-1s) — this is by design.
|
|
3508
|
+
--
|
|
3509
|
+
-- Params:
|
|
3510
|
+
-- trackIndex (number) — zero-based track index
|
|
3511
|
+
-- startTime (number) — render start, seconds from project start
|
|
3512
|
+
-- endTime (number) — render end, seconds from project start
|
|
3513
|
+
-- commandId (string) — used to generate a unique temp filename
|
|
3514
|
+
--
|
|
3515
|
+
-- Returns:
|
|
3516
|
+
-- { trackIndex, trackName, wavPath, durationSeconds, sampleRate, channelCount }
|
|
3517
|
+
-- =============================================================================
|
|
3518
|
+
|
|
3519
|
+
function handlers.render_track_to_wav(params)
|
|
3520
|
+
local track_index = params.trackIndex
|
|
3521
|
+
local start_time = params.startTime
|
|
3522
|
+
local end_time = params.endTime
|
|
3523
|
+
local command_id = params.commandId or tostring(os.time())
|
|
3524
|
+
|
|
3525
|
+
-- Validate inputs
|
|
3526
|
+
if track_index == nil then
|
|
3527
|
+
return nil, "Missing required param: trackIndex"
|
|
3528
|
+
end
|
|
3529
|
+
if start_time == nil or end_time == nil then
|
|
3530
|
+
return nil, "Missing required params: startTime, endTime"
|
|
3531
|
+
end
|
|
3532
|
+
if end_time <= start_time then
|
|
3533
|
+
return nil, "endTime must be greater than startTime"
|
|
3534
|
+
end
|
|
3535
|
+
|
|
3536
|
+
-- Get track
|
|
3537
|
+
local track = reaper.GetTrack(0, track_index)
|
|
3538
|
+
if not track then
|
|
3539
|
+
return nil, "Track " .. tostring(track_index) .. " not found"
|
|
3540
|
+
end
|
|
3541
|
+
local track_name = ({reaper.GetSetMediaTrackInfo_String(track, "P_NAME", "", false)})[2] or ""
|
|
3542
|
+
if track_name == "" then track_name = "Track " .. (track_index + 1) end
|
|
3543
|
+
|
|
3544
|
+
-- Render target is always 44100 Hz (WAV for sidecar inference).
|
|
3545
|
+
-- The actual project sample rate is irrelevant here; Audiobox resamples internally.
|
|
3546
|
+
local sample_rate = 44100
|
|
3547
|
+
|
|
3548
|
+
-- Build unique temp file path
|
|
3549
|
+
-- Use GetTempPath() if available (REAPER 6.29+), fall back to os.tmpname pattern
|
|
3550
|
+
local temp_dir
|
|
3551
|
+
if reaper.GetTempPath then
|
|
3552
|
+
temp_dir = reaper.GetTempPath()
|
|
3553
|
+
else
|
|
3554
|
+
-- Fallback: derive temp dir from os.tmpname()
|
|
3555
|
+
local t = os.tmpname()
|
|
3556
|
+
temp_dir = t:match("^(.+)[/\\][^/\\]+$") or "/tmp"
|
|
3557
|
+
os.remove(t)
|
|
3558
|
+
end
|
|
3559
|
+
|
|
3560
|
+
-- Ensure temp dir ends with separator
|
|
3561
|
+
if temp_dir:sub(-1) ~= "/" and temp_dir:sub(-1) ~= "\\" then
|
|
3562
|
+
temp_dir = temp_dir .. "/"
|
|
3563
|
+
end
|
|
3564
|
+
|
|
3565
|
+
local wav_filename = "mcp_aes_" .. command_id:gsub("-", ""):sub(1, 16) .. ".wav"
|
|
3566
|
+
local wav_path = temp_dir .. wav_filename
|
|
3567
|
+
|
|
3568
|
+
-- -------------------------------------------------------------------------
|
|
3569
|
+
-- Save current render settings
|
|
3570
|
+
-- -------------------------------------------------------------------------
|
|
3571
|
+
local saved_file = ({reaper.GetSetProjectInfo_String(0, "RENDER_FILE", "", false)})[2] or ""
|
|
3572
|
+
local saved_pattern = ({reaper.GetSetProjectInfo_String(0, "RENDER_PATTERN", "", false)})[2] or ""
|
|
3573
|
+
local saved_format = ({reaper.GetSetProjectInfo_String(0, "RENDER_FORMAT", "", false)})[2] or ""
|
|
3574
|
+
local saved_bounds = reaper.GetSetProjectInfo(0, "RENDER_BOUNDSFLAG", 0, false)
|
|
3575
|
+
local saved_settings = reaper.GetSetProjectInfo(0, "RENDER_SETTINGS", 0, false)
|
|
3576
|
+
local saved_start = reaper.GetSetProjectInfo(0, "RENDER_STARTPOS", 0, false)
|
|
3577
|
+
local saved_end = reaper.GetSetProjectInfo(0, "RENDER_ENDPOS", 0, false)
|
|
3578
|
+
local saved_srate = reaper.GetSetProjectInfo(0, "RENDER_SRATE", 0, false)
|
|
3579
|
+
local saved_channels = reaper.GetSetProjectInfo(0, "RENDER_CHANNELS", 0, false)
|
|
3580
|
+
|
|
3581
|
+
-- Save solo states for all tracks
|
|
3582
|
+
local track_count = reaper.CountTracks(0)
|
|
3583
|
+
local saved_solo_states = {}
|
|
3584
|
+
for i = 0, track_count - 1 do
|
|
3585
|
+
local t = reaper.GetTrack(0, i)
|
|
3586
|
+
saved_solo_states[i] = reaper.GetMediaTrackInfo_Value(t, "I_SOLO")
|
|
3587
|
+
end
|
|
3588
|
+
|
|
3589
|
+
-- -------------------------------------------------------------------------
|
|
3590
|
+
-- Apply render settings
|
|
3591
|
+
-- -------------------------------------------------------------------------
|
|
3592
|
+
local ok, restore_err = pcall(function()
|
|
3593
|
+
-- Set output: render to temp dir with unique filename (no extension in pattern)
|
|
3594
|
+
reaper.GetSetProjectInfo_String(0, "RENDER_FILE", temp_dir, true)
|
|
3595
|
+
reaper.GetSetProjectInfo_String(0, "RENDER_PATTERN", wav_filename:match("^(.+)%.wav$") or wav_filename, true)
|
|
3596
|
+
|
|
3597
|
+
-- Set render bounds: 0 = custom time range
|
|
3598
|
+
reaper.GetSetProjectInfo(0, "RENDER_BOUNDSFLAG", 0, true)
|
|
3599
|
+
reaper.GetSetProjectInfo(0, "RENDER_STARTPOS", start_time, true)
|
|
3600
|
+
reaper.GetSetProjectInfo(0, "RENDER_ENDPOS", end_time, true)
|
|
3601
|
+
|
|
3602
|
+
-- Set render mode: 2 = stems (selected tracks via master mix / solo)
|
|
3603
|
+
reaper.GetSetProjectInfo(0, "RENDER_SETTINGS", 2, true)
|
|
3604
|
+
|
|
3605
|
+
-- Force WAV at 44100 Hz stereo so the sidecar always gets a known format,
|
|
3606
|
+
-- independent of whatever the user last rendered. Restored in finally.
|
|
3607
|
+
-- "evaw" is the little-endian "wave" fourcc REAPER uses for WAV format.
|
|
3608
|
+
reaper.GetSetProjectInfo_String(0, "RENDER_FORMAT", "evaw", true)
|
|
3609
|
+
reaper.GetSetProjectInfo(0, "RENDER_SRATE", sample_rate, true)
|
|
3610
|
+
reaper.GetSetProjectInfo(0, "RENDER_CHANNELS", 2, true)
|
|
3611
|
+
|
|
3612
|
+
-- Unsolo all tracks, then solo only the target track
|
|
3613
|
+
for i = 0, track_count - 1 do
|
|
3614
|
+
local t = reaper.GetTrack(0, i)
|
|
3615
|
+
reaper.SetMediaTrackInfo_Value(t, "I_SOLO", 0)
|
|
3616
|
+
end
|
|
3617
|
+
reaper.SetMediaTrackInfo_Value(track, "I_SOLO", 1)
|
|
3618
|
+
|
|
3619
|
+
-- Trigger render: action 42230 = "Render project, using the most recent render settings, auto-close render dialog"
|
|
3620
|
+
-- This call is synchronous — it blocks the defer loop until rendering completes.
|
|
3621
|
+
reaper.Main_OnCommand(42230, 0)
|
|
3622
|
+
end)
|
|
3623
|
+
|
|
3624
|
+
-- -------------------------------------------------------------------------
|
|
3625
|
+
-- Restore all settings and solo states (always, even on error)
|
|
3626
|
+
-- -------------------------------------------------------------------------
|
|
3627
|
+
local restore_ok, restore_err2 = pcall(function()
|
|
3628
|
+
reaper.GetSetProjectInfo_String(0, "RENDER_FILE", saved_file, true)
|
|
3629
|
+
reaper.GetSetProjectInfo_String(0, "RENDER_PATTERN", saved_pattern, true)
|
|
3630
|
+
reaper.GetSetProjectInfo_String(0, "RENDER_FORMAT", saved_format, true)
|
|
3631
|
+
reaper.GetSetProjectInfo(0, "RENDER_BOUNDSFLAG", saved_bounds, true)
|
|
3632
|
+
reaper.GetSetProjectInfo(0, "RENDER_SETTINGS", saved_settings, true)
|
|
3633
|
+
reaper.GetSetProjectInfo(0, "RENDER_STARTPOS", saved_start, true)
|
|
3634
|
+
reaper.GetSetProjectInfo(0, "RENDER_ENDPOS", saved_end, true)
|
|
3635
|
+
reaper.GetSetProjectInfo(0, "RENDER_SRATE", saved_srate, true)
|
|
3636
|
+
reaper.GetSetProjectInfo(0, "RENDER_CHANNELS", saved_channels, true)
|
|
3637
|
+
for i = 0, track_count - 1 do
|
|
3638
|
+
local t = reaper.GetTrack(0, i)
|
|
3639
|
+
reaper.SetMediaTrackInfo_Value(t, "I_SOLO", saved_solo_states[i] or 0)
|
|
3640
|
+
end
|
|
3641
|
+
end)
|
|
3642
|
+
|
|
3643
|
+
if not restore_ok then
|
|
3644
|
+
-- Log but don't fail — the render may have succeeded
|
|
3645
|
+
reaper.ShowConsoleMsg("[reaper-mcp] Warning: failed to restore render settings: " .. tostring(restore_err2) .. "\n")
|
|
3646
|
+
end
|
|
3647
|
+
|
|
3648
|
+
if not ok then
|
|
3649
|
+
return nil, "Render failed: " .. tostring(restore_err)
|
|
3650
|
+
end
|
|
3651
|
+
|
|
3652
|
+
-- Verify the file was actually created.
|
|
3653
|
+
-- RENDER_PATTERN was set without the .wav extension so REAPER appends it;
|
|
3654
|
+
-- wav_path already has the full expected name including the extension.
|
|
3655
|
+
local f = io.open(wav_path, "rb")
|
|
3656
|
+
if not f then
|
|
3657
|
+
return nil, "Render produced no output file at: " .. wav_path
|
|
3658
|
+
end
|
|
3659
|
+
-- Verify the rendered file is actually a WAV. RENDER_FORMAT was set to "evaw"
|
|
3660
|
+
-- (REAPER's WAV fourcc) above; if REAPER silently ignored that value the file
|
|
3661
|
+
-- could be any codec the user last rendered (MP3/FLAC/etc.). Check the RIFF
|
|
3662
|
+
-- magic bytes so we fail loudly here rather than crashing in the Python sidecar
|
|
3663
|
+
-- with a confusing "Cannot read audio file" error.
|
|
3664
|
+
local magic = f:read(4)
|
|
3665
|
+
f:close()
|
|
3666
|
+
if magic ~= "RIFF" then
|
|
3667
|
+
os.remove(wav_path)
|
|
3668
|
+
return nil, "Render produced a non-WAV file (RENDER_FORMAT may not have been applied). " ..
|
|
3669
|
+
"First 4 bytes: " .. (magic and string.format("%q", magic) or "<empty>")
|
|
3670
|
+
end
|
|
3671
|
+
|
|
3672
|
+
local duration = end_time - start_time
|
|
3673
|
+
return {
|
|
3674
|
+
trackIndex = track_index,
|
|
3675
|
+
trackName = track_name,
|
|
3676
|
+
wavPath = wav_path,
|
|
3677
|
+
durationSeconds = duration,
|
|
3678
|
+
sampleRate = sample_rate,
|
|
3679
|
+
channelCount = 2,
|
|
3680
|
+
}
|
|
3681
|
+
end
|
|
3682
|
+
|
|
3502
3683
|
-- =============================================================================
|
|
3503
3684
|
-- Bridge diagnostics handler
|
|
3504
3685
|
-- =============================================================================
|