Skip to content

Fix synthio oscillator wrapping one sample late - #11276

Merged
tannewt merged 1 commit into
adafruit:mainfrom
bdbarnett:fix-dds-oscillator-off-by-one
Aug 31, 2026
Merged

Fix synthio oscillator wrapping one sample late#11276
tannewt merged 1 commit into
adafruit:mainfrom
bdbarnett:fix-dds-oscillator-off-by-one

Conversation

@bdbarnett

Copy link
Copy Markdown

Fixes #11266.

synth_note_into_buffer() in shared-module/synthio/__init__.c wraps the
DDS accumulator on

if (accum > lim) {
    accum = accum - lim + offset;
}

but lim is the exclusive end of the waveform loop -- the readable
samples are [offset, lim). Wrapping on > rather than >= lets the
accumulator land exactly on lim, and that iteration indexes
waveform[waveform_length]: one past the loop, and for the common case of a
note looping the whole table, one past the buffer itself. The ring-modulator
loop just below has the identical bug with ring_waveform.

Any note whose dds_rate divides lim hits the boundary on a schedule --
a table played at exactly one sample per output sample hits it every
waveform_length samples. When the loop covers the whole buffer, the read
is genuinely out of bounds, so the same script with the same events does
not render the same audio twice
.

Repro

A table with a loop end short of the buffer end, so the extra read stays
in-bounds and lands on a marker value instead of undefined memory
(waveform_loop_end=256 of a 512-sample table, frequency set so the
oscillator advances exactly one table step per output sample):

Measured on a unix coverage build of main, four blocks of 256 samples
(1024 total), marker at index 256:

before after
marker hits [255, 511, 767, 1023] []

Includes a regression test
(tests/circuitpython/synthio_oscillator_loop_end.py) built the same way;
it fails on current main and passes with the fix. The neighboring
biquad/filter tests that pass on unmodified main in the same environment
still pass.

synth_note_into_buffer() wraps the DDS accumulator on `accum > lim`,
but `lim` is the exclusive end of the waveform loop, so the boundary
sample (accum == lim) reads waveform[waveform_length] before wrapping
on the next iteration. For a note looping the whole table this reads
one sample past the buffer, so the same script can render different
audio from run to run depending on what the allocator left there.
The ring-modulator loop just below has the same bug.

Fix both by wrapping on `>=` instead of `>`.

Adds a regression test with a table whose loop end falls short of the
buffer end, so the read stays in-bounds and the extra sample shows up
deterministically as a marker value.

Fixes adafruit#11266

@tannewt tannewt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@tannewt
tannewt merged commit 24bd08f into adafruit:main Aug 31, 2026
38 of 41 checks passed
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.

synthio oscillator wraps one sample late and reads past the end of the waveform

2 participants