Waveshaper: document the clipper's own headroom knee, with a trait test (#99) - #100
Merged
Merged
Conversation
…M (audioif#99)
A curve that reaches the rails and a post_gain above about 0.74 saturates
the Waveshaper's own output, after the decimator, where no oversampling
factor reaches it -- and the node's documentation said nothing about it.
Distortion's second fix round found this the hard way and carries its own
0.74 ceiling; every other drive class was exposed to the same knee with no
name for it.
Where it happens (src/shared/audioif_shaper.c): shape_sample runs the
curve at the oversampled rate and curve_lookup's own clamp (:213) bounds
each of those samples to +-1, but the decimated one is not clamped there.
halfband_down (:201-207) is a low-pass, not a clip, so a hard edge through
it can overshoot the rails on the way back down -- the band-limited
version of a full-scale clipped edge does not fit in int16. post_gain
scales that decimated (and possibly overshot) value (:288,
oversampled[0] * config->post_gain * 32768.0f), after the half-band, and
the only place this node ever clips to int16 is to_s16, two lines later
(:289-290). Everything from the curve to post_gain is float; nothing here
is int16 until that last line.
Measured on the CPython twin: a straight hard clip (int16 Q15, flat beyond
+-10% of input), a 1010 Hz sine driven to 98% of full scale, 48 kHz,
oversample x4, bare node, alias floor over an exactly periodic 4800-sample
window (bin 101, no window, no rounding):
post=0.66 -54.344 | 0.70 -54.343 | 0.74 -54.345 | 0.78 -54.343
post=0.80 -54.344 | 0.90 -41.238 | 1.00 -36.441
Flat to the hundredth of a dB through 0.80, then 13.1 dB worse by 0.90.
The control is oversample x1, same curve and same drive: -34.915 dB at
every one of those seven settings, so the knee is the decimator's doing,
not the curve's. Full table, x8 included, in docs/upstream-diff.md's new
"clipper's own headroom" section.
CLIP_HEADROOM = 0.74 names the ceiling, on the CPython twin only
(src/cpython/audioshaper.py), the same as GROUP_DELAY_SAMPLES beside it:
neither the MicroPython usermod's module globals (src/audioshaper/module.c)
nor the CircuitPython spike's (shared-bindings/audioshaper/__init__.c)
export anything past __version__/__revision__ and the two types, so there
is no second target for this figure to agree with -- adding it there too
would be a constant that is not identical on all three targets.
HeadroomTest in tests/test_cpython_audioshaper.py pins the knee: the floor
at 0.74 is within 1 dB of 0.66 (0.001 dB measured), and at least 10 dB
worse by 0.90 (13.1 dB measured) -- 10 rather than the 20 the issue's own
Distortion-table measurement showed, because that table is a different
curve in a different repository. An extensive parameter search here (clip
steepness, tanh steepness, drive level, pre_gain, oversample x2/x4/x8)
never found a bare-node reproduction that robustly cleared 20 dB between
exactly 0.74 and 0.90; 13.1 dB at x4 was the ceiling. Shown able to fail,
at the literal 20 dB bar:
FAIL test_the_floor_falls_past_the_knee
AssertionError: -54.34461222147358 not less than -61.23844766655389
: 0.90 was not at least 20 dB worse than 0.74: -54.345 vs -41.238 dB
A third test is the control: at oversample x1 the same curve and the same
drive show no knee at all (max-min under 0.5 dB across all seven post_gain
settings), which is what pins the mechanism as the decimator's rather than
the curve's own clipping.
Docstrings (the twin, docs/upstream-diff.md, README's module paragraph)
and CHANGELOG carry the rule for callers: keep post_gain * max(abs(curve))
at or below CLIP_HEADROOM for a curve that reaches the rails, and put the
rest of the wanted level on a mixer voice after this node. No DSP changed.
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.
Closes #99. Documentation and a trait test only, no DSP change: the decimated sample is scaled by post_gain after the half-band and clipped once to int16, so a curve that reaches the rails re-clips its own overshoot above about 0.74–0.80 of full scale. Measured table on the twin in docs/upstream-diff.md (x4: flat to 0.80, −41.2 at 0.90, −36.4 at 1.00; x1 control −34.9 throughout). CLIP_HEADROOM = 0.74 on the CPython twin, documentation only, the same footing as GROUP_DELAY_SAMPLES. 37 audioshaper traits OK; the new test fails at a 20 dB bar (quoted in the commit).