@grame/faustwasm 0.16.4 → 0.16.5
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/assets/standalone/create-node.js +304 -250
- package/assets/standalone/faust-pwa.js +63 -12
- package/assets/standalone/index-pwa.js +12 -5
- package/package.json +1 -1
- package/test/organ/create-node.js +0 -253
- package/test/organ/dsp-meta.json +0 -132
- package/test/organ/dsp-module.wasm +0 -0
- package/test/organ/faust-pwa.js +0 -344
- package/test/organ/faust-ui/index.css +0 -442
- package/test/organ/faust-ui/index.css.map +0 -1
- package/test/organ/faust-ui/index.d.ts +0 -1
- package/test/organ/faust-ui/index.js +0 -3756
- package/test/organ/faust-ui/index.js.map +0 -1
- package/test/organ/faustwasm/index.d.ts +0 -1824
- package/test/organ/faustwasm/index.js +0 -6178
- package/test/organ/faustwasm/index.js.map +0 -7
- package/test/organ/icon.png +0 -0
- package/test/organ/index.html +0 -76
- package/test/organ/index.js +0 -69
- package/test/organ/manifest.json +0 -17
- package/test/organ/service-worker.js +0 -165
|
@@ -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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
|