@grame/faust-web-component 0.1.1 → 0.2.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/package.json CHANGED
@@ -1,6 +1,8 @@
1
1
  {
2
2
  "name": "@grame/faust-web-component",
3
- "version": "0.1.1",
3
+ "description": "Web component embedding the Faust Compiler",
4
+ "version": "0.2.1",
5
+ "module": "dist/faust-web-component.js",
4
6
  "type": "module",
5
7
  "scripts": {
6
8
  "dev": "vite",
package/src/common.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { FaustCompiler, FaustMonoDspGenerator, FaustPolyDspGenerator, FaustSvgDiagrams, LibFaust, instantiateFaustModuleFromFile } from "@grame/faustwasm"
1
+ import { IFaustMonoWebAudioNode, IFaustPolyWebAudioNode, FaustCompiler, FaustMonoDspGenerator, FaustPolyDspGenerator, FaustSvgDiagrams, LibFaust, instantiateFaustModuleFromFile } from "@grame/faustwasm"
2
2
  import jsURL from "@grame/faustwasm/libfaust-wasm/libfaust-wasm.js?url"
3
3
  import dataURL from "@grame/faustwasm/libfaust-wasm/libfaust-wasm.data?url"
4
4
  import wasmURL from "@grame/faustwasm/libfaust-wasm/libfaust-wasm.wasm?url"
@@ -75,7 +75,8 @@ export async function accessMIDIDevice(
75
75
  });
76
76
  }
77
77
 
78
- export const midiInputCallback = (node: IFaustPolyWebAudioNode | undefined) => {
78
+ // Set up MIDI input callback
79
+ export const midiInputCallback = (node: IFaustMonoWebAudioNode | IFaustPolyWebAudioNode) => {
79
80
  return (data) => {
80
81
 
81
82
  const cmd = data[0] >> 4;
@@ -89,4 +90,25 @@ export const midiInputCallback = (node: IFaustPolyWebAudioNode | undefined) => {
89
90
  else if (cmd === 11) node.ctrlChange(channel, data1, data2);
90
91
  else if (cmd === 14) node.pitchWheel(channel, (data2 * 128.0 + data1));
91
92
  }
93
+ }
94
+
95
+ // Analyze the metadata of a Faust JSON file extract the [midi:on] and [nvoices:n] options
96
+ export function extractMidiAndNvoices(jsonData: JSONData): { midi: boolean, nvoices: number } {
97
+ const optionsMetadata = jsonData.meta.find(meta => meta.options);
98
+ if (optionsMetadata) {
99
+ const options = optionsMetadata.options;
100
+
101
+ const midiRegex = /\[midi:(on|off)\]/;
102
+ const nvoicesRegex = /\[nvoices:(\d+)\]/;
103
+
104
+ const midiMatch = options.match(midiRegex);
105
+ const nvoicesMatch = options.match(nvoicesRegex);
106
+
107
+ const midi = midiMatch ? midiMatch[1] === "on" : false;
108
+ const nvoices = nvoicesMatch ? parseInt(nvoicesMatch[1]) : -1;
109
+
110
+ return { midi, nvoices };
111
+ } else {
112
+ return { midi: false, nvoices: -1 };
113
+ }
92
114
  }
@@ -3,7 +3,7 @@ import { IFaustMonoWebAudioNode } from "@grame/faustwasm"
3
3
  import { FaustUI } from "@shren/faust-ui"
4
4
  import faustCSS from "@shren/faust-ui/dist/esm/index.css?inline"
5
5
  import Split from "split.js"
6
- import { faustPromise, audioCtx, compiler, svgDiagrams, mono_generator, getInputDevices, deviceUpdateCallbacks } from "./common"
6
+ import { faustPromise, audioCtx, compiler, svgDiagrams, mono_generator, poly_generator, getInputDevices, deviceUpdateCallbacks, accessMIDIDevice, midiInputCallback, extractMidiAndNvoices } from "./common"
7
7
  import { createEditor, setError, clearError } from "./editor"
8
8
  import { Scope } from "./scope"
9
9
  import faustSvg from "./faustText.svg"
@@ -265,6 +265,9 @@ export default class FaustEditor extends HTMLElement {
265
265
  let analyser: AnalyserNode | undefined
266
266
  let scope: Scope | undefined
267
267
  let spectrum: Scope | undefined
268
+ let gmidi = false
269
+ let gnvoices = -1
270
+
268
271
 
269
272
  runButton.onclick = async () => {
270
273
  if (audioCtx.state === "suspended") {
@@ -273,8 +276,19 @@ export default class FaustEditor extends HTMLElement {
273
276
  await faustPromise
274
277
  // Compile Faust code
275
278
  const code = editor.state.doc.toString()
279
+ let generator = null
276
280
  try {
281
+ // Compile Faust code to access JSON metadata
277
282
  await mono_generator.compile(compiler, "main", code, "")
283
+ const json = mono_generator.getMeta()
284
+ let { midi, nvoices } = extractMidiAndNvoices(json);
285
+ gmidi = midi;
286
+ gnvoices = nvoices;
287
+
288
+ // Build the generator
289
+ generator = nvoices > 0 ? poly_generator : mono_generator;
290
+ await generator.compile(compiler, "main", code, "");
291
+
278
292
  } catch (e: any) {
279
293
  setError(editor, e)
280
294
  return
@@ -284,7 +298,11 @@ export default class FaustEditor extends HTMLElement {
284
298
 
285
299
  // Create an audio node from compiled Faust
286
300
  if (node !== undefined) node.disconnect()
287
- node = (await mono_generator.createNode(audioCtx))!
301
+ if (gnvoices > 0) {
302
+ node = (await poly_generator.createNode(audioCtx, gnvoices))!
303
+ } else {
304
+ node = (await mono_generator.createNode(audioCtx))!
305
+ }
288
306
  if (node.numberOfInputs > 0) {
289
307
  audioInputSelector.disabled = false
290
308
  updateInputDevices(await getInputDevices())
@@ -298,6 +316,18 @@ export default class FaustEditor extends HTMLElement {
298
316
  for (const tabButton of tabButtons) {
299
317
  tabButton.disabled = false
300
318
  }
319
+
320
+ // Access MIDI device
321
+ if (gmidi) {
322
+ accessMIDIDevice(midiInputCallback(node))
323
+ .then(() => {
324
+ console.log('Successfully connected to the MIDI device.');
325
+ })
326
+ .catch((error) => {
327
+ console.error('Error accessing MIDI device:', error.message);
328
+ });
329
+ }
330
+
301
331
  openSidebar()
302
332
  // Clear old tab contents
303
333
  for (const tab of tabContents) {
@@ -310,13 +340,16 @@ export default class FaustEditor extends HTMLElement {
310
340
  node.connect(analyser)
311
341
  scope = new Scope(tabContents[2])
312
342
  spectrum = new Scope(tabContents[3])
343
+
313
344
  // If there are UI elements, open Faust UI (controls tab); otherwise open spectrum analyzer.
314
345
  const ui = node.getUI()
315
346
  openTab(ui.length > 1 || ui[0].items.length > 0 ? 0 : 3)
347
+
316
348
  // Create controls via Faust UI
317
349
  const faustUI = new FaustUI({ ui, root: faustUIRoot })
318
350
  faustUI.paramChangeByUI = (path, value) => node?.setParamValue(path, value)
319
351
  node.setOutputParamHandler((path, value) => faustUI.paramChangeByDSP(path, value))
352
+
320
353
  // Create SVG block diagram
321
354
  setSVG(svgDiagrams.from("main", code, "")["process.svg"])
322
355
  }
@@ -4,7 +4,7 @@ import faustSvg from "./faustText.svg"
4
4
  import { IFaustMonoWebAudioNode } from "@grame/faustwasm"
5
5
  import { IFaustPolyWebAudioNode } from "@grame/faustwasm"
6
6
  import { FaustUI } from "@shren/faust-ui"
7
- import { faustPromise, audioCtx, mono_generator, poly_generator, compiler, getInputDevices, deviceUpdateCallbacks, accessMIDIDevice, midiInputCallback } from "./common"
7
+ import { faustPromise, audioCtx, mono_generator, poly_generator, compiler, getInputDevices, deviceUpdateCallbacks, accessMIDIDevice, midiInputCallback, extractMidiAndNvoices } from "./common"
8
8
 
9
9
  const template = document.createElement("template")
10
10
  template.innerHTML = `
@@ -111,33 +111,55 @@ export default class FaustWidget extends HTMLElement {
111
111
  faustPromise.then(() => powerButton.disabled = false)
112
112
 
113
113
  let on = false
114
- //let node: IFaustMonoWebAudioNode | undefined
115
- let node: IFaustPolyWebAudioNode | undefined
114
+ let gmidi = false
115
+ let gnvoices = -1
116
+ let node: IFaustMonoWebAudioNode | IFaustPolyWebAudioNode
116
117
  let input: MediaStreamAudioSourceNode | undefined
117
118
  let faustUI: FaustUI
118
119
 
119
120
  const setup = async () => {
120
121
  await faustPromise
121
- // Compile Faust code
122
- //await mono_generator.compile(compiler, "main", code, "")
123
- await poly_generator.compile(compiler, "main", code, "")
124
- // Create controls via Faust UI
125
- //const ui = mono_generator.getUI()
126
- const ui = poly_generator.getUI()
127
- faustUI = new FaustUI({ ui, root: faustUIRoot })
128
- faustUIRoot.style.width = faustUI.minWidth * 1.25 + "px"
129
- faustUIRoot.style.height = faustUI.minHeight * 1.25 + "px"
130
- faustUI.resize()
122
+ // Compile Faust code to access JSON metadata
123
+ await mono_generator.compile(compiler, "main", code, "")
124
+ const json = mono_generator.getMeta()
125
+ let { midi, nvoices } = extractMidiAndNvoices(json);
126
+ gmidi = midi;
127
+ gnvoices = nvoices;
128
+
129
+ // Build the generator and generate UI
130
+ const generator = nvoices > 0 ? poly_generator : mono_generator;
131
+ await generator.compile(compiler, "main", code, "");
132
+ const ui = generator.getUI();
133
+
134
+ faustUI = new FaustUI({ ui, root: faustUIRoot });
135
+ faustUIRoot.style.width = faustUI.minWidth * 1.25 + "px";
136
+ faustUIRoot.style.height = faustUI.minHeight * 1.25 + "px";
137
+ faustUI.resize();
131
138
  }
132
139
 
133
140
  const start = async () => {
134
141
  if (audioCtx.state === "suspended") {
135
142
  await audioCtx.resume()
136
143
  }
144
+
137
145
  // Create an audio node from compiled Faust
138
146
  if (node === undefined) {
139
- //node = (await mono_generator.createNode(audioCtx))!
140
- node = (await poly_generator.createNode(audioCtx, 16))!
147
+ if (gnvoices > 0) {
148
+ node = (await poly_generator.createNode(audioCtx, gnvoices))!
149
+ } else {
150
+ node = (await mono_generator.createNode(audioCtx))!
151
+ }
152
+ }
153
+
154
+ // Access MIDI device
155
+ if (gmidi) {
156
+ accessMIDIDevice(midiInputCallback(node))
157
+ .then(() => {
158
+ console.log('Successfully connected to the MIDI device.');
159
+ })
160
+ .catch((error) => {
161
+ console.error('Error accessing MIDI device:', error.message);
162
+ });
141
163
  }
142
164
 
143
165
  faustUI.paramChangeByUI = (path, value) => node?.setParamValue(path, value)
@@ -152,14 +174,6 @@ export default class FaustWidget extends HTMLElement {
152
174
  audioInputSelector.innerHTML = "<option>Audio input</option>"
153
175
  }
154
176
 
155
- accessMIDIDevice(midiInputCallback(node))
156
- .then(() => {
157
- console.log('Successfully connected to the MIDI device.');
158
- })
159
- .catch((error) => {
160
- console.error('Error accessing MIDI device:', error.message);
161
- });
162
-
163
177
  node.connect(audioCtx.destination)
164
178
  powerButton.style.color = "#ffa500"
165
179
  }
@@ -1,212 +0,0 @@
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 { IFaustMonoWebAudioNode } from "@grame/faustwasm"
5
- import { IFaustPolyWebAudioNode } from "@grame/faustwasm"
6
- import { FaustUI } from "@shren/faust-ui"
7
- import { faustPromise, audioCtx, mono_generator, poly_generator, compiler, getInputDevices, deviceUpdateCallbacks, accessMIDIDevice, midiInputCallback } from "./common"
8
-
9
- const template = document.createElement("template")
10
- template.innerHTML = `
11
- <div id="root">
12
- <div id="controls">
13
- <button title="On/off" class="button" id="power" disabled>${icon({ prefix: "fas", iconName: "power-off" }).html[0]}</button>
14
- <select id="audio-input" class="dropdown" disabled>
15
- <option>Audio input</option>
16
- </select>
17
- <!-- TODO: MIDI input
18
- <select id="midi-input" class="dropdown" disabled>
19
- <option>MIDI input</option>
20
- </select>
21
- -->
22
- <!-- TODO: volume control? <input id="volume" type="range" min="0" max="100"> -->
23
- <a title="Faust website" id="faust" href="https://faust.grame.fr/" target="_blank"><img src="${faustSvg}" height="15px" /></a>
24
- </div>
25
- <div id="faust-ui"></div>
26
- </div>
27
- <style>
28
- #root {
29
- border: 1px solid black;
30
- border-radius: 5px;
31
- box-sizing: border-box;
32
- display: inline-block;
33
- background-color: #384d64;
34
- }
35
-
36
- *, *:before, *:after {
37
- box-sizing: inherit;
38
- }
39
-
40
- #controls {
41
- display: flex;
42
- margin-bottom: -20px;
43
- position: relative;
44
- z-index: 1;
45
- }
46
-
47
- #faust {
48
- margin-left: auto;
49
- padding-left: 10px;
50
- margin-right: 10px;
51
- display: flex;
52
- align-items: center;
53
- }
54
-
55
- a.button {
56
- appearance: button;
57
- }
58
-
59
- .button {
60
- background-color: #384d64;
61
- border: 0;
62
- padding: 5px;
63
- width: 25px;
64
- height: 25px;
65
- color: #fff;
66
- }
67
-
68
- .button:hover {
69
- background-color: #4b71a1;
70
- }
71
-
72
- .button:active {
73
- background-color: #373736;
74
- }
75
-
76
- .button:disabled {
77
- opacity: 0.65;
78
- cursor: not-allowed;
79
- pointer-events: none;
80
- }
81
-
82
- #controls > .button > svg {
83
- width: 15px;
84
- height: 15px;
85
- vertical-align: top;
86
- }
87
-
88
- .dropdown {
89
- height: 19px;
90
- margin: 3px 0 3px 10px;
91
- border: 0;
92
- background: #fff;
93
- }
94
-
95
- ${faustCSS}
96
- </style>
97
- `
98
-
99
- export default class FaustWidget extends HTMLElement {
100
- constructor() {
101
- super()
102
- }
103
-
104
- connectedCallback() {
105
- const code = this.innerHTML.replace("<!--", "").replace("-->", "").trim()
106
- this.attachShadow({ mode: "open" }).appendChild(template.content.cloneNode(true))
107
-
108
- const powerButton = this.shadowRoot!.querySelector("#power") as HTMLButtonElement
109
- const faustUIRoot = this.shadowRoot!.querySelector("#faust-ui") as HTMLDivElement
110
-
111
- faustPromise.then(() => powerButton.disabled = false)
112
-
113
- let on = false
114
- let node: IFaustMonoWebAudioNode | undefined
115
- //let node: IFaustPolyWebAudioNode | undefined
116
- let input: MediaStreamAudioSourceNode | undefined
117
- let faustUI: FaustUI
118
-
119
- const setup = async () => {
120
- await faustPromise
121
- // Compile Faust code
122
- await mono_generator.compile(compiler, "main", code, "")
123
- //await poly_generator.compile(compiler, "main", code, "")
124
- // Create controls via Faust UI
125
- const ui = mono_generator.getUI()
126
- //const ui = poly_generator.getUI()
127
- faustUI = new FaustUI({ ui, root: faustUIRoot })
128
- faustUIRoot.style.width = faustUI.minWidth * 1.25 + "px"
129
- faustUIRoot.style.height = faustUI.minHeight * 1.25 + "px"
130
- faustUI.resize()
131
- }
132
-
133
- const start = async () => {
134
- if (audioCtx.state === "suspended") {
135
- await audioCtx.resume()
136
- }
137
- // Create an audio node from compiled Faust
138
- if (node === undefined) {
139
- node = (await mono_generator.createNode(audioCtx))!
140
- //node = (await poly_generator.createNode(audioCtx, 16))!
141
- }
142
-
143
- faustUI.paramChangeByUI = (path, value) => node?.setParamValue(path, value)
144
- node.setOutputParamHandler((path, value) => faustUI.paramChangeByDSP(path, value))
145
-
146
- if (node.numberOfInputs > 0) {
147
- audioInputSelector.disabled = false
148
- updateInputDevices(await getInputDevices())
149
- await connectInput()
150
- } else {
151
- audioInputSelector.disabled = true
152
- audioInputSelector.innerHTML = "<option>Audio input</option>"
153
- }
154
-
155
- accessMIDIDevice(midiInputCallback(node))
156
- .then(() => {
157
- console.log('Successfully connected to the MIDI device.');
158
- })
159
- .catch((error) => {
160
- console.error('Error accessing MIDI device:', error.message);
161
- });
162
-
163
- node.connect(audioCtx.destination)
164
- powerButton.style.color = "#ffa500"
165
- }
166
-
167
- const stop = () => {
168
- node?.disconnect()
169
- powerButton.style.color = "#fff"
170
- }
171
-
172
- powerButton.onclick = () => {
173
- if (on) {
174
- stop()
175
- } else {
176
- start()
177
- }
178
- on = !on
179
- }
180
-
181
- const audioInputSelector = this.shadowRoot!.querySelector("#audio-input") as HTMLSelectElement
182
-
183
- const updateInputDevices = (devices: MediaDeviceInfo[]) => {
184
- if (audioInputSelector.disabled) return
185
- while (audioInputSelector.lastChild) audioInputSelector.lastChild.remove()
186
- for (const device of devices) {
187
- if (device.kind === "audioinput") {
188
- audioInputSelector.appendChild(new Option(device.label || device.deviceId, device.deviceId))
189
- }
190
- }
191
- }
192
- deviceUpdateCallbacks.push(updateInputDevices)
193
-
194
- const connectInput = async () => {
195
- const deviceId = audioInputSelector.value
196
- const stream = await navigator.mediaDevices.getUserMedia({ audio: { deviceId, echoCancellation: false, noiseSuppression: false, autoGainControl: false } })
197
- if (input) {
198
- input.disconnect()
199
- input = undefined
200
- }
201
- if (node && node.numberOfInputs > 0) {
202
- input = audioCtx.createMediaStreamSource(stream)
203
- input.connect(node!)
204
- }
205
- }
206
-
207
- audioInputSelector.onchange = connectInput
208
-
209
- setup()
210
- }
211
- }
212
-