spessasynth_core 1.1.3 → 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 -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,611 @@
1
+ import { messageTypes, midiControllers, MIDIMessage } from "../midi_message.js";
2
+ import { IndexedByteArray } from "../../utils/indexed_array.js";
3
+ import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from "../../utils/loggin.js";
4
+ import { consoleColors } from "../../utils/other.js";
5
+
6
+ import { customControllers } from "../../synthetizer/audio_engine/engine_components/controller_tables.js";
7
+ import { DEFAULT_PERCUSSION } from "../../synthetizer/synth_constants.js";
8
+ import { isGM2On, isGMOn, isGSOn, isXGOn } from "../../utils/sysex_detector.js";
9
+ import { isSystemXG, isXGDrums, XG_SFX_VOICE } from "../../utils/xg_hacks.js";
10
+
11
+ /**
12
+ * @param ticks {number}
13
+ * @returns {MIDIMessage}
14
+ */
15
+ export function getGsOn(ticks)
16
+ {
17
+ return new MIDIMessage(
18
+ ticks,
19
+ messageTypes.systemExclusive,
20
+ new IndexedByteArray([
21
+ 0x41, // Roland
22
+ 0x10, // Device ID (defaults to 16 on roland)
23
+ 0x42, // GS
24
+ 0x12, // Command ID (DT1) (whatever that means...)
25
+ 0x40, // System parameter - Address
26
+ 0x00, // Global parameter - Address
27
+ 0x7F, // GS Change - Address
28
+ 0x00, // turn on - Data
29
+ 0x41, // checksum
30
+ 0xF7 // end of exclusive
31
+ ])
32
+ );
33
+ }
34
+
35
+ /**
36
+ * @param channel {number}
37
+ * @param cc {number}
38
+ * @param value {number}
39
+ * @param ticks {number}
40
+ * @returns {MIDIMessage}
41
+ */
42
+ function getControllerChange(channel, cc, value, ticks)
43
+ {
44
+ return new MIDIMessage(
45
+ ticks,
46
+ messageTypes.controllerChange | (channel % 16),
47
+ new IndexedByteArray([cc, value])
48
+ );
49
+ }
50
+
51
+ /**
52
+ * @param channel {number}
53
+ * @param ticks {number}
54
+ * @returns {MIDIMessage}
55
+ */
56
+ function getDrumChange(channel, ticks)
57
+ {
58
+ const chanAddress = 0x10 | [1, 2, 3, 4, 5, 6, 7, 8, 0, 9, 10, 11, 12, 13, 14, 15][channel % 16];
59
+ // excluding manufacturerID DeviceID and ModelID (and F7)
60
+ const sysexData = [
61
+ 0x41, // Roland
62
+ 0x10, // Device ID (defaults to 16 on roland)
63
+ 0x42, // GS
64
+ 0x12, // Command ID (DT1) (whatever that means...)
65
+ 0x40, // System parameter }
66
+ chanAddress, // Channel parameter } Address
67
+ 0x15, // Drum change }
68
+ 0x01 // Is Drums } Data
69
+ ];
70
+ // calculate checksum
71
+ // https://cdn.roland.com/assets/media/pdf/F-20_MIDI_Imple_e01_W.pdf section 4
72
+ const sum = 0x40 + chanAddress + 0x15 + 0x01;
73
+ const checksum = 128 - (sum % 128);
74
+ // add system exclusive to enable drums
75
+ return new MIDIMessage(
76
+ ticks,
77
+ messageTypes.systemExclusive,
78
+ new IndexedByteArray([
79
+ ...sysexData,
80
+ checksum,
81
+ 0xF7
82
+ ])
83
+ );
84
+ }
85
+
86
+ /**
87
+ * @typedef {Object} DesiredProgramChange
88
+ * @property {number} channel - The channel number.
89
+ * @property {number} program - The program number.
90
+ * @property {number} bank - The bank number.
91
+ * @property {boolean} isDrum - Indicates if the channel is a drum channel.
92
+ * If it is, then the bank number is ignored.
93
+ */
94
+
95
+ /**
96
+ * @typedef {Object} DesiredControllerChange
97
+ * @property {number} channel - The channel number.
98
+ * @property {number} controllerNumber - The MIDI controller number.
99
+ * @property {number} controllerValue - The new controller value.
100
+ */
101
+
102
+ /**
103
+ * @typedef {Object} DesiredChanneltranspose
104
+ * @property {number} channel - The channel number.
105
+ * @property {number} keyShift - The number of semitones to transpose.
106
+ * Note that this can use floating point numbers,
107
+ * which will be used to fine-tune the pitch in cents using RPN.
108
+ */
109
+
110
+
111
+ /**
112
+ * Allows easy editing of the file by removing channels, changing programs,
113
+ * changing controllers and transposing channels. Note that this modifies the MIDI in-place.
114
+ *
115
+ * @this {BasicMIDI}
116
+ * @param {DesiredProgramChange[]} desiredProgramChanges - The programs to set on given channels.
117
+ * @param {DesiredControllerChange[]} desiredControllerChanges - The controllers to set on given channels.
118
+ * @param {number[]} desiredChannelsToClear - The channels to remove from the sequence.
119
+ * @param {DesiredChanneltranspose[]} desiredChannelsToTranspose - The channels to transpose.
120
+ */
121
+ export function modifyMIDI(
122
+ desiredProgramChanges = [],
123
+ desiredControllerChanges = [],
124
+ desiredChannelsToClear = [],
125
+ desiredChannelsToTranspose = []
126
+ )
127
+ {
128
+ const midi = this;
129
+ SpessaSynthGroupCollapsed("%cApplying changes to the MIDI file...", consoleColors.info);
130
+
131
+ SpessaSynthInfo("Desired program changes:", desiredProgramChanges);
132
+ SpessaSynthInfo("Desired CC changes:", desiredControllerChanges);
133
+ SpessaSynthInfo("Desired channels to clear:", desiredChannelsToClear);
134
+ SpessaSynthInfo("Desired channels to transpose:", desiredChannelsToTranspose);
135
+
136
+ /**
137
+ * @type {Set<number>}
138
+ */
139
+ const channelsToChangeProgram = new Set();
140
+ desiredProgramChanges.forEach(c =>
141
+ {
142
+ channelsToChangeProgram.add(c.channel);
143
+ });
144
+
145
+
146
+ // go through all events one by one
147
+ let system = "gs";
148
+ let addedGs = false;
149
+ /**
150
+ * indexes for tracks
151
+ * @type {number[]}
152
+ */
153
+ const eventIndexes = Array(midi.tracks.length).fill(0);
154
+ let remainingTracks = midi.tracks.length;
155
+
156
+ function findFirstEventIndex()
157
+ {
158
+ let index = 0;
159
+ let ticks = Infinity;
160
+ midi.tracks.forEach((track, i) =>
161
+ {
162
+ if (eventIndexes[i] >= track.length)
163
+ {
164
+ return;
165
+ }
166
+ if (track[eventIndexes[i]].ticks < ticks)
167
+ {
168
+ index = i;
169
+ ticks = track[eventIndexes[i]].ticks;
170
+ }
171
+ });
172
+ return index;
173
+ }
174
+
175
+ // it copies midiPorts everywhere else, but here 0 works so DO NOT CHANGE!
176
+ /**
177
+ * midi port number for the corresponding track
178
+ * @type {number[]}
179
+ */
180
+ const midiPorts = midi.midiPorts.slice();
181
+ /**
182
+ * midi port: channel offset
183
+ * @type {Object<number, number>}
184
+ */
185
+ const midiPortChannelOffsets = {};
186
+ let midiPortChannelOffset = 0;
187
+
188
+ function assignMIDIPort(trackNum, port)
189
+ {
190
+ // do not assign ports to empty tracks
191
+ if (midi.usedChannelsOnTrack[trackNum].size === 0)
192
+ {
193
+ return;
194
+ }
195
+
196
+ // assign new 16 channels if the port is not occupied yet
197
+ if (midiPortChannelOffset === 0)
198
+ {
199
+ midiPortChannelOffset += 16;
200
+ midiPortChannelOffsets[port] = 0;
201
+ }
202
+
203
+ if (midiPortChannelOffsets[port] === undefined)
204
+ {
205
+ midiPortChannelOffsets[port] = midiPortChannelOffset;
206
+ midiPortChannelOffset += 16;
207
+ }
208
+
209
+ midiPorts[trackNum] = port;
210
+ }
211
+
212
+ // assign port offsets
213
+ midi.midiPorts.forEach((port, trackIndex) =>
214
+ {
215
+ assignMIDIPort(trackIndex, port);
216
+ });
217
+
218
+ const channelsAmount = midiPortChannelOffset;
219
+ /**
220
+ * Tracks if the channel already had its first note on
221
+ * @type {boolean[]}
222
+ */
223
+ const isFirstNoteOn = Array(channelsAmount).fill(true);
224
+
225
+ /**
226
+ * MIDI key transpose
227
+ * @type {number[]}
228
+ */
229
+ const coarseTranspose = Array(channelsAmount).fill(0);
230
+ /**
231
+ * RPN fine transpose
232
+ * @type {number[]}
233
+ */
234
+ const fineTranspose = Array(channelsAmount).fill(0);
235
+ desiredChannelsToTranspose.forEach(transpose =>
236
+ {
237
+ const coarse = Math.trunc(transpose.keyShift);
238
+ const fine = transpose.keyShift - coarse;
239
+ coarseTranspose[transpose.channel] = coarse;
240
+ fineTranspose[transpose.channel] = fine;
241
+ });
242
+
243
+ while (remainingTracks > 0)
244
+ {
245
+ let trackNum = findFirstEventIndex();
246
+ const track = midi.tracks[trackNum];
247
+ if (eventIndexes[trackNum] >= track.length)
248
+ {
249
+ remainingTracks--;
250
+ continue;
251
+ }
252
+ const index = eventIndexes[trackNum]++;
253
+ const e = track[index];
254
+
255
+ const deleteThisEvent = () =>
256
+ {
257
+ track.splice(index, 1);
258
+ eventIndexes[trackNum]--;
259
+ };
260
+
261
+ /**
262
+ * @param e {MIDIMessage}
263
+ * @param offset{number}
264
+ */
265
+ const addEventBefore = (e, offset = 0) =>
266
+ {
267
+ track.splice(index + offset, 0, e);
268
+ eventIndexes[trackNum]++;
269
+ };
270
+
271
+
272
+ let portOffset = midiPortChannelOffsets[midiPorts[trackNum]] || 0;
273
+ if (e.messageStatusByte === messageTypes.midiPort)
274
+ {
275
+ assignMIDIPort(trackNum, e.messageData[0]);
276
+ continue;
277
+ }
278
+ // don't clear meta
279
+ if (e.messageStatusByte <= messageTypes.sequenceSpecific && e.messageStatusByte >= messageTypes.sequenceNumber)
280
+ {
281
+ continue;
282
+ }
283
+ const status = e.messageStatusByte & 0xF0;
284
+ const midiChannel = e.messageStatusByte & 0xF;
285
+ const channel = midiChannel + portOffset;
286
+ // clear channel?
287
+ if (desiredChannelsToClear.indexOf(channel) !== -1)
288
+ {
289
+ deleteThisEvent();
290
+ continue;
291
+ }
292
+ switch (status)
293
+ {
294
+ case messageTypes.noteOn:
295
+ // is it first?
296
+ if (isFirstNoteOn[channel])
297
+ {
298
+ isFirstNoteOn[channel] = false;
299
+ // all right, so this is the first note on
300
+ // first: controllers
301
+ // because FSMP does not like program changes after cc changes in embedded midis
302
+ // and since we use splice,
303
+ // controllers get added first, then programs before them
304
+ // now add controllers
305
+ desiredControllerChanges.filter(c => c.channel === channel).forEach(change =>
306
+ {
307
+ const ccChange = getControllerChange(
308
+ midiChannel,
309
+ change.controllerNumber,
310
+ change.controllerValue,
311
+ e.ticks
312
+ );
313
+ addEventBefore(ccChange);
314
+ });
315
+ const fineTune = fineTranspose[channel];
316
+
317
+ if (fineTune !== 0)
318
+ {
319
+ // add rpn
320
+ // 64 is the center, 96 = 50 cents up
321
+ const centsCoarse = (fineTune * 64) + 64;
322
+ const rpnCoarse = getControllerChange(midiChannel, midiControllers.RPNMsb, 0, e.ticks);
323
+ const rpnFine = getControllerChange(midiChannel, midiControllers.RPNLsb, 1, e.ticks);
324
+ const dataEntryCoarse = getControllerChange(
325
+ channel,
326
+ midiControllers.dataEntryMsb,
327
+ centsCoarse,
328
+ e.ticks
329
+ );
330
+ const dataEntryFine = getControllerChange(
331
+ midiChannel,
332
+ midiControllers.lsbForControl6DataEntry,
333
+ 0,
334
+ e.ticks
335
+ );
336
+ addEventBefore(dataEntryFine);
337
+ addEventBefore(dataEntryCoarse);
338
+ addEventBefore(rpnFine);
339
+ addEventBefore(rpnCoarse);
340
+
341
+ }
342
+
343
+ if (channelsToChangeProgram.has(channel))
344
+ {
345
+ const change = desiredProgramChanges.find(c => c.channel === channel);
346
+ let desiredBank = Math.max(0, Math.min(change.bank, 127));
347
+ const desiredProgram = change.program;
348
+ SpessaSynthInfo(
349
+ `%cSetting %c${change.channel}%c to %c${desiredBank}:${desiredProgram}%c. Track num: %c${trackNum}`,
350
+ consoleColors.info,
351
+ consoleColors.recognized,
352
+ consoleColors.info,
353
+ consoleColors.recognized,
354
+ consoleColors.info,
355
+ consoleColors.recognized
356
+ );
357
+
358
+ // note: this is in reverse.
359
+ // the output event order is: drums -> lsb -> msb -> program change
360
+
361
+ // add program change
362
+ const programChange = new MIDIMessage(
363
+ e.ticks,
364
+ messageTypes.programChange | midiChannel,
365
+ new IndexedByteArray([
366
+ desiredProgram
367
+ ])
368
+ );
369
+ addEventBefore(programChange);
370
+
371
+ const addBank = (isLSB, v) =>
372
+ {
373
+ const bankChange = getControllerChange(
374
+ midiChannel,
375
+ isLSB ? midiControllers.lsbForControl0BankSelect : midiControllers.bankSelect,
376
+ v,
377
+ e.ticks
378
+ );
379
+ addEventBefore(bankChange);
380
+ };
381
+
382
+ // on xg, add lsb
383
+ if (isSystemXG(system))
384
+ {
385
+ // xg drums: msb can be 120, 126 or 127
386
+ if (change.isDrum)
387
+ {
388
+ SpessaSynthInfo(
389
+ `%cAdding XG Drum change on track %c${trackNum}`,
390
+ consoleColors.recognized,
391
+ consoleColors.value
392
+ );
393
+ addBank(false, isXGDrums(desiredBank) ? desiredBank : 127);
394
+ addBank(true, 0);
395
+ }
396
+ else
397
+ {
398
+ // sfx voice is set via MSB
399
+ if (desiredBank === XG_SFX_VOICE)
400
+ {
401
+ addBank(false, XG_SFX_VOICE);
402
+ addBank(true, 0);
403
+ }
404
+ else
405
+ {
406
+ // add variation as LSB
407
+ addBank(false, 0);
408
+ addBank(true, desiredBank);
409
+ }
410
+ }
411
+ }
412
+ else
413
+ {
414
+ // add just msb
415
+ addBank(false, desiredBank);
416
+
417
+ if (change.isDrum && midiChannel !== DEFAULT_PERCUSSION)
418
+ {
419
+ // add gs drum change
420
+ SpessaSynthInfo(
421
+ `%cAdding GS Drum change on track %c${trackNum}`,
422
+ consoleColors.recognized,
423
+ consoleColors.value
424
+ );
425
+ addEventBefore(getDrumChange(midiChannel, e.ticks));
426
+ }
427
+ }
428
+ }
429
+ }
430
+ // transpose key (for zero it won't change anyway)
431
+ e.messageData[0] += coarseTranspose[channel];
432
+ break;
433
+
434
+ case messageTypes.noteOff:
435
+ e.messageData[0] += coarseTranspose[channel];
436
+ break;
437
+
438
+ case messageTypes.programChange:
439
+ // do we delete it?
440
+ if (channelsToChangeProgram.has(channel))
441
+ {
442
+ // this channel has program change. BEGONE!
443
+ deleteThisEvent();
444
+ continue;
445
+ }
446
+ break;
447
+
448
+ case messageTypes.controllerChange:
449
+ const ccNum = e.messageData[0];
450
+ const changes = desiredControllerChanges.find(c => c.channel === channel && ccNum === c.controllerNumber);
451
+ if (changes !== undefined)
452
+ {
453
+ // this controller is locked, BEGONE CHANGE!
454
+ deleteThisEvent();
455
+ continue;
456
+ }
457
+ // bank maybe?
458
+ if (ccNum === midiControllers.bankSelect || ccNum === midiControllers.lsbForControl0BankSelect)
459
+ {
460
+ if (channelsToChangeProgram.has(channel))
461
+ {
462
+ // BEGONE!
463
+ deleteThisEvent();
464
+ continue;
465
+ }
466
+ }
467
+ break;
468
+
469
+ case messageTypes.systemExclusive:
470
+ // check for xg on
471
+ if (isXGOn(e))
472
+ {
473
+ SpessaSynthInfo("%cXG system on detected", consoleColors.info);
474
+ system = "xg";
475
+ addedGs = true; // flag as true so gs won't get added
476
+ }
477
+ else
478
+ // check for xg program change
479
+ if (
480
+ e.messageData[0] === 0x43 // yamaha
481
+ && e.messageData[2] === 0x4C // XG
482
+ && e.messageData[3] === 0x08 // part parameter
483
+ && e.messageData[5] === 0x03 // program change
484
+ )
485
+ {
486
+ // do we delete it?
487
+ if (channelsToChangeProgram.has(e.messageData[4] + portOffset))
488
+ {
489
+ // this channel has program change. BEGONE!
490
+ deleteThisEvent();
491
+ }
492
+ }
493
+ else
494
+ // check for GS on
495
+ if (isGSOn(e))
496
+ {
497
+ // that's a GS on, we're done here
498
+ addedGs = true;
499
+ SpessaSynthInfo(
500
+ "%cGS on detected!",
501
+ consoleColors.recognized
502
+ );
503
+ break;
504
+ }
505
+ else
506
+ // check for GM/2 on
507
+ if (isGMOn(e) || isGM2On(e))
508
+ {
509
+ // that's a GM1 system change, remove it!
510
+ SpessaSynthInfo(
511
+ "%cGM/2 on detected, removing!",
512
+ consoleColors.info
513
+ );
514
+ deleteThisEvent();
515
+ addedGs = false;
516
+ }
517
+ }
518
+ }
519
+ // check for gs
520
+ if (!addedGs && desiredProgramChanges.length > 0)
521
+ {
522
+ // gs is not on, add it on the first track at index 0 (or 1 if track name is first)
523
+ let index = 0;
524
+ if (midi.tracks[0][0].messageStatusByte === messageTypes.trackName)
525
+ {
526
+ index++;
527
+ }
528
+ midi.tracks[0].splice(index, 0, getGsOn(0));
529
+ SpessaSynthInfo("%cGS on not detected. Adding it.", consoleColors.info);
530
+ }
531
+ this.flush();
532
+ SpessaSynthGroupEnd();
533
+ }
534
+
535
+ /**
536
+ * Modifies the sequence according to the locked presets and controllers in the given snapshot
537
+ * @this {BasicMIDI}
538
+ * @param snapshot {SynthesizerSnapshot}
539
+ */
540
+ export function applySnapshotToMIDI(snapshot)
541
+ {
542
+ /**
543
+ * @type {{
544
+ * channel: number,
545
+ * keyShift: number
546
+ * }[]}
547
+ */
548
+ const channelsToTranspose = [];
549
+ /**
550
+ * @type {number[]}
551
+ */
552
+ const channelsToClear = [];
553
+ /**
554
+ * @type {{
555
+ * channel: number,
556
+ * program: number,
557
+ * bank: number,
558
+ * isDrum: boolean
559
+ * }[]}
560
+ */
561
+ const programChanges = [];
562
+ /**
563
+ *
564
+ * @type {{
565
+ * channel: number,
566
+ * controllerNumber: number,
567
+ * controllerValue: number
568
+ * }[]}
569
+ */
570
+ const controllerChanges = [];
571
+ snapshot.channelSnapshots.forEach((channel, channelNumber) =>
572
+ {
573
+ if (channel.isMuted)
574
+ {
575
+ channelsToClear.push(channelNumber);
576
+ return;
577
+ }
578
+ const transposeFloat = channel.channelTransposeKeyShift + channel.customControllers[customControllers.channelTransposeFine] / 100;
579
+ if (transposeFloat !== 0)
580
+ {
581
+ channelsToTranspose.push({
582
+ channel: channelNumber,
583
+ keyShift: transposeFloat
584
+ });
585
+ }
586
+ if (channel.lockPreset)
587
+ {
588
+ programChanges.push({
589
+ channel: channelNumber,
590
+ program: channel.program,
591
+ bank: channel.bank,
592
+ isDrum: channel.drumChannel
593
+ });
594
+ }
595
+ // check for locked controllers and change them appropriately
596
+ channel.lockedControllers.forEach((l, ccNumber) =>
597
+ {
598
+ if (!l || ccNumber > 127 || ccNumber === midiControllers.bankSelect)
599
+ {
600
+ return;
601
+ }
602
+ const targetValue = channel.midiControllers[ccNumber] >> 7; // channel controllers are stored as 14 bit values
603
+ controllerChanges.push({
604
+ channel: channelNumber,
605
+ controllerNumber: ccNumber,
606
+ controllerValue: targetValue
607
+ });
608
+ });
609
+ });
610
+ this.modifyMIDI(programChanges, controllerChanges, channelsToClear, channelsToTranspose);
611
+ }
@@ -0,0 +1,99 @@
1
+ import { messageTypes } from "../midi_message.js";
2
+ import { writeVariableLengthQuantity } from "../../utils/byte_functions/variable_length_quantity.js";
3
+ import { writeBytesAsUintBigEndian } from "../../utils/byte_functions/big_endian.js";
4
+
5
+ /**
6
+ * Exports the midi as a standard MIDI file
7
+ * @this {BasicMIDI}
8
+ */
9
+ export function writeMIDI()
10
+ {
11
+ const midi = this;
12
+ if (!midi.tracks)
13
+ {
14
+ throw new Error("MIDI has no tracks!");
15
+ }
16
+ /**
17
+ * @type {Uint8Array[]}
18
+ */
19
+ const binaryTrackData = [];
20
+ for (const track of midi.tracks)
21
+ {
22
+ const binaryTrack = [];
23
+ let currentTick = 0;
24
+ let runningByte = undefined;
25
+ for (const event of track)
26
+ {
27
+ // Ticks stored in MIDI are absolute, but SMF wants relative. Convert them here.
28
+ const deltaTicks = event.ticks - currentTick;
29
+ /**
30
+ * @type {number[]}
31
+ */
32
+ let messageData;
33
+ // determine the message
34
+ if (event.messageStatusByte <= messageTypes.sequenceSpecific)
35
+ {
36
+ // this is a meta-message
37
+ // syntax is FF<type><length><data>
38
+ messageData = [0xff, event.messageStatusByte, ...writeVariableLengthQuantity(event.messageData.length), ...event.messageData];
39
+ }
40
+ else if (event.messageStatusByte === messageTypes.systemExclusive)
41
+ {
42
+ // this is a system exclusive message
43
+ // syntax is F0<length><data>
44
+ messageData = [0xf0, ...writeVariableLengthQuantity(event.messageData.length), ...event.messageData];
45
+ }
46
+ else
47
+ {
48
+ // this is a midi message
49
+ messageData = [];
50
+ if (runningByte !== event.messageStatusByte)
51
+ {
52
+ // Running byte was not the byte we want. Add the byte here.
53
+ runningByte = event.messageStatusByte;
54
+ // add the status byte to the midi
55
+ messageData.push(event.messageStatusByte);
56
+ }
57
+ // add the data
58
+ messageData.push(...event.messageData);
59
+ }
60
+ // write VLQ
61
+ binaryTrack.push(...writeVariableLengthQuantity(deltaTicks));
62
+ // write the message
63
+ binaryTrack.push(...messageData);
64
+ currentTick += deltaTicks;
65
+ }
66
+ binaryTrackData.push(new Uint8Array(binaryTrack));
67
+ }
68
+
69
+ /**
70
+ * @param text {string}
71
+ * @param arr {number[]}
72
+ */
73
+ function writeText(text, arr)
74
+ {
75
+ for (let i = 0; i < text.length; i++)
76
+ {
77
+ arr.push(text.charCodeAt(i));
78
+ }
79
+ }
80
+
81
+ // write the file
82
+ const binaryData = [];
83
+ // write header
84
+ writeText("MThd", binaryData); // MThd
85
+ binaryData.push(...writeBytesAsUintBigEndian(6, 4)); // length
86
+ binaryData.push(0, midi.format); // format
87
+ binaryData.push(...writeBytesAsUintBigEndian(midi.tracksAmount, 2)); // num tracks
88
+ binaryData.push(...writeBytesAsUintBigEndian(midi.timeDivision, 2)); // time division
89
+
90
+ // write tracks
91
+ for (const track of binaryTrackData)
92
+ {
93
+ // write track header
94
+ writeText("MTrk", binaryData); // MTrk
95
+ binaryData.push(...writeBytesAsUintBigEndian(track.length, 4)); // length
96
+ binaryData.push(...track); // write data
97
+ }
98
+ return new Uint8Array(binaryData);
99
+ }