spessasynth_lib 3.26.0 → 3.26.2

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.
@@ -0,0 +1,808 @@
1
+ import { Synthetizer } from "../synthetizer/synthetizer.js";
2
+ import {
3
+ ALL_CHANNELS_OR_DIFFERENT_ACTION,
4
+ BasicMIDI,
5
+ messageTypes,
6
+ MIDI,
7
+ MIDIMessage,
8
+ SpessaSynthCoreUtils as util
9
+ } from "spessasynth_core";
10
+ import { workletMessageType } from "../synthetizer/worklet_message.js";
11
+ import {
12
+ SongChangeType,
13
+ SpessaSynthSequencerMessageType,
14
+ SpessaSynthSequencerReturnMessageType
15
+ } from "./sequencer_message.js";
16
+ import { DUMMY_MIDI_DATA, MIDIData } from "./midi_data.js";
17
+ import { DEFAULT_SEQUENCER_OPTIONS } from "./default_sequencer_options.js";
18
+
19
+ /**
20
+ * sequencer.js
21
+ * purpose: plays back the midi file decoded by midi_loader.js, including support for multichannel midis
22
+ * (adding channels when more than one midi port is detected)
23
+ * note: this is the sequencer class that runs on the main thread
24
+ * and only communicates with the worklet sequencer which does the actual playback
25
+ */
26
+
27
+ /**
28
+ * @typedef MidFile {Object}
29
+ * @property {ArrayBuffer} binary - the binary data of the file.
30
+ * @property {string|undefined} altName - the alternative name for the file
31
+ */
32
+
33
+ /**
34
+ * @typedef {BasicMIDI|MidFile} MIDIFile
35
+ */
36
+
37
+ // noinspection JSUnusedGlobalSymbols
38
+ /**
39
+ * @typedef {Object} SequencerOptions
40
+ * @property {boolean|undefined} skipToFirstNoteOn - if true, the sequencer will skip to the first note
41
+ * @property {boolean|undefined} autoPlay - if true, the sequencer will automatically start playing the MIDI
42
+ * @property {boolean|unescape} preservePlaybackState - if true,
43
+ * the sequencer will stay paused when seeking or changing the playback rate
44
+ */
45
+
46
+ // noinspection JSUnusedGlobalSymbols
47
+ export class Sequencer
48
+ {
49
+ /**
50
+ * Executes when MIDI parsing has an error.
51
+ * @type {function(Error)}
52
+ */
53
+ onError;
54
+
55
+ /**
56
+ * Fires on text event
57
+ * @type {Function}
58
+ * @param data {Uint8Array} the data text
59
+ * @param type {number} the status byte of the message (the meta-status byte)
60
+ * @param lyricsIndex {number} if the text is a lyric, the index of the lyric in midiData.lyrics, otherwise -1
61
+ */
62
+ onTextEvent;
63
+
64
+ /**
65
+ * The current MIDI data, with the exclusion of the embedded sound bank and event data.
66
+ * @type {MIDIData}
67
+ */
68
+ midiData;
69
+
70
+ /**
71
+ * The current MIDI data for all songs, like the midiData property.
72
+ * @type {MIDIData[]}
73
+ */
74
+ songListData = [];
75
+
76
+ /**
77
+ * @type {Object<string, function(MIDIData)>}
78
+ * @private
79
+ */
80
+ onSongChange = {};
81
+
82
+ /**
83
+ * Fires when CurrentTime changes
84
+ * @type {Object<string, function(number)>} the time that was changed to
85
+ * @private
86
+ */
87
+ onTimeChange = {};
88
+
89
+ /**
90
+ * @type {Object<string, function>}
91
+ * @private
92
+ */
93
+ onSongEnded = {};
94
+
95
+ /**
96
+ * Fires on tempo change
97
+ * @type {Object<string, function(number)>}
98
+ */
99
+ onTempoChange = {};
100
+
101
+ /**
102
+ * Fires on meta-event
103
+ * @type {Object<string, function([number, Uint8Array, number, number])>}
104
+ */
105
+ onMetaEvent = {};
106
+
107
+ /**
108
+ * Current song's tempo in BPM
109
+ * @type {number}
110
+ */
111
+ currentTempo = 120;
112
+ /**
113
+ * Current song index
114
+ * @type {number}
115
+ */
116
+ songIndex = 0;
117
+ /**
118
+ * @type {function(BasicMIDI)}
119
+ * @private
120
+ */
121
+ _getMIDIResolve = undefined;
122
+ /**
123
+ * Indicates if the current midiData property has fake data in it (not yet loaded)
124
+ * @type {boolean}
125
+ */
126
+ hasDummyData = true;
127
+ /**
128
+ * Indicates whether the sequencer has finished playing a sequence
129
+ * @type {boolean}
130
+ */
131
+ isFinished = false;
132
+ /**
133
+ * The current sequence's length, in seconds
134
+ * @type {number}
135
+ */
136
+ duration = 0;
137
+
138
+ /**
139
+ * Indicates if the sequencer is paused.
140
+ * Paused if a number, undefined if playing
141
+ * @type {undefined|number}
142
+ * @private
143
+ */
144
+ pausedTime = undefined;
145
+
146
+ /**
147
+ * Creates a new Midi sequencer for playing back MIDI files
148
+ * @param midiBinaries {MIDIFile[]} List of the buffers of the MIDI files
149
+ * @param synth {Synthetizer} synth to send events to
150
+ * @param options {SequencerOptions} the sequencer's options
151
+ */
152
+ constructor(midiBinaries, synth, options = DEFAULT_SEQUENCER_OPTIONS)
153
+ {
154
+ this.ignoreEvents = false;
155
+ this.synth = synth;
156
+ this.highResTimeOffset = 0;
157
+
158
+ /**
159
+ * Absolute playback startTime, bases on the synth's time
160
+ * @type {number}
161
+ */
162
+ this.absoluteStartTime = this.synth.currentTime;
163
+
164
+ this.synth.sequencerCallbackFunction = this._handleMessage.bind(this);
165
+
166
+ /**
167
+ * @type {boolean}
168
+ * @private
169
+ */
170
+ this._skipToFirstNoteOn = options?.skipToFirstNoteOn ?? true;
171
+ /**
172
+ * @type {boolean}
173
+ * @private
174
+ */
175
+ this._preservePlaybackState = options?.preservePlaybackState ?? false;
176
+
177
+ if (this._skipToFirstNoteOn === false)
178
+ {
179
+ // setter sends message
180
+ this._sendMessage(SpessaSynthSequencerMessageType.setSkipToFirstNote, false);
181
+ }
182
+
183
+ if (this._preservePlaybackState === true)
184
+ {
185
+ this._sendMessage(SpessaSynthSequencerMessageType.setPreservePlaybackState, true);
186
+ }
187
+
188
+ this.loadNewSongList(midiBinaries, options?.autoPlay ?? true);
189
+
190
+ window.addEventListener("beforeunload", this.resetMIDIOut.bind(this));
191
+ }
192
+
193
+ /**
194
+ * Internal loop marker
195
+ * @type {boolean}
196
+ * @private
197
+ */
198
+ _loop = true;
199
+
200
+ /**
201
+ * Indicates if the sequencer is currently looping
202
+ * @returns {boolean}
203
+ */
204
+ get loop()
205
+ {
206
+ return this._loop;
207
+ }
208
+
209
+ set loop(value)
210
+ {
211
+ this._sendMessage(SpessaSynthSequencerMessageType.setLoop, [value, this._loopsRemaining]);
212
+ this._loop = value;
213
+ }
214
+
215
+ /**
216
+ * Internal loop count marker (-1 is infinite)
217
+ * @type {number}
218
+ * @private
219
+ */
220
+ _loopsRemaining = -1;
221
+
222
+ /**
223
+ * The current remaining number of loops. -1 means infinite looping
224
+ * @returns {number}
225
+ */
226
+ get loopsRemaining()
227
+ {
228
+ return this._loopsRemaining;
229
+ }
230
+
231
+ /**
232
+ * The current remaining number of loops. -1 means infinite looping
233
+ * @param val {number}
234
+ */
235
+ set loopsRemaining(val)
236
+ {
237
+ this._loopsRemaining = val;
238
+ this._sendMessage(SpessaSynthSequencerMessageType.setLoop, [this._loop, val]);
239
+ }
240
+
241
+ /**
242
+ * Controls the playback's rate
243
+ * @type {number}
244
+ * @private
245
+ */
246
+ _playbackRate = 1;
247
+
248
+ /**
249
+ * @returns {number}
250
+ */
251
+ get playbackRate()
252
+ {
253
+ return this._playbackRate;
254
+ }
255
+
256
+ /**
257
+ * @param value {number}
258
+ */
259
+ set playbackRate(value)
260
+ {
261
+ this._sendMessage(SpessaSynthSequencerMessageType.setPlaybackRate, value);
262
+ this.highResTimeOffset *= (value / this._playbackRate);
263
+ this._playbackRate = value;
264
+ }
265
+
266
+ /**
267
+ * @type {boolean}
268
+ * @private
269
+ */
270
+ _shuffleSongs = false;
271
+
272
+ /**
273
+ * Indicates if the song order is random
274
+ * @returns {boolean}
275
+ */
276
+ get shuffleSongs()
277
+ {
278
+ return this._shuffleSongs;
279
+ }
280
+
281
+ /**
282
+ * Indicates if the song order is random
283
+ * @param value {boolean}
284
+ */
285
+ set shuffleSongs(value)
286
+ {
287
+ this._shuffleSongs = value;
288
+ if (value)
289
+ {
290
+ this._sendMessage(SpessaSynthSequencerMessageType.changeSong, [SongChangeType.shuffleOn]);
291
+ }
292
+ else
293
+ {
294
+ this._sendMessage(SpessaSynthSequencerMessageType.changeSong, [SongChangeType.shuffleOff]);
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Indicates if the sequencer should skip to first note on
300
+ * @return {boolean}
301
+ */
302
+ get skipToFirstNoteOn()
303
+ {
304
+ return this._skipToFirstNoteOn;
305
+ }
306
+
307
+ /**
308
+ * Indicates if the sequencer should skip to first note on
309
+ * @param val {boolean}
310
+ */
311
+ set skipToFirstNoteOn(val)
312
+ {
313
+ this._skipToFirstNoteOn = val;
314
+ this._sendMessage(SpessaSynthSequencerMessageType.setSkipToFirstNote, this._skipToFirstNoteOn);
315
+ }
316
+
317
+ /**
318
+ * if true,
319
+ * the sequencer will stay paused when seeking or changing the playback rate
320
+ * @returns {boolean}
321
+ */
322
+ get preservePlaybackState()
323
+ {
324
+ return this._preservePlaybackState;
325
+ }
326
+
327
+ /**
328
+ * if true,
329
+ * the sequencer will stay paused when seeking or changing the playback rate
330
+ * @param val {boolean}
331
+ */
332
+ set preservePlaybackState(val)
333
+ {
334
+ this._preservePlaybackState = val;
335
+ this._sendMessage(SpessaSynthSequencerMessageType.setPreservePlaybackState, val);
336
+ }
337
+
338
+ /**
339
+ * @returns {number} Current playback time, in seconds
340
+ */
341
+ get currentTime()
342
+ {
343
+ // return the paused time if it's set to something other than undefined
344
+ if (this.pausedTime !== undefined)
345
+ {
346
+ return this.pausedTime;
347
+ }
348
+
349
+ return (this.synth.currentTime - this.absoluteStartTime) * this._playbackRate;
350
+ }
351
+
352
+ set currentTime(time)
353
+ {
354
+ if (!this._preservePlaybackState)
355
+ {
356
+ this.unpause();
357
+ }
358
+ this._sendMessage(SpessaSynthSequencerMessageType.setTime, time);
359
+ }
360
+
361
+ /**
362
+ * Use for visualization as it's not affected by the audioContext stutter
363
+ * @returns {number}
364
+ */
365
+ get currentHighResolutionTime()
366
+ {
367
+ if (this.pausedTime !== undefined)
368
+ {
369
+ return this.pausedTime;
370
+ }
371
+ const highResTimeOffset = this.highResTimeOffset;
372
+ const absoluteStartTime = this.absoluteStartTime;
373
+
374
+ // sync performance.now to current time
375
+ const performanceElapsedTime = ((performance.now() / 1000) - absoluteStartTime) * this._playbackRate;
376
+
377
+ let currentPerformanceTime = highResTimeOffset + performanceElapsedTime;
378
+ const currentAudioTime = this.currentTime;
379
+
380
+ const smoothingFactor = 0.01 * this._playbackRate;
381
+
382
+ // diff times smoothing factor
383
+ const timeDifference = currentAudioTime - currentPerformanceTime;
384
+ this.highResTimeOffset += timeDifference * smoothingFactor;
385
+
386
+ // return a smoothed performance time
387
+ currentPerformanceTime = this.highResTimeOffset + performanceElapsedTime;
388
+ return currentPerformanceTime;
389
+ }
390
+
391
+ /**
392
+ * true if paused, false if playing or stopped
393
+ * @returns {boolean}
394
+ */
395
+ get paused()
396
+ {
397
+ return this.pausedTime !== undefined;
398
+ }
399
+
400
+ /**
401
+ * Adds a new event that gets called when the song changes
402
+ * @param callback {function(MIDIData)}
403
+ * @param id {string} must be unique
404
+ */
405
+ addOnSongChangeEvent(callback, id)
406
+ {
407
+ this.onSongChange[id] = callback;
408
+ }
409
+
410
+ /**
411
+ * Adds a new event that gets called when the song ends
412
+ * @param callback {function}
413
+ * @param id {string} must be unique
414
+ */
415
+ addOnSongEndedEvent(callback, id)
416
+ {
417
+ this.onSongEnded[id] = callback;
418
+ }
419
+
420
+ /**
421
+ * Adds a new event that gets called when the time changes
422
+ * @param callback {function(number)} the new time, in seconds
423
+ * @param id {string} must be unique
424
+ */
425
+ addOnTimeChangeEvent(callback, id)
426
+ {
427
+ this.onTimeChange[id] = callback;
428
+ }
429
+
430
+ /**
431
+ * Adds a new event that gets called when the tempo changes
432
+ * @param callback {function(number)} the new tempo, in BPM
433
+ * @param id {string} must be unique
434
+ */
435
+ addOnTempoChangeEvent(callback, id)
436
+ {
437
+ this.onTempoChange[id] = callback;
438
+ }
439
+
440
+ /**
441
+ * Adds a new event that gets called when a meta-event occurs
442
+ * @param callback {function([number, Uint8Array, number, number])} the meta-event type,
443
+ * its data, the track number and MIDI ticks
444
+ * @param id {string} must be unique
445
+ */
446
+ addOnMetaEvent(callback, id)
447
+ {
448
+ this.onMetaEvent[id] = callback;
449
+ }
450
+
451
+ resetMIDIOut()
452
+ {
453
+ if (!this.MIDIout)
454
+ {
455
+ return;
456
+ }
457
+ for (let i = 0; i < 16; i++)
458
+ {
459
+ this.MIDIout.send([messageTypes.controllerChange | i, 120, 0]); // all notes off
460
+ this.MIDIout.send([messageTypes.controllerChange | i, 123, 0]); // all sound off
461
+ }
462
+ this.MIDIout.send([messageTypes.reset]); // reset
463
+ }
464
+
465
+ /**
466
+ * @param messageType {SpessaSynthSequencerMessageType}
467
+ * @param messageData {any}
468
+ * @private
469
+ */
470
+ _sendMessage(messageType, messageData = undefined)
471
+ {
472
+ this.synth.post({
473
+ channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION,
474
+ messageType: workletMessageType.sequencerSpecific,
475
+ messageData: {
476
+ messageType: messageType,
477
+ messageData: messageData
478
+ }
479
+ });
480
+ }
481
+
482
+ /**
483
+ * Switch to the next song in the playlist
484
+ */
485
+ nextSong()
486
+ {
487
+ this._sendMessage(SpessaSynthSequencerMessageType.changeSong, [SongChangeType.forwards]);
488
+ }
489
+
490
+ /**
491
+ * Switch to the previous song in the playlist
492
+ */
493
+ previousSong()
494
+ {
495
+ this._sendMessage(SpessaSynthSequencerMessageType.changeSong, [SongChangeType.backwards]);
496
+ }
497
+
498
+ /**
499
+ * Sets the song index in the playlist
500
+ * @param index
501
+ */
502
+ setSongIndex(index)
503
+ {
504
+ const clamped = Math.max(Math.min(this.songsAmount - 1, index), 0);
505
+ this._sendMessage(SpessaSynthSequencerMessageType.changeSong, [SongChangeType.index, clamped]);
506
+ }
507
+
508
+ /**
509
+ * @param type {Object<string, function>}
510
+ * @param params {any}
511
+ * @private
512
+ */
513
+ _callEvents(type, params)
514
+ {
515
+ for (const key in type)
516
+ {
517
+ const callback = type[key];
518
+ try
519
+ {
520
+ callback(params);
521
+ }
522
+ catch (e)
523
+ {
524
+ util.SpessaSynthWarn(`Failed to execute callback for ${callback[0]}:`, e);
525
+ }
526
+ }
527
+ }
528
+
529
+ /**
530
+ * @param {SpessaSynthSequencerReturnMessageType} messageType
531
+ * @param {any} messageData
532
+ * @private
533
+ */
534
+ _handleMessage(messageType, messageData)
535
+ {
536
+ if (this.ignoreEvents)
537
+ {
538
+ return;
539
+ }
540
+ switch (messageType)
541
+ {
542
+ case SpessaSynthSequencerReturnMessageType.midiEvent:
543
+ /**
544
+ * @type {number[]}
545
+ */
546
+ let midiEventData = messageData;
547
+ if (this.MIDIout)
548
+ {
549
+ if (midiEventData[0] >= 0x80)
550
+ {
551
+ this.MIDIout.send(midiEventData);
552
+ return;
553
+ }
554
+ }
555
+ break;
556
+
557
+ case SpessaSynthSequencerReturnMessageType.songChange:
558
+ this.songIndex = messageData[0];
559
+ const songChangeData = this.songListData[this.songIndex];
560
+ this.midiData = songChangeData;
561
+ this.hasDummyData = false;
562
+ this.absoluteStartTime = 0;
563
+ this.duration = this.midiData.duration;
564
+ this._callEvents(this.onSongChange, songChangeData);
565
+ // if is auto played, unpause
566
+ if (messageData[1] === true)
567
+ {
568
+ this.unpause();
569
+ }
570
+ break;
571
+
572
+ case SpessaSynthSequencerReturnMessageType.timeChange:
573
+ // message data is absolute time
574
+ const time = messageData;
575
+ this._callEvents(this.onTimeChange, time);
576
+ this._recalculateStartTime(time);
577
+ if (this.paused && this._preservePlaybackState)
578
+ {
579
+ this.pausedTime = time;
580
+ }
581
+ else
582
+ {
583
+ this.unpause();
584
+ }
585
+ break;
586
+
587
+ case SpessaSynthSequencerReturnMessageType.pause:
588
+ this.pausedTime = this.currentTime;
589
+ this.isFinished = messageData;
590
+ if (this.isFinished)
591
+ {
592
+ this._callEvents(this.onSongEnded, undefined);
593
+ }
594
+ break;
595
+
596
+ case SpessaSynthSequencerReturnMessageType.midiError:
597
+ if (this.onError)
598
+ {
599
+ this.onError(messageData);
600
+ }
601
+ else
602
+ {
603
+ throw new Error("Sequencer error: " + messageData);
604
+ }
605
+ return;
606
+
607
+ case SpessaSynthSequencerReturnMessageType.getMIDI:
608
+ if (this._getMIDIResolve)
609
+ {
610
+ this._getMIDIResolve(BasicMIDI.copyFrom(messageData));
611
+ }
612
+ break;
613
+
614
+ case SpessaSynthSequencerReturnMessageType.metaEvent:
615
+ /**
616
+ * @type {MIDIMessage}
617
+ */
618
+ const event = messageData[0];
619
+ switch (event.messageStatusByte)
620
+ {
621
+ case messageTypes.setTempo:
622
+ event.messageData.currentIndex = 0;
623
+ const bpm = 60000000 / util.readBytesAsUintBigEndian(event.messageData, 3);
624
+ event.messageData.currentIndex = 0;
625
+ this.currentTempo = Math.round(bpm * 100) / 100;
626
+ if (this.onTempoChange)
627
+ {
628
+ this._callEvents(this.onTempoChange, this.currentTempo);
629
+ }
630
+ break;
631
+
632
+ case messageTypes.text:
633
+ case messageTypes.lyric:
634
+ case messageTypes.copyright:
635
+ case messageTypes.trackName:
636
+ case messageTypes.marker:
637
+ case messageTypes.cuePoint:
638
+ case messageTypes.instrumentName:
639
+ case messageTypes.programName:
640
+ let lyricsIndex = -1;
641
+ if (event.messageStatusByte === messageTypes.lyric)
642
+ {
643
+ lyricsIndex = Math.min(
644
+ this.midiData.lyricsTicks.indexOf(event.ticks),
645
+ this.midiData.lyrics.length - 1
646
+ );
647
+ }
648
+ let sentStatus = event.messageStatusByte;
649
+ // if MIDI is a karaoke file, it uses the "text" event type or "lyrics" for lyrics (duh)
650
+ // why?
651
+ // because the MIDI standard is a messy pile of garbage,
652
+ // and it's not my fault that it's like this :(
653
+ // I'm just trying to make the best out of a bad situation.
654
+ // I'm sorry
655
+ // okay I should get back to work
656
+ // anyway,
657
+ // check for a karaoke file and change the status byte to "lyric"
658
+ // if it's a karaoke file
659
+ if (this.midiData.isKaraokeFile && (
660
+ event.messageStatusByte === messageTypes.text ||
661
+ event.messageStatusByte === messageTypes.lyric
662
+ ))
663
+ {
664
+ lyricsIndex = Math.min(
665
+ this.midiData.lyricsTicks.indexOf(event.ticks),
666
+ this.midiData.lyricsTicks.length
667
+ );
668
+ sentStatus = messageTypes.lyric;
669
+ }
670
+ if (this.onTextEvent)
671
+ {
672
+ this.onTextEvent(event.messageData, sentStatus, lyricsIndex, event.ticks);
673
+ }
674
+ break;
675
+ }
676
+ this._callEvents(this.onMetaEvent, messageData);
677
+ break;
678
+
679
+ case SpessaSynthSequencerReturnMessageType.loopCountChange:
680
+ this._loopsRemaining = messageData;
681
+ if (this._loopsRemaining === 0)
682
+ {
683
+ this._loop = false;
684
+ }
685
+ break;
686
+
687
+ case SpessaSynthSequencerReturnMessageType.songListChange:
688
+ this.songListData = messageData;
689
+ break;
690
+
691
+ default:
692
+ break;
693
+ }
694
+ }
695
+
696
+ /**
697
+ * @param time
698
+ * @private
699
+ */
700
+ _recalculateStartTime(time)
701
+ {
702
+ this.absoluteStartTime = this.synth.currentTime - time / this._playbackRate;
703
+ this.highResTimeOffset = (this.synth.currentTime - (performance.now() / 1000)) * this._playbackRate;
704
+ }
705
+
706
+ /**
707
+ * @returns {Promise<MIDI>}
708
+ */
709
+ async getMIDI()
710
+ {
711
+ return new Promise(resolve =>
712
+ {
713
+ this._getMIDIResolve = resolve;
714
+ this._sendMessage(SpessaSynthSequencerMessageType.getMIDI, undefined);
715
+ });
716
+ }
717
+
718
+ /**
719
+ * Loads a new song list
720
+ * @param midiBuffers {MIDIFile[]} - the MIDI files to play
721
+ * @param autoPlay {boolean} - if true, the first sequence will automatically start playing
722
+ */
723
+ loadNewSongList(midiBuffers, autoPlay = true)
724
+ {
725
+ this.pause();
726
+ // add some fake data
727
+ this.midiData = DUMMY_MIDI_DATA;
728
+ this.hasDummyData = true;
729
+ this.duration = 99999;
730
+ /**
731
+ * sanitize MIDIs
732
+ * @type {({binary: ArrayBuffer, altName: string}|BasicMIDI)[]}
733
+ */
734
+ const sanitizedMidis = midiBuffers.map(m =>
735
+ {
736
+ if (m.binary !== undefined)
737
+ {
738
+ return m;
739
+ }
740
+ return BasicMIDI.copyFrom(m);
741
+ });
742
+ this._sendMessage(SpessaSynthSequencerMessageType.loadNewSongList, [sanitizedMidis, autoPlay]);
743
+ this.songIndex = 0;
744
+ this.songsAmount = midiBuffers.length;
745
+ if (this.songsAmount > 1)
746
+ {
747
+ this.loop = false;
748
+ }
749
+ if (autoPlay === false)
750
+ {
751
+ this.pausedTime = this.currentTime;
752
+ }
753
+ }
754
+
755
+ /**
756
+ * @param output {MIDIOutput}
757
+ */
758
+ connectMidiOutput(output)
759
+ {
760
+ this.resetMIDIOut();
761
+ this.MIDIout = output;
762
+ this._sendMessage(SpessaSynthSequencerMessageType.changeMIDIMessageSending, output !== undefined);
763
+ this.currentTime -= 0.1;
764
+ }
765
+
766
+ /**
767
+ * Pauses the playback
768
+ */
769
+ pause()
770
+ {
771
+ if (this.paused)
772
+ {
773
+ util.SpessaSynthWarn("Already paused");
774
+ return;
775
+ }
776
+ this.pausedTime = this.currentTime;
777
+ this._sendMessage(SpessaSynthSequencerMessageType.pause);
778
+ }
779
+
780
+ unpause()
781
+ {
782
+ this.pausedTime = undefined;
783
+ this.isFinished = false;
784
+ }
785
+
786
+ /**
787
+ * Starts the playback
788
+ * @param resetTime {boolean} If true, time is set to 0 s
789
+ */
790
+ play(resetTime = false)
791
+ {
792
+ if (this.isFinished)
793
+ {
794
+ resetTime = true;
795
+ }
796
+ this._recalculateStartTime(this.pausedTime || 0);
797
+ this.unpause();
798
+ this._sendMessage(SpessaSynthSequencerMessageType.play, resetTime);
799
+ }
800
+
801
+ /**
802
+ * Stops the playback
803
+ */
804
+ stop()
805
+ {
806
+ this._sendMessage(SpessaSynthSequencerMessageType.stop);
807
+ }
808
+ }