audio-mixer-engine 0.1.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/LICENSE +21 -0
- package/README.md +816 -0
- package/dist/audio-mixer-engine.cjs.js +1 -0
- package/dist/audio-mixer-engine.es.js +1669 -0
- package/package.json +54 -0
- package/src/assets/stick-4cs.mp3 +0 -0
- package/src/assets/stick-4d.mp3 +0 -0
- package/src/index.js +18 -0
- package/src/lib/audio-engine.js +526 -0
- package/src/lib/beat-mapper.js +155 -0
- package/src/lib/midi-parser.js +718 -0
- package/src/lib/midi-player.js +700 -0
- package/src/lib/playback-manager.js +1257 -0
- package/src/lib/spessasynth-audio-engine.js +310 -0
- package/src/lib/spessasynth-channel-handle.js +151 -0
|
@@ -0,0 +1,1669 @@
|
|
|
1
|
+
class k {
|
|
2
|
+
/**
|
|
3
|
+
* Create a new AudioEngine instance
|
|
4
|
+
* @param {AudioContext} audioContext - Web Audio API context
|
|
5
|
+
* @param {Object} options - Engine-specific options
|
|
6
|
+
*/
|
|
7
|
+
constructor(t, e = {}) {
|
|
8
|
+
if (new.target === k)
|
|
9
|
+
throw new Error("AudioEngine is abstract and cannot be instantiated directly");
|
|
10
|
+
this.audioContext = t, this.options = e, this.isInitialized = !1, this.channels = /* @__PURE__ */ new WeakMap(), this.activeChannels = /* @__PURE__ */ new Set();
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Initialize the audio engine - load soundfont and set up synthesis
|
|
14
|
+
* @param {string|ArrayBuffer} soundfontData - Path to soundfont or binary data
|
|
15
|
+
* @returns {Promise<void>}
|
|
16
|
+
*/
|
|
17
|
+
async initialize(t) {
|
|
18
|
+
throw new Error("initialize() must be implemented by subclass");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Create a new channel for a musical part
|
|
22
|
+
* @param {string} partId - Unique identifier for this part (e.g., 'soprano', 'piano')
|
|
23
|
+
* @param {Object} options - Channel configuration
|
|
24
|
+
* @param {string|number} [options.instrument] - Initial instrument
|
|
25
|
+
* @param {number} [options.initialVolume=1.0] - Initial volume (0.0-1.0)
|
|
26
|
+
* @returns {ChannelHandle} Handle object for controlling this channel
|
|
27
|
+
*/
|
|
28
|
+
createChannel(t, e = {}) {
|
|
29
|
+
throw new Error("createChannel() must be implemented by subclass");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Stop all notes on all channels
|
|
33
|
+
*/
|
|
34
|
+
allSoundsOff() {
|
|
35
|
+
throw new Error("allSoundsOff() must be implemented by subclass");
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get list of all active channel handles
|
|
39
|
+
* @returns {Array<ChannelHandle>} Array of active channel handles
|
|
40
|
+
*/
|
|
41
|
+
getActiveChannels() {
|
|
42
|
+
return Array.from(this.activeChannels);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Clean up all resources and disconnect audio nodes
|
|
46
|
+
*/
|
|
47
|
+
destroy() {
|
|
48
|
+
this.allSoundsOff(), this.activeChannels.clear(), this.isInitialized = !1;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Validate that the engine is initialized
|
|
52
|
+
* @protected
|
|
53
|
+
*/
|
|
54
|
+
_validateInitialized() {
|
|
55
|
+
if (!this.isInitialized)
|
|
56
|
+
throw new Error("AudioEngine not initialized. Call initialize() first.");
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Register a new channel handle (called by subclasses)
|
|
60
|
+
* @param {ChannelHandle} handle - Channel handle to register
|
|
61
|
+
* @protected
|
|
62
|
+
*/
|
|
63
|
+
_registerChannel(t) {
|
|
64
|
+
this.activeChannels.add(t);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Unregister a channel handle (called when channel is destroyed)
|
|
68
|
+
* @param {ChannelHandle} handle - Channel handle to unregister
|
|
69
|
+
* @protected
|
|
70
|
+
*/
|
|
71
|
+
_unregisterChannel(t) {
|
|
72
|
+
this.activeChannels.delete(t), this.channels.delete(t);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
class v {
|
|
76
|
+
constructor(t, e, s = {}) {
|
|
77
|
+
if (new.target === v)
|
|
78
|
+
throw new Error("ChannelHandle is abstract and cannot be instantiated directly");
|
|
79
|
+
this.engine = t, this.partId = e, this.options = { initialVolume: 1, ...s }, this.isDestroyed = !1, this.noteRefCounts = /* @__PURE__ */ new Map(), this.scheduledEvents = /* @__PURE__ */ new Map(), this.activeNotes = /* @__PURE__ */ new Set();
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get the output audio node for this channel
|
|
83
|
+
* This node can be connected to gain controls, analyzers, etc.
|
|
84
|
+
* @returns {AudioNode} Output node (typically a GainNode)
|
|
85
|
+
*/
|
|
86
|
+
getOutputNode() {
|
|
87
|
+
throw new Error("getOutputNode() must be implemented by subclass");
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Start a note with reference counting (handles overlaps automatically)
|
|
91
|
+
* @param {number} pitch - MIDI pitch (0-127)
|
|
92
|
+
* @param {number} velocity - Note velocity (0-127)
|
|
93
|
+
*/
|
|
94
|
+
noteOn(t, e) {
|
|
95
|
+
this._validateActive();
|
|
96
|
+
const s = this.noteRefCounts.get(t) || 0;
|
|
97
|
+
this.noteRefCounts.set(t, s + 1), this._actualNoteOn(t, e), s === 0 && this.activeNotes.add(t);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Stop a note with reference counting (only stops when count reaches 0)
|
|
101
|
+
* @param {number} pitch - MIDI pitch (0-127)
|
|
102
|
+
*/
|
|
103
|
+
noteOff(t) {
|
|
104
|
+
this._validateActive();
|
|
105
|
+
const e = this.noteRefCounts.get(t) || 0;
|
|
106
|
+
if (e <= 0)
|
|
107
|
+
return;
|
|
108
|
+
const s = e - 1;
|
|
109
|
+
this.noteRefCounts.set(t, s), s === 0 && (this._actualNoteOff(t), this.activeNotes.delete(t), this.noteRefCounts.delete(t));
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Schedule a note with automatic timing adjustment
|
|
113
|
+
* @param {number} startTime - Absolute audio context time when note should start
|
|
114
|
+
* @param {number} pitch - MIDI pitch (0-127)
|
|
115
|
+
* @param {number} velocity - Note velocity (0-127)
|
|
116
|
+
* @param {number} duration - Note duration in seconds
|
|
117
|
+
* @returns {string} Event ID for cancellation
|
|
118
|
+
*/
|
|
119
|
+
playNote(t, e, s, i) {
|
|
120
|
+
this._validateActive();
|
|
121
|
+
const r = this.engine.audioContext.currentTime, n = `${this.partId}_${t}_${e}_${Date.now()}`;
|
|
122
|
+
let l = t, o = i;
|
|
123
|
+
if (t < r) {
|
|
124
|
+
const f = r - t;
|
|
125
|
+
l = r, o = Math.max(0, i - f);
|
|
126
|
+
}
|
|
127
|
+
if (o <= 0)
|
|
128
|
+
return n;
|
|
129
|
+
const a = Math.max(0, (l - r) * 1e3), c = setTimeout(() => {
|
|
130
|
+
this.noteOn(e, s), this.scheduledEvents.delete(`${n}_on`);
|
|
131
|
+
}, a), h = a + o * 1e3, m = setTimeout(() => {
|
|
132
|
+
this.noteOff(e), this.scheduledEvents.delete(`${n}_off`);
|
|
133
|
+
}, h);
|
|
134
|
+
return this.scheduledEvents.set(`${n}_on`, c), this.scheduledEvents.set(`${n}_off`, m), n;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Stop all notes on this channel
|
|
138
|
+
*/
|
|
139
|
+
allNotesOff() {
|
|
140
|
+
this._validateActive(), this.scheduledEvents.forEach((t) => {
|
|
141
|
+
clearTimeout(t);
|
|
142
|
+
}), this.scheduledEvents.clear(), this.activeNotes.forEach((t) => {
|
|
143
|
+
this._actualNoteOff(t);
|
|
144
|
+
}), this.noteRefCounts.clear(), this.activeNotes.clear();
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Actually start a note (implemented by subclass)
|
|
148
|
+
* @param {number} pitch - MIDI pitch (0-127)
|
|
149
|
+
* @param {number} velocity - Note velocity (0-127)
|
|
150
|
+
* @protected
|
|
151
|
+
*/
|
|
152
|
+
_actualNoteOn(t, e) {
|
|
153
|
+
throw new Error("_actualNoteOn() must be implemented by subclass");
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Actually stop a note (implemented by subclass)
|
|
157
|
+
* @param {number} pitch - MIDI pitch (0-127)
|
|
158
|
+
* @protected
|
|
159
|
+
*/
|
|
160
|
+
_actualNoteOff(t) {
|
|
161
|
+
throw new Error("_actualNoteOff() must be implemented by subclass");
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Change the instrument for this channel
|
|
165
|
+
* @param {string|number} instrument - Instrument name or program number
|
|
166
|
+
* @returns {Promise<void>}
|
|
167
|
+
*/
|
|
168
|
+
async setInstrument(t) {
|
|
169
|
+
throw new Error("setInstrument() must be implemented by subclass");
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Get current instrument for this channel
|
|
173
|
+
* @returns {string|number} Current instrument
|
|
174
|
+
*/
|
|
175
|
+
getInstrument() {
|
|
176
|
+
throw new Error("getInstrument() must be implemented by subclass");
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Set volume for this channel (affects the internal channel volume)
|
|
180
|
+
* Note: External volume control should use the output node from getOutputNode()
|
|
181
|
+
* @param {number} volume - Volume level (0.0-1.0)
|
|
182
|
+
*/
|
|
183
|
+
setVolume(t) {
|
|
184
|
+
throw new Error("setVolume() must be implemented by subclass");
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Get current volume for this channel
|
|
188
|
+
* @returns {number} Current volume (0.0-1.0)
|
|
189
|
+
*/
|
|
190
|
+
getVolume() {
|
|
191
|
+
throw new Error("getVolume() must be implemented by subclass");
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Get the part ID for this channel
|
|
195
|
+
* @returns {string} Part identifier
|
|
196
|
+
*/
|
|
197
|
+
getPartId() {
|
|
198
|
+
return this.partId;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Check if this channel is still active
|
|
202
|
+
* @returns {boolean} True if channel is active
|
|
203
|
+
*/
|
|
204
|
+
isActive() {
|
|
205
|
+
return !this.isDestroyed && this.engine.isInitialized && this.engine.activeChannels.has(this);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Destroy this channel and clean up resources
|
|
209
|
+
*/
|
|
210
|
+
destroy() {
|
|
211
|
+
if (!this.isDestroyed) {
|
|
212
|
+
this.allNotesOff();
|
|
213
|
+
const t = this.getOutputNode();
|
|
214
|
+
t && t.disconnect(), this.noteRefCounts.clear(), this.scheduledEvents.clear(), this.activeNotes.clear(), this.engine._unregisterChannel(this), this.isDestroyed = !0;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Validate that this channel is still active
|
|
219
|
+
* @protected
|
|
220
|
+
*/
|
|
221
|
+
_validateActive() {
|
|
222
|
+
if (this.isDestroyed)
|
|
223
|
+
throw new Error("Channel has been destroyed");
|
|
224
|
+
if (!this.engine.isInitialized)
|
|
225
|
+
throw new Error("AudioEngine is not initialized");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
class P {
|
|
229
|
+
/**
|
|
230
|
+
* Map common instrument names to MIDI program numbers
|
|
231
|
+
* @param {string|number} instrument - Instrument name or program number
|
|
232
|
+
* @returns {number} MIDI program number
|
|
233
|
+
*/
|
|
234
|
+
static getInstrumentProgram(t) {
|
|
235
|
+
if (typeof t == "number") return t;
|
|
236
|
+
const s = {
|
|
237
|
+
// Piano family (0-7)
|
|
238
|
+
piano: 0,
|
|
239
|
+
bright_piano: 1,
|
|
240
|
+
electric_grand: 2,
|
|
241
|
+
honky_tonk: 3,
|
|
242
|
+
electric_piano_1: 4,
|
|
243
|
+
electric_piano_2: 5,
|
|
244
|
+
harpsichord: 6,
|
|
245
|
+
clavinet: 7,
|
|
246
|
+
// Chromatic percussion (8-15)
|
|
247
|
+
celesta: 8,
|
|
248
|
+
glockenspiel: 9,
|
|
249
|
+
music_box: 10,
|
|
250
|
+
vibraphone: 11,
|
|
251
|
+
marimba: 12,
|
|
252
|
+
xylophone: 13,
|
|
253
|
+
tubular_bells: 14,
|
|
254
|
+
dulcimer: 15,
|
|
255
|
+
// Organ (16-23)
|
|
256
|
+
drawbar_organ: 16,
|
|
257
|
+
percussive_organ: 17,
|
|
258
|
+
rock_organ: 18,
|
|
259
|
+
church_organ: 19,
|
|
260
|
+
reed_organ: 20,
|
|
261
|
+
accordion: 21,
|
|
262
|
+
harmonica: 22,
|
|
263
|
+
tango_accordion: 23,
|
|
264
|
+
organ: 19,
|
|
265
|
+
// Guitar (24-31)
|
|
266
|
+
nylon_guitar: 24,
|
|
267
|
+
steel_guitar: 25,
|
|
268
|
+
electric_guitar_jazz: 26,
|
|
269
|
+
electric_guitar_clean: 27,
|
|
270
|
+
electric_guitar_muted: 28,
|
|
271
|
+
overdriven_guitar: 29,
|
|
272
|
+
distortion_guitar: 30,
|
|
273
|
+
guitar_harmonics: 31,
|
|
274
|
+
guitar: 24,
|
|
275
|
+
// Bass (32-39)
|
|
276
|
+
acoustic_bass: 32,
|
|
277
|
+
electric_bass_finger: 33,
|
|
278
|
+
electric_bass_pick: 34,
|
|
279
|
+
fretless_bass: 35,
|
|
280
|
+
slap_bass_1: 36,
|
|
281
|
+
slap_bass_2: 37,
|
|
282
|
+
synth_bass_1: 38,
|
|
283
|
+
synth_bass_2: 39,
|
|
284
|
+
bass: 32,
|
|
285
|
+
// Strings (40-47)
|
|
286
|
+
violin: 40,
|
|
287
|
+
viola: 41,
|
|
288
|
+
cello: 42,
|
|
289
|
+
contrabass: 43,
|
|
290
|
+
tremolo_strings: 44,
|
|
291
|
+
pizzicato_strings: 45,
|
|
292
|
+
orchestral_harp: 46,
|
|
293
|
+
timpani: 47,
|
|
294
|
+
strings: 48,
|
|
295
|
+
strings_ensemble: 48,
|
|
296
|
+
// Ensemble (48-55)
|
|
297
|
+
slow_strings: 49,
|
|
298
|
+
synth_strings_1: 50,
|
|
299
|
+
synth_strings_2: 51,
|
|
300
|
+
choir_aahs: 52,
|
|
301
|
+
voice_oohs: 53,
|
|
302
|
+
synth_voice: 54,
|
|
303
|
+
orchestra_hit: 55,
|
|
304
|
+
// Brass (56-63)
|
|
305
|
+
trumpet: 56,
|
|
306
|
+
trombone: 57,
|
|
307
|
+
tuba: 58,
|
|
308
|
+
muted_trumpet: 59,
|
|
309
|
+
french_horn: 60,
|
|
310
|
+
brass_section: 61,
|
|
311
|
+
synth_brass_1: 62,
|
|
312
|
+
synth_brass_2: 63,
|
|
313
|
+
// Reed (64-71)
|
|
314
|
+
soprano_sax: 64,
|
|
315
|
+
alto_sax: 65,
|
|
316
|
+
tenor_sax: 66,
|
|
317
|
+
baritone_sax: 67,
|
|
318
|
+
oboe: 68,
|
|
319
|
+
english_horn: 69,
|
|
320
|
+
bassoon: 70,
|
|
321
|
+
clarinet: 71,
|
|
322
|
+
saxophone: 64,
|
|
323
|
+
// Pipe (72-79)
|
|
324
|
+
piccolo: 72,
|
|
325
|
+
flute: 73,
|
|
326
|
+
recorder: 74,
|
|
327
|
+
pan_flute: 75,
|
|
328
|
+
blown_bottle: 76,
|
|
329
|
+
shakuhachi: 77,
|
|
330
|
+
whistle: 78,
|
|
331
|
+
ocarina: 79,
|
|
332
|
+
// Synth lead (80-87)
|
|
333
|
+
lead_1_square: 80,
|
|
334
|
+
lead_2_sawtooth: 81,
|
|
335
|
+
lead_3_calliope: 82,
|
|
336
|
+
lead_4_chiff: 83,
|
|
337
|
+
lead_5_charang: 84,
|
|
338
|
+
lead_6_voice: 85,
|
|
339
|
+
lead_7_fifths: 86,
|
|
340
|
+
lead_8_bass: 87,
|
|
341
|
+
// Synth pad (88-95)
|
|
342
|
+
pad_1_new_age: 88,
|
|
343
|
+
pad_2_warm: 89,
|
|
344
|
+
pad_3_polysynth: 90,
|
|
345
|
+
pad_4_choir: 91,
|
|
346
|
+
pad_5_bowed: 92,
|
|
347
|
+
pad_6_metallic: 93,
|
|
348
|
+
pad_7_halo: 94,
|
|
349
|
+
pad_8_sweep: 95,
|
|
350
|
+
// Synth effects (96-103)
|
|
351
|
+
fx_1_rain: 96,
|
|
352
|
+
fx_2_soundtrack: 97,
|
|
353
|
+
fx_3_crystal: 98,
|
|
354
|
+
fx_4_atmosphere: 99,
|
|
355
|
+
fx_5_brightness: 100,
|
|
356
|
+
fx_6_goblins: 101,
|
|
357
|
+
fx_7_echoes: 102,
|
|
358
|
+
fx_8_sci_fi: 103,
|
|
359
|
+
// Ethnic (104-111)
|
|
360
|
+
sitar: 104,
|
|
361
|
+
banjo: 105,
|
|
362
|
+
shamisen: 106,
|
|
363
|
+
koto: 107,
|
|
364
|
+
kalimba: 108,
|
|
365
|
+
bag_pipe: 109,
|
|
366
|
+
fiddle: 110,
|
|
367
|
+
shanai: 111,
|
|
368
|
+
// Percussive (112-119)
|
|
369
|
+
tinkle_bell: 112,
|
|
370
|
+
agogo: 113,
|
|
371
|
+
steel_drums: 114,
|
|
372
|
+
woodblock: 115,
|
|
373
|
+
taiko_drum: 116,
|
|
374
|
+
melodic_tom: 117,
|
|
375
|
+
synth_drum: 118,
|
|
376
|
+
reverse_cymbal: 119,
|
|
377
|
+
// Sound effects (120-127)
|
|
378
|
+
guitar_fret_noise: 120,
|
|
379
|
+
breath_noise: 121,
|
|
380
|
+
seashore: 122,
|
|
381
|
+
bird_tweet: 123,
|
|
382
|
+
telephone_ring: 124,
|
|
383
|
+
helicopter: 125,
|
|
384
|
+
applause: 126,
|
|
385
|
+
gunshot: 127
|
|
386
|
+
}[t.toLowerCase()];
|
|
387
|
+
return s !== void 0 ? s : 0;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Get instrument name from MIDI program number (for display purposes)
|
|
391
|
+
* @param {number} programNumber - MIDI program number (0-127)
|
|
392
|
+
* @returns {string} Instrument name or fallback
|
|
393
|
+
*/
|
|
394
|
+
static getProgramName(t) {
|
|
395
|
+
return [
|
|
396
|
+
// Piano family (0-7)
|
|
397
|
+
"Piano",
|
|
398
|
+
"Bright Piano",
|
|
399
|
+
"Electric Grand",
|
|
400
|
+
"Honky-tonk Piano",
|
|
401
|
+
"Electric Piano 1",
|
|
402
|
+
"Electric Piano 2",
|
|
403
|
+
"Harpsichord",
|
|
404
|
+
"Clavinet",
|
|
405
|
+
// Chromatic percussion (8-15)
|
|
406
|
+
"Celesta",
|
|
407
|
+
"Glockenspiel",
|
|
408
|
+
"Music Box",
|
|
409
|
+
"Vibraphone",
|
|
410
|
+
"Marimba",
|
|
411
|
+
"Xylophone",
|
|
412
|
+
"Tubular Bells",
|
|
413
|
+
"Dulcimer",
|
|
414
|
+
// Organ (16-23)
|
|
415
|
+
"Drawbar Organ",
|
|
416
|
+
"Percussive Organ",
|
|
417
|
+
"Rock Organ",
|
|
418
|
+
"Church Organ",
|
|
419
|
+
"Reed Organ",
|
|
420
|
+
"Accordion",
|
|
421
|
+
"Harmonica",
|
|
422
|
+
"Tango Accordion",
|
|
423
|
+
// Guitar (24-31)
|
|
424
|
+
"Nylon Guitar",
|
|
425
|
+
"Steel Guitar",
|
|
426
|
+
"Electric Guitar (jazz)",
|
|
427
|
+
"Electric Guitar (clean)",
|
|
428
|
+
"Electric Guitar (muted)",
|
|
429
|
+
"Overdriven Guitar",
|
|
430
|
+
"Distortion Guitar",
|
|
431
|
+
"Guitar Harmonics",
|
|
432
|
+
// Bass (32-39)
|
|
433
|
+
"Acoustic Bass",
|
|
434
|
+
"Electric Bass (finger)",
|
|
435
|
+
"Electric Bass (pick)",
|
|
436
|
+
"Fretless Bass",
|
|
437
|
+
"Slap Bass 1",
|
|
438
|
+
"Slap Bass 2",
|
|
439
|
+
"Synth Bass 1",
|
|
440
|
+
"Synth Bass 2",
|
|
441
|
+
// Strings (40-47)
|
|
442
|
+
"Violin",
|
|
443
|
+
"Viola",
|
|
444
|
+
"Cello",
|
|
445
|
+
"Contrabass",
|
|
446
|
+
"Tremolo Strings",
|
|
447
|
+
"Pizzicato Strings",
|
|
448
|
+
"Orchestral Harp",
|
|
449
|
+
"Timpani",
|
|
450
|
+
// Ensemble (48-55)
|
|
451
|
+
"String Ensemble 1",
|
|
452
|
+
"String Ensemble 2",
|
|
453
|
+
"Synth Strings 1",
|
|
454
|
+
"Synth Strings 2",
|
|
455
|
+
"Choir Aahs",
|
|
456
|
+
"Voice Oohs",
|
|
457
|
+
"Synth Voice",
|
|
458
|
+
"Orchestra Hit",
|
|
459
|
+
// Brass (56-63)
|
|
460
|
+
"Trumpet",
|
|
461
|
+
"Trombone",
|
|
462
|
+
"Tuba",
|
|
463
|
+
"Muted Trumpet",
|
|
464
|
+
"French Horn",
|
|
465
|
+
"Brass Section",
|
|
466
|
+
"Synth Brass 1",
|
|
467
|
+
"Synth Brass 2",
|
|
468
|
+
// Reed (64-71)
|
|
469
|
+
"Soprano Sax",
|
|
470
|
+
"Alto Sax",
|
|
471
|
+
"Tenor Sax",
|
|
472
|
+
"Baritone Sax",
|
|
473
|
+
"Oboe",
|
|
474
|
+
"English Horn",
|
|
475
|
+
"Bassoon",
|
|
476
|
+
"Clarinet",
|
|
477
|
+
// Pipe (72-79)
|
|
478
|
+
"Piccolo",
|
|
479
|
+
"Flute",
|
|
480
|
+
"Recorder",
|
|
481
|
+
"Pan Flute",
|
|
482
|
+
"Blown Bottle",
|
|
483
|
+
"Shakuhachi",
|
|
484
|
+
"Whistle",
|
|
485
|
+
"Ocarina",
|
|
486
|
+
// Synth lead (80-87)
|
|
487
|
+
"Lead 1 (square)",
|
|
488
|
+
"Lead 2 (sawtooth)",
|
|
489
|
+
"Lead 3 (calliope)",
|
|
490
|
+
"Lead 4 (chiff)",
|
|
491
|
+
"Lead 5 (charang)",
|
|
492
|
+
"Lead 6 (voice)",
|
|
493
|
+
"Lead 7 (fifths)",
|
|
494
|
+
"Lead 8 (bass + lead)",
|
|
495
|
+
// Synth pad (88-95)
|
|
496
|
+
"Pad 1 (new age)",
|
|
497
|
+
"Pad 2 (warm)",
|
|
498
|
+
"Pad 3 (polysynth)",
|
|
499
|
+
"Pad 4 (choir)",
|
|
500
|
+
"Pad 5 (bowed)",
|
|
501
|
+
"Pad 6 (metallic)",
|
|
502
|
+
"Pad 7 (halo)",
|
|
503
|
+
"Pad 8 (sweep)",
|
|
504
|
+
// Synth effects (96-103)
|
|
505
|
+
"FX 1 (rain)",
|
|
506
|
+
"FX 2 (soundtrack)",
|
|
507
|
+
"FX 3 (crystal)",
|
|
508
|
+
"FX 4 (atmosphere)",
|
|
509
|
+
"FX 5 (brightness)",
|
|
510
|
+
"FX 6 (goblins)",
|
|
511
|
+
"FX 7 (echoes)",
|
|
512
|
+
"FX 8 (sci-fi)",
|
|
513
|
+
// Ethnic (104-111)
|
|
514
|
+
"Sitar",
|
|
515
|
+
"Banjo",
|
|
516
|
+
"Shamisen",
|
|
517
|
+
"Koto",
|
|
518
|
+
"Kalimba",
|
|
519
|
+
"Bag pipe",
|
|
520
|
+
"Fiddle",
|
|
521
|
+
"Shanai",
|
|
522
|
+
// Percussive (112-119)
|
|
523
|
+
"Tinkle Bell",
|
|
524
|
+
"Agogo",
|
|
525
|
+
"Steel Drums",
|
|
526
|
+
"Woodblock",
|
|
527
|
+
"Taiko Drum",
|
|
528
|
+
"Melodic Tom",
|
|
529
|
+
"Synth Drum",
|
|
530
|
+
"Reverse Cymbal",
|
|
531
|
+
// Sound effects (120-127)
|
|
532
|
+
"Guitar Fret Noise",
|
|
533
|
+
"Breath Noise",
|
|
534
|
+
"Seashore",
|
|
535
|
+
"Bird Tweet",
|
|
536
|
+
"Telephone Ring",
|
|
537
|
+
"Helicopter",
|
|
538
|
+
"Applause",
|
|
539
|
+
"Gunshot"
|
|
540
|
+
][t] || `Program ${t}`;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Generate a unique note ID
|
|
544
|
+
* @param {number} channel - MIDI channel
|
|
545
|
+
* @param {number} pitch - MIDI pitch
|
|
546
|
+
* @param {number} startTime - Start time
|
|
547
|
+
* @returns {string} Unique note ID
|
|
548
|
+
*/
|
|
549
|
+
static generateNoteId(t, e, s) {
|
|
550
|
+
return `${t}_${e}_${Math.round(s)}`;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
class x extends v {
|
|
554
|
+
constructor(t, e, s, i = {}) {
|
|
555
|
+
super(t, e, i), this.midiChannel = s, this.currentVolume = i.initialVolume || 1, this.currentInstrument = i.instrument || "piano", this.outputGain = null, this._setupOutputNode(), this.setVolume(this.currentVolume), i.instrument && this.setInstrument(i.instrument);
|
|
556
|
+
}
|
|
557
|
+
getOutputNode() {
|
|
558
|
+
return this.outputGain;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Actually start a note in the synthesizer
|
|
562
|
+
* @param {number} pitch - MIDI pitch (0-127)
|
|
563
|
+
* @param {number} velocity - Note velocity (0-127)
|
|
564
|
+
* @protected
|
|
565
|
+
*/
|
|
566
|
+
_actualNoteOn(t, e) {
|
|
567
|
+
const s = this.engine._getSynthesizer();
|
|
568
|
+
if (s && s.noteOn) {
|
|
569
|
+
const i = Math.round(e * this.currentVolume);
|
|
570
|
+
s.noteOn(this.midiChannel, t, i);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Actually stop a note in the synthesizer
|
|
575
|
+
* @param {number} pitch - MIDI pitch (0-127)
|
|
576
|
+
* @protected
|
|
577
|
+
*/
|
|
578
|
+
_actualNoteOff(t) {
|
|
579
|
+
const e = this.engine._getSynthesizer();
|
|
580
|
+
e && e.noteOff && e.noteOff(this.midiChannel, t);
|
|
581
|
+
}
|
|
582
|
+
async setInstrument(t) {
|
|
583
|
+
this._validateActive();
|
|
584
|
+
const e = P.getInstrumentProgram(t);
|
|
585
|
+
this.currentInstrument = t;
|
|
586
|
+
const s = this.engine._getSynthesizer();
|
|
587
|
+
s && s.programChange ? s.programChange(this.midiChannel, e) : console.warn("Cannot set instrument: synthesizer not available or no programChange method");
|
|
588
|
+
}
|
|
589
|
+
getInstrument() {
|
|
590
|
+
return this.currentInstrument;
|
|
591
|
+
}
|
|
592
|
+
setVolume(t) {
|
|
593
|
+
this._validateActive(), t = Math.max(0, Math.min(1, t)), this.currentVolume = t;
|
|
594
|
+
const e = Math.round(t * 127), s = this.engine._getSynthesizer();
|
|
595
|
+
s && s.controllerChange && s.controllerChange(this.midiChannel, 7, e);
|
|
596
|
+
}
|
|
597
|
+
getVolume() {
|
|
598
|
+
return this.currentVolume;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Get the MIDI channel number used by this handle
|
|
602
|
+
* @returns {number} MIDI channel (0-15)
|
|
603
|
+
*/
|
|
604
|
+
getMidiChannel() {
|
|
605
|
+
return this.midiChannel;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Get count of active notes
|
|
609
|
+
* @returns {number} Number of active notes
|
|
610
|
+
*/
|
|
611
|
+
getActiveNoteCount() {
|
|
612
|
+
return this.activeNotes.size;
|
|
613
|
+
}
|
|
614
|
+
destroy() {
|
|
615
|
+
if (!this.isDestroyed) {
|
|
616
|
+
const t = this.engine._getIndividualOutput(this.midiChannel);
|
|
617
|
+
this.outputGain && this.outputGain !== t && this.outputGain.disconnect(), this.outputGain = null;
|
|
618
|
+
}
|
|
619
|
+
super.destroy();
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Set up the output gain node for this channel
|
|
623
|
+
* This connects to the individual synthesizer output for this MIDI channel
|
|
624
|
+
* @private
|
|
625
|
+
*/
|
|
626
|
+
_setupOutputNode() {
|
|
627
|
+
const t = this.engine._getIndividualOutput(this.midiChannel);
|
|
628
|
+
t ? this.outputGain = t : (console.warn(`No individual output available for MIDI channel ${this.midiChannel}, using fallback`), this.outputGain = this.engine.audioContext.createGain(), this.outputGain.gain.value = this.currentVolume);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
class B extends k {
|
|
632
|
+
constructor(t, e = {}) {
|
|
633
|
+
super(t, e), this.synthesizer = null, this.soundfont = null, this.channelCounter = 0, this.partToMidiChannel = /* @__PURE__ */ new Map(), this.midiChannelToPart = /* @__PURE__ */ new Map(), this.individualOutputs = [];
|
|
634
|
+
}
|
|
635
|
+
async initialize(t) {
|
|
636
|
+
try {
|
|
637
|
+
const { Synthetizer: e } = await import("spessasynth_lib");
|
|
638
|
+
let s;
|
|
639
|
+
if (typeof t == "string")
|
|
640
|
+
console.log("Loading soundfont from path:", t), s = await this._loadSoundfontFromPath(t), console.log("Soundfont loaded successfully, size:", s.byteLength, "bytes");
|
|
641
|
+
else if (t instanceof ArrayBuffer)
|
|
642
|
+
s = t;
|
|
643
|
+
else
|
|
644
|
+
throw new Error("Invalid soundfont data type. Expected string path or ArrayBuffer.");
|
|
645
|
+
await this._loadAudioWorkletSafely(), this._setupIndividualOutputs(), this.dummyTarget = this.audioContext.createGain(), await new Promise((i) => setTimeout(i, 50)), this.synthesizer = new e(this.dummyTarget, s), this._connectIndividualOutputs(), this.isInitialized = !0;
|
|
646
|
+
} catch (e) {
|
|
647
|
+
throw console.error("Failed to initialize SpessaSynthAudioEngine:", e), e;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
createChannel(t, e = {}) {
|
|
651
|
+
if (this._validateInitialized(), this.partToMidiChannel.has(t))
|
|
652
|
+
throw new Error(`Channel for part '${t}' already exists`);
|
|
653
|
+
const s = this.channelCounter;
|
|
654
|
+
if (s >= 16)
|
|
655
|
+
throw new Error("Maximum number of MIDI channels (16) exceeded");
|
|
656
|
+
this.channelCounter++, this.partToMidiChannel.set(t, s), this.midiChannelToPart.set(s, t);
|
|
657
|
+
const i = new x(
|
|
658
|
+
this,
|
|
659
|
+
t,
|
|
660
|
+
s,
|
|
661
|
+
e
|
|
662
|
+
);
|
|
663
|
+
return this._registerChannel(i), e.instrument && i.setInstrument(e.instrument), i;
|
|
664
|
+
}
|
|
665
|
+
allSoundsOff() {
|
|
666
|
+
this.synthesizer && this.midiChannelToPart.forEach((t, e) => {
|
|
667
|
+
this.synthesizer.controllerChange && this.synthesizer.controllerChange(e, 120, 0);
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Clear all channels (for loading new scores)
|
|
672
|
+
*/
|
|
673
|
+
clearAllChannels() {
|
|
674
|
+
this.allSoundsOff(), this.partToMidiChannel.clear(), this.midiChannelToPart.clear(), this.channelCounter = 0;
|
|
675
|
+
}
|
|
676
|
+
destroy() {
|
|
677
|
+
this.allSoundsOff(), this.synthesizer && typeof this.synthesizer.disconnect == "function" && this.synthesizer.disconnect(), this.individualOutputs.forEach((t) => {
|
|
678
|
+
t && t.disconnect && t.disconnect();
|
|
679
|
+
}), this.individualOutputs = [], this.dummyTarget && (this.dummyTarget.disconnect(), this.dummyTarget = null), this.partToMidiChannel.clear(), this.midiChannelToPart.clear(), this.channelCounter = 0, super.destroy(), this.synthesizer = null, this.soundfont = null;
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Get the MIDI channel number for a part
|
|
683
|
+
* @param {string} partId - Part identifier
|
|
684
|
+
* @returns {number|null} MIDI channel number or null if not found
|
|
685
|
+
*/
|
|
686
|
+
getMidiChannelForPart(t) {
|
|
687
|
+
return this.partToMidiChannel.has(t) ? this.partToMidiChannel.get(t) : null;
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Get access to the underlying synthesizer for channel handles
|
|
691
|
+
* @returns {Object} SpessaSynth synthesizer instance
|
|
692
|
+
* @protected
|
|
693
|
+
*/
|
|
694
|
+
_getSynthesizer() {
|
|
695
|
+
return this.synthesizer;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Get the individual output node for a specific MIDI channel
|
|
699
|
+
* @param {number} midiChannel - MIDI channel number (0-15)
|
|
700
|
+
* @returns {AudioNode|null} Individual output node or null if not available
|
|
701
|
+
* @protected
|
|
702
|
+
*/
|
|
703
|
+
_getIndividualOutput(t) {
|
|
704
|
+
return t >= 0 && t < this.individualOutputs.length ? this.individualOutputs[t] : null;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Set up individual output nodes for each MIDI channel
|
|
708
|
+
* @private
|
|
709
|
+
*/
|
|
710
|
+
_setupIndividualOutputs() {
|
|
711
|
+
this.individualOutputs = [];
|
|
712
|
+
for (let t = 0; t < 16; t++) {
|
|
713
|
+
const e = this.audioContext.createGain();
|
|
714
|
+
e.gain.value = 1, this.individualOutputs.push(e);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Connect synthesizer individual outputs to channel-specific gain nodes
|
|
719
|
+
* @private
|
|
720
|
+
*/
|
|
721
|
+
_connectIndividualOutputs() {
|
|
722
|
+
try {
|
|
723
|
+
this.synthesizer && this.synthesizer.connectIndividualOutputs ? this.synthesizer.connectIndividualOutputs(this.individualOutputs) : console.warn("Synthesizer does not support individual outputs, using master output only");
|
|
724
|
+
} catch (t) {
|
|
725
|
+
console.warn("Failed to connect individual outputs:", t.message), console.warn("Falling back to master output routing");
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Load soundfont from file path or URL
|
|
730
|
+
* @param {string} path - Path to soundfont file
|
|
731
|
+
* @returns {Promise<ArrayBuffer>} Soundfont data
|
|
732
|
+
* @private
|
|
733
|
+
*/
|
|
734
|
+
async _loadSoundfontFromPath(t) {
|
|
735
|
+
if (typeof window < "u") {
|
|
736
|
+
const e = await fetch(t);
|
|
737
|
+
if (!e.ok)
|
|
738
|
+
throw new Error(`Failed to load soundfont: ${e.status} ${e.statusText}`);
|
|
739
|
+
return await e.arrayBuffer();
|
|
740
|
+
} else {
|
|
741
|
+
const e = await Promise.resolve().then(() => C), s = await Promise.resolve().then(() => C), i = s.isAbsolute(t) ? t : s.resolve(process.cwd(), t);
|
|
742
|
+
return e.readFileSync(i).buffer;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Load AudioWorklet with aggressive Firefox compatibility measures
|
|
747
|
+
* @private
|
|
748
|
+
*/
|
|
749
|
+
async _loadAudioWorkletSafely() {
|
|
750
|
+
const t = "/node_modules/spessasynth_lib/synthetizer/worklet_processor.min.js", s = typeof navigator < "u" && navigator.userAgent.toLowerCase().includes("firefox");
|
|
751
|
+
for (let i = 1; i <= 5; i++)
|
|
752
|
+
try {
|
|
753
|
+
if (s) {
|
|
754
|
+
const r = i * 200;
|
|
755
|
+
await new Promise((n) => setTimeout(n, r)), "gc" in window && typeof window.gc == "function" && window.gc(), this.audioContext.state === "suspended" && await this.audioContext.resume();
|
|
756
|
+
}
|
|
757
|
+
console.log(`Attempting to load AudioWorklet (attempt ${i}/5)`), await this.audioContext.audioWorklet.addModule(t), console.log(`AudioWorklet loaded successfully on attempt ${i}`);
|
|
758
|
+
return;
|
|
759
|
+
} catch (r) {
|
|
760
|
+
if (console.warn(`AudioWorklet loading failed (attempt ${i}/5):`, r.message), i === 5)
|
|
761
|
+
throw new Error(`AudioWorklet failed after 5 attempts: ${r.message}`);
|
|
762
|
+
const n = s ? i * 1e3 : i * 500;
|
|
763
|
+
await new Promise((l) => setTimeout(l, n));
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
class E {
|
|
768
|
+
constructor() {
|
|
769
|
+
this.partNames = [
|
|
770
|
+
"soprano",
|
|
771
|
+
"alto",
|
|
772
|
+
"tenor",
|
|
773
|
+
"bass",
|
|
774
|
+
"treble",
|
|
775
|
+
"mezzo",
|
|
776
|
+
"baritone",
|
|
777
|
+
"s",
|
|
778
|
+
"a",
|
|
779
|
+
"t",
|
|
780
|
+
"b",
|
|
781
|
+
"satb"
|
|
782
|
+
], this.parsedData = {
|
|
783
|
+
parts: {},
|
|
784
|
+
// Will contain separate arrays for each vocal part
|
|
785
|
+
barStructure: [],
|
|
786
|
+
// Will contain bar timing information
|
|
787
|
+
metadata: {}
|
|
788
|
+
// Will contain title, composer, etc.
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Main method to parse a MIDI file
|
|
793
|
+
* @param {ArrayBuffer} midiFileBuffer - The MIDI file as an ArrayBuffer
|
|
794
|
+
* @returns {Object} Parsed data with parts, barStructure and metadata
|
|
795
|
+
*/
|
|
796
|
+
async parse(t) {
|
|
797
|
+
try {
|
|
798
|
+
const e = await this._parseMidiBuffer(t);
|
|
799
|
+
return this._extractMetadata(e), this._extractBarStructure(e), this._extractParts(e), this.parsedData;
|
|
800
|
+
} catch (e) {
|
|
801
|
+
throw console.error("Error parsing MIDI file:", e), e;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Parse the MIDI buffer into a workable format
|
|
806
|
+
* @private
|
|
807
|
+
*/
|
|
808
|
+
async _parseMidiBuffer(t) {
|
|
809
|
+
const e = new Uint8Array(t);
|
|
810
|
+
if (!(e[0] === 77 && e[1] === 84 && e[2] === 104 && e[3] === 100))
|
|
811
|
+
throw new Error("Not a valid MIDI file");
|
|
812
|
+
const s = this._bytesToNumber(e.slice(4, 8)), i = this._bytesToNumber(e.slice(8, 10)), r = this._bytesToNumber(e.slice(10, 12)), n = this._bytesToNumber(e.slice(12, 14)), l = n & 32768 ? null : n, o = {
|
|
813
|
+
format: i,
|
|
814
|
+
ticksPerBeat: l,
|
|
815
|
+
tracks: [],
|
|
816
|
+
duration: 0
|
|
817
|
+
};
|
|
818
|
+
let a = 8 + s;
|
|
819
|
+
for (let c = 0; c < r; c++)
|
|
820
|
+
if (e[a] === 77 && e[a + 1] === 84 && e[a + 2] === 114 && e[a + 3] === 107) {
|
|
821
|
+
const h = this._bytesToNumber(e.slice(a + 4, a + 8)), m = e.slice(a + 8, a + 8 + h), f = this._parseTrack(m);
|
|
822
|
+
o.tracks.push(f), a += 8 + h;
|
|
823
|
+
} else
|
|
824
|
+
throw new Error(`Invalid track header at position ${a}`);
|
|
825
|
+
return o;
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Parse a single MIDI track
|
|
829
|
+
* @private
|
|
830
|
+
*/
|
|
831
|
+
_parseTrack(t) {
|
|
832
|
+
const e = {
|
|
833
|
+
notes: [],
|
|
834
|
+
name: null,
|
|
835
|
+
lyrics: [],
|
|
836
|
+
events: [],
|
|
837
|
+
duration: 0
|
|
838
|
+
};
|
|
839
|
+
let s = 0, i = 0, r = null;
|
|
840
|
+
for (; s < t.length; ) {
|
|
841
|
+
let n = 0, l = 0;
|
|
842
|
+
do
|
|
843
|
+
l = t[s++], n = n << 7 | l & 127;
|
|
844
|
+
while (l & 128);
|
|
845
|
+
i += n, l = t[s++];
|
|
846
|
+
let o = l;
|
|
847
|
+
if ((l & 128) === 0) {
|
|
848
|
+
if (r === null)
|
|
849
|
+
throw new Error("Running status byte encountered before status byte");
|
|
850
|
+
o = r, s--;
|
|
851
|
+
} else
|
|
852
|
+
r = o;
|
|
853
|
+
if (o === 255) {
|
|
854
|
+
const a = t[s++], c = this._readVariableLengthValue(t, s);
|
|
855
|
+
s += c.bytesRead;
|
|
856
|
+
const h = t.slice(s, s + c.value);
|
|
857
|
+
switch (s += c.value, a) {
|
|
858
|
+
case 3:
|
|
859
|
+
e.name = this._bytesToString(h);
|
|
860
|
+
break;
|
|
861
|
+
case 1:
|
|
862
|
+
e.events.push({
|
|
863
|
+
type: "text",
|
|
864
|
+
text: this._bytesToString(h),
|
|
865
|
+
tick: i
|
|
866
|
+
});
|
|
867
|
+
break;
|
|
868
|
+
case 5:
|
|
869
|
+
e.lyrics.push({
|
|
870
|
+
text: this._bytesToString(h),
|
|
871
|
+
tick: i
|
|
872
|
+
});
|
|
873
|
+
break;
|
|
874
|
+
case 81:
|
|
875
|
+
const m = this._bytesToNumber(h), f = Math.round(6e7 / m);
|
|
876
|
+
e.events.push({
|
|
877
|
+
type: "tempo",
|
|
878
|
+
bpm: f,
|
|
879
|
+
tick: i
|
|
880
|
+
});
|
|
881
|
+
break;
|
|
882
|
+
case 88:
|
|
883
|
+
e.events.push({
|
|
884
|
+
type: "timeSignature",
|
|
885
|
+
numerator: h[0],
|
|
886
|
+
denominator: Math.pow(2, h[1]),
|
|
887
|
+
tick: i
|
|
888
|
+
});
|
|
889
|
+
break;
|
|
890
|
+
case 47:
|
|
891
|
+
e.duration = i;
|
|
892
|
+
break;
|
|
893
|
+
}
|
|
894
|
+
} else if ((o & 240) === 144) {
|
|
895
|
+
const a = o & 15, c = t[s++], h = t[s++];
|
|
896
|
+
h > 0 ? e.notes.push({
|
|
897
|
+
type: "noteOn",
|
|
898
|
+
noteNumber: c,
|
|
899
|
+
velocity: h,
|
|
900
|
+
tick: i,
|
|
901
|
+
channel: a
|
|
902
|
+
}) : e.notes.push({
|
|
903
|
+
type: "noteOff",
|
|
904
|
+
noteNumber: c,
|
|
905
|
+
tick: i,
|
|
906
|
+
channel: a
|
|
907
|
+
});
|
|
908
|
+
} else if ((o & 240) === 128) {
|
|
909
|
+
const a = o & 15, c = t[s++];
|
|
910
|
+
t[s++], e.notes.push({
|
|
911
|
+
type: "noteOff",
|
|
912
|
+
noteNumber: c,
|
|
913
|
+
tick: i,
|
|
914
|
+
channel: a
|
|
915
|
+
});
|
|
916
|
+
} else if (o === 240 || o === 247) {
|
|
917
|
+
const a = this._readVariableLengthValue(t, s);
|
|
918
|
+
s += a.bytesRead + a.value;
|
|
919
|
+
} else if ((o & 240) === 176) {
|
|
920
|
+
const a = o & 15, c = t[s++], h = t[s++];
|
|
921
|
+
e.events.push({
|
|
922
|
+
type: "controller",
|
|
923
|
+
controllerNumber: c,
|
|
924
|
+
value: h,
|
|
925
|
+
channel: a,
|
|
926
|
+
tick: i
|
|
927
|
+
});
|
|
928
|
+
} else if ((o & 240) === 192) {
|
|
929
|
+
const a = o & 15, c = t[s++];
|
|
930
|
+
e.events.push({
|
|
931
|
+
type: "programChange",
|
|
932
|
+
programNumber: c,
|
|
933
|
+
channel: a,
|
|
934
|
+
tick: i
|
|
935
|
+
});
|
|
936
|
+
} else if ((o & 240) === 208) {
|
|
937
|
+
const a = o & 15, c = t[s++];
|
|
938
|
+
e.events.push({
|
|
939
|
+
type: "channelAftertouch",
|
|
940
|
+
pressure: c,
|
|
941
|
+
channel: a,
|
|
942
|
+
tick: i
|
|
943
|
+
});
|
|
944
|
+
} else if ((o & 240) === 224) {
|
|
945
|
+
const a = o & 15, c = t[s++], m = (t[s++] << 7 | c) - 8192;
|
|
946
|
+
e.events.push({
|
|
947
|
+
type: "pitchBend",
|
|
948
|
+
value: m,
|
|
949
|
+
channel: a,
|
|
950
|
+
tick: i
|
|
951
|
+
});
|
|
952
|
+
} else if ((o & 240) === 160) {
|
|
953
|
+
const a = o & 15, c = t[s++], h = t[s++];
|
|
954
|
+
e.events.push({
|
|
955
|
+
type: "noteAftertouch",
|
|
956
|
+
noteNumber: c,
|
|
957
|
+
pressure: h,
|
|
958
|
+
channel: a,
|
|
959
|
+
tick: i
|
|
960
|
+
});
|
|
961
|
+
} else
|
|
962
|
+
console.warn(`Unknown event type: ${o.toString(16)} at position ${s - 1}`), s++;
|
|
963
|
+
}
|
|
964
|
+
return e;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Extract metadata from the MIDI object
|
|
968
|
+
* @private
|
|
969
|
+
*/
|
|
970
|
+
_extractMetadata(t) {
|
|
971
|
+
const e = {
|
|
972
|
+
title: null,
|
|
973
|
+
composer: null,
|
|
974
|
+
partNames: [],
|
|
975
|
+
format: t.format,
|
|
976
|
+
ticksPerBeat: t.ticksPerBeat
|
|
977
|
+
};
|
|
978
|
+
t.tracks.forEach((s, i) => {
|
|
979
|
+
if (s.name && !e.title && (e.title = s.name), s.events.filter((r) => r.type === "text").forEach((r) => {
|
|
980
|
+
const n = r.text.toLowerCase();
|
|
981
|
+
(n.includes("compos") || n.includes("by")) && !e.composer && (e.composer = r.text);
|
|
982
|
+
}), s.name) {
|
|
983
|
+
const r = s.name.toLowerCase();
|
|
984
|
+
for (const n of this.partNames)
|
|
985
|
+
if (r.includes(n)) {
|
|
986
|
+
e.partNames.push({
|
|
987
|
+
index: i,
|
|
988
|
+
name: s.name
|
|
989
|
+
});
|
|
990
|
+
break;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}), this.parsedData.metadata = e;
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Extract bar structure (time signatures, tempo changes)
|
|
997
|
+
* New format: {"sig": [numerator, denominator], "bpm": tempo_or_array}
|
|
998
|
+
* @private
|
|
999
|
+
*/
|
|
1000
|
+
_extractBarStructure(t) {
|
|
1001
|
+
const e = t.ticksPerBeat || 480, s = [];
|
|
1002
|
+
t.tracks.forEach((h) => {
|
|
1003
|
+
h.events.forEach((m) => {
|
|
1004
|
+
(m.type === "timeSignature" || m.type === "tempo") && s.push(m);
|
|
1005
|
+
});
|
|
1006
|
+
}), s.sort((h, m) => h.tick - m.tick);
|
|
1007
|
+
let i = 0;
|
|
1008
|
+
t.tracks.forEach((h) => {
|
|
1009
|
+
h.notes && h.notes.forEach((m) => {
|
|
1010
|
+
m.type === "noteOff" && m.tick > i && (i = m.tick);
|
|
1011
|
+
});
|
|
1012
|
+
}), i === 0 && (i = e * 8);
|
|
1013
|
+
const r = [], n = s.filter((h) => h.type === "timeSignature").sort((h, m) => h.tick - m.tick);
|
|
1014
|
+
let l = { numerator: 4, denominator: 4 }, o = 120, a = 0, c = 0;
|
|
1015
|
+
for (; a < i; ) {
|
|
1016
|
+
for (; c < n.length && n[c].tick <= a; )
|
|
1017
|
+
l = n[c], c++;
|
|
1018
|
+
let h;
|
|
1019
|
+
h = a + e * 4 * l.numerator / l.denominator;
|
|
1020
|
+
const m = s.filter(
|
|
1021
|
+
(g) => g.type === "tempo" && g.tick >= a && g.tick < h
|
|
1022
|
+
), f = l.numerator, u = [];
|
|
1023
|
+
for (let g = 0; g < f; g++) {
|
|
1024
|
+
const y = a + g * e;
|
|
1025
|
+
let w = o;
|
|
1026
|
+
for (let b = m.length - 1; b >= 0; b--) {
|
|
1027
|
+
const T = m[b];
|
|
1028
|
+
if (T.tick <= y) {
|
|
1029
|
+
w = T.bpm;
|
|
1030
|
+
break;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
for (let b = s.length - 1; b >= 0; b--) {
|
|
1034
|
+
const T = s[b];
|
|
1035
|
+
if (T.type === "tempo" && T.tick <= y) {
|
|
1036
|
+
w = T.bpm, T.tick <= a && (o = T.bpm);
|
|
1037
|
+
break;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
u.push(w * l.denominator / 4);
|
|
1041
|
+
}
|
|
1042
|
+
const _ = u.every((g) => g === u[0]) ? u[0] : u;
|
|
1043
|
+
r.push({
|
|
1044
|
+
sig: [l.numerator, l.denominator],
|
|
1045
|
+
bpm: _
|
|
1046
|
+
}), a = h;
|
|
1047
|
+
}
|
|
1048
|
+
this.parsedData.barStructure = r;
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Extract notes for each voice part
|
|
1052
|
+
* @private
|
|
1053
|
+
*/
|
|
1054
|
+
_extractParts(t) {
|
|
1055
|
+
const e = {}, s = t.ticksPerBeat;
|
|
1056
|
+
t.tracks.forEach((i, r) => {
|
|
1057
|
+
if (!i.notes.length) return;
|
|
1058
|
+
let n = null;
|
|
1059
|
+
if (i.name) {
|
|
1060
|
+
const u = i.name.toLowerCase();
|
|
1061
|
+
for (const d of this.partNames)
|
|
1062
|
+
if (d.length === 1) {
|
|
1063
|
+
if (u === d) {
|
|
1064
|
+
n = d;
|
|
1065
|
+
break;
|
|
1066
|
+
}
|
|
1067
|
+
} else if (u.includes(d)) {
|
|
1068
|
+
n = d;
|
|
1069
|
+
break;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
n || (n = i.name || `Track ${r + 1}`), n === "s" && (n = "soprano"), n === "a" && (n = "alto"), n === "t" && (n = "tenor"), n === "b" && (n = "bass");
|
|
1073
|
+
let l = n, o = 2;
|
|
1074
|
+
for (; e[l]; )
|
|
1075
|
+
l = `${n} ${o}`, o++;
|
|
1076
|
+
n = l;
|
|
1077
|
+
const a = [], c = {};
|
|
1078
|
+
i.notes.forEach((u) => {
|
|
1079
|
+
if (u.type === "noteOn")
|
|
1080
|
+
c[u.noteNumber] = {
|
|
1081
|
+
tick: u.tick,
|
|
1082
|
+
velocity: u.velocity
|
|
1083
|
+
};
|
|
1084
|
+
else if (u.type === "noteOff" && c[u.noteNumber]) {
|
|
1085
|
+
const d = c[u.noteNumber], _ = u.tick - d.tick;
|
|
1086
|
+
a.push({
|
|
1087
|
+
pitch: u.noteNumber,
|
|
1088
|
+
name: this._midiNoteToName(u.noteNumber),
|
|
1089
|
+
startTick: d.tick,
|
|
1090
|
+
endTick: u.tick,
|
|
1091
|
+
duration: _,
|
|
1092
|
+
// Convert ticks to actual time considering tempo changes
|
|
1093
|
+
startTime: this._ticksToTime(d.tick, t),
|
|
1094
|
+
endTime: this._ticksToTime(u.tick, t),
|
|
1095
|
+
velocity: d.velocity
|
|
1096
|
+
}), delete c[u.noteNumber];
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
const h = i.lyrics.map((u) => ({
|
|
1100
|
+
text: u.text,
|
|
1101
|
+
tick: u.tick,
|
|
1102
|
+
time: u.tick / s
|
|
1103
|
+
// Time in quarter notes
|
|
1104
|
+
}));
|
|
1105
|
+
a.sort((u, d) => u.startTick - d.startTick);
|
|
1106
|
+
const m = i.events.filter((u) => u.type === "programChange").map((u) => ({
|
|
1107
|
+
programNumber: u.programNumber,
|
|
1108
|
+
tick: u.tick,
|
|
1109
|
+
time: this._ticksToTime(u.tick, t)
|
|
1110
|
+
})).sort((u, d) => u.tick - d.tick), f = m.length > 0 ? m[0].programNumber : 0;
|
|
1111
|
+
e[n] = {
|
|
1112
|
+
notes: a,
|
|
1113
|
+
lyrics: h,
|
|
1114
|
+
trackIndex: r,
|
|
1115
|
+
programChanges: m,
|
|
1116
|
+
defaultInstrument: f
|
|
1117
|
+
};
|
|
1118
|
+
}), this.parsedData.parts = e;
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Convert a MIDI note number to note name
|
|
1122
|
+
* @private
|
|
1123
|
+
*/
|
|
1124
|
+
_midiNoteToName(t) {
|
|
1125
|
+
const e = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"], s = Math.floor(t / 12) - 1;
|
|
1126
|
+
return `${e[t % 12]}${s}`;
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Convert bytes to number
|
|
1130
|
+
* @private
|
|
1131
|
+
*/
|
|
1132
|
+
_bytesToNumber(t) {
|
|
1133
|
+
let e = 0;
|
|
1134
|
+
for (let s = 0; s < t.length; s++)
|
|
1135
|
+
e = e << 8 | t[s];
|
|
1136
|
+
return e;
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Convert bytes to string
|
|
1140
|
+
* @private
|
|
1141
|
+
*/
|
|
1142
|
+
_bytesToString(t) {
|
|
1143
|
+
return new TextDecoder().decode(t);
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Read variable-length value from MIDI data
|
|
1147
|
+
* @private
|
|
1148
|
+
*/
|
|
1149
|
+
_readVariableLengthValue(t, e) {
|
|
1150
|
+
let s = 0, i, r = 0;
|
|
1151
|
+
do
|
|
1152
|
+
i = t[e + r++], s = s << 7 | i & 127;
|
|
1153
|
+
while (i & 128);
|
|
1154
|
+
return { value: s, bytesRead: r };
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Convert ticks to time in seconds considering tempo changes within bars
|
|
1158
|
+
* @private
|
|
1159
|
+
*/
|
|
1160
|
+
_ticksToTime(t, e) {
|
|
1161
|
+
const s = e.ticksPerBeat || 480, i = [];
|
|
1162
|
+
e.tracks.forEach((o) => {
|
|
1163
|
+
o.events.forEach((a) => {
|
|
1164
|
+
a.type === "tempo" && i.push(a);
|
|
1165
|
+
});
|
|
1166
|
+
}), i.sort((o, a) => o.tick - a.tick);
|
|
1167
|
+
let r = 0, n = 0, l = 120;
|
|
1168
|
+
for (const o of i) {
|
|
1169
|
+
if (o.tick > t) break;
|
|
1170
|
+
if (o.tick > n) {
|
|
1171
|
+
const c = (o.tick - n) / s * (60 / l);
|
|
1172
|
+
r += c, n = o.tick;
|
|
1173
|
+
}
|
|
1174
|
+
l = o.bpm;
|
|
1175
|
+
}
|
|
1176
|
+
if (t > n) {
|
|
1177
|
+
const a = (t - n) / s * (60 / l);
|
|
1178
|
+
r += a;
|
|
1179
|
+
}
|
|
1180
|
+
return r;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
class S {
|
|
1184
|
+
constructor() {
|
|
1185
|
+
this.barOrder = [], this.beatTable = [];
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Main method to generate beat mapping from parsed MIDI and structure metadata
|
|
1189
|
+
* @param {Object} parsedMidiData - Output from MidiParser
|
|
1190
|
+
* @param {Object} structureMetadata - Score structure with sections and order
|
|
1191
|
+
* @returns {Array} Beat table with time, repeat, bar, and beat information
|
|
1192
|
+
*/
|
|
1193
|
+
mapBeats(t, e) {
|
|
1194
|
+
try {
|
|
1195
|
+
return this.barOrder = this.generateBarOrder(e.sections, e.order), this.beatTable = this.generateBeatTable(this.barOrder, t.barStructure), this.beatTable;
|
|
1196
|
+
} catch (s) {
|
|
1197
|
+
throw console.error("Error mapping beats:", s), s;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Generate ordered sequence of bars with repeat information
|
|
1202
|
+
* @param {Array} sections - Section definitions from structure metadata
|
|
1203
|
+
* @param {Array} order - Play order from structure metadata
|
|
1204
|
+
* @returns {Array} Bar order with bar numbers and repeat counts
|
|
1205
|
+
*/
|
|
1206
|
+
generateBarOrder(t, e) {
|
|
1207
|
+
const s = [], i = {};
|
|
1208
|
+
for (const r of e) {
|
|
1209
|
+
const n = t[r.section];
|
|
1210
|
+
if (!n)
|
|
1211
|
+
throw new Error(`Invalid section index: ${r.section}`);
|
|
1212
|
+
const l = r.section;
|
|
1213
|
+
i[l] || (i[l] = 0), i[l]++;
|
|
1214
|
+
const o = i[l], a = r.from !== void 0 ? r.from : this._getSectionStartBar(t, r.section), c = r.to !== void 0 ? r.to : n.to, h = r.as || 1;
|
|
1215
|
+
for (let m = a; m <= c; m++)
|
|
1216
|
+
this._shouldPlayBar(n, m, h) && s.push({
|
|
1217
|
+
barNumber: m,
|
|
1218
|
+
repeat: o,
|
|
1219
|
+
sectionIndex: r.section,
|
|
1220
|
+
voltaTime: h
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
return s;
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Generate beat table from bar order and MIDI bar structure
|
|
1227
|
+
* @param {Array} barOrder - Bar sequence with repeat information
|
|
1228
|
+
* @param {Array} barStructure - MIDI bar structure with timing info
|
|
1229
|
+
* @returns {Array} Beat table with time, repeat, bar, and beat information
|
|
1230
|
+
*/
|
|
1231
|
+
generateBeatTable(t, e) {
|
|
1232
|
+
const s = [];
|
|
1233
|
+
let i = 0;
|
|
1234
|
+
for (const r of t) {
|
|
1235
|
+
const n = r.barNumber, l = e[n];
|
|
1236
|
+
if (!l)
|
|
1237
|
+
throw new Error(`No bar structure data found for bar ${n}`);
|
|
1238
|
+
const { sig: [o, a], bpm: c } = l, h = 60 / c;
|
|
1239
|
+
for (let m = 1; m <= o; m++)
|
|
1240
|
+
s.push({
|
|
1241
|
+
time: i,
|
|
1242
|
+
repeat: r.repeat,
|
|
1243
|
+
bar: n,
|
|
1244
|
+
beat: m
|
|
1245
|
+
}), i += h;
|
|
1246
|
+
}
|
|
1247
|
+
return s;
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Determine the starting bar for a section
|
|
1251
|
+
* @private
|
|
1252
|
+
*/
|
|
1253
|
+
_getSectionStartBar(t, e) {
|
|
1254
|
+
return t[e].pickup !== void 0 ? 0 : e === 0 ? 1 : t[e - 1].to + 1;
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Check if a bar should be played based on volta information
|
|
1258
|
+
* @private
|
|
1259
|
+
*/
|
|
1260
|
+
_shouldPlayBar(t, e, s) {
|
|
1261
|
+
if (!t.voltas)
|
|
1262
|
+
return !0;
|
|
1263
|
+
const i = t.voltas.indexOf(e);
|
|
1264
|
+
return i === -1 ? !0 : i + 1 === s;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
function O(p) {
|
|
1268
|
+
return { all: p = p || /* @__PURE__ */ new Map(), on: function(t, e) {
|
|
1269
|
+
var s = p.get(t);
|
|
1270
|
+
s ? s.push(e) : p.set(t, [e]);
|
|
1271
|
+
}, off: function(t, e) {
|
|
1272
|
+
var s = p.get(t);
|
|
1273
|
+
s && (e ? s.splice(s.indexOf(e) >>> 0, 1) : p.set(t, []));
|
|
1274
|
+
}, emit: function(t, e) {
|
|
1275
|
+
var s = p.get(t);
|
|
1276
|
+
s && s.slice().map(function(i) {
|
|
1277
|
+
i(e);
|
|
1278
|
+
}), (s = p.get("*")) && s.slice().map(function(i) {
|
|
1279
|
+
i(t, e);
|
|
1280
|
+
});
|
|
1281
|
+
} };
|
|
1282
|
+
}
|
|
1283
|
+
class N {
|
|
1284
|
+
/**
|
|
1285
|
+
* Create a new MidiPlayer instance
|
|
1286
|
+
* @param {AudioEngine} audioEngine - Initialized audio engine instance
|
|
1287
|
+
* @param {Object} instrumentMap - Mapping of part names to instrument configurations
|
|
1288
|
+
* @param {Object} parsedMidiData - Output from MidiParser
|
|
1289
|
+
* @param {Object} [structureMetadata] - Optional score structure for beat mapping
|
|
1290
|
+
*/
|
|
1291
|
+
constructor(t, e, s, i = null) {
|
|
1292
|
+
if (!t || !t.isInitialized)
|
|
1293
|
+
throw new Error("Initialized AudioEngine is required");
|
|
1294
|
+
if (!s)
|
|
1295
|
+
throw new Error("Parsed MIDI data is required");
|
|
1296
|
+
this.audioEngine = t, this.instrumentMap = e || {}, this.parsedData = s, this._isPlaying = !1, this._currentTime = 0, this._totalDuration = 0, this.playbackSpeed = 1, this.partChannels = /* @__PURE__ */ new Map(), this.partOutputs = /* @__PURE__ */ new Map(), this.playbackStartTime = 0, this.lookAheadTime = 0.05, this.scheduleInterval = null, this.partNotePointers = /* @__PURE__ */ new Map(), this.partProgramPointers = /* @__PURE__ */ new Map(), this.eventBus = O(), this.beatMapper = new S(), this.beats = [], this._setupPartChannels(), this._calculateTotalDuration(), this._resetNotePointers(), this._resetProgramPointers(), i ? this.beats = this.beatMapper.mapBeats(s, i) : this.beats = this._generateSimpleBeatMapping();
|
|
1297
|
+
}
|
|
1298
|
+
// ========================================
|
|
1299
|
+
// PUBLIC API METHODS (unchanged interface)
|
|
1300
|
+
// ========================================
|
|
1301
|
+
/**
|
|
1302
|
+
* Start playback from current position
|
|
1303
|
+
*/
|
|
1304
|
+
play() {
|
|
1305
|
+
this._isPlaying || (this._isPlaying = !0, this.playbackStartTime = this.audioEngine.audioContext.currentTime - this._currentTime / this.playbackSpeed, this._resetNotePointers(), this._resetProgramPointers(), this._schedulePlayback(), this._startTimeUpdateLoop());
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Start playback at a specific audio context time
|
|
1309
|
+
* @param {number} startTime - Audio context time when playback should begin
|
|
1310
|
+
*/
|
|
1311
|
+
playAt(t) {
|
|
1312
|
+
this._isPlaying || (this._isPlaying = !0, this.playbackStartTime = t - this._currentTime / this.playbackSpeed, this._resetNotePointers(), this._resetProgramPointers(), this._schedulePlayback(), this._startTimeUpdateLoop());
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Pause playback (resumable)
|
|
1316
|
+
*/
|
|
1317
|
+
pause() {
|
|
1318
|
+
this._isPlaying && (this._isPlaying = !1, this._stopScheduling(), this._stopTimeUpdateLoop());
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Stop playback and reset to beginning
|
|
1322
|
+
*/
|
|
1323
|
+
stop() {
|
|
1324
|
+
this._isPlaying = !1, this._currentTime = 0, this._stopScheduling(), this._stopTimeUpdateLoop(), this._resetNotePointers(), this._resetProgramPointers();
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Skip to a specific time in seconds
|
|
1328
|
+
* @param {number} seconds - Time to skip to
|
|
1329
|
+
*/
|
|
1330
|
+
skipToTime(t) {
|
|
1331
|
+
t = Math.max(0, Math.min(t, this._totalDuration));
|
|
1332
|
+
const e = this._isPlaying;
|
|
1333
|
+
e && this.pause(), this._currentTime = t, this._resetNotePointers(), this._resetProgramPointers(), e && this.play();
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Set playback speed
|
|
1337
|
+
* @param {number} speed - Speed multiplier (1.0 = normal)
|
|
1338
|
+
*/
|
|
1339
|
+
setPlaybackSpeed(t) {
|
|
1340
|
+
if (t <= 0)
|
|
1341
|
+
throw new Error("Playback speed must be greater than 0");
|
|
1342
|
+
const e = this._isPlaying;
|
|
1343
|
+
e && this.pause(), this.playbackSpeed = t, e && this.play();
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Navigate to a specific bar position
|
|
1347
|
+
* @param {number} barNumber - Bar number (1-based)
|
|
1348
|
+
* @param {number} repeat - Repeat section number (0-based)
|
|
1349
|
+
*/
|
|
1350
|
+
setBar(t, e = 0) {
|
|
1351
|
+
const s = this.getTimeFromBar(t, e);
|
|
1352
|
+
s !== null && (this.skipToTime(s), this._emitEvent("barChanged", { bar: t, beat: 1, repeat: e, time: s }));
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Get time position for a specific bar
|
|
1356
|
+
* @param {number} barNumber - Bar number (1-based)
|
|
1357
|
+
* @param {number} repeat - Repeat section number (0-based)
|
|
1358
|
+
* @returns {number|null} Time in seconds or null if not found
|
|
1359
|
+
*/
|
|
1360
|
+
getTimeFromBar(t, e = 0) {
|
|
1361
|
+
const s = this.beats.find(
|
|
1362
|
+
(i) => i.bar === t && i.beat === 1 && i.repeat === e
|
|
1363
|
+
);
|
|
1364
|
+
return s ? s.time : null;
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* Get bar information for a specific time position
|
|
1368
|
+
* @param {number} time - Time in seconds
|
|
1369
|
+
* @returns {Object|null} Bar info object or null
|
|
1370
|
+
*/
|
|
1371
|
+
getBarFromTime(t) {
|
|
1372
|
+
if (!this.beats.length) return null;
|
|
1373
|
+
let e = null;
|
|
1374
|
+
for (let s = this.beats.length - 1; s >= 0; s--)
|
|
1375
|
+
if (this.beats[s].time <= t) {
|
|
1376
|
+
e = this.beats[s];
|
|
1377
|
+
break;
|
|
1378
|
+
}
|
|
1379
|
+
return e;
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Stop all sounds immediately
|
|
1383
|
+
*/
|
|
1384
|
+
allSoundsOff() {
|
|
1385
|
+
this.audioEngine.allSoundsOff();
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Get the output node for a specific part for external mixer routing
|
|
1389
|
+
*
|
|
1390
|
+
* IMPORTANT: You MUST connect this output to external gain/analyzer nodes:
|
|
1391
|
+
*
|
|
1392
|
+
* @example
|
|
1393
|
+
* const partOutput = player.getPartOutput('soprano');
|
|
1394
|
+
* const externalGain = audioContext.createGain();
|
|
1395
|
+
* const analyzer = audioContext.createAnalyser();
|
|
1396
|
+
*
|
|
1397
|
+
* partOutput.connect(externalGain);
|
|
1398
|
+
* externalGain.connect(analyzer);
|
|
1399
|
+
* analyzer.connect(masterGain); // External master gain
|
|
1400
|
+
*
|
|
1401
|
+
* // Now control volume via external gain:
|
|
1402
|
+
* externalGain.gain.value = 0.5; // 50% volume
|
|
1403
|
+
*
|
|
1404
|
+
* @param {string} partName - Name of the part (e.g., 'soprano', 'alto')
|
|
1405
|
+
* @returns {AudioNode|null} Output gain node for this part
|
|
1406
|
+
*/
|
|
1407
|
+
getPartOutput(t) {
|
|
1408
|
+
return this.partOutputs.get(t) || null;
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Get the channel handle for a specific part (for direct note control)
|
|
1412
|
+
* @param {string} partName - Name of the part
|
|
1413
|
+
* @returns {ChannelHandle|null} Channel handle for this part
|
|
1414
|
+
*/
|
|
1415
|
+
getPartChannel(t) {
|
|
1416
|
+
return this.partChannels.get(t) || null;
|
|
1417
|
+
}
|
|
1418
|
+
// ========================================
|
|
1419
|
+
// STATE ACCESS METHODS (unchanged)
|
|
1420
|
+
// ========================================
|
|
1421
|
+
getCurrentTime() {
|
|
1422
|
+
if (this._isPlaying) {
|
|
1423
|
+
const t = (this.audioEngine.audioContext.currentTime - this.playbackStartTime) * this.playbackSpeed;
|
|
1424
|
+
this._currentTime = Math.min(t, this._totalDuration);
|
|
1425
|
+
}
|
|
1426
|
+
return this._currentTime;
|
|
1427
|
+
}
|
|
1428
|
+
getTotalDuration() {
|
|
1429
|
+
return this._totalDuration;
|
|
1430
|
+
}
|
|
1431
|
+
isPlaying() {
|
|
1432
|
+
return this._isPlaying;
|
|
1433
|
+
}
|
|
1434
|
+
// ========================================
|
|
1435
|
+
// EVENT HANDLING (mitt-based)
|
|
1436
|
+
// ========================================
|
|
1437
|
+
on(t, e) {
|
|
1438
|
+
this.eventBus.on(t, e);
|
|
1439
|
+
}
|
|
1440
|
+
off(t, e) {
|
|
1441
|
+
this.eventBus.off(t, e);
|
|
1442
|
+
}
|
|
1443
|
+
// ========================================
|
|
1444
|
+
// PART-CENTRIC IMPLEMENTATION METHODS
|
|
1445
|
+
// ========================================
|
|
1446
|
+
/**
|
|
1447
|
+
* Set up channel handles for each musical part
|
|
1448
|
+
* @private
|
|
1449
|
+
*/
|
|
1450
|
+
_setupPartChannels() {
|
|
1451
|
+
Object.keys(this.parsedData.parts).forEach((t) => {
|
|
1452
|
+
const e = this.parsedData.parts[t], s = this.instrumentMap[t] || {}, i = s.instrument !== void 0 ? s.instrument : e.defaultInstrument !== void 0 ? e.defaultInstrument : 0;
|
|
1453
|
+
try {
|
|
1454
|
+
const r = this.audioEngine.createChannel(t, {
|
|
1455
|
+
instrument: i,
|
|
1456
|
+
initialVolume: s.volume || 1
|
|
1457
|
+
});
|
|
1458
|
+
this.partChannels.set(t, r);
|
|
1459
|
+
const n = this.audioEngine.audioContext.createGain();
|
|
1460
|
+
n.gain.value = 1;
|
|
1461
|
+
const l = r.getOutputNode();
|
|
1462
|
+
l && l.connect(n), this.partOutputs.set(t, n);
|
|
1463
|
+
} catch (r) {
|
|
1464
|
+
console.error(`Failed to create channel for part '${t}':`, r), this._emitEvent("error", r);
|
|
1465
|
+
}
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Calculate total duration from parsed MIDI data
|
|
1470
|
+
* @private
|
|
1471
|
+
*/
|
|
1472
|
+
_calculateTotalDuration() {
|
|
1473
|
+
let t = 0;
|
|
1474
|
+
Object.values(this.parsedData.parts).forEach((e) => {
|
|
1475
|
+
e.notes.forEach((s) => {
|
|
1476
|
+
s.endTime > t && (t = s.endTime);
|
|
1477
|
+
});
|
|
1478
|
+
}), this._totalDuration = t;
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Schedule all notes for playback using simplified approach
|
|
1482
|
+
* @private
|
|
1483
|
+
*/
|
|
1484
|
+
_schedulePlayback() {
|
|
1485
|
+
this._stopScheduling(), this._startScheduleLoop();
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Start the simplified scheduling loop
|
|
1489
|
+
* @private
|
|
1490
|
+
*/
|
|
1491
|
+
_startScheduleLoop() {
|
|
1492
|
+
this.scheduleInterval || (this.scheduleInterval = setInterval(() => {
|
|
1493
|
+
if (!this._isPlaying) return;
|
|
1494
|
+
const e = (this.audioEngine.audioContext.currentTime - this.playbackStartTime) * this.playbackSpeed, s = e + this.lookAheadTime;
|
|
1495
|
+
for (const [i, r] of this.partChannels) {
|
|
1496
|
+
const n = this.parsedData.parts[i];
|
|
1497
|
+
if (n) {
|
|
1498
|
+
if (n.programChanges && n.programChanges.length > 0) {
|
|
1499
|
+
let l = this.partProgramPointers.get(i) || 0;
|
|
1500
|
+
const o = n.programChanges;
|
|
1501
|
+
for (; l < o.length && o[l].time < e; )
|
|
1502
|
+
l++;
|
|
1503
|
+
for (; l < o.length && o[l].time <= s; ) {
|
|
1504
|
+
const a = o[l];
|
|
1505
|
+
r.setInstrument(a.programNumber), l++;
|
|
1506
|
+
}
|
|
1507
|
+
this.partProgramPointers.set(i, l);
|
|
1508
|
+
}
|
|
1509
|
+
if (n.notes) {
|
|
1510
|
+
let l = this.partNotePointers.get(i) || 0;
|
|
1511
|
+
const o = n.notes;
|
|
1512
|
+
for (; l < o.length && o[l].endTime < e; )
|
|
1513
|
+
l++;
|
|
1514
|
+
for (; l < o.length && o[l].startTime <= s; ) {
|
|
1515
|
+
const a = o[l];
|
|
1516
|
+
if (a.endTime - a.startTime >= 0.01) {
|
|
1517
|
+
const c = this.playbackStartTime + a.startTime / this.playbackSpeed, h = (a.endTime - a.startTime) / this.playbackSpeed;
|
|
1518
|
+
r.playNote(c, a.pitch, a.velocity, h);
|
|
1519
|
+
}
|
|
1520
|
+
l++;
|
|
1521
|
+
}
|
|
1522
|
+
this.partNotePointers.set(i, l);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}, 50));
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* Reset note pointers for all parts based on current playback time
|
|
1530
|
+
* @private
|
|
1531
|
+
*/
|
|
1532
|
+
_resetNotePointers() {
|
|
1533
|
+
const t = this._currentTime;
|
|
1534
|
+
for (const [e] of this.partChannels) {
|
|
1535
|
+
const s = this.parsedData.parts[e];
|
|
1536
|
+
if (!s || !s.notes) continue;
|
|
1537
|
+
let i = 0;
|
|
1538
|
+
for (; i < s.notes.length && s.notes[i].endTime < t; )
|
|
1539
|
+
i++;
|
|
1540
|
+
this.partNotePointers.set(e, i);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Reset program change pointers for all parts based on current playback time
|
|
1545
|
+
* @private
|
|
1546
|
+
*/
|
|
1547
|
+
_resetProgramPointers() {
|
|
1548
|
+
const t = this._currentTime;
|
|
1549
|
+
for (const [e, s] of this.partChannels) {
|
|
1550
|
+
const i = this.parsedData.parts[e];
|
|
1551
|
+
if (!i || !i.programChanges) {
|
|
1552
|
+
this.partProgramPointers.set(e, 0);
|
|
1553
|
+
continue;
|
|
1554
|
+
}
|
|
1555
|
+
let r = 0, n = i.defaultInstrument;
|
|
1556
|
+
for (; r < i.programChanges.length && i.programChanges[r].time <= t; )
|
|
1557
|
+
n = i.programChanges[r].programNumber, r++;
|
|
1558
|
+
s.setInstrument(n), this.partProgramPointers.set(e, r);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* Stop scheduling and all channel notes
|
|
1563
|
+
* @private
|
|
1564
|
+
*/
|
|
1565
|
+
_stopScheduling() {
|
|
1566
|
+
this.scheduleInterval && (clearInterval(this.scheduleInterval), this.scheduleInterval = null), this.partChannels.forEach((t) => {
|
|
1567
|
+
t.isActive() && t.allNotesOff();
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Start the time update loop for firing timeupdate events
|
|
1572
|
+
* @private
|
|
1573
|
+
*/
|
|
1574
|
+
_startTimeUpdateLoop() {
|
|
1575
|
+
this.timeUpdateInterval = setInterval(() => {
|
|
1576
|
+
const t = this.getCurrentTime();
|
|
1577
|
+
this._emitEvent("timeupdate", { currentTime: t });
|
|
1578
|
+
const e = this.getBarFromTime(t);
|
|
1579
|
+
e && this._emitEvent("barChanged", e), t >= this._totalDuration && (this.stop(), this._emitEvent("ended", { finalTime: t }));
|
|
1580
|
+
}, 100);
|
|
1581
|
+
}
|
|
1582
|
+
/**
|
|
1583
|
+
* Stop the time update loop
|
|
1584
|
+
* @private
|
|
1585
|
+
*/
|
|
1586
|
+
_stopTimeUpdateLoop() {
|
|
1587
|
+
this.timeUpdateInterval && (clearInterval(this.timeUpdateInterval), this.timeUpdateInterval = null);
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* Emit an event to all registered listeners
|
|
1591
|
+
* @param {string} eventType - Type of event
|
|
1592
|
+
* @param {any} data - Data to pass to listeners
|
|
1593
|
+
* @private
|
|
1594
|
+
*/
|
|
1595
|
+
_emitEvent(t, e) {
|
|
1596
|
+
(this.eventBus.all.get(t) || []).forEach((i) => {
|
|
1597
|
+
try {
|
|
1598
|
+
i(e);
|
|
1599
|
+
} catch (r) {
|
|
1600
|
+
console.error(`Error in ${t} event listener:`, r);
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
/**
|
|
1605
|
+
* Generate simple beat mapping from MIDI tempo changes when no structure metadata provided
|
|
1606
|
+
* @returns {Array} Array of beat mapping objects
|
|
1607
|
+
* @private
|
|
1608
|
+
*/
|
|
1609
|
+
_generateSimpleBeatMapping() {
|
|
1610
|
+
const t = [], e = this.parsedData.tempoChanges || [], s = 120, i = this.parsedData.barStructure || [], r = i[0] || { sig: [4, 4], bpm: s }, n = r.sig[0], a = 60 / (Array.isArray(r.bpm) ? r.bpm[0] : r.bpm) * n;
|
|
1611
|
+
if (e.length === 0) {
|
|
1612
|
+
const m = Math.max(1, Math.ceil(this._totalDuration / a));
|
|
1613
|
+
let f = 0;
|
|
1614
|
+
for (let u = 1; u <= m; u++) {
|
|
1615
|
+
const d = Math.max(0, u - 1), _ = i[d] || r, g = _.sig[0], y = Array.isArray(_.bpm) ? _.bpm[0] : _.bpm, w = 60 / y;
|
|
1616
|
+
for (let b = 1; b <= g; b++)
|
|
1617
|
+
t.push({
|
|
1618
|
+
bar: u,
|
|
1619
|
+
beat: b,
|
|
1620
|
+
repeat: 0,
|
|
1621
|
+
tempo: y,
|
|
1622
|
+
time: f,
|
|
1623
|
+
timeSig: g
|
|
1624
|
+
}), f += w;
|
|
1625
|
+
}
|
|
1626
|
+
return t;
|
|
1627
|
+
}
|
|
1628
|
+
let c = 0, h = 1;
|
|
1629
|
+
for (let m = 0; m < e.length; m++) {
|
|
1630
|
+
const f = e[m], u = e[m + 1], d = u ? u.time : this._totalDuration, _ = f.tempo || s, g = 60 / _;
|
|
1631
|
+
for (; c < d; ) {
|
|
1632
|
+
for (let y = 1; y <= n && c < d; y++)
|
|
1633
|
+
t.push({
|
|
1634
|
+
bar: h,
|
|
1635
|
+
beat: y,
|
|
1636
|
+
repeat: 0,
|
|
1637
|
+
tempo: _,
|
|
1638
|
+
time: c,
|
|
1639
|
+
timeSig: n
|
|
1640
|
+
}), c += g;
|
|
1641
|
+
h++;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
return t;
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Clean up resources and destroy the player
|
|
1648
|
+
*/
|
|
1649
|
+
destroy() {
|
|
1650
|
+
this.stop(), this.partChannels.forEach((t) => {
|
|
1651
|
+
t.destroy();
|
|
1652
|
+
}), this.partChannels.clear(), this.partOutputs.forEach((t) => {
|
|
1653
|
+
t.disconnect();
|
|
1654
|
+
}), this.partOutputs.clear(), this.partNotePointers.clear(), this.partProgramPointers.clear(), this.eventBus.all.clear();
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
const C = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
1658
|
+
__proto__: null
|
|
1659
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
1660
|
+
export {
|
|
1661
|
+
k as AudioEngine,
|
|
1662
|
+
P as AudioEngineUtils,
|
|
1663
|
+
S as BeatMapper,
|
|
1664
|
+
v as ChannelHandle,
|
|
1665
|
+
E as MidiParser,
|
|
1666
|
+
N as MidiPlayer,
|
|
1667
|
+
B as SpessaSynthAudioEngine,
|
|
1668
|
+
x as SpessaSynthChannelHandle
|
|
1669
|
+
};
|