diff --git a/README b/README
--- a/README
+++ b/README
@@ -9,4 +9,6 @@
   http://opensoundcontrol.org/
 
 (c) rohan drape, 2006-2008
-    gpl.2, http://gnu.org/copyleft/
+    gpl, http://gnu.org/copyleft/
+    with contributions by alex mclean
+    see darcs history for details
diff --git a/Sound/OpenSoundControl/Byte.hs b/Sound/OpenSoundControl/Byte.hs
--- a/Sound/OpenSoundControl/Byte.hs
+++ b/Sound/OpenSoundControl/Byte.hs
@@ -33,15 +33,15 @@
 
 -- | Encode a 32-bit IEEE floating point number.
 encode_f32 :: Double -> B.ByteString
-encode_f32 n = encode (f32_i32 (realToFrac n))
+encode_f32 = encode . f32_i32 . realToFrac
 
 -- | Encode a 64-bit IEEE floating point number.
 encode_f64 :: Double -> B.ByteString
-encode_f64 n = encode (f64_i64 n)
+encode_f64 = encode . f64_i64
 
 -- | Encode an ASCII string.
 encode_str :: String -> B.ByteString
-encode_str s = B.pack (map (fromIntegral . ord) s)
+encode_str = B.pack . map (fromIntegral . ord)
 
 -- | Decode a signed 8-bit integer.
 decode_i8 :: B.ByteString -> Int
@@ -77,4 +77,4 @@
 
 -- | Decode an ASCII string.
 decode_str :: B.ByteString -> String
-decode_str b = map (chr . fromIntegral) (B.unpack b)
+decode_str = map (chr . fromIntegral) . B.unpack
diff --git a/Sound/OpenSoundControl/Cast.hs b/Sound/OpenSoundControl/Cast.hs
--- a/Sound/OpenSoundControl/Cast.hs
+++ b/Sound/OpenSoundControl/Cast.hs
@@ -43,15 +43,15 @@
 
 -- | Inverse of 'str_cstr'.
 cstr_str :: [Word8] -> String
-cstr_str s = map (chr . fromIntegral) (takeWhile (/= 0) s)
+cstr_str = map (chr . fromIntegral) . takeWhile (/= 0)
 
 -- | Transform a haskell string to a pascal string (a length prefixed byte string).
 str_pstr :: String -> [Word8]
-str_pstr s = (fromIntegral (length s)) : map (fromIntegral . ord) s
+str_pstr s = fromIntegral (length s) : map (fromIntegral . ord) s
 
 -- | Inverse of 'str_pstr'.
 pstr_str :: [Word8] -> String
-pstr_str s = map (chr . fromIntegral) (drop 1 s)
+pstr_str = map (chr . fromIntegral) . drop 1
 
 -- One element marray.
 unit_array :: (MArray a e m) => e -> m (a Int e)
diff --git a/Sound/OpenSoundControl/OSC.hs b/Sound/OpenSoundControl/OSC.hs
--- a/Sound/OpenSoundControl/OSC.hs
+++ b/Sound/OpenSoundControl/OSC.hs
@@ -19,6 +19,7 @@
            | Double Double
            | String String
            | Blob [Word8]
+           | TimeStamp Time
              deriving (Eq, Show)
 
 -- | An OSC packet.
@@ -38,6 +39,7 @@
 tag (Double _) = 'd'
 tag (String _) = 's'
 tag (Blob _) = 'b'
+tag (TimeStamp _) = 't'
 
 -- Command argument types are given by a descriptor.
 descriptor :: [Datum] -> Datum
@@ -56,6 +58,7 @@
 encode_datum (Int i) = encode_i32 i
 encode_datum (Float f) = encode_f32 f
 encode_datum (Double d) = encode_f64 d
+encode_datum (TimeStamp t) = encode_u64 $ as_ntpi t
 encode_datum (String s) = B.pack (extend 0 (str_cstr s))
 encode_datum (Blob b) = B.concat [encode_i32 (length b), B.pack (extend 0 b)]
 
@@ -73,7 +76,7 @@
 -- Encode an OSC bundle.
 encode_bundle_ntpi :: Integer -> [OSC] -> B.ByteString
 encode_bundle_ntpi t l =
-    B.concat [ encode_datum (String "#bundle")
+    B.concat [ bundle_header
              , encode_u64 t
              , B.concat (map (encode_datum . encode_osc_blob) l) ]
 
@@ -89,6 +92,7 @@
 size 'i' _ = 4
 size 'f' _ = 4
 size 'd' _ = 8
+size 't' _ = 8 -- timetag
 size 's' b = fromIntegral (fromMaybe
                            (error ("size: no terminating zero: " ++ show b))
                            (B.elemIndex 0 b))
@@ -108,9 +112,10 @@
 decode_datum 'd' b = Double (decode_f64 b)
 decode_datum 's' b = String (decode_str (b_take n b)) where n = size 's' b
 decode_datum 'b' b = Blob (B.unpack (b_take n (B.drop 4 b))) where n = size 'b' b
-decode_datum _ _ = error "decode_datum: illegal type"
+decode_datum 't' b = TimeStamp $ NTPi (decode_u64 b)
+decode_datum t _ = error ("decode_datum: illegal type (" ++ [t] ++ ")")
 
--- Decode a sequenc of OSC datum given a type descriptor string.
+-- Decode a sequence of OSC datum given a type descriptor string.
 decode_datum_seq :: [Char] -> B.ByteString -> [Datum]
 decode_datum_seq cs b = zipWith decode_datum cs (snd (mapAccumL f b cs))
     where swap (x,y) = (y,x)
@@ -125,12 +130,32 @@
           (String dsc) = decode_datum 's' (b_drop n b)
           arg = decode_datum_seq (drop 1 dsc) (b_drop (n + m) b)
 
+-- Decode a sequence of OSC messages, each one headed by its length
+decode_message_seq :: B.ByteString -> [OSC]
+decode_message_seq b | B.length b == 0 = []
+                     | otherwise = m:nxt
+                     where s = decode_i32 b
+                           m = decode_message $ b_drop 4 b
+                           nxt = decode_message_seq $ b_drop (4+s) b
+
+decode_bundle :: B.ByteString -> OSC
+decode_bundle b = Bundle timeStamp ms
+    where h = storage 's' b -- header (should be '#bundle'
+          t = storage 't' (b_drop h b) -- time tag
+          (TimeStamp timeStamp) = decode_datum 't' (b_drop h b)
+          ms = decode_message_seq $ b_drop (h+t) b
+
 -- | Decode an OSC packet.
 decodeOSC :: B.ByteString -> OSC
-decodeOSC = decode_message
+decodeOSC b | bundle_header `B.isPrefixOf` b = decode_bundle b
+            | otherwise = decode_message b
 
 b_take :: Int -> B.ByteString -> B.ByteString
-b_take n = B.take (fromIntegral n)
+b_take = B.take . fromIntegral
 
 b_drop :: Int -> B.ByteString -> B.ByteString
-b_drop n = B.drop (fromIntegral n)
+b_drop = B.drop . fromIntegral
+
+bundle_header :: B.ByteString
+bundle_header = encode_datum (String "#bundle")
+
diff --git a/Sound/OpenSoundControl/Time.hs b/Sound/OpenSoundControl/Time.hs
--- a/Sound/OpenSoundControl/Time.hs
+++ b/Sound/OpenSoundControl/Time.hs
@@ -16,6 +16,12 @@
 as_ntpi (NTPr t) = ntpr_ntpi t
 as_ntpi (NTPi t) = t
 
+-- | Coerce to UTCr form.
+as_utcr :: Time -> Double
+as_utcr (UTCr t) = t
+as_utcr (NTPr t) = ntpr_utcr t
+as_utcr (NTPi t) = ntpi_utcr t
+
 -- | Times can be ordered, avoid coercion if not required.
 instance Ord Time where
     compare (UTCr p) (UTCr q) = compare p q
@@ -27,10 +33,23 @@
 ntpr_ntpi :: Double -> Integer
 ntpr_ntpi t = round (t * 2^(32::Int))
 
+-- | Convert an NTP timestamp to a real-valued NTP timestamp.
+ntpi_ntpr :: Integer -> Double
+ntpi_ntpr t = fromIntegral t / 2^(32::Int)
+
 -- | Convert UTC timestamp to NTP timestamp.
 utcr_ntpi :: Double -> Integer
 utcr_ntpi t = ntpr_ntpi (t + secdif)
     where secdif = (70 * 365 + 17) * 24 * 60 * 60
+
+-- | Convert NTP timestamp to UTC timestamp.
+ntpr_utcr :: Double -> Double
+ntpr_utcr t = t - secdif
+    where secdif = (70 * 365 + 17) * 24 * 60 * 60
+
+-- | Convert NTP timestamp to UTC timestamp.
+ntpi_utcr :: Integer -> Double
+ntpi_utcr = ntpr_utcr . ntpi_ntpr
 
 -- | The time at 1970-01-01:00:00:00.
 utc_base :: T.UTCTime
diff --git a/Sound/OpenSoundControl/Transport/TCP.hs b/Sound/OpenSoundControl/Transport/TCP.hs
--- a/Sound/OpenSoundControl/Transport/TCP.hs
+++ b/Sound/OpenSoundControl/Transport/TCP.hs
@@ -28,7 +28,7 @@
 
 -- | Make a TCP connection.
 openTCP :: String -> Int -> IO TCP
-openTCP host port = liftM TCP (connectTo host (PortNumber (fromIntegral port)))
+openTCP host = liftM TCP . connectTo host . PortNumber . fromIntegral
 
 -- | A trivial TCP OSC server.
 tcpServer :: Int -> (TCP -> IO ()) -> IO ()
diff --git a/Sound/OpenSoundControl/Transport/UDP.hs b/Sound/OpenSoundControl/Transport/UDP.hs
--- a/Sound/OpenSoundControl/Transport/UDP.hs
+++ b/Sound/OpenSoundControl/Transport/UDP.hs
@@ -1,7 +1,8 @@
 -- | OSC over UDP implementation.
-module Sound.OpenSoundControl.Transport.UDP ( UDP
+module Sound.OpenSoundControl.Transport.UDP ( UDP(udpSocket)
                                             , openUDP
                                             , udpServer
+                                            , udpPort
                                             , sendTo, recvFrom ) where
 
 import Control.Monad
@@ -11,7 +12,8 @@
 import Sound.OpenSoundControl.Transport
 
 -- | The UDP transport handle data type.
-data UDP = UDP N.Socket deriving (Eq, Show)
+data UDP = UDP { udpSocket :: N.Socket }
+           deriving (Eq, Show)
 
 instance Transport UDP where
    send  (UDP fd) msg = N.send fd (decode_str (encodeOSC msg)) >> return ()
@@ -45,3 +47,6 @@
     do (s, _, a) <- N.recvFrom fd 8192
        let o = (decodeOSC . encode_str) s
        return (o, a)
+
+udpPort :: Integral n => UDP -> IO n
+udpPort (UDP fd) = fmap fromIntegral (N.socketPort fd)
diff --git a/hosc.cabal b/hosc.cabal
--- a/hosc.cabal
+++ b/hosc.cabal
@@ -1,23 +1,28 @@
 Name:              hosc
-Version:           0.6
+Version:           0.7
 Synopsis:          Haskell Open Sound Control
 Description:       hosc provides Sound.OpenSoundControl, a haskell
                    module implementing a subset of the Open Sound
                    Control byte protocol.
 License:           GPL
 Category:          Sound
-Copyright:         (c) Rohan Drape, 2006-2008
+Copyright:         (c) Rohan Drape, 2006-2009
 Author:            Rohan Drape
 Maintainer:        rd@slavepianos.org
 Stability:         Experimental
 Homepage:          http://slavepianos.org/rd/f/269573/
-Tested-With:       GHC==6.10.1
+Tested-With:       GHC == 6.8.2
 Build-Type:        Simple
-Cabal-Version:     >= 1.2
+Cabal-Version:     >= 1.6
 Data-Files:        README
 
 Library
-  Build-Depends:   array, base, bytestring, binary, network, time
+  Build-Depends:   array,
+                   base == 3.*,
+                   bytestring,
+                   binary,
+                   network,
+                   time
   GHC-Options:     -Wall -fwarn-tabs
   Exposed-modules: Sound.OpenSoundControl
                    Sound.OpenSoundControl.Byte
