Skip to content

Fix rate-position desync from repeated sonicSetPitch calls - #10

Open
austek wants to merge 1 commit into
espeak-ng:masterfrom
ZirekHQ:fix/pitch-position-desync-espeak
Open

Fix rate-position desync from repeated sonicSetPitch calls#10
austek wants to merge 1 commit into
espeak-ng:masterfrom
ZirekHQ:fix/pitch-position-desync-espeak

Conversation

@austek

@austek austek commented Aug 23, 2026

Copy link
Copy Markdown

Summary

Same bug, same fix as waywardgeek/sonic#73 — this repo's sonicSetPitch/sonicSetRate/adjustRate are byte-identical to what those had upstream, so the fix ports directly.

adjustRate's wraparound arithmetic (oldRatePosition/newRatePosition) assumes both counters were accumulated under one consistent (oldSampleRate, newSampleRate) ratio — processStreamInput computes that ratio as stream->rate * stream->pitch. sonicSetRate resets both counters to 0 because changing rate invalidates the ratio the counters were accumulated under; sonicSetPitch changes the exact same ratio but never got the same reset.

Left to run, the counters drift further from what the (now-changed) ratio expects with every subsequent pitch change. Eventually the drifted counters overflow interpolate()'s position arithmetic, indexing sincTable far out of bounds — confirmed via ASan: global-buffer-overflow in findSincCoefficient, called from interpolate, called from adjustRate.

Fix

Reset oldRatePosition/newRatePosition in sonicSetPitch too, mirroring sonicSetRate exactly.

Adds tests/pitch_position_test.c with a deterministic repro (10 fixed pitch values, no randomness) — verified crashing under ASan before this fix, clean after.

Test plan

  • cd tests && make runtests && ./runtests — passes, new test included
  • Same build under -fsanitize=address,undefined — no finding from this path; only the separate, already-known findSincCoefficient finding remains

adjustRate's wraparound arithmetic (oldRatePosition/newRatePosition)
assumes both counters were accumulated under one consistent
(oldSampleRate, newSampleRate) ratio -- processStreamInput computes
that ratio as stream->rate * stream->pitch. sonicSetRate resets both
counters to 0 because changing rate invalidates the ratio the
counters were accumulated under; sonicSetPitch changes the exact same
ratio but never got the same reset.

Left to run, the counters drift further from what the (now-changed)
ratio expects with every subsequent pitch change. Eventually the
drifted counters overflow interpolate()'s position arithmetic,
indexing sincTable far out of bounds -- confirmed via ASan:
global-buffer-overflow in findSincCoefficient, called from
interpolate, called from adjustRate.

Fix: reset oldRatePosition/newRatePosition in sonicSetPitch too,
mirroring sonicSetRate exactly.

Adds tests/pitch_position_test.c with a deterministic repro (10 fixed
pitch values, no randomness) -- verified crashing under ASan before
this fix, clean after.

Ported from waywardgeek/sonic#73, which fixes the same bug (also
present here, byte-identical sonicSetPitch/sonicSetRate/adjustRate)
upstream.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant