spessasynth_lib 3.24.13 → 3.24.16
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/midi_parser/basic_midi.js +457 -68
- package/midi_parser/midi_loader.js +18 -503
- package/midi_parser/midi_message.js +18 -5
- package/midi_parser/midi_sequence.js +2 -2
- package/package.json +1 -1
- package/sequencer/worklet_sequencer/process_event.js +1 -6
- package/synthetizer/synthetizer.js +13 -7
- package/synthetizer/worklet_processor.min.js +12 -12
- package/synthetizer/worklet_system/README.md +2 -2
- package/synthetizer/worklet_system/main_processor.js +106 -95
- package/synthetizer/worklet_system/message_protocol/handle_message.js +22 -17
- package/synthetizer/worklet_system/message_protocol/worklet_message.js +2 -1
- package/synthetizer/worklet_system/snapshot/apply_synthesizer_snapshot.js +14 -0
- package/synthetizer/worklet_system/snapshot/channel_snapshot.js +166 -0
- package/synthetizer/worklet_system/snapshot/send_synthesizer_snapshot.js +14 -0
- package/synthetizer/worklet_system/snapshot/synthesizer_snapshot.js +121 -0
- package/synthetizer/worklet_system/worklet_methods/controller_control/controller_change.js +196 -0
- package/synthetizer/worklet_system/worklet_methods/controller_control/master_parameters.js +34 -0
- package/synthetizer/worklet_system/worklet_methods/{reset_controllers.js → controller_control/reset_controllers.js} +33 -39
- package/synthetizer/worklet_system/worklet_methods/create_worklet_channel.js +26 -0
- package/synthetizer/worklet_system/worklet_methods/{data_entry.js → data_entry/data_entry_coarse.js} +38 -105
- package/synthetizer/worklet_system/worklet_methods/data_entry/data_entry_fine.js +64 -0
- package/synthetizer/worklet_system/worklet_methods/mute_channel.js +17 -0
- package/synthetizer/worklet_system/worklet_methods/note_on.js +36 -34
- package/synthetizer/worklet_system/worklet_methods/program_change.js +49 -0
- package/synthetizer/worklet_system/worklet_methods/{voice_control.js → render_voice.js} +37 -120
- package/synthetizer/worklet_system/worklet_methods/soundfont_management/clear_sound_font.js +35 -0
- package/synthetizer/worklet_system/worklet_methods/soundfont_management/get_preset.js +20 -0
- package/synthetizer/worklet_system/worklet_methods/soundfont_management/reload_sound_font.js +43 -0
- package/synthetizer/worklet_system/worklet_methods/soundfont_management/send_preset_list.js +31 -0
- package/synthetizer/worklet_system/worklet_methods/soundfont_management/set_embedded_sound_font.js +21 -0
- package/synthetizer/worklet_system/worklet_methods/stopping_notes/kill_note.js +20 -0
- package/synthetizer/worklet_system/worklet_methods/stopping_notes/note_off.js +55 -0
- package/synthetizer/worklet_system/worklet_methods/stopping_notes/stop_all_channels.js +16 -0
- package/synthetizer/worklet_system/worklet_methods/stopping_notes/stop_all_notes.js +30 -0
- package/synthetizer/worklet_system/worklet_methods/stopping_notes/voice_killing.js +63 -0
- package/synthetizer/worklet_system/worklet_methods/system_exclusive.js +31 -30
- package/synthetizer/worklet_system/worklet_methods/tuning_control/channel_pressure.js +24 -0
- package/synthetizer/worklet_system/worklet_methods/tuning_control/pitch_wheel.js +33 -0
- package/synthetizer/worklet_system/worklet_methods/tuning_control/poly_pressure.js +31 -0
- package/synthetizer/worklet_system/worklet_methods/tuning_control/set_master_tuning.js +15 -0
- package/synthetizer/worklet_system/worklet_methods/tuning_control/set_modulation_depth.js +27 -0
- package/synthetizer/worklet_system/worklet_methods/tuning_control/set_octave_tuning.js +15 -0
- package/synthetizer/worklet_system/worklet_methods/tuning_control/set_tuning.js +24 -0
- package/synthetizer/worklet_system/worklet_methods/tuning_control/set_tuning_semitones.js +19 -0
- package/synthetizer/worklet_system/worklet_methods/tuning_control/transpose_all_channels.js +15 -0
- package/synthetizer/worklet_system/worklet_methods/tuning_control/transpose_channel.js +31 -0
- package/synthetizer/worklet_system/worklet_utilities/controller_tables.js +10 -1
- package/synthetizer/worklet_system/worklet_utilities/lfo.js +2 -1
- package/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js +4 -4
- package/synthetizer/worklet_system/worklet_utilities/modulator_curves.js +4 -5
- package/synthetizer/worklet_system/worklet_utilities/stereo_panner.js +18 -18
- package/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js +210 -206
- package/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js +354 -108
- package/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +22 -9
- package/synthetizer/worklet_system/snapshot/snapshot.js +0 -311
- package/synthetizer/worklet_system/worklet_methods/controller_control.js +0 -260
- package/synthetizer/worklet_system/worklet_methods/note_off.js +0 -119
- package/synthetizer/worklet_system/worklet_methods/program_control.js +0 -282
- package/synthetizer/worklet_system/worklet_methods/tuning_control.js +0 -233
- package/synthetizer/worklet_system/worklet_methods/vibrato_control.js +0 -29
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { dataBytesAmount, getChannel,
|
|
1
|
+
import { dataBytesAmount, getChannel, MidiMessage } from "./midi_message.js";
|
|
2
2
|
import { IndexedByteArray } from "../utils/indexed_array.js";
|
|
3
|
-
import { consoleColors
|
|
3
|
+
import { consoleColors } from "../utils/other.js";
|
|
4
4
|
import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn } from "../utils/loggin.js";
|
|
5
5
|
import { readRIFFChunk } from "../soundfont/basic_soundfont/riff_chunk.js";
|
|
6
6
|
import { readVariableLengthQuantity } from "../utils/byte_functions/variable_length_quantity.js";
|
|
7
7
|
import { readBytesAsUintBigEndian } from "../utils/byte_functions/big_endian.js";
|
|
8
|
-
import {
|
|
8
|
+
import { readBytesAsString } from "../utils/byte_functions/string.js";
|
|
9
9
|
import { readLittleEndian } from "../utils/byte_functions/little_endian.js";
|
|
10
10
|
import { RMIDINFOChunks } from "./rmidi_writer.js";
|
|
11
|
-
import { BasicMIDI
|
|
11
|
+
import { BasicMIDI } from "./basic_midi.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* midi_loader.js
|
|
@@ -32,22 +32,11 @@ class MIDI extends BasicMIDI
|
|
|
32
32
|
{
|
|
33
33
|
super();
|
|
34
34
|
SpessaSynthGroupCollapsed(`%cParsing MIDI File...`, consoleColors.info);
|
|
35
|
+
this.fileName = fileName;
|
|
35
36
|
const binaryData = new IndexedByteArray(arrayBuffer);
|
|
36
37
|
let fileByteArray;
|
|
37
38
|
|
|
38
39
|
// check for rmid
|
|
39
|
-
let copyrightDetected = false;
|
|
40
|
-
|
|
41
|
-
let nameDetected = false;
|
|
42
|
-
|
|
43
|
-
let DLSRMID = false;
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Will be joined with "\n" to form the final string
|
|
47
|
-
* @type {string[]}
|
|
48
|
-
*/
|
|
49
|
-
let copyrightComponents = [];
|
|
50
|
-
|
|
51
40
|
const initialString = readBytesAsString(binaryData, 4);
|
|
52
41
|
binaryData.currentIndex -= 4;
|
|
53
42
|
if (initialString === "RIFF")
|
|
@@ -70,7 +59,7 @@ class MIDI extends BasicMIDI
|
|
|
70
59
|
// this is a rmid, load the midi into an array for parsing
|
|
71
60
|
fileByteArray = riff.chunkData;
|
|
72
61
|
|
|
73
|
-
// keep loading chunks until we get
|
|
62
|
+
// keep loading chunks until we get the "SFBK" header
|
|
74
63
|
while (binaryData.currentIndex <= binaryData.length)
|
|
75
64
|
{
|
|
76
65
|
const startIndex = binaryData.currentIndex;
|
|
@@ -90,7 +79,7 @@ class MIDI extends BasicMIDI
|
|
|
90
79
|
if (type === "dls ")
|
|
91
80
|
{
|
|
92
81
|
// Assume bank offset of 0 by default. If we find any bank selects, then the offset is 1.
|
|
93
|
-
|
|
82
|
+
this.isDLSRMIDI = true;
|
|
94
83
|
}
|
|
95
84
|
}
|
|
96
85
|
else if (currentChunk.header === "LIST")
|
|
@@ -108,7 +97,6 @@ class MIDI extends BasicMIDI
|
|
|
108
97
|
if (this.RMIDInfo["ICOP"])
|
|
109
98
|
{
|
|
110
99
|
// special case, overwrites the copyright components array
|
|
111
|
-
copyrightDetected = true;
|
|
112
100
|
this.copyright = readBytesAsString(
|
|
113
101
|
this.RMIDInfo["ICOP"],
|
|
114
102
|
this.RMIDInfo["ICOP"].length,
|
|
@@ -119,20 +107,20 @@ class MIDI extends BasicMIDI
|
|
|
119
107
|
if (this.RMIDInfo["INAM"])
|
|
120
108
|
{
|
|
121
109
|
this.rawMidiName = this.RMIDInfo[RMIDINFOChunks.name];
|
|
110
|
+
// noinspection JSCheckFunctionSignatures
|
|
122
111
|
this.midiName = readBytesAsString(
|
|
123
112
|
this.rawMidiName,
|
|
124
113
|
this.rawMidiName.length,
|
|
125
114
|
undefined,
|
|
126
115
|
false
|
|
127
116
|
).replaceAll("\n", " ");
|
|
128
|
-
nameDetected = true;
|
|
129
117
|
}
|
|
130
118
|
// these can be used interchangeably
|
|
131
119
|
if (this.RMIDInfo["IALB"] && !this.RMIDInfo["IPRD"])
|
|
132
120
|
{
|
|
133
121
|
this.RMIDInfo["IPRD"] = this.RMIDInfo["IALB"];
|
|
134
122
|
}
|
|
135
|
-
if (this.RMIDInfo["
|
|
123
|
+
if (this.RMIDInfo["IPRD"] && !this.RMIDInfo["IALB"])
|
|
136
124
|
{
|
|
137
125
|
this.RMIDInfo["IALB"] = this.RMIDInfo["IPRD"];
|
|
138
126
|
}
|
|
@@ -145,7 +133,7 @@ class MIDI extends BasicMIDI
|
|
|
145
133
|
}
|
|
146
134
|
}
|
|
147
135
|
|
|
148
|
-
if (
|
|
136
|
+
if (this.isDLSRMIDI)
|
|
149
137
|
{
|
|
150
138
|
// Assume bank offset of 0 by default. If we find any bank selects, then the offset is 1.
|
|
151
139
|
this.bankOffset = 0;
|
|
@@ -180,64 +168,7 @@ class MIDI extends BasicMIDI
|
|
|
180
168
|
this.tracksAmount = readBytesAsUintBigEndian(headerChunk.data, 2);
|
|
181
169
|
// time division
|
|
182
170
|
this.timeDivision = readBytesAsUintBigEndian(headerChunk.data, 2);
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* The MIDI's key range
|
|
186
|
-
* @type {{min: number, max: number}}
|
|
187
|
-
*/
|
|
188
|
-
this.keyRange = { min: 127, max: 0 };
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Contains the lyrics as binary chunks
|
|
192
|
-
* @type {Uint8Array[]}
|
|
193
|
-
*/
|
|
194
|
-
this.lyrics = [];
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Contains all the tempo changes in the file. (Ordered from last to first)
|
|
198
|
-
* @type {{
|
|
199
|
-
* ticks: number,
|
|
200
|
-
* tempo: number
|
|
201
|
-
* }[]}
|
|
202
|
-
*/
|
|
203
|
-
this.tempoChanges = [{ ticks: 0, tempo: 120 }];
|
|
204
|
-
|
|
205
|
-
let loopStart = null;
|
|
206
|
-
let loopEnd = null;
|
|
207
|
-
|
|
208
|
-
/**
|
|
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.
|
|
211
|
-
* @type {boolean}
|
|
212
|
-
*/
|
|
213
|
-
let karaokeHasTitle = false;
|
|
214
|
-
|
|
215
|
-
this.lastVoiceEventTick = 0;
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Midi port numbers for each one of the tracks
|
|
219
|
-
* @type {number[]}
|
|
220
|
-
*/
|
|
221
|
-
this.midiPorts = [];
|
|
222
|
-
|
|
223
|
-
let portOffset = 0;
|
|
224
|
-
/**
|
|
225
|
-
* Channel offsets for each port, using the SpessaSynth method
|
|
226
|
-
* @type {number[]}
|
|
227
|
-
*/
|
|
228
|
-
this.midiPortChannelOffsets = [];
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* All channels that each track uses. Note: these channels range from 0 to 15, excluding the port offsets!
|
|
232
|
-
* @type {Set<number>[]}
|
|
233
|
-
*/
|
|
234
|
-
this.usedChannelsOnTrack = [];
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Read all the tracks
|
|
238
|
-
* @type {MidiMessage[][]}
|
|
239
|
-
*/
|
|
240
|
-
this.tracks = [];
|
|
171
|
+
// read all the tracks
|
|
241
172
|
for (let i = 0; i < this.tracksAmount; i++)
|
|
242
173
|
{
|
|
243
174
|
/**
|
|
@@ -245,8 +176,6 @@ class MIDI extends BasicMIDI
|
|
|
245
176
|
*/
|
|
246
177
|
const track = [];
|
|
247
178
|
const trackChunk = this.readMIDIChunk(fileByteArray);
|
|
248
|
-
const usedChannels = new Set();
|
|
249
|
-
this.midiPorts.push(-1);
|
|
250
179
|
|
|
251
180
|
if (trackChunk.type !== "MTrk")
|
|
252
181
|
{
|
|
@@ -254,7 +183,6 @@ class MIDI extends BasicMIDI
|
|
|
254
183
|
throw new SyntaxError(`Invalid track header! Expected "MTrk" got "${trackChunk.type}"`);
|
|
255
184
|
}
|
|
256
185
|
|
|
257
|
-
let trackHasVoiceMessages = false;
|
|
258
186
|
|
|
259
187
|
/**
|
|
260
188
|
* MIDI running byte
|
|
@@ -322,19 +250,7 @@ class MIDI extends BasicMIDI
|
|
|
322
250
|
default:
|
|
323
251
|
// voice message
|
|
324
252
|
// gets the midi message length
|
|
325
|
-
if (totalTicks > this.lastVoiceEventTick)
|
|
326
|
-
{
|
|
327
|
-
this.lastVoiceEventTick = totalTicks;
|
|
328
|
-
}
|
|
329
253
|
eventDataLength = dataBytesAmount[statusByte >> 4];
|
|
330
|
-
if ((statusByte & 0xF0) === messageTypes.noteOn)
|
|
331
|
-
{
|
|
332
|
-
usedChannels.add(statusByteChannel);
|
|
333
|
-
const note = trackChunk.data[trackChunk.data.currentIndex];
|
|
334
|
-
this.keyRange.min = Math.min(this.keyRange.min, note);
|
|
335
|
-
this.keyRange.max = Math.max(this.keyRange.max, note);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
254
|
// save the status byte
|
|
339
255
|
runningByte = statusByte;
|
|
340
256
|
break;
|
|
@@ -342,211 +258,16 @@ class MIDI extends BasicMIDI
|
|
|
342
258
|
|
|
343
259
|
// put the event data into the array
|
|
344
260
|
const eventData = new IndexedByteArray(eventDataLength);
|
|
345
|
-
|
|
261
|
+
eventData.set(trackChunk.data.slice(
|
|
346
262
|
trackChunk.data.currentIndex,
|
|
347
263
|
trackChunk.data.currentIndex + eventDataLength
|
|
348
|
-
);
|
|
264
|
+
), 0);
|
|
265
|
+
const event = new MidiMessage(totalTicks, statusByte, eventData);
|
|
266
|
+
track.push(event);
|
|
267
|
+
// advance the track chunk
|
|
349
268
|
trackChunk.data.currentIndex += eventDataLength;
|
|
350
|
-
eventData.set(messageData, 0);
|
|
351
|
-
|
|
352
|
-
const message = new MidiMessage(totalTicks, statusByte, eventData);
|
|
353
|
-
track.push(message);
|
|
354
|
-
|
|
355
|
-
switch (statusByteChannel)
|
|
356
|
-
{
|
|
357
|
-
case -2:
|
|
358
|
-
// since this is a meta-message
|
|
359
|
-
const eventText = readBytesAsString(eventData, eventData.length);
|
|
360
|
-
switch (statusByte)
|
|
361
|
-
{
|
|
362
|
-
case messageTypes.setTempo:
|
|
363
|
-
// add the tempo change
|
|
364
|
-
this.tempoChanges.push({
|
|
365
|
-
ticks: totalTicks,
|
|
366
|
-
tempo: 60000000 / readBytesAsUintBigEndian(messageData, 3)
|
|
367
|
-
});
|
|
368
|
-
break;
|
|
369
|
-
|
|
370
|
-
case messageTypes.marker:
|
|
371
|
-
// check for loop markers
|
|
372
|
-
const text = eventText.trim().toLowerCase();
|
|
373
|
-
switch (text)
|
|
374
|
-
{
|
|
375
|
-
default:
|
|
376
|
-
break;
|
|
377
|
-
|
|
378
|
-
case "start":
|
|
379
|
-
case "loopstart":
|
|
380
|
-
loopStart = totalTicks;
|
|
381
|
-
break;
|
|
382
|
-
|
|
383
|
-
case "loopend":
|
|
384
|
-
loopEnd = totalTicks;
|
|
385
|
-
}
|
|
386
|
-
eventData.currentIndex = 0;
|
|
387
|
-
break;
|
|
388
|
-
|
|
389
|
-
case messageTypes.midiPort:
|
|
390
|
-
const port = eventData[0];
|
|
391
|
-
this.midiPorts[i] = port;
|
|
392
|
-
if (this.midiPortChannelOffsets[port] === undefined)
|
|
393
|
-
{
|
|
394
|
-
this.midiPortChannelOffsets[port] = portOffset;
|
|
395
|
-
portOffset += 16;
|
|
396
|
-
}
|
|
397
|
-
break;
|
|
398
|
-
|
|
399
|
-
case messageTypes.copyright:
|
|
400
|
-
if (!copyrightDetected)
|
|
401
|
-
{
|
|
402
|
-
|
|
403
|
-
eventData.currentIndex = 0;
|
|
404
|
-
copyrightComponents.push(readBytesAsString(
|
|
405
|
-
eventData,
|
|
406
|
-
eventData.length,
|
|
407
|
-
undefined,
|
|
408
|
-
false
|
|
409
|
-
));
|
|
410
|
-
}
|
|
411
|
-
break;
|
|
412
|
-
|
|
413
|
-
case messageTypes.lyric:
|
|
414
|
-
|
|
415
|
-
// note here: .kar files sometimes just use...
|
|
416
|
-
// lyrics instead of text because why not (of course)
|
|
417
|
-
// perform the same check for @KMIDI KARAOKE FILE
|
|
418
|
-
if (eventText.trim().startsWith("@KMIDI KARAOKE FILE"))
|
|
419
|
-
{
|
|
420
|
-
this.isKaraokeFile = true;
|
|
421
|
-
SpessaSynthInfo("%cKaraoke MIDI detected!", consoleColors.recognized);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
if (this.isKaraokeFile)
|
|
425
|
-
{
|
|
426
|
-
// replace the type of the message with text
|
|
427
|
-
message.messageStatusByte = messageTypes.text;
|
|
428
|
-
statusByte = messageTypes.text;
|
|
429
|
-
}
|
|
430
|
-
else
|
|
431
|
-
{
|
|
432
|
-
// add lyrics like a regular midi file
|
|
433
|
-
this.lyrics.push(eventData);
|
|
434
|
-
this.lyricsTicks.push(totalTicks);
|
|
435
|
-
break;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// kar: treat the same as text
|
|
439
|
-
// fallthrough
|
|
440
|
-
case messageTypes.text:
|
|
441
|
-
// possibly Soft Karaoke MIDI file
|
|
442
|
-
// it has a text event at the start of the file
|
|
443
|
-
// "@KMIDI KARAOKE FILE"
|
|
444
|
-
const checkedText = eventText.trim();
|
|
445
|
-
if (checkedText.startsWith("@KMIDI KARAOKE FILE"))
|
|
446
|
-
{
|
|
447
|
-
this.isKaraokeFile = true;
|
|
448
|
-
|
|
449
|
-
SpessaSynthInfo("%cKaraoke MIDI detected!", consoleColors.recognized);
|
|
450
|
-
}
|
|
451
|
-
else if (this.isKaraokeFile)
|
|
452
|
-
{
|
|
453
|
-
// check for @T (title)
|
|
454
|
-
// or @A because it is a title too sometimes?
|
|
455
|
-
// IDK it's strange
|
|
456
|
-
if (checkedText.startsWith("@T") || checkedText.startsWith("@A"))
|
|
457
|
-
{
|
|
458
|
-
if (!karaokeHasTitle)
|
|
459
|
-
{
|
|
460
|
-
this.midiName = checkedText.substring(2).trim();
|
|
461
|
-
karaokeHasTitle = true;
|
|
462
|
-
nameDetected = true;
|
|
463
|
-
// encode to rawMidiName
|
|
464
|
-
this.rawMidiName = getStringBytes(this.midiName);
|
|
465
|
-
}
|
|
466
|
-
else
|
|
467
|
-
{
|
|
468
|
-
// append to copyright
|
|
469
|
-
copyrightComponents.push(checkedText.substring(2).trim());
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
else if (checkedText[0] !== "@")
|
|
473
|
-
{
|
|
474
|
-
// non @: the lyrics
|
|
475
|
-
this.lyrics.push(sanitizeKarLyrics(eventData));
|
|
476
|
-
this.lyricsTicks.push(totalTicks);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
break;
|
|
480
|
-
}
|
|
481
|
-
break;
|
|
482
|
-
|
|
483
|
-
case -3:
|
|
484
|
-
// since this is a sysex message, do nothing
|
|
485
|
-
break;
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
default:
|
|
489
|
-
// since this is a voice message
|
|
490
|
-
// check for loop (CC 2/4)
|
|
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
|
-
}
|
|
497
|
-
if ((statusByte & 0xF0) === messageTypes.controllerChange)
|
|
498
|
-
{
|
|
499
|
-
switch (eventData[0])
|
|
500
|
-
{
|
|
501
|
-
case 2:
|
|
502
|
-
case 116:
|
|
503
|
-
loopStart = totalTicks;
|
|
504
|
-
break;
|
|
505
|
-
|
|
506
|
-
case 4:
|
|
507
|
-
case 117:
|
|
508
|
-
if (loopEnd === null)
|
|
509
|
-
{
|
|
510
|
-
loopEnd = totalTicks;
|
|
511
|
-
}
|
|
512
|
-
else
|
|
513
|
-
{
|
|
514
|
-
// this controller has occured more than once;
|
|
515
|
-
// this means
|
|
516
|
-
// that it doesn't indicate the loop
|
|
517
|
-
loopEnd = 0;
|
|
518
|
-
}
|
|
519
|
-
break;
|
|
520
|
-
|
|
521
|
-
case 0:
|
|
522
|
-
// check RMID
|
|
523
|
-
if (DLSRMID && eventData[1] !== 0 && eventData[1] !== 127)
|
|
524
|
-
{
|
|
525
|
-
SpessaSynthInfo(
|
|
526
|
-
"%cDLS RMIDI with offset 1 detected!",
|
|
527
|
-
consoleColors.recognized
|
|
528
|
-
);
|
|
529
|
-
this.bankOffset = 1;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
269
|
}
|
|
535
270
|
this.tracks.push(track);
|
|
536
|
-
this.usedChannelsOnTrack.push(usedChannels);
|
|
537
|
-
|
|
538
|
-
// If the track has no voice messages, its "track name" event (if it has any)
|
|
539
|
-
// is some metadata. Add it to copyright
|
|
540
|
-
if (!trackHasVoiceMessages)
|
|
541
|
-
{
|
|
542
|
-
const trackName = track.find(e => e.messageStatusByte === messageTypes.trackName);
|
|
543
|
-
if (trackName)
|
|
544
|
-
{
|
|
545
|
-
trackName.messageData.currentIndex = 0;
|
|
546
|
-
const name = readBytesAsString(trackName.messageData, trackName.messageData.length);
|
|
547
|
-
copyrightComponents.push(name);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
271
|
|
|
551
272
|
SpessaSynthInfo(
|
|
552
273
|
`%cParsed %c${this.tracks.length}%c / %c${this.tracksAmount}`,
|
|
@@ -561,213 +282,8 @@ class MIDI extends BasicMIDI
|
|
|
561
282
|
`%cAll tracks parsed correctly!`,
|
|
562
283
|
consoleColors.recognized
|
|
563
284
|
);
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
`%cCorrecting loops, ports and detecting notes...`,
|
|
567
|
-
consoleColors.info
|
|
568
|
-
);
|
|
569
|
-
|
|
570
|
-
const firstNoteOns = [];
|
|
571
|
-
for (const t of this.tracks)
|
|
572
|
-
{
|
|
573
|
-
const firstNoteOn = t.find(e => (e.messageStatusByte & 0xF0) === messageTypes.noteOn);
|
|
574
|
-
if (firstNoteOn)
|
|
575
|
-
{
|
|
576
|
-
firstNoteOns.push(firstNoteOn.ticks);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
this.firstNoteOn = Math.min(...firstNoteOns);
|
|
580
|
-
|
|
581
|
-
SpessaSynthInfo(
|
|
582
|
-
`%cFirst note-on detected at: %c${this.firstNoteOn}%c ticks!`,
|
|
583
|
-
consoleColors.info,
|
|
584
|
-
consoleColors.recognized,
|
|
585
|
-
consoleColors.info
|
|
586
|
-
);
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
if (loopStart !== null && loopEnd === null)
|
|
590
|
-
{
|
|
591
|
-
// not a loop
|
|
592
|
-
loopStart = this.firstNoteOn;
|
|
593
|
-
loopEnd = this.lastVoiceEventTick;
|
|
594
|
-
}
|
|
595
|
-
else
|
|
596
|
-
{
|
|
597
|
-
if (loopStart === null)
|
|
598
|
-
{
|
|
599
|
-
loopStart = this.firstNoteOn;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
if (loopEnd === null || loopEnd === 0)
|
|
603
|
-
{
|
|
604
|
-
loopEnd = this.lastVoiceEventTick;
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
/**
|
|
609
|
-
*
|
|
610
|
-
* @type {{start: number, end: number}}
|
|
611
|
-
*/
|
|
612
|
-
this.loop = { start: loopStart, end: loopEnd };
|
|
613
|
-
|
|
614
|
-
SpessaSynthInfo(
|
|
615
|
-
`%cLoop points: start: %c${this.loop.start}%c end: %c${this.loop.end}`,
|
|
616
|
-
consoleColors.info,
|
|
617
|
-
consoleColors.recognized,
|
|
618
|
-
consoleColors.info,
|
|
619
|
-
consoleColors.recognized
|
|
620
|
-
);
|
|
621
|
-
|
|
622
|
-
// fix midi ports:
|
|
623
|
-
// midi tracks without ports will have a value of -1
|
|
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.
|
|
631
|
-
let defaultPort = 0;
|
|
632
|
-
for (let port of this.midiPorts)
|
|
633
|
-
{
|
|
634
|
-
if (port !== -1)
|
|
635
|
-
{
|
|
636
|
-
defaultPort = port;
|
|
637
|
-
break;
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
this.midiPorts = this.midiPorts.map(port => port === -1 ? defaultPort : port);
|
|
641
|
-
// add fake port if empty
|
|
642
|
-
if (this.midiPortChannelOffsets.length === 0)
|
|
643
|
-
{
|
|
644
|
-
this.midiPortChannelOffsets = [0];
|
|
645
|
-
}
|
|
646
|
-
if (this.midiPortChannelOffsets.length < 2)
|
|
647
|
-
{
|
|
648
|
-
SpessaSynthInfo(`%cNo additional MIDI Ports detected.`, consoleColors.info);
|
|
649
|
-
}
|
|
650
|
-
else
|
|
651
|
-
{
|
|
652
|
-
SpessaSynthInfo(`%cMIDI Ports detected!`, consoleColors.recognized);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
// midi name
|
|
656
|
-
if (!nameDetected)
|
|
657
|
-
{
|
|
658
|
-
if (this.tracks.length > 1)
|
|
659
|
-
{
|
|
660
|
-
// if more than 1 track and the first track has no notes, just find the first trackName in the first track
|
|
661
|
-
if (
|
|
662
|
-
this.tracks[0].find(
|
|
663
|
-
message => message.messageStatusByte >= messageTypes.noteOn
|
|
664
|
-
&&
|
|
665
|
-
message.messageStatusByte < messageTypes.polyPressure
|
|
666
|
-
) === undefined
|
|
667
|
-
)
|
|
668
|
-
{
|
|
669
|
-
|
|
670
|
-
let name = this.tracks[0].find(message => message.messageStatusByte === messageTypes.trackName);
|
|
671
|
-
if (name)
|
|
672
|
-
{
|
|
673
|
-
this.rawMidiName = name.messageData;
|
|
674
|
-
name.messageData.currentIndex = 0;
|
|
675
|
-
this.midiName = readBytesAsString(name.messageData, name.messageData.length, undefined, false);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
else
|
|
680
|
-
{
|
|
681
|
-
// if only 1 track, find the first "track name" event
|
|
682
|
-
let name = this.tracks[0].find(message => message.messageStatusByte === messageTypes.trackName);
|
|
683
|
-
if (name)
|
|
684
|
-
{
|
|
685
|
-
this.rawMidiName = name.messageData;
|
|
686
|
-
name.messageData.currentIndex = 0;
|
|
687
|
-
this.midiName = readBytesAsString(name.messageData, name.messageData.length, undefined, false);
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
if (!copyrightDetected)
|
|
693
|
-
{
|
|
694
|
-
this.copyright = copyrightComponents
|
|
695
|
-
// trim and group newlines into one
|
|
696
|
-
.map(c => c.trim().replace(/(\r?\n)+/g, "\n"))
|
|
697
|
-
// remove empty strings
|
|
698
|
-
.filter(c => c.length > 0)
|
|
699
|
-
// join with newlines
|
|
700
|
-
.join("\n") || "";
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
this.fileName = fileName;
|
|
704
|
-
this.midiName = this.midiName.trim();
|
|
705
|
-
this.midiNameUsesFileName = false;
|
|
706
|
-
// if midiName is "", use the file name
|
|
707
|
-
if (this.midiName.length === 0)
|
|
708
|
-
{
|
|
709
|
-
SpessaSynthInfo(
|
|
710
|
-
`%cNo name detected. Using the alt name!`,
|
|
711
|
-
consoleColors.info
|
|
712
|
-
);
|
|
713
|
-
this.midiName = formatTitle(fileName);
|
|
714
|
-
this.midiNameUsesFileName = true;
|
|
715
|
-
// encode it too
|
|
716
|
-
this.rawMidiName = new Uint8Array(this.midiName.length);
|
|
717
|
-
for (let i = 0; i < this.midiName.length; i++)
|
|
718
|
-
{
|
|
719
|
-
this.rawMidiName[i] = this.midiName.charCodeAt(i);
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
else
|
|
723
|
-
{
|
|
724
|
-
SpessaSynthInfo(
|
|
725
|
-
`%cMIDI Name detected! %c"${this.midiName}"`,
|
|
726
|
-
consoleColors.info,
|
|
727
|
-
consoleColors.recognized
|
|
728
|
-
);
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// lyrics fix:
|
|
732
|
-
// sometimes, all lyrics events lack spaces at the start or end of the lyric
|
|
733
|
-
// then, and only then, add space at the end of each lyric
|
|
734
|
-
// space ASCII is 32
|
|
735
|
-
let lacksSpaces = true;
|
|
736
|
-
for (const lyric of this.lyrics)
|
|
737
|
-
{
|
|
738
|
-
if (lyric[0] === 32 || lyric[lyric.length - 1] === 32)
|
|
739
|
-
{
|
|
740
|
-
lacksSpaces = false;
|
|
741
|
-
break;
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
if (lacksSpaces)
|
|
746
|
-
{
|
|
747
|
-
this.lyrics = this.lyrics.map(lyric =>
|
|
748
|
-
{
|
|
749
|
-
// One exception: hyphens at the end. Don't add a space to them
|
|
750
|
-
if (lyric[lyric.length - 1] === 45)
|
|
751
|
-
{
|
|
752
|
-
return lyric;
|
|
753
|
-
}
|
|
754
|
-
const withSpaces = new Uint8Array(lyric.length + 1);
|
|
755
|
-
withSpaces.set(lyric, 0);
|
|
756
|
-
withSpaces[lyric.length] = 32;
|
|
757
|
-
return withSpaces;
|
|
758
|
-
});
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
// reverse the tempo changes
|
|
763
|
-
this.tempoChanges.reverse();
|
|
764
|
-
|
|
765
|
-
/**
|
|
766
|
-
* The total playback time, in seconds
|
|
767
|
-
* @type {number}
|
|
768
|
-
*/
|
|
769
|
-
this.duration = MIDIticksToSeconds(this.lastVoiceEventTick, this);
|
|
770
|
-
|
|
285
|
+
// parse the events
|
|
286
|
+
this._parseInternal();
|
|
771
287
|
SpessaSynthGroupEnd();
|
|
772
288
|
SpessaSynthInfo(
|
|
773
289
|
`%cMIDI file parsed. Total tick time: %c${this.lastVoiceEventTick}%c, total seconds time: %c${this.duration}`,
|
|
@@ -776,7 +292,6 @@ class MIDI extends BasicMIDI
|
|
|
776
292
|
consoleColors.info,
|
|
777
293
|
consoleColors.recognized
|
|
778
294
|
);
|
|
779
|
-
SpessaSynthGroupEnd();
|
|
780
295
|
}
|
|
781
296
|
|
|
782
297
|
/**
|
|
@@ -7,6 +7,24 @@ import { IndexedByteArray } from "../utils/indexed_array.js";
|
|
|
7
7
|
|
|
8
8
|
export class MidiMessage
|
|
9
9
|
{
|
|
10
|
+
/**
|
|
11
|
+
* Absolute number of MIDI ticks from the start of the track.
|
|
12
|
+
* @type {number}
|
|
13
|
+
*/
|
|
14
|
+
ticks;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The MIDI message status byte. Note that for meta events, it is the second byte. (not 0xFF)
|
|
18
|
+
* @type {number}
|
|
19
|
+
*/
|
|
20
|
+
messageStatusByte;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Message's binary data
|
|
24
|
+
* @type {IndexedByteArray}
|
|
25
|
+
*/
|
|
26
|
+
messageData;
|
|
27
|
+
|
|
10
28
|
/**
|
|
11
29
|
* @param ticks {number}
|
|
12
30
|
* @param byte {number} the message status byte
|
|
@@ -14,13 +32,8 @@ export class MidiMessage
|
|
|
14
32
|
*/
|
|
15
33
|
constructor(ticks, byte, data)
|
|
16
34
|
{
|
|
17
|
-
// absolute ticks from the start
|
|
18
35
|
this.ticks = ticks;
|
|
19
|
-
// message status byte (for meta it's the second byte)
|
|
20
36
|
this.messageStatusByte = byte;
|
|
21
|
-
/**
|
|
22
|
-
* @type {IndexedByteArray}
|
|
23
|
-
*/
|
|
24
37
|
this.messageData = data;
|
|
25
38
|
}
|
|
26
39
|
}
|
|
@@ -124,9 +124,9 @@ export class MIDISequenceData
|
|
|
124
124
|
format = 0;
|
|
125
125
|
|
|
126
126
|
/**
|
|
127
|
-
* The RMID (Resource
|
|
127
|
+
* The RMID (Resource-Interchangeable MIDI) info data, if the file is RMID formatted.
|
|
128
128
|
* Otherwise, this field is undefined.
|
|
129
|
-
* Chunk type (e.g. "INAM"): Chunk data as binary array.
|
|
129
|
+
* Chunk type (e.g. "INAM"): Chunk data as a binary array.
|
|
130
130
|
* @type {Object<string, IndexedByteArray>}
|
|
131
131
|
*/
|
|
132
132
|
RMIDInfo = {};
|