Show a Chip node as a device with its wave and its pitch (#386)
The Chip had eleven numbers in rows and nothing that showed what they do to a note. Its node is now a device of one size like the Compressor's, and two screens draw the wave and the pitch from the settings, worked right in the instrument graph. - ChipPanel is 840x280 in the warm grey of a handheld console. A button per wave, the "follow note" lever and a chrome knob per number, grouped Oscillator, Slide, Vibrato, Arpeggio and Repeat. - The wave screen draws two periods as loud as the Chip plays them, a square at its duty. toneValue and periodicSteps come out of chip.ts, so the screen and the worklet share one formula. - The pitch screen draws what slide, delta slide, vibrato, arpeggio and repeat make of the pitch over a note. It ends where the note falls below "min frequency" and starts from A4 while the Chip follows the note. - Idle settings are faded, wired ones greyed and locked, as the rows were. Every change is one undo step. - Knobs take a bipolar range: 0 straight up, the taper bends each half. Slide, delta slide, duty sweep and arpeggio use it. - The Compressor's auto-gain lever is now a Lever with a name of its own, which the Chip uses for "follow note". - Presets give a Chip five columns, as a Filter has four since #385. Found on the way: - With the Chip this wide the Laser preset is about 3060 units. In a 1280 px window Fit stops at the least zoom of 0.25 and cuts the Notes node off. The zoom limit is left as it is. Tests: - Unit: chip-view (9 new), knob (1 new), chip, chip-presets and hardware-view, 53 green - e2e 34 on melektaus-atx: ALL PASSED, 131 checks (1 new). The first run was red on the check itself: a Laser's "min frequency" can lie above A4, and the screen then rightly says the note ends at once. - Gate on melektaus-atx: CHECK EXIT 0, 285 files, 4377 tests - Counter-check: a square drawn with its duty mirrored turns the wave test red Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c087993000
commit
eb78f3cbf3
14 files changed
+1190
-26
No files matched your search
@@ -393,8 +393,11 @@ function held<K extends string>(
|
||||
/** Room between the nodes of a preset, left to right and top to bottom: a Chip is a tall node. */
|
||||
const COLUMN = 240;
|
||||
const ROW = 340;
|
||||
/** Columns a node takes that is drawn as its editor in the graph (#385): a Filter's curve is wide. */
|
||||
const COLUMNS_OF: Readonly<Record<string, number>> = { [SoundFxFilter.type]: 4 };
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const COLUMNS_OF: Readonly<Record<string, number>> = { [SoundFxFilter.type]: 4, [INSTRUMENT_CHIP]: 5 };
|
||||
|
||||
/** The column right of a node, past all it takes. */
|
||||
const columnAfter = (node: GraphNode): number => node.x / COLUMN + (COLUMNS_OF[node.type] ?? 1);
|
||||
|
||||
@@ -320,7 +320,7 @@ export const InstrumentChip: InstrumentNodeDef = {
|
||||
category: 'sound',
|
||||
instrument: true,
|
||||
description:
|
||||
'A raw wave as old game consoles made it, for every note that triggers it, the way sfxr makes its sounds. "Wave" is Square, Triangle, Saw, Sine, Noise, or Noise (periodic), the short metallic noise of the NES; a square and that noise are half as loud as the others, as in sfxr. With "follow note" it plays at the pitch of the note, else at "frequency" in Hz; a number connected to "frequency" goes before both. "duty" is how much of a square is up: 0.5 is even, 0.125 thin and nasal; "duty sweep" moves it by that much per second. "slide" glides the pitch in semitones per second, "delta slide" changes the slide by that much per second. Where it falls below "min frequency" the note ends; at 0 it never does. "vibrato depth" in semitones and "vibrato speed" in Hz make it wobble. "arpeggio" jumps it by that many semitones "arpeggio time" seconds into the note. "repeat" starts slide, duty sweep and arpeggio over every so many seconds, 0 never. The Chip plays until its note ends, faded out in a few milliseconds; under an Envelope it plays on through the release. An Envelope can move "frequency" and "duty" while it plays, a Pitch moves its frequency itself. Settings that do nothing as things stand are grey.',
|
||||
'A raw wave as old game consoles made it, for every note that triggers it, the way sfxr makes its sounds. "Wave" is Square, Triangle, Saw, Sine, Noise, or Noise (periodic), the short metallic noise of the NES; a square and that noise are half as loud as the others, as in sfxr. With "follow note" it plays at the pitch of the note, else at "frequency" in Hz; a number connected to "frequency" goes before both. "duty" is how much of a square is up: 0.5 is even, 0.125 thin and nasal; "duty sweep" moves it by that much per second. "slide" glides the pitch in semitones per second, "delta slide" changes the slide by that much per second. Where it falls below "min frequency" the note ends; at 0 it never does. "vibrato depth" in semitones and "vibrato speed" in Hz make it wobble. "arpeggio" jumps it by that many semitones "arpeggio time" seconds into the note. "repeat" starts slide, duty sweep and arpeggio over every so many seconds, 0 never. The Chip plays until its note ends, faded out in a few milliseconds; under an Envelope it plays on through the release. An Envelope can move "frequency" and "duty" while it plays, a Pitch moves its frequency itself. Settings that do nothing as things stand are grey. In the graph the node is a device: it draws its wave over two periods, and what slide, vibrato, arpeggio and repeat make of the pitch over a note, from A4 when it follows the note.',
|
||||
inputs: [
|
||||
triggerIn,
|
||||
...Object.values(CHIP_ROWS).map((row): PortDef =>
|
||||
|
||||
+41
-11
@@ -119,18 +119,12 @@ export class ChipPlayer {
|
||||
private sample(width: number): number {
|
||||
const p = this.phase;
|
||||
switch (this.shape.wave) {
|
||||
case 'square':
|
||||
return p < width ? 0.5 : -0.5;
|
||||
case 'saw':
|
||||
return 1 - 2 * p;
|
||||
case 'triangle':
|
||||
return p < 0.5 ? 4 * p - 1 : 3 - 4 * p;
|
||||
case 'sine':
|
||||
return Math.sin(2 * Math.PI * p);
|
||||
case 'noise':
|
||||
return this.noise[Math.min(NOISE_STEPS - 1, Math.floor(p * NOISE_STEPS))]!;
|
||||
case 'periodic':
|
||||
return this.lfsr & 1 ? 0.5 : -0.5;
|
||||
default:
|
||||
return toneValue(this.shape.wave, p, width);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,12 +139,48 @@ export class ChipPlayer {
|
||||
for (let i = 0; i < NOISE_STEPS; i++) this.noise[i] = this.random() * 2 - 1;
|
||||
}
|
||||
|
||||
/** One step of the 15-bit noise register of the NES in its short mode: bit 0 and bit 6 feed back. */
|
||||
private clock(): void {
|
||||
const feedback = (this.lfsr ^ (this.lfsr >> 6)) & 1;
|
||||
this.lfsr = (this.lfsr >> 1) | (feedback << 14);
|
||||
this.lfsr = clocked(this.lfsr);
|
||||
}
|
||||
}
|
||||
|
||||
/** A wave with no noise in it at `phase` of its period, 0 to 1; also what a Chip's node draws (#386). */
|
||||
export function toneValue(
|
||||
wave: Exclude<ChipWave, 'noise' | 'periodic'>,
|
||||
phase: number,
|
||||
width: number,
|
||||
): number {
|
||||
switch (wave) {
|
||||
case 'square':
|
||||
return phase < width ? 0.5 : -0.5;
|
||||
case 'saw':
|
||||
return 1 - 2 * phase;
|
||||
case 'triangle':
|
||||
return phase < 0.5 ? 4 * phase - 1 : 3 - 4 * phase;
|
||||
case 'sine':
|
||||
return Math.sin(2 * Math.PI * phase);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The short noise of the NES as its register gives it, `steps` values from the start: what a
|
||||
* periodic Chip plays over its first period, the same every time.
|
||||
*/
|
||||
export function periodicSteps(steps = PERIODIC_STEPS): number[] {
|
||||
let lfsr = 1;
|
||||
const out: number[] = [];
|
||||
for (let i = 0; i < steps; i++) {
|
||||
lfsr = clocked(lfsr);
|
||||
out.push(lfsr & 1 ? 0.5 : -0.5);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** One step of the 15-bit noise register of the NES in its short mode: bit 0 and bit 6 feed back. */
|
||||
function clocked(lfsr: number): number {
|
||||
const feedback = (lfsr ^ (lfsr >> 6)) & 1;
|
||||
return (lfsr >> 1) | (feedback << 14);
|
||||
}
|
||||
|
||||
const at = (values: Float32Array, i: number) => (values.length > 1 ? values[i]! : values[0]!);
|
||||
const finite = (value: number, fallback: number) => (Number.isFinite(value) ? value : fallback);
|
||||
Reference in new issue
Block a user