From 992c68b03b477cbc8817db7ee58eb78a26a56cbd Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Thu, 24 Dec 2015 16:58:00 +0100 Subject: [PATCH 01/13] Fixes #11 - NTP timetag should be two Int32 Previously timetags were encoding JavaScript numbers (which are max 32 bit) as a 64 bit Int which means all time tags were still trapped in the first second of January 1, 1900. Now supports: nil (immediate) float (seconds from now) Date (millisecond accurate) Array [secondsSince1900, fractionalSeconds] (0.00000000023283 second accuracy) --- lib/osc-utilities.coffee | 108 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 100 insertions(+), 8 deletions(-) diff --git a/lib/osc-utilities.coffee b/lib/osc-utilities.coffee index 93bcfb0..60b5739 100644 --- a/lib/osc-utilities.coffee +++ b/lib/osc-utilities.coffee @@ -129,6 +129,99 @@ exports.splitInteger = (buffer, type) -> return {integer : value, rest : rest} +# Split off an OSC timetag from buffer +# returning {timetag: [seconds, fractionalSeconds], rest: restOfBuffer} +exports.splitTimetag = (buffer) -> + type = "Int32" + bytes = (binpack["pack" + type] 0).length + + if buffer.length < (bytes * 2) + throw new Error "buffer is not big enough to contain a timetag" + + # integers are stored in big endian format. + a = 0 + b = bytes + seconds = binpack["unpack" + type] buffer[a...b], "big" + c = bytes + d = bytes + bytes + fractional = binpack["unpack" + type] buffer[c...d], "big" + rest = buffer[d...(buffer.length)] + + return {timetag: [seconds, fractional], rest: rest} + +UNIX_EPOCH = 2208988800 +TWO_POW_32 = 4294967296 + +# Convert a JavaScript Date to a NTP timetag array. +# Time zone of the Date object is respected, as the NTP +# timetag uses UTC. +exports.dateToTimetag = (date) -> + return exports.timestampToTimetag(date.getTime()) + +# Convert a unix timestamp (milliseconds since jan 1 1970) +# to NTP timestamp array +exports.timestampToTimetag = (timestamp) -> + secs = timestamp / 1000 + wholeSecs = Math.floor(secs) + fracSeconds = secs - wholeSecs + return makeTimetag(wholeSecs, fracSeconds) + +makeTimetag = (unixseconds, fracSeconds) -> + # NTP epoch is 1900, JavaScript Date is unix 1970 + ntpSecs = unixseconds + UNIX_EPOCH + ntpFracs = Math.round(TWO_POW_32 * fracSeconds) + return [ntpSecs, ntpFracs] + +# Convert NTP timestamp array to a JavaScript Date +# in your systems local time zone. +exports.timetagToDate = (timetag) -> + [seconds, fractional] = timetag + seconds = seconds - UNIX_EPOCH + fracs = exports.ntpToFractionalSeconds(fractional) + date = new Date() + # Sets date to UTC/GMT + date.setTime((seconds * 1000) + (fracs * 1000)) + # Create a local timezone date + dd = new Date() + dd.setUTCFullYear(date.getUTCFullYear()) + dd.setUTCMonth(date.getUTCMonth()) + dd.setUTCDate(date.getUTCDate()) + dd.setUTCHours(date.getUTCHours()) + dd.setUTCMinutes(date.getUTCMinutes()) + dd.setUTCSeconds(date.getUTCSeconds()) + dd.setUTCMilliseconds(fracs * 1000) + # set fractional seconds with full precision + # to date object + dd.fractionalSecondsInt = fractional + dd.fractionalSecondsFloat = fracs + return dd + +# Make NTP timestamp array for relative future: now + seconds +# Accuracy is limited to 1 millisecond +exports.deltaTimetag = (seconds, now) -> + d = (now ? new Date()) * 1 + (seconds * 1000) + return exports.timestampToTimetag(d) + +# Convert 32 bit int for NTP fractional seconds +# to a 32 bit float +exports.ntpToFractionalSeconds = (fracSeconds) -> + return parseFloat(fracSeconds) / TWO_POW_32 + +# Encodes a timetag of type null|Number|Array|Date +# as a Buffer for adding to an OSC bundle. +exports.toTimetagBuffer = (timetag) -> + if typeof timetag is "number" + timetag = exports.deltaTimetag(timetag) + else if typeof timetag is "object" and ("getTime" of timetag) + # quacks like a Date + timetag = exports.dateToTimetag(timetag) + else if timetag.length != 2 + throw new Error("Invalid timetag" + timetag) + type = "Int32" + high = binpack["pack" + type] timetag[0], "big" + low = binpack["pack" + type] timetag[1], "big" + return exports.concat([high, low]) + exports.toIntegerBuffer = (number, type) -> type = "Int32" if not type? if typeof number isnt "number" @@ -164,11 +257,10 @@ oscTypeCodes = t : { representation : "timetag" split : (buffer, strict) -> - split = exports.splitInteger buffer, "UInt64" - {value : split.integer, rest : split.rest} + split = exports.splitTimetag buffer + {value: split.timetag, rest: split.rest} toArg : (value, strict) -> - throw new Error "expected number" if typeof value isnt "number" - exports.toIntegerBuffer value, "UInt64" + exports.toTimetagBuffer value } f : { representation : "float" @@ -388,8 +480,8 @@ exports.fromOscBundle = (buffer, strict) -> if bundleTag isnt "\#bundle" throw new Error "osc-bundles must begin with \#bundle" - # grab the 8 - bit timetag - { integer : timetag, rest : buffer} = exports.splitInteger buffer, "UInt64" + # grab the 8 byte timetag + {timetag: timetag, rest: buffer} = exports.splitTimetag buffer # convert each element. convertedElems = mapBundleList buffer, (buffer) -> @@ -480,7 +572,7 @@ exports.toOscBundle = (bundle, strict) -> # the bundle must have timetag and elements. if strict and not bundle?.timetag? throw StrictError "bundles must have timetags." - timetag = bundle?.timetag ? 0 + timetag = bundle?.timetag ? new Date() elements = bundle?.elements ? [] if not IsArray elements elemstr = elements @@ -488,7 +580,7 @@ exports.toOscBundle = (bundle, strict) -> elements.push elemstr oscBundleTag = exports.toOscString "\#bundle" - oscTimeTag = exports.toIntegerBuffer timetag, "UInt64" + oscTimeTag = exports.toTimetagBuffer timetag oscElems = [] for elem in elements From 3a99b4b3d410299e260e51deed4329ccae8732fb Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Thu, 24 Dec 2015 16:58:11 +0100 Subject: [PATCH 02/13] unit tests --- test/test-osc-utilities.coffee | 121 ++++++++++++++++++++++++--------- 1 file changed, 90 insertions(+), 31 deletions(-) diff --git a/test/test-osc-utilities.coffee b/test/test-osc-utilities.coffee index e276ee2..dfa5b11 100644 --- a/test/test-osc-utilities.coffee +++ b/test/test-osc-utilities.coffee @@ -203,11 +203,12 @@ test 'fromOscMessage with integer argument works', -> test 'fromOscMessage with timetag argument works', -> oscaddr = osc.toOscString "/stuff" osctype = osc.toOscString ",t" - oscarg = osc.toIntegerBuffer 8888, "UInt64" + timetag = [8888, 9999] + oscarg = osc.toTimetagBuffer timetag translate = osc.fromOscMessage osc.concat [oscaddr, osctype, oscarg] assert.strictEqual translate?.address, "/stuff" assert.strictEqual translate?.args?[0]?.type, "timetag" - assert.strictEqual (translate?.args?[0]?.value), 8888 + assert.deepEqual (translate?.args?[0]?.value), timetag test 'fromOscMessage with mismatched array doesn\'t throw', -> oscaddr = osc.toOscString "/stuff" @@ -301,28 +302,31 @@ test 'fromOscMessage strict fails if type address doesn\'t begin with /', -> test 'fromOscBundle works with no messages', -> oscbundle = osc.toOscString "#bundle" - osctimetag = osc.toIntegerBuffer 0, "UInt64" + timetag = [0, 0] + osctimetag = osc.toTimetagBuffer timetag buffer = osc.concat [oscbundle, osctimetag] translate = osc.fromOscBundle buffer - assert.strictEqual translate?.timetag, 0 + assert.deepEqual translate?.timetag, timetag assert.deepEqual translate?.elements, [] test 'fromOscBundle works with single message', -> oscbundle = osc.toOscString "#bundle" - osctimetag = osc.toIntegerBuffer 0, "UInt64" + timetag = [0, 0] + osctimetag = osc.toTimetagBuffer timetag oscaddr = osc.toOscString "/addr" osctype = osc.toOscString "," oscmessage = osc.concat [oscaddr, osctype] osclen = osc.toIntegerBuffer oscmessage.length buffer = osc.concat [oscbundle, osctimetag, osclen, oscmessage] translate = osc.fromOscBundle buffer - assert.strictEqual translate?.timetag, 0 + assert.deepEqual translate?.timetag, timetag assert.strictEqual translate?.elements?.length, 1 assert.strictEqual translate?.elements?[0]?.address, "/addr" test 'fromOscBundle works with multiple messages', -> oscbundle = osc.toOscString "#bundle" - osctimetag = osc.toIntegerBuffer 0, "UInt64" + timetag = [0, 0] + osctimetag = osc.toTimetagBuffer timetag oscaddr1 = osc.toOscString "/addr" osctype1 = osc.toOscString "," oscmessage1 = osc.concat [oscaddr1, osctype1] @@ -333,32 +337,35 @@ test 'fromOscBundle works with multiple messages', -> osclen2 = osc.toIntegerBuffer oscmessage2.length buffer = osc.concat [oscbundle, osctimetag, osclen1, oscmessage1, osclen2, oscmessage2] translate = osc.fromOscBundle buffer - assert.strictEqual translate?.timetag, 0 + assert.deepEqual translate?.timetag, timetag assert.strictEqual translate?.elements?.length, 2 assert.strictEqual translate?.elements?[0]?.address, "/addr" assert.strictEqual translate?.elements?[1]?.address, "/addr2" test 'fromOscBundle works with nested bundles', -> oscbundle = osc.toOscString "#bundle" - osctimetag = osc.toIntegerBuffer 0, "UInt64" + timetag = [0, 0] + osctimetag = osc.toTimetagBuffer timetag oscaddr1 = osc.toOscString "/addr" osctype1 = osc.toOscString "," oscmessage1 = osc.concat [oscaddr1, osctype1] osclen1 = osc.toIntegerBuffer oscmessage1.length oscbundle2 = osc.toOscString "#bundle" - osctimetag2 = osc.toIntegerBuffer 0, "UInt64" + timetag2 = [0, 0] + osctimetag2 = osc.toTimetagBuffer timetag2 oscmessage2 = osc.concat [oscbundle2, osctimetag2] osclen2 = osc.toIntegerBuffer oscmessage2.length buffer = osc.concat [oscbundle, osctimetag, osclen1, oscmessage1, osclen2, oscmessage2] translate = osc.fromOscBundle buffer - assert.strictEqual translate?.timetag, 0 + assert.deepEqual translate?.timetag, timetag assert.strictEqual translate?.elements?.length, 2 assert.strictEqual translate?.elements?[0]?.address, "/addr" - assert.strictEqual translate?.elements?[1]?.timetag, 0 + assert.deepEqual translate?.elements?[1]?.timetag, timetag2 test 'fromOscBundle works with non-understood messages', -> oscbundle = osc.toOscString "#bundle" - osctimetag = osc.toIntegerBuffer 0, "UInt64" + timetag = [0, 0] + osctimetag = osc.toTimetagBuffer timetag oscaddr1 = osc.toOscString "/addr" osctype1 = osc.toOscString "," oscmessage1 = osc.concat [oscaddr1, osctype1] @@ -369,7 +376,7 @@ test 'fromOscBundle works with non-understood messages', -> osclen2 = osc.toIntegerBuffer oscmessage2.length buffer = osc.concat [oscbundle, osctimetag, osclen1, oscmessage1, osclen2, oscmessage2] translate = osc.fromOscBundle buffer - assert.strictEqual translate?.timetag, 0 + assert.deepEqual translate?.timetag, timetag assert.strictEqual translate?.elements?.length, 1 assert.strictEqual translate?.elements?[0]?.address, "/addr" @@ -378,9 +385,10 @@ test 'fromOscBundle fails with bad bundle ID', -> assert.throws -> osc.fromOscBundle oscbundle test 'fromOscBundle fails with ridiculous sizes', -> + timetag = [0, 0] oscbundle = osc.concat [ osc.toOscString "#bundle" - osc.toIntegerBuffer 1234567, "Int64" + osc.toTimetagBuffer timetag osc.toIntegerBuffer 999999 ] assert.throws -> osc.fromOscBundle oscbundle @@ -526,10 +534,10 @@ test 'toOscMessage with type bang argument works', -> assert.strictEqual roundTrip.args[0].type, "bang" test 'toOscMessage with type timetag argument works', -> - roundTripMessage [{type: "timetag", value:8888}] + roundTripMessage [{type: "timetag", value: [8888, 9999]}] test 'toOscMessage with type double argument works', -> - roundTripMessage [{type: "double", value:8888}] + roundTripMessage [{type: "double", value: 8888}] test 'toOscMessage strict with type null with value true throws', -> assert.throws -> osc.toOscMessage({address: "/addr/", args: {type : "null", value : true}}, true) @@ -607,16 +615,16 @@ test 'toOscMessage fails argument is a random type', -> roundTripBundle = (elems) -> oscMessage = { - timetag : 0 + timetag : [0, 0] elements : elems } roundTrip = osc.fromOscBundle (osc.toOscBundle oscMessage), true - assert.strictEqual roundTrip?.timetag, 0 + assert.deepEqual roundTrip?.timetag, [0, 0] length = if typeof elems is "object" then elems.length else 1 assert.strictEqual roundTrip?.elements?.length, length for i in [0...length] if typeof elems is "object" - assert.strictEqual roundTrip?.elements?[i]?.timetag, elems[i].timetag + assert.deepEqual roundTrip?.elements?[i]?.timetag, elems[i].timetag assert.strictEqual roundTrip?.elements?[i]?.address, elems[i].address else assert.strictEqual roundTrip?.elements?[i]?.address, elems @@ -634,15 +642,15 @@ test 'toOscBundle with one message works', -> roundTripBundle [{address : "/addr"}] test 'toOscBundle with nested bundles works', -> - roundTripBundle [{address : "/addr"}, {timetag : 0}] + roundTripBundle [{address : "/addr"}, {timetag : [8888, 9999]}] test 'toOscBundle with bogus packets works', -> roundTrip = osc.fromOscBundle osc.toOscBundle { - timetag : 0 - elements : [{timetag : 0}, {maddress : "/addr"}] + timetag : [0, 0] + elements : [{timetag : [0, 0]}, {maddress : "/addr"}] } assert.strictEqual roundTrip.elements.length, 1 - assert.strictEqual roundTrip.elements[0].timetag, 0 + assert.deepEqual roundTrip.elements[0].timetag, [0, 0] test 'toOscBundle strict fails without timetags', -> assert.throws -> osc.toOscBundle {elements :[]}, true @@ -666,7 +674,7 @@ test 'toOscPacket works when explicitly set to message', -> test 'identity applyTransform works with a simple bundle', -> base = { - timetag : 0 + timetag : [0, 0] elements : [ {address : "test1"} {address : "test2"} @@ -674,10 +682,10 @@ test 'identity applyTransform works with a simple bundle', -> } transformed = osc.fromOscPacket (osc.applyTransform (osc.toOscPacket base), (a) -> a) - assert.strictEqual transformed?.timetag, 0 + assert.deepEqual transformed?.timetag, [0, 0] assert.strictEqual transformed?.elements?.length, base.elements.length for i in [0...base.elements.length] - assert.strictEqual transformed?.elements?[i]?.timetag, base.elements[i].timetag + assert.equal transformed?.elements?[i]?.timetag, base.elements[i].timetag assert.strictEqual transformed?.elements?[i]?.address, base.elements[i].address test 'applyMessageTranformerToBundle fails on bundle without tag', -> @@ -696,7 +704,7 @@ test 'addressTransform works with identity', -> test 'addressTransform works with bundles', -> base = { - timetag : 0 + timetag : [0, 0] elements : [ {address : "test1"} {address : "test2"} @@ -704,10 +712,10 @@ test 'addressTransform works with bundles', -> } transformed = osc.fromOscPacket (osc.applyTransform (osc.toOscPacket base), osc.addressTransform((a) -> "/prelude/" + a)) - assert.strictEqual transformed?.timetag, 0 + assert.deepEqual transformed?.timetag, [0, 0] assert.strictEqual transformed?.elements?.length, base.elements.length for i in [0...base.elements.length] - assert.strictEqual transformed?.elements?[i]?.timetag, base.elements[i].timetag + assert.equal transformed?.elements?[i]?.timetag, base.elements[i].timetag assert.strictEqual transformed?.elements?[i]?.address, "/prelude/" + base.elements[i].address test 'messageTransform works with identity function for single message', -> @@ -720,7 +728,7 @@ test 'messageTransform works with identity function for single message', -> test 'messageTransform works with bundles', -> message = { - timetag : 0 + timetag : [0, 0] elements : [ {address : "test1"} {address : "test2"} @@ -728,3 +736,54 @@ test 'messageTransform works with bundles', -> } buff = osc.toOscPacket message buffeq (osc.applyTransform buff, osc.messageTransform (a) -> a), buff + +test 'toTimetagBuffer works with a delta number', -> + delta = 1.2345 + buf = osc.toTimetagBuffer delta + +# assert dates are equal to within floating point conversion error +assertDatesEqual = (date1, date2) -> + assert Math.abs(date1.getTime() - date2.getTime()) <= 1, '' + date1 + ' != ' + date2 + +test 'toTimetagBuffer works with a Date', -> + date = new Date() + buf = osc.toTimetagBuffer date + +test 'toTimetagBuffer works with a timetag array', -> + timetag = [1000, 10001] + buf = osc.toTimetagBuffer timetag + +test 'toTimetagBuffer throws with invalid', -> + assert.throws -> osc.toTimetagBuffer "some bullshit" + +test 'deltaTimetag makes array from a delta', -> + delta = 1.2345 + ntp = osc.deltaTimetag(delta) + +test 'timetagToDate converts timetag to a Date', -> + date = new Date() + timetag = osc.dateToTimetag(date) + date2 = osc.timetagToDate(timetag) + assertDatesEqual date, date2 + +test 'timestampToTimetag converts a unix time to ntp array', -> + date = new Date() + timetag = osc.timestampToTimetag(date.getTime()) + date2 = osc.timetagToDate(timetag) + assertDatesEqual date, date2 + +test 'dateToTimetag converts date to ntp array', -> + date = new Date() + timetag = osc.dateToTimetag(date) + date2 = osc.timetagToDate(timetag) + assertDatesEqual date, date2 + +test 'splitTimetag returns timetag from a buffer', -> + timetag = [1000, 1001] + rest = "the rest" + buf = osc.concat [ + osc.toTimetagBuffer(timetag), + new Buffer(rest) + ] + {timetag: timetag2, rest: rest2} = osc.splitTimetag buf + assert.deepEqual timetag2, timetag From 0a42887d965539f8f89dbf6df39b89c2ab62f2da Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Thu, 24 Dec 2015 17:41:46 +0100 Subject: [PATCH 03/13] export timetagToDate, dateToTimetag, deltaTimetag Update docstrings and Readme with new API additions --- lib/index.js | 50 ++++++++++++++++++++++++++++++++++++----- readme.md | 63 +++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 98 insertions(+), 15 deletions(-) diff --git a/lib/index.js b/lib/index.js index 4f8e06d..1079d10 100644 --- a/lib/index.js +++ b/lib/index.js @@ -76,7 +76,7 @@ // `string`, `float`, `array` or `blob`, depending on its javascript type // (String, Number, Array, Buffer, respectively) // -// + An _OSC Bundle_ is represented as a javascript object with the following layout +// + An _OSC Bundle_ is represented as a javascript object with the following fields: // // { // oscType : "bundle" @@ -84,12 +84,24 @@ // elements : [element1, element] // } // -// Where the timetag is a javascript-native numeric value of the timetag, -// and elements is an array of either an _OSC Bundle_ or an _OSC Message_ -// The `oscType` field is optional, but is always returned by api functions. +// `oscType` ("bundle"|"message") +// +// `timetag` is one of: +// `null` - meaning now, the current time. +// By the time the bundle is received it will too late and depending +// on the receiver may be discarded or you may be scolded for being late. +// `number` - relative seconds from now with millisecond accuracy. +// `Date` - a JavaScript Date object in your local time zone. +// OSC timetags use UTC timezone, so do not try to adjust for timezones, +// this is not needed. +// `Array` - [numberOfSecondsSince1900, fractionalSeconds] +// Both values are Int32. This gives full timing accuracy of 1/(2^32) seconds which may be needed for some granular synthesis applications. +// +// `elements` is an Array of either _OSC Message_ or _OSC Bundle_ +// // // [spec]: http://opensoundcontrol.org/spec-1_0 - + var utils, coffee; utils = require("./osc-utilities"); // ~api~ @@ -166,4 +178,32 @@ return utils.applyTransform(buffer, utils.messageTransform(transform)); }; + +//~api~ +//---- +//### .timetagToDate(ntpTimeTag) +// Convert a timetag array to a JavaScript Date object in your local timezone. +// +// Received OSC bundles converted with `fromBuffer` will have a timetag array: +// [secondsSince1970, fractionalSeconds] +// This utility is useful for logging. Accuracy is reduced to milliseconds, +// but the returned Date object also has `fractionalSecondsInt` and `fractionalSecondsFloat` set if you need full accuracy (0.00000000023283 second, or 2^32 per second) + exports.timetagToDate = utils.timetagToDate; + +//~api~ +//---- +//### .dateToTimetag(date) +// Convert a JavaScript Date to a NTP timetag array [secondsSince1970, fractionalSeconds]. +// +// `toBuffer` already accepts Dates for timetags so you might not need this function. If you need to schedule bundles with finer than millisecond accuracy then you could use this to help assemble the NTP array. + exports.dateToTimetag = utils.dateToTimetag; + +//~api~ +//---- +//### .deltaTimetag(secondsFromNow, [now]) +// Make NTP timetag array relative to the current time. +// +// `toBuffer` already accepts floats for timetags and interprets this as a relative time, so you might not need this function. If you need to schedule bundles with finer than millisecond accuracy then you could use this to help assemble the NTP array. + exports.deltaTimetag = utils.deltaTimetag; + }).call(this); diff --git a/readme.md b/readme.md index 4f9d31c..94f9ab7 100644 --- a/readme.md +++ b/readme.md @@ -121,11 +121,36 @@ outputs the javascript representation, or throws if the buffer is ill-formed. takes a _OSC packet_ javascript representation as defined below and returns a node.js Buffer, or throws if the representation is ill-formed. +See "JavaScript representations of the OSC types" below + ---- ### .toBuffer(address, args[], [strict]) -alternative syntax for above. Assumes this is an _OSC Message_ as defined below, +alternative syntax for above. Assumes this is an _OSC Message_ as defined below, and `args` is an array of _OSC Arguments_ or single _OSC Argument_ +---- + +### .timetagToDate(ntpTimeTag) +Convert a timetag array to a JavaScript Date object in your local timezone. + +Received OSC bundles converted with `fromBuffer` will have a timetag array: +[secondsSince1970, fractionalSeconds] +This utility is useful for logging. Accuracy is reduced to milliseconds, +but the returned Date object also has `fractionalSecondsInt` and `fractionalSecondsFloat` set if you need full accuracy (0.00000000023283 second, or 2^32 per second) + +---- +### .dateToTimetag(date) +Convert a JavaScript Date to a NTP timetag array [secondsSince1970, fractionalSeconds]. + +`toBuffer` already accepts Dates for timetags so you might not need this function. If you need to schedule bundles with finer than millisecond accuracy then you could use this to help assemble the NTP array. + +---- +### .deltaTimetag(secondsFromNow, [now]) +Make NTP timetag array relative to the current time. + +`toBuffer` already accepts floats for timetags and interprets this as a relative time, so you might not need this function. If you need to schedule bundles with finer than millisecond accuracy then you could use this to help assemble the NTP array. + + ---- ### .applyAddressTransform(buffer, transform) takes a callback that takes a string and outputs a string, @@ -205,17 +230,35 @@ See the [spec][spec] for more information on the OSC types. `string`, `float`, `array` or `blob`, depending on its javascript type (String, Number, Array, Buffer, respectively) -+ An _OSC Bundle_ is represented as a javascript object with the following layout ++ An _OSC Bundle_ is represented as a javascript object with the following fields: - { - oscType : "bundle" - timetag : 7 - elements : [element1, element] - } +``` +{ + oscType : "bundle" + timetag : 7 + elements : [ + message1, + message2 + // ... + ] +} +``` + + `oscType` "bundle" + + `timetag` is one of: + - `null` - meaning now, the current time. + By the time the bundle is received it will be too late and depending + on the receiver may be discarded or you may be scolded for being late. + - `number` - relative seconds from now with millisecond accuracy. + - `Date` - a JavaScript Date object in your local time zone. + OSC timetags use UTC timezone, so do not try to adjust for timezones, + this is not needed. + - `Array` - `[numberOfSecondsSince1900, fractionalSeconds]` + Both values are `Int32`. This gives full timing accuracy of 1/(2^32) seconds which may be needed for some granular synthesis applications. + + `elements` is an `Array` of either _OSC Message_ or _OSC Bundle_ - Where the timetag is a javascript-native numeric value of the timetag, - and elements is an array of either an _OSC Bundle_ or an _OSC Message_ - The `oscType` field is optional, but is always returned by api functions. [spec]: http://opensoundcontrol.org/spec-1_0 From 8bf851458b34822f729417cc1ad7745f6f601406 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Thu, 24 Dec 2015 17:42:00 +0100 Subject: [PATCH 04/13] update bundle example --- examples/oscbundle_heartbeat.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/oscbundle_heartbeat.coffee b/examples/oscbundle_heartbeat.coffee index 58bf7f5..ee9b7d5 100644 --- a/examples/oscbundle_heartbeat.coffee +++ b/examples/oscbundle_heartbeat.coffee @@ -12,7 +12,7 @@ else sendHeartbeat = () -> buf = osc.toBuffer( - timetag : 12345 + timetag : 0.001 # 0.001 seconds from now elements : [ { address : "/p1" @@ -38,4 +38,4 @@ sendHeartbeat = () -> setInterval sendHeartbeat, 2000 -console.log "sending heartbeat messages to http://localhost:" + outport \ No newline at end of file +console.log "sending heartbeat messages to http://localhost:" + outport From e0c68933a96dedd76d13379fff6ec4ab001c1b02 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Mon, 28 Dec 2015 16:12:10 +0100 Subject: [PATCH 05/13] change timestampToTimetag to use unix seconds, not unix milliseconds --- lib/osc-utilities.coffee | 15 +++++++-------- test/test-osc-utilities.coffee | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/osc-utilities.coffee b/lib/osc-utilities.coffee index 60b5739..6b2de8c 100644 --- a/lib/osc-utilities.coffee +++ b/lib/osc-utilities.coffee @@ -156,12 +156,11 @@ TWO_POW_32 = 4294967296 # Time zone of the Date object is respected, as the NTP # timetag uses UTC. exports.dateToTimetag = (date) -> - return exports.timestampToTimetag(date.getTime()) + return exports.timestampToTimetag(date.getTime() / 1000) -# Convert a unix timestamp (milliseconds since jan 1 1970) +# Convert a unix timestamp (seconds since jan 1 1970 UTC) # to NTP timestamp array -exports.timestampToTimetag = (timestamp) -> - secs = timestamp / 1000 +exports.timestampToTimetag = (secs) -> wholeSecs = Math.floor(secs) fracSeconds = secs - wholeSecs return makeTimetag(wholeSecs, fracSeconds) @@ -197,10 +196,10 @@ exports.timetagToDate = (timetag) -> return dd # Make NTP timestamp array for relative future: now + seconds -# Accuracy is limited to 1 millisecond +# Accuracy of 'now' limited to milliseconds but 'seconds' may be a full 32 bit float exports.deltaTimetag = (seconds, now) -> - d = (now ? new Date()) * 1 + (seconds * 1000) - return exports.timestampToTimetag(d) + n = (now ? new Date()) / 1000 + return exports.timestampToTimetag(n + seconds) # Convert 32 bit int for NTP fractional seconds # to a 32 bit float @@ -769,4 +768,4 @@ mapBundleList = (buffer, func) -> for elem in elems (nonNullElems.push elem) if elem? - nonNullElems \ No newline at end of file + nonNullElems diff --git a/test/test-osc-utilities.coffee b/test/test-osc-utilities.coffee index dfa5b11..2f92525 100644 --- a/test/test-osc-utilities.coffee +++ b/test/test-osc-utilities.coffee @@ -768,7 +768,7 @@ test 'timetagToDate converts timetag to a Date', -> test 'timestampToTimetag converts a unix time to ntp array', -> date = new Date() - timetag = osc.timestampToTimetag(date.getTime()) + timetag = osc.timestampToTimetag(date.getTime() / 1000) date2 = osc.timetagToDate(timetag) assertDatesEqual date, date2 From 4defdcc52bd9f411d5957751222716d10974ab4e Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Mon, 28 Dec 2015 16:13:21 +0100 Subject: [PATCH 06/13] change toTimetagBuffer: interpret a number as a unix timestamp, not a relative time --- lib/index.js | 1 - lib/osc-utilities.coffee | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/index.js b/lib/index.js index 1079d10..41e6339 100644 --- a/lib/index.js +++ b/lib/index.js @@ -203,7 +203,6 @@ //### .deltaTimetag(secondsFromNow, [now]) // Make NTP timetag array relative to the current time. // -// `toBuffer` already accepts floats for timetags and interprets this as a relative time, so you might not need this function. If you need to schedule bundles with finer than millisecond accuracy then you could use this to help assemble the NTP array. exports.deltaTimetag = utils.deltaTimetag; }).call(this); diff --git a/lib/osc-utilities.coffee b/lib/osc-utilities.coffee index 6b2de8c..300e8bb 100644 --- a/lib/osc-utilities.coffee +++ b/lib/osc-utilities.coffee @@ -210,7 +210,7 @@ exports.ntpToFractionalSeconds = (fracSeconds) -> # as a Buffer for adding to an OSC bundle. exports.toTimetagBuffer = (timetag) -> if typeof timetag is "number" - timetag = exports.deltaTimetag(timetag) + timetag = exports.timestampToTimetag(timetag) else if typeof timetag is "object" and ("getTime" of timetag) # quacks like a Date timetag = exports.dateToTimetag(timetag) From 4c2396f88ed2cc68d91e5da33ab5c139386cc1b6 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Mon, 28 Dec 2015 16:13:36 +0100 Subject: [PATCH 07/13] update examples --- examples/oscbundle_heartbeat.coffee | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/oscbundle_heartbeat.coffee b/examples/oscbundle_heartbeat.coffee index ee9b7d5..e4edd4b 100644 --- a/examples/oscbundle_heartbeat.coffee +++ b/examples/oscbundle_heartbeat.coffee @@ -10,9 +10,12 @@ if process.argv[2]? else outport = 41234 +# unix timestamp in seconds with fractional +start = (new Date()).getTime() / 1000; + sendHeartbeat = () -> buf = osc.toBuffer( - timetag : 0.001 # 0.001 seconds from now + timetag : start + 0.05 # 0.05 seconds from now elements : [ { address : "/p1" @@ -23,7 +26,7 @@ sendHeartbeat = () -> args : "string" } { - timetag: 34567 + timetag: start + 1 # 1 second from now elements : [ { address : "/p3" From 0a45194eb0b8b91cddecf784ca0cec98697ae858 Mon Sep 17 00:00:00 2001 From: crucialfelix Date: Tue, 5 Jan 2016 11:40:12 +0100 Subject: [PATCH 08/13] change: do not set fractionalSecondsInt and fractionalSecondsDate on Date objects Setting unique properties on objects in javascript that were created with a prototype (in this case Date) causes V8 to create hidden classes and kills some important optimizations. --- lib/index.js | 5 ++--- lib/osc-utilities.coffee | 4 ---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/index.js b/lib/index.js index 41e6339..865d9d7 100644 --- a/lib/index.js +++ b/lib/index.js @@ -84,7 +84,7 @@ // elements : [element1, element] // } // -// `oscType` ("bundle"|"message") +// `oscType` "bundle" // // `timetag` is one of: // `null` - meaning now, the current time. @@ -186,8 +186,7 @@ // // Received OSC bundles converted with `fromBuffer` will have a timetag array: // [secondsSince1970, fractionalSeconds] -// This utility is useful for logging. Accuracy is reduced to milliseconds, -// but the returned Date object also has `fractionalSecondsInt` and `fractionalSecondsFloat` set if you need full accuracy (0.00000000023283 second, or 2^32 per second) +// This utility is useful for logging. Accuracy is reduced to milliseconds. exports.timetagToDate = utils.timetagToDate; //~api~ diff --git a/lib/osc-utilities.coffee b/lib/osc-utilities.coffee index 300e8bb..9484254 100644 --- a/lib/osc-utilities.coffee +++ b/lib/osc-utilities.coffee @@ -189,10 +189,6 @@ exports.timetagToDate = (timetag) -> dd.setUTCMinutes(date.getUTCMinutes()) dd.setUTCSeconds(date.getUTCSeconds()) dd.setUTCMilliseconds(fracs * 1000) - # set fractional seconds with full precision - # to date object - dd.fractionalSecondsInt = fractional - dd.fractionalSecondsFloat = fracs return dd # Make NTP timestamp array for relative future: now + seconds From 107a5a9f030e9eecb7d742e55c8412a3351c002b Mon Sep 17 00:00:00 2001 From: Russell McClellan Date: Wed, 6 Jan 2016 20:04:58 -0500 Subject: [PATCH 09/13] improve example --- examples/oscbundle_heartbeat.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/oscbundle_heartbeat.coffee b/examples/oscbundle_heartbeat.coffee index e4edd4b..6968c96 100644 --- a/examples/oscbundle_heartbeat.coffee +++ b/examples/oscbundle_heartbeat.coffee @@ -10,12 +10,12 @@ if process.argv[2]? else outport = 41234 -# unix timestamp in seconds with fractional -start = (new Date()).getTime() / 1000; +# Get the unix timestamp in seconds +now = -> (new Date()).getTime() / 1000; sendHeartbeat = () -> buf = osc.toBuffer( - timetag : start + 0.05 # 0.05 seconds from now + timetag : now() + 0.05 # 0.05 seconds from now elements : [ { address : "/p1" @@ -26,7 +26,7 @@ sendHeartbeat = () -> args : "string" } { - timetag: start + 1 # 1 second from now + timetag: now() + 1 # 1 second from now elements : [ { address : "/p3" From 9d1b880f74749492d1d80ae3c0ba39796e7617cc Mon Sep 17 00:00:00 2001 From: Russell McClellan Date: Wed, 6 Jan 2016 20:06:04 -0500 Subject: [PATCH 10/13] switch to Zlib license for more permissiveness --- COPYING | 34 +++++++++++++--------------------- lib/index.js | 3 +++ package.json | 3 ++- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/COPYING b/COPYING index 127a5bc..ceb43c9 100644 --- a/COPYING +++ b/COPYING @@ -1,23 +1,15 @@ -Boost Software License - Version 1.0 - August 17th, 2003 +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. -Permission is hereby granted, free of charge, to any person or organization -obtaining a copy of the software and accompanying documentation covered by -this license (the "Software") to use, reproduce, display, distribute, -execute, and transmit the Software, and to prepare derivative works of the -Software, and to permit third-parties to whom the Software is furnished to -do so, all subject to the following: +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: -The copyright notices in the Software and this entire statement, including -the above license grant, this restriction and the following disclaimer, -must be included in all copies of the Software, in whole or in part, and -all derivative works of the Software, unless such copies or derivative -works are solely in the form of machine-executable object code generated by -a source language processor. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT -SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE -FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. \ No newline at end of file +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgement in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. diff --git a/lib/index.js b/lib/index.js index 865d9d7..50de469 100644 --- a/lib/index.js +++ b/lib/index.js @@ -28,6 +28,9 @@ //~api~ //---- //~representation~ +//---- +//## License +// Licensed under the terms found in COPYING (zlib license) //~representation~ //## Javascript representations of the OSC types. diff --git a/package.json b/package.json index 000987d..62edf54 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ }, "devDependencies": { "mocha": "*", - "docket": ">=0.0.3", + "docket": "0.0.5", "coveralls": "*", "blanket": "*", "mocha-lcov-reporter": "*", @@ -42,6 +42,7 @@ "lib": "lib", "examples": "examples" }, + "license": "Zlib", "engines": { "node": ">=0.10.0" }, From 489173bf5dd78c85129b6b457b19fff889ba1f29 Mon Sep 17 00:00:00 2001 From: Russell McClellan Date: Wed, 6 Jan 2016 20:12:11 -0500 Subject: [PATCH 11/13] Remove deltaTimetag I don't think we need to include this as it is trivial for a client who needs it to implement, and many clients will not need it. After all, this is a minimal OSC library. --- lib/index.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/index.js b/lib/index.js index 50de469..d69df66 100644 --- a/lib/index.js +++ b/lib/index.js @@ -200,11 +200,4 @@ // `toBuffer` already accepts Dates for timetags so you might not need this function. If you need to schedule bundles with finer than millisecond accuracy then you could use this to help assemble the NTP array. exports.dateToTimetag = utils.dateToTimetag; -//~api~ -//---- -//### .deltaTimetag(secondsFromNow, [now]) -// Make NTP timetag array relative to the current time. -// - exports.deltaTimetag = utils.deltaTimetag; - }).call(this); From 231da7b6178a765df987c63d4bbf901690094532 Mon Sep 17 00:00:00 2001 From: Russell McClellan Date: Wed, 6 Jan 2016 20:20:36 -0500 Subject: [PATCH 12/13] Clean up documentation --- lib/index.js | 18 ++++++------ lib/install.md | 8 ++++++ readme.md | 77 ++++++++++++++++++++------------------------------ 3 files changed, 49 insertions(+), 54 deletions(-) diff --git a/lib/index.js b/lib/index.js index d69df66..6db7449 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,7 +1,7 @@ (function() { //~readme.out~ -//[![build status](https://secure.travis-ci.org/russellmcc/node-osc-min.png)](http://travis-ci.org/russellmcc/node-osc-min) +//[![build status](https://secure.travis-ci.org/russellmcc/node-osc-min.png)](http://travis-ci.org/russellmcc/node-osc-min) [![Coverage Status](https://coveralls.io/repos/russellmcc/node-osc-min/badge.png?branch=master)](https://coveralls.io/r/russellmcc/node-osc-min?branch=master) [![dependencies](https://david-dm.org/russellmcc/node-osc-min.png)](https://david-dm.org/russellmcc/node-osc-min) //# osc-min // // _simple utilities for open sound control in node.js_ @@ -90,17 +90,17 @@ // `oscType` "bundle" // // `timetag` is one of: -// `null` - meaning now, the current time. +// - `null` - meaning now, the current time. // By the time the bundle is received it will too late and depending // on the receiver may be discarded or you may be scolded for being late. -// `number` - relative seconds from now with millisecond accuracy. -// `Date` - a JavaScript Date object in your local time zone. +// - `number` - relative seconds from now with millisecond accuracy. +// - `Date` - a JavaScript Date object in your local time zone. // OSC timetags use UTC timezone, so do not try to adjust for timezones, // this is not needed. -// `Array` - [numberOfSecondsSince1900, fractionalSeconds] -// Both values are Int32. This gives full timing accuracy of 1/(2^32) seconds which may be needed for some granular synthesis applications. +// - `Array` - `[numberOfSecondsSince1900, fractionalSeconds]` +// Both values are `number`s. This gives full timing accuracy of 1/(2^32) seconds. // -// `elements` is an Array of either _OSC Message_ or _OSC Bundle_ +// `elements` is an `Array` of either _OSC Message_ or _OSC Bundle_ // // // [spec]: http://opensoundcontrol.org/spec-1_0 @@ -131,9 +131,11 @@ // takes a _OSC packet_ javascript representation as defined below and returns // a node.js Buffer, or throws if the representation is ill-formed. // +// See "JavaScript representations of the OSC types" below. +// //---- //### .toBuffer(address, args[], [strict]) -// alternative syntax for above. Assumes this is an _OSC Message_ as defined below, +// alternative syntax for above. Assumes this is an _OSC Message_ as defined below, // and `args` is an array of _OSC Arguments_ or single _OSC Argument_ exports.toBuffer = function(object, strict, opt) { if(typeof object === "string") diff --git a/lib/install.md b/lib/install.md index 24dafdd..d1a1c3c 100644 --- a/lib/install.md +++ b/lib/install.md @@ -20,4 +20,12 @@ tests with ``` npm test npm run-script coverage +``` + +### For the browser +If you want to use this library in a browser, you can build a browserified file (`build/osc-min.js`) with + +``` +npm install --dev +npm run-script browserify ``` \ No newline at end of file diff --git a/readme.md b/readme.md index 94f9ab7..2b7f532 100644 --- a/readme.md +++ b/readme.md @@ -37,7 +37,7 @@ npm test npm run-script coverage ``` -### for browser +### For the browser If you want to use this library in a browser, you can build a browserified file (`build/osc-min.js`) with ``` @@ -51,11 +51,11 @@ npm run-script browserify ```javascript sock = udp.createSocket("udp4", function(msg, rinfo) { - var error; + var error, error1; try { return console.log(osc.fromBuffer(msg)); - } catch (_error) { - error = _error; + } catch (error1) { + error = error1; return console.log("invalid OSC packet"); } }); @@ -87,14 +87,14 @@ setInterval(sendHeartbeat, 2000); ```javascript sock = udp.createSocket("udp4", function(msg, rinfo) { - var error, redirected; + var error, error1, redirected; try { redirected = osc.applyAddressTransform(msg, function(address) { return "/redirect" + address; }); return sock.send(redirected, 0, redirected.length, outport, "localhost"); - } catch (_error) { - error = _error; + } catch (error1) { + error = error1; return console.log("error redirecting: " + error); } }); @@ -121,36 +121,13 @@ outputs the javascript representation, or throws if the buffer is ill-formed. takes a _OSC packet_ javascript representation as defined below and returns a node.js Buffer, or throws if the representation is ill-formed. -See "JavaScript representations of the OSC types" below +See "JavaScript representations of the OSC types" below. ---- ### .toBuffer(address, args[], [strict]) alternative syntax for above. Assumes this is an _OSC Message_ as defined below, and `args` is an array of _OSC Arguments_ or single _OSC Argument_ ----- - -### .timetagToDate(ntpTimeTag) -Convert a timetag array to a JavaScript Date object in your local timezone. - -Received OSC bundles converted with `fromBuffer` will have a timetag array: -[secondsSince1970, fractionalSeconds] -This utility is useful for logging. Accuracy is reduced to milliseconds, -but the returned Date object also has `fractionalSecondsInt` and `fractionalSecondsFloat` set if you need full accuracy (0.00000000023283 second, or 2^32 per second) - ----- -### .dateToTimetag(date) -Convert a JavaScript Date to a NTP timetag array [secondsSince1970, fractionalSeconds]. - -`toBuffer` already accepts Dates for timetags so you might not need this function. If you need to schedule bundles with finer than millisecond accuracy then you could use this to help assemble the NTP array. - ----- -### .deltaTimetag(secondsFromNow, [now]) -Make NTP timetag array relative to the current time. - -`toBuffer` already accepts floats for timetags and interprets this as a relative time, so you might not need this function. If you need to schedule bundles with finer than millisecond accuracy then you could use this to help assemble the NTP array. - - ---- ### .applyAddressTransform(buffer, transform) takes a callback that takes a string and outputs a string, @@ -183,6 +160,20 @@ See notes above for applyAddressTransform for why you might want to use this. While this does parse and re-pack the messages, the bundle timetags are left in their accurate and prestine state. +---- +### .timetagToDate(ntpTimeTag) +Convert a timetag array to a JavaScript Date object in your local timezone. + +Received OSC bundles converted with `fromBuffer` will have a timetag array: +[secondsSince1970, fractionalSeconds] +This utility is useful for logging. Accuracy is reduced to milliseconds. + +---- +### .dateToTimetag(date) +Convert a JavaScript Date to a NTP timetag array [secondsSince1970, fractionalSeconds]. + +`toBuffer` already accepts Dates for timetags so you might not need this function. If you need to schedule bundles with finer than millisecond accuracy then you could use this to help assemble the NTP array. + ---- ## Javascript representations of the OSC types. See the [spec][spec] for more information on the OSC types. @@ -232,36 +223,30 @@ See the [spec][spec] for more information on the OSC types. + An _OSC Bundle_ is represented as a javascript object with the following fields: -``` -{ - oscType : "bundle" - timetag : 7 - elements : [ - message1, - message2 - // ... - ] -} -``` + { + oscType : "bundle" + timetag : 7 + elements : [element1, element] + } `oscType` "bundle" `timetag` is one of: - `null` - meaning now, the current time. - By the time the bundle is received it will be too late and depending + By the time the bundle is received it will too late and depending on the receiver may be discarded or you may be scolded for being late. - `number` - relative seconds from now with millisecond accuracy. - `Date` - a JavaScript Date object in your local time zone. OSC timetags use UTC timezone, so do not try to adjust for timezones, this is not needed. - `Array` - `[numberOfSecondsSince1900, fractionalSeconds]` - Both values are `Int32`. This gives full timing accuracy of 1/(2^32) seconds which may be needed for some granular synthesis applications. + Both values are `number`s. This gives full timing accuracy of 1/(2^32) seconds. `elements` is an `Array` of either _OSC Message_ or _OSC Bundle_ [spec]: http://opensoundcontrol.org/spec-1_0 +---- ## License - -Boost Software License v1.0 +Licensed under the terms found in COPYING (zlib license) From 6a22eec79050535d0f0b1ce16dd2d5ed1f6169b8 Mon Sep 17 00:00:00 2001 From: Russell McClellan Date: Wed, 6 Jan 2016 20:21:21 -0500 Subject: [PATCH 13/13] 1.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 62edf54..8649578 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "osc-min", - "version": "0.2.0", + "version": "1.0.0", "main": "lib/index", "author": { "name": "Russell McClellan",