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