mathr / blog / #

Arrow transformers for sample rate conversion

In my previous post you may have noticed this:

phasor rate phase0 = ...

where 'rate' is the sample rate. It's rather annoying having to pass the sample rate to every ugen that might need it, increasingly so if we imagine that there might be more things in the environment that various ugens might need. Control.Arrow.Transformer.Reader provides such an environment.

Say we want to be able to change the sampling rate of a subgraph of our DSP algorithm, either upsampling to allow higher precision or downsampling to reduce the amount of calculation required. We can define a class or resamplable graphs like this: *

> class ArrowCircuit a => ReSample a where
>   expand   :: Num b => Int -> a b b   -- zero interleaving
>   decimate :: Int -> a b b            -- decimation
>   zoh      :: Int -> a b b            -- low quality reconstruction filter (zero order hold)
>   --       :: Num b => Int -> a b b   -- high quality reconstruction filter(s)

But this doesn't help much, because we'd need some way of keeping track of the sample rate changes. The solution is to store the sample rate in an environment, which our upSample and downSample graph manipulators can use:

> data AudioState = AudioState

> runSample sr graph = proc p -> (|runReader (graph -< p)|) sr

> upSample n graph = proc p -> do
>   sr <- readState -< AudioState
>   (|runReader (decimate n <<< graph <<< expand n -< p)|) (sr * fromIntegral n)

> downSample n graph = proc p -> do
>   sr <- readState -< AudioState
>   (|runReader (expand n <<< graph <<< decimate n -< p)|) (sr / fromIntegral n)

Then our ugens that need the current sample rate can get it:

> phasor phase0 = proc hz -> do
>   sr <- readState -< AudioState
>   rec accum <- delay (wrap phase0) -< wrap (accum + hz / sr)
>   returnA -< accum

while ugens that don't need the sample rate don't need to worry about it at all, even if they use ugens that do need the sample rate:

> osc phase0 = proc hz -> do
>   phase <- phasor phase0 -< hz
>   returnA -< cos (2 * pi * phase)

* Note: the above ReSample class is somewhat ill, as the (Num b) constraint in expand means that only graphs with a single numeric input and output can be resampled. I'm not sure how to fix this yet, perhaps something like (expand :: Int -> a b (Maybe b)) would be useful, as that would make explicit the padding, and force resampled graphs to use a suitable reconstruction filter.