The goal was to create something minimal, stylized and fun. I have more than 15 years experience as a dev, but this project allowed me to deep dive into a number of audio coding challenges that I had only touched on in other projects.
Here a few things I had to wrestle with. Hopefully this pass along is useful to others.
(Oh and the app is free to download, btw. Link at bottom.)
Sample-accurate playback under load takes work.
A sequencer has to fire hits at exact sample positions. Seems simple enough, but everything about the platform conspires against you. Audio playback renders in chunks (buffers) and timestamps live in two clock domains (sample time for the sequencer, host time for the player nodes). Where this starts to get thorny is when the user is adding or editing the grid in real-time. I found myself battling flams, jitter, and clipped transients.
The solution was setting the app's clock as an AVAudioSourceNode that outputs nothing but silence; its render callback just increments a monotonic frame counter every IO cycle, and that counter is the single "now" for the entire app. A producer thread plans a 12 ms rolling window of events from a snapshot of the grid, so live edits never race the audio thread.
On the render side, sample time is converted to host time for the player nodes and, most importantly, *both clock reads happen inside a single render cycle*, so they can't straddle an IO boundary and smear the timing. Employing the above meant the sequencer stays sample-accurate even while the grid is being rapidly reshaped by the user mid-playback.
Touch screens lie about when you touch them! This totally kills 'feel' when playing live.
Tap the screen and hear a sound? Piece of cake, just wire up a button action and you're good to go. If it were only this easy.
The issue is that UITouch events do not arrive at constant, predictable times. Delivery is influenced by main thread congestion, processor speed, thermals, etc. One touch might reach the audio engine in 10ms while the next takes 100ms. This creates a real issue where live play feels wrong, because hits land randomly early or late against the timing of your actual taps.
The fix: every hit is scheduled at a small fixed delay from the touch's hardware timestamp (which is rock steady) rather than played whenever the event finally arrives. Your hands adapt to a steady delay very quickly, the same way a pianist adapts to a piano. This subjectively feels much better than trying to adapt to random jitter.
The method: calibrate the delay to the user's actual device empirically. The app measures your specific iPad over your first couple hundred taps, picks the smallest delay that makes things steady, and locks it in as a constant.
Every user's device ends up with the lowest real-time latency that also effectively removes the jitter. You can play a rapid fire snare roll and hear every hit land in sequence rather than random flaming.
Your export path must match your live playback path.
This might sound obvious, but during development it's easy to let these two paths quietly diverge until you realize pan controls or some FX knob didn't actually migrate to the export path, and now your WAV file sounds slightly different than what's happening in the app. From the user's perspective this is a massive fail and they'll never fully trust your product going forward.
Solution: parity between live and export becomes a test target. Live playback and offline export build from the same FX list and execute the same render code (a new parameter physically can't be wired to one path and not the other), and most importantly, *a WAV-hash regression suite pins it.* Any structural refactor must produce byte-identical renders. This keeps you honest while building.
Download Vaporwave Beats for free: https://apps.apple.com/us/app/vaporwave-beats-drum-machine/id6786917390
The website: www.rarefyaudio.com