spessasynth_lib 3.23.13 → 3.23.14

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.
@@ -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
  }
@@ -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;
@@ -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
@@ -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,11 +1,11 @@
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";
@@ -193,6 +193,13 @@ class MIDI extends BasicMIDI
193
193
  let loopStart = null;
194
194
  let loopEnd = null;
195
195
 
196
+ /**
197
+ * For karaoke files, text events starting with @T are considered titles
198
+ * usually the first one is the title, and the latter are things such as "sequenced by" etc.
199
+ * @type {boolean}
200
+ */
201
+ let karaokeHasTitle = false;
202
+
196
203
  this.lastVoiceEventTick = 0;
197
204
 
198
205
  /**
@@ -332,6 +339,7 @@ class MIDI extends BasicMIDI
332
339
  {
333
340
  case -2:
334
341
  // since this is a meta message
342
+ const eventText = readBytesAsString(eventData, eventData.length);
335
343
  switch (statusByte)
336
344
  {
337
345
  case messageTypes.setTempo:
@@ -344,7 +352,7 @@ class MIDI extends BasicMIDI
344
352
 
345
353
  case messageTypes.marker:
346
354
  // check for loop markers
347
- const text = readBytesAsString(eventData, eventData.length).trim().toLowerCase();
355
+ const text = eventText.trim().toLowerCase();
348
356
  switch (text)
349
357
  {
350
358
  default:
@@ -384,7 +392,71 @@ class MIDI extends BasicMIDI
384
392
  break;
385
393
 
386
394
  case messageTypes.lyric:
387
- this.lyrics.push(eventData);
395
+
396
+ // note here: .kar files sometimes just use...
397
+ // lyrics instead of text because why not (of course)
398
+ // perform the same check for @KMIDI KARAOKE FILE
399
+ if (eventText.trim() === "@KMIDI KARAOKE FILE")
400
+ {
401
+ this.isKaraokeFile = true;
402
+ SpessaSynthInfo("%cKaraoke MIDI detected!", consoleColors.recognized);
403
+ }
404
+
405
+ if (this.isKaraokeFile)
406
+ {
407
+ // replace the type of the message with text
408
+ message.messageStatusByte = messageTypes.text;
409
+ statusByte = messageTypes.text;
410
+ }
411
+ else
412
+ {
413
+ // add lyrics like a regular midi file
414
+ this.lyrics.push(eventData);
415
+ this.lyricsTicks.push(totalTicks);
416
+ break;
417
+ }
418
+
419
+ // kar: treat the same as text
420
+ // fallthrough
421
+ case messageTypes.text:
422
+ // possibly Soft Karaoke MIDI file
423
+ // it has a text event at the start of the file
424
+ // "@KMIDI KARAOKE FILE"
425
+ const checkedText = eventText.trim();
426
+ if (checkedText === "@KMIDI KARAOKE FILE")
427
+ {
428
+ this.isKaraokeFile = true;
429
+
430
+ SpessaSynthInfo("%cKaraoke MIDI detected!", consoleColors.recognized);
431
+ }
432
+ else if (this.isKaraokeFile)
433
+ {
434
+ // check for @T (title)
435
+ // or @A because it is a title too sometimes??? idk it's weird
436
+ if (checkedText.startsWith("@T") || checkedText.startsWith("@A"))
437
+ {
438
+ if (!karaokeHasTitle)
439
+ {
440
+ this.midiName = checkedText.substring(2).trim();
441
+ karaokeHasTitle = true;
442
+ nameDetected = true;
443
+ // encode to rawMidiName
444
+ this.rawMidiName = getStringBytes(this.midiName);
445
+ }
446
+ else
447
+ {
448
+ // append to copyright
449
+ this.copyright += checkedText.substring(2).trim() + " \n";
450
+ }
451
+ }
452
+ else if (checkedText[0] !== "@")
453
+ {
454
+ // non @: the lyrics
455
+ this.lyrics.push(sanitizeKarLyrics(eventData));
456
+ this.lyricsTicks.push(totalTicks);
457
+ }
458
+ }
459
+ break;
388
460
  }
389
461
  break;
390
462
 
@@ -612,6 +684,38 @@ class MIDI extends BasicMIDI
612
684
  consoleColors.recognized
613
685
  );
614
686
  }
687
+
688
+ // lyrics fix:
689
+ // sometimes, all lyrics events lack spaces at the start or end of the lyric
690
+ // then, and only then, add space at the end of each lyric
691
+ // space ASCII is 32
692
+ let lacksSpaces = true;
693
+ for (const lyric of this.lyrics)
694
+ {
695
+ if (lyric[0] === 32 || lyric[lyric.length - 1] === 32)
696
+ {
697
+ lacksSpaces = false;
698
+ break;
699
+ }
700
+ }
701
+
702
+ if (lacksSpaces)
703
+ {
704
+ this.lyrics = this.lyrics.map(lyric =>
705
+ {
706
+ // one exception: hyphens at the end. Don't add a space to them
707
+ if (lyric[lyric.length - 1] === 45)
708
+ {
709
+ return lyric;
710
+ }
711
+ const withSpaces = new Uint8Array(lyric.length + 1);
712
+ withSpaces.set(lyric, 0);
713
+ withSpaces[lyric.length] = 32;
714
+ return withSpaces;
715
+ });
716
+ }
717
+
718
+
615
719
  // reverse the tempo changes
616
720
  this.tempoChanges.reverse();
617
721
 
@@ -44,6 +44,12 @@ export class MIDISequenceData
44
44
  */
45
45
  lyrics = [];
46
46
 
47
+ /**
48
+ * An array of tick positions where lyrics events occur in the sequence.
49
+ * @type {number[]}
50
+ */
51
+ lyricsTicks = [];
52
+
47
53
  /**
48
54
  * The tick position of the first note-on event in the MIDI sequence.
49
55
  * @type {number}
@@ -130,4 +136,11 @@ export class MIDISequenceData
130
136
  * @type {number}
131
137
  */
132
138
  bankOffset = 0;
139
+
140
+ /**
141
+ * If the MIDI file is a Soft Karaoke file (.kar), this flag is set to true.
142
+ * https://www.mixagesoftware.com/en/midikit/help/HTML/karaoke_formats.html
143
+ * @type {boolean}
144
+ */
145
+ isKaraokeFile = false;
133
146
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spessasynth_lib",
3
- "version": "3.23.13",
3
+ "version": "3.23.14",
4
4
  "description": "MIDI and SoundFont2/DLS library with no compromises",
5
5
  "browser": "index.js",
6
6
  "types": "@types/index.d.ts",
@@ -121,13 +121,33 @@ export function _processEvent(event, trackIndex)
121
121
  if (statusByteData.status === messageTypes.lyric)
122
122
  {
123
123
  lyricsIndex = Math.min(
124
- this.midiData.lyrics.indexOf(event.messageData) + 1,
124
+ this.midiData.lyricsTicks.indexOf(event.ticks) + 1,
125
125
  this.midiData.lyrics.length - 1
126
126
  );
127
127
  }
128
+ let sentStatus = statusByteData.status;
129
+ // if MIDI is a karaoke file, it uses the "text" event type or "lyrics" for lyrics (duh)
130
+ // why?
131
+ // because the MIDI standard is a messy pile of garbage and it's not my fault that it's like this :(
132
+ // I'm just trying to make the best out of a bad situation
133
+ // I'm sorry
134
+ // okay i should get back to work
135
+ // anyways,
136
+ // check for karaoke file and change the status byte to "lyric" if it's a karaoke file
137
+ if (this.midiData.isKaraokeFile && (
138
+ statusByteData.status === messageTypes.text ||
139
+ statusByteData.status === messageTypes.lyric
140
+ ))
141
+ {
142
+ lyricsIndex = Math.min(
143
+ this.midiData.lyricsTicks.indexOf(event.ticks) + 1,
144
+ this.midiData.lyricsTicks.length
145
+ );
146
+ sentStatus = messageTypes.lyric;
147
+ }
128
148
  this.post(
129
149
  WorkletSequencerReturnMessageType.textEvent,
130
- [event.messageData, statusByteData.status, lyricsIndex]
150
+ [event.messageData, sentStatus, lyricsIndex]
131
151
  );
132
152
  break;
133
153