spessasynth_lib 3.23.12 → 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/index.d.ts CHANGED
@@ -3,6 +3,13 @@ import { Synthetizer } from './synthetizer/synthetizer.js';
3
3
  import { DEFAULT_PERCUSSION } from './synthetizer/synthetizer.js';
4
4
  import { VOICE_CAP } from './synthetizer/synthetizer.js';
5
5
  import { BasicSoundFont } from "./soundfont/basic_soundfont/basic_soundfont.js";
6
+ import { BasicSample } from "./soundfont/basic_soundfont/basic_sample.js";
7
+ import { BasicInstrumentZone } from "./soundfont/basic_soundfont/basic_zones.js";
8
+ import { BasicInstrument } from "./soundfont/basic_soundfont/basic_instrument.js";
9
+ import { BasicPreset } from "./soundfont/basic_soundfont/basic_preset.js";
10
+ import { BasicPresetZone } from "./soundfont/basic_soundfont/basic_zones.js";
11
+ import { Generator } from "./soundfont/basic_soundfont/generator.js";
12
+ import { Modulator } from "./soundfont/basic_soundfont/modulator.js";
6
13
  import { loadSoundFont } from "./soundfont/load_soundfont.js";
7
14
  import { trimSoundfont } from "./soundfont/basic_soundfont/write_sf2/soundfont_trimmer.js";
8
15
  import { modulatorSources } from "./soundfont/basic_soundfont/modulator.js";
@@ -34,4 +41,4 @@ import { readBytesAsUintBigEndian } from './utils/byte_functions/big_endian.js';
34
41
  import { NON_CC_INDEX_OFFSET } from "./synthetizer/worklet_system/worklet_utilities/controller_tables.js";
35
42
  import { ALL_CHANNELS_OR_DIFFERENT_ACTION } from './synthetizer/worklet_system/message_protocol/worklet_message.js';
36
43
  import { WORKLET_URL_ABSOLUTE } from './synthetizer/worklet_url.js';
37
- export { Sequencer, Synthetizer, DEFAULT_PERCUSSION, VOICE_CAP, BasicSoundFont, loadSoundFont, trimSoundfont, modulatorSources, MIDI, MIDIBuilder, IndexedByteArray, writeMIDIFile, writeRMIDI, applySnapshotToMIDI, modifyMIDI, MIDIticksToSeconds, audioBufferToWav, SpessaSynthLogging, SpessaSynthGroup, SpessaSynthTable, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn, SpessaSynthGroupCollapsed, midiControllers, messageTypes, MIDIDeviceHandler, WebMidiLinkHandler, arrayToHexString, consoleColors, formatTitle, formatTime, readBytesAsUintBigEndian, NON_CC_INDEX_OFFSET, ALL_CHANNELS_OR_DIFFERENT_ACTION, WORKLET_URL_ABSOLUTE };
44
+ export { Sequencer, Synthetizer, DEFAULT_PERCUSSION, VOICE_CAP, BasicSoundFont, BasicSample, BasicInstrumentZone, BasicInstrument, BasicPreset, BasicPresetZone, Generator, Modulator, loadSoundFont, trimSoundfont, modulatorSources, MIDI, MIDIBuilder, IndexedByteArray, writeMIDIFile, writeRMIDI, applySnapshotToMIDI, modifyMIDI, MIDIticksToSeconds, audioBufferToWav, SpessaSynthLogging, SpessaSynthGroup, SpessaSynthTable, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn, SpessaSynthGroupCollapsed, midiControllers, messageTypes, MIDIDeviceHandler, WebMidiLinkHandler, arrayToHexString, consoleColors, formatTitle, formatTime, readBytesAsUintBigEndian, NON_CC_INDEX_OFFSET, ALL_CHANNELS_OR_DIFFERENT_ACTION, WORKLET_URL_ABSOLUTE };
@@ -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,10 +23,12 @@ export class Sequencer {
23
23
  private onSongChange;
24
24
  /**
25
25
  * Fires on text event
26
+ * @type {function}
26
27
  * @param data {Uint8Array} the data text
27
28
  * @param type {number} the status byte of the message (the meta status byte)
29
+ * @param lyricsIndex {number} if the text is a lyric, the index of the lyric in midiData.lyrics, otherwise -1
28
30
  */
29
- onTextEvent: any;
31
+ onTextEvent: Function;
30
32
  /**
31
33
  * Fires when CurrentTime changes
32
34
  * @type {Object<string, function(number)>} the time that was changed to
@@ -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/index.js CHANGED
@@ -1,6 +1,13 @@
1
1
  // Import modules
2
2
  import { loadSoundFont } from "./soundfont/load_soundfont.js";
3
3
  import { BasicSoundFont } from "./soundfont/basic_soundfont/basic_soundfont.js";
4
+ import { BasicSample } from "./soundfont/basic_soundfont/basic_sample.js";
5
+ import { BasicInstrumentZone } from "./soundfont/basic_soundfont/basic_zones.js";
6
+ import { BasicInstrument } from "./soundfont/basic_soundfont/basic_instrument.js";
7
+ import { Generator } from "./soundfont/basic_soundfont/generator.js";
8
+ import { Modulator } from "./soundfont/basic_soundfont/modulator.js";
9
+ import { BasicPresetZone } from "./soundfont/basic_soundfont/basic_zones.js";
10
+ import { BasicPreset } from "./soundfont/basic_soundfont/basic_preset.js";
4
11
  import { MIDI } from './midi_parser/midi_loader.js';
5
12
  import { MIDIticksToSeconds } from './midi_parser/basic_midi.js';
6
13
  import { MIDIBuilder } from "./midi_parser/midi_builder.js";
@@ -38,13 +45,20 @@ export {
38
45
  Synthetizer,
39
46
  DEFAULT_PERCUSSION,
40
47
  VOICE_CAP,
41
-
48
+
42
49
  // SoundFont
43
50
  BasicSoundFont,
51
+ BasicSample,
52
+ BasicInstrumentZone,
53
+ BasicInstrument,
54
+ BasicPreset,
55
+ BasicPresetZone,
56
+ Generator,
57
+ Modulator,
44
58
  loadSoundFont,
45
59
  trimSoundfont,
46
60
  modulatorSources,
47
-
61
+
48
62
  // MIDI
49
63
  MIDI,
50
64
  MIDIBuilder,
@@ -54,7 +68,7 @@ export {
54
68
  applySnapshotToMIDI,
55
69
  modifyMIDI,
56
70
  MIDIticksToSeconds,
57
-
71
+
58
72
  // Utilities
59
73
  audioBufferToWav,
60
74
  SpessaSynthLogging,
@@ -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.12",
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",
@@ -61,8 +61,10 @@ export class Sequencer
61
61
  onSongChange = {};
62
62
  /**
63
63
  * Fires on text event
64
+ * @type {function}
64
65
  * @param data {Uint8Array} the data text
65
66
  * @param type {number} the status byte of the message (the meta status byte)
67
+ * @param lyricsIndex {number} if the text is a lyric, the index of the lyric in midiData.lyrics, otherwise -1
66
68
  */
67
69
  onTextEvent;
68
70
  /**
@@ -304,7 +306,6 @@ export class Sequencer
304
306
  addOnSongChangeEvent(callback, id)
305
307
  {
306
308
  this.onSongChange[id] = callback;
307
- callback(this.midiData);
308
309
  }
309
310
 
310
311
  /**
@@ -419,13 +420,9 @@ export class Sequencer
419
420
  break;
420
421
 
421
422
  case WorkletSequencerReturnMessageType.textEvent:
422
- /**
423
- * @type {[Uint8Array, number]}
424
- */
425
- let textEventData = messageData;
426
423
  if (this.onTextEvent)
427
424
  {
428
- this.onTextEvent(textEventData[0], textEventData[1]);
425
+ this.onTextEvent(...(messageData));
429
426
  }
430
427
  break;
431
428
 
@@ -117,7 +117,38 @@ export function _processEvent(event, trackIndex)
117
117
  case messageTypes.cuePoint:
118
118
  case messageTypes.instrumentName:
119
119
  case messageTypes.programName:
120
- this.post(WorkletSequencerReturnMessageType.textEvent, [event.messageData, statusByteData.status]);
120
+ let lyricsIndex = -1;
121
+ if (statusByteData.status === messageTypes.lyric)
122
+ {
123
+ lyricsIndex = Math.min(
124
+ this.midiData.lyricsTicks.indexOf(event.ticks) + 1,
125
+ this.midiData.lyrics.length - 1
126
+ );
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
+ }
148
+ this.post(
149
+ WorkletSequencerReturnMessageType.textEvent,
150
+ [event.messageData, sentStatus, lyricsIndex]
151
+ );
121
152
  break;
122
153
 
123
154
  case messageTypes.midiPort:
@@ -35,7 +35,7 @@ export const WorkletSequencerMessageType = {
35
35
  export const WorkletSequencerReturnMessageType = {
36
36
  midiEvent: 0, // [...midiEventBytes<number>]
37
37
  songChange: 1, // [midiData<MidiData>, songIndex<number>, isAutoPlayed<boolean>]
38
- textEvent: 2, // [messageData<number[]>, statusByte<number]
38
+ textEvent: 2, // [messageData<number[]>, statusByte<number>, lyricsIndex<number>]
39
39
  timeChange: 3, // newAbsoluteTime<number>
40
40
  pause: 4, // no data
41
41
  getMIDI: 5, // midiData<MIDI>
@@ -219,7 +219,6 @@ class WorkletSequencer
219
219
  for (let c = 0; c < MIDI_CHANNEL_COUNT; c++)
220
220
  {
221
221
  this.sendMIDICC(c, midiControllers.allNotesOff, 0);
222
- this.sendMIDICC(c, midiControllers.allSoundOff, 0);
223
222
  }
224
223
  }
225
224
  }