spessasynth_lib 3.24.4 → 3.24.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -4
- package/midi_parser/midi_loader.js +51 -28
- package/package.json +1 -1
- package/sequencer/sequencer.js +95 -22
- package/sequencer/worklet_sequencer/events.js +11 -2
- package/sequencer/worklet_sequencer/process_event.js +11 -18
- package/sequencer/worklet_sequencer/process_tick.js +21 -6
- package/sequencer/worklet_sequencer/sequencer_message.js +3 -2
- package/sequencer/worklet_sequencer/song_control.js +0 -2
- package/sequencer/worklet_sequencer/worklet_sequencer.js +0 -1
- package/soundfont/dls/dls_soundfont.js +8 -4
- package/soundfont/dls/read_lart.js +2 -2
- package/synthetizer/synthetizer.js +8 -6
- package/synthetizer/worklet_processor.min.js +9 -9
- package/synthetizer/worklet_system/main_processor.js +9 -7
- package/synthetizer/worklet_system/worklet_methods/note_on.js +2 -1
package/README.md
CHANGED
|
@@ -43,12 +43,21 @@ document.getElementById("button").onclick = async () =>
|
|
|
43
43
|
- **Excellent SoundFont support:**
|
|
44
44
|
- **Full Generator Support**
|
|
45
45
|
- **Full Modulator Support:** *First (to my knowledge) JavaScript SoundFont synth with that feature!*
|
|
46
|
-
- **GeneralUserGS
|
|
46
|
+
- **GeneralUserGS Compatible:** *[See more here!](https://github.com/mrbumpy409/GeneralUser-GS/blob/main/documentation/README.md)*
|
|
47
47
|
- **SoundFont3 Support:** Play compressed SoundFonts!
|
|
48
48
|
- **Experimental SF2Pack Support:** Play soundfonts compressed with BASSMIDI! (*Note: only works with vorbis compression*)
|
|
49
49
|
- **Can load very large SoundFonts:** up to 4GB! *Note: Only Firefox handles this well; Chromium has a hard-coded memory limit*
|
|
50
|
+
- **Great DLS Support:**
|
|
51
|
+
- **DLS Level 1 Support**
|
|
52
|
+
- **DLS Level 2 Support**
|
|
53
|
+
- **Mobile DLS Support**
|
|
54
|
+
- **Correct articulator support:** *Converts articulators to both modulators and generators!*
|
|
55
|
+
- **Tested and working with gm.dls!**
|
|
56
|
+
- **Correct volume:** *Properly translated to SoundFont volume!*
|
|
57
|
+
- **A-Law encoding support**
|
|
58
|
+
- **Both unsigned 8-bit and signed 16-bit sample support (24-bit theoretically supported as well!)**
|
|
59
|
+
- **Detects special articulator combinations:** *Such as vibratoLfoToPitch*
|
|
50
60
|
- **Soundfont manager:** Stack multiple soundfonts!
|
|
51
|
-
- **DLS Level 1 and 2 Support:** *works with gm.dls!*
|
|
52
61
|
- **Reverb and chorus support:** [customizable!](https://github.com/spessasus/SpessaSynth/wiki/Synthetizer-Class#effects-configuration-object)
|
|
53
62
|
- **Export audio files** using [OfflineAudioContext](https://developer.mozilla.org/en-US/docs/Web/API/OfflineAudioContext)
|
|
54
63
|
- **[Custom modulators for additional controllers](https://github.com/spessasus/SpessaSynth/wiki/Modulator-Class#default-modulators):** *Why not?*
|
|
@@ -106,15 +115,16 @@ document.getElementById("button").onclick = async () =>
|
|
|
106
115
|
- **Variable compression quality:** You choose between file size and quality!
|
|
107
116
|
- **Compression preserving:** Avoid decompressing and recompressing uncompressed samples for minimal quality loss!
|
|
108
117
|
|
|
109
|
-
#### Read and write DLS Level
|
|
118
|
+
#### Read and write DLS Level One or Two files
|
|
110
119
|
- Read DLS (DownLoadable Sounds) files as SF2 files!
|
|
111
120
|
- **Works like a normal soundfont:** *Saving it as sf2 is still [just one function!](https://github.com/spessasus/SpessaSynth/wiki/SoundFont2-Class#write)*
|
|
112
121
|
- Converts articulators to both **modulators** and **generators**!
|
|
113
122
|
- Works with both unsigned 8-bit samples and signed 16-bit samples!
|
|
123
|
+
- A-Law encoding support
|
|
114
124
|
- **Covers special generator cases:** *such as modLfoToPitch*!
|
|
115
125
|
- **Correct volume:** *looking at you, Viena and gm.sf2!*
|
|
116
126
|
- Support built right into the synthesizer!
|
|
117
|
-
- **Convert SF2 to DLS:** [with limitations](https://github.com/spessasus/SpessaSynth/wiki/DLS-Conversion-Problem)
|
|
127
|
+
- **Convert SF2 to DLS:** [with limitations](https://github.com/spessasus/SpessaSynth/wiki/DLS-Conversion-Problem)
|
|
118
128
|
|
|
119
129
|
### Export MIDI as WAV
|
|
120
130
|
- Save the MIDI file as WAV audio!
|
|
@@ -12,7 +12,9 @@ import { BasicMIDI, MIDIticksToSeconds } from "./basic_midi.js";
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* midi_loader.js
|
|
15
|
-
* purpose:
|
|
15
|
+
* purpose:
|
|
16
|
+
* parses a midi file for the seqyencer,
|
|
17
|
+
* including things like marker or CC 2/4 loop detection, copyright detection, etc.
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
20
|
/**
|
|
@@ -65,7 +67,7 @@ class MIDI extends BasicMIDI
|
|
|
65
67
|
SpessaSynthGroupEnd();
|
|
66
68
|
throw new SyntaxError(`Invalid RMIDI Chunk header! Expected "data", got "${rmid}"`);
|
|
67
69
|
}
|
|
68
|
-
// this is
|
|
70
|
+
// this is a rmid, load the midi into an array for parsing
|
|
69
71
|
fileByteArray = riff.chunkData;
|
|
70
72
|
|
|
71
73
|
// keep loading chunks until we get sfbk
|
|
@@ -87,7 +89,7 @@ class MIDI extends BasicMIDI
|
|
|
87
89
|
}
|
|
88
90
|
if (type === "dls ")
|
|
89
91
|
{
|
|
90
|
-
//
|
|
92
|
+
// Assume bank offset of 0 by default. If we find any bank selects, then the offset is 1.
|
|
91
93
|
DLSRMID = true;
|
|
92
94
|
}
|
|
93
95
|
}
|
|
@@ -145,7 +147,13 @@ class MIDI extends BasicMIDI
|
|
|
145
147
|
|
|
146
148
|
if (DLSRMID)
|
|
147
149
|
{
|
|
148
|
-
//
|
|
150
|
+
// Assume bank offset of 0 by default. If we find any bank selects, then the offset is 1.
|
|
151
|
+
this.bankOffset = 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// if no embedded bank, assume 0
|
|
155
|
+
if (this.embeddedSoundFont === undefined)
|
|
156
|
+
{
|
|
149
157
|
this.bankOffset = 0;
|
|
150
158
|
}
|
|
151
159
|
}
|
|
@@ -198,8 +206,8 @@ class MIDI extends BasicMIDI
|
|
|
198
206
|
let loopEnd = null;
|
|
199
207
|
|
|
200
208
|
/**
|
|
201
|
-
* For karaoke files, text events starting with @T are considered titles
|
|
202
|
-
* usually the first one is the title, and the latter
|
|
209
|
+
* For karaoke files, text events starting with @T are considered titles,
|
|
210
|
+
* usually the first one is the title, and the latter is things such as "sequenced by" etc.
|
|
203
211
|
* @type {boolean}
|
|
204
212
|
*/
|
|
205
213
|
let karaokeHasTitle = false;
|
|
@@ -207,7 +215,7 @@ class MIDI extends BasicMIDI
|
|
|
207
215
|
this.lastVoiceEventTick = 0;
|
|
208
216
|
|
|
209
217
|
/**
|
|
210
|
-
* Midi port numbers for each tracks
|
|
218
|
+
* Midi port numbers for each one of the tracks
|
|
211
219
|
* @type {number[]}
|
|
212
220
|
*/
|
|
213
221
|
this.midiPorts = [];
|
|
@@ -274,16 +282,19 @@ class MIDI extends BasicMIDI
|
|
|
274
282
|
{
|
|
275
283
|
statusByte = runningByte;
|
|
276
284
|
}
|
|
277
|
-
else if (!runningByte && statusByteCheck < 0x80)
|
|
278
|
-
{
|
|
279
|
-
// if we don't have a running byte and the status byte isn't valid, it's an error.
|
|
280
|
-
SpessaSynthGroupEnd();
|
|
281
|
-
throw new SyntaxError(`Unexpected byte with no running byte. (${statusByteCheck})`);
|
|
282
|
-
}
|
|
283
285
|
else
|
|
284
|
-
{
|
|
285
|
-
|
|
286
|
-
|
|
286
|
+
{ // noinspection PointlessBooleanExpressionJS
|
|
287
|
+
if (!runningByte && statusByteCheck < 0x80)
|
|
288
|
+
{
|
|
289
|
+
// if we don't have a running byte and the status byte isn't valid, it's an error.
|
|
290
|
+
SpessaSynthGroupEnd();
|
|
291
|
+
throw new SyntaxError(`Unexpected byte with no running byte. (${statusByteCheck})`);
|
|
292
|
+
}
|
|
293
|
+
else
|
|
294
|
+
{
|
|
295
|
+
// if the status byte is valid, use that
|
|
296
|
+
statusByte = trackChunk.data[trackChunk.data.currentIndex++];
|
|
297
|
+
}
|
|
287
298
|
}
|
|
288
299
|
const statusByteChannel = getChannel(statusByte);
|
|
289
300
|
|
|
@@ -310,7 +321,7 @@ class MIDI extends BasicMIDI
|
|
|
310
321
|
|
|
311
322
|
default:
|
|
312
323
|
// voice message
|
|
313
|
-
//
|
|
324
|
+
// gets the midi message length
|
|
314
325
|
if (totalTicks > this.lastVoiceEventTick)
|
|
315
326
|
{
|
|
316
327
|
this.lastVoiceEventTick = totalTicks;
|
|
@@ -344,7 +355,7 @@ class MIDI extends BasicMIDI
|
|
|
344
355
|
switch (statusByteChannel)
|
|
345
356
|
{
|
|
346
357
|
case -2:
|
|
347
|
-
// since this is a meta
|
|
358
|
+
// since this is a meta-message
|
|
348
359
|
const eventText = readBytesAsString(eventData, eventData.length);
|
|
349
360
|
switch (statusByte)
|
|
350
361
|
{
|
|
@@ -404,7 +415,7 @@ class MIDI extends BasicMIDI
|
|
|
404
415
|
// note here: .kar files sometimes just use...
|
|
405
416
|
// lyrics instead of text because why not (of course)
|
|
406
417
|
// perform the same check for @KMIDI KARAOKE FILE
|
|
407
|
-
if (eventText.trim()
|
|
418
|
+
if (eventText.trim().startsWith("@KMIDI KARAOKE FILE"))
|
|
408
419
|
{
|
|
409
420
|
this.isKaraokeFile = true;
|
|
410
421
|
SpessaSynthInfo("%cKaraoke MIDI detected!", consoleColors.recognized);
|
|
@@ -431,7 +442,7 @@ class MIDI extends BasicMIDI
|
|
|
431
442
|
// it has a text event at the start of the file
|
|
432
443
|
// "@KMIDI KARAOKE FILE"
|
|
433
444
|
const checkedText = eventText.trim();
|
|
434
|
-
if (checkedText
|
|
445
|
+
if (checkedText.startsWith("@KMIDI KARAOKE FILE"))
|
|
435
446
|
{
|
|
436
447
|
this.isKaraokeFile = true;
|
|
437
448
|
|
|
@@ -440,7 +451,8 @@ class MIDI extends BasicMIDI
|
|
|
440
451
|
else if (this.isKaraokeFile)
|
|
441
452
|
{
|
|
442
453
|
// check for @T (title)
|
|
443
|
-
// or @A because it is a title too sometimes
|
|
454
|
+
// or @A because it is a title too sometimes?
|
|
455
|
+
// IDK it's strange
|
|
444
456
|
if (checkedText.startsWith("@T") || checkedText.startsWith("@A"))
|
|
445
457
|
{
|
|
446
458
|
if (!karaokeHasTitle)
|
|
@@ -477,6 +489,11 @@ class MIDI extends BasicMIDI
|
|
|
477
489
|
// since this is a voice message
|
|
478
490
|
// check for loop (CC 2/4)
|
|
479
491
|
trackHasVoiceMessages = true;
|
|
492
|
+
// voice messages are 7-bit always
|
|
493
|
+
for (let j = 0; j < eventData.length; j++)
|
|
494
|
+
{
|
|
495
|
+
eventData[j] = Math.min(127, eventData[j]);
|
|
496
|
+
}
|
|
480
497
|
if ((statusByte & 0xF0) === messageTypes.controllerChange)
|
|
481
498
|
{
|
|
482
499
|
switch (eventData[0])
|
|
@@ -494,7 +511,9 @@ class MIDI extends BasicMIDI
|
|
|
494
511
|
}
|
|
495
512
|
else
|
|
496
513
|
{
|
|
497
|
-
// this controller has occured more than once
|
|
514
|
+
// this controller has occured more than once;
|
|
515
|
+
// this means
|
|
516
|
+
// that it doesn't indicate the loop
|
|
498
517
|
loopEnd = 0;
|
|
499
518
|
}
|
|
500
519
|
break;
|
|
@@ -516,7 +535,7 @@ class MIDI extends BasicMIDI
|
|
|
516
535
|
this.tracks.push(track);
|
|
517
536
|
this.usedChannelsOnTrack.push(usedChannels);
|
|
518
537
|
|
|
519
|
-
//
|
|
538
|
+
// If the track has no voice messages, its "track name" event (if it has any)
|
|
520
539
|
// is some metadata. Add it to copyright
|
|
521
540
|
if (!trackHasVoiceMessages)
|
|
522
541
|
{
|
|
@@ -602,9 +621,13 @@ class MIDI extends BasicMIDI
|
|
|
602
621
|
|
|
603
622
|
// fix midi ports:
|
|
604
623
|
// midi tracks without ports will have a value of -1
|
|
605
|
-
// if all ports have a value of -1, set it to 0,
|
|
606
|
-
//
|
|
607
|
-
//
|
|
624
|
+
// if all ports have a value of -1, set it to 0,
|
|
625
|
+
// otherwise take the first midi port and replace all -1 with it,
|
|
626
|
+
// why would we do this?
|
|
627
|
+
// some midis (for some reason) specify all channels to port 1 or else,
|
|
628
|
+
// but leave the conductor track with no port pref.
|
|
629
|
+
// this spessasynth to reserve the first 16 channels for the conductor track
|
|
630
|
+
// (which doesn't play anything) and use the additional 16 for the actual ports.
|
|
608
631
|
let defaultPort = 0;
|
|
609
632
|
for (let port of this.midiPorts)
|
|
610
633
|
{
|
|
@@ -615,7 +638,7 @@ class MIDI extends BasicMIDI
|
|
|
615
638
|
}
|
|
616
639
|
}
|
|
617
640
|
this.midiPorts = this.midiPorts.map(port => port === -1 ? defaultPort : port);
|
|
618
|
-
// add
|
|
641
|
+
// add fake port if empty
|
|
619
642
|
if (this.midiPortChannelOffsets.length === 0)
|
|
620
643
|
{
|
|
621
644
|
this.midiPortChannelOffsets = [0];
|
|
@@ -723,7 +746,7 @@ class MIDI extends BasicMIDI
|
|
|
723
746
|
{
|
|
724
747
|
this.lyrics = this.lyrics.map(lyric =>
|
|
725
748
|
{
|
|
726
|
-
//
|
|
749
|
+
// One exception: hyphens at the end. Don't add a space to them
|
|
727
750
|
if (lyric[lyric.length - 1] === 45)
|
|
728
751
|
{
|
|
729
752
|
return lyric;
|
package/package.json
CHANGED
package/sequencer/sequencer.js
CHANGED
|
@@ -9,11 +9,15 @@ import {
|
|
|
9
9
|
import { SpessaSynthWarn } from "../utils/loggin.js";
|
|
10
10
|
import { DUMMY_MIDI_DATA, MidiData } from "../midi_parser/midi_data.js";
|
|
11
11
|
import { BasicMIDI } from "../midi_parser/basic_midi.js";
|
|
12
|
+
import { readBytesAsUintBigEndian } from "../utils/byte_functions/big_endian.js";
|
|
13
|
+
import { IndexedByteArray } from "../utils/indexed_array.js";
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* sequencer.js
|
|
15
17
|
* purpose: plays back the midi file decoded by midi_loader.js, including support for multichannel midis
|
|
16
18
|
* (adding channels when more than one midi port is detected)
|
|
19
|
+
* note: this is the sequencer class that runs on the main thread
|
|
20
|
+
* and only communicates with the worklet sequencer which does the actual playback
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
23
|
/**
|
|
@@ -43,6 +47,7 @@ const DEFAULT_OPTIONS = {
|
|
|
43
47
|
preservePlaybackState: false
|
|
44
48
|
};
|
|
45
49
|
|
|
50
|
+
// noinspection JSUnusedGlobalSymbols
|
|
46
51
|
export class Sequencer
|
|
47
52
|
{
|
|
48
53
|
/**
|
|
@@ -50,30 +55,35 @@ export class Sequencer
|
|
|
50
55
|
* @type {function(string)}
|
|
51
56
|
*/
|
|
52
57
|
onError;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Fires on text event
|
|
61
|
+
* @type {function}
|
|
62
|
+
* @param data {Uint8Array} the data text
|
|
63
|
+
* @param type {number} the status byte of the message (the meta-status byte)
|
|
64
|
+
* @param lyricsIndex {number} if the text is a lyric, the index of the lyric in midiData.lyrics, otherwise -1
|
|
65
|
+
*/
|
|
66
|
+
onTextEvent;
|
|
67
|
+
|
|
53
68
|
/**
|
|
54
69
|
* The sequence's data, except for the track data.
|
|
55
70
|
* @type {MidiData}
|
|
56
71
|
*/
|
|
57
72
|
midiData;
|
|
73
|
+
|
|
58
74
|
/**
|
|
59
75
|
* @type {Object<string, function(MidiData)>}
|
|
60
76
|
* @private
|
|
61
77
|
*/
|
|
62
78
|
onSongChange = {};
|
|
63
|
-
|
|
64
|
-
* Fires on text event
|
|
65
|
-
* @type {function}
|
|
66
|
-
* @param data {Uint8Array} the data text
|
|
67
|
-
* @param type {number} the status byte of the message (the meta-status byte)
|
|
68
|
-
* @param lyricsIndex {number} if the text is a lyric, the index of the lyric in midiData.lyrics, otherwise -1
|
|
69
|
-
*/
|
|
70
|
-
onTextEvent;
|
|
79
|
+
|
|
71
80
|
/**
|
|
72
81
|
* Fires when CurrentTime changes
|
|
73
82
|
* @type {Object<string, function(number)>} the time that was changed to
|
|
74
83
|
* @private
|
|
75
84
|
*/
|
|
76
85
|
onTimeChange = {};
|
|
86
|
+
|
|
77
87
|
/**
|
|
78
88
|
* @type {Object<string, function>}
|
|
79
89
|
* @private
|
|
@@ -86,6 +96,12 @@ export class Sequencer
|
|
|
86
96
|
*/
|
|
87
97
|
onTempoChange = {};
|
|
88
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Fires on meta-event
|
|
101
|
+
* @type {Object<string, function([number, Uint8Array])>}
|
|
102
|
+
*/
|
|
103
|
+
onMetaEvent = {};
|
|
104
|
+
|
|
89
105
|
/**
|
|
90
106
|
* Current song's tempo in BPM
|
|
91
107
|
* @type {number}
|
|
@@ -149,12 +165,12 @@ export class Sequencer
|
|
|
149
165
|
* @type {boolean}
|
|
150
166
|
* @private
|
|
151
167
|
*/
|
|
152
|
-
this._skipToFirstNoteOn = options?.skipToFirstNoteOn
|
|
168
|
+
this._skipToFirstNoteOn = options?.skipToFirstNoteOn ?? true;
|
|
153
169
|
/**
|
|
154
170
|
* @type {boolean}
|
|
155
171
|
* @private
|
|
156
172
|
*/
|
|
157
|
-
this._preservePlaybackState = options?.preservePlaybackState
|
|
173
|
+
this._preservePlaybackState = options?.preservePlaybackState ?? false;
|
|
158
174
|
|
|
159
175
|
if (this._skipToFirstNoteOn === false)
|
|
160
176
|
{
|
|
@@ -167,7 +183,7 @@ export class Sequencer
|
|
|
167
183
|
this._sendMessage(WorkletSequencerMessageType.setPreservePlaybackState, true);
|
|
168
184
|
}
|
|
169
185
|
|
|
170
|
-
this.loadNewSongList(midiBinaries, options?.autoPlay
|
|
186
|
+
this.loadNewSongList(midiBinaries, options?.autoPlay ?? true);
|
|
171
187
|
|
|
172
188
|
window.addEventListener("beforeunload", this.resetMIDIOut.bind(this));
|
|
173
189
|
}
|
|
@@ -179,6 +195,10 @@ export class Sequencer
|
|
|
179
195
|
*/
|
|
180
196
|
_loop = true;
|
|
181
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Indicates if the sequencer is currently looping
|
|
200
|
+
* @returns {boolean}
|
|
201
|
+
*/
|
|
182
202
|
get loop()
|
|
183
203
|
{
|
|
184
204
|
return this._loop;
|
|
@@ -186,10 +206,36 @@ export class Sequencer
|
|
|
186
206
|
|
|
187
207
|
set loop(value)
|
|
188
208
|
{
|
|
189
|
-
this._sendMessage(WorkletSequencerMessageType.setLoop, value);
|
|
209
|
+
this._sendMessage(WorkletSequencerMessageType.setLoop, [value, this._loopsRemaining]);
|
|
190
210
|
this._loop = value;
|
|
191
211
|
}
|
|
192
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Internal loop count marker (-1 is infinite)
|
|
215
|
+
* @type {number}
|
|
216
|
+
* @private
|
|
217
|
+
*/
|
|
218
|
+
_loopsRemaining = -1;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The current remaining number of loops. -1 means infinite looping
|
|
222
|
+
* @returns {number}
|
|
223
|
+
*/
|
|
224
|
+
get loopsRemaining()
|
|
225
|
+
{
|
|
226
|
+
return this._loopsRemaining;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The current remaining number of loops. -1 means infinite looping
|
|
231
|
+
* @param val {number}
|
|
232
|
+
*/
|
|
233
|
+
set loopsRemaining(val)
|
|
234
|
+
{
|
|
235
|
+
this._loopsRemaining = val;
|
|
236
|
+
this._sendMessage(WorkletSequencerMessageType.setLoop, [this._loop, val]);
|
|
237
|
+
}
|
|
238
|
+
|
|
193
239
|
/**
|
|
194
240
|
* Controls the playback's rate
|
|
195
241
|
* @type {number}
|
|
@@ -357,6 +403,16 @@ export class Sequencer
|
|
|
357
403
|
this.onTempoChange[id] = callback;
|
|
358
404
|
}
|
|
359
405
|
|
|
406
|
+
/**
|
|
407
|
+
* Adds a new event that gets called when a meta-event occurs
|
|
408
|
+
* @param callback {function([number, Uint8Array])} the meta-event type and its data
|
|
409
|
+
* @param id {string} must be unique
|
|
410
|
+
*/
|
|
411
|
+
addOnMetaEvent(callback, id)
|
|
412
|
+
{
|
|
413
|
+
this.onMetaEvent[id] = callback;
|
|
414
|
+
}
|
|
415
|
+
|
|
360
416
|
resetMIDIOut()
|
|
361
417
|
{
|
|
362
418
|
if (!this.MIDIout)
|
|
@@ -405,17 +461,18 @@ export class Sequencer
|
|
|
405
461
|
*/
|
|
406
462
|
_callEvents(type, params)
|
|
407
463
|
{
|
|
408
|
-
|
|
464
|
+
for (const key in type)
|
|
409
465
|
{
|
|
466
|
+
const callback = type[key];
|
|
410
467
|
try
|
|
411
468
|
{
|
|
412
|
-
callback
|
|
469
|
+
callback(params);
|
|
413
470
|
}
|
|
414
471
|
catch (e)
|
|
415
472
|
{
|
|
416
473
|
SpessaSynthWarn(`Failed to execute callback for ${callback[0]}:`, e);
|
|
417
474
|
}
|
|
418
|
-
}
|
|
475
|
+
}
|
|
419
476
|
}
|
|
420
477
|
|
|
421
478
|
/**
|
|
@@ -431,9 +488,6 @@ export class Sequencer
|
|
|
431
488
|
}
|
|
432
489
|
switch (messageType)
|
|
433
490
|
{
|
|
434
|
-
default:
|
|
435
|
-
break;
|
|
436
|
-
|
|
437
491
|
case WorkletSequencerReturnMessageType.midiEvent:
|
|
438
492
|
/**
|
|
439
493
|
* @type {number[]}
|
|
@@ -517,12 +571,31 @@ export class Sequencer
|
|
|
517
571
|
}
|
|
518
572
|
break;
|
|
519
573
|
|
|
520
|
-
case WorkletSequencerReturnMessageType.
|
|
521
|
-
|
|
522
|
-
if (
|
|
574
|
+
case WorkletSequencerReturnMessageType.metaEvent:
|
|
575
|
+
const type = messageData[0];
|
|
576
|
+
if (type === messageTypes.setTempo)
|
|
577
|
+
{
|
|
578
|
+
const arr = new IndexedByteArray(messageData[1]);
|
|
579
|
+
const bpm = 60000000 / readBytesAsUintBigEndian(arr, 3);
|
|
580
|
+
this.currentTempo = Math.round(bpm * 100) / 100;
|
|
581
|
+
if (this.onTempoChange)
|
|
582
|
+
{
|
|
583
|
+
this._callEvents(this.onTempoChange, this.currentTempo);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
this._callEvents(this.onMetaEvent, messageData);
|
|
587
|
+
break;
|
|
588
|
+
|
|
589
|
+
case WorkletSequencerReturnMessageType.loopCountChange:
|
|
590
|
+
this._loopsRemaining = messageData;
|
|
591
|
+
if (this._loopsRemaining === 0)
|
|
523
592
|
{
|
|
524
|
-
this.
|
|
593
|
+
this._loop = false;
|
|
525
594
|
}
|
|
595
|
+
break;
|
|
596
|
+
|
|
597
|
+
default:
|
|
598
|
+
break;
|
|
526
599
|
}
|
|
527
600
|
}
|
|
528
601
|
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ALL_CHANNELS_OR_DIFFERENT_ACTION,
|
|
3
|
+
returnMessageType
|
|
4
|
+
} from "../../synthetizer/worklet_system/message_protocol/worklet_message.js";
|
|
2
5
|
import { WorkletSequencerMessageType, WorkletSequencerReturnMessageType } from "./sequencer_message.js";
|
|
3
6
|
import { messageTypes, midiControllers } from "../../midi_parser/midi_message.js";
|
|
4
7
|
import { MIDI_CHANNEL_COUNT } from "../../synthetizer/synthetizer.js";
|
|
@@ -44,7 +47,13 @@ export function processMessage(messageType, messageData)
|
|
|
44
47
|
break;
|
|
45
48
|
|
|
46
49
|
case WorkletSequencerMessageType.setLoop:
|
|
47
|
-
|
|
50
|
+
const [loop, count] = messageData;
|
|
51
|
+
this.loop = loop;
|
|
52
|
+
if (count === ALL_CHANNELS_OR_DIFFERENT_ACTION)
|
|
53
|
+
{
|
|
54
|
+
this.loopCount = Infinity;
|
|
55
|
+
}
|
|
56
|
+
this.loopCount = count;
|
|
48
57
|
break;
|
|
49
58
|
|
|
50
59
|
case WorkletSequencerMessageType.changeSong:
|
|
@@ -90,7 +90,8 @@ export function _processEvent(event, trackIndex)
|
|
|
90
90
|
break;
|
|
91
91
|
|
|
92
92
|
case messageTypes.setTempo:
|
|
93
|
-
|
|
93
|
+
event.messageData.currentIndex = 0;
|
|
94
|
+
let tempoBPM = 60000000 / readBytesAsUintBigEndian(event.messageData, 3);
|
|
94
95
|
this.oneTickToSeconds = 60 / (tempoBPM * this.midiData.timeDivision);
|
|
95
96
|
if (this.oneTickToSeconds === 0)
|
|
96
97
|
{
|
|
@@ -98,7 +99,6 @@ export function _processEvent(event, trackIndex)
|
|
|
98
99
|
SpessaSynthWarn("invalid tempo! falling back to 120 BPM");
|
|
99
100
|
tempoBPM = 120;
|
|
100
101
|
}
|
|
101
|
-
this.post(WorkletSequencerReturnMessageType.tempoChange, Math.floor(tempoBPM * 100) / 100);
|
|
102
102
|
break;
|
|
103
103
|
|
|
104
104
|
// recongized but ignored
|
|
@@ -131,12 +131,12 @@ export function _processEvent(event, trackIndex)
|
|
|
131
131
|
let sentStatus = statusByteData.status;
|
|
132
132
|
// if MIDI is a karaoke file, it uses the "text" event type or "lyrics" for lyrics (duh)
|
|
133
133
|
// why?
|
|
134
|
-
// because the MIDI standard is a messy pile of garbage and it's not my fault that it's like this :(
|
|
135
|
-
// I'm just trying to make the best out of a bad situation
|
|
134
|
+
// because the MIDI standard is a messy pile of garbage, and it's not my fault that it's like this :(
|
|
135
|
+
// I'm just trying to make the best out of a bad situation.
|
|
136
136
|
// I'm sorry
|
|
137
|
-
// okay
|
|
138
|
-
//
|
|
139
|
-
// check for karaoke file and change the status byte to "lyric" if it's a karaoke file
|
|
137
|
+
// okay I should get back to work
|
|
138
|
+
// anyway,
|
|
139
|
+
// check for a karaoke file and change the status byte to "lyric" if it's a karaoke file
|
|
140
140
|
if (this.midiData.isKaraokeFile && (
|
|
141
141
|
statusByteData.status === messageTypes.text ||
|
|
142
142
|
statusByteData.status === messageTypes.lyric
|
|
@@ -174,6 +174,10 @@ export function _processEvent(event, trackIndex)
|
|
|
174
174
|
);
|
|
175
175
|
break;
|
|
176
176
|
}
|
|
177
|
+
if (statusByteData.status >= 0 && statusByteData.status < 0x80)
|
|
178
|
+
{
|
|
179
|
+
this.post(WorkletSequencerReturnMessageType.metaEvent, [event.messageStatusByte, event.messageData]);
|
|
180
|
+
}
|
|
177
181
|
}
|
|
178
182
|
|
|
179
183
|
/**
|
|
@@ -191,15 +195,4 @@ export function _addNewMidiPort()
|
|
|
191
195
|
this.synth.setDrums(this.synth.workletProcessorChannels.length - 1, true);
|
|
192
196
|
}
|
|
193
197
|
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* gets tempo from the midi message
|
|
198
|
-
* @param event {MidiMessage}
|
|
199
|
-
* @return {number} the tempo in bpm
|
|
200
|
-
*/
|
|
201
|
-
function getTempo(event)
|
|
202
|
-
{
|
|
203
|
-
event.messageData.currentIndex = 0;
|
|
204
|
-
return 60000000 / readBytesAsUintBigEndian(event.messageData, 3);
|
|
205
198
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { WorkletSequencerReturnMessageType } from "./sequencer_message.js";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Processes a single tick
|
|
3
5
|
* @private
|
|
@@ -36,22 +38,35 @@ export function _processTick()
|
|
|
36
38
|
let eventNext = this.tracks[trackIndex][this.eventIndex[trackIndex]];
|
|
37
39
|
this.playedTime += this.oneTickToSeconds * (eventNext.ticks - event.ticks);
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
const canLoop = this.loop && this.loopCount > 0;
|
|
42
|
+
|
|
43
|
+
// if we reached loop.end
|
|
44
|
+
if ((this.midiData.loop.end <= event.ticks) && canLoop)
|
|
41
45
|
{
|
|
42
|
-
|
|
46
|
+
// loop
|
|
47
|
+
if (this.loopCount !== Infinity)
|
|
48
|
+
{
|
|
49
|
+
this.loopCount--;
|
|
50
|
+
this.post(WorkletSequencerReturnMessageType.loopCountChange, this.loopCount);
|
|
51
|
+
}
|
|
43
52
|
this.setTimeTicks(this.midiData.loop.start);
|
|
44
53
|
return;
|
|
45
54
|
}
|
|
46
|
-
// if the song has
|
|
55
|
+
// if the song has endeed
|
|
47
56
|
else if (current >= this.duration)
|
|
48
57
|
{
|
|
49
|
-
if (
|
|
58
|
+
if (canLoop)
|
|
50
59
|
{
|
|
51
|
-
|
|
60
|
+
// loop
|
|
61
|
+
if (this.loopCount !== Infinity)
|
|
62
|
+
{
|
|
63
|
+
this.loopCount--;
|
|
64
|
+
this.post(WorkletSequencerReturnMessageType.loopCountChange, this.loopCount);
|
|
65
|
+
}
|
|
52
66
|
this.setTimeTicks(this.midiData.loop.start);
|
|
53
67
|
return;
|
|
54
68
|
}
|
|
69
|
+
// stop the playback
|
|
55
70
|
this.eventIndex[trackIndex]--;
|
|
56
71
|
this.pause(true);
|
|
57
72
|
if (this.songs.length > 1)
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @property {number} setTime - 4 -> time<number>
|
|
8
8
|
* @property {number} changeMIDIMessageSending - 5 -> sendMIDIMessages<boolean>
|
|
9
9
|
* @property {number} setPlaybackRate - 6 -> playbackRate<number>
|
|
10
|
-
* @property {number} setLoop - 7 -> loop<boolean
|
|
10
|
+
* @property {number} setLoop - 7 -> [loop<boolean>, count<number]
|
|
11
11
|
* @property {number} changeSong - 8 -> goForwards<boolean> if true, next song, if false, previous
|
|
12
12
|
* @property {number} getMIDI - 9 -> (no data)
|
|
13
13
|
* @property {number} setSkipToFirstNote -10 -> skipToFirstNoteOn<boolean>
|
|
@@ -40,5 +40,6 @@ export const WorkletSequencerReturnMessageType = {
|
|
|
40
40
|
pause: 4, // no data
|
|
41
41
|
getMIDI: 5, // midiData<MIDI>
|
|
42
42
|
midiError: 6, // errorMSG<string>
|
|
43
|
-
|
|
43
|
+
metaEvent: 7, // [messageType<number>, messageData<Uint8Array>]
|
|
44
|
+
loopCountChange: 8 // newLoopCount<number>
|
|
44
45
|
};
|