/* * Copyright (C) 2004-2014, C. Ramakrishnan / Illposed Software. * All rights reserved. * * This code is licensed under the BSD 3-Clause license. * See file LICENSE (or LICENSE.html) for more information. */ package com.illposed.osc.utility; import com.illposed.osc.OSCBundle; import com.illposed.osc.OSCImpulse; import com.illposed.osc.OSCMessage; import com.illposed.osc.OSCPacket; import java.math.BigInteger; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * Utility class to convert a byte array, * conforming to the OSC byte stream format, * into Java objects. * * @author Chandrasekhar Ramakrishnan */ public class OSCByteArrayToJavaConverter { private static final String BUNDLE_START = "#bundle"; private static final char BUNDLE_IDENTIFIER = BUNDLE_START.charAt(0); private byte[] bytes; /** Used to decode message addresses and string parameters. */ private Charset charset; private int bytesLength; private int streamPosition; /** * Creates a helper object for converting from a byte array * to an {@link OSCPacket} object. */ public OSCByteArrayToJavaConverter() { this.charset = Charset.defaultCharset(); } /** * Returns the character set used to decode message addresses * and string parameters. * @return the character-encoding-set used by this converter */ public Charset getCharset() { return charset; } /** * Sets the character set used to decode message addresses * and string parameters. * @param charset the desired character-encoding-set to be used by this converter */ public void setCharset(Charset charset) { this.charset = charset; } /** * Converts a byte array into an {@link OSCPacket} * (either an {@link OSCMessage} or {@link OSCBundle}). * @param bytes the storage containing the raw OSC packet * @param bytesLength indicates how many bytes the package consists of (<code><= bytes.length</code>) * @return the successfully parsed OSC packet; in case of a problem, * a <code>RuntimeException</code> is thrown * XXX It is bad design to use instance variables like this. Either create a new instance of the class for each conversion, or pass on the variables to each method call, in turn not having them as member variables. */ public OSCPacket convert(byte[] bytes, int bytesLength) { this.bytes = bytes; this.bytesLength = bytesLength; this.streamPosition = 0; if (isBundle()) { return convertBundle(); } else { return convertMessage(); } } /** * Checks whether my byte array is a bundle. * From the OSC 1.0 specifications: * <quote> * The contents of an OSC packet must be either an OSC Message * or an OSC Bundle. The first byte of the packet's contents unambiguously * distinguishes between these two alternatives. * </quote> * @return true if it the byte array is a bundle, false o.w. */ private boolean isBundle() { // The shortest valid packet may be no shorter then 4 bytes, // thus we may assume to always have a byte at index 0. return bytes[0] == BUNDLE_IDENTIFIER; } /** * Converts the byte array to a bundle. * Assumes that the byte array is a bundle. * @return a bundle containing the data specified in the byte stream */ private OSCBundle convertBundle() { // skip the "#bundle " stuff streamPosition = BUNDLE_START.length() + 1; Date timestamp = readTimeTag(); OSCBundle bundle = new OSCBundle(timestamp); OSCByteArrayToJavaConverter myConverter = new OSCByteArrayToJavaConverter(); myConverter.setCharset(charset); while (streamPosition < bytesLength) { // recursively read through the stream and convert packets you find final int packetLength = readInteger(); if (packetLength == 0) { throw new IllegalArgumentException("Packet length may not be 0"); } else if ((packetLength % 4) != 0) { throw new IllegalArgumentException("Packet length has to be a multiple of 4, is:" + packetLength); } final byte[] packetBytes = new byte[packetLength]; System.arraycopy(bytes, streamPosition, packetBytes, 0, packetLength); streamPosition += packetLength; OSCPacket packet = myConverter.convert(packetBytes, packetLength); bundle.addPacket(packet); } return bundle; } /** * Converts the byte array to a simple message. * Assumes that the byte array is a message. * @return a message containing the data specified in the byte stream */ private OSCMessage convertMessage() { OSCMessage message = new OSCMessage(); message.setAddress(readString()); List<Character> types = readTypes(); if (null == types) { // we are done return message; } moveToFourByteBoundry(); for (int i = 0; i < types.size(); ++i) { if ('[' == types.get(i).charValue()) { // we're looking at an array -- read it in message.addArgument(readArray(types, ++i)); // then increment i to the end of the array while (types.get(i).charValue() != ']') { i++; } } else { message.addArgument(readArgument(types.get(i))); } } return message; } /** * Reads a string from the byte stream. * @return the next string in the byte stream */ private String readString() { int strLen = lengthOfCurrentString(); String res = new String(bytes, streamPosition, strLen, charset); streamPosition += strLen; moveToFourByteBoundry(); return res; } /** * Reads a binary blob from the byte stream. * @return the next blob in the byte stream */ private byte[] readBlob() { final int blobLen = readInteger(); final byte[] res = new byte[blobLen]; System.arraycopy(bytes, streamPosition, res, 0, blobLen); streamPosition += blobLen; moveToFourByteBoundry(); return res; } /** * Reads the types of the arguments from the byte stream. * @return a char array with the types of the arguments, * or <code>null</code>, in case of no arguments */ private List<Character> readTypes() { // The next byte should be a ',', but some legacy code may omit it // in case of no arguments, refering to "OSC Messages" in: // http://opensoundcontrol.org/spec-1_0 if (bytes.length <= streamPosition) { return null; // no arguments } if (bytes[streamPosition] != ',') { // XXX should we not rather fail-fast -> throw exception? return null; } streamPosition++; // find out how long the list of types is int typesLen = lengthOfCurrentString(); if (0 == typesLen) { return null; // no arguments } // read in the types List<Character> typesChars = new ArrayList<Character>(typesLen); for (int i = 0; i < typesLen; i++) { typesChars.add((char) bytes[streamPosition++]); } return typesChars; } /** * Reads an object of the type specified by the type char. * @param type type of the argument to read * @return a Java representation of the argument */ private Object readArgument(char type) { switch (type) { case 'u' : return readUnsignedInteger(); case 'i' : return readInteger(); case 'h' : return readLong(); case 'f' : return readFloat(); case 'd' : return readDouble(); case 's' : return readString(); case 'b' : return readBlob(); case 'c' : return readChar(); case 'N' : return null; case 'T' : return Boolean.TRUE; case 'F' : return Boolean.FALSE; case 'I' : return OSCImpulse.INSTANCE; case 't' : return readTimeTag(); default: // XXX Maybe we should let the user choose what to do in this // case (we encountered an unknown argument type in an // incomming message): // just ignore (return null), or throw an exception? // throw new UnsupportedOperationException( // "Invalid or not yet supported OSC type: '" + type + "'"); return null; } } /** * Reads a char from the byte stream. * @return a {@link Character} */ private Character readChar() { return (char) bytes[streamPosition++]; } private BigInteger readBigInteger(final int numBytes) { final byte[] myBytes = new byte[numBytes]; System.arraycopy(bytes, streamPosition, myBytes, 0, numBytes); streamPosition += numBytes; return new BigInteger(myBytes); } /** * Reads a double from the byte stream. * @return a 64bit precision floating point value */ private Object readDouble() { final BigInteger doubleBits = readBigInteger(8); return Double.longBitsToDouble(doubleBits.longValue()); } /** * Reads a float from the byte stream. * @return a 32bit precision floating point value */ private Float readFloat() { final BigInteger floatBits = readBigInteger(4); return Float.intBitsToFloat(floatBits.intValue()); } /** * Reads a double precision integer (64 bit integer) from the byte stream. * @return double precision integer (64 bit) */ private Long readLong() { final BigInteger longintBytes = readBigInteger(8); return longintBytes.longValue(); } /** * Reads an Integer (32 bit integer) from the byte stream. * @return an {@link Integer} */ private Integer readInteger() { final BigInteger intBits = readBigInteger(4); return intBits.intValue(); } /** * Reads an unsigned integer (32 bit) from the byte stream. * This code is copied from {@see http://darksleep.com/player/JavaAndUnsignedTypes.html}, * which is licensed under the Public Domain. * @return single precision, unsigned integer (32 bit) wrapped in a 64 bit integer (long) */ private Long readUnsignedInteger() { int firstByte = (0x000000FF & ((int) bytes[streamPosition++])); int secondByte = (0x000000FF & ((int) bytes[streamPosition++])); int thirdByte = (0x000000FF & ((int) bytes[streamPosition++])); int fourthByte = (0x000000FF & ((int) bytes[streamPosition++])); return ((long) (firstByte << 24 | secondByte << 16 | thirdByte << 8 | fourthByte)) & 0xFFFFFFFFL; } /** * Reads the time tag and convert it to a Java Date object. * A timestamp is a 64 bit number representing the time in NTP format. * The first 32 bits are seconds since 1900, the second 32 bits are * fractions of a second. * @return a {@link Date} */ private Date readTimeTag() { byte[] secondBytes = new byte[8]; byte[] fractionBytes = new byte[8]; for (int i = 0; i < 4; i++) { // clear the higher order 4 bytes secondBytes[i] = 0; fractionBytes[i] = 0; } // while reading in the seconds & fraction, check if // this timetag has immediate semantics boolean isImmediate = true; for (int i = 4; i < 8; i++) { secondBytes[i] = bytes[streamPosition++]; if (secondBytes[i] > 0) { isImmediate = false; } } for (int i = 4; i < 8; i++) { fractionBytes[i] = bytes[streamPosition++]; if (i < 7) { if (fractionBytes[i] > 0) { isImmediate = false; } } else { if (fractionBytes[i] > 1) { isImmediate = false; } } } if (isImmediate) { return OSCBundle.TIMESTAMP_IMMEDIATE; } final long secsSince1900 = new BigInteger(secondBytes).longValue(); long secsSince1970 = secsSince1900 - OSCBundle.SECONDS_FROM_1900_TO_1970; // no point maintaining times in the distant past if (secsSince1970 < 0) { secsSince1970 = 0; } long fraction = (new BigInteger(fractionBytes).longValue()); // this line was cribbed from jakarta commons-net's NTP TimeStamp code fraction = (fraction * 1000) / 0x100000000L; // I do not know where, but I'm losing 1ms somewhere... fraction = (fraction > 0) ? fraction + 1 : 0; long millisecs = (secsSince1970 * 1000) + fraction; return new Date(millisecs); } /** * Reads an array from the byte stream. * @param types * @param pos at which position to start reading * @return the array that was read */ private List<Object> readArray(List<Character> types, int pos) { int arrayLen = 0; while (types.get(pos + arrayLen).charValue() != ']') { arrayLen++; } List<Object> array = new ArrayList<Object>(arrayLen); for (int j = 0; j < arrayLen; j++) { array.add(readArgument(types.get(pos + j))); } return array; } /** * Get the length of the string currently in the byte stream. */ private int lengthOfCurrentString() { int len = 0; while (bytes[streamPosition + len] != 0) { len++; } return len; } /** * Move to the next byte with an index in the byte array * which is dividable by four. */ private void moveToFourByteBoundry() { // If i am already at a 4 byte boundry, I need to move to the next one int mod = streamPosition % 4; streamPosition += (4 - mod); } }