packages feed

zwirn-0.2.3.1: src/zwirn-doux/Zwirn/Doux/Process.hs

{-# LANGUAGE BangPatterns #-}

module Zwirn.Doux.Process where

import Control.Concurrent (MVar, forkIO, modifyMVar_, threadDelay)
import Control.Concurrent.MVar (readMVar)
import Control.Monad (void)
import Data.Bifunctor (Bifunctor (..))
import Data.List
import qualified Data.Map as Map
import qualified Data.Text as T
import Data.Tuple (swap)
import Data.Word (Word64)
import Sound.Doux.Engine
import Sound.Tidal.Clock
import qualified Sound.Tidal.Clock as Clock
import Sound.Tidal.Link (Micros, SessionState, clock)
import Zwirn.Core.Query (findAllValuesWithTimePrec, findAllValuesWithTimeStatePrec)
import qualified Zwirn.Core.Time as Z
import Zwirn.Core.Types (ToList (..), Value (..), unzwirn)
import Zwirn.Language.Evaluate (Expression (..), ExpressionMap, toExp)
import Zwirn.Language.Evaluate.Expression (Zwirn)
import Zwirn.Language.Play

tickAction ::
  Doux ->
  MVar PlayMap ->
  MVar ActionMap ->
  MVar BusMap ->
  MVar ExpressionMap ->
  Time ->
  (Time, Time) ->
  Double ->
  ClockConfig ->
  ClockRef ->
  (SessionState, SessionState) ->
  IO ()
tickAction d pMV acMV busMV stMV prec (star, end) nudge cconf cref (ss, _) = do
  vs <- processPlayMap prec (star, end) pMV stMV
  bs <- processBusMap prec (star, end) busMV stMV
  as <- processActionMap prec (star, end) acMV stMV
  mapM_ (tickAndSend d False nudge cconf cref ss) vs
  mapM_ (tickAndSendBus d nudge cconf cref ss) bs
  mapM_ (execAction nudge cconf cref ss) as
  updateTempo cconf cref stMV

processPlayMap :: Time -> (Time, Time) -> MVar PlayMap -> MVar ExpressionMap -> IO [(Z.Time, Expression)]
processPlayMap prec (star, end) pMV stMV = do
  pm <- readMVar pMV
  let ps = resolvePlayMap pm
  st <- readMVar stMV

  let (enst, vs) = mapAccumL (\ !s (Targeted _ p) -> swap $ findAllValuesWithTimeStatePrec (Z.Time prec 0) (Z.Time (align prec star) 1, Z.Time (align prec end) 1) s p) st ps
  modifyMVar_ stMV (const $ return enst)
  return $ concat vs

processBusMap :: Time -> (Time, Time) -> MVar BusMap -> MVar ExpressionMap -> IO [(Int, Z.Time, Expression)]
processBusMap prec (star, end) busMV stMV = do
  bm <- readMVar busMV
  let bs = Map.toList bm
  st <- readMVar stMV

  return $ concatMap (\(i, Targeted _ p) -> map (\(x, y) -> (i, x, y)) $ findAllValuesWithTimePrec (Z.Time prec 0) (Z.Time (align prec star) 1, Z.Time (align prec end) 1) st p) bs

processActionMap :: Time -> (Time, Time) -> MVar ActionMap -> MVar ExpressionMap -> IO [(Z.Time, IO ())]
processActionMap prec (star, end) zMV stMV = do
  pm <- readMVar zMV
  let ps = Map.elems pm
  st <- readMVar stMV

  let (_, vs) = mapAccumL (\ !s p -> swap $ findAllValuesWithTimeStatePrec (Z.Time prec 0) (Z.Time (align prec star) 1, Z.Time (align prec end) 1) s p) st ps

  return $ map (second expressionToAction) (concat vs)

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

expressionToPath :: Expression -> IO String
expressionToPath (ENum x) = return $ show x
expressionToPath (EText x) = return $ T.unpack x
expressionToPath (EMap x) = do
  xs <- mapM (\(k, v) -> (\y -> T.unpack k ++ "/" ++ y) <$> expressionToPath v) $ Map.toList x
  return $ intercalate "/" xs
expressionToPath (EAction x) = forkIO x >> return ""
expressionToPath _ = return ""

expressionToAction :: Expression -> IO ()
expressionToAction (EAction x) = x
expressionToAction _ = return ()

expressionToPathSound :: Expression -> Double -> IO (Maybe (String, String))
expressionToPathSound (EMap m) gate = case Map.lookup "s" gm of
  Just s -> do
    samp <- expressionToPath s
    rest <- expressionToPath (EMap $ Map.delete "s" gm)
    return $ Just ("sound/" ++ samp, rest)
  Nothing -> return Nothing
  where
    gm =
      Map.alter altGate "gate"
        . Map.alter altOrbitFx "feedback"
        . Map.alter altOrbitFx "verb"
        . Map.alter altOrbitFx "delay"
        . Map.alter altOrbitFx "comb"
        $ m
    altGate (Just x) = Just x
    altGate Nothing = Just (ENum gate)
    altOrbitFx (Just x) = Just x
    altOrbitFx Nothing = Just (ENum 0)
expressionToPathSound (EAction x) _ = forkIO x >> return Nothing
expressionToPathSound _ _ = return Nothing

tickAndSend :: Doux -> Bool -> Double -> ClockConfig -> ClockRef -> SessionState -> (Z.Time, Expression) -> IO ()
tickAndSend d immediate nudge cconf cref ss (Z.Time r s, ex) = do
  let onBeat = Clock.cyclesToBeat cconf (fromRational r)

  on <- Clock.timeAtBeat cconf ss onBeat
  tic <- scheduleAtLink d cref nudge on
  cps <- getCPS cconf cref

  let gate = fromRational $ 1 / (cps * s) :: Double

  mpath <- expressionToPathSound ex gate

  case mpath of
    Just (sound, path) -> if immediate then void (eval d (sound ++ "/" ++ path)) else void (eval d (sound ++ "/tick/" ++ show tic ++ "/" ++ path))
    Nothing -> return ()

tickAndSendBus :: Doux -> Double -> ClockConfig -> ClockRef -> SessionState -> (Int, Z.Time, Expression) -> IO ()
tickAndSendBus d nudge cconf cref ss (bus, t, ex) = do
  let onBeat = Clock.cyclesToBeat cconf ((\(Z.Time r _) -> fromRational r :: Double) t)

  on <- Clock.timeAtBeat cconf ss onBeat
  tic <- scheduleAtLink d cref nudge on

  path <- expressionToPath ex

  void (eval d ("tick/" ++ show tic ++ "/" ++ path ++ "/voice/" ++ show bus))

-- | execAction runs actions in the action map. this is quite inefficient, since for every action a separate green thread is spawned
-- | eventually we should probably have a separate thread for running actions that reacts to a queue
execAction :: Double -> ClockConfig -> ClockRef -> SessionState -> (Z.Time, IO ()) -> IO ()
execAction nudge cconf cref ss (t, action) = do
  let onBeat = Clock.cyclesToBeat cconf ((\(Z.Time r _) -> fromRational r :: Double) t)
  on <- Clock.timeAtBeat cconf ss onBeat
  nowLink <- clock (rAbletonLink cref)
  let delay = max 0 (on - nowLink - round (nudge * 1000))
  void $ forkIO $ do
    threadDelay (fromIntegral delay)
    action

updateTempo :: ClockConfig -> ClockRef -> MVar ExpressionMap -> IO ()
updateTempo cconf cref stMV = do
  bpm <- realToFrac <$> Clock.getBPM cref
  cps <- realToFrac <$> Clock.getCPS cconf cref
  modifyMVar_ stMV (return . Map.insert "_cps" (toExp (pure cps :: Zwirn Double)) . Map.insert "_tempo" (toExp (pure bpm :: Zwirn Double)))

tickActionOnce ::
  Doux ->
  MVar PlayMap ->
  MVar ExpressionMap ->
  Time ->
  (Time, Time) ->
  Double ->
  ClockConfig ->
  ClockRef ->
  (SessionState, SessionState) ->
  IO ()
tickActionOnce d pMV stMV _ (star, _) nudge cconf cref (ss, _) = do
  vs <- processPlayMapOnce star pMV stMV
  mapM_ (tickAndSend d True nudge cconf cref ss) vs

scheduleAtLink :: Doux -> ClockRef -> Double -> Micros -> IO Word64
scheduleAtLink doux cref nudge targetLinkTime = do
  liveTick <- currentTick doux
  nowLink <- clock (rAbletonLink cref)
  sr <- sampleRate doux
  let deltaMicros = targetLinkTime - nowLink
      deltaTicks = round (fromIntegral deltaMicros * realToFrac sr / 1000000 :: Double)
      nudgeTicks = round (nudge * 1000 * realToFrac sr / 1000000) :: Word64
      actual = if liveTick + deltaTicks > nudgeTicks then liveTick + deltaTicks - nudgeTicks else liveTick + deltaTicks
  return actual

processPlayMapOnce :: Time -> MVar PlayMap -> MVar ExpressionMap -> IO [(Z.Time, Expression)]
processPlayMapOnce star pMV stMV = do
  pm <- readMVar pMV
  let ps = resolvePlayMap pm
  st <- readMVar stMV

  let func !s (Targeted _ p) = (s, map (\(v, _) -> (Z.Time star (Z.tDiff $ time v), value v)) $ toList $ unzwirn p (Z.Time star 1) s)
      (enst, vs) = mapAccumL func st ps
  modifyMVar_ stMV (const $ return enst)
  return $ concat vs