You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1120 lines
36 KiB
JavaScript
1120 lines
36 KiB
JavaScript
7 months ago
|
/*! osc.js 2.4.4, Copyright 2023 Colin Clark | github.com/colinbdclark/osc.js */
|
||
|
|
||
|
/*
|
||
|
* osc.js: An Open Sound Control library for JavaScript that works in both the browser and Node.js
|
||
|
*
|
||
|
* Copyright 2014-2016, Colin Clark
|
||
|
* Licensed under the MIT and GPL 3 licenses.
|
||
|
*/
|
||
|
|
||
|
/* global require, module, process, Buffer, Long, util */
|
||
|
|
||
|
var osc = osc || {};
|
||
|
|
||
|
(function () {
|
||
|
|
||
|
"use strict";
|
||
|
|
||
|
osc.SECS_70YRS = 2208988800;
|
||
|
osc.TWO_32 = 4294967296;
|
||
|
|
||
|
osc.defaults = {
|
||
|
metadata: false,
|
||
|
unpackSingleArgs: true
|
||
|
};
|
||
|
|
||
|
// Unsupported, non-API property.
|
||
|
osc.isCommonJS = typeof module !== "undefined" && module.exports ? true : false;
|
||
|
|
||
|
// Unsupported, non-API property.
|
||
|
osc.isNode = osc.isCommonJS && typeof window === "undefined";
|
||
|
|
||
|
// Unsupported, non-API property.
|
||
|
osc.isElectron = typeof process !== "undefined" &&
|
||
|
process.versions && process.versions.electron ? true : false;
|
||
|
|
||
|
// Unsupported, non-API property.
|
||
|
osc.isBufferEnv = osc.isNode || osc.isElectron;
|
||
|
|
||
|
// Unsupported, non-API function.
|
||
|
osc.isArray = function (obj) {
|
||
|
return obj && Object.prototype.toString.call(obj) === "[object Array]";
|
||
|
};
|
||
|
|
||
|
// Unsupported, non-API function.
|
||
|
osc.isTypedArrayView = function (obj) {
|
||
|
return obj.buffer && obj.buffer instanceof ArrayBuffer;
|
||
|
};
|
||
|
|
||
|
// Unsupported, non-API function.
|
||
|
osc.isBuffer = function (obj) {
|
||
|
return osc.isBufferEnv && obj instanceof Buffer;
|
||
|
};
|
||
|
|
||
|
// Unsupported, non-API member.
|
||
|
osc.Long = typeof Long !== "undefined" ? Long :
|
||
|
osc.isNode ? require("long") : undefined;
|
||
|
|
||
|
// Unsupported, non-API member. Can be removed when supported versions
|
||
|
// of Node.js expose TextDecoder as a global, as in the browser.
|
||
|
osc.TextDecoder = typeof TextDecoder !== "undefined" ? new TextDecoder("utf-8") :
|
||
|
typeof util !== "undefined" && typeof (util.TextDecoder !== "undefined") ? new util.TextDecoder("utf-8") : undefined;
|
||
|
|
||
|
osc.TextEncoder = typeof TextEncoder !== "undefined" ? new TextEncoder("utf-8") :
|
||
|
typeof util !== "undefined" && typeof (util.TextEncoder !== "undefined") ? new util.TextEncoder("utf-8") : undefined;
|
||
|
|
||
|
/**
|
||
|
* Wraps the specified object in a DataView.
|
||
|
*
|
||
|
* @param {Array-like} obj the object to wrap in a DataView instance
|
||
|
* @return {DataView} the DataView object
|
||
|
*/
|
||
|
// Unsupported, non-API function.
|
||
|
osc.dataView = function (obj, offset, length) {
|
||
|
if (obj.buffer) {
|
||
|
return new DataView(obj.buffer, offset, length);
|
||
|
}
|
||
|
|
||
|
if (obj instanceof ArrayBuffer) {
|
||
|
return new DataView(obj, offset, length);
|
||
|
}
|
||
|
|
||
|
return new DataView(new Uint8Array(obj), offset, length);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Takes an ArrayBuffer, TypedArray, DataView, Buffer, or array-like object
|
||
|
* and returns a Uint8Array view of it.
|
||
|
*
|
||
|
* Throws an error if the object isn't suitably array-like.
|
||
|
*
|
||
|
* @param {Array-like or Array-wrapping} obj an array-like or array-wrapping object
|
||
|
* @returns {Uint8Array} a typed array of octets
|
||
|
*/
|
||
|
// Unsupported, non-API function.
|
||
|
osc.byteArray = function (obj) {
|
||
|
if (obj instanceof Uint8Array) {
|
||
|
return obj;
|
||
|
}
|
||
|
|
||
|
var buf = obj.buffer ? obj.buffer : obj;
|
||
|
|
||
|
if (!(buf instanceof ArrayBuffer) && (typeof buf.length === "undefined" || typeof buf === "string")) {
|
||
|
throw new Error("Can't wrap a non-array-like object as Uint8Array. Object was: " +
|
||
|
JSON.stringify(obj, null, 2));
|
||
|
}
|
||
|
|
||
|
|
||
|
// TODO gh-39: This is a potentially unsafe algorithm;
|
||
|
// if we're getting anything other than a TypedArrayView (such as a DataView),
|
||
|
// we really need to determine the range of the view it is viewing.
|
||
|
return new Uint8Array(buf);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Takes an ArrayBuffer, TypedArray, DataView, or array-like object
|
||
|
* and returns a native buffer object
|
||
|
* (i.e. in Node.js, a Buffer object and in the browser, a Uint8Array).
|
||
|
*
|
||
|
* Throws an error if the object isn't suitably array-like.
|
||
|
*
|
||
|
* @param {Array-like or Array-wrapping} obj an array-like or array-wrapping object
|
||
|
* @returns {Buffer|Uint8Array} a buffer object
|
||
|
*/
|
||
|
// Unsupported, non-API function.
|
||
|
osc.nativeBuffer = function (obj) {
|
||
|
if (osc.isBufferEnv) {
|
||
|
return osc.isBuffer(obj) ? obj :
|
||
|
Buffer.from(obj.buffer ? obj : new Uint8Array(obj));
|
||
|
}
|
||
|
|
||
|
return osc.isTypedArrayView(obj) ? obj : new Uint8Array(obj);
|
||
|
};
|
||
|
|
||
|
// Unsupported, non-API function
|
||
|
osc.copyByteArray = function (source, target, offset) {
|
||
|
if (osc.isTypedArrayView(source) && osc.isTypedArrayView(target)) {
|
||
|
target.set(source, offset);
|
||
|
} else {
|
||
|
var start = offset === undefined ? 0 : offset,
|
||
|
len = Math.min(target.length - offset, source.length);
|
||
|
|
||
|
for (var i = 0, j = start; i < len; i++, j++) {
|
||
|
target[j] = source[i];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return target;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Reads an OSC-formatted string.
|
||
|
*
|
||
|
* @param {DataView} dv a DataView containing the raw bytes of the OSC string
|
||
|
* @param {Object} offsetState an offsetState object used to store the current offset index
|
||
|
* @return {String} the JavaScript String that was read
|
||
|
*/
|
||
|
osc.readString = function (dv, offsetState) {
|
||
|
var charCodes = [],
|
||
|
idx = offsetState.idx;
|
||
|
|
||
|
for (; idx < dv.byteLength; idx++) {
|
||
|
var charCode = dv.getUint8(idx);
|
||
|
if (charCode !== 0) {
|
||
|
charCodes.push(charCode);
|
||
|
} else {
|
||
|
idx++;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Round to the nearest 4-byte block.
|
||
|
idx = (idx + 3) & ~0x03;
|
||
|
offsetState.idx = idx;
|
||
|
|
||
|
var decoder = osc.isBufferEnv ? osc.readString.withBuffer :
|
||
|
osc.TextDecoder ? osc.readString.withTextDecoder : osc.readString.raw;
|
||
|
|
||
|
return decoder(charCodes);
|
||
|
};
|
||
|
|
||
|
osc.readString.raw = function (charCodes) {
|
||
|
// If no Buffer or TextDecoder, resort to fromCharCode
|
||
|
// This does not properly decode multi-byte Unicode characters.
|
||
|
var str = "";
|
||
|
var sliceSize = 10000;
|
||
|
|
||
|
// Processing the array in chunks so as not to exceed argument
|
||
|
// limit, see https://bugs.webkit.org/show_bug.cgi?id=80797
|
||
|
for (var i = 0; i < charCodes.length; i += sliceSize) {
|
||
|
str += String.fromCharCode.apply(null, charCodes.slice(i, i + sliceSize));
|
||
|
}
|
||
|
|
||
|
return str;
|
||
|
};
|
||
|
|
||
|
osc.readString.withTextDecoder = function (charCodes) {
|
||
|
var data = new Int8Array(charCodes);
|
||
|
return osc.TextDecoder.decode(data);
|
||
|
};
|
||
|
|
||
|
osc.readString.withBuffer = function (charCodes) {
|
||
|
return Buffer.from(charCodes).toString("utf-8");
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Writes a JavaScript string as an OSC-formatted string.
|
||
|
*
|
||
|
* @param {String} str the string to write
|
||
|
* @return {Uint8Array} a buffer containing the OSC-formatted string
|
||
|
*/
|
||
|
osc.writeString = function (str) {
|
||
|
|
||
|
var encoder = osc.isBufferEnv ? osc.writeString.withBuffer :
|
||
|
osc.TextEncoder ? osc.writeString.withTextEncoder : null,
|
||
|
terminated = str + "\u0000",
|
||
|
encodedStr;
|
||
|
|
||
|
if (encoder) {
|
||
|
encodedStr = encoder(terminated);
|
||
|
}
|
||
|
|
||
|
var len = encoder ? encodedStr.length : terminated.length,
|
||
|
paddedLen = (len + 3) & ~0x03,
|
||
|
arr = new Uint8Array(paddedLen);
|
||
|
|
||
|
for (var i = 0; i < len - 1; i++) {
|
||
|
var charCode = encoder ? encodedStr[i] : terminated.charCodeAt(i);
|
||
|
arr[i] = charCode;
|
||
|
}
|
||
|
|
||
|
return arr;
|
||
|
};
|
||
|
|
||
|
osc.writeString.withTextEncoder = function (str) {
|
||
|
return osc.TextEncoder.encode(str);
|
||
|
};
|
||
|
|
||
|
osc.writeString.withBuffer = function (str) {
|
||
|
return Buffer.from(str);
|
||
|
};
|
||
|
|
||
|
// Unsupported, non-API function.
|
||
|
osc.readPrimitive = function (dv, readerName, numBytes, offsetState) {
|
||
|
var val = dv[readerName](offsetState.idx, false);
|
||
|
offsetState.idx += numBytes;
|
||
|
|
||
|
return val;
|
||
|
};
|
||
|
|
||
|
// Unsupported, non-API function.
|
||
|
osc.writePrimitive = function (val, dv, writerName, numBytes, offset) {
|
||
|
offset = offset === undefined ? 0 : offset;
|
||
|
|
||
|
var arr;
|
||
|
if (!dv) {
|
||
|
arr = new Uint8Array(numBytes);
|
||
|
dv = new DataView(arr.buffer);
|
||
|
} else {
|
||
|
arr = new Uint8Array(dv.buffer);
|
||
|
}
|
||
|
|
||
|
dv[writerName](offset, val, false);
|
||
|
|
||
|
return arr;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Reads an OSC int32 ("i") value.
|
||
|
*
|
||
|
* @param {DataView} dv a DataView containing the raw bytes
|
||
|
* @param {Object} offsetState an offsetState object used to store the current offset index into dv
|
||
|
* @return {Number} the number that was read
|
||
|
*/
|
||
|
osc.readInt32 = function (dv, offsetState) {
|
||
|
return osc.readPrimitive(dv, "getInt32", 4, offsetState);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Writes an OSC int32 ("i") value.
|
||
|
*
|
||
|
* @param {Number} val the number to write
|
||
|
* @param {DataView} [dv] a DataView instance to write the number into
|
||
|
* @param {Number} [offset] an offset into dv
|
||
|
*/
|
||
|
osc.writeInt32 = function (val, dv, offset) {
|
||
|
return osc.writePrimitive(val, dv, "setInt32", 4, offset);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Reads an OSC int64 ("h") value.
|
||
|
*
|
||
|
* @param {DataView} dv a DataView containing the raw bytes
|
||
|
* @param {Object} offsetState an offsetState object used to store the current offset index into dv
|
||
|
* @return {Number} the number that was read
|
||
|
*/
|
||
|
osc.readInt64 = function (dv, offsetState) {
|
||
|
var high = osc.readPrimitive(dv, "getInt32", 4, offsetState),
|
||
|
low = osc.readPrimitive(dv, "getInt32", 4, offsetState);
|
||
|
|
||
|
if (osc.Long) {
|
||
|
return new osc.Long(low, high);
|
||
|
} else {
|
||
|
return {
|
||
|
high: high,
|
||
|
low: low,
|
||
|
unsigned: false
|
||
|
};
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Writes an OSC int64 ("h") value.
|
||
|
*
|
||
|
* @param {Number} val the number to write
|
||
|
* @param {DataView} [dv] a DataView instance to write the number into
|
||
|
* @param {Number} [offset] an offset into dv
|
||
|
*/
|
||
|
osc.writeInt64 = function (val, dv, offset) {
|
||
|
var arr = new Uint8Array(8);
|
||
|
arr.set(osc.writePrimitive(val.high, dv, "setInt32", 4, offset), 0);
|
||
|
arr.set(osc.writePrimitive(val.low, dv, "setInt32", 4, offset + 4), 4);
|
||
|
return arr;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Reads an OSC float32 ("f") value.
|
||
|
*
|
||
|
* @param {DataView} dv a DataView containing the raw bytes
|
||
|
* @param {Object} offsetState an offsetState object used to store the current offset index into dv
|
||
|
* @return {Number} the number that was read
|
||
|
*/
|
||
|
osc.readFloat32 = function (dv, offsetState) {
|
||
|
return osc.readPrimitive(dv, "getFloat32", 4, offsetState);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Writes an OSC float32 ("f") value.
|
||
|
*
|
||
|
* @param {Number} val the number to write
|
||
|
* @param {DataView} [dv] a DataView instance to write the number into
|
||
|
* @param {Number} [offset] an offset into dv
|
||
|
*/
|
||
|
osc.writeFloat32 = function (val, dv, offset) {
|
||
|
return osc.writePrimitive(val, dv, "setFloat32", 4, offset);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Reads an OSC float64 ("d") value.
|
||
|
*
|
||
|
* @param {DataView} dv a DataView containing the raw bytes
|
||
|
* @param {Object} offsetState an offsetState object used to store the current offset index into dv
|
||
|
* @return {Number} the number that was read
|
||
|
*/
|
||
|
osc.readFloat64 = function (dv, offsetState) {
|
||
|
return osc.readPrimitive(dv, "getFloat64", 8, offsetState);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Writes an OSC float64 ("d") value.
|
||
|
*
|
||
|
* @param {Number} val the number to write
|
||
|
* @param {DataView} [dv] a DataView instance to write the number into
|
||
|
* @param {Number} [offset] an offset into dv
|
||
|
*/
|
||
|
osc.writeFloat64 = function (val, dv, offset) {
|
||
|
return osc.writePrimitive(val, dv, "setFloat64", 8, offset);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Reads an OSC 32-bit ASCII character ("c") value.
|
||
|
*
|
||
|
* @param {DataView} dv a DataView containing the raw bytes
|
||
|
* @param {Object} offsetState an offsetState object used to store the current offset index into dv
|
||
|
* @return {String} a string containing the read character
|
||
|
*/
|
||
|
osc.readChar32 = function (dv, offsetState) {
|
||
|
var charCode = osc.readPrimitive(dv, "getUint32", 4, offsetState);
|
||
|
return String.fromCharCode(charCode);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Writes an OSC 32-bit ASCII character ("c") value.
|
||
|
*
|
||
|
* @param {String} str the string from which the first character will be written
|
||
|
* @param {DataView} [dv] a DataView instance to write the character into
|
||
|
* @param {Number} [offset] an offset into dv
|
||
|
* @return {String} a string containing the read character
|
||
|
*/
|
||
|
osc.writeChar32 = function (str, dv, offset) {
|
||
|
var charCode = str.charCodeAt(0);
|
||
|
if (charCode === undefined || charCode < -1) {
|
||
|
return undefined;
|
||
|
}
|
||
|
|
||
|
return osc.writePrimitive(charCode, dv, "setUint32", 4, offset);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Reads an OSC blob ("b") (i.e. a Uint8Array).
|
||
|
*
|
||
|
* @param {DataView} dv a DataView instance to read from
|
||
|
* @param {Object} offsetState an offsetState object used to store the current offset index into dv
|
||
|
* @return {Uint8Array} the data that was read
|
||
|
*/
|
||
|
osc.readBlob = function (dv, offsetState) {
|
||
|
var len = osc.readInt32(dv, offsetState),
|
||
|
paddedLen = (len + 3) & ~0x03,
|
||
|
blob = new Uint8Array(dv.buffer, offsetState.idx, len);
|
||
|
|
||
|
offsetState.idx += paddedLen;
|
||
|
|
||
|
return blob;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Writes a raw collection of bytes to a new ArrayBuffer.
|
||
|
*
|
||
|
* @param {Array-like} data a collection of octets
|
||
|
* @return {ArrayBuffer} a buffer containing the OSC-formatted blob
|
||
|
*/
|
||
|
osc.writeBlob = function (data) {
|
||
|
data = osc.byteArray(data);
|
||
|
|
||
|
var len = data.byteLength,
|
||
|
paddedLen = (len + 3) & ~0x03,
|
||
|
offset = 4, // Extra 4 bytes is for the size.
|
||
|
blobLen = paddedLen + offset,
|
||
|
arr = new Uint8Array(blobLen),
|
||
|
dv = new DataView(arr.buffer);
|
||
|
|
||
|
// Write the size.
|
||
|
osc.writeInt32(len, dv);
|
||
|
|
||
|
// Since we're writing to a real ArrayBuffer,
|
||
|
// we don't need to pad the remaining bytes.
|
||
|
arr.set(data, offset);
|
||
|
|
||
|
return arr;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Reads an OSC 4-byte MIDI message.
|
||
|
*
|
||
|
* @param {DataView} dv the DataView instance to read from
|
||
|
* @param {Object} offsetState an offsetState object used to store the current offset index into dv
|
||
|
* @return {Uint8Array} an array containing (in order) the port ID, status, data1 and data1 bytes
|
||
|
*/
|
||
|
osc.readMIDIBytes = function (dv, offsetState) {
|
||
|
var midi = new Uint8Array(dv.buffer, offsetState.idx, 4);
|
||
|
offsetState.idx += 4;
|
||
|
|
||
|
return midi;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Writes an OSC 4-byte MIDI message.
|
||
|
*
|
||
|
* @param {Array-like} bytes a 4-element array consisting of the port ID, status, data1 and data1 bytes
|
||
|
* @return {Uint8Array} the written message
|
||
|
*/
|
||
|
osc.writeMIDIBytes = function (bytes) {
|
||
|
bytes = osc.byteArray(bytes);
|
||
|
|
||
|
var arr = new Uint8Array(4);
|
||
|
arr.set(bytes);
|
||
|
|
||
|
return arr;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Reads an OSC RGBA colour value.
|
||
|
*
|
||
|
* @param {DataView} dv the DataView instance to read from
|
||
|
* @param {Object} offsetState an offsetState object used to store the current offset index into dv
|
||
|
* @return {Object} a colour object containing r, g, b, and a properties
|
||
|
*/
|
||
|
osc.readColor = function (dv, offsetState) {
|
||
|
var bytes = new Uint8Array(dv.buffer, offsetState.idx, 4),
|
||
|
alpha = bytes[3] / 255;
|
||
|
|
||
|
offsetState.idx += 4;
|
||
|
|
||
|
return {
|
||
|
r: bytes[0],
|
||
|
g: bytes[1],
|
||
|
b: bytes[2],
|
||
|
a: alpha
|
||
|
};
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Writes an OSC RGBA colour value.
|
||
|
*
|
||
|
* @param {Object} color a colour object containing r, g, b, and a properties
|
||
|
* @return {Uint8Array} a byte array containing the written color
|
||
|
*/
|
||
|
osc.writeColor = function (color) {
|
||
|
var alpha = Math.round(color.a * 255),
|
||
|
arr = new Uint8Array([color.r, color.g, color.b, alpha]);
|
||
|
|
||
|
return arr;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Reads an OSC true ("T") value by directly returning the JavaScript Boolean "true".
|
||
|
*/
|
||
|
osc.readTrue = function () {
|
||
|
return true;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Reads an OSC false ("F") value by directly returning the JavaScript Boolean "false".
|
||
|
*/
|
||
|
osc.readFalse = function () {
|
||
|
return false;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Reads an OSC nil ("N") value by directly returning the JavaScript "null" value.
|
||
|
*/
|
||
|
osc.readNull = function () {
|
||
|
return null;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Reads an OSC impulse/bang/infinitum ("I") value by directly returning 1.0.
|
||
|
*/
|
||
|
osc.readImpulse = function () {
|
||
|
return 1.0;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Reads an OSC time tag ("t").
|
||
|
*
|
||
|
* @param {DataView} dv the DataView instance to read from
|
||
|
* @param {Object} offsetState an offset state object containing the current index into dv
|
||
|
* @param {Object} a time tag object containing both the raw NTP as well as the converted native (i.e. JS/UNIX) time
|
||
|
*/
|
||
|
osc.readTimeTag = function (dv, offsetState) {
|
||
|
var secs1900 = osc.readPrimitive(dv, "getUint32", 4, offsetState),
|
||
|
frac = osc.readPrimitive(dv, "getUint32", 4, offsetState),
|
||
|
native = (secs1900 === 0 && frac === 1) ? Date.now() : osc.ntpToJSTime(secs1900, frac);
|
||
|
|
||
|
return {
|
||
|
raw: [secs1900, frac],
|
||
|
native: native
|
||
|
};
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Writes an OSC time tag ("t").
|
||
|
*
|
||
|
* Takes, as its argument, a time tag object containing either a "raw" or "native property."
|
||
|
* The raw timestamp must conform to the NTP standard representation, consisting of two unsigned int32
|
||
|
* values. The first represents the number of seconds since January 1, 1900; the second, fractions of a second.
|
||
|
* "Native" JavaScript timestamps are specified as a Number representing milliseconds since January 1, 1970.
|
||
|
*
|
||
|
* @param {Object} timeTag time tag object containing either a native JS timestamp (in ms) or a NTP timestamp pair
|
||
|
* @return {Uint8Array} raw bytes for the written time tag
|
||
|
*/
|
||
|
osc.writeTimeTag = function (timeTag) {
|
||
|
var raw = timeTag.raw ? timeTag.raw : osc.jsToNTPTime(timeTag.native),
|
||
|
arr = new Uint8Array(8), // Two Unit32s.
|
||
|
dv = new DataView(arr.buffer);
|
||
|
|
||
|
osc.writeInt32(raw[0], dv, 0);
|
||
|
osc.writeInt32(raw[1], dv, 4);
|
||
|
|
||
|
return arr;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Produces a time tag containing a raw NTP timestamp
|
||
|
* relative to now by the specified number of seconds.
|
||
|
*
|
||
|
* @param {Number} secs the number of seconds relative to now (i.e. + for the future, - for the past)
|
||
|
* @param {Number} now the number of milliseconds since epoch to use as the current time. Defaults to Date.now()
|
||
|
* @return {Object} the time tag
|
||
|
*/
|
||
|
osc.timeTag = function (secs, now) {
|
||
|
secs = secs || 0;
|
||
|
now = now || Date.now();
|
||
|
|
||
|
var nowSecs = now / 1000,
|
||
|
nowWhole = Math.floor(nowSecs),
|
||
|
nowFracs = nowSecs - nowWhole,
|
||
|
secsWhole = Math.floor(secs),
|
||
|
secsFracs = secs - secsWhole,
|
||
|
fracs = nowFracs + secsFracs;
|
||
|
|
||
|
if (fracs > 1) {
|
||
|
var fracsWhole = Math.floor(fracs),
|
||
|
fracsFracs = fracs - fracsWhole;
|
||
|
|
||
|
secsWhole += fracsWhole;
|
||
|
fracs = fracsFracs;
|
||
|
}
|
||
|
|
||
|
var ntpSecs = nowWhole + secsWhole + osc.SECS_70YRS,
|
||
|
ntpFracs = Math.round(osc.TWO_32 * fracs);
|
||
|
|
||
|
return {
|
||
|
raw: [ntpSecs, ntpFracs]
|
||
|
};
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Converts OSC's standard time tag representation (which is the NTP format)
|
||
|
* into the JavaScript/UNIX format in milliseconds.
|
||
|
*
|
||
|
* @param {Number} secs1900 the number of seconds since 1900
|
||
|
* @param {Number} frac the number of fractions of a second (between 0 and 2^32)
|
||
|
* @return {Number} a JavaScript-compatible timestamp in milliseconds
|
||
|
*/
|
||
|
osc.ntpToJSTime = function (secs1900, frac) {
|
||
|
var secs1970 = secs1900 - osc.SECS_70YRS,
|
||
|
decimals = frac / osc.TWO_32,
|
||
|
msTime = (secs1970 + decimals) * 1000;
|
||
|
|
||
|
return msTime;
|
||
|
};
|
||
|
|
||
|
osc.jsToNTPTime = function (jsTime) {
|
||
|
var secs = jsTime / 1000,
|
||
|
secsWhole = Math.floor(secs),
|
||
|
secsFrac = secs - secsWhole,
|
||
|
ntpSecs = secsWhole + osc.SECS_70YRS,
|
||
|
ntpFracs = Math.round(osc.TWO_32 * secsFrac);
|
||
|
|
||
|
return [ntpSecs, ntpFracs];
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Reads the argument portion of an OSC message.
|
||
|
*
|
||
|
* @param {DataView} dv a DataView instance to read from
|
||
|
* @param {Object} offsetState the offsetState object that stores the current offset into dv
|
||
|
* @param {Object} [options] read options
|
||
|
* @return {Array} an array of the OSC arguments that were read
|
||
|
*/
|
||
|
osc.readArguments = function (dv, options, offsetState) {
|
||
|
var typeTagString = osc.readString(dv, offsetState);
|
||
|
if (typeTagString.indexOf(",") !== 0) {
|
||
|
// Despite what the OSC 1.0 spec says,
|
||
|
// it just doesn't make sense to handle messages without type tags.
|
||
|
// scsynth appears to read such messages as if they have a single
|
||
|
// Uint8 argument. sclang throws an error if the type tag is omitted.
|
||
|
throw new Error("A malformed type tag string was found while reading " +
|
||
|
"the arguments of an OSC message. String was: " +
|
||
|
typeTagString, " at offset: " + offsetState.idx);
|
||
|
}
|
||
|
|
||
|
var argTypes = typeTagString.substring(1).split(""),
|
||
|
args = [];
|
||
|
|
||
|
osc.readArgumentsIntoArray(args, argTypes, typeTagString, dv, options, offsetState);
|
||
|
|
||
|
return args;
|
||
|
};
|
||
|
|
||
|
// Unsupported, non-API function.
|
||
|
osc.readArgument = function (argType, typeTagString, dv, options, offsetState) {
|
||
|
var typeSpec = osc.argumentTypes[argType];
|
||
|
if (!typeSpec) {
|
||
|
throw new Error("'" + argType + "' is not a valid OSC type tag. Type tag string was: " + typeTagString);
|
||
|
}
|
||
|
|
||
|
var argReader = typeSpec.reader,
|
||
|
arg = osc[argReader](dv, offsetState);
|
||
|
|
||
|
if (options.metadata) {
|
||
|
arg = {
|
||
|
type: argType,
|
||
|
value: arg
|
||
|
};
|
||
|
}
|
||
|
|
||
|
return arg;
|
||
|
};
|
||
|
|
||
|
// Unsupported, non-API function.
|
||
|
osc.readArgumentsIntoArray = function (arr, argTypes, typeTagString, dv, options, offsetState) {
|
||
|
var i = 0;
|
||
|
|
||
|
while (i < argTypes.length) {
|
||
|
var argType = argTypes[i],
|
||
|
arg;
|
||
|
|
||
|
if (argType === "[") {
|
||
|
var fromArrayOpen = argTypes.slice(i + 1),
|
||
|
endArrayIdx = fromArrayOpen.indexOf("]");
|
||
|
|
||
|
if (endArrayIdx < 0) {
|
||
|
throw new Error("Invalid argument type tag: an open array type tag ('[') was found " +
|
||
|
"without a matching close array tag ('[]'). Type tag was: " + typeTagString);
|
||
|
}
|
||
|
|
||
|
var typesInArray = fromArrayOpen.slice(0, endArrayIdx);
|
||
|
arg = osc.readArgumentsIntoArray([], typesInArray, typeTagString, dv, options, offsetState);
|
||
|
i += endArrayIdx + 2;
|
||
|
} else {
|
||
|
arg = osc.readArgument(argType, typeTagString, dv, options, offsetState);
|
||
|
i++;
|
||
|
}
|
||
|
|
||
|
arr.push(arg);
|
||
|
}
|
||
|
|
||
|
return arr;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Writes the specified arguments.
|
||
|
*
|
||
|
* @param {Array} args an array of arguments
|
||
|
* @param {Object} options options for writing
|
||
|
* @return {Uint8Array} a buffer containing the OSC-formatted argument type tag and values
|
||
|
*/
|
||
|
osc.writeArguments = function (args, options) {
|
||
|
var argCollection = osc.collectArguments(args, options);
|
||
|
return osc.joinParts(argCollection);
|
||
|
};
|
||
|
|
||
|
// Unsupported, non-API function.
|
||
|
osc.joinParts = function (dataCollection) {
|
||
|
var buf = new Uint8Array(dataCollection.byteLength),
|
||
|
parts = dataCollection.parts,
|
||
|
offset = 0;
|
||
|
|
||
|
for (var i = 0; i < parts.length; i++) {
|
||
|
var part = parts[i];
|
||
|
osc.copyByteArray(part, buf, offset);
|
||
|
offset += part.length;
|
||
|
}
|
||
|
|
||
|
return buf;
|
||
|
};
|
||
|
|
||
|
// Unsupported, non-API function.
|
||
|
osc.addDataPart = function (dataPart, dataCollection) {
|
||
|
dataCollection.parts.push(dataPart);
|
||
|
dataCollection.byteLength += dataPart.length;
|
||
|
};
|
||
|
|
||
|
osc.writeArrayArguments = function (args, dataCollection) {
|
||
|
var typeTag = "[";
|
||
|
|
||
|
for (var i = 0; i < args.length; i++) {
|
||
|
var arg = args[i];
|
||
|
typeTag += osc.writeArgument(arg, dataCollection);
|
||
|
}
|
||
|
|
||
|
typeTag += "]";
|
||
|
|
||
|
return typeTag;
|
||
|
};
|
||
|
|
||
|
osc.writeArgument = function (arg, dataCollection) {
|
||
|
if (osc.isArray(arg)) {
|
||
|
return osc.writeArrayArguments(arg, dataCollection);
|
||
|
}
|
||
|
|
||
|
var type = arg.type,
|
||
|
writer = osc.argumentTypes[type].writer;
|
||
|
|
||
|
if (writer) {
|
||
|
var data = osc[writer](arg.value);
|
||
|
osc.addDataPart(data, dataCollection);
|
||
|
}
|
||
|
|
||
|
return arg.type;
|
||
|
};
|
||
|
|
||
|
// Unsupported, non-API function.
|
||
|
osc.collectArguments = function (args, options, dataCollection) {
|
||
|
if (!osc.isArray(args)) {
|
||
|
args = typeof args === "undefined" ? [] : [args];
|
||
|
}
|
||
|
|
||
|
dataCollection = dataCollection || {
|
||
|
byteLength: 0,
|
||
|
parts: []
|
||
|
};
|
||
|
|
||
|
if (!options.metadata) {
|
||
|
args = osc.annotateArguments(args);
|
||
|
}
|
||
|
|
||
|
var typeTagString = ",",
|
||
|
currPartIdx = dataCollection.parts.length;
|
||
|
|
||
|
for (var i = 0; i < args.length; i++) {
|
||
|
var arg = args[i];
|
||
|
typeTagString += osc.writeArgument(arg, dataCollection);
|
||
|
}
|
||
|
|
||
|
var typeData = osc.writeString(typeTagString);
|
||
|
dataCollection.byteLength += typeData.byteLength;
|
||
|
dataCollection.parts.splice(currPartIdx, 0, typeData);
|
||
|
|
||
|
return dataCollection;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Reads an OSC message.
|
||
|
*
|
||
|
* @param {Array-like} data an array of bytes to read from
|
||
|
* @param {Object} [options] read options
|
||
|
* @param {Object} [offsetState] an offsetState object that stores the current offset into dv
|
||
|
* @return {Object} the OSC message, formatted as a JavaScript object containing "address" and "args" properties
|
||
|
*/
|
||
|
osc.readMessage = function (data, options, offsetState) {
|
||
|
options = options || osc.defaults;
|
||
|
|
||
|
var dv = osc.dataView(data, data.byteOffset, data.byteLength);
|
||
|
offsetState = offsetState || {
|
||
|
idx: 0
|
||
|
};
|
||
|
|
||
|
var address = osc.readString(dv, offsetState);
|
||
|
return osc.readMessageContents(address, dv, options, offsetState);
|
||
|
};
|
||
|
|
||
|
// Unsupported, non-API function.
|
||
|
osc.readMessageContents = function (address, dv, options, offsetState) {
|
||
|
if (address.indexOf("/") !== 0) {
|
||
|
throw new Error("A malformed OSC address was found while reading " +
|
||
|
"an OSC message. String was: " + address);
|
||
|
}
|
||
|
|
||
|
var args = osc.readArguments(dv, options, offsetState);
|
||
|
|
||
|
return {
|
||
|
address: address,
|
||
|
args: args.length === 1 && options.unpackSingleArgs ? args[0] : args
|
||
|
};
|
||
|
};
|
||
|
|
||
|
// Unsupported, non-API function.
|
||
|
osc.collectMessageParts = function (msg, options, dataCollection) {
|
||
|
dataCollection = dataCollection || {
|
||
|
byteLength: 0,
|
||
|
parts: []
|
||
|
};
|
||
|
|
||
|
osc.addDataPart(osc.writeString(msg.address), dataCollection);
|
||
|
return osc.collectArguments(msg.args, options, dataCollection);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Writes an OSC message.
|
||
|
*
|
||
|
* @param {Object} msg a message object containing "address" and "args" properties
|
||
|
* @param {Object} [options] write options
|
||
|
* @return {Uint8Array} an array of bytes containing the OSC message
|
||
|
*/
|
||
|
osc.writeMessage = function (msg, options) {
|
||
|
options = options || osc.defaults;
|
||
|
|
||
|
if (!osc.isValidMessage(msg)) {
|
||
|
throw new Error("An OSC message must contain a valid address. Message was: " +
|
||
|
JSON.stringify(msg, null, 2));
|
||
|
}
|
||
|
|
||
|
var msgCollection = osc.collectMessageParts(msg, options);
|
||
|
return osc.joinParts(msgCollection);
|
||
|
};
|
||
|
|
||
|
osc.isValidMessage = function (msg) {
|
||
|
return msg.address && msg.address.indexOf("/") === 0;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Reads an OSC bundle.
|
||
|
*
|
||
|
* @param {DataView} dv the DataView instance to read from
|
||
|
* @param {Object} [options] read optoins
|
||
|
* @param {Object} [offsetState] an offsetState object that stores the current offset into dv
|
||
|
* @return {Object} the bundle or message object that was read
|
||
|
*/
|
||
|
osc.readBundle = function (dv, options, offsetState) {
|
||
|
return osc.readPacket(dv, options, offsetState);
|
||
|
};
|
||
|
|
||
|
// Unsupported, non-API function.
|
||
|
osc.collectBundlePackets = function (bundle, options, dataCollection) {
|
||
|
dataCollection = dataCollection || {
|
||
|
byteLength: 0,
|
||
|
parts: []
|
||
|
};
|
||
|
|
||
|
osc.addDataPart(osc.writeString("#bundle"), dataCollection);
|
||
|
osc.addDataPart(osc.writeTimeTag(bundle.timeTag), dataCollection);
|
||
|
|
||
|
for (var i = 0; i < bundle.packets.length; i++) {
|
||
|
var packet = bundle.packets[i],
|
||
|
collector = packet.address ? osc.collectMessageParts : osc.collectBundlePackets,
|
||
|
packetCollection = collector(packet, options);
|
||
|
|
||
|
dataCollection.byteLength += packetCollection.byteLength;
|
||
|
osc.addDataPart(osc.writeInt32(packetCollection.byteLength), dataCollection);
|
||
|
dataCollection.parts = dataCollection.parts.concat(packetCollection.parts);
|
||
|
}
|
||
|
|
||
|
return dataCollection;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Writes an OSC bundle.
|
||
|
*
|
||
|
* @param {Object} a bundle object containing "timeTag" and "packets" properties
|
||
|
* @param {object} [options] write options
|
||
|
* @return {Uint8Array} an array of bytes containing the message
|
||
|
*/
|
||
|
osc.writeBundle = function (bundle, options) {
|
||
|
if (!osc.isValidBundle(bundle)) {
|
||
|
throw new Error("An OSC bundle must contain 'timeTag' and 'packets' properties. " +
|
||
|
"Bundle was: " + JSON.stringify(bundle, null, 2));
|
||
|
}
|
||
|
|
||
|
options = options || osc.defaults;
|
||
|
var bundleCollection = osc.collectBundlePackets(bundle, options);
|
||
|
|
||
|
return osc.joinParts(bundleCollection);
|
||
|
};
|
||
|
|
||
|
osc.isValidBundle = function (bundle) {
|
||
|
return bundle.timeTag !== undefined && bundle.packets !== undefined;
|
||
|
};
|
||
|
|
||
|
// Unsupported, non-API function.
|
||
|
osc.readBundleContents = function (dv, options, offsetState, len) {
|
||
|
var timeTag = osc.readTimeTag(dv, offsetState),
|
||
|
packets = [];
|
||
|
|
||
|
while (offsetState.idx < len) {
|
||
|
var packetSize = osc.readInt32(dv, offsetState),
|
||
|
packetLen = offsetState.idx + packetSize,
|
||
|
packet = osc.readPacket(dv, options, offsetState, packetLen);
|
||
|
|
||
|
packets.push(packet);
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
timeTag: timeTag,
|
||
|
packets: packets
|
||
|
};
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Reads an OSC packet, which may consist of either a bundle or a message.
|
||
|
*
|
||
|
* @param {Array-like} data an array of bytes to read from
|
||
|
* @param {Object} [options] read options
|
||
|
* @return {Object} a bundle or message object
|
||
|
*/
|
||
|
osc.readPacket = function (data, options, offsetState, len) {
|
||
|
var dv = osc.dataView(data, data.byteOffset, data.byteLength);
|
||
|
|
||
|
len = len === undefined ? dv.byteLength : len;
|
||
|
offsetState = offsetState || {
|
||
|
idx: 0
|
||
|
};
|
||
|
|
||
|
var header = osc.readString(dv, offsetState),
|
||
|
firstChar = header[0];
|
||
|
|
||
|
if (firstChar === "#") {
|
||
|
return osc.readBundleContents(dv, options, offsetState, len);
|
||
|
} else if (firstChar === "/") {
|
||
|
return osc.readMessageContents(header, dv, options, offsetState);
|
||
|
}
|
||
|
|
||
|
throw new Error("The header of an OSC packet didn't contain an OSC address or a #bundle string." +
|
||
|
" Header was: " + header);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Writes an OSC packet, which may consist of either of a bundle or a message.
|
||
|
*
|
||
|
* @param {Object} a bundle or message object
|
||
|
* @param {Object} [options] write options
|
||
|
* @return {Uint8Array} an array of bytes containing the message
|
||
|
*/
|
||
|
osc.writePacket = function (packet, options) {
|
||
|
if (osc.isValidMessage(packet)) {
|
||
|
return osc.writeMessage(packet, options);
|
||
|
} else if (osc.isValidBundle(packet)) {
|
||
|
return osc.writeBundle(packet, options);
|
||
|
} else {
|
||
|
throw new Error("The specified packet was not recognized as a valid OSC message or bundle." +
|
||
|
" Packet was: " + JSON.stringify(packet, null, 2));
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Unsupported, non-API.
|
||
|
osc.argumentTypes = {
|
||
|
i: {
|
||
|
reader: "readInt32",
|
||
|
writer: "writeInt32"
|
||
|
},
|
||
|
h: {
|
||
|
reader: "readInt64",
|
||
|
writer: "writeInt64"
|
||
|
},
|
||
|
f: {
|
||
|
reader: "readFloat32",
|
||
|
writer: "writeFloat32"
|
||
|
},
|
||
|
s: {
|
||
|
reader: "readString",
|
||
|
writer: "writeString"
|
||
|
},
|
||
|
S: {
|
||
|
reader: "readString",
|
||
|
writer: "writeString"
|
||
|
},
|
||
|
b: {
|
||
|
reader: "readBlob",
|
||
|
writer: "writeBlob"
|
||
|
},
|
||
|
t: {
|
||
|
reader: "readTimeTag",
|
||
|
writer: "writeTimeTag"
|
||
|
},
|
||
|
T: {
|
||
|
reader: "readTrue"
|
||
|
},
|
||
|
F: {
|
||
|
reader: "readFalse"
|
||
|
},
|
||
|
N: {
|
||
|
reader: "readNull"
|
||
|
},
|
||
|
I: {
|
||
|
reader: "readImpulse"
|
||
|
},
|
||
|
d: {
|
||
|
reader: "readFloat64",
|
||
|
writer: "writeFloat64"
|
||
|
},
|
||
|
c: {
|
||
|
reader: "readChar32",
|
||
|
writer: "writeChar32"
|
||
|
},
|
||
|
r: {
|
||
|
reader: "readColor",
|
||
|
writer: "writeColor"
|
||
|
},
|
||
|
m: {
|
||
|
reader: "readMIDIBytes",
|
||
|
writer: "writeMIDIBytes"
|
||
|
},
|
||
|
// [] are special cased within read/writeArguments()
|
||
|
};
|
||
|
|
||
|
// Unsupported, non-API function.
|
||
|
osc.inferTypeForArgument = function (arg) {
|
||
|
var type = typeof arg;
|
||
|
|
||
|
// TODO: This is freaking hideous.
|
||
|
switch (type) {
|
||
|
case "boolean":
|
||
|
return arg ? "T" : "F";
|
||
|
case "string":
|
||
|
return "s";
|
||
|
case "number":
|
||
|
return "f";
|
||
|
case "undefined":
|
||
|
return "N";
|
||
|
case "object":
|
||
|
if (arg === null) {
|
||
|
return "N";
|
||
|
} else if (arg instanceof Uint8Array ||
|
||
|
arg instanceof ArrayBuffer) {
|
||
|
return "b";
|
||
|
} else if (typeof arg.high === "number" && typeof arg.low === "number") {
|
||
|
return "h";
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
throw new Error("Can't infer OSC argument type for value: " +
|
||
|
JSON.stringify(arg, null, 2));
|
||
|
};
|
||
|
|
||
|
// Unsupported, non-API function.
|
||
|
osc.annotateArguments = function (args) {
|
||
|
var annotated = [];
|
||
|
|
||
|
for (var i = 0; i < args.length; i++) {
|
||
|
var arg = args[i],
|
||
|
msgArg;
|
||
|
|
||
|
if (typeof (arg) === "object" && arg.type && arg.value !== undefined) {
|
||
|
// We've got an explicitly typed argument.
|
||
|
msgArg = arg;
|
||
|
} else if (osc.isArray(arg)) {
|
||
|
// We've got an array of arguments,
|
||
|
// so they each need to be inferred and expanded.
|
||
|
msgArg = osc.annotateArguments(arg);
|
||
|
} else {
|
||
|
var oscType = osc.inferTypeForArgument(arg);
|
||
|
msgArg = {
|
||
|
type: oscType,
|
||
|
value: arg
|
||
|
};
|
||
|
}
|
||
|
|
||
|
annotated.push(msgArg);
|
||
|
}
|
||
|
|
||
|
return annotated;
|
||
|
};
|
||
|
|
||
|
if (osc.isCommonJS) {
|
||
|
module.exports = osc;
|
||
|
}
|
||
|
}());
|