From cabdbefe3795d9fe09dd28eb24b59a426fd7097c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 15:58:23 +0000 Subject: [PATCH 1/4] =?UTF-8?q?=EC=8B=9C=EC=9E=91=20=E2=80=94=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC=204=EB=8B=A8=EA=B3=84=20PR=2017:=20MIDI=20=EB=B0=9B?= =?UTF-8?q?=EB=8A=94=20=EC=AA=BD=20=EB=B2=A1=ED=84=B0=EC=99=80=20check:mid?= =?UTF-8?q?i-vectors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UYMwmokDNM7DPd588Afarz From c7cc2db097c96e5353ecb57f54898c6e05d10827 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 16:04:59 +0000 Subject: [PATCH 2/4] tools: MIDI vectors in, the test initiator and check:midi-vectors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit k17 of the step 4 plan: nine vectors of midi-spec.md's host → device table, replayed against the virtual panel through an RTP-MIDI initiator of their own. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UYMwmokDNM7DPd588Afarz --- docs/midi-vectors/README.md | 15 +++ docs/midi-vectors/in-cc-absolute.json | 19 ++++ docs/midi-vectors/in-cc-hold.json | 13 +++ docs/midi-vectors/in-cc-keeps-hold.json | 11 ++ docs/midi-vectors/in-cc-merged.json | 12 +++ docs/midi-vectors/in-cc-relative.json | 15 +++ docs/midi-vectors/in-channel.json | 14 +++ docs/midi-vectors/in-ignored.json | 20 ++++ docs/midi-vectors/in-notes.json | 18 ++++ docs/midi-vectors/in-program-change.json | 13 +++ web/scripts/drift/_rtpmidi.ts | 130 +++++++++++++++++++++++ web/scripts/drift/midi-vectors.ts | 10 ++ 12 files changed, 290 insertions(+) create mode 100644 docs/midi-vectors/README.md create mode 100644 docs/midi-vectors/in-cc-absolute.json create mode 100644 docs/midi-vectors/in-cc-hold.json create mode 100644 docs/midi-vectors/in-cc-keeps-hold.json create mode 100644 docs/midi-vectors/in-cc-merged.json create mode 100644 docs/midi-vectors/in-cc-relative.json create mode 100644 docs/midi-vectors/in-channel.json create mode 100644 docs/midi-vectors/in-ignored.json create mode 100644 docs/midi-vectors/in-notes.json create mode 100644 docs/midi-vectors/in-program-change.json create mode 100644 web/scripts/drift/_rtpmidi.ts create mode 100644 web/scripts/drift/midi-vectors.ts diff --git a/docs/midi-vectors/README.md b/docs/midi-vectors/README.md new file mode 100644 index 000000000..271f5751c --- /dev/null +++ b/docs/midi-vectors/README.md @@ -0,0 +1,15 @@ +# MIDI vectors + +Each file here is one sentence of [midi-spec.md](../midi-spec.md) and the messages that show a panel doing it, written as [contract-vectors.md](../contract-vectors.md) sets out. `npm run check:midi-vectors` in `web/` replays every one against a fresh [virtual panel](../../tools/virtual-panel/README.md) built with the `midi` feature, and prints what the vector expects beside what the panel did. CI runs it on every pull request with the other drift checks. When the spec rewords a sentence a vector quotes, that vector fails until someone brings it along. + +A file's name says which table of the spec it reads. `in-` is a row of *Host → device*, or the channel rule under it, and shows what the panel does with that message. `out-` is a row of *Device → host*, something the panel sends when a hand moves it. `/api/midi` is REST, so its vectors are in [rest-api-vectors](../rest-api-vectors/README.md) and start with `midi-`. + +A message is its bytes, the status and then its data. `[176, 24, 65]` is CC 24 at 65 on channel 1, `[144, 60, 127]` is note 60 on and `[192, 3]` is Program Change 3. Each one the vector sends goes in an RTP-MIDI packet of its own. What the panel sends is read back into the same form, one message to an entry, with running status filled in. + +The suite brings the session. `web/scripts/drift/_rtpmidi.ts` is an initiator shaped like [`rtpmidi_probe.py`](../../tools/rtpmidi-probe/rtpmidi_probe.py). Before the first step it invites the panel on its control port and then on the data port after it, and syncs clocks once. After the last step it says `BY`. + +The panel applies what has arrived at the end of a frame, and the pattern is given it on the next. So a vector gives a message 20 ms, a frame at 60 fps and a little more, before it looks. A hand does not take back a channel a CC wrote less than 250 ms before (`ABSOLUTE_RELEASE_GRACE_MS` in `core_bus.h`). That rule is the firmware's, not the spec's, and the REST vectors wait it out the same way: `in-cc-hold.json` holds the channel a second before the turn. + +One vector keeps the spec where the firmware does something else. `in-ignored.json` sends CC 120, 121 and 123 while a note is held, and the firmware lets the note go on any of them (`core_midi.h`). + +Some of the spec has no vectors. MIDI over USB needs the S3's OTG controller, and a Node process has no USB device side. The receive map both transports share is checked natively in CI by `midi_test.cpp`. A vector opens one session, so the limit of two participants is not replayed, and the virtual panel does not advertise itself over Bonjour. It does not reboot or run shows, so a Program Change remembered across a reboot and one a show ignores are not replayed either. Channel `0` is a build setting. CC 24 to 27 are sent only at 63, 64 and 65, where the spec's two readings of a relative value agree. diff --git a/docs/midi-vectors/in-cc-absolute.json b/docs/midi-vectors/in-cc-absolute.json new file mode 100644 index 000000000..1cd64c29f --- /dev/null +++ b/docs/midi-vectors/in-cc-absolute.json @@ -0,0 +1,19 @@ +{ + "contract": "docs/midi-spec.md#host--device", + "says": "`0..127` maps to the device's `0..1000` bus (rounded; 127 = 1000).", + "features": ["midi"], + "steps": [ + { "send": { "midi": [176, 20, 0] } }, + { "send": { "wait": 20 }, "expect": { "status": { "params": [0, "", "", ""], "paramActive": [true, false, false, false] } } }, + { "send": { "midi": [176, 20, 1] } }, + { "send": { "wait": 20 }, "expect": { "status": { "params": [8, "", "", ""] } } }, + { "send": { "midi": [176, 20, 64] } }, + { "send": { "wait": 20 }, "expect": { "status": { "params": [504, "", "", ""] } } }, + { "send": { "midi": [176, 20, 127] } }, + { "send": { "wait": 20 }, "expect": { "status": { "params": [1000, "", "", ""] } } }, + { "send": { "midi": [176, 21, 127] } }, + { "send": { "midi": [176, 22, 64] } }, + { "send": { "midi": [176, 23, 1] } }, + { "send": { "wait": 20 }, "expect": { "status": { "params": [1000, 1000, 504, 8], "paramActive": [true, true, true, true] } } } + ] +} diff --git a/docs/midi-vectors/in-cc-hold.json b/docs/midi-vectors/in-cc-hold.json new file mode 100644 index 000000000..56eede142 --- /dev/null +++ b/docs/midi-vectors/in-cc-hold.json @@ -0,0 +1,13 @@ +{ + "contract": "docs/midi-spec.md#host--device", + "says": "The value **holds** until a hand turns that knob, exactly like `POST /api/params` and MQTT `param/N`.", + "features": ["midi"], + "steps": [ + { "send": { "midi": [176, 21, 100] } }, + { "send": { "wait": 1000 }, "expect": { "status": { "params": ["", 787, "", ""], "paramActive": [false, true, false, false] } } }, + { "send": { "hand": { "knob": 1, "turn": 1 } } }, + { "send": { "wait": 20 }, "expect": { "status": { "paramActive": [false, true, false, false] } } }, + { "send": { "hand": { "knob": 2, "turn": -1 } } }, + { "send": { "wait": 20 }, "expect": { "status": { "paramActive": [false, false, false, false] } } } + ] +} diff --git a/docs/midi-vectors/in-cc-keeps-hold.json b/docs/midi-vectors/in-cc-keeps-hold.json new file mode 100644 index 000000000..48caf6ab3 --- /dev/null +++ b/docs/midi-vectors/in-cc-keeps-hold.json @@ -0,0 +1,11 @@ +{ + "contract": "docs/midi-spec.md#host--device", + "says": "Does not release an absolute hold.", + "features": ["midi"], + "steps": [ + { "send": { "midi": [176, 20, 127] } }, + { "send": { "wait": 1000 }, "expect": { "status": { "paramActive": [true, false, false, false] } } }, + { "send": { "midi": [176, 24, 63] } }, + { "send": { "wait": 20 }, "expect": { "status": { "params": [1000, "", "", ""], "paramActive": [true, false, false, false] } } } + ] +} diff --git a/docs/midi-vectors/in-cc-merged.json b/docs/midi-vectors/in-cc-merged.json new file mode 100644 index 000000000..15aec9fc2 --- /dev/null +++ b/docs/midi-vectors/in-cc-merged.json @@ -0,0 +1,12 @@ +{ + "contract": "docs/midi-spec.md#host--device", + "says": "Merged with physical motion at 1× per step — the same lane as OSC's `/patternflow/knob/N/delta`.", + "features": ["midi"], + "steps": [ + { "send": { "hand": { "knob": 3, "turn": 2 } } }, + { "send": { "midi": [176, 26, 65] } }, + { "send": { "midi": [176, 26, 65] } }, + { "send": { "midi": [176, 26, 65] } }, + { "send": { "wait": 20 }, "expect": { "input": { "given": { "deltas": [0, 0, 5, 0] } } } } + ] +} diff --git a/docs/midi-vectors/in-cc-relative.json b/docs/midi-vectors/in-cc-relative.json new file mode 100644 index 000000000..b13a0feaa --- /dev/null +++ b/docs/midi-vectors/in-cc-relative.json @@ -0,0 +1,15 @@ +{ + "contract": "docs/midi-spec.md#host--device", + "says": "`64` = no motion, `65` = +1 detent, `63` = −1", + "features": ["midi"], + "steps": [ + { "send": { "midi": [176, 24, 65] } }, + { "send": { "midi": [176, 25, 63] } }, + { "send": { "midi": [176, 26, 64] } }, + { "send": { "wait": 20 }, "expect": { "input": { "given": { "deltas": [1, -1, 0, 0] } } } }, + { "send": { "midi": [176, 27, 63] } }, + { "send": { "midi": [176, 27, 63] } }, + { "send": { "midi": [176, 24, 65] } }, + { "send": { "wait": 20 }, "expect": { "input": { "given": { "deltas": [2, -1, 0, -2] } } } } + ] +} diff --git a/docs/midi-vectors/in-channel.json b/docs/midi-vectors/in-channel.json new file mode 100644 index 000000000..707c0a6ac --- /dev/null +++ b/docs/midi-vectors/in-channel.json @@ -0,0 +1,14 @@ +{ + "contract": "docs/midi-spec.md#host--device", + "says": "Messages on another channel are ignored unless the channel is `0`.", + "features": ["midi"], + "steps": [ + { "send": { "midi": [177, 20, 127] } }, + { "send": { "midi": [178, 25, 65] } }, + { "send": { "midi": [159, 60, 127] } }, + { "send": { "midi": [193, 3] } }, + { "send": { "wait": 20 }, "expect": { "status": { "paramActive": [false, false, false, false] }, "input": { "given": { "deltas": [0, 0, 0, 0], "presses": [0, 0, 0, 0] }, "pattern": { "index": 0 } } } }, + { "send": { "midi": [176, 25, 65] } }, + { "send": { "wait": 20 }, "expect": { "input": { "given": { "deltas": [0, 1, 0, 0] } } } } + ] +} diff --git a/docs/midi-vectors/in-ignored.json b/docs/midi-vectors/in-ignored.json new file mode 100644 index 000000000..b3532ce6a --- /dev/null +++ b/docs/midi-vectors/in-ignored.json @@ -0,0 +1,20 @@ +{ + "contract": "docs/midi-spec.md#host--device", + "says": "Everything else is ignored.", + "features": ["midi"], + "steps": [ + { "send": { "midi": [144, 60, 127] } }, + { "send": { "wait": 20 }, "expect": { "input": { "given": { "held": [true, false, false, false] } } } }, + { "send": { "midi": [176, 123, 0] } }, + { "send": { "midi": [176, 120, 0] } }, + { "send": { "midi": [176, 121, 0] } }, + { "send": { "midi": [176, 19, 127] } }, + { "send": { "midi": [176, 28, 65] } }, + { "send": { "midi": [144, 64, 127] } }, + { "send": { "midi": [144, 59, 127] } }, + { "send": { "midi": [224, 0, 127] } }, + { "send": { "midi": [208, 100] } }, + { "send": { "midi": [160, 61, 100] } }, + { "send": { "wait": 20 }, "expect": { "status": { "paramActive": [false, false, false, false] }, "input": { "given": { "deltas": [0, 0, 0, 0], "presses": [1, 0, 0, 0], "held": [true, false, false, false] }, "pattern": { "index": 0 } } } } + ] +} diff --git a/docs/midi-vectors/in-notes.json b/docs/midi-vectors/in-notes.json new file mode 100644 index 000000000..74173ff96 --- /dev/null +++ b/docs/midi-vectors/in-notes.json @@ -0,0 +1,18 @@ +{ + "contract": "docs/midi-spec.md#host--device", + "says": "Note-on = press (edge), the button reads as held for as long as the note is held; note-off (or velocity 0) releases.", + "features": ["midi"], + "steps": [ + { "send": { "midi": [144, 60, 127] } }, + { "send": { "wait": 20 }, "expect": { "input": { "given": { "presses": [1, 0, 0, 0], "held": [true, false, false, false] } } } }, + { "send": { "wait": 1000 }, "expect": { "input": { "given": { "presses": [1, 0, 0, 0], "held": [true, false, false, false] } } } }, + { "send": { "midi": [144, 61, 127] } }, + { "send": { "midi": [144, 62, 1] } }, + { "send": { "midi": [144, 63, 64] } }, + { "send": { "wait": 20 }, "expect": { "input": { "given": { "presses": [1, 1, 1, 1], "held": [true, true, true, true] } } } }, + { "send": { "midi": [128, 60, 0] } }, + { "send": { "midi": [144, 61, 0] } }, + { "send": { "midi": [128, 62, 64] } }, + { "send": { "wait": 20 }, "expect": { "input": { "given": { "presses": [1, 1, 1, 1], "held": [false, false, false, true] } } } } + ] +} diff --git a/docs/midi-vectors/in-program-change.json b/docs/midi-vectors/in-program-change.json new file mode 100644 index 000000000..71b403f6a --- /dev/null +++ b/docs/midi-vectors/in-program-change.json @@ -0,0 +1,13 @@ +{ + "contract": "docs/midi-spec.md#host--device", + "says": "Select pattern index `n` (0-based, the order on `/patterns`).", + "features": ["midi"], + "steps": [ + { "send": { "midi": [192, 3] } }, + { "send": { "wait": 20 } }, + { "send": { "http": "GET /api/patterns" }, "expect": { "reply": { "body": { "active": 3 } } } }, + { "send": { "midi": [192, 0] } }, + { "send": { "wait": 20 } }, + { "send": { "http": "GET /api/patterns" }, "expect": { "reply": { "body": { "active": 0 } } } } + ] +} diff --git a/web/scripts/drift/_rtpmidi.ts b/web/scripts/drift/_rtpmidi.ts new file mode 100644 index 000000000..0d988ad3c --- /dev/null +++ b/web/scripts/drift/_rtpmidi.ts @@ -0,0 +1,130 @@ +/** + * The RTP-MIDI sender check:midi-vectors gives _vectors.ts, an initiator shaped like + * tools/rtpmidi-probe/rtpmidi_probe.py. It opens a control and a data socket, invites the panel on its + * midi port and then on the next, syncs clocks once, and puts each step's message in an RTP-MIDI packet + * of its own. A vector writes a message as its bytes (docs/midi-vectors/README.md), [176, 24, 65] for + * CC 24 at 65 on channel 1, and heard is each message the panel sent, read back into the same form. + */ +import { randomBytes } from "node:crypto"; +import dgram from "node:dgram"; + +import type { Sender } from "./_vectors"; + +const NAME = "check:midi-vectors"; +const ANSWER_MS = 2000; // the longest an invitation or a clock sync waits for the panel's answer +// Data octets after a channel status, by its high nibble. +const DATA: Record = { 0x8: 2, 0x9: 2, 0xa: 2, 0xb: 2, 0xc: 1, 0xd: 1, 0xe: 2 }; + +// A session packet is two 0xFF octets and a two-letter command (AppleMIDI). Its clock counts 100 µs. +const isSession = (b: Buffer) => b.length >= 4 && b.readUInt16BE(0) === 0xffff; +const ticks = () => BigInt(Math.round(performance.now() * 10)); +function session(command: string, ...words: number[]) { + const b = Buffer.alloc(4 + 4 * words.length); + b.writeUInt16BE(0xffff, 0); + b.write(command, 2, "latin1"); + words.forEach((w, i) => b.writeUInt32BE(w, 4 + 4 * i)); + return b; +} +// A clock sync: the initiator's SSRC, the count, three pad octets and three times. +function sync(ssrc: number, count: number, times: bigint[]) { + const b = Buffer.concat([session("CK", ssrc, count << 24), Buffer.alloc(24)]); + times.forEach((t, i) => b.writeBigUInt64BE(t, 12 + 8 * i)); + return b; +} + +const isMessage = (m: unknown): m is number[] => + Array.isArray(m) && m.every((b, i) => Number.isInteger(b) && b >= (i ? 0 : 0x80) && b < (i ? 0x80 : 0xf0)) && m.length === 1 + DATA[m[0] >> 4]; + +/** Each channel message in an RTP-MIDI packet (RFC 6295), running status filled in. Anything else comes back in hex. */ +function read(b: Buffer): unknown[] { + const long = (b[12] & 0x80) !== 0; // B: a 12-bit length + let at = long ? 14 : 13; + const end = at + (long ? ((b[12] & 0x0f) << 8) | b[13] : b[12] & 0x0f); + if (b.length < at || b[0] >> 6 !== 2 || (b[1] & 0x7f) !== 0x61 || end > b.length) return [`not RTP-MIDI: ${b.toString("hex")}`]; + let [delta, running] = [(b[12] & 0x20) !== 0, 0]; // Z: the first message has a delta time too + const out: unknown[] = []; + while (at < end) { + if (delta) while (at < end && b[at++] & 0x80); // one to four octets, the last under 128 + delta = true; + const status = b[at] & 0x80 ? b[at++] : running; + const m = [status, ...b.subarray(at, (at += DATA[status >> 4] ?? end))]; + if (!isMessage(m)) return [...out, `not a channel message: ${b.toString("hex", 12, end)}`]; + out.push(m); + running = status; + } + return out; +} + +/** Sends a session packet and resolves with the answer of that command on the same socket. */ +function ask(socket: dgram.Socket, port: number, b: Buffer, command: string) { + const what = `${b.toString("latin1", 2, 4)} to port ${port}`; + return new Promise((done, fail) => { + const timer = setTimeout(() => (socket.off("message", hear), fail(new Error(`${what}: no answer in ${ANSWER_MS} ms`))), ANSWER_MS); + function hear(r: Buffer) { + if (!isSession(r) || ![command, "NO"].includes(r.toString("latin1", 2, 4))) return; + clearTimeout(timer); + socket.off("message", hear); + if (r.toString("latin1", 2, 4) === "NO") fail(new Error(`${what}: the panel said NO`)); + else done(r); + } + socket.on("message", hear); + socket.send(b, port, "127.0.0.1", (e) => e && fail(e)); + }); +} + +const MARK = Buffer.from("settle"); + +async function socket() { + const s = dgram.createSocket("udp4"); + await new Promise((done, fail) => void s.once("error", fail).bind(0, "127.0.0.1", () => done())); + return s; +} + +export const midi: Sender = async () => { + const [control, data] = [await socket(), await socket()]; + const me = data.address().port; + const [ssrc, token] = [randomBytes(4).readUInt32BE(0), randomBytes(4).readUInt32BE(0)]; + const [heard, marks] = [[] as unknown[], [] as (() => void)[]]; + data.on("message", (b, from) => { + if (from.port === me) marks.shift()?.(); + else if (!isSession(b)) heard.push(...read(b)); // session packets are open()'s + }); + const send = (b: Buffer, port: number) => new Promise((done, fail) => data.send(b, port, "127.0.0.1", (e) => (e ? fail(e) : done()))); + + let [panel, seq] = [0, randomBytes(2).readUInt16BE(0)]; + return { + // An invitation on control and then on data, which the panel keeps on the port after it, and one clock sync. + open: async ({ ports }) => { + const invite = Buffer.concat([session("IN", 2, token, ssrc), Buffer.from(`${NAME}\0`)]); + await ask(control, ports.midi, invite, "OK"); + await ask(data, ports.midi + 1, invite, "OK"); + const answer = await ask(data, ports.midi + 1, sync(ssrc, 0, [ticks(), 0n, 0n]), "CK"); + await send(sync(ssrc, 2, [answer.readBigUInt64BE(12), answer.readBigUInt64BE(20), ticks()]), ports.midi + 1); + panel = ports.midi; + }, + send: async (what) => { + if (!isMessage(what)) throw new Error(`midi sends one channel message, [status, ...data], not ${JSON.stringify(what)}`); + const b = Buffer.alloc(13); + b[0] = 0x80; // version 2 + b[1] = 0x61; // payload type 97 + b.writeUInt16BE(seq, 2); + b.writeUInt32BE(Number(ticks() % 2n ** 32n), 4); + b.writeUInt32BE(ssrc, 8); + b[12] = what.length; // a list under 16 octets has the short header + seq = (seq + 1) & 0xffff; + await send(Buffer.concat([b, Buffer.from(what)]), panel + 1); + }, + heard, + // A datagram to itself. The socket reads in order, so once it is back, all the panel sent before it is in heard. + settle: () => + new Promise((done, fail) => { + marks.push(done); + send(MARK, me).catch(fail); + }), + // BY, as the probe ends, since a device keeps two participants at most. + close: async () => { + if (panel) await new Promise((done) => control.send(session("BY", 2, token, ssrc), panel, "127.0.0.1", () => done())); + await Promise.all([control, data].map((s) => new Promise((done) => s.close(() => done())))); + }, + }; +}; diff --git a/web/scripts/drift/midi-vectors.ts b/web/scripts/drift/midi-vectors.ts new file mode 100644 index 000000000..fd1c38cca --- /dev/null +++ b/web/scripts/drift/midi-vectors.ts @@ -0,0 +1,10 @@ +/** + * check:midi-vectors: docs/midi-vectors/ against a fresh virtual panel each + * (docs/contract-vectors.md), built with the midi feature. The sender is + * _rtpmidi.ts, one RTP-MIDI session a vector. + */ +import { done } from "./_lib"; +import { midi } from "./_rtpmidi"; +import { replay } from "./_vectors"; + +replay("docs/midi-vectors", { midi }).then(done); From ddbd9f6d82622847abb9f2e70fe243493215b603 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 16:06:44 +0000 Subject: [PATCH 3/4] tools: keep _rtpmidi.ts inside the ES2017 target Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UYMwmokDNM7DPd588Afarz --- web/scripts/drift/_rtpmidi.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/web/scripts/drift/_rtpmidi.ts b/web/scripts/drift/_rtpmidi.ts index 0d988ad3c..dcf7aa713 100644 --- a/web/scripts/drift/_rtpmidi.ts +++ b/web/scripts/drift/_rtpmidi.ts @@ -17,7 +17,7 @@ const DATA: Record = { 0x8: 2, 0x9: 2, 0xa: 2, 0xb: 2, 0xc: 1, 0 // A session packet is two 0xFF octets and a two-letter command (AppleMIDI). Its clock counts 100 µs. const isSession = (b: Buffer) => b.length >= 4 && b.readUInt16BE(0) === 0xffff; -const ticks = () => BigInt(Math.round(performance.now() * 10)); +const ticks = () => Math.round(performance.now() * 10); function session(command: string, ...words: number[]) { const b = Buffer.alloc(4 + 4 * words.length); b.writeUInt16BE(0xffff, 0); @@ -26,9 +26,9 @@ function session(command: string, ...words: number[]) { return b; } // A clock sync: the initiator's SSRC, the count, three pad octets and three times. -function sync(ssrc: number, count: number, times: bigint[]) { +function sync(ssrc: number, count: number, times: (number | bigint)[]) { const b = Buffer.concat([session("CK", ssrc, count << 24), Buffer.alloc(24)]); - times.forEach((t, i) => b.writeBigUInt64BE(t, 12 + 8 * i)); + times.forEach((t, i) => b.writeBigUInt64BE(BigInt(t), 12 + 8 * i)); return b; } @@ -98,7 +98,7 @@ export const midi: Sender = async () => { const invite = Buffer.concat([session("IN", 2, token, ssrc), Buffer.from(`${NAME}\0`)]); await ask(control, ports.midi, invite, "OK"); await ask(data, ports.midi + 1, invite, "OK"); - const answer = await ask(data, ports.midi + 1, sync(ssrc, 0, [ticks(), 0n, 0n]), "CK"); + const answer = await ask(data, ports.midi + 1, sync(ssrc, 0, [ticks(), 0, 0]), "CK"); await send(sync(ssrc, 2, [answer.readBigUInt64BE(12), answer.readBigUInt64BE(20), ticks()]), ports.midi + 1); panel = ports.midi; }, @@ -108,7 +108,7 @@ export const midi: Sender = async () => { b[0] = 0x80; // version 2 b[1] = 0x61; // payload type 97 b.writeUInt16BE(seq, 2); - b.writeUInt32BE(Number(ticks() % 2n ** 32n), 4); + b.writeUInt32BE(ticks() % 2 ** 32, 4); b.writeUInt32BE(ssrc, 8); b[12] = what.length; // a list under 16 octets has the short header seq = (seq + 1) & 0xffff; From 7004cd5ef6b56a70130518b06916d7efc909ca95 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 16:09:26 +0000 Subject: [PATCH 4/4] tools: split the _rtpmidi.ts header the voice lint flagged Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UYMwmokDNM7DPd588Afarz --- web/scripts/drift/_rtpmidi.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/scripts/drift/_rtpmidi.ts b/web/scripts/drift/_rtpmidi.ts index dcf7aa713..61dde6a52 100644 --- a/web/scripts/drift/_rtpmidi.ts +++ b/web/scripts/drift/_rtpmidi.ts @@ -1,9 +1,9 @@ /** * The RTP-MIDI sender check:midi-vectors gives _vectors.ts, an initiator shaped like - * tools/rtpmidi-probe/rtpmidi_probe.py. It opens a control and a data socket, invites the panel on its - * midi port and then on the next, syncs clocks once, and puts each step's message in an RTP-MIDI packet - * of its own. A vector writes a message as its bytes (docs/midi-vectors/README.md), [176, 24, 65] for - * CC 24 at 65 on channel 1, and heard is each message the panel sent, read back into the same form. + * tools/rtpmidi-probe/rtpmidi_probe.py. It invites the panel on its midi port from a control socket and + * on the next port from a data socket, then syncs clocks once. Each step's message goes in an RTP-MIDI + * packet of its own. A vector writes a message as its bytes (docs/midi-vectors/README.md), [176, 24, 65] + * for CC 24 at 65 on channel 1, and heard is each message the panel sent, read back into the same form. */ import { randomBytes } from "node:crypto"; import dgram from "node:dgram";