authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-06-21 00:44:44-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-07-05 00:39:04-07:00
logeb53fbaf3fc1988fc111b1e36755f557c537ba2d
tree7604e9af02f5f7e1c868bf0fd890b1712261b011
parentdc68ac0c426180e0b724d0b1acfe8bd30c199c36
signaturebadge-check Signed by SSH key SHA256:xbd+BjjhyBfwk7GVoURf9Yx0gzDerHbvYv7SddNWmAs

wip: pre-reorg snapshot


43 files changed, 5706 insertions(+), 1981 deletions(-)

config/reaper.ts+64-65
......@@ -1,72 +1,71 @@
11import * as config from "#config";
2import { lucide, mdi, txt } from "@clo/creative-control/icons";
23import { Reaper } from "@clo/creative-control/Reaper";
3import { SpeedEditor } from "@clo/creative-control/SpeedEditor";
44
5const cfg = config.forApp("com.cockos.reaper", ({ speededitor: se, mac }) => {
6 const reaper = new Reaper();
5export default config.forApp(
6 "com.cockos.reaper",
7 ({ keypad, dialpad, mac }) => {
8 const reaper = new Reaper();
79
8 // Sync state to LEDs
9 reaper.on("transport", (transport) => {
10 se.leds.cut = transport.recording;
11 se.leds.dissolve = transport.recording;
12 se.leds.smoothCut = transport.recording;
13 });
14 mac.on("window", (window) => {
15 se.leds.videoOnly = Boolean(window?.title.startsWith("FX:"));
16 });
10 const addInstrument = keypad.menu((menu) => {
11 menu.key("up-left", txt("KK"), () => {
12 mac.toast("Add Komplete Kontrol");
13 reaper.runScript("insert_komplete_kontrol_track");
14 keypad.back();
15 });
16 menu.key("up", txt("AD2"), () => {
17 mac.toast("Add Addictive Drums");
18 reaper.runScript("insert_addictive_drums_track");
19 keypad.back();
20 });
21 menu.key("up-right", txt("Blank"), () => {
22 mac.toast("Blank Track");
23 reaper.runScript("insert_blank_track");
24 keypad.back();
25 });
26 });
1727
18 // Keyboard Actions
19 SpeedEditor.camNumbersToNumpad(se, mac);
20 se.onPress("stopPlay", () => {
21 if (reaper.transport.recording) {
22 reaper.runAction("transport-stop-save-all-recorded-media");
23 } else {
24 reaper.runAction("transport-play-stop");
25 }
26 });
27 se.onPress("smartInsert", () => {
28 mac.toast("Add Komplete Kontrol");
29 reaper.runScript("insert_komplete_kontrol_track");
30 });
31 se.onPress("append", () => {
32 mac.toast("Add Addictive Drums");
33 reaper.runScript("insert_addictive_drums_track");
34 });
35 se.onPress("rippleOverwrite", () => {
36 mac.toast("Blank Track");
37 reaper.runScript("insert_blank_track");
38 });
39 se.onPress("cut", () => {
40 if (reaper.transport.recording) {
41 reaper.runAction("transport-stop-delete-all-recorded-media");
42 reaper.runAction("transport-record");
43 } else {
44 reaper.runAction("transport-record");
45 }
46 });
47 se.onPress("dissolve", () => {
48 reaper.runAction("transport-stop-delete-all-recorded-media");
49 });
50 se.onPress("smoothCut", () => {
51 if (reaper.transport.recording) {
52 reaper.runAction("transport-stop-save-all-recorded-media");
28 const recording = keypad.overlay((overlay) => {
29 overlay.key("down-left", lucide("Save").fg("green"), () => {
30 reaper.runAction("transport-stop-save-all-recorded-media");
31 });
32 overlay.key("down", lucide("Redo").fg("green"), () => {
33 reaper.runAction("transport-stop-save-all-recorded-media");
34 reaper.runAction("transport-record");
35 });
36 overlay.key("down-right", lucide("Trash").fg("#ff6b6b"), () => {
37 reaper.runAction("transport-stop-delete-all-recorded-media");
38 });
39 });
40
41 keypad.key("up-left", mdi("metronome"), () => {
42 reaper.runAction("options-toggle-metronome");
43 });
44 keypad.key("up", lucide("Clock"), () => reaper.runAction("file-project-settings"));
45 keypad.key("up-right", txt("BPM"), () => {
46 reaper.runAction("tempo-increase-current-project-tempo-01-bpm");
47 });
48 keypad.key("left", lucide("Plus"), () => {
49 addInstrument.open();
50 });
51 keypad.key("down-left", lucide("Mic").fg("red"), () => {
5352 reaper.runAction("transport-record");
54 }
55 });
56 se.onPress("videoOnly", () => {
57 if (mac.window?.title.startsWith("FX:") && mac.mainWindow) {
58 mac.focusMainWindow();
59 return;
60 }
61 reaper.runAction("track-view-fx-chain-for-current-last-touched-track");
62 });
63 se.onPress("fullView", () => {
64 mac.pressKey("x");
65 });
53 });
54 keypad.key("down-right", lucide("Play"), () => {
55 reaper.runAction("transport-play-stop");
56 });
57
58 reaper.on("transport", (transport) => {
59 recording.active = transport.recording;
60 });
6661
67 // TODO: stabilize / find a better use-case for the knob
68 // se.on("jog", ({ value = 0 }) => {
69 // reaper.scrub(value / 20);
70 // });
71});
72export default cfg;
62 dialpad.on("rotate", (delta) => {
63 console.log({delta})
64 reaper.runAction(
65 delta > 0
66 ? "tempo-increase-current-project-tempo-0-1-bpm"
67 : "tempo-decrease-current-project-tempo-0-1-bpm",
68 );
69 });
70 },
71);
config/reaper/scripts/clover_feedback.lua created+54
......@@ -0,0 +1,54 @@
1-- Clover live feedback: continuously write the project's tempo and time
2-- signature to state.json (next to this script) so the Clover Node process can
3-- watch the file and update the keypad. Re-schedules itself via reaper.defer,
4-- writing only when a value actually changes.
5--
6-- REAPER's OSC has a tempo token but no time-signature feedback, so we read both
7-- here in one place (reaper.TimeMap_GetTimeSigAtTime returns num, denom, tempo).
8
9-- Avoid stacking multiple defer loops if the script gets launched again (e.g. a
10-- Clover restart while REAPER keeps running).
11if reaper.GetExtState("clover", "feedback") == "1" then
12 return
13end
14reaper.SetExtState("clover", "feedback", "1", false)
15reaper.atexit(function()
16 reaper.SetExtState("clover", "feedback", "", false)
17end)
18
19local source = debug.getinfo(1, "S").source
20local script_path = source:match("^@(.+)$")
21local script_dir = script_path:match("^(.*)[/\\].-$")
22local sep = package.config:sub(1, 1)
23local state_path = script_dir .. sep .. "state.json"
24
25local last = nil
26
27local function snapshot()
28 local position
29 if reaper.GetPlayState() > 0 then
30 position = reaper.GetPlayPosition()
31 else
32 position = reaper.GetCursorPosition()
33 end
34
35 local num, denom, tempo = reaper.TimeMap_GetTimeSigAtTime(0, position)
36 num = math.floor(num + 0.5)
37 denom = math.floor(denom + 0.5)
38 return string.format('{"tempo":%.3f,"timesig":"%d/%d"}', tempo, num, denom)
39end
40
41local function poll()
42 local snap = snapshot()
43 if snap ~= last then
44 last = snap
45 local file = io.open(state_path, "w")
46 if file then
47 file:write(snap)
48 file:close()
49 end
50 end
51 reaper.defer(poll)
52end
53
54poll()
examples/dialpad.ts created+14
......@@ -0,0 +1,14 @@
1// Listen to the dialpad. NOTE: until the OS-seize phase, turning the dial also
2// scrolls macOS and the buttons act as mouse buttons.
3import { Dialpad } from "../src/Dialpad.ts";
4
5const dialpad = await Dialpad.open();
6console.info(dialpad.connected ? "Dialpad connected" : "Waiting for dialpad…");
7
8dialpad.on("connect", () => console.info("connect"));
9dialpad.on("disconnect", () => console.info("disconnect"));
10dialpad.on("rotate", (delta) => console.info("rotate", delta));
11dialpad.on("spin", (delta) => console.info("spin", delta));
12dialpad.on("keydown", (button) => console.info("down", button));
13dialpad.on("keyup", (button) => console.info("up", button));
14dialpad.onPress("circle", () => console.info("circle pressed!"));
examples/enumerate-hid.ts created+43
......@@ -0,0 +1,43 @@
1// Phase 0: list HID devices so we can identify the MX Creative Console halves.
2// Keypad is expected as Elgato (0x0fd9); the Bluetooth dialpad is likely
3// Logitech (0x046d) speaking HID++.
4const { devices } = await import("node-hid");
5
6const VENDOR_NAMES: Record<number, string> = {
7 0x046d: "Logitech",
8 0x0fd9: "Elgato",
9 0x05ac: "Apple",
10};
11
12const hex = (n: number | undefined, width = 4) => "0x" + (n ?? 0).toString(16).padStart(width, "0");
13
14const all = devices();
15
16const format = (d: import("node-hid").Device) =>
17 [
18 `${hex(d.vendorId)}:${hex(d.productId)}`,
19 (VENDOR_NAMES[d.vendorId] ?? "?").padEnd(8),
20 `usage=${hex(d.usagePage)}/${hex(d.usage)}`,
21 `iface=${d.interface}`,
22 `| ${d.manufacturer ?? ""} ${d.product ?? ""}`.trim(),
23 d.path ? `\n path=${d.path}` : "",
24 ].join(" ");
25
26const interesting = all.filter(
27 (d) => d.vendorId === 0x046d || d.vendorId === 0x0fd9,
28);
29
30console.info(`Total HID devices: ${all.length}`);
31console.info(`\n=== Logitech (0x046d) + Elgato (0x0fd9) ===`);
32if (interesting.length === 0) {
33 console.info(" (none found — dialpad may not surface as a HID device)");
34} else {
35 for (const d of interesting) console.info(" " + format(d));
36}
37
38console.info(`\n=== All vendors present ===`);
39const byVendor = new Map<number, number>();
40for (const d of all) byVendor.set(d.vendorId, (byVendor.get(d.vendorId) ?? 0) + 1);
41for (const [vid, count] of [...byVendor].sort((a, b) => b[1] - a[1])) {
42 console.info(` ${hex(vid)} ${(VENDOR_NAMES[vid] ?? "").padEnd(8)} ${count}`);
43}
examples/event-listener.ts+1-1
......@@ -15,7 +15,7 @@ editor.on("keypress", (key) => {
1515 }
1616});
1717editor.onDoublePress("transition", () => {
18 console.info('(double press) "Title"');
18 console.info("(double press) \"Title\"");
1919});
2020
2121console.info("init");
examples/face-preview.ts created+26
......@@ -0,0 +1,26 @@
1// Render key faces to PNGs (no hardware needed) so you can eyeball them before
2// pushing to the device. Writes to /tmp/face-preview/.
3import { mkdirSync, writeFileSync } from "node:fs";
4import sharp from "sharp";
5import { blank, lucide, mdi, txt } from "../src/icons.ts";
6
7const faces: Record<string, string> = {
8 blank: blank.svg,
9 metronome: mdi("metronome").svg,
10 "time-sig": lucide("Clock").svg,
11 bpm: txt("BPM").svg,
12 "add-instrument": lucide("Plus").svg,
13 record: lucide("Disc").fg("red").svg,
14 play: lucide("Play").svg,
15 save: lucide("Save").fg("green").svg,
16};
17
18const outDir = "/tmp/face-preview";
19mkdirSync(outDir, { recursive: true });
20
21for (const [name, face] of Object.entries(faces)) {
22 const png = await sharp(Buffer.from(face)).resize(118, 118).png().toBuffer();
23 const file = `${outDir}/${name}.png`;
24 writeFileSync(file, png);
25 console.info(`${name.padEnd(16)} ${png.length} bytes ${file}`);
26}
examples/hid-sniff.ts created+67
......@@ -0,0 +1,67 @@
1// Phase 0 sniffer: open an MX Creative Console half by product-name match and
2// log every HID input report. Operate the control you want to map and watch the
3// report id + bytes. Logitech HID++ events arrive as report id 0x10 (short, 7B)
4// or 0x11 (long, 20B). Default mouse/consumer reports use other ids.
5//
6// node examples/hid-sniff.ts [name-substring] [seconds]
7// node examples/hid-sniff.ts dialpad 25
8const { devices, HID } = await import("node-hid");
9
10const match = (process.argv[2] ?? "dialpad").toLowerCase();
11const durationSec = Number(process.argv[3] ?? 0);
12
13const hex = (n: number, w = 2) => n.toString(16).padStart(w, "0");
14
15const dev = devices().find(
16 (d) => (d.product ?? "").toLowerCase().includes(match) && d.path,
17);
18if (!dev?.path) {
19 console.error(
20 `No HID device matching "${match}". Run examples/enumerate-hid.ts to list.`,
21 );
22 process.exit(1);
23}
24
25console.info(
26 `Opening ${dev.product} 0x${hex(dev.vendorId, 4)}:0x${hex(dev.productId, 4)}\n path=${dev.path}`,
27);
28
29let device: import("node-hid").HID;
30try {
31 device = new HID(dev.path);
32} catch (error) {
33 console.error(
34 "Failed to open device. On macOS, grant the terminal/node Input Monitoring\n"
35 + "(System Settings > Privacy & Security > Input Monitoring), then retry.\n",
36 error,
37 );
38 process.exit(1);
39}
40
41const t0 = Date.now();
42let count = 0;
43device.on("data", (buf: Buffer) => {
44 const bytes = [...buf];
45 const id = bytes[0];
46 const kind = id === 0x11
47 ? "hid++ long "
48 : id === 0x10
49 ? "hid++ short"
50 : "report ";
51 const ms = String(Date.now() - t0).padStart(6);
52 console.info(
53 `+${ms}ms ${kind} id=0x${hex(id)} ${bytes.map((b) => hex(b)).join(" ")}`,
54 );
55 count += 1;
56});
57device.on("error", (error) => console.error("device error:", error));
58
59console.info("Listening — operate the dial / knob / buttons. Ctrl-C to stop.\n");
60
61if (durationSec > 0) {
62 setTimeout(() => {
63 console.info(`\nCaptured ${count} reports in ${durationSec}s. Closing.`);
64 device.close();
65 process.exit(0);
66 }, durationSec * 1000);
67}
examples/keypad-demo.ts created+30
......@@ -0,0 +1,30 @@
1// Push the Reaper root layout to the physical keypad as one atomic panel write
2// and exit (faces persist on the device). Validates the panel image protocol.
3import { Keypad } from "../src/Keypad.ts";
4import { composePanel, type Face } from "../src/KeypadUI.ts";
5import { blank, lucide, txt } from "../src/icons.ts";
6
7const keypad = await Keypad.open();
8if (!keypad.connected) {
9 console.error("Keypad not connected.");
10 process.exit(1);
11}
12
13keypad.setBrightness(0.85);
14
15// Faces in grid order: up-left, up, up-right, left, center, right, down-*.
16const faces: Face[] = [
17 lucide("AlarmClock"),
18 lucide("Clock"),
19 txt("BPM"),
20 lucide("Plus"),
21 blank,
22 blank,
23 lucide("Disc").fg("red"),
24 blank,
25 lucide("Play"),
26];
27
28keypad.setPanel(await composePanel(faces));
29console.info("Pushed the panel to the keypad — look at the device.");
30process.exit(0);
package.json+9
......@@ -10,8 +10,12 @@
1010 },
1111 "dependencies": {
1212 "@clo/lib": "jsr:^3.0.0",
13 "@mdi/svg": "^7.4.47",
1314 "@types/node": "^25.5.0",
15 "lucide-static": "^1.21.0",
16 "mdi-ts": "^1.0.3",
1417 "node-hid": "^3.3.0",
18 "sharp": "^0.35.2",
1519 "usb": "^2.17.0"
1620 },
1721 "imports": {
......@@ -20,6 +24,11 @@
2024 "exports": {
2125 "./Mac": "./src/Mac.ts",
2226 "./SpeedEditor": "./src/SpeedEditor.ts",
27 "./Keypad": "./src/Keypad.ts",
28 "./KeypadUI": "./src/KeypadUI.ts",
29 "./icons": "./src/icons.ts",
30 "./signals": "./src/signals.ts",
31 "./Dialpad": "./src/Dialpad.ts",
2332 "./Reaper": "./src/Reaper.ts",
2433 "./Reaper/actions": "./src/Reaper/actions.ts"
2534 },
pnpm-lock.yaml+409-110
......@@ -1,186 +1,438 @@
1lockfileVersion: "9.0"
1lockfileVersion: '9.0'
22
33settings:
44 autoInstallPeers: true
55 excludeLinksFromLockfile: false
66
77importers:
8
89 .:
910 dependencies:
10 "@clo/lib":
11 '@clo/lib':
1112 specifier: jsr:^3.0.0
12 version: "@jsr/clo__lib@3.0.0"
13 "@types/node":
13 version: '@jsr/clo__lib@3.0.0'
14 '@mdi/svg':
15 specifier: ^7.4.47
16 version: 7.4.47
17 '@types/node':
1418 specifier: ^25.5.0
1519 version: 25.5.0
20 lucide-static:
21 specifier: ^1.21.0
22 version: 1.21.0
23 mdi-ts:
24 specifier: ^1.0.3
25 version: 1.0.3
1626 node-hid:
1727 specifier: ^3.3.0
1828 version: 3.3.0
29 sharp:
30 specifier: ^0.35.2
31 version: 0.35.2
1932 usb:
2033 specifier: ^2.17.0
2134 version: 2.17.0
2235
2336packages:
24 "@jsr/clo__lib@3.0.0":
25 resolution: {
26 integrity: sha512-oseZwHCAcXNPbqnGZ37l7+wAoj6ikIXE1VM0s6eD6fz4DcgM030Slf0T7Lgtn7fIdas5hlfx4JF54TR+vo4THw==,
27 tarball: https://npm.jsr.io/~/11/@jsr/clo__lib/3.0.0.tgz,
28 }
29
30 "@types/node@25.5.0":
31 resolution: {
32 integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==,
33 }
34
35 "@types/w3c-web-usb@1.0.13":
36 resolution: {
37 integrity: sha512-N2nSl3Xsx8mRHZBvMSdNGtzMyeleTvtlEw+ujujgXalPqOjIA6UtrqcB6OzyUjkTbDm3J7P1RNK1lgoO7jxtsw==,
38 }
37
38 '@emnapi/runtime@1.11.1':
39 resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==}
40
41 '@img/colour@1.1.0':
42 resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
43 engines: {node: '>=18'}
44
45 '@img/sharp-darwin-arm64@0.35.2':
46 resolution: {integrity: sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==}
47 engines: {node: '>=20.9.0'}
48 cpu: [arm64]
49 os: [darwin]
50
51 '@img/sharp-darwin-x64@0.35.2':
52 resolution: {integrity: sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==}
53 engines: {node: '>=20.9.0'}
54 cpu: [x64]
55 os: [darwin]
56
57 '@img/sharp-freebsd-wasm32@0.35.2':
58 resolution: {integrity: sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==}
59 engines: {node: '>=20.9.0'}
60 os: [freebsd]
61
62 '@img/sharp-libvips-darwin-arm64@1.3.1':
63 resolution: {integrity: sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==}
64 cpu: [arm64]
65 os: [darwin]
66
67 '@img/sharp-libvips-darwin-x64@1.3.1':
68 resolution: {integrity: sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==}
69 cpu: [x64]
70 os: [darwin]
71
72 '@img/sharp-libvips-linux-arm64@1.3.1':
73 resolution: {integrity: sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==}
74 cpu: [arm64]
75 os: [linux]
76
77 '@img/sharp-libvips-linux-arm@1.3.1':
78 resolution: {integrity: sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==}
79 cpu: [arm]
80 os: [linux]
81
82 '@img/sharp-libvips-linux-ppc64@1.3.1':
83 resolution: {integrity: sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==}
84 cpu: [ppc64]
85 os: [linux]
86
87 '@img/sharp-libvips-linux-riscv64@1.3.1':
88 resolution: {integrity: sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==}
89 cpu: [riscv64]
90 os: [linux]
91
92 '@img/sharp-libvips-linux-s390x@1.3.1':
93 resolution: {integrity: sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==}
94 cpu: [s390x]
95 os: [linux]
96
97 '@img/sharp-libvips-linux-x64@1.3.1':
98 resolution: {integrity: sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==}
99 cpu: [x64]
100 os: [linux]
101
102 '@img/sharp-libvips-linuxmusl-arm64@1.3.1':
103 resolution: {integrity: sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==}
104 cpu: [arm64]
105 os: [linux]
106
107 '@img/sharp-libvips-linuxmusl-x64@1.3.1':
108 resolution: {integrity: sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==}
109 cpu: [x64]
110 os: [linux]
111
112 '@img/sharp-linux-arm64@0.35.2':
113 resolution: {integrity: sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==}
114 engines: {node: '>=20.9.0'}
115 cpu: [arm64]
116 os: [linux]
117
118 '@img/sharp-linux-arm@0.35.2':
119 resolution: {integrity: sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==}
120 engines: {node: '>=20.9.0'}
121 cpu: [arm]
122 os: [linux]
123
124 '@img/sharp-linux-ppc64@0.35.2':
125 resolution: {integrity: sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==}
126 engines: {node: '>=20.9.0'}
127 cpu: [ppc64]
128 os: [linux]
129
130 '@img/sharp-linux-riscv64@0.35.2':
131 resolution: {integrity: sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==}
132 engines: {node: '>=20.9.0'}
133 cpu: [riscv64]
134 os: [linux]
135
136 '@img/sharp-linux-s390x@0.35.2':
137 resolution: {integrity: sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==}
138 engines: {node: '>=20.9.0'}
139 cpu: [s390x]
140 os: [linux]
141
142 '@img/sharp-linux-x64@0.35.2':
143 resolution: {integrity: sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==}
144 engines: {node: '>=20.9.0'}
145 cpu: [x64]
146 os: [linux]
147
148 '@img/sharp-linuxmusl-arm64@0.35.2':
149 resolution: {integrity: sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==}
150 engines: {node: '>=20.9.0'}
151 cpu: [arm64]
152 os: [linux]
153
154 '@img/sharp-linuxmusl-x64@0.35.2':
155 resolution: {integrity: sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==}
156 engines: {node: '>=20.9.0'}
157 cpu: [x64]
158 os: [linux]
159
160 '@img/sharp-wasm32@0.35.2':
161 resolution: {integrity: sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==}
162 engines: {node: '>=20.9.0'}
163
164 '@img/sharp-webcontainers-wasm32@0.35.2':
165 resolution: {integrity: sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==}
166 engines: {node: '>=20.9.0'}
167 cpu: [wasm32]
168
169 '@img/sharp-win32-arm64@0.35.2':
170 resolution: {integrity: sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==}
171 engines: {node: '>=20.9.0'}
172 cpu: [arm64]
173 os: [win32]
174
175 '@img/sharp-win32-ia32@0.35.2':
176 resolution: {integrity: sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==}
177 engines: {node: ^20.9.0}
178 cpu: [ia32]
179 os: [win32]
180
181 '@img/sharp-win32-x64@0.35.2':
182 resolution: {integrity: sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==}
183 engines: {node: '>=20.9.0'}
184 cpu: [x64]
185 os: [win32]
186
187 '@jsr/clo__lib@3.0.0':
188 resolution: {integrity: sha512-oseZwHCAcXNPbqnGZ37l7+wAoj6ikIXE1VM0s6eD6fz4DcgM030Slf0T7Lgtn7fIdas5hlfx4JF54TR+vo4THw==, tarball: https://npm.jsr.io/~/11/@jsr/clo__lib/3.0.0.tgz}
189
190 '@mdi/svg@7.4.47':
191 resolution: {integrity: sha512-WQ2gDll12T9WD34fdRFgQVgO8bag3gavrAgJ0frN4phlwdJARpE6gO1YvLEMJR0KKgoc+/Ea/A0Pp11I00xBvw==}
192
193 '@types/node@25.5.0':
194 resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==}
195
196 '@types/w3c-web-usb@1.0.13':
197 resolution: {integrity: sha512-N2nSl3Xsx8mRHZBvMSdNGtzMyeleTvtlEw+ujujgXalPqOjIA6UtrqcB6OzyUjkTbDm3J7P1RNK1lgoO7jxtsw==}
39198
40199 ansi-regex@5.0.1:
41 resolution: {
42 integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==,
43 }
44 engines: { node: ">=8" }
200 resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
201 engines: {node: '>=8'}
45202
46203 ansi-styles@4.3.0:
47 resolution: {
48 integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==,
49 }
50 engines: { node: ">=8" }
204 resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
205 engines: {node: '>=8'}
51206
52207 cliui@8.0.1:
53 resolution: {
54 integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==,
55 }
56 engines: { node: ">=12" }
208 resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
209 engines: {node: '>=12'}
57210
58211 color-convert@2.0.1:
59 resolution: {
60 integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==,
61 }
62 engines: { node: ">=7.0.0" }
212 resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
213 engines: {node: '>=7.0.0'}
63214
64215 color-name@1.1.4:
65 resolution: {
66 integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==,
67 }
216 resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
217
218 detect-libc@2.1.2:
219 resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
220 engines: {node: '>=8'}
68221
69222 emoji-regex@8.0.0:
70 resolution: {
71 integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==,
72 }
223 resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
73224
74225 escalade@3.2.0:
75 resolution: {
76 integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==,
77 }
78 engines: { node: ">=6" }
226 resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
227 engines: {node: '>=6'}
79228
80229 get-caller-file@2.0.5:
81 resolution: {
82 integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==,
83 }
84 engines: { node: 6.* || 8.* || >= 10.* }
230 resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
231 engines: {node: 6.* || 8.* || >= 10.*}
85232
86233 is-fullwidth-code-point@3.0.0:
87 resolution: {
88 integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==,
89 }
90 engines: { node: ">=8" }
234 resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
235 engines: {node: '>=8'}
236
237 linq@4.0.3:
238 resolution: {integrity: sha512-dP0w2ERJXfVUk6VmmAK+Tz/SxFHwyY7VM6Mrq4fnJmeQf9JNEYFH6qJfV6Qn0N91mfwz2GEE/4S+RDkmDNyUJw==}
239
240 lucide-static@1.21.0:
241 resolution: {integrity: sha512-6248z2/4sEyKkYAPPUYxOPiB2RCfMmLdMHuoOhsTFnoD40ixAoHmTVhOPux8ADa1NTBmzpEKF7WNePm+Ms503Q==}
242
243 mdi-ts@1.0.3:
244 resolution: {integrity: sha512-wtVNYoCkvyYuTJ8osV5af6jfsE5o+UT1sAnaPVjAZiIXHVFpP2l66JzdAdNClnLfCk7g5wu8cWHrr/9ru1V8vQ==}
91245
92246 node-addon-api@3.2.1:
93 resolution: {
94 integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==,
95 }
247 resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==}
96248
97249 node-addon-api@8.6.0:
98 resolution: {
99 integrity: sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==,
100 }
101 engines: { node: ^18 || ^20 || >= 21 }
250 resolution: {integrity: sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==}
251 engines: {node: ^18 || ^20 || >= 21}
102252
103253 node-gyp-build@4.8.4:
104 resolution: {
105 integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==,
106 }
254 resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
107255 hasBin: true
108256
109257 node-hid@3.3.0:
110 resolution: {
111 integrity: sha512-j+dFgJLRAE0nufQKXk3IfS6T6YuHhCgMvz4TrG0sgtb6DSCdYpfJ1etcdmeCmPQjUgO+yo32ktVrRliNs/+fmg==,
112 }
113 engines: { node: ">=10.16" }
258 resolution: {integrity: sha512-j+dFgJLRAE0nufQKXk3IfS6T6YuHhCgMvz4TrG0sgtb6DSCdYpfJ1etcdmeCmPQjUgO+yo32ktVrRliNs/+fmg==}
259 engines: {node: '>=10.16'}
114260 hasBin: true
115261
116262 pkg-prebuilds@1.0.0:
117 resolution: {
118 integrity: sha512-D9wlkXZCmjxj2kBHTw3fGSyjoahr33breGBoJcoezpi7ouYS59DJVOHMZ+dgqacSrZiJo4qtkXxLQTE+BqXJmQ==,
119 }
120 engines: { node: ">= 14.15.0" }
263 resolution: {integrity: sha512-D9wlkXZCmjxj2kBHTw3fGSyjoahr33breGBoJcoezpi7ouYS59DJVOHMZ+dgqacSrZiJo4qtkXxLQTE+BqXJmQ==}
264 engines: {node: '>= 14.15.0'}
121265 hasBin: true
122266
123267 require-directory@2.1.1:
124 resolution: {
125 integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==,
126 }
127 engines: { node: ">=0.10.0" }
268 resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
269 engines: {node: '>=0.10.0'}
270
271 semver@7.8.5:
272 resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
273 engines: {node: '>=10'}
274 hasBin: true
275
276 sharp@0.35.2:
277 resolution: {integrity: sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==}
278 engines: {node: '>=20.9.0'}
128279
129280 string-width@4.2.3:
130 resolution: {
131 integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==,
132 }
133 engines: { node: ">=8" }
281 resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
282 engines: {node: '>=8'}
134283
135284 strip-ansi@6.0.1:
136 resolution: {
137 integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==,
138 }
139 engines: { node: ">=8" }
285 resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
286 engines: {node: '>=8'}
287
288 tslib@2.8.1:
289 resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
140290
141291 undici-types@7.18.2:
142 resolution: {
143 integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==,
144 }
292 resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
145293
146294 usb@2.17.0:
147 resolution: {
148 integrity: sha512-UuFgrlglgDn5ll6d5l7kl3nDb2Yx43qLUGcDq+7UNLZLtbNug0HZBb2Xodhgx2JZB1LqvU+dOGqLEeYUeZqsHg==,
149 }
150 engines: { node: ">=12.22.0 <13.0 || >=14.17.0" }
295 resolution: {integrity: sha512-UuFgrlglgDn5ll6d5l7kl3nDb2Yx43qLUGcDq+7UNLZLtbNug0HZBb2Xodhgx2JZB1LqvU+dOGqLEeYUeZqsHg==}
296 engines: {node: '>=12.22.0 <13.0 || >=14.17.0'}
151297
152298 wrap-ansi@7.0.0:
153 resolution: {
154 integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==,
155 }
156 engines: { node: ">=10" }
299 resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
300 engines: {node: '>=10'}
157301
158302 y18n@5.0.8:
159 resolution: {
160 integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==,
161 }
162 engines: { node: ">=10" }
303 resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
304 engines: {node: '>=10'}
163305
164306 yargs-parser@21.1.1:
165 resolution: {
166 integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==,
167 }
168 engines: { node: ">=12" }
307 resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
308 engines: {node: '>=12'}
169309
170310 yargs@17.7.2:
171 resolution: {
172 integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==,
173 }
174 engines: { node: ">=12" }
311 resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
312 engines: {node: '>=12'}
175313
176314snapshots:
177 "@jsr/clo__lib@3.0.0": {}
178315
179 "@types/node@25.5.0":
316 '@emnapi/runtime@1.11.1':
317 dependencies:
318 tslib: 2.8.1
319 optional: true
320
321 '@img/colour@1.1.0': {}
322
323 '@img/sharp-darwin-arm64@0.35.2':
324 optionalDependencies:
325 '@img/sharp-libvips-darwin-arm64': 1.3.1
326 optional: true
327
328 '@img/sharp-darwin-x64@0.35.2':
329 optionalDependencies:
330 '@img/sharp-libvips-darwin-x64': 1.3.1
331 optional: true
332
333 '@img/sharp-freebsd-wasm32@0.35.2':
334 dependencies:
335 '@img/sharp-wasm32': 0.35.2
336 optional: true
337
338 '@img/sharp-libvips-darwin-arm64@1.3.1':
339 optional: true
340
341 '@img/sharp-libvips-darwin-x64@1.3.1':
342 optional: true
343
344 '@img/sharp-libvips-linux-arm64@1.3.1':
345 optional: true
346
347 '@img/sharp-libvips-linux-arm@1.3.1':
348 optional: true
349
350 '@img/sharp-libvips-linux-ppc64@1.3.1':
351 optional: true
352
353 '@img/sharp-libvips-linux-riscv64@1.3.1':
354 optional: true
355
356 '@img/sharp-libvips-linux-s390x@1.3.1':
357 optional: true
358
359 '@img/sharp-libvips-linux-x64@1.3.1':
360 optional: true
361
362 '@img/sharp-libvips-linuxmusl-arm64@1.3.1':
363 optional: true
364
365 '@img/sharp-libvips-linuxmusl-x64@1.3.1':
366 optional: true
367
368 '@img/sharp-linux-arm64@0.35.2':
369 optionalDependencies:
370 '@img/sharp-libvips-linux-arm64': 1.3.1
371 optional: true
372
373 '@img/sharp-linux-arm@0.35.2':
374 optionalDependencies:
375 '@img/sharp-libvips-linux-arm': 1.3.1
376 optional: true
377
378 '@img/sharp-linux-ppc64@0.35.2':
379 optionalDependencies:
380 '@img/sharp-libvips-linux-ppc64': 1.3.1
381 optional: true
382
383 '@img/sharp-linux-riscv64@0.35.2':
384 optionalDependencies:
385 '@img/sharp-libvips-linux-riscv64': 1.3.1
386 optional: true
387
388 '@img/sharp-linux-s390x@0.35.2':
389 optionalDependencies:
390 '@img/sharp-libvips-linux-s390x': 1.3.1
391 optional: true
392
393 '@img/sharp-linux-x64@0.35.2':
394 optionalDependencies:
395 '@img/sharp-libvips-linux-x64': 1.3.1
396 optional: true
397
398 '@img/sharp-linuxmusl-arm64@0.35.2':
399 optionalDependencies:
400 '@img/sharp-libvips-linuxmusl-arm64': 1.3.1
401 optional: true
402
403 '@img/sharp-linuxmusl-x64@0.35.2':
404 optionalDependencies:
405 '@img/sharp-libvips-linuxmusl-x64': 1.3.1
406 optional: true
407
408 '@img/sharp-wasm32@0.35.2':
409 dependencies:
410 '@emnapi/runtime': 1.11.1
411 optional: true
412
413 '@img/sharp-webcontainers-wasm32@0.35.2':
414 dependencies:
415 '@img/sharp-wasm32': 0.35.2
416 optional: true
417
418 '@img/sharp-win32-arm64@0.35.2':
419 optional: true
420
421 '@img/sharp-win32-ia32@0.35.2':
422 optional: true
423
424 '@img/sharp-win32-x64@0.35.2':
425 optional: true
426
427 '@jsr/clo__lib@3.0.0': {}
428
429 '@mdi/svg@7.4.47': {}
430
431 '@types/node@25.5.0':
180432 dependencies:
181433 undici-types: 7.18.2
182434
183 "@types/w3c-web-usb@1.0.13": {}
435 '@types/w3c-web-usb@1.0.13': {}
184436
185437 ansi-regex@5.0.1: {}
186438
......@@ -200,6 +452,8 @@ snapshots:
200452
201453 color-name@1.1.4: {}
202454
455 detect-libc@2.1.2: {}
456
203457 emoji-regex@8.0.0: {}
204458
205459 escalade@3.2.0: {}
......@@ -208,6 +462,14 @@ snapshots:
208462
209463 is-fullwidth-code-point@3.0.0: {}
210464
465 linq@4.0.3: {}
466
467 lucide-static@1.21.0: {}
468
469 mdi-ts@1.0.3:
470 dependencies:
471 linq: 4.0.3
472
211473 node-addon-api@3.2.1: {}
212474
213475 node-addon-api@8.6.0: {}
......@@ -225,6 +487,40 @@ snapshots:
225487
226488 require-directory@2.1.1: {}
227489
490 semver@7.8.5: {}
491
492 sharp@0.35.2:
493 dependencies:
494 '@img/colour': 1.1.0
495 detect-libc: 2.1.2
496 semver: 7.8.5
497 optionalDependencies:
498 '@img/sharp-darwin-arm64': 0.35.2
499 '@img/sharp-darwin-x64': 0.35.2
500 '@img/sharp-freebsd-wasm32': 0.35.2
501 '@img/sharp-libvips-darwin-arm64': 1.3.1
502 '@img/sharp-libvips-darwin-x64': 1.3.1
503 '@img/sharp-libvips-linux-arm': 1.3.1
504 '@img/sharp-libvips-linux-arm64': 1.3.1
505 '@img/sharp-libvips-linux-ppc64': 1.3.1
506 '@img/sharp-libvips-linux-riscv64': 1.3.1
507 '@img/sharp-libvips-linux-s390x': 1.3.1
508 '@img/sharp-libvips-linux-x64': 1.3.1
509 '@img/sharp-libvips-linuxmusl-arm64': 1.3.1
510 '@img/sharp-libvips-linuxmusl-x64': 1.3.1
511 '@img/sharp-linux-arm': 0.35.2
512 '@img/sharp-linux-arm64': 0.35.2
513 '@img/sharp-linux-ppc64': 0.35.2
514 '@img/sharp-linux-riscv64': 0.35.2
515 '@img/sharp-linux-s390x': 0.35.2
516 '@img/sharp-linux-x64': 0.35.2
517 '@img/sharp-linuxmusl-arm64': 0.35.2
518 '@img/sharp-linuxmusl-x64': 0.35.2
519 '@img/sharp-webcontainers-wasm32': 0.35.2
520 '@img/sharp-win32-arm64': 0.35.2
521 '@img/sharp-win32-ia32': 0.35.2
522 '@img/sharp-win32-x64': 0.35.2
523
228524 string-width@4.2.3:
229525 dependencies:
230526 emoji-regex: 8.0.0
......@@ -235,11 +531,14 @@ snapshots:
235531 dependencies:
236532 ansi-regex: 5.0.1
237533
534 tslib@2.8.1:
535 optional: true
536
238537 undici-types@7.18.2: {}
239538
240539 usb@2.17.0:
241540 dependencies:
242 "@types/w3c-web-usb": 1.0.13
541 '@types/w3c-web-usb': 1.0.13
243542 node-addon-api: 8.6.0
244543 node-gyp-build: 4.8.4
245544
readme.md+3-4
......@@ -15,17 +15,16 @@ other configurations.
1515
1616## Hardware
1717
18TODO:
18### DaVinci Resolve Speed Editor
1919
20- generic keyboard / karabiner elements integration?
21- custom trackpad integration?
20A $200 dual-hardware system.
2221
2322### DaVinci Resolve Speed Editor
2423
2524A $300 bundle containing Fusion Studio and the control surface, it's a great
2625deal. There is a large, high resolution knob, as well as many keys with some
2726having lights. This repo includes an SDK to reprogram it to be useful in any
28program.
27program. **Note**: It is unused as of 2026-06-24.
2928
3029![DaVinci Resolve Speed Editor](docs/speed-editor.jpg)
3130
readme.new.md created+3
......@@ -0,0 +1,3 @@
1# Creative Toolkit
2
3Clover's **Creative Toolkit** is collection of macOS software that she use to create the projects seen on [paper clover](https://paperclover.net).
\ No newline at end of file
src/Dialpad.ts created+208
......@@ -0,0 +1,208 @@
1import { Events } from "@clo/lib/Events.ts";
2import type { Dispose } from "@clo/lib/ts.ts";
3
4// The MX Creative Console Dialpad pairs over Bluetooth as a Logitech HID++
5// device. It does NOT need HID++ feature access: it streams a single 8-byte
6// input report (id 0x02) that decodes cleanly. Discovered by sniffing — see
7// examples/hid-sniff.ts.
8//
9// 02 buttons -- -- -- -- spin rotate
10// b1 b6 b7
11//
12// `rotate` (main dial) and `spin` (knob) are signed int8 deltas; `buttons` is a
13// bitmask. NOTE: until the OS-seize phase, macOS also consumes these reports
14// (the dial scrolls, the buttons act as mouse buttons).
15const LOGITECH_VENDOR_ID = 0x046d;
16const DIALPAD_PRODUCT_ID = 0xbc00;
17
18const REPORT_ID = 0x02;
19const BUTTON_BYTE = 1;
20const SPIN_BYTE = 6;
21const ROTATE_BYTE = 7;
22
23const buttonIds = ["circle", "triangle", "square", "cross"] as const;
24
25const BUTTON_BIT_BY_ID = new Map<Dialpad.Button, number>([
26 ["square", 0x08],
27 ["cross", 0x10],
28 ["circle", 0x20],
29 ["triangle", 0x40],
30]);
31
32const RECONNECT_INTERVAL_MS = 1000;
33
34/**
35 * Node.js bindings for the Logitech MX Creative Console Dialpad (Bluetooth).
36 */
37export class Dialpad extends Events<Dialpad.EventMap> {
38 static buttons = buttonIds;
39
40 #options: Required<Dialpad.Options>;
41 #device: import("node-hid").HID | null = null;
42 #closed = false;
43 #ready = false;
44 #reconnectTimer: ReturnType<typeof setInterval> | null = null;
45 #activeButtons = new Set<Dialpad.Button>();
46 #lastButtonMask = 0;
47
48 private constructor(options: Dialpad.Options = {}) {
49 super();
50 this.#options = {
51 vendorId: options.vendorId ?? LOGITECH_VENDOR_ID,
52 productId: options.productId ?? DIALPAD_PRODUCT_ID,
53 path: options.path ?? null,
54 };
55 }
56
57 static async open(options: Dialpad.Options = {}) {
58 const dialpad = new Dialpad(options);
59 await dialpad.#start();
60 return dialpad;
61 }
62
63 get connected(): boolean {
64 return this.#ready;
65 }
66
67 onPress(button: Dialpad.Button, listener: () => void): Dispose {
68 return this.on("keypress", (code) => {
69 if (button === code) listener();
70 });
71 }
72
73 close() {
74 if (this.#closed) return;
75 this.#closed = true;
76 if (this.#reconnectTimer) clearInterval(this.#reconnectTimer);
77 this.#reconnectTimer = null;
78 this.#disconnect(false);
79 this.emit("close");
80 }
81
82 async #start() {
83 await this.#connect();
84 // Bluetooth devices don't raise `usb` hotplug events, so poll instead.
85 this.#reconnectTimer = setInterval(() => {
86 if (!this.#device && !this.#closed) void this.#connect();
87 }, RECONNECT_INTERVAL_MS);
88 this.#reconnectTimer.unref?.();
89 }
90
91 async #connect() {
92 if (this.#device || this.#closed) return;
93
94 const { devices, HID } = await import("node-hid");
95 const match = devices().find(
96 (device) =>
97 device.vendorId === this.#options.vendorId
98 && device.productId === this.#options.productId
99 && (this.#options.path ? device.path === this.#options.path : true)
100 && Boolean(device.path),
101 );
102 if (!match?.path) return;
103
104 try {
105 const device = new HID(match.path);
106 this.#device = device;
107 device.on("data", (report) => {
108 if (this.#device === device) this.#handleReport(report);
109 });
110 device.on("error", (error) => {
111 if (this.#device === device) this.#handleDeviceError(error);
112 });
113 this.#ready = true;
114 this.emit("connect");
115 } catch {
116 this.#device = null;
117 // Will retry on the next poll tick.
118 }
119 }
120
121 #handleReport(report: Buffer | number[]) {
122 const bytes = Uint8Array.from(report);
123 if (bytes[0] !== REPORT_ID) return;
124
125 const rotate = toInt8(bytes[ROTATE_BYTE] ?? 0);
126 if (rotate !== 0) this.emit("rotate", rotate);
127
128 const spin = toInt8(bytes[SPIN_BYTE] ?? 0);
129 if (spin !== 0) this.emit("spin", spin);
130
131 const mask = bytes[BUTTON_BYTE] ?? 0;
132 if (mask !== this.#lastButtonMask) {
133 this.#lastButtonMask = mask;
134 this.#applyButtonState(mask);
135 }
136 }
137
138 #applyButtonState(mask: number) {
139 const next = new Set<Dialpad.Button>();
140 for (const [button, bit] of BUTTON_BIT_BY_ID) {
141 if (mask & bit) next.add(button);
142 }
143
144 for (const button of this.#activeButtons) {
145 if (!next.has(button)) this.emit("keyup", button);
146 }
147 for (const button of next) {
148 if (!this.#activeButtons.has(button)) {
149 this.emit("keydown", button);
150 this.emit("keypress", button);
151 }
152 }
153
154 this.#activeButtons = next;
155 this.emit("key", [...next]);
156 }
157
158 #handleDeviceError(_error: unknown) {
159 this.#disconnect(true);
160 }
161
162 #disconnect(emitEvent: boolean) {
163 const device = this.#device;
164 this.#device = null;
165 this.#ready = false;
166 this.#activeButtons.clear();
167 this.#lastButtonMask = 0;
168 if (device) {
169 device.removeAllListeners("data");
170 device.removeAllListeners("error");
171 try {
172 device.close();
173 } catch {
174 // Ignore close races when the device disappears mid-reconnect.
175 }
176 }
177 if (emitEvent) this.emit("disconnect");
178 }
179}
180
181function toInt8(byte: number): number {
182 return byte > 127 ? byte - 256 : byte;
183}
184
185export declare namespace Dialpad {
186 export type Button = typeof buttonIds[number];
187
188 export interface Options {
189 vendorId?: number;
190 productId?: number;
191 path?: string | null;
192 }
193
194 export type EventMap = {
195 "connect": [];
196 "disconnect": [];
197 "close": [];
198 "error": [error: unknown];
199 /** Main dial delta (signed, clockwise positive). */
200 "rotate": [delta: number];
201 /** Up/down knob delta (signed, up positive). */
202 "spin": [delta: number];
203 "key": [activeButtons: ReadonlyArray<Button>];
204 "keydown": [button: Button];
205 "keyup": [button: Button];
206 "keypress": [button: Button];
207 };
208}
src/Keypad.ts created+397
......@@ -0,0 +1,397 @@
1import { Events } from "@clo/lib/Events.ts";
2import type { Dispose } from "@clo/lib/ts.ts";
3
4// Node.js bindings for the Logitech MX Creative Keypad — the 3x3 LCD grid plus
5// the two screenless buttons below it. Talks the device's HID protocol directly
6// (no vendor SDK). Protocol cross-referenced from the Stream-Deck-style wire
7// format the hardware uses:
8// - input report 0x13: grid buttons (hidId = index + 1, int8 list from off 5)
9// - input report 0x11: back/forward (hidId 0x01a1/0x01a2, uint16 BE from off 3)
10// - output report 0x14: image data, 4095-byte packets with a positioned header
11// - output report 0x11: brightness (0x11 ff 0f 2b 00 <pct>)
12// - feature report 0x03: reset to logo
13const LOGITECH_VENDOR_ID = 0x046d;
14const KEYPAD_PRODUCT_ID = 0xc354;
15
16const KEY_SIZE = 118;
17// Each grid key writes a sub-rect of the panel framebuffer. Positions are
18// offset (23, 6) with a 158px pitch (118px key + 40px gap).
19const GRID_OFFSET = { x: 23, y: 6 };
20const GRID_PITCH = KEY_SIZE + 40;
21
22const NAME_BY_INDEX = [
23 "up-left",
24 "up",
25 "up-right",
26 "left",
27 "center",
28 "right",
29 "down-left",
30 "down",
31 "down-right",
32 "back",
33 "forward",
34] as const;
35
36const LCD_KEYS = NAME_BY_INDEX.slice(0, 9) as readonly Keypad.Key[];
37const INDEX_BY_NAME = new Map<Keypad.Key, number>(
38 NAME_BY_INDEX.map((name, index) => [name, index]),
39);
40
41const KEY_POSITION = LCD_KEYS.map((_, index) => ({
42 x: GRID_OFFSET.x + (index % 3) * GRID_PITCH,
43 y: GRID_OFFSET.y + Math.floor(index / 3) * GRID_PITCH,
44}));
45
46const PANEL_SIZE = 480;
47
48/** Grid key (x,y) positions within the 480x480 panel framebuffer, row-major. */
49export const KEY_POSITIONS: ReadonlyArray<{ x: number; y: number }> = KEY_POSITION;
50/** Full panel pixel size (square). */
51export const PANEL_SIZE_PX = PANEL_SIZE;
52
53// Input hidId -> key name. Grid keys use hidId = index + 1; the two page buttons
54// report 16-bit ids.
55const NAME_BY_HID = new Map<number, Keypad.Key>(
56 LCD_KEYS.map((name, index) => [index + 1, name]),
57);
58NAME_BY_HID.set(0x01a1, "back");
59NAME_BY_HID.set(0x01a2, "forward");
60
61// Sent on connect so the back/forward buttons emit raw HID events.
62const INIT_WRITES = [0x01a1, 0x01a2].map((hidId) => {
63 const buffer = Buffer.alloc(20);
64 buffer.set([0x11, 0xff, 0x0b, 0x3b, (hidId >> 8) & 0xff, hidId & 0xff, 0x03]);
65 return buffer;
66});
67
68const IMAGE_REPORT_ID = 0x14;
69const MAX_PACKET_SIZE = 4095;
70const PACKET1_HEADER = 20;
71const PACKETN_HEADER = 5;
72
73const RECONNECT_INTERVAL_MS = 1000;
74
75export class Keypad extends Events<Keypad.EventMap> {
76 static keys = NAME_BY_INDEX;
77 static lcdKeys = LCD_KEYS;
78
79 #device: import("node-hid").HID | null = null;
80 #closed = false;
81 #ready = false;
82 #reconnectTimer: ReturnType<typeof setInterval> | null = null;
83 #images = new Map<Keypad.Key, Uint8Array>();
84 #shown = new Map<Keypad.Key, Uint8Array>();
85 #panel: Uint8Array | null = null;
86 #shownPanel: Uint8Array | null = null;
87 #gridDown = new Set<Keypad.Key>();
88 #pageDown = new Set<Keypad.Key>();
89
90 private constructor() {
91 super();
92 }
93
94 static async open() {
95 const keypad = new Keypad();
96 await keypad.#start();
97 return keypad;
98 }
99
100 get connected(): boolean {
101 return this.#ready;
102 }
103
104 onPress(key: Keypad.Key, listener: () => void): Dispose {
105 return this.on("keypress", (code) => {
106 if (key === code) listener();
107 });
108 }
109
110 /**
111 * Show a pre-encoded JPEG on a grid key. The caller owns encoding (see
112 * KeypadUI); identical buffers are deduped so unchanged keys never re-send.
113 */
114 setImage(key: Keypad.Key, image: Uint8Array) {
115 this.#images.set(key, image);
116 this.#panel = null; // a per-key image supersedes any full-panel image
117 if (this.#shown.get(key) === image) return; // already on screen — cached
118 if (this.#writeImage(key, image)) {
119 this.#shown.set(key, image);
120 this.#shownPanel = null;
121 }
122 }
123
124 /**
125 * Show one composed image across the whole 480x480 panel as a single
126 * image-write. The device repaints every key in one refresh — no per-key
127 * cascade. Identical buffers are deduped.
128 */
129 setPanel(image: Uint8Array) {
130 this.#panel = image;
131 this.#images.clear(); // a full-panel image supersedes per-key images
132 if (this.#shownPanel === image) return; // already on screen — cached
133 if (this.#writeRegion(0, 0, PANEL_SIZE, PANEL_SIZE, image)) {
134 this.#shownPanel = image;
135 this.#shown.clear();
136 }
137 }
138
139 /** Brightness as 0..1. */
140 setBrightness(level: number) {
141 const percentage = Math.max(
142 1,
143 Math.min(100, Math.round(Math.max(0, Math.min(1, level)) * 100)),
144 );
145 const command = Buffer.alloc(20);
146 command.set([0x11, 0xff, 0x0f, 0x2b, 0x00, percentage]);
147 this.#write(command);
148 }
149
150 /** Reset all screens to the startup logo. */
151 reset() {
152 this.#shown.clear();
153 this.#shownPanel = null;
154 const command = Buffer.alloc(32);
155 command.set([0x03, 0x02]);
156 try {
157 this.#device?.sendFeatureReport(command);
158 } catch {
159 // Ignore if the device vanished.
160 }
161 }
162
163 close() {
164 if (this.#closed) return;
165 this.#closed = true;
166 if (this.#reconnectTimer) clearInterval(this.#reconnectTimer);
167 this.#reconnectTimer = null;
168 this.#disconnect(false);
169 this.emit("close");
170 }
171
172 async #start() {
173 await this.#connect();
174 // The keypad pairs over Bluetooth too, where `usb` hotplug is silent — poll.
175 this.#reconnectTimer = setInterval(() => {
176 if (!this.#device && !this.#closed) void this.#connect();
177 }, RECONNECT_INTERVAL_MS);
178 this.#reconnectTimer.unref?.();
179 }
180
181 async #connect() {
182 if (this.#device || this.#closed) return;
183
184 const { devices, HID } = await import("node-hid");
185 const match = devices().find(
186 (device) =>
187 device.vendorId === LOGITECH_VENDOR_ID &&
188 device.productId === KEYPAD_PRODUCT_ID &&
189 Boolean(device.path),
190 );
191 if (!match?.path) return;
192
193 try {
194 const device = new HID(match.path);
195 this.#device = device;
196 device.on("data", (report) => {
197 if (this.#device === device) this.#handleReport(report);
198 });
199 device.on("error", () => {
200 if (this.#device === device) this.#handleDeviceError();
201 });
202
203 for (const write of INIT_WRITES) device.write(write);
204 this.#ready = true;
205 this.emit("connect");
206 this.#reapplyImages();
207 } catch {
208 this.#device = null;
209 // Retry on the next poll tick.
210 }
211 }
212
213 #handleReport(report: Buffer | number[]) {
214 const buffer = Buffer.isBuffer(report) ? report : Buffer.from(report);
215 const reportId = buffer[0];
216 const data = buffer.subarray(1);
217 if (data[2] === 0x2b) return; // ack to a drawing write
218
219 if (reportId === 0x13) this.#handleGridInput(data);
220 else if (reportId === 0x11) this.#handlePageInput(data);
221 }
222
223 #handleGridInput(data: Buffer) {
224 if (data[0] !== 0xff || data[1] !== 0x02 || data[2] !== 0x00 || data[4] !== 0x01) {
225 return;
226 }
227 const pressed = new Set<Keypad.Key>();
228 for (let i = 5; i < data.length; i += 1) {
229 const value = data.readInt8(i);
230 if (value === 0) break;
231 const key = NAME_BY_HID.get(value);
232 if (key) pressed.add(key);
233 }
234 this.#applyPressed(pressed, this.#gridDown);
235 }
236
237 #handlePageInput(data: Buffer) {
238 if (data[0] !== 0xff || data[1] !== 0x0b || data[2] !== 0x00) return;
239 const pressed = new Set<Keypad.Key>();
240 for (let i = 3; i + 1 < data.length; i += 2) {
241 const value = data.readUInt16BE(i);
242 if (value === 0) break;
243 const key = NAME_BY_HID.get(value);
244 if (key) pressed.add(key);
245 }
246 this.#applyPressed(pressed, this.#pageDown);
247 }
248
249 #applyPressed(pressed: Set<Keypad.Key>, downSet: Set<Keypad.Key>) {
250 for (const key of downSet) {
251 if (!pressed.has(key)) {
252 downSet.delete(key);
253 this.emit("keyup", key);
254 }
255 }
256 for (const key of pressed) {
257 if (!downSet.has(key)) {
258 downSet.add(key);
259 this.emit("keydown", key);
260 this.emit("keypress", key);
261 }
262 }
263 }
264
265 #writeImage(key: Keypad.Key, image: Uint8Array): boolean {
266 const index = INDEX_BY_NAME.get(key);
267 if (index === undefined || index >= LCD_KEYS.length) return false;
268 const position = KEY_POSITION[index];
269 return this.#writeRegion(position.x, position.y, KEY_SIZE, KEY_SIZE, image);
270 }
271
272 #writeRegion(
273 x: number,
274 y: number,
275 width: number,
276 height: number,
277 image: Uint8Array,
278 ): boolean {
279 if (!this.#device) return false;
280 try {
281 for (const packet of packetizeImage(x, y, width, height, image)) {
282 this.#device.write(packet);
283 }
284 return true;
285 } catch {
286 return false;
287 }
288 }
289
290 #reapplyImages() {
291 this.#shown.clear();
292 this.#shownPanel = null;
293 if (this.#panel) {
294 if (this.#writeRegion(0, 0, PANEL_SIZE, PANEL_SIZE, this.#panel)) {
295 this.#shownPanel = this.#panel;
296 }
297 return;
298 }
299 for (const [key, image] of this.#images) {
300 if (this.#writeImage(key, image)) this.#shown.set(key, image);
301 }
302 }
303
304 #write(buffer: Buffer) {
305 try {
306 this.#device?.write(buffer);
307 } catch {
308 // Ignore if the device vanished.
309 }
310 }
311
312 #handleDeviceError() {
313 this.#disconnect(true);
314 }
315
316 #disconnect(emitEvent: boolean) {
317 const device = this.#device;
318 this.#device = null;
319 this.#ready = false;
320 this.#shown.clear();
321 this.#shownPanel = null;
322 this.#gridDown.clear();
323 this.#pageDown.clear();
324 if (device) {
325 device.removeAllListeners("data");
326 device.removeAllListeners("error");
327 try {
328 device.close();
329 } catch {
330 // Ignore close races when the device disappears mid-reconnect.
331 }
332 }
333 if (emitEvent) this.emit("disconnect");
334 }
335}
336
337/** Split a JPEG into the keypad's positioned image-write packets. */
338function packetizeImage(
339 x: number,
340 y: number,
341 width: number,
342 height: number,
343 jpeg: Uint8Array,
344): Buffer[] {
345 const packets: Buffer[] = [];
346 const total = jpeg.length;
347
348 const first = Buffer.alloc(MAX_PACKET_SIZE);
349 const firstBytes = Math.min(total, MAX_PACKET_SIZE - PACKET1_HEADER);
350 first.set([IMAGE_REPORT_ID, 0xff, 0x02, 0x2b]);
351 first[4] = packetByte(1, true, firstBytes >= total);
352 first.writeUInt16BE(0x0100, 5);
353 first.writeUInt16BE(0x0100, 7);
354 first.writeUInt16BE(x, 9);
355 first.writeUInt16BE(y, 11);
356 first.writeUInt16BE(width, 13);
357 first.writeUInt16BE(height, 15);
358 first.writeUInt16BE(total, 18);
359 first.set(jpeg.subarray(0, firstBytes), PACKET1_HEADER);
360 packets.push(first);
361
362 let remaining = total - firstBytes;
363 let part = 2;
364 while (remaining > 0) {
365 const packet = Buffer.alloc(MAX_PACKET_SIZE);
366 const bytes = Math.min(remaining, MAX_PACKET_SIZE - PACKETN_HEADER);
367 const offset = total - remaining;
368 packet.set([IMAGE_REPORT_ID, 0xff, 0x02, 0x2b]);
369 packet[4] = packetByte(part, false, remaining - bytes === 0);
370 packet.set(jpeg.subarray(offset, offset + bytes), PACKETN_HEADER);
371 packets.push(packet);
372 remaining -= bytes;
373 part += 1;
374 }
375 return packets;
376}
377
378function packetByte(index: number, isFirst: boolean, isLast: boolean): number {
379 let value = index | 0b0010_0000;
380 if (isFirst) value |= 0b1000_0000;
381 if (isLast) value |= 0b0100_0000;
382 return value;
383}
384
385export declare namespace Keypad {
386 export type Key = typeof NAME_BY_INDEX[number];
387
388 export type EventMap = {
389 "connect": [];
390 "disconnect": [];
391 "close": [];
392 "error": [error: unknown];
393 "keydown": [key: Key];
394 "keyup": [key: Key];
395 "keypress": [key: Key];
396 };
397}
src/KeypadUI.ts
Binary files /dev/null and b/src/KeypadUI.ts differ
src/Mac.ts+18-20
......@@ -32,7 +32,7 @@ const FRONTMOST_APP_HELPER_FRAMEWORKS = [
3232 "ApplicationServices",
3333] as const;
3434const KEYBOARD_EVENT_SCRIPT = [
35 'ObjC.import("ApplicationServices");',
35 "ObjC.import(\"ApplicationServices\");",
3636 "function run(argv) {",
3737 " const payload = JSON.parse(argv[0] ?? '{}');",
3838 " const actions = Array.isArray(payload.actions) ? payload.actions : [];",
......@@ -51,7 +51,7 @@ const KEYBOARD_EVENT_SCRIPT = [
5151 " $.CGEventPost($.kCGHIDEventTap, event);",
5252 " $.CFRelease(event);",
5353 " }",
54 ' return "";',
54 " return \"\";",
5555 "}",
5656].join("\n");
5757let frontmostAppHelperBinaryPromise: Promise<string> | null = null;
......@@ -709,9 +709,7 @@ export declare namespace Mac {
709709}
710710
711711function parseFrontmostState(stdout: string): FrontmostState {
712 const line = stdout.split(/\r?\n/u).find((candidate) =>
713 candidate.trim() !== ""
714 );
712 const line = stdout.split(/\r?\n/u).find((candidate) => candidate.trim() !== "");
715713 return parseFrontmostStateLine(line ?? "");
716714}
717715
......@@ -742,12 +740,12 @@ function parseFrontmostStateLine(line: string): FrontmostState {
742740
743741function normalizeBundleId(value: unknown) {
744742 const directBundleId = typeof value === "string" ? value.trim() : "";
745 const bundleId = BUNDLE_ID_PATTERN.exec(directBundleId)?.[0] ??
746 directBundleId;
743 const bundleId = BUNDLE_ID_PATTERN.exec(directBundleId)?.[0]
744 ?? directBundleId;
747745 if (
748 bundleId === "" ||
749 bundleId === "undefined" ||
750 bundleId === "[id nil]"
746 bundleId === ""
747 || bundleId === "undefined"
748 || bundleId === "[id nil]"
751749 ) {
752750 return null;
753751 }
......@@ -847,10 +845,10 @@ function windowEquals(left: Mac.Window | null, right: Mac.Window | null) {
847845 if (!left || !right) {
848846 return left === right;
849847 }
850 return left.id === right.id &&
851 left.title === right.title &&
852 left.main === right.main &&
853 left.focused === right.focused;
848 return left.id === right.id
849 && left.title === right.title
850 && left.main === right.main
851 && left.focused === right.focused;
854852}
855853
856854function normalizeDelayMs(value: number, name: string) {
......@@ -894,9 +892,9 @@ function resolveKeyCode(key: Mac.Key): number {
894892
895893function normalizeKeyCode(keyCode: number) {
896894 if (
897 !Number.isInteger(keyCode) ||
898 keyCode < 0 ||
899 keyCode > 0xFFFF
895 !Number.isInteger(keyCode)
896 || keyCode < 0
897 || keyCode > 0xFFFF
900898 ) {
901899 throw new Error(`Mac keyCode must be an integer between 0 and 65535`);
902900 }
......@@ -956,9 +954,9 @@ function formatKeyboardDispatchError(error: unknown) {
956954 const details = extractCommandErrorOutput(error);
957955 const suffix = details ? ` ${details}` : "";
958956 return (
959 "Failed to send a macOS keyboard event via osascript. " +
960 "Make sure this process is allowed in System Settings > Privacy & Security > Accessibility." +
961 suffix
957 "Failed to send a macOS keyboard event via osascript. "
958 + "Make sure this process is allowed in System Settings > Privacy & Security > Accessibility."
959 + suffix
962960 );
963961}
964962
src/Reaper.ts+122-23
......@@ -2,8 +2,8 @@ import { Events } from "@clo/lib/Events.ts";
22import * as log from "@clo/lib/log.ts";
33import { execFile } from "node:child_process";
44import { createSocket, type Socket } from "node:dgram";
5import { readFileSync } from "node:fs";
6import { cp, mkdir, writeFile } from "node:fs/promises";
5import { type FSWatcher, readFileSync, watch } from "node:fs";
6import { cp, mkdir, readFile, writeFile } from "node:fs/promises";
77import { basename, dirname, join } from "node:path";
88import process from "node:process";
99import { fileURLToPath } from "node:url";
......@@ -29,6 +29,10 @@ export interface ReaperTransportState {
2929 positionSeconds: number;
3030 positionString: string;
3131 positionBeatsString: string;
32 /** Project tempo in BPM (live, from the feedback script). */
33 tempo: number;
34 /** Project time signature as "num/denom" (live, from the feedback script). */
35 timeSignature: string;
3236 readAtMs: number;
3337 source: "osc" | "optimistic";
3438}
......@@ -94,14 +98,19 @@ const DEFAULT_REAPER_OSC_TARGET_DIR = join(
9498 DEFAULT_REAPER_RESOURCE_DIR,
9599 "OSC",
96100);
97const REAPER_SCRIPT_TARGET_DIR = process.env.REAPER_SCRIPTS_DIR ??
98 DEFAULT_REAPER_SCRIPT_TARGET_DIR;
99const REAPER_OSC_TARGET_DIR = process.env.REAPER_OSC_DIR ??
100 DEFAULT_REAPER_OSC_TARGET_DIR;
101const REAPER_SCRIPT_TARGET_DIR = process.env.REAPER_SCRIPTS_DIR
102 ?? DEFAULT_REAPER_SCRIPT_TARGET_DIR;
103// The feedback script writes live tempo/time-signature here (next to itself);
104// we watch the file for changes. See config/reaper/scripts/clover_feedback.lua.
105const REAPER_FEEDBACK_DIR = join(REAPER_SCRIPT_TARGET_DIR, "scripts");
106const REAPER_FEEDBACK_FILE = "state.json";
107const REAPER_FEEDBACK_STATE_PATH = join(REAPER_FEEDBACK_DIR, REAPER_FEEDBACK_FILE);
108const REAPER_FEEDBACK_SCRIPT = "clover_feedback";
109const REAPER_OSC_TARGET_DIR = process.env.REAPER_OSC_DIR
110 ?? DEFAULT_REAPER_OSC_TARGET_DIR;
101111const OSC_PATTERN_FILE = "CloverAutomation.ReaperOSC";
102112const OSC_PATTERN_NAME = stripReaperOscExtension(OSC_PATTERN_FILE);
103const OSC_PATTERN_CONFIG =
104 `# OSC pattern config file for Clover Creative Control's REAPER integration.
113const OSC_PATTERN_CONFIG = `# OSC pattern config file for Clover Creative Control's REAPER integration.
105114DEVICE_TRACK_COUNT 1
106115DEVICE_SEND_COUNT 0
107116DEVICE_RECEIVE_COUNT 0
......@@ -150,6 +159,8 @@ export class Reaper extends Events<Reaper.EventMap> {
150159 #receiveSocket: Socket;
151160 #sendSocket: Socket;
152161 #closed = false;
162 #feedbackWatcher: FSWatcher | null = null;
163 #feedbackLaunched = false;
153164
154165 constructor(options: ReaperOptions = {}) {
155166 super();
......@@ -163,6 +174,7 @@ export class Reaper extends Events<Reaper.EventMap> {
163174 void this.#installOscPatternConfig().catch((error) => {
164175 this.#logOscPatternInstallError(error);
165176 });
177 void this.#startFeedback();
166178 }
167179
168180 get transport(): ReaperTransportState {
......@@ -175,10 +187,10 @@ export class Reaper extends Events<Reaper.EventMap> {
175187 }
176188
177189 console.info(
178 `${OSC_PATTERN_FILE} is not registered yet. ` +
179 `Add an OSC control surface manually using the "${OSC_PATTERN_NAME}" pattern, set REAPER's local listen port to ` +
180 `${this.#oscPort}, and send to ${this.#oscHost}:${this.#oscBindPort}. ` +
181 "The REAPER web interface is not required for this config.",
190 `${OSC_PATTERN_FILE} is not registered yet. `
191 + `Add an OSC control surface manually using the "${OSC_PATTERN_NAME}" pattern, set REAPER's local listen port to `
192 + `${this.#oscPort}, and send to ${this.#oscHost}:${this.#oscBindPort}. `
193 + "The REAPER web interface is not required for this config.",
182194 );
183195 }
184196
......@@ -190,6 +202,9 @@ export class Reaper extends Events<Reaper.EventMap> {
190202 this.#closed = true;
191203 this.#clearScrubTimer();
192204
205 this.#feedbackWatcher?.close();
206 this.#feedbackWatcher = null;
207
193208 this.#receiveSocket.removeAllListeners();
194209 this.#sendSocket.removeAllListeners();
195210 closeSocket(this.#receiveSocket);
......@@ -281,6 +296,57 @@ export class Reaper extends Events<Reaper.EventMap> {
281296 );
282297 }
283298
299 // Live tempo + time signature come from a deferred REAPER Lua script that
300 // writes them to a JSON file whenever they change; we watch that file. This
301 // covers what OSC can't (REAPER has no time-signature feedback token).
302 async #startFeedback() {
303 try {
304 await this.#ensureManagedScripts();
305 await this.#readFeedbackState();
306 this.#watchFeedbackState();
307 await this.#launchFeedbackScript();
308 } catch (error) {
309 this.#logFeedbackError(error);
310 }
311 }
312
313 #watchFeedbackState() {
314 if (this.#feedbackWatcher || this.#closed) return;
315 try {
316 const watcher = watch(REAPER_FEEDBACK_DIR, (_event, filename) => {
317 if (!filename || filename === REAPER_FEEDBACK_FILE) {
318 void this.#readFeedbackState();
319 }
320 });
321 watcher.on("error", () => {});
322 watcher.unref?.();
323 this.#feedbackWatcher = watcher;
324 } catch {
325 // Directory may not be watchable; the periodic writes still land via reads.
326 }
327 }
328
329 async #readFeedbackState() {
330 let raw: string;
331 try {
332 raw = await readFile(REAPER_FEEDBACK_STATE_PATH, "utf8");
333 } catch {
334 return; // not written yet
335 }
336 const patch = parseFeedbackState(raw);
337 if (patch) {
338 this.#updateTransport(patch, "osc");
339 }
340 }
341
342 async #launchFeedbackScript() {
343 if (this.#feedbackLaunched || this.#closed) return;
344 // Only start it once REAPER is actually up, so we don't log a spurious error.
345 if (!await isProcessRunning(reaperProcessName())) return;
346 this.#feedbackLaunched = true;
347 await this.runScript(REAPER_FEEDBACK_SCRIPT);
348 }
349
284350 async #runManagedScript(name: ReaperScriptName) {
285351 if (!await isProcessRunning(reaperProcessName())) {
286352 throw new Error(
......@@ -319,6 +385,10 @@ export class Reaper extends Events<Reaper.EventMap> {
319385 if (!this.#receivedOscFeedback) {
320386 this.#receivedOscFeedback = true;
321387 this.emit("osc-feedback");
388 // REAPER is confirmed up — (re)start the feedback script if we hadn't yet.
389 void this.#launchFeedbackScript().catch((error) => {
390 this.#logFeedbackError(error);
391 });
322392 }
323393
324394 this.#updateTransport(patch, "osc");
......@@ -445,6 +515,13 @@ export class Reaper extends Events<Reaper.EventMap> {
445515 );
446516 }
447517
518 #logFeedbackError(error: unknown) {
519 this.#logError(
520 `Failed to start live tempo/time-signature feedback (${REAPER_FEEDBACK_STATE_PATH}).`,
521 error,
522 );
523 }
524
448525 #logError(message: string, error: unknown) {
449526 console.error(`[REAPER] ${message}`);
450527 console.error(`[REAPER] ${formatError(error)}`);
......@@ -467,6 +544,8 @@ function blankTransportState(): ReaperTransportState {
467544 positionSeconds: 0,
468545 positionString: "",
469546 positionBeatsString: "",
547 tempo: 120,
548 timeSignature: "4/4",
470549 readAtMs: 0,
471550 source: "optimistic",
472551 };
......@@ -476,13 +555,33 @@ function sameTransportState(
476555 left: ReaperTransportState,
477556 right: ReaperTransportState,
478557) {
479 return left.playing === right.playing &&
480 left.paused === right.paused &&
481 left.recording === right.recording &&
482 left.repeatOn === right.repeatOn &&
483 left.positionSeconds === right.positionSeconds &&
484 left.positionString === right.positionString &&
485 left.positionBeatsString === right.positionBeatsString;
558 return left.playing === right.playing
559 && left.paused === right.paused
560 && left.recording === right.recording
561 && left.repeatOn === right.repeatOn
562 && left.positionSeconds === right.positionSeconds
563 && left.positionString === right.positionString
564 && left.positionBeatsString === right.positionBeatsString
565 && left.tempo === right.tempo
566 && left.timeSignature === right.timeSignature;
567}
568
569/** Parse the feedback script's `{ "tempo": <bpm>, "timesig": "n/d" }` payload. */
570function parseFeedbackState(raw: string): ReaperTransportPatch | null {
571 let data: { tempo?: unknown; timesig?: unknown };
572 try {
573 data = JSON.parse(raw);
574 } catch {
575 return null;
576 }
577 const patch: ReaperTransportPatch = {};
578 if (typeof data.tempo === "number" && Number.isFinite(data.tempo)) {
579 patch.tempo = data.tempo;
580 }
581 if (typeof data.timesig === "string" && data.timesig.length > 0) {
582 patch.timeSignature = data.timesig;
583 }
584 return Object.keys(patch).length > 0 ? patch : null;
486585}
487586
488587function optimisticTransportPatchForAction(
......@@ -614,8 +713,8 @@ function hasManagedOscSurface(config: string) {
614713
615714 const normalizedTokens = tokens.map((token) => unquoteReaperToken(token));
616715 if (
617 normalizedTokens.includes(OSC_PATTERN_FILE) ||
618 normalizedTokens.includes(OSC_PATTERN_NAME)
716 normalizedTokens.includes(OSC_PATTERN_FILE)
717 || normalizedTokens.includes(OSC_PATTERN_NAME)
619718 ) {
620719 return true;
621720 }
......@@ -644,8 +743,8 @@ function formatError(error: unknown) {
644743
645744function unquoteReaperToken(token: string) {
646745 if (
647 (token.startsWith("'") && token.endsWith("'")) ||
648 (token.startsWith('"') && token.endsWith('"'))
746 (token.startsWith("'") && token.endsWith("'"))
747 || (token.startsWith("\"") && token.endsWith("\""))
649748 ) {
650749 return token.slice(1, -1);
651750 }
src/Reaper/actions.ts+715-1430
......@@ -41,8 +41,7 @@ export const REAPER_ACTIONS = {
4141 "action-momentarily-send-next-action-to-project-tab-n-8": 3040,
4242 "action-momentarily-send-next-action-to-project-tab-n-9": 3041,
4343 "action-prompt-to-continue-only-valid-within-custom-actions": 2000,
44 "action-prompt-to-go-to-action-loop-start-only-valid-within-custom-actions":
45 2002,
44 "action-prompt-to-go-to-action-loop-start-only-valid-within-custom-actions": 2002,
4645 "action-repeat-the-action-prior-to-the-most-recent-action": 3000,
4746 "action-repeat-the-most-recent-action": 2999,
4847 "action-set-action-loop-start-only-valid-within-custom-actions": 2001,
......@@ -52,8 +51,7 @@ export const REAPER_ACTIONS = {
5251 "action-skip-next-action-if-cc-parameter-0-mid-2016": 2016,
5352 "action-skip-next-action-if-cc-parameter-0-mid-2017": 2017,
5453 "action-skip-next-action-if-cc-parameter-0-mid-2018": 2018,
55 "action-skip-next-action-set-cc-parameter-to-relative-plus-1-if-action-armed-0-otherwise":
56 2023,
54 "action-skip-next-action-set-cc-parameter-to-relative-plus-1-if-action-armed-0-otherwise": 2023,
5755 "action-skip-next-action-set-cc-parameter-to-relative-plus-1-if-action-toggle-state-enabled-1-if-disabled-0-if-toggle-state-unavailable":
5856 2022,
5957 "action-toggle-arm-of-next-action": 2021,
......@@ -302,11 +300,9 @@ export const REAPER_ACTIONS = {
302300 "automation-restore-all-saved-track-envelope-latches": 43562,
303301 "automation-restore-saved-track-envelope-latches": 43564,
304302 "automation-save-and-clear-all-track-envelope-latches": 43561,
305 "automation-save-and-clear-all-track-envelope-latches-if-any-otherwise-restore-saved-latches":
306 43565,
303 "automation-save-and-clear-all-track-envelope-latches-if-any-otherwise-restore-saved-latches": 43565,
307304 "automation-save-and-clear-track-envelope-latches": 43563,
308 "automation-save-and-clear-track-envelope-latches-if-any-otherwise-restore-saved-latches":
309 43566,
305 "automation-save-and-clear-track-envelope-latches-if-any-otherwise-restore-saved-latches": 43566,
310306 "automation-save-latch-preset-1-for-all-tracks": 50500,
311307 "automation-save-latch-preset-1-for-selected-tracks": 50628,
312308 "automation-save-latch-preset-10-for-all-tracks": 50509,
......@@ -449,35 +445,23 @@ export const REAPER_ACTIONS = {
449445 "automation-set-track-automation-mode-to-write": 40403,
450446 "automation-toggle-track-between-touch-and-trim-read-modes": 41109,
451447 "automation-unarm-all-envelopes": 41163,
452 "automation-write-current-values-for-actively-writing-envelopes-from-cursor-to-end-of-project":
453 42015,
454 "automation-write-current-values-for-actively-writing-envelopes-from-cursor-to-first-touch-position":
455 42016,
456 "automation-write-current-values-for-actively-writing-envelopes-from-cursor-to-start-of-project":
457 42014,
458 "automation-write-current-values-for-actively-writing-envelopes-to-entire-envelope":
459 42017,
460 "automation-write-current-values-for-actively-writing-envelopes-to-time-selection":
461 42013,
462 "automation-write-current-values-for-all-writing-envelopes-from-cursor-to-end-of-project":
463 41162,
464 "automation-write-current-values-for-all-writing-envelopes-from-cursor-to-start-of-project":
465 41161,
466 "automation-write-current-values-for-all-writing-envelopes-to-time-selection":
467 41160,
448 "automation-write-current-values-for-actively-writing-envelopes-from-cursor-to-end-of-project": 42015,
449 "automation-write-current-values-for-actively-writing-envelopes-from-cursor-to-first-touch-position": 42016,
450 "automation-write-current-values-for-actively-writing-envelopes-from-cursor-to-start-of-project": 42014,
451 "automation-write-current-values-for-actively-writing-envelopes-to-entire-envelope": 42017,
452 "automation-write-current-values-for-actively-writing-envelopes-to-time-selection": 42013,
453 "automation-write-current-values-for-all-writing-envelopes-from-cursor-to-end-of-project": 41162,
454 "automation-write-current-values-for-all-writing-envelopes-from-cursor-to-start-of-project": 41161,
455 "automation-write-current-values-for-all-writing-envelopes-to-time-selection": 41160,
468456 "big-clock-plus-extended-display-recording-pass-markers-etc": 1101,
469457 "calculate-loudness-of-master-mix-via-dry-run-render": 42440,
470 "calculate-loudness-of-master-mix-within-time-selection-via-dry-run-render":
471 42441,
472 "calculate-loudness-of-selected-items-including-take-and-track-fx-and-settings-via-dry-run-render":
473 42437,
458 "calculate-loudness-of-master-mix-within-time-selection-via-dry-run-render": 42441,
459 "calculate-loudness-of-selected-items-including-take-and-track-fx-and-settings-via-dry-run-render": 42437,
474460 "calculate-loudness-of-selected-items-source-media-via-dry-run-render": 42468,
475461 "calculate-loudness-of-selected-tracks-via-dry-run-render": 42438,
476 "calculate-loudness-of-selected-tracks-within-time-selection-via-dry-run-render":
477 42439,
462 "calculate-loudness-of-selected-tracks-within-time-selection-via-dry-run-render": 42439,
478463 "calculate-mono-loudness-of-selected-tracks-via-dry-run-render": 42447,
479 "calculate-mono-loudness-of-selected-tracks-within-time-selection-via-dry-run-render":
480 42448,
464 "calculate-mono-loudness-of-selected-tracks-within-time-selection-via-dry-run-render": 42448,
481465 "calculate-transient-guides": 42028,
482466 "calculate-transient-guides-for-visible-areas-in-items": 42029,
483467 "clear-project-recording-tag-rectag-wildcard": 43465,
......@@ -488,8 +472,7 @@ export const REAPER_ACTIONS = {
488472 "colors-reset-random-color-generator": 41343,
489473 "comp-takes-activate-next-comp": 41376,
490474 "comp-takes-activate-previous-comp": 41375,
491 "comp-takes-choose-active-comp-for-item-under-mouse-and-all-other-items-in-the-comp":
492 41382,
475 "comp-takes-choose-active-comp-for-item-under-mouse-and-all-other-items-in-the-comp": 41382,
493476 "comp-takes-crop-list-to-active-comp": 41379,
494477 "comp-takes-move-active-comp-to-top-lane": 41378,
495478 "comp-takes-remove-active-comp-from-list": 41374,
......@@ -498,15 +481,12 @@ export const REAPER_ACTIONS = {
498481 "control-surface-refresh-all-surfaces": 41743,
499482 "convert-active-take-midi-to-in-project-midi-source-data": 40684,
500483 "convert-active-take-midi-to-mid-file-reference": 40685,
501 "create-measure-from-time-selection-detect-tempo-detect-number-of-measures":
502 40338,
503 "create-measure-from-time-selection-detect-tempo-try-to-create-single-measure":
504 42407,
484 "create-measure-from-time-selection-detect-tempo-detect-number-of-measures": 40338,
485 "create-measure-from-time-selection-detect-tempo-try-to-create-single-measure": 42407,
505486 "create-measure-from-time-selection-new-time-signature": 40801,
506487 "developer-debug-console": 41075,
507488 "developer-write-c-plus-plus-api-functions-header": 41064,
508 "dock-undock-currently-focused-dockable-window-or-attach-unattach-focused-docker":
509 41172,
489 "dock-undock-currently-focused-dockable-window-or-attach-unattach-focused-docker": 41172,
510490 "docker-activate-next-tab": 41624,
511491 "docker-activate-previous-tab": 41625,
512492 "docker-show-in-bottom-of-main-window": 41598,
......@@ -515,39 +495,23 @@ export const REAPER_ACTIONS = {
515495 "docker-show-in-top-of-main-window": 41600,
516496 "dockers-compact-when-small-and-single-tab": 41691,
517497 "edit-copy-items": 40698,
518 "edit-copy-items-tracks-envelope-points-depending-on-focus-ignoring-time-selection":
519 40057,
520 "edit-copy-items-tracks-envelope-points-depending-on-focus-within-time-selection-if-any-smart-copy":
521 41383,
498 "edit-copy-items-tracks-envelope-points-depending-on-focus-ignoring-time-selection": 40057,
499 "edit-copy-items-tracks-envelope-points-depending-on-focus-within-time-selection-if-any-smart-copy": 41383,
522500 "edit-cut-items": 40699,
523 "edit-cut-items-tracks-envelope-points-depending-on-focus-ignoring-time-selection":
524 40059,
525 "edit-cut-items-tracks-envelope-points-depending-on-focus-within-time-selection-if-any-smart-cut":
526 41384,
527 "edit-delete-notes-of-less-than-1-128-note-in-length-in-selected-midi-items":
528 41738,
529 "edit-delete-notes-of-less-than-1-16-note-in-length-in-selected-midi-items":
530 41735,
531 "edit-delete-notes-of-less-than-1-256-note-in-length-in-selected-midi-items":
532 41739,
533 "edit-delete-notes-of-less-than-1-32-note-in-length-in-selected-midi-items":
534 41736,
535 "edit-delete-notes-of-less-than-1-64-note-in-length-in-selected-midi-items":
536 41737,
537 "edit-delete-notes-of-less-than-1-8-note-in-length-in-selected-midi-items":
538 41734,
539 "edit-delete-trailing-notes-of-less-than-1-128-note-in-length-in-selected-midi-items":
540 41732,
541 "edit-delete-trailing-notes-of-less-than-1-16-note-in-length-in-selected-midi-items":
542 41729,
543 "edit-delete-trailing-notes-of-less-than-1-256-note-in-length-in-selected-midi-items":
544 41733,
545 "edit-delete-trailing-notes-of-less-than-1-32-note-in-length-in-selected-midi-items":
546 41730,
547 "edit-delete-trailing-notes-of-less-than-1-64-note-in-length-in-selected-midi-items":
548 41731,
549 "edit-delete-trailing-notes-of-less-than-1-8-note-in-length-in-selected-midi-items":
550 41728,
501 "edit-cut-items-tracks-envelope-points-depending-on-focus-ignoring-time-selection": 40059,
502 "edit-cut-items-tracks-envelope-points-depending-on-focus-within-time-selection-if-any-smart-cut": 41384,
503 "edit-delete-notes-of-less-than-1-128-note-in-length-in-selected-midi-items": 41738,
504 "edit-delete-notes-of-less-than-1-16-note-in-length-in-selected-midi-items": 41735,
505 "edit-delete-notes-of-less-than-1-256-note-in-length-in-selected-midi-items": 41739,
506 "edit-delete-notes-of-less-than-1-32-note-in-length-in-selected-midi-items": 41736,
507 "edit-delete-notes-of-less-than-1-64-note-in-length-in-selected-midi-items": 41737,
508 "edit-delete-notes-of-less-than-1-8-note-in-length-in-selected-midi-items": 41734,
509 "edit-delete-trailing-notes-of-less-than-1-128-note-in-length-in-selected-midi-items": 41732,
510 "edit-delete-trailing-notes-of-less-than-1-16-note-in-length-in-selected-midi-items": 41729,
511 "edit-delete-trailing-notes-of-less-than-1-256-note-in-length-in-selected-midi-items": 41733,
512 "edit-delete-trailing-notes-of-less-than-1-32-note-in-length-in-selected-midi-items": 41730,
513 "edit-delete-trailing-notes-of-less-than-1-64-note-in-length-in-selected-midi-items": 41731,
514 "edit-delete-trailing-notes-of-less-than-1-8-note-in-length-in-selected-midi-items": 41728,
551515 "edit-dynamic-split-items": 40760,
552516 "edit-dynamic-split-items-using-most-recent-settings": 42951,
553517 "edit-redo": 40030,
......@@ -555,19 +519,14 @@ export const REAPER_ACTIONS = {
555519 "envelope-add-edge-points-to-automation-item": 42209,
556520 "envelope-add-edit-envelope-point-value-at-cursor": 41987,
557521 "envelope-add-edit-envelope-point-value-exactly-at-cursor": 40152,
558 "envelope-apply-all-vcas-from-selected-tracks-to-grouped-tracks-and-reset-volume-pan-mute":
559 41982,
560 "envelope-apply-all-vcas-to-selected-tracks-and-remove-from-vca-groups":
561 41981,
522 "envelope-apply-all-vcas-from-selected-tracks-to-grouped-tracks-and-reset-volume-pan-mute": 41982,
523 "envelope-apply-all-vcas-to-selected-tracks-and-remove-from-vca-groups": 41981,
562524 "envelope-automation-item-properties": 42090,
563 "envelope-automation-items-connect-to-the-underlying-envelope-on-both-sides":
564 42223,
565 "envelope-automation-items-connect-to-the-underlying-envelope-on-the-right-side":
566 42222,
525 "envelope-automation-items-connect-to-the-underlying-envelope-on-both-sides": 42223,
526 "envelope-automation-items-connect-to-the-underlying-envelope-on-the-right-side": 42222,
567527 "envelope-automation-items-do-not-connect-to-the-underlying-envelope": 42221,
568528 "envelope-bypass-underlying-envelope-outside-of-automation-items": 42224,
569 "envelope-chase-non-fx-envelope-to-automation-items-when-underlying-envelope-is-bypassed":
570 42345,
529 "envelope-chase-non-fx-envelope-to-automation-items-when-underlying-envelope-is-bypassed": 42345,
571530 "envelope-clear-or-remove-envelope": 40065,
572531 "envelope-convert-all-project-automation-to-automation-items": 42207,
573532 "envelope-copy-points-within-time-selection": 40324,
......@@ -589,18 +548,14 @@ export const REAPER_ACTIONS = {
589548 "envelope-increase-bezier-tension-for-selected-points-by-5-percent": 41122,
590549 "envelope-insert-4-envelope-points-at-time-selection": 40726,
591550 "envelope-insert-automation-item": 42082,
592 "envelope-insert-new-point-at-current-position-do-not-remove-nearby-points":
593 40106,
551 "envelope-insert-new-point-at-current-position-do-not-remove-nearby-points": 40106,
594552 "envelope-insert-new-point-at-current-position-remove-nearby-points": 40915,
595 "envelope-insert-new-point-at-current-position-to-all-visible-track-envelopes-do-not-remove-nearby-points":
596 40064,
597 "envelope-insert-new-point-at-current-position-to-all-visible-track-envelopes-remove-nearby-points":
598 41126,
553 "envelope-insert-new-point-at-current-position-to-all-visible-track-envelopes-do-not-remove-nearby-points": 40064,
554 "envelope-insert-new-point-at-current-position-to-all-visible-track-envelopes-remove-nearby-points": 41126,
599555 "envelope-invert-selected-points": 40334,
600556 "envelope-load-automation-item": 42093,
601557 "envelope-mute-automation-items": 42211,
602 "envelope-obey-project-default-setting-to-bypass-underlying-envelope-outside-of-automation-items":
603 42215,
558 "envelope-obey-project-default-setting-to-bypass-underlying-envelope-outside-of-automation-items": 42215,
604559 "envelope-reduce-number-of-points": 40887,
605560 "envelope-reduce-number-of-points-by-half": 42199,
606561 "envelope-reduce-number-of-points-by-half-within-time-selection": 42201,
......@@ -655,8 +610,7 @@ export const REAPER_ACTIONS = {
655610 "envelopes-move-selected-points-up-a-tiny-bit": 42381,
656611 "envelopes-view-envelopes-for-last-touched-track-item": 40019,
657612 "export-track-lyrics": 42071,
658 "file-add-project-to-render-queue-using-the-most-recent-render-settings":
659 41823,
613 "file-add-project-to-render-queue-using-the-most-recent-render-settings": 41823,
660614 "file-batch-file-converter": 41076,
661615 "file-choose-project-s-to-open": 43697,
662616 "file-clean-current-project-directory": 40098,
......@@ -670,43 +624,33 @@ export const REAPER_ACTIONS = {
670624 "file-new-project": 40023,
671625 "file-open-project": 40025,
672626 "file-open-render-queue": 40929,
673 "file-open-render-statistics-charts-from-most-recent-render-in-web-browser":
674 42497,
675 "file-open-render-statistics-charts-from-most-recent-render-in-web-browser-hide-file-paths":
676 42510,
627 "file-open-render-statistics-charts-from-most-recent-render-in-web-browser": 42497,
628 "file-open-render-statistics-charts-from-most-recent-render-in-web-browser-hide-file-paths": 42510,
677629 "file-open-render-statistics-charts-from-most-recent-render-in-web-browser-hide-file-paths-and-project-regions-markers":
678630 43343,
679 "file-open-render-statistics-charts-from-most-recent-render-in-web-browser-hide-project-regions-markers":
680 43342,
631 "file-open-render-statistics-charts-from-most-recent-render-in-web-browser-hide-project-regions-markers": 43342,
681632 "file-project-settings": 40021,
682633 "file-quit-reaper": 40004,
683634 "file-render-project-to-disk": 40015,
684635 "file-render-project-using-the-most-recent-render-settings": 41824,
685 "file-render-project-using-the-most-recent-render-settings-auto-close-render-dialog":
686 42230,
687 "file-render-project-using-the-most-recent-render-settings-with-a-new-target-file-name":
688 41855,
636 "file-render-project-using-the-most-recent-render-settings-auto-close-render-dialog": 42230,
637 "file-render-project-using-the-most-recent-render-settings-with-a-new-target-file-name": 41855,
689638 "file-save-all-projects": 40897,
690639 "file-save-copy-of-project-as-prompt-with-current-name": 43578,
691640 "file-save-copy-of-project-as-prompt-with-incremented-project-name": 42347,
692641 "file-save-copy-of-project-automatically-increment-project-name": 42346,
693642 "file-save-live-output-to-disk-bounce": 40017,
694 "file-save-live-output-to-disk-bounce-using-the-most-recent-bounce-settings":
695 42317,
696 "file-save-new-version-of-project-automatically-increment-project-name":
697 41895,
643 "file-save-live-output-to-disk-bounce-using-the-most-recent-bounce-settings": 42317,
644 "file-save-new-version-of-project-automatically-increment-project-name": 41895,
698645 "file-save-project": 40026,
699646 "file-save-project-and-render-rpp-prox": 42332,
700647 "file-save-project-as": 40022,
701648 "file-save-project-as-template": 40394,
702649 "file-show-project-render-metadata-window": 42397,
703650 "file-spawn-new-instance-of-reaper": 40063,
704 "fixed-lane-comp-area-add-comp-area-at-time-selection-for-lane-at-mouse":
705 42657,
706 "fixed-lane-comp-area-add-comp-area-between-previous-and-next-comp-areas-for-lane-at-mouse":
707 42599,
708 "fixed-lane-comp-area-add-comp-area-from-mouse-position-to-next-area-or-end-of-media":
709 42613,
651 "fixed-lane-comp-area-add-comp-area-at-time-selection-for-lane-at-mouse": 42657,
652 "fixed-lane-comp-area-add-comp-area-between-previous-and-next-comp-areas-for-lane-at-mouse": 42599,
653 "fixed-lane-comp-area-add-comp-area-from-mouse-position-to-next-area-or-end-of-media": 42613,
710654 "fixed-lane-comp-area-delete-comp-area": 42642,
711655 "fixed-lane-comp-area-delete-comp-area-at-mouse": 42643,
712656 "fixed-lane-comp-area-delete-comp-area-at-mouse-but-not-media-items": 42644,
......@@ -722,16 +666,13 @@ export const REAPER_ACTIONS = {
722666 "fixed-lane-comp-area-move-up": 41082,
723667 "fixed-lane-comp-area-set-loop-points-to-comp-area": 42495,
724668 "fixed-lane-comp-area-set-loop-points-to-comp-area-at-mouse": 42504,
725 "fixed-lane-comp-area-set-loop-points-to-comp-area-at-mouse-half-second-preroll-postroll":
726 42711,
727 "fixed-lane-comp-area-set-loop-points-to-comp-area-at-mouse-one-second-preroll-postroll":
728 42712,
669 "fixed-lane-comp-area-set-loop-points-to-comp-area-at-mouse-half-second-preroll-postroll": 42711,
670 "fixed-lane-comp-area-set-loop-points-to-comp-area-at-mouse-one-second-preroll-postroll": 42712,
729671 "fixed-lane-comp-area-split-comp-area-at-edit-cursor": 42600,
730672 "fixed-lane-comp-area-split-comp-area-at-mouse-position": 42641,
731673 "fixed-lane-comp-area-split-comp-area-at-mouse-position-ignoring-snap": 40146,
732674 "fixed-lane-comp-area-split-media-items-at-comp-area-edges": 42602,
733 "fixed-lane-comp-area-split-media-items-at-edges-of-comp-area-at-mouse":
734 42603,
675 "fixed-lane-comp-area-split-media-items-at-edges-of-comp-area-at-mouse": 42603,
735676 "fully-unload-unloaded-vsts": 41204,
736677 "fx-activate-bypass-track-take-envelope-for-last-touched-fx-parameter": 41983,
737678 "fx-arm-track-take-envelope-for-last-touched-fx-parameter": 41984,
......@@ -753,8 +694,7 @@ export const REAPER_ACTIONS = {
753694 "fx-show-previous-single-fx-embedded-ui-in-tcp-selected-tracks": 42343,
754695 "fx-toggle-delta-solo-for-last-focused-fx": 42455,
755696 "fx-toggle-map-to-container-parameter-for-last-touched-fx-parameter": 42650,
756 "fx-toggle-preference-auto-bypass-fx-that-require-pdc-on-record-arm-affected-tracks":
757 43150,
697 "fx-toggle-preference-auto-bypass-fx-that-require-pdc-on-record-arm-affected-tracks": 43150,
758698 "global-automation-override-all-automation-in-latch-mode": 40881,
759699 "global-automation-override-all-automation-in-latch-preview-mode": 42022,
760700 "global-automation-override-all-automation-in-read-mode": 40879,
......@@ -762,8 +702,7 @@ export const REAPER_ACTIONS = {
762702 "global-automation-override-all-automation-in-trim-read-mode": 40878,
763703 "global-automation-override-all-automation-in-write-mode": 40882,
764704 "global-automation-override-bypass-all-automation": 40885,
765 "global-automation-override-no-override-set-automation-modes-per-track":
766 40876,
705 "global-automation-override-no-override-set-automation-modes-per-track": 40876,
767706 "global-automation-override-toggle-bypass-all-automation": 40908,
768707 "go-to-end-of-loop": 40633,
769708 "go-to-end-of-time-selection": 40631,
......@@ -1065,8 +1004,7 @@ export const REAPER_ACTIONS = {
10651004 "help-all-actions": 40845,
10661005 "help-check-for-new-versions": 40442,
10671006 "help-mouse-modifier-keys-and-action-shortcuts": 40308,
1068 "help-show-mouse-editing-help-in-the-area-beneath-the-track-control-panels":
1069 41345,
1007 "help-show-mouse-editing-help-in-the-area-beneath-the-track-control-panels": 41345,
10701008 "i-o-dialog-close-window-on-enter-key": 41828,
10711009 "import-track-lyrics": 42070,
10721010 "insert-click-source": 40013,
......@@ -1104,8 +1042,7 @@ export const REAPER_ACTIONS = {
11041042 "item-clear-up-rank-down-rank-markers-within-time-selection": 43202,
11051043 "item-close-item-inline-editors": 41887,
11061044 "item-collapse-empty-take": 41747,
1107 "item-convert-embedded-source-transient-information-to-transient-guides":
1108 42380,
1045 "item-convert-embedded-source-transient-information-to-transient-guides": 42380,
11091046 "item-copy-items-to-time-selection-trim-loop-to-fit": 41319,
11101047 "item-copy-loop-of-selected-area-of-audio-items": 40014,
11111048 "item-copy-selected-area-of-items": 40060,
......@@ -1116,32 +1053,23 @@ export const REAPER_ACTIONS = {
11161053 "item-cycle-through-crossfade-shapes": 41534,
11171054 "item-cycle-through-fade-in-shapes": 41520,
11181055 "item-cycle-through-fade-out-shapes": 41527,
1119 "item-cycle-through-up-rank-down-rank-levels-for-active-take-or-last-recording-pass":
1120 43197,
1121 "item-cycle-through-up-rank-down-rank-levels-for-take-marker-at-mouse-position":
1122 43200,
1123 "item-cycle-through-up-rank-down-rank-levels-for-take-marker-at-play-position-or-edit-cursor":
1124 43199,
1125 "item-cycle-through-up-rank-down-rank-levels-for-take-marker-at-time-selection":
1126 43201,
1127 "item-cycle-through-up-rank-down-rank-levels-for-take-or-comp-area-under-mouse":
1128 43198,
1056 "item-cycle-through-up-rank-down-rank-levels-for-active-take-or-last-recording-pass": 43197,
1057 "item-cycle-through-up-rank-down-rank-levels-for-take-marker-at-mouse-position": 43200,
1058 "item-cycle-through-up-rank-down-rank-levels-for-take-marker-at-play-position-or-edit-cursor": 43199,
1059 "item-cycle-through-up-rank-down-rank-levels-for-take-marker-at-time-selection": 43201,
1060 "item-cycle-through-up-rank-down-rank-levels-for-take-or-comp-area-under-mouse": 43198,
11291061 "item-delete-all-take-markers": 42387,
11301062 "item-delete-take-marker-at-cursor": 42386,
11311063 "item-delete-take-marker-at-mouse-position": 42389,
11321064 "item-delete-take-markers-in-time-selection": 43182,
1133 "item-delete-takes-for-item-under-mouse-that-are-down-ranked-no-confirm":
1134 43163,
1135 "item-delete-takes-for-item-under-mouse-that-are-not-up-ranked-no-confirm":
1136 43164,
1065 "item-delete-takes-for-item-under-mouse-that-are-down-ranked-no-confirm": 43163,
1066 "item-delete-takes-for-item-under-mouse-that-are-not-up-ranked-no-confirm": 43164,
11371067 "item-delete-takes-that-are-down-ranked-no-confirm": 42682,
11381068 "item-delete-takes-that-are-not-up-ranked-no-confirm": 40229,
11391069 "item-disable-default-fadein-fadeout": 41196,
11401070 "item-down-rank-active-take-or-last-recording-pass": 42681,
1141 "item-down-rank-take-marker-at-1-second-before-play-position-or-at-edit-cursor-if-not-playing-back":
1142 43175,
1143 "item-down-rank-take-marker-at-2-seconds-before-play-position-or-at-edit-cursor-if-not-playing-back":
1144 43177,
1071 "item-down-rank-take-marker-at-1-second-before-play-position-or-at-edit-cursor-if-not-playing-back": 43175,
1072 "item-down-rank-take-marker-at-2-seconds-before-play-position-or-at-edit-cursor-if-not-playing-back": 43177,
11451073 "item-down-rank-take-marker-at-mouse-position": 43160,
11461074 "item-down-rank-take-marker-at-play-position-or-edit-cursor": 43158,
11471075 "item-down-rank-take-marker-at-time-selection": 43184,
......@@ -1167,16 +1095,12 @@ export const REAPER_ACTIONS = {
11671095 "item-edit-move-items-envelope-points-up-one-track-a-bit": 40117,
11681096 "item-edit-move-items-left-preserving-timing-of-contents": 40121,
11691097 "item-edit-move-items-right-preserving-timing-of-contents": 40122,
1170 "item-edit-move-left-edge-of-item-to-edit-cursor-preserving-item-right-edge":
1171 41306,
1172 "item-edit-move-left-edge-of-item-under-mouse-to-edit-cursor-preserving-item-right-edge":
1173 41301,
1098 "item-edit-move-left-edge-of-item-to-edit-cursor-preserving-item-right-edge": 41306,
1099 "item-edit-move-left-edge-of-item-under-mouse-to-edit-cursor-preserving-item-right-edge": 41301,
11741100 "item-edit-move-position-of-item-to-edit-cursor": 41205,
11751101 "item-edit-move-position-of-item-under-mouse-to-edit-cursor": 41299,
1176 "item-edit-move-right-edge-of-item-to-edit-cursor-preserving-item-length":
1177 41307,
1178 "item-edit-move-right-edge-of-item-under-mouse-to-edit-cursor-preserving-item-length":
1179 41302,
1102 "item-edit-move-right-edge-of-item-to-edit-cursor-preserving-item-length": 41307,
1103 "item-edit-move-right-edge-of-item-under-mouse-to-edit-cursor-preserving-item-length": 41302,
11801104 "item-edit-nudge-left-by-last-nudge-dialog-settings": 41250,
11811105 "item-edit-nudge-left-by-saved-nudge-dialog-settings-1": 41279,
11821106 "item-edit-nudge-left-by-saved-nudge-dialog-settings-2": 41280,
......@@ -1228,15 +1152,11 @@ export const REAPER_ACTIONS = {
12281152 "item-force-transient-optimized-mode-for-stretch-markers": 42337,
12291153 "item-glue-items-auto-increase-channel-count-with-take-fx": 42434,
12301154 "item-glue-items-expanding-to-time-selection-if-any": 41588,
1231 "item-glue-items-expanding-to-time-selection-if-any-auto-increase-channel-count-with-take-fx":
1232 42009,
1233 "item-glue-items-expanding-to-time-selection-if-any-including-leading-fade-in-and-trailing-fade-out":
1234 40606,
1155 "item-glue-items-expanding-to-time-selection-if-any-auto-increase-channel-count-with-take-fx": 42009,
1156 "item-glue-items-expanding-to-time-selection-if-any-including-leading-fade-in-and-trailing-fade-out": 40606,
12351157 "item-glue-items-ignoring-time-selection": 40362,
1236 "item-glue-items-ignoring-time-selection-auto-increase-channel-count-with-take-fx":
1237 42008,
1238 "item-glue-items-ignoring-time-selection-including-leading-fade-in-and-trailing-fade-out":
1239 40257,
1158 "item-glue-items-ignoring-time-selection-auto-increase-channel-count-with-take-fx": 42008,
1159 "item-glue-items-ignoring-time-selection-including-leading-fade-in-and-trailing-fade-out": 40257,
12401160 "item-glue-items-including-leading-fade-in-and-trailing-fade-out": 42433,
12411161 "item-glue-items-within-time-selection": 42432,
12421162 "item-go-to-nearest-stretch-marker": 41862,
......@@ -1269,18 +1189,15 @@ export const REAPER_ACTIONS = {
12691189 "item-navigation-move-cursor-to-previous-transient-in-items": 40376,
12701190 "item-navigation-move-cursor-to-start-of-items": 41173,
12711191 "item-navigation-select-and-move-to-item-in-next-track": 40419,
1272 "item-navigation-select-and-move-to-item-in-next-track-without-changing-track-selection":
1273 41140,
1192 "item-navigation-select-and-move-to-item-in-next-track-without-changing-track-selection": 41140,
12741193 "item-navigation-select-and-move-to-item-in-previous-track": 40418,
1275 "item-navigation-select-and-move-to-item-in-previous-track-without-changing-track-selection":
1276 41139,
1194 "item-navigation-select-and-move-to-item-in-previous-track-without-changing-track-selection": 41139,
12771195 "item-navigation-select-and-move-to-next-item": 40417,
12781196 "item-navigation-select-and-move-to-previous-item": 40416,
12791197 "item-nudge-items-volume-1db": 41924,
12801198 "item-nudge-items-volume-plus-1db": 41925,
12811199 "item-open-associated-project-in-new-tab": 41816,
1282 "item-open-in-built-in-midi-editor-set-default-behavior-in-preferences":
1283 40153,
1200 "item-open-in-built-in-midi-editor-set-default-behavior-in-preferences": 40153,
12841201 "item-open-item-copies-in-primary-external-editor": 40132,
12851202 "item-open-item-copies-in-secondary-external-editor": 40203,
12861203 "item-open-item-inline-editors": 40847,
......@@ -1295,54 +1212,38 @@ export const REAPER_ACTIONS = {
12951212 "item-propagate-to-similarly-named-items-on-track": 41977,
12961213 "item-properties-clear-take-preserve-pitch": 40796,
12971214 "item-properties-decrease-item-rate-by-0-6-percent-10-cents": 40520,
1298 "item-properties-decrease-item-rate-by-0-6-percent-10-cents-clear-preserve-pitch":
1299 40800,
1215 "item-properties-decrease-item-rate-by-0-6-percent-10-cents-clear-preserve-pitch": 40800,
13001216 "item-properties-decrease-item-rate-by-6-percent-one-semitone": 40518,
1301 "item-properties-decrease-item-rate-by-6-percent-one-semitone-clear-preserve-pitch":
1302 40798,
1217 "item-properties-decrease-item-rate-by-6-percent-one-semitone-clear-preserve-pitch": 40798,
13031218 "item-properties-display-item-beats-ruler-constant-time-signature": 42315,
1304 "item-properties-display-item-beats-ruler-minimal-constant-time-signature":
1305 42359,
1219 "item-properties-display-item-beats-ruler-minimal-constant-time-signature": 42359,
13061220 "item-properties-display-item-source-time-ruler": 42313,
1307 "item-properties-display-item-source-time-ruler-apply-media-source-bwf-start-offset":
1308 42419,
1221 "item-properties-display-item-source-time-ruler-apply-media-source-bwf-start-offset": 42419,
13091222 "item-properties-display-item-source-time-ruler-in-h-m-s-f-format": 42358,
1310 "item-properties-display-item-source-time-ruler-in-h-m-s-f-format-apply-media-source-bwf-start-offset":
1311 42420,
1223 "item-properties-display-item-source-time-ruler-in-h-m-s-f-format-apply-media-source-bwf-start-offset": 42420,
13121224 "item-properties-display-item-time-ruler": 42312,
13131225 "item-properties-display-item-time-ruler-in-h-m-s-f-format": 42314,
13141226 "item-properties-increase-item-rate-by-0-6-percent-10-cents": 40519,
1315 "item-properties-increase-item-rate-by-0-6-percent-10-cents-clear-preserve-pitch":
1316 40799,
1227 "item-properties-increase-item-rate-by-0-6-percent-10-cents-clear-preserve-pitch": 40799,
13171228 "item-properties-increase-item-rate-by-6-percent-one-semitone": 40517,
1318 "item-properties-increase-item-rate-by-6-percent-one-semitone-clear-preserve-pitch":
1319 40797,
1229 "item-properties-increase-item-rate-by-6-percent-one-semitone-clear-preserve-pitch": 40797,
13201230 "item-properties-item-ruler-settings": 42355,
13211231 "item-properties-lock": 40688,
1322 "item-properties-lock-to-active-take-mouse-click-will-not-change-active-take":
1323 41340,
1232 "item-properties-lock-to-active-take-mouse-click-will-not-change-active-take": 41340,
13241233 "item-properties-loop-item-source": 40636,
13251234 "item-properties-loop-section-of-audio-item-source": 40547,
13261235 "item-properties-mute": 40719,
1327 "item-properties-normalize-items-each-item-separately-to-plus-0db-peak":
1328 40108,
1236 "item-properties-normalize-items-each-item-separately-to-plus-0db-peak": 40108,
13291237 "item-properties-normalize-items-peak-rms-lufs": 42460,
13301238 "item-properties-normalize-items-to-loudest-item-to-plus-0db-peak": 40254,
1331 "item-properties-normalize-items-to-loudest-item-to-plus-0db-peak-reset-to-unity-if-already-normalized":
1332 40937,
1333 "item-properties-normalize-items-to-plus-0db-peak-reset-to-unity-if-already-normalized":
1334 40936,
1239 "item-properties-normalize-items-to-loudest-item-to-plus-0db-peak-reset-to-unity-if-already-normalized": 40937,
1240 "item-properties-normalize-items-to-plus-0db-peak-reset-to-unity-if-already-normalized": 40936,
13351241 "item-properties-normalize-items-using-most-recent-settings": 42461,
1336 "item-properties-normalize-items-using-most-recent-settings-force-normalize-as-if-one-long-item":
1337 42463,
1338 "item-properties-normalize-items-using-most-recent-settings-force-normalize-each-item-separately":
1339 42464,
1340 "item-properties-normalize-items-using-most-recent-settings-force-normalize-to-loudest-item":
1341 43344,
1342 "item-properties-normalize-items-using-most-recent-settings-reset-to-unity-if-already-normalized":
1343 42462,
1344 "item-properties-open-media-item-take-channel-mapper-for-selected-items":
1345 42429,
1242 "item-properties-normalize-items-using-most-recent-settings-force-normalize-as-if-one-long-item": 42463,
1243 "item-properties-normalize-items-using-most-recent-settings-force-normalize-each-item-separately": 42464,
1244 "item-properties-normalize-items-using-most-recent-settings-force-normalize-to-loudest-item": 43344,
1245 "item-properties-normalize-items-using-most-recent-settings-reset-to-unity-if-already-normalized": 42462,
1246 "item-properties-open-media-item-take-channel-mapper-for-selected-items": 42429,
13461247 "item-properties-pitch-item-down-one-cent": 40207,
13471248 "item-properties-pitch-item-down-one-octave": 40516,
13481249 "item-properties-pitch-item-down-one-semitone": 40205,
......@@ -1351,11 +1252,9 @@ export const REAPER_ACTIONS = {
13511252 "item-properties-pitch-item-up-one-semitone": 40204,
13521253 "item-properties-reset-item-pitch": 40653,
13531254 "item-properties-reset-item-take-gain-to-plus-0db-un-normalize": 40938,
1354 "item-properties-set-item-rate-from-user-supplied-source-media-tempo-bpm":
1355 42374,
1255 "item-properties-set-item-rate-from-user-supplied-source-media-tempo-bpm": 42374,
13561256 "item-properties-set-item-rate-to-1-0": 40652,
1357 "item-properties-set-item-timebase-to-beats-auto-stretch-at-tempo-changes":
1358 42375,
1257 "item-properties-set-item-timebase-to-beats-auto-stretch-at-tempo-changes": 42375,
13591258 "item-properties-set-item-timebase-to-beats-position-length-rate": 40484,
13601259 "item-properties-set-item-timebase-to-beats-position-only": 40485,
13611260 "item-properties-set-item-timebase-to-project-track-default": 40380,
......@@ -1504,8 +1403,7 @@ export const REAPER_ACTIONS = {
15041403 "item-properties-toggle-item-play-all-takes": 40437,
15051404 "item-properties-toggle-items-tracks-mute-depending-on-focus": 40183,
15061405 "item-properties-toggle-lock": 40687,
1507 "item-properties-toggle-lock-to-active-take-toggle-mouse-click-changes-active-take":
1508 41339,
1406 "item-properties-toggle-lock-to-active-take-toggle-mouse-click-changes-active-take": 41339,
15091407 "item-properties-toggle-mute": 40175,
15101408 "item-properties-toggle-polarity-phase-for-active-take": 40181,
15111409 "item-properties-toggle-show-media-item-take-properties": 41589,
......@@ -1550,18 +1448,14 @@ export const REAPER_ACTIONS = {
15501448 "item-select-all-items-in-current-time-selection": 40717,
15511449 "item-select-all-items-in-track": 40421,
15521450 "item-select-all-items-on-selected-tracks-in-current-time-selection": 40718,
1553 "item-select-all-other-media-items-that-share-pooled-ara-edits-with-selected-items":
1554 42607,
1555 "item-select-all-other-media-items-that-share-pooled-midi-source-data-with-selected-items":
1556 41611,
1557 "item-select-all-other-media-items-that-share-the-same-source-media-as-selected-items":
1558 42608,
1451 "item-select-all-other-media-items-that-share-pooled-ara-edits-with-selected-items": 42607,
1452 "item-select-all-other-media-items-that-share-pooled-midi-source-data-with-selected-items": 41611,
1453 "item-select-all-other-media-items-that-share-the-same-source-media-as-selected-items": 42608,
15591454 "item-select-item-under-mouse-cursor": 40528,
15601455 "item-select-item-under-mouse-cursor-leaving-other-items-selected": 40529,
15611456 "item-select-next-adjacent-non-overlapping-item": 41127,
15621457 "item-select-previous-adjacent-non-overlapping-item": 41128,
1563 "item-set-all-media-item-takes-that-share-the-same-source-media-to-the-same-random-color":
1564 42693,
1458 "item-set-all-media-item-takes-that-share-the-same-source-media-to-the-same-random-color": 42693,
15651459 "item-set-all-media-offline": 40100,
15661460 "item-set-all-media-online": 40101,
15671461 "item-set-crossfade-shape-to-type-1-linear-equal-gain": 41528,
......@@ -1626,8 +1520,7 @@ export const REAPER_ACTIONS = {
16261520 "item-split-at-take-markers-for-all-items-on-track-all-takes": 43173,
16271521 "item-split-at-take-markers-for-item-under-mouse-active-take-only": 43169,
16281522 "item-split-at-take-markers-for-item-under-mouse-all-takes": 43172,
1629 "item-split-item-under-mouse-cursor-ignore-grouping-no-change-selection":
1630 40746,
1523 "item-split-item-under-mouse-cursor-ignore-grouping-no-change-selection": 40746,
16311524 "item-split-item-under-mouse-cursor-ignore-grouping-select-left": 43180,
16321525 "item-split-item-under-mouse-cursor-ignore-grouping-select-right": 40748,
16331526 "item-split-item-under-mouse-cursor-no-change-selection": 42575,
......@@ -1653,10 +1546,8 @@ export const REAPER_ACTIONS = {
16531546 "item-trim-items-to-selected-area": 40508,
16541547 "item-unselect-clear-selection-of-all-items": 40289,
16551548 "item-up-rank-active-take-or-last-recording-pass": 42680,
1656 "item-up-rank-take-marker-at-1-second-before-play-position-or-at-edit-cursor-if-not-playing-back":
1657 43174,
1658 "item-up-rank-take-marker-at-2-seconds-before-play-position-or-at-edit-cursor-if-not-playing-back":
1659 43176,
1549 "item-up-rank-take-marker-at-1-second-before-play-position-or-at-edit-cursor-if-not-playing-back": 43174,
1550 "item-up-rank-take-marker-at-2-seconds-before-play-position-or-at-edit-cursor-if-not-playing-back": 43176,
16601551 "item-up-rank-take-marker-at-mouse-position": 43159,
16611552 "item-up-rank-take-marker-at-play-position-or-edit-cursor": 43157,
16621553 "item-up-rank-take-marker-at-time-selection": 43183,
......@@ -1668,8 +1559,7 @@ export const REAPER_ACTIONS = {
16681559 "items-set-group-to-random-color-for-selected-items": 43655,
16691560 "items-set-take-fx-offline-for-all-inactive-takes-in-the-project": 43691,
16701561 "items-set-take-fx-online-for-all-active-takes-in-the-project": 43692,
1671 "items-set-take-fx-online-if-active-offline-if-inactive-for-all-takes-in-the-project":
1672 43693,
1562 "items-set-take-fx-online-if-active-offline-if-inactive-for-all-takes-in-the-project": 43693,
16731563 "layout-apply-custom-layout-number-01": 41696,
16741564 "layout-apply-custom-layout-number-02": 41697,
16751565 "layout-apply-custom-layout-number-03": 41698,
......@@ -1739,8 +1629,7 @@ export const REAPER_ACTIONS = {
17391629 "loop-points-move-end-point-to-cursor-preserve-length": 43211,
17401630 "loop-points-move-start-point-to-cursor-preserve-length": 43210,
17411631 "loop-points-remove-unselect-loop-point-selection": 40634,
1742 "loop-points-remove-unselect-loop-points-if-not-linked-to-time-selection-ignoring-lock-state":
1743 40624,
1632 "loop-points-remove-unselect-loop-points-if-not-linked-to-time-selection-ignoring-lock-state": 40624,
17441633 "loop-points-set-end-point": 40223,
17451634 "loop-points-set-loop-points-to-items": 41039,
17461635 "loop-points-set-start-point": 40222,
......@@ -1849,19 +1738,14 @@ export const REAPER_ACTIONS = {
18491738 "master-track-toggle-stereo-mono-l-plus-r": 40917,
18501739 "media-explorer-show-hide-media-explorer": 50124,
18511740 "media-item-add-stretch-markers-at-project-tempo-changes": 42377,
1852 "media-item-clear-and-recalculate-auto-stretch-at-project-tempo-changes":
1853 42376,
1741 "media-item-clear-and-recalculate-auto-stretch-at-project-tempo-changes": 42376,
18541742 "menu-customize": 1528,
18551743 "midi-clear-retroactive-midi-history": 42378,
1856 "midi-insert-all-available-retroactively-recorded-midi-for-armed-and-selected-tracks":
1857 40212,
1858 "midi-insert-all-available-retroactively-recorded-midi-for-armed-tracks":
1859 40213,
1860 "midi-insert-recent-retroactively-recorded-midi-for-armed-and-selected-tracks":
1861 40211,
1744 "midi-insert-all-available-retroactively-recorded-midi-for-armed-and-selected-tracks": 40212,
1745 "midi-insert-all-available-retroactively-recorded-midi-for-armed-tracks": 40213,
1746 "midi-insert-recent-retroactively-recorded-midi-for-armed-and-selected-tracks": 40211,
18621747 "midi-insert-recent-retroactively-recorded-midi-for-armed-tracks": 40686,
1863 "midi-reload-track-support-data-bank-program-files-notation-etc-for-all-midi-items-on-selected-tracks":
1864 42465,
1748 "midi-reload-track-support-data-bank-program-files-notation-etc-for-all-midi-items-on-selected-tracks": 42465,
18651749 "minimize-reaper": 41171,
18661750 "mixer-clickable-icon-for-folder-tracks-to-show-hide-children": 41154,
18671751 "mixer-group-fx-parameters-with-their-inserts": 41829,
......@@ -1878,8 +1762,7 @@ export const REAPER_ACTIONS = {
18781762 "mixer-toggle-show-fx-parameters-when-size-permits": 40910,
18791763 "mixer-toggle-show-icons-for-the-last-track-in-a-folder": 41153,
18801764 "mixer-toggle-show-master-track-on-right-side": 40389,
1881 "mixer-toggle-show-multiple-rows-even-when-space-to-fit-tracks-in-less-rows":
1882 40372,
1765 "mixer-toggle-show-multiple-rows-even-when-space-to-fit-tracks-in-less-rows": 40372,
18831766 "mixer-toggle-show-multiple-rows-when-space": 40371,
18841767 "mixer-toggle-show-normal-top-level-tracks-in-mixer": 40082,
18851768 "mixer-toggle-show-sends-when-size-permits": 40557,
......@@ -1890,19 +1773,14 @@ export const REAPER_ACTIONS = {
18901773 "monitoring-fx-toggle-bypass": 41884,
18911774 "mouse-modifiers-clear-arrange-view-override-mouse-modifiers": 42621,
18921775 "mouse-modifiers-set-arrange-view-override-mouse-modifiers-a": 42615,
1893 "mouse-modifiers-set-arrange-view-override-mouse-modifiers-a-until-next-mouseup":
1894 42622,
1776 "mouse-modifiers-set-arrange-view-override-mouse-modifiers-a-until-next-mouseup": 42622,
18951777 "mouse-modifiers-set-arrange-view-override-mouse-modifiers-b": 42617,
1896 "mouse-modifiers-set-arrange-view-override-mouse-modifiers-b-until-next-mouseup":
1897 42623,
1778 "mouse-modifiers-set-arrange-view-override-mouse-modifiers-b-until-next-mouseup": 42623,
18981779 "mouse-modifiers-set-arrange-view-override-mouse-modifiers-c": 42619,
1899 "mouse-modifiers-set-arrange-view-override-mouse-modifiers-c-until-next-mouseup":
1900 42624,
1780 "mouse-modifiers-set-arrange-view-override-mouse-modifiers-c-until-next-mouseup": 42624,
19011781 "mouse-modifiers-set-arrange-view-override-mouse-modifiers-d": 42632,
1902 "mouse-modifiers-set-arrange-view-override-mouse-modifiers-d-until-next-mouseup":
1903 42634,
1904 "mouse-modifiers-swap-arrange-view-right-drag-modifiers-for-marquee-item-selection-and-select-razor-edit-area":
1905 42401,
1782 "mouse-modifiers-set-arrange-view-override-mouse-modifiers-d-until-next-mouseup": 42634,
1783 "mouse-modifiers-swap-arrange-view-right-drag-modifiers-for-marquee-item-selection-and-select-razor-edit-area": 42401,
19061784 "mouse-modifiers-toggle-arrange-view-override-mouse-modifiers-a": 42616,
19071785 "mouse-modifiers-toggle-arrange-view-override-mouse-modifiers-b": 42618,
19081786 "mouse-modifiers-toggle-arrange-view-override-mouse-modifiers-c": 42620,
......@@ -1939,66 +1817,51 @@ export const REAPER_ACTIONS = {
19391817 "open-selected-item-source-media-in-media-explorer": 41623,
19401818 "options-add-edge-points-when-moving-envelope-points-with-items": 40648,
19411819 "options-add-edge-points-when-moving-multiple-envelope-points": 42030,
1942 "options-add-envelope-edge-points-when-ripple-editing-or-inserting-time":
1943 40649,
1820 "options-add-envelope-edge-points-when-ripple-editing-or-inserting-time": 40649,
19441821 "options-allow-drag-drop-media-import-to-target-the-top-part-of-a-track-to-insert-a-new-track-to-receive-the-media":
19451822 42477,
19461823 "options-allow-selecting-empty-takes": 41355,
19471824 "options-always-record-to-automation-items": 42212,
1948 "options-always-trim-content-behind-razor-edits-otherwise-follow-media-item-editing-preferences":
1949 42421,
1825 "options-always-trim-content-behind-razor-edits-otherwise-follow-media-item-editing-preferences": 42421,
19501826 "options-auto-crossfade-media-items-when-editing": 40041,
19511827 "options-automatically-insert-automation-item-when-activating-envelope-that-is-bypassed-outside-of-automation-items-and-not-displayed-in-envelope-lane":
19521828 42220,
1953 "options-automation-item-baseline-amplitude-edits-affect-pooled-copies":
1954 42194,
1955 "options-automation-items-connect-to-the-underlying-envelope-on-both-sides":
1956 42205,
1957 "options-automation-items-connect-to-the-underlying-envelope-on-the-right-side":
1958 42204,
1829 "options-automation-item-baseline-amplitude-edits-affect-pooled-copies": 42194,
1830 "options-automation-items-connect-to-the-underlying-envelope-on-both-sides": 42205,
1831 "options-automation-items-connect-to-the-underlying-envelope-on-the-right-side": 42204,
19591832 "options-automation-items-do-not-connect-to-the-underlying-envelope": 42203,
19601833 "options-avoid-including-empty-track-space-in-comp-areas": 42795,
19611834 "options-bypass-underlying-envelopes-outside-of-automation-items": 42213,
19621835 "options-chase-midi-note-on-cc-pc-pitch-in-project-playback": 41992,
1963 "options-chase-non-fx-envelopes-to-automation-items-when-underlying-envelope-is-bypassed":
1964 42344,
1836 "options-chase-non-fx-envelopes-to-automation-items-when-underlying-envelope-is-bypassed": 42344,
19651837 "options-crossfade-center-when-splitting": 43193,
19661838 "options-crossfade-left-when-splitting": 43191,
19671839 "options-crossfade-right-when-splitting": 43192,
19681840 "options-cycle-ripple-editing-mode": 1155,
1969 "options-cycle-split-options-crossfade-left-crossfade-center-crossfade-right-no-crossfade":
1970 43196,
1971 "options-cycle-through-editing-modes-auto-crossfade-off-auto-crossfade-on-trim-content-behind-media-items":
1972 41116,
1841 "options-cycle-split-options-crossfade-left-crossfade-center-crossfade-right-no-crossfade": 43196,
1842 "options-cycle-through-editing-modes-auto-crossfade-off-auto-crossfade-on-trim-content-behind-media-items": 41116,
19731843 "options-disable-auto-crossfades": 41119,
1974 "options-disable-crossfade-on-split-disregard-toolbar-auto-crossfade-button":
1975 40928,
1844 "options-disable-crossfade-on-split-disregard-toolbar-auto-crossfade-button": 40928,
19761845 "options-disable-display-group-names-colors-in-arrange-view": 43653,
19771846 "options-disable-metronome": 41746,
19781847 "options-disable-trim-content-behind-media-items-when-editing": 41121,
1979 "options-do-not-change-comp-area-source-lane-when-clicking-empty-track-space":
1980 42794,
1981 "options-do-not-display-tracks-in-folder-when-folder-is-fully-collapsed":
1982 42699,
1848 "options-do-not-change-comp-area-source-lane-when-clicking-empty-track-space": 42794,
1849 "options-do-not-display-tracks-in-folder-when-folder-is-fully-collapsed": 42699,
19831850 "options-enable-auto-crossfades": 41118,
1984 "options-enable-crossfade-on-split-disregard-toolbar-auto-crossfade-button":
1985 40927,
1851 "options-enable-crossfade-on-split-disregard-toolbar-auto-crossfade-button": 40927,
19861852 "options-enable-display-group-names-colors-in-arrange-view": 43652,
19871853 "options-enable-metronome": 41745,
19881854 "options-enable-trim-content-behind-media-items-when-editing": 41120,
19891855 "options-envelope-point-selection-follows-time-selection": 41576,
1990 "options-limit-media-item-edge-edits-to-source-media-content-for-unlooped-media-items":
1991 42218,
1856 "options-limit-media-item-edge-edits-to-source-media-content-for-unlooped-media-items": 42218,
19921857 "options-loop-new-automation-items-by-default": 42195,
19931858 "options-loop-recording-always-adds-takes": 40114,
19941859 "options-mouse-modifier-preferences": 41356,
19951860 "options-move-edit-cursor-to-end-of-recording-when-recording-ends": 40300,
1996 "options-move-edit-cursor-to-start-of-time-selection-when-time-selection-changes":
1997 40276,
1861 "options-move-edit-cursor-to-start-of-time-selection-when-time-selection-changes": 40276,
19981862 "options-move-envelope-points-with-media-items": 40070,
19991863 "options-move-take-envelope-points-when-moving-media-item-contents": 43636,
2000 "options-new-recording-adds-lanes-in-layers-multiple-lanes-play-at-once":
2001 43151,
1864 "options-new-recording-adds-lanes-in-layers-multiple-lanes-play-at-once": 43151,
20021865 "options-new-recording-adds-lanes-new-lanes-play-exclusively": 43152,
20031866 "options-new-recording-adds-media-items-in-layers": 42677,
20041867 "options-new-recording-does-not-add-lanes": 43153,
......@@ -2008,8 +1871,7 @@ export const REAPER_ACTIONS = {
20081871 "options-pre-fader-track-metering": 42076,
20091872 "options-preferences": 40016,
20101873 "options-preserve-trailing-values-when-recording-automation": 42640,
2011 "options-prevent-mouse-edits-of-single-envelope-points-from-moving-past-other-envelope-points":
2012 42202,
1874 "options-prevent-mouse-edits-of-single-envelope-points-from-moving-past-other-envelope-points": 42202,
20131875 "options-razor-edits-in-media-item-lane-affect-all-track-envelopes": 42459,
20141876 "options-razor-edits-on-collapsed-fixed-lane-tracks-affect-all-lanes": 42601,
20151877 "options-razor-edits-on-small-fixed-lane-tracks-affect-all-lanes": 43097,
......@@ -2034,49 +1896,39 @@ export const REAPER_ACTIONS = {
20341896 "options-show-fx-inserts-in-tcp-when-size-permits": 40302,
20351897 "options-show-lock-settings": 40277,
20361898 "options-show-metronome-pre-roll-settings": 40363,
2037 "options-show-peak-value-tooltips-on-media-items-and-loudness-if-peaks-are-configured-to-calculate-it":
2038 43149,
1899 "options-show-peak-value-tooltips-on-media-items-and-loudness-if-peaks-are-configured-to-calculate-it": 43149,
20391900 "options-show-sends-in-tcp-when-size-permits": 40677,
20401901 "options-show-snap-grid-settings": 40071,
20411902 "options-show-theme-adjuster": 42234,
20421903 "options-show-theme-color-controls": 42392,
20431904 "options-show-tooltips-on-media-items-and-envelopes": 41344,
20441905 "options-solo-in-front": 40745,
2045 "options-solo-via-dedicated-solo-bus-preference-master-outputs-can-be-set-to-bypass-solo-bus":
2046 43631,
1906 "options-solo-via-dedicated-solo-bus-preference-master-outputs-can-be-set-to-bypass-solo-bus": 43631,
20471907 "options-switch-to-a-random-color-theme": 40383,
20481908 "options-switch-to-next-color-theme": 40381,
20491909 "options-switch-to-previous-color-theme": 40382,
20501910 "options-toggle-always-on-top": 40239,
20511911 "options-toggle-auto-fade-auto-crossfade-when-comping-in-fixed-lanes": 42631,
2052 "options-toggle-crossfade-on-split-disregard-toolbar-auto-crossfade-button":
2053 40912,
1912 "options-toggle-crossfade-on-split-disregard-toolbar-auto-crossfade-button": 40912,
20541913 "options-toggle-display-group-names-colors-in-arrange-view": 43651,
2055 "options-toggle-editing-active-take-source-start-offset-slip-editing-adjusts-all-takes":
2056 41338,
1914 "options-toggle-editing-active-take-source-start-offset-slip-editing-adjusts-all-takes": 41338,
20571915 "options-toggle-grid-lines": 40145,
20581916 "options-toggle-item-grouping-and-track-media-razor-edit-grouping": 1156,
20591917 "options-toggle-locking": 1135,
20601918 "options-toggle-loop-points-linked-to-time-selection": 40621,
20611919 "options-toggle-metronome": 40364,
2062 "options-toggle-new-recording-adds-lanes-in-layers-multiple-lanes-play-at-once":
2063 41329,
1920 "options-toggle-new-recording-adds-lanes-in-layers-multiple-lanes-play-at-once": 41329,
20641921 "options-toggle-new-recording-adds-lanes-new-lanes-play-exclusively": 42702,
2065 "options-toggle-pooled-ghost-midi-source-data-when-copying-media-items":
2066 41071,
1922 "options-toggle-pooled-ghost-midi-source-data-when-copying-media-items": 41071,
20671923 "options-toggle-running-fx-when-playback-is-stopped": 41583,
20681924 "options-toggle-smooth-seek-see-preferences-audio-seeking": 40390,
20691925 "options-toggle-snapping": 1157,
2070 "options-track-media-razor-edit-grouping-affects-only-items-that-start-and-end-at-the-same-time":
2071 42788,
2072 "options-trim-content-behind-automation-items-when-editing-or-writing-automation":
2073 42206,
1926 "options-track-media-razor-edit-grouping-affects-only-items-that-start-and-end-at-the-same-time": 42788,
1927 "options-trim-content-behind-automation-items-when-editing-or-writing-automation": 42206,
20741928 "options-trim-content-behind-media-items-when-editing": 41117,
20751929 "options-unset-loop-points-linked-to-time-selection": 40750,
2076 "options-when-auto-punch-recording-into-a-fixed-lane-track-add-the-whole-recording":
2077 42793,
2078 "options-when-importing-copy-imported-media-to-project-media-directory":
2079 40263,
1930 "options-when-auto-punch-recording-into-a-fixed-lane-track-add-the-whole-recording": 42793,
1931 "options-when-importing-copy-imported-media-to-project-media-directory": 40263,
20801932 "peaks-build-any-missing-peaks": 40047,
20811933 "peaks-build-any-missing-peaks-for-selected-items": 40245,
20821934 "peaks-decrease-peaks-display-zoom-for-project": 40156,
......@@ -2097,18 +1949,15 @@ export const REAPER_ACTIONS = {
20971949 "peaks-rectify-peaks": 42307,
20981950 "peaks-remove-all-peak-cache-files": 40097,
20991951 "peaks-reset-peaks-display-zoom-for-project": 42449,
2100 "peaks-scale-peaks-by-square-root-half-of-range-is-12db-rather-than-6db":
2101 42306,
1952 "peaks-scale-peaks-by-square-root-half-of-range-is-12db-rather-than-6db": 42306,
21021953 "peaks-show-normal-peaks": 42301,
21031954 "peaks-toggle-color-peaks-by-momentary-loudness-lufs-m": 43145,
21041955 "peaks-toggle-color-peaks-by-short-term-loudness-lufs-s": 43147,
21051956 "peaks-toggle-normal-peaks-plus-spectrogram": 42295,
21061957 "peaks-toggle-show-graph-of-momentary-loudness-lufs-m": 43146,
21071958 "peaks-toggle-show-graph-of-short-term-loudness-lufs-s": 43148,
2108 "peaks-toggle-show-spectral-peaks-and-graph-of-momentary-loudness-lufs-m":
2109 43207,
2110 "peaks-toggle-show-spectral-peaks-and-graph-of-short-term-loudness-lufs-s":
2111 43208,
1959 "peaks-toggle-show-spectral-peaks-and-graph-of-momentary-loudness-lufs-m": 43207,
1960 "peaks-toggle-show-spectral-peaks-and-graph-of-short-term-loudness-lufs-s": 43208,
21121961 "peaks-toggle-spectral-peaks": 42073,
21131962 "peaks-toggle-spectral-peaks-plus-spectrogram": 43209,
21141963 "peaks-toggle-spectrogram": 42294,
......@@ -2127,21 +1976,16 @@ export const REAPER_ACTIONS = {
21271976 "project-set-project-timebase-to-beats-position-only": 43463,
21281977 "project-set-project-timebase-to-time": 43461,
21291978 "project-set-tempo-time-signature-envelope-timebase-to-beats": 43571,
2130 "project-set-tempo-time-signature-envelope-timebase-to-beats-for-time-signature-time-for-tempo":
2131 43572,
1979 "project-set-tempo-time-signature-envelope-timebase-to-beats-for-time-signature-time-for-tempo": 43572,
21321980 "project-set-tempo-time-signature-envelope-timebase-to-time": 43570,
21331981 "project-tabs-always-show-project-tabs": 40874,
21341982 "project-tabs-auto-offline-background-project-media": 40872,
2135 "project-tabs-defer-rendering-of-subprojects-render-on-tab-switch-rather-than-save":
2136 41998,
2137 "project-tabs-display-video-from-background-projects-if-active-project-lacks-video":
2138 42653,
2139 "project-tabs-do-not-automatically-render-subprojects-require-manual-render":
2140 42333,
1983 "project-tabs-defer-rendering-of-subprojects-render-on-tab-switch-rather-than-save": 41998,
1984 "project-tabs-display-video-from-background-projects-if-active-project-lacks-video": 42653,
1985 "project-tabs-do-not-automatically-render-subprojects-require-manual-render": 42333,
21411986 "project-tabs-force-project-tabs-visible-when-monitoring-fx-in-use": 42072,
21421987 "project-tabs-hide-all-background-project-windows": 40909,
2143 "project-tabs-leave-subproject-open-in-tab-after-automatic-open-and-render":
2144 42012,
1988 "project-tabs-leave-subproject-open-in-tab-after-automatic-open-and-render": 42012,
21451989 "project-tabs-move-project-tab-left-by-one": 3242,
21461990 "project-tabs-move-project-tab-right-by-one": 3243,
21471991 "project-tabs-move-project-tab-to-position-1": 3182,
......@@ -2165,8 +2009,7 @@ export const REAPER_ACTIONS = {
21652009 "project-tabs-move-project-tab-to-position-n-8": 3220,
21662010 "project-tabs-move-project-tab-to-position-n-9": 3221,
21672011 "project-tabs-play-stopped-background-projects-with-active-project": 41062,
2168 "project-tabs-prompt-before-automatic-rerender-of-background-subprojects":
2169 42334,
2012 "project-tabs-prompt-before-automatic-rerender-of-background-subprojects": 42334,
21702013 "project-tabs-run-background-projects": 40871,
21712014 "project-tabs-run-stopped-background-projects": 40873,
21722015 "project-tabs-show-project-tabs-on-left-side-of-window": 41883,
......@@ -2193,8 +2036,7 @@ export const REAPER_ACTIONS = {
21932036 "project-tabs-switch-to-project-tab-n-7": 3159,
21942037 "project-tabs-switch-to-project-tab-n-8": 3160,
21952038 "project-tabs-switch-to-project-tab-n-9": 3161,
2196 "project-tabs-synchronize-any-parent-projects-when-playing-back-subproject":
2197 41994,
2039 "project-tabs-synchronize-any-parent-projects-when-playing-back-subproject": 41994,
21982040 "project-tabs-synchronize-play-start-times-w-play-background-projects": 41063,
21992041 "project-toggle-project-timebase-to-time": 43637,
22002042 "razor-edit-clear-all-areas": 42406,
......@@ -2224,95 +2066,52 @@ export const REAPER_ACTIONS = {
22242066 "record-set-record-mode-to-selected-item-auto-punch": 40253,
22252067 "record-set-record-mode-to-time-selection-auto-punch": 40076,
22262068 "record-start-new-files-during-recording": 40666,
2227 "region-render-matrix-add-selected-tracks-to-render-list-for-all-regions":
2228 41893,
2069 "region-render-matrix-add-selected-tracks-to-render-list-for-all-regions": 41893,
22292070 "region-render-matrix-render-all-tracks-for-all-regions": 41891,
22302071 "region-render-matrix-render-master-mix-for-all-regions": 41890,
22312072 "region-render-matrix-render-only-selected-tracks-for-all-regions": 41892,
2232 "regions-go-to-next-region-after-current-region-finishes-playing-smooth-seek":
2233 41802,
2234 "regions-go-to-previous-region-after-current-region-finishes-playing-smooth-seek":
2235 41801,
2236 "regions-go-to-region-01-after-current-region-finishes-playing-smooth-seek":
2237 41761,
2238 "regions-go-to-region-02-after-current-region-finishes-playing-smooth-seek":
2239 41762,
2240 "regions-go-to-region-03-after-current-region-finishes-playing-smooth-seek":
2241 41763,
2242 "regions-go-to-region-04-after-current-region-finishes-playing-smooth-seek":
2243 41764,
2244 "regions-go-to-region-05-after-current-region-finishes-playing-smooth-seek":
2245 41765,
2246 "regions-go-to-region-06-after-current-region-finishes-playing-smooth-seek":
2247 41766,
2248 "regions-go-to-region-07-after-current-region-finishes-playing-smooth-seek":
2249 41767,
2250 "regions-go-to-region-08-after-current-region-finishes-playing-smooth-seek":
2251 41768,
2252 "regions-go-to-region-09-after-current-region-finishes-playing-smooth-seek":
2253 41769,
2254 "regions-go-to-region-10-after-current-region-finishes-playing-smooth-seek":
2255 41770,
2256 "regions-go-to-region-11-after-current-region-finishes-playing-smooth-seek":
2257 41771,
2258 "regions-go-to-region-12-after-current-region-finishes-playing-smooth-seek":
2259 41772,
2260 "regions-go-to-region-13-after-current-region-finishes-playing-smooth-seek":
2261 41773,
2262 "regions-go-to-region-14-after-current-region-finishes-playing-smooth-seek":
2263 41774,
2264 "regions-go-to-region-15-after-current-region-finishes-playing-smooth-seek":
2265 41775,
2266 "regions-go-to-region-16-after-current-region-finishes-playing-smooth-seek":
2267 41776,
2268 "regions-go-to-region-17-after-current-region-finishes-playing-smooth-seek":
2269 41777,
2270 "regions-go-to-region-18-after-current-region-finishes-playing-smooth-seek":
2271 41778,
2272 "regions-go-to-region-19-after-current-region-finishes-playing-smooth-seek":
2273 41779,
2274 "regions-go-to-region-20-after-current-region-finishes-playing-smooth-seek":
2275 41780,
2276 "regions-go-to-region-21-after-current-region-finishes-playing-smooth-seek":
2277 41781,
2278 "regions-go-to-region-22-after-current-region-finishes-playing-smooth-seek":
2279 41782,
2280 "regions-go-to-region-23-after-current-region-finishes-playing-smooth-seek":
2281 41783,
2282 "regions-go-to-region-24-after-current-region-finishes-playing-smooth-seek":
2283 41784,
2284 "regions-go-to-region-25-after-current-region-finishes-playing-smooth-seek":
2285 41785,
2286 "regions-go-to-region-26-after-current-region-finishes-playing-smooth-seek":
2287 41786,
2288 "regions-go-to-region-27-after-current-region-finishes-playing-smooth-seek":
2289 41787,
2290 "regions-go-to-region-28-after-current-region-finishes-playing-smooth-seek":
2291 41788,
2292 "regions-go-to-region-29-after-current-region-finishes-playing-smooth-seek":
2293 41789,
2294 "regions-go-to-region-30-after-current-region-finishes-playing-smooth-seek":
2295 41790,
2296 "regions-go-to-region-31-after-current-region-finishes-playing-smooth-seek":
2297 41791,
2298 "regions-go-to-region-32-after-current-region-finishes-playing-smooth-seek":
2299 41792,
2300 "regions-go-to-region-33-after-current-region-finishes-playing-smooth-seek":
2301 41793,
2302 "regions-go-to-region-34-after-current-region-finishes-playing-smooth-seek":
2303 41794,
2304 "regions-go-to-region-35-after-current-region-finishes-playing-smooth-seek":
2305 41795,
2306 "regions-go-to-region-36-after-current-region-finishes-playing-smooth-seek":
2307 41796,
2308 "regions-go-to-region-37-after-current-region-finishes-playing-smooth-seek":
2309 41797,
2310 "regions-go-to-region-38-after-current-region-finishes-playing-smooth-seek":
2311 41798,
2312 "regions-go-to-region-39-after-current-region-finishes-playing-smooth-seek":
2313 41799,
2314 "regions-go-to-region-40-after-current-region-finishes-playing-smooth-seek":
2315 41800,
2073 "regions-go-to-next-region-after-current-region-finishes-playing-smooth-seek": 41802,
2074 "regions-go-to-previous-region-after-current-region-finishes-playing-smooth-seek": 41801,
2075 "regions-go-to-region-01-after-current-region-finishes-playing-smooth-seek": 41761,
2076 "regions-go-to-region-02-after-current-region-finishes-playing-smooth-seek": 41762,
2077 "regions-go-to-region-03-after-current-region-finishes-playing-smooth-seek": 41763,
2078 "regions-go-to-region-04-after-current-region-finishes-playing-smooth-seek": 41764,
2079 "regions-go-to-region-05-after-current-region-finishes-playing-smooth-seek": 41765,
2080 "regions-go-to-region-06-after-current-region-finishes-playing-smooth-seek": 41766,
2081 "regions-go-to-region-07-after-current-region-finishes-playing-smooth-seek": 41767,
2082 "regions-go-to-region-08-after-current-region-finishes-playing-smooth-seek": 41768,
2083 "regions-go-to-region-09-after-current-region-finishes-playing-smooth-seek": 41769,
2084 "regions-go-to-region-10-after-current-region-finishes-playing-smooth-seek": 41770,
2085 "regions-go-to-region-11-after-current-region-finishes-playing-smooth-seek": 41771,
2086 "regions-go-to-region-12-after-current-region-finishes-playing-smooth-seek": 41772,
2087 "regions-go-to-region-13-after-current-region-finishes-playing-smooth-seek": 41773,
2088 "regions-go-to-region-14-after-current-region-finishes-playing-smooth-seek": 41774,
2089 "regions-go-to-region-15-after-current-region-finishes-playing-smooth-seek": 41775,
2090 "regions-go-to-region-16-after-current-region-finishes-playing-smooth-seek": 41776,
2091 "regions-go-to-region-17-after-current-region-finishes-playing-smooth-seek": 41777,
2092 "regions-go-to-region-18-after-current-region-finishes-playing-smooth-seek": 41778,
2093 "regions-go-to-region-19-after-current-region-finishes-playing-smooth-seek": 41779,
2094 "regions-go-to-region-20-after-current-region-finishes-playing-smooth-seek": 41780,
2095 "regions-go-to-region-21-after-current-region-finishes-playing-smooth-seek": 41781,
2096 "regions-go-to-region-22-after-current-region-finishes-playing-smooth-seek": 41782,
2097 "regions-go-to-region-23-after-current-region-finishes-playing-smooth-seek": 41783,
2098 "regions-go-to-region-24-after-current-region-finishes-playing-smooth-seek": 41784,
2099 "regions-go-to-region-25-after-current-region-finishes-playing-smooth-seek": 41785,
2100 "regions-go-to-region-26-after-current-region-finishes-playing-smooth-seek": 41786,
2101 "regions-go-to-region-27-after-current-region-finishes-playing-smooth-seek": 41787,
2102 "regions-go-to-region-28-after-current-region-finishes-playing-smooth-seek": 41788,
2103 "regions-go-to-region-29-after-current-region-finishes-playing-smooth-seek": 41789,
2104 "regions-go-to-region-30-after-current-region-finishes-playing-smooth-seek": 41790,
2105 "regions-go-to-region-31-after-current-region-finishes-playing-smooth-seek": 41791,
2106 "regions-go-to-region-32-after-current-region-finishes-playing-smooth-seek": 41792,
2107 "regions-go-to-region-33-after-current-region-finishes-playing-smooth-seek": 41793,
2108 "regions-go-to-region-34-after-current-region-finishes-playing-smooth-seek": 41794,
2109 "regions-go-to-region-35-after-current-region-finishes-playing-smooth-seek": 41795,
2110 "regions-go-to-region-36-after-current-region-finishes-playing-smooth-seek": 41796,
2111 "regions-go-to-region-37-after-current-region-finishes-playing-smooth-seek": 41797,
2112 "regions-go-to-region-38-after-current-region-finishes-playing-smooth-seek": 41798,
2113 "regions-go-to-region-39-after-current-region-finishes-playing-smooth-seek": 41799,
2114 "regions-go-to-region-40-after-current-region-finishes-playing-smooth-seek": 41800,
23162115 "regions-select-unselect-all-regions-for-rendering": 42679,
23172116 "regions-set-loop-points-to-current-region": 43102,
23182117 "regions-set-loop-points-to-next-region": 43144,
......@@ -2328,10 +2127,8 @@ export const REAPER_ACTIONS = {
23282127 "ruler-display-project-regions-markers-as-gridlines-in-arrange-view": 42328,
23292128 "ruler-display-region-number-even-if-region-is-named": 42435,
23302129 "ruler-display-region-number-name-when-region-edge-is-not-visible": 42436,
2331 "ruler-display-selected-regions-over-unselected-regions-when-overlapping":
2332 43206,
2333 "ruler-display-tempo-and-time-signature-changes-in-separate-lanes-when-size-permits":
2334 42325,
2130 "ruler-display-selected-regions-over-unselected-regions-when-overlapping": 43206,
2131 "ruler-display-tempo-and-time-signature-changes-in-separate-lanes-when-size-permits": 42325,
23352132 "ruler-display-tempo-changes": 42326,
23362133 "ruler-display-time-signature-changes": 42327,
23372134 "ruler-display-time-signature-changes-as-gridlines-in-arrange-view": 42329,
......@@ -2408,8 +2205,7 @@ export const REAPER_ACTIONS = {
24082205 "scrub-play-one-one-shot-segment-scrub-at-edit-cursor": 43594,
24092206 "scrub-prompt-to-edit-looped-segment-scrub-range": 43632,
24102207 "scrub-toggle-looped-segment-scrub-at-edit-cursor": 41187,
2411 "scrub-toggle-preference-for-one-shot-segment-scrub-when-moving-edit-cursor":
2412 43593,
2208 "scrub-toggle-preference-for-one-shot-segment-scrub-when-moving-edit-cursor": 43593,
24132209 "select-all-items-tracks-envelope-points-depending-on-focus": 40035,
24142210 "selection-set-load-set-number-01": 41239,
24152211 "selection-set-load-set-number-02": 41240,
......@@ -2451,8 +2247,7 @@ export const REAPER_ACTIONS = {
24512247 "set-project-recording-pass-counter-recpass-wildcard": 42032,
24522248 "set-project-recording-tag-rectag-wildcard": 43464,
24532249 "set-project-tempo-from-time-selection-detect-tempo": 41597,
2454 "set-project-tempo-from-time-selection-detect-tempo-align-items-and-loop-points-to-measure-start":
2455 40002,
2250 "set-project-tempo-from-time-selection-detect-tempo-align-items-and-loop-points-to-measure-start": 40002,
24562251 "set-project-tempo-from-time-selection-new-time-signature": 40843,
24572252 "set-ripple-editing-all-tracks": 40311,
24582253 "set-ripple-editing-off": 40309,
......@@ -2512,17 +2307,13 @@ export const REAPER_ACTIONS = {
25122307 "sws-aw-fade-in-out-crossfade-selected-area-of-selected-items": 53506,
25132308 "sws-aw-fill-gaps-between-selected-items-advanced": 53492,
25142309 "sws-aw-fill-gaps-between-selected-items-advanced-use-last-settings": 53493,
2515 "sws-aw-fill-gaps-between-selected-items-quick-crossfade-using-default-fade-length":
2516 53495,
2310 "sws-aw-fill-gaps-between-selected-items-quick-crossfade-using-default-fade-length": 53495,
25172311 "sws-aw-fill-gaps-between-selected-items-quick-no-crossfade": 53494,
25182312 "sws-aw-insert-click-track": 53555,
2519 "sws-aw-nf-toggle-assign-random-colors-if-auto-group-newly-recorded-items-is-enabled":
2520 53504,
2313 "sws-aw-nf-toggle-assign-random-colors-if-auto-group-newly-recorded-items-is-enabled": 53504,
25212314 "sws-aw-paste": 53553,
2522 "sws-aw-play-stop-automatically-group-simultaneously-recorded-items-deprecated":
2523 53503,
2524 "sws-aw-record-automatically-group-simultaneously-recorded-items-deprecated":
2525 53500,
2315 "sws-aw-play-stop-automatically-group-simultaneously-recorded-items-deprecated": 53503,
2316 "sws-aw-record-automatically-group-simultaneously-recorded-items-deprecated": 53500,
25262317 "sws-aw-record-conditional-normal-or-time-selection-only": 53497,
25272318 "sws-aw-record-conditional-normal-or-time-selection-only-automatically-group-simultaneously-recorded-items-deprecated":
25282319 53501,
......@@ -2530,8 +2321,7 @@ export const REAPER_ACTIONS = {
25302321 53502,
25312322 "sws-aw-record-conditional-normal-time-selection-or-item-selection": 53498,
25322323 "sws-aw-remove-overlaps-in-selected-items-preserving-item-starts": 53496,
2533 "sws-aw-remove-tracks-items-env-obeying-time-selection-and-leaving-children":
2534 53554,
2324 "sws-aw-remove-tracks-items-env-obeying-time-selection-and-leaving-children": 53554,
25352325 "sws-aw-render-tracks-to-mono-stem-tracks-obeying-time-selection": 53558,
25362326 "sws-aw-render-tracks-to-stereo-stem-tracks-obeying-time-selection": 53557,
25372327 "sws-aw-select-all-items-in-group-if-grouping-is-enabled": 53560,
......@@ -2549,8 +2339,7 @@ export const REAPER_ACTIONS = {
25492339 "sws-aw-set-project-timebase-to-beats-position-length-rate": 53531,
25502340 "sws-aw-set-project-timebase-to-beats-position-only": 53530,
25512341 "sws-aw-set-project-timebase-to-time": 53529,
2552 "sws-aw-set-selected-items-timebase-to-beats-auto-stretch-at-tempo-changes":
2553 53566,
2342 "sws-aw-set-selected-items-timebase-to-beats-auto-stretch-at-tempo-changes": 53566,
25542343 "sws-aw-set-selected-items-timebase-to-beats-position-length-rate": 53539,
25552344 "sws-aw-set-selected-items-timebase-to-beats-position-only": 53538,
25562345 "sws-aw-set-selected-items-timebase-to-project-track-default": 53536,
......@@ -2578,13 +2367,10 @@ export const REAPER_ACTIONS = {
25782367 "sws-aw-toggle-triplet-grid": 53540,
25792368 "sws-aw-trim-selected-items-to-fill-selection": 53508,
25802369 "sws-aw-trim-selected-items-to-selection-or-cursor-crop": 53507,
2581 "sws-br-add-envelope-points-located-between-grid-to-existing-selection":
2582 54041,
2583 "sws-br-add-envelope-points-located-between-grid-to-existing-selection-obey-time-selection-if-any":
2584 54042,
2370 "sws-br-add-envelope-points-located-between-grid-to-existing-selection": 54041,
2371 "sws-br-add-envelope-points-located-between-grid-to-existing-selection-obey-time-selection-if-any": 54042,
25852372 "sws-br-add-envelope-points-located-on-grid-to-existing-selection": 54039,
2586 "sws-br-add-envelope-points-located-on-grid-to-existing-selection-obey-time-selection-if-any":
2587 54040,
2373 "sws-br-add-envelope-points-located-on-grid-to-existing-selection-obey-time-selection-if-any": 54040,
25882374 "sws-br-adjust-playrate-midi-cc-only": 54782,
25892375 "sws-br-adjust-playrate-options": 54783,
25902376 "sws-br-alter-slope-of-gradual-tempo-marker-decrease-0-001-bpm": 54826,
......@@ -2605,10 +2391,8 @@ export const REAPER_ACTIONS = {
26052391 "sws-br-alter-slope-of-gradual-tempo-marker-increase-01-percent": 54833,
26062392 "sws-br-analyze-loudness": 54171,
26072393 "sws-br-apply-next-action-to-all-visible-envelopes-in-selected-tracks": 54100,
2608 "sws-br-apply-next-action-to-all-visible-envelopes-in-selected-tracks-if-there-is-no-track-envelope-selected":
2609 54102,
2610 "sws-br-apply-next-action-to-all-visible-record-armed-envelopes-in-selected-tracks":
2611 54101,
2394 "sws-br-apply-next-action-to-all-visible-envelopes-in-selected-tracks-if-there-is-no-track-envelope-selected": 54102,
2395 "sws-br-apply-next-action-to-all-visible-record-armed-envelopes-in-selected-tracks": 54101,
26122396 "sws-br-apply-next-action-to-all-visible-record-armed-envelopes-in-selected-tracks-if-there-is-no-track-envelope-selected":
26132397 54103,
26142398 "sws-br-check-for-new-sws-version": 55821,
......@@ -2622,52 +2406,37 @@ export const REAPER_ACTIONS = {
26222406 54049,
26232407 "sws-br-convert-selected-points-in-selected-envelope-to-cc-events-in-last-clicked-cc-lane-in-last-active-midi-editor-clear-existing-events":
26242408 54050,
2625 "sws-br-copy-points-in-time-selection-in-selected-envelope-to-all-visible-envelopes-in-selected-tracks":
2626 54072,
2409 "sws-br-copy-points-in-time-selection-in-selected-envelope-to-all-visible-envelopes-in-selected-tracks": 54072,
26272410 "sws-br-copy-points-in-time-selection-in-selected-envelope-to-all-visible-envelopes-in-selected-tracks-paste-at-edit-cursor":
26282411 54076,
26292412 "sws-br-copy-points-in-time-selection-in-selected-envelope-to-all-visible-record-armed-in-envelopes-of-selected-tracks":
26302413 54073,
26312414 "sws-br-copy-points-in-time-selection-in-selected-envelope-to-all-visible-record-armed-in-envelopes-of-selected-tracks-paste-at-edit-cursor":
26322415 54077,
2633 "sws-br-copy-points-in-time-selection-in-selected-envelope-to-envelope-at-mouse-cursor":
2634 54079,
2635 "sws-br-copy-points-in-time-selection-in-selected-envelope-to-envelope-at-mouse-cursor-paste-at-edit-cursor":
2636 54081,
2637 "sws-br-copy-selected-points-in-selected-envelope-to-all-visible-envelopes-in-selected-tracks":
2638 54070,
2416 "sws-br-copy-points-in-time-selection-in-selected-envelope-to-envelope-at-mouse-cursor": 54079,
2417 "sws-br-copy-points-in-time-selection-in-selected-envelope-to-envelope-at-mouse-cursor-paste-at-edit-cursor": 54081,
2418 "sws-br-copy-selected-points-in-selected-envelope-to-all-visible-envelopes-in-selected-tracks": 54070,
26392419 "sws-br-copy-selected-points-in-selected-envelope-to-all-visible-envelopes-in-selected-tracks-paste-at-edit-cursor":
26402420 54074,
2641 "sws-br-copy-selected-points-in-selected-envelope-to-all-visible-record-armed-envelopes-in-selected-tracks":
2642 54071,
2421 "sws-br-copy-selected-points-in-selected-envelope-to-all-visible-record-armed-envelopes-in-selected-tracks": 54071,
26432422 "sws-br-copy-selected-points-in-selected-envelope-to-all-visible-record-armed-envelopes-in-selected-tracks-paste-at-edit-cursor":
26442423 54075,
2645 "sws-br-copy-selected-points-in-selected-envelope-to-envelope-at-mouse-cursor":
2646 54078,
2647 "sws-br-copy-selected-points-in-selected-envelope-to-to-envelope-at-mouse-cursor-paste-at-edit-cursor":
2648 54080,
2649 "sws-br-copy-take-media-source-file-path-of-selected-items-to-clipboard":
2650 54503,
2424 "sws-br-copy-selected-points-in-selected-envelope-to-envelope-at-mouse-cursor": 54078,
2425 "sws-br-copy-selected-points-in-selected-envelope-to-to-envelope-at-mouse-cursor-paste-at-edit-cursor": 54080,
2426 "sws-br-copy-take-media-source-file-path-of-selected-items-to-clipboard": 54503,
26512427 "sws-br-create-project-marker-at-mouse-cursor": 54472,
26522428 "sws-br-create-project-marker-at-mouse-cursor-obey-snapping": 54473,
26532429 "sws-br-create-project-markers-from-notes-in-selected-midi-items": 54470,
2654 "sws-br-create-project-markers-from-selected-items-name-by-item-s-notes":
2655 54474,
2430 "sws-br-create-project-markers-from-selected-items-name-by-item-s-notes": 54474,
26562431 "sws-br-create-project-markers-from-selected-tempo-markers": 54464,
26572432 "sws-br-create-project-markers-from-stretch-markers-in-selected-items": 54471,
26582433 "sws-br-create-regions-from-selected-items-name-by-item-s-notes": 54475,
2659 "sws-br-create-tempo-markers-at-grid-after-every-selected-tempo-marker":
2660 54841,
2661 "sws-br-decrease-selected-envelope-points-by-0-1-db-volume-envelope-only":
2662 54089,
2663 "sws-br-decrease-selected-envelope-points-by-0-5-db-volume-envelope-only":
2664 54090,
2665 "sws-br-decrease-selected-envelope-points-by-1-db-volume-envelope-only":
2666 54091,
2667 "sws-br-decrease-selected-envelope-points-by-10-db-volume-envelope-only":
2668 54093,
2669 "sws-br-decrease-selected-envelope-points-by-5-db-volume-envelope-only":
2670 54092,
2434 "sws-br-create-tempo-markers-at-grid-after-every-selected-tempo-marker": 54841,
2435 "sws-br-decrease-selected-envelope-points-by-0-1-db-volume-envelope-only": 54089,
2436 "sws-br-decrease-selected-envelope-points-by-0-5-db-volume-envelope-only": 54090,
2437 "sws-br-decrease-selected-envelope-points-by-1-db-volume-envelope-only": 54091,
2438 "sws-br-decrease-selected-envelope-points-by-10-db-volume-envelope-only": 54093,
2439 "sws-br-decrease-selected-envelope-points-by-5-db-volume-envelope-only": 54092,
26712440 "sws-br-decrease-tempo-marker-0-001-bpm-preserve-overall-tempo": 54810,
26722441 "sws-br-decrease-tempo-marker-0-001-percent-preserve-overall-tempo": 54818,
26732442 "sws-br-decrease-tempo-marker-0-01-bpm-preserve-overall-tempo": 54811,
......@@ -2679,40 +2448,26 @@ export const REAPER_ACTIONS = {
26792448 "sws-br-delete-envelope-point-at-mouse-cursor": 54098,
26802449 "sws-br-delete-envelope-point-at-mouse-cursor-selected-envelope-only": 54097,
26812450 "sws-br-delete-envelope-points-between-grid": 54045,
2682 "sws-br-delete-envelope-points-between-grid-obey-time-selection-if-any":
2683 54046,
2451 "sws-br-delete-envelope-points-between-grid-obey-time-selection-if-any": 54046,
26842452 "sws-br-delete-envelope-points-on-grid": 54043,
26852453 "sws-br-delete-envelope-points-on-grid-obey-time-selection-if-any": 54044,
26862454 "sws-br-delete-take-under-mouse-cursor": 54504,
2687 "sws-br-delete-tempo-marker-and-preserve-position-and-length-of-items-including-midi-events":
2688 54839,
2689 "sws-br-delete-tempo-marker-and-preserve-position-and-length-of-selected-items-including-midi-events":
2690 54840,
2691 "sws-br-delete-tempo-marker-preserve-overall-tempo-and-positions-if-possible":
2692 54838,
2455 "sws-br-delete-tempo-marker-and-preserve-position-and-length-of-items-including-midi-events": 54839,
2456 "sws-br-delete-tempo-marker-and-preserve-position-and-length-of-selected-items-including-midi-events": 54840,
2457 "sws-br-delete-tempo-marker-preserve-overall-tempo-and-positions-if-possible": 54838,
26932458 "sws-br-disable-ignore-project-tempo-for-selected-midi-items": 54465,
2694 "sws-br-disable-ignore-project-tempo-for-selected-midi-items-preserving-time-position-of-midi-events":
2695 54466,
2459 "sws-br-disable-ignore-project-tempo-for-selected-midi-items-preserving-time-position-of-midi-events": 54466,
26962460 "sws-br-enable-ignore-project-tempo-for-selected-midi-items-preserving-time-position-of-midi-events-use-tempo-at-item-s-start":
26972461 54468,
2698 "sws-br-enable-ignore-project-tempo-for-selected-midi-items-use-tempo-at-item-s-start":
2699 54467,
2700 "sws-br-exclusive-toggle-contextual-toolbar-under-mouse-cursor-preset-1":
2701 53979,
2702 "sws-br-exclusive-toggle-contextual-toolbar-under-mouse-cursor-preset-2":
2703 53984,
2704 "sws-br-exclusive-toggle-contextual-toolbar-under-mouse-cursor-preset-3":
2705 53989,
2706 "sws-br-exclusive-toggle-contextual-toolbar-under-mouse-cursor-preset-4":
2707 53994,
2708 "sws-br-exclusive-toggle-contextual-toolbar-under-mouse-cursor-preset-5":
2709 53999,
2710 "sws-br-exclusive-toggle-contextual-toolbar-under-mouse-cursor-preset-6":
2711 54004,
2712 "sws-br-exclusive-toggle-contextual-toolbar-under-mouse-cursor-preset-7":
2713 54009,
2714 "sws-br-exclusive-toggle-contextual-toolbar-under-mouse-cursor-preset-8":
2715 54014,
2462 "sws-br-enable-ignore-project-tempo-for-selected-midi-items-use-tempo-at-item-s-start": 54467,
2463 "sws-br-exclusive-toggle-contextual-toolbar-under-mouse-cursor-preset-1": 53979,
2464 "sws-br-exclusive-toggle-contextual-toolbar-under-mouse-cursor-preset-2": 53984,
2465 "sws-br-exclusive-toggle-contextual-toolbar-under-mouse-cursor-preset-3": 53989,
2466 "sws-br-exclusive-toggle-contextual-toolbar-under-mouse-cursor-preset-4": 53994,
2467 "sws-br-exclusive-toggle-contextual-toolbar-under-mouse-cursor-preset-5": 53999,
2468 "sws-br-exclusive-toggle-contextual-toolbar-under-mouse-cursor-preset-6": 54004,
2469 "sws-br-exclusive-toggle-contextual-toolbar-under-mouse-cursor-preset-7": 54009,
2470 "sws-br-exclusive-toggle-contextual-toolbar-under-mouse-cursor-preset-8": 54014,
27162471 "sws-br-expand-envelope-point-selection-to-the-left": 54028,
27172472 "sws-br-expand-envelope-point-selection-to-the-left-end-point-only": 54030,
27182473 "sws-br-expand-envelope-point-selection-to-the-right": 54027,
......@@ -2720,34 +2475,24 @@ export const REAPER_ACTIONS = {
27202475 "sws-br-fit-selected-envelope-points-to-time-selection": 54082,
27212476 "sws-br-focus-arrange": 54482,
27222477 "sws-br-focus-tracks": 54483,
2723 "sws-br-freehand-draw-envelope-while-snapping-points-to-left-side-grid-line-perform-until-shortcut-released":
2724 54854,
2478 "sws-br-freehand-draw-envelope-while-snapping-points-to-left-side-grid-line-perform-until-shortcut-released": 54854,
27252479 "sws-br-global-loudness-preferences": 54170,
27262480 "sws-br-hide-all-but-selected-track-envelope-for-all-tracks": 54136,
2727 "sws-br-hide-all-but-selected-track-envelope-for-all-tracks-except-envelopes-in-separate-lanes":
2728 54138,
2729 "sws-br-hide-all-but-selected-track-envelope-for-all-tracks-except-envelopes-in-track-lanes":
2730 54140,
2481 "sws-br-hide-all-but-selected-track-envelope-for-all-tracks-except-envelopes-in-separate-lanes": 54138,
2482 "sws-br-hide-all-but-selected-track-envelope-for-all-tracks-except-envelopes-in-track-lanes": 54140,
27312483 "sws-br-hide-all-but-selected-track-envelope-for-selected-tracks": 54137,
2732 "sws-br-hide-all-but-selected-track-envelope-for-selected-tracks-except-envelopes-in-separate-lanes":
2733 54139,
2734 "sws-br-hide-all-but-selected-track-envelope-for-selected-tracks-except-envelopes-in-track-lanes":
2735 54141,
2484 "sws-br-hide-all-but-selected-track-envelope-for-selected-tracks-except-envelopes-in-separate-lanes": 54139,
2485 "sws-br-hide-all-but-selected-track-envelope-for-selected-tracks-except-envelopes-in-track-lanes": 54141,
27362486 "sws-br-hide-all-fx-envelopes-for-selected-tracks": 54149,
27372487 "sws-br-hide-all-send-envelopes-for-selected-tracks": 54166,
27382488 "sws-br-hide-mute-send-envelopes-for-selected-tracks": 54169,
27392489 "sws-br-hide-pan-send-envelopes-for-selected-tracks": 54168,
27402490 "sws-br-hide-volume-send-envelopes-for-selected-tracks": 54167,
2741 "sws-br-increase-selected-envelope-points-by-0-1-db-volume-envelope-only":
2742 54084,
2743 "sws-br-increase-selected-envelope-points-by-0-5-db-volume-envelope-only":
2744 54085,
2745 "sws-br-increase-selected-envelope-points-by-1-db-volume-envelope-only":
2746 54086,
2747 "sws-br-increase-selected-envelope-points-by-10-db-volume-envelope-only":
2748 54088,
2749 "sws-br-increase-selected-envelope-points-by-5-db-volume-envelope-only":
2750 54087,
2491 "sws-br-increase-selected-envelope-points-by-0-1-db-volume-envelope-only": 54084,
2492 "sws-br-increase-selected-envelope-points-by-0-5-db-volume-envelope-only": 54085,
2493 "sws-br-increase-selected-envelope-points-by-1-db-volume-envelope-only": 54086,
2494 "sws-br-increase-selected-envelope-points-by-10-db-volume-envelope-only": 54088,
2495 "sws-br-increase-selected-envelope-points-by-5-db-volume-envelope-only": 54087,
27512496 "sws-br-increase-tempo-marker-0-001-bpm-preserve-overall-tempo": 54806,
27522497 "sws-br-increase-tempo-marker-0-001-percent-preserve-overall-tempo": 54814,
27532498 "sws-br-increase-tempo-marker-0-01-bpm-preserve-overall-tempo": 54807,
......@@ -2757,61 +2502,36 @@ export const REAPER_ACTIONS = {
27572502 "sws-br-increase-tempo-marker-01-bpm-preserve-overall-tempo": 54809,
27582503 "sws-br-increase-tempo-marker-01-percent-preserve-overall-tempo": 54817,
27592504 "sws-br-insert-2-envelope-points-at-time-selection": 54067,
2760 "sws-br-insert-2-envelope-points-at-time-selection-to-all-visible-track-envelopes":
2761 54068,
2762 "sws-br-insert-2-envelope-points-at-time-selection-to-all-visible-track-envelopes-in-selected-tracks":
2763 54069,
2764 "sws-br-insert-envelope-points-on-grid-using-shape-of-the-previous-point":
2765 54047,
2766 "sws-br-insert-envelope-points-on-grid-using-shape-of-the-previous-point-obey-time-selection-if-any":
2767 54048,
2768 "sws-br-insert-new-envelope-point-at-mouse-cursor-using-value-at-current-position-obey-snapping":
2769 54083,
2770 "sws-br-move-active-floating-track-fx-window-to-mouse-cursor-horizontal-left-vertical-bottom":
2771 54493,
2772 "sws-br-move-active-floating-track-fx-window-to-mouse-cursor-horizontal-left-vertical-middle":
2773 54494,
2774 "sws-br-move-active-floating-track-fx-window-to-mouse-cursor-horizontal-left-vertical-top":
2775 54495,
2776 "sws-br-move-active-floating-track-fx-window-to-mouse-cursor-horizontal-middle-vertical-bottom":
2777 54496,
2778 "sws-br-move-active-floating-track-fx-window-to-mouse-cursor-horizontal-middle-vertical-middle":
2779 54497,
2780 "sws-br-move-active-floating-track-fx-window-to-mouse-cursor-horizontal-middle-vertical-top":
2781 54498,
2782 "sws-br-move-active-floating-track-fx-window-to-mouse-cursor-horizontal-right-vertical-bottom":
2783 54499,
2784 "sws-br-move-active-floating-track-fx-window-to-mouse-cursor-horizontal-right-vertical-middle":
2785 54500,
2786 "sws-br-move-active-floating-track-fx-window-to-mouse-cursor-horizontal-right-vertical-top":
2787 54501,
2788 "sws-br-move-active-floating-window-to-mouse-cursor-horizontal-left-vertical-bottom":
2789 54484,
2790 "sws-br-move-active-floating-window-to-mouse-cursor-horizontal-left-vertical-middle":
2791 54485,
2792 "sws-br-move-active-floating-window-to-mouse-cursor-horizontal-left-vertical-top":
2793 54486,
2794 "sws-br-move-active-floating-window-to-mouse-cursor-horizontal-middle-vertical-bottom":
2795 54487,
2796 "sws-br-move-active-floating-window-to-mouse-cursor-horizontal-middle-vertical-middle":
2797 54488,
2798 "sws-br-move-active-floating-window-to-mouse-cursor-horizontal-middle-vertical-top":
2799 54489,
2800 "sws-br-move-active-floating-window-to-mouse-cursor-horizontal-right-vertical-bottom":
2801 54490,
2802 "sws-br-move-active-floating-window-to-mouse-cursor-horizontal-right-vertical-middle":
2803 54491,
2804 "sws-br-move-active-floating-window-to-mouse-cursor-horizontal-right-vertical-top":
2805 54492,
2505 "sws-br-insert-2-envelope-points-at-time-selection-to-all-visible-track-envelopes": 54068,
2506 "sws-br-insert-2-envelope-points-at-time-selection-to-all-visible-track-envelopes-in-selected-tracks": 54069,
2507 "sws-br-insert-envelope-points-on-grid-using-shape-of-the-previous-point": 54047,
2508 "sws-br-insert-envelope-points-on-grid-using-shape-of-the-previous-point-obey-time-selection-if-any": 54048,
2509 "sws-br-insert-new-envelope-point-at-mouse-cursor-using-value-at-current-position-obey-snapping": 54083,
2510 "sws-br-move-active-floating-track-fx-window-to-mouse-cursor-horizontal-left-vertical-bottom": 54493,
2511 "sws-br-move-active-floating-track-fx-window-to-mouse-cursor-horizontal-left-vertical-middle": 54494,
2512 "sws-br-move-active-floating-track-fx-window-to-mouse-cursor-horizontal-left-vertical-top": 54495,
2513 "sws-br-move-active-floating-track-fx-window-to-mouse-cursor-horizontal-middle-vertical-bottom": 54496,
2514 "sws-br-move-active-floating-track-fx-window-to-mouse-cursor-horizontal-middle-vertical-middle": 54497,
2515 "sws-br-move-active-floating-track-fx-window-to-mouse-cursor-horizontal-middle-vertical-top": 54498,
2516 "sws-br-move-active-floating-track-fx-window-to-mouse-cursor-horizontal-right-vertical-bottom": 54499,
2517 "sws-br-move-active-floating-track-fx-window-to-mouse-cursor-horizontal-right-vertical-middle": 54500,
2518 "sws-br-move-active-floating-track-fx-window-to-mouse-cursor-horizontal-right-vertical-top": 54501,
2519 "sws-br-move-active-floating-window-to-mouse-cursor-horizontal-left-vertical-bottom": 54484,
2520 "sws-br-move-active-floating-window-to-mouse-cursor-horizontal-left-vertical-middle": 54485,
2521 "sws-br-move-active-floating-window-to-mouse-cursor-horizontal-left-vertical-top": 54486,
2522 "sws-br-move-active-floating-window-to-mouse-cursor-horizontal-middle-vertical-bottom": 54487,
2523 "sws-br-move-active-floating-window-to-mouse-cursor-horizontal-middle-vertical-middle": 54488,
2524 "sws-br-move-active-floating-window-to-mouse-cursor-horizontal-middle-vertical-top": 54489,
2525 "sws-br-move-active-floating-window-to-mouse-cursor-horizontal-right-vertical-bottom": 54490,
2526 "sws-br-move-active-floating-window-to-mouse-cursor-horizontal-right-vertical-middle": 54491,
2527 "sws-br-move-active-floating-window-to-mouse-cursor-horizontal-right-vertical-top": 54492,
28062528 "sws-br-move-closest-envelope-point-to-edit-cursor": 54065,
28072529 "sws-br-move-closest-grid-line-to-edit-cursor": 54787,
2808 "sws-br-move-closest-grid-line-to-mouse-cursor-perform-until-shortcut-released":
2809 54871,
2530 "sws-br-move-closest-grid-line-to-mouse-cursor-perform-until-shortcut-released": 54871,
28102531 "sws-br-move-closest-grid-line-to-play-cursor": 54788,
28112532 "sws-br-move-closest-left-side-grid-line-to-edit-cursor": 54791,
28122533 "sws-br-move-closest-measure-grid-line-to-edit-cursor": 54789,
2813 "sws-br-move-closest-measure-grid-line-to-mouse-cursor-perform-until-shortcut-released":
2814 54872,
2534 "sws-br-move-closest-measure-grid-line-to-mouse-cursor-perform-until-shortcut-released": 54872,
28152535 "sws-br-move-closest-measure-grid-line-to-play-cursor": 54790,
28162536 "sws-br-move-closest-project-marker-to-edit-cursor": 54477,
28172537 "sws-br-move-closest-project-marker-to-edit-cursor-obey-snapping": 54480,
......@@ -2822,14 +2542,12 @@ export const REAPER_ACTIONS = {
28222542 "sws-br-move-closest-right-side-grid-line-to-edit-cursor": 54792,
28232543 "sws-br-move-closest-selected-envelope-point-to-edit-cursor": 54066,
28242544 "sws-br-move-closest-tempo-marker-to-edit-cursor": 54805,
2825 "sws-br-move-closest-tempo-marker-to-mouse-cursor-perform-until-shortcut-released":
2826 54870,
2545 "sws-br-move-closest-tempo-marker-to-mouse-cursor-perform-until-shortcut-released": 54870,
28272546 "sws-br-move-edit-cursor-to-next-envelope-point": 54019,
28282547 "sws-br-move-edit-cursor-to-next-envelope-point-and-add-to-selection": 54021,
28292548 "sws-br-move-edit-cursor-to-next-envelope-point-and-select-it": 54020,
28302549 "sws-br-move-edit-cursor-to-previous-envelope-point": 54022,
2831 "sws-br-move-edit-cursor-to-previous-envelope-point-and-add-to-selection":
2832 54024,
2550 "sws-br-move-edit-cursor-to-previous-envelope-point-and-add-to-selection": 54024,
28332551 "sws-br-move-edit-cursor-to-previous-envelope-point-and-select-it": 54023,
28342552 "sws-br-move-tempo-marker-back": 54804,
28352553 "sws-br-move-tempo-marker-back-0-1-ms": 54798,
......@@ -2843,8 +2561,7 @@ export const REAPER_ACTIONS = {
28432561 "sws-br-move-tempo-marker-forward-10-ms": 54795,
28442562 "sws-br-move-tempo-marker-forward-100-ms": 54796,
28452563 "sws-br-move-tempo-marker-forward-1000-ms": 54797,
2846 "sws-br-nf-toggle-use-dual-mono-mode-for-mono-takes-channel-modes-for-loudness-analyzing":
2847 54178,
2564 "sws-br-nf-toggle-use-dual-mono-mode-for-mono-takes-channel-modes-for-loudness-analyzing": 54178,
28482565 "sws-br-nf-toggle-use-high-precision-mode-for-loudness-analyzing": 54177,
28492566 "sws-br-normalize-loudness-of-selected-items-to-0-lu": 54174,
28502567 "sws-br-normalize-loudness-of-selected-items-to-23-lufs": 54173,
......@@ -2859,15 +2576,11 @@ export const REAPER_ACTIONS = {
28592576 "sws-br-open-close-contextual-toolbar-under-mouse-cursor-preset-6": 53964,
28602577 "sws-br-open-close-contextual-toolbar-under-mouse-cursor-preset-7": 53969,
28612578 "sws-br-open-close-contextual-toolbar-under-mouse-cursor-preset-8": 53974,
2862 "sws-br-options-automatically-insert-stretch-markers-when-inserting-tempo-markers-with-sws-actions":
2863 54720,
2579 "sws-br-options-automatically-insert-stretch-markers-when-inserting-tempo-markers-with-sws-actions": 54720,
28642580 "sws-br-options-cycle-through-record-modes": 54721,
2865 "sws-br-options-set-apply-trim-when-adding-volume-pan-envelopes-to-always":
2866 54685,
2867 "sws-br-options-set-apply-trim-when-adding-volume-pan-envelopes-to-in-read-write":
2868 54686,
2869 "sws-br-options-set-apply-trim-when-adding-volume-pan-envelopes-to-never":
2870 54687,
2581 "sws-br-options-set-apply-trim-when-adding-volume-pan-envelopes-to-always": 54685,
2582 "sws-br-options-set-apply-trim-when-adding-volume-pan-envelopes-to-in-read-write": 54686,
2583 "sws-br-options-set-apply-trim-when-adding-volume-pan-envelopes-to-never": 54687,
28712584 "sws-br-options-set-grid-line-z-order-to-over-items": 54714,
28722585 "sws-br-options-set-grid-line-z-order-to-through-items": 54715,
28732586 "sws-br-options-set-grid-line-z-order-to-under-items": 54716,
......@@ -2893,13 +2606,10 @@ export const REAPER_ACTIONS = {
28932606 "sws-br-options-toggle-flush-fx-on-stop": 54694,
28942607 "sws-br-options-toggle-flush-fx-when-looping": 54695,
28952608 "sws-br-options-toggle-grid-snap-settings-follow-grid-visibility": 54683,
2896 "sws-br-options-toggle-move-edit-cursor-to-end-of-recorded-items-on-record-stop":
2897 54711,
2898 "sws-br-options-toggle-move-edit-cursor-to-start-of-time-selection-on-time-selection-change":
2899 54709,
2609 "sws-br-options-toggle-move-edit-cursor-to-end-of-recorded-items-on-record-stop": 54711,
2610 "sws-br-options-toggle-move-edit-cursor-to-start-of-time-selection-on-time-selection-change": 54709,
29002611 "sws-br-options-toggle-move-edit-cursor-when-pasting-inserting-media": 54710,
2901 "sws-br-options-toggle-playback-position-follows-project-timebase-when-changing-tempo":
2902 54684,
2612 "sws-br-options-toggle-playback-position-follows-project-timebase-when-changing-tempo": 54684,
29032613 "sws-br-options-toggle-reset-cc-on-stop-play": 54693,
29042614 "sws-br-options-toggle-reset-pitch-on-stop-play": 54692,
29052615 "sws-br-options-toggle-scroll-view-to-edit-cursor-on-stop": 54713,
......@@ -2909,64 +2619,47 @@ export const REAPER_ACTIONS = {
29092619 54863,
29102620 "sws-br-play-from-edit-cursor-position-and-solo-track-under-mouse-for-the-duration-perform-until-shortcut-released":
29112621 54862,
2912 "sws-br-play-from-edit-cursor-position-perform-until-shortcut-released":
2913 54861,
2622 "sws-br-play-from-edit-cursor-position-perform-until-shortcut-released": 54861,
29142623 "sws-br-play-from-mouse-cursor-position": 54459,
29152624 "sws-br-play-from-mouse-cursor-position-and-solo-item-and-track-under-mouse-for-the-duration-perform-until-shortcut-released":
29162625 54860,
29172626 "sws-br-play-from-mouse-cursor-position-and-solo-track-under-mouse-for-the-duration-perform-until-shortcut-released":
29182627 54859,
2919 "sws-br-play-from-mouse-cursor-position-perform-until-shortcut-released":
2920 54858,
2628 "sws-br-play-from-mouse-cursor-position-perform-until-shortcut-released": 54858,
29212629 "sws-br-play-pause-from-mouse-cursor-position": 54460,
29222630 "sws-br-play-stop-from-mouse-cursor-position": 54461,
29232631 "sws-br-preview-media-item-under-mouse": 54722,
29242632 "sws-br-preview-media-item-under-mouse-and-pause-during-preview": 54725,
2925 "sws-br-preview-media-item-under-mouse-and-pause-during-preview-start-from-mouse-cursor-position":
2926 54726,
2633 "sws-br-preview-media-item-under-mouse-and-pause-during-preview-start-from-mouse-cursor-position": 54726,
29272634 "sws-br-preview-media-item-under-mouse-at-track-fader-volume": 54727,
2928 "sws-br-preview-media-item-under-mouse-at-track-fader-volume-and-pause-during-preview":
2929 54730,
2635 "sws-br-preview-media-item-under-mouse-at-track-fader-volume-and-pause-during-preview": 54730,
29302636 "sws-br-preview-media-item-under-mouse-at-track-fader-volume-and-pause-during-preview-start-from-mouse-cursor-position":
29312637 54731,
2932 "sws-br-preview-media-item-under-mouse-at-track-fader-volume-start-from-mouse-position":
2933 54728,
2934 "sws-br-preview-media-item-under-mouse-at-track-fader-volume-sync-with-next-measure":
2935 54729,
2936 "sws-br-preview-media-item-under-mouse-start-from-mouse-cursor-position":
2937 54723,
2638 "sws-br-preview-media-item-under-mouse-at-track-fader-volume-start-from-mouse-position": 54728,
2639 "sws-br-preview-media-item-under-mouse-at-track-fader-volume-sync-with-next-measure": 54729,
2640 "sws-br-preview-media-item-under-mouse-start-from-mouse-cursor-position": 54723,
29382641 "sws-br-preview-media-item-under-mouse-sync-with-next-measure": 54724,
29392642 "sws-br-preview-media-item-under-mouse-through-track": 54732,
2940 "sws-br-preview-media-item-under-mouse-through-track-and-pause-during-preview":
2941 54735,
2643 "sws-br-preview-media-item-under-mouse-through-track-and-pause-during-preview": 54735,
29422644 "sws-br-preview-media-item-under-mouse-through-track-and-pause-during-preview-start-from-mouse-cursor-position":
29432645 54736,
2944 "sws-br-preview-media-item-under-mouse-through-track-start-from-mouse-position":
2945 54733,
2946 "sws-br-preview-media-item-under-mouse-through-track-sync-with-next-measure":
2947 54734,
2646 "sws-br-preview-media-item-under-mouse-through-track-start-from-mouse-position": 54733,
2647 "sws-br-preview-media-item-under-mouse-through-track-sync-with-next-measure": 54734,
29482648 "sws-br-preview-take-under-mouse": 54752,
29492649 "sws-br-preview-take-under-mouse-and-pause-during-preview": 54755,
2950 "sws-br-preview-take-under-mouse-and-pause-during-preview-start-from-mouse-cursor-position":
2951 54756,
2650 "sws-br-preview-take-under-mouse-and-pause-during-preview-start-from-mouse-cursor-position": 54756,
29522651 "sws-br-preview-take-under-mouse-at-track-fader-volume": 54757,
2953 "sws-br-preview-take-under-mouse-at-track-fader-volume-and-pause-during-preview":
2954 54760,
2652 "sws-br-preview-take-under-mouse-at-track-fader-volume-and-pause-during-preview": 54760,
29552653 "sws-br-preview-take-under-mouse-at-track-fader-volume-and-pause-during-preview-start-from-mouse-cursor-position":
29562654 54761,
2957 "sws-br-preview-take-under-mouse-at-track-fader-volume-start-from-mouse-position":
2958 54758,
2959 "sws-br-preview-take-under-mouse-at-track-fader-volume-sync-with-next-measure":
2960 54759,
2655 "sws-br-preview-take-under-mouse-at-track-fader-volume-start-from-mouse-position": 54758,
2656 "sws-br-preview-take-under-mouse-at-track-fader-volume-sync-with-next-measure": 54759,
29612657 "sws-br-preview-take-under-mouse-start-from-mouse-cursor-position": 54753,
29622658 "sws-br-preview-take-under-mouse-sync-with-next-measure": 54754,
29632659 "sws-br-preview-take-under-mouse-through-track": 54762,
2964 "sws-br-preview-take-under-mouse-through-track-and-pause-during-preview":
2965 54765,
2966 "sws-br-preview-take-under-mouse-through-track-and-pause-during-preview-start-from-mouse-cursor-position":
2967 54766,
2968 "sws-br-preview-take-under-mouse-through-track-start-from-mouse-position":
2969 54763,
2660 "sws-br-preview-take-under-mouse-through-track-and-pause-during-preview": 54765,
2661 "sws-br-preview-take-under-mouse-through-track-and-pause-during-preview-start-from-mouse-cursor-position": 54766,
2662 "sws-br-preview-take-under-mouse-through-track-start-from-mouse-position": 54763,
29702663 "sws-br-preview-take-under-mouse-through-track-sync-with-next-measure": 54764,
29712664 "sws-br-project-track-selection-action-clear": 54786,
29722665 "sws-br-project-track-selection-action-set": 54784,
......@@ -3195,8 +2888,7 @@ export const REAPER_ACTIONS = {
31952888 "sws-br-select-envelope-point-at-mouse-cursor": 54096,
31962889 "sws-br-select-envelope-point-at-mouse-cursor-selected-envelope-only": 54095,
31972890 "sws-br-select-envelope-points-between-grid": 54037,
3198 "sws-br-select-envelope-points-between-grid-obey-time-selection-if-any":
3199 54038,
2891 "sws-br-select-envelope-points-between-grid-obey-time-selection-if-any": 54038,
32002892 "sws-br-select-envelope-points-on-grid": 54035,
32012893 "sws-br-select-envelope-points-on-grid-obey-time-selection-if-any": 54036,
32022894 "sws-br-select-mcp-track-under-mouse-cursor": 54506,
......@@ -3205,10 +2897,8 @@ export const REAPER_ACTIONS = {
32052897 "sws-br-select-peaks-in-envelope-add-to-selection": 54055,
32062898 "sws-br-select-previous-envelope-point": 54026,
32072899 "sws-br-select-tcp-track-under-mouse-cursor": 54505,
3208 "sws-br-set-closest-envelope-point-s-value-to-mouse-cursor-perform-until-shortcut-released":
3209 54852,
3210 "sws-br-set-closest-left-side-envelope-point-s-value-to-mouse-cursor-perform-until-shortcut-released":
3211 54853,
2900 "sws-br-set-closest-envelope-point-s-value-to-mouse-cursor-perform-until-shortcut-released": 54852,
2901 "sws-br-set-closest-left-side-envelope-point-s-value-to-mouse-cursor-perform-until-shortcut-released": 54853,
32122902 "sws-br-set-selected-envelope-points-to-first-selected-point-s-value": 54064,
32132903 "sws-br-set-selected-envelope-points-to-last-selected-point-s-value": 54063,
32142904 "sws-br-set-selected-envelope-points-to-next-point-s-value": 54061,
......@@ -3226,8 +2916,7 @@ export const REAPER_ACTIONS = {
32262916 "sws-br-show-all-fx-envelopes-for-selected-tracks": 54148,
32272917 "sws-br-show-all-send-envelopes-for-selected-tracks": 54162,
32282918 "sws-br-show-hide-pan-track-envelope-for-last-adjusted-send": 54144,
3229 "sws-br-show-hide-track-envelope-for-last-adjusted-send-volume-pan-only":
3230 54142,
2919 "sws-br-show-hide-track-envelope-for-last-adjusted-send-volume-pan-only": 54142,
32312920 "sws-br-show-hide-volume-track-envelope-for-last-adjusted-send": 54143,
32322921 "sws-br-show-mute-send-envelopes-for-selected-tracks": 54165,
32332922 "sws-br-show-pan-send-envelopes-for-selected-tracks": 54164,
......@@ -3236,71 +2925,50 @@ export const REAPER_ACTIONS = {
32362925 "sws-br-shrink-envelope-point-selection-from-the-left-end-point-only": 54034,
32372926 "sws-br-shrink-envelope-point-selection-from-the-right": 54031,
32382927 "sws-br-shrink-envelope-point-selection-from-the-right-end-point-only": 54033,
3239 "sws-br-snap-position-of-selected-partial-time-signature-markers-to-closest-grid-line":
3240 54844,
2928 "sws-br-snap-position-of-selected-partial-time-signature-markers-to-closest-grid-line": 54844,
32412929 "sws-br-split-selected-items-at-stretch-markers": 54463,
32422930 "sws-br-split-selected-items-at-tempo-markers": 54462,
32432931 "sws-br-tempo-help": 54845,
32442932 "sws-br-toggle-media-item-online-offline": 54502,
3245 "sws-br-toggle-play-from-edit-cursor-position-and-solo-item-and-track-under-mouse-for-the-duration":
3246 54458,
3247 "sws-br-toggle-play-from-edit-cursor-position-and-solo-track-under-mouse-for-the-duration":
3248 54457,
2933 "sws-br-toggle-play-from-edit-cursor-position-and-solo-item-and-track-under-mouse-for-the-duration": 54458,
2934 "sws-br-toggle-play-from-edit-cursor-position-and-solo-track-under-mouse-for-the-duration": 54457,
32492935 "sws-br-toggle-play-from-mouse-cursor-position": 54454,
3250 "sws-br-toggle-play-from-mouse-cursor-position-and-solo-item-and-track-under-mouse-for-the-duration":
3251 54456,
3252 "sws-br-toggle-play-from-mouse-cursor-position-and-solo-track-under-mouse-for-the-duration":
3253 54455,
2936 "sws-br-toggle-play-from-mouse-cursor-position-and-solo-item-and-track-under-mouse-for-the-duration": 54456,
2937 "sws-br-toggle-play-from-mouse-cursor-position-and-solo-track-under-mouse-for-the-duration": 54455,
32542938 "sws-br-toggle-preview-media-item-under-mouse": 54737,
3255 "sws-br-toggle-preview-media-item-under-mouse-and-pause-during-preview":
3256 54740,
3257 "sws-br-toggle-preview-media-item-under-mouse-and-pause-during-preview-start-from-mouse-cursor-position":
3258 54741,
2939 "sws-br-toggle-preview-media-item-under-mouse-and-pause-during-preview": 54740,
2940 "sws-br-toggle-preview-media-item-under-mouse-and-pause-during-preview-start-from-mouse-cursor-position": 54741,
32592941 "sws-br-toggle-preview-media-item-under-mouse-at-track-fader-volume": 54742,
3260 "sws-br-toggle-preview-media-item-under-mouse-at-track-fader-volume-and-pause-during-preview":
3261 54745,
2942 "sws-br-toggle-preview-media-item-under-mouse-at-track-fader-volume-and-pause-during-preview": 54745,
32622943 "sws-br-toggle-preview-media-item-under-mouse-at-track-fader-volume-and-pause-during-preview-start-from-mouse-cursor-position":
32632944 54746,
3264 "sws-br-toggle-preview-media-item-under-mouse-at-track-fader-volume-start-from-mouse-position":
3265 54743,
3266 "sws-br-toggle-preview-media-item-under-mouse-at-track-fader-volume-sync-with-next-measure":
3267 54744,
3268 "sws-br-toggle-preview-media-item-under-mouse-start-from-mouse-position":
3269 54738,
2945 "sws-br-toggle-preview-media-item-under-mouse-at-track-fader-volume-start-from-mouse-position": 54743,
2946 "sws-br-toggle-preview-media-item-under-mouse-at-track-fader-volume-sync-with-next-measure": 54744,
2947 "sws-br-toggle-preview-media-item-under-mouse-start-from-mouse-position": 54738,
32702948 "sws-br-toggle-preview-media-item-under-mouse-sync-with-next-measure": 54739,
32712949 "sws-br-toggle-preview-media-item-under-mouse-through-track": 54747,
3272 "sws-br-toggle-preview-media-item-under-mouse-through-track-and-pause-during-preview":
3273 54750,
2950 "sws-br-toggle-preview-media-item-under-mouse-through-track-and-pause-during-preview": 54750,
32742951 "sws-br-toggle-preview-media-item-under-mouse-through-track-and-pause-during-preview-start-from-mouse-cursor-position":
32752952 54751,
3276 "sws-br-toggle-preview-media-item-under-mouse-through-track-start-from-mouse-position":
3277 54748,
3278 "sws-br-toggle-preview-media-item-under-mouse-through-track-sync-with-next-measure":
3279 54749,
2953 "sws-br-toggle-preview-media-item-under-mouse-through-track-start-from-mouse-position": 54748,
2954 "sws-br-toggle-preview-media-item-under-mouse-through-track-sync-with-next-measure": 54749,
32802955 "sws-br-toggle-preview-take-under-mouse": 54767,
32812956 "sws-br-toggle-preview-take-under-mouse-and-pause-during-preview": 54770,
3282 "sws-br-toggle-preview-take-under-mouse-and-pause-during-preview-start-from-mouse-cursor-position":
3283 54771,
2957 "sws-br-toggle-preview-take-under-mouse-and-pause-during-preview-start-from-mouse-cursor-position": 54771,
32842958 "sws-br-toggle-preview-take-under-mouse-at-track-fader-volume": 54772,
3285 "sws-br-toggle-preview-take-under-mouse-at-track-fader-volume-and-pause-during-preview":
3286 54775,
2959 "sws-br-toggle-preview-take-under-mouse-at-track-fader-volume-and-pause-during-preview": 54775,
32872960 "sws-br-toggle-preview-take-under-mouse-at-track-fader-volume-and-pause-during-preview-start-from-mouse-cursor-position":
32882961 54776,
3289 "sws-br-toggle-preview-take-under-mouse-at-track-fader-volume-start-from-mouse-position":
3290 54773,
3291 "sws-br-toggle-preview-take-under-mouse-at-track-fader-volume-sync-with-next-measure":
3292 54774,
2962 "sws-br-toggle-preview-take-under-mouse-at-track-fader-volume-start-from-mouse-position": 54773,
2963 "sws-br-toggle-preview-take-under-mouse-at-track-fader-volume-sync-with-next-measure": 54774,
32932964 "sws-br-toggle-preview-take-under-mouse-start-from-mouse-position": 54768,
32942965 "sws-br-toggle-preview-take-under-mouse-sync-with-next-measure": 54769,
32952966 "sws-br-toggle-preview-take-under-mouse-through-track": 54777,
3296 "sws-br-toggle-preview-take-under-mouse-through-track-and-pause-during-preview":
3297 54780,
2967 "sws-br-toggle-preview-take-under-mouse-through-track-and-pause-during-preview": 54780,
32982968 "sws-br-toggle-preview-take-under-mouse-through-track-and-pause-during-preview-start-from-mouse-cursor-position":
32992969 54781,
3300 "sws-br-toggle-preview-take-under-mouse-through-track-start-from-mouse-position":
3301 54778,
3302 "sws-br-toggle-preview-take-under-mouse-through-track-sync-with-next-measure":
3303 54779,
2970 "sws-br-toggle-preview-take-under-mouse-through-track-start-from-mouse-position": 54778,
2971 "sws-br-toggle-preview-take-under-mouse-through-track-sync-with-next-measure": 54779,
33042972 "sws-br-toggle-show-active-mute-send-envelopes-for-selected-tracks": 54153,
33052973 "sws-br-toggle-show-active-pan-send-envelopes-for-selected-tracks": 54152,
33062974 "sws-br-toggle-show-active-volume-send-envelopes-for-selected-tracks": 54151,
......@@ -3324,18 +2992,15 @@ export const REAPER_ACTIONS = {
33242992 "sws-copy-current-snapshot": 53168,
33252993 "sws-copy-items-tracks-env-obey-time-selection-razor-edit-areas": 53576,
33262994 "sws-copy-marker-set-to-clipboard": 53086,
3327 "sws-copy-markers-in-time-selection-to-clipboard-relative-to-selection-start":
3328 53087,
2995 "sws-copy-markers-in-time-selection-to-clipboard-relative-to-selection-start": 53087,
33292996 "sws-copy-new-snapshot-all-track-s": 53170,
33302997 "sws-copy-new-snapshot-selected-track-s": 53169,
33312998 "sws-create-and-select-first-track": 53742,
33322999 "sws-create-regions-from-selected-items-name-by-active-take": 53110,
33333000 "sws-crossfade-adjacent-selected-items-move-edges-of-adjacent-items": 53633,
33343001 "sws-cut-items-tracks-env-obey-time-selection-razor-edit-areas": 53577,
3335 "sws-decrease-item-rate-by-0-6-percent-10-cents-preserving-length-clear-preserve-pitch":
3336 53635,
3337 "sws-decrease-item-rate-by-6-percent-one-semitone-preserving-length-clear-preserve-pitch":
3338 53637,
3002 "sws-decrease-item-rate-by-0-6-percent-10-cents-preserving-length-clear-preserve-pitch": 53635,
3003 "sws-decrease-item-rate-by-6-percent-one-semitone-preserving-length-clear-preserve-pitch": 53637,
33393004 "sws-delete-all-items-on-selected-track-s": 53609,
33403005 "sws-delete-all-markers": 53093,
33413006 "sws-delete-all-regions": 53094,
......@@ -3359,22 +3024,17 @@ export const REAPER_ACTIONS = {
33593024 "sws-exported-marker-list-format": 53097,
33603025 "sws-fng-apply-groove-to-selected-media-items-within-16th": 53893,
33613026 "sws-fng-apply-groove-to-selected-media-items-within-32nd": 53894,
3362 "sws-fng-apply-groove-to-selected-midi-notes-in-active-midi-editor-within-16th":
3363 53895,
3364 "sws-fng-apply-groove-to-selected-midi-notes-in-active-midi-editor-within-32nd":
3365 53896,
3027 "sws-fng-apply-groove-to-selected-midi-notes-in-active-midi-editor-within-16th": 53895,
3028 "sws-fng-apply-groove-to-selected-midi-notes-in-active-midi-editor-within-32nd": 53896,
33663029 "sws-fng-apply-midi-hardware-emulation-to-selected-midi-takes": 53886,
3367 "sws-fng-apply-selected-groove-use-curent-settings-from-opened-groove-tool":
3368 53911,
3030 "sws-fng-apply-selected-groove-use-curent-settings-from-opened-groove-tool": 53911,
33693031 "sws-fng-clean-selected-overlapping-media-items-on-same-track": 53863,
3370 "sws-fng-compress-amplitude-of-selected-envelope-points-around-midpoint":
3371 53853,
3032 "sws-fng-compress-amplitude-of-selected-envelope-points-around-midpoint": 53853,
33723033 "sws-fng-contract-selected-media-items": 53857,
33733034 "sws-fng-contract-selected-media-items-by-1-2": 53862,
33743035 "sws-fng-contract-selected-media-items-fine": 53859,
33753036 "sws-fng-cycle-through-cc-lanes-in-active-midi-editor": 53912,
3376 "sws-fng-cycle-through-cc-lanes-in-active-midi-editor-keep-lane-heights-constant":
3377 53913,
3037 "sws-fng-cycle-through-cc-lanes-in-active-midi-editor-keep-lane-heights-constant": 53913,
33783038 "sws-fng-decrease-selected-midi-items-velocity-by-1": 53877,
33793039 "sws-fng-decrease-selected-midi-items-velocity-by-10": 53879,
33803040 "sws-fng-expand-amplitude-of-selected-envelope-points-around-midpoint": 53852,
......@@ -3450,10 +3110,8 @@ export const REAPER_ACTIONS = {
34503110 "sws-horizontal-scroll-to-put-play-cursor-at-50-percent": 53783,
34513111 "sws-horizontal-zoom-to-selected-items": 53792,
34523112 "sws-ignore-next-marker-action": 53107,
3453 "sws-increase-item-rate-by-0-6-percent-10-cents-preserving-length-clear-preserve-pitch":
3454 53634,
3455 "sws-increase-item-rate-by-6-percent-one-semitone-preserving-length-clear-preserve-pitch":
3456 53636,
3113 "sws-increase-item-rate-by-0-6-percent-10-cents-preserving-length-clear-preserve-pitch": 53634,
3114 "sws-increase-item-rate-by-6-percent-one-semitone-preserving-length-clear-preserve-pitch": 53636,
34573115 "sws-indent-selected-track-s": 53603,
34583116 "sws-insert-file-matching-selected-track-s-name": 53613,
34593117 "sws-insert-track-above-selected-tracks": 53741,
......@@ -3492,17 +3150,12 @@ export const REAPER_ACTIONS = {
34923150 "sws-nf-disable-multichannel-metering-selected-tracks": 54928,
34933151 "sws-nf-enable-multichannel-metering-all-tracks": 54929,
34943152 "sws-nf-enable-multichannel-metering-selected-tracks": 54930,
3495 "sws-nf-eraser-tool-marquee-sel-items-and-time-cut-on-shortcut-release":
3496 54874,
3497 "sws-nf-eraser-tool-marquee-sel-items-and-time-ignoring-snap-cut-on-shortcut-release":
3498 54873,
3499 "sws-nf-play-stop-or-play-pause-obey-sws-nf-toggle-play-stop-or-play-pause-toggle-state":
3500 54937,
3501 "sws-nf-toggle-obey-track-height-lock-in-vertical-zoom-and-track-height-actions":
3502 54935,
3153 "sws-nf-eraser-tool-marquee-sel-items-and-time-cut-on-shortcut-release": 54874,
3154 "sws-nf-eraser-tool-marquee-sel-items-and-time-ignoring-snap-cut-on-shortcut-release": 54873,
3155 "sws-nf-play-stop-or-play-pause-obey-sws-nf-toggle-play-stop-or-play-pause-toggle-state": 54937,
3156 "sws-nf-toggle-obey-track-height-lock-in-vertical-zoom-and-track-height-actions": 54935,
35033157 "sws-nf-toggle-play-stop-off-or-play-pause-on": 54936,
3504 "sws-nf-toggle-render-speed-apply-fx-render-stems-realtime-not-limited":
3505 54934,
3158 "sws-nf-toggle-render-speed-apply-fx-render-stems-realtime-not-limited": 54934,
35063159 "sws-normalize-item-s-to-peak-rms": 53573,
35073160 "sws-normalize-items-to-overall-peak-rms": 53574,
35083161 "sws-normalize-items-to-rms-entire-item": 53572,
......@@ -3698,13 +3351,10 @@ export const REAPER_ACTIONS = {
36983351 "sws-s-and-m-cut-selected-tracks-sends": 54952,
36993352 "sws-s-and-m-cut-selected-tracks-with-routing": 54946,
37003353 "sws-s-and-m-decrease-metronome-volume": 55262,
3701 "sws-s-and-m-delete-active-take-and-source-file-in-selected-items-no-undo":
3702 55147,
3703 "sws-s-and-m-delete-active-take-and-source-file-in-selected-items-prompt-no-undo":
3704 55146,
3354 "sws-s-and-m-delete-active-take-and-source-file-in-selected-items-no-undo": 55147,
3355 "sws-s-and-m-delete-active-take-and-source-file-in-selected-items-prompt-no-undo": 55146,
37053356 "sws-s-and-m-delete-selected-items-takes-and-source-files-no-undo": 55145,
3706 "sws-s-and-m-delete-selected-items-takes-and-source-files-prompt-no-undo":
3707 55144,
3357 "sws-s-and-m-delete-selected-items-takes-and-source-files-prompt-no-undo": 55144,
37083358 "sws-s-and-m-disarm-all-active-envelopes-for-selected-tracks": 55210,
37093359 "sws-s-and-m-dummy-toggle-1": 55675,
37103360 "sws-s-and-m-dummy-toggle-2": 55676,
......@@ -3777,8 +3427,7 @@ export const REAPER_ACTIONS = {
37773427 "sws-s-and-m-insert-silence-seconds": 55039,
37783428 "sws-s-and-m-live-config-number-1-apply-config-midi-osc-only": 55563,
37793429 "sws-s-and-m-live-config-number-1-apply-next-config": 55579,
3780 "sws-s-and-m-live-config-number-1-apply-preloaded-config-swap-preload-current":
3781 55611,
3430 "sws-s-and-m-live-config-number-1-apply-preloaded-config-swap-preload-current": 55611,
37823431 "sws-s-and-m-live-config-number-1-apply-previous-config": 55587,
37833432 "sws-s-and-m-live-config-number-1-open-close-monitoring-window": 55555,
37843433 "sws-s-and-m-live-config-number-1-preload-config-midi-osc-only": 55571,
......@@ -3786,18 +3435,13 @@ export const REAPER_ACTIONS = {
37863435 "sws-s-and-m-live-config-number-1-preload-previous-config": 55603,
37873436 "sws-s-and-m-live-config-number-1-toggle-enable": 55619,
37883437 "sws-s-and-m-live-config-number-1-toggle-enable-tiny-fades": 55659,
3789 "sws-s-and-m-live-config-number-1-toggle-option-disarm-all-but-active-track":
3790 55643,
3791 "sws-s-and-m-live-config-number-1-toggle-option-mute-all-but-active-track":
3792 55627,
3793 "sws-s-and-m-live-config-number-1-toggle-option-offline-all-but-active-preloaded-tracks":
3794 55635,
3795 "sws-s-and-m-live-config-number-1-toggle-option-send-all-notes-off-when-switching-configs":
3796 55651,
3438 "sws-s-and-m-live-config-number-1-toggle-option-disarm-all-but-active-track": 55643,
3439 "sws-s-and-m-live-config-number-1-toggle-option-mute-all-but-active-track": 55627,
3440 "sws-s-and-m-live-config-number-1-toggle-option-offline-all-but-active-preloaded-tracks": 55635,
3441 "sws-s-and-m-live-config-number-1-toggle-option-send-all-notes-off-when-switching-configs": 55651,
37973442 "sws-s-and-m-live-config-number-2-apply-config-midi-osc-only": 55564,
37983443 "sws-s-and-m-live-config-number-2-apply-next-config": 55580,
3799 "sws-s-and-m-live-config-number-2-apply-preloaded-config-swap-preload-current":
3800 55612,
3444 "sws-s-and-m-live-config-number-2-apply-preloaded-config-swap-preload-current": 55612,
38013445 "sws-s-and-m-live-config-number-2-apply-previous-config": 55588,
38023446 "sws-s-and-m-live-config-number-2-open-close-monitoring-window": 55556,
38033447 "sws-s-and-m-live-config-number-2-preload-config-midi-osc-only": 55572,
......@@ -3805,18 +3449,13 @@ export const REAPER_ACTIONS = {
38053449 "sws-s-and-m-live-config-number-2-preload-previous-config": 55604,
38063450 "sws-s-and-m-live-config-number-2-toggle-enable": 55620,
38073451 "sws-s-and-m-live-config-number-2-toggle-enable-tiny-fades": 55660,
3808 "sws-s-and-m-live-config-number-2-toggle-option-disarm-all-but-active-track":
3809 55644,
3810 "sws-s-and-m-live-config-number-2-toggle-option-mute-all-but-active-track":
3811 55628,
3812 "sws-s-and-m-live-config-number-2-toggle-option-offline-all-but-active-preloaded-tracks":
3813 55636,
3814 "sws-s-and-m-live-config-number-2-toggle-option-send-all-notes-off-when-switching-configs":
3815 55652,
3452 "sws-s-and-m-live-config-number-2-toggle-option-disarm-all-but-active-track": 55644,
3453 "sws-s-and-m-live-config-number-2-toggle-option-mute-all-but-active-track": 55628,
3454 "sws-s-and-m-live-config-number-2-toggle-option-offline-all-but-active-preloaded-tracks": 55636,
3455 "sws-s-and-m-live-config-number-2-toggle-option-send-all-notes-off-when-switching-configs": 55652,
38163456 "sws-s-and-m-live-config-number-3-apply-config-midi-osc-only": 55565,
38173457 "sws-s-and-m-live-config-number-3-apply-next-config": 55581,
3818 "sws-s-and-m-live-config-number-3-apply-preloaded-config-swap-preload-current":
3819 55613,
3458 "sws-s-and-m-live-config-number-3-apply-preloaded-config-swap-preload-current": 55613,
38203459 "sws-s-and-m-live-config-number-3-apply-previous-config": 55589,
38213460 "sws-s-and-m-live-config-number-3-open-close-monitoring-window": 55557,
38223461 "sws-s-and-m-live-config-number-3-preload-config-midi-osc-only": 55573,
......@@ -3824,18 +3463,13 @@ export const REAPER_ACTIONS = {
38243463 "sws-s-and-m-live-config-number-3-preload-previous-config": 55605,
38253464 "sws-s-and-m-live-config-number-3-toggle-enable": 55621,
38263465 "sws-s-and-m-live-config-number-3-toggle-enable-tiny-fades": 55661,
3827 "sws-s-and-m-live-config-number-3-toggle-option-disarm-all-but-active-track":
3828 55645,
3829 "sws-s-and-m-live-config-number-3-toggle-option-mute-all-but-active-track":
3830 55629,
3831 "sws-s-and-m-live-config-number-3-toggle-option-offline-all-but-active-preloaded-tracks":
3832 55637,
3833 "sws-s-and-m-live-config-number-3-toggle-option-send-all-notes-off-when-switching-configs":
3834 55653,
3466 "sws-s-and-m-live-config-number-3-toggle-option-disarm-all-but-active-track": 55645,
3467 "sws-s-and-m-live-config-number-3-toggle-option-mute-all-but-active-track": 55629,
3468 "sws-s-and-m-live-config-number-3-toggle-option-offline-all-but-active-preloaded-tracks": 55637,
3469 "sws-s-and-m-live-config-number-3-toggle-option-send-all-notes-off-when-switching-configs": 55653,
38353470 "sws-s-and-m-live-config-number-4-apply-config-midi-osc-only": 55566,
38363471 "sws-s-and-m-live-config-number-4-apply-next-config": 55582,
3837 "sws-s-and-m-live-config-number-4-apply-preloaded-config-swap-preload-current":
3838 55614,
3472 "sws-s-and-m-live-config-number-4-apply-preloaded-config-swap-preload-current": 55614,
38393473 "sws-s-and-m-live-config-number-4-apply-previous-config": 55590,
38403474 "sws-s-and-m-live-config-number-4-open-close-monitoring-window": 55558,
38413475 "sws-s-and-m-live-config-number-4-preload-config-midi-osc-only": 55574,
......@@ -3843,18 +3477,13 @@ export const REAPER_ACTIONS = {
38433477 "sws-s-and-m-live-config-number-4-preload-previous-config": 55606,
38443478 "sws-s-and-m-live-config-number-4-toggle-enable": 55622,
38453479 "sws-s-and-m-live-config-number-4-toggle-enable-tiny-fades": 55662,
3846 "sws-s-and-m-live-config-number-4-toggle-option-disarm-all-but-active-track":
3847 55646,
3848 "sws-s-and-m-live-config-number-4-toggle-option-mute-all-but-active-track":
3849 55630,
3850 "sws-s-and-m-live-config-number-4-toggle-option-offline-all-but-active-preloaded-tracks":
3851 55638,
3852 "sws-s-and-m-live-config-number-4-toggle-option-send-all-notes-off-when-switching-configs":
3853 55654,
3480 "sws-s-and-m-live-config-number-4-toggle-option-disarm-all-but-active-track": 55646,
3481 "sws-s-and-m-live-config-number-4-toggle-option-mute-all-but-active-track": 55630,
3482 "sws-s-and-m-live-config-number-4-toggle-option-offline-all-but-active-preloaded-tracks": 55638,
3483 "sws-s-and-m-live-config-number-4-toggle-option-send-all-notes-off-when-switching-configs": 55654,
38543484 "sws-s-and-m-live-config-number-5-apply-config-midi-osc-only": 55567,
38553485 "sws-s-and-m-live-config-number-5-apply-next-config": 55583,
3856 "sws-s-and-m-live-config-number-5-apply-preloaded-config-swap-preload-current":
3857 55615,
3486 "sws-s-and-m-live-config-number-5-apply-preloaded-config-swap-preload-current": 55615,
38583487 "sws-s-and-m-live-config-number-5-apply-previous-config": 55591,
38593488 "sws-s-and-m-live-config-number-5-open-close-monitoring-window": 55559,
38603489 "sws-s-and-m-live-config-number-5-preload-config-midi-osc-only": 55575,
......@@ -3862,18 +3491,13 @@ export const REAPER_ACTIONS = {
38623491 "sws-s-and-m-live-config-number-5-preload-previous-config": 55607,
38633492 "sws-s-and-m-live-config-number-5-toggle-enable": 55623,
38643493 "sws-s-and-m-live-config-number-5-toggle-enable-tiny-fades": 55663,
3865 "sws-s-and-m-live-config-number-5-toggle-option-disarm-all-but-active-track":
3866 55647,
3867 "sws-s-and-m-live-config-number-5-toggle-option-mute-all-but-active-track":
3868 55631,
3869 "sws-s-and-m-live-config-number-5-toggle-option-offline-all-but-active-preloaded-tracks":
3870 55639,
3871 "sws-s-and-m-live-config-number-5-toggle-option-send-all-notes-off-when-switching-configs":
3872 55655,
3494 "sws-s-and-m-live-config-number-5-toggle-option-disarm-all-but-active-track": 55647,
3495 "sws-s-and-m-live-config-number-5-toggle-option-mute-all-but-active-track": 55631,
3496 "sws-s-and-m-live-config-number-5-toggle-option-offline-all-but-active-preloaded-tracks": 55639,
3497 "sws-s-and-m-live-config-number-5-toggle-option-send-all-notes-off-when-switching-configs": 55655,
38733498 "sws-s-and-m-live-config-number-6-apply-config-midi-osc-only": 55568,
38743499 "sws-s-and-m-live-config-number-6-apply-next-config": 55584,
3875 "sws-s-and-m-live-config-number-6-apply-preloaded-config-swap-preload-current":
3876 55616,
3500 "sws-s-and-m-live-config-number-6-apply-preloaded-config-swap-preload-current": 55616,
38773501 "sws-s-and-m-live-config-number-6-apply-previous-config": 55592,
38783502 "sws-s-and-m-live-config-number-6-open-close-monitoring-window": 55560,
38793503 "sws-s-and-m-live-config-number-6-preload-config-midi-osc-only": 55576,
......@@ -3881,18 +3505,13 @@ export const REAPER_ACTIONS = {
38813505 "sws-s-and-m-live-config-number-6-preload-previous-config": 55608,
38823506 "sws-s-and-m-live-config-number-6-toggle-enable": 55624,
38833507 "sws-s-and-m-live-config-number-6-toggle-enable-tiny-fades": 55664,
3884 "sws-s-and-m-live-config-number-6-toggle-option-disarm-all-but-active-track":
3885 55648,
3886 "sws-s-and-m-live-config-number-6-toggle-option-mute-all-but-active-track":
3887 55632,
3888 "sws-s-and-m-live-config-number-6-toggle-option-offline-all-but-active-preloaded-tracks":
3889 55640,
3890 "sws-s-and-m-live-config-number-6-toggle-option-send-all-notes-off-when-switching-configs":
3891 55656,
3508 "sws-s-and-m-live-config-number-6-toggle-option-disarm-all-but-active-track": 55648,
3509 "sws-s-and-m-live-config-number-6-toggle-option-mute-all-but-active-track": 55632,
3510 "sws-s-and-m-live-config-number-6-toggle-option-offline-all-but-active-preloaded-tracks": 55640,
3511 "sws-s-and-m-live-config-number-6-toggle-option-send-all-notes-off-when-switching-configs": 55656,
38923512 "sws-s-and-m-live-config-number-7-apply-config-midi-osc-only": 55569,
38933513 "sws-s-and-m-live-config-number-7-apply-next-config": 55585,
3894 "sws-s-and-m-live-config-number-7-apply-preloaded-config-swap-preload-current":
3895 55617,
3514 "sws-s-and-m-live-config-number-7-apply-preloaded-config-swap-preload-current": 55617,
38963515 "sws-s-and-m-live-config-number-7-apply-previous-config": 55593,
38973516 "sws-s-and-m-live-config-number-7-open-close-monitoring-window": 55561,
38983517 "sws-s-and-m-live-config-number-7-preload-config-midi-osc-only": 55577,
......@@ -3900,18 +3519,13 @@ export const REAPER_ACTIONS = {
39003519 "sws-s-and-m-live-config-number-7-preload-previous-config": 55609,
39013520 "sws-s-and-m-live-config-number-7-toggle-enable": 55625,
39023521 "sws-s-and-m-live-config-number-7-toggle-enable-tiny-fades": 55665,
3903 "sws-s-and-m-live-config-number-7-toggle-option-disarm-all-but-active-track":
3904 55649,
3905 "sws-s-and-m-live-config-number-7-toggle-option-mute-all-but-active-track":
3906 55633,
3907 "sws-s-and-m-live-config-number-7-toggle-option-offline-all-but-active-preloaded-tracks":
3908 55641,
3909 "sws-s-and-m-live-config-number-7-toggle-option-send-all-notes-off-when-switching-configs":
3910 55657,
3522 "sws-s-and-m-live-config-number-7-toggle-option-disarm-all-but-active-track": 55649,
3523 "sws-s-and-m-live-config-number-7-toggle-option-mute-all-but-active-track": 55633,
3524 "sws-s-and-m-live-config-number-7-toggle-option-offline-all-but-active-preloaded-tracks": 55641,
3525 "sws-s-and-m-live-config-number-7-toggle-option-send-all-notes-off-when-switching-configs": 55657,
39113526 "sws-s-and-m-live-config-number-8-apply-config-midi-osc-only": 55570,
39123527 "sws-s-and-m-live-config-number-8-apply-next-config": 55586,
3913 "sws-s-and-m-live-config-number-8-apply-preloaded-config-swap-preload-current":
3914 55618,
3528 "sws-s-and-m-live-config-number-8-apply-preloaded-config-swap-preload-current": 55618,
39153529 "sws-s-and-m-live-config-number-8-apply-previous-config": 55594,
39163530 "sws-s-and-m-live-config-number-8-open-close-monitoring-window": 55562,
39173531 "sws-s-and-m-live-config-number-8-preload-config-midi-osc-only": 55578,
......@@ -3919,14 +3533,10 @@ export const REAPER_ACTIONS = {
39193533 "sws-s-and-m-live-config-number-8-preload-previous-config": 55610,
39203534 "sws-s-and-m-live-config-number-8-toggle-enable": 55626,
39213535 "sws-s-and-m-live-config-number-8-toggle-enable-tiny-fades": 55666,
3922 "sws-s-and-m-live-config-number-8-toggle-option-disarm-all-but-active-track":
3923 55650,
3924 "sws-s-and-m-live-config-number-8-toggle-option-mute-all-but-active-track":
3925 55634,
3926 "sws-s-and-m-live-config-number-8-toggle-option-offline-all-but-active-preloaded-tracks":
3927 55642,
3928 "sws-s-and-m-live-config-number-8-toggle-option-send-all-notes-off-when-switching-configs":
3929 55658,
3536 "sws-s-and-m-live-config-number-8-toggle-option-disarm-all-but-active-track": 55650,
3537 "sws-s-and-m-live-config-number-8-toggle-option-mute-all-but-active-track": 55634,
3538 "sws-s-and-m-live-config-number-8-toggle-option-offline-all-but-active-preloaded-tracks": 55642,
3539 "sws-s-and-m-live-config-number-8-toggle-option-send-all-notes-off-when-switching-configs": 55658,
39303540 "sws-s-and-m-map-selected-tracks-midi-input-to-channel-1": 55715,
39313541 "sws-s-and-m-map-selected-tracks-midi-input-to-channel-10": 55724,
39323542 "sws-s-and-m-map-selected-tracks-midi-input-to-channel-11": 55725,
......@@ -4005,12 +3615,9 @@ export const REAPER_ACTIONS = {
40053615 "sws-s-and-m-paste-takes": 55135,
40063616 "sws-s-and-m-paste-takes-after-active-takes": 55136,
40073617 "sws-s-and-m-paste-tracks-with-routing-or-items": 54945,
4008 "sws-s-and-m-reassign-midi-learned-channels-of-all-fx-for-selected-tracks-prompt":
4009 55119,
4010 "sws-s-and-m-reassign-midi-learned-channels-of-all-fx-to-input-channel-for-selected-tracks":
4011 55121,
4012 "sws-s-and-m-reassign-midi-learned-channels-of-selected-fx-for-selected-tracks-prompt":
4013 55120,
3618 "sws-s-and-m-reassign-midi-learned-channels-of-all-fx-for-selected-tracks-prompt": 55119,
3619 "sws-s-and-m-reassign-midi-learned-channels-of-all-fx-to-input-channel-for-selected-tracks": 55121,
3620 "sws-s-and-m-reassign-midi-learned-channels-of-selected-fx-for-selected-tracks-prompt": 55120,
40143621 "sws-s-and-m-recall-default-track-send-preferences": 54957,
40153622 "sws-s-and-m-region-playlist-add-all-regions-to-current-playlist": 55256,
40163623 "sws-s-and-m-region-playlist-append-playlist-to-project": 55245,
......@@ -4020,25 +3627,17 @@ export const REAPER_ACTIONS = {
40203627 "sws-s-and-m-region-playlist-number-2-play": 55668,
40213628 "sws-s-and-m-region-playlist-number-3-play": 55669,
40223629 "sws-s-and-m-region-playlist-number-4-play": 55670,
4023 "sws-s-and-m-region-playlist-options-disable-shuffle-only-in-region-playlist":
4024 55254,
4025 "sws-s-and-m-region-playlist-options-disable-smooth-seek-only-in-region-playlist":
4026 55251,
4027 "sws-s-and-m-region-playlist-options-enable-shuffle-only-in-region-playlist":
4028 55253,
4029 "sws-s-and-m-region-playlist-options-enable-smooth-seek-only-in-region-playlist":
4030 55250,
4031 "sws-s-and-m-region-playlist-options-toggle-shuffle-only-in-region-playlist":
4032 55255,
4033 "sws-s-and-m-region-playlist-options-toggle-smooth-seek-only-in-region-playlist":
4034 55252,
3630 "sws-s-and-m-region-playlist-options-disable-shuffle-only-in-region-playlist": 55254,
3631 "sws-s-and-m-region-playlist-options-disable-smooth-seek-only-in-region-playlist": 55251,
3632 "sws-s-and-m-region-playlist-options-enable-shuffle-only-in-region-playlist": 55253,
3633 "sws-s-and-m-region-playlist-options-enable-smooth-seek-only-in-region-playlist": 55250,
3634 "sws-s-and-m-region-playlist-options-toggle-shuffle-only-in-region-playlist": 55255,
3635 "sws-s-and-m-region-playlist-options-toggle-smooth-seek-only-in-region-playlist": 55252,
40353636 "sws-s-and-m-region-playlist-paste-playlist-at-edit-cursor": 55246,
40363637 "sws-s-and-m-region-playlist-play": 55238,
4037 "sws-s-and-m-region-playlist-play-next-region-based-on-current-playing-region":
4038 55242,
3638 "sws-s-and-m-region-playlist-play-next-region-based-on-current-playing-region": 55242,
40393639 "sws-s-and-m-region-playlist-play-next-region-smooth-seek": 55240,
4040 "sws-s-and-m-region-playlist-play-previous-region-based-on-current-playing-region":
4041 55241,
3640 "sws-s-and-m-region-playlist-play-previous-region-based-on-current-playing-region": 55241,
40423641 "sws-s-and-m-region-playlist-play-previous-region-smooth-seek": 55239,
40433642 "sws-s-and-m-region-playlist-set-repeat-off": 55247,
40443643 "sws-s-and-m-region-playlist-set-repeat-on": 55248,
......@@ -4060,32 +3659,19 @@ export const REAPER_ACTIONS = {
40603659 "sws-s-and-m-resources-add-media-file-to-new-track-slot-2": 55440,
40613660 "sws-s-and-m-resources-add-media-file-to-new-track-slot-3": 55441,
40623661 "sws-s-and-m-resources-add-media-file-to-new-track-slot-4": 55442,
4063 "sws-s-and-m-resources-add-media-file-to-selected-items-as-takes-last-slot":
4064 55098,
4065 "sws-s-and-m-resources-add-media-file-to-selected-items-as-takes-slot-1":
4066 55443,
4067 "sws-s-and-m-resources-add-media-file-to-selected-items-as-takes-slot-2":
4068 55444,
4069 "sws-s-and-m-resources-add-media-file-to-selected-items-as-takes-slot-3":
4070 55445,
4071 "sws-s-and-m-resources-add-media-file-to-selected-items-as-takes-slot-4":
4072 55446,
4073 "sws-s-and-m-resources-apply-track-template-plus-envelopes-items-to-selected-tracks-last-slot":
4074 55071,
4075 "sws-s-and-m-resources-apply-track-template-plus-envelopes-items-to-selected-tracks-prompt-for-slot":
4076 55286,
4077 "sws-s-and-m-resources-apply-track-template-plus-envelopes-items-to-selected-tracks-slot-1":
4078 55391,
4079 "sws-s-and-m-resources-apply-track-template-plus-envelopes-items-to-selected-tracks-slot-2":
4080 55392,
4081 "sws-s-and-m-resources-apply-track-template-plus-envelopes-items-to-selected-tracks-slot-3":
4082 55393,
4083 "sws-s-and-m-resources-apply-track-template-plus-envelopes-items-to-selected-tracks-slot-4":
4084 55394,
4085 "sws-s-and-m-resources-apply-track-template-to-selected-tracks-last-slot":
4086 55070,
4087 "sws-s-and-m-resources-apply-track-template-to-selected-tracks-prompt-for-slot":
4088 55285,
3662 "sws-s-and-m-resources-add-media-file-to-selected-items-as-takes-last-slot": 55098,
3663 "sws-s-and-m-resources-add-media-file-to-selected-items-as-takes-slot-1": 55443,
3664 "sws-s-and-m-resources-add-media-file-to-selected-items-as-takes-slot-2": 55444,
3665 "sws-s-and-m-resources-add-media-file-to-selected-items-as-takes-slot-3": 55445,
3666 "sws-s-and-m-resources-add-media-file-to-selected-items-as-takes-slot-4": 55446,
3667 "sws-s-and-m-resources-apply-track-template-plus-envelopes-items-to-selected-tracks-last-slot": 55071,
3668 "sws-s-and-m-resources-apply-track-template-plus-envelopes-items-to-selected-tracks-prompt-for-slot": 55286,
3669 "sws-s-and-m-resources-apply-track-template-plus-envelopes-items-to-selected-tracks-slot-1": 55391,
3670 "sws-s-and-m-resources-apply-track-template-plus-envelopes-items-to-selected-tracks-slot-2": 55392,
3671 "sws-s-and-m-resources-apply-track-template-plus-envelopes-items-to-selected-tracks-slot-3": 55393,
3672 "sws-s-and-m-resources-apply-track-template-plus-envelopes-items-to-selected-tracks-slot-4": 55394,
3673 "sws-s-and-m-resources-apply-track-template-to-selected-tracks-last-slot": 55070,
3674 "sws-s-and-m-resources-apply-track-template-to-selected-tracks-prompt-for-slot": 55285,
40893675 "sws-s-and-m-resources-apply-track-template-to-selected-tracks-slot-1": 55387,
40903676 "sws-s-and-m-resources-apply-track-template-to-selected-tracks-slot-2": 55388,
40913677 "sws-s-and-m-resources-apply-track-template-to-selected-tracks-slot-3": 55389,
......@@ -4118,8 +3704,7 @@ export const REAPER_ACTIONS = {
41183704 "sws-s-and-m-resources-delete-last-theme-slot-file": 55112,
41193705 "sws-s-and-m-resources-delete-last-track-template-slot-file": 55063,
41203706 "sws-s-and-m-resources-import-tracks-from-track-template-last-slot": 55069,
4121 "sws-s-and-m-resources-import-tracks-from-track-template-prompt-for-slot":
4122 55284,
3707 "sws-s-and-m-resources-import-tracks-from-track-template-prompt-for-slot": 55284,
41233708 "sws-s-and-m-resources-import-tracks-from-track-template-slot-1": 55395,
41243709 "sws-s-and-m-resources-import-tracks-from-track-template-slot-2": 55396,
41253710 "sws-s-and-m-resources-import-tracks-from-track-template-slot-3": 55397,
......@@ -4134,26 +3719,16 @@ export const REAPER_ACTIONS = {
41343719 "sws-s-and-m-resources-loop-media-file-in-selected-tracks-slot-2": 55412,
41353720 "sws-s-and-m-resources-loop-media-file-in-selected-tracks-slot-3": 55413,
41363721 "sws-s-and-m-resources-loop-media-file-in-selected-tracks-slot-4": 55414,
4137 "sws-s-and-m-resources-loop-media-file-in-selected-tracks-sync-with-next-measure-slot-1":
4138 55419,
4139 "sws-s-and-m-resources-loop-media-file-in-selected-tracks-sync-with-next-measure-slot-2":
4140 55420,
4141 "sws-s-and-m-resources-loop-media-file-in-selected-tracks-sync-with-next-measure-slot-3":
4142 55421,
4143 "sws-s-and-m-resources-loop-media-file-in-selected-tracks-sync-with-next-measure-slot-4":
4144 55422,
4145 "sws-s-and-m-resources-loop-media-file-in-selected-tracks-toggle-last-slot":
4146 55093,
4147 "sws-s-and-m-resources-loop-media-file-in-selected-tracks-toggle-pause-last-slot":
4148 55095,
4149 "sws-s-and-m-resources-loop-media-file-in-selected-tracks-toggle-slot-1":
4150 55427,
4151 "sws-s-and-m-resources-loop-media-file-in-selected-tracks-toggle-slot-2":
4152 55428,
4153 "sws-s-and-m-resources-loop-media-file-in-selected-tracks-toggle-slot-3":
4154 55429,
4155 "sws-s-and-m-resources-loop-media-file-in-selected-tracks-toggle-slot-4":
4156 55430,
3722 "sws-s-and-m-resources-loop-media-file-in-selected-tracks-sync-with-next-measure-slot-1": 55419,
3723 "sws-s-and-m-resources-loop-media-file-in-selected-tracks-sync-with-next-measure-slot-2": 55420,
3724 "sws-s-and-m-resources-loop-media-file-in-selected-tracks-sync-with-next-measure-slot-3": 55421,
3725 "sws-s-and-m-resources-loop-media-file-in-selected-tracks-sync-with-next-measure-slot-4": 55422,
3726 "sws-s-and-m-resources-loop-media-file-in-selected-tracks-toggle-last-slot": 55093,
3727 "sws-s-and-m-resources-loop-media-file-in-selected-tracks-toggle-pause-last-slot": 55095,
3728 "sws-s-and-m-resources-loop-media-file-in-selected-tracks-toggle-slot-1": 55427,
3729 "sws-s-and-m-resources-loop-media-file-in-selected-tracks-toggle-slot-2": 55428,
3730 "sws-s-and-m-resources-loop-media-file-in-selected-tracks-toggle-slot-3": 55429,
3731 "sws-s-and-m-resources-loop-media-file-in-selected-tracks-toggle-slot-4": 55430,
41573732 "sws-s-and-m-resources-open-project-last-slot": 55078,
41583733 "sws-s-and-m-resources-open-project-last-slot-new-tab": 55079,
41593734 "sws-s-and-m-resources-open-project-next-slot-cycle": 55080,
......@@ -4170,68 +3745,42 @@ export const REAPER_ACTIONS = {
41703745 "sws-s-and-m-resources-open-project-slot-3-new-tab": 55405,
41713746 "sws-s-and-m-resources-open-project-slot-4": 55402,
41723747 "sws-s-and-m-resources-open-project-slot-4-new-tab": 55406,
4173 "sws-s-and-m-resources-paste-fx-chain-to-selected-items-all-takes-last-slot":
4174 55059,
4175 "sws-s-and-m-resources-paste-fx-chain-to-selected-items-all-takes-prompt-for-slot":
4176 55281,
3748 "sws-s-and-m-resources-paste-fx-chain-to-selected-items-all-takes-last-slot": 55059,
3749 "sws-s-and-m-resources-paste-fx-chain-to-selected-items-all-takes-prompt-for-slot": 55281,
41773750 "sws-s-and-m-resources-paste-fx-chain-to-selected-items-last-slot": 55058,
4178 "sws-s-and-m-resources-paste-fx-chain-to-selected-items-prompt-for-slot":
4179 55280,
3751 "sws-s-and-m-resources-paste-fx-chain-to-selected-items-prompt-for-slot": 55280,
41803752 "sws-s-and-m-resources-paste-fx-chain-to-selected-items-slot-1": 55367,
41813753 "sws-s-and-m-resources-paste-fx-chain-to-selected-items-slot-2": 55368,
41823754 "sws-s-and-m-resources-paste-fx-chain-to-selected-items-slot-3": 55369,
41833755 "sws-s-and-m-resources-paste-fx-chain-to-selected-items-slot-4": 55370,
41843756 "sws-s-and-m-resources-paste-fx-chain-to-selected-tracks-last-slot": 55061,
4185 "sws-s-and-m-resources-paste-fx-chain-to-selected-tracks-prompt-for-slot":
4186 55283,
3757 "sws-s-and-m-resources-paste-fx-chain-to-selected-tracks-prompt-for-slot": 55283,
41873758 "sws-s-and-m-resources-paste-fx-chain-to-selected-tracks-slot-1": 55375,
41883759 "sws-s-and-m-resources-paste-fx-chain-to-selected-tracks-slot-2": 55376,
41893760 "sws-s-and-m-resources-paste-fx-chain-to-selected-tracks-slot-3": 55377,
41903761 "sws-s-and-m-resources-paste-fx-chain-to-selected-tracks-slot-4": 55378,
4191 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-items-all-takes-last-slot":
4192 55057,
4193 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-items-all-takes-prompt-for-slot":
4194 55279,
4195 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-items-last-slot":
4196 55056,
4197 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-items-prompt-for-slot":
4198 55278,
4199 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-items-slot-1":
4200 55363,
4201 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-items-slot-2":
4202 55364,
4203 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-items-slot-3":
4204 55365,
4205 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-items-slot-4":
4206 55366,
4207 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-tracks-last-slot":
4208 55060,
4209 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-tracks-prompt-for-slot":
4210 55282,
4211 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-tracks-slot-1":
4212 55371,
4213 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-tracks-slot-2":
4214 55372,
4215 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-tracks-slot-3":
4216 55373,
4217 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-tracks-slot-4":
4218 55374,
4219 "sws-s-and-m-resources-paste-replace-template-items-to-selected-tracks-last-slot":
4220 55072,
4221 "sws-s-and-m-resources-paste-replace-template-items-to-selected-tracks-last-slot-55287":
4222 55287,
4223 "sws-s-and-m-resources-paste-replace-template-items-to-selected-tracks-slot-1":
4224 55379,
4225 "sws-s-and-m-resources-paste-replace-template-items-to-selected-tracks-slot-2":
4226 55380,
4227 "sws-s-and-m-resources-paste-replace-template-items-to-selected-tracks-slot-3":
4228 55381,
4229 "sws-s-and-m-resources-paste-replace-template-items-to-selected-tracks-slot-4":
4230 55382,
4231 "sws-s-and-m-resources-paste-template-items-to-selected-tracks-last-slot":
4232 55073,
4233 "sws-s-and-m-resources-paste-template-items-to-selected-tracks-last-slot-55288":
4234 55288,
3762 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-items-all-takes-last-slot": 55057,
3763 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-items-all-takes-prompt-for-slot": 55279,
3764 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-items-last-slot": 55056,
3765 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-items-prompt-for-slot": 55278,
3766 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-items-slot-1": 55363,
3767 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-items-slot-2": 55364,
3768 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-items-slot-3": 55365,
3769 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-items-slot-4": 55366,
3770 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-tracks-last-slot": 55060,
3771 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-tracks-prompt-for-slot": 55282,
3772 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-tracks-slot-1": 55371,
3773 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-tracks-slot-2": 55372,
3774 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-tracks-slot-3": 55373,
3775 "sws-s-and-m-resources-paste-replace-fx-chain-to-selected-tracks-slot-4": 55374,
3776 "sws-s-and-m-resources-paste-replace-template-items-to-selected-tracks-last-slot": 55072,
3777 "sws-s-and-m-resources-paste-replace-template-items-to-selected-tracks-last-slot-55287": 55287,
3778 "sws-s-and-m-resources-paste-replace-template-items-to-selected-tracks-slot-1": 55379,
3779 "sws-s-and-m-resources-paste-replace-template-items-to-selected-tracks-slot-2": 55380,
3780 "sws-s-and-m-resources-paste-replace-template-items-to-selected-tracks-slot-3": 55381,
3781 "sws-s-and-m-resources-paste-replace-template-items-to-selected-tracks-slot-4": 55382,
3782 "sws-s-and-m-resources-paste-template-items-to-selected-tracks-last-slot": 55073,
3783 "sws-s-and-m-resources-paste-template-items-to-selected-tracks-last-slot-55288": 55288,
42353784 "sws-s-and-m-resources-paste-template-items-to-selected-tracks-slot-1": 55383,
42363785 "sws-s-and-m-resources-paste-template-items-to-selected-tracks-slot-2": 55384,
42373786 "sws-s-and-m-resources-paste-template-items-to-selected-tracks-slot-3": 55385,
......@@ -4241,43 +3790,25 @@ export const REAPER_ACTIONS = {
42413790 "sws-s-and-m-resources-play-media-file-in-selected-tracks-slot-2": 55408,
42423791 "sws-s-and-m-resources-play-media-file-in-selected-tracks-slot-3": 55409,
42433792 "sws-s-and-m-resources-play-media-file-in-selected-tracks-slot-4": 55410,
4244 "sws-s-and-m-resources-play-media-file-in-selected-tracks-sync-with-next-measure-slot-1":
4245 55415,
4246 "sws-s-and-m-resources-play-media-file-in-selected-tracks-sync-with-next-measure-slot-2":
4247 55416,
4248 "sws-s-and-m-resources-play-media-file-in-selected-tracks-sync-with-next-measure-slot-3":
4249 55417,
4250 "sws-s-and-m-resources-play-media-file-in-selected-tracks-sync-with-next-measure-slot-4":
4251 55418,
4252 "sws-s-and-m-resources-play-media-file-in-selected-tracks-toggle-last-slot":
4253 55092,
4254 "sws-s-and-m-resources-play-media-file-in-selected-tracks-toggle-pause-last-slot":
4255 55094,
4256 "sws-s-and-m-resources-play-media-file-in-selected-tracks-toggle-pause-slot-1":
4257 55431,
4258 "sws-s-and-m-resources-play-media-file-in-selected-tracks-toggle-pause-slot-2":
4259 55432,
4260 "sws-s-and-m-resources-play-media-file-in-selected-tracks-toggle-pause-slot-3":
4261 55433,
4262 "sws-s-and-m-resources-play-media-file-in-selected-tracks-toggle-pause-slot-4":
4263 55434,
4264 "sws-s-and-m-resources-play-media-file-in-selected-tracks-toggle-slot-1":
4265 55423,
4266 "sws-s-and-m-resources-play-media-file-in-selected-tracks-toggle-slot-2":
4267 55424,
4268 "sws-s-and-m-resources-play-media-file-in-selected-tracks-toggle-slot-3":
4269 55425,
4270 "sws-s-and-m-resources-play-media-file-in-selected-tracks-toggle-slot-4":
4271 55426,
3793 "sws-s-and-m-resources-play-media-file-in-selected-tracks-sync-with-next-measure-slot-1": 55415,
3794 "sws-s-and-m-resources-play-media-file-in-selected-tracks-sync-with-next-measure-slot-2": 55416,
3795 "sws-s-and-m-resources-play-media-file-in-selected-tracks-sync-with-next-measure-slot-3": 55417,
3796 "sws-s-and-m-resources-play-media-file-in-selected-tracks-sync-with-next-measure-slot-4": 55418,
3797 "sws-s-and-m-resources-play-media-file-in-selected-tracks-toggle-last-slot": 55092,
3798 "sws-s-and-m-resources-play-media-file-in-selected-tracks-toggle-pause-last-slot": 55094,
3799 "sws-s-and-m-resources-play-media-file-in-selected-tracks-toggle-pause-slot-1": 55431,
3800 "sws-s-and-m-resources-play-media-file-in-selected-tracks-toggle-pause-slot-2": 55432,
3801 "sws-s-and-m-resources-play-media-file-in-selected-tracks-toggle-pause-slot-3": 55433,
3802 "sws-s-and-m-resources-play-media-file-in-selected-tracks-toggle-pause-slot-4": 55434,
3803 "sws-s-and-m-resources-play-media-file-in-selected-tracks-toggle-slot-1": 55423,
3804 "sws-s-and-m-resources-play-media-file-in-selected-tracks-toggle-slot-2": 55424,
3805 "sws-s-and-m-resources-play-media-file-in-selected-tracks-toggle-slot-3": 55425,
3806 "sws-s-and-m-resources-play-media-file-in-selected-tracks-toggle-slot-4": 55426,
42723807 "sws-s-and-m-resources-set-add-media-file-option-to-default": 55099,
4273 "sws-s-and-m-resources-set-add-media-file-option-to-stretch-loop-to-fit-time-sel":
4274 55100,
4275 "sws-s-and-m-resources-set-add-media-file-option-to-try-to-match-tempo-0-5x":
4276 55101,
4277 "sws-s-and-m-resources-set-add-media-file-option-to-try-to-match-tempo-1x":
4278 55102,
4279 "sws-s-and-m-resources-set-add-media-file-option-to-try-to-match-tempo-2x":
4280 55103,
3808 "sws-s-and-m-resources-set-add-media-file-option-to-stretch-loop-to-fit-time-sel": 55100,
3809 "sws-s-and-m-resources-set-add-media-file-option-to-try-to-match-tempo-0-5x": 55101,
3810 "sws-s-and-m-resources-set-add-media-file-option-to-try-to-match-tempo-1x": 55102,
3811 "sws-s-and-m-resources-set-add-media-file-option-to-try-to-match-tempo-2x": 55103,
42813812 "sws-s-and-m-resources-set-track-icon-for-selected-tracks-last-slot": 55110,
42823813 "sws-s-and-m-resources-set-track-icon-for-selected-tracks-slot-1": 55451,
42833814 "sws-s-and-m-resources-set-track-icon-for-selected-tracks-slot-2": 55452,
......@@ -4291,8 +3822,7 @@ export const REAPER_ACTIONS = {
42913822 "sws-s-and-m-resources-show-next-image-slot": 55105,
42923823 "sws-s-and-m-resources-show-previous-image-slot": 55106,
42933824 "sws-s-and-m-resources-stop-all-playing-media-files": 55085,
4294 "sws-s-and-m-resources-stop-all-playing-media-files-in-selected-tracks":
4295 55086,
3825 "sws-s-and-m-resources-stop-all-playing-media-files-in-selected-tracks": 55086,
42963826 "sws-s-and-m-restore-selected-tracks-folder-compact-states": 55183,
42973827 "sws-s-and-m-restore-selected-tracks-folder-states": 55177,
42983828 "sws-s-and-m-save-default-track-send-preferences": 54956,
......@@ -4406,10 +3936,8 @@ export const REAPER_ACTIONS = {
44063936 "sws-s-and-m-show-theme-helper-all-tracks": 55263,
44073937 "sws-s-and-m-show-theme-helper-selected-tracks": 55264,
44083938 "sws-s-and-m-split-and-select-items-in-region-near-cursor": 55166,
4409 "sws-s-and-m-split-selected-items-at-edit-cursor-midi-or-prior-zero-crossing-audio":
4410 55163,
4411 "sws-s-and-m-split-selected-items-at-time-selection-edit-cursor-midi-or-prior-zero-crossing-audio":
4412 55164,
3939 "sws-s-and-m-split-selected-items-at-edit-cursor-midi-or-prior-zero-crossing-audio": 55163,
3940 "sws-s-and-m-split-selected-items-at-time-selection-edit-cursor-midi-or-prior-zero-crossing-audio": 55164,
44133941 "sws-s-and-m-takes-activate-lane-under-mouse-cursor": 55139,
44143942 "sws-s-and-m-takes-activate-lanes-from-selected-items": 55138,
44153943 "sws-s-and-m-takes-clear-active-takes-items": 55137,
......@@ -4419,21 +3947,15 @@ export const REAPER_ACTIONS = {
44193947 "sws-s-and-m-takes-remove-empty-takes-items-among-selected-items": 55140,
44203948 "sws-s-and-m-toggle-all-fx-bypass-for-selected-tracks": 55003,
44213949 "sws-s-and-m-toggle-all-fx-except-selected-bypass-for-selected-tracks": 55007,
4422 "sws-s-and-m-toggle-all-fx-except-selected-online-offline-for-selected-tracks":
4423 55006,
3950 "sws-s-and-m-toggle-all-fx-except-selected-online-offline-for-selected-tracks": 55006,
44243951 "sws-s-and-m-toggle-all-fx-online-offline-for-selected-tracks": 54996,
44253952 "sws-s-and-m-toggle-all-take-fx-bypass-for-selected-items": 55011,
44263953 "sws-s-and-m-toggle-all-take-fx-online-offline-for-selected-items": 55008,
4427 "sws-s-and-m-toggle-arming-of-all-active-envelopes-for-selected-tracks":
4428 55208,
4429 "sws-s-and-m-toggle-arming-of-all-plugin-envelopes-for-selected-tracks":
4430 55217,
4431 "sws-s-and-m-toggle-arming-of-all-receive-mute-envelopes-for-selected-tracks":
4432 55216,
4433 "sws-s-and-m-toggle-arming-of-all-receive-pan-envelopes-for-selected-tracks":
4434 55215,
4435 "sws-s-and-m-toggle-arming-of-all-receive-volume-envelopes-for-selected-tracks":
4436 55214,
3954 "sws-s-and-m-toggle-arming-of-all-active-envelopes-for-selected-tracks": 55208,
3955 "sws-s-and-m-toggle-arming-of-all-plugin-envelopes-for-selected-tracks": 55217,
3956 "sws-s-and-m-toggle-arming-of-all-receive-mute-envelopes-for-selected-tracks": 55216,
3957 "sws-s-and-m-toggle-arming-of-all-receive-pan-envelopes-for-selected-tracks": 55215,
3958 "sws-s-and-m-toggle-arming-of-all-receive-volume-envelopes-for-selected-tracks": 55214,
44373959 "sws-s-and-m-toggle-arming-of-mute-envelope-for-selected-tracks": 55213,
44383960 "sws-s-and-m-toggle-arming-of-pan-envelope-for-selected-tracks": 55212,
44393961 "sws-s-and-m-toggle-arming-of-volume-envelope-for-selected-tracks": 55211,
......@@ -4484,8 +4006,7 @@ export const REAPER_ACTIONS = {
44844006 "sws-s-and-m-toolbar-toggle-offscreen-item-selection-left": 55221,
44854007 "sws-s-and-m-toolbar-toggle-offscreen-item-selection-right": 55222,
44864008 "sws-s-and-m-toolbar-toggle-offscreen-item-selection-top": 55223,
4487 "sws-s-and-m-toolbar-toggle-track-envelopes-in-touch-latch-latch-preview-write":
4488 55220,
4009 "sws-s-and-m-toolbar-toggle-track-envelopes-in-touch-latch-latch-preview-write": 55220,
44894010 "sws-s-and-m-trigger-next-preset-for-fx-1-of-selected-tracks": 55459,
44904011 "sws-s-and-m-trigger-next-preset-for-fx-2-of-selected-tracks": 55460,
44914012 "sws-s-and-m-trigger-next-preset-for-fx-3-of-selected-tracks": 55461,
......@@ -4500,15 +4021,13 @@ export const REAPER_ACTIONS = {
45004021 "sws-s-and-m-trigger-preset-for-fx-6-of-selected-track-midi-osc-only": 55472,
45014022 "sws-s-and-m-trigger-preset-for-fx-7-of-selected-track-midi-osc-only": 55473,
45024023 "sws-s-and-m-trigger-preset-for-fx-8-of-selected-track-midi-osc-only": 55474,
4503 "sws-s-and-m-trigger-preset-for-selected-fx-of-selected-track-midi-osc-only":
4504 55014,
4024 "sws-s-and-m-trigger-preset-for-selected-fx-of-selected-track-midi-osc-only": 55014,
45054025 "sws-s-and-m-trigger-previous-preset-for-fx-1-of-selected-tracks": 55463,
45064026 "sws-s-and-m-trigger-previous-preset-for-fx-2-of-selected-tracks": 55464,
45074027 "sws-s-and-m-trigger-previous-preset-for-fx-3-of-selected-tracks": 55465,
45084028 "sws-s-and-m-trigger-previous-preset-for-fx-4-of-selected-tracks": 55466,
45094029 "sws-s-and-m-trigger-previous-preset-for-last-touched-fx": 55118,
4510 "sws-s-and-m-trigger-previous-preset-for-selected-fx-of-selected-tracks":
4511 55116,
4030 "sws-s-and-m-trigger-previous-preset-for-selected-fx-of-selected-tracks": 55116,
45124031 "sws-s-and-m-unbypass-all-fx-for-selected-tracks": 55005,
45134032 "sws-s-and-m-unbypass-all-take-fx-for-selected-items": 55013,
45144033 "sws-s-and-m-unbypass-fx-1-for-selected-tracks": 55331,
......@@ -4774,8 +4293,7 @@ export const REAPER_ACTIONS = {
47744293 "sws-set-takes-in-selected-item-s-to-color-gradient": 53064,
47754294 "sws-set-takes-in-selected-item-s-to-ordered-custom-colors": 53065,
47764295 "sws-set-takes-in-selected-item-s-to-random-custom-color-s": 53063,
4777 "sws-set-time-selection-to-selected-items-skip-if-time-selection-exists":
4778 53585,
4296 "sws-set-time-selection-to-selected-items-skip-if-time-selection-exists": 53585,
47794297 "sws-set-track-name-from-first-selected-item-in-project": 53745,
47804298 "sws-set-track-name-from-first-selected-item-on-track": 53744,
47814299 "sws-set-transport-repeat-state": 53681,
......@@ -4801,14 +4319,10 @@ export const REAPER_ACTIONS = {
48014319 "sws-show-tracklist-with-filter-focused": 53194,
48024320 "sws-sn-focus-midi-editor": 54938,
48034321 "sws-snapshot-current-track-visibility": 53215,
4804 "sws-split-items-at-time-selection-razor-edit-areas-edit-cursor-also-during-playback-or-mouse-cursor":
4805 53582,
4806 "sws-split-items-at-time-selection-razor-edit-areas-edit-cursor-play-cursor-during-playback-or-mouse-cursor":
4807 53581,
4808 "sws-split-items-at-time-selection-razor-edit-areas-if-exists-else-at-edit-cursor-also-during-playback":
4809 53580,
4810 "sws-split-items-at-time-selection-razor-edit-areas-if-exists-play-cursor-during-playback-else-at-edit-cursor":
4811 53579,
4322 "sws-split-items-at-time-selection-razor-edit-areas-edit-cursor-also-during-playback-or-mouse-cursor": 53582,
4323 "sws-split-items-at-time-selection-razor-edit-areas-edit-cursor-play-cursor-during-playback-or-mouse-cursor": 53581,
4324 "sws-split-items-at-time-selection-razor-edit-areas-if-exists-else-at-edit-cursor-also-during-playback": 53580,
4325 "sws-split-items-at-time-selection-razor-edit-areas-if-exists-play-cursor-during-playback-else-at-edit-cursor": 53579,
48124326 "sws-switch-to-last-project-tab": 53223,
48134327 "sws-switch-to-project-tab-1": 53224,
48144328 "sws-switch-to-project-tab-10": 53233,
......@@ -4875,33 +4389,22 @@ export const REAPER_ACTIONS = {
48754389 "sws-toggle-snapshot-vol": 53181,
48764390 "sws-toggle-zoom-to-selected-items": 53807,
48774391 "sws-toggle-zoom-to-selected-items-hide-other-tracks": 53809,
4878 "sws-toggle-zoom-to-selected-items-hide-other-tracks-ignore-last-track-s-envelope-lanes":
4879 53818,
4392 "sws-toggle-zoom-to-selected-items-hide-other-tracks-ignore-last-track-s-envelope-lanes": 53818,
48804393 "sws-toggle-zoom-to-selected-items-ignore-last-track-s-envelope-lanes": 53816,
48814394 "sws-toggle-zoom-to-selected-items-minimize-other-tracks": 53808,
4882 "sws-toggle-zoom-to-selected-items-minimize-other-tracks-ignore-last-track-s-envelope-lanes":
4883 53817,
4395 "sws-toggle-zoom-to-selected-items-minimize-other-tracks-ignore-last-track-s-envelope-lanes": 53817,
48844396 "sws-toggle-zoom-to-selected-items-or-time-selection": 53804,
4885 "sws-toggle-zoom-to-selected-items-or-time-selection-hide-other-tracks":
4886 53806,
4887 "sws-toggle-zoom-to-selected-items-or-time-selection-hide-other-tracks-ignore-last-track-s-envelope-lanes":
4888 53815,
4889 "sws-toggle-zoom-to-selected-items-or-time-selection-ignore-last-track-s-envelope-lanes":
4890 53813,
4891 "sws-toggle-zoom-to-selected-items-or-time-selection-minimize-other-tracks":
4892 53805,
4893 "sws-toggle-zoom-to-selected-items-or-time-selection-minimize-other-tracks-ignore-last-track-s-envelope-lanes":
4894 53814,
4397 "sws-toggle-zoom-to-selected-items-or-time-selection-hide-other-tracks": 53806,
4398 "sws-toggle-zoom-to-selected-items-or-time-selection-hide-other-tracks-ignore-last-track-s-envelope-lanes": 53815,
4399 "sws-toggle-zoom-to-selected-items-or-time-selection-ignore-last-track-s-envelope-lanes": 53813,
4400 "sws-toggle-zoom-to-selected-items-or-time-selection-minimize-other-tracks": 53805,
4401 "sws-toggle-zoom-to-selected-items-or-time-selection-minimize-other-tracks-ignore-last-track-s-envelope-lanes": 53814,
48954402 "sws-toggle-zoom-to-selected-tracks-and-time-selection": 53801,
48964403 "sws-toggle-zoom-to-selected-tracks-and-time-selection-hide-others": 53803,
4897 "sws-toggle-zoom-to-selected-tracks-and-time-selection-hide-others-ignore-last-track-s-envelope-lanes":
4898 53812,
4899 "sws-toggle-zoom-to-selected-tracks-and-time-selection-ignore-last-track-s-envelope-lanes":
4900 53810,
4901 "sws-toggle-zoom-to-selected-tracks-and-time-selection-minimize-others":
4902 53802,
4903 "sws-toggle-zoom-to-selected-tracks-and-time-selection-minimize-others-ignore-last-track-s-envelope-lanes":
4904 53811,
4404 "sws-toggle-zoom-to-selected-tracks-and-time-selection-hide-others-ignore-last-track-s-envelope-lanes": 53812,
4405 "sws-toggle-zoom-to-selected-tracks-and-time-selection-ignore-last-track-s-envelope-lanes": 53810,
4406 "sws-toggle-zoom-to-selected-tracks-and-time-selection-minimize-others": 53802,
4407 "sws-toggle-zoom-to-selected-tracks-and-time-selection-minimize-others-ignore-last-track-s-envelope-lanes": 53811,
49054408 "sws-toolbar-arm-toggle": 53740,
49064409 "sws-toolbar-mute-toggle": 53748,
49074410 "sws-toolbar-solo-toggle": 53746,
......@@ -4926,31 +4429,23 @@ export const REAPER_ACTIONS = {
49264429 "sws-unset-selected-track-s-monitor-track-media-while-recording": 53739,
49274430 "sws-unset-transport-repeat-state": 53682,
49284431 "sws-vertical-zoom-to-selected-items": 53786,
4929 "sws-vertical-zoom-to-selected-items-ignore-last-track-s-envelope-lanes":
4930 53790,
4432 "sws-vertical-zoom-to-selected-items-ignore-last-track-s-envelope-lanes": 53790,
49314433 "sws-vertical-zoom-to-selected-items-minimize-others": 53787,
4932 "sws-vertical-zoom-to-selected-items-minimize-others-ignore-last-track-s-envelope-lanes":
4933 53791,
4434 "sws-vertical-zoom-to-selected-items-minimize-others-ignore-last-track-s-envelope-lanes": 53791,
49344435 "sws-vertical-zoom-to-selected-tracks": 53784,
4935 "sws-vertical-zoom-to-selected-tracks-ignore-last-track-s-envelope-lanes":
4936 53788,
4436 "sws-vertical-zoom-to-selected-tracks-ignore-last-track-s-envelope-lanes": 53788,
49374437 "sws-vertical-zoom-to-selected-tracks-minimize-others": 53785,
4938 "sws-vertical-zoom-to-selected-tracks-minimize-others-ignore-last-track-s-envelope-lanes":
4939 53789,
4438 "sws-vertical-zoom-to-selected-tracks-minimize-others-ignore-last-track-s-envelope-lanes": 53789,
49404439 "sws-wait-for-next-bar-if-playing": 53663,
49414440 "sws-wait-for-next-beat-if-playing": 53664,
49424441 "sws-wait-until-end-of-loop-if-playing": 53665,
4943 "sws-wol-adjust-envelope-or-track-height-under-mouse-cursor-midi-cc-relative-mousewheel":
4944 54892,
4442 "sws-wol-adjust-envelope-or-track-height-under-mouse-cursor-midi-cc-relative-mousewheel": 54892,
49454443 "sws-wol-adjust-envelope-or-track-height-under-mouse-cursor-zoom-center-to-mouse-cursor-midi-cc-relative-mousewheel":
49464444 54893,
49474445 "sws-wol-adjust-selected-envelope-height-midi-cc-relative-mousewheel": 54886,
4948 "sws-wol-adjust-selected-envelope-height-zoom-center-to-middle-arrange-midi-cc-relative-mousewheel":
4949 54887,
4950 "sws-wol-adjust-selected-envelope-height-zoom-center-to-mouse-cursor-midi-cc-relative-mousewheel":
4951 54888,
4952 "sws-wol-adjust-selected-envelope-or-last-touched-track-height-midi-cc-relative-mousewheel":
4953 54889,
4446 "sws-wol-adjust-selected-envelope-height-zoom-center-to-middle-arrange-midi-cc-relative-mousewheel": 54887,
4447 "sws-wol-adjust-selected-envelope-height-zoom-center-to-mouse-cursor-midi-cc-relative-mousewheel": 54888,
4448 "sws-wol-adjust-selected-envelope-or-last-touched-track-height-midi-cc-relative-mousewheel": 54889,
49544449 "sws-wol-adjust-selected-envelope-or-last-touched-track-height-zoom-center-to-middle-arrange-midi-cc-relative-mousewheel":
49554450 54890,
49564451 "sws-wol-adjust-selected-envelope-or-last-touched-track-height-zoom-center-to-mouse-cursor-midi-cc-relative-mousewheel":
......@@ -4963,18 +4458,14 @@ export const REAPER_ACTIONS = {
49634458 "sws-wol-apply-height-to-selected-envelope-slot-6": 54917,
49644459 "sws-wol-apply-height-to-selected-envelope-slot-7": 54918,
49654460 "sws-wol-apply-height-to-selected-envelope-slot-8": 54919,
4966 "sws-wol-force-overlap-for-selected-envelope-in-track-lane-in-its-track-height":
4967 54896,
4968 "sws-wol-full-zoom-selected-envelope-in-media-lane-only-to-lower-half-in-time-selection":
4969 54901,
4970 "sws-wol-full-zoom-selected-envelope-in-media-lane-only-to-upper-half-in-time-selection":
4971 54900,
4461 "sws-wol-force-overlap-for-selected-envelope-in-track-lane-in-its-track-height": 54896,
4462 "sws-wol-full-zoom-selected-envelope-in-media-lane-only-to-lower-half-in-time-selection": 54901,
4463 "sws-wol-full-zoom-selected-envelope-in-media-lane-only-to-upper-half-in-time-selection": 54900,
49724464 "sws-wol-full-zoom-selected-envelope-in-time-selection": 54899,
49734465 "sws-wol-horizontal-zoom-selected-envelope-in-time-selection": 54898,
49744466 "sws-wol-options-set-horizontal-zoom-center-to-center-of-view": 54881,
49754467 "sws-wol-options-set-horizontal-zoom-center-to-edit-cursor": 54880,
4976 "sws-wol-options-set-horizontal-zoom-center-to-edit-cursor-or-play-cursor-default":
4977 54879,
4468 "sws-wol-options-set-horizontal-zoom-center-to-edit-cursor-or-play-cursor-default": 54879,
49784469 "sws-wol-options-set-horizontal-zoom-center-to-mouse-cursor": 54882,
49794470 "sws-wol-options-set-vertical-zoom-center-to-last-selected-track": 54877,
49804471 "sws-wol-options-set-vertical-zoom-center-to-top-visible-track": 54876,
......@@ -4997,22 +4488,17 @@ export const REAPER_ACTIONS = {
49974488 "sws-wol-set-selected-envelope-height-to-minimum": 54884,
49984489 "sws-wol-toggle-enable-envelope-overlap-for-envelopes-in-track-lane": 54895,
49994490 "sws-wol-toggle-enable-extended-zoom-for-envelopes-in-track-lane": 54894,
5000 "sws-wol-vertical-zoom-selected-envelope-in-media-lane-only-to-lower-half":
5001 54903,
5002 "sws-wol-vertical-zoom-selected-envelope-in-media-lane-only-to-upper-half":
5003 54902,
4491 "sws-wol-vertical-zoom-selected-envelope-in-media-lane-only-to-lower-half": 54903,
4492 "sws-wol-vertical-zoom-selected-envelope-in-media-lane-only-to-upper-half": 54902,
50044493 "sws-zoom-preferences": 53839,
50054494 "sws-zoom-to-selected-items": 53793,
50064495 "sws-zoom-to-selected-items-ignore-last-track-s-envelope-lanes": 53797,
50074496 "sws-zoom-to-selected-items-minimize-others": 53794,
5008 "sws-zoom-to-selected-items-minimize-others-ignore-last-track-s-envelope-lanes":
5009 53798,
4497 "sws-zoom-to-selected-items-minimize-others-ignore-last-track-s-envelope-lanes": 53798,
50104498 "sws-zoom-to-selected-items-or-time-selection": 53795,
5011 "sws-zoom-to-selected-items-or-time-selection-ignore-last-track-s-envelope-lanes":
5012 53799,
4499 "sws-zoom-to-selected-items-or-time-selection-ignore-last-track-s-envelope-lanes": 53799,
50134500 "sws-zoom-to-selected-items-or-time-selection-minimize-others": 53796,
5014 "sws-zoom-to-selected-items-or-time-selection-minimize-others-ignore-last-track-s-envelope-lanes":
5015 53800,
4501 "sws-zoom-to-selected-items-or-time-selection-minimize-others-ignore-last-track-s-envelope-lanes": 53800,
50164502 "sws-zoom-tool-marquee": 53838,
50174503 "take-activate-take-under-mouse": 41342,
50184504 "take-crop-to-active-take-in-items": 40131,
......@@ -5025,12 +4511,10 @@ export const REAPER_ACTIONS = {
50254511 "take-explode-takes-of-items-in-order": 40643,
50264512 "take-explode-takes-of-items-in-place": 40642,
50274513 "take-explode-takes-on-selected-tracks-to-fixed-lanes": 42635,
5028 "take-explode-takes-on-selected-tracks-to-fixed-lanes-and-add-comp-areas-from-active-takes":
5029 42636,
4514 "take-explode-takes-on-selected-tracks-to-fixed-lanes-and-add-comp-areas-from-active-takes": 42636,
50304515 "take-implode-items-across-tracks-into-takes": 40438,
50314516 "take-implode-items-on-same-track-into-takes": 40543,
5032 "take-implode-selected-fixed-lane-tracks-to-takes-using-best-efforts-overlapping-items-may-be-shortened":
5033 42637,
4517 "take-implode-selected-fixed-lane-tracks-to-takes-using-best-efforts-overlapping-items-may-be-shortened": 42637,
50344518 "take-nudge-active-takes-volume-1db": 41926,
50354519 "take-nudge-active-takes-volume-plus-1db": 41927,
50364520 "take-paste-as-takes-in-items": 40603,
......@@ -5048,12 +4532,9 @@ export const REAPER_ACTIONS = {
50484532 "take-set-active-take-to-custom-color": 41331,
50494533 "take-set-active-take-to-default-color": 41333,
50504534 "take-set-active-take-to-one-random-color": 41332,
5051 "take-set-all-takes-created-in-the-same-recording-pass-to-custom-color":
5052 41334,
5053 "take-set-all-takes-created-in-the-same-recording-pass-to-default-color":
5054 41336,
5055 "take-set-all-takes-created-in-the-same-recording-pass-to-one-random-color":
5056 41335,
4535 "take-set-all-takes-created-in-the-same-recording-pass-to-custom-color": 41334,
4536 "take-set-all-takes-created-in-the-same-recording-pass-to-default-color": 41336,
4537 "take-set-all-takes-created-in-the-same-recording-pass-to-one-random-color": 41335,
50574538 "take-set-all-takes-of-selected-items-to-default-color": 41337,
50584539 "take-switch-items-to-next-take": 40125,
50594540 "take-switch-items-to-previous-take": 40126,
......@@ -5083,10 +4564,8 @@ export const REAPER_ACTIONS = {
50834564 "tempo-envelope-increase-all-tempo-markers-0-1-percent": 41810,
50844565 "tempo-envelope-increase-all-tempo-markers-01-bpm": 41215,
50854566 "tempo-envelope-increase-all-tempo-markers-01-percent": 41808,
5086 "tempo-envelope-insert-tempo-marker-at-edit-cursor-without-opening-tempo-edit-dialog":
5087 42330,
5088 "tempo-envelope-insert-tempo-time-signature-change-marker-at-edit-cursor":
5089 40256,
4567 "tempo-envelope-insert-tempo-marker-at-edit-cursor-without-opening-tempo-edit-dialog": 42330,
4568 "tempo-envelope-insert-tempo-time-signature-change-marker-at-edit-cursor": 40256,
50904569 "tempo-envelope-set-display-range": 40933,
50914570 "tempo-envelope-set-display-range-to-current-project-min-max-bpm": 41804,
50924571 "tempo-increase-current-project-tempo-0-1-bpm": 41137,
......@@ -5096,14 +4575,11 @@ export const REAPER_ACTIONS = {
50964575 "tempo-increase-current-project-tempo-100-percent-double": 41133,
50974576 "theme-development-show-theme-element-finder": 40690,
50984577 "theme-development-show-theme-tweak-configuration-window": 41930,
5099 "time-selection-copy-contents-of-time-selection-to-edit-cursor-moving-later-items":
5100 40397,
4578 "time-selection-copy-contents-of-time-selection-to-edit-cursor-moving-later-items": 40397,
51014579 "time-selection-crop-project-to-time-selection": 40049,
51024580 "time-selection-extend-time-selection-to-next-transient-in-items": 40802,
5103 "time-selection-insert-empty-space-at-time-selection-moving-later-items":
5104 40200,
5105 "time-selection-move-contents-of-time-selection-to-edit-cursor-moving-later-items":
5106 40396,
4581 "time-selection-insert-empty-space-at-time-selection-moving-later-items": 40200,
4582 "time-selection-move-contents-of-time-selection-to-edit-cursor-moving-later-items": 40396,
51074583 "time-selection-move-cursor-left-creating-time-selection": 40102,
51084584 "time-selection-move-cursor-right-creating-time-selection": 40103,
51094585 "time-selection-move-end-point-to-cursor-preserve-length": 43213,
......@@ -5124,8 +4600,7 @@ export const REAPER_ACTIONS = {
51244600 "time-selection-set-time-selection-to-items": 40290,
51254601 "time-selection-shift-left-by-time-selection-length": 40037,
51264602 "time-selection-shift-right-by-time-selection-length": 40038,
5127 "time-selection-swap-left-edge-of-time-selection-to-next-transient-in-items":
5128 40803,
4603 "time-selection-swap-left-edge-of-time-selection-to-next-transient-in-items": 40803,
51294604 "toggle-external-timecode-synchronization": 40620,
51304605 "toggle-fullscreen": 40346,
51314606 "toggle-ripple-editing-all-tracks": 41991,
......@@ -5355,22 +4830,16 @@ export const REAPER_ACTIONS = {
53554830 "toolbars-switch-to-toolbar-7": 41649,
53564831 "toolbars-switch-to-toolbar-8": 41650,
53574832 "toolbars-switch-to-toolbar-9": 41948,
5358 "track-2nd-pass-render-selected-area-of-tracks-to-mono-post-fader-stem-tracks-and-mute-originals":
5359 42415,
5360 "track-2nd-pass-render-selected-area-of-tracks-to-mono-stem-tracks-and-mute-originals":
5361 42418,
4833 "track-2nd-pass-render-selected-area-of-tracks-to-mono-post-fader-stem-tracks-and-mute-originals": 42415,
4834 "track-2nd-pass-render-selected-area-of-tracks-to-mono-stem-tracks-and-mute-originals": 42418,
53624835 "track-2nd-pass-render-selected-area-of-tracks-to-multichannel-parent-send-only-post-fader-stem-tracks-and-mute-originals":
53634836 42593,
53644837 "track-2nd-pass-render-selected-area-of-tracks-to-multichannel-parent-send-only-stem-tracks-and-mute-originals":
53654838 42594,
5366 "track-2nd-pass-render-selected-area-of-tracks-to-multichannel-post-fader-stem-tracks-and-mute-originals":
5367 42414,
5368 "track-2nd-pass-render-selected-area-of-tracks-to-multichannel-stem-tracks-and-mute-originals":
5369 42417,
5370 "track-2nd-pass-render-selected-area-of-tracks-to-stereo-post-fader-stem-tracks-and-mute-originals":
5371 42413,
5372 "track-2nd-pass-render-selected-area-of-tracks-to-stereo-stem-tracks-and-mute-originals":
5373 42416,
4839 "track-2nd-pass-render-selected-area-of-tracks-to-multichannel-post-fader-stem-tracks-and-mute-originals": 42414,
4840 "track-2nd-pass-render-selected-area-of-tracks-to-multichannel-stem-tracks-and-mute-originals": 42417,
4841 "track-2nd-pass-render-selected-area-of-tracks-to-stereo-post-fader-stem-tracks-and-mute-originals": 42413,
4842 "track-2nd-pass-render-selected-area-of-tracks-to-stereo-stem-tracks-and-mute-originals": 42416,
53744843 "track-allow-track-anticipative-fx": 40609,
53754844 "track-allow-track-media-buffering": 40607,
53764845 "track-apply-media-playback-offset-to-receive-source-tracks": 42233,
......@@ -5390,33 +4859,26 @@ export const REAPER_ACTIONS = {
53904859 "track-crop-to-playing-media-items": 42943,
53914860 "track-crop-to-playing-media-items-for-track-under-mouse": 42942,
53924861 "track-crop-to-playing-media-items-preserving-fixed-lanes": 42945,
5393 "track-crop-to-playing-media-items-preserving-fixed-lanes-for-track-under-mouse":
5394 42944,
4862 "track-crop-to-playing-media-items-preserving-fixed-lanes-for-track-under-mouse": 42944,
53954863 "track-cut-tracks": 40337,
53964864 "track-cycle-folder-collapsed-state": 1042,
53974865 "track-cycle-track-folder-state": 1041,
53984866 "track-cycle-track-record-monitor": 40495,
5399 "track-delete-takes-for-all-items-on-track-that-are-down-ranked-no-confirm":
5400 43167,
5401 "track-delete-takes-for-all-items-on-track-that-are-not-up-ranked-no-confirm":
5402 43166,
4867 "track-delete-takes-for-all-items-on-track-that-are-down-ranked-no-confirm": 43167,
4868 "track-delete-takes-for-all-items-on-track-that-are-not-up-ranked-no-confirm": 43166,
54034869 "track-disable-midi-input-quantize-for-all-tracks": 42066,
54044870 "track-disable-midi-input-quantize-for-last-touched-track": 42068,
54054871 "track-disable-midi-input-quantize-for-selected-tracks": 42064,
5406 "track-display-gain-reduction-in-track-meters-for-plug-ins-that-support-it":
5407 42705,
4872 "track-display-gain-reduction-in-track-meters-for-plug-ins-that-support-it": 42705,
54084873 "track-do-not-link-track-volume-pan-controls-to-midi-volume-pan": 41538,
54094874 "track-duplicate-tracks": 40062,
54104875 "track-enable-midi-input-quantize-for-all-tracks": 42065,
54114876 "track-enable-midi-input-quantize-for-last-touched-track": 42067,
54124877 "track-enable-midi-input-quantize-for-selected-tracks": 42063,
54134878 "track-exit-fixed-lane-view-for-all-fixed-lane-tracks-in-the-project": 42960,
5414 "track-freeze-to-mono-render-pre-fader-save-remove-items-and-online-fx":
5415 40901,
5416 "track-freeze-to-multichannel-render-pre-fader-save-remove-items-and-online-fx":
5417 40877,
5418 "track-freeze-to-stereo-render-pre-fader-save-remove-items-and-online-fx":
5419 41223,
4879 "track-freeze-to-mono-render-pre-fader-save-remove-items-and-online-fx": 40901,
4880 "track-freeze-to-multichannel-render-pre-fader-save-remove-items-and-online-fx": 40877,
4881 "track-freeze-to-stereo-render-pre-fader-save-remove-items-and-online-fx": 41223,
54204882 "track-go-to-next-track": 40285,
54214883 "track-go-to-next-track-leaving-other-tracks-selected": 40287,
54224884 "track-go-to-previous-track": 40286,
......@@ -5431,8 +4893,7 @@ export const REAPER_ACTIONS = {
54314893 "track-insert-new-7-1-2-surround-track-embed-reasurroundpan-in-tcp": 42423,
54324894 "track-insert-new-7-1-4-surround-track-embed-reasurroundpan-in-tcp": 42424,
54334895 "track-insert-new-7-1-surround-track-embed-reasurroundpan-in-tcp": 42422,
5434 "track-insert-new-surround-track-using-selected-tracks-as-source-audio":
5435 41585,
4896 "track-insert-new-surround-track-using-selected-tracks-as-source-audio": 41585,
54364897 "track-insert-new-track": 40001,
54374898 "track-insert-new-track-as-first-track": 43093,
54384899 "track-insert-new-track-at-end-of-mixer": 41147,
......@@ -5448,29 +4909,21 @@ export const REAPER_ACTIONS = {
54484909 "track-invert-track-polarity-phase": 40282,
54494910 "track-lanes-add-comp-areas-for-selected-items": 42652,
54504911 "track-lanes-add-empty-lane-at-bottom-of-track": 42647,
5451 "track-lanes-comp-into-a-new-copy-of-lane-under-mouse-for-track-under-mouse":
5452 42487,
4912 "track-lanes-comp-into-a-new-copy-of-lane-under-mouse-for-track-under-mouse": 42487,
54534913 "track-lanes-comp-into-lane-under-mouse-for-track-under-mouse": 42499,
54544914 "track-lanes-comp-into-new-empty-lane": 42797,
5455 "track-lanes-comp-into-new-empty-lane-automatically-creating-comp-areas":
5456 42798,
4915 "track-lanes-comp-into-new-empty-lane-automatically-creating-comp-areas": 42798,
54574916 "track-lanes-comp-into-new-empty-lane-for-track-under-mouse": 42486,
5458 "track-lanes-comp-into-new-empty-lane-for-track-under-mouse-automatically-creating-comp-areas":
5459 42649,
5460 "track-lanes-copy-edited-media-item-back-to-source-lane-and-re-comp-for-track-under-mouse":
5461 42949,
5462 "track-lanes-copy-edited-media-items-to-new-lane-and-re-comp-for-track-under-mouse":
5463 42654,
5464 "track-lanes-copy-edited-media-items-with-no-matching-source-lane-to-new-lane-and-re-comp":
5465 42802,
5466 "track-lanes-delete-all-lanes-except-lane-under-mouse-for-track-under-mouse-including-media-items":
5467 42933,
4917 "track-lanes-comp-into-new-empty-lane-for-track-under-mouse-automatically-creating-comp-areas": 42649,
4918 "track-lanes-copy-edited-media-item-back-to-source-lane-and-re-comp-for-track-under-mouse": 42949,
4919 "track-lanes-copy-edited-media-items-to-new-lane-and-re-comp-for-track-under-mouse": 42654,
4920 "track-lanes-copy-edited-media-items-with-no-matching-source-lane-to-new-lane-and-re-comp": 42802,
4921 "track-lanes-delete-all-lanes-except-lane-under-mouse-for-track-under-mouse-including-media-items": 42933,
54684922 "track-lanes-delete-all-lanes-including-media-items": 42796,
54694923 "track-lanes-delete-comp-areas": 42955,
54704924 "track-lanes-delete-comp-areas-for-track-under-mouse": 42789,
54714925 "track-lanes-delete-comp-areas-including-source-media": 42956,
5472 "track-lanes-delete-comp-areas-including-source-media-for-track-under-mouse":
5473 42683,
4926 "track-lanes-delete-comp-areas-including-source-media-for-track-under-mouse": 42683,
54744927 "track-lanes-delete-empty-comp-areas": 42954,
54754928 "track-lanes-delete-empty-comp-areas-for-track-under-mouse": 42953,
54764929 "track-lanes-delete-empty-lanes-with-no-media-items": 42689,
......@@ -5480,24 +4933,18 @@ export const REAPER_ACTIONS = {
54804933 "track-lanes-delete-lanes-including-media-items-that-are-not-playing": 42691,
54814934 "track-lanes-delete-lanes-including-media-items-with-no-comp-areas": 42690,
54824935 "track-lanes-delete-source-media-within-comp-areas-and-re-comp": 42629,
5483 "track-lanes-delete-source-media-within-comp-areas-and-re-comp-for-track-under-mouse":
5484 42684,
5485 "track-lanes-discard-media-item-edits-and-re-comp-from-source-lane-for-track-under-mouse":
5486 42950,
4936 "track-lanes-delete-source-media-within-comp-areas-and-re-comp-for-track-under-mouse": 42684,
4937 "track-lanes-discard-media-item-edits-and-re-comp-from-source-lane-for-track-under-mouse": 42950,
54874938 "track-lanes-duplicate-items-from-playing-lanes-to-new-lanes": 42505,
54884939 "track-lanes-insert-empty-lane-at-top-of-track": 42500,
5489 "track-lanes-media-items-in-higher-numbered-lanes-mask-playback-of-lower-lanes":
5490 42941,
5491 "track-lanes-media-items-in-higher-numbered-lanes-mask-playback-of-lower-lanes-for-track-under-mouse":
5492 42940,
4940 "track-lanes-media-items-in-higher-numbered-lanes-mask-playback-of-lower-lanes": 42941,
4941 "track-lanes-media-items-in-higher-numbered-lanes-mask-playback-of-lower-lanes-for-track-under-mouse": 42940,
54934942 "track-lanes-move-items-down-one-lane": 40107,
5494 "track-lanes-move-items-down-to-first-available-lane-add-lane-if-needed":
5495 42787,
4943 "track-lanes-move-items-down-to-first-available-lane-add-lane-if-needed": 42787,
54964944 "track-lanes-move-items-to-bottom-lane": 42588,
54974945 "track-lanes-move-items-to-top-lane": 42587,
54984946 "track-lanes-move-items-up-if-possible-to-minimize-lane-usage": 42959,
5499 "track-lanes-move-items-up-if-possible-to-minimize-lane-usage-preserve-relative-lane-positions":
5500 42938,
4947 "track-lanes-move-items-up-if-possible-to-minimize-lane-usage-preserve-relative-lane-positions": 42938,
55014948 "track-lanes-move-items-up-one-lane": 40068,
55024949 "track-lanes-play-all-lanes": 42799,
55034950 "track-lanes-play-all-lanes-for-track-under-mouse": 42479,
......@@ -5507,8 +4954,7 @@ export const REAPER_ACTIONS = {
55074954 "track-lanes-play-only-first-lane-for-track-under-mouse": 42791,
55084955 "track-lanes-play-only-lane-under-mouse": 42478,
55094956 "track-lanes-play-only-most-recently-playing-lane": 43701,
5510 "track-lanes-play-only-most-recently-playing-lane-for-track-under-mouse":
5511 43702,
4957 "track-lanes-play-only-most-recently-playing-lane-for-track-under-mouse": 43702,
55124958 "track-lanes-play-only-next-lane": 42482,
55134959 "track-lanes-play-only-next-lane-for-track-under-mouse": 42484,
55144960 "track-lanes-play-only-previous-lane": 42481,
......@@ -5522,23 +4968,15 @@ export const REAPER_ACTIONS = {
55224968 "track-lanes-toggle-playing-lane-under-mouse": 42480,
55234969 "track-lanes-turn-off-comping": 42692,
55244970 "track-lanes-turn-off-comping-for-track-under-mouse": 42506,
5525 "track-link-track-volume-pan-controls-to-midi-volume-pan-on-all-channels":
5526 41555,
4971 "track-link-track-volume-pan-controls-to-midi-volume-pan-on-all-channels": 41555,
55274972 "track-link-track-volume-pan-controls-to-midi-volume-pan-on-channel-1": 41539,
5528 "track-link-track-volume-pan-controls-to-midi-volume-pan-on-channel-10":
5529 41548,
5530 "track-link-track-volume-pan-controls-to-midi-volume-pan-on-channel-11":
5531 41549,
5532 "track-link-track-volume-pan-controls-to-midi-volume-pan-on-channel-12":
5533 41550,
5534 "track-link-track-volume-pan-controls-to-midi-volume-pan-on-channel-13":
5535 41551,
5536 "track-link-track-volume-pan-controls-to-midi-volume-pan-on-channel-14":
5537 41552,
5538 "track-link-track-volume-pan-controls-to-midi-volume-pan-on-channel-15":
5539 41553,
5540 "track-link-track-volume-pan-controls-to-midi-volume-pan-on-channel-16":
5541 41554,
4973 "track-link-track-volume-pan-controls-to-midi-volume-pan-on-channel-10": 41548,
4974 "track-link-track-volume-pan-controls-to-midi-volume-pan-on-channel-11": 41549,
4975 "track-link-track-volume-pan-controls-to-midi-volume-pan-on-channel-12": 41550,
4976 "track-link-track-volume-pan-controls-to-midi-volume-pan-on-channel-13": 41551,
4977 "track-link-track-volume-pan-controls-to-midi-volume-pan-on-channel-14": 41552,
4978 "track-link-track-volume-pan-controls-to-midi-volume-pan-on-channel-15": 41553,
4979 "track-link-track-volume-pan-controls-to-midi-volume-pan-on-channel-16": 41554,
55424980 "track-link-track-volume-pan-controls-to-midi-volume-pan-on-channel-2": 41540,
55434981 "track-link-track-volume-pan-controls-to-midi-volume-pan-on-channel-3": 41541,
55444982 "track-link-track-volume-pan-controls-to-midi-volume-pan-on-channel-4": 41542,
......@@ -5578,8 +5016,7 @@ export const REAPER_ACTIONS = {
55785016 "track-override-show-all-hidden-tracks-in-tcp": 43574,
55795017 "track-override-unpin-all-pinned-tracks-in-tcp": 43573,
55805018 "track-pin-tracks-to-top-of-arrange-view": 40000,
5581 "track-pin-tracks-to-top-of-arrange-view-unpin-all-other-tracks-except-master":
5582 40008,
5019 "track-pin-tracks-to-top-of-arrange-view-unpin-all-other-tracks-except-master": 40008,
55835020 "track-prevent-spectral-peaks-spectrogram": 42075,
55845021 "track-prevent-track-anticipative-fx": 40610,
55855022 "track-prevent-track-media-buffering": 40608,
......@@ -5603,10 +5040,8 @@ export const REAPER_ACTIONS = {
56035040 "track-properties-toggle-fixed-item-lanes-convert-takes-to-lanes": 42660,
56045041 "track-properties-toggle-show-play-only-one-fixed-item-lane": 42638,
56055042 "track-properties-unset-free-item-positioning-fixed-item-lanes": 40752,
5606 "track-properties-unset-free-item-positioning-fixed-item-lanes-convert-fixed-lanes-to-takes":
5607 42662,
5608 "track-remove-selected-tracks-from-all-track-media-razor-editing-groups":
5609 42579,
5043 "track-properties-unset-free-item-positioning-fixed-item-lanes-convert-fixed-lanes-to-takes": 42662,
5044 "track-remove-selected-tracks-from-all-track-media-razor-editing-groups": 42579,
56105045 "track-remove-track-icon": 40900,
56115046 "track-remove-track-spacers": 42670,
56125047 "track-remove-tracks": 40005,
......@@ -5615,34 +5050,22 @@ export const REAPER_ACTIONS = {
56155050 "track-remove-visual-spacer-before-last-touched-track": 42673,
56165051 "track-remove-visual-spacer-before-tracks": 42667,
56175052 "track-rename-last-touched-track": 40696,
5618 "track-render-selected-area-of-tracks-to-mono-post-fader-stem-tracks-and-mute-originals":
5619 41718,
5620 "track-render-selected-area-of-tracks-to-mono-stem-tracks-and-mute-originals":
5621 41721,
5053 "track-render-selected-area-of-tracks-to-mono-post-fader-stem-tracks-and-mute-originals": 41718,
5054 "track-render-selected-area-of-tracks-to-mono-stem-tracks-and-mute-originals": 41721,
56225055 "track-render-selected-area-of-tracks-to-multichannel-parent-send-only-post-fader-stem-tracks-and-mute-originals":
56235056 42591,
5624 "track-render-selected-area-of-tracks-to-multichannel-parent-send-only-stem-tracks-and-mute-originals":
5625 42592,
5626 "track-render-selected-area-of-tracks-to-multichannel-post-fader-stem-tracks-and-mute-originals":
5627 41717,
5628 "track-render-selected-area-of-tracks-to-multichannel-stem-tracks-and-mute-originals":
5629 41720,
5630 "track-render-selected-area-of-tracks-to-stereo-post-fader-stem-tracks-and-mute-originals":
5631 41716,
5632 "track-render-selected-area-of-tracks-to-stereo-stem-tracks-and-mute-originals":
5633 41719,
5634 "track-render-tracks-to-mono-post-fader-stem-tracks-and-mute-originals":
5635 40537,
5057 "track-render-selected-area-of-tracks-to-multichannel-parent-send-only-stem-tracks-and-mute-originals": 42592,
5058 "track-render-selected-area-of-tracks-to-multichannel-post-fader-stem-tracks-and-mute-originals": 41717,
5059 "track-render-selected-area-of-tracks-to-multichannel-stem-tracks-and-mute-originals": 41720,
5060 "track-render-selected-area-of-tracks-to-stereo-post-fader-stem-tracks-and-mute-originals": 41716,
5061 "track-render-selected-area-of-tracks-to-stereo-stem-tracks-and-mute-originals": 41719,
5062 "track-render-tracks-to-mono-post-fader-stem-tracks-and-mute-originals": 40537,
56365063 "track-render-tracks-to-mono-stem-tracks-and-mute-originals": 40789,
5637 "track-render-tracks-to-multichannel-parent-send-only-post-fader-stem-tracks-and-mute-originals":
5638 42589,
5639 "track-render-tracks-to-multichannel-parent-send-only-stem-tracks-and-mute-originals":
5640 42590,
5641 "track-render-tracks-to-multichannel-post-fader-stem-tracks-and-mute-originals":
5642 40892,
5064 "track-render-tracks-to-multichannel-parent-send-only-post-fader-stem-tracks-and-mute-originals": 42589,
5065 "track-render-tracks-to-multichannel-parent-send-only-stem-tracks-and-mute-originals": 42590,
5066 "track-render-tracks-to-multichannel-post-fader-stem-tracks-and-mute-originals": 40892,
56435067 "track-render-tracks-to-multichannel-stem-tracks-and-mute-originals": 40893,
5644 "track-render-tracks-to-stereo-post-fader-stem-tracks-and-mute-originals":
5645 40405,
5068 "track-render-tracks-to-stereo-post-fader-stem-tracks-and-mute-originals": 40405,
56465069 "track-render-tracks-to-stereo-stem-tracks-and-mute-originals": 40788,
56475070 "track-save-tracks-as-track-template": 40392,
56485071 "track-select-all-top-level-tracks": 41803,
......@@ -6119,210 +5542,108 @@ export const REAPER_ACTIONS = {
61195542 "track-set-solo-for-track-97-midi-cc-osc-only": 795,
61205543 "track-set-solo-for-track-98-midi-cc-osc-only": 803,
61215544 "track-set-solo-for-track-99-midi-cc-osc-only": 811,
6122 "track-set-stereo-width-or-right-channel-pan-for-last-touched-track-midi-cc-osc-only":
6123 30101,
6124 "track-set-stereo-width-or-right-channel-pan-for-master-track-midi-cc-osc-only":
6125 30001,
6126 "track-set-stereo-width-or-right-channel-pan-for-selected-tracks-midi-cc-osc-only":
6127 30000,
6128 "track-set-stereo-width-or-right-channel-pan-for-track-01-midi-cc-osc-only":
6129 30002,
6130 "track-set-stereo-width-or-right-channel-pan-for-track-02-midi-cc-osc-only":
6131 30003,
6132 "track-set-stereo-width-or-right-channel-pan-for-track-03-midi-cc-osc-only":
6133 30004,
6134 "track-set-stereo-width-or-right-channel-pan-for-track-04-midi-cc-osc-only":
6135 30005,
6136 "track-set-stereo-width-or-right-channel-pan-for-track-05-midi-cc-osc-only":
6137 30006,
6138 "track-set-stereo-width-or-right-channel-pan-for-track-06-midi-cc-osc-only":
6139 30007,
6140 "track-set-stereo-width-or-right-channel-pan-for-track-07-midi-cc-osc-only":
6141 30008,
6142 "track-set-stereo-width-or-right-channel-pan-for-track-08-midi-cc-osc-only":
6143 30009,
6144 "track-set-stereo-width-or-right-channel-pan-for-track-09-midi-cc-osc-only":
6145 30010,
6146 "track-set-stereo-width-or-right-channel-pan-for-track-10-midi-cc-osc-only":
6147 30011,
6148 "track-set-stereo-width-or-right-channel-pan-for-track-11-midi-cc-osc-only":
6149 30012,
6150 "track-set-stereo-width-or-right-channel-pan-for-track-12-midi-cc-osc-only":
6151 30013,
6152 "track-set-stereo-width-or-right-channel-pan-for-track-13-midi-cc-osc-only":
6153 30014,
6154 "track-set-stereo-width-or-right-channel-pan-for-track-14-midi-cc-osc-only":
6155 30015,
6156 "track-set-stereo-width-or-right-channel-pan-for-track-15-midi-cc-osc-only":
6157 30016,
6158 "track-set-stereo-width-or-right-channel-pan-for-track-16-midi-cc-osc-only":
6159 30017,
6160 "track-set-stereo-width-or-right-channel-pan-for-track-17-midi-cc-osc-only":
6161 30018,
6162 "track-set-stereo-width-or-right-channel-pan-for-track-18-midi-cc-osc-only":
6163 30019,
6164 "track-set-stereo-width-or-right-channel-pan-for-track-19-midi-cc-osc-only":
6165 30020,
6166 "track-set-stereo-width-or-right-channel-pan-for-track-20-midi-cc-osc-only":
6167 30021,
6168 "track-set-stereo-width-or-right-channel-pan-for-track-21-midi-cc-osc-only":
6169 30022,
6170 "track-set-stereo-width-or-right-channel-pan-for-track-22-midi-cc-osc-only":
6171 30023,
6172 "track-set-stereo-width-or-right-channel-pan-for-track-23-midi-cc-osc-only":
6173 30024,
6174 "track-set-stereo-width-or-right-channel-pan-for-track-24-midi-cc-osc-only":
6175 30025,
6176 "track-set-stereo-width-or-right-channel-pan-for-track-25-midi-cc-osc-only":
6177 30026,
6178 "track-set-stereo-width-or-right-channel-pan-for-track-26-midi-cc-osc-only":
6179 30027,
6180 "track-set-stereo-width-or-right-channel-pan-for-track-27-midi-cc-osc-only":
6181 30028,
6182 "track-set-stereo-width-or-right-channel-pan-for-track-28-midi-cc-osc-only":
6183 30029,
6184 "track-set-stereo-width-or-right-channel-pan-for-track-29-midi-cc-osc-only":
6185 30030,
6186 "track-set-stereo-width-or-right-channel-pan-for-track-30-midi-cc-osc-only":
6187 30031,
6188 "track-set-stereo-width-or-right-channel-pan-for-track-31-midi-cc-osc-only":
6189 30032,
6190 "track-set-stereo-width-or-right-channel-pan-for-track-32-midi-cc-osc-only":
6191 30033,
6192 "track-set-stereo-width-or-right-channel-pan-for-track-33-midi-cc-osc-only":
6193 30034,
6194 "track-set-stereo-width-or-right-channel-pan-for-track-34-midi-cc-osc-only":
6195 30035,
6196 "track-set-stereo-width-or-right-channel-pan-for-track-35-midi-cc-osc-only":
6197 30036,
6198 "track-set-stereo-width-or-right-channel-pan-for-track-36-midi-cc-osc-only":
6199 30037,
6200 "track-set-stereo-width-or-right-channel-pan-for-track-37-midi-cc-osc-only":
6201 30038,
6202 "track-set-stereo-width-or-right-channel-pan-for-track-38-midi-cc-osc-only":
6203 30039,
6204 "track-set-stereo-width-or-right-channel-pan-for-track-39-midi-cc-osc-only":
6205 30040,
6206 "track-set-stereo-width-or-right-channel-pan-for-track-40-midi-cc-osc-only":
6207 30041,
6208 "track-set-stereo-width-or-right-channel-pan-for-track-41-midi-cc-osc-only":
6209 30042,
6210 "track-set-stereo-width-or-right-channel-pan-for-track-42-midi-cc-osc-only":
6211 30043,
6212 "track-set-stereo-width-or-right-channel-pan-for-track-43-midi-cc-osc-only":
6213 30044,
6214 "track-set-stereo-width-or-right-channel-pan-for-track-44-midi-cc-osc-only":
6215 30045,
6216 "track-set-stereo-width-or-right-channel-pan-for-track-45-midi-cc-osc-only":
6217 30046,
6218 "track-set-stereo-width-or-right-channel-pan-for-track-46-midi-cc-osc-only":
6219 30047,
6220 "track-set-stereo-width-or-right-channel-pan-for-track-47-midi-cc-osc-only":
6221 30048,
6222 "track-set-stereo-width-or-right-channel-pan-for-track-48-midi-cc-osc-only":
6223 30049,
6224 "track-set-stereo-width-or-right-channel-pan-for-track-49-midi-cc-osc-only":
6225 30050,
6226 "track-set-stereo-width-or-right-channel-pan-for-track-50-midi-cc-osc-only":
6227 30051,
6228 "track-set-stereo-width-or-right-channel-pan-for-track-51-midi-cc-osc-only":
6229 30052,
6230 "track-set-stereo-width-or-right-channel-pan-for-track-52-midi-cc-osc-only":
6231 30053,
6232 "track-set-stereo-width-or-right-channel-pan-for-track-53-midi-cc-osc-only":
6233 30054,
6234 "track-set-stereo-width-or-right-channel-pan-for-track-54-midi-cc-osc-only":
6235 30055,
6236 "track-set-stereo-width-or-right-channel-pan-for-track-55-midi-cc-osc-only":
6237 30056,
6238 "track-set-stereo-width-or-right-channel-pan-for-track-56-midi-cc-osc-only":
6239 30057,
6240 "track-set-stereo-width-or-right-channel-pan-for-track-57-midi-cc-osc-only":
6241 30058,
6242 "track-set-stereo-width-or-right-channel-pan-for-track-58-midi-cc-osc-only":
6243 30059,
6244 "track-set-stereo-width-or-right-channel-pan-for-track-59-midi-cc-osc-only":
6245 30060,
6246 "track-set-stereo-width-or-right-channel-pan-for-track-60-midi-cc-osc-only":
6247 30061,
6248 "track-set-stereo-width-or-right-channel-pan-for-track-61-midi-cc-osc-only":
6249 30062,
6250 "track-set-stereo-width-or-right-channel-pan-for-track-62-midi-cc-osc-only":
6251 30063,
6252 "track-set-stereo-width-or-right-channel-pan-for-track-63-midi-cc-osc-only":
6253 30064,
6254 "track-set-stereo-width-or-right-channel-pan-for-track-64-midi-cc-osc-only":
6255 30065,
6256 "track-set-stereo-width-or-right-channel-pan-for-track-65-midi-cc-osc-only":
6257 30066,
6258 "track-set-stereo-width-or-right-channel-pan-for-track-66-midi-cc-osc-only":
6259 30067,
6260 "track-set-stereo-width-or-right-channel-pan-for-track-67-midi-cc-osc-only":
6261 30068,
6262 "track-set-stereo-width-or-right-channel-pan-for-track-68-midi-cc-osc-only":
6263 30069,
6264 "track-set-stereo-width-or-right-channel-pan-for-track-69-midi-cc-osc-only":
6265 30070,
6266 "track-set-stereo-width-or-right-channel-pan-for-track-70-midi-cc-osc-only":
6267 30071,
6268 "track-set-stereo-width-or-right-channel-pan-for-track-71-midi-cc-osc-only":
6269 30072,
6270 "track-set-stereo-width-or-right-channel-pan-for-track-72-midi-cc-osc-only":
6271 30073,
6272 "track-set-stereo-width-or-right-channel-pan-for-track-73-midi-cc-osc-only":
6273 30074,
6274 "track-set-stereo-width-or-right-channel-pan-for-track-74-midi-cc-osc-only":
6275 30075,
6276 "track-set-stereo-width-or-right-channel-pan-for-track-75-midi-cc-osc-only":
6277 30076,
6278 "track-set-stereo-width-or-right-channel-pan-for-track-76-midi-cc-osc-only":
6279 30077,
6280 "track-set-stereo-width-or-right-channel-pan-for-track-77-midi-cc-osc-only":
6281 30078,
6282 "track-set-stereo-width-or-right-channel-pan-for-track-78-midi-cc-osc-only":
6283 30079,
6284 "track-set-stereo-width-or-right-channel-pan-for-track-79-midi-cc-osc-only":
6285 30080,
6286 "track-set-stereo-width-or-right-channel-pan-for-track-80-midi-cc-osc-only":
6287 30081,
6288 "track-set-stereo-width-or-right-channel-pan-for-track-81-midi-cc-osc-only":
6289 30082,
6290 "track-set-stereo-width-or-right-channel-pan-for-track-82-midi-cc-osc-only":
6291 30083,
6292 "track-set-stereo-width-or-right-channel-pan-for-track-83-midi-cc-osc-only":
6293 30084,
6294 "track-set-stereo-width-or-right-channel-pan-for-track-84-midi-cc-osc-only":
6295 30085,
6296 "track-set-stereo-width-or-right-channel-pan-for-track-85-midi-cc-osc-only":
6297 30086,
6298 "track-set-stereo-width-or-right-channel-pan-for-track-86-midi-cc-osc-only":
6299 30087,
6300 "track-set-stereo-width-or-right-channel-pan-for-track-87-midi-cc-osc-only":
6301 30088,
6302 "track-set-stereo-width-or-right-channel-pan-for-track-88-midi-cc-osc-only":
6303 30089,
6304 "track-set-stereo-width-or-right-channel-pan-for-track-89-midi-cc-osc-only":
6305 30090,
6306 "track-set-stereo-width-or-right-channel-pan-for-track-90-midi-cc-osc-only":
6307 30091,
6308 "track-set-stereo-width-or-right-channel-pan-for-track-91-midi-cc-osc-only":
6309 30092,
6310 "track-set-stereo-width-or-right-channel-pan-for-track-92-midi-cc-osc-only":
6311 30093,
6312 "track-set-stereo-width-or-right-channel-pan-for-track-93-midi-cc-osc-only":
6313 30094,
6314 "track-set-stereo-width-or-right-channel-pan-for-track-94-midi-cc-osc-only":
6315 30095,
6316 "track-set-stereo-width-or-right-channel-pan-for-track-95-midi-cc-osc-only":
6317 30096,
6318 "track-set-stereo-width-or-right-channel-pan-for-track-96-midi-cc-osc-only":
6319 30097,
6320 "track-set-stereo-width-or-right-channel-pan-for-track-97-midi-cc-osc-only":
6321 30098,
6322 "track-set-stereo-width-or-right-channel-pan-for-track-98-midi-cc-osc-only":
6323 30099,
6324 "track-set-stereo-width-or-right-channel-pan-for-track-99-midi-cc-osc-only":
6325 30100,
5545 "track-set-stereo-width-or-right-channel-pan-for-last-touched-track-midi-cc-osc-only": 30101,
5546 "track-set-stereo-width-or-right-channel-pan-for-master-track-midi-cc-osc-only": 30001,
5547 "track-set-stereo-width-or-right-channel-pan-for-selected-tracks-midi-cc-osc-only": 30000,
5548 "track-set-stereo-width-or-right-channel-pan-for-track-01-midi-cc-osc-only": 30002,
5549 "track-set-stereo-width-or-right-channel-pan-for-track-02-midi-cc-osc-only": 30003,
5550 "track-set-stereo-width-or-right-channel-pan-for-track-03-midi-cc-osc-only": 30004,
5551 "track-set-stereo-width-or-right-channel-pan-for-track-04-midi-cc-osc-only": 30005,
5552 "track-set-stereo-width-or-right-channel-pan-for-track-05-midi-cc-osc-only": 30006,
5553 "track-set-stereo-width-or-right-channel-pan-for-track-06-midi-cc-osc-only": 30007,
5554 "track-set-stereo-width-or-right-channel-pan-for-track-07-midi-cc-osc-only": 30008,
5555 "track-set-stereo-width-or-right-channel-pan-for-track-08-midi-cc-osc-only": 30009,
5556 "track-set-stereo-width-or-right-channel-pan-for-track-09-midi-cc-osc-only": 30010,
5557 "track-set-stereo-width-or-right-channel-pan-for-track-10-midi-cc-osc-only": 30011,
5558 "track-set-stereo-width-or-right-channel-pan-for-track-11-midi-cc-osc-only": 30012,
5559 "track-set-stereo-width-or-right-channel-pan-for-track-12-midi-cc-osc-only": 30013,
5560 "track-set-stereo-width-or-right-channel-pan-for-track-13-midi-cc-osc-only": 30014,
5561 "track-set-stereo-width-or-right-channel-pan-for-track-14-midi-cc-osc-only": 30015,
5562 "track-set-stereo-width-or-right-channel-pan-for-track-15-midi-cc-osc-only": 30016,
5563 "track-set-stereo-width-or-right-channel-pan-for-track-16-midi-cc-osc-only": 30017,
5564 "track-set-stereo-width-or-right-channel-pan-for-track-17-midi-cc-osc-only": 30018,
5565 "track-set-stereo-width-or-right-channel-pan-for-track-18-midi-cc-osc-only": 30019,
5566 "track-set-stereo-width-or-right-channel-pan-for-track-19-midi-cc-osc-only": 30020,
5567 "track-set-stereo-width-or-right-channel-pan-for-track-20-midi-cc-osc-only": 30021,
5568 "track-set-stereo-width-or-right-channel-pan-for-track-21-midi-cc-osc-only": 30022,
5569 "track-set-stereo-width-or-right-channel-pan-for-track-22-midi-cc-osc-only": 30023,
5570 "track-set-stereo-width-or-right-channel-pan-for-track-23-midi-cc-osc-only": 30024,
5571 "track-set-stereo-width-or-right-channel-pan-for-track-24-midi-cc-osc-only": 30025,
5572 "track-set-stereo-width-or-right-channel-pan-for-track-25-midi-cc-osc-only": 30026,
5573 "track-set-stereo-width-or-right-channel-pan-for-track-26-midi-cc-osc-only": 30027,
5574 "track-set-stereo-width-or-right-channel-pan-for-track-27-midi-cc-osc-only": 30028,
5575 "track-set-stereo-width-or-right-channel-pan-for-track-28-midi-cc-osc-only": 30029,
5576 "track-set-stereo-width-or-right-channel-pan-for-track-29-midi-cc-osc-only": 30030,
5577 "track-set-stereo-width-or-right-channel-pan-for-track-30-midi-cc-osc-only": 30031,
5578 "track-set-stereo-width-or-right-channel-pan-for-track-31-midi-cc-osc-only": 30032,
5579 "track-set-stereo-width-or-right-channel-pan-for-track-32-midi-cc-osc-only": 30033,
5580 "track-set-stereo-width-or-right-channel-pan-for-track-33-midi-cc-osc-only": 30034,
5581 "track-set-stereo-width-or-right-channel-pan-for-track-34-midi-cc-osc-only": 30035,
5582 "track-set-stereo-width-or-right-channel-pan-for-track-35-midi-cc-osc-only": 30036,
5583 "track-set-stereo-width-or-right-channel-pan-for-track-36-midi-cc-osc-only": 30037,
5584 "track-set-stereo-width-or-right-channel-pan-for-track-37-midi-cc-osc-only": 30038,
5585 "track-set-stereo-width-or-right-channel-pan-for-track-38-midi-cc-osc-only": 30039,
5586 "track-set-stereo-width-or-right-channel-pan-for-track-39-midi-cc-osc-only": 30040,
5587 "track-set-stereo-width-or-right-channel-pan-for-track-40-midi-cc-osc-only": 30041,
5588 "track-set-stereo-width-or-right-channel-pan-for-track-41-midi-cc-osc-only": 30042,
5589 "track-set-stereo-width-or-right-channel-pan-for-track-42-midi-cc-osc-only": 30043,
5590 "track-set-stereo-width-or-right-channel-pan-for-track-43-midi-cc-osc-only": 30044,
5591 "track-set-stereo-width-or-right-channel-pan-for-track-44-midi-cc-osc-only": 30045,
5592 "track-set-stereo-width-or-right-channel-pan-for-track-45-midi-cc-osc-only": 30046,
5593 "track-set-stereo-width-or-right-channel-pan-for-track-46-midi-cc-osc-only": 30047,
5594 "track-set-stereo-width-or-right-channel-pan-for-track-47-midi-cc-osc-only": 30048,
5595 "track-set-stereo-width-or-right-channel-pan-for-track-48-midi-cc-osc-only": 30049,
5596 "track-set-stereo-width-or-right-channel-pan-for-track-49-midi-cc-osc-only": 30050,
5597 "track-set-stereo-width-or-right-channel-pan-for-track-50-midi-cc-osc-only": 30051,
5598 "track-set-stereo-width-or-right-channel-pan-for-track-51-midi-cc-osc-only": 30052,
5599 "track-set-stereo-width-or-right-channel-pan-for-track-52-midi-cc-osc-only": 30053,
5600 "track-set-stereo-width-or-right-channel-pan-for-track-53-midi-cc-osc-only": 30054,
5601 "track-set-stereo-width-or-right-channel-pan-for-track-54-midi-cc-osc-only": 30055,
5602 "track-set-stereo-width-or-right-channel-pan-for-track-55-midi-cc-osc-only": 30056,
5603 "track-set-stereo-width-or-right-channel-pan-for-track-56-midi-cc-osc-only": 30057,
5604 "track-set-stereo-width-or-right-channel-pan-for-track-57-midi-cc-osc-only": 30058,
5605 "track-set-stereo-width-or-right-channel-pan-for-track-58-midi-cc-osc-only": 30059,
5606 "track-set-stereo-width-or-right-channel-pan-for-track-59-midi-cc-osc-only": 30060,
5607 "track-set-stereo-width-or-right-channel-pan-for-track-60-midi-cc-osc-only": 30061,
5608 "track-set-stereo-width-or-right-channel-pan-for-track-61-midi-cc-osc-only": 30062,
5609 "track-set-stereo-width-or-right-channel-pan-for-track-62-midi-cc-osc-only": 30063,
5610 "track-set-stereo-width-or-right-channel-pan-for-track-63-midi-cc-osc-only": 30064,
5611 "track-set-stereo-width-or-right-channel-pan-for-track-64-midi-cc-osc-only": 30065,
5612 "track-set-stereo-width-or-right-channel-pan-for-track-65-midi-cc-osc-only": 30066,
5613 "track-set-stereo-width-or-right-channel-pan-for-track-66-midi-cc-osc-only": 30067,
5614 "track-set-stereo-width-or-right-channel-pan-for-track-67-midi-cc-osc-only": 30068,
5615 "track-set-stereo-width-or-right-channel-pan-for-track-68-midi-cc-osc-only": 30069,
5616 "track-set-stereo-width-or-right-channel-pan-for-track-69-midi-cc-osc-only": 30070,
5617 "track-set-stereo-width-or-right-channel-pan-for-track-70-midi-cc-osc-only": 30071,
5618 "track-set-stereo-width-or-right-channel-pan-for-track-71-midi-cc-osc-only": 30072,
5619 "track-set-stereo-width-or-right-channel-pan-for-track-72-midi-cc-osc-only": 30073,
5620 "track-set-stereo-width-or-right-channel-pan-for-track-73-midi-cc-osc-only": 30074,
5621 "track-set-stereo-width-or-right-channel-pan-for-track-74-midi-cc-osc-only": 30075,
5622 "track-set-stereo-width-or-right-channel-pan-for-track-75-midi-cc-osc-only": 30076,
5623 "track-set-stereo-width-or-right-channel-pan-for-track-76-midi-cc-osc-only": 30077,
5624 "track-set-stereo-width-or-right-channel-pan-for-track-77-midi-cc-osc-only": 30078,
5625 "track-set-stereo-width-or-right-channel-pan-for-track-78-midi-cc-osc-only": 30079,
5626 "track-set-stereo-width-or-right-channel-pan-for-track-79-midi-cc-osc-only": 30080,
5627 "track-set-stereo-width-or-right-channel-pan-for-track-80-midi-cc-osc-only": 30081,
5628 "track-set-stereo-width-or-right-channel-pan-for-track-81-midi-cc-osc-only": 30082,
5629 "track-set-stereo-width-or-right-channel-pan-for-track-82-midi-cc-osc-only": 30083,
5630 "track-set-stereo-width-or-right-channel-pan-for-track-83-midi-cc-osc-only": 30084,
5631 "track-set-stereo-width-or-right-channel-pan-for-track-84-midi-cc-osc-only": 30085,
5632 "track-set-stereo-width-or-right-channel-pan-for-track-85-midi-cc-osc-only": 30086,
5633 "track-set-stereo-width-or-right-channel-pan-for-track-86-midi-cc-osc-only": 30087,
5634 "track-set-stereo-width-or-right-channel-pan-for-track-87-midi-cc-osc-only": 30088,
5635 "track-set-stereo-width-or-right-channel-pan-for-track-88-midi-cc-osc-only": 30089,
5636 "track-set-stereo-width-or-right-channel-pan-for-track-89-midi-cc-osc-only": 30090,
5637 "track-set-stereo-width-or-right-channel-pan-for-track-90-midi-cc-osc-only": 30091,
5638 "track-set-stereo-width-or-right-channel-pan-for-track-91-midi-cc-osc-only": 30092,
5639 "track-set-stereo-width-or-right-channel-pan-for-track-92-midi-cc-osc-only": 30093,
5640 "track-set-stereo-width-or-right-channel-pan-for-track-93-midi-cc-osc-only": 30094,
5641 "track-set-stereo-width-or-right-channel-pan-for-track-94-midi-cc-osc-only": 30095,
5642 "track-set-stereo-width-or-right-channel-pan-for-track-95-midi-cc-osc-only": 30096,
5643 "track-set-stereo-width-or-right-channel-pan-for-track-96-midi-cc-osc-only": 30097,
5644 "track-set-stereo-width-or-right-channel-pan-for-track-97-midi-cc-osc-only": 30098,
5645 "track-set-stereo-width-or-right-channel-pan-for-track-98-midi-cc-osc-only": 30099,
5646 "track-set-stereo-width-or-right-channel-pan-for-track-99-midi-cc-osc-only": 30100,
63265647 "track-set-to-custom-color": 40357,
63275648 "track-set-to-default-color": 40359,
63285649 "track-set-to-one-random-color": 40360,
......@@ -6455,20 +5776,16 @@ export const REAPER_ACTIONS = {
64555776 "track-show-hide-all-pinned-tracks-in-tcp-ignore-master": 43576,
64565777 "track-show-hide-children-of-selected-folder-tracks-in-mixer": 41665,
64575778 "track-show-hide-children-of-selected-folder-tracks-in-tcp": 42696,
6458 "track-show-only-one-lane-for-all-fixed-lane-tracks-in-the-project-that-have-only-one-lane-playing":
6459 42961,
5779 "track-show-only-one-lane-for-all-fixed-lane-tracks-in-the-project-that-have-only-one-lane-playing": 42961,
64605780 "track-solo-tracks": 40728,
64615781 "track-solo-unsolo-tracks": 40281,
64625782 "track-swap-volume-envelope-and-trim-envelope": 42021,
64635783 "track-toggle-all-track-grouping-enabled": 40771,
64645784 "track-toggle-allow-editing-media-items-while-comping": 42597,
6465 "track-toggle-allow-editing-media-items-while-comping-for-track-at-mouse":
6466 42598,
5785 "track-toggle-allow-editing-media-items-while-comping-for-track-at-mouse": 42598,
64675786 "track-toggle-automatic-record-arm-when-track-selected": 40736,
6468 "track-toggle-automatically-creating-comp-areas-for-new-recording-while-comping":
6469 42675,
6470 "track-toggle-automatically-delete-empty-fixed-lanes-at-bottom-of-track":
6471 42659,
5787 "track-toggle-automatically-creating-comp-areas-for-new-recording-while-comping": 42675,
5788 "track-toggle-automatically-delete-empty-fixed-lanes-at-bottom-of-track": 42659,
64725789 "track-toggle-comping": 42645,
64735790 "track-toggle-comping-for-track-at-mouse": 42646,
64745791 "track-toggle-full-multichannel-metering": 41726,
......@@ -6576,8 +5893,7 @@ export const REAPER_ACTIONS = {
65765893 "track-toggle-fx-bypass-for-track-98": 800,
65775894 "track-toggle-fx-bypass-for-track-99": 808,
65785895 "track-toggle-fx-bypass-on-all-tracks": 40344,
6579 "track-toggle-link-unlink-track-volume-pan-controls-to-midi-volume-pan-on-all-channels":
6580 41556,
5896 "track-toggle-link-unlink-track-volume-pan-controls-to-midi-volume-pan-on-all-channels": 41556,
65815897 "track-toggle-lock-unlock-track-controls": 41314,
65825898 "track-toggle-midi-input-quantize-for-all-tracks": 42034,
65835899 "track-toggle-midi-input-quantize-for-last-touched-track": 42035,
......@@ -6925,14 +6241,10 @@ export const REAPER_ACTIONS = {
69256241 "track-view-input-fx-chain-for-current-last-touched-track": 40844,
69266242 "track-view-routing-and-i-o-for-current-last-touched-track": 40293,
69276243 "track-view-routing-and-i-o-for-master-track": 42235,
6928 "track-view-track-recording-settings-midi-quantize-file-format-path-for-last-touched-track":
6929 40604,
6930 "tracks-copy-items-on-currently-playing-lanes-on-selected-fixed-lane-tracks-to-one-new-track-per-lane":
6931 42694,
6932 "tracks-explode-items-on-selected-fixed-lane-tracks-to-one-new-track-per-lane":
6933 42695,
6934 "tracks-explode-selected-items-on-fixed-lane-tracks-to-one-new-track-per-lane":
6935 42639,
6244 "track-view-track-recording-settings-midi-quantize-file-format-path-for-last-touched-track": 40604,
6245 "tracks-copy-items-on-currently-playing-lanes-on-selected-fixed-lane-tracks-to-one-new-track-per-lane": 42694,
6246 "tracks-explode-items-on-selected-fixed-lane-tracks-to-one-new-track-per-lane": 42695,
6247 "tracks-explode-selected-items-on-fixed-lane-tracks-to-one-new-track-per-lane": 42639,
69366248 "tracks-implode-selected-items-across-tracks-to-one-fixed-lane-track": 42596,
69376249 "transient-detection-sensitivity-adjust-midi-cc-mousewheel-only": 967,
69386250 "transient-detection-sensitivity-decrease": 41537,
......@@ -6995,8 +6307,7 @@ export const REAPER_ACTIONS = {
69956307 "transport-time-unit-samples": 40413,
69966308 "transport-time-unit-seconds": 40412,
69976309 "transport-time-unit-to-ruler": 40379,
6998 "transport-toggle-preserve-pitch-in-audio-items-when-changing-master-playrate":
6999 40671,
6310 "transport-toggle-preserve-pitch-in-audio-items-when-changing-master-playrate": 40671,
70006311 "transport-toggle-repeat": 1068,
70016312 "transport-toggle-stop-playback-at-end-of-loop-if-repeat-is-disabled": 41834,
70026313 "transport-toggle-transport-docked-to-main-window": 40260,
......@@ -7007,8 +6318,7 @@ export const REAPER_ACTIONS = {
70076318 "video-show-hide-video-window": 50125,
70086319 "view-adjust-horizontal-scroll-midi-cc-osc-only-relative-recommended": 997,
70096320 "view-adjust-horizontal-zoom-midi-cc-osc-only": 998,
7010 "view-adjust-selected-track-heights-a-little-bit-midi-cc-relative-mousewheel":
7011 970,
6321 "view-adjust-selected-track-heights-a-little-bit-midi-cc-relative-mousewheel": 970,
70126322 "view-adjust-selected-track-heights-midi-cc-osc-only": 971,
70136323 "view-adjust-selected-track-heights-midi-cc-relative-mousewheel": 972,
70146324 "view-adjust-vertical-scroll-midi-cc-osc-only": 995,
......@@ -7016,10 +6326,8 @@ export const REAPER_ACTIONS = {
70166326 "view-attach-unattach-docker-to-from-main-window": 40313,
70176327 "view-clear-all-peak-indicators": 40527,
70186328 "view-continuous-scrolling-during-playback": 41817,
7019 "view-cycle-track-zoom-between-minimum-default-and-maximum-height-even-if-over-100-percent-of-arrange-view":
7020 42701,
7021 "view-cycle-track-zoom-between-minimum-default-and-maximum-height-limit-to-100-percent-of-arrange-view":
7022 42698,
6329 "view-cycle-track-zoom-between-minimum-default-and-maximum-height-even-if-over-100-percent-of-arrange-view": 42701,
6330 "view-cycle-track-zoom-between-minimum-default-and-maximum-height-limit-to-100-percent-of-arrange-view": 42698,
70236331 "view-decrease-selected-track-heights": 41326,
70246332 "view-decrease-selected-track-heights-a-little-bit": 41328,
70256333 "view-expand-selected-track-height-minimize-others": 40723,
......@@ -7027,8 +6335,7 @@ export const REAPER_ACTIONS = {
70276335 "view-go-to-play-cursor-position": 40150,
70286336 "view-go-to-track-midi-cc-osc-only": 993,
70296337 "view-hide-item-labels": 40708,
7030 "view-if-displayed-toggle-mouse-position-indicator-vertical-line-respects-toolbar-snap-button":
7031 43203,
6338 "view-if-displayed-toggle-mouse-position-indicator-vertical-line-respects-toolbar-snap-button": 43203,
70326339 "view-increase-selected-track-heights": 41325,
70336340 "view-increase-selected-track-heights-a-little-bit": 41327,
70346341 "view-jump-go-to-time-window": 40069,
......@@ -7056,14 +6363,12 @@ export const REAPER_ACTIONS = {
70566363 "view-scroll-vertically-reversed-midi-cc-relative-mousewheel": 978,
70576364 "view-scroll-view-down": 40139,
70586365 "view-scroll-view-horizontally-one-page-midi-cc-relative-mousewheel": 981,
7059 "view-scroll-view-horizontally-one-page-reversed-midi-cc-relative-mousewheel":
7060 975,
6366 "view-scroll-view-horizontally-one-page-reversed-midi-cc-relative-mousewheel": 975,
70616367 "view-scroll-view-left": 40140,
70626368 "view-scroll-view-right": 40141,
70636369 "view-scroll-view-up": 40138,
70646370 "view-scroll-view-vertically-one-page-midi-cc-relative-mousewheel": 976,
7065 "view-scroll-view-vertically-one-page-reversed-midi-cc-relative-mousewheel":
7066 982,
6371 "view-scroll-view-vertically-one-page-reversed-midi-cc-relative-mousewheel": 982,
70676372 "view-secondary-time-unit-for-ruler-absolute-frames": 42365,
70686373 "view-secondary-time-unit-for-ruler-hours-minutes-seconds-frames": 42364,
70696374 "view-secondary-time-unit-for-ruler-minutes-seconds": 42361,
......@@ -7115,26 +6420,22 @@ export const REAPER_ACTIONS = {
71156420 "view-time-unit-for-ruler-seconds": 40368,
71166421 "view-toggle-auto-view-scroll-during-playback": 40036,
71176422 "view-toggle-auto-view-scroll-while-recording": 40262,
7118 "view-toggle-display-mouse-position-indicator-vertical-line-in-arrange-view":
7119 43194,
6423 "view-toggle-display-mouse-position-indicator-vertical-line-in-arrange-view": 43194,
71206424 "view-toggle-displaying-labels-above-within-media-items": 40258,
71216425 "view-toggle-master-track-in-separate-docked-window": 41609,
71226426 "view-toggle-master-track-visible": 40075,
71236427 "view-toggle-mixer-visible": 40078,
71246428 "view-toggle-show-hide-item-labels": 40651,
71256429 "view-toggle-show-hide-media-item-timebase-buttons": 43642,
7126 "view-toggle-show-hide-media-item-timebase-buttons-if-overridden-for-the-track-or-item":
7127 43643,
6430 "view-toggle-show-hide-media-item-timebase-buttons-if-overridden-for-the-track-or-item": 43643,
71286431 "view-toggle-show-media-cues-in-items": 40691,
71296432 "view-toggle-show-midi-editor-windows": 40716,
71306433 "view-toggle-show-tcp-area": 43185,
71316434 "view-toggle-to-alternate-tcp-area-width-alternate-is-zero-by-default": 43188,
71326435 "view-toggle-track-zoom-to-default-height": 42697,
71336436 "view-toggle-track-zoom-to-default-height-ignore-pinned-tracks": 43678,
7134 "view-toggle-track-zoom-to-maximum-height-even-if-over-100-percent-of-arrange-view":
7135 42700,
7136 "view-toggle-track-zoom-to-maximum-height-limit-to-100-percent-of-arrange-view":
7137 40113,
6437 "view-toggle-track-zoom-to-maximum-height-even-if-over-100-percent-of-arrange-view": 42700,
6438 "view-toggle-track-zoom-to-maximum-height-limit-to-100-percent-of-arrange-view": 40113,
71386439 "view-toggle-track-zoom-to-minimum-height": 40110,
71396440 "view-toggle-track-zoom-to-minimum-height-ignore-pinned-tracks": 43677,
71406441 "view-toggle-transport-visible-play-record-stop": 40259,
......@@ -7149,10 +6450,8 @@ export const REAPER_ACTIONS = {
71496450 "view-zoom-time-selection": 40031,
71506451 "view-zoom-vertically-midi-cc-relative-mousewheel": 1000,
71516452 "view-zoom-vertically-reversed-midi-cc-relative-mousewheel": 1001,
7152 "view-zoom-vertically-reversed-snap-to-theme-defined-sizes-midi-cc-relative-mousewheel":
7153 980,
7154 "view-zoom-vertically-snap-to-theme-defined-sizes-midi-cc-relative-mousewheel":
7155 991,
6453 "view-zoom-vertically-reversed-snap-to-theme-defined-sizes-midi-cc-relative-mousewheel": 980,
6454 "view-zoom-vertically-snap-to-theme-defined-sizes-midi-cc-relative-mousewheel": 991,
71566455 "virtual-midi-keyboard-send-all-input-to-vkb": 40637,
71576456 "xenakios-sws-apply-track-fx-to-items-and-reset-volume": 53300,
71586457 "xenakios-sws-apply-track-fx-to-items-mono-and-reset-volume": 53301,
......@@ -7161,17 +6460,12 @@ export const REAPER_ACTIONS = {
71616460 "xenakios-sws-choose-files-for-random-insert": 53254,
71626461 "xenakios-sws-choose-new-source-file-for-selected-takes": 53273,
71636462 "xenakios-sws-command-parameters": 53491,
7164 "xenakios-sws-create-markers-from-selected-items-name-by-take-source-file-name":
7165 53452,
6463 "xenakios-sws-create-markers-from-selected-items-name-by-take-source-file-name": 53452,
71666464 "xenakios-sws-deprecated-create-new-tracks": 53441,
7167 "xenakios-sws-deprecated-delete-active-take-of-item-and-send-source-media-to-recycle-bin":
7168 53380,
7169 "xenakios-sws-deprecated-delete-active-take-of-item-and-take-source-media-immediately":
7170 53379,
7171 "xenakios-sws-deprecated-delete-selected-item-and-send-active-take-s-source-media-to-recycle-bin":
7172 53378,
7173 "xenakios-sws-deprecated-delete-selected-item-and-source-media-immediately":
7174 53377,
6465 "xenakios-sws-deprecated-delete-active-take-of-item-and-send-source-media-to-recycle-bin": 53380,
6466 "xenakios-sws-deprecated-delete-active-take-of-item-and-take-source-media-immediately": 53379,
6467 "xenakios-sws-deprecated-delete-selected-item-and-send-active-take-s-source-media-to-recycle-bin": 53378,
6468 "xenakios-sws-deprecated-delete-selected-item-and-source-media-immediately": 53377,
71756469 "xenakios-sws-deprecated-load-project-template-01": 53459,
71766470 "xenakios-sws-deprecated-load-project-template-02": 53460,
71776471 "xenakios-sws-deprecated-load-project-template-03": 53461,
......@@ -7220,24 +6514,20 @@ export const REAPER_ACTIONS = {
72206514 "xenakios-sws-launch-external-tool-2": 53345,
72216515 "xenakios-sws-loop-and-play-selected-items": 53347,
72226516 "xenakios-sws-maximize-selected-tracks-fx-panel-height-in-mixer": 53342,
7223 "xenakios-sws-minimize-selected-tracks-send-and-fx-panel-height-in-mixer":
7224 53341,
6517 "xenakios-sws-minimize-selected-tracks-send-and-fx-panel-height-in-mixer": 53341,
72256518 "xenakios-sws-minimize-selected-tracks-send-panel-height-in-mixer": 53340,
72266519 "xenakios-sws-move-cursor-left-10-pixels": 53358,
72276520 "xenakios-sws-move-cursor-left-10-pixels-creating-time-selection": 53359,
72286521 "xenakios-sws-move-cursor-left-configured-pixels": 53369,
7229 "xenakios-sws-move-cursor-left-configured-pixels-creating-time-selection":
7230 53371,
6522 "xenakios-sws-move-cursor-left-configured-pixels-creating-time-selection": 53371,
72316523 "xenakios-sws-move-cursor-left-configured-seconds": 53373,
72326524 "xenakios-sws-move-cursor-right-10-pixels": 53357,
72336525 "xenakios-sws-move-cursor-right-10-pixels-creating-time-selection": 53360,
72346526 "xenakios-sws-move-cursor-right-configured-pixels": 53368,
7235 "xenakios-sws-move-cursor-right-configured-pixels-creating-time-selection":
7236 53370,
6527 "xenakios-sws-move-cursor-right-configured-pixels-creating-time-selection": 53370,
72376528 "xenakios-sws-move-cursor-right-configured-seconds": 53374,
72386529 "xenakios-sws-move-cursor-to-next-transient-minus-default-fade-time": 53302,
7239 "xenakios-sws-move-cursor-to-previous-transient-minus-default-fade-time":
7240 53303,
6530 "xenakios-sws-move-cursor-to-previous-transient-minus-default-fade-time": 53303,
72416531 "xenakios-sws-move-edit-cursor-32nd-note-left": 53486,
72426532 "xenakios-sws-move-edit-cursor-32nd-note-right": 53487,
72436533 "xenakios-sws-move-edit-cursor-64th-note-left": 53488,
......@@ -7276,8 +6566,7 @@ export const REAPER_ACTIONS = {
72766566 "xenakios-sws-open-associated-reaper-project-of-item": 53351,
72776567 "xenakios-sws-open-audio-take-in-external-editor-3": 53420,
72786568 "xenakios-sws-open-audio-take-in-external-editor-4": 53421,
7279 "xenakios-sws-open-reaper-project-in-item-bwav-info-autosearch-for-rpp-if-necessary":
7280 53404,
6569 "xenakios-sws-open-reaper-project-in-item-bwav-info-autosearch-for-rpp-if-necessary": 53404,
72816570 "xenakios-sws-pan-selected-tracks-randomly": 53308,
72826571 "xenakios-sws-pan-selected-tracks-symmetrically-left-to-right": 53306,
72836572 "xenakios-sws-pan-selected-tracks-symmetrically-right-to-left": 53307,
......@@ -7288,8 +6577,7 @@ export const REAPER_ACTIONS = {
72886577 "xenakios-sws-play-selected-items-once": 53348,
72896578 "xenakios-sws-preview-selected-media-item": 53362,
72906579 "xenakios-sws-preview-selected-media-item-at-track-fader-volume": 53364,
7291 "xenakios-sws-preview-selected-media-item-at-track-fader-volume-toggle":
7292 53365,
6580 "xenakios-sws-preview-selected-media-item-at-track-fader-volume-toggle": 53365,
72936581 "xenakios-sws-preview-selected-media-item-through-track": 53366,
72946582 "xenakios-sws-preview-selected-media-item-through-track-toggle": 53367,
72956583 "xenakios-sws-preview-selected-media-item-toggle": 53363,
......@@ -7369,10 +6657,8 @@ export const REAPER_ACTIONS = {
73696657 "xenakios-sws-set-volume-and-pan-of-selected-takes": 53291,
73706658 "xenakios-sws-set-volume-of-selected-items": 53292,
73716659 "xenakios-sws-set-volume-of-selected-tracks-to-0-0-db": 53305,
7372 "xenakios-sws-shift-all-points-in-selected-envelope-to-left-by-1-second":
7373 53479,
7374 "xenakios-sws-shift-all-points-in-selected-envelope-to-right-by-1-second":
7375 53478,
6660 "xenakios-sws-shift-all-points-in-selected-envelope-to-left-by-1-second": 53479,
6661 "xenakios-sws-shift-all-points-in-selected-envelope-to-right-by-1-second": 53478,
73766662 "xenakios-sws-show-hide-floating-item-track-info": 53390,
73776663 "xenakios-sws-shuffle-order-of-selected-items": 53451,
73786664 "xenakios-sws-shuffle-order-of-selected-items-keep-relative-positions": 53450,
......@@ -7391,8 +6677,7 @@ export const REAPER_ACTIONS = {
73916677 "xenakios-sws-switch-item-contents-to-next-cue": 53240,
73926678 "xenakios-sws-switch-item-contents-to-next-cue-preserve-item-length": 53241,
73936679 "xenakios-sws-switch-item-contents-to-previous-cue": 53242,
7394 "xenakios-sws-switch-item-contents-to-previous-cue-preserve-item-length":
7395 53243,
6680 "xenakios-sws-switch-item-contents-to-previous-cue-preserve-item-length": 53243,
73966681 "xenakios-sws-switch-item-contents-to-random-cue": 53245,
73976682 "xenakios-sws-switch-item-contents-to-random-cue-preserve-item-length": 53246,
73986683 "xenakios-sws-switch-item-source-file-to-next-in-folder": 53444,
src/Reaper/generate-actions.ts+6-15
......@@ -1,12 +1,5 @@
11import { execFile } from "node:child_process";
2import {
3 access,
4 mkdir,
5 mkdtemp,
6 readFile,
7 rm,
8 writeFile,
9} from "node:fs/promises";
2import { access, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
103import { tmpdir } from "node:os";
114import { dirname, join, resolve } from "node:path";
125import process from "node:process";
......@@ -78,10 +71,10 @@ function parseRawActions(value: unknown): RawReaperAction[] {
7871
7972 return value.flatMap((entry) => {
8073 if (
81 !entry ||
82 typeof entry !== "object" ||
83 !("commandId" in entry) ||
84 !("name" in entry)
74 !entry
75 || typeof entry !== "object"
76 || !("commandId" in entry)
77 || !("name" in entry)
8578 ) {
8679 return [];
8780 }
......@@ -149,9 +142,7 @@ function renderActionsFile(actions: ReadonlyArray<GeneratedReaperAction>) {
149142 "// Source: REAPER main action section via kbd_enumerateActions()/kbd_getTextFromCmd().",
150143 "",
151144 "export const REAPER_ACTIONS = {",
152 ...actions.map((action) =>
153 ` ${JSON.stringify(action.actionId)}: ${action.commandId},`
154 ),
145 ...actions.map((action) => ` ${JSON.stringify(action.actionId)}: ${action.commandId},`),
155146 "} as const satisfies Record<string, number>;",
156147 "",
157148 "export type ReaperActionId = keyof typeof REAPER_ACTIONS;",
src/Recorder/build.sh+8
......@@ -23,6 +23,14 @@ rm -rf "$APP"
2323mkdir -p "$APP/Contents/MacOS"
2424cp "$BIN" "$APP/Contents/MacOS/recorder"
2525
26echo "==> compiling uvc-powerline helper"
27clang -O2 -o "$APP/Contents/MacOS/uvc-powerline" "$HERE/uvc/uvc-powerline.c" \
28 -framework IOKit -framework CoreFoundation
29
30echo "==> bundling dictation scripts"
31mkdir -p "$APP/Contents/Resources"
32cp "$HERE"/dictation/*.py "$APP/Contents/Resources/"
33
2634cat > "$APP/Contents/Info.plist" <<EOF
2735<?xml version="1.0" encoding="UTF-8"?>
2836<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
src/Recorder/dictation/diarize.py created+37
......@@ -0,0 +1,37 @@
1#!/usr/bin/env python3
2"""Speaker diarization (pyannote) — prints turn boundaries as JSON.
3
4Runs in the dedicated ~/.clover-diarize venv (pinned deps). session_transcript
5calls this to get precise "who spoke when" boundaries; the main venv then names
6the speakers (ECAPA) and splits the forced-aligned words at the turn edges.
7
8 python diarize.py <audio> -> [{"start":..,"end":..,"speaker":"SPEAKER_00"}, ...]
9"""
10import json
11import subprocess
12import sys
13import warnings
14
15warnings.filterwarnings("ignore")
16
17import numpy as np
18import torch
19from pyannote.audio import Pipeline
20
21audio = sys.argv[1]
22pipe = Pipeline.from_pretrained("pyannote/speaker-diarization-3.1")
23if torch.backends.mps.is_available():
24 pipe.to(torch.device("mps"))
25
26raw = subprocess.run(
27 ["ffmpeg", "-nostdin", "-i", audio, "-f", "f32le", "-ac", "1", "-ar", "16000", "-"],
28 capture_output=True,
29).stdout
30wav = torch.from_numpy(np.frombuffer(raw, np.float32).copy()).unsqueeze(0)
31
32dia = pipe({"waveform": wav, "sample_rate": 16000})
33turns = [
34 {"start": float(t.start), "end": float(t.end), "speaker": spk}
35 for t, _, spk in dia.itertracks(yield_label=True)
36]
37print(json.dumps(turns))
src/Recorder/dictation/enroll.py created+15
......@@ -0,0 +1,15 @@
1#!/usr/bin/env python3
2"""Enroll a named voice into the library from a recording.
3
4 python enroll.py <audio> [name] # default name: You
5"""
6import os
7import sys
8
9sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
10from speaker_id import embed_file, upsert_voice
11
12audio = sys.argv[1]
13name = sys.argv[2] if len(sys.argv) > 2 else "You"
14upsert_voice(name, embed_file(audio))
15print(f"enrolled {name}")
src/Recorder/dictation/forced_align.py created+70
......@@ -0,0 +1,70 @@
1"""Word-level forced alignment (torchaudio MMS_FA, the modern wav2vec2-CTC
2approach WhisperX popularized) — precise per-word start/end times, no token.
3
4Aligned per Whisper segment (small, fast) and offset back to absolute time.
5"""
6import re
7import warnings
8
9warnings.filterwarnings("ignore")
10
11import numpy as np
12import torch
13from torchaudio.pipelines import MMS_FA as B
14
15_model = _tok = _aligner = None
16
17
18def _load():
19 global _model, _tok, _aligner
20 if _model is None:
21 _model, _tok, _aligner = B.get_model(), B.get_tokenizer(), B.get_aligner()
22 return _model, _tok, _aligner
23
24
25def _norm(word):
26 return re.sub(r"[^a-z']", "", word.lower())
27
28
29def align_segment(data, sr, start, end, text):
30 """Return [{word, start, end}] for the words in this segment, in absolute
31 seconds. Words that can't be aligned (pure numbers/symbols) get times
32 interpolated from their neighbours."""
33 a, b = int(start * sr), int(end * sr)
34 seg = data[a:b]
35 raw = text.split()
36 if len(seg) < int(0.2 * sr) or not raw:
37 return [{"word": w, "start": start, "end": end} for w in raw]
38
39 norm = [_norm(w) for w in raw]
40 idx = [i for i, n in enumerate(norm) if n]
41 if not idx:
42 return [{"word": w, "start": start, "end": end} for w in raw]
43
44 model, tok, aligner = _load()
45 wav = torch.from_numpy(np.ascontiguousarray(seg)).unsqueeze(0)
46 with torch.inference_mode():
47 emit, _ = model(wav)
48 try:
49 spans = aligner(emit[0], tok([norm[i] for i in idx]))
50 except Exception:
51 return [{"word": w, "start": start, "end": end} for w in raw]
52
53 ratio = wav.shape[1] / emit.shape[1] / sr
54 times = {}
55 for k, i in enumerate(idx):
56 s = spans[k]
57 times[i] = (round(start + s[0].start * ratio, 3), round(start + s[-1].end * ratio, 3))
58
59 out = [{"word": w, "start": None, "end": None} for w in raw]
60 for i, t in times.items():
61 out[i]["start"], out[i]["end"] = t
62 # Interpolate unaligned words from neighbours.
63 last_end = start
64 for i, o in enumerate(out):
65 if o["start"] is None:
66 o["start"] = last_end
67 nxt = next((out[j]["start"] for j in range(i + 1, len(out)) if out[j]["start"]), end)
68 o["end"] = nxt
69 last_end = o["end"]
70 return out
src/Recorder/dictation/relabel.py created+49
......@@ -0,0 +1,49 @@
1#!/usr/bin/env python3
2"""Apply speaker names to a session: add newly-named voices to the library and
3re-render transcript.md — no re-transcription.
4
5 python relabel.py <session_dir> <mapping.json>
6
7mapping.json maps the auto-labels to names, e.g. {"Speaker 2": "Maya"}.
8"""
9import json
10import os
11import sys
12
13sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
14import speaker_id as sid
15from transcript_render import render
16
17session_dir, mapping_path = sys.argv[1], sys.argv[2]
18mapping = json.load(open(mapping_path))
19
20transcript = json.load(open(os.path.join(session_dir, "transcript.json")))
21spk_path = os.path.join(session_dir, "speakers.json")
22speakers = json.load(open(spk_path)) if os.path.exists(spk_path) else {"speakers": []}
23
24# Teach the library each newly-named unknown voice.
25for sp in speakers.get("speakers", []):
26 new = mapping.get(sp["label"])
27 if new and new != sp["label"] and sp.get("unknown") and sp.get("centroid"):
28 sid.upsert_voice(new, sp["centroid"])
29
30# Re-map labels and re-render.
31markers = []
32mp = os.path.join(session_dir, "markers.json")
33if os.path.exists(mp):
34 for mk in json.load(open(mp)).get("markers", []):
35 markers.append((float(mk.get("offsetSeconds", 0)), mk.get("text")))
36markers.sort(key=lambda x: x[0])
37
38segments = transcript["segments"]
39for s in segments:
40 s["label"] = mapping.get(s.get("label"), s.get("label"))
41render(transcript.get("title", "Session"), segments,
42 [s.get("label") for s in segments], markers,
43 os.path.join(session_dir, "transcript.md"))
44
45json.dump(transcript, open(os.path.join(session_dir, "transcript.json"), "w"))
46for sp in speakers.get("speakers", []):
47 sp["label"] = mapping.get(sp["label"], sp["label"])
48json.dump(speakers, open(spk_path, "w"))
49print("relabeled")
src/Recorder/dictation/session_transcript.py created+235
......@@ -0,0 +1,235 @@
1#!/usr/bin/env python3
2"""Transcribe a session's mic audio into a Markdown transcript with speakers.
3
4 python session_transcript.py <audio> <markers.json|none> <out.md> <title>
5
6Each segment is matched against the enrolled voice library (You + named guests);
7unmatched voices are clustered into distinct Speaker 2/3/… Writes:
8 - <out.md> meeting-minutes transcript, paragraphs labelled by speaker
9 - transcript.json segments + labels (for fast re-labelling after naming)
10 - speakers.json detected speakers + a sample clip + centroid (for the UI)
11"""
12import json
13import os
14import subprocess
15import sys
16
17import numpy as np
18import mlx_whisper
19
20sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
21import speaker_id as sid
22from transcript_render import PAUSE_SPLIT, render
23
24DIARIZE_PY = os.path.expanduser("~/.clover-diarize/.venv/bin/python")
25
26
27def assign_speakers(segments, data, sr):
28 """Cluster segments into voices, then match each *cluster* to the library —
29 far more robust than per-segment matching, so one person stays one speaker.
30
31 Returns (labels, speakers): labels[i] is the speaker for segment i; speakers
32 is per-cluster metadata for the review UI."""
33 from scipy.cluster.hierarchy import fcluster, linkage
34
35 library = [(v["name"], np.asarray(v["centroid"], dtype=np.float32)) for v in sid.load_library()]
36 embs = [sid.embed_segment(data, sr, float(s["start"]), float(s["end"])) for s in segments]
37 valid = [i for i, e in enumerate(embs) if e is not None]
38 if not valid:
39 return [None] * len(segments), []
40
41 # Agglomerative clustering on cosine distance (average linkage).
42 if len(valid) == 1:
43 cids = [1]
44 else:
45 Z = linkage(np.stack([embs[i] for i in valid]), method="average", metric="cosine")
46 cids = fcluster(Z, t=sid.CLUSTER_DIST, criterion="distance")
47 # Cap runaway fragmentation: if acoustic distance alone invents too many
48 # voices, collapse to at most MAX_SPEAKERS by cutting the tree higher.
49 if len(set(cids)) > sid.MAX_SPEAKERS:
50 cids = fcluster(Z, t=sid.MAX_SPEAKERS, criterion="maxclust")
51
52 members = {}
53 for pos, cid in enumerate(cids):
54 members.setdefault(int(cid), []).append(valid[pos])
55
56 # Label each cluster: known name if its centroid matches the library, else
57 # Speaker N (numbered by first appearance).
58 cluster_label, cluster_centroid, unknown_n = {}, {}, 2
59 for cid, idxs in sorted(members.items(), key=lambda kv: min(kv[1])):
60 cen = np.mean([embs[i] for i in idxs], axis=0)
61 cen /= np.linalg.norm(cen)
62 cluster_centroid[cid] = cen
63 name, sim = None, -1.0
64 for nm, c in library:
65 d = float(cen @ c)
66 if d > sim:
67 sim, name = d, nm
68 if sim >= sid.MATCH_THRESHOLD:
69 cluster_label[cid] = name
70 else:
71 cluster_label[cid] = f"Speaker {unknown_n}"
72 unknown_n += 1
73
74 labels = [None] * len(segments)
75 for cid, idxs in members.items():
76 for i in idxs:
77 labels[i] = cluster_label[cid]
78 prev = None # short (un-embedded) segments inherit the previous speaker
79 for i in range(len(labels)):
80 if labels[i] is None:
81 labels[i] = prev
82 else:
83 prev = labels[i]
84
85 speakers = []
86 for cid, idxs in sorted(members.items(), key=lambda kv: min(kv[1])):
87 lab = cluster_label[cid]
88 unknown = lab.startswith("Speaker ")
89 s = segments[idxs[0]]
90 speakers.append({
91 "label": lab, "unknown": unknown,
92 "sample": {"start": s["start"], "end": s["end"]},
93 "centroid": cluster_centroid[cid].tolist() if unknown else None,
94 })
95 return labels, speakers
96
97
98def run_diarization(audio):
99 """Precise speaker turns via the pyannote venv, or None if unavailable."""
100 if not os.path.exists(DIARIZE_PY):
101 return None
102 script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "diarize.py")
103 try:
104 out = subprocess.run([DIARIZE_PY, script, audio], capture_output=True, text=True, timeout=3600)
105 turns = json.loads(out.stdout.strip().splitlines()[-1])
106 return turns or None
107 except Exception:
108 return None
109
110
111def name_speakers(turns, data, sr):
112 """Name each diarized speaker against the voice library (ECAPA)."""
113 library = [(v["name"], np.asarray(v["centroid"], dtype=np.float32)) for v in sid.load_library()]
114 by_spk, sample = {}, {}
115 for tr in turns:
116 e = sid.embed_segment(data, sr, tr["start"], tr["end"])
117 if e is not None:
118 by_spk.setdefault(tr["speaker"], []).append(e)
119 sample.setdefault(tr["speaker"], tr)
120
121 names, meta, unknown_n = {}, [], 2
122 for spk in sorted(sample, key=lambda s: sample[s]["start"]):
123 cen = None
124 if by_spk.get(spk):
125 cen = np.mean(by_spk[spk], axis=0)
126 cen /= np.linalg.norm(cen)
127 name, sim = None, -1.0
128 if cen is not None:
129 for nm, c in library:
130 d = float(cen @ c)
131 if d > sim:
132 sim, name = d, nm
133 if cen is not None and sim >= sid.MATCH_THRESHOLD:
134 names[spk], unknown = name, False
135 else:
136 names[spk], unknown = f"Speaker {unknown_n}", True
137 unknown_n += 1
138 meta.append({
139 "label": names[spk], "unknown": unknown,
140 "sample": {"start": sample[spk]["start"], "end": sample[spk]["end"]},
141 "centroid": cen.tolist() if (unknown and cen is not None) else None,
142 })
143 return names, meta
144
145
146def _speaker_at(turns, t):
147 for tr in turns:
148 if tr["start"] <= t < tr["end"]:
149 return tr["speaker"]
150 return min(turns, key=lambda tr: min(abs(tr["start"] - t), abs(tr["end"] - t)))["speaker"]
151
152
153def build_from_diarization(segments, turns, data, sr):
154 """Split forced-aligned words at diarization boundaries (precise) and label
155 each by the named speaker. Returns (final_segments, labels, speakers)."""
156 names, meta = name_speakers(turns, data, sr)
157 words = [dict(w) for s in segments for w in s.get("words", [])]
158 if not words:
159 return None
160
161 final = []
162 for w in words:
163 name = names.get(_speaker_at(turns, (float(w["start"]) + float(w["end"])) / 2))
164 if (final and final[-1]["label"] == name
165 and float(w["start"]) - final[-1]["end"] <= PAUSE_SPLIT):
166 final[-1]["end"] = float(w["end"])
167 final[-1]["text"] += " " + w["word"]
168 final[-1]["words"].append(w)
169 else:
170 final.append({
171 "start": float(w["start"]), "end": float(w["end"]),
172 "text": w["word"], "label": name, "words": [w],
173 })
174 return final, [s["label"] for s in final], meta
175
176
177def main():
178 audio, markers_path, out_path, title = sys.argv[1:5]
179 # "solo" (default): one speaker, no diarization — right for journaling and
180 # improv, where clustering just shatters your voice into fake speakers.
181 # "multi": diarize + name, for the occasional session with other people.
182 mode = sys.argv[5] if len(sys.argv) > 5 else "solo"
183 folder = os.path.dirname(out_path)
184
185 segments = mlx_whisper.transcribe(
186 audio, path_or_hf_repo="mlx-community/whisper-large-v3-turbo"
187 ).get("segments", [])
188 segments = [{"start": s["start"], "end": s["end"], "text": s["text"]} for s in segments]
189
190 data, sr = sid.load_audio(audio)
191 try:
192 import forced_align as fa
193
194 for s in segments:
195 s["words"] = fa.align_segment(data, sr, float(s["start"]), float(s["end"]), s["text"])
196 except Exception:
197 for s in segments:
198 s["words"] = []
199
200 if mode != "multi":
201 # Solo: everyone is "You". render() hides the single label, and the
202 # non-unknown speaker keeps the Speakers-review popup from firing.
203 labels = ["You"] * len(segments)
204 final_segments = [dict(s, label="You") for s in segments]
205 speakers = [{"label": "You", "unknown": False,
206 "sample": {"start": 0.0, "end": 0.0}, "centroid": None}]
207 else:
208 # Precise mode (pyannote) when available, else cluster-then-match fallback.
209 diarized = None
210 turns = run_diarization(audio)
211 if turns:
212 diarized = build_from_diarization(segments, turns, data, sr)
213 if diarized:
214 final_segments, labels, speakers = diarized
215 else:
216 try:
217 labels, speakers = assign_speakers(segments, data, sr)
218 except Exception:
219 labels, speakers = [None] * len(segments), []
220 final_segments = [dict(s, label=l) for s, l in zip(segments, labels)]
221
222 markers = []
223 if markers_path and markers_path != "none" and os.path.exists(markers_path):
224 for mk in json.load(open(markers_path)).get("markers", []):
225 markers.append((float(mk.get("offsetSeconds", 0)), mk.get("text")))
226 markers.sort(key=lambda x: x[0])
227
228 render(title, final_segments, labels, markers, out_path)
229 json.dump({"title": title, "segments": final_segments},
230 open(os.path.join(folder, "transcript.json"), "w"))
231 json.dump({"speakers": speakers}, open(os.path.join(folder, "speakers.json"), "w"))
232
233
234if __name__ == "__main__":
235 main()
src/Recorder/dictation/speaker_id.py created+114
......@@ -0,0 +1,114 @@
1"""Speaker embeddings via SpeechBrain ECAPA-TDNN (no HF token needed).
2
3Used to tell the user's voice from others in a session: enroll a voiceprint
4once, then score each transcript segment against it by cosine similarity.
5"""
6import json
7import os
8import subprocess
9import warnings
10
11warnings.filterwarnings("ignore")
12
13import numpy as np
14import torch
15import torchaudio
16
17LIBRARY = os.path.expanduser("~/.clover-whisper/voices.json")
18# Cosine above MATCH_THRESHOLD => a cluster is that library voice. CLUSTER_DIST
19# is the cosine *distance* below which segments merge into one speaker (so your
20# own voice stays a single cluster instead of fragmenting). Tunable.
21MATCH_THRESHOLD = 0.45
22CLUSTER_DIST = 0.55
23# Hard cap on distinct voices the fallback clustering may invent. Without it,
24# short/noisy segments and (in improv) character voices fragment one person into
25# dozens of "speakers". Real sessions here have a handful of people at most.
26MAX_SPEAKERS = 6
27
28_model = None
29
30
31def load_library():
32 if os.path.exists(LIBRARY):
33 return json.load(open(LIBRARY)).get("voices", [])
34 return []
35
36
37def save_library(voices):
38 os.makedirs(os.path.dirname(LIBRARY), exist_ok=True)
39 json.dump({"voices": voices}, open(LIBRARY, "w"))
40
41
42def upsert_voice(name, centroid, count=1):
43 """Add a named voice, or blend into an existing one (running average)."""
44 centroid = np.asarray(centroid, dtype=np.float32)
45 voices = load_library()
46 for v in voices:
47 if v["name"] == name:
48 c, n = np.asarray(v["centroid"], dtype=np.float32), v.get("count", 1)
49 blended = (c * n + centroid * count) / (n + count)
50 blended /= np.linalg.norm(blended)
51 v["centroid"], v["count"] = blended.tolist(), n + count
52 save_library(voices)
53 return
54 voices.append({"name": name, "centroid": centroid.tolist(), "count": count})
55 save_library(voices)
56
57
58def model():
59 global _model
60 if _model is None:
61 from speechbrain.inference.speaker import EncoderClassifier
62
63 _model = EncoderClassifier.from_hparams(
64 source="speechbrain/spkrec-ecapa-voxceleb", run_opts={"device": "cpu"}
65 )
66 return _model
67
68
69def _ffmpeg():
70 user = os.environ.get("USER", "")
71 for c in (
72 f"/etc/profiles/per-user/{user}/bin/ffmpeg",
73 "/run/current-system/sw/bin/ffmpeg",
74 "/opt/homebrew/bin/ffmpeg",
75 "/usr/local/bin/ffmpeg",
76 ):
77 if os.path.exists(c):
78 return c
79 return "ffmpeg"
80
81
82def load_audio(path, target_sr=16000):
83 """Decode any format (m4a/wav/aiff/…) to mono float32 via ffmpeg."""
84 out = subprocess.run(
85 [_ffmpeg(), "-nostdin", "-i", path, "-f", "f32le", "-ac", "1", "-ar", str(target_sr), "-"],
86 capture_output=True,
87 ).stdout
88 return np.frombuffer(out, dtype=np.float32).copy(), target_sr
89
90
91def embed_array(data, sr):
92 sig = torch.from_numpy(np.ascontiguousarray(data)).unsqueeze(0)
93 if sr != 16000:
94 sig = torchaudio.functional.resample(sig, sr, 16000)
95 e = model().encode_batch(sig).squeeze()
96 e = e / e.norm()
97 return e.detach().cpu().numpy()
98
99
100def embed_file(path):
101 data, sr = load_audio(path)
102 return embed_array(data, sr)
103
104
105def embed_segment(data, sr, start, end):
106 a, b = int(start * sr), int(end * sr)
107 seg = data[a:b]
108 if len(seg) < int(0.8 * sr): # too short to be reliable
109 return None
110 return embed_array(seg, sr)
111
112
113def cosine(a, b):
114 return float(np.dot(a, b))
src/Recorder/dictation/transcribe.py created+20
......@@ -0,0 +1,20 @@
1#!/usr/bin/env python3
2"""Transcribe an audio file with MLX Whisper (large-v3-turbo) and print the text.
3
4Used by Clover Recorder's marker overlay for voice notes. Runs against the venv
5created by setup-dictation.sh (~/.clover-whisper/.venv).
6
7 python transcribe.py <audio-file>
8"""
9import sys
10
11import mlx_whisper
12
13if len(sys.argv) < 2:
14 sys.exit("usage: transcribe.py <audio-file>")
15
16result = mlx_whisper.transcribe(
17 sys.argv[1],
18 path_or_hf_repo="mlx-community/whisper-large-v3-turbo",
19)
20print(result["text"].strip())
src/Recorder/dictation/transcript_render.py created+47
......@@ -0,0 +1,47 @@
1"""Render a Markdown transcript from segments + speaker labels + markers.
2
3Shared by session_transcript.py (first pass) and relabel.py (after naming).
4"""
5PAUSE_SPLIT = 2.0 # seconds of silence that starts a new paragraph
6
7
8def fmt(seconds):
9 s = int(seconds)
10 h, m, sec = s // 3600, (s % 3600) // 60, s % 60
11 return f"{h}:{m:02d}:{sec:02d}" if h else f"{m:02d}:{sec:02d}"
12
13
14def render(title, segments, labels, markers, out_path):
15 lines = [f"# {title}", ""]
16 para, p_start, p_spk, prev_end, mi = [], 0.0, None, 0.0, 0
17 # Only show speaker names when there's actually more than one speaker.
18 show_speakers = len({l for l in labels if l}) > 1
19
20 def flush():
21 if para:
22 who = f"{p_spk} · " if (show_speakers and p_spk) else ""
23 lines.append(f"**{who}[{fmt(p_start)}]** " + " ".join(para).strip())
24 lines.append("")
25 para.clear()
26
27 def emit_marker(t, text):
28 flush()
29 lines.append(f"**{text.strip()}**" if text and text.strip() else f"**â—† [{fmt(t)}]**")
30 lines.append("")
31
32 for seg, spk in zip(segments, labels):
33 start = float(seg["start"])
34 while mi < len(markers) and markers[mi][0] <= start:
35 emit_marker(*markers[mi])
36 mi += 1
37 if para and (start - prev_end > PAUSE_SPLIT or spk != p_spk):
38 flush()
39 if not para:
40 p_start, p_spk = start, spk
41 para.append(seg["text"].strip())
42 prev_end = float(seg["end"])
43 flush()
44 while mi < len(markers):
45 emit_marker(*markers[mi])
46 mi += 1
47 open(out_path, "w").write("\n".join(lines) + "\n")
src/Recorder/engine/Sources/recorder/CameraPreview.swift created+46
......@@ -0,0 +1,46 @@
1import AVFoundation
2import AppKit
3import SwiftUI
4
5/// Live camera preview for the popover, backed by an AVCaptureVideoPreviewLayer.
6struct CameraPreview: NSViewRepresentable {
7 let session: AVCaptureSession?
8
9 func makeNSView(context: Context) -> PreviewNSView { PreviewNSView() }
10
11 func updateNSView(_ nsView: PreviewNSView, context: Context) {
12 if nsView.previewLayer.session !== session {
13 nsView.previewLayer.session = session
14 }
15 }
16}
17
18/// Detachable camera window — stays live during recording for mid-stream framing.
19struct CameraPopoutView: View {
20 @ObservedObject var controller: AppController
21 var body: some View {
22 CameraPreview(session: controller.previewSession)
23 .frame(maxWidth: .infinity, maxHeight: .infinity)
24 .background(Color.black)
25 }
26}
27
28final class PreviewNSView: NSView {
29 let previewLayer = AVCaptureVideoPreviewLayer()
30
31 override init(frame frameRect: NSRect) {
32 super.init(frame: frameRect)
33 wantsLayer = true
34 layer = CALayer()
35 layer?.backgroundColor = NSColor.black.cgColor
36 previewLayer.videoGravity = .resizeAspect
37 layer?.addSublayer(previewLayer)
38 }
39
40 required init?(coder: NSCoder) { fatalError("init(coder:) unused") }
41
42 override func layout() {
43 super.layout()
44 previewLayer.frame = bounds
45 }
46}
src/Recorder/engine/Sources/recorder/CaptureEngine.swift+159-22
......@@ -16,6 +16,9 @@ struct RecordConfig {
1616 var bitsPerPixel: Double
1717 var duration: Double? // auto-stop after N seconds; nil = until signal
1818 var logPath: String?
19 var safeDir: URL? // internal-SSD home for audio + rollover parts (nil = outDir)
20 var cameraHeight: Int = 1080 // 720 / 1080 / …
21 var cameraFps: Int = 30
1922}
2023
2124final class CaptureEngine {
......@@ -26,15 +29,27 @@ final class CaptureEngine {
2629 private var scOutputs: [SCOutput] = []
2730 private var captureSessions: [AVCaptureSession] = []
2831 private var avOutputs: [AVOutput] = []
32 private var sessionObservers: [NSObjectProtocol] = []
2933
3034 private var createdEpoch: Double = 0
3135 private var hostClockAtStart: Double = 0
3236
37 /// Problem reports for the UI (device silent, disk vanished, stream lost…).
38 /// Each distinct condition fires once. Called on a background queue.
39 var onEvent: ((String) -> Void)?
40 private var watchdog: DispatchSourceTimer?
41 private var reportedEvents = Set<String>()
42 private let reportLock = NSLock()
43
44 /// Where audio and rescued streams live: the most reliable disk we have.
45 private var safeRoot: URL { cfg.safeDir ?? cfg.outDir }
46
3347 init(_ cfg: RecordConfig) { self.cfg = cfg }
3448
3549 func start() async throws {
3650 if let logPath = cfg.logPath { openLogFile(logPath) }
3751 try FileManager.default.createDirectory(at: cfg.outDir, withIntermediateDirectories: true)
52 try? FileManager.default.createDirectory(at: safeRoot, withIntermediateDirectories: true)
3853 createdEpoch = Date().timeIntervalSince1970
3954 hostClockAtStart = hostSeconds()
4055
......@@ -58,12 +73,13 @@ final class CaptureEngine {
5873 }
5974
6075 for (index, display) in chosen.enumerated() {
61 let name = screenName(index: index, count: chosen.count)
76 let name = "screen-\(index + 1)"
6277 let (outW, outH) = outputSize(for: display)
6378
6479 let writer = try CFRVideoWriter(
6580 url: cfg.outDir.appendingPathComponent("\(name).mov"),
66 name: name, width: outW, height: outH, fps: cfg.fps, bitrate: screenBitrate(outW, outH))
81 name: name, width: outW, height: outH, fps: cfg.fps, bitrate: screenBitrate(outW, outH),
82 fallbackDir: safeRoot)
6783 writer.displayID = display.displayID
6884 sinks.append(writer)
6985 cfrWriters.append(writer)
......@@ -89,6 +105,9 @@ final class CaptureEngine {
89105 onScreen: { [weak writer] sb in writer?.update(sb) },
90106 onAudio: attachAudioHere
91107 ? { [weak systemAudioWriter] sb in systemAudioWriter?.append(sb) } : nil)
108 output.onStreamError = { [weak self] message in
109 self?.report("scstop-\(name)", message + " (stop and restart the session)")
110 }
92111 scOutputs.append(output)
93112
94113 let stream = SCStream(filter: filter, configuration: cfgSC, delegate: output)
......@@ -117,6 +136,7 @@ final class CaptureEngine {
117136 for writer in cfrWriters {
118137 writer.start()
119138 }
139 startWatchdog()
120140 logInfo(
121141 "recording \(chosen.count) screen(s)"
122142 + (cfg.systemAudio ? " + desktop" : "")
......@@ -124,7 +144,12 @@ final class CaptureEngine {
124144 + (cfg.cameraUID != nil ? " + camera" : ""))
125145 }
126146
127 func stop() async {
147 @discardableResult
148 func stop() async -> [StreamManifest] {
149 watchdog?.cancel()
150 watchdog = nil
151 for observer in sessionObservers { NotificationCenter.default.removeObserver(observer) }
152 sessionObservers.removeAll()
128153 for output in scOutputs {
129154 logInfo(
130155 "SC \(output.label): screen seen \(output.screenSeen) complete \(output.screenComplete)"
......@@ -141,7 +166,65 @@ final class CaptureEngine {
141166 for sink in sinks {
142167 await sink.finish()
143168 }
144 writeManifest()
169 return writeManifest()
170 }
171
172 // MARK: Watchdog
173
174 /// Every couple of seconds: is every stream still producing data, and is the
175 /// scratch volume still there? Problems surface immediately (so a dead mic is
176 /// caught seconds in, not discovered after a 4-hour session) and disk loss
177 /// triggers a live rollover onto the internal SSD.
178 private func startWatchdog() {
179 let startedAt = hostSeconds()
180 let t = DispatchSource.makeTimerSource(queue: DispatchQueue(label: "clover.watchdog"))
181 t.schedule(deadline: .now() + 2, repeating: 2)
182 t.setEventHandler { [weak self] in self?.checkHealth(startedAt: startedAt) }
183 watchdog = t
184 t.resume()
185 }
186
187 private func report(_ key: String, _ message: String) {
188 reportLock.lock()
189 let fresh = reportedEvents.insert(key).inserted
190 reportLock.unlock()
191 guard fresh else { return }
192 logErr(message)
193 onEvent?(message)
194 }
195
196 private func checkHealth(startedAt: Double) {
197 // Scratch volume gone? Move every writer still pointing at it onto the
198 // safe (internal) disk before more data piles up in doomed buffers.
199 if let safe = cfg.safeDir, safe.path != cfg.outDir.path,
200 !FileManager.default.fileExists(atPath: cfg.outDir.path)
201 {
202 report(
203 "outdir-gone",
204 "Recording drive disappeared — video continues on the internal SSD. "
205 + "Earlier video is recoverable from the drive once it's reconnected.")
206 for sink in sinks { sink.rollover(to: safe) }
207 }
208
209 let now = hostSeconds()
210 for sink in sinks {
211 let h = sink.health()
212 if h.dead {
213 report(
214 "dead-\(h.name)",
215 "\(h.name): recording stopped after repeated write failures — "
216 + "stop the session and check the disks.")
217 } else if !h.started, h.lastAppendHost.isNaN, now - startedAt > 10 {
218 report(
219 "silent-\(h.name)",
220 "\(h.name) has produced no data — check the device "
221 + "(it may be disconnected or in use by another app).")
222 } else if h.started, !h.lastAppendHost.isNaN, now - h.lastAppendHost > 8 {
223 report(
224 "stall-\(h.name)",
225 "\(h.name) stopped producing data — check the device and disks.")
226 }
227 }
145228 }
146229
147230 // MARK: writers
......@@ -150,6 +233,30 @@ final class CaptureEngine {
150233 max(2_000_000, Int(Double(width * height * cfg.fps) * cfg.bitsPerPixel))
151234 }
152235
236 private func cameraPreset(forHeight height: Int) -> (AVCaptureSession.Preset, Int, Int) {
237 switch height {
238 case ...480: return (.vga640x480, 640, 480)
239 case 481...720: return (.hd1280x720, 1280, 720)
240 default: return (.hd1920x1080, 1920, 1080)
241 }
242 }
243
244 /// Best-effort frame-rate lock, clamped to what the camera supports (old
245 /// webcams are often 30-only, so 60 just stays 30).
246 private func configureCameraFrameRate(_ device: AVCaptureDevice) {
247 guard let range = device.activeFormat.videoSupportedFrameRateRanges.first else { return }
248 do {
249 try device.lockForConfiguration()
250 let wanted = CMTime(value: 1, timescale: Int32(cfg.cameraFps))
251 let dur = CMTimeMaximum(range.minFrameDuration, CMTimeMinimum(wanted, range.maxFrameDuration))
252 device.activeVideoMinFrameDuration = dur
253 device.activeVideoMaxFrameDuration = dur
254 device.unlockForConfiguration()
255 } catch {
256 logErr("camera fps: \(error.localizedDescription)")
257 }
258 }
259
153260 private func makeVideoWriter(name: String, kind: String, width: Int, height: Int) throws
154261 -> StreamWriter
155262 {
......@@ -165,12 +272,15 @@ final class CaptureEngine {
165272 ]
166273 return try StreamWriter(
167274 url: cfg.outDir.appendingPathComponent("\(name).mov"),
168 name: name, kind: kind, fileType: .mov, settings: settings, mediaType: .video)
275 name: name, kind: kind, fileType: .mov, settings: settings, mediaType: .video,
276 fallbackDir: safeRoot)
169277 }
170278
171279 private func makeAudioWriter(name: String, kind: String) throws -> StreamWriter {
172280 // AAC in .m4a: ~256 kbps stereo, transparent for voice/desktop and tiny next
173 // to the uncompressed PCM we used to write.
281 // to the uncompressed PCM we used to write. Audio is the irreplaceable
282 // stream and costs ~115 MB/hour, so it records to the safe (internal) disk
283 // rather than the removable scratch drive.
174284 let settings: [String: Any] = [
175285 AVFormatIDKey: kAudioFormatMPEG4AAC,
176286 AVSampleRateKey: 48_000,
......@@ -178,8 +288,9 @@ final class CaptureEngine {
178288 AVEncoderBitRateKey: 256_000,
179289 ]
180290 return try StreamWriter(
181 url: cfg.outDir.appendingPathComponent("\(name).m4a"),
182 name: name, kind: kind, fileType: .m4a, settings: settings, mediaType: .audio)
291 url: safeRoot.appendingPathComponent("\(name).m4a"),
292 name: name, kind: kind, fileType: .m4a, settings: settings, mediaType: .audio,
293 fallbackDir: safeRoot)
183294 }
184295
185296 // MARK: AVCapture (mic + camera)
......@@ -216,8 +327,13 @@ final class CaptureEngine {
216327 guard session.canAddInput(input) else { throw RecorderError("cannot add camera input") }
217328 session.addInput(input)
218329
219 let camWriter = try makeVideoWriter(
220 name: "cam", kind: "camera", width: 1920, height: 1080)
330 // Match the writer to the preset's true dimensions so a 4:3 mode (480p =
331 // 640×480) isn't pillarboxed into 16:9.
332 let (preset, camW, camH) = cameraPreset(forHeight: cfg.cameraHeight)
333 if session.canSetSessionPreset(preset) { session.sessionPreset = preset }
334 configureCameraFrameRate(device)
335
336 let camWriter = try makeVideoWriter(name: "cam", kind: "camera", width: camW, height: camH)
221337 camWriter.deviceUID = device.uniqueID
222338 sinks.append(camWriter)
223339
......@@ -230,17 +346,22 @@ final class CaptureEngine {
230346 }
231347
232348 session.commitConfiguration()
349 // Runtime errors (device wedged, media services reset) otherwise vanish
350 // silently — the watchdog would notice the stall, but this names the cause.
351 let observer = NotificationCenter.default.addObserver(
352 forName: .AVCaptureSessionRuntimeError, object: session, queue: nil
353 ) { [weak self] note in
354 let reason =
355 (note.userInfo?[AVCaptureSessionErrorKey] as? NSError)?.localizedDescription ?? "unknown"
356 self?.report("avsession-\(reason)", "Mic/camera capture error: \(reason)")
357 }
358 sessionObservers.append(observer)
233359 session.startRunning()
234360 captureSessions.append(session)
235361 }
236362
237363 // MARK: helpers
238364
239 private func screenName(index: Int, count: Int) -> String {
240 if count == 2 { return index == 0 ? "screen-left" : "screen-right" }
241 return "screen-\(index + 1)"
242 }
243
244365 private func outputSize(for display: SCDisplay) -> (Int, Int) {
245366 // Prefer the true framebuffer pixel size; fall back to the points frame.
246367 var pxW = display.width
......@@ -257,12 +378,14 @@ final class CaptureEngine {
257378
258379 private func even(_ v: Int) -> Int { v - (v % 2) }
259380
260 private func writeManifest() {
261 let withData = sinks.filter { !$0.firstPTS.isNaN }
262 let tStart = withData.map { $0.firstPTS }.min() ?? hostClockAtStart
381 @discardableResult
382 private func writeManifest() -> [StreamManifest] {
383 let all = sinks.flatMap { $0.manifests() }
384 let tStart = all.map { $0.firstSampleHostSeconds }.filter { !$0.isNaN }.min()
385 ?? hostClockAtStart
263386
264 var streamManifests = sinks.map { sink -> StreamManifest in
265 var m = sink.manifest()
387 var streamManifests = all.map { m -> StreamManifest in
388 var m = m
266389 m.offsetSeconds = m.firstSampleHostSeconds.isNaN ? 0 : (m.firstSampleHostSeconds - tStart)
267390 return m
268391 }
......@@ -276,14 +399,27 @@ final class CaptureEngine {
276399 tStartHostSeconds: tStart,
277400 streams: streamManifests)
278401
402 // Write to both roots (they may be different volumes); losing one disk
403 // must not cost the alignment data for the surviving streams.
404 var targets = [cfg.outDir]
405 if safeRoot.path != cfg.outDir.path { targets.append(safeRoot) }
406 var wrote = false
279407 do {
280408 let encoder = JSONEncoder()
281409 encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
282410 let data = try encoder.encode(manifest)
283 try data.write(to: cfg.outDir.appendingPathComponent("sync.json"))
411 for dir in targets {
412 do {
413 try data.write(to: dir.appendingPathComponent("sync.json"))
414 wrote = true
415 } catch {
416 logErr("failed to write manifest to \(dir.path): \(error)")
417 }
418 }
284419 } catch {
285 logErr("failed to write manifest: \(error)")
420 logErr("failed to encode manifest: \(error)")
286421 }
422 if !wrote { report("manifest", "Couldn't save sync.json — stream alignment data was lost.") }
287423
288424 // Human-readable summary to stderr.
289425 logInfo("session: \(cfg.outDir.path)")
......@@ -295,6 +431,7 @@ final class CaptureEngine {
295431 m.name as NSString, m.durationSeconds, m.frames, m.dropped, m.repeated,
296432 m.offsetSeconds, size as NSString))
297433 }
434 return streamManifests
298435 }
299436
300437 private func fileSize(_ url: URL) -> String {
src/Recorder/engine/Sources/recorder/MarkerOverlay.swift created+232
......@@ -0,0 +1,232 @@
1import AVFoundation
2import AppKit
3import Carbon.HIToolbox
4import SwiftUI
5
6// MARK: - Global hotkey
7
8/// A system-wide hotkey via the Carbon Event Manager. Unlike an NSEvent global
9/// monitor this needs no Accessibility/Input-Monitoring grant, and it consumes
10/// the key so nothing else sees it. Fires on the main thread.
11private func hotKeyEventHandler(
12 _ next: EventHandlerCallRef?, _ event: EventRef?, _ userData: UnsafeMutableRawPointer?
13) -> OSStatus {
14 guard let userData else { return noErr }
15 Unmanaged<HotKey>.fromOpaque(userData).takeUnretainedValue().onPress()
16 return noErr
17}
18
19final class HotKey {
20 let onPress: () -> Void
21 private var hotKeyRef: EventHotKeyRef?
22 private var eventHandler: EventHandlerRef?
23
24 init?(keyCode: UInt32, modifiers: UInt32 = 0, onPress: @escaping () -> Void) {
25 self.onPress = onPress
26 var spec = EventTypeSpec(
27 eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed))
28 guard
29 InstallEventHandler(
30 GetApplicationEventTarget(), hotKeyEventHandler, 1, &spec,
31 Unmanaged.passUnretained(self).toOpaque(), &eventHandler) == noErr
32 else { return nil }
33
34 let id = EventHotKeyID(signature: OSType(0x434C_5652), id: 1) // 'CLVR'
35 guard
36 RegisterEventHotKey(keyCode, modifiers, id, GetApplicationEventTarget(), 0, &hotKeyRef)
37 == noErr
38 else { return nil }
39 }
40
41 deinit {
42 if let hotKeyRef { UnregisterEventHotKey(hotKeyRef) }
43 if let eventHandler { RemoveEventHandler(eventHandler) }
44 }
45}
46
47// MARK: - Marker model
48
49struct Marker: Codable {
50 let hostSeconds: Double // mach host clock — align against sync.json
51 let offsetSeconds: Double // from session start, for humans
52 let text: String?
53 let wallClock: String
54}
55
56// MARK: - Overlay panel
57
58/// Borderless floating panel that can take key focus so you can type into it.
59final class MarkerPanel: NSPanel {
60 override var canBecomeKey: Bool { true }
61}
62
63// MARK: - Identify overlay
64
65/// Big number flashed on a physical display so you can see which is screen 1/2.
66struct IdentifyView: View {
67 let number: Int
68 var body: some View {
69 Text("\(number)")
70 .font(.system(size: 150, weight: .bold, design: .rounded))
71 .foregroundStyle(.white)
72 .frame(width: 230, height: 230)
73 .background(.blue.opacity(0.85), in: RoundedRectangle(cornerRadius: 30))
74 }
75}
76
77// MARK: - Voice dictation (local Whisper)
78
79/// Records the mic while the marker overlay is open and transcribes it on demand
80/// via on-device MLX Whisper (large-v3-turbo) — ~1.5 s per note on this machine.
81@MainActor
82final class DictationModel: ObservableObject {
83 enum Phase { case idle, listening, transcribing }
84
85 @Published var text = ""
86 @Published var phase: Phase = .idle
87
88 private var recorder: AVAudioRecorder?
89 private let audioURL = FileManager.default.temporaryDirectory
90 .appendingPathComponent("clover-marker.wav")
91
92 static let pythonPath = NSHomeDirectory() + "/.clover-whisper/.venv/bin/python"
93 static var scriptPath: String {
94 Bundle.main.url(forResource: "transcribe", withExtension: "py")?.path ?? ""
95 }
96 var available: Bool {
97 FileManager.default.isExecutableFile(atPath: Self.pythonPath) && !Self.scriptPath.isEmpty
98 }
99
100 func startListening() {
101 guard available else { return }
102 let settings: [String: Any] = [
103 AVFormatIDKey: kAudioFormatLinearPCM,
104 AVSampleRateKey: 16_000,
105 AVNumberOfChannelsKey: 1,
106 AVLinearPCMBitDepthKey: 16,
107 AVLinearPCMIsFloatKey: false,
108 AVLinearPCMIsBigEndianKey: false,
109 ]
110 do {
111 let rec = try AVAudioRecorder(url: audioURL, settings: settings)
112 rec.record()
113 recorder = rec
114 phase = .listening
115 } catch {
116 phase = .idle
117 }
118 }
119
120 func stopAndTranscribe() async {
121 guard phase == .listening else { return }
122 recorder?.stop()
123 recorder = nil
124 phase = .transcribing
125 let result = await Self.run(Self.pythonPath, [Self.scriptPath, audioURL.path])
126 if !result.isEmpty { text = result }
127 phase = .idle
128 }
129
130 func cancel() {
131 recorder?.stop()
132 recorder = nil
133 phase = .idle
134 }
135
136 private static func run(_ path: String, _ args: [String]) async -> String {
137 await withCheckedContinuation { (cont: CheckedContinuation<String, Never>) in
138 DispatchQueue.global(qos: .userInitiated).async {
139 let p = Process()
140 p.executableURL = URL(fileURLWithPath: path)
141 p.arguments = args
142 p.environment = cloverToolEnvironment()
143 let pipe = Pipe()
144 p.standardOutput = pipe
145 p.standardError = Pipe()
146 do { try p.run() } catch {
147 cont.resume(returning: "")
148 return
149 }
150 let data = pipe.fileHandleForReading.readDataToEndOfFile()
151 p.waitUntilExit()
152 cont.resume(
153 returning: String(decoding: data, as: UTF8.self)
154 .trimmingCharacters(in: .whitespacesAndNewlines))
155 }
156 }
157 }
158}
159
160struct MarkerOverlayView: View {
161 let offsetText: String
162 let onSubmit: (String) -> Void
163 let onCancel: () -> Void
164
165 @StateObject private var dictation = DictationModel()
166 @FocusState private var focused: Bool
167
168 var body: some View {
169 HStack(spacing: 10) {
170 Image(systemName: icon).foregroundStyle(iconColor).font(.title3)
171 Text(offsetText)
172 .font(.system(.callout, design: .monospaced)).foregroundStyle(.secondary)
173 TextField(placeholder, text: $dictation.text)
174 .textFieldStyle(.plain)
175 .focused($focused)
176 .onSubmit { handleEnter() }
177 .frame(width: 320)
178 // Typing while listening switches to manual entry.
179 .onChange(of: dictation.text) { _, newValue in
180 if dictation.phase == .listening && !newValue.isEmpty { dictation.cancel() }
181 }
182 }
183 .padding(.horizontal, 16)
184 .padding(.vertical, 12)
185 .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 14))
186 .overlay(RoundedRectangle(cornerRadius: 14).strokeBorder(iconColor.opacity(0.5), lineWidth: 1))
187 .onAppear {
188 focused = true
189 dictation.startListening()
190 }
191 .onKeyPress(.escape) {
192 dictation.cancel()
193 onCancel()
194 return .handled
195 }
196 }
197
198 private func handleEnter() {
199 switch dictation.phase {
200 case .listening:
201 Task { await dictation.stopAndTranscribe() }
202 case .transcribing:
203 break
204 case .idle:
205 onSubmit(dictation.text)
206 }
207 }
208
209 private var icon: String {
210 switch dictation.phase {
211 case .listening: return "mic.fill"
212 case .transcribing: return "waveform"
213 case .idle: return "mappin.circle.fill"
214 }
215 }
216 private var iconColor: Color {
217 switch dictation.phase {
218 case .listening: return .red
219 case .transcribing: return .blue
220 case .idle: return .orange
221 }
222 }
223 private var placeholder: String {
224 switch dictation.phase {
225 case .listening: return "Listening… speak, then Enter to transcribe (or just type)"
226 case .transcribing: return "Transcribing…"
227 case .idle:
228 return dictation.available
229 ? "Enter to drop · Esc to cancel" : "Marker note (optional) — Enter to drop"
230 }
231 }
232}
src/Recorder/engine/Sources/recorder/MenubarApp.swift+1165-84
......@@ -1,10 +1,25 @@
11import AppKit
22import AVFoundation
3import Carbon.HIToolbox
4import CoreMedia
35import Foundation
46import SwiftUI
7import UserNotifications
58
69// MARK: - Entry
710
11/// Environment with the Nix/Homebrew bins on PATH so spawned Python tools can
12/// find ffmpeg (a GUI app launched via Finder/`open` has a bare PATH otherwise).
13func cloverToolEnvironment() -> [String: String] {
14 var env = ProcessInfo.processInfo.environment
15 let dirs = [
16 "/etc/profiles/per-user/\(NSUserName())/bin", "/run/current-system/sw/bin",
17 "/opt/homebrew/bin", "/usr/local/bin",
18 ]
19 env["PATH"] = dirs.joined(separator: ":") + ":" + (env["PATH"] ?? "/usr/bin:/bin")
20 return env
21}
22
823@MainActor
924func runMenubar() {
1025 let app = NSApplication.shared
......@@ -17,7 +32,9 @@ func runMenubar() {
1732// MARK: - Controller
1833
1934@MainActor
20final class AppController: NSObject, NSApplicationDelegate, ObservableObject {
35final class AppController: NSObject, NSApplicationDelegate, NSPopoverDelegate, NSWindowDelegate,
36 ObservableObject
37{
2138 // Settings (persisted)
2239 @Published var destination: String = UserDefaults.standard.string(forKey: "destination") ?? "Sessions"
2340 {
......@@ -31,18 +48,71 @@ final class AppController: NSObject, NSApplicationDelegate, ObservableObject {
3148 @Published var includeDesktop = UserDefaults.standard.object(forKey: "desktop") as? Bool ?? true
3249 @Published var includeMic = UserDefaults.standard.object(forKey: "mic") as? Bool ?? true
3350 @Published var reaperMidi = UserDefaults.standard.object(forKey: "reaper") as? Bool ?? false
51 // Off by default: solo journaling/improv gets one clean voice. On only for the
52 // occasional session with other people (diarize + name via the Speakers review).
53 @Published var detectSpeakers = UserDefaults.standard.object(forKey: "detectSpeakers") as? Bool ?? false
54 @Published var reverseScreens = UserDefaults.standard.object(forKey: "reverseScreens") as? Bool ?? false
3455 @Published var enabledDisplays: Set<UInt32> = []
3556
57 @Published var includeCamera = UserDefaults.standard.object(forKey: "camera") as? Bool ?? false {
58 didSet { updatePreview() }
59 }
60 @Published var cameraUID: String? = UserDefaults.standard.string(forKey: "cameraUID") {
61 didSet { if oldValue != cameraUID { restartPreview() } }
62 }
63 @Published var cameraHeight = UserDefaults.standard.object(forKey: "cameraHeight") as? Int ?? 1080
64 {
65 didSet { if oldValue != cameraHeight { restartPreview() } } // re-apply aspect/format
66 }
67 @Published var cameraFps = UserDefaults.standard.object(forKey: "cameraFps") as? Int ?? 30
68 @Published var cameraAntiFlickerHz: Int =
69 UserDefaults.standard.object(forKey: "antiFlicker") as? Int ?? 60
70 {
71 didSet { if includeCamera { applyAntiFlicker() } }
72 }
73 @Published var cameras: [DeviceInfo] = []
74 @Published var previewSession: AVCaptureSession?
75 /// Actual width/height ratio the camera delivers, observed from its active
76 /// format — the preview frame uses this so the feed never letterboxes.
77 @Published var observedCameraAspect: CGFloat?
78 var hasCamera: Bool { !cameras.isEmpty }
79
80 /// Displays in screen-number order (left→right, or reversed if you flip it).
81 var orderedDisplays: [DisplayInfo] {
82 reverseScreens ? displays.reversed() : displays
83 }
84
3685 // Discovered hardware
3786 @Published var displays: [DisplayInfo] = []
38 @Published var hasMic = false
87 @Published var audioInputs: [DeviceInfo] = [] // real mics only (webcam mic filtered out)
88 @Published var micUID: String? = UserDefaults.standard.string(forKey: "micUID") {
89 didSet { if !settingMicProgrammatically { micExplicit = true } }
90 }
91 /// True once the user picks a mic by hand — lets refreshDevices re-default away
92 /// from undesirable mics (webcam/Continuity) without clobbering a deliberate pick.
93 private var micExplicit = UserDefaults.standard.bool(forKey: "micExplicit")
94 private var settingMicProgrammatically = false
3995 @Published var permissionOK = true
96 var hasMic: Bool { !audioInputs.isEmpty }
4097
4198 // Live state
4299 @Published var isRecording = false
43100 @Published var elapsed: TimeInterval = 0
101 @Published var markerCount = 0
44102 @Published var lastSession: String?
45 @Published var errorMessage: String?
103 @Published var compressProgress: Double? // 0…1 while re-encoding, else nil
104 @Published var errorMessage: String? {
105 didSet { updateIcon() } // problem state shows in the menubar, not just here
106 }
107 @Published var voiceEnrolled = FileManager.default.fileExists(
108 atPath: NSHomeDirectory() + "/.clover-whisper/voices.json")
109 @Published var enrolling = false
110 @Published var enrollSecondsLeft = 0
111 private var lastSessionURL: URL?
112 private var enrollRecorder: AVAudioRecorder?
113 private var enrollTimer: Timer?
114 private var speakersWindow: NSWindow?
115 private var cameraPopout: NSWindow?
46116
47117 private var statusItem: NSStatusItem?
48118 private var popover: NSPopover?
......@@ -51,10 +121,21 @@ final class AppController: NSObject, NSApplicationDelegate, ObservableObject {
51121 private var startHost: Double = 0
52122 private var tickTimer: Timer?
53123 private var currentSessionDir: URL?
124 private var currentSafeDir: URL?
54125 private var zenithDir: URL?
126 private var postProcessing = false
127 private var reconciling = false
128 private var reconcileTimer: Timer?
129 private var hotKey: HotKey?
130 private var markers: [Marker] = []
131 private var markerPanel: MarkerPanel?
132 private var identifyWindows: [NSWindow] = []
55133
56 // Fast local scratch for video/audio; the NAS archive is the final home.
134 // Fast local scratch for video; the NAS archive is the final home. Audio and
135 // anything rescued mid-session live under recoveryRoot on the internal SSD,
136 // which can't disconnect the way an external volume can.
57137 let tempRoot = URL(fileURLWithPath: "/Volumes/Documents/Temp")
138 let recoveryRoot = URL(fileURLWithPath: NSHomeDirectory() + "/Movies/Clover Recovery")
58139 let archiveRoot = URL(fileURLWithPath: "/Volumes/clover/Archive")
59140 let reaperTemplate = URL(fileURLWithPath: "/Volumes/Documents/Recorder Template.rpp")
60141 @Published var statusText: String?
......@@ -69,11 +150,118 @@ final class AppController: NSObject, NSApplicationDelegate, ObservableObject {
69150
70151 let pop = NSPopover()
71152 pop.behavior = .transient
72 pop.contentSize = NSSize(width: 320, height: 460)
153 pop.delegate = self
73154 pop.contentViewController = NSHostingController(rootView: ContentView(controller: self))
74155 popover = pop
75156
157 // F14 anywhere drops a session marker (Carbon hotkey — no extra permission).
158 hotKey = HotKey(keyCode: UInt32(kVK_F14)) { [weak self] in
159 MainActor.assumeIsolated { self?.markerPressed() }
160 }
161
162 setupNotifications()
76163 Task { await refreshDevices() }
164
165 // Sessions stranded locally (zenith was down, a crash, a failed archive)
166 // are retried at launch and periodically while idle.
167 Task { await reconcilePending() }
168 reconcileTimer = Timer.scheduledTimer(withTimeInterval: 900, repeats: true) { [weak self] _ in
169 Task { @MainActor in await self?.reconcilePending() }
170 }
171 }
172
173 // MARK: Problem reporting
174
175 /// One path for anything going wrong: inline message in the popover, an
176 /// orange warning in the menubar, and a system notification (recordings run
177 /// unattended — a problem must not wait for the popover to be opened).
178 func reportProblem(_ message: String) {
179 errorMessage = message
180 notify(message)
181 }
182
183 private func setupNotifications() {
184 guard Bundle.main.bundleIdentifier != nil else { return }
185 UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { _, _ in }
186 }
187
188 private func notify(_ body: String, title: String = "Clover Recorder") {
189 guard Bundle.main.bundleIdentifier != nil else { return }
190 let content = UNMutableNotificationContent()
191 content.title = title
192 content.body = body
193 UNUserNotificationCenter.current().add(
194 UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil))
195 }
196
197 // MARK: Markers
198
199 func markerPressed() {
200 guard isRecording, let dir = currentSessionDir, markerPanel == nil else { return }
201 let host = hostSeconds()
202 showMarkerOverlay(host: host, offset: host - startHost, dir: dir)
203 }
204
205 private func showMarkerOverlay(host: Double, offset: Double, dir: URL) {
206 let panel = MarkerPanel(
207 contentRect: NSRect(x: 0, y: 0, width: 420, height: 56),
208 styleMask: [.borderless], backing: .buffered, defer: false)
209 panel.level = .floating
210 panel.isOpaque = false
211 panel.backgroundColor = .clear
212 panel.hasShadow = true
213 panel.isMovableByWindowBackground = true
214 panel.contentViewController = NSHostingController(
215 rootView: MarkerOverlayView(
216 offsetText: clockString(offset),
217 onSubmit: { [weak self] text in
218 self?.commitMarker(host: host, offset: offset, text: text, dir: dir)
219 },
220 onCancel: { [weak self] in self?.dismissMarkerOverlay() }))
221 if let screen = NSScreen.main {
222 let f = screen.frame
223 panel.setFrameOrigin(NSPoint(x: f.midX - panel.frame.width / 2, y: f.maxY - 220))
224 }
225 markerPanel = panel
226 NSApp.activate(ignoringOtherApps: true)
227 panel.makeKeyAndOrderFront(nil)
228 }
229
230 private func commitMarker(host: Double, offset: Double, text: String, dir: URL) {
231 let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
232 markers.append(
233 Marker(
234 hostSeconds: host, offsetSeconds: offset,
235 text: trimmed.isEmpty ? nil : trimmed,
236 wallClock: ISO8601DateFormatter().string(from: Date())))
237 markerCount = markers.count
238 writeMarkers(to: dir)
239 dismissMarkerOverlay()
240 }
241
242 private func dismissMarkerOverlay() {
243 markerPanel?.orderOut(nil)
244 markerPanel = nil
245 }
246
247 private func writeMarkers(to dir: URL) {
248 let encoder = JSONEncoder()
249 encoder.outputFormatting = [.prettyPrinted]
250 guard let data = try? encoder.encode(MarkersFile(markers: markers)) else { return }
251 // Written to the scratch AND the internal recovery dir, so markers survive
252 // either volume disappearing mid-session.
253 var targets = [dir]
254 if let safe = currentSafeDir, safe != dir { targets.append(safe) }
255 var ok = false
256 for t in targets {
257 if (try? data.write(to: t.appendingPathComponent("markers.json"))) != nil { ok = true }
258 }
259 if !ok { reportProblem("Couldn't save markers — check the recording disks.") }
260 }
261
262 private func clockString(_ t: TimeInterval) -> String {
263 let s = Int(t)
264 return String(format: "%02d:%02d", s / 60, s % 60)
77265 }
78266
79267 @objc private func togglePopover() {
......@@ -81,7 +269,10 @@ final class AppController: NSObject, NSApplicationDelegate, ObservableObject {
81269 if popover.isShown {
82270 popover.performClose(nil)
83271 } else {
84 Task { await refreshDevices() } // re-check displays + mic each time it opens
272 Task {
273 await refreshDevices() // re-check displays + mic + cameras each time it opens
274 updatePreview()
275 }
85276 popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
86277 popover.contentViewController?.view.window?.makeKey()
87278 }
......@@ -91,7 +282,25 @@ final class AppController: NSObject, NSApplicationDelegate, ObservableObject {
91282 do {
92283 let devices = try await Devices.discover()
93284 displays = devices.displays
94 hasMic = !devices.audioInputs.isEmpty
285 cameras = devices.cameras
286 audioInputs = devices.audioInputs // all selectable
287
288 // Never DEFAULT to a webcam mic (shares a camera's name) or a Continuity
289 // (iPhone/iPad) mic — but leave them pickable. The engine flags Continuity
290 // mics via transport type; also treat any mic named like a camera as one.
291 let camNames = Set(devices.cameras.map { $0.name })
292 func deprioritized(_ d: DeviceInfo) -> Bool { d.continuity || camNames.contains(d.name) }
293 // Re-default if unset, gone, or auto-pointing at a deprioritized device. A
294 // mic the user picked by hand is left alone even if it's a webcam/Continuity.
295 let current = audioInputs.first { $0.uid == micUID }
296 if current == nil || (!micExplicit && deprioritized(current!)) {
297 settingMicProgrammatically = true
298 micUID = (audioInputs.first { !deprioritized($0) } ?? audioInputs.first)?.uid
299 settingMicProgrammatically = false
300 }
301 if cameraUID == nil || !cameras.contains(where: { $0.uid == cameraUID }) {
302 cameraUID = (cameras.first { !$0.continuity } ?? cameras.first)?.uid
303 }
95304 permissionOK = true
96305 // Display IDs change across sleep/wake, so drop stale selections and fall
97306 // back to all current displays if nothing valid remains.
......@@ -108,12 +317,168 @@ final class AppController: NSObject, NSApplicationDelegate, ObservableObject {
108317 if on { enabledDisplays.insert(id) } else { enabledDisplays.remove(id) }
109318 }
110319
320 /// Flash the screen number on each physical display, in recorder order, so you
321 /// can see which monitor is screen-1 / screen-2.
322 func identifyScreens() {
323 dismissIdentify()
324 for (idx, d) in orderedDisplays.enumerated() {
325 guard let screen = NSScreen.screens.first(where: { screenNumber($0) == d.id }) else {
326 continue
327 }
328 let size = NSSize(width: 230, height: 230)
329 let frame = NSRect(
330 x: screen.frame.midX - size.width / 2, y: screen.frame.midY - size.height / 2,
331 width: size.width, height: size.height)
332 let win = NSPanel(
333 contentRect: frame, styleMask: [.borderless, .nonactivatingPanel], backing: .buffered,
334 defer: false)
335 win.level = .screenSaver
336 win.isOpaque = false
337 win.backgroundColor = .clear
338 win.hasShadow = false
339 win.ignoresMouseEvents = true
340 win.collectionBehavior = [.canJoinAllSpaces, .stationary]
341 win.contentViewController = NSHostingController(rootView: IdentifyView(number: idx + 1))
342 win.orderFrontRegardless()
343 identifyWindows.append(win)
344 }
345 DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) { [weak self] in self?.dismissIdentify() }
346 }
347
348 private func dismissIdentify() {
349 identifyWindows.forEach { $0.orderOut(nil) }
350 identifyWindows.removeAll()
351 }
352
353 private func screenNumber(_ screen: NSScreen) -> UInt32? {
354 (screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber)?.uint32Value
355 }
356
357 // MARK: Camera preview
358
359 /// Run the preview whenever the camera's on and somewhere is showing it (the
360 /// popover or the pop-out window) — including during recording (macOS shares
361 /// the camera across sessions).
362 func updatePreview() {
363 let want = includeCamera && hasCamera && (popover?.isShown == true || cameraPopout != nil)
364 if want && previewSession == nil {
365 startPreviewSession()
366 } else if !want && previewSession != nil {
367 stopPreview()
368 }
369 }
370
371 func restartPreview() {
372 if previewSession != nil {
373 stopPreview()
374 updatePreview()
375 }
376 }
377
378 /// Preset matching the selected recording quality, so the preview's aspect/FOV
379 /// equals the output's (480p = 4:3, 720p/1080p = 16:9).
380 func cameraPreset() -> AVCaptureSession.Preset {
381 switch cameraHeight {
382 case ...480: return .vga640x480
383 case 481...720: return .hd1280x720
384 default: return .hd1920x1080
385 }
386 }
387
388 /// Real aspect once we've seen a frame's format; otherwise the preset's guess.
389 var cameraAspect: CGFloat {
390 observedCameraAspect ?? (cameraHeight <= 480 ? 4.0 / 3.0 : 16.0 / 9.0)
391 }
392
393 private func startPreviewSession() {
394 guard let uid = cameraUID, let device = Devices.videoDevice(matching: uid),
395 let input = try? AVCaptureDeviceInput(device: device)
396 else { return }
397 let session = AVCaptureSession()
398 session.sessionPreset = cameraPreset()
399 guard session.canAddInput(input) else { return }
400 session.addInput(input)
401 // Size the preview to what the camera actually outputs, not the preset's
402 // nominal aspect — many webcams ignore a 4:3 preset and stay 16:9.
403 let dims = CMVideoFormatDescriptionGetDimensions(device.activeFormat.formatDescription)
404 observedCameraAspect =
405 dims.width > 0 && dims.height > 0 ? CGFloat(dims.width) / CGFloat(dims.height) : nil
406 previewSession = session
407 DispatchQueue.global(qos: .userInitiated).async { session.startRunning() }
408 DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { [weak self] in self?.applyAntiFlicker() }
409 }
410
411 func stopPreview() {
412 if let session = previewSession {
413 DispatchQueue.global(qos: .userInitiated).async { session.stopRunning() }
414 }
415 previewSession = nil
416 }
417
418 func toggleCameraPopout() {
419 if let win = cameraPopout {
420 win.close() // windowWillClose clears the ref and re-evaluates the preview
421 return
422 }
423 let win = NSWindow(
424 contentRect: NSRect(x: 0, y: 0, width: 480, height: 480 / cameraAspect),
425 styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: false)
426 win.title = "Camera"
427 win.contentAspectRatio = NSSize(width: cameraAspect, height: 1)
428 win.contentViewController = NSHostingController(rootView: CameraPopoutView(controller: self))
429 win.isReleasedWhenClosed = false
430 win.level = .floating
431 win.delegate = self
432 win.center()
433 cameraPopout = win
434 updatePreview()
435 NSApp.activate(ignoringOtherApps: true)
436 win.makeKeyAndOrderFront(nil)
437 }
438
439 func windowWillClose(_ notification: Notification) {
440 if (notification.object as? NSWindow) === cameraPopout {
441 cameraPopout = nil
442 updatePreview()
443 }
444 }
445
446 /// Push the UVC Power Line Frequency to the camera (kills mains flicker). Best
447 /// applied once the camera is already streaming; safe to call repeatedly.
448 func applyAntiFlicker() {
449 let helper = Bundle.main.bundleURL.appendingPathComponent("Contents/MacOS/uvc-powerline")
450 guard FileManager.default.isExecutableFile(atPath: helper.path) else { return }
451 let arg: String
452 switch cameraAntiFlickerHz {
453 case 50: arg = "1"
454 case 60: arg = "2"
455 default: arg = "0"
456 }
457 let process = Process()
458 process.executableURL = helper
459 process.arguments = [arg]
460 DispatchQueue.global(qos: .utility).async { try? process.run() }
461 }
462
463 func popoverDidClose(_ notification: Notification) {
464 updatePreview() // keep running if the pop-out window is up
465 }
466
111467 private func persist() {
112468 let d = UserDefaults.standard
113469 d.set(destination, forKey: "destination")
114470 d.set(includeDesktop, forKey: "desktop")
115471 d.set(includeMic, forKey: "mic")
472 d.set(micUID, forKey: "micUID")
473 d.set(micExplicit, forKey: "micExplicit")
116474 d.set(reaperMidi, forKey: "reaper")
475 d.set(detectSpeakers, forKey: "detectSpeakers")
476 d.set(reverseScreens, forKey: "reverseScreens")
477 d.set(includeCamera, forKey: "camera")
478 d.set(cameraUID, forKey: "cameraUID")
479 d.set(cameraHeight, forKey: "cameraHeight")
480 d.set(cameraFps, forKey: "cameraFps")
481 d.set(cameraAntiFlickerHz, forKey: "antiFlicker")
117482 }
118483
119484 // MARK: recording
......@@ -125,71 +490,104 @@ final class AppController: NSObject, NSApplicationDelegate, ObservableObject {
125490 errorMessage = nil
126491 statusText = "Checking zenith…"
127492 Task {
128 guard await self.ensureZenithMounted() else {
129 self.starting = false
130 self.statusText = nil
131 self.errorMessage = "zenith archive isn't mounted — mount /Volumes/clover and try again."
132 return
493 // zenith being down must never block a recording — archive later instead.
494 let zenithOK = await self.ensureZenithMounted()
495 if !zenithOK {
496 self.notify("zenith isn't mounted — recording locally; it will archive when zenith is back.")
133497 }
134498 await self.refreshDevices() // display IDs shift across sleep/wake
135 await self.beginRecording()
499 await self.beginRecording(zenithOK: zenithOK)
136500 self.starting = false
137501 }
138502 }
139503
140 private func beginRecording() async {
504 private func beginRecording(zenithOK: Bool) async {
141505 let name = resolveSessionName()
142 let temp = tempRoot.appendingPathComponent(name)
143 let zenith = zenithSessionDir(name)
144 try? FileManager.default.createDirectory(at: temp, withIntermediateDirectories: true)
145
146 // The final NAS home — created now so REAPER can record straight into it.
147 var zenithReady: URL? = zenith
506 // Big video goes to the scratch drive; if that's missing, everything
507 // records to the internal recovery dir rather than blocking the session.
508 var temp = tempRoot.appendingPathComponent(name)
509 let safe = recoveryRoot.appendingPathComponent(name)
148510 do {
149 try FileManager.default.createDirectory(at: zenith, withIntermediateDirectories: true)
511 try FileManager.default.createDirectory(at: temp, withIntermediateDirectories: true)
150512 } catch {
151 errorMessage = "Archive folder unavailable (\(error.localizedDescription)); will keep local copy."
152 zenithReady = nil
513 temp = safe
514 reportProblem("Scratch drive unavailable — recording to the internal SSD instead.")
515 }
516 do {
517 try FileManager.default.createDirectory(at: safe, withIntermediateDirectories: true)
518 } catch {
519 statusText = nil
520 reportProblem("Can't create \(safe.path) (\(error.localizedDescription)) — not recording.")
521 return
522 }
523
524 // The final NAS home — created now so REAPER can record straight into it.
525 var zenithReady: URL? = nil
526 if zenithOK {
527 let zenith = zenithSessionDir(name)
528 do {
529 try FileManager.default.createDirectory(at: zenith, withIntermediateDirectories: true)
530 zenithReady = zenith
531 } catch {
532 reportProblem(
533 "Archive folder unavailable (\(error.localizedDescription)); will keep a local copy.")
534 }
153535 }
536 writeSessionStamp(to: temp, name: name)
537 if safe != temp { writeSessionStamp(to: safe, name: name) }
154538
155 // Record only currently-available displays.
539 // Record only currently-available displays, in screen-number order so the
540 // files are deterministic (screen-1, screen-2, …) rather than Set order.
156541 let current = Set(displays.map { $0.id })
157 var useDisplays = enabledDisplays.intersection(current)
158 if useDisplays.isEmpty { useDisplays = current }
542 var enabled = enabledDisplays.intersection(current)
543 if enabled.isEmpty { enabled = current }
544 let displayIDs = orderedDisplays.map { $0.id }.filter { enabled.contains($0) }
159545
160546 let cfg = RecordConfig(
161547 outDir: temp,
162548 label: name,
163 displayIDs: Array(useDisplays),
549 displayIDs: displayIDs,
164550 systemAudio: includeDesktop,
165 micUID: (includeMic && hasMic) ? "default" : nil,
166 cameraUID: nil,
551 micUID: (includeMic && hasMic) ? micUID : nil,
552 cameraUID: (includeCamera && hasCamera) ? cameraUID : nil,
167553 fps: 30,
168554 maxWidth: 3840,
169 bitsPerPixel: 0.05,
555 bitsPerPixel: 0.04,
170556 duration: nil,
171 logPath: temp.appendingPathComponent("recorder.log").path)
557 logPath: safe.appendingPathComponent("recorder.log").path,
558 safeDir: safe,
559 cameraHeight: cameraHeight, cameraFps: cameraFps)
172560
173561 // Start capture FIRST; only wire up REAPER + UI once it's confirmed live, so
174562 // a failure cleans up instead of leaving empty session folders / a stray
175563 // REAPER project behind.
176564 let engine = CaptureEngine(cfg)
565 engine.onEvent = { [weak self] message in
566 Task { @MainActor in self?.reportProblem(message) }
567 }
177568 statusText = "Starting…"
178569 do {
179570 try await engine.start()
180571 } catch {
181572 statusText = nil
182 errorMessage = error.localizedDescription
573 reportProblem(error.localizedDescription)
183574 if "\(error)".contains("declined") { permissionOK = false }
184575 try? FileManager.default.removeItem(at: temp)
576 try? FileManager.default.removeItem(at: safe)
185577 if let z = zenithReady { try? FileManager.default.removeItem(at: z) }
186578 return
187579 }
188580
189581 self.engine = engine
190582 currentSessionDir = temp
583 currentSafeDir = safe
191584 zenithDir = zenithReady
192585 if reaperMidi { setupReaper(name: name) }
586 if includeCamera && hasCamera {
587 DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { [weak self] in self?.applyAntiFlicker() }
588 }
589 markers = []
590 markerCount = 0
193591 isRecording = true
194592 elapsed = 0
195593 startHost = hostSeconds()
......@@ -202,36 +600,118 @@ final class AppController: NSObject, NSApplicationDelegate, ObservableObject {
202600 guard isRecording, let engine else { return }
203601 isRecording = false
204602 stopTick()
603 dismissMarkerOverlay()
205604 updateIcon()
206605 statusText = "Finalizing…"
207606 let temp = currentSessionDir
607 let safe = currentSafeDir
208608 let zenith = zenithDir
609 currentSessionDir = nil
610 currentSafeDir = nil
611 zenithDir = nil
612 postProcessing = true
209613 Task {
210 await engine.stop()
614 defer { self.postProcessing = false }
615 let manifests = await engine.stop()
211616 self.engine = nil
212617 guard let temp else { return }
213 guard let zenith else {
214 self.lastSession = temp.lastPathComponent
215 self.statusText = "Saved locally: \(temp.lastPathComponent)"
618 let name = temp.lastPathComponent
619
620 // A stream with zero frames means a device silently recorded nothing
621 // (e.g. a mic that never delivered a sample) — say so loudly.
622 let empty = manifests.filter { $0.frames == 0 }.map { $0.name }
623 if !empty.isEmpty {
624 self.reportProblem(
625 "No data was recorded from: \(empty.joined(separator: ", ")). "
626 + "Check the device before the next session.")
627 }
628
629 // The session may span two local dirs (scratch video + internal audio),
630 // and either may have vanished mid-recording.
631 var dirs = [temp]
632 if let safe, safe != temp { dirs.append(safe) }
633 dirs = dirs.filter { FileManager.default.fileExists(atPath: $0.path) }
634 guard !dirs.isEmpty else {
635 self.statusText = nil
636 self.reportProblem("Session \(name): no local files survived — nothing to archive.")
216637 return
217638 }
218 self.statusText = "Archiving to zenith…"
219 guard await self.ensureZenithMounted() else {
220 self.lastSession = temp.lastPathComponent
221 self.statusText = "zenith offline — kept local copy"
222 self.errorMessage =
223 "zenith not mounted; files remain in /Temp/\(temp.lastPathComponent)."
639
640 self.statusText = "Compressing video…"
641 for dir in dirs { await self.compress(dir) }
642
643 let mounted = await self.ensureZenithMounted()
644 var target = zenith
645 if target == nil, mounted {
646 // zenith wasn't there at start but is now — archive after all.
647 let z = self.zenithSessionDir(name)
648 if (try? FileManager.default.createDirectory(at: z, withIntermediateDirectories: true))
649 != nil
650 {
651 target = z
652 }
653 }
654 guard mounted, let zenith = target else {
655 self.keepLocal(dirs: dirs, name: name, why: "zenith is offline")
224656 return
225657 }
226 let ok = await self.archive(temp: temp, zenith: zenith)
227 self.lastSession = zenith.lastPathComponent
228 if ok {
229 self.statusText = "Saved: \(zenith.lastPathComponent)"
658
659 self.statusText = "Archiving to zenith…"
660 var allOK = true
661 for dir in dirs {
662 if !(await self.archiveVerified(from: dir, to: zenith)) { allOK = false }
663 }
664 if allOK {
665 for dir in dirs { try? FileManager.default.removeItem(at: dir) }
666 self.writeSequence(in: zenith)
667 self.lastSession = zenith.lastPathComponent
668 self.lastSessionURL = zenith
669 // Transcript runs after archiving (into the final folder) so "Saved"
670 // isn't held up by a long transcription.
671 self.statusText = "Transcribing session…"
672 await self.transcribeSession(zenith)
673 self.statusText = nil
674 if self.unknownSpeakers(in: zenith) { self.openSpeakersReview() }
230675 } else {
231 self.statusText = "Archive failed — kept local copy"
232 self.errorMessage = "rsync to zenith failed; files remain in /Temp/\(temp.lastPathComponent)."
676 self.keepLocal(dirs: dirs, name: name, why: "copying to zenith kept failing")
677 }
678 }
679 }
680
681 /// Archive failed: consolidate the session into one local folder and leave it
682 /// for the reconciler, which retries whenever zenith comes back.
683 private func keepLocal(dirs: [URL], name: String, why: String) {
684 let home = dirs[0]
685 for dir in dirs.dropFirst() { mergeDir(dir, into: home) }
686 lastSession = name
687 lastSessionURL = home
688 statusText = "Saved locally — will archive when zenith returns"
689 reportProblem(
690 "Session \(name) saved locally (\(why)). It will archive automatically once "
691 + "zenith is reachable; files: \(home.path)")
692 }
693
694 private func mergeDir(_ src: URL, into dst: URL) {
695 guard
696 let items = try? FileManager.default.contentsOfDirectory(
697 at: src, includingPropertiesForKeys: nil)
698 else { return }
699 // These exist in both dirs by design; either copy is fine.
700 let duplicates: Set<String> = ["session.json", "sync.json", "markers.json", "recorder.log"]
701 var allMoved = true
702 for item in items {
703 let to = dst.appendingPathComponent(item.lastPathComponent)
704 if FileManager.default.fileExists(atPath: to.path) {
705 if duplicates.contains(item.lastPathComponent) {
706 try? FileManager.default.removeItem(at: item)
707 } else {
708 allMoved = false
709 }
710 continue
233711 }
712 do { try FileManager.default.moveItem(at: item, to: to) } catch { allMoved = false }
234713 }
714 if allMoved { try? FileManager.default.removeItem(at: src) }
235715 }
236716
237717 func toggle() { isRecording ? stop() : start() }
......@@ -247,9 +727,17 @@ final class AppController: NSObject, NSApplicationDelegate, ObservableObject {
247727 }
248728
249729 private func updateIcon() {
250 let name = isRecording ? "stop.circle.fill" : "record.circle"
730 let name: String
731 if errorMessage != nil {
732 name = isRecording ? "stop.circle.fill" : "exclamationmark.triangle.fill"
733 } else {
734 name = isRecording ? "stop.circle.fill" : "record.circle"
735 }
251736 let image = NSImage(systemSymbolName: name, accessibilityDescription: "Clover Recorder")
252 if isRecording {
737 if errorMessage != nil {
738 image?.isTemplate = false
739 statusItem?.button?.contentTintColor = .systemOrange
740 } else if isRecording {
253741 image?.isTemplate = false
254742 statusItem?.button?.contentTintColor = .systemRed
255743 } else {
......@@ -267,13 +755,32 @@ final class AppController: NSObject, NSApplicationDelegate, ObservableObject {
267755 let name = stamp("yyyy-MM-dd_HH.mm", date)
268756 let taken =
269757 FileManager.default.fileExists(atPath: tempRoot.appendingPathComponent(name).path)
270 || FileManager.default.fileExists(atPath: zenithSessionDir(name).path)
758 || FileManager.default.fileExists(atPath: recoveryRoot.appendingPathComponent(name).path)
759 || (zenithMounted() && FileManager.default.fileExists(atPath: zenithSessionDir(name).path))
271760 if !taken { return name }
272761 date = date.addingTimeInterval(60)
273762 }
274763 return stamp("yyyy-MM-dd_HH.mm.ss") // fallback, should never hit
275764 }
276765
766 /// Dropped into every local session dir so a stranded session can be routed
767 /// to its archive home later (the folder name alone doesn't say Sessions vs
768 /// Journal). Its presence is also what marks a folder as reconcilable.
769 private struct SessionStamp: Codable {
770 let name: String
771 let destination: String
772 let created: String
773 }
774
775 private func writeSessionStamp(to dir: URL, name: String) {
776 let stamp = SessionStamp(
777 name: name, destination: destination,
778 created: ISO8601DateFormatter().string(from: Date()))
779 if let data = try? JSONEncoder().encode(stamp) {
780 try? data.write(to: dir.appendingPathComponent("session.json"))
781 }
782 }
783
277784 func copyReference() {
278785 guard let ref = lastSession else { return }
279786 NSPasteboard.general.clearContents()
......@@ -281,9 +788,11 @@ final class AppController: NSObject, NSApplicationDelegate, ObservableObject {
281788 statusText = "Copied \(ref)"
282789 }
283790
284 private func zenithSessionDir(_ name: String) -> URL {
285 archiveRoot.appendingPathComponent(stamp("yyyy"))
286 .appendingPathComponent(destination)
791 private func zenithSessionDir(_ name: String, destination: String? = nil) -> URL {
792 // Year comes from the session name, not the clock — a session archived (or
793 // reconciled) after midnight or months later still lands in its own year.
794 archiveRoot.appendingPathComponent(String(name.prefix(4)))
795 .appendingPathComponent(destination ?? self.destination)
287796 .appendingPathComponent(name)
288797 }
289798
......@@ -317,14 +826,311 @@ final class AppController: NSObject, NSApplicationDelegate, ObservableObject {
317826
318827 // MARK: Archive
319828
320 /// rsync the local scratch session into its NAS home, then remove the scratch
321 /// copy on success. Runs off the main actor so the UI stays responsive.
322 private func archive(temp: URL, zenith: URL) async -> Bool {
829 /// rsync a local session dir into its NAS home with retries, then verify
830 /// every file actually arrived (same size) before the caller deletes
831 /// anything. Never deletes the source itself.
832 private func archiveVerified(from dir: URL, to zenith: URL) async -> Bool {
833 for attempt in 1...3 {
834 if attempt > 1 {
835 statusText = "Archiving to zenith… (retry \(attempt))"
836 _ = await ensureZenithMounted()
837 try? await Task.sleep(nanoseconds: UInt64(attempt) * 2_000_000_000)
838 }
839 let status = await runProcess(
840 "/usr/bin/rsync",
841 ["-a", "--partial", "--timeout=120", dir.path + "/", zenith.path + "/"])
842 if status == 0, verifyCopied(from: dir, to: zenith) { return true }
843 }
844 return false
845 }
846
847 /// Every regular file under `dir` exists on zenith with the same byte size.
848 /// rsync's exit code alone once let a 0-byte file pass as "archived".
849 private func verifyCopied(from dir: URL, to zenith: URL) -> Bool {
850 let fm = FileManager.default
851 guard let walker = fm.enumerator(at: dir, includingPropertiesForKeys: [.isRegularFileKey])
852 else { return false }
853 for case let file as URL in walker {
854 guard (try? file.resourceValues(forKeys: [.isRegularFileKey]))?.isRegularFile == true else {
855 continue
856 }
857 let rel = String(file.path.dropFirst(dir.path.count))
858 let src = (try? fm.attributesOfItem(atPath: file.path))?[.size] as? Int64
859 let dst = (try? fm.attributesOfItem(atPath: zenith.path + rel))?[.size] as? Int64
860 if src == nil || dst == nil || src != dst { return false }
861 }
862 return true
863 }
864
865 /// Run a command off the main actor, returning its exit status (-1 if it
866 /// couldn't launch).
867 private func runProcess(_ path: String, _ args: [String]) async -> Int32 {
868 await withCheckedContinuation { (cont: CheckedContinuation<Int32, Never>) in
869 DispatchQueue.global(qos: .utility).async {
870 let p = Process()
871 p.executableURL = URL(fileURLWithPath: path)
872 p.arguments = args
873 p.standardOutput = Pipe()
874 p.standardError = Pipe()
875 do {
876 try p.run()
877 p.waitUntilExit()
878 } catch {
879 cont.resume(returning: -1)
880 return
881 }
882 cont.resume(returning: p.terminationStatus)
883 }
884 }
885 }
886
887 // MARK: Sequencer project (.sq)
888
889 /// Write `<session>/<session>.sq` — a Clover Sequencer project (see the
890 /// top-level `writeSequenceFile`).
891 private func writeSequence(in dir: URL) {
892 if let err = writeSequenceFile(in: dir) { logErr(err) }
893 }
894
895 // MARK: Stranded-session reconciler
896
897 /// Archive any local session folders left behind by a crash, an offline
898 /// zenith, or a failed copy. Runs at launch and every 15 minutes while idle;
899 /// only folders carrying a session.json stamp are touched.
900 private func reconcilePending() async {
901 guard !isRecording, !postProcessing, !reconciling else { return }
902 reconciling = true
903 defer { reconciling = false }
904
905 var pending: [URL] = []
906 for base in [tempRoot, recoveryRoot] {
907 let dirs =
908 (try? FileManager.default.contentsOfDirectory(at: base, includingPropertiesForKeys: nil))
909 ?? []
910 pending += dirs.filter {
911 FileManager.default.fileExists(atPath: $0.appendingPathComponent("session.json").path)
912 }
913 }
914 guard !pending.isEmpty else { return }
915 guard await ensureZenithMounted() else { return } // try again next pass
916
917 for dir in pending {
918 guard !isRecording, !postProcessing else { return }
919 guard let data = try? Data(contentsOf: dir.appendingPathComponent("session.json")),
920 let stamp = try? JSONDecoder().decode(SessionStamp.self, from: data)
921 else { continue }
922
923 // No sync.json means the recorder died mid-session: remux the fragmented
924 // media so everything downstream can read it.
925 let crashed = !FileManager.default.fileExists(
926 atPath: dir.appendingPathComponent("sync.json").path)
927 if crashed { await finalizeCrashedMedia(in: dir) }
928
929 let zenith = zenithSessionDir(stamp.name, destination: stamp.destination)
930 try? FileManager.default.createDirectory(at: zenith, withIntermediateDirectories: true)
931 statusText = "Archiving recovered session \(stamp.name)…"
932 if await archiveVerified(from: dir, to: zenith) {
933 try? FileManager.default.removeItem(at: dir)
934 writeSequence(in: zenith)
935 statusText = nil
936 notify(
937 "Recovered session \(stamp.name)\(crashed ? " (interrupted)" : "") archived to zenith.")
938 if !FileManager.default.fileExists(
939 atPath: zenith.appendingPathComponent("transcript.md").path)
940 {
941 await transcribeSession(zenith)
942 }
943 } else {
944 statusText = nil
945 reportProblem(
946 "Couldn't archive recovered session \(stamp.name) — files remain at \(dir.path)")
947 return // zenith is flaky; retry the rest next pass instead of hammering
948 }
949 }
950 }
951
952 /// A crash leaves media ending in movie fragments with no final index; a
953 /// stream-copy remux rebuilds one so any player/tool can read the file.
954 private func finalizeCrashedMedia(in dir: URL) async {
955 guard let ffmpeg = ffmpegPath() else { return }
956 let media =
957 ((try? FileManager.default.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil))
958 ?? [])
959 .filter { ["mov", "m4a"].contains($0.pathExtension) }
960 for file in media {
961 let fixed = file.deletingPathExtension().appendingPathExtension("fixed")
962 .appendingPathExtension(file.pathExtension)
963 let status = await runProcess(
964 ffmpeg,
965 ["-y", "-nostdin", "-loglevel", "error", "-i", file.path, "-c", "copy", fixed.path])
966 let size =
967 ((try? FileManager.default.attributesOfItem(atPath: fixed.path))?[.size] as? Int64) ?? 0
968 if status == 0, size > 1024 {
969 try? FileManager.default.removeItem(at: file)
970 try? FileManager.default.moveItem(at: fixed, to: file)
971 } else {
972 try? FileManager.default.removeItem(at: fixed)
973 }
974 }
975 }
976
977 // MARK: Compression
978
979 /// Re-encode the session's screen videos with x265 (CRF) before archiving.
980 /// Realtime hardware capture trades size for speed; this offline pass (the
981 /// Studio is idle between sessions) shrinks them several-fold while preserving
982 /// exact frame timing, so alignment is unaffected. Falls back to the original
983 /// if ffmpeg is missing or the re-encode looks wrong.
984 private func compress(_ dir: URL) async {
985 guard let ffmpeg = ffmpegPath() else { return }
986 let movs =
987 ((try? FileManager.default.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil))
988 ?? [])
989 .filter { $0.pathExtension == "mov" }
990 .sorted { $0.lastPathComponent < $1.lastPathComponent }
991 if movs.isEmpty { return }
992
993 let durations = streamDurations(in: dir)
994 let total = movs.reduce(0.0) { $0 + (durations[$1.lastPathComponent] ?? 0) }
995 var done = 0.0
996 compressProgress = total > 0 ? 0 : nil
997
998 for mov in movs {
999 let dur = durations[mov.lastPathComponent] ?? 0
1000 let out = mov.deletingPathExtension().appendingPathExtension("x265.mov")
1001 // Light denoise on the webcam only (sensor noise is costly to encode);
1002 // screens are clean and would just lose text crispness.
1003 let filter = mov.lastPathComponent == "cam.mov" ? "hqdn3d=1.5:1.5:6:6" : nil
1004 let ok = await encode(ffmpeg, input: mov, output: out, filter: filter) { secs in
1005 if total > 0 { self.compressProgress = min(1, (done + min(secs, dur)) / total) }
1006 }
1007 done += dur
1008 if total > 0 { compressProgress = min(1, done / total) }
1009
1010 let attrs = try? FileManager.default.attributesOfItem(atPath: out.path)
1011 let outSize = (attrs?[.size] as? Int64) ?? 0
1012 if ok, outSize > 1024 {
1013 try? FileManager.default.removeItem(at: mov)
1014 try? FileManager.default.moveItem(at: out, to: mov)
1015 } else {
1016 try? FileManager.default.removeItem(at: out) // keep the original
1017 }
1018 }
1019 compressProgress = nil
1020 }
1021
1022 /// Per-stream durations from sync.json, so the progress bar knows the total.
1023 private func streamDurations(in dir: URL) -> [String: Double] {
1024 guard let data = try? Data(contentsOf: dir.appendingPathComponent("sync.json")),
1025 let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
1026 let streams = obj["streams"] as? [[String: Any]]
1027 else { return [:] }
1028 var map: [String: Double] = [:]
1029 for s in streams {
1030 if let file = s["file"] as? String, let d = s["durationSeconds"] as? Double { map[file] = d }
1031 }
1032 return map
1033 }
1034
1035 /// Transcribe the session's mic audio into transcript.md (meeting-minutes
1036 /// style, markers interleaved). Skips if there's no mic audio; anything else
1037 /// going wrong is reported, not swallowed.
1038 private func transcribeSession(_ dir: URL) async {
1039 let python = NSHomeDirectory() + "/.clover-whisper/.venv/bin/python"
1040 let mic = dir.appendingPathComponent("mic.m4a")
1041 guard FileManager.default.fileExists(atPath: mic.path) else { return }
1042 guard FileManager.default.isExecutableFile(atPath: python),
1043 let script = Bundle.main.url(forResource: "session_transcript", withExtension: "py")?.path
1044 else {
1045 reportProblem("Transcription skipped — whisper env missing (run setup-dictation.sh).")
1046 return
1047 }
1048 let markers = dir.appendingPathComponent("markers.json")
1049 let markersArg = FileManager.default.fileExists(atPath: markers.path) ? markers.path : "none"
1050 let out = dir.appendingPathComponent("transcript.md").path
1051 let mode = detectSpeakers ? "multi" : "solo"
1052 let result = await runTool(python, [script, mic.path, markersArg, out, dir.lastPathComponent, mode])
1053 if result.status != 0 {
1054 reportProblem(
1055 "Transcription failed for \(dir.lastPathComponent): "
1056 + (result.errorTail.isEmpty ? "exit \(result.status)" : result.errorTail))
1057 }
1058 }
1059
1060 /// Run a command to completion off the main actor, capturing its exit status
1061 /// and the tail of stderr so failures can be reported instead of vanishing.
1062 @discardableResult
1063 private func runTool(_ path: String, _ args: [String]) async -> (status: Int32, errorTail: String)
1064 {
1065 await withCheckedContinuation {
1066 (cont: CheckedContinuation<(status: Int32, errorTail: String), Never>) in
1067 DispatchQueue.global(qos: .utility).async {
1068 let p = Process()
1069 p.executableURL = URL(fileURLWithPath: path)
1070 p.arguments = args
1071 p.environment = cloverToolEnvironment()
1072 p.standardOutput = Pipe()
1073 let err = Pipe()
1074 p.standardError = err
1075 var tail = Data()
1076 err.fileHandleForReading.readabilityHandler = { fh in
1077 tail.append(fh.availableData)
1078 if tail.count > 8192 { tail = tail.suffix(4096) }
1079 }
1080 do {
1081 try p.run()
1082 p.waitUntilExit()
1083 } catch {
1084 err.fileHandleForReading.readabilityHandler = nil
1085 cont.resume(returning: (-1, error.localizedDescription))
1086 return
1087 }
1088 err.fileHandleForReading.readabilityHandler = nil
1089 if let rest = try? err.fileHandleForReading.readToEnd() { tail.append(rest) }
1090 let text = String(decoding: tail, as: UTF8.self)
1091 .trimmingCharacters(in: .whitespacesAndNewlines)
1092 cont.resume(returning: (p.terminationStatus, String(text.suffix(300))))
1093 }
1094 }
1095 }
1096
1097 private func ffmpegPath() -> String? {
1098 let candidates = [
1099 "/etc/profiles/per-user/\(NSUserName())/bin/ffmpeg",
1100 "/run/current-system/sw/bin/ffmpeg",
1101 "/opt/homebrew/bin/ffmpeg",
1102 "/usr/local/bin/ffmpeg",
1103 ]
1104 return candidates.first { FileManager.default.isExecutableFile(atPath: $0) }
1105 }
1106
1107 /// Run one x265 encode, reporting encoded-seconds via ffmpeg's `-progress`.
1108 private func encode(
1109 _ ffmpeg: String, input: URL, output: URL, filter: String? = nil,
1110 onProgress: @escaping (Double) -> Void
1111 ) async -> Bool {
3231112 await withCheckedContinuation { (cont: CheckedContinuation<Bool, Never>) in
3241113 DispatchQueue.global(qos: .utility).async {
3251114 let p = Process()
326 p.executableURL = URL(fileURLWithPath: "/usr/bin/rsync")
327 p.arguments = ["-a", "--exclude=recorder.log", temp.path + "/", zenith.path + "/"]
1115 p.executableURL = URL(fileURLWithPath: ffmpeg)
1116 var args = [
1117 "-y", "-nostats", "-loglevel", "error", "-i", input.path,
1118 "-an", "-c:v", "libx265", "-crf", "24", "-preset", "fast",
1119 "-tag:v", "hvc1", "-fps_mode", "passthrough", "-progress", "pipe:1",
1120 ]
1121 if let filter { args += ["-vf", filter] }
1122 args.append(output.path)
1123 p.arguments = args
1124 let pipe = Pipe()
1125 p.standardOutput = pipe
1126 pipe.fileHandleForReading.readabilityHandler = { fh in
1127 let text = String(decoding: fh.availableData, as: UTF8.self)
1128 for line in text.split(separator: "\n") where line.hasPrefix("out_time_us=") {
1129 if let us = Double(line.dropFirst("out_time_us=".count)) {
1130 DispatchQueue.main.async { onProgress(us / 1_000_000) }
1131 }
1132 }
1133 }
3281134 do {
3291135 try p.run()
3301136 p.waitUntilExit()
......@@ -332,13 +1138,163 @@ final class AppController: NSObject, NSApplicationDelegate, ObservableObject {
3321138 cont.resume(returning: false)
3331139 return
3341140 }
335 let ok = p.terminationStatus == 0
336 if ok { try? FileManager.default.removeItem(at: temp) }
337 cont.resume(returning: ok)
1141 pipe.fileHandleForReading.readabilityHandler = nil
1142 cont.resume(returning: p.terminationStatus == 0)
3381143 }
3391144 }
3401145 }
3411146
1147 // MARK: Speaker review
1148
1149 var lastSessionHasSpeakers: Bool {
1150 guard let dir = lastSessionURL else { return false }
1151 // Only surface review when there's actually an unnamed voice — solo sessions
1152 // write a single known "You" speaker, which needs no review.
1153 return unknownSpeakers(in: dir)
1154 }
1155
1156 private func unknownSpeakers(in dir: URL) -> Bool {
1157 guard let data = try? Data(contentsOf: dir.appendingPathComponent("speakers.json")),
1158 let file = try? JSONDecoder().decode(SpeakersFile.self, from: data)
1159 else { return false }
1160 return file.speakers.contains { $0.unknown }
1161 }
1162
1163 func openSpeakersReview() {
1164 guard let dir = lastSessionURL,
1165 let data = try? Data(contentsOf: dir.appendingPathComponent("speakers.json")),
1166 let file = try? JSONDecoder().decode(SpeakersFile.self, from: data),
1167 !file.speakers.isEmpty
1168 else { return }
1169
1170 let model = SpeakersModel(
1171 speakers: file.speakers, libraryNames: libraryNames(),
1172 audioURL: dir.appendingPathComponent("mic.m4a"))
1173 let view = SpeakersView(
1174 model: model,
1175 onSave: { [weak self] mapping in self?.applySpeakerNames(dir: dir, mapping: mapping) },
1176 onCancel: { [weak self] in self?.closeSpeakersWindow() })
1177
1178 let win = NSWindow(
1179 contentRect: NSRect(x: 0, y: 0, width: 380, height: 300),
1180 styleMask: [.titled, .closable], backing: .buffered, defer: false)
1181 win.title = "Speakers · \(dir.lastPathComponent)"
1182 win.contentViewController = NSHostingController(rootView: view)
1183 win.isReleasedWhenClosed = false
1184 win.center()
1185 speakersWindow = win
1186 NSApp.activate(ignoringOtherApps: true)
1187 win.makeKeyAndOrderFront(nil)
1188 }
1189
1190 private func closeSpeakersWindow() {
1191 speakersWindow?.close()
1192 speakersWindow = nil
1193 }
1194
1195 private func libraryNames() -> [String] {
1196 let lib = NSHomeDirectory() + "/.clover-whisper/voices.json"
1197 guard let data = try? Data(contentsOf: URL(fileURLWithPath: lib)),
1198 let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
1199 let voices = obj["voices"] as? [[String: Any]]
1200 else { return [] }
1201 return voices.compactMap { $0["name"] as? String }
1202 }
1203
1204 private func applySpeakerNames(dir: URL, mapping: [String: String]) {
1205 closeSpeakersWindow()
1206 guard !mapping.isEmpty,
1207 let script = Bundle.main.url(forResource: "relabel", withExtension: "py")?.path
1208 else { return }
1209 let python = NSHomeDirectory() + "/.clover-whisper/.venv/bin/python"
1210 let mapURL = dir.appendingPathComponent("_speaker_mapping.json")
1211 guard let mdata = try? JSONSerialization.data(withJSONObject: mapping) else { return }
1212 try? mdata.write(to: mapURL)
1213 statusText = "Updating speakers…"
1214 Task {
1215 let result = await runTool(python, [script, dir.path, mapURL.path])
1216 try? FileManager.default.removeItem(at: mapURL)
1217 if result.status == 0 {
1218 self.statusText = "Speakers updated ✓"
1219 } else {
1220 self.statusText = nil
1221 self.reportProblem(
1222 "Speaker update failed: "
1223 + (result.errorTail.isEmpty ? "exit \(result.status)" : result.errorTail))
1224 }
1225 }
1226 }
1227
1228 // MARK: Voice enrollment
1229
1230 /// Record ~12 s of your voice and store a voiceprint so transcripts can tell
1231 /// you from other speakers.
1232 func enrollVoice() {
1233 guard !enrolling, !isRecording else { return }
1234 let python = NSHomeDirectory() + "/.clover-whisper/.venv/bin/python"
1235 guard FileManager.default.isExecutableFile(atPath: python) else {
1236 errorMessage = "Dictation env not set up (run setup-dictation.sh)."
1237 return
1238 }
1239 let url = FileManager.default.temporaryDirectory.appendingPathComponent("clover-enroll.wav")
1240 let settings: [String: Any] = [
1241 AVFormatIDKey: kAudioFormatLinearPCM, AVSampleRateKey: 16_000,
1242 AVNumberOfChannelsKey: 1, AVLinearPCMBitDepthKey: 16,
1243 AVLinearPCMIsFloatKey: false, AVLinearPCMIsBigEndianKey: false,
1244 ]
1245 do {
1246 let rec = try AVAudioRecorder(url: url, settings: settings)
1247 rec.record()
1248 enrollRecorder = rec
1249 } catch {
1250 errorMessage = "Couldn't open the mic for enrollment."
1251 return
1252 }
1253 errorMessage = nil
1254 statusText = nil
1255 enrolling = true
1256 enrollSecondsLeft = 12
1257 enrollTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
1258 Task { @MainActor in
1259 guard let self else { return }
1260 self.enrollSecondsLeft -= 1
1261 if self.enrollSecondsLeft <= 0 { self.finishEnroll(url: url, python: python) }
1262 }
1263 }
1264 }
1265
1266 private func finishEnroll(url: URL, python: String) {
1267 enrollTimer?.invalidate()
1268 enrollTimer = nil
1269 enrollRecorder?.stop()
1270 enrollRecorder = nil
1271 enrolling = false
1272 statusText = "Processing voice…"
1273 guard let script = Bundle.main.url(forResource: "enroll", withExtension: "py")?.path else {
1274 return
1275 }
1276 let library = NSHomeDirectory() + "/.clover-whisper/voices.json"
1277 Task {
1278 await runTool(python, [script, url.path, "Clover"])
1279 self.voiceEnrolled = FileManager.default.fileExists(atPath: library)
1280 self.statusText = self.voiceEnrolled ? "Voice enrolled ✓" : "Enrollment failed"
1281 }
1282 }
1283
1284 // MARK: Reveal / copy folder
1285
1286 func openFolder() {
1287 guard let url = lastSessionURL else { return }
1288 NSWorkspace.shared.open(url)
1289 }
1290
1291 func copyPath() {
1292 guard let url = lastSessionURL else { return }
1293 NSPasteboard.general.clearContents()
1294 NSPasteboard.general.setString(url.path, forType: .string)
1295 statusText = "Copied path"
1296 }
1297
3421298 // MARK: zenith mount
3431299
3441300 private func zenithMounted() -> Bool {
......@@ -392,6 +1348,13 @@ struct ContentView: View {
3921348 }
3931349 }
3941350
1351 if controller.isRecording {
1352 Label(
1353 "F14 to drop a marker" + (controller.markerCount > 0 ? " · \(controller.markerCount)" : ""),
1354 systemImage: "mappin.and.ellipse"
1355 ).font(.caption2).foregroundStyle(.secondary)
1356 }
1357
3951358 if !controller.permissionOK {
3961359 Label("Screen Recording permission needed", systemImage: "exclamationmark.triangle")
3971360 .font(.caption).foregroundStyle(.orange)
......@@ -408,42 +1371,138 @@ struct ContentView: View {
4081371
4091372 VStack(alignment: .leading, spacing: 6) {
4101373 Text("Capture").font(.caption).foregroundStyle(.secondary)
411 ForEach(controller.displays, id: \.id) { d in
1374 ForEach(Array(controller.orderedDisplays.enumerated()), id: \.element.id) { idx, d in
4121375 Toggle(
413 displayLabel(d),
1376 displayLabel(idx + 1, d),
4141377 isOn: Binding(
4151378 get: { controller.enabledDisplays.contains(d.id) },
4161379 set: { controller.setDisplay(d.id, on: $0) })
4171380 ).disabled(controller.isRecording)
4181381 }
419 Toggle("Desktop audio", isOn: $controller.includeDesktop).disabled(controller.isRecording)
420 if controller.hasMic {
421 Toggle("Microphone", isOn: $controller.includeMic).disabled(controller.isRecording)
422 } else {
423 Label("No microphone connected", systemImage: "exclamationmark.triangle.fill")
424 .foregroundStyle(.yellow)
1382 if controller.displays.count > 1 {
1383 HStack {
1384 Toggle("Reverse screen order", isOn: $controller.reverseScreens)
1385 .disabled(controller.isRecording)
1386 Spacer()
1387 Button("Identify") { controller.identifyScreens() }
1388 .font(.caption)
1389 }
4251390 }
1391 Toggle("Desktop audio", isOn: $controller.includeDesktop).disabled(controller.isRecording)
1392 deviceRow(
1393 "Microphone", isOn: $controller.includeMic, selection: $controller.micUID,
1394 options: controller.audioInputs, available: controller.hasMic)
1395 deviceRow(
1396 "Camera", isOn: $controller.includeCamera, selection: $controller.cameraUID,
1397 options: controller.cameras, available: controller.hasCamera)
4261398 Toggle("REAPER (MIDI)", isOn: $controller.reaperMidi).disabled(controller.isRecording)
1399 Toggle("Detect multiple speakers", isOn: $controller.detectSpeakers)
1400 .disabled(controller.isRecording)
1401 .help("Off: transcribe as one voice. On: separate and name speakers (for sessions with other people).")
4271402 }
4281403 .toggleStyle(.checkbox)
4291404
430 Spacer()
1405 if controller.includeCamera && controller.hasCamera {
1406 VStack(alignment: .leading, spacing: 6) {
1407 CameraPreview(session: controller.previewSession)
1408 .frame(width: 290, height: 290 / controller.cameraAspect)
1409 .background(Color.black)
1410 .clipShape(RoundedRectangle(cornerRadius: 8))
1411 HStack(spacing: 10) {
1412 Picker("Quality", selection: $controller.cameraHeight) {
1413 Text("1080p").tag(1080)
1414 Text("720p").tag(720)
1415 Text("480p").tag(480)
1416 }
1417 .disabled(controller.isRecording)
1418 Picker("FPS", selection: $controller.cameraFps) {
1419 Text("30").tag(30)
1420 Text("24").tag(24)
1421 Text("15").tag(15)
1422 Text("60").tag(60)
1423 }
1424 .disabled(controller.isRecording)
1425 Spacer()
1426 Button { controller.toggleCameraPopout() } label: {
1427 Image(systemName: "rectangle.on.rectangle")
1428 }
1429 .help("Pop out camera (stays up while recording)")
1430 }
1431 .font(.caption)
4311432
432 if let last = controller.lastSession, !controller.isRecording {
433 Button(action: { controller.copyReference() }) {
434 HStack(spacing: 6) {
435 Image(systemName: "doc.on.doc")
436 Text(last).font(.system(.caption, design: .monospaced))
1433 Picker("Anti-flicker", selection: $controller.cameraAntiFlickerHz) {
1434 Text("Off").tag(0)
1435 Text("60 Hz").tag(60)
1436 Text("50 Hz").tag(50)
4371437 }
1438 .pickerStyle(.segmented)
1439 .font(.caption)
1440 }
1441 }
1442
1443 HStack(spacing: 6) {
1444 Image(
1445 systemName: controller.voiceEnrolled
1446 ? "person.fill.checkmark" : "person.crop.circle.badge.plus"
1447 )
1448 .foregroundStyle(controller.voiceEnrolled ? .green : .secondary)
1449 if controller.enrolling {
1450 Text("Recording voice… \(controller.enrollSecondsLeft)s — keep talking")
1451 .foregroundStyle(.red)
1452 } else {
1453 Text(controller.voiceEnrolled ? "Voice enrolled" : "Enroll voice for speaker labels")
1454 .foregroundStyle(.secondary)
1455 Spacer()
1456 Button(controller.voiceEnrolled ? "Re-enroll" : "Enroll") { controller.enrollVoice() }
1457 .disabled(controller.isRecording)
4381458 }
439 .buttonStyle(.borderless)
440 .help("Copy session reference to clipboard")
4411459 }
442 if let status = controller.statusText {
1460 .font(.caption)
1461
1462 Spacer()
1463
1464 if let p = controller.compressProgress {
1465 VStack(alignment: .leading, spacing: 3) {
1466 Text("Compressing video… \(Int(p * 100))%")
1467 .font(.caption).foregroundStyle(.secondary)
1468 ProgressView(value: p)
1469 }
1470 } else if let status = controller.statusText {
4431471 Text(status).font(.caption).foregroundStyle(.secondary).lineLimit(1)
4441472 }
1473
1474 if let last = controller.lastSession, !controller.isRecording {
1475 HStack(spacing: 8) {
1476 Button(action: { controller.copyReference() }) {
1477 HStack(spacing: 4) {
1478 Image(systemName: "checkmark.circle.fill").foregroundStyle(.green)
1479 Text(last).font(.system(.caption, design: .monospaced))
1480 }
1481 }
1482 .buttonStyle(.borderless)
1483 .help("Copy session reference")
1484 Spacer()
1485 if controller.lastSessionHasSpeakers {
1486 Button("Speakers") { controller.openSpeakersReview() }
1487 }
1488 Button("Open") { controller.openFolder() }
1489 Button("Copy path") { controller.copyPath() }
1490 }
1491 .font(.caption)
1492 }
1493
4451494 if let err = controller.errorMessage {
446 Text(err).font(.caption).foregroundStyle(.red).lineLimit(2)
1495 HStack(alignment: .top, spacing: 6) {
1496 Text(err).font(.caption).foregroundStyle(.red).lineLimit(4)
1497 Spacer()
1498 Button {
1499 controller.errorMessage = nil
1500 } label: {
1501 Image(systemName: "xmark.circle.fill").foregroundStyle(.secondary)
1502 }
1503 .buttonStyle(.borderless)
1504 .help("Dismiss")
1505 }
4471506 }
4481507
4491508 VStack(spacing: 4) {
......@@ -475,10 +1534,32 @@ struct ContentView: View {
4751534 .frame(width: 320)
4761535 }
4771536
478 private func displayLabel(_ d: DisplayInfo) -> String {
479 let idx = controller.displays.firstIndex(where: { $0.id == d.id }) ?? 0
480 let side = controller.displays.count == 2 ? (idx == 0 ? " (left)" : " (right)") : ""
481 return "Screen \(idx + 1)\(side) — \(d.width)×\(d.height)"
1537 /// A capture toggle with an inline device dropdown, so all the checkboxes line
1538 /// up in a column.
1539 @ViewBuilder
1540 private func deviceRow(
1541 _ label: String, isOn: Binding<Bool>, selection: Binding<String?>,
1542 options: [DeviceInfo], available: Bool
1543 ) -> some View {
1544 HStack(spacing: 6) {
1545 Toggle(label, isOn: isOn).disabled(controller.isRecording || !available)
1546 Spacer()
1547 if isOn.wrappedValue && available && options.count > 1 {
1548 Picker("", selection: selection) {
1549 ForEach(options, id: \.uid) { Text($0.name).tag(Optional($0.uid)) }
1550 }
1551 .labelsHidden().font(.caption).frame(maxWidth: 150).disabled(controller.isRecording)
1552 }
1553 }
1554 }
1555
1556 private func displayLabel(_ number: Int, _ d: DisplayInfo) -> String {
1557 let xs = controller.displays.map { $0.x }
1558 var pos = ""
1559 if controller.displays.count > 1 {
1560 if d.x == xs.min() { pos = " · left" } else if d.x == xs.max() { pos = " · right" }
1561 }
1562 return "Screen \(number)\(pos) — \(d.width)×\(d.height)"
4821563 }
4831564
4841565 private func timeString(_ t: TimeInterval) -> String {
src/Recorder/engine/Sources/recorder/Recorder.swift+301-80
......@@ -21,7 +21,9 @@ private func emit(_ message: String) {
2121 let line = "[recorder] " + message + "\n"
2222 FileHandle.standardError.write(Data(line.utf8))
2323 if let logFile {
24 logFile.write(Data(line.utf8))
24 // Throwing variant: the legacy write() raises an ObjC exception if the log's
25 // volume vanishes mid-recording, which would kill the whole process.
26 try? logFile.write(contentsOf: Data(line.utf8))
2527 }
2628}
2729
......@@ -50,6 +52,7 @@ struct DisplayInfo: Codable {
5052struct DeviceInfo: Codable {
5153 let uid: String
5254 let name: String
55 var continuity: Bool = false // iPhone/iPad Continuity device
5356}
5457
5558struct DiscoveredDevices: Codable {
......@@ -78,12 +81,28 @@ enum Devices {
7881 let cameras = AVCaptureDevice.DiscoverySession(
7982 deviceTypes: [.builtInWideAngleCamera, .external, .continuityCamera],
8083 mediaType: .video, position: .unspecified
81 ).devices.map { DeviceInfo(uid: $0.uniqueID, name: $0.localizedName) }
84 ).devices.map {
85 DeviceInfo(
86 uid: $0.uniqueID, name: $0.localizedName, continuity: $0.deviceType == .continuityCamera)
87 }
8288
89 // Base names of Continuity cameras (e.g. "small phone, for small girl") so we
90 // can flag the matching iPhone/iPad mic even when its camera isn't active.
91 let continuityCamBases = cameras
92 .filter { $0.continuity }
93 .map { $0.name.replacingOccurrences(of: " Camera", with: "") }
94 .filter { !$0.isEmpty }
8395 let audioInputs = AVCaptureDevice.DiscoverySession(
8496 deviceTypes: [.microphone, .external],
8597 mediaType: .audio, position: .unspecified
86 ).devices.map { DeviceInfo(uid: $0.uniqueID, name: $0.localizedName) }
98 ).devices.map { dev -> DeviceInfo in
99 // Continuity Capture audio transport types: 'ccwd' (wired) / 'ccwl' (wireless).
100 let tt = dev.transportType
101 let byTransport = tt == 0x6363_7764 || tt == 0x6363_776C
102 let byName = continuityCamBases.contains { dev.localizedName.hasPrefix($0) }
103 return DeviceInfo(
104 uid: dev.uniqueID, name: dev.localizedName, continuity: byTransport || byName)
105 }
87106
88107 return DiscoveredDevices(displays: displays, cameras: cameras, audioInputs: audioInputs)
89108 }
......@@ -142,12 +161,34 @@ struct SessionManifest: Codable {
142161
143162// MARK: - Stream sink protocol
144163
145/// Anything that records one stream to one file and reports where it sat on the
146/// shared host clock, so the engine can align and summarize them uniformly.
164/// Point-in-time view of a stream for the engine's watchdog.
165struct StreamHealth {
166 let name: String
167 let started: Bool // has produced at least one sample in the current part
168 let dead: Bool // rollover exhausted; the stream is permanently lost
169 let lastAppendHost: Double // host seconds of the last successful append (NaN if none)
170}
171
172/// How often movie fragments are flushed. A crash, power cut, or vanishing
173/// volume loses at most this much media instead of the entire (index-less) file.
174let fragmentSeconds = 5.0
175
176/// Give up on a stream after this many rollover attempts — a target that keeps
177/// failing writers instantly would otherwise spray part-files forever.
178let maxParts = 5
179
180/// Anything that records one stream to one file (or, after failures, a series
181/// of part files) and reports where each sat on the shared host clock, so the
182/// engine can align and summarize them uniformly.
147183protocol RecordingStream: AnyObject {
148 var firstPTS: Double { get }
149184 func finish() async
150 func manifest() -> StreamManifest
185 /// One manifest entry per written part, in order.
186 func manifests() -> [StreamManifest]
187 func health() -> StreamHealth
188 /// Abandon the current file and continue into a fresh part inside `dir`
189 /// (used when the volume under the current file disappears). No-op if the
190 /// current file already lives there.
191 func rollover(to dir: URL)
151192}
152193
153194// MARK: - Stream writer (passthrough: audio + camera)
......@@ -158,96 +199,170 @@ protocol RecordingStream: AnyObject {
158199final class StreamWriter: RecordingStream {
159200 let name: String
160201 let kind: String
161 let url: URL
202 private(set) var url: URL
162203 var displayID: UInt32?
163204 var deviceUID: String?
164205 var width: Int?
165206 var height: Int?
166207 var fps: Int?
167208
168 private let writer: AVAssetWriter
169 private let input: AVAssetWriterInput
209 private let fileType: AVFileType
210 private let settings: [String: Any]
211 private let mediaType: AVMediaType
212 private var writer: AVAssetWriter
213 private var input: AVAssetWriterInput
170214 private let lock = NSLock()
171215
172 private(set) var firstPTS: Double = .nan
173 private(set) var lastPTS: Double = .nan
174 private(set) var frames = 0
175 private(set) var dropped = 0
216 private var partFirstPTS: Double = .nan
217 private var partLastPTS: Double = .nan
218 private var partFrames = 0
219 private var partDropped = 0
220 private var lastAppendHost: Double = .nan
176221 private var started = false
177 private var failed = false
222 private var dead = false
223 private var partIndex = 1
224 private var doneParts: [StreamManifest] = []
225 private var rolloverDir: URL?
178226
179227 init(
180228 url: URL, name: String, kind: String, fileType: AVFileType,
181 settings: [String: Any], mediaType: AVMediaType
229 settings: [String: Any], mediaType: AVMediaType, fallbackDir: URL? = nil
182230 ) throws {
183231 self.url = url
184232 self.name = name
185233 self.kind = kind
186 self.writer = try AVAssetWriter(outputURL: url, fileType: fileType)
187 self.input = AVAssetWriterInput(mediaType: mediaType, outputSettings: settings)
188 self.input.expectsMediaDataInRealTime = true
189 if writer.canAdd(input) {
190 writer.add(input)
191 } else {
234 self.fileType = fileType
235 self.settings = settings
236 self.mediaType = mediaType
237 self.rolloverDir = fallbackDir
238 (self.writer, self.input) = try Self.makeWriter(
239 url: url, fileType: fileType, settings: settings, mediaType: mediaType, name: name)
240 }
241
242 private static func makeWriter(
243 url: URL, fileType: AVFileType, settings: [String: Any], mediaType: AVMediaType, name: String
244 ) throws -> (AVAssetWriter, AVAssetWriterInput) {
245 let w = try AVAssetWriter(outputURL: url, fileType: fileType)
246 // Periodic fragments keep the file readable up to the last flush even if
247 // we never get to finalize it (crash, power loss, disk disconnect).
248 w.movieFragmentInterval = CMTime(seconds: fragmentSeconds, preferredTimescale: 600)
249 let i = AVAssetWriterInput(mediaType: mediaType, outputSettings: settings)
250 i.expectsMediaDataInRealTime = true
251 guard w.canAdd(i) else {
192252 throw RecorderError("cannot add \(mediaType.rawValue) input for \(name)")
193253 }
254 w.add(i)
255 return (w, i)
194256 }
195257
196258 func append(_ sb: CMSampleBuffer) {
197259 lock.lock()
198260 defer { lock.unlock() }
199 if failed { return }
261 if dead { return }
200262
201263 let pts = CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sb))
202264
203265 if !started {
204266 guard writer.startWriting() else {
205 failed = true
206267 logErr("\(name): startWriting failed: \(writer.error?.localizedDescription ?? "?")")
268 rolloverLocked()
207269 return
208270 }
209271 writer.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(sb))
210 firstPTS = pts
272 partFirstPTS = pts
211273 started = true
212274 }
213275
214276 if input.isReadyForMoreMediaData {
215277 if input.append(sb) {
216 frames += 1
217 lastPTS = pts
278 partFrames += 1
279 partLastPTS = pts
280 lastAppendHost = hostSeconds()
218281 } else {
219 dropped += 1
282 partDropped += 1
220283 if writer.status == .failed {
221 failed = true
222284 logErr("\(name): append failed: \(writer.error?.localizedDescription ?? "?")")
285 rolloverLocked()
223286 }
224287 }
225288 } else {
226 dropped += 1
289 partDropped += 1
227290 }
228291 }
229292
293 /// Abandon the current writer and continue into a fresh part file. The old
294 /// file keeps whatever fragments reached disk (recoverable when the volume
295 /// returns). Caller must hold `lock`.
296 private func rolloverLocked() {
297 if started { doneParts.append(partManifestLocked()) }
298 if writer.status == .writing {
299 // Proactive roll (volume vanished from under a healthy writer): try to
300 // finalize in the background; we don't wait on a possibly-dead disk.
301 input.markAsFinished()
302 writer.finishWriting {}
303 }
304 started = false
305 partFirstPTS = .nan
306 partLastPTS = .nan
307 partFrames = 0
308 partDropped = 0
309 partIndex += 1
310 guard partIndex <= maxParts, let dir = rolloverDir else {
311 dead = true
312 logErr("\(name): stream lost (no rollover target or too many failures)")
313 return
314 }
315 let next = dir.appendingPathComponent("\(name)-\(partIndex).\(url.pathExtension)")
316 do {
317 try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
318 (writer, input) = try Self.makeWriter(
319 url: next, fileType: fileType, settings: settings, mediaType: mediaType, name: name)
320 url = next
321 logErr("\(name): rolled over to \(next.path)")
322 } catch {
323 dead = true
324 logErr("\(name): rollover failed (\(error.localizedDescription)) — stream lost")
325 }
326 }
327
328 func rollover(to dir: URL) {
329 lock.lock()
330 defer { lock.unlock() }
331 guard !dead, !url.deletingLastPathComponent().path.hasPrefix(dir.path) else { return }
332 rolloverDir = dir
333 rolloverLocked()
334 }
335
336 func health() -> StreamHealth {
337 lock.lock()
338 defer { lock.unlock() }
339 return StreamHealth(name: name, started: started, dead: dead, lastAppendHost: lastAppendHost)
340 }
341
230342 func finish() async {
231343 lock.lock()
232 let didStart = started
233 if didStart { input.markAsFinished() }
344 let w = writer
345 let i = input
346 let finalize = started && w.status == .writing
347 if finalize { i.markAsFinished() }
234348 lock.unlock()
235349
236 guard didStart else {
237 logErr("\(name): no samples captured, nothing written")
350 guard finalize else {
351 if doneParts.isEmpty { logErr("\(name): no samples captured, nothing written") }
238352 return
239353 }
240354 await withCheckedContinuation { (cont: CheckedContinuation<Void, Never>) in
241 writer.finishWriting { cont.resume() }
355 w.finishWriting { cont.resume() }
242356 }
243 if writer.status == .failed {
244 logErr("\(name): finishWriting failed: \(writer.error?.localizedDescription ?? "?")")
357 if w.status == .failed {
358 logErr("\(name): finishWriting failed: \(w.error?.localizedDescription ?? "?")")
245359 }
246360 }
247361
248 func manifest() -> StreamManifest {
362 /// Caller must hold `lock`.
363 private func partManifestLocked() -> StreamManifest {
249364 StreamManifest(
250 name: name,
365 name: partIndex == 1 ? name : "\(name)-\(partIndex)",
251366 file: url.lastPathComponent,
252367 kind: kind,
253368 displayID: displayID,
......@@ -255,14 +370,22 @@ final class StreamWriter: RecordingStream {
255370 width: width,
256371 height: height,
257372 fps: fps,
258 firstSampleHostSeconds: firstPTS,
259 lastSampleHostSeconds: lastPTS,
260 durationSeconds: (firstPTS.isNaN || lastPTS.isNaN) ? 0 : (lastPTS - firstPTS),
261 frames: frames,
262 dropped: dropped,
373 firstSampleHostSeconds: partFirstPTS,
374 lastSampleHostSeconds: partLastPTS,
375 durationSeconds: (partFirstPTS.isNaN || partLastPTS.isNaN) ? 0 : (partLastPTS - partFirstPTS),
376 frames: partFrames,
377 dropped: partDropped,
263378 repeated: 0,
264379 offsetSeconds: 0)
265380 }
381
382 func manifests() -> [StreamManifest] {
383 lock.lock()
384 defer { lock.unlock() }
385 var all = doneParts
386 if started || all.isEmpty { all.append(partManifestLocked()) }
387 return all
388 }
266389}
267390
268391struct RecorderError: Error, CustomStringConvertible, LocalizedError {
......@@ -284,15 +407,16 @@ struct RecorderError: Error, CustomStringConvertible, LocalizedError {
284407final class CFRVideoWriter: RecordingStream {
285408 let name: String
286409 let kind = "screen"
287 let url: URL
410 private(set) var url: URL
288411 var displayID: UInt32?
289412 let width: Int
290413 let height: Int
291414 let fps: Int
292415
293 private let writer: AVAssetWriter
294 private let input: AVAssetWriterInput
295 private let adaptor: AVAssetWriterInputPixelBufferAdaptor
416 private let settings: [String: Any]
417 private var writer: AVAssetWriter
418 private var input: AVAssetWriterInput
419 private var adaptor: AVAssetWriterInputPixelBufferAdaptor
296420 private let queue = DispatchQueue(label: "clover.cfr")
297421 private let lock = NSLock()
298422
......@@ -300,43 +424,71 @@ final class CFRVideoWriter: RecordingStream {
300424 private var lastEmitted: CVPixelBuffer?
301425 private var timer: DispatchSourceTimer?
302426
303 private(set) var firstPTS = Double.nan
427 private var partFirstPTS = Double.nan
304428 private var lastPTS = Double.nan
305 private(set) var frames = 0
306 private(set) var repeated = 0
307 private(set) var dropped = 0
429 private var lastEmitHost = Double.nan
430 private var frames = 0
431 private var repeated = 0
432 private var dropped = 0
308433 private var started = false
309 private var failed = false
310
311 init(url: URL, name: String, width: Int, height: Int, fps: Int, bitrate: Int) throws {
434 private var dead = false
435 private var partIndex = 1
436 private var doneParts: [StreamManifest] = []
437 private var rolloverDir: URL?
438
439 /// Longest gap between emitted frames while the screen is static. Real changes
440 /// emit immediately at full rate; this just keeps the timeline progressing so
441 /// a frozen screen costs ~1 fps instead of 30.
442 private let keepAliveSeconds = 1.0
443
444 init(url: URL, name: String, width: Int, height: Int, fps: Int, bitrate: Int, fallbackDir: URL? = nil)
445 throws
446 {
312447 self.url = url
313448 self.name = name
314449 self.width = width
315450 self.height = height
316451 self.fps = fps
452 self.rolloverDir = fallbackDir
317453
318 let settings: [String: Any] = [
454 // Screen content is highly compressible: a long keyframe interval lets a
455 // nearly-static screen cost almost nothing (the periodic keyframes were the
456 // bulk of the size before), and frame reordering (B-frames) tightens it
457 // further. Average bitrate is just a ceiling for busy moments.
458 self.settings = [
319459 AVVideoCodecKey: AVVideoCodecType.hevc,
320460 AVVideoWidthKey: width,
321461 AVVideoHeightKey: height,
322462 AVVideoCompressionPropertiesKey: [
323463 AVVideoAverageBitRateKey: bitrate,
324464 AVVideoExpectedSourceFrameRateKey: fps,
325 AVVideoMaxKeyFrameIntervalKey: fps * 2,
465 AVVideoMaxKeyFrameIntervalKey: fps * 10,
466 AVVideoMaxKeyFrameIntervalDurationKey: 10.0,
467 AVVideoAllowFrameReorderingKey: true,
326468 ],
327469 ]
328 writer = try AVAssetWriter(outputURL: url, fileType: .mov)
329 input = AVAssetWriterInput(mediaType: .video, outputSettings: settings)
330 input.expectsMediaDataInRealTime = true
331 adaptor = AVAssetWriterInputPixelBufferAdaptor(
332 assetWriterInput: input,
470 (self.writer, self.input, self.adaptor) = try Self.makeWriter(
471 url: url, settings: settings, width: width, height: height, name: name)
472 }
473
474 private static func makeWriter(
475 url: URL, settings: [String: Any], width: Int, height: Int, name: String
476 ) throws -> (AVAssetWriter, AVAssetWriterInput, AVAssetWriterInputPixelBufferAdaptor) {
477 let w = try AVAssetWriter(outputURL: url, fileType: .mov)
478 // Fragments bound the loss from a crash/power cut/vanishing disk to seconds.
479 w.movieFragmentInterval = CMTime(seconds: fragmentSeconds, preferredTimescale: 600)
480 let i = AVAssetWriterInput(mediaType: .video, outputSettings: settings)
481 i.expectsMediaDataInRealTime = true
482 let a = AVAssetWriterInputPixelBufferAdaptor(
483 assetWriterInput: i,
333484 sourcePixelBufferAttributes: [
334485 kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
335486 kCVPixelBufferWidthKey as String: width,
336487 kCVPixelBufferHeightKey as String: height,
337488 ])
338 guard writer.canAdd(input) else { throw RecorderError("cannot add video input for \(name)") }
339 writer.add(input)
489 guard w.canAdd(i) else { throw RecorderError("cannot add video input for \(name)") }
490 w.add(i)
491 return (w, i, a)
340492 }
341493
342494 /// Latest frame from ScreenCaptureKit; retained until replaced (SCK won't
......@@ -361,39 +513,97 @@ final class CFRVideoWriter: RecordingStream {
361513 lock.lock()
362514 let pb = latest
363515 lock.unlock()
364 guard let pb else { return } // no frame captured yet
516 guard let pb, !dead else { return }
517
518 let now = hostSeconds()
519 let changed = pb !== lastEmitted
520
521 // Once running, drop identical frames unless it's time for a keep-alive.
522 if started, !changed, !lastEmitHost.isNaN, now - lastEmitHost < keepAliveSeconds {
523 return
524 }
365525
366 let pts = hostSeconds()
367526 if !started {
368527 guard writer.startWriting() else {
369 failed = true
370528 logErr("\(name): startWriting failed: \(writer.error?.localizedDescription ?? "?")")
529 rolloverOnQueue()
371530 return
372531 }
373 writer.startSession(atSourceTime: CMTime(seconds: pts, preferredTimescale: 1_000_000))
374 firstPTS = pts
532 writer.startSession(atSourceTime: CMTime(seconds: now, preferredTimescale: 1_000_000))
533 partFirstPTS = now
375534 started = true
376535 }
377 guard !failed else { return }
378536 guard input.isReadyForMoreMediaData else {
379537 dropped += 1
380538 return
381539 }
382 let time = CMTime(seconds: pts, preferredTimescale: 1_000_000)
540 let time = CMTime(seconds: now, preferredTimescale: 1_000_000)
383541 if adaptor.append(pb, withPresentationTime: time) {
384542 frames += 1
385 lastPTS = pts
386 if pb === lastEmitted { repeated += 1 }
543 lastPTS = now
544 lastEmitHost = now
545 if !changed { repeated += 1 }
387546 lastEmitted = pb
388547 } else {
389548 dropped += 1
390549 if writer.status == .failed {
391 failed = true
392550 logErr("\(name): append failed: \(writer.error?.localizedDescription ?? "?")")
551 rolloverOnQueue()
393552 }
394553 }
395554 }
396555
556 /// Abandon the current writer and continue into a fresh part file. Must run
557 /// on `queue`. The retained `latest` frame lives in memory, so the new part
558 /// picks up on the very next tick.
559 private func rolloverOnQueue() {
560 if started { doneParts.append(partManifestOnQueue()) }
561 if writer.status == .writing {
562 input.markAsFinished()
563 writer.finishWriting {} // fire-and-forget: the volume may be gone
564 }
565 started = false
566 partFirstPTS = .nan
567 lastPTS = .nan
568 frames = 0
569 repeated = 0
570 dropped = 0
571 lastEmitted = nil // force the next tick to emit a frame immediately
572 partIndex += 1
573 guard partIndex <= maxParts, let dir = rolloverDir else {
574 dead = true
575 logErr("\(name): stream lost (no rollover target or too many failures)")
576 return
577 }
578 let next = dir.appendingPathComponent("\(name)-\(partIndex).mov")
579 do {
580 try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
581 (writer, input, adaptor) = try Self.makeWriter(
582 url: next, settings: settings, width: width, height: height, name: name)
583 url = next
584 logErr("\(name): rolled over to \(next.path)")
585 } catch {
586 dead = true
587 logErr("\(name): rollover failed (\(error.localizedDescription)) — stream lost")
588 }
589 }
590
591 func rollover(to dir: URL) {
592 queue.async { [weak self] in
593 guard let self, !self.dead,
594 !self.url.deletingLastPathComponent().path.hasPrefix(dir.path)
595 else { return }
596 self.rolloverDir = dir
597 self.rolloverOnQueue()
598 }
599 }
600
601 func health() -> StreamHealth {
602 queue.sync {
603 StreamHealth(name: name, started: started, dead: dead, lastAppendHost: lastEmitHost)
604 }
605 }
606
397607 func finish() async {
398608 timer?.cancel()
399609 timer = nil
......@@ -401,8 +611,8 @@ final class CFRVideoWriter: RecordingStream {
401611 await withCheckedContinuation { (cont: CheckedContinuation<Void, Never>) in
402612 queue.async { cont.resume() }
403613 }
404 guard started else {
405 logErr("\(name): no frames captured, nothing written")
614 guard started, writer.status == .writing else {
615 if doneParts.isEmpty { logErr("\(name): no frames captured, nothing written") }
406616 return
407617 }
408618 input.markAsFinished()
......@@ -414,9 +624,10 @@ final class CFRVideoWriter: RecordingStream {
414624 }
415625 }
416626
417 func manifest() -> StreamManifest {
627 /// Must run on `queue` (or after it is drained).
628 private func partManifestOnQueue() -> StreamManifest {
418629 StreamManifest(
419 name: name,
630 name: partIndex == 1 ? name : "\(name)-\(partIndex)",
420631 file: url.lastPathComponent,
421632 kind: kind,
422633 displayID: displayID,
......@@ -424,20 +635,29 @@ final class CFRVideoWriter: RecordingStream {
424635 width: width,
425636 height: height,
426637 fps: fps,
427 firstSampleHostSeconds: firstPTS,
638 firstSampleHostSeconds: partFirstPTS,
428639 lastSampleHostSeconds: lastPTS,
429 durationSeconds: (firstPTS.isNaN || lastPTS.isNaN) ? 0 : (lastPTS - firstPTS),
640 durationSeconds: (partFirstPTS.isNaN || lastPTS.isNaN) ? 0 : (lastPTS - partFirstPTS),
430641 frames: frames,
431642 dropped: dropped,
432643 repeated: repeated,
433644 offsetSeconds: 0)
434645 }
646
647 func manifests() -> [StreamManifest] {
648 queue.sync {
649 var all = doneParts
650 if started || all.isEmpty { all.append(partManifestOnQueue()) }
651 return all
652 }
653 }
435654}
436655
437656// MARK: - Sample delegates
438657
439658final class SCOutput: NSObject, SCStreamOutput, SCStreamDelegate {
440659 let label: String
660 var onStreamError: ((String) -> Void)?
441661 private let onScreen: (CMSampleBuffer) -> Void
442662 private let onAudio: ((CMSampleBuffer) -> Void)?
443663
......@@ -483,6 +703,7 @@ final class SCOutput: NSObject, SCStreamOutput, SCStreamDelegate {
483703
484704 func stream(_ stream: SCStream, didStopWithError error: Error) {
485705 logErr("SCStream stopped with error: \(error.localizedDescription)")
706 onStreamError?("\(label): screen capture stopped — \(error.localizedDescription)")
486707 }
487708
488709 static func isComplete(_ sb: CMSampleBuffer) -> Bool {
src/Recorder/engine/Sources/recorder/SequenceExport.swift created+189
......@@ -0,0 +1,189 @@
1import CryptoKit
2import Foundation
3
4// Emits a Clover Sequencer project (`.sq`) from a finished session folder — a
5// convenience duplicate of sync.json + markers.json that lays every recorded
6// stream on its own time-aligned track so a session opens for review in one
7// double-click. The Sequencer's on-disk model (sequencer/Model.swift) decodes
8// tolerantly, so emitting just the fields below is enough; the rest default.
9// Times are seconds throughout, matching the Sequencer model.
10
11struct MarkersFile: Codable { let markers: [Marker] }
12
13// v2 envelope: { formatVersion, project, view }. `view` is portable UI state we
14// leave at defaults (empty object → the Sequencer's ViewState defaults).
15private struct SeqDocument: Codable {
16 let formatVersion: Int
17 let project: SeqProject
18 let view: SeqView
19}
20private struct SeqView: Codable {}
21private struct SeqProject: Codable {
22 let fps: Double
23 let boardWidth: Int
24 let boardHeight: Int
25 let media: [SeqMedia]
26 let tracks: [SeqTrack]
27 let clips: [SeqClip]
28 let markers: [SeqMarker]
29 let preferredTakes: [String]
30}
31private struct SeqMedia: Codable {
32 let id: String
33 let path: String
34 let duration: Double
35 let fps: Double
36 let width: Int
37 let height: Int
38 let hasAudio: Bool
39 let isAudio: Bool
40 let cacheKey: String
41}
42// A track is just a hue now — its index in `tracks` is its number.
43private struct SeqTrack: Codable {
44 let hue: Double
45}
46private struct SeqClip: Codable {
47 let id: String
48 let kind: String
49 let mediaId: String
50 let track: String // TrackRef wire form: "v0", "v1", … (video lane index)
51 let start: Double
52 let srcIn: Double
53 let duration: Double
54 let speed: Double
55 let muted: Bool
56 let fadeIn: Double
57 let fadeOut: Double
58 let linkId: String
59 let newShot: Bool
60}
61private struct SeqMarker: Codable {
62 let id: String
63 let time: Double
64 let label: String
65}
66
67private func isAudioKind(_ k: String) -> Bool { k == "mic" || k == "system-audio" }
68
69/// Sequencer's content-addressed cache key — MUST match MediaPipeline.cacheKey
70/// (sequencer/MediaPipeline.swift): SHA256("path|size|mtime").hex.prefix(16).
71/// On project load Sequencer trusts this stored key rather than recomputing, so
72/// a wrong/empty value collides every clip onto one media's proxy. Computed from
73/// the same file the app will read, so it also lands as a cache hit.
74private func sequencerCacheKey(for url: URL) -> String {
75 let attrs = try? FileManager.default.attributesOfItem(atPath: url.path)
76 let size = (attrs?[.size] as? NSNumber)?.int64Value ?? 0
77 let mtime = (attrs?[.modificationDate] as? Date)?.timeIntervalSince1970 ?? 0
78 let seed = "\(url.path)|\(size)|\(Int(mtime))"
79 let digest = SHA256.hash(data: Data(seed.utf8))
80 return String(digest.map { String(format: "%02x", $0) }.joined().prefix(16))
81}
82
83/// Write `<dir>/<dirname>.sq`. Returns nil on success (or a silent skip when
84/// there's no sync.json — e.g. a crashed session that was never finalized), or
85/// an error message string on a real write failure.
86@discardableResult
87func writeSequenceFile(in dir: URL) -> String? {
88 let out = dir.appendingPathComponent("\(dir.lastPathComponent).sq")
89 guard let data = try? Data(contentsOf: dir.appendingPathComponent("sync.json")),
90 let manifest = try? JSONDecoder().decode(SessionManifest.self, from: data)
91 else { return nil }
92
93 // A 0-frame stream is a dead device (e.g. the 0-byte mic) — skip it rather
94 // than add unplayable media.
95 let streams = manifest.streams.filter { $0.frames > 0 && $0.durationSeconds > 0.01 }
96 guard !streams.isEmpty else { return nil }
97
98 // Rollover parts of one source share a displayID (screens) or deviceUID
99 // (mic/camera), so grouping by that keeps a part on its source's track rather
100 // than spawning a new lane. Desktop audio has neither and is the only one of
101 // its kind, so the kind alone is a stable key.
102 func laneKey(_ s: StreamManifest) -> String {
103 if let d = s.displayID { return "d\(d)" }
104 if let u = s.deviceUID { return "u\(u)" }
105 return s.kind
106 }
107 // Lanes ordered camera → screens → mic → desktop, so the viewer grid reads
108 // top-down the way you'd expect.
109 func priority(_ k: String) -> Int {
110 switch k {
111 case "camera": return 0
112 case "screen": return 1
113 case "mic": return 2
114 default: return 3 // system-audio
115 }
116 }
117
118 var laneOrder: [String] = []
119 var laneStreams: [String: [StreamManifest]] = [:]
120 for s in streams {
121 let key = laneKey(s)
122 if laneStreams[key] == nil { laneOrder.append(key) }
123 laneStreams[key, default: []].append(s)
124 }
125 laneOrder.sort { a, b in
126 let ka = laneStreams[a]![0], kb = laneStreams[b]![0]
127 if priority(ka.kind) != priority(kb.kind) { return priority(ka.kind) < priority(kb.kind) }
128 return ka.name < kb.name
129 }
130
131 let linkId = UUID().uuidString // whole session moves together (multicam)
132 var media: [SeqMedia] = []
133 var tracks: [SeqTrack] = []
134 var clips: [SeqClip] = []
135
136 for (index, key) in laneOrder.enumerated() {
137 // Golden-ratio hue spacing (matches ProjectModel.defaultHue) — adjacent
138 // lanes stay visibly distinct.
139 let hue = (Double(index) * 0.6180339887498949).truncatingRemainder(dividingBy: 1)
140 tracks.append(SeqTrack(hue: hue))
141 let trackRef = "v\(index)" // clips reference the lane by index
142 for s in laneStreams[key]! {
143 let audio = isAudioKind(s.kind)
144 let mediaId = UUID().uuidString
145 let fileURL = dir.appendingPathComponent(s.file)
146 media.append(
147 SeqMedia(
148 id: mediaId, path: fileURL.path,
149 duration: s.durationSeconds, fps: Double(s.fps ?? 30),
150 width: s.width ?? 0, height: s.height ?? 0,
151 hasAudio: audio, isAudio: audio, cacheKey: sequencerCacheKey(for: fileURL)))
152 clips.append(
153 SeqClip(
154 id: UUID().uuidString, kind: audio ? "audio" : "video", mediaId: mediaId,
155 track: trackRef, start: max(0, s.offsetSeconds), srcIn: 0,
156 duration: s.durationSeconds, speed: 1,
157 muted: s.kind == "system-audio", // desktop off by default; mic is the voice
158 fadeIn: 0, fadeOut: 0, linkId: linkId, newShot: false))
159 }
160 }
161
162 // Markers: hostSeconds is directly comparable to sync's tStartHostSeconds,
163 // placing each note at its exact spot on the same timeline as the clips.
164 var markers: [SeqMarker] = []
165 if let mdata = try? Data(contentsOf: dir.appendingPathComponent("markers.json")),
166 let file = try? JSONDecoder().decode(MarkersFile.self, from: mdata)
167 {
168 for m in file.markers {
169 markers.append(
170 SeqMarker(
171 id: UUID().uuidString, time: max(0, m.hostSeconds - manifest.tStartHostSeconds),
172 label: m.text ?? ""))
173 }
174 }
175
176 let fps = streams.first { !isAudioKind($0.kind) }?.fps ?? 30
177 let project = SeqProject(
178 fps: Double(fps), boardWidth: 1920, boardHeight: 1080, media: media, tracks: tracks,
179 clips: clips, markers: markers, preferredTakes: [])
180 let doc = SeqDocument(formatVersion: 2, project: project, view: SeqView())
181 do {
182 let encoder = JSONEncoder()
183 encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
184 try encoder.encode(doc).write(to: out)
185 return nil
186 } catch {
187 return "failed to write \(out.lastPathComponent): \(error.localizedDescription)"
188 }
189}
src/Recorder/engine/Sources/recorder/SpeakersReview.swift created+124
......@@ -0,0 +1,124 @@
1import AVFoundation
2import AppKit
3import SwiftUI
4
5// MARK: - speakers.json model
6
7struct SpeakerInfo: Decodable {
8 let label: String
9 let unknown: Bool
10 let sample: Sample
11 struct Sample: Decodable {
12 let start: Double
13 let end: Double
14 }
15}
16
17struct SpeakersFile: Decodable {
18 let speakers: [SpeakerInfo]
19}
20
21// MARK: - Review model
22
23@MainActor
24final class SpeakersModel: ObservableObject {
25 struct Row: Identifiable {
26 let id = UUID()
27 let original: String
28 let unknown: Bool
29 let start: Double
30 let end: Double
31 var name: String
32 }
33
34 @Published var rows: [Row]
35 let libraryNames: [String]
36 let audioURL: URL
37
38 private var player: AVAudioPlayer?
39 private var stopWork: DispatchWorkItem?
40
41 init(speakers: [SpeakerInfo], libraryNames: [String], audioURL: URL) {
42 rows = speakers.map {
43 Row(original: $0.label, unknown: $0.unknown, start: $0.sample.start, end: $0.sample.end,
44 name: $0.label)
45 }
46 self.libraryNames = libraryNames
47 self.audioURL = audioURL
48 }
49
50 func play(_ row: Row) {
51 stopWork?.cancel()
52 guard let p = try? AVAudioPlayer(contentsOf: audioURL) else { return }
53 p.prepareToPlay()
54 p.currentTime = row.start
55 p.play()
56 player = p
57 let work = DispatchWorkItem { [weak self] in self?.player?.stop() }
58 stopWork = work
59 DispatchQueue.main.asyncAfter(deadline: .now() + max(0.6, row.end - row.start), execute: work)
60 }
61
62 /// Only the speakers whose name was actually changed.
63 func mapping() -> [String: String] {
64 var m: [String: String] = [:]
65 for r in rows {
66 let n = r.name.trimmingCharacters(in: .whitespaces)
67 if !n.isEmpty && n != r.original { m[r.original] = n }
68 }
69 return m
70 }
71}
72
73// MARK: - Review view
74
75struct SpeakersView: View {
76 @ObservedObject var model: SpeakersModel
77 let onSave: ([String: String]) -> Void
78 let onCancel: () -> Void
79
80 var body: some View {
81 VStack(alignment: .leading, spacing: 12) {
82 Text("Who's speaking?").font(.headline)
83 Text("Name each voice — anyone you name is remembered and auto-labelled next time.")
84 .font(.caption).foregroundStyle(.secondary)
85
86 ForEach($model.rows) { $row in
87 HStack(spacing: 10) {
88 Button { model.play(row) } label: {
89 Image(systemName: "play.circle.fill").font(.title2)
90 }
91 .buttonStyle(.borderless)
92 .help("Hear this voice")
93
94 VStack(alignment: .leading, spacing: 1) {
95 Text(row.original).font(.caption2).foregroundStyle(.secondary)
96 TextField("Name", text: $row.name)
97 .textFieldStyle(.roundedBorder).frame(width: 200)
98 }
99
100 if !model.libraryNames.isEmpty {
101 Menu {
102 ForEach(model.libraryNames, id: \.self) { name in
103 Button(name) { row.name = name }
104 }
105 } label: {
106 Image(systemName: "person.crop.circle")
107 }
108 .menuStyle(.borderlessButton).frame(width: 28)
109 .help("Pick a known person")
110 }
111 }
112 }
113
114 HStack {
115 Button("Cancel") { onCancel() }
116 Spacer()
117 Button("Save") { onSave(model.mapping()) }.keyboardShortcut(.defaultAction)
118 }
119 .padding(.top, 6)
120 }
121 .padding(16)
122 .frame(width: 380)
123 }
124}
src/Recorder/engine/Sources/recorder/main.swift+22-1
......@@ -18,6 +18,7 @@ import Foundation
1818// --max-width N clamp the longest screen side, px (default 3840; 0 = native)
1919// --bpp F HEVC bits per pixel-frame (default 0.05)
2020// --duration S auto-stop after S seconds (otherwise runs until SIGINT/SIGTERM)
21// --safe DIR internal-SSD dir for audio + rescued streams (default: --out DIR)
2122
2223func fail(_ message: String) -> Never {
2324 logErr(message)
......@@ -58,6 +59,9 @@ func parseRecordConfig(_ args: [String]) -> RecordConfig {
5859 var bpp = 0.05
5960 var duration: Double?
6061 var logPath: String?
62 var safeDir: URL?
63 var cameraHeight = 1080
64 var cameraFps = 30
6165
6266 var i = 0
6367 func next(_ flag: String) -> String {
......@@ -82,6 +86,9 @@ func parseRecordConfig(_ args: [String]) -> RecordConfig {
8286 case "--bpp": bpp = Double(next(arg)) ?? 0.05
8387 case "--duration": duration = Double(next(arg))
8488 case "--log": logPath = next(arg)
89 case "--safe": safeDir = URL(fileURLWithPath: next(arg))
90 case "--camera-height": cameraHeight = Int(next(arg)) ?? 1080
91 case "--camera-fps": cameraFps = Int(next(arg)) ?? 30
8592 default: fail("unknown option: \(arg)")
8693 }
8794 i += 1
......@@ -91,7 +98,8 @@ func parseRecordConfig(_ args: [String]) -> RecordConfig {
9198 return RecordConfig(
9299 outDir: outDir, label: label, displayIDs: displayIDs, systemAudio: systemAudio,
93100 micUID: micUID, cameraUID: cameraUID, fps: fps, maxWidth: maxWidth, bitsPerPixel: bpp,
94 duration: duration, logPath: logPath)
101 duration: duration, logPath: logPath, safeDir: safeDir,
102 cameraHeight: cameraHeight, cameraFps: cameraFps)
95103}
96104
97105func runRecord(_ args: [String]) {
......@@ -162,6 +170,19 @@ let argv = Array(CommandLine.arguments.dropFirst())
162170switch argv.first {
163171case "list": runList()
164172case "record": runRecord(Array(argv.dropFirst()))
173case "sequence":
174 // recorder sequence <session-dir> — (re)generate <name>.sq from sync.json.
175 guard argv.count > 1 else { fail("usage: recorder sequence <session-dir>") }
176 let dir = URL(fileURLWithPath: argv[1])
177 if let err = writeSequenceFile(in: dir) { fail(err) }
178 if FileManager.default.fileExists(
179 atPath: dir.appendingPathComponent("\(dir.lastPathComponent).sq").path)
180 {
181 logInfo("wrote \(dir.lastPathComponent).sq")
182 } else {
183 logInfo("skipped: no usable sync.json in \(dir.path)")
184 }
185 exit(0)
165186case nil, "menubar": MainActor.assumeIsolated { runMenubar() }
166187default: fail("unknown command: \(argv.first ?? "")")
167188}
src/Recorder/setup-diarization.sh created+31
......@@ -0,0 +1,31 @@
1#!/usr/bin/env bash
2# Optional "precise mode": pyannote speaker diarization for exact turn
3# boundaries (so mid-sentence interjections are attributed correctly).
4#
5# It lives in its OWN venv because pyannote 3.x needs older torch/torchaudio/
6# huggingface_hub than the main transcription venv — isolating it avoids
7# breaking Whisper/forced-alignment. session_transcript.py auto-detects this
8# venv and uses it; without it, it falls back to cluster-then-match labeling.
9#
10# Prerequisites (one-time, free):
11# 1. A HuggingFace token — log in once so it's cached:
12# ~/.clover-diarize/.venv/bin/huggingface-cli login (or set HF_TOKEN)
13# 2. Accept the model terms (click "Agree") at:
14# https://huggingface.co/pyannote/speaker-diarization-3.1
15# https://huggingface.co/pyannote/segmentation-3.0
16#
17# bash ~/dev/creative-control/src/Recorder/setup-diarization.sh
18set -euo pipefail
19
20DIR="$HOME/.clover-diarize"
21echo "==> creating diarization venv at $DIR"
22mkdir -p "$DIR"
23cd "$DIR"
24uv venv
25# Pinned, mutually-compatible set (torch 2.4 keeps numpy 2; torchaudio 2.4 still
26# exposes AudioMetaData; hf_hub 0.25 still has use_auth_token).
27uv pip install \
28 "pyannote.audio==3.3.2" "torch==2.4.1" "torchaudio==2.4.1" \
29 "huggingface_hub==0.25.2" matplotlib
30
31echo "✅ precise mode ready (ensure the token is logged in and model terms accepted)"
src/Recorder/setup-dictation.sh created+26
......@@ -0,0 +1,26 @@
1#!/usr/bin/env bash
2# One-time: set up local Whisper for the marker overlay's voice dictation.
3#
4# Creates a venv at ~/.clover-whisper with MLX Whisper (Apple-Silicon optimized)
5# and pre-downloads large-v3-turbo (~1.5 GB). Transcription then runs fully
6# on-device in ~1.5 s per note on this machine.
7#
8# bash ~/dev/creative-control/src/Recorder/setup-dictation.sh
9set -euo pipefail
10
11DIR="$HOME/.clover-whisper"
12echo "==> creating venv at $DIR"
13mkdir -p "$DIR"
14cd "$DIR"
15uv venv
16# mlx-whisper: transcription · speechbrain/torchaudio/scipy: speaker ID + clustering
17uv pip install mlx-whisper speechbrain torchaudio scipy
18
19echo "==> pre-downloading whisper-large-v3-turbo (one-time)"
20say -o /tmp/clover-dictation-warm.aiff "Clover dictation is ready." 2>/dev/null || true
21"$DIR/.venv/bin/python" - /tmp/clover-dictation-warm.aiff <<'PY' || true
22import sys, mlx_whisper
23mlx_whisper.transcribe(sys.argv[1], path_or_hf_repo="mlx-community/whisper-large-v3-turbo")
24PY
25
26echo "✅ dictation ready ($DIR/.venv/bin/python)"
src/Recorder/uvc/uvc-powerline.c created+80
......@@ -0,0 +1,80 @@
1// Set a UVC webcam's "Power Line Frequency" (anti-flicker) control over USB.
2//
3// macOS's camera API (AVFoundation) doesn't expose this control, but the camera
4// accepts a UVC SET_CUR on its default control pipe via IOKit even while the
5// system UVC driver is streaming — which cancels mains-flicker banding in
6// hardware (verified: the scrolling bars disappear the instant it's set).
7//
8// uvc-powerline <value> [vidHex] [pidHex]
9// value: 0 = off, 1 = 50 Hz, 2 = 60 Hz
10//
11// Defaults to the ZS CAMERA (VID 0x328f / PID 0x0072). Exits 0 if the control
12// was accepted by at least one processing-unit entity.
13#include <CoreFoundation/CoreFoundation.h>
14#include <IOKit/IOCFPlugIn.h>
15#include <IOKit/IOKitLib.h>
16#include <IOKit/usb/IOUSBLib.h>
17#include <stdio.h>
18#include <stdlib.h>
19
20#define UVC_SET_CUR 0x01
21#define PU_POWER_LINE_FREQUENCY_CONTROL 0x05
22
23int main(int argc, char** argv) {
24 int value = (argc > 1) ? atoi(argv[1]) : 2;
25 SInt32 vid = (argc > 2) ? (SInt32)strtol(argv[2], NULL, 16) : 0x328f;
26 SInt32 pid = (argc > 3) ? (SInt32)strtol(argv[3], NULL, 16) : 0x0072;
27
28 CFMutableDictionaryRef match = IOServiceMatching(kIOUSBDeviceClassName);
29 CFNumberRef vr = CFNumberCreate(NULL, kCFNumberSInt32Type, &vid);
30 CFNumberRef pr = CFNumberCreate(NULL, kCFNumberSInt32Type, &pid);
31 CFDictionarySetValue(match, CFSTR(kUSBVendorID), vr);
32 CFDictionarySetValue(match, CFSTR(kUSBProductID), pr);
33 CFRelease(vr);
34 CFRelease(pr);
35
36 io_service_t svc = IOServiceGetMatchingService(kIOMainPortDefault, match);
37 if (!svc) {
38 fprintf(stderr, "uvc-powerline: camera %04x:%04x not found\n", vid, pid);
39 return 1;
40 }
41
42 IOCFPlugInInterface** plug = NULL;
43 SInt32 score;
44 IOCreatePlugInInterfaceForService(
45 svc, kIOUSBDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &plug, &score);
46 IOObjectRelease(svc);
47 if (!plug) return 1;
48
49 IOUSBDeviceInterface** dev = NULL;
50 (*plug)->QueryInterface(plug, CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID), (LPVOID*)&dev);
51 (*plug)->Release(plug);
52 if (!dev) return 1;
53
54 IOReturn r = (*dev)->USBDeviceOpen(dev);
55 if (r != kIOReturnSuccess) {
56 r = (*dev)->USBDeviceOpenSeize(dev);
57 if (r != kIOReturnSuccess) {
58 fprintf(stderr, "uvc-powerline: cannot open device (0x%x)\n", r);
59 (*dev)->Release(dev);
60 return 2;
61 }
62 }
63
64 UInt8 val = (UInt8)value;
65 int ok = 0;
66 for (int entity = 1; entity <= 6; entity++) {
67 IOUSBDevRequest req;
68 req.bmRequestType = USBmakebmRequestType(kUSBOut, kUSBClass, kUSBInterface);
69 req.bRequest = UVC_SET_CUR;
70 req.wValue = (PU_POWER_LINE_FREQUENCY_CONTROL << 8);
71 req.wIndex = (entity << 8) | 0; // VideoControl interface 0
72 req.wLength = 1;
73 req.pData = &val;
74 if ((*dev)->DeviceRequest(dev, &req) == kIOReturnSuccess) ok = 1;
75 }
76
77 (*dev)->USBDeviceClose(dev);
78 (*dev)->Release(dev);
79 return ok ? 0 : 3;
80}
src/SpeedEditor.ts+10-10
......@@ -215,8 +215,8 @@ export class SpeedEditor extends Events<SpeedEditor.EventMap> {
215215 }
216216
217217 onDoublePress(key: SpeedEditor.Key, listener: () => void): Dispose {
218 const listeners = this.#doublePressListeners.get(key) ??
219 new Set<() => void>();
218 const listeners = this.#doublePressListeners.get(key)
219 ?? new Set<() => void>();
220220 listeners.add(listener);
221221 this.#doublePressListeners.set(key, listeners);
222222
......@@ -267,8 +267,8 @@ export class SpeedEditor extends Events<SpeedEditor.EventMap> {
267267
268268 #matchesHotplugDevice(device: import("usb").Device) {
269269 const descriptor = device.deviceDescriptor;
270 return descriptor.idVendor === this.#options.vendorId &&
271 descriptor.idProduct === this.#options.productId;
270 return descriptor.idVendor === this.#options.vendorId
271 && descriptor.idProduct === this.#options.productId;
272272 }
273273
274274 async #refreshConnection() {
......@@ -309,8 +309,8 @@ export class SpeedEditor extends Events<SpeedEditor.EventMap> {
309309 }
310310
311311 const currentKey = getDeviceKey(this.#deviceInfo);
312 const currentStillPresent = currentKey !== null &&
313 devices.some((device) => getDeviceKey(device) === currentKey);
312 const currentStillPresent = currentKey !== null
313 && devices.some((device) => getDeviceKey(device) === currentKey);
314314
315315 if (this.#device && !currentStillPresent) {
316316 this.#disconnect(true);
......@@ -330,8 +330,8 @@ export class SpeedEditor extends Events<SpeedEditor.EventMap> {
330330
331331 #selectDevice(devices: ReadonlyArray<SpeedEditor.DeviceInfo>) {
332332 if (this.#options.path) {
333 return devices.find((device) => device.path === this.#options.path) ??
334 null;
333 return devices.find((device) => device.path === this.#options.path)
334 ?? null;
335335 }
336336
337337 return devices[0] ?? null;
......@@ -912,8 +912,8 @@ function getDeviceKey(device: SpeedEditor.DeviceInfo | null) {
912912 return null;
913913 }
914914
915 return device.path ??
916 [
915 return device.path
916 ?? [
917917 device.vendorId,
918918 device.productId,
919919 device.serialNumber ?? "",
src/config.ts+46-116
......@@ -1,16 +1,19 @@
11import { Events } from "@clo/lib/Events.ts";
22import * as console from "@clo/lib/log.ts";
3import * as ts from "@clo/lib/ts.ts";
43import type { Dispose } from "@clo/lib/ts.ts";
54import * as fs from "node:fs/promises";
65import * as path from "node:path";
76import * as url from "node:url";
7import { Dialpad } from "./Dialpad.ts";
8import { Keypad } from "./Keypad.ts";
9import { KeypadSurface } from "./KeypadUI.ts";
810import { Mac } from "./Mac.ts";
9import { SpeedEditor } from "./SpeedEditor.ts";
1011
1112export interface Configure {
12 /** Events are only active when the application is active. */
13 speededitor: AppSpeedEditor;
13 /** 9 LCD keys + back/forward. Events/screens are only active when the app is focused. */
14 keypad: KeypadSurface;
15 /** One dial, one knob, four buttons. Events are only active when the app is focused. */
16 dialpad: AppDialpad;
1417 /** Not intended to listen for events. */
1518 mac: Mac;
1619 /** Control the application itself */
......@@ -24,7 +27,8 @@ export interface AppConfig {
2427
2528interface AppInstance {
2629 bundle: Mac.BundleId;
27 speedEditorProxy: AppSpeedEditor;
30 keypad: KeypadSurface;
31 dialpad: AppDialpad;
2832 app: Events<AppEvents>;
2933}
3034
......@@ -45,28 +49,22 @@ export function forApp(
4549 return { bundle, configure };
4650}
4751
48export class AppSpeedEditor extends Events<SpeedEditor.EventMap> {
49 #stack = new DisposableStack();
50 #enabledStack = new DisposableStack();
51 #source: SpeedEditor;
52 #doubleKeyEvents = new Set<{ key: SpeedEditor.Key; listener: () => void }>();
53 #intendedLeds = new Set<SpeedEditor.Led>();
54 #intendedJogLeds = new Set<SpeedEditor.JogLed>();
55 #intendedJogMode: SpeedEditor.JogMode = "relative-normalized";
52/** Per-app proxy that only forwards Dialpad events while its app is focused. */
53export class AppDialpad extends Events<Dialpad.EventMap> {
54 #source: Dialpad;
5655 #enabled = false;
5756
58 constructor(source: SpeedEditor) {
57 constructor(source: Dialpad) {
5958 super();
6059 this.#source = source;
61 this.#stack = new DisposableStack();
62 this.#stack.use(source.onAny((event, args) => {
60 source.onAny((event, args) => {
6361 if (this.#enabled) {
6462 this.emit(
6563 event,
66 ...args as SpeedEditor.EventMap[keyof SpeedEditor.EventMap],
64 ...args as Dialpad.EventMap[keyof Dialpad.EventMap],
6765 );
6866 }
69 }));
67 });
7068 }
7169
7270 get enabled() {
......@@ -74,93 +72,14 @@ export class AppSpeedEditor extends Events<SpeedEditor.EventMap> {
7472 }
7573
7674 set enabled(enabled: boolean) {
77 if (this.#enabled === enabled) return;
7875 this.#enabled = enabled;
79 if (!enabled) {
80 this.#enabledStack.dispose();
81 this.#enabledStack = new DisposableStack();
82 } else {
83 this.#source.setLeds(this.#intendedLeds);
84 this.#source.setJogLeds(this.#intendedJogLeds);
85 this.#source.setJogMode(this.#intendedJogMode);
86 for (const { key, listener } of this.#doubleKeyEvents) {
87 this.#enabledStack.use(this.#source.onDoublePress(key, listener));
88 }
89 }
90 }
91
92 setLeds(states: Iterable<SpeedEditor.Led>) {
93 this.#intendedLeds = new Set(states);
94 if (this.#enabled) {
95 this.#source.setLeds(this.#intendedLeds);
96 }
97 }
98
99 setJogLeds(states: Iterable<SpeedEditor.JogLed>) {
100 this.#intendedJogLeds = new Set(states);
101 if (this.#enabled) {
102 this.#source.setJogLeds(this.#intendedJogLeds);
103 }
104 }
105
106 setJogMode(mode: SpeedEditor.JogMode) {
107 this.#intendedJogMode = mode;
108 if (this.#enabled) {
109 this.#source.setJogMode(mode);
110 }
11176 }
11277
113 leds = new Proxy({} as Record<SpeedEditor.Led, boolean>, {
114 get: (_, key) => {
115 return this.#intendedLeds.has(key as SpeedEditor.Led);
116 },
117 set: (_, key, value) => {
118 if (value) this.#intendedLeds.add(key as SpeedEditor.Led);
119 else this.#intendedLeds.delete(key as SpeedEditor.Led);
120 this.setLeds(this.#intendedLeds);
121 return true;
122 },
123 });
124
125 jogLeds = new Proxy({} as Record<SpeedEditor.JogLed, boolean>, {
126 get: (_, key) => {
127 return this.#intendedJogLeds.has(key as SpeedEditor.JogLed);
128 },
129 set: (_, key, value) => {
130 if (value) this.#intendedJogLeds.add(key as SpeedEditor.JogLed);
131 else this.#intendedJogLeds.delete(key as SpeedEditor.JogLed);
132 this.setJogLeds(this.#intendedJogLeds);
133 return true;
134 },
135 });
136
137 get jogMode(): SpeedEditor.JogMode {
138 return this.#intendedJogMode;
139 }
140
141 set jogMode(mode: SpeedEditor.JogMode) {
142 this.setJogMode(mode);
143 }
144
145 onPress(key: SpeedEditor.Key, listener: () => void): Dispose {
78 onPress(button: Dialpad.Button, listener: () => void): Dispose {
14679 return this.on("keypress", (code) => {
147 if (key === code) listener();
148 });
149 }
150
151 onDoublePress(key: SpeedEditor.Key, listener: () => void): Dispose {
152 const pair = { key, listener };
153 this.#doubleKeyEvents.add(pair);
154 this.#enabled &&
155 this.#enabledStack.use(this.#source.onDoublePress(key, listener));
156 return ts.defer(() => {
157 throw new Error("TODO");
80 if (button === code) listener();
15881 });
15982 }
160
161 [Symbol.dispose]() {
162 this.#stack.dispose();
163 }
16483}
16584
16685async function loadConfigs(configDir: string) {
......@@ -192,19 +111,22 @@ async function loadConfigs(configDir: string) {
192111export async function runConfigs(dir: string) {
193112 const definitions = await loadConfigs(dir);
194113
195 const [mac, speededitor] = await Promise.all([
114 const [mac, keypad, dialpad] = await Promise.all([
196115 Mac.open(),
197 SpeedEditor.open(),
116 Keypad.open(),
117 Dialpad.open(),
198118 ]);
199 if (!speededitor.connected) {
200 console.warn("Speed Editor is not connected");
119
120 if (!keypad.connected) {
121 console.warn("MX Creative Keypad is not connected (connect it over USB-C)");
201122 }
202 speededitor.on("connect", () => {
203 console.info("Speed Editor connected");
204 });
205 speededitor.on("disconnect", () => {
206 console.warn("Speed Editor lost connection");
207 });
123 keypad.on("connect", () => console.info("Keypad connected"));
124 keypad.on("disconnect", () => console.warn("Keypad lost connection"));
125 if (!dialpad.connected) {
126 console.warn("MX Creative Dialpad is not connected");
127 }
128 dialpad.on("connect", () => console.info("Dialpad connected"));
129 dialpad.on("disconnect", () => console.warn("Dialpad lost connection"));
208130
209131 const instances = new Map<Mac.BundleId, AppInstance>();
210132 const pendingInstances = new Map<Mac.BundleId, Promise<AppInstance | null>>();
......@@ -230,15 +152,19 @@ export async function runConfigs(dir: string) {
230152 );
231153 }
232154
233 const speedEditorProxy = new AppSpeedEditor(speededitor);
234 const app = new Events<AppEvents>();
155 const instance: AppInstance = {
156 bundle,
157 keypad: new KeypadSurface(keypad),
158 dialpad: new AppDialpad(dialpad),
159 app: new Events<AppEvents>(),
160 };
235161 config.configure({
236 speededitor: speedEditorProxy,
162 keypad: instance.keypad,
163 dialpad: instance.dialpad,
237164 mac,
238 app,
165 app: instance.app,
239166 });
240167
241 const instance = { bundle, speedEditorProxy, app };
242168 instances.set(bundle, instance);
243169 return instance;
244170 }).finally(() => {
......@@ -253,7 +179,8 @@ export async function runConfigs(dir: string) {
253179 const version = ++switchVersion;
254180
255181 if (currentInstance) {
256 currentInstance.speedEditorProxy.enabled = false;
182 currentInstance.keypad.setActive(false);
183 currentInstance.dialpad.enabled = false;
257184 currentInstance.app.emit("blur");
258185 currentInstance = null;
259186 }
......@@ -264,8 +191,11 @@ export async function runConfigs(dir: string) {
264191 currentInstance = nextInstance;
265192
266193 if (currentInstance) {
267 currentInstance.speedEditorProxy.enabled = true;
194 currentInstance.keypad.setActive(true);
195 currentInstance.dialpad.enabled = true;
268196 currentInstance.app.emit("focus");
197 } else {
198 keypad.reset();
269199 }
270200 }
271201
src/icons.ts created+387
......@@ -0,0 +1,387 @@
1// Build-free key "faces" for the MX Creative Keypad: a fluent icon builder that
2// renders to a key-sized SVG document. Three sources — Lucide (stroke icons,
3// typed from `lucide-static`), Material Design Icons (filled icons, typed from
4// `mdi-ts`), and plain text — with chainable `.bg()`/`.fg()`/`.size()`.
5//
6// lucide("Play") // a white play glyph on the default bg
7// mdi("metronome").fg("red") // an MDI metronome, tinted red
8// txt("BPM").bg("#101010") // a centered text label
9//
10// The result's `.svg` is what KeypadUI rasterizes to JPEG. Everything here is
11// pure string building — no build step, no runtime SVG parsing beyond a trim.
12import type { MdiIcon } from "mdi-ts";
13import * as lucideIcons from "lucide-static";
14import { readFileSync } from "node:fs";
15import { createRequire } from "node:module";
16import { dirname, join } from "node:path";
17
18/** The pixel size of a single key face (square) — the SVG viewBox. */
19const KEY_SIZE = 118;
20
21const DEFAULT_BG = "#1b1b1b";
22const DEFAULT_FG = "#ffffff";
23const DEFAULT_ICON_SIZE = 56;
24const DEFAULT_TEXT_SIZE = 34;
25
26// The "unassigned" face: a near-black key with a small grey dot.
27const BLANK_BG = "#141414";
28const BLANK_DOT = "#555";
29
30/** Named palette. Extend freely — any unknown name falls through as a raw color. */
31const colorMap = {
32 black: "#000000",
33 white: "#ffffff",
34 grey: "#8a8a8a",
35 gray: "#8a8a8a",
36 red: "#ff5a36",
37 orange: "#ff9f43",
38 amber: "#ffbf47",
39 yellow: "#ffd23f",
40 lime: "#b6f36b",
41 green: "#7cfc9b",
42 teal: "#2dd4bf",
43 cyan: "#3ad6e8",
44 blue: "#4aa8ff",
45 indigo: "#6c7bff",
46 violet: "#9b6bff",
47 purple: "#b47cff",
48 magenta: "#ff5ccd",
49 pink: "#ff6bd6",
50} as const;
51
52/** Hex string, a name from the palette, or any other CSS color. */
53export type Color = `#${string}` | keyof typeof colorMap | (string & {});
54
55function resolveColor(color: Color): string {
56 return (colorMap as Record<string, string>)[color] ?? color;
57}
58
59/** Anything with SVG markup for a single key. */
60export interface Svg {
61 readonly svg: string;
62}
63
64/** Lucide icon names (PascalCase), typed from `lucide-static`. */
65export type LucideIconName = keyof typeof lucideIcons;
66
67/** MDI icon names (kebab-case, without the `mdi-` prefix), typed from `mdi-ts`. */
68export type MdiIconName = `${MdiIcon}` extends `mdi-${infer Name}` ? Name : never;
69
70/** One line of a {@link stack}: bare text, or text with its own size/color. */
71export type StackLine =
72 | string
73 | number
74 | { text: string | number; size?: number; color?: Color };
75
76interface StackSpec {
77 text: string;
78 size?: number;
79 color?: Color;
80}
81
82type IconSource =
83 | { readonly kind: "lucide"; readonly name: string }
84 | { readonly kind: "mdi"; readonly name: string }
85 | { readonly kind: "text"; readonly text: string }
86 | { readonly kind: "stack"; readonly lines: readonly StackSpec[] }
87 | { readonly kind: "timesig"; readonly numerator: string; readonly denominator: string }
88 | { readonly kind: "blank" };
89
90/**
91 * An immutable, lazily-rendered key face. `.bg()`/`.fg()`/`.size()` each return
92 * a new `Icon`, so definitions compose without mutating shared instances.
93 */
94export class Icon implements Svg {
95 readonly #source: IconSource;
96 readonly #bg: Color;
97 readonly #fg: Color;
98 readonly #size: number | null;
99 #rendered: string | null = null;
100
101 constructor(
102 source: IconSource,
103 bg: Color = DEFAULT_BG,
104 fg: Color = DEFAULT_FG,
105 size: number | null = null,
106 ) {
107 this.#source = source;
108 this.#bg = bg;
109 this.#fg = fg;
110 this.#size = size;
111 }
112
113 /** A copy with a different background color. */
114 bg(color: Color): Icon {
115 return new Icon(this.#source, color, this.#fg, this.#size);
116 }
117
118 /** A copy with a different foreground (stroke/fill/text) color. */
119 fg(color: Color): Icon {
120 return new Icon(this.#source, this.#bg, color, this.#size);
121 }
122
123 /** A copy with a different glyph size in key pixels (icon or text height). */
124 size(px: number): Icon {
125 return new Icon(this.#source, this.#bg, this.#fg, px);
126 }
127
128 /** The full key-sized SVG document. Rendered once, then cached. */
129 get svg(): string {
130 return this.#rendered ??= wrapSvg(this.#inner(), resolveColor(this.#bg));
131 }
132
133 #inner(): string {
134 const fg = resolveColor(this.#fg);
135 switch (this.#source.kind) {
136 case "lucide":
137 return glyph(lucideInner(this.#source.name), this.#glyphSize(), {
138 fill: "none",
139 stroke: fg,
140 extra:
141 `stroke-width="2" stroke-linecap="round" stroke-linejoin="round"`,
142 });
143 case "mdi":
144 return glyph(mdiInner(this.#source.name), this.#glyphSize(), {
145 fill: fg,
146 stroke: "none",
147 });
148 case "text":
149 return text(this.#source.text, fg, this.#size ?? DEFAULT_TEXT_SIZE);
150 case "stack":
151 return stackInner(this.#source.lines, fg);
152 case "timesig":
153 return timeSignatureInner(
154 this.#source.numerator,
155 this.#source.denominator,
156 fg,
157 );
158 case "blank":
159 return `<circle cx="${KEY_SIZE / 2}" cy="${KEY_SIZE / 2}" r="7" ` +
160 `fill="${BLANK_DOT}"/>`;
161 }
162 }
163
164 #glyphSize(): number {
165 return this.#size ?? DEFAULT_ICON_SIZE;
166 }
167}
168
169/** A Lucide (stroke) icon. `key` is the PascalCase name, e.g. `"AlarmClock"`. */
170export function lucide(key: LucideIconName): Icon {
171 return new Icon({ kind: "lucide", name: key });
172}
173
174/** A Material Design (filled) icon. `key` is the kebab name, e.g. `"metronome"`. */
175export function mdi(key: MdiIconName): Icon {
176 return new Icon({ kind: "mdi", name: key });
177}
178
179/** A short, centered text label. */
180export function txt(label: string): Icon {
181 return new Icon({ kind: "text", text: label });
182}
183
184/**
185 * A vertical stack of text lines, e.g. `stack(130, "BPM")` — the first line is
186 * emphasized (larger), the rest are secondary. Pass `{ text, size, color }` to
187 * override a line. Great for live readouts: `() => stack(bpm(), "BPM")`.
188 */
189export function stack(...lines: StackLine[]): Icon {
190 return new Icon({ kind: "stack", lines: lines.map(normalizeStackLine) });
191}
192
193function normalizeStackLine(line: StackLine): StackSpec {
194 if (typeof line === "string" || typeof line === "number") {
195 return { text: String(line) };
196 }
197 return { text: String(line.text), size: line.size, color: line.color };
198}
199
200/**
201 * A musical time signature: serif numerals stacked over a set of staff lines.
202 * `timeSignature("4/4")` or `timeSignature(6, 8)`.
203 */
204export function timeSignature(signature: string): Icon;
205export function timeSignature(
206 numerator: number | string,
207 denominator: number | string,
208): Icon;
209export function timeSignature(
210 a: number | string,
211 b?: number | string,
212): Icon {
213 let numerator: string;
214 let denominator: string;
215 if (b === undefined) {
216 const [top, bottom] = String(a).split("/");
217 numerator = (top ?? "4").trim();
218 denominator = (bottom ?? "4").trim();
219 } else {
220 numerator = String(a);
221 denominator = String(b);
222 }
223 return new Icon({ kind: "timesig", numerator, denominator });
224}
225
226/** The "unassigned" face: a near-black key with a small grey dot. */
227export const blank: Icon = new Icon({ kind: "blank" }, BLANK_BG);
228
229// ---------------------------------------------------------------------------
230// SVG building — icons live in a 24x24 viewBox; scale + center them in the key.
231// ---------------------------------------------------------------------------
232
233/** Wrap inner markup in a full key-sized document with a solid background. */
234function wrapSvg(inner: string, bg: string): string {
235 return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${KEY_SIZE} ${KEY_SIZE}">` +
236 `<rect width="${KEY_SIZE}" height="${KEY_SIZE}" fill="${bg}"/>${inner}</svg>`;
237}
238
239interface GlyphStyle {
240 fill: string;
241 stroke: string;
242 extra?: string;
243}
244
245/** Place a 24x24 glyph, scaled to `size` and centered, with the given paint. */
246function glyph(inner: string, size: number, style: GlyphStyle): string {
247 const offset = (KEY_SIZE - size) / 2;
248 const scale = size / 24;
249 const extra = style.extra ? ` ${style.extra}` : "";
250 return `<g transform="translate(${offset} ${offset}) scale(${scale})" ` +
251 `fill="${style.fill}" stroke="${style.stroke}"${extra}>${inner}</g>`;
252}
253
254const SANS_FONT = "Helvetica, Arial, sans-serif";
255const SERIF_FONT = "Georgia, 'Times New Roman', Times, serif";
256
257interface TextStyle {
258 family?: string;
259 weight?: number;
260}
261
262/** A single centered `<text>` at (x, y), sized in key pixels. */
263function textAt(
264 x: number,
265 y: number,
266 label: string,
267 fg: string,
268 size: number,
269 style: TextStyle = {},
270): string {
271 const family = style.family ?? SANS_FONT;
272 const weight = style.weight ?? 600;
273 return `<text x="${x}" y="${y}" fill="${fg}" font-family="${family}" ` +
274 `font-size="${size}" font-weight="${weight}" text-anchor="middle" ` +
275 `dominant-baseline="central">${escapeXml(label)}</text>`;
276}
277
278function text(label: string, fg: string, size: number): string {
279 return textAt(KEY_SIZE / 2, KEY_SIZE / 2, label, fg, size);
280}
281
282const STACK_PRIMARY = 42;
283const STACK_SECONDARY = 22;
284const STACK_GAP = 6;
285
286/** Lay text lines out vertically, centered as a group. */
287function stackInner(lines: readonly StackSpec[], fg: string): string {
288 const sizes = lines.map((line, index) =>
289 line.size ?? (index === 0 ? STACK_PRIMARY : STACK_SECONDARY)
290 );
291 const height = sizes.reduce((sum, size) => sum + size, 0) +
292 STACK_GAP * Math.max(0, lines.length - 1);
293 let top = (KEY_SIZE - height) / 2;
294
295 return lines
296 .map((line, index) => {
297 const size = sizes[index];
298 const centerY = top + size / 2;
299 top += size + STACK_GAP;
300 const color = line.color ? resolveColor(line.color) : fg;
301 return textAt(KEY_SIZE / 2, centerY, line.text, color, size);
302 })
303 .join("");
304}
305
306// Four horizontal staff lines with the serif numerals stacked across them —
307// numerator in the upper half, denominator in the lower, like real sheet music.
308const STAFF_LINES = 4;
309const STAFF_GAP = 12;
310const STAFF_INSET = 16;
311const TIMESIG_GLYPH = 40;
312
313function timeSignatureInner(
314 numerator: string,
315 denominator: string,
316 fg: string,
317): string {
318 const span = STAFF_GAP * (STAFF_LINES - 1);
319 const top = (KEY_SIZE - span) / 2;
320 let staff = "";
321 for (let i = 0; i < STAFF_LINES; i += 1) {
322 const y = top + i * STAFF_GAP;
323 staff += `<line x1="${STAFF_INSET}" y1="${y}" x2="${KEY_SIZE - STAFF_INSET}" ` +
324 `y2="${y}" stroke="${fg}" stroke-width="1.5" opacity="0.4"/>`;
325 }
326 const style: TextStyle = { family: SERIF_FONT, weight: 700 };
327 const numeral = TIMESIG_GLYPH;
328 const glyphs =
329 textAt(KEY_SIZE / 2, KEY_SIZE / 2 - 16, numerator, fg, numeral, style) +
330 textAt(KEY_SIZE / 2, KEY_SIZE / 2 + 16, denominator, fg, numeral, style);
331 return staff + glyphs;
332}
333
334function escapeXml(value: string): string {
335 return value.replace(/[<>&"']/g, (char) =>
336 char === "<"
337 ? "&lt;"
338 : char === ">"
339 ? "&gt;"
340 : char === "&"
341 ? "&amp;"
342 : char === '"'
343 ? "&quot;"
344 : "&apos;");
345}
346
347/** Strip the outer `<svg>` wrapper (and any leading comment) from icon markup. */
348function extractInner(markup: string): string {
349 return markup
350 .replace(/^[\s\S]*?<svg[^>]*>/, "")
351 .replace(/<\/svg>[\s\S]*$/, "")
352 .trim();
353}
354
355const require = createRequire(import.meta.url);
356const MDI_DIR = join(dirname(require.resolve("@mdi/svg/package.json")), "svg");
357
358const lucideCache = new Map<string, string>();
359const mdiCache = new Map<string, string>();
360
361function lucideInner(name: string): string {
362 let inner = lucideCache.get(name);
363 if (inner === undefined) {
364 const raw = (lucideIcons as Record<string, unknown>)[name];
365 if (typeof raw !== "string") {
366 throw new Error(`Unknown Lucide icon: "${name}"`);
367 }
368 inner = extractInner(raw);
369 lucideCache.set(name, inner);
370 }
371 return inner;
372}
373
374function mdiInner(name: string): string {
375 let inner = mdiCache.get(name);
376 if (inner === undefined) {
377 let raw: string;
378 try {
379 raw = readFileSync(join(MDI_DIR, `${name}.svg`), "utf8");
380 } catch {
381 throw new Error(`Unknown MDI icon: "${name}"`);
382 }
383 inner = extractInner(raw);
384 mdiCache.set(name, inner);
385 }
386 return inner;
387}
src/signals.ts created+104
......@@ -0,0 +1,104 @@
1// Minimal fine-grained reactive signals — no dependencies. A `signal` holds a
2// value; reading it inside a tracking scope (an `effect` or `computed`)
3// subscribes that scope, and `set` re-runs the scopes that read it. Just enough
4// reactivity to let keypad faces re-render themselves when their data changes,
5// keeping app configs declarative:
6//
7// const bpm = signal(120);
8// reaper.on("transport", (t) => bpm.set(Math.round(t.tempo)));
9// keypad.key("up-right", () => stack(bpm(), "BPM"), increaseTempo);
10//
11// Reads during a scope are tracked as dependencies and cleared on each re-run,
12// so conditional reads don't leave stale subscriptions behind.
13
14export type Cleanup = () => void;
15
16interface Reaction {
17 run: () => void;
18 deps: Set<Set<Reaction>>;
19}
20
21let activeReaction: Reaction | null = null;
22
23export interface ReadonlySignal<T> {
24 (): T;
25 /** Read without subscribing the current scope. */
26 peek(): T;
27}
28
29export interface Signal<T> extends ReadonlySignal<T> {
30 set(value: T): void;
31 update(fn: (previous: T) => T): void;
32}
33
34/** A reactive value. Call it to read (and subscribe); `.set()` to write. */
35export function signal<T>(initial: T): Signal<T> {
36 let value = initial;
37 const subscribers = new Set<Reaction>();
38
39 const read = (() => {
40 if (activeReaction) {
41 subscribers.add(activeReaction);
42 activeReaction.deps.add(subscribers);
43 }
44 return value;
45 }) as Signal<T>;
46
47 read.peek = () => value;
48 read.set = (next: T) => {
49 if (Object.is(next, value)) return;
50 value = next;
51 // Copy first: a reaction may resubscribe (or unsubscribe) while running.
52 for (const reaction of [...subscribers]) reaction.run();
53 };
54 read.update = (fn) => read.set(fn(value));
55
56 return read;
57}
58
59function runReaction(reaction: Reaction, body: () => void) {
60 // Drop the previous run's subscriptions so stale dependencies don't linger.
61 for (const dep of reaction.deps) dep.delete(reaction);
62 reaction.deps.clear();
63 const previous = activeReaction;
64 activeReaction = reaction;
65 try {
66 body();
67 } finally {
68 activeReaction = previous;
69 }
70}
71
72/** Run `fn` now, and again whenever a signal it read changes. Returns a disposer. */
73export function effect(fn: () => void): Cleanup {
74 const reaction: Reaction = {
75 run: () => runReaction(reaction, fn),
76 deps: new Set(),
77 };
78 reaction.run();
79 return () => {
80 for (const dep of reaction.deps) dep.delete(reaction);
81 reaction.deps.clear();
82 };
83}
84
85/** A memoized derived value that recomputes when its dependencies change. */
86export function computed<T>(compute: () => T): ReadonlySignal<T> {
87 const holder = signal<T>(undefined as T);
88 let started = false;
89 const reaction: Reaction = {
90 run: () => runReaction(reaction, () => holder.set(compute())),
91 deps: new Set(),
92 };
93
94 const read = (() => {
95 if (!started) {
96 started = true;
97 reaction.run();
98 }
99 return holder();
100 }) as ReadonlySignal<T>;
101 read.peek = () => holder.peek();
102
103 return read;
104}