diff resources/osc/node_modules/osc-min/lib/osc-utilities.coffee @ 271:fb9c28a4676b prerelease

Added osc example project and node script for testing
author Liam Donovan <l.b.donovan@qmul.ac.uk>
date Tue, 17 May 2016 16:01:06 +0100
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/resources/osc/node_modules/osc-min/lib/osc-utilities.coffee	Tue May 17 16:01:06 2016 +0100
@@ -0,0 +1,772 @@
+# # osc-utilities.coffee
+# ## Intro
+#  This file contains some lower-level utilities for OSC handling.
+#  My guess is client code won't need this.  If you do need this, you must
+#  require coffee first, then write:
+#
+#       require("coffee-script/register");
+#       osc-utils = require("osc/lib/osc-utilities");
+#
+#  See the comments in osc.coffee for more information about the structure of
+# the objects we're dealing with here.
+#
+
+# ## Dependencies
+# require the minimal binary packing utilities
+binpack = require "binpack"
+
+# ## Exported Functions
+
+# Utility for working with buffers. takes an array of buffers,
+# output one buffer with all of the array concatenated
+#
+# This is really only exported for TDD, but maybe it'll be useful
+# to someone else too.
+exports.concat = (buffers) ->
+  if not IsArray buffers
+    throw new Error "concat must take an array of buffers"
+
+  for buffer in buffers
+    if not Buffer.isBuffer(buffer)
+      throw new Error "concat must take an array of buffers"
+
+  sumLength = 0
+  sumLength += buffer.length for buffer in buffers
+
+  destBuffer = new Buffer(sumLength)
+
+  copyTo = 0
+  for buffer in buffers
+    buffer.copy destBuffer, copyTo
+    copyTo += buffer.length
+
+  destBuffer
+
+#
+# Convert a javascript string into a node.js Buffer containing an OSC-String.
+#
+# str must not contain any \u0000 characters.
+#
+# `strict` is an optional boolean paramter that fails if the string is invalid
+# (i.e. contains a \u0000 character)
+exports.toOscString = (str, strict) ->
+  if not (typeof str == "string")
+    throw new Error "can't pack a non-string into an osc-string"
+
+  # strip off any \u0000 characters.
+  nullIndex = str.indexOf("\u0000")
+
+  # if we're being strict, we can't allow strings with null characters
+  if (nullIndex != -1 and strict)
+    throw StrictError "Can't pack an osc-string that contains NULL characters"
+
+  str = str[0...nullIndex] if nullIndex != -1
+
+  # osc-strings must have length divisible by 4 and end with at least one zero.
+  for i in [0...(padding str)]
+    str += "\u0000"
+
+  # create a new buffer from the string.
+  new Buffer(str)
+
+#
+# Try to split a buffer into a leading osc-string and the rest of the buffer,
+# with the following layout:
+# { string : "blah" rest : <Buffer>}.
+#
+# `strict`, as above, is an optional boolean parameter that defaults to false -
+# if it is true, then an invalid buffer will always return null.
+#
+exports.splitOscString = (buffer, strict) ->
+  if not Buffer.isBuffer buffer
+    throw StrictError "Can't split something that isn't a buffer"
+
+  # extract the string
+  rawStr = buffer.toString "utf8"
+  nullIndex = rawStr.indexOf "\u0000"
+
+  # the rest of the code doesn't apply if there's no null character.
+  if nullIndex == -1
+    throw new Error "All osc-strings must contain a null character" if strict
+    return {string:rawStr, rest:(new Buffer 0)}
+
+  # extract the string.
+  str = rawStr[0...nullIndex]
+
+  # find the length of the string's buffer
+  splitPoint = Buffer.byteLength(str) + padding(str)
+
+  # in strict mode, don't succeed if there's not enough padding.
+  if strict and splitPoint > buffer.length
+    throw StrictError "Not enough padding for osc-string"
+
+  # if we're in strict mode, check that all the padding is null
+  if strict
+    for i in [Buffer.byteLength(str)...splitPoint]
+      if buffer[i] != 0
+        throw StrictError "Not enough or incorrect padding for osc-string"
+
+  # return a split
+  rest = buffer[splitPoint...(buffer.length)]
+
+  {string: str, rest: rest}
+
+# This has similar semantics to splitOscString but works with integers instead.
+# bytes is the number of bytes in the integer, defaults to 4.
+exports.splitInteger = (buffer, type) ->
+  type = "Int32" if not type?
+  bytes = (binpack["pack" + type] 0).length
+
+  if buffer.length < bytes
+    throw new Error "buffer is not big enough for integer type"
+
+  num = 0
+
+  # integers are stored in big endian format.
+  value = binpack["unpack" + type] buffer[0...bytes], "big"
+
+  rest = buffer[bytes...(buffer.length)]
+
+  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() / 1000)
+
+# Convert a unix timestamp (seconds since jan 1 1970 UTC)
+# to NTP timestamp array
+exports.timestampToTimetag = (secs) ->
+  wholeSecs = Math.floor(secs)
+  fracSeconds = secs - wholeSecs
+  return makeTimetag(wholeSecs, fracSeconds)
+
+# Convert a timetag to unix timestamp (seconds since unix epoch)
+exports.timetagToTimestamp = (timetag) ->
+  seconds = timetag[0] + exports.ntpToFractionalSeconds(timetag[1])
+  return seconds - UNIX_EPOCH
+
+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)
+  return dd
+
+# Make NTP timestamp array for relative future: now + seconds
+# Accuracy of 'now' limited to milliseconds but 'seconds' may be a full 32 bit float
+exports.deltaTimetag = (seconds, now) ->
+  n = (now ? new Date()) / 1000
+  return exports.timestampToTimetag(n + seconds)
+
+# 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.timestampToTimetag(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"
+    throw new Error "cannot pack a non-number into an integer buffer"
+  binpack["pack" + type] number, "big"
+
+# This mapping contains three fields for each type:
+#  - representation : the javascript string representation of this type.
+#  - split : a function to split a buffer into a decoded value and
+#            the rest of the buffer.
+#  - toArg : a function that takes the representation of the type and
+#            outputs a buffer.
+oscTypeCodes =
+  s : {
+    representation : "string"
+    split : (buffer, strict) ->
+      # just pass it through to splitOscString
+      split = exports.splitOscString buffer, strict
+      {value : split.string, rest : split.rest}
+    toArg : (value, strict) ->
+      throw new Error "expected string" if typeof value isnt "string"
+      exports.toOscString value, strict
+  }
+  i : {
+    representation : "integer"
+    split : (buffer, strict) ->
+      split = exports.splitInteger buffer
+      {value : split.integer, rest : split.rest}
+    toArg : (value, strict) ->
+      throw new Error "expected number" if typeof value isnt "number"
+      exports.toIntegerBuffer value
+  }
+  t : {
+    representation : "timetag"
+    split : (buffer, strict) ->
+      split = exports.splitTimetag buffer
+      {value: split.timetag, rest: split.rest}
+    toArg : (value, strict) ->
+      exports.toTimetagBuffer value
+  }
+  f : {
+    representation : "float"
+    split : (buffer, strict) ->
+      value : (binpack.unpackFloat32 buffer[0...4], "big")
+      rest : buffer[4...(buffer.length)]
+    toArg : (value, strict) ->
+      throw new Error "expected number" if typeof value isnt "number"
+      binpack.packFloat32 value, "big"
+  }
+  d : {
+    representation : "double"
+    split : (buffer, strict) ->
+      value : (binpack.unpackFloat64 buffer[0...8], "big")
+      rest : buffer[8...(buffer.length)]
+    toArg : (value, strict) ->
+      throw new Error "expected number" if typeof value isnt "number"
+      binpack.packFloat64 value, "big"
+  }
+  b : {
+    representation : "blob"
+    split : (buffer, strict) ->
+      # not much to do here, first grab an 4 byte int from the buffer
+      {integer : length, rest : buffer}  = exports.splitInteger buffer
+      {value : buffer[0...length], rest : buffer[length...(buffer.length)]}
+    toArg : (value, strict) ->
+      throw new Error "expected node.js Buffer" if not Buffer.isBuffer value
+      size = exports.toIntegerBuffer value.length
+      exports.concat [size, value]
+  }
+  T : {
+  representation : "true"
+  split : (buffer, strict) ->
+    rest : buffer
+    value : true
+  toArg : (value, strict) ->
+    throw new Error "true must be true" if not value and strict
+    new Buffer 0
+  }
+  F : {
+  representation : "false"
+  split : (buffer, strict) ->
+    rest : buffer
+    value : false
+  toArg : (value, strict) ->
+    throw new Error "false must be false" if value and strict
+    new Buffer 0
+  }
+  N : {
+  representation : "null"
+  split : (buffer, strict) ->
+    rest : buffer
+    value : null
+  toArg : (value, strict) ->
+    throw new Error "null must be false" if value and strict
+    new Buffer 0
+  }
+  I : {
+  representation : "bang"
+  split : (buffer, strict) ->
+    rest : buffer
+    value : "bang"
+  toArg : (value, strict) ->
+    new Buffer 0
+  }
+
+# simple function that converts a type code into it's javascript
+# string representation.
+exports.oscTypeCodeToTypeString = (code) ->
+  oscTypeCodes[code]?.representation
+
+# simple function that converts a javascript string representation
+# into its OSC type code.
+exports.typeStringToOscTypeCode = (rep) ->
+  for own code, {representation : str} of oscTypeCodes
+    return code if str is rep
+  return null
+
+exports.argToTypeCode = (arg, strict) ->
+  # if there's an explicit type annotation, back-translate that.
+  if arg?.type? and
+     (typeof arg.type is 'string') and
+     (code = exports.typeStringToOscTypeCode arg.type)?
+    return code
+
+  value = if arg?.value? then arg.value else arg
+
+  # now, we try to guess the type.
+  throw new Error 'Argument has no value' if strict and not value?
+
+  # if it's a string, use 's'
+  if typeof value is 'string'
+    return 's'
+
+  # if it's a number, use 'f' by default.
+  if typeof value is 'number'
+    return 'f'
+
+  # if it's a buffer, use 'b'
+  if Buffer.isBuffer(value)
+    return 'b'
+
+  #### These are 1.1 specific types.
+
+  # if it's a boolean, use 'T' or 'F'
+  if typeof value is 'boolean'
+    if value then return 'T' else return 'F'
+
+  # if it's null, use 'N'
+  if value is null
+    return 'N'
+
+  throw new Error "I don't know what type this is supposed to be."
+
+# Splits out an argument from buffer.  Same thing as splitOscString but
+# works for all argument types.
+exports.splitOscArgument = (buffer, type, strict) ->
+  osctype = exports.typeStringToOscTypeCode type
+  if osctype?
+    oscTypeCodes[osctype].split buffer, strict
+  else
+    throw new Error "I don't understand how I'm supposed to unpack #{type}"
+
+# Create a buffer with the given javascript type
+exports.toOscArgument = (value, type, strict) ->
+  osctype = exports.typeStringToOscTypeCode type
+  if osctype?
+    oscTypeCodes[osctype].toArg value, strict
+  else
+    throw new Error "I don't know how to pack #{type}"
+
+#
+# translates an OSC message into a javascript representation.
+#
+exports.fromOscMessage = (buffer, strict) ->
+  # break off the address
+  { string : address, rest : buffer}  = exports.splitOscString buffer, strict
+
+  # technically, addresses have to start with '/'.
+  if strict and address[0] isnt '/'
+    throw StrictError 'addresses must start with /'
+
+  # if there's no type string, this is technically illegal, but
+  # the specification says we should accept this until all
+  # implementations that send message without a type string are fixed.
+  # this will never happen, so we should accept this, even in
+  # strict mode.
+  return {address : address, args : []} if not buffer.length
+
+  # if there's more data but no type string, we can't parse the arguments.
+  {string : types, rest : buffer} = exports.splitOscString buffer, strict
+
+  # if the first letter isn't a ',' this isn't a valid type so we can't
+  # parse the arguments.
+  if types[0] isnt ','
+    throw StrictError 'Argument lists must begin with ,' if strict
+    return {address : address, args : []}
+
+  # we don't need the comma anymore
+  types = types[1..(types.length)]
+
+  args = []
+
+  # we use this to build up array arguments.
+  # arrayStack[-1] is always the currently contructing
+  # array.
+  arrayStack = [args]
+
+  # grab each argument.
+  for type in types
+    # special case: we're beginning construction of an array.
+    if type is '['
+      arrayStack.push([])
+      continue
+
+    # special case: we've just finished constructing an array.
+    if type is ']'
+      if arrayStack.length <= 1
+        throw new StrictError "Mismatched ']' character." if strict
+      else
+        built = arrayStack.pop()
+        arrayStack[arrayStack.length-1].push(
+          type: 'array'
+          value: built
+        )
+      continue
+
+    # by the standard, we have to ignore the whole message
+    # if we don't understand an argument
+    typeString = exports.oscTypeCodeToTypeString type
+    if not typeString?
+      throw new Error "I don't understand the argument code #{type}"
+
+    arg = exports.splitOscArgument buffer, typeString, strict
+
+    # consume the argument from the buffer
+    buffer = arg.rest if arg?
+
+    # add it to the list.
+    arrayStack[arrayStack.length-1].push(
+      type : typeString
+      value : arg?.value
+    )
+
+  if arrayStack.length isnt 1 and strict
+    throw new StrictError "Mismatched '[' character"
+  {address : address, args : args, oscType : "message"}
+
+#
+# Try to parse an OSC bundle into a javascript object.
+#
+exports.fromOscBundle = (buffer, strict) ->
+  # break off the bundletag
+  { string : bundleTag, rest : buffer} = exports.splitOscString buffer, strict
+
+  # bundles have to start with "#bundle".
+  if bundleTag isnt "\#bundle"
+    throw new Error "osc-bundles must begin with \#bundle"
+
+  # grab the 8 byte timetag
+  {timetag: timetag, rest: buffer} = exports.splitTimetag buffer
+
+  # convert each element.
+  convertedElems = mapBundleList buffer, (buffer) ->
+    exports.fromOscPacket buffer, strict
+
+  return {timetag : timetag, elements : convertedElems, oscType : "bundle"}
+
+#
+# convert the buffer into a bundle or a message, depending on the first string
+#
+exports.fromOscPacket = (buffer, strict) ->
+  if isOscBundleBuffer buffer, strict
+    exports.fromOscBundle buffer, strict
+  else
+    exports.fromOscMessage buffer, strict
+
+# helper - is it an argument that represents an array?
+getArrayArg = (arg) ->
+  if IsArray arg
+    arg
+  else if (arg?.type is "array") and (IsArray arg?.value)
+    arg.value
+  else if arg? and (not arg.type?) and (IsArray arg.value)
+    arg.value
+  else
+    null
+
+# helper - converts an argument list into a pair of a type string and a
+# data buffer
+# argList must be an array!!!
+toOscTypeAndArgs = (argList, strict) ->
+  osctype = ""
+  oscargs = []
+  for arg in argList
+    if (getArrayArg arg)?
+      [thisType, thisArgs] = toOscTypeAndArgs (getArrayArg arg), strict
+      osctype += "[" + thisType + "]"
+      oscargs = oscargs.concat thisArgs
+      continue
+    typeCode = exports.argToTypeCode arg, strict
+    if typeCode?
+      value = arg?.value
+      if value is undefined
+        value = arg
+      buff = exports.toOscArgument value,
+        (exports.oscTypeCodeToTypeString typeCode), strict
+      if buff?
+        oscargs.push buff
+        osctype += typeCode
+  [osctype, oscargs]
+
+#
+# convert a javascript format message into an osc buffer
+#
+exports.toOscMessage = (message, strict) ->
+  # the message must have addresses and arguments.
+  address = if message?.address? then message.address else message
+  if typeof address isnt "string"
+    throw new Error "message must contain an address"
+
+  args = message?.args
+
+  if args is undefined
+    args = []
+
+  # pack single args
+  if not IsArray args
+    old_arg = args
+    args = []
+    args[0] = old_arg
+
+  oscaddr = exports.toOscString address, strict
+  [osctype, oscargs] = toOscTypeAndArgs args, strict
+  osctype = "," + osctype
+
+  # bundle everything together.
+  allArgs = exports.concat oscargs
+
+  # convert the type tag into an oscString.
+  osctype = exports.toOscString osctype
+
+  exports.concat [oscaddr, osctype, allArgs]
+
+#
+# convert a javascript format bundle into an osc buffer
+#
+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 ? new Date()
+  elements = bundle?.elements ? []
+  if not IsArray elements
+    elemstr = elements
+    elements = []
+    elements.push elemstr
+
+  oscBundleTag = exports.toOscString "\#bundle"
+  oscTimeTag = exports.toTimetagBuffer timetag
+
+  oscElems = []
+  for elem in elements
+    try
+      # try to convert this sub-element into a buffer
+      buff = exports.toOscPacket elem, strict
+
+      # okay, pack in the size.
+      size = exports.toIntegerBuffer buff.length
+      oscElems.push exports.concat [size, buff]
+    catch e
+      null
+
+  allElems = exports.concat oscElems
+  exports.concat [oscBundleTag, oscTimeTag, allElems]
+
+# convert a javascript format bundle or message into a buffer
+exports.toOscPacket = (bundleOrMessage, strict) ->
+  # first, determine whether or not this is a bundle.
+  if bundleOrMessage?.oscType?
+    if bundleOrMessage.oscType is "bundle"
+      return exports.toOscBundle bundleOrMessage, strict
+    return exports.toOscMessage bundleOrMessage, strict
+
+  # bundles have "timetags" and "elements"
+  if bundleOrMessage?.timetag? or bundleOrMessage?.elements?
+    return exports.toOscBundle bundleOrMessage, strict
+
+  exports.toOscMessage bundleOrMessage, strict
+
+#
+# Helper function for transforming all messages in a bundle with a given message
+# transform.
+#
+exports.applyMessageTranformerToBundle = (transform) -> (buffer) ->
+
+  # parse out the bundle-id and the tag, we don't want to change these
+  { string, rest : buffer} = exports.splitOscString buffer
+
+  # bundles have to start with "#bundle".
+  if string isnt "\#bundle"
+    throw new Error "osc-bundles must begin with \#bundle"
+
+  bundleTagBuffer = exports.toOscString string
+
+  # we know that the timetag is 8 bytes, we don't want to mess with it,
+  # so grab it as a buffer.  There is some subtle loss of precision with
+  # the round trip from int64 to float64.
+  timetagBuffer = buffer[0...8]
+  buffer = buffer[8...buffer.length]
+
+  # convert each element.
+  elems = mapBundleList buffer, (buffer) ->
+    exports.applyTransform(
+      buffer,
+      transform,
+      exports.applyMessageTranformerToBundle transform
+    )
+
+  totalLength = bundleTagBuffer.length + timetagBuffer.length
+  totalLength += 4 + elem.length for elem in elems
+
+  # okay, now we have to reconcatenate everything.
+  outBuffer = new Buffer totalLength
+  bundleTagBuffer.copy outBuffer, 0
+  timetagBuffer.copy outBuffer, bundleTagBuffer.length
+  copyIndex = bundleTagBuffer.length + timetagBuffer.length
+  for elem in elems
+    lengthBuff = exports.toIntegerBuffer elem.length
+    lengthBuff.copy outBuffer, copyIndex
+    copyIndex += 4
+    elem.copy outBuffer, copyIndex
+    copyIndex += elem.length
+  outBuffer
+
+#
+# Applies a transformation function (that is, a function from buffers
+# to buffers) to each element of given osc-bundle or message.
+#
+# `buffer` is the buffer to transform, which must be a buffer of a full packet.
+# `messageTransform` is function from message buffers to message buffers
+#  `bundleTransform` is an optional parameter for functions from bundle buffers
+#  to bundle buffers.
+# if `bundleTransform` is not set, it defaults to just applying the
+# `messageTransform` to each message in the bundle.
+#
+exports.applyTransform = (buffer, mTransform, bundleTransform) ->
+  if not bundleTransform?
+    bundleTransform = exports.applyMessageTranformerToBundle mTransform
+
+  if isOscBundleBuffer buffer
+    bundleTransform buffer
+  else
+    mTransform buffer
+
+# Converts a javascript function from string to string to a function
+# from message buffer to message buffer, applying the function to the
+# parsed strings.
+#
+# We pre-curry this because we expect to use this with `applyMessageTransform`
+# above
+#
+exports.addressTransform = (transform) -> (buffer) ->
+  # parse out the address
+  {string, rest} = exports.splitOscString buffer
+
+  # apply the function
+  string = transform string
+
+  # re-concatenate
+  exports.concat [
+    exports.toOscString string
+    rest
+  ]
+
+#
+# Take a function that transform a javascript _OSC Message_ and
+# convert it to a function that transforms osc-buffers.
+#
+exports.messageTransform = (transform) -> (buffer) ->
+  message = exports.fromOscMessage buffer
+  exports.toOscMessage transform message
+
+## Private utilities
+
+#
+# is it an array?
+#
+IsArray = Array.isArray
+
+#
+# An error that only throws when we're in strict mode.
+#
+StrictError = (str) ->
+  new Error "Strict Error: " + str
+
+# this private utility finds the amount of padding for a given string.
+padding = (str) ->
+  bufflength = Buffer.byteLength(str)
+  4 - (bufflength % 4)
+
+#
+# Internal function to check if this is a message or bundle.
+#
+isOscBundleBuffer = (buffer, strict) ->
+  # both formats begin with strings, so we should just grab the front but not
+  # consume it.
+  {string} = exports.splitOscString buffer, strict
+
+  return string is "\#bundle"
+
+#
+# Does something for each element in an array of osc-message-or-bundles,
+# each prefixed by a length (such as appears in osc-messages), then
+# return the result as an array.
+#
+# This is not exported because it doesn't validate the format and it's
+# not really a generally useful function.
+#
+# If a function throws on an element, we discard that element in the map
+# but we don't give up completely.
+#
+mapBundleList = (buffer, func) ->
+  elems = while buffer.length
+    # the length of the element is stored in an integer
+    {integer : size, rest : buffer}  = exports.splitInteger buffer
+
+    # if the size is bigger than the packet, something's messed up, so give up.
+    if size > buffer.length
+      throw new Error(
+        "Invalid bundle list: size of element is bigger than buffer")
+
+    thisElemBuffer = buffer[0...size]
+
+    # move the buffer to after the element we're just parsing.
+    buffer = buffer[size...buffer.length]
+
+    # record this element
+    try
+      func thisElemBuffer
+    catch e
+      null
+
+  # remove all null from elements
+  nonNullElems = []
+  for elem in elems
+    (nonNullElems.push elem) if elem?
+
+  nonNullElems