@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,748 @@
|
|
|
1
|
+
-- mcp_envelopes_lib.lua — track envelopes, their points and automation items.
|
|
2
|
+
--
|
|
3
|
+
-- Pure logic: every function takes the ReaScript API as `api`, so it runs
|
|
4
|
+
-- under a fake API in unit tests (envelopes-lib.test.ts). Loaded by
|
|
5
|
+
-- mcp_bridge.lua with dofile.
|
|
6
|
+
--
|
|
7
|
+
-- Values: a point's `value` is the raw envelope value REAPER stores. Volume
|
|
8
|
+
-- envelopes are fader-scaled by default (716.2 = 0 dB), so for those every
|
|
9
|
+
-- point also reads and writes `db`, converted with the envelope's own scaling
|
|
10
|
+
-- mode (ScaleToEnvelopeMode / ScaleFromEnvelopeMode).
|
|
11
|
+
--
|
|
12
|
+
-- Automation items: `automationItemIndex` (optional on every point tool)
|
|
13
|
+
-- addresses the points inside one automation item instead of the underlying
|
|
14
|
+
-- envelope. Instances of one pool share their points, so an edit to one is an
|
|
15
|
+
-- edit to all of them.
|
|
16
|
+
--
|
|
17
|
+
-- Every function validates all of its input before the first write, so a
|
|
18
|
+
-- refused request changes nothing. Writes happen inside one undo block.
|
|
19
|
+
|
|
20
|
+
local M = {}
|
|
21
|
+
|
|
22
|
+
-- Built-in envelopes REAPER creates and shows with a native action (the
|
|
23
|
+
-- reliable path: a hand-written chunk was rejected on some tracks). Width has
|
|
24
|
+
-- no such action and falls back to the chunk.
|
|
25
|
+
M.BUILTIN = {
|
|
26
|
+
["Volume"] = { action = 40406, chunk = "VOLENV2", neutral = 1 },
|
|
27
|
+
["Pan"] = { action = 40407, chunk = "PANENV2", neutral = 0 },
|
|
28
|
+
["Mute"] = { action = 40867, chunk = "MUTEENV", neutral = 1 },
|
|
29
|
+
["Trim Volume"] = { action = 42020, chunk = "VOLENV3", neutral = 1 },
|
|
30
|
+
["Width"] = { chunk = "WIDTHENV2", neutral = 1 },
|
|
31
|
+
}
|
|
32
|
+
M.DELETE_AUTOMATION_ITEMS = 42086
|
|
33
|
+
M.MIN_DB = -150
|
|
34
|
+
M.POOL_POINTS = 0x10000000
|
|
35
|
+
|
|
36
|
+
local function int(v)
|
|
37
|
+
local n = tonumber(v)
|
|
38
|
+
if not n or n ~= math.floor(n) then return nil end
|
|
39
|
+
return n
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
local function with_undo(api, label, fn)
|
|
43
|
+
api.Undo_BeginBlock()
|
|
44
|
+
local ok, res, err = pcall(fn)
|
|
45
|
+
api.Undo_EndBlock(label, -1)
|
|
46
|
+
if api.UpdateArrange then api.UpdateArrange() end
|
|
47
|
+
if not ok then return nil, "Internal error: " .. tostring(res) end
|
|
48
|
+
return res, err
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
-- -----------------------------------------------------------------------------
|
|
52
|
+
-- Lookups (reads only)
|
|
53
|
+
-- -----------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
local function get_track(api, p)
|
|
56
|
+
local i = int(p.trackIndex)
|
|
57
|
+
if not i then return nil, "trackIndex required" end
|
|
58
|
+
local tr = i == -1 and api.GetMasterTrack(0) or (i >= 0 and api.GetTrack(0, i) or nil)
|
|
59
|
+
if not tr then return nil, "Track " .. i .. " not found" end
|
|
60
|
+
return tr
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
-- -> env, ai (-1 = the underlying envelope), track
|
|
64
|
+
function M.resolve(api, p)
|
|
65
|
+
local tr, err = get_track(api, p)
|
|
66
|
+
if not tr then return nil, err end
|
|
67
|
+
local ei = int(p.envelopeIndex)
|
|
68
|
+
if not ei then return nil, "envelopeIndex required" end
|
|
69
|
+
local count = api.CountTrackEnvelopes(tr)
|
|
70
|
+
if ei < 0 or ei >= count then
|
|
71
|
+
return nil, "Envelope index " .. ei .. " out of range (track has " .. count .. " envelopes)"
|
|
72
|
+
end
|
|
73
|
+
local env = api.GetTrackEnvelope(tr, ei)
|
|
74
|
+
local ai = -1
|
|
75
|
+
if p.automationItemIndex ~= nil then
|
|
76
|
+
ai = int(p.automationItemIndex)
|
|
77
|
+
local n = api.CountAutomationItems(env)
|
|
78
|
+
if not ai or ai < 0 or ai >= n then
|
|
79
|
+
return nil, "Automation item " .. tostring(p.automationItemIndex) .. " out of range (envelope has " .. n .. ")"
|
|
80
|
+
end
|
|
81
|
+
-- Point indexes over one loop iteration (the pool's own points), not every visible repeat
|
|
82
|
+
ai = ai | M.POOL_POINTS
|
|
83
|
+
end
|
|
84
|
+
return env, ai, tr
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
-- Volume envelopes (Volume, Volume (Pre-FX), Trim Volume, Send Volume) read in dB.
|
|
88
|
+
-- An FX parameter envelope is named "<param> / <fx>", so "Volume / ReaComp" is not one.
|
|
89
|
+
function M.is_volume(api, env)
|
|
90
|
+
local _, name = api.GetEnvelopeName(env)
|
|
91
|
+
name = name or ""
|
|
92
|
+
if name:find(" / ", 1, true) then return false, name end
|
|
93
|
+
return name:lower():find("volume", 1, true) ~= nil, name
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
local function scaling(api, env)
|
|
97
|
+
return api.GetEnvelopeScalingMode and api.GetEnvelopeScalingMode(env) or 0
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
function M.to_db(api, env, raw)
|
|
101
|
+
local amp = raw
|
|
102
|
+
if api.ScaleFromEnvelopeMode then amp = api.ScaleFromEnvelopeMode(scaling(api, env), raw) end
|
|
103
|
+
if not amp or amp <= 0 then return M.MIN_DB end
|
|
104
|
+
return math.max(M.MIN_DB, 20 * math.log(amp, 10))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
function M.from_db(api, env, db)
|
|
108
|
+
if db <= M.MIN_DB then return 0 end
|
|
109
|
+
local amp = 10 ^ (db / 20)
|
|
110
|
+
if api.ScaleToEnvelopeMode then return api.ScaleToEnvelopeMode(scaling(api, env), amp) end
|
|
111
|
+
return amp
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
local function round(v, places)
|
|
115
|
+
local m = 10 ^ places
|
|
116
|
+
return math.floor(v * m + 0.5) / m
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
local function read_point(api, env, ai, i, volume)
|
|
120
|
+
local _, time, value, shape, tension, selected = api.GetEnvelopePointEx(env, ai, i)
|
|
121
|
+
local pt = { index = i, time = time, value = value, shape = shape, tension = tension, selected = selected }
|
|
122
|
+
if volume then pt.db = round(M.to_db(api, env, value), 2) end
|
|
123
|
+
return pt
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
-- A point's value from `value` or `db` (one, not both; db only on volume envelopes)
|
|
127
|
+
local function point_value(api, env, volume, pt, where, required)
|
|
128
|
+
if pt.value ~= nil and pt.db ~= nil then return nil, where .. ": give value or db, not both" end
|
|
129
|
+
if pt.db ~= nil then
|
|
130
|
+
local db = tonumber(pt.db)
|
|
131
|
+
if not db then return nil, where .. ": db must be a number" end
|
|
132
|
+
if not volume then return nil, where .. ": db only applies to volume envelopes; use value" end
|
|
133
|
+
return M.from_db(api, env, db)
|
|
134
|
+
end
|
|
135
|
+
if pt.value ~= nil then
|
|
136
|
+
local v = tonumber(pt.value)
|
|
137
|
+
if not v then return nil, where .. ": value must be a number" end
|
|
138
|
+
return v
|
|
139
|
+
end
|
|
140
|
+
if required then return nil, where .. ": value or db required" end
|
|
141
|
+
return nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
local function check_shape(pt, where)
|
|
145
|
+
if pt.shape ~= nil then
|
|
146
|
+
local s = int(pt.shape)
|
|
147
|
+
if not s or s < 0 or s > 5 then return nil, where .. ": shape must be 0-5" end
|
|
148
|
+
end
|
|
149
|
+
if pt.tension ~= nil then
|
|
150
|
+
local t = tonumber(pt.tension)
|
|
151
|
+
if not t or t < -1 or t > 1 then return nil, where .. ": tension must be -1..1" end
|
|
152
|
+
end
|
|
153
|
+
return true
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
local function sort(api, env, ai)
|
|
157
|
+
if ai >= 0 then ai = ai & ~M.POOL_POINTS end
|
|
158
|
+
if api.Envelope_SortPointsEx then api.Envelope_SortPointsEx(env, ai) else api.Envelope_SortPoints(env) end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
-- REAPER keeps an automation item's points only inside its pool's own length
|
|
162
|
+
-- (D_POOL_QNLEN, end exclusive): a point past it is dropped from the pool with
|
|
163
|
+
-- no error. A new pool's length can also come out shorter than its item (49 QN
|
|
164
|
+
-- for a 50.25 QN item, seen live in REAPER 7.40). A point at the very end is
|
|
165
|
+
-- kept POOL_EDGE seconds inside it.
|
|
166
|
+
M.POOL_EDGE = 0.001
|
|
167
|
+
|
|
168
|
+
-- -> start, stop (project seconds) where the pool points of item `i` (plain index)
|
|
169
|
+
-- can sit; nil when an offset or play rate makes that mapping unclear
|
|
170
|
+
local function pool_span(api, env, i)
|
|
171
|
+
local get = function(k) return api.GetSetAutomationItemInfo(env, i, k, 0, false) end
|
|
172
|
+
local pos = get("D_POSITION")
|
|
173
|
+
if get("D_STARTOFFS") ~= 0 or get("D_PLAYRATE") ~= 1 then return nil end
|
|
174
|
+
local qn = api.TimeMap2_timeToQN and get("D_POOL_QNLEN")
|
|
175
|
+
if not qn or qn <= 0 then return pos, pos + get("D_LENGTH") end
|
|
176
|
+
return pos, api.TimeMap2_QNToTime(0, api.TimeMap2_timeToQN(0, pos) + qn)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
-- A point time inside automation item `ai` (flagged or plain): the time to write,
|
|
180
|
+
-- or nil and why not
|
|
181
|
+
local function pool_time(api, env, ai, t, where)
|
|
182
|
+
if ai < 0 then return t end
|
|
183
|
+
local i = ai & ~M.POOL_POINTS
|
|
184
|
+
local start, stop = pool_span(api, env, i)
|
|
185
|
+
if not start then return t end
|
|
186
|
+
if t < start - 1e-9 or t > stop + 1e-9 then
|
|
187
|
+
return nil, string.format("%s: time %.3f is outside automation item %d (its points run %.3f..%.3f)",
|
|
188
|
+
where, t, i, start, stop)
|
|
189
|
+
end
|
|
190
|
+
return math.min(t, stop - M.POOL_EDGE)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
local function target(p, ai)
|
|
194
|
+
local t = { trackIndex = int(p.trackIndex), envelopeIndex = int(p.envelopeIndex) }
|
|
195
|
+
if ai >= 0 then t.automationItemIndex = ai & ~M.POOL_POINTS end
|
|
196
|
+
return t
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
-- The send a send envelope belongs to (every one is named "Send Volume", "Send Pan", ...);
|
|
200
|
+
-- nil for any other envelope. I_SEND_IDX is 1-based, 0 when not a send.
|
|
201
|
+
local function send_of(api, env)
|
|
202
|
+
if not api.GetEnvelopeInfo_Value then return nil end
|
|
203
|
+
local idx = math.floor((tonumber((api.GetEnvelopeInfo_Value(env, "I_SEND_IDX"))) or 0) + 0.5)
|
|
204
|
+
if idx <= 0 then return nil end
|
|
205
|
+
local send = { index = idx - 1 }
|
|
206
|
+
local dest = api.GetEnvelopeInfo_Value(env, "P_DESTTRACK")
|
|
207
|
+
if type(dest) == "userdata" or type(dest) == "table" then
|
|
208
|
+
local _, name = api.GetTrackName(dest)
|
|
209
|
+
send.destTrackName = name
|
|
210
|
+
local n = math.floor(api.GetMediaTrackInfo_Value(dest, "IP_TRACKNUMBER") + 0.5)
|
|
211
|
+
send.destTrackIndex = n == -1 and -1 or n - 1
|
|
212
|
+
end
|
|
213
|
+
return send
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
function M.list_envelopes(api, p)
|
|
217
|
+
local tr, err = get_track(api, p)
|
|
218
|
+
if not tr then return nil, err end
|
|
219
|
+
local count = api.CountTrackEnvelopes(tr)
|
|
220
|
+
local envelopes = {}
|
|
221
|
+
for i = 0, count - 1 do
|
|
222
|
+
local env = api.GetTrackEnvelope(tr, i)
|
|
223
|
+
local _, name = api.GetEnvelopeName(env)
|
|
224
|
+
-- SWS reads the active/visible/armed state; without it these are defaults
|
|
225
|
+
local active, visible, armed = true, true, false
|
|
226
|
+
if api.BR_EnvAlloc then
|
|
227
|
+
local br = api.BR_EnvAlloc(env, false)
|
|
228
|
+
active, visible, armed = api.BR_EnvGetProperties(br)
|
|
229
|
+
api.BR_EnvFree(br, false)
|
|
230
|
+
end
|
|
231
|
+
envelopes[#envelopes + 1] = {
|
|
232
|
+
index = i, name = name, pointCount = api.CountEnvelopePoints(env),
|
|
233
|
+
active = active, visible = visible, armed = armed,
|
|
234
|
+
automationItems = api.CountAutomationItems and api.CountAutomationItems(env) or 0,
|
|
235
|
+
send = send_of(api, env),
|
|
236
|
+
}
|
|
237
|
+
end
|
|
238
|
+
return { trackIndex = int(p.trackIndex), envelopes = envelopes, count = count }
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
-- -----------------------------------------------------------------------------
|
|
242
|
+
-- Points
|
|
243
|
+
-- -----------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
function M.get_points(api, p)
|
|
246
|
+
local env, ai = M.resolve(api, p)
|
|
247
|
+
if not env then return nil, ai end
|
|
248
|
+
local volume, name = M.is_volume(api, env)
|
|
249
|
+
local total = api.CountEnvelopePointsEx(env, ai)
|
|
250
|
+
local offset = int(p.offset) or 0
|
|
251
|
+
local limit = int(p.limit) or total
|
|
252
|
+
local last = math.min(offset + limit, total)
|
|
253
|
+
local points = {}
|
|
254
|
+
for i = offset, last - 1 do points[#points + 1] = read_point(api, env, ai, i, volume) end
|
|
255
|
+
local r = target(p, ai)
|
|
256
|
+
r.name, r.unit, r.points, r.total, r.offset, r.hasMore = name, volume and "dB" or "raw", points, total, offset, last < total
|
|
257
|
+
return r
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
-- points: list of { time, value | db, shape?, tension? }
|
|
261
|
+
function M.insert_points(api, p, points)
|
|
262
|
+
local env, ai = M.resolve(api, p)
|
|
263
|
+
if not env then return nil, ai end
|
|
264
|
+
if type(points) ~= "table" or #points == 0 then return nil, "points must be a non-empty array" end
|
|
265
|
+
local volume = M.is_volume(api, env)
|
|
266
|
+
local todo = {}
|
|
267
|
+
for i, pt in ipairs(points) do
|
|
268
|
+
local where = "point " .. (i - 1)
|
|
269
|
+
local time = tonumber(pt.time)
|
|
270
|
+
if not time then return nil, where .. ": time required" end
|
|
271
|
+
local terr
|
|
272
|
+
time, terr = pool_time(api, env, ai, time, where)
|
|
273
|
+
if not time then return nil, terr end
|
|
274
|
+
local value, err = point_value(api, env, volume, pt, where, true)
|
|
275
|
+
if not value then return nil, err end
|
|
276
|
+
local ok, serr = check_shape(pt, where)
|
|
277
|
+
if not ok then return nil, serr end
|
|
278
|
+
todo[i] = { time, value, int(pt.shape) or 0, tonumber(pt.tension) or 0 }
|
|
279
|
+
end
|
|
280
|
+
return with_undo(api, "MCP: Insert " .. #todo .. " envelope point" .. (#todo == 1 and "" or "s"), function()
|
|
281
|
+
for _, t in ipairs(todo) do api.InsertEnvelopePointEx(env, ai, t[1], t[2], t[3], t[4], false, true) end
|
|
282
|
+
sort(api, env, ai)
|
|
283
|
+
local r = target(p, ai)
|
|
284
|
+
r.insertedPoints, r.totalPoints = #todo, api.CountEnvelopePointsEx(env, ai)
|
|
285
|
+
return r
|
|
286
|
+
end)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
-- edits: list of { index, time?, value? | db?, shape?, tension? }; fields left out keep their value
|
|
290
|
+
function M.edit_points(api, p, edits)
|
|
291
|
+
local env, ai = M.resolve(api, p)
|
|
292
|
+
if not env then return nil, ai end
|
|
293
|
+
if type(edits) ~= "table" or #edits == 0 then return nil, "points must be a non-empty array" end
|
|
294
|
+
local volume = M.is_volume(api, env)
|
|
295
|
+
local total = api.CountEnvelopePointsEx(env, ai)
|
|
296
|
+
local seen, todo = {}, {}
|
|
297
|
+
for i, e in ipairs(edits) do
|
|
298
|
+
local idx = int(e.index)
|
|
299
|
+
local where = "edit " .. (i - 1)
|
|
300
|
+
if not idx or idx < 0 or idx >= total then
|
|
301
|
+
return nil, where .. ": point index " .. tostring(e.index) .. " out of range (envelope has " .. total .. " points)"
|
|
302
|
+
end
|
|
303
|
+
if seen[idx] then return nil, where .. ": point " .. idx .. " is edited twice" end
|
|
304
|
+
seen[idx] = true
|
|
305
|
+
if e.time ~= nil and not tonumber(e.time) then return nil, where .. ": time must be a number" end
|
|
306
|
+
local time = tonumber(e.time)
|
|
307
|
+
if time then
|
|
308
|
+
local terr
|
|
309
|
+
time, terr = pool_time(api, env, ai, time, where)
|
|
310
|
+
if not time then return nil, terr end
|
|
311
|
+
end
|
|
312
|
+
local value, err = point_value(api, env, volume, e, where, false)
|
|
313
|
+
if err then return nil, err end
|
|
314
|
+
local ok, serr = check_shape(e, where)
|
|
315
|
+
if not ok then return nil, serr end
|
|
316
|
+
local cur = read_point(api, env, ai, idx, false)
|
|
317
|
+
todo[#todo + 1] = {
|
|
318
|
+
idx, time or cur.time, value or cur.value,
|
|
319
|
+
int(e.shape) or cur.shape, tonumber(e.tension) or cur.tension, cur.selected,
|
|
320
|
+
}
|
|
321
|
+
end
|
|
322
|
+
return with_undo(api, "MCP: Edit " .. #todo .. " envelope point" .. (#todo == 1 and "" or "s"), function()
|
|
323
|
+
-- SetEnvelopePointEx ignores a flagged index (seen live, REAPER 7.40): the plain item index edits its
|
|
324
|
+
-- first loop iteration, which is the pool's own points
|
|
325
|
+
local set_ai = ai >= 0 and (ai & ~M.POOL_POINTS) or ai
|
|
326
|
+
for _, t in ipairs(todo) do api.SetEnvelopePointEx(env, set_ai, t[1], t[2], t[3], t[4], t[5], t[6], true) end
|
|
327
|
+
sort(api, env, ai)
|
|
328
|
+
local r = target(p, ai)
|
|
329
|
+
r.editedPoints, r.totalPoints = #todo, api.CountEnvelopePointsEx(env, ai)
|
|
330
|
+
return r
|
|
331
|
+
end)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
function M.delete_point(api, p)
|
|
335
|
+
local env, ai = M.resolve(api, p)
|
|
336
|
+
if not env then return nil, ai end
|
|
337
|
+
local idx = int(p.pointIndex)
|
|
338
|
+
local total = api.CountEnvelopePointsEx(env, ai)
|
|
339
|
+
if not idx or idx < 0 or idx >= total then
|
|
340
|
+
return nil, "Point index " .. tostring(p.pointIndex) .. " out of range (envelope has " .. total .. " points)"
|
|
341
|
+
end
|
|
342
|
+
local deleted = read_point(api, env, ai, idx, M.is_volume(api, env))
|
|
343
|
+
return with_undo(api, "MCP: Delete envelope point", function()
|
|
344
|
+
api.DeleteEnvelopePointEx(env, ai, idx)
|
|
345
|
+
sort(api, env, ai)
|
|
346
|
+
local r = target(p, ai)
|
|
347
|
+
r.success, r.deleted, r.totalPoints = true, deleted, api.CountEnvelopePointsEx(env, ai)
|
|
348
|
+
return r
|
|
349
|
+
end)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
-- Deletes the points with timeStart <= time < timeEnd (all of them when both are nil)
|
|
353
|
+
function M.remove_range(api, p, label)
|
|
354
|
+
local env, ai = M.resolve(api, p)
|
|
355
|
+
if not env then return nil, ai end
|
|
356
|
+
local t0, t1 = tonumber(p.timeStart), tonumber(p.timeEnd)
|
|
357
|
+
local all = p.timeStart == nil and p.timeEnd == nil
|
|
358
|
+
if not all then
|
|
359
|
+
if not t0 or not t1 then return nil, "timeStart and timeEnd required" end
|
|
360
|
+
if t1 <= t0 then return nil, "timeEnd must be after timeStart" end
|
|
361
|
+
end
|
|
362
|
+
local before = api.CountEnvelopePointsEx(env, ai)
|
|
363
|
+
return with_undo(api, label, function()
|
|
364
|
+
if all then
|
|
365
|
+
api.DeleteEnvelopePointRangeEx(env, ai, -math.huge, math.huge)
|
|
366
|
+
else
|
|
367
|
+
api.DeleteEnvelopePointRangeEx(env, ai, t0, t1)
|
|
368
|
+
end
|
|
369
|
+
sort(api, env, ai)
|
|
370
|
+
local after = api.CountEnvelopePointsEx(env, ai)
|
|
371
|
+
local r = target(p, ai)
|
|
372
|
+
if not all then r.timeStart, r.timeEnd = t0, t1 end
|
|
373
|
+
r.deletedPoints, r.totalPoints = before - after, after
|
|
374
|
+
return r
|
|
375
|
+
end)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
-- -----------------------------------------------------------------------------
|
|
379
|
+
-- Creating built-in envelopes
|
|
380
|
+
-- -----------------------------------------------------------------------------
|
|
381
|
+
|
|
382
|
+
local function track_selection(api)
|
|
383
|
+
local sel = {}
|
|
384
|
+
for i = 0, api.CountTracks(0) - 1 do
|
|
385
|
+
local tr = api.GetTrack(0, i)
|
|
386
|
+
sel[#sel + 1] = { tr, api.IsTrackSelected(tr) }
|
|
387
|
+
end
|
|
388
|
+
local master = api.GetMasterTrack(0)
|
|
389
|
+
sel[#sel + 1] = { master, api.IsTrackSelected(master) }
|
|
390
|
+
return sel
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
local function restore_selection(api, sel)
|
|
394
|
+
for _, s in ipairs(sel) do api.SetTrackSelected(s[1], s[2]) end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
-- REAPER drops an envelope without points, so a new one gets one neutral point at 0
|
|
398
|
+
local function add_by_chunk(api, tr, spec)
|
|
399
|
+
local _, chunk = api.GetTrackStateChunk(tr, "", false)
|
|
400
|
+
if chunk:find("<" .. spec.chunk .. "\n", 1, true) then
|
|
401
|
+
chunk = chunk:gsub("(<" .. spec.chunk .. "[^\n]*\n)ACT 0", "%1ACT 1")
|
|
402
|
+
chunk = chunk:gsub("(<" .. spec.chunk .. "[^\n]*\n[^\n]*\n)VIS 0", "%1VIS 1")
|
|
403
|
+
else
|
|
404
|
+
local env_chunk = "<" .. spec.chunk .. "\nACT 1 -1\nVIS 1 1 1\nLANEHEIGHT 0 0\nARM 0\nDEFSHAPE 0 -1 -1\nPT 0 "
|
|
405
|
+
.. spec.neutral .. " 0\n>\n"
|
|
406
|
+
-- Envelopes belong before the first item; else before the closing > of the track
|
|
407
|
+
local at = chunk:find("\n<ITEM", 1, true)
|
|
408
|
+
if at then
|
|
409
|
+
chunk = chunk:sub(1, at) .. env_chunk .. chunk:sub(at + 1)
|
|
410
|
+
else
|
|
411
|
+
local last_gt = chunk:match(".*()>")
|
|
412
|
+
if not last_gt then return end
|
|
413
|
+
chunk = chunk:sub(1, last_gt - 1) .. env_chunk .. chunk:sub(last_gt)
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
api.SetTrackStateChunk(tr, chunk, false)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
local function show(api, env)
|
|
420
|
+
local ok, chunk = api.GetEnvelopeStateChunk(env, "", false)
|
|
421
|
+
if not ok or type(chunk) ~= "string" then return end
|
|
422
|
+
local shown = chunk:gsub("\nACT 0", "\nACT 1"):gsub("\nVIS 0", "\nVIS 1")
|
|
423
|
+
if shown ~= chunk then api.SetEnvelopeStateChunk(env, shown, false) end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
local function index_of(api, tr, env)
|
|
427
|
+
for i = 0, api.CountTrackEnvelopes(tr) - 1 do
|
|
428
|
+
if api.GetTrackEnvelope(tr, i) == env then return i end
|
|
429
|
+
end
|
|
430
|
+
return -1
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
-- A send's envelope always exists (GetTrackSendInfo_Value P_ENV) but stays out of the
|
|
434
|
+
-- track's envelope list until it is active, visible and has a point; it starts at the
|
|
435
|
+
-- send's current level, pan or mute so showing it changes nothing audible.
|
|
436
|
+
local SEND_ENVELOPES = {
|
|
437
|
+
Volume = { key = "<VOLENV", field = "D_VOL" },
|
|
438
|
+
Pan = { key = "<PANENV", field = "D_PAN" },
|
|
439
|
+
Mute = { key = "<MUTEENV", field = "B_MUTE" },
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
local function create_send_envelope(api, tr, p)
|
|
443
|
+
local spec = SEND_ENVELOPES[p.envelopeName or ""]
|
|
444
|
+
if not spec then return nil, "A send envelope is Volume, Pan or Mute (got " .. tostring(p.envelopeName) .. ")" end
|
|
445
|
+
if p.fxIndex ~= nil then return nil, "Give sendIndex or fxIndex, not both" end
|
|
446
|
+
local send, sends = int(p.sendIndex), api.GetTrackNumSends(tr, 0)
|
|
447
|
+
if not send or send < 0 or send >= sends then
|
|
448
|
+
return nil, "Send index " .. tostring(p.sendIndex) .. " out of range (track has " .. sends .. " sends)"
|
|
449
|
+
end
|
|
450
|
+
local env = api.GetTrackSendInfo_Value(tr, 0, send, "P_ENV:" .. spec.key)
|
|
451
|
+
if not env then return nil, "REAPER returned no envelope for send " .. send end
|
|
452
|
+
local name = "Send " .. p.envelopeName
|
|
453
|
+
return with_undo(api, "MCP: Create envelope '" .. name .. "'", function()
|
|
454
|
+
local created = index_of(api, tr, env) < 0
|
|
455
|
+
local ok, chunk = api.GetEnvelopeStateChunk(env, "", false)
|
|
456
|
+
if not ok or type(chunk) ~= "string" then return nil, "Could not read the send envelope" end
|
|
457
|
+
local shown = chunk:gsub("\nACT 0", "\nACT 1"):gsub("\nVIS 0", "\nVIS 1")
|
|
458
|
+
if not shown:find("\nPT ", 1, true) then
|
|
459
|
+
local current = api.GetTrackSendInfo_Value(tr, 0, send, spec.field)
|
|
460
|
+
local value
|
|
461
|
+
if spec.field == "D_VOL" then value = M.from_db(api, env, current > 0 and 20 * math.log(current, 10) or M.MIN_DB)
|
|
462
|
+
elseif spec.field == "B_MUTE" then value = current ~= 0 and 0 or 1
|
|
463
|
+
else value = -current end -- pan envelopes store left as positive (seen live, REAPER 7.40)
|
|
464
|
+
shown = shown:gsub(">%s*$", "PT 0 " .. string.format("%.10g", value) .. " 0\n>")
|
|
465
|
+
end
|
|
466
|
+
if shown ~= chunk then api.SetEnvelopeStateChunk(env, shown, false) end
|
|
467
|
+
if api.TrackList_AdjustWindows then api.TrackList_AdjustWindows(false) end
|
|
468
|
+
local dest = api.GetTrackSendInfo_Value(tr, 0, send, "P_DESTTRACK")
|
|
469
|
+
local res = {
|
|
470
|
+
trackIndex = int(p.trackIndex), envelopeIndex = index_of(api, tr, env), name = name,
|
|
471
|
+
created = created, pointCount = api.CountEnvelopePoints(env), send = { index = send },
|
|
472
|
+
}
|
|
473
|
+
if dest then res.send.destTrackName = select(2, api.GetTrackName(dest)) end
|
|
474
|
+
return res
|
|
475
|
+
end)
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
function M.create(api, p)
|
|
479
|
+
local tr, err = get_track(api, p)
|
|
480
|
+
if not tr then return nil, err end
|
|
481
|
+
if p.sendIndex ~= nil then return create_send_envelope(api, tr, p) end
|
|
482
|
+
local name = p.envelopeName
|
|
483
|
+
if p.fxIndex ~= nil then
|
|
484
|
+
local fx, param = int(p.fxIndex), int(p.paramIndex)
|
|
485
|
+
if not param then return nil, "paramIndex is required when fxIndex is provided" end
|
|
486
|
+
local fx_count = api.TrackFX_GetCount(tr)
|
|
487
|
+
if not fx or fx < 0 or fx >= fx_count then
|
|
488
|
+
return nil, "FX index " .. tostring(p.fxIndex) .. " out of range (track has " .. fx_count .. " FX)"
|
|
489
|
+
end
|
|
490
|
+
local param_count = api.TrackFX_GetNumParams(tr, fx)
|
|
491
|
+
if param < 0 or param >= param_count then
|
|
492
|
+
return nil, "Parameter index " .. param .. " out of range (FX has " .. param_count .. " parameters)"
|
|
493
|
+
end
|
|
494
|
+
elseif name then
|
|
495
|
+
if not M.BUILTIN[name] then
|
|
496
|
+
return nil, "Unknown envelope name: " .. tostring(name) .. ". Use Volume, Pan, Mute, Width, or Trim Volume"
|
|
497
|
+
end
|
|
498
|
+
else
|
|
499
|
+
return nil, "Must provide either envelopeName or fxIndex+paramIndex"
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
local label = "MCP: Create envelope '" .. (name or "FX parameter") .. "'"
|
|
503
|
+
return with_undo(api, label, function()
|
|
504
|
+
local env, created = nil, false
|
|
505
|
+
if p.fxIndex ~= nil then
|
|
506
|
+
created = api.GetFXEnvelope(tr, p.fxIndex, p.paramIndex, false) == nil
|
|
507
|
+
env = api.GetFXEnvelope(tr, p.fxIndex, p.paramIndex, true)
|
|
508
|
+
if not env then return nil, "Failed to create FX parameter envelope (fxIndex=" .. p.fxIndex .. ", paramIndex=" .. p.paramIndex .. ")" end
|
|
509
|
+
local _, pname = api.TrackFX_GetParamName(tr, p.fxIndex, p.paramIndex)
|
|
510
|
+
local _, fname = api.TrackFX_GetFXName(tr, p.fxIndex)
|
|
511
|
+
name = fname .. " / " .. pname
|
|
512
|
+
else
|
|
513
|
+
local spec = M.BUILTIN[name]
|
|
514
|
+
env = api.GetTrackEnvelopeByName(tr, name)
|
|
515
|
+
if not env then
|
|
516
|
+
created = true
|
|
517
|
+
if spec.action then
|
|
518
|
+
-- The action works on the selected tracks: select only this one, then put the selection back
|
|
519
|
+
local sel = track_selection(api)
|
|
520
|
+
api.SetOnlyTrackSelected(tr)
|
|
521
|
+
api.Main_OnCommand(spec.action, 0)
|
|
522
|
+
restore_selection(api, sel)
|
|
523
|
+
env = api.GetTrackEnvelopeByName(tr, name)
|
|
524
|
+
end
|
|
525
|
+
if not env then
|
|
526
|
+
add_by_chunk(api, tr, spec)
|
|
527
|
+
env = api.GetTrackEnvelopeByName(tr, name)
|
|
528
|
+
end
|
|
529
|
+
if not env then return nil, "Failed to create envelope: " .. name end
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
show(api, env)
|
|
533
|
+
if api.TrackList_AdjustWindows then api.TrackList_AdjustWindows(false) end
|
|
534
|
+
return {
|
|
535
|
+
trackIndex = int(p.trackIndex),
|
|
536
|
+
envelopeIndex = index_of(api, tr, env),
|
|
537
|
+
name = name,
|
|
538
|
+
created = created,
|
|
539
|
+
pointCount = api.CountEnvelopePoints(env),
|
|
540
|
+
}
|
|
541
|
+
end)
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
-- -----------------------------------------------------------------------------
|
|
545
|
+
-- Automation items
|
|
546
|
+
-- -----------------------------------------------------------------------------
|
|
547
|
+
|
|
548
|
+
local AI_FIELDS = {
|
|
549
|
+
position = "D_POSITION", length = "D_LENGTH", startOffset = "D_STARTOFFS", playRate = "D_PLAYRATE",
|
|
550
|
+
baseline = "D_BASELINE", amplitude = "D_AMPLITUDE",
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
local function ai_info(api, env, i)
|
|
554
|
+
local function get(parm) return api.GetSetAutomationItemInfo(env, i, parm, 0, false) end
|
|
555
|
+
local _, pool_name = api.GetSetAutomationItemInfo_String(env, i, "P_POOL_NAME", "", false)
|
|
556
|
+
local pool = math.floor(get("D_POOL_ID") + 0.5)
|
|
557
|
+
return {
|
|
558
|
+
index = i,
|
|
559
|
+
poolId = pool,
|
|
560
|
+
name = pool_name or "",
|
|
561
|
+
position = get("D_POSITION"),
|
|
562
|
+
length = get("D_LENGTH"),
|
|
563
|
+
startOffset = get("D_STARTOFFS"),
|
|
564
|
+
playRate = get("D_PLAYRATE"),
|
|
565
|
+
baseline = get("D_BASELINE"),
|
|
566
|
+
amplitude = get("D_AMPLITUDE"),
|
|
567
|
+
loop = get("D_LOOPSRC") ~= 0,
|
|
568
|
+
selected = get("D_UISEL") ~= 0,
|
|
569
|
+
pointCount = api.CountEnvelopePointsEx(env, i | M.POOL_POINTS),
|
|
570
|
+
}
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
-- Every automation item in the project, for pool ids and for selection
|
|
574
|
+
local function each_automation_item(api, fn)
|
|
575
|
+
local function track_envs(tr)
|
|
576
|
+
for e = 0, api.CountTrackEnvelopes(tr) - 1 do
|
|
577
|
+
local env = api.GetTrackEnvelope(tr, e)
|
|
578
|
+
for i = 0, api.CountAutomationItems(env) - 1 do fn(env, i) end
|
|
579
|
+
end
|
|
580
|
+
end
|
|
581
|
+
track_envs(api.GetMasterTrack(0))
|
|
582
|
+
for t = 0, api.CountTracks(0) - 1 do track_envs(api.GetTrack(0, t)) end
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
-- Instances of a pool anywhere in the project: pools are shared across tracks and envelopes
|
|
586
|
+
local function pool_instances(api, pool)
|
|
587
|
+
local n = 0
|
|
588
|
+
each_automation_item(api, function(e, i)
|
|
589
|
+
if math.floor(api.GetSetAutomationItemInfo(e, i, "D_POOL_ID", 0, false) + 0.5) == pool then n = n + 1 end
|
|
590
|
+
end)
|
|
591
|
+
return n
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
function M.list_items(api, p)
|
|
595
|
+
local env, err = M.resolve(api, p)
|
|
596
|
+
if not env then return nil, err end
|
|
597
|
+
local items = {}
|
|
598
|
+
for i = 0, api.CountAutomationItems(env) - 1 do items[#items + 1] = ai_info(api, env, i) end
|
|
599
|
+
local _, name = M.is_volume(api, env)
|
|
600
|
+
local r = target(p, -1)
|
|
601
|
+
r.name, r.items, r.count = name, items, #items
|
|
602
|
+
return r
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
local function check_item_fields(p)
|
|
606
|
+
for key in pairs(AI_FIELDS) do
|
|
607
|
+
if p[key] ~= nil and not tonumber(p[key]) then return nil, key .. " must be a number" end
|
|
608
|
+
end
|
|
609
|
+
if p.length ~= nil and tonumber(p.length) <= 0 then return nil, "length must be > 0" end
|
|
610
|
+
if p.position ~= nil and tonumber(p.position) < 0 then return nil, "position must be >= 0" end
|
|
611
|
+
if p.playRate ~= nil and tonumber(p.playRate) <= 0 then return nil, "playRate must be > 0" end
|
|
612
|
+
if p.baseline ~= nil and (tonumber(p.baseline) < 0 or tonumber(p.baseline) > 1) then return nil, "baseline must be 0..1" end
|
|
613
|
+
if p.amplitude ~= nil and (tonumber(p.amplitude) < -1 or tonumber(p.amplitude) > 1) then return nil, "amplitude must be -1..1" end
|
|
614
|
+
if p.name ~= nil and type(p.name) ~= "string" then return nil, "name must be a string" end
|
|
615
|
+
return true
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
local function apply_item_fields(api, env, i, p)
|
|
619
|
+
for key, parm in pairs(AI_FIELDS) do
|
|
620
|
+
if p[key] ~= nil and key ~= "position" and key ~= "length" then
|
|
621
|
+
api.GetSetAutomationItemInfo(env, i, parm, tonumber(p[key]), true)
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
if p.loop ~= nil then api.GetSetAutomationItemInfo(env, i, "D_LOOPSRC", p.loop and 1 or 0, true) end
|
|
625
|
+
if p.name ~= nil then api.GetSetAutomationItemInfo_String(env, i, "P_POOL_NAME", p.name, true) end
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
-- New automation item at position/length. poolId: an existing pool makes a pooled
|
|
629
|
+
-- instance (same points); omitted makes a new empty pool, filled from `points`
|
|
630
|
+
-- (times relative to the item start). captureEnvelope moves the underlying points
|
|
631
|
+
-- in that range into the new item instead.
|
|
632
|
+
function M.insert_item(api, p, points)
|
|
633
|
+
local env, err = M.resolve(api, { trackIndex = p.trackIndex, envelopeIndex = p.envelopeIndex })
|
|
634
|
+
if not env then return nil, err end
|
|
635
|
+
local pos, len = tonumber(p.position), tonumber(p.length)
|
|
636
|
+
if not pos or pos < 0 then return nil, "position (seconds, >= 0) required" end
|
|
637
|
+
if not len or len <= 0 then return nil, "length (seconds, > 0) required" end
|
|
638
|
+
local ok, ferr = check_item_fields(p)
|
|
639
|
+
if not ok then return nil, ferr end
|
|
640
|
+
local pool = p.poolId ~= nil and int(p.poolId) or nil
|
|
641
|
+
if p.poolId ~= nil and (not pool or pool < 0) then return nil, "poolId must be an integer >= 0" end
|
|
642
|
+
if p.captureEnvelope and pool then return nil, "captureEnvelope makes a new pool; leave out poolId" end
|
|
643
|
+
local existing = false
|
|
644
|
+
if pool then
|
|
645
|
+
each_automation_item(api, function(e, i)
|
|
646
|
+
if math.floor(api.GetSetAutomationItemInfo(e, i, "D_POOL_ID", 0, false) + 0.5) == pool then existing = true end
|
|
647
|
+
end)
|
|
648
|
+
end
|
|
649
|
+
if existing and points then return nil, "points only apply to a new pool; edit the pool's points with edit_envelope_points" end
|
|
650
|
+
local volume = M.is_volume(api, env)
|
|
651
|
+
local todo = {}
|
|
652
|
+
for i, pt in ipairs(points or {}) do
|
|
653
|
+
local where = "point " .. (i - 1)
|
|
654
|
+
local t = tonumber(pt.time)
|
|
655
|
+
if not t or t < 0 or t > len then return nil, where .. ": time must be 0.." .. len .. " (seconds from the item start)" end
|
|
656
|
+
local value, verr = point_value(api, env, volume, pt, where, true)
|
|
657
|
+
if not value then return nil, verr end
|
|
658
|
+
local sok, serr = check_shape(pt, where)
|
|
659
|
+
if not sok then return nil, serr end
|
|
660
|
+
todo[#todo + 1] = { pos + t, value, int(pt.shape) or 0, tonumber(pt.tension) or 0 }
|
|
661
|
+
end
|
|
662
|
+
if not pool and not p.captureEnvelope then
|
|
663
|
+
local max = -1
|
|
664
|
+
each_automation_item(api, function(e, i)
|
|
665
|
+
max = math.max(max, math.floor(api.GetSetAutomationItemInfo(e, i, "D_POOL_ID", 0, false) + 0.5))
|
|
666
|
+
end)
|
|
667
|
+
pool = max + 1
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
return with_undo(api, "MCP: Insert automation item", function()
|
|
671
|
+
local idx = api.InsertAutomationItem(env, p.captureEnvelope and -1 or pool, pos, len)
|
|
672
|
+
if idx == nil or idx < 0 then return nil, "REAPER refused to insert the automation item" end
|
|
673
|
+
if not existing and api.TimeMap2_timeToQN then
|
|
674
|
+
-- The pool spans the whole item, or points near its end are dropped
|
|
675
|
+
api.GetSetAutomationItemInfo(env, idx, "D_POOL_QNLEN",
|
|
676
|
+
api.TimeMap2_timeToQN(0, pos + len) - api.TimeMap2_timeToQN(0, pos), true)
|
|
677
|
+
end
|
|
678
|
+
if #todo > 0 then
|
|
679
|
+
-- A new pool's item can start with points of its own; replace them with ours
|
|
680
|
+
local pidx = idx | M.POOL_POINTS
|
|
681
|
+
api.DeleteEnvelopePointRangeEx(env, pidx, -math.huge, math.huge)
|
|
682
|
+
for _, t in ipairs(todo) do
|
|
683
|
+
api.InsertEnvelopePointEx(env, pidx, (pool_time(api, env, pidx, t[1], "")), t[2], t[3], t[4], false, true)
|
|
684
|
+
end
|
|
685
|
+
sort(api, env, pidx)
|
|
686
|
+
end
|
|
687
|
+
apply_item_fields(api, env, idx, p)
|
|
688
|
+
local r = target(p, -1)
|
|
689
|
+
r.automationItem = ai_info(api, env, idx)
|
|
690
|
+
r.pooledWith = pool_instances(api, r.automationItem.poolId) - 1
|
|
691
|
+
return r
|
|
692
|
+
end)
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
-- The item itself, not its points: the plain index without the pool-points flag
|
|
696
|
+
local function resolve_item(api, p)
|
|
697
|
+
if p.automationItemIndex == nil then return nil, "automationItemIndex required" end
|
|
698
|
+
local env, ai = M.resolve(api, p)
|
|
699
|
+
if not env then return nil, ai end
|
|
700
|
+
return env, ai & ~M.POOL_POINTS
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
function M.set_item(api, p)
|
|
704
|
+
local env, ai = resolve_item(api, p)
|
|
705
|
+
if not env then return nil, ai end
|
|
706
|
+
local ok, ferr = check_item_fields(p)
|
|
707
|
+
if not ok then return nil, ferr end
|
|
708
|
+
return with_undo(api, "MCP: Edit automation item", function()
|
|
709
|
+
if p.position ~= nil then api.GetSetAutomationItemInfo(env, ai, "D_POSITION", tonumber(p.position), true) end
|
|
710
|
+
if p.length ~= nil then api.GetSetAutomationItemInfo(env, ai, "D_LENGTH", tonumber(p.length), true) end
|
|
711
|
+
apply_item_fields(api, env, ai, p)
|
|
712
|
+
local r = target(p, -1)
|
|
713
|
+
r.automationItem = ai_info(api, env, ai)
|
|
714
|
+
return r
|
|
715
|
+
end)
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
-- Deletes one automation item (its points go with it unless preservePoints).
|
|
719
|
+
-- REAPER has no API call for it: select only this item, run the delete action,
|
|
720
|
+
-- then put every other item's selection back.
|
|
721
|
+
function M.delete_item(api, p)
|
|
722
|
+
local env, ai = resolve_item(api, p)
|
|
723
|
+
if not env then return nil, ai end
|
|
724
|
+
local deleted = ai_info(api, env, ai)
|
|
725
|
+
return with_undo(api, "MCP: Delete automation item", function()
|
|
726
|
+
local sel = {}
|
|
727
|
+
each_automation_item(api, function(e, i)
|
|
728
|
+
local was = api.GetSetAutomationItemInfo(e, i, "D_UISEL", 0, false) ~= 0
|
|
729
|
+
local this = e == env and i == ai
|
|
730
|
+
if not this then sel[#sel + 1] = { e, i, was } end
|
|
731
|
+
api.GetSetAutomationItemInfo(e, i, "D_UISEL", this and 1 or 0, true)
|
|
732
|
+
end)
|
|
733
|
+
local before = api.CountAutomationItems(env)
|
|
734
|
+
api.Main_OnCommand(p.preservePoints and 42088 or M.DELETE_AUTOMATION_ITEMS, 0)
|
|
735
|
+
-- Items after the deleted one on this envelope moved down one index
|
|
736
|
+
for _, s in ipairs(sel) do
|
|
737
|
+
local i = s[2]
|
|
738
|
+
if s[1] == env and i > ai then i = i - 1 end
|
|
739
|
+
api.GetSetAutomationItemInfo(s[1], i, "D_UISEL", s[3] and 1 or 0, true)
|
|
740
|
+
end
|
|
741
|
+
if api.CountAutomationItems(env) ~= before - 1 then return nil, "Internal error: the automation item was not deleted" end
|
|
742
|
+
local r = target(p, -1)
|
|
743
|
+
r.deleted, r.count = deleted, api.CountAutomationItems(env)
|
|
744
|
+
return r
|
|
745
|
+
end)
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
return M
|