spessasynth_core 1.1.3 → 1.1.5

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 +74 -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 -313
  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 -223
  177. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js +0 -133
  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 -272
  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 -175
  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 -285
  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,202 @@
1
+ import { BasicMIDI } from "./basic_midi.js";
2
+ import { messageTypes, MIDIMessage } from "./midi_message.js";
3
+ import { IndexedByteArray } from "../utils/indexed_array.js";
4
+ import { SpessaSynthWarn } from "../utils/loggin.js";
5
+
6
+ /**
7
+ * A class that helps to build a MIDI file from scratch.
8
+ */
9
+ export class MIDIBuilder extends BasicMIDI
10
+ {
11
+ /**
12
+ * @param name {string} The MIDI's name
13
+ * @param timeDivision {number} the file's time division
14
+ * @param initialTempo {number} the file's initial tempo
15
+ */
16
+ constructor(name, timeDivision = 480, initialTempo = 120)
17
+ {
18
+ super();
19
+ this.timeDivision = timeDivision;
20
+ this.midiName = name;
21
+ this.encoder = new TextEncoder();
22
+ this.rawMidiName = this.encoder.encode(name);
23
+
24
+ // create the first track with the file name
25
+ this.addNewTrack(name);
26
+ this.addSetTempo(0, initialTempo);
27
+ }
28
+
29
+ /**
30
+ * Adds a new Set Tempo event
31
+ * @param ticks {number} the tick number of the event
32
+ * @param tempo {number} the tempo in beats per minute (BPM)
33
+ */
34
+ addSetTempo(ticks, tempo)
35
+ {
36
+ const array = new IndexedByteArray(3);
37
+
38
+ tempo = 60000000 / tempo;
39
+
40
+ // Extract each byte in big-endian order
41
+ array[0] = (tempo >> 16) & 0xFF;
42
+ array[1] = (tempo >> 8) & 0xFF;
43
+ array[2] = tempo & 0xFF;
44
+
45
+ this.addEvent(ticks, 0, messageTypes.setTempo, array);
46
+ }
47
+
48
+ /**
49
+ * Adds a new MIDI track
50
+ * @param name {string} the new track's name
51
+ * @param port {number} the new track's port
52
+ */
53
+ addNewTrack(name, port = 0)
54
+ {
55
+ this.tracksAmount++;
56
+ if (this.tracksAmount > 1)
57
+ {
58
+ this.format = 1;
59
+ }
60
+ this.tracks.push([]);
61
+ this.tracks[this.tracksAmount - 1].push(
62
+ new MIDIMessage(0, messageTypes.endOfTrack, new IndexedByteArray(0))
63
+ );
64
+ this.addEvent(0, this.tracksAmount - 1, messageTypes.trackName, this.encoder.encode(name));
65
+ this.addEvent(0, this.tracksAmount - 1, messageTypes.midiPort, [port]);
66
+ }
67
+
68
+ /**
69
+ * Adds a new MIDI Event
70
+ * @param ticks {number} the tick time of the event
71
+ * @param track {number} the track number to use
72
+ * @param event {number} the MIDI event number
73
+ * @param eventData {Uint8Array|Iterable<number>} the raw event data
74
+ */
75
+ addEvent(ticks, track, event, eventData)
76
+ {
77
+ if (!this.tracks[track])
78
+ {
79
+ throw new Error(`Track ${track} does not exist. Add it via addTrack method.`);
80
+ }
81
+ if (event === messageTypes.endOfTrack)
82
+ {
83
+ SpessaSynthWarn(
84
+ "The EndOfTrack is added automatically and does not influence the duration. Consider adding a voice event instead.");
85
+ return;
86
+ }
87
+ // remove the end of track
88
+ this.tracks[track].pop();
89
+ this.tracks[track].push(new MIDIMessage(
90
+ ticks,
91
+ event,
92
+ new IndexedByteArray(eventData)
93
+ ));
94
+ // add the end of track
95
+ this.tracks[track].push(new MIDIMessage(
96
+ ticks,
97
+ messageTypes.endOfTrack,
98
+ new IndexedByteArray(0)
99
+ ));
100
+ }
101
+
102
+ /**
103
+ * Adds a new Note On event
104
+ * @param ticks {number} the tick time of the event
105
+ * @param track {number} the track number to use
106
+ * @param channel {number} the channel to use
107
+ * @param midiNote {number} the midi note of the keypress
108
+ * @param velocity {number} the velocity of the keypress
109
+ */
110
+ addNoteOn(ticks, track, channel, midiNote, velocity)
111
+ {
112
+ channel %= 16;
113
+ midiNote %= 128;
114
+ velocity %= 128;
115
+ this.addEvent(
116
+ ticks,
117
+ track,
118
+ messageTypes.noteOn | channel,
119
+ [midiNote, velocity]
120
+ );
121
+ }
122
+
123
+ /**
124
+ * Adds a new Note Off event
125
+ * @param ticks {number} the tick time of the event
126
+ * @param track {number} the track number to use
127
+ * @param channel {number} the channel to use
128
+ * @param midiNote {number} the midi note of the key release
129
+ */
130
+ addNoteOff(ticks, track, channel, midiNote)
131
+ {
132
+ channel %= 16;
133
+ midiNote %= 128;
134
+ this.addEvent(
135
+ ticks,
136
+ track,
137
+ messageTypes.noteOff | channel,
138
+ [midiNote, 64]
139
+ );
140
+ }
141
+
142
+ /**
143
+ * Adds a new Program Change event
144
+ * @param ticks {number} the tick time of the event
145
+ * @param track {number} the track number to use
146
+ * @param channel {number} the channel to use
147
+ * @param programNumber {number} the MIDI program to use
148
+ */
149
+ addProgramChange(ticks, track, channel, programNumber)
150
+ {
151
+ channel %= 16;
152
+ programNumber %= 128;
153
+ this.addEvent(
154
+ ticks,
155
+ track,
156
+ messageTypes.programChange | channel,
157
+ [programNumber]
158
+ );
159
+ }
160
+
161
+ /**
162
+ * Adds a new Controller Change event
163
+ * @param ticks {number} the tick time of the event
164
+ * @param track {number} the track number to use
165
+ * @param channel {number} the channel to use
166
+ * @param controllerNumber {number} the MIDI CC to use
167
+ * @param controllerValue {number} the new CC value
168
+ */
169
+ addControllerChange(ticks, track, channel, controllerNumber, controllerValue)
170
+ {
171
+ channel %= 16;
172
+ controllerNumber %= 128;
173
+ controllerValue %= 128;
174
+ this.addEvent(
175
+ ticks,
176
+ track,
177
+ messageTypes.controllerChange | channel,
178
+ [controllerNumber, controllerValue]
179
+ );
180
+ }
181
+
182
+ /**
183
+ * Adds a new Pitch Wheel event
184
+ * @param ticks {number} the tick time of the event
185
+ * @param track {number} the track to use
186
+ * @param channel {number} the channel to use
187
+ * @param MSB {number} SECOND byte of the MIDI pitchWheel message
188
+ * @param LSB {number} FIRST byte of the MIDI pitchWheel message
189
+ */
190
+ addPitchWheel(ticks, track, channel, MSB, LSB)
191
+ {
192
+ channel %= 16;
193
+ MSB %= 128;
194
+ LSB %= 128;
195
+ this.addEvent(
196
+ ticks,
197
+ track,
198
+ messageTypes.pitchBend | channel,
199
+ [LSB, MSB]
200
+ );
201
+ }
202
+ }
@@ -0,0 +1,324 @@
1
+ import { dataBytesAmount, getChannel, MIDIMessage } from "./midi_message.js";
2
+ import { IndexedByteArray } from "../utils/indexed_array.js";
3
+ import { consoleColors } from "../utils/other.js";
4
+ import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn } from "../utils/loggin.js";
5
+ import { readRIFFChunk } from "../soundfont/basic_soundfont/riff_chunk.js";
6
+ import { readVariableLengthQuantity } from "../utils/byte_functions/variable_length_quantity.js";
7
+ import { readBytesAsUintBigEndian } from "../utils/byte_functions/big_endian.js";
8
+ import { readBytesAsString } from "../utils/byte_functions/string.js";
9
+ import { readLittleEndian } from "../utils/byte_functions/little_endian.js";
10
+ import { RMIDINFOChunks } from "./midi_tools/rmidi_writer.js";
11
+ import { BasicMIDI } from "./basic_midi.js";
12
+ import { loadXMF } from "./xmf_loader.js";
13
+
14
+ /**
15
+ * midi_loader.js
16
+ * purpose:
17
+ * parses a midi file for the seqyencer,
18
+ * including things like marker or CC 2/4 loop detection, copyright detection, etc.
19
+ */
20
+
21
+ /**
22
+ * The MIDI class is a MIDI file parser that reads a MIDI file and extracts all the necessary information from it.
23
+ * Supported formats are .mid and .rmi files.
24
+ */
25
+ class MIDI extends BasicMIDI
26
+ {
27
+ /**
28
+ * Parses a given midi file
29
+ * @param arrayBuffer {ArrayBuffer}
30
+ * @param fileName {string} optional, replaces the decoded title if empty
31
+ */
32
+ constructor(arrayBuffer, fileName = "")
33
+ {
34
+ super();
35
+ SpessaSynthGroupCollapsed(`%cParsing MIDI File...`, consoleColors.info);
36
+ this.fileName = fileName;
37
+ const binaryData = new IndexedByteArray(arrayBuffer);
38
+ let fileByteArray;
39
+
40
+ // check for rmid
41
+ const initialString = readBytesAsString(binaryData, 4);
42
+ binaryData.currentIndex -= 4;
43
+ if (initialString === "RIFF")
44
+ {
45
+ // possibly an RMID file (https://github.com/spessasus/sf2-rmidi-specification#readme)
46
+ // skip size
47
+ binaryData.currentIndex += 8;
48
+ const rmid = readBytesAsString(binaryData, 4, undefined, false);
49
+ if (rmid !== "RMID")
50
+ {
51
+ SpessaSynthGroupEnd();
52
+ throw new SyntaxError(`Invalid RMIDI Header! Expected "RMID", got "${rmid}"`);
53
+ }
54
+ const riff = readRIFFChunk(binaryData);
55
+ if (riff.header !== "data")
56
+ {
57
+ SpessaSynthGroupEnd();
58
+ throw new SyntaxError(`Invalid RMIDI Chunk header! Expected "data", got "${rmid}"`);
59
+ }
60
+ // this is a rmid, load the midi into an array for parsing
61
+ fileByteArray = riff.chunkData;
62
+
63
+ // keep loading chunks until we get the "SFBK" header
64
+ while (binaryData.currentIndex <= binaryData.length)
65
+ {
66
+ const startIndex = binaryData.currentIndex;
67
+ const currentChunk = readRIFFChunk(binaryData, true);
68
+ if (currentChunk.header === "RIFF")
69
+ {
70
+ const type = readBytesAsString(currentChunk.chunkData, 4).toLowerCase();
71
+ if (type === "sfbk" || type === "sfpk" || type === "dls ")
72
+ {
73
+ SpessaSynthInfo("%cFound embedded soundfont!", consoleColors.recognized);
74
+ this.embeddedSoundFont = binaryData.slice(startIndex, startIndex + currentChunk.size).buffer;
75
+ }
76
+ else
77
+ {
78
+ SpessaSynthWarn(`Unknown RIFF chunk: "${type}"`);
79
+ }
80
+ if (type === "dls ")
81
+ {
82
+ // Assume bank offset of 0 by default. If we find any bank selects, then the offset is 1.
83
+ this.isDLSRMIDI = true;
84
+ }
85
+ }
86
+ else if (currentChunk.header === "LIST")
87
+ {
88
+ const type = readBytesAsString(currentChunk.chunkData, 4);
89
+ if (type === "INFO")
90
+ {
91
+ SpessaSynthInfo("%cFound RMIDI INFO chunk!", consoleColors.recognized);
92
+ this.RMIDInfo = {};
93
+ while (currentChunk.chunkData.currentIndex <= currentChunk.size)
94
+ {
95
+ const infoChunk = readRIFFChunk(currentChunk.chunkData, true);
96
+ this.RMIDInfo[infoChunk.header] = infoChunk.chunkData;
97
+ }
98
+ if (this.RMIDInfo["ICOP"])
99
+ {
100
+ // special case, overwrites the copyright components array
101
+ this.copyright = readBytesAsString(
102
+ this.RMIDInfo["ICOP"],
103
+ this.RMIDInfo["ICOP"].length,
104
+ undefined,
105
+ false
106
+ ).replaceAll("\n", " ");
107
+ }
108
+ if (this.RMIDInfo["INAM"])
109
+ {
110
+ this.rawMidiName = this.RMIDInfo[RMIDINFOChunks.name];
111
+ // noinspection JSCheckFunctionSignatures
112
+ this.midiName = readBytesAsString(
113
+ this.rawMidiName,
114
+ this.rawMidiName.length,
115
+ undefined,
116
+ false
117
+ ).replaceAll("\n", " ");
118
+ }
119
+ // these can be used interchangeably
120
+ if (this.RMIDInfo["IALB"] && !this.RMIDInfo["IPRD"])
121
+ {
122
+ this.RMIDInfo["IPRD"] = this.RMIDInfo["IALB"];
123
+ }
124
+ if (this.RMIDInfo["IPRD"] && !this.RMIDInfo["IALB"])
125
+ {
126
+ this.RMIDInfo["IALB"] = this.RMIDInfo["IPRD"];
127
+ }
128
+ this.bankOffset = 1; // defaults to 1
129
+ if (this.RMIDInfo[RMIDINFOChunks.bankOffset])
130
+ {
131
+ this.bankOffset = readLittleEndian(this.RMIDInfo[RMIDINFOChunks.bankOffset], 2);
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ if (this.isDLSRMIDI)
138
+ {
139
+ // Assume bank offset of 0 by default. If we find any bank selects, then the offset is 1.
140
+ this.bankOffset = 0;
141
+ }
142
+
143
+ // if no embedded bank, assume 0
144
+ if (this.embeddedSoundFont === undefined)
145
+ {
146
+ this.bankOffset = 0;
147
+ }
148
+ }
149
+ else if (initialString === "XMF_")
150
+ {
151
+ // XMF file
152
+ fileByteArray = loadXMF(this, binaryData);
153
+ }
154
+ else
155
+ {
156
+ fileByteArray = binaryData;
157
+ }
158
+ const headerChunk = this._readMIDIChunk(fileByteArray);
159
+ if (headerChunk.type !== "MThd")
160
+ {
161
+ SpessaSynthGroupEnd();
162
+ throw new SyntaxError(`Invalid MIDI Header! Expected "MThd", got "${headerChunk.type}"`);
163
+ }
164
+
165
+ if (headerChunk.size !== 6)
166
+ {
167
+ SpessaSynthGroupEnd();
168
+ throw new RangeError(`Invalid MIDI header chunk size! Expected 6, got ${headerChunk.size}`);
169
+ }
170
+
171
+ // format
172
+ this.format = readBytesAsUintBigEndian(headerChunk.data, 2);
173
+ // tracks count
174
+ this.tracksAmount = readBytesAsUintBigEndian(headerChunk.data, 2);
175
+ // time division
176
+ this.timeDivision = readBytesAsUintBigEndian(headerChunk.data, 2);
177
+ // read all the tracks
178
+ for (let i = 0; i < this.tracksAmount; i++)
179
+ {
180
+ /**
181
+ * @type {MIDIMessage[]}
182
+ */
183
+ const track = [];
184
+ const trackChunk = this._readMIDIChunk(fileByteArray);
185
+
186
+ if (trackChunk.type !== "MTrk")
187
+ {
188
+ SpessaSynthGroupEnd();
189
+ throw new SyntaxError(`Invalid track header! Expected "MTrk" got "${trackChunk.type}"`);
190
+ }
191
+
192
+
193
+ /**
194
+ * MIDI running byte
195
+ * @type {number}
196
+ */
197
+ let runningByte = undefined;
198
+
199
+ let totalTicks = 0;
200
+ // format 2 plays sequentially
201
+ if (this.format === 2 && i > 0)
202
+ {
203
+ totalTicks += this.tracks[i - 1][this.tracks[i - 1].length - 1].ticks;
204
+ }
205
+ // loop until we reach the end of track
206
+ while (trackChunk.data.currentIndex < trackChunk.size)
207
+ {
208
+ totalTicks += readVariableLengthQuantity(trackChunk.data);
209
+
210
+ // check if the status byte is valid (IE. larger than 127)
211
+ const statusByteCheck = trackChunk.data[trackChunk.data.currentIndex];
212
+
213
+ let statusByte;
214
+ // if we have a running byte and the status byte isn't valid
215
+ if (runningByte !== undefined && statusByteCheck < 0x80)
216
+ {
217
+ statusByte = runningByte;
218
+ }
219
+ else
220
+ { // noinspection PointlessBooleanExpressionJS
221
+ if (runningByte === undefined && statusByteCheck < 0x80)
222
+ {
223
+ // if we don't have a running byte and the status byte isn't valid, it's an error.
224
+ SpessaSynthGroupEnd();
225
+ throw new SyntaxError(`Unexpected byte with no running byte. (${statusByteCheck})`);
226
+ }
227
+ else
228
+ {
229
+ // if the status byte is valid, use that
230
+ statusByte = trackChunk.data[trackChunk.data.currentIndex++];
231
+ }
232
+ }
233
+ const statusByteChannel = getChannel(statusByte);
234
+
235
+ let eventDataLength;
236
+
237
+ // determine the message's length;
238
+ switch (statusByteChannel)
239
+ {
240
+ case -1:
241
+ // system common/realtime (no length)
242
+ eventDataLength = 0;
243
+ break;
244
+
245
+ case -2:
246
+ // meta (the next is the actual status byte)
247
+ statusByte = trackChunk.data[trackChunk.data.currentIndex++];
248
+ eventDataLength = readVariableLengthQuantity(trackChunk.data);
249
+ break;
250
+
251
+ case -3:
252
+ // sysex
253
+ eventDataLength = readVariableLengthQuantity(trackChunk.data);
254
+ break;
255
+
256
+ default:
257
+ // voice message
258
+ // gets the midi message length
259
+ eventDataLength = dataBytesAmount[statusByte >> 4];
260
+ // save the status byte
261
+ runningByte = statusByte;
262
+ break;
263
+ }
264
+
265
+ // put the event data into the array
266
+ const eventData = new IndexedByteArray(eventDataLength);
267
+ eventData.set(trackChunk.data.slice(
268
+ trackChunk.data.currentIndex,
269
+ trackChunk.data.currentIndex + eventDataLength
270
+ ), 0);
271
+ const event = new MIDIMessage(totalTicks, statusByte, eventData);
272
+ track.push(event);
273
+ // advance the track chunk
274
+ trackChunk.data.currentIndex += eventDataLength;
275
+ }
276
+ this.tracks.push(track);
277
+
278
+ SpessaSynthInfo(
279
+ `%cParsed %c${this.tracks.length}%c / %c${this.tracksAmount}`,
280
+ consoleColors.info,
281
+ consoleColors.value,
282
+ consoleColors.info,
283
+ consoleColors.value
284
+ );
285
+ }
286
+
287
+ SpessaSynthInfo(
288
+ `%cAll tracks parsed correctly!`,
289
+ consoleColors.recognized
290
+ );
291
+ // parse the events
292
+ this._parseInternal();
293
+ SpessaSynthGroupEnd();
294
+ SpessaSynthInfo(
295
+ `%cMIDI file parsed. Total tick time: %c${this.lastVoiceEventTick}%c, total seconds time: %c${this.duration}`,
296
+ consoleColors.info,
297
+ consoleColors.recognized,
298
+ consoleColors.info,
299
+ consoleColors.recognized
300
+ );
301
+ }
302
+
303
+ /**
304
+ * @param fileByteArray {IndexedByteArray}
305
+ * @returns {{type: string, size: number, data: IndexedByteArray}}
306
+ * @private
307
+ */
308
+ _readMIDIChunk(fileByteArray)
309
+ {
310
+ const chunk = {};
311
+ // type
312
+ chunk.type = readBytesAsString(fileByteArray, 4);
313
+ // size
314
+ chunk.size = readBytesAsUintBigEndian(fileByteArray, 4);
315
+ // data
316
+ chunk.data = new IndexedByteArray(chunk.size);
317
+ const dataSlice = fileByteArray.slice(fileByteArray.currentIndex, fileByteArray.currentIndex + chunk.size);
318
+ chunk.data.set(dataSlice, 0);
319
+ fileByteArray.currentIndex += chunk.size;
320
+ return chunk;
321
+ }
322
+ }
323
+
324
+ export { MIDI };