◉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-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.

Rork583expo-av6sleep timerReact Native239audiohealing app

✦ 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 $10 for lifetime access
View Membership →

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.