Skip to content
This repository was archived by the owner on Jun 13, 2025. It is now read-only.

Commit 1bf3826

Browse files
authored
Merge pull request #1090 from polymorphicengine/dev
simplify the clock logic by removing LinkOperations
2 parents dc74821 + 1300230 commit 1bf3826

File tree

5 files changed

+89
-109
lines changed

5 files changed

+89
-109
lines changed

src/Sound/Tidal/Stream/Process.hs

Lines changed: 27 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import Control.Monad (forM_, when)
3434
import Data.Coerce (coerce)
3535
import qualified Data.Map.Strict as Map
3636
import Data.Maybe (catMaybes, fromJust, fromMaybe)
37-
import Foreign.C.Types
3837
import System.IO (hPutStrLn, stderr)
3938

4039
import qualified Sound.Osc.Fd as O
@@ -47,7 +46,6 @@ import qualified Sound.Tidal.Link as Link
4746
import Sound.Tidal.Params (pS)
4847
import Sound.Tidal.Pattern
4948
import Sound.Tidal.Show ()
50-
import Sound.Tidal.Stream.Config
5149
import Sound.Tidal.Utils ((!!!))
5250

5351
import Sound.Tidal.Stream.Target
@@ -57,7 +55,7 @@ data ProcessedEvent =
5755
ProcessedEvent {
5856
peHasOnset :: Bool,
5957
peEvent :: Event ValueMap,
60-
peCps :: Link.BPM,
58+
peCps :: Double,
6159
peDelta :: Link.Micros,
6260
peCycle :: Time,
6361
peOnWholeOrPart :: Link.Micros,
@@ -88,9 +86,11 @@ doTick :: MVar ValueMap -- pattern state
8886
-> Maybe O.Udp -- network socket
8987
-> (Time,Time) -- current arc
9088
-> Double -- nudge
91-
-> Clock.LinkOperations -- ableton link operations
89+
-> Clock.ClockConfig -- config of the clock
90+
-> Clock.ClockRef -- reference to the clock
91+
-> (Link.SessionState, Link.SessionState) -- second session state is for keeping track of tempo changes
9292
-> IO ()
93-
doTick stateMV busMV playMV globalFMV cxs listen (st,end) nudge ops =
93+
doTick stateMV busMV playMV globalFMV cxs listen (st,end) nudge cconf cref (ss, temposs) =
9494
E.handle (\ (e :: E.SomeException) -> do
9595
hPutStrLn stderr $ "Failed to Stream.doTick: " ++ show e
9696
hPutStrLn stderr $ "Return to previous pattern."
@@ -99,10 +99,10 @@ doTick stateMV busMV playMV globalFMV cxs listen (st,end) nudge ops =
9999
pMap <- readMVar playMV
100100
busses <- readMVar busMV
101101
sGlobalF <- readMVar globalFMV
102-
bpm <- (Clock.getTempo ops)
102+
bpm <- Clock.getTempo ss
103103
let
104104
patstack = sGlobalF $ playStack pMap
105-
cps = ((Clock.beatToCycles ops) bpm) / 60
105+
cps = ((Clock.beatToCycles cconf) $ fromRational bpm) / 60
106106
sMap' = Map.insert "_cps" (VF $ coerce cps) sMap
107107
extraLatency = nudge
108108
-- First the state is used to query the pattern
@@ -112,7 +112,7 @@ doTick stateMV busMV playMV globalFMV cxs listen (st,end) nudge ops =
112112
)
113113
-- Then it's passed through the events
114114
(sMap'', es') = resolveState sMap' es
115-
tes <- processCps ops es'
115+
tes <- processCps cconf cref (ss, temposs) es'
116116
-- For each OSC target
117117
forM_ cxs $ \cx@(Cx target _ oscs _ _) -> do
118118
-- Latency is configurable per target.
@@ -124,27 +124,29 @@ doTick stateMV busMV playMV globalFMV cxs listen (st,end) nudge ops =
124124
hPutStrLn stderr $ "Failed to send. Is the '" ++ oName target ++ "' target running? " ++ show e
125125
putMVar stateMV sMap'')
126126

127-
processCps :: Clock.LinkOperations -> [Event ValueMap] -> IO [ProcessedEvent]
128-
processCps ops = mapM processEvent
127+
processCps :: Clock.ClockConfig -> Clock.ClockRef -> (Link.SessionState, Link.SessionState) -> [Event ValueMap] -> IO [ProcessedEvent]
128+
processCps cconf cref (ss, temposs) = mapM processEvent
129129
where
130130
processEvent :: Event ValueMap -> IO ProcessedEvent
131131
processEvent e = do
132132
let wope = wholeOrPart e
133133
partStartCycle = start $ part e
134-
partStartBeat = (Clock.cyclesToBeat ops) (realToFrac partStartCycle)
134+
partStartBeat = (Clock.cyclesToBeat cconf) (realToFrac partStartCycle)
135135
onCycle = start wope
136-
onBeat = (Clock.cyclesToBeat ops) (realToFrac onCycle)
136+
onBeat = (Clock.cyclesToBeat cconf) (realToFrac onCycle)
137137
offCycle = stop wope
138-
offBeat = (Clock.cyclesToBeat ops) (realToFrac offCycle)
139-
on <- (Clock.timeAtBeat ops) onBeat
140-
onPart <- (Clock.timeAtBeat ops) partStartBeat
138+
offBeat = (Clock.cyclesToBeat cconf) (realToFrac offCycle)
139+
on <- Clock.timeAtBeat cconf ss onBeat
140+
onPart <- Clock.timeAtBeat cconf ss partStartBeat
141141
when (eventHasOnset e) (do
142142
let cps' = Map.lookup "cps" (value e) >>= getF
143-
maybe (return ()) (\newCps -> (Clock.setTempo ops) ((Clock.cyclesToBeat ops) (newCps * 60)) on) $ coerce cps'
143+
maybe (return ()) (\newCps -> Clock.setTempoCPS newCps on cconf temposs) (fmap toRational cps')
144144
)
145-
off <- (Clock.timeAtBeat ops) offBeat
146-
bpm <- (Clock.getTempo ops)
147-
let cps = ((Clock.beatToCycles ops) bpm) / 60
145+
off <- Clock.timeAtBeat cconf ss offBeat
146+
bpm <- Clock.getTempo ss
147+
wholeOrPartOsc <- Clock.linkToOscTime cref on
148+
onPartOsc <- Clock.linkToOscTime cref onPart
149+
let cps = ((Clock.beatToCycles cconf) $ fromRational bpm) / 60
148150
let delta = off - on
149151
return $! ProcessedEvent {
150152
peHasOnset = eventHasOnset e,
@@ -153,9 +155,9 @@ processCps ops = mapM processEvent
153155
peDelta = delta,
154156
peCycle = onCycle,
155157
peOnWholeOrPart = on,
156-
peOnWholeOrPartOsc = (Clock.linkToOscTime ops) on,
158+
peOnWholeOrPartOsc = wholeOrPartOsc,
157159
peOnPart = onPart,
158-
peOnPartOsc = (Clock.linkToOscTime ops) onPart
160+
peOnPartOsc = onPartOsc
159161
}
160162

161163

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

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

306299

307300
-- Used for Tempo callback

src/Sound/Tidal/Stream/UI.hs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,13 @@ streamReplace stream k !pat = do
6767
hPutStrLn stderr $ "Return to previous pattern."
6868
setPreviousPatternOrSilence (sPMapMV stream)) (updatePattern stream k t pat)
6969

70-
-- = modifyMVar_ (sActionsMV s) (\actions -> return $ (T.StreamReplace k pat) : actions)
71-
7270
-- streamFirst but with random cycle instead of always first cicle
7371
streamOnce :: Stream -> ControlPattern -> IO ()
7472
streamOnce st p = do i <- getStdRandom $ randomR (0, 8192)
7573
streamFirst st $ rotL (toRational (i :: Int)) p
7674

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

8078
streamMute :: Stream -> ID -> IO ()
8179
streamMute s k = withPatIds s [k] (\x -> x {psMute = True})

tidal-link/src/hs/Sound/Tidal/Clock.hs

Lines changed: 59 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -56,29 +56,16 @@ data ClockConfig
5656
}
5757

5858
-- | action to be executed on a tick,
59-
-- | given the current timespan and nudge
59+
-- | given the current timespan, nudge and reference to the clock
6060
type TickAction
61-
= (Time,Time) -> Double -> LinkOperations -> IO ()
62-
63-
-- | link operations for easy interaction with the clock
64-
data LinkOperations
65-
= LinkOperations
66-
{timeAtBeat :: Link.Beat -> IO Link.Micros
67-
,timeToCycles :: Link.Micros -> IO Time
68-
,getTempo :: IO Link.BPM
69-
,setTempo :: Link.BPM -> Link.Micros -> IO ()
70-
,linkToOscTime :: Link.Micros -> O.Time
71-
,beatToCycles :: CDouble -> CDouble
72-
,cyclesToBeat :: CDouble -> CDouble
73-
}
61+
= (Time,Time) -> Double -> ClockConfig -> ClockRef -> (Link.SessionState, Link.SessionState) -> IO ()
7462

7563
-- | possible actions for interacting with the clock
7664
data ClockAction
7765
= NoAction
7866
| SetCycle Time
7967
| SetTempo Time
8068
| SetNudge Double
81-
deriving Show
8269

8370
defaultCps :: Double
8471
defaultCps = 0.575
@@ -187,34 +174,19 @@ tick = do
187174
-- hands the current link operations to the TickAction
188175
clockProcess :: Clock ()
189176
clockProcess = do
190-
(ClockMemory config (ClockRef _ abletonLink) action) <- ask
177+
(ClockMemory config ref@(ClockRef _ abletonLink) action) <- ask
191178
st <- get
192179
let logicalEnd = logicalTime config (start st) $ ticks st + 1
193180
startCycle = arcEnd $ nowArc st
194181

195182
sessionState <- liftIO $ Link.createAndCaptureAppSessionState abletonLink
196-
endCycle <- liftIO $ timeToCycles' config sessionState logicalEnd
197-
198-
let st' = st {nowArc = (startCycle,endCycle)}
199-
200-
nowOsc <- O.time
201-
nowLink <- liftIO $ Link.clock abletonLink
183+
endCycle <- liftIO $ timeToCycles config sessionState logicalEnd
202184

203-
let ops = LinkOperations {
204-
timeAtBeat = \beat -> Link.timeAtBeat sessionState beat (cQuantum config) ,
205-
timeToCycles = timeToCycles' config sessionState,
206-
getTempo = Link.getTempo sessionState,
207-
setTempo = Link.setTempo sessionState,
208-
linkToOscTime = \lt -> addMicrosToOsc (lt - nowLink) nowOsc,
209-
beatToCycles = \beat -> beat / (cBeatsPerCycle config),
210-
cyclesToBeat = \cyc -> cyc * (cBeatsPerCycle config)
211-
}
212-
213-
liftIO $ action (nowArc st') (nudged st') ops
185+
liftIO $ action (startCycle,endCycle) (nudged st) config ref (sessionState, sessionState)
214186

215187
liftIO $ Link.commitAndDestroyAppSessionState abletonLink sessionState
216188

217-
put st'
189+
put (st {nowArc = (startCycle,endCycle)})
218190
tick
219191

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

242214
---------------------------------------------------------------
243-
-------------------- helper functions -------------------------
215+
----------- functions representing link operations ------------
244216
---------------------------------------------------------------
245217

246218
arcStart :: (Time, Time) -> Time
@@ -249,8 +221,37 @@ arcStart = fst
249221
arcEnd :: (Time, Time) -> Time
250222
arcEnd = snd
251223

252-
timeToCycles' :: ClockConfig -> Link.SessionState -> Link.Micros -> IO Time
253-
timeToCycles' config ss time = do
224+
beatToCycles :: ClockConfig -> Double -> Double
225+
beatToCycles config beat = beat / (coerce $ cBeatsPerCycle config)
226+
227+
cyclesToBeat :: ClockConfig -> Double -> Double
228+
cyclesToBeat config cyc = cyc * (coerce $ cBeatsPerCycle config)
229+
230+
getSessionState :: ClockRef -> IO Link.SessionState
231+
getSessionState (ClockRef _ abletonLink) = Link.createAndCaptureAppSessionState abletonLink
232+
233+
-- onSingleTick assumes it runs at beat 0.
234+
-- The best way to achieve that is to use forceBeatAtTime.
235+
-- But using forceBeatAtTime means we can not commit its session state.
236+
getZeroedSessionState :: ClockConfig -> ClockRef -> IO Link.SessionState
237+
getZeroedSessionState config (ClockRef _ abletonLink) = do
238+
ss <- Link.createAndCaptureAppSessionState abletonLink
239+
nowLink <- liftIO $ Link.clock abletonLink
240+
Link.forceBeatAtTime ss 0 (nowLink + processAhead) (cQuantum config)
241+
return ss
242+
where processAhead = round $ (cProcessAhead config) * 1000000
243+
244+
getTempo :: Link.SessionState -> IO Time
245+
getTempo ss = fmap toRational $ Link.getTempo ss
246+
247+
setTempoCPS :: Time -> Link.Micros -> ClockConfig -> Link.SessionState -> IO ()
248+
setTempoCPS cps now conf ss = Link.setTempo ss (coerce $ cyclesToBeat conf ((fromRational cps) * 60)) now
249+
250+
timeAtBeat :: ClockConfig -> Link.SessionState -> Double -> IO Link.Micros
251+
timeAtBeat config ss beat = Link.timeAtBeat ss (coerce beat) (cQuantum config)
252+
253+
timeToCycles :: ClockConfig -> Link.SessionState -> Link.Micros -> IO Time
254+
timeToCycles config ss time = do
254255
beat <- Link.beatAtTime ss time (cQuantum config)
255256
return $! (toRational beat) / (toRational (cBeatsPerCycle config))
256257

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

264+
linkToOscTime :: ClockRef -> Link.Micros -> IO O.Time
265+
linkToOscTime (ClockRef _ abletonLink) lt = do
266+
nowOsc <- O.time
267+
nowLink <- liftIO $ Link.clock abletonLink
268+
return $ addMicrosToOsc (lt - nowLink) nowOsc
269+
263270
addMicrosToOsc :: Link.Micros -> O.Time -> O.Time
264271
addMicrosToOsc m t = ((fromIntegral m) / 1000000) + t
265272

@@ -288,42 +295,10 @@ getCycleTime :: ClockConfig -> ClockRef -> IO Time
288295
getCycleTime config (ClockRef _ abletonLink) = do
289296
now <- Link.clock abletonLink
290297
ss <- Link.createAndCaptureAppSessionState abletonLink
291-
c <- timeToCycles' config ss now
298+
c <- timeToCycles config ss now
292299
Link.destroySessionState ss
293300
return $! c
294301

295-
-- onSingleTick assumes it runs at beat 0.
296-
-- The best way to achieve that is to use forceBeatAtTime.
297-
-- But using forceBeatAtTime means we can not commit its session state.
298-
-- Another session state, which we will commit,
299-
-- is introduced to keep track of tempo changes.
300-
getZeroedLinkOperations :: ClockConfig -> ClockRef -> IO LinkOperations
301-
getZeroedLinkOperations config (ClockRef _ abletonLink) = do
302-
sessionState <- Link.createAndCaptureAppSessionState abletonLink
303-
zeroedSessionState <- Link.createAndCaptureAppSessionState abletonLink
304-
305-
nowOsc <- O.time
306-
nowLink <- Link.clock abletonLink
307-
308-
Link.forceBeatAtTime zeroedSessionState 0 (nowLink + processAhead) (cQuantum config)
309-
310-
Link.commitAndDestroyAppSessionState abletonLink sessionState
311-
Link.destroySessionState zeroedSessionState
312-
313-
return $ LinkOperations {
314-
timeAtBeat = \beat -> Link.timeAtBeat zeroedSessionState beat (cQuantum config),
315-
timeToCycles = timeToCycles' config zeroedSessionState,
316-
getTempo = Link.getTempo zeroedSessionState,
317-
setTempo = \bpm micros ->
318-
Link.setTempo zeroedSessionState bpm micros >>
319-
Link.setTempo sessionState bpm micros,
320-
linkToOscTime = \lt -> addMicrosToOsc (lt - nowLink) nowOsc,
321-
beatToCycles = \beat -> beat / (cBeatsPerCycle config),
322-
cyclesToBeat = \cyc -> cyc * (cBeatsPerCycle config)
323-
}
324-
where processAhead = round $ (cProcessAhead config) * 1000000
325-
326-
327302
resetClock :: ClockRef -> IO ()
328303
resetClock clock = setClock clock 0
329304

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

330+
-- Used for Tempo callback
331+
-- Tempo changes will be applied.
332+
-- However, since the full arc is processed at once and since Link does not support
333+
-- scheduling, tempo change may affect scheduling of events that happen earlier
334+
-- in the normal stream (the one handled by onTick).
335+
clockOnce :: TickAction -> ClockConfig -> ClockRef -> IO ()
336+
clockOnce action config ref@(ClockRef _ abletonLink) = do
337+
ss <- getZeroedSessionState config ref
338+
temposs <- Link.createAndCaptureAppSessionState abletonLink
339+
-- The nowArc is a full cycle
340+
action (0,1) 0 config ref (ss, temposs)
341+
Link.destroySessionState ss
342+
Link.commitAndDestroyAppSessionState abletonLink temposs
343+
355344
disableLink :: ClockRef -> IO ()
356345
disableLink (ClockRef _ abletonLink) = Link.disable abletonLink
357346

tidal-link/tidal-link.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
cabal-version: 2.4
22
name: tidal-link
3-
version: 1.0.3
3+
version: 1.0.4
44
synopsis: Ableton Link integration for Tidal
55
-- description:
66
homepage: http://tidalcycles.org/

tidal.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ library
7373
, random < 1.3
7474
, exceptions < 0.11
7575
, mtl >= 2.2
76-
, tidal-link == 1.0.3
76+
, tidal-link == 1.0.4
7777

7878
test-suite tests
7979
type: exitcode-stdio-1.0

0 commit comments

Comments
 (0)