@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,1378 @@
|
|
|
1
|
+
-- Edit views for the MCP Bridge status page: what a write changed and what a listing showed, kept on the
|
|
2
|
+
-- command's log entry (`r`). Writes read the state they touch before the handler runs (M.before) and again
|
|
3
|
+
-- after (M.summary); listings summarise their response. Every list is capped so one entry stays a few KB.
|
|
4
|
+
-- Pure Lua over an injected API (M.init), loaded by mcp_bridge.lua and tested through it in wasmoon.
|
|
5
|
+
local M = {}
|
|
6
|
+
|
|
7
|
+
local api, ctx
|
|
8
|
+
|
|
9
|
+
--- ctx: { api, to_db, json_decode, json_decode_array, get_midi_notes, get_midi_take, get_midi_cc, resolve_track }
|
|
10
|
+
function M.init(c)
|
|
11
|
+
ctx = c
|
|
12
|
+
api = c.api
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
local function round2(v)
|
|
16
|
+
if type(v) ~= "number" or v ~= v or v == math.huge or v == -math.huge then return nil end
|
|
17
|
+
return math.floor(v * 100 + 0.5) / 100
|
|
18
|
+
end
|
|
19
|
+
local EDIT_MAX_ROWS = 32
|
|
20
|
+
local EDIT_MAX_POINTS = 200
|
|
21
|
+
local EDIT_MAX_NOTES = 256
|
|
22
|
+
local EDIT_MAX_MARKS = 64
|
|
23
|
+
|
|
24
|
+
local function round3(v)
|
|
25
|
+
if type(v) ~= "number" or v ~= v or v == math.huge or v == -math.huge then return nil end
|
|
26
|
+
return math.floor(v * 1000 + 0.5) / 1000
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
-- Arrays in params arrive as JSON strings
|
|
30
|
+
local function decode_param_list(v)
|
|
31
|
+
if type(v) == "table" then return v end
|
|
32
|
+
if type(v) ~= "string" then return nil end
|
|
33
|
+
for _, decode in ipairs({ ctx.json_decode, ctx.json_decode_array }) do
|
|
34
|
+
local ok, val = pcall(decode, v)
|
|
35
|
+
if ok and type(val) == "table" then return val end
|
|
36
|
+
end
|
|
37
|
+
return nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
local function track_named(idx)
|
|
41
|
+
idx = tonumber(idx)
|
|
42
|
+
if idx == nil then return nil end
|
|
43
|
+
local tr = idx == -1 and api.GetMasterTrack(0) or api.GetTrack(0, idx)
|
|
44
|
+
if not tr then return nil end
|
|
45
|
+
if idx == -1 then return tr, "Master" end
|
|
46
|
+
local _, name = api.GetTrackName(tr)
|
|
47
|
+
return tr, name or ("Track " .. (idx + 1))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
-- Track properties: [info key, scale] (db = volume in dB, pan = −1..1, bool = on/off)
|
|
51
|
+
local TRACK_PROPS = {
|
|
52
|
+
volume = { "D_VOL", "db" }, pan = { "D_PAN", "pan" }, mute = { "B_MUTE", "bool" },
|
|
53
|
+
solo = { "I_SOLO", "bool" }, recordArm = { "I_RECARM", "bool" }, phase = { "B_PHASE", "bool" },
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
local function track_targets(params, cmd_type)
|
|
57
|
+
local out = {}
|
|
58
|
+
if cmd_type == "set_track_property" then
|
|
59
|
+
out[1] = { track = params.trackIndex, prop = params.property }
|
|
60
|
+
else
|
|
61
|
+
for _, t in ipairs(decode_param_list(params.tracks) or {}) do
|
|
62
|
+
for prop in pairs(TRACK_PROPS) do
|
|
63
|
+
if t[prop] ~= nil and #out < EDIT_MAX_ROWS then out[#out + 1] = { track = t.trackIndex, prop = prop } end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
return out
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
local function read_track_values(targets)
|
|
71
|
+
local out = {}
|
|
72
|
+
for i, t in ipairs(targets) do
|
|
73
|
+
local spec = TRACK_PROPS[t.prop]
|
|
74
|
+
local tr, name = track_named(t.track)
|
|
75
|
+
if spec and tr then
|
|
76
|
+
local v = api.GetMediaTrackInfo_Value(tr, spec[1])
|
|
77
|
+
if spec[2] == "db" then v = ctx.to_db(v) elseif spec[2] == "bool" then v = v ~= 0 and 1 or 0 end
|
|
78
|
+
out[i] = { t = name, p = t.prop, v = round2(v) or -150, s = spec[2] }
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
return out
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
local function fx_targets(params, cmd_type)
|
|
85
|
+
if cmd_type == "set_fx_parameter" then return { { track = params.trackIndex, fx = params.fxIndex, param = params.paramIndex } } end
|
|
86
|
+
local out = {}
|
|
87
|
+
for _, u in ipairs(decode_param_list(params.updates) or {}) do
|
|
88
|
+
if #out < EDIT_MAX_ROWS then out[#out + 1] = { track = u.trackIndex, fx = u.fxIndex, param = u.paramIndex } end
|
|
89
|
+
end
|
|
90
|
+
return out
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
local function read_fx_values(targets)
|
|
94
|
+
local out = {}
|
|
95
|
+
for i, t in ipairs(targets) do
|
|
96
|
+
local tr, name = track_named(t.track)
|
|
97
|
+
local fx, p = tonumber(t.fx), tonumber(t.param)
|
|
98
|
+
if tr and fx and p then
|
|
99
|
+
local _, fx_name = api.TrackFX_GetFXName(tr, fx)
|
|
100
|
+
local _, p_name = api.TrackFX_GetParamName(tr, fx, p)
|
|
101
|
+
local _, text = api.TrackFX_GetFormattedParamValue(tr, fx, p)
|
|
102
|
+
out[i] = {
|
|
103
|
+
t = name .. " · " .. (fx_name or ("FX " .. (fx + 1))), p = p_name or ("Param " .. (p + 1)),
|
|
104
|
+
v = round3(api.TrackFX_GetParamNormalized(tr, fx, p)), x = text, s = "norm",
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
return out
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
-- Rows of before → after for the same targets
|
|
112
|
+
local function change_rows(before, after)
|
|
113
|
+
local rows = {}
|
|
114
|
+
for i, b in pairs(after) do
|
|
115
|
+
local a = before and before[i]
|
|
116
|
+
rows[#rows + 1] = { i = i, t = b.t, p = b.p, s = b.s, a = a and a.v, b = b.v, at = a and a.x, bt = b.x }
|
|
117
|
+
end
|
|
118
|
+
table.sort(rows, function(x, y) return x.i < y.i end)
|
|
119
|
+
for _, r in ipairs(rows) do r.i = nil end
|
|
120
|
+
return { rows = rows }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
-- Envelopes: values in the unit the envelope reads in
|
|
124
|
+
local function env_of(params)
|
|
125
|
+
local tr = api.GetTrack(0, tonumber(params.trackIndex) or -2)
|
|
126
|
+
local ei = tonumber(params.envelopeIndex)
|
|
127
|
+
if not tr or not ei then return nil end
|
|
128
|
+
return api.GetTrackEnvelope(tr, ei)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
local function env_unit(env)
|
|
132
|
+
local _, name = api.GetEnvelopeName(env)
|
|
133
|
+
local n = (name or ""):lower()
|
|
134
|
+
if n:find("volume") then return name, "dB" end
|
|
135
|
+
if n:find("pan") or n:find("width") then return name, "pan" end
|
|
136
|
+
if n:find("mute") then return name, "mute" end
|
|
137
|
+
return name, "raw"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
local function env_points(env)
|
|
141
|
+
local _, unit = env_unit(env)
|
|
142
|
+
local mode = api.GetEnvelopeScalingMode and api.GetEnvelopeScalingMode(env) or 0
|
|
143
|
+
local pts = {}
|
|
144
|
+
for i = 0, api.CountEnvelopePoints(env) - 1 do
|
|
145
|
+
local _, time, value, shape = api.GetEnvelopePoint(env, i)
|
|
146
|
+
local v = value
|
|
147
|
+
if unit == "dB" then
|
|
148
|
+
local amp = api.ScaleFromEnvelopeMode and api.ScaleFromEnvelopeMode(mode, value) or value
|
|
149
|
+
v = amp > 0 and 20 * math.log(amp, 10) or -150
|
|
150
|
+
end
|
|
151
|
+
pts[#pts + 1] = { round3(time) or 0, round2(v) or 0, shape or 0 }
|
|
152
|
+
end
|
|
153
|
+
return pts
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
-- Automation items as blocks [position, length, pool id, pool name] plus each pool's points
|
|
157
|
+
-- ([time from the item start, value, shape]) once, so pooled items draw the same curve.
|
|
158
|
+
local EDIT_MAX_ITEMS = 32
|
|
159
|
+
local AUTOMATION_ITEM_COMMANDS = {
|
|
160
|
+
list_automation_items = true, insert_automation_item = true, set_automation_item = true, delete_automation_item = true,
|
|
161
|
+
}
|
|
162
|
+
local POOL_POINTS = 0x10000000
|
|
163
|
+
local function env_items(env)
|
|
164
|
+
if not api.CountAutomationItems then return nil end
|
|
165
|
+
local n = tonumber((api.CountAutomationItems(env))) or 0
|
|
166
|
+
if n == 0 then return nil end
|
|
167
|
+
local _, unit = env_unit(env)
|
|
168
|
+
local mode = api.GetEnvelopeScalingMode and api.GetEnvelopeScalingMode(env) or 0
|
|
169
|
+
local blocks, pools, seen = {}, {}, {}
|
|
170
|
+
for i = 0, math.min(n, EDIT_MAX_ITEMS) - 1 do
|
|
171
|
+
local function info(parm) return api.GetSetAutomationItemInfo(env, i, parm, 0, false) end
|
|
172
|
+
local pos, pool = info("D_POSITION"), math.floor(info("D_POOL_ID") + 0.5)
|
|
173
|
+
local _, name = api.GetSetAutomationItemInfo_String(env, i, "P_POOL_NAME", "", false)
|
|
174
|
+
blocks[#blocks + 1] = { round3(pos) or 0, round3(info("D_LENGTH")) or 0, pool, name or "" }
|
|
175
|
+
if not seen[pool] then
|
|
176
|
+
seen[pool] = true
|
|
177
|
+
local pts = {}
|
|
178
|
+
for k = 0, math.min(api.CountEnvelopePointsEx(env, i | POOL_POINTS), EDIT_MAX_POINTS) - 1 do
|
|
179
|
+
local _, time, value, shape = api.GetEnvelopePointEx(env, i | POOL_POINTS, k)
|
|
180
|
+
local v = value
|
|
181
|
+
if unit == "dB" then
|
|
182
|
+
local amp = api.ScaleFromEnvelopeMode and api.ScaleFromEnvelopeMode(mode, value) or value
|
|
183
|
+
v = amp > 0 and 20 * math.log(amp, 10) or -150
|
|
184
|
+
end
|
|
185
|
+
pts[#pts + 1] = { round3(time - pos) or 0, round2(v) or 0, shape or 0 }
|
|
186
|
+
end
|
|
187
|
+
pools[#pools + 1] = { pool, pts }
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
return { items = blocks, pools = pools, more = math.max(0, n - EDIT_MAX_ITEMS) }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
-- The points inside [from, to] plus one on each side, so the curve reaches the edges
|
|
194
|
+
local function points_in(pts, from, to)
|
|
195
|
+
local first, last
|
|
196
|
+
for i, p in ipairs(pts) do
|
|
197
|
+
if p[1] >= from and p[1] <= to then first = first or i; last = i end
|
|
198
|
+
end
|
|
199
|
+
if not first then
|
|
200
|
+
for i, p in ipairs(pts) do if p[1] < from then first = i end end
|
|
201
|
+
first = first or 1
|
|
202
|
+
last = math.min(#pts, first + 1)
|
|
203
|
+
end
|
|
204
|
+
local out = {}
|
|
205
|
+
for i = math.max(1, first - 1), math.min(#pts, last + 1) do
|
|
206
|
+
if #out < EDIT_MAX_POINTS then out[#out + 1] = pts[i] end
|
|
207
|
+
end
|
|
208
|
+
return out
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
local function envelope_view(cmd_type, params, data, before)
|
|
212
|
+
local env = env_of(params)
|
|
213
|
+
if not env then return nil end
|
|
214
|
+
local name, unit = env_unit(env)
|
|
215
|
+
local after = env_points(env)
|
|
216
|
+
local from, to
|
|
217
|
+
local function span(t) if t then from = math.min(from or t, t); to = math.max(to or t, t) end end
|
|
218
|
+
if cmd_type == "insert_envelope_point" then span(tonumber(params.time))
|
|
219
|
+
elseif cmd_type == "insert_envelope_points" then
|
|
220
|
+
for _, p in ipairs(decode_param_list(params.points) or {}) do span(tonumber(p.time)) end
|
|
221
|
+
elseif cmd_type == "remove_envelope_points" then span(tonumber(params.timeStart)); span(tonumber(params.timeEnd))
|
|
222
|
+
elseif cmd_type == "delete_envelope_point" then
|
|
223
|
+
-- The handler returns the point it deleted (also one inside an automation item)
|
|
224
|
+
local gone = type(data) == "table" and type(data.deleted) == "table" and data.deleted.time
|
|
225
|
+
local p = before and before[(tonumber(params.pointIndex) or -1) + 1]
|
|
226
|
+
span(tonumber(gone) or (p and p[1]))
|
|
227
|
+
elseif type(data) == "table" and type(data.points) == "table" then
|
|
228
|
+
for _, p in ipairs(data.points) do span(p.time) end
|
|
229
|
+
end
|
|
230
|
+
-- Automation items: the item a call touched (before and after a move), or every item for a listing
|
|
231
|
+
local items = env_items(env)
|
|
232
|
+
local was_items = type(before) == "table" and before.items or nil
|
|
233
|
+
local touched = tonumber(params.automationItemIndex)
|
|
234
|
+
if type(data) == "table" and type(data.automationItem) == "table" then touched = data.automationItem.index end
|
|
235
|
+
if cmd_type == "delete_automation_item" then touched = nil end
|
|
236
|
+
local function span_block(b) if b then span(b[1]); span(b[1] + b[2]) end end
|
|
237
|
+
if AUTOMATION_ITEM_COMMANDS[cmd_type] or touched then
|
|
238
|
+
if touched and items then span_block(items.items[touched + 1]) end
|
|
239
|
+
if was_items and params.automationItemIndex ~= nil then span_block(was_items.items[(tonumber(params.automationItemIndex) or -1) + 1]) end
|
|
240
|
+
if not from then
|
|
241
|
+
for _, b in ipairs(items and items.items or {}) do span_block(b) end
|
|
242
|
+
for _, b in ipairs(was_items and was_items.items or {}) do span_block(b) end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
if not from then
|
|
246
|
+
for _, p in ipairs(after) do span(p[1]) end
|
|
247
|
+
for _, p in ipairs(before or {}) do span(p[1]) end
|
|
248
|
+
end
|
|
249
|
+
from, to = from or 0, to or 0
|
|
250
|
+
local pad = math.max(to - from, 4) * 0.25
|
|
251
|
+
from, to = math.max(0, from - pad), to + pad
|
|
252
|
+
local r = { env = name, unit = unit, from = round2(from), to = round2(to), pts = points_in(after, from, to), count = #after }
|
|
253
|
+
if before then r.was = points_in(before, from, to) end
|
|
254
|
+
if items then
|
|
255
|
+
r.ais, r.pools = items.items, items.pools
|
|
256
|
+
if items.more > 0 then r.aisMore = items.more end
|
|
257
|
+
if touched then r.ai = touched end
|
|
258
|
+
end
|
|
259
|
+
if was_items and cmd_type ~= "list_automation_items" then r.wasAis, r.wasPools = was_items.items, was_items.pools end
|
|
260
|
+
return r
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
-- MIDI: notes as [pitch, start, length, velocity, flag] in beats from the item start; flag 1 = new or changed
|
|
264
|
+
local function midi_notes(params)
|
|
265
|
+
local ok, data = pcall(ctx.get_midi_notes, { trackIndex = params.trackIndex, itemIndex = params.itemIndex })
|
|
266
|
+
if not ok or type(data) ~= "table" or type(data.notes) ~= "table" then return nil end
|
|
267
|
+
local notes = {}
|
|
268
|
+
for _, n in ipairs(data.notes) do
|
|
269
|
+
notes[#notes + 1] = { n.pitch, round3(n.startPosition) or 0, round3(n.duration) or 0, n.velocity }
|
|
270
|
+
end
|
|
271
|
+
return notes
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
local function note_key(n) return n[1] .. ":" .. n[2] .. ":" .. n[3] .. ":" .. n[4] end
|
|
275
|
+
|
|
276
|
+
local function midi_view(params, before)
|
|
277
|
+
local after = midi_notes(params)
|
|
278
|
+
if not after then return nil end
|
|
279
|
+
local _, item = ctx.get_midi_take(params)
|
|
280
|
+
local r = { total = #after }
|
|
281
|
+
if item then
|
|
282
|
+
local pos = api.GetMediaItemInfo_Value(item, "D_POSITION")
|
|
283
|
+
local len = api.GetMediaItemInfo_Value(item, "D_LENGTH")
|
|
284
|
+
r.beats = round3(api.TimeMap2_timeToQN(0, pos + len) - api.TimeMap2_timeToQN(0, pos))
|
|
285
|
+
local num, den = api.TimeMap_GetTimeSigAtTime(0, pos)
|
|
286
|
+
r.ts = { num or 4, den or 4 }
|
|
287
|
+
end
|
|
288
|
+
local had = {}
|
|
289
|
+
for _, n in ipairs(before or {}) do had[note_key(n)] = true end
|
|
290
|
+
local has = {}
|
|
291
|
+
r.notes = {}
|
|
292
|
+
for _, n in ipairs(after) do
|
|
293
|
+
has[note_key(n)] = true
|
|
294
|
+
if #r.notes < EDIT_MAX_NOTES then
|
|
295
|
+
r.notes[#r.notes + 1] = { n[1], n[2], n[3], n[4], (before and not had[note_key(n)]) and 1 or 0 }
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
r.more = #after - #r.notes
|
|
299
|
+
if before then
|
|
300
|
+
r.gone = {}
|
|
301
|
+
for _, n in ipairs(before) do
|
|
302
|
+
if not has[note_key(n)] and #r.gone < EDIT_MAX_ROWS then r.gone[#r.gone + 1] = n end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
return r
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
-- MIDI CC as an automation lane: one controller's values (0-127) over the item, in beats, held until the next event.
|
|
309
|
+
-- The controller is the call's ccNumber, the deleted event's, or the item's busiest one.
|
|
310
|
+
local CC_NAMES = { [1] = "Mod wheel", [2] = "Breath", [4] = "Foot", [7] = "Volume", [10] = "Pan", [11] = "Expression", [64] = "Sustain", [74] = "Brightness" }
|
|
311
|
+
local CC_COMMANDS = { get_midi_cc = true, insert_midi_cc = true, delete_midi_cc = true }
|
|
312
|
+
|
|
313
|
+
local function cc_events(params)
|
|
314
|
+
if not ctx.get_midi_cc then return nil end
|
|
315
|
+
local ok, data = pcall(ctx.get_midi_cc, { trackIndex = params.trackIndex, itemIndex = params.itemIndex })
|
|
316
|
+
if not ok or type(data) ~= "table" or type(data.events) ~= "table" then return nil end
|
|
317
|
+
return data.events
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
local function cc_view(cmd_type, params, before)
|
|
321
|
+
local events = cc_events(params) or {}
|
|
322
|
+
local cc = tonumber(params.ccNumber)
|
|
323
|
+
if not cc and cmd_type == "delete_midi_cc" then
|
|
324
|
+
for _, e in ipairs(before or {}) do if e.ccIndex == tonumber(params.ccIndex) then cc = e.ccNumber end end
|
|
325
|
+
end
|
|
326
|
+
if not cc then
|
|
327
|
+
local counts, best = {}, 0
|
|
328
|
+
for _, e in ipairs(events) do
|
|
329
|
+
counts[e.ccNumber] = (counts[e.ccNumber] or 0) + 1
|
|
330
|
+
if counts[e.ccNumber] > best then best, cc = counts[e.ccNumber], e.ccNumber end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
if not cc then return nil end
|
|
334
|
+
local function lane(list)
|
|
335
|
+
local pts = {}
|
|
336
|
+
for _, e in ipairs(list or {}) do
|
|
337
|
+
if e.ccNumber == cc and #pts < EDIT_MAX_POINTS then pts[#pts + 1] = { round3(e.position) or 0, e.value, 1 } end
|
|
338
|
+
end
|
|
339
|
+
table.sort(pts, function(a, b) return a[1] < b[1] end)
|
|
340
|
+
return pts
|
|
341
|
+
end
|
|
342
|
+
local pts, was = lane(events), before and lane(before) or nil
|
|
343
|
+
local from, to = 0, 4
|
|
344
|
+
for _, p in ipairs(pts) do to = math.max(to, p[1]) end
|
|
345
|
+
for _, p in ipairs(was or {}) do to = math.max(to, p[1]) end
|
|
346
|
+
local name = "CC " .. cc .. (CC_NAMES[cc] and (" · " .. CC_NAMES[cc]) or "")
|
|
347
|
+
local others = {}
|
|
348
|
+
for _, e in ipairs(events) do if e.ccNumber ~= cc then others[e.ccNumber] = true end end
|
|
349
|
+
local more = 0
|
|
350
|
+
for _ in pairs(others) do more = more + 1 end
|
|
351
|
+
local r = { env = name, unit = "cc", beats = 1, from = 0, to = round2(math.ceil(to + 1)), pts = pts, count = #pts }
|
|
352
|
+
if more > 0 then r.otherCcs = more end
|
|
353
|
+
if before then r.was = was end
|
|
354
|
+
return r
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
local function midi_stats_view(data)
|
|
358
|
+
if type(data) ~= "table" or type(data.pitchStats) ~= "table" then return nil end
|
|
359
|
+
local gun = {}
|
|
360
|
+
for _, g in ipairs(data.machineGunWarnings or {}) do gun[g.pitch] = g.maxConsecutive end
|
|
361
|
+
local pitches = {}
|
|
362
|
+
for _, p in ipairs(data.pitchStats) do
|
|
363
|
+
if #pitches < EDIT_MAX_ROWS then
|
|
364
|
+
pitches[#pitches + 1] = { p.pitch, p.count, p.minVelocity, p.maxVelocity, p.avgVelocity, gun[p.pitch] or 0 }
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
return { total = data.totalNotes, beats = data.durationBeats, pitches = pitches, hist = data.velocityHistogram }
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
-- Timeline: regions [start, end, name, color, index], markers [pos, name, color, index], tempo [pos, bpm, num, den, linear]
|
|
371
|
+
local function hex_color(native)
|
|
372
|
+
native = tonumber(native) or 0
|
|
373
|
+
if native == 0 or not api.ColorFromNative then return "" end
|
|
374
|
+
local r, g, b = api.ColorFromNative(native & 0xFFFFFF)
|
|
375
|
+
return string.format("#%02x%02x%02x", r or 0, g or 0, b or 0)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
-- A track from an index, "master", a name or "{GUID}" (the same resolution the structure tools use)
|
|
379
|
+
local function track_ref(ref)
|
|
380
|
+
if ref == nil then return nil end
|
|
381
|
+
if type(ref) == "number" or (type(ref) == "string" and ref:match("^%-?%d+$")) then
|
|
382
|
+
local tr = track_named(ref)
|
|
383
|
+
if tr then return tr end
|
|
384
|
+
end
|
|
385
|
+
return ctx.resolve_track and ctx.resolve_track(ref) or nil
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
local function name_of(tr)
|
|
389
|
+
if not tr then return nil end
|
|
390
|
+
if api.GetMasterTrack and tr == api.GetMasterTrack(0) then return "Master" end
|
|
391
|
+
local _, name = api.GetTrackName(tr)
|
|
392
|
+
return name or "Track"
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
-- A stable short id for diffing before and after (GUIDs survive moves and renames)
|
|
396
|
+
local function short_id(guid)
|
|
397
|
+
return type(guid) == "string" and guid:gsub("[{}]", ""):sub(1, 8) or ""
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
-- FX chain: [name, flags (B bypassed, O offline), id, preset]
|
|
401
|
+
local function fx_chain(tr)
|
|
402
|
+
local out = {}
|
|
403
|
+
for i = 0, math.min(api.TrackFX_GetCount(tr) or 0, EDIT_MAX_ROWS) - 1 do
|
|
404
|
+
local _, name = api.TrackFX_GetFXName(tr, i)
|
|
405
|
+
local enabled = not api.TrackFX_GetEnabled or api.TrackFX_GetEnabled(tr, i) ~= false
|
|
406
|
+
local offline = api.TrackFX_GetOffline and api.TrackFX_GetOffline(tr, i) == true
|
|
407
|
+
local preset = ""
|
|
408
|
+
if api.TrackFX_GetPreset then
|
|
409
|
+
local _, p = api.TrackFX_GetPreset(tr, i)
|
|
410
|
+
preset = type(p) == "string" and p or ""
|
|
411
|
+
end
|
|
412
|
+
out[#out + 1] = { name or ("FX " .. (i + 1)), (enabled and "" or "B") .. (offline and "O" or ""), short_id(api.TrackFX_GetFXGUID(tr, i)), preset }
|
|
413
|
+
end
|
|
414
|
+
return out
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
local FX_CHAIN_COMMANDS = {
|
|
418
|
+
add_fx = true, remove_fx = true, move_fx = true, copy_fx = true, set_fx_enabled = true, set_fx_offline = true,
|
|
419
|
+
setup_fx_chain = true, load_fx_chain = true, rename_fx = true, set_fx_preset = true, get_track_properties = true,
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
-- The tracks an FX command touches: its track, and the other track of a move/copy
|
|
423
|
+
local function fx_tracks(params)
|
|
424
|
+
local out = {}
|
|
425
|
+
for _, ref in ipairs({ params.trackIndex, params.toTrackIndex }) do
|
|
426
|
+
local tr = track_ref(ref)
|
|
427
|
+
if tr and #out < 2 and (out[1] == nil or out[1] ~= tr) then out[#out + 1] = tr end
|
|
428
|
+
end
|
|
429
|
+
return out
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
local function fx_chains(params)
|
|
433
|
+
local out = {}
|
|
434
|
+
for i, tr in ipairs(fx_tracks(params)) do out[i] = fx_chain(tr) end
|
|
435
|
+
return out
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
local function fx_chain_view(params, before)
|
|
439
|
+
local chains = {}
|
|
440
|
+
for i, tr in ipairs(fx_tracks(params)) do
|
|
441
|
+
chains[i] = { t = name_of(tr), fx = fx_chain(tr), was = before and before[i] or nil }
|
|
442
|
+
end
|
|
443
|
+
return #chains > 0 and { chains = chains } or nil
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
-- Media items of a track, in the timeline's item shape; `gain` adds the item's gain in dB
|
|
447
|
+
local function track_items(tr, gain)
|
|
448
|
+
local out = {}
|
|
449
|
+
for i = 0, math.min(api.CountTrackMediaItems(tr) or 0, EDIT_MAX_MARKS) - 1 do
|
|
450
|
+
local it = api.GetTrackMediaItem(tr, i)
|
|
451
|
+
local take = api.GetActiveTake and api.GetActiveTake(it)
|
|
452
|
+
out[#out + 1] = {
|
|
453
|
+
round2(api.GetMediaItemInfo_Value(it, "D_POSITION")) or 0, round2(api.GetMediaItemInfo_Value(it, "D_LENGTH")) or 0,
|
|
454
|
+
round2(api.GetMediaItemInfo_Value(it, "D_FADEINLEN")) or 0, round2(api.GetMediaItemInfo_Value(it, "D_FADEOUTLEN")) or 0,
|
|
455
|
+
(take and api.TakeIsMIDI and api.TakeIsMIDI(take)) and 1 or 0, api.GetMediaItemInfo_Value(it, "B_MUTE") ~= 0 and 1 or 0,
|
|
456
|
+
}
|
|
457
|
+
if gain then
|
|
458
|
+
local v = api.GetMediaItemInfo_Value(it, "D_VOL") or 1
|
|
459
|
+
out[#out][7] = v > 0 and round2(20 * math.log(v, 10)) or -150
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
return out
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
local ITEM_COMMANDS = {
|
|
466
|
+
move_media_item = true, split_media_item = true, trim_media_item = true, delete_media_item = true,
|
|
467
|
+
set_media_item_properties = true, set_media_items_properties = true, set_item_fades = true, insert_audio_file = true,
|
|
468
|
+
list_midi_items = true,
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
-- The first four tracks the items are on (one lane each), and how many tracks that is in all
|
|
472
|
+
local function item_tracks(params)
|
|
473
|
+
local out, seen, n = {}, {}, 0
|
|
474
|
+
local function add(idx)
|
|
475
|
+
idx = tonumber(idx)
|
|
476
|
+
if idx and not seen[idx] and api.GetTrack(0, idx) then
|
|
477
|
+
seen[idx] = true
|
|
478
|
+
n = n + 1
|
|
479
|
+
if #out < 4 then out[#out + 1] = idx end
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
add(params.trackIndex)
|
|
483
|
+
add(params.newTrackIndex)
|
|
484
|
+
for _, it in ipairs(decode_param_list(params.items) or {}) do if type(it) == "table" then add(it.trackIndex) end end
|
|
485
|
+
return out, n
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
local function item_lanes(params)
|
|
489
|
+
local out = {}
|
|
490
|
+
for i, idx in ipairs(item_tracks(params)) do out[i] = track_items(api.GetTrack(0, idx)) end
|
|
491
|
+
return out
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
-- Time selection [start, end] (nil when empty) and edit cursor
|
|
495
|
+
local function time_state()
|
|
496
|
+
local s, e = api.GetSet_LoopTimeRange(false, false, 0, 0, false)
|
|
497
|
+
local sel = (type(s) == "number" and type(e) == "number" and e > s) and { round2(s), round2(e) } or nil
|
|
498
|
+
return { sel = sel, cur = round2(api.GetCursorPosition and api.GetCursorPosition() or 0) or 0 }
|
|
499
|
+
end
|
|
500
|
+
local TIME_COMMANDS = { set_time_selection = true, get_time_selection = true, set_cursor_position = true }
|
|
501
|
+
|
|
502
|
+
-- Routing: [kind (s send, r receive, h hardware), other end, volume dB, pan, flags (M muted)]
|
|
503
|
+
local function routes_of(tr)
|
|
504
|
+
local out = {}
|
|
505
|
+
for _, cat in ipairs({ { 0, "s" }, { -1, "r" }, { 1, "h" } }) do
|
|
506
|
+
for i = 0, (api.GetTrackNumSends(tr, cat[1]) or 0) - 1 do
|
|
507
|
+
if #out < EDIT_MAX_ROWS then
|
|
508
|
+
local other
|
|
509
|
+
if cat[2] == "h" then
|
|
510
|
+
local dst = math.floor(api.GetTrackSendInfo_Value(tr, 1, i, "I_DSTCHAN") or 0)
|
|
511
|
+
local ch = (dst & 0x3FF) + 1
|
|
512
|
+
other = (dst & 1024) ~= 0 and ("Out " .. ch) or ("Out " .. ch .. "/" .. (ch + 1))
|
|
513
|
+
else
|
|
514
|
+
other = name_of(api.GetTrackSendInfo_Value(tr, cat[1], i, cat[2] == "s" and "P_DESTTRACK" or "P_SRCTRACK")) or "?"
|
|
515
|
+
end
|
|
516
|
+
out[#out + 1] = {
|
|
517
|
+
cat[2], other, round2(ctx.to_db(api.GetTrackSendInfo_Value(tr, cat[1], i, "D_VOL"))) or -150,
|
|
518
|
+
round2(api.GetTrackSendInfo_Value(tr, cat[1], i, "D_PAN")) or 0,
|
|
519
|
+
api.GetTrackSendInfo_Value(tr, cat[1], i, "B_MUTE") ~= 0 and "M" or "",
|
|
520
|
+
}
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
return out
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
local ROUTING_COMMANDS = { get_track_routing = true, add_send = true, set_send = true, remove_send = true, add_hardware_output = true }
|
|
528
|
+
local function routing_track(params)
|
|
529
|
+
return track_ref(params.trackIndex) or track_ref(params.track) or track_ref(params.from)
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
-- Structure edits redraw the mixer with the ids before and after, so the page can mark added, removed and moved tracks
|
|
533
|
+
local STRUCTURE_COMMANDS = { add_track = true, delete_track = true, move_track = true, set_track_folder = true, set_track_info = true }
|
|
534
|
+
|
|
535
|
+
-- get_fx_parameters: the first parameters of the FX as bars with the plugin's readout
|
|
536
|
+
local function fx_param_rows(params)
|
|
537
|
+
local tr = track_ref(params.trackIndex)
|
|
538
|
+
local fx = tonumber(params.fxIndex)
|
|
539
|
+
if not tr or not fx or not api.TrackFX_GetNumParams then return nil end
|
|
540
|
+
local n = api.TrackFX_GetNumParams(tr, fx) or 0
|
|
541
|
+
local _, fx_name = api.TrackFX_GetFXName(tr, fx)
|
|
542
|
+
local rows = {}
|
|
543
|
+
for p = 0, math.min(n, EDIT_MAX_ROWS) - 1 do
|
|
544
|
+
local _, p_name = api.TrackFX_GetParamName(tr, fx, p)
|
|
545
|
+
local _, text = api.TrackFX_GetFormattedParamValue(tr, fx, p)
|
|
546
|
+
rows[#rows + 1] = { t = fx_name or ("FX " .. (fx + 1)), p = p_name or ("Param " .. (p + 1)), s = "norm", b = round3(api.TrackFX_GetParamNormalized(tr, fx, p)), bt = text }
|
|
547
|
+
end
|
|
548
|
+
return { rows = rows, more = math.max(0, n - #rows) }
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
local function timeline_snapshot()
|
|
552
|
+
local regions, markers, tempo = {}, {}, {}
|
|
553
|
+
local _, nm, nr = api.CountProjectMarkers(0)
|
|
554
|
+
for i = 0, (nm or 0) + (nr or 0) - 1 do
|
|
555
|
+
local _, isrgn, pos, rend, name, idx, color = api.EnumProjectMarkers3(0, i)
|
|
556
|
+
if isrgn and #regions < EDIT_MAX_MARKS then
|
|
557
|
+
regions[#regions + 1] = { round2(pos) or 0, round2(rend) or 0, name or "", hex_color(color), idx }
|
|
558
|
+
elseif not isrgn and #markers < EDIT_MAX_MARKS then
|
|
559
|
+
markers[#markers + 1] = { round2(pos) or 0, name or "", hex_color(color), idx }
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
for i = 0, (api.CountTempoTimeSigMarkers(0) or 0) - 1 do
|
|
563
|
+
local _, pos, _, _, bpm, num, den, lin = api.GetTempoTimeSigMarker(0, i)
|
|
564
|
+
if #tempo < EDIT_MAX_MARKS then tempo[#tempo + 1] = { round2(pos) or 0, round2(bpm) or 0, num or -1, den or -1, lin and 1 or 0 } end
|
|
565
|
+
end
|
|
566
|
+
if #tempo == 0 then
|
|
567
|
+
local num, den = api.TimeMap_GetTimeSigAtTime(0, 0)
|
|
568
|
+
tempo[1] = { 0, round2(api.Master_GetTempo()) or 120, num or 4, den or 4, 0 }
|
|
569
|
+
end
|
|
570
|
+
return { regions = regions, markers = markers, tempo = tempo }
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
local function project_length(t, items)
|
|
574
|
+
local len = api.GetProjectLength and api.GetProjectLength(0) or 0
|
|
575
|
+
for _, r in ipairs(t.regions) do len = math.max(len, r[2]) end
|
|
576
|
+
for _, m in ipairs(t.markers) do len = math.max(len, m[1]) end
|
|
577
|
+
for _, m in ipairs(t.tempo) do len = math.max(len, m[1]) end
|
|
578
|
+
for _, it in ipairs(items or {}) do len = math.max(len, it[1] + it[2]) end
|
|
579
|
+
return round2(len) or 0
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
local function timeline_view(cmd_type, data, before)
|
|
583
|
+
local r = timeline_snapshot()
|
|
584
|
+
if cmd_type == "list_media_items" and type(data) == "table" and type(data.items) == "table" then
|
|
585
|
+
r.items = {}
|
|
586
|
+
for _, it in ipairs(data.items) do
|
|
587
|
+
if #r.items < EDIT_MAX_MARKS then
|
|
588
|
+
r.items[#r.items + 1] = {
|
|
589
|
+
round2(it.position) or 0, round2(it.length) or 0, round2(it.fadeInLength) or 0, round2(it.fadeOutLength) or 0,
|
|
590
|
+
it.isMidi and 1 or 0, it.muted and 1 or 0,
|
|
591
|
+
}
|
|
592
|
+
end
|
|
593
|
+
end
|
|
594
|
+
end
|
|
595
|
+
r.len = project_length(r, r.items)
|
|
596
|
+
if before then
|
|
597
|
+
r.was = before
|
|
598
|
+
r.len = math.max(r.len, project_length(before))
|
|
599
|
+
end
|
|
600
|
+
return r
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
-- Item edits: the timeline with one lane per touched track, each with its items before the edit
|
|
604
|
+
local function item_view(params, before)
|
|
605
|
+
local r = timeline_snapshot()
|
|
606
|
+
r.lanes = {}
|
|
607
|
+
local all = {}
|
|
608
|
+
for i, idx in ipairs(item_tracks(params)) do
|
|
609
|
+
local tr = api.GetTrack(0, idx)
|
|
610
|
+
local lane = { t = name_of(tr), items = track_items(tr), was = before and before[i] or nil }
|
|
611
|
+
r.lanes[i] = lane
|
|
612
|
+
for _, it in ipairs(lane.items) do all[#all + 1] = it end
|
|
613
|
+
for _, it in ipairs(lane.was or {}) do all[#all + 1] = it end
|
|
614
|
+
end
|
|
615
|
+
r.len = project_length(r, all)
|
|
616
|
+
return r
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
-- Arrangement edits: the whole timeline before and after, a lane for up to four tracks the
|
|
620
|
+
-- edit touches, and the span it worked on (copied from, opened, cut) and where it copied to
|
|
621
|
+
local ARRANGE = {}
|
|
622
|
+
ARRANGE.OPS = { copy_media_items = "copy", insert_time = "insert", remove_time = "remove", copy_section = "section" }
|
|
623
|
+
|
|
624
|
+
-- The section a copy_section call copies, from a region index or start/end
|
|
625
|
+
function ARRANGE.section(params)
|
|
626
|
+
local idx = tonumber(params.regionIndex)
|
|
627
|
+
if idx == nil then return tonumber(params.start), tonumber(params["end"]) end
|
|
628
|
+
local _, nm, nr = api.CountProjectMarkers(0)
|
|
629
|
+
for i = 0, (nm or 0) + (nr or 0) - 1 do
|
|
630
|
+
local _, isrgn, pos, rend, _, num = api.EnumProjectMarkers3(0, i)
|
|
631
|
+
if isrgn and num == idx then return pos, rend end
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
-- Track indexes for the lanes (at most four) and how many tracks the edit touches
|
|
636
|
+
function ARRANGE.tracks(cmd_type, params)
|
|
637
|
+
if cmd_type == "copy_media_items" or cmd_type == "process_items" then
|
|
638
|
+
return item_tracks(params)
|
|
639
|
+
end
|
|
640
|
+
local only = cmd_type == "copy_section" and decode_param_list(params.tracks)
|
|
641
|
+
local s, e
|
|
642
|
+
if cmd_type == "insert_time" then s, e = tonumber(params.position), math.huge
|
|
643
|
+
elseif cmd_type == "remove_time" then s, e = tonumber(params.start), math.huge
|
|
644
|
+
else s, e = ARRANGE.section(params) end
|
|
645
|
+
if not s or not e then return {}, 0 end
|
|
646
|
+
local out, n = {}, 0
|
|
647
|
+
local function consider(t)
|
|
648
|
+
local tr = api.GetTrack(0, t)
|
|
649
|
+
if not tr then return end
|
|
650
|
+
for i = 0, (api.CountTrackMediaItems(tr) or 0) - 1 do
|
|
651
|
+
local it = api.GetTrackMediaItem(tr, i)
|
|
652
|
+
local pos = api.GetMediaItemInfo_Value(it, "D_POSITION")
|
|
653
|
+
if pos < e and pos + api.GetMediaItemInfo_Value(it, "D_LENGTH") > s then
|
|
654
|
+
n = n + 1
|
|
655
|
+
if #out < 4 then out[#out + 1] = t end
|
|
656
|
+
return
|
|
657
|
+
end
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
if type(only) == "table" then
|
|
661
|
+
for _, t in ipairs(only) do if tonumber(t) then consider(tonumber(t)) end end
|
|
662
|
+
else
|
|
663
|
+
for t = 0, (api.CountTracks(0) or 0) - 1 do consider(t) end
|
|
664
|
+
end
|
|
665
|
+
return out, n
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
function ARRANGE.before(cmd_type, params)
|
|
669
|
+
local idx, n = ARRANGE.tracks(cmd_type, params)
|
|
670
|
+
local gain = cmd_type == "process_items" and params.operation == "normalize"
|
|
671
|
+
local b = { idx = idx, n = n, tl = timeline_snapshot(), lanes = {} }
|
|
672
|
+
for i, t in ipairs(idx) do b.lanes[i] = track_items(api.GetTrack(0, t), gain) end
|
|
673
|
+
if cmd_type == "copy_media_items" then
|
|
674
|
+
-- The sources' span, read before the copies land
|
|
675
|
+
local first, last
|
|
676
|
+
for _, ref in ipairs(decode_param_list(params.items) or {}) do
|
|
677
|
+
local tr = type(ref) == "table" and api.GetTrack(0, tonumber(ref.trackIndex) or -2)
|
|
678
|
+
local it = tr and api.GetTrackMediaItem(tr, tonumber(ref.itemIndex) or -1)
|
|
679
|
+
if it then
|
|
680
|
+
local pos = api.GetMediaItemInfo_Value(it, "D_POSITION")
|
|
681
|
+
first = math.min(first or pos, pos)
|
|
682
|
+
last = math.max(last or pos, pos + api.GetMediaItemInfo_Value(it, "D_LENGTH"))
|
|
683
|
+
end
|
|
684
|
+
end
|
|
685
|
+
if first then b.span = { round2(first), round2(last) } end
|
|
686
|
+
end
|
|
687
|
+
return b
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
function ARRANGE.view(cmd_type, params, data, before)
|
|
691
|
+
if type(before) ~= "table" or type(data) ~= "table" then return nil end
|
|
692
|
+
local r = timeline_snapshot()
|
|
693
|
+
r.was = before.tl
|
|
694
|
+
r.op = ARRANGE.OPS[cmd_type] or params.operation
|
|
695
|
+
local gain = cmd_type == "process_items" and params.operation == "normalize"
|
|
696
|
+
r.lanes = {}
|
|
697
|
+
local all = {}
|
|
698
|
+
for i, t in ipairs(before.idx) do
|
|
699
|
+
local tr = api.GetTrack(0, t)
|
|
700
|
+
if tr then
|
|
701
|
+
local lane = { t = name_of(tr), items = track_items(tr, gain), was = before.lanes[i] }
|
|
702
|
+
r.lanes[#r.lanes + 1] = lane
|
|
703
|
+
for _, it in ipairs(lane.items) do all[#all + 1] = it end
|
|
704
|
+
for _, it in ipairs(lane.was) do all[#all + 1] = it end
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
if before.n > #r.lanes then r.moreTracks = before.n - #r.lanes end
|
|
708
|
+
local num = function(v) return tonumber(v) end
|
|
709
|
+
if cmd_type == "insert_time" and num(data.position) and num(data.length) then
|
|
710
|
+
r.span = { data.position, round2(data.position + data.length) }
|
|
711
|
+
elseif cmd_type == "remove_time" and num(data.start) then
|
|
712
|
+
r.span = { data.start, data["end"] }
|
|
713
|
+
elseif cmd_type == "copy_section" and num(data.start) then
|
|
714
|
+
r.span = { data.start, data["end"] }
|
|
715
|
+
-- An insert at or before the section moved it along
|
|
716
|
+
if data.inserted and data.position <= data.start then r.span = { round2(data.start + data.length), round2(data["end"] + data.length) } end
|
|
717
|
+
r.to = { data.position, round2(data.position + data.length) }
|
|
718
|
+
elseif cmd_type == "copy_media_items" then
|
|
719
|
+
r.span = before.span
|
|
720
|
+
local first, last
|
|
721
|
+
for _, c in ipairs(type(data.copies) == "table" and data.copies or {}) do
|
|
722
|
+
first = math.min(first or c.position, c.position)
|
|
723
|
+
last = math.max(last or c.position, c.position + c.length)
|
|
724
|
+
end
|
|
725
|
+
if first then r.to = { round2(first), round2(last) } end
|
|
726
|
+
end
|
|
727
|
+
r.len = math.max(project_length(r, all), project_length(before.tl))
|
|
728
|
+
for _, v in ipairs({ r.span and r.span[2], r.to and r.to[2] }) do r.len = math.max(r.len, v or 0) end
|
|
729
|
+
r.len = round2(r.len) or 0
|
|
730
|
+
return r
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
-- Time selection and cursor on the timeline, with where they were before a write
|
|
734
|
+
local function time_view(before)
|
|
735
|
+
local r = timeline_snapshot()
|
|
736
|
+
local now = time_state()
|
|
737
|
+
r.sel, r.cur = now.sel, now.cur
|
|
738
|
+
if before then r.wasSel, r.wasCur = before.sel, before.cur end
|
|
739
|
+
local len = project_length(r)
|
|
740
|
+
for _, v in ipairs({ r.cur, r.wasCur, r.sel and r.sel[2], r.wasSel and r.wasSel[2] }) do len = math.max(len, v or 0) end
|
|
741
|
+
r.len = round2(len) or 0
|
|
742
|
+
return r
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
-- Mixer: [name, indent, color, volume dB, pan, flags]; flags M mute, S solo, R armed, F folder
|
|
746
|
+
local function mixer_row(tr, name, indent)
|
|
747
|
+
local flags = (api.GetMediaTrackInfo_Value(tr, "B_MUTE") ~= 0 and "M" or "")
|
|
748
|
+
.. (api.GetMediaTrackInfo_Value(tr, "I_SOLO") ~= 0 and "S" or "")
|
|
749
|
+
.. (api.GetMediaTrackInfo_Value(tr, "I_RECARM") ~= 0 and "R" or "")
|
|
750
|
+
.. (api.GetMediaTrackInfo_Value(tr, "I_FOLDERDEPTH") == 1 and "F" or "")
|
|
751
|
+
local color = api.GetMediaTrackInfo_Value(tr, "I_CUSTOMCOLOR")
|
|
752
|
+
return {
|
|
753
|
+
name, indent, (color and color & 0x1000000 ~= 0) and hex_color(color) or "",
|
|
754
|
+
round2(ctx.to_db(api.GetMediaTrackInfo_Value(tr, "D_VOL"))) or -150, round2(api.GetMediaTrackInfo_Value(tr, "D_PAN")) or 0, flags,
|
|
755
|
+
}
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
-- Every track's row and id, with folder indent (structure edits diff the whole project)
|
|
759
|
+
local function mixer_all()
|
|
760
|
+
local rows, ids, level = {}, {}, 0
|
|
761
|
+
for i = 0, api.CountTracks(0) - 1 do
|
|
762
|
+
local tr = api.GetTrack(0, i)
|
|
763
|
+
local _, name = api.GetTrackName(tr)
|
|
764
|
+
rows[#rows + 1] = mixer_row(tr, name or ("Track " .. (i + 1)), level)
|
|
765
|
+
ids[#ids + 1] = short_id(api.GetTrackGUID(tr))
|
|
766
|
+
level = math.max(0, level + (api.GetMediaTrackInfo_Value(tr, "I_FOLDERDEPTH") or 0))
|
|
767
|
+
end
|
|
768
|
+
return rows, ids
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
local function same_row(a, b)
|
|
772
|
+
if not a or not b or #a ~= #b then return false end
|
|
773
|
+
for k = 1, #a do if a[k] ~= b[k] then return false end end
|
|
774
|
+
return true
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
local function slice(list, from, count)
|
|
778
|
+
local out = {}
|
|
779
|
+
for k = from + 1, math.min(#list, from + count) do out[#out + 1] = list[k] end
|
|
780
|
+
return out
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
local function mixer_view(before)
|
|
784
|
+
local rows, ids = mixer_all()
|
|
785
|
+
local r = { total = #rows }
|
|
786
|
+
local master = api.GetMasterTrack(0)
|
|
787
|
+
if master then r.master = mixer_row(master, "Master", 0) end
|
|
788
|
+
local from = 0
|
|
789
|
+
if before then
|
|
790
|
+
-- A big project only fits a window of rows: start it just above the first row that differs
|
|
791
|
+
local first = 1
|
|
792
|
+
while first <= math.max(#ids, #before.ids)
|
|
793
|
+
and ids[first] == before.ids[first] and same_row(rows[first], before.tracks[first]) do
|
|
794
|
+
first = first + 1
|
|
795
|
+
end
|
|
796
|
+
if math.max(#rows, #before.tracks) > EDIT_MAX_MARKS then from = math.max(0, first - 1 - 4) end
|
|
797
|
+
r.ids = slice(ids, from, EDIT_MAX_MARKS)
|
|
798
|
+
r.was = { tracks = slice(before.tracks, from, EDIT_MAX_MARKS), ids = slice(before.ids, from, EDIT_MAX_MARKS) }
|
|
799
|
+
if from > 0 then r.from = from end
|
|
800
|
+
end
|
|
801
|
+
r.tracks = slice(rows, from, EDIT_MAX_MARKS)
|
|
802
|
+
return r
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
-- run_plan: the ops in order, as [op, what]
|
|
806
|
+
local function plan_view(params, data)
|
|
807
|
+
local ops = decode_param_list(params.ops)
|
|
808
|
+
if type(ops) ~= "table" then return nil end
|
|
809
|
+
local rows = {}
|
|
810
|
+
for _, op in ipairs(ops) do
|
|
811
|
+
if #rows < EDIT_MAX_ROWS and type(op) == "table" then
|
|
812
|
+
local target = op.track or op.to or op.target or op.name or ""
|
|
813
|
+
local what = tostring(target)
|
|
814
|
+
if op.fxName or op.fx then what = what .. " · " .. tostring(op.fxName or op.fx) end
|
|
815
|
+
if op.from then what = tostring(op.from) .. " → " .. what end
|
|
816
|
+
if op.action then what = tostring(op.action) end
|
|
817
|
+
rows[#rows + 1] = { tostring(op.op or "?"), what }
|
|
818
|
+
end
|
|
819
|
+
end
|
|
820
|
+
return { ops = rows, total = #ops, dryRun = params.dryRun == true or nil, applied = type(data) == "table" and data.applied or nil }
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
local TIMELINE_WRITES = {
|
|
824
|
+
add_marker = true, add_region = true, edit_marker = true, edit_region = true, delete_marker = true, delete_region = true,
|
|
825
|
+
add_tempo_marker = true, edit_tempo_marker = true, delete_tempo_marker = true, set_project_tempo = true,
|
|
826
|
+
}
|
|
827
|
+
local ENVELOPE_WRITES = {
|
|
828
|
+
insert_envelope_point = true, insert_envelope_points = true, delete_envelope_point = true,
|
|
829
|
+
remove_envelope_points = true, clear_envelope = true, edit_envelope_points = true,
|
|
830
|
+
}
|
|
831
|
+
local MIDI_WRITES = {
|
|
832
|
+
insert_midi_note = true, insert_midi_notes = true, edit_midi_note = true, edit_midi_notes = true, delete_midi_note = true,
|
|
833
|
+
quantize_midi = true, transform_midi = true,
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
-- Measurements: numbers rounded to 0.01 so the feed stays small
|
|
837
|
+
local SPECTRUM_BANDS = 48
|
|
838
|
+
local RESULT_FIELDS = {
|
|
839
|
+
read_track_meters = { "peakL", "peakR", "rmsL", "rmsR" },
|
|
840
|
+
read_track_lufs = { "integrated", "shortTerm", "momentary", "truePeakL", "truePeakR", "duration" },
|
|
841
|
+
read_track_correlation = { "correlation", "stereoWidth", "midLevel", "sideLevel" },
|
|
842
|
+
read_track_crest = { "crestFactor", "peakLevel", "rmsLevel" },
|
|
843
|
+
read_track_spectrum = { "sampleRate", "fftSize", "peakDb", "rmsDb" },
|
|
844
|
+
render_track_to_wav = { "durationSeconds", "sampleRate" },
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
-- Loudest bin per log-spaced band from 20 Hz to 20 kHz; a band narrower than one bin takes the bin it falls in
|
|
848
|
+
local function spectrum_bands(data)
|
|
849
|
+
local bins, res = data.bins, data.frequencyResolution
|
|
850
|
+
if type(bins) ~= "table" or #bins == 0 or type(res) ~= "number" or res <= 0 then return nil end
|
|
851
|
+
local bands = {}
|
|
852
|
+
for b = 0, SPECTRUM_BANDS - 1 do
|
|
853
|
+
local f0 = 20 * 1000 ^ (b / SPECTRUM_BANDS)
|
|
854
|
+
local f1 = 20 * 1000 ^ ((b + 1) / SPECTRUM_BANDS)
|
|
855
|
+
local i0 = math.max(1, math.floor(f0 / res) + 1)
|
|
856
|
+
local i1 = math.min(#bins, math.max(i0, math.ceil(f1 / res)))
|
|
857
|
+
local top
|
|
858
|
+
for i = i0, i1 do
|
|
859
|
+
local v = bins[i]
|
|
860
|
+
if type(v) == "number" and (top == nil or v > top) then top = v end
|
|
861
|
+
end
|
|
862
|
+
bands[#bands + 1] = round2(top) or -150
|
|
863
|
+
end
|
|
864
|
+
return bands
|
|
865
|
+
end
|
|
866
|
+
|
|
867
|
+
-- Min/max per slice of a rendered WAV (all channels), read while the file still exists: the server deletes it
|
|
868
|
+
-- once it has the response, and the log entry is recorded before the response is written
|
|
869
|
+
local WAVE_BUCKETS = 120
|
|
870
|
+
local WAVE_MAX_SAMPLES = 2000000 -- longer renders are read with a stride
|
|
871
|
+
local WAV_SAMPLE = { [1] = { [16] = { "<i2", 32768 }, [24] = { "<i3", 8388608 }, [32] = { "<i4", 2147483648 } },
|
|
872
|
+
[3] = { [32] = { "<f", 1 }, [64] = { "<d", 1 } } }
|
|
873
|
+
|
|
874
|
+
local function wav_waveform(path)
|
|
875
|
+
if type(path) ~= "string" then return nil end
|
|
876
|
+
local f = io.open(path, "rb")
|
|
877
|
+
if not f then return nil end
|
|
878
|
+
local s = f:read("a")
|
|
879
|
+
f:close()
|
|
880
|
+
if not s or #s < 44 or s:sub(1, 4) ~= "RIFF" or s:sub(9, 12) ~= "WAVE" then return nil end
|
|
881
|
+
local pos, tag, channels, bits, data_pos, data_len = 13
|
|
882
|
+
while pos + 8 <= #s do
|
|
883
|
+
local id, len = s:sub(pos, pos + 3), string.unpack("<I4", s, pos + 4)
|
|
884
|
+
if id == "fmt " and len >= 16 then
|
|
885
|
+
tag, channels = string.unpack("<I2I2", s, pos + 8)
|
|
886
|
+
bits = string.unpack("<I2", s, pos + 22)
|
|
887
|
+
if tag == 0xFFFE and len >= 26 then tag = string.unpack("<I2", s, pos + 32) end -- WAVE_FORMAT_EXTENSIBLE
|
|
888
|
+
elseif id == "data" then
|
|
889
|
+
data_pos, data_len = pos + 8, math.min(len, #s - pos - 7)
|
|
890
|
+
break
|
|
891
|
+
end
|
|
892
|
+
pos = pos + 8 + len + (len % 2)
|
|
893
|
+
end
|
|
894
|
+
local spec = tag and WAV_SAMPLE[tag] and WAV_SAMPLE[tag][bits]
|
|
895
|
+
if not spec or not data_pos or not channels or channels < 1 then return nil end
|
|
896
|
+
local fmt, scale, width = spec[1], spec[2], bits // 8
|
|
897
|
+
local frame_bytes = width * channels
|
|
898
|
+
local frames = data_len // frame_bytes
|
|
899
|
+
if frames < 1 then return nil end
|
|
900
|
+
local stride = math.max(1, math.ceil(frames * channels / WAVE_MAX_SAMPLES))
|
|
901
|
+
local hi, lo = {}, {}
|
|
902
|
+
for b = 1, WAVE_BUCKETS do hi[b], lo[b] = 0, 0 end
|
|
903
|
+
local peak, sum, count, clipped = 0, 0, 0, 0
|
|
904
|
+
for fr = 0, frames - 1, stride do
|
|
905
|
+
local b = fr * WAVE_BUCKETS // frames + 1
|
|
906
|
+
local at = data_pos + fr * frame_bytes
|
|
907
|
+
for _ = 1, channels do
|
|
908
|
+
local v = string.unpack(fmt, s, at) / scale
|
|
909
|
+
at = at + width
|
|
910
|
+
if v > hi[b] then hi[b] = v end
|
|
911
|
+
if v < lo[b] then lo[b] = v end
|
|
912
|
+
local a = math.abs(v)
|
|
913
|
+
if a > peak then peak = a end
|
|
914
|
+
if a >= 0.999 then clipped = clipped + 1 end
|
|
915
|
+
sum = sum + v * v
|
|
916
|
+
count = count + 1
|
|
917
|
+
end
|
|
918
|
+
end
|
|
919
|
+
for b = 1, WAVE_BUCKETS do hi[b], lo[b] = round2(hi[b]), round2(lo[b]) end
|
|
920
|
+
local db = function(v) return v > 0 and round2(20 * math.log(v, 10)) or -150 end
|
|
921
|
+
return { hi = hi, lo = lo, peakDb = db(peak), rmsDb = db(math.sqrt(sum / count)), clipped = clipped, channels = channels }
|
|
922
|
+
end
|
|
923
|
+
|
|
924
|
+
-- Mixing depth views: built from the handler's own result, which carries the state before
|
|
925
|
+
local MIX = {}
|
|
926
|
+
|
|
927
|
+
MIX.MODES = { trim = 0, read = 1, touch = 2, write = 3, latch = 4, latch_preview = 5 }
|
|
928
|
+
|
|
929
|
+
function MIX.automation(data)
|
|
930
|
+
local rows = {}
|
|
931
|
+
for i, t in ipairs(data.tracks or {}) do
|
|
932
|
+
if i > EDIT_MAX_ROWS then break end
|
|
933
|
+
rows[#rows + 1] = { t = t.name, p = "Automation", s = "bool",
|
|
934
|
+
a = MIX.MODES[t.before] or 0, b = MIX.MODES[t.mode] or 0, at = t.before, bt = t.mode }
|
|
935
|
+
end
|
|
936
|
+
if data.globalOverrideBefore then
|
|
937
|
+
rows[#rows + 1] = { t = "All tracks", p = "Global override", s = "bool",
|
|
938
|
+
a = data.globalOverrideBefore == data.globalOverride and 1 or 0, b = 1,
|
|
939
|
+
at = data.globalOverrideBefore, bt = data.globalOverride }
|
|
940
|
+
end
|
|
941
|
+
return { rows = rows }
|
|
942
|
+
end
|
|
943
|
+
|
|
944
|
+
-- One modulated parameter in the page's compact shape (nil when it has none)
|
|
945
|
+
function MIX.mod_row(m)
|
|
946
|
+
if type(m) ~= "table" or m.enabled == false then return nil end
|
|
947
|
+
local r = { p = m.name or ("Param " .. tostring(m.paramIndex)), base = round2(m.baseline) or 0, v = round2(m.value) or 0 }
|
|
948
|
+
if m.lfo then
|
|
949
|
+
r.lfo = { m.lfo.shape, round2(m.lfo.speed) or 0, round2(m.lfo.strength) or 0, round2(m.lfo.phase) or 0,
|
|
950
|
+
m.lfo.direction or 1, m.lfo.tempoSync and 1 or 0 }
|
|
951
|
+
end
|
|
952
|
+
if m.audio then
|
|
953
|
+
r.acs = { round2(m.audio.attackMs) or 0, round2(m.audio.releaseMs) or 0, round2(m.audio.minDb) or 0,
|
|
954
|
+
round2(m.audio.maxDb) or 0, round2(m.audio.strength) or 0, m.audio.channel or 1, m.audio.direction or 1 }
|
|
955
|
+
end
|
|
956
|
+
if m.link then
|
|
957
|
+
local src = (m.link.fxName or ("FX " .. tostring(m.link.fxIndex))) .. " / " .. (m.link.paramName or ("param " .. tostring(m.link.paramIndex)))
|
|
958
|
+
r.link = { src, round2(m.link.scale) or 1, round2(m.link.offset) or 0 }
|
|
959
|
+
end
|
|
960
|
+
return r
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
function MIX.modulation(cmd_type, data)
|
|
964
|
+
if type(data) ~= "table" then return nil end
|
|
965
|
+
local rows = {}
|
|
966
|
+
if cmd_type == "set_fx_param_modulation" then
|
|
967
|
+
local now = MIX.mod_row(data.modulation) or { p = (data.before and data.before.name) or ("Param " .. tostring(data.paramIndex)), off = 1 }
|
|
968
|
+
now.was = MIX.mod_row(data.before) or false
|
|
969
|
+
rows[1] = now
|
|
970
|
+
else
|
|
971
|
+
for i, m in ipairs(data.modulated or {}) do
|
|
972
|
+
if i > EDIT_MAX_ROWS then break end
|
|
973
|
+
rows[#rows + 1] = MIX.mod_row(m)
|
|
974
|
+
end
|
|
975
|
+
end
|
|
976
|
+
return { fx = data.fxName or "", rows = rows, total = data.modulated and #data.modulated or nil }
|
|
977
|
+
end
|
|
978
|
+
|
|
979
|
+
function MIX.pin_lists(side)
|
|
980
|
+
local out = {}
|
|
981
|
+
for i, pin in ipairs(side or {}) do
|
|
982
|
+
if i > EDIT_MAX_ROWS then break end
|
|
983
|
+
out[i] = pin.channels or {}
|
|
984
|
+
end
|
|
985
|
+
return out
|
|
986
|
+
end
|
|
987
|
+
|
|
988
|
+
function MIX.pins(data)
|
|
989
|
+
if type(data) ~= "table" or not data.inputs then return nil end
|
|
990
|
+
local r = { fx = data.fxName or "", ch = data.trackChannels or 2, ["in"] = MIX.pin_lists(data.inputs), out = MIX.pin_lists(data.outputs) }
|
|
991
|
+
local names = {}
|
|
992
|
+
for i, pin in ipairs(data.inputs) do if i <= EDIT_MAX_ROWS then names[i] = pin.name or "" end end
|
|
993
|
+
r.names = names
|
|
994
|
+
if data.before then
|
|
995
|
+
r.chWas = data.before.trackChannels
|
|
996
|
+
r.inWas = MIX.pin_lists(data.before.inputs)
|
|
997
|
+
r.outWas = MIX.pin_lists(data.before.outputs)
|
|
998
|
+
end
|
|
999
|
+
return r
|
|
1000
|
+
end
|
|
1001
|
+
|
|
1002
|
+
-- Grid lines inside an item, seconds from its start (at most EDIT_MAX_POINTS)
|
|
1003
|
+
function MIX.grid_lines(pos, len, grid)
|
|
1004
|
+
if not grid or grid <= 0 or not api.TimeMap2_timeToQN then return nil end
|
|
1005
|
+
local lines = {}
|
|
1006
|
+
local qn = math.ceil(api.TimeMap2_timeToQN(0, pos) / grid - 1e-9) * grid
|
|
1007
|
+
while #lines < EDIT_MAX_POINTS do
|
|
1008
|
+
local t = api.TimeMap2_QNToTime(0, qn) - pos
|
|
1009
|
+
if t > len + 1e-9 then break end
|
|
1010
|
+
lines[#lines + 1] = round2(t)
|
|
1011
|
+
qn = qn + grid
|
|
1012
|
+
end
|
|
1013
|
+
return lines
|
|
1014
|
+
end
|
|
1015
|
+
|
|
1016
|
+
function MIX.stretch(cmd_type, data)
|
|
1017
|
+
if type(data) ~= "table" then return nil end
|
|
1018
|
+
if cmd_type == "align_audio_to_guide" then
|
|
1019
|
+
return { len = data.length, moves = data.moves or {}, more = data.more or 0, guide = true,
|
|
1020
|
+
off = { data.offsetBefore and data.offsetBefore.avgMs or 0, data.offsetAfter and data.offsetAfter.avgMs or 0 } }
|
|
1021
|
+
end
|
|
1022
|
+
if cmd_type == "quantize_audio" then
|
|
1023
|
+
return { len = data.length, moves = data.moves or {}, more = data.more or 0, grid = MIX.grid_lines(data.position or 0, data.length or 0, data.grid),
|
|
1024
|
+
off = { data.offsetBefore and data.offsetBefore.avgMs or 0, data.offsetAfter and data.offsetAfter.avgMs or 0 } }
|
|
1025
|
+
end
|
|
1026
|
+
local marks, was = {}, {}
|
|
1027
|
+
for i, m in ipairs(data.markers or {}) do if i <= EDIT_MAX_POINTS then marks[#marks + 1] = m.position end end
|
|
1028
|
+
for i, m in ipairs(data.before or {}) do if i <= EDIT_MAX_POINTS then was[#was + 1] = m.position end end
|
|
1029
|
+
return { len = data.length, marks = marks, was = was }
|
|
1030
|
+
end
|
|
1031
|
+
|
|
1032
|
+
function MIX.freeze(cmd_type, data)
|
|
1033
|
+
if type(data) ~= "table" then return nil end
|
|
1034
|
+
local rows = {}
|
|
1035
|
+
for i, t in ipairs(data.tracks or {}) do
|
|
1036
|
+
if i > EDIT_MAX_ROWS then break end
|
|
1037
|
+
local b = t.before or {}
|
|
1038
|
+
local label = cmd_type == "freeze_tracks" and ("frozen · " .. tostring(data.mode)) or (t.frozen > 0 and ("frozen · " .. t.frozen .. " layers") or "live")
|
|
1039
|
+
rows[#rows + 1] = { t = t.name, p = "Freeze", s = "bool", a = (b.frozen or 0) > 0 and 1 or 0, b = (t.frozen or 0) > 0 and 1 or 0,
|
|
1040
|
+
at = (b.frozen or 0) > 0 and "frozen" or "live", bt = label }
|
|
1041
|
+
rows[#rows + 1] = { t = t.name, p = "FX", s = "norm", a = math.min(1, (b.fx or 0) / 16), b = math.min(1, (t.fx or 0) / 16),
|
|
1042
|
+
at = tostring(b.fx or 0), bt = tostring(t.fx or 0) }
|
|
1043
|
+
end
|
|
1044
|
+
for i, it in ipairs(data.items or {}) do
|
|
1045
|
+
if i > EDIT_MAX_ROWS then break end
|
|
1046
|
+
local b = it.before or {}
|
|
1047
|
+
rows[#rows + 1] = { t = "Track " .. ((it.trackIndex or 0) + 1) .. " · item " .. ((it.itemIndex or 0) + 1), p = "Active take", s = "norm",
|
|
1048
|
+
a = math.min(1, (b.takes or 1) / 8), b = math.min(1, (it.takes or 1) / 8), at = b.activeTake or "", bt = it.activeTake or "" }
|
|
1049
|
+
end
|
|
1050
|
+
return { rows = rows }
|
|
1051
|
+
end
|
|
1052
|
+
|
|
1053
|
+
function MIX.monitor_slots()
|
|
1054
|
+
local m = api.GetMasterTrack(0)
|
|
1055
|
+
local out = {}
|
|
1056
|
+
if not api.TrackFX_GetRecCount then return out end
|
|
1057
|
+
for i = 0, math.min(api.TrackFX_GetRecCount(m), EDIT_MAX_ROWS) - 1 do
|
|
1058
|
+
local idx = 0x1000000 + i
|
|
1059
|
+
local _, name = api.TrackFX_GetFXName(m, idx)
|
|
1060
|
+
local enabled = api.TrackFX_GetEnabled(m, idx) ~= false
|
|
1061
|
+
local offline = api.TrackFX_GetOffline and api.TrackFX_GetOffline(m, idx) == true
|
|
1062
|
+
out[#out + 1] = { name or ("FX " .. (i + 1)), (enabled and "" or "B") .. (offline and "O" or ""), short_id(api.TrackFX_GetFXGUID(m, idx)), "" }
|
|
1063
|
+
end
|
|
1064
|
+
return out
|
|
1065
|
+
end
|
|
1066
|
+
|
|
1067
|
+
MIX.MONITOR = { list_monitoring_fx = true, add_monitoring_fx = true, set_monitoring_fx = true, remove_monitoring_fx = true }
|
|
1068
|
+
|
|
1069
|
+
-- Render results: one row per file written, with a few waveform slices read through REAPER's peak API
|
|
1070
|
+
-- (any format REAPER reads). At most RENDER_MAX_FILES files get a row; the rest are counted.
|
|
1071
|
+
local RENDER_MAX_FILES = 8
|
|
1072
|
+
-- Waveforms are built on REAPER's main thread after the render already blocked it: once this much
|
|
1073
|
+
-- time went to them in one call, the remaining files get rows without a waveform
|
|
1074
|
+
local RENDER_PEAKS_BUDGET_S = 1.0
|
|
1075
|
+
local RENDER_SLICES = 60
|
|
1076
|
+
|
|
1077
|
+
-- The .reapeaks file REAPER would build for path (nil when it cannot say)
|
|
1078
|
+
local function peak_file(path)
|
|
1079
|
+
if not api.GetPeakFileName then return nil end
|
|
1080
|
+
local a, b = api.GetPeakFileName(path, "")
|
|
1081
|
+
local name = type(b) == "string" and b ~= "" and b or a
|
|
1082
|
+
return type(name) == "string" and name ~= "" and name or nil
|
|
1083
|
+
end
|
|
1084
|
+
local function exists(path)
|
|
1085
|
+
local f = path and io.open(path, "rb")
|
|
1086
|
+
if f then f:close() end
|
|
1087
|
+
return f ~= nil
|
|
1088
|
+
end
|
|
1089
|
+
|
|
1090
|
+
local function file_peaks(path)
|
|
1091
|
+
if not (api.PCM_Source_CreateFromFile and api.PCM_Source_GetPeaks and api.new_array) then return nil end
|
|
1092
|
+
local src = api.PCM_Source_CreateFromFile(path)
|
|
1093
|
+
if not src then return nil end
|
|
1094
|
+
-- Peaks built only for this view are removed again, so no .reapeaks lands beside a delivered file
|
|
1095
|
+
local pk = peak_file(path)
|
|
1096
|
+
local had_peaks = exists(pk)
|
|
1097
|
+
local ok, hi, lo = pcall(function()
|
|
1098
|
+
local len = api.GetMediaSourceLength(src)
|
|
1099
|
+
if type(len) ~= "number" or len <= 0 then return nil end
|
|
1100
|
+
local ch = math.min(2, math.max(1, math.floor(api.GetMediaSourceNumChannels(src) or 1)))
|
|
1101
|
+
if api.PCM_Source_BuildPeaks and api.PCM_Source_BuildPeaks(src, 0) ~= 0 then
|
|
1102
|
+
local guard = 0
|
|
1103
|
+
while api.PCM_Source_BuildPeaks(src, 1) ~= 0 and guard < 1000000 do guard = guard + 1 end
|
|
1104
|
+
api.PCM_Source_BuildPeaks(src, 2)
|
|
1105
|
+
end
|
|
1106
|
+
local n = RENDER_SLICES
|
|
1107
|
+
local buf = api.new_array(n * ch * 2)
|
|
1108
|
+
-- The buffer holds n * ch maxima, then n * ch minima, channels interleaved
|
|
1109
|
+
local got = math.floor(api.PCM_Source_GetPeaks(src, n / len, 0, ch, n, 0, buf)) & 0xFFFFF
|
|
1110
|
+
if got <= 0 then return nil end
|
|
1111
|
+
local hi, lo = {}, {}
|
|
1112
|
+
for i = 0, math.min(got, n) - 1 do
|
|
1113
|
+
local mx, mn = 0, 0
|
|
1114
|
+
for c = 1, ch do
|
|
1115
|
+
local a, b = buf[i * ch + c], buf[n * ch + i * ch + c]
|
|
1116
|
+
if a and a > mx then mx = a end
|
|
1117
|
+
if b and b < mn then mn = b end
|
|
1118
|
+
end
|
|
1119
|
+
hi[#hi + 1], lo[#lo + 1] = round2(mx), round2(mn)
|
|
1120
|
+
end
|
|
1121
|
+
return hi, lo
|
|
1122
|
+
end)
|
|
1123
|
+
api.PCM_Source_Destroy(src)
|
|
1124
|
+
if pk and not had_peaks and exists(pk) then os.remove(pk) end
|
|
1125
|
+
if ok and hi then return hi, lo end
|
|
1126
|
+
end
|
|
1127
|
+
|
|
1128
|
+
local function base_name(path) return (tostring(path):match("([^/\\]+)$")) or tostring(path) end
|
|
1129
|
+
|
|
1130
|
+
-- [name, duration s, LUFS-I, true peak, sample peak, hi[], lo[]] per file
|
|
1131
|
+
local function render_rows(files, analysis, budget)
|
|
1132
|
+
local by_file = {}
|
|
1133
|
+
for _, a in ipairs(analysis or {}) do by_file[a.file] = a end
|
|
1134
|
+
local rows = {}
|
|
1135
|
+
for _, f in ipairs(files or {}) do
|
|
1136
|
+
if budget.left <= 0 then budget.more = budget.more + 1 else
|
|
1137
|
+
budget.left = budget.left - 1
|
|
1138
|
+
local a = by_file[f] or {}
|
|
1139
|
+
local hi, lo
|
|
1140
|
+
local clock = api.time_precise
|
|
1141
|
+
if not clock or clock() - budget.started < RENDER_PEAKS_BUDGET_S then hi, lo = file_peaks(f) end
|
|
1142
|
+
rows[#rows + 1] = { base_name(f), round2(a.durationSeconds) or -1, round2(a.lufsIntegrated) or -150, round2(a.truePeak) or -150,
|
|
1143
|
+
round2(a.samplePeak) or -150, hi or {}, lo or {} }
|
|
1144
|
+
end
|
|
1145
|
+
end
|
|
1146
|
+
return rows
|
|
1147
|
+
end
|
|
1148
|
+
|
|
1149
|
+
local function render_view(cmd_type, data)
|
|
1150
|
+
if type(data) ~= "table" then return nil end
|
|
1151
|
+
local budget = { left = RENDER_MAX_FILES, more = 0, started = api.time_precise and api.time_precise() or 0 }
|
|
1152
|
+
local groups = {}
|
|
1153
|
+
if cmd_type == "render_versions" then
|
|
1154
|
+
for _, v in ipairs(data.versions or {}) do
|
|
1155
|
+
groups[#groups + 1] = { v = tostring(v.name), m = v.muted or {}, f = render_rows(v.files, v.analysis, budget) }
|
|
1156
|
+
end
|
|
1157
|
+
else
|
|
1158
|
+
groups[1] = { v = "", m = {}, f = render_rows(data.files, data.analysis, budget) }
|
|
1159
|
+
end
|
|
1160
|
+
local r = { fmt = data.format or "", src = data.source or "master", groups = groups }
|
|
1161
|
+
if data.bitDepth then r.bd = tostring(data.bitDepth) end
|
|
1162
|
+
if data.mp3Bitrate then r.kbps = data.mp3Bitrate end
|
|
1163
|
+
if data.normalize then r.norm, r.nt = data.normalize, round2(data.normalizeTarget) end
|
|
1164
|
+
if data.limitCeiling then r.lim, r.tp = round2(data.limitCeiling), data.limitTruePeak == true end
|
|
1165
|
+
if budget.more > 0 then r.more = budget.more end
|
|
1166
|
+
return r
|
|
1167
|
+
end
|
|
1168
|
+
|
|
1169
|
+
-- Recording: the global mode and one row per track [name, armed 0/1, input, monitor, mode]
|
|
1170
|
+
local RECORD_COMMANDS = { get_record_setup = true, set_track_record = true, set_record_mode = true }
|
|
1171
|
+
|
|
1172
|
+
-- Rows for the first EDIT_MAX_ROWS states, and how many states did not fit
|
|
1173
|
+
local function record_rows(states)
|
|
1174
|
+
local rows, dropped = {}, 0
|
|
1175
|
+
for _, s in ipairs(states or {}) do
|
|
1176
|
+
if #rows < EDIT_MAX_ROWS then
|
|
1177
|
+
rows[#rows + 1] = { s.name or "", s.armed and 1 or 0, (s.input and s.input.label) or "", tostring(s.monitor or ""), tostring(s.mode or "") }
|
|
1178
|
+
else
|
|
1179
|
+
dropped = dropped + 1
|
|
1180
|
+
end
|
|
1181
|
+
end
|
|
1182
|
+
return rows, dropped
|
|
1183
|
+
end
|
|
1184
|
+
|
|
1185
|
+
local function record_global()
|
|
1186
|
+
local g = ctx.recording.global_state(api)
|
|
1187
|
+
return { mode = g.mode, path = g.recordPath, punch = g.punch and { g.punch.start, g.punch["end"] } or nil }
|
|
1188
|
+
end
|
|
1189
|
+
|
|
1190
|
+
local function record_tracks(params)
|
|
1191
|
+
local states = {}
|
|
1192
|
+
for _, u in ipairs(decode_param_list(params.tracks) or {}) do
|
|
1193
|
+
local tr = type(u) == "table" and ctx.resolve_track(u.track)
|
|
1194
|
+
if tr and #states < EDIT_MAX_ROWS then states[#states + 1] = ctx.recording.record_state(api, tr) end
|
|
1195
|
+
end
|
|
1196
|
+
return states
|
|
1197
|
+
end
|
|
1198
|
+
|
|
1199
|
+
local function record_view(cmd_type, params, data, before)
|
|
1200
|
+
local r = record_global()
|
|
1201
|
+
local dropped = 0
|
|
1202
|
+
if cmd_type == "get_record_setup" then
|
|
1203
|
+
r.rows, dropped = record_rows(type(data) == "table" and data.tracks or nil)
|
|
1204
|
+
dropped = dropped + (type(data) == "table" and tonumber(data.tracksMore) or 0)
|
|
1205
|
+
elseif cmd_type == "set_track_record" then
|
|
1206
|
+
r.rows, dropped = record_rows(type(data) == "table" and data.tracks or record_tracks(params))
|
|
1207
|
+
end
|
|
1208
|
+
if dropped > 0 then r.more = dropped end
|
|
1209
|
+
if before then
|
|
1210
|
+
r.was = { mode = before.mode, path = before.path, punch = before.punch, rows = before.rows }
|
|
1211
|
+
end
|
|
1212
|
+
return r
|
|
1213
|
+
end
|
|
1214
|
+
|
|
1215
|
+
-- Fixed lanes: [name, plays 0 off / 1 alone / 2 layered] per lane and [position, length, lane, 1 = MIDI] per item
|
|
1216
|
+
local LANE_COMMANDS = { get_track_lanes = true, set_track_lanes = true, comp_lanes = true }
|
|
1217
|
+
local PLAYS_CODE = { off = 0, exclusive = 1, layered = 2 }
|
|
1218
|
+
|
|
1219
|
+
local function lane_snapshot(tr)
|
|
1220
|
+
local st = ctx.recording.lanes_state(api, tr)
|
|
1221
|
+
local lanes, items = {}, {}
|
|
1222
|
+
-- Lanes past EDIT_MAX_ROWS are counted in `more`, their items left out (they have no row to sit on)
|
|
1223
|
+
for i, l in ipairs(st.lanes) do
|
|
1224
|
+
if i > EDIT_MAX_ROWS then break end
|
|
1225
|
+
lanes[i] = { l.name, PLAYS_CODE[l.plays] or 0 }
|
|
1226
|
+
for _, it in ipairs(l.items) do
|
|
1227
|
+
if #items < EDIT_MAX_NOTES / 2 then items[#items + 1] = { round2(it.position) or 0, round2(it.length) or 0, l.lane, it.midi and 1 or 0 } end
|
|
1228
|
+
end
|
|
1229
|
+
end
|
|
1230
|
+
local more = #st.lanes - #lanes
|
|
1231
|
+
return { t = st.name, fixed = st.fixedLanes and 1 or 0, lanes = lanes, items = items, more = more > 0 and more or nil }
|
|
1232
|
+
end
|
|
1233
|
+
|
|
1234
|
+
local function lane_track(params)
|
|
1235
|
+
local tr = ctx.resolve_track(params.track)
|
|
1236
|
+
if not tr or (api.GetMasterTrack and tr == api.GetMasterTrack(0)) then return nil end
|
|
1237
|
+
return tr
|
|
1238
|
+
end
|
|
1239
|
+
|
|
1240
|
+
local function lane_view(params, data, before)
|
|
1241
|
+
local tr = lane_track(params)
|
|
1242
|
+
if not tr then return nil end
|
|
1243
|
+
local r = lane_snapshot(tr)
|
|
1244
|
+
local len = 0
|
|
1245
|
+
for _, it in ipairs(r.items) do len = math.max(len, it[1] + it[2]) end
|
|
1246
|
+
if before then
|
|
1247
|
+
r.was = { lanes = before.lanes, items = before.items, fixed = before.fixed }
|
|
1248
|
+
for _, it in ipairs(before.items) do len = math.max(len, it[1] + it[2]) end
|
|
1249
|
+
end
|
|
1250
|
+
if type(data) == "table" and data.compLane then
|
|
1251
|
+
r.comp = data.compLane
|
|
1252
|
+
r.segs = {}
|
|
1253
|
+
for _, s in ipairs(data.segments or {}) do
|
|
1254
|
+
if #r.segs < EDIT_MAX_MARKS then r.segs[#r.segs + 1] = { round2(s.start) or 0, round2(s["end"]) or 0, s.lane } end
|
|
1255
|
+
end
|
|
1256
|
+
end
|
|
1257
|
+
r.len = round2(len) or 0
|
|
1258
|
+
return r
|
|
1259
|
+
end
|
|
1260
|
+
|
|
1261
|
+
-- The state a write is about to change, read before its handler runs (nil for every other command)
|
|
1262
|
+
function M.before(cmd_type, params)
|
|
1263
|
+
if cmd_type == "set_track_property" or cmd_type == "set_multiple_track_properties" then
|
|
1264
|
+
return read_track_values(track_targets(params, cmd_type))
|
|
1265
|
+
elseif cmd_type == "set_fx_parameter" or cmd_type == "set_multiple_fx_parameters" then
|
|
1266
|
+
return read_fx_values(fx_targets(params, cmd_type))
|
|
1267
|
+
elseif ENVELOPE_WRITES[cmd_type] or (AUTOMATION_ITEM_COMMANDS[cmd_type] and cmd_type ~= "list_automation_items") then
|
|
1268
|
+
local env = env_of(params)
|
|
1269
|
+
if not env then return nil end
|
|
1270
|
+
local pts = env_points(env)
|
|
1271
|
+
pts.items = env_items(env)
|
|
1272
|
+
return pts
|
|
1273
|
+
elseif MIDI_WRITES[cmd_type] then
|
|
1274
|
+
return midi_notes(params)
|
|
1275
|
+
elseif CC_COMMANDS[cmd_type] and cmd_type ~= "get_midi_cc" then
|
|
1276
|
+
return cc_events(params)
|
|
1277
|
+
elseif TIMELINE_WRITES[cmd_type] then
|
|
1278
|
+
return timeline_snapshot()
|
|
1279
|
+
elseif FX_CHAIN_COMMANDS[cmd_type] and cmd_type ~= "get_track_properties" then
|
|
1280
|
+
return fx_chains(params)
|
|
1281
|
+
elseif ITEM_COMMANDS[cmd_type] and cmd_type ~= "list_midi_items" then
|
|
1282
|
+
return item_lanes(params)
|
|
1283
|
+
elseif ARRANGE.OPS[cmd_type] or cmd_type == "process_items" then
|
|
1284
|
+
return ARRANGE.before(cmd_type, params)
|
|
1285
|
+
elseif cmd_type == "set_time_selection" or cmd_type == "set_cursor_position" then
|
|
1286
|
+
return time_state()
|
|
1287
|
+
elseif ROUTING_COMMANDS[cmd_type] and cmd_type ~= "get_track_routing" then
|
|
1288
|
+
local tr = routing_track(params)
|
|
1289
|
+
return tr and { track = tr, routes = routes_of(tr) }
|
|
1290
|
+
elseif STRUCTURE_COMMANDS[cmd_type] then
|
|
1291
|
+
local rows, ids = mixer_all()
|
|
1292
|
+
return { tracks = rows, ids = ids }
|
|
1293
|
+
elseif ctx.recording and (cmd_type == "set_track_record" or cmd_type == "set_record_mode") then
|
|
1294
|
+
local g = record_global()
|
|
1295
|
+
if cmd_type == "set_track_record" then g.rows = record_rows(record_tracks(params)) end
|
|
1296
|
+
return g
|
|
1297
|
+
elseif ctx.recording and (cmd_type == "set_track_lanes" or cmd_type == "comp_lanes") then
|
|
1298
|
+
local tr = lane_track(params)
|
|
1299
|
+
return tr and lane_snapshot(tr)
|
|
1300
|
+
elseif MIX.MONITOR[cmd_type] and cmd_type ~= "list_monitoring_fx" then
|
|
1301
|
+
return MIX.monitor_slots()
|
|
1302
|
+
end
|
|
1303
|
+
end
|
|
1304
|
+
|
|
1305
|
+
-- The entry's `r`: an edit view, a measurement, or nil
|
|
1306
|
+
function M.summary(cmd_type, params, data, before)
|
|
1307
|
+
if cmd_type == "set_track_property" or cmd_type == "set_multiple_track_properties" then
|
|
1308
|
+
return change_rows(before, read_track_values(track_targets(params, cmd_type)))
|
|
1309
|
+
elseif cmd_type == "set_fx_parameter" or cmd_type == "set_multiple_fx_parameters" then
|
|
1310
|
+
return change_rows(before, read_fx_values(fx_targets(params, cmd_type)))
|
|
1311
|
+
elseif ENVELOPE_WRITES[cmd_type] or AUTOMATION_ITEM_COMMANDS[cmd_type] or cmd_type == "get_envelope_points" then
|
|
1312
|
+
return envelope_view(cmd_type, params, data, before)
|
|
1313
|
+
elseif MIDI_WRITES[cmd_type] or cmd_type == "get_midi_notes" then
|
|
1314
|
+
return midi_view(params, before)
|
|
1315
|
+
elseif cmd_type == "import_midi_file" then
|
|
1316
|
+
-- The first created item, all notes new; the other items (one per new track) are counted
|
|
1317
|
+
local first = type(data) == "table" and type(data.items) == "table" and data.items[1]
|
|
1318
|
+
local view = first and first.itemIndex and midi_view({ trackIndex = first.trackIndex, itemIndex = first.itemIndex }, {})
|
|
1319
|
+
if view and #data.items > 1 then view.items = #data.items - 1 end
|
|
1320
|
+
return view
|
|
1321
|
+
elseif cmd_type == "analyze_midi" then
|
|
1322
|
+
return midi_stats_view(data)
|
|
1323
|
+
elseif CC_COMMANDS[cmd_type] then
|
|
1324
|
+
return cc_view(cmd_type, params, before)
|
|
1325
|
+
elseif TIMELINE_WRITES[cmd_type] or cmd_type == "list_markers" or cmd_type == "list_regions"
|
|
1326
|
+
or cmd_type == "get_tempo_map" or cmd_type == "list_media_items" then
|
|
1327
|
+
return timeline_view(cmd_type, data, before)
|
|
1328
|
+
elseif cmd_type == "list_tracks" or STRUCTURE_COMMANDS[cmd_type] then
|
|
1329
|
+
return mixer_view(before)
|
|
1330
|
+
elseif FX_CHAIN_COMMANDS[cmd_type] then
|
|
1331
|
+
return fx_chain_view(params, before)
|
|
1332
|
+
elseif ITEM_COMMANDS[cmd_type] then
|
|
1333
|
+
return item_view(params, before)
|
|
1334
|
+
elseif ARRANGE.OPS[cmd_type] or cmd_type == "process_items" then
|
|
1335
|
+
return ARRANGE.view(cmd_type, params, data, before)
|
|
1336
|
+
elseif TIME_COMMANDS[cmd_type] then
|
|
1337
|
+
return time_view(before)
|
|
1338
|
+
elseif ROUTING_COMMANDS[cmd_type] then
|
|
1339
|
+
-- A write reads the same track it read before (remove_send can leave nothing to resolve by index)
|
|
1340
|
+
local tr = before and before.track or routing_track(params)
|
|
1341
|
+
if not tr then return nil end
|
|
1342
|
+
return { t = name_of(tr), routes = routes_of(tr), was = before and before.routes or nil }
|
|
1343
|
+
elseif cmd_type == "get_fx_parameters" then
|
|
1344
|
+
return fx_param_rows(params)
|
|
1345
|
+
elseif cmd_type == "run_plan" then
|
|
1346
|
+
return plan_view(params, data)
|
|
1347
|
+
elseif cmd_type == "set_automation_mode" then
|
|
1348
|
+
return type(data) == "table" and MIX.automation(data) or nil
|
|
1349
|
+
elseif cmd_type == "get_fx_param_modulation" or cmd_type == "set_fx_param_modulation" then
|
|
1350
|
+
return MIX.modulation(cmd_type, data)
|
|
1351
|
+
elseif cmd_type == "get_fx_pins" or cmd_type == "set_fx_pins" then
|
|
1352
|
+
return MIX.pins(data)
|
|
1353
|
+
elseif cmd_type == "add_stretch_markers" or cmd_type == "edit_stretch_markers" or cmd_type == "quantize_audio" or cmd_type == "align_audio_to_guide" then
|
|
1354
|
+
return MIX.stretch(cmd_type, data)
|
|
1355
|
+
elseif cmd_type == "freeze_tracks" or cmd_type == "unfreeze_tracks" or cmd_type == "apply_fx_to_items" then
|
|
1356
|
+
return MIX.freeze(cmd_type, data)
|
|
1357
|
+
elseif MIX.MONITOR[cmd_type] then
|
|
1358
|
+
return { chains = { { t = "Monitoring FX", fx = MIX.monitor_slots(), was = before } } }
|
|
1359
|
+
elseif cmd_type == "render_project" or cmd_type == "render_versions" then
|
|
1360
|
+
return render_view(cmd_type, data)
|
|
1361
|
+
elseif RECORD_COMMANDS[cmd_type] then
|
|
1362
|
+
return ctx.recording and record_view(cmd_type, params, data, before)
|
|
1363
|
+
elseif LANE_COMMANDS[cmd_type] then
|
|
1364
|
+
return ctx.recording and lane_view(params, data, before)
|
|
1365
|
+
end
|
|
1366
|
+
local fields = RESULT_FIELDS[cmd_type]
|
|
1367
|
+
if not fields or type(data) ~= "table" then return nil end
|
|
1368
|
+
local r = {}
|
|
1369
|
+
for _, k in ipairs(fields) do r[k] = round2(data[k]) end
|
|
1370
|
+
if cmd_type == "read_track_spectrum" then r.bands = spectrum_bands(data) end
|
|
1371
|
+
if cmd_type == "render_track_to_wav" then
|
|
1372
|
+
local ok, wave = pcall(wav_waveform, data.wavPath)
|
|
1373
|
+
if ok and wave then for k, v in pairs(wave) do r[k] = v end end
|
|
1374
|
+
end
|
|
1375
|
+
return r
|
|
1376
|
+
end
|
|
1377
|
+
|
|
1378
|
+
return M
|