@mthines/reaper-mcp 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -12
- package/knowledge/CLAUDE.md +71 -0
- package/knowledge/workflows/learn-plugin.md +143 -0
- package/main.js +166 -53
- package/package.json +1 -1
- package/reaper/CLAUDE.md +60 -0
- package/reaper/install.sh +8 -4
- package/reaper/mcp_bridge.lua +232 -4
package/README.md
CHANGED
|
@@ -17,8 +17,7 @@ npx @mthines/reaper-mcp setup
|
|
|
17
17
|
|
|
18
18
|
# 2. In REAPER: Actions > Load ReaScript > select mcp_bridge.lua > Run
|
|
19
19
|
|
|
20
|
-
# 3. Install AI mix knowledge
|
|
21
|
-
cd your-project
|
|
20
|
+
# 3. Install AI mix knowledge (globally by default, or --project for local)
|
|
22
21
|
npx @mthines/reaper-mcp install-skills
|
|
23
22
|
|
|
24
23
|
# 4. Open Claude Code — you're ready to mix
|
|
@@ -94,19 +93,27 @@ This copies into your REAPER resource folder:
|
|
|
94
93
|
|
|
95
94
|
You should see in REAPER's console: `MCP Bridge: Started`
|
|
96
95
|
|
|
97
|
-
### Step 3: Install AI mix knowledge
|
|
96
|
+
### Step 3: Install AI mix knowledge
|
|
98
97
|
|
|
99
98
|
```bash
|
|
100
|
-
|
|
99
|
+
# Install globally (default) — available from any directory
|
|
101
100
|
npx @mthines/reaper-mcp install-skills
|
|
101
|
+
|
|
102
|
+
# Or install into a specific project
|
|
103
|
+
cd your-music-project
|
|
104
|
+
npx @mthines/reaper-mcp install-skills --project
|
|
102
105
|
```
|
|
103
106
|
|
|
104
|
-
|
|
107
|
+
**Global install** (`--global`, default) installs to `~/.claude/`:
|
|
108
|
+
|
|
109
|
+
- `~/.claude/agents/` — mix engineer subagents (`@mix-engineer`, `@gain-stage`, `@mix-analyzer`, `@master`)
|
|
110
|
+
- `~/.claude/rules/` — architecture and development rules
|
|
111
|
+
- `~/.claude/skills/` — skills like `/learn-plugin`
|
|
112
|
+
- `~/.claude/knowledge/` — plugin knowledge, genre rules, workflows, reference data
|
|
113
|
+
|
|
114
|
+
**Project install** (`--project`) installs to your current directory:
|
|
105
115
|
|
|
106
|
-
- `.claude/agents/` —
|
|
107
|
-
- `.claude/rules/` — architecture and development rules
|
|
108
|
-
- `.claude/skills/` — skills like `/learn-plugin`
|
|
109
|
-
- `knowledge/` — plugin knowledge, genre rules, workflows, reference data
|
|
116
|
+
- `.claude/agents/`, `.claude/rules/`, `.claude/skills/`, `knowledge/` — same as above, scoped to the project
|
|
110
117
|
- `.mcp.json` — MCP server configuration for Claude Code
|
|
111
118
|
|
|
112
119
|
### Step 4: Verify
|
|
@@ -238,7 +245,7 @@ Checks that the bridge is connected, knowledge is installed, and MCP config exis
|
|
|
238
245
|
|
|
239
246
|
## Using the Mix Agents
|
|
240
247
|
|
|
241
|
-
Once you've run `setup` and `install-skills`, open Claude Code
|
|
248
|
+
Once you've run `setup` and `install-skills`, open Claude Code. Four specialized mix agents are available:
|
|
242
249
|
|
|
243
250
|
### Available Agents
|
|
244
251
|
|
|
@@ -440,7 +447,8 @@ The format is `mcp__reaper__{tool_name}`. Once added, Claude Code will run these
|
|
|
440
447
|
npx @mthines/reaper-mcp # Start MCP server (default)
|
|
441
448
|
npx @mthines/reaper-mcp serve # Start MCP server (stdio mode)
|
|
442
449
|
npx @mthines/reaper-mcp setup # Install Lua bridge + JSFX into REAPER
|
|
443
|
-
npx @mthines/reaper-mcp install-skills # Install AI knowledge + agents
|
|
450
|
+
npx @mthines/reaper-mcp install-skills # Install AI knowledge + agents (globally by default)
|
|
451
|
+
npx @mthines/reaper-mcp install-skills --project # Install into current project directory
|
|
444
452
|
npx @mthines/reaper-mcp doctor # Verify everything is configured
|
|
445
453
|
npx @mthines/reaper-mcp status # Check bridge connection
|
|
446
454
|
```
|
|
@@ -454,7 +462,7 @@ reaper-mcp setup
|
|
|
454
462
|
|
|
455
463
|
## Claude Code Integration
|
|
456
464
|
|
|
457
|
-
After `install-skills`, your project has a `.mcp.json`:
|
|
465
|
+
After `install-skills --project`, your project has a `.mcp.json`:
|
|
458
466
|
|
|
459
467
|
```json
|
|
460
468
|
{
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Knowledge Base
|
|
2
|
+
|
|
3
|
+
Audio engineering knowledge consumed by `reaper-mix-agent` at runtime. Markdown files with YAML frontmatter.
|
|
4
|
+
|
|
5
|
+
## File Organization
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
knowledge/
|
|
9
|
+
plugins/{vendor-slug}/{plugin-slug}.md # Plugin knowledge (fx_match, parameters, presets)
|
|
10
|
+
genres/{genre-id}.md # Genre mixing conventions (targets, EQ, compression)
|
|
11
|
+
workflows/{workflow-id}.md # Step-by-step mixing workflows
|
|
12
|
+
reference/{topic}.md # Reference material (frequencies, metering, etc.)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## How Files Are Loaded
|
|
16
|
+
|
|
17
|
+
`knowledge-loader.ts` in `reaper-mix-agent`:
|
|
18
|
+
- Recursively collects all `.md` files
|
|
19
|
+
- Categorizes by first path segment (`plugins/` → plugin, `genres/` → genre, etc.)
|
|
20
|
+
- Skips files with `_template` in the path
|
|
21
|
+
- Parses frontmatter with a simple line-by-line parser (NOT full YAML)
|
|
22
|
+
|
|
23
|
+
## Frontmatter Constraints
|
|
24
|
+
|
|
25
|
+
The parser is simple — respect these limitations:
|
|
26
|
+
- Arrays must be single-line: `fx_match: ["Pro-Q 3", "VST3: FabFilter Pro-Q 3"]`
|
|
27
|
+
- No multi-line values or nested objects
|
|
28
|
+
- `#` in frontmatter is treated as a comment — don't use in values
|
|
29
|
+
- Numbers parse automatically; booleans must be `true`/`false` (lowercase)
|
|
30
|
+
|
|
31
|
+
## Required Frontmatter by Type
|
|
32
|
+
|
|
33
|
+
### Plugins
|
|
34
|
+
| Field | Required | Description |
|
|
35
|
+
|-------|----------|-------------|
|
|
36
|
+
| `name` | Yes | Display name |
|
|
37
|
+
| `fx_match` | Yes | Array of strings for case-insensitive substring match against REAPER FX list |
|
|
38
|
+
| `category` | Yes | `eq`, `compressor`, `limiter`, `reverb`, `delay`, `gate`, etc. |
|
|
39
|
+
| `vendor` | Yes | Vendor name |
|
|
40
|
+
| `preference` | Yes | 0-100 score (stock: 30-50, third-party: 70-85, industry-standard: 85-95) |
|
|
41
|
+
| `style` | No | `transparent`, `character`, `vintage`, `modern`, `surgical` |
|
|
42
|
+
| `replaces` | No | Array of stock plugin IDs this replaces |
|
|
43
|
+
|
|
44
|
+
### Genres
|
|
45
|
+
| Field | Required | Description |
|
|
46
|
+
|-------|----------|-------------|
|
|
47
|
+
| `name` | Yes | Genre display name |
|
|
48
|
+
| `id` | Yes | Must match filename (e.g., `rock.md` → `id: rock`) |
|
|
49
|
+
| `lufs_target` | Yes | Array `[-13, -10]` for min/max LUFS |
|
|
50
|
+
| `true_peak` | Yes | dBTP ceiling (e.g., `-1.0`) |
|
|
51
|
+
| `parent` | No | Inherits from parent genre |
|
|
52
|
+
|
|
53
|
+
### Workflows
|
|
54
|
+
| Field | Required | Description |
|
|
55
|
+
|-------|----------|-------------|
|
|
56
|
+
| `name` | Yes | Workflow display name |
|
|
57
|
+
| `id` | Yes | Must match filename |
|
|
58
|
+
|
|
59
|
+
## fx_match Pattern Guide
|
|
60
|
+
|
|
61
|
+
Patterns are matched as case-insensitive substrings against REAPER's installed FX names:
|
|
62
|
+
- `"Pro-Q 3"` matches `"VST3: FabFilter Pro-Q 3 (FabFilter)"`, `"AU: Pro-Q 3"`, etc.
|
|
63
|
+
- Include multiple patterns for VST/VST3/AU variants if needed
|
|
64
|
+
- Check REAPER's FX Browser for exact names
|
|
65
|
+
|
|
66
|
+
## Coupled Code
|
|
67
|
+
|
|
68
|
+
Changes here may require updates in `apps/reaper-mix-agent/`:
|
|
69
|
+
- New knowledge type → update `typeFromPath()` in `knowledge-loader.ts`
|
|
70
|
+
- New frontmatter fields → update consumers in `agent.ts` or `plugin-resolver.ts`
|
|
71
|
+
- New workflow file → should have a matching mode in `modes/`
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Learn Plugin
|
|
3
|
+
id: learn-plugin
|
|
4
|
+
description: Discover an unknown plugin by reading its parameters, researching online, and creating a knowledge file
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Learn Plugin
|
|
8
|
+
|
|
9
|
+
## When to Use
|
|
10
|
+
|
|
11
|
+
When you encounter an installed FX plugin that has no knowledge file and you need to understand what it does, how to use it, and what parameters to set. This workflow turns an unknown plugin into a documented, reusable knowledge file.
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
- The plugin must be loaded on a track in the current session (so you can read its parameters)
|
|
16
|
+
- You need the track index and FX index of the plugin
|
|
17
|
+
|
|
18
|
+
## Steps
|
|
19
|
+
|
|
20
|
+
### Step 1: Read the plugin's parameters
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
tool: get_fx_parameters
|
|
24
|
+
params:
|
|
25
|
+
trackIndex: N
|
|
26
|
+
fxIndex: M
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Examine the parameter list carefully. Parameter names are strong clues:
|
|
30
|
+
- **Frequency, Q, Gain, Bandwidth, Slope** → EQ
|
|
31
|
+
- **Threshold, Ratio, Attack, Release, Knee, Makeup** → Compressor
|
|
32
|
+
- **Ceiling, Lookahead, True Peak** → Limiter
|
|
33
|
+
- **Pre-delay, Decay, Damping, Room Size, Diffusion** → Reverb
|
|
34
|
+
- **Time, Feedback, Mix, Sync, Ping-pong** → Delay
|
|
35
|
+
- **Drive, Saturation, Harmonics, Warmth** → Saturator
|
|
36
|
+
- **Range, Hold, Hysteresis** → Gate
|
|
37
|
+
- **Frequency, Sensitivity, Reduction** → De-esser
|
|
38
|
+
- **Amp Model, Cabinet, Gain** → Amp sim
|
|
39
|
+
- **Width, Mid/Side, Rotation** → Stereo imager
|
|
40
|
+
- **Pitch, Formant, Speed, Correction** → Pitch correction
|
|
41
|
+
|
|
42
|
+
Record the full parameter list with names, current values, and ranges.
|
|
43
|
+
|
|
44
|
+
### Step 2: Check factory presets
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
tool: get_fx_preset_list
|
|
48
|
+
params:
|
|
49
|
+
trackIndex: N
|
|
50
|
+
fxIndex: M
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Preset names often reveal intended use cases and the plugin's character. Look for:
|
|
54
|
+
- Category hints (e.g., "Vocal", "Drum Bus", "Mastering")
|
|
55
|
+
- Character hints (e.g., "Warm", "Transparent", "Aggressive", "Vintage")
|
|
56
|
+
- Genre hints (e.g., "Hip Hop Vocal", "Rock Guitar")
|
|
57
|
+
|
|
58
|
+
### Step 3: Research the plugin (if still unclear)
|
|
59
|
+
|
|
60
|
+
If the parameter names and presets don't clearly identify the plugin, do a web search:
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
Search: "{plugin name}" audio plugin parameters guide
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Look for:
|
|
67
|
+
- Official product page (features, description)
|
|
68
|
+
- Manual or parameter reference
|
|
69
|
+
- Professional reviews describing the plugin's character and best use cases
|
|
70
|
+
- Forum discussions about recommended settings
|
|
71
|
+
|
|
72
|
+
### Step 4: Determine metadata
|
|
73
|
+
|
|
74
|
+
From what you've learned, decide:
|
|
75
|
+
|
|
76
|
+
| Field | How to determine |
|
|
77
|
+
|-------|-----------------|
|
|
78
|
+
| `name` | Full display name as shown in REAPER |
|
|
79
|
+
| `fx_match` | The exact name from REAPER's FX list, plus common variations (VST/VST3/AU) |
|
|
80
|
+
| `category` | Primary category from: eq, compressor, limiter, saturator, reverb, delay, gate, de-esser, amp-sim, channel-strip, stereo-imager, multiband, pitch-correction |
|
|
81
|
+
| `vendor` | The plugin manufacturer |
|
|
82
|
+
| `preference` | 30-50 for free/stock, 55-70 for decent third-party, 70-85 for preferred, 85-95 for industry-standard |
|
|
83
|
+
| `style` | transparent, character, vintage, modern, or surgical |
|
|
84
|
+
| `replaces` | Stock REAPER plugin IDs this can replace (e.g., "rea-eq", "rea-comp") |
|
|
85
|
+
|
|
86
|
+
### Step 5: Write the knowledge file
|
|
87
|
+
|
|
88
|
+
Create the file at `knowledge/plugins/{vendor-slug}/{plugin-slug}.md` using the template structure:
|
|
89
|
+
|
|
90
|
+
```markdown
|
|
91
|
+
---
|
|
92
|
+
name: {Plugin Name}
|
|
93
|
+
fx_match: ["{exact REAPER name}", "{alternate match}"]
|
|
94
|
+
category: {category}
|
|
95
|
+
style: {style}
|
|
96
|
+
vendor: {Vendor Name}
|
|
97
|
+
preference: {score}
|
|
98
|
+
replaces: [{stock plugins if applicable}]
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
# {Plugin Name}
|
|
102
|
+
|
|
103
|
+
## What it does
|
|
104
|
+
|
|
105
|
+
{1-2 paragraphs: purpose, character, primary use cases}
|
|
106
|
+
|
|
107
|
+
## Key parameters by name
|
|
108
|
+
|
|
109
|
+
| Parameter | Range | Description |
|
|
110
|
+
|-----------|-------|-------------|
|
|
111
|
+
| {exact name} | {range} | {description} |
|
|
112
|
+
|
|
113
|
+
## Recommended settings
|
|
114
|
+
|
|
115
|
+
### Use case: {instrument or purpose}
|
|
116
|
+
|
|
117
|
+
| Parameter | Value | Why |
|
|
118
|
+
|-----------|-------|-----|
|
|
119
|
+
| {name} | {value} | {reason} |
|
|
120
|
+
|
|
121
|
+
## Presets worth knowing
|
|
122
|
+
|
|
123
|
+
- **{Preset Name}** — {what it does}
|
|
124
|
+
|
|
125
|
+
## When to prefer this
|
|
126
|
+
|
|
127
|
+
- {situations where this plugin is the best choice}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Step 6: Verify the knowledge file
|
|
131
|
+
|
|
132
|
+
After writing the file, confirm:
|
|
133
|
+
- `fx_match` patterns actually match the installed FX name (case-insensitive substring)
|
|
134
|
+
- All parameter names are EXACT (copy from `get_fx_parameters` output)
|
|
135
|
+
- Category and preference score are appropriate
|
|
136
|
+
- Recommended settings include at least one concrete use case
|
|
137
|
+
|
|
138
|
+
## Tips
|
|
139
|
+
|
|
140
|
+
- When multiple parameters share a prefix (e.g., "Band 1 Frequency", "Band 2 Frequency"), document the pattern once with "Band N" notation
|
|
141
|
+
- Include the normalized value range (0.0-1.0) alongside the human-readable range when the mapping isn't obvious
|
|
142
|
+
- If the plugin has multiple modes or routing options, document each mode separately
|
|
143
|
+
- For channel-strip plugins with multiple sections (EQ + comp + gate), consider documenting under the most prominent category but mention all capabilities
|
package/main.js
CHANGED
|
@@ -1456,6 +1456,109 @@ function registerEnvelopeTools(server) {
|
|
|
1456
1456
|
return { content: [{ type: "text", text: `Deleted envelope point ${pointIndex}` }] };
|
|
1457
1457
|
}
|
|
1458
1458
|
);
|
|
1459
|
+
server.tool(
|
|
1460
|
+
"create_track_envelope",
|
|
1461
|
+
"Create/show an automation envelope on a track. Use envelopeName for built-in envelopes (Volume, Pan, Mute, Width, Trim Volume) or fxIndex+paramIndex for FX parameter envelopes. The envelope is made visible and active.",
|
|
1462
|
+
{
|
|
1463
|
+
trackIndex: z14.coerce.number().int().min(0).describe("Zero-based track index"),
|
|
1464
|
+
envelopeName: z14.string().optional().describe('Built-in envelope name: "Volume", "Pan", "Mute", "Width", "Trim Volume"'),
|
|
1465
|
+
fxIndex: z14.coerce.number().int().min(0).optional().describe("FX chain index (for FX parameter envelopes)"),
|
|
1466
|
+
paramIndex: z14.coerce.number().int().min(0).optional().describe("FX parameter index (required if fxIndex provided)")
|
|
1467
|
+
},
|
|
1468
|
+
async ({ trackIndex, envelopeName, fxIndex, paramIndex }) => {
|
|
1469
|
+
const res = await sendCommand("create_track_envelope", {
|
|
1470
|
+
trackIndex,
|
|
1471
|
+
envelopeName,
|
|
1472
|
+
fxIndex,
|
|
1473
|
+
paramIndex
|
|
1474
|
+
});
|
|
1475
|
+
if (!res.success) {
|
|
1476
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1477
|
+
}
|
|
1478
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
1479
|
+
}
|
|
1480
|
+
);
|
|
1481
|
+
server.tool(
|
|
1482
|
+
"set_envelope_properties",
|
|
1483
|
+
"Set properties (active, visible, armed) on a track envelope. Requires SWS extension for full support.",
|
|
1484
|
+
{
|
|
1485
|
+
trackIndex: z14.coerce.number().int().min(0).describe("Zero-based track index"),
|
|
1486
|
+
envelopeIndex: z14.coerce.number().int().min(0).describe("Zero-based envelope index on the track"),
|
|
1487
|
+
active: z14.boolean().optional().describe("Set envelope active/inactive"),
|
|
1488
|
+
visible: z14.boolean().optional().describe("Set envelope visible/hidden in arrange view"),
|
|
1489
|
+
armed: z14.boolean().optional().describe("Set envelope armed for writing automation")
|
|
1490
|
+
},
|
|
1491
|
+
async ({ trackIndex, envelopeIndex, active, visible, armed }) => {
|
|
1492
|
+
const res = await sendCommand("set_envelope_properties", {
|
|
1493
|
+
trackIndex,
|
|
1494
|
+
envelopeIndex,
|
|
1495
|
+
active,
|
|
1496
|
+
visible,
|
|
1497
|
+
armed
|
|
1498
|
+
});
|
|
1499
|
+
if (!res.success) {
|
|
1500
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1501
|
+
}
|
|
1502
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
1503
|
+
}
|
|
1504
|
+
);
|
|
1505
|
+
server.tool(
|
|
1506
|
+
"clear_envelope",
|
|
1507
|
+
"Delete ALL automation points from an envelope, resetting it to its default state",
|
|
1508
|
+
{
|
|
1509
|
+
trackIndex: z14.coerce.number().int().min(0).describe("Zero-based track index"),
|
|
1510
|
+
envelopeIndex: z14.coerce.number().int().min(0).describe("Zero-based envelope index on the track")
|
|
1511
|
+
},
|
|
1512
|
+
async ({ trackIndex, envelopeIndex }) => {
|
|
1513
|
+
const res = await sendCommand("clear_envelope", { trackIndex, envelopeIndex });
|
|
1514
|
+
if (!res.success) {
|
|
1515
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1516
|
+
}
|
|
1517
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
1518
|
+
}
|
|
1519
|
+
);
|
|
1520
|
+
server.tool(
|
|
1521
|
+
"remove_envelope_points",
|
|
1522
|
+
"Delete automation points in a time range from a track envelope. Use to surgically remove a section of automation.",
|
|
1523
|
+
{
|
|
1524
|
+
trackIndex: z14.coerce.number().int().min(0).describe("Zero-based track index"),
|
|
1525
|
+
envelopeIndex: z14.coerce.number().int().min(0).describe("Zero-based envelope index on the track"),
|
|
1526
|
+
timeStart: z14.coerce.number().describe("Start of time range in seconds (inclusive)"),
|
|
1527
|
+
timeEnd: z14.coerce.number().describe("End of time range in seconds (exclusive)")
|
|
1528
|
+
},
|
|
1529
|
+
async ({ trackIndex, envelopeIndex, timeStart, timeEnd }) => {
|
|
1530
|
+
const res = await sendCommand("remove_envelope_points", {
|
|
1531
|
+
trackIndex,
|
|
1532
|
+
envelopeIndex,
|
|
1533
|
+
timeStart,
|
|
1534
|
+
timeEnd
|
|
1535
|
+
});
|
|
1536
|
+
if (!res.success) {
|
|
1537
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1538
|
+
}
|
|
1539
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
1540
|
+
}
|
|
1541
|
+
);
|
|
1542
|
+
server.tool(
|
|
1543
|
+
"insert_envelope_points",
|
|
1544
|
+
"Batch insert multiple automation points on a track envelope. Much faster than repeated insert_envelope_point calls.",
|
|
1545
|
+
{
|
|
1546
|
+
trackIndex: z14.coerce.number().int().min(0).describe("Zero-based track index"),
|
|
1547
|
+
envelopeIndex: z14.coerce.number().int().min(0).describe("Zero-based envelope index on the track"),
|
|
1548
|
+
points: z14.string().describe("JSON array of point objects: [{time, value, shape?, tension?}, ...]")
|
|
1549
|
+
},
|
|
1550
|
+
async ({ trackIndex, envelopeIndex, points }) => {
|
|
1551
|
+
const res = await sendCommand("insert_envelope_points", {
|
|
1552
|
+
trackIndex,
|
|
1553
|
+
envelopeIndex,
|
|
1554
|
+
points
|
|
1555
|
+
});
|
|
1556
|
+
if (!res.success) {
|
|
1557
|
+
return { content: [{ type: "text", text: `Error: ${res.error}` }], isError: true };
|
|
1558
|
+
}
|
|
1559
|
+
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
1560
|
+
}
|
|
1561
|
+
);
|
|
1459
1562
|
}
|
|
1460
1563
|
|
|
1461
1564
|
// apps/reaper-mcp-server/src/server.ts
|
|
@@ -1669,7 +1772,12 @@ var MCP_TOOL_NAMES = [
|
|
|
1669
1772
|
"get_track_envelopes",
|
|
1670
1773
|
"get_envelope_points",
|
|
1671
1774
|
"insert_envelope_point",
|
|
1672
|
-
"
|
|
1775
|
+
"insert_envelope_points",
|
|
1776
|
+
"delete_envelope_point",
|
|
1777
|
+
"create_track_envelope",
|
|
1778
|
+
"set_envelope_properties",
|
|
1779
|
+
"clear_envelope",
|
|
1780
|
+
"remove_envelope_points"
|
|
1673
1781
|
];
|
|
1674
1782
|
function ensureClaudeSettings(settingsPath) {
|
|
1675
1783
|
const allowList = MCP_TOOL_NAMES.map((t) => `mcp__reaper__${t}`);
|
|
@@ -1708,7 +1816,7 @@ async function setup() {
|
|
|
1708
1816
|
} else {
|
|
1709
1817
|
console.log(` Not found: ${luaSrc}`);
|
|
1710
1818
|
}
|
|
1711
|
-
const effectsDir = getReaperEffectsPath();
|
|
1819
|
+
const effectsDir = join4(getReaperEffectsPath(), "reaper-mcp");
|
|
1712
1820
|
mkdirSync2(effectsDir, { recursive: true });
|
|
1713
1821
|
console.log("\nInstalling JSFX analyzers...");
|
|
1714
1822
|
for (const jsfx of REAPER_ASSETS) {
|
|
@@ -1716,7 +1824,7 @@ async function setup() {
|
|
|
1716
1824
|
const src = join4(reaperDir, jsfx);
|
|
1717
1825
|
const dest = join4(effectsDir, jsfx);
|
|
1718
1826
|
if (installFile(src, dest)) {
|
|
1719
|
-
console.log(` Installed:
|
|
1827
|
+
console.log(` Installed: reaper-mcp/${jsfx}`);
|
|
1720
1828
|
} else {
|
|
1721
1829
|
console.log(` Not found: ${src}`);
|
|
1722
1830
|
}
|
|
@@ -1729,70 +1837,66 @@ async function setup() {
|
|
|
1729
1837
|
console.log(" 4. Run the script (it will keep running in background via defer loop)");
|
|
1730
1838
|
console.log(" 5. Add reaper-mcp to your Claude Code config (see: npx @mthines/reaper-mcp doctor)");
|
|
1731
1839
|
}
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1840
|
+
function parseInstallScope(args) {
|
|
1841
|
+
if (args.includes("--project")) return "project";
|
|
1842
|
+
return "global";
|
|
1843
|
+
}
|
|
1844
|
+
async function installSkills(scope) {
|
|
1845
|
+
console.log(`REAPER MCP \u2014 Install AI Mix Engineer Skills (scope: ${scope})
|
|
1846
|
+
`);
|
|
1847
|
+
const isGlobal = scope === "global";
|
|
1848
|
+
const baseDir = isGlobal ? join4(homedir2(), ".claude") : process.cwd();
|
|
1849
|
+
const claudeDir = isGlobal ? baseDir : join4(baseDir, ".claude");
|
|
1736
1850
|
const knowledgeSrc = resolveAssetDir(__dirname, "knowledge");
|
|
1737
|
-
const knowledgeDest = join4(targetDir, "knowledge");
|
|
1738
1851
|
if (existsSync2(knowledgeSrc)) {
|
|
1739
|
-
const
|
|
1740
|
-
|
|
1852
|
+
const dest = join4(baseDir, "knowledge");
|
|
1853
|
+
const count = copyDirSync(knowledgeSrc, dest);
|
|
1854
|
+
console.log(`Installed knowledge base: ${count} files \u2192 ${dest}`);
|
|
1741
1855
|
} else {
|
|
1742
1856
|
console.log("Knowledge base not found in package. Skipping.");
|
|
1743
1857
|
}
|
|
1744
1858
|
const rulesSrc = resolveAssetDir(__dirname, "claude-rules");
|
|
1745
|
-
const rulesDir = join4(targetDir, ".claude", "rules");
|
|
1746
1859
|
if (existsSync2(rulesSrc)) {
|
|
1747
|
-
const
|
|
1748
|
-
|
|
1860
|
+
const dest = join4(claudeDir, "rules");
|
|
1861
|
+
const count = copyDirSync(rulesSrc, dest);
|
|
1862
|
+
console.log(`Installed Claude rules: ${count} files \u2192 ${dest}`);
|
|
1749
1863
|
} else {
|
|
1750
1864
|
console.log("Claude rules not found in package. Skipping.");
|
|
1751
1865
|
}
|
|
1752
1866
|
const skillsSrc = resolveAssetDir(__dirname, "claude-skills");
|
|
1753
|
-
const skillsDir = join4(targetDir, ".claude", "skills");
|
|
1754
1867
|
if (existsSync2(skillsSrc)) {
|
|
1755
|
-
const
|
|
1756
|
-
|
|
1868
|
+
const dest = join4(claudeDir, "skills");
|
|
1869
|
+
const count = copyDirSync(skillsSrc, dest);
|
|
1870
|
+
console.log(`Installed Claude skills: ${count} files \u2192 ${dest}`);
|
|
1757
1871
|
} else {
|
|
1758
1872
|
console.log("Claude skills not found in package. Skipping.");
|
|
1759
1873
|
}
|
|
1760
1874
|
const agentsSrc = resolveAssetDir(__dirname, "claude-agents");
|
|
1761
|
-
const agentsDir = join4(targetDir, ".claude", "agents");
|
|
1762
1875
|
if (existsSync2(agentsSrc)) {
|
|
1763
|
-
const
|
|
1764
|
-
|
|
1876
|
+
const dest = join4(claudeDir, "agents");
|
|
1877
|
+
const count = copyDirSync(agentsSrc, dest);
|
|
1878
|
+
console.log(`Installed Claude agents: ${count} files \u2192 ${dest}`);
|
|
1765
1879
|
} else {
|
|
1766
1880
|
console.log("Claude agents not found in package. Skipping.");
|
|
1767
1881
|
}
|
|
1768
|
-
const
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
console.log(`
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
const localResult = ensureClaudeSettings(localSettingsPath);
|
|
1775
|
-
if (localResult === "created") {
|
|
1776
|
-
console.log(`Created Claude settings: ${localSettingsPath}`);
|
|
1777
|
-
} else if (localResult === "updated") {
|
|
1778
|
-
console.log(`Updated Claude settings with new REAPER tools: ${localSettingsPath}`);
|
|
1882
|
+
const settingsPath = join4(claudeDir, "settings.json");
|
|
1883
|
+
const result = ensureClaudeSettings(settingsPath);
|
|
1884
|
+
if (result === "created") {
|
|
1885
|
+
console.log(`Created Claude settings: ${settingsPath}`);
|
|
1886
|
+
} else if (result === "updated") {
|
|
1887
|
+
console.log(`Updated Claude settings with new REAPER tools: ${settingsPath}`);
|
|
1779
1888
|
} else {
|
|
1780
|
-
console.log(`Claude settings already has all REAPER tools: ${
|
|
1781
|
-
}
|
|
1782
|
-
const globalSettingsPath = join4(globalClaudeDir, "settings.json");
|
|
1783
|
-
const globalResult = ensureClaudeSettings(globalSettingsPath);
|
|
1784
|
-
if (globalResult === "created") {
|
|
1785
|
-
console.log(`Created Claude settings (global): ${globalSettingsPath}`);
|
|
1786
|
-
} else if (globalResult === "updated") {
|
|
1787
|
-
console.log(`Updated Claude settings (global) with new REAPER tools: ${globalSettingsPath}`);
|
|
1889
|
+
console.log(`Claude settings already has all REAPER tools: ${settingsPath}`);
|
|
1788
1890
|
}
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1891
|
+
if (!isGlobal) {
|
|
1892
|
+
const mcpJsonPath = join4(baseDir, ".mcp.json");
|
|
1893
|
+
if (createMcpJson(mcpJsonPath)) {
|
|
1894
|
+
console.log(`
|
|
1792
1895
|
Created: ${mcpJsonPath}`);
|
|
1793
|
-
|
|
1794
|
-
|
|
1896
|
+
} else {
|
|
1897
|
+
console.log(`
|
|
1795
1898
|
.mcp.json already exists \u2014 add the reaper server config manually if needed.`);
|
|
1899
|
+
}
|
|
1796
1900
|
}
|
|
1797
1901
|
console.log("\nDone! Claude Code now has mix engineer agents, knowledge, and REAPER MCP tools.");
|
|
1798
1902
|
console.log("All 48 REAPER tools are pre-approved \u2014 agents work autonomously.");
|
|
@@ -1806,20 +1910,27 @@ async function doctor() {
|
|
|
1806
1910
|
if (!bridgeRunning) {
|
|
1807
1911
|
console.log(' \u2192 Run "npx @mthines/reaper-mcp setup" then load mcp_bridge.lua in REAPER');
|
|
1808
1912
|
}
|
|
1809
|
-
const
|
|
1810
|
-
|
|
1913
|
+
const globalClaudeDir = join4(homedir2(), ".claude");
|
|
1914
|
+
const localAgents = existsSync2(join4(process.cwd(), ".claude", "agents"));
|
|
1915
|
+
const globalAgents = existsSync2(join4(globalClaudeDir, "agents"));
|
|
1916
|
+
const agentsExist = localAgents || globalAgents;
|
|
1917
|
+
const agentsLocation = localAgents ? ".claude/agents/" : globalAgents ? "~/.claude/agents/" : "";
|
|
1918
|
+
console.log(`Mix agents: ${agentsExist ? `\u2713 Found (${agentsLocation})` : "\u2717 Not installed"}`);
|
|
1811
1919
|
if (!agentsExist) {
|
|
1812
|
-
console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"
|
|
1920
|
+
console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"');
|
|
1813
1921
|
}
|
|
1814
|
-
const
|
|
1815
|
-
|
|
1922
|
+
const localKnowledge = existsSync2(join4(process.cwd(), "knowledge"));
|
|
1923
|
+
const globalKnowledge = existsSync2(join4(globalClaudeDir, "knowledge"));
|
|
1924
|
+
const knowledgeExists = localKnowledge || globalKnowledge;
|
|
1925
|
+
const knowledgeLocation = localKnowledge ? "project" : globalKnowledge ? "~/.claude/" : "";
|
|
1926
|
+
console.log(`Knowledge base: ${knowledgeExists ? `\u2713 Found (${knowledgeLocation})` : "\u2717 Not installed"}`);
|
|
1816
1927
|
if (!knowledgeExists) {
|
|
1817
|
-
console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"
|
|
1928
|
+
console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills"');
|
|
1818
1929
|
}
|
|
1819
1930
|
const mcpJsonExists = existsSync2(join4(process.cwd(), ".mcp.json"));
|
|
1820
1931
|
console.log(`MCP config: ${mcpJsonExists ? "\u2713 .mcp.json found" : "\u2717 .mcp.json missing"}`);
|
|
1821
1932
|
if (!mcpJsonExists) {
|
|
1822
|
-
console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills" to create .mcp.json');
|
|
1933
|
+
console.log(' \u2192 Run "npx @mthines/reaper-mcp install-skills --project" to create .mcp.json');
|
|
1823
1934
|
}
|
|
1824
1935
|
console.log('\nTo check SWS Extensions, start REAPER and use the "list_available_fx" tool.');
|
|
1825
1936
|
console.log("SWS provides enhanced plugin discovery and snapshot support.\n");
|
|
@@ -1884,7 +1995,7 @@ switch (command) {
|
|
|
1884
1995
|
});
|
|
1885
1996
|
break;
|
|
1886
1997
|
case "install-skills":
|
|
1887
|
-
installSkills().catch((err) => {
|
|
1998
|
+
installSkills(parseInstallScope(process.argv.slice(3))).catch((err) => {
|
|
1888
1999
|
console.error("Install failed:", err);
|
|
1889
2000
|
process.exit(1);
|
|
1890
2001
|
});
|
|
@@ -1917,14 +2028,16 @@ Usage:
|
|
|
1917
2028
|
npx @mthines/reaper-mcp Start MCP server (stdio mode)
|
|
1918
2029
|
npx @mthines/reaper-mcp serve Start MCP server (stdio mode)
|
|
1919
2030
|
npx @mthines/reaper-mcp setup Install Lua bridge + JSFX analyzers into REAPER
|
|
1920
|
-
npx @mthines/reaper-mcp install-skills Install AI
|
|
2031
|
+
npx @mthines/reaper-mcp install-skills Install AI knowledge + agents globally (default)
|
|
2032
|
+
npx @mthines/reaper-mcp install-skills --project Install into current project directory
|
|
2033
|
+
npx @mthines/reaper-mcp install-skills --global Install into ~/.claude/ (default)
|
|
1921
2034
|
npx @mthines/reaper-mcp doctor Check that everything is configured correctly
|
|
1922
2035
|
npx @mthines/reaper-mcp status Check if Lua bridge is running in REAPER
|
|
1923
2036
|
|
|
1924
2037
|
Quick Start:
|
|
1925
2038
|
1. npx @mthines/reaper-mcp setup # install REAPER components
|
|
1926
2039
|
2. Load mcp_bridge.lua in REAPER (Actions > Load ReaScript > Run)
|
|
1927
|
-
3. npx @mthines/reaper-mcp install-skills # install AI knowledge + agents
|
|
2040
|
+
3. npx @mthines/reaper-mcp install-skills # install AI knowledge + agents (globally)
|
|
1928
2041
|
4. Open Claude Code \u2014 REAPER tools + mix engineer agents are ready
|
|
1929
2042
|
|
|
1930
2043
|
Tip: install globally for shorter commands:
|
package/package.json
CHANGED
package/reaper/CLAUDE.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# REAPER Scripts
|
|
2
|
+
|
|
3
|
+
Files installed INTO the REAPER DAW by the `setup` command. These run inside REAPER's scripting environment.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
| File | Language | Purpose |
|
|
8
|
+
|------|----------|---------|
|
|
9
|
+
| `mcp_bridge.lua` | Lua | Persistent bridge: polls for JSON commands, executes ReaScript API, writes responses |
|
|
10
|
+
| `mcp_analyzer.jsfx` | JSFX/EEL2 | Real-time FFT spectrum analyzer, writes to gmem[] |
|
|
11
|
+
| `mcp_lufs_meter.jsfx` | JSFX/EEL2 | LUFS loudness metering |
|
|
12
|
+
| `mcp_correlation_meter.jsfx` | JSFX/EEL2 | Stereo correlation and width analysis |
|
|
13
|
+
| `mcp_crest_factor.jsfx` | JSFX/EEL2 | Crest factor (peak-to-RMS) measurement |
|
|
14
|
+
| `install.sh` | Shell | Manual install helper |
|
|
15
|
+
|
|
16
|
+
## Lua Bridge (`mcp_bridge.lua`)
|
|
17
|
+
|
|
18
|
+
### How It Works
|
|
19
|
+
1. Runs as a persistent `reaper.defer()` loop (polls every ~30ms)
|
|
20
|
+
2. Reads `command_{uuid}.json` from bridge directory
|
|
21
|
+
3. Dispatches to handler function in the `handlers` table
|
|
22
|
+
4. Writes `response_{uuid}.json` with results
|
|
23
|
+
5. Writes `heartbeat.json` every 1s for liveness detection
|
|
24
|
+
|
|
25
|
+
### Adding a Handler
|
|
26
|
+
```lua
|
|
27
|
+
handlers["command_type"] = function(params)
|
|
28
|
+
local result = reaper.SomeApiCall(params.paramName)
|
|
29
|
+
return { field = result }
|
|
30
|
+
end
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The command type string must exactly match `CommandType` in `libs/protocol/src/commands.ts`.
|
|
34
|
+
|
|
35
|
+
### Key Constraints
|
|
36
|
+
- REAPER Lua is sandboxed: **no sockets, no HTTP, no stdin/stdout** — file-based IPC only
|
|
37
|
+
- JSON parsing: uses `CF_Json_Parse` if available (REAPER 7+), falls back to custom Lua parser
|
|
38
|
+
- Track indices are 0-based (same as ReaScript)
|
|
39
|
+
- Volume values: bridge converts between dB (MCP protocol) and linear (ReaScript internally)
|
|
40
|
+
- Always wrap file reads in `pcall` for resilience
|
|
41
|
+
|
|
42
|
+
## JSFX Meters
|
|
43
|
+
|
|
44
|
+
- Run in REAPER's **audio thread** (not scripting thread)
|
|
45
|
+
- Communicate with Lua via `gmem[]` shared memory
|
|
46
|
+
- Each JSFX uses a unique gmem namespace (e.g., `MCPAnalyzer`, `MCPLufsMeter`)
|
|
47
|
+
- Must pass audio through unmodified (transparent inserts)
|
|
48
|
+
- Auto-inserted by corresponding MCP tools (`read_track_spectrum`, `read_track_lufs`, etc.)
|
|
49
|
+
|
|
50
|
+
## Testing
|
|
51
|
+
|
|
52
|
+
- **No automated tests possible** — REAPER's Lua/JSFX environment cannot be unit tested outside REAPER
|
|
53
|
+
- Test manually: install bridge, run MCP Inspector, exercise commands
|
|
54
|
+
- Server-side tests mock `sendCommand()` in `bridge.ts`
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
Files are copied to `{REAPER_RESOURCE_PATH}/Scripts/` by:
|
|
59
|
+
- `node dist/apps/reaper-mcp-server/main.js setup` (programmatic)
|
|
60
|
+
- `install.sh` (manual)
|
package/reaper/install.sh
CHANGED
|
@@ -29,11 +29,15 @@ mkdir -p "$SCRIPTS_DIR"
|
|
|
29
29
|
cp "$SCRIPT_DIR/mcp_bridge.lua" "$SCRIPTS_DIR/mcp_bridge.lua"
|
|
30
30
|
echo "Installed: $SCRIPTS_DIR/mcp_bridge.lua"
|
|
31
31
|
|
|
32
|
-
# Install JSFX
|
|
33
|
-
EFFECTS_DIR="$REAPER_PATH/Effects"
|
|
32
|
+
# Install JSFX analyzers
|
|
33
|
+
EFFECTS_DIR="$REAPER_PATH/Effects/reaper-mcp"
|
|
34
34
|
mkdir -p "$EFFECTS_DIR"
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
for jsfx in "$SCRIPT_DIR"/*.jsfx; do
|
|
36
|
+
[ -f "$jsfx" ] || continue
|
|
37
|
+
fname="$(basename "$jsfx")"
|
|
38
|
+
cp "$jsfx" "$EFFECTS_DIR/$fname"
|
|
39
|
+
echo "Installed: $EFFECTS_DIR/$fname"
|
|
40
|
+
done
|
|
37
41
|
|
|
38
42
|
# Create bridge data directory
|
|
39
43
|
BRIDGE_DIR="$SCRIPTS_DIR/mcp_bridge_data"
|
package/reaper/mcp_bridge.lua
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
local POLL_INTERVAL = 0.030 -- 30ms between polls
|
|
13
13
|
local HEARTBEAT_INTERVAL = 1.0 -- write heartbeat every 1s
|
|
14
|
-
local MCP_ANALYZER_FX_NAME = "mcp_analyzer" -- JSFX analyzer name
|
|
14
|
+
local MCP_ANALYZER_FX_NAME = "reaper-mcp/mcp_analyzer" -- JSFX analyzer name
|
|
15
15
|
|
|
16
16
|
-- Determine bridge directory (REAPER resource path / Scripts / mcp_bridge_data)
|
|
17
17
|
local bridge_dir = reaper.GetResourcePath() .. "/Scripts/mcp_bridge_data/"
|
|
@@ -1007,9 +1007,9 @@ end
|
|
|
1007
1007
|
-- Phase 4: Custom JSFX analyzer handlers
|
|
1008
1008
|
-- =============================================================================
|
|
1009
1009
|
|
|
1010
|
-
local MCP_LUFS_METER_FX_NAME = "mcp_lufs_meter"
|
|
1011
|
-
local MCP_CORRELATION_METER_FX_NAME = "mcp_correlation_meter"
|
|
1012
|
-
local MCP_CREST_FACTOR_FX_NAME = "mcp_crest_factor"
|
|
1010
|
+
local MCP_LUFS_METER_FX_NAME = "reaper-mcp/mcp_lufs_meter"
|
|
1011
|
+
local MCP_CORRELATION_METER_FX_NAME = "reaper-mcp/mcp_correlation_meter"
|
|
1012
|
+
local MCP_CREST_FACTOR_FX_NAME = "reaper-mcp/mcp_crest_factor"
|
|
1013
1013
|
|
|
1014
1014
|
-- Helper: find or auto-insert a named JSFX on a track.
|
|
1015
1015
|
-- Returns the FX index (0-based) on success, or nil + error message on failure.
|
|
@@ -2396,6 +2396,234 @@ function handlers.delete_envelope_point(params)
|
|
|
2396
2396
|
return { success = true, totalPoints = reaper.CountEnvelopePoints(env) }
|
|
2397
2397
|
end
|
|
2398
2398
|
|
|
2399
|
+
function handlers.create_track_envelope(params)
|
|
2400
|
+
local track = reaper.GetTrack(0, params.trackIndex)
|
|
2401
|
+
if not track then return nil, "Track " .. params.trackIndex .. " not found" end
|
|
2402
|
+
|
|
2403
|
+
local env = nil
|
|
2404
|
+
local env_name = nil
|
|
2405
|
+
|
|
2406
|
+
if params.fxIndex ~= nil then
|
|
2407
|
+
-- FX parameter envelope
|
|
2408
|
+
if params.paramIndex == nil then
|
|
2409
|
+
return nil, "paramIndex is required when fxIndex is provided"
|
|
2410
|
+
end
|
|
2411
|
+
local fx_count = reaper.TrackFX_GetNumParms(track)
|
|
2412
|
+
if fx_count == 0 then
|
|
2413
|
+
return nil, "Track " .. params.trackIndex .. " has no FX"
|
|
2414
|
+
end
|
|
2415
|
+
-- create=true to create the envelope if it doesn't exist
|
|
2416
|
+
env = reaper.GetFXEnvelope(track, params.fxIndex, params.paramIndex, true)
|
|
2417
|
+
if not env then
|
|
2418
|
+
return nil, "Failed to create FX parameter envelope (fxIndex=" .. params.fxIndex .. ", paramIndex=" .. params.paramIndex .. ")"
|
|
2419
|
+
end
|
|
2420
|
+
local _, pname = reaper.TrackFX_GetParamName(track, params.fxIndex, params.paramIndex)
|
|
2421
|
+
local _, fname = reaper.TrackFX_GetFXName(track, params.fxIndex)
|
|
2422
|
+
env_name = fname .. " / " .. pname
|
|
2423
|
+
elseif params.envelopeName then
|
|
2424
|
+
-- Built-in envelope: use GetTrackEnvelopeByName + show via state chunk
|
|
2425
|
+
env = reaper.GetTrackEnvelopeByName(track, params.envelopeName)
|
|
2426
|
+
if not env then
|
|
2427
|
+
-- Some envelopes need to be activated first via the track state chunk
|
|
2428
|
+
-- Toggle the envelope visibility via action or chunk editing
|
|
2429
|
+
local env_map = {
|
|
2430
|
+
["Volume"] = "VOLENV",
|
|
2431
|
+
["Pan"] = "PANENV",
|
|
2432
|
+
["Mute"] = "MUTEENV",
|
|
2433
|
+
["Width"] = "WIDTHENV",
|
|
2434
|
+
["Trim Volume"] = "VOLENV2",
|
|
2435
|
+
}
|
|
2436
|
+
local chunk_key = env_map[params.envelopeName]
|
|
2437
|
+
if not chunk_key then
|
|
2438
|
+
return nil, "Unknown envelope name: " .. params.envelopeName .. ". Use Volume, Pan, Mute, Width, or Trim Volume"
|
|
2439
|
+
end
|
|
2440
|
+
-- Get track chunk, insert envelope chunk if missing
|
|
2441
|
+
local _, chunk = reaper.GetTrackStateChunk(track, "", false)
|
|
2442
|
+
if not chunk:find(chunk_key) then
|
|
2443
|
+
-- Insert a minimal envelope chunk before the closing >
|
|
2444
|
+
local env_chunk = "\n<" .. chunk_key .. "\nACT 1 -1\nVIS 1 1 1\nLANEHEIGHT 0 0\nARM 0\nDEFSHAPE 0 -1 -1\n>\n"
|
|
2445
|
+
chunk = chunk:gsub("\n>$", env_chunk .. ">")
|
|
2446
|
+
reaper.SetTrackStateChunk(track, chunk, false)
|
|
2447
|
+
else
|
|
2448
|
+
-- Envelope exists in chunk but may be hidden; make it visible
|
|
2449
|
+
chunk = chunk:gsub("(" .. chunk_key .. "[^\n]*\n)ACT 0", "%1ACT 1")
|
|
2450
|
+
chunk = chunk:gsub("(" .. chunk_key .. "[^\n]*\n[^\n]*\n)VIS 0", "%1VIS 1")
|
|
2451
|
+
reaper.SetTrackStateChunk(track, chunk, false)
|
|
2452
|
+
end
|
|
2453
|
+
env = reaper.GetTrackEnvelopeByName(track, params.envelopeName)
|
|
2454
|
+
if not env then
|
|
2455
|
+
return nil, "Failed to create envelope: " .. params.envelopeName
|
|
2456
|
+
end
|
|
2457
|
+
else
|
|
2458
|
+
-- Envelope exists, ensure it's visible and active
|
|
2459
|
+
local _, chunk = reaper.GetEnvelopeStateChunk(env, "", false)
|
|
2460
|
+
chunk = chunk:gsub("ACT 0", "ACT 1")
|
|
2461
|
+
chunk = chunk:gsub("VIS 0", "VIS 1")
|
|
2462
|
+
reaper.SetEnvelopeStateChunk(env, chunk, false)
|
|
2463
|
+
end
|
|
2464
|
+
env_name = params.envelopeName
|
|
2465
|
+
else
|
|
2466
|
+
return nil, "Must provide either envelopeName or fxIndex+paramIndex"
|
|
2467
|
+
end
|
|
2468
|
+
|
|
2469
|
+
-- Find the index of the newly created envelope
|
|
2470
|
+
local env_count = reaper.CountTrackEnvelopes(track)
|
|
2471
|
+
local env_index = -1
|
|
2472
|
+
for i = 0, env_count - 1 do
|
|
2473
|
+
if reaper.GetTrackEnvelope(track, i) == env then
|
|
2474
|
+
env_index = i
|
|
2475
|
+
break
|
|
2476
|
+
end
|
|
2477
|
+
end
|
|
2478
|
+
|
|
2479
|
+
reaper.TrackList_AdjustWindows(false)
|
|
2480
|
+
reaper.UpdateArrange()
|
|
2481
|
+
|
|
2482
|
+
return {
|
|
2483
|
+
trackIndex = params.trackIndex,
|
|
2484
|
+
envelopeIndex = env_index,
|
|
2485
|
+
name = env_name,
|
|
2486
|
+
pointCount = reaper.CountEnvelopePoints(env),
|
|
2487
|
+
}
|
|
2488
|
+
end
|
|
2489
|
+
|
|
2490
|
+
function handlers.set_envelope_properties(params)
|
|
2491
|
+
local track = reaper.GetTrack(0, params.trackIndex)
|
|
2492
|
+
if not track then return nil, "Track " .. params.trackIndex .. " not found" end
|
|
2493
|
+
local env_count = reaper.CountTrackEnvelopes(track)
|
|
2494
|
+
if params.envelopeIndex >= env_count then
|
|
2495
|
+
return nil, "Envelope index " .. params.envelopeIndex .. " out of range (track has " .. env_count .. " envelopes)"
|
|
2496
|
+
end
|
|
2497
|
+
local env = reaper.GetTrackEnvelope(track, params.envelopeIndex)
|
|
2498
|
+
|
|
2499
|
+
if reaper.BR_EnvAlloc then
|
|
2500
|
+
-- SWS extension available: use BR_Env* functions
|
|
2501
|
+
local br_env = reaper.BR_EnvAlloc(env, false)
|
|
2502
|
+
local cur_active, cur_visible, cur_armed = reaper.BR_EnvGetProperties(br_env)
|
|
2503
|
+
local new_active = params.active ~= nil and params.active or cur_active
|
|
2504
|
+
local new_visible = params.visible ~= nil and params.visible or cur_visible
|
|
2505
|
+
local new_armed = params.armed ~= nil and params.armed or cur_armed
|
|
2506
|
+
reaper.BR_EnvSetProperties(br_env, new_active, new_visible, new_armed, false)
|
|
2507
|
+
reaper.BR_EnvFree(br_env, true) -- commit=true
|
|
2508
|
+
else
|
|
2509
|
+
-- Fallback: modify envelope state chunk directly
|
|
2510
|
+
local _, chunk = reaper.GetEnvelopeStateChunk(env, "", false)
|
|
2511
|
+
if params.active ~= nil then
|
|
2512
|
+
local val = params.active and "1" or "0"
|
|
2513
|
+
chunk = chunk:gsub("ACT %d", "ACT " .. val)
|
|
2514
|
+
end
|
|
2515
|
+
if params.visible ~= nil then
|
|
2516
|
+
local val = params.visible and "1" or "0"
|
|
2517
|
+
chunk = chunk:gsub("VIS %d", "VIS " .. val)
|
|
2518
|
+
end
|
|
2519
|
+
if params.armed ~= nil then
|
|
2520
|
+
local val = params.armed and "1" or "0"
|
|
2521
|
+
chunk = chunk:gsub("ARM %d", "ARM " .. val)
|
|
2522
|
+
end
|
|
2523
|
+
reaper.SetEnvelopeStateChunk(env, chunk, false)
|
|
2524
|
+
end
|
|
2525
|
+
|
|
2526
|
+
reaper.TrackList_AdjustWindows(false)
|
|
2527
|
+
reaper.UpdateArrange()
|
|
2528
|
+
|
|
2529
|
+
local _, name = reaper.GetEnvelopeName(env)
|
|
2530
|
+
-- Re-read properties
|
|
2531
|
+
local active, visible, armed = true, true, false
|
|
2532
|
+
if reaper.BR_EnvAlloc then
|
|
2533
|
+
local br_env = reaper.BR_EnvAlloc(env, false)
|
|
2534
|
+
active, visible, armed = reaper.BR_EnvGetProperties(br_env)
|
|
2535
|
+
reaper.BR_EnvFree(br_env, false)
|
|
2536
|
+
end
|
|
2537
|
+
return {
|
|
2538
|
+
trackIndex = params.trackIndex,
|
|
2539
|
+
envelopeIndex = params.envelopeIndex,
|
|
2540
|
+
name = name,
|
|
2541
|
+
active = active,
|
|
2542
|
+
visible = visible,
|
|
2543
|
+
armed = armed,
|
|
2544
|
+
}
|
|
2545
|
+
end
|
|
2546
|
+
|
|
2547
|
+
function handlers.clear_envelope(params)
|
|
2548
|
+
local track = reaper.GetTrack(0, params.trackIndex)
|
|
2549
|
+
if not track then return nil, "Track " .. params.trackIndex .. " not found" end
|
|
2550
|
+
local env_count = reaper.CountTrackEnvelopes(track)
|
|
2551
|
+
if params.envelopeIndex >= env_count then
|
|
2552
|
+
return nil, "Envelope index " .. params.envelopeIndex .. " out of range (track has " .. env_count .. " envelopes)"
|
|
2553
|
+
end
|
|
2554
|
+
local env = reaper.GetTrackEnvelope(track, params.envelopeIndex)
|
|
2555
|
+
local prev_count = reaper.CountEnvelopePoints(env)
|
|
2556
|
+
-- Delete all points by using a range from -infinity to +infinity
|
|
2557
|
+
reaper.DeleteEnvelopePointRange(env, -math.huge, math.huge)
|
|
2558
|
+
reaper.Envelope_SortPoints(env)
|
|
2559
|
+
return {
|
|
2560
|
+
trackIndex = params.trackIndex,
|
|
2561
|
+
envelopeIndex = params.envelopeIndex,
|
|
2562
|
+
deletedPoints = prev_count,
|
|
2563
|
+
totalPoints = reaper.CountEnvelopePoints(env),
|
|
2564
|
+
}
|
|
2565
|
+
end
|
|
2566
|
+
|
|
2567
|
+
function handlers.remove_envelope_points(params)
|
|
2568
|
+
local track = reaper.GetTrack(0, params.trackIndex)
|
|
2569
|
+
if not track then return nil, "Track " .. params.trackIndex .. " not found" end
|
|
2570
|
+
local env_count = reaper.CountTrackEnvelopes(track)
|
|
2571
|
+
if params.envelopeIndex >= env_count then
|
|
2572
|
+
return nil, "Envelope index " .. params.envelopeIndex .. " out of range (track has " .. env_count .. " envelopes)"
|
|
2573
|
+
end
|
|
2574
|
+
local env = reaper.GetTrackEnvelope(track, params.envelopeIndex)
|
|
2575
|
+
local prev_count = reaper.CountEnvelopePoints(env)
|
|
2576
|
+
reaper.DeleteEnvelopePointRange(env, params.timeStart, params.timeEnd)
|
|
2577
|
+
reaper.Envelope_SortPoints(env)
|
|
2578
|
+
local new_count = reaper.CountEnvelopePoints(env)
|
|
2579
|
+
return {
|
|
2580
|
+
trackIndex = params.trackIndex,
|
|
2581
|
+
envelopeIndex = params.envelopeIndex,
|
|
2582
|
+
timeStart = params.timeStart,
|
|
2583
|
+
timeEnd = params.timeEnd,
|
|
2584
|
+
deletedPoints = prev_count - new_count,
|
|
2585
|
+
totalPoints = new_count,
|
|
2586
|
+
}
|
|
2587
|
+
end
|
|
2588
|
+
|
|
2589
|
+
function handlers.insert_envelope_points(params)
|
|
2590
|
+
local track = reaper.GetTrack(0, params.trackIndex)
|
|
2591
|
+
if not track then return nil, "Track " .. params.trackIndex .. " not found" end
|
|
2592
|
+
local env_count = reaper.CountTrackEnvelopes(track)
|
|
2593
|
+
if params.envelopeIndex >= env_count then
|
|
2594
|
+
return nil, "Envelope index " .. params.envelopeIndex .. " out of range (track has " .. env_count .. " envelopes)"
|
|
2595
|
+
end
|
|
2596
|
+
local env = reaper.GetTrackEnvelope(track, params.envelopeIndex)
|
|
2597
|
+
|
|
2598
|
+
-- Parse the points JSON string
|
|
2599
|
+
local points_str = params.points
|
|
2600
|
+
local points = nil
|
|
2601
|
+
if type(points_str) == "string" then
|
|
2602
|
+
points = json_decode(points_str)
|
|
2603
|
+
elseif type(points_str) == "table" then
|
|
2604
|
+
points = points_str
|
|
2605
|
+
end
|
|
2606
|
+
if not points then return nil, "Failed to parse points JSON" end
|
|
2607
|
+
|
|
2608
|
+
local inserted = 0
|
|
2609
|
+
for _, pt in ipairs(points) do
|
|
2610
|
+
if pt.time and pt.value then
|
|
2611
|
+
local shape = pt.shape or 0
|
|
2612
|
+
local tension = pt.tension or 0
|
|
2613
|
+
reaper.InsertEnvelopePoint(env, pt.time, pt.value, shape, tension, false, true)
|
|
2614
|
+
inserted = inserted + 1
|
|
2615
|
+
end
|
|
2616
|
+
end
|
|
2617
|
+
|
|
2618
|
+
reaper.Envelope_SortPoints(env)
|
|
2619
|
+
return {
|
|
2620
|
+
trackIndex = params.trackIndex,
|
|
2621
|
+
envelopeIndex = params.envelopeIndex,
|
|
2622
|
+
insertedPoints = inserted,
|
|
2623
|
+
totalPoints = reaper.CountEnvelopePoints(env),
|
|
2624
|
+
}
|
|
2625
|
+
end
|
|
2626
|
+
|
|
2399
2627
|
-- =============================================================================
|
|
2400
2628
|
-- Command dispatcher
|
|
2401
2629
|
-- =============================================================================
|