Conversation
The mix, decay and level an effect works with are read once per block from their BlockInput and do not change for the rest of it, but four of the audio callbacks were applying them to every sample in floating point. On a part with no floating point unit each of those is an out-of-line call into a soft-float routine, inside a callback that has to keep up with the sample rate. Each parameter is now converted to Q15 where it is already being read, and the per-sample arithmetic is an integer multiply and a shift. audiodelays/Flanger, audiofilters/Phaser and audiofreeverb/Freeverb already work this way; this brings audiomixer, audiodelays/Echo, audiodelays/MultiTapDelay, audiodelays/Chorus and audiofilters/Filter in line with them. The scale is 1<<15, which is exactly unity. audiomixer had been dividing by 32767 instead, and its level reaches that code as a float in 0 to 1 scaled by 1<<15, so unity became a gain slightly above one: a full-scale sample came back clipped, and the portable branch disagreed with the ARM one it exists to mirror. The effects that already use fixed point scale by 32767, which errs the other way and by less, and they are left alone.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Code written by Claude Code, guided and corrected by @peterbay.
The problem
An audio effect reads its
mix,decayorlevelonce per block from aBlockInput, and those values do not change again until the next block. Four of the callbacks were nonetheless applying them to every sample in floating point.On a part with a floating point unit that is a couple of instructions. On one without — RP2040, SAMD21, the RISC-V ESP32s — every one of them is an out-of-line call into a soft-float routine, and it happens inside a callback that has to keep up with the sample rate.
nm -uon the builtMixer.oshowsU __divsf3, which in an RP2040 firmware resolves to0x40002274: the ROM routine, a real unpack, divide and round.audiodelays/Flanger,audiofilters/Phaserandaudiofreeverb/Freeverbalready avoid this — they convert their parameters to fixed point once per block and keep the sample loop in integers. The five files this PR touches did not.What this does
Each block-constant parameter is converted to Q15 where it is already being read, and the per-sample arithmetic becomes an integer multiply and a shift. No behaviour is added or removed; the same values are applied to the same samples.
The scale is
1 << 15, which is exactly unity.audiomixerhad been dividing by32767, and itslevelreachesmult16signedas a float in0.0–1.0scaled by1 << 15, so unity became a gain of32768/32767— slightly above one. A sample of32766came back as32767, and full-scale samples clipped. The portable branch also disagreed with the ARM branch it exists to mirror. Both are now exact.The effects that already use fixed point are left alone. They scale by
32767, which errs the other way — a nominal unity mix is32767/32768— and by less. Changing them is a separate question from removing the soft-float, so this PR does not.What it costs and what it saves
Measured on a Waveshare RP2040-GEEK, two channels at 22050 Hz, 512-byte buffers, PWM output. A fixed Python loop is timed with the effect feeding the output and again with it stopped; the shortfall is the callback's share of the core. Three rounds each, spread within a round under half a point:
audiodelays/Echoaudiodelays/MultiTapDelayaudiofilters/Filteraudiodelays/Chorusaudiomixerwas measured separately, one 16-bit stereo voice at 44.1 kHz with I2S running throughout: 4.07 % of the core before and 1.81 % after, five rounds each, ranges 3.93–4.64 % and 1.25–2.31 %. Its compiled function went from 5394 to 4170 bytes, which matters on its own — it was the largest inshared-bindingsandshared-moduleagainst a 16 kB instruction cache.Soft-float calls remaining in each callback, counted in the built objects. What is left is the once-per-block setup, not per-sample work, which is why
MultiTapDelayloses only three calls and still runs three and a half times lighter:EchoFilterMultiTapDelayChorusFirmware size is 32 bytes smaller on that build. No new translatable strings.
Accuracy
Checked exhaustively on the host against the floating point it replaces.
For
audiomixer, over all 2 147 549 184(sample, multiplier)pairs the mixer can produce — multiplier0–32768, sample-32768–32767: every product stays insideint32, the new result never differs from the old by more than one LSB, and the old code lands on a rail three times as often as the new one.For the effects, over 65 601 536
(sample, coefficient)combinations: at most one LSB per term, two for a dry/wet sum, which is the difference between truncating toward zero and shifting — about −84 dB.One thing noticed and not verified
audiomixer's ARMv7EM branch doeslomul <<= 16on anint32_tthat reaches32768at level1.0, which shifts into the sign bit. Modellingsmulwbandssaton the host makes that branch return-samplethere. I have no M4 or M7 board with an audio setup to check it on, so I am only mentioning it — it is not addressed here and it is not what this PR changes.Testing
Waveshare RP2040-GEEK, built with
CIRCUITPY_AUDIODELAYS,CIRCUITPY_AUDIOFILTERSandCIRCUITPY_AUDIOFREEVERBenabled. Each effect is constructed, played throughaudiopwmio.PWMAudioOutinto two unused pins, and torn down before the next, with the timing loop described above run three times on each side of the switch.audiomixerwas tested on a Seeed XIAO nRF52840 Sense with an Adafruit Audio BFF over I2S.