Building a Granular Synth in Swift, Part 4: Control

It would be great to add some more control to our grain source, and to make this work with SwiftUI. A class derived from ObservableObject gives us a nice way to package up controls and expose them to the UI. We can put it in its own file later, but for now it is convenient to keep all the grainy things in one file, like so: GrainSwift/GrainSource.swift at 6a5117fe15b3251708817c09393486fe1d79cf63 · thestrangeagency/GrainSwift · GitHub.

One thing to note is that apparently events do not bubble through nested observables, so we have to put in this bit of a kludge if we want to access our GrainControl via the Audio environment object: GrainSwift/Audio.swift at 6a5117fe15b3251708817c09393486fe1d79cf63 · thestrangeagency/GrainSwift · GitHub.

This gives us a clean interface on the UI side, allowing, for example, slider control of density with a single line:

Slider(value: $audio.grainControl.density, in: 0...1, step: 0.01)

An updated repo with slider control is here: GitHub - thestrangeagency/GrainSwift at 6a5117fe15b3251708817c09393486fe1d79cf63.

From there, we can quickly build out some more controls following a similar pattern: add controls for size and position · thestrangeagency/GrainSwift@3b764d4 · GitHub. Note that we are taking care with the boundary conditions for each variable, so as to avoid division by zero and other unpleasantness.

The new controls surface a couple related issues. One is zipper noise when changing parameters, as each grain is changed abruptly. Two is the fact that we are controlling all grains at once. A potential first step to addressing these is giving each grain more internal state, and having it update to match global parameters only when it begins to play or starts anew having completed a loop: GrainSwift/GrainSource.swift at 30b934e2ed2e91ac5c7a52e9045fd205c2aa968c · thestrangeagency/GrainSwift · GitHub.

Also, the control code isn’t very DRY, so maybe we can do a bit of refactoring there: refactor controls · thestrangeagency/GrainSwift@1f9fa22 · GitHub.

The grains are sounding a little bit loopy, because they all play nearly in synch (though each new grain is offset by a sample because of the way we increase count with regard to density). We can quickly introduce a short delay phase where each grain initially emits a random bit of silence before playing: smear grains in time · thestrangeagency/GrainSwift@e8c017d · GitHub.

We can make things more smeary still by smoothing out the edges of each grain with an attack and decay envelope. Probably the cheapest to calculate is a simple trapezoidal envelope, with mirror attack and decay phases: scale grains with trapezoidal envelope · thestrangeagency/GrainSwift@0eb59b7 · GitHub.

Actually we can make the ramp a little better with the proverbial one more tweak: make ramp size proportional to grain size · thestrangeagency/GrainSwift@86e2c07 · GitHub. This way our ramp size will never exceed grain size, giving us a triangular envelope at the maximum extreme and a rectangular one at the minimum.

We are now juggling four parameters and starting to surpass the bounds of intuition. Maybe it’s now time for some visualization: add wave visualization by lucaskuzma · Pull Request #1 · thestrangeagency/GrainSwift · GitHub.

Finally, let’s add a handy version bump and tag script and tag this build for release: Release v0.0.12 · thestrangeagency/GrainSwift · GitHub.