How We Built Whisperloop — A Custom Subliminal Audio App | Bitter Works
How we shipped a personalized audio app — voice cloning, dual-track playback, binaural tones — across iOS and web with a single Flutter codebase.
Whisperloop is a subliminal audio maker. Users record affirmations in their own voice, and the app turns them into looping, layered audio tracks designed to play softly in the background — at the gym, while you sleep, on a walk. We built it at Bitter Works, and this is a quick walk through the pieces that made it interesting.
The product, in one paragraph
Pick a goal (sleep, focus, confidence, manifestation, anything). Record a handful of affirmations — or have the app generate them. Whisperloop layers your whispered voice over an ambient background track, optionally adds a binaural tone underneath, and loops the whole thing for as long as you want it to play. Lock screen controls, background playback, custom volumes per track, playback speed, voice cloning so you don't have to record everything yourself.
Stack
- Flutter — one codebase, two real targets: iOS and web. (We started with Android too, but Play Store policy friction around the "subliminal" category made it a poor fit, so we dropped it and focused on iOS + web. More on that below.)
- Provider for state management — pragmatic, lightweight, and the team already knew it.
- Firebase — Auth, Firestore, Cloud Functions, Storage. Functions handle anything that touches a secret (Stripe, voice cloning provider keys).
- RevenueCat on iOS for subscriptions, Stripe on web. Two payment systems, one entitlement model.
- just_audio + audio_service + audio_session for the audio engine.
The hard part: dual-track audio
The core mechanic is two audio streams playing in sync: the whispered affirmation track on top, and an ambient background underneath. They need independent volume, they need to loop forever without gaps, they need to keep playing when the screen is off, and they need to show up on the lock screen with proper controls.
We run two just_audio AudioPlayer instances — one for whispers, one for background — coordinated through audio_service, which is what gives us the lock screen UI and background playback on both platforms. audio_session handles the platform-specific session config (iOS audio categories, Android audio focus) so playback survives interruptions like phone calls and routes correctly through Bluetooth and AirPods.
Gapless looping was the bit that took the most iteration. Naive looping ticks audibly at the seam; the fix was pre-loading the asset and using just_audio's loop mode rather than restarting playback on completion. Per-track volume and playback speed are independent so users can drop the background to almost zero and crank the whispers, or vice versa.
Voice cloning
Recording is on-device with the record package. The recording goes to Firebase Storage, a Cloud Function calls our voice cloning provider, the cloned voice gets cached, and from then on the user can generate new affirmations in their own voice without re-recording. The whole pipeline is async and the UI just polls for completion — keeps the app responsive even when cloning takes a minute.
Binaural tones
Optional, generated programmatically rather than shipped as audio files. A small native service synthesizes the two stereo frequencies (the difference between them is the binaural beat) and the audio engine layers it under the whisper + background tracks as a third stream. Generating the tone instead of bundling it means we can offer arbitrary frequencies — the file size doesn't grow with the option count.
Web vs mobile, branched cleanly
Flutter's kIsWeb lets us split behavior at the few spots where the platforms genuinely differ:
- Auth — email + password on iOS; Google and Apple popups on the web.
- Payments — RevenueCat SDK on iOS; Stripe Checkout via Cloud Functions on the web.
- Storage — SharedPreferences on iOS; localStorage on the web.
Everything else — the audio engine, the UI, the providers, the API layer — is shared. The split is small enough that the "one codebase" promise actually held.
Why we dropped Android
Play Store policy around "subliminal" content is significantly stricter than the App Store's, and getting consistent review outcomes was costing us more time than the Android user share justified at this stage. We pulled it, focused the roadmap on iOS and web (where we had a clean reviewer pass and a working Stripe integration), and shipped faster as a result. Android may come back; for now it isn't worth the friction.
What we'd do again
- Flutter for cross-platform audio apps — the audio plugin ecosystem is mature, and the iOS/web split was cheap.
- Cloud Functions as the secrets boundary — anything that touches Stripe or third-party API keys lives there. The client never sees them.
- Two payment providers, one entitlement model — RevenueCat and Stripe write to the same "is the user subscribed?" check. The rest of the app doesn't care which one paid.
- Cut platforms when policy fights aren't worth it. Shipping beats completeness.
Try it
Whisperloop is live at whisperloop.com on iOS and web. If you want something similar built — an audio app, a voice product, anything Flutter-shaped that needs to ship on more than one platform — get in touch with Bitter Works.