(J. Waldmann, Fri 21 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 tidal combines patterns in time. This is interesting since at first sight, patterns like
[bd sn, oh*4]
seem to use standard operators of
bd
then sn
)oh
four times)as known from other computer music (notation) systems, e.g., https://hackage.haskell.org/package/Euterpea
The comma (parallel) operator is fine, but for bd
then sn
, looks are deceiving: tidal does not have sequential composition - the prime reason being that patterns are endless (omega words, if you’re so inclined). So it makes no sense to put one pattern after another.
I will be discussing these natural questions:
As with my other texts on tidal, this is a result of using the system, reading its source code, and chatting with Alex. The goal is to find out (after the fact) the underlying design principles.
Think of a pattern as tape machine. The tape contains magnetic cells, and the amplitude of their magnetic field encodes music. The tape as sequence of cells is a model for pattern as sequence (list) of events in tidal.
If you press the right buttons, the machine’s transport motor moves the tape in front of the head, inducing currents that we can amplify, send to a loudspeaker, and then hear. In Tidal, events are sent to the supercollider back-end.
Now a combined pattern in tidal should be thought of as a collection of such tape machines, and we are hearing the sum of what they produce.
Each tape is effectively infinite - it has no start and no end. This could be realized by gluing together the ends of a finite tape, to make a loop. The machine has no way of checking whether its tape is a loop, and even if it plays a loop, it has no way to detect its length.
In formal terms, speed is a piecewise constant function of time. So what we hear of the machine is the contents of its tape, where the relation between “time on tape” (as recorded) to “time in the world” (as played) is given by a piecewise linear function.
We use this model to explain oh*4
: it’s not "four times the oh
event but it’s the infinite tape with oh
at each clock tick, played at four times the speed of the clock.
The fundamental composition operator is cat
, also called slowcat
, and written “< … >” in concrete syntax. The semantics of cat [p_1, .. , p_n]
is the function
\ t -> let (d,m) = divMod t 1 -- integer, fractional part
(q,r) = divMod d n -- quotient, remainder
in p_r(q+m)
Example: we evaluate cat [p_0, p_1]
at time t = 5.8. We have
So at global time 5.8, we’ll hear p_1(2.8), the value of patter p_1 at 2.8 local time.
Now, in the case that all patterns p_1, .. p_n are loops of length one, the combined cat [p_1, .. , p_n]
is a loop of length n where each of p_i is played once, in turn. We would get the same result if we cut each loop, concatenate all the tapes, and make a large loop.
map (\e -> (part e, value e))
$ queryArc ("<0 1 2>" :: Pattern Int) (Arc 0 7)
[(0>1,0),(1>2,1),(2>3,2),(3>4,0),(4>5,1),(5>6,2),(6>7,0)]
But this model (concatenation) breaks down as soon as one of the p_i is something different. In the following example, the second pattern has period 2. I will omit the mechanics of printing (map, queryArc) from now on.
"<0 <1 2>>" :: Pattern Int
[(0>1,0),(1>2,1),(2>3,0),(3>4,2),(4>5,0),(5>6,1),(6>7,0)]
"<<0 1><2 3 4>>" :: Pattern Int
[(0>1,0),(1>2,2),(2>3,1),(3>4,3),(4>5,0),(5>6,4),(6>7,1)]
We can slow down (sub)patterns by notating a division
"<<0 1>/2 <2 3 4>>" :: Pattern Int
[(0>1,0),(1>2,2),(2>3,0),(3>4,3),(4>5,1),(5>6,4),(6>7,1)]
"<<0 1> <2 3 4>/2>" :: Pattern Int
[(0>1,0),(1>2,2),(2>3,1),(3>4,2),(4>5,0),(5>6,3),(6>7,1)]
and we can speed up
<<0 1>*2 <2 3 4>/2>" :: Pattern Int
[(0>½,0),(½>1,1),(1>2,2),(2>2½,0),(2½>3,1),(3>4,2),(4>4½,0),(4½>5,1),(5>6,3),(6>6½,0),(6½>7,1)]
When we speed up cat [p_1, .. p_n]
by exactly n (the number of patterns that are combined), then we get fastcat [p_1,.. p_n]
.
"<[0 1] <2 3 4>/2>" :: Pattern Int
[(0>½,0),(½>1,1),(1>2,2),(2>2½,0),(2½>3,1),(3>4,2),(4>4½,0),(4½>5,1),(5>6,3),(6>6½,0),(6½>7,1)]