RORK LABJP
DEADLINE — From today, August 31, new submissions to Google Play and updates to existing apps must target Android 16, API level 36 or higherSCOPE — Existing apps are not removed. But anything below the requirement stops appearing for users on newer Android versions, so it does not vanish, it just stops reaching peopleEXTENSION — If you cannot make it, an extension can be requested through November 1. Every year some developers reach the deadline without knowing that option existedMIGRATION — What Rork generates sits on React Native and Expo, so raising targetSdkVersion always drags the Expo SDK and its native dependencies along with itTESTING — The time really goes after the build passes. Background execution limits, the permission model, and foreground service type declarations all take effect at onceORDER — A workable sequence: check whether an extension applies, plan the Expo SDK upgrade, then run device regression tests on a minimal build. Doing all three at once hides the causeDEADLINE — From today, August 31, new submissions to Google Play and updates to existing apps must target Android 16, API level 36 or higherSCOPE — Existing apps are not removed. But anything below the requirement stops appearing for users on newer Android versions, so it does not vanish, it just stops reaching peopleEXTENSION — If you cannot make it, an extension can be requested through November 1. Every year some developers reach the deadline without knowing that option existedMIGRATION — What Rork generates sits on React Native and Expo, so raising targetSdkVersion always drags the Expo SDK and its native dependencies along with itTESTING — The time really goes after the build passes. Background execution limits, the permission model, and foreground service type declarations all take effect at onceORDER — A workable sequence: check whether an extension applies, plan the Expo SDK upgrade, then run device regression tests on a minimal build. Doing all three at once hides the cause
Articles/Dev Tools
Dev Tools/2026-07-15Advanced

My Rork sleep timer faded out on time — but the sound didn't

Rork writes sleep-timer fades against the wall clock with setInterval. The numbers say 30 minutes, but the real audio fades early or cuts out abruptly. Here is how to drive the fade from the actual playback position, why a logarithmic curve sounds natural, and the honest limit of JS fades when the screen is locked.

Rork547expo-av6sleep timerReact Native233audiohealing app2

Premium Article

After years of running a healing app called Relaxing Healing, I have learned that the moment the sound disappears shapes how the whole app feels. If the audio cuts off with a click right as someone is falling asleep, they wake up. That is why the sleep-timer fade-out, unglamorous as it is, matters as the final touch.

While prototyping a new healing app in Rork recently, I lost a few hours to exactly this feature. In the simulator the sound faded away beautifully after 30 minutes, but on a real device it felt off — the volume dropped sooner than expected, and the last few seconds cut out sharply. Tracing it back, Rork's generated code stacked up several classic audio-fade pitfalls. Anyone building in this genre will hit the same wall, so here is how I untangled it.

What Rork generates for a sleep timer

Ask Rork to "add a sleep timer that slowly lowers the volume after 30 minutes and stops," and you usually get something like this.

// A pattern Rork tends to generate
const FADE_MS = 30_000; // fade over the last 30 seconds
const TICK_MS = 200;
 
function startSleepTimer(sound, totalMinutes) {
  const stopAt = Date.now() + totalMinutes * 60_000;
 
  const id = setInterval(async () => {
    const remaining = stopAt - Date.now();
    if (remaining <= 0) {
      await sound.stopAsync();
      clearInterval(id);
      return;
    }
    if (remaining <= FADE_MS) {
      const volume = remaining / FADE_MS; // 1.0 -> 0.0
      await sound.setVolumeAsync(volume);
    }
  }, TICK_MS);
 
  return id;
}

It looks correct. It measures the remaining time with Date.now() and ramps the volume down linearly once the fade window opens. And the volume does go down. But this code drifts away from the real audio in three separate layers.

Why measuring against the wall clock drifts

The first problem is that setInterval is anchored to the wall clock. setInterval(fn, 200) does not fire at exactly 200ms. If the JS thread is busy decoding images or rendering a list, that 200ms stretches to 250ms or 400ms. For something like a fade, where smoothness is the whole point, that jitter turns directly into an uneven volume ramp.

Second, sound.setVolumeAsync() is asynchronous. Awaiting it every 200ms means that when the native side backs up, the callbacks pile on top of each other and the volume updates arrive in bursts.

Third, and most easily missed: the audio's playback position and the wall clock do not necessarily advance at the same rate. When the audio session recovers from an interruption (a phone call, another app's sound), or when a loop seam triggers a tiny rebuffer, the position of the sound you actually hear falls behind the wall clock. Watching only the wall clock, the timer decides "we're done" while sound is still playing, and it cuts out mid-fade.

AspectWall-clock setIntervalPosition-driven
Time referenceDate.now()actual positionMillis
When JS is busyinterval stretches, volume jittersnext update snaps to the right volume
After an interruptionsound and time diverge, cuts out earlystays in sync with the sound
Determinismaccumulated error, drifts each runvolume is a pure function of position

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
Understand why Rork's setInterval fade drifts away from the real audio, and replace it with a playback-position-driven implementation you can trust
Get the concrete math for why a linear volume ramp sounds like it drops too fast, and how a logarithmic curve fixes the perceived fade
Learn the real limit of JS fades once the screen is locked, and the pragmatic fix of a pre-faded tail asset that always plays to the end
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
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 $15 for lifetime access
View Membership →

Related Articles

Dev Tools2026-05-13
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.
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-03-28
Build a Music Player App with Rork — Audio Playback Implementation Guide Using expo-av
Learn how to build a music player app with Rork and expo-av. This step-by-step guide covers playback controls, seek bars, playlist management, and background audio with practical code examples.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →