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,193 @@
1
+ import { SpessaSynthInfo } from "./loggin.js";
2
+ import { consoleColors } from "./other.js";
3
+ import { DEFAULT_PERCUSSION } from "../synthetizer/synth_constants.js";
4
+
5
+ export const XG_SFX_VOICE = 64;
6
+
7
+ const GM2_DEFAULT_BANK = 121;
8
+
9
+ /**
10
+ * @param sys {SynthSystem}
11
+ * @returns {number}
12
+ */
13
+ export function getDefaultBank(sys)
14
+ {
15
+ return sys === "gm2" ? GM2_DEFAULT_BANK : 0;
16
+ }
17
+
18
+ /**
19
+ * @param bankNr {number}
20
+ * @returns {boolean}
21
+ */
22
+ export function isXGDrums(bankNr)
23
+ {
24
+ return bankNr === 120 || bankNr === 126 || bankNr === 127;
25
+ }
26
+
27
+ /**
28
+ * @param bank {number}
29
+ * @returns {boolean}
30
+ */
31
+ export function isValidXGMSB(bank)
32
+ {
33
+ return isXGDrums(bank) || bank === XG_SFX_VOICE || bank === GM2_DEFAULT_BANK;
34
+ }
35
+
36
+ /**
37
+ * Bank select hacks abstracted here
38
+ * @param bankBefore {number} the current bank number
39
+ * @param bank {number} the cc change bank number
40
+ * @param system {SynthSystem} MIDI system
41
+ * @param isLSB {boolean} is bank LSB?
42
+ * @param isDrums {boolean} is drum channel?
43
+ * @param channelNumber {number} channel number
44
+ * @returns {{
45
+ * newBank: number,
46
+ * drumsStatus: 0|1|2
47
+ * }} 0 - unchanged, 1 - OFF, 2 - ON
48
+ */
49
+ export function parseBankSelect(bankBefore, bank, system, isLSB, isDrums, channelNumber)
50
+ {
51
+ // 64 means SFX in MSB, so it is allowed
52
+ let out = bankBefore;
53
+ let drumsStatus = 0;
54
+ if (isLSB)
55
+ {
56
+ if (isSystemXG(system))
57
+ {
58
+ if (!isValidXGMSB(bank))
59
+ {
60
+ out = bank;
61
+ }
62
+ }
63
+ else if (system === "gm2")
64
+ {
65
+ out = bank;
66
+ }
67
+ }
68
+ else
69
+ {
70
+ let canSetBankSelect = true;
71
+ switch (system)
72
+ {
73
+ case "gm":
74
+ // gm ignores bank select
75
+ SpessaSynthInfo(
76
+ `%cIgnoring the Bank Select (${bank}), as the synth is in GM mode.`,
77
+ consoleColors.info
78
+ );
79
+ canSetBankSelect = false;
80
+ break;
81
+
82
+ case "xg":
83
+ canSetBankSelect = isValidXGMSB(bank);
84
+ // for xg, if msb is 120, 126 or 127, then it's drums
85
+ if (isXGDrums(bank))
86
+ {
87
+ drumsStatus = 2;
88
+ }
89
+ else
90
+ {
91
+ // drums shall not be disabled on channel 9
92
+ if (channelNumber % 16 !== DEFAULT_PERCUSSION)
93
+ {
94
+ drumsStatus = 1;
95
+ }
96
+ }
97
+ break;
98
+
99
+ case "gm2":
100
+ if (bank === 120)
101
+ {
102
+ drumsStatus = 2;
103
+ }
104
+ else
105
+ {
106
+ if (channelNumber % 16 !== DEFAULT_PERCUSSION)
107
+ {
108
+ drumsStatus = 1;
109
+ }
110
+ }
111
+ }
112
+
113
+ if (isDrums)
114
+ {
115
+ // 128 for percussion channel
116
+ bank = 128;
117
+ }
118
+ if (bank === 128 && !isDrums)
119
+ {
120
+ // if a channel is not for percussion, default to bank current
121
+ bank = bankBefore;
122
+ }
123
+ if (canSetBankSelect)
124
+ {
125
+ out = bank;
126
+ }
127
+ }
128
+ return {
129
+ newBank: out,
130
+ drumsStatus: drumsStatus
131
+ };
132
+ }
133
+
134
+
135
+ /**
136
+ * Chooses a bank number according to spessasynth logic
137
+ * That is:
138
+ * for GS, bank MSB if not drum, otherwise 128
139
+ * for XG: bank MSB if drum and MSB is valid, 128 othewise, bank MSB if it is SFX voice, LSB otherwise
140
+ * @param msb {number}
141
+ * @param lsb {number}
142
+ * @param isDrums {boolean}
143
+ * @param isXG {boolean}
144
+ * @returns {number}
145
+ */
146
+ export function chooseBank(msb, lsb, isDrums, isXG)
147
+ {
148
+ if (isXG)
149
+ {
150
+ if (isDrums)
151
+ {
152
+ if (isXGDrums(msb))
153
+ {
154
+ return msb;
155
+ }
156
+ else
157
+ {
158
+ return 128;
159
+ }
160
+ }
161
+ else
162
+ {
163
+ // check for SFX
164
+ if (isValidXGMSB(msb))
165
+ {
166
+ return msb;
167
+ }
168
+ // if lsb is 0 and msb is not, use that
169
+ if (lsb === 0 && msb !== 0)
170
+ {
171
+ return msb;
172
+ }
173
+ if (!isValidXGMSB(lsb))
174
+ {
175
+ return lsb;
176
+ }
177
+ return 0;
178
+ }
179
+ }
180
+ else
181
+ {
182
+ return isDrums ? 128 : msb;
183
+ }
184
+ }
185
+
186
+ /**
187
+ * @param system {SynthSystem}
188
+ * @returns boolean
189
+ */
190
+ export function isSystemXG(system)
191
+ {
192
+ return system === "gm2" || system === "xg";
193
+ }
@@ -1,10 +0,0 @@
1
- <component name="InspectionProjectProfileManager">
2
- <profile version="1.0">
3
- <option name="myName" value="Project Default" />
4
- <inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
5
- <option name="processCode" value="true" />
6
- <option name="processLiterals" value="true" />
7
- <option name="processComments" value="true" />
8
- </inspection_tool>
9
- </profile>
10
- </component>
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="JavaScriptLibraryMappings">
4
- <includedPredefinedLibrary name="Node.js Core" />
5
- </component>
6
- </project>
package/.idea/modules.xml DELETED
@@ -1,8 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="ProjectModuleManager">
4
- <modules>
5
- <module fileurl="file://$PROJECT_DIR$/.idea/spessasynth_core.iml" filepath="$PROJECT_DIR$/.idea/spessasynth_core.iml" />
6
- </modules>
7
- </component>
8
- </project>
@@ -1,12 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <module type="WEB_MODULE" version="4">
3
- <component name="NewModuleRootManager">
4
- <content url="file://$MODULE_DIR$">
5
- <excludeFolder url="file://$MODULE_DIR$/.tmp" />
6
- <excludeFolder url="file://$MODULE_DIR$/temp" />
7
- <excludeFolder url="file://$MODULE_DIR$/tmp" />
8
- </content>
9
- <orderEntry type="inheritedJdk" />
10
- <orderEntry type="sourceFolder" forTests="false" />
11
- </component>
12
- </module>
package/.idea/vcs.xml DELETED
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="VcsDirectoryMappings">
4
- <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
- </component>
6
- </project>
@@ -1,3 +0,0 @@
1
- ## This is the MIDI file parsing folder.
2
- The code here is responsible for parsing the MIDI files and interpreting the messsages.
3
- All the events are defined in the `midi_message.js` file.
@@ -1,386 +0,0 @@
1
- import { dataBytesAmount, getChannel, messageTypes, MidiMessage } from './midi_message.js'
2
- import {ShiftableByteArray} from "../utils/shiftable_array.js";
3
- import {
4
- readByte,
5
- readBytesAsString,
6
- readBytesAsUintBigEndian,
7
- readVariableLengthQuantity
8
- } from "../utils/byte_functions.js";
9
- import { arrayToHexString, consoleColors, formatTitle } from '../utils/other.js'
10
- import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from '../utils/loggin.js'
11
-
12
- /**
13
- * midi_loader.js
14
- * purpose: parses a midi file for the seqyencer, including things like marker or CC 2/4 loop detection, copyright detection etc.
15
- */
16
- export class MIDI{
17
- /**
18
- * Parses a given midi file
19
- * @param arrayBuffer {Buffer}
20
- * @param fileName {string} optional, replaces the decoded title if empty
21
- */
22
- constructor(arrayBuffer, fileName="") {
23
- SpessaSynthGroupCollapsed(`%cParsing MIDI File...`, consoleColors.info);
24
-
25
- const fileByteArray = new ShiftableByteArray(arrayBuffer);
26
- const headerChunk = this.readMIDIChunk(fileByteArray);
27
- if(headerChunk.type !== "MThd")
28
- {
29
- SpessaSynthGroupEnd();
30
- throw new SyntaxError(`Invalid MIDI Header! Expected "MThd", got "${headerChunk.type}"`);
31
- }
32
-
33
- if(headerChunk.size !== 6)
34
- {
35
- SpessaSynthGroupEnd();
36
- throw new RangeError(`Invalid MIDI header chunk size! Expected 6, got ${headerChunk.size}`);
37
- }
38
-
39
- // format
40
- const format = readBytesAsUintBigEndian(headerChunk.data, 2);
41
- // tracks count
42
- this.tracksAmount = readBytesAsUintBigEndian(headerChunk.data, 2);
43
- // time division
44
- this.timeDivision = readBytesAsUintBigEndian(headerChunk.data, 2);
45
-
46
- const decoder = new TextDecoder('shift-jis');
47
-
48
- // read the copyright
49
- this.copyright = "";
50
-
51
- /**
52
- * Contains all the tempo changes in the file. (Ordered from last to first)
53
- * @type {{
54
- * ticks: number,
55
- * tempo: number
56
- * }[]}
57
- */
58
- this.tempoChanges = [{ticks: 0, tempo: 120}];
59
-
60
- let loopStart = null;
61
- let loopEnd = null;
62
-
63
- this.lastVoiceEventTick = 0;
64
-
65
- /**
66
- * Midi port numbers for each tracks
67
- * @type {number[]}
68
- */
69
- this.midiPorts = [];
70
-
71
- /**
72
- * Read all the tracks
73
- * @type {MidiMessage[][]}
74
- */
75
- this.tracks = [];
76
- for(let i = 0; i < this.tracksAmount; i++)
77
- {
78
- /**
79
- * @type {MidiMessage[]}
80
- */
81
- const track = [];
82
- const trackChunk = this.readMIDIChunk(fileByteArray);
83
- this.midiPorts.push(0)
84
-
85
- if(trackChunk.type !== "MTrk")
86
- {
87
- SpessaSynthGroupEnd();
88
- throw new SyntaxError(`Invalid track header! Expected "MTrk" got "${trackChunk.type}"`);
89
- }
90
-
91
- /**
92
- * MIDI running byte
93
- * @type {number}
94
- */
95
- let runningByte = undefined;
96
-
97
- let totalTicks = 0;
98
- // format 2 plays sequentially
99
- if(format === 2 && i > 0)
100
- {
101
- totalTicks += this.tracks[i - 1][this.tracks[i - 1].length - 1].ticks;
102
- }
103
- // loop until we reach the end of track
104
- while(trackChunk.data.currentIndex < trackChunk.size)
105
- {
106
- totalTicks += readVariableLengthQuantity(trackChunk.data);
107
-
108
- // check if the status byte is valid (IE. larger than 127)
109
- const statusByteCheck = trackChunk.data[trackChunk.data.currentIndex];
110
-
111
- let statusByte;
112
- // if we have a running byte and the status byte isn't valid
113
- if(runningByte !== undefined && statusByteCheck < 0x80)
114
- {
115
- statusByte = runningByte;
116
- }
117
- else if(!runningByte && statusByteCheck < 0x80)
118
- {
119
- // if we don't have a running byte and the status byte isn't valid, it's an error.
120
- SpessaSynthGroupEnd();
121
- throw new SyntaxError(`Unexpected byte with no running byte. (${statusByteCheck})`);
122
- }
123
- else
124
- {
125
- // if the status byte is valid, just use that
126
- statusByte = readByte(trackChunk.data);
127
- }
128
- const statusByteChannel = getChannel(statusByte);
129
-
130
- let eventDataLength;
131
-
132
- // determine the message's length;
133
- switch(statusByteChannel)
134
- {
135
- case -1:
136
- // system common/realtime (no length)
137
- eventDataLength = 0;
138
- break;
139
- case -2:
140
- // meta (the next is the actual status byte)
141
- statusByte = readByte(trackChunk.data);
142
- eventDataLength = readVariableLengthQuantity(trackChunk.data);
143
- break;
144
- case -3:
145
- // sysex
146
- eventDataLength = readVariableLengthQuantity(trackChunk.data);
147
- break;
148
- default:
149
- // voice message
150
- // get the midi message length
151
- if(totalTicks > this.lastVoiceEventTick)
152
- {
153
- this.lastVoiceEventTick = totalTicks;
154
- }
155
- eventDataLength = dataBytesAmount[statusByte >> 4];
156
- break;
157
- }
158
-
159
- // put the event data into the array
160
- const eventData = new ShiftableByteArray(eventDataLength);
161
- const messageData = trackChunk.data.slice(trackChunk.data.currentIndex, trackChunk.data.currentIndex + eventDataLength);
162
- trackChunk.data.currentIndex += eventDataLength;
163
- eventData.set(messageData, 0);
164
-
165
- runningByte = statusByte;
166
-
167
- const message = new MidiMessage(totalTicks, statusByte, eventData);
168
- track.push(message);
169
-
170
- // check for tempo change
171
- if(statusByte === messageTypes.setTempo)
172
- {
173
- this.tempoChanges.push({
174
- ticks: totalTicks,
175
- tempo: 60000000 / readBytesAsUintBigEndian(messageData, 3)
176
- });
177
- }
178
- else
179
- // check for loop start (Marker "start")
180
-
181
- if(statusByte === messageTypes.marker)
182
- {
183
- const text = readBytesAsString(eventData, eventData.length).trim().toLowerCase();
184
- switch (text)
185
- {
186
- default:
187
- break;
188
-
189
- case "start":
190
- case "loopstart":
191
- loopStart = totalTicks;
192
- break;
193
-
194
- case "loopend":
195
- loopEnd = totalTicks;
196
- }
197
- eventData.currentIndex = 0;
198
-
199
- }
200
- else
201
- // check for loop (CC 2/4)
202
- if((statusByte & 0xF0) === messageTypes.controllerChange)
203
- {
204
- switch(eventData[0])
205
- {
206
- case 2:
207
- case 116:
208
- loopStart = totalTicks;
209
- break;
210
-
211
- case 4:
212
- case 117:
213
- if(loopEnd === null)
214
- {
215
- loopEnd = totalTicks;
216
- }
217
- else
218
- {
219
- // this controller has occured more than once, this means that it doesn't indicate the loop
220
- loopEnd = 0;
221
- }
222
- break;
223
- }
224
- }
225
- else
226
- // check for midi port
227
- if(statusByte === messageTypes.midiPort)
228
- {
229
- this.midiPorts[i] = eventData[0];
230
- }
231
- else
232
- // check for copyright
233
- if(statusByte === messageTypes.copyright)
234
- {
235
- this.copyright += decoder.decode(eventData) + "\n";
236
- }
237
-
238
- // check for embedded copyright (roland SC display sysex) http://www.bandtrax.com.au/sysex.htm
239
- if(statusByte === messageTypes.systemExclusive)
240
- {
241
- // header goes like this: 41 10 45 12 10 00 00
242
- if(arrayToHexString(messageData.slice(0, 7)).trim() === "41 10 45 12 10 00 00")
243
- {
244
- const decoded = decoder.decode(messageData.slice(7, messageData.length - 3)) + "\n";
245
- this.copyright += decoded;
246
- SpessaSynthInfo(`%cDecoded Roland SC message! %c${decoded}`,
247
- consoleColors.recognized,
248
- consoleColors.value)
249
- }
250
- }
251
- }
252
- this.tracks.push(track);
253
- SpessaSynthInfo(`%cParsed %c${this.tracks.length}%c / %c${this.tracksAmount}`,
254
- consoleColors.info,
255
- consoleColors.value,
256
- consoleColors.info,
257
- consoleColors.value);
258
- }
259
-
260
- //this.lastVoiceEventTick = Math.max(...this.tracks.map(track =>
261
- //track[track.length - 1].ticks));
262
- const firstNoteOns = [];
263
- for(const t of this.tracks)
264
- {
265
- const firstNoteOn = t.find(e => (e.messageStatusByte & 0xF0) === messageTypes.noteOn);
266
- if(firstNoteOn)
267
- {
268
- firstNoteOns.push(firstNoteOn.ticks);
269
- }
270
- }
271
- this.firstNoteOn = Math.min(...firstNoteOns);
272
-
273
- SpessaSynthInfo(`%cMIDI file parsed. Total tick time: %c${this.lastVoiceEventTick}`,
274
- consoleColors.info,
275
- consoleColors.recognized);
276
- SpessaSynthGroupEnd();
277
-
278
- if(loopStart !== null && loopEnd === null)
279
- {
280
- // not a loop
281
- loopStart = this.firstNoteOn;
282
- loopEnd = this.lastVoiceEventTick;
283
- }
284
- else {
285
- if (loopStart === null) {
286
- loopStart = this.firstNoteOn;
287
- }
288
-
289
- if (loopEnd === null || loopEnd === 0) {
290
- loopEnd = this.lastVoiceEventTick;
291
- }
292
- }
293
-
294
- /**
295
- *
296
- * @type {{start: number, end: number}}
297
- */
298
- this.loop = {start: loopStart, end: loopEnd};
299
-
300
- // get track name
301
- this.midiName = "";
302
-
303
- // midi name
304
- if(this.tracks.length > 1)
305
- {
306
- // if more than 1 track and the first track has no notes, just find the first trackName in the first track
307
- if(this.tracks[0].find(
308
- message => message.messageStatusByte >= messageTypes.noteOn
309
- &&
310
- message.messageStatusByte < messageTypes.systemExclusive
311
- ) === undefined)
312
- {
313
- let name = this.tracks[0].find(message => message.messageStatusByte === messageTypes.trackName);
314
- if(name)
315
- {
316
- this.midiName = decoder.decode(name.messageData);
317
- }
318
- }
319
- }
320
- else
321
- {
322
- // if only 1 track, find the first "track name" event
323
- let name = this.tracks[0].find(message => message.messageStatusByte === messageTypes.trackName);
324
- if(name)
325
- {
326
- this.midiName = decoder.decode(name.messageData);
327
- }
328
- }
329
-
330
- this.fileName = fileName;
331
-
332
- // if midiName is "", use the file name
333
- if(this.midiName.trim().length === 0 && fileName.length > 0)
334
- {
335
- this.midiName = formatTitle(fileName);
336
- }
337
-
338
- // reverse the tempo changes
339
- this.tempoChanges.reverse();
340
-
341
- /**
342
- * The total playback time, in seconds
343
- * @type {number}
344
- */
345
- this.duration = this._ticksToSeconds(this.lastVoiceEventTick);
346
- }
347
-
348
- /**
349
- * @param fileByteArray {ShiftableByteArray}
350
- * @returns {{type: string, size: number, data: ShiftableByteArray}}
351
- */
352
- readMIDIChunk(fileByteArray)
353
- {
354
- const chunk = {};
355
- // type
356
- chunk.type = readBytesAsString(fileByteArray, 4);
357
- // size
358
- chunk.size = readBytesAsUintBigEndian(fileByteArray, 4);
359
- // data
360
- chunk.data = new ShiftableByteArray(chunk.size);
361
- const dataSlice = fileByteArray.slice(fileByteArray.currentIndex, fileByteArray.currentIndex + chunk.size);
362
- chunk.data.set(dataSlice, 0);
363
- fileByteArray.currentIndex += chunk.size;
364
- return chunk;
365
- }
366
-
367
-
368
- /**
369
- * Coverts ticks to time in seconds
370
- * @param ticks {number}
371
- * @returns {number}
372
- * @private
373
- */
374
- _ticksToSeconds(ticks)
375
- {
376
- if (ticks <= 0) {
377
- return 0;
378
- }
379
-
380
- // find the last tempo change that has occured
381
- let tempo = this.tempoChanges.find(v => v.ticks < ticks);
382
-
383
- let timeSinceLastTempo = ticks - tempo.ticks;
384
- return this._ticksToSeconds(ticks - timeSinceLastTempo) + (timeSinceLastTempo * 60) / (tempo.tempo * this.timeDivision);
385
- }
386
- }