A new module that subscribes to AbleSet's OSC API (UDP 39051) and streams what it is playing to ten tiles: Now Playing (song, tempo, meter, duration, time left, section, progress bar, next song, songs left), Song Sections (a sliding strip, or a lottery wheel from the title bar), Setlist, Transport, Tempo, Song Time, Set Time, Section, Next Song and Status, each coloured by its state. The song and set countdowns are offered to the Timers module. A heartbeat watchdog resubscribes when AbleSet goes quiet (a restarted AbleSet forgets its subscribers). Time left falls back to the song length and playhead when AbleSet stops sending it (Live disconnected). tools/ableset-mock.js plays a setlist over the same API for testing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
102 lines
3.1 KiB
JavaScript
102 lines
3.1 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* The slice of OSC 1.0 AbleSet speaks — messages and bundles with int32,
|
|
* float32, string, blob, true/false/nil — encoded and decoded with Buffer
|
|
* alone, so the module needs nothing outside Node.
|
|
*/
|
|
|
|
function pad4(n) {
|
|
return (n + 3) & ~3;
|
|
}
|
|
|
|
function encodeString(s) {
|
|
const raw = Buffer.from(String(s), 'utf8');
|
|
const out = Buffer.alloc(pad4(raw.length + 1));
|
|
raw.copy(out);
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* One message. Arguments are typed by their JS value: integers are int32,
|
|
* other numbers float32, booleans T/F, null nil, everything else a string.
|
|
*/
|
|
function encodeMessage(address, args = []) {
|
|
let tags = ',';
|
|
const parts = [];
|
|
for (const a of args) {
|
|
if (a === null || a === undefined) {
|
|
tags += 'N';
|
|
} else if (typeof a === 'boolean') {
|
|
tags += a ? 'T' : 'F';
|
|
} else if (typeof a === 'number' && Number.isInteger(a)) {
|
|
tags += 'i';
|
|
const b = Buffer.alloc(4);
|
|
b.writeInt32BE(a);
|
|
parts.push(b);
|
|
} else if (typeof a === 'number') {
|
|
tags += 'f';
|
|
const b = Buffer.alloc(4);
|
|
b.writeFloatBE(a);
|
|
parts.push(b);
|
|
} else {
|
|
tags += 's';
|
|
parts.push(encodeString(a));
|
|
}
|
|
}
|
|
return Buffer.concat([encodeString(address), encodeString(tags), ...parts]);
|
|
}
|
|
|
|
function readString(buf, at) {
|
|
let end = at;
|
|
while (end < buf.length && buf[end] !== 0) end += 1;
|
|
if (end >= buf.length) throw new Error('unterminated OSC string');
|
|
return { value: buf.toString('utf8', at, end), next: pad4(end + 1) };
|
|
}
|
|
|
|
function decodeMessage(buf, out) {
|
|
const addr = readString(buf, 0);
|
|
let at = addr.next;
|
|
const args = [];
|
|
if (at < buf.length && buf[at] === 0x2c) {
|
|
const tagStr = readString(buf, at);
|
|
at = tagStr.next;
|
|
for (const t of tagStr.value.slice(1)) {
|
|
switch (t) {
|
|
case 'i': args.push(buf.readInt32BE(at)); at += 4; break;
|
|
case 'f': args.push(buf.readFloatBE(at)); at += 4; break;
|
|
case 'd': args.push(buf.readDoubleBE(at)); at += 8; break;
|
|
case 'h': args.push(Number(buf.readBigInt64BE(at))); at += 8; break;
|
|
case 's': case 'S': { const s = readString(buf, at); args.push(s.value); at = s.next; break; }
|
|
case 'b': { const n = buf.readInt32BE(at); args.push(buf.subarray(at + 4, at + 4 + n)); at = pad4(at + 4 + n); break; }
|
|
case 'T': args.push(true); break;
|
|
case 'F': args.push(false); break;
|
|
case 'N': args.push(null); break;
|
|
case 'I': args.push(Infinity); break;
|
|
case 't': at += 8; break;
|
|
default: throw new Error(`unsupported OSC type tag "${t}"`);
|
|
}
|
|
}
|
|
}
|
|
out.push({ address: addr.value, args });
|
|
}
|
|
|
|
/** Every message in a packet, bundles flattened, in order. */
|
|
function decodePacket(buf, out = []) {
|
|
if (buf.length >= 16 && buf.toString('ascii', 0, 8) === '#bundle\0') {
|
|
let at = 16; // "#bundle\0" + 8-byte timetag
|
|
while (at + 4 <= buf.length) {
|
|
const n = buf.readInt32BE(at);
|
|
at += 4;
|
|
if (n <= 0 || at + n > buf.length) break;
|
|
decodePacket(buf.subarray(at, at + n), out);
|
|
at += n;
|
|
}
|
|
return out;
|
|
}
|
|
decodeMessage(buf, out);
|
|
return out;
|
|
}
|
|
|
|
module.exports = { encodeMessage, decodePacket };
|