@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
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
-- mcp_tracks_lib.lua — track structure and routing (sends, receives,
|
|
2
|
+
-- hardware outputs, master send, sidechain channels, groups/VCA).
|
|
3
|
+
--
|
|
4
|
+
-- Pure logic: every function takes the ReaScript API as `api` and tracks
|
|
5
|
+
-- already resolved by the caller, so it runs under a fake API in unit tests.
|
|
6
|
+
-- Loaded by mcp_bridge.lua with dofile.
|
|
7
|
+
|
|
8
|
+
local M = {}
|
|
9
|
+
|
|
10
|
+
local function to_db(v)
|
|
11
|
+
if v <= 0 then return -150 end
|
|
12
|
+
return 20 * math.log(v, 10)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
local function from_db(db)
|
|
16
|
+
if db <= -150 then return 0 end
|
|
17
|
+
return 10 ^ (db / 20)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
local function is_master(api, tr)
|
|
21
|
+
return tr == api.GetMasterTrack(0)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
local function track_index(api, tr)
|
|
25
|
+
if is_master(api, tr) then return -1 end
|
|
26
|
+
return math.floor(api.GetMediaTrackInfo_Value(tr, "IP_TRACKNUMBER")) - 1
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
local function track_name(api, tr)
|
|
30
|
+
if is_master(api, tr) then return "MASTER" end
|
|
31
|
+
local _, name = api.GetTrackName(tr)
|
|
32
|
+
return name
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
local function label(api, tr)
|
|
36
|
+
return { index = track_index(api, tr), name = track_name(api, tr) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
-- ---------------------------------------------------------------------------
|
|
40
|
+
-- Colors: "#RRGGBB" <-> REAPER's native color with the custom-color flag.
|
|
41
|
+
-- ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
local CUSTOM_COLOR = 0x1000000
|
|
44
|
+
|
|
45
|
+
function M.parse_color(s)
|
|
46
|
+
if s == nil or s == "" or s == "none" then return 0 end
|
|
47
|
+
local hex = tostring(s):match("^#?(%x%x%x%x%x%x)$")
|
|
48
|
+
if not hex then return nil, "color must be #RRGGBB or \"none\", got '" .. tostring(s) .. "'" end
|
|
49
|
+
return tonumber(hex:sub(1, 2), 16), tonumber(hex:sub(3, 4), 16), tonumber(hex:sub(5, 6), 16)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
local function read_color(api, tr)
|
|
53
|
+
local c = math.floor(api.GetMediaTrackInfo_Value(tr, "I_CUSTOMCOLOR"))
|
|
54
|
+
if c & CUSTOM_COLOR == 0 then return nil end
|
|
55
|
+
local r, g, b = api.ColorFromNative(c & 0xFFFFFF)
|
|
56
|
+
return string.format("#%02X%02X%02X", r, g, b)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
-- ---------------------------------------------------------------------------
|
|
60
|
+
-- Track structure
|
|
61
|
+
-- ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
local FOLDER_MODES = { normal = 0, folder = 1, last = -1 }
|
|
64
|
+
|
|
65
|
+
function M.describe_track(api, tr)
|
|
66
|
+
local out = label(api, tr)
|
|
67
|
+
out.guid = api.GetTrackGUID(tr)
|
|
68
|
+
out.color = read_color(api, tr)
|
|
69
|
+
out.folderDepth = math.floor(api.GetMediaTrackInfo_Value(tr, "I_FOLDERDEPTH"))
|
|
70
|
+
out.folderCollapsed = math.floor(api.GetMediaTrackInfo_Value(tr, "I_FOLDERCOMPACT"))
|
|
71
|
+
out.channels = math.floor(api.GetMediaTrackInfo_Value(tr, "I_NCHAN"))
|
|
72
|
+
out.masterSend = api.GetMediaTrackInfo_Value(tr, "B_MAINSEND") ~= 0
|
|
73
|
+
return out
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
--- add_track: insert at `index` (default: end) with an optional name.
|
|
77
|
+
function M.add_track(api, params)
|
|
78
|
+
local count = api.CountTracks(0)
|
|
79
|
+
local at = tonumber(params.index) or count
|
|
80
|
+
if at < 0 or at > count then
|
|
81
|
+
return nil, "index " .. tostring(params.index) .. " is out of range (0-" .. count .. ")"
|
|
82
|
+
end
|
|
83
|
+
api.InsertTrackAtIndex(at, true)
|
|
84
|
+
local tr = api.GetTrack(0, at)
|
|
85
|
+
if params.name and params.name ~= "" then
|
|
86
|
+
api.GetSetMediaTrackInfo_String(tr, "P_NAME", params.name, true)
|
|
87
|
+
end
|
|
88
|
+
return M.describe_track(api, tr)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
--- delete_track: the master can never be deleted.
|
|
92
|
+
function M.delete_track(api, tr)
|
|
93
|
+
if is_master(api, tr) then return nil, "the master track cannot be deleted" end
|
|
94
|
+
local gone = label(api, tr)
|
|
95
|
+
api.DeleteTrack(tr)
|
|
96
|
+
return { deleted = gone, trackCount = api.CountTracks(0) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
--- move_track: move one track so it ends up at `to` (0-based, in the final
|
|
100
|
+
--- list). Uses ReorderSelectedTracks, so the selection is saved and restored.
|
|
101
|
+
function M.move_track(api, tr, to)
|
|
102
|
+
if is_master(api, tr) then return nil, "the master track cannot be moved" end
|
|
103
|
+
local count = api.CountTracks(0)
|
|
104
|
+
to = tonumber(to)
|
|
105
|
+
if not to or to < 0 or to >= count then
|
|
106
|
+
return nil, "toIndex " .. tostring(to) .. " is out of range (0-" .. (count - 1) .. ")"
|
|
107
|
+
end
|
|
108
|
+
local from = track_index(api, tr)
|
|
109
|
+
if from == to then return { track = label(api, tr), moved = false } end
|
|
110
|
+
|
|
111
|
+
local saved = {}
|
|
112
|
+
for i = 0, count - 1 do
|
|
113
|
+
local t = api.GetTrack(0, i)
|
|
114
|
+
saved[#saved + 1] = { t, api.IsTrackSelected(t) }
|
|
115
|
+
api.SetTrackSelected(t, t == tr)
|
|
116
|
+
end
|
|
117
|
+
-- "before" is an index in the list as it is now; moving down skips the
|
|
118
|
+
-- moved track's own slot.
|
|
119
|
+
local before = to > from and to + 1 or to
|
|
120
|
+
local ok = api.ReorderSelectedTracks(before, 0)
|
|
121
|
+
for _, s in ipairs(saved) do api.SetTrackSelected(s[1], s[2]) end
|
|
122
|
+
if not ok then return nil, "REAPER refused to move the track" end
|
|
123
|
+
return { track = label(api, tr), moved = true, from = from }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
--- set_track_folder: mode folder | normal | last (closes the enclosing
|
|
127
|
+
--- folder), or a raw depth change; optional collapsed state 0/1/2.
|
|
128
|
+
function M.set_track_folder(api, tr, params)
|
|
129
|
+
if is_master(api, tr) then return nil, "the master track cannot be in a folder" end
|
|
130
|
+
local depth = FOLDER_MODES[params.mode or ""]
|
|
131
|
+
if depth == nil then depth = tonumber(params.depth) end
|
|
132
|
+
if depth == nil and params.collapsed == nil then
|
|
133
|
+
return nil, "give mode (folder, normal or last), depth, or collapsed"
|
|
134
|
+
end
|
|
135
|
+
if depth ~= nil then api.SetMediaTrackInfo_Value(tr, "I_FOLDERDEPTH", depth) end
|
|
136
|
+
if params.collapsed ~= nil then
|
|
137
|
+
local c = tonumber(params.collapsed)
|
|
138
|
+
if c == nil or c < 0 or c > 2 then return nil, "collapsed must be 0 (open), 1 (small) or 2 (collapsed)" end
|
|
139
|
+
api.SetMediaTrackInfo_Value(tr, "I_FOLDERCOMPACT", c)
|
|
140
|
+
end
|
|
141
|
+
return M.describe_track(api, tr)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
--- set_track_info: name, color, channel count, master/parent send. Every
|
|
145
|
+
--- value is checked before anything is written.
|
|
146
|
+
function M.set_track_info(api, tr, params)
|
|
147
|
+
local r, g, b
|
|
148
|
+
if params.color ~= nil then
|
|
149
|
+
r, g, b = M.parse_color(params.color)
|
|
150
|
+
if r == nil then return nil, g end
|
|
151
|
+
end
|
|
152
|
+
local nchan = params.channels ~= nil and tonumber(params.channels) or nil
|
|
153
|
+
if params.channels ~= nil and (not nchan or nchan < 2 or nchan > 128 or nchan % 2 ~= 0) then
|
|
154
|
+
return nil, "channels must be an even number from 2 to 128"
|
|
155
|
+
end
|
|
156
|
+
if params.name ~= nil and is_master(api, tr) then return nil, "the master track cannot be renamed" end
|
|
157
|
+
if params.masterSend ~= nil and is_master(api, tr) then return nil, "the master track has no master send" end
|
|
158
|
+
|
|
159
|
+
if params.name ~= nil then api.GetSetMediaTrackInfo_String(tr, "P_NAME", tostring(params.name), true) end
|
|
160
|
+
if params.color ~= nil then
|
|
161
|
+
local native = g == nil and 0 or (api.ColorToNative(r, g, b) | CUSTOM_COLOR)
|
|
162
|
+
api.SetMediaTrackInfo_Value(tr, "I_CUSTOMCOLOR", native)
|
|
163
|
+
end
|
|
164
|
+
if nchan then api.SetMediaTrackInfo_Value(tr, "I_NCHAN", nchan) end
|
|
165
|
+
if params.masterSend ~= nil then api.SetMediaTrackInfo_Value(tr, "B_MAINSEND", params.masterSend and 1 or 0) end
|
|
166
|
+
return M.describe_track(api, tr)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
-- ---------------------------------------------------------------------------
|
|
170
|
+
-- Routing
|
|
171
|
+
-- ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
M.CATEGORIES = { send = 0, receive = -1, hardware = 1 }
|
|
174
|
+
local SEND_MODES = { ["post-fader"] = 0, ["pre-fx"] = 1, ["post-fx"] = 3 }
|
|
175
|
+
local MODE_NAMES = { [0] = "post-fader", [1] = "pre-fx", [3] = "post-fx" }
|
|
176
|
+
|
|
177
|
+
local function category(name)
|
|
178
|
+
local c = M.CATEGORIES[name or "send"]
|
|
179
|
+
if c == nil then error("unknown category '" .. tostring(name) .. "' (use send, receive or hardware)", 0) end
|
|
180
|
+
return c
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
-- A channel pair is given as its first channel, 1-based (1 = 1/2, 3 = 3/4).
|
|
184
|
+
-- REAPER stores the 0-based offset; -1 on a source means "no audio".
|
|
185
|
+
local function chan_offset(first, what)
|
|
186
|
+
if first == nil then return nil end
|
|
187
|
+
local n = tonumber(first)
|
|
188
|
+
if n == -1 and what == "srcChannel" then return -1 end
|
|
189
|
+
if not n or n < 1 or n > 127 or n % 1 ~= 0 then
|
|
190
|
+
return nil, what .. " must be the first channel of the pair, 1-127 (1 = 1/2, 3 = 3/4)"
|
|
191
|
+
end
|
|
192
|
+
return n - 1
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
local function describe_route(api, tr, cat, i)
|
|
196
|
+
local v = function(k) return api.GetTrackSendInfo_Value(tr, cat, i, k) end
|
|
197
|
+
local mode = math.floor(v("I_SENDMODE"))
|
|
198
|
+
local src = math.floor(v("I_SRCCHAN"))
|
|
199
|
+
local dst = math.floor(v("I_DSTCHAN"))
|
|
200
|
+
local out = {
|
|
201
|
+
index = i,
|
|
202
|
+
volume = to_db(v("D_VOL")),
|
|
203
|
+
pan = v("D_PAN"),
|
|
204
|
+
muted = v("B_MUTE") ~= 0,
|
|
205
|
+
mode = MODE_NAMES[mode] or tostring(mode),
|
|
206
|
+
srcChannel = src < 0 and -1 or (src & 0x3FF) + 1,
|
|
207
|
+
}
|
|
208
|
+
if cat == 1 then
|
|
209
|
+
out.outputChannel = (dst & 0x3FF) + 1
|
|
210
|
+
out.mono = dst & 1024 ~= 0
|
|
211
|
+
else
|
|
212
|
+
out.destChannel = (dst & 0x3FF) + 1
|
|
213
|
+
local other = cat == 0 and v("P_DESTTRACK") or v("P_SRCTRACK")
|
|
214
|
+
if other and other ~= 0 then
|
|
215
|
+
local who = label(api, other)
|
|
216
|
+
if cat == 0 then out.destTrack = who else out.srcTrack = who end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
return out
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
--- routing: everything get_track_routing reports beyond the parent/folder.
|
|
223
|
+
function M.routing(api, tr)
|
|
224
|
+
local function list(cat)
|
|
225
|
+
local out = {}
|
|
226
|
+
for i = 0, api.GetTrackNumSends(tr, cat) - 1 do out[#out + 1] = describe_route(api, tr, cat, i) end
|
|
227
|
+
return out
|
|
228
|
+
end
|
|
229
|
+
local master_send -- nil on the master, which has none
|
|
230
|
+
if not is_master(api, tr) then master_send = api.GetMediaTrackInfo_Value(tr, "B_MAINSEND") ~= 0 end
|
|
231
|
+
return {
|
|
232
|
+
sends = list(0),
|
|
233
|
+
receives = list(-1),
|
|
234
|
+
hardwareOutputs = list(1),
|
|
235
|
+
masterSend = master_send,
|
|
236
|
+
channels = math.floor(api.GetMediaTrackInfo_Value(tr, "I_NCHAN")),
|
|
237
|
+
}
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
-- Validate and collect the writable send fields; nothing is written here.
|
|
241
|
+
local function send_fields(params)
|
|
242
|
+
local f = {}
|
|
243
|
+
if params.volume ~= nil then
|
|
244
|
+
local db = tonumber(params.volume)
|
|
245
|
+
if not db then return nil, "volume must be a number in dB" end
|
|
246
|
+
f.D_VOL = from_db(db)
|
|
247
|
+
end
|
|
248
|
+
if params.pan ~= nil then
|
|
249
|
+
local p = tonumber(params.pan)
|
|
250
|
+
if not p or p < -1 or p > 1 then return nil, "pan must be from -1 to 1" end
|
|
251
|
+
f.D_PAN = p
|
|
252
|
+
end
|
|
253
|
+
if params.mute ~= nil then f.B_MUTE = params.mute and 1 or 0 end
|
|
254
|
+
if params.mode ~= nil then
|
|
255
|
+
f.I_SENDMODE = SEND_MODES[params.mode]
|
|
256
|
+
if f.I_SENDMODE == nil then return nil, "mode must be post-fader, pre-fx or post-fx" end
|
|
257
|
+
end
|
|
258
|
+
local err
|
|
259
|
+
f.I_SRCCHAN, err = chan_offset(params.srcChannel, "srcChannel")
|
|
260
|
+
if err then return nil, err end
|
|
261
|
+
f.I_DSTCHAN, err = chan_offset(params.destChannel, "destChannel")
|
|
262
|
+
if err then return nil, err end
|
|
263
|
+
return f
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
-- Sidechain: a send into channels 3/4 needs a destination with 4+ channels.
|
|
267
|
+
local function ensure_channels(api, dest, dst_offset)
|
|
268
|
+
if not dest or not dst_offset or dst_offset < 0 then return end
|
|
269
|
+
local need = dst_offset + 2
|
|
270
|
+
if need % 2 == 1 then need = need + 1 end
|
|
271
|
+
if api.GetMediaTrackInfo_Value(dest, "I_NCHAN") < need then
|
|
272
|
+
api.SetMediaTrackInfo_Value(dest, "I_NCHAN", need)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
local function apply_fields(api, tr, cat, i, f)
|
|
277
|
+
for _, k in ipairs({ "D_VOL", "D_PAN", "B_MUTE", "I_SENDMODE", "I_SRCCHAN", "I_DSTCHAN" }) do
|
|
278
|
+
if f[k] ~= nil then api.SetTrackSendInfo_Value(tr, cat, i, k, f[k]) end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
--- add_send: track -> track. Options: volume (dB), pan, mute, mode,
|
|
283
|
+
--- srcChannel / destChannel (first channel of a pair; 3 = sidechain 3/4).
|
|
284
|
+
function M.add_send(api, src, dest, params)
|
|
285
|
+
if src == dest then return nil, "a track cannot send to itself" end
|
|
286
|
+
if is_master(api, src) then return nil, "the master track cannot send to another track" end
|
|
287
|
+
-- REAPER creates nothing for a send into the master: tracks reach it through B_MAINSEND.
|
|
288
|
+
if is_master(api, dest) then return nil, "a track reaches the master through its master send, not a send: use set_track_info masterSend" end
|
|
289
|
+
local f, err = send_fields(params or {})
|
|
290
|
+
if not f then return nil, err end
|
|
291
|
+
local i = api.CreateTrackSend(src, dest)
|
|
292
|
+
if not i or i < 0 then return nil, "REAPER refused the send" end
|
|
293
|
+
ensure_channels(api, dest, f.I_DSTCHAN)
|
|
294
|
+
apply_fields(api, src, 0, i, f)
|
|
295
|
+
return { track = label(api, src), send = describe_route(api, src, 0, i) }
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
--- add_hardware_output: route a track to an audio output (outputChannel is
|
|
299
|
+
--- 1-based; mono uses one channel).
|
|
300
|
+
function M.add_hardware_output(api, tr, params)
|
|
301
|
+
local ch = tonumber(params.outputChannel or 1)
|
|
302
|
+
if not ch or ch < 1 or ch > 512 or ch % 1 ~= 0 then return nil, "outputChannel must be 1-512" end
|
|
303
|
+
local f, err = send_fields({ volume = params.volume, pan = params.pan, mute = params.mute, mode = params.mode })
|
|
304
|
+
if not f then return nil, err end
|
|
305
|
+
local i = api.CreateTrackSend(tr, nil)
|
|
306
|
+
if not i or i < 0 then return nil, "REAPER refused the hardware output" end
|
|
307
|
+
f.I_DSTCHAN = (ch - 1) | (params.mono and 1024 or 0)
|
|
308
|
+
apply_fields(api, tr, 1, i, f)
|
|
309
|
+
return { track = label(api, tr), hardwareOutput = describe_route(api, tr, 1, i) }
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
--- set_send: edit an existing send, receive or hardware output by index.
|
|
313
|
+
function M.set_send(api, tr, params)
|
|
314
|
+
local cat = category(params.category)
|
|
315
|
+
local i = tonumber(params.index)
|
|
316
|
+
local n = api.GetTrackNumSends(tr, cat)
|
|
317
|
+
if not i or i < 0 or i >= n then
|
|
318
|
+
return nil, (params.category or "send") .. " " .. tostring(params.index) .. " not found on track (it has " .. n .. ")"
|
|
319
|
+
end
|
|
320
|
+
local f, err = send_fields(params)
|
|
321
|
+
if not f then return nil, err end
|
|
322
|
+
if cat == 1 and params.destChannel ~= nil then return nil, "use outputChannel for a hardware output" end
|
|
323
|
+
if cat == 1 and (params.outputChannel ~= nil or params.mono ~= nil) then
|
|
324
|
+
local ch = tonumber(params.outputChannel)
|
|
325
|
+
if params.outputChannel ~= nil and (not ch or ch < 1 or ch > 512) then return nil, "outputChannel must be 1-512" end
|
|
326
|
+
f.I_DSTCHAN = ch and (ch - 1) or (math.floor(api.GetTrackSendInfo_Value(tr, cat, i, "I_DSTCHAN")) & 0x3FF)
|
|
327
|
+
if params.mono ~= nil then f.I_DSTCHAN = f.I_DSTCHAN | (params.mono and 1024 or 0) end
|
|
328
|
+
end
|
|
329
|
+
-- A channel edit moves only the offset (low 10 bits): it keeps the source
|
|
330
|
+
-- width (I_SRCCHAN >> 10) and the destination's mix-to-mono bit (1024).
|
|
331
|
+
local keep = { I_SRCCHAN = ~0x3FF, I_DSTCHAN = 1024 }
|
|
332
|
+
for k, mask in pairs(keep) do
|
|
333
|
+
local cur = math.floor(api.GetTrackSendInfo_Value(tr, cat, i, k))
|
|
334
|
+
if f[k] and f[k] >= 0 and cur >= 0 and not (k == "I_DSTCHAN" and cat == 1 and params.mono ~= nil) then
|
|
335
|
+
f[k] = f[k] | (cur & mask)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
local dst_offset = f.I_DSTCHAN and f.I_DSTCHAN & 0x3FF
|
|
339
|
+
if cat == 0 then ensure_channels(api, api.GetTrackSendInfo_Value(tr, 0, i, "P_DESTTRACK"), dst_offset) end
|
|
340
|
+
if cat == -1 then ensure_channels(api, tr, dst_offset) end
|
|
341
|
+
apply_fields(api, tr, cat, i, f)
|
|
342
|
+
return { track = label(api, tr), [params.category or "send"] = describe_route(api, tr, cat, i) }
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
--- remove_send: remove a send, receive or hardware output by index.
|
|
346
|
+
function M.remove_send(api, tr, params)
|
|
347
|
+
local cat = category(params.category)
|
|
348
|
+
local i = tonumber(params.index)
|
|
349
|
+
local n = api.GetTrackNumSends(tr, cat)
|
|
350
|
+
if not i or i < 0 or i >= n then
|
|
351
|
+
return nil, (params.category or "send") .. " " .. tostring(params.index) .. " not found on track (it has " .. n .. ")"
|
|
352
|
+
end
|
|
353
|
+
api.RemoveTrackSend(tr, cat, i)
|
|
354
|
+
return { track = label(api, tr), removed = i, remaining = api.GetTrackNumSends(tr, cat) }
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
-- ---------------------------------------------------------------------------
|
|
358
|
+
-- Groups / VCA
|
|
359
|
+
-- ---------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
M.GROUP_ROLES = {
|
|
362
|
+
volume_lead = "VOLUME_LEAD", volume_follow = "VOLUME_FOLLOW",
|
|
363
|
+
vca_lead = "VOLUME_VCA_LEAD", vca_follow = "VOLUME_VCA_FOLLOW",
|
|
364
|
+
pan_lead = "PAN_LEAD", pan_follow = "PAN_FOLLOW",
|
|
365
|
+
mute_lead = "MUTE_LEAD", mute_follow = "MUTE_FOLLOW",
|
|
366
|
+
solo_lead = "SOLO_LEAD", solo_follow = "SOLO_FOLLOW",
|
|
367
|
+
recarm_lead = "RECARM_LEAD", recarm_follow = "RECARM_FOLLOW",
|
|
368
|
+
polarity_lead = "POLARITY_LEAD", polarity_follow = "POLARITY_FOLLOW",
|
|
369
|
+
automode_lead = "AUTOMODE_LEAD", automode_follow = "AUTOMODE_FOLLOW",
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
local function group_call(api, group)
|
|
373
|
+
if group <= 32 then return api.GetSetTrackGroupMembership, 1 << (group - 1) end
|
|
374
|
+
return api.GetSetTrackGroupMembershipHigh, 1 << (group - 33)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
--- The roles a track has in one group (1-64).
|
|
378
|
+
function M.group_roles(api, tr, group)
|
|
379
|
+
local fn, bit = group_call(api, group)
|
|
380
|
+
local roles = {}
|
|
381
|
+
for role, name in pairs(M.GROUP_ROLES) do
|
|
382
|
+
if fn(tr, name, 0, 0) & bit ~= 0 then roles[#roles + 1] = role end
|
|
383
|
+
end
|
|
384
|
+
table.sort(roles)
|
|
385
|
+
return roles
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
--- set_track_group: turn the given roles on (or off with enable = false)
|
|
389
|
+
--- for one group. Roles are checked before anything is written.
|
|
390
|
+
function M.set_track_group(api, tr, params)
|
|
391
|
+
local group = tonumber(params.group)
|
|
392
|
+
if not group or group < 1 or group > 64 or group % 1 ~= 0 then return nil, "group must be 1-64" end
|
|
393
|
+
if type(params.roles) ~= "table" or #params.roles == 0 then return nil, "roles array required" end
|
|
394
|
+
for _, role in ipairs(params.roles) do
|
|
395
|
+
if not M.GROUP_ROLES[role] then
|
|
396
|
+
local names = {}
|
|
397
|
+
for k in pairs(M.GROUP_ROLES) do names[#names + 1] = k end
|
|
398
|
+
table.sort(names)
|
|
399
|
+
return nil, "unknown role '" .. tostring(role) .. "' (use " .. table.concat(names, ", ") .. ")"
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
local fn, bit = group_call(api, group)
|
|
403
|
+
local on = params.enable ~= false
|
|
404
|
+
for _, role in ipairs(params.roles) do fn(tr, M.GROUP_ROLES[role], bit, on and bit or 0) end
|
|
405
|
+
return { track = label(api, tr), group = group, roles = M.group_roles(api, tr, group) }
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
M.is_master = is_master
|
|
409
|
+
return M
|