Generalizing swing and rotating uneven rhythms, by mapping integers from a latent space to time

Consider a classic (3,8) Euclidean rhythm with a kick and two snares:

"bd ~ ~ sn ~ ~ sn ~"

Wouldn't it be nice to be able to rotate those events through the 3 beats of interest? Something like this:

rotate 1 "bd ~ ~ sn ~ ~ sn ~"
       = "sn ~ ~ bd ~ ~ sn ~"
rotate 2 "bd ~ ~ sn ~ ~ sn ~"
       = "sn ~ ~ sn ~ ~ bd ~"
rotate 3 = id -- (when applied to a rhythm with three events)

A natural way to do that would be to represent those three moments (that is, 0/8, 3/8 and 6/8) with the first three integers (i.e. 0,1 and 2). By doing that, uneven rotation through the target space (ordinary time) becomes simple addition in the latent indexing space. Index values above two would map to the same times but in later cycles.

As soon as I imagine this being possible, I want to go further, because what if I wanted to index a moment between two of the beats in the grid? For that I would need a second, finer grid to fill in the time between moments in the initial coarse grid. Given those two nested grids, I would need to use a pair of integers to identify moments -- one for the big grid, and one for the points in the small grid between two points in the big grid. I might even want to introduce more than two grids. (Sidenote: Rather than tuples, lists seem like a more convenient way to represent time -- for instance, if there were three nested grids, you could use [1] to represent (1,0,0), [1,2] to represent (1,2,0), etc.)

This idea has bugged me for years, because the music I hear sounds like it was conceptualized this way. Swing is the most common example. You could represent notes in a "swing manifold" by mapping pairs of integers to time, where the second integer can only be 0 or 1. If the second integer is 0, then the pair represents one of the quarter notes, and if the second integer is 1 then it represents a moment roughly two-thirds of a quarter note after one of the quarter notes. Of course real swing is more complex, but it has that flavor.

More generally, in rhythmically interesting music, not all moments are created equal, and it sounds good if every voice cooperates to make certain moments special. Think of Smooth Criminal. Throughout most of that song, every fourth measure starts an eighth-note early. You could program each voice to do that -- but it might be easier to program them all as if they were playing in "straight time", and just warp time underneath them so that what they "think of" as beat 0 is actually beat (-1/8).

An indexing scheme like this would resemble musical scales, but for time rather than frequency. Scales designate a certain set of frequencies as more important than the rest, and make it easy to refer to them by indexing (integer-lookup into) the scale. This kind of nested-dimensional indexing for time might similarly be (at least in some cases) worth it.

I'm hopeful about it in the context of Tidal, because I find it hard to coordinate voices in Tidal. I of course very much enjoy the unpredictable results of a few voices modified by various combinations of fast, slow, (<~), (~>)andrev(to say nothing of compound operations likebrak`). But it can get pretty out of hand pretty quick. Imposing some sort of structure on time as a whole seems like a potentially promising way of reestablishing a little order.

Would you use something like this? What would you like to be able to express? Are there representations that seem more natural to you? Any thoughts would be greatly appreciated!

your speculative function rotate above already exists and it's name is rot :slight_smile:

i think you're idea of indexing a specified grid in time like a scale is actually very interesting and could result in very cool rhythms. i always thought of this the other way around somehow: making the vertical/chord structure more flexible (i.e. removing it's grid), which i think could have some interesting harmonic results.

i actually think it wouldn't be too hard to implement as a function in tidal (somewhat similar in usage to the scale function maybe)

1 Like

Wow. Thanks! The definition of rot is not something I'm understanding at a glance, to put it mildly, but its usage seems clear and helpful.

1 Like

i think it's basically looking for all whole events in the current cycle and then rotates their value (thus it is probably one of the most expensive functions in tidal)

It makes sense that rot would be expensive. It should be much cheaper to represent the uneven rhythm in the latent space and then translate from there to ordinary time, rather than to translate from ordinary time into a latent space and then back to ordinary time.

probably one of the most expensive functions in tidal

interesting. how did you measure? could you contribute a test case for a performance benchmark?

There are some non-obvious performance properties (penalties, maybe) but instances I know are with nested function calls, e.g., Fix + sometimesBy performance issues · Issue #564 · tidalcycles/Tidal · GitHub , and are (I think) un-fixable in the current model of pattern as function, without any form of manifestation/caching/memoisation. Still it'd be good to know where they might appear, exactly.

1 Like

i didn't measure, but it was definitely noticable when used in combination with lots of whiles or whenmods.

would be interesting to look into performance in more detail, but i've never really managed to do profiling in ghc

poor man's profiling, using +s in ghci ( 3. Using GHCi — Glasgow Haskell Compiler 9.8.1 User's Guide )

$ ghci -XOverloadedStrings
Loaded package environment from /home/waldmann/.ghc/x86_64-linux-9.6.3/environments/default
GHCi, version 9.6.3: https://www.haskell.org/ghc/  :? for help
ghci> import Sound.Tidal.Context 
ghci> :set +s
ghci> flip queryArc  (Arc 0 1) $ iterate ( while "0 " id) (pure True) !! 10
[(0>1)|True]
(0.06 secs, 10,384,328 bytes)
ghci> flip queryArc  (Arc 0 1) $ iterate ( while "0 " id) (pure True) !! 11
[(0>1)|True]
(0.03 secs, 20,666,192 bytes)
ghci> flip queryArc  (Arc 0 1) $ iterate ( while "0 " id) (pure True) !! 12
[(0>1)|True]
(0.05 secs, 41,236,736 bytes)

yes, that looks exponential (as a function of the nesting depth). Is that expected? (perhaps move to separate thread).

[EDIT]

yes:

while b f pat = sew b (f pat) pat  -- copies pat!
sew pb a b = overlay (mask pb a) (mask (inv pb) b)
overlay = (<>)
instance Semigroup (Pattern a) where
  (<>) !p !p' = Pattern $ \st -> query p st ++ query p' st

so this will call the original pat for 2^n times when nested n-fold.

1 Like

ah that's a cool trick! thanks!! :slight_smile:

would be great to make while more performant, it's probably not too hard to implement it directly instead of via sew

not too hard ...

I don't see how.

NB: since you mentioned it - nesting of whenmod seems fine (linear)

ghci> flip queryArc  (Arc 1000 1001) $ iterate ( whenmod 2 1 id) (pure True) !! 100
[(1000>1001)|True]
(0.01 secs, 717,472 bytes)

ghci> flip queryArc  (Arc 1000 1001) $ iterate ( whenmod 2 1 id) (pure True) !! 200
[(1000>1001)|True]
(0.01 secs, 1,353,472 bytes)

well, by itself. certainly different for whenmod _ _ f where f makes a copy ...