Arsis Metronome
A metronome is designed to do one thing: click. That works until you’re practicing loud, in a bright room or with the sound off, and you can’t tell whether you’re ahead of the beat or behind it.
I wanted to design one you could hear, see and feel, without a screen full of knobs.
So I built Arsis, a metronome for iPhone, Mac and Apple Watch that brings the mechanical metronome to all three senses.
More than a click.
What started as a way to make myself practice with a metronome more often turned into a question: how could something that only clicks be experienced in different ways? And could it stand out among the countless free metronomes already out there?
The answer turned out to be less about features and more about time. Hearing, seeing and feeling the beat only works if all three land on exactly the same instant. Getting them there was the hardest part of the whole project.
It’s all in the details.
Arsis is a simple tool that does one thing well: it helps you keep time.
But it isn’t just a clicker. At its core, a metronome is a pulse, a repeated signal that tells you whether or not you’re in time. Arsis puts musicianship first, and I wanted to give you as many ways to reach that signal as possible.
Every beat can land in three ways: a click from one of a few sound kits, a pulse of color on screen (a flash or a ring) and a tap from the haptic engine, in case your phone is in your pocket. On Apple Watch, the beat moves to your wrist, so it keeps going with your arm down.
The screen itself is the interface; your phone becomes the metronome. Tap to start, hold for menu.
Tap to set the tempo.
Setting the tempo on a mechanical metronome takes constant fiddling until you get it right. With Tap Mode, you just tap along. Whether you’re counting in 4/4, 3/4 or 6/8, you can set the rhythm naturally.
Tap one full bar and Arsis takes the average. The first click lands exactly one beat after your last tap, right where your next tap would have been.
Accents, ghosts and color.
If you need extra help keeping time, add some accent notes or ghost notes. Accent notes land harder. Ghost notes are silent, but still show on screen, so you keep your place in the bar. Pick a color for regular beats and another for accents. A single slider sets the intensity of the color, from a soft wash to a full bleed. All of it carries over to Apple Watch.
Alive even when paused.
Good time takes practice, and practice takes time. I wanted Arsis to be something you’d happily leave open for a whole session.
A static screen makes you want to turn your device off. So when Arsis is paused, a faint glow rests around the edge of the screen and slowly drifts. You can change its color, which makes it feel like yours and gives you one more reason to keep it up. It gives the app life even when it’s not in use... and it looks awesome.
Under the hood.
Arsis is built in SwiftUI, with its sound running on Apple’s AVAudioEngine. Almost every hard problem came down to the same question: when, exactly, is the beat? A sound, a color and a tap all have to answer it the same way, or the whole idea falls apart.
The click that wasn’t late.
Early on, something felt off. In Tap Mode, the sound, the flash and the vibration all landed together. Once the metronome was running on its own, the click seemed to drag a hair behind everything else.
It turned out the click was the only thing on time. The screen is a bad place to keep time, because animations and touches are constantly interrupting it. So Arsis keeps time on the audio hardware’s own clock, booking each click a fraction of a second ahead, down to the exact sample. The problem was that the color and the vibration fired the moment a click was booked, not when it played. They were arriving up to 150 milliseconds early.
The sound wasn’t late. Everything else was early. The fix was to make the eyes and the hands wait for the ears.
// Every 20 ms, look 0.15 seconds ahead and book any beats due in that window.
while nextNoteSampleTime < currentSampleTime + scheduleAheadSamples {
// The click is booked on the audio clock, down to the exact sample.
let when = AVAudioTime(sampleTime: nextNoteSampleTime, atRate: sampleRate)
playerNode.scheduleBuffer(buffer, at: when, options: .interrupts)
// The color and the tap wait for that same moment, instead of firing now.
let secondsUntilBeat = Double(nextNoteSampleTime - currentSampleTime) / sampleRate
DispatchQueue.main.asyncAfter(deadline: .now() + secondsUntilBeat) {
self.pulseEvent = event
}
nextNoteSampleTime += AVAudioFramePosition(60.0 / currentBPM * sampleRate)
}Tap Mode never had the problem, because there, your finger is the clock.
Faster than sound.
Bringing Arsis to Apple Watch brought the same problem back in a new form. On the wrist, the tap is the beat. It’s how you keep time with your arm down and the screen off. But now the tap was arriving before the click.
Every speaker has a small delay between a sound being sent and a sound being heard, and on the Watch it’s long enough to feel. A tap from the haptic engine, on the other hand, is almost instant. So the Watch asks how long its sound takes to come out, and holds each tap for exactly that long.
// How long a click takes to leave the speaker once it has been booked.
// Read again whenever the output changes, like when headphones connect.
private func refreshOutputDelay() {
let session = AVAudioSession.sharedInstance()
outputDelay = session.outputLatency + session.ioBufferDuration
}
// On the Watch, each tap waits out that delay. With the sound off,
// there is nothing to wait for.
let syncDelay = playsSound ? outputDelay : 0
DispatchQueue.main.asyncAfter(deadline: .now() + secondsUntilBeat + syncDelay) {
self.pulseEvent = event
self.playHaptic(for: event.state)
}Connect headphones, which take longer still, and the taps adjust on their own. Turn the sound off, and they land right on the beat. The taps are played by the timing engine rather than the screen, so they keep going even after the display goes to sleep.
A glow that costs nothing.
The resting glow is on screen every time Arsis is paused, which is most of the time it’s open. It had to be beautiful, but it also had to be cheap.
My first version was neither. It painted the travelling light into the glow itself, which meant the blur, the most expensive thing on screen, was being recalculated across the whole display sixty times a second. It was extremely laggy.
The answer was to separate what the glow looks like from how it moves. The blurred glow is drawn once and never changes, so the phone can simply reuse it. The movement comes from a mask laid over the top, a pattern of light and shade that slides around the edge, and masking costs almost nothing.
// The glow itself never changes, so it is blurred once and reused.
ZStack {
RoundedRectangle(cornerRadius: 0)
.stroke(regularColor, lineWidth: 60)
.blur(radius: 40)
RoundedRectangle(cornerRadius: 0)
.stroke(regularColor, lineWidth: 24)
.blur(radius: 16)
}
.mask(restingMask)
// All the movement lives in the mask: two soft crests of light
// that travel around the edge of the screen every four seconds.
func restingMaskGradient(at date: Date) -> AngularGradient {
let phase = date.timeIntervalSinceReferenceDate
.truncatingRemainder(dividingBy: 4) / 4
let colors: [Color] = (0...12).map { i in
let t = Double(i) / 12
let wave = (sin((t + phase) * 2 * 2 * .pi) + 1) / 2
return Color.white.opacity(0.35 + 0.40 * wave)
}
return AngularGradient(gradient: Gradient(colors: colors), center: .center)
}Same glow, same motion, and it runs smoothly for as long as you leave it open.
Turn the phone, not the pulse.
After launch, the Ring pulse started to lag, but only after the phone had been turned sideways. Even back in portrait, the lag stayed. The obvious suspect was the large blur behind the ring.
Watching it closely, I noticed something simpler: the pulse has no reason to know which way the phone is held. It looks the same either way. Yet every time the phone turned, Arsis was measuring and drawing the pulses all over again.
// Measured once, at launch. A circle as wide as the screen's diagonal
// covers it in portrait and landscape alike, so turning the phone
// never touches the pulse.
private let screenDiagonal: CGFloat = {
let bounds = UIScreen.main.bounds
return hypot(bounds.width, bounds.height)
}()
private let pulseCanvasSide: CGFloat = screenDiagonal * 1.15Now the pulse is sized once, from the screen’s diagonal. The text, the glow and the menu turn with the phone, and the pulse just keeps going. The lag disappeared, and the blur was never the problem.
Perfection ★★★★★
Arsis Metronome feels like the metronome I’ve been waiting my entire life for. Its design is simple, effortless and extremely effective. Before I tried Arsis, I could hardly even follow a 4/4 time signature going 60bpm, but within a week of using Arsis there’s no time signature or bpm I can’t keep up with! Phi Lam is a genius and innovator! Highly recommended!!
Saved my life 😱 ★★★★★
Omg so simple and easy to use, great product, great accessibility, great for any musician on the go or at home. Definitely try it out!!!
Great ★★★★★
Such a simple user friendly interface
EXCELLENT METRONOME!!! ★★★★★
Would strongly recommend to any musician looking for an intuitive and functional metronome.
Amazing app ★★★★★
Helps me calm and regulate my mind
🎼🎨 ★★★★★
really cool and awesome
Get Arsis.
Free for iPhone, Mac and Apple Watch. No accounts, no ads and no tracking.