spessasynth_lib 3.23.13 → 3.24.0
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/@types/midi_parser/midi_data.d.ts +2 -0
- package/@types/midi_parser/midi_sequence.d.ts +11 -0
- package/@types/synthetizer/synth_event_handler.d.ts +157 -60
- package/@types/utils/other.d.ts +5 -0
- package/README.md +1 -1
- package/midi_parser/basic_midi.js +2 -0
- package/midi_parser/midi_data.js +2 -0
- package/midi_parser/midi_loader.js +150 -26
- package/midi_parser/midi_sequence.js +13 -0
- package/package.json +1 -1
- package/sequencer/worklet_sequencer/process_event.js +22 -2
- package/synthetizer/synth_event_handler.js +92 -71
- package/synthetizer/worklet_processor.min.js +12 -12
- package/synthetizer/worklet_system/message_protocol/message_sending.js +1 -1
- package/synthetizer/worklet_system/worklet_methods/system_exclusive.js +77 -0
- package/utils/other.js +28 -1
|
@@ -21,6 +21,7 @@ export class MidiData extends MIDISequenceData {
|
|
|
21
21
|
copyright: any;
|
|
22
22
|
tracksAmount: any;
|
|
23
23
|
lyrics: any;
|
|
24
|
+
lyricsTicks: any;
|
|
24
25
|
firstNoteOn: any;
|
|
25
26
|
keyRange: any;
|
|
26
27
|
lastVoiceEventTick: any;
|
|
@@ -35,6 +36,7 @@ export class MidiData extends MIDISequenceData {
|
|
|
35
36
|
format: any;
|
|
36
37
|
RMIDInfo: any;
|
|
37
38
|
bankOffset: any;
|
|
39
|
+
isKaraokeFile: any;
|
|
38
40
|
}
|
|
39
41
|
/**
|
|
40
42
|
* Temporary MIDI data used when the MIDI is not loaded.
|
|
@@ -40,6 +40,11 @@ export class MIDISequenceData {
|
|
|
40
40
|
* @type {Uint8Array[]}
|
|
41
41
|
*/
|
|
42
42
|
lyrics: Uint8Array[];
|
|
43
|
+
/**
|
|
44
|
+
* An array of tick positions where lyrics events occur in the sequence.
|
|
45
|
+
* @type {number[]}
|
|
46
|
+
*/
|
|
47
|
+
lyricsTicks: number[];
|
|
43
48
|
/**
|
|
44
49
|
* The tick position of the first note-on event in the MIDI sequence.
|
|
45
50
|
* @type {number}
|
|
@@ -121,4 +126,10 @@ export class MIDISequenceData {
|
|
|
121
126
|
* @type {number}
|
|
122
127
|
*/
|
|
123
128
|
bankOffset: number;
|
|
129
|
+
/**
|
|
130
|
+
* If the MIDI file is a Soft Karaoke file (.kar), this flag is set to true.
|
|
131
|
+
* https://www.mixagesoftware.com/en/midikit/help/HTML/karaoke_formats.html
|
|
132
|
+
* @type {boolean}
|
|
133
|
+
*/
|
|
134
|
+
isKaraokeFile: boolean;
|
|
124
135
|
}
|
|
@@ -3,62 +3,68 @@
|
|
|
3
3
|
* purpose: manages the synthesizer's event system, calling assinged functions when synthesizer requests dispatching the event
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* @
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
*
|
|
13
|
-
* @
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*
|
|
18
|
-
* @
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
*
|
|
23
|
-
* @
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
*
|
|
30
|
-
* @
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
*
|
|
36
|
-
* @
|
|
37
|
-
*
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
*
|
|
41
|
-
* @
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
*
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
* @typedef {
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* }
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
*
|
|
61
|
-
*
|
|
6
|
+
* @typedef {Object} NoteOnCallback
|
|
7
|
+
* @property {number} midiNote - The MIDI note number.
|
|
8
|
+
* @property {number} channel - The MIDI channel number.
|
|
9
|
+
* @property {number} velocity - The velocity of the note.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} NoteOffCallback
|
|
13
|
+
* @property {number} midiNote - The MIDI note number.
|
|
14
|
+
* @property {number} channel - The MIDI channel number.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} DrumChangeCallback
|
|
18
|
+
* @property {number} channel - The MIDI channel number.
|
|
19
|
+
* @property {boolean} isDrumChannel - Indicates if the channel is a drum channel.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} ProgramChangeCallback
|
|
23
|
+
* @property {number} channel - The MIDI channel number.
|
|
24
|
+
* @property {number} program - The program number.
|
|
25
|
+
* @property {number} bank - The bank number.
|
|
26
|
+
* @property {boolean} userCalled - Indicates if the change was user-initiated.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {Object} ControllerChangeCallback
|
|
30
|
+
* @property {number} channel - The MIDI channel number.
|
|
31
|
+
* @property {number} controllerNumber - The controller number.
|
|
32
|
+
* @property {number} controllerValue - The value of the controller.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {Object} MuteChannelCallback
|
|
36
|
+
* @property {number} channel - The MIDI channel number.
|
|
37
|
+
* @property {boolean} isMuted - Indicates if the channel is muted.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {Object} PresetListChangeCallbackSingle
|
|
41
|
+
* @property {string} presetName - The name of the preset.
|
|
42
|
+
* @property {number} bank - The bank number.
|
|
43
|
+
* @property {number} program - The program number.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {PresetListChangeCallbackSingle[]} PresetListChangeCallback - A list of preset objects.
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* @typedef {Object} SynthDisplayCallback
|
|
50
|
+
* @property {Uint8Array} displayData - The data to display.
|
|
51
|
+
* @property {SynthDisplayType} displayType - The type of display.
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {Object} PitchWheelCallback
|
|
55
|
+
* @property {number} channel - The MIDI channel number.
|
|
56
|
+
* @property {number} MSB - The most significant byte of the pitch wheel value.
|
|
57
|
+
* @property {number} LSB - The least significant byte of the pitch wheel value.
|
|
58
|
+
*/
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {Object} ChannelPressureCallback
|
|
61
|
+
* @property {number} channel - The MIDI channel number.
|
|
62
|
+
* @property {number} pressure - The pressure value.
|
|
63
|
+
*/
|
|
64
|
+
/**
|
|
65
|
+
* @typedef {Error} SoundfontErrorCallback - The error message for soundfont errors.
|
|
66
|
+
*/
|
|
67
|
+
/**
|
|
62
68
|
* @typedef {
|
|
63
69
|
* NoteOnCallback |
|
|
64
70
|
* NoteOffCallback |
|
|
@@ -70,6 +76,7 @@
|
|
|
70
76
|
* PitchWheelCallback |
|
|
71
77
|
* SoundfontErrorCallback |
|
|
72
78
|
* ChannelPressureCallback |
|
|
79
|
+
* SynthDisplayCallback |
|
|
73
80
|
* undefined
|
|
74
81
|
* } EventCallbackData
|
|
75
82
|
*/
|
|
@@ -88,7 +95,8 @@
|
|
|
88
95
|
* "mutechannel"|
|
|
89
96
|
* "presetlistchange"|
|
|
90
97
|
* "allcontrollerreset"|
|
|
91
|
-
* "soundfonterror"
|
|
98
|
+
* "soundfonterror"|
|
|
99
|
+
* "synthdisplay"} EventTypes
|
|
92
100
|
*/
|
|
93
101
|
export class EventHandler {
|
|
94
102
|
/**
|
|
@@ -122,45 +130,134 @@ export class EventHandler {
|
|
|
122
130
|
callEvent(name: EventTypes, eventData: EventCallbackData): void;
|
|
123
131
|
}
|
|
124
132
|
export type NoteOnCallback = {
|
|
133
|
+
/**
|
|
134
|
+
* - The MIDI note number.
|
|
135
|
+
*/
|
|
125
136
|
midiNote: number;
|
|
137
|
+
/**
|
|
138
|
+
* - The MIDI channel number.
|
|
139
|
+
*/
|
|
126
140
|
channel: number;
|
|
141
|
+
/**
|
|
142
|
+
* - The velocity of the note.
|
|
143
|
+
*/
|
|
127
144
|
velocity: number;
|
|
128
145
|
};
|
|
129
146
|
export type NoteOffCallback = {
|
|
147
|
+
/**
|
|
148
|
+
* - The MIDI note number.
|
|
149
|
+
*/
|
|
130
150
|
midiNote: number;
|
|
151
|
+
/**
|
|
152
|
+
* - The MIDI channel number.
|
|
153
|
+
*/
|
|
131
154
|
channel: number;
|
|
132
155
|
};
|
|
133
156
|
export type DrumChangeCallback = {
|
|
157
|
+
/**
|
|
158
|
+
* - The MIDI channel number.
|
|
159
|
+
*/
|
|
134
160
|
channel: number;
|
|
161
|
+
/**
|
|
162
|
+
* - Indicates if the channel is a drum channel.
|
|
163
|
+
*/
|
|
135
164
|
isDrumChannel: boolean;
|
|
136
165
|
};
|
|
137
166
|
export type ProgramChangeCallback = {
|
|
167
|
+
/**
|
|
168
|
+
* - The MIDI channel number.
|
|
169
|
+
*/
|
|
138
170
|
channel: number;
|
|
171
|
+
/**
|
|
172
|
+
* - The program number.
|
|
173
|
+
*/
|
|
139
174
|
program: number;
|
|
175
|
+
/**
|
|
176
|
+
* - The bank number.
|
|
177
|
+
*/
|
|
140
178
|
bank: number;
|
|
179
|
+
/**
|
|
180
|
+
* - Indicates if the change was user-initiated.
|
|
181
|
+
*/
|
|
141
182
|
userCalled: boolean;
|
|
142
183
|
};
|
|
143
184
|
export type ControllerChangeCallback = {
|
|
185
|
+
/**
|
|
186
|
+
* - The MIDI channel number.
|
|
187
|
+
*/
|
|
144
188
|
channel: number;
|
|
189
|
+
/**
|
|
190
|
+
* - The controller number.
|
|
191
|
+
*/
|
|
145
192
|
controllerNumber: number;
|
|
193
|
+
/**
|
|
194
|
+
* - The value of the controller.
|
|
195
|
+
*/
|
|
146
196
|
controllerValue: number;
|
|
147
197
|
};
|
|
148
198
|
export type MuteChannelCallback = {
|
|
199
|
+
/**
|
|
200
|
+
* - The MIDI channel number.
|
|
201
|
+
*/
|
|
149
202
|
channel: number;
|
|
203
|
+
/**
|
|
204
|
+
* - Indicates if the channel is muted.
|
|
205
|
+
*/
|
|
150
206
|
isMuted: boolean;
|
|
151
207
|
};
|
|
152
|
-
export type
|
|
208
|
+
export type PresetListChangeCallbackSingle = {
|
|
209
|
+
/**
|
|
210
|
+
* - The name of the preset.
|
|
211
|
+
*/
|
|
153
212
|
presetName: string;
|
|
213
|
+
/**
|
|
214
|
+
* - The bank number.
|
|
215
|
+
*/
|
|
154
216
|
bank: number;
|
|
217
|
+
/**
|
|
218
|
+
* - The program number.
|
|
219
|
+
*/
|
|
155
220
|
program: number;
|
|
156
|
-
}
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* - A list of preset objects.
|
|
224
|
+
*/
|
|
225
|
+
export type PresetListChangeCallback = PresetListChangeCallbackSingle[];
|
|
226
|
+
export type SynthDisplayCallback = {
|
|
227
|
+
/**
|
|
228
|
+
* - The data to display.
|
|
229
|
+
*/
|
|
230
|
+
displayData: Uint8Array;
|
|
231
|
+
/**
|
|
232
|
+
* - The type of display.
|
|
233
|
+
*/
|
|
234
|
+
displayType: SynthDisplayType;
|
|
235
|
+
};
|
|
157
236
|
export type PitchWheelCallback = {
|
|
237
|
+
/**
|
|
238
|
+
* - The MIDI channel number.
|
|
239
|
+
*/
|
|
158
240
|
channel: number;
|
|
241
|
+
/**
|
|
242
|
+
* - The most significant byte of the pitch wheel value.
|
|
243
|
+
*/
|
|
159
244
|
MSB: number;
|
|
245
|
+
/**
|
|
246
|
+
* - The least significant byte of the pitch wheel value.
|
|
247
|
+
*/
|
|
160
248
|
LSB: number;
|
|
161
249
|
};
|
|
162
250
|
export type ChannelPressureCallback = {
|
|
251
|
+
/**
|
|
252
|
+
* - The MIDI channel number.
|
|
253
|
+
*/
|
|
163
254
|
channel: number;
|
|
255
|
+
/**
|
|
256
|
+
* - The pressure value.
|
|
257
|
+
*/
|
|
164
258
|
pressure: number;
|
|
165
259
|
};
|
|
166
|
-
|
|
260
|
+
/**
|
|
261
|
+
* - The error message for soundfont errors.
|
|
262
|
+
*/
|
|
263
|
+
export type SoundfontErrorCallback = Error;
|
package/@types/utils/other.d.ts
CHANGED
|
@@ -23,6 +23,11 @@ export function formatTitle(fileName: string): string;
|
|
|
23
23
|
* @returns {string}
|
|
24
24
|
*/
|
|
25
25
|
export function arrayToHexString(arr: number[]): string;
|
|
26
|
+
/**
|
|
27
|
+
* @param eventData {Uint8Array}
|
|
28
|
+
* @returns {Uint8Array}
|
|
29
|
+
*/
|
|
30
|
+
export function sanitizeKarLyrics(eventData: Uint8Array): Uint8Array;
|
|
26
31
|
export namespace consoleColors {
|
|
27
32
|
let warn: string;
|
|
28
33
|
let unrecognized: string;
|
package/README.md
CHANGED
|
@@ -82,6 +82,7 @@ document.getElementById("button").onclick = async () =>
|
|
|
82
82
|
- **Easy MIDI editing:** Use [helper functions](https://github.com/spessasus/SpessaSynth/wiki/Writing-MIDI-Files#modifymidi) to modify the song to your needs!
|
|
83
83
|
- **Loop detection:** Automatically detects loops in MIDIs (e.g., from _Touhou Project_)
|
|
84
84
|
- **First note detection:** Skip unnecessary silence at the start by jumping to the first note!
|
|
85
|
+
- **Lyrics support:** Both regular MIDI and .kar files!
|
|
85
86
|
- **[Write MIDI files from scratch](https://github.com/spessasus/SpessaSynth/wiki/Creating-MIDI-Files)**
|
|
86
87
|
- **Easy saving:** Save with just [one function!](https://github.com/spessasus/SpessaSynth/wiki/Writing-MIDI-Files#writemidifile)
|
|
87
88
|
|
|
@@ -122,7 +123,6 @@ document.getElementById("button").onclick = async () =>
|
|
|
122
123
|
- **Loop multiple times:** *Render two (or more) loops into the file for seamless transitions!*
|
|
123
124
|
- *That's right, saving as WAV is also [just one function!](https://github.com/spessasus/SpessaSynth/wiki/Writing-Wave-Files#audiobuffertowav)*
|
|
124
125
|
|
|
125
|
-
|
|
126
126
|
# License
|
|
127
127
|
|
|
128
128
|
MIT License, except for the stbvorbis_sync.js in the `externals` folder which is licensed under the Apache-2.0 license.
|
|
@@ -45,10 +45,12 @@ export class BasicMIDI extends MIDISequenceData
|
|
|
45
45
|
m.loop = { ...mid.loop }; // Deep copy of loop
|
|
46
46
|
m.format = mid.format;
|
|
47
47
|
m.bankOffset = mid.bankOffset;
|
|
48
|
+
m.isKaraokeFile = mid.isKaraokeFile;
|
|
48
49
|
|
|
49
50
|
// Copying arrays
|
|
50
51
|
m.tempoChanges = [...mid.tempoChanges]; // Shallow copy
|
|
51
52
|
m.lyrics = mid.lyrics.map(arr => new Uint8Array(arr)); // Deep copy of each binary chunk
|
|
53
|
+
m.lyricsTicks = [...mid.lyricsTicks]; // Shallow copy
|
|
52
54
|
m.midiPorts = [...mid.midiPorts]; // Shallow copy
|
|
53
55
|
m.midiPortChannelOffsets = [...mid.midiPortChannelOffsets]; // Shallow copy
|
|
54
56
|
m.usedChannelsOnTrack = mid.usedChannelsOnTrack.map(set => new Set(set)); // Deep copy
|
package/midi_parser/midi_data.js
CHANGED
|
@@ -28,6 +28,7 @@ export class MidiData extends MIDISequenceData
|
|
|
28
28
|
this.copyright = midi.copyright;
|
|
29
29
|
this.tracksAmount = midi.tracksAmount;
|
|
30
30
|
this.lyrics = midi.lyrics;
|
|
31
|
+
this.lyricsTicks = midi.lyricsTicks;
|
|
31
32
|
this.firstNoteOn = midi.firstNoteOn;
|
|
32
33
|
this.keyRange = midi.keyRange;
|
|
33
34
|
this.lastVoiceEventTick = midi.lastVoiceEventTick;
|
|
@@ -42,6 +43,7 @@ export class MidiData extends MIDISequenceData
|
|
|
42
43
|
this.format = midi.format;
|
|
43
44
|
this.RMIDInfo = midi.RMIDInfo;
|
|
44
45
|
this.bankOffset = midi.bankOffset;
|
|
46
|
+
this.isKaraokeFile = midi.isKaraokeFile;
|
|
45
47
|
|
|
46
48
|
// Set isEmbedded based on the presence of an embeddedSoundFont
|
|
47
49
|
this.isEmbedded = midi.embeddedSoundFont !== undefined;
|
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
import { dataBytesAmount, getChannel, messageTypes, MidiMessage } from "./midi_message.js";
|
|
2
2
|
import { IndexedByteArray } from "../utils/indexed_array.js";
|
|
3
|
-
import { consoleColors, formatTitle } from "../utils/other.js";
|
|
3
|
+
import { consoleColors, formatTitle, sanitizeKarLyrics } 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 { readBytesAsString } from "../utils/byte_functions/string.js";
|
|
8
|
+
import { getStringBytes, 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
11
|
import { BasicMIDI, MIDIticksToSeconds } from "./basic_midi.js";
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
const GS_TEXT_HEADER = new Uint8Array([0x41, 0x10, 0x45, 0x12, 0x10, 0x00, 0x00]);
|
|
15
|
-
|
|
16
13
|
/**
|
|
17
14
|
* midi_loader.js
|
|
18
15
|
* purpose: parses a midi file for the seqyencer, including things like marker or CC 2/4 loop detection, copyright detection etc.
|
|
@@ -43,6 +40,12 @@ class MIDI extends BasicMIDI
|
|
|
43
40
|
|
|
44
41
|
let DLSRMID = false;
|
|
45
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Will be joined with "\n" to form the final string
|
|
45
|
+
* @type {string[]}
|
|
46
|
+
*/
|
|
47
|
+
let copyrightComponents = [];
|
|
48
|
+
|
|
46
49
|
const initialString = readBytesAsString(binaryData, 4);
|
|
47
50
|
binaryData.currentIndex -= 4;
|
|
48
51
|
if (initialString === "RIFF")
|
|
@@ -102,6 +105,7 @@ class MIDI extends BasicMIDI
|
|
|
102
105
|
}
|
|
103
106
|
if (this.RMIDInfo["ICOP"])
|
|
104
107
|
{
|
|
108
|
+
// special case, overwrites the copyright components array
|
|
105
109
|
copyrightDetected = true;
|
|
106
110
|
this.copyright = readBytesAsString(
|
|
107
111
|
this.RMIDInfo["ICOP"],
|
|
@@ -193,6 +197,13 @@ class MIDI extends BasicMIDI
|
|
|
193
197
|
let loopStart = null;
|
|
194
198
|
let loopEnd = null;
|
|
195
199
|
|
|
200
|
+
/**
|
|
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.
|
|
203
|
+
* @type {boolean}
|
|
204
|
+
*/
|
|
205
|
+
let karaokeHasTitle = false;
|
|
206
|
+
|
|
196
207
|
this.lastVoiceEventTick = 0;
|
|
197
208
|
|
|
198
209
|
/**
|
|
@@ -235,6 +246,8 @@ class MIDI extends BasicMIDI
|
|
|
235
246
|
throw new SyntaxError(`Invalid track header! Expected "MTrk" got "${trackChunk.type}"`);
|
|
236
247
|
}
|
|
237
248
|
|
|
249
|
+
let trackHasVoiceMessages = false;
|
|
250
|
+
|
|
238
251
|
/**
|
|
239
252
|
* MIDI running byte
|
|
240
253
|
* @type {number}
|
|
@@ -332,6 +345,7 @@ class MIDI extends BasicMIDI
|
|
|
332
345
|
{
|
|
333
346
|
case -2:
|
|
334
347
|
// since this is a meta message
|
|
348
|
+
const eventText = readBytesAsString(eventData, eventData.length);
|
|
335
349
|
switch (statusByte)
|
|
336
350
|
{
|
|
337
351
|
case messageTypes.setTempo:
|
|
@@ -344,7 +358,7 @@ class MIDI extends BasicMIDI
|
|
|
344
358
|
|
|
345
359
|
case messageTypes.marker:
|
|
346
360
|
// check for loop markers
|
|
347
|
-
const text =
|
|
361
|
+
const text = eventText.trim().toLowerCase();
|
|
348
362
|
switch (text)
|
|
349
363
|
{
|
|
350
364
|
default:
|
|
@@ -374,44 +388,95 @@ class MIDI extends BasicMIDI
|
|
|
374
388
|
case messageTypes.copyright:
|
|
375
389
|
if (!copyrightDetected)
|
|
376
390
|
{
|
|
377
|
-
|
|
391
|
+
|
|
392
|
+
eventData.currentIndex = 0;
|
|
393
|
+
copyrightComponents.push(readBytesAsString(
|
|
378
394
|
eventData,
|
|
379
395
|
eventData.length,
|
|
380
396
|
undefined,
|
|
381
397
|
false
|
|
382
|
-
)
|
|
398
|
+
));
|
|
383
399
|
}
|
|
384
400
|
break;
|
|
385
401
|
|
|
386
402
|
case messageTypes.lyric:
|
|
387
|
-
|
|
403
|
+
|
|
404
|
+
// note here: .kar files sometimes just use...
|
|
405
|
+
// lyrics instead of text because why not (of course)
|
|
406
|
+
// perform the same check for @KMIDI KARAOKE FILE
|
|
407
|
+
if (eventText.trim() === "@KMIDI KARAOKE FILE")
|
|
408
|
+
{
|
|
409
|
+
this.isKaraokeFile = true;
|
|
410
|
+
SpessaSynthInfo("%cKaraoke MIDI detected!", consoleColors.recognized);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (this.isKaraokeFile)
|
|
414
|
+
{
|
|
415
|
+
// replace the type of the message with text
|
|
416
|
+
message.messageStatusByte = messageTypes.text;
|
|
417
|
+
statusByte = messageTypes.text;
|
|
418
|
+
}
|
|
419
|
+
else
|
|
420
|
+
{
|
|
421
|
+
// add lyrics like a regular midi file
|
|
422
|
+
this.lyrics.push(eventData);
|
|
423
|
+
this.lyricsTicks.push(totalTicks);
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// kar: treat the same as text
|
|
428
|
+
// fallthrough
|
|
429
|
+
case messageTypes.text:
|
|
430
|
+
// possibly Soft Karaoke MIDI file
|
|
431
|
+
// it has a text event at the start of the file
|
|
432
|
+
// "@KMIDI KARAOKE FILE"
|
|
433
|
+
const checkedText = eventText.trim();
|
|
434
|
+
if (checkedText === "@KMIDI KARAOKE FILE")
|
|
435
|
+
{
|
|
436
|
+
this.isKaraokeFile = true;
|
|
437
|
+
|
|
438
|
+
SpessaSynthInfo("%cKaraoke MIDI detected!", consoleColors.recognized);
|
|
439
|
+
}
|
|
440
|
+
else if (this.isKaraokeFile)
|
|
441
|
+
{
|
|
442
|
+
// check for @T (title)
|
|
443
|
+
// or @A because it is a title too sometimes??? idk it's weird
|
|
444
|
+
if (checkedText.startsWith("@T") || checkedText.startsWith("@A"))
|
|
445
|
+
{
|
|
446
|
+
if (!karaokeHasTitle)
|
|
447
|
+
{
|
|
448
|
+
this.midiName = checkedText.substring(2).trim();
|
|
449
|
+
karaokeHasTitle = true;
|
|
450
|
+
nameDetected = true;
|
|
451
|
+
// encode to rawMidiName
|
|
452
|
+
this.rawMidiName = getStringBytes(this.midiName);
|
|
453
|
+
}
|
|
454
|
+
else
|
|
455
|
+
{
|
|
456
|
+
// append to copyright
|
|
457
|
+
copyrightComponents.push(checkedText.substring(2).trim());
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
else if (checkedText[0] !== "@")
|
|
461
|
+
{
|
|
462
|
+
// non @: the lyrics
|
|
463
|
+
this.lyrics.push(sanitizeKarLyrics(eventData));
|
|
464
|
+
this.lyricsTicks.push(totalTicks);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
break;
|
|
388
468
|
}
|
|
389
469
|
break;
|
|
390
470
|
|
|
391
471
|
case -3:
|
|
392
|
-
// since this is a sysex message
|
|
393
|
-
// check for embedded copyright (roland SC display sysex) http://www.bandtrax.com.au/sysex.htm
|
|
394
|
-
// header goes like this: 41 10 45 12 10 00 00
|
|
395
|
-
if (eventData.slice(0, 7).every((n, i) => GS_TEXT_HEADER[i] === n))
|
|
396
|
-
{
|
|
397
|
-
/**
|
|
398
|
-
* @type {IndexedByteArray}
|
|
399
|
-
*/
|
|
400
|
-
const cutText = eventData.slice(7, messageData.length - 3);
|
|
401
|
-
const decoded = readBytesAsString(cutText, cutText.length) + "\n";
|
|
402
|
-
this.copyright += decoded;
|
|
403
|
-
SpessaSynthInfo(
|
|
404
|
-
`%cDecoded Roland SC message! %c${decoded}`,
|
|
405
|
-
consoleColors.recognized,
|
|
406
|
-
consoleColors.value
|
|
407
|
-
);
|
|
408
|
-
}
|
|
472
|
+
// since this is a sysex message, do nothing
|
|
409
473
|
break;
|
|
410
474
|
|
|
411
475
|
|
|
412
476
|
default:
|
|
413
477
|
// since this is a voice message
|
|
414
478
|
// check for loop (CC 2/4)
|
|
479
|
+
trackHasVoiceMessages = true;
|
|
415
480
|
if ((statusByte & 0xF0) === messageTypes.controllerChange)
|
|
416
481
|
{
|
|
417
482
|
switch (eventData[0])
|
|
@@ -450,6 +515,20 @@ class MIDI extends BasicMIDI
|
|
|
450
515
|
}
|
|
451
516
|
this.tracks.push(track);
|
|
452
517
|
this.usedChannelsOnTrack.push(usedChannels);
|
|
518
|
+
|
|
519
|
+
// if the track has no voice messages, its "track name" event (if it has any)
|
|
520
|
+
// is some metadata. Add it to copyright
|
|
521
|
+
if (!trackHasVoiceMessages)
|
|
522
|
+
{
|
|
523
|
+
const trackName = track.find(e => e.messageStatusByte === messageTypes.trackName);
|
|
524
|
+
if (trackName)
|
|
525
|
+
{
|
|
526
|
+
trackName.messageData.currentIndex = 0;
|
|
527
|
+
const name = readBytesAsString(trackName.messageData, trackName.messageData.length);
|
|
528
|
+
copyrightComponents.push(name);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
453
532
|
SpessaSynthInfo(
|
|
454
533
|
`%cParsed %c${this.tracks.length}%c / %c${this.tracksAmount}`,
|
|
455
534
|
consoleColors.info,
|
|
@@ -569,6 +648,7 @@ class MIDI extends BasicMIDI
|
|
|
569
648
|
if (name)
|
|
570
649
|
{
|
|
571
650
|
this.rawMidiName = name.messageData;
|
|
651
|
+
name.messageData.currentIndex = 0;
|
|
572
652
|
this.midiName = readBytesAsString(name.messageData, name.messageData.length, undefined, false);
|
|
573
653
|
}
|
|
574
654
|
}
|
|
@@ -580,11 +660,23 @@ class MIDI extends BasicMIDI
|
|
|
580
660
|
if (name)
|
|
581
661
|
{
|
|
582
662
|
this.rawMidiName = name.messageData;
|
|
663
|
+
name.messageData.currentIndex = 0;
|
|
583
664
|
this.midiName = readBytesAsString(name.messageData, name.messageData.length, undefined, false);
|
|
584
665
|
}
|
|
585
666
|
}
|
|
586
667
|
}
|
|
587
668
|
|
|
669
|
+
if (!copyrightDetected)
|
|
670
|
+
{
|
|
671
|
+
this.copyright = copyrightComponents
|
|
672
|
+
// trim and group newlines into one
|
|
673
|
+
.map(c => c.trim().replace(/(\r?\n)+/g, "\n"))
|
|
674
|
+
// remove empty strings
|
|
675
|
+
.filter(c => c.length > 0)
|
|
676
|
+
// join with newlines
|
|
677
|
+
.join("\n") || "";
|
|
678
|
+
}
|
|
679
|
+
|
|
588
680
|
this.fileName = fileName;
|
|
589
681
|
this.midiName = this.midiName.trim();
|
|
590
682
|
this.midiNameUsesFileName = false;
|
|
@@ -612,6 +704,38 @@ class MIDI extends BasicMIDI
|
|
|
612
704
|
consoleColors.recognized
|
|
613
705
|
);
|
|
614
706
|
}
|
|
707
|
+
|
|
708
|
+
// lyrics fix:
|
|
709
|
+
// sometimes, all lyrics events lack spaces at the start or end of the lyric
|
|
710
|
+
// then, and only then, add space at the end of each lyric
|
|
711
|
+
// space ASCII is 32
|
|
712
|
+
let lacksSpaces = true;
|
|
713
|
+
for (const lyric of this.lyrics)
|
|
714
|
+
{
|
|
715
|
+
if (lyric[0] === 32 || lyric[lyric.length - 1] === 32)
|
|
716
|
+
{
|
|
717
|
+
lacksSpaces = false;
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (lacksSpaces)
|
|
723
|
+
{
|
|
724
|
+
this.lyrics = this.lyrics.map(lyric =>
|
|
725
|
+
{
|
|
726
|
+
// one exception: hyphens at the end. Don't add a space to them
|
|
727
|
+
if (lyric[lyric.length - 1] === 45)
|
|
728
|
+
{
|
|
729
|
+
return lyric;
|
|
730
|
+
}
|
|
731
|
+
const withSpaces = new Uint8Array(lyric.length + 1);
|
|
732
|
+
withSpaces.set(lyric, 0);
|
|
733
|
+
withSpaces[lyric.length] = 32;
|
|
734
|
+
return withSpaces;
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
|
|
615
739
|
// reverse the tempo changes
|
|
616
740
|
this.tempoChanges.reverse();
|
|
617
741
|
|