@libraz/libsonare 1.2.3 → 1.3.0
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 +38 -1
- package/dist/index.d.ts +1 -2842
- package/dist/index.js +3602 -1934
- package/dist/index.js.map +1 -1
- package/dist/sonare-rt-module.js +1 -1
- package/dist/sonare-rt.js +1 -1
- package/dist/sonare-rt.wasm +0 -0
- package/dist/sonare.js +1 -1
- package/dist/sonare.wasm +0 -0
- package/dist/worklet.d.ts +4816 -483
- package/dist/worklet.js +747 -440
- package/dist/worklet.js.map +1 -1
- package/package.json +2 -1
- package/src/analysis_helpers.ts +152 -0
- package/src/audio.ts +493 -0
- package/src/codes.ts +56 -0
- package/src/effects_mastering.ts +964 -0
- package/src/feature_core.ts +248 -0
- package/src/feature_music.ts +419 -0
- package/src/feature_pitch.ts +80 -0
- package/src/feature_resample.ts +21 -0
- package/src/feature_spectral.ts +330 -0
- package/src/feature_spectrogram.ts +454 -0
- package/src/features.ts +84 -0
- package/src/index.ts +341 -4963
- package/src/live_audio.ts +45 -0
- package/src/metering.ts +380 -0
- package/src/mixer.ts +523 -0
- package/src/module_state.ts +14 -0
- package/src/opfs_clip_pages.ts +188 -0
- package/src/project.ts +1614 -0
- package/src/public_types.ts +177 -2
- package/src/quick_analysis.ts +508 -0
- package/src/realtime_engine.ts +667 -0
- package/src/realtime_voice_changer.ts +275 -0
- package/src/scale.ts +42 -0
- package/src/sonare.js.d.ts +302 -4
- package/src/stream_analyzer.ts +275 -0
- package/src/stream_types.ts +26 -1
- package/src/streaming_mixing.ts +18 -0
- package/src/streaming_processors.ts +335 -0
- package/src/validation.ts +82 -0
- package/src/web_midi.ts +367 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-call validation options accepted by guarded wrappers. Empty-buffer
|
|
3
|
+
* checks are always performed; pass `{ validate: false }` to opt out of the
|
|
4
|
+
* O(n) NaN/Inf scan on hot paths.
|
|
5
|
+
*/
|
|
6
|
+
export interface ValidateOptions {
|
|
7
|
+
validate?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function assertNonEmptySamples(
|
|
11
|
+
fnName: string,
|
|
12
|
+
samples: ArrayLike<number>,
|
|
13
|
+
argName = 'samples',
|
|
14
|
+
): void {
|
|
15
|
+
if (samples.length === 0) {
|
|
16
|
+
throw new RangeError(`${fnName}: ${argName} must not be empty`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function assertFiniteSamples(
|
|
21
|
+
fnName: string,
|
|
22
|
+
samples: ArrayLike<number>,
|
|
23
|
+
validate: boolean,
|
|
24
|
+
argName = 'samples',
|
|
25
|
+
): void {
|
|
26
|
+
if (!validate) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
for (let i = 0; i < samples.length; i++) {
|
|
30
|
+
const v = samples[i] as number;
|
|
31
|
+
if (!Number.isFinite(v)) {
|
|
32
|
+
throw new RangeError(`${fnName}: ${argName} contains NaN or Inf at index ${i}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function assertSamples(
|
|
38
|
+
fnName: string,
|
|
39
|
+
samples: ArrayLike<number>,
|
|
40
|
+
validate: boolean,
|
|
41
|
+
argName = 'samples',
|
|
42
|
+
): void {
|
|
43
|
+
assertNonEmptySamples(fnName, samples, argName);
|
|
44
|
+
assertFiniteSamples(fnName, samples, validate, argName);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function assertFiniteScalar(fnName: string, value: number, argName: string): void {
|
|
48
|
+
if (!Number.isFinite(value)) {
|
|
49
|
+
throw new RangeError(`${fnName}: ${argName} must be a finite number`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function assertSampleRate(fnName: string, sampleRate: number): void {
|
|
54
|
+
if (!Number.isInteger(sampleRate) || sampleRate < 8000 || sampleRate > 384000) {
|
|
55
|
+
throw new RangeError(`${fnName}: sampleRate out of supported range [8000, 384000]`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function assertNonNegativeInteger(fnName: string, value: number, argName: string): void {
|
|
60
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
61
|
+
throw new RangeError(`${fnName}: ${argName} must be a non-negative integer`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function assertPositiveInteger(fnName: string, value: number, argName: string): void {
|
|
66
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
67
|
+
throw new RangeError(`${fnName}: ${argName} must be a positive integer`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function assertInterleavedSamples(
|
|
72
|
+
fnName: string,
|
|
73
|
+
samples: ArrayLike<number>,
|
|
74
|
+
channels: number,
|
|
75
|
+
validate: boolean,
|
|
76
|
+
): void {
|
|
77
|
+
assertSamples(fnName, samples, validate);
|
|
78
|
+
assertPositiveInteger(fnName, channels, 'channels');
|
|
79
|
+
if (samples.length % channels !== 0) {
|
|
80
|
+
throw new RangeError(`${fnName}: samples length must be a multiple of channels`);
|
|
81
|
+
}
|
|
82
|
+
}
|
package/src/web_midi.ts
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import type { MidiCcBindOptions, RealtimeEngine } from './realtime_engine';
|
|
2
|
+
|
|
3
|
+
type MidiInputState = 'connected' | 'disconnected';
|
|
4
|
+
|
|
5
|
+
interface MidiPortLike {
|
|
6
|
+
id: string;
|
|
7
|
+
name?: string | null;
|
|
8
|
+
manufacturer?: string | null;
|
|
9
|
+
state?: MidiInputState;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface MidiMessageEventLike {
|
|
13
|
+
data: ArrayLike<number>;
|
|
14
|
+
timeStamp?: number;
|
|
15
|
+
receivedTime?: number;
|
|
16
|
+
target?: MidiPortLike;
|
|
17
|
+
currentTarget?: MidiPortLike;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface MidiInputLike extends MidiPortLike {
|
|
21
|
+
type?: 'input';
|
|
22
|
+
onmidimessage: ((event: MidiMessageEventLike) => void) | null;
|
|
23
|
+
addEventListener?: (type: 'midimessage', listener: (event: MidiMessageEventLike) => void) => void;
|
|
24
|
+
removeEventListener?: (
|
|
25
|
+
type: 'midimessage',
|
|
26
|
+
listener: (event: MidiMessageEventLike) => void,
|
|
27
|
+
) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface MidiConnectionEventLike {
|
|
31
|
+
port?: MidiPortLike | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface MidiAccessLike {
|
|
35
|
+
inputs: Map<string, MidiInputLike> | Iterable<[string, MidiInputLike]>;
|
|
36
|
+
onstatechange: ((event: MidiConnectionEventLike) => void) | null;
|
|
37
|
+
addEventListener?: (
|
|
38
|
+
type: 'statechange',
|
|
39
|
+
listener: (event: MidiConnectionEventLike) => void,
|
|
40
|
+
) => void;
|
|
41
|
+
removeEventListener?: (
|
|
42
|
+
type: 'statechange',
|
|
43
|
+
listener: (event: MidiConnectionEventLike) => void,
|
|
44
|
+
) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface NavigatorWithMidi {
|
|
48
|
+
requestMIDIAccess?: (options?: {
|
|
49
|
+
sysex?: boolean;
|
|
50
|
+
software?: boolean;
|
|
51
|
+
}) => Promise<MidiAccessLike>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface WebMidiCcBinding {
|
|
55
|
+
channel: number;
|
|
56
|
+
controller: number;
|
|
57
|
+
paramId: number;
|
|
58
|
+
options?: MidiCcBindOptions;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface WebMidiInputInfo {
|
|
62
|
+
id: string;
|
|
63
|
+
name: string;
|
|
64
|
+
manufacturer: string;
|
|
65
|
+
state: MidiInputState;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface BindWebMidiOptions {
|
|
69
|
+
/** Realtime-engine MIDI destination receiving the live input source. Default `0`. */
|
|
70
|
+
destinationId?: number;
|
|
71
|
+
/** UMP group used for MIDI 1.0 channel voice events. Default `0`. */
|
|
72
|
+
group?: number;
|
|
73
|
+
/** Restrict binding to specific Web MIDI input ids. Omit or empty = all connected inputs. */
|
|
74
|
+
inputIds?: readonly string[];
|
|
75
|
+
/** Request SysEx-capable access from the browser. Default `false`. */
|
|
76
|
+
sysex?: boolean;
|
|
77
|
+
/** Request software ports from the browser where supported. Default `true`. */
|
|
78
|
+
software?: boolean;
|
|
79
|
+
/** Bind CC-to-parameter mappings before ports are connected. */
|
|
80
|
+
ccBindings?: readonly WebMidiCcBinding[];
|
|
81
|
+
/** Convert a Web MIDI event timestamp to engine port-time samples. */
|
|
82
|
+
timestampToSamples?: (eventTimeMs: number) => number;
|
|
83
|
+
/** Observe hot-plug updates after the helper rebinds matching inputs. */
|
|
84
|
+
onInputsChanged?: (inputs: WebMidiInputInfo[]) => void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface WebMidiBinding {
|
|
88
|
+
access: MidiAccessLike;
|
|
89
|
+
inputs(): WebMidiInputInfo[];
|
|
90
|
+
close(): void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type BoundInput = {
|
|
94
|
+
input: MidiInputLike;
|
|
95
|
+
listener: (event: MidiMessageEventLike) => void;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export function isWebMidiAvailable(): boolean {
|
|
99
|
+
return (
|
|
100
|
+
typeof (globalThis.navigator as NavigatorWithMidi | undefined)?.requestMIDIAccess === 'function'
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function bindWebMidi(
|
|
105
|
+
engine: RealtimeEngine,
|
|
106
|
+
options: BindWebMidiOptions = {},
|
|
107
|
+
): Promise<WebMidiBinding> {
|
|
108
|
+
const requestMIDIAccess = (globalThis.navigator as NavigatorWithMidi | undefined)
|
|
109
|
+
?.requestMIDIAccess;
|
|
110
|
+
if (typeof requestMIDIAccess !== 'function') {
|
|
111
|
+
throw new Error('Web MIDI is not available in this environment');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const group = options.group ?? 0;
|
|
115
|
+
assertNibble('bindWebMidi', group, 'group');
|
|
116
|
+
const destinationId = options.destinationId ?? 0;
|
|
117
|
+
const selectedIds = new Set(options.inputIds ?? []);
|
|
118
|
+
const access = await requestMIDIAccess({
|
|
119
|
+
sysex: options.sysex ?? false,
|
|
120
|
+
software: options.software ?? true,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
for (const binding of options.ccBindings ?? []) {
|
|
124
|
+
engine.bindMidiCc(binding.channel, binding.controller, binding.paramId, binding.options);
|
|
125
|
+
}
|
|
126
|
+
engine.setMidiInputSource(destinationId);
|
|
127
|
+
|
|
128
|
+
const bound = new Map<string, BoundInput>();
|
|
129
|
+
let closed = false;
|
|
130
|
+
let runningStatus = 0;
|
|
131
|
+
|
|
132
|
+
const shouldBind = (input: MidiInputLike) =>
|
|
133
|
+
input.state !== 'disconnected' && (selectedIds.size === 0 || selectedIds.has(input.id));
|
|
134
|
+
|
|
135
|
+
const snapshotInputs = (): WebMidiInputInfo[] =>
|
|
136
|
+
Array.from(iterInputs(access), ([id, input]) => ({
|
|
137
|
+
id,
|
|
138
|
+
name: input.name ?? '',
|
|
139
|
+
manufacturer: input.manufacturer ?? '',
|
|
140
|
+
state: input.state ?? 'connected',
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
const notify = () => options.onInputsChanged?.(snapshotInputs());
|
|
144
|
+
|
|
145
|
+
const bindInput = (input: MidiInputLike) => {
|
|
146
|
+
if (bound.has(input.id) || !shouldBind(input)) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const listener = (event: MidiMessageEventLike) => {
|
|
150
|
+
const status = dispatchMidiMessage(
|
|
151
|
+
engine,
|
|
152
|
+
event,
|
|
153
|
+
group,
|
|
154
|
+
runningStatus,
|
|
155
|
+
options.timestampToSamples,
|
|
156
|
+
);
|
|
157
|
+
if (status !== 0) {
|
|
158
|
+
runningStatus = status;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
if (input.addEventListener) {
|
|
162
|
+
input.addEventListener('midimessage', listener);
|
|
163
|
+
} else {
|
|
164
|
+
input.onmidimessage = listener;
|
|
165
|
+
}
|
|
166
|
+
bound.set(input.id, { input, listener });
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const unbindInput = (input: MidiInputLike) => {
|
|
170
|
+
const entry = bound.get(input.id);
|
|
171
|
+
if (!entry) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (entry.input.removeEventListener) {
|
|
175
|
+
entry.input.removeEventListener('midimessage', entry.listener);
|
|
176
|
+
} else if (entry.input.onmidimessage === entry.listener) {
|
|
177
|
+
entry.input.onmidimessage = null;
|
|
178
|
+
}
|
|
179
|
+
bound.delete(input.id);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const refreshInputs = () => {
|
|
183
|
+
for (const [, entry] of bound) {
|
|
184
|
+
if (!shouldBind(entry.input)) {
|
|
185
|
+
unbindInput(entry.input);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
for (const [, input] of iterInputs(access)) {
|
|
189
|
+
bindInput(input);
|
|
190
|
+
}
|
|
191
|
+
notify();
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const stateListener = (event: MidiConnectionEventLike) => {
|
|
195
|
+
if (closed) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (event.port && 'onmidimessage' in event.port) {
|
|
199
|
+
const input = event.port as MidiInputLike;
|
|
200
|
+
if (shouldBind(input)) {
|
|
201
|
+
bindInput(input);
|
|
202
|
+
} else {
|
|
203
|
+
unbindInput(input);
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
refreshInputs();
|
|
207
|
+
}
|
|
208
|
+
notify();
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
refreshInputs();
|
|
212
|
+
if (access.addEventListener) {
|
|
213
|
+
access.addEventListener('statechange', stateListener);
|
|
214
|
+
} else {
|
|
215
|
+
access.onstatechange = stateListener;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
access,
|
|
220
|
+
inputs: snapshotInputs,
|
|
221
|
+
close() {
|
|
222
|
+
closed = true;
|
|
223
|
+
if (access.removeEventListener) {
|
|
224
|
+
access.removeEventListener('statechange', stateListener);
|
|
225
|
+
} else if (access.onstatechange === stateListener) {
|
|
226
|
+
access.onstatechange = null;
|
|
227
|
+
}
|
|
228
|
+
for (const [, entry] of Array.from(bound)) {
|
|
229
|
+
unbindInput(entry.input);
|
|
230
|
+
}
|
|
231
|
+
engine.clearMidiInputSource();
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function dispatchMidiMessage(
|
|
237
|
+
engine: RealtimeEngine,
|
|
238
|
+
event: MidiMessageEventLike,
|
|
239
|
+
group: number,
|
|
240
|
+
runningStatus: number,
|
|
241
|
+
timestampToSamples?: (eventTimeMs: number) => number,
|
|
242
|
+
): number {
|
|
243
|
+
const data = event.data;
|
|
244
|
+
if (data.length === 0) {
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
const first = data[0];
|
|
248
|
+
if (first > 0xff) {
|
|
249
|
+
dispatchUmpMessage(
|
|
250
|
+
engine,
|
|
251
|
+
data,
|
|
252
|
+
timestampToSamples?.(event.receivedTime ?? event.timeStamp ?? 0) ?? 0,
|
|
253
|
+
);
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let offset = 0;
|
|
258
|
+
let status = first & 0xff;
|
|
259
|
+
if (status < 0x80) {
|
|
260
|
+
if (runningStatus === 0) {
|
|
261
|
+
return 0;
|
|
262
|
+
}
|
|
263
|
+
status = runningStatus;
|
|
264
|
+
} else {
|
|
265
|
+
offset = 1;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const message = status & 0xf0;
|
|
269
|
+
const channel = status & 0x0f;
|
|
270
|
+
if (message < 0x80 || message > 0xe0) {
|
|
271
|
+
return status;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const a = readU7(data, offset);
|
|
275
|
+
const b = readU7(data, offset + 1);
|
|
276
|
+
if (a < 0) {
|
|
277
|
+
return status;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const portTimeSamples = timestampToSamples
|
|
281
|
+
? timestampToSamples(event.receivedTime ?? event.timeStamp ?? 0)
|
|
282
|
+
: 0;
|
|
283
|
+
|
|
284
|
+
if (message === 0x80) {
|
|
285
|
+
engine.pushMidiInputNoteOff(group, channel, a, b < 0 ? 0 : b, portTimeSamples);
|
|
286
|
+
} else if (message === 0x90) {
|
|
287
|
+
if ((b < 0 ? 0 : b) === 0) {
|
|
288
|
+
engine.pushMidiInputNoteOff(group, channel, a, 0, portTimeSamples);
|
|
289
|
+
} else {
|
|
290
|
+
engine.pushMidiInputNoteOn(group, channel, a, b, portTimeSamples);
|
|
291
|
+
}
|
|
292
|
+
} else if (message === 0xb0 && b >= 0) {
|
|
293
|
+
engine.pushMidiInputCc(group, channel, a, b, portTimeSamples);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return status;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function dispatchUmpMessage(
|
|
300
|
+
engine: RealtimeEngine,
|
|
301
|
+
words: ArrayLike<number>,
|
|
302
|
+
portTimeSamples: number,
|
|
303
|
+
): void {
|
|
304
|
+
const word0 = words[0] >>> 0;
|
|
305
|
+
const messageType = word0 >>> 28;
|
|
306
|
+
const group = (word0 >>> 24) & 0x0f;
|
|
307
|
+
|
|
308
|
+
if (messageType === 0x2) {
|
|
309
|
+
const status = (word0 >>> 16) & 0xff;
|
|
310
|
+
const message = status & 0xf0;
|
|
311
|
+
const channel = status & 0x0f;
|
|
312
|
+
const a = (word0 >>> 8) & 0x7f;
|
|
313
|
+
const b = word0 & 0x7f;
|
|
314
|
+
if (message === 0x80) {
|
|
315
|
+
engine.pushMidiInputNoteOff(group, channel, a, b, portTimeSamples);
|
|
316
|
+
} else if (message === 0x90) {
|
|
317
|
+
if (b === 0) {
|
|
318
|
+
engine.pushMidiInputNoteOff(group, channel, a, 0, portTimeSamples);
|
|
319
|
+
} else {
|
|
320
|
+
engine.pushMidiInputNoteOn(group, channel, a, b, portTimeSamples);
|
|
321
|
+
}
|
|
322
|
+
} else if (message === 0xb0) {
|
|
323
|
+
engine.pushMidiInputCc(group, channel, a, b, portTimeSamples);
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (messageType === 0x4 && words.length >= 2) {
|
|
329
|
+
const status = (word0 >>> 20) & 0x0f;
|
|
330
|
+
const channel = (word0 >>> 16) & 0x0f;
|
|
331
|
+
const data1 = (word0 >>> 8) & 0x7f;
|
|
332
|
+
const word1 = words[1] >>> 0;
|
|
333
|
+
if (status === 0x8) {
|
|
334
|
+
engine.pushMidiInputNoteOff(group, channel, data1, (word1 >>> 25) & 0x7f, portTimeSamples);
|
|
335
|
+
} else if (status === 0x9) {
|
|
336
|
+
const velocity = (word1 >>> 25) & 0x7f;
|
|
337
|
+
if (velocity === 0) {
|
|
338
|
+
engine.pushMidiInputNoteOff(group, channel, data1, 0, portTimeSamples);
|
|
339
|
+
} else {
|
|
340
|
+
engine.pushMidiInputNoteOn(group, channel, data1, velocity, portTimeSamples);
|
|
341
|
+
}
|
|
342
|
+
} else if (status === 0xb) {
|
|
343
|
+
engine.pushMidiInputCc(group, channel, data1, (word1 >>> 25) & 0x7f, portTimeSamples);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function readU7(data: ArrayLike<number>, index: number): number {
|
|
349
|
+
if (index >= data.length) {
|
|
350
|
+
return -1;
|
|
351
|
+
}
|
|
352
|
+
const value = data[index];
|
|
353
|
+
if (!Number.isInteger(value) || value < 0 || value > 127) {
|
|
354
|
+
return -1;
|
|
355
|
+
}
|
|
356
|
+
return value;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function assertNibble(fnName: string, value: number, field: string): void {
|
|
360
|
+
if (!Number.isInteger(value) || value < 0 || value > 15) {
|
|
361
|
+
throw new RangeError(`${fnName}: ${field} must be an integer in [0, 15]`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function iterInputs(access: MidiAccessLike): Iterable<[string, MidiInputLike]> {
|
|
366
|
+
return access.inputs instanceof Map ? access.inputs.entries() : access.inputs;
|
|
367
|
+
}
|