diff --git a/src/models.ts b/src/models.ts index bd1f695..41d90e7 100644 --- a/src/models.ts +++ b/src/models.ts @@ -1,7 +1,14 @@ -export type OSCType = 's' | 'i' | 'f' | 'b' | 'T' | 'F'; +export type OSCType = 's' | 'i' | 'f' | 'b' | 'T' | 'F' | 't'; export type OSCArg = { type: OSCType; - value: string | number | Buffer | boolean; + value: string | number | Buffer | boolean | OSCTimeTag; +}; + +export type OSCTimeTag = [number, number]; + +export type OSCBundle = { + timeTag: OSCTimeTag; + contents: (OSCBundle | OSCMessage)[]; }; export type OSCMessage = { @@ -10,6 +17,6 @@ export type OSCMessage = { }; export type OSCTypeConverter = { - toBuffer: (value: string | number | Buffer | boolean) => Buffer | undefined; - fromBuffer: (buffer: Buffer) => [string | number | Buffer | boolean | undefined, Buffer]; + toBuffer: (value: string | number | Buffer | boolean | OSCTimeTag) => Buffer | undefined; + fromBuffer: (buffer: Buffer) => [string | number | Buffer | boolean | OSCTimeTag | undefined, Buffer]; }; diff --git a/src/osc.ts b/src/osc.ts index 3917645..483a6b7 100644 --- a/src/osc.ts +++ b/src/osc.ts @@ -1,4 +1,4 @@ -import { OSCType, OSCArg, OSCMessage, OSCTypeConverter } from './models'; +import { OSCType, OSCArg, OSCMessage, OSCTypeConverter, OSCBundle, OSCTimeTag } from './models'; const oscTypeConverterMap: { [key: string]: OSCTypeConverter } = { s: { @@ -111,6 +111,46 @@ const oscTypeConverterMap: { [key: string]: OSCTypeConverter } = { return [false, buffer]; }, }, + t: { + toBuffer: (timetag) => { + if (!Array.isArray(timetag)) { + throw new TypeError('osc type t toBuffer called with non array value'); + } + + if (timetag.length != 2) { + throw new TypeError('osc type t array should have exactly 2 elements'); + } + + const seconds = timetag[0]; + if (typeof seconds !== 'number') { + throw new TypeError('osc type t seconds part should be a number'); + } + + const fractional = timetag[1]; + if (typeof fractional !== 'number') { + throw new TypeError('osc type t fractional part should be a number'); + } + + const secondsBuffer = oscTypeConverterMap.i.toBuffer(seconds); + const fractionalBuffer = oscTypeConverterMap.i.toBuffer(fractional); + + if (secondsBuffer && fractionalBuffer) { + return Buffer.concat([secondsBuffer, fractionalBuffer]); + } + }, + fromBuffer: (buffer) => { + if (buffer.length < 8) { + throw new Error('osc time tag must be greater than 8 bytes'); + } + const [seconds, bytesAfterSeconds] = oscTypeConverterMap.i.fromBuffer(buffer); + const [fractional, bytesAfterFractional] = oscTypeConverterMap.i.fromBuffer(bytesAfterSeconds); + if (typeof seconds === 'number' && typeof fractional === 'number') { + const timeTag: OSCTimeTag = [seconds, fractional]; + return [timeTag, bytesAfterFractional]; + } + return [undefined, bytesAfterFractional]; + }, + }, }; function argsToBuffer(args: OSCArg[]) { @@ -186,3 +226,90 @@ export function messageFromBuffer(bytes: Buffer): OSCMessage | undefined { } } } + +export function bundleFromBuffer(bytes: Buffer): OSCBundle | undefined { + if (bytes.length < 8) { + throw new Error('bundle has to be at least 20 bytes'); + } + + if (bytes.subarray(0, 7).toString('ascii') !== '#bundle') { + throw new Error('bundle must start with #bundle'); + } + + const [bundleHeader, bytesAfterHeader] = oscTypeConverterMap.s.fromBuffer(bytes); + let [timeTag, bytesAfterTimeTag] = oscTypeConverterMap.t.fromBuffer(bytesAfterHeader); + + if (!Array.isArray(timeTag)) { + throw new Error('problem getting bundle time tag'); + } + + const bundleContents = []; + + let endOfBundle = false; + + let [contentSize, remainingBytes] = oscTypeConverterMap.i.fromBuffer(bytesAfterTimeTag); + if (typeof contentSize === 'number') { + while (!endOfBundle) { + if (remainingBytes.length < contentSize) { + throw new Error('bundle does not contain enough data'); + } + + const bundleContentBytes = remainingBytes.subarray(0, contentSize); + console.log(bundleContentBytes); + + if (bundleContentBytes[0] === 35) { + // # character indicating contents is a bundle + const content = bundleFromBuffer(bundleContentBytes); + if (content) { + bundleContents.push(content); + } + } else if (bundleContentBytes[0] === 47) { + const content = messageFromBuffer(bundleContentBytes); + if (content) { + bundleContents.push(content); + } + } else { + throw new Error('bundle contents does not look like a OSC message or bundle'); + } + + remainingBytes = remainingBytes.subarray(contentSize); + if (remainingBytes.length === 0) { + endOfBundle = true; + } + } + } + + return { + timeTag, + contents: bundleContents, + }; +} + +export function bundleToBuffer(bundle: OSCBundle): Buffer { + const headerBuffer = oscTypeConverterMap.s.toBuffer('#bundle'); + + if (headerBuffer === undefined) { + throw new Error('problem encoding buffer header'); + } + + const timeTagBuffer = oscTypeConverterMap.t.toBuffer(bundle.timeTag); + + if (timeTagBuffer === undefined) { + throw new Error('problem encoding buffer time tag'); + } + + const contentsBuffers: Buffer[] = []; + + bundle.contents.forEach((bundleContent) => { + if ('address' in bundleContent) { + const contentBuffer = messageToBuffer(bundleContent); + const contentSizeBuffer = oscTypeConverterMap.i.toBuffer(contentBuffer.length); + + if (contentBuffer && contentSizeBuffer) { + contentsBuffers.push(Buffer.concat([contentSizeBuffer, contentBuffer])); + } + } + }); + + return Buffer.concat([headerBuffer, timeTagBuffer, ...contentsBuffers]); +}