Skip to content

rational: Q15 mixed ratios going down at a power-of-two row gain - #65

Merged
tap merged 1 commit into
mainfrom
claude/sample-rate-expansion-strategies-ezqzu6
Oct 7, 2026
Merged

tap merged 1 commit into
mainfrom
claude/sample-rate-expansion-strategies-ezqzu6

Conversation

@tap

@tap tap commented Oct 6, 2026

Copy link
Copy Markdown
Owner

What this changes

  • Scaled rows. In Q15, the rows of the mixed ratios going down (2/3, 3/4, 3/8) now sum to G × unity instead of unity.
    • G is the largest power of two with G·L/M < 2: 2 for 2/3 and 3/4, 4 for 3/8. That keeps every tap inside Q1.14.
    • finalize_divided shifts G back out exactly in the single rounding. basic_stage::k_table_gain and finalize_output() carry it, the same mechanism as the Q15 decimators.
  • Snapping. The scaled rows are snapped to 2^-34 (2^-20 of a Q1.14 LSB) before quantizing; see the notes for why.
  • Tests. Twelve Q15 table pins are re-pinned (hash, MACs, stopband, RMS), and the unity-sum checks now account for the gain.
  • DspTap pin. It moves to DspTap main, which carries decimate: Q15 stores M·h, the M divided out in finalize_divided DspTap#55: decimate.h's Q15 tables at M·h. This engine's tests use decimate.h only in float.
  • Docs: plan v0.11, the section 6 table and limit, the stage.h contract, README and CLAUDE.md.

Why

You asked me to model the Q15 mixed-down limit and build it only if it reached the 70 dB tier. Rows normalized to unity put the centre tap near L/M (0.67 for 2/3, 0.38 for 3/8). That leaves part of the Q1.14 range unused: the same shrinkage as the decimators', at a smaller factor.

Attained Q15 stopband, measured by the test on the 16,384-point grid:

Stage super_economy economy balanced transparent
2/3 −69.1 → −71.1 −69.7 → −70.5 −71.1 → −72.1 −73.9 → −78.2
3/4 −70.7 → −71.6 −72.8 → −70.0 −68.2 → −72.4 −75.8 → −78.3
3/8 −69.2 → −71.5 −71.2 → −71.8 −69.4 → −71.7 −69.6 → −83.1
  • Every Q15 stage now reaches the 70 dB tier at every 70 dB profile.
  • One cell moves the wrong way: 3/4 at economy goes from −72.8 to −70.02 dB. It stays in the tier, but with no margin.
  • Q15 RMS error against double is 4–10 dB lower.
  • Q31, float and double are unchanged.

Verification

  • Host: 286/286 tests pass with clang 18 -Werror. gcc -Werror passes the rational label (113/113). clang-tidy and clang-format are clean.
  • Embedded: the rational battery passes 7/7 on M33 and on M55 under QEMU, with the tables matching the host bit for bit.
  • Ratchet: every engine's counts stay within ±3 % on M33, M55 and Hexagon, so no baselines move. ratio23_q15_eco changes by +0.01 % on M33, +1.9 % on M55 and 0.00 % on Hexagon.

Notes for the reviewer

  • Why the snap is needed. Without it, the M33 and M55 legs pinned a different 2/3 super_economy table than the host.
    • The cause: quantize_row_preserving_sum gives each leftover LSB to the tap with the largest remainder. A symmetric design has pairs of exactly equal remainders, and the design's last bits differ between glibc and newlib, so the tie broke differently.
    • Snapping makes those ties exact everywhere, and they break by index. Two host pins moved because of it: 2/3 transparent and 3/4 economy.
    • The same fragility exists for any table whose residual lands on a near-tie. Today's other tables happen to agree across targets. The general fix would be a deterministic near-tie rule in DspTap's quantize.h, which would move pins family-wide, so I've left it as a finding for you to decide.
  • What comes next: the Q15/Q31 C ABI is ready, and I'll open it as the next PR once this one merges.

🤖 Generated with Claude Code

https://claude.ai/code/session_015VR1VC4SDGxHZQQsQvPBaA


Generated by Claude Code

Normalized to unity, a mixed ratio going down's L-phase rows put their
centre tap near L / M (2/3: 0.67, 3/8: 0.38), leaving part of the Q1.14
range unused, the decimator's shrinkage at a smaller factor. Their Q15
rows now sum to G x unity instead, where G is the largest power of two
with G L / M < 2: 2 for 2/3 and 3/4, 4 for 3/8. finalize_divided shifts
G back out exactly in the single rounding (basic_stage::k_table_gain,
finalize_output).

Every Q15 stage now attains the 70 dB tier at every 70 dB profile. The
mixed ratios going down reach -70.0 to -72.4 dB, from -68.2 to -69.7
(3/4 at balanced: -68.2 to -72.4). One cell moves against the trend:
3/4 at economy goes from -72.8 to -70.0 dB, its tightest, still the
tier. Q15 RMS from double is 4 to 10 dB lower. Q31, float and
double are unchanged.

The scaled rows are snapped to 2^-34 (2^-20 of a Q1.14 LSB) before
quantizing. Otherwise the design's last-bit differences between libms
(glibc on the host, newlib on the Cortex-M legs, measured) move a
row-sum residual between the equal remainders of a symmetric pair, so
one table differed between host and target. Ties are now exact and
broken by index everywhere.

Twelve Q15 table pins are re-pinned (hash, MACs, stopband, RMS). The
DspTap pin moves to its main, which carries tap/DspTap#55 (decimate.h's
Q15 tables at M h; this engine's tests use decimate.h only in float).
Every ratchet count stays within +/-3 %: ratio23_q15_eco moves +0.01 %
on M33, +1.9 % on M55 and 0.00 % on Hexagon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VR1VC4SDGxHZQQsQvPBaA
@tap
tap merged commit ab8fea3 into main Oct 7, 2026
28 checks passed
@tap
tap deleted the claude/sample-rate-expansion-strategies-ezqzu6 branch October 7, 2026 22:21
tap pushed a commit to tap/DspTap that referenced this pull request Oct 8, 2026
The window and the sinc are even, but the per-branch normalization is
not: branch j and its mirror branch (-2 - j) mod L are summed in
opposite orders, so their gains can differ in the last bit and leave
the two halves of the design an ulp apart. Measured: equal under glibc
and unequal under newlib for the third-band m = 8 design, which is how
the 2/3 super_economy Q15 table of SampleRateTap's rational engine came
to differ between the host and its Cortex-M legs (tap/SampleRateTap#65):
quantize_row_preserving_sum gives a leftover LSB to the largest
remainder, a mirrored pair that lands in one row ties for it, and an
ulp decided which member won.

design_nyquist now copies the second half from the first after the
normalization, so h[i] == h[N - 1 - i] bit for bit (the contract states
it; each branch still sums to 1 within double rounding). quantize.h
states its tie rule, the lowest index, and why a mirrored design makes
it the same on every host. Nyquist.IsSymmetricBitForBit covers every
band of the vocabulary at the tiers' lengths; Quantize.TiesStepThe
LowestIndex pins the rule. Anchors for the family book on the designer,
the search, the sync_stage concept and chain<>::flush; comment-only.

The host batteries (clang, gcc) are unchanged: no pinned table moves
on glibc, where the mirror gains happened to agree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VR1VC4SDGxHZQQsQvPBaA
tap pushed a commit that referenced this pull request Oct 8, 2026
…oved

DspTap pinned at tap/DspTap#56: design_nyquist copies its second half
from the first after the per-branch normalization, whose mirror
branches were summed in opposite orders and could carry gains an ulp
apart (equal under glibc, unequal under newlib for the third-band
m = 8 design). That was the cause of the 2/3 super_economy Q15 table
differing between the host and the Cortex-M legs in #65; the 2^-34
snap that PR added to stage.h is removed, since the ties it made exact
are exact by construction now.

Every Q15 pin holds. Two Q31 pins move on the host, ↓6 balanced and
3/4 economy: one LSB moves between the members of a mirror pair
(branch 0 tap 8 to branch 4 tap 19; row 1 tap 11 to tap 17), the row
sums unchanged, the two designs where glibc's mirror gains differed.
The tables are the same on the host, the M33 and the M55.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VR1VC4SDGxHZQQsQvPBaA
tap pushed a commit to tap/DspTap that referenced this pull request Oct 8, 2026
The window and the sinc are even, but the per-branch normalization is
not: branch j and its mirror branch (-2 - j) mod L are summed in
opposite orders, so their gains can differ in the last bit and leave
the two halves of the design an ulp apart. Measured: equal under glibc
and unequal under newlib for the third-band m = 8 design, which is how
the 2/3 super_economy Q15 table of SampleRateTap's rational engine came
to differ between the host and its Cortex-M legs (tap/SampleRateTap#65):
quantize_row_preserving_sum gives a leftover LSB to the largest
remainder, a mirrored pair that lands in one row ties for it, and an
ulp decided which member won.

design_nyquist now copies the second half from the first after the
normalization, so h[i] == h[N - 1 - i] bit for bit (the contract states
it; each branch still sums to 1 within double rounding). quantize.h
states its tie rule, the lowest index, and why a mirrored design makes
it the same on every host. Nyquist.IsSymmetricBitForBit covers every
band of the vocabulary at the tiers' lengths; Quantize.TiesStepThe
LowestIndex pins the rule. Anchors for the family book on the designer,
the search, the sync_stage concept and chain<>::flush; comment-only.

The host batteries (clang, gcc) are unchanged: no pinned table moves
on glibc, where the mirror gains happened to agree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VR1VC4SDGxHZQQsQvPBaA
tap pushed a commit that referenced this pull request Oct 8, 2026
…oved

DspTap pinned at tap/DspTap#56: design_nyquist copies its second half
from the first after the per-branch normalization, whose mirror
branches were summed in opposite orders and could carry gains an ulp
apart (equal under glibc, unequal under newlib for the third-band
m = 8 design). That was the cause of the 2/3 super_economy Q15 table
differing between the host and the Cortex-M legs in #65; the 2^-34
snap that PR added to stage.h is removed, since the ties it made exact
are exact by construction now.

Every Q15 pin holds. Two Q31 pins move on the host, ↓6 balanced and
3/4 economy: one LSB moves between the members of a mirror pair
(branch 0 tap 8 to branch 4 tap 19; row 1 tap 11 to tap 17), the row
sums unchanged, the two designs where glibc's mirror gains differed.
The tables are the same on the host, the M33 and the M55.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VR1VC4SDGxHZQQsQvPBaA
tap pushed a commit that referenced this pull request Oct 8, 2026
…oved

DspTap pinned at tap/DspTap#56: design_nyquist copies its second half
from the first after the per-branch normalization, whose mirror
branches were summed in opposite orders and could carry gains an ulp
apart (equal under glibc, unequal under newlib for the third-band
m = 8 design). That was the cause of the 2/3 super_economy Q15 table
differing between the host and the Cortex-M legs in #65; the 2^-34
snap that PR added to stage.h is removed, since the ties it made exact
are exact by construction now.

Every Q15 pin holds. Two Q31 pins move on the host, ↓6 balanced and
3/4 economy: one LSB moves between the members of a mirror pair
(branch 0 tap 8 to branch 4 tap 19; row 1 tap 11 to tap 17), the row
sums unchanged, the two designs where glibc's mirror gains differed.
The tables are the same on the host, the M33 and the M55.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015VR1VC4SDGxHZQQsQvPBaA
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.

2 participants