◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-05-13Intermediate

Three Things I Got Stuck on When Implementing Ambient Sound in a Healing App with Rork

Three common pitfalls when implementing ambient looping audio in meditation and healing apps built with Rork: loop clicks, iOS silent mode, and battery drain — with working expo-av code.

Rork585expo-av6ambient soundhealing appmeditation app3React Native239background music

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.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ Dev Tools2026-05-05
Taking a Rork Podcast App to Production — Migrating to expo-audio, Recovering from Interruptions, and Resumable Downloads
Implementation notes for making a Rork podcast app survive real use: migrating off expo-av before SDK 55 removes it, recovering from call interruptions, resuming broken downloads, and budgeting storage against iCloud backup.
⬡ Dev Tools2026-09-04
EAS secret visibility does not keep a value out of your app — deciding prefix and visibility separately
The EXPO_PUBLIC_ prefix decides what ships inside your app; EAS visibility decides who can read it. Why stacking them blanks a value on OTA updates, and how to check your build.
⬡ Dev Tools2026-08-22
Every bulk replace exited zero. The damage was in the lines I did not delete
Run a bulk replace over generated code and the breakage lands on the neighbouring lines, not the matched ones. Here is what broke in a live project, and a dependency-free guard that checks the invariants a replace must preserve.