nik gaffney
f8cc808c44
#bundle support added to the feet. small step towards watertightness darcs-hash:20050314140505-2648a-268ae71e964c269e196c590d867970411d786804.gz
335 lines
10 KiB
Common Lisp
335 lines
10 KiB
Common Lisp
;; -*- mode: lisp -*-
|
|
;;
|
|
;; an implementation of the OSC (Open Sound Control) protocol
|
|
;;
|
|
;; copyright (C) 2004 FoAM vzw.
|
|
;;
|
|
;; You are granted the rights to distribute and use this software
|
|
;; under the terms of the Lisp Lesser GNU Public License, known
|
|
;; as the LLGPL. The LLGPL consists of a preamble and the LGPL.
|
|
;; Where these conflict, the preamble takes precedence. The LLGPL
|
|
;; is available online at http://opensource.franz.com/preamble.html
|
|
;; and is distributed with this code (see: LICENCE and LGPL files)
|
|
;;
|
|
;; authors
|
|
;;
|
|
;; nik gaffney <nik@f0.am>
|
|
;;
|
|
;; requirements
|
|
;;
|
|
;; dependent on sbcl or cmucl for float encoding, other suggestions welcome.
|
|
;;
|
|
;; commentary
|
|
;;
|
|
;; this is a partial implementation of the OSC protocol which is used
|
|
;; for communication mostly amongst music programs and their attatched
|
|
;; musicians. eg. sc3, max/pd, reaktor/traktorska etc+. more details
|
|
;; of the protocol can be found at the open sound control pages -=>
|
|
;; http://www.cnmat.berkeley.edu/OpenSoundControl/
|
|
;;
|
|
;; - doesnt send nested bundles or timetags later than 'now'
|
|
;; - will most likely crash if the input is malformed
|
|
;; - int32 en/de-coding based on code (c) Walter C. Pelissero
|
|
;;
|
|
;; see the README file for more details...
|
|
;;
|
|
;; known BUGS
|
|
;; - only unknown for now.. .
|
|
|
|
|
|
(defpackage :osc
|
|
(:use :cl)
|
|
(:documentation "OSC aka the 'open sound control' protocol")
|
|
(:export :encode-message
|
|
:encode-bundle
|
|
:decode-message
|
|
:decode-bundle))
|
|
|
|
(in-package :osc)
|
|
|
|
;(declaim (optimize (speed 3) (safety 1)))
|
|
|
|
;;;;;; ; ;; ; ; ; ; ; ; ;
|
|
;;
|
|
;; eNcoding OSC messages
|
|
;;
|
|
;;; ;; ;; ; ; ;; ; ; ; ;
|
|
|
|
|
|
(defun encode-bundle (data)
|
|
"will encode an osc message, or list of messages as a bundle
|
|
with an optional timetag. doesnt handle nested bundles"
|
|
(cat '(35 98 117 110 100 108 101 0) ; #bundle
|
|
(encode-timetag :now)
|
|
(if (listp (car data))
|
|
(apply #'cat (mapcar #'encode-bundle-elt data))
|
|
(encode-bundle-elt data))))
|
|
|
|
(defun encode-bundle-elt (data)
|
|
(let ((message (apply #'encode-message data)))
|
|
(cat (encode-int32 (length message)) message)))
|
|
|
|
(defun encode-message (address &rest data)
|
|
"encodes an osc message with the given address and data."
|
|
(concatenate '(vector (unsigned-byte 8))
|
|
(encode-address address)
|
|
(encode-typetags data)
|
|
(encode-data data)))
|
|
|
|
(defun encode-address (address)
|
|
(cat (map 'vector #'char-code address)
|
|
(string-padding address)))
|
|
|
|
(defun encode-typetags (data)
|
|
"creates a typetag string suitable for the given data.
|
|
valid typetags according to the osc spec are ,i ,f ,s and ,b
|
|
non-std extensions include ,{h|t|d|S|c|r|m|T|F|N|I|[|]}
|
|
see the spec for more details. ..
|
|
|
|
NOTE: currently handles the following tags
|
|
i => #(105) => int32
|
|
f => #(102) => float
|
|
s => #(115) => string
|
|
b => #(98) => blob"
|
|
|
|
(let ((lump (make-array 0 :adjustable t
|
|
:fill-pointer t
|
|
:element-type 'char)))
|
|
(macrolet ((write-to-vector (char)
|
|
`(vector-push-extend
|
|
(char-code ,char) lump)))
|
|
(write-to-vector #\,)
|
|
(dolist (x data)
|
|
(typecase x
|
|
(integer (write-to-vector #\i))
|
|
(float (write-to-vector #\f))
|
|
(simple-string (write-to-vector #\s))
|
|
(simple-vector (write-to-vector #\b))
|
|
(t (error "can only encode ints, floats or strings"))))
|
|
(cat lump
|
|
(pad (padding-length (length lump)))))))
|
|
|
|
(defun encode-data (data)
|
|
"encodes data in a format suitable for an OSC message"
|
|
(let ((lump (make-array 0 :adjustable t :fill-pointer t)))
|
|
(macrolet ((enc (f)
|
|
`(setf lump (cat lump (,f x)))))
|
|
(dolist (x data)
|
|
(typecase x
|
|
(integer (enc encode-int32))
|
|
(float (enc encode-float32))
|
|
(simple-string (enc encode-string))
|
|
(simple-vector (enc encode-blob))
|
|
(t (error "wrong type. turn back"))))
|
|
lump)))
|
|
|
|
|
|
|
|
;;;;;; ; ;; ; ; ; ; ; ; ;
|
|
;;
|
|
;; decoding OSC messages
|
|
;;
|
|
;;; ;; ;; ; ; ; ; ; ;
|
|
|
|
|
|
(defun decode-bundle (data)
|
|
"decodes an osc bundle into a list of decoded-messages, which has
|
|
an osc-timetagas its first element"
|
|
(let ((contents '()))
|
|
(if (equalp 35 (elt data 0)) ; a bundle begins with '#'
|
|
(let ((timetag (subseq data 8 16))
|
|
(i 16)
|
|
(bundle-length (length data)))
|
|
(loop while (< i bundle-length)
|
|
do (let ((mark (+ i 4))
|
|
(size (decode-int32
|
|
(subseq data i (+ i 4)))))
|
|
(if (eq size 0)
|
|
(setf bundle-length 0)
|
|
(push (decode-bundle
|
|
(subseq data mark (+ mark size)))
|
|
contents))
|
|
(incf i (+ 4 size))))
|
|
(push timetag contents))
|
|
(decode-message data))))
|
|
|
|
(defun decode-message (message)
|
|
"reduces an osc message to an (address . data) pair. .."
|
|
(declare (type (vector *) message))
|
|
(let ((x (position (char-code #\,) message)))
|
|
(if (eq x NIL)
|
|
(format t "message contains no data.. ")
|
|
(cons (decode-address (subseq message 0 x))
|
|
(decode-taged-data (subseq message x))))))
|
|
|
|
(defun decode-address (address)
|
|
(coerce (map 'vector #'code-char
|
|
(delete 0 address))
|
|
'string))
|
|
|
|
(defun decode-taged-data (data)
|
|
"decodes data encoded with typetags...
|
|
NOTE: currently handles the following tags
|
|
i => #(105) => int32
|
|
f => #(102) => float
|
|
s => #(115) => string
|
|
b => #(98) => blob"
|
|
|
|
(let ((div (position 0 data)))
|
|
(let ((tags (subseq data 1 div))
|
|
(acc (subseq data (padded-length div)))
|
|
(result '()))
|
|
(map 'vector
|
|
#'(lambda (x)
|
|
(cond
|
|
((eq x (char-code #\i))
|
|
(push (decode-int32 (subseq acc 0 4))
|
|
result)
|
|
(setf acc (subseq acc 4)))
|
|
((eq x (char-code #\f))
|
|
(push (decode-float32 (subseq acc 0 4))
|
|
result)
|
|
(setf acc (subseq acc 4)))
|
|
((eq x (char-code #\s))
|
|
(let ((pointer (padded-length (position 0 acc))))
|
|
(push (decode-string
|
|
(subseq acc 0 pointer))
|
|
result)
|
|
(setf acc (subseq acc pointer))))
|
|
((eq x (char-code #\b))
|
|
(let ((size (decode-int32 (subseq acc 0 4))))
|
|
(let ((end (padded-length (+ 4 size))))
|
|
(push (decode-blob (subseq acc 0 end))
|
|
result)
|
|
(setf acc (subseq acc end)))))
|
|
(t (error "unrecognised typetag"))))
|
|
tags)
|
|
(nreverse result))))
|
|
|
|
|
|
;;; ; ;; ;
|
|
;;
|
|
;; timetags
|
|
;;
|
|
;; - not yet, but probably something using
|
|
;; (get-universal-time) > see also: CLHS 25.1.4 Time
|
|
;; or connecting to an ntp server.,. - ntpdate, ntpq
|
|
;;
|
|
;; - begin with bundles using 'now' as the timetag
|
|
;; - this should really handle 64bit fixed ints,
|
|
;; not signed 32bit ints
|
|
;;
|
|
;;;; ;; ; ;
|
|
|
|
(defconstant +unix-epoch+ (encode-universal-time 0 0 0 1 1 1970 0))
|
|
|
|
(defun encode-timetag (ut &optional subseconds)
|
|
"encodes an osc timetag from a universal-time and 32bit 'sub-second' part
|
|
for an 'instantaneous' timetag use (encode-timetag :now) "
|
|
(if (equalp ut :now)
|
|
#(0 0 0 0 0 0 0 1)
|
|
(cat (encode-int32 (+ ut +unix-epoch+))
|
|
(encode-int32 (subseconds)))))
|
|
|
|
(defun decode-timetag (timetag)
|
|
"decomposes a timetag into ut and a subsecond,. . ."
|
|
(list
|
|
(decode-int32 (subseq timetag 0 4))
|
|
(decode-int32 (subseq timetag 4 8))))
|
|
|
|
|
|
;;;;; ; ; ;; ;; ; ;
|
|
;;
|
|
;; dataformat en- de- cetera.
|
|
;;
|
|
;;; ;; ; ; ;
|
|
|
|
(defun encode-float32 (f)
|
|
"encode an ieee754 float as a 4 byte vector. currently sbcl/cmucl specifc"
|
|
#+sbcl (encode-int32 (sb-kernel:single-float-bits f))
|
|
#+cmucl (encode-int32 (kernel:single-float-bits f))
|
|
#-(or sbcl cmucl) (error "cant encode floats using this implementation"))
|
|
|
|
(defun decode-float32 (s)
|
|
"ieee754 float from a vector of 4 bytes in network byte order"
|
|
#+sbcl (sb-kernel:make-single-float (decode-int32 s))
|
|
#+cmucl (kernel:make-single-float (decode-int32 s))
|
|
#-(or sbcl cmucl) (error "cant decode floats using this implementation"))
|
|
|
|
(defun decode-int32 (s)
|
|
"4 byte > 32 bit int > two's compliment (in network byte order)"
|
|
(let ((i (+ (ash (elt s 0) 24)
|
|
(ash (elt s 1) 16)
|
|
(ash (elt s 2) 8)
|
|
(elt s 3))))
|
|
(if (>= i #x7fffffff)
|
|
(- 0 (- #x100000000 i))
|
|
i)))
|
|
|
|
(defun encode-int32 (i)
|
|
"convert integer into a sequence of 4 bytes in network byte order."
|
|
(declare (type integer i n))
|
|
(let ((buf (make-sequence
|
|
'(vector (unsigned-byte 8)) 4)))
|
|
(macrolet ((set-byte (n)
|
|
`(setf (elt buf ,n)
|
|
(logand #xff (ash i ,(* 8 (- n 3)))))))
|
|
(set-byte 0)
|
|
(set-byte 1)
|
|
(set-byte 2)
|
|
(set-byte 3))
|
|
buf))
|
|
|
|
;; 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)
|
|
"encodes a string as a vector of character-codes, padded to 4 byte boundary"
|
|
(cat (map 'vector #'char-code string)
|
|
(string-padding string)))
|
|
|
|
;; blobs are binary data, consisting of a length (int32) and bytes which are
|
|
;; 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)
|
|
"encodes a blob from a given vector"
|
|
(let ((bl (length blob)))
|
|
(cat (encode-int32 bl) blob
|
|
(pad (padding-length bl)))))
|
|
|
|
|
|
;; utility functions for osc-string/padding slonking
|
|
|
|
(defun cat (&rest catatac)
|
|
(apply #'concatenate '(vector *) catatac))
|
|
|
|
(defun padding-length (s)
|
|
"returns the length of padding required for a given length of string"
|
|
(declare (type fixnum s))
|
|
(- 4 (mod s 4)))
|
|
|
|
(defun padded-length (s)
|
|
"returns the length of an osc-string made from a given length of string"
|
|
(declare (type fixnum s))
|
|
(+ s (- 4 (mod s 4))))
|
|
|
|
(defun string-padding (string)
|
|
"returns the padding required for a given osc string"
|
|
(declare (type simple-string string))
|
|
(pad (- 4 (mod (length string) 4))))
|
|
|
|
(defun pad (n)
|
|
"make a sequence of the required number of #\Nul characters"
|
|
(declare (type fixnum n))
|
|
(make-array n :initial-element 0 :fill-pointer n))
|
|
|
|
;; end
|