spessasynth_lib 3.24.6 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spessasynth_lib",
3
- "version": "3.24.6",
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}
@@ -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
  /**
@@ -136,13 +136,17 @@ class DLSSoundFont extends BasicSoundFont
136
136
  * @param chunk {RiffChunk}
137
137
  * @param expected {string}
138
138
  */
139
- verifyHeader(chunk, expected)
139
+ verifyHeader(chunk, ...expected)
140
140
  {
141
- if (chunk.header.toLowerCase() !== expected.toLowerCase())
141
+ for (const expect of expected)
142
142
  {
143
- SpessaSynthGroupEnd();
144
- this.parsingError(`Invalid DLS chunk header! Expected "${expected.toLowerCase()}" got "${chunk.header.toLowerCase()}"`);
143
+ if (chunk.header.toLowerCase() === expect.toLowerCase())
144
+ {
145
+ return;
146
+ }
145
147
  }
148
+ SpessaSynthGroupEnd();
149
+ this.parsingError(`Invalid DLS chunk header! Expected "${expected.toString()}" got "${chunk.header.toLowerCase()}"`);
146
150
  }
147
151
 
148
152
  /**
@@ -14,7 +14,7 @@ export function readLart(lartChunk, lar2Chunk, zone)
14
14
  while (lartChunk.chunkData.currentIndex < lartChunk.chunkData.length)
15
15
  {
16
16
  const art1 = readRIFFChunk(lartChunk.chunkData);
17
- this.verifyHeader(art1, "art1");
17
+ this.verifyHeader(art1, "art1", "art2");
18
18
  const modsAndGens = readArticulation(art1, true);
19
19
  zone.generators.push(...modsAndGens.generators);
20
20
  zone.modulators.push(...modsAndGens.modulators);
@@ -26,7 +26,7 @@ export function readLart(lartChunk, lar2Chunk, zone)
26
26
  while (lar2Chunk.chunkData.currentIndex < lar2Chunk.chunkData.length)
27
27
  {
28
28
  const art2 = readRIFFChunk(lar2Chunk.chunkData);
29
- this.verifyHeader(art2, "art2");
29
+ this.verifyHeader(art2, "art2", "art1");
30
30
  const modsAndGens = readArticulation(art2, false);
31
31
  zone.generators.push(...modsAndGens.generators);
32
32
  zone.modulators.push(...modsAndGens.modulators);