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,557 @@
1
+ import { messageTypes, midiControllers, MidiMessage } from './midi_message.js'
2
+ import { IndexedByteArray } from '../utils/indexed_array.js'
3
+ import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn } from '../utils/loggin.js'
4
+ import { consoleColors } from '../utils/other.js'
5
+ import { DEFAULT_PERCUSSION } from '../synthetizer/synthetizer.js'
6
+ import { customControllers } from '../synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js'
7
+
8
+ /**
9
+ * @param ticks {number}
10
+ * @returns {MidiMessage}
11
+ */
12
+ export function getGsOn(ticks)
13
+ {
14
+ return new MidiMessage(
15
+ ticks,
16
+ messageTypes.systemExclusive,
17
+ new IndexedByteArray([
18
+ 0x41, // Roland
19
+ 0x10, // Device ID (defaults to 16 on roland)
20
+ 0x42, // GS
21
+ 0x12, // Command ID (DT1) (whatever that means...)
22
+ 0x40, // System parameter }
23
+ 0x00, // Global parameter } Address
24
+ 0x7F, // GS Change }
25
+ 0x00, // turn on } Data
26
+ 0x41, // checksum
27
+ 0xF7, // end of exclusive
28
+ ])
29
+ );
30
+ }
31
+
32
+ function getControllerChange(channel, cc, value, ticks)
33
+ {
34
+ return new MidiMessage(
35
+ ticks,
36
+ messageTypes.controllerChange | (channel % 16),
37
+ new IndexedByteArray([cc, value])
38
+ );
39
+ }
40
+
41
+ /**
42
+ * @param channel {number}
43
+ * @param ticks {number}
44
+ * @returns {MidiMessage}
45
+ */
46
+ function getDrumChange(channel, ticks)
47
+ {
48
+ const chanAddress = 0x10 | [1, 2, 3, 4, 5, 6, 7, 8, 0, 9, 10, 11, 12, 13, 14, 15][channel % 16];
49
+ // excluding manufacturerID DeviceID and ModelID (and F7)
50
+ const sysexData = [
51
+ 0x41, // Roland
52
+ 0x10, // Device ID (defaults to 16 on roland)
53
+ 0x42, // GS
54
+ 0x12, // Command ID (DT1) (whatever that means...)
55
+ 0x40, // System parameter }
56
+ chanAddress, // Channel parameter } Address
57
+ 0x15, // Drum change }
58
+ 0x01, // Is Drums } Data
59
+ ]
60
+ // calculate checksum
61
+ // https://cdn.roland.com/assets/media/pdf/F-20_MIDI_Imple_e01_W.pdf section 4
62
+ const sum = 0x40 + chanAddress + 0x15 + 0x01;
63
+ const checksum = 128 - (sum % 128);
64
+ // add system exclusive to enable drums
65
+ return new MidiMessage(
66
+ ticks,
67
+ messageTypes.systemExclusive,
68
+ new IndexedByteArray([
69
+ ...sysexData,
70
+ checksum,
71
+ 0xF7
72
+ ])
73
+ );
74
+ }
75
+
76
+ /**
77
+ * Allows easy editing of the file
78
+ * @param midi {MIDI}
79
+ * @param desiredProgramChanges {{
80
+ * channel: number,
81
+ * program: number,
82
+ * bank: number,
83
+ * isDrum: boolean
84
+ * }[]} the programs to set on given channels. Note that the channel may be more than 16, function will adjust midi ports automatically
85
+ * @param desiredControllerChanges {{
86
+ * channel: number,
87
+ * controllerNumber: number,
88
+ * controllerValue: number,
89
+ * }[]} the controllers to set on given channels. Note that the channel may be more than 16, function will adjust midi ports automatically
90
+ * @param desiredChannelsToClear {number[]} the channels to remove from the sequence. Note that the channel may be more than 16, function will adjust midi ports automatically
91
+ * @param desiredChannelsToTranspose {{
92
+ * channel: number,
93
+ * keyShift: number
94
+ * }[]} the channels to transpose. if keyShift is float, rpn fine tuning will be applied as well. Note that the channel may be more than 16, function will adjust midi ports automatically
95
+ */
96
+ export function modifyMIDI(
97
+ midi,
98
+ desiredProgramChanges = [],
99
+ desiredControllerChanges = [],
100
+ desiredChannelsToClear = [],
101
+ desiredChannelsToTranspose = []
102
+ )
103
+ {
104
+ SpessaSynthGroupCollapsed("%cApplying changes to the MIDI file...", consoleColors.info);
105
+ /**
106
+ * @param channel {number}
107
+ * @param port {number}
108
+ */
109
+ const clearChannelMessages = (channel, port) => {
110
+ midi.tracks.forEach((track, trackNum) => {
111
+ if(midi.midiPorts[trackNum] !== port)
112
+ {
113
+ return;
114
+ }
115
+ for(let i = track.length - 1; i >= 0; i--) // iterate in reverse to not mess up indexes
116
+ {
117
+ if(track[i].messageStatusByte >= 0x80 && track[i].messageStatusByte < 0xF0) // do not clear sysexes
118
+ {
119
+ if((track[i].messageStatusByte & 0xF) === channel)
120
+ {
121
+ track.splice(i, 1);
122
+ }
123
+ }
124
+ }
125
+ });
126
+ }
127
+ desiredChannelsToClear.forEach(c => {
128
+ const port = Math.floor(c / 16);
129
+ const channel = c % 16;
130
+ clearChannelMessages(channel, port);
131
+ SpessaSynthInfo(`%cRemoving channel %c${c}%c!`,
132
+ consoleColors.info,
133
+ consoleColors.recognized,
134
+ consoleColors.info);
135
+ });
136
+ let addedGs = false;
137
+ let midiSystem = "gs";
138
+ /**
139
+ * find all controller changes in the file
140
+ * @type {{
141
+ * track: number,
142
+ * message: MidiMessage,
143
+ * channel: number
144
+ * }[]}
145
+ */
146
+ const ccChanges = [];
147
+ /**
148
+ * @type {{
149
+ * track: number,
150
+ * message: MidiMessage,
151
+ * channel: number
152
+ * }[]}
153
+ */
154
+ const programChanges = [];
155
+ midi.tracks.forEach((track, trackNum) => {
156
+ track.forEach(message => {
157
+ const status = message.messageStatusByte & 0xF0;
158
+ if(status === messageTypes.controllerChange)
159
+ {
160
+ ccChanges.push({
161
+ track: trackNum,
162
+ message: message,
163
+ channel: message.messageStatusByte & 0xF
164
+ })
165
+ }
166
+ else if(status === messageTypes.programChange)
167
+ {
168
+ programChanges.push({
169
+ track: trackNum,
170
+ message: message,
171
+ channel: message.messageStatusByte & 0xF
172
+ });
173
+ }
174
+ else if(message.messageStatusByte === messageTypes.systemExclusive)
175
+ {
176
+ // check for xg
177
+ if(
178
+ message.messageData[0] === 0x43 && // Yamaha
179
+ message.messageData[2] === 0x4C && // XG ON
180
+ message.messageData[5] === 0x7E &&
181
+ message.messageData[6] === 0x00
182
+ )
183
+ {
184
+ SpessaSynthInfo("%cXG system on detected", consoleColors.info);
185
+ midiSystem = "xg";
186
+ addedGs = true; // flag as true so gs won't get added
187
+ }
188
+ }
189
+ })
190
+ });
191
+
192
+ const getFirstVoiceForChannel = (chan, port) => {
193
+ return midi.tracks
194
+ .reduce((noteOns, track, trackNum) => {
195
+ if(midi.usedChannelsOnTrack[trackNum].has(chan) && midi.midiPorts[trackNum] === port)
196
+ {
197
+ const eventIndex = track.findIndex(event =>
198
+ // event is a voice event
199
+ (event.messageStatusByte > 0x80 && event.messageStatusByte < 0xF0) &&
200
+ // event has the channel we want
201
+ (event.messageStatusByte & 0xF) === chan &&
202
+ // event is not a controller change which resets all controllers or kills all sounds
203
+ (
204
+ (event.messageStatusByte & 0xF0) === messageTypes.controllerChange &&
205
+ event.messageData[0] !== midiControllers.resetAllControllers &&
206
+ event.messageData[0] !== midiControllers.allNotesOff &&
207
+ event.messageData[0] !== midiControllers.allSoundOff
208
+ )
209
+ );
210
+ if(eventIndex !== -1)
211
+ {
212
+ noteOns.push({
213
+ index: eventIndex,
214
+ track: trackNum
215
+ });
216
+ }
217
+ }
218
+ return noteOns;
219
+ }, []);
220
+ }
221
+
222
+
223
+ /**
224
+ * @param channel {number}
225
+ * @param port {number}
226
+ * @param cc {number}
227
+ */
228
+ const clearControllers = (channel, port, cc,) => {
229
+ const thisCcChanges = ccChanges.filter(m =>
230
+ m.channel === channel
231
+ && m.message.messageData[0] === cc
232
+ && midi.midiPorts[m.track] === port);
233
+ // delete
234
+ for(let i = 0; i < thisCcChanges.length; i++)
235
+ {
236
+ // remove
237
+ const e = thisCcChanges[i];
238
+ midi.tracks[e.track].splice(midi.tracks[e.track].indexOf(e.message), 1);
239
+ ccChanges.splice(ccChanges.indexOf(e), 1);
240
+ }
241
+
242
+ }
243
+ desiredControllerChanges.forEach(desiredChange => {
244
+ const channel = desiredChange.channel;
245
+ const midiChannel = channel % 16;
246
+ const port = Math.floor(channel / 16);
247
+ const targetValue = desiredChange.controllerValue;
248
+ const ccNumber = desiredChange.controllerNumber;
249
+ // the controller is locked. Clear all controllers
250
+ clearControllers(midiChannel, port, ccNumber);
251
+ // since we've removed all ccs, we need to add the first one.
252
+ SpessaSynthInfo(`%cNo controller %c${ccNumber}%c on channel %c${channel}%c found. Adding it!`,
253
+ consoleColors.info,
254
+ consoleColors.unrecognized,
255
+ consoleColors.info,
256
+ consoleColors.value,
257
+ consoleColors.info
258
+ );
259
+ /**
260
+ * @type {{index: number, track: number}[]}
261
+ */
262
+ const firstNoteOnForTrack = getFirstVoiceForChannel(midiChannel, port);
263
+ if(firstNoteOnForTrack.length === 0)
264
+ {
265
+ SpessaSynthWarn("Program change but no notes... ignoring!");
266
+ return;
267
+ }
268
+ const firstNoteOn = firstNoteOnForTrack.reduce((first, current) =>
269
+ midi.tracks[current.track][current.index].ticks < midi.tracks[first.track][first.index] ? current : first);
270
+ // prepend with controller change
271
+ const ccChange = getControllerChange(midiChannel, ccNumber, targetValue, midi.tracks[firstNoteOn.track][firstNoteOn.index].ticks);
272
+ midi.tracks[firstNoteOn.track].splice(firstNoteOn.index, 0, ccChange);
273
+ });
274
+
275
+ desiredProgramChanges.forEach(change => {
276
+ const midiChannel = change.channel % 16;
277
+ const port = Math.floor(change.channel / 16);
278
+ let desiredBank = change.isDrum ? 0 : change.bank;
279
+ const desiredProgram = change.program;
280
+
281
+ // get the program changes that are relevant for this channel (and port)
282
+ const thisProgramChanges = programChanges.filter(c => midi.midiPorts[c.track] === port && c.channel === midiChannel);
283
+
284
+
285
+ // clear bank selects
286
+ clearControllers(midiChannel, port, midiControllers.bankSelect);
287
+ clearControllers(midiChannel, port, midiControllers.lsbForControl0BankSelect);
288
+
289
+ // if drums or the program uses bank select, flag as gs
290
+ if((change.isDrum || desiredBank > 0) && !addedGs)
291
+ {
292
+ // make sure that GS is on
293
+ // GS on: F0 41 10 42 12 40 00 7F 00 41 F7
294
+ midi.tracks.forEach(track => {
295
+ for(let eventIndex = 0; eventIndex < track.length; eventIndex++)
296
+ {
297
+ const event = track[eventIndex];
298
+ if(event.messageStatusByte === messageTypes.systemExclusive)
299
+ {
300
+ if(
301
+ event.messageData[0] === 0x41 // roland
302
+ && event.messageData[2] === 0x42 // GS
303
+ && event.messageData[6] === 0x7F // Mode set
304
+ )
305
+ {
306
+ // thats a GS on, we're done here
307
+ addedGs = true;
308
+ SpessaSynthInfo("%cGS on detected!", consoleColors.recognized);
309
+ break;
310
+ }
311
+ else if(
312
+ event.messageData[0] === 0x7E // non realtime
313
+ && event.messageData[2] === 0x09 // gm system
314
+ )
315
+ {
316
+ // thats a GM/2 system change, remove it!
317
+ SpessaSynthInfo("%cGM/2 on detected, removing!", consoleColors.info);
318
+ track.splice(eventIndex, 1);
319
+ // adjust program and bank changes
320
+ eventIndex--;
321
+ }
322
+ }
323
+ }
324
+
325
+ });
326
+ if(!addedGs)
327
+ {
328
+ // gs is not on, add it on the first track at index 0 (or 1 if track name is first)
329
+ let index = 0;
330
+ if(midi.tracks[0][0].messageStatusByte === messageTypes.trackName)
331
+ index++;
332
+ midi.tracks[0].splice(index, 0, getGsOn(0));
333
+ SpessaSynthInfo("%cGS on not detected. Adding it.", consoleColors.info);
334
+ addedGs = true;
335
+ }
336
+ }
337
+
338
+ // remove all program changes
339
+ for(const change of thisProgramChanges)
340
+ {
341
+ midi.tracks[change.track].splice(midi.tracks[change.track].indexOf(change.message), 1);
342
+ }
343
+ /**
344
+ * Find the first voice message
345
+ * @type {{index: number, track: number}[]}
346
+ */
347
+ const firstVoiceForTrack = getFirstVoiceForChannel(midiChannel, port);
348
+ if(firstVoiceForTrack.length === 0)
349
+ {
350
+ SpessaSynthWarn("Program change but no notes... ignoring!");
351
+ return;
352
+ }
353
+ // get the first voice overall
354
+ const firstVoice = firstVoiceForTrack.reduce((first, current) =>
355
+ midi.tracks[current.track][current.index].ticks < midi.tracks[first.track][first.index] ? current : first);
356
+ // get the index and ticks
357
+ let firstIndex = firstVoice.index;
358
+ const ticks = midi.tracks[firstVoice.track][firstVoice.index].ticks;
359
+
360
+ // add drums if needed
361
+ if(change.isDrum)
362
+ {
363
+ // do not add gs drum change on drum channel
364
+ if(midiSystem === "gs" && midiChannel !== DEFAULT_PERCUSSION)
365
+ {
366
+ SpessaSynthInfo(`%cAdding GS Drum change on track %c${firstVoice.track}`,
367
+ consoleColors.recognized,
368
+ consoleColors.value
369
+ );
370
+ midi.tracks[firstVoice.track].splice(firstIndex, 0, getDrumChange(change.channel, ticks));
371
+ firstIndex++;
372
+ }
373
+ else if(midiSystem === "xg")
374
+ {
375
+ SpessaSynthInfo(`%cAdding XG Drum change on track %c${firstVoice.track}`,
376
+ consoleColors.recognized,
377
+ consoleColors.value
378
+ );
379
+ // system is xg. drums are on msb bank 127.
380
+ desiredBank = 127;
381
+ }
382
+ }
383
+
384
+ SpessaSynthInfo(`%cSetting %c${change.channel}%c to %c${desiredBank}:${desiredProgram}`,
385
+ consoleColors.info,
386
+ consoleColors.recognized,
387
+ consoleColors.info,
388
+ consoleColors.recognized);
389
+
390
+ // add bank
391
+ const bankChange = getControllerChange(midiChannel, midiControllers.bankSelect, desiredBank, ticks);
392
+ midi.tracks[firstVoice.track].splice(firstIndex, 0, bankChange);
393
+ firstIndex++;
394
+
395
+ // add program change
396
+ const programChange = new MidiMessage(
397
+ ticks,
398
+ messageTypes.programChange | midiChannel,
399
+ new IndexedByteArray([
400
+ desiredProgram
401
+ ])
402
+ );
403
+ midi.tracks[firstVoice.track].splice(firstIndex, 0, programChange);
404
+
405
+
406
+ });
407
+
408
+ // transpose channels
409
+ for(const transpose of desiredChannelsToTranspose)
410
+ {
411
+ const midiChannel = transpose.channel % 16;
412
+ const port = Math.floor(transpose.channel / 16);
413
+ const keyShift = Math.trunc(transpose.keyShift);
414
+ const fineTune = transpose.keyShift - keyShift;
415
+ SpessaSynthInfo(`%cTransposing channel %c${transpose.channel}%c by %c${transpose.keyShift}%c semitones`,
416
+ consoleColors.info,
417
+ consoleColors.recognized,
418
+ consoleColors.info,
419
+ consoleColors.value,
420
+ consoleColors.info);
421
+ if(keyShift !== 0)
422
+ {
423
+ midi.tracks.forEach((track, trackNum) => {
424
+ if (
425
+ midi.midiPorts[trackNum] !== port ||
426
+ !midi.usedChannelsOnTrack[trackNum].has(midiChannel)
427
+ ) {
428
+ return;
429
+ }
430
+ const onStatus = messageTypes.noteOn | midiChannel;
431
+ const offStatus = messageTypes.noteOff | midiChannel;
432
+ const polyStatus = messageTypes.polyPressure | midiChannel;
433
+ track.forEach(event => {
434
+ if (
435
+ event.messageStatusByte !== onStatus &&
436
+ event.messageStatusByte !== offStatus &&
437
+ event.messageStatusByte !== polyStatus
438
+ ) {
439
+ return;
440
+ }
441
+ event.messageData[0] = Math.max(0, Math.min(127, event.messageData[0] + keyShift));
442
+ })
443
+ });
444
+ }
445
+
446
+ if(fineTune !== 0)
447
+ {
448
+ // find the first track that uses this channel
449
+ const track = midi.tracks.find((t, tNum) => midi.usedChannelsOnTrack[tNum].has(transpose.channel));
450
+ if(track === undefined)
451
+ {
452
+ SpessaSynthWarn(`Channel ${transpose.channel} unused but transpose requested???`);
453
+ continue;
454
+ }
455
+ // find first noteon for this channel
456
+ const noteOn = messageTypes.noteOn | (transpose.channel % 16);
457
+ const noteIndex = track.findIndex(n => n.messageStatusByte === noteOn);
458
+ if(noteIndex === -1)
459
+ {
460
+ SpessaSynthWarn(`No notes on channel ${transpose.channel} but transpose requested???`);
461
+ continue;
462
+ }
463
+ const ticks = track[noteIndex].ticks;
464
+ // add rpn
465
+ // 64 is the center, 96 = 50 cents up
466
+ const centsCoarse = (fineTune * 64) + 64;
467
+ const ccChange = messageTypes.controllerChange | (transpose.channel % 16);
468
+ const rpnCoarse = new MidiMessage(ticks, ccChange, new IndexedByteArray([midiControllers.RPNMsb, 0]));
469
+ const rpnFine = new MidiMessage(ticks, ccChange, new IndexedByteArray([midiControllers.RPNLsb, 1]));
470
+ const deCoarse = new MidiMessage(ticks, ccChange, new IndexedByteArray([midiControllers.dataEntryMsb, centsCoarse]));
471
+ const deFine = new MidiMessage(ticks, ccChange, new IndexedByteArray([midiControllers.lsbForControl6DataEntry, 0]));
472
+ // add in reverse
473
+ track.splice(noteIndex, 0, deFine);
474
+ track.splice(noteIndex, 0, deCoarse);
475
+ track.splice(noteIndex, 0, rpnFine);
476
+ track.splice(noteIndex, 0, rpnCoarse);
477
+
478
+ }
479
+ }
480
+ SpessaSynthGroupEnd();
481
+ }
482
+
483
+ /**
484
+ * Modifies the sequence according to the locked presets and controllers in the given snapshot
485
+ * @param midi {MIDI}
486
+ * @param snapshot {SynthesizerSnapshot}
487
+ */
488
+ export function applySnapshotToMIDI(midi, snapshot)
489
+ {
490
+ /**
491
+ * @type {{
492
+ * channel: number,
493
+ * keyShift: number
494
+ * }[]}
495
+ */
496
+ const channelsToTranspose = [];
497
+ /**
498
+ * @type {number[]}
499
+ */
500
+ const channelsToClear = [];
501
+ /**
502
+ * @type {{
503
+ * channel: number,
504
+ * program: number,
505
+ * bank: number,
506
+ * isDrum: boolean
507
+ * }[]}
508
+ */
509
+ const programChanges = [];
510
+ /**
511
+ *
512
+ * @type {{
513
+ * channel: number,
514
+ * controllerNumber: number,
515
+ * controllerValue: number
516
+ * }[]}
517
+ */
518
+ const controllerChanges = [];
519
+ snapshot.channelSnapshots.forEach((channel, channelNumber) => {
520
+ if(channel.isMuted)
521
+ {
522
+ channelsToClear.push(channelNumber);
523
+ return;
524
+ }
525
+ const transposeFloat = channel.channelTransposeKeyShift + channel.customControllers[customControllers.channelTransposeFine] / 100;
526
+ if(transposeFloat !== 0)
527
+ {
528
+ channelsToTranspose.push({
529
+ channel: channelNumber,
530
+ keyShift: transposeFloat,
531
+ });
532
+ }
533
+ if(channel.lockPreset)
534
+ {
535
+ programChanges.push({
536
+ channel: channelNumber,
537
+ program: channel.program,
538
+ bank: channel.bank,
539
+ isDrum: channel.drumChannel
540
+ });
541
+ }
542
+ // check for locked controllers and change them appropriately
543
+ channel.lockedControllers.forEach((l, ccNumber) => {
544
+ if(!l || ccNumber > 127 || ccNumber === midiControllers.bankSelect)
545
+ {
546
+ return;
547
+ }
548
+ const targetValue = channel.midiControllers[ccNumber] >> 7; // channel controllers are stored as 14 bit values
549
+ controllerChanges.push({
550
+ channel: channelNumber,
551
+ controllerNumber: ccNumber,
552
+ controllerValue: targetValue
553
+ });
554
+ });
555
+ });
556
+ modifyMIDI(midi, programChanges, controllerChanges, channelsToClear, channelsToTranspose);
557
+ }