Building a Granular Synth in Swift, Part 9: Pitch Control

Today’s pull request is almost self-explanatory. We have added a couple pitch controls, detuning all grains, and, more interestingly still, detuning each grain individually with our now standard jitter approach.

The addition is straightforward, but the results are even lovelier than I expected. Pitch slightly up and add a little jitter, and you can almost feel the bone-chill of outer space, à la Ligeti.

As everyone who has ever toyed with a record player knows, slowing down or speeding up the record will alter the music’s pitch. Similarly here we just need to speed up or slow down how quickly we play our grains. We took it for granted before, but notice the constant 1 in our offset calculations.

offset = (offset + 1) % (length + delay)

We track playback position in a given grain with this offset, and every time the audio system asks for a new sample, we advance this position to the grain’s next sample. But we can just as well advance the position more quickly or slowly. We could even move it in reverse! In order to advance it a little slower or faster, we can add a pitch variable, but we will now need to keep track of our offset with a Double, so we can advance the pitch by a fractional amount that won't disappear when rounding.

To this end, we can make a smoothOffset variable which is a Double that we cast to a UInt32 when we need to use it to actually index into the sound buffer. As before, we want to loop our position based on the grain size, but the modulo operator doesn’t work with Double values. Swift, however, kindly provides a more or less equivalent function with truncatingRemainder(dividingBy:).

smoothOffset = (smoothOffset + pitch).truncatingRemainder(dividingBy: Double(length + delay))

Beyond the pitch tweaks, we made the controls a little more cramped by adding a (spacing: -8) to our VStack and setting a custom font size. This is probably as far as we can reasonably go with our current interface!