Skip to content

Repeated setTargetAtTime re-scheduling renders as FM warble / inharmonic sidebands on iOS (browsers render a smooth glide) #1191

Description

@abiassi

Description

Repeated re-scheduling of AudioParam.setTargetAtTime on the same param renders as audible FM warble / inharmonic sidebands on iOS, where browsers render a smooth exponential approach to the (moving) target.

We hit this in a meditation app that glides oscillator frequencies with the common "retarget on a timer" pattern: every tick, call setTargetAtTime(newTarget, ctx.currentTime, tc) on OscillatorNode.frequency. In every browser this produces a clean glide — each call replaces the previous exponential approach and the param converges smoothly. In react-native-audio-api 0.13.2 on iOS the oscillator instead sounds frequency-modulated: a steady drone of 131/198/266 Hz oscillators came out as a dense inharmonic cluster spanning roughly 199–410 Hz (measured with an in-line AnalyserNode, 8192-pt FFT, context sample rate 48000).

Notes on severity/scope:

  • It is not just fast re-scheduling: 10 Hz retargeting is badly warbled, but even one setTargetAtTime every 5 s with a long time constant produces audible pitch instability.
  • Same behavior on BiquadFilterNode.frequency.
  • linearRampToValueAtTime is rendered sample-accurately, so cancelScheduledValues + setValueAtTime(param.value) + linearRampToValueAtTime is a working substitute (this is the workaround we shipped).

Possibly related to the AudioParam event-handling refactor in #995 / the boundary bug in #1025 — but this reproduces on 0.13.2, i.e. after that fix.

Steps to reproduce

Minimal repro — one oscillator, retarget frequency every 100 ms. Expected (and what browsers do): a smooth glide between 220 and 330 Hz. Actual on iOS: audible warble / sidebands around the pitch.

import { AudioContext } from 'react-native-audio-api';

const ctx = new AudioContext();
const osc = ctx.createOscillator();
osc.frequency.value = 220;
osc.connect(ctx.destination);
osc.start();

let high = false;
setInterval(() => {
  high = !high;
  // Browsers: smooth exponential approach toward the new target.
  // RNAA iOS: audible FM warble / inharmonic sidebands.
  osc.frequency.setTargetAtTime(high ? 330 : 220, ctx.currentTime, 0.5);
}, 100);

Swapping the setTargetAtTime line for the ramp equivalent renders cleanly:

const t = ctx.currentTime;
osc.frequency.cancelScheduledValues(t);
osc.frequency.setValueAtTime(osc.frequency.value, t);
osc.frequency.linearRampToValueAtTime(high ? 330 : 220, t + 0.5);

To make the artifact visible without listening: insert an AnalyserNode before destination and FFT the output — with setTargetAtTime the spectrum shows a spread of inharmonic partials instead of a single moving peak.

Secondary observations (mention in case they're useful)

Environment

  • react-native-audio-api: 0.13.2
  • Platform: iOS (reproduced on the iOS simulator; the audible warble was first reported on device)
  • React Native: 0.85.3 / Expo SDK 56
  • Real-world context: https://deepbreathingexercises.com mobile app — a drone of 3 detuned oscillators re-targeted at 10 Hz became an inharmonic 199–410 Hz cluster; switching to linear ramps restored the intended 131/198/266 Hz stack exactly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions