tidal-1.10.2: src/Sound/Tidal/Stream/Target.hs
module Sound.Tidal.Stream.Target where
import Control.Concurrent
( forkIO,
forkOS,
newMVar,
readMVar,
swapMVar,
threadDelay,
)
import Control.Monad (when)
import Data.Maybe (catMaybes, fromJust, isJust)
import Foreign (Word8)
import qualified Network.Socket as N
import qualified Sound.Osc.Fd as O
import qualified Sound.Osc.Time.Timeout as O
import qualified Sound.Osc.Transport.Fd.Udp as O
import Sound.Tidal.Config
import Sound.Tidal.Pattern
import Sound.Tidal.Stream.Types
{-
Target.hs - Create and send to OSC targets
Copyright (C) 2020, Alex McLean and contributors
This library is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this library. If not, see <http://www.gnu.org/licenses/>.
-}
getCXs :: Config -> [(Target, [OSC])] -> IO [Cx]
getCXs config oscmap =
mapM
( \(target, os) -> do
remote_addr <- resolve (oAddress target) (oPort target)
remote_bus_addr <- mapM (resolve (oAddress target)) (oBusPort target)
remote_busses <- sequence (oBusPort target >> Just (newMVar []))
let broadcast = if cCtrlBroadcast config then 1 else 0
u <-
O.udp_socket
(\sock _ -> do N.setSocketOption sock N.Broadcast broadcast)
(oAddress target)
(oPort target)
let cx = Cx {cxUDP = u, cxAddr = remote_addr, cxBusAddr = remote_bus_addr, cxBusses = remote_busses, cxTarget = target, cxOSCs = os}
_ <- forkIO $ handshake cx config
return cx
)
oscmap
resolve :: String -> Int -> IO N.AddrInfo
resolve host port = do
let hints = N.defaultHints {N.addrSocketType = N.Stream}
addr : _ <- N.getAddrInfo (Just hints) (Just host) (Just $ show port)
return addr
handshake :: Cx -> Config -> IO ()
handshake Cx {cxUDP = udp, cxBusses = Just bussesMV, cxAddr = addr} c = sendHandshake >> listen 0
where
sendHandshake :: IO ()
sendHandshake = O.sendTo udp (O.Packet_Message $ O.Message "/dirt/handshake" []) (N.addrAddress addr)
listen :: Int -> IO ()
listen waits = do
ms <- recvMessagesTimeout 2 udp
if null ms
then do
checkHandshake waits -- there was a timeout, check handshake
listen (waits + 1)
else do
mapM_ respond ms
listen 0
checkHandshake :: Int -> IO ()
checkHandshake waits = do
busses <- readMVar bussesMV
when (null busses) $ do
when (waits == 0) $ verbose c $ "Waiting for SuperDirt (v.1.7.2 or higher).."
sendHandshake
respond :: O.Message -> IO ()
respond (O.Message "/dirt/hello" _) = sendHandshake
respond (O.Message "/dirt/handshake/reply" xs) = do
prev <- swapMVar bussesMV $ bufferIndices xs
-- Only report the first time..
when (null prev) $ verbose c $ "Connected to SuperDirt."
respond _ = return ()
bufferIndices :: [O.Datum] -> [Int]
bufferIndices [] = []
bufferIndices (x : xs')
| x == O.AsciiString (O.ascii "&controlBusIndices") = catMaybes $ takeWhile isJust $ map O.datum_integral xs'
| otherwise = bufferIndices xs'
handshake _ _ = return ()
recvMessagesTimeout :: (O.Transport t) => Double -> t -> IO [O.Message]
recvMessagesTimeout n sock = fmap (maybe [] O.packetMessages) $ O.recvPacketTimeout n sock
-- send has three modes:
-- Send events early using timestamp in the OSC bundle - used by Superdirt
-- Send events early by adding timestamp to the OSC message - used by Dirt
-- Send events live by delaying the thread
send :: Cx -> Double -> Double -> (Double, Bool, O.Message) -> IO ()
send cx latency extraLatency (time, isBusMsg, m)
| oSchedule target == Pre BundleStamp = sendBndl isBusMsg cx $ O.Bundle timeWithLatency [m]
| oSchedule target == Pre MessageStamp = sendO isBusMsg cx $ addtime m
| otherwise = do
_ <- forkOS $ do
now <- O.time
threadDelay $ floor $ (timeWithLatency - now) * 1000000
sendO isBusMsg cx m
return ()
where
addtime (O.Message mpath params) = O.Message mpath ((O.int32 sec) : ((O.int32 usec) : params))
ut = O.ntpr_to_posix timeWithLatency
sec :: Int
sec = floor ut
usec :: Int
usec = floor $ 1000000 * (ut - (fromIntegral sec))
target = cxTarget cx
timeWithLatency = time - latency + extraLatency
sendBndl :: Bool -> Cx -> O.Bundle -> IO ()
sendBndl isBusMsg cx bndl = O.sendTo (cxUDP cx) (O.Packet_Bundle bndl) (N.addrAddress addr)
where
addr
| isBusMsg && isJust (cxBusAddr cx) = fromJust $ cxBusAddr cx
| otherwise = cxAddr cx
sendO :: Bool -> Cx -> O.Message -> IO ()
sendO isBusMsg cx msg = O.sendTo (cxUDP cx) (O.Packet_Message msg) (N.addrAddress addr)
where
addr
| isBusMsg && isJust (cxBusAddr cx) = fromJust $ cxBusAddr cx
| otherwise = cxAddr cx
superdirtTarget :: Target
superdirtTarget =
Target
{ oName = "SuperDirt",
oAddress = "127.0.0.1",
oPort = 57120,
oBusPort = Just 57110,
oLatency = 0.2,
oWindow = Nothing,
oSchedule = Pre BundleStamp,
oHandshake = True
}
superdirtShape :: OSC
superdirtShape = OSC "/dirt/play" $ Named {requiredArgs = ["s"]}
dirtTarget :: Target
dirtTarget =
Target
{ oName = "Dirt",
oAddress = "127.0.0.1",
oPort = 7771,
oBusPort = Nothing,
oLatency = 0.02,
oWindow = Nothing,
oSchedule = Pre MessageStamp,
oHandshake = False
}
dirtShape :: OSC
dirtShape =
OSC "/play" $
ArgList
[ ("cps", fDefault 0),
("s", Nothing),
("offset", fDefault 0),
("begin", fDefault 0),
("end", fDefault 1),
("speed", fDefault 1),
("pan", fDefault 0.5),
("velocity", fDefault 0.5),
("vowel", sDefault ""),
("cutoff", fDefault 0),
("resonance", fDefault 0),
("accelerate", fDefault 0),
("shape", fDefault 0),
("kriole", iDefault 0),
("gain", fDefault 1),
("cut", iDefault 0),
("delay", fDefault 0),
("delaytime", fDefault (-1)),
("delayfeedback", fDefault (-1)),
("crush", fDefault 0),
("coarse", iDefault 0),
("hcutoff", fDefault 0),
("hresonance", fDefault 0),
("bandf", fDefault 0),
("bandq", fDefault 0),
("unit", sDefault "rate"),
("loop", fDefault 0),
("n", fDefault 0),
("attack", fDefault (-1)),
("hold", fDefault 0),
("release", fDefault (-1)),
("orbit", iDefault 0) -- ,
-- ("id", iDefault 0)
]
sDefault :: String -> Maybe Value
sDefault x = Just $ VS x
fDefault :: Double -> Maybe Value
fDefault x = Just $ VF x
rDefault :: Rational -> Maybe Value
rDefault x = Just $ VR x
iDefault :: Int -> Maybe Value
iDefault x = Just $ VI x
bDefault :: Bool -> Maybe Value
bDefault x = Just $ VB x
xDefault :: [Word8] -> Maybe Value
xDefault x = Just $ VX x
required :: Maybe Value
required = Nothing