# Sound that never repeats > Your brain detects loops; Estua generates fresh ambient sound every night. How real-time synthesis at 48 kHz replaces repeating rain clips. 2026-04-10 · 20 min read · estua, dsp · by ntan (ntan) for uranashel Canonical HTML: https://uranashel.com/blog/estua-non-repeating-audio.html --- Loop-based sleep apps fail for a perceptual reason, and better microphones cannot fix it. An 8-minute rain recording, however professionally captured, carries repeating micro-structure: the same cluster of droplet impacts, the same phase relationship between low rumble and high hiss, coming back every 480 s. By the third minute your auditory cortex has reclassified the signal from "environment" to "recording," and sleep grows lighter at exactly the moment you needed darkness and continuity. Estua goes the other way. It **synthesizes ambient audio in real time** at 48 kHz on the phone, so the output has no fixed period and no seam to detect. This post walks the whole chain, from the sample clock to the limiter. ## The clock and the contract Everything hangs off a master clock: 48 kHz, fixed blocks of 512 samples. Divide one by the other and you get the deadline, `512 / 48000 = 10.67 ms` per callback. Miss it once and the OS ships whatever stale data sits in the output buffer, which a listener hears as a click. Miss it at 3 AM and the product has failed at its one job. So the audio thread runs under a contract we treat like physics: no heap allocation, no locks, no logging, no network calls. Sonarish's capture path obeys the same rules, and the full discipline (pre-allocated buffers, lock-free parameter snapshots) is written up in [life on the audio thread](https://uranashel.com/blog/dsp-audio-thread.html). Estua adds one property that makes the contract simpler and stranger at the same time. It is a pure generator. No file to decode, no stream to buffer, no disk read racing the deadline. Every one of those 512 samples is computed from scratch, every 10.67 ms, all night. The signal path has to stay cheap enough to leave headroom for Bluetooth encoding and whatever the OS decides to do behind our back, and deterministic enough that block N sounds like it belongs after block N−1 with no stored history beyond a few filter states. ## From white to pink The chain starts with white noise. One PRNG draw per sample, flat spectrum, equal energy per hertz. Cheap and wrong. Each octave spans twice the bandwidth of the octave below it, so a flat-per-hertz spectrum doubles its energy per octave as frequency climbs, and the ear reads that as harsh hiss. What sleep wants is a 1/f power spectrum, `S(f) ∝ 1/f`. Integrate that across any octave and you get `∫ df/f = ln 2`, whether the octave starts at 100 Hz or at 4 kHz. Constant energy per octave, a slope of −3 dB per octave. That is pink noise, and it is roughly what rain on a roof and distant surf look like on a spectrum analyzer. An exact 1/f filter would need infinite state, because every ordinary filter pole contributes −6 dB per octave and we need precisely half of that. Paul Kellet's economical recursive filter solves it by staggering three one-pole low-pass sections at different time constants and summing them, so the staircase of their responses leans along the 1/f line: ``` // per sample; white is a uniform PRNG draw in [-1, 1] b0 = 0.99765 * b0 + white * 0.0990460 b1 = 0.96300 * b1 + white * 0.2965164 b2 = 0.57000 * b2 + white * 1.0526913 pink = (b0 + b1 + b2 + white * 0.1848) * outGain ``` Eight multiplies and six adds per sample, three floats of state, no transcendental functions. On our bench the staircase wobbles around the ideal slope by about ±0.5 dB from 20 Hz to 16 kHz, measured by averaging 300 FFT frames of the raw generator output. Nobody can hear that ripple against the noise itself. The iOS build sometimes swaps in an equivalent biquad stack for tighter shaping; the CPU difference is lost in measurement noise. Underneath sits an optional brown layer for ocean scenes: 1/f², a −6 dB per octave slope, generated by leaky integration of white noise, all rumble below roughly 200 Hz. ## Slow oscillators against periodicity Static pink noise beats a loop, but it is still static. Real environments breathe. The fix is modulation slow enough that it never registers as rhythm: a bank of low-frequency oscillators running between 0.02 and 0.15 Hz, periods of 6.7 to 50 s, each phase drawn uncorrelated from the session seed. One LFO leans on a filter cutoff. Another rides a layer's gain. A third nudges stereo width. The rates are chosen deliberately incommensurate, no period an integer multiple of another, so the joint state of the bank realigns only after the least common multiple of every period involved, which for our chosen ratios exceeds any plausible night of listening. Nothing repeats at 30 or 60 s intervals, exactly the timescales where loop-based apps betray themselves. Amplitude envelopes come from the same toolbox. A one-pole low-pass on the absolute value of a signal tracks its envelope at the full 48 kHz rate using precomputed coefficients, so a slow swell in the ocean layer costs two multiplies per sample and zero branches. Everything stays on the audio thread, every operation is O(1) per sample, and nothing allocates. ## Scenes, seeds, and the statistics of surf Each scene (Rain, Ocean, Wind, a few others) layers its own processing on the pink foundation. Rain adds band-passed noise bursts whose inter-arrival times follow a Poisson process: draw `u` uniform in (0, 1], schedule the next droplet at `t = −ln(u)/λ`, and let the rate λ itself wander under LFO control so the shower thickens and thins. Real droplets on a roof arrive with exactly this statistic; they do not queue politely. Ocean mixes slow amplitude modulation on a low band between 80 and 400 Hz with mid-frequency hiss, which produces the breathing quality of surf without any recorded waveform to repeat. Wind is pink noise pushed through a resonant band-pass whose center frequency drifts, close to what a doorframe does to a gust. At the end of the chain a soft limiter clips at −1 dBFS, preventing digital full-scale spikes that would startle a sleeping listener at 3 AM. Each night you choose a scene and a seed, or let the app pick randomly. The seed initializes the pseudorandom generators behind the noise samples and the LFO phase offsets, so the same seed reproduces the same soundscape for that session. If you liked last night's rain, you can have it back. We also optionally stir the seed with the calendar date, so that "Rain plus seed 42" is not bit-identical every night for years. Reproducibility within a session, variety across the weeks. ## Why crossfading two loops is not enough The standard industry workaround crossfades between two copies of a loop to hide the seam. It works, in the narrow sense that the audible click at the splice point disappears. The statistics do not move. Autocorrelation of looped rain still spikes at the loop period T and at every integer multiple of it, because the sample you hear at time t is literally the sample from time t − T. Human hearing picks up that kind of periodicity at remarkably subtle levels; evolution spent a long time rewarding brains that noticed patterned sounds in rustling grass. Real ocean surf approximates pink noise with occasional transient swash events whose timing follows no fixed schedule, and no crossfade manufactures that from an 8-minute source. Autocorrelation is the time-domain cousin of the power spectrum; the gentle introduction lives in [FFT made readable](https://uranashel.com/blog/fft-made-readable.html). We checked our own homework. Bench note: 30-minute captures of one commercial rain loop and of Estua's Rain scene, normalized autocorrelation computed offline in Python on the recorded output. The loop shows a correlation peak of 0.6 at its period. Estua stays below 0.05 at every lag beyond the filter memory of a few hundred milliseconds. The full synthesis stack costs 3–8% CPU on modern phones, and what that buys is hours of continuous playback with nothing for the auditory cortex to latch onto. ## Platform plumbing On iOS, Estua drives an AVAudioEngine manual source node callback under the playback session category, which keeps audio alive while the screen is locked. On Android it feeds an AudioTrack in PCM float encoding, requesting low-latency mode where the OEM supports it — latency matters less for a sleep app than for an instrument, but the low-latency path tends to be the best-tested one. Both platforms enforce the no-allocation rule in the hot path; parameters cross over from the UI thread as atomically swapped snapshot structs, never through locks. Bench note: across 6 phones (iPhone 12 through 15, a Pixel 6a, a Galaxy A54), an 8-hour overnight synthesis run with the screen off drained 11–19% of battery, logged by recording the battery level at the start and end of each run. The short version of all this lives on the [Estua app page](https://uranashel.com/apps/estua.html). ## What Estua refuses to be Estua contains no binaural beats, no AI-generated music, and no streaming wrapper around somebody else's server. There are no accounts and no analytics on listening habits; how you sleep is nobody's dataset, ours included. A timer fades the volume over 20 to 45 minutes on a curve slow enough that the fade never becomes an audible event of its own. A separate alarm uses a gentle synthesized tone drawn from a different scene, so the sound that wakes you is not the sound you slept to — waking a brain with the exact texture it spent all night filtering out works poorly. For the psychoacoustic background on 1/f noise and sleep, see [sleep, waves, and why your brain likes surf](https://uranashel.com/blog/estua-sleep-and-waves.html). For why the whole thing runs locally, read [why everything runs on your phone](https://uranashel.com/blog/on-device-first.html). --- uranashel · [Home](https://uranashel.com/) · [Apps](https://uranashel.com/apps.html) · [Lab](https://uranashel.com/lab.html) · [About](https://uranashel.com/about.html) · [Blog](https://uranashel.com/blog/) · [Developers](https://uranashel.com/developers/) · [API docs](https://uranashel.com/docs/) · [Privacy](https://uranashel.com/privacy.html) Machine-readable: [llms.txt](https://uranashel.com/llms.txt) · [sitemap.xml](https://uranashel.com/sitemap.xml) · [openapi.json](https://uranashel.com/openapi.json) · [API](https://uranashel.com/api/v1/)