SOLVED: Scheduling multiple transitions (for the same voice at different times) at once

EDIT: If you'd like to skip to the solution it's here.

[Note that I'm using branch 1.8.2. Transitions went away in 1.9.]

It's common for musicians, especially drummers, to throw in a short change before a longer-lasting one. For instance, in the last bar before the chorus, the drummer is likely to play a fill, and then during the chorus another beat.

I want to do the same thing using Tidal. Here's some code that works, but doesn't do what I want:

do setcps 1
   d1 $ (1/16) <~ ( s "numbers" |*| n (slow 8 $ run 8) )
   d2 $ s "[gabba/8, bd, hh*4]"            -- verse

do jumpMod' 2 8 0 $ sound "[bd,<hc hh>*8]" -- chorus
   jumpMod' 2 8 7 $ sound "bd*4"           -- fill

Voice 1 here counts off the beats the whole time. (Those "numbers" samples are laggy so I've made them a little early.)

The idea is that voice 2 starts playing the verse, and then should briefly (for the 7th, that is the last, of 8 cycles) play a fill before entering the chorus. That's what I would expect if I evaluated the second do-block around beat 2, which is what I do.

Is this kind of scheduling multiple transitions at once something others already have a solution to? I'm trying to understand how transitions are coded, so I can write a version of wash that accepts multiple patterns and delays, but it's a mind-bender.

Returning to the code above, if I evaluate the jumpMod' instructions one at a time, the transitions always happen on the right beat mod 8, although they might take an extra 8 cycles -- for instance, I might ask at cycle 3 for a change to happen at cycle 7 mod 8, and rather than changing at cycle 7, it changes on cycle 15.

But what's weirder is the interaction. If I evaluate the second do-block around cycle 2 mod 8, it changes into the chorus immediately, and then 13 cycles later (when voice 1 says "seven") switches to the fill and stays there.

Playing with this a bit, it seems like jumpMod' always triggers after the next instance where cycle mod x is 0. So, triggering jumpMod' 3 8 on cycle 5 works fine, but triggering jumpMod' 5 8 on cycle 3 waits until the next cycle 0 mod 8 before counting up to cycle 5 mod 8. I don't know if anyone intended this behavior, but I agree that it's a bug as far as what I'd expect.

Now, in terms of how transitions work, they're not really "scheduled" exactly. Rather, when you call jumpMod' or whatever, it pulls the current pattern for that ID, and then creates a new pattern that combines the old pattern and the new pattern. As an example:

-- start with this
d1 $ s "bd"

-- and let's say you trigger the following at cycle 11
jumpMod 1 4 $ s "hh"

-- that's basically equivalent to calling
d1 $ stack [filterWhen (< 12) $ s "bd", filterWhen (>= 12) $ s "hh"]

Calling multiple transition functions should work, but each subsequent transition will take precedence over the previous one. Since you call your fill transition last, it makes sense that it would end with the fill looping. Calling the transitions in the other order (fill then chorus) would work, except for the weirdness around jumpMod' mentioned above.

The strangest thing to me is that the chorus starts playing immediately. Transitions take advantage of the fact that Tidal injects metadata about when the pattern was triggered (that's why in the example above it knows that the jump point is cycle 12 rather than any other mod 4). There must be some issue where the second transition is breaking that metadata for the first transition.

I may have found the reason for what you call the strangest thing. Here are three function definitions from Transition.hs, laid out a little differently for clarity:

-- | Sharp `jump` transition at next cycle boundary where cycle mod n == p
jumpMod' :: Int -> Int -> Time -> [ControlPattern] -> ControlPattern
jumpMod' n p now =
  jumpIn' (n - 1 - (floor now `mod` n) + p) now

jumpIn' :: Int -> Time -> [ControlPattern] -> ControlPattern
jumpIn' n now = wash id id
                (nextSam now - now + fromIntegral n)
                0 0 now

wash :: (Pattern a -> Pattern a)
     -> (Pattern a -> Pattern a)
     -> Time -> Time -> Time -> Time
     -> [Pattern a] -> Pattern a
wash _ _ _ _ _ _ [] = silence
wash _ _ _ _ _ _ (pat:[]) = pat
wash fout fin delay durin durout now (pat:pat':_) = stack
  [ filterWhen (< (now + delay))                                                     pat',
    filterWhen (between (now + delay) (now + delay + durin))                  $ fout pat',
    filterWhen (between (now + delay + durin) (now + delay + durin + durout)) $ fin  pat,
    filterWhen (>= (now + delay + durin + durout))                                   pat
  ]
  where between lo hi x = (x >= lo) &&
                          (x < hi)

The details of jumpMod' and jumpIn' are not important to me; I just need to know that jumpMod' calls jumpIn', which calls wash. That last clause defining wash is the interesting one. Its last argument is a list of patterns, of which it ignores all but the first two, pat and pat'. pat is, I believe, the most recently scheduled pattern, and pat' is the one that was most recently scheduled or playing before pat. wash assumes that pat' should be playing until pat starts.

What a user is likely to expect is that the third element of that list, call it pat'', should play until pat' was scheduled. But the start and end times associated with those patterns are not actually persistent values; every new transition that appends to the pattern list assumes that whatever was previously at the front of that list should now be playing.

I think that's what's happening, based on the argument names and the direction of the inequalities being passed to filterWhen.

I haven't figured out where the pattern history (pat:pat':_) comes from, and how it gets passed to transition (which underlies all the transition functions like xfade defined in BootTidal.hs). It seems like it must happen in the last clause defining handleActions in Tidal.Tempo, which begins handleActions st (Transition historyFlag f patId pat : otherActions) streamState =. It must be there because, aside from transition, that's the only other place in the code that I see a Transition object being manipulated. But I have not grasped the definition of handleActions.

I got it working! It still needs some work but if you're interested here's what I've got so far:

I'll have made it friendly before the weekend is over, I'm sure.

1 Like

Done, and pull request issued.