Stasis Tuner

Most guitar tuners are a needle and a guess. You pluck, the needle swings, and you wait for it to settle somewhere near the middle.

Strobe tuners have always been the exception. Instead of a needle, a pattern spins: the further off you are, the faster it moves, and when you’re in tune, it stands perfectly still. They’re the most precise tuners there are, and also the most intimidating.

I built Stasis, a strobe tuner for iPhone that anyone can read at a glance. In tune simply means nothing moves.

Harder than it sounds.

Measuring a pitch is the easy part. Knowing which note someone is actually playing is not. A guitar string doesn’t make one clean tone, it makes a whole family of them, and the phone hears everything else in the room along with it: the body of the guitar, a fridge, a fan, the next string still ringing.

For a while, Stasis would flash an A when I played a high E, cling to a note long after it had faded or follow the hum of my apartment instead of my guitar. I rebuilt the way it listens more times than I’d like to admit, and the version I kept is simpler than most of the ones I threw away.

Built around stillness.

Stasis does one thing. It tells you exactly how far a string is from its note.

Everything on screen follows a single idea: motion means out of tune, stillness means in tune.

The screen itself is the interface. Tune to start, hold for menu.

A gauge you can read.

At the center sits a wheel of note names with a ruler, read against a fixed pointer, like the needle of a classic tuner bent into a circle. Every line is five cents.

The wheel follows the string exactly, wobble and all. The rings around it show where the note settles.

Still means in tune.

Two rings spin around the edge of the screen. The outer ring follows the note, and the inner ring turns twice as fast, like the second band on a strobe disc, so it catches the smallest error. Sharp spins one way, flat the other. As you get close they slow down, and when you’re there, they stop.

The rings have weight, too. They speed up and coast like a fan instead of snapping to every change, so the strobe feels like a physical thing rather than a readout.

Turn the phone sideways and the letters turn with you. The rings don’t need to.

Your tuner, your type.

Hold anywhere for the menu. Set the reference pitch and how close counts as in tune, then make it yours: colors for the strobe, the gauge and the pointer, sharps or flats, and a choice of typeface, San Francisco or New York, Apple’s serif.

New York has no flat sign. Ask for one and iOS quietly borrows it from a Japanese typeface, with a gap wide enough to park a car in. In serif mode, Stasis draws its flats from STIX, a classical serif with a flat that sits right where it should.

Under the hood.

Stasis is built in SwiftUI, with the signal processing written in Swift on top of Apple’s Accelerate framework. A few problems took longer than everything else combined. These are the three I’m proudest of.

One ear instead of two.

The first versions of Stasis listened twice. One part worked out which note you were playing, and the strobe measured how far off it was. Most of the time they agreed. When they didn’t, the letter said one thing and the rings another, and I patched each disagreement with another rule. Twenty rules later, I couldn’t trust it.

So I started over with a single measurement. Fifty times a second, Stasis reads the string once, and the note and the cents both come from that one number. They can’t disagree, because they’re the same thing.

That uncovered a quieter problem. A real string’s overtones run slightly sharp, and they pulled every reading a few cents with them. Pedal tuners read the string’s fundamental, its lowest and truest frequency, so Stasis does too: it watches that one frequency turn over a fraction of a second, and how far it turned is the exact pitch.

// A real string's overtones sit a little above exact multiples, which pulls
// the repeat a few cents sharp. So the note's own frequency is measured:
// its phase at the start and the end of the window, and how far it turned.
let a = phasor(0, frequency), b = phasor(hop, frequency)
var turn = atan2(b.im, b.re) - atan2(a.im, a.re)
    - 2 * .pi * frequency * Double(hop) / sampleRate
turn -= 2 * .pi * (turn / (2 * .pi)).rounded()
return frequency + turn * sampleRate / (2 * .pi * Double(hop))

The listening code shrank from about 1,000 lines to just over 600, and in testing, most strings read to within a cent.

The guitar’s own voice.

Stasis finds a note by measuring how often the sound repeats. It’s fast and it’s precise. But every so often, a soft high E would show up as an A, or stranger still, a C.

A recording of my own guitar explained it. My apartment hums at 60 Hz, the sound of something plugged into the wall, and a quiet high E is barely louder than that hum. The guitar’s body adds a low note of its own, too. Mixed together, the whole sound only repeats every third E wave, or every fifth. That reads as an A (110 Hz, a third of 330) or a C (66 Hz, a fifth). The detector wasn’t wrong. It was answering a different question: how often the whole sound repeats, rather than which note stands out.

So after finding the repeat, Stasis checks where the energy actually sits. A real A spreads its energy across all of its harmonics. A hum plus a high E puts nearly all of it on every third one.

// A real note spreads its energy across its harmonics. A quiet note at 3, 4
// or 5 times the repeat puts nearly all of it on every 3rd, 4th or 5th one.
for m in [5, 4, 3] {
    let multiples = (2...10).filter { $0 % m == 0 }
        .map { harmonic[$0] }.reduce(0, +)
    let others = (2...10).filter { $0 % m != 0 }
        .map { harmonic[$0] }.reduce(0, +)
    if multiples > 4 * others, multiples > 0.1 * total {
        return f0 * Double(m)
    }
}
return f0

Two smaller rules finish the job. A note can drift while you turn a peg, but it can’t wander more than a semitone from where it started, so a fading string never slides onto the hum. And anything lower than a guitar’s lowest string isn’t a note at all.

On that recording of 23 plucks, Stasis went from 14 right to all 23. The two quietest, barely louder than the hum, now show nothing rather than a guess. And that recording runs against every change I make.

Rings that don’t panic.

Some strings never sit still. The open A on my guitar wobbles a few cents either way on every pluck, which kept the inner ring starting and stopping. The gauge is allowed to wobble. That’s its job. The rings should say something calmer: it’s a little wobbly, but it’s in tune.

So the rings follow the median of the last 0.4 seconds instead of every reading, and once a ring has stopped, it takes a clearly bigger error to start it again.

// The rings follow where the note sits, not every wobble:
// the median of the last 0.4 seconds of cents.
let sorted = centsHistory.map { $0.cents }.sorted()
let median = sorted[sorted.count / 2]

// A ring freezes once the median is inside its window, and only
// starts turning again when it is clearly outside it.
innerFrozen = innerFrozen
    ? abs(median) <= innerInTuneCents * freezeExitFactor
    : abs(median) <= innerInTuneCents

A wobbling note averages out to stillness, while a string that’s genuinely sharp or flat still spins the rings in the right direction.

Get Stasis.

Coming soon to iPhone, Mac and Apple Watch. No accounts, no ads and no tracking.