packages feed

zwirn-0.1.0.0: src/Zwirn/Stream.hs

{-# LANGUAGE DeriveGeneric #-}
{-# OPTIONS_GHC -Wno-type-defaults #-}

module Zwirn.Stream where

{-
    Stream.hs - query and send messages, code adapted from
    https://github.com/tidalcycles/Tidal/tree/dev/src/Sound/Tidal/Stream
    Copyright (C) 2023, Martin Gius

    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/>.
-}

import Control.Concurrent (forkIO)
import Control.Concurrent.MVar (MVar, modifyMVar_, newMVar, readMVar, swapMVar)
import Control.Monad (when)
import Data.Bifunctor (second)
import qualified Data.Map as Map
import Data.Maybe (catMaybes, isJust)
import Data.Text (Text, pack)
import qualified Data.Text as T
import GHC.Generics (Generic)
import qualified Network.Socket as N
import qualified Sound.Osc as O
import Sound.Osc.Time.Timeout (recvPacketTimeout)
import qualified Sound.Osc.Transport.Fd.Udp as O
import Sound.Tidal.Clock
import qualified Sound.Tidal.Clock as Clock
import Sound.Tidal.Link
import Zwirn.Core.Cord (stack)
import Zwirn.Core.Query
import qualified Zwirn.Core.Time as Z
import Zwirn.Language.Evaluate

type PlayMap = Map.Map Text (Zwirn Expression)

type BusMap = Map.Map Int (Zwirn Expression)

data StreamConfig = StreamConfig
  { streamConfigPort :: Int,
    streamConfigBusPort :: Int,
    streamConfigAddress :: String
  }
  deriving (Generic)

data Stream = Stream
  { sPlayMap :: MVar PlayMap,
    sBusMap :: MVar BusMap,
    sState :: MVar ExpressionMap,
    sBusses :: MVar [Int],
    sAddress :: RemoteAddress,
    sBusAddress :: RemoteAddress,
    sLocal :: O.Udp,
    sClockRef :: ClockRef,
    sClockConfig :: ClockConfig
  }

type RemoteAddress = N.SockAddr

streamReplace :: Stream -> Text -> Zwirn Expression -> IO ()
streamReplace str key p = modifyMVar_ (sPlayMap str) (return . Map.insert key p)

streamReplaceBus :: Stream -> Int -> Zwirn Expression -> IO ()
streamReplaceBus str key p = modifyMVar_ (sBusMap str) (return . Map.insert key p)

streamSet :: Stream -> T.Text -> Expression -> IO ()
streamSet str x ex = modifyMVar_ (sState str) (return . Map.insert x ex)

streamSetCPS :: Stream -> Time -> IO ()
streamSetCPS s = Clock.setCPS (sClockConfig s) (sClockRef s)

streamSetBPM :: Stream -> Time -> IO ()
streamSetBPM s = Clock.setBPM (sClockRef s)

streamFirst :: Stream -> Zwirn Expression -> IO ()
streamFirst str z = do
  dummy <- newMVar $ Map.singleton (pack "_streamOnceDummy_") z
  Clock.clockOnce (tickAction dummy (sBusMap str) (sState str) (sBusses str) (sAddress str) (sBusAddress str) (sLocal str)) (sClockConfig str) (sClockRef str)

startStream :: StreamConfig -> MVar PlayMap -> MVar ExpressionMap -> ClockConfig -> IO Stream
startStream config zMV stMV conf = do
  let target_address = streamConfigAddress config
      target_port = streamConfigPort config
      target_bus_port = streamConfigBusPort config
  remote <- resolve target_address target_port
  remoteBus <- resolve target_address target_bus_port
  local <- O.udp_server 2323

  busMapMV <- newMVar Map.empty
  bussesMV <- newMVar []

  _ <- forkIO $ handshake (N.addrAddress remote) local bussesMV

  cref <- clocked conf (tickAction zMV busMapMV stMV bussesMV (N.addrAddress remote) (N.addrAddress remoteBus) local)
  return $ Stream zMV busMapMV stMV bussesMV (N.addrAddress remote) (N.addrAddress remoteBus) local cref conf

tickAction :: MVar PlayMap -> MVar BusMap -> MVar ExpressionMap -> MVar [Int] -> RemoteAddress -> RemoteAddress -> O.Udp -> (Time, Time) -> Double -> ClockConfig -> ClockRef -> (SessionState, SessionState) -> IO ()
tickAction zMV busMapMV stMV bussesMV remote remoteBus local (star, end) nudge cconf cref (ss, _) = do
  cps <- Clock.getCPS cconf cref
  vs <- processPlayMap (star, end) cps zMV stMV
  bs <- processBusMap (star, end) busMapMV stMV bussesMV
  mapM_ (processAndSend remote local nudge cconf cref ss) vs
  mapM_ (processAndSend remoteBus local nudge cconf cref ss) bs

processPlayMap :: (Time, Time) -> Time -> MVar PlayMap -> MVar ExpressionMap -> IO [(Z.Time, O.Message)]
processPlayMap (star, end) cps zMV stMV = do
  pm <- readMVar zMV
  let p = playMapToCord pm
  st <- readMVar stMV
  let qs = findAllValuesWithTimeState (Z.Time (align star) 1, Z.Time (align end) 1) st p
      vs = map (\(t, v, _) -> (t, v)) qs
      sts = map (\(_, _, x) -> x) qs

  -- TODO: what about race conditions?
  updateState stMV sts

  return $ (\(t, ex) -> (t, expressionToMessage (fromIntegral $ floor t) (realToFrac cps) ex)) <$> vs

processBusMap :: (Time, Time) -> MVar BusMap -> MVar ExpressionMap -> MVar [Int] -> IO [(Z.Time, O.Message)]
processBusMap (star, end) busMV stMV bussesMV = do
  bm <- readMVar busMV
  let bs = Map.toList bm
  busses <- readMVar bussesMV
  st <- readMVar stMV
  return $ concatMap (\(i, p) -> second (busExpressionToMessage $ toBus busses i) <$> findAllValuesWithTime (Z.Time (align star) 1, Z.Time (align end) 1) st p) bs

toBus :: [Int] -> Int -> Int
toBus [] i = i
toBus xs i = xs !! (i `mod` length xs)

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

playMapToCord :: PlayMap -> Zwirn Expression
playMapToCord = stack . Map.elems

align :: Time -> Time
align t = fromIntegral (floor $ t / 0.001) * 0.001

expressionToOSC :: Expression -> [O.Datum]
expressionToOSC (ENum n) = [O.float n]
expressionToOSC (EText n) = [O.string $ T.unpack n]
expressionToOSC (EMap m) = concatMap (\(k, v) -> O.string (T.unpack k) : expressionToOSC v) $ Map.toList m
expressionToOSC _ = []

additionalData :: Double -> Double -> [O.Datum]
additionalData cyc cps = [O.string "cps", O.float cps, O.string "cycle", O.float cyc]

expressionToMessage :: Double -> Double -> Expression -> O.Message
expressionToMessage cyc cps ex = O.message "/dirt/play" (additionalData cyc cps ++ expressionToOSC ex)

busExpressionToMessage :: Int -> Expression -> O.Message
busExpressionToMessage bus ex = O.message "/c_set" (O.int32 bus : expressionToOSC ex)

sendMessage :: RemoteAddress -> O.Udp -> Double -> Double -> (Double, O.Message) -> IO ()
sendMessage remote local latency extraLatency (time, m) = sendBndl remote local $ O.Bundle timeWithLatency [m]
  where
    timeWithLatency = time - latency + extraLatency

sendBndl :: RemoteAddress -> O.Udp -> O.Bundle -> IO ()
sendBndl remote local bndl = O.sendTo local (O.Packet_Bundle bndl) remote

defaultLatency :: Double
defaultLatency = 0.2

processAndSend :: RemoteAddress -> O.Udp -> Double -> ClockConfig -> ClockRef -> SessionState -> (Z.Time, O.Message) -> IO ()
processAndSend remote local nudge cconf cref ss (t, msg) = do
  let onBeat = Clock.cyclesToBeat cconf (realToFrac ((\(Z.Time r _) -> fromRational r) t))

  on <- Clock.timeAtBeat cconf ss onBeat
  onOSC <- Clock.linkToOscTime cref on

  sendMessage remote local defaultLatency nudge (onOSC, msg)

updateState :: MVar ExpressionMap -> [ExpressionMap] -> IO ()
updateState _ [] = return ()
updateState stmv (st : _) = modifyMVar_ stmv (const $ return st)

handshake :: RemoteAddress -> O.Udp -> MVar [Int] -> IO ()
handshake addr udp bussesMV = sendHandshake >> listen 0
  where
    sendHandshake :: IO ()
    sendHandshake = O.sendTo udp (O.Packet_Message $ O.Message "/dirt/handshake" []) 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) $ print "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
      return ()
    -- Only report the first time..
    -- when (null prev) $ print "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'

recvMessagesTimeout :: Double -> O.Udp -> IO [O.Message]
recvMessagesTimeout n sock = maybe [] O.packetMessages <$> recvPacketTimeout n sock