Rebuild the Acid node after the acid voice of nodeseq (#421)
game-maker / gate (push) Successful in 4m23s
game-maker / suite (push) Failing after 18m20s

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:
adminandClaude Opus 5.5 committed 2026-10-13 12:39:01 +02:00
1 parent d844f2734a
commit f92d69d902
15 files changed
+645 -592

No files matched your search

+33 -23
View File
@@ -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,
}),