diff --git a/examples/faust-bell.js b/examples/faust-bell.js new file mode 100644 index 0000000..bbd973a --- /dev/null +++ b/examples/faust-bell.js @@ -0,0 +1,152 @@ +// Example: French church bell physical model with parameter tweaks. +// Source DSP: https://faustdoc.grame.fr/examples/physicalModeling/#frenchbell +// Requires: npm install @grame/faustwasm +// Usage: node faust-bell.js + +import { fileURLToPath } from 'node:url'; +import { AudioContext, AudioWorkletNode } from '../index.mjs'; + +// Expose AudioWorkletNode globally so faustwasm can patch it into worklet factories. +if (typeof globalThis.AudioWorkletNode === 'undefined') { + globalThis.AudioWorkletNode = AudioWorkletNode; +} + +const { + instantiateFaustModuleFromFile, + LibFaust, + FaustCompiler, + FaustMonoDspGenerator, +} = await import('@grame/faustwasm/dist/esm/index.js'); + +const audioContext = new AudioContext({ latencyHint: 'interactive' }); + +// French bell physical model with the stock UI (strike position, cutoff, sharpness, gain, gate) +// plus a stereo freeverb tail with mix/feedback/damping controls. +const dspCode = ` +declare name "FrenchChurchBell"; +declare description "French church bell physical model."; +declare license "MIT"; +declare copyright "(c)Romain Michon, CCRMA (Stanford University), GRAME"; + +import("stdfaust.lib"); + +mix = hslider("reverb/[0]mix", 0.3, 0, 1, 0.01); +fb1 = hslider("reverb/[1]feedback1", 0.7, 0, 0.95, 0.01); +fb2 = hslider("reverb/[2]feedback2", 0.5, 0, 0.95, 0.01); +damp = hslider("reverb/[3]damp", 0.3, 0, 1, 0.01); +spread = hslider("reverb/[4]spread[samp]", 30, 0, 80, 1); + +dryWet(mix, fx) = (_,_) <: (_,_),fx : *(1-mix), *(1-mix), *(mix), *(mix) : +, +; + +// The bell UI is mono; duplicate to stereo before the wet/dry split. +process = pm.frenchBell_ui <: dryWet(mix, re.stereo_freeverb(fb1, fb2, damp, spread)); +`; + +// Load the Faust toolchain from the wasm distribution. +const faustModule = await instantiateFaustModuleFromFile( + fileURLToPath(import.meta.resolve('@grame/faustwasm/libfaust-wasm/libfaust-wasm.js')), + fileURLToPath(import.meta.resolve('@grame/faustwasm/libfaust-wasm/libfaust-wasm.data')), + fileURLToPath(import.meta.resolve('@grame/faustwasm/libfaust-wasm/libfaust-wasm.wasm')), +); + +const libFaust = new LibFaust(faustModule); +const compiler = new FaustCompiler(libFaust); +const generator = new FaustMonoDspGenerator(); + +const compiled = await generator.compile(compiler, 'faust-bell', dspCode, '-I libraries/'); +if (!compiled) { + throw new Error('Faust compilation failed'); +} + +const faustNode = await generator.createNode(audioContext, 'faust-bell', generator.factory, false); +if (!faustNode) { + throw new Error('Failed to create Faust bell node'); +} + +console.log('Faust bell node created.', faustNode); + +faustNode.connect(audioContext.destination); +faustNode.start(); + +// Gather parameters from the compiled UI and prepare helpers. +const params = faustNode.getParams?.() ?? []; +console.log('Available parameters:', params); + +// Find the first parameter path that contains all substrings (case-insensitive). +const findParam = substrings => { + const match = params.find(p => { + const low = p.toLowerCase(); + return substrings.every(s => low.includes(s)); + }); + if (!match) throw new Error(`Parameter containing "${substrings.join(' & ')}" not found`); + return match; +}; + +const set = (name, value) => { + faustNode.setParamValue(name, value); + console.log(`Set ${name} -> ${value}`); +}; + +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); + +// Map important controls. +const strikePosition = findParam(['strikeposition']); // 0..6 (higher = nearer to rim) +const strikeCutoff = findParam(['strikecutoff']); // Hz +const strikeSharpness = findParam(['strikesharpness']); // 0.01..5 +const gain = findParam(['gain']); // 0..1 +const gate = findParam(['gate']); // trigger (button) + +const rvMix = findParam(['reverb', 'mix']); +const rvFb1 = findParam(['reverb', 'feedback1']); +const rvFb2 = findParam(['reverb', 'feedback2']); +const rvDamp = findParam(['reverb', 'damp']); +const rvSpread = findParam(['reverb', 'spread']); + +// Strike helper: short pulse on the gate param. +const strike = async () => { + set(gate, 1); + await sleep(40); + set(gate, 0); +}; + +// Initialize to a mellow strike. +set(strikePosition, 2); // toward the waist +set(strikeCutoff, 6500); +set(strikeSharpness, 0.6); +set(gain, 0.7); +set(rvMix, 0.35); +set(rvFb1, 0.7); +set(rvFb2, 0.5); +set(rvDamp, 0.35); +set(rvSpread, 35); + +console.log('Playing bell variations...'); +await strike(); + +await sleep(3500); +set(strikePosition, 4); // closer to the rim, brighter partials +set(strikeCutoff, 9000); +set(strikeSharpness, 1.2); +set(gain, 0.9); +set(rvMix, 0.5); +set(rvDamp, 0.25); +set(rvSpread, 45); +await strike(); + +await sleep(3500); +set(strikePosition, 1); // nearer the crown, darker tone +set(strikeCutoff, 4500); +set(strikeSharpness, 0.4); +set(gain, 0.6); +set(rvMix, 0.42); +set(rvFb1, 0.78); +set(rvFb2, 0.55); +set(rvDamp, 0.55); +set(rvSpread, 55); +await strike(); + +await sleep(2500); +console.log('Stopping...'); + +faustNode.stop(); +await audioContext.close(); diff --git a/examples/faust-params.js b/examples/faust-params.js new file mode 100644 index 0000000..a982374 --- /dev/null +++ b/examples/faust-params.js @@ -0,0 +1,95 @@ +// Example: Faust DSP with parameters and runtime tweaks using node-web-audio-api. +// Requires: npm install @grame/faustwasm +// Usage: node faust-params.js + +import { fileURLToPath } from 'node:url'; +import { AudioContext, AudioWorkletNode } from '../index.mjs'; + +// Expose AudioWorkletNode globally so faustwasm can patch it into worklet factories. +if (typeof globalThis.AudioWorkletNode === 'undefined') { + globalThis.AudioWorkletNode = AudioWorkletNode; +} + +const { + instantiateFaustModuleFromFile, + LibFaust, + FaustCompiler, + FaustMonoDspGenerator, +} = await import('@grame/faustwasm/dist/esm/index.js'); + +const audioContext = new AudioContext({ latencyHint: 'interactive' }); + +// Stereo tone with controllable gain, pitch and filter cutoff. +const dspCode = ` +import("stdfaust.lib"); + +gain = hslider("gain[dB]", -6, -24, 6, 0.1) : ba.db2linear; +freq = hslider("freq[Hz]", 220, 50, 2000, 1); +cutoff = hslider("cutoff[Hz]", 800, 200, 4000, 1); + +process = vgroup("faust-params", + os.osc(freq) * gain : fi.lowpass(2, cutoff) +) <: _,_; +`; + +// Load the Faust toolchain from the wasm distribution. +const faustModule = await instantiateFaustModuleFromFile( + fileURLToPath(import.meta.resolve('@grame/faustwasm/libfaust-wasm/libfaust-wasm.js')), + fileURLToPath(import.meta.resolve('@grame/faustwasm/libfaust-wasm/libfaust-wasm.data')), + fileURLToPath(import.meta.resolve('@grame/faustwasm/libfaust-wasm/libfaust-wasm.wasm')), +); + +const libFaust = new LibFaust(faustModule); +const compiler = new FaustCompiler(libFaust); +const generator = new FaustMonoDspGenerator(); + +const compiled = await generator.compile(compiler, 'faust-params', dspCode, '-ftz 2'); +if (!compiled) { + throw new Error('Faust compilation failed'); +} + +const faustNode = await generator.createNode(audioContext); +if (!faustNode) { + throw new Error('Failed to create Faust node'); +} + +console.log('Faust parameterized node created.', faustNode); + +faustNode.connect(audioContext.destination); +faustNode.start(); + +// Helpers to inspect and tweak parameters. The parameter names come from the Faust UI paths. +const params = faustNode.getParams?.() ?? []; +console.log('Available parameters:', params); + +const set = (name, value) => { + faustNode.setParamValue(name, value); + console.log(`Set ${name} -> ${value}`); +}; + +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); + +// Parameter automation demo. +set('/faust-params/gain', -12); +set('/faust-params/freq', 220); +set('/faust-params/cutoff', 800); +set('/faust-params/resonance', 0.9); + +console.log('Sweeping parameters for 6 seconds...'); +await sleep(1500); +set('/faust-params/freq', 440); +set('/faust-params/cutoff', 1200); + +await sleep(1500); +set('/faust-params/gain', -3); +set('/faust-params/resonance', 1.8); + +await sleep(1500); +set('/faust-params/freq', 660); +set('/faust-params/cutoff', 1800); + +await sleep(1500); +console.log('Stopping...'); + +faustNode.stop(); +await audioContext.close(); diff --git a/examples/faust.js b/examples/faust.js new file mode 100644 index 0000000..fb0d7f7 --- /dev/null +++ b/examples/faust.js @@ -0,0 +1,61 @@ + + +// Example: run a simple Faust oscillator graph using node-web-audio-api. +// Requires: npm install @grame/faustwasm +// Usage: node faust.js + +import { fileURLToPath } from 'node:url'; +import { AudioContext, AudioWorkletNode } from '../index.mjs'; + +// Expose AudioWorkletNode globally so faustwasm can patch it into worklet factories. +if (typeof globalThis.AudioWorkletNode === 'undefined') { + globalThis.AudioWorkletNode = AudioWorkletNode; +} + +const { + instantiateFaustModuleFromFile, + LibFaust, + FaustCompiler, + FaustMonoDspGenerator, +} = await import('@grame/faustwasm/dist/esm/index.js'); + +const latencyHint = process.env.WEB_AUDIO_LATENCY === 'playback' ? 'playback' : 'interactive'; +const audioContext = new AudioContext({ latencyHint }); + +// Minimal Faust DSP: two sine oscillators mixed to stereo. +const dspCode = ` +import("stdfaust.lib"); +process = os.osc(440)*0.3, os.osc(800)*0.3; +`; + +// Load the Faust toolchain from the wasm distribution. +const faustModule = await instantiateFaustModuleFromFile( + fileURLToPath(import.meta.resolve('@grame/faustwasm/libfaust-wasm/libfaust-wasm.js')), + fileURLToPath(import.meta.resolve('@grame/faustwasm/libfaust-wasm/libfaust-wasm.data')), + fileURLToPath(import.meta.resolve('@grame/faustwasm/libfaust-wasm/libfaust-wasm.wasm')), +); + +const libFaust = new LibFaust(faustModule); +const compiler = new FaustCompiler(libFaust); +const generator = new FaustMonoDspGenerator(); + +const compiled = await generator.compile(compiler, 'faust-osc', dspCode, '-ftz 2'); +if (!compiled) { + throw new Error('Faust compilation failed'); +} + +const faustNode = await generator.createNode(audioContext); +if (!faustNode) { + throw new Error('Failed to create Faust node'); +} + +console.log('Faust node created successfully.', faustNode); + +faustNode.connect(audioContext.destination); +faustNode.start(); + +console.log('Faust oscillator playing for 4 seconds...'); +await new Promise(resolve => setTimeout(resolve, 4000)); + +faustNode.stop(); +await audioContext.close(); diff --git a/package.json b/package.json index 08b30e1..e94e158 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "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"