Rebuild the Acid node after the acid voice of nodeseq (#421)
The first Acid hardly squelched. Its tanh ate the resonance, and its sweep fell evenly in octaves. The user pointed at the acid tone of nodeseq.com/studio, so the node now follows how that voice is built from Web Audio parts. - The filter is a two-pole 12 dB low-pass with Q in dB up to 17.5, like a BiquadFilterNode. The ladder and its tanh are gone. - One ADSR drives both the volume and the filter. The sweep falls back in Hz, fast from the top and long at the bottom. - Every note glides from the one before. A new note cuts the last one off, and the node plays its release itself. - Knobs: cutoff, reso, env, attack, decay, sustain, release and glide. The defaults are the acid preset of nodeseq. - Accent, slide, drive and tuning are dropped. Old projects were not kept, the user had only placed the node for a try. - A hook __gm.acid.compare renders a line offline through our worklet and through the nodeseq graph, for e2e 34. Found on the way: in Chrome an oscillator that starts inside a render block runs its first samples at 440 Hz. The comparison evens this out. Tests: - tsc, eslint, Prettier clean - Unit: acid.test.ts rewritten with 12 tests. chip-presets and sound-library follow the new node. - e2e 34 on melektaus-atx: ALL PASSED, 148 checks. Against nodeseq the loudness per 10 ms window is at most 0.14 dB apart, and 95 to 97 % of the windows are equally bright. - With a linear Q instead of Q in dB the comparison is red. - Gate on melektaus-atx: green, 306 files, 4752 tests. That is 4754 less the 14 old and plus the 12 new Acid tests. - Not checked: the sound by ear, the master chain of nodeseq, the keyboard and a bake with the new node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d844f2734a
commit
f92d69d902
15 files changed
+645
-592
No files matched your search
+33
-23
@@ -2922,9 +2922,9 @@ export async function sound(t) {
|
||||
|
||||
// #421: Preset → Acid Bass makes Notes → Acid → Delay → Output, the Acid drawn as a device between
|
||||
// its ports: a button per wave and a knob per number, each change one undo step, its screen
|
||||
// drawn from it. The worklet hands the synth to a note that slides in; of three notes the
|
||||
// second, struck where the first ends, slides, the third after a gap does not. An undo
|
||||
// brings the Laser back.
|
||||
// drawn from it. The worklet sounds as nodeseq's acid built from Web Audio nodes does; of
|
||||
// three notes the second and third glide from the one before, and all of them play out
|
||||
// their release and end by themselves. An undo brings the Laser back.
|
||||
await clickHook('presetAt');
|
||||
await clickText('Acid Bass');
|
||||
await sleep(300);
|
||||
@@ -2952,35 +2952,41 @@ export async function sound(t) {
|
||||
'Preset → Acid Bass makes an Acid shown as its device; a wave button and a knob each change it as one undo step, and its screen follows',
|
||||
acidBuilt.types.join() === 'InstrumentNotes,InstrumentAcid,SoundFxDelay,InstrumentOutput' &&
|
||||
acidShown.shown &&
|
||||
acidShown.size.w === 520 &&
|
||||
acidShown.size.w === 580 &&
|
||||
acidShown.size.h === 280 &&
|
||||
Object.values(acidShown.inputs).length === 8 &&
|
||||
Object.values(acidShown.inputs).length === 9 &&
|
||||
Object.values(acidShown.inputs).every((p) => p.x < acidRect.x && acidBeside(p)) &&
|
||||
Object.values(acidShown.outputs).every((p) => p.x > acidRect.x + acidRect.w && acidBeside(p)) &&
|
||||
acidShown.wave === 'saw' &&
|
||||
acidShown.knobs.cutoff.value === 300 &&
|
||||
acidShown.screen.rest === 'REST 300 Hz' &&
|
||||
acidShown.knobs.cutoff.value === 250 &&
|
||||
acidShown.screen.rest === 'REST 250 Hz' &&
|
||||
acidShown.trace.peakDb > 6 &&
|
||||
acidSquared.params.wave === 'square' &&
|
||||
acidSquared.panel.wave === 'square' &&
|
||||
acidTurned.params.cutoff > 300 &&
|
||||
acidTurned.params.cutoff > 250 &&
|
||||
acidTurned.panel.screen.rest !== acidShown.screen.rest &&
|
||||
acidUndone.wave === 'saw' &&
|
||||
acidUndone.cutoff === 300,
|
||||
acidUndone.cutoff === 250,
|
||||
JSON.stringify({ acidBuilt, acidShown, acidSquared, acidTurned, acidUndone }),
|
||||
);
|
||||
|
||||
const acidRender = JSON.parse(await evaluate('__gm.acid.render(0.1, 0.3).then(JSON.stringify)'));
|
||||
// The same line with nodeseq's acid and with the resonance and the sweep all the way up.
|
||||
const acidCompared = [];
|
||||
for (const set of [{}, { resonance: 1, envMod: 1 }])
|
||||
acidCompared.push(
|
||||
JSON.parse(await evaluate(`__gm.acid.compare(${JSON.stringify(set)}).then(JSON.stringify)`)),
|
||||
);
|
||||
check(
|
||||
'the worklet hands the synth of an Acid to the note that slides in, from its first sample on and without a jump',
|
||||
acidRender.a.first >= 0 &&
|
||||
acidRender.a.first < 4 &&
|
||||
acidRender.a.last < acidRender.handover &&
|
||||
acidRender.b.first === acidRender.handover &&
|
||||
acidRender.b.last === 0.3 * 48000 - 1 &&
|
||||
acidRender.jump > 0 &&
|
||||
acidRender.jump <= acidRender.step,
|
||||
JSON.stringify(acidRender),
|
||||
"the Acid's worklet sounds as nodeseq's acid built from Web Audio nodes: as loud, as bright, and it ends where that stops",
|
||||
acidCompared.every(
|
||||
(c) =>
|
||||
c.windows.length > 40 &&
|
||||
c.louder < 0.5 &&
|
||||
c.alike > 0.9 &&
|
||||
c.lastHeard[0] > 0.8 &&
|
||||
Math.abs(c.lastHeard[0] - c.lastHeard[1]) < 0.03,
|
||||
),
|
||||
JSON.stringify(acidCompared.map(({ windows, ...rest }) => ({ ...rest, windows: windows.length }))),
|
||||
);
|
||||
|
||||
await evaluate('__gm.sound.stopAll()');
|
||||
@@ -2996,14 +3002,18 @@ export async function sound(t) {
|
||||
);
|
||||
const acidLine = await trace(stats, (s) => s.started >= beforeLine.started + 3 && s.playing === 0);
|
||||
await evaluate('__gm.music.stop()');
|
||||
const slidOf = (n) => acidLine.find((s) => s.started === beforeLine.started + n)?.last?.slid ?? null;
|
||||
const glidedOf = (n) => acidLine.find((s) => s.started === beforeLine.started + n)?.last?.glided ?? null;
|
||||
await press('z', { ctrl: true });
|
||||
const acidGone = await acidGraph();
|
||||
check(
|
||||
'of three Acid notes the one struck where the last ends slides, the one after a gap does not; an undo brings the Laser back',
|
||||
slidOf(1) === 0 && slidOf(2) === 1 && slidOf(3) === 0 && acidGone.types.join() === laser.types.join(),
|
||||
'of three Acid notes the second and third glide from the one before and all end by themselves; an undo brings the Laser back',
|
||||
glidedOf(1) === 0 &&
|
||||
glidedOf(2) === 1 &&
|
||||
glidedOf(3) === 1 &&
|
||||
acidLine.at(-1).playing === 0 &&
|
||||
acidGone.types.join() === laser.types.join(),
|
||||
JSON.stringify({
|
||||
slid: [1, 2, 3].map(slidOf),
|
||||
glided: [1, 2, 3].map(glidedOf),
|
||||
started: acidLine.map((s) => s.started),
|
||||
acidGone: acidGone.types,
|
||||
}),
|
||||
|
||||
Reference in new issue
Block a user