@grame/faust-web-component 0.3.9 → 0.4.1

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.
package/index.html CHANGED
@@ -8,11 +8,14 @@
8
8
 
9
9
  <faust-editor>
10
10
  <!--
11
+ declare name "Vocal FOF MIDI";
12
+ declare description "MIDI-controllable FOF vocal synthesizer.";
13
+ declare license "MIT";
14
+ declare copyright "(c)Mike Olsen, CCRMA (Stanford University)";
15
+
11
16
  import("stdfaust.lib");
12
- ctFreq = hslider("cutoffFrequency",500,50,10000,0.01);
13
- q = hslider("q",5,1,30,0.1);
14
- gain = hslider("gain",1,0,1,0.01);
15
- process = no.noise : fi.resonlp(ctFreq,q,gain);
17
+
18
+ process = pm.SFFormantModelFofSmooth_ui_MIDI <: _,_;
16
19
  -->
17
20
  </faust-editor>
18
21
  <center>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@grame/faust-web-component",
3
3
  "description": "Web component embedding the Faust Compiler",
4
- "version": "0.3.9",
4
+ "version": "0.4.1",
5
5
  "module": "dist/faust-web-component.js",
6
6
  "files": [
7
7
  "src/",
@@ -42,7 +42,7 @@
42
42
  "@codemirror/legacy-modes": "^6.3.3",
43
43
  "@fortawesome/fontawesome-svg-core": "^6.4.2",
44
44
  "@fortawesome/free-solid-svg-icons": "^6.4.2",
45
- "@grame/faustwasm": "^0.5.4",
45
+ "@grame/faustwasm": "^0.7.0",
46
46
  "@shren/faust-ui": "^1.1.13",
47
47
  "codemirror": "^6.0.1",
48
48
  "split.js": "^1.6.5"
@@ -1,13 +1,28 @@
1
- import { icon } from "@fortawesome/fontawesome-svg-core"
2
- import { FaustMonoDspGenerator, FaustPolyDspGenerator, IFaustMonoWebAudioNode } from "@grame/faustwasm"
3
- import { FaustUI } from "@shren/faust-ui"
4
- import faustCSS from "@shren/faust-ui/dist/esm/index.css?inline"
5
- import Split from "split.js"
6
- import { faustPromise, audioCtx, compiler, svgDiagrams, default_generator, get_mono_generator, get_poly_generator, getInputDevices, deviceUpdateCallbacks, accessMIDIDevice, midiInputCallback, extractMidiAndNvoices } from "./common"
7
- import { createEditor, setError, clearError } from "./editor"
8
- import { Scope } from "./scope"
9
- import faustSvg from "./faustText.svg"
10
-
1
+ // Import necessary libraries and modules
2
+ import { icon } from "@fortawesome/fontawesome-svg-core";
3
+ import { FaustMonoDspGenerator, FaustPolyDspGenerator, IFaustMonoWebAudioNode } from "@grame/faustwasm";
4
+ import { FaustUI } from "@shren/faust-ui";
5
+ import faustCSS from "@shren/faust-ui/dist/esm/index.css?inline";
6
+ import Split from "split.js";
7
+ import {
8
+ faustPromise,
9
+ audioCtx,
10
+ compiler,
11
+ svgDiagrams,
12
+ default_generator,
13
+ get_mono_generator,
14
+ get_poly_generator,
15
+ getInputDevices,
16
+ deviceUpdateCallbacks,
17
+ accessMIDIDevice,
18
+ midiInputCallback,
19
+ extractMidiAndNvoices
20
+ } from "./common";
21
+ import { createEditor, setError, clearError } from "./editor";
22
+ import { Scope } from "./scope";
23
+ import faustSvg from "./faustText.svg";
24
+
25
+ // Create a template for the component
11
26
  const template = document.createElement("template")
12
27
  template.innerHTML = `
13
28
  <div id="root">
@@ -212,16 +227,22 @@ template.innerHTML = `
212
227
  </style>
213
228
  `
214
229
 
230
+ // FaustEditor Web Component
215
231
  export default class FaustEditor extends HTMLElement {
216
232
  constructor() {
217
233
  super()
218
234
  }
219
235
 
220
236
  connectedCallback() {
237
+ // Initial setup when the component is attached to the DOM
221
238
  const code = this.innerHTML.replace("<!--", "").replace("-->", "").trim()
222
239
  this.attachShadow({ mode: "open" }).appendChild(template.content.cloneNode(true))
223
240
 
241
+ // Set up links, buttons, and editor
224
242
  const ideLink = this.shadowRoot!.querySelector("#ide") as HTMLAnchorElement
243
+ const editorEl = this.shadowRoot!.querySelector("#editor") as HTMLDivElement
244
+ const editor = createEditor(editorEl, code)
245
+
225
246
  ideLink.onfocus = () => {
226
247
  // Open current contents of editor in IDE
227
248
  const urlParams = new URLSearchParams()
@@ -229,9 +250,6 @@ export default class FaustEditor extends HTMLElement {
229
250
  ideLink.href = `https://faustide.grame.fr/?${urlParams.toString()}`
230
251
  }
231
252
 
232
- const editorEl = this.shadowRoot!.querySelector("#editor") as HTMLDivElement
233
- const editor = createEditor(editorEl, code)
234
-
235
253
  const runButton = this.shadowRoot!.querySelector("#run") as HTMLButtonElement
236
254
  const stopButton = this.shadowRoot!.querySelector("#stop") as HTMLButtonElement
237
255
  const faustUIRoot = this.shadowRoot!.querySelector("#faust-ui") as HTMLDivElement
@@ -241,6 +259,7 @@ export default class FaustEditor extends HTMLElement {
241
259
  const tabButtons = [...this.shadowRoot!.querySelectorAll(".tab")] as HTMLButtonElement[]
242
260
  const tabContents = [...sidebarContent.querySelectorAll("div")] as HTMLDivElement[]
243
261
 
262
+ // Initialize split.js for resizable editor and sidebar
244
263
  const split = Split([editorEl, sidebar], {
245
264
  sizes: [100, 0],
246
265
  minSize: [0, 20],
@@ -251,8 +270,11 @@ export default class FaustEditor extends HTMLElement {
251
270
 
252
271
  faustPromise.then(() => runButton.disabled = false)
253
272
 
273
+ // Default sizes for sidebar
254
274
  const defaultSizes = [70, 30]
255
275
  let sidebarOpen = false
276
+
277
+ // Function to open the sidebar with predefined sizes
256
278
  const openSidebar = () => {
257
279
  if (!sidebarOpen) {
258
280
  split.setSizes(defaultSizes)
@@ -260,6 +282,7 @@ export default class FaustEditor extends HTMLElement {
260
282
  sidebarOpen = true
261
283
  }
262
284
 
285
+ // Variables for audio and visualization nodes
263
286
  let node: IFaustMonoWebAudioNode | undefined
264
287
  let input: MediaStreamAudioSourceNode | undefined
265
288
  let analyser: AnalyserNode | undefined
@@ -269,30 +292,33 @@ export default class FaustEditor extends HTMLElement {
269
292
  let gnvoices = -1
270
293
  let sourceNode: AudioBufferSourceNode = undefined;
271
294
 
295
+ // Event handler for the run button
272
296
  runButton.onclick = async () => {
273
297
  if (audioCtx.state === "suspended") {
274
298
  await audioCtx.resume()
275
299
  }
276
300
  await faustPromise
301
+
277
302
  // Compile Faust code
278
303
  const code = editor.state.doc.toString()
279
304
  let generator = null
280
305
  try {
281
306
  // Compile Faust code to access JSON metadata
282
- await default_generator.compile(compiler, "main", code, "")
307
+ await default_generator.compile(compiler, "main", code, "-ftz 2")
283
308
  const json = default_generator.getMeta()
284
309
  let { midi, nvoices } = extractMidiAndNvoices(json);
285
310
  gmidi = midi;
286
311
  gnvoices = nvoices;
287
312
 
288
- // Build the generator
289
- generator = nvoices > 0 ? get_poly_generator() : get_mono_generator();
313
+ // Build the generator (possibly reusing default_generator which is a FaustMonoDspGenerator)
314
+ generator = nvoices > 0 ? get_poly_generator() : default_generator;
290
315
  await generator.compile(compiler, "main", code, "-ftz 2");
291
316
 
292
317
  } catch (e: any) {
293
318
  setError(editor, e)
294
319
  return
295
320
  }
321
+
296
322
  // Clear any old errors
297
323
  clearError(editor)
298
324
 
@@ -303,6 +329,8 @@ export default class FaustEditor extends HTMLElement {
303
329
  } else {
304
330
  node = (await (generator as FaustMonoDspGenerator).createNode(audioCtx))!
305
331
  }
332
+
333
+ // Set up audio input if necessary
306
334
  if (node.numberOfInputs > 0) {
307
335
  audioInputSelector.disabled = false
308
336
  updateInputDevices(await getInputDevices())
@@ -317,7 +345,10 @@ export default class FaustEditor extends HTMLElement {
317
345
  tabButton.disabled = false
318
346
  }
319
347
 
320
- // Access MIDI device
348
+ // Start sensors if available
349
+ await node.startSensors();
350
+
351
+ // Access MIDI device if available
321
352
  if (gmidi) {
322
353
  accessMIDIDevice(midiInputCallback(node))
323
354
  .then(() => {
@@ -329,6 +360,7 @@ export default class FaustEditor extends HTMLElement {
329
360
  }
330
361
 
331
362
  openSidebar()
363
+
332
364
  // Clear old tab contents
333
365
  for (const tab of tabContents) {
334
366
  while (tab.lastChild) tab.lastChild.remove()
@@ -350,15 +382,13 @@ export default class FaustEditor extends HTMLElement {
350
382
  faustUI.paramChangeByUI = (path, value) => node?.setParamValue(path, value)
351
383
  node.setOutputParamHandler((path, value) => faustUI.paramChangeByDSP(path, value))
352
384
 
353
- // Create SVG block diagram
354
- setSVG(svgDiagrams.from("main", code, "")["process.svg"])
355
-
356
385
  // Set editor size to fit UI size
357
386
  editorEl.style.height = `${Math.max(125, faustUI.minHeight)}px`;
358
387
  faustUIRoot.style.width = faustUI.minWidth * 1.25 + "px"
359
388
  faustUIRoot.style.height = faustUI.minHeight * 1.25 + "px"
360
389
  }
361
390
 
391
+ // Function to set SVG in the block diagram tab
362
392
  const setSVG = (svgString: string) => {
363
393
  faustDiagram.innerHTML = svgString
364
394
 
@@ -373,6 +403,8 @@ export default class FaustEditor extends HTMLElement {
373
403
  }
374
404
 
375
405
  let animPlot: number | undefined
406
+
407
+ // Function to render the scope
376
408
  const drawScope = () => {
377
409
  scope!.renderScope([{
378
410
  analyser: analyser!,
@@ -382,11 +414,13 @@ export default class FaustEditor extends HTMLElement {
382
414
  animPlot = requestAnimationFrame(drawScope)
383
415
  }
384
416
 
417
+ // Function to render the spectrum
385
418
  const drawSpectrum = () => {
386
419
  spectrum!.renderSpectrum(analyser!)
387
420
  animPlot = requestAnimationFrame(drawSpectrum)
388
421
  }
389
422
 
423
+ // Function to switch between tabs
390
424
  const openTab = (i: number) => {
391
425
  for (const [j, tab] of tabButtons.entries()) {
392
426
  if (i === j) {
@@ -397,7 +431,20 @@ export default class FaustEditor extends HTMLElement {
397
431
  tabContents[j].classList.remove("active")
398
432
  }
399
433
  }
400
- if (i === 2) {
434
+ // Check if the clicked tab is the "Block Diagram" tab (index 1)
435
+ if (i === 1) {
436
+ // Only set the SVG if it hasn't been set before (avoiding multiple loads)
437
+ if (faustDiagram.innerHTML.trim() === "") {
438
+
439
+ // Display a "Computing SVG..." message while the SVG is being generated
440
+ faustDiagram.innerHTML = "<p><center>Computing SVG...</center></p>";
441
+
442
+ // Use setTimeout to defer the SVG rendering to a separate task
443
+ setTimeout(() => {
444
+ setSVG(svgDiagrams.from("main", code, "")["process.svg"]);
445
+ }, 0);
446
+ }
447
+ } else if (i === 2) {
401
448
  scope!.onResize()
402
449
  if (animPlot !== undefined) cancelAnimationFrame(animPlot)
403
450
  animPlot = requestAnimationFrame(drawScope)
@@ -411,22 +458,27 @@ export default class FaustEditor extends HTMLElement {
411
458
  }
412
459
  }
413
460
 
461
+ // Attach event listeners to tab buttons
414
462
  for (const [i, tabButton] of tabButtons.entries()) {
415
463
  tabButton.onclick = () => openTab(i)
416
464
  }
417
465
 
466
+ // Event handler for the stop button
418
467
  stopButton.onclick = () => {
419
468
  if (node !== undefined) {
420
- node.disconnect()
421
- node.destroy()
422
- node = undefined
423
- stopButton.disabled = true
469
+ node.disconnect();
470
+ node.stopSensors();
471
+ node.destroy();
472
+ node = undefined;
473
+ stopButton.disabled = true;
424
474
  // TODO: Maybe disable controls in faust-ui tab.
425
475
  }
426
476
  }
427
477
 
478
+ // Audio input selector element
428
479
  const audioInputSelector = this.shadowRoot!.querySelector("#audio-input") as HTMLSelectElement
429
480
 
481
+ // Update the audio input device list
430
482
  const updateInputDevices = (devices: MediaDeviceInfo[]) => {
431
483
  if (audioInputSelector.disabled) return
432
484
  while (audioInputSelector.lastChild) audioInputSelector.lastChild.remove()
@@ -439,6 +491,7 @@ export default class FaustEditor extends HTMLElement {
439
491
  }
440
492
  deviceUpdateCallbacks.push(updateInputDevices)
441
493
 
494
+ // Connect the selected audio input device
442
495
  const connectInput = async () => {
443
496
  const deviceId = audioInputSelector.value
444
497
  const stream = await navigator.mediaDevices.getUserMedia({ audio: { deviceId, echoCancellation: false, noiseSuppression: false, autoGainControl: false } })
@@ -1,11 +1,29 @@
1
- import { icon } from "@fortawesome/fontawesome-svg-core"
2
- import faustCSS from "@shren/faust-ui/dist/esm/index.css?inline"
3
- import faustSvg from "./faustText.svg"
4
- import { FaustMonoDspGenerator, FaustPolyDspGenerator, IFaustMonoWebAudioNode } from "@grame/faustwasm"
5
- import { IFaustPolyWebAudioNode } from "@grame/faustwasm"
6
- import { FaustUI } from "@shren/faust-ui"
7
- import { faustPromise, audioCtx, get_mono_generator, get_poly_generator, compiler, getInputDevices, deviceUpdateCallbacks, accessMIDIDevice, midiInputCallback, extractMidiAndNvoices, default_generator } from "./common"
8
-
1
+ // Import necessary libraries and modules
2
+ import { icon } from "@fortawesome/fontawesome-svg-core";
3
+ import faustCSS from "@shren/faust-ui/dist/esm/index.css?inline";
4
+ import faustSvg from "./faustText.svg";
5
+ import {
6
+ FaustMonoDspGenerator,
7
+ FaustPolyDspGenerator,
8
+ IFaustMonoWebAudioNode,
9
+ IFaustPolyWebAudioNode,
10
+ } from "@grame/faustwasm";
11
+ import { FaustUI } from "@shren/faust-ui";
12
+ import {
13
+ faustPromise,
14
+ audioCtx,
15
+ get_mono_generator,
16
+ get_poly_generator,
17
+ compiler,
18
+ getInputDevices,
19
+ deviceUpdateCallbacks,
20
+ accessMIDIDevice,
21
+ midiInputCallback,
22
+ extractMidiAndNvoices,
23
+ default_generator,
24
+ } from "./common";
25
+
26
+ // Create a template for the FaustWidget component
9
27
  const template = document.createElement("template")
10
28
  template.innerHTML = `
11
29
  <div id="root">
@@ -96,20 +114,27 @@ template.innerHTML = `
96
114
  </style>
97
115
  `
98
116
 
117
+ // Define the FaustWidget web component
99
118
  export default class FaustWidget extends HTMLElement {
100
119
  constructor() {
101
120
  super()
102
121
  }
103
122
 
123
+ // Called when the component is connected to the DOM
104
124
  connectedCallback() {
125
+ // Extract the Faust code from the inner HTML
105
126
  const code = this.innerHTML.replace("<!--", "").replace("-->", "").trim()
106
127
  this.attachShadow({ mode: "open" }).appendChild(template.content.cloneNode(true))
107
128
 
129
+ // Query and initialize various elements in the shadow DOM
108
130
  const powerButton = this.shadowRoot!.querySelector("#power") as HTMLButtonElement
109
131
  const faustUIRoot = this.shadowRoot!.querySelector("#faust-ui") as HTMLDivElement
132
+ const audioInputSelector = this.shadowRoot!.querySelector("#audio-input") as HTMLSelectElement
110
133
 
134
+ // Enable the power button once Faust is ready
111
135
  faustPromise.then(() => powerButton.disabled = false)
112
136
 
137
+ // State variables
113
138
  let on = false
114
139
  let gmidi = false
115
140
  let gnvoices = -1
@@ -119,32 +144,37 @@ export default class FaustWidget extends HTMLElement {
119
144
  let generator: FaustMonoDspGenerator | FaustPolyDspGenerator
120
145
  let sourceNode: AudioBufferSourceNode = undefined;
121
146
 
147
+ // Function to setup the Faust environment
122
148
  const setup = async () => {
123
149
  await faustPromise
124
- // Compile Faust code to access JSON metadata
125
- await default_generator.compile(compiler, "main", code, "")
150
+
151
+ // Compile Faust code to access JSON metadata
152
+ await default_generator.compile(compiler, "main", code, "-ftz 2")
126
153
  const json = default_generator.getMeta()
127
154
  let { midi, nvoices } = extractMidiAndNvoices(json);
128
155
  gmidi = midi;
129
156
  gnvoices = nvoices;
130
157
 
131
- // Build the generator and generate UI
132
- generator = nvoices > 0 ? get_poly_generator() : get_mono_generator();
158
+ // Build the generator (possibly reusing default_generator which is a FaustMonoDspGenerator)
159
+ // and generate UI
160
+ generator = nvoices > 0 ? get_poly_generator() : default_generator;
133
161
  await generator.compile(compiler, "main", code, "-ftz 2");
134
162
  const ui = generator.getUI();
135
163
 
164
+ // Generate Faust UI
136
165
  faustUI = new FaustUI({ ui, root: faustUIRoot });
137
166
  faustUIRoot.style.width = faustUI.minWidth * 1.25 + "px";
138
167
  faustUIRoot.style.height = faustUI.minHeight * 1.25 + "px";
139
168
  faustUI.resize();
140
169
  }
141
170
 
171
+ // Function to start the Faust node and audio context
142
172
  const start = async () => {
143
173
  if (audioCtx.state === "suspended") {
144
174
  await audioCtx.resume()
145
175
  }
146
176
 
147
- // Create an audio node from compiled Faust
177
+ // Create an audio node from compiled Faust if not already created
148
178
  if (node === undefined) {
149
179
  if (gnvoices > 0) {
150
180
  node = (await (generator as FaustPolyDspGenerator).createNode(audioCtx, gnvoices))!
@@ -153,7 +183,10 @@ export default class FaustWidget extends HTMLElement {
153
183
  }
154
184
  }
155
185
 
156
- // Access MIDI device
186
+ // Start sensors if available
187
+ await node.startSensors();
188
+
189
+ // Access MIDI device if available
157
190
  if (gmidi) {
158
191
  accessMIDIDevice(midiInputCallback(node))
159
192
  .then(() => {
@@ -164,9 +197,11 @@ export default class FaustWidget extends HTMLElement {
164
197
  });
165
198
  }
166
199
 
200
+ // Set up parameter handling for Faust UI
167
201
  faustUI.paramChangeByUI = (path, value) => node?.setParamValue(path, value)
168
202
  node.setOutputParamHandler((path, value) => faustUI.paramChangeByDSP(path, value))
169
203
 
204
+ // Enable audio input if necessary
170
205
  if (node.numberOfInputs > 0) {
171
206
  audioInputSelector.disabled = false
172
207
  updateInputDevices(await getInputDevices())
@@ -176,15 +211,19 @@ export default class FaustWidget extends HTMLElement {
176
211
  audioInputSelector.innerHTML = "<option>Audio input</option>"
177
212
  }
178
213
 
214
+ // Connect Faust node to the audio context destination
179
215
  node.connect(audioCtx.destination)
180
216
  powerButton.style.color = "#ffa500"
181
217
  }
182
218
 
219
+ // Function to stop the Faust node
183
220
  const stop = () => {
184
221
  node?.disconnect()
222
+ node?.stopSensors();
185
223
  powerButton.style.color = "#fff"
186
224
  }
187
225
 
226
+ // Toggle the Faust node on/off
188
227
  powerButton.onclick = () => {
189
228
  if (on) {
190
229
  stop()
@@ -194,8 +233,7 @@ export default class FaustWidget extends HTMLElement {
194
233
  on = !on
195
234
  }
196
235
 
197
- const audioInputSelector = this.shadowRoot!.querySelector("#audio-input") as HTMLSelectElement
198
-
236
+ // Function to update available audio input devices
199
237
  const updateInputDevices = (devices: MediaDeviceInfo[]) => {
200
238
  if (audioInputSelector.disabled) return
201
239
  while (audioInputSelector.lastChild) audioInputSelector.lastChild.remove()
@@ -208,6 +246,7 @@ export default class FaustWidget extends HTMLElement {
208
246
  }
209
247
  deviceUpdateCallbacks.push(updateInputDevices)
210
248
 
249
+ // Function to connect selected audio input device
211
250
  const connectInput = async () => {
212
251
  const deviceId = audioInputSelector.value
213
252
  const stream = await navigator.mediaDevices.getUserMedia({ audio: { deviceId, echoCancellation: false, noiseSuppression: false, autoGainControl: false } })
@@ -247,9 +286,16 @@ export default class FaustWidget extends HTMLElement {
247
286
  }
248
287
  }
249
288
 
289
+ // Set input change handler
250
290
  audioInputSelector.onchange = connectInput
251
291
 
252
- setup()
292
+ // Initial setup
293
+ setTimeout(() => {
294
+ // Display a "Compiling..." message while Faust is compiling
295
+ faustUIRoot.innerHTML = "<p><center>Compiling...</center></p>";
296
+ setup();
297
+ }, 0);
298
+
253
299
  }
254
300
  }
255
301