@grame/faust-web-component 0.4.0 → 0.4.2
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/README.md +2 -2
- package/dist/faust-web-component.js +156 -99
- package/package.json +2 -2
- package/src/faust-editor.ts +60 -18
- package/src/faust-widget.ts +52 -12
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.4.
|
4
|
+
"version": "0.4.2",
|
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.
|
45
|
+
"@grame/faustwasm": "^0.7.3",
|
46
46
|
"@shren/faust-ui": "^1.1.13",
|
47
47
|
"codemirror": "^6.0.1",
|
48
48
|
"split.js": "^1.6.5"
|
package/src/faust-editor.ts
CHANGED
@@ -1,13 +1,28 @@
|
|
1
|
-
|
2
|
-
import {
|
3
|
-
import {
|
4
|
-
import
|
5
|
-
import
|
6
|
-
import
|
7
|
-
import {
|
8
|
-
|
9
|
-
|
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,11 +292,13 @@ 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
|
@@ -293,6 +318,7 @@ export default class FaustEditor extends HTMLElement {
|
|
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
|
-
//
|
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()
|
@@ -356,6 +388,7 @@ export default class FaustEditor extends HTMLElement {
|
|
356
388
|
faustUIRoot.style.height = faustUI.minHeight * 1.25 + "px"
|
357
389
|
}
|
358
390
|
|
391
|
+
// Function to set SVG in the block diagram tab
|
359
392
|
const setSVG = (svgString: string) => {
|
360
393
|
faustDiagram.innerHTML = svgString
|
361
394
|
|
@@ -371,6 +404,7 @@ export default class FaustEditor extends HTMLElement {
|
|
371
404
|
|
372
405
|
let animPlot: number | undefined
|
373
406
|
|
407
|
+
// Function to render the scope
|
374
408
|
const drawScope = () => {
|
375
409
|
scope!.renderScope([{
|
376
410
|
analyser: analyser!,
|
@@ -380,11 +414,13 @@ export default class FaustEditor extends HTMLElement {
|
|
380
414
|
animPlot = requestAnimationFrame(drawScope)
|
381
415
|
}
|
382
416
|
|
417
|
+
// Function to render the spectrum
|
383
418
|
const drawSpectrum = () => {
|
384
419
|
spectrum!.renderSpectrum(analyser!)
|
385
420
|
animPlot = requestAnimationFrame(drawSpectrum)
|
386
421
|
}
|
387
422
|
|
423
|
+
// Function to switch between tabs
|
388
424
|
const openTab = (i: number) => {
|
389
425
|
for (const [j, tab] of tabButtons.entries()) {
|
390
426
|
if (i === j) {
|
@@ -422,22 +458,27 @@ export default class FaustEditor extends HTMLElement {
|
|
422
458
|
}
|
423
459
|
}
|
424
460
|
|
461
|
+
// Attach event listeners to tab buttons
|
425
462
|
for (const [i, tabButton] of tabButtons.entries()) {
|
426
463
|
tabButton.onclick = () => openTab(i)
|
427
464
|
}
|
428
465
|
|
466
|
+
// Event handler for the stop button
|
429
467
|
stopButton.onclick = () => {
|
430
468
|
if (node !== undefined) {
|
431
|
-
node.disconnect()
|
432
|
-
node.
|
433
|
-
node
|
434
|
-
|
469
|
+
node.disconnect();
|
470
|
+
node.stopSensors();
|
471
|
+
node.destroy();
|
472
|
+
node = undefined;
|
473
|
+
stopButton.disabled = true;
|
435
474
|
// TODO: Maybe disable controls in faust-ui tab.
|
436
475
|
}
|
437
476
|
}
|
438
477
|
|
478
|
+
// Audio input selector element
|
439
479
|
const audioInputSelector = this.shadowRoot!.querySelector("#audio-input") as HTMLSelectElement
|
440
480
|
|
481
|
+
// Update the audio input device list
|
441
482
|
const updateInputDevices = (devices: MediaDeviceInfo[]) => {
|
442
483
|
if (audioInputSelector.disabled) return
|
443
484
|
while (audioInputSelector.lastChild) audioInputSelector.lastChild.remove()
|
@@ -450,6 +491,7 @@ export default class FaustEditor extends HTMLElement {
|
|
450
491
|
}
|
451
492
|
deviceUpdateCallbacks.push(updateInputDevices)
|
452
493
|
|
494
|
+
// Connect the selected audio input device
|
453
495
|
const connectInput = async () => {
|
454
496
|
const deviceId = audioInputSelector.value
|
455
497
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: { deviceId, echoCancellation: false, noiseSuppression: false, autoGainControl: false } })
|
package/src/faust-widget.ts
CHANGED
@@ -1,11 +1,29 @@
|
|
1
|
-
|
2
|
-
import
|
3
|
-
import
|
4
|
-
import
|
5
|
-
import {
|
6
|
-
|
7
|
-
|
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,8 +144,10 @@ 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
|
150
|
+
|
124
151
|
// Compile Faust code to access JSON metadata
|
125
152
|
await default_generator.compile(compiler, "main", code, "-ftz 2")
|
126
153
|
const json = default_generator.getMeta()
|
@@ -134,18 +161,20 @@ export default class FaustWidget extends HTMLElement {
|
|
134
161
|
await generator.compile(compiler, "main", code, "-ftz 2");
|
135
162
|
const ui = generator.getUI();
|
136
163
|
|
164
|
+
// Generate Faust UI
|
137
165
|
faustUI = new FaustUI({ ui, root: faustUIRoot });
|
138
166
|
faustUIRoot.style.width = faustUI.minWidth * 1.25 + "px";
|
139
167
|
faustUIRoot.style.height = faustUI.minHeight * 1.25 + "px";
|
140
168
|
faustUI.resize();
|
141
169
|
}
|
142
170
|
|
171
|
+
// Function to start the Faust node and audio context
|
143
172
|
const start = async () => {
|
144
173
|
if (audioCtx.state === "suspended") {
|
145
174
|
await audioCtx.resume()
|
146
175
|
}
|
147
176
|
|
148
|
-
// Create an audio node from compiled Faust
|
177
|
+
// Create an audio node from compiled Faust if not already created
|
149
178
|
if (node === undefined) {
|
150
179
|
if (gnvoices > 0) {
|
151
180
|
node = (await (generator as FaustPolyDspGenerator).createNode(audioCtx, gnvoices))!
|
@@ -154,7 +183,10 @@ export default class FaustWidget extends HTMLElement {
|
|
154
183
|
}
|
155
184
|
}
|
156
185
|
|
157
|
-
//
|
186
|
+
// Start sensors if available
|
187
|
+
await node.startSensors();
|
188
|
+
|
189
|
+
// Access MIDI device if available
|
158
190
|
if (gmidi) {
|
159
191
|
accessMIDIDevice(midiInputCallback(node))
|
160
192
|
.then(() => {
|
@@ -165,9 +197,11 @@ export default class FaustWidget extends HTMLElement {
|
|
165
197
|
});
|
166
198
|
}
|
167
199
|
|
200
|
+
// Set up parameter handling for Faust UI
|
168
201
|
faustUI.paramChangeByUI = (path, value) => node?.setParamValue(path, value)
|
169
202
|
node.setOutputParamHandler((path, value) => faustUI.paramChangeByDSP(path, value))
|
170
203
|
|
204
|
+
// Enable audio input if necessary
|
171
205
|
if (node.numberOfInputs > 0) {
|
172
206
|
audioInputSelector.disabled = false
|
173
207
|
updateInputDevices(await getInputDevices())
|
@@ -177,15 +211,19 @@ export default class FaustWidget extends HTMLElement {
|
|
177
211
|
audioInputSelector.innerHTML = "<option>Audio input</option>"
|
178
212
|
}
|
179
213
|
|
214
|
+
// Connect Faust node to the audio context destination
|
180
215
|
node.connect(audioCtx.destination)
|
181
216
|
powerButton.style.color = "#ffa500"
|
182
217
|
}
|
183
218
|
|
219
|
+
// Function to stop the Faust node
|
184
220
|
const stop = () => {
|
185
221
|
node?.disconnect()
|
222
|
+
node?.stopSensors();
|
186
223
|
powerButton.style.color = "#fff"
|
187
224
|
}
|
188
225
|
|
226
|
+
// Toggle the Faust node on/off
|
189
227
|
powerButton.onclick = () => {
|
190
228
|
if (on) {
|
191
229
|
stop()
|
@@ -195,8 +233,7 @@ export default class FaustWidget extends HTMLElement {
|
|
195
233
|
on = !on
|
196
234
|
}
|
197
235
|
|
198
|
-
|
199
|
-
|
236
|
+
// Function to update available audio input devices
|
200
237
|
const updateInputDevices = (devices: MediaDeviceInfo[]) => {
|
201
238
|
if (audioInputSelector.disabled) return
|
202
239
|
while (audioInputSelector.lastChild) audioInputSelector.lastChild.remove()
|
@@ -209,6 +246,7 @@ export default class FaustWidget extends HTMLElement {
|
|
209
246
|
}
|
210
247
|
deviceUpdateCallbacks.push(updateInputDevices)
|
211
248
|
|
249
|
+
// Function to connect selected audio input device
|
212
250
|
const connectInput = async () => {
|
213
251
|
const deviceId = audioInputSelector.value
|
214
252
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: { deviceId, echoCancellation: false, noiseSuppression: false, autoGainControl: false } })
|
@@ -248,8 +286,10 @@ export default class FaustWidget extends HTMLElement {
|
|
248
286
|
}
|
249
287
|
}
|
250
288
|
|
289
|
+
// Set input change handler
|
251
290
|
audioInputSelector.onchange = connectInput
|
252
291
|
|
292
|
+
// Initial setup
|
253
293
|
setTimeout(() => {
|
254
294
|
// Display a "Compiling..." message while Faust is compiling
|
255
295
|
faustUIRoot.innerHTML = "<p><center>Compiling...</center></p>";
|