@mthines/reaper-mcp 0.8.0 → 0.9.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.
@@ -0,0 +1,205 @@
1
+ ---
2
+ name: Session Preparation
3
+ id: session-prep
4
+ description: Organize and prepare a REAPER session for mixing — naming, coloring, routing, markers, bus structure
5
+ ---
6
+
7
+ # Session Preparation
8
+
9
+ ## When to Use
10
+
11
+ Before any mixing begins. Session preparation is the organizational groundwork that makes everything downstream faster and less error-prone. A well-prepared session prevents routing mistakes, makes navigation instant, and ensures no tracks are overlooked.
12
+
13
+ Use this workflow when:
14
+ - Opening a new session for the first time before mixing
15
+ - A session has grown organically and needs reorganization
16
+ - Tracks have generic names like "Audio_001" or "Track 14"
17
+ - There is no bus/routing structure in place
18
+ - The session has no markers for song sections
19
+
20
+ ## Prerequisites
21
+
22
+ - REAPER session is open with all recorded tracks
23
+ - You have a general idea of the song structure (verse, chorus, bridge, etc.)
24
+ - No mixing has been done yet (or you are willing to reorganize before continuing)
25
+
26
+ ## Step-by-Step
27
+
28
+ ### Step 1: Save a safety snapshot
29
+
30
+ ```
31
+ tool: snapshot_save
32
+ params:
33
+ name: "pre-session-prep"
34
+ description: "State before session organization"
35
+ ```
36
+
37
+ ### Step 2: Inventory all tracks
38
+
39
+ ```
40
+ tool: get_project_info
41
+ tool: list_tracks
42
+ ```
43
+
44
+ Note: track count, existing names, any folder/bus structure already present, sample rate, tempo.
45
+
46
+ Identify each track's instrument/source by its name, audio content, or position. If names are unclear, read media items to check source filenames:
47
+
48
+ ```
49
+ tool: list_media_items
50
+ params:
51
+ trackIndex: [n]
52
+ ```
53
+
54
+ ### Step 3: Rename tracks with descriptive names
55
+
56
+ Apply clear, consistent names. Standard naming conventions:
57
+
58
+ | Category | Examples |
59
+ |----------|---------|
60
+ | Drums | Kick In, Kick Out, Snare Top, Snare Bot, Hi-Hat, Tom 1, Tom 2, OH L, OH R, Room L, Room R |
61
+ | Bass | Bass DI, Bass Amp |
62
+ | Guitars | Gtr Rhythm L, Gtr Rhythm R, Gtr Lead, Gtr Clean, Gtr Acoustic |
63
+ | Keys | Piano, Organ, Synth Pad, Synth Lead |
64
+ | Vocals | Lead Vox, BV 1, BV 2, BV Harmony, Dbl Vox |
65
+ | Effects | FX Riser, FX Impact, FX Ambience |
66
+
67
+ ```
68
+ tool: set_track_property
69
+ params:
70
+ trackIndex: [n]
71
+ property: "name"
72
+ value: "Kick In"
73
+ ```
74
+
75
+ ### Step 4: Reorder and group tracks
76
+
77
+ Standard track ordering (top to bottom):
78
+ 1. **Drums** — Kick, Snare, Toms, Hi-Hat, Overheads, Room
79
+ 2. **Bass** — DI, Amp
80
+ 3. **Guitars** — Rhythm (L/R pairs), Lead, Clean, Acoustic
81
+ 4. **Keys/Synths** — Piano, Organ, Pads, Leads
82
+ 5. **Vocals** — Lead, Doubles, Backing Vocals, Harmonies
83
+ 6. **Effects/Samples** — Risers, Impacts, Pads
84
+ 7. **Buses** — Drum Bus, Instrument Bus, Vocal Bus, Effects Bus
85
+ 8. **Returns** — Reverb, Delay
86
+ 9. **Master/Mix Bus** — Always last
87
+
88
+ ### Step 5: Color code by group
89
+
90
+ Apply consistent colors across the session:
91
+
92
+ | Group | Suggested Color |
93
+ |-------|----------------|
94
+ | Drums | Blue |
95
+ | Bass | Dark Blue / Navy |
96
+ | Guitars | Green |
97
+ | Keys/Synths | Purple |
98
+ | Vocals | Orange / Yellow |
99
+ | Effects/Samples | Pink / Magenta |
100
+ | Buses | Gray |
101
+ | Returns | Teal |
102
+
103
+ ```
104
+ tool: set_track_property
105
+ params:
106
+ trackIndex: [n]
107
+ property: "color"
108
+ value: "0,100,200"
109
+ ```
110
+
111
+ ### Step 6: Set up bus/routing structure
112
+
113
+ Create submix buses for each instrument group. Typical bus structure:
114
+
115
+ | Bus | Routes From | Purpose |
116
+ |-----|------------|---------|
117
+ | Drum Bus | All drum tracks | Group processing, glue compression |
118
+ | Bass Bus | Bass DI + Amp | Blend and control |
119
+ | Guitar Bus | All guitar tracks | Group EQ, width control |
120
+ | Vocal Bus | Lead + BVs | Unified vocal processing |
121
+ | Instrument Bus | Guitar Bus + Keys | Non-rhythm instrument group |
122
+ | Effects Bus | FX tracks | Level control for effects |
123
+ | Reverb Return | Aux send destination | Shared reverb space |
124
+ | Delay Return | Aux send destination | Shared delay effects |
125
+
126
+ Check existing routing first:
127
+
128
+ ```
129
+ tool: get_track_routing
130
+ params:
131
+ trackIndex: [n]
132
+ ```
133
+
134
+ ### Step 7: Add markers for song sections
135
+
136
+ Navigate through the session and identify song sections. Add markers at each section boundary:
137
+
138
+ ```
139
+ tool: add_marker
140
+ params:
141
+ position: [seconds]
142
+ name: "Intro"
143
+ ```
144
+
145
+ Standard section markers:
146
+ - Intro
147
+ - Verse 1
148
+ - Pre-Chorus 1
149
+ - Chorus 1
150
+ - Verse 2
151
+ - Pre-Chorus 2
152
+ - Chorus 2
153
+ - Bridge
154
+ - Chorus 3 / Final Chorus
155
+ - Outro
156
+
157
+ Optionally add regions for each section:
158
+
159
+ ```
160
+ tool: add_region
161
+ params:
162
+ startPosition: [seconds]
163
+ endPosition: [seconds]
164
+ name: "Chorus 1"
165
+ ```
166
+
167
+ ### Step 8: Verify session parameters
168
+
169
+ ```
170
+ tool: get_project_info
171
+ ```
172
+
173
+ Confirm:
174
+ - Sample rate is consistent (44.1 kHz, 48 kHz, etc.)
175
+ - Tempo is correct
176
+ - Time signature is set
177
+
178
+ ### Step 9: Save post-prep snapshot
179
+
180
+ ```
181
+ tool: snapshot_save
182
+ params:
183
+ name: "post-session-prep"
184
+ description: "Session organized — tracks named, colored, routed, markers placed"
185
+ ```
186
+
187
+ ## Verification
188
+
189
+ After completing session preparation:
190
+
191
+ 1. Every track has a descriptive name (no "Audio_001" or "Track 14")
192
+ 2. Tracks are ordered by instrument group
193
+ 3. Each group has a consistent color
194
+ 4. Bus structure is in place (at minimum: drum bus, vocal bus)
195
+ 5. Song section markers are placed at correct positions
196
+ 6. All tracks route to appropriate buses (no orphan tracks going directly to master)
197
+ 7. Session parameters are verified
198
+
199
+ ## Common Pitfalls
200
+
201
+ - **Renaming without checking content**: Listen or check media items before naming — a track labeled "Guitar" might actually be a synth
202
+ - **Over-complex routing**: Start simple. A drum bus, vocal bus, and instrument bus is sufficient for most sessions. Add complexity only when needed.
203
+ - **Forgetting returns**: Reverb and delay sends need return tracks routed to the master bus
204
+ - **Inconsistent naming**: Pick a convention and stick with it — "Lead Vox" or "Lead Vocal" but not both
205
+ - **Not saving a snapshot**: Session prep involves many changes. Save before starting so you can revert if needed.
@@ -0,0 +1,175 @@
1
+ ---
2
+ name: Stem Preparation
3
+ id: stem-prep
4
+ description: Verify bus structure and routing for stem export readiness
5
+ ---
6
+
7
+ # Stem Preparation
8
+
9
+ ## When to Use
10
+
11
+ After mastering is complete and before final delivery. Stem preparation ensures the session's bus structure, routing, and naming conventions are correct so that exported stems will sum to match the full mix exactly.
12
+
13
+ Use this workflow when:
14
+ - The mix is complete and mastered, ready for stem export
15
+ - Stems are needed for sync licensing, remix, or immersive audio
16
+ - A mastering engineer has requested stems instead of a stereo mix
17
+ - The session needs routing verification before export
18
+
19
+ ## Prerequisites
20
+
21
+ - Mix is complete (all mixing and mastering decisions are finalized)
22
+ - Bus structure exists (drum bus, vocal bus, instrument bus, etc.)
23
+ - Session is playing back correctly (no muted tracks that should be active)
24
+ - Genre and delivery specs are known
25
+
26
+ ## Step-by-Step
27
+
28
+ ### Step 1: Save a safety snapshot
29
+
30
+ ```
31
+ tool: snapshot_save
32
+ params:
33
+ name: "pre-stem-prep"
34
+ description: "State before stem preparation"
35
+ ```
36
+
37
+ ### Step 2: Document the current bus structure
38
+
39
+ ```
40
+ tool: list_tracks
41
+ tool: get_project_info
42
+ ```
43
+
44
+ Map out the full routing hierarchy. For each track:
45
+
46
+ ```
47
+ tool: get_track_routing
48
+ params:
49
+ trackIndex: [n]
50
+ ```
51
+
52
+ Identify:
53
+ - Which tracks route to which buses
54
+ - Which buses route to the master
55
+ - Any orphan tracks routing directly to master (should go through a bus)
56
+ - Any tracks with sends to multiple destinations
57
+
58
+ ### Step 3: Verify standard stem groups
59
+
60
+ Typical stem groups for export:
61
+
62
+ | Stem | Contains | Bus |
63
+ |------|----------|-----|
64
+ | Drums | Kick, Snare, Toms, OH, Room | Drum Bus |
65
+ | Bass | Bass DI, Bass Amp | Bass Bus |
66
+ | Guitars | All guitar tracks | Guitar Bus |
67
+ | Keys/Synths | Piano, Organ, Pads, Leads | Keys Bus |
68
+ | Vocals | Lead, BVs, Harmonies | Vocal Bus |
69
+ | Effects | Reverbs, Delays (printed) | Effects Bus |
70
+
71
+ Verify each group routes correctly:
72
+
73
+ ```
74
+ tool: get_track_properties
75
+ params:
76
+ trackIndex: [bus track index]
77
+ ```
78
+
79
+ ### Step 4: Check for routing problems
80
+
81
+ Common issues to flag:
82
+
83
+ 1. **Orphan tracks**: Source tracks routing directly to master instead of through a bus
84
+ 2. **Double-routing**: A track routing to both a bus AND the master (will be in the stem AND the full mix twice)
85
+ 3. **Missing tracks**: Tracks that are muted or have no output
86
+ 4. **Send-only tracks**: Reverb returns that should be captured in a stem but are going to master instead of a bus
87
+ 5. **Master bus processing**: Note which FX are on the master bus — stems are typically exported pre-master-bus-processing
88
+
89
+ For each bus, check that all expected source tracks are present:
90
+
91
+ ```
92
+ tool: get_track_routing
93
+ params:
94
+ trackIndex: [bus index]
95
+ ```
96
+
97
+ ### Step 5: Verify naming conventions
98
+
99
+ Stems need clear, consistent names for delivery. Verify bus names follow a convention:
100
+
101
+ ```
102
+ [Song Title]_[Stem Name]_[Bit Depth]-[Sample Rate]
103
+ ```
104
+
105
+ Examples:
106
+ - `MySong_Drums_24-48.wav`
107
+ - `MySong_Vocals_24-48.wav`
108
+ - `MySong_Bass_24-48.wav`
109
+
110
+ Check that bus track names are clean and descriptive:
111
+
112
+ ```
113
+ tool: get_track_properties
114
+ params:
115
+ trackIndex: [bus index]
116
+ ```
117
+
118
+ Rename if needed:
119
+ ```
120
+ tool: set_track_property
121
+ params:
122
+ trackIndex: [bus index]
123
+ property: "name"
124
+ value: "Drum Bus"
125
+ ```
126
+
127
+ ### Step 6: Verify technical specs
128
+
129
+ ```
130
+ tool: get_project_info
131
+ ```
132
+
133
+ Confirm:
134
+ - **Sample rate**: Matches delivery requirements (typically 44.1 kHz or 48 kHz)
135
+ - **Bit depth**: 24-bit or 32-bit float for stems
136
+ - **All stems have same duration**: Check that all bus tracks span the full song (from before first note to after last decay)
137
+
138
+ ### Step 7: Document the stem map
139
+
140
+ Create a clear report of:
141
+ - Each stem name and what tracks it contains
142
+ - Routing chain for each stem
143
+ - Any processing on stem buses (baked into the stem)
144
+ - Master bus processing that will NOT be in individual stems
145
+ - Total stem count
146
+ - Technical specs (sample rate, bit depth)
147
+
148
+ ### Step 8: Save post-prep snapshot
149
+
150
+ ```
151
+ tool: snapshot_save
152
+ params:
153
+ name: "post-stem-prep"
154
+ description: "Stems verified — routing correct, naming consistent, ready for export"
155
+ ```
156
+
157
+ ## Verification
158
+
159
+ After completing stem preparation:
160
+
161
+ 1. Every source track routes to exactly one bus (no orphans, no double-routing)
162
+ 2. All buses route to the master
163
+ 3. Bus names are clear and follow a consistent convention
164
+ 4. No tracks are accidentally muted or disabled
165
+ 5. Stem groups make musical sense (all drums together, all vocals together, etc.)
166
+ 6. Technical specs are documented (sample rate, bit depth, duration)
167
+ 7. The user knows which master bus processing will NOT be included in individual stems
168
+
169
+ ## Common Pitfalls
170
+
171
+ - **Forgetting reverb/delay returns**: Shared effects (reverb bus, delay bus) need to be assigned to a stem or exported as their own stem. If they route to master only, they'll be in the full mix but not in any stem.
172
+ - **Double-counting sends**: If a track sends to both a bus and a reverb return, the dry signal is in one stem and the wet signal might be in another. This is usually correct, but document it.
173
+ - **Master bus processing**: If the master bus has EQ/compression/limiting, individual stems will sound different than the full mix. This is normal — stems are typically pre-master-bus.
174
+ - **Mismatched levels**: All stems should sum to equal the full mix at the master bus. If they don't, there's a routing or level issue.
175
+ - **Not checking mute states**: A muted track won't export. Verify all intended tracks are unmuted and active.
package/main.js CHANGED
@@ -134,12 +134,14 @@ function getTimeoutCounter() {
134
134
 
135
135
  // apps/reaper-mcp-server/src/bridge.ts
136
136
  import { randomUUID } from "node:crypto";
137
- import { readFile, writeFile, readdir, unlink, mkdir, stat } from "node:fs/promises";
137
+ import { appendFile, readFile, writeFile, readdir, unlink, mkdir, stat } from "node:fs/promises";
138
138
  import { join as join2 } from "node:path";
139
139
  import { homedir, platform } from "node:os";
140
140
  import { SpanKind, SpanStatusCode } from "@opentelemetry/api";
141
- var POLL_INTERVAL_MS = 50;
141
+ var POLL_INTERVAL_MS = 10;
142
142
  var DEFAULT_TIMEOUT_MS = 1e4;
143
+ var PROFILE_BRIDGE = process.env["BRIDGE_PROFILE"] === "1";
144
+ var lastTimings = null;
143
145
  function getReaperResourcePath() {
144
146
  const env = process.env["REAPER_RESOURCE_PATH"];
145
147
  if (env) return env;
@@ -164,6 +166,10 @@ async function ensureBridgeDir() {
164
166
  async function sendCommand(type, params = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
165
167
  const tracer = getTracer();
166
168
  const startMs = Date.now();
169
+ const t = {};
170
+ const profiling = PROFILE_BRIDGE;
171
+ const now = profiling ? () => performance.now() : () => 0;
172
+ const t0 = now();
167
173
  return tracer.startActiveSpan(
168
174
  `mcp.bridge ${type}`,
169
175
  {
@@ -174,7 +180,10 @@ async function sendCommand(type, params = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
174
180
  }
175
181
  },
176
182
  async (span) => {
183
+ if (profiling) t["spanSetup"] = now() - t0;
184
+ const tDir = now();
177
185
  const dir = await ensureBridgeDir();
186
+ if (profiling) t["ensureDir"] = now() - tDir;
178
187
  const id = randomUUID();
179
188
  span.setAttribute("mcp.command.id", id);
180
189
  const command2 = {
@@ -183,8 +192,13 @@ async function sendCommand(type, params = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
183
192
  params,
184
193
  timestamp: Date.now()
185
194
  };
195
+ const tWrite = now();
186
196
  const commandPath = join2(dir, `command_${id}.json`);
187
197
  await writeFile(commandPath, JSON.stringify(command2, null, 2), "utf-8");
198
+ const notifyPath = join2(dir, "_notify");
199
+ await appendFile(notifyPath, id + "\n");
200
+ if (profiling) t["write"] = now() - tWrite;
201
+ const tLog = now();
188
202
  const traceCtx = getTraceContext();
189
203
  console.error(
190
204
  JSON.stringify({
@@ -194,13 +208,22 @@ async function sendCommand(type, params = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
194
208
  ...traceCtx
195
209
  })
196
210
  );
211
+ if (profiling) t["log"] = now() - tLog;
197
212
  const responsePath = join2(dir, `response_${id}.json`);
198
213
  const deadline = Date.now() + timeoutMs;
214
+ const tPoll = now();
215
+ let pollAttempts = 0;
199
216
  while (Date.now() < deadline) {
200
217
  try {
218
+ pollAttempts++;
219
+ const tRead = now();
201
220
  const data = await readFile(responsePath, "utf-8");
202
221
  const response = JSON.parse(data);
222
+ if (profiling) t["readParse"] = now() - tRead;
223
+ if (profiling) t["pollWait"] = now() - tPoll - (t["readParse"] ?? 0);
224
+ const tCleanup = now();
203
225
  await Promise.allSettled([unlink(commandPath), unlink(responsePath)]);
226
+ if (profiling) t["cleanup"] = now() - tCleanup;
204
227
  const durationMs2 = Date.now() - startMs;
205
228
  const succeeded = response.success;
206
229
  span.setAttribute("mcp.response.success", succeeded);
@@ -219,8 +242,24 @@ async function sendCommand(type, params = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
219
242
  );
220
243
  }
221
244
  span.end();
245
+ const tMetrics = now();
222
246
  getCommandDurationHistogram().record(durationMs2, { command_type: type });
223
247
  getCommandCounter().add(1, { command_type: type, success: String(succeeded) });
248
+ if (profiling) t["metrics"] = now() - tMetrics;
249
+ if (profiling) {
250
+ lastTimings = {
251
+ spanSetupMs: Math.round(t["spanSetup"] ?? 0),
252
+ ensureDirMs: Math.round(t["ensureDir"] ?? 0),
253
+ writeMs: Math.round(t["write"] ?? 0),
254
+ logMs: Math.round(t["log"] ?? 0),
255
+ pollWaitMs: Math.round(t["pollWait"] ?? 0),
256
+ pollAttempts,
257
+ readParseMs: Math.round(t["readParse"] ?? 0),
258
+ cleanupMs: Math.round(t["cleanup"] ?? 0),
259
+ metricsMs: Math.round(t["metrics"] ?? 0),
260
+ totalMs: Math.round(now() - t0)
261
+ };
262
+ }
224
263
  return response;
225
264
  } catch {
226
265
  await sleep(POLL_INTERVAL_MS);
@@ -1797,6 +1836,11 @@ function ensureClaudeSettings(settingsPath) {
1797
1836
  writeFileSync(settingsPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
1798
1837
  return "updated";
1799
1838
  }
1839
+ function resolveAssetDirWithFallback(baseDir, buildName, sourceName) {
1840
+ const resolved = resolveAssetDir(baseDir, buildName);
1841
+ if (existsSync(resolved)) return resolved;
1842
+ return resolveAssetDir(baseDir, sourceName);
1843
+ }
1800
1844
 
1801
1845
  // apps/reaper-mcp-server/src/main.ts
1802
1846
  var __dirname = dirname2(fileURLToPath2(import.meta.url));
@@ -1855,7 +1899,7 @@ async function installSkills(scope) {
1855
1899
  } else {
1856
1900
  console.log("Knowledge base not found in package. Skipping.");
1857
1901
  }
1858
- const rulesSrc = resolveAssetDir(__dirname, "claude-rules");
1902
+ const rulesSrc = resolveAssetDirWithFallback(__dirname, "claude-rules", join4(".claude", "rules"));
1859
1903
  if (existsSync2(rulesSrc)) {
1860
1904
  const dest = join4(claudeDir, "rules");
1861
1905
  const count = copyDirSync(rulesSrc, dest);
@@ -1863,7 +1907,7 @@ async function installSkills(scope) {
1863
1907
  } else {
1864
1908
  console.log("Claude rules not found in package. Skipping.");
1865
1909
  }
1866
- const skillsSrc = resolveAssetDir(__dirname, "claude-skills");
1910
+ const skillsSrc = resolveAssetDirWithFallback(__dirname, "claude-skills", join4(".claude", "skills"));
1867
1911
  if (existsSync2(skillsSrc)) {
1868
1912
  const dest = join4(claudeDir, "skills");
1869
1913
  const count = copyDirSync(skillsSrc, dest);
@@ -1871,7 +1915,7 @@ async function installSkills(scope) {
1871
1915
  } else {
1872
1916
  console.log("Claude skills not found in package. Skipping.");
1873
1917
  }
1874
- const agentsSrc = resolveAssetDir(__dirname, "claude-agents");
1918
+ const agentsSrc = resolveAssetDirWithFallback(__dirname, "claude-agents", join4(".claude", "agents"));
1875
1919
  if (existsSync2(agentsSrc)) {
1876
1920
  const dest = join4(claudeDir, "agents");
1877
1921
  const count = copyDirSync(agentsSrc, dest);
@@ -1899,7 +1943,7 @@ Created: ${mcpJsonPath}`);
1899
1943
  }
1900
1944
  }
1901
1945
  console.log("\nDone! Claude Code now has mix engineer agents, knowledge, and REAPER MCP tools.");
1902
- console.log("All 48 REAPER tools are pre-approved \u2014 agents work autonomously.");
1946
+ console.log(`All ${MCP_TOOL_NAMES.length} REAPER tools are pre-approved \u2014 agents work autonomously.`);
1903
1947
  console.log('\nTry: @mix-engineer "Please gain stage my tracks"');
1904
1948
  console.log('Or: @mix-analyzer "Roast my mix"');
1905
1949
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mthines/reaper-mcp",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "description": "MCP server for controlling REAPER DAW — real-time mixing, FX control, and frequency analysis for AI agents",
6
6
  "license": "MIT",
@@ -9,7 +9,7 @@
9
9
  -- The script will keep running in the background via defer().
10
10
  -- =============================================================================
11
11
 
12
- local POLL_INTERVAL = 0.030 -- 30ms between polls
12
+ local POLL_INTERVAL = 0 -- scan every defer cycle; the notify file makes this cheap
13
13
  local HEARTBEAT_INTERVAL = 1.0 -- write heartbeat every 1s
14
14
  local MCP_ANALYZER_FX_NAME = "reaper-mcp/mcp_analyzer" -- JSFX analyzer name
15
15
 
@@ -21,6 +21,14 @@ reaper.RecursiveCreateDirectory(bridge_dir, 0)
21
21
 
22
22
  local last_poll = 0
23
23
  local last_heartbeat = 0
24
+ local defer_count = 0
25
+ local defer_intervals = {} -- circular buffer of last 100 defer intervals
26
+ local defer_idx = 0
27
+ local defer_buf_size = 100
28
+ local last_defer_time = 0
29
+ local scan_count = 0
30
+ local scan_durations = {} -- last 100 scan durations
31
+ local scan_idx = 0
24
32
 
25
33
  -- =============================================================================
26
34
  -- JSON Parser (minimal, sufficient for our command format)
@@ -243,6 +251,23 @@ local function list_files(dir, prefix)
243
251
  return files
244
252
  end
245
253
 
254
+ -- Read pending command IDs from the notify file (avoids directory listing entirely).
255
+ -- Returns a list of command filenames, or falls back to list_files if no notify file.
256
+ local notify_path = bridge_dir .. "_notify"
257
+ local function read_notify()
258
+ local f = io.open(notify_path, "r")
259
+ if not f then return nil end
260
+ local content = f:read("*a")
261
+ f:close()
262
+ os.remove(notify_path)
263
+ if not content or content == "" then return nil end
264
+ local files = {}
265
+ for id in content:gmatch("[^\n]+") do
266
+ files[#files + 1] = "command_" .. id .. ".json"
267
+ end
268
+ return files
269
+ end
270
+
246
271
  -- =============================================================================
247
272
  -- dB conversion helpers
248
273
  -- =============================================================================
@@ -2442,7 +2467,12 @@ function handlers.create_track_envelope(params)
2442
2467
  if not chunk:find(chunk_key) then
2443
2468
  -- Insert a minimal envelope chunk before the closing >
2444
2469
  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 .. ">")
2470
+ -- Use position capture to find the last ">" (closing the <TRACK block).
2471
+ -- We cannot use "\n>$" because GetTrackStateChunk may include a trailing newline.
2472
+ local last_gt = chunk:match(".*()>")
2473
+ if last_gt then
2474
+ chunk = chunk:sub(1, last_gt - 1) .. env_chunk .. chunk:sub(last_gt)
2475
+ end
2446
2476
  reaper.SetTrackStateChunk(track, chunk, false)
2447
2477
  else
2448
2478
  -- Envelope exists in chunk but may be hidden; make it visible
@@ -2624,11 +2654,57 @@ function handlers.insert_envelope_points(params)
2624
2654
  }
2625
2655
  end
2626
2656
 
2657
+ -- =============================================================================
2658
+ -- Bridge diagnostics handler
2659
+ -- =============================================================================
2660
+
2661
+ handlers["_bridge_diagnostics"] = function(params)
2662
+ -- Compute defer interval stats from circular buffer
2663
+ local intervals = {}
2664
+ for i = 1, math.min(defer_count, defer_buf_size) do
2665
+ intervals[#intervals + 1] = defer_intervals[i]
2666
+ end
2667
+ table.sort(intervals)
2668
+ local n = #intervals
2669
+ local sum = 0
2670
+ for _, v in ipairs(intervals) do sum = sum + v end
2671
+
2672
+ local scan_times = {}
2673
+ for i = 1, math.min(scan_count, defer_buf_size) do
2674
+ scan_times[#scan_times + 1] = scan_durations[i]
2675
+ end
2676
+ table.sort(scan_times)
2677
+ local sn = #scan_times
2678
+ local ssum = 0
2679
+ for _, v in ipairs(scan_times) do ssum = ssum + v end
2680
+
2681
+ return {
2682
+ pollInterval = POLL_INTERVAL * 1000,
2683
+ deferCount = defer_count,
2684
+ scanCount = scan_count,
2685
+ deferIntervals = n > 0 and {
2686
+ count = n,
2687
+ avgMs = math.floor(sum / n * 1000 + 0.5) / 1000,
2688
+ minMs = math.floor(intervals[1] * 1000 * 1000 + 0.5) / 1000,
2689
+ maxMs = math.floor(intervals[n] * 1000 * 1000 + 0.5) / 1000,
2690
+ p50Ms = math.floor(intervals[math.floor(n * 0.5) + 1] * 1000 * 1000 + 0.5) / 1000,
2691
+ p95Ms = math.floor(intervals[math.floor(n * 0.95) + 1] * 1000 * 1000 + 0.5) / 1000,
2692
+ } or nil,
2693
+ scanDurations = sn > 0 and {
2694
+ count = sn,
2695
+ avgMs = math.floor(ssum / sn * 1000 + 0.5) / 1000,
2696
+ minMs = math.floor(scan_times[1] * 1000 * 1000 + 0.5) / 1000,
2697
+ maxMs = math.floor(scan_times[sn] * 1000 * 1000 + 0.5) / 1000,
2698
+ } or nil,
2699
+ }
2700
+ end
2701
+
2627
2702
  -- =============================================================================
2628
2703
  -- Command dispatcher
2629
2704
  -- =============================================================================
2630
2705
 
2631
2706
  local function process_command(filename)
2707
+ local pickup_time = reaper.time_precise()
2632
2708
  local path = bridge_dir .. filename
2633
2709
  local content = read_file(path)
2634
2710
  if not content then return end
@@ -2660,6 +2736,13 @@ local function process_command(filename)
2660
2736
  }
2661
2737
  end
2662
2738
 
2739
+ -- Add pickup timing to response for profiling
2740
+ if cmd.timestamp then
2741
+ response._pickupMs = math.floor((pickup_time - (cmd.timestamp / 1000)) * 1000 + 0.5)
2742
+ response._deferCycle = defer_count
2743
+ response._scanCycle = scan_count
2744
+ end
2745
+
2663
2746
  -- Write response
2664
2747
  local response_path = bridge_dir .. "response_" .. cmd.id .. ".json"
2665
2748
  write_file(response_path, json_encode(response))
@@ -2688,13 +2771,26 @@ end
2688
2771
  local function main_loop()
2689
2772
  local now = reaper.time_precise()
2690
2773
 
2691
- -- Poll for commands at interval
2774
+ -- Track defer cadence
2775
+ if last_defer_time > 0 then
2776
+ defer_count = defer_count + 1
2777
+ defer_idx = (defer_idx % defer_buf_size) + 1
2778
+ defer_intervals[defer_idx] = now - last_defer_time
2779
+ end
2780
+ last_defer_time = now
2781
+
2782
+ -- Poll for commands: prefer notify file (instant), fall back to dir listing
2692
2783
  if now - last_poll >= POLL_INTERVAL then
2693
2784
  last_poll = now
2694
- local files = list_files(bridge_dir, "command_")
2785
+ local scan_start = reaper.time_precise()
2786
+ local files = read_notify() or list_files(bridge_dir, "command_")
2695
2787
  for _, filename in ipairs(files) do
2696
2788
  process_command(filename)
2697
2789
  end
2790
+ local scan_dur = reaper.time_precise() - scan_start
2791
+ scan_count = scan_count + 1
2792
+ scan_idx = (scan_idx % defer_buf_size) + 1
2793
+ scan_durations[scan_idx] = scan_dur
2698
2794
  end
2699
2795
 
2700
2796
  -- Write heartbeat at interval