Some notes from the embedded conference “working out situated universality” hosted by Julian Rohrhuber at IMM Duesseldorf. If you’re reading this on mastodon/the fediverse, it might be easier to read the code directly on my blog.
I started out live coding in a general-purpose language called Perl, as part of Slub. Working with improv drummer Alexandre Garacotche, I was compelled to make a more expressive language for live coding rhythm. Inspired by the Bol Processor, originally for notating Tabla rhythms, I looked for ways of representing cyclic patterns, and ended up making what became known as TidalCycles, or Tidal for short.
Here follows a brief history of Tidal’s representation of rhythm, from the perspective of types.
First, a straightforward tree structure, where sounds could be sequenced within cycles, and layered up as “polymetries”, co-occuring sequences with potentially different meters.
data Event =
Sound String
| Silence
data Structure =
Atom Event
| Cycle [ Structure ]
| Polymetry [ Structure ]
A more functional approach, representing pattern as a function from discrete time to events, along with the period of the cycle.
data Pattern a =
Pattern { at :: Int → [ a ], period :: Int }
This goes well for making grid-based music. But while working with live artist Hester Reeve, I felt the need for more fluid patterns. I returned to a tree structure again, where cycles could contain timespans (known as arcs), which had floating point onset and duration, allowing a freer approach to time. The functional approach was preserved in the Signal
data constructor, but for continuous patterns which continuously vary, rather than having discrete events which begin and end.
data Pattern a =
Atom { event :: a }
| Arc { pattern :: Pattern a,
onset :: Double,
duration :: Maybe Double
}
| Cycle { patterns :: [ Pattern a ]}
| Signal { at :: Double → Pattern a }
Next, a simplification brought by the realisation that discrete patterns could also be represented as functions, from time ranges to events (this is a basic assumption for me, but no-one else seems to see it…). Also, time became rational.
data Pattern a =
Sequence { arc :: Range → [ Event a ] }
| Signal { at :: Rational → [ a ] }
type Event a = ( Range, a )
type Range = ( Rational, Rational )
Dealing with Sequences and Signals within the same type resulted in complex code. This was resolved with the insight that both discrete sequences and continuous signals could be represented as a single, unified ‘pattern’, simply by sampling the midpoint of a range in the latter case.
type Time = Rational
type Arc = ( Time, Time )
type Event a = ( Arc, Arc, a )
data Pattern a = Pattern ( Arc → [ Event a ] )
This proved well-suited for making cycle-based pattern transformations, and as I remember, was also the point where active communities of practice begun to grow around it, starting with a workshop in Barcelona in 2013. Here tidal started shifting from a personal project to a collective one, the source code was always available but now knowledge, use and contribution moved to a community that was also making itself.
At some point later I decided to try making Tidal from scratch, only from memory, as a kind of durational, online performance. I got quite far in two hours, and others said that seeing Tidal being made was useful in helping understand it. Perhaps this relates to the idea of oral culture, of more plastic ideas that are not tied to a particular instance in code. This puts forward the possibility of ‘a tidal’ that can be separate from its codebase.
This helped encourage me to explore porting Tidal to other languages, first to python as vortex, and then to javascript as strudel. I think vortex has some life as part of Raphael Forment’s sardine, and strudel has become popular thanks to Felix Roos quickly picking it up and putting a great deal of work into making it useable with an ecosystem around it.
While it was straightforward to port Tidal’s patterns to languages without strict type systems, I still feel it would have been impossible to write it in something like javascript. So for me, type systems seem more important in helping write and change code, than to running it.
Within the past few years while learning Konnakol, I became frustrated with the opacity of these function of timespans, which make ‘beat’ based transformations impossible. So I sunk some months into separating Signal
and Sequence
again, but unified with a Pattern
typeclass:
data Signal a = Signal {query :: Arc -> [Event a]}
data Sequence a = Atom {atomMetadata :: Metadata,
atomDuration :: Time,
atomInset :: Time, atomOutset :: Time,
atomValue :: Maybe a
}
| Cat [Sequence a]
| Stack [Sequence a]
class (Functor p, Applicative p, Monad p) => Pattern p where
toSignal :: p a -> Signal a
slowcat :: [p a] -> p a
fastcat :: [p a] -> p a
fastcat pats = _fast (toRational $ length pats) $ slowcat pats
_fast :: Time -> p a -> p a
_early :: Time -> p a -> p a
silence :: p a
atom :: a -> p a
stack :: [p a] -> p a
_appAlign :: (a -> p b -> p c) -> Align (p a) (p b) -> p c
rev :: p a -> p a
_ply :: Time -> p a-> p a
_euclid :: Int -> Int -> p a -> p a
timeCat :: [(Time, p a)] -> p a
-- every :: p Int -> (p b -> p b) -> p b -> p b
when :: p Bool -> (p b -> p b) -> p b -> p b
-- listToPat :: [a] -> p a
_iter :: Int -> p a -> p a
_iterBack :: Int -> p a -> p a
collect :: Eq a => p a -> p [a]
uncollect :: p [a] -> p a
_pressBy :: Time -> p a -> p a
innerJoin :: p (p a) -> p a
(<*) :: p (a -> b) -> p a -> p b
(*>) :: p (a -> b) -> p a -> p b
filterOnsets :: p a -> p a
filterValues :: (a -> Bool) -> p a -> p a```
The challenge became to make the Signal
and Sequence
modules as simple as possible, with as many functions as possible defined relative to the Pattern
typeclass. This worked to some extent, but after a lot of work, I became dispondent, and lost in the weeds.
So I dropped everything, and returned to the previous types to explore a simpler approach that I had in mind. This involved adding a single tactus
field to the Pattern, to indicate how you could clap along to a pattern:
type Time = Rational
data Arc = Arc {
start :: Time,
stop :: Time
}
data Event a = Event {
context :: Context,
whole :: Maybe Arc,
part :: Arc,
value :: a
}
data Pattern a = Pattern {
query :: Arc -> [Event a],
tactus :: Maybe Time,
pureValue :: Maybe a
}
This represented just enough about the structure of a pattern to start building a combinated library of ‘step’ based functions around it. I didn’t need to have a whole datastructure for representing beat-driven patterns, I just needed to know how to clap along to a pattern, and therefore how to compose patterns together to maintain the right tactus. This is going well, but not gelling perfectly – it only works for ‘stable’ patterns that have a fixed tactus (the additional pureValue
helps maintain stability). So now, I’m hoping that it’s possible to pattern the tactus:
-- 8< --- 8< ---- 8< ---- 8< --
data Pattern a = Pattern {
query :: Arc -> [Event a],
tactus :: Maybe (Pattern Time),
pureValue :: Maybe a
}
It was touch-and-go, but I have a proof-of-concept…
Tactus stands for finding a pulse in a rhythm by clapping with the hands, which feels right in the context of Carnatic music, where the tala is clapped. In the world of drum machines and sequencers, a similar concept is known as a ‘step’, related to the feet. There is also the ‘beat’, referring to the drum, or the heart, often used in European classical music. What we end up calling it will probably have consequences in terms of how it’s used.
This is only a very small part of the story! There is a much wider story in e.g. implementations of monads (patterns have several monad implementations for different ways of aligning patterns), applicatives and so on. And far wider story in what the end-user live coders see – the combinator library of pattern transformations and the mini-notation for expressing rhythms (the latter also inspired by the Bol Processor, in particular its polymetric expressions). Sometimes I start to write this book…
It was nice to work with my friend Rosamaria Cisneros on a Pattern Club workshop last week, together with fellow live coders Ray Morrison and Lucy Cheesman. The workshop involved programming movement using Strudel (the javascript port of Tidal), but was lead by Rosa, who talked about and shared numbers and patterns brought from India to Spain by Roma to help form the ‘young’ (a few centuries old) artform of Flamenco. It was good to plug the gap a bit between ancient (thousands of years old) konnakol and the (barely born) live coding.
@yaxu waoh!!!! this is so cool! I loved reading this.
The part about live-implementing Tidal again from scratch is extremely cool to me, I'm going to take a look at that soon. Loved seeing how the ideas grew together and combined and so on. Yayyyy :)))
@yaxu excellent work, look forward to hearing the results. The workshop sounds amazing 🙂
@yaxu
This was a lovely read, albeit with a lot of the code formatting not making it through to masto. I'd be fascinated if you ended up getting back to vortex at some point, as someone who finds python much easier to think in.
@yaxu I would welcome a book on this! I think the context and history behind a line of code, a data abstraction or a refactor can be related with the current understanding of the domain and the evolution of practice.