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 +1 -1
- package/sequencer/sequencer.js +92 -19
- 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/worklet_processor.min.js +8 -8
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}
|
|
@@ -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
|
};
|
|
@@ -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
|
-
|
|
141
|
+
for (const expect of expected)
|
|
142
142
|
{
|
|
143
|
-
|
|
144
|
-
|
|
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);
|