UNPKG

a2r-osc

Version:

An OSC implementation for node.js and the browser

968 lines (817 loc) 27.1 kB
# a2r-osc 0.0.1 # http://github.com/beyama/a2r-osc # (c) 2013 Alexander Jentz, beyama.de # a2r-osc may be freely distributed under the MIT license. # Export the a2r-osc functions and classes. # If we're in the browser, add a2r.osc as a # global object. if typeof module is "undefined" window.a2r ||= {} exports = window.a2r.osc = {} else exports = module.exports do (exports)-> # Do we have a Node.js Buffer or are we in a browser? nodeBuffer = typeof Buffer is 'function' # Convert a value to a number and throw an error if value # is Not-a-Number. toNumber = (val)-> val = Number(val) throw new Error("Value isn't a number") if val is NaN val # Convert a value to a number and round the result. toInteger = (val)-> val = toNumber(val) Math.round(val) # Convert NTP Timestamp to Date and vice versa # # http://commons.apache.org/net/api-3.2/src-html/org/apache/commons/net/ntp/TimeStamp.html SECONDS_FROM_1900_to_1970 = 2208988800 # Get a Date object for seconds and fraction fromNTP = (seconds, fraction)-> # immediately return new Date if seconds is 0 and fraction is 1 ms = (seconds - SECONDS_FROM_1900_to_1970) * 1000 ms += Math.round(1000 * fraction / 0x100000000) date = new Date(ms) date.ntpSeconds = seconds date.ntpFraction = fraction date # Get an array with seconds and fraction from JavaScript Date-object. toNTP = (date)-> # special case in OSC, 1 means process immediately return [0, 1] if date is 1 return date if Array.isArray(date) time = date.getTime() seconds = Math.floor(time / 1000) fraction = Math.round(((time % 1000) * 0x100000000) / 1000) [seconds + SECONDS_FROM_1900_to_1970, fraction] # Type handler OSC_TYPES = i: name: "integer" read: (reader)-> reader.readInt32() write: (writer, value)-> writer.writeInt32(value) cast: toInteger sizeOf: (value)-> 4 f: name: "float" read: (reader)-> reader.readFloat() write: (writer, value)-> writer.writeFloat(value) cast: toNumber sizeOf: (value)-> 4 s: name: "string" read: (reader)-> reader.readString() write: (writer, value)-> writer.writeString(value) cast: (value) -> value.toString() sizeOf: (value)-> oscSizeOfString(value.toString()) b: name: "blob" read: (reader)-> reader.readBlob() write: (writer, value) -> writer.writeBlob(value) sizeOf: (value)-> oscSizeOfBlob(value) d: name: "double" read: (reader)-> reader.readDouble() write: (writer, value)-> writer.writeDouble(value) sizeOf: (value)-> 8 c: name: "char" read: (reader)-> String.fromCharCode(reader.readInt32() & 0x7F) write: (writer, value)-> writer.writeInt32(value.charCodeAt(0)) cast: (value)-> value.toString().charAt(0) sizeOf: (value)-> 4 r: name: "color" read: (reader)-> reader.readInt32() write: (writer, value)-> writer.writeInt32(value) cast: toInteger sizeOf: (value)-> 4 t: name: "time" read: (reader)-> reader.readTimetag() write: (writer, value)-> writer.writeTimetag(value) cast: (value)-> return value if value instanceof Date new Date(value) sizeOf: -> 8 T: name: "true" read: -> true F: name: "false" read: -> false N: name: "null" read: -> null I: name: "impulse" read: -> Impulse OSC_TYPES.S = OSC_TYPES.s OSC_TYPES_BY_NAME = {} for code, type of OSC_TYPES type.code = code if code isnt 'S' OSC_TYPES_BY_NAME[type.name] = type NUMBERS = Int32: dataViewReader: "getInt32" dataViewWriter: "setInt32" bufferReader: "readInt32BE" bufferWriter: "writeInt32BE" size: 4 UInt32: dataViewReader: "getUint32" dataViewWriter: "setUint32" bufferReader: "readUInt32BE" bufferWriter: "writeUInt32BE" size: 4 Float: dataViewReader: "getFloat32" dataViewWriter: "setFloat32" bufferReader: "readFloatBE" bufferWriter: "writeFloatBE" size: 4 Double: dataViewReader: "getFloat64" dataViewWriter: "setFloat64" bufferReader: "readDoubleBE" bufferWriter: "writeDoubleBE" size: 8 oscPadding = (len)-> (4 - len % 4) # Impulse, singleton marker object Impulse = new Object # Get type code for supported JavaScript types. oscTypeCodeOf = (val)-> switch typeof val when 'string' then 's' when 'number' then 'f' when 'boolean' if val then 'T' else 'F' when 'undefined' throw new Error("Value can't be undefined") when 'object' if val is null then 'N' else if val instanceof Date then 't' else if (nodeBuffer and Buffer.isBuffer(val)) or val instanceof ArrayBuffer then 'b' else if val is Impulse then 'I' else throw new Error("Unsupported type `#{val}`") else throw new Error("Unsupported type `#{val}`") # Get string length plus padding. oscSizeOfString = (str)-> str.length + oscPadding(str.length) # Get size of buffer plus 4 bytes for length # and plus padding. oscSizeOfBlob = (buf)-> if buf instanceof ArrayBuffer length = 4 + buf.byteLength else length = 4 + buf.length pad = oscPadding(length) length += pad if pad < 4 length # Calculate size of bundle. oscSizeOfBundle = (bundle, dict)-> # #bundle string + timetag size = 16 # sizeof elements for elem in bundle.elements size += 4 + oscSizeOfMessage(elem, dict) size # Calculate size of message oscSizeOfMessage = (msg, dict)-> if dict if msg.isPattern() id = dict.getPatternId(msg.address) else id = dict.getAddressId(msg.address) # sizeof address if id # 4 byte for '/' or '/?' and 4 byte for integer id size = 8 else # size of osc string size = oscSizeOfString(msg.address) # sizeof typeTag if id # typeTag + ',' and 'i' for addressId tl = msg.typeTag.length + 2 else # typeTag + ',' tl = msg.typeTag.length + 1 size += tl + oscPadding(tl) # sizeof arguments data i = 0 l = msg.typeTag.length while i < l typeCode = msg.typeTag.charAt(i) value = msg.arguments[i++] size += oscSizeOf(value, typeCode) size # Get size of every supported type including # message and bundle. oscSizeOf = (value, code)-> if code type = OSC_TYPES[code] || OSC_TYPES_BY_NAME[code] unless type throw new Error("Type `#{code}` isn't supported") return 0 unless type.sizeOf type.sizeOf(value) else code = oscTypeCodeOf(value) oscSizeOf(value, code) # A simple dictionary class to work with compressed # address- / pattern strings. class Dictionary # The constructor takes an optional # id-to-address map and/or an optional # id-to-pattern map. constructor: (addressMap, patternMap)-> @idToAddress = {} @addressToId = {} @idToPattern = {} @patternToId = {} if addressMap for id, address of addressMap @addAddress(id, address) if patternMap for id, pattern of patternMap @addPattern(id, pattern) # Add an address to the dictionary addAddress: (id, address)-> @idToAddress[id] = address @addressToId[address] = Number(id) # Get address by id getAddress: (id)-> @idToAddress[id] # Get id by address getAddressId: (addr)-> @addressToId[addr] # Remove an address mapping by # id or address string removeAddress: (idOrAddress)-> if typeof idOrAddress is "number" id = idOrAddress address = @idToAddress[id] else address = idOrAddress id = @addressToId[address] delete @idToAddress[id] delete @addressToId[address] # Add an pattern to the dictionary addPattern: (id, pattern)-> @idToPattern[id] = pattern @patternToId[pattern] = Number(id) # Get pattern by id getPattern: (id)-> @idToPattern[id] # Get id by pattern getPatternId: (pattern)-> @patternToId[pattern] # Remove a pattern mapping by # id or pattern removePattern: (idOrPattern)-> if typeof idOrPattern is "number" id = idOrPattern pattern = @idToPattern[id] else pattern = idOrPattern id = @patternToId[pattern] delete @idToPattern[id] delete @patternToId[pattern] # Class for representing a message. class Message constructor: (address, typeTag, args)-> # copy constructor if address instanceof Message msg = address @address = msg.address @typeTag = msg.typeTag @arguments = msg.arguments[..] return # else @address = address @arguments = [] if typeTag isnt undefined and args is undefined args = typeTag typeTag = null return if args is undefined args = [args] unless Array.isArray(args) if typeTag # check for consistent lengths of arguments and type tag if args.length isnt typeTag.length throw new Error("Arguments length doesn't match typetag length") for arg, i in args @add(typeTag.charAt(i), arg) else for value in args code = null if typeof value is 'object' and value?.type? type = value.type type = OSC_TYPES[type] || OSC_TYPES_BY_NAME[type] code = type.code throw new Error("Unsupported type `#{code}`") unless type # Types without argument data have no sizeOf method # and return their values on read. value = if type.sizeOf then value.value else type.read() if code @add(code, value) else @add(value) # Clone this message clone: -> new Message(@) isPattern: -> return @_isPattern if @_isPattern? @_isPattern = /(?:\*|\?|\[|\{|\/\/)/.test(@address) # Add a value to the arguments list add: (code, value)-> # delete buffer cache delete @buffer if @buffer if value is undefined value = code code = null if code type = OSC_TYPES[code] || OSC_TYPES_BY_NAME[code] throw new Error("Unsupported type `#{code}`") unless type value = type.cast(value) if type.cast else code = oscTypeCodeOf(value) type = OSC_TYPES[code] @arguments.push(value) if @typeTag @typeTag += code else @typeTag = code @ # Creates an OSC packet buffer. toBuffer: (dict)-> @buffer ||= if nodeBuffer new OscBufferPacketGenerator(@, dict).generate() else new OscArrayBufferPacketGenerator(@, dict).generate() equal: (other)-> return false unless other instanceof Message return false if other.address isnt @address return false if other.typeTag isnt @typeTag return false if other.arguments.length isnt @arguments.length for arg, i in @arguments when other.arguments[i] isnt arg return false true # Class for representing a bundle. class Bundle constructor: (timetag, elements)-> # copy constructor if timetag instanceof Bundle bundle = timetag @timetag = new Date(bundle.timetag.valueOf()) @elements = [] for elem in bundle.elements @addElement(elem.clone()) return # else if timetag instanceof Date @timetag = timetag else if timetag is 1 @timetag = new Date else @timetag = new Date elements = timetag @elements = [] if elements if Array.isArray(elements) @addElement(elem) for elem in elements else @addElement(elements) # Clone this bundle clone: -> new Bundle(@) # Add a message to elements list and return the message. addElement: (address, typeTag, args)-> # delete buffer cache delete @buffer if @buffer if address instanceof Message @elements.push address address else if typeof address is "string" msg = new Message(address, typeTag, args) @elements.push msg msg else throw new Error("A bundle element must be an instance of Message") # Add a message to elements list and returns self (for chaining). add: (address, typeTag, args)-> @addElement(address, typeTag, args) @ # Creates an OSC packet buffer. toBuffer: (dict)-> @buffer ||= if nodeBuffer new OscBufferPacketGenerator(@, dict).generate() else new OscArrayBufferPacketGenerator(@, dict).generate() # Returns true if this bundle is equal to another bundle otherwise returns false. equal: (other)-> return false unless other instanceof Bundle return false if other.timetag isnt @timetag return false if other.elements.length isnt @elements.length for elem, i in @elements when not elem.equal(other.elements[i]) return false true # The abstract OSC packet generator class AbstractOscPacketGenerator constructor: (messageOrBundle, dict)-> @dict = dict if messageOrBundle instanceof Bundle @bundle = messageOrBundle @size = oscSizeOfBundle(@bundle, @dict) else @message = messageOrBundle @size = oscSizeOfMessage(@message, @dict) _generateMessage: (msg)-> # compress if possible if @dict id = if msg.isPattern() @dict.getPatternId(msg.address) else @dict.getAddressId(msg.address) if id if msg.isPattern() @writeString("/?") else @writeString("/") @writeString(",i#{msg.typeTag}") @writeInt32(toInteger(id)) else @writeString(msg.address) @writeString(",#{msg.typeTag}") i = 0 l = msg.typeTag.length while i < l code = msg.typeTag.charAt(i) value = msg.arguments[i++] # get type handler type = OSC_TYPES[code] unless type throw new Error("Type `#{code}` isn't supported") if type.write type.write(@, value) _generateBundle: (bundle)-> # bundle-id @writeString("#bundle") # timetag if bundle.timetag <= new Date tag = [0, 1] else tag = toNTP(bundle.timetag) @writeTimetag(tag) # generate elements for elem in bundle.elements @writeInt32(oscSizeOfMessage(elem, @dict)) @_generateMessage(elem) null # Write a timetag to the underlying buffer. writeTimetag: (date)-> tag = toNTP(date) @writeUInt32(tag[0]) @writeUInt32(tag[1]) # Generate a package and return the buffer # object generate: -> if @bundle @_generateBundle(@bundle) else @_generateMessage(@message) @buffer writeString: (string, encoding="ascii")-> throw new Error("Abstract method `AbstractOscPacketGenerator::writeString` called") for name, desc of NUMBERS do (name)-> name = "write#{name}" AbstractOscPacketGenerator::[name] = -> throw new Error("Abstract method `AbstractOscPacketGenerator::#{name}` called") # OSC packet generator which generates ArrayBuffer class OscArrayBufferPacketGenerator extends AbstractOscPacketGenerator constructor: (messageOrBundle, dict)-> super(messageOrBundle, dict) @buffer = new ArrayBuffer(@size) @view = new DataView(@buffer) @pos = 0 # Write a string to the underlying buffer. writeString: (string, encoding="ascii")-> if encoding isnt "ascii" throw new Error("OscBufferWriter::writeString only supports ASCII encoding for ArrayBuffer") # copy string content to ArrayBuffer l = string.length i = 0 while i < l char = string.charCodeAt(i++) @view.setInt8(@pos++, char & 0x7F) pad = oscPadding(l) # append 0-padding i = 0 while i < pad @view.setInt8(@pos++, 0) i++ # Write a blob to the underlying ArrayBuffer. writeBlob: (buffer)-> # if buffer is a node buffer if nodeBuffer and Buffer.isBuffer(buffer) l = buffer.length @writeInt32(l) i = 0 while i < l @view.setInt8(@pos + i, buffer[i]) i++ @pos += l # it's an ArrayBuffer else l = buffer.byteLength array = new Int8Array(buffer) @writeInt32(l) i = 0 while i < l @view.setInt8(@pos + i, array[i]) i++ @pos += l # add padding pad = oscPadding(4 + l) if pad and pad < 4 i = 0 while i < pad @view.setInt8(@pos + i, 0) i++ @pos += pad # Generate writer methods # for Int32, UInt32, Float, Double. for type, desc of NUMBERS do(type, desc)-> OscArrayBufferPacketGenerator::["write#{type}"] = (value)-> value = @view[desc.dataViewWriter](@pos, value, false) @pos += desc.size value #only_node # OSC packet generator which generates node buffer class OscBufferPacketGenerator extends AbstractOscPacketGenerator constructor: (messageOrBundle, dict)-> super(messageOrBundle, dict) @buffer = new Buffer(@size) @pos = 0 # Write a string to the underlying buffer. writeString: (string, encoding="ascii")-> length = Buffer.byteLength(string, encoding) @buffer.write(string, @pos, length, encoding) @pos += length pad = oscPadding(length) @buffer.fill(0, @pos, @pos + pad) @pos += pad # Write a blob to the underlying buffer. writeBlob: (buffer)-> # copy content from an ArrayBuffer to the underlying buffer if buffer instanceof ArrayBuffer length = buffer.byteLength @writeInt32(length) array = new Int8Array(buffer) i = 0 while i < length @buffer[@pos + i] = array[i] i++ # copy content from a node buffer to the underlying buffer else length = buffer.length @writeInt32(length) buffer.copy(@buffer, @pos) pad = oscPadding(4 + length) @pos += length if pad and pad < 4 @buffer.fill(0, @pos, @pos + pad) @pos += pad # Generate writer methods # for Int32, UInt32, Float, Double. for type, desc of NUMBERS do(type, desc)-> OscBufferPacketGenerator::["write#{type}"] = (value)-> value = @buffer[desc.bufferWriter](value, @pos) @pos += desc.size value #end_only_node # The abstract osc packet parser class AbstractOscPacketParser constructor: (buffer, pos=0, dict)-> @buffer = buffer if typeof pos is "object" @dict = pos @pos = 0 else @dict = dict @pos = pos parse: -> address = @readString() if address is "#bundle" @_parseBundle() else @_parseMessage(address) _parseMessage: (address)-> if address.charAt(0) isnt '/' throw new Error("An address must start with a '/'") if @dict and ((isAddress = address is "/") or address is "/?") # compressed address typeTag = @readTypeTag() args = @parseArguments(typeTag) # check for integer as first type if typeTag.charAt(0) is "i" # resolve address or pattern string id = args[0] # resolve address or pattern address = if isAddress @dict.getAddress(id) else @dict.getPattern(id) if address # slice type tag typeTag = typeTag[1..0] # remove address id args.shift() else typeTag = @readTypeTag() args = @parseArguments(typeTag) new Message(address, typeTag, args) _parseBundle: -> timetag = @readTimetag() elements = [] while not @isEnd() size = @readInt32() # TODO: Do something useful with the boundary boundary = @pos + size elements.push(@parse()) new Bundle(timetag, elements) # Read all values declared in `tag` from # the underlying buffer. # # tag: The type tag. # boundary: The boundary of the message, used for bundles. parseArguments: (tag, boundary)-> i = 0 values = [] while i < tag.length if boundary and @pos >= boundary throw new Error("Message boundary reached") code = tag.charAt(i++) # get type handler type = OSC_TYPES[code] unless type throw new Error("Type `#{code}` isn't supported") # read value and add to values list values.push type.read(@) values readTypeTag: -> tag = @readString() if tag.charAt(0) is ',' tag = tag[1..-1] else throw new Error("A type tag must start with a ','") tag # read timetag and convert it to Date readTimetag: -> fromNTP(@readUInt32(), @readUInt32()) # Read a string from the underlying buffer. # # The implementing class must support ASCII encoding. readString: (encoding, move)-> throw new Error("Abstract method `AbstractOscPacketParser::writeString` called") # data left? isEnd: -> throw new Error("Abstract method `AbstractOscPacketParser::isEnd` called") for name, desc of NUMBERS do (name)-> name = "read#{name}" AbstractOscPacketParser::[name] = -> throw new Error("Abstract method `AbstractOscPacketParser::#{name}` called") # OSC packet parser which operates on ArrayBuffer. class OscArrayBufferPacketParser extends AbstractOscPacketParser constructor: (buffer, pos, dict)-> super @view = new DataView(@buffer) # bytes left? isEnd: -> @buffer.byteLength is 0 or @pos is @buffer.byteLength toString: (encoding, start, end)-> start = start ? 0 end = end ? @buffer.byteLength str = "" while start < end charCode = @view.getInt8(start++) str += String.fromCharCode(charCode & 0x7F) str # Read a blob from the underlying buffer. readBlob: (move=true)-> # get size of blob size = @readInt32() i = 0 array = new Int8Array(new ArrayBuffer(size)) # copy buffer while i < size array[i] = @view.getInt8(@pos+i) i++ if move # total bits must be a multiple of 32bits pad = oscPadding(4 + size) size += pad if pad < 4 @pos += size array.buffer # Read a string from the underlying buffer readString: (encoding="ascii", move=true)-> throw new Error("No data left") if @isEnd() length = 4 nullSeen = false while (pos = @pos + length - 1) < @buffer.byteLength if @view.getInt8(pos) is 0 nullSeen = true break length += 4 if length is 0 or nullSeen is false throw new Error("No string data found") # length of string without null-bytes stringLength = length - 4 while stringLength < length if @view.getInt8(@pos + stringLength) is 0 break stringLength++ string = @toString(encoding, @pos, @pos + stringLength) @pos += length if move string # Generate reader methods # for Int32, UInt32, Float, Double. for type, desc of NUMBERS do(type, desc)-> OscArrayBufferPacketParser::["read#{type}"] = (move=true)-> value = @view[desc.dataViewReader](@pos, false) @pos += desc.size if move value #only_node # OSC packet parser which operates on node buffer. class OscBufferPacketParser extends AbstractOscPacketParser constructor: (buffer, pos, dict)-> super # bytes left? isEnd: -> @buffer.length is 0 or @pos is @buffer.length toString: -> @buffer.toString.apply(@buffer, arguments) # Read a blob from the underlying buffer. readBlob: (move=true)-> # get size of blob size = @readInt32() # create new buffer buf = new Buffer(size) # copy content to the new buffer @buffer.copy(buf, 0, @pos, @pos + size) if move # total bits must be a multiple of 32bits pad = oscPadding(4 + size) size += pad if pad < 4 @pos += size buf # Read a string from the underlying buffer readString: (encoding="ascii", move=true)-> throw new Error("No data left") if @isEnd() length = 4 nullSeen = false while (pos = @pos + length - 1) < @buffer.length if @buffer[pos] is 0 nullSeen = true break length += 4 if length is 0 or nullSeen is false throw new Error("No string data found") # length of string without null-bytes stringLength = length - 4 while stringLength < length if @buffer[@pos + stringLength] is 0 break stringLength++ string = @toString(encoding, @pos, @pos + stringLength) @pos += length if move string # Generate reader methods # for Int32, UInt32, Float, Double. for type, desc of NUMBERS do(type, desc)-> OscBufferPacketParser::["read#{type}"] = (move=true)-> value = @buffer[desc.bufferReader](@pos) @pos += desc.size if move value #end_only_node # Takes a Node.js Buffer- or an ArrayBuffer-object and returns either a osc.Message or oscBundle, # or throws an Error if the buffer isn't well-formed. # # The optional dictionary is for compressed address string support (see below). fromBuffer = (buffer, pos, dict)-> if nodeBuffer and Buffer.isBuffer(buffer) new OscBufferPacketParser(buffer, pos, dict).parse() else new OscArrayBufferPacketParser(buffer, pos, dict).parse() exports.NUMBERS = NUMBERS exports.toNTP = toNTP exports.Message = Message exports.Bundle = Bundle exports.Impulse = Impulse exports.Dictionary = Dictionary exports.AbstractOscPacketGenerator = AbstractOscPacketGenerator exports.AbstractOscPacketParser = AbstractOscPacketParser #only_node exports.OscBufferPacketGenerator = OscBufferPacketGenerator exports.OscBufferPacketParser = OscBufferPacketParser #end_only_node exports.OscArrayBufferPacketGenerator = OscArrayBufferPacketGenerator exports.OscArrayBufferPacketParser = OscArrayBufferPacketParser exports.fromBuffer = fromBuffer