Thoughts about better support of sequences and beat-wise (rather than cycle-wise) operations in TidalCycles

Tidal is currently cycle-based, apart from in some parts of the mini-notation

So if you append a-b-c- to "d-e-" you get a-b-c-d--e--
i.e., the relative cycle duration is preserved, not the beat duration

A simple append operation that gives
a-b-c-d-e-

isn't really possible because a pattern is a function of time, rather than a sequence of beats, so you can't count the beats ahead of time.

However mininotation does have some beat-wise functionality, like
"{a b c, d e}"

gives this over two cycles:

a-b-c-a-b-c
d-e-d-e-d-e

whereas the cycle-wise
"[a b c, d e]"
gives

a-b-c-
d--e--

The {} beat-wise approach is possible because the mininotation parser can count the elements in each subpattern and use that information when combining the subpatterns. But an equivalent haskell-level function Pattern a -> Pattern a -> Pattern a to combine patterns beat-wise isn't really possible.

But what if we changed the representation of patterns so that mini-notation-like beat-wise compositioln was supported in tidal itself?

Potential approaches:

  1. Adding a field to the Pattern datatype constructor to store the number of steps in the cycle, and keeping it updated as far as possible through pattern transformations

e.g.

data Pattern a = Pattern {query :: Span -> [Event a],
                          beats :: Maybe Rational
                         }

The beats could then be used to combine patterns beat-wise.

What to call this? Tactus? Tatum? Tempo? Beats?

If we cat a four-beat a-b- with a three-beat c-- to make the pattern [a-b-c--], how many beats are in that? 7 I guess?

  1. Adding a field to the Pattern datatype constructor to store the 'period' of the cycle, and keeping it updated as far as possible through pattern transformations

Similar and related to the above, but for representing the relative duration of a cycle in the pattern, rather than assuming everything has the same cycle duration. I suppose '1.' would be for finding the tactus (clapping rate) or beat-rate of the rhythm in order to align the beats of two patterns accordingly, and this would be for finding the wider cycle period for the pattern in order to concatenate them or align cycles.

Well there are probably different concepts that are being conflated here..

  1. Adding a separate datatype for representing rhythm as a sequence.

Something like:

data Rhythm a = Atom a
  | Silence
  | Subsequence {rSteps   :: [Step a]  }
  | StackCycles {rRhythms :: [Rhythm a]}
  | StackSteps {rPerCycle :: Rational,
                rRhythms  :: [Rhythm a]
               }
  | Patterning {rFunction :: Pattern a -> Pattern a,
                rRhythm :: Rhythm a
               }

This is a different approach to 1. and 2., involving representing sequences as a tree structure of lists that can be converted to a pattern. That would allow different kinds of transformations. Perhaps it would allow stateful lazy lists as well, like l-systems, but it's unclear how that could be efficiently converted to a stateless pattern..

7 Likes

I don't really understand Tidal's innards well enough to have good opinions on options 1, 2, and 3. But I do really like the objective of this whole idea because I would probably use it all the time :smiley:

I'll try and grok the options you presented @yaxu and see if I can think of any reasons to avoid one of them.

I'd like there have these 2 types of append ops. "1 2 3 4" + " 5 6" = "1 2 3 4 5 6" in one cycle and "1 2 3 4" "5 6 1 2" "3 4 5 6" over 3 cycles.

There's also this standard techno trick where a pulse "1 1 ~ 1 ~ 1" when applied to a progression of "a b c d e f g" would output "a b ~ c ~ d" "e f ~ g ~ a".

1 Like

just read the wiki on "tatum", love the historical reference. maybe your idea could have something to do with composer raymond scott, who seems to have also invented the first analog sequencer?

I find my thoughts shifting on this in an interesting way during the last few days. At first, I thought a new dedicated type was surely the way to go, but more and more I’ve found myself wondering how much mileage could be had, and how much other existing Pattern code could be leveraged, through minimal extension of Pattern, likely a combination of both #1 and #2 from yaxu’s original message:

data Pattern a = Pattern {query :: Span -> [Event a],
                          beats :: Maybe Rational,
                          cycles :: Maybe Rational 
                         }

(Or perhaps with beats & cycles behind a single field holding a Maybe of some tuple/record-like type.)

With even just those two additional fields in place, it isn’t too hard to picture it as a metadata addition, populated/propagated on a best-efforts basis, that can be pretty easily a) converted into a single-sequence-worth-of-events in a list and b) generated going in the opposite direction, not unlike the current “from list” functions.

I do find myself wondering, though, whether or not it might need to look more like this, where beats and cycles are also themselves queries of a sort:

data Pattern a = Pattern {query :: Span -> [Event a],
                          beats :: Span -> Maybe Rational,
                          cycles :: Span -> Maybe Rational 
                         }

(Or perhaps with beats & cycles behind a single field holding a Span -> Maybe of some tuple/record-like type.)

I haven’t really thought too much about that, though, and I’m far from an expert on Tidal as of yet

I'm a bit confused about what exactly beats or cycles mean.. i guess a couple of examples would be great at this point to really get on the same level here. I'll start with a sufficiently complex one

consider the following patterns: p1 = "1 2 3", p2 = "[[1 2 3 4] [1 2 3]]/2".
so the first pattern spans over one cycle and has 3 beats per cycle, the second pattern spans over two cycles, but what number of beats? i am guessing 12 beats, since 12 is the least common multiple of 4 and 3, the number of beats of the first subpattern and the second subpattern respectively?

now let's put p1 and p2 in sequence and let p = seq p1 p2, it is clear that p will have 15 beats (based on my assumption above) and span over cycles like this:

| 1 2 3 1 . . 2 . . 3 . . 4 . . | 1 . . . 2 . . . 3 . . . 1 2 3 | 1 . . 2 . . 3 . . 4 . . 1 . . | . 2 . . . 3 . . . 1 2 3 1 . . |
| 2 . . 3 . . 4 . . 1 . . . 2 . | . . 3 . . . 1 2 3 1 . . 2 . . | 3 . . 4 . . 1 . . . 2 . . . 3 | . . . 1 2 3 1 . . 2 . . 3 . . |
| 4 . . 1 . . . 2 . . . 3 . . . |

so 9 cycles.. which is the least common multiple of 15 and 27 (the new beats per cycle and the total beats 27 = 3 + 2*12) divided by 15 ( i guess? )

so under this interpretation i don't really understand why the beats/period should be Rational. my first thought would be something like this:

data Pattern a = Pattern {query :: Span -> [Event a],
                          beats :: Maybe Int,
                          period :: Maybe Int
                         }

i don't think there is a need to make the numbers relative to a span, as you can see with p2 where we calculated the number of beats by the number of beats of the subpatterns

1 Like

After posting my comment I too found myself thinking about beats being Int (or similar) rather than Rational, but figured I’d leave the post roughly as it was.

The cycles/period aspect seem like they should be Rational, though, to handle the most scenarios (as when doing polymeter/polyrhythm work the natural length of the “sequence” would not be an integral number of cycles.)

[Addendum: And, if Tidal needed to pursue an angle where beats/cycles were queried given a Span then I imagine both would need to be Rational anyway. Hard to say for sure, other than that I keep going back and forth a bit on whether or not extending Pattern is the way to go...

Upside: large benefits from direct integration with the many functions taking/returning Pattern.
Downside: having to update all those functions. :wink: ]

1 Like

Given the very flexible nature of patterns I'm not sure how to build beats into the type. A new type might be easier, but we can already re-use pattern notation for a lot of things:

ps2pat n ps = fast n (cat ps)

ps2pat 8 ([s "bd" # shape 0.5, s "~", s "sn"] ++ [s "cp", s "cp"])

This isn't the nicest thing to use, and there are probably some additional functions to think of to make it easier to combine lists... but are there specific examples of what people are trying to do?

well patterns have a limitation because they are purely functions of time, there is no way (for tidal) to infer the period of a pattern (the number of cycles after a pattern repeats) or the beats, or measure within a pattern. we would need to know both of these things for combining patterns sequentially rather than periodically.

for example currently in tidal if you reverse a pattern that has a period of two cycles, it will reverse each cycle seperately, e.g.

rev "<[1 2] [3 4]>" = "<[2 1] [4 3]>"

but with the additional information you could have a function revv so that

revv "<[1 2] [3 4]>" = "<[4 3] [2 1]>"

so in total it would just be a new way of being able to combine patterns quickly (and intuitively)

1 Like

2 posts were merged into an existing topic: Isorhythms in tidal

I was leaning towards adding metadata to the existing pattern type as well, it would work up to a point. After playing around with it a little though, as others argue above I found it to all too ambiguous to define, and would only allow surface-level transformations.. So I think we need a new type for sequences.

3 Likes

but then i think we have to handle another problem: how do we integrate this type with Patterns?

i guess we would define a new set of functions that combine and manipulate this new type and then have a function that converts it to Pattern?

I think this would result in making it quite hard to type fast and use it in a meaningful way (having to use lots of brackets in the right places, not being able to apply functions after conversion to Pattern etc.)

Maybe we could do something with typeclasses, but i think this would result in having to specify what type one wants to use (via ::)

I guess the main problem here is clashing namespaces, where it would be annoying to have different works for reversing sequences and patterns. With my experiments so far a type class is indeed working pretty well for this so far. I just develop patterns and sequences in separate modules, then combine with an additional module called Common.hs


import Sound.Tidal.Pattern as Pat
import Sound.Tidal.Core as Pat
import Sound.Tidal.UI as Pat
import Sound.Tidal.Sequence as Seq

class Transformable a where
  rev :: a -> a
  cat :: [a] -> a

instance Transformable (Pattern a) where
  rev = Pat.rev
  cat = Pat.cat

instance Transformable (Sequence a) where
  rev = Seq.rev
  cat = Seq.cat

Then everything works from type inference. You have to have a function to convert from sequences to patterns but I think that's fine. We can also make sequences of patterns I guess. I'll share my workings soon.

1 Like

I think the class should expect a type of kind * -> * instead of *, it doesn't matter so much for functions like rev or cat that don't really depend on the 'inner type' but if we look at something like ply :: Pattern Int -> Pattern a -> Pattern a, it wouldn't work with the current definition, since we couldn't type this as a -> a -> a. So it would look something like this:

class Transformable f where
  rev :: f a -> f a
  cat :: [f a] -> f a
  ply :: f Int -> f a -> f a

instance Transformable Pattern where
  rev = Pat.rev
  cat = Pat.cat
  ply = Pat.ply

instance Transformable Sequence where
  rev = Seq.rev
  cat = Seq.cat
  ply = Seq.ply

1 Like

Some work-in-progress:

I've been thinking about alternative ways of beatwise alignment as I go, there's some interesting discussion over on the algorithmic pattern forum.