Time-Scaled Streams: Tidal-Cycle’s Applicative and Monadic Structures

(J. Waldmann, Mon 03 Feb 2020)

Tidal-Cycles (short: tidal) https://tidalcycles.org/ by Alex McLean is a system for describing patterns with code. It is applied for musical live-coding. Technically, tidal is an embedded domain-specific language. The host language is Haskell https://www.haskell.org/.

In the present text, I explain how the structure of tidal patterns relates to standard structures Applicative and Monad of Haskell’s standard library. Some knowledge of Haskell is assumed.

I am pretty sure that tidal’s author, and other contributors, have discussed at lengths everything that I am going to say, and more; but I did not find it stated explicitly in official documentation. One likely reason is that the intended audience of that documentation is musicians, not programmers.

Patterns are Streams of Data

The basic model is: a pattern of type a describes data of type a that depends on time, where Time is the non-negative reals, or rationals.

Pattern a = (Time -> a)

In practice, the type argument a ultimately gets instantiated by a record type that contains, as components, control data (e.g., settings for filters, delays) for the audio rendering engine (https://github.com/musikinformatik/SuperDirt, https://supercollider.github.io/).

This model of data as a function of time is known from Functional Reactive Programming. E.g., in it is the Behaviour type of http://hackage.haskell.org/package/threepenny-gui-0.8.3.1/docs/Reactive-Threepenny.html#t:Behavior

It has immediate applicative structure: given a pattern p of functions and a pattern q of arguments, we obtain a pattern p <*> q of results, by applying, at time t, the function p t at the time, to the argument q t at the time.

(<*>) :: Pattern (a -> b) -> Pattern a -> Pattern b
p <*> q = \ t -> p t (q t)

(NB. I like this: it is the S combinator, the subject of my PhD thesis.)

Patterns are piecewise constant

Patterns in tidal actually are piecewise constant. We can imagine that a pattern is represented by a contiguous sequence of intervals, and for each interval, we have a fixed value.

Interval = (Time,Time)    -- start, end
Pattern = [(Interval, a)] -- lazy infinite list

In FRP, this is is the behaviour that results from applying stepper http://hackage.haskell.org/package/threepenny-gui-0.8.3.1/docs/Reactive-Threepenny.html#v:stepper to an event http://hackage.haskell.org/package/threepenny-gui-0.8.3.1/docs/Reactive-Threepenny.html#t:Event

The underlying events are semantically important in computer music: tidal’s basic event is to trigger the playback of a sample, or start a software synthesizer, at a certain time.

Tidal’s patterns have more information than FRP’s behaviours, since we can notice (can hear, actually) subdivisions of constant behaviours: Both of the following expressions describe a constant behaviour (the value is always s "bd"):

pure $ s "bd"
fastcat [pure $ s "bd", pure $ s "bd"]

but they have different representation (first: sequence of intervals of length 1, second: sequence of intervals of length 1/2) and they will sound different, since a sample is triggered at the start of each interval.

More Operations on Patterns

So, tidal patterns represent events, and not just behaviours. This means that there are more ways of combining patterns. In FRP, we can apply http://hackage.haskell.org/package/threepenny-gui-0.8.3.1/docs/Reactive-Threepenny.html#v:apply an event to a behaviour, to obtain an event.

apply :: Behavior (a -> b) -> Event a -> Event b 

This corresponds to sampling the value of the behaviour, each time the event occurs. In Tidal, the equivalent combinator is

(Sound.Tidal.Context.*>)
  :: Pattern (a -> b) -> Pattern a -> Pattern b

NB. The naming is unfortunate since *> already has a diffent meaning and type:

(Control.Applicative.*>)
  :: Applicative f => f a -> f b -> f b

In FRP, we could also have the following: when an event (containing a function) happens, it samples the value of its argument behavior:

apply' :: Event (a -> b) -> Behaviour a -> Event b
apply' e b = apply (fmap (flip ($)) b) e

In Tidal, this is

(Sound.Tidal.Context.<*)
  :: Pattern (a -> b) -> Pattern a -> Pattern b

Note that tidal’s (<*) and (*>) have the same type as (<*>), and one may discuss whether they also fulfil the axioms of Applicative.

Monad (First Installement)

The following function could be used to define instance Monad Behaviour, where Behaviour a = (Time -> a) as in the first section:

join :: Behaviour (Behaviour a) -> Behaviour a
join b = \ t -> (b t) t

For each time t, we sample the “outer” behaviour (b of type Behaviour (Behaviour a)), which will give us an “inner behaviour” (b t of type Behaviour a), which we sample at the same time t.

For a steady stream of events, represented by an infinite list, this join :: [[a]] -> [a] corresponds to taking the diagonal (of an infinite list of infinite lists).

For piecewise constant streams, represented by intervals, the corresponding instance Monad Pattern is in tidal. E.g.,

fastFromList[False,True] >>= \ f -> if not f then run 3 else  5 + run 4
[](0>⅓)|0
[](⅓>½)|1
[](½>¾)|7
[](¾>1)|8

This means that in the first half of the total pattern (where the value of fastFromList[False,True] is False), we see the first half of run 3, and in the second half, we see the second half of 5 + run 4. The function join goes by the name of unwrap. This Monad instance means that parts of patterns go unnoticed. Most parts, in fact - if we combine more.

Here is an example (sounds silly, but show the effect of the operation)

d1 $ (run 8 # irand 2) >>= \ i ->
  if even i
  then  chop 8 $ s "moog" # end 0.33
  else  chop 8 $ s "sitar" # begin 0.2 # end 0.45

where we, at each time t of run 8, flip a coin (irand 2) and pick the slice t of “moog”, or slite t of “sitar”, etc. Note that we needed to chop for that. Else, we’d re-trigger the sample from the start.

Monad (Another)

There is another way to define join/>>= and it’s also useful for music. The outer pattern describes global structure, the inner patterns describe local (short-term) structure. For instance, we can write this

fastFromList[False,False,True] >>>= \ f  ->
  if not f then run 2 else run 3
where p >>>= g  =  squeezeJoin (fmap g p)

[](0>⅙)|0
[](⅙>⅓)|1
[](⅓>½)|0
[](½>⅔)|1
[](⅔>⁷₉)|0
[](⁷₉>⁸₉)|1
[](⁸₉>1)|2

This works by squeezing the inner patterns. For each interval i of the outer pattern p, the corresponding inner pattern q is sped up so that one cycle of q fits i. In the example, run 3 is squeezed into the interval from 2/3 to 1, where the outer pattern has value True.

Note that this could not be done in FRP, as events don’t have an extension. In tidal, we have not events but intervals, and these have endpoints.

NB. Intervals have extension, but patterns have not! A tidal pattern is, conceptually, an infinite list of intervals. In practice, this list may be periodic, but there is no programmatic way to check whether it is indeed, and to determine the period. What we can do, is to speed up (or down) the pattern.