TSA

The Strange Agency

Building a Granular Synth in Swift, Part 6: Jitter

Way back in Part 4 we smeared grains in time to make the synth sound less like it was looping over a section of audio and more like it was playing a cloud of sounds.

delay = UInt32.random(in: 0..<Self.delay)

Let’s add some of this shaky jitter to grain position and size as well. We can quickly whip up a ControlTwinSliderView that will give us two sliders, one for the primary value and one more for how much we want it to vary. We can add some jitter parameters to our GrainControl object and adjust the code that copies global grain parameters to each grain at the outset of its loop: add jitter controls · thestrangeagency/GrainSwift@1455779 · GitHub.

One small gotcha is that we cannot have an upper bound of zero in the random() range, so we have to uglify our code a bit.

delay = Self.delay + (Self.delayJitter != 0 ? UInt32.random(in: 0..<Self.delayJitter) : 0)

Apparently because the ..< operator gives us a half-open range, we would have a logical contradiction if it included its upper bound because that is also its lower bound. Upon revisiting this code, I think it’s much cleaner to switch to a closed range, where 0…0 is perfectly fine: use closed range for jitter randomization · thestrangeagency/GrainSwift@215327a · GitHub. For future reference, there is a helpful discussion of the range types on Stack Overflow.

Now things get quite smeary as we crank up the three kinds of jitter. It would be nice to have some visual feedback of this smearing, so let’s make a ZStack of our WaveViews to show the variability jitter creates: make grain view jitter more jittery · thestrangeagency/GrainSwift@4792cb0 · GitHub.

We just need to do a linear interpolation between the possible extremes of the random range. The bottom of the range is our start and end variables as before, and the top is easily calculated as follows:

let startMax = start + Grain.indexJitter
let endMax = startMax + Grain.length + Grain.lengthJitter

One bit of wackiness is all the type casting between Double and UInt32, so the interpolation needed to be broken down into individual steps. Otherwise the compiler found the resultant types too ambiguous, no matter how many parentheses I sprinkled in.

ZStack {
    let steps = 10
    ForEach(0..<steps) { step in
        let perStep = 1.0 / Double(steps)

        // linterp unwrapped else compiler confused about types
        let progress = Double(step) * perStep
        let startProgress = Double(startMax - start) * progress
        let endProgress = Double(endMax - end) * progress
        let stepStart = start + UInt32(startProgress)
        let stepEnd = end + UInt32(endProgress)

        WaveView(buffer: buffer, start: stepStart, end: stepEnd).opacity(perStep)
    }
}

This works nicely and gives us great visual feedback. Adding an extension to Int or UInt32 to hide the interpolation will probably prove irresistible soon enough, but that’s not what we’re here for.