spessasynth_lib 3.24.13 → 3.24.15
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 +9 -6
- 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 +19 -0
- package/synthetizer/worklet_system/worklet_methods/stopping_notes/note_off.js +51 -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,6 +1,9 @@
|
|
|
1
|
+
import { MIDISequenceData } from "./midi_sequence.js";
|
|
2
|
+
import { getStringBytes, readBytesAsString } from "../utils/byte_functions/string.js";
|
|
1
3
|
import { messageTypes } from "./midi_message.js";
|
|
2
4
|
import { readBytesAsUintBigEndian } from "../utils/byte_functions/big_endian.js";
|
|
3
|
-
import {
|
|
5
|
+
import { SpessaSynthGroup, SpessaSynthGroupEnd, SpessaSynthInfo } from "../utils/loggin.js";
|
|
6
|
+
import { consoleColors, formatTitle, sanitizeKarLyrics } from "../utils/other.js";
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* BasicMIDI is the base of a complete MIDI file, used by the sequencer internally.
|
|
@@ -23,6 +26,12 @@ export class BasicMIDI extends MIDISequenceData
|
|
|
23
26
|
*/
|
|
24
27
|
tracks = [];
|
|
25
28
|
|
|
29
|
+
/**
|
|
30
|
+
* If the MIDI file is a DLS RMIDI file.
|
|
31
|
+
* @type {boolean}
|
|
32
|
+
*/
|
|
33
|
+
isDLSRMIDI = false;
|
|
34
|
+
|
|
26
35
|
/**
|
|
27
36
|
* Copies a MIDI
|
|
28
37
|
* @param mid {BasicMIDI}
|
|
@@ -46,6 +55,7 @@ export class BasicMIDI extends MIDISequenceData
|
|
|
46
55
|
m.format = mid.format;
|
|
47
56
|
m.bankOffset = mid.bankOffset;
|
|
48
57
|
m.isKaraokeFile = mid.isKaraokeFile;
|
|
58
|
+
m.isDLSRMIDI = mid.isDLSRMIDI;
|
|
49
59
|
|
|
50
60
|
// Copying arrays
|
|
51
61
|
m.tempoChanges = [...mid.tempoChanges]; // Shallow copy
|
|
@@ -55,7 +65,7 @@ export class BasicMIDI extends MIDISequenceData
|
|
|
55
65
|
m.midiPortChannelOffsets = [...mid.midiPortChannelOffsets]; // Shallow copy
|
|
56
66
|
m.usedChannelsOnTrack = mid.usedChannelsOnTrack.map(set => new Set(set)); // Deep copy
|
|
57
67
|
m.rawMidiName = mid.rawMidiName ? new Uint8Array(mid.rawMidiName) : undefined; // Deep copy
|
|
58
|
-
m.embeddedSoundFont = mid.embeddedSoundFont ? mid.embeddedSoundFont.slice() : undefined; // Deep copy
|
|
68
|
+
m.embeddedSoundFont = mid.embeddedSoundFont ? mid.embeddedSoundFont.slice(0) : undefined; // Deep copy
|
|
59
69
|
|
|
60
70
|
// Copying RMID Info object (deep copy)
|
|
61
71
|
m.RMIDInfo = { ...mid.RMIDInfo };
|
|
@@ -67,107 +77,486 @@ export class BasicMIDI extends MIDISequenceData
|
|
|
67
77
|
}
|
|
68
78
|
|
|
69
79
|
/**
|
|
70
|
-
*
|
|
80
|
+
* Parses internal MIDI values
|
|
81
|
+
* @protected
|
|
71
82
|
*/
|
|
72
|
-
|
|
83
|
+
_parseInternal()
|
|
73
84
|
{
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
this.firstNoteOn = Math.min(...firstNoteOns);
|
|
88
|
-
|
|
89
|
-
// find tempo changes
|
|
90
|
-
// and used channels on tracks
|
|
91
|
-
// and midi ports
|
|
92
|
-
// and last voice event tick
|
|
93
|
-
// and loop
|
|
94
|
-
this.lastVoiceEventTick = 0;
|
|
95
|
-
this.tempoChanges = [{ ticks: 0, tempo: 120 }];
|
|
85
|
+
SpessaSynthGroup(
|
|
86
|
+
"%cInterpreting MIDI events...",
|
|
87
|
+
consoleColors.info
|
|
88
|
+
);
|
|
89
|
+
/**
|
|
90
|
+
* For karaoke files, text events starting with @T are considered titles,
|
|
91
|
+
* usually the first one is the title, and the latter is things such as "sequenced by" etc.
|
|
92
|
+
* @type {boolean}
|
|
93
|
+
*/
|
|
94
|
+
let karaokeHasTitle = false;
|
|
95
|
+
let portOffset = 0;
|
|
96
96
|
this.midiPorts = [];
|
|
97
97
|
this.midiPortChannelOffsets = [];
|
|
98
|
-
|
|
98
|
+
|
|
99
99
|
/**
|
|
100
|
-
*
|
|
100
|
+
* Will be joined with "\n" to form the final string
|
|
101
|
+
* @type {string[]}
|
|
101
102
|
*/
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
let copyrightComponents = [];
|
|
104
|
+
let copyrightDetected = false;
|
|
105
|
+
if (typeof this.RMIDInfo["ICOP"] !== "undefined")
|
|
106
|
+
{
|
|
107
|
+
// if RMIDI has copyright info, don't try to detect one.
|
|
108
|
+
copyrightDetected = true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
let nameDetected = false;
|
|
113
|
+
if (typeof this.RMIDInfo["INAM"] !== "undefined")
|
|
114
|
+
{
|
|
115
|
+
// same as with copyright
|
|
116
|
+
nameDetected = true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// loop tracking
|
|
120
|
+
let loopStart = null;
|
|
121
|
+
let loopEnd = null;
|
|
122
|
+
|
|
123
|
+
for (let i = 0; i < this.tracks.length; i++)
|
|
104
124
|
{
|
|
125
|
+
const track = this.tracks[i];
|
|
126
|
+
const usedChannels = new Set();
|
|
105
127
|
this.midiPorts.push(-1);
|
|
106
|
-
|
|
128
|
+
let trackHasVoiceMessages = false;
|
|
129
|
+
|
|
130
|
+
for (const e of track)
|
|
107
131
|
{
|
|
108
|
-
//
|
|
132
|
+
// check if it's a voice message
|
|
109
133
|
if (e.messageStatusByte >= 0x80 && e.messageStatusByte < 0xF0)
|
|
110
134
|
{
|
|
135
|
+
trackHasVoiceMessages = true;
|
|
136
|
+
// voice messages are 7-bit always
|
|
137
|
+
for (let j = 0; j < e.messageData.length; j++)
|
|
138
|
+
{
|
|
139
|
+
e.messageData[j] = Math.min(127, e.messageData[j]);
|
|
140
|
+
}
|
|
141
|
+
// last voice event tick
|
|
111
142
|
if (e.ticks > this.lastVoiceEventTick)
|
|
112
143
|
{
|
|
113
144
|
this.lastVoiceEventTick = e.ticks;
|
|
114
145
|
}
|
|
146
|
+
|
|
147
|
+
// interpret the voice message
|
|
148
|
+
switch (e.messageStatusByte & 0xF0)
|
|
149
|
+
{
|
|
150
|
+
// cc change: loop points
|
|
151
|
+
case messageTypes.controllerChange:
|
|
152
|
+
switch (e.messageData[0])
|
|
153
|
+
{
|
|
154
|
+
case 2:
|
|
155
|
+
case 116:
|
|
156
|
+
loopStart = e.ticks;
|
|
157
|
+
break;
|
|
158
|
+
|
|
159
|
+
case 4:
|
|
160
|
+
case 117:
|
|
161
|
+
if (loopEnd === null)
|
|
162
|
+
{
|
|
163
|
+
loopEnd = e.ticks;
|
|
164
|
+
}
|
|
165
|
+
else
|
|
166
|
+
{
|
|
167
|
+
// this controller has occurred more than once;
|
|
168
|
+
// this means
|
|
169
|
+
// that it doesn't indicate the loop
|
|
170
|
+
loopEnd = 0;
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
|
|
174
|
+
case 0:
|
|
175
|
+
// check RMID
|
|
176
|
+
if (this.isDLSRMIDI && e.messageData[1] !== 0 && e.messageData[1] !== 127)
|
|
177
|
+
{
|
|
178
|
+
SpessaSynthInfo(
|
|
179
|
+
"%cDLS RMIDI with offset 1 detected!",
|
|
180
|
+
consoleColors.recognized
|
|
181
|
+
);
|
|
182
|
+
this.bankOffset = 1;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
break;
|
|
186
|
+
|
|
187
|
+
// note on: used notes tracking and key range
|
|
188
|
+
case messageTypes.noteOn:
|
|
189
|
+
usedChannels.add(e.messageStatusByte & 0x0F);
|
|
190
|
+
const note = e.messageData[0];
|
|
191
|
+
this.keyRange.min = Math.min(this.keyRange.min, note);
|
|
192
|
+
this.keyRange.max = Math.max(this.keyRange.max, note);
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
115
195
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
196
|
+
e.messageData.currentIndex = 0;
|
|
197
|
+
const eventText = readBytesAsString(e.messageData, e.messageData.length);
|
|
198
|
+
e.messageData.currentIndex = 0;
|
|
199
|
+
// interpret the message
|
|
200
|
+
switch (e.messageStatusByte)
|
|
119
201
|
{
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
202
|
+
case messageTypes.setTempo:
|
|
203
|
+
// add the tempo change
|
|
204
|
+
e.messageData.currentIndex = 0;
|
|
205
|
+
this.tempoChanges.push({
|
|
206
|
+
ticks: e.ticks,
|
|
207
|
+
tempo: 60000000 / readBytesAsUintBigEndian(e.messageData, 3)
|
|
208
|
+
});
|
|
209
|
+
e.messageData.currentIndex = 0;
|
|
210
|
+
break;
|
|
211
|
+
|
|
212
|
+
case messageTypes.marker:
|
|
213
|
+
// check for loop markers
|
|
214
|
+
const text = eventText.trim().toLowerCase();
|
|
215
|
+
switch (text)
|
|
216
|
+
{
|
|
217
|
+
default:
|
|
218
|
+
break;
|
|
219
|
+
|
|
220
|
+
case "start":
|
|
221
|
+
case "loopstart":
|
|
222
|
+
loopStart = e.ticks;
|
|
223
|
+
break;
|
|
224
|
+
|
|
225
|
+
case "loopend":
|
|
226
|
+
loopEnd = e.ticks;
|
|
227
|
+
}
|
|
228
|
+
e.messageData.currentIndex = 0;
|
|
229
|
+
break;
|
|
230
|
+
|
|
231
|
+
case messageTypes.midiPort:
|
|
232
|
+
const port = e.messageData[0];
|
|
233
|
+
this.midiPorts[i] = port;
|
|
234
|
+
if (this.midiPortChannelOffsets[port] === undefined)
|
|
235
|
+
{
|
|
236
|
+
this.midiPortChannelOffsets[port] = portOffset;
|
|
237
|
+
portOffset += 16;
|
|
238
|
+
}
|
|
239
|
+
break;
|
|
240
|
+
|
|
241
|
+
case messageTypes.copyright:
|
|
242
|
+
if (!copyrightDetected)
|
|
243
|
+
{
|
|
244
|
+
e.messageData.currentIndex = 0;
|
|
245
|
+
copyrightComponents.push(readBytesAsString(
|
|
246
|
+
e.messageData,
|
|
247
|
+
e.messageData.length,
|
|
248
|
+
undefined,
|
|
249
|
+
false
|
|
250
|
+
));
|
|
251
|
+
e.messageData.currentIndex = 0;
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
|
|
255
|
+
case messageTypes.lyric:
|
|
256
|
+
// note here: .kar files sometimes just use...
|
|
257
|
+
// lyrics instead of text because why not (of course)
|
|
258
|
+
// perform the same check for @KMIDI KARAOKE FILE
|
|
259
|
+
if (eventText.trim().startsWith("@KMIDI KARAOKE FILE"))
|
|
260
|
+
{
|
|
261
|
+
this.isKaraokeFile = true;
|
|
262
|
+
SpessaSynthInfo("%cKaraoke MIDI detected!", consoleColors.recognized);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (this.isKaraokeFile)
|
|
266
|
+
{
|
|
267
|
+
// replace the type of the message with text
|
|
268
|
+
e.messageStatusByte = messageTypes.text;
|
|
269
|
+
}
|
|
270
|
+
else
|
|
271
|
+
{
|
|
272
|
+
// add lyrics like a regular midi file
|
|
273
|
+
this.lyrics.push(e.messageData);
|
|
274
|
+
this.lyricsTicks.push(e.ticks);
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// kar: treat the same as text
|
|
279
|
+
// fallthrough
|
|
280
|
+
case messageTypes.text:
|
|
281
|
+
// possibly Soft Karaoke MIDI file
|
|
282
|
+
// it has a text event at the start of the file
|
|
283
|
+
// "@KMIDI KARAOKE FILE"
|
|
284
|
+
const checkedText = eventText.trim();
|
|
285
|
+
if (checkedText.startsWith("@KMIDI KARAOKE FILE"))
|
|
286
|
+
{
|
|
287
|
+
this.isKaraokeFile = true;
|
|
288
|
+
|
|
289
|
+
SpessaSynthInfo("%cKaraoke MIDI detected!", consoleColors.recognized);
|
|
290
|
+
}
|
|
291
|
+
else if (this.isKaraokeFile)
|
|
292
|
+
{
|
|
293
|
+
// check for @T (title)
|
|
294
|
+
// or @A because it is a title too sometimes?
|
|
295
|
+
// IDK it's strange
|
|
296
|
+
if (checkedText.startsWith("@T") || checkedText.startsWith("@A"))
|
|
297
|
+
{
|
|
298
|
+
if (!karaokeHasTitle)
|
|
299
|
+
{
|
|
300
|
+
this.midiName = checkedText.substring(2).trim();
|
|
301
|
+
karaokeHasTitle = true;
|
|
302
|
+
nameDetected = true;
|
|
303
|
+
// encode to rawMidiName
|
|
304
|
+
this.rawMidiName = getStringBytes(this.midiName);
|
|
305
|
+
}
|
|
306
|
+
else
|
|
307
|
+
{
|
|
308
|
+
// append to copyright
|
|
309
|
+
copyrightComponents.push(checkedText.substring(2).trim());
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
else if (checkedText[0] !== "@")
|
|
313
|
+
{
|
|
314
|
+
// non @: the lyrics
|
|
315
|
+
this.lyrics.push(sanitizeKarLyrics(e.messageData));
|
|
316
|
+
this.lyricsTicks.push(e.ticks);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
break;
|
|
127
320
|
}
|
|
128
|
-
|
|
321
|
+
}
|
|
322
|
+
// add used channels
|
|
323
|
+
this.usedChannelsOnTrack.push(usedChannels);
|
|
324
|
+
|
|
325
|
+
// If the track has no voice messages, its "track name" event (if it has any)
|
|
326
|
+
// is some metadata.
|
|
327
|
+
// Add it to copyright
|
|
328
|
+
if (!trackHasVoiceMessages)
|
|
329
|
+
{
|
|
330
|
+
const trackName = track.find(e => e.messageStatusByte === messageTypes.trackName);
|
|
331
|
+
if (trackName)
|
|
129
332
|
{
|
|
130
|
-
|
|
333
|
+
trackName.messageData.currentIndex = 0;
|
|
334
|
+
const name = readBytesAsString(trackName.messageData, trackName.messageData.length);
|
|
335
|
+
copyrightComponents.push(name);
|
|
131
336
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const port = e.messageData[0];
|
|
135
|
-
this.midiPorts[trackNum] = port;
|
|
136
|
-
if (this.midiPortChannelOffsets[port] === undefined)
|
|
137
|
-
{
|
|
138
|
-
this.midiPortChannelOffsets[port] = portOffset;
|
|
139
|
-
portOffset += 16;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
this.loop = { start: this.firstNoteOn, end: this.lastVoiceEventTick };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
146
339
|
|
|
147
|
-
// reverse tempo
|
|
340
|
+
// reverse the tempo changes
|
|
148
341
|
this.tempoChanges.reverse();
|
|
149
|
-
|
|
342
|
+
|
|
343
|
+
SpessaSynthInfo(
|
|
344
|
+
`%cCorrecting loops, ports and detecting notes...`,
|
|
345
|
+
consoleColors.info
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
const firstNoteOns = [];
|
|
349
|
+
for (const t of this.tracks)
|
|
350
|
+
{
|
|
351
|
+
const firstNoteOn = t.find(e => (e.messageStatusByte & 0xF0) === messageTypes.noteOn);
|
|
352
|
+
if (firstNoteOn)
|
|
353
|
+
{
|
|
354
|
+
firstNoteOns.push(firstNoteOn.ticks);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
this.firstNoteOn = Math.min(...firstNoteOns);
|
|
358
|
+
|
|
359
|
+
SpessaSynthInfo(
|
|
360
|
+
`%cFirst note-on detected at: %c${this.firstNoteOn}%c ticks!`,
|
|
361
|
+
consoleColors.info,
|
|
362
|
+
consoleColors.recognized,
|
|
363
|
+
consoleColors.info
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
if (loopStart !== null && loopEnd === null)
|
|
368
|
+
{
|
|
369
|
+
// not a loop
|
|
370
|
+
loopStart = this.firstNoteOn;
|
|
371
|
+
loopEnd = this.lastVoiceEventTick;
|
|
372
|
+
}
|
|
373
|
+
else
|
|
374
|
+
{
|
|
375
|
+
if (loopStart === null)
|
|
376
|
+
{
|
|
377
|
+
loopStart = this.firstNoteOn;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (loopEnd === null || loopEnd === 0)
|
|
381
|
+
{
|
|
382
|
+
loopEnd = this.lastVoiceEventTick;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
*
|
|
388
|
+
* @type {{start: number, end: number}}
|
|
389
|
+
*/
|
|
390
|
+
this.loop = { start: loopStart, end: loopEnd };
|
|
391
|
+
|
|
392
|
+
SpessaSynthInfo(
|
|
393
|
+
`%cLoop points: start: %c${this.loop.start}%c end: %c${this.loop.end}`,
|
|
394
|
+
consoleColors.info,
|
|
395
|
+
consoleColors.recognized,
|
|
396
|
+
consoleColors.info,
|
|
397
|
+
consoleColors.recognized
|
|
398
|
+
);
|
|
150
399
|
|
|
151
400
|
// fix midi ports:
|
|
152
401
|
// midi tracks without ports will have a value of -1
|
|
153
|
-
// if all ports have a value of -1, set it to 0,
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
|
|
402
|
+
// if all ports have a value of -1, set it to 0,
|
|
403
|
+
// otherwise take the first midi port and replace all -1 with it,
|
|
404
|
+
// why would we do this?
|
|
405
|
+
// some midis (for some reason) specify all channels to port 1 or else,
|
|
406
|
+
// but leave the conductor track with no port pref.
|
|
407
|
+
// this spessasynth to reserve the first 16 channels for the conductor track
|
|
408
|
+
// (which doesn't play anything) and use the additional 16 for the actual ports.
|
|
409
|
+
let defaultPort = 0;
|
|
157
410
|
for (let port of this.midiPorts)
|
|
158
411
|
{
|
|
159
412
|
if (port !== -1)
|
|
160
413
|
{
|
|
161
|
-
|
|
414
|
+
defaultPort = port;
|
|
162
415
|
break;
|
|
163
416
|
}
|
|
164
417
|
}
|
|
165
|
-
this.midiPorts = this.midiPorts.map(port => port === -1 ?
|
|
166
|
-
// add
|
|
418
|
+
this.midiPorts = this.midiPorts.map(port => port === -1 ? defaultPort : port);
|
|
419
|
+
// add fake port if empty
|
|
167
420
|
if (this.midiPortChannelOffsets.length === 0)
|
|
168
421
|
{
|
|
169
422
|
this.midiPortChannelOffsets = [0];
|
|
170
423
|
}
|
|
424
|
+
if (this.midiPortChannelOffsets.length < 2)
|
|
425
|
+
{
|
|
426
|
+
SpessaSynthInfo(`%cNo additional MIDI Ports detected.`, consoleColors.info);
|
|
427
|
+
}
|
|
428
|
+
else
|
|
429
|
+
{
|
|
430
|
+
SpessaSynthInfo(`%cMIDI Ports detected!`, consoleColors.recognized);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// midi name
|
|
434
|
+
if (!nameDetected)
|
|
435
|
+
{
|
|
436
|
+
if (this.tracks.length > 1)
|
|
437
|
+
{
|
|
438
|
+
// if more than 1 track and the first track has no notes,
|
|
439
|
+
// just find the first trackName in the first track.
|
|
440
|
+
if (
|
|
441
|
+
this.tracks[0].find(
|
|
442
|
+
message => message.messageStatusByte >= messageTypes.noteOn
|
|
443
|
+
&&
|
|
444
|
+
message.messageStatusByte < messageTypes.polyPressure
|
|
445
|
+
) === undefined
|
|
446
|
+
)
|
|
447
|
+
{
|
|
448
|
+
|
|
449
|
+
let name = this.tracks[0].find(message => message.messageStatusByte === messageTypes.trackName);
|
|
450
|
+
if (name)
|
|
451
|
+
{
|
|
452
|
+
this.rawMidiName = name.messageData;
|
|
453
|
+
name.messageData.currentIndex = 0;
|
|
454
|
+
this.midiName = readBytesAsString(name.messageData, name.messageData.length, undefined, false);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
else
|
|
459
|
+
{
|
|
460
|
+
// if only 1 track, find the first "track name" event
|
|
461
|
+
let name = this.tracks[0].find(message => message.messageStatusByte === messageTypes.trackName);
|
|
462
|
+
if (name)
|
|
463
|
+
{
|
|
464
|
+
this.rawMidiName = name.messageData;
|
|
465
|
+
name.messageData.currentIndex = 0;
|
|
466
|
+
this.midiName = readBytesAsString(name.messageData, name.messageData.length, undefined, false);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (!copyrightDetected)
|
|
472
|
+
{
|
|
473
|
+
this.copyright = copyrightComponents
|
|
474
|
+
// trim and group newlines into one
|
|
475
|
+
.map(c => c.trim().replace(/(\r?\n)+/g, "\n"))
|
|
476
|
+
// remove empty strings
|
|
477
|
+
.filter(c => c.length > 0)
|
|
478
|
+
// join with newlines
|
|
479
|
+
.join("\n") || "";
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
this.midiName = this.midiName.trim();
|
|
483
|
+
this.midiNameUsesFileName = false;
|
|
484
|
+
// if midiName is "", use the file name
|
|
485
|
+
if (this.midiName.length === 0)
|
|
486
|
+
{
|
|
487
|
+
SpessaSynthInfo(
|
|
488
|
+
`%cNo name detected. Using the alt name!`,
|
|
489
|
+
consoleColors.info
|
|
490
|
+
);
|
|
491
|
+
this.midiName = formatTitle(this.fileName);
|
|
492
|
+
this.midiNameUsesFileName = true;
|
|
493
|
+
// encode it too
|
|
494
|
+
this.rawMidiName = new Uint8Array(this.midiName.length);
|
|
495
|
+
for (let i = 0; i < this.midiName.length; i++)
|
|
496
|
+
{
|
|
497
|
+
this.rawMidiName[i] = this.midiName.charCodeAt(i);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
else
|
|
501
|
+
{
|
|
502
|
+
SpessaSynthInfo(
|
|
503
|
+
`%cMIDI Name detected! %c"${this.midiName}"`,
|
|
504
|
+
consoleColors.info,
|
|
505
|
+
consoleColors.recognized
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// lyrics fix:
|
|
510
|
+
// sometimes, all lyrics events lack spaces at the start or end of the lyric
|
|
511
|
+
// then, and only then, add space at the end of each lyric
|
|
512
|
+
// space ASCII is 32
|
|
513
|
+
let lacksSpaces = true;
|
|
514
|
+
for (const lyric of this.lyrics)
|
|
515
|
+
{
|
|
516
|
+
if (lyric[0] === 32 || lyric[lyric.length - 1] === 32)
|
|
517
|
+
{
|
|
518
|
+
lacksSpaces = false;
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (lacksSpaces)
|
|
524
|
+
{
|
|
525
|
+
this.lyrics = this.lyrics.map(lyric =>
|
|
526
|
+
{
|
|
527
|
+
// One exception: hyphens at the end. Don't add a space to them
|
|
528
|
+
if (lyric[lyric.length - 1] === 45)
|
|
529
|
+
{
|
|
530
|
+
return lyric;
|
|
531
|
+
}
|
|
532
|
+
const withSpaces = new Uint8Array(lyric.length + 1);
|
|
533
|
+
withSpaces.set(lyric, 0);
|
|
534
|
+
withSpaces[lyric.length] = 32;
|
|
535
|
+
return withSpaces;
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* The total playback time, in seconds
|
|
540
|
+
* @type {number}
|
|
541
|
+
*/
|
|
542
|
+
this.duration = MIDIticksToSeconds(this.lastVoiceEventTick, this);
|
|
543
|
+
|
|
544
|
+
SpessaSynthInfo("%cSuccess!", consoleColors.recognized);
|
|
545
|
+
SpessaSynthGroupEnd();
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Updates all internal values
|
|
550
|
+
*/
|
|
551
|
+
flush()
|
|
552
|
+
{
|
|
553
|
+
|
|
554
|
+
for (const t of this.tracks)
|
|
555
|
+
{
|
|
556
|
+
// sort the track by ticks
|
|
557
|
+
t.sort((e1, e2) => e1.ticks - e2.ticks);
|
|
558
|
+
}
|
|
559
|
+
this._parseInternal();
|
|
171
560
|
}
|
|
172
561
|
}
|
|
173
562
|
|
|
@@ -183,7 +572,7 @@ export function MIDIticksToSeconds(ticks, mid)
|
|
|
183
572
|
|
|
184
573
|
while (ticks > 0)
|
|
185
574
|
{
|
|
186
|
-
// tempo changes are reversed so the first element is the last tempo change
|
|
575
|
+
// tempo changes are reversed, so the first element is the last tempo change
|
|
187
576
|
// and the last element is the first tempo change
|
|
188
577
|
// (always at tick 0 and tempo 120)
|
|
189
578
|
// find the last tempo change that has occurred
|