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 ...

sorry it took me a while to get to this. here is my implementation of while:


let 
while' :: Pattern Bool -> (Pattern a -> Pattern a) -> Pattern a -> Pattern a
while' pb f px = Pattern $ \st -> case or $ map value $ query pb st of
                                            True -> query (f px) st
                                            False -> query px st

and sure enough it performs quite well:

martin@martin-XPS-13-9300:~$ ghci -XOverloadedStrings
Loaded package environment from /home/martin/.ghc/x86_64-linux-9.4.2/environments/default
GHCi, version 9.4.2: https://www.haskell.org/ghc/  :? for help
Loaded GHCi configuration from /home/martin/.ghci
ghci> import Sound.Tidal.Context
ghci> :set +s
ghci> :{
ghci| let
ghci| while' pb f px = Pattern $ \st -> case or $ map value $ query pb st of
ghci|                                             True -> query (f px) st
ghci|                                             False -> query px st
ghci| :}
(0.02 secs, 71,728 bytes)
ghci> flip queryArc  (Arc 0 1) $ iterate ( while "0 " id) (pure True) !! 10
[(0>1)|True]
(0.05 secs, 11,658,504 bytes)
ghci> flip queryArc  (Arc 0 1) $ iterate ( while "0 " id) (pure True) !! 20
[(0>1)|True]
(11.12 secs, 11,836,427,024 bytes)
ghci> flip queryArc  (Arc 0 1) $ iterate ( while' "0 " id) (pure True) !! 20
[(0>1)|True]
(0.01 secs, 184,496 bytes)

this should probably be the default implementation

We get problems though when the query isn't from the boolean event 'part':

drawLine $ while "[~ t] t" id "a b c d"
-- |.bcd|.bcd|.bcd|.bcd|.bcd|.bcd|.bcd|.bcd|.bcd|.bcd|.bcd|.bcd|.bcd|.bcd|.bcd
drawLine $ while' "[~ t] t" id "a b c d"
-- |abcd|abcd|abcd|abcd|abcd|abcd|abcd|abcd|abcd|abcd|abcd|abcd|abcd|abcd|abcd
drawLine $ while "[t,f] t" id "a b" 
-- |ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab
-- |a.|a.|a.|a.|a.|a.|a.|a.|a.|a.|a.|a.|a.|a.|a.|a.|a.|a.|a.|a.|a.|a.|a.|a.|a.|a.
drawLine $ while' "[t,f] t" id "a b" 
-- |ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab|ab

But I guess we can still just query once, then map over the part arcs afterwards?

It might be worth querying px not by the original query arc, but by the min/max of the begin/end parts of the pb events.

Howabout this?

while' pb f px = Pattern $ pf
  where pf st = concatMap match evs
          where evs = query pb st
                parts = map part evs
                subarc = Arc (minimum $ map start parts) (maximum $ map stop parts)
                with = query (f px) st {arc = subarc}
                without = query px st {arc = subarc}
                match ev | value ev = find with ev
                         | otherwise = find without ev
                find evs ev = catMaybes $ map (check ev) evs
                check bev xev = do a <- subArc (part bev) (part xev)
                                   return $ xev {part = a}

That seems to fix my tests in 0.01 secs :slight_smile:

1 Like

yes makes sense!

I think it would also be nice to rename it to when. This is a breaking change as when already exists for something with a boolean test on the cycle number, but I don't think it's often used.

hmm why not stick with while? with when i kindof associate some kind of event structure, like when this happens do that (struture comes from the booleans) but with while it is clear that the strucutre comes from the pattern itself

Hmm I always found it awkward to remember while, I guess because I associate it with imperative looping. It's when in strudel. What do others think?

I made the change without the rename for now, and to sew:

1 Like

I made a general issue:

hmm yeah i kindof get that, if you think it would be more intuitive for a bigger audience (and thus be used more, because it's actually incredibly useful) then i'm all for the rename. i could still make an alias if wanted to :slight_smile: