spessasynth_lib 0.0.1

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 (154) hide show
  1. package/.idea/modules.xml +8 -0
  2. package/.idea/spessasynth_lib.iml +12 -0
  3. package/.idea/vcs.xml +6 -0
  4. package/copy_version.sh +38 -0
  5. package/index.js +73 -0
  6. package/package/@types/externals/stbvorbis_sync/stbvorbis_sync.min.d.ts +1 -0
  7. package/package/@types/index.d.ts +34 -0
  8. package/package/@types/midi_handler/midi_handler.d.ts +39 -0
  9. package/package/@types/midi_handler/web_midi_link.d.ts +12 -0
  10. package/package/@types/midi_parser/midi_data.d.ts +95 -0
  11. package/package/@types/midi_parser/midi_editor.d.ts +45 -0
  12. package/package/@types/midi_parser/midi_loader.d.ts +100 -0
  13. package/package/@types/midi_parser/midi_message.d.ts +154 -0
  14. package/package/@types/midi_parser/midi_writer.d.ts +6 -0
  15. package/package/@types/midi_parser/rmidi_writer.d.ts +9 -0
  16. package/package/@types/midi_parser/used_keys_loaded.d.ts +7 -0
  17. package/package/@types/sequencer/sequencer.d.ts +180 -0
  18. package/package/@types/sequencer/worklet_sequencer/sequencer_message.d.ts +28 -0
  19. package/package/@types/soundfont/read/generators.d.ts +98 -0
  20. package/package/@types/soundfont/read/instruments.d.ts +50 -0
  21. package/package/@types/soundfont/read/modulators.d.ts +73 -0
  22. package/package/@types/soundfont/read/presets.d.ts +87 -0
  23. package/package/@types/soundfont/read/riff_chunk.d.ts +31 -0
  24. package/package/@types/soundfont/read/samples.d.ts +134 -0
  25. package/package/@types/soundfont/read/zones.d.ts +141 -0
  26. package/package/@types/soundfont/soundfont.d.ts +76 -0
  27. package/package/@types/soundfont/write/ibag.d.ts +6 -0
  28. package/package/@types/soundfont/write/igen.d.ts +6 -0
  29. package/package/@types/soundfont/write/imod.d.ts +6 -0
  30. package/package/@types/soundfont/write/inst.d.ts +6 -0
  31. package/package/@types/soundfont/write/pbag.d.ts +6 -0
  32. package/package/@types/soundfont/write/pgen.d.ts +6 -0
  33. package/package/@types/soundfont/write/phdr.d.ts +6 -0
  34. package/package/@types/soundfont/write/pmod.d.ts +6 -0
  35. package/package/@types/soundfont/write/sdta.d.ts +11 -0
  36. package/package/@types/soundfont/write/shdr.d.ts +8 -0
  37. package/package/@types/soundfont/write/soundfont_trimmer.d.ts +6 -0
  38. package/package/@types/soundfont/write/write.d.ts +21 -0
  39. package/package/@types/synthetizer/audio_effects/effects_config.d.ts +29 -0
  40. package/package/@types/synthetizer/audio_effects/fancy_chorus.d.ts +93 -0
  41. package/package/@types/synthetizer/audio_effects/reverb.d.ts +7 -0
  42. package/package/@types/synthetizer/synth_event_handler.d.ts +161 -0
  43. package/package/@types/synthetizer/synthetizer.d.ts +294 -0
  44. package/package/@types/synthetizer/worklet_system/message_protocol/worklet_message.d.ts +89 -0
  45. package/package/@types/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.d.ts +134 -0
  46. package/package/@types/synthetizer/worklet_url.d.ts +5 -0
  47. package/package/@types/utils/buffer_to_wav.d.ts +8 -0
  48. package/package/@types/utils/byte_functions/big_endian.d.ts +13 -0
  49. package/package/@types/utils/byte_functions/little_endian.d.ts +35 -0
  50. package/package/@types/utils/byte_functions/string.d.ts +22 -0
  51. package/package/@types/utils/byte_functions/variable_length_quantity.d.ts +12 -0
  52. package/package/@types/utils/indexed_array.d.ts +21 -0
  53. package/package/@types/utils/loggin.d.ts +26 -0
  54. package/package/@types/utils/other.d.ts +32 -0
  55. package/package/LICENSE +26 -0
  56. package/package/README.md +84 -0
  57. package/package/externals/NOTICE +9 -0
  58. package/package/externals/libvorbis/@types/OggVorbisEncoder.d.ts +34 -0
  59. package/package/externals/libvorbis/OggVorbisEncoder.min.js +1 -0
  60. package/package/externals/stbvorbis_sync/@types/stbvorbis_sync.d.ts +12 -0
  61. package/package/externals/stbvorbis_sync/LICENSE +202 -0
  62. package/package/externals/stbvorbis_sync/stbvorbis_sync.min.js +1 -0
  63. package/package/index.js +73 -0
  64. package/package/midi_handler/README.md +3 -0
  65. package/package/midi_handler/midi_handler.js +118 -0
  66. package/package/midi_handler/web_midi_link.js +41 -0
  67. package/package/midi_parser/README.md +3 -0
  68. package/package/midi_parser/midi_data.js +121 -0
  69. package/package/midi_parser/midi_editor.js +557 -0
  70. package/package/midi_parser/midi_loader.js +502 -0
  71. package/package/midi_parser/midi_message.js +234 -0
  72. package/package/midi_parser/midi_writer.js +95 -0
  73. package/package/midi_parser/rmidi_writer.js +271 -0
  74. package/package/midi_parser/used_keys_loaded.js +172 -0
  75. package/package/package.json +43 -0
  76. package/package/sequencer/README.md +23 -0
  77. package/package/sequencer/sequencer.js +439 -0
  78. package/package/sequencer/worklet_sequencer/events.js +92 -0
  79. package/package/sequencer/worklet_sequencer/play.js +309 -0
  80. package/package/sequencer/worklet_sequencer/process_event.js +167 -0
  81. package/package/sequencer/worklet_sequencer/process_tick.js +85 -0
  82. package/package/sequencer/worklet_sequencer/sequencer_message.js +39 -0
  83. package/package/sequencer/worklet_sequencer/song_control.js +193 -0
  84. package/package/sequencer/worklet_sequencer/worklet_sequencer.js +218 -0
  85. package/package/soundfont/README.md +8 -0
  86. package/package/soundfont/read/generators.js +212 -0
  87. package/package/soundfont/read/instruments.js +125 -0
  88. package/package/soundfont/read/modulators.js +249 -0
  89. package/package/soundfont/read/presets.js +300 -0
  90. package/package/soundfont/read/riff_chunk.js +81 -0
  91. package/package/soundfont/read/samples.js +398 -0
  92. package/package/soundfont/read/zones.js +310 -0
  93. package/package/soundfont/soundfont.js +357 -0
  94. package/package/soundfont/write/ibag.js +39 -0
  95. package/package/soundfont/write/igen.js +75 -0
  96. package/package/soundfont/write/imod.js +46 -0
  97. package/package/soundfont/write/inst.js +34 -0
  98. package/package/soundfont/write/pbag.js +39 -0
  99. package/package/soundfont/write/pgen.js +77 -0
  100. package/package/soundfont/write/phdr.js +42 -0
  101. package/package/soundfont/write/pmod.js +46 -0
  102. package/package/soundfont/write/sdta.js +72 -0
  103. package/package/soundfont/write/shdr.js +54 -0
  104. package/package/soundfont/write/soundfont_trimmer.js +169 -0
  105. package/package/soundfont/write/write.js +180 -0
  106. package/package/synthetizer/README.md +6 -0
  107. package/package/synthetizer/audio_effects/effects_config.js +21 -0
  108. package/package/synthetizer/audio_effects/fancy_chorus.js +120 -0
  109. package/package/synthetizer/audio_effects/impulse_response_2.flac +0 -0
  110. package/package/synthetizer/audio_effects/reverb.js +24 -0
  111. package/package/synthetizer/synth_event_handler.js +156 -0
  112. package/package/synthetizer/synthetizer.js +766 -0
  113. package/package/synthetizer/worklet_processor.min.js +13 -0
  114. package/package/synthetizer/worklet_system/README.md +6 -0
  115. package/package/synthetizer/worklet_system/main_processor.js +363 -0
  116. package/package/synthetizer/worklet_system/message_protocol/handle_message.js +197 -0
  117. package/package/synthetizer/worklet_system/message_protocol/message_sending.js +74 -0
  118. package/package/synthetizer/worklet_system/message_protocol/worklet_message.js +121 -0
  119. package/package/synthetizer/worklet_system/minify_processor.sh +4 -0
  120. package/package/synthetizer/worklet_system/worklet_methods/controller_control.js +230 -0
  121. package/package/synthetizer/worklet_system/worklet_methods/data_entry.js +277 -0
  122. package/package/synthetizer/worklet_system/worklet_methods/note_off.js +109 -0
  123. package/package/synthetizer/worklet_system/worklet_methods/note_on.js +91 -0
  124. package/package/synthetizer/worklet_system/worklet_methods/program_control.js +183 -0
  125. package/package/synthetizer/worklet_system/worklet_methods/reset_controllers.js +177 -0
  126. package/package/synthetizer/worklet_system/worklet_methods/snapshot.js +129 -0
  127. package/package/synthetizer/worklet_system/worklet_methods/system_exclusive.js +272 -0
  128. package/package/synthetizer/worklet_system/worklet_methods/tuning_control.js +195 -0
  129. package/package/synthetizer/worklet_system/worklet_methods/vibrato_control.js +29 -0
  130. package/package/synthetizer/worklet_system/worklet_methods/voice_control.js +233 -0
  131. package/package/synthetizer/worklet_system/worklet_processor.js +9 -0
  132. package/package/synthetizer/worklet_system/worklet_utilities/lfo.js +23 -0
  133. package/package/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js +130 -0
  134. package/package/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js +73 -0
  135. package/package/synthetizer/worklet_system/worklet_utilities/modulator_curves.js +86 -0
  136. package/package/synthetizer/worklet_system/worklet_utilities/stereo_panner.js +81 -0
  137. package/package/synthetizer/worklet_system/worklet_utilities/unit_converter.js +66 -0
  138. package/package/synthetizer/worklet_system/worklet_utilities/volume_envelope.js +265 -0
  139. package/package/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js +83 -0
  140. package/package/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js +234 -0
  141. package/package/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js +116 -0
  142. package/package/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +272 -0
  143. package/package/synthetizer/worklet_url.js +5 -0
  144. package/package/utils/README.md +4 -0
  145. package/package/utils/buffer_to_wav.js +101 -0
  146. package/package/utils/byte_functions/big_endian.js +28 -0
  147. package/package/utils/byte_functions/little_endian.js +74 -0
  148. package/package/utils/byte_functions/string.js +97 -0
  149. package/package/utils/byte_functions/variable_length_quantity.js +37 -0
  150. package/package/utils/encode_vorbis.js +30 -0
  151. package/package/utils/indexed_array.js +41 -0
  152. package/package/utils/loggin.js +79 -0
  153. package/package/utils/other.js +54 -0
  154. package/package.json +43 -0
@@ -0,0 +1,502 @@
1
+ import { dataBytesAmount, getChannel, messageTypes, MidiMessage } from './midi_message.js'
2
+ import { IndexedByteArray } from '../utils/indexed_array.js'
3
+ import { arrayToHexString, consoleColors, formatTitle } from '../utils/other.js'
4
+ import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from '../utils/loggin.js'
5
+ import { readRIFFChunk } from '../soundfont/read/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
+
10
+ /**
11
+ * midi_loader.js
12
+ * purpose: parses a midi file for the seqyencer, including things like marker or CC 2/4 loop detection, copyright detection etc.
13
+ */
14
+ class MIDI{
15
+ /**
16
+ * Parses a given midi file
17
+ * @param arrayBuffer {ArrayBuffer}
18
+ * @param fileName {string} optional, replaces the decoded title if empty
19
+ */
20
+ constructor(arrayBuffer, fileName="") {
21
+ SpessaSynthGroupCollapsed(`%cParsing MIDI File...`, consoleColors.info);
22
+ const binaryData = new IndexedByteArray(arrayBuffer);
23
+ let fileByteArray;
24
+
25
+ // check for rmid
26
+ /**
27
+ * If the RMI file has an embedded sf2 in it, it will appeear here, otherwise undefined
28
+ * @type {ArrayBuffer}
29
+ */
30
+ this.embeddedSoundFont = undefined;
31
+
32
+ /**
33
+ * Contains the copyright strings
34
+ * @type {string}
35
+ */
36
+ this.copyright = "";
37
+
38
+ const initialString = readBytesAsString(binaryData, 4);
39
+ binaryData.currentIndex -= 4;
40
+ if(initialString === "RIFF")
41
+ {
42
+ // possibly an RMID file (https://web.archive.org/web/20110610135604/http://www.midi.org/about-midi/rp29spec(rmid).pdf)
43
+ // skip size
44
+ binaryData.currentIndex += 8;
45
+ const rmid = readBytesAsString(binaryData, 4, undefined, false);
46
+ if(rmid !== "RMID")
47
+ {
48
+ SpessaSynthGroupEnd();
49
+ throw new SyntaxError(`Invalid RMIDI Header! Expected "RMID", got "${rmid}"`);
50
+ }
51
+ const riff = readRIFFChunk(binaryData);
52
+ if(riff.header !== 'data')
53
+ {
54
+ SpessaSynthGroupEnd();
55
+ throw new SyntaxError(`Invalid RMIDI Chunk header! Expected "data", got "${rmid}"`);
56
+ }
57
+ // this is an rmid, load the midi into array for parsing
58
+ fileByteArray = riff.chunkData;
59
+
60
+ // keep loading chunks until we get sfbk
61
+ while(binaryData.currentIndex <= binaryData.length)
62
+ {
63
+ const startIndex = binaryData.currentIndex;
64
+ const currentChunk = readRIFFChunk(binaryData, true);
65
+ if(currentChunk.header === "RIFF")
66
+ {
67
+ const type = readBytesAsString(currentChunk.chunkData, 4);
68
+ if(type === "sfbk")
69
+ {
70
+ SpessaSynthInfo("%cFound embedded soundfont!", consoleColors.recognized);
71
+ this.embeddedSoundFont = binaryData.slice(startIndex, startIndex + currentChunk.size).buffer;
72
+ }
73
+ }
74
+ }
75
+ }
76
+ else
77
+ {
78
+ fileByteArray = binaryData;
79
+ }
80
+ const headerChunk = this.readMIDIChunk(fileByteArray);
81
+ if(headerChunk.type !== "MThd")
82
+ {
83
+ SpessaSynthGroupEnd();
84
+ throw new SyntaxError(`Invalid MIDI Header! Expected "MThd", got "${headerChunk.type}"`);
85
+ }
86
+
87
+ if(headerChunk.size !== 6)
88
+ {
89
+ SpessaSynthGroupEnd();
90
+ throw new RangeError(`Invalid MIDI header chunk size! Expected 6, got ${headerChunk.size}`);
91
+ }
92
+
93
+ // format
94
+ this.format = readBytesAsUintBigEndian(headerChunk.data, 2);
95
+ // tracks count
96
+ this.tracksAmount = readBytesAsUintBigEndian(headerChunk.data, 2);
97
+ // time division
98
+ this.timeDivision = readBytesAsUintBigEndian(headerChunk.data, 2);
99
+
100
+ /**
101
+ * The MIDI's key range
102
+ * @type {{min: number, max: number}}
103
+ */
104
+ this.keyRange = {min: 127, max: 0};
105
+
106
+ /**
107
+ * Contains the lyrics as binary chunks
108
+ * @type {Uint8Array[]}
109
+ */
110
+ this.lyrics = [];
111
+
112
+ /**
113
+ * Contains all the tempo changes in the file. (Ordered from last to first)
114
+ * @type {{
115
+ * ticks: number,
116
+ * tempo: number
117
+ * }[]}
118
+ */
119
+ this.tempoChanges = [{ticks: 0, tempo: 120}];
120
+
121
+ let loopStart = null;
122
+ let loopEnd = null;
123
+
124
+ this.lastVoiceEventTick = 0;
125
+
126
+ /**
127
+ * Midi port numbers for each tracks
128
+ * @type {number[]}
129
+ */
130
+ this.midiPorts = [];
131
+
132
+ /**
133
+ * All channels that each track uses. Note: these channels range from 0 to 15, excluding the port offsets!
134
+ * @type {Set<number>[]}
135
+ */
136
+ this.usedChannelsOnTrack = [];
137
+
138
+ /**
139
+ * Read all the tracks
140
+ * @type {MidiMessage[][]}
141
+ */
142
+ this.tracks = [];
143
+ for(let i = 0; i < this.tracksAmount; i++)
144
+ {
145
+ /**
146
+ * @type {MidiMessage[]}
147
+ */
148
+ const track = [];
149
+ const trackChunk = this.readMIDIChunk(fileByteArray);
150
+ const usedChannels = new Set();
151
+ this.midiPorts.push(-1);
152
+
153
+ if(trackChunk.type !== "MTrk")
154
+ {
155
+ SpessaSynthGroupEnd();
156
+ throw new SyntaxError(`Invalid track header! Expected "MTrk" got "${trackChunk.type}"`);
157
+ }
158
+
159
+ /**
160
+ * MIDI running byte
161
+ * @type {number}
162
+ */
163
+ let runningByte = undefined;
164
+
165
+ let totalTicks = 0;
166
+ // format 2 plays sequentially
167
+ if(this.format === 2 && i > 0)
168
+ {
169
+ totalTicks += this.tracks[i - 1][this.tracks[i - 1].length - 1].ticks;
170
+ }
171
+ // loop until we reach the end of track
172
+ while(trackChunk.data.currentIndex < trackChunk.size)
173
+ {
174
+ totalTicks += readVariableLengthQuantity(trackChunk.data);
175
+
176
+ // check if the status byte is valid (IE. larger than 127)
177
+ const statusByteCheck = trackChunk.data[trackChunk.data.currentIndex];
178
+
179
+ let statusByte;
180
+ // if we have a running byte and the status byte isn't valid
181
+ if(runningByte !== undefined && statusByteCheck < 0x80)
182
+ {
183
+ statusByte = runningByte;
184
+ }
185
+ else if(!runningByte && statusByteCheck < 0x80)
186
+ {
187
+ // if we don't have a running byte and the status byte isn't valid, it's an error.
188
+ SpessaSynthGroupEnd();
189
+ throw new SyntaxError(`Unexpected byte with no running byte. (${statusByteCheck})`);
190
+ }
191
+ else
192
+ {
193
+ // if the status byte is valid, just use that
194
+ statusByte = trackChunk.data[trackChunk.data.currentIndex++];
195
+ }
196
+ const statusByteChannel = getChannel(statusByte);
197
+
198
+ let eventDataLength;
199
+
200
+ // determine the message's length;
201
+ switch(statusByteChannel)
202
+ {
203
+ case -1:
204
+ // system common/realtime (no length)
205
+ eventDataLength = 0;
206
+ break;
207
+
208
+ case -2:
209
+ // meta (the next is the actual status byte)
210
+ statusByte = trackChunk.data[trackChunk.data.currentIndex++];
211
+ eventDataLength = readVariableLengthQuantity(trackChunk.data);
212
+ break;
213
+
214
+ case -3:
215
+ // sysex
216
+ eventDataLength = readVariableLengthQuantity(trackChunk.data);
217
+ break;
218
+
219
+ default:
220
+ // voice message
221
+ // get the midi message length
222
+ if(totalTicks > this.lastVoiceEventTick)
223
+ {
224
+ this.lastVoiceEventTick = totalTicks;
225
+ }
226
+ eventDataLength = dataBytesAmount[statusByte >> 4];
227
+ if((statusByte & 0xF0) === messageTypes.noteOn)
228
+ {
229
+ usedChannels.add(statusByteChannel);
230
+ const note = trackChunk.data[trackChunk.data.currentIndex]
231
+ this.keyRange.min = Math.min(this.keyRange.min, note);
232
+ this.keyRange.max = Math.max(this.keyRange.max, note);
233
+ }
234
+
235
+ // save the status byte
236
+ runningByte = statusByte;
237
+ break;
238
+ }
239
+
240
+ // put the event data into the array
241
+ const eventData = new IndexedByteArray(eventDataLength);
242
+ const messageData = trackChunk.data.slice(trackChunk.data.currentIndex, trackChunk.data.currentIndex + eventDataLength);
243
+ trackChunk.data.currentIndex += eventDataLength;
244
+ eventData.set(messageData, 0);
245
+
246
+ const message = new MidiMessage(totalTicks, statusByte, eventData);
247
+ track.push(message);
248
+
249
+ switch(statusByteChannel)
250
+ {
251
+ case -2:
252
+ // since this is a meta message
253
+ switch(statusByte)
254
+ {
255
+ case messageTypes.setTempo:
256
+ // add the tempo change
257
+ this.tempoChanges.push({
258
+ ticks: totalTicks,
259
+ tempo: 60000000 / readBytesAsUintBigEndian(messageData, 3)
260
+ });
261
+ break;
262
+
263
+ case messageTypes.marker:
264
+ // check for loop markers
265
+ const text = readBytesAsString(eventData, eventData.length).trim().toLowerCase();
266
+ switch (text)
267
+ {
268
+ default:
269
+ break;
270
+
271
+ case "start":
272
+ case "loopstart":
273
+ loopStart = totalTicks;
274
+ break;
275
+
276
+ case "loopend":
277
+ loopEnd = totalTicks;
278
+ }
279
+ eventData.currentIndex = 0;
280
+ break;
281
+
282
+ case messageTypes.midiPort:
283
+ this.midiPorts[i] = eventData[0];
284
+ break;
285
+
286
+ case messageTypes.copyright:
287
+ this.copyright += readBytesAsString(eventData, eventData.length) + "\n";
288
+ break;
289
+
290
+ case messageTypes.lyric:
291
+ this.lyrics.push(eventData);
292
+ }
293
+ break;
294
+
295
+ case -3:
296
+ // since this is a sysex message
297
+ // check for embedded copyright (roland SC display sysex) http://www.bandtrax.com.au/sysex.htm
298
+ // header goes like this: 41 10 45 12 10 00 00
299
+ if(arrayToHexString(eventData.slice(0, 7)).trim() === "41 10 45 12 10 00 00")
300
+ {
301
+ /**
302
+ * @type {IndexedByteArray}
303
+ */
304
+ const cutText = eventData.slice(7, messageData.length - 3);
305
+ const decoded = readBytesAsString(cutText, cutText.length) + "\n";
306
+ this.copyright += decoded;
307
+ SpessaSynthInfo(`%cDecoded Roland SC message! %c${decoded}`,
308
+ consoleColors.recognized,
309
+ consoleColors.value)
310
+ }
311
+ break;
312
+
313
+
314
+ default:
315
+ // since this is a voice message
316
+ // check for loop (CC 2/4)
317
+ if((statusByte & 0xF0) === messageTypes.controllerChange)
318
+ {
319
+ switch(eventData[0])
320
+ {
321
+ case 2:
322
+ case 116:
323
+ loopStart = totalTicks;
324
+ break;
325
+
326
+ case 4:
327
+ case 117:
328
+ if(loopEnd === null)
329
+ {
330
+ loopEnd = totalTicks;
331
+ }
332
+ else
333
+ {
334
+ // this controller has occured more than once, this means that it doesnt indicate the loop
335
+ loopEnd = 0;
336
+ }
337
+ break;
338
+ }
339
+ }
340
+ }
341
+ }
342
+ this.tracks.push(track);
343
+ this.usedChannelsOnTrack.push(usedChannels);
344
+ SpessaSynthInfo(`%cParsed %c${this.tracks.length}%c / %c${this.tracksAmount}`,
345
+ consoleColors.info,
346
+ consoleColors.value,
347
+ consoleColors.info,
348
+ consoleColors.value);
349
+ }
350
+
351
+ const firstNoteOns = [];
352
+ for(const t of this.tracks)
353
+ {
354
+ const firstNoteOn = t.find(e => (e.messageStatusByte & 0xF0) === messageTypes.noteOn);
355
+ if(firstNoteOn)
356
+ {
357
+ firstNoteOns.push(firstNoteOn.ticks);
358
+ }
359
+ }
360
+ this.firstNoteOn = Math.min(...firstNoteOns);
361
+
362
+ SpessaSynthInfo(`%cMIDI file parsed. Total tick time: %c${this.lastVoiceEventTick}`,
363
+ consoleColors.info,
364
+ consoleColors.recognized);
365
+ SpessaSynthGroupEnd();
366
+
367
+ if(loopStart !== null && loopEnd === null)
368
+ {
369
+ // not a loop
370
+ loopStart = this.firstNoteOn;
371
+ loopEnd = this.lastVoiceEventTick;
372
+ }
373
+ else {
374
+ if (loopStart === null) {
375
+ loopStart = this.firstNoteOn;
376
+ }
377
+
378
+ if (loopEnd === null || loopEnd === 0) {
379
+ loopEnd = this.lastVoiceEventTick;
380
+ }
381
+ }
382
+
383
+ // fix midi ports:
384
+ // midi tracks without ports will have a value of -1
385
+ // if all ports have a value of -1, set it to 0, otherwise take the first midi port and replace all -1 with it
386
+ // why do this? some midis (for some reason) specify all channels to port 1 or else, but leave the conductor track with no port pref.
387
+ // this spessasynth to reserve the first 16 channels for the conductor track (which doesn't play anything) and use additional 16 for the actual ports.
388
+ let defaultPort = 0;
389
+ for(let port of this.midiPorts)
390
+ {
391
+ if(port !== -1)
392
+ {
393
+ defaultPort = port;
394
+ break;
395
+ }
396
+ }
397
+ this.midiPorts = this.midiPorts.map(port => port === -1 ? defaultPort : port);
398
+
399
+ /**
400
+ *
401
+ * @type {{start: number, end: number}}
402
+ */
403
+ this.loop = {start: loopStart, end: loopEnd};
404
+
405
+ // get track name
406
+ this.midiName = "";
407
+
408
+ this.rawMidiName = new Uint8Array(0);
409
+
410
+ // midi name
411
+ if(this.tracks.length > 1)
412
+ {
413
+ // if more than 1 track and the first track has no notes, just find the first trackName in the first track
414
+ if(this.tracks[0].find(
415
+ message => message.messageStatusByte >= messageTypes.noteOn
416
+ &&
417
+ message.messageStatusByte < messageTypes.systemExclusive
418
+ ) === undefined)
419
+ {
420
+ let name = this.tracks[0].find(message => message.messageStatusByte === messageTypes.trackName);
421
+ if(name)
422
+ {
423
+ this.rawMidiName = name.messageData;
424
+ this.midiName = readBytesAsString(name.messageData, name.messageData.length, undefined, false);
425
+ }
426
+ }
427
+ }
428
+ else
429
+ {
430
+ // if only 1 track, find the first "track name" event
431
+ let name = this.tracks[0].find(message => message.messageStatusByte === messageTypes.trackName);
432
+ if(name)
433
+ {
434
+ this.rawMidiName = name.messageData;
435
+ this.midiName = readBytesAsString(name.messageData, name.messageData.length, undefined, false);
436
+ }
437
+ }
438
+
439
+ this.fileName = fileName;
440
+ this.midiName = this.midiName.trim();
441
+ // if midiName is "", use the file name
442
+ if(this.midiName.length === 0)
443
+ {
444
+ this.midiName = formatTitle(fileName);
445
+ // encode it too
446
+ this.rawMidiName = new Uint8Array(this.midiName.length);
447
+ for(let i = 0; i < this.midiName.length; i++)
448
+ {
449
+ this.rawMidiName[i] = this.midiName.charCodeAt(i);
450
+ }
451
+ }
452
+
453
+ // reverse the tempo changes
454
+ this.tempoChanges.reverse();
455
+
456
+ /**
457
+ * The total playback time, in seconds
458
+ * @type {number}
459
+ */
460
+ this.duration = this._ticksToSeconds(this.lastVoiceEventTick);
461
+ }
462
+
463
+ /**
464
+ * @param fileByteArray {IndexedByteArray}
465
+ * @returns {{type: string, size: number, data: IndexedByteArray}}
466
+ */
467
+ readMIDIChunk(fileByteArray)
468
+ {
469
+ const chunk = {};
470
+ // type
471
+ chunk.type = readBytesAsString(fileByteArray, 4);
472
+ // size
473
+ chunk.size = readBytesAsUintBigEndian(fileByteArray, 4);
474
+ // data
475
+ chunk.data = new IndexedByteArray(chunk.size);
476
+ const dataSlice = fileByteArray.slice(fileByteArray.currentIndex, fileByteArray.currentIndex + chunk.size);
477
+ chunk.data.set(dataSlice, 0);
478
+ fileByteArray.currentIndex += chunk.size;
479
+ return chunk;
480
+ }
481
+
482
+
483
+ /**
484
+ * Coverts ticks to time in seconds
485
+ * @param ticks {number}
486
+ * @returns {number}
487
+ * @private
488
+ */
489
+ _ticksToSeconds(ticks)
490
+ {
491
+ if (ticks <= 0) {
492
+ return 0;
493
+ }
494
+
495
+ // find the last tempo change that has occured
496
+ let tempo = this.tempoChanges.find(v => v.ticks < ticks);
497
+
498
+ let timeSinceLastTempo = ticks - tempo.ticks;
499
+ return this._ticksToSeconds(ticks - timeSinceLastTempo) + (timeSinceLastTempo * 60) / (tempo.tempo * this.timeDivision);
500
+ }
501
+ }
502
+ export { MIDI }