Show Tremolo, Vibrato, Phaser and Flanger as devices that swing (#398)
The four effects that swing were still drawn as rows of numbers. Like
the Delay, the Saturation and the Limiter, each node now shows a device
of a fixed size. Its screen draws what it does over the first seconds
of a sound, and a light plays that in real time, over and over.
- One device for all four (ModulationPanel, ModulationFace and
modulationView), as KnobPanel is for Gain, Pitch, Mix and Pan. A short
description gives each its title, plate, buttons and knobs.
- Tremolo, 640x280 in cream: a wave its volume shapes, the volume as
its outline. It says DOWN TO 50%, EVERY 200 ms and DIPS -6 dB.
- Vibrato, 640x280 in plum: the pitch in semitones, cool below and warm
above. Under a wet below 1 the dry pitch stands beside it. It says
how late the voice is (LATE 0.93 ms).
- Phaser, 760x280 in mustard, and Flanger, 680x280 in steel blue: the
response from 20 Hz to 20 kHz, and under it a lane of the turn or the
delay over time. The response is drawn where the light is, so the
notches swirl and the comb jets. A comb finer than a column fills it.
- The light loops over 1, 2, 4 or 8 s, two LFO swings at least, for a
Flanger as long as its sweep takes to reach 20 ms. The face only
keeps the editor drawing while something swings and is heard.
- A Rate lamp beside each name goes from dark to lit with the swing.
- Nothing is listened to. The pictures use the worklets' own numbers:
lfoAt, vibratoSpeedUp (now exported from lfo.ts), the allpass chain
with its feedback, and the comb, reckoned at 48 kHz.
- Phaser and Flanger take 4 preset columns. Phased Pad was looked at
in a picture, nothing overlaps.
- Exported from nodes.ts: LFO_SHAPE, lfoShapeOf, TREMOLO_ROWS,
VIBRATO_ROWS, PHASER_STAGE_ROW and phaserStagesOf. FLANGER_MAX_MS is
exported from phaser.ts.
Decided here, as the request left the look open:
- A notch counts where the response falls below -3 dB, not some dB
under the loudest point. Feedback rings peaks far above 0 dB, and the
first rule counted 7 notches on a Phaser of 12 stages.
- Rate and Depth of Tremolo and Vibrato are big knobs, Wet is small.
Tests:
Unit modulation-view (stein2): 22 tests, new. Phaser and Flanger
give the dB of their worklets on a sine within 0.005 dB, the
Tremolo its samples, the Vibrato the pitch it reads at.
Counter-check: with the sign of the Phaser's feedback flipped in one
place, exactly the worklet test fails.
e2e 34 (atx): ALL PASSED, 142 checks, 4 new. Each device has its
size, its ports outside, a light that runs by itself, and a button
and a knob as one undo step each. Picking 8 stages on the Phaser
draws 4 notches.
Gate (atx): CHECK EXIT 0, 291 files, 4467 tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
28909f0adf
commit
b6749cc43d
12 files changed
+1860
-11
No files matched your search
@@ -396,11 +396,13 @@ const ROW = 340;
|
||||
/**
|
||||
* Columns a node takes that is drawn as its editor in the graph (#385): a Filter's curve is wide,
|
||||
* a Chip's device (#386) wider, an Envelope's (#388) a little less, a Pitch's and a Mix's (#390) two,
|
||||
* a Delay's (#393) four.
|
||||
* a Delay's (#393), a Phaser's and a Flanger's (#398) four.
|
||||
*/
|
||||
const COLUMNS_OF: Readonly<Record<string, number>> = {
|
||||
[SoundFxFilter.type]: 4,
|
||||
[SoundFxDelay.type]: 4,
|
||||
[SoundFxPhaser.type]: 4,
|
||||
[SoundFxFlanger.type]: 4,
|
||||
[SoundFxPitch.type]: 2,
|
||||
[SoundFxMix.type]: 2,
|
||||
[INSTRUMENT_CHIP]: 5,
|
||||
|
||||
@@ -68,7 +68,7 @@ export function lfoSwing(shape: LfoShape): number {
|
||||
}
|
||||
|
||||
/** How much faster the sound is read at the top of a Vibrato `depth` semitones deep. */
|
||||
const speedUp = (depth: number) => 2 ** (Math.max(0, depth) / 12) - 1;
|
||||
export const vibratoSpeedUp = (depth: number) => 2 ** (Math.max(0, depth) / 12) - 1;
|
||||
|
||||
/** The longest a Vibrato's delay may get, and so what its line holds. */
|
||||
const VIBRATO_MAX_DELAY = 0.5;
|
||||
@@ -79,7 +79,7 @@ const VIBRATO_MAX_DELAY = 0.5;
|
||||
*/
|
||||
export function vibratoDelay(shape: LfoShape, rate: number, depth: number): number {
|
||||
if (!(rate > 0)) return 0;
|
||||
return Math.min(VIBRATO_MAX_DELAY / 2, (speedUp(depth) * lfoSwing(shape)) / rate);
|
||||
return Math.min(VIBRATO_MAX_DELAY / 2, (vibratoSpeedUp(depth) * lfoSwing(shape)) / rate);
|
||||
}
|
||||
|
||||
const at = (values: Float32Array, i: number) => (values.length > 1 ? values[i]! : values[0]!);
|
||||
@@ -134,7 +134,7 @@ export class Vibrato {
|
||||
while (this.lines.length < outputs.length) this.lines.push(new Float32Array(this.size));
|
||||
for (let i = 0; i < length; i++) {
|
||||
const rate = Math.max(1e-3, at(numbers.rate, i));
|
||||
const swing = (speedUp(at(numbers.depth, i)) / rate) * swingAt(this.shape, this.phase);
|
||||
const swing = (vibratoSpeedUp(at(numbers.depth, i)) / rate) * swingAt(this.shape, this.phase);
|
||||
const delay = Math.max(0, Math.min(VIBRATO_MAX_DELAY, this.center + swing)) * this.sampleRate;
|
||||
const read = this.written - delay;
|
||||
const from = Math.floor(read);
|
||||
|
||||
@@ -871,7 +871,7 @@ export const PHASER_ROWS = {
|
||||
feedback: PHASER_FEEDBACK,
|
||||
mix: PHASER_MIX,
|
||||
};
|
||||
const PHASER_STAGE_ROW = {
|
||||
export const PHASER_STAGE_ROW = {
|
||||
key: 'stages',
|
||||
label: 'Stages',
|
||||
kind: 'enum' as const,
|
||||
@@ -879,7 +879,7 @@ const PHASER_STAGE_ROW = {
|
||||
options: PHASER_STAGES.map((n) => ({ value: String(n), label: String(n) })),
|
||||
};
|
||||
|
||||
function phaserStagesOf(settings: PropertyBag): number {
|
||||
export function phaserStagesOf(settings: PropertyBag): number {
|
||||
const stages = Number(settings[PHASER_STAGE_ROW.key] ?? PHASER_STAGE_ROW.default);
|
||||
return PHASER_STAGES.find((n) => n === stages) ?? 4;
|
||||
}
|
||||
@@ -1146,7 +1146,7 @@ export const SoundFxSaturation: SoundFxNodeDef = effect(
|
||||
'own',
|
||||
);
|
||||
|
||||
const LFO_SHAPE = {
|
||||
export const LFO_SHAPE = {
|
||||
key: 'shape',
|
||||
label: 'Shape',
|
||||
kind: 'enum' as const,
|
||||
@@ -1154,7 +1154,7 @@ const LFO_SHAPE = {
|
||||
options: LFO_SHAPES,
|
||||
};
|
||||
|
||||
function lfoShapeOf(settings: PropertyBag): LfoShape {
|
||||
export function lfoShapeOf(settings: PropertyBag): LfoShape {
|
||||
return LFO_SHAPES.find((option) => option.value === settings[LFO_SHAPE.key])?.value ?? 'sine';
|
||||
}
|
||||
|
||||
@@ -1162,6 +1162,8 @@ const TREMOLO_RATE = { ...numberRow('rate', 'Rate (Hz)', 0, 20, 0.01), default:
|
||||
const TREMOLO_DEPTH = { ...numberRow('depth', 'Depth (%)', 0, 100, 1), default: 50 };
|
||||
const VIBRATO_RATE = { ...numberRow('rate', 'Rate (Hz)', 0.5, 20, 0.01), default: 5 };
|
||||
const VIBRATO_DEPTH = { ...numberRow('depth', 'Depth (st)', 0, 4, 0.01), default: 0.5 };
|
||||
export const TREMOLO_ROWS = { rate: TREMOLO_RATE, depth: TREMOLO_DEPTH, wet: WET };
|
||||
export const VIBRATO_ROWS = { rate: VIBRATO_RATE, depth: VIBRATO_DEPTH, wet: WET };
|
||||
|
||||
export const SoundFxTremolo: SoundFxNodeDef = effect(
|
||||
{
|
||||
|
||||
@@ -72,7 +72,7 @@ export class Phaser {
|
||||
}
|
||||
|
||||
/** The longest a Flanger delays its copy, in ms; a delay past it stays there, as sfxr's does at its own end. */
|
||||
const FLANGER_MAX_MS = 20;
|
||||
export const FLANGER_MAX_MS = 20;
|
||||
|
||||
/**
|
||||
* The flanger, which sfxr calls its phaser: the sound and a copy `offset` ms late. `sweep` moves
|
||||
|
||||
Reference in new issue
Block a user