Semi-random thoughts and tales of tinkering
What we're building, what you'll learn, and how to read this tutorial.
By the end of this tutorial you will have a fully working real-time spectrum analyzer running on your iPhone (or in the iOS Simulator). The app captures live audio from the device's microphone, performs Fast Fourier Transform analysis on each incoming buffer of samples, and renders the frequency content as a bank of animated bars — all at 60 frames per second, all in about 400 lines of Swift.
The finished product has a dark-themed UI with green frequency bars that bounce in response to sound, peak-hold indicators that linger at each bar's maximum, and musical note labels beneath each band so you can see which notes are loudest. Clap your hands and watch the high-frequency bars spike. Hum a note and watch the corresponding bar light up.
We won't build this all at once. The tutorial starts with the simplest possible audio app — a VU meter that shows a single bar representing overall loudness — and we'll evolve it step by step into the full spectrum analyzer. Each section adds one concept and one visible change to the app. You'll always have something that compiles, runs, and does something interesting.
If you've ever built a WPF or WinForms app that reads audio through NAudio or CSCore, the overall architecture will feel familiar: capture audio on a background thread, crunch numbers, push results to the UI. The Apple APIs have different names, but the shape of the problem is identical.
This tutorial teaches two subjects in parallel: iOS app development and digital audio / DSP fundamentals. You don't need prior experience in either. The table below gives you the full map of topics — don't worry if half of these terms are unfamiliar right now. That's the point.
| iOS iOS Development | DSP Audio / DSP |
|---|---|
| SwiftUI — Apple's declarative UI framework. You describe what the screen should look like, and the framework figures out how to update it. | PCM audio — What a microphone actually gives you: a stream of floating-point numbers representing air pressure over time. |
| AVAudioEngine — Apple's real-time audio graph. We'll use it to open the mic and tap into the raw sample stream. | RMS & decibels — How to turn a buffer of samples into a single "loudness" number, and why we measure in dB instead of linear values. |
@Observable pattern — Swift's built-in mechanism for telling the UI "this data changed, please redraw." The iOS equivalent of INotifyPropertyChanged. |
FFT (Fast Fourier Transform) — The algorithm that decomposes a chunk of audio into its frequency components. The heart of any spectrum analyzer. |
| GeometryReader — A SwiftUI tool for building layouts that adapt to the available screen size. Essential for drawing bars that fill the width evenly. | Window functions — A preprocessing step that reduces spectral leakage and makes your FFT output cleaner. We'll use a Hann window. |
| App lifecycle — How iOS apps start, go to the background, come back, and handle permissions like microphone access. | Frequency-to-note mapping — Converting FFT bin frequencies (Hz) into musical note names (A4 = 440 Hz, etc.). |
| Log vs. linear scaling — Why human hearing is logarithmic and how that affects the way we display frequency bands. |
Each section focuses primarily on one track or the other, but they're designed to interleave: you'll learn a DSP concept in one section, then immediately apply it in the next section's iOS code. Theory always arrives before the code that uses it, so you're never typing something you don't understand.
You need surprisingly little to get started:
If you do want to test with a real iPhone, you'll need a Lightning or USB-C cable and you'll need to enable Developer Mode on the device (Settings → Privacy & Security → Developer Mode). The Simulator works perfectly for this tutorial, though — it can even simulate microphone input on macOS Sonoma and later.
If you're coming from C#, Swift will feel both familiar and slightly alien. The good news: the languages share a surprising amount of DNA. The table below maps the concepts you already know to their Swift equivalents. Bookmark this page — you'll want to glance back at it during the first few sections.
| C# | Swift | Notes |
|---|---|---|
class / struct |
class / struct |
Same keywords, but Swift structs are value types and are used far more heavily than in C#. Most SwiftUI views are structs. Classes are reference types, just like C#. |
int, float, string |
Int, Float, String |
Capitalized. Swift also has Double (64-bit, the default floating-point type) and Int is 64-bit on modern platforms. |
null / T? |
nil / T? |
Nearly identical syntax. Swift calls them Optionals. You unwrap with if let, guard let, or ?? (null coalescing, same as C#'s ??). |
async / await |
async / await |
Almost identical semantics. Swift uses structured concurrency with Task { } blocks instead of Task.Run(). |
List<T> |
[T] (Array) |
Shorthand syntax. [Int] is an array of integers. Supports .append(), .count, subscript access, and all the usual operations. |
Dictionary<K,V> |
[K: V] |
Also shorthand. [String: Int] is a dictionary mapping strings to ints. |
(x) => x + 1 |
{ x in x + 1 } |
Swift closures use { } braces and in to separate parameters from the body. Trailing closure syntax lets you write .map { $0 + 1 } using shorthand argument names. |
INotifyPropertyChanged |
@Observable |
One macro replaces all that boilerplate. Decorate a class with @Observable and SwiftUI automatically tracks which properties your view reads. |
LINQ .Where() / .Select() |
.filter() / .map() |
Same concepts, different names. Swift's functional collection methods are chainable just like LINQ: items.filter { $0 > 5 }.map { $0 * 2 } |
using |
import |
import AVFoundation is the equivalent of using System.Media; — it brings a framework's types into scope. |
namespace |
(none) | Swift doesn't have namespaces. Each module (framework/package) acts as an implicit namespace. Name collisions are resolved with ModuleName.TypeName. |
var / let (C# const) |
var / let |
Here's the big cultural shift: in Swift, let is the default. You use let for anything that won't change, and var only when mutation is needed. Xcode will warn you if you use var for something that never changes. |
Don't try to memorize this table. You'll internalize the mappings naturally as you write code. The biggest mental shift isn't syntax — it's that Swift leans heavily into value types (struct) and immutability (let) by default. Coming from C# where class is king, this feels backwards at first. Give it a few sections and it'll click.
Here's a tiny side-by-side to make the syntax feel less foreign. This function takes an array of floating-point audio samples and returns the peak value:
C#:
float FindPeak(float[] samples)
{
float peak = 0f;
foreach (var sample in samples)
{
var abs = MathF.Abs(sample);
if (abs > peak) peak = abs;
}
return peak;
}
Swift:
func findPeak(_ samples: [Float]) -> Float {
var peak: Float = 0
for sample in samples {
let abs = abs(sample)
if abs > peak { peak = abs }
}
return peak
}
The differences are cosmetic: func instead of a return-type-first declaration, -> for the return type, let/var instead of implicit mutability, no semicolons. The logic is character-for-character identical. And in practice, most Swift developers would write this as a one-liner:
let peak = samples.map { abs($0) }.max() ?? 0
That $0 is Swift's shorthand for "the first argument to this closure" (like a positional lambda parameter). The ?? 0 provides a default value in case the array is empty and .max() returns nil. You'll see patterns like this throughout the tutorial.
This tutorial uses a two-track system. Each section is tagged with one of three labels:
The tracks are sequenced so that theory always arrives one section before the code that implements it. For example, you'll learn what RMS and decibels are (amber DSP section) before we write the Swift code that computes them (purple integration section). This means you'll never be typing code you don't understand — when you write 20 * log10(rms), you'll already know why the formula looks like that.
If you're already comfortable with iOS development and just want the DSP material, you can skim the blue sections. If you've done signal processing before and just want to learn SwiftUI, skim the amber ones. But if both domains are new to you, read everything in order — the sections are designed to reinforce each other.
Every section follows a consistent structure:
You'll also encounter three types of callout boxes throughout:
Blue callouts explain iOS-specific details — platform conventions, Xcode behavior, or Apple framework quirks you should know about.
Amber callouts explain audio and signal processing concepts — the math, the intuition, and the "why" behind the formulas.
Purple callouts appear where iOS and DSP concepts intersect — they highlight how a platform feature enables (or constrains) a particular DSP technique.
A few pieces of advice before we start:
Type the code, don't copy-paste it. This sounds tedious, but it's how you build muscle memory for Swift syntax. Xcode's autocomplete will help you along the way, and you'll start to internalize patterns like guard let and trailing closures much faster if your fingers type them out.
Run the app after every change. One of the best things about iOS development is the feedback loop. You can hit Cmd+R in Xcode and see your changes in the Simulator within seconds. Use this aggressively. If something looks wrong, you'll catch it immediately instead of accumulating errors across multiple steps.
Read the compiler errors. Swift's compiler is strict but helpful. When it tells you "Value of type 'Float?' must be unwrapped," it's telling you exactly what to fix. Coming from C# where nullability warnings are suggestions, this takes some getting used to — in Swift, they're hard errors. Embrace that. It means fewer crashes at runtime.
Don't skip the DSP sections. If you've never done signal processing, the FFT section might look intimidating. It's not. We build up from first principles, and you don't need to understand the implementation of FFT to use it effectively. You just need to understand what goes in and what comes out. Think of it like using Dictionary<K,V> without knowing how hash tables work internally.
Let's build something.