Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

simplify the clock logic by removing LinkOperations #1090

Merged
merged 6 commits into from
Jul 30, 2024
61 changes: 27 additions & 34 deletions src/Sound/Tidal/Stream/Process.hs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import Control.Monad (forM_, when)
import Data.Coerce (coerce)
import qualified Data.Map.Strict as Map
import Data.Maybe (catMaybes, fromJust, fromMaybe)
import Foreign.C.Types
import System.IO (hPutStrLn, stderr)

import qualified Sound.Osc.Fd as O
Expand All @@ -47,7 +46,6 @@ import qualified Sound.Tidal.Link as Link
import Sound.Tidal.Params (pS)
import Sound.Tidal.Pattern
import Sound.Tidal.Show ()
import Sound.Tidal.Stream.Config
import Sound.Tidal.Utils ((!!!))

import Sound.Tidal.Stream.Target
Expand All @@ -57,7 +55,7 @@ data ProcessedEvent =
ProcessedEvent {
peHasOnset :: Bool,
peEvent :: Event ValueMap,
peCps :: Link.BPM,
peCps :: Double,
peDelta :: Link.Micros,
peCycle :: Time,
peOnWholeOrPart :: Link.Micros,
Expand Down Expand Up @@ -88,9 +86,11 @@ doTick :: MVar ValueMap -- pattern state
-> Maybe O.Udp -- network socket
-> (Time,Time) -- current arc
-> Double -- nudge
-> Clock.LinkOperations -- ableton link operations
-> Clock.ClockConfig -- config of the clock
-> Clock.ClockRef -- reference to the clock
-> (Link.SessionState, Link.SessionState) -- second session state is for keeping track of tempo changes
-> IO ()
doTick stateMV busMV playMV globalFMV cxs listen (st,end) nudge ops =
doTick stateMV busMV playMV globalFMV cxs listen (st,end) nudge cconf cref (ss, temposs) =
E.handle (\ (e :: E.SomeException) -> do
hPutStrLn stderr $ "Failed to Stream.doTick: " ++ show e
hPutStrLn stderr $ "Return to previous pattern."
Expand All @@ -99,10 +99,10 @@ doTick stateMV busMV playMV globalFMV cxs listen (st,end) nudge ops =
pMap <- readMVar playMV
busses <- readMVar busMV
sGlobalF <- readMVar globalFMV
bpm <- (Clock.getTempo ops)
bpm <- Clock.getTempo ss
let
patstack = sGlobalF $ playStack pMap
cps = ((Clock.beatToCycles ops) bpm) / 60
cps = ((Clock.beatToCycles cconf) $ fromRational bpm) / 60
sMap' = Map.insert "_cps" (VF $ coerce cps) sMap
extraLatency = nudge
-- First the state is used to query the pattern
Expand All @@ -112,7 +112,7 @@ doTick stateMV busMV playMV globalFMV cxs listen (st,end) nudge ops =
)
-- Then it's passed through the events
(sMap'', es') = resolveState sMap' es
tes <- processCps ops es'
tes <- processCps cconf cref (ss, temposs) es'
-- For each OSC target
forM_ cxs $ \cx@(Cx target _ oscs _ _) -> do
-- Latency is configurable per target.
Expand All @@ -124,27 +124,29 @@ doTick stateMV busMV playMV globalFMV cxs listen (st,end) nudge ops =
hPutStrLn stderr $ "Failed to send. Is the '" ++ oName target ++ "' target running? " ++ show e
putMVar stateMV sMap'')

processCps :: Clock.LinkOperations -> [Event ValueMap] -> IO [ProcessedEvent]
processCps ops = mapM processEvent
processCps :: Clock.ClockConfig -> Clock.ClockRef -> (Link.SessionState, Link.SessionState) -> [Event ValueMap] -> IO [ProcessedEvent]
processCps cconf cref (ss, temposs) = mapM processEvent
where
processEvent :: Event ValueMap -> IO ProcessedEvent
processEvent e = do
let wope = wholeOrPart e
partStartCycle = start $ part e
partStartBeat = (Clock.cyclesToBeat ops) (realToFrac partStartCycle)
partStartBeat = (Clock.cyclesToBeat cconf) (realToFrac partStartCycle)
onCycle = start wope
onBeat = (Clock.cyclesToBeat ops) (realToFrac onCycle)
onBeat = (Clock.cyclesToBeat cconf) (realToFrac onCycle)
offCycle = stop wope
offBeat = (Clock.cyclesToBeat ops) (realToFrac offCycle)
on <- (Clock.timeAtBeat ops) onBeat
onPart <- (Clock.timeAtBeat ops) partStartBeat
offBeat = (Clock.cyclesToBeat cconf) (realToFrac offCycle)
on <- Clock.timeAtBeat cconf ss onBeat
onPart <- Clock.timeAtBeat cconf ss partStartBeat
when (eventHasOnset e) (do
let cps' = Map.lookup "cps" (value e) >>= getF
maybe (return ()) (\newCps -> (Clock.setTempo ops) ((Clock.cyclesToBeat ops) (newCps * 60)) on) $ coerce cps'
maybe (return ()) (\newCps -> Clock.setTempoCPS newCps on cconf temposs) (fmap toRational cps')
)
off <- (Clock.timeAtBeat ops) offBeat
bpm <- (Clock.getTempo ops)
let cps = ((Clock.beatToCycles ops) bpm) / 60
off <- Clock.timeAtBeat cconf ss offBeat
bpm <- Clock.getTempo ss
wholeOrPartOsc <- Clock.linkToOscTime cref on
onPartOsc <- Clock.linkToOscTime cref onPart
let cps = ((Clock.beatToCycles cconf) $ fromRational bpm) / 60
let delta = off - on
return $! ProcessedEvent {
peHasOnset = eventHasOnset e,
Expand All @@ -153,9 +155,9 @@ processCps ops = mapM processEvent
peDelta = delta,
peCycle = onCycle,
peOnWholeOrPart = on,
peOnWholeOrPartOsc = (Clock.linkToOscTime ops) on,
peOnWholeOrPartOsc = wholeOrPartOsc,
peOnPart = onPart,
peOnPartOsc = (Clock.linkToOscTime ops) onPart
peOnPartOsc = onPartOsc
}


Expand All @@ -182,7 +184,7 @@ toOSC busses pe osc@(OSC _ _)
-- Only events that start within the current nowArc are included
playmsg | peHasOnset pe = do
-- If there is already cps in the event, the union will preserve that.
let extra = Map.fromList [("cps", (VF (coerce $! peCps pe))),
let extra = Map.fromList [("cps", (VF (peCps pe))),
("delta", VF (Clock.addMicrosToOsc (peDelta pe) 0)),
("cycle", VF (fromRational (peCycle pe)))
]
Expand Down Expand Up @@ -283,25 +285,16 @@ playStack pMap = stack . (map psPattern) . (filter active) . Map.elems $ pMap
hasSolo :: Map.Map k PlayState -> Bool
hasSolo = (>= 1) . length . filter psSolo . Map.elems


-- Used for Tempo callback
-- Tempo changes will be applied.
-- However, since the full arc is processed at once and since Link does not support
-- scheduling, tempo change may affect scheduling of events that happen earlier
-- in the normal stream (the one handled by onTick).
onSingleTick :: Config -> Clock.ClockRef -> MVar ValueMap -> MVar [Int] -> MVar PlayMap -> MVar (ControlPattern -> ControlPattern) -> [Cx] -> Maybe O.Udp -> ControlPattern -> IO ()
onSingleTick config clockRef stateMV busMV _ globalFMV cxs listen pat = do
ops <- Clock.getZeroedLinkOperations (cClockConfig config) clockRef
onSingleTick :: Clock.ClockConfig -> Clock.ClockRef -> MVar ValueMap -> MVar [Int] -> MVar PlayMap -> MVar (ControlPattern -> ControlPattern) -> [Cx] -> Maybe O.Udp -> ControlPattern -> IO ()
onSingleTick clockConfig clockRef stateMV busMV _ globalFMV cxs listen pat = do
pMapMV <- newMVar $ Map.singleton "fake"
(PlayState {psPattern = pat,
psMute = False,
psSolo = False,
psHistory = []
}
)
-- The nowArc is a full cycle
doTick stateMV busMV pMapMV globalFMV cxs listen (0,1) 0 ops

Clock.clockOnce (doTick stateMV busMV pMapMV globalFMV cxs listen) clockConfig clockRef


-- Used for Tempo callback
Expand Down
4 changes: 1 addition & 3 deletions src/Sound/Tidal/Stream/UI.hs
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,13 @@ streamReplace stream k !pat = do
hPutStrLn stderr $ "Return to previous pattern."
setPreviousPatternOrSilence (sPMapMV stream)) (updatePattern stream k t pat)

-- = modifyMVar_ (sActionsMV s) (\actions -> return $ (T.StreamReplace k pat) : actions)

-- streamFirst but with random cycle instead of always first cicle
streamOnce :: Stream -> ControlPattern -> IO ()
streamOnce st p = do i <- getStdRandom $ randomR (0, 8192)
streamFirst st $ rotL (toRational (i :: Int)) p

streamFirst :: Stream -> ControlPattern -> IO ()
streamFirst stream pat = onSingleTick (sConfig stream) (sClockRef stream) (sStateMV stream) (sBusses stream) (sPMapMV stream) (sGlobalFMV stream) (sCxs stream) (sListen stream) pat
streamFirst stream pat = onSingleTick (cClockConfig $ sConfig stream) (sClockRef stream) (sStateMV stream) (sBusses stream) (sPMapMV stream) (sGlobalFMV stream) (sCxs stream) (sListen stream) pat

streamMute :: Stream -> ID -> IO ()
streamMute s k = withPatIds s [k] (\x -> x {psMute = True})
Expand Down
129 changes: 59 additions & 70 deletions tidal-link/src/hs/Sound/Tidal/Clock.hs
Original file line number Diff line number Diff line change
Expand Up @@ -56,29 +56,16 @@ data ClockConfig
}

-- | action to be executed on a tick,
-- | given the current timespan and nudge
-- | given the current timespan, nudge and reference to the clock
type TickAction
= (Time,Time) -> Double -> LinkOperations -> IO ()

-- | link operations for easy interaction with the clock
data LinkOperations
= LinkOperations
{timeAtBeat :: Link.Beat -> IO Link.Micros
,timeToCycles :: Link.Micros -> IO Time
,getTempo :: IO Link.BPM
,setTempo :: Link.BPM -> Link.Micros -> IO ()
,linkToOscTime :: Link.Micros -> O.Time
,beatToCycles :: CDouble -> CDouble
,cyclesToBeat :: CDouble -> CDouble
}
= (Time,Time) -> Double -> ClockConfig -> ClockRef -> (Link.SessionState, Link.SessionState) -> IO ()

-- | possible actions for interacting with the clock
data ClockAction
= NoAction
| SetCycle Time
| SetTempo Time
| SetNudge Double
deriving Show

defaultCps :: Double
defaultCps = 0.575
Expand Down Expand Up @@ -187,34 +174,19 @@ tick = do
-- hands the current link operations to the TickAction
clockProcess :: Clock ()
clockProcess = do
(ClockMemory config (ClockRef _ abletonLink) action) <- ask
(ClockMemory config ref@(ClockRef _ abletonLink) action) <- ask
st <- get
let logicalEnd = logicalTime config (start st) $ ticks st + 1
startCycle = arcEnd $ nowArc st

sessionState <- liftIO $ Link.createAndCaptureAppSessionState abletonLink
endCycle <- liftIO $ timeToCycles' config sessionState logicalEnd

let st' = st {nowArc = (startCycle,endCycle)}

nowOsc <- O.time
nowLink <- liftIO $ Link.clock abletonLink
endCycle <- liftIO $ timeToCycles config sessionState logicalEnd

let ops = LinkOperations {
timeAtBeat = \beat -> Link.timeAtBeat sessionState beat (cQuantum config) ,
timeToCycles = timeToCycles' config sessionState,
getTempo = Link.getTempo sessionState,
setTempo = Link.setTempo sessionState,
linkToOscTime = \lt -> addMicrosToOsc (lt - nowLink) nowOsc,
beatToCycles = \beat -> beat / (cBeatsPerCycle config),
cyclesToBeat = \cyc -> cyc * (cBeatsPerCycle config)
}

liftIO $ action (nowArc st') (nudged st') ops
liftIO $ action (startCycle,endCycle) (nudged st) config ref (sessionState, sessionState)

liftIO $ Link.commitAndDestroyAppSessionState abletonLink sessionState

put st'
put (st {nowArc = (startCycle,endCycle)})
tick

processAction :: ClockAction -> Clock ()
Expand All @@ -240,7 +212,7 @@ processAction (SetCycle cyc) = do
modify (\st -> st {ticks = 0, start = now, nowArc = (cyc,cyc)})

---------------------------------------------------------------
-------------------- helper functions -------------------------
----------- functions representing link operations ------------
---------------------------------------------------------------

arcStart :: (Time, Time) -> Time
Expand All @@ -249,8 +221,37 @@ arcStart = fst
arcEnd :: (Time, Time) -> Time
arcEnd = snd

timeToCycles' :: ClockConfig -> Link.SessionState -> Link.Micros -> IO Time
timeToCycles' config ss time = do
beatToCycles :: ClockConfig -> Double -> Double
beatToCycles config beat = beat / (coerce $ cBeatsPerCycle config)

cyclesToBeat :: ClockConfig -> Double -> Double
cyclesToBeat config cyc = cyc * (coerce $ cBeatsPerCycle config)

getSessionState :: ClockRef -> IO Link.SessionState
getSessionState (ClockRef _ abletonLink) = Link.createAndCaptureAppSessionState abletonLink

-- onSingleTick assumes it runs at beat 0.
-- The best way to achieve that is to use forceBeatAtTime.
-- But using forceBeatAtTime means we can not commit its session state.
getZeroedSessionState :: ClockConfig -> ClockRef -> IO Link.SessionState
getZeroedSessionState config (ClockRef _ abletonLink) = do
ss <- Link.createAndCaptureAppSessionState abletonLink
nowLink <- liftIO $ Link.clock abletonLink
Link.forceBeatAtTime ss 0 (nowLink + processAhead) (cQuantum config)
return ss
where processAhead = round $ (cProcessAhead config) * 1000000

getTempo :: Link.SessionState -> IO Time
getTempo ss = fmap toRational $ Link.getTempo ss

setTempoCPS :: Time -> Link.Micros -> ClockConfig -> Link.SessionState -> IO ()
setTempoCPS cps now conf ss = Link.setTempo ss (coerce $ cyclesToBeat conf ((fromRational cps) * 60)) now

timeAtBeat :: ClockConfig -> Link.SessionState -> Double -> IO Link.Micros
timeAtBeat config ss beat = Link.timeAtBeat ss (coerce beat) (cQuantum config)

timeToCycles :: ClockConfig -> Link.SessionState -> Link.Micros -> IO Time
timeToCycles config ss time = do
beat <- Link.beatAtTime ss time (cQuantum config)
return $! (toRational beat) / (toRational (cBeatsPerCycle config))

Expand All @@ -260,6 +261,12 @@ cyclesToTime config ss cyc = do
let beat = (fromRational cyc) * (cBeatsPerCycle config)
Link.timeAtBeat ss beat (cQuantum config)

linkToOscTime :: ClockRef -> Link.Micros -> IO O.Time
linkToOscTime (ClockRef _ abletonLink) lt = do
nowOsc <- O.time
nowLink <- liftIO $ Link.clock abletonLink
return $ addMicrosToOsc (lt - nowLink) nowOsc

addMicrosToOsc :: Link.Micros -> O.Time -> O.Time
addMicrosToOsc m t = ((fromIntegral m) / 1000000) + t

Expand Down Expand Up @@ -288,42 +295,10 @@ getCycleTime :: ClockConfig -> ClockRef -> IO Time
getCycleTime config (ClockRef _ abletonLink) = do
now <- Link.clock abletonLink
ss <- Link.createAndCaptureAppSessionState abletonLink
c <- timeToCycles' config ss now
c <- timeToCycles config ss now
Link.destroySessionState ss
return $! c

-- onSingleTick assumes it runs at beat 0.
-- The best way to achieve that is to use forceBeatAtTime.
-- But using forceBeatAtTime means we can not commit its session state.
-- Another session state, which we will commit,
-- is introduced to keep track of tempo changes.
getZeroedLinkOperations :: ClockConfig -> ClockRef -> IO LinkOperations
getZeroedLinkOperations config (ClockRef _ abletonLink) = do
sessionState <- Link.createAndCaptureAppSessionState abletonLink
zeroedSessionState <- Link.createAndCaptureAppSessionState abletonLink

nowOsc <- O.time
nowLink <- Link.clock abletonLink

Link.forceBeatAtTime zeroedSessionState 0 (nowLink + processAhead) (cQuantum config)

Link.commitAndDestroyAppSessionState abletonLink sessionState
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be the source of the bug? We are running commitAndDestroy immediately - before the IO where the session states are used. Compare to clockProcess where I understand we first process the arc using liftIO, then we run commitAndDestroy.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The suggestion in https://github.com/tidalcycles/Tidal/pull/1090/files/81c19870a81294e4b5def9cdc24365fa0b23e726#r1645196629 could be applied here. The callback would invoke both destroy functions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm yes that makes sense, the callback is probably a good idea! thanks!

Link.destroySessionState zeroedSessionState

return $ LinkOperations {
timeAtBeat = \beat -> Link.timeAtBeat zeroedSessionState beat (cQuantum config),
timeToCycles = timeToCycles' config zeroedSessionState,
getTempo = Link.getTempo zeroedSessionState,
setTempo = \bpm micros ->
Link.setTempo zeroedSessionState bpm micros >>
Link.setTempo sessionState bpm micros,
linkToOscTime = \lt -> addMicrosToOsc (lt - nowLink) nowOsc,
beatToCycles = \beat -> beat / (cBeatsPerCycle config),
cyclesToBeat = \cyc -> cyc * (cBeatsPerCycle config)
}
where processAhead = round $ (cProcessAhead config) * 1000000


resetClock :: ClockRef -> IO ()
resetClock clock = setClock clock 0

Expand Down Expand Up @@ -352,6 +327,20 @@ setNudge (ClockRef clock _) n = atomically $ do
NoAction -> modifyTVar' clock (const $ SetNudge n)
_ -> retry

-- Used for Tempo callback
-- Tempo changes will be applied.
-- However, since the full arc is processed at once and since Link does not support
-- scheduling, tempo change may affect scheduling of events that happen earlier
-- in the normal stream (the one handled by onTick).
clockOnce :: TickAction -> ClockConfig -> ClockRef -> IO ()
clockOnce action config ref@(ClockRef _ abletonLink) = do
ss <- getZeroedSessionState config ref
temposs <- Link.createAndCaptureAppSessionState abletonLink
-- The nowArc is a full cycle
action (0,1) 0 config ref (ss, temposs)
Link.destroySessionState ss
Link.commitAndDestroyAppSessionState abletonLink temposs

disableLink :: ClockRef -> IO ()
disableLink (ClockRef _ abletonLink) = Link.disable abletonLink

Expand Down
2 changes: 1 addition & 1 deletion tidal-link/tidal-link.cabal
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cabal-version: 2.4
name: tidal-link
version: 1.0.3
version: 1.0.4
synopsis: Ableton Link integration for Tidal
-- description:
homepage: http://tidalcycles.org/
Expand Down
2 changes: 1 addition & 1 deletion tidal.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ library
, random < 1.3
, exceptions < 0.11
, mtl >= 2.2
, tidal-link == 1.0.3
, tidal-link == 1.0.4

test-suite tests
type: exitcode-stdio-1.0
Expand Down
Loading