Draw the Flanger's comb as a thin line with a fading trail (#398)
The comb was still drawn too heavy. Each frame showed the whole band
the comb had swept over the last 1/20 s, edged by two lines 1.5 wide,
and the faster the delay moved, the thicker that band got.
- flangerLine draws the comb where the light is as one line, 1 wide.
It runs through the ends of each column and the peaks and notches
in it, where they are. A comb finer than a column zigzags over it.
- Behind the line a faint trail shows what the comb swept over the
last 1/20 s, and over the last 1/40 s once more, so it fades with
age. A Flanger that stands still has none.
- The line fades out where its teeth move more than a quarter of a
tooth within the trail (clearAt), and is gone from half a tooth on.
There only the calm band of the trail is left, so nothing flickers.
- responseFrame gives the screen the line, how clear each point is,
the sharp columns for the notches and the trail. It replaces
responseColumnsOf, and RESPONSE_BLUR is now RESPONSE_TRAIL.
- The notches are counted on the sharp comb again, not on the band.
- The Phaser's response is drawn as the same thin line, without a
trail.
Tests:
Unit modulation-view (stein2): 26 tests, 2 new. The line holds the
comb's dB at each point and every notch. On a fast swing it fades
out over 8 kHz, stays clear under 1 kHz, and the trail is the same
band over 16 kHz in three frames. A still Flanger has no trail.
Counter-check: with clearAt always 1, the fade test fails.
e2e 34 (atx): ALL PASSED, 142 checks.
Gate (atx): CHECK EXIT 0, 291 files, 4471 tests.
Pictures of the default Flanger, a fast one (2 Hz, 5 ms) and one
sweeping to 20 ms, and of the Phaser: a thin line, the trail
behind it, a calm band high up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f9a953f3d3
commit
b145e1f288
3 files changed
+152
-44
No files matched your search
@@ -474,27 +474,92 @@ export function flangerColumns(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* How long a frame of the Flanger's response is blurred over, about what an eye blends: the delays
|
||||
* its light passed in that time. High up its teeth pass faster than frames are drawn, and sharp
|
||||
* frames of them flicker. Over 10 kHz this calms a column from 14 dB a frame to 0.04, under 5 kHz,
|
||||
* where the teeth can be followed, it hardly changes them.
|
||||
*/
|
||||
export const RESPONSE_BLUR = 1 / 20;
|
||||
/** A point of a response drawn as a line: how far along the screen, and its dB. */
|
||||
export interface ResponsePoint {
|
||||
share: number;
|
||||
db: number;
|
||||
}
|
||||
|
||||
/** A Phaser's or a Flanger's response over `columns` columns of the screen, `t` seconds after the sound. */
|
||||
export function responseColumnsOf(
|
||||
/**
|
||||
* A Flanger's response as a line over `columns` columns while its copy is `ms` late, exactly: the
|
||||
* ends of each column and the peaks and notches between them, where they are. A column the comb
|
||||
* runs too fine for gets one peak and one notch, so the line zigzags over all it holds.
|
||||
*/
|
||||
export function flangerLine(values: ModValues, ms: number, columns: number): ResponsePoint[] {
|
||||
const db = (halfTurns: number): number => flangerDb(values, 1, halfTurns * 500);
|
||||
const halfTurnsAt = (share: number): number => (hzAt(share) * Math.max(0, ms)) / 500;
|
||||
const shareOf = (halfTurns: number): number => shareOfHz((halfTurns * 500) / Math.max(0, ms));
|
||||
const points: ResponsePoint[] = [];
|
||||
for (let c = 0; c <= columns; c++) {
|
||||
const from = halfTurnsAt(c / columns);
|
||||
points.push({ share: c / columns, db: db(from) });
|
||||
if (c === columns) break;
|
||||
const to = halfTurnsAt((c + 1) / columns);
|
||||
const first = Math.ceil(from);
|
||||
for (let k = first; k <= first + 1 && k < to; k++)
|
||||
if (k > from) points.push({ share: shareOf(k), db: db(k) });
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
/**
|
||||
* How long the Flanger's trail lasts, about what an eye blends: the delays its light passed in that
|
||||
* time, drawn faintly behind the line. High up its teeth pass faster than frames are drawn, and a
|
||||
* sharp line of them flickers. There the trail is a calm band, over 10 kHz it changes 0.04 dB a frame.
|
||||
*/
|
||||
export const RESPONSE_TRAIL = 1 / 20;
|
||||
|
||||
/**
|
||||
* How clearly the line can be followed at `hz` while the delay moves `movedMs` within the trail:
|
||||
* fully while its teeth move up to a quarter of one, not at all from half of one on. There they
|
||||
* would flicker, and only the trail is left.
|
||||
*/
|
||||
export function clearAt(hz: number, movedMs: number): number {
|
||||
const halfTurns = (hz * Math.abs(movedMs)) / 500;
|
||||
return clamp01(2 - 2 * halfTurns);
|
||||
}
|
||||
|
||||
/** A Phaser's or a Flanger's response as the screen draws it, `t` seconds after the sound. */
|
||||
export interface ResponseFrame {
|
||||
/** Where the light is now, left to right. */
|
||||
line: ResponsePoint[];
|
||||
/** How clearly each point of the line can be followed, 0 to 1. */
|
||||
clear: number[];
|
||||
/** The lowest and highest of the line in each column: its notches are counted on them. */
|
||||
columns: { low: number; high: number }[];
|
||||
/** What it swept through just before, each band from longer ago to now, the longest first. */
|
||||
trail: { low: number; high: number }[][];
|
||||
}
|
||||
|
||||
export function responseFrame(
|
||||
device: ModDevice,
|
||||
values: ModValues,
|
||||
t: number,
|
||||
columns: number,
|
||||
): { low: number; high: number }[] {
|
||||
if (device.kind === 'phaser')
|
||||
return responseColumns((hz) => phaserDb(values, phaserTurnAt(values, t), hz), columns);
|
||||
const delays = [t - RESPONSE_BLUR, t - RESPONSE_BLUR / 2, t].map((at) =>
|
||||
flangerDelayAt(values, Math.max(0, at)),
|
||||
);
|
||||
return flangerColumns(values, Math.min(...delays), Math.max(...delays), columns);
|
||||
): ResponseFrame {
|
||||
if (device.kind === 'phaser') {
|
||||
const db = (hz: number) => phaserDb(values, phaserTurnAt(values, t), hz);
|
||||
const line = Array.from({ length: columns + 1 }, (_, c) => ({
|
||||
share: c / columns,
|
||||
db: db(hzAt(c / columns)),
|
||||
}));
|
||||
return { line, clear: line.map(() => 1), columns: responseColumns(db, columns), trail: [] };
|
||||
}
|
||||
const delayAt = (at: number) => flangerDelayAt(values, Math.max(0, at));
|
||||
const ms = delayAt(t);
|
||||
const line = flangerLine(values, ms, columns);
|
||||
const band = (age: number) => {
|
||||
const delays = [t - age, t - age / 2, t].map(delayAt);
|
||||
return { low: Math.min(...delays), high: Math.max(...delays) };
|
||||
};
|
||||
const trail = [band(RESPONSE_TRAIL), band(RESPONSE_TRAIL / 2)];
|
||||
const moved = trail[0]!.high - trail[0]!.low;
|
||||
return {
|
||||
line,
|
||||
clear: line.map((point) => clearAt(hzAt(point.share), moved)),
|
||||
columns: flangerColumns(values, ms, ms, columns),
|
||||
trail: moved > 0 ? trail.map((d) => flangerColumns(values, d.low, d.high, columns)) : [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user