├── .gitignore ├── .npmignore ├── COPYING.txt ├── README.md ├── assets └── standalone │ ├── create-node.js │ ├── icon.png │ ├── index-pwa.html │ ├── index-pwa.js │ ├── index-template.html │ ├── index-template.js │ ├── index.html │ ├── index.js │ ├── manifest.json │ └── service-worker.js ├── fileutils.js ├── libfaust-wasm ├── libfaust-wasm.d.cts ├── libfaust-wasm.d.ts ├── libfaust-wasm.data ├── libfaust-wasm.data.d.ts ├── libfaust-wasm.js ├── libfaust-wasm.wasm └── libfaust-wasm.wasm.d.ts ├── package-lock.json ├── package.json ├── postbuild-bundled.js ├── postbuild.js ├── prebuild-bundled.js ├── rsrc ├── overview.png └── overview.xlsx ├── scripts ├── faust2cmajor.js ├── faust2sndfile.js ├── faust2svg.js └── faust2wasm.js ├── src ├── FaustAudioWorkletCommunicator.ts ├── FaustAudioWorkletNode.ts ├── FaustAudioWorkletProcessor.ts ├── FaustCmajor.ts ├── FaustCompiler.ts ├── FaustDspGenerator.ts ├── FaustDspInstance.ts ├── FaustFFTAudioWorkletProcessor.ts ├── FaustOfflineProcessor.ts ├── FaustScriptProcessorNode.ts ├── FaustSensors.ts ├── FaustSvgDiagrams.ts ├── FaustWasmInstantiator.ts ├── FaustWebAudioDsp.ts ├── LibFaust.ts ├── SoundfileReader.ts ├── WavDecoder.ts ├── WavEncoder.ts ├── copyWebStandaloneAssets.d.ts ├── copyWebStandaloneAssets.js ├── exports-bundle.ts ├── exports.ts ├── faust2CmajorFiles.d.ts ├── faust2cmajorFiles.js ├── faust2svgFiles.d.ts ├── faust2svgFiles.js ├── faust2wasmFiles.d.ts ├── faust2wasmFiles.js ├── faust2wavFiles.d.ts ├── faust2wavFiles.js ├── index-bundle-iife.ts ├── index-bundle.ts ├── index.ts ├── instantiateFaustModule.ts ├── instantiateFaustModuleFromFile.ts └── types.ts ├── test ├── clarinet.dsp ├── djembe.dsp ├── faustlive-wasm │ ├── index.css │ ├── index.html │ └── index.js ├── guitar.dsp ├── guitar1.dsp ├── libfaust-in-worklet │ ├── index.html │ └── index.js ├── midi.dsp ├── mono.dsp ├── node │ └── test.js ├── noise.dsp ├── organ.dsp ├── organ1.dsp ├── osc.dsp ├── osc2.dsp ├── osc3.dsp ├── poly.dsp ├── rev.dsp ├── soundfile.dsp ├── soundfile1.dsp └── web │ ├── fft.html │ ├── fft1.html │ ├── fft2.html │ ├── fftw.js │ ├── index.html │ ├── index.js │ ├── kissfft.js │ ├── mono.html │ ├── poly-key.html │ ├── poly.html │ └── soundfile.html └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directories 27 | node_modules 28 | jspm_packages 29 | 30 | # Optional npm cache directory 31 | .npm 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | 36 | # ========================= 37 | # Operating System Files 38 | # ========================= 39 | 40 | # OSX 41 | # ========================= 42 | 43 | .DS_Store 44 | .AppleDouble 45 | .LSOverride 46 | 47 | # Thumbnails 48 | ._* 49 | 50 | # Files that might appear in the root of a volume 51 | .DocumentRevisions-V100 52 | .fseventsd 53 | .Spotlight-V100 54 | .TemporaryItems 55 | .Trashes 56 | .VolumeIcon.icns 57 | 58 | # Directories potentially created on remote AFP share 59 | .AppleDB 60 | .AppleDesktop 61 | Network Trash Folder 62 | Temporary Items 63 | .apdisk 64 | 65 | # Windows 66 | # ========================= 67 | 68 | # Windows image file caches 69 | Thumbs.db 70 | ehthumbs.db 71 | 72 | # Folder config file 73 | Desktop.ini 74 | 75 | # Recycle Bin used on file shares 76 | $RECYCLE.BIN/ 77 | 78 | # Windows Installer files 79 | *.cab 80 | *.msi 81 | *.msm 82 | *.msp 83 | 84 | # Windows shortcuts 85 | *.lnk 86 | 87 | # Local files 88 | local/ 89 | 90 | .vscode/ 91 | 92 | *.wav 93 | test/out/ 94 | dist/ 95 | assets/standalone/faust-ui 96 | assets/standalone/faustwasm 97 | test/faustlive-wasm/faust-ui 98 | test/faustlive-wasm/faustwasm 99 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directories 27 | node_modules 28 | jspm_packages 29 | 30 | # Optional npm cache directory 31 | .npm 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | 36 | # ========================= 37 | # Operating System Files 38 | # ========================= 39 | 40 | # OSX 41 | # ========================= 42 | 43 | .DS_Store 44 | .AppleDouble 45 | .LSOverride 46 | 47 | # Thumbnails 48 | ._* 49 | 50 | # Files that might appear in the root of a volume 51 | .DocumentRevisions-V100 52 | .fseventsd 53 | .Spotlight-V100 54 | .TemporaryItems 55 | .Trashes 56 | .VolumeIcon.icns 57 | 58 | # Directories potentially created on remote AFP share 59 | .AppleDB 60 | .AppleDesktop 61 | Network Trash Folder 62 | Temporary Items 63 | .apdisk 64 | 65 | # Windows 66 | # ========================= 67 | 68 | # Windows image file caches 69 | Thumbs.db 70 | ehthumbs.db 71 | 72 | # Folder config file 73 | Desktop.ini 74 | 75 | # Recycle Bin used on file shares 76 | $RECYCLE.BIN/ 77 | 78 | # Windows Installer files 79 | *.cab 80 | *.msi 81 | *.msm 82 | *.msp 83 | 84 | # Windows shortcuts 85 | *.lnk 86 | 87 | # Local files 88 | local/ 89 | 90 | .vscode/ 91 | 92 | *.wav 93 | test/out/ 94 | -------------------------------------------------------------------------------- /assets/standalone/create-node.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @typedef {{ dspModule: WebAssembly.Module; dspMeta: FaustDspMeta; effectModule?: WebAssembly.Module; effectMeta?: FaustDspMeta; mixerModule?: WebAssembly.Module }} FaustDspDistribution 5 | * @typedef {import("./faustwasm").FaustDspMeta} FaustDspMeta 6 | * @typedef {import("./faustwasm").FaustMonoAudioWorkletNode} FaustMonoAudioWorkletNode 7 | * @typedef {import("./faustwasm").FaustPolyAudioWorkletNode} FaustPolyAudioWorkletNode 8 | * @typedef {import("./faustwasm").FaustMonoScriptProcessorNode} FaustMonoScriptProcessorNode 9 | * @typedef {import("./faustwasm").FaustPolyScriptProcessorNode} FaustPolyScriptProcessorNode 10 | * @typedef {FaustMonoAudioWorkletNode | FaustPolyAudioWorkletNode | FaustMonoScriptProcessorNode | FaustPolyScriptProcessorNode} FaustNode 11 | */ 12 | 13 | /** 14 | * Creates a Faust audio node for use in the Web Audio API. 15 | * 16 | * @param {AudioContext} audioContext - The Web Audio API AudioContext to which the Faust audio node will be connected. 17 | * @param {string} [dspName] - The name of the DSP to be loaded. 18 | * @param {number} [voices] - The number of voices to be used for polyphonic DSPs. 19 | * @param {boolean} [sp] - Whether to create a ScriptProcessorNode instead of an AudioWorkletNode. 20 | * @returns {Promise<{ faustNode: FaustNode | null; dspMeta: FaustDspMeta }>} - An object containing the Faust audio node and the DSP metadata. 21 | */ 22 | const createFaustNode = async (audioContext, dspName = "template", voices = 0, sp = false, bufferSize = 512) => { 23 | // Set to true if the DSP has an effect 24 | const FAUST_DSP_HAS_EFFECT = false; 25 | 26 | // Import necessary Faust modules and data 27 | const { FaustMonoDspGenerator, FaustPolyDspGenerator } = await import("./faustwasm/index.js"); 28 | 29 | // Load DSP metadata from JSON 30 | /** @type {FaustDspMeta} */ 31 | const dspMeta = await (await fetch("./dsp-meta.json")).json(); 32 | 33 | // Compile the DSP module from WebAssembly binary data 34 | const dspModule = await WebAssembly.compileStreaming(await fetch("./dsp-module.wasm")); 35 | 36 | // Create an object representing Faust DSP with metadata and module 37 | /** @type {FaustDspDistribution} */ 38 | const faustDsp = { dspMeta, dspModule }; 39 | 40 | /** @type {FaustNode | null} */ 41 | let faustNode = null; 42 | 43 | // Create either a polyphonic or monophonic Faust audio node based on the number of voices 44 | if (voices > 0) { 45 | 46 | // Try to load optional mixer and effect modules 47 | faustDsp.mixerModule = await WebAssembly.compileStreaming(await fetch("./mixer-module.wasm")); 48 | 49 | if (FAUST_DSP_HAS_EFFECT) { 50 | faustDsp.effectMeta = await (await fetch("./effect-meta.json")).json(); 51 | faustDsp.effectModule = await WebAssembly.compileStreaming(await fetch("./effect-module.wasm")); 52 | } 53 | 54 | // Create a polyphonic Faust audio node 55 | const generator = new FaustPolyDspGenerator(); 56 | faustNode = await generator.createNode( 57 | audioContext, 58 | voices, 59 | dspName, 60 | { module: faustDsp.dspModule, json: JSON.stringify(faustDsp.dspMeta), soundfiles: {} }, 61 | faustDsp.mixerModule, 62 | faustDsp.effectModule ? { module: faustDsp.effectModule, json: JSON.stringify(faustDsp.effectMeta), soundfiles: {} } : undefined, 63 | sp, 64 | bufferSize 65 | ); 66 | } else { 67 | // Create a standard Faust audio node 68 | const generator = new FaustMonoDspGenerator(); 69 | faustNode = await generator.createNode( 70 | audioContext, 71 | dspName, 72 | { module: faustDsp.dspModule, json: JSON.stringify(faustDsp.dspMeta), soundfiles: {} }, 73 | sp, 74 | bufferSize 75 | ); 76 | } 77 | 78 | // Return an object with the Faust audio node and the DSP metadata 79 | return { faustNode, dspMeta }; 80 | } 81 | 82 | /** 83 | * Connects an audio input stream to a Faust WebAudio node. 84 | * 85 | * @param {AudioContext} audioContext - The Web Audio API AudioContext to which the Faust audio node is connected. 86 | * @param {string} id - The ID of the audio input device to connect. 87 | * @param {FaustNode} faustNode - The Faust audio node to which the audio input stream will be connected. 88 | * @param {MediaStreamAudioSourceNode} oldInputStreamNode - The old audio input stream node to be disconnected from the Faust audio node. 89 | * @returns {Promise} - The new audio input stream node connected to the Faust audio node. 90 | */ 91 | async function connectToAudioInput(audioContext, id, faustNode, oldInputStreamNode) { 92 | // Create an audio input stream node 93 | const constraints = { 94 | audio: { 95 | echoCancellation: false, 96 | noiseSuppression: false, 97 | autoGainControl: false, 98 | deviceId: id ? { exact: id } : undefined, 99 | }, 100 | }; 101 | // Get the audio input stream 102 | const stream = await navigator.mediaDevices.getUserMedia(constraints); 103 | if (stream) { 104 | if (oldInputStreamNode) oldInputStreamNode.disconnect(); 105 | const newInputStreamNode = audioContext.createMediaStreamSource(stream); 106 | newInputStreamNode.connect(faustNode); 107 | return newInputStreamNode; 108 | } else { 109 | return oldInputStreamNode; 110 | } 111 | }; 112 | 113 | /** 114 | * Creates a Faust UI for a Faust audio node. 115 | * 116 | * @param {FaustAudioWorkletNode} faustNode 117 | */ 118 | async function createFaustUI(divFaustUI, faustNode) { 119 | const { FaustUI } = await import("./faust-ui/index.js"); 120 | const $container = document.createElement("div"); 121 | $container.style.margin = "0"; 122 | $container.style.position = "absolute"; 123 | $container.style.overflow = "auto"; 124 | $container.style.display = "flex"; 125 | $container.style.flexDirection = "column"; 126 | $container.style.width = "100%"; 127 | $container.style.height = "100%"; 128 | divFaustUI.appendChild($container); 129 | const faustUI = new FaustUI({ 130 | ui: faustNode.getUI(), 131 | root: $container, 132 | listenWindowMessage: false, 133 | listenWindowResize: true, 134 | }); 135 | faustUI.paramChangeByUI = (path, value) => faustNode.setParamValue(path, value); 136 | faustNode.setOutputParamHandler((path, value) => faustUI.paramChangeByDSP(path, value)); 137 | $container.style.minWidth = `${faustUI.minWidth}px`; 138 | $container.style.minHeight = `${faustUI.minHeight}px`; 139 | faustUI.resize(); 140 | }; 141 | 142 | /** 143 | * Request permission to use motion and orientation sensors. 144 | */ 145 | async function requestPermissions() { 146 | 147 | // Explicitly request permission on iOS before calling startSensors() 148 | if (typeof window.DeviceMotionEvent !== "undefined" && typeof window.DeviceMotionEvent.requestPermission === "function") { 149 | try { 150 | const permissionState = await window.DeviceMotionEvent.requestPermission(); 151 | if (permissionState !== "granted") { 152 | console.warn("Motion sensor permission denied."); 153 | } else { 154 | console.log("Motion sensor permission granted."); 155 | } 156 | } catch (error) { 157 | console.error("Error requesting motion sensor permission:", error); 158 | } 159 | } 160 | 161 | if (typeof window.DeviceOrientationEvent !== "undefined" && typeof window.DeviceOrientationEvent.requestPermission === "function") { 162 | try { 163 | const permissionState = await window.DeviceOrientationEvent.requestPermission(); 164 | if (permissionState !== "granted") { 165 | console.warn("Orientation sensor permission denied."); 166 | } else { 167 | console.log("Orientation sensor permission granted."); 168 | } 169 | } catch (error) { 170 | console.error("Error requesting orientation sensor permission:", error); 171 | } 172 | } 173 | } 174 | 175 | /** 176 | * Key2Midi: maps keyboard input to MIDI messages. 177 | */ 178 | class Key2Midi { 179 | static KEY_MAP = { 180 | a: 0, w: 1, s: 2, e: 3, d: 4, f: 5, t: 6, g: 7, 181 | y: 8, h: 9, u: 10, j: 11, k: 12, o: 13, l: 14, p: 15, ";": 16, 182 | z: "PREV", x: "NEXT", c: "VELDOWN", v: "VELUP" 183 | }; 184 | 185 | constructor({ keyMap = Key2Midi.KEY_MAP, offset = 60, velocity = 100, handler = console.log } = {}) { 186 | this.keyMap = keyMap; 187 | this.offset = offset; 188 | this.velocity = velocity; 189 | this.velMap = [20, 40, 60, 80, 100, 127]; 190 | this.handler = handler; 191 | this.pressed = {}; 192 | 193 | this.onKeyDown = this.onKeyDown.bind(this); 194 | this.onKeyUp = this.onKeyUp.bind(this); 195 | } 196 | 197 | start() { 198 | window.addEventListener("keydown", this.onKeyDown); 199 | window.addEventListener("keyup", this.onKeyUp); 200 | } 201 | 202 | stop() { 203 | window.removeEventListener("keydown", this.onKeyDown); 204 | window.removeEventListener("keyup", this.onKeyUp); 205 | } 206 | 207 | onKeyDown(e) { 208 | const key = e.key.toLowerCase(); 209 | if (this.pressed[key]) return; 210 | this.pressed[key] = true; 211 | 212 | const val = this.keyMap[key]; 213 | if (typeof val === "number") { 214 | const note = val + this.offset; 215 | this.handler([0x90, note, this.velocity]); 216 | } else if (val === "PREV") { 217 | this.offset -= 1; 218 | } else if (val === "NEXT") { 219 | this.offset += 1; 220 | } else if (val === "VELDOWN") { 221 | const idx = Math.max(0, this.velMap.indexOf(this.velocity) - 1); 222 | this.velocity = this.velMap[idx]; 223 | } else if (val === "VELUP") { 224 | const idx = Math.min(this.velMap.length - 1, this.velMap.indexOf(this.velocity) + 1); 225 | this.velocity = this.velMap[idx]; 226 | } 227 | } 228 | 229 | onKeyUp(e) { 230 | const key = e.key.toLowerCase(); 231 | const val = this.keyMap[key]; 232 | if (typeof val === "number") { 233 | const note = val + this.offset; 234 | this.handler([0x80, note, this.velocity]); 235 | } 236 | delete this.pressed[key]; 237 | } 238 | } 239 | 240 | /** 241 | * Creates a Key2Midi instance. 242 | * 243 | * @param {function} handler - The function to handle MIDI messages. 244 | * @returns {Key2Midi} - The Key2Midi instance. 245 | */ 246 | function createKey2MIDI(handler) { 247 | return new Key2Midi({ handler: handler }); 248 | } 249 | 250 | // Export the functions 251 | export { createFaustNode, createFaustUI, createKey2MIDI, connectToAudioInput, requestPermissions }; 252 | 253 | -------------------------------------------------------------------------------- /assets/standalone/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grame-cncm/faustwasm/1e4f94abc4c4ad6319be1e47207eeedce823badf/assets/standalone/icon.png -------------------------------------------------------------------------------- /assets/standalone/index-pwa.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Faust DSP 11 | 65 | 66 | 67 | 68 | 69 |
70 |
71 |
72 |
73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /assets/standalone/index-pwa.js: -------------------------------------------------------------------------------- 1 | // Set to > 0 if the DSP is polyphonic 2 | const FAUST_DSP_VOICES = 0; 3 | 4 | /** 5 | * @typedef {import("./faustwasm").FaustAudioWorkletNode} FaustAudioWorkletNode 6 | * @typedef {import("./faustwasm").FaustDspMeta} FaustDspMeta 7 | * @typedef {import("./faustwasm").FaustUIDescriptor} FaustUIDescriptor 8 | * @typedef {import("./faustwasm").FaustUIGroup} FaustUIGroup 9 | * @typedef {import("./faustwasm").FaustUIItem} FaustUIItem 10 | */ 11 | 12 | /** 13 | * Registers the service worker. 14 | */ 15 | if ("serviceWorker" in navigator) { 16 | window.addEventListener("load", () => { 17 | navigator.serviceWorker.register("./service-worker.js") 18 | .then(reg => console.log("Service Worker registered", reg)) 19 | .catch(err => console.log("Service Worker registration failed", err)); 20 | }); 21 | } 22 | 23 | /** @type {HTMLDivElement} */ 24 | const $divFaustUI = document.getElementById("div-faust-ui"); 25 | 26 | /** @type {typeof AudioContext} */ 27 | const AudioCtx = window.AudioContext || window.webkitAudioContext; 28 | const audioContext = new AudioCtx({ latencyHint: 0.00001 }); 29 | audioContext.destination.channelInterpretation = "discrete"; 30 | audioContext.suspend(); 31 | 32 | // Declare faustNode as a global variable 33 | let faustNode; 34 | 35 | // Called at load time 36 | (async () => { 37 | 38 | // Import the create-node module 39 | const { createFaustNode, createFaustUI } = await import("./create-node.js"); 40 | 41 | // To test the ScriptProcessorNode mode 42 | // const result = await createFaustNode(audioContext, "FAUST_DSP_NAME", FAUST_DSP_VOICES, true, 512); 43 | const result = await createFaustNode(audioContext, "FAUST_DSP_NAME", FAUST_DSP_VOICES); 44 | faustNode = result.faustNode; // Assign to the global variable 45 | if (!faustNode) throw new Error("Faust DSP not compiled"); 46 | 47 | // Create the Faust UI 48 | await createFaustUI($divFaustUI, faustNode); 49 | 50 | })(); 51 | 52 | // Synchronous function to resume AudioContext, to be called first in the synchronous event listener 53 | function resumeAudioContext() { 54 | if (audioContext.state === 'suspended') { 55 | audioContext.resume().then(() => { 56 | console.log('AudioContext resumed successfully'); 57 | }).catch(error => { 58 | console.error('Error when resuming AudioContext:', error); 59 | }); 60 | } 61 | } 62 | 63 | // Function to start MIDI 64 | function startMIDI() { 65 | // Check if the browser supports the Web MIDI API 66 | if (navigator.requestMIDIAccess) { 67 | navigator.requestMIDIAccess().then( 68 | midiAccess => { 69 | console.log("MIDI Access obtained."); 70 | for (let input of midiAccess.inputs.values()) { 71 | input.onmidimessage = (event) => faustNode.midiMessage(event.data); 72 | console.log(`Connected to input: ${input.name}`); 73 | } 74 | }, 75 | () => console.error("Failed to access MIDI devices.") 76 | ); 77 | } else { 78 | console.log("Web MIDI API is not supported in this browser."); 79 | } 80 | } 81 | 82 | // Function to stop MIDI 83 | function stopMIDI() { 84 | // Check if the browser supports the Web MIDI API 85 | if (navigator.requestMIDIAccess) { 86 | navigator.requestMIDIAccess().then( 87 | midiAccess => { 88 | console.log("MIDI Access obtained."); 89 | for (let input of midiAccess.inputs.values()) { 90 | input.onmidimessage = null; 91 | console.log(`Disconnected from input: ${input.name}`); 92 | } 93 | }, 94 | () => console.error("Failed to access MIDI devices.") 95 | ); 96 | } else { 97 | console.log("Web MIDI API is not supported in this browser."); 98 | } 99 | } 100 | 101 | let sensorHandlersBound = false; 102 | let midiHandlersBound = false; 103 | 104 | // Keyboard to MIDI handing 105 | let keyboard2MIDI = null; 106 | 107 | async function startKeyboard2MIDI() { 108 | // Import the create-node module 109 | const { createKey2MIDI } = await import("./create-node.js"); 110 | 111 | keyboard2MIDI = createKey2MIDI((event) => faustNode.midiMessage(event)); 112 | keyboard2MIDI.start(); 113 | } 114 | 115 | function stopKeyboard2MIDI() { 116 | keyboard2MIDI.stop(); 117 | keyboard2MIDI = null; 118 | } 119 | 120 | // Function to activate MIDI and Sensors on user interaction 121 | async function activateMIDISensors() { 122 | 123 | // Import the create-node module 124 | const { connectToAudioInput, requestPermissions } = await import("./create-node.js"); 125 | 126 | // Request permission for sensors 127 | await requestPermissions(); 128 | 129 | // Activate sensor listeners 130 | if (!sensorHandlersBound) { 131 | await faustNode.startSensors(); 132 | sensorHandlersBound = true; 133 | } 134 | 135 | // Initialize the MIDI setup 136 | if (!midiHandlersBound) { 137 | startMIDI(); 138 | await startKeyboard2MIDI(); 139 | midiHandlersBound = true; 140 | } 141 | 142 | // Connect the Faust node to the audio output 143 | faustNode.connect(audioContext.destination); 144 | 145 | // Connect the Faust node to the audio input 146 | if (faustNode.numberOfInputs > 0) { 147 | await connectToAudioInput(audioContext, null, faustNode, null); 148 | } 149 | 150 | // Resume the AudioContext 151 | if (audioContext.state === 'suspended') { 152 | await audioContext.resume(); 153 | } 154 | } 155 | 156 | // Function to suspend AudioContext, deactivate MIDI and Sensors on user interaction 157 | async function deactivateAudioMIDISensors() { 158 | 159 | // Suspend the AudioContext 160 | if (audioContext.state === 'running') { 161 | await audioContext.suspend(); 162 | } 163 | 164 | // Deactivate sensor listeners 165 | if (sensorHandlersBound) { 166 | faustNode.stopSensors(); 167 | sensorHandlersBound = false; 168 | } 169 | 170 | // Deactivate the MIDI setup 171 | if (midiHandlersBound && FAUST_DSP_VOICES > 0) { 172 | stopMIDI(); 173 | stopKeyboard2MIDI(); 174 | midiHandlersBound = false; 175 | } 176 | } 177 | 178 | // Event listener to handle user interaction 179 | function handleUserInteraction() { 180 | 181 | // Resume AudioContext synchronously 182 | resumeAudioContext(); 183 | 184 | // Launch the activation of MIDI and Sensors 185 | activateMIDISensors().catch(error => { 186 | console.error('Error when activating audio, MIDI and sensors:', error); 187 | }); 188 | } 189 | 190 | // Activate AudioContext, MIDI and Sensors on user interaction 191 | window.addEventListener('click', handleUserInteraction); 192 | window.addEventListener('touchstart', handleUserInteraction); 193 | 194 | // Deactivate AudioContext, MIDI and Sensors on user interaction 195 | window.addEventListener('visibilitychange', function () { 196 | if (window.visibilityState === 'hidden') { 197 | deactivateAudioMIDISensors(); 198 | } 199 | }); 200 | 201 | 202 | -------------------------------------------------------------------------------- /assets/standalone/index-template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Faust DSP 6 | 7 | 8 | 9 |
10 | 11 |
12 |
13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /assets/standalone/index-template.js: -------------------------------------------------------------------------------- 1 | // Set to > 0 if the DSP is polyphonic 2 | const FAUST_DSP_VOICES = 0; 3 | 4 | // Declare faustNode as a global variable 5 | let faustNode; 6 | 7 | // Create audio context activation button 8 | /** @type {HTMLButtonElement} */ 9 | const $buttonDsp = document.getElementById("button-dsp"); 10 | 11 | // Create audio context 12 | const AudioCtx = window.AudioContext || window.webkitAudioContext; 13 | const audioContext = new AudioCtx({ latencyHint: 0.00001 }); 14 | 15 | // Activate AudioContext and Sensors on user interaction 16 | $buttonDsp.disabled = true; 17 | let sensorHandlersBound = false; 18 | $buttonDsp.onclick = async () => { 19 | 20 | // Import the requestPermissions function 21 | const { requestPermissions } = await import("./create-node.js"); 22 | 23 | // Request permission for sensors 24 | await requestPermissions(); 25 | 26 | // Activate sensor listeners 27 | if (!sensorHandlersBound) { 28 | await faustNode.startSensors(); 29 | sensorHandlersBound = true; 30 | } 31 | 32 | // Activate or suspend the AudioContext 33 | if (audioContext.state === "running") { 34 | $buttonDsp.textContent = "Suspended"; 35 | await audioContext.suspend(); 36 | } else if (audioContext.state === "suspended") { 37 | $buttonDsp.textContent = "Running"; 38 | await audioContext.resume(); 39 | if (FAUST_DSP_VOICES) play(faustNode); 40 | } 41 | } 42 | 43 | // Called at load time 44 | (async () => { 45 | 46 | const { createFaustNode, connectToAudioInput } = await import("./create-node.js"); 47 | 48 | const play = (node) => { 49 | node.keyOn(0, 60, 100); 50 | setTimeout(() => node.keyOn(0, 64, 100), 1000); 51 | setTimeout(() => node.keyOn(0, 67, 100), 2000); 52 | setTimeout(() => node.allNotesOff(), 5000); 53 | setTimeout(() => play(node), 7000); 54 | } 55 | 56 | // Create Faust node 57 | const result = await createFaustNode(audioContext, "FAUST_DSP_NAME", FAUST_DSP_VOICES); 58 | faustNode = result.faustNode; // Assign to the global variable 59 | if (!faustNode) throw new Error("Faust DSP not compiled"); 60 | 61 | // Connect the Faust node to the audio output 62 | faustNode.connect(audioContext.destination); 63 | 64 | // Connect the Faust node to the audio input 65 | if (faustNode.getNumInputs() > 0) { 66 | await connectToAudioInput(audioContext, null, faustNode, null); 67 | } 68 | 69 | // Create Faust node activation button 70 | $buttonDsp.disabled = false; 71 | 72 | // Set page title to the DSP name 73 | document.title = name; 74 | 75 | })(); 76 | -------------------------------------------------------------------------------- /assets/standalone/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Faust DSP 11 | 65 | 66 | 67 | 68 | 69 |
70 | 71 | Select Audio Input Device 72 | 73 | 74 | 75 | Select MIDI Input Device 76 | 77 | 78 | 79 | 80 | 81 |
82 |
83 |
84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /assets/standalone/index.js: -------------------------------------------------------------------------------- 1 | // Set to > 0 if the DSP is polyphonic 2 | const FAUST_DSP_VOICES = 0; 3 | 4 | /** 5 | * @typedef {import("./faustwasm").FaustAudioWorkletNode} FaustAudioWorkletNode 6 | * @typedef {import("./faustwasm").FaustDspMeta} FaustDspMeta 7 | * @typedef {import("./faustwasm").FaustUIDescriptor} FaustUIDescriptor 8 | * @typedef {import("./faustwasm").FaustUIGroup} FaustUIGroup 9 | * @typedef {import("./faustwasm").FaustUIItem} FaustUIItem 10 | */ 11 | 12 | /** 13 | * Registers the service worker. 14 | */ 15 | if ("serviceWorker" in navigator) { 16 | window.addEventListener("load", () => { 17 | navigator.serviceWorker.register("./service-worker.js") 18 | .then(reg => console.log("Service Worker registered", reg)) 19 | .catch(err => console.log("Service Worker registration failed", err)); 20 | }); 21 | } 22 | 23 | /** @type {HTMLSpanElement} */ 24 | const $spanAudioInput = document.getElementById("audio-input"); 25 | /** @type {HTMLSpanElement} */ 26 | const $spanMidiInput = document.getElementById("midi-input"); 27 | /** @type {HTMLSelectElement} */ 28 | const $selectAudioInput = document.getElementById("select-audio-input"); 29 | /** @type {HTMLSelectElement} */ 30 | const $selectMidiInput = document.getElementById("select-midi-input"); 31 | /** @type {HTMLSelectElement} */ 32 | const $buttonDsp = document.getElementById("button-dsp"); 33 | /** @type {HTMLDivElement} */ 34 | const $divFaustUI = document.getElementById("div-faust-ui"); 35 | 36 | /** @type {typeof AudioContext} */ 37 | const AudioCtx = window.AudioContext || window.webkitAudioContext; 38 | const audioContext = new AudioCtx({ latencyHint: 0.00001 }); 39 | audioContext.destination.channelInterpretation = "discrete"; 40 | audioContext.suspend(); 41 | 42 | $buttonDsp.disabled = true; 43 | 44 | // Declare faustNode as a global variable 45 | let faustNode; 46 | 47 | // Called at load time 48 | (async () => { 49 | 50 | const { createFaustNode, createKey2MIDI, connectToAudioInput, createFaustUI } = await import("./create-node.js"); 51 | 52 | /** 53 | * @param {FaustAudioWorkletNode} faustNode 54 | */ 55 | const buildAudioDeviceMenu = async (faustNode) => { 56 | 57 | let inputStreamNode = null; 58 | const handleDeviceChange = async () => { 59 | const devicesInfo = await navigator.mediaDevices.enumerateDevices(); 60 | $selectAudioInput.innerHTML = ""; 61 | devicesInfo.forEach((deviceInfo, i) => { 62 | const { kind, deviceId, label } = deviceInfo; 63 | if (kind === "audioinput") { 64 | const option = new Option(label || `microphone ${i + 1}`, deviceId); 65 | $selectAudioInput.add(option); 66 | } 67 | }); 68 | } 69 | await handleDeviceChange(); 70 | navigator.mediaDevices.addEventListener("devicechange", handleDeviceChange) 71 | $selectAudioInput.onchange = async () => { 72 | const id = $selectAudioInput.value; 73 | if (faustNode.getNumInputs() > 0) { 74 | inputStreamNode = await connectToAudioInput(audioContext, id, faustNode, inputStreamNode); 75 | } 76 | }; 77 | 78 | // Connect to the default audio input device 79 | if (faustNode.getNumInputs() > 0) { 80 | inputStreamNode = await connectToAudioInput(audioContext, null, faustNode, inputStreamNode); 81 | } 82 | }; 83 | 84 | /** 85 | * @param {FaustAudioWorkletNode} faustNode 86 | */ 87 | const buildMidiDeviceMenu = async (faustNode) => { 88 | 89 | // Keyboard to MIDI handling - create but don't start yet 90 | let keyboard2MIDI = createKey2MIDI((event) => faustNode.midiMessage(event)); 91 | let isKeyboardActive = false; 92 | 93 | const midiAccess = await navigator.requestMIDIAccess(); 94 | /** @type {WebMidi.MIDIInput} */ 95 | let currentInput; 96 | /** 97 | * @param {WebMidi.MIDIMessageEvent} e 98 | */ 99 | const handleMidiMessage = e => faustNode.midiMessage(e.data); 100 | const handleStateChange = () => { 101 | const { inputs } = midiAccess; 102 | // Check if we need to rebuild the menu 103 | const expectedOptions = inputs.size + 2; // +1 for "Select..." and +1 for "Keyboard" 104 | if ($selectMidiInput.options.length === expectedOptions) return; 105 | 106 | if (currentInput) currentInput.removeEventListener("midimessage", handleMidiMessage); 107 | $selectMidiInput.innerHTML = ''; 108 | 109 | // Add computer keyboard option 110 | const keyboardOption = new Option("Computer Keyboard", "Computer Keyboard"); 111 | $selectMidiInput.add(keyboardOption); 112 | 113 | // Add MIDI device options 114 | inputs.forEach((midiInput) => { 115 | const { name, id } = midiInput; 116 | const option = new Option(name, id); 117 | $selectMidiInput.add(option); 118 | }); 119 | }; 120 | handleStateChange(); 121 | midiAccess.addEventListener("statechange", handleStateChange); 122 | $selectMidiInput.onchange = () => { 123 | // Disconnect previous MIDI input 124 | if (currentInput) currentInput.removeEventListener("midimessage", handleMidiMessage); 125 | currentInput = null; 126 | 127 | // Stop Computer Keyboard if it was active 128 | if (isKeyboardActive) { 129 | keyboard2MIDI.stop(); 130 | isKeyboardActive = false; 131 | } 132 | 133 | const selectedValue = $selectMidiInput.value; 134 | 135 | if (selectedValue === "Computer Keyboard") { 136 | // Activate keyboard MIDI 137 | keyboard2MIDI.start(); 138 | isKeyboardActive = true; 139 | } else { 140 | // Activate selected MIDI device 141 | currentInput = midiAccess.inputs.get(selectedValue); 142 | if (currentInput) { 143 | currentInput.addEventListener("midimessage", handleMidiMessage); 144 | } 145 | } 146 | }; 147 | }; 148 | 149 | // To test the ScriptProcessorNode mode 150 | // const { faustNode, dspMeta: { name } } = await createFaustNode(audioContext, "FAUST_DSP_NAME", FAUST_DSP_VOICES, true); 151 | const result = await createFaustNode(audioContext, "FAUST_DSP_NAME", FAUST_DSP_VOICES); 152 | faustNode = result.faustNode; // Assign to the global variable 153 | if (!faustNode) throw new Error("Faust DSP not compiled"); 154 | 155 | // Create the Faust UI 156 | await createFaustUI($divFaustUI, faustNode); 157 | 158 | // Connect the Faust node to the audio output 159 | faustNode.connect(audioContext.destination); 160 | 161 | // Build the audio device menu 162 | if (faustNode.numberOfInputs > 0) await buildAudioDeviceMenu(faustNode); 163 | else $spanAudioInput.hidden = true; 164 | 165 | // Build the MIDI device menu 166 | if (navigator.requestMIDIAccess) await buildMidiDeviceMenu(faustNode); 167 | else $spanMidiInput.hidden = true; 168 | 169 | })(); 170 | 171 | // Set the title and enable the DSP button 172 | $buttonDsp.disabled = false; 173 | document.title = name; 174 | let sensorHandlersBound = false; 175 | 176 | // Activate AudioContext and Sensors on user interaction 177 | $buttonDsp.onclick = async () => { 178 | 179 | // Import the requestPermissions function 180 | const { requestPermissions } = await import("./create-node.js"); 181 | 182 | // Request permission for sensors 183 | await requestPermissions(); 184 | 185 | // Activate sensor listeners 186 | if (!sensorHandlersBound) { 187 | await faustNode.startSensors(); 188 | sensorHandlersBound = true; 189 | } 190 | 191 | // Activate or suspend the AudioContext 192 | if (audioContext.state === "running") { 193 | $buttonDsp.textContent = "Suspended"; 194 | await audioContext.suspend(); 195 | } else if (audioContext.state === "suspended") { 196 | $buttonDsp.textContent = "Running"; 197 | await audioContext.resume(); 198 | } 199 | } -------------------------------------------------------------------------------- /assets/standalone/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Faust FAUST_DSP_NAME", 3 | "short_name": "FAUST_DSP_NAME", 4 | "start_url": "./index.html", 5 | "description": "Progressive Web App for FAUST_DSP_NAME", 6 | "display": "standalone", 7 | "background_color": "#ffffff", 8 | "theme_color": "#000000", 9 | "orientation": "portrait", 10 | "icons": [ 11 | { 12 | "src": "./icon.png", 13 | "sizes": "390x390", 14 | "type": "image/png" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /assets/standalone/service-worker.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // Set to > 0 if the DSP is polyphonic 4 | const FAUST_DSP_VOICES = 0; 5 | // Set to true if the DSP has an effect 6 | const FAUST_DSP_HAS_EFFECT = false; 7 | 8 | const CACHE_NAME = "FAUST_DSP_NAME_VERSION_DATE"; // Cache name with versioning 9 | 10 | /** 11 | * List of essential resources required for the **Mono DSP** version of the application. 12 | * 13 | * - These files are cached to enable offline functionality and improve loading speed. 14 | * - Includes the main HTML, JavaScript, and CSS files required for the app. 15 | * - Contains Faust-related files needed for DSP processing. 16 | */ 17 | const MONO_RESOURCES = [ 18 | "./index.html", 19 | "./index.js", 20 | "./create-node.js", 21 | "./faust-ui/index.js", 22 | "./faust-ui/index.css", 23 | "./faustwasm/index.js", 24 | "./dsp-module.wasm", 25 | "./dsp-meta.json" 26 | ]; 27 | 28 | /** 29 | * List of resources for the **Polyphonic DSP** version of the application. 30 | * 31 | * - Extends the mono resource list by adding a **mixer module**. 32 | * - The mixer module is required to handle multiple simultaneous voices used in a polyphonic instrument. 33 | */ 34 | const POLY_RESOURCES = [ 35 | ...MONO_RESOURCES, 36 | "./mixer-module.wasm", 37 | ]; 38 | 39 | /** 40 | * List of resources for the **Polyphonic DSP with Effects** version. 41 | * 42 | * - Extends the polyphonic resource list by adding an **effect module**. 43 | * - The effect module allows applying audio effects to the polyphonic instrument. 44 | */ 45 | const POLY_EFFECT_RESOURCES = [ 46 | ...POLY_RESOURCES, 47 | "./effect-module.wasm", 48 | "./effect-meta.json", 49 | ]; 50 | 51 | /** @type {ServiceWorkerGlobalScope} */ 52 | const serviceWorkerGlobalScope = self; 53 | 54 | /** 55 | * Install the service worker, cache essential resources, and prepare for immediate activation. 56 | * 57 | * - Opens the cache and stores required assets based on the app's configuration. 58 | * - Ensures resources are preloaded for offline access. 59 | */ 60 | serviceWorkerGlobalScope.addEventListener("install", (event) => { 61 | console.log("Service worker installed"); 62 | event.waitUntil((async () => { 63 | const cache = await caches.open(CACHE_NAME); 64 | const resources = (FAUST_DSP_VOICES && FAUST_DSP_HAS_EFFECT) ? POLY_EFFECT_RESOURCES : (FAUST_DSP_VOICES ? POLY_RESOURCES : MONO_RESOURCES); 65 | try { 66 | return cache.addAll(resources); 67 | } catch (error) { 68 | console.error("Failed to cache resources during install:", error); 69 | } 70 | })()); 71 | }); 72 | 73 | /** 74 | * Handles the activation of the Service Worker. 75 | * 76 | * - Claims control over all clients immediately, bypassing the default behavior that 77 | * requires a page reload before the new service worker takes effect. 78 | * - Once claimed, it finds all active window clients and reloads them to ensure they are 79 | * controlled by the latest version of the service worker. 80 | * - This approach ensures that updates to the service worker take effect immediately 81 | * across all open pages, preventing potential inconsistencies. 82 | */ 83 | serviceWorkerGlobalScope.addEventListener("activate", (event) => { 84 | console.log("Service worker activated"); 85 | event.waitUntil( 86 | clients.claim().then(() => { 87 | return clients.matchAll({ type: "window" }).then((clients) => { 88 | clients.forEach((client) => { 89 | client.navigate(client.url); 90 | }); 91 | }); 92 | }) 93 | ); 94 | }); 95 | 96 | /** 97 | * Adjusts response headers to enforce Cross-Origin Opener Policy (COOP) and Cross-Origin Embedder Policy (COEP). 98 | * 99 | * - Ensures that the response is served with `Cross-Origin-Opener-Policy: same-origin` 100 | * and `Cross-Origin-Embedder-Policy: require-corp`. 101 | * - Required for enabling **cross-origin isolated** environments in web applications. 102 | * - Necessary for features like **SharedArrayBuffer**, WebAssembly threads, and 103 | * high-performance APIs that require isolation. 104 | * - Creates a new `Response` object with the modified headers while preserving 105 | * the original response body and status. 106 | * 107 | * @param {Response} response - The original HTTP response object. 108 | * @returns {Response} A new response with updated security headers. 109 | */ 110 | const getCrossOriginIsolatedResponse = (response) => { 111 | // Modify headers to include COOP & COEP 112 | const headers = new Headers(response.headers); 113 | headers.set("Cross-Origin-Opener-Policy", "same-origin"); 114 | headers.set("Cross-Origin-Embedder-Policy", "require-corp"); 115 | 116 | // Create a new response with the modified headers 117 | const modifiedResponse = new Response(response.body, { 118 | status: response.status, 119 | statusText: response.statusText, 120 | headers 121 | }); 122 | 123 | return modifiedResponse; 124 | }; 125 | 126 | /** 127 | * Intercepts fetch requests and enforces COOP and COEP headers for security. 128 | * 129 | * - Checks if the requested resource is available in the cache: 130 | * - If found, returns a cached response with `Cross-Origin-Opener-Policy` and `Cross-Origin-Embedder-Policy` headers. 131 | * - If not found, fetches the resource from the network, applies COOP/COEP headers, and caches the response (for GET requests). 132 | * - Ensures cross-origin isolation, required for APIs like `SharedArrayBuffer` and WebAssembly threading. 133 | * - Handles network errors gracefully by returning a 503 "Service Unavailable" response when needed. 134 | * 135 | * @param {FetchEvent} event - The fetch event triggered by the browser. 136 | */ 137 | serviceWorkerGlobalScope.addEventListener("fetch", (event) => { 138 | 139 | event.respondWith((async () => { 140 | const cache = await caches.open(CACHE_NAME); 141 | const cachedResponse = await cache.match(event.request); 142 | 143 | if (cachedResponse) { 144 | return getCrossOriginIsolatedResponse(cachedResponse); 145 | } else { 146 | try { 147 | const fetchResponse = await fetch(event.request); 148 | 149 | if (event.request.method === "GET" && fetchResponse && fetchResponse.status === 200 && fetchResponse.type === "basic") { 150 | const modifiedResponse = getCrossOriginIsolatedResponse(fetchResponse); 151 | // Store the modified response in the cache 152 | await cache.put(event.request, modifiedResponse.clone()); 153 | // Return the modified response to the browser 154 | return modifiedResponse; 155 | } 156 | 157 | return fetchResponse; 158 | } catch (error) { 159 | console.error("Network access error", error); 160 | return new Response("Network error", { status: 503, statusText: "Service Unavailable" }); 161 | } 162 | } 163 | })()); 164 | }); 165 | 166 | -------------------------------------------------------------------------------- /fileutils.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | 5 | /** 6 | import * as path from "path"; 7 | import { fileURLToPath } from "url"; 8 | 9 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 10 | const __filename = fileURLToPath(import.meta.url); 11 | */ 12 | 13 | /** 14 | * @param {string} src - The source path. 15 | * @param {string} dest - The destination path. 16 | */ 17 | const cpSync = (src, dest) => { 18 | if (!fs.existsSync(src)) { 19 | console.error(`${src} does not exist.`) 20 | return; 21 | } 22 | if (fs.lstatSync(src).isDirectory()) { 23 | if (!fs.existsSync(dest)) fs.mkdirSync(dest); 24 | fs.readdirSync(src).forEach(child => cpSync(path.join(src, child), path.join(dest, child))); 25 | } else { 26 | fs.copyFileSync(src, dest); 27 | } 28 | }; 29 | 30 | /** 31 | * @param {string} src - The source path of the file to modify and copy. 32 | * @param {string} dest - The destination path. 33 | * @param {string[]} findAndReplace - The string to find and to replace pairs. 34 | */ 35 | const cpSyncModify = (src, dest, ...findAndReplace) => { 36 | if (!fs.existsSync(src)) { 37 | console.error(`${src} does not exist.`) 38 | return; 39 | } 40 | let data = fs.readFileSync(src, "utf-8"); 41 | for (let i = 0; i < findAndReplace.length; i += 2) { 42 | const find = findAndReplace[i]; 43 | const replace = findAndReplace[i + 1]; 44 | // Create a regular expression from the 'find' string 45 | const regex = new RegExp(find, "g"); 46 | // Replace 'find' with 'replace' 47 | data = data.replace(regex, replace); 48 | } 49 | // Write the modified data to a new file 50 | fs.writeFileSync(dest, data, "utf-8"); 51 | } 52 | /** 53 | * @param {string} dir - The directory to remove. 54 | */ 55 | const rmSync = (dir) => { 56 | if (!fs.existsSync(dir)) return; 57 | if (fs.lstatSync(dir).isDirectory()) { 58 | fs.readdirSync(dir).forEach(child => rmSync(path.join(dir, child))); 59 | fs.rmdirSync(dir); 60 | } else { 61 | fs.unlinkSync(dir); 62 | } 63 | }; 64 | 65 | export { cpSync, cpSyncModify, rmSync }; 66 | -------------------------------------------------------------------------------- /libfaust-wasm/libfaust-wasm.d.cts: -------------------------------------------------------------------------------- 1 | import { FaustModuleFactory } from "../src/types"; 2 | declare const faustModuleFactory: FaustModuleFactory; 3 | export default faustModuleFactory; 4 | -------------------------------------------------------------------------------- /libfaust-wasm/libfaust-wasm.d.ts: -------------------------------------------------------------------------------- 1 | import { FaustModuleFactory } from "../src/types"; 2 | declare const faustModuleFactory: FaustModuleFactory; 3 | export default faustModuleFactory; 4 | -------------------------------------------------------------------------------- /libfaust-wasm/libfaust-wasm.data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grame-cncm/faustwasm/1e4f94abc4c4ad6319be1e47207eeedce823badf/libfaust-wasm/libfaust-wasm.data -------------------------------------------------------------------------------- /libfaust-wasm/libfaust-wasm.data.d.ts: -------------------------------------------------------------------------------- 1 | declare const data: Uint8Array; 2 | export default data; -------------------------------------------------------------------------------- /libfaust-wasm/libfaust-wasm.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grame-cncm/faustwasm/1e4f94abc4c4ad6319be1e47207eeedce823badf/libfaust-wasm/libfaust-wasm.wasm -------------------------------------------------------------------------------- /libfaust-wasm/libfaust-wasm.wasm.d.ts: -------------------------------------------------------------------------------- 1 | declare const data: Uint8Array; 2 | export default data; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@grame/faustwasm", 3 | "version": "0.10.1", 4 | "description": "WebAssembly version of Faust Compiler", 5 | "main": "dist/cjs/index.js", 6 | "types": "dist/esm/index.d.ts", 7 | "module": "dist/esm/index.js", 8 | "type": "module", 9 | "scripts": { 10 | "build": "npm run build-cjs && npm run build-cjs-bundle && npm run build-esm && npm run build-esm-bundle && node postbuild-bundled.js & npm run build-types & npm run build-types-bundle", 11 | "build-cjs": "esbuild src/index.ts --target=es2019 --bundle --sourcemap --outdir=dist/cjs --format=iife --external:fs --external:url", 12 | "build-cjs-bundle": "node prebuild-bundled.js && esbuild src/index-bundle-iife.ts --target=es2019 --bundle --sourcemap --loader:.wasm=binary --loader:.data=binary --outfile=dist/cjs-bundle/index.js --format=iife --external:fs --external:url --external:path --external:ws && node postbuild-bundled.js", 13 | "build-esm": "esbuild src/index.ts --target=es2019 --bundle --sourcemap --outdir=dist/esm --format=esm --external:fs --external:url", 14 | "build-esm-bundle": "node prebuild-bundled.js && esbuild src/index-bundle.ts --target=es2019 --bundle --sourcemap --loader:.wasm=binary --loader:.data=binary --outfile=dist/esm-bundle/index.js --format=esm --external:fs --external:url --external:path --external:ws && node postbuild-bundled.js", 15 | "build-types": "dts-bundle-generator -o dist/cjs/index.d.ts src/index.ts --external-imports", 16 | "build-types-bundle": "dts-bundle-generator -o dist/cjs-bundle/index.d.ts src/index-bundle.ts --external-imports", 17 | "postbuild": "node postbuild.js" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/grame-cncm/faustwasm.git" 22 | }, 23 | "keywords": [ 24 | "faust", 25 | "webassembly", 26 | "audio", 27 | "signal processing" 28 | ], 29 | "bin": { 30 | "faust2sndfile-ts": "scripts/faust2sndfile.js", 31 | "faust2svg-ts": "scripts/faust2svg.js", 32 | "faust2wasm-ts": "scripts/faust2wasm.js" 33 | }, 34 | "author": "Grame-CNCM", 35 | "license": "LGPL-3.0", 36 | "bugs": { 37 | "url": "https://github.com/grame-cncm/faustwasm/issues" 38 | }, 39 | "homepage": "https://github.com/grame-cncm/faustwasm#readme", 40 | "devDependencies": { 41 | "@aws-crypto/sha256-js": "^5.2.0", 42 | "@shren/faust-ui": "^1.1.16", 43 | "@types/node": "^20.12.7", 44 | "@types/webmidi": "^2.0.10", 45 | "@webaudiomodules/api": "^2.0.0-alpha.6", 46 | "@webaudiomodules/sdk-parammgr": "^0.0.13", 47 | "dts-bundle-generator": "^9.5.1", 48 | "esbuild": "^0.20.2", 49 | "typescript": "^5.4.5" 50 | }, 51 | "dependencies": { 52 | "@types/emscripten": "^1.39.10" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /postbuild-bundled.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | import { rmSync } from "./fileutils.js" 3 | import * as path from "path"; 4 | import { fileURLToPath } from "url"; 5 | 6 | // @ts-ignore 7 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 8 | // @ts-ignore 9 | const __filename = fileURLToPath(import.meta.url); 10 | 11 | rmSync(path.join(__dirname, "./libfaust-wasm/libfaust-wasm.cjs")); 12 | -------------------------------------------------------------------------------- /postbuild.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | import { cpSync, rmSync } from "./fileutils.js"; 3 | import * as fs from "fs"; 4 | import * as path from "path"; 5 | import { fileURLToPath } from "url"; 6 | 7 | // @ts-ignore 8 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 9 | // @ts-ignore 10 | const __filename = fileURLToPath(import.meta.url); 11 | 12 | const faustUiDistPath = path.join(__dirname, "./node_modules/@shren/faust-ui/dist/esm"); 13 | const faustUiDistDest = path.join(__dirname, "./assets/standalone/faust-ui"); 14 | const faustUiDistDest2 = path.join(__dirname, "./test/faustlive-wasm/faust-ui"); 15 | 16 | try { 17 | rmSync(faustUiDistDest); 18 | rmSync(faustUiDistDest2); 19 | } catch (e) { 20 | console.warn(e); 21 | } 22 | try { 23 | fs.mkdirSync(faustUiDistDest); 24 | fs.mkdirSync(faustUiDistDest2); 25 | } catch (e) { 26 | console.warn(e); 27 | } 28 | 29 | cpSync(faustUiDistPath, faustUiDistDest); 30 | cpSync(faustUiDistPath, faustUiDistDest2); 31 | const faustUiDts = `export * from "@shren/faust-ui";\n`; 32 | fs.writeFileSync(path.join(faustUiDistDest, "index.d.ts"), faustUiDts); 33 | fs.writeFileSync(path.join(faustUiDistDest2, "index.d.ts"), faustUiDts); 34 | 35 | console.log("FaustUI files copied.") 36 | 37 | const faustWasmDistPath = path.join(__dirname, "./dist/cjs"); 38 | const faustWasmDistEsmPath = path.join(__dirname, "./dist/esm"); 39 | const faustWasmDistBundlePath = path.join(__dirname, "./dist/cjs-bundle"); 40 | const faustWasmDistEsmBundlePath = path.join(__dirname, "./dist/esm-bundle"); 41 | const faustWasmDistDest = path.join(__dirname, "./assets/standalone/faustwasm"); 42 | const faustWasmDistDest2 = path.join(__dirname, "./test/faustlive-wasm/faustwasm"); 43 | 44 | fs.copyFileSync(path.join(faustWasmDistPath, "index.d.ts"), path.join(faustWasmDistEsmPath, "index.d.ts")); 45 | fs.copyFileSync(path.join(faustWasmDistBundlePath, "index.d.ts"), path.join(faustWasmDistEsmBundlePath, "index.d.ts")); 46 | 47 | try { 48 | rmSync(faustWasmDistDest); 49 | rmSync(faustWasmDistDest2); 50 | } catch (e) { 51 | console.warn(e); 52 | } 53 | try { 54 | fs.mkdirSync(faustWasmDistDest); 55 | fs.mkdirSync(faustWasmDistDest2); 56 | } catch (e) { 57 | console.warn(e); 58 | } 59 | 60 | cpSync(faustWasmDistEsmPath, faustWasmDistDest); 61 | cpSync(faustWasmDistEsmPath, faustWasmDistDest2); 62 | 63 | console.log("FaustWasm files copied.") 64 | -------------------------------------------------------------------------------- /prebuild-bundled.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | import { cpSync } from "./fileutils.js" 3 | import * as path from "path"; 4 | import { fileURLToPath } from "url"; 5 | 6 | // @ts-ignore 7 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 8 | // @ts-ignore 9 | const __filename = fileURLToPath(import.meta.url); 10 | 11 | cpSync(path.join(__dirname, "./libfaust-wasm/libfaust-wasm.js"), path.join(__dirname, "./libfaust-wasm/libfaust-wasm.cjs")); 12 | -------------------------------------------------------------------------------- /rsrc/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grame-cncm/faustwasm/1e4f94abc4c4ad6319be1e47207eeedce823badf/rsrc/overview.png -------------------------------------------------------------------------------- /rsrc/overview.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grame-cncm/faustwasm/1e4f94abc4c4ad6319be1e47207eeedce823badf/rsrc/overview.xlsx -------------------------------------------------------------------------------- /scripts/faust2cmajor.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | //@ts-check 3 | import * as process from "process"; 4 | import faust2cmajor from "../src/faust2cmajorFiles.js"; 5 | 6 | const argv = process.argv.slice(2); 7 | 8 | if (argv[0] === "-help" || argv[0] === "-h") { 9 | console.log(` 10 | faust2cmajor.js 11 | Compile a given Faust DSP to a Cmajor file. 12 | `); 13 | process.exit(); 14 | } 15 | 16 | const [inputFile, outputDir, ...argvFaust] = argv; 17 | 18 | faust2cmajor(inputFile, outputDir, argvFaust); 19 | -------------------------------------------------------------------------------- /scripts/faust2sndfile.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | //@ts-check 3 | import * as process from "process"; 4 | import faust2wavFiles from "../src/faust2wavFiles.js"; 5 | 6 | const argv = process.argv.slice(2); 7 | 8 | if (argv[0] === "-help" || argv[0] === "-h") { 9 | console.log(` 10 | faust2sndfile.js 11 | Generates audio file from a Faust DSP. 12 | \t -bs\t to setup the rendering buffer size in frames (default: 64) 13 | \t -bd\t 16|24|32 to setup the output file bit-depth (default: 16) 14 | \t -c \t to setup the output file length in frames, when -ct is not used (default: SR*5) 15 | \t -in\t specify an input file to process 16 | \t -sr\t to setup the output file sample rate (default: 44100) 17 | `); 18 | process.exit(); 19 | } 20 | 21 | const $in = argv.indexOf("-in"); 22 | /** @type {string} */let inputWav; 23 | if ($in !== -1) [, inputWav] = argv.splice($in, 2); 24 | 25 | const $bs = argv.indexOf("-bs"); 26 | /** @type {number} */let bufferSize; 27 | if ($bs !== -1) bufferSize = +argv.splice($bs, 2)[1]; 28 | 29 | const $bd = argv.indexOf("-bd"); 30 | /** @type {number} */let bitDepth; 31 | if ($bd !== -1) bitDepth = +argv.splice($bd, 2)[1]; 32 | 33 | const $c = argv.indexOf("-c"); 34 | /** @type {number} */let samples; 35 | if ($c !== -1) samples = +argv.splice($c, 2)[1]; 36 | 37 | const $sr = argv.indexOf("-sr"); 38 | /** @type {number} */let sampleRate; 39 | if ($sr !== -1) sampleRate = +argv.splice($sr, 2)[1]; 40 | 41 | const [inputFile, outputWav, ...argvFaust] = argv; 42 | 43 | faust2wavFiles(inputFile, inputWav, outputWav, bufferSize, sampleRate, samples, bitDepth, argvFaust); 44 | -------------------------------------------------------------------------------- /scripts/faust2svg.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | //@ts-check 3 | import * as process from "process"; 4 | import faust2svgFiles from "../src/faust2svgFiles.js"; 5 | 6 | const argv = process.argv.slice(2); 7 | 8 | if (argv[0] === "-help" || argv[0] === "-h") { 9 | console.log(` 10 | faust2svg.js 11 | Generates Diagram SVGs of a given Faust DSP. 12 | `); 13 | process.exit(); 14 | } 15 | 16 | const [inputFile, outputDir, ...argvFaust] = argv; 17 | 18 | faust2svgFiles(inputFile, outputDir, argvFaust); 19 | -------------------------------------------------------------------------------- /scripts/faust2wasm.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | //@ts-check 3 | import * as process from "process"; 4 | import faust2wasmFiles from "../src/faust2wasmFiles.js"; 5 | import { copyWebStandaloneAssets, copyWebPWAAssets, copyWebTemplateAssets } from "../src/copyWebStandaloneAssets.js"; 6 | 7 | const argv = process.argv.slice(2); 8 | 9 | if (argv[0] === "-help" || argv[0] === "-h") { 10 | console.log(` 11 | faust2wasm.js [-poly] [-standalone] [-pwa] [-no-template] 12 | Generates WebAssembly and metadata JSON files of a given Faust DSP. 13 | `); 14 | process.exit(); 15 | } 16 | 17 | const $poly = argv.indexOf("-poly"); 18 | const poly = $poly !== -1; 19 | if (poly) argv.splice($poly, 1); 20 | 21 | const $standalone = argv.indexOf("-standalone"); 22 | const standalone = $standalone !== -1; 23 | if (standalone) argv.splice($standalone, 1); 24 | 25 | const $pwa = argv.indexOf("-pwa"); 26 | const pwa = $pwa !== -1; 27 | if (pwa) argv.splice($pwa, 1); 28 | 29 | const $noTemplate = argv.indexOf("-no-template"); 30 | const noTemplate = $noTemplate !== -1; 31 | if (noTemplate) argv.splice($noTemplate, 1); 32 | 33 | const [inputFile, outputDir, ...argvFaust] = argv; 34 | const fileName = inputFile.split('/').pop(); 35 | if (!fileName) throw new Error("No input DSP file"); 36 | const dspName = fileName.replace(/\.dsp$/, ''); 37 | 38 | (async () => { 39 | const { dspMeta, effectMeta } = await faust2wasmFiles(inputFile, outputDir, argvFaust, poly); 40 | if (standalone) { 41 | copyWebStandaloneAssets(outputDir, dspName, poly, !!effectMeta); 42 | } else if (pwa) { 43 | copyWebPWAAssets(outputDir, dspName, poly, !!effectMeta); 44 | } else if (!noTemplate) { 45 | copyWebTemplateAssets(outputDir, dspName, poly, !!effectMeta); 46 | } 47 | })(); 48 | -------------------------------------------------------------------------------- /src/FaustAudioWorkletCommunicator.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Layout: 4 | * 5 | * 6 | * invert-isAndroid (uint8) 7 | * new-acc-data-available (uint8) 8 | * new-gyr-data-available (uint8) 9 | * empty (uint8) 10 | * 11 | * acc.x, acc.y, acc.z (f32) 12 | * 13 | * gyr.alpha, gyr.beta, gyr.gamma (f32) 14 | */ 15 | export class FaustAudioWorkletCommunicator { 16 | protected readonly port: MessagePort; 17 | protected readonly supportSharedArrayBuffer: boolean; 18 | protected readonly byteLength: number; 19 | protected uin8Invert: Uint8ClampedArray; 20 | protected uin8NewAccData: Uint8ClampedArray; 21 | protected uin8NewGyrData: Uint8ClampedArray; 22 | protected f32Acc: Float32Array; 23 | protected f32Gyr: Float32Array; 24 | constructor(port: MessagePort) { 25 | this.port = port; 26 | this.supportSharedArrayBuffer = !!globalThis.SharedArrayBuffer; 27 | this.byteLength 28 | = 4 * Uint8Array.BYTES_PER_ELEMENT 29 | + 3 * Float32Array.BYTES_PER_ELEMENT 30 | + 3 * Float32Array.BYTES_PER_ELEMENT; 31 | } 32 | initializeBuffer(ab: SharedArrayBuffer | ArrayBuffer) { 33 | let ptr = 0; 34 | this.uin8Invert = new Uint8ClampedArray(ab, ptr, 1); 35 | ptr += Uint8ClampedArray.BYTES_PER_ELEMENT; 36 | this.uin8NewAccData = new Uint8ClampedArray(ab, ptr, 1); 37 | ptr += Uint8ClampedArray.BYTES_PER_ELEMENT; 38 | this.uin8NewGyrData = new Uint8ClampedArray(ab, ptr, 1); 39 | ptr += Uint8ClampedArray.BYTES_PER_ELEMENT; 40 | ptr += Uint8ClampedArray.BYTES_PER_ELEMENT;; // empty 41 | this.f32Acc = new Float32Array(ab, ptr, 3); 42 | ptr += 3 * Float32Array.BYTES_PER_ELEMENT; 43 | this.f32Gyr = new Float32Array(ab, ptr, 3); 44 | ptr += 3 * Float32Array.BYTES_PER_ELEMENT; 45 | } 46 | setNewAccDataAvailable(value: boolean) { 47 | if (!this.uin8NewAccData) return; 48 | this.uin8NewAccData[0] = +value; 49 | } 50 | getNewAccDataAvailable() { 51 | return !!this.uin8NewAccData?.[0]; 52 | } 53 | setNewGyrDataAvailable(value: boolean) { 54 | if (!this.uin8NewGyrData) return; 55 | this.uin8NewGyrData[0] = +value; 56 | } 57 | getNewGyrDataAvailable() { 58 | return !!this.uin8NewGyrData?.[0]; 59 | } 60 | setAcc({ x, y, z }: { x: number, y: number, z: number }, invert = false) { 61 | if (!this.supportSharedArrayBuffer) { 62 | const e = { type: "acc", data: { x, y, z }, invert }; 63 | this.port.postMessage(e); 64 | } 65 | if (!this.uin8NewAccData) return; 66 | this.uin8Invert[0] = +invert; 67 | this.f32Acc[0] = x; 68 | this.f32Acc[1] = y; 69 | this.f32Acc[2] = z; 70 | this.uin8NewAccData[0] = 1; 71 | } 72 | getAcc() { 73 | if (!this.uin8NewAccData) return; 74 | const invert = !!this.uin8Invert[0]; 75 | const [x, y, z] = this.f32Acc; 76 | return { x, y, z, invert }; 77 | } 78 | setGyr({ alpha, beta, gamma }: { alpha: number, beta: number, gamma: number }) { 79 | if (!this.supportSharedArrayBuffer) { 80 | const e = { type: "gyr", data: { alpha, beta, gamma } }; 81 | this.port.postMessage(e); 82 | } 83 | if (!this.uin8NewGyrData) return; 84 | this.f32Gyr[0] = alpha; 85 | this.f32Gyr[1] = beta; 86 | this.f32Gyr[2] = gamma; 87 | this.uin8NewGyrData[0] = 1; 88 | } 89 | getGyr() { 90 | if (!this.uin8NewGyrData) return; 91 | const [alpha, beta, gamma] = this.f32Gyr; 92 | return { alpha, beta, gamma }; 93 | } 94 | } 95 | 96 | export class FaustAudioWorkletNodeCommunicator extends FaustAudioWorkletCommunicator { 97 | constructor(port: MessagePort) { 98 | super(port); 99 | if (this.supportSharedArrayBuffer) { 100 | const sab = new SharedArrayBuffer(this.byteLength); 101 | this.initializeBuffer(sab); 102 | this.port.postMessage({ type: "initSab", sab }); 103 | } else { 104 | const ab = new ArrayBuffer(this.byteLength); 105 | this.initializeBuffer(ab); 106 | } 107 | } 108 | } 109 | 110 | export class FaustAudioWorkletProcessorCommunicator extends FaustAudioWorkletCommunicator { 111 | constructor(port: MessagePort) { 112 | super(port); 113 | 114 | if (this.supportSharedArrayBuffer) { 115 | this.port.addEventListener("message", (event) => { 116 | const { data } = event; 117 | if (data.type === "initSab") { 118 | this.initializeBuffer(data.sab); 119 | } 120 | }); 121 | } else { 122 | const ab = new ArrayBuffer(this.byteLength); 123 | this.initializeBuffer(ab); 124 | this.port.addEventListener("message", (event) => { 125 | const msg = event.data; 126 | 127 | switch (msg.type) { 128 | // Sensors messages 129 | case "acc": { 130 | this.setAcc(msg.data, msg.invert); 131 | break; 132 | } 133 | case "gyr": { 134 | this.setGyr(msg.data); 135 | break; 136 | } 137 | default: 138 | break; 139 | } 140 | }); 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/FaustCmajor.ts: -------------------------------------------------------------------------------- 1 | import type FaustCompiler from "./FaustCompiler"; 2 | 3 | interface IFaustCmajor { 4 | /** 5 | * Generates auxiliary files from Faust code. The output depends on the compiler options. 6 | * 7 | * @param name - the DSP's name 8 | * @param code - Faust code 9 | * @param args - compilation args 10 | * @returns the Cmajor compiled string 11 | */ 12 | compile(name: string, code: string, args: string): string; 13 | } 14 | 15 | class FaustCmajor implements IFaustCmajor { 16 | private fCompiler: FaustCompiler; 17 | 18 | constructor(compiler: FaustCompiler) { 19 | this.fCompiler = compiler; 20 | } 21 | 22 | compile(name: string, code: string, args: string) { 23 | const fs = this.fCompiler.fs(); 24 | const success = this.fCompiler.generateAuxFiles(name, code, `-lang cmajor-hybrid -cn ${name} -o ${name}.cmajor`); 25 | return (success) ? fs.readFile(`${name}.cmajor`, { encoding: "utf8" }) as string : ""; 26 | } 27 | } 28 | 29 | export default FaustCmajor; 30 | -------------------------------------------------------------------------------- /src/FaustCompiler.ts: -------------------------------------------------------------------------------- 1 | import { Sha256 } from "@aws-crypto/sha256-js"; 2 | import type { ILibFaust } from "./LibFaust"; 3 | import type { FaustDspFactory, IntVector } from "./types"; 4 | 5 | export const ab2str = (buf: Uint8Array) => String.fromCharCode.apply(null, buf); 6 | 7 | export const str2ab = (str: string) => { 8 | const buf = new ArrayBuffer(str.length); 9 | const bufView = new Uint8Array(buf); 10 | for (let i = 0, strLen = str.length; i < strLen; i++) { 11 | bufView[i] = str.charCodeAt(i); 12 | } 13 | return bufView; 14 | }; 15 | const sha256 = async (str: string) => { 16 | const sha256 = new Sha256(); 17 | sha256.update(str); 18 | const hashArray = Array.from(await sha256.digest()); 19 | const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join(""); 20 | return hashHex; 21 | }; 22 | 23 | export interface IFaustCompiler { 24 | /** 25 | * Gives the Faust compiler version. 26 | * @return a version string 27 | */ 28 | version(): string; 29 | 30 | /** 31 | * Gives the last compilation error. 32 | * @return an error string 33 | */ 34 | getErrorMessage(): string; 35 | 36 | /** 37 | * Create a wasm factory from Faust code i.e. wasm compiled code, to be used to create monophonic instances. 38 | * This function is running asynchronously. 39 | * 40 | * @param name - an arbitrary name for the Faust factory 41 | * @param code - Faust dsp code 42 | * @param args - the compiler options 43 | * @returns returns the wasm factory 44 | */ 45 | createMonoDSPFactory(name: string, code: string, args: string): Promise; 46 | 47 | /** 48 | * Create a wasm factory from Faust code i.e. wasm compiled code, to be used to create polyphonic instances. 49 | * This function is running asynchronously. 50 | * 51 | * @param name - an arbitrary name for the Faust factory 52 | * @param code - Faust dsp code 53 | * @param args - the compiler options 54 | * @returns returns the wasm factory 55 | */ 56 | createPolyDSPFactory(name: string, code: string, args: string): Promise; 57 | 58 | /** 59 | * Delete a dsp factory. 60 | * 61 | * @param factory - the factory to be deleted 62 | */ 63 | deleteDSPFactory(factory: FaustDspFactory): void; 64 | 65 | /** 66 | * Expand Faust code i.e. linearize included libraries. 67 | * 68 | * @param code - Faust dsp code 69 | * @param args - the compiler options 70 | * @returns returns the expanded dsp code 71 | */ 72 | expandDSP(code: string, args: string): string | null; 73 | 74 | /** 75 | * Generates auxiliary files from Faust code. The output depends on the compiler options. 76 | * 77 | * @param name - an arbitrary name for the Faust module 78 | * @param code - Faust dsp code 79 | * @param args - the compiler options 80 | * @returns whether the generation actually succeded 81 | */ 82 | generateAuxFiles(name: string, code: string, args: string): boolean; 83 | 84 | /** 85 | * Delete all factories. 86 | */ 87 | deleteAllDSPFactories(): void; 88 | 89 | fs(): typeof FS; 90 | 91 | getAsyncInternalMixerModule(isDouble?: boolean): Promise<{ mixerBuffer: Uint8Array; mixerModule: WebAssembly.Module }>; 92 | getSyncInternalMixerModule(isDouble?: boolean): { mixerBuffer: Uint8Array; mixerModule: WebAssembly.Module }; 93 | } 94 | 95 | class FaustCompiler implements IFaustCompiler { 96 | private fLibFaust: ILibFaust; 97 | private fErrorMessage: string; 98 | private static gFactories: Map = new Map(); 99 | private mixer32Buffer!: Uint8Array; 100 | private mixer64Buffer!: Uint8Array; 101 | private mixer32Module!: WebAssembly.Module; 102 | private mixer64Module!: WebAssembly.Module; 103 | 104 | /** 105 | * Get a stringified DSP factories table 106 | */ 107 | static serializeDSPFactories() { 108 | const table: Record = {}; 109 | this.gFactories.forEach((factory, shaKey) => { 110 | const { code, json, poly } = factory; 111 | table[shaKey] = { code: btoa(ab2str(code)), json: JSON.parse(json), poly }; 112 | }); 113 | return table; 114 | } 115 | /** 116 | * Get a stringified DSP factories table as string 117 | */ 118 | static stringifyDSPFactories() { 119 | return JSON.stringify(this.serializeDSPFactories()); 120 | } 121 | /** 122 | * Import a DSP factories table 123 | */ 124 | static deserializeDSPFactories(table: Record) { 125 | const awaited: Promise>[] = []; 126 | for (const shaKey in table) { 127 | const factory = table[shaKey]; 128 | const { code, json, poly } = factory; 129 | const ab = str2ab(atob(code)) 130 | awaited.push(WebAssembly.compile(ab).then(module => this.gFactories.set(shaKey, { shaKey, cfactory: 0, code: ab, module, json: JSON.stringify(json), poly, soundfiles: {} }))); 131 | } 132 | return Promise.all(awaited); 133 | } 134 | /** 135 | * Import a stringified DSP factories table 136 | */ 137 | static importDSPFactories(tableStr: string) { 138 | const table: Record = JSON.parse(tableStr); 139 | return this.deserializeDSPFactories(table); 140 | } 141 | constructor(libFaust: ILibFaust) { 142 | this.fLibFaust = libFaust; 143 | this.fErrorMessage = ""; 144 | } 145 | private intVec2intArray(vec: IntVector) { 146 | const size = vec.size(); 147 | const ui8Code = new Uint8Array(size); 148 | for (let i = 0; i < size; i++) { 149 | ui8Code[i] = vec.get(i); 150 | } 151 | return ui8Code; 152 | } 153 | private async createDSPFactory(name: string, code: string, args: string, poly: boolean) { 154 | // Cleanup the cache 155 | if (FaustCompiler.gFactories.size > 10) { 156 | FaustCompiler.gFactories.clear(); 157 | } 158 | 159 | // If code is already compiled, return the cached factory 160 | let shaKey = await sha256(name + code + args + (poly ? "poly" : "mono")); 161 | if (FaustCompiler.gFactories.has(shaKey)) { 162 | return FaustCompiler.gFactories.get(shaKey) || null; 163 | } else { 164 | try { 165 | // Can possibly raise a C++ exception catched by the second catch() 166 | const faustDspWasm = this.fLibFaust.createDSPFactory(name, code, args, !poly); 167 | const ui8Code = this.intVec2intArray(faustDspWasm.data); 168 | faustDspWasm.data.delete(); 169 | const module = await WebAssembly.compile(ui8Code); 170 | const factory: FaustDspFactory = { shaKey, cfactory: faustDspWasm.cfactory, code: ui8Code, module, json: faustDspWasm.json, poly, soundfiles: {} }; 171 | // Factory C++ side can be deallocated immediately 172 | this.deleteDSPFactory(factory); 173 | // Keep the compiled factory in the cache 174 | FaustCompiler.gFactories.set(shaKey, factory); 175 | return factory; 176 | } catch (e) { 177 | this.fErrorMessage = this.fLibFaust.getErrorAfterException(); 178 | // console.error(`=> exception raised while running createDSPFactory: ${this.fErrorMessage}`, e); 179 | this.fLibFaust.cleanupAfterException(); 180 | throw this.fErrorMessage ? new Error(this.fErrorMessage) : e; 181 | } 182 | } 183 | } 184 | version() { 185 | return this.fLibFaust.version(); 186 | } 187 | getErrorMessage() { 188 | return this.fErrorMessage; 189 | } 190 | async createMonoDSPFactory(name: string, code: string, args: string) { 191 | return this.createDSPFactory(name, code, args, false); 192 | } 193 | async createPolyDSPFactory(name: string, code: string, args: string) { 194 | return this.createDSPFactory(name, code, args, true); 195 | } 196 | deleteDSPFactory(factory: FaustDspFactory) { 197 | this.fLibFaust.deleteDSPFactory(factory.cfactory); 198 | factory.cfactory = 0; 199 | } 200 | expandDSP(code: string, args: string) { 201 | try { 202 | return this.fLibFaust.expandDSP("FaustDSP", code, args); 203 | } catch (e) { 204 | this.fErrorMessage = this.fLibFaust.getErrorAfterException(); 205 | // console.error(`=> exception raised while running expandDSP: ${this.fErrorMessage}`); 206 | this.fLibFaust.cleanupAfterException(); 207 | throw this.fErrorMessage ? new Error(this.fErrorMessage) : e; 208 | } 209 | } 210 | generateAuxFiles(name: string, code: string, args: string) { 211 | try { 212 | return this.fLibFaust.generateAuxFiles(name, code, args); 213 | } catch (e) { 214 | this.fErrorMessage = this.fLibFaust.getErrorAfterException(); 215 | // console.error(`=> exception raised while running generateAuxFiles: ${this.fErrorMessage}`); 216 | this.fLibFaust.cleanupAfterException(); 217 | throw this.fErrorMessage ? new Error(this.fErrorMessage) : e; 218 | } 219 | } 220 | deleteAllDSPFactories(): void { 221 | this.fLibFaust.deleteAllDSPFactories(); 222 | } 223 | fs() { 224 | return this.fLibFaust.fs(); 225 | } 226 | async getAsyncInternalMixerModule(isDouble = false) { 227 | const bufferKey = isDouble ? "mixer64Buffer" : "mixer32Buffer"; 228 | const moduleKey = isDouble ? "mixer64Module" : "mixer32Module"; 229 | if (this[moduleKey]) return { mixerBuffer: this[bufferKey], mixerModule: this[moduleKey] }; 230 | const path = isDouble ? "/usr/rsrc/mixer64.wasm" : "/usr/rsrc/mixer32.wasm"; 231 | const mixerBuffer = this.fs().readFile(path, { encoding: "binary" }); 232 | this[bufferKey] = mixerBuffer; 233 | // Compile mixer 234 | const mixerModule = await WebAssembly.compile(mixerBuffer); 235 | this[moduleKey] = mixerModule; 236 | return { mixerBuffer, mixerModule }; 237 | } 238 | getSyncInternalMixerModule(isDouble = false) { 239 | const bufferKey = isDouble ? "mixer64Buffer" : "mixer32Buffer"; 240 | const moduleKey = isDouble ? "mixer64Module" : "mixer32Module"; 241 | if (this[moduleKey]) return { mixerBuffer: this[bufferKey], mixerModule: this[moduleKey] }; 242 | const path = isDouble ? "/usr/rsrc/mixer64.wasm" : "/usr/rsrc/mixer32.wasm"; 243 | const mixerBuffer = this.fs().readFile(path, { encoding: "binary" }); 244 | this[bufferKey] = mixerBuffer; 245 | // Compile mixer 246 | const mixerModule = new WebAssembly.Module(mixerBuffer); 247 | this[moduleKey] = mixerModule; 248 | return { mixerBuffer, mixerModule }; 249 | } 250 | } 251 | 252 | export default FaustCompiler; 253 | -------------------------------------------------------------------------------- /src/FaustDspInstance.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The Faust wasm instance interface. 3 | */ 4 | export interface IFaustDspInstance { 5 | /** 6 | * The dsp computation, to be called with successive input/output audio buffers. 7 | * 8 | * @param $dsp - the DSP pointer 9 | * @param count - the audio buffer size in frames 10 | * @param $inputs - the input audio buffer as in index in wasm memory 11 | * @param $output - the output audio buffer as in index in wasm memory 12 | */ 13 | compute($dsp: number, count: number, $inputs: number, $output: number): void; 14 | 15 | /** 16 | * Give the number of inputs of a Faust wasm instance. 17 | * 18 | * @param $dsp - the DSP pointer 19 | */ 20 | getNumInputs($dsp: number): number; 21 | 22 | /** 23 | * Give the number of outputs of a Faust wasm instance. 24 | * 25 | * @param $dsp - the DSP pointer 26 | */ 27 | getNumOutputs($dsp: number): number; 28 | 29 | /** 30 | * Give a parameter current value. 31 | * 32 | * @param $dsp - the DSP pointer 33 | * @param index - the parameter index 34 | * @return the parameter value 35 | */ 36 | getParamValue($dsp: number, index: number): number; 37 | 38 | /** 39 | * Give the Faust wasm instance sample rate. 40 | * 41 | * @param $dsp - the DSP pointer 42 | * @return the sample rate 43 | */ 44 | getSampleRate($dsp: number): number; 45 | 46 | /** 47 | * Global init, calls the following methods: 48 | * - static class 'classInit': static tables initialization 49 | * - 'instanceInit': constants and instance state initialization 50 | * 51 | * @param $dsp - the DSP pointer 52 | * @param sampleRate - the sampling rate in Hertz 53 | */ 54 | init($dsp: number, sampleRate: number): void; 55 | 56 | /** Init instance state (delay lines...). 57 | * 58 | * @param $dsp - the DSP pointer 59 | */ 60 | instanceClear($dsp: number): void; 61 | 62 | /** Init instance constant state. 63 | * 64 | * @param $dsp - the DSP pointer 65 | * @param sampleRate - the sampling rate in Hertz 66 | */ 67 | instanceConstants($dsp: number, sampleRate: number): void; 68 | 69 | /** Init instance state. 70 | * 71 | * @param $dsp - the DSP pointer 72 | * @param sampleRate - the sampling rate in Hertz 73 | */ 74 | instanceInit($dsp: number, sampleRate: number): void; 75 | 76 | /** Init default control parameters values. 77 | * 78 | * @param $dsp - the DSP pointer 79 | */ 80 | instanceResetUserInterface($dsp: number): void; 81 | 82 | /** 83 | * Set a parameter current value. 84 | * 85 | * @param $dsp - the DSP pointer 86 | * @param index - the parameter index 87 | * @param value - the parameter value 88 | */ 89 | setParamValue($dsp: number, index: number, value: number): void; 90 | } 91 | 92 | /** 93 | * Mixer used in polyphonic mode. 94 | */ 95 | export interface IFaustMixerInstance { 96 | clearOutput(bufferSize: number, chans: number, $outputs: number): void; 97 | mixCheckVoice(bufferSize: number, chans: number, $inputs: number, $outputs: number): number; 98 | fadeOut(bufferSize: number, chans: number, $outputs: number): void; 99 | } 100 | 101 | /** 102 | * Monophonic instance. 103 | */ 104 | export interface FaustMonoDspInstance { 105 | memory: WebAssembly.Memory; 106 | api: IFaustDspInstance; 107 | json: string; 108 | } 109 | 110 | /** 111 | * Polyphonic instance. 112 | */ 113 | export interface FaustPolyDspInstance { 114 | memory: WebAssembly.Memory; 115 | voices: number; 116 | voiceAPI: IFaustDspInstance; 117 | effectAPI?: IFaustDspInstance; 118 | mixerAPI: IFaustMixerInstance; 119 | voiceJSON: string; 120 | effectJSON?: string; 121 | } 122 | 123 | export class FaustDspInstance implements IFaustDspInstance { 124 | private readonly fExports: IFaustDspInstance; 125 | 126 | constructor(exports: IFaustDspInstance) { this.fExports = exports; } 127 | 128 | compute($dsp: number, count: number, $input: number, $output: number) { this.fExports.compute($dsp, count, $input, $output); } 129 | getNumInputs($dsp: number) { return this.fExports.getNumInputs($dsp); } 130 | getNumOutputs($dsp: number) { return this.fExports.getNumOutputs($dsp); } 131 | getParamValue($dsp: number, index: number) { return this.fExports.getParamValue($dsp, index); } 132 | getSampleRate($dsp: number) { return this.fExports.getSampleRate($dsp); } 133 | init($dsp: number, sampleRate: number) { this.fExports.init($dsp, sampleRate); } 134 | instanceClear($dsp: number) { this.fExports.instanceClear($dsp); } 135 | instanceConstants($dsp: number, sampleRate: number) { this.fExports.instanceConstants($dsp, sampleRate); } 136 | instanceInit($dsp: number, sampleRate: number) { this.fExports.instanceInit($dsp, sampleRate); } 137 | instanceResetUserInterface($dsp: number) { this.fExports.instanceResetUserInterface($dsp); } 138 | setParamValue($dsp: number, index: number, value: number) { this.fExports.setParamValue($dsp, index, value); } 139 | } 140 | -------------------------------------------------------------------------------- /src/FaustOfflineProcessor.ts: -------------------------------------------------------------------------------- 1 | import { ComputeHandler, FaustBaseWebAudioDsp, FaustMonoWebAudioDsp, FaustPolyWebAudioDsp, IFaustBaseWebAudioDsp, IFaustMonoWebAudioDsp, IFaustPolyWebAudioDsp, MetadataHandler, OutputParamHandler, PlotHandler } from "./FaustWebAudioDsp"; 2 | import { AudioParamDescriptor, FaustUIItem } from "./types"; 3 | 4 | /** 5 | * For offline rendering. 6 | */ 7 | export interface IFaustOfflineProcessor extends IFaustBaseWebAudioDsp { 8 | render(inputs?: Float32Array[], length?: number, onUpdate?: (sample: number) => any): Float32Array[]; 9 | } 10 | 11 | export interface IFaustMonoOfflineProcessor extends IFaustOfflineProcessor, IFaustMonoWebAudioDsp { } 12 | export interface IFaustPolyOfflineProcessor extends IFaustOfflineProcessor, IFaustPolyWebAudioDsp { } 13 | 14 | export class FaustOfflineProcessor { 15 | protected fDSPCode!: Poly extends true ? FaustPolyWebAudioDsp : FaustMonoWebAudioDsp; 16 | 17 | protected fBufferSize: number; 18 | protected fInputs: Float32Array[]; 19 | protected fOutputs: Float32Array[]; 20 | 21 | constructor(instance: Poly extends true ? FaustPolyWebAudioDsp : FaustMonoWebAudioDsp, bufferSize: number) { 22 | this.fDSPCode = instance; 23 | this.fBufferSize = bufferSize; 24 | this.fInputs = new Array(this.fDSPCode.getNumInputs()).fill(null).map(() => new Float32Array(bufferSize)); 25 | this.fOutputs = new Array(this.fDSPCode.getNumOutputs()).fill(null).map(() => new Float32Array(bufferSize)); 26 | } 27 | 28 | // Public API 29 | 30 | getParameterDescriptors() { 31 | const params = [] as AudioParamDescriptor[]; 32 | // Analyse voice JSON to generate AudioParam parameters 33 | const callback = (item: FaustUIItem) => { 34 | let param: AudioParamDescriptor | null = null; 35 | const polyKeywords = ["/gate", "/freq", "/gain", "/key", "/vel", "/velocity"]; 36 | const isPolyReserved = "address" in item && !!polyKeywords.find(k => item.address.endsWith(k)); 37 | if (this.fDSPCode instanceof FaustMonoWebAudioDsp || !isPolyReserved) { 38 | if (item.type === "vslider" || item.type === "hslider" || item.type === "nentry") { 39 | param = { name: item.address, defaultValue: item.init || 0, minValue: item.min || 0, maxValue: item.max || 0 }; 40 | } else if (item.type === "button" || item.type === "checkbox") { 41 | param = { name: item.address, defaultValue: item.init || 0, minValue: 0, maxValue: 1 }; 42 | } 43 | } 44 | if (param) params.push(param); 45 | } 46 | FaustBaseWebAudioDsp.parseUI(this.fDSPCode.getUI(), callback); 47 | return params; 48 | } 49 | compute(input: Float32Array[], output: Float32Array[]) { return this.fDSPCode.compute(input, output); } 50 | 51 | setOutputParamHandler(handler: OutputParamHandler) { this.fDSPCode.setOutputParamHandler(handler); } 52 | getOutputParamHandler() { return this.fDSPCode.getOutputParamHandler(); } 53 | 54 | setComputeHandler(handler: ComputeHandler) { this.fDSPCode.setComputeHandler(handler); } 55 | getComputeHandler() { return this.fDSPCode.getComputeHandler(); } 56 | 57 | setPlotHandler(handler: PlotHandler) { this.fDSPCode.setPlotHandler(handler); } 58 | getPlotHandler() { return this.fDSPCode.getPlotHandler(); } 59 | 60 | getNumInputs() { return this.fDSPCode.getNumInputs(); } 61 | getNumOutputs() { return this.fDSPCode.getNumOutputs(); } 62 | 63 | metadata(handler: MetadataHandler) { } 64 | 65 | midiMessage(data: number[] | Uint8Array) { this.fDSPCode.midiMessage(data); } 66 | 67 | ctrlChange(chan: number, ctrl: number, value: number) { this.fDSPCode.ctrlChange(chan, ctrl, value); } 68 | pitchWheel(chan: number, value: number) { this.fDSPCode.pitchWheel(chan, value); } 69 | keyOn(channel: number, pitch: number, velocity: number) { this.fDSPCode.keyOn(channel, pitch, velocity); } 70 | keyOff(channel: number, pitch: number, velocity: number) { this.fDSPCode.keyOff(channel, pitch, velocity); } 71 | 72 | setParamValue(path: string, value: number) { this.fDSPCode.setParamValue(path, value); } 73 | getParamValue(path: string) { return this.fDSPCode.getParamValue(path); } 74 | getParams() { return this.fDSPCode.getParams(); } 75 | 76 | getMeta() { return this.fDSPCode.getMeta(); } 77 | getJSON() { return this.fDSPCode.getJSON(); } 78 | getDescriptors() { return this.fDSPCode.getDescriptors(); } 79 | getUI() { return this.fDSPCode.getUI(); } 80 | 81 | start() { this.fDSPCode.start(); } 82 | stop() { this.fDSPCode.stop(); } 83 | 84 | destroy() { this.fDSPCode.destroy(); } 85 | 86 | get hasAccInput() { return this.fDSPCode.hasAccInput; } 87 | 88 | propagateAcc(accelerationIncludingGravity: NonNullable, invert: boolean = false) { 89 | this.fDSPCode.propagateAcc(accelerationIncludingGravity, invert); 90 | } 91 | 92 | get hasGyrInput() { return this.fDSPCode.hasGyrInput; } 93 | 94 | propagateGyr(event: Pick) { 95 | this.fDSPCode.propagateGyr(event); 96 | } 97 | 98 | startSensors(): void { } 99 | 100 | stopSensors(): void { } 101 | 102 | /** 103 | * Render frames in an array. 104 | * 105 | * @param inputs - input signal 106 | * @param length - the number of frames to render (default: bufferSize) 107 | * @param onUpdate - a callback after each buffer calculated, with an argument "current sample" 108 | * @return an array of Float32Array with the rendered frames 109 | */ 110 | render(inputs: Float32Array[] = [], length = this.fBufferSize, onUpdate?: (sample: number) => any): Float32Array[] { 111 | let l = 0; 112 | const outputs = new Array(this.fDSPCode.getNumOutputs()).fill(null).map(() => new Float32Array(length)); 113 | // The node has to be started before rendering 114 | this.fDSPCode.start(); 115 | while (l < length) { 116 | const sliceLength = Math.min(length - l, this.fBufferSize); 117 | for (let i = 0; i < this.fDSPCode.getNumInputs(); i++) { 118 | let input: Float32Array; 119 | if (inputs[i]) { 120 | if (inputs[i].length <= l) { 121 | input = new Float32Array(sliceLength); 122 | } else if (inputs[i].length > l + sliceLength) { 123 | input = inputs[i].subarray(l, l + sliceLength); 124 | } else { 125 | input = inputs[i].subarray(l, inputs[i].length); 126 | } 127 | } else { 128 | input = new Float32Array(sliceLength); 129 | } 130 | this.fInputs[i] = input; 131 | } 132 | this.fDSPCode.compute(this.fInputs, this.fOutputs); 133 | for (let i = 0; i < this.fDSPCode.getNumOutputs(); i++) { 134 | const output = this.fOutputs[i]; 135 | if (sliceLength < this.fBufferSize) { 136 | outputs[i].set(output.subarray(0, sliceLength), l); 137 | } else { 138 | outputs[i].set(output, l); 139 | } 140 | } 141 | l += this.fBufferSize; 142 | onUpdate?.(l); 143 | } 144 | // The node can be stopped after rendering 145 | this.fDSPCode.stop(); 146 | return outputs; 147 | } 148 | } 149 | 150 | export class FaustMonoOfflineProcessor extends FaustOfflineProcessor implements IFaustMonoWebAudioDsp { 151 | } 152 | 153 | export class FaustPolyOfflineProcessor extends FaustOfflineProcessor implements IFaustPolyWebAudioDsp { 154 | keyOn(channel: number, pitch: number, velocity: number) { this.fDSPCode.keyOn(channel, pitch, velocity); } 155 | keyOff(channel: number, pitch: number, velocity: number) { this.fDSPCode.keyOff(channel, pitch, velocity); } 156 | allNotesOff(hard: boolean) { this.fDSPCode.allNotesOff(hard); } 157 | } 158 | 159 | export default FaustOfflineProcessor; 160 | -------------------------------------------------------------------------------- /src/FaustScriptProcessorNode.ts: -------------------------------------------------------------------------------- 1 | import type { ComputeHandler, FaustMonoWebAudioDsp, FaustPolyWebAudioDsp, IFaustMonoWebAudioDsp, IFaustPolyWebAudioDsp, MetadataHandler, OutputParamHandler, PlotHandler } from "./FaustWebAudioDsp"; 2 | 3 | /** 4 | * Base class for Monophonic and Polyphonic ScriptProcessorNode 5 | */ 6 | export class FaustScriptProcessorNode extends (globalThis.ScriptProcessorNode || null) { 7 | protected fDSPCode!: Poly extends true ? FaustPolyWebAudioDsp : FaustMonoWebAudioDsp; 8 | 9 | // Needed for ScriptProcessorNode 10 | protected fInputs!: Float32Array[]; 11 | protected fOutputs!: Float32Array[]; 12 | protected handleDeviceMotion = undefined as any; 13 | protected handleDeviceOrientation = undefined as any; 14 | 15 | init(instance: Poly extends true ? FaustPolyWebAudioDsp : FaustMonoWebAudioDsp) { 16 | this.fDSPCode = instance; 17 | 18 | this.fInputs = new Array(this.fDSPCode.getNumInputs()); 19 | this.fOutputs = new Array(this.fDSPCode.getNumOutputs()); 20 | 21 | // Accelerometer and gyroscope handlers 22 | this.handleDeviceMotion = ({ accelerationIncludingGravity }: DeviceMotionEvent) => { 23 | const isAndroid: boolean = /Android/i.test(navigator.userAgent); 24 | if (!accelerationIncludingGravity) return; 25 | const { x, y, z } = accelerationIncludingGravity; 26 | this.propagateAcc({ x, y, z }, isAndroid); 27 | }; 28 | 29 | this.handleDeviceOrientation = ({ alpha, beta, gamma }: DeviceOrientationEvent) => { 30 | this.propagateGyr({ alpha, beta, gamma }); 31 | }; 32 | 33 | this.onaudioprocess = (e) => { 34 | 35 | // Read inputs 36 | for (let chan = 0; chan < this.fDSPCode.getNumInputs(); chan++) { 37 | this.fInputs[chan] = e.inputBuffer.getChannelData(chan); 38 | } 39 | 40 | // Read outputs 41 | for (let chan = 0; chan < this.fDSPCode.getNumOutputs(); chan++) { 42 | this.fOutputs[chan] = e.outputBuffer.getChannelData(chan); 43 | } 44 | 45 | return this.fDSPCode.compute(this.fInputs, this.fOutputs); 46 | } 47 | 48 | this.start(); 49 | } 50 | 51 | // Public API 52 | 53 | /** Start accelerometer and gyroscope handlers */ 54 | async startSensors() { 55 | if (this.hasAccInput) { 56 | if (window.DeviceMotionEvent) { 57 | // iOS 13+ requires a user gesture to enable DeviceMotionEvent, to be done in the main thread 58 | window.addEventListener("devicemotion", this.handleDeviceMotion, true); 59 | } else { 60 | // Browser doesn't support DeviceMotionEvent 61 | console.log("Cannot set the accelerometer handler."); 62 | } 63 | } 64 | if (this.hasGyrInput) { 65 | if (window.DeviceMotionEvent) { 66 | // iOS 13+ requires a user gesture to enable DeviceMotionEvent, to be done in the main thread 67 | window.addEventListener("deviceorientation", this.handleDeviceOrientation, true); 68 | } else { 69 | // Browser doesn't support DeviceMotionEvent 70 | console.log("Cannot set the gyroscope handler."); 71 | } 72 | } 73 | } 74 | 75 | /** Stop accelerometer and gyroscope handlers */ 76 | stopSensors() { 77 | if (this.hasAccInput) { 78 | window.removeEventListener("devicemotion", this.handleDeviceMotion, true); 79 | } 80 | if (this.hasGyrInput) { 81 | window.removeEventListener("deviceorientation", this.handleDeviceOrientation, true); 82 | } 83 | } 84 | 85 | compute(input: Float32Array[], output: Float32Array[]) { return this.fDSPCode.compute(input, output); } 86 | 87 | setOutputParamHandler(handler: OutputParamHandler) { this.fDSPCode.setOutputParamHandler(handler); } 88 | getOutputParamHandler() { return this.fDSPCode.getOutputParamHandler(); } 89 | 90 | setComputeHandler(handler: ComputeHandler) { this.fDSPCode.setComputeHandler(handler); } 91 | getComputeHandler() { return this.fDSPCode.getComputeHandler(); } 92 | 93 | setPlotHandler(handler: PlotHandler) { this.fDSPCode.setPlotHandler(handler); } 94 | getPlotHandler() { return this.fDSPCode.getPlotHandler(); } 95 | 96 | getNumInputs() { return this.fDSPCode.getNumInputs(); } 97 | getNumOutputs() { return this.fDSPCode.getNumOutputs(); } 98 | 99 | metadata(handler: MetadataHandler) { } 100 | 101 | midiMessage(data: number[] | Uint8Array) { this.fDSPCode.midiMessage(data); } 102 | 103 | ctrlChange(chan: number, ctrl: number, value: number) { this.fDSPCode.ctrlChange(chan, ctrl, value); } 104 | pitchWheel(chan: number, value: number) { this.fDSPCode.pitchWheel(chan, value); } 105 | keyOn(channel: number, pitch: number, velocity: number) { this.fDSPCode.keyOn(channel, pitch, velocity); } 106 | keyOff(channel: number, pitch: number, velocity: number) { this.fDSPCode.keyOff(channel, pitch, velocity); } 107 | 108 | setParamValue(path: string, value: number) { this.fDSPCode.setParamValue(path, value); } 109 | getParamValue(path: string) { return this.fDSPCode.getParamValue(path); } 110 | getParams() { return this.fDSPCode.getParams(); } 111 | 112 | getMeta() { return this.fDSPCode.getMeta(); } 113 | getJSON() { return this.fDSPCode.getJSON(); } 114 | getDescriptors() { return this.fDSPCode.getDescriptors(); } 115 | getUI() { return this.fDSPCode.getUI(); } 116 | 117 | start() { this.fDSPCode.start(); } 118 | stop() { this.fDSPCode.stop(); } 119 | 120 | destroy() { this.fDSPCode.destroy(); } 121 | 122 | get hasAccInput() { return this.fDSPCode.hasAccInput; } 123 | 124 | propagateAcc(accelerationIncludingGravity: NonNullable, invert: boolean = false) { 125 | this.fDSPCode.propagateAcc(accelerationIncludingGravity, invert); 126 | } 127 | 128 | get hasGyrInput() { return this.fDSPCode.hasGyrInput; } 129 | 130 | propagateGyr(event: Pick) { 131 | this.fDSPCode.propagateGyr(event); 132 | } 133 | } 134 | 135 | export class FaustMonoScriptProcessorNode extends FaustScriptProcessorNode implements IFaustMonoWebAudioDsp { 136 | } 137 | 138 | export class FaustPolyScriptProcessorNode extends FaustScriptProcessorNode implements IFaustPolyWebAudioDsp { 139 | keyOn(channel: number, pitch: number, velocity: number) { this.fDSPCode.keyOn(channel, pitch, velocity); } 140 | keyOff(channel: number, pitch: number, velocity: number) { this.fDSPCode.keyOff(channel, pitch, velocity); } 141 | allNotesOff(hard: boolean) { this.fDSPCode.allNotesOff(hard); } 142 | } 143 | -------------------------------------------------------------------------------- /src/FaustSvgDiagrams.ts: -------------------------------------------------------------------------------- 1 | import type FaustCompiler from "./FaustCompiler"; 2 | 3 | interface IFaustSvgDiagrams { 4 | /** 5 | * Generates auxiliary files from Faust code. The output depends on the compiler options. 6 | * 7 | * @param name - the DSP's name 8 | * @param code - Faust code 9 | * @param args - compilation args 10 | * @returns the svg diagrams as a filename - svg string map 11 | */ 12 | from(name: string, code: string, args: string): Record; 13 | } 14 | 15 | class FaustSvgDiagrams implements IFaustSvgDiagrams { 16 | private compiler: FaustCompiler; 17 | 18 | constructor(compiler: FaustCompiler) { 19 | this.compiler = compiler; 20 | } 21 | 22 | from(name: string, code: string, args: string) { 23 | const fs = this.compiler.fs(); 24 | try { 25 | const files: string[] = fs.readdir(`/${name}-svg/`); 26 | files.filter(file => file !== "." && file !== "..").forEach(file => fs.unlink(`/${name}-svg/${file}`)); 27 | } catch { } 28 | const success = this.compiler.generateAuxFiles(name, code, `-lang wasm -o binary -svg ${args}`); 29 | if (!success) throw new Error(this.compiler.getErrorMessage()); 30 | const svgs: Record = {}; 31 | const files: string[] = fs.readdir(`/${name}-svg/`); 32 | files.filter(file => file !== "." && file !== "..").forEach(file => svgs[file] = fs.readFile(`/${name}-svg/${file}`, { encoding: "utf8" }) as string); 33 | return svgs; 34 | } 35 | } 36 | 37 | export default FaustSvgDiagrams; 38 | -------------------------------------------------------------------------------- /src/FaustWasmInstantiator.ts: -------------------------------------------------------------------------------- 1 | import { FaustDspInstance, FaustMonoDspInstance, FaustPolyDspInstance, IFaustDspInstance, IFaustMixerInstance } from "./FaustDspInstance"; 2 | import type { FaustDspFactory, FaustDspMeta, LooseFaustDspFactory } from "./types"; 3 | 4 | class FaustWasmInstantiator { 5 | private static createWasmImport(memory?: WebAssembly.Memory) { 6 | return { 7 | env: { 8 | memory: memory || new WebAssembly.Memory({ initial: 100 }), 9 | memoryBase: 0, 10 | tableBase: 0, 11 | // Integer version 12 | _abs: Math.abs, 13 | // Float version 14 | _acosf: Math.acos, _asinf: Math.asin, _atanf: Math.atan, _atan2f: Math.atan2, 15 | _ceilf: Math.ceil, _cosf: Math.cos, _expf: Math.exp, _floorf: Math.floor, 16 | _fmodf: (x: number, y: number) => x % y, 17 | _logf: Math.log, _log10f: Math.log10, _max_f: Math.max, _min_f: Math.min, 18 | _remainderf: (x: number, y: number) => x - Math.round(x / y) * y, 19 | _powf: Math.pow, _roundf: Math.round, _sinf: Math.sin, _sqrtf: Math.sqrt, _tanf: Math.tan, 20 | _acoshf: Math.acosh, _asinhf: Math.asinh, _atanhf: Math.atanh, 21 | _coshf: Math.cosh, _sinhf: Math.sinh, _tanhf: Math.tanh, 22 | _isnanf: Number.isNaN, _isinff: (x: number) => !isFinite(x), 23 | _copysignf: (x: number, y: number) => (Math.sign(x) === Math.sign(y) ? x : -x), 24 | 25 | // Double version 26 | _acos: Math.acos, _asin: Math.asin, _atan: Math.atan, _atan2: Math.atan2, 27 | _ceil: Math.ceil, _cos: Math.cos, _exp: Math.exp, _floor: Math.floor, 28 | _fmod: (x: number, y: number) => x % y, 29 | _log: Math.log, _log10: Math.log10, _max_: Math.max, _min_: Math.min, 30 | _remainder: (x: number, y: number) => x - Math.round(x / y) * y, 31 | _pow: Math.pow, _round: Math.round, _sin: Math.sin, _sqrt: Math.sqrt, _tan: Math.tan, 32 | _acosh: Math.acosh, _asinh: Math.asinh, _atanh: Math.atanh, 33 | _cosh: Math.cosh, _sinh: Math.sinh, _tanh: Math.tanh, 34 | _isnan: Number.isNaN, _isinf: (x: number) => !isFinite(x), 35 | _copysign: (x: number, y: number) => (Math.sign(x) === Math.sign(y) ? x : -x), 36 | 37 | table: new WebAssembly.Table({ initial: 0, element: "anyfunc" }) 38 | } 39 | }; 40 | } 41 | private static createWasmMemoryPoly(voicesIn: number, sampleSize: number, dspMeta: FaustDspMeta, effectMeta: FaustDspMeta, bufferSize: number) { 42 | // Hack : at least 4 voices (to avoid weird wasm memory bug?) 43 | const voices = Math.max(4, voicesIn); 44 | // Memory allocator 45 | const ptrSize = sampleSize; // Done on wast/wasm backend side 46 | const pow2limit = (x: number) => { 47 | let n = 65536; // Minimum = 64 kB 48 | while (n < x) { n *= 2; } 49 | return n; 50 | }; 51 | const effectSize = effectMeta ? effectMeta.size : 0; 52 | let memorySize = pow2limit( 53 | effectSize 54 | + dspMeta.size * voices 55 | + (dspMeta.inputs + dspMeta.outputs * 2) // + 2 for effect 56 | * (ptrSize + bufferSize * sampleSize) 57 | ) / 65536; 58 | memorySize = Math.max(2, memorySize); // At least 2 59 | return new WebAssembly.Memory({ initial: memorySize }); 60 | }; 61 | 62 | private static createWasmMemoryMono(sampleSize: number, dspMeta: FaustDspMeta, bufferSize: number) { 63 | // Memory allocator 64 | const ptrSize = sampleSize; // Done on wast/wasm backend side 65 | const memorySize = (dspMeta.size + (dspMeta.inputs + dspMeta.outputs) * (ptrSize + bufferSize * sampleSize)) / 65536; 66 | return new WebAssembly.Memory({ initial: memorySize * 2 }); // Safer to have a bit more memory 67 | } 68 | 69 | private static createMonoDSPInstanceAux(instance: WebAssembly.Instance, json: string, mem: WebAssembly.Memory | null = null) { 70 | const functions = instance.exports as IFaustDspInstance & WebAssembly.Exports; 71 | const api = new FaustDspInstance(functions); 72 | const memory: any = (mem) ? mem : instance.exports.memory; 73 | return { memory, api, json } as FaustMonoDspInstance; 74 | } 75 | 76 | private static createMemoryMono(monoFactory: LooseFaustDspFactory) { 77 | // Parse JSON to get 'size' and 'inputs/outputs' infos 78 | const monoMeta: FaustDspMeta = JSON.parse(monoFactory.json); 79 | const sampleSize = monoMeta.compile_options.match("-double") ? 8 : 4; 80 | return this.createWasmMemoryMono(sampleSize, monoMeta, 8192); 81 | 82 | } 83 | private static createMemoryPoly(voices: number, voiceFactory: LooseFaustDspFactory, effectFactory?: LooseFaustDspFactory) { 84 | // Parse JSON to get 'size' and 'inputs/outputs' infos 85 | const voiceMeta: FaustDspMeta = JSON.parse(voiceFactory.json); 86 | const effectMeta: FaustDspMeta = (effectFactory && effectFactory.json) ? JSON.parse(effectFactory.json) : null; 87 | const sampleSize = voiceMeta.compile_options.match("-double") ? 8 : 4; 88 | // Memory will be shared by voice, mixer and (possibly) effect instances 89 | return this.createWasmMemoryPoly(voices, sampleSize, voiceMeta, effectMeta, 8192); 90 | } 91 | 92 | private static createMixerAux(mixerModule: WebAssembly.Module, memory: WebAssembly.Memory) { 93 | // Create mixer instance 94 | const mixerImport = { 95 | imports: { print: console.log }, 96 | memory: { memory } 97 | }; 98 | const mixerInstance = new WebAssembly.Instance(mixerModule, mixerImport); 99 | const mixerFunctions = mixerInstance.exports as IFaustMixerInstance & WebAssembly.Exports; 100 | return mixerFunctions; 101 | } 102 | 103 | // Public API 104 | static async loadDSPFactory(wasmPath: string, jsonPath: string) { 105 | const wasmFile = await fetch(wasmPath); 106 | if (!wasmFile.ok) { 107 | throw new Error(`=> exception raised while running loadDSPFactory, file not found: ${wasmPath}`); 108 | } 109 | try { 110 | const wasmBuffer = await wasmFile.arrayBuffer(); 111 | const module = await WebAssembly.compile(wasmBuffer); 112 | const jsonFile = await fetch(jsonPath); 113 | const json = await jsonFile.text(); 114 | const meta: FaustDspMeta = JSON.parse(json); 115 | const cOptions = meta.compile_options; 116 | const poly = cOptions.indexOf('wasm-e') !== -1; 117 | return { cfactory: 0, code: new Uint8Array(wasmBuffer), module, json, poly } as FaustDspFactory; 118 | } catch (e) { 119 | // console.error(`=> exception raised while running loadDSPFactory: ${e}`); 120 | throw e; 121 | } 122 | } 123 | 124 | static async loadDSPMixer(mixerPath: string, fs?: typeof FS) { 125 | try { 126 | let mixerBuffer = null; 127 | if (fs) { 128 | mixerBuffer = fs.readFile(mixerPath, { encoding: "binary" }); 129 | } else { 130 | const mixerFile = await fetch(mixerPath); 131 | mixerBuffer = await mixerFile.arrayBuffer(); 132 | } 133 | // Compile mixer 134 | return WebAssembly.compile(mixerBuffer); 135 | } catch (e) { 136 | // console.error(`=> exception raised while running loadMixer: ${e}`); 137 | throw e; 138 | } 139 | } 140 | 141 | static async createAsyncMonoDSPInstance(factory: LooseFaustDspFactory) { 142 | 143 | // Regular expression to match the 'type: soundfile' pattern 144 | const pattern = /"type":\s*"soundfile"/; 145 | // Check if the pattern exists in the JSON string 146 | const isDetected = pattern.test(factory.json); 147 | 148 | if (isDetected) { 149 | const memory = this.createMemoryMono(factory); 150 | const instance = await WebAssembly.instantiate(factory.module, this.createWasmImport(memory)); 151 | return this.createMonoDSPInstanceAux(instance, factory.json, memory); 152 | } else { 153 | // Otherwise, we can create the instance using the wasm internal memory allocated by the wasm module 154 | const instance = await WebAssembly.instantiate(factory.module, this.createWasmImport()); 155 | return this.createMonoDSPInstanceAux(instance, factory.json); 156 | } 157 | } 158 | 159 | static createSyncMonoDSPInstance(factory: LooseFaustDspFactory) { 160 | 161 | // Regular expression to match the 'type: soundfile' pattern 162 | const pattern = /"type":\s*"soundfile"/; 163 | // Check if the pattern exists in the JSON string 164 | const isDetected = pattern.test(factory.json); 165 | 166 | // If the JSON contains a soundfile UI element, we need to create a memory object 167 | if (isDetected) { 168 | const memory = this.createMemoryMono(factory); 169 | const instance = new WebAssembly.Instance(factory.module, this.createWasmImport(memory)); 170 | return this.createMonoDSPInstanceAux(instance, factory.json, memory); 171 | } else { 172 | // Otherwise, we can create the instance using the wasm internal memory allocated by the wasm module 173 | const instance = new WebAssembly.Instance(factory.module, this.createWasmImport()); 174 | return this.createMonoDSPInstanceAux(instance, factory.json); 175 | } 176 | } 177 | 178 | static async createAsyncPolyDSPInstance(voiceFactory: LooseFaustDspFactory, mixerModule: WebAssembly.Module, voices: number, effectFactory?: LooseFaustDspFactory): Promise { 179 | const memory = this.createMemoryPoly(voices, voiceFactory, effectFactory); 180 | // Create voice 181 | const voiceInstance = await WebAssembly.instantiate(voiceFactory.module, this.createWasmImport(memory)); 182 | const voiceFunctions = voiceInstance.exports as IFaustDspInstance & WebAssembly.Exports; 183 | const voiceAPI = new FaustDspInstance(voiceFunctions); 184 | // Create mixer 185 | const mixerAPI = this.createMixerAux(mixerModule, memory); 186 | 187 | // Possibly create effect instance 188 | if (effectFactory) { 189 | const effectInstance = await WebAssembly.instantiate(effectFactory.module, this.createWasmImport(memory)); 190 | const effectFunctions = effectInstance.exports as IFaustDspInstance & WebAssembly.Exports; 191 | const effectAPI = new FaustDspInstance(effectFunctions); 192 | return { 193 | memory, 194 | voices, 195 | voiceAPI, 196 | effectAPI, 197 | mixerAPI, 198 | voiceJSON: voiceFactory.json, 199 | effectJSON: effectFactory.json 200 | } as FaustPolyDspInstance; 201 | } else { 202 | return { 203 | memory, 204 | voices, 205 | voiceAPI, 206 | mixerAPI, 207 | voiceJSON: voiceFactory.json 208 | } as FaustPolyDspInstance; 209 | } 210 | } 211 | 212 | static createSyncPolyDSPInstance(voiceFactory: LooseFaustDspFactory, mixerModule: WebAssembly.Module, voices: number, effectFactory?: LooseFaustDspFactory): FaustPolyDspInstance { 213 | const memory = this.createMemoryPoly(voices, voiceFactory, effectFactory); 214 | // Create voice 215 | const voiceInstance = new WebAssembly.Instance(voiceFactory.module, this.createWasmImport(memory)); 216 | const voiceFunctions = voiceInstance.exports as IFaustDspInstance & WebAssembly.Exports; 217 | const voiceAPI = new FaustDspInstance(voiceFunctions); 218 | // Create mixer 219 | const mixerAPI = this.createMixerAux(mixerModule, memory); 220 | 221 | // Possibly create effect instance 222 | if (effectFactory) { 223 | const effectInstance = new WebAssembly.Instance(effectFactory.module, this.createWasmImport(memory)); 224 | const effectFunctions = effectInstance.exports as IFaustDspInstance & WebAssembly.Exports; 225 | const effectAPI = new FaustDspInstance(effectFunctions); 226 | return { 227 | memory, 228 | voices, 229 | voiceAPI, 230 | effectAPI, 231 | mixerAPI, 232 | voiceJSON: voiceFactory.json, 233 | effectJSON: effectFactory.json 234 | } as FaustPolyDspInstance; 235 | } else { 236 | return { 237 | memory, 238 | voices, 239 | voiceAPI, 240 | mixerAPI, 241 | voiceJSON: voiceFactory.json 242 | } as FaustPolyDspInstance; 243 | } 244 | } 245 | } 246 | 247 | export default FaustWasmInstantiator; 248 | -------------------------------------------------------------------------------- /src/LibFaust.ts: -------------------------------------------------------------------------------- 1 | import type { FaustModule, LibFaustWasm, FaustInfoType } from "./types"; 2 | 3 | export interface ILibFaust extends LibFaustWasm { 4 | module(): FaustModule; 5 | fs(): typeof FS; 6 | } 7 | 8 | class LibFaust implements ILibFaust { 9 | private fModule: FaustModule; 10 | private fCompiler: LibFaustWasm; 11 | private fFileSystem: typeof FS; 12 | 13 | constructor(module: FaustModule) { 14 | this.fModule = module; 15 | this.fCompiler = new module.libFaustWasm(); 16 | this.fFileSystem = this.fModule.FS; 17 | } 18 | module() { 19 | return this.fModule; 20 | } 21 | fs() { 22 | return this.fFileSystem; 23 | } 24 | version() { 25 | return this.fCompiler.version(); 26 | } 27 | createDSPFactory(name: string, code: string, args: string, useInternalMemory: boolean) { 28 | return this.fCompiler.createDSPFactory(name, code, args, useInternalMemory); 29 | } 30 | deleteDSPFactory(cFactory: number) { 31 | return this.fCompiler.deleteDSPFactory(cFactory); 32 | } 33 | expandDSP(name: string, code: string, args: string) { 34 | return this.fCompiler.expandDSP(name, code, args); 35 | } 36 | generateAuxFiles(name: string, code: string, args: string) { 37 | return this.fCompiler.generateAuxFiles(name, code, args); 38 | } 39 | deleteAllDSPFactories() { 40 | return this.fCompiler.deleteAllDSPFactories(); 41 | } 42 | getErrorAfterException() { 43 | return this.fCompiler.getErrorAfterException(); 44 | } 45 | cleanupAfterException() { 46 | return this.fCompiler.cleanupAfterException(); 47 | } 48 | getInfos(what: FaustInfoType) { 49 | return this.fCompiler.getInfos(what); 50 | } 51 | toString() { 52 | return `LibFaust module: ${this.fModule}, compiler: ${this.fCompiler}`; 53 | } 54 | 55 | } 56 | 57 | export default LibFaust; 58 | -------------------------------------------------------------------------------- /src/SoundfileReader.ts: -------------------------------------------------------------------------------- 1 | import { FaustBaseWebAudioDsp } from "./FaustWebAudioDsp"; 2 | import type { AudioData, FaustDspMeta, FaustUIItem, LooseFaustDspFactory } from "./types"; 3 | 4 | /** Read metadata and fetch soundfiles */ 5 | class SoundfileReader { 6 | 7 | // Set the fallback paths 8 | static get fallbackPaths() { return [location.href, this.getParentUrl(location.href), location.origin]; } 9 | 10 | /** 11 | * Extract the parent URL from an URL. 12 | * @param url : the URL 13 | * @returns : the parent URL 14 | */ 15 | private static getParentUrl(url: string) { 16 | return url.substring(0, url.lastIndexOf('/') + 1); 17 | } 18 | 19 | /** 20 | * Convert an audio buffer to audio data. 21 | * 22 | * @param audioBuffer : the audio buffer to convert 23 | * @returns : the audio data 24 | */ 25 | private static toAudioData(audioBuffer: AudioBuffer): AudioData { 26 | const { sampleRate, numberOfChannels } = audioBuffer; 27 | return { 28 | sampleRate, 29 | audioBuffer: new Array(numberOfChannels).fill(null).map((v, i) => audioBuffer.getChannelData(i)) 30 | } as AudioData; 31 | } 32 | 33 | /** 34 | * Extract the URLs from the metadata. 35 | * 36 | * @param dspMeta : the metadata 37 | * @returns : the URLs 38 | */ 39 | static findSoundfilesFromMeta(dspMeta: FaustDspMeta): LooseFaustDspFactory["soundfiles"] { 40 | const soundfiles: LooseFaustDspFactory["soundfiles"] = {}; 41 | const callback = (item: FaustUIItem) => { 42 | if (item.type === "soundfile") { 43 | const urls = FaustBaseWebAudioDsp.splitSoundfileNames(item.url); 44 | // soundfiles.map[item.label] = urls; 45 | urls.forEach(url => soundfiles[url] = null); 46 | } 47 | }; 48 | FaustBaseWebAudioDsp.parseUI(dspMeta.ui, callback); 49 | return soundfiles; 50 | } 51 | /** 52 | * Check if the file exists. 53 | * 54 | * @param url : the url of the file to check 55 | * @returns : true if the file exists, otherwise false 56 | */ 57 | private static async checkFileExists(url: string): Promise { 58 | try { 59 | console.log(`"checkFileExists" url: ${url}`); 60 | // Fetch in "HEAD" mode does not properly work with the service-worker.js cache, so use "GET" mode for now 61 | //const response = await fetch(url, { method: "HEAD" }); 62 | const response = await fetch(url); 63 | console.log(`"checkFileExists" response.ok: ${response.ok}`); 64 | return response.ok; // Will be true if the status code is 200-299 65 | } catch (error) { 66 | console.error('Fetch error:', error); 67 | return false; 68 | } 69 | } 70 | 71 | /** 72 | * Fetch the soundfile. 73 | * 74 | * @param url : the url of the soundfile 75 | * @param audioCtx : the audio context 76 | * @returns : the audio data 77 | */ 78 | private static async fetchSoundfile(url: string, audioCtx: BaseAudioContext): Promise { 79 | console.log(`Loading sound file from ${url}`); 80 | const response = await fetch(url); 81 | if (!response.ok) throw new Error(`Failed to load sound file from ${url}: ${response.statusText}`); 82 | // Decode the audio data 83 | const arrayBuffer = await response.arrayBuffer(); 84 | const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer); 85 | return this.toAudioData(audioBuffer); 86 | } 87 | 88 | /** 89 | * Load the soundfile. 90 | * 91 | * @param filename : the filename 92 | * @param metaUrls : the metadata URLs 93 | * @param soundfiles : the soundfiles 94 | * @param audioCtx : the audio context 95 | */ 96 | private static async loadSoundfile(filename: string, metaUrls: string[], soundfiles: LooseFaustDspFactory["soundfiles"], audioCtx: BaseAudioContext): Promise { 97 | if (soundfiles[filename]) return; 98 | const urlsToCheck = [filename, ...[...metaUrls, ...this.fallbackPaths].map(path => new URL(filename, path.endsWith("/") ? path : `${path}/`).href)]; 99 | const checkResults = await Promise.all(urlsToCheck.map(url => this.checkFileExists(url))); 100 | const successIndex = checkResults.findIndex(r => !!r); 101 | if (successIndex === -1) throw new Error(`Failed to load sound file ${filename}, all check failed.`); 102 | soundfiles[filename] = await this.fetchSoundfile(urlsToCheck[successIndex], audioCtx); 103 | } 104 | 105 | /** 106 | * Load the soundfiles, public API. 107 | * 108 | * @param dspMeta : the metadata 109 | * @param soundfilesIn : the soundfiles 110 | * @param audioCtx : the audio context 111 | * @returns : the soundfiles 112 | */ 113 | static async loadSoundfiles(dspMeta: FaustDspMeta, soundfilesIn: LooseFaustDspFactory["soundfiles"], audioCtx: BaseAudioContext): Promise { 114 | const metaUrls = FaustBaseWebAudioDsp.extractUrlsFromMeta(dspMeta); 115 | const soundfiles = this.findSoundfilesFromMeta(dspMeta); 116 | for (const id in soundfiles) { 117 | if (soundfilesIn[id]) { 118 | soundfiles[id] = soundfilesIn[id]; 119 | continue; 120 | } 121 | try { 122 | await this.loadSoundfile(id, metaUrls, soundfiles, audioCtx); 123 | } catch (error) { 124 | console.error(error); 125 | } 126 | } 127 | return soundfiles; 128 | } 129 | } 130 | 131 | export default SoundfileReader; 132 | -------------------------------------------------------------------------------- /src/WavDecoder.ts: -------------------------------------------------------------------------------- 1 | export interface WavDecoderOptions { 2 | symmetric?: boolean; 3 | shared?: boolean; 4 | } 5 | interface Format { 6 | formatId: number; 7 | float: boolean; 8 | numberOfChannels: number; 9 | sampleRate: number; 10 | byteRate: number; 11 | blockSize: number; 12 | bitDepth: number; 13 | } 14 | 15 | /** 16 | * Code from https://github.com/mohayonao/wav-decoder 17 | */ 18 | class WavDecoder { 19 | static decode(buffer: ArrayBuffer, options?: WavDecoderOptions) { 20 | const dataView = new DataView(buffer); 21 | const reader = new Reader(dataView); 22 | if (reader.string(4) !== "RIFF") { 23 | throw new TypeError("Invalid WAV file"); 24 | } 25 | reader.uint32(); // skip file length 26 | if (reader.string(4) !== "WAVE") { 27 | throw new TypeError("Invalid WAV file"); 28 | } 29 | let format: Format | null = null; 30 | let audioData: { 31 | numberOfChannels: number; 32 | length: number; 33 | sampleRate: number; 34 | channelData: Float32Array[]; 35 | } | null = null; 36 | do { 37 | const chunkType = reader.string(4); 38 | const chunkSize = reader.uint32(); 39 | if (chunkType === "fmt ") { 40 | format = this.decodeFormat(reader, chunkSize); 41 | } else if (chunkType === "data") { 42 | audioData = this.decodeData(reader, chunkSize, format as Format, options || {}); 43 | } else { 44 | reader.skip(chunkSize); 45 | } 46 | } while (audioData === null); 47 | return audioData; 48 | } 49 | private static decodeFormat(reader: Reader, chunkSize: number) { 50 | const formats = { 51 | 0x0001: "lpcm", 52 | 0x0003: "lpcm" 53 | }; 54 | const formatId = reader.uint16(); 55 | if (!formats.hasOwnProperty(formatId)) { 56 | throw new TypeError("Unsupported format in WAV file: 0x" + formatId.toString(16)); 57 | } 58 | const format: Format = { 59 | formatId: formatId, 60 | float: formatId === 0x0003, 61 | numberOfChannels: reader.uint16(), 62 | sampleRate: reader.uint32(), 63 | byteRate: reader.uint32(), 64 | blockSize: reader.uint16(), 65 | bitDepth: reader.uint16() 66 | }; 67 | reader.skip(chunkSize - 16); 68 | return format; 69 | } 70 | private static decodeData(reader: Reader, chunkSizeIn: number, format: Format, options: WavDecoderOptions) { 71 | const chunkSize = Math.min(chunkSizeIn, reader.remain()); 72 | const length = Math.floor(chunkSize / format.blockSize); 73 | const numberOfChannels = format.numberOfChannels; 74 | const sampleRate = format.sampleRate; 75 | const channelData: Float32Array[] = new Array(numberOfChannels); 76 | for (let ch = 0; ch < numberOfChannels; ch++) { 77 | const AB = options.shared ? (globalThis.SharedArrayBuffer || globalThis.ArrayBuffer) : globalThis.ArrayBuffer; 78 | const ab = new AB(length * Float32Array.BYTES_PER_ELEMENT); 79 | channelData[ch] = new Float32Array(ab); 80 | } 81 | this.readPCM(reader, channelData, length, format, options); 82 | return { 83 | numberOfChannels, 84 | length, 85 | sampleRate, 86 | channelData 87 | }; 88 | } 89 | private static readPCM(reader: Reader, channelData: Float32Array[], length: number, format: Format, options: WavDecoderOptions) { 90 | const bitDepth = format.bitDepth; 91 | const decoderOption = format.float ? "f" : options.symmetric ? "s" : ""; 92 | const methodName = "pcm" + bitDepth + decoderOption as `pcm${8 | 16 | 32}${"f" | "s" | ""}`; 93 | if (!(reader as any)[methodName]) { 94 | throw new TypeError("Not supported bit depth: " + format.bitDepth); 95 | } 96 | const read: () => number = (reader as any)[methodName].bind(reader); 97 | const numberOfChannels = format.numberOfChannels; 98 | for (let i = 0; i < length; i++) { 99 | for (let ch = 0; ch < numberOfChannels; ch++) { 100 | channelData[ch][i] = read(); 101 | } 102 | } 103 | } 104 | } 105 | 106 | class Reader { 107 | pos = 0; 108 | dataView: DataView; 109 | constructor(dataView: DataView) { 110 | this.dataView = dataView; 111 | } 112 | remain() { 113 | return this.dataView.byteLength - this.pos; 114 | } 115 | skip(n: number) { 116 | this.pos += n; 117 | } 118 | uint8() { 119 | const data = this.dataView.getUint8(this.pos); 120 | this.pos += 1; 121 | return data; 122 | } 123 | int16() { 124 | const data = this.dataView.getInt16(this.pos, true); 125 | this.pos += 2; 126 | return data; 127 | } 128 | uint16() { 129 | const data = this.dataView.getUint16(this.pos, true); 130 | this.pos += 2; 131 | return data; 132 | } 133 | uint32() { 134 | const data = this.dataView.getUint32(this.pos, true); 135 | this.pos += 4; 136 | return data; 137 | } 138 | string(n: number) { 139 | let data = ""; 140 | for (let i = 0; i < n; i++) { 141 | data += String.fromCharCode(this.uint8()); 142 | } 143 | return data; 144 | } 145 | pcm8() { 146 | const data = this.dataView.getUint8(this.pos) - 128; 147 | this.pos += 1; 148 | return data < 0 ? data / 128 : data / 127; 149 | } 150 | pcm8s() { 151 | const data = this.dataView.getUint8(this.pos) - 127.5; 152 | this.pos += 1; 153 | return data / 127.5; 154 | } 155 | pcm16() { 156 | const data = this.dataView.getInt16(this.pos, true); 157 | this.pos += 2; 158 | return data < 0 ? data / 32768 : data / 32767; 159 | } 160 | pcm16s() { 161 | const data = this.dataView.getInt16(this.pos, true); 162 | this.pos += 2; 163 | return data / 32768; 164 | } 165 | pcm24() { 166 | const x0 = this.dataView.getUint8(this.pos + 0); 167 | const x1 = this.dataView.getUint8(this.pos + 1); 168 | const x2 = this.dataView.getUint8(this.pos + 2); 169 | const xx = (x0 + (x1 << 8) + (x2 << 16)); 170 | 171 | const data = xx > 0x800000 ? xx - 0x1000000 : xx; 172 | this.pos += 3; 173 | return data < 0 ? data / 8388608 : data / 8388607; 174 | } 175 | pcm24s() { 176 | const x0 = this.dataView.getUint8(this.pos + 0); 177 | const x1 = this.dataView.getUint8(this.pos + 1); 178 | const x2 = this.dataView.getUint8(this.pos + 2); 179 | const xx = (x0 + (x1 << 8) + (x2 << 16)); 180 | 181 | const data = xx > 0x800000 ? xx - 0x1000000 : xx; 182 | this.pos += 3; 183 | return data / 8388608; 184 | } 185 | pcm32() { 186 | const data = this.dataView.getInt32(this.pos, true); 187 | this.pos += 4; 188 | return data < 0 ? data / 2147483648 : data / 2147483647; 189 | } 190 | pcm32s() { 191 | const data = this.dataView.getInt32(this.pos, true); 192 | this.pos += 4; 193 | return data / 2147483648; 194 | } 195 | pcm32f() { 196 | const data = this.dataView.getFloat32(this.pos, true); 197 | this.pos += 4; 198 | return data; 199 | } 200 | pcm64f() { 201 | const data = this.dataView.getFloat64(this.pos, true); 202 | this.pos += 8; 203 | return data; 204 | } 205 | } 206 | 207 | export default WavDecoder; 208 | -------------------------------------------------------------------------------- /src/WavEncoder.ts: -------------------------------------------------------------------------------- 1 | export interface WavEncoderOptions { 2 | bitDepth: number; 3 | float?: boolean; 4 | symmetric?: boolean; 5 | shared?: boolean; 6 | sampleRate: number; 7 | } 8 | interface Format { 9 | formatId: number; 10 | float: boolean; 11 | symmetric: boolean; 12 | numberOfChannels: number; 13 | sampleRate: number; 14 | length: number; 15 | bitDepth: number; 16 | byteDepth: number; 17 | } 18 | 19 | /** 20 | * Code from https://github.com/mohayonao/wav-encoder 21 | */ 22 | class WavEncoder { 23 | static encode(audioBuffer: Float32Array[], options: WavEncoderOptions) { 24 | const numberOfChannels = audioBuffer.length; 25 | const length = audioBuffer[0].length; 26 | const { shared, float } = options; 27 | const bitDepth = float ? 32 : ((options.bitDepth | 0) || 16); 28 | const byteDepth = bitDepth >> 3; 29 | const byteLength = length * numberOfChannels * byteDepth; 30 | // eslint-disable-next-line no-undef 31 | const AB = shared ? (globalThis.SharedArrayBuffer || globalThis.ArrayBuffer) : globalThis.ArrayBuffer; 32 | const ab = new AB((44 + byteLength) * Uint8Array.BYTES_PER_ELEMENT); 33 | const dataView = new DataView(ab); 34 | const writer = new Writer(dataView); 35 | const format: Format = { 36 | formatId: float ? 0x0003 : 0x0001, 37 | float: !!float, 38 | numberOfChannels, 39 | sampleRate: options.sampleRate, 40 | symmetric: !!options.symmetric, 41 | length, 42 | bitDepth, 43 | byteDepth 44 | }; 45 | this.writeHeader(writer, format); 46 | this.writeData(writer, audioBuffer, format); 47 | return ab; 48 | } 49 | private static writeHeader(writer: Writer, format: Format) { 50 | const { formatId, sampleRate, bitDepth, numberOfChannels, length, byteDepth } = format; 51 | writer.string("RIFF"); 52 | writer.uint32(writer.dataView.byteLength - 8); 53 | writer.string("WAVE"); 54 | writer.string("fmt "); 55 | writer.uint32(16); 56 | writer.uint16(formatId); 57 | writer.uint16(numberOfChannels); 58 | writer.uint32(sampleRate); 59 | writer.uint32(sampleRate * numberOfChannels * byteDepth); 60 | writer.uint16(numberOfChannels * byteDepth); 61 | writer.uint16(bitDepth); 62 | writer.string("data"); 63 | writer.uint32(length * numberOfChannels * byteDepth); 64 | return writer.pos; 65 | } 66 | private static writeData(writer: Writer, audioBuffer: Float32Array[], format: Format) { 67 | const { bitDepth, float, length, numberOfChannels, symmetric } = format; 68 | if (bitDepth === 32 && float) { 69 | const { dataView, pos } = writer; 70 | const ab = dataView.buffer; 71 | const f32View = new Float32Array(ab, pos); 72 | if (numberOfChannels === 1) { 73 | f32View.set(audioBuffer[0]); 74 | return; 75 | } 76 | for (let ch = 0; ch < numberOfChannels; ch++) { 77 | const channel = audioBuffer[ch]; 78 | for (let i = 0; i < length; i++) { 79 | f32View[i * numberOfChannels + ch] = channel[i]; 80 | } 81 | } 82 | return; 83 | } 84 | const encoderOption = float ? "f" : symmetric ? "s" : ""; 85 | const methodName = "pcm" + bitDepth + encoderOption; 86 | 87 | if (!(writer as any)[methodName]) { 88 | throw new TypeError("Not supported bit depth: " + bitDepth); 89 | } 90 | 91 | const write: (value: number) => void = (writer as any)[methodName].bind(writer); 92 | 93 | for (let i = 0; i < length; i++) { 94 | for (let j = 0; j < numberOfChannels; j++) { 95 | write(audioBuffer[j][i]); 96 | } 97 | } 98 | } 99 | } 100 | 101 | class Writer { 102 | pos = 0; 103 | dataView: DataView; 104 | constructor(dataView: DataView) { 105 | this.dataView = dataView; 106 | } 107 | int16(value: number) { 108 | this.dataView.setInt16(this.pos, value, true); 109 | this.pos += 2; 110 | } 111 | uint16(value: number) { 112 | this.dataView.setUint16(this.pos, value, true); 113 | this.pos += 2; 114 | } 115 | uint32(value: number) { 116 | this.dataView.setUint32(this.pos, value, true); 117 | this.pos += 4; 118 | } 119 | string(value: string) { 120 | for (let i = 0, imax = value.length; i < imax; i++) { 121 | this.dataView.setUint8(this.pos++, value.charCodeAt(i)); 122 | } 123 | } 124 | pcm8(valueIn: number) { 125 | let value = valueIn; 126 | value = Math.max(-1, Math.min(value, +1)); 127 | value = (value * 0.5 + 0.5) * 255; 128 | value = Math.round(value) | 0; 129 | this.dataView.setUint8(this.pos, value/* , true*/); 130 | this.pos += 1; 131 | } 132 | pcm8s(valueIn: number) { 133 | let value = valueIn; 134 | value = Math.round(value * 128) + 128; 135 | value = Math.max(0, Math.min(value, 255)); 136 | this.dataView.setUint8(this.pos, value/* , true*/); 137 | this.pos += 1; 138 | } 139 | pcm16(valueIn: number) { 140 | let value = valueIn; 141 | value = Math.max(-1, Math.min(value, +1)); 142 | value = value < 0 ? value * 32768 : value * 32767; 143 | value = Math.round(value) | 0; 144 | this.dataView.setInt16(this.pos, value, true); 145 | this.pos += 2; 146 | } 147 | pcm16s(valueIn: number) { 148 | let value = valueIn; 149 | value = Math.round(value * 32768); 150 | value = Math.max(-32768, Math.min(value, 32767)); 151 | this.dataView.setInt16(this.pos, value, true); 152 | this.pos += 2; 153 | } 154 | pcm24(valueIn: number) { 155 | let value = valueIn; 156 | value = Math.max(-1, Math.min(value, +1)); 157 | value = value < 0 ? 0x1000000 + value * 8388608 : value * 8388607; 158 | value = Math.round(value) | 0; 159 | 160 | const x0 = (value >> 0) & 0xFF; 161 | const x1 = (value >> 8) & 0xFF; 162 | const x2 = (value >> 16) & 0xFF; 163 | 164 | this.dataView.setUint8(this.pos + 0, x0); 165 | this.dataView.setUint8(this.pos + 1, x1); 166 | this.dataView.setUint8(this.pos + 2, x2); 167 | this.pos += 3; 168 | } 169 | pcm24s(valueIn: number) { 170 | let value = valueIn; 171 | value = Math.round(value * 8388608); 172 | value = Math.max(-8388608, Math.min(value, 8388607)); 173 | 174 | const x0 = (value >> 0) & 0xFF; 175 | const x1 = (value >> 8) & 0xFF; 176 | const x2 = (value >> 16) & 0xFF; 177 | 178 | this.dataView.setUint8(this.pos + 0, x0); 179 | this.dataView.setUint8(this.pos + 1, x1); 180 | this.dataView.setUint8(this.pos + 2, x2); 181 | this.pos += 3; 182 | } 183 | pcm32(valueIn: number) { 184 | let value = valueIn; 185 | value = Math.max(-1, Math.min(value, +1)); 186 | value = value < 0 ? value * 2147483648 : value * 2147483647; 187 | value = Math.round(value) | 0; 188 | this.dataView.setInt32(this.pos, value, true); 189 | this.pos += 4; 190 | } 191 | pcm32s(valueIn: number) { 192 | let value = valueIn; 193 | value = Math.round(value * 2147483648); 194 | value = Math.max(-2147483648, Math.min(value, +2147483647)); 195 | this.dataView.setInt32(this.pos, value, true); 196 | this.pos += 4; 197 | } 198 | pcm32f(value: number) { 199 | this.dataView.setFloat32(this.pos, value, true); 200 | this.pos += 4; 201 | } 202 | } 203 | 204 | export default WavEncoder; 205 | -------------------------------------------------------------------------------- /src/copyWebStandaloneAssets.d.ts: -------------------------------------------------------------------------------- 1 | declare const copyWebStandaloneAssets: (outputDir: string, dspName: string, poly?: boolean, effect?: boolean) => void; 2 | declare const copyWebPWAAssets: (outputDir: string, dspName: string, poly?: boolean, effect?: boolean) => void; 3 | declare const copyWebTemplateAssets: (outputDir: string, dspName: string, poly?: boolean, effect?: boolean) => void; 4 | 5 | export { copyWebStandaloneAssets, copyWebPWAAssets, copyWebTemplateAssets }; 6 | -------------------------------------------------------------------------------- /src/copyWebStandaloneAssets.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import { cpSync, cpSyncModify } from "../fileutils.js"; 5 | import { fileURLToPath } from "url"; 6 | 7 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 8 | const __filename = fileURLToPath(import.meta.url); 9 | 10 | /** 11 | * @param {string} outputDir - The output directory. 12 | * @param {string} dspName - The name of the DSP to be loaded. 13 | * @param {boolean} [poly] - Whether the DSP is polyphonic. 14 | * @param {boolean} [effect] - Whether the DSP has an effect module. 15 | */ 16 | const copyWebStandaloneAssets = (outputDir, dspName, poly = false, effect = false) => { 17 | console.log(`Writing assets files.`) 18 | if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir); 19 | 20 | // Define find and replace patterns 21 | const findAndReplace = ["FAUST_DSP_NAME", dspName]; 22 | if (poly) findAndReplace.push("FAUST_DSP_VOICES = 0", "FAUST_DSP_VOICES = 16"); 23 | if (poly && effect) findAndReplace.push("FAUST_DSP_HAS_EFFECT = false", "FAUST_DSP_HAS_EFFECT = true"); 24 | 25 | // Copy some files 26 | const createNodeJSPath = path.join(__dirname, "../assets/standalone/create-node.js"); 27 | cpSyncModify(createNodeJSPath, outputDir + `/create-node.js`, ...findAndReplace); 28 | 29 | const templateJSPath = path.join(__dirname, "../assets/standalone/index.js"); 30 | cpSyncModify(templateJSPath, outputDir + `/index.js`, ...findAndReplace); 31 | 32 | const templateHTMLPath = path.join(__dirname, "../assets/standalone/index.html"); 33 | cpSyncModify(templateHTMLPath, outputDir + `/index.html`, ...findAndReplace); 34 | 35 | const templateWorkerPath = path.join(__dirname, "../assets/standalone/service-worker.js"); 36 | cpSyncModify(templateWorkerPath, outputDir + `/service-worker.js`, ...findAndReplace); 37 | 38 | const templateIconPath = path.join(__dirname, "../assets/standalone/icon.png"); 39 | cpSync(templateIconPath, outputDir + `/icon.png`); 40 | 41 | const faustwasmPath = path.join(__dirname, "../assets/standalone/faustwasm"); 42 | cpSync(faustwasmPath, outputDir + "/faustwasm"); 43 | 44 | const faustuiPath = path.join(__dirname, "../assets/standalone/faust-ui"); 45 | cpSync(faustuiPath, outputDir + "/faust-ui"); 46 | 47 | const templateManifestPath = path.join(__dirname, "../assets/standalone/manifest.json"); 48 | cpSyncModify(templateManifestPath, outputDir + `/manifest.json`, ...findAndReplace); 49 | }; 50 | 51 | /** 52 | * @param {string} outputDir - The output directory. 53 | * @param {string} dspName - The name of the DSP to be loaded. 54 | * @param {boolean} [poly] - Whether the DSP is polyphonic. 55 | * @param {boolean} [effect] - Whether the DSP has an effect module. 56 | */ 57 | const copyWebPWAAssets = (outputDir, dspName, poly = false, effect = false) => { 58 | console.log(`Writing assets files.`) 59 | if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir); 60 | 61 | // Generate VERSION_DATE 62 | const now = new Date(); 63 | const VERSION_DATE = now.getFullYear().toString() + 64 | ("0" + (now.getMonth() + 1)).slice(-2) + 65 | ("0" + now.getDate()).slice(-2) + "-" + 66 | ("0" + now.getHours()).slice(-2) + 67 | ("0" + now.getMinutes()).slice(-2); 68 | 69 | // Define find and replace patterns 70 | const findAndReplace = ["FAUST_DSP_NAME", dspName]; 71 | findAndReplace.push("VERSION_DATE", VERSION_DATE); 72 | 73 | if (poly) findAndReplace.push("FAUST_DSP_VOICES = 0", "FAUST_DSP_VOICES = 16"); 74 | if (poly && effect) findAndReplace.push("FAUST_DSP_HAS_EFFECT = false", "FAUST_DSP_HAS_EFFECT = true"); 75 | 76 | // Copy some files 77 | const createNodeJSPath = path.join(__dirname, "../assets/standalone/create-node.js"); 78 | cpSyncModify(createNodeJSPath, outputDir + `/create-node.js`, ...findAndReplace); 79 | 80 | const templateJSPath = path.join(__dirname, "../assets/standalone/index-pwa.js"); 81 | cpSyncModify(templateJSPath, outputDir + `/index.js`, ...findAndReplace); 82 | 83 | const templateHTMLPath = path.join(__dirname, "../assets/standalone/index-pwa.html"); 84 | cpSyncModify(templateHTMLPath, outputDir + `/index.html`, ...findAndReplace); 85 | 86 | const templateWorkerPath = path.join(__dirname, "../assets/standalone/service-worker.js"); 87 | cpSyncModify(templateWorkerPath, outputDir + `/service-worker.js`, ...findAndReplace); 88 | 89 | const templateIconPath = path.join(__dirname, "../assets/standalone/icon.png"); 90 | cpSync(templateIconPath, outputDir + `/icon.png`); 91 | 92 | const faustwasmPath = path.join(__dirname, "../assets/standalone/faustwasm"); 93 | cpSync(faustwasmPath, outputDir + "/faustwasm"); 94 | 95 | const faustuiPath = path.join(__dirname, "../assets/standalone/faust-ui"); 96 | cpSync(faustuiPath, outputDir + "/faust-ui"); 97 | 98 | const templateManifestPath = path.join(__dirname, "../assets/standalone/manifest.json"); 99 | cpSyncModify(templateManifestPath, outputDir + `/manifest.json`, ...findAndReplace); 100 | }; 101 | /** 102 | * @param {string} outputDir - The output directory. 103 | * @param {string} dspName - The name of the DSP to be loaded. 104 | * @param {boolean} [poly] - Whether the DSP is polyphonic. 105 | * @param {boolean} [effect] - Whether the DSP has an effect module. 106 | */ 107 | const copyWebTemplateAssets = (outputDir, dspName, poly = false, effect = false) => { 108 | 109 | // Create output directory if it doesn't exist 110 | if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir); 111 | 112 | // Define find and replace patterns 113 | const findAndReplace = ["FAUST_DSP_NAME", dspName]; 114 | 115 | if (poly) findAndReplace.push("FAUST_DSP_VOICES = 0", "FAUST_DSP_VOICES = 16"); 116 | if (poly && effect) findAndReplace.push("FAUST_DSP_HAS_EFFECT = false", "FAUST_DSP_HAS_EFFECT = true"); 117 | 118 | // Copy some files 119 | const createNodeJSPath = path.join(__dirname, "../assets/standalone/create-node.js"); 120 | cpSyncModify(createNodeJSPath, outputDir + `/create-node.js`, ...findAndReplace); 121 | 122 | const templateJSPath = path.join(__dirname, "../assets/standalone/index-template.js"); 123 | cpSyncModify(templateJSPath, outputDir + `/index.js`, ...findAndReplace); 124 | 125 | const templateHTMLPath = path.join(__dirname, "../assets/standalone/index-template.html"); 126 | cpSyncModify(templateHTMLPath, outputDir + `/index.html`, "index-template.js", "index.js", ...findAndReplace); 127 | 128 | const faustwasmPath = path.join(__dirname, "../assets/standalone/faustwasm"); 129 | cpSync(faustwasmPath, outputDir + "/faustwasm"); 130 | }; 131 | 132 | export { copyWebStandaloneAssets, copyWebPWAAssets, copyWebTemplateAssets }; 133 | 134 | -------------------------------------------------------------------------------- /src/exports-bundle.ts: -------------------------------------------------------------------------------- 1 | export { default as instantiateFaustModule } from "./instantiateFaustModule"; 2 | 3 | export * from "./instantiateFaustModule"; 4 | 5 | export * from "./exports"; 6 | -------------------------------------------------------------------------------- /src/exports.ts: -------------------------------------------------------------------------------- 1 | export { default as instantiateFaustModuleFromFile } from "./instantiateFaustModuleFromFile"; 2 | export { default as getFaustAudioWorkletProcessor } from "./FaustAudioWorkletProcessor"; 3 | export { default as getFaustFFTAudioWorkletProcessor } from "./FaustFFTAudioWorkletProcessor"; 4 | export { default as FaustCompiler } from "./FaustCompiler"; 5 | export { FaustDspInstance } from "./FaustDspInstance"; 6 | export { default as FaustWasmInstantiator } from "./FaustWasmInstantiator"; 7 | export { default as FaustOfflineProcessor } from "./FaustOfflineProcessor"; 8 | export { default as FaustSvgDiagrams } from "./FaustSvgDiagrams"; 9 | export { default as FaustCmajor } from "./FaustCmajor"; 10 | export { default as LibFaust } from "./LibFaust"; 11 | export { default as WavEncoder } from "./WavEncoder"; 12 | export { default as WavDecoder } from "./WavDecoder"; 13 | export { default as SoundfileReader } from "./SoundfileReader"; 14 | 15 | export * from "./FaustAudioWorkletNode"; 16 | export * from "./FaustAudioWorkletProcessor"; 17 | export * from "./FaustFFTAudioWorkletProcessor"; 18 | export * from "./FaustCompiler"; 19 | export * from "./FaustDspInstance"; 20 | export * from "./FaustOfflineProcessor"; 21 | export * from "./FaustScriptProcessorNode"; 22 | export * from "./FaustWebAudioDsp"; 23 | export * from "./FaustDspGenerator"; 24 | export * from "./LibFaust"; 25 | 26 | export * from "./types"; 27 | -------------------------------------------------------------------------------- /src/faust2CmajorFiles.d.ts: -------------------------------------------------------------------------------- 1 | declare const faust2CmajorFiles: (inputFile: string, outputDir: string, argv?: string[]) => Promise; 2 | 3 | export default faust2CmajorFiles; 4 | -------------------------------------------------------------------------------- /src/faust2cmajorFiles.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import { 5 | instantiateFaustModuleFromFile, 6 | LibFaust, 7 | FaustCompiler, 8 | FaustCmajor 9 | } from "../dist/esm/index.js"; 10 | import { fileURLToPath } from "url"; 11 | 12 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 13 | const __filename = fileURLToPath(import.meta.url); 14 | 15 | /** 16 | * @param {string} inputFile 17 | * @param {string} outputDir 18 | * @param {string[]} [argv] 19 | */ 20 | const faust2CmajorFiles = async (inputFile, outputDir, argv = []) => { 21 | const faustModule = await instantiateFaustModuleFromFile(path.join(__dirname, "../libfaust-wasm/libfaust-wasm.js")); 22 | const libFaust = new LibFaust(faustModule); 23 | const compiler = new FaustCompiler(libFaust); 24 | console.log(`Faust Compiler version: ${compiler.version()}`); 25 | console.log(`Reading file ${inputFile}`); 26 | const code = fs.readFileSync(inputFile, { encoding: "utf8" }); 27 | const { name } = path.parse(inputFile); 28 | const cmajor = new FaustCmajor(compiler); 29 | if (!argv.find(a => a === "-I")) argv.push("-I", "libraries/"); 30 | const cmajor_file = cmajor.compile(name, code, argv.join(" ")); 31 | console.log(`Writing files to ${outputDir}`); 32 | if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir); 33 | const cmajorPath = path.join(outputDir, `${name}.cmajor`); 34 | fs.writeFileSync(cmajorPath, cmajor_file); 35 | return cmajor_file; 36 | }; 37 | 38 | export default faust2CmajorFiles; 39 | -------------------------------------------------------------------------------- /src/faust2svgFiles.d.ts: -------------------------------------------------------------------------------- 1 | declare const faust2svgFiles: (inputFile: string, outputDir: string, argv?: string[]) => Promise>; 2 | 3 | export default faust2svgFiles; 4 | -------------------------------------------------------------------------------- /src/faust2svgFiles.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import { 5 | instantiateFaustModuleFromFile, 6 | LibFaust, 7 | FaustCompiler, 8 | FaustSvgDiagrams 9 | } from "../dist/esm/index.js"; 10 | import { fileURLToPath } from "url"; 11 | 12 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 13 | const __filename = fileURLToPath(import.meta.url); 14 | 15 | /** 16 | * @param {string} inputFile 17 | * @param {string} outputDir 18 | * @param {string[]} [argv] 19 | */ 20 | const faust2svgFiles = async (inputFile, outputDir, argv = []) => { 21 | const faustModule = await instantiateFaustModuleFromFile(path.join(__dirname, "../libfaust-wasm/libfaust-wasm.js")); 22 | const libFaust = new LibFaust(faustModule); 23 | const compiler = new FaustCompiler(libFaust); 24 | console.log(`Faust Compiler version: ${compiler.version()}`); 25 | console.log(`Reading file ${inputFile}`); 26 | const code = fs.readFileSync(inputFile, { encoding: "utf8" }); 27 | const { name } = path.parse(inputFile); 28 | const diagram = new FaustSvgDiagrams(compiler); 29 | const svgs = diagram.from(name, code, argv.join(" ")); 30 | console.log(`Generated ${Object.keys(svgs).length} files.`); 31 | console.log(`Writing files to ${outputDir}`); 32 | if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir); 33 | for (const file in svgs) { 34 | const svgPath = path.join(outputDir, file); 35 | fs.writeFileSync(svgPath, svgs[file]); 36 | } 37 | return svgs; 38 | }; 39 | 40 | export default faust2svgFiles; 41 | -------------------------------------------------------------------------------- /src/faust2wasmFiles.d.ts: -------------------------------------------------------------------------------- 1 | import { FaustDspMeta } from "./types"; 2 | 3 | declare const faust2wasmFiles: (inputFile: string, outputDir: string, argv?: string[], poly?: boolean) => Promise<{ dspMeta: FaustDspMeta; effectMeta: FaustDspMeta | null }>; 4 | 5 | export default faust2wasmFiles; 6 | -------------------------------------------------------------------------------- /src/faust2wasmFiles.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import { 5 | instantiateFaustModuleFromFile, 6 | LibFaust, 7 | FaustCompiler, 8 | FaustMonoDspGenerator, 9 | FaustPolyDspGenerator 10 | } from "../dist/esm/index.js"; 11 | import { fileURLToPath } from "url"; 12 | 13 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 14 | const __filename = fileURLToPath(import.meta.url); 15 | 16 | /** 17 | * Script to compile a Faust DSP file to WebAssembly and write the output to files. 18 | * 19 | * @param {string} inputFile : The path to the Faust DSP file. 20 | * @param {string} outputDir : The path to the output directory. 21 | * @param {string[]} [argv] : An array of command-line arguments to pass to the Faust compiler. 22 | * @param {boolean} [poly] : Whether to compile the DSP as a polyphonic instrument. 23 | */ 24 | const faust2wasmFiles = async (inputFile, outputDir, argv = [], poly = false) => { 25 | const faustModule = await instantiateFaustModuleFromFile(path.join(__dirname, "../libfaust-wasm/libfaust-wasm.js")); 26 | const libFaust = new LibFaust(faustModule); 27 | const compiler = new FaustCompiler(libFaust); 28 | console.log(`Faust Compiler version: ${compiler.version()}`); 29 | console.log(`Reading file ${inputFile}`); 30 | const code = fs.readFileSync(inputFile, { encoding: "utf8" }); 31 | 32 | const fileName = /** @type {string} */(inputFile.split('/').pop()); 33 | const dspName = fileName.replace(/\.dsp$/, ''); 34 | // Flush to zero to avoid costly denormalized numbers 35 | argv.push("-ftz", "2"); 36 | const dspModulePath = path.join(outputDir, `dsp-module.wasm`); 37 | const dspMetaPath = path.join(outputDir, `dsp-meta.json`); 38 | const effectModulePath = path.join(outputDir, `effect-module.wasm`); 39 | const effectMetaPath = path.join(outputDir, `effect-meta.json`); 40 | const mixerModulePath = path.join(outputDir, "mixer-module.wasm"); 41 | /** @type {Uint8Array} */ 42 | let dspModule; 43 | /** @type {import("./types").FaustDspMeta} */ 44 | let dspMeta; 45 | /** @type {Uint8Array | null} */ 46 | let effectModule = null; 47 | /** @type {import("./types").FaustDspMeta | null} */ 48 | let effectMeta = null; 49 | /** @type {Uint8Array | null} */ 50 | let mixerModule = null; 51 | const { name } = path.parse(inputFile); 52 | if (poly) { 53 | const generator = new FaustPolyDspGenerator(); 54 | const t1 = Date.now(); 55 | const dsp = await generator.compile(compiler, name, code, argv.join(" ")); 56 | if (!dsp) throw new Error("Faust DSP not compiled"); 57 | const { voiceFactory, effectFactory, mixerBuffer } = dsp; 58 | if (!voiceFactory) throw new Error("Faust DSP Factory not compiled"); 59 | console.log(`Compilation successful (${Date.now() - t1} ms).`); 60 | dspModule = voiceFactory.code; 61 | dspMeta = JSON.parse(voiceFactory.json); 62 | mixerModule = mixerBuffer; 63 | if (effectFactory) { 64 | effectModule = effectFactory.code; 65 | effectMeta = JSON.parse(effectFactory.json); 66 | } 67 | } else { 68 | const generator = new FaustMonoDspGenerator(); 69 | const t1 = Date.now(); 70 | const dsp = await generator.compile(compiler, name, code, argv.join(" ")); 71 | if (!dsp) throw new Error("Faust DSP not compiled"); 72 | const { factory } = dsp; 73 | if (!factory) throw new Error("Faust DSP Factory not compiled"); 74 | console.log(`Compilation successful (${Date.now() - t1} ms).`); 75 | dspModule = factory.code; 76 | dspMeta = JSON.parse(factory.json); 77 | } 78 | const files = [dspModulePath, dspMetaPath]; 79 | if (mixerModule) files.push(mixerModulePath); 80 | if (effectModule) files.push(effectModulePath, effectMetaPath); 81 | 82 | console.log(`Writing files to ${outputDir}`); 83 | if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir); 84 | for (const filePath in files) { 85 | if (fs.existsSync(filePath)) fs.unlinkSync(filePath); 86 | } 87 | fs.writeFileSync(dspModulePath, dspModule); 88 | fs.writeFileSync(dspMetaPath, JSON.stringify(dspMeta, null, 4)); 89 | if (effectModule && effectMeta) { 90 | fs.writeFileSync(effectModulePath, effectModule); 91 | fs.writeFileSync(effectMetaPath, JSON.stringify(effectMeta, null, 4)); 92 | } 93 | if (mixerModule) fs.writeFileSync(mixerModulePath, mixerModule); 94 | return { dspMeta, effectMeta }; 95 | }; 96 | 97 | export default faust2wasmFiles; 98 | -------------------------------------------------------------------------------- /src/faust2wavFiles.d.ts: -------------------------------------------------------------------------------- 1 | declare const faust2wavFiles: (inputFile: string, inputWav: string, outputWav: string, bufferSize?: number, sampleRate?: number, samples?: number, bitDepth?: number, argv?: string[]) => Promise; 2 | 3 | export default faust2wavFiles; 4 | -------------------------------------------------------------------------------- /src/faust2wavFiles.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import * as process from "process"; 5 | import { 6 | instantiateFaustModuleFromFile, 7 | LibFaust, 8 | FaustCompiler, 9 | FaustMonoDspGenerator, 10 | WavDecoder, 11 | WavEncoder 12 | } from "../dist/esm/index.js"; 13 | import { fileURLToPath } from "url"; 14 | 15 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 16 | const __filename = fileURLToPath(import.meta.url); 17 | 18 | /** 19 | * @param {string} inputFile 20 | * @param {string} inputWav 21 | * @param {string} outputWav 22 | * @param {number} bufferSize 23 | * @param {number} sampleRate 24 | * @param {number} samples 25 | * @param {number} bitDepth 26 | * @param {string[]} [argv] 27 | */ 28 | const faust2wavFiles = async (inputFile, inputWav, outputWav, bufferSize = 64, sampleRate = 44100, samples = 5 * sampleRate, bitDepth = 16, argv = []) => { 29 | const faustModule = await instantiateFaustModuleFromFile(path.join(__dirname, "../libfaust-wasm/libfaust-wasm.js")); 30 | const libFaust = new LibFaust(faustModule); 31 | const compiler = new FaustCompiler(libFaust); 32 | console.log(`Faust Compiler version: ${compiler.version()}`); 33 | console.log(`Reading file ${inputFile}`); 34 | const code = fs.readFileSync(inputFile, { encoding: "utf8" }); 35 | const { name } = path.parse(inputFile); 36 | const gen = new FaustMonoDspGenerator(); 37 | await gen.compile(compiler, name, code, argv.join(" ")); 38 | const processor = await gen.createOfflineProcessor(sampleRate, bufferSize); 39 | if (!processor) throw Error("Processor not generated"); 40 | /** @type {Float32Array[] | undefined} */ 41 | let input = undefined; 42 | if (inputWav) { 43 | console.log(`Reading input wav file ${inputWav}.`); 44 | const inputBuffer = fs.readFileSync(inputWav).buffer; 45 | console.log(`Decoding...`); 46 | input = WavDecoder.decode(inputBuffer).channelData; 47 | } 48 | console.log(`Processing...`); 49 | const output = processor.render(input, samples, sample => process.stdout.write(`\r${sample} / ${samples}`)); 50 | console.log(""); 51 | console.log(`Encoding...`); 52 | const outputBuffer = WavEncoder.encode(output, { bitDepth, sampleRate }); 53 | console.log(`Writing output wav file ${outputWav}.`); 54 | fs.writeFileSync(outputWav, new Uint8Array(outputBuffer)); 55 | }; 56 | 57 | export default faust2wavFiles; 58 | -------------------------------------------------------------------------------- /src/index-bundle-iife.ts: -------------------------------------------------------------------------------- 1 | import * as faustwasm from "./exports-bundle"; 2 | // export default faustwasm; 3 | // Bug with dts-bundle-generator 4 | 5 | export * from "./exports-bundle"; 6 | 7 | (globalThis as any).faustwasm = faustwasm; 8 | -------------------------------------------------------------------------------- /src/index-bundle.ts: -------------------------------------------------------------------------------- 1 | // import * as faustwasm from "./exports-bundle"; 2 | // export default faustwasm; 3 | // Bug with dts-bundle-generator 4 | 5 | export * from "./exports-bundle"; 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // import * as faustwasm from "./exports"; 2 | // export default faustwasm; 3 | // Bug with dts-bundle-generator 4 | 5 | export * from "./exports"; 6 | -------------------------------------------------------------------------------- /src/instantiateFaustModule.ts: -------------------------------------------------------------------------------- 1 | import factoryFn from "../libfaust-wasm/libfaust-wasm.cjs"; 2 | import wasmBinary from "../libfaust-wasm/libfaust-wasm.wasm"; 3 | import dataBinary from "../libfaust-wasm/libfaust-wasm.data"; 4 | 5 | export const FaustModuleFactoryFn = factoryFn; 6 | export const FaustModuleFactoryWasm = wasmBinary; 7 | export const FaustModuleFactoryData = dataBinary; 8 | 9 | /** 10 | * Instantiate `FaustModule` using bundled binaries. Module constructor and files can be overriden. 11 | */ 12 | const instantiateFaustModule = async (FaustModuleFactoryIn = factoryFn, dataBinaryIn = dataBinary, wasmBinaryIn = wasmBinary) => { 13 | const g = globalThis as any; 14 | if (g.AudioWorkletGlobalScope) { 15 | g.importScripts = () => {}; 16 | g.self = { location: { href: "" } }; 17 | } 18 | const faustModule = await FaustModuleFactoryIn({ 19 | wasmBinary: wasmBinaryIn, 20 | getPreloadedPackage: (remotePackageName: string, remotePackageSize: number) => { 21 | if (remotePackageName === "libfaust-wasm.data") return dataBinaryIn.buffer; 22 | return new ArrayBuffer(0); 23 | } 24 | }); 25 | if (g.AudioWorkletGlobalScope) { 26 | delete g.importScripts; 27 | delete g.self; 28 | } 29 | return faustModule; 30 | }; 31 | 32 | export default instantiateFaustModule; 33 | -------------------------------------------------------------------------------- /src/instantiateFaustModuleFromFile.ts: -------------------------------------------------------------------------------- 1 | import type { FaustModuleFactory } from "./types"; 2 | 3 | /** 4 | * Load libfaust-wasm files, than instantiate libFaust 5 | * @param jsFile path to `libfaust-wasm.js` 6 | * @param dataFile path to `libfaust-wasm.data` 7 | * @param wasmFile path to `libfaust-wasm.wasm` 8 | */ 9 | const instantiateFaustModuleFromFile = async (jsFile: string, dataFile = jsFile.replace(/c?js$/, "data"), wasmFile = jsFile.replace(/c?js$/, "wasm")) => { 10 | let FaustModule: FaustModuleFactory; 11 | let dataBinary: ArrayBuffer; 12 | let wasmBinary: Uint8Array | ArrayBuffer; 13 | const jsCodeHead = /var (.+) = \(/; 14 | if (typeof window === "object") { 15 | let jsCode = await (await fetch(jsFile)).text(); 16 | jsCode = `${jsCode} 17 | export default ${jsCode.match(jsCodeHead)?.[1]}; 18 | `; 19 | const jsFileMod = URL.createObjectURL(new Blob([jsCode], { type: "text/javascript" })); 20 | FaustModule = (await import(/* webpackIgnore: true */jsFileMod)).default; 21 | dataBinary = await (await fetch(dataFile)).arrayBuffer(); 22 | wasmBinary = new Uint8Array(await (await fetch(wasmFile)).arrayBuffer()); 23 | } else { 24 | const { promises: fs } = await import("fs"); 25 | const { pathToFileURL } = await import("url"); 26 | let jsCode = (await fs.readFile(jsFile, { encoding: "utf-8" })); 27 | jsCode = ` 28 | import process from "process"; 29 | import * as path from "path"; 30 | import { createRequire } from "module"; 31 | import { fileURLToPath } from "url"; 32 | 33 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 34 | const __filename = fileURLToPath(import.meta.url); 35 | const require = createRequire(import.meta.url); 36 | 37 | ${jsCode} 38 | 39 | export default ${jsCode.match(jsCodeHead)?.[1]}; 40 | `; 41 | const jsFileMod = jsFile.replace(/c?js$/, "mjs"); 42 | await fs.writeFile(jsFileMod, jsCode); 43 | FaustModule = (await import(/* webpackIgnore: true */pathToFileURL(jsFileMod).href)).default; 44 | await fs.unlink(jsFileMod); 45 | dataBinary = (await fs.readFile(dataFile)).buffer; 46 | wasmBinary = (await fs.readFile(wasmFile)).buffer; 47 | } 48 | const faustModule = await FaustModule({ 49 | wasmBinary, 50 | getPreloadedPackage: (remotePackageName: string, remotePackageSize: number) => { 51 | if (remotePackageName === "libfaust-wasm.data") return dataBinary; 52 | return new ArrayBuffer(0); 53 | }}); 54 | return faustModule; 55 | }; 56 | 57 | export default instantiateFaustModuleFromFile; 58 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type FaustModuleFactory = EmscriptenModuleFactory; 2 | 3 | export interface FaustModule extends EmscriptenModule { 4 | ccall: typeof ccall; 5 | cwrap: typeof cwrap; 6 | UTF8ArrayToString(u8Array: number[], ptr: number, maxBytesToRead?: number): string; 7 | stringToUTF8Array(str: string, outU8Array: number[], outIdx: number, maxBytesToWrite: number): number; 8 | UTF8ToString: typeof UTF8ToString; 9 | UTF16ToString: typeof UTF16ToString; 10 | UTF32ToString: typeof UTF32ToString; 11 | stringToUTF8: typeof stringToUTF8; 12 | stringToUTF16: typeof stringToUTF16; 13 | stringToUTF32: typeof stringToUTF32; 14 | allocateUTF8: typeof allocateUTF8; 15 | lengthBytesUTF8: typeof lengthBytesUTF8; 16 | lengthBytesUTF16: typeof lengthBytesUTF16; 17 | lengthBytesUTF32: typeof lengthBytesUTF32; 18 | FS: typeof FS; 19 | libFaustWasm: new () => LibFaustWasm; 20 | } 21 | 22 | export type FaustInfoType = "help" | "version" | "libdir" | "includedir" | "archdir" | "dspdir" | "pathslist"; 23 | 24 | export interface IntVector { 25 | size(): number; 26 | get(i: number): number; 27 | delete(): void; 28 | } 29 | 30 | export interface FaustDspWasm { 31 | /* The C++ factory pointer as in integer */ 32 | cfactory: number; 33 | /* The compiled wasm binary code */ 34 | data: IntVector; 35 | /* The DSP JSON description */ 36 | json: string; 37 | } 38 | 39 | export interface LibFaustWasm { 40 | /** 41 | * Return the Faust compiler version. 42 | * 43 | * @returns the version 44 | */ 45 | version(): string; 46 | 47 | /** 48 | * Create a dsp factory from Faust code. 49 | * 50 | * @param name - an arbitrary name for the Faust module 51 | * @param code - Faust dsp code 52 | * @param args - the compiler options 53 | * @param useInternalMemory - tell the compiler to generate static embedded memory or not 54 | * @returns an opaque reference to the factory 55 | */ 56 | createDSPFactory(name: string, code: string, args: string, useInternalMemory: boolean): FaustDspWasm; 57 | 58 | /** 59 | * Delete a dsp factory. 60 | * 61 | * @param cFactory - the factory C++ internal pointer as a number 62 | */ 63 | deleteDSPFactory(cFactory: number): void; 64 | 65 | /** 66 | * Expand Faust code i.e. linearize included libraries. 67 | * 68 | * @param name - an arbitrary name for the Faust module 69 | * @param code - Faust dsp code 70 | * @param args - the compiler options 71 | * @returns return the expanded dsp code 72 | */ 73 | expandDSP(name: string, code: string, args: string): string; 74 | 75 | /** 76 | * Generates auxiliary files from Faust code. The output depends on the compiler options. 77 | * 78 | * @param name - an arbitrary name for the faust module 79 | * @param code - Faust dsp code 80 | * @param args - the compiler options 81 | */ 82 | generateAuxFiles(name: string, code: string, args: string): boolean; 83 | 84 | /** 85 | * Delete all existing dsp factories. 86 | */ 87 | deleteAllDSPFactories(): void; 88 | 89 | /** 90 | * Exception management: gives an error string 91 | */ 92 | getErrorAfterException(): string; 93 | 94 | /** 95 | * Exception management: cleanup 96 | * Should be called after each exception generated by the LibFaust methods. 97 | */ 98 | cleanupAfterException(): void; 99 | 100 | /** 101 | * Get info about the embedded Faust engine 102 | * @param what - the requested info 103 | */ 104 | getInfos(what: FaustInfoType): string; 105 | } 106 | 107 | export interface FaustDspFactory extends Required { } 108 | 109 | /** 110 | * The Factory structure. 111 | */ 112 | export interface LooseFaustDspFactory { 113 | /** a "pointer" (as an integer) on the internal C++ factory */ 114 | cfactory?: number; 115 | /** the WASM code as a binary array */ 116 | code?: Uint8Array; 117 | /** the compule WASM module */ 118 | module: WebAssembly.Module; 119 | /** the compiled DSP JSON description */ 120 | json: string; 121 | /** whether the factory is a polyphonic one or not */ 122 | poly?: boolean; 123 | /** a unique identifier */ 124 | shaKey?: string; 125 | /** a map of transferable audio buffers for the `soundfile` function */ 126 | soundfiles: Record; 127 | } 128 | 129 | export interface FaustDspMeta { 130 | name: string; 131 | filename: string; 132 | compile_options: string; 133 | include_pathnames: string[]; 134 | inputs: number; 135 | outputs: number; 136 | size: number; 137 | version: string; 138 | library_list: string[]; 139 | meta: { [key: string]: string }[]; 140 | ui: FaustUIDescriptor; 141 | } 142 | 143 | export type FaustUIDescriptor = FaustUIGroup[]; 144 | export type FaustUIItem = FaustUIInputItem | FaustUIOutputItem | FaustUIGroup; 145 | export interface FaustUIInputItem { 146 | type: FaustUIInputType; 147 | label: string; 148 | address: string; 149 | url: string; 150 | index: number; 151 | init?: number; 152 | min?: number; 153 | max?: number; 154 | step?: number; 155 | meta?: FaustUIMeta[]; 156 | } 157 | export interface FaustUIOutputItem { 158 | type: FaustUIOutputType; 159 | label: string; 160 | address: string; 161 | index: number; 162 | min?: number; 163 | max?: number; 164 | meta?: FaustUIMeta[]; 165 | } 166 | export interface FaustUIMeta { 167 | [order: number]: string; 168 | style?: string; // "knob" | "menu{'Name0':value0;'Name1':value1}" | "radio{'Name0':value0;'Name1':value1}" | "led"; 169 | unit?: string; 170 | scale?: "linear" | "exp" | "log"; 171 | tooltip?: string; 172 | hidden?: string; 173 | [key: string]: string | undefined; 174 | } 175 | export type FaustUIGroupType = "vgroup" | "hgroup" | "tgroup"; 176 | export type FaustUIOutputType = "hbargraph" | "vbargraph"; 177 | export type FaustUIInputType = "vslider" | "hslider" | "button" | "checkbox" | "nentry" | "soundfile"; 178 | export interface FaustUIGroup { 179 | type: FaustUIGroupType; 180 | label: string; 181 | items: FaustUIItem[]; 182 | } 183 | export type FaustUIType = FaustUIGroupType | FaustUIOutputType | FaustUIInputType; 184 | 185 | export interface AudioParamDescriptor { 186 | automationRate?: AutomationRate; 187 | defaultValue?: number; 188 | maxValue?: number; 189 | minValue?: number; 190 | name: string; 191 | } 192 | 193 | export interface AudioWorkletProcessor { 194 | port: MessagePort; 195 | process(inputs: Float32Array[][], outputs: Float32Array[][], parameters: Record): boolean; 196 | } 197 | export declare const AudioWorkletProcessor: { 198 | prototype: AudioWorkletProcessor; 199 | parameterDescriptors: AudioParamDescriptor[]; 200 | new(options: AudioWorkletNodeOptions): AudioWorkletProcessor; 201 | }; 202 | 203 | export interface AudioWorkletGlobalScope { 204 | AudioWorkletGlobalScope: any; 205 | globalThis: AudioWorkletGlobalScope; 206 | registerProcessor: (name: string, constructor: new (options: any) => AudioWorkletProcessor) => void; 207 | currentFrame: number; 208 | currentTime: number; 209 | sampleRate: number; 210 | AudioWorkletProcessor: typeof AudioWorkletProcessor; 211 | } 212 | 213 | export interface InterfaceFFT { 214 | forward(arr: ArrayLike | ((arr: Float32Array) => any)): Float32Array; 215 | inverse(arr: ArrayLike | ((arr: Float32Array) => any)): Float32Array; 216 | dispose(): void; 217 | } 218 | export declare const InterfaceFFT: { 219 | new(size: number): InterfaceFFT; 220 | } 221 | 222 | export type TWindowFunction = (index: number, length: number, ...args: any[]) => number; 223 | export type Writeable = { -readonly [P in keyof T]: T[P] }; 224 | export type TypedArray = Int8Array | Uint8Array | Int16Array | Uint16Array | Int32Array | Uint32Array | Uint8ClampedArray | Float32Array | Float64Array; 225 | export type TypedArrayConstructor = typeof Int8Array | typeof Uint8Array | typeof Int16Array | typeof Uint16Array | typeof Int32Array | typeof Uint32Array | typeof Uint8ClampedArray | typeof Float32Array | typeof Float64Array; 226 | 227 | export declare const FFTUtils: { 228 | /** Inject window functions as array, no need to add rectangular (no windowing) */ 229 | windowFunctions?: TWindowFunction[]; 230 | /** Get a FFT interface constructor */ 231 | getFFT: () => Promise; 232 | /** Convert from FFTed (spectral) signal to three arrays for Faust processor's input, fft is readonly, real/imag/index length = *fftSize* / 2 + 1; fft length depends on the FFT implementation */ 233 | fftToSignal: (fft: Float32Array | Float64Array, real: Float32Array | Float64Array, imag?: Float32Array | Float64Array, index?: Float32Array | Float64Array) => any; 234 | /** Convert from Faust processor's output to spectral data for Inversed FFT, real/imag are readonly, real/imag length = *fftSize* / 2 + 1; fft length depends on the FFT implementation */ 235 | signalToFFT: (real: Float32Array | Float64Array, imag: Float32Array | Float64Array, fft: Float32Array | Float64Array) => any; 236 | /** Convert from Faust processor's output to direct audio output, real/imag are readonly, fft length = fftSize = (real/imag length - 1) * 2 */ 237 | signalToNoFFT: (real: Float32Array | Float64Array, imag: Float32Array | Float64Array, fft: Float32Array | Float64Array) => any; 238 | } 239 | 240 | export interface AudioData { 241 | sampleRate: number; 242 | audioBuffer: Float32Array[]; 243 | } 244 | -------------------------------------------------------------------------------- /test/clarinet.dsp: -------------------------------------------------------------------------------- 1 | import("stdfaust.lib"); 2 | process = pm.clarinet_ui_MIDI <: _,_; -------------------------------------------------------------------------------- /test/djembe.dsp: -------------------------------------------------------------------------------- 1 | import("stdfaust.lib"); 2 | process = ba.pulsen(1, 10000) : pm.djembe(60, 0.3, 0.4, 1); 3 | -------------------------------------------------------------------------------- /test/faustlive-wasm/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: 'Roboto', sans-serif; 3 | font-size: 14px; 4 | font-weight: normal; 5 | } 6 | 7 | body { 8 | margin-left: 10%; 9 | margin-right: 10%; 10 | background-color: #FFFFFF, 11 | } 12 | 13 | h1 { 14 | font-family: 'Open Sans', sans-serif; 15 | font-size: 30px; 16 | font-weight: lighter; 17 | } 18 | 19 | h2 { 20 | font-family: 'Open Sans', sans-serif; 21 | font-size: 24px; 22 | } 23 | 24 | h3 { 25 | font-family: 'Open Sans', sans-serif; 26 | font-size: 16px; 27 | font-weight: bold; 28 | } 29 | 30 | h4 { 31 | font-family: 'Open Sans', sans-serif; 32 | font-size: 14px; 33 | font-weight: bold; 34 | } 35 | 36 | .config { 37 | display: grid; 38 | grid-template-columns: 1fr 1fr 1fr 1fr; 39 | grid-gap: 8px; 40 | } 41 | 42 | .config-element { 43 | border: none; 44 | background-color: #9FA8EB; 45 | padding: 10px; 46 | } 47 | 48 | .config-element p { 49 | font-style: italic; 50 | } 51 | 52 | .config-element ul { 53 | font-style: italic; 54 | list-style-type: none; 55 | } 56 | 57 | .config-element li { 58 | font-style: italic; 59 | list-style-type: none; 60 | } 61 | 62 | .line2 { 63 | display: grid; 64 | grid-template-columns: 1fr 1fr; 65 | } 66 | 67 | /* WEBKIT */ 68 | text { 69 | -webkit-touch-callout: none; 70 | -webkit-user-select: none; 71 | -khtml-user-select: none; 72 | -moz-user-select: none; 73 | -ms-user-select: none; 74 | user-select: none; 75 | } 76 | 77 | #filedrag { 78 | font-weight: bold; 79 | text-align: center; 80 | padding: 1em 0; 81 | margin: 1em 0; 82 | color: #8d97e8; 83 | border: 4px dashed #8d97e8; 84 | cursor: default; 85 | font-size: 20px; 86 | } 87 | 88 | #filedrag.hover { 89 | color: #FF7F00; 90 | border-color: #FF7F00; 91 | border-style: solid; 92 | } 93 | 94 | td { 95 | text-align: center; 96 | padding: 40px; 97 | } -------------------------------------------------------------------------------- /test/faustlive-wasm/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | FaustLive WASM 8 | 9 | 10 | 11 | 12 |
13 |

Testing the embedded dynamic Faust compiler

14 |
15 | 16 |

This page embeds the Faust compiler as a JavaScript library named libfaust.js (associated with the auxiliary 17 | WebAssembly library), including its WebAssembly generating backend, and compiled using Emscripten. 19 | 20 |

You can compile Faust .dsp files or URL by just dropping them on the drop zone. A WebAudio node will be created 21 | and connected to audio inputs/outputs and controlled with the displayed HTML/SVG based GUI. 22 |

Settings (buffer size, polyphonic mode and voices, audio rendering model, sample size and ftz mode) can be 23 | changed dynamically 24 | and the DSP will be recompiled on the fly. 25 |

Clicking on the yellow cross on the top right allows to access to the GRAME online compiler, to generate 26 | different targets (like for instance standalone applications, VST, Max/MSP plugins, iOS or JUCE ready 27 | projects...). 28 |

29 | 30 |
31 |

Buffer size

32 |
33 | 42 |
43 |

You can change the buffer size (from 256 to 8192 frames in ScripProcessor mode, it will be fixed at 128 44 | frames in AudioWorklet mode).

45 |
46 | 47 |
48 |

Polyphonic instrument

49 |
50 |
51 | 55 |
56 |
57 | 67 |
68 |
69 |

Assuming your DSP code is polyphonic ready, you 71 | can activate the polyphonic mode, adjust the number of available voices, and test it with a MIDI device 72 | or application (usable with Chrome which implements the Web MIDI API). 74 |

75 | 76 |
77 |

ScriptProcessor/AudioWorklet

78 |
79 | 84 |
85 |

ScriptProcessor: audio rendering is done using the old ScriptProcessor model.

86 |

AudioWorklet: audio rendering is done using the new AudioWorklet model. 87 |

88 |

89 | 90 |
91 |

Sample format

92 |
93 | 98 |
99 |

Float denormals handling

100 |
101 | 106 |
107 |

0: means no denormal handling.

108 |

1: uses fabs and a threshold to detect denormal values (slower). 109 |

110 |

2: uses a bitmask to detect denormal values (faster).

111 |
112 | 113 |
114 | 115 |

Save page and DSP control 116 | parameters state

117 | 118 |
119 | Loading JavaScript/WebAssembly ressources... 120 |
121 | 122 |
123 | 124 |
125 | 126 |
127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /test/guitar.dsp: -------------------------------------------------------------------------------- 1 | import("stdfaust.lib"); 2 | process = pm.guitar_ui_MIDI <: _,_; -------------------------------------------------------------------------------- /test/guitar1.dsp: -------------------------------------------------------------------------------- 1 | import("stdfaust.lib"); 2 | process = pm.guitar_ui_MIDI <: _,_; 3 | effect = *(hslider("Left", 0.1, 0, 1, 0.01)), *(hslider("Right", 0.1, 0, 1, 0.01)); -------------------------------------------------------------------------------- /test/libfaust-in-worklet/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Faust Wasm Generator Demo 6 | 7 | 8 | 9 |

...

10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/libfaust-in-worklet/index.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | const div = document.getElementById("log"); 3 | const log = (/** @type {string} */str) => div.innerHTML += str.replace("\n", "
") + "
"; 4 | 5 | (async () => { 6 | const ctx = new AudioContext(); 7 | globalThis.ctx = ctx; 8 | globalThis.faustModule = await faustwasm.instantiateFaustModule(); 9 | const post = async () => { 10 | const options = "-ftz 2"; 11 | const { 12 | instantiateFaustModule, 13 | LibFaust, 14 | getFaustAudioWorkletProcessor, 15 | WavEncoder, 16 | FaustWasmInstantiator, 17 | FaustMonoDspGenerator, 18 | FaustPolyDspGenerator, 19 | FaustMonoWebAudioDsp, 20 | FaustOfflineProcessor, 21 | FaustCompiler, 22 | FaustSvgDiagrams 23 | } = faustwasm; 24 | const faustModule = await instantiateFaustModule(); 25 | const code = `import("stdfaust.lib"); 26 | process = os.osc(440);`; 27 | const gen = new FaustMonoDspGenerator(); 28 | const libFaust = new LibFaust(faustModule); 29 | const compiler = new FaustCompiler(libFaust); 30 | console.log(compiler.version()); 31 | await gen.compile(compiler, 'dsp', code, options); 32 | 33 | console.log(await gen.createAudioWorkletProcessor()); 34 | const processor = await gen.createOfflineProcessor(48000, 128); 35 | console.log(processor); 36 | console.log(processor.render([], 48000)); 37 | }; 38 | await ctx.audioWorklet.addModule("../../dist/cjs-bundle/index.js"); 39 | await ctx.audioWorklet.addModule(URL.createObjectURL(new Blob([`(${post.toString()})()`], { type: "application/javascript" }))); 40 | })(); 41 | -------------------------------------------------------------------------------- /test/midi.dsp: -------------------------------------------------------------------------------- 1 | 2 | import("stdfaust.lib"); 3 | 4 | /* 5 | process = os.osc(freq)*vol 6 | with { 7 | freq = hslider("Freq[midi:ctrl 1]", 200, 200, 2000, 0.01); 8 | vol = hslider("Volume[midi:ctrl 7 5]", 0.5, 0, 1, 0.01); 9 | }; 10 | */ 11 | 12 | process = (os.osc(500)*en.adsr(0.1, 0.1, 0.8, 0.5, button("gate1[midi:key 60]")), 13 | os.osc(800)*en.adsr(0.1, 0.1, 0.8, 1.0, button("gate2[midi:key 62]")), 14 | os.osc(200)*en.adsr(0.1, 0.1, 0.8, 1.5, button("gate3[midi:key 64]"))) :> _; 15 | -------------------------------------------------------------------------------- /test/mono.dsp: -------------------------------------------------------------------------------- 1 | declare name "Oscillator440"; 2 | declare version "1.0"; 3 | declare author "Fr0stbyteR"; 4 | declare license "BSD"; 5 | declare copyright "shren2021"; 6 | declare description "This is an oscillator"; 7 | import("stdfaust.lib"); 8 | 9 | process = os.osc(440); 10 | -------------------------------------------------------------------------------- /test/node/test.js: -------------------------------------------------------------------------------- 1 | import * as FaustWasm from "../../dist/esm/index.js"; 2 | import * as path from "path"; 3 | import * as fs from "fs"; 4 | import { fileURLToPath, pathToFileURL } from "url"; 5 | 6 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 7 | const __filename = fileURLToPath(import.meta.url); 8 | 9 | const wasmBuf = fs.readFileSync(path.join(__dirname, "../../libfaust-wasm/libfaust-wasm.wasm")).buffer; 10 | 11 | (async () => { 12 | // const w = await WebAssembly.instantiate(wasmBuf, {env:{},wasi_snapshot_preview1:{}}); 13 | // console.log(w); 14 | const faustModule = await FaustWasm.instantiateFaustModuleFromFile(path.join(__dirname, "../../libfaust-wasm/libfaust-wasm.js")); 15 | console.log(faustModule); 16 | })(); 17 | -------------------------------------------------------------------------------- /test/noise.dsp: -------------------------------------------------------------------------------- 1 | random = +(12345)~*(1103515245); 2 | noise = random/2147483647.0; 3 | process = noise * vslider("Volume", 0.2, 0, 1, 0.1) <: (*(0.01),*(0.01)); -------------------------------------------------------------------------------- /test/organ.dsp: -------------------------------------------------------------------------------- 1 | import("stdfaust.lib"); 2 | 3 | timbre(f) = os.osc(f)*0.5 + os.osc(f*2)*0.25 + os.osc(f*3)*0.125; 4 | 5 | process = timbre(hslider("freq [midi:ctrl 7]", 440, 20, 3000, 1)) 6 | * hslider("gain", 0.5, 0, 1, 0.01) 7 | * (button("gate") : en.adsr(0.1,0.1,0.98,1.5)); 8 | -------------------------------------------------------------------------------- /test/organ1.dsp: -------------------------------------------------------------------------------- 1 | import("stdfaust.lib"); 2 | 3 | timbre(f) = os.osc(f)*0.5 + os.osc(f*2)*0.25 + os.osc(f*3)*0.125; 4 | effect = _*(hslider("Left", 0.1, 0, 1, 0.01)), _*(hslider("Right", 0.1, 0, 1, 0.01)); 5 | 6 | process = timbre(hslider("freq", 440, 20, 3000, 1)) 7 | * hslider("gain", 0.5, 0, 1, 0.01) 8 | * (button("gate") : en.adsr(0.1,0.1,0.98,1.5)); 9 | -------------------------------------------------------------------------------- /test/osc.dsp: -------------------------------------------------------------------------------- 1 | import("stdfaust.lib"); 2 | 3 | vol = hslider("volume [unit:dB][acc:1 1 -10 0 10]", 0, -96, 0, 0.1) : ba.db2linear : si.smoo; 4 | freq = hslider("freq [unit:Hz][acc:0 1 -10 0 10]", 500, 20, 2500, 1) : si.smoo; 5 | 6 | process = vgroup("Oscillator", os.osc(freq) * vol); -------------------------------------------------------------------------------- /test/osc2.dsp: -------------------------------------------------------------------------------- 1 | import("stdfaust.lib"); 2 | f = hslider("freq [midi:ctrl 7][acc:0 1 -10 0 10]",440,50,2000,0.01) : si.smoo; 3 | phasor(freq) = (+(freq/ma.SR) ~ ma.decimal); 4 | osc(freq) = sin(phasor(freq)*2*ma.PI); 5 | process = osc(f); -------------------------------------------------------------------------------- /test/osc3.dsp: -------------------------------------------------------------------------------- 1 | import("stdfaust.lib"); 2 | f = hslider("freq",440,50,2000,0.01); 3 | phasor(freq) = (+(freq/ma.SR) ~ ma.decimal); 4 | process = phasor(f); -------------------------------------------------------------------------------- /test/poly.dsp: -------------------------------------------------------------------------------- 1 | declare name "FluteMIDI"; 2 | declare version "1.0"; 3 | declare author "Romain Michon"; 4 | declare description "Simple MIDI-controllable flute physical model with physical parameters."; 5 | declare license "MIT"; 6 | declare copyright "(c)Romain Michon, CCRMA (Stanford University), GRAME"; 7 | declare isInstrument "true"; 8 | 9 | import("stdfaust.lib"); 10 | 11 | process = pm.clarinet_ui_MIDI <: _,_; 12 | 13 | effect = dm.freeverb_demo; 14 | -------------------------------------------------------------------------------- /test/rev.dsp: -------------------------------------------------------------------------------- 1 | import("stdfaust.lib"); 2 | process = _ <: dm.freeverb_demo; 3 | -------------------------------------------------------------------------------- /test/soundfile.dsp: -------------------------------------------------------------------------------- 1 | declare name "DroneLAN"; 2 | declare author "Developpement Grame - CNCM par Elodie Rabibisoa et Romain Constant."; 3 | declare soundfiles "https://raw.githubusercontent.com/grame-cncm/GameLAN/master/drone/"; 4 | 5 | import ("stdfaust.lib"); 6 | 7 | // 2 drones : 8 | process = par(i, 1, (multi(i) :> _)); 9 | 10 | // 4 sounds per drone : 11 | multi(N) = par(i, 2, so.loop(drone(N), i) *(0.25) * volume(i)); 12 | drone(0) = soundfile("Drone_1 [url:{'Alonepad_reverb_stereo_instru1.flac'; 'Dronepad_test_stereo_instru1.flac'}]", 1); 13 | 14 | volume(0) = hslider("Volume 0 [acc:0 0 0 0 10][hidden:1]", 1, 0, 1, 0.001) : fi.lowpass(1, 1); 15 | volume(1) = hslider("Volume 1 [acc:0 1 -10 0 0][hidden:1]", 0, 0, 1, 0.001) : fi.lowpass(1, 1); -------------------------------------------------------------------------------- /test/soundfile1.dsp: -------------------------------------------------------------------------------- 1 | import ("stdfaust.lib"); 2 | 3 | declare name "DroneLAN"; 4 | declare author "Developpement Grame - CNCM par Elodie Rabibisoa et Romain Constant."; 5 | declare soundfiles "https://raw.githubusercontent.com/grame-cncm/GameLAN/master/drone"; 6 | 7 | // 2 drones : 8 | process = par(i, 2, (multi(i) :> _* (select_drone == i))) :>_ * on_off <:_,_; 9 | 10 | select_drone = hslider("[1]Drones[style:radio{'1':0;'2':1}]", 0, 0, 1, 1); 11 | 12 | on_off = checkbox("[0]ON / OFF"); 13 | 14 | // 4 sounds per drone : 15 | multi(N) = par(i, 4, so.loop(drone(N), i) *(0.25) * volume(i)); 16 | 17 | drone(0) = soundfile("Drone_1 [url:{'Alonepad_reverb_stereo_instru1.flac'; 'Dronepad_test_stereo_instru1.flac'; 'Rain_full_stereo_instru1.flac'; 'Gouttes_eau_mono_instru1.flac'}]", 1); 18 | drone(1) = soundfile("Drone_2 [url:{'Drone_C_filter_stereo_instru2.flac'; 'Pad_C_tremolo_stereo_instru2.flac'; 'Pedale_C_filter_stereo_instru2.flac'; 'String_freeze_stereo_instru2.flac'}]", 1); 19 | 20 | volume(0) = hslider("Volume 0 [acc:0 0 0 0 10][hidden:1]", 0, 0, 1, 0.001) : fi.lowpass(1, 1); 21 | volume(1) = hslider("Volume 1 [acc:0 1 -10 0 0][hidden:1]", 0, 0, 1, 0.001) : fi.lowpass(1, 1); 22 | volume(2) = hslider("Volume 2 [acc:1 0 0 0 10][hidden:1]", 0, 0, 1, 0.001) : fi.lowpass(1, 1); 23 | volume(3) = hslider("Volume 3 [acc:1 1 -10 0 0][hidden:1]", 0, 0, 1, 0.001) : fi.lowpass(1, 1); 24 | -------------------------------------------------------------------------------- /test/web/fft.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 87 | -------------------------------------------------------------------------------- /test/web/fft1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 88 | -------------------------------------------------------------------------------- /test/web/fft2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 114 | -------------------------------------------------------------------------------- /test/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Faust Wasm Generator Demo 6 | 7 | 8 | 9 |

...

10 | 11 | 12 | -------------------------------------------------------------------------------- /test/web/index.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | const div = document.getElementById("log"); 3 | const log = (/** @type {string} */str) => div.innerHTML += str.replace("\n", "
") + "
"; 4 | const svg = (/** @type {string} */str, /** @type {string} */name) => log(str); 5 | const options = "-ftz 2 -I libraries/"; 6 | const errCode = "foo"; 7 | const effectCode = 'process = _*(hslider("Left", 0.1, 0, 1, 0.01)), _*(hslider("Right", 0.0, 0, 1, 0.01));'; 8 | 9 | 10 | (async () => { 11 | const { 12 | instantiateFaustModule, 13 | LibFaust, 14 | WavEncoder, 15 | FaustWasmInstantiator, 16 | FaustMonoDspGenerator, 17 | FaustPolyDspGenerator, 18 | FaustMonoWebAudioDsp, 19 | FaustOfflineProcessor, 20 | FaustCompiler, 21 | FaustSvgDiagrams 22 | } = await import("../../dist/esm-bundle/index.js"); 23 | const faustModule = await instantiateFaustModule(); 24 | 25 | const libFaust = new LibFaust(faustModule); 26 | globalThis.libFaust = libFaust; 27 | 28 | /** 29 | * @param {InstanceType} faust 30 | * @param {(msg: string) => any} log 31 | * @param {string} code 32 | */ 33 | const misc = (faust, log, code) => { 34 | let exp; 35 | try { 36 | exp = faust.expandDSP(code, options); 37 | } catch (e) { 38 | log(" expandDSP " + exp || e.message); 39 | } 40 | 41 | let res; 42 | try { 43 | res = faust.generateAuxFiles("test", code, options + " -svg"); 44 | } catch (e) { 45 | log(" generateAuxFiles " + res ? "done" : e.message); 46 | } 47 | } 48 | /** 49 | * @param {InstanceType} faust 50 | * @param {(msg: string) => any} log 51 | * @param {string} code 52 | */ 53 | const createDsp = async (faust, log, code) => { 54 | log("createMonoDSPFactory: "); 55 | let factory = await faust.createMonoDSPFactory("test", code, options); 56 | if (factory) { 57 | log("factory JSON: " + factory.json); 58 | log("factory poly: " + factory.poly); 59 | } else { 60 | log("factory is null"); 61 | return; 62 | } 63 | log("deleteDSPFactory"); 64 | faust.deleteDSPFactory(factory); 65 | 66 | log("createSyncMonoDSPInstance: "); 67 | let instance1 = FaustWasmInstantiator.createSyncMonoDSPInstance(factory); 68 | if (instance1) { 69 | log(" getNumInputs : " + instance1.api.getNumInputs(0)); 70 | log(" getNumOutputs: " + instance1.api.getNumOutputs(0)); 71 | log(" JSON: " + instance1.json); 72 | } else { 73 | log("instance1 is null"); 74 | } 75 | 76 | log("createAsyncMonoDSPInstance: "); 77 | let instance2 = await FaustWasmInstantiator.createAsyncMonoDSPInstance(factory); 78 | if (instance2) { 79 | log(" getNumInputs : " + instance2.api.getNumInputs(0)); 80 | log(" getNumOutputs: " + instance2.api.getNumOutputs(0)); 81 | log(" JSON: " + instance2.json); 82 | } else { 83 | log("instance2 is null"); 84 | } 85 | } 86 | /** 87 | * @param {InstanceType} faust 88 | * @param {(msg: string) => any} log 89 | * @param {string} voiceCode 90 | * @param {string} effectCode 91 | */ 92 | const createPolyDsp = async (faust, log, voiceCode, effectCode) => { 93 | const { mixerModule } = await faust.getAsyncInternalMixerModule(); 94 | log("createPolyDSPFactory for voice: "); 95 | let voiceFactory = await faust.createPolyDSPFactory("voice", voiceCode, options); 96 | if (voiceFactory) { 97 | log("voice factory JSON: " + voiceFactory.json); 98 | log("voice factory poly: " + voiceFactory.poly); 99 | } else { 100 | log("voice_factory is null"); 101 | return; 102 | } 103 | log("createPolyDSPFactory for effect: "); 104 | let effectFactory = await faust.createPolyDSPFactory("effect", effectCode, options); 105 | if (effectFactory) { 106 | log("effect factory JSON: " + effectFactory.json); 107 | log("effect factory poly: " + effectFactory.poly); 108 | } else { 109 | log("effect_factory is null"); 110 | } 111 | 112 | log("createSyncPolyDSPInstance: "); 113 | let polyInstance1 = FaustWasmInstantiator.createSyncPolyDSPInstance(voiceFactory, mixerModule, 8, effectFactory); 114 | if (polyInstance1) { 115 | log(" voice_api getNumInputs : " + polyInstance1.voiceAPI.getNumInputs(0)); 116 | log(" voice_api getNumOutputs: " + polyInstance1.voiceAPI.getNumOutputs(0)); 117 | log(" JSON: " + polyInstance1.voiceJSON); 118 | log(" effect_api getNumInputs : " + polyInstance1.voiceAPI.getNumInputs(0)); 119 | log(" effect_api getNumOutputs: " + polyInstance1.voiceAPI.getNumOutputs(0)); 120 | log(" JSON: " + polyInstance1.effectJSON); 121 | } else { 122 | log("poly_instance1 is null"); 123 | } 124 | 125 | log("createAsyncPolyDSPInstance: "); 126 | let polyInstance2 = await FaustWasmInstantiator.createAsyncPolyDSPInstance(voiceFactory, mixerModule, 8, effectFactory); 127 | if (polyInstance2) { 128 | log(" voice_api getNumInputs : " + polyInstance2.voiceAPI.getNumInputs(0)); 129 | log(" voice_api getNumOutputs: " + polyInstance2.voiceAPI.getNumOutputs(0)); 130 | log(" JSON: " + polyInstance2.voiceJSON); 131 | log(" effect_api getNumInputs : " + polyInstance2.effectAPI.getNumInputs(0)); 132 | log(" effect_api getNumOutputs: " + polyInstance2.effectAPI.getNumOutputs(0)); 133 | log(" JSON: " + polyInstance2.effectJSON); 134 | } else { 135 | log("poly_instance2 is null"); 136 | } 137 | } 138 | 139 | /** 140 | * @param {InstanceType} compiler 141 | * @param {(msg: string) => any} log 142 | * @param {string} code 143 | */ 144 | const svgdiagrams = (compiler, log, code) => { 145 | const filter = "import(\"stdfaust.lib\");\nprocess = dm.oscrs_demo;"; 146 | const SvgDiagrams = new FaustSvgDiagrams(compiler); 147 | 148 | let svg1 = SvgDiagrams.from("TestSVG1", code, options); 149 | log(`
${svg1["process.svg"]}
`); 150 | 151 | let svg2 = SvgDiagrams.from("TestSVG2", filter, options) 152 | log(`
${svg2["process.svg"]}
`); 153 | } 154 | 155 | /** 156 | * @param {InstanceType} faust 157 | * @param {(msg: string) => any} log 158 | */ 159 | const offlineProcessor = async (faust, log) => { 160 | 161 | let signal = "import(\"stdfaust.lib\");\nprocess = 0.25,0.33, 0.6;"; 162 | let factory = await faust.createMonoDSPFactory("test", signal, options); 163 | const instance = await FaustWasmInstantiator.createAsyncMonoDSPInstance(factory); 164 | const dsp = new FaustMonoWebAudioDsp(instance, 48000, 4, 33); 165 | 166 | log("offlineProcessor"); 167 | let offline = new FaustOfflineProcessor(dsp, 33); 168 | let plotted = offline.render(null, 100); 169 | for (let chan = 0; chan < plotted.length; chan++) { 170 | for (let frame = 0; frame < 100; frame++) { 171 | console.log("Chan %d sample %f\n", chan, plotted[chan][frame]) 172 | } 173 | } 174 | } 175 | /** 176 | * 177 | * @param {InstanceType} libFaust 178 | * @param {(msg: string) => any} log 179 | * @param {string} code 180 | * @param {AudioContext} context 181 | */ 182 | const run = async (libFaust, log, code, context) => { 183 | const compiler = new FaustCompiler(libFaust); 184 | log("libfaust version: " + compiler.version()); 185 | 186 | log("\n-----------------\nMisc tests" + compiler.version()); 187 | misc(compiler, log, code); 188 | log("\n-----------------\nMisc tests with error code"); 189 | misc(compiler, log, errCode); 190 | 191 | log("\n-----------------\nCreating DSP instance:"); 192 | await createDsp(compiler, log, code); 193 | 194 | log("\n-----------------\nCreating Poly DSP instance:"); 195 | await createPolyDsp(compiler, log, code, effectCode); 196 | 197 | log("\n-----------------\nCreating DSP instance with error code:"); 198 | await createDsp(compiler, log, errCode).catch(e => { log(e.message); }); 199 | 200 | log("\n-----------------\nCreating Poly DSP instance with error code:"); 201 | await createPolyDsp(compiler, log, errCode, effectCode).catch(e => { log(e.message); }); 202 | 203 | log("\n-----------------\nTest SVG diagrams: "); 204 | svgdiagrams(compiler, log, code); 205 | 206 | log("\n-----------------\nTest Offline processor "); 207 | offlineProcessor(compiler, log); 208 | 209 | const gen = new FaustPolyDspGenerator() 210 | await gen.compile(compiler, "mydsp2", code, options, effectCode); 211 | const node = await gen.createNode(ctx, 8); 212 | console.log(node); 213 | console.log(node.getParams()); 214 | console.log(node.getMeta()); 215 | node.connect(ctx.destination); 216 | node.keyOn(0, 60, 50); 217 | node.keyOn(0, 64, 50); 218 | node.keyOn(0, 67, 50); 219 | node.keyOn(0, 71, 50); 220 | node.keyOn(0, 76, 50); 221 | 222 | log("\nEnd of API tests"); 223 | }; 224 | const sampleRate = 48000; 225 | const code1Fetch = await fetch("../organ.dsp"); 226 | const code1 = await code1Fetch.text(); 227 | const name1 = "pdj"; 228 | const code2Fetch = await fetch("../rev.dsp"); 229 | const code2 = await code2Fetch.text(); 230 | 231 | const ctx = new AudioContext(); 232 | 233 | await run(libFaust, log, code1, ctx); 234 | globalThis.ctx = ctx; 235 | })(); 236 | -------------------------------------------------------------------------------- /test/web/mono.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 46 | -------------------------------------------------------------------------------- /test/web/poly-key.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 62 | -------------------------------------------------------------------------------- /test/web/poly.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 59 | -------------------------------------------------------------------------------- /test/web/soundfile.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 69 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "target": "ES2019", 7 | "sourceMap": true, 8 | "outDir": "dist", 9 | "noImplicitAny": true, 10 | "strictNullChecks": true 11 | }, 12 | "include": [ 13 | "src/**/*" 14 | ], 15 | "exclude": [ 16 | "dist", 17 | "node_modules", 18 | "libfaust-wasm", 19 | "test", 20 | "assets" 21 | ] 22 | } --------------------------------------------------------------------------------