I've been building apps independently since 2014, and my wellness, wallpaper, and manifestation apps have crossed 50 million cumulative downloads. In this category, sound is everything. The moment ambient audio stutters or cuts out, users snap back to reality — and a lot of them quietly uninstall.
Recently, I was prototyping a new meditation app in Rork and spent more time than I expected fighting ambient sound implementation. Rork handles screen layout and API wiring beautifully, but the code it generates for audio has a few recurring weak spots. Since I kept running into the same three problems, I want to write them down — with the fixes that actually worked.
Problem 1: A Click at the Loop Point
The first issue: a small "pop" or "click" sound every time the audio loops. When you ask Rork to implement looping ambient audio, it usually produces something like this:
// Pattern Rork commonly generates (prone to loop clicks)
const sound = await Audio.Sound.createAsync(
require('./assets/rain.mp3'),
{ isLooping: true }
);
await sound.playAsync();Setting isLooping: true should work in theory, but if there's any silence encoded at the tail of an MP3 file — which there almost always is — you'll hear a click on every loop. This isn't a Rork bug or a React Native bug; it's a fundamental quirk of the MP3 format.
Fix: Use WAV or m4a + manual loop control via PlaybackStatus
import { Audio } from 'expo-av';
import { useEffect, useRef } from 'react';
export function useAmbientSound(assetSource) {
const soundRef = useRef(null);
useEffect(() => {
let isMounted = true;
const loadAndPlay = async () => {
await Audio.setAudioModeAsync({
playsInSilentModeIOS: true, // also covers Problem 2
staysActiveInBackground: true,
shouldDuckAndroid: false,
});
const { sound } = await Audio.Sound.createAsync(
assetSource,
{ shouldPlay: true, volume: 0.6 }
);
if (!isMounted) return;
soundRef.current = sound;
// Loop fix: use onPlaybackStatusUpdate instead of isLooping
sound.setOnPlaybackStatusUpdate(async (status) => {
if (status.didJustFinish && isMounted) {
await sound.replayAsync();
}
});
};
loadAndPlay();
return () => {
isMounted = false;
soundRef.current?.unloadAsync();
};
}, []);
return soundRef;
}Listening for didJustFinish and calling replayAsync() is far more reliable on physical devices than relying on isLooping. For audio assets, use WAV or m4a (AAC) — avoid MP3 for anything that needs to loop seamlessly.
Problem 2: Silent Mode on iOS Kills All Audio
This one shows up in App Store reviews almost every time. A user opens your meditation app right before bed, their phone is on silent, and they hear nothing. It's one of those problems that's invisible in testing if you always test with sound on.
Rork's default generated code almost never includes the silent mode override. You have to add it explicitly:
// Call this at app startup or before loading any sound
await Audio.setAudioModeAsync({
playsInSilentModeIOS: true, // ← Without this, silent switch = no sound
allowsRecordingIOS: false,
staysActiveInBackground: true,
interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX,
shouldDuckAndroid: false,
playThroughEarpieceAndroid: false,
});This only needs to run once per session, but you'll also need corresponding entries in Info.plist for background audio mode. When prompting Rork, explicitly write "play audio even when iOS silent switch is on" — that phrasing significantly improves what it generates.
Problem 3: Battery Drain During Long Background Sessions
This one took me the longest to track down. During real-device testing, I noticed the app draining about 20% battery in an hour of background use — way more than expected for a simple ambient sound player.
Two causes turned out to be responsible. First, combining staysActiveInBackground: true with shouldDuckAndroid: false on a high-bitrate MP3 kept the audio decoder running at full priority the entire time. Second, the PlaybackStatus callback Rork generated was polling at the default 500ms interval — creating a constant drip of work on the JS thread even when nothing needed to change.
Fix: Optimize audio files + set progressUpdateIntervalMillis
const { sound } = await Audio.Sound.createAsync(
assetSource,
{
shouldPlay: true,
volume: 0.6,
progressUpdateIntervalMillis: 1000, // default 500ms → 1000ms
}
);For ambient audio, 96kbps AAC (m4a) is entirely sufficient. Streaming 320kbps MP3 or uncompressed WAV for hours of background playback is overkill. In my own apps, switching to 96kbps m4a reduced battery consumption by roughly 40%.
A Rork Prompt Pattern That Actually Works
Given these three problems, here's the prompt structure I now use when asking Rork to implement ambient audio:
Implement looping ambient sound with these requirements:
- Seamless looping — use didJustFinish + replayAsync(), NOT isLooping
- Play in iOS silent mode (playsInSilentModeIOS: true)
- Support background playback (staysActiveInBackground: true)
- Set progressUpdateIntervalMillis to 1000ms
- Asset format: m4a or WAV (not MP3)
Spelling out what to avoid, not just what to build, makes a significant difference in Rork's output quality.
The Part Rork Can't Do for You
Sound reaches users' emotions faster than visuals. After more than a decade building wellness apps, I'm more convinced of this than ever.
My grandfathers on both sides were shrine carpenters — craftsmen who believed that invisible joinery deserved the same care as what anyone could see. That feeling stuck with me. A single line of audio configuration that users will never notice is still worth getting right.
Rork lets you build the skeleton of an app at remarkable speed. But the texture of the experience — the parts that make someone keep coming back — still comes from the developer paying careful attention to exactly this kind of detail.
If you're building a meditation or wellness app with Rork, I hope the patterns here save you some time.