I talk to myself in the car. Not in a worrying way — in the “remember to tell the landlord about the boiler, and also that idea for the settings screen” way. For years the options were a voice memo I’d never listen to again, or a cloud dictation app that turned my ramble into text on somebody else’s server and kept a copy.
Neither felt right, and by 2026 neither was necessary. An iPhone can run a very good speech model and a small language model at the same time, with the radio off. Murmur is the app that assumes that and builds everything on it: tap, talk, and the phone writes the note.
Table of contents
Open Table of contents
The rule: the audio never leaves
This was the design constraint before any code existed. No account, no server, no “we may process your data to improve the service”. If a feature couldn’t run on the device, the feature didn’t ship.
That rules out the easy architecture — post the audio to an API, get JSON back — and forces two hard problems onto the phone: transcription, and turning a transcript into a note a human would actually want to read.
Two models, one phone
Transcription is WhisperKit, a CoreML port of OpenAI’s Whisper that runs on the Neural Engine. Murmur ships with Whisper small by default — a good trade of accuracy for speed — and offers a larger model in Pro for accents, noise and long dictation.
Note writing is a 0.6-billion-parameter Qwen3 model running through Apple’s MLX framework, using the mlx-swift-lm runtime. Its job is narrower than “be a chatbot”: take a messy transcript and produce a title, two or three tags, and the content cleaned into sentences and bullet points, in Markdown. It also spots “remind me to…” and hands the phrase to a local notification.
A 0.6B model is small enough to be fast and just big enough to do that job well. It is emphatically not big enough to answer general questions, and Murmur doesn’t pretend it is: “Ask your notes” answers only from your notes, and quotes the note it found the answer in.
The budget: seven seconds
On an iPhone 13 Pro Max a typical note takes about seven seconds end to end: three to transcribe, four to write. Model loads are cached at roughly three seconds for Whisper and one and a half for the LLM.
Two things had to be true for that budget to hold. First, both models stay resident in memory. A 6 GB phone can hold Whisper small (about half a gigabyte) and Qwen3-0.6B together without unloading either, so there’s no load/unload ping-pong between recording and writing. Second — and this one bit me — both models must be loaded before recording starts. Loading WhisperKit while the microphone is live resets the audio session and kills the mic tap mid-sentence. The fix was a one-time setup screen on first launch that downloads and warms both models, gated by an @AppStorage flag, so the record button never has to wait for anything.
The first launch is the only slow one: downloading the models and compiling Whisper for the Neural Engine takes about seventy seconds. After that the app works in airplane mode forever.
The crash that wasn’t memory pressure
Early builds crashed with signal 6 a few seconds into the second note. Everything about it said “out of memory”: two models loaded, an audio buffer, a big transcript. I spent an afternoon on unloading strategies before I did what I should have done first — pulled the actual crash log with devicectl:
xcrun devicectl device process launch --console --terminate-existing \
--device <UDID> com.saltserv.saltvoice
The log said: “Attempted to read an unowned reference but the object was already deallocated.” A dangling unowned reference to a throwaway store object I’d created in a closure. Nothing to do with memory pressure at all.
The lesson is boring and universal: get the real failure before theorising. devicectl … launch --console gives you stdout, stderr and the termination signal from a device build; WhisperKit and MLX log through os_log, so only your own print() shows up there — which, in this case, was enough.
Dependency archaeology
Two notes for anyone building on the same stack, because these cost real time:
- The MLX LLM runtime moved. It is no longer in
mlx-swift-examples(which now only ships MNIST and Stable Diffusion samples); it lives inml-explore/mlx-swift-lmwith the productsMLXLLM,MLXLMCommonandMLXHuggingFace. The load path is a macro:#huggingFaceLoadModelContainer(configuration:) { progress in … }, thenChatSession(container, instructions:, generateParameters:)andrespond(to:). - WhisperKit 0.18 pins
swift-transformersto1.1.6..<1.2.0. The MLX macro doesn’t hard-pin, so pinning transformers to 1.1.x in the app lets both resolve.swift-huggingface0.9 does not depend on transformers at all.
Notes are files
Every note is a Markdown file with YAML front matter — title, date, tags — stored where the app can export the whole folder in one tap. They open in Obsidian, iA Writer or a text editor. A voice-note app whose output you can’t leave with is a trap, and I didn’t want to build a trap.
Why a one-time price
Murmur’s marginal cost per user is zero: no API bill, no storage, no server. Charging a monthly fee for software whose only ongoing cost is my time felt dishonest, so Pro is a single purchase — unlimited Ask, the larger voice model, reminders, export, unlimited recording length. If Apple’s on-device foundation models get good enough to replace the bundled LLM, Murmur will switch to them and the price won’t change.
The store-title lesson
Murmur launched as “Murmur: AI Voice Notes” with “Record, transcribe & ask” as the subtitle and ranked for nothing — thirty tracked keywords, all unranked, because “voice notes” turned out to be a search term almost nobody types. The retitle to “Voice Recorder & Memo - Murmur” put the two phrases people actually search for in the title and “AI Note Taker & Transcription” in the subtitle. If you ship apps: the title is the ranking signal, and the name you like is worth exactly one slot in one locale.
Murmur is on the App Store. It’s free to try, and the setup screen will tell you honestly that the first minute is a download.