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 };
|