Show a Saturation node as a device that glows with its drive (#396)
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:
1 parent
8c53ce0cbe
commit
80b25488ce
11 files changed
+1092
-21
No files matched your search
@@ -8,6 +8,7 @@ import { EnvelopePanel } from '../soundfx/EnvelopePanel';
|
||||
import { CompressorPanel } from '../soundfx/CompressorPanel';
|
||||
import { KnobPanel } from '../soundfx/KnobPanel';
|
||||
import { DelayPanel } from '../soundfx/DelayPanel';
|
||||
import { SaturationPanel } from '../soundfx/SaturationPanel';
|
||||
import { ReverbPanel } from '../soundfx/ReverbPanel';
|
||||
import { CurvePanel } from '../soundfx/CurvePanel';
|
||||
import type { EqEditor } from '../soundfx/EqEditor';
|
||||
@@ -404,8 +405,8 @@ export function graphHooks({ state, editor }: HookContext) {
|
||||
]),
|
||||
),
|
||||
idle: [...panel.idle],
|
||||
room: panel.roomButtons.find((b) => b.picked)?.room ?? null,
|
||||
rooms: Object.fromEntries(panel.roomButtons.map((b) => [b.room, screenCentre(b)])),
|
||||
room: panel.roomButtons.find((b) => b.picked)?.value ?? null,
|
||||
rooms: Object.fromEntries(panel.roomButtons.map((b) => [b.value, screenCentre(b)])),
|
||||
highs: panel.highs.text,
|
||||
sound: { text: panel.soundButton.text, at: screenCentre(panel.soundButton) },
|
||||
scope: {
|
||||
@@ -470,6 +471,57 @@ export function graphHooks({ state, editor }: HookContext) {
|
||||
};
|
||||
},
|
||||
},
|
||||
/**
|
||||
* The panel a Saturation node shows (#396), in the sound FX graph or the instrument graph: whether
|
||||
* it is shown and where, its knobs, buttons and lever, which settings are idle, how hot it runs,
|
||||
* what the screen draws and says, and the node's ports.
|
||||
*/
|
||||
saturation: {
|
||||
panel: (nodeId: string) => {
|
||||
const canvas = [editor.soundFxEditor.canvas, editor.instrumentEditor.canvas].find(
|
||||
(c) => c.faceOf(nodeId)?.widget instanceof SaturationPanel,
|
||||
);
|
||||
const panel = canvas?.faceOf(nodeId)?.widget;
|
||||
if (!canvas || !(panel instanceof SaturationPanel)) return null;
|
||||
const node = canvas.nodeScreen(nodeId);
|
||||
const ports = (keys: string[], isOutput: boolean) =>
|
||||
Object.fromEntries(keys.map((key) => [key, canvas.portGlobal(nodeId, key, isOutput)]));
|
||||
const trace = panel.scope.trace;
|
||||
return {
|
||||
shown: shownOnScreen(panel),
|
||||
rect: screenRect(panel),
|
||||
size: { w: panel.w, h: panel.h },
|
||||
node: node?.rect ?? null,
|
||||
inputs: ports(node?.inputs ?? [], false),
|
||||
outputs: ports(node?.outputs ?? [], true),
|
||||
knobs: Object.fromEntries(
|
||||
Object.entries(panel.knobs).map(([key, knob]) => [
|
||||
key,
|
||||
{ value: knob.value, number: knob.shown, enabled: knob.enabled, dial: knob.dialGlobal() },
|
||||
]),
|
||||
),
|
||||
idle: [...panel.idle],
|
||||
type: panel.typeButtons.find((b) => b.picked)?.value ?? null,
|
||||
types: Object.fromEntries(panel.typeButtons.map((b) => [b.value, screenCentre(b)])),
|
||||
autoGain: {
|
||||
lit: panel.autoGain.lit,
|
||||
says: panel.autoGain.says.text,
|
||||
at: screenCentre(panel.autoGain),
|
||||
},
|
||||
heat: { share: panel.heat, word: panel.heatText.text, burning: panel.burning },
|
||||
scope: {
|
||||
says: panel.scope.texts,
|
||||
/** The loudest point out, the gain after the curve, and whether the wave leaves the screen. */
|
||||
peak: trace?.peak ?? null,
|
||||
gain: trace?.gain ?? null,
|
||||
over: panel.scope.over,
|
||||
/** The embers drawn now and how the glow flickers: both move by themselves. */
|
||||
embers: panel.scope.embers,
|
||||
flare: panel.scope.flare,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
instrument: {
|
||||
current: () => editor.instrumentEditor.currentAssetId,
|
||||
info: () => editor.instrumentEditor.shownInfo,
|
||||
|
||||
Reference in new issue
Block a user