Murmur: Sleep Sounds & Timer
A native Android (and iOS) ambient sound app built to solve one problem: playing rain and white noise overnight without interruptions. No engagement prompts. No accounts. No data collected.
Tech Stack
Flutter, Dart, just_audio, audio_service, Android, iOS
Timeline
In Development
Category
Mobile Apps
Mission
Replace YouTube Kids for overnight audio — indefinite background playback with no 'Are you still watching?' interruptions, built on native Android foreground services and seamless audio looping.

My daughter listens to rain sounds to sleep. We’d been using YouTube Kids on her TV overnight — which works until the “Are you still watching?” prompt wakes her up. Murmur is the fix.
The Problem
YouTube Kids is built for engagement, not for indefinite passive audio. Its session-timeout logic is by design: it exists to reduce bandwidth for inactive users. The result is a hard failure at 3am that resets a sleeping child’s arousal threshold.
Dedicated native apps solve this because iOS and Android provide specialised frameworks explicitly for background playback. A properly configured foreground service on Android — or an AVAudioSession(.playback) on iOS — tells the OS to treat this app differently from a video player.
Architecture
Audio Engine
The core is built on just_audio + audio_service. This combination provides:
- Gapless looping with under 40ms latency between iterations — below the human perception threshold for continuous ambient noise
- Platform-agnostic playback — same Dart code drives both Android and iOS
- Foreground service integration — Android’s
mediaPlaybackforeground service (mandatory from API 34+) surfaces system notification controls while satisfying OS lifecycle requirements - Lock screen controls — media style notifications with play/pause, track skip, and progress on both platforms
The AudioHandler class is the single source of truth for all playback state. It extends BaseAudioHandler from audio_service and manages:
- Audio source concatenation for seamless track transitions
- Playback rate and volume state
- Notification metadata updates
- Sleep timer countdown integration
Sound Processing Pipeline
Each audio file is engineered for gapless looping:
- Source selection — CC0-licensed recordings from validated public domain archives
- Zero-crossing detection — trim to frame-accurate loop points at waveform zero-crossings to eliminate clicks
- Crossfade equalisation — apply equal-power crossfades (typically 50–100ms) at loop boundaries
- Encoding — AAC 128kbps for optimal size/quality balance on mobile
This pipeline eliminates the “metronome effect” — the faint rhythmic pattern that prevents the brain from habituating to a sound. Properly looped ambient noise should be indistinguishable from a continuous source.
Sleep Timer
The sleep timer uses a descending countdown that syncs with audio position. When it triggers:
- The app initiates a 5-second volume fade-out (linear ramp)
- Audio is paused, not stopped — preserving position for resume
- A system notification confirms the timer completed
The fade-out is critical. An abrupt silence at 3am is more disruptive than the sound stopping naturally.
Zero-Data Architecture
Murmur collects nothing. No analytics SDK, no crash reporters, no device identifiers. This is COPPA and GDPR-K compliance by architecture — not by policy.
The trade-off is visibility: without crash reporting, I rely on Firebase Test Lab and manual pre-release testing for quality assurance.
Platform-Specific Challenges
Android Foreground Services
From Android API 34 (Android 14), foreground services must declare a foregroundServiceType in the manifest and request POST_NOTIFICATIONS permission at runtime. The android.app.ForegroundServiceStartNotAllowedException crash is a common issue for apps targeting modern Android.
Murmur uses the mediaPlayback type with the FOREGROUND_SERVICE_DATA_SYNC manifest declaration. The notification is mandatory and cannot be dismissed — this is an Android OS requirement, not app behaviour.
iOS Audio Session
On iOS, the AVAudioSession must be configured as .playback category with .mixWithOthers option to allow other audio (alarms, calls) to interrupt gracefully. The app registers for AVAudioSessionInterruption notifications to handle phone calls and alarms without losing state.
Sound Library (v1.0)
8 curated CC0-licensed ambient tracks covering the full noise spectrum:
| Track | Type | Character |
|---|---|---|
| Rain | Natural | Steady medium-frequency patter |
| Thunderstorm | Natural | Rain with low-frequency rumbles |
| Ocean | Natural | Rhythmic wave cycles |
| Forest | Natural | High-frequency bird/distance blend |
| Fan | Mechanical | Broad-spectrum constant drone |
| Brown Noise | Synthesised | Deep, rumbling low frequencies |
| Pink Noise | Synthesised | Balanced, natural-sounding spectrum |
| White Noise | Synthesised | Full-spectrum, best for masking |
What I’d Do Differently
Start with platform channels from day one. The audio_service package abstracts most platform differences, but certain features (custom equalisation, per-track volume normalisation) require native platform channels. Retrofitting these is more complex than planning them early.
Use Isar or Drift for local state. The current version uses simple JSON file storage for sleep timer preferences and recent tracks. A local database would have been cleaner for tracking usage patterns (anonymously) to improve default recommendations.
Status
Currently in active development. Google Play submission is the initial target, with iOS to follow via the same Flutter codebase. The core audio engine, sleep timer, and zero-data model are implemented and tested on Android 14+.