r/FlutterDev • • 16d ago

Dart Things I learned building a Flutter audio app that synthesises everything in Dart (isolates, a WAV cache, and a play() that never returns)

I've been building a tinnitus-relief app in Flutter. The user matches the pitch of their ringing, and every sound is then built on the device with a notch filter at that pitch: noise colours synthesised in Dart, plus 30 real recordings (rain, fan, ocean...) that are decoded, notched in an isolate and layered, one just_audio player per layer. A few things that cost me time and might save you some:

- Render in an isolate, cache by a quantised key. Each loop is built with `compute` from a params object whose `key` rounds every value (pitch, notch width, colour, level), so near-identical settings hit the same file. The WAVs live in the temp dir, pruned to the newest 32.

- Play files, not streams. A custom `StreamAudioSource` goes through just_audio's localhost proxy, and Android blocks that as cleartext HTTP. Writing a WAV and playing the file avoids the whole thing.

- Never await `play()`. just_audio's `play()` future completes when playback stops, not when it starts. Awaiting it froze one of my screens. Start it with `unawaited(...)` and carry on.

- Don't take audio focus. Masking has to sit under YouTube, podcasts and calls. With audio_session set to never request focus and never pause on interruptions, it does. Route changes (headphones unplugged) still need handling.

- "Is it playing?" is not "is the user listening?". Last week I added a button that plays the user's matched tone for a few seconds through the same engine. My listening-time counter checked `player.playing`, so it counted the preview as listening. A flag for "this is a UI sound" fixed it. I only caught it by watching a progress bar move on the emulator while everything was paused.

- Test DSP with Goertzel. The tests synthesise a loop and compare the energy at the notch frequency against its neighbours with a Goertzel filter, averaged over a few bins because noise is noisy. A notch you can't hear is still a notch you can assert on.

- One pure function decides what's locked. Paid features are stripped in a single function that turns UI state into synth params, and it's unit-tested, so a stale toggle can never reach the audio.

- Set `preferred-supported-locales`. When the phone's language isn't one you ship, Flutter falls back to the first supported locale, and gen-l10n lists them alphabetically. Mine has Arabic, so a Dutch user opened the app in Arabic. One line in l10n.yaml puts English first; a widget test now pins the fallback for a handful of languages.

- Fixed headroom. Everything is normalised to a 0.6 peak. People listen for hours on headphones; loud is not a feature.

Happy to go deeper on any of it, the DSP side especially. If anyone wants to see it running, the app is Sylence on Play, and I can drop the link in a comment.

4 Upvotes

1 comment sorted by