Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ of this repository in the parent workspace, same pattern as
`.venv/bin/python tests/parity/verify_dsp.py --micropython ../micropython/ports/unix/build-pydevices/micropython`
is the first. There is **no stored digest** — the gate is the comparison, and
it **refuses a run with fewer than two interpreters** rather than passing one
that cannot fail. Add `--circuitpython ../bin/circuitpython-oracle-11.0.0-alpha.1`
that cannot fail. Add `--circuitpython ../bin/circuitpython-oracle-11.0.0-alpha.2`
for the three-way. The arithmetic is all in `src/shared/`, so two
interpreters disagreeing is never a difference of intent: it is a width, an
undefined shift, a compiler's choice or an architecture.
Expand Down
13 changes: 7 additions & 6 deletions CIRCUITPYTHON_ORACLE
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# The CircuitPython oracle pin. Every parity golden is measured against
# this exact commit (tag 11.0.0-alpha.1; the pin moved from 10.3.0 on
# 2026-10-08 with audiodsp#201, from 10.2.1 to 10.3.0 on 2026-09-09).
# this exact commit (tag 11.0.0-alpha.2; the pin moved from 11.0.0-alpha.1 on
# 2026-10-22, from 10.3.0 on 2026-10-08 with audiodsp#201, from 10.2.1 to
# 10.3.0 on 2026-09-09).
# Moving it is a re-port, not a bump: every move is a resync with upstream
# (docs/upstream-sync.md; the last was audiodsp#220). See also AGENTS.md
# "The CircuitPython oracle" and docs/upstream-diff.md. The 64-voice oracle binary built from
# it lives at bin/circuitpython-oracle-11.0.0-alpha.1 (build_interpreters.sh
# (docs/upstream-sync.md; the one before this was audiodsp#220). See also
# AGENTS.md "The CircuitPython oracle" and docs/upstream-diff.md. The 64-voice oracle binary built from
# it lives at bin/circuitpython-oracle-11.0.0-alpha.2 (build_interpreters.sh
# cp-oracle); tests/test_voice_ceiling_consistency.py pins its sha256.
11.0.0-alpha.1 520805e12127bdb51722ba1caa5c670ecd8c4f72
11.0.0-alpha.2 d53b2277cfad71589028e5da77c984c045db990d
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -850,7 +850,7 @@ it.

**CPython:** the public surface and wheel plumbing are present, and the
committed synthesis, mixer, MIDI, streaming, and effects fixtures match
CircuitPython 11.0.0-alpha.1 PCM byte-for-byte — built at the same voice ceiling this
CircuitPython 11.0.0-alpha.2 PCM byte-for-byte — built at the same voice ceiling this
port ships, which is the only comparison worth making. Import/API smoke success
is not used as a substitute for those comparisons.

Expand Down
111 changes: 73 additions & 38 deletions docs/upstream-diff.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,13 +218,64 @@ skipped on that probe, since its own nodes keep upstream's behaviour, and
`pitchshift_freeze_probe.py` no longer renders after its reset, so it stays
held to CircuitPython's bytes.

## Resynced with 11.0.0-alpha.2

The oracle moved from 11.0.0-alpha.1 to 11.0.0-alpha.2 (d53b2277c) on
2026-10-22, with the binary at `bin/circuitpython-oracle-11.0.0-alpha.2`.
Three upstream commits touched the ported code in between, all in
`audiomixer` and `synthio_sat16`. What was taken is in
[upstream-sync.md](upstream-sync.md#the-last-resync-1100-alpha2).

**The mixer scales in integers (11a329885, fc57c0f36; adafruit/circuitpython#11617).**
Upstream's non-DSP `mult16signed` was the float quotient `mul / 32767.0f`
times the sample, truncated toward zero. It is `(ai * mul) >> 15` now, which
upstream says "matches the DSP result bit for bit", and a centred pan is
`32768 - panning` where it was `32767 - panning`. Both changes are in
`src/audiomixer/Mixer.c`, in the CPython extension's scaling kernel
(`src/cpython/_audiodsp.c`) and in its Python twin (`src/cpython/audiomixer.py`),
as upstream has them. Three things follow:

- Every level below 1.0 moves by up to one LSB. The divisor is 32768 where it
was 32767, and the shift floors where the cast truncated, so a small
negative sample rounds down: -1 at level 0.5 stays -1, where it used to
become 0. Material that never goes through a mixer voice below unity does
not move.
- Level 1.0 is a wire without help. This port's unity branch is deleted, and
with it the difference recorded under
[A voice at level 1.0 is a wire](#a-voice-at-level-10-is-a-wire-audiodsp95).
On the CPython target the block-level shortcut that skips the scaling at
unity stays, keyed on Q15 32768 alone: 32767 used to scale by exactly 1.0
and no longer does.
- A stereo voice at pan 0 is the same on both channels. The left channel no
longer sits one Q15 step below the right.

`mixer_q15_level_probe.py` (it replaces `mixer_level_precision_probe.py`,
whose material was the float32-versus-float64 flips that no longer exist)
renders the rails and the values either side of zero at four levels and five
pans, mono and stereo, and `verify_dsp.py` holds all three interpreters to
the same bytes. A build from before the change prints different bytes from
the oracle's.

**`synthio_sat16` is inline (cbbde1208; adafruit/circuitpython#11621).** Taken
as upstream has it in `src/synthio/__init__.h`. The rounding is the same
arithmetic written without a branch, so it cannot change a sample; the
`__ssat` form is for cores that define `__ARM_FEATURE_SAT`. `audiodsp_sat16`, the same
function for the shared C and the CPython extension, is unchanged.

**What moved.** Two stored digests: `streaming_component`'s streaming half
(`f3ffad8e…` to `33b27a2f…`) and `synthtools_acceptance` (`b6e3b06f…` to
`757d944a…`; `lead`, `bend` and `bass`). Both were re-captured from the new
oracle's output, and CPython, desktop MicroPython and the oracle agree on
them. Every other stored digest held: `midi_component`, `effects_component`,
`biquad_component`, `mixdown_knee`.

## Resynced with 11.0.0-alpha.1 (audiodsp#220)

Every audio fix CircuitPython made between 10.3.0 and 11.0.0-alpha.1 is in
this port now, and so is its one audio addition, `PitchShift.freeze`, which
followed in audiodsp#222. The finalisers upstream gave every audio object
are left out on purpose. What was taken, module by module, and what was left
out is in [upstream-sync.md](upstream-sync.md#the-last-resync-1100-alpha1-audiodsp220).
out is in [upstream-sync.md](upstream-sync.md#the-resync-before-1100-alpha1-audiodsp220).

It retires five differences recorded below, because upstream now does the
same: the stream I/O in `WaveFile` and `synthio.from_file` (upstream switched
Expand All @@ -241,6 +292,9 @@ in audiocomponents' parity sequences on CPython and on MicroPython.

## The oracle is CircuitPython 11.0.0-alpha.1 (audiodsp#201)

> The oracle has moved on to 11.0.0-alpha.2, see
> [above](#resynced-with-1100-alpha2). This is how it got to alpha.1.

The oracle moved from 10.3.0 to 11.0.0-alpha.1 (520805e12) on 2026-10-08,
with the binary at `bin/circuitpython-oracle-11.0.0-alpha.1`
(`CIRCUITPYTHON_ORACLE`, `tests/test_voice_ceiling_consistency.py`). It moved
Expand Down Expand Up @@ -1950,17 +2004,21 @@ peaking-EQ fix below let it drop the Mixer entirely.)

## A voice at level 1.0 is a wire (audiodsp#95)

Upstream maps a voice level of 1.0 to Q15 **32768**:
> **Upstream since 11.0.0-alpha.2** (11a329885, fc57c0f36), so no longer a
> deviation from the oracle. The unity branch described below is gone from
> this port; what is left is the record of what was wrong.

Upstream mapped a voice level of 1.0 to Q15 **32768**:

```c
uint16_t level = (uint16_t)(synthio_block_slot_get_limited(
&voice->level, 0.0, 1.0) * (1 << 15));
```

and then scales by `level / 32767.0f` in `mult16signed`. So "unity" is really
**1.0000305**, and every sample whose magnitude reaches 32736 comes out one
LSB larger — away from zero — with 32767 clamping. A voice at level 1.0 is
not a wire, and never was.
and then scaled by `level / 32767.0f` in `mult16signed`. So "unity" was really
**1.0000305**, and every sample whose magnitude reached 32736 came out one
LSB larger, away from zero, with 32767 clamping. A voice at level 1.0 was
not a wire.

Measured here on the full int16 range, once per value, through a one-voice
Mixer with the level gate opened on a leading block of silence:
Expand All @@ -1970,38 +2028,15 @@ channels=1: 63 samples differ, |value| >= 32736, deltas [-1, 1], channel index [
channels=2: 63 samples differ, |value| >= 32736, deltas [-1, 1], channel index [1]
```

Stereo differs on the right channel only, because a voice at panning 0 gets
`32767 - panning` on the left and a flat `32768` on the right: the left
scale's quotient is 32767/32767, which is exactly one, and only the right
carries the lifted multiplier. Mono takes the right-hand path on both.

**The deviation.** A multiplier of exactly `1 << 15` means unity and passes
the sample through, in `mult16signed` (`src/audiomixer/Mixer.c`) and in
`_mod_mul` (`src/cpython/audiomixer.py`). Every other level is upstream's
arithmetic bit for bit — the single-precision quotient and product that
audiodsp#84 pinned, which `tests/test_cpython_audiomixer_level.py` still
enumerates over Q15 0..32767.

**Why deviate rather than route around it.** Answer (1) on the issue — a
class claiming a wire at Mix 0 routes through no mixer — is what the shipped
effect classes took, and five of them were rewired for it. That fixed the
classes; it did not fix `audiomixer`, and the next class to put a full-scale
signal through a level-1.0 voice would have found it again. The scale is one
number in one kernel and the fix is one compare, so the module now means what
it says.

**What moved.** Nothing any committed gate covers: `verify_acceptance`,
`verify_effects`, `verify_streaming`, `verify_biquad` and
`verify_mixdown_knee` all match their recorded hashes unchanged, because none
of their material carries a full-scale sample through a level-1.0 voice. What
does move is any render that does — by one LSB, on samples at |value| >= 32736
only, and toward the sample's own value rather than away from it.

**Not applied to the CircuitPython target,** per the standing rule: the oracle
keeps upstream's arithmetic, so this is one of the enumerated places where our
targets and the oracle disagree by design. A parity script that renders
full-scale material through a level-1.0 voice will see it, and should expect
it.
Stereo differed on the right channel only, because a voice at panning 0 got
`32767 - panning` on the left and a flat `32768` on the right.

This port answered with a branch: a multiplier of exactly `1 << 15` passed
the sample through, in `mult16signed` and in `_mod_mul`. Upstream's answer, in
11.0.0-alpha.2, is to change the arithmetic: a sample is `(x * level) >> 15`,
and a centred pan is `32768 - panning`, so level 1.0 at pan 0 is 32768 on both
channels and the product is the sample itself. See
[Resynced with 11.0.0-alpha.2](#resynced-with-1100-alpha2).

## Peaking EQ computed `b2` with the wrong sign (effects-extension tier)

Expand Down
35 changes: 32 additions & 3 deletions docs/upstream-sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,38 @@ to turn up.
7. Update the record below: what was taken, what was held back, and any
difference that a fix retired or added.

## The last resync: 11.0.0-alpha.1 (audiodsp#220)
## The last resync: 11.0.0-alpha.2

The pin moved from 11.0.0-alpha.1 to 11.0.0-alpha.2 (d53b2277c). Between the
two, upstream touched the ported modules in three commits, all in `audiomixer`
and `synthio_sat16`; nothing else in the eight modules changed, and `lib/mp3`
is the same commit (aac02afd9f) as at the pin.

- **audiomixer** (11a329885, fc57c0f36). A voice is scaled in integers:
`(ai * mul) >> 15`, where it was `ai * (mul / 32767.0f)` truncated toward
zero, and a centred pan is `32768 - panning`. Taken in upstream's words in
`src/audiomixer/Mixer.c`, in the CPython extension's scaling kernel and in
`src/cpython/audiomixer.py`. Every level below 1.0 moves by up to one LSB,
and a small negative sample now rounds down (-1 at level 0.5 stays -1). The
ARM `smulwb` path and the `__ARM_FEATURE_DSP` guards are not here, as before:
this port's `mult16signed` is the portable C, which upstream says gives the
same bytes.
- **synthio** (cbbde1208). `synthio_sat16` is inline in `src/synthio/__init__.h`
as upstream has it, with the `__ssat` form on cores that define
`__ARM_FEATURE_SAT`. It cannot change a sample.

`mixer_q15_level_probe.py` covers the mixer in `verify_dsp.py`, where
CircuitPython, MicroPython and CPython print the same bytes, and a build from
before the change prints different ones. Two stored digests moved, both
re-captured from the alpha.2 oracle's output: the streaming half of
`streaming_component`, and `synthtools_acceptance`. The rest held, and so did
audiocomponents' instrument parity sequences on CPython and on MicroPython.

**A difference retired:** a voice at level 1.0 is a wire (audiodsp#95).
Upstream's arithmetic makes it so, and the unity branch in `mult16signed` is
deleted. It also retires the left-below-right pan asymmetry at pan 0.

## The resync before: 11.0.0-alpha.1 (audiodsp#220)

The pin moved from 10.3.0 to 11.0.0-alpha.1 with audiodsp#201, which took
6dddbda87 (buffer lengths and silence fills) and nothing else. audiodsp#220
Expand Down Expand Up @@ -198,8 +229,6 @@ reason, and a resync should not take them.
- **audiomixer**: a reset keeps every voice playing where it was instead of
stopping them
([why](upstream-diff.md#resetting-a-mixer-silenced-it-permanently-audioeffects-tier)).
A voice at level 1.0 is a wire
([why](upstream-diff.md#a-voice-at-level-10-is-a-wire-audiodsp95)).
A looping sample under one word is refused
([why](upstream-diff.md#audiomixer-a-looping-sample-under-one-word-is-refused-not-spun-on-audiodsp85)).
Level and panning are always block inputs, and the ARM CMSIS intrinsics are
Expand Down
19 changes: 3 additions & 16 deletions src/audiomixer/Mixer.c
Original file line number Diff line number Diff line change
Expand Up @@ -264,24 +264,11 @@ static inline uint32_t add16signed(uint32_t a, uint32_t b) {
return result;
}

// Deviation from upstream: a multiplier of 1<<15 is unity, and passes the
// sample through. Upstream maps a voice level of 1.0 to Q15 32768 and then
// scales by `level / 32767.0f`, so unity is really 1.0000305 and every
// sample at |value| >= 32736 comes out one LSB larger. See
// docs/upstream-diff.md, "A voice at level 1.0 is a wire". Only the unity
// multiplier takes this branch; every other level is upstream's arithmetic,
// bit for bit.
static inline uint32_t mult16signed(uint32_t val, int32_t lomul, int32_t himul) {
uint32_t result = 0;
for (int8_t i = 0; i < 2; i++) {
int32_t mul = i ? himul : lomul;
int16_t ai = (val >> (sizeof(uint16_t) * 8 * i));
if (mul == (1 << 15)) {
result |= (((uint32_t)ai) & 0xffff) << (sizeof(int16_t) * 8 * i);
continue;
}
float mod_mul = (float)mul / (float)((1 << 15) - 1);
int32_t intermediate = (int32_t)(ai * mod_mul);
int32_t intermediate = (ai * (i ? himul : lomul)) >> 15;
if (intermediate > SHRT_MAX) {
intermediate = SHRT_MAX;
} else if (intermediate < SHRT_MIN) {
Expand Down Expand Up @@ -430,9 +417,9 @@ static void mix_down_one_voice(audiomixer_mixer_obj_t *self,
// panning > 0 attenuates the LEFT channel. CircuitPython 10.3.0
// flipped this and matched synthio.Note to it.
if (panning >= 0) {
left_panning_scaled = 32767 - panning;
left_panning_scaled = 32768 - panning;
} else {
right_panning_scaled = 32767 + panning;
right_panning_scaled = 32768 + panning;
}
}

Expand Down
26 changes: 9 additions & 17 deletions src/cpython/_audiodsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -4538,17 +4538,9 @@ static PyObject *audiodsp_mix_s16(PyObject *module, PyObject *args) {
return result;
}

/* One voice's level, as the single-precision multiplier the native mixer
* uses: `(float)level / (float)32767`, with Q15 32768 (a level of 1.0) as an
* exact 1.0. `_mod_mul` in audiomixer.py is the same arithmetic. */
static float mixer_mod_mul(long level) {
if (level == 32768) return 1.0f;
return (float)level / (float)32767;
}

/* Scale a block of interleaved int16 pairs by a voice's per-channel level, the
* loop `Mixer._get_buffer` runs in Python for any voice that is not at
* unity: product in float32, truncation toward zero, clamp to int16. A level
* unity: `(sample * level) >> 15`, clamp to int16. A level
* move (`pending`) lands at the first zero crossing, or is forced at each
* index in `forced` (ascending sample indices, where a source stretch ends),
* exactly as the Python loop does. */
Expand Down Expand Up @@ -4581,8 +4573,8 @@ static PyObject *audiodsp_scale_s16_levels(PyObject *module, PyObject *args) {
int16_t *out = (int16_t *)PyBytes_AS_STRING(result);
const Py_ssize_t count = data.len / 2;
long active[2] = {active_lo, active_hi};
float mul_lo = mixer_mod_mul(active[0]);
float mul_hi = mixer_mod_mul(active[1]);
long mul_lo = active[0];
long mul_hi = active[1];
int32_t last_lo = 0, last_hi = 0;
Py_ssize_t next_forced = 0;
for (Py_ssize_t index = 0; index < count; index += 2) {
Expand All @@ -4599,8 +4591,8 @@ static PyObject *audiodsp_scale_s16_levels(PyObject *module, PyObject *args) {
if (at == index && index) {
active[0] = pending_lo;
active[1] = pending_hi;
mul_lo = mixer_mod_mul(active[0]);
mul_hi = mixer_mod_mul(active[1]);
mul_lo = active[0];
mul_hi = active[1];
last_lo = last_hi = 0;
}
break;
Expand All @@ -4615,18 +4607,18 @@ static PyObject *audiodsp_scale_s16_levels(PyObject *module, PyObject *args) {
&& ((last_lo < 0) != (lo < 0) || (last_hi < 0) != (hi < 0)))) {
active[0] = pending_lo;
active[1] = pending_hi;
mul_lo = mixer_mod_mul(active[0]);
mul_hi = mixer_mod_mul(active[1]);
mul_lo = active[0];
mul_hi = active[1];
} else {
last_lo = lo;
last_hi = hi;
}
}
int32_t scaled = (int32_t)((float)lo * mul_lo);
int32_t scaled = (int32_t)((lo * mul_lo) >> 15);
out[index] = (int16_t)(scaled > INT16_MAX ? INT16_MAX
: scaled < INT16_MIN ? INT16_MIN : scaled);
if (index + 1 < count) {
scaled = (int32_t)((float)hi * mul_hi);
scaled = (int32_t)((hi * mul_hi) >> 15);
out[index + 1] = (int16_t)(scaled > INT16_MAX ? INT16_MAX
: scaled < INT16_MIN ? INT16_MIN : scaled);
}
Expand Down
Loading
Loading