Skip to content

audiodynamics.Dynamics DYN_TRANSIENT attack path: P4 ≠ S3 ≠ desktop on the same probe #66

Description

@bdbarnett

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions