@mthines/reaper-mcp 0.19.0 → 0.70.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/README.md +19 -570
- package/bridge-ui/mcp_bridge.html +129 -0
- package/bridge-ui/mcp_bridge.webmanifest +16 -0
- package/bridge-ui/mcp_bridge_icon_192.png +0 -0
- package/bridge-ui/mcp_bridge_icon_512.png +0 -0
- package/bridge-ui/mcp_bridge_sw.js +35 -0
- package/claude/agents/plugin-researcher.md +188 -0
- package/claude/knowledge/reference/aesthetics-scoring.md +48 -0
- package/{knowledge → claude/knowledge}/reference/common-mistakes.md +1 -1
- package/claude/knowledge/reference/lorekit-setup.md +36 -0
- package/claude/knowledge/reference/memory-protocol.md +377 -0
- package/{knowledge → claude/knowledge}/reference/metering.md +5 -5
- package/claude/knowledge/reference/perceived-loudness.md +182 -0
- package/{knowledge → claude/knowledge}/workflows/editing.md +4 -0
- package/{knowledge → claude/knowledge}/workflows/gain-staging.md +21 -23
- package/{knowledge → claude/knowledge}/workflows/learn-plugin.md +15 -48
- package/{knowledge → claude/knowledge}/workflows/master-bus.md +13 -1
- package/claude/knowledge/workflows/section-levels.md +138 -0
- package/{knowledge → claude/knowledge}/workflows/session-prep.md +4 -0
- package/claude/knowledge/workflows/transient-lighting.md +52 -0
- package/claude/skills/critique/SKILL.md +131 -0
- package/claude/skills/editing/SKILL.md +281 -0
- package/claude/skills/learn-plugin/SKILL.md +210 -0
- package/claude/skills/mastering/SKILL.md +151 -0
- package/claude/skills/mix-memory/SKILL.md +140 -0
- package/claude/skills/mixer/SKILL.md +253 -0
- package/claude/skills/new-extension/SKILL.md +324 -0
- package/main.js +7843 -1940
- package/package.json +17 -15
- package/reaper/CLAUDE.md +22 -4
- package/reaper/mcp_actions_lib.lua +375 -0
- package/reaper/mcp_analyzer.jsfx +27 -12
- package/reaper/mcp_arrange_lib.lua +949 -0
- package/reaper/mcp_bridge.lua +2321 -758
- package/reaper/mcp_edit_views_lib.lua +1378 -0
- package/reaper/mcp_envelopes_lib.lua +748 -0
- package/reaper/mcp_extensions_lib.lua +55 -0
- package/reaper/mcp_fxstate_lib.lua +144 -0
- package/reaper/mcp_gain_trace.jsfx +126 -0
- package/reaper/mcp_items_lib.lua +877 -0
- package/reaper/mcp_lufs_meter.jsfx +16 -13
- package/reaper/mcp_midi_lib.lua +555 -0
- package/reaper/mcp_mixing_lib.lua +1021 -0
- package/reaper/mcp_plan_lib.lua +801 -0
- package/reaper/mcp_prefs_lib.lua +127 -0
- package/reaper/mcp_project_lib.lua +1088 -0
- package/reaper/mcp_recording_lib.lua +595 -0
- package/reaper/mcp_render_lib.lua +492 -0
- package/reaper/mcp_snapshot_lib.lua +547 -120
- package/reaper/mcp_snapshot_next.lua +10 -4
- package/reaper/mcp_snapshot_prev.lua +11 -4
- package/reaper/mcp_snapshot_quick_save.lua +7 -32
- package/reaper/mcp_snapshots_ui_lib.lua +258 -0
- package/reaper/mcp_tracks_lib.lua +409 -0
- package/sidecar/analysis.py +661 -0
- package/sidecar/chords.py +96 -0
- package/sidecar/mixcheck.py +295 -0
- package/sidecar/requirements-chords.txt +6 -0
- package/sidecar/requirements-separation.txt +3 -0
- package/sidecar/requirements.txt +17 -0
- package/sidecar/separation.py +142 -0
- package/sidecar/server.py +622 -0
- package/sidecar/tests/test_analysis.py +249 -0
- package/sidecar/tests/test_chords.py +117 -0
- package/sidecar/tests/test_mixcheck.py +163 -0
- package/sidecar/tests/test_separation.py +73 -0
- package/sidecar/tests/test_server.py +464 -0
- package/sidecar/tests/test_verify.py +243 -0
- package/sidecar/tests/test_voice_render.py +268 -0
- package/sidecar/tests/test_voice_sketch.py +140 -0
- package/sidecar/verify.py +224 -0
- package/sidecar/voice_render.py +295 -0
- package/sidecar/voice_sketch.py +280 -0
- package/claude-agents/critique.md +0 -147
- package/claude-agents/editor.md +0 -184
- package/claude-agents/levels.md +0 -107
- package/claude-agents/mastering.md +0 -129
- package/claude-agents/mixer.md +0 -248
- package/claude-agents/preflight.md +0 -149
- package/claude-agents/producer.md +0 -159
- package/claude-agents/setup.md +0 -150
- package/claude-agents/stems.md +0 -135
- package/claude-rules/architecture.md +0 -39
- package/claude-rules/development.md +0 -54
- package/claude-rules/lua-bridge.md +0 -50
- package/claude-rules/testing.md +0 -42
- package/claude-skills/learn-plugin.md +0 -123
- package/knowledge/CLAUDE.md +0 -71
- package/knowledge/plugins/_template.md +0 -82
- package/knowledge/plugins/fabfilter/pro-c-2.md +0 -117
- package/knowledge/plugins/fabfilter/pro-l-2.md +0 -95
- package/knowledge/plugins/fabfilter/pro-q-3.md +0 -112
- package/knowledge/plugins/neural-dsp/helix-native.md +0 -104
- package/knowledge/plugins/stock-reaper/js-1175-compressor.md +0 -94
- package/knowledge/plugins/stock-reaper/rea-comp.md +0 -100
- package/knowledge/plugins/stock-reaper/rea-delay.md +0 -95
- package/knowledge/plugins/stock-reaper/rea-eq.md +0 -103
- package/knowledge/plugins/stock-reaper/rea-gate.md +0 -99
- package/knowledge/plugins/stock-reaper/rea-limit.md +0 -75
- package/knowledge/plugins/stock-reaper/rea-verb.md +0 -76
- package/knowledge/reference/perceived-loudness.md +0 -122
- package/reaper/mcp_snapshot_manager.lua +0 -1001
- /package/{knowledge → claude/knowledge}/genres/_template.md +0 -0
- /package/{knowledge → claude/knowledge}/genres/electronic.md +0 -0
- /package/{knowledge → claude/knowledge}/genres/hip-hop.md +0 -0
- /package/{knowledge → claude/knowledge}/genres/metal.md +0 -0
- /package/{knowledge → claude/knowledge}/genres/orchestral.md +0 -0
- /package/{knowledge → claude/knowledge}/genres/pop.md +0 -0
- /package/{knowledge → claude/knowledge}/genres/rock.md +0 -0
- /package/{knowledge → claude/knowledge}/reference/compression.md +0 -0
- /package/{knowledge → claude/knowledge}/reference/frequencies.md +0 -0
- /package/{knowledge → claude/knowledge}/workflows/delivery.md +0 -0
- /package/{knowledge → claude/knowledge}/workflows/drum-bus.md +0 -0
- /package/{knowledge → claude/knowledge}/workflows/low-end.md +0 -0
- /package/{knowledge → claude/knowledge}/workflows/stem-prep.md +0 -0
- /package/{knowledge → claude/knowledge}/workflows/vocal-chain.md +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mthines/reaper-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.70.2",
|
|
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",
|
|
@@ -26,11 +26,26 @@
|
|
|
26
26
|
"bin": {
|
|
27
27
|
"reaper-mcp": "./main.js"
|
|
28
28
|
},
|
|
29
|
+
"files": [
|
|
30
|
+
"main.js",
|
|
31
|
+
"reaper",
|
|
32
|
+
"sidecar",
|
|
33
|
+
"claude",
|
|
34
|
+
"bridge-ui",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public",
|
|
40
|
+
"provenance": true
|
|
41
|
+
},
|
|
29
42
|
"dependencies": {
|
|
30
|
-
"@inquirer/prompts": "^8.3.2",
|
|
31
43
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
32
44
|
"@opentelemetry/api": "^1.9.0",
|
|
45
|
+
"@opentelemetry/api-logs": "^0.213.0",
|
|
33
46
|
"@opentelemetry/auto-instrumentations-node": "^0.71.0",
|
|
47
|
+
"@opentelemetry/exporter-metrics-otlp-proto": "^0.213.0",
|
|
48
|
+
"@opentelemetry/exporter-trace-otlp-proto": "^0.213.0",
|
|
34
49
|
"@opentelemetry/resources": "^2.6.0",
|
|
35
50
|
"@opentelemetry/sdk-node": "^0.213.0",
|
|
36
51
|
"@opentelemetry/semantic-conventions": "^1.40.0",
|
|
@@ -38,18 +53,5 @@
|
|
|
38
53
|
},
|
|
39
54
|
"engines": {
|
|
40
55
|
"node": ">=20.0.0"
|
|
41
|
-
},
|
|
42
|
-
"files": [
|
|
43
|
-
"main.js",
|
|
44
|
-
"reaper/**",
|
|
45
|
-
"knowledge/**",
|
|
46
|
-
"claude-rules/**",
|
|
47
|
-
"claude-skills/**",
|
|
48
|
-
"claude-agents/**",
|
|
49
|
-
"README.md",
|
|
50
|
-
"LICENSE"
|
|
51
|
-
],
|
|
52
|
-
"publishConfig": {
|
|
53
|
-
"access": "public"
|
|
54
56
|
}
|
|
55
57
|
}
|
package/reaper/CLAUDE.md
CHANGED
|
@@ -7,8 +7,21 @@ Files installed INTO the REAPER DAW by the `setup` command. These run inside REA
|
|
|
7
7
|
| File | Language | Purpose |
|
|
8
8
|
|------|----------|---------|
|
|
9
9
|
| `mcp_bridge.lua` | Lua | Persistent bridge: polls for JSON commands, executes ReaScript API, writes responses |
|
|
10
|
-
| `
|
|
10
|
+
| `mcp_tracks_lib.lua` | Lua | Track structure and routing |
|
|
11
|
+
| `mcp_plan_lib.lua` | Lua | `run_plan` executor, JSON decoder, chunk helpers |
|
|
12
|
+
| `mcp_actions_lib.lua` | Lua | Action search/run proxy, selection setters |
|
|
13
|
+
| `mcp_project_lib.lua` | Lua | Project ops, render settings, tempo map, markers |
|
|
14
|
+
| `mcp_items_lib.lua` | Lua | Items, takes, take FX, FX chains, track templates, stem tracks (`insert_stems`) |
|
|
15
|
+
| `mcp_recording_lib.lua` | Lua | Record inputs, monitoring and modes, punch, fixed lanes, comping |
|
|
16
|
+
| `mcp_arrange_lib.lua` | Lua | Copy items, insert/remove time, copy a section, normalize/reverse/glue |
|
|
17
|
+
| `mcp_envelopes_lib.lua` | Lua | Envelopes, points (dB through the envelope's scaling), pooled automation items |
|
|
18
|
+
| `mcp_mixing_lib.lua` | Lua | Automation mode, FX modulation and pins, monitoring FX, freeze, apply FX, stretch markers and audio quantize |
|
|
19
|
+
| `mcp_render_lib.lua` | Lua | Track renders for the analysis tools: cache, multi-track stem pass, macOS focus hand-back |
|
|
20
|
+
| `mcp_midi_lib.lua` | Lua | Quantize, transform, MIDI file import, pitch-class profile for detect_key, bar/beat grid + per-segment profiles for detect_chords |
|
|
21
|
+
| `mcp_edit_views_lib.lua` | Lua | Status page results and before/after edit views |
|
|
11
22
|
| `mcp_snapshot_lib.lua` | Lua | Shared library for snapshot action scripts (path helpers, JSON, restore logic) |
|
|
23
|
+
| `mcp_prefs_lib.lua` | Lua | Keeps the page's read-only (lk_ro_) LoreKit token in `~/.config/reaper-mcp/lorekit.json` (mode 600) so every way of opening the page shares them; bridge holds it in `ext.prefs` (the bridge is at Lua's 200-locals limit: add no top-level local) |
|
|
24
|
+
| `mcp_snapshots_ui_lib.lua` | Lua | Status page Snapshots view: lists the folder, runs page restore/save requests through the bridge's dispatcher |
|
|
12
25
|
| `mcp_snapshot_next.lua` | Lua | Action: restore next snapshot (bind to key for A/B testing) |
|
|
13
26
|
| `mcp_snapshot_prev.lua` | Lua | Action: restore previous snapshot (bind to key for A/B testing) |
|
|
14
27
|
| `mcp_snapshot_quick_save.lua` | Lua | Action: save snapshot with auto-name, no dialog |
|
|
@@ -16,6 +29,7 @@ Files installed INTO the REAPER DAW by the `setup` command. These run inside REA
|
|
|
16
29
|
| `mcp_lufs_meter.jsfx` | JSFX/EEL2 | LUFS loudness metering |
|
|
17
30
|
| `mcp_correlation_meter.jsfx` | JSFX/EEL2 | Stereo correlation and width analysis |
|
|
18
31
|
| `mcp_crest_factor.jsfx` | JSFX/EEL2 | Crest factor (peak-to-RMS) measurement |
|
|
32
|
+
| `mcp_gain_trace.jsfx` | JSFX/EEL2 | Pre/post gain trace for pumping/ducking: pair it around a chain (Mode pre/post, same Slot), read p5/p1/std after playback. No `1e9`-style literals (EEL2 rejects them; `jsfx-lint.test.ts` checks) |
|
|
19
33
|
| `install.sh` | Shell | Manual install helper |
|
|
20
34
|
|
|
21
35
|
## Lua Bridge (`mcp_bridge.lua`)
|
|
@@ -26,6 +40,7 @@ Files installed INTO the REAPER DAW by the `setup` command. These run inside REA
|
|
|
26
40
|
3. Dispatches to handler function in the `handlers` table
|
|
27
41
|
4. Writes `response_{uuid}.json` with results
|
|
28
42
|
5. Writes `heartbeat.json` every 1s for liveness detection
|
|
43
|
+
6. Publishes a status feed to ExtState `MCPBridge/status` for the web status page (`apps/bridge-ui`)
|
|
29
44
|
|
|
30
45
|
### Adding a Handler
|
|
31
46
|
```lua
|
|
@@ -54,9 +69,12 @@ The command type string must exactly match `CommandType` in `libs/protocol/src/c
|
|
|
54
69
|
|
|
55
70
|
## Testing
|
|
56
71
|
|
|
57
|
-
- **
|
|
58
|
-
|
|
59
|
-
|
|
72
|
+
- **Lua runs in tests through wasmoon** (Lua 5.4 in WebAssembly) against a fake `reaper` API: the `*_lib.lua`
|
|
73
|
+
files are pure (they take the API as an argument) and are tested in `*-lib.test.ts`; the whole bridge runs in
|
|
74
|
+
`bridge-*.test.ts`. Put new logic in a lib, not in `mcp_bridge.lua` (its main chunk is at Lua's 200-locals limit)
|
|
75
|
+
- Check REAPER's real behaviour live before trusting the fake: some calls ignore arguments without an error
|
|
76
|
+
(e.g. `SetEnvelopePointEx` with a flagged automation item index), and the fake should model what you saw
|
|
77
|
+
- Server-side tests stub `sendCommand()` in `bridge.ts`
|
|
60
78
|
|
|
61
79
|
## Installation
|
|
62
80
|
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
-- mcp_actions_lib.lua — REAPER action proxy and selection setters.
|
|
2
|
+
--
|
|
3
|
+
-- Pure logic: every function takes the ReaScript API as `api`, so it runs
|
|
4
|
+
-- under a fake API in unit tests. Loaded by mcp_bridge.lua with dofile and
|
|
5
|
+
-- passed to mcp_plan_lib.lua (run_plan ops) through its ctx.
|
|
6
|
+
--
|
|
7
|
+
-- Command ids of custom actions, scripts and extension actions are assigned
|
|
8
|
+
-- per session; the stable handle is the named id ("_aee8…", "_SWS_…"), which
|
|
9
|
+
-- every result carries.
|
|
10
|
+
|
|
11
|
+
local M = {}
|
|
12
|
+
|
|
13
|
+
-- Sections with a command entry point. Inline MIDI editor and media explorer
|
|
14
|
+
-- have none, so they are not offered.
|
|
15
|
+
M.SECTIONS = { main = 0, midi_editor = 32060, midi_event_list = 32061 }
|
|
16
|
+
|
|
17
|
+
-- App-level actions that would end, replace or switch away from the project
|
|
18
|
+
-- the agent works in. Refused even with allowDialog. Lua patterns matched
|
|
19
|
+
-- against the lowercased name; REAPER lists some with a "File:" prefix and
|
|
20
|
+
-- some without ("Close current project tab", "New project tab").
|
|
21
|
+
local DENY_PATTERNS = {
|
|
22
|
+
"^file: quit", "^quit reaper",
|
|
23
|
+
"^file: close", "^close project", "^close all projects", "^close current project",
|
|
24
|
+
"^file: new project", "^new project$", "^new project %(", "^new project tab",
|
|
25
|
+
"^file: open project", "^open project$", "^open project%.%.%.", "^open project in new tab",
|
|
26
|
+
"^project tabs: close", "^project tabs: switch to",
|
|
27
|
+
-- Anywhere in the name: SWS and item actions that open or switch to another project.
|
|
28
|
+
"switch to .*project tab", "send next action to .*project tab",
|
|
29
|
+
"new project tab", "in new tab", "%(new tab",
|
|
30
|
+
"open project,", "open project %(", "open projects from", "open last project",
|
|
31
|
+
"open related project", "open associated", "open reaper project",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
local function section_id(name)
|
|
35
|
+
if name == nil or name == "" then return 0 end
|
|
36
|
+
local id = M.SECTIONS[name]
|
|
37
|
+
if not id then error("unknown section '" .. tostring(name) .. "' (use main, midi_editor or midi_event_list)", 0) end
|
|
38
|
+
return id
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
local function handle(api, sec)
|
|
42
|
+
return api.SectionFromUniqueID and api.SectionFromUniqueID(sec) or sec
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
local function enumerate(api, sec, visit)
|
|
46
|
+
local h = handle(api, sec)
|
|
47
|
+
local i = 0
|
|
48
|
+
while true do
|
|
49
|
+
local id, name = api.kbd_enumerateActions(h, i)
|
|
50
|
+
if not id or id <= 0 then return end
|
|
51
|
+
if visit(id, name or "") then return end
|
|
52
|
+
i = i + 1
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
local function named_id(api, id)
|
|
57
|
+
local s = api.ReverseNamedCommandLookup(id)
|
|
58
|
+
if s and s ~= "" then return s:sub(1, 1) == "_" and s or "_" .. s end
|
|
59
|
+
return nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
local function trim(s) return (s:gsub("^%s+", ""):gsub("%s+$", "")) end
|
|
63
|
+
|
|
64
|
+
function M.is_dialog(name)
|
|
65
|
+
name = trim(name)
|
|
66
|
+
return name:sub(-3) == "..." or name:sub(-3) == "\226\128\166" -- "…"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
function M.deny_reason(name)
|
|
70
|
+
local lower = trim(name):lower()
|
|
71
|
+
for _, p in ipairs(DENY_PATTERNS) do
|
|
72
|
+
if lower:find(p) then
|
|
73
|
+
return "'" .. trim(name) .. "' would close or replace the project the agent is working in"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
return nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
local function unsaved_project(api)
|
|
80
|
+
if not api.EnumProjects then return false end
|
|
81
|
+
local _, path = api.EnumProjects(-1, "")
|
|
82
|
+
return path == nil or path == ""
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
--- Whether running `name` opens a modal dialog: a "..." name, or any Save
|
|
86
|
+
--- (project, all projects, new version) on a project that was never saved,
|
|
87
|
+
--- where REAPER asks where to save it.
|
|
88
|
+
function M.opens_dialog(api, name)
|
|
89
|
+
if M.is_dialog(name) then return true end
|
|
90
|
+
return trim(name):lower():sub(1, 10) == "file: save" and unsaved_project(api)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
local function step_id(api, s)
|
|
94
|
+
local n = tonumber(s)
|
|
95
|
+
if n then return n end
|
|
96
|
+
local id = api.NamedCommandLookup(s:sub(1, 1) == "_" and s or "_" .. s)
|
|
97
|
+
return id and id > 0 and id or nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
--- Check an action and, for a Custom: macro, every step it runs (nested
|
|
101
|
+
--- macros too), since running the macro runs them all. Returns the deny
|
|
102
|
+
--- reason, or nil, and a label of what opens a dialog, or nil.
|
|
103
|
+
local function inspect(api, id, name, sec, macros, seen)
|
|
104
|
+
local denied = M.deny_reason(name)
|
|
105
|
+
if denied then return denied, nil end
|
|
106
|
+
local dialog = M.opens_dialog(api, name) and ("'" .. trim(name) .. "'") or nil
|
|
107
|
+
local nid = macros and named_id(api, id)
|
|
108
|
+
local macro = nid and macros[nid]
|
|
109
|
+
if not macro or seen[nid] then return nil, dialog end
|
|
110
|
+
seen[nid] = true
|
|
111
|
+
local h = handle(api, sec)
|
|
112
|
+
for k, s in ipairs(macro.steps) do
|
|
113
|
+
local sid = step_id(api, s)
|
|
114
|
+
local sname = sid and api.kbd_getTextFromCmd(sid, h)
|
|
115
|
+
if sname and sname ~= "" then
|
|
116
|
+
local d, g = inspect(api, sid, sname, sec, macros, seen)
|
|
117
|
+
if d then return "macro step " .. k .. ": " .. d, nil end
|
|
118
|
+
dialog = dialog or (g and ("macro step " .. k .. " " .. g))
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
return nil, dialog
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
local function shortcuts(api, sec, id)
|
|
125
|
+
if not api.CountActionShortcuts then return {} end
|
|
126
|
+
local h = handle(api, sec)
|
|
127
|
+
local out = {}
|
|
128
|
+
for k = 0, api.CountActionShortcuts(h, id) - 1 do
|
|
129
|
+
local ok, desc = api.GetActionShortcutDesc(h, id, k, "")
|
|
130
|
+
if ok and desc and desc ~= "" then out[#out + 1] = desc end
|
|
131
|
+
end
|
|
132
|
+
return out
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
local function section_name(sec)
|
|
136
|
+
for name, id in pairs(M.SECTIONS) do if id == sec then return name end end
|
|
137
|
+
return tostring(sec)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
--- `macros` (from parse_kb) is optional; with it a macro is flagged by its steps.
|
|
141
|
+
function M.describe(api, sec, id, name, macros)
|
|
142
|
+
local denied, dialog = inspect(api, id, name, sec, macros, {})
|
|
143
|
+
return {
|
|
144
|
+
id = id,
|
|
145
|
+
namedId = named_id(api, id),
|
|
146
|
+
name = name,
|
|
147
|
+
section = section_name(sec),
|
|
148
|
+
toggle = api.GetToggleCommandStateEx(sec, id), -- -1: not a toggle
|
|
149
|
+
shortcuts = shortcuts(api, sec, id),
|
|
150
|
+
opensDialog = dialog ~= nil,
|
|
151
|
+
denied = denied,
|
|
152
|
+
}
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
--- search_actions: every query word must appear (case-insensitive) in the
|
|
156
|
+
--- name or named id. Returns at most `limit` actions plus the total count.
|
|
157
|
+
function M.search(api, params, kb_text)
|
|
158
|
+
local sec = section_id(params.section)
|
|
159
|
+
local macros = M.parse_kb(kb_text)
|
|
160
|
+
local limit = math.max(1, math.min(tonumber(params.limit) or 20, 100))
|
|
161
|
+
local words = {}
|
|
162
|
+
for w in tostring(params.query or ""):lower():gmatch("%S+") do words[#words + 1] = w end
|
|
163
|
+
|
|
164
|
+
local found, total = {}, 0
|
|
165
|
+
enumerate(api, sec, function(id, name)
|
|
166
|
+
local hay = name:lower() .. " " .. (named_id(api, id) or ""):lower()
|
|
167
|
+
for _, w in ipairs(words) do
|
|
168
|
+
if not hay:find(w, 1, true) then return false end
|
|
169
|
+
end
|
|
170
|
+
total = total + 1
|
|
171
|
+
if #found < limit then found[#found + 1] = M.describe(api, sec, id, name, macros) end
|
|
172
|
+
return false
|
|
173
|
+
end)
|
|
174
|
+
return { section = section_name(sec), query = params.query or "", total = total, returned = #found, actions = found }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
--- Resolve an action reference to (id, name). Accepts a command id (number
|
|
178
|
+
--- or digit string), a named id with or without the leading underscore, or
|
|
179
|
+
--- an exact action name (case-insensitive, must be unique).
|
|
180
|
+
function M.resolve(api, ref, sec)
|
|
181
|
+
sec = sec or 0
|
|
182
|
+
local h = handle(api, sec)
|
|
183
|
+
local function by_id(id)
|
|
184
|
+
local name = api.kbd_getTextFromCmd(id, h)
|
|
185
|
+
if not name or name == "" then return nil, "no action with id " .. id .. " in section " .. section_name(sec) end
|
|
186
|
+
return id, name
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
if type(ref) == "number" then return by_id(math.floor(ref)) end
|
|
190
|
+
if type(ref) ~= "string" or trim(ref) == "" then return nil, "action reference missing" end
|
|
191
|
+
ref = trim(ref)
|
|
192
|
+
if ref:match("^%d+$") then return by_id(tonumber(ref)) end
|
|
193
|
+
|
|
194
|
+
if not ref:find("%s") then
|
|
195
|
+
local id = api.NamedCommandLookup(ref:sub(1, 1) == "_" and ref or "_" .. ref)
|
|
196
|
+
if id and id > 0 then return by_id(id) end
|
|
197
|
+
if ref:sub(1, 1) == "_" then return nil, "no action with named id " .. ref end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
local want, hits = ref:lower(), {}
|
|
201
|
+
enumerate(api, sec, function(id, name)
|
|
202
|
+
if trim(name):lower() == want then hits[#hits + 1] = { id = id, name = name } end
|
|
203
|
+
return false
|
|
204
|
+
end)
|
|
205
|
+
if #hits == 1 then return hits[1].id, hits[1].name end
|
|
206
|
+
if #hits > 1 then
|
|
207
|
+
local ids = {}
|
|
208
|
+
for k = 1, math.min(#hits, 4) do ids[k] = tostring(hits[k].id) end
|
|
209
|
+
return nil, "action name '" .. ref .. "' is ambiguous (ids " .. table.concat(ids, ", ") .. ") — use an id"
|
|
210
|
+
end
|
|
211
|
+
return nil, "no action named '" .. ref .. "' — use search_actions to find it"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
--- Why an action must not run now, or nil when it may. Pass `macros` (from
|
|
215
|
+
--- parse_kb) so a Custom: macro is judged by the steps it runs.
|
|
216
|
+
function M.check_runnable(api, id, name, sec, allow_dialog, macros)
|
|
217
|
+
local denied, dialog = inspect(api, id, name, sec, macros, {})
|
|
218
|
+
if denied then return "refused: " .. denied end
|
|
219
|
+
if dialog and not allow_dialog then
|
|
220
|
+
return "refused: " .. dialog .. " opens a dialog, which blocks REAPER (and this bridge) until a person closes it — pass allowDialog only when someone is at the screen"
|
|
221
|
+
end
|
|
222
|
+
if sec ~= 0 and not api.MIDIEditor_GetActive() then
|
|
223
|
+
return "'" .. trim(name) .. "' is a MIDI editor action, but no MIDI editor is open"
|
|
224
|
+
end
|
|
225
|
+
return nil
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
--- Run a resolved, checked action. No undo block of its own (callers own it).
|
|
229
|
+
function M.exec(api, id, sec)
|
|
230
|
+
if sec == 0 then
|
|
231
|
+
api.Main_OnCommandEx(id, 0, 0)
|
|
232
|
+
elseif sec == M.SECTIONS.midi_editor then
|
|
233
|
+
api.MIDIEditor_OnCommand(api.MIDIEditor_GetActive(), id)
|
|
234
|
+
else
|
|
235
|
+
api.MIDIEditor_LastFocused_OnCommand(id, true)
|
|
236
|
+
end
|
|
237
|
+
return api.GetToggleCommandStateEx(sec, id)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
--- run_action: resolve, refuse unsafe actions, run once in an undo block.
|
|
241
|
+
function M.run(api, params, kb_text)
|
|
242
|
+
local sec = section_id(params.section)
|
|
243
|
+
local id, name = M.resolve(api, params.action, sec)
|
|
244
|
+
if not id then return nil, name end
|
|
245
|
+
local macros = M.parse_kb(kb_text)
|
|
246
|
+
local refused = M.check_runnable(api, id, name, sec, params.allowDialog == true, macros)
|
|
247
|
+
if refused then return nil, refused end
|
|
248
|
+
|
|
249
|
+
api.Undo_BeginBlock()
|
|
250
|
+
local ok, toggle = pcall(M.exec, api, id, sec)
|
|
251
|
+
api.Undo_EndBlock("MCP: " .. trim(name), -1)
|
|
252
|
+
if not ok then return nil, "action '" .. trim(name) .. "' failed: " .. tostring(toggle) end
|
|
253
|
+
if api.UpdateArrange then api.UpdateArrange() end
|
|
254
|
+
local out = M.describe(api, sec, id, name, macros)
|
|
255
|
+
out.ran = true
|
|
256
|
+
out.toggle = toggle
|
|
257
|
+
return out
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
--- Parse reaper-kb.ini: Custom: macros (ACT) and scripts (SCR), keyed by
|
|
261
|
+
--- named id with a leading underscore.
|
|
262
|
+
function M.parse_kb(text)
|
|
263
|
+
local macros, scripts = {}, {}
|
|
264
|
+
for line in (text or ""):gmatch("[^\r\n]+") do
|
|
265
|
+
local sec, id, name, rest = line:match('^ACT%s+%d+%s+(%d+)%s+"([^"]*)"%s+"([^"]*)"%s*(.*)$')
|
|
266
|
+
if sec then
|
|
267
|
+
local steps = {}
|
|
268
|
+
for s in rest:gmatch("%S+") do steps[#steps + 1] = s end
|
|
269
|
+
macros["_" .. id] = { name = name, section = tonumber(sec), steps = steps }
|
|
270
|
+
else
|
|
271
|
+
local ssec, sid, sname, path = line:match('^SCR%s+%d+%s+(%d+)%s+(%S+)%s+"([^"]*)"%s+(.+)$')
|
|
272
|
+
if ssec then scripts["_" .. sid] = { name = sname, section = tonumber(ssec), path = (trim(path):gsub('^"(.*)"$', "%1")) } end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
return macros, scripts
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
--- get_action: details, plus a macro's steps or a script's path.
|
|
279
|
+
function M.get(api, params, kb_text)
|
|
280
|
+
local sec = section_id(params.section)
|
|
281
|
+
local id, name = M.resolve(api, params.action, sec)
|
|
282
|
+
if not id then return nil, name end
|
|
283
|
+
local macros, scripts = M.parse_kb(kb_text)
|
|
284
|
+
local out = M.describe(api, sec, id, name, macros)
|
|
285
|
+
local macro = out.namedId and macros[out.namedId]
|
|
286
|
+
if macro then
|
|
287
|
+
out.kind = "custom"
|
|
288
|
+
out.steps = {}
|
|
289
|
+
for k, s in ipairs(macro.steps) do
|
|
290
|
+
local sid = step_id(api, s)
|
|
291
|
+
local sname = sid and api.kbd_getTextFromCmd(sid, handle(api, sec)) or nil
|
|
292
|
+
out.steps[k] = { action = s, id = sid, name = (sname ~= "" and sname) or "(no such action in this REAPER — the macro step does nothing)" }
|
|
293
|
+
end
|
|
294
|
+
elseif out.namedId and scripts[out.namedId] then
|
|
295
|
+
out.kind = "script"
|
|
296
|
+
out.scriptPath = scripts[out.namedId].path
|
|
297
|
+
else
|
|
298
|
+
out.kind = out.namedId and "extension" or "native"
|
|
299
|
+
end
|
|
300
|
+
return out
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
-- ---------------------------------------------------------------------------
|
|
304
|
+
-- Selection
|
|
305
|
+
-- ---------------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
local function selected_tracks(api)
|
|
308
|
+
local out = {}
|
|
309
|
+
local master = api.GetMasterTrack(0)
|
|
310
|
+
if api.IsTrackSelected(master) then out[1] = { index = -1, name = "MASTER" } end
|
|
311
|
+
for i = 0, api.CountTracks(0) - 1 do
|
|
312
|
+
local tr = api.GetTrack(0, i)
|
|
313
|
+
if api.IsTrackSelected(tr) then
|
|
314
|
+
local _, name = api.GetTrackName(tr)
|
|
315
|
+
out[#out + 1] = { index = i, name = name }
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
return out
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
--- Select tracks. `tracks` are MediaTracks already resolved by the caller;
|
|
322
|
+
--- mode "replace" (default) clears the selection first, "add" keeps it.
|
|
323
|
+
function M.select_tracks(api, tracks, mode)
|
|
324
|
+
if mode ~= "add" then
|
|
325
|
+
api.SetTrackSelected(api.GetMasterTrack(0), false)
|
|
326
|
+
for i = 0, api.CountTracks(0) - 1 do api.SetTrackSelected(api.GetTrack(0, i), false) end
|
|
327
|
+
end
|
|
328
|
+
for _, tr in ipairs(tracks) do api.SetTrackSelected(tr, true) end
|
|
329
|
+
return selected_tracks(api)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
--- set_selected_tracks: resolve every ref first (via the injected resolver)
|
|
333
|
+
--- so an unknown track changes nothing.
|
|
334
|
+
function M.set_selected_tracks(api, refs, mode, resolve_track)
|
|
335
|
+
if type(refs) ~= "table" or #refs == 0 then return nil, "tracks array required" end
|
|
336
|
+
local tracks = {}
|
|
337
|
+
for k, ref in ipairs(refs) do
|
|
338
|
+
local res, err = resolve_track(api, ref, {})
|
|
339
|
+
if not res then return nil, "tracks[" .. (k - 1) .. "]: " .. err .. " — selection unchanged" end
|
|
340
|
+
tracks[k] = res.track
|
|
341
|
+
end
|
|
342
|
+
return { selected = M.select_tracks(api, tracks, mode) }
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
--- set_selected_items: items are { trackIndex, itemIndex } like the media
|
|
346
|
+
--- tools. All are resolved before the selection changes.
|
|
347
|
+
function M.set_selected_items(api, items, mode)
|
|
348
|
+
if type(items) ~= "table" or #items == 0 then return nil, "items array required" end
|
|
349
|
+
local found = {}
|
|
350
|
+
for k, it in ipairs(items) do
|
|
351
|
+
local tr = api.GetTrack(0, tonumber(it.trackIndex) or -1)
|
|
352
|
+
local item = tr and api.GetTrackMediaItem(tr, tonumber(it.itemIndex) or -1)
|
|
353
|
+
if not item then
|
|
354
|
+
return nil, "items[" .. (k - 1) .. "]: no item " .. tostring(it.itemIndex) .. " on track " .. tostring(it.trackIndex) .. " — selection unchanged"
|
|
355
|
+
end
|
|
356
|
+
found[k] = item
|
|
357
|
+
end
|
|
358
|
+
if mode ~= "add" then api.SelectAllMediaItems(0, false) end
|
|
359
|
+
for _, item in ipairs(found) do api.SetMediaItemSelected(item, true) end
|
|
360
|
+
if api.UpdateArrange then api.UpdateArrange() end
|
|
361
|
+
|
|
362
|
+
local selected = {}
|
|
363
|
+
for i = 0, api.CountTracks(0) - 1 do
|
|
364
|
+
local tr = api.GetTrack(0, i)
|
|
365
|
+
for j = 0, api.CountTrackMediaItems(tr) - 1 do
|
|
366
|
+
if api.IsMediaItemSelected(api.GetTrackMediaItem(tr, j)) then
|
|
367
|
+
selected[#selected + 1] = { trackIndex = i, itemIndex = j }
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
return { selected = selected }
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
M.section_id = section_id
|
|
375
|
+
return M
|
package/reaper/mcp_analyzer.jsfx
CHANGED
|
@@ -3,17 +3,20 @@ desc:MCP Spectrum Analyzer
|
|
|
3
3
|
// Writes frequency bin magnitudes (in dB) to shared memory (gmem)
|
|
4
4
|
// so the Lua bridge can read them and pass to the MCP server.
|
|
5
5
|
//
|
|
6
|
-
// gmem layout
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// gmem[
|
|
10
|
-
// gmem[
|
|
6
|
+
// gmem layout, one block of SLOT_STRIDE values per instance (base = slot * SLOT_STRIDE,
|
|
7
|
+
// slot set by the bridge to trackIndex + 1, master = 0), so analyzers on different
|
|
8
|
+
// tracks never overwrite each other:
|
|
9
|
+
// gmem[base + 0] = bin count (fft_size / 2)
|
|
10
|
+
// gmem[base + 1] = peak dB
|
|
11
|
+
// gmem[base + 2] = RMS dB
|
|
12
|
+
// gmem[base + 3 .. base + 3 + bin_count - 1] = magnitude in dB per frequency bin
|
|
11
13
|
|
|
12
14
|
options:gmem=MCPAnalyzer
|
|
13
15
|
|
|
14
16
|
slider1:fft_size_slider=4096<512,8192,1>FFT Size
|
|
15
17
|
slider2:window_type=1<0,3,1{Rectangular,Hann,Hamming,Blackman-Harris}>Window Function
|
|
16
18
|
slider3:smoothing=0.8<0,0.99,0.01>Smoothing
|
|
19
|
+
slider4:gmem_slot=0<0,2000,1>gmem Slot
|
|
17
20
|
|
|
18
21
|
@init
|
|
19
22
|
// Allocate memory blocks
|
|
@@ -62,8 +65,19 @@ slider3:smoothing=0.8<0,0.99,0.01>Smoothing
|
|
|
62
65
|
rms_sum = 0;
|
|
63
66
|
rms_count = 0;
|
|
64
67
|
|
|
68
|
+
SLOT_STRIDE = 4100; // 3 header values + up to 4096 bins (fft size 8192)
|
|
69
|
+
base = 0;
|
|
70
|
+
|
|
65
71
|
@slider
|
|
66
|
-
|
|
72
|
+
// A new slot starts empty, so the bridge reports "no data yet" instead of
|
|
73
|
+
// whatever a previous instance left there.
|
|
74
|
+
new_base = floor(gmem_slot + 0.5) * SLOT_STRIDE;
|
|
75
|
+
new_base != base ? (
|
|
76
|
+
base = new_base;
|
|
77
|
+
gmem[base] = 0;
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
new_fft_size = fft_size_slider; // named slider: slider1 stays 0
|
|
67
81
|
// Snap to valid FFT sizes
|
|
68
82
|
new_fft_size <= 512 ? new_fft_size = 512 :
|
|
69
83
|
new_fft_size <= 1024 ? new_fft_size = 1024 :
|
|
@@ -106,8 +120,9 @@ slider3:smoothing=0.8<0,0.99,0.01>Smoothing
|
|
|
106
120
|
// Copy to work buffer for FFT (real FFT needs 2x size)
|
|
107
121
|
memcpy(work_buf, fft_buf, fft_size);
|
|
108
122
|
|
|
109
|
-
// Perform real FFT
|
|
123
|
+
// Perform real FFT; its output is in permuted order, so restore frequency order
|
|
110
124
|
fft_real(work_buf, fft_size);
|
|
125
|
+
fft_permute(work_buf, fft_size / 2);
|
|
111
126
|
|
|
112
127
|
// Calculate magnitudes and write to gmem with smoothing
|
|
113
128
|
smooth = smoothing;
|
|
@@ -136,25 +151,25 @@ slider3:smoothing=0.8<0,0.99,0.01>Smoothing
|
|
|
136
151
|
mag_buf[i] = smooth * mag_buf[i] + inv_smooth * mag_db;
|
|
137
152
|
|
|
138
153
|
// Write to gmem
|
|
139
|
-
gmem[3 + i] = mag_buf[i];
|
|
154
|
+
gmem[base + 3 + i] = mag_buf[i];
|
|
140
155
|
|
|
141
156
|
i += 1;
|
|
142
157
|
);
|
|
143
158
|
|
|
144
159
|
// Write metadata to gmem
|
|
145
|
-
gmem[
|
|
160
|
+
gmem[base] = bin_count;
|
|
146
161
|
|
|
147
162
|
// Peak dB
|
|
148
163
|
peak_val < 0.000001 ? peak_val = 0.000001;
|
|
149
|
-
gmem[1] = 20.0 * log10(peak_val);
|
|
164
|
+
gmem[base + 1] = 20.0 * log10(peak_val);
|
|
150
165
|
|
|
151
166
|
// RMS dB
|
|
152
167
|
rms_count > 0 ? (
|
|
153
168
|
rms_val = sqrt(rms_sum / rms_count);
|
|
154
169
|
rms_val < 0.000001 ? rms_val = 0.000001;
|
|
155
|
-
gmem[2] = 20.0 * log10(rms_val);
|
|
170
|
+
gmem[base + 2] = 20.0 * log10(rms_val);
|
|
156
171
|
) : (
|
|
157
|
-
gmem[2] = -150.0;
|
|
172
|
+
gmem[base + 2] = -150.0;
|
|
158
173
|
);
|
|
159
174
|
|
|
160
175
|
// Reset accumulators
|