Show a Bitcrusher node as a device that draws its pixels (#399)
A Bitcrusher only said what it does in rows of numbers. How coarse its steps and holds are could not be seen. Now the node is a device of its own size. Its screen sends a sine through the node's own crush and draws what comes out in cells, as tall as a step and as wide as a hold, so the coarser the crush, the coarser the picture. - CrusherPanel draws a 680x280 plate in bubblegum pink. Bits and rate are large knobs, wet is small. Rate gives its low end more of the turn, 8 kHz sits just past the middle. - crushTrace runs a 440 Hz sine at 0 dBFS through Crusher at 48 kHz and blends it as the worklet does. Where each hold starts comes from Crusher too: a ramp through it at 16 bits takes a new value per hold. - cellOf shapes a hold. From 5 px a step or hold gets a grid line and a gap. Finer steps give a 2 px bar. A cell is never more than twice as tall as wide, so 1 bit gives bars at -1, 0 and 1. - The screen shows two periods, or 6 holds where the rate is low. The sine going in is a grey ghost. Below wet 1 the heard line lies over the cells. - Above the screen a word for how crushed it is, CLEAN to BROKEN. Under it the noise the bits leave and where the rate folds back. - 16 lamps show the bits kept, the highest on the left. A CRUSH lamp beside the name glows the more it crushes. - COLUMNS_OF gives the Bitcrusher 4 columns in the presets. - Hook __gm.bitcrusher.panel(id) for the sound FX and instrument graphs. Decided myself: - The screen is still. Nothing on it moves, so tick() says false. - How crushed counts whichever of bits or rate takes more, the rate in octaves, times wet. Found: at 8 kHz the first hold is 7 samples long, the rest 6. Six sixths add up to just under 1 in Crusher. The worklet does the same, so the screen shows what is heard. Every sfxr effect preset ends in a Bitcrusher and gets 3 columns wider. The Laser preset still fits whole on Fit. Tests: - Unit: crusher-view 13 tests (new). The screen matches Crusher with its wet on three settings. - Counter-test: without the wet in the blend the worklet test fails. - e2e 34: ALL PASSED, 143 checks (1 new). - Gate: CHECK EXIT 0, 292 files, 4484 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b145e1f288
commit
a6c00f7b8b
9 files changed
+942
-3
No files matched your search
@@ -6,6 +6,7 @@ import { currentNodePalette } from '../graph/NodePalette';
|
||||
import { ChipPanel } from '../soundfx/ChipPanel';
|
||||
import { EnvelopePanel } from '../soundfx/EnvelopePanel';
|
||||
import { CompressorPanel } from '../soundfx/CompressorPanel';
|
||||
import { CrusherPanel } from '../soundfx/CrusherPanel';
|
||||
import { KnobPanel } from '../soundfx/KnobPanel';
|
||||
import { DelayPanel } from '../soundfx/DelayPanel';
|
||||
import { LimiterPanel } from '../soundfx/LimiterPanel';
|
||||
@@ -627,6 +628,51 @@ export function graphHooks({ state, editor }: HookContext) {
|
||||
};
|
||||
},
|
||||
},
|
||||
/**
|
||||
* The panel a Bitcrusher node shows (#399), in the sound FX graph or the instrument graph: whether
|
||||
* it is shown and where, its knobs, which settings are idle, how crushed it is and how lit each
|
||||
* bit's lamp is, what the screen draws and says, and the node's ports.
|
||||
*/
|
||||
bitcrusher: {
|
||||
panel: (nodeId: string) => {
|
||||
const canvas = [editor.soundFxEditor.canvas, editor.instrumentEditor.canvas].find(
|
||||
(c) => c.faceOf(nodeId)?.widget instanceof CrusherPanel,
|
||||
);
|
||||
const panel = canvas?.faceOf(nodeId)?.widget;
|
||||
if (!canvas || !(panel instanceof CrusherPanel)) 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],
|
||||
crush: { share: panel.crush, word: panel.crushText.text },
|
||||
/** The bits' lamps, the highest first: 1 lit, 0 dark, a fraction for part of a bit. */
|
||||
bits: panel.bitsLit,
|
||||
scope: {
|
||||
says: panel.scope.texts,
|
||||
/** The step between two levels, the seconds across, and the holds the sine was cut into. */
|
||||
step: trace?.step ?? null,
|
||||
span: trace?.span ?? null,
|
||||
holds: trace?.holds.length ?? 0,
|
||||
/** The cells drawn, their shape, the grid lines, and whether the line that comes out is over them. */
|
||||
drawn: panel.scope.drawn,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
tremolo: { panel: modulationPanel },
|
||||
vibrato: { panel: modulationPanel },
|
||||
phaser: { panel: modulationPanel },
|
||||
|
||||
Reference in new issue
Block a user