osc-min
Version:
Simple utilities for open sound control in node.js
560 lines (559 loc) • 19.4 kB
JavaScript
const toView = (buffer) => {
if (buffer instanceof ArrayBuffer) {
return new DataView(buffer);
}
return new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
};
export const toOscString = (str) => {
if (!(typeof str === "string")) {
throw new OSCError("can't pack a non-string into an osc-string");
}
if (str.indexOf("\u0000") !== -1) {
throw new OSCError("Can't pack an osc-string that contains NULL characters");
}
const buffer = new TextEncoder().encode(str);
const padding = 4 - (buffer.length % 4);
const ret = new ArrayBuffer(buffer.length + padding);
new Uint8Array(ret).set(buffer);
return ret;
};
export const splitOscString = (bufferInput) => {
const buffer = toView(bufferInput);
const uint8Array = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
// First, find the first null character.
const nullIndex = uint8Array.indexOf(0);
if (nullIndex === -1) {
throw new OSCError("All osc-strings must contain a null character");
}
const stringPart = uint8Array.slice(0, nullIndex);
const padding = 4 - (stringPart.length % 4);
if (uint8Array.length < nullIndex + padding) {
throw new OSCError(`Not enough padding for osc-string`);
}
// Verify padding is all zeros
for (let i = 0; i < padding; i++) {
if (uint8Array[stringPart.length + i] !== 0) {
throw new OSCError("Corrupt padding for osc-string");
}
}
return {
value: new TextDecoder().decode(stringPart),
rest: sliceDataView(buffer, nullIndex + padding),
};
};
const sliceDataView = (dataView, start) => new DataView(dataView.buffer, dataView.byteOffset + start, dataView.byteLength - start);
export const splitInteger = (bufferInput) => {
const buffer = toView(bufferInput);
const bytes = 4;
if (buffer.byteLength < bytes) {
throw new OSCError("buffer is not big enough for integer type");
}
return {
value: buffer.getInt32(0, false),
rest: sliceDataView(buffer, bytes),
};
};
export const splitTimetag = (bufferInput) => {
const buffer = toView(bufferInput);
const bytes = 4;
if (buffer.byteLength < bytes * 2) {
throw new OSCError("buffer is not big enough to contain a timetag");
}
const seconds = buffer.getUint32(0, false);
const fractional = buffer.getUint32(bytes, false);
return {
value: [seconds, fractional],
rest: sliceDataView(buffer, bytes * 2),
};
};
const UNIX_EPOCH = 2208988800;
const TWO_POW_32 = 4294967296;
export const dateToTimetag = (date) => {
const timeStamp = date.getTime() / 1000;
const wholeSecs = Math.floor(timeStamp);
return makeTimetag(wholeSecs, timeStamp - wholeSecs);
};
const makeTimetag = (unixseconds, fracSeconds) => {
const ntpSecs = unixseconds + UNIX_EPOCH;
const ntpFracs = Math.round(TWO_POW_32 * fracSeconds);
return [ntpSecs, ntpFracs];
};
export const timetagToDate = ([seconds, fractional]) => {
const date = new Date();
date.setTime((seconds - UNIX_EPOCH) * 1000 + (fractional * 1000) / TWO_POW_32);
return date;
};
export const toTimetagBuffer = (timetag) => {
let high, low;
if (typeof timetag === "object" && "getTime" in timetag) {
[high, low] = dateToTimetag(timetag);
}
else {
[high, low] = timetag;
}
const ret = new DataView(new ArrayBuffer(8));
ret.setUint32(0, high, false);
ret.setUint32(4, low, false);
return ret.buffer;
};
export const toIntegerBuffer = (number) => {
const ret = new DataView(new ArrayBuffer(4));
ret.setInt32(0, number, false);
return ret.buffer;
};
const parseOscArg = (code, buffer) => {
switch (code) {
case "s": {
const { value, rest } = splitOscString(buffer);
return { value: { type: "string", value }, rest };
}
case "S": {
const { value, rest } = splitOscString(buffer);
return { value: { type: "symbol", value }, rest };
}
case "i": {
const { value, rest } = splitInteger(buffer);
return { value: { type: "integer", value }, rest };
}
case "t": {
const { value, rest } = splitTimetag(buffer);
return { value: { type: "timetag", value }, rest };
}
case "f": {
const view = toView(buffer);
return {
value: { type: "float", value: view.getFloat32(0, false) },
rest: sliceDataView(view, 4),
};
}
case "d": {
const view = toView(buffer);
return {
value: { type: "double", value: view.getFloat64(0, false) },
rest: sliceDataView(view, 8),
};
}
case "b": {
const view = toView(buffer);
const { value: length, rest: data } = splitInteger(view);
// We added padding to make sure the blob's length in the buffer is a multiple of 4
const padding = (4 - (length % 4)) % 4;
return {
value: {
type: "blob",
value: new DataView(data.buffer, data.byteOffset, length),
},
rest: sliceDataView(data, length + padding),
};
}
case "r": {
const view = toView(buffer);
return {
value: {
type: "color",
value: {
red: view.getUint8(0),
green: view.getUint8(1),
blue: view.getUint8(2),
alpha: view.getUint8(3),
},
},
rest: sliceDataView(view, 4),
};
}
case "T":
return {
value: { type: "true", value: true },
rest: toView(buffer),
};
case "F":
return {
value: { type: "false", value: false },
rest: toView(buffer),
};
case "N":
return {
value: { type: "null", value: null },
rest: toView(buffer),
};
case "I":
return {
value: { type: "bang", value: "bang" },
rest: toView(buffer),
};
case "c": {
const view = toView(buffer);
const codepoint = view.getUint32(0, false);
return {
value: { type: "character", value: String.fromCodePoint(codepoint) },
rest: sliceDataView(view, 4),
};
}
case "h": {
const view = toView(buffer);
const bigint = view.getBigInt64(0, false);
return {
value: { type: "bigint", value: bigint },
rest: sliceDataView(view, 8),
};
}
case "m": {
const view = toView(buffer);
if (view.byteLength < 4) {
throw new OSCError("buffer is not big enough to contain a midi packet");
}
const array = new Uint8Array(view.buffer, view.byteOffset, 4);
return {
value: { type: "midi", value: [...array] },
rest: sliceDataView(view, 4),
};
}
}
return undefined;
};
const toOscArgument = (arg) => {
switch (arg.type) {
case "string":
return toOscString(arg.value);
case "symbol":
return toOscString(arg.value);
case "integer":
return toIntegerBuffer(arg.value);
case "timetag":
return toTimetagBuffer(arg.value);
case "character": {
const chars = [...arg.value];
if (chars.length !== 1) {
throw new OSCError("Can only send a single character");
}
const ret = new DataView(new ArrayBuffer(4));
// ! is safe here because we checked length === 1 above
ret.setUint32(0, chars[0].codePointAt(0) ?? 0, false);
return ret.buffer;
}
case "float": {
const ret = new DataView(new ArrayBuffer(4));
ret.setFloat32(0, arg.value, false);
return ret.buffer;
}
case "bigint": {
const ret = new DataView(new ArrayBuffer(8));
ret.setBigInt64(0, arg.value, false);
return ret.buffer;
}
case "double": {
const ret = new DataView(new ArrayBuffer(8));
ret.setFloat64(0, arg.value, false);
return ret.buffer;
}
case "blob": {
const view = toView(arg.value);
// Add padding to make the blob's length a multiple of 4
const padding = (4 - (arg.value.byteLength % 4)) % 4;
const ret = new DataView(new ArrayBuffer(4 + arg.value.byteLength + padding));
ret.setUint32(0, arg.value.byteLength, false);
new Uint8Array(ret.buffer, ret.byteOffset + 4).set(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
return ret.buffer;
}
case "color": {
const ret = new DataView(new ArrayBuffer(4 * 4));
ret.setUint8(0, arg.value.red);
ret.setUint8(1, arg.value.green);
ret.setUint8(2, arg.value.blue);
ret.setUint8(3, arg.value.alpha);
return ret.buffer;
}
case "midi": {
return new Uint8Array(arg.value).buffer;
}
case "true":
return new ArrayBuffer(0);
case "false":
return new ArrayBuffer(0);
case "null":
return new ArrayBuffer(0);
case "bang":
return new ArrayBuffer(0);
}
};
const RepresentationToTypeCode = {
string: "s",
integer: "i",
timetag: "t",
float: "f",
double: "d",
blob: "b",
true: "T",
false: "F",
null: "N",
bang: "I",
symbol: "S",
character: "c",
color: "r",
midi: "m",
bigint: "h",
};
const toOscArgWithType = (arg) => {
if (arg === null) {
return { type: "null" };
}
if (typeof arg === "object" && "type" in arg) {
return arg;
}
if (arg === "bang") {
return { type: "bang" };
}
if (typeof arg === "string") {
return { type: "string", value: arg };
}
if (typeof arg === "number") {
return { type: "float", value: arg };
}
if (arg instanceof Date) {
return { type: "timetag", value: arg };
}
if (arg instanceof ArrayBuffer) {
return { type: "blob", value: arg };
}
if (typeof arg === "object" && "buffer" in arg) {
return { type: "blob", value: arg };
}
if (typeof arg === "object" && "red" in arg) {
return { type: "color", value: arg };
}
if (typeof arg === "bigint") {
return { type: "bigint", value: arg };
}
if (arg === true) {
return { type: "true" };
}
// This unnecessary condition helps exhaustivity checking
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (arg === false) {
return { type: "false" };
}
arg;
throw new OSCError("Invalid argument: " + arg);
};
export const fromOscMessage = (buffer) => {
const { value: address, rest } = splitOscString(buffer);
buffer = rest;
if (address[0] !== "/") {
throw new OSCError("addresses must start with /");
}
if (!buffer.byteLength) {
return {
address: address,
args: [],
oscType: "message",
};
}
const split = splitOscString(buffer);
const types = split.value;
buffer = split.rest;
if (types[0] !== ",") {
throw new OSCError("Argument lists must begin with ,");
}
const args = [];
const arrayStack = [args];
for (const parsedType of types.slice(1)) {
if (parsedType === "[") {
arrayStack.push([]);
continue;
}
if (parsedType === "]") {
if (arrayStack.length <= 1) {
throw new OSCError("Mismatched ']' character.");
}
else {
const built = arrayStack.pop();
arrayStack[arrayStack.length - 1].push({
type: "array",
value: built,
});
}
continue;
}
const parsed = parseOscArg(parsedType, buffer);
if (parsed === undefined) {
throw new OSCError("I don't understand the argument code " + parsedType);
}
buffer = parsed.rest;
arrayStack[arrayStack.length - 1].push(parsed.value);
}
if (arrayStack.length !== 1) {
throw new OSCError("Mismatched '[' character");
}
return {
address: address,
args: args,
oscType: "message",
};
};
export const fromOscBundle = (buffer) => {
const split1 = splitOscString(buffer);
const bundleTag = split1.value;
buffer = split1.rest;
if (bundleTag !== "#bundle") {
throw new OSCError("osc-bundles must begin with #bundle");
}
const split2 = splitTimetag(buffer);
const timetag = split2.value;
buffer = split2.rest;
const convertedElems = mapBundleList(buffer, (buffer) => fromOscPacket(buffer));
return {
timetag: timetag,
elements: convertedElems,
oscType: "bundle",
};
};
export const fromOscPacket = (buffer) => {
if (isOscBundleBuffer(buffer)) {
return fromOscBundle(buffer);
}
else {
return fromOscMessage(buffer);
}
};
const toOscTypeAndArgs = (args) => {
let osctype = "";
let oscargs = [];
for (const arg of args) {
if (arg !== null &&
(Array.isArray(arg) ||
(typeof arg === "object" && "type" in arg && arg.type === "array"))) {
const { type, args: newargs } = toOscTypeAndArgs(Array.isArray(arg) ? arg : arg.value);
osctype += "[" + type + "]";
oscargs = oscargs.concat(newargs);
}
else {
const withType = toOscArgWithType(arg);
const typeCode = RepresentationToTypeCode[withType.type];
const buff = toOscArgument(withType);
osctype += typeCode;
oscargs.push(buff);
}
}
return { type: osctype, args: oscargs };
};
export const concat = (buffers) => {
const totalLength = buffers.reduce((acc, buffer) => acc + buffer.byteLength, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const buffer of buffers) {
const view = toView(buffer);
result.set(new Uint8Array(view.buffer, view.byteOffset, view.byteLength), offset);
offset += view.byteLength;
}
return result.buffer;
};
export const toOscMessage = (message) => {
const address = typeof message === "string" ? message : message.address;
const rawArgs = typeof message === "string"
? []
: message.args === undefined
? []
: Array.isArray(message.args)
? message.args
: [message.args];
const oscaddr = toOscString(address);
const { type, args } = toOscTypeAndArgs(rawArgs);
return new DataView(concat([oscaddr, toOscString("," + type)].concat(args)));
};
export const toOscBundle = (bundle) => {
const elements = bundle.elements === undefined
? []
: Array.isArray(bundle.elements)
? bundle.elements
: [bundle.elements];
const oscBundleTag = toOscString("#bundle");
const oscTimeTag = toTimetagBuffer(bundle.timetag);
const oscElems = elements.reduce((acc, x) => {
const buffer = toOscPacket(x);
const size = toIntegerBuffer(buffer.byteLength);
return acc.concat([new DataView(size), buffer]);
}, new Array());
return new DataView(concat([oscBundleTag, oscTimeTag, ...oscElems]));
};
export const toOscPacket = (packet) => {
if (typeof packet === "object" && "timetag" in packet) {
return toOscBundle(packet);
}
else {
return toOscMessage(packet);
}
};
export const applyMessageTranformerToBundle = (transform) => (buffer) => {
const splitStart = splitOscString(buffer);
buffer = splitStart.rest;
if (splitStart.value !== "#bundle") {
throw new OSCError("osc-bundles must begin with #bundle");
}
const bundleTagBuffer = toOscString(splitStart.value);
const timetagBuffer = new DataView(buffer.buffer, buffer.byteOffset, 8);
buffer = sliceDataView(buffer, 8);
const elems = mapBundleList(buffer, (buffer) => applyTransform(buffer, transform, applyMessageTranformerToBundle(transform)));
const totalLength = bundleTagBuffer.byteLength +
timetagBuffer.byteLength +
elems.reduce((acc, elem) => acc + 4 + elem.byteLength, 0);
const outBuffer = new Uint8Array(totalLength);
outBuffer.set(new Uint8Array(bundleTagBuffer), 0);
outBuffer.set(new Uint8Array(timetagBuffer.buffer, timetagBuffer.byteOffset, timetagBuffer.byteLength), bundleTagBuffer.byteLength);
let copyIndex = bundleTagBuffer.byteLength + timetagBuffer.byteLength;
for (const elem of elems) {
outBuffer.set(new Uint8Array(toIntegerBuffer(elem.byteLength)), copyIndex);
copyIndex += 4;
outBuffer.set(new Uint8Array(elem.buffer, elem.byteOffset, elem.byteLength), copyIndex);
copyIndex += elem.byteLength;
}
return new DataView(outBuffer.buffer, outBuffer.byteOffset, outBuffer.byteLength);
};
export const applyTransform = (buffer, mTransform, bundleTransform) => {
if (bundleTransform == null) {
bundleTransform = applyMessageTranformerToBundle(mTransform);
}
const view = toView(buffer);
if (isOscBundleBuffer(view)) {
return bundleTransform(view);
}
else {
return mTransform(view);
}
};
export const addressTransform = (transform) => (buffer) => {
const { value, rest } = splitOscString(buffer);
return new DataView(concat([toOscString(transform(value)), rest]));
};
export const messageTransform = (transform) => (buffer) => toOscMessage(transform(fromOscMessage(buffer)));
class OSCError extends Error {
constructor(message) {
super(message);
this.name = "OSCError";
}
}
const isOscBundleBuffer = (buffer) => {
const string = splitOscString(buffer).value;
return string === "#bundle";
};
const mapBundleList = (buffer, func) => {
let view = toView(buffer);
const results = new Array();
while (view.byteLength) {
const { value: size, rest } = splitInteger(view);
view = rest;
if (size > view.byteLength) {
throw new OSCError("Invalid bundle list: size of element is bigger than buffer");
}
const subView = new DataView(view.buffer, view.byteOffset, size);
try {
// If there's an exception thrown from the map function, just ignore
// this result.
results.push(func(subView));
}
catch (_) {
/* empty */
}
view = sliceDataView(view, size);
}
return results;
};