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 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 Certified:** *[See more here!](https://github.com/mrbumpy409/GeneralUser-GS/blob/main/documentation/README.md)*
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 1 or 2 files
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: parses a midi file for the seqyencer, including things like marker or CC 2/4 loop detection, copyright detection etc.
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 an rmid, load the midi into array for parsing
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
- // assume bank offset of 0 by default. If we find any bank selects, then the offset is 1.
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
- // assume bank offset of 0 by default. If we find any bank selects, then the offset is 1.
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 are things such as "sequenced by" etc.
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
- // if the status byte is valid, just use that
286
- statusByte = trackChunk.data[trackChunk.data.currentIndex++];
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
- // get the midi message length
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 message
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() === "@KMIDI KARAOKE FILE")
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 === "@KMIDI KARAOKE FILE")
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??? idk it's weird
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, this means that it doesnt indicate the loop
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
- // if the track has no voice messages, its "track name" event (if it has any)
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, otherwise take the first midi port and replace all -1 with it
606
- // why do this? some midis (for some reason) specify all channels to port 1 or else, but leave the conductor track with no port pref.
607
- // this spessasynth to reserve the first 16 channels for the conductor track (which doesn't play anything) and use additional 16 for the actual ports.
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 dummy port if empty
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
- // one exception: hyphens at the end. Don't add a space to them
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spessasynth_lib",
3
- "version": "3.24.4",
3
+ "version": "3.24.7",
4
4
  "description": "MIDI and SoundFont2/DLS library with no compromises",
5
5
  "browser": "index.js",
6
6
  "type": "module",
@@ -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 || true;
168
+ this._skipToFirstNoteOn = options?.skipToFirstNoteOn ?? true;
153
169
  /**
154
170
  * @type {boolean}
155
171
  * @private
156
172
  */
157
- this._preservePlaybackState = options?.preservePlaybackState || false;
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 || true);
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
- Object.entries(type).forEach((callback) =>
464
+ for (const key in type)
409
465
  {
466
+ const callback = type[key];
410
467
  try
411
468
  {
412
- callback[1](params);
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.tempoChange:
521
- this.currentTempo = messageData;
522
- if (this.onTempoChange)
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._callEvents(this.onTempoChange, this.currentTempo);
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 { returnMessageType } from "../../synthetizer/worklet_system/message_protocol/worklet_message.js";
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
- this.loop = messageData;
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
- let tempoBPM = getTempo(event);
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 i should get back to work
138
- // anyways,
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
- // loop
40
- if ((this.midiData.loop.end <= event.ticks) && this.loop && this.currentLoopCount > 0)
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
- this.currentLoopCount--;
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 ended
55
+ // if the song has endeed
47
56
  else if (current >= this.duration)
48
57
  {
49
- if (this.loop && this.currentLoopCount > 0)
58
+ if (canLoop)
50
59
  {
51
- this.currentLoopCount--;
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
- tempoChange: 7 // newTempoBPM<number>
43
+ metaEvent: 7, // [messageType<number>, messageData<Uint8Array>]
44
+ loopCountChange: 8 // newLoopCount<number>
44
45
  };
@@ -65,8 +65,6 @@ export function loadNewSequence(parsedMidi, autoPlay = true)
65
65
  */
66
66
  this.midiData = parsedMidi;
67
67
 
68
- this.currentLoopCount = this.loopCount;
69
-
70
68
  // check for embedded soundfont
71
69
  if (this.midiData.embeddedSoundFont !== undefined)
72
70
  {
@@ -33,7 +33,6 @@ class WorkletSequencer
33
33
  this.sendMIDIMessages = false;
34
34
 
35
35
  this.loopCount = Infinity;
36
- this.currentLoopCount = this.loopCount;
37
36
 
38
37
  // event's number in this.events
39
38
  /**