From 0f17edc03d1296835fa3e7a0adfe03ac32154368 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Tue, 8 Oct 2024 19:52:58 -0500 Subject: [PATCH 1/2] add bundleFromBuffer and bundleToBuffer --- src/models.ts | 15 ++++-- src/osc.ts | 129 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 139 insertions(+), 5 deletions(-) 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]); +} From fbaf0727cc3bd87aada7b55c447355a4d43b0759 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Tue, 8 Oct 2024 19:53:10 -0500 Subject: [PATCH 2/2] add real basic bundle tests --- tests/bundle-decode.test.js | 29 +++++++++++++++++++++++++++++ tests/bundle-encode.test.js | 26 ++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 tests/bundle-decode.test.js create mode 100644 tests/bundle-encode.test.js diff --git a/tests/bundle-decode.test.js b/tests/bundle-decode.test.js new file mode 100644 index 0000000..6a559dc --- /dev/null +++ b/tests/bundle-decode.test.js @@ -0,0 +1,29 @@ +const { deepEqual } = require('assert'); +const { describe, it } = require('node:test'); +const osc = require('../dist/index'); + +const tests = [ + { + description: 'simple contents single message', + expected: { + timeTag: [32, 0], + contents: [{ address: '/oscillator/4/frequency', args: [{ type: 'f', value: 440 }] }], + }, + bundle: Buffer.concat([ + Buffer.from('#bundle', 'ascii'), + Buffer.from([0x00]), + Buffer.from([0, 0, 0, 32, 0, 0, 0, 0]), + Buffer.from('00000020', 'hex'), + Buffer.from('2f6f7363696c6c61746f722f342f6672657175656e6379002c66000043dc0000', 'hex'), + ]), + }, +]; + +describe('OSC Bundle Decoding', () => { + tests.forEach((bundleTest) => { + it(bundleTest.description, () => { + const encoded = osc.bundleFromBuffer(bundleTest.bundle); + deepEqual(encoded, bundleTest.expected); + }); + }); +}); diff --git a/tests/bundle-encode.test.js b/tests/bundle-encode.test.js new file mode 100644 index 0000000..2338cef --- /dev/null +++ b/tests/bundle-encode.test.js @@ -0,0 +1,26 @@ +const { deepEqual, throws } = require('assert'); +const { describe, it } = require('node:test'); +const osc = require('../dist/index'); + +const tests = [ + { + description: 'simple contents single message', + bundle: { timeTag: [32, 0], contents: [{ address: '/oscillator/4/frequency', args: [{ type: 'f', value: 440 }] }] }, + expected: Buffer.concat([ + Buffer.from('#bundle', 'ascii'), + Buffer.from([0x00]), + Buffer.from([0, 0, 0, 32, 0, 0, 0, 0]), + Buffer.from('00000020', 'hex'), + Buffer.from('2f6f7363696c6c61746f722f342f6672657175656e6379002c66000043dc0000', 'hex'), + ]), + }, +]; + +describe('OSC Bundle Encoding', () => { + tests.forEach((bundleTest) => { + it(bundleTest.description, () => { + const encoded = osc.bundleToBuffer(bundleTest.bundle); + deepEqual(encoded, bundleTest.expected); + }); + }); +});