spessasynth_core 1.1.2 → 1.1.4

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 (189) hide show
  1. package/LICENSE +3 -26
  2. package/README.md +156 -474
  3. package/index.js +73 -8
  4. package/package.json +21 -8
  5. package/src/externals/fflate/LICENSE +21 -0
  6. package/src/externals/fflate/fflate.min.js +1 -0
  7. package/src/externals/stbvorbis_sync/@types/stbvorbis_sync.d.ts +12 -0
  8. package/src/externals/stbvorbis_sync/LICENSE +202 -0
  9. package/src/externals/stbvorbis_sync/NOTICE +6 -0
  10. package/src/externals/stbvorbis_sync/stbvorbis_sync.min.js +1 -0
  11. package/src/midi/README.md +32 -0
  12. package/src/midi/basic_midi.js +567 -0
  13. package/src/midi/midi_builder.js +202 -0
  14. package/src/midi/midi_loader.js +324 -0
  15. package/{spessasynth_core/midi_parser → src/midi}/midi_message.js +58 -35
  16. package/src/midi/midi_sequence.js +224 -0
  17. package/src/midi/midi_tools/get_note_times.js +154 -0
  18. package/src/midi/midi_tools/midi_editor.js +611 -0
  19. package/src/midi/midi_tools/midi_writer.js +99 -0
  20. package/src/midi/midi_tools/rmidi_writer.js +567 -0
  21. package/src/midi/midi_tools/used_keys_loaded.js +238 -0
  22. package/src/midi/xmf_loader.js +454 -0
  23. package/src/sequencer/README.md +5 -0
  24. package/src/sequencer/events.js +81 -0
  25. package/src/sequencer/play.js +349 -0
  26. package/src/sequencer/process_event.js +165 -0
  27. package/{spessasynth_core/sequencer/worklet_sequencer → src/sequencer}/process_tick.js +103 -84
  28. package/src/sequencer/sequencer_engine.js +367 -0
  29. package/src/sequencer/song_control.js +201 -0
  30. package/src/soundfont/README.md +13 -0
  31. package/src/soundfont/basic_soundfont/basic_instrument.js +77 -0
  32. package/src/soundfont/basic_soundfont/basic_preset.js +336 -0
  33. package/src/soundfont/basic_soundfont/basic_sample.js +206 -0
  34. package/src/soundfont/basic_soundfont/basic_soundfont.js +565 -0
  35. package/src/soundfont/basic_soundfont/basic_zone.js +64 -0
  36. package/src/soundfont/basic_soundfont/basic_zones.js +43 -0
  37. package/src/soundfont/basic_soundfont/generator.js +220 -0
  38. package/src/soundfont/basic_soundfont/modulator.js +378 -0
  39. package/src/soundfont/basic_soundfont/riff_chunk.js +149 -0
  40. package/src/soundfont/basic_soundfont/write_dls/art2.js +173 -0
  41. package/src/soundfont/basic_soundfont/write_dls/articulator.js +49 -0
  42. package/src/soundfont/basic_soundfont/write_dls/combine_zones.js +400 -0
  43. package/src/soundfont/basic_soundfont/write_dls/ins.js +103 -0
  44. package/src/soundfont/basic_soundfont/write_dls/lins.js +18 -0
  45. package/src/soundfont/basic_soundfont/write_dls/modulator_converter.js +330 -0
  46. package/src/soundfont/basic_soundfont/write_dls/rgn2.js +121 -0
  47. package/src/soundfont/basic_soundfont/write_dls/wave.js +94 -0
  48. package/src/soundfont/basic_soundfont/write_dls/write_dls.js +119 -0
  49. package/src/soundfont/basic_soundfont/write_dls/wsmp.js +78 -0
  50. package/src/soundfont/basic_soundfont/write_dls/wvpl.js +32 -0
  51. package/src/soundfont/basic_soundfont/write_sf2/ibag.js +39 -0
  52. package/src/soundfont/basic_soundfont/write_sf2/igen.js +80 -0
  53. package/src/soundfont/basic_soundfont/write_sf2/imod.js +46 -0
  54. package/src/soundfont/basic_soundfont/write_sf2/inst.js +34 -0
  55. package/src/soundfont/basic_soundfont/write_sf2/pbag.js +39 -0
  56. package/src/soundfont/basic_soundfont/write_sf2/pgen.js +82 -0
  57. package/src/soundfont/basic_soundfont/write_sf2/phdr.js +42 -0
  58. package/src/soundfont/basic_soundfont/write_sf2/pmod.js +46 -0
  59. package/src/soundfont/basic_soundfont/write_sf2/sdta.js +80 -0
  60. package/src/soundfont/basic_soundfont/write_sf2/shdr.js +55 -0
  61. package/src/soundfont/basic_soundfont/write_sf2/write.js +222 -0
  62. package/src/soundfont/dls/articulator_converter.js +396 -0
  63. package/src/soundfont/dls/dls_destinations.js +38 -0
  64. package/src/soundfont/dls/dls_preset.js +44 -0
  65. package/src/soundfont/dls/dls_sample.js +75 -0
  66. package/src/soundfont/dls/dls_soundfont.js +186 -0
  67. package/src/soundfont/dls/dls_sources.js +62 -0
  68. package/src/soundfont/dls/dls_zone.js +95 -0
  69. package/src/soundfont/dls/read_articulation.js +299 -0
  70. package/src/soundfont/dls/read_instrument.js +121 -0
  71. package/src/soundfont/dls/read_instrument_list.js +17 -0
  72. package/src/soundfont/dls/read_lart.js +35 -0
  73. package/src/soundfont/dls/read_region.js +152 -0
  74. package/src/soundfont/dls/read_samples.js +270 -0
  75. package/src/soundfont/load_soundfont.js +21 -0
  76. package/src/soundfont/read_sf2/generators.js +46 -0
  77. package/{spessasynth_core/soundfont/chunk → src/soundfont/read_sf2}/instruments.js +20 -14
  78. package/src/soundfont/read_sf2/modulators.js +36 -0
  79. package/src/soundfont/read_sf2/presets.js +80 -0
  80. package/src/soundfont/read_sf2/samples.js +304 -0
  81. package/src/soundfont/read_sf2/soundfont.js +305 -0
  82. package/{spessasynth_core/soundfont/chunk → src/soundfont/read_sf2}/zones.js +68 -69
  83. package/src/synthetizer/README.md +7 -0
  84. package/src/synthetizer/audio_engine/README.md +9 -0
  85. package/src/synthetizer/audio_engine/engine_components/compute_modulator.js +266 -0
  86. package/src/synthetizer/audio_engine/engine_components/controller_tables.js +88 -0
  87. package/src/synthetizer/audio_engine/engine_components/key_modifier_manager.js +150 -0
  88. package/{spessasynth_core/synthetizer/worklet_system/worklet_utilities → src/synthetizer/audio_engine/engine_components}/lfo.js +9 -6
  89. package/src/synthetizer/audio_engine/engine_components/lowpass_filter.js +282 -0
  90. package/src/synthetizer/audio_engine/engine_components/midi_audio_channel.js +467 -0
  91. package/src/synthetizer/audio_engine/engine_components/modulation_envelope.js +181 -0
  92. package/{spessasynth_core/synthetizer/worklet_system/worklet_utilities → src/synthetizer/audio_engine/engine_components}/modulator_curves.js +33 -30
  93. package/src/synthetizer/audio_engine/engine_components/soundfont_manager.js +221 -0
  94. package/src/synthetizer/audio_engine/engine_components/stereo_panner.js +120 -0
  95. package/{spessasynth_core/synthetizer/worklet_system/worklet_utilities → src/synthetizer/audio_engine/engine_components}/unit_converter.js +11 -4
  96. package/src/synthetizer/audio_engine/engine_components/voice.js +519 -0
  97. package/src/synthetizer/audio_engine/engine_components/volume_envelope.js +401 -0
  98. package/src/synthetizer/audio_engine/engine_components/wavetable_oscillator.js +263 -0
  99. package/src/synthetizer/audio_engine/engine_methods/controller_control/controller_change.js +132 -0
  100. package/src/synthetizer/audio_engine/engine_methods/controller_control/master_parameters.js +48 -0
  101. package/src/synthetizer/audio_engine/engine_methods/controller_control/reset_controllers.js +241 -0
  102. package/src/synthetizer/audio_engine/engine_methods/create_midi_channel.js +27 -0
  103. package/src/synthetizer/audio_engine/engine_methods/data_entry/data_entry_coarse.js +253 -0
  104. package/src/synthetizer/audio_engine/engine_methods/data_entry/data_entry_fine.js +66 -0
  105. package/src/synthetizer/audio_engine/engine_methods/mute_channel.js +17 -0
  106. package/src/synthetizer/audio_engine/engine_methods/note_on.js +175 -0
  107. package/src/synthetizer/audio_engine/engine_methods/portamento_time.js +92 -0
  108. package/src/synthetizer/audio_engine/engine_methods/program_change.js +61 -0
  109. package/src/synthetizer/audio_engine/engine_methods/render_voice.js +196 -0
  110. package/src/synthetizer/audio_engine/engine_methods/soundfont_management/clear_sound_font.js +30 -0
  111. package/src/synthetizer/audio_engine/engine_methods/soundfont_management/get_preset.js +22 -0
  112. package/src/synthetizer/audio_engine/engine_methods/soundfont_management/reload_sound_font.js +28 -0
  113. package/src/synthetizer/audio_engine/engine_methods/soundfont_management/send_preset_list.js +31 -0
  114. package/src/synthetizer/audio_engine/engine_methods/soundfont_management/set_embedded_sound_font.js +21 -0
  115. package/src/synthetizer/audio_engine/engine_methods/stopping_notes/kill_note.js +20 -0
  116. package/src/synthetizer/audio_engine/engine_methods/stopping_notes/note_off.js +55 -0
  117. package/src/synthetizer/audio_engine/engine_methods/stopping_notes/stop_all_channels.js +16 -0
  118. package/src/synthetizer/audio_engine/engine_methods/stopping_notes/stop_all_notes.js +30 -0
  119. package/src/synthetizer/audio_engine/engine_methods/stopping_notes/voice_killing.js +63 -0
  120. package/src/synthetizer/audio_engine/engine_methods/system_exclusive.js +776 -0
  121. package/src/synthetizer/audio_engine/engine_methods/tuning_control/channel_pressure.js +24 -0
  122. package/src/synthetizer/audio_engine/engine_methods/tuning_control/pitch_wheel.js +33 -0
  123. package/src/synthetizer/audio_engine/engine_methods/tuning_control/poly_pressure.js +31 -0
  124. package/src/synthetizer/audio_engine/engine_methods/tuning_control/set_master_tuning.js +15 -0
  125. package/src/synthetizer/audio_engine/engine_methods/tuning_control/set_modulation_depth.js +27 -0
  126. package/src/synthetizer/audio_engine/engine_methods/tuning_control/set_octave_tuning.js +19 -0
  127. package/src/synthetizer/audio_engine/engine_methods/tuning_control/set_tuning.js +27 -0
  128. package/src/synthetizer/audio_engine/engine_methods/tuning_control/transpose_all_channels.js +15 -0
  129. package/src/synthetizer/audio_engine/engine_methods/tuning_control/transpose_channel.js +34 -0
  130. package/src/synthetizer/audio_engine/main_processor.js +804 -0
  131. package/src/synthetizer/audio_engine/snapshot/apply_synthesizer_snapshot.js +15 -0
  132. package/src/synthetizer/audio_engine/snapshot/channel_snapshot.js +175 -0
  133. package/src/synthetizer/audio_engine/snapshot/synthesizer_snapshot.js +116 -0
  134. package/src/synthetizer/synth_constants.js +22 -0
  135. package/{spessasynth_core → src}/utils/README.md +1 -0
  136. package/src/utils/buffer_to_wav.js +185 -0
  137. package/src/utils/byte_functions/big_endian.js +32 -0
  138. package/src/utils/byte_functions/little_endian.js +77 -0
  139. package/src/utils/byte_functions/string.js +107 -0
  140. package/src/utils/byte_functions/variable_length_quantity.js +42 -0
  141. package/src/utils/fill_with_defaults.js +21 -0
  142. package/src/utils/indexed_array.js +52 -0
  143. package/{spessasynth_core → src}/utils/loggin.js +70 -78
  144. package/src/utils/other.js +92 -0
  145. package/src/utils/sysex_detector.js +58 -0
  146. package/src/utils/xg_hacks.js +193 -0
  147. package/.idea/inspectionProfiles/Project_Default.xml +0 -10
  148. package/.idea/jsLibraryMappings.xml +0 -6
  149. package/.idea/modules.xml +0 -8
  150. package/.idea/spessasynth_core.iml +0 -12
  151. package/.idea/vcs.xml +0 -6
  152. package/spessasynth_core/midi_parser/README.md +0 -3
  153. package/spessasynth_core/midi_parser/midi_loader.js +0 -386
  154. package/spessasynth_core/sequencer/sequencer.js +0 -202
  155. package/spessasynth_core/sequencer/worklet_sequencer/play.js +0 -209
  156. package/spessasynth_core/sequencer/worklet_sequencer/process_event.js +0 -120
  157. package/spessasynth_core/sequencer/worklet_sequencer/song_control.js +0 -112
  158. package/spessasynth_core/soundfont/README.md +0 -4
  159. package/spessasynth_core/soundfont/chunk/generators.js +0 -205
  160. package/spessasynth_core/soundfont/chunk/modulators.js +0 -232
  161. package/spessasynth_core/soundfont/chunk/presets.js +0 -264
  162. package/spessasynth_core/soundfont/chunk/riff_chunk.js +0 -46
  163. package/spessasynth_core/soundfont/chunk/samples.js +0 -250
  164. package/spessasynth_core/soundfont/soundfont_parser.js +0 -301
  165. package/spessasynth_core/synthetizer/README.md +0 -6
  166. package/spessasynth_core/synthetizer/synthesizer.js +0 -303
  167. package/spessasynth_core/synthetizer/worklet_system/README.md +0 -3
  168. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/controller_control.js +0 -290
  169. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/data_entry.js +0 -280
  170. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/note_off.js +0 -102
  171. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/note_on.js +0 -77
  172. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/program_control.js +0 -140
  173. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/system_exclusive.js +0 -266
  174. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/tuning_control.js +0 -104
  175. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/vibrato_control.js +0 -29
  176. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/voice_control.js +0 -222
  177. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js +0 -95
  178. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js +0 -73
  179. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/stereo_panner.js +0 -76
  180. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/volume_envelope.js +0 -194
  181. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js +0 -83
  182. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js +0 -173
  183. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js +0 -106
  184. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +0 -313
  185. package/spessasynth_core/utils/buffer_to_wav.js +0 -70
  186. package/spessasynth_core/utils/byte_functions.js +0 -141
  187. package/spessasynth_core/utils/other.js +0 -49
  188. package/spessasynth_core/utils/shiftable_array.js +0 -26
  189. package/spessasynth_core/utils/stbvorbis_sync.js +0 -1877
@@ -0,0 +1,567 @@
1
+ // noinspection JSUnusedGlobalSymbols
2
+
3
+ import { MIDISequenceData } from "./midi_sequence.js";
4
+ import { getStringBytes, readBytesAsString } from "../utils/byte_functions/string.js";
5
+ import { messageTypes, MIDIMessage } from "./midi_message.js";
6
+ import { readBytesAsUintBigEndian } from "../utils/byte_functions/big_endian.js";
7
+ import { SpessaSynthGroup, SpessaSynthGroupEnd, SpessaSynthInfo } from "../utils/loggin.js";
8
+ import { consoleColors, formatTitle, sanitizeKarLyrics } from "../utils/other.js";
9
+ import { writeMIDI } from "./midi_tools/midi_writer.js";
10
+ import { applySnapshotToMIDI, modifyMIDI } from "./midi_tools/midi_editor.js";
11
+ import { writeRMIDI } from "./midi_tools/rmidi_writer.js";
12
+ import { getUsedProgramsAndKeys } from "./midi_tools/used_keys_loaded.js";
13
+ import { IndexedByteArray } from "../utils/indexed_array.js";
14
+ import { getNoteTimes } from "./midi_tools/get_note_times.js";
15
+
16
+ /**
17
+ * BasicMIDI is the base of a complete MIDI file, used by the sequencer internally.
18
+ * BasicMIDI is not available on the main thread, as it contains the actual track data which can be large.
19
+ * It can be accessed by calling getMIDI() on the Sequencer.
20
+ */
21
+ class BasicMIDI extends MIDISequenceData
22
+ {
23
+
24
+ /**
25
+ * The embedded soundfont in the MIDI file, represented as an ArrayBuffer, if available.
26
+ * @type {ArrayBuffer|undefined}
27
+ */
28
+ embeddedSoundFont = undefined;
29
+
30
+ /**
31
+ * The actual track data of the MIDI file, represented as an array of tracks.
32
+ * Tracks are arrays of MIDIMessage objects.
33
+ * @type {MIDIMessage[][]}
34
+ */
35
+ tracks = [];
36
+
37
+ /**
38
+ * If the MIDI file is a DLS RMIDI file.
39
+ * @type {boolean}
40
+ */
41
+ isDLSRMIDI = false;
42
+
43
+ /**
44
+ * Copies a MIDI
45
+ * @param mid {BasicMIDI}
46
+ * @returns {BasicMIDI}
47
+ */
48
+ static copyFrom(mid)
49
+ {
50
+ const m = new BasicMIDI();
51
+ m._copyFromSequence(mid);
52
+
53
+ m.isDLSRMIDI = mid.isDLSRMIDI;
54
+ m.embeddedSoundFont = mid.embeddedSoundFont ? mid.embeddedSoundFont.slice(0) : undefined; // Deep copy
55
+ m.tracks = mid.tracks.map(track => [...track]); // Shallow copy of each track array
56
+
57
+ return m;
58
+ }
59
+
60
+ /**
61
+ * Parses internal MIDI values
62
+ * @protected
63
+ */
64
+ _parseInternal()
65
+ {
66
+ SpessaSynthGroup(
67
+ "%cInterpreting MIDI events...",
68
+ consoleColors.info
69
+ );
70
+ /**
71
+ * For karaoke files, text events starting with @T are considered titles,
72
+ * usually the first one is the title, and the latter is things such as "sequenced by" etc.
73
+ * @type {boolean}
74
+ */
75
+ let karaokeHasTitle = false;
76
+
77
+ this.keyRange = { max: 0, min: 127 };
78
+
79
+ /**
80
+ * Will be joined with "\n" to form the final string
81
+ * @type {string[]}
82
+ */
83
+ let copyrightComponents = [];
84
+ let copyrightDetected = false;
85
+ if (typeof this.RMIDInfo["ICOP"] !== "undefined")
86
+ {
87
+ // if RMIDI has copyright info, don't try to detect one.
88
+ copyrightDetected = true;
89
+ }
90
+
91
+
92
+ let nameDetected = false;
93
+ if (typeof this.RMIDInfo["INAM"] !== "undefined")
94
+ {
95
+ // same as with copyright
96
+ nameDetected = true;
97
+ }
98
+
99
+ // loop tracking
100
+ let loopStart = null;
101
+ let loopEnd = null;
102
+
103
+ for (let i = 0; i < this.tracks.length; i++)
104
+ {
105
+ /**
106
+ * @type {MIDIMessage[]}
107
+ */
108
+ const track = this.tracks[i];
109
+ const usedChannels = new Set();
110
+ let trackHasVoiceMessages = false;
111
+
112
+ for (const e of track)
113
+ {
114
+ // check if it's a voice message
115
+ if (e.messageStatusByte >= 0x80 && e.messageStatusByte < 0xF0)
116
+ {
117
+ trackHasVoiceMessages = true;
118
+ // voice messages are 7-bit always
119
+ for (let j = 0; j < e.messageData.length; j++)
120
+ {
121
+ e.messageData[j] = Math.min(127, e.messageData[j]);
122
+ }
123
+ // last voice event tick
124
+ if (e.ticks > this.lastVoiceEventTick)
125
+ {
126
+ this.lastVoiceEventTick = e.ticks;
127
+ }
128
+
129
+ // interpret the voice message
130
+ switch (e.messageStatusByte & 0xF0)
131
+ {
132
+ // cc change: loop points
133
+ case messageTypes.controllerChange:
134
+ switch (e.messageData[0])
135
+ {
136
+ case 2:
137
+ case 116:
138
+ loopStart = e.ticks;
139
+ break;
140
+
141
+ case 4:
142
+ case 117:
143
+ if (loopEnd === null)
144
+ {
145
+ loopEnd = e.ticks;
146
+ }
147
+ else
148
+ {
149
+ // this controller has occurred more than once;
150
+ // this means
151
+ // that it doesn't indicate the loop
152
+ loopEnd = 0;
153
+ }
154
+ break;
155
+
156
+ case 0:
157
+ // check RMID
158
+ if (this.isDLSRMIDI && e.messageData[1] !== 0 && e.messageData[1] !== 127)
159
+ {
160
+ SpessaSynthInfo(
161
+ "%cDLS RMIDI with offset 1 detected!",
162
+ consoleColors.recognized
163
+ );
164
+ this.bankOffset = 1;
165
+ }
166
+ }
167
+ break;
168
+
169
+ // note on: used notes tracking and key range
170
+ case messageTypes.noteOn:
171
+ usedChannels.add(e.messageStatusByte & 0x0F);
172
+ const note = e.messageData[0];
173
+ this.keyRange.min = Math.min(this.keyRange.min, note);
174
+ this.keyRange.max = Math.max(this.keyRange.max, note);
175
+ break;
176
+ }
177
+ }
178
+ e.messageData.currentIndex = 0;
179
+ const eventText = readBytesAsString(e.messageData, e.messageData.length);
180
+ e.messageData.currentIndex = 0;
181
+ // interpret the message
182
+ switch (e.messageStatusByte)
183
+ {
184
+ case messageTypes.setTempo:
185
+ // add the tempo change
186
+ e.messageData.currentIndex = 0;
187
+ this.tempoChanges.push({
188
+ ticks: e.ticks,
189
+ tempo: 60000000 / readBytesAsUintBigEndian(e.messageData, 3)
190
+ });
191
+ e.messageData.currentIndex = 0;
192
+ break;
193
+
194
+ case messageTypes.marker:
195
+ // check for loop markers
196
+ const text = eventText.trim().toLowerCase();
197
+ switch (text)
198
+ {
199
+ default:
200
+ break;
201
+
202
+ case "start":
203
+ case "loopstart":
204
+ loopStart = e.ticks;
205
+ break;
206
+
207
+ case "loopend":
208
+ loopEnd = e.ticks;
209
+ }
210
+ e.messageData.currentIndex = 0;
211
+ break;
212
+
213
+ case messageTypes.copyright:
214
+ if (!copyrightDetected)
215
+ {
216
+ e.messageData.currentIndex = 0;
217
+ copyrightComponents.push(readBytesAsString(
218
+ e.messageData,
219
+ e.messageData.length,
220
+ undefined,
221
+ false
222
+ ));
223
+ e.messageData.currentIndex = 0;
224
+ }
225
+ break;
226
+
227
+ case messageTypes.lyric:
228
+ // note here: .kar files sometimes just use...
229
+ // lyrics instead of text because why not (of course)
230
+ // perform the same check for @KMIDI KARAOKE FILE
231
+ if (eventText.trim().startsWith("@KMIDI KARAOKE FILE"))
232
+ {
233
+ this.isKaraokeFile = true;
234
+ SpessaSynthInfo("%cKaraoke MIDI detected!", consoleColors.recognized);
235
+ }
236
+
237
+ if (this.isKaraokeFile)
238
+ {
239
+ // replace the type of the message with text
240
+ e.messageStatusByte = messageTypes.text;
241
+ }
242
+ else
243
+ {
244
+ // add lyrics like a regular midi file
245
+ this.lyrics.push(e.messageData);
246
+ this.lyricsTicks.push(e.ticks);
247
+ break;
248
+ }
249
+
250
+ // kar: treat the same as text
251
+ // fallthrough
252
+ case messageTypes.text:
253
+ // possibly Soft Karaoke MIDI file
254
+ // it has a text event at the start of the file
255
+ // "@KMIDI KARAOKE FILE"
256
+ const checkedText = eventText.trim();
257
+ if (checkedText.startsWith("@KMIDI KARAOKE FILE"))
258
+ {
259
+ this.isKaraokeFile = true;
260
+
261
+ SpessaSynthInfo("%cKaraoke MIDI detected!", consoleColors.recognized);
262
+ }
263
+ else if (this.isKaraokeFile)
264
+ {
265
+ // check for @T (title)
266
+ // or @A because it is a title too sometimes?
267
+ // IDK it's strange
268
+ if (checkedText.startsWith("@T") || checkedText.startsWith("@A"))
269
+ {
270
+ if (!karaokeHasTitle)
271
+ {
272
+ this.midiName = checkedText.substring(2).trim();
273
+ karaokeHasTitle = true;
274
+ nameDetected = true;
275
+ // encode to rawMidiName
276
+ this.rawMidiName = getStringBytes(this.midiName);
277
+ }
278
+ else
279
+ {
280
+ // append to copyright
281
+ copyrightComponents.push(checkedText.substring(2).trim());
282
+ }
283
+ }
284
+ else if (checkedText[0] !== "@")
285
+ {
286
+ // non @: the lyrics
287
+ this.lyrics.push(sanitizeKarLyrics(e.messageData));
288
+ this.lyricsTicks.push(e.ticks);
289
+ }
290
+ }
291
+ break;
292
+
293
+ case messageTypes.trackName:
294
+ break;
295
+ }
296
+ }
297
+ // add used channels
298
+ this.usedChannelsOnTrack.push(usedChannels);
299
+
300
+ // track name
301
+ this.trackNames[i] = "";
302
+ const trackName = track.find(e => e.messageStatusByte === messageTypes.trackName);
303
+ if (trackName)
304
+ {
305
+ trackName.messageData.currentIndex = 0;
306
+ const name = readBytesAsString(trackName.messageData, trackName.messageData.length);
307
+ this.trackNames[i] = name;
308
+ // If the track has no voice messages, its "track name" event (if it has any)
309
+ // is some metadata.
310
+ // Add it to copyright
311
+ if (!trackHasVoiceMessages)
312
+ {
313
+ copyrightComponents.push(name);
314
+ }
315
+ }
316
+ }
317
+
318
+ // reverse the tempo changes
319
+ this.tempoChanges.reverse();
320
+
321
+ SpessaSynthInfo(
322
+ `%cCorrecting loops, ports and detecting notes...`,
323
+ consoleColors.info
324
+ );
325
+
326
+ const firstNoteOns = [];
327
+ for (const t of this.tracks)
328
+ {
329
+ const firstNoteOn = t.find(e => (e.messageStatusByte & 0xF0) === messageTypes.noteOn);
330
+ if (firstNoteOn)
331
+ {
332
+ firstNoteOns.push(firstNoteOn.ticks);
333
+ }
334
+ }
335
+ this.firstNoteOn = Math.min(...firstNoteOns);
336
+
337
+ SpessaSynthInfo(
338
+ `%cFirst note-on detected at: %c${this.firstNoteOn}%c ticks!`,
339
+ consoleColors.info,
340
+ consoleColors.recognized,
341
+ consoleColors.info
342
+ );
343
+
344
+
345
+ if (loopStart !== null && loopEnd === null)
346
+ {
347
+ // not a loop
348
+ loopStart = this.firstNoteOn;
349
+ loopEnd = this.lastVoiceEventTick;
350
+ }
351
+ else
352
+ {
353
+ if (loopStart === null)
354
+ {
355
+ loopStart = this.firstNoteOn;
356
+ }
357
+
358
+ if (loopEnd === null || loopEnd === 0)
359
+ {
360
+ loopEnd = this.lastVoiceEventTick;
361
+ }
362
+ }
363
+
364
+ /**
365
+ *
366
+ * @type {{start: number, end: number}}
367
+ */
368
+ this.loop = { start: loopStart, end: loopEnd };
369
+
370
+ SpessaSynthInfo(
371
+ `%cLoop points: start: %c${this.loop.start}%c end: %c${this.loop.end}`,
372
+ consoleColors.info,
373
+ consoleColors.recognized,
374
+ consoleColors.info,
375
+ consoleColors.recognized
376
+ );
377
+
378
+ // determine ports
379
+ let portOffset = 0;
380
+ this.midiPorts = [];
381
+ this.midiPortChannelOffsets = [];
382
+ for (let trackNum = 0; trackNum < this.tracks.length; trackNum++)
383
+ {
384
+ this.midiPorts.push(-1);
385
+ if (this.usedChannelsOnTrack[trackNum].size === 0)
386
+ {
387
+ continue;
388
+ }
389
+ for (const e of this.tracks[trackNum])
390
+ {
391
+ if (e.messageStatusByte !== messageTypes.midiPort)
392
+ {
393
+ continue;
394
+ }
395
+ const port = e.messageData[0];
396
+ this.midiPorts[trackNum] = port;
397
+ if (this.midiPortChannelOffsets[port] === undefined)
398
+ {
399
+ this.midiPortChannelOffsets[port] = portOffset;
400
+ portOffset += 16;
401
+ }
402
+ }
403
+ }
404
+
405
+ // fix midi ports:
406
+ // midi tracks without ports will have a value of -1
407
+ // if all ports have a value of -1, set it to 0,
408
+ // otherwise take the first midi port and replace all -1 with it,
409
+ // why would we do this?
410
+ // some midis (for some reason) specify all channels to port 1 or else,
411
+ // but leave the conductor track with no port pref.
412
+ // this spessasynth to reserve the first 16 channels for the conductor track
413
+ // (which doesn't play anything) and use the additional 16 for the actual ports.
414
+ let defaultPort = Infinity;
415
+ for (let port of this.midiPorts)
416
+ {
417
+ if (port !== -1)
418
+ {
419
+ if (defaultPort > port)
420
+ {
421
+ defaultPort = port;
422
+ }
423
+ }
424
+ }
425
+ if (defaultPort === Infinity)
426
+ {
427
+ defaultPort = 0;
428
+ }
429
+ this.midiPorts = this.midiPorts.map(port => port === -1 ? defaultPort : port);
430
+ // add fake port if empty
431
+ if (this.midiPortChannelOffsets.length === 0)
432
+ {
433
+ this.midiPortChannelOffsets = [0];
434
+ }
435
+ if (this.midiPortChannelOffsets.length < 2)
436
+ {
437
+ SpessaSynthInfo(`%cNo additional MIDI Ports detected.`, consoleColors.info);
438
+ }
439
+ else
440
+ {
441
+ this.isMultiPort = true;
442
+ SpessaSynthInfo(`%cMIDI Ports detected!`, consoleColors.recognized);
443
+ }
444
+
445
+ // midi name
446
+ if (!nameDetected)
447
+ {
448
+ if (this.tracks.length > 1)
449
+ {
450
+ // if more than 1 track and the first track has no notes,
451
+ // just find the first trackName in the first track.
452
+ if (
453
+ this.tracks[0].find(
454
+ message => message.messageStatusByte >= messageTypes.noteOn
455
+ &&
456
+ message.messageStatusByte < messageTypes.polyPressure
457
+ ) === undefined
458
+ )
459
+ {
460
+
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
+ else
471
+ {
472
+ // if only 1 track, find the first "track name" event
473
+ let name = this.tracks[0].find(message => message.messageStatusByte === messageTypes.trackName);
474
+ if (name)
475
+ {
476
+ this.rawMidiName = name.messageData;
477
+ name.messageData.currentIndex = 0;
478
+ this.midiName = readBytesAsString(name.messageData, name.messageData.length, undefined, false);
479
+ }
480
+ }
481
+ }
482
+
483
+ if (!copyrightDetected)
484
+ {
485
+ this.copyright = copyrightComponents
486
+ // trim and group newlines into one
487
+ .map(c => c.trim().replace(/(\r?\n)+/g, "\n"))
488
+ // remove empty strings
489
+ .filter(c => c.length > 0)
490
+ // join with newlines
491
+ .join("\n") || "";
492
+ }
493
+
494
+ this.midiName = this.midiName.trim();
495
+ this.midiNameUsesFileName = false;
496
+ // if midiName is "", use the file name
497
+ if (this.midiName.length === 0)
498
+ {
499
+ SpessaSynthInfo(
500
+ `%cNo name detected. Using the alt name!`,
501
+ consoleColors.info
502
+ );
503
+ this.midiName = formatTitle(this.fileName);
504
+ this.midiNameUsesFileName = true;
505
+ // encode it too
506
+ this.rawMidiName = new Uint8Array(this.midiName.length);
507
+ for (let i = 0; i < this.midiName.length; i++)
508
+ {
509
+ this.rawMidiName[i] = this.midiName.charCodeAt(i);
510
+ }
511
+ }
512
+ else
513
+ {
514
+ SpessaSynthInfo(
515
+ `%cMIDI Name detected! %c"${this.midiName}"`,
516
+ consoleColors.info,
517
+ consoleColors.recognized
518
+ );
519
+ }
520
+
521
+ // if the first event is not at 0 ticks, add a track name
522
+ // https://github.com/spessasus/SpessaSynth/issues/145
523
+ if (!this.tracks.some(t => t[0].ticks === 0))
524
+ {
525
+ const track = this.tracks[0];
526
+ // can copy
527
+ track.unshift(new MIDIMessage(
528
+ 0,
529
+ messageTypes.trackName,
530
+ new IndexedByteArray(this.rawMidiName.buffer)
531
+ ));
532
+ }
533
+
534
+
535
+ /**
536
+ * The total playback time, in seconds
537
+ * @type {number}
538
+ */
539
+ this.duration = this.MIDIticksToSeconds(this.lastVoiceEventTick);
540
+
541
+ SpessaSynthInfo("%cSuccess!", consoleColors.recognized);
542
+ SpessaSynthGroupEnd();
543
+ }
544
+
545
+ /**
546
+ * Updates all internal values
547
+ */
548
+ flush()
549
+ {
550
+
551
+ for (const t of this.tracks)
552
+ {
553
+ // sort the track by ticks
554
+ t.sort((e1, e2) => e1.ticks - e2.ticks);
555
+ }
556
+ this._parseInternal();
557
+ }
558
+ }
559
+
560
+ BasicMIDI.prototype.writeMIDI = writeMIDI;
561
+ BasicMIDI.prototype.modifyMIDI = modifyMIDI;
562
+ BasicMIDI.prototype.applySnapshotToMIDI = applySnapshotToMIDI;
563
+ BasicMIDI.prototype.writeRMIDI = writeRMIDI;
564
+ BasicMIDI.prototype.getUsedProgramsAndKeys = getUsedProgramsAndKeys;
565
+ BasicMIDI.prototype.getNoteTimes = getNoteTimes;
566
+
567
+ export { BasicMIDI };