Found by the effects program's TransientShaper G5 hunt (audiocomponents effects/p2b-integration, tools tools/phase2_probes/transientshaper_digest_bisect.py and transientshaper_digest_bisect2.py), 2026-09-07, on firmware that reports the same build string on both boards. audioif is pinned at 2f6cbc3 and was not edited.
This is not audioif#55. That issue is boards agree, desktop differs on the baseline DYN_COMPRESS palette row. Here the two boards disagree with each other, and only on the transient attack (positive envelope-difference) path.
Boards
|
ESP32-P4 (COM4) |
ESP32-S3 (COM49) |
os.uname().version |
v1.28.0-dirty on 2026-09-07 |
v1.28.0-dirty on 2026-09-07 |
_build |
ESP32_GENERIC_P4-C6_WIFI |
ESP32_GENERIC_S3-SPIRAM_OCT |
Symptom
tools/measure_effect_cost.py target rebuilt:TransientShaper@1, 128 blocks of the integer probe, 256-frame stereo at 48 kHz:
mpftp run -d COM4 /tmp/ts_repro.py --follow
ROW rebuilt:TransientShaper@1 881.0 4.70 1.135 0.297 0.838 1776 0754b902fe5025a1
mpftp run -d COM49 /tmp/ts_repro.py --follow
ROW rebuilt:TransientShaper@1 441.2 2.35 2.267 0.482 1.784 1808 a3942224c339ede3
PYTHONPATH=lib .venv/bin/python -c "import sys; sys.path.insert(0,'tools'); import measure_effect_cost as m; m.main('rebuilt:TransientShaper@1')"
ROW rebuilt:TransientShaper@1 … a7f7c4de0042a7cd
A second construction of the same target on each board reproduced the same digest. The bare probe is 4169efd90ecf44dd on all three.
Minimal reproduction (class not involved)
Same probe and digest window as the cost runner. One audiodynamics.Dynamics at the node's defaults (DYN_TRANSIENT, peak detector, transient_dual=False) with only attack_gain_db set:
import measure_effect_cost as m
import audiodynamics
def build(probe):
node = audiodynamics.Dynamics(
audiodynamics.DYN_TRANSIENT,
sample_rate=m.SAMPLE_RATE,
channel_count=m.CHANNELS)
node.set(attack_gain_db=9.094488188976378) # Snap Attack; f32 06831141 on every host
node.play(probe.output)
return node, (), (node,)
print(m.run(build, True)["digest"])
| Host |
digest (nodual+peak+att) |
| desktop CPython |
64ef8393dbee988a |
| ESP32-P4 |
557c6bc78f3aa8c4 |
| ESP32-S3 |
856f86d55a882edd |
The class's own constructor options (transient_dual=True, detector="rms", rms_ms=3.0, sustain_fast_attack_ms=0.0) plus the same Attack give a different three-way split (988ebb97d751190e / 3082b8dcfd77385a / 3c23187048f0c180) that is byte-identical to transient_dual=False with those RMS options. Dual is not the splitter.
What agrees (so this is not "float differs between architectures" as a blanket)
Same tool, same boards, same session:
- Bare ctor (gains still 0):
4169efd90ecf44dd on all three (a wire).
DYN_EXPAND + RMS + a threshold/ratio: 85887b7b83768fe4 on all three.
DYN_TRANSIENT, RMS, transient_dual=False, sustain_gain_db=12 (the negative-difference / sustain path): 6ab77a94abff440e on all three.
- First eight 256-frame blocks of the attack-only render: identical on P4, S3 and desktop. S3 first differs at block 8; P4 and desktop still match through block 15 and diverge later in the 128-block window.
- The five Snap macro values as f32 are identical on P4 and S3. State is zeroed (
audioif_dynamics_state_init / clear_extras). Buffer length is 1024 on every pull. No NaN/clip difference in the first block. A second run on each board matches the first.
The C that moves is audioif_dynamics.c:603-679: one-pole envelopes, audioif_dynamics_gain_to_db (logf) and audioif_dynamics_db_to_gain (expf) on the positive envelope difference, then a multiply by Attack. Coefficients are rebuilt every process via expf in audioif_dynamics_ms_to_coef. RISC-V P4 vs Xtensa S3 libm / FMA contraction is the remaining mechanism; it was not isolated below that.
Consequence
TransientShaper is one Dynamics and a set of node.set calls. The class is exonerated. A per-architecture accepted baseline for DYN_TRANSIENT attack, the same shape as #55 asks for the compress row, would close the board half of the class gate.
Found by the effects program's TransientShaper G5 hunt (audiocomponents
effects/p2b-integration, toolstools/phase2_probes/transientshaper_digest_bisect.pyandtransientshaper_digest_bisect2.py), 2026-09-07, on firmware that reports the same build string on both boards. audioif is pinned at2f6cbc3and was not edited.This is not audioif#55. That issue is boards agree, desktop differs on the baseline
DYN_COMPRESSpalette row. Here the two boards disagree with each other, and only on the transient attack (positive envelope-difference) path.Boards
os.uname().versionv1.28.0-dirty on 2026-09-07v1.28.0-dirty on 2026-09-07_buildESP32_GENERIC_P4-C6_WIFIESP32_GENERIC_S3-SPIRAM_OCTSymptom
tools/measure_effect_cost.pytargetrebuilt:TransientShaper@1, 128 blocks of the integer probe, 256-frame stereo at 48 kHz:A second construction of the same target on each board reproduced the same digest. The bare probe is
4169efd90ecf44ddon all three.Minimal reproduction (class not involved)
Same probe and digest window as the cost runner. One
audiodynamics.Dynamicsat the node's defaults (DYN_TRANSIENT, peak detector,transient_dual=False) with onlyattack_gain_dbset:nodual+peak+att)64ef8393dbee988a557c6bc78f3aa8c4856f86d55a882eddThe class's own constructor options (
transient_dual=True,detector="rms",rms_ms=3.0,sustain_fast_attack_ms=0.0) plus the same Attack give a different three-way split (988ebb97d751190e/3082b8dcfd77385a/3c23187048f0c180) that is byte-identical totransient_dual=Falsewith those RMS options. Dual is not the splitter.What agrees (so this is not "float differs between architectures" as a blanket)
Same tool, same boards, same session:
4169efd90ecf44ddon all three (a wire).DYN_EXPAND+ RMS + a threshold/ratio:85887b7b83768fe4on all three.DYN_TRANSIENT, RMS,transient_dual=False,sustain_gain_db=12(the negative-difference / sustain path):6ab77a94abff440eon all three.audioif_dynamics_state_init/clear_extras). Buffer length is 1024 on every pull. No NaN/clip difference in the first block. A second run on each board matches the first.The C that moves is
audioif_dynamics.c:603-679: one-pole envelopes,audioif_dynamics_gain_to_db(logf) andaudioif_dynamics_db_to_gain(expf) on the positive envelope difference, then a multiply by Attack. Coefficients are rebuilt everyprocessviaexpfinaudioif_dynamics_ms_to_coef. RISC-V P4 vs Xtensa S3 libm / FMA contraction is the remaining mechanism; it was not isolated below that.Consequence
TransientShaperis oneDynamicsand a set ofnode.setcalls. The class is exonerated. A per-architecture accepted baseline forDYN_TRANSIENTattack, the same shape as #55 asks for the compress row, would close the board half of the class gate.