@grame/faustwasm 0.0.49 → 0.0.51
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 +18 -3
- package/assets/standalone/index-poly.js +215 -0
- package/assets/standalone/index.html +12 -1
- package/assets/standalone/index.js +19 -19
- package/assets/standalone/template-poly.html +3 -3
- package/assets/standalone/template.html +2 -2
- package/assets/standalone/template.js +17 -17
- package/fileutils.js +31 -4
- package/package.json +1 -1
- package/scripts/faust2wasm.js +5 -3
- package/src/copyWebStandaloneAssets.d.ts +3 -3
- package/src/copyWebStandaloneAssets.js +30 -15
- package/src/faust2wasmFiles.js +7 -4
- package/test/midi.dsp +8 -0
- package/test/organ1.dsp +1 -1
package/README.md
CHANGED
|
@@ -40,20 +40,36 @@ You'll have to raise the package version number in `package.json` before `npm ru
|
|
|
40
40
|
#### Generate WebAssembly version of a Faust DSP
|
|
41
41
|
|
|
42
42
|
For example:
|
|
43
|
+
|
|
43
44
|
```bash
|
|
44
45
|
rm -rf test/out # make sure you are under the faustwasm directory.
|
|
45
46
|
node scripts/faust2wasm.js test/mono.dsp test/out
|
|
46
47
|
```
|
|
47
|
-
|
|
48
|
+
will create a set of files: `mono.js`, `mono.wasm`, `mono.json`, `mono.html` and the `faustwasm` folder.
|
|
49
|
+
|
|
50
|
+
Polyphonic instrument with:
|
|
51
|
+
|
|
48
52
|
```bash
|
|
49
53
|
rm -rf test/out # make sure you are under the faustwasm directory.
|
|
50
54
|
node scripts/faust2wasm.js test/poly.dsp test/out -poly
|
|
51
55
|
```
|
|
52
|
-
|
|
56
|
+
will create a set of files: `poly.js`, `poly.wasm`, `poly.json`, `poly.html` (and possibly `poly_effect.wasm`, `poly_effect.json`) and the `faustwasm` folder.
|
|
57
|
+
|
|
58
|
+
You can create a standalone DSP on a web page using the same command line:
|
|
59
|
+
|
|
53
60
|
```bash
|
|
54
61
|
rm -rf test/out # make sure you are under the faustwasm directory.
|
|
55
62
|
node scripts/faust2wasm.js test/rev.dsp test/out -standalone
|
|
56
63
|
```
|
|
64
|
+
will create a set of files: `rev.js`, `rev.wasm`, `rev.json`, `rev.html`, and the `faustwasm`, `faust-ui` folders.
|
|
65
|
+
|
|
66
|
+
Or a standalone polyphonic DSP on a web page with:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
rm -rf test/out # make sure you are under the faustwasm directory.
|
|
70
|
+
node scripts/faust2wasm.js test/organ1.dsp test/out -standalone
|
|
71
|
+
```
|
|
72
|
+
will create a set of files: `organ1.js`, `organ1.wasm`, `organ1.json`, `organ1_effect.wasm`, `organ1_effect.json`, `organ1.html`, and the `faustwasm`, `faust-ui` folders.
|
|
57
73
|
|
|
58
74
|
#### Generate SVG Diagrams of a Faust DSP
|
|
59
75
|
|
|
@@ -223,7 +239,6 @@ The package is used in the following projects:
|
|
|
223
239
|
- [Misc. services](#misc)
|
|
224
240
|
- [Important note](#note)
|
|
225
241
|
|
|
226
|
-
|
|
227
242
|
### Organisation of the API <a name="org"></a>
|
|
228
243
|
|
|
229
244
|
The API is organised from low to high level as illustrated by the figure below.
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import("./types").FaustDspDistribution} FaustDspDistribution
|
|
3
|
+
* @typedef {import("./faustwasm").FaustAudioWorkletNode} FaustAudioWorkletNode
|
|
4
|
+
* @typedef {import("./faustwasm").FaustDspMeta} FaustDspMeta
|
|
5
|
+
* @typedef {import("./faustwasm").FaustUIDescriptor} FaustUIDescriptor
|
|
6
|
+
* @typedef {import("./faustwasm").FaustUIGroup} FaustUIGroup
|
|
7
|
+
* @typedef {import("./faustwasm").FaustUIItem} FaustUIItem
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** @type {HTMLSpanElement} */
|
|
11
|
+
const $spanAudioInput = document.getElementById("audio-input");
|
|
12
|
+
/** @type {HTMLSpanElement} */
|
|
13
|
+
const $spanMidiInput = document.getElementById("midi-input");
|
|
14
|
+
/** @type {HTMLSelectElement} */
|
|
15
|
+
const $selectAudioInput = document.getElementById("select-audio-input");
|
|
16
|
+
/** @type {HTMLSelectElement} */
|
|
17
|
+
const $selectMidiInput = document.getElementById("select-midi-input");
|
|
18
|
+
/** @type {HTMLSelectElement} */
|
|
19
|
+
const $buttonDsp = document.getElementById("button-dsp");
|
|
20
|
+
/** @type {HTMLDivElement} */
|
|
21
|
+
const $divFaustUI = document.getElementById("div-faust-ui");
|
|
22
|
+
|
|
23
|
+
/** @type {typeof AudioContext} */
|
|
24
|
+
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
|
25
|
+
const audioContext = new AudioCtx();
|
|
26
|
+
audioContext.suspend();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {FaustAudioWorkletNode} faustNode
|
|
30
|
+
*/
|
|
31
|
+
const buildAudioDeviceMenu = async (faustNode) => {
|
|
32
|
+
/** @type {MediaStreamAudioSourceNode} */
|
|
33
|
+
let inputStreamNode;
|
|
34
|
+
const handleDeviceChange = async () => {
|
|
35
|
+
const devicesInfo = await navigator.mediaDevices.enumerateDevices();
|
|
36
|
+
$selectAudioInput.innerHTML = '';
|
|
37
|
+
devicesInfo.forEach((deviceInfo, i) => {
|
|
38
|
+
const { kind, deviceId, label } = deviceInfo;
|
|
39
|
+
if (kind === "audioinput") {
|
|
40
|
+
const option = new Option(label || `microphone ${i + 1}`, deviceId);
|
|
41
|
+
$selectAudioInput.add(option);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
await handleDeviceChange();
|
|
46
|
+
navigator.mediaDevices.addEventListener("devicechange", handleDeviceChange)
|
|
47
|
+
$selectAudioInput.onchange = async () => {
|
|
48
|
+
const id = $selectAudioInput.value;
|
|
49
|
+
const constraints = {
|
|
50
|
+
audio: {
|
|
51
|
+
echoCancellation: false,
|
|
52
|
+
mozNoiseSuppression: false,
|
|
53
|
+
mozAutoGainControl: false,
|
|
54
|
+
deviceId: id ? { exact: id } : undefined,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
58
|
+
if (inputStreamNode) inputStreamNode.disconnect();
|
|
59
|
+
inputStreamNode = audioContext.createMediaStreamSource(stream);
|
|
60
|
+
inputStreamNode.connect(faustNode);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const defaultConstraints = {
|
|
64
|
+
audio: {
|
|
65
|
+
echoCancellation: false,
|
|
66
|
+
mozNoiseSuppression: false,
|
|
67
|
+
mozAutoGainControl: false
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const defaultStream = await navigator.mediaDevices.getUserMedia(defaultConstraints);
|
|
71
|
+
if (defaultStream) {
|
|
72
|
+
inputStreamNode = audioContext.createMediaStreamSource(defaultStream);
|
|
73
|
+
inputStreamNode.connect(faustNode);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param {FaustAudioWorkletNode} faustNode
|
|
79
|
+
*/
|
|
80
|
+
const buildMidiDeviceMenu = async (faustNode) => {
|
|
81
|
+
const midiAccess = await navigator.requestMIDIAccess();
|
|
82
|
+
/** @type {WebMidi.MIDIInput} */
|
|
83
|
+
let currentInput;
|
|
84
|
+
/**
|
|
85
|
+
* @param {WebMidi.MIDIMessageEvent} e
|
|
86
|
+
*/
|
|
87
|
+
const handleMidiMessage = e => faustNode.midiMessage(e.data);
|
|
88
|
+
const handleStateChange = () => {
|
|
89
|
+
const { inputs } = midiAccess;
|
|
90
|
+
if ($selectMidiInput.options.length === inputs.size + 1) return;
|
|
91
|
+
if (currentInput) currentInput.removeEventListener("midimessage", handleMidiMessage);
|
|
92
|
+
$selectMidiInput.innerHTML = '<option value="-1" disabled selected>Select...</option>';
|
|
93
|
+
inputs.forEach((midiInput) => {
|
|
94
|
+
const { name, id } = midiInput;
|
|
95
|
+
const option = new Option(name, id);
|
|
96
|
+
$selectMidiInput.add(option);
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
handleStateChange();
|
|
100
|
+
midiAccess.addEventListener("statechange", handleStateChange);
|
|
101
|
+
$selectMidiInput.onchange = () => {
|
|
102
|
+
if (currentInput) currentInput.removeEventListener("midimessage", handleMidiMessage);
|
|
103
|
+
const id = $selectMidiInput.value;
|
|
104
|
+
currentInput = midiAccess.inputs.get(id);
|
|
105
|
+
currentInput.addEventListener("midimessage", handleMidiMessage);
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
$buttonDsp.disabled = true;
|
|
110
|
+
$buttonDsp.onclick = () => {
|
|
111
|
+
if (audioContext.state === "running") {
|
|
112
|
+
$buttonDsp.textContent = "Suspended";
|
|
113
|
+
audioContext.suspend();
|
|
114
|
+
} else if (audioContext.state === "suspended") {
|
|
115
|
+
$buttonDsp.textContent = "Running";
|
|
116
|
+
audioContext.resume();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Creates a Faust audio node for use in the Web Audio API.
|
|
122
|
+
*
|
|
123
|
+
* @param {AudioContext} audioContext - The Web Audio API AudioContext to which the Faust audio node will be connected.
|
|
124
|
+
* @param {string} dspName - The name of the DSP to be loaded.
|
|
125
|
+
* @param {number} voices - The number of voices to be used for polyphonic DSPs.
|
|
126
|
+
* @returns {Object} - An object containing the Faust audio node and the DSP metadata.
|
|
127
|
+
*/
|
|
128
|
+
const createFaustNode = async (audioContext, dspName = "template", voices = 0) => {
|
|
129
|
+
// Import necessary Faust modules and data
|
|
130
|
+
const { FaustMonoDspGenerator, FaustPolyDspGenerator } = await import("./faustwasm/index.js");
|
|
131
|
+
|
|
132
|
+
// Load DSP metadata from JSON
|
|
133
|
+
/** @type {FaustDspMeta} */
|
|
134
|
+
const dspMeta = await (await fetch(`./${dspName}.json`)).json();
|
|
135
|
+
|
|
136
|
+
// Compile the DSP module from WebAssembly binary data
|
|
137
|
+
const dspModule = await WebAssembly.compileStreaming(await fetch(`./${dspName}.wasm`));
|
|
138
|
+
|
|
139
|
+
// Create an object representing Faust DSP with metadata and module
|
|
140
|
+
/** @type {FaustDspDistribution} */
|
|
141
|
+
const faustDsp = { dspMeta, dspModule };
|
|
142
|
+
|
|
143
|
+
/** @type {FaustAudioWorkletNode} */
|
|
144
|
+
let faustNode;
|
|
145
|
+
|
|
146
|
+
// Create either a polyphonic or monophonic Faust audio node based on the number of voices
|
|
147
|
+
if (voices > 0) {
|
|
148
|
+
|
|
149
|
+
// Try to load optional mixer and effect modules
|
|
150
|
+
try {
|
|
151
|
+
faustDsp.mixerModule = await WebAssembly.compileStreaming(await fetch("./mixerModule.wasm"));
|
|
152
|
+
faustDsp.effectMeta = await (await fetch(`./${dspName}_effect.json`)).json();
|
|
153
|
+
faustDsp.effectModule = await WebAssembly.compileStreaming(await fetch(`./${dspName}_effect.wasm`));
|
|
154
|
+
} catch (e) { }
|
|
155
|
+
|
|
156
|
+
const generator = new FaustPolyDspGenerator();
|
|
157
|
+
faustNode = await generator.createNode(
|
|
158
|
+
audioContext,
|
|
159
|
+
voices,
|
|
160
|
+
"FaustPolyDSP",
|
|
161
|
+
{ module: faustDsp.dspModule, json: JSON.stringify(faustDsp.dspMeta) },
|
|
162
|
+
faustDsp.mixerModule,
|
|
163
|
+
faustDsp.effectModule ? { module: faustDsp.effectModule, json: JSON.stringify(faustDsp.effectMeta) } : undefined
|
|
164
|
+
);
|
|
165
|
+
} else {
|
|
166
|
+
const generator = new FaustMonoDspGenerator();
|
|
167
|
+
faustNode = await generator.createNode(
|
|
168
|
+
audioContext,
|
|
169
|
+
"FaustMonoDSP",
|
|
170
|
+
{ module: faustDsp.dspModule, json: JSON.stringify(faustDsp.dspMeta) }
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Return an object with the Faust audio node and the DSP metadata
|
|
175
|
+
return { faustNode, dspMeta };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @param {FaustAudioWorkletNode} faustNode
|
|
180
|
+
*/
|
|
181
|
+
const createFaustUI = async (faustNode) => {
|
|
182
|
+
const { FaustUI } = await import("./faust-ui/index.js");
|
|
183
|
+
const $container = document.createElement("div");
|
|
184
|
+
$container.style.margin = "0";
|
|
185
|
+
$container.style.position = "absolute";
|
|
186
|
+
$container.style.overflow = "auto";
|
|
187
|
+
$container.style.display = "flex";
|
|
188
|
+
$container.style.flexDirection = "column";
|
|
189
|
+
$container.style.width = "100%";
|
|
190
|
+
$container.style.height = "100%";
|
|
191
|
+
$divFaustUI.appendChild($container);
|
|
192
|
+
const faustUI = new FaustUI({
|
|
193
|
+
ui: faustNode.getUI(),
|
|
194
|
+
root: $container,
|
|
195
|
+
listenWindowMessage: false,
|
|
196
|
+
listenWindowResize: true,
|
|
197
|
+
});
|
|
198
|
+
faustUI.paramChangeByUI = (path, value) => faustNode.setParamValue(path, value);
|
|
199
|
+
faustNode.setOutputParamHandler((path, value) => faustUI.paramChangeByDSP(path, value));
|
|
200
|
+
$container.style.minWidth = `${faustUI.minWidth}px`;
|
|
201
|
+
$container.style.minHeight = `${faustUI.minHeight}px`;
|
|
202
|
+
faustUI.resize();
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
(async () => {
|
|
206
|
+
const { faustNode, dspMeta: { name } } = await createFaustNode(audioContext, "FAUST_DSP", 16);
|
|
207
|
+
await createFaustUI(faustNode);
|
|
208
|
+
faustNode.connect(audioContext.destination);
|
|
209
|
+
if (faustNode.numberOfInputs) await buildAudioDeviceMenu(faustNode);
|
|
210
|
+
else $spanAudioInput.hidden = true;
|
|
211
|
+
if (navigator.requestMIDIAccess) await buildMidiDeviceMenu(faustNode);
|
|
212
|
+
else $spanMidiInput.hidden = true;
|
|
213
|
+
$buttonDsp.disabled = false;
|
|
214
|
+
document.title = name;
|
|
215
|
+
})();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
<html lang="en">
|
|
3
|
+
|
|
3
4
|
<head>
|
|
4
5
|
<meta charset="UTF-8">
|
|
5
6
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
@@ -17,32 +18,40 @@
|
|
|
17
18
|
margin: 0;
|
|
18
19
|
position: absolute;
|
|
19
20
|
}
|
|
21
|
+
|
|
20
22
|
main {
|
|
21
23
|
margin: 20px;
|
|
22
24
|
display: flex;
|
|
23
25
|
flex-direction: column;
|
|
24
26
|
flex: 1 1 100%;
|
|
25
27
|
}
|
|
28
|
+
|
|
26
29
|
main span {
|
|
27
30
|
position: relative;
|
|
28
31
|
display: flex;
|
|
29
32
|
}
|
|
33
|
+
|
|
30
34
|
main span[hidden] {
|
|
31
35
|
display: none;
|
|
32
36
|
}
|
|
37
|
+
|
|
33
38
|
main span span {
|
|
34
39
|
margin: 0px 10px;
|
|
35
40
|
flex: 1;
|
|
36
41
|
}
|
|
42
|
+
|
|
37
43
|
main span select {
|
|
38
44
|
flex: 2;
|
|
39
45
|
}
|
|
46
|
+
|
|
40
47
|
#dsp-switch {
|
|
41
48
|
margin: 20px auto;
|
|
42
49
|
}
|
|
50
|
+
|
|
43
51
|
#button-dsp {
|
|
44
52
|
padding: 10px;
|
|
45
53
|
}
|
|
54
|
+
|
|
46
55
|
#div-faust-ui {
|
|
47
56
|
flex: 1 1 auto;
|
|
48
57
|
display: flex;
|
|
@@ -52,6 +61,7 @@
|
|
|
52
61
|
</style>
|
|
53
62
|
<link rel="stylesheet" href="./faust-ui/index.css">
|
|
54
63
|
</head>
|
|
64
|
+
|
|
55
65
|
<body>
|
|
56
66
|
<main>
|
|
57
67
|
<span id="audio-input">
|
|
@@ -68,6 +78,7 @@
|
|
|
68
78
|
<div id="div-faust-ui">
|
|
69
79
|
</div>
|
|
70
80
|
</main>
|
|
71
|
-
<script src="./
|
|
81
|
+
<script src="./FAUST_DSP.js"></script>
|
|
72
82
|
</body>
|
|
83
|
+
|
|
73
84
|
</html>
|
|
@@ -121,38 +121,38 @@ $buttonDsp.onclick = () => {
|
|
|
121
121
|
* Creates a Faust audio node for use in the Web Audio API.
|
|
122
122
|
*
|
|
123
123
|
* @param {AudioContext} audioContext - The Web Audio API AudioContext to which the Faust audio node will be connected.
|
|
124
|
-
* @
|
|
124
|
+
* @param {string} dspName - The name of the DSP to be loaded.
|
|
125
|
+
* @param {number} voices - The number of voices to be used for polyphonic DSPs.
|
|
126
|
+
* @returns {Object} - An object containing the Faust audio node and the DSP metadata.
|
|
125
127
|
*/
|
|
126
|
-
const createFaustNode = async (audioContext) => {
|
|
128
|
+
const createFaustNode = async (audioContext, dspName = "template", voices = 0) => {
|
|
127
129
|
// Import necessary Faust modules and data
|
|
128
130
|
const { FaustMonoDspGenerator, FaustPolyDspGenerator } = await import("./faustwasm/index.js");
|
|
129
131
|
|
|
130
132
|
// Load DSP metadata from JSON
|
|
131
133
|
/** @type {FaustDspMeta} */
|
|
132
|
-
const dspMeta = await (await fetch(
|
|
134
|
+
const dspMeta = await (await fetch(`./${dspName}.json`)).json();
|
|
133
135
|
|
|
134
136
|
// Compile the DSP module from WebAssembly binary data
|
|
135
|
-
const dspModule = await WebAssembly.compileStreaming(await fetch(
|
|
137
|
+
const dspModule = await WebAssembly.compileStreaming(await fetch(`./${dspName}.wasm`));
|
|
136
138
|
|
|
137
139
|
// Create an object representing Faust DSP with metadata and module
|
|
138
140
|
/** @type {FaustDspDistribution} */
|
|
139
141
|
const faustDsp = { dspMeta, dspModule };
|
|
140
142
|
|
|
141
|
-
// Try to load optional mixer and effect modules
|
|
142
|
-
try {
|
|
143
|
-
faustDsp.mixerModule = await WebAssembly.compileStreaming(await fetch("./mixerModule.wasm"));
|
|
144
|
-
faustDsp.effectMeta = await (await fetch("./effectMeta.json")).json();
|
|
145
|
-
faustDsp.effectModule = await WebAssembly.compileStreaming(await fetch("./effectModule.wasm"));
|
|
146
|
-
} catch (e) { }
|
|
147
|
-
|
|
148
|
-
// Determine the number of voices based on the mixer module
|
|
149
|
-
const voices = faustDsp.mixerModule ? 64 : 0;
|
|
150
|
-
|
|
151
143
|
/** @type {FaustAudioWorkletNode} */
|
|
152
144
|
let faustNode;
|
|
153
145
|
|
|
154
146
|
// Create either a polyphonic or monophonic Faust audio node based on the number of voices
|
|
155
|
-
if (voices) {
|
|
147
|
+
if (voices > 0) {
|
|
148
|
+
|
|
149
|
+
// Try to load optional mixer and effect modules
|
|
150
|
+
try {
|
|
151
|
+
faustDsp.mixerModule = await WebAssembly.compileStreaming(await fetch("./mixerModule.wasm"));
|
|
152
|
+
faustDsp.effectMeta = await (await fetch(`./${dspName}_effect.json`)).json();
|
|
153
|
+
faustDsp.effectModule = await WebAssembly.compileStreaming(await fetch(`./${dspName}_effect.wasm`));
|
|
154
|
+
} catch (e) { }
|
|
155
|
+
|
|
156
156
|
const generator = new FaustPolyDspGenerator();
|
|
157
157
|
faustNode = await generator.createNode(
|
|
158
158
|
audioContext,
|
|
@@ -171,8 +171,8 @@ const createFaustNode = async (audioContext) => {
|
|
|
171
171
|
);
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
// Return an object with the Faust audio node
|
|
175
|
-
return { faustNode,
|
|
174
|
+
// Return an object with the Faust audio node and the DSP metadata
|
|
175
|
+
return { faustNode, dspMeta };
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
/**
|
|
@@ -203,12 +203,12 @@ const createFaustUI = async (faustNode) => {
|
|
|
203
203
|
};
|
|
204
204
|
|
|
205
205
|
(async () => {
|
|
206
|
-
const { faustNode,
|
|
206
|
+
const { faustNode, dspMeta: { name } } = await createFaustNode(audioContext, "FAUST_DSP");
|
|
207
207
|
await createFaustUI(faustNode);
|
|
208
208
|
faustNode.connect(audioContext.destination);
|
|
209
209
|
if (faustNode.numberOfInputs) await buildAudioDeviceMenu(faustNode);
|
|
210
210
|
else $spanAudioInput.hidden = true;
|
|
211
|
-
if (
|
|
211
|
+
if (navigator.requestMIDIAccess) await buildMidiDeviceMenu(faustNode);
|
|
212
212
|
else $spanMidiInput.hidden = true;
|
|
213
213
|
$buttonDsp.disabled = false;
|
|
214
214
|
document.title = name;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<center><button id="button-dsp">Activate DSP</button></center>
|
|
12
12
|
</span>
|
|
13
13
|
</main>
|
|
14
|
-
<script src="./
|
|
14
|
+
<script src="./FAUST_DSP.js"></script>
|
|
15
15
|
<script>
|
|
16
16
|
(async () => {
|
|
17
17
|
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// Create audio context activation button
|
|
24
24
|
const $buttonDsp = document.getElementById("button-dsp");
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
// Play some notes in loop
|
|
27
27
|
function play(node) {
|
|
28
28
|
node.keyOn(0, 60, 100);
|
|
29
29
|
setTimeout(() => node.keyOn(0, 64, 100), 1000);
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// Create Faust node
|
|
49
|
-
const { faustNode,
|
|
49
|
+
const { faustNode, dspMeta: { name } } = await createFaustNode(audioContext, "FAUST_DSP", 16);
|
|
50
50
|
|
|
51
51
|
// Connect Faust node to audio context
|
|
52
52
|
faustNode.connect(audioContext.destination);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<center><button id="button-dsp">Activate DSP</button></center>
|
|
12
12
|
</span>
|
|
13
13
|
</main>
|
|
14
|
-
<script src="./
|
|
14
|
+
<script src="./FAUST_DSP.js"></script>
|
|
15
15
|
<script>
|
|
16
16
|
(async () => {
|
|
17
17
|
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// Create Faust node
|
|
39
|
-
const { faustNode,
|
|
39
|
+
const { faustNode, dspMeta: { name } } = await createFaustNode(audioContext, "FAUST_DSP");
|
|
40
40
|
|
|
41
41
|
// Connect Faust node to audio context
|
|
42
42
|
faustNode.connect(audioContext.destination);
|
|
@@ -8,38 +8,38 @@
|
|
|
8
8
|
* Creates a Faust audio node for use in the Web Audio API.
|
|
9
9
|
*
|
|
10
10
|
* @param {AudioContext} audioContext - The Web Audio API AudioContext to which the Faust audio node will be connected.
|
|
11
|
-
* @
|
|
11
|
+
* @param {string} dspName - The name of the DSP to be loaded.
|
|
12
|
+
* @param {number} voices - The number of voices to be used for polyphonic DSPs.
|
|
13
|
+
* @returns {Object} - An object containing the Faust audio node and the DSP metadata.
|
|
12
14
|
*/
|
|
13
|
-
const createFaustNode = async (audioContext) => {
|
|
15
|
+
const createFaustNode = async (audioContext, dspName = "template", voices = 0) => {
|
|
14
16
|
// Import necessary Faust modules and data
|
|
15
17
|
const { FaustMonoDspGenerator, FaustPolyDspGenerator } = await import("./faustwasm/index.js");
|
|
16
18
|
|
|
17
19
|
// Load DSP metadata from JSON
|
|
18
20
|
/** @type {FaustDspMeta} */
|
|
19
|
-
const dspMeta = await (await fetch(
|
|
21
|
+
const dspMeta = await (await fetch(`./${dspName}.json`)).json();
|
|
20
22
|
|
|
21
23
|
// Compile the DSP module from WebAssembly binary data
|
|
22
|
-
const dspModule = await WebAssembly.compileStreaming(await fetch(
|
|
24
|
+
const dspModule = await WebAssembly.compileStreaming(await fetch(`./${dspName}.wasm`));
|
|
23
25
|
|
|
24
26
|
// Create an object representing Faust DSP with metadata and module
|
|
25
27
|
/** @type {FaustDspDistribution} */
|
|
26
28
|
const faustDsp = { dspMeta, dspModule };
|
|
27
29
|
|
|
28
|
-
// Try to load optional mixer and effect modules
|
|
29
|
-
try {
|
|
30
|
-
faustDsp.mixerModule = await WebAssembly.compileStreaming(await fetch("./mixerModule.wasm"));
|
|
31
|
-
faustDsp.effectMeta = await (await fetch("./effectMeta.json")).json();
|
|
32
|
-
faustDsp.effectModule = await WebAssembly.compileStreaming(await fetch("./effectModule.wasm"));
|
|
33
|
-
} catch (e) { }
|
|
34
|
-
|
|
35
|
-
// Determine the number of voices based on the mixer module
|
|
36
|
-
const voices = faustDsp.mixerModule ? 64 : 0;
|
|
37
|
-
|
|
38
30
|
/** @type {FaustAudioWorkletNode} */
|
|
39
31
|
let faustNode;
|
|
40
32
|
|
|
41
33
|
// Create either a polyphonic or monophonic Faust audio node based on the number of voices
|
|
42
|
-
if (voices) {
|
|
34
|
+
if (voices > 0) {
|
|
35
|
+
|
|
36
|
+
// Try to load optional mixer and effect modules
|
|
37
|
+
try {
|
|
38
|
+
faustDsp.mixerModule = await WebAssembly.compileStreaming(await fetch("./mixerModule.wasm"));
|
|
39
|
+
faustDsp.effectMeta = await (await fetch(`./${dspName}_effect.json`)).json();
|
|
40
|
+
faustDsp.effectModule = await WebAssembly.compileStreaming(await fetch(`./${dspName}_effect.wasm`));
|
|
41
|
+
} catch (e) { }
|
|
42
|
+
|
|
43
43
|
const generator = new FaustPolyDspGenerator();
|
|
44
44
|
faustNode = await generator.createNode(
|
|
45
45
|
audioContext,
|
|
@@ -58,6 +58,6 @@ const createFaustNode = async (audioContext) => {
|
|
|
58
58
|
);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
// Return an object with the Faust audio node
|
|
62
|
-
return { faustNode,
|
|
61
|
+
// Return an object with the Faust audio node and the DSP metadata
|
|
62
|
+
return { faustNode, dspMeta };
|
|
63
63
|
}
|
package/fileutils.js
CHANGED
|
@@ -11,8 +11,8 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* @param {string} src
|
|
15
|
-
* @param {string} dest
|
|
14
|
+
* @param {string} src - The source path.
|
|
15
|
+
* @param {string} dest - The destination path.
|
|
16
16
|
*/
|
|
17
17
|
const cpSync = (src, dest) => {
|
|
18
18
|
if (!fs.existsSync(src)) return;
|
|
@@ -25,7 +25,34 @@ const cpSync = (src, dest) => {
|
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
* @param {string}
|
|
28
|
+
* @param {string} src - The source path of the file to modify and copy.
|
|
29
|
+
* @param {string} dest - The destination path.
|
|
30
|
+
* @param {string} find - The string to find.
|
|
31
|
+
* @param {string} replace - The string to replace.
|
|
32
|
+
*/
|
|
33
|
+
const cpSyncModify = (src, dest, find, replace) => {
|
|
34
|
+
fs.readFile(src, 'utf8', function (err, data) {
|
|
35
|
+
if (err) {
|
|
36
|
+
console.error(err);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Create a regular expression from the 'find' string
|
|
41
|
+
const regex = new RegExp(find, 'g');
|
|
42
|
+
|
|
43
|
+
// Replace 'find' with 'replace'
|
|
44
|
+
let modifiedData = data.replace(regex, replace);
|
|
45
|
+
|
|
46
|
+
// Write the modified data to a new file
|
|
47
|
+
fs.writeFile(dest, modifiedData, 'utf8', function (err) {
|
|
48
|
+
if (err) {
|
|
49
|
+
console.error(err);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} dir - The directory to remove.
|
|
29
56
|
*/
|
|
30
57
|
const rmSync = (dir) => {
|
|
31
58
|
if (!fs.existsSync(dir)) return;
|
|
@@ -37,4 +64,4 @@ const rmSync = (dir) => {
|
|
|
37
64
|
}
|
|
38
65
|
};
|
|
39
66
|
|
|
40
|
-
export { cpSync, rmSync };
|
|
67
|
+
export { cpSync, cpSyncModify, rmSync };
|
package/package.json
CHANGED
package/scripts/faust2wasm.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//@ts-check
|
|
3
3
|
import * as process from "process";
|
|
4
4
|
import faust2wasmFiles from "../src/faust2wasmFiles.js";
|
|
5
|
-
import { copyWebStandaloneAssets,
|
|
5
|
+
import { copyWebStandaloneAssets, copyWebTemplateAssets } from "../src/copyWebStandaloneAssets.js";
|
|
6
6
|
|
|
7
7
|
const argv = process.argv.slice(2);
|
|
8
8
|
|
|
@@ -23,12 +23,14 @@ const standalone = $standalone !== -1;
|
|
|
23
23
|
if (standalone) argv.splice($standalone, 1);
|
|
24
24
|
|
|
25
25
|
const [inputFile, outputDir, ...argvFaust] = argv;
|
|
26
|
+
const fileName = inputFile.split('/').pop();
|
|
27
|
+
const dspName = fileName.replace(/\.dsp$/, '');
|
|
26
28
|
|
|
27
29
|
(async () => {
|
|
28
30
|
await faust2wasmFiles(inputFile, outputDir, argvFaust, poly);
|
|
29
31
|
if (standalone) {
|
|
30
|
-
copyWebStandaloneAssets(outputDir);
|
|
32
|
+
copyWebStandaloneAssets(outputDir, dspName, poly);
|
|
31
33
|
} else {
|
|
32
|
-
|
|
34
|
+
copyWebTemplateAssets(outputDir, dspName, poly);
|
|
33
35
|
}
|
|
34
36
|
})();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
declare const copyWebStandaloneAssets: (outputDir: string) => void;
|
|
2
|
-
declare const
|
|
1
|
+
declare const copyWebStandaloneAssets: (outputDir: string, dspName: string, poly: boolean) => void;
|
|
2
|
+
declare const copyWebTemplateAssets: (outputDir: string, dspName: string, poly: boolean) => void;
|
|
3
3
|
|
|
4
|
-
export { copyWebStandaloneAssets,
|
|
4
|
+
export { copyWebStandaloneAssets, copyWebTemplateAssets };
|
|
@@ -1,41 +1,56 @@
|
|
|
1
1
|
//@ts-check
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
|
-
import { cpSync, rmSync } from "../fileutils.js";
|
|
4
|
+
import { cpSync, cpSyncModify, rmSync } from "../fileutils.js";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
|
|
7
7
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* @param {string} outputDir
|
|
11
|
+
* @param {string} outputDir - The output directory.
|
|
12
|
+
* @param {string} dspName - The name of the DSP to be loaded.
|
|
13
|
+
* @param {boolean} [poly] - Whether the DSP is polyphonic.
|
|
12
14
|
*/
|
|
13
|
-
const copyWebStandaloneAssets = (outputDir) => {
|
|
15
|
+
const copyWebStandaloneAssets = (outputDir, dspName, poly = false) => {
|
|
14
16
|
console.log(`Writing assets files.`)
|
|
15
17
|
const assetsPath = path.join(__dirname, "../assets/standalone");
|
|
16
18
|
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
|
|
20
|
+
// Copy some files
|
|
21
|
+
const templateJSPath = (poly) ? path.join(__dirname, "../assets/standalone/index-poly.js") : path.join(__dirname, "../assets/standalone/index.js");
|
|
22
|
+
cpSyncModify(templateJSPath, outputDir + `/${dspName}.js`, "FAUST_DSP", dspName);
|
|
23
|
+
|
|
24
|
+
const templateHTMLPath = path.join(__dirname, "../assets/standalone/index.html");
|
|
25
|
+
cpSyncModify(templateHTMLPath, outputDir + `/${dspName}.html`, "FAUST_DSP", dspName);
|
|
26
|
+
|
|
27
|
+
const faustwasmPath = path.join(__dirname, "../assets/standalone/faustwasm");
|
|
28
|
+
cpSync(faustwasmPath, outputDir + "/faustwasm");
|
|
29
|
+
|
|
30
|
+
const faustuiPath = path.join(__dirname, "../assets/standalone/faust-ui");
|
|
31
|
+
cpSync(faustuiPath, outputDir + "/faust-ui");
|
|
23
32
|
};
|
|
24
33
|
|
|
25
34
|
/**
|
|
26
|
-
* @param {string} outputDir
|
|
35
|
+
* @param {string} outputDir - The output directory.
|
|
36
|
+
* @param {string} dspName - The name of the DSP to be loaded.
|
|
37
|
+
* @param {boolean} [poly] - Whether the DSP is polyphonic.
|
|
27
38
|
*/
|
|
28
|
-
const
|
|
29
|
-
|
|
39
|
+
const copyWebTemplateAssets = (outputDir, dspName, poly = false) => {
|
|
40
|
+
|
|
41
|
+
// Create output directory if it doesn't exist
|
|
30
42
|
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir);
|
|
43
|
+
|
|
31
44
|
// Copy some files
|
|
32
45
|
const templateJSPath = path.join(__dirname, "../assets/standalone/template.js");
|
|
46
|
+
cpSync(templateJSPath, outputDir + `/${dspName}.js`);
|
|
47
|
+
|
|
33
48
|
const templateHTMLPath = (poly) ? path.join(__dirname, "../assets/standalone/template-poly.html") : path.join(__dirname, "../assets/standalone/template.html");
|
|
49
|
+
cpSyncModify(templateHTMLPath, outputDir + `/${dspName}.html`, "FAUST_DSP", dspName);
|
|
50
|
+
|
|
34
51
|
const faustwasmPath = path.join(__dirname, "../assets/standalone/faustwasm");
|
|
35
|
-
cpSync(templateJSPath, outputDir + "/template.js");
|
|
36
|
-
cpSync(templateHTMLPath, outputDir + "/template.html");
|
|
37
52
|
cpSync(faustwasmPath, outputDir + "/faustwasm");
|
|
38
53
|
};
|
|
39
54
|
|
|
40
|
-
export { copyWebStandaloneAssets,
|
|
55
|
+
export { copyWebStandaloneAssets, copyWebTemplateAssets };
|
|
41
56
|
|
package/src/faust2wasmFiles.js
CHANGED
|
@@ -27,11 +27,14 @@ const faust2wasmFiles = async (inputFile, outputDir, argv = [], poly = false) =>
|
|
|
27
27
|
console.log(`Reading file ${inputFile}`);
|
|
28
28
|
const code = fs.readFileSync(inputFile, { encoding: "utf8" });
|
|
29
29
|
|
|
30
|
+
const fileName = inputFile.split('/').pop();
|
|
31
|
+
const dspName = fileName.replace(/\.dsp$/, '');
|
|
32
|
+
|
|
30
33
|
if (!argv.find(a => a === "-I")) argv.push("-I", "libraries/");
|
|
31
|
-
const dspModulePath = path.join(outputDir,
|
|
32
|
-
const dspMetaPath = path.join(outputDir,
|
|
33
|
-
const effectModulePath = path.join(outputDir,
|
|
34
|
-
const effectMetaPath = path.join(outputDir,
|
|
34
|
+
const dspModulePath = path.join(outputDir, `${dspName}.wasm`);
|
|
35
|
+
const dspMetaPath = path.join(outputDir, `${dspName}.json`);
|
|
36
|
+
const effectModulePath = path.join(outputDir, `${dspName}_effect.wasm`);
|
|
37
|
+
const effectMetaPath = path.join(outputDir, `${dspName}_effect.json`);
|
|
35
38
|
const mixerModulePath = path.join(outputDir, "mixerModule.wasm");
|
|
36
39
|
/** @type {Uint8Array} */
|
|
37
40
|
let dspModule;
|
package/test/midi.dsp
ADDED
package/test/organ1.dsp
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import("stdfaust.lib");
|
|
2
2
|
timbre(f) = os.osc(f)*0.5 + os.osc(f*2)*0.25 + os.osc(f*3)*0.125;
|
|
3
|
-
effect = _*(hslider("Left", 0.1, 0, 1, 0.01)), _*(hslider("Right", 0.
|
|
3
|
+
effect = _*(hslider("Left", 0.1, 0, 1, 0.01)), _*(hslider("Right", 0.1, 0, 1, 0.01));
|
|
4
4
|
process = timbre(hslider("freq", 440, 20, 10000, 1))
|
|
5
5
|
* hslider("gain", 0.0, 0, 1, 0.01)
|
|
6
6
|
* (button("gate") : en.adsr(0.1,0.1,0.98,1.5));
|