@grame/faustwasm 0.11.3 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,344 @@
1
+ /**
2
+ * @module faust-pwa
3
+ * High-level controller around a Faust DSP node for Web Audio apps.
4
+ *
5
+ * ## Public surface
6
+ * - {@link createFaustPWA} – factory function (recommended entry point)
7
+ * - {@link FaustPWA} – class (advanced use). Methods marked @public are supported.
8
+ *
9
+ * ## Private surface
10
+ * Any symbol/method/field marked `@private` or not listed as @public is internal.
11
+ * Names prefixed with `f` (e.g. `fFaustNode`) are implementation details.
12
+ *
13
+ * ## Emitted events
14
+ * Instances emit the following CustomEvent names via {@link FaustPWA#on}:
15
+ * - `created` — when the DSP node and (optional) UI are created.
16
+ * - `started` — when audio/MIDI/sensors are activated.
17
+ * - `stopped` — when audio/MIDI/sensors are deactivated.
18
+ * - `destroy` — after teardown completes.
19
+ * - `error` — when an operational error occurs; payload in `event.detail.error`.
20
+ */
21
+
22
+ /**
23
+ * @typedef {import("./faustwasm").FaustAudioWorkletNode} FaustAudioWorkletNode
24
+ * @typedef {import("./faustwasm").FaustDspMeta} FaustDspMeta
25
+ * @typedef {import("./faustwasm").FaustUIDescriptor} FaustUIDescriptor
26
+ * @typedef {import("./faustwasm").FaustUIGroup} FaustUIGroup
27
+ * @typedef {import("./faustwasm").FaustUIItem} FaustUIItem
28
+ */
29
+
30
+ /**
31
+ * @typedef FaustPWAOptions
32
+ * @property {string} dspName - DSP name string for createFaustNode (required).
33
+ * @property {number} [voices=0] - Polyphony: 0 for mono, >0 for poly.
34
+ * @property {boolean} [useScriptProcessor=false] - Fallback to ScriptProcessorNode mode.
35
+ * @property {number} [bufferSize=512] - Buffer size for ScriptProcessorNode mode.
36
+ * @property {HTMLElement | null} uiContainer - Where to render the Faust UI (optional).
37
+ */
38
+
39
+ /**
40
+ * FaustPWA class to manage Faust DSP with PWA features (service worker, MIDI, sensors, keyboard).
41
+ */
42
+ export class FaustPWA {
43
+
44
+ /**
45
+ * @param {FaustPWAOptions} options;
46
+ */
47
+
48
+ constructor(options) {
49
+
50
+ /** @private internal activation flags */
51
+ this.fActive = { midi: false, sensors: false };
52
+
53
+ /** @type {FaustPWAOptions} */
54
+ this.fOptions = {
55
+ voices: 0,
56
+ useScriptProcessor: false,
57
+ bufferSize: 512,
58
+ uiContainer: null,
59
+ ...options,
60
+ };
61
+
62
+ /** @type {AudioContext} */
63
+ const AudioCtx = window.AudioContext || window.webkitAudioContext;
64
+ this.fAudioContext = this.fOptions.audioContext || new AudioCtx({ latencyHint: 0.00001 })
65
+ this.fAudioContext.destination.channelInterpretation = "discrete";
66
+ this.fAudioContext.suspend();
67
+
68
+ /** @type {FaustAudioWorkletNode | null} */
69
+ this.fFaustNode = null;
70
+
71
+ // Event system
72
+ this.fEvents = new EventTarget();
73
+
74
+ // MIDI and Sensor handlers state
75
+ this.fActive.sensors = false;
76
+ this.fActive.midi = false;
77
+
78
+ // Keyboard to MIDI handing
79
+ this.fKeyboard2MIDI = null;
80
+
81
+ /**
82
+ * Registers the service worker.
83
+ */
84
+ if ("serviceWorker" in navigator) {
85
+ window.addEventListener("load", () => {
86
+ navigator.serviceWorker.register("./service-worker.js")
87
+ .then(reg => console.log("Service Worker registered", reg))
88
+ .catch(err => console.log("Service Worker registration failed", err));
89
+ });
90
+ }
91
+ }
92
+
93
+ // Function to start MIDI
94
+ startMIDI() {
95
+ // Check if the browser supports the Web MIDI API
96
+ if (navigator.requestMIDIAccess) {
97
+ navigator.requestMIDIAccess().then(
98
+ midiAccess => {
99
+ console.log("MIDI Access obtained.");
100
+ for (let input of midiAccess.inputs.values()) {
101
+ input.onmidimessage = (event) => this.fFaustNode.midiMessage(event.data);
102
+ console.log(`Connected to input: ${input.name}`);
103
+ }
104
+ },
105
+ () => console.error("Failed to access MIDI devices.")
106
+ );
107
+ } else {
108
+ console.log("Web MIDI API is not supported in this browser.");
109
+ }
110
+ }
111
+
112
+ // Function to stop MIDI
113
+ stopMIDI() {
114
+ // Check if the browser supports the Web MIDI API
115
+ if (navigator.requestMIDIAccess) {
116
+ navigator.requestMIDIAccess().then(
117
+ midiAccess => {
118
+ console.log("MIDI Access obtained.");
119
+ for (let input of midiAccess.inputs.values()) {
120
+ input.onmidimessage = null;
121
+ console.log(`Disconnected from input: ${input.name}`);
122
+ }
123
+ },
124
+ () => console.error("Failed to access MIDI devices.")
125
+ );
126
+ } else {
127
+ console.log("Web MIDI API is not supported in this browser.");
128
+ }
129
+ }
130
+
131
+ // Function to start Keyboard to MIDI
132
+ async startKeyboard2MIDI() {
133
+ // Import the create-node module
134
+ const { createKey2MIDI } = await import("./create-node.js");
135
+
136
+ this.fKeyboard2MIDI = createKey2MIDI((event) => this.fFaustNode.midiMessage(event));
137
+ this.fKeyboard2MIDI.start();
138
+ }
139
+
140
+ // Function to stop Keyboard to MIDI
141
+ stopKeyboard2MIDI() {
142
+ this.fKeyboard2MIDI.stop();
143
+ this.fKeyboard2MIDI = null;
144
+ }
145
+
146
+ /**
147
+ * Dispatch a custom event.
148
+ * @param {string} type
149
+ * @param {any} detail
150
+ * @private
151
+ */
152
+ dispatch(type, detail = undefined) {
153
+ this.fEvents.dispatchEvent(new CustomEvent(type, { detail, bubbles: false, composed: true }));
154
+ }
155
+
156
+ // Public API
157
+
158
+ // ---------------------- Event API ---------------------------------
159
+
160
+ /**
161
+ * Add a listener for a custom event.
162
+ * @param {string} type
163
+ * @param {(e: CustomEvent) => void} handler
164
+ */
165
+ on(type, handler) {
166
+ this.fEvents.addEventListener(type, handler);
167
+ }
168
+
169
+ /**
170
+ * Remove a listener for a custom event.
171
+ * @param {string} type
172
+ * @param {(e: CustomEvent) => void} handler
173
+ */
174
+ off(type, handler) {
175
+ this.fEvents.removeEventListener(type, handler);
176
+ }
177
+
178
+ // ---------------------- Audio/MIDI/Sensors API ---------------------------------
179
+
180
+ // Getters for AudioContext
181
+ get audioContext() {
182
+ return this.fAudioContext;
183
+ }
184
+
185
+ // Getter for FaustNode
186
+ get faustNode() {
187
+ return this.fFaustNode;
188
+ }
189
+
190
+ // Synchronous function to resume AudioContext, to be called first in the synchronous event listener
191
+ resumeAudioContext() {
192
+ if (this.fAudioContext.state === 'suspended') {
193
+ this.fAudioContext.resume().then(() => {
194
+ console.log('AudioContext resumed successfully');
195
+ }).catch(error => {
196
+ console.error('Error when resuming AudioContext:', error);
197
+ });
198
+ }
199
+ }
200
+
201
+ // Asynchronous function to suspend AudioContext
202
+ async suspendAudioContext() {
203
+ // Suspend the AudioContext
204
+ if (this.fAudioContext.state === 'running') {
205
+ await this.fAudioContext.suspend();
206
+ }
207
+ }
208
+
209
+ // Function to activate MIDI and Sensors on user interaction
210
+ async activateMIDISensors() {
211
+
212
+ // Import the create-node module
213
+ const { connectToAudioInput, requestPermissions } = await import("./create-node.js");
214
+
215
+ // Request permission for sensors
216
+ await requestPermissions();
217
+
218
+ // Activate sensor listeners
219
+ if (!this.fActive.sensors) {
220
+ await this.fFaustNode.startSensors();
221
+ this.fActive.sensors = true;
222
+ }
223
+
224
+ // Initialize the MIDI setup
225
+ if (!this.fActive.midi) {
226
+ this.startMIDI();
227
+ await this.startKeyboard2MIDI();
228
+ this.fActive.midi = true;
229
+ }
230
+
231
+ // Connect the Faust node to the audio output@
232
+ this.fFaustNode.connect(this.fAudioContext.destination);
233
+
234
+ // Connect the Faust node to the audio input
235
+ if (this.fFaustNode.numberOfInputs > 0) {
236
+ await connectToAudioInput(this.fAudioContext, null, this.fFaustNode, null);
237
+ }
238
+ }
239
+
240
+ // Function to deactivate MIDI and Sensors on user interaction
241
+ async deactivateMIDISensors() {
242
+
243
+ // Deactivate sensor listeners
244
+ if (this.fActive.sensors) {
245
+ this.fFaustNode.stopSensors();
246
+ this.fActive.sensors = false;
247
+ }
248
+
249
+ // Deactivate the MIDI setup
250
+ if (this.fActive.midi && this.fOptions.voices > 0) {
251
+ this.stopMIDI();
252
+ this.stopKeyboard2MIDI();
253
+ this.fActive.midi = false;
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Load and initialize the Faust node and (optionally) UI.
259
+ * @returns {Promise<void>}
260
+ */
261
+ async create() {
262
+
263
+ try {
264
+ const { createFaustNode, createFaustUI } = await import('./create-node.js');
265
+
266
+ // Create the Faust node
267
+ const result = await createFaustNode(
268
+ this.fAudioContext,
269
+ this.fOptions.dspName,
270
+ this.fOptions.voices ?? 0,
271
+ this.fOptions.useScriptProcessor ?? false,
272
+ this.fOptions.bufferSize ?? 512
273
+ );
274
+
275
+ this.fFaustNode = result.faustNode; // Assign to the global variable
276
+ if (!this.fFaustNode) throw new Error("Faust DSP not compiled");
277
+
278
+ // Create the Faust UI
279
+ await createFaustUI(this.fOptions.uiContainer, this.fFaustNode);
280
+
281
+ this.dispatch('created', {
282
+ workletName: result.workletName,
283
+ sampleRate: result.sampleRate,
284
+ voices: this.fOptions.voices ?? 0,
285
+ });
286
+
287
+ } catch (err) {
288
+ this.dispatch('error', { error: err });
289
+ throw err;
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Fully disposes audio resources and UI.
295
+ * @returns {Promise<void>}
296
+ */
297
+ async destroy() {
298
+ // Cleanup MIDI and Sensors
299
+ this.stop();
300
+ this.fFaustNode.destroy();
301
+ this.dispatch('destroy', {});
302
+ }
303
+
304
+ /**
305
+ * Start audio: resumes AudioContext if needed and connects to destination.
306
+ * @returns {Promise<void>}
307
+ */
308
+ async start() {
309
+ // Resume AudioContext synchronously
310
+ this.resumeAudioContext();
311
+
312
+ // Launch the activation of MIDI and Sensors
313
+ this.activateMIDISensors().catch(error => {
314
+ console.error('Error when activating audio, MIDI and sensors:', error);
315
+ });
316
+
317
+ // Dispatch the started event
318
+ this.dispatch('started', { when: this.audioContext.currentTime });
319
+ }
320
+
321
+ /**
322
+ * Stop audio: disconnects from destination but keeps node allocated.
323
+ */
324
+ stop() {
325
+
326
+ // Deactivate MIDI and Sensors
327
+ this.deactivateMIDISensors();
328
+ this.fFaustNode.stop();
329
+
330
+ // Dispatch the stopped event
331
+ this.dispatch('stopped', { when: this.audioContext.currentTime });
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Factory to create a {@link FaustPWA} instance.
337
+ * @function
338
+ * @public
339
+ * @param {FaustPWAOptions} options
340
+ * @returns {FaustPWA}
341
+ */
342
+ export function createFaustPWA(options) {
343
+ return new FaustPWA(options);
344
+ }
@@ -1,201 +1,69 @@
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
1
 
12
2
  /**
13
- * Registers the service worker.
3
+ * @file index-pwa.js
4
+ * Entry point for initializing and managing the Faust PWA instance.
5
+ *
6
+ * ## Overview
7
+ * This script dynamically imports {@link createFaustPWA} from `faust-pwa.js`,
8
+ * creates the Faust PWA controller, and manages user-activation of the
9
+ * Web Audio API (click/touch) as well as visibility-based suspension.
10
+ *
11
+ * ## Responsibilities
12
+ * - Dynamically import `createFaustPWA` (ES module)
13
+ * - Create the PWA instance for the selected DSP
14
+ * - Resume/suspend AudioContext on user interaction or page visibility changes
15
+ * - Activate/deactivate MIDI and sensors
16
+ *
17
+ * ## Emitted Events (from FaustPWA)
18
+ * - `created` — DSP and UI successfully initialized
19
+ * - `started` — Audio, MIDI, and sensors activated
20
+ * - `stopped` — Audio, MIDI, and sensors deactivated
21
+ * - `destroy` — Resources released
22
+ * - `error` — Operational error; see event.detail.error
23
+ *
24
+ * @module index-pwa
14
25
  */
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
26
 
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;
27
+ // Set to > 0 if the DSP is polyphonic
28
+ const FAUST_DSP_VOICES = 0;
34
29
 
35
- // Called at load time
36
30
  (async () => {
37
31
 
38
- // Import the create-node module
39
- const { createFaustNode, createFaustUI } = await import("./create-node.js");
32
+ const { FaustPWA } = await import("./faust-pwa.js");
40
33
 
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");
34
+ /** @type {HTMLDivElement} */
35
+ const divFaustUI = document.getElementById("div-faust-ui");
46
36
 
47
- // Create the Faust UI
48
- await createFaustUI($divFaustUI, faustNode);
37
+ // Declare faustNode as a global variable
38
+ const pwa = new FaustPWA({ dspName: "FAUST_DSP_NAME", voices: FAUST_DSP_VOICES, uiContainer: divFaustUI });
49
39
 
50
- })();
40
+ // Create PWA and UI
41
+ await pwa.create();
51
42
 
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
- }
43
+ // Event listener to handle user interaction
44
+ function handleUserInteraction() {
62
45
 
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
- }
46
+ // Resume AudioContext synchronously
47
+ pwa.resumeAudioContext();
81
48
 
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.");
49
+ // Activate MIDI and Sensors
50
+ pwa.activateMIDISensors()
98
51
  }
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
52
 
111
- keyboard2MIDI = createKey2MIDI((event) => faustNode.midiMessage(event));
112
- keyboard2MIDI.start();
113
- }
53
+ // Activate AudioContext, MIDI and Sensors on user interaction
54
+ window.addEventListener('click', handleUserInteraction);
55
+ window.addEventListener('touchstart', handleUserInteraction);
114
56
 
115
- function stopKeyboard2MIDI() {
116
- keyboard2MIDI.stop();
117
- keyboard2MIDI = null;
118
- }
57
+ // Deactivate AudioContext, MIDI and Sensors on user interaction
58
+ window.addEventListener('visibilitychange', function () {
59
+ if (window.visibilityState === 'hidden') {
119
60
 
120
- // Function to activate MIDI and Sensors on user interaction
121
- async function activateMIDISensors() {
61
+ // Suspend AudioContext
62
+ pwa.suspendAudioContext();
122
63
 
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);
64
+ // Deactivate MIDI and Sensors
65
+ pwa.deactivateMIDISensors();
66
+ }
187
67
  });
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
68
 
69
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grame/faustwasm",
3
- "version": "0.11.3",
3
+ "version": "0.12.0",
4
4
  "description": "WebAssembly version of Faust Compiler",
5
5
  "main": "dist/cjs/index.js",
6
6
  "types": "dist/esm/index.d.ts",
@@ -80,6 +80,9 @@ const copyWebPWAAssets = (outputDir, dspName, poly = false, effect = false) => {
80
80
  const templateJSPath = path.join(__dirname, "../assets/standalone/index-pwa.js");
81
81
  cpSyncModify(templateJSPath, outputDir + `/index.js`, ...findAndReplace);
82
82
 
83
+ const templatePWAJSPath = path.join(__dirname, "../assets/standalone/faust-pwa.js");
84
+ cpSyncModify(templatePWAJSPath, outputDir + `/faust-pwa.js`, ...findAndReplace);
85
+
83
86
  const templateHTMLPath = path.join(__dirname, "../assets/standalone/index-pwa.html");
84
87
  cpSyncModify(templateHTMLPath, outputDir + `/index.html`, ...findAndReplace);
85
88
 
@@ -22,9 +22,6 @@
22
22
  <P>Settings (buffer size, polyphonic mode and voices, audio rendering model, sample size and ftz mode) can be
23
23
  changed dynamically
24
24
  and the DSP will be recompiled on the fly.
25
- <P>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
25
  <div class="config">
29
26
 
30
27
  <div class="config-element">