Let’s Do The Time Warp - Tidal’s temporal combinators

(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

  • sequential composition (first bd then sn)
  • repetition (oh four times)
  • parallel composition (the comma operator)

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:

  • how do we combine infinite patterns,
  • and why does the above example still work?

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.

Tape Machines

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.

  • at any point in time, each of the machines play their tape at a certain speed
  • speeds may vary: at a fixed time t, machines A may use a speed different from that of machine B; and a fixed machine A may have different speeds at time t_0 than at time t_1.
  • speed can be zero, then the machine is silent.

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.

Combining Tape Machines

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

  • d = 5 (the fifth global tick),
  • m = 0.8 (time after that tick),
  • q = 2 (the second local cycle),
  • r = 1 (the machine that’s active).

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)]