Keep the Flanger's comb calm and smooth where its teeth crowd (#398)
game-maker / gate (push) Successful in 4m24s
game-maker / suite (push) Failing after 16m35s

High up the Flanger's comb flickered and looked pixelated. Each column
of its screen took six readings, and its teeth pass there faster than
frames are drawn, up to 0.4 of a tooth a frame at 15 kHz by default.
Each frame caught other teeth, and each column was drawn as a sharp
rectangle.

- flangerColumns reckons each column exactly. The response only hangs
  on the phase, and between two multiples of pi it runs one way. So a
  column holds its lowest and highest at its ends or on the peaks and
  notches inside it.
- Each frame is blurred over the delays of the last 1/20 s
  (RESPONSE_BLUR), about what an eye blends. Where teeth pass faster
  than that, they become a calm band from the deepest notch to the
  highest peak. Further down, where they can be followed, they stay
  sharp and move.
- The response is drawn as a band between two smooth lines through
  the columns, no longer as one rectangle per column.
- responseColumnsOf picks the Phaser's readings or the Flanger's exact
  columns. responseDb had no caller left and is gone.

Measured before choosing the blur, as the mean change of a column from
one frame to the next on the default Flanger (2-5 / 5-10 / 10-20 kHz):

  readings as before   5.6 / 10.6 / 14.3 dB
  exact, no blur       5.5 / 10.2 / 12.9 dB
  exact, 1/60 s        5.0 /  7.6 /  4.0 dB
  exact, 1/20 s        3.9 /  2.6 /  0.04 dB

Under 5 kHz most of what is left is the teeth really moving.

Tests:
  Unit modulation-view (stein2): 24 tests, 2 new. The exact columns
    hold what 4000 readings a column find there, and a fast swing
    leaves the same band over 16 kHz in three frames in a row.
  Counter-check: without the peaks and notches inside a column, both
    new tests fail.
  e2e 34 (atx): ALL PASSED, 142 checks.
  Gate (atx): CHECK EXIT 0, 291 files, 4469 tests.
  Pictures of the default Flanger and of one sweeping to 20 ms, near
    and far, two of each 40 ms apart: smooth edges, the band the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-10 21:15:53 +02:00
1 parent b6749cc43d
commit f9a953f3d3
3 files changed
+117 -23

No files matched your search

+19 -16
View File
@@ -24,8 +24,7 @@ import {
notchesOf,
RESPONSE_FLOOR,
RESPONSE_TOP,
responseColumns,
responseDb,
responseColumnsOf,
shareOfHz,
type SwingPicture,
swingValueAt,
@@ -482,23 +481,27 @@ class ResponseScope extends ModScope {
const values = this.values;
if (!picture || !values || this.w <= 0) return;
const r = this.response;
const columns = responseColumns(
(hz) => responseDb(this.device, values, this.reading, hz),
Math.max(1, Math.round(r.w)),
);
const columns = responseColumnsOf(this.device, values, this.at, Math.max(1, Math.round(r.w)));
this.notches = notchesOf(columns);
const bottom = r.y + r.h;
// Under the response a faint fill, the response itself as thin as a line where it is smooth
// and as tall as a column holds where a comb runs too fine for them.
columns.forEach((column, i) =>
c.rect(r.x + i, this.yOfDb(column.high), 1, bottom - this.yOfDb(column.high)),
);
c.fill({ color: this.look.trace, alpha: 0.12 });
columns.forEach((column, i) => {
const top = this.yOfDb(column.high);
c.rect(r.x + i, top - 0.75, 1, Math.max(1.5, this.yOfDb(column.low) - top + 0.75));
const step = r.w / columns.length;
const xs = columns.map((_, i) => r.x + (i + 0.5) * step);
const highs = columns.map((column) => this.yOfDb(column.high));
const lows = columns.map((column) => this.yOfDb(column.low));
// Under the response a faint fill. The response is a band from the highest to the lowest of
// each column, edged by two smooth lines: one line where it is smooth, filled where a comb runs
// too fine for the columns.
c.poly([r.x, bottom, ...xs.flatMap((x, i) => [x, highs[i]!]), r.x + r.w, bottom]).fill({
color: this.look.trace,
alpha: 0.12,
});
c.fill(this.look.trace);
const band = xs.flatMap((x, i) => [x, highs[i]!]);
for (let i = xs.length - 1; i >= 0; i--) band.push(xs[i]!, lows[i]!);
c.poly(band).fill(this.look.trace);
for (const edge of [highs, lows]) {
xs.forEach((x, i) => (i === 0 ? c.moveTo(x, edge[i]!) : c.lineTo(x, edge[i]!)));
c.stroke({ color: this.look.trace, width: 1.5, join: 'round' });
}
const hz = this.markHz(this.reading);
if (hz !== null && shareOfHz(hz) >= 0 && shareOfHz(hz) <= 1)
s.rect(this.xOfHz(hz) - 0.5, r.y, 1, r.h).fill({ color: 0xffffff, alpha: 0.3 });