@mthines/reaper-mcp 0.6.0 → 0.8.0
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 +209 -46
- package/claude-agents/gain-stage.md +25 -8
- package/claude-agents/master.md +3 -0
- package/claude-agents/mix-analyzer.md +17 -8
- package/claude-agents/mix-engineer.md +56 -16
- package/knowledge/CLAUDE.md +71 -0
- package/knowledge/reference/common-mistakes.md +29 -1
- package/knowledge/reference/frequencies.md +15 -13
- package/knowledge/reference/metering.md +19 -0
- package/knowledge/reference/perceived-loudness.md +122 -0
- package/knowledge/workflows/gain-staging.md +20 -6
- package/knowledge/workflows/learn-plugin.md +143 -0
- package/main.js +877 -132
- package/package.json +6 -1
- package/reaper/CLAUDE.md +60 -0
- package/reaper/install.sh +8 -4
- package/reaper/mcp_bridge.lua +873 -48
package/reaper/mcp_bridge.lua
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
local POLL_INTERVAL = 0.030 -- 30ms between polls
|
|
13
13
|
local HEARTBEAT_INTERVAL = 1.0 -- write heartbeat every 1s
|
|
14
|
-
local MCP_ANALYZER_FX_NAME = "mcp_analyzer" -- JSFX analyzer name
|
|
14
|
+
local MCP_ANALYZER_FX_NAME = "reaper-mcp/mcp_analyzer" -- JSFX analyzer name
|
|
15
15
|
|
|
16
16
|
-- Determine bridge directory (REAPER resource path / Scripts / mcp_bridge_data)
|
|
17
17
|
local bridge_dir = reaper.GetResourcePath() .. "/Scripts/mcp_bridge_data/"
|
|
@@ -26,57 +26,140 @@ local last_heartbeat = 0
|
|
|
26
26
|
-- JSON Parser (minimal, sufficient for our command format)
|
|
27
27
|
-- =============================================================================
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
-- Helper: extract a JSON string value starting at pos (after the opening quote).
|
|
30
|
+
-- Handles escaped quotes (\") and other escape sequences.
|
|
31
|
+
local function extract_json_string(str, pos)
|
|
32
|
+
local result = {}
|
|
33
|
+
local i = pos
|
|
34
|
+
while i <= #str do
|
|
35
|
+
local ch = str:sub(i, i)
|
|
36
|
+
if ch == '\\' then
|
|
37
|
+
local next_ch = str:sub(i + 1, i + 1)
|
|
38
|
+
if next_ch == '"' then
|
|
39
|
+
result[#result + 1] = '"'
|
|
40
|
+
elseif next_ch == '\\' then
|
|
41
|
+
result[#result + 1] = '\\'
|
|
42
|
+
elseif next_ch == 'n' then
|
|
43
|
+
result[#result + 1] = '\n'
|
|
44
|
+
elseif next_ch == 't' then
|
|
45
|
+
result[#result + 1] = '\t'
|
|
46
|
+
elseif next_ch == '/' then
|
|
47
|
+
result[#result + 1] = '/'
|
|
48
|
+
else
|
|
49
|
+
result[#result + 1] = next_ch
|
|
50
|
+
end
|
|
51
|
+
i = i + 2
|
|
52
|
+
elseif ch == '"' then
|
|
53
|
+
return table.concat(result), i + 1 -- return string and position after closing quote
|
|
54
|
+
else
|
|
55
|
+
result[#result + 1] = ch
|
|
56
|
+
i = i + 1
|
|
57
|
+
end
|
|
34
58
|
end
|
|
59
|
+
return nil, i -- unterminated string
|
|
60
|
+
end
|
|
35
61
|
|
|
36
|
-
|
|
37
|
-
|
|
62
|
+
-- Parse a flat JSON object with string/number values in params.
|
|
63
|
+
-- Handles escaped quotes in string values (e.g. JSON arrays passed as strings).
|
|
64
|
+
local function parse_flat_object(str)
|
|
38
65
|
local obj = {}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if not
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
66
|
+
local i = 1
|
|
67
|
+
while i <= #str do
|
|
68
|
+
-- Find a key
|
|
69
|
+
local key_start = str:find('"', i)
|
|
70
|
+
if not key_start then break end
|
|
71
|
+
local key_end = str:find('"', key_start + 1)
|
|
72
|
+
if not key_end then break end
|
|
73
|
+
local key = str:sub(key_start + 1, key_end - 1)
|
|
74
|
+
|
|
75
|
+
-- Find the colon
|
|
76
|
+
local colon = str:find(':', key_end + 1)
|
|
77
|
+
if not colon then break end
|
|
78
|
+
|
|
79
|
+
-- Skip whitespace after colon
|
|
80
|
+
local val_start = str:match('^%s*()', colon + 1)
|
|
81
|
+
|
|
82
|
+
local ch = str:sub(val_start, val_start)
|
|
83
|
+
if ch == '"' then
|
|
84
|
+
-- String value (handles escaped quotes)
|
|
85
|
+
local val, next_pos = extract_json_string(str, val_start + 1)
|
|
86
|
+
if val then
|
|
87
|
+
obj[key] = val
|
|
88
|
+
i = next_pos
|
|
89
|
+
else
|
|
90
|
+
i = val_start + 1
|
|
91
|
+
end
|
|
92
|
+
elseif ch == '{' then
|
|
93
|
+
-- Nested object — extract with balanced braces
|
|
94
|
+
local nested = str:match('%b{}', val_start)
|
|
95
|
+
if nested then
|
|
96
|
+
obj[key] = parse_flat_object(nested)
|
|
97
|
+
i = val_start + #nested
|
|
98
|
+
else
|
|
99
|
+
i = val_start + 1
|
|
100
|
+
end
|
|
101
|
+
elseif ch == '[' then
|
|
102
|
+
-- Array — extract with balanced brackets
|
|
103
|
+
local arr = str:match('%b[]', val_start)
|
|
104
|
+
if arr then
|
|
105
|
+
obj[key] = arr -- keep as string for json_decode_array to parse later
|
|
106
|
+
i = val_start + #arr
|
|
107
|
+
else
|
|
108
|
+
i = val_start + 1
|
|
109
|
+
end
|
|
110
|
+
elseif ch:match('[%d%-]') then
|
|
111
|
+
-- Number
|
|
112
|
+
local num_str = str:match('-?%d+%.?%d*', val_start)
|
|
113
|
+
if num_str then
|
|
114
|
+
obj[key] = tonumber(num_str)
|
|
115
|
+
i = val_start + #num_str
|
|
116
|
+
else
|
|
117
|
+
i = val_start + 1
|
|
118
|
+
end
|
|
119
|
+
elseif str:sub(val_start, val_start + 3) == 'true' then
|
|
120
|
+
obj[key] = true
|
|
121
|
+
i = val_start + 4
|
|
122
|
+
elseif str:sub(val_start, val_start + 4) == 'false' then
|
|
123
|
+
obj[key] = false
|
|
124
|
+
i = val_start + 5
|
|
125
|
+
elseif str:sub(val_start, val_start + 3) == 'null' then
|
|
126
|
+
i = val_start + 4
|
|
127
|
+
else
|
|
128
|
+
i = val_start + 1
|
|
56
129
|
end
|
|
57
130
|
end
|
|
58
131
|
return obj
|
|
59
132
|
end
|
|
60
133
|
|
|
61
|
-
|
|
62
|
-
--
|
|
63
|
-
local function json_decode_array(str)
|
|
134
|
+
local function json_decode(str)
|
|
135
|
+
-- Use REAPER 7+ built-in JSON if available, otherwise basic parser
|
|
64
136
|
if reaper.CF_Json_Parse then
|
|
65
137
|
local ok, val = reaper.CF_Json_Parse(str)
|
|
66
138
|
if ok then return val end
|
|
67
139
|
end
|
|
68
140
|
|
|
141
|
+
-- Fallback: parse the command JSON using our robust parser
|
|
142
|
+
-- that handles escaped quotes in string values (needed for batch commands)
|
|
143
|
+
return parse_flat_object(str)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
-- Parse a JSON array of objects. Accepts either a Lua table (already parsed by
|
|
147
|
+
-- CF_Json_Parse) or a JSON string. Falls back to extracting each {...} and
|
|
148
|
+
-- parsing with parse_flat_object.
|
|
149
|
+
local function json_decode_array(input)
|
|
150
|
+
-- If already a table (e.g. CF_Json_Parse decoded it), return directly
|
|
151
|
+
if type(input) == "table" then return input end
|
|
152
|
+
if type(input) ~= "string" then return nil end
|
|
153
|
+
|
|
154
|
+
if reaper.CF_Json_Parse then
|
|
155
|
+
local ok, val = reaper.CF_Json_Parse(input)
|
|
156
|
+
if ok then return val end
|
|
157
|
+
end
|
|
158
|
+
|
|
69
159
|
-- Fallback: extract each {...} from the array and parse individually
|
|
70
160
|
local arr = {}
|
|
71
|
-
for obj_str in
|
|
72
|
-
|
|
73
|
-
for k, v in obj_str:gmatch('"([^"]+)"%s*:%s*"([^"]*)"') do
|
|
74
|
-
obj[k] = v
|
|
75
|
-
end
|
|
76
|
-
for k, v in obj_str:gmatch('"([^"]+)"%s*:%s*(-?%d+%.?%d*)') do
|
|
77
|
-
if not obj[k] then obj[k] = tonumber(v) end
|
|
78
|
-
end
|
|
79
|
-
arr[#arr + 1] = obj
|
|
161
|
+
for obj_str in input:gmatch("%b{}") do
|
|
162
|
+
arr[#arr + 1] = parse_flat_object(obj_str)
|
|
80
163
|
end
|
|
81
164
|
if #arr > 0 then return arr end
|
|
82
165
|
return nil
|
|
@@ -924,9 +1007,9 @@ end
|
|
|
924
1007
|
-- Phase 4: Custom JSFX analyzer handlers
|
|
925
1008
|
-- =============================================================================
|
|
926
1009
|
|
|
927
|
-
local MCP_LUFS_METER_FX_NAME = "mcp_lufs_meter"
|
|
928
|
-
local MCP_CORRELATION_METER_FX_NAME = "mcp_correlation_meter"
|
|
929
|
-
local MCP_CREST_FACTOR_FX_NAME = "mcp_crest_factor"
|
|
1010
|
+
local MCP_LUFS_METER_FX_NAME = "reaper-mcp/mcp_lufs_meter"
|
|
1011
|
+
local MCP_CORRELATION_METER_FX_NAME = "reaper-mcp/mcp_correlation_meter"
|
|
1012
|
+
local MCP_CREST_FACTOR_FX_NAME = "reaper-mcp/mcp_crest_factor"
|
|
930
1013
|
|
|
931
1014
|
-- Helper: find or auto-insert a named JSFX on a track.
|
|
932
1015
|
-- Returns the FX index (0-based) on success, or nil + error message on failure.
|
|
@@ -1176,9 +1259,17 @@ function handlers.get_midi_notes(params)
|
|
|
1176
1259
|
|
|
1177
1260
|
local _, note_cnt, _, _ = reaper.MIDI_CountEvts(take)
|
|
1178
1261
|
local to_beats = ppq_to_beats(take, item)
|
|
1179
|
-
local notes = {}
|
|
1180
1262
|
|
|
1181
|
-
|
|
1263
|
+
local offset = params.offset or 0
|
|
1264
|
+
local limit = params.limit
|
|
1265
|
+
local start_idx = math.min(offset, note_cnt)
|
|
1266
|
+
local end_idx = note_cnt - 1
|
|
1267
|
+
if limit then
|
|
1268
|
+
end_idx = math.min(start_idx + limit - 1, note_cnt - 1)
|
|
1269
|
+
end
|
|
1270
|
+
|
|
1271
|
+
local notes = {}
|
|
1272
|
+
for i = start_idx, end_idx do
|
|
1182
1273
|
local _, sel, muted, start_ppq, end_ppq, chan, pitch, vel = reaper.MIDI_GetNote(take, i)
|
|
1183
1274
|
local start_beats = to_beats(start_ppq)
|
|
1184
1275
|
local end_beats = to_beats(end_ppq)
|
|
@@ -1194,7 +1285,114 @@ function handlers.get_midi_notes(params)
|
|
|
1194
1285
|
}
|
|
1195
1286
|
end
|
|
1196
1287
|
|
|
1197
|
-
return {
|
|
1288
|
+
return {
|
|
1289
|
+
trackIndex = params.trackIndex,
|
|
1290
|
+
itemIndex = params.itemIndex,
|
|
1291
|
+
notes = notes,
|
|
1292
|
+
returned = #notes,
|
|
1293
|
+
total = note_cnt,
|
|
1294
|
+
offset = start_idx,
|
|
1295
|
+
hasMore = end_idx < note_cnt - 1,
|
|
1296
|
+
}
|
|
1297
|
+
end
|
|
1298
|
+
|
|
1299
|
+
function handlers.analyze_midi(params)
|
|
1300
|
+
local track, item, take, err = get_midi_take(params)
|
|
1301
|
+
if err then return nil, err end
|
|
1302
|
+
|
|
1303
|
+
local _, note_cnt, cc_cnt, _ = reaper.MIDI_CountEvts(take)
|
|
1304
|
+
local to_beats = ppq_to_beats(take, item)
|
|
1305
|
+
|
|
1306
|
+
-- Per-pitch accumulators
|
|
1307
|
+
local pitch_data = {} -- pitch -> { count, sum_vel, sum_sq, min_vel, max_vel, last_vel, consec, max_consec, sequences }
|
|
1308
|
+
-- Velocity histogram: 13 buckets (0-9, 10-19, ..., 120-127)
|
|
1309
|
+
local vel_histogram = {}
|
|
1310
|
+
for i = 1, 13 do vel_histogram[i] = 0 end
|
|
1311
|
+
|
|
1312
|
+
local max_end_beat = 0
|
|
1313
|
+
|
|
1314
|
+
for i = 0, note_cnt - 1 do
|
|
1315
|
+
local _, _, _, start_ppq, end_ppq, _, pitch, vel = reaper.MIDI_GetNote(take, i)
|
|
1316
|
+
|
|
1317
|
+
-- Track duration
|
|
1318
|
+
local end_beat = to_beats(end_ppq)
|
|
1319
|
+
if end_beat > max_end_beat then max_end_beat = end_beat end
|
|
1320
|
+
|
|
1321
|
+
-- Velocity histogram bucket
|
|
1322
|
+
local bucket = math.min(math.floor(vel / 10) + 1, 13)
|
|
1323
|
+
vel_histogram[bucket] = vel_histogram[bucket] + 1
|
|
1324
|
+
|
|
1325
|
+
-- Per-pitch stats
|
|
1326
|
+
if not pitch_data[pitch] then
|
|
1327
|
+
pitch_data[pitch] = {
|
|
1328
|
+
count = 0, sum_vel = 0, sum_sq = 0,
|
|
1329
|
+
min_vel = 127, max_vel = 0,
|
|
1330
|
+
last_vel = -1, consec = 1, max_consec = 1, sequences = 0,
|
|
1331
|
+
}
|
|
1332
|
+
end
|
|
1333
|
+
local pd = pitch_data[pitch]
|
|
1334
|
+
pd.count = pd.count + 1
|
|
1335
|
+
pd.sum_vel = pd.sum_vel + vel
|
|
1336
|
+
pd.sum_sq = pd.sum_sq + vel * vel
|
|
1337
|
+
if vel < pd.min_vel then pd.min_vel = vel end
|
|
1338
|
+
if vel > pd.max_vel then pd.max_vel = vel end
|
|
1339
|
+
|
|
1340
|
+
-- Machine gun detection: consecutive identical velocities
|
|
1341
|
+
if vel == pd.last_vel then
|
|
1342
|
+
pd.consec = pd.consec + 1
|
|
1343
|
+
if pd.consec > pd.max_consec then pd.max_consec = pd.consec end
|
|
1344
|
+
else
|
|
1345
|
+
if pd.consec >= 3 then pd.sequences = pd.sequences + 1 end
|
|
1346
|
+
pd.consec = 1
|
|
1347
|
+
end
|
|
1348
|
+
pd.last_vel = vel
|
|
1349
|
+
end
|
|
1350
|
+
|
|
1351
|
+
-- Build pitch stats array
|
|
1352
|
+
local pitch_stats = {}
|
|
1353
|
+
local machine_gun = {}
|
|
1354
|
+
local sorted_pitches = {}
|
|
1355
|
+
for p, _ in pairs(pitch_data) do sorted_pitches[#sorted_pitches + 1] = p end
|
|
1356
|
+
table.sort(sorted_pitches)
|
|
1357
|
+
|
|
1358
|
+
for _, pitch in ipairs(sorted_pitches) do
|
|
1359
|
+
local pd = pitch_data[pitch]
|
|
1360
|
+
-- Close final run
|
|
1361
|
+
if pd.consec >= 3 then pd.sequences = pd.sequences + 1 end
|
|
1362
|
+
|
|
1363
|
+
local avg = pd.sum_vel / pd.count
|
|
1364
|
+
local variance = (pd.sum_sq / pd.count) - (avg * avg)
|
|
1365
|
+
local std_dev = math.sqrt(math.max(0, variance))
|
|
1366
|
+
|
|
1367
|
+
pitch_stats[#pitch_stats + 1] = {
|
|
1368
|
+
pitch = pitch,
|
|
1369
|
+
count = pd.count,
|
|
1370
|
+
minVelocity = pd.min_vel,
|
|
1371
|
+
maxVelocity = pd.max_vel,
|
|
1372
|
+
avgVelocity = math.floor(avg * 10 + 0.5) / 10, -- round to 1 decimal
|
|
1373
|
+
stdDev = math.floor(std_dev * 10 + 0.5) / 10,
|
|
1374
|
+
maxConsecutiveSameVelocity = pd.max_consec,
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
if pd.max_consec >= 3 then
|
|
1378
|
+
machine_gun[#machine_gun + 1] = {
|
|
1379
|
+
pitch = pitch,
|
|
1380
|
+
maxConsecutive = pd.max_consec,
|
|
1381
|
+
sequences = pd.sequences,
|
|
1382
|
+
}
|
|
1383
|
+
end
|
|
1384
|
+
end
|
|
1385
|
+
|
|
1386
|
+
return {
|
|
1387
|
+
trackIndex = params.trackIndex,
|
|
1388
|
+
itemIndex = params.itemIndex,
|
|
1389
|
+
totalNotes = note_cnt,
|
|
1390
|
+
totalCC = cc_cnt,
|
|
1391
|
+
durationBeats = math.floor(max_end_beat * 100 + 0.5) / 100,
|
|
1392
|
+
pitchStats = pitch_stats,
|
|
1393
|
+
velocityHistogram = vel_histogram,
|
|
1394
|
+
machineGunWarnings = machine_gun,
|
|
1395
|
+
}
|
|
1198
1396
|
end
|
|
1199
1397
|
|
|
1200
1398
|
function handlers.insert_midi_note(params)
|
|
@@ -1234,13 +1432,13 @@ function handlers.insert_midi_notes(params)
|
|
|
1234
1432
|
local track, item, take, err = get_midi_take(params)
|
|
1235
1433
|
if err then return nil, err end
|
|
1236
1434
|
|
|
1237
|
-
local
|
|
1238
|
-
if not
|
|
1435
|
+
local notes_input = params.notes
|
|
1436
|
+
if not notes_input or notes_input == "" then return nil, "notes array required" end
|
|
1239
1437
|
|
|
1240
|
-
-- Parse notes
|
|
1241
|
-
local notes_list = json_decode_array(
|
|
1438
|
+
-- Parse notes array (accepts native table from CF_Json_Parse or JSON string fallback)
|
|
1439
|
+
local notes_list = json_decode_array(notes_input)
|
|
1242
1440
|
if not notes_list or #notes_list == 0 then
|
|
1243
|
-
return nil, "Failed to parse notes
|
|
1441
|
+
return nil, "Failed to parse notes array. Expected array of {pitch, velocity, startPosition, duration} objects."
|
|
1244
1442
|
end
|
|
1245
1443
|
|
|
1246
1444
|
reaper.Undo_BeginBlock()
|
|
@@ -1321,6 +1519,65 @@ function handlers.edit_midi_note(params)
|
|
|
1321
1519
|
return { success = true, noteIndex = note_idx }
|
|
1322
1520
|
end
|
|
1323
1521
|
|
|
1522
|
+
function handlers.edit_midi_notes(params)
|
|
1523
|
+
local track, item, take, err = get_midi_take(params)
|
|
1524
|
+
if err then return nil, err end
|
|
1525
|
+
|
|
1526
|
+
local edits_input = params.edits
|
|
1527
|
+
if not edits_input then return nil, "edits array required" end
|
|
1528
|
+
|
|
1529
|
+
local edits = json_decode_array(edits_input)
|
|
1530
|
+
if not edits then return nil, "Failed to parse edits array" end
|
|
1531
|
+
|
|
1532
|
+
local _, note_cnt, _, _ = reaper.MIDI_CountEvts(take)
|
|
1533
|
+
local to_beats = ppq_to_beats(take, item)
|
|
1534
|
+
local edited = 0
|
|
1535
|
+
local errors = {}
|
|
1536
|
+
|
|
1537
|
+
reaper.Undo_BeginBlock()
|
|
1538
|
+
|
|
1539
|
+
for _, edit in ipairs(edits) do
|
|
1540
|
+
local note_idx = edit.noteIndex
|
|
1541
|
+
if note_idx == nil then
|
|
1542
|
+
errors[#errors + 1] = "edit missing noteIndex"
|
|
1543
|
+
elseif note_idx >= note_cnt then
|
|
1544
|
+
errors[#errors + 1] = "noteIndex " .. note_idx .. " out of range"
|
|
1545
|
+
else
|
|
1546
|
+
local _, sel, muted, cur_start_ppq, cur_end_ppq, cur_chan, cur_pitch, cur_vel = reaper.MIDI_GetNote(take, note_idx)
|
|
1547
|
+
|
|
1548
|
+
local new_pitch = edit.pitch and math.max(0, math.min(127, math.floor(edit.pitch))) or nil
|
|
1549
|
+
local new_vel = edit.velocity and math.max(1, math.min(127, math.floor(edit.velocity))) or nil
|
|
1550
|
+
local new_chan = edit.channel and math.max(0, math.min(15, math.floor(edit.channel))) or nil
|
|
1551
|
+
|
|
1552
|
+
local new_start_ppq = nil
|
|
1553
|
+
local new_end_ppq = nil
|
|
1554
|
+
if edit.startPosition ~= nil then
|
|
1555
|
+
new_start_ppq = beats_to_ppq(take, item, edit.startPosition)
|
|
1556
|
+
if edit.duration ~= nil then
|
|
1557
|
+
new_end_ppq = beats_to_ppq(take, item, edit.startPosition + edit.duration)
|
|
1558
|
+
else
|
|
1559
|
+
local dur_ppq = cur_end_ppq - cur_start_ppq
|
|
1560
|
+
new_end_ppq = new_start_ppq + dur_ppq
|
|
1561
|
+
end
|
|
1562
|
+
elseif edit.duration ~= nil then
|
|
1563
|
+
local cur_start_beats = to_beats(cur_start_ppq)
|
|
1564
|
+
new_end_ppq = beats_to_ppq(take, item, cur_start_beats + edit.duration)
|
|
1565
|
+
end
|
|
1566
|
+
|
|
1567
|
+
reaper.MIDI_SetNote(take, note_idx, sel, muted, new_start_ppq, new_end_ppq, new_chan, new_pitch, new_vel, false)
|
|
1568
|
+
edited = edited + 1
|
|
1569
|
+
end
|
|
1570
|
+
end
|
|
1571
|
+
|
|
1572
|
+
reaper.MIDI_Sort(take)
|
|
1573
|
+
reaper.Undo_EndBlock("MCP: Batch edit " .. edited .. " MIDI notes", -1)
|
|
1574
|
+
reaper.UpdateArrange()
|
|
1575
|
+
|
|
1576
|
+
local result = { success = true, edited = edited, total = #edits }
|
|
1577
|
+
if #errors > 0 then result.errors = errors end
|
|
1578
|
+
return result
|
|
1579
|
+
end
|
|
1580
|
+
|
|
1324
1581
|
function handlers.delete_midi_note(params)
|
|
1325
1582
|
local track, item, take, err = get_midi_take(params)
|
|
1326
1583
|
if err then return nil, err end
|
|
@@ -1597,6 +1854,71 @@ function handlers.set_media_item_properties(params)
|
|
|
1597
1854
|
return { success = true, trackIndex = params.trackIndex, itemIndex = params.itemIndex }
|
|
1598
1855
|
end
|
|
1599
1856
|
|
|
1857
|
+
function handlers.set_media_items_properties(params)
|
|
1858
|
+
local items_input = params.items
|
|
1859
|
+
if not items_input then return nil, "items array required" end
|
|
1860
|
+
|
|
1861
|
+
local items = json_decode_array(items_input)
|
|
1862
|
+
if not items then return nil, "Failed to parse items array" end
|
|
1863
|
+
|
|
1864
|
+
local edited = 0
|
|
1865
|
+
local errors = {}
|
|
1866
|
+
|
|
1867
|
+
reaper.Undo_BeginBlock()
|
|
1868
|
+
|
|
1869
|
+
for _, item_params in ipairs(items) do
|
|
1870
|
+
local track_idx = item_params.trackIndex
|
|
1871
|
+
local item_idx = item_params.itemIndex
|
|
1872
|
+
if track_idx == nil or item_idx == nil then
|
|
1873
|
+
errors[#errors + 1] = "item missing trackIndex or itemIndex"
|
|
1874
|
+
else
|
|
1875
|
+
local track = reaper.GetTrack(0, track_idx)
|
|
1876
|
+
if not track then
|
|
1877
|
+
errors[#errors + 1] = "Track " .. track_idx .. " not found"
|
|
1878
|
+
else
|
|
1879
|
+
local item_count = reaper.CountTrackMediaItems(track)
|
|
1880
|
+
if item_idx >= item_count then
|
|
1881
|
+
errors[#errors + 1] = "Item " .. item_idx .. " not found on track " .. track_idx
|
|
1882
|
+
else
|
|
1883
|
+
local item = reaper.GetTrackMediaItem(track, item_idx)
|
|
1884
|
+
if item_params.position ~= nil then
|
|
1885
|
+
reaper.SetMediaItemInfo_Value(item, "D_POSITION", item_params.position)
|
|
1886
|
+
end
|
|
1887
|
+
if item_params.length ~= nil then
|
|
1888
|
+
reaper.SetMediaItemInfo_Value(item, "D_LENGTH", item_params.length)
|
|
1889
|
+
end
|
|
1890
|
+
if item_params.volume ~= nil then
|
|
1891
|
+
reaper.SetMediaItemInfo_Value(item, "D_VOL", from_db(item_params.volume))
|
|
1892
|
+
end
|
|
1893
|
+
if item_params.mute ~= nil then
|
|
1894
|
+
reaper.SetMediaItemInfo_Value(item, "B_MUTE", item_params.mute)
|
|
1895
|
+
end
|
|
1896
|
+
if item_params.fadeInLength ~= nil then
|
|
1897
|
+
reaper.SetMediaItemInfo_Value(item, "D_FADEINLEN", item_params.fadeInLength)
|
|
1898
|
+
end
|
|
1899
|
+
if item_params.fadeOutLength ~= nil then
|
|
1900
|
+
reaper.SetMediaItemInfo_Value(item, "D_FADEOUTLEN", item_params.fadeOutLength)
|
|
1901
|
+
end
|
|
1902
|
+
if item_params.playRate ~= nil then
|
|
1903
|
+
local take = reaper.GetActiveTake(item)
|
|
1904
|
+
if take then
|
|
1905
|
+
reaper.SetMediaItemTakeInfo_Value(take, "D_PLAYRATE", item_params.playRate)
|
|
1906
|
+
end
|
|
1907
|
+
end
|
|
1908
|
+
edited = edited + 1
|
|
1909
|
+
end
|
|
1910
|
+
end
|
|
1911
|
+
end
|
|
1912
|
+
end
|
|
1913
|
+
|
|
1914
|
+
reaper.Undo_EndBlock("MCP: Batch set " .. edited .. " media item properties", -1)
|
|
1915
|
+
reaper.UpdateArrange()
|
|
1916
|
+
|
|
1917
|
+
local result = { success = true, edited = edited, total = #items }
|
|
1918
|
+
if #errors > 0 then result.errors = errors end
|
|
1919
|
+
return result
|
|
1920
|
+
end
|
|
1921
|
+
|
|
1600
1922
|
function handlers.split_media_item(params)
|
|
1601
1923
|
local track, item, err = get_media_item(params)
|
|
1602
1924
|
if err then return nil, err end
|
|
@@ -1799,6 +2121,509 @@ function handlers.delete_stretch_marker(params)
|
|
|
1799
2121
|
return { success = true, totalMarkers = reaper.GetTakeNumStretchMarkers(take) }
|
|
1800
2122
|
end
|
|
1801
2123
|
|
|
2124
|
+
-- =============================================================================
|
|
2125
|
+
-- FX Enable / Offline
|
|
2126
|
+
-- =============================================================================
|
|
2127
|
+
|
|
2128
|
+
function handlers.set_fx_enabled(params)
|
|
2129
|
+
local track = reaper.GetTrack(0, params.trackIndex)
|
|
2130
|
+
if not track then return nil, "Track " .. params.trackIndex .. " not found" end
|
|
2131
|
+
local fx_count = reaper.TrackFX_GetCount(track)
|
|
2132
|
+
if params.fxIndex >= fx_count then
|
|
2133
|
+
return nil, "FX index " .. params.fxIndex .. " out of range (track has " .. fx_count .. " FX)"
|
|
2134
|
+
end
|
|
2135
|
+
reaper.TrackFX_SetEnabled(track, params.fxIndex, params.enabled == 1)
|
|
2136
|
+
return { trackIndex = params.trackIndex, fxIndex = params.fxIndex, enabled = params.enabled == 1 }
|
|
2137
|
+
end
|
|
2138
|
+
|
|
2139
|
+
function handlers.set_fx_offline(params)
|
|
2140
|
+
local track = reaper.GetTrack(0, params.trackIndex)
|
|
2141
|
+
if not track then return nil, "Track " .. params.trackIndex .. " not found" end
|
|
2142
|
+
local fx_count = reaper.TrackFX_GetCount(track)
|
|
2143
|
+
if params.fxIndex >= fx_count then
|
|
2144
|
+
return nil, "FX index " .. params.fxIndex .. " out of range (track has " .. fx_count .. " FX)"
|
|
2145
|
+
end
|
|
2146
|
+
reaper.TrackFX_SetOffline(track, params.fxIndex, params.offline == 1)
|
|
2147
|
+
return { trackIndex = params.trackIndex, fxIndex = params.fxIndex, offline = params.offline == 1 }
|
|
2148
|
+
end
|
|
2149
|
+
|
|
2150
|
+
-- =============================================================================
|
|
2151
|
+
-- Selection & Navigation
|
|
2152
|
+
-- =============================================================================
|
|
2153
|
+
|
|
2154
|
+
function handlers.get_selected_tracks(params)
|
|
2155
|
+
local count = reaper.CountSelectedTracks(0)
|
|
2156
|
+
local tracks = {}
|
|
2157
|
+
for i = 0, count - 1 do
|
|
2158
|
+
local track = reaper.GetSelectedTrack(0, i)
|
|
2159
|
+
local idx = reaper.GetMediaTrackInfo_Value(track, "IP_TRACKNUMBER") - 1
|
|
2160
|
+
local _, name = reaper.GetTrackName(track)
|
|
2161
|
+
tracks[#tracks + 1] = { index = idx, name = name }
|
|
2162
|
+
end
|
|
2163
|
+
return { tracks = tracks, count = count }
|
|
2164
|
+
end
|
|
2165
|
+
|
|
2166
|
+
function handlers.get_time_selection(params)
|
|
2167
|
+
local start_pos, end_pos = reaper.GetSet_LoopTimeRange(false, false, 0, 0, false)
|
|
2168
|
+
return {
|
|
2169
|
+
start = start_pos,
|
|
2170
|
+
["end"] = end_pos,
|
|
2171
|
+
length = end_pos - start_pos,
|
|
2172
|
+
hasSelection = end_pos > start_pos,
|
|
2173
|
+
}
|
|
2174
|
+
end
|
|
2175
|
+
|
|
2176
|
+
function handlers.set_time_selection(params)
|
|
2177
|
+
if params.start == nil then return nil, "start required" end
|
|
2178
|
+
if params["end"] == nil then return nil, "end required" end
|
|
2179
|
+
reaper.GetSet_LoopTimeRange(true, false, params.start, params["end"], false)
|
|
2180
|
+
return { start = params.start, ["end"] = params["end"] }
|
|
2181
|
+
end
|
|
2182
|
+
|
|
2183
|
+
-- =============================================================================
|
|
2184
|
+
-- Markers & Regions
|
|
2185
|
+
-- =============================================================================
|
|
2186
|
+
|
|
2187
|
+
function handlers.list_markers(params)
|
|
2188
|
+
local _, num_markers, num_regions = reaper.CountProjectMarkers(0)
|
|
2189
|
+
local total = num_markers + num_regions
|
|
2190
|
+
local markers = {}
|
|
2191
|
+
for i = 0, total - 1 do
|
|
2192
|
+
local _, isrgn, pos, rgnend, name, markrgnindexnumber, color = reaper.EnumProjectMarkers3(0, i)
|
|
2193
|
+
if not isrgn then
|
|
2194
|
+
markers[#markers + 1] = {
|
|
2195
|
+
index = markrgnindexnumber,
|
|
2196
|
+
name = name,
|
|
2197
|
+
position = pos,
|
|
2198
|
+
color = color,
|
|
2199
|
+
}
|
|
2200
|
+
end
|
|
2201
|
+
end
|
|
2202
|
+
return { markers = markers, count = #markers }
|
|
2203
|
+
end
|
|
2204
|
+
|
|
2205
|
+
function handlers.list_regions(params)
|
|
2206
|
+
local _, num_markers, num_regions = reaper.CountProjectMarkers(0)
|
|
2207
|
+
local total = num_markers + num_regions
|
|
2208
|
+
local regions = {}
|
|
2209
|
+
for i = 0, total - 1 do
|
|
2210
|
+
local _, isrgn, pos, rgnend, name, markrgnindexnumber, color = reaper.EnumProjectMarkers3(0, i)
|
|
2211
|
+
if isrgn then
|
|
2212
|
+
regions[#regions + 1] = {
|
|
2213
|
+
index = markrgnindexnumber,
|
|
2214
|
+
name = name,
|
|
2215
|
+
start = pos,
|
|
2216
|
+
["end"] = rgnend,
|
|
2217
|
+
length = rgnend - pos,
|
|
2218
|
+
color = color,
|
|
2219
|
+
}
|
|
2220
|
+
end
|
|
2221
|
+
end
|
|
2222
|
+
return { regions = regions, count = #regions }
|
|
2223
|
+
end
|
|
2224
|
+
|
|
2225
|
+
function handlers.add_marker(params)
|
|
2226
|
+
if params.position == nil then return nil, "position required" end
|
|
2227
|
+
local color = params.color or 0
|
|
2228
|
+
local name = params.name or ""
|
|
2229
|
+
local idx = reaper.AddProjectMarker2(0, false, params.position, 0, name, -1, color)
|
|
2230
|
+
if idx < 0 then return nil, "Failed to add marker" end
|
|
2231
|
+
return { index = idx, position = params.position, name = name }
|
|
2232
|
+
end
|
|
2233
|
+
|
|
2234
|
+
function handlers.add_region(params)
|
|
2235
|
+
if params.start == nil then return nil, "start required" end
|
|
2236
|
+
if params["end"] == nil then return nil, "end required" end
|
|
2237
|
+
local color = params.color or 0
|
|
2238
|
+
local name = params.name or ""
|
|
2239
|
+
local idx = reaper.AddProjectMarker2(0, true, params.start, params["end"], name, -1, color)
|
|
2240
|
+
if idx < 0 then return nil, "Failed to add region" end
|
|
2241
|
+
return { index = idx, start = params.start, ["end"] = params["end"], name = name }
|
|
2242
|
+
end
|
|
2243
|
+
|
|
2244
|
+
function handlers.delete_marker(params)
|
|
2245
|
+
if params.markerIndex == nil then return nil, "markerIndex required" end
|
|
2246
|
+
-- DeleteProjectMarker takes (proj, isrgn, markrgnindexnumber)
|
|
2247
|
+
local deleted = reaper.DeleteProjectMarker(0, false, params.markerIndex, false)
|
|
2248
|
+
if not deleted then return nil, "Marker " .. params.markerIndex .. " not found" end
|
|
2249
|
+
return { success = true }
|
|
2250
|
+
end
|
|
2251
|
+
|
|
2252
|
+
function handlers.delete_region(params)
|
|
2253
|
+
if params.regionIndex == nil then return nil, "regionIndex required" end
|
|
2254
|
+
local deleted = reaper.DeleteProjectMarker(0, true, params.regionIndex, false)
|
|
2255
|
+
if not deleted then return nil, "Region " .. params.regionIndex .. " not found" end
|
|
2256
|
+
return { success = true }
|
|
2257
|
+
end
|
|
2258
|
+
|
|
2259
|
+
-- =============================================================================
|
|
2260
|
+
-- Tempo Map
|
|
2261
|
+
-- =============================================================================
|
|
2262
|
+
|
|
2263
|
+
function handlers.get_tempo_map(params)
|
|
2264
|
+
local count = reaper.CountTempoTimeSigMarkers(0)
|
|
2265
|
+
local markers = {}
|
|
2266
|
+
for i = 0, count - 1 do
|
|
2267
|
+
local _, timepos, measurepos, beatpos, bpm, timesig_num, timesig_denom, lineartempo = reaper.GetTempoTimeSigMarker(0, i)
|
|
2268
|
+
markers[#markers + 1] = {
|
|
2269
|
+
index = i,
|
|
2270
|
+
position = timepos,
|
|
2271
|
+
bpm = bpm,
|
|
2272
|
+
timeSigNumerator = timesig_num,
|
|
2273
|
+
timeSigDenominator = timesig_denom,
|
|
2274
|
+
linearTempo = lineartempo,
|
|
2275
|
+
}
|
|
2276
|
+
end
|
|
2277
|
+
-- If no markers, return the project tempo as the single entry
|
|
2278
|
+
if count == 0 then
|
|
2279
|
+
local bpm = reaper.Master_GetTempo()
|
|
2280
|
+
local _, num, denom = reaper.TimeMap_GetTimeSigAtTime(0, 0)
|
|
2281
|
+
markers[1] = {
|
|
2282
|
+
index = 0,
|
|
2283
|
+
position = 0,
|
|
2284
|
+
bpm = bpm,
|
|
2285
|
+
timeSigNumerator = num,
|
|
2286
|
+
timeSigDenominator = denom,
|
|
2287
|
+
linearTempo = false,
|
|
2288
|
+
}
|
|
2289
|
+
end
|
|
2290
|
+
return { markers = markers, count = #markers }
|
|
2291
|
+
end
|
|
2292
|
+
|
|
2293
|
+
-- =============================================================================
|
|
2294
|
+
-- Envelopes / Automation
|
|
2295
|
+
-- =============================================================================
|
|
2296
|
+
|
|
2297
|
+
function handlers.get_track_envelopes(params)
|
|
2298
|
+
local track = reaper.GetTrack(0, params.trackIndex)
|
|
2299
|
+
if not track then return nil, "Track " .. params.trackIndex .. " not found" end
|
|
2300
|
+
local count = reaper.CountTrackEnvelopes(track)
|
|
2301
|
+
local envelopes = {}
|
|
2302
|
+
for i = 0, count - 1 do
|
|
2303
|
+
local env = reaper.GetTrackEnvelope(track, i)
|
|
2304
|
+
local _, name = reaper.GetEnvelopeName(env)
|
|
2305
|
+
local point_count = reaper.CountEnvelopePoints(env)
|
|
2306
|
+
-- Get envelope state info (SWS extension provides BR_Env*, fallback if unavailable)
|
|
2307
|
+
local active, visible, armed = true, true, false
|
|
2308
|
+
if reaper.BR_EnvAlloc then
|
|
2309
|
+
local br_env = reaper.BR_EnvAlloc(env, false)
|
|
2310
|
+
active, visible, armed = reaper.BR_EnvGetProperties(br_env)
|
|
2311
|
+
reaper.BR_EnvFree(br_env, false)
|
|
2312
|
+
end
|
|
2313
|
+
envelopes[#envelopes + 1] = {
|
|
2314
|
+
index = i,
|
|
2315
|
+
name = name,
|
|
2316
|
+
pointCount = point_count,
|
|
2317
|
+
active = active,
|
|
2318
|
+
visible = visible,
|
|
2319
|
+
armed = armed,
|
|
2320
|
+
}
|
|
2321
|
+
end
|
|
2322
|
+
return { trackIndex = params.trackIndex, envelopes = envelopes, count = count }
|
|
2323
|
+
end
|
|
2324
|
+
|
|
2325
|
+
function handlers.get_envelope_points(params)
|
|
2326
|
+
local track = reaper.GetTrack(0, params.trackIndex)
|
|
2327
|
+
if not track then return nil, "Track " .. params.trackIndex .. " not found" end
|
|
2328
|
+
local env_count = reaper.CountTrackEnvelopes(track)
|
|
2329
|
+
if params.envelopeIndex >= env_count then
|
|
2330
|
+
return nil, "Envelope index " .. params.envelopeIndex .. " out of range (track has " .. env_count .. " envelopes)"
|
|
2331
|
+
end
|
|
2332
|
+
local env = reaper.GetTrackEnvelope(track, params.envelopeIndex)
|
|
2333
|
+
local total = reaper.CountEnvelopePoints(env)
|
|
2334
|
+
local offset = params.offset or 0
|
|
2335
|
+
local limit = params.limit or total
|
|
2336
|
+
local points = {}
|
|
2337
|
+
local end_idx = math.min(offset + limit, total)
|
|
2338
|
+
for i = offset, end_idx - 1 do
|
|
2339
|
+
local _, time, value, shape, tension, selected = reaper.GetEnvelopePoint(env, i)
|
|
2340
|
+
points[#points + 1] = {
|
|
2341
|
+
index = i,
|
|
2342
|
+
time = time,
|
|
2343
|
+
value = value,
|
|
2344
|
+
shape = shape,
|
|
2345
|
+
tension = tension,
|
|
2346
|
+
selected = selected,
|
|
2347
|
+
}
|
|
2348
|
+
end
|
|
2349
|
+
return {
|
|
2350
|
+
trackIndex = params.trackIndex,
|
|
2351
|
+
envelopeIndex = params.envelopeIndex,
|
|
2352
|
+
points = points,
|
|
2353
|
+
total = total,
|
|
2354
|
+
offset = offset,
|
|
2355
|
+
hasMore = end_idx < total,
|
|
2356
|
+
}
|
|
2357
|
+
end
|
|
2358
|
+
|
|
2359
|
+
function handlers.insert_envelope_point(params)
|
|
2360
|
+
local track = reaper.GetTrack(0, params.trackIndex)
|
|
2361
|
+
if not track then return nil, "Track " .. params.trackIndex .. " not found" end
|
|
2362
|
+
local env_count = reaper.CountTrackEnvelopes(track)
|
|
2363
|
+
if params.envelopeIndex >= env_count then
|
|
2364
|
+
return nil, "Envelope index " .. params.envelopeIndex .. " out of range (track has " .. env_count .. " envelopes)"
|
|
2365
|
+
end
|
|
2366
|
+
local env = reaper.GetTrackEnvelope(track, params.envelopeIndex)
|
|
2367
|
+
local shape = params.shape or 0
|
|
2368
|
+
local tension = params.tension or 0
|
|
2369
|
+
reaper.InsertEnvelopePoint(env, params.time, params.value, shape, tension, false, true)
|
|
2370
|
+
reaper.Envelope_SortPoints(env)
|
|
2371
|
+
local total = reaper.CountEnvelopePoints(env)
|
|
2372
|
+
return {
|
|
2373
|
+
trackIndex = params.trackIndex,
|
|
2374
|
+
envelopeIndex = params.envelopeIndex,
|
|
2375
|
+
time = params.time,
|
|
2376
|
+
value = params.value,
|
|
2377
|
+
shape = shape,
|
|
2378
|
+
tension = tension,
|
|
2379
|
+
totalPoints = total,
|
|
2380
|
+
}
|
|
2381
|
+
end
|
|
2382
|
+
|
|
2383
|
+
function handlers.delete_envelope_point(params)
|
|
2384
|
+
local track = reaper.GetTrack(0, params.trackIndex)
|
|
2385
|
+
if not track then return nil, "Track " .. params.trackIndex .. " not found" end
|
|
2386
|
+
local env_count = reaper.CountTrackEnvelopes(track)
|
|
2387
|
+
if params.envelopeIndex >= env_count then
|
|
2388
|
+
return nil, "Envelope index " .. params.envelopeIndex .. " out of range (track has " .. env_count .. " envelopes)"
|
|
2389
|
+
end
|
|
2390
|
+
local env = reaper.GetTrackEnvelope(track, params.envelopeIndex)
|
|
2391
|
+
local total = reaper.CountEnvelopePoints(env)
|
|
2392
|
+
if params.pointIndex >= total then
|
|
2393
|
+
return nil, "Point index " .. params.pointIndex .. " out of range (envelope has " .. total .. " points)"
|
|
2394
|
+
end
|
|
2395
|
+
reaper.DeleteEnvelopePointRange(env, params.pointIndex, params.pointIndex + 1)
|
|
2396
|
+
return { success = true, totalPoints = reaper.CountEnvelopePoints(env) }
|
|
2397
|
+
end
|
|
2398
|
+
|
|
2399
|
+
function handlers.create_track_envelope(params)
|
|
2400
|
+
local track = reaper.GetTrack(0, params.trackIndex)
|
|
2401
|
+
if not track then return nil, "Track " .. params.trackIndex .. " not found" end
|
|
2402
|
+
|
|
2403
|
+
local env = nil
|
|
2404
|
+
local env_name = nil
|
|
2405
|
+
|
|
2406
|
+
if params.fxIndex ~= nil then
|
|
2407
|
+
-- FX parameter envelope
|
|
2408
|
+
if params.paramIndex == nil then
|
|
2409
|
+
return nil, "paramIndex is required when fxIndex is provided"
|
|
2410
|
+
end
|
|
2411
|
+
local fx_count = reaper.TrackFX_GetNumParms(track)
|
|
2412
|
+
if fx_count == 0 then
|
|
2413
|
+
return nil, "Track " .. params.trackIndex .. " has no FX"
|
|
2414
|
+
end
|
|
2415
|
+
-- create=true to create the envelope if it doesn't exist
|
|
2416
|
+
env = reaper.GetFXEnvelope(track, params.fxIndex, params.paramIndex, true)
|
|
2417
|
+
if not env then
|
|
2418
|
+
return nil, "Failed to create FX parameter envelope (fxIndex=" .. params.fxIndex .. ", paramIndex=" .. params.paramIndex .. ")"
|
|
2419
|
+
end
|
|
2420
|
+
local _, pname = reaper.TrackFX_GetParamName(track, params.fxIndex, params.paramIndex)
|
|
2421
|
+
local _, fname = reaper.TrackFX_GetFXName(track, params.fxIndex)
|
|
2422
|
+
env_name = fname .. " / " .. pname
|
|
2423
|
+
elseif params.envelopeName then
|
|
2424
|
+
-- Built-in envelope: use GetTrackEnvelopeByName + show via state chunk
|
|
2425
|
+
env = reaper.GetTrackEnvelopeByName(track, params.envelopeName)
|
|
2426
|
+
if not env then
|
|
2427
|
+
-- Some envelopes need to be activated first via the track state chunk
|
|
2428
|
+
-- Toggle the envelope visibility via action or chunk editing
|
|
2429
|
+
local env_map = {
|
|
2430
|
+
["Volume"] = "VOLENV",
|
|
2431
|
+
["Pan"] = "PANENV",
|
|
2432
|
+
["Mute"] = "MUTEENV",
|
|
2433
|
+
["Width"] = "WIDTHENV",
|
|
2434
|
+
["Trim Volume"] = "VOLENV2",
|
|
2435
|
+
}
|
|
2436
|
+
local chunk_key = env_map[params.envelopeName]
|
|
2437
|
+
if not chunk_key then
|
|
2438
|
+
return nil, "Unknown envelope name: " .. params.envelopeName .. ". Use Volume, Pan, Mute, Width, or Trim Volume"
|
|
2439
|
+
end
|
|
2440
|
+
-- Get track chunk, insert envelope chunk if missing
|
|
2441
|
+
local _, chunk = reaper.GetTrackStateChunk(track, "", false)
|
|
2442
|
+
if not chunk:find(chunk_key) then
|
|
2443
|
+
-- Insert a minimal envelope chunk before the closing >
|
|
2444
|
+
local env_chunk = "\n<" .. chunk_key .. "\nACT 1 -1\nVIS 1 1 1\nLANEHEIGHT 0 0\nARM 0\nDEFSHAPE 0 -1 -1\n>\n"
|
|
2445
|
+
chunk = chunk:gsub("\n>$", env_chunk .. ">")
|
|
2446
|
+
reaper.SetTrackStateChunk(track, chunk, false)
|
|
2447
|
+
else
|
|
2448
|
+
-- Envelope exists in chunk but may be hidden; make it visible
|
|
2449
|
+
chunk = chunk:gsub("(" .. chunk_key .. "[^\n]*\n)ACT 0", "%1ACT 1")
|
|
2450
|
+
chunk = chunk:gsub("(" .. chunk_key .. "[^\n]*\n[^\n]*\n)VIS 0", "%1VIS 1")
|
|
2451
|
+
reaper.SetTrackStateChunk(track, chunk, false)
|
|
2452
|
+
end
|
|
2453
|
+
env = reaper.GetTrackEnvelopeByName(track, params.envelopeName)
|
|
2454
|
+
if not env then
|
|
2455
|
+
return nil, "Failed to create envelope: " .. params.envelopeName
|
|
2456
|
+
end
|
|
2457
|
+
else
|
|
2458
|
+
-- Envelope exists, ensure it's visible and active
|
|
2459
|
+
local _, chunk = reaper.GetEnvelopeStateChunk(env, "", false)
|
|
2460
|
+
chunk = chunk:gsub("ACT 0", "ACT 1")
|
|
2461
|
+
chunk = chunk:gsub("VIS 0", "VIS 1")
|
|
2462
|
+
reaper.SetEnvelopeStateChunk(env, chunk, false)
|
|
2463
|
+
end
|
|
2464
|
+
env_name = params.envelopeName
|
|
2465
|
+
else
|
|
2466
|
+
return nil, "Must provide either envelopeName or fxIndex+paramIndex"
|
|
2467
|
+
end
|
|
2468
|
+
|
|
2469
|
+
-- Find the index of the newly created envelope
|
|
2470
|
+
local env_count = reaper.CountTrackEnvelopes(track)
|
|
2471
|
+
local env_index = -1
|
|
2472
|
+
for i = 0, env_count - 1 do
|
|
2473
|
+
if reaper.GetTrackEnvelope(track, i) == env then
|
|
2474
|
+
env_index = i
|
|
2475
|
+
break
|
|
2476
|
+
end
|
|
2477
|
+
end
|
|
2478
|
+
|
|
2479
|
+
reaper.TrackList_AdjustWindows(false)
|
|
2480
|
+
reaper.UpdateArrange()
|
|
2481
|
+
|
|
2482
|
+
return {
|
|
2483
|
+
trackIndex = params.trackIndex,
|
|
2484
|
+
envelopeIndex = env_index,
|
|
2485
|
+
name = env_name,
|
|
2486
|
+
pointCount = reaper.CountEnvelopePoints(env),
|
|
2487
|
+
}
|
|
2488
|
+
end
|
|
2489
|
+
|
|
2490
|
+
function handlers.set_envelope_properties(params)
|
|
2491
|
+
local track = reaper.GetTrack(0, params.trackIndex)
|
|
2492
|
+
if not track then return nil, "Track " .. params.trackIndex .. " not found" end
|
|
2493
|
+
local env_count = reaper.CountTrackEnvelopes(track)
|
|
2494
|
+
if params.envelopeIndex >= env_count then
|
|
2495
|
+
return nil, "Envelope index " .. params.envelopeIndex .. " out of range (track has " .. env_count .. " envelopes)"
|
|
2496
|
+
end
|
|
2497
|
+
local env = reaper.GetTrackEnvelope(track, params.envelopeIndex)
|
|
2498
|
+
|
|
2499
|
+
if reaper.BR_EnvAlloc then
|
|
2500
|
+
-- SWS extension available: use BR_Env* functions
|
|
2501
|
+
local br_env = reaper.BR_EnvAlloc(env, false)
|
|
2502
|
+
local cur_active, cur_visible, cur_armed = reaper.BR_EnvGetProperties(br_env)
|
|
2503
|
+
local new_active = params.active ~= nil and params.active or cur_active
|
|
2504
|
+
local new_visible = params.visible ~= nil and params.visible or cur_visible
|
|
2505
|
+
local new_armed = params.armed ~= nil and params.armed or cur_armed
|
|
2506
|
+
reaper.BR_EnvSetProperties(br_env, new_active, new_visible, new_armed, false)
|
|
2507
|
+
reaper.BR_EnvFree(br_env, true) -- commit=true
|
|
2508
|
+
else
|
|
2509
|
+
-- Fallback: modify envelope state chunk directly
|
|
2510
|
+
local _, chunk = reaper.GetEnvelopeStateChunk(env, "", false)
|
|
2511
|
+
if params.active ~= nil then
|
|
2512
|
+
local val = params.active and "1" or "0"
|
|
2513
|
+
chunk = chunk:gsub("ACT %d", "ACT " .. val)
|
|
2514
|
+
end
|
|
2515
|
+
if params.visible ~= nil then
|
|
2516
|
+
local val = params.visible and "1" or "0"
|
|
2517
|
+
chunk = chunk:gsub("VIS %d", "VIS " .. val)
|
|
2518
|
+
end
|
|
2519
|
+
if params.armed ~= nil then
|
|
2520
|
+
local val = params.armed and "1" or "0"
|
|
2521
|
+
chunk = chunk:gsub("ARM %d", "ARM " .. val)
|
|
2522
|
+
end
|
|
2523
|
+
reaper.SetEnvelopeStateChunk(env, chunk, false)
|
|
2524
|
+
end
|
|
2525
|
+
|
|
2526
|
+
reaper.TrackList_AdjustWindows(false)
|
|
2527
|
+
reaper.UpdateArrange()
|
|
2528
|
+
|
|
2529
|
+
local _, name = reaper.GetEnvelopeName(env)
|
|
2530
|
+
-- Re-read properties
|
|
2531
|
+
local active, visible, armed = true, true, false
|
|
2532
|
+
if reaper.BR_EnvAlloc then
|
|
2533
|
+
local br_env = reaper.BR_EnvAlloc(env, false)
|
|
2534
|
+
active, visible, armed = reaper.BR_EnvGetProperties(br_env)
|
|
2535
|
+
reaper.BR_EnvFree(br_env, false)
|
|
2536
|
+
end
|
|
2537
|
+
return {
|
|
2538
|
+
trackIndex = params.trackIndex,
|
|
2539
|
+
envelopeIndex = params.envelopeIndex,
|
|
2540
|
+
name = name,
|
|
2541
|
+
active = active,
|
|
2542
|
+
visible = visible,
|
|
2543
|
+
armed = armed,
|
|
2544
|
+
}
|
|
2545
|
+
end
|
|
2546
|
+
|
|
2547
|
+
function handlers.clear_envelope(params)
|
|
2548
|
+
local track = reaper.GetTrack(0, params.trackIndex)
|
|
2549
|
+
if not track then return nil, "Track " .. params.trackIndex .. " not found" end
|
|
2550
|
+
local env_count = reaper.CountTrackEnvelopes(track)
|
|
2551
|
+
if params.envelopeIndex >= env_count then
|
|
2552
|
+
return nil, "Envelope index " .. params.envelopeIndex .. " out of range (track has " .. env_count .. " envelopes)"
|
|
2553
|
+
end
|
|
2554
|
+
local env = reaper.GetTrackEnvelope(track, params.envelopeIndex)
|
|
2555
|
+
local prev_count = reaper.CountEnvelopePoints(env)
|
|
2556
|
+
-- Delete all points by using a range from -infinity to +infinity
|
|
2557
|
+
reaper.DeleteEnvelopePointRange(env, -math.huge, math.huge)
|
|
2558
|
+
reaper.Envelope_SortPoints(env)
|
|
2559
|
+
return {
|
|
2560
|
+
trackIndex = params.trackIndex,
|
|
2561
|
+
envelopeIndex = params.envelopeIndex,
|
|
2562
|
+
deletedPoints = prev_count,
|
|
2563
|
+
totalPoints = reaper.CountEnvelopePoints(env),
|
|
2564
|
+
}
|
|
2565
|
+
end
|
|
2566
|
+
|
|
2567
|
+
function handlers.remove_envelope_points(params)
|
|
2568
|
+
local track = reaper.GetTrack(0, params.trackIndex)
|
|
2569
|
+
if not track then return nil, "Track " .. params.trackIndex .. " not found" end
|
|
2570
|
+
local env_count = reaper.CountTrackEnvelopes(track)
|
|
2571
|
+
if params.envelopeIndex >= env_count then
|
|
2572
|
+
return nil, "Envelope index " .. params.envelopeIndex .. " out of range (track has " .. env_count .. " envelopes)"
|
|
2573
|
+
end
|
|
2574
|
+
local env = reaper.GetTrackEnvelope(track, params.envelopeIndex)
|
|
2575
|
+
local prev_count = reaper.CountEnvelopePoints(env)
|
|
2576
|
+
reaper.DeleteEnvelopePointRange(env, params.timeStart, params.timeEnd)
|
|
2577
|
+
reaper.Envelope_SortPoints(env)
|
|
2578
|
+
local new_count = reaper.CountEnvelopePoints(env)
|
|
2579
|
+
return {
|
|
2580
|
+
trackIndex = params.trackIndex,
|
|
2581
|
+
envelopeIndex = params.envelopeIndex,
|
|
2582
|
+
timeStart = params.timeStart,
|
|
2583
|
+
timeEnd = params.timeEnd,
|
|
2584
|
+
deletedPoints = prev_count - new_count,
|
|
2585
|
+
totalPoints = new_count,
|
|
2586
|
+
}
|
|
2587
|
+
end
|
|
2588
|
+
|
|
2589
|
+
function handlers.insert_envelope_points(params)
|
|
2590
|
+
local track = reaper.GetTrack(0, params.trackIndex)
|
|
2591
|
+
if not track then return nil, "Track " .. params.trackIndex .. " not found" end
|
|
2592
|
+
local env_count = reaper.CountTrackEnvelopes(track)
|
|
2593
|
+
if params.envelopeIndex >= env_count then
|
|
2594
|
+
return nil, "Envelope index " .. params.envelopeIndex .. " out of range (track has " .. env_count .. " envelopes)"
|
|
2595
|
+
end
|
|
2596
|
+
local env = reaper.GetTrackEnvelope(track, params.envelopeIndex)
|
|
2597
|
+
|
|
2598
|
+
-- Parse the points JSON string
|
|
2599
|
+
local points_str = params.points
|
|
2600
|
+
local points = nil
|
|
2601
|
+
if type(points_str) == "string" then
|
|
2602
|
+
points = json_decode(points_str)
|
|
2603
|
+
elseif type(points_str) == "table" then
|
|
2604
|
+
points = points_str
|
|
2605
|
+
end
|
|
2606
|
+
if not points then return nil, "Failed to parse points JSON" end
|
|
2607
|
+
|
|
2608
|
+
local inserted = 0
|
|
2609
|
+
for _, pt in ipairs(points) do
|
|
2610
|
+
if pt.time and pt.value then
|
|
2611
|
+
local shape = pt.shape or 0
|
|
2612
|
+
local tension = pt.tension or 0
|
|
2613
|
+
reaper.InsertEnvelopePoint(env, pt.time, pt.value, shape, tension, false, true)
|
|
2614
|
+
inserted = inserted + 1
|
|
2615
|
+
end
|
|
2616
|
+
end
|
|
2617
|
+
|
|
2618
|
+
reaper.Envelope_SortPoints(env)
|
|
2619
|
+
return {
|
|
2620
|
+
trackIndex = params.trackIndex,
|
|
2621
|
+
envelopeIndex = params.envelopeIndex,
|
|
2622
|
+
insertedPoints = inserted,
|
|
2623
|
+
totalPoints = reaper.CountEnvelopePoints(env),
|
|
2624
|
+
}
|
|
2625
|
+
end
|
|
2626
|
+
|
|
1802
2627
|
-- =============================================================================
|
|
1803
2628
|
-- Command dispatcher
|
|
1804
2629
|
-- =============================================================================
|