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.
Description
Repeated re-scheduling of
AudioParam.setTargetAtTimeon 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)onOscillatorNode.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-lineAnalyserNode, 8192-pt FFT, context sample rate 48000).Notes on severity/scope:
setTargetAtTimeevery 5 s with a long time constant produces audible pitch instability.BiquadFilterNode.frequency.linearRampToValueAtTimeis rendered sample-accurately, socancelScheduledValues+setValueAtTime(param.value)+linearRampToValueAtTimeis a working substitute (this is the workaround we shipped).Possibly related to the
AudioParamevent-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
frequencyevery 100 ms. Expected (and what browsers do): a smooth glide between 220 and 330 Hz. Actual on iOS: audible warble / sidebands around the pitch.Swapping the
setTargetAtTimeline for the ramp equivalent renders cleanly:To make the artifact visible without listening: insert an
AnalyserNodebeforedestinationand FFT the output — withsetTargetAtTimethe spectrum shows a spread of inharmonic partials instead of a single moving peak.Secondary observations (mention in case they're useful)
AnalyserNodeon a side branch (connected from a source but not itself on a path todestination) is never rendered and reads silence. AnalyserNode requires connection to AudioContext.destination, causing unnecessary memory usage #860 tracks this; noting it here because it makes debugging this bug harder — the analyser must be in-line.AudioContext.sampleRateis 48000 on the iOS simulator (worth knowing when analyzing captured PCM; browsers commonly default to 44100).Environment