Keep the Flanger's comb calm and smooth where its teeth crowd (#398)
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:
1 parent
b6749cc43d
commit
f9a953f3d3
3 files changed
+117
-23
No files matched your search
@@ -9,6 +9,7 @@ import { Flanger, Phaser } from '../../src/core/soundfx/phaser';
|
||||
import { type Box, knobBox } from '../../src/editor/soundfx/hardwareView';
|
||||
import {
|
||||
FLANGER_DEVICE,
|
||||
flangerColumns,
|
||||
flangerDb,
|
||||
flangerDelayAt,
|
||||
idleModKeys,
|
||||
@@ -23,7 +24,10 @@ import {
|
||||
notchesOf,
|
||||
PHASER_DEVICE,
|
||||
phaserDb,
|
||||
RESPONSE_FLOOR,
|
||||
responseColumns,
|
||||
responseColumnsOf,
|
||||
shareOfHz,
|
||||
SHOWN_RATE,
|
||||
swingPicture,
|
||||
swingValueAt,
|
||||
@@ -257,6 +261,47 @@ describe('the Flanger screen', () => {
|
||||
expect(flangerDb(values, 1, 500)).toBeLessThan(-15);
|
||||
});
|
||||
|
||||
it('holds in each column what a fine reading of the comb finds there', () => {
|
||||
// A notch without feedback goes down to nothing, where no reading lands: compare what is shown.
|
||||
const shown = (db: number) => Math.max(RESPONSE_FLOOR, db);
|
||||
for (const [feedback, mix] of [
|
||||
[0, 1],
|
||||
[0.5, 1],
|
||||
[0.9, 0.4],
|
||||
] as const) {
|
||||
const values = modValues(FLANGER_DEVICE, { feedback, mix });
|
||||
for (const ms of [0.3, 3, 12]) {
|
||||
const columns = flangerColumns(values, ms, ms, 200);
|
||||
const fine = responseColumns((hz) => flangerDb(values, ms, hz), 200, 4000);
|
||||
columns.forEach((column, c) => {
|
||||
expect(column.low).toBeLessThanOrEqual(fine[c]!.low + 1e-9);
|
||||
expect(column.high).toBeGreaterThanOrEqual(fine[c]!.high - 1e-9);
|
||||
expect(shown(column.low)).toBeCloseTo(shown(fine[c]!.low), 1);
|
||||
expect(column.high).toBeCloseTo(fine[c]!.high, 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('blurs the teeth that pass faster than frames into the same band, so they do not flicker', () => {
|
||||
// The default swings 1 to 3 ms every 4 s, at 0.9 s about its fastest: high up, the time of a
|
||||
// blur lets more than a tooth pass, low down a sliver of one.
|
||||
const values = modValues(FLANGER_DEVICE, { feedback: 0.5 });
|
||||
const frames = [0, 1, 2].map((k) => responseColumnsOf(FLANGER_DEVICE, values, 0.9 + k / 60, 640));
|
||||
const peak = 20 * Math.log10(1.5);
|
||||
const notch = flangerDb(values, 1, 500);
|
||||
const high = Math.ceil(640 * shareOfHz(16000));
|
||||
for (const columns of frames)
|
||||
for (const column of columns.slice(high)) {
|
||||
expect(column.high).toBeCloseTo(peak, 6);
|
||||
expect(column.low).toBeCloseTo(notch, 6);
|
||||
}
|
||||
// Below the first notch it stays a sharp line that moves.
|
||||
const low = Math.floor(640 * shareOfHz(100));
|
||||
for (const columns of frames) expect(columns[low]!.high - columns[low]!.low).toBeLessThan(0.5);
|
||||
expect(frames[0]![low]!.high).not.toBeCloseTo(frames[2]![low]!.high, 3);
|
||||
});
|
||||
|
||||
it('sweeps the delay down through 0 and up again, the LFO on top', () => {
|
||||
const values = modValues(FLANGER_DEVICE, { offset: 1, sweep: -2, depth: 0 });
|
||||
expect(flangerDelayAt(values, 0.5)).toBeCloseTo(0);
|
||||
|
||||
Reference in new issue
Block a user