diff --git a/examples/faust-create-faustnode.js b/examples/faust-create-faustnode.js new file mode 100644 index 0000000..e2d9533 --- /dev/null +++ b/examples/faust-create-faustnode.js @@ -0,0 +1,212 @@ +// Node demo for the `createFaustNode` convenience helper. +// Requires: npm install @grame/faustwasm +// Usage: node examples/faust-create-faustnode.js + +import { fileURLToPath } from 'node:url'; +import { AudioContext, AudioWorkletNode, ScriptProcessorNode } from '../index.mjs'; + +// faustwasm captures AudioWorkletNode at module init, so patch the global first. +if (typeof globalThis.AudioWorkletNode === 'undefined') { + globalThis.AudioWorkletNode = AudioWorkletNode; +} +if (typeof globalThis.ScriptProcessorNode === 'undefined') { + globalThis.ScriptProcessorNode = ScriptProcessorNode; +} + +const { + instantiateFaustModuleFromFile, + LibFaust, + FaustCompiler, + FaustDspGenerator, +} = await import('@grame/faustwasm/dist/esm/index.js'); + +// Pre-seed the compiler promise so createFaustNode works in Node (no window/document). +const libfaustJs = fileURLToPath( + import.meta.resolve('@grame/faustwasm/libfaust-wasm/libfaust-wasm.js'), +); +const libfaustData = libfaustJs.replace(/c?js$/, 'data'); +const libfaustWasm = libfaustJs.replace(/c?js$/, 'wasm'); + +FaustDspGenerator.compilerPromise ??= instantiateFaustModuleFromFile( + libfaustJs, + libfaustData, + libfaustWasm, +).then((module) => new FaustCompiler(new LibFaust(module))); + +const monoCode = ` +import("stdfaust.lib"); +process = os.osc(220) * 0.5; +`; + +const polyCode = ` +declare options "[nvoices:8][midi:on]"; +import("stdfaust.lib"); +freq = hslider("freq", 440, 50, 2000, 1); +gain = hslider("gain", 0.5, 0, 1, 0.01); +gate = button("gate"); +process = gain * os.osc(freq) * gate * 0.25; +`; + +const polyEffectCode = ` +declare options "[nvoices:8][midi:on]"; +import("stdfaust.lib"); +process = pm.clarinet_ui_MIDI; +effect = dm.zita_light; +`; + +const audioContext = new AudioContext({ latencyHint: 'interactive' }); +const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function runMonoPolyTest(modeLabel, sp = false, bufferSize) { + if (sp && typeof audioContext.createScriptProcessor !== 'function') { + console.warn('[faust]', 'ScriptProcessor not supported; skipping test'); + return; + } + + const log = (...args) => console.log('[faust]', ...args); + const error = (...args) => console.error('[faust]', ...args); + + try { + log(`Starting ${modeLabel} mono test for 5 seconds…`); + const generator = new FaustDspGenerator(); + const monoNode = await generator.createFaustNode( + audioContext, + `${modeLabel}_mono_test`, + monoCode, + sp, + bufferSize, + ); + if (!monoNode) { + error(`${modeLabel} mono createFaustNode failed`); + return; + } + monoNode.connect(audioContext.destination); + log( + `${modeLabel} mono running; inputs/outputs =`, + monoNode.numberOfInputs, + monoNode.numberOfOutputs, + ); + + await wait(5000); + monoNode.disconnect(); + log(`${modeLabel} mono stopped after 5 seconds; starting poly test…`); + + const polyNode = await generator.createFaustNode( + audioContext, + `${modeLabel}_poly_test`, + polyCode, + sp, + bufferSize, + ); + if (!polyNode) { + error(`${modeLabel} poly createFaustNode failed`); + return; + } + polyNode.connect(audioContext.destination); + log( + `${modeLabel} poly running; inputs/outputs =`, + polyNode.numberOfInputs, + polyNode.numberOfOutputs, + ); + + const noteSeq = [ + { note: 60, vel: 100, dur: 500 }, + { note: 64, vel: 100, dur: 500 }, + { note: 67, vel: 100, dur: 500 }, + ]; + for (const { note, vel, dur } of noteSeq) { + log(`${modeLabel} poly keyOn note ${note}`); + polyNode.keyOn(0, note, vel); + await wait(dur); + polyNode.keyOff(0, note, 0); + log(`${modeLabel} poly keyOff note ${note}`); + await wait(100); + } + + const chord = [60, 64, 67]; + log(`${modeLabel} poly chord on (C major)`); + chord.forEach((n) => polyNode.keyOn(0, n, 100)); + await wait(2000); + chord.forEach((n) => polyNode.keyOff(0, n, 0)); + log(`${modeLabel} poly chord off`); + polyNode.disconnect(); + log(`${modeLabel} poly stopped`); + } catch (e) { + error(`${modeLabel} test error:`, e); + } +} + +async function runPolyEffectTest(modeLabel, sp = false, bufferSize) { + if (sp && typeof audioContext.createScriptProcessor !== 'function') { + console.warn('[faust]', 'ScriptProcessor not supported; skipping test'); + return; + } + + const log = (...args) => console.log('[faust]', ...args); + const error = (...args) => console.error('[faust]', ...args); + + try { + log(`Starting ${modeLabel} poly+effect test…`); + const generator = new FaustDspGenerator(); + const node = await generator.createFaustNode( + audioContext, + `${modeLabel}_poly_effect_test`, + polyEffectCode, + sp, + bufferSize, + ); + if (!node) { + error(`${modeLabel} poly+effect createFaustNode failed`); + return; + } + node.connect(audioContext.destination); + log( + `${modeLabel} poly+effect running; inputs/outputs =`, + node.numberOfInputs, + node.numberOfOutputs, + ); + + const noteSeq = [ + { note: 60, vel: 100, dur: 600 }, + { note: 64, vel: 100, dur: 600 }, + { note: 67, vel: 100, dur: 600 }, + ]; + for (const { note, vel, dur } of noteSeq) { + log(`${modeLabel} poly+effect keyOn note ${note}`); + node.keyOn(0, note, vel); + await wait(dur); + node.keyOff(0, note, 0); + log(`${modeLabel} poly+effect keyOff note ${note}`); + await wait(150); + } + + const chord = [60, 64, 67]; + log(`${modeLabel} poly+effect chord on (C major)`); + chord.forEach((n) => node.keyOn(0, n, 100)); + await wait(2500); + chord.forEach((n) => node.keyOff(0, n, 0)); + log(`${modeLabel} poly+effect chord off`); + node.disconnect(); + log(`${modeLabel} poly+effect stopped`); + } catch (e) { + error(`${modeLabel} poly+effect test error:`, e); + } +} + +async function main() { + await audioContext.resume(); + + await runMonoPolyTest('AudioWorklet'); + await runPolyEffectTest('AudioWorklet'); + await runMonoPolyTest('ScriptProcessor', true, 1024); + await runPolyEffectTest('ScriptProcessor', true, 1024); +} + +try { + await main(); +} catch (err) { + console.error('[faust]', 'Fatal error:', err); + process.exitCode = 1; +} finally { + await audioContext.close(); +} diff --git a/examples/faust.js b/examples/faust-osc.js similarity index 100% rename from examples/faust.js rename to examples/faust-osc.js diff --git a/package.json b/package.json index e94e158..732d6a3 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "wpt:only": "node ./.scripts/wpt-harness.mjs" }, "devDependencies": { + "@grame/faustwasm": "^0.13.4", "@ircam/eslint-config": "^2.0.0", "@ircam/sc-scheduling": "^1.0.0", "@ircam/sc-utils": "^1.9.0", @@ -70,7 +71,6 @@ "wpt-runner": "^5.0.0" }, "dependencies": { - "@grame/faustwasm": "^0.12.2", "caller": "^1.1.0", "node-fetch": "^3.3.2", "webidl-conversions": "^7.0.0"