spessasynth_lib 3.24.0 → 3.24.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.
@@ -25,7 +25,7 @@ export class Sequencer {
25
25
  * Fires on text event
26
26
  * @type {function}
27
27
  * @param data {Uint8Array} the data text
28
- * @param type {number} the status byte of the message (the meta status byte)
28
+ * @param type {number} the status byte of the message (the meta-status byte)
29
29
  * @param lyricsIndex {number} if the text is a lyric, the index of the lyric in midiData.lyrics, otherwise -1
30
30
  */
31
31
  onTextEvent: Function;
@@ -40,47 +40,58 @@ export class Sequencer {
40
40
  * @private
41
41
  */
42
42
  private onSongEnded;
43
- ignoreEvents: boolean;
44
- synth: Synthetizer;
45
- highResTimeOffset: number;
46
43
  /**
47
- * Absolute playback startTime, bases on the synth's time
48
- * @type {number}
44
+ * Fires on tempo change
45
+ * @type {Object<string, function(number)>}
49
46
  */
50
- absoluteStartTime: number;
47
+ onTempoChange: {
48
+ [x: string]: (arg0: number) => any;
49
+ };
51
50
  /**
52
- * @type {function(BasicMIDI)}
53
- * @private
51
+ * Current song's tempo in BPM
52
+ * @type {number}
54
53
  */
55
- private _getMIDIResolve;
54
+ currentTempo: number;
56
55
  /**
57
- * Controls the playback's rate
56
+ * Current song index
58
57
  * @type {number}
59
58
  */
60
- _playbackRate: number;
61
59
  songIndex: number;
62
60
  /**
63
- * Indicates if the current midiData property has dummy data in it (not yet loaded)
61
+ * @type {function(BasicMIDI)}
62
+ * @private
63
+ */
64
+ private _getMIDIResolve;
65
+ /**
66
+ * Indicates if the current midiData property has fake data in it (not yet loaded)
64
67
  * @type {boolean}
65
68
  */
66
69
  hasDummyData: boolean;
67
- _loop: boolean;
68
70
  /**
69
71
  * Indicates whether the sequencer has finished playing a sequence
70
72
  * @type {boolean}
71
73
  */
72
74
  isFinished: boolean;
75
+ /**
76
+ * The current sequence's length, in seconds
77
+ * @type {number}
78
+ */
79
+ duration: number;
73
80
  /**
74
81
  * Indicates if the sequencer is paused.
75
82
  * Paused if a number, undefined if playing
76
83
  * @type {undefined|number}
84
+ * @private
77
85
  */
78
- pausedTime: undefined | number;
86
+ private pausedTime;
87
+ ignoreEvents: boolean;
88
+ synth: Synthetizer;
89
+ highResTimeOffset: number;
79
90
  /**
80
- * The current sequence's length, in seconds
91
+ * Absolute playback startTime, bases on the synth's time
81
92
  * @type {number}
82
93
  */
83
- duration: number;
94
+ absoluteStartTime: number;
84
95
  /**
85
96
  * @type {boolean}
86
97
  * @private
@@ -91,6 +102,28 @@ export class Sequencer {
91
102
  * @private
92
103
  */
93
104
  private _preservePlaybackState;
105
+ /**
106
+ * Internal loop marker
107
+ * @type {boolean}
108
+ * @private
109
+ */
110
+ private _loop;
111
+ set loop(value: boolean);
112
+ get loop(): boolean;
113
+ /**
114
+ * Controls the playback's rate
115
+ * @type {number}
116
+ * @private
117
+ */
118
+ private _playbackRate;
119
+ /**
120
+ * @param value {number}
121
+ */
122
+ set playbackRate(value: number);
123
+ /**
124
+ * @returns {number}
125
+ */
126
+ get playbackRate(): number;
94
127
  /**
95
128
  * Indicates if the sequencer should skip to first note on
96
129
  * @param val {boolean}
@@ -118,21 +151,11 @@ export class Sequencer {
118
151
  * @returns {number} Current playback time, in seconds
119
152
  */
120
153
  get currentTime(): number;
121
- set loop(value: boolean);
122
- get loop(): boolean;
123
154
  /**
124
155
  * Use for visualization as it's not affected by the audioContext stutter
125
156
  * @returns {number}
126
157
  */
127
158
  get currentHighResolutionTime(): number;
128
- /**
129
- * @param value {number}
130
- */
131
- set playbackRate(value: number);
132
- /**
133
- * @returns {number}
134
- */
135
- get playbackRate(): number;
136
159
  /**
137
160
  * true if paused, false if playing or stopped
138
161
  * @returns {boolean}
@@ -156,6 +179,12 @@ export class Sequencer {
156
179
  * @param id {string} must be unique
157
180
  */
158
181
  addOnTimeChangeEvent(callback: (arg0: number) => any, id: string): void;
182
+ /**
183
+ * Adds a new event that gets called when the tempo changes
184
+ * @param callback {function(number)} the new tempo, in BPM
185
+ * @param id {string} must be unique
186
+ */
187
+ addOnTempoChangeEvent(callback: (arg0: number) => any, id: string): void;
159
188
  resetMIDIOut(): void;
160
189
  /**
161
190
  * @param messageType {WorkletSequencerMessageType}
@@ -165,6 +194,12 @@ export class Sequencer {
165
194
  private _sendMessage;
166
195
  nextSong(): void;
167
196
  previousSong(): void;
197
+ /**
198
+ * @param type {Object<string, function>}
199
+ * @param params {any}
200
+ * @private
201
+ */
202
+ private _callEvents;
168
203
  /**
169
204
  * @param {WorkletSequencerReturnMessageType} messageType
170
205
  * @param {any} messageData
@@ -199,7 +234,7 @@ export class Sequencer {
199
234
  unpause(): void;
200
235
  /**
201
236
  * Starts the playback
202
- * @param resetTime {boolean} If true, time is set to 0s
237
+ * @param resetTime {boolean} If true, time is set to 0 s
203
238
  */
204
239
  play(resetTime?: boolean): void;
205
240
  /**
@@ -27,4 +27,5 @@ export namespace WorkletSequencerReturnMessageType {
27
27
  let getMIDI_1: number;
28
28
  export { getMIDI_1 as getMIDI };
29
29
  export let midiError: number;
30
+ export let tempoChange: number;
30
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spessasynth_lib",
3
- "version": "3.24.0",
3
+ "version": "3.24.2",
4
4
  "description": "MIDI and SoundFont2/DLS library with no compromises",
5
5
  "browser": "index.js",
6
6
  "types": "@types/index.d.ts",
@@ -12,7 +12,8 @@ import { BasicMIDI } from "../midi_parser/basic_midi.js";
12
12
 
13
13
  /**
14
14
  * sequencer.js
15
- * purpose: plays back the midi file decoded by midi_loader.js, including support for multi-channel midis (adding channels when more than 1 midi port is detected)
15
+ * purpose: plays back the midi file decoded by midi_loader.js, including support for multichannel midis
16
+ * (adding channels when more than one midi port is detected)
16
17
  */
17
18
 
18
19
  /**
@@ -63,7 +64,7 @@ export class Sequencer
63
64
  * Fires on text event
64
65
  * @type {function}
65
66
  * @param data {Uint8Array} the data text
66
- * @param type {number} the status byte of the message (the meta status byte)
67
+ * @param type {number} the status byte of the message (the meta-status byte)
67
68
  * @param lyricsIndex {number} if the text is a lyric, the index of the lyric in midiData.lyrics, otherwise -1
68
69
  */
69
70
  onTextEvent;
@@ -79,6 +80,51 @@ export class Sequencer
79
80
  */
80
81
  onSongEnded = {};
81
82
 
83
+ /**
84
+ * Fires on tempo change
85
+ * @type {Object<string, function(number)>}
86
+ */
87
+ onTempoChange = {};
88
+
89
+ /**
90
+ * Current song's tempo in BPM
91
+ * @type {number}
92
+ */
93
+ currentTempo = 120;
94
+ /**
95
+ * Current song index
96
+ * @type {number}
97
+ */
98
+ songIndex = 0;
99
+ /**
100
+ * @type {function(BasicMIDI)}
101
+ * @private
102
+ */
103
+ _getMIDIResolve = undefined;
104
+ /**
105
+ * Indicates if the current midiData property has fake data in it (not yet loaded)
106
+ * @type {boolean}
107
+ */
108
+ hasDummyData = true;
109
+ /**
110
+ * Indicates whether the sequencer has finished playing a sequence
111
+ * @type {boolean}
112
+ */
113
+ isFinished = false;
114
+ /**
115
+ * The current sequence's length, in seconds
116
+ * @type {number}
117
+ */
118
+ duration = 0;
119
+
120
+ /**
121
+ * Indicates if the sequencer is paused.
122
+ * Paused if a number, undefined if playing
123
+ * @type {undefined|number}
124
+ * @private
125
+ */
126
+ pausedTime = undefined;
127
+
82
128
  /**
83
129
  * Creates a new Midi sequencer for playing back MIDI files
84
130
  * @param midiBinaries {MIDIFile[]} List of the buffers of the MIDI files
@@ -97,47 +143,6 @@ export class Sequencer
97
143
  */
98
144
  this.absoluteStartTime = this.synth.currentTime;
99
145
 
100
- /**
101
- * @type {function(BasicMIDI)}
102
- * @private
103
- */
104
- this._getMIDIResolve = undefined;
105
-
106
- /**
107
- * Controls the playback's rate
108
- * @type {number}
109
- */
110
- this._playbackRate = 1;
111
-
112
- this.songIndex = 0;
113
-
114
- /**
115
- * Indicates if the current midiData property has dummy data in it (not yet loaded)
116
- * @type {boolean}
117
- */
118
- this.hasDummyData = true;
119
-
120
- this._loop = true;
121
-
122
- /**
123
- * Indicates whether the sequencer has finished playing a sequence
124
- * @type {boolean}
125
- */
126
- this.isFinished = false;
127
-
128
- /**
129
- * Indicates if the sequencer is paused.
130
- * Paused if a number, undefined if playing
131
- * @type {undefined|number}
132
- */
133
- this.pausedTime = undefined;
134
-
135
- /**
136
- * The current sequence's length, in seconds
137
- * @type {number}
138
- */
139
- this.duration = 0;
140
-
141
146
  this.synth.sequencerCallbackFunction = this._handleMessage.bind(this);
142
147
 
143
148
  /**
@@ -167,6 +172,49 @@ export class Sequencer
167
172
  window.addEventListener("beforeunload", this.resetMIDIOut.bind(this));
168
173
  }
169
174
 
175
+ /**
176
+ * Internal loop marker
177
+ * @type {boolean}
178
+ * @private
179
+ */
180
+ _loop = true;
181
+
182
+ get loop()
183
+ {
184
+ return this._loop;
185
+ }
186
+
187
+ set loop(value)
188
+ {
189
+ this._sendMessage(WorkletSequencerMessageType.setLoop, value);
190
+ this._loop = value;
191
+ }
192
+
193
+ /**
194
+ * Controls the playback's rate
195
+ * @type {number}
196
+ * @private
197
+ */
198
+ _playbackRate = 1;
199
+
200
+ /**
201
+ * @returns {number}
202
+ */
203
+ get playbackRate()
204
+ {
205
+ return this._playbackRate;
206
+ }
207
+
208
+ /**
209
+ * @param value {number}
210
+ */
211
+ set playbackRate(value)
212
+ {
213
+ this._sendMessage(WorkletSequencerMessageType.setPlaybackRate, value);
214
+ this.highResTimeOffset *= (value / this._playbackRate);
215
+ this._playbackRate = value;
216
+ }
217
+
170
218
  /**
171
219
  * Indicates if the sequencer should skip to first note on
172
220
  * @return {boolean}
@@ -230,17 +278,6 @@ export class Sequencer
230
278
  this._sendMessage(WorkletSequencerMessageType.setTime, time);
231
279
  }
232
280
 
233
- get loop()
234
- {
235
- return this._loop;
236
- }
237
-
238
- set loop(value)
239
- {
240
- this._sendMessage(WorkletSequencerMessageType.setLoop, value);
241
- this._loop = value;
242
- }
243
-
244
281
  /**
245
282
  * Use for visualization as it's not affected by the audioContext stutter
246
283
  * @returns {number}
@@ -271,24 +308,6 @@ export class Sequencer
271
308
  return currentPerformanceTime;
272
309
  }
273
310
 
274
- /**
275
- * @returns {number}
276
- */
277
- get playbackRate()
278
- {
279
- return this._playbackRate;
280
- }
281
-
282
- /**
283
- * @param value {number}
284
- */
285
- set playbackRate(value)
286
- {
287
- this._sendMessage(WorkletSequencerMessageType.setPlaybackRate, value);
288
- this.highResTimeOffset *= (value / this._playbackRate);
289
- this._playbackRate = value;
290
- }
291
-
292
311
  /**
293
312
  * true if paused, false if playing or stopped
294
313
  * @returns {boolean}
@@ -328,6 +347,16 @@ export class Sequencer
328
347
  this.onTimeChange[id] = callback;
329
348
  }
330
349
 
350
+ /**
351
+ * Adds a new event that gets called when the tempo changes
352
+ * @param callback {function(number)} the new tempo, in BPM
353
+ * @param id {string} must be unique
354
+ */
355
+ addOnTempoChangeEvent(callback, id)
356
+ {
357
+ this.onTempoChange[id] = callback;
358
+ }
359
+
331
360
  resetMIDIOut()
332
361
  {
333
362
  if (!this.MIDIout)
@@ -369,6 +398,26 @@ export class Sequencer
369
398
  this._sendMessage(WorkletSequencerMessageType.changeSong, false);
370
399
  }
371
400
 
401
+ /**
402
+ * @param type {Object<string, function>}
403
+ * @param params {any}
404
+ * @private
405
+ */
406
+ _callEvents(type, params)
407
+ {
408
+ Object.entries(type).forEach((callback) =>
409
+ {
410
+ try
411
+ {
412
+ callback[1](params);
413
+ }
414
+ catch (e)
415
+ {
416
+ SpessaSynthWarn(`Failed to execute callback for ${callback[0]}:`, e);
417
+ }
418
+ });
419
+ }
420
+
372
421
  /**
373
422
  * @param {WorkletSequencerReturnMessageType} messageType
374
423
  * @param {any} messageData
@@ -411,7 +460,7 @@ export class Sequencer
411
460
  this.hasDummyData = false;
412
461
  this.absoluteStartTime = 0;
413
462
  this.duration = this.midiData.duration;
414
- Object.entries(this.onSongChange).forEach((callback) => callback[1](songChangeData));
463
+ this._callEvents(this.onSongChange, songChangeData);
415
464
  // if is auto played, unpause
416
465
  if (messageData[2] === true)
417
466
  {
@@ -429,7 +478,7 @@ export class Sequencer
429
478
  case WorkletSequencerReturnMessageType.timeChange:
430
479
  // message data is absolute time
431
480
  const time = this.synth.currentTime - messageData;
432
- Object.entries(this.onTimeChange).forEach((callback) => callback[1](time));
481
+ this._callEvents(this.onTimeChange, time);
433
482
  this._recalculateStartTime(time);
434
483
  if (this.paused && this._preservePlaybackState)
435
484
  {
@@ -446,7 +495,7 @@ export class Sequencer
446
495
  this.isFinished = messageData;
447
496
  if (this.isFinished)
448
497
  {
449
- Object.entries(this.onSongEnded).forEach((callback) => callback[1]());
498
+ this._callEvents(this.onSongEnded, undefined);
450
499
  }
451
500
  break;
452
501
 
@@ -466,6 +515,14 @@ export class Sequencer
466
515
  {
467
516
  this._getMIDIResolve(BasicMIDI.copyFrom(messageData));
468
517
  }
518
+ break;
519
+
520
+ case WorkletSequencerReturnMessageType.tempoChange:
521
+ this.currentTempo = messageData;
522
+ if (this.onTempoChange)
523
+ {
524
+ this._callEvents(this.onTempoChange, this.currentTempo);
525
+ }
469
526
  }
470
527
  }
471
528
 
@@ -499,7 +556,7 @@ export class Sequencer
499
556
  loadNewSongList(midiBuffers, autoPlay = true)
500
557
  {
501
558
  this.pause();
502
- // add some dummy data
559
+ // add some fake data
503
560
  this.midiData = DUMMY_MIDI_DATA;
504
561
  this.hasDummyData = true;
505
562
  this.duration = 99999;
@@ -549,7 +606,7 @@ export class Sequencer
549
606
 
550
607
  /**
551
608
  * Starts the playback
552
- * @param resetTime {boolean} If true, time is set to 0s
609
+ * @param resetTime {boolean} If true, time is set to 0 s
553
610
  */
554
611
  play(resetTime = false)
555
612
  {
@@ -90,12 +90,15 @@ export function _processEvent(event, trackIndex)
90
90
  break;
91
91
 
92
92
  case messageTypes.setTempo:
93
- this.oneTickToSeconds = 60 / (getTempo(event) * this.midiData.timeDivision);
93
+ let tempoBPM = getTempo(event);
94
+ this.oneTickToSeconds = 60 / (tempoBPM * this.midiData.timeDivision);
94
95
  if (this.oneTickToSeconds === 0)
95
96
  {
96
97
  this.oneTickToSeconds = 60 / (120 * this.midiData.timeDivision);
97
98
  SpessaSynthWarn("invalid tempo! falling back to 120 BPM");
99
+ tempoBPM = 120;
98
100
  }
101
+ this.post(WorkletSequencerReturnMessageType.tempoChange, Math.floor(tempoBPM * 100) / 100);
99
102
  break;
100
103
 
101
104
  // recongized but ignored
@@ -8,18 +8,18 @@ export function _processTick()
8
8
  let current = this.currentTime;
9
9
  while (this.playedTime < current)
10
10
  {
11
- // find next event
11
+ // find the next event
12
12
  let trackIndex = this._findFirstEventIndex();
13
13
  let event = this.tracks[trackIndex][this.eventIndex[trackIndex]];
14
14
  this._processEvent(event, trackIndex);
15
15
 
16
16
  this.eventIndex[trackIndex]++;
17
17
 
18
- // find next event
18
+ // find the next event
19
19
  trackIndex = this._findFirstEventIndex();
20
20
  if (this.tracks[trackIndex].length <= this.eventIndex[trackIndex])
21
21
  {
22
- // song has ended
22
+ // the song has ended
23
23
  if (this.loop)
24
24
  {
25
25
  this.setTimeTicks(this.midiData.loop.start);
@@ -43,7 +43,7 @@ export function _processTick()
43
43
  this.setTimeTicks(this.midiData.loop.start);
44
44
  return;
45
45
  }
46
- // if song has ended
46
+ // if the song has ended
47
47
  else if (current >= this.duration)
48
48
  {
49
49
  if (this.loop && this.currentLoopCount > 0)
@@ -39,5 +39,6 @@ export const WorkletSequencerReturnMessageType = {
39
39
  timeChange: 3, // newAbsoluteTime<number>
40
40
  pause: 4, // no data
41
41
  getMIDI: 5, // midiData<MIDI>
42
- midiError: 6 // errorMSG<string>
42
+ midiError: 6, // errorMSG<string>
43
+ tempoChange: 7 // newTempoBPM<number>
43
44
  };
File without changes