software-mansion / software-mansion/react-native-audio-api
pitchCorrection: audible periodic tone at any playbackRate other than 1 (iOS, realtime only)
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 839
- Forks
- 92
- Avg merge
- 4d 15h
- Merged PRs (30d)
- 27
Description
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.14and1.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: trueandfalserender
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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with common/cpp/audioapi/dsp/WsolaTimeStretcher.{h,cpp} and reproduce the issue on a physical iOS device using the provided AudioBufferSourceNode example at non-unity playback rates. Compare realtime feeding and stitching with the offline render, paying attention to the fixed 10 ms hop and decimated search. Done means pitch-corrected realtime playback is free of the periodic tone while unity-rate and offline behavior remain unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, ios, react-native
- Domain
- audio-video-rtc, mobile-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100