Merge changes from jamieforth

This commit is contained in:
Jamie Forth 2022-08-24 20:44:54 +02:00 committed by nik gaffney
commit 67118a4a14
Signed by: nik
GPG key ID: 989F5E6EDB478160
16 changed files with 1583 additions and 259 deletions

View file

@ -1,19 +1,20 @@
# Open Sound Control # Open Sound Control
This is a common lisp implementation of the Open Sound Control Protocol aka OSC. The code should be close to the ansi standard, and does not rely on any external code/ffi/etc+ to do the basic encoding and decoding of packets. since OSC does not specify a transport layer, messages can be send using TCP or UDP (or carrier pigeons), however it seems UDP is more common amongst the programmes that communicate using the OSC protocol. the osc-examples.lisp file contains a few simple examples of how to send and recieve OSC via UDP, and so far seems reasonably compatible with the packets send from/to max-msp, pd, supercollider and liblo. more details about OSC can be found at http://www.cnmat.berkeley.edu/OpenSoundControl/ This is a common lisp implementation of the Open Sound Control Protocol aka OSC. The code should be close to the ansi standard, and does not rely on any external code/ffi/etc+ to do the basic encoding and decoding of packets. since OSC does not specify a transport layer, messages can be send using TCP or UDP (or carrier pigeons), however it seems UDP is more common amongst the programmes that communicate using the OSC protocol. the osc-examples.lisp file contains a few simple examples of how to send and recieve OSC via UDP, and so far seems reasonably compatible with the packets send from/to max-msp, pd, supercollider and liblo. more details about OSC can be found at https://opensoundcontrol.org/
The devices/examples/osc-device-examples.lisp contains examples of a higher-level API for sending and receiving OSC messages.
the current version of this code is avilable from github the current version of this code is avilable from github
git clone https://github.com/zzkt/osc `git clone https://github.com/zzkt/osc`
or via quicklisp.. . or via quicklisp.. .
(ql:quickload "osc") `(ql:quickload "osc")`
## limitations ## limitations
- doesn't send nested bundles or syncronisable timetags - will raise an exception if input is malformed
- will raise an exception if the input is malformed
- doesn't do any pattern matching on addresses - doesn't do any pattern matching on addresses
- float en/decoding only tested on sbcl, cmucl, openmcl and allegro - float en/decoding only tested on sbcl, cmucl, openmcl and allegro
- only supports the type(tag)s specified in the OSC spec - only supports the type(tag)s specified in the OSC spec
@ -24,7 +25,6 @@ or via quicklisp.. .
- data checking and error handling - data checking and error handling
- portable en/decoding of floats -=> ieee754 tests - portable en/decoding of floats -=> ieee754 tests
- doubles and other defacto typetags - doubles and other defacto typetags
- correct en/decoding of timetags
## things to do in :osc-ex[tensions|tras] ## things to do in :osc-ex[tensions|tras]
@ -32,20 +32,29 @@ or via quicklisp.. .
- add namespace exploration using cl-zeroconf - add namespace exploration using cl-zeroconf
# changes # changes
- 2022-08-22
- version 0.6
- further improvements from jamieforth
- 2019-04-02 - 2019-04-02
- encoder/decoder refactoring from Javier Olaechea @PuercoPop - encoder/decoder refactoring from Javier Olaechea @PuercoPop
- 2017-12-10 - 2017-12-10
- osc-examples use usocket for portability from @boqs - osc-examples converted to usocket for portability from @boqs
- 2015-08-25 - 2015-08-25
- support for 64bit ints from Erik Ronström https://github.com/erikronstrom - support for 64bit ints from Erik Ronström https://github.com/erikronstrom
- 2015-08-21
- implement nested bundles from jamieforth https://github.com/jamieforth
- 2011-04-19 - 2011-04-19
- converted repo from darcs->git - converted repo from darcs->git
- 2010-09-25
- add osc-devices API from jamieforth
- 2010-09-10
- timetag improvements from jamieforth https://github.com/jamieforth/osc
- 2007-02-20 - 2007-02-20
- version 0.5 - version 0.5
- Allegro CL float en/decoding from vincent akkermans <vincent.akkermans@gmail.com> - Allegro CL float en/decoding from vincent akkermans <vincent.akkermans@gmail.com>
- 2006-02-11 - 2006-02-11
- version 0.4 - version 0.4
- partial timetag implemetation - partial timetag implementation
- 2005-12-05 - 2005-12-05
- version 0.3 - version 0.3
- fixed openmcl float bug (decode-uint32) - fixed openmcl float bug (decode-uint32)
@ -74,4 +83,3 @@ or via quicklisp.. .
- tests in osc-tests.lisp - tests in osc-tests.lisp
- 2004-12-18 - 2004-12-18
- initial version, single args only - initial version, single args only

87
devices/client.lisp Normal file
View file

@ -0,0 +1,87 @@
(cl:in-package #:osc)
(defun make-osc-client (&key (protocol :udp) debug-mode
(buffer-size *default-osc-buffer-size*)
address-tree cleanup-fun)
(ecase protocol
(:udp (make-instance 'osc-client-udp
:debug-mode debug-mode
:socket-buffer (make-socket-buffer
buffer-size)
:address-tree (if address-tree
address-tree
(make-osc-tree))
:cleanup-fun cleanup-fun))
(:tcp (make-instance 'osc-client-tcp
:debug-mode debug-mode
:socket-buffer (make-socket-buffer
buffer-size)
:address-tree (if address-tree
address-tree
(make-osc-tree))
:cleanup-fun cleanup-fun))))
(defmethod initialize-instance :after ((client osc-client-udp) &key)
(make-client-responders client))
(defgeneric make-client-responders (server))
(defmethod make-client-responders ((client osc-client-udp))
(add-osc-responder client "/cl-osc/server/registered"
(cmd args device address port timetag bundle)
(format t "Registered with server at ~A~%"
(make-addr+port-string address port)))
(add-osc-responder client "/cl-osc/server/quit"
(cmd args device address port timetag bundle)
(format t "Server ~A has quit~%"
(make-addr+port-string address port))))
(defgeneric register (client)
(:method ((client osc-client-udp))
(send-msg client "/cl-osc/register" (port client))))
(defmethod osc-device-cleanup ((device osc-client-udp))
(send-msg device "/cl-osc/quit")
(call-next-method))
(defun make-osc-client-endpoint-tcp (socket debug-mode buffer-size
address-tree clients &optional
cleanup-fun)
(socket-make-stream socket
:input nil :output t
:element-type '(unsigned-byte 8)
:buffering :full)
(let ((client (make-instance 'osc-client-endpoint-tcp
:debug-mode debug-mode
:address-tree address-tree
:socket-buffer (make-socket-buffer
buffer-size)
:clients clients
:cleanup-fun cleanup-fun)))
(set-socket socket client)
(set-listening-thread (make-listening-thread client) client)
client))
(defmethod make-listening-thread ((receiver osc-client-tcp))
"Creates a listening thread for tcp clients."
(sb-thread:make-thread
(lambda ()
(unwind-protect
(loop
do (multiple-value-bind (buffer length address port)
(socket-receive (socket receiver)
(socket-buffer receiver) nil)
(when (eq length 0) ; Closed by remote
(sb-thread:terminate-thread
sb-thread:*current-thread*))
(multiple-value-bind (data timetag)
(decode-bundle buffer :end length)
(when (debug-mode receiver)
(print-osc-debug-msg receiver data length
(peer-address receiver)
(peer-port receiver) timetag))
(dispatch (address-tree receiver) data receiver
address port))))
(osc-device-cleanup receiver)))
:name (format nil "osc-client-tcp-connection: ~A~%"
(name receiver))))

129
devices/device.lisp Normal file
View file

@ -0,0 +1,129 @@
(cl:in-package #:osc)
;;;=====================================================================
;;; OSC device base class
;;;=====================================================================
(defclass osc-device ()
((socket
:reader socket
:writer set-socket
:initform nil)
(debug-mode
:reader debug-mode
:writer set-debug-mode
:initarg :debug-mode)
(cleanup-fun
:reader cleanup-fun
:initarg :cleanup-fun
:initform nil)))
;;;=====================================================================
;;; OSC device mixin classes
;;;=====================================================================
(defclass udp-device (osc-device) ())
(defclass tcp-device (osc-device) ())
(defclass listening-device (osc-device)
((listening-thread
:reader listening-thread
:writer set-listening-thread
:initform nil)))
(defclass receiving-device (listening-device)
((socket-buffer
:reader socket-buffer
:initarg :socket-buffer)))
(defclass dispatching-device (listening-device)
((address-tree
:reader address-tree
:initarg :address-tree
:initform (make-osc-tree))))
(defclass dispatching-device-udp (dispatching-device receiving-device
udp-device) ())
;;;=====================================================================
;;; OSC device abstract classes
;;;=====================================================================
(defclass osc-transmitter (osc-device) ())
(defclass osc-client (dispatching-device receiving-device
osc-transmitter) ())
(defclass osc-server (dispatching-device osc-transmitter)
((buffer-size
:reader buffer-size
:initarg :buffer-size)
(clients
:reader clients
:initarg :clients
:initform (make-clients-hash))))
(defclass osc-client-endpoint (osc-client)
((clients
:reader clients
:initarg :clients)))
;;;=====================================================================
;;; OSC device concrete classes
;;;=====================================================================
(defclass osc-transmitter-udp (osc-transmitter udp-device) ())
(defclass osc-client-udp (osc-client dispatching-device-udp) ())
(defclass osc-client-tcp (osc-client tcp-device) ())
(defclass osc-server-udp (osc-server dispatching-device-udp
osc-transmitter-udp) ())
(defclass osc-server-tcp (osc-server osc-transmitter tcp-device) ())
(defclass osc-client-endpoint-tcp (osc-client-endpoint
osc-client-tcp) ())
;;;=====================================================================
;;; Device generic functions
;;;=====================================================================
(defgeneric protocol (osc-device)
(:method ((osc-device udp-device))
:udp)
(:method ((osc-device tcp-device))
:tcp))
(defgeneric name (osc-device)
(:method ((osc-device osc-device))
(concatenate 'string
(symbol-name (class-name (class-of osc-device)))
"-"
(make-name-string osc-device))))
(defmethod buffer-size ((osc-device dispatching-device))
(length (socket-buffer osc-device)))
(defgeneric quit (osc-device))
(defgeneric osc-device-cleanup (device)
(:method :before ((osc-device osc-device))
(when (cleanup-fun osc-device)
(funcall (cleanup-fun osc-device) osc-device)))
(:method ((osc-device osc-device))
(when (debug-mode osc-device)
(format t "~%OSC device stopped: ~A~%"
(name osc-device)))
(when (socket osc-device)
(handler-case
(socket-close (socket osc-device) :abort t)
(sb-int:simple-stream-error ()
(when (debug-mode osc-device)
(warn "Device ~A gone away." (name osc-device)))))
(set-socket nil osc-device))))

View file

@ -0,0 +1,39 @@
(cl:in-package #:osc)
(defmethod make-listening-thread ((receiver dispatching-device-udp))
"Creates a listening thread for udp devices (client and server)."
(sb-thread:make-thread
(lambda ()
(unwind-protect
(loop
do (multiple-value-bind (buffer length address port)
(socket-receive (socket receiver)
(socket-buffer receiver) nil)
(multiple-value-bind (data timetag)
(osc:decode-bundle buffer :end length)
(when (debug-mode receiver)
(print-osc-debug-msg receiver data length
address port timetag))
(dispatch (address-tree receiver) data receiver
address port))))
(osc-device-cleanup receiver)))
:name (format nil "osc-receiver-udp: ~A~%" (name receiver))))
;;;=====================================================================
;;; OSC Responders
;;;=====================================================================
(defmacro add-osc-responder (dispatcher cmd-name
(cmd args device address port timetag bundle)
&body body)
`(dp-register (address-tree ,dispatcher) ,cmd-name
(lambda (,cmd ,args ,device ,address ,port ,timetag
,bundle)
(declare (ignorable ,cmd ,args ,device ,address
,port ,timetag ,bundle))
,@body)))
(defgeneric remove-osc-responder (dispatcher address)
(:method ((dispatcher dispatching-device) address)
(dp-remove (address-tree dispatcher) address)))

View file

@ -0,0 +1,294 @@
(cl:in-package #:osc)
(ql:quickload "osc")
;;;=====================================================================
;;; OSC UDP transmitter -> server
;;;=====================================================================
(defparameter *osc-server* (make-osc-server :protocol :udp
:debug-mode t))
(boot *osc-server* 57127)
(defparameter *osc-transmitter* (make-osc-transmitter
:debug-mode t))
(connect *osc-transmitter* 57127 :host-name "localhost")
(device-active-p *osc-transmitter*)
(device-socket-name *osc-transmitter*)
(address *osc-transmitter*)
(port *osc-transmitter*)
(device-socket-peername *osc-transmitter*)
(peer-address *osc-transmitter*)
(peer-port *osc-transmitter*)
(send-msg *osc-transmitter* "/bar" 1 2 9)
(send-bundle *osc-transmitter*
:time ; current real time
"/foo" 1 2 3)
(send-bundle *osc-transmitter*
:now ; immediately
"/foo" 1 2 3)
(send-bundle *osc-transmitter*
(unix-time->timetag 1234567890.1234567d0)
"/foo" 1 2 3)
;; The lower-level send function can be used to send message and
;; bundle objects directly. This allows more complex (nested) bundles
;; to be created.
(send *osc-transmitter* (message "/foo" 1 2 3))
(send *osc-transmitter* (bundle :now
(message "/foo" 1 2 3)))
(let ((bundle
(bundle :now
(message "/foo" '(1 2 3))
(bundle :now
(bundle :now
(message "/bar"
'(10 20 30)))))))
(send *osc-transmitter* bundle))
(quit *osc-transmitter*)
(quit *osc-server*)
;;;=====================================================================
;;; OSC UDP client <-> server
;;;=====================================================================
(defparameter *osc-server* (make-osc-server :protocol :udp
:debug-mode t))
(boot *osc-server* 57127)
(defparameter *osc-client* (make-osc-client
:protocol :udp
:debug-mode t))
(connect *osc-client* 57127 :host-name "localhost")
;; A UDP server can't know about a client unless it registers.
(print-clients *osc-server*)
(register *osc-client*)
(print-clients *osc-server*)
(quit *osc-client*) ; quit notifies the server
(print-clients *osc-server*)
(connect *osc-client* 57127 :host-name "localhost")
(send-msg *osc-client* "/foo" 2 99)
(send-bundle *osc-client*
(unix-time->timetag 1234567890.1234567d0)
"/foo" 1 2 3)
(send-bundle *osc-client* :now "/foo" 1)
(send-bundle *osc-client* :time "/foo" 1)
;; Using the server as a transmitter.
(send-msg-to *osc-server*
(address *osc-client*) (port *osc-client*)
"/bar" 1 2 3)
(send-bundle-to *osc-server*
(address *osc-client*) (port *osc-client*)
:now "/bar" 1 2 3)
;; If a client is registered...
(send-msg-to-client *osc-server* (make-name-string *osc-client*)
"/bar" 2 99)
(register *osc-client*)
(send-msg-to-client *osc-server* (make-name-string *osc-client*)
"/bar" 2 99)
(send-bundle-to-client *osc-server*
(make-name-string *osc-client*)
:time "/bar" 2 99)
(add-osc-responder *osc-server* "/echo-sum"
(cmd args dev addr port timetag bundle)
(send-msg-to dev addr port
"/echo-answer" (apply #'+ args)))
(add-osc-responder *osc-client* "/echo-answer"
(cmd args dev addr port timetag bundle)
(format t "Sum is ~a~%" (car args)))
(send-msg *osc-client* "/echo-sum" 1 2 3 4)
(add-osc-responder *osc-server* "/timetag+1"
(cmd args dev addr port timetag bundle)
(send-bundle-to dev addr port (timetag+ timetag 1) "/the-future"))
(send-bundle *osc-client* (get-current-timetag)
"/timetag+1")
;; Send a messages to all registered clients.
(send-msg-all *osc-server* "/foo" 1 2 3)
(send-bundle-all *osc-server* :now "/foo" 1 2 3)
(defparameter *osc-client2* (make-osc-client
:protocol :udp
:debug-mode t))
(connect *osc-client2* 57127)
(register *osc-client2*)
(add-osc-responder *osc-server* "/echo-sum"
(cmd args dev addr port timetag bundle)
(send-msg-all dev "/echo-answer" (apply #'+ args)))
(send-msg *osc-client* "/echo-sum" 1 2 3 4)
(quit *osc-client*)
(quit *osc-client2*)
(quit *osc-server*)
;;;=====================================================================
;;; OSC TCP client <-> server
;;;=====================================================================
(defparameter *osc-server* (make-osc-server :protocol :tcp
:debug-mode t))
(boot *osc-server* 57127)
(defparameter *osc-client* (make-osc-client
:protocol :tcp
:debug-mode t))
(connect *osc-client* 57127 :host-name "localhost")
(device-active-p *osc-client*)
(device-socket-name *osc-client*)
(device-socket-peername *osc-client*)
(send-msg *osc-client* "/foo" 1 2 3)
(send-msg-to-client *osc-server* (make-name-string
*osc-client*)
"/foo" 1 2 3)
(defparameter *osc-client2* (make-osc-client
:protocol :tcp
:debug-mode t))
(connect *osc-client2* 57127
:host-address "127.0.0.1"
:port 57666) ; choose local port
(device-socket-name *osc-client2*)
(send-msg *osc-client2* "/bar" 4 5 6 9)
(print-clients *osc-server*)
(add-osc-responder *osc-server* "/print-sum"
(cmd args dev addr port timetag bundle)
(format t "Sum = ~A~%" (apply #'+ args)))
(send-msg *osc-client2* "/print-sum" 4 5 6 9)
(add-osc-responder *osc-server* "/echo-sum"
(cmd args dev addr port timetag bundle)
(send-msg dev cmd (apply #'+ args)))
(send-msg *osc-client2* "/echo-sum" 4 5 6 9)
(send-msg-all *osc-server* "/bar" 1 2 3) ; send to all peers
(add-osc-responder *osc-server* "/echo-sum-all"
(cmd args dev addr port timetag bundle)
(send-msg-all dev cmd (apply #'+ args)))
; Send to all peers (including self).
(send-msg *osc-client2* "/echo-sum-all" 1 2 3)
(quit *osc-client*)
(quit *osc-client2*)
(quit *osc-server*)
;;;=====================================================================
;;; OSC UDP client <-> sclang
;;;=====================================================================
(defparameter *osc-client* (make-osc-client
:protocol :udp
:debug-mode t))
(connect *osc-client* 57120 :host-name "localhost" :port 57127)
(address *osc-client*)
(port *osc-client*)
(peer-address *osc-client*)
(peer-port *osc-client*)
;;---------------------------------------------------------------------
;; run in sc
c=OSCresponder(nil,
'/foo',
{|t,r,msg,addr| [t,r,msg,addr].postln}).add
;;---------------------------------------------------------------------
(send-msg *osc-client* "/foo" 1 2 3)
(send-bundle *osc-client*
(get-current-timetag)
"/foo" 3)
(add-osc-responder *osc-client* "/echo-sum"
(cmd args dev addr port timetag bundle)
(send-msg dev cmd (apply #'+ args)))
;;---------------------------------------------------------------------
;; Send /echo-sum from sc, and lisp returns the sum.
n=NetAddr("localhost", 57127)
e=OSCresponder(nil,
'/echo-sum',
{|t,r,msg,addr|
[t,r,msg,addr].postln;
}).add
n.sendMsg('/echo-sum', 1, 2, 3) // send numbers, lisp returns sum.
;;---------------------------------------------------------------------
(quit *osc-client*)
;;;=====================================================================
;;; OSC UDP client <-> scsynth
;;;=====================================================================
(defparameter *osc-client* (make-osc-client
:protocol :udp
:debug-mode t))
(connect *osc-client* 57110 :host-name "localhost" :port 57127)
(send-msg *osc-client* "/s_new" "default" 1001 0 0 "freq" 500)
(send-msg *osc-client* "/n_free" 1001)
(send-bundle *osc-client*
(timetag+ (get-current-timetag) 2) ; 2 secs later
"/s_new" "default" 1001 0 0 "freq" 500)
(send-msg *osc-client* "/n_free" 1001)
(quit *osc-client*) ; Sends default /quit notification which scsynth
; ignores. Ideally osc-client should be subclassed
; to allow scsynth specific behaviour to be
; implemented.

View file

@ -0,0 +1,31 @@
(cl:in-package #:osc)
(defgeneric make-listening-thread (listening-device))
(defmethod connect progn ((listening-device listening-device)
host-port &key host-address host-name port)
(declare (ignore host-port host-address host-name port))
(set-listening-thread (make-listening-thread listening-device)
listening-device))
(defmethod quit ((device listening-device))
(sb-thread:terminate-thread (listening-thread device)))
(defmethod osc-device-cleanup ((device listening-device))
(set-listening-thread nil device)
(call-next-method))
(defmethod osc-device-cleanup ((device receiving-device))
(fill (socket-buffer device) 0)
(call-next-method))
(defun print-osc-debug-msg (receiver data length address port
timetag &optional (stream t))
(format stream
"~&~a~%bytes rx:~a~a~%from:~a~a~a ~a~%timetag:~a~a~%unix-time:~a~f~%data:~a~a"
(name receiver) #\Tab length #\Tab #\Tab
address port #\Tab timetag #\Tab
(when timetag (timetag->unix-time timetag))
#\Tab #\Tab)
(format-osc-data data :stream stream)
(format stream "~%"))

224
devices/server.lisp Normal file
View file

@ -0,0 +1,224 @@
(cl:in-package #:osc)
(defun make-osc-server (&key (protocol :udp) debug-mode
(buffer-size *default-osc-buffer-size*)
cleanup-fun)
(ecase protocol
(:udp (make-instance 'osc-server-udp
:debug-mode debug-mode
:cleanup-fun cleanup-fun
:buffer-size buffer-size
:socket-buffer (make-socket-buffer buffer-size)))
(:tcp (make-instance 'osc-server-tcp
:debug-mode debug-mode
:cleanup-fun cleanup-fun
:buffer-size buffer-size))))
(defgeneric boot (osc-server port))
(defmethod boot :around ((server osc-server) port)
(if (device-active-p server)
(warn "~%Server ~A already running" (machine-instance)))
(set-socket (make-socket (protocol server)) server)
(socket-bind (socket server) #(0 0 0 0) port)
(call-next-method)
(format t "~%Server ~A listening on port ~A~%"
(machine-instance) port))
(defmethod boot ((server osc-server-udp) port)
(declare (ignore port))
"UDP server sockets are used for receiving and unconnected sending."
(set-listening-thread (make-listening-thread server) server))
(defmethod boot ((server osc-server-tcp) port)
(declare (ignore port))
(set-listening-thread
(sb-thread:make-thread
(lambda ()
(unwind-protect
(progn (socket-listen (socket server) 10)
(loop for socket = (socket-accept (socket server))
for endpoint = (make-osc-client-endpoint-tcp
socket
(debug-mode server)
(buffer-size server)
(address-tree server)
(clients server)
(make-unregister-self-fun server))
do (register-tcp-client server endpoint)))
(osc-device-cleanup server)))
:name (format nil "osc-server-tcp: ~A" (name server)))
server)
server)
(defmethod osc-device-cleanup ((device osc-server-udp))
(loop for client-name being the hash-key in (clients device)
using (hash-value addr+port)
do (notify-quit device client-name)
do (unregister-udp-client device
(first addr+port)
(second addr+port)))
(call-next-method))
(defmethod osc-device-cleanup ((device osc-server-tcp))
(loop for client being the hash-value in (clients device)
do (quit client))
(call-next-method))
(defun make-clients-hash ()
(make-hash-table :test 'equal))
;;;=====================================================================
;;; UDP server functions
;;;=====================================================================
(defmethod initialize-instance :after ((server osc-server-udp) &key)
(make-server-responders server))
(defgeneric make-server-responders (server))
(defmethod make-server-responders ((server osc-server-udp))
(add-osc-responder server "/cl-osc/register"
(cmd args device address port timetag bundle)
(let ((listening-port (car args))) ; Optional port for sending
; return messages.
(register-udp-client device address
(if listening-port listening-port port))))
(add-osc-responder server "/cl-osc/quit"
(cmd args device address port timetag bundle)
(unregister-udp-client device address port)))
(defun register-udp-client (server addr port)
(let ((client-name (make-addr+port-string addr port)))
(format t "Client registered: ~A~%" client-name)
(setf (gethash client-name (clients server))
(list addr port))
(post-register-hook server client-name)))
(defun unregister-udp-client (server addr port)
(let ((client-name (make-addr+port-string addr port)))
(format t "Client quit: ~A~%" client-name)
(remhash client-name (clients server))))
(defgeneric post-register-hook (server client-name)
(:method ((server osc-server-udp) client-name)
(format t "Post-register hook for client: ~A~%" client-name)
(notify-registered server client-name)))
(defun notify-registered (server client-name)
(send-msg-to-client server client-name "/cl-osc/server/registered"))
(defun notify-quit (server client-name)
(send-msg-to-client server client-name "/cl-osc/server/quit"))
;;;=====================================================================
;;; TCP server functions
;;;=====================================================================
(defun register-tcp-client (server transmitter)
(setf (gethash (make-peername-string transmitter)
(clients server))
transmitter))
(defun unregister-tcp-client (server transmitter)
(remhash (make-peername-string transmitter)
(clients server)))
(defun make-unregister-self-fun (server)
#'(lambda (client)
(unregister-tcp-client server client)))
(defun get-tcp-client (server socket-peername)
(gethash socket-peername (clients server)))
(defgeneric print-clients (server))
(defmethod print-clients ((server osc-server-udp))
(loop for addr+port being the hash-value in (clients server)
for i from 1
do (format t "~A. Connected to: ~A~%" i (make-addr+port-string
(first addr+port)
(second addr+port)))))
(defmethod print-clients ((server osc-server-tcp))
(loop for endpoint being the hash-value in (clients server)
for i from 1
do (format t "~A. Connected to: ~A~%" i (make-addr+port-string
(peer-address endpoint)
(peer-port endpoint)))))
;;;=====================================================================
;;; Server sending functions
;;;=====================================================================
;; Send to a client
(defgeneric send-to-client (server client-name data)
(:method :around ((server osc-server) client-name data)
(let ((client (gethash client-name (clients server))))
(if client
(call-next-method server client data)
(warn "No client called ~A~%" client-name)))))
(defmethod send-to-client ((server osc-server-udp) client-name data)
(send-to server (first client-name) (second client-name) data))
(defmethod send-to-client ((server osc-server-tcp) client data)
(send client data))
(defgeneric send-msg-to-client (server client-name command &rest args)
(:method ((server osc-server) client-name command &rest args)
(let ((message (make-message command args)))
(send-to-client server client-name message))))
(defgeneric send-bundle-to-client (server client-name timetag command
&rest args)
(:method ((server osc-server) client-name timetag command &rest
args)
(let ((bundle (bundle timetag
(make-message command args))))
(send-to-client server client-name bundle))))
;; Send all
(defgeneric send-all (server data))
(defmethod send-all ((server osc-server-udp) data)
(loop for addr+port being the hash-value in (clients server)
do (send-to server (first addr+port) (second addr+port) data)))
(defmethod send-all ((server osc-server-tcp) data)
(loop for endpoint being the hash-value in (clients server)
do (send endpoint data)))
(defmethod send-all ((client-endpoint osc-client-endpoint) data)
(loop for endpoint being the hash-value in (clients client-endpoint)
;; FIXME: Don't not reply to the sender in this case so that the
;; behaviour of send-all is uniform for both UDP and TCP. But
;; could be useful to have a means of broadcasting messages to
;; all clients of a server except the client that generated the
;; message.
;;
;; unless (eq endpoint client-endpoint) ; don't send to sender
do (send endpoint data)))
(defgeneric send-msg-all (server command &rest args)
(:method ((server osc-server) command &rest args)
(let ((message (make-message command args)))
(send-all server message)))
(:method ((client-endpoint osc-client-endpoint) command &rest args)
(let ((message (make-message command args)))
(send-all client-endpoint message))))
(defgeneric send-bundle-all (server timetag command &rest args)
(:method ((server osc-server) timetag command &rest args)
(let ((bundle (bundle timetag
(make-message command args))))
(send-all server bundle)))
(:method ((client-endpoint osc-client-endpoint) timetag command
&rest args)
(let ((bundle (bundle timetag
(make-message command args))))
(send-all client-endpoint bundle))))

View file

@ -0,0 +1,84 @@
(cl:in-package #:osc)
(defparameter *default-osc-buffer-size* 1024)
(defun make-socket-buffer (&optional (size *default-osc-buffer-size*))
(make-sequence '(vector (unsigned-byte 8)) size))
(defun make-socket (protocol)
(ecase protocol
(:udp (make-udp-socket))
(:tcp (make-tcp-socket))))
(defun make-tcp-socket ()
(make-instance 'inet-socket :type :stream :protocol :tcp))
(defun make-udp-socket ()
(make-instance 'inet-socket :type :datagram :protocol :udp))
(defun make-peername-string (osc-device)
(when (socket osc-device)
(multiple-value-bind (addr port)
(socket-peername (socket osc-device))
(make-addr+port-string addr port))))
(defun make-name-string (osc-device)
(when (socket osc-device)
(multiple-value-bind (addr port)
(socket-name (socket osc-device))
(make-addr+port-string addr port))))
(defun make-addr+port-string (addr port)
(format nil "~{~A~^.~}:~A" (coerce addr 'list) port))
(defun device-active-p (osc-device)
(when (socket osc-device)
(socket-open-p (socket osc-device))))
(defun device-socket-name (osc-device)
(socket-name (socket osc-device)))
(defun port (osc-device)
(if (device-active-p osc-device)
(multiple-value-bind (addr port)
(device-socket-name osc-device)
(declare (ignore addr))
port)
(warn "Device not active.")))
(defun address (osc-device)
(if (device-active-p osc-device)
(multiple-value-bind (addr port)
(device-socket-name osc-device)
(declare (ignore port))
addr)
(warn "Device not active.")))
(defun device-socket-peername (osc-device)
(socket-peername (socket osc-device)))
(defun peer-port (osc-device)
(if (device-active-p osc-device)
(handler-case
(multiple-value-bind (addr port)
(device-socket-peername osc-device)
(declare (ignore addr))
port)
(sb-bsd-sockets:not-connected-error ()
(warn "Device ~a not connected: device removed."
(device-socket-name osc-device))
(osc-device-cleanup osc-device)))
(warn "Device not active.")))
(defun peer-address (osc-device)
(if (device-active-p osc-device)
(handler-case
(multiple-value-bind (addr port)
(device-socket-peername osc-device)
(declare (ignore port))
addr)
(sb-bsd-sockets:not-connected-error ()
(warn "Device ~a not connected: device removed."
(device-socket-name osc-device))
(osc-device-cleanup osc-device)))
(warn "Device not active.")))

99
devices/transmitter.lisp Normal file
View file

@ -0,0 +1,99 @@
(cl:in-package #:osc)
;; Only UDP devices can be transmitters.
(defun make-osc-transmitter (&key debug-mode cleanup-fun)
(make-instance 'osc-transmitter-udp
:debug-mode debug-mode
:cleanup-fun cleanup-fun))
(defgeneric connect (osc-transmitter host-port &key host-address
host-name port)
(:method-combination progn :most-specific-last))
(defmethod connect progn ((transmitter osc-transmitter) host-port
&key (host-address nil addr)
(host-name "localhost" name) port)
(when (and addr name)
(error "Supplied both :host-address and :host-name to connect"))
(cond (addr
(when (typep host-address 'string)
(setf host-address
(sb-bsd-sockets:make-inet-address host-address))))
(t
(setf host-address
(sb-bsd-sockets:host-ent-address
(sb-bsd-sockets:get-host-by-name
host-name)))))
(if (not (device-active-p transmitter))
(progn
(let ((socket (make-socket (protocol transmitter))))
(if port
(socket-bind socket #(127 0 0 1) port)
(socket-bind socket))
(socket-connect socket host-address host-port)
(socket-make-stream socket
:input nil :output t
:element-type '(unsigned-byte 8)
:buffering :full)
(set-socket socket transmitter))
(when (debug-mode transmitter)
(format t "~%Device connected: ~A~%~A -> ~A~%"
(name transmitter) #\Tab
(make-addr+port-string (peer-address transmitter)
(peer-port transmitter)))))
(warn "Already connected"))
transmitter)
(defmethod quit ((transmitter osc-transmitter-udp))
(if (device-active-p transmitter)
(osc-device-cleanup transmitter)
(warn "Not connected: ~A" (name transmitter))))
;;;=====================================================================
;;; Sending functions
;;;=====================================================================
(defmacro osc-write-to-stream (stream &body msg)
`(progn (write-sequence ,@msg ,stream)
(finish-output ,stream)))
(defgeneric send (transmitter data)
(:method ((transmitter osc-transmitter) data)
(let ((bytes (encode-osc-data data)))
(osc-write-to-stream
(slot-value (socket transmitter) 'stream) bytes))))
(defgeneric send-msg (transmitter command &rest args)
(:method ((transmitter osc-transmitter) command &rest args)
(let ((message (make-message command args)))
(send transmitter message))))
(defgeneric send-bundle (transmitter timetag command &rest args)
(:method ((transmitter osc-transmitter) timetag command &rest args)
(let ((bundle (bundle timetag
(make-message command args))))
(send transmitter bundle))))
;; Unconnected sending (UDP only)
(defgeneric send-to (transmitter address port data)
(:method ((transmitter osc-transmitter-udp) address port data)
(socket-send (socket transmitter)
(encode-osc-data data) nil
:address (list address port))))
(defgeneric send-msg-to (transmitter address port command &rest args)
(:method ((transmitter osc-transmitter-udp) address port command
&rest args)
(let ((message (make-message command args)))
(send-to transmitter address port message))))
(defgeneric send-bundle-to (transmitter address port timetag command
&rest args)
(:method ((transmitter osc-transmitter-udp) address port timetag
command &rest args)
(let ((bundle (bundle timetag
(make-message command args))))
(send-to transmitter address port bundle))))

66
osc-data.lisp Normal file
View file

@ -0,0 +1,66 @@
(cl:in-package #:osc)
;; Classes
(defclass osc-data () ())
(defclass message (osc-data)
((command
:reader command
:initarg :command)
(args
:reader args
:initarg :args
:initform nil)))
(defclass bundle (osc-data)
((timetag
:reader timetag
:initarg :timetag
:initform :now)
(elements
:reader elements
:initarg :elements
:initform nil)))
;; Constructors
(defun make-message (command args)
(unless (listp args)
(setf args (list args)))
(make-instance 'message
:command command
:args args))
(defun message (command &rest args)
(make-message command args))
(defun make-bundle (timetag elements)
(unless (listp elements)
(setf elements (list elements)))
(make-instance 'bundle
:timetag timetag
:elements elements))
(defun bundle (timetag &rest elements)
(make-bundle timetag elements))
(defgeneric format-osc-data (data &key stream width))
(defmethod format-osc-data ((message message) &key (stream t)
(width 80))
(let ((args-string (format nil "~{~a~^ ~}" (args message))))
(when (> (length args-string) width)
(setf args-string
(concatenate 'string
(subseq args-string 0 width)
"...")))
(format stream "~a ~a~%"
(command message)
args-string)))
(defmethod format-osc-data ((bundle bundle) &key (stream t) (width 80))
(format stream "~&[ ~a~%" (timetag bundle))
(dolist (element (elements bundle))
(format-osc-data element :stream stream :width width))
(format stream "~&]~%"))

View file

@ -47,26 +47,41 @@
;;;; ; ; ; ;; ;;;; ; ; ; ;;
(defun dp-register (tree address function) (defun dp-register (tree address function)
"registers a function to respond to incoming osc message. since "Registers a function to respond to incoming osc messages. Since
only one function should be associated with an address, any only one function should be associated with an address, any
previous registration will be overwritten" previous registration will be overwritten."
(setf (gethash address tree) (setf (gethash address tree)
function)) function))
(defun dp-remove (tree address) (defun dp-remove (tree address)
"removes the function associated with the given address.." "Removes the function associated with the given address."
(remhash address tree)) (remhash address tree))
(defun dp-match (tree pattern) (defun dp-match (tree pattern)
"returns a list of functions which are registered for "Returns a list of functions which are registered for dispatch for a
dispatch for a given address pattern.." given address pattern."
(list (gethash pattern tree))) (list (gethash pattern tree)))
(defun dispatch (tree osc-message) (defgeneric dispatch (tree data device address port &optional timetag
"calls the function(s) matching the address(pattern) in the osc parent-bundle))
message with the data contained in the message"
(let ((pattern (car osc-message))) (defmethod dispatch (tree (data message) device address port &optional
timetag
parent-bundle)
"Calls the function(s) matching the address(pattern) in the osc
message passing the message object, the recieving device, and
optionally in the case where a message is part of a bundle, the
timetag of the bundle and the enclosing bundle."
(let ((pattern (command data)))
(dolist (x (dp-match tree pattern)) (dolist (x (dp-match tree pattern))
(unless (eq x NIL) (unless (eq x NIL)
(apply #'x (cdr osc-message)))))) (funcall x (command data) (args data) device address port
timetag parent-bundle)))))
(defmethod dispatch (tree (data bundle) device address port &optional
timetag
parent-bundle)
"Dispatches each bundle element in sequence."
(declare (ignore timetag parent-bundle))
(dolist (element (elements data))
(dispatch tree element device address port (timetag data) data)))

View file

@ -5,7 +5,7 @@
;; Copyright (C) 2004 FoAM vzw ;; Copyright (C) 2004 FoAM vzw
;; ;;
;; Authors ;; Authors
;; - nik gaffney <nik@f0.am> ;; - nik gaffney <nik@fo.am>
;; ;;
;;;;; ;; ; ; ;; ; ; ;;;;; ;; ; ; ;; ; ;
@ -14,18 +14,19 @@
;; Commentry ;; Commentry
;; ;;
;; These examples are currently sbcl specific, but should be easily ported to ;; These examples are currently sbcl specific, but should be easily ported to
;; work with trivial-sockets, acl-compat or something similar. They should be ;; work with trivial-sockets, acl-compat or something similar.
;; able to explain enough to get you started. .. ;; They should be enough to get you started.
;; ;;
;; eg. listen on port 6667 for incoming msgs ;; eg. listen on port 6667 for incoming messages
;; ;;
;; (osc-receive-test 6667) ;; (osc-receive-test 6667)
;; eg. send a test message to localhost port 6668 ;;
;; send a test message to localhost port 6668
;; ;;
;; (osc-send-test #(127 0 0 1) 6668) ;; (osc-send-test #(127 0 0 1) 6668)
;; ;;
;; eg. listen on port 6667 and send to 10.0.89:6668 ;; listen on port 6667 and send to 10.0.89:6668
;; note the ip# is formatted as a vector ;; (note the ip# is formatted as a vector)
;; ;;
;; (osc-reflector-test 6667 #(10 0 0 89) 6668) ;; (osc-reflector-test 6667 #(10 0 0 89) 6668)
;; ;;
@ -41,52 +42,53 @@
"a basic test function which attempts to decode an osc message on given port. "a basic test function which attempts to decode an osc message on given port.
note ip#s need to be in the format #(127 0 0 1) for now.. ." note ip#s need to be in the format #(127 0 0 1) for now.. ."
(let ((s (socket-connect nil nil (let ((s (socket-connect nil nil
:local-port port :local-port port
:local-host #(127 0 0 1) :local-host #(127 0 0 1)
:protocol :datagram :protocol :datagram
:element-type '(unsigned-byte 8))) :element-type '(unsigned-byte 8)))
(buffer (make-sequence '(vector (unsigned-byte 8)) 1024))) (buffer (make-sequence '(vector (unsigned-byte 8)) 1024)))
(format t "listening on localhost port ~A~%~%" port) (format t "listening on localhost port ~A~%~%" port)
(unwind-protect (unwind-protect
(loop do (loop do
(socket-receive s buffer (length buffer)) (socket-receive s buffer (length buffer))
(format t "received -=> ~S~%" (osc:decode-bundle buffer))) (format t "received -=> ~S~%" (osc:decode-bundle buffer)))
(when s (socket-close s))))) (when s (socket-close s)))))
(defun osc-send-test (host port) (defun osc-send-test (host port)
"a basic test function which sends osc test message to a given port/hostname. "a basic test function which sends osc test message to a given port/hostname.
note ip#s need to be in the format #(127 0 0 1) for now.. ." note ip#s need to be in the format #(127 0 0 1) for now.. ."
(let ((s (socket-connect host port (let ((s (socket-connect host port
:protocol :datagram :protocol :datagram
:element-type '(unsigned-byte 8))) :element-type '(unsigned-byte 8)))
(b (osc:encode-message "/foo/bar" "baz" 1 2 3 (coerce PI 'single-float)))) (b (osc:encode-message "/foo/bar" "baz" 1 2 3 (coerce PI 'single-float))))
(format t "sending to ~a on port ~A~%~%" host port) (format t "sending to ~a on port ~A~%~%" host port)
(unwind-protect (unwind-protect
(socket-send s b (length b)) (socket-send s b (length b))
(when s (socket-close s))))) (when s (socket-close s)))))
(defun osc-reflector-test (listen-port send-host send-port) (defun osc-reflector-test (listen-port send-host send-port)
"reflector.. . listens on a given port and sends out on another "reflector. listens on a given port and sends out on another.
note ip#s need to be in the format #(127 0 0 1) for now.. ." note ip#s need to be in the format #(127 0 0 1) for now.. ."
(let ((in (socket-connect nil nil (let ((in (socket-connect nil nil
:local-port listen-port :local-port listen-port
:local-host #(127 0 0 1) :protocol :datagram
:protocol :datagram :element-type '(unsigned-byte 8)))
:element-type '(unsigned-byte 8))) (out (socket-connect send-host send-port
(out (socket-connect send-host send-port :protocol :datagram
:protocol :datagram :element-type '(unsigned-byte 8)))
:element-type '(unsigned-byte 8))) (buffer (make-sequence '(vector (unsigned-byte 8)) 1024)))
(buffer (make-sequence '(vector (unsigned-byte 8)) 1024)))
(unwind-protect (unwind-protect
(loop do (loop do
(socket-receive in buffer (length buffer)) (socket-receive in buffer (length buffer))
(format t "glonked -=> message: ~{~A, ~}~%" (format t "glonked -=> message: ~{~A, ~}~%"
(osc:decode-bundle buffer)) (osc:decode-bundle buffer))
(let ((mess (apply #'osc:encode-message (let ((mess (apply #'osc:encode-message
(cons "/echo" (cons "/echo"
(osc:decode-message buffer))))) (osc:decode-message buffer)))))
(socket-send out mess (length mess)))) (socket-send out mess (length mess))))
(when in (socket-close in)) (when in (socket-close in))
(when out (socket-close out))))) (when out (socket-close out)))))
;end ;;end

77
osc-time.lisp Normal file
View file

@ -0,0 +1,77 @@
(in-package #:osc)
(defconstant +unix-epoch+ (encode-universal-time 0 0 0 1 1 1970 0))
(defconstant +2^32+ (expt 2 32))
(defconstant +2^32/million+ (/ +2^32+ (expt 10 6)))
(defconstant +usecs+ (expt 10 6))
(deftype timetag () '(unsigned-byte 64))
(defun timetagp (object)
(typep object 'timetag))
(defun unix-secs+usecs->timetag (secs usecs)
(let ((sec-offset (+ secs +unix-epoch+))) ; Seconds from 1900.
(setf sec-offset (ash sec-offset 32)) ; Make seconds the top 32
; bits.
(let ((usec-offset
(round (* usecs +2^32/MILLION+)))) ; Fractional part.
(the timetag (+ sec-offset usec-offset)))))
(defun get-current-timetag ()
"Returns a fixed-point 64 bit NTP-style timetag, where the top 32
bits represent seconds since midnight 19000101, and the bottom 32 bits
represent the fractional parts of a second."
(multiple-value-bind (secs usecs)
(sb-ext:get-time-of-day)
(the timetag (unix-secs+usecs->timetag secs usecs))))
(defun timetag+ (original seconds-offset)
(declare (type timetag original))
(let ((offset (round (* seconds-offset +2^32+))))
(the timetag (+ original offset))))
;;;=====================================================================
;;; Functions for using double-float unix timestamps.
;;;=====================================================================
(defun get-unix-time ()
"Returns a a double-float representing real-time now in seconds,
with microsecond precision, relative to 19700101."
(multiple-value-bind (secs usecs)
(sb-ext:get-time-of-day)
(the double-float (+ secs (microseconds->subsecs usecs)))))
(defun unix-time->timetag (unix-time)
(multiple-value-bind (secs subsecs)
(floor unix-time)
(the timetag
(unix-secs+usecs->timetag secs
(subsecs->microseconds subsecs)))))
(defun timetag->unix-time (timetag)
(if (= timetag 1)
1 ; immediate timetag
(let* ((secs (ash timetag -32))
(subsec-int32 (- timetag (ash secs 32))))
(the double-float (+ (- secs +unix-epoch+)
(int32->subsecs subsec-int32))))))
(defun microseconds->subsecs (usecs)
(declare (type (integer 0 1000000) usecs))
(coerce (/ usecs +usecs+) 'double-float))
(defun subsecs->microseconds (subsecs)
(declare (type (float 0 1) subsecs))
(round (* subsecs +usecs+)))
(defun int32->subsecs (int32)
"This maps a 32 bit integer, representing subsecond time, to a
double float in the range 0-1."
(declare (type (unsigned-byte 32) int32))
(coerce (/ int32 +2^32+) 'double-float))
(defun print-as-double (time)
(format t "~%~F" (coerce time 'double-float))
time)

28
osc.asd
View file

@ -1,11 +1,31 @@
;; -*- mode: lisp -*- ;; -*- mode: lisp -*-
(in-package #:asdf) (in-package #:common-lisp-user)
(defsystem osc (asdf:defsystem osc
:name "osc" :name "osc"
:author "nik gaffney <nik@fo.am>" :author "nik gaffney <nik@fo.am>"
:licence "LLGPL" :licence "LLGPL"
:description "The Open Sound Control protocol, aka OSC" :description "The Open Sound Control protocol, aka OSC"
:version "0.5" :version "0.6"
:components ((:file "osc"))) :components
((:file "osc" :depends-on ("osc-data" "osc-time"))
(:file "osc-data" :depends-on ("package"))
(:file "osc-dispatch" :depends-on ("osc"))
(:file "osc-time" :depends-on ("package"))
(:file "package")
(:module "devices"
:depends-on ("package" "osc-data")
::components
((:file "socket-functions")
(:file "device")
(:file "transmitter"
:depends-on ("device"
"socket-functions"))
(:file "listening-device"
:depends-on ("transmitter"))
(:file "dispatching-device"
:depends-on ("listening-device"))
(:file "client"
:depends-on ("dispatching-device"))
(:file "server" :depends-on ("client"))))))

359
osc.lisp
View file

@ -40,17 +40,9 @@
;;; an error ;;; an error
;;; ;;;
(defpackage :osc
(:use :cl)
(:documentation "OSC aka the 'open sound control' protocol")
(:export :encode-message
:encode-bundle
:decode-message
:decode-bundle))
(in-package :osc) (in-package :osc)
;(declaim (optimize (speed 3) (safety 1) (debug 3))) ;;(declaim (optimize (speed 3) (safety 1) (debug 3)))
;;;;;; ; ;; ; ; ; ; ; ; ; ;;;;;; ; ;; ; ; ; ; ; ; ;
;; ;;
@ -58,28 +50,41 @@
;; ;;
;;;; ;; ;; ; ; ;; ; ; ; ; ;;;; ;; ;; ; ; ;; ; ; ; ;
(defun encode-bundle (data &optional timetag) (defparameter *debug* 0
"will encode an osc message, or list of messages as a bundle "Set debug verbosity for core library functions. Currently levels
with an optional timetag (symbol or 64bit int). are 0-3.")
doesnt handle nested bundles"
(cat '(35 98 117 110 100 108 101 0) ; #bundle
(if timetag
(encode-timetag timetag)
(encode-timetag :now))
(if (listp (car data))
(apply #'cat (mapcar #'encode-bundle-elt data))
(encode-bundle-elt data))))
(defun encode-bundle-elt (data) (defgeneric encode-osc-data (data))
(let ((message (apply #'encode-message data)))
(cat (encode-int32 (length message)) message)))
(defun encode-message (address &rest data) (defmethod encode-osc-data ((data message))
"encodes an osc message with the given address and data." "Encode an osc message with the given address and args."
(concatenate '(vector (unsigned-byte 8)) (with-slots (command args) data
(encode-address address) (concatenate '(vector (unsigned-byte 8))
(encode-typetags data) (encode-address command)
(encode-data data))) (encode-typetags args)
(encode-args args))))
(defmethod encode-osc-data ((data bundle))
"Encode an osc bundle. A bundle contains a timetag (symbol or 64bit
int) and a list of message or nested bundle elements."
(with-slots (timetag elements) data
(cat '(35 98 117 110 100 108 101 0) ; #bundle
(if timetag
(encode-timetag timetag)
(encode-timetag :now))
(apply #'cat (mapcar #'encode-bundle-elt elements)))))
(defgeneric encode-bundle-elt (data))
(defmethod encode-bundle-elt ((data message))
(let ((bytes (encode-osc-data data)))
(cat (encode-int32 (length bytes)) bytes)))
(defmethod encode-bundle-elt ((data bundle))
(let ((bytes (encode-osc-data data)))
(cat (encode-int32 (length bytes)) bytes)))
;; Auxilary functions
(defun encode-address (address) (defun encode-address (address)
(cat (map 'vector #'char-code address) (cat (map 'vector #'char-code address)
@ -100,7 +105,7 @@
and considers non int/float/string data to be a blob." and considers non int/float/string data to be a blob."
(let ((lump (make-array 0 :adjustable t (let ((lump (make-array 0 :adjustable t
:fill-pointer t))) :fill-pointer t)))
(macrolet ((write-to-vector (char) (macrolet ((write-to-vector (char)
`(vector-push-extend `(vector-push-extend
(char-code ,char) lump))) (char-code ,char) lump)))
@ -110,21 +115,22 @@
(integer (if (>= x 4294967296) (write-to-vector #\h) (write-to-vector #\i))) (integer (if (>= x 4294967296) (write-to-vector #\h) (write-to-vector #\i)))
(float (write-to-vector #\f)) (float (write-to-vector #\f))
(simple-string (write-to-vector #\s)) (simple-string (write-to-vector #\s))
(t (write-to-vector #\b))))) (keyword (write-to-vector #\s))
(t (write-to-vector #\b)))))
(cat lump (cat lump
(pad (padding-length (length lump)))))) (pad (padding-length (length lump))))))
(defun encode-data (data) (defun encode-args (args)
"encodes data in a format suitable for an OSC message" "encodes args in a format suitable for an OSC message"
(let ((lump (make-array 0 :adjustable t :fill-pointer t))) (let ((lump (make-array 0 :adjustable t :fill-pointer t)))
(macrolet ((enc (f) (macrolet ((enc (f)
`(setf lump (cat lump (,f x))))) `(setf lump (cat lump (,f x)))))
(dolist (x data) (dolist (x args)
(typecase x (typecase x
(integer (if (>= x 4294967296) (enc encode-int64) (enc encode-int32))) (integer (if (>= x 4294967296) (enc encode-int64) (enc encode-int32)))
(float (enc encode-float32)) (float (enc encode-float32))
(simple-string (enc encode-string)) (simple-string (enc encode-string))
(t (enc encode-blob)))) (t (enc encode-blob))))
lump))) lump)))
@ -134,40 +140,98 @@
;; ;;
;;; ;; ;; ; ; ; ; ; ; ;;; ;; ;; ; ; ; ; ; ;
(defun decode-bundle (data) (defun bundle-p (buffer &optional (start 0))
"decodes an osc bundle into a list of decoded-messages, which has "A bundle begins with '#bundle' (8 bytes). The start argument should
an osc-timetagas its first element" index the beginning of a bundle in the buffer."
(let ((contents '())) (= 35 (elt buffer start)))
(if (equalp 35 (elt data 0)) ; a bundle begins with '#'
(let ((timetag (subseq data 8 16)) (defun get-timetag (buffer &optional (start 0))
(i 16) "Bytes 8-15 are the bundle timestamp. The start argument should
(bundle-length (length data))) index the beginning of a bundle in the buffer."
(loop while (< i bundle-length) (decode-timetag (subseq buffer
do (let ((mark (+ i 4)) (+ 8 start)
(size (decode-int32 (+ 16 start))))
(subseq data i (+ i 4)))))
(if (eq size 0) (defun get-bundle-element-length (buffer &optional (start 16))
(setf bundle-length 0) "Bytes 16-19 are the size of the bundle element. The start argument
(push (decode-bundle should index the beginning of the bundle element (length, content)
(subseq data mark (+ mark size))) pair in the buffer."
contents)) (decode-int32 (subseq buffer start (+ 4 start))))
(incf i (+ 4 size))))
(push timetag contents)) (defun get-bundle-element (buffer &optional (start 16))
(decode-message data)))) "Bytes 20 upto to the length of the content (defined by the
preceding 4 bytes) are the content of the bundle. The start argument
should index the beginning of the bundle element (length, content)
pair in the buffer."
(let ((length (get-bundle-element-length buffer start)))
(subseq buffer
(+ 4 start)
(+ (+ 4 start)
(+ length)))))
(defun split-sequence-by-n (sequence n)
(loop :with length := (length sequence)
:for start :from 0 :by n :below length
:collecting (coerce
(subseq sequence start (min length (+ start n)))
'list)))
(defun print-buffer (buffer &optional (n 8))
(format t "~%~{~{ ~5d~}~%~}Total: ~a bytes~2%"
(split-sequence-by-n buffer n)
(length buffer)))
(defun decode-bundle (buffer &key (start 0) end)
"Decodes an osc bundle/message into a bundle/message object. Bundles
comprise an osc-timetag and a list of elements, which may be
messages or bundles recursively. An optional end argument can be
supplied (i.e. the length value returned by socket-receive, or the
element length in the case of nested bundles), otherwise the entire
buffer is decoded - in which case, if you are reusing buffers, you
are responsible for ensuring that the buffer does not contain stale
data."
(unless end
(setf end (- (length buffer) start)))
(when (>= *debug* 2)
(format t "~%Buffer start: ~a end: ~a~%" start end)
(print-buffer (subseq buffer start end)))
(if (bundle-p buffer start)
;; Bundle
(let ((timetag (get-timetag buffer start)))
(incf start (+ 8 8)) ; #bundle, timetag bytes
(loop while (< start end)
for element-length = (get-bundle-element-length
buffer start)
do (incf start 4) ; length bytes
when (>= *debug* 1)
do (format t "~&Bundle element length: ~a~%" element-length)
collect (decode-bundle buffer
:start start
:end (+ start element-length))
into elements
do (incf start (+ element-length))
finally (return
(values (make-bundle timetag elements)
timetag))))
;; Message
(let ((message
(decode-message
(subseq buffer start (+ start end)))))
(make-message (car message) (cdr message)))))
(defun decode-message (message) (defun decode-message (message)
"reduces an osc message to an (address . data) pair. .." "reduces an osc message to an (address . data) pair. .."
(declare (type (vector *) message)) (declare (type (vector *) message))
(let ((x (position (char-code #\,) message))) (let ((x (position (char-code #\,) message)))
(if (eq x NIL) (if (eq x nil)
(format t "message contains no data.. ") (format t "message contains no data.. ")
(cons (decode-address (subseq message 0 x)) (cons (decode-address (subseq message 0 x))
(decode-taged-data (subseq message x)))))) (decode-taged-data (subseq message x))))))
(defun decode-address (address) (defun decode-address (address)
(coerce (map 'vector #'code-char (coerce (map 'vector #'code-char
(delete 0 address)) (delete 0 address))
'string)) 'string))
(defun decode-taged-data (data) (defun decode-taged-data (data)
"decodes data encoded with typetags... "decodes data encoded with typetags...
@ -185,32 +249,34 @@
(map 'vector (map 'vector
#'(lambda (x) #'(lambda (x)
(cond (cond
((eq x (char-code #\i)) ((eq x (char-code #\i))
(push (decode-int32 (subseq acc 0 4)) (push (decode-int32 (subseq acc 0 4))
result) result)
(setf acc (subseq acc 4))) (setf acc (subseq acc 4)))
((eq x (char-code #\h)) ((eq x (char-code #\h))
(push (decode-uint64 (subseq acc 0 8)) (push (decode-uint64 (subseq acc 0 8))
result) result)
(setf acc (subseq acc 8))) (setf acc (subseq acc 8)))
((eq x (char-code #\f)) ((eq x (char-code #\f))
(push (decode-float32 (subseq acc 0 4)) (push (decode-float32 (subseq acc 0 4))
result) result)
(setf acc (subseq acc 4))) (setf acc (subseq acc 4)))
((eq x (char-code #\s)) ((eq x (char-code #\s))
(let ((pointer (padded-length (position 0 acc)))) (let ((pointer (padded-length (position 0 acc))))
(push (decode-string (push (decode-string
(subseq acc 0 pointer)) (subseq acc 0 pointer))
result) result)
(setf acc (subseq acc pointer)))) (setf acc (subseq acc pointer))))
((eq x (char-code #\b)) ((eq x (char-code #\b))
(let* ((size (decode-int32 (subseq acc 0 4))) (let* ((size (decode-int32 (subseq acc 0 4)))
(bl (+ 4 size)) (bl (+ 4 size))
(end (+ bl (mod (- 4 bl) 4)))) ; NOTE: cannot use (padded-length bl), as it is not the same algorithm. Blobs of 4, 8, 12 etc bytes should not be padded! (end (+ bl (mod (- 4 bl) 4))))
(push (decode-blob (subseq acc 0 end)) ;; NOTE: cannot use (padded-length bl), as it is not the same algorithm.
result) ;; Blobs of 4, 8, 12 etc bytes should not be padded!
(setf acc (subseq acc end)))) (push (decode-blob (subseq acc 0 end))
(t (error "unrecognised typetag ~a" x)))) result)
(setf acc (subseq acc end))))
(t (error "unrecognised typetag ~a" x))))
tags) tags)
(nreverse result)))) (nreverse result))))
@ -219,50 +285,45 @@
;; ;;
;; timetags ;; timetags
;; ;;
;; - timetags can be encoded using a value, or the :now and :time keywords. the ;; - timetags can be encoded using a value, or the :now and :time
;; keywords enable either a tag indicating 'immediate' execution, or ;; keywords. the keywords enable either a tag indicating 'immediate'
;; a tag containing the current time (which will most likely be in the past ;; execution, or a tag containing the current time (which will most
;; of anyt receiver) to be created. ;; likely be in the past of any receiver) to be created.
;;
;; - note: not well tested, and probably not accurate enough for syncronisation.
;; see also: CLHS 25.1.4 Time, and the ntp timestamp format. also needs to
;; convert from 2 32bit ints to 64bit fixed point value.
;; ;;
;; - see this c.l.l thread to sync universal-time and internal-time ;; - see this c.l.l thread to sync universal-time and internal-time
;; http://groups.google.com/group/comp.lang.lisp/browse_thread/thread/c207fef63a78d720/adc7442d2e4de5a0?lnk=gst&q=internal-real-time-sync&rnum=1#adc7442d2e4de5a0 ;; http://groups.google.com/group/comp.lang.lisp/browse_thread/thread/c207fef63a78d720/adc7442d2e4de5a0?lnk=gst&q=internal-real-time-sync&rnum=1#adc7442d2e4de5a0
;; - In SBCL, using sb-ext:get-time-of-day to get accurate seconds and
;; microseconds from OS.
;; ;;
;;;; ;; ; ; ;;;; ;; ; ;
(defconstant +unix-epoch+ (encode-universal-time 0 0 0 1 1 1970 0)) (defun encode-timetag (timetag)
"From the spec: `Time tags are represented by a 64 bit fixed point
(defun encode-timetag (utime &optional subseconds) number. The first 32 bits specify the number of seconds since midnight
"encodes an osc timetag from a universal-time and 32bit 'sub-second' part. on January 1, 1900, and the last 32 bits specify fractional parts of a
for an 'instantaneous' timetag use (encode-timetag :now) second to a precision of about 200 picoseconds. This is the
for a timetag with the current time use (encode-timetag :time)" representation used by Internet NTP timestamps'. For an
'instantaneous' timetag use (encode-timetag :now), and for a timetag
with the current time use (encode-timetag :time)."
(cond (cond
;; a 1bit timetag will be interpreted as 'imediately' ((equalp timetag :now)
((equalp utime :now) ;; a 1 bit timetag will be interpreted as 'immediately'
#(0 0 0 0 0 0 0 1)) #(0 0 0 0 0 0 0 1))
;; converts seconds since 19000101 to seconds since 19700101 ((equalp timetag :time)
;; note: fractions of a second is accurate, but not syncronised. ;; encode timetag with current real time
((equalp utime :time) (encode-int64 (get-current-timetag)))
(cat (encode-int32 (- (get-universal-time) +unix-epoch+)) ((timetagp timetag)
(encode-int32 ;; encode osc timetag
(round (* internal-time-units-per-second (encode-int64 timetag))
(second (multiple-value-list (t (error "Argument given is not one of :now, :time, or timetagp."))))
(floor (/ (get-internal-real-time)
internal-time-units-per-second)))))))))
((integerp utime)
(cat (encode-int32 (+ utime +unix-epoch+))
(encode-int32 subseconds)))
(t (error "the time or subsecond given is not an integer"))))
(defun decode-timetag (timetag) (defun decode-timetag (timetag)
"decomposes a timetag into unix-time and a subsecond,. . ." "Return a 64 bit timetag from a vector of 8 bytes in network byte
(list order."
(decode-int32 (subseq timetag 0 4)) (if (equalp timetag #(0 0 0 0 0 0 0 1))
(decode-int32 (subseq timetag 4 8)))) 1 ; A timetag of 1 is defined as immediately.
(decode-uint64 timetag)))
;;;;; ; ; ;; ;; ; ; ;;;;; ; ; ;; ;; ; ;
;; ;;
@ -274,12 +335,12 @@
;; particulaly portable, but 'works for now'. ;; particulaly portable, but 'works for now'.
(defun encode-float32 (f) (defun encode-float32 (f)
"encode an ieee754 float as a 4 byte vector. currently sbcl/cmucl specifc" "encode an ieee754 float as a 4 byte vector. currently sbcl/cmucl specific"
#+sbcl (encode-int32 (sb-kernel:single-float-bits f)) #+sbcl (encode-int32 (sb-kernel:single-float-bits f))
#+cmucl (encode-int32 (kernel:single-float-bits f)) #+cmucl (encode-int32 (kernel:single-float-bits f))
#+openmcl (encode-int32 (CCL::SINGLE-FLOAT-BITS f)) #+openmcl (encode-int32 (CCL::SINGLE-FLOAT-BITS f))
#+allegro (encode-int32 (multiple-value-bind (x y) (excl:single-float-to-shorts f) #+allegro (encode-int32 (multiple-value-bind (x y) (excl:single-float-to-shorts f)
(+ (ash x 16) y))) (+ (ash x 16) y)))
#-(or sbcl cmucl openmcl allegro) (error "cant encode floats using this implementation")) #-(or sbcl cmucl openmcl allegro) (error "cant encode floats using this implementation"))
(defun decode-float32 (s) (defun decode-float32 (s)
@ -288,7 +349,7 @@
#+cmucl (kernel:make-single-float (decode-int32 s)) #+cmucl (kernel:make-single-float (decode-int32 s))
#+openmcl (CCL::HOST-SINGLE-FLOAT-FROM-UNSIGNED-BYTE-32 (decode-uint32 s)) #+openmcl (CCL::HOST-SINGLE-FLOAT-FROM-UNSIGNED-BYTE-32 (decode-uint32 s))
#+allegro (excl:shorts-to-single-float (ldb (byte 16 16) (decode-int32 s)) #+allegro (excl:shorts-to-single-float (ldb (byte 16 16) (decode-int32 s))
(ldb (byte 16 0) (decode-int32 s))) (ldb (byte 16 0) (decode-int32 s)))
#-(or sbcl cmucl openmcl allegro) (error "cant decode floats using this implementation")) #-(or sbcl cmucl openmcl allegro) (error "cant decode floats using this implementation"))
(defmacro defint-decoder (num-of-octets &optional docstring) (defmacro defint-decoder (num-of-octets &optional docstring)
@ -305,8 +366,13 @@
,int)))) ,int))))
,int)))) ,int))))
(defint-decoder 4 "4 byte -> 32 bit unsigned int") (defun decode-uint32 (s)
(defint-decoder 8 "8 byte -> 64 bit unsigned int") "4 byte -> 32 bit unsigned int"
(let ((i (+ (ash (elt s 0) 24)
(ash (elt s 1) 16)
(ash (elt s 2) 8)
(elt s 3))))
i))
(defmacro defint-encoder (num-of-octets &optional docstring) (defmacro defint-encoder (num-of-octets &optional docstring)
(let ((enc-name (intern (format nil "~:@(encode-int~)~D" (* 8 num-of-octets)))) (let ((enc-name (intern (format nil "~:@(encode-int~)~D" (* 8 num-of-octets))))
@ -317,59 +383,61 @@
(list docstring)) (list docstring))
(let ((,buf (make-array ,num-of-octets :element-type '(unsigned-byte 8)))) (let ((,buf (make-array ,num-of-octets :element-type '(unsigned-byte 8))))
,@(loop ,@(loop
for n below num-of-octets for n below num-of-octets
collect `(setf (aref ,buf ,n) collect `(setf (aref ,buf ,n)
(ldb (byte 8 (* 8 (- (1- ,num-of-octets) ,n))) (ldb (byte 8 (* 8 (- (1- ,num-of-octets) ,n)))
,int))) ,int)))
,buf)))) ,buf))))
(defint-encoder 4 "Convert an integer into a sequence of 4 bytes in network byte order.") (defint-encoder 4 "Convert an integer into a sequence of 4 bytes in network byte order (32 bit).")
(defint-encoder 8 "Convert an integer into a sequence of 8 bytes in network byte order.") (defint-encoder 8 "Convert an integer into a sequence of 8 bytes in network byte order (64 bit).")
(defun decode-int32 (s) (defun decode-int32 (s)
"4 byte -> 32 bit int -> two's complement (in network byte order)" "4 byte -> 32 bit int -> two's complement (in network byte order)"
(let ((i (decode-uint32 s))) (let ((i (decode-uint32 s)))
(if (>= i #.(1- (expt 2 31))) (if (>= i #.(1- (expt 2 31)))
(- (- #.(expt 2 32) i)) (- (- #.(expt 2 32) i))
i))) i)))
(defun decode-int64 (s) (defun decode-int64 (s)
"8 byte -> 64 bit int -> two's complement (in network byte order)" "8 byte -> 64 bit int -> two's complement (in network byte order)"
(let ((i (decode-uint64 s))) (let ((i (decode-uint64 s)))
(if (>= i #.(1- (expt 2 63))) (if (>= i #.(1- (expt 2 63)))
(- (- #.(expt 2 64) i)) (- (- #.(expt 2 64) i))
i))) i)))
;; osc-strings are unsigned bytes, padded to a 4 byte boundary ;; osc-strings are unsigned bytes, padded to a 4 byte boundary
(defun decode-string (data)
"converts a binary vector to a string and removes trailing #\nul characters"
(string-trim '(#\nul) (coerce (map 'vector #'code-char data) 'string)))
(defun encode-string (string) (defun encode-string (string)
"encodes a string as a vector of character-codes, padded to 4 byte boundary" "encodes a string as a vector of character-codes, padded to 4 byte boundary"
(cat (map 'vector #'char-code string) (cat (map 'vector #'char-code string)
(string-padding string))) (string-padding string)))
(defun decode-string (data)
"converts a binary vector to a string and removes trailing #\nul characters"
(string-trim '(#\nul) (coerce (map 'vector #'code-char data) 'string)))
;; blobs are binary data, consisting of a length (int32) and bytes which are ;; blobs are binary data, consisting of a length (int32) and bytes which are
;; osc-padded to a 4 byte boundary. ;; osc-padded to a 4 byte boundary.
(defun decode-blob (blob)
"decode a blob as a vector of unsigned bytes."
(let ((size (decode-int32
(subseq blob 0 4))))
(subseq blob 4 (+ 4 size))))
(defun encode-blob (blob) (defun encode-blob (blob)
"encodes a blob from a given vector" "encodes a blob from a given vector"
(let ((bl (length blob))) (let ((bl (length blob)))
(cat (encode-int32 bl) blob (cat (encode-int32 bl) blob
(pad (mod (- 4 bl) 4))))) ; NOTE: cannot use (padding-length bl), as it is not the same algorithm. Blobs of 4, 8, 12 etc bytes should not be padded! <<<<<<< HEAD
(pad (padding-length bl)))))
(defun decode-blob (blob)
"decode a blob as a vector of unsigned bytes."
(let ((size (decode-int32
(subseq blob 0 4))))
(subseq blob 4 (+ 4 size))))
;; utility functions for osc-string/padding slonking ;; utility functions for osc-string/padding slonking
(defun cat (&rest catatac) (defun cat (&rest catatac)
(apply #'concatenate '(vector *) catatac)) (apply #'concatenate '(vector (unsigned-byte 8)) catatac))
(defun padding-length (s) (defun padding-length (s)
"returns the length of padding required for a given length of string" "returns the length of padding required for a given length of string"
@ -392,4 +460,5 @@
(make-array n :initial-element 0 :fill-pointer n)) (make-array n :initial-element 0 :fill-pointer n))
(provide :osc) (provide :osc)
;; end ;; end

80
package.lisp Normal file
View file

@ -0,0 +1,80 @@
(defpackage :osc
(:use :cl :sb-bsd-sockets)
(:documentation "OSC aka the 'open sound control' protocol")
(:export
#:make-message
#:message
#:make-bundle
#:bundle
#:format-osc-data
#:command
#:args
#:timetag
#:elements
#:encode-message
#:encode-bundle
#:decode-message
#:decode-bundle
#:make-osc-tree
#:dp-register
#:dp-remove
#:dp-match
#:dispatch
#:get-current-timetag ; osc-time
#:timetag+
#:get-unix-time
#:unix-time->timetag
#:timetag->unix-time
#:print-as-double
#:osc-transmitter ; osc-devices
#:osc-transmitter-udp
#:osc-client
#:osc-client-udp
#:osc-client-tcp
#:osc-server
#:osc-server-udp
#:osc-server-tcp
#:protocol
#:name
#:buffer-size
#:quit
#:osc-device-cleanup
#:make-listening-thread ; listening
#:add-osc-responder ; dispatching
#:remove-osc-responder
#:make-osc-transmitter ; transmitters
#:connect
#:send
#:send-msg
#:send-bundle
#:send-to
#:send-msg-to
#:send-bundle-to
#:send-all
#:send-msg-all
#:send-bundle-all
#:make-osc-client ; clients
#:make-client-responders
#:register
#:make-osc-server ; servers
#:boot
#:make-server-responders
#:register-udp-client
#:unregister-udp-client
#:register-tcp-client
#:unregister-tcp-client
#:post-register-hook
#:get-tcp-client
#:print-clients
#:send-to-client
#:send-bundle-to-client
#:*default-osc-buffer-size* ; sockets
#:make-name-string
#:device-active-p
#:device-socket-name
#:address
#:port
#:peer-address
#:peer-port))