<?php
/** Open Sound Control (OSC) Client Library for PHP
* Author: Andy W. Schmeder <andy@a2hd.com>
* Copyright 2003
*
* Version 0.1
*
* Requirements: PHP 4.1.0 or later.
* For information about Open Sound Control,
* see http://cnmat.berkeley.edu/OSC
*
* This is free software.
* It may contain bugs, design flaws or other unforseeable problems.
* Please feel free to report problems (or success stories) to the author.
*
* License: LGPL version 2.1 or later.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* For questions regarding this library contact
* Andy W. Schmeder <andy@a2hd.com>
*/
// Test if this machine is a little endian architecture
function test_little_endian() {
$cpu_int = pack("L", 1); // Machine dependent
$be_int = pack("N", 1); // Machine independent
if($cpu_int[0] == $be_int[0]) {
return FALSE;
} else {
return TRUE;
}
}
// Test if this machine uses twos complement representation
function test_twos_complement() {
$cpu_int = pack("i", -1); // Machine dependent
if(ord($cpu_int[0]) == 255) {
return TRUE;
} else {
return FALSE;
}
}
// Take note of the configuration for this machine.
$_arch_little_endian = test_little_endian();
$_arch_twos_complement = test_twos_complement();
if(! $_arch_twos_complement) {
trigger_error("WARNING: This machine does not use twos-complement integers. " .
"Negative numbers may not be represented correctly.",
E_USER_NOTICE);
}
/** This is a utility function to convert from CPU byte order to network order (big endian).
*
* It is necessary to use this function because PHP's pack() function does not support
* big endian encoding for most data types. (It only does big endian for unsigned ints).
*/
function host_to_network_order($str) {
global $_arch_little_endian;
if($_arch_little_endian) {
$swstr = "";
for($i = 0; $i < strlen($str); $i++) {
$swstr .= $str[(strlen($str)-1)-$i];
}
return $swstr;
} else {
// No conversion necessary for big-endian arch
return $str;
}
}
/** OSCDatagram is a virtual base class for OSCMessage and OSCBundle.
*/
class OSCDatagram {
// Virtual private data
var $bin = NULL;
var $data = NULL;
// Virtual functions
function get_binary() {}
function clear() {}
// Shared functions
/** Returns a semi-human readable representation of the binary data.
* Printable bytes will appear as "_C" where C is the printable character.
* Non-printable bytes will appear in hex, e.g. '20' for a space (\s) character
* Bytes are clustered in groups of 4 to show the alignment.
*/
function get_human_readable() {
$bin = $this->get_binary();
$hex = "";
for($i = 0; $i < strlen($bin); $i++) {
if(ord($bin[$i]) >= 33 && ord($bin[$i]) <= 126) { // Printable characters
$hex .= "_" . chr(ord($bin[$i]));
} else {
$hex .= sprintf("%02x", ord($bin[$i]));
}
if($i != 0 && $i < strlen($bin) && ($i+1) % 4 == 0) {
$hex .= " ";
}
}
return $hex . "\n";
}
/** Pack data into $this->bin as 4-byte aligned, network-byte order.
*/
function pack_data($data, $type_hint) {
$bin = "";
switch($type_hint) {
case "T":
case "F":
case "N":
case "I":
return; // These types have no allocated space
case "A":
foreach($data as $arg) {
$this->pack_data($arg[0], $arg[1]);
}
break;
case "s":
$data .= "\0"; // The builtin \0 terminator is ignored... we must explicitly request one.
$bin = pack("a*" . $this->get_strpad($data), $data);
break;
case "b":
$this->pack_data(strlen($data->bin), "i");
$bin = pack("a*" . $this->get_strpad($data->bin), $data->bin);
break;
case "i":
$bin = host_to_network_order(pack("i", $data)); // Machine-independent size (4-bytes)
break;
case "f":
$bin = host_to_network_order(pack("f", $data)); // Machine-dependent size
if(strlen($bin) != 4) {
$this->error("Sorry, your machine uses an unsupported single-precision floating point size.");
}
break;
case "d":
$bin = host_to_network_order(pack("d", $data)); // Machine-dependent size
if(strlen($bin) != 8) {
$this->error("Sorry, your machine uses an unsupported double-precision floating point size.");
}
break;
case "t":
if(is_null($data)) { $data = array(0, 1); }
$bin = host_to_network_order(pack("L", $data->sec)) .
host_to_network_order(pack("L", $data->frac_sec));
break;
}
if(strlen($bin) % 4 != 0) {
$this->error("$data failed to align properly, size is " . strlen($bin) . " bytes.");
}
$this->bin .= $bin;
}
/** Utility to generate padding for strings
*/
function get_strpad($str) {
$x = (strlen($str)) % 4;
if($x == 0) {
return '';
} else {
$x = 4 - $x;
}
switch($x) {
case 1:
return 'x';
case 2:
return 'xx';
case 3:
return 'xxx';
default:
$this->error("Pad calculation is screwy, x = $x");
}
}
/** Report an error
*/
function error($message) {
trigger_error("OSCDatagram Error: $message", E_USER_ERROR);
}
}
/** OSCMessage type
*/
class OSCMessage extends OSCDatagram {
var $address = "/";
var $typetags = ",";
var $data = array();
/** Make a new message - Optionally specify address and arguements.
*
* e.g. $a = new OSCMessage("/foo", array(1, 2.94, "bar"))
* It is not possible to provide type-hinting using this initialization method.
*/
function OSCMessage($address = NULL, $args = NULL) {
if(! is_null($address)) {
$this->address = $address;
}
if(is_array($args)) {
foreach($args as $arg) {
$this->add_arg($arg);
}
}
}
/** Reset internal data structures
*/
function clear() {
$this->address = "/";
$this->typetags = ",";
$this->data = array();
$this->bin = NULL;
}
/** Set packet address
* e.g. "/test".
* See OSC spec for details on allowed characters in an OSC address.
*/
function set_address($addr) {
$this->bin = NULL;
$this->address = $addr;
}
/** Add an arg to the OSC message.
* $data can be an integer, float, string, boolean, NULL, or an array of those types.
* $type-hint is optional.
*/
function add_arg($data, $type_hint = NULL) {
$this->bin = NULL;
if($type_hint == NULL) {
$type_hint = $this->get_type($data);
}
$data = $this->set_type($data, $type_hint);
array_push($this->data, array($data, $type_hint));
}
/** Try to guess the type of data.
* If this does not work for you, try using a type-hint.
*/
function get_type($data) {
switch(gettype($data)) {
case "integer":
return "i";
case "double":
case "float":
return "f";
case "string":
return "s";
case "boolean":
if($data) {
return "T";
} else {
return "F";
}
case "array":
// Array type will be handled later... 'A' is not actually an OSC type.
return "A";
case "object":
switch(strtolower(get_class($data))) {
case "infinitum":
return "I";
case "timetag":
return "t";
case "blob":
return "b";
default:
$this->error("Unknown or unsupported object type.");
}
case "NULL":
return "N";
default:
$this->error("Unknown or unsupported data type.");
}
}
/** Cast data to type, and add type info to typetags.
*/
function set_type($data, $type_tag) {
switch($type_tag) {
case "i":
$this->typetags .= "i";
return (int)$data;
case "f";
$this->typetags .= "f";
return (double)$data;
case "d";
$this->typetags .= "d";
return (double)$data;
case "s":
case "c":
$this->typetags .= "s";
return (string)$data;
case "T":
$this->typetags .= "T";
return TRUE;
case "F":
$this->typetags .= "F";
return FALSE;
case "N":
$this->typetags .= "N";
return NULL;
case "I":
$this->typetags .= "I";
return $data;
case "t":
$this->typetags .= "t";
return $data;
case "b":
$this->typetags .= "b";
return $data;
case "A":
// Array is now expanded...
$this->typetags .= "[";
$data = (array)$data;
for($i = 0; $i < count($data); $i++) {
$type_tag = $this->get_type($data[$i]);
$data[$i] = array($this->set_type($data[$i], $type_tag), $type_tag);
}
$this->typetags .= "]";
return $data;
default:
trigger_error("Unrecognized type tag, '$type_tag'", E_USER_ERROR);
}
}
function get_binary() {
// Check for cached binary representation and reuse if found.
if(! is_null($this->bin)) {
return $this->bin;
}
// Pack address...
$this->pack_data($this->address, "s");
// Pack typetags...
$this->pack_data($this->typetags, "s");
// Pack args...
foreach($this->data as $arg) {
$this->pack_data($arg[0], $arg[1]);
}
return $this->bin;
}
}
/** OSCBundle datagram type
* This object can contain any number of other OSCDatagram objects.
*/
class OSCBundle extends OSCDatagram {
var $data = array();
var $timetag = NULL;
/** Create a new OSCBundle datagram
*
* $init may be an array of OSCDatagram objects,
* e.g. $b = new OSCBundle(new OSCMessage(...), new OSCBundle(...))
*
* Otherwise, add messages at runtime using OSCBundle::add_datagram.
*/
function OSCBundle($init = NULL) {
if(is_array($init)) {
foreach($init as $d) {
$this->add_datagram($d);
}
}
}
/** Set time tag as whole seconds since July 1, 1970, and fraction of a second.
* This feature is not tested, but it should work if you need it.
*
* If timetag is not set, it will default to "Immediate".
*/
function set_timetag($timetag_obj) {
$this->timetag = $timetag_obj;
}
/** Add an OSCDatagram object to a bundle.
* This can be either an OSCMessage or an OSCBundle.
* However, you cannot reasonably add a bundle to itself.
*/
function add_datagram($osc_datagram) {
$this->bin = NULL;
array_push($this->data, $osc_datagram);
}
function clear() {
$this->bin = NULL;
$this->data = NULL;
}
function get_binary() {
if($this->bin != NULL) {
return $this->bin;
}
$this->bin = "";
$this->pack_data("#bundle", "s");
$this->pack_data(NULL, "t");
foreach($this->data as $datagram) {
$bin = $datagram->get_binary();
$this->pack_data((int)strlen($bin), "i");
$this->bin .= $bin;
}
return $this->bin;
}
}
/** OSCClient uses a connectionless UDP socket to transmit binary to its destination.
*
* Example of use:
*
* $c = new OSCClient();
* $c->set_destination("192.168.1.5", 3890);
* $c->send(new OSCMessage("/foo", array(1,2,3)));
* ... etc.
*
* Since it is connectionless, you can change the destination address/port at any time.
* If you are having problems establishing communication, it may be due to a bad address,
* improper setup of the IP routing table, or a problem on the other end. When in doubt,
* use tcpdump or ethereal to check that packets are indeed being transmitted.
*/
class OSCClient {
var $sock = NULL;
var $address = NULL;
var $port = NULL;
function OSCClient($address = NULL, $port = NULL) {
$this->address = $address;
$this->port = $port;
if(($this->sock = socket_create(AF_INET, SOCK_DGRAM, 0)) < 0) {
$this->error("Could not create datagram socket.");
}
}
/** Destructor function, usually not needed, provided in case you want to free the socket.
*/
function destroy() {
socket_close($this->sock);
}
/*
// You can enable this part if you have PHP 4.3.0 or later...
function enable_broadcast() {
if(($ret = socket_set_option($this->sock, SOL_SOCKET, SO_BROADCAST, 1)) < 0) {
$this->error("Failed to enable broadcast option.");
}
}
function disable_broadcast() {
if(($ret = socket_set_option($this->sock, SOL_SOCKET, SO_BROADCAST, 0)) < 0) {
$this->error("Failed to disable broadcast option.");
}
}
*/
/** Address is an IP address, given as a string.
* To convert a hostname to IP, use gethostbyname('www.example.com')
* You must also specify a port as an integer, typically $port is larger than 1024.
*/
function set_destination($address, $port) {
$this->address = $address;
$this->port = $port;
}
/** send() accepts either an OSCDatagram object or a binary string
*/
function send($message) {
if(is_null($this->address) || is_null($this->port)) {
$this->error("Destination is not well-defined. Please use OSCClient::set_destination().");
}
if(is_object($message)) {
$message = $message->get_binary();
}
if(($ret = socket_sendto($this->sock, $message, strlen($message), 0, $this->address, $this->port)) < 0) {
$this->error("Transmission failure.");
}
if($ret != strlen($message)) {
$mlen = strlen($message);
$this->error("Could not send the entire message, only $ret bytes were sent, of $mlen total");
}
return $ret;
}
/** Report a fatal error.
*/
function error($message) {
trigger_error("OSCClient Error: $message", E_USER_ERROR);
}
}
/** Object to represent the OSC Infinitum type ("I")
*/
class Infinitum {
}
/** 64-bit OSC timetag type, Refer to NTP format for details.
*/
/**class Timetag {
function Timetag($sec = 0, $frac_sec = 1) {
$this->sec = $sec;
$this->frac_sec = $frac_sec;
}
}*/
/** Binary Blob datatype
* Blob is basically a non-null-terminated string prefixed by a size indicator.
*/
class Blob {
function Blob($bin) {
$this->bin = $bin;
}
}
/** Run some tests to make sure the library behaves in a sane way.
the modifications begins here
replace localhost with server's adress and put the correct port
*/
function test_osc_lib() {
$c = new OSCClient();
$c->set_destination("localhost", 3333);
/**
$m1 = new OSCMessage("/test", array(new Timetag(3294967295, 5), new Infinitum(), new Blob("aoeuaoeu!")));
$m1->add_arg(28658.93, "d");
*/
$m2 = new OSCMessage("/bar", array(test, one, two));
$b = new OSCBundle();
$b->add_datagram($m1);
$b->add_datagram($m2);
$b2 = new OSCBundle(array($m1, $b));
//echo $b2->get_human_readable();
//echo $m1->get_human_readable();
$c->send($m2);
}
// Uncomment to run the test
test_osc_lib();
?>