@grame/faustwasm 0.16.4 → 0.16.6

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.
@@ -1,253 +1,307 @@
1
- // @ts-check
2
-
3
- /**
4
- * @typedef {{ dspModule: WebAssembly.Module; dspMeta: FaustDspMeta; effectModule?: WebAssembly.Module; effectMeta?: FaustDspMeta; mixerModule?: WebAssembly.Module }} FaustDspDistribution
5
- * @typedef {import("./faustwasm").FaustDspMeta} FaustDspMeta
6
- * @typedef {import("./faustwasm").FaustMonoAudioWorkletNode} FaustMonoAudioWorkletNode
7
- * @typedef {import("./faustwasm").FaustPolyAudioWorkletNode} FaustPolyAudioWorkletNode
8
- * @typedef {import("./faustwasm").FaustMonoScriptProcessorNode} FaustMonoScriptProcessorNode
9
- * @typedef {import("./faustwasm").FaustPolyScriptProcessorNode} FaustPolyScriptProcessorNode
10
- * @typedef {FaustMonoAudioWorkletNode | FaustPolyAudioWorkletNode | FaustMonoScriptProcessorNode | FaustPolyScriptProcessorNode} FaustNode
11
- */
12
-
13
- /**
14
- * Creates a Faust audio node for use in the Web Audio API.
15
- *
16
- * @param {AudioContext} audioContext - The Web Audio API AudioContext to which the Faust audio node will be connected.
17
- * @param {string} [dspName] - The name of the DSP to be loaded.
18
- * @param {number} [voices] - The number of voices to be used for polyphonic DSPs.
19
- * @param {boolean} [sp] - Whether to create a ScriptProcessorNode instead of an AudioWorkletNode.
20
- * @returns {Promise<{ faustNode: FaustNode | null; dspMeta: FaustDspMeta }>} - An object containing the Faust audio node and the DSP metadata.
21
- */
22
- const createFaustNode = async (audioContext, dspName = "template", voices = 0, sp = false, bufferSize = 512) => {
23
- // Set to true if the DSP has an effect
24
- const FAUST_DSP_HAS_EFFECT = false;
25
-
26
- // Import necessary Faust modules and data
27
- const { FaustMonoDspGenerator, FaustPolyDspGenerator } = await import("./faustwasm/index.js");
28
-
29
- // Load DSP metadata from JSON
30
- /** @type {FaustDspMeta} */
31
- const dspMeta = await (await fetch("./dsp-meta.json")).json();
32
-
33
- // Compile the DSP module from WebAssembly binary data
34
- const dspModule = await WebAssembly.compileStreaming(await fetch("./dsp-module.wasm"));
35
-
36
- // Create an object representing Faust DSP with metadata and module
37
- /** @type {FaustDspDistribution} */
38
- const faustDsp = { dspMeta, dspModule };
39
-
40
- /** @type {FaustNode | null} */
41
- let faustNode = null;
42
-
43
- // Create either a polyphonic or monophonic Faust audio node based on the number of voices
44
- if (voices > 0) {
45
-
46
- // Try to load optional mixer and effect modules
47
- faustDsp.mixerModule = await WebAssembly.compileStreaming(await fetch("./mixer-module.wasm"));
48
-
49
- if (FAUST_DSP_HAS_EFFECT) {
50
- faustDsp.effectMeta = await (await fetch("./effect-meta.json")).json();
51
- faustDsp.effectModule = await WebAssembly.compileStreaming(await fetch("./effect-module.wasm"));
52
- }
53
-
54
- // Create a polyphonic Faust audio node
55
- const generator = new FaustPolyDspGenerator();
56
- faustNode = await generator.createNode(
57
- audioContext,
58
- voices,
59
- dspName,
60
- { module: faustDsp.dspModule, json: JSON.stringify(faustDsp.dspMeta), soundfiles: {} },
61
- faustDsp.mixerModule,
62
- faustDsp.effectModule ? { module: faustDsp.effectModule, json: JSON.stringify(faustDsp.effectMeta), soundfiles: {} } : undefined,
63
- sp,
64
- bufferSize
65
- );
66
- } else {
67
- // Create a standard Faust audio node
68
- const generator = new FaustMonoDspGenerator();
69
- faustNode = await generator.createNode(
70
- audioContext,
71
- dspName,
72
- { module: faustDsp.dspModule, json: JSON.stringify(faustDsp.dspMeta), soundfiles: {} },
73
- sp,
74
- bufferSize
75
- );
76
- }
77
-
78
- // Return an object with the Faust audio node and the DSP metadata
79
- return { faustNode, dspMeta };
80
- }
81
-
82
- /**
83
- * Connects an audio input stream to a Faust WebAudio node.
84
- *
85
- * @param {AudioContext} audioContext - The Web Audio API AudioContext to which the Faust audio node is connected.
86
- * @param {string} id - The ID of the audio input device to connect.
87
- * @param {FaustNode} faustNode - The Faust audio node to which the audio input stream will be connected.
88
- * @param {MediaStreamAudioSourceNode} oldInputStreamNode - The old audio input stream node to be disconnected from the Faust audio node.
89
- * @returns {Promise<MediaStreamAudioSourceNode>} - The new audio input stream node connected to the Faust audio node.
90
- */
91
- async function connectToAudioInput(audioContext, id, faustNode, oldInputStreamNode) {
92
- // Create an audio input stream node
93
- const constraints = {
94
- audio: {
95
- echoCancellation: false,
96
- noiseSuppression: false,
97
- autoGainControl: false,
98
- deviceId: id ? { exact: id } : undefined,
99
- },
100
- };
101
- // Get the audio input stream
102
- const stream = await navigator.mediaDevices.getUserMedia(constraints);
103
- if (stream) {
104
- if (oldInputStreamNode) oldInputStreamNode.disconnect();
105
- const newInputStreamNode = audioContext.createMediaStreamSource(stream);
106
- newInputStreamNode.connect(faustNode);
107
- return newInputStreamNode;
108
- } else {
109
- return oldInputStreamNode;
110
- }
111
- };
112
-
113
- /**
114
- * Creates a Faust UI for a Faust audio node.
115
- *
116
- * @param {FaustAudioWorkletNode} faustNode
117
- */
118
- async function createFaustUI(divFaustUI, faustNode) {
119
- const { FaustUI } = await import("./faust-ui/index.js");
120
- const $container = document.createElement("div");
121
- $container.style.margin = "0";
122
- $container.style.position = "absolute";
123
- $container.style.overflow = "auto";
124
- $container.style.display = "flex";
125
- $container.style.flexDirection = "column";
126
- $container.style.width = "100%";
127
- $container.style.height = "100%";
128
- divFaustUI.appendChild($container);
129
- const faustUI = new FaustUI({
130
- ui: faustNode.getUI(),
131
- root: $container,
132
- listenWindowMessage: false,
133
- listenWindowResize: true,
134
- });
1
+ // @ts-check
2
+
3
+ /**
4
+ * @typedef {{ dspModule: WebAssembly.Module; dspMeta: FaustDspMeta; effectModule?: WebAssembly.Module; effectMeta?: FaustDspMeta; mixerModule?: WebAssembly.Module }} FaustDspDistribution
5
+ * @typedef {import("./faustwasm").FaustDspMeta} FaustDspMeta
6
+ * @typedef {import("./faustwasm").FaustMonoAudioWorkletNode} FaustMonoAudioWorkletNode
7
+ * @typedef {import("./faustwasm").FaustPolyAudioWorkletNode} FaustPolyAudioWorkletNode
8
+ * @typedef {import("./faustwasm").FaustMonoScriptProcessorNode} FaustMonoScriptProcessorNode
9
+ * @typedef {import("./faustwasm").FaustPolyScriptProcessorNode} FaustPolyScriptProcessorNode
10
+ * @typedef {FaustMonoAudioWorkletNode | FaustPolyAudioWorkletNode | FaustMonoScriptProcessorNode | FaustPolyScriptProcessorNode} FaustNode
11
+ */
12
+
13
+ /**
14
+ * Compile a standalone `.wasm` asset into a WebAssembly module.
15
+ *
16
+ * `WebAssembly.compileStreaming(fetch(url))` is preferred because it lets the
17
+ * browser compile while bytes are still downloading, but it is strict about the
18
+ * response being a real wasm response, notably `Content-Type: application/wasm`.
19
+ * Standalone/PWA deployments often go through static file servers or service
20
+ * worker caches that preserve or invent a generic MIME type such as
21
+ * `application/octet-stream`. Some iOS Safari/PWA versions then reject the
22
+ * streaming path even though the bytes are valid wasm.
23
+ *
24
+ * The ArrayBuffer fallback compiles the same bytes after download and does not
25
+ * depend on the HTTP MIME type, making generated PWAs more tolerant of hosting
26
+ * and cache configuration.
27
+ *
28
+ * @param {string} url - Relative URL of the wasm asset to compile.
29
+ * @returns {Promise<WebAssembly.Module>} Compiled WebAssembly module.
30
+ */
31
+ const compileWasmModule = async (url) => {
32
+ const response = await fetch(url);
33
+ if (WebAssembly.compileStreaming) {
34
+ try {
35
+ return await WebAssembly.compileStreaming(Promise.resolve(response.clone()));
36
+ } catch (error) {
37
+ console.warn(`compileStreaming failed for ${url}, falling back to ArrayBuffer compilation.`, error);
38
+ }
39
+ }
40
+ return WebAssembly.compile(await response.arrayBuffer());
41
+ };
42
+
43
+ /**
44
+ * Creates a Faust audio node for use in the Web Audio API.
45
+ *
46
+ * AudioWorklet is the primary backend. When it fails and the caller did not
47
+ * explicitly request ScriptProcessor mode, creation is retried with
48
+ * ScriptProcessorNode. This fallback is especially useful for iOS standalone
49
+ * PWAs, where the page can load and the AudioContext can resume while
50
+ * AudioWorklet processing still fails or stays silent on some OS/browser
51
+ * combinations.
52
+ *
53
+ * @param {AudioContext} audioContext - The Web Audio API AudioContext to which the Faust audio node will be connected.
54
+ * @param {string} [dspName] - The name of the DSP to be loaded.
55
+ * @param {number} [voices] - The number of voices to be used for polyphonic DSPs.
56
+ * @param {boolean} [sp] - Whether to create a ScriptProcessorNode instead of an AudioWorkletNode.
57
+ * @param {number} [bufferSize] - ScriptProcessorNode buffer size, ignored by AudioWorkletNode.
58
+ * @returns {Promise<{ faustNode: FaustNode | null; dspMeta: FaustDspMeta }>} - An object containing the Faust audio node and the DSP metadata.
59
+ */
60
+ const createFaustNode = async (audioContext, dspName = "template", voices = 0, sp = false, bufferSize = 512) => {
61
+ // Set to true if the DSP has an effect
62
+ const FAUST_DSP_HAS_EFFECT = false;
63
+
64
+ // Import necessary Faust modules and data
65
+ const { FaustMonoDspGenerator, FaustPolyDspGenerator } = await import("./faustwasm/index.js");
66
+
67
+ // Load DSP metadata from JSON
68
+ /** @type {FaustDspMeta} */
69
+ const dspMeta = await (await fetch("./dsp-meta.json")).json();
70
+
71
+ // Compile the DSP module from WebAssembly binary data
72
+ const dspModule = await compileWasmModule("./dsp-module.wasm");
73
+
74
+ // Create an object representing Faust DSP with metadata and module
75
+ /** @type {FaustDspDistribution} */
76
+ const faustDsp = { dspMeta, dspModule };
77
+
78
+ /** @type {FaustNode | null} */
79
+ let faustNode = null;
80
+
81
+ // Create either a polyphonic or monophonic Faust audio node based on the number of voices
82
+ if (voices > 0) {
83
+
84
+ // Try to load optional mixer and effect modules
85
+ faustDsp.mixerModule = await compileWasmModule("./mixer-module.wasm");
86
+
87
+ if (FAUST_DSP_HAS_EFFECT) {
88
+ faustDsp.effectMeta = await (await fetch("./effect-meta.json")).json();
89
+ faustDsp.effectModule = await compileWasmModule("./effect-module.wasm");
90
+ }
91
+
92
+ // Keep both backends behind the same closure so fallback recreates the
93
+ // exact same DSP distribution with only the backend flag changed.
94
+ const generator = new FaustPolyDspGenerator();
95
+ const createPolyNode = (useScriptProcessor) => generator.createNode(
96
+ audioContext,
97
+ voices,
98
+ dspName,
99
+ { module: faustDsp.dspModule, json: JSON.stringify(faustDsp.dspMeta), soundfiles: {} },
100
+ faustDsp.mixerModule,
101
+ faustDsp.effectModule ? { module: faustDsp.effectModule, json: JSON.stringify(faustDsp.effectMeta), soundfiles: {} } : undefined,
102
+ useScriptProcessor,
103
+ bufferSize
104
+ );
105
+ try {
106
+ faustNode = await createPolyNode(sp);
107
+ } catch (error) {
108
+ if (sp) throw error;
109
+ console.warn("AudioWorklet creation failed, retrying with ScriptProcessorNode.", error);
110
+ faustNode = await createPolyNode(true);
111
+ }
112
+ } else {
113
+ // Keep both backends behind the same closure so fallback recreates the
114
+ // exact same DSP distribution with only the backend flag changed.
115
+ const generator = new FaustMonoDspGenerator();
116
+ const createMonoNode = (useScriptProcessor) => generator.createNode(
117
+ audioContext,
118
+ dspName,
119
+ { module: faustDsp.dspModule, json: JSON.stringify(faustDsp.dspMeta), soundfiles: {} },
120
+ useScriptProcessor,
121
+ bufferSize
122
+ );
123
+ try {
124
+ faustNode = await createMonoNode(sp);
125
+ } catch (error) {
126
+ if (sp) throw error;
127
+ console.warn("AudioWorklet creation failed, retrying with ScriptProcessorNode.", error);
128
+ faustNode = await createMonoNode(true);
129
+ }
130
+ }
131
+
132
+ // Return an object with the Faust audio node and the DSP metadata
133
+ return { faustNode, dspMeta };
134
+ }
135
+
136
+ /**
137
+ * Connects an audio input stream to a Faust WebAudio node.
138
+ *
139
+ * @param {AudioContext} audioContext - The Web Audio API AudioContext to which the Faust audio node is connected.
140
+ * @param {string} id - The ID of the audio input device to connect.
141
+ * @param {FaustNode} faustNode - The Faust audio node to which the audio input stream will be connected.
142
+ * @param {MediaStreamAudioSourceNode} oldInputStreamNode - The old audio input stream node to be disconnected from the Faust audio node.
143
+ * @returns {Promise<MediaStreamAudioSourceNode>} - The new audio input stream node connected to the Faust audio node.
144
+ */
145
+ async function connectToAudioInput(audioContext, id, faustNode, oldInputStreamNode) {
146
+ // Create an audio input stream node
147
+ const constraints = {
148
+ audio: {
149
+ echoCancellation: false,
150
+ noiseSuppression: false,
151
+ autoGainControl: false,
152
+ deviceId: id ? { exact: id } : undefined,
153
+ },
154
+ };
155
+ // Get the audio input stream
156
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
157
+ if (stream) {
158
+ if (oldInputStreamNode) oldInputStreamNode.disconnect();
159
+ const newInputStreamNode = audioContext.createMediaStreamSource(stream);
160
+ newInputStreamNode.connect(faustNode);
161
+ return newInputStreamNode;
162
+ } else {
163
+ return oldInputStreamNode;
164
+ }
165
+ };
166
+
167
+ /**
168
+ * Creates a Faust UI for a Faust audio node.
169
+ *
170
+ * @param {FaustAudioWorkletNode} faustNode
171
+ */
172
+ async function createFaustUI(divFaustUI, faustNode) {
173
+ const { FaustUI } = await import("./faust-ui/index.js");
174
+ const $container = document.createElement("div");
175
+ $container.style.margin = "0";
176
+ $container.style.position = "absolute";
177
+ $container.style.overflow = "auto";
178
+ $container.style.display = "flex";
179
+ $container.style.flexDirection = "column";
180
+ $container.style.width = "100%";
181
+ $container.style.height = "100%";
182
+ divFaustUI.appendChild($container);
183
+ const faustUI = new FaustUI({
184
+ ui: faustNode.getUI(),
185
+ root: $container,
186
+ listenWindowMessage: false,
187
+ listenWindowResize: true,
188
+ });
135
189
  faustUI.paramChangeByUI = (path, value) => faustNode.setParamValue(path, value);
136
190
  faustNode.setOutputParamHandler((path, value) => faustUI.paramChangeByDSP(path, value));
137
191
  faustNode.setInputParamHandler((path, value) => faustUI.paramChangeByDSP(path, value));
138
- $container.style.minWidth = `${faustUI.minWidth}px`;
139
- $container.style.minHeight = `${faustUI.minHeight}px`;
140
- faustUI.resize();
141
- };
142
-
143
- /**
144
- * Request permission to use motion and orientation sensors.
145
- */
146
- async function requestPermissions() {
147
-
148
- // Explicitly request permission on iOS before calling startSensors()
149
- if (typeof window.DeviceMotionEvent !== "undefined" && typeof window.DeviceMotionEvent.requestPermission === "function") {
150
- try {
151
- const permissionState = await window.DeviceMotionEvent.requestPermission();
152
- if (permissionState !== "granted") {
153
- console.warn("Motion sensor permission denied.");
154
- } else {
155
- console.log("Motion sensor permission granted.");
156
- }
157
- } catch (error) {
158
- console.error("Error requesting motion sensor permission:", error);
159
- }
160
- }
161
-
162
- if (typeof window.DeviceOrientationEvent !== "undefined" && typeof window.DeviceOrientationEvent.requestPermission === "function") {
163
- try {
164
- const permissionState = await window.DeviceOrientationEvent.requestPermission();
165
- if (permissionState !== "granted") {
166
- console.warn("Orientation sensor permission denied.");
167
- } else {
168
- console.log("Orientation sensor permission granted.");
169
- }
170
- } catch (error) {
171
- console.error("Error requesting orientation sensor permission:", error);
172
- }
173
- }
174
- }
175
-
176
- /**
177
- * Key2Midi: maps keyboard input to MIDI messages.
178
- */
179
- class Key2Midi {
180
- static KEY_MAP = {
181
- a: 0, w: 1, s: 2, e: 3, d: 4, f: 5, t: 6, g: 7,
182
- y: 8, h: 9, u: 10, j: 11, k: 12, o: 13, l: 14, p: 15, ";": 16,
183
- z: "PREV", x: "NEXT", c: "VELDOWN", v: "VELUP"
184
- };
185
-
186
- constructor({ keyMap = Key2Midi.KEY_MAP, offset = 60, velocity = 100, handler = console.log } = {}) {
187
- this.keyMap = keyMap;
188
- this.offset = offset;
189
- this.velocity = velocity;
190
- this.velMap = [20, 40, 60, 80, 100, 127];
191
- this.handler = handler;
192
- this.pressed = {};
193
-
194
- this.onKeyDown = this.onKeyDown.bind(this);
195
- this.onKeyUp = this.onKeyUp.bind(this);
196
- }
197
-
198
- start() {
199
- window.addEventListener("keydown", this.onKeyDown);
200
- window.addEventListener("keyup", this.onKeyUp);
201
- }
202
-
203
- stop() {
204
- window.removeEventListener("keydown", this.onKeyDown);
205
- window.removeEventListener("keyup", this.onKeyUp);
206
- }
207
-
208
- onKeyDown(e) {
209
- const key = e.key.toLowerCase();
210
- if (this.pressed[key]) return;
211
- this.pressed[key] = true;
212
-
213
- const val = this.keyMap[key];
214
- if (typeof val === "number") {
215
- const note = val + this.offset;
216
- this.handler([0x90, note, this.velocity]);
217
- } else if (val === "PREV") {
218
- this.offset -= 1;
219
- } else if (val === "NEXT") {
220
- this.offset += 1;
221
- } else if (val === "VELDOWN") {
222
- const idx = Math.max(0, this.velMap.indexOf(this.velocity) - 1);
223
- this.velocity = this.velMap[idx];
224
- } else if (val === "VELUP") {
225
- const idx = Math.min(this.velMap.length - 1, this.velMap.indexOf(this.velocity) + 1);
226
- this.velocity = this.velMap[idx];
227
- }
228
- }
229
-
230
- onKeyUp(e) {
231
- const key = e.key.toLowerCase();
232
- const val = this.keyMap[key];
233
- if (typeof val === "number") {
234
- const note = val + this.offset;
235
- this.handler([0x80, note, this.velocity]);
236
- }
237
- delete this.pressed[key];
238
- }
239
- }
240
-
241
- /**
242
- * Creates a Key2Midi instance.
243
- *
244
- * @param {function} handler - The function to handle MIDI messages.
245
- * @returns {Key2Midi} - The Key2Midi instance.
246
- */
247
- function createKey2MIDI(handler) {
248
- return new Key2Midi({ handler: handler });
249
- }
250
-
251
- // Export the functions
252
- export { createFaustNode, createFaustUI, createKey2MIDI, connectToAudioInput, requestPermissions };
253
-
192
+ $container.style.minWidth = `${faustUI.minWidth}px`;
193
+ $container.style.minHeight = `${faustUI.minHeight}px`;
194
+ faustUI.resize();
195
+ };
196
+
197
+ /**
198
+ * Request permission to use motion and orientation sensors.
199
+ */
200
+ async function requestPermissions() {
201
+
202
+ // Explicitly request permission on iOS before calling startSensors()
203
+ if (typeof window.DeviceMotionEvent !== "undefined" && typeof window.DeviceMotionEvent.requestPermission === "function") {
204
+ try {
205
+ const permissionState = await window.DeviceMotionEvent.requestPermission();
206
+ if (permissionState !== "granted") {
207
+ console.warn("Motion sensor permission denied.");
208
+ } else {
209
+ console.log("Motion sensor permission granted.");
210
+ }
211
+ } catch (error) {
212
+ console.error("Error requesting motion sensor permission:", error);
213
+ }
214
+ }
215
+
216
+ if (typeof window.DeviceOrientationEvent !== "undefined" && typeof window.DeviceOrientationEvent.requestPermission === "function") {
217
+ try {
218
+ const permissionState = await window.DeviceOrientationEvent.requestPermission();
219
+ if (permissionState !== "granted") {
220
+ console.warn("Orientation sensor permission denied.");
221
+ } else {
222
+ console.log("Orientation sensor permission granted.");
223
+ }
224
+ } catch (error) {
225
+ console.error("Error requesting orientation sensor permission:", error);
226
+ }
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Key2Midi: maps keyboard input to MIDI messages.
232
+ */
233
+ class Key2Midi {
234
+ static KEY_MAP = {
235
+ a: 0, w: 1, s: 2, e: 3, d: 4, f: 5, t: 6, g: 7,
236
+ y: 8, h: 9, u: 10, j: 11, k: 12, o: 13, l: 14, p: 15, ";": 16,
237
+ z: "PREV", x: "NEXT", c: "VELDOWN", v: "VELUP"
238
+ };
239
+
240
+ constructor({ keyMap = Key2Midi.KEY_MAP, offset = 60, velocity = 100, handler = console.log } = {}) {
241
+ this.keyMap = keyMap;
242
+ this.offset = offset;
243
+ this.velocity = velocity;
244
+ this.velMap = [20, 40, 60, 80, 100, 127];
245
+ this.handler = handler;
246
+ this.pressed = {};
247
+
248
+ this.onKeyDown = this.onKeyDown.bind(this);
249
+ this.onKeyUp = this.onKeyUp.bind(this);
250
+ }
251
+
252
+ start() {
253
+ window.addEventListener("keydown", this.onKeyDown);
254
+ window.addEventListener("keyup", this.onKeyUp);
255
+ }
256
+
257
+ stop() {
258
+ window.removeEventListener("keydown", this.onKeyDown);
259
+ window.removeEventListener("keyup", this.onKeyUp);
260
+ }
261
+
262
+ onKeyDown(e) {
263
+ const key = e.key.toLowerCase();
264
+ if (this.pressed[key]) return;
265
+ this.pressed[key] = true;
266
+
267
+ const val = this.keyMap[key];
268
+ if (typeof val === "number") {
269
+ const note = val + this.offset;
270
+ this.handler([0x90, note, this.velocity]);
271
+ } else if (val === "PREV") {
272
+ this.offset -= 1;
273
+ } else if (val === "NEXT") {
274
+ this.offset += 1;
275
+ } else if (val === "VELDOWN") {
276
+ const idx = Math.max(0, this.velMap.indexOf(this.velocity) - 1);
277
+ this.velocity = this.velMap[idx];
278
+ } else if (val === "VELUP") {
279
+ const idx = Math.min(this.velMap.length - 1, this.velMap.indexOf(this.velocity) + 1);
280
+ this.velocity = this.velMap[idx];
281
+ }
282
+ }
283
+
284
+ onKeyUp(e) {
285
+ const key = e.key.toLowerCase();
286
+ const val = this.keyMap[key];
287
+ if (typeof val === "number") {
288
+ const note = val + this.offset;
289
+ this.handler([0x80, note, this.velocity]);
290
+ }
291
+ delete this.pressed[key];
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Creates a Key2Midi instance.
297
+ *
298
+ * @param {function} handler - The function to handle MIDI messages.
299
+ * @returns {Key2Midi} - The Key2Midi instance.
300
+ */
301
+ function createKey2MIDI(handler) {
302
+ return new Key2Midi({ handler: handler });
303
+ }
304
+
305
+ // Export the functions
306
+ export { createFaustNode, createFaustUI, createKey2MIDI, connectToAudioInput, requestPermissions };
307
+