|
6 | 6 | was asked for -- does oversampling actually lower the alias floor, does the |
7 | 7 | hysteresis option actually enclose an area and does that area grow with |
8 | 8 | drive, is the hold's ratio exact over a distance where a fixed-point one has |
9 | | -visibly walked -- each with the control that would go red if the mechanism |
10 | | -were absent. |
| 9 | +visibly walked, does a hard-clipping curve's own headroom knee sit where |
| 10 | +`CLIP_HEADROOM` says it does -- each with the control that would go red if |
| 11 | +the mechanism were absent. |
11 | 12 | """ |
12 | 13 |
|
13 | 14 | import math |
@@ -40,9 +41,23 @@ def cubic_curve(): |
40 | 41 | CUBIC = cubic_curve() |
41 | 42 |
|
42 | 43 |
|
43 | | -def render(values, **options): |
| 44 | +def hard_clip_curve(threshold=0.10, points=2048): |
| 45 | + """A straight hard clip: linear to the rails over +-`threshold` of the |
| 46 | + input span, flat beyond it -- unlike CUBIC, which never has a flat top |
| 47 | + at all. Driven hard, this is the shape audioif#99 is about.""" |
| 48 | + last = points - 1 |
| 49 | + return array("h", [ |
| 50 | + clamp15(int(round(max(-1.0, min(1.0, ( |
| 51 | + -1.0 + 2.0 * index / last) / threshold)) * 32767))) |
| 52 | + for index in range(points)]) |
| 53 | + |
| 54 | + |
| 55 | +HARD_CLIP = hard_clip_curve() |
| 56 | + |
| 57 | + |
| 58 | +def render(values, curve=CUBIC, **options): |
44 | 59 | """Push one array of interleaved frames through a node and take it back.""" |
45 | | - node = audioshaper.Waveshaper(sample_rate=SAMPLE_RATE, curve=CUBIC, |
| 60 | + node = audioshaper.Waveshaper(sample_rate=SAMPLE_RATE, curve=curve, |
46 | 61 | **options) |
47 | 62 | node.play(audiocore.RawSample( |
48 | 63 | values, sample_rate=SAMPLE_RATE, |
@@ -210,6 +225,83 @@ def test_the_floor_falls_as_the_factor_rises(self): |
210 | 225 | "x8 was worse than x4: %r" % (floors,)) |
211 | 226 |
|
212 | 227 |
|
| 228 | +class HeadroomTest(unittest.TestCase): |
| 229 | + """audioif#99: a hard-clipping curve driven hard rings past the rails |
| 230 | + once decimated, and `post_gain` re-clips that overshoot at the base |
| 231 | + rate -- after the oversampling is done, where no factor of it reaches. |
| 232 | +
|
| 233 | + Where this happens in the kernel (`src/shared/audioif_shaper.c`): |
| 234 | + `shape_sample` runs the curve at the oversampled rate and its own clamp |
| 235 | + (`curve_lookup`, clampf to +-1) bounds each of those samples, but the |
| 236 | + *decimated* one is not clamped there -- `halfband_down` is a low-pass, |
| 237 | + not a clip, so a hard edge through it can overshoot +-1. `post_gain` |
| 238 | + scales that decimated value (`audioif_shaper_process_s16`, |
| 239 | + `oversampled[0] * config->post_gain * 32768.0f`), and the only place |
| 240 | + this node ever clips to int16 is `to_s16`, two lines later, on |
| 241 | + `dry_gain * source + wet_gain * wet`. Everything from the curve to |
| 242 | + `post_gain` is `float` -- nothing here is int16 until that last line. |
| 243 | +
|
| 244 | + HARD_CLIP ramps to the rails over the inner 10% of its span and is flat |
| 245 | + beyond it; driven at 32000 (98% of full scale) it is pinned flat for |
| 246 | + most of every half-cycle, the edge the issue is about. Bare node, |
| 247 | + 1010 Hz, 48 kHz, oversample x4; the window is 4800 samples -- exactly |
| 248 | + 101 cycles of 1010 Hz, so the fundamental and every harmonic fall on |
| 249 | + their own bin with no window and no rounding. |
| 250 | +
|
| 251 | + Measured (`docs/upstream-diff.md`'s `audioshaper` section carries the |
| 252 | + full table): flat at -54.34 dB through `post_gain` 0.80, -41.24 at |
| 253 | + 0.90, -36.44 at 1.00 -- a 13.1 dB fall by 0.90 that the bars below ask |
| 254 | + 10 of, for margin. The control is oversample x1: no half-band, no |
| 255 | + ringing, and the floor reads -34.92 dB at every `post_gain` from 0.66 |
| 256 | + to 1.00 -- the same curve and the same drive, showing no knee at all, |
| 257 | + which is what proves the x4 knee is the decimator's and not the |
| 258 | + curve's. |
| 259 | + """ |
| 260 | + |
| 261 | + HZ = 1010 |
| 262 | + FRAMES = 4800 # 48000 / 1010 * 101 == 4800 exactly: bin 101, no rounding |
| 263 | + CYCLES = HZ * FRAMES // SAMPLE_RATE |
| 264 | + LEVEL = 32000 |
| 265 | + |
| 266 | + def _floor(self, post_gain, oversample=4): |
| 267 | + return alias_floor_db(oversample, cycles=self.CYCLES, |
| 268 | + frames=self.FRAMES, level=self.LEVEL, |
| 269 | + curve=HARD_CLIP, post_gain=post_gain) |
| 270 | + |
| 271 | + def test_the_floor_is_flat_below_the_knee(self): |
| 272 | + """0.66 and 0.74 read the same node to within 1 dB: on this table, |
| 273 | + the knee has not been reached yet at either.""" |
| 274 | + low = self._floor(0.66) |
| 275 | + at = self._floor(0.74) |
| 276 | + self.assertLess(abs(at - low), 1.0, |
| 277 | + "0.66 and 0.74 already differ: %.3f vs %.3f dB" |
| 278 | + % (low, at)) |
| 279 | + |
| 280 | + def test_the_floor_falls_past_the_knee(self): |
| 281 | + """0.90 is well past it. `CLIP_HEADROOM`'s ~0.74 ceiling has to be |
| 282 | + buying real room, not a fraction of a dB, or the constant is |
| 283 | + theatre -- measured here it is 13.1 dB; the bar asks 10, leaving |
| 284 | + margin rather than pinning the exact figure.""" |
| 285 | + at = self._floor(0.74) |
| 286 | + past = self._floor(0.90) |
| 287 | + self.assertLess(at, past - 10.0, |
| 288 | + "0.90 was not at least 10 dB worse than 0.74: " |
| 289 | + "%.3f vs %.3f dB" % (at, past)) |
| 290 | + |
| 291 | + def test_the_knee_is_the_decimators_not_the_curves(self): |
| 292 | + """Its control. At oversample x1 there is no half-band to ring, so |
| 293 | + the same curve and the same drive must show no knee at all -- if |
| 294 | + this one went red, the "knee" above would be the curve clipping on |
| 295 | + its own, not the decimator, and `CLIP_HEADROOM` would be the wrong |
| 296 | + fix.""" |
| 297 | + floors = [self._floor(post, oversample=1) |
| 298 | + for post in (0.66, 0.70, 0.74, 0.78, 0.80, 0.90, 1.00)] |
| 299 | + self.assertLess(max(floors) - min(floors), 0.5, |
| 300 | + "the un-oversampled curve already shows a knee, so " |
| 301 | + "x4's knee is not the decimator's doing: %r" |
| 302 | + % (floors,)) |
| 303 | + |
| 304 | + |
213 | 305 | class HysteresisTest(unittest.TestCase): |
214 | 306 | """The option encloses an area, the area grows with drive, and off is off. |
215 | 307 |
|
|
0 commit comments