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.
- package/external_midi/README.md +4 -0
- package/external_midi/midi_handler.js +130 -0
- package/external_midi/web_midi_link.js +43 -0
- package/index.js +1 -56
- package/package.json +5 -2
- package/sequencer/README.md +32 -0
- package/sequencer/default_sequencer_options.js +8 -0
- package/sequencer/midi_data.js +63 -0
- package/sequencer/sequencer.js +808 -0
- package/sequencer/sequencer_message.js +53 -0
- package/synthetizer/README.md +38 -0
- package/synthetizer/audio_effects/effects_config.js +25 -0
- package/synthetizer/audio_effects/fancy_chorus.js +162 -0
- package/synthetizer/audio_effects/rb_compressed.min.js +1 -0
- package/synthetizer/audio_effects/reverb.js +35 -0
- package/synthetizer/audio_effects/reverb_as_binary.js +18 -0
- package/synthetizer/key_modifier_manager.js +113 -0
- package/synthetizer/sfman_message.js +9 -0
- package/synthetizer/synth_event_handler.js +217 -0
- package/synthetizer/synth_soundfont_manager.js +112 -0
- package/synthetizer/synthetizer.js +1033 -0
- package/synthetizer/worklet_message.js +120 -0
- package/synthetizer/worklet_processor.js +637 -0
- package/synthetizer/worklet_processor.min.js +22 -0
- package/synthetizer/worklet_processor.min.js.map +7 -0
- package/synthetizer/worklet_url.js +18 -0
- package/utils/buffer_to_wav.js +28 -0
- package/utils/fill_with_defaults.js +21 -0
- package/utils/other.js +11 -0
|
@@ -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
|
+
}
|