Challenge: non-linear time transform

most (?) library functions that modify time, do this by applying a linear transformation.

Example: fast https://hackage-content.haskell.org/package/tidal-core-1.10.2/docs/src/Sound.Tidal.Pattern.html#_fast

... withResultTime (/ rate) $ withQueryTime (* rate) pat

Example: rev is piece-wise linear. implementation https://hackage-content.haskell.org/package/tidal-core-1.10.2/docs/src/Sound.Tidal.Pattern.html#rev looks a bit funny but the transformation really (?) is

r :: Time -> Time 
r t = let (p,f) = properFraction t in fromIntegral p + 1 - f

(the 1 - f is in mirrorArc, I think)

NOW: Can we build something interesting with a transformation of time that is not piece-wise linear?

Interesting question! I'll approach from a math theory angle:

I think that type Time = Rational limits usable functions to (piecewise) rational functions, which are a ratio of two polynomials with rational coefficients.

As shown by fast and rev the time transformation needs to be (piecewise) invertible to get coherent results (resultTime needs to match up with queryTime). This restricts things further: not only does the function need to be a rational function, but so does its inverse.

I think this rules out everything quadratic and above (square roots aren't always rational, wept Pythagoras), so one is left with only (piecewise) Moebius transformations: \t -> (a*t+b)/(c*t+d)(see Wikipedia for lots of properties including inverse).

Haps don't have a notion of speed of time changing inside them, only start+end, so maybe everything becomes piecewise linear eventually anyway?

module Moebius where

import Sound.Tidal.Core
import Sound.Tidal.Time
import Sound.Tidal.Pattern
import Sound.Tidal.Control
import Sound.Tidal.UI
import Sound.Tidal.Params
import Sound.Tidal.Show

type Bi a = (a -> a, a -> a)

timeFunc :: Bi Time -> Pattern a -> Pattern a
timeFunc (f1, f) = withResultTime f1 . withQueryTime f

moebius :: Rational -> Rational -> Rational -> Rational -> Bi Time
moebius a b c d
  = ( \t -> (d * t - b) / (-c * t + a)
    , \t -> (a * t + b) / ( c * t + d)
    )

apply :: Pattern (Pattern a -> Pattern a) -> Pattern a -> Pattern a
apply fs p = fs >>= \f -> f p

piecewise :: Pattern a -> [Pattern a] -> Pattern a -> Pattern a
piecewise before during after =
  when (< 0) (const before) $
  go 1 during
  where
    go n (p:ps) = when (< n) (const p) $ go (n + 1) ps
    go _ [ ] = after

doubleEspresso :: Pattern a -> Pattern a
doubleEspresso = apply . fmap timeFunc $ piecewise
  ( pure $ moebius 1 0 0 1 )
  [ pure $ moebius 2 0 (-1) 2
  , pure $ moebius 6 (-4) 1 0
  ]
  ( pure $ moebius 1 2 0 1 )

example :: Pattern Char
example = doubleEspresso "<[a b c d] [e f g h] [i j k l] [m n o p]>"

this looks like it does something, haven't listened yet...

I was thinking about a similar thing when making a sketch in strudel. I hope that I am not going too far on a tangent by not being versed in tidal itself (I'll try to translate from strudel to tidal where adequate). Please bear with me.

The question I had was: how to produce a struct which follows the sound of a bouncing ball. So the kind of transformations/functions I am interested are mostly "specific structures" like euclid 3 8 (I am not sure if something like euclid even is withing the scope of the original question)

Not a bouncing ball, but similar is the mini-noation "[x [x [x [x [x [x -]]]]]]" which is the equivalent of a (terminated) geometric series in duration: 0.5 0.25 0.125 0.625 ... the exponent is whole, the duration is halved after every step. the duration of the single step is 1/2^i, i.e. 2^(-i)

The times for a bouncing ball are described by a terminated geometric series with a non-whole exponent. there's some bouncyness/elasticity involved, but it boils down to a duration which is the square root of the elasticity (say 1/2) and thus 1/sqrt(2^î) or 2^(-i/2). (the 2 is arbitrary the 1/2 in the exponent is particular to an actual, physical bounce).

To get back to claude's piecewise functions: Indeed, if time is rational, we can only approximate this kind of pattern by rounding the bounce times with some given precision to a rational number. Being a struct, it's easily inverted (take only the first hap).

I'm not sure if this kind of behaviour (rounding algebraic numbers to rational ones) already fits into the Möbius transform setting (but maybe I am missing the point by wanting a structure as a result).

Moebius: nice. I needed to resetCycles in order to hear the non-trivial part of the transform.

("haven't listened yet" - that's a version of Donald Knuth's "beware I have only proved this code correct, but have not actually run it")

Transforming with the function r t = let (p,f) = properFraction t in fromIntegral p + 1 - f does not work since it has a dis-continuity, and is not self-inverse, at each integer, and this confuses tidal (sc shows "late event" messagaes and does skip several events). It seems that with(Result/Query)Time have an implicit (un-documented) requirement that functions should be monotonic? In order to keep the (also implicit) requirement that for each pattern, queryArc p i must return events inside the interval i. The implementation does not care: withResultArc f pat = pat { query = map (\(Event c w p e) -> Event c (f <$> w) (f p) e) . query pat } . That seems to be the reason for the complexity of the actual rev implementation.

I found that tidal/sc is getting nicely confused by the following (where the transforms are not inverse to each other)

d1  $ withinArc (Arc (1/4) (3/4))
   (withResultTime (/  2) . withQueryTime (/ 2))
  $ s "superpiano"
     + note (scale "major" $ run 16 - 12)

we can build a non-linear time transform (that is still monotonic, rational to rational, and invertible) by repeated application of piece-wise linear transforms (mathematically, we want infinite repeation, but with the naive implementation, we have to stop somewhere)

imagine this process: play a pattern from 0 to 1/2 (outer time) with speed 2/3, so the pattern gets to 1/3 in its local time. Then from 1/2 to 1 outer time, play the rest of the pattern (1/3 to 1 local time) with speed 3/4. Then - apply this (recursively) on the two parts. The following code does this (?), but tidal introduces extra events (?), and even sometimes misses note-offs (?)

let f g p = fastcat [ g $ zoom (0, 1/3) p, g $ zoom (1/3, 1) p ]
in d1 $ (\ p -> stack [  outside 8 (iterate f id !! 8) p , p - note 12 ]) -- could use jux?
      $  s "superpiano"
       + note (scale "major" $ fast 2 $ slow 8 ( run 8) + fastcat [-7,0])

[EDIT] this transformation (in the limit) is https://mathworld.wolfram.com/deRhamsFunction.html first described by Salem 1943 https://www.ams.org/journals/tran/1943-053-03/S0002-9947-1943-0007929-6/S0002-9947-1943-0007929-6.pdf . It does map rationals to rationals. It is clear by construction that each iteration does, but the limit also has this property. So the remaining challenge is - to implement this limit function exactly.

1 Like

I think for practical purposes Rational is not as restrictive as it might seem at first. Here's my approach for a bouncing-ball effect, and I think you can substitute in more or less any monotonic rational injection of the unit interval to itself (which includes a lot more than just mobius transforms, especially since we don't care much about precision beyond audio-rate):

transTime :: (Time -> Time) -> Pattern a -> Pattern a
transTime f = splitQueries . (withResultArc $ mapCycle f)

-- a polynomial that sends 0 to 0 and 1 to 1
poly :: Rational -> Rational
poly x = 1 - (1-x)^2

d1 $ transTime poly $ n "1*16" # s "cpu"

More general functions seem fine too in principle, though you'd need to account for any discontinuities that split an arc and such.

My solution was buggy (fix below, I hope). It works for the simple example pattern I used but will break if you transform it or combine it with other patterns.

The problem is that while it produced a pattern whose output events vary quadratically with time, it didn't adjust which interval you query to get them. For example:

p = transTime poly $ n "1 .. 16" # s "bd"
queryArc p $ Arc (3/4) 1

[
	(15/16>247/256)|n: 13.0n (cs6), s: "bd",
	(247/256>63/64)|n: 14.0n (d6), s: "bd",
	(63/64>255/256)|n: 15.0n (ds6), s: "bd",
	(255/256>1)|n: 16.0n (e6), s: "bd"]

Only four events show up from 3/4 to 1, because only that many appeared in the linear pattern, even though in fact there should be 8:

queryArc p $ Arc (1/2) (3/4)

[
	(¾>207/256)|n: 9.0n (a5), s: "bd",
	(207/256>55/64)|n: 10.0n (as5), s: "bd",
	(55/64>231/256)|n: 11.0n (b5), s: "bd",
	(231/256>15/16)|n: 12.0n (c6), s: "bd"]

All of these are also 3/4 or above, and should be returned when we query that interval. It's possible for this to work anyway, but only by coincidence.

Fixing this is possible but no longer a one-liner :slight_smile: Because the time transformation maps each time cycle back to itself, we're guaranteed to get all the events that matter if we query the entire cycle. In that case we'll also get events that aren't within the query interval, so we also need to clip/filter to the right range. Here's what I ended up with:

import Data.Maybe(mapMaybe)

clipTo :: Arc -> [Event a] -> [Event a]
clipTo a = mapMaybe $ \evt -> fmap (\p -> evt {part = p}) (subArc a $ part evt)

queryFullCycles :: Pattern a -> Pattern a
queryFullCycles p = p {query = query'}
  where
    query' st = clipTo (arc st)
      $ concat
      $ map (\a -> query p st {arc = a})
      $ cycleArcsInArc (arc st)

transTime :: (Time -> Time) -> Pattern a -> Pattern a
transTime f = queryFullCycles . (withResultArc $ mapCycle f)

And with these definitions, we can see that querying the last 1/4 of the range now gives 1/2 of the events as it should:

queryArc p $ Arc (3/4) 1

t> [(¾>207/256)|n: 9.0n (a5), s: "bd",
	(207/256>55/64)|n: 10.0n (as5), s: "bd",
	(55/64>231/256)|n: 11.0n (b5), s: "bd",
	(231/256>15/16)|n: 12.0n (c6), s: "bd",
	(15/16>247/256)|n: 13.0n (cs6), s: "bd",
	(247/256>63/64)|n: 14.0n (d6), s: "bd",
	(63/64>255/256)|n: 15.0n (ds6), s: "bd",
	(255/256>1)|n: 16.0n (e6), s: "bd"]

interesting!

I was surprised that your program uses the mapping from Time to Time only in one direction. Does the second version really fix this? If yes, then it's some theorem that you could write a paper about...

but only after making explicit the the unwritten (?) assumptions (invariants) about the pattern type, like "for each p :: Pattern a, each s :: State, each e :: Event, if e is-elem-of query p s, then part e :: Arc is a sub-interval of arc s. Or something.

Such properties could be stated and checked in Sound.Tidal.PatternTest? In practice, they are needed by the back-end (the scheduler will get confused if it gets events with out-of-order arcs? Could be used for artistic effect.)

I don't think it's much of a theorem -- a real (hah) inverse can never be irrational because all of the time values were produced by passing in rational inputs, so its image would never have appeared in the function's output in the first place. Since querying the entire range [0, 1) gives every possible event arc over the cycle, we can just throw away any that are outside our actual query range.

(Though, there would still be options even without a rationality guarantee. If y = t0^2 and i want to compare t0 to t1, I might not be able to represent sqrt y as a rational but I don't need to, because (over a nonnegative range) sqrt y < t1 has the same value as y < t1^2, which I can compute fine.)

I don't feel like the solution is especially elegant, but the way Tidal represents patterns doesn't seem to have this sort of thing in mind ^,^