Summary
With pitchCorrection: true, an AudioBufferSourceNode played at any
playbackRate other than exactly 1 adds an audible, steady periodic tone on a
physical device. The tone's level follows the material's envelope, so on a drum
loop it reads as a "beep" behind the kit that swells with each hit.
At playbackRate === 1 the output is clean, and the artifact appears as soon as
the rate leaves 1 — including small deviations such as 1.05.
What is affected
Time-stretched playback of pre-recorded audio — in our case a drum-loop practice
feature, where the whole point is to play a loop recorded at 92 BPM at some
other tempo while keeping its pitch. pitchCorrection: true is exactly the
right tool for that and it is unusable at present, so we have had to fall back
to plain resampling and let the pitch move with the tempo.
Reproduction
const ctx = new AudioContext();
const buf = await ctx.decodeAudioData(require('./drums.wav')); // any dense/percussive material
const src = ctx.createBufferSource({ pitchCorrection: true });
src.buffer = buf;
src.playbackRate.value = 1.14; // clean at exactly 1.0; rings at 1.02 and above
src.connect(ctx.destination);
src.start(ctx.currentTime + 0.05);
Listen on a device. Percussive, broadband material makes it obvious; a sustained
tone or speech hides it.
It does not reproduce offline
This is the part that may narrow it down. Rendering the identical graph through
OfflineAudioContext produces no artifact at any rate:
- Rendered the same buffer at rates
1.00, 1.02, 1.05, 1.14 and 1.30
and measured narrowband energy (peak-to-median across a Goertzel bin bank,
taking the 10th-percentile level per bin across frames so a sustained tone
separates from percussive transients). All five score the same, ~11x. Nothing
is added by stretching.
- At
playbackRate === 1, pitchCorrection: true and false render
bit-identical output (worst sample difference 0.00000), which is
consistent with the stretcher being bypassed at unity — and unity is the one
case with no audible artifact.
So the fault appears to be in the realtime path rather than in the WSOLA
arithmetic itself.
Source observations
From common/cpp/audioapi/dsp/WsolaTimeStretcher.{h,cpp}:
OLA_WINDOW_MS = 20.0f, hopSize_ = windowSize_ / 2 — a fixed 10 ms
synthesis hop, i.e. a 100 Hz periodicity, which is roughly the character of
what we hear.
SEARCH_DECIMATION = 12 — the similarity search is decimated 12x, so poor
splice points seem plausible if the realtime path is also under-feeding the
stretcher.
- None of these are reachable from JS, so there is no way to tune around it from
the application side.
WsolaTimeStretcher.cpp is byte-identical between 0.13.3,
1.0.0-nightly-1c00547-20260729 and 1.0.0-nightly-c12bd7c-20260901, so this
is not a regression — it reproduces on all three.
Environment
|
|
| react-native-audio-api |
1.0.0-nightly-1c00547-20260729 (also present in 0.13.3 and the 09-01 nightly) |
| react-native |
0.86.3 |
| expo |
57.0.18 |
| Platform |
iOS 26.6, physical iPhone 17 Pro Max, Release build |
| Context sample rate |
48000 (source files also 48 kHz, so no resampling involved) |
Happy to supply the offline measurement harness or a sample file if that would
help.
Summary
With
pitchCorrection: true, anAudioBufferSourceNodeplayed at anyplaybackRateother than exactly1adds an audible, steady periodic tone on aphysical device. The tone's level follows the material's envelope, so on a drum
loop it reads as a "beep" behind the kit that swells with each hit.
At
playbackRate === 1the output is clean, and the artifact appears as soon asthe rate leaves 1 — including small deviations such as
1.05.What is affected
Time-stretched playback of pre-recorded audio — in our case a drum-loop practice
feature, where the whole point is to play a loop recorded at 92 BPM at some
other tempo while keeping its pitch.
pitchCorrection: trueis exactly theright tool for that and it is unusable at present, so we have had to fall back
to plain resampling and let the pitch move with the tempo.
Reproduction
Listen on a device. Percussive, broadband material makes it obvious; a sustained
tone or speech hides it.
It does not reproduce offline
This is the part that may narrow it down. Rendering the identical graph through
OfflineAudioContextproduces no artifact at any rate:1.00,1.02,1.05,1.14and1.30and measured narrowband energy (peak-to-median across a Goertzel bin bank,
taking the 10th-percentile level per bin across frames so a sustained tone
separates from percussive transients). All five score the same, ~11x. Nothing
is added by stretching.
playbackRate === 1,pitchCorrection: trueandfalserenderbit-identical output (worst sample difference 0.00000), which is
consistent with the stretcher being bypassed at unity — and unity is the one
case with no audible artifact.
So the fault appears to be in the realtime path rather than in the WSOLA
arithmetic itself.
Source observations
From
common/cpp/audioapi/dsp/WsolaTimeStretcher.{h,cpp}:OLA_WINDOW_MS = 20.0f,hopSize_ = windowSize_ / 2— a fixed 10 mssynthesis hop, i.e. a 100 Hz periodicity, which is roughly the character of
what we hear.
SEARCH_DECIMATION = 12— the similarity search is decimated 12x, so poorsplice points seem plausible if the realtime path is also under-feeding the
stretcher.
the application side.
WsolaTimeStretcher.cppis byte-identical between0.13.3,1.0.0-nightly-1c00547-20260729and1.0.0-nightly-c12bd7c-20260901, so thisis not a regression — it reproduces on all three.
Environment
1.0.0-nightly-1c00547-20260729(also present in0.13.3and the 09-01 nightly)Happy to supply the offline measurement harness or a sample file if that would
help.