diff --git a/Sound/SC3/Server/Allocator.hs b/Sound/SC3/Server/Allocator.hs
--- a/Sound/SC3/Server/Allocator.hs
+++ b/Sound/SC3/Server/Allocator.hs
@@ -4,32 +4,70 @@
   , TypeFamilies #-}
 
 module Sound.SC3.Server.Allocator
-    ( AllocFailure(..)
+    ( -- *Allocation errors
+      AllocFailure(..)
+      -- * Allocator statistics
+    , Statistics(..)
+    , percentFree
+    , percentUsed
+      -- * Allocator classes
     , IdAllocator(..)
     , RangeAllocator(..)
+      -- * Identifier ranges
     , module Sound.SC3.Server.Allocator.Range
     ) where
 
 import Control.Exception (Exception)
 import Control.Failure (Failure)
-import Control.Monad (replicateM)
+import Control.Monad (foldM, replicateM)
 import qualified Control.Monad.Trans.Class as State
 import qualified Control.Monad.Trans.State.Strict as State
 import Data.Typeable (Typeable)
 import Sound.SC3.Server.Allocator.Range
 
+-- | Failure type for allocators.
 data AllocFailure =
-    NoFreeIds
-  | InvalidId
+    NoFreeIds   -- ^ There are no free ids left in the allocator.
+  | InvalidId   -- ^ The id being released has not been allocated by this allocator.
   deriving (Show, Typeable)
 
 instance Exception AllocFailure
 
+-- | Simple allocator usage statistics.
+data Statistics = Statistics {
+    numAvailable :: Int     -- ^ Total number of available identifiers
+  , numFree :: Int          -- ^ Number of currently available identifiers
+  , numUsed :: Int          -- ^ Number of identifiers currently in use
+  } deriving (Eq, Show)
+
+-- | Percentage of currently available identifiers.
+--
+-- > percentFree s = numFree s / numAvailable s
+-- > percentFree s + percentUsed s = 1
+percentFree :: Statistics -> Double
+percentFree s = fromIntegral (numFree s) / fromIntegral (numAvailable s)
+
+-- | Percentage of identifiers currently in use.
+--
+-- > percentUsed s = numUsed s / numAvailable s
+-- > percentUsed s + percentFree s = 1
+percentUsed :: Statistics -> Double
+percentUsed s = fromIntegral (numUsed s) / fromIntegral (numAvailable s)
+
+-- | IdAllocator provides an interface for allocating and releasing
+-- identifiers that correspond to server resources, such as node, buffer and
+-- bus ids.
 class IdAllocator a where
     type Id a
+    -- | Allocate a new identifier and return the changed allocator.
     alloc :: Failure AllocFailure m => a -> m (Id a, a)
+
+    -- | Free a previously allocated identifier and return the changed allocator.
+    --
+    -- Freeing an identifier that hasn't been allocated with this allocator may trigger a failure.
     free  :: Failure AllocFailure m => Id a -> a -> m a
-    
+
+    -- | Allocate a number of - not necessarily consecutive - identifiers and return the changed allocator.
     allocMany :: Failure AllocFailure m => Int -> a -> m ([Id a], a)
     allocMany n = State.runStateT (replicateM n (modifyM alloc))
         where
@@ -38,6 +76,17 @@
                 State.put $! s'
                 return a
 
+    -- | Free a list of previously allocated identifiers.
+    freeMany :: Failure AllocFailure m => [Id a] -> a -> m a
+    freeMany = flip (foldM (flip free))
+
+    -- | Return usage statistics.
+    statistics :: a -> Statistics
+
+-- | RangeAllocator provides an interface for allocating and releasing ranges
+-- of consecutive identifiers.
 class IdAllocator a => RangeAllocator a where
+    -- | Allocate n consecutive identifiers and return the changed allocator.
     allocRange :: Failure AllocFailure m => Int -> a -> m (Range (Id a), a)
+    -- | Free a range of previously allocated identifiers and return the changed allocator.
     freeRange  :: Failure AllocFailure m => Range (Id a) -> a -> m a
diff --git a/Sound/SC3/Server/Allocator/BlockAllocator/FirstFit.hs b/Sound/SC3/Server/Allocator/BlockAllocator/FirstFit.hs
new file mode 100644
--- /dev/null
+++ b/Sound/SC3/Server/Allocator/BlockAllocator/FirstFit.hs
@@ -0,0 +1,91 @@
+{-# LANGUAGE FlexibleContexts
+           , TypeFamilies #-}
+module Sound.SC3.Server.Allocator.BlockAllocator.FirstFit
+  (
+    FirstFitAllocator
+  , Sorting(..)
+  , Coalescing(..)
+  , cons
+  , addressFit
+  , bestFit
+  , worstFit
+  ) where
+
+import           Control.Arrow (first)
+import           Control.DeepSeq (NFData(..))
+import           Control.Failure (Failure, failure)
+import           Control.Monad (liftM)
+import           Sound.SC3.Server.Allocator
+import qualified Sound.SC3.Server.Allocator.Range as Range
+import           Sound.SC3.Server.Allocator.BlockAllocator.FreeList (FreeList, Sorting(..))
+import qualified Sound.SC3.Server.Allocator.BlockAllocator.FreeList as FreeList
+
+data Coalescing = NoCoalescing | LazyCoalescing deriving (Enum, Eq, Show)
+
+data FirstFitAllocator i = FirstFitAllocator {
+    coalescing :: Coalescing
+  , available :: !Int
+  , used :: !Int
+  , freeList :: !(FreeList i)
+  } deriving (Eq, Show)
+
+instance NFData i => NFData (FirstFitAllocator i) where
+    rnf (FirstFitAllocator x1 x2 x3 x4) = x1 `seq` rnf x2 `seq` rnf x3 `seq` rnf x4 `seq` ()
+
+cons :: Integral i => Sorting -> Coalescing -> Range i -> FirstFitAllocator i
+cons s c r = FirstFitAllocator c (fromIntegral (Range.size r)) 0 (FreeList.singleton s r)
+
+addressFit :: Integral i => Coalescing -> Range i -> FirstFitAllocator i
+addressFit = cons Address
+
+bestFit :: Integral i => Coalescing -> Range i -> FirstFitAllocator i
+bestFit = cons IncreasingSize
+
+worstFit :: Integral i => Coalescing -> Range i -> FirstFitAllocator i
+worstFit = cons DecreasingSize
+
+_alloc :: (Integral i, Failure AllocFailure m) => Int -> FirstFitAllocator i -> m (Range i, FirstFitAllocator i)
+_alloc n a =
+    case FreeList.alloc fits (freeList a) of
+        Nothing -> case coalescing a of
+                    NoCoalescing ->
+                        failure NoFreeIds
+                    LazyCoalescing ->
+                        case FreeList.alloc fits (FreeList.coalesce (freeList a)) of
+                            Nothing -> failure NoFreeIds
+                            Just (r, l) -> alloc r l
+        Just (r, l) -> alloc r l
+    where
+        fits r = Range.size r >= n
+        alloc r l =
+            if Range.size r == n
+            then return (r, a { freeList = l
+                              , used = used a + n })
+            else let (r1, r2) = Range.split n r
+                 in return (r1, a { freeList = FreeList.insert r2 l
+                                  , used = used a + n })
+
+_free :: (Integral i, Failure AllocFailure m) => Range i -> FirstFitAllocator i -> m (FirstFitAllocator i)
+_free r a =
+    let u = used a - fromIntegral (Range.size r)
+    in if u < 0
+       then failure InvalidId
+       else return a { freeList = FreeList.insert r (freeList a)
+                     , used = u }
+
+_statistics :: (Integral i) => FirstFitAllocator i -> Statistics
+_statistics a =
+    Statistics {
+        numAvailable = available a
+      , numFree = available a - used a
+      , numUsed = used a }
+
+instance (Integral i) => IdAllocator (FirstFitAllocator i) where
+    type Id (FirstFitAllocator i) = i
+    alloc = liftM (first Range.begin) . _alloc 1
+    free = _free . sized 1
+    statistics = _statistics
+
+instance (Integral i) => RangeAllocator (FirstFitAllocator i) where
+    allocRange = _alloc
+    freeRange = _free
diff --git a/Sound/SC3/Server/Allocator/BlockAllocator/FreeList.hs b/Sound/SC3/Server/Allocator/BlockAllocator/FreeList.hs
new file mode 100644
--- /dev/null
+++ b/Sound/SC3/Server/Allocator/BlockAllocator/FreeList.hs
@@ -0,0 +1,73 @@
+module Sound.SC3.Server.Allocator.BlockAllocator.FreeList
+  (
+    FreeList
+  , Sorting(..)
+  , empty
+  , singleton
+  , fromList
+  , insert
+  , alloc
+  , coalesce
+  ) where
+
+import           Control.DeepSeq (NFData(..))
+import           Data.Ord (comparing)
+import qualified Data.List as List
+import           Sound.SC3.Server.Allocator.Range (Range)
+import qualified Sound.SC3.Server.Allocator.Range as Range
+
+data Sorting = Address | IncreasingSize | DecreasingSize deriving (Enum, Eq, Show)
+
+data FreeList i = FreeList Sorting [Range i] deriving (Eq, Show)
+
+instance NFData i => NFData (FreeList i) where
+    rnf (FreeList x1 x2) = x1 `seq` rnf x2 `seq` ()
+
+type SortFunc i = Range i -> Range i -> Ordering
+
+sortFunc :: Integral i => Sorting -> SortFunc i
+sortFunc s = f
+    where f = case s of
+                Address -> comparing Range.begin
+                IncreasingSize -> comparing Range.size
+                DecreasingSize -> flip (comparing Range.size)
+
+sortBy :: Integral i => Sorting -> [Range i] -> [Range i]
+sortBy s = List.sortBy (sortFunc s)
+
+insertBy :: Integral i => Sorting -> Range i -> [Range i] -> [Range i]
+insertBy s = List.insertBy (sortFunc s)
+
+empty :: Sorting -> FreeList i
+empty s = FreeList s []
+
+singleton :: Sorting -> Range i -> FreeList i
+singleton s r = FreeList s [r]
+
+fromList :: Integral i => Sorting -> [Range i] -> FreeList i
+fromList s = FreeList s . sortBy s
+
+insert :: Integral i => Range i -> FreeList i -> FreeList i
+insert r (FreeList s l) = FreeList s (insertBy s r l)
+
+alloc :: (Range i -> Bool) -> FreeList i -> Maybe (Range i, FreeList i)
+alloc p (FreeList s l) =
+    case List.break p l of
+        (_, []) -> Nothing
+        (l1, (r:l2)) -> Just (r, FreeList s (l1 ++ l2))
+
+coalescel :: Ord i => [Range i] -> [Range i]
+coalescel [] = []
+coalescel l@(_:[]) = l
+coalescel (r1:r2:l)
+    | Range.adjoins r1 r2 = coalescel (Range.join r1 r2 : l)
+    | otherwise           = r1 : coalescel (r2 : l)
+
+sortedByAddress :: Integral i => ([Range i] -> [Range i]) -> FreeList i -> FreeList i
+sortedByAddress f (FreeList s l) =
+    case s of
+        Address -> FreeList s (f l)
+        _       -> FreeList s (sortBy s (f (sortBy Address l)))
+
+coalesce :: Integral i => FreeList i -> FreeList i
+coalesce = sortedByAddress coalescel
diff --git a/Sound/SC3/Server/Allocator/Range.hs b/Sound/SC3/Server/Allocator/Range.hs
--- a/Sound/SC3/Server/Allocator/Range.hs
+++ b/Sound/SC3/Server/Allocator/Range.hs
@@ -1,9 +1,11 @@
 module Sound.SC3.Server.Allocator.Range (
     Range
   , range
+  , sized
   , empty
-  , lowerBound
-  , upperBound
+  , begin
+  , end
+  , last
   , size
   , null
   , toList
@@ -12,14 +14,18 @@
   , adjoins
   , overlaps
   , contains
+  , split
   , join
 ) where
 
 import Control.DeepSeq (NFData(..))
-import Prelude hiding (null)
+import Prelude hiding (last, null)
 
--- |Model intervals [a, b[
-data Range a = Range !a !a deriving (Eq, Show)
+-- | Model intervals [begin, end[
+data Range a = Range {
+    begin :: !a
+  , end :: !a
+  } deriving (Eq, Show)
 
 instance NFData a => NFData (Range a) where
     rnf (Range x1 x2) = rnf x1 `seq` rnf x2 `seq` ()
@@ -31,35 +37,42 @@
 range a b | a <= b    = mkRange a b
           | otherwise = mkRange b a
 
-empty :: Num a => Range a
-empty = mkRange 0 0
+sized :: Num a => Int -> a -> Range a
+sized n a = mkRange a (a + fromIntegral n)
 
-lowerBound :: Range a -> a
-lowerBound (Range a _) = a
+empty :: Num a => a -> Range a
+empty a = mkRange a a
 
-upperBound :: Range a -> a
-upperBound (Range _ a) = a
+last :: Enum a => Range a -> a
+last = pred . end
 
-size :: Num a => Range a -> a
-size a = upperBound a - lowerBound a
+size :: Integral a => Range a -> Int
+size a = fromIntegral (end a - begin a)
 
 null :: Eq a => Range a -> Bool
-null a = lowerBound a == upperBound a
+null a = begin a == end a
 
 toList :: Enum a => Range a -> [a]
-toList a = [lowerBound a..pred (upperBound a)]
+toList a = [begin a..last a]
 
 within :: Ord a => a -> Range a -> Bool
-x `within` a = x >= lowerBound a && x < upperBound a
+x `within` a = x >= begin a && x < end a
 
 adjoins :: Eq a => Range a -> Range a -> Bool
-a `adjoins` b = (upperBound a == lowerBound b) || (upperBound b == lowerBound a)
+a `adjoins` b = (end a == begin b) || (end b == begin a)
 
 overlaps :: Ord a => Range a -> Range a -> Bool
-a `overlaps` b = (upperBound a > lowerBound b) || (upperBound b > lowerBound a)
+a `overlaps` b = (end a > begin b) || (end b > begin a)
 
 contains :: Ord a => Range a -> Range a -> Bool
-a `contains` b = lowerBound b >= lowerBound a && upperBound b <= upperBound a
+a `contains` b = begin b >= begin a && end b <= end a
 
+split :: Integral a => Int -> Range a -> (Range a, Range a)
+split n r@(Range l u)
+    | n <= 0 = (empty l, r)
+    | n >= size r = (r, empty u)
+    | otherwise = (mkRange l (l+k), mkRange (l+k) u)
+    where k = fromIntegral n
+
 join :: Ord a => Range a -> Range a -> Range a
-join a b = mkRange (min (lowerBound a) (lowerBound b)) (max (upperBound a) (upperBound b))
+join a b = mkRange (min (begin a) (begin b)) (max (end a) (end b))
diff --git a/Sound/SC3/Server/Allocator/SetAllocator.hs b/Sound/SC3/Server/Allocator/SetAllocator.hs
--- a/Sound/SC3/Server/Allocator/SetAllocator.hs
+++ b/Sound/SC3/Server/Allocator/SetAllocator.hs
@@ -8,7 +8,7 @@
 
 import Control.Failure (Failure, failure)
 import Control.DeepSeq (NFData(..))
-import Data.BitSet as Set
+import qualified Data.BitSet as Set
 import Sound.SC3.Server.Allocator
 
 data SetAllocator i =
@@ -25,30 +25,51 @@
         rnf x3 `seq` ()
 
 cons :: Range i -> SetAllocator i
-cons r = SetAllocator r Set.empty (lowerBound r)
+cons r = SetAllocator r Set.empty (begin r)
 
+-- | Convert an id to a bit index.
+--
+-- This is necessary to keep the BitSet size bounded between [0, numIds[.
+toBit :: Integral i => Range i -> i -> i
+toBit r i = i - begin r
+
 findNext :: (Integral i) => SetAllocator i -> Maybe i
-findNext (SetAllocator r u n) = loop (succ n)
+findNext (SetAllocator r u i)
+    | fromIntegral (size r) == Set.size u = Nothing
+    | otherwise = loop i
     where
-        loop !i
-            | i == n = Nothing
-            | i == upperBound r = loop (lowerBound r)
-            | Set.member (fromIntegral i) u = loop (succ i)
-            | otherwise = Just i
+        wrap i = if i >= end r
+                    then begin r
+                    else i
+        loop !i = let i' = wrap (i+1)
+                  in if Set.member (toBit r i') u
+                     then loop i'
+                     else Just i'
 
-sa_alloc :: (Integral i, Failure AllocFailure m) => SetAllocator i -> m (i, SetAllocator i)
-sa_alloc s@(SetAllocator r u n)
-    | Set.member (fromIntegral n) u = failure NoFreeIds
-    | otherwise = case findNext s of
-                    Nothing -> failure NoFreeIds
-                    Just n' -> return (n, SetAllocator r (Set.insert (fromIntegral n) u) n')
+_alloc :: (Integral i, Failure AllocFailure m) => SetAllocator i -> m (i, SetAllocator i)
+_alloc a@(SetAllocator r u i) =
+    case findNext a of
+        Nothing -> failure NoFreeIds
+        Just i' -> return (i, SetAllocator r (Set.insert (toBit r i) u) i')
 
-sa_free :: (Integral i, Failure AllocFailure m) => i -> SetAllocator i -> m (SetAllocator i)
-sa_free i (SetAllocator r u n) | Set.member (fromIntegral i) u = return (SetAllocator r u' n)
-                               | otherwise = failure InvalidId
-    where u' = Set.delete (fromIntegral i) u
+_free :: (Integral i, Failure AllocFailure m) => i -> SetAllocator i -> m (SetAllocator i)
+_free i (SetAllocator r u n) =
+    if Set.member (toBit r i) u
+    then let u' = Set.delete (toBit r i) u
+         in return (SetAllocator r u' n)
+    else failure InvalidId
 
+_statistics :: (Integral i) => SetAllocator i -> Statistics
+_statistics (SetAllocator r u _) =
+    let k = fromIntegral (size r)
+        n = Set.size u
+    in Statistics {
+        numAvailable = k
+      , numFree = k - n
+      , numUsed = n }
+
 instance (Integral i) => IdAllocator (SetAllocator i) where
     type Id (SetAllocator i) = i
-    alloc = sa_alloc
-    free  = sa_free
+    alloc                    = _alloc
+    free                     = _free
+    statistics               = _statistics
diff --git a/Sound/SC3/Server/Allocator/SimpleAllocator.hs b/Sound/SC3/Server/Allocator/SimpleAllocator.hs
--- a/Sound/SC3/Server/Allocator/SimpleAllocator.hs
+++ b/Sound/SC3/Server/Allocator/SimpleAllocator.hs
@@ -1,27 +1,49 @@
-{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE FlexibleContexts
+           , TypeFamilies #-}
 module Sound.SC3.Server.Allocator.SimpleAllocator (
     SimpleAllocator
   , cons
 ) where
 
 import Control.DeepSeq (NFData(..))
+import Control.Failure (Failure, failure)
 import Sound.SC3.Server.Allocator
 
-newtype SimpleAllocator i = SimpleAllocator i deriving (Eq, Show)
+data SimpleAllocator i =
+    SimpleAllocator
+        {-# UNPACK #-}!(Range i)
+        {-# UNPACK #-}!Int
+                      !i
+        deriving (Eq, Show)
 
 instance NFData i => NFData (SimpleAllocator i) where
-    rnf (SimpleAllocator x1) = rnf x1 `seq` ()
+    rnf (SimpleAllocator x1 x2 x3) = rnf x1 `seq` rnf x2 `seq` rnf x3 `seq` ()
 
-cons :: Num i => SimpleAllocator i
-cons = SimpleAllocator 0
+cons :: Range i -> SimpleAllocator i
+cons r = SimpleAllocator r 0 (begin r)
 
-sa_alloc :: (Num i, Monad m) => SimpleAllocator i -> m (i, SimpleAllocator i)
-sa_alloc (SimpleAllocator n) = return (n, SimpleAllocator (n+1))
+_alloc :: (Enum i, Ord i, Monad m) => SimpleAllocator i -> m (i, SimpleAllocator i)
+_alloc (SimpleAllocator r n i) =
+    let i' = succ i
+    in return (i, SimpleAllocator r (n+1) (if i' >= end r then begin r else i'))
 
-sa_free :: (Monad m) => i -> SimpleAllocator i -> m (SimpleAllocator i)
-sa_free _ sa = return sa
+_free :: (Failure AllocFailure m) => i -> SimpleAllocator i -> m (SimpleAllocator i)
+_free _ (SimpleAllocator r n i) =
+    let n' = n-1
+    in if n' < 0
+       then failure InvalidId
+       else return (SimpleAllocator r n' i)
 
+_statistics :: Integral i => SimpleAllocator i -> Statistics
+_statistics (SimpleAllocator r n _) =
+    let k = fromIntegral (size r)
+    in Statistics {
+        numAvailable = k
+      , numFree = k - n
+      , numUsed = n }
+
 instance (Integral i) => IdAllocator (SimpleAllocator i) where
     type Id (SimpleAllocator i) = i
-    alloc = sa_alloc
-    free  = sa_free
+    alloc                       = _alloc
+    free                        = _free
+    statistics                  = _statistics
diff --git a/Sound/SC3/Server/Allocator/Wrapped.hs b/Sound/SC3/Server/Allocator/Wrapped.hs
new file mode 100644
--- /dev/null
+++ b/Sound/SC3/Server/Allocator/Wrapped.hs
@@ -0,0 +1,35 @@
+{-# LANGUAGE FlexibleContexts #-}
+-- | Helper functions for newtype wrappers.
+module Sound.SC3.Server.Allocator.Wrapped
+  (
+    alloc
+  , free
+  , statistics
+  , allocRange
+  , freeRange
+  ) where
+
+import           Control.Arrow (second)
+import           Control.Failure (Failure)
+import           Control.Monad (liftM)
+import           Sound.SC3.Server.Allocator (AllocFailure, Id, IdAllocator, Range, RangeAllocator, Statistics)
+import qualified Sound.SC3.Server.Allocator as Alloc
+
+alloc :: (Failure AllocFailure m, IdAllocator a) =>
+    (a -> a') -> a -> m (Id a, a')
+alloc f = liftM (second f) . Alloc.alloc
+
+free :: (Failure AllocFailure m, IdAllocator a) =>
+    (a -> a') -> Id a -> a -> m a'
+free f i = liftM f . Alloc.free i
+
+statistics :: IdAllocator a => a -> Statistics
+statistics = Alloc.statistics
+
+allocRange :: (Failure AllocFailure m, RangeAllocator a) =>
+    (a -> a') -> Int -> a -> m (Range (Id a), a')
+allocRange f n = liftM (second f) . Alloc.allocRange n
+
+freeRange :: (Failure AllocFailure m, RangeAllocator a) =>
+    (a -> a') -> Range (Id a) -> a -> m a'
+freeRange f r = liftM f . Alloc.freeRange r
diff --git a/Sound/SC3/Server/Connection.hs b/Sound/SC3/Server/Connection.hs
--- a/Sound/SC3/Server/Connection.hs
+++ b/Sound/SC3/Server/Connection.hs
@@ -1,33 +1,45 @@
 {-# LANGUAGE ExistentialQuantification
+           , FlexibleContexts
            , GeneralizedNewtypeDeriving #-}
-module Sound.SC3.Server.Connection (
-    Connection
+-- | A 'Connection' encapsulates the transport needed for communicating with the synthesis server, the client-side state (e.g. resource id allocators) and various synchronisation primitives.
+module Sound.SC3.Server.Connection
+  ( Connection
   , state
   , new
   , close
-  , fork
-  , async
-  , syncWith
-  , syncAddress
+    -- * Allocation
+  , alloc
+  , free
+  , allocMany
+  , freeMany
+  , allocRange
+  , freeRange
+    -- * Communication and synchronisation
+  , send
+  , waitFor
+  , waitFor_
+  , waitForAll
+  , waitForAll_
   , sync
   , unsafeSync
-) where
+  ) where
 
-import           Control.Concurrent (ThreadId, forkIO)
+import           Control.Concurrent (forkIO)
 import           Control.Concurrent.MVar
-import qualified Data.HashTable as Hash
+import           Control.Concurrent.Chan
+import           Control.Monad (liftM, replicateM, void)
+import           Data.Accessor
 import           Sound.OpenSoundControl (Datum(..), OSC(..), Transport, immediately)
 import qualified Sound.OpenSoundControl as OSC
-
 import           Sound.SC3 (notify)
-import           Sound.SC3.Server.Notification (done, synced)
-import           Sound.SC3.Server.State (State, SyncId)
+import           Sound.SC3.Server.Notification (Notification(..), synced)
+import           Sound.SC3.Server.Allocator (Id, IdAllocator, Range, RangeAllocator)
+import qualified Sound.SC3.Server.Allocator as Alloc
+import           Sound.SC3.Server.Connection.ListenerMap (Listener, ListenerId, ListenerMap)
+import qualified Sound.SC3.Server.Connection.ListenerMap as ListenerMap
+import           Sound.SC3.Server.State (Allocator, State, SyncId)
 import qualified Sound.SC3.Server.State as State
-import qualified Sound.SC3.Server.State.Concurrent as IOState
 
-type ListenerId  = Int
-type Listener    = OSC -> IO ()
-data ListenerMap = ListenerMap !(Hash.HashTable ListenerId Listener) !ListenerId
 data Connection  = forall t . Transport t => Connection t (MVar State) (MVar ListenerMap)
 
 state :: Connection -> MVar State
@@ -37,68 +49,124 @@
 listeners (Connection _ _ l) = l
 
 initServer :: Connection -> IO ()
-initServer c = do
-    Bundle immediately [notify True] `syncWith` done "notify" $ c
-    return ()
+initServer c = sync c (Bundle immediately [notify True])
 
 recvLoop :: Connection -> IO ()
 recvLoop c@(Connection t _ _) = do
     osc <- OSC.recv t
-    withMVar (listeners c) (\(ListenerMap h _) -> mapM_ (\(_, l) -> l osc) =<< Hash.toList h)
+    withMVar (listeners c) (ListenerMap.broadcast osc)
     recvLoop c
 
+-- | Create a new connection given the initial server state and an OSC transport.
 new :: Transport t => State -> t -> IO Connection
 new s t = do
-    ios <- IOState.fromState s
-    h  <- Hash.new (==) Hash.hashInt
-    lm <- newMVar (ListenerMap h 0)
+    ios <- newMVar s
+    lm <- newMVar =<< ListenerMap.empty
     let c = Connection t ios lm
     _ <- forkIO $ recvLoop c
     initServer c
     return c
 
+-- | Close the connection.
+--
+-- The behavior of sending messages after closing the connection is undefined.
 close :: Connection -> IO ()
 close (Connection t _ _) = OSC.close t
 
-fork :: Connection -> (Connection -> IO ()) -> IO ThreadId
-fork c f = forkIO (f c)
+-- ====================================================================
+-- Allocation
 
-addListener :: Listener -> Connection -> IO ListenerId
-addListener l c = modifyMVar
-                    (listeners c) $
-                    \(ListenerMap h lid) -> do
-                        Hash.insert h lid l
-                        -- lc <- Hash.longestChain h
-                        -- putStrLn $ "addListener: longestChain=" ++ show (length lc)
-                        return (ListenerMap h (lid+1), lid)
+withAllocator :: Connection -> Allocator a -> (a -> IO (b, a)) -> IO b
+withAllocator c a f = modifyMVar (state c) $ \s -> do
+    let x = s ^. a
+    (i, x') <- f x
+    return $ (a ^= x' $ s, i)
 
-removeListener :: ListenerId -> Connection -> IO ()
-removeListener uid c = modifyMVar_ (listeners c) (\lm@(ListenerMap h _) -> Hash.delete h uid >> return lm)
+withAllocator_ :: Connection -> Allocator a -> (a -> IO a) -> IO ()
+withAllocator_ c a f = withAllocator c a $ liftM ((,)()) . f
 
-async :: OSC -> Connection -> IO ()
-async osc (Connection t _ _) = OSC.send t osc
+alloc :: IdAllocator a => Connection -> Allocator a -> IO (Id a)
+alloc c a = withAllocator c a Alloc.alloc
 
-syncWith :: OSC -> (OSC -> Maybe a) -> Connection -> IO a
-syncWith s f c = do
+free :: IdAllocator a => Connection -> Allocator a -> Id a -> IO ()
+free c a = withAllocator_ c a . Alloc.free
+
+allocMany :: IdAllocator a => Connection -> Allocator a -> Int -> IO [Id a]
+allocMany c a = withAllocator c a . Alloc.allocMany
+
+freeMany :: IdAllocator a => Connection -> Allocator a -> [Id a] -> IO ()
+freeMany c a = withAllocator_ c a . Alloc.freeMany
+
+allocRange :: RangeAllocator a => Connection -> Allocator a -> Int -> IO (Range (Id a))
+allocRange c a = withAllocator c a . Alloc.allocRange
+
+freeRange :: RangeAllocator a => Connection -> Allocator a -> Range (Id a) -> IO ()
+freeRange c a = withAllocator_ c a . Alloc.freeRange
+
+-- ====================================================================
+-- Communication and synchronization
+
+-- | Create a listener from an IO action and a notification.
+mkListener :: (a -> IO ()) -> Notification a -> Listener
+mkListener f n osc =
+    case n `match` osc of
+        Nothing -> return ()
+        Just a  -> f a
+
+-- | Add a listener.
+--
+-- Listeners are entered in a hash table, although the allocation behavior may be more stack-like.
+addListener :: Connection -> Listener -> IO ListenerId
+addListener c l = modifyMVar (listeners c) $ \lm -> do
+    (uid, lm') <- ListenerMap.add l lm
+    return (lm', uid)
+
+-- | Remove a listener.
+removeListener :: Connection -> ListenerId -> IO ()
+removeListener c uid = modifyMVar_ (listeners c) (ListenerMap.delete uid)
+
+-- | Send an OSC packet asynchronously.
+send :: Connection -> OSC -> IO ()
+send (Connection t _ _) = OSC.send t
+
+-- | Send an OSC packet and wait for a notification.
+--
+-- Returns the transformed value.
+waitFor :: Connection -> OSC -> Notification a -> IO a
+waitFor c osc n = do
     res <- newEmptyMVar
-    uid <- addListener (action res) c
-    s `async` c
+    uid <- addListener c (mkListener (putMVar res) n)
+    send c osc
     a <- takeMVar res
-    removeListener uid c
+    removeListener c uid
     return a
-    where
-        action res osc = do
-            case f osc of
-                Nothing -> return ()
-                Just a  -> putMVar res a
 
--- | Wait for an OSC message matching a specific address.
-syncAddress :: OSC -> String -> Connection -> IO OSC
-syncAddress s a = s `syncWith` hasAddress
-    where
-        hasAddress m@(Message a' _) = if a == a' then Just m else Nothing
-        hasAddress _                = Nothing
+-- | Send an OSC packet and wait for a notification.
+--
+-- Ignores the transformed value.
+waitFor_ :: Connection -> OSC -> Notification a -> IO ()
+waitFor_ c osc n = void $ waitFor c osc n
 
+-- | Send an OSC packet and wait for a list of notifications.
+--
+-- Returns the transformed values, in unspecified order.
+waitForAll :: Connection -> OSC -> [Notification a] -> IO [a]
+waitForAll c osc [] =
+    send c osc >> return []
+waitForAll c osc ns = do
+    res <- newChan
+    uids <- mapM (addListener c . mkListener (writeChan res)) ns
+    send c osc
+    as <- replicateM (length ns) (readChan res)
+    mapM_ (removeListener c) uids
+    return as
+
+-- | Send an OSC packet and wait for a list of notifications.
+--
+-- Ignores the transformed values.
+waitForAll_ :: Connection -> OSC -> [Notification a] -> IO ()
+waitForAll_ c osc ns = void $ waitForAll c osc ns
+
 -- | Append a @\/sync@ message to an OSC packet.
 appendSync :: OSC -> SyncId -> OSC
 appendSync p i =
@@ -107,14 +175,14 @@
         (Bundle t xs)   -> Bundle t (xs ++ [s])
     where s = Message "/sync" [Int (fromIntegral i)]
 
-sync :: OSC -> Connection -> IO ()
-sync osc c = do
-    i <- IOState.alloc State.syncId (state c)
-    _ <- osc `appendSync` i `syncWith` synced i $ c
-    IOState.free State.syncId (state c) i
-    return ()
+-- | Send an OSC packet and wait for the synchronization barrier.
+sync :: Connection -> OSC -> IO ()
+sync c osc = do
+    i <- alloc c State.syncIdAllocator
+    waitFor_ c (osc `appendSync` i) (synced i)
+    free c State.syncIdAllocator i
 
 -- NOTE: This is only guaranteed to work with a transport that preserves
 -- packet order. NOTE 2: And not even then ;)
 unsafeSync :: Connection -> IO ()
-unsafeSync = sync (Bundle immediately [])
+unsafeSync c = sync c (Bundle immediately [])
diff --git a/Sound/SC3/Server/Connection/ListenerMap.hs b/Sound/SC3/Server/Connection/ListenerMap.hs
new file mode 100644
--- /dev/null
+++ b/Sound/SC3/Server/Connection/ListenerMap.hs
@@ -0,0 +1,6 @@
+module Sound.SC3.Server.Connection.ListenerMap
+  ( module Sound.SC3.Server.Connection.ListenerMap.HashTable
+  ) where
+
+import Sound.SC3.Server.Connection.ListenerMap.HashTable
+-- import Sound.SC3.Server.Connection.ListenerMap.List
diff --git a/Sound/SC3/Server/Connection/ListenerMap/HashTable.hs b/Sound/SC3/Server/Connection/ListenerMap/HashTable.hs
new file mode 100644
--- /dev/null
+++ b/Sound/SC3/Server/Connection/ListenerMap/HashTable.hs
@@ -0,0 +1,34 @@
+module Sound.SC3.Server.Connection.ListenerMap.HashTable where
+
+
+import qualified Data.HashTable as Hash
+import           Sound.OpenSoundControl (OSC)
+
+type ListenerId  = Int
+type Listener    = OSC -> IO ()
+data ListenerMap = ListenerMap !(Hash.HashTable ListenerId Listener) !ListenerId
+
+broadcast :: OSC -> ListenerMap -> IO ()
+broadcast osc (ListenerMap h _) = mapM_ (\(_, l) -> l osc) =<< Hash.toList h
+
+empty :: IO ListenerMap
+empty = do
+    h  <- Hash.new (==) Hash.hashInt
+    return $ ListenerMap h 0
+
+-- | Add a listener.
+--
+-- Listeners are entered in a hash table, although the allocation behavior may be more stack-like.
+add :: Listener -> ListenerMap -> IO (ListenerId, ListenerMap)
+add l (ListenerMap h lid) = do
+    Hash.insert h lid l
+    -- xs <- Hash.toList h
+    -- lc <- Hash.longestChain h
+    -- putStrLn $ "addListener: n=" ++ show (length xs) ++ " lc=" ++ show (length lc)
+    return (lid, ListenerMap h (lid+1))
+
+-- | Remove a listener.
+delete :: ListenerId -> ListenerMap -> IO ListenerMap
+delete uid lm@(ListenerMap h _) = do
+    Hash.delete h uid
+    return lm
diff --git a/Sound/SC3/Server/Connection/ListenerMap/List.hs b/Sound/SC3/Server/Connection/ListenerMap/List.hs
new file mode 100644
--- /dev/null
+++ b/Sound/SC3/Server/Connection/ListenerMap/List.hs
@@ -0,0 +1,26 @@
+module Sound.SC3.Server.Connection.ListenerMap.List where
+
+import Sound.OpenSoundControl (OSC)
+
+type ListenerId  = Int
+type Listener    = OSC -> IO ()
+data ListenerMap = ListenerMap [(ListenerId, Listener)] !ListenerId
+
+broadcast :: OSC -> ListenerMap -> IO ()
+broadcast osc (ListenerMap m _) = mapM_ (\(_, l) -> l osc) m
+
+empty :: IO ListenerMap
+empty = return $ ListenerMap [] 0
+
+add :: Listener -> ListenerMap -> IO (ListenerId, ListenerMap)
+add l (ListenerMap m i) = return (i, ListenerMap ((i,l):m) (i+1))
+
+deletePred :: (a -> Bool) -> [a] -> [a]
+deletePred _ [] = []
+deletePred f (x:xs) =
+    if f x
+    then xs
+    else x : deletePred f xs
+
+delete :: ListenerId -> ListenerMap -> IO ListenerMap
+delete uid (ListenerMap m i) = return (ListenerMap  (deletePred (\(uid', _) -> uid' == uid) m) i)
diff --git a/Sound/SC3/Server/Monad.hs b/Sound/SC3/Server/Monad.hs
--- a/Sound/SC3/Server/Monad.hs
+++ b/Sound/SC3/Server/Monad.hs
@@ -1,114 +1,161 @@
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-module Sound.SC3.Server.Monad (
-  -- *Server Monad
+{-# LANGUAGE FlexibleContexts
+           , GeneralizedNewtypeDeriving
+           , MultiParamTypeClasses #-}
+module Sound.SC3.Server.Monad
+  ( -- * Server Monad
     ServerT
   , runServerT
   , Server
   , runServer
-  , lift
   , liftIO
-  , rootNode
-  -- *Allocators
+  , connection
+  -- * Server options
+  , MonadServer(..)
+  , serverOption
+  -- * Allocation
+  , Allocator
   , BufferId
+  , BufferIdAllocator
+  , bufferIdAllocator
   , BusId
+  , BusIdAllocator
+  , audioBusIdAllocator
+  , controlBusIdAllocator
   , NodeId
-  , nodeId
-  , bufferId
-  , busId
-  , alloc
-  , allocMany
-  , free
-  , allocRange
-  , freeRange
-  -- *Synchronization
-  , fork
-  , async
-  , syncWith
-  , syncAddress
+  , NodeIdAllocator
+  , nodeIdAllocator
+  , Range
+  , MonadIdAllocator(..)
+  -- * Communication and synchronization
+  , MonadSendOSC(..)
+  , MonadRecvOSC(..)
+  , SyncId
+  , SyncIdAllocator
+  , syncIdAllocator
   , sync
   , unsafeSync
-) where
+  -- * Concurrency
+  , fork
+  ) where
 
-import           Control.Concurrent (ThreadId)
+import           Control.Applicative
+import           Control.Concurrent (ThreadId, forkIO)
 import           Control.Concurrent.MVar.Strict
-import           Control.Monad.Reader (MonadReader, ReaderT(..), ask, asks, lift)
-import           Control.Monad.Trans (MonadIO, MonadTrans, liftIO)
+import           Control.Monad (MonadPlus, liftM)
+import           Control.Monad.Fix (MonadFix)
+import           Control.Monad.IO.Class (MonadIO, liftIO)
+import           Control.Monad.Trans.Reader (ReaderT(..), ask, asks)
+import           Control.Monad.Trans.Class (MonadTrans)
 import           Data.Accessor
-import           Sound.SC3 (Rate(..))
+import           Sound.OpenSoundControl (OSC)
 import           Sound.SC3.Server.Allocator (Id, IdAllocator, RangeAllocator, Range)
 import           Sound.SC3.Server.Connection (Connection)
 import qualified Sound.SC3.Server.Connection as C
--- import           Sound.SC3.Server.Process.Options (ServerOptions, numberOfInputBusChannels, numberOfOutputBusChannels)
-import           Sound.SC3.Server.State (BufferId, BufferIdAllocator, BusId, BusIdAllocator, NodeId, NodeIdAllocator, State)
+import           Sound.SC3.Server.Notification (Notification)
+import           Sound.SC3.Server.Options (ServerOptions)
+import           Sound.SC3.Server.State ( Allocator
+                                        , BufferId, BufferIdAllocator, bufferIdAllocator
+                                        , BusId, BusIdAllocator, audioBusIdAllocator, controlBusIdAllocator
+                                        , NodeId, NodeIdAllocator, nodeIdAllocator
+                                        , SyncId, SyncIdAllocator, syncIdAllocator
+                                        , State)
 import qualified Sound.SC3.Server.State as State
-import qualified Sound.SC3.Server.State.Concurrent as IOState
 
-import           Sound.OpenSoundControl (OSC)
-
 newtype ServerT m a = ServerT (ReaderT Connection m a)
-    deriving (Functor, Monad, MonadReader Connection, MonadIO, MonadTrans)
+    deriving (Alternative, Applicative, Functor, Monad, MonadFix, MonadIO, MonadPlus, MonadTrans)
 
 type Server = ServerT IO
 
-type Allocator a = Accessor State a
-
 liftConn :: MonadIO m => (Connection -> IO a) -> ServerT m a
-liftConn f = ask >>= \c -> liftIO (f c)
+liftConn f = ServerT $ ask >>= \c -> liftIO (f c)
 
 liftState :: MonadIO m => (State -> a) -> ServerT m a
-liftState f = asks C.state >>= liftIO . readMVar >>= return . f
+liftState f = ServerT $ asks C.state >>= liftIO . readMVar >>= return . f
 
+-- | Run a 'ServerT' computation given a connection and return the result.
 runServerT :: ServerT m a -> Connection -> m a
 runServerT (ServerT r) = runReaderT r
 
+-- | Run a 'Server' computation given a connection and return the result in the IO monad.
 runServer :: Server a -> Connection -> IO a
 runServer = runServerT
 
-rootNode :: MonadIO m => ServerT m NodeId
-rootNode = liftState State.rootNode
+-- | Return the connection.
+connection :: MonadIO m => ServerT m Connection
+connection = liftConn return
 
+class Monad m => MonadServer m where
+    -- | Return the server options.
+    serverOptions :: m ServerOptions
 
-nodeId :: Allocator NodeIdAllocator
-nodeId = State.nodeId
+-- | Return a server option.
+serverOption :: MonadServer m => (ServerOptions -> a) -> m a
+serverOption = flip liftM serverOptions
 
-bufferId :: Allocator BufferIdAllocator
-bufferId = State.bufferId
+-- serverOptions :: MonadIO m => ServerT m ServerOptions
+instance MonadIO m => MonadServer (ServerT m) where
+    serverOptions = liftState (getVal State.serverOptions)
 
-busId :: Rate -> Allocator BusIdAllocator
-busId AR = State.audioBusId
-busId KR = State.controlBusId
-busId r  = error ("No bus allocator for rate " ++ show r)
+-- | Monadic resource id management interface.
+class Monad m => MonadIdAllocator m where
+    -- | Return the root node id.
+    rootNodeId :: m NodeId
 
+    -- | Allocate an id using the given allocator.
+    alloc :: (IdAllocator a) => Allocator a -> m (Id a)
 
-alloc :: (IdAllocator a, MonadIO m) => Allocator a -> ServerT m (Id a)
-alloc a = asks C.state >>= liftIO . IOState.alloc a
+    -- | Free an id using the given allocator.
+    free :: (IdAllocator a) => Allocator a -> Id a -> m ()
 
-allocMany :: (IdAllocator a, MonadIO m) => Allocator a -> Int -> ServerT m [Id a]
-allocMany a n = asks C.state >>= liftIO . flip (IOState.allocMany a) n
+    -- | Allocate a number of ids using the given allocator.
+    allocMany :: (IdAllocator a) => Allocator a -> Int -> m [Id a]
 
-free :: (IdAllocator a, MonadIO m) => Allocator a -> Id a -> ServerT m ()
-free a i = asks C.state >>= liftIO . flip (IOState.free a) i
+    -- | Free a number of ids using the given allocator.
+    freeMany :: (IdAllocator a) => Allocator a -> [Id a] -> m ()
 
-allocRange :: (RangeAllocator a, MonadIO m) => Allocator a -> Int -> ServerT m (Range (Id a))
-allocRange a n = asks C.state >>= liftIO . flip (IOState.allocRange a) n
+    -- | Allocate a contiguous range of ids using the given allocator.
+    allocRange :: (RangeAllocator a) => Allocator a -> Int -> m (Range (Id a))
 
-freeRange :: (RangeAllocator a, MonadIO m) => Allocator a -> Range (Id a) -> ServerT m ()
-freeRange a r = asks C.state >>= liftIO . flip (IOState.freeRange a) r
+    -- | Free a contiguous range of ids using the given allocator.
+    freeRange :: (RangeAllocator a) => Allocator a -> Range (Id a) -> m ()
 
-fork :: (MonadIO m) => ServerT IO () -> ServerT m ThreadId
-fork = liftConn . flip C.fork . runServerT
+instance (MonadIO m) => MonadIdAllocator (ServerT m) where
+    rootNodeId = liftState State.rootNodeId
+    alloc a = liftConn $ \c -> C.alloc c a
+    free a i = liftConn $ \c -> C.free c a i
+    allocMany a n = liftConn $ \c -> C.allocMany c a n
+    freeMany a is = liftConn $ \c -> C.freeMany c a is
+    allocRange a n = liftConn $ \c -> C.allocRange c a n
+    freeRange a r = liftConn $ \c -> C.freeRange c a r
 
-async :: (MonadIO m) => OSC -> ServerT m ()
-async = liftConn . C.async
+class Monad m => MonadSendOSC m where
+    send :: OSC -> m ()
 
-syncWith :: (MonadIO m) => OSC -> (OSC -> Maybe a) -> ServerT m a
-syncWith s = liftConn . C.syncWith s
+instance MonadIO m => MonadSendOSC (ServerT m) where
+    send osc = liftConn $ \c -> C.send c osc
 
-syncAddress :: (MonadIO m) => OSC -> String -> ServerT m OSC
-syncAddress s = liftConn . C.syncAddress s
+class Monad m => MonadRecvOSC m where
+    -- | Wait for a notification and return the result.
+    waitFor :: OSC -> Notification a -> m a
+    -- | Wait for a notification and ignore the result.
+    waitFor_ :: OSC -> Notification a -> m ()
+    -- | Wait for a set of notifications and return their results in unspecified order.
+    waitForAll :: OSC -> [Notification a] -> m [a]
+    -- | Wait for a set of notifications and ignore their results.
+    waitForAll_ :: OSC -> [Notification a] -> m ()
 
+instance MonadIO m => MonadRecvOSC (ServerT m) where
+    waitFor osc n = liftConn $ \c -> C.waitFor c osc n
+    waitFor_ osc n = liftConn $ \c -> C.waitFor_ c osc n
+    waitForAll osc ns = liftConn $ \c -> C.waitForAll c osc ns
+    waitForAll_ osc ns = liftConn $ \c -> C.waitForAll_ c osc ns
+
 sync :: (MonadIO m) => OSC -> ServerT m ()
-sync = liftConn . C.sync
+sync osc = liftConn $ \c -> C.sync c osc
 
 unsafeSync :: (MonadIO m) => ServerT m ()
 unsafeSync = liftConn C.unsafeSync
+
+-- | Fork a computation in a new thread and return the thread id.
+fork :: (MonadIO m) => ServerT IO () -> ServerT m ThreadId
+fork a = liftConn $ \c -> forkIO (runServerT a c)
diff --git a/Sound/SC3/Server/Monad/Command.hs b/Sound/SC3/Server/Monad/Command.hs
new file mode 100644
--- /dev/null
+++ b/Sound/SC3/Server/Monad/Command.hs
@@ -0,0 +1,476 @@
+{-# LANGUAGE ExistentialQuantification
+           , FlexibleContexts
+           , FlexibleInstances
+           , GeneralizedNewtypeDeriving
+           , MultiParamTypeClasses #-}
+module Sound.SC3.Server.Monad.Command
+  (
+  -- * Master controls
+    status
+  , PrintLevel(..)
+  , dumpOSC
+  -- * Synth definitions
+  , SynthDef(name)
+  -- , d_recv
+  -- , d_load
+  -- , d_loadDir
+  , d_named
+  , d_default
+  , d_new
+  , d_free
+  -- * Resources
+  -- ** Nodes
+  , Node(..)
+  , AddAction(..)
+  , n_after
+  , n_before
+  , n_fill
+  , n_free
+  , BusMapping(..)
+  , n_query_
+  , n_query
+  , n_run_
+  , n_set
+  , n_setn
+  , n_trace
+  , n_order
+  -- *** Synths
+  , Synth(..)
+  , s_new
+  , s_new_
+  , s_release
+  -- *** Groups
+  , Group(..)
+  , rootNode
+  , g_new
+  , g_new_
+  , g_deepFree
+  , g_freeAll
+  , g_head
+  , g_tail
+  , g_dumpTree
+  -- ** Buffers
+  , Buffer
+  , b_alloc
+  , b_read
+  , HeaderFormat(..)
+  , SampleFormat(..)
+  , b_write
+  , b_free
+  , b_zero
+  , b_query
+  -- ** Buses
+  , Bus(..)
+  , busId
+  , numChannels
+  , AudioBus(..)
+  , inputBus
+  , outputBus
+  , newAudioBus
+  , ControlBus(..)
+  , newControlBus
+  ) where
+
+--import qualified Codec.Compression.BZip as BZip
+--import qualified Codec.Digest.SHA as SHA
+import           Control.Arrow (first)
+import           Control.Failure (Failure, failure)
+import           Control.Monad (liftM)
+import           Control.Monad.IO.Class (MonadIO)
+import           Sound.SC3 (Rate(..), UGen)
+import qualified Sound.SC3.Server.Allocator.Range as Range
+import           Sound.SC3.Server.Monad hiding (sync, unsafeSync)
+import qualified Sound.SC3.Server.Monad as M
+import           Sound.SC3.Server.Monad.Send
+import qualified Sound.SC3.Server.State as State
+import qualified Sound.SC3.Server.Synthdef as Synthdef
+import           Sound.SC3.Server.Allocator (AllocFailure(..))
+import           Sound.SC3.Server.Command (AddAction(..), PrintLevel(..))
+import qualified Sound.SC3.Server.Command as C
+import qualified Sound.SC3.Server.Command.Completion as C
+import qualified Sound.SC3.Server.Notification as N
+import           Sound.SC3.Server.Options (ServerOptions(..))
+import           Sound.OpenSoundControl (OSC(..))
+
+-- ====================================================================
+-- Utils
+
+-- | Construct a function suitable for 'mkAsync'.
+mkC :: a -> (OSC -> a) -> (Maybe OSC -> a)
+mkC f _ Nothing    = f
+mkC _ f (Just osc) = f osc
+
+-- ====================================================================
+-- Master controls
+
+status :: MonadIO m => SendT m (Deferred m N.Status)
+status = send C.status >> after N.status_reply (return ())
+
+dumpOSC :: MonadIO m => PrintLevel -> SendT m ()
+dumpOSC p = do
+    i <- M.alloc M.syncIdAllocator
+    send (C.dumpOSC p)
+    send (C.sync (fromIntegral i))
+    after_ (N.synced i) (return ())
+
+-- ====================================================================
+-- Synth definitions
+
+newtype SynthDef = SynthDef {
+    name  :: String
+  } deriving (Eq, Show)
+
+d_named :: String -> SynthDef
+d_named = SynthDef
+
+d_default :: SynthDef
+d_default = d_named "default"
+
+-- | Compute a unique name for a UGen graph.
+-- graphName :: UGen -> String
+-- graphName = SHA.showBSasHex . SHA.hash SHA.SHA256 . BZip.compress . Synthdef.graphdef . Synthdef.synth
+
+-- | Create a new synth definition.
+-- d_new :: Monad m => String -> UGen -> Async m SynthDef
+-- d_new prefix ugen
+--     | length prefix < 127 = mkAsync $ return (sd, f)
+--     | otherwise = error "d_new: name prefix too long, resulting string exceeds 255 characters"
+--     where
+--         sd = SynthDef (prefix ++ "-" ++ graphName ugen)
+--         f osc = (mkC C.d_recv C.d_recv' osc) (Synthdef.synthdef (name sd) ugen)
+d_new :: Monad m => String -> UGen -> Async m SynthDef
+d_new name ugen
+    | length name < 255 = mkAsync $ return (SynthDef name, f)
+    | otherwise = error "d_new: name too long, resulting string exceeds 255 characters"
+    where
+        f osc = (mkC C.d_recv C.d_recv' osc) (Synthdef.synthdef name ugen)
+
+-- | Remove definition once all nodes using it have ended.
+d_free :: Monad m => SynthDef -> SendT m ()
+d_free = send . C.d_free . (:[]) . name
+
+-- ====================================================================
+-- Node
+
+class Node a where
+    nodeId :: a -> NodeId
+
+data AbstractNode = forall n . (Eq n, Node n, Show n) => AbstractNode n
+
+instance Eq AbstractNode where
+    (AbstractNode a) == (AbstractNode b) = nodeId a == nodeId b
+
+instance Node AbstractNode where
+    nodeId (AbstractNode n) = nodeId n
+
+instance Show AbstractNode where
+    show (AbstractNode n) = show n
+
+n_wrap :: (Eq n, Node n, Show n) => n -> AbstractNode
+n_wrap = AbstractNode
+
+-- | Place node @a@ after node @b@.
+n_after :: (Node a, Node b, Monad m) => a -> b -> SendT m ()
+n_after a b = send $ C.n_after [(fromIntegral (nodeId a), fromIntegral (nodeId b))]
+
+-- | Place node @a@ before node @b@.
+n_before :: (Node a, Node b, Monad m) => a -> b -> SendT m ()
+n_before a b = send $ C.n_after [(fromIntegral (nodeId a), fromIntegral (nodeId b))]
+
+-- | Fill ranges of a node's control values.
+n_fill :: (Node a, Monad m) => a -> [(String, Int, Double)] -> SendT m ()
+n_fill n = send . C.n_fill (fromIntegral (nodeId n))
+
+-- | Delete a node.
+n_free :: (Node a, MonadIO m) => a -> SendT m ()
+n_free n = do
+    send $ C.n_free [fromIntegral (nodeId n)]
+    finally $ M.free M.nodeIdAllocator (nodeId n)
+
+-- | Mapping node controls to buses.
+class BusMapping n b where
+    -- | Map a node's controls to read from a control bus.
+    n_map :: (Node n, Bus b, Monad m) => n -> String -> b -> SendT m ()
+    -- | Remove a control's mapping to a control bus.
+    n_unmap :: (Node n, Bus b, Monad m) => n -> String -> b -> SendT m ()
+
+instance BusMapping n ControlBus where
+    n_map n c b = send msg
+        where
+            nid = fromIntegral (nodeId n)
+            bid = fromIntegral (busId b)
+            msg = if numChannels b > 1
+                  then C.n_mapn nid [(c, bid, numChannels b)]
+                  else C.n_map  nid [(c, bid)]
+    n_unmap n c b = send msg
+        where
+            nid = fromIntegral (nodeId n)
+            msg = if numChannels b > 1
+                  then C.n_mapn nid [(c, -1, numChannels b)]
+                  else C.n_map  nid [(c, -1)]
+
+instance BusMapping n AudioBus where
+    n_map n c b = send msg
+        where
+            nid = fromIntegral (nodeId n)
+            bid = fromIntegral (busId b)
+            msg = if numChannels b > 1
+                  then C.n_mapan nid [(c, bid, numChannels b)]
+                  else C.n_mapa  nid [(c, bid)]
+    n_unmap n c b = send msg
+        where
+            nid = fromIntegral (nodeId n)
+            msg = if numChannels b > 1
+                  then C.n_mapan nid [(c, -1, numChannels b)]
+                  else C.n_mapa  nid [(c, -1)]
+
+-- | Query a node.
+n_query_ :: (Node a, Monad m) => a -> SendT m ()
+n_query_ n = send $ C.n_query [fromIntegral (nodeId n)]
+
+-- | Query a node.
+n_query :: (Node a, MonadIO m) => a -> SendT m (Deferred m N.NodeNotification)
+n_query n = n_query_ n >> after (N.n_info (nodeId n)) (return ())
+
+-- | Turn node on or off.
+n_run_ :: (Node a, Monad m) => a -> Bool -> SendT m ()
+n_run_ n b = send $ C.n_run [(fromIntegral (nodeId n), b)]
+
+-- | Set a node's control values.
+n_set :: (Node a, Monad m) => a -> [(String, Double)] -> SendT m ()
+n_set n = send . C.n_set (fromIntegral (nodeId n))
+
+-- | Set ranges of a node's control values.
+n_setn :: (Node a, Monad m) => a -> [(String, [Double])] -> SendT m ()
+n_setn n = send . C.n_setn (fromIntegral (nodeId n))
+
+-- | Trace a node.
+n_trace :: (Node a, Monad m) => a -> SendT m ()
+n_trace n = send $ C.n_trace [fromIntegral (nodeId n)]
+
+-- | Move an ordered sequence of nodes.
+n_order :: (Node n, Monad m) => AddAction -> n -> [AbstractNode] -> SendT m ()
+n_order a n = send . C.n_order a (fromIntegral (nodeId n)) . map (fromIntegral.nodeId)
+
+-- ====================================================================
+-- Synth
+
+newtype Synth = Synth NodeId deriving (Eq, Ord, Show)
+
+instance Node Synth where
+    nodeId (Synth nid) = nid
+
+s_new :: MonadIO m => SynthDef -> AddAction -> Group -> [(String, Double)] -> SendT m Synth
+s_new d a g xs = do
+    nid <- M.alloc M.nodeIdAllocator
+    send $ C.s_new (name d) (fromIntegral nid) a (fromIntegral (nodeId g)) xs
+    return $ Synth nid
+
+s_new_ :: MonadIO m => SynthDef -> AddAction -> [(String, Double)] -> SendT m Synth
+s_new_ d a xs = rootNode >>= \g -> s_new d a g xs
+
+s_release :: (Node a, MonadIO m) => Double -> a -> SendT m ()
+s_release r n = do
+    send (C.n_set1 (fromIntegral nid) "gate" r)
+    after_ (N.n_end_ nid) (M.free M.nodeIdAllocator nid)
+    where nid = nodeId n
+
+-- ====================================================================
+-- Group
+
+newtype Group = Group NodeId deriving (Eq, Ord, Show)
+
+instance Node Group where
+    nodeId (Group nid) = nid
+
+rootNode :: MonadIdAllocator m => m Group
+rootNode = liftM Group M.rootNodeId
+
+g_new :: MonadIO m => AddAction -> Group -> SendT m Group
+g_new a p = do
+    nid <- M.alloc State.nodeIdAllocator
+    send $ C.g_new [(fromIntegral nid, a, fromIntegral (nodeId p))]
+    return $ Group nid
+
+g_new_ :: MonadIO m => AddAction -> SendT m Group
+g_new_ a = rootNode >>= g_new a
+
+g_deepFree :: Monad m => Group -> SendT m ()
+g_deepFree g = send $ C.g_deepFree [fromIntegral (nodeId g)]
+
+g_freeAll :: Monad m => Group -> SendT m ()
+g_freeAll g = send $ C.g_freeAll [fromIntegral (nodeId g)]
+
+g_head :: (Node n, Monad m) => Group -> n -> SendT m ()
+g_head g n = send $ C.g_head [(fromIntegral (nodeId g), fromIntegral (nodeId n))]
+
+g_tail :: (Node n, Monad m) => Group -> n -> SendT m ()
+g_tail g n = send $ C.g_tail [(fromIntegral (nodeId g), fromIntegral (nodeId n))]
+
+g_dumpTree :: Monad m => [(Group, Bool)] -> SendT m ()
+g_dumpTree = send . C.g_dumpTree . map (first (fromIntegral . nodeId))
+
+-- ====================================================================
+-- Buffer
+
+newtype Buffer = Buffer { bufferId :: BufferId } deriving (Eq, Ord, Show)
+
+b_alloc :: MonadIO m => Int -> Int -> Async m Buffer
+b_alloc n c = mkAsync $ do
+    bid <- M.alloc State.bufferIdAllocator
+    let f osc = (mkC C.b_alloc C.b_alloc' osc) (fromIntegral bid) n c
+    return (Buffer bid, f)
+
+b_read :: MonadIO m =>
+    Buffer
+ -> FilePath
+ -> Maybe Int
+ -> Maybe Int
+ -> Maybe Int
+ -> Bool
+ -> Async m ()
+b_read (Buffer bid) path
+       fileOffset numFrames bufferOffset
+       leaveOpen = mkAsync_ f
+    where
+        f osc = (mkC C.b_read C.b_read' osc)
+                    (fromIntegral bid) path
+                    (maybe 0 id fileOffset)
+                    (maybe (-1) id numFrames)
+                    (maybe 0 id bufferOffset)
+                    leaveOpen
+
+data HeaderFormat =
+    Aiff
+  | Next
+  | Wav
+  | Ircam
+  | Raw
+  deriving (Enum, Eq, Read, Show)
+
+data SampleFormat =
+    PcmInt8
+  | PcmInt16
+  | PcmInt24
+  | PcmInt32
+  | PcmFloat
+  | PcmDouble
+  | PcmMulaw
+  | PcmAlaw
+  deriving (Enum, Eq, Read, Show)
+
+headerFormatString :: HeaderFormat -> String
+headerFormatString Aiff  = "aiff"
+headerFormatString Next  = "next"
+headerFormatString Wav   = "wav"
+headerFormatString Ircam = "ircam"
+headerFormatString Raw   = "raw"
+
+sampleFormatString :: SampleFormat -> String
+sampleFormatString PcmInt8   = "int8"
+sampleFormatString PcmInt16  = "int16"
+sampleFormatString PcmInt24  = "int24"
+sampleFormatString PcmInt32  = "int32"
+sampleFormatString PcmFloat  = "float"
+sampleFormatString PcmDouble = "double"
+sampleFormatString PcmMulaw  = "mulaw"
+sampleFormatString PcmAlaw   = "alaw"
+
+b_write :: MonadIO m =>
+    Buffer
+ -> FilePath
+ -> HeaderFormat
+ -> SampleFormat
+ -> Maybe Int
+ -> Maybe Int
+ -> Bool
+ -> Async m ()
+b_write (Buffer bid) path
+        headerFormat sampleFormat
+        fileOffset numFrames
+        leaveOpen = mkAsync_ f
+    where
+        f osc = (mkC C.b_write C.b_write' osc)
+                    (fromIntegral bid) path
+                    (headerFormatString headerFormat)
+                    (sampleFormatString sampleFormat)
+                    (maybe 0 id fileOffset)
+                    (maybe (-1) id numFrames)
+                    leaveOpen
+
+b_free :: MonadIO m => Buffer -> Async m ()
+b_free b = mkAsync $ do
+    let bid = bufferId b
+    M.free State.bufferIdAllocator bid
+    let f osc = (mkC C.b_free C.b_free' osc) (fromIntegral bid)
+    return ((), f)
+
+b_zero :: MonadIO m => Buffer -> Async m ()
+b_zero (Buffer bid) = mkAsync_ f
+    where
+        f osc = (mkC C.b_zero C.b_zero' osc) (fromIntegral bid)
+
+b_query :: MonadIO m => Buffer -> SendT m (Deferred m N.BufferInfo)
+b_query (Buffer bid) = do
+    send (C.b_query [fromIntegral bid])
+    after (N.b_info bid) (return ())
+
+-- ====================================================================
+-- Bus
+
+-- | Abstract interface for control and audio rate buses.
+class Bus a where
+    rate :: a -> Rate
+    busIdRange :: a -> Range BusId
+    freeBus :: MonadIdAllocator m => a -> m ()
+
+-- | Bus id.
+busId :: Bus a => a -> BusId
+busId = Range.begin . busIdRange
+
+-- | Number of channels of the bus.
+numChannels :: Bus a => a -> Int
+numChannels = Range.size . busIdRange
+
+-- | Audio bus.
+newtype AudioBus = AudioBus { audioBusId :: Range BusId } deriving (Eq, Show)
+
+instance Bus AudioBus where
+    rate _ = AR
+    busIdRange = audioBusId
+    freeBus = M.freeRange M.audioBusIdAllocator . audioBusId
+
+-- | Allocate audio bus with the specified number of channels.
+newAudioBus :: MonadIdAllocator m => Int -> m AudioBus
+newAudioBus = liftM AudioBus . M.allocRange M.audioBusIdAllocator
+
+-- | Get hardware input bus.
+inputBus :: (MonadServer m, Failure AllocFailure m) => Int -> Int -> m AudioBus
+inputBus n i = do
+    k <- serverOption numberOfOutputBusChannels
+    m <- serverOption numberOfInputBusChannels
+    let r = Range.sized n (fromIntegral (k+i))
+    if Range.begin r < fromIntegral k || Range.end r >= fromIntegral (k+m)
+        then failure InvalidId
+        else return (AudioBus r)
+
+-- | Get hardware output bus.
+outputBus :: (MonadServer m, Failure AllocFailure m) => Int -> Int -> m AudioBus
+outputBus n i = do
+    k <- serverOption numberOfOutputBusChannels
+    let r = Range.sized n (fromIntegral i)
+    if Range.begin r < 0 || Range.end r >= fromIntegral k
+        then failure InvalidId
+        else return (AudioBus r)
+
+-- | Control bus.
+newtype ControlBus = ControlBus { controlBusId :: Range BusId } deriving (Eq, Show)
+
+instance Bus ControlBus where
+    rate _ = KR
+    busIdRange = controlBusId
+    freeBus = M.freeRange M.controlBusIdAllocator . controlBusId
+
+-- | Allocate control bus with the specified number of channels.
+newControlBus :: MonadIdAllocator m => Int -> m ControlBus
+newControlBus = liftM ControlBus . M.allocRange M.controlBusIdAllocator
diff --git a/Sound/SC3/Server/Monad/Send.hs b/Sound/SC3/Server/Monad/Send.hs
new file mode 100644
--- /dev/null
+++ b/Sound/SC3/Server/Monad/Send.hs
@@ -0,0 +1,347 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving
+           , MultiParamTypeClasses #-}
+
+-- | This module provides abstractions for constructing bundles for server
+-- resource allocation in a type safe manner. In particular, the exposed types
+-- and functions make sure that asynchronous command results cannot be used
+-- before they have been allocated on the server.
+--
+-- TODO: Real usage example
+--
+-- > (b0, (g, ig, b)) <- immediately !> do
+-- > b0 <- async $ b_alloc 1024 1
+-- > x <- b_alloc 1024 1 `whenDone` immediately $ \b -> do
+-- >     b_free b `whenDone` OSC.UTCr t' $ \() -> do
+-- >         g <- g_new_ AddToTail
+-- >         ig <- g_new AddToTail g
+-- >         return $ pure (g, ig, b)
+-- > return $ (,) <$> b0 <*> x
+module Sound.SC3.Server.Monad.Send
+  ( SendT
+  , AllocT
+  -- * Deferred values
+  , Deferred
+  , after
+  , after_
+  , finally
+  -- * Asynchronous commands
+  , Async
+  , mkAsync
+  , mkAsync_
+  , mkAsyncCM
+  , whenDone
+  , asyncM
+  , async
+  -- * Command execution
+  , run
+  , exec
+  , (!>)
+  , execPure
+  , (~>)
+  ) where
+
+import           Control.Applicative
+import           Control.Arrow (second)
+import           Control.Monad (ap, liftM, when)
+import           Control.Monad.IO.Class (MonadIO(..))
+import qualified Control.Monad.Trans.Class as Trans
+import           Control.Monad.Trans.State (StateT(..))
+import qualified Control.Monad.Trans.State as State
+import           Data.IORef
+import           Sound.SC3.Server.Monad (MonadIdAllocator, MonadSendOSC(..), MonadServer, ServerT)
+import qualified Sound.SC3.Server.Monad as M
+import qualified Sound.SC3.Server.State as State
+import qualified Sound.SC3.Server.Command as C
+import           Sound.SC3.Server.Notification (Notification)
+import qualified Sound.SC3.Server.Notification as N
+import           Sound.OpenSoundControl (OSC(..), Time, immediately)
+
+{-
+
+goals:
+
+* after executing action and synchronizing, all server actions have been executed
+* server actions are consistent, i.e. asynchronous resources are not used before they are allocated (Deferred)
+
+async sets sync state to "needs sync"
+whenDone overrides sync state to "has sync"
+whenDone adds a sync barrier to the completion packet when its subaction didn't add one (syncIds empty); the subaction always needs to sync!
+exec adds a sync barrier when sync state is "needs sync"
+-}
+
+-- | Synchronisation state.
+data SyncState =
+    NoSync      -- ^ No synchronisation barrier needed.
+  | NeedsSync   -- ^ Need to add a synchronisation barrier to the current context.
+  | HasSync     -- ^ Synchronisation barrier already present in the current context.
+  deriving (Eq, Ord, Show)
+
+-- | Internal state used for constructing bundles from 'SendT' actions.
+data State m = State {
+    buildOSC      :: [OSC]                         -- ^ Current list of OSC messages.
+  , notifications :: [Notification (ServerT m ())] -- ^ Current list of notifications to synchronise on.
+  , cleanup       :: ServerT m ()                  -- ^ Cleanup action to deallocate resources.
+  , timeTag       :: Time                          -- ^ Time tag.
+  , syncState     :: SyncState                     -- ^ Synchronisation barrier state.
+  }
+
+-- | Construct a 'SendT' state with a given synchronisation state.
+mkState :: Monad m => Time -> SyncState -> State m
+mkState = State [] [] (return ())
+
+-- | Push an OSC packet.
+pushOSC :: OSC -> State m -> State m
+pushOSC osc s = s { buildOSC = osc : buildOSC s }
+
+-- | Return 'True' if the current context contains OSC messages.
+hasOSC :: State m -> Bool
+hasOSC = not . null . buildOSC
+
+-- | Get the list of OSC packets.
+getOSC :: State m -> [OSC]
+getOSC = reverse . buildOSC
+
+-- | Update the synchronisation state.
+setSyncState :: SyncState -> State m -> State m
+setSyncState ss s | ss > syncState s = s { syncState = ss }
+                  | otherwise        = s
+
+-- | Representation of a server-side action (or sequence of actions).
+newtype SendT m a = SendT (StateT (State m) (ServerT m) a)
+                    deriving (Applicative, Functor, Monad)
+
+instance MonadIO m => MonadServer (SendT m) where
+    serverOptions = liftServer M.serverOptions
+
+instance MonadIO m => MonadIdAllocator (SendT m) where
+    rootNodeId = liftServer M.rootNodeId
+    alloc = liftServer . M.alloc
+    free a = liftServer . M.free a
+    allocMany a = liftServer . M.allocMany a
+    freeMany a = liftServer . M.freeMany a
+    allocRange a = liftServer . M.allocRange a
+    freeRange a = liftServer . M.freeRange a
+
+-- | Bundles are flattened into the resulting bundle because @scsynth@ doesn't
+-- support nested bundles.
+instance Monad m => MonadSendOSC (SendT m) where
+    send osc@(Message _ _) = modify (pushOSC osc)
+    send (Bundle _ xs)     = mapM_ send xs
+
+-- | Execute a SendT action, returning the result and the final state.
+runSendT :: Monad m => Time -> SyncState -> SendT m a -> ServerT m (a, State m)
+runSendT t s (SendT m) = State.runStateT m (mkState t s)
+
+-- | Get a value from the state.
+gets :: Monad m => (State m -> a) -> SendT m a
+gets = SendT . State.gets
+
+-- | Modify the state in a SendT action.
+modify :: Monad m => (State m -> State m) -> SendT m ()
+modify = SendT . State.modify
+
+-- | Lift a ServerT action into SendT.
+--
+-- This is potentially unsafe and should only be used for the allocation of
+-- server resources. Lifting actions that rely on communication and
+-- synchronisation primitives will not work as expected.
+liftServer :: Monad m => ServerT m a -> SendT m a
+liftServer = SendT . Trans.lift
+
+-- | Allocation action newtype wrapper.
+newtype AllocT m a = AllocT (ServerT m a)
+                     deriving (Applicative, MonadIdAllocator, Functor, Monad)
+
+-- | Representation of a deferred server resource.
+--
+-- Deferred resource values can only be observed a return value of the 'SendT'
+-- action after 'exec' has been called.
+--
+-- Deferred has 'Applicative' and 'Functor' instances, so that complex values
+-- can be built from simple ones.
+newtype Deferred m a = Deferred { unDefer :: ServerT m a } deriving (Monad)
+
+instance Monad m => Functor (Deferred m) where
+    fmap f (Deferred a) = Deferred (liftM f a)
+
+instance Monad m => Applicative (Deferred m) where
+    pure = Deferred . return
+    (<*>) (Deferred f) (Deferred a) = Deferred (f `ap` a)
+
+-- | Construct a deferred value from an IO action.
+deferredIO :: MonadIO m => IO a -> Deferred m a
+deferredIO = Deferred . liftIO
+
+-- | Register a cleanup action, to be executed after a notification has been
+-- received and return the deferred notification result.
+after :: MonadIO m => Notification a -> AllocT m () -> SendT m (Deferred m a)
+after n (AllocT m) = do
+    v <- liftServer $ liftIO $ newIORef (error "BUG: after: uninitialized IORef")
+    modify $ \s -> s { notifications = fmap (liftIO . writeIORef v) n : notifications s
+                     , cleanup = cleanup s >> m }
+    return $ deferredIO (readIORef v)
+
+-- | Register a cleanup action, to be executed after a notification has been
+-- received and ignore the notification result.
+after_ :: Monad m => Notification a -> AllocT m () -> SendT m ()
+after_ n (AllocT m) = modify $ \s -> s { notifications = fmap (const (return ())) n : notifications s
+                                       , cleanup = cleanup s >> m }
+
+-- | Register a cleanup action, to be executed after all asynchronous commands
+-- and notification have finished.
+finally :: Monad m => AllocT m () -> SendT m ()
+finally (AllocT m) = modify $ \s -> s { cleanup = cleanup s >> m }
+
+-- | Representation of an asynchronous server command. Asynchronous commands
+-- are executed asynchronously with respect to other server commands.
+--
+-- There are two different ways of synchronising with an asynchronous command:
+--
+-- * using 'whenDone' for server-side synchronisation, or
+--
+-- * using 'async' and observing the result of a 'SendT' action after calling
+-- 'exec'.
+newtype Async m a = Async (SendT m (a, (Maybe OSC -> OSC)))
+
+-- | Create an asynchronous command from an allocation action.
+--
+-- The first return value should be a server resource allocated on the client,
+-- the second a function that, given a completion packet, returns an OSC packet
+-- that asynchronously allocates the resource on the server.
+mkAsync :: Monad m => AllocT m (a, (Maybe OSC -> OSC)) -> Async m a
+mkAsync (AllocT m) = Async (liftServer m)
+
+-- | Create an asynchronous command from a side effecting OSC function.
+mkAsync_ :: Monad m => (Maybe OSC -> OSC) -> Async m ()
+mkAsync_ f = mkAsync $ return ((), f)
+
+-- | Create an asynchronous command.
+--
+-- The completion message will be appended at the end of the returned message.
+mkAsyncCM :: Monad m => AllocT m (a, OSC) -> Async m a
+mkAsyncCM = mkAsync . liftM (second f)
+    where
+        f msg Nothing   = msg
+        f msg (Just cm) = C.withCM msg cm
+
+-- | Add a synchronisation barrier.
+addSync :: MonadIO m => SendT m a -> SendT m a
+addSync m = do
+    a <- m
+    b <- gets hasOSC
+    when b $ do
+        s <- gets syncState
+        case s of
+            NeedsSync -> do
+                sid <- liftServer $ M.alloc State.syncIdAllocator
+                send (C.sync (fromIntegral sid))
+                after_ (N.synced sid) (M.free State.syncIdAllocator sid)
+            _ -> return ()
+    return a
+
+-- | Execute an server-side action after the asynchronous command has
+-- finished.
+whenDone :: MonadIO m => Async m a -> (a -> SendT m b) -> Async m b
+whenDone (Async m) f = Async $ do
+    (a, g) <- m
+    b <- f a
+    return (b, g)
+
+-- | Execute an asynchronous command asynchronously.
+asyncM :: MonadIO m => Async m (Deferred m a) -> SendT m (Deferred m a)
+asyncM (Async m) = do
+    t <- gets timeTag
+    ((a, g), s) <- liftServer $ runSendT t NeedsSync $ addSync m
+    case getOSC s of
+        [] -> do
+            send (g Nothing)
+            modify $ \s' -> (setSyncState NeedsSync s') {
+                notifications = notifications s' ++ notifications s
+              , cleanup = cleanup s' >> cleanup s }
+        osc -> do
+            let t' = case syncState s of
+                        HasSync -> immediately
+                        _       -> t
+            send $ g (Just (Bundle t' osc))
+            modify $ \s' -> (setSyncState HasSync s') {
+                notifications = notifications s' ++ notifications s
+              , cleanup = cleanup s' >> cleanup s }
+    return a
+
+-- | Execute an asynchronous command asynchronously.
+async :: MonadIO m => Async m a -> SendT m (Deferred m a)
+async = asyncM . flip whenDone (return . pure)
+
+{-
+-- | Execute an server-side action after the asynchronous command has
+-- finished. The corresponding server commands are scheduled at a time @t@ in
+-- the future.
+whenDone :: MonadIO m => Async m a -> (a -> SendT m b) -> SendT m (Deferred b)
+whenDone (Async m) f = do
+    t <- gets timeTag
+    (a, g) <- m
+    (b, s) <- liftServer $ runSendT t NeedsSync $ addSync (f a)
+    let t' = case syncState s of
+                HasSync -> immediately
+                _       -> t
+    send $ g (Just (Bundle t' (getOSC s)))
+    modify $ \s' -> s' {
+        notifications = notifications s' ++ notifications s
+      , cleanup = cleanup s' >> cleanup s
+      , syncState = HasSync }
+    return b
+
+-- | Execute an asynchronous command asynchronously.
+async :: MonadIO m => Async m a -> SendT m (Deferred a)
+async (Async m) = do
+    (a, g) <- m
+    send (g Nothing)
+    modify $ setSyncState NeedsSync
+    return $ pure a
+-}
+
+run :: MonadIO m => Time -> SendT m (Deferred m a) -> ServerT m (ServerT m a, Maybe (OSC, [Notification (ServerT m ())]))
+run t m = do
+    (a, s) <- runSendT t NoSync $ addSync m
+    let result = cleanup s >> unDefer a
+    case getOSC s of
+        [] -> return (result, Nothing)
+        osc -> let t' = case syncState s of
+                            HasSync -> immediately
+                            _ -> t
+               in return (result, Just (Bundle t' osc, notifications s))
+
+-- | Run the 'SendT' action and return the result.
+--
+-- All asynchronous commands and notifications are guaranteed to have finished
+-- when this function returns.
+exec :: MonadIO m => Time -> SendT m (Deferred m a) -> ServerT m a
+exec t m = do
+    -- (a, s) <- runSendT t NoSync $ addSync m
+    -- case getOSC s of
+    --     [] -> return ()
+    --     osc -> do
+    --         -- liftIO $ print osc
+    --         let t' = case syncState s of
+    --                     HasSync -> immediately
+    --                     _ -> t
+    (action, sync) <- run t m
+    case sync of
+        Nothing -> return ()
+        Just (osc, ns) -> M.waitForAll osc ns >>= sequence_
+    action
+
+-- | Infix operator version of 'exec'.
+(!>) :: MonadIO m => Time -> SendT m (Deferred m a) -> ServerT m a
+(!>) = exec
+
+-- | Run a 'SendT' action that returns a pure result.
+--
+-- All asynchronous commands and notifications are guaranteed to have finished
+-- when this function returns.
+execPure :: MonadIO m => Time -> SendT m a -> ServerT m a
+execPure t m = exec t (m >>= return . pure)
+
+-- | Infix operator version of 'execPure'.
+(~>) :: MonadIO m => Time -> SendT m a -> ServerT m a
+(~>) = execPure
diff --git a/Sound/SC3/Server/Notification.hs b/Sound/SC3/Server/Notification.hs
--- a/Sound/SC3/Server/Notification.hs
+++ b/Sound/SC3/Server/Notification.hs
@@ -1,17 +1,38 @@
 -- | Server notification processors.
 module Sound.SC3.Server.Notification (
-    Status(..)
+    Notification(..)
+  , hasAddress
+  , Status(..)
   , status_reply
   , tr
   , synced
   , done
-  , NodeNotification(..)
+  , NodeNotification(nodeId, parentGroupId, previousNodeId, nextNodeId)
+  , headNodeId, tailNodeId
   , n_go , n_end , n_off , n_on , n_move , n_info
+  , n_go_, n_end_, n_off_, n_on_
+  , BufferInfo(..)
+  , b_info
 ) where
 
-import Sound.SC3.Server.State (NodeId, SyncId)
+import Sound.SC3.Server.State (BufferId, NodeId, SyncId)
 import Sound.OpenSoundControl (OSC(..), Datum(..))
 
+-- | A notification transformer, extracting a value from a matching OSC packet.
+newtype Notification a = Notification { match :: OSC -> Maybe a }
+
+instance Functor Notification where
+    fmap f = Notification . (.) (fmap f) . match
+
+-- | Wait for an OSC message matching a specific address.
+--
+-- Returns the matched OSC message.
+hasAddress :: String -> Notification OSC
+hasAddress a = Notification f
+    where
+        f m@(Message a' _) = if a == a' then Just m else Nothing
+        f _                = Nothing
+
 data Status = Status {
     numUGens          :: Int
   , numSynths         :: Int
@@ -23,56 +44,136 @@
   , actualSampleRate  :: Double
 } deriving (Eq, Show)
 
-status_reply :: OSC -> Maybe Status
-status_reply (Message "/status.reply" [Int _, Int u, Int s, Int g, Int d, Float a, Float p, Double sr, Double sr']) =
-    Just $ Status u s g d a p sr sr'
-status_reply _ = Nothing
+status_reply :: Notification Status
+status_reply = Notification f
+    where
+        f (Message "/status.reply" [Int _, Int u, Int s, Int g, Int d, Float a, Float p, Double sr, Double sr'])
+            = Just $ Status u s g d a p sr sr'
+        f _ = Nothing
 
-tr :: NodeId -> Maybe Int -> OSC -> Maybe Double
-tr n (Just i) (Message "/tr" [Int n', Int i', Float r]) | fromIntegral n == n' && i == i' = Just r
-tr n Nothing  (Message "/tr" [Int n', Int _, Float r])  | fromIntegral n == n' = Just r
-tr _ _        _                                         = Nothing
+tr :: NodeId -> Maybe Int -> Notification Double
+tr n = Notification . f
+    where
+        f (Just i) (Message "/tr" [Int n', Int i', Float r])
+            | fromIntegral n == n' && i == i' = Just r
+        f Nothing  (Message "/tr" [Int n', Int _, Float r])
+            | fromIntegral n == n' = Just r
+        f _ _ = Nothing
 
-synced :: SyncId -> OSC -> Maybe SyncId
-synced i (Message "/synced" [Int j]) | fromIntegral j == i = Just i
-synced _ _                                                 = Nothing
+synced :: SyncId -> Notification SyncId
+synced i = Notification f
+    where
+        f (Message "/synced" [Int j]) | fromIntegral j == i = Just i
+        f _                                                 = Nothing
 
 normalize :: String -> String
 normalize ('/':s) = s
 normalize s       = s
 
-done :: String -> OSC -> Maybe [Datum]
-done c (Message "/done" (String s:xs)) | normalize c == normalize s = Just xs
-done _ _                                                            = Nothing
+done :: String -> Notification [Datum]
+done c = Notification f
+    where
+        f (Message "/done" (String s:xs)) | normalize c == normalize s = Just xs
+        f _                                                            = Nothing
 
 data NodeNotification =
-    SynthNotification NodeId NodeId NodeId NodeId
-  | GroupNotification NodeId NodeId NodeId NodeId NodeId NodeId
+    SynthNotification {
+        nodeId :: NodeId
+      , parentGroupId :: NodeId
+      , previousNodeId :: Maybe NodeId
+      , nextNodeId :: Maybe NodeId
+      }
+  | GroupNotification {
+        nodeId :: NodeId
+      , parentGroupId :: NodeId
+      , previousNodeId :: Maybe NodeId
+      , nextNodeId :: Maybe NodeId
+      , _headNodeId :: Maybe NodeId
+      , _tailNodeId :: Maybe NodeId
+      }
+  deriving (Eq, Show)
 
-n_notification :: String -> NodeId -> OSC -> Maybe NodeNotification
-n_notification s nid (Message s' (Int nid':Int g:Int p:Int n:Int b:r))
-    | s == s' && fromIntegral nid == nid' =
-        case b of
-            0 -> Just $ SynthNotification nid (fromIntegral g) (fromIntegral p) (fromIntegral n)
-            _ -> case r of
-                    [Int h, Int t] -> Just $ GroupNotification nid (fromIntegral g) (fromIntegral p) (fromIntegral n) (fromIntegral h) (fromIntegral t)
-                    _              -> Just $ GroupNotification nid (fromIntegral g) (fromIntegral p) (fromIntegral n) (fromIntegral (-1 :: Int)) (fromIntegral (-1 :: Int))
-n_notification _ _ _ = Nothing
+isSynthNotification :: NodeNotification -> Bool
+isSynthNotification (SynthNotification _ _ _ _) = True
+isSynthNotification _ = False
 
-n_go :: NodeId -> OSC -> Maybe NodeNotification
+headNodeId :: NodeNotification -> Maybe NodeId
+headNodeId n | isSynthNotification n = Nothing
+             | otherwise             = _headNodeId n
+
+tailNodeId :: NodeNotification -> Maybe NodeId
+tailNodeId n | isSynthNotification n = Nothing
+             | otherwise             = _tailNodeId n
+
+n_notification :: String -> NodeId -> Notification NodeNotification
+n_notification s nid = Notification f
+    where
+        nodeIdToMaybe (-1) = Nothing
+        nodeIdToMaybe i    = Just (fromIntegral i)
+        f osc =
+            case osc of
+                (Message s' (Int nid':xs)) ->
+                    if s == s' && fromIntegral nid == nid'
+                    then case xs of
+                            (Int g:Int p:Int n:Int b:rest) ->
+                                let group = fromIntegral g
+                                    prev = nodeIdToMaybe p
+                                    next = nodeIdToMaybe n
+                                in case b of
+                                    1 -> case rest of
+                                        [Int h, Int t] -> Just $ GroupNotification nid group prev next (nodeIdToMaybe h) (nodeIdToMaybe t)
+                                        _              -> Nothing
+                                    _ -> Just $ SynthNotification nid group prev next
+                            _ -> Nothing
+                    else Nothing
+                _ -> Nothing
+
+n_go :: NodeId -> Notification NodeNotification
 n_go = n_notification "/n_go"
 
-n_end :: NodeId -> OSC -> Maybe NodeNotification
+n_end :: NodeId -> Notification NodeNotification
 n_end = n_notification "/n_end"
 
-n_off :: NodeId -> OSC -> Maybe NodeNotification
+n_off :: NodeId -> Notification NodeNotification
 n_off = n_notification "/n_off"
 
-n_on :: NodeId -> OSC -> Maybe NodeNotification
+n_on :: NodeId -> Notification NodeNotification
 n_on = n_notification "/n_on"
 
-n_move :: NodeId -> OSC -> Maybe NodeNotification
+n_move :: NodeId -> Notification NodeNotification
 n_move = n_notification "/n_move"
 
-n_info :: NodeId -> OSC -> Maybe NodeNotification
+n_info :: NodeId -> Notification NodeNotification
 n_info = n_notification "/n_info"
+
+n_notification_ :: String -> NodeId -> Notification ()
+n_notification_ s nid = Notification f
+    where
+        f (Message s' (Int nid':_))
+            | s == s' && fromIntegral nid == nid' = Just ()
+        f _ = Nothing
+
+n_go_ :: NodeId -> Notification ()
+n_go_ = n_notification_ "/n_go"
+
+n_end_ :: NodeId -> Notification ()
+n_end_ = n_notification_ "/n_end"
+
+n_off_ :: NodeId -> Notification ()
+n_off_ = n_notification_ "/n_off"
+
+n_on_ :: NodeId -> Notification ()
+n_on_ = n_notification_ "/n_on"
+
+data BufferInfo = BufferInfo {
+    numFrames :: Int
+  , numChannels :: Int
+  , sampleRate :: Double
+  } deriving (Eq, Show)
+
+b_info :: BufferId -> Notification BufferInfo
+b_info bid = Notification f
+    where
+        f (Message "/b_info" [Int bid', Int f, Int c, Float r])
+            | fromIntegral bid == bid' = Just $ BufferInfo f c r
+        f _ = Nothing
diff --git a/Sound/SC3/Server/Process/Monad.hs b/Sound/SC3/Server/Process/Monad.hs
--- a/Sound/SC3/Server/Process/Monad.hs
+++ b/Sound/SC3/Server/Process/Monad.hs
@@ -1,26 +1,34 @@
 module Sound.SC3.Server.Process.Monad (
     withSynth
+  , withDefaultSynth
   , withInternal
+  , withDefaultInternal
+  -- * Re-exported for convenience
+  , module Sound.SC3.Server.Options
+  , OutputHandler(..)
+  , defaultOutputHandler
 ) where
 
-import           Sound.SC3.Server.Process (ServerOptions, OutputHandler, RTOptions)
-import qualified Sound.SC3.Server.Process as Process
 import qualified Sound.SC3.Server.Connection as Conn
-import qualified Sound.SC3.Server.State as State
 import qualified Sound.SC3.Server.Internal as Process
 import           Sound.SC3.Server.Monad (Server)
 import qualified Sound.SC3.Server.Monad as Server
-import qualified Sound.OpenSoundControl as OSC
+import           Sound.SC3.Server.Options
+import           Sound.SC3.Server.Process ( OutputHandler(..), defaultOutputHandler )
+import qualified Sound.SC3.Server.Process as Process
+import qualified Sound.SC3.Server.State as State
 
-withSynth :: OSC.Transport t => (ServerOptions -> RTOptions -> IO t) -> ServerOptions -> RTOptions -> OutputHandler -> Server a -> IO a
-withSynth openTransport serverOptions rtOptions outputHandler action =
+withSynth :: ServerOptions -> RTOptions -> OutputHandler -> Server a -> IO a
+withSynth serverOptions rtOptions outputHandler action =
     Process.withSynth
-        openTransport
         serverOptions
         rtOptions
         outputHandler
         $ \t -> Conn.new (State.new serverOptions) t >>= Server.runServer action
 
+withDefaultSynth :: Server a -> IO a
+withDefaultSynth = withSynth defaultServerOptions defaultRTOptions defaultOutputHandler
+
 withInternal :: ServerOptions -> RTOptions -> OutputHandler -> Server a -> IO a
 withInternal serverOptions rtOptions outputHandler action =
     Process.withInternal
@@ -28,3 +36,6 @@
         rtOptions
         outputHandler
         $ \t -> Conn.new (State.new serverOptions) t >>= Server.runServer action
+
+withDefaultInternal :: Server a -> IO a
+withDefaultInternal = withInternal defaultServerOptions defaultRTOptions defaultOutputHandler
diff --git a/Sound/SC3/Server/State.hs b/Sound/SC3/Server/State.hs
--- a/Sound/SC3/Server/State.hs
+++ b/Sound/SC3/Server/State.hs
@@ -7,96 +7,120 @@
 
 #include "Accessor.h"
 
+-- | Data type for holding server state.
+--
+-- The server state consists mainly of the allocators needed for different types of resources, such as nodes, buffers and buses.
 module Sound.SC3.Server.State (
     State
-  , options
+  , serverOptions
+  , Allocator
   , SyncId
   , SyncIdAllocator
+  , syncIdAllocator
   , NodeId
   , NodeIdAllocator
+  , nodeIdAllocator
   , BufferId
   , BufferIdAllocator
+  , bufferIdAllocator
   , BusId
   , BusIdAllocator
-  , syncId
-  , nodeId
-  , bufferId
-  , controlBusId
-  , audioBusId
-  , rootNode
+  , controlBusIdAllocator
+  , audioBusIdAllocator
+  , rootNodeId
   , new
-  , Alloc.alloc
-  , Alloc.allocMany
-  , Alloc.allocRange
-  , Alloc.free
 ) where
 
-import           Control.Arrow (second)
 import           Control.DeepSeq (NFData(..))
-import           Control.Monad (liftM)
 import           Data.Accessor
-import           Sound.SC3.Server.Allocator (IdAllocator(..))
+import           Data.Int (Int32)
+import           Sound.SC3.Server.Allocator (IdAllocator(..), RangeAllocator(..))
 import qualified Sound.SC3.Server.Allocator as Alloc
-import           Sound.SC3.Server.Allocator.SetAllocator (SetAllocator)
-import qualified Sound.SC3.Server.Allocator.SetAllocator as SetAlloc
-import           Sound.SC3.Server.Allocator.SimpleAllocator (SimpleAllocator)
-import qualified Sound.SC3.Server.Allocator.SimpleAllocator as SAlloc
+import qualified Sound.SC3.Server.Allocator.BlockAllocator.FirstFit as FirstFitAllocator
+import qualified Sound.SC3.Server.Allocator.SetAllocator as SetAllocator
+import qualified Sound.SC3.Server.Allocator.SimpleAllocator as SimpleAllocator
+import qualified Sound.SC3.Server.Allocator.Wrapped as Wrapped
 import           Sound.SC3.Server.Options (ServerOptions(..))
 
-allocM f = liftM (second f) . Alloc.alloc
-freeM f i = liftM f . Alloc.free i
+-- | Allocator accessor.
+type Allocator a = Accessor State a
 
-newtype SyncId = SyncId Int deriving (Bounded, Enum, Eq, Integral, NFData, Num, Ord, Real, Show)
+-- | Synchronisation barrier id.
+newtype SyncId = SyncId Int32 deriving (Bounded, Enum, Eq, Integral, NFData, Num, Ord, Real, Show)
+
+-- | Synchronisation barrier id allocator.
 data SyncIdAllocator = forall a . (IdAllocator a, NFData a, Id a ~ SyncId) => SyncIdAllocator !a
 
 instance IdAllocator SyncIdAllocator where
     type Id SyncIdAllocator = SyncId
-    alloc  (SyncIdAllocator a) = allocM SyncIdAllocator a
-    free i (SyncIdAllocator a) = freeM SyncIdAllocator i a
+    alloc  (SyncIdAllocator a) = Wrapped.alloc SyncIdAllocator a
+    free i (SyncIdAllocator a) = Wrapped.free SyncIdAllocator i a
+    statistics (SyncIdAllocator a) = Wrapped.statistics a
 
 instance NFData SyncIdAllocator where
     rnf (SyncIdAllocator a) = rnf a `seq` ()
 
-newtype NodeId = NodeId Int deriving (Bounded, Enum, Eq, Integral, NFData, Num, Ord, Real, Show)
+-- | Node id.
+newtype NodeId = NodeId Int32 deriving (Bounded, Enum, Eq, Integral, NFData, Num, Ord, Real, Show)
+
+-- | Node id allocator.
 data NodeIdAllocator = forall a . (IdAllocator a, NFData a, Id a ~ NodeId) => NodeIdAllocator !a
 
 instance IdAllocator NodeIdAllocator where
     type Id NodeIdAllocator = NodeId
-    alloc  (NodeIdAllocator a) = allocM NodeIdAllocator a
-    free i (NodeIdAllocator a) = freeM NodeIdAllocator i a
+    alloc  (NodeIdAllocator a) = Wrapped.alloc NodeIdAllocator a
+    free i (NodeIdAllocator a) = Wrapped.free NodeIdAllocator i a
+    statistics (NodeIdAllocator a) = Wrapped.statistics a
 
 instance NFData NodeIdAllocator where
     rnf (NodeIdAllocator a) = rnf a `seq` ()
 
-newtype BufferId = BufferId Int deriving (Bounded, Enum, Eq, Integral, NFData, Num, Ord, Real, Show)
-data BufferIdAllocator = forall a . (IdAllocator a, NFData a, Id a ~ BufferId) => BufferIdAllocator !a
+-- | Buffer id.
+newtype BufferId = BufferId Int32 deriving (Bounded, Enum, Eq, Integral, NFData, Num, Ord, Real, Show)
 
+-- | Buffer id allocator.
+data BufferIdAllocator = forall a . (RangeAllocator a, NFData a, Id a ~ BufferId) => BufferIdAllocator !a
+
 instance IdAllocator BufferIdAllocator where
     type Id BufferIdAllocator = BufferId
-    alloc  (BufferIdAllocator a) = allocM BufferIdAllocator a
-    free i (BufferIdAllocator a) = freeM BufferIdAllocator i a
+    alloc  (BufferIdAllocator a) = Wrapped.alloc BufferIdAllocator a
+    free i (BufferIdAllocator a) = Wrapped.free BufferIdAllocator i a
+    statistics (BufferIdAllocator a) = Wrapped.statistics a
 
+instance RangeAllocator BufferIdAllocator where
+    allocRange n (BufferIdAllocator a) = Wrapped.allocRange BufferIdAllocator n a
+    freeRange r (BufferIdAllocator a) = Wrapped.freeRange BufferIdAllocator r a
+
 instance NFData BufferIdAllocator where
     rnf (BufferIdAllocator a) = rnf a `seq` ()
 
-newtype BusId = BusId Int deriving (Bounded, Enum, Eq, Integral, NFData, Num, Ord, Real, Show)
-data BusIdAllocator = forall a . (IdAllocator a, NFData a, Id a ~ BusId) => BusIdAllocator !a
+-- | Bus id.
+newtype BusId = BusId Int32 deriving (Bounded, Enum, Eq, Integral, NFData, Num, Ord, Real, Show)
 
+-- | Bus id allocator.
+data BusIdAllocator = forall a . (RangeAllocator a, NFData a, Id a ~ BusId) => BusIdAllocator !a
+
 instance IdAllocator BusIdAllocator where
     type Id BusIdAllocator = BusId
-    alloc  (BusIdAllocator a) = allocM BusIdAllocator a
-    free i (BusIdAllocator a) = freeM BusIdAllocator i a
+    alloc  (BusIdAllocator a) = Wrapped.alloc BusIdAllocator a
+    free i (BusIdAllocator a) = Wrapped.free BusIdAllocator i a
+    statistics (BusIdAllocator a) = Wrapped.statistics a
 
+instance RangeAllocator BusIdAllocator where
+    allocRange n (BusIdAllocator a) = Wrapped.allocRange BusIdAllocator n a
+    freeRange r (BusIdAllocator a) = Wrapped.freeRange BusIdAllocator r a
+
 instance NFData BusIdAllocator where
     rnf (BusIdAllocator a) = rnf a `seq` ()
 
+-- | Server state.
 data State = State {
-    _options      :: !ServerOptions
-  , _syncId       :: !SyncIdAllocator
-  , _nodeId       :: !NodeIdAllocator
-  , _bufferId     :: !BufferIdAllocator
-  , _controlBusId :: !BusIdAllocator
-  , _audioBusId   :: !BusIdAllocator
+    _serverOptions         :: !ServerOptions
+  , _syncIdAllocator       :: !SyncIdAllocator
+  , _nodeIdAllocator       :: !NodeIdAllocator
+  , _bufferIdAllocator     :: !BufferIdAllocator
+  , _controlBusIdAllocator :: !BusIdAllocator
+  , _audioBusIdAllocator   :: !BusIdAllocator
   }
 
 instance NFData State where
@@ -108,38 +132,47 @@
         rnf x5 `seq`
         rnf x6 `seq` ()
 
-ACCESSOR(options,      _options,      State, ServerOptions)
-ACCESSOR(syncId,       _syncId,       State, SyncIdAllocator)
-ACCESSOR(nodeId,       _nodeId,       State, NodeIdAllocator)
-ACCESSOR(bufferId,     _bufferId,     State, BufferIdAllocator)
-ACCESSOR(controlBusId, _controlBusId, State, BusIdAllocator)
-ACCESSOR(audioBusId,   _audioBusId,   State, BusIdAllocator)
+-- | Server options used to create this instance.
+ACCESSOR(serverOptions,         _serverOptions,         State, ServerOptions)
+-- | Synchronisation id allocator.
+ACCESSOR(syncIdAllocator,       _syncIdAllocator,       State, SyncIdAllocator)
+-- | Node id allocator.
+ACCESSOR(nodeIdAllocator,       _nodeIdAllocator,       State, NodeIdAllocator)
+-- | Buffer id allocator.
+ACCESSOR(bufferIdAllocator,     _bufferIdAllocator,     State, BufferIdAllocator)
+-- | Control bus id allocator.
+ACCESSOR(controlBusIdAllocator, _controlBusIdAllocator, State, BusIdAllocator)
+-- | Audio bus id allocator.
+ACCESSOR(audioBusIdAllocator,   _audioBusIdAllocator,   State, BusIdAllocator)
 
-rootNode :: State -> NodeId
-rootNode = const (NodeId 0)
+-- | Root node id.
+rootNodeId :: State -> NodeId
+rootNodeId = const (NodeId 0)
 
+-- | Create a new state with default allocators.
 new :: ServerOptions -> State
 new os =
     State {
-        _options      = os
-      , _syncId       = sid
-      , _nodeId       = nid
-      , _bufferId     = bid
-      , _controlBusId = cid
-      , _audioBusId   = aid
+        _serverOptions         = os
+      , _syncIdAllocator       = sid
+      , _nodeIdAllocator       = nid
+      , _bufferIdAllocator     = bid
+      , _controlBusIdAllocator = cid
+      , _audioBusIdAllocator   = aid
     }
     where
-        sid = SyncIdAllocator
-                (SAlloc.cons :: SimpleAllocator SyncId)
-        nid = NodeIdAllocator
-                (SetAlloc.cons $ Alloc.range 1000 (1000 + fromIntegral (maxNumberOfNodes os)) :: SetAllocator NodeId)
-        bid = BufferIdAllocator
-                (SetAlloc.cons $ Alloc.range 0 (fromIntegral (numberOfSampleBuffers os - 1)) :: SetAllocator BufferId)
-        cid = BusIdAllocator
-                (SetAlloc.cons $ Alloc.range 0 (fromIntegral (numberOfControlBusChannels os - 1)) :: SetAllocator BusId)
-        aid = BusIdAllocator
-                (SetAlloc.cons $ Alloc.range
-                    (fromIntegral numHardwareChannels)
-                    (fromIntegral (numHardwareChannels + numberOfAudioBusChannels os)) :: SetAllocator BusId)
+        sid = SyncIdAllocator (SimpleAllocator.cons (Alloc.range 0 (maxBound :: SyncId)))
+        nid = NodeIdAllocator (SetAllocator.cons (Alloc.range 1000 (1000 + fromIntegral (maxNumberOfNodes os))))
+        bid = BufferIdAllocator (FirstFitAllocator.bestFit
+                                 FirstFitAllocator.LazyCoalescing
+                                 (Alloc.range 0 (fromIntegral (numberOfSampleBuffers os))))
+        cid = BusIdAllocator (FirstFitAllocator.bestFit
+                              FirstFitAllocator.LazyCoalescing
+                              (Alloc.range 0 (fromIntegral (numberOfControlBusChannels os))))
+        aid = BusIdAllocator (FirstFitAllocator.bestFit
+                              FirstFitAllocator.LazyCoalescing
+                              (Alloc.range
+                                (fromIntegral numHardwareChannels)
+                                (fromIntegral (numHardwareChannels + numberOfAudioBusChannels os))))
         numHardwareChannels = numberOfInputBusChannels os
                             + numberOfOutputBusChannels os
diff --git a/Sound/SC3/Server/State/Concurrent.hs b/Sound/SC3/Server/State/Concurrent.hs
deleted file mode 100644
--- a/Sound/SC3/Server/State/Concurrent.hs
+++ /dev/null
@@ -1,53 +0,0 @@
-{-# LANGUAGE FlexibleContexts #-}
-module Sound.SC3.Server.State.Concurrent (
-    fromState
-  , new
-  , alloc
-  , allocMany
-  , free
-  , allocRange
-  , freeRange
-) where
-
-import           Control.Arrow (second)
-import           Control.Failure (Failure)
--- import           Control.Concurrent.MVar (MVar, newMVar)
-import           Control.Concurrent.MVar.Strict (MVar, modifyMVar, modifyMVar_, newMVar)
-import           Data.Accessor
-import           Sound.SC3.Server.Allocator (AllocFailure, IdAllocator, Id, RangeAllocator, Range)
-import qualified Sound.SC3.Server.Allocator as Alloc
-import           Sound.SC3.Server.State (State)
-import qualified Sound.SC3.Server.State as State
-import           Sound.SC3.Server.Options (ServerOptions)
-
-type IOState = MVar State
-
-fromState :: State -> IO (MVar State)
-fromState = newMVar
-
-new :: ServerOptions -> IO (MVar State)
-new = fromState . State.new
-
-swap :: (a, b) -> (b, a)
-swap (a, b) = (b, a)
-
-alloc :: (IdAllocator a, Failure AllocFailure IO) => (Accessor State a) -> MVar State -> IO (Id a)
-alloc f ios = modifyMVar ios (\s -> fmap (swap . second (flip (setVal f) s)) . Alloc.alloc . getVal f $ s)
-
-allocMany :: (IdAllocator a, Failure AllocFailure IO)  => (Accessor State a) -> MVar State -> Int -> IO [Id a]
-allocMany f m n = modifyMVar m (\s -> fmap (swap . second (flip (setVal f) s)) . Alloc.allocMany n . getVal f $ s)
-
-free :: IdAllocator a => (Accessor State a) -> IOState -> Id a -> IO ()
-free f ios i = modifyMVar_ ios $ \s -> do
-    let a  = s ^. f
-    a' <- Alloc.free i a
-    return $ f ^= a' $ s
-
-allocRange :: (RangeAllocator a, Failure AllocFailure IO) => (Accessor State a) -> MVar State -> Int -> IO (Range (Id a))
-allocRange f ios n = modifyMVar ios (\s -> fmap (swap . second (flip (setVal f) s)) . Alloc.allocRange n . getVal f $ s)
-
-freeRange :: (RangeAllocator a, Failure AllocFailure IO) => (Accessor State a) -> MVar State -> Range (Id a) -> IO ()
-freeRange f ios r = modifyMVar_ ios $ \s -> do
-    let a  = s ^. f
-    a' <- Alloc.freeRange r a
-    return $ f ^= a' $ s
diff --git a/hsc3-server.cabal b/hsc3-server.cabal
--- a/hsc3-server.cabal
+++ b/hsc3-server.cabal
@@ -1,18 +1,22 @@
 Name:               hsc3-server
-Version:            0.1.0
+Version:            0.3.0
 Synopsis:           SuperCollider server resource management and synchronization.
-Description:        This library provides abstractions for managing SuperCollider server resources like node, buffer and bus ids and synchronization primitives.
+Description:
+    This library provides abstractions for managing SuperCollider server
+    resources like node, buffer and bus ids and synchronization primitives.
+    .
+    ChangeLog: <https://github.com/kaoskorobase/hsc3-server/blob/master/ChangeLog.md>
 License:            GPL
 License-File:       LICENSE
 Category:           Sound
-Copyright:          Copyright (c) Stefan Kersten 2008-2011
+Copyright:          Copyright (c) Stefan Kersten 2008-2012
 Author:             Stefan Kersten
 Maintainer:         Stefan Kersten
 Stability:          experimental
 Homepage:           http://space.k-hornz.de/software/hsc3-server
 Tested-With:        GHC == 6.10.1, GHC == 6.12.3, GHC == 7.0.1
 Build-Type:         Simple
-Cabal-Version:      >= 1.6
+Cabal-Version:      >= 1.9.2
 
 Extra-Source-Files:
     include/Accessor.h
@@ -21,31 +25,59 @@
     Exposed-Modules:
         Sound.SC3.Server.Allocator
         Sound.SC3.Server.Allocator.Range
+        Sound.SC3.Server.Allocator.BlockAllocator.FirstFit
         Sound.SC3.Server.Allocator.SetAllocator
         Sound.SC3.Server.Allocator.SimpleAllocator
+        Sound.SC3.Server.Allocator.Wrapped
         Sound.SC3.Server.Connection
+        Sound.SC3.Server.Connection.ListenerMap
+        Sound.SC3.Server.Connection.ListenerMap.HashTable
+        Sound.SC3.Server.Connection.ListenerMap.List
         Sound.SC3.Server.Monad
+        Sound.SC3.Server.Monad.Command
+        Sound.SC3.Server.Monad.Send
         Sound.SC3.Server.Notification
         Sound.SC3.Server.Process.Monad
         Sound.SC3.Server.State
-        Sound.SC3.Server.State.Concurrent
+    Other-Modules:
+        Sound.SC3.Server.Allocator.BlockAllocator.FreeList        
     Build-Depends:
         base >= 3 && < 5
       , bitset >= 1.0
-      , data-accessor
-      , deepseq
-      , failure
+      , containers
+      , data-accessor >= 0.2
+      , deepseq >= 1.1
+      , failure >= 0.1
       , hosc >= 0.8
       , hsc3 >= 0.7
-      , hsc3-process >= 0.5
-      , mtl >= 2
+      , hsc3-process >= 0.6
       , strict-concurrency
       , transformers
     Include-Dirs:
         include
     Ghc-Options:
         -W
+    Ghc-Prof-Options:
+        -W -auto-all
 
 Source-Repository head
-  Type:             git
-  Location:         git://github.com/kaoskorobase/hsc3-server.git
+    Type:       git
+    Location:   git://github.com/kaoskorobase/hsc3-server.git
+
+Test-Suite hsc3-server-test
+    Type: exitcode-stdio-1.0
+    Main-Is: test.hs
+    Other-Modules:
+        Sound.SC3.Server.Allocator.Test
+        Sound.SC3.Server.Allocator.Range.Test
+    Build-Depends:
+        base
+      , bitset >= 1.0
+      , deepseq >= 1.1
+      , failure >= 0.1
+      , QuickCheck >= 2.4
+      , random >= 1.0
+      , test-framework
+      , test-framework-quickcheck2
+      , transformers >= 0.2
+    Hs-Source-Dirs: tests, .
diff --git a/tests/Sound/SC3/Server/Allocator/Range/Test.hs b/tests/Sound/SC3/Server/Allocator/Range/Test.hs
new file mode 100644
--- /dev/null
+++ b/tests/Sound/SC3/Server/Allocator/Range/Test.hs
@@ -0,0 +1,27 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+module Sound.SC3.Server.Allocator.Range.Test
+    (
+        tests
+    ) where
+
+import Sound.SC3.Server.Allocator.Range
+
+import Control.Monad (liftM)
+import System.Random (Random)
+import Test.Framework (Test, testGroup)
+import Test.Framework.Providers.QuickCheck2 (testProperty)
+import Test.QuickCheck
+
+instance (Integral i, Arbitrary i, Random i) => Arbitrary (Range i) where
+    arbitrary = do
+        l <- liftM fromIntegral (arbitrary :: Gen (NonNegative i))
+        h <- choose (l, l + 2048)
+        return $ range l h
+
+tests :: [Test]
+tests =
+    [ testGroup "Sound.SC3.Server.Allocator.Range"
+        [ testProperty "bounds" $ \(r :: Range Int) -> begin r <= end r
+        , testProperty "split/join" $ \(n :: Int) (r :: Range Int) -> uncurry join (split n r) == r
+        ]
+    ]
diff --git a/tests/Sound/SC3/Server/Allocator/Test.hs b/tests/Sound/SC3/Server/Allocator/Test.hs
new file mode 100644
--- /dev/null
+++ b/tests/Sound/SC3/Server/Allocator/Test.hs
@@ -0,0 +1,63 @@
+{-# LANGUAGE ExistentialQuantification
+           , FlexibleContexts
+           , GADTs
+           , ScopedTypeVariables
+           , TypeFamilies #-}
+module Sound.SC3.Server.Allocator.Test
+    (
+        tests
+    ) where
+
+import           Sound.SC3.Server.Allocator
+import qualified Sound.SC3.Server.Allocator.Wrapped as Wrapped
+import qualified Sound.SC3.Server.Allocator.SimpleAllocator
+import qualified Sound.SC3.Server.Allocator.SetAllocator
+import           Sound.SC3.Server.Allocator.BlockAllocator.FirstFit (Coalescing(LazyCoalescing), Sorting(..))
+import qualified Sound.SC3.Server.Allocator.BlockAllocator.FirstFit
+
+import Sound.SC3.Server.Allocator.Range.Test ()
+
+import Test.Framework (Test, testGroup)
+import Test.Framework.Providers.QuickCheck2 (testProperty)
+import Test.QuickCheck
+
+data WrappedIdAllocator = forall a . (IdAllocator a, Id a ~ Id WrappedIdAllocator, Show a) => WrappedIdAllocator a
+
+instance Show WrappedIdAllocator where
+    show (WrappedIdAllocator a) = show a
+
+instance IdAllocator WrappedIdAllocator where
+    type Id WrappedIdAllocator = Int
+    alloc (WrappedIdAllocator a) = Wrapped.alloc WrappedIdAllocator a
+    free i (WrappedIdAllocator a) = Wrapped.free WrappedIdAllocator i a
+    statistics (WrappedIdAllocator a) = Wrapped.statistics a
+
+instance Arbitrary Sorting where
+    arbitrary = elements (enumFromTo Address DecreasingSize)
+
+instance Arbitrary WrappedIdAllocator where
+    arbitrary = do
+        r <- arbitrary
+        s <- arbitrary
+        elements [ WrappedIdAllocator (Sound.SC3.Server.Allocator.SimpleAllocator.cons r)
+                 , WrappedIdAllocator (Sound.SC3.Server.Allocator.SetAllocator.cons r)
+                 , WrappedIdAllocator (Sound.SC3.Server.Allocator.BlockAllocator.FirstFit.cons s LazyCoalescing r) ]
+
+tests :: [Test]
+tests = [ testGroup "Sound.SC3.Server.Allocator"
+            [ testGroup "IdAllocator"
+                [ testProperty "initial statistics" $ \(a :: WrappedIdAllocator) ->
+                    let s = statistics a
+                    in numFree s == numAvailable s && numUsed s == 0
+                , testProperty "statistics after allocating something" $ \(a :: WrappedIdAllocator) (n :: Int) ->
+                    let n' = max 0 (min n (numAvailable (statistics a)))
+                        Just (_, a') = allocMany n' a
+                        s = statistics a'
+                    in numFree s == (numAvailable s - n') && numUsed s == n'
+                , testProperty "statistics after allocating everything" $ \(a :: WrappedIdAllocator) ->
+                    let Just (_, a') = allocMany (numAvailable (statistics a)) a
+                        s = statistics a'
+                    in numFree s == 0 && numUsed s == numAvailable s
+                ]
+            ]
+        ]
diff --git a/tests/test.hs b/tests/test.hs
new file mode 100644
--- /dev/null
+++ b/tests/test.hs
@@ -0,0 +1,11 @@
+import Test.Framework (Test, defaultMain)
+import qualified Sound.SC3.Server.Allocator.Range.Test
+import qualified Sound.SC3.Server.Allocator.Test
+
+tests :: [Test]
+tests =
+    Sound.SC3.Server.Allocator.Range.Test.tests
+ ++ Sound.SC3.Server.Allocator.Test.tests
+
+main :: IO ()
+main = defaultMain tests
