@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 +3 -1
- package/src/common.ts +24 -2
- package/src/faust-editor.ts +35 -2
- package/src/faust-widget.ts +37 -23
- package/src/faust-widget copie.ts +0 -212
package/package.json
CHANGED
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
|
-
|
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
|
}
|
package/src/faust-editor.ts
CHANGED
@@ -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
|
-
|
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
|
}
|
package/src/faust-widget.ts
CHANGED
@@ -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
|
-
|
115
|
-
let
|
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
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
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
|
-
|
140
|
-
|
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
|
-
|