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.
- package/@types/midi_parser/midi_data.d.ts +2 -0
- package/@types/midi_parser/midi_sequence.d.ts +11 -0
- package/@types/utils/other.d.ts +5 -0
- package/midi_parser/basic_midi.js +2 -0
- package/midi_parser/midi_data.js +2 -0
- package/midi_parser/midi_loader.js +108 -4
- package/midi_parser/midi_sequence.js +13 -0
- package/package.json +1 -1
- package/sequencer/worklet_sequencer/process_event.js +22 -2
- package/synthetizer/worklet_processor.min.js +13 -12
- 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
|
}
|
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;
|
|
@@ -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,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 =
|
|
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
|
-
|
|
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
|
@@ -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.
|
|
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,
|
|
150
|
+
[event.messageData, sentStatus, lyricsIndex]
|
|
131
151
|
);
|
|
132
152
|
break;
|
|
133
153
|
|