Show a Saturation node as a device that glows with its drive (#396)
game-maker / gate (push) Successful in 4m5s
game-maker / suite (push) Successful in 16m2s

A Saturation in the graph was a stack of rows. The user wanted a device
of its own style and size, like the Delay and the Reverb, that shows
warmth and heat the harder the sound is driven.

- The node is a copper plate of 720x280 with a button per type, the
  auto-gain lever, a big drive knob and small knobs for tone, output
  and wet. Each change is one undo step, wired numbers lock their knob.
- The screen draws a sine at -12 dBFS, the level auto-gain is reckoned
  at, as a grey ghost. Over it is the wave that comes out: bent by the
  type, its DC taken out, auto-gain and output after it, blended by
  wet. Under it stand the auto-gain and the peak.
- The heat is the drive over 48 dB, as far as wet lets it be heard.
  The wave goes from cold steel through ember red to white, a glow
  rises from below and flickers, and embers fly up from about 8 dB on.
  A lamp beside the name glows with it.
- The node keeps the editor drawing only while there is heat. Without
  drive it draws nothing, unlike the Delay, which always animates.
- The Reverb's private type button is now PickButton with its own
  colours, and the Saturation's type buttons use it as well.
- No instrument preset uses a Saturation, so no preset needs more
  columns.

Tests:
- Unit saturation-view: 14 new tests.
- Counter-check: without taking out the DC, exactly the DC test fails.
- e2e 34 (atx): ALL PASSED, 137 checks, one new for type, lever and
  drive with undo.
- Gate (atx): CHECK EXIT 0, 289 files, 4431 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-10 20:05:11 +02:00
1 parent 8c53ce0cbe
commit 80b25488ce
11 files changed
+1092 -21

No files matched your search

+4 -4
View File
@@ -1095,15 +1095,15 @@ export const SoundFxLimiter: SoundFxNodeDef = effect(
const DRIVE = { ...numberRow('drive', 'Drive (dB)', 0, 48, 0.5), default: 12 };
const TONE = { ...numberRow('tone', 'Tone', 0, 1, 0.01), default: 1 };
const OUTPUT = numberRow('output', 'Output (dB)', -24, 24, 0.5);
const SATURATION_AUTO_GAIN = { ...COMPRESS_AUTO_GAIN, default: true };
export const SATURATION_AUTO_GAIN = { ...COMPRESS_AUTO_GAIN, default: true };
const SATURATION_MIX = { ...numberRow('mix', 'Wet', 0, 1, 0.01), default: 1 };
const SATURATION_ROWS = {
export const SATURATION_ROWS = {
drive: DRIVE,
tone: TONE,
output: OUTPUT,
mix: SATURATION_MIX,
};
const SATURATION_TYPE = {
export const SATURATION_TYPE = {
key: 'curve',
label: 'Type',
kind: 'enum' as const,
@@ -1111,7 +1111,7 @@ const SATURATION_TYPE = {
options: SATURATION_TYPES,
};
function saturationTypeOf(settings: PropertyBag): SaturationType {
export function saturationTypeOf(settings: PropertyBag): SaturationType {
return SATURATION_TYPES.find((option) => option.value === settings[SATURATION_TYPE.key])?.value ?? 'soft';
}
+3 -3
View File
@@ -71,7 +71,7 @@ export function saturationCurve(type: SaturationType): Float32Array {
}
/** The level a reference sine is at: -12 dBFS, about where a sound is loud but not at its peak. */
const REFERENCE = 0.25;
export const SATURATION_REFERENCE = 0.25;
const STEPS = 256;
/**
@@ -82,11 +82,11 @@ const STEPS = 256;
export function saturationAutoGain(type: SaturationType, driveDb: number): number {
const drive = 10 ** (driveDb / 20);
const out = Array.from({ length: STEPS }, (_, i) =>
saturate(type, REFERENCE * Math.sin((2 * Math.PI * i) / STEPS) * drive),
saturate(type, SATURATION_REFERENCE * Math.sin((2 * Math.PI * i) / STEPS) * drive),
);
const mean = out.reduce((sum, v) => sum + v, 0) / STEPS;
const rms = Math.sqrt(out.reduce((sum, v) => sum + (v - mean) ** 2, 0) / STEPS);
const gain = rms > 0 ? 20 * Math.log10(REFERENCE / Math.SQRT2 / rms) : 48;
const gain = rms > 0 ? 20 * Math.log10(SATURATION_REFERENCE / Math.SQRT2 / rms) : 48;
return Math.max(-48, Math.min(48, gain));
}