@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,595 @@
|
|
|
1
|
+
-- mcp_recording_lib.lua — record setup (arm, input, monitoring, record mode)
|
|
2
|
+
-- and fixed lanes with comping.
|
|
3
|
+
--
|
|
4
|
+
-- Pure logic: every function takes the ReaScript API as `api`, so it runs
|
|
5
|
+
-- under a fake API in unit tests. Loaded by mcp_bridge.lua with dofile.
|
|
6
|
+
-- Every write validates all of its input first: a refused call changes nothing.
|
|
7
|
+
|
|
8
|
+
local M = {}
|
|
9
|
+
|
|
10
|
+
-- Global record mode: one of three toggle actions.
|
|
11
|
+
M.RECORD_MODES = { normal = 40252, time_selection = 40076, selected_items = 40253 }
|
|
12
|
+
|
|
13
|
+
-- I_RECMODE values by name (REAPER 7).
|
|
14
|
+
M.TRACK_MODES = {
|
|
15
|
+
input = 0, stereo_out = 1, none = 2, stereo_out_latency = 3, midi_out = 4,
|
|
16
|
+
mono_out = 5, mono_out_latency = 6, midi_overdub = 7, midi_replace = 8,
|
|
17
|
+
midi_touch_replace = 9, multichannel_out = 10, multichannel_out_latency = 11,
|
|
18
|
+
input_mono = 12, input_stereo = 13, input_multichannel = 14, input_midi = 15,
|
|
19
|
+
midi_latch_replace = 16,
|
|
20
|
+
}
|
|
21
|
+
local MODE_NAME = {}
|
|
22
|
+
for k, v in pairs(M.TRACK_MODES) do MODE_NAME[v] = k end
|
|
23
|
+
|
|
24
|
+
M.MONITOR = { off = 0, on = 1, auto = 2 }
|
|
25
|
+
local MONITOR_NAME = { [0] = "off", [1] = "on", [2] = "auto" }
|
|
26
|
+
|
|
27
|
+
local MAX_TRACKS = 64
|
|
28
|
+
local MAX_LANES = 64
|
|
29
|
+
local MAX_LANE_ITEMS = 100
|
|
30
|
+
local MAX_RANGES = 200
|
|
31
|
+
local MIDI_ALL, MIDI_VKB = 63, 62
|
|
32
|
+
|
|
33
|
+
local function int(v) return math.floor((tonumber(v) or 0) + 0.5) end
|
|
34
|
+
local function round3(v) return v and math.floor(v * 1000 + 0.5) / 1000 or nil end
|
|
35
|
+
local function secs(v) return string.format("%g", round3(v)) end
|
|
36
|
+
|
|
37
|
+
local function track_name(api, tr)
|
|
38
|
+
local _, name = api.GetTrackName(tr)
|
|
39
|
+
return name or ""
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
local function track_index(api, tr)
|
|
43
|
+
return int(api.GetMediaTrackInfo_Value(tr, "IP_TRACKNUMBER")) - 1
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
-- Resolve a user track ref to a real (non-master) track.
|
|
47
|
+
local function resolve(api, ref, resolve_track, what)
|
|
48
|
+
local res, err = resolve_track(api, ref, {})
|
|
49
|
+
if not res then return nil, (what and (what .. ": ") or "") .. err end
|
|
50
|
+
if not res.track or res.track == api.GetMasterTrack(0) then
|
|
51
|
+
return nil, (what and (what .. ": ") or "") .. "the master track cannot record or hold lanes"
|
|
52
|
+
end
|
|
53
|
+
return res.track
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
-- =============================================================================
|
|
57
|
+
-- Record inputs
|
|
58
|
+
-- =============================================================================
|
|
59
|
+
|
|
60
|
+
local function midi_device_name(api, dev)
|
|
61
|
+
if dev == MIDI_ALL then return "all MIDI inputs" end
|
|
62
|
+
if dev == MIDI_VKB then return "virtual MIDI keyboard" end
|
|
63
|
+
local ok, name = api.GetMIDIInputName(dev, "")
|
|
64
|
+
return ok and name or ("MIDI device " .. dev)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
-- I_RECINPUT → { kind, ... , label }
|
|
68
|
+
function M.decode_input(api, v)
|
|
69
|
+
v = int(v)
|
|
70
|
+
if v < 0 then return { kind = "none", label = "No input" } end
|
|
71
|
+
if v & 4096 ~= 0 then
|
|
72
|
+
local channel = v & 31
|
|
73
|
+
local device = (v >> 5) & 63
|
|
74
|
+
local dev_label = device == MIDI_ALL and "all" or device == MIDI_VKB and "vkb" or device
|
|
75
|
+
return {
|
|
76
|
+
kind = "midi", device = dev_label, channel = channel,
|
|
77
|
+
label = "MIDI: " .. midi_device_name(api, device) .. (channel == 0 and ", all channels" or (", channel " .. channel)),
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
local start = (v & 1023) + 1
|
|
81
|
+
local width = (v & 2048 ~= 0) and "multichannel" or (v & 1024 ~= 0) and "stereo" or "mono"
|
|
82
|
+
local label = width == "stereo" and ("In " .. start .. "/" .. (start + 1)) or ("In " .. start)
|
|
83
|
+
if width == "multichannel" then label = label .. " (multichannel)" end
|
|
84
|
+
if api.GetInputChannelName and start <= (api.GetNumAudioInputs() or 0) then
|
|
85
|
+
local name = api.GetInputChannelName(start - 1)
|
|
86
|
+
if name and name ~= "" and width == "mono" then label = label .. " · " .. name end
|
|
87
|
+
end
|
|
88
|
+
return { kind = "audio", channel = start, width = width, label = label }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
-- { kind = "audio", channel = 1-based, stereo = bool } | { kind = "midi", device = n | "all" | "vkb", channel = 0..16 }
|
|
92
|
+
-- | { kind = "none" } → I_RECINPUT, warnings
|
|
93
|
+
local function encode_input(api, spec, what)
|
|
94
|
+
if type(spec) ~= "table" then return nil, what .. ": input must be an object" end
|
|
95
|
+
if spec.kind == "none" then return -1 end
|
|
96
|
+
if spec.kind == "audio" then
|
|
97
|
+
local ch = tonumber(spec.channel)
|
|
98
|
+
if not ch or ch < 1 or ch ~= math.floor(ch) then return nil, what .. ": input.channel must be a whole number from 1" end
|
|
99
|
+
local stereo = spec.stereo == true
|
|
100
|
+
local count = api.GetNumAudioInputs() or 0
|
|
101
|
+
local last = ch + (stereo and 1 or 0)
|
|
102
|
+
local warn
|
|
103
|
+
if count == 0 then
|
|
104
|
+
warn = what .. ": the audio device reports no inputs (is it open?), so input " .. ch .. " is set unchecked"
|
|
105
|
+
elseif last > count then
|
|
106
|
+
return nil, what .. ": input " .. (stereo and (ch .. "/" .. last) or tostring(ch)) .. " does not exist (the audio device has " .. count .. " inputs)"
|
|
107
|
+
end
|
|
108
|
+
return (int(ch) - 1) | (stereo and 1024 or 0), warn
|
|
109
|
+
end
|
|
110
|
+
if spec.kind == "midi" then
|
|
111
|
+
local dev = spec.device
|
|
112
|
+
if dev == nil or dev == "all" then dev = MIDI_ALL
|
|
113
|
+
elseif dev == "vkb" then dev = MIDI_VKB
|
|
114
|
+
else
|
|
115
|
+
dev = tonumber(dev)
|
|
116
|
+
if not dev or dev < 0 or dev > 61 or dev ~= math.floor(dev) then
|
|
117
|
+
return nil, what .. ": input.device must be a MIDI input index (0-61), \"all\" or \"vkb\""
|
|
118
|
+
end
|
|
119
|
+
local ok = api.GetMIDIInputName(int(dev), "")
|
|
120
|
+
if not ok then return nil, what .. ": MIDI input " .. dev .. " does not exist (get_record_setup lists them)" end
|
|
121
|
+
end
|
|
122
|
+
local channel = tonumber(spec.channel or 0)
|
|
123
|
+
if not channel or channel < 0 or channel > 16 or channel ~= math.floor(channel) then
|
|
124
|
+
return nil, what .. ": input.channel must be 0 (all) to 16 for MIDI"
|
|
125
|
+
end
|
|
126
|
+
return 4096 | int(channel) | (int(dev) << 5)
|
|
127
|
+
end
|
|
128
|
+
return nil, what .. ": input.kind must be audio, midi or none"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
local function record_state(api, tr)
|
|
132
|
+
local mode = int(api.GetMediaTrackInfo_Value(tr, "I_RECMODE"))
|
|
133
|
+
local monitor = int(api.GetMediaTrackInfo_Value(tr, "I_RECMON"))
|
|
134
|
+
return {
|
|
135
|
+
index = track_index(api, tr),
|
|
136
|
+
name = track_name(api, tr),
|
|
137
|
+
armed = api.GetMediaTrackInfo_Value(tr, "I_RECARM") ~= 0,
|
|
138
|
+
input = M.decode_input(api, api.GetMediaTrackInfo_Value(tr, "I_RECINPUT")),
|
|
139
|
+
monitor = MONITOR_NAME[monitor] or monitor,
|
|
140
|
+
monitorItems = api.GetMediaTrackInfo_Value(tr, "I_RECMONITEMS") ~= 0,
|
|
141
|
+
mode = MODE_NAME[mode] or mode,
|
|
142
|
+
inputFx = api.TrackFX_GetRecCount and api.TrackFX_GetRecCount(tr) or 0,
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
M.record_state = record_state
|
|
146
|
+
|
|
147
|
+
local function global_mode(api)
|
|
148
|
+
if api.GetToggleCommandState(M.RECORD_MODES.time_selection) == 1 then return "time_selection" end
|
|
149
|
+
if api.GetToggleCommandState(M.RECORD_MODES.selected_items) == 1 then return "selected_items" end
|
|
150
|
+
return "normal"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
local function time_selection(api)
|
|
154
|
+
local s, e = api.GetSet_LoopTimeRange(false, false, 0, 0, false)
|
|
155
|
+
if type(s) == "number" and type(e) == "number" and e > s then return { start = round3(s), ["end"] = round3(e) } end
|
|
156
|
+
return nil
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
local function record_path(api)
|
|
160
|
+
local _, path = api.GetSetProjectInfo_String(0, "RECORD_PATH", "", false)
|
|
161
|
+
return path or ""
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
local function inputs(api)
|
|
165
|
+
local audio, midi = {}, {}
|
|
166
|
+
local n_audio = api.GetNumAudioInputs() or 0
|
|
167
|
+
for i = 0, math.min(n_audio, MAX_LANES) - 1 do
|
|
168
|
+
audio[#audio + 1] = { channel = i + 1, name = api.GetInputChannelName(i) or ("Input " .. (i + 1)) }
|
|
169
|
+
end
|
|
170
|
+
for i = 0, math.min(api.GetNumMIDIInputs() or 0, MIDI_VKB) - 1 do
|
|
171
|
+
local ok, name = api.GetMIDIInputName(i, "")
|
|
172
|
+
if ok then midi[#midi + 1] = { device = i, name = name } end
|
|
173
|
+
end
|
|
174
|
+
return { audioCount = n_audio, audio = audio, audioMore = math.max(0, n_audio - #audio), midi = midi }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
--- The global record mode, punch range (time_selection mode) and record path
|
|
178
|
+
function M.global_state(api)
|
|
179
|
+
local mode = global_mode(api)
|
|
180
|
+
return { mode = mode, punch = mode == "time_selection" and time_selection(api) or nil, recordPath = record_path(api) }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
--- get_record_setup: global mode, punch range, record path, the armed tracks
|
|
184
|
+
--- (or every track with `all`) and the inputs that exist.
|
|
185
|
+
function M.get_record_setup(api, params)
|
|
186
|
+
params = params or {}
|
|
187
|
+
local tracks, total = {}, api.CountTracks(0)
|
|
188
|
+
local listed, armed = 0, 0
|
|
189
|
+
for i = 0, total - 1 do
|
|
190
|
+
local tr = api.GetTrack(0, i)
|
|
191
|
+
local is_armed = api.GetMediaTrackInfo_Value(tr, "I_RECARM") ~= 0
|
|
192
|
+
if is_armed then armed = armed + 1 end
|
|
193
|
+
if params.all or is_armed then
|
|
194
|
+
listed = listed + 1
|
|
195
|
+
if #tracks < MAX_TRACKS then tracks[#tracks + 1] = record_state(api, tr) end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
local mode = global_mode(api)
|
|
199
|
+
local path = record_path(api)
|
|
200
|
+
return {
|
|
201
|
+
mode = mode,
|
|
202
|
+
punch = mode == "time_selection" and time_selection(api) or nil,
|
|
203
|
+
recordPath = path,
|
|
204
|
+
projectPath = api.GetProjectPath and api.GetProjectPath("") or nil,
|
|
205
|
+
armedCount = armed,
|
|
206
|
+
tracks = tracks,
|
|
207
|
+
tracksMore = math.max(0, listed - #tracks),
|
|
208
|
+
inputs = inputs(api),
|
|
209
|
+
}
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
--- set_track_record: per-track arm, input, monitoring, record mode and
|
|
213
|
+
--- monitor-items, all validated before the first write; one undo point.
|
|
214
|
+
function M.set_track_record(api, updates, resolve_track)
|
|
215
|
+
if type(updates) ~= "table" or #updates == 0 then return nil, "tracks must be a non-empty array" end
|
|
216
|
+
if #updates > MAX_TRACKS then return nil, "at most " .. MAX_TRACKS .. " tracks per call" end
|
|
217
|
+
local plan, warnings, seen = {}, {}, {}
|
|
218
|
+
for k, u in ipairs(updates) do
|
|
219
|
+
local what = "tracks[" .. (k - 1) .. "]"
|
|
220
|
+
if type(u) ~= "table" then return nil, what .. " must be an object" end
|
|
221
|
+
local tr, err = resolve(api, u.track, resolve_track, what)
|
|
222
|
+
if not tr then return nil, err .. " — nothing changed" end
|
|
223
|
+
if seen[tr] then return nil, what .. ": track listed twice — nothing changed" end
|
|
224
|
+
seen[tr] = true
|
|
225
|
+
local p = { track = tr, sets = {} }
|
|
226
|
+
if u.arm ~= nil then p.sets[#p.sets + 1] = { "I_RECARM", u.arm and 1 or 0 } end
|
|
227
|
+
if u.input ~= nil then
|
|
228
|
+
local v, warn = encode_input(api, u.input, what)
|
|
229
|
+
if not v then return nil, warn .. " — nothing changed" end
|
|
230
|
+
if warn then warnings[#warnings + 1] = warn end
|
|
231
|
+
p.sets[#p.sets + 1] = { "I_RECINPUT", v }
|
|
232
|
+
end
|
|
233
|
+
if u.monitor ~= nil then
|
|
234
|
+
local v = M.MONITOR[u.monitor]
|
|
235
|
+
if not v then return nil, what .. ": monitor must be off, on or auto — nothing changed" end
|
|
236
|
+
p.sets[#p.sets + 1] = { "I_RECMON", v }
|
|
237
|
+
end
|
|
238
|
+
if u.mode ~= nil then
|
|
239
|
+
local v = M.TRACK_MODES[u.mode]
|
|
240
|
+
if not v then return nil, what .. ": unknown record mode '" .. tostring(u.mode) .. "' — nothing changed" end
|
|
241
|
+
p.sets[#p.sets + 1] = { "I_RECMODE", v }
|
|
242
|
+
end
|
|
243
|
+
if u.monitorItems ~= nil then p.sets[#p.sets + 1] = { "I_RECMONITEMS", u.monitorItems and 1 or 0 } end
|
|
244
|
+
if #p.sets == 0 then return nil, what .. ": nothing to set (arm, input, monitor, mode or monitorItems) — nothing changed" end
|
|
245
|
+
plan[k] = p
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
api.Undo_BeginBlock()
|
|
249
|
+
for _, p in ipairs(plan) do
|
|
250
|
+
for _, s in ipairs(p.sets) do api.SetMediaTrackInfo_Value(p.track, s[1], s[2]) end
|
|
251
|
+
end
|
|
252
|
+
api.Undo_EndBlock("MCP: Set track record", -1)
|
|
253
|
+
|
|
254
|
+
local out = {}
|
|
255
|
+
for k, p in ipairs(plan) do
|
|
256
|
+
out[k] = record_state(api, p.track)
|
|
257
|
+
-- MIDI mode on an audio input (or the reverse) records nothing useful.
|
|
258
|
+
local midi_mode = out[k].mode == "midi_overdub" or out[k].mode == "midi_replace" or out[k].mode == "midi_touch_replace"
|
|
259
|
+
or out[k].mode == "midi_latch_replace" or out[k].mode == "input_midi"
|
|
260
|
+
if midi_mode and out[k].input.kind == "audio" then
|
|
261
|
+
warnings[#warnings + 1] = out[k].name .. ": a MIDI record mode with an audio input records nothing"
|
|
262
|
+
end
|
|
263
|
+
if out[k].armed and out[k].input.kind == "none" and out[k].mode == "input" then
|
|
264
|
+
warnings[#warnings + 1] = out[k].name .. ": armed with no input — it records silence"
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
return { tracks = out, warnings = #warnings > 0 and warnings or nil }
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
--- set_record_mode: global record mode, optional punch range (as the time
|
|
271
|
+
--- selection) and record path.
|
|
272
|
+
function M.set_record_mode(api, params)
|
|
273
|
+
params = params or {}
|
|
274
|
+
local mode = params.mode
|
|
275
|
+
if mode ~= nil and not M.RECORD_MODES[mode] then
|
|
276
|
+
return nil, "mode must be normal, time_selection or selected_items — nothing changed"
|
|
277
|
+
end
|
|
278
|
+
local ps, pe = tonumber(params.punchStart), tonumber(params.punchEnd)
|
|
279
|
+
if (ps == nil) ~= (pe == nil) then return nil, "give both punchStart and punchEnd — nothing changed" end
|
|
280
|
+
if ps then
|
|
281
|
+
if ps < 0 or pe <= ps then return nil, "punchEnd must be after punchStart (seconds, from 0) — nothing changed" end
|
|
282
|
+
if (mode or global_mode(api)) ~= "time_selection" then
|
|
283
|
+
return nil, "a punch range needs mode time_selection — nothing changed"
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
local path = params.recordPath
|
|
287
|
+
if path ~= nil and (type(path) ~= "string" or path:match("^%s*$")) then
|
|
288
|
+
return nil, "recordPath must be a non-empty folder name or path — nothing changed"
|
|
289
|
+
end
|
|
290
|
+
if mode == nil and ps == nil and path == nil then return nil, "nothing to set (mode, punchStart/punchEnd or recordPath)" end
|
|
291
|
+
|
|
292
|
+
api.Undo_BeginBlock()
|
|
293
|
+
if mode and global_mode(api) ~= mode then api.Main_OnCommand(M.RECORD_MODES[mode], 0) end
|
|
294
|
+
local now = global_mode(api)
|
|
295
|
+
-- The mode is a toggle action REAPER may not run (another section focused, a newer action id): never report
|
|
296
|
+
-- a switch that did not happen, and stop before the punch range and path are written
|
|
297
|
+
if mode and now ~= mode then
|
|
298
|
+
api.Undo_EndBlock("MCP: Set record mode (failed)", -1)
|
|
299
|
+
return nil, "REAPER did not switch the record mode to " .. mode .. " (it reads " .. now .. ") — nothing changed"
|
|
300
|
+
end
|
|
301
|
+
if ps then api.GetSet_LoopTimeRange(true, false, ps, pe, false) end
|
|
302
|
+
if path then api.GetSetProjectInfo_String(0, "RECORD_PATH", path, true) end
|
|
303
|
+
api.Undo_EndBlock("MCP: Set record mode", -1)
|
|
304
|
+
return {
|
|
305
|
+
mode = now,
|
|
306
|
+
punch = now == "selected_items" and nil or (now == "time_selection" and time_selection(api) or nil),
|
|
307
|
+
recordPath = record_path(api),
|
|
308
|
+
warnings = (now == "time_selection" and not time_selection(api))
|
|
309
|
+
and { "time_selection mode with no time selection records nothing until one is set" } or nil,
|
|
310
|
+
}
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
-- =============================================================================
|
|
314
|
+
-- Fixed lanes
|
|
315
|
+
-- =============================================================================
|
|
316
|
+
|
|
317
|
+
local function fixed(api, tr) return int(api.GetMediaTrackInfo_Value(tr, "I_FREEMODE")) == 2 end
|
|
318
|
+
local function lane_count(api, tr) return math.max(1, int(api.GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES"))) end
|
|
319
|
+
local function lane_name(api, tr, n)
|
|
320
|
+
local _, name = api.GetSetMediaTrackInfo_String(tr, "P_LANENAME:" .. n, "", false)
|
|
321
|
+
return name or ""
|
|
322
|
+
end
|
|
323
|
+
local PLAYS = { [0] = "off", [1] = "exclusive", [2] = "layered" }
|
|
324
|
+
|
|
325
|
+
local function item_info(api, it, k)
|
|
326
|
+
local take = api.GetActiveTake(it)
|
|
327
|
+
local name = take and api.GetTakeName(take) or ""
|
|
328
|
+
return {
|
|
329
|
+
index = k,
|
|
330
|
+
position = round3(api.GetMediaItemInfo_Value(it, "D_POSITION")),
|
|
331
|
+
length = round3(api.GetMediaItemInfo_Value(it, "D_LENGTH")),
|
|
332
|
+
name = name,
|
|
333
|
+
midi = take ~= nil and api.TakeIsMIDI(take) or false,
|
|
334
|
+
}
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
--- The track's lanes: name, play state and items (capped per lane).
|
|
338
|
+
function M.lanes_state(api, tr)
|
|
339
|
+
local on = fixed(api, tr)
|
|
340
|
+
local count = on and lane_count(api, tr) or 1
|
|
341
|
+
local lanes = {}
|
|
342
|
+
for n = 0, count - 1 do
|
|
343
|
+
local plays = on and int(api.GetMediaTrackInfo_Value(tr, "C_LANEPLAYS:" .. n)) or 1
|
|
344
|
+
lanes[n + 1] = { lane = n, name = on and lane_name(api, tr, n) or "", plays = PLAYS[plays] or plays, items = {}, itemsMore = 0 }
|
|
345
|
+
end
|
|
346
|
+
for k = 0, api.CountTrackMediaItems(tr) - 1 do
|
|
347
|
+
local it = api.GetTrackMediaItem(tr, k)
|
|
348
|
+
local n = on and int(api.GetMediaItemInfo_Value(it, "I_FIXEDLANE")) or 0
|
|
349
|
+
local lane = lanes[n + 1]
|
|
350
|
+
if lane then
|
|
351
|
+
if #lane.items < MAX_LANE_ITEMS then lane.items[#lane.items + 1] = item_info(api, it, k)
|
|
352
|
+
else lane.itemsMore = lane.itemsMore + 1 end
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
return { index = track_index(api, tr), name = track_name(api, tr), fixedLanes = on, laneCount = count, lanes = lanes }
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
function M.get_track_lanes(api, params, resolve_track)
|
|
359
|
+
local tr, err = resolve(api, params.track, resolve_track)
|
|
360
|
+
if not tr then return nil, err end
|
|
361
|
+
return M.lanes_state(api, tr)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
local function highest_used_lane(api, tr)
|
|
365
|
+
local top = 0
|
|
366
|
+
for k = 0, api.CountTrackMediaItems(tr) - 1 do
|
|
367
|
+
top = math.max(top, int(api.GetMediaItemInfo_Value(api.GetTrackMediaItem(tr, k), "I_FIXEDLANE")))
|
|
368
|
+
end
|
|
369
|
+
return top
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
local function lanes_with_items(api, tr)
|
|
373
|
+
local used = {}
|
|
374
|
+
for k = 0, api.CountTrackMediaItems(tr) - 1 do
|
|
375
|
+
used[int(api.GetMediaItemInfo_Value(api.GetTrackMediaItem(tr, k), "I_FIXEDLANE"))] = true
|
|
376
|
+
end
|
|
377
|
+
local n = 0
|
|
378
|
+
for _ in pairs(used) do n = n + 1 end
|
|
379
|
+
return n
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
--- set_track_lanes: fixed lanes on/off, lane count, names and which lanes play.
|
|
383
|
+
--- `play` is a lane number (plays alone) or an array of lanes (layered).
|
|
384
|
+
function M.set_track_lanes(api, params, resolve_track)
|
|
385
|
+
local tr, err = resolve(api, params.track, resolve_track)
|
|
386
|
+
if not tr then return nil, err end
|
|
387
|
+
local was_on = fixed(api, tr)
|
|
388
|
+
local on = params.fixedLanes == nil and was_on or params.fixedLanes == true
|
|
389
|
+
local warnings = {}
|
|
390
|
+
local count = on and (was_on and lane_count(api, tr) or 1) or 1
|
|
391
|
+
|
|
392
|
+
local want_count = params.laneCount ~= nil and tonumber(params.laneCount) or nil
|
|
393
|
+
if params.laneCount ~= nil then
|
|
394
|
+
if not want_count or want_count < 1 or want_count > MAX_LANES or want_count ~= math.floor(want_count) then
|
|
395
|
+
return nil, "laneCount must be 1-" .. MAX_LANES .. " — nothing changed"
|
|
396
|
+
end
|
|
397
|
+
if not on then return nil, "laneCount needs fixed lanes (set fixedLanes: true) — nothing changed" end
|
|
398
|
+
local top = was_on and highest_used_lane(api, tr) or 0
|
|
399
|
+
if want_count <= top then
|
|
400
|
+
return nil, "lane " .. top .. " still has items: laneCount must be at least " .. (top + 1) .. " — nothing changed"
|
|
401
|
+
end
|
|
402
|
+
count = int(want_count)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
local names = params.names
|
|
406
|
+
if names ~= nil then
|
|
407
|
+
if type(names) ~= "table" then return nil, "names must be an array of { lane, name } — nothing changed" end
|
|
408
|
+
if not on then return nil, "lane names need fixed lanes (set fixedLanes: true) — nothing changed" end
|
|
409
|
+
for k, n in ipairs(names) do
|
|
410
|
+
local lane = type(n) == "table" and tonumber(n.lane)
|
|
411
|
+
if not lane or lane < 0 or lane >= count or type(n.name) ~= "string" then
|
|
412
|
+
return nil, "names[" .. (k - 1) .. "]: lane must be 0-" .. (count - 1) .. " with a string name — nothing changed"
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
local play
|
|
418
|
+
if params.play ~= nil then
|
|
419
|
+
if not on then return nil, "play needs fixed lanes (set fixedLanes: true) — nothing changed" end
|
|
420
|
+
local list = type(params.play) == "table" and params.play or { params.play }
|
|
421
|
+
if #list == 0 then return nil, "play must be a lane number or a non-empty array of lanes — nothing changed" end
|
|
422
|
+
play = {}
|
|
423
|
+
for _, n in ipairs(list) do
|
|
424
|
+
local lane = tonumber(n)
|
|
425
|
+
if not lane or lane < 0 or lane >= count or lane ~= math.floor(lane) then
|
|
426
|
+
return nil, "play: lane " .. tostring(n) .. " does not exist (lanes 0-" .. (count - 1) .. ") — nothing changed"
|
|
427
|
+
end
|
|
428
|
+
play[int(lane)] = true
|
|
429
|
+
end
|
|
430
|
+
play.layered = type(params.play) == "table"
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
if params.fixedLanes == nil and params.laneCount == nil and names == nil and play == nil then
|
|
434
|
+
return nil, "nothing to set (fixedLanes, laneCount, names or play)"
|
|
435
|
+
end
|
|
436
|
+
if was_on and not on and lanes_with_items(api, tr) > 1 then
|
|
437
|
+
warnings[#warnings + 1] = "fixed lanes off: the items of every lane now sit on one lane and all play — undo_project brings the lanes back"
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
api.Undo_BeginBlock()
|
|
441
|
+
if on ~= was_on then api.SetMediaTrackInfo_Value(tr, "I_FREEMODE", on and 2 or 0) end
|
|
442
|
+
if on and want_count then api.SetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES", count) end
|
|
443
|
+
for _, n in ipairs(names or {}) do api.GetSetMediaTrackInfo_String(tr, "P_LANENAME:" .. int(n.lane), n.name, true) end
|
|
444
|
+
if play then
|
|
445
|
+
for n = 0, count - 1 do
|
|
446
|
+
api.SetMediaTrackInfo_Value(tr, "C_LANEPLAYS:" .. n, play[n] and (play.layered and 2 or 1) or 0)
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
api.Undo_EndBlock("MCP: Set track lanes", -1)
|
|
450
|
+
if api.UpdateArrange then api.UpdateArrange() end
|
|
451
|
+
|
|
452
|
+
local out = M.lanes_state(api, tr)
|
|
453
|
+
out.warnings = #warnings > 0 and warnings or nil
|
|
454
|
+
return out
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
-- =============================================================================
|
|
458
|
+
-- Comping
|
|
459
|
+
-- =============================================================================
|
|
460
|
+
|
|
461
|
+
-- A copy of `src` (via its state chunk, GUIDs stripped so REAPER makes new
|
|
462
|
+
-- ones) trimmed to [from, to] on `lane`.
|
|
463
|
+
local function copy_segment(api, tr, src, from, to, lane)
|
|
464
|
+
local ok, chunk = api.GetItemStateChunk(src, "", false)
|
|
465
|
+
if not ok then error("could not read an item on the source lane", 0) end
|
|
466
|
+
local lines = {}
|
|
467
|
+
for line in (chunk .. "\n"):gmatch("(.-)\r?\n") do
|
|
468
|
+
if not (line:match("^%s*IGUID ") or line:match("^%s*GUID ") or line:match("^%s*POOLEDEVTS ")) then
|
|
469
|
+
lines[#lines + 1] = line
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
local it = api.AddMediaItemToTrack(tr)
|
|
473
|
+
api.SetItemStateChunk(it, table.concat(lines, "\n"), false)
|
|
474
|
+
local pos = api.GetMediaItemInfo_Value(src, "D_POSITION")
|
|
475
|
+
for t = 0, api.CountTakes(it) - 1 do
|
|
476
|
+
local take = api.GetTake(it, t)
|
|
477
|
+
local rate = api.GetMediaItemTakeInfo_Value(take, "D_PLAYRATE")
|
|
478
|
+
local offs = api.GetMediaItemTakeInfo_Value(take, "D_STARTOFFS")
|
|
479
|
+
api.SetMediaItemTakeInfo_Value(take, "D_STARTOFFS", offs + (from - pos) * (rate > 0 and rate or 1))
|
|
480
|
+
end
|
|
481
|
+
api.SetMediaItemInfo_Value(it, "D_POSITION", from)
|
|
482
|
+
api.SetMediaItemInfo_Value(it, "D_LENGTH", to - from)
|
|
483
|
+
api.SetMediaItemInfo_Value(it, "I_FIXEDLANE", lane)
|
|
484
|
+
api.SetMediaItemInfo_Value(it, "B_UISEL", 0)
|
|
485
|
+
return it
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
--- comp_lanes: build a comp on a new lane from time ranges of the source
|
|
489
|
+
--- lanes, crossfaded at each switch, and play it alone.
|
|
490
|
+
function M.comp_lanes(api, params, resolve_track)
|
|
491
|
+
local tr, err = resolve(api, params.track, resolve_track)
|
|
492
|
+
if not tr then return nil, err end
|
|
493
|
+
if not fixed(api, tr) then return nil, "the track has no fixed lanes (set_track_lanes fixedLanes: true, or record in lanes first)" end
|
|
494
|
+
local count = lane_count(api, tr)
|
|
495
|
+
if count >= MAX_LANES then return nil, "the track already has " .. count .. " lanes (max " .. MAX_LANES .. ")" end
|
|
496
|
+
local ranges = params.ranges
|
|
497
|
+
if type(ranges) ~= "table" or #ranges == 0 then return nil, "ranges must be a non-empty array of { start, end, lane }" end
|
|
498
|
+
if #ranges > MAX_RANGES then return nil, "at most " .. MAX_RANGES .. " ranges per comp" end
|
|
499
|
+
local xf = tonumber(params.crossfadeMs or 10)
|
|
500
|
+
if not xf or xf < 0 or xf > 500 then return nil, "crossfadeMs must be 0-500" end
|
|
501
|
+
xf = xf / 1000
|
|
502
|
+
|
|
503
|
+
-- Items per lane, sorted by position.
|
|
504
|
+
local by_lane = {}
|
|
505
|
+
for k = 0, api.CountTrackMediaItems(tr) - 1 do
|
|
506
|
+
local it = api.GetTrackMediaItem(tr, k)
|
|
507
|
+
local n = int(api.GetMediaItemInfo_Value(it, "I_FIXEDLANE"))
|
|
508
|
+
local pos = api.GetMediaItemInfo_Value(it, "D_POSITION")
|
|
509
|
+
by_lane[n] = by_lane[n] or {}
|
|
510
|
+
table.insert(by_lane[n], { item = it, s = pos, e = pos + api.GetMediaItemInfo_Value(it, "D_LENGTH") })
|
|
511
|
+
end
|
|
512
|
+
for _, list in pairs(by_lane) do table.sort(list, function(a, b) return a.s < b.s end) end
|
|
513
|
+
|
|
514
|
+
local rs = {}
|
|
515
|
+
for k, r in ipairs(ranges) do
|
|
516
|
+
local what = "ranges[" .. (k - 1) .. "]"
|
|
517
|
+
local s, e, lane = type(r) == "table" and tonumber(r.start), type(r) == "table" and tonumber(r["end"]), type(r) == "table" and tonumber(r.lane)
|
|
518
|
+
if not s or not e or not lane then return nil, what .. " needs start, end (seconds) and lane — nothing changed" end
|
|
519
|
+
if s < 0 or e <= s then return nil, what .. ": end must be after start — nothing changed" end
|
|
520
|
+
if lane < 0 or lane >= count or lane ~= math.floor(lane) then
|
|
521
|
+
return nil, what .. ": lane " .. lane .. " does not exist (lanes 0-" .. (count - 1) .. ") — nothing changed"
|
|
522
|
+
end
|
|
523
|
+
rs[#rs + 1] = { s = s, e = e, lane = int(lane), k = k - 1 }
|
|
524
|
+
end
|
|
525
|
+
table.sort(rs, function(a, b) return a.s < b.s end)
|
|
526
|
+
for i = 2, #rs do
|
|
527
|
+
if rs[i].s < rs[i - 1].e - 1e-9 then
|
|
528
|
+
return nil, "ranges[" .. rs[i - 1].k .. "] and ranges[" .. rs[i].k .. "] overlap — nothing changed"
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
-- Each range must be covered by audio on its lane, with no gap.
|
|
533
|
+
local segments = {}
|
|
534
|
+
for _, r in ipairs(rs) do
|
|
535
|
+
local at = r.s
|
|
536
|
+
for _, src in ipairs(by_lane[r.lane] or {}) do
|
|
537
|
+
if src.e > at + 1e-9 and src.s < r.e - 1e-9 then
|
|
538
|
+
if src.s > at + 1e-6 then break end
|
|
539
|
+
local to = math.min(src.e, r.e)
|
|
540
|
+
segments[#segments + 1] = { src = src, s = at, e = to, lane = r.lane }
|
|
541
|
+
at = to
|
|
542
|
+
if at >= r.e - 1e-9 then break end
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
if at < r.e - 1e-6 then
|
|
546
|
+
local gap_end = r.e
|
|
547
|
+
for _, src in ipairs(by_lane[r.lane] or {}) do
|
|
548
|
+
if src.s > at + 1e-6 then gap_end = math.min(gap_end, src.s); break end
|
|
549
|
+
end
|
|
550
|
+
return nil, "lane " .. r.lane .. " has no audio from " .. secs(at) .. " s to " .. secs(gap_end) .. " s (ranges[" .. r.k .. "]) — nothing changed"
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
local comp_lane = count
|
|
555
|
+
local label = "MCP: Comp lanes"
|
|
556
|
+
api.Undo_BeginBlock()
|
|
557
|
+
api.PreventUIRefresh(1)
|
|
558
|
+
local ok, res = pcall(function()
|
|
559
|
+
api.SetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES", count + 1)
|
|
560
|
+
api.GetSetMediaTrackInfo_String(tr, "P_LANENAME:" .. comp_lane, type(params.name) == "string" and params.name ~= "" and params.name or "Comp", true)
|
|
561
|
+
local made = {}
|
|
562
|
+
for i, seg in ipairs(segments) do
|
|
563
|
+
local nxt, prev = segments[i + 1], segments[i - 1]
|
|
564
|
+
local joins_next = nxt and math.abs(nxt.s - seg.e) < 1e-6
|
|
565
|
+
local joins_prev = prev and math.abs(prev.e - seg.s) < 1e-6
|
|
566
|
+
-- Overlap the next segment by the crossfade, as far as this source item reaches and
|
|
567
|
+
-- never past half the next segment, so a short part is still heard.
|
|
568
|
+
local tail = joins_next and math.min(xf, seg.src.e - seg.e, (nxt.e - nxt.s) / 2) or 0
|
|
569
|
+
local it = copy_segment(api, tr, seg.src.item, seg.s, seg.e + tail, comp_lane)
|
|
570
|
+
local keep_in = math.abs(seg.s - seg.src.s) < 1e-6 and api.GetMediaItemInfo_Value(seg.src.item, "D_FADEINLEN") or 0
|
|
571
|
+
local keep_out = math.abs(seg.e - seg.src.e) < 1e-6 and api.GetMediaItemInfo_Value(seg.src.item, "D_FADEOUTLEN") or 0
|
|
572
|
+
local fade_in = joins_prev and (made[i - 1] and made[i - 1].tail or 0) or keep_in
|
|
573
|
+
api.SetMediaItemInfo_Value(it, "D_FADEINLEN", fade_in)
|
|
574
|
+
api.SetMediaItemInfo_Value(it, "D_FADEOUTLEN", joins_next and tail or keep_out)
|
|
575
|
+
made[i] = { item = it, tail = tail, s = round3(seg.s), e = round3(seg.e), lane = seg.lane, fadeMs = round3(tail * 1000) }
|
|
576
|
+
end
|
|
577
|
+
for n = 0, count do api.SetMediaTrackInfo_Value(tr, "C_LANEPLAYS:" .. n, n == comp_lane and 1 or 0) end
|
|
578
|
+
return made
|
|
579
|
+
end)
|
|
580
|
+
api.PreventUIRefresh(-1)
|
|
581
|
+
api.Undo_EndBlock(label, -1)
|
|
582
|
+
if not ok then
|
|
583
|
+
if api.Undo_CanUndo2(0) == label then api.Undo_DoUndo2(0) end
|
|
584
|
+
return nil, "comp failed and was undone: " .. tostring(res)
|
|
585
|
+
end
|
|
586
|
+
if api.UpdateArrange then api.UpdateArrange() end
|
|
587
|
+
|
|
588
|
+
local out = M.lanes_state(api, tr)
|
|
589
|
+
out.compLane = comp_lane
|
|
590
|
+
out.segments = {}
|
|
591
|
+
for i, m in ipairs(res) do out.segments[i] = { start = m.s, ["end"] = m.e, lane = m.lane, crossfadeMs = m.fadeMs } end
|
|
592
|
+
return out
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
return M
|