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.
Files changed (61) hide show
  1. package/midi_parser/basic_midi.js +457 -68
  2. package/midi_parser/midi_loader.js +18 -503
  3. package/midi_parser/midi_message.js +18 -5
  4. package/midi_parser/midi_sequence.js +2 -2
  5. package/package.json +1 -1
  6. package/sequencer/worklet_sequencer/process_event.js +1 -6
  7. package/synthetizer/synthetizer.js +9 -6
  8. package/synthetizer/worklet_processor.min.js +12 -12
  9. package/synthetizer/worklet_system/README.md +2 -2
  10. package/synthetizer/worklet_system/main_processor.js +106 -95
  11. package/synthetizer/worklet_system/message_protocol/handle_message.js +22 -17
  12. package/synthetizer/worklet_system/message_protocol/worklet_message.js +2 -1
  13. package/synthetizer/worklet_system/snapshot/apply_synthesizer_snapshot.js +14 -0
  14. package/synthetizer/worklet_system/snapshot/channel_snapshot.js +166 -0
  15. package/synthetizer/worklet_system/snapshot/send_synthesizer_snapshot.js +14 -0
  16. package/synthetizer/worklet_system/snapshot/synthesizer_snapshot.js +121 -0
  17. package/synthetizer/worklet_system/worklet_methods/controller_control/controller_change.js +196 -0
  18. package/synthetizer/worklet_system/worklet_methods/controller_control/master_parameters.js +34 -0
  19. package/synthetizer/worklet_system/worklet_methods/{reset_controllers.js → controller_control/reset_controllers.js} +33 -39
  20. package/synthetizer/worklet_system/worklet_methods/create_worklet_channel.js +26 -0
  21. package/synthetizer/worklet_system/worklet_methods/{data_entry.js → data_entry/data_entry_coarse.js} +38 -105
  22. package/synthetizer/worklet_system/worklet_methods/data_entry/data_entry_fine.js +64 -0
  23. package/synthetizer/worklet_system/worklet_methods/mute_channel.js +17 -0
  24. package/synthetizer/worklet_system/worklet_methods/note_on.js +36 -34
  25. package/synthetizer/worklet_system/worklet_methods/program_change.js +49 -0
  26. package/synthetizer/worklet_system/worklet_methods/{voice_control.js → render_voice.js} +37 -120
  27. package/synthetizer/worklet_system/worklet_methods/soundfont_management/clear_sound_font.js +35 -0
  28. package/synthetizer/worklet_system/worklet_methods/soundfont_management/get_preset.js +20 -0
  29. package/synthetizer/worklet_system/worklet_methods/soundfont_management/reload_sound_font.js +43 -0
  30. package/synthetizer/worklet_system/worklet_methods/soundfont_management/send_preset_list.js +31 -0
  31. package/synthetizer/worklet_system/worklet_methods/soundfont_management/set_embedded_sound_font.js +21 -0
  32. package/synthetizer/worklet_system/worklet_methods/stopping_notes/kill_note.js +19 -0
  33. package/synthetizer/worklet_system/worklet_methods/stopping_notes/note_off.js +51 -0
  34. package/synthetizer/worklet_system/worklet_methods/stopping_notes/stop_all_channels.js +16 -0
  35. package/synthetizer/worklet_system/worklet_methods/stopping_notes/stop_all_notes.js +30 -0
  36. package/synthetizer/worklet_system/worklet_methods/stopping_notes/voice_killing.js +63 -0
  37. package/synthetizer/worklet_system/worklet_methods/system_exclusive.js +31 -30
  38. package/synthetizer/worklet_system/worklet_methods/tuning_control/channel_pressure.js +24 -0
  39. package/synthetizer/worklet_system/worklet_methods/tuning_control/pitch_wheel.js +33 -0
  40. package/synthetizer/worklet_system/worklet_methods/tuning_control/poly_pressure.js +31 -0
  41. package/synthetizer/worklet_system/worklet_methods/tuning_control/set_master_tuning.js +15 -0
  42. package/synthetizer/worklet_system/worklet_methods/tuning_control/set_modulation_depth.js +27 -0
  43. package/synthetizer/worklet_system/worklet_methods/tuning_control/set_octave_tuning.js +15 -0
  44. package/synthetizer/worklet_system/worklet_methods/tuning_control/set_tuning.js +24 -0
  45. package/synthetizer/worklet_system/worklet_methods/tuning_control/set_tuning_semitones.js +19 -0
  46. package/synthetizer/worklet_system/worklet_methods/tuning_control/transpose_all_channels.js +15 -0
  47. package/synthetizer/worklet_system/worklet_methods/tuning_control/transpose_channel.js +31 -0
  48. package/synthetizer/worklet_system/worklet_utilities/controller_tables.js +10 -1
  49. package/synthetizer/worklet_system/worklet_utilities/lfo.js +2 -1
  50. package/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js +4 -4
  51. package/synthetizer/worklet_system/worklet_utilities/modulator_curves.js +4 -5
  52. package/synthetizer/worklet_system/worklet_utilities/stereo_panner.js +18 -18
  53. package/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js +210 -206
  54. package/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js +354 -108
  55. package/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +22 -9
  56. package/synthetizer/worklet_system/snapshot/snapshot.js +0 -311
  57. package/synthetizer/worklet_system/worklet_methods/controller_control.js +0 -260
  58. package/synthetizer/worklet_system/worklet_methods/note_off.js +0 -119
  59. package/synthetizer/worklet_system/worklet_methods/program_control.js +0 -282
  60. package/synthetizer/worklet_system/worklet_methods/tuning_control.js +0 -233
  61. 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 { MIDISequenceData } from "./midi_sequence.js";
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
- * Updates all internal values
80
+ * Parses internal MIDI values
81
+ * @protected
71
82
  */
72
- flush()
83
+ _parseInternal()
73
84
  {
74
-
75
- // find first note on
76
- const firstNoteOns = [];
77
- for (const t of this.tracks)
78
- {
79
- // sost the track by ticks
80
- t.sort((e1, e2) => e1.ticks - e2.ticks);
81
- const firstNoteOn = t.find(e => (e.messageStatusByte & 0xF0) === messageTypes.noteOn);
82
- if (firstNoteOn)
83
- {
84
- firstNoteOns.push(firstNoteOn.ticks);
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
- let portOffset = 0;
98
+
99
99
  /**
100
- * @type {Set<number>[]}
100
+ * Will be joined with "\n" to form the final string
101
+ * @type {string[]}
101
102
  */
102
- this.usedChannelsOnTrack = this.tracks.map(() => new Set());
103
- this.tracks.forEach((t, trackNum) =>
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
- t.forEach(e =>
128
+ let trackHasVoiceMessages = false;
129
+
130
+ for (const e of track)
107
131
  {
108
- // last voice event tick
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
- // tempo, used channels, port
118
- if (e.messageStatusByte === messageTypes.setTempo)
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
- this.tempoChanges.push({
121
- ticks: e.ticks,
122
- tempo: 60000000 / readBytesAsUintBigEndian(
123
- e.messageData,
124
- 3
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
- else if ((e.messageStatusByte & 0xF0) === messageTypes.noteOn)
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
- this.usedChannelsOnTrack[trackNum].add(e.messageData[0]);
333
+ trackName.messageData.currentIndex = 0;
334
+ const name = readBytesAsString(trackName.messageData, trackName.messageData.length);
335
+ copyrightComponents.push(name);
131
336
  }
132
- else if (e.messageStatusByte === messageTypes.midiPort)
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 and compute duration
340
+ // reverse the tempo changes
148
341
  this.tempoChanges.reverse();
149
- this.duration = MIDIticksToSeconds(this.lastVoiceEventTick, this);
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, otherwise take the first midi port and replace all -1 with it
154
- // why do this? some midis (for some reason) specify all channels to port 1 or else, but leave the conductor track with no port pref.
155
- // this spessasynth to reserve the first 16 channels for the conductor track (which doesn't play anything) and use additional 16 for the actual ports.
156
- let defaultP = 0;
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
- defaultP = port;
414
+ defaultPort = port;
162
415
  break;
163
416
  }
164
417
  }
165
- this.midiPorts = this.midiPorts.map(port => port === -1 ? defaultP : port);
166
- // add dummy port if empty
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