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,766 @@
1
+ import { IndexedByteArray } from '../utils/indexed_array.js'
2
+ import { consoleColors } from '../utils/other.js'
3
+ import { getEvent, messageTypes, midiControllers } from '../midi_parser/midi_message.js'
4
+ import { EventHandler } from './synth_event_handler.js'
5
+ import { FancyChorus } from './audio_effects/fancy_chorus.js'
6
+ import { getReverbProcessor } from './audio_effects/reverb.js'
7
+ import {
8
+ ALL_CHANNELS_OR_DIFFERENT_ACTION, masterParameterType,
9
+ returnMessageType,
10
+ workletMessageType,
11
+ } from './worklet_system/message_protocol/worklet_message.js'
12
+ import { SpessaSynthInfo, SpessaSynthWarn } from '../utils/loggin.js'
13
+ import { DEFAULT_EFFECTS_CONFIG } from './audio_effects/effects_config.js'
14
+
15
+
16
+ /**
17
+ * synthesizer.js
18
+ * purpose: responds to midi messages and called functions, managing the channels and passing the messages to them
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} StartRenderingDataConfig
23
+ * @property {MIDI} parsedMIDI - the MIDI to render
24
+ * @property {SynthesizerSnapshot} snapshot - the snapshot to apply
25
+ * @property {boolean|undefined} oneOutput - if synth should use one output with 32 channels (2 audio channels for each midi channel). this disables chorus and reverb.
26
+ */
27
+
28
+ export const WORKLET_PROCESSOR_NAME = "spessasynth-worklet-system";
29
+
30
+ export const VOICE_CAP = 450;
31
+
32
+ export const DEFAULT_PERCUSSION = 9;
33
+ export const MIDI_CHANNEL_COUNT = 16;
34
+ export const DEFAULT_SYNTH_MODE = "gs";
35
+
36
+ /**
37
+ * Creates a new instance of the SpessaSynth synthesizer
38
+ * @param targetNode {AudioNode}
39
+ * @param soundFontBuffer {ArrayBuffer} the soundfont file array buffer
40
+ * @param enableEventSystem {boolean} enables the event system. Defaults to true
41
+ * @param startRenderingData {StartRenderingDataConfig} if set, starts playing this immediately and restores the values
42
+ * @param effectsConfig {EffectsConfig} optional configuration for the audio effects.
43
+ */
44
+ export class Synthetizer {
45
+ /**
46
+ * Creates a new instance of the SpessaSynth synthesizer
47
+ * @param targetNode {AudioNode}
48
+ * @param soundFontBuffer {ArrayBuffer} the soundfont file array buffer
49
+ * @param enableEventSystem {boolean} enables the event system. Defaults to true
50
+ * @param startRenderingData {StartRenderingDataConfig} if set, starts playing this immediately and restores the values
51
+ * @param effectsConfig {EffectsConfig} optional configuration for the audio effects.
52
+ */
53
+ constructor(targetNode,
54
+ soundFontBuffer,
55
+ enableEventSystem = true,
56
+ startRenderingData = undefined,
57
+ effectsConfig = DEFAULT_EFFECTS_CONFIG) {
58
+ SpessaSynthInfo("%cInitializing SpessaSynth synthesizer...", consoleColors.info);
59
+ this.context = targetNode.context;
60
+ const oneOutputMode = startRenderingData?.oneOutput === true;
61
+
62
+ /**
63
+ * Allows to set up custom event listeners for the synthesizer
64
+ * @type {EventHandler}
65
+ */
66
+ this.eventHandler = new EventHandler();
67
+
68
+ this._voiceCap = VOICE_CAP;
69
+
70
+ /**
71
+ * the new channels will have their audio sent to the moduled output by this constant.
72
+ * what does that mean? e.g. if outputsAmount is 16, then channel's 16 audio will be sent to channel 0
73
+ * @type {number}
74
+ * @private
75
+ */
76
+ this._outputsAmount = MIDI_CHANNEL_COUNT;
77
+
78
+ /**
79
+ * the amount of midi channels
80
+ * @type {number}
81
+ */
82
+ this.channelsAmount = this._outputsAmount;
83
+
84
+ /**
85
+ * Indicates if the synth is fully ready
86
+ * @type {Promise<void>}
87
+ */
88
+ this.isReady = new Promise(resolve => this._resolveReady = resolve);
89
+
90
+
91
+ /**
92
+ * individual channel voices amount
93
+ * @type {ChannelProperty[]}
94
+ */
95
+ this.channelProperties = [];
96
+ for (let i = 0; i < this.channelsAmount; i++)
97
+ {
98
+ this.addNewChannel(false);
99
+ }
100
+ this.channelProperties[DEFAULT_PERCUSSION].isDrum = true;
101
+ this._voicesAmount = 0;
102
+
103
+ /**
104
+ * For Black MIDI's - forces release time to 50ms
105
+ * @type {boolean}
106
+ */
107
+ this._highPerformanceMode = false;
108
+
109
+ // create a worklet processor
110
+ let processorChannelCount = Array(this._outputsAmount + 2).fill(2);
111
+ let processorOutputsCount = this._outputsAmount + 2;
112
+ if(oneOutputMode)
113
+ {
114
+ processorOutputsCount = 1;
115
+ processorChannelCount = [32];
116
+ }
117
+
118
+ // first two outputs: reverb, chorsu, the others are the channel outputs
119
+ try {
120
+ this.worklet = new AudioWorkletNode(this.context, WORKLET_PROCESSOR_NAME, {
121
+ outputChannelCount: processorChannelCount,
122
+ numberOfOutputs: processorOutputsCount,
123
+ processorOptions: {
124
+ midiChannels: this._outputsAmount,
125
+ soundfont: soundFontBuffer,
126
+ enableEventSystem: enableEventSystem,
127
+ startRenderingData: startRenderingData
128
+ }
129
+ });
130
+ }
131
+ catch (e)
132
+ {
133
+ throw new Error("Could not create the audioWorklet. Did you forget to addModule()?");
134
+ }
135
+
136
+ /**
137
+ * @typedef {Object} PresetListElement
138
+ * @property {string} presetName
139
+ * @property {number} program
140
+ * @property {number} bank
141
+ *
142
+ * used in "presetlistchange" event
143
+ */
144
+
145
+ // worklet sends us some data back
146
+ this.worklet.port.onmessage = e => this.handleMessage(e.data);
147
+
148
+ /**
149
+ * @type {function(SynthesizerSnapshot)}
150
+ * @private
151
+ */
152
+ this._snapshotCallback = undefined;
153
+
154
+ /**
155
+ * for the worklet sequencer's messages
156
+ * @type {function(WorkletSequencerReturnMessageType, any)}
157
+ */
158
+ this.sequencerCallbackFunction = undefined;
159
+
160
+ // add reverb
161
+ if(effectsConfig.reverbEnabled && !oneOutputMode)
162
+ {
163
+ this.reverbProcessor = getReverbProcessor(this.context, effectsConfig.reverbImpulseResponse);
164
+ this.reverbProcessor.connect(targetNode);
165
+ this.worklet.connect(this.reverbProcessor, 0);
166
+ }
167
+
168
+ if(effectsConfig.chorusEnabled && !oneOutputMode)
169
+ {
170
+ this.chorusProcessor = new FancyChorus(targetNode, effectsConfig.chorusConfig);
171
+ this.worklet.connect(this.chorusProcessor.input, 1);
172
+ }
173
+
174
+ if(oneOutputMode)
175
+ {
176
+ // one output mode: one output (duh)
177
+ this.worklet.connect(targetNode, 0);
178
+ }
179
+ else
180
+ {
181
+ // connect all outputs to the output node
182
+ for (let i = 2; i < this.channelsAmount + 2; i++)
183
+ {
184
+ this.worklet.connect(targetNode, i);
185
+ }
186
+ }
187
+
188
+ // attach newchannel to keep track of channels count
189
+ this.eventHandler.addEvent("newchannel", "synth-new-channel", () => {
190
+ this.channelsAmount++;
191
+ });
192
+ }
193
+
194
+ /**
195
+ * The maximum amount of voices allowed at once
196
+ * @returns {number}
197
+ */
198
+ get voiceCap()
199
+ {
200
+ return this._voiceCap;
201
+ }
202
+
203
+ /**
204
+ * The maximum amount of voices allowed at once
205
+ * @param value {number}
206
+ */
207
+ set voiceCap(value)
208
+ {
209
+ this.post({
210
+ messageType: workletMessageType.setMasterParameter,
211
+ messageData: [masterParameterType.voicesCap, value]
212
+ })
213
+ this._voiceCap = value;
214
+ }
215
+
216
+ /**
217
+ * For Black MIDI's - forces release time to 50ms
218
+ * @param {boolean} value
219
+ */
220
+ set highPerformanceMode(value)
221
+ {
222
+ this._highPerformanceMode = value;
223
+
224
+ }
225
+
226
+ get highPerformanceMode()
227
+ {
228
+ return this._highPerformanceMode;
229
+ }
230
+
231
+ /**
232
+ * Sets the SpessaSynth's log level
233
+ * @param enableInfo {boolean} - enable info (verbose)
234
+ * @param enableWarning {boolean} - enable warnings (unrecognized messages)
235
+ * @param enableGroup {boolean} - enable groups (recomended)
236
+ * @param enableTable {boolean} - enable table (debug message)
237
+ */
238
+ setLogLevel(enableInfo, enableWarning, enableGroup, enableTable)
239
+ {
240
+ this.post({
241
+ channelNumber: -1,
242
+ messageType: workletMessageType.setLogLevel,
243
+ messageData: [enableInfo, enableWarning, enableGroup, enableTable]
244
+ });
245
+ }
246
+
247
+ /**
248
+ * Handles the messages received from the worklet
249
+ * @param message {WorkletReturnMessage}
250
+ * @private
251
+ */
252
+ handleMessage(message)
253
+ {
254
+ const messageData = message.messageData;
255
+ switch (message.messageType)
256
+ {
257
+ case returnMessageType.channelProperties:
258
+ /**
259
+ * @type {ChannelProperty[]}
260
+ */
261
+ this.channelProperties = messageData;
262
+
263
+ this._voicesAmount = this.channelProperties.reduce((sum, voices) => sum + voices.voicesAmount, 0);
264
+ break;
265
+
266
+ case returnMessageType.eventCall:
267
+ this.eventHandler.callEvent(messageData.eventName, messageData.eventData);
268
+ break;
269
+
270
+ case returnMessageType.sequencerSpecific:
271
+ if(this.sequencerCallbackFunction)
272
+ {
273
+ this.sequencerCallbackFunction(messageData.messageType, messageData.messageData);
274
+ }
275
+ break;
276
+
277
+ case returnMessageType.synthesizerSnapshot:
278
+ if(this._snapshotCallback)
279
+ {
280
+ this._snapshotCallback(messageData);
281
+ }
282
+ break;
283
+
284
+ case returnMessageType.ready:
285
+ this._resolveReady();
286
+ break;
287
+
288
+ case returnMessageType.soundfontError:
289
+ SpessaSynthWarn(new Error(messageData));
290
+ this.eventHandler.callEvent("soundfonterror", messageData);
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Gets a complete snapshot of the synthesizer, including controllers
296
+ * @returns {Promise<SynthesizerSnapshot>}
297
+ */
298
+ async getSynthesizerSnapshot()
299
+ {
300
+ return new Promise(resolve => {
301
+ this._snapshotCallback = s => {
302
+ this._snapshotCallback = undefined;
303
+ resolve(s);
304
+ };
305
+ this.post({
306
+ messageType: workletMessageType.requestSynthesizerSnapshot,
307
+ messageData: undefined,
308
+ channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION
309
+ });
310
+ });
311
+ }
312
+
313
+ /**
314
+ * Adds a new channel to the synthesizer
315
+ * @param postMessage {boolean} leave at true, set to false only at initialization
316
+ */
317
+ addNewChannel(postMessage = true)
318
+ {
319
+ this.channelProperties.push({
320
+ voicesAmount: 0,
321
+ pitchBend: 0,
322
+ pitchBendRangeSemitones: 0,
323
+ isMuted: false,
324
+ isDrum: false
325
+ });
326
+ if(!postMessage)
327
+ {
328
+ return;
329
+ }
330
+ this.post({
331
+ channelNumber: 0,
332
+ messageType: workletMessageType.addNewChannel,
333
+ messageData: null
334
+ });
335
+ }
336
+
337
+ /**
338
+ * @param channel {number}
339
+ * @param value {{delay: number, depth: number, rate: number}}
340
+ */
341
+ setVibrato(channel, value)
342
+ {
343
+ this.post({
344
+ channelNumber: channel,
345
+ messageType: workletMessageType.setChannelVibrato,
346
+ messageData: value
347
+ });
348
+ }
349
+
350
+ /**
351
+ * Connects the individual audio outputs to the given audio nodes. In the app it's used by the renderer.
352
+ * @param audioNodes {AudioNode[]}
353
+ */
354
+ connectIndividualOutputs(audioNodes)
355
+ {
356
+ if(audioNodes.length !== this._outputsAmount)
357
+ {
358
+ throw new Error(`input nodes amount differs from the system's outputs amount!
359
+ Expected ${this._outputsAmount} got ${audioNodes.length}`);
360
+ }
361
+ for (let outputNumber = 0; outputNumber < this._outputsAmount; outputNumber++) {
362
+ // + 2 because chorus and reverb come first!
363
+ this.worklet.connect(audioNodes[outputNumber], outputNumber + 2);
364
+ }
365
+ }
366
+
367
+ /*
368
+ * Prevents any further changes to the vibrato via NRPN messages and sets it to disabled
369
+ */
370
+ lockAndResetChannelVibrato()
371
+ {
372
+ // rate -1 disables, see worklet_message.js line 9
373
+ // channel -1 is all
374
+ this.setVibrato(ALL_CHANNELS_OR_DIFFERENT_ACTION, {depth: 0, rate: -1, delay: 0});
375
+ }
376
+
377
+ /**
378
+ * A message for debugging
379
+ */
380
+ debugMessage()
381
+ {
382
+ SpessaSynthInfo(this);
383
+ this.post({
384
+ channelNumber: 0,
385
+ messageType: workletMessageType.debugMessage,
386
+ messageData: undefined
387
+ });
388
+ }
389
+
390
+ /**
391
+ * Starts playing a note
392
+ * @param channel {number} usually 0-15: the channel to play the note
393
+ * @param midiNote {number} 0-127 the key number of the note
394
+ * @param velocity {number} 0-127 the velocity of the note (generally controls loudness)
395
+ * @param enableDebugging {boolean} set to true to log technical details to console
396
+ */
397
+ noteOn(channel, midiNote, velocity, enableDebugging = false) {
398
+ this.post({
399
+ channelNumber: channel,
400
+ messageType: workletMessageType.noteOn,
401
+ messageData: [midiNote, velocity, enableDebugging]
402
+ });
403
+ }
404
+
405
+ /**
406
+ * Stops playing a note
407
+ * @param channel {number} usually 0-15: the channel of the note
408
+ * @param midiNote {number} 0-127 the key number of the note
409
+ * @param force {boolean} instantly kills the note if true
410
+ */
411
+ noteOff(channel, midiNote, force = false) {
412
+ if(force)
413
+ {
414
+ this.post({
415
+ channelNumber: channel,
416
+ messageType: workletMessageType.killNote,
417
+ messageData: midiNote
418
+ });
419
+ }
420
+ else {
421
+ this.post({
422
+ channelNumber: channel,
423
+ messageType: workletMessageType.noteOff,
424
+ messageData: midiNote
425
+ });
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Stops all notes
431
+ * @param force {boolean} if we should instantly kill the note, defaults to false
432
+ */
433
+ stopAll(force=false) {
434
+ this.post({
435
+ channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION,
436
+ messageType: workletMessageType.stopAll,
437
+ messageData: force ? 1 : 0
438
+ });
439
+
440
+ }
441
+
442
+ /**
443
+ * Changes the given controller
444
+ * @param channel {number} usually 0-15: the channel to change the controller
445
+ * @param controllerNumber {number} 0-127 the MIDI CC number
446
+ * @param controllerValue {number} 0-127 the controller value
447
+ * @param force {boolean} forces the controller change, even if it's locked or gm system is set and the cc is bank select
448
+ */
449
+ controllerChange(channel, controllerNumber, controllerValue, force=false)
450
+ {
451
+ controllerValue = Math.floor(controllerValue);
452
+ controllerNumber = Math.floor(controllerNumber);
453
+ this.post({
454
+ channelNumber: channel,
455
+ messageType: workletMessageType.ccChange,
456
+ messageData: [controllerNumber, controllerValue, force]
457
+ });
458
+ }
459
+
460
+ /**
461
+ * Resets all controllers (for every channel)
462
+ */
463
+ resetControllers()
464
+ {
465
+ this.post({
466
+ channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION,
467
+ messageType: workletMessageType.ccReset,
468
+ messageData: undefined
469
+ })
470
+ }
471
+
472
+ /**
473
+ * Applies pressure to a given channel
474
+ * @param channel {number} usually 0-15: the channel to change the controller
475
+ * @param pressure {number} 0-127: the pressure to apply
476
+ */
477
+ channelPressure(channel, pressure)
478
+ {
479
+ this.post({
480
+ channelNumber: channel,
481
+ messageType: workletMessageType.channelPressure,
482
+ messageData: pressure
483
+ });
484
+ }
485
+
486
+ /**
487
+ * Applies pressure to a given note
488
+ * @param channel {number} usually 0-15: the channel to change the controller
489
+ * @param midiNote {number} 0-127: the MIDI note
490
+ * @param pressure {number} 0-127: the pressure to apply
491
+ */
492
+ polyPressure(channel, midiNote, pressure)
493
+ {
494
+ this.post({
495
+ channelNumber: channel,
496
+ messageType: workletMessageType.polyPressure,
497
+ messageData: [midiNote, pressure]
498
+ });
499
+ }
500
+
501
+ /**
502
+ * @param data {WorkletMessage}
503
+ */
504
+ post(data)
505
+ {
506
+ this.worklet.port.postMessage(data);
507
+ }
508
+
509
+ /**
510
+ * Sets the pitch of the given channel
511
+ * @param channel {number} usually 0-15: the channel to change pitch
512
+ * @param MSB {number} SECOND byte of the MIDI pitchWheel message
513
+ * @param LSB {number} FIRST byte of the MIDI pitchWheel message
514
+ */
515
+ pitchWheel(channel, MSB, LSB)
516
+ {
517
+ this.post({
518
+ channelNumber: channel,
519
+ messageType: workletMessageType.pitchWheel,
520
+ messageData: [MSB, LSB],
521
+ });
522
+ }
523
+
524
+ /**
525
+ * Transposes the synthetizer's pitch by given semitones amount (percussion channels do not get affected)
526
+ * @param semitones {number} the semitones to transpose by. Can be a floating point number for more precision
527
+ */
528
+ transpose(semitones)
529
+ {
530
+ this.transposeChannel(ALL_CHANNELS_OR_DIFFERENT_ACTION, semitones, false);
531
+ }
532
+
533
+ /**
534
+ * Transposes the channel by given amount of semitones
535
+ * @param channel {number} the channel number
536
+ * @param semitones {number} the transposition of the channel, can be a float
537
+ * @param force {boolean} defaults to false, if true transposes the channel even if it's a drum channel
538
+ */
539
+ transposeChannel(channel, semitones, force=false)
540
+ {
541
+ this.post({
542
+ channelNumber: channel,
543
+ messageType: workletMessageType.transpose,
544
+ messageData: [semitones, force]
545
+ });
546
+ }
547
+
548
+ /**
549
+ * Sets the main volume
550
+ * @param volume {number} 0-1 the volume
551
+ */
552
+ setMainVolume(volume)
553
+ {
554
+ this.post({
555
+ channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION,
556
+ messageType: workletMessageType.setMasterParameter,
557
+ messageData: [masterParameterType.mainVolume, volume]
558
+ });
559
+ }
560
+
561
+ /**
562
+ * Sets the master stereo panning
563
+ * @param pan {number} -1 to 1, the pan (-1 is left, 0 is midde, 1 is right)
564
+ */
565
+ setMasterPan(pan)
566
+ {
567
+ this.post({
568
+ channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION,
569
+ messageType: workletMessageType.setMasterParameter,
570
+ messageData: [masterParameterType.masterPan, pan]
571
+ });
572
+ }
573
+
574
+ /**
575
+ * Sets the channel's pitch bend range, in semitones
576
+ * @param channel {number} usually 0-15: the channel to change
577
+ * @param pitchBendRangeSemitones {number} the bend range in semitones
578
+ */
579
+ setPitchBendRange(channel, pitchBendRangeSemitones)
580
+ {
581
+ // set range
582
+ this.controllerChange(channel, midiControllers.RPNMsb, 0);
583
+ this.controllerChange(channel, midiControllers.dataEntryMsb, pitchBendRangeSemitones);
584
+
585
+ // reset rpn
586
+ this.controllerChange(channel, midiControllers.RPNMsb, 127);
587
+ this.controllerChange(channel, midiControllers.RPNLsb, 127);
588
+ this.controllerChange(channel, midiControllers.dataEntryMsb, 0);
589
+ }
590
+
591
+ /**
592
+ * Changes the patch for a given channel
593
+ * @param channel {number} usually 0-15: the channel to change
594
+ * @param programNumber {number} 0-127 the MIDI patch number
595
+ * @param userChange {boolean} indicates if the program change has been called by user. defaults to false
596
+ */
597
+ programChange(channel, programNumber, userChange=false)
598
+ {
599
+ this.post({
600
+ channelNumber: channel,
601
+ messageType: workletMessageType.programChange,
602
+ messageData: [programNumber, userChange]
603
+ })
604
+ }
605
+
606
+ /**
607
+ * Causes the given midi channel to ignore controller messages for the given controller number
608
+ * @param channel {number} usually 0-15: the channel to lock
609
+ * @param controllerNumber {number} 0-127 MIDI CC number NOTE: -1 locks the preset
610
+ * @param isLocked {boolean} true if locked, false if unlocked
611
+ */
612
+ lockController(channel, controllerNumber, isLocked)
613
+ {
614
+ this.post({
615
+ channelNumber: channel,
616
+ messageType: workletMessageType.lockController,
617
+ messageData: [controllerNumber, isLocked]
618
+ });
619
+ }
620
+
621
+ /**
622
+ * Mutes or unmutes the given channel
623
+ * @param channel {number} usually 0-15: the channel to lock
624
+ * @param isMuted {boolean} indicates if the channel is muted
625
+ */
626
+ muteChannel(channel, isMuted)
627
+ {
628
+ this.post({
629
+ channelNumber: channel,
630
+ messageType: workletMessageType.muteChannel,
631
+ messageData: isMuted
632
+ });
633
+ }
634
+
635
+ /**
636
+ * Reloads the sounfont.
637
+ * @param soundFontBuffer {ArrayBuffer} the new soundfont file array buffer
638
+ * @return {Promise<void>}
639
+ */
640
+ async reloadSoundFont(soundFontBuffer)
641
+ {
642
+ // copy and use transferable
643
+ const bufferCopy = soundFontBuffer.slice(0);
644
+ await new Promise(resolve => {
645
+ this._resolveReady = resolve;
646
+ this.worklet.port.postMessage({
647
+ channelNumber: 0,
648
+ messageType: workletMessageType.reloadSoundFont,
649
+ messageData: bufferCopy
650
+ }, [bufferCopy]);
651
+ });
652
+ }
653
+
654
+ /**
655
+ * Sends a MIDI Sysex message to the synthesizer
656
+ * @param messageData {IndexedByteArray} the message's data (excluding the F0 byte, but including the F7 at the end)
657
+ */
658
+ systemExclusive(messageData)
659
+ {
660
+ this.post({
661
+ channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION,
662
+ messageType: workletMessageType.systemExclusive,
663
+ messageData: Array.from(messageData)
664
+ });
665
+ }
666
+
667
+ /**
668
+ * Toggles drums on a given channel
669
+ * @param channel {number}
670
+ * @param isDrum {boolean}
671
+ */
672
+ setDrums(channel, isDrum)
673
+ {
674
+ this.post({
675
+ channelNumber: channel,
676
+ messageType: workletMessageType.setDrums,
677
+ messageData: isDrum
678
+ });
679
+ }
680
+
681
+ /**
682
+ * sends a raw MIDI message to the synthesizer
683
+ * @param message {ArrayLike<number>} the midi message, each number is a byte
684
+ */
685
+ sendMessage(message)
686
+ {
687
+ // discard as soon as possible if high perf
688
+ const statusByteData = getEvent(message[0]);
689
+
690
+
691
+ // process the event
692
+ switch (statusByteData.status)
693
+ {
694
+ case messageTypes.noteOn:
695
+ const velocity = message[2];
696
+ if(velocity > 0) {
697
+ this.noteOn(statusByteData.channel, message[1], velocity);
698
+ }
699
+ else
700
+ {
701
+ this.noteOff(statusByteData.channel, message[1]);
702
+ }
703
+ break;
704
+
705
+ case messageTypes.noteOff:
706
+ this.noteOff(statusByteData.channel, message[1]);
707
+ break;
708
+
709
+ case messageTypes.pitchBend:
710
+ this.pitchWheel(statusByteData.channel, message[2], message[1]);
711
+ break;
712
+
713
+ case messageTypes.controllerChange:
714
+ this.controllerChange(statusByteData.channel, message[1], message[2]);
715
+ break;
716
+
717
+ case messageTypes.programChange:
718
+ this.programChange(statusByteData.channel, message[1]);
719
+ break;
720
+
721
+ case messageTypes.polyPressure:
722
+ this.polyPressure(statusByteData.channel, message[0], message[1]);
723
+ break;
724
+
725
+ case messageTypes.channelPressure:
726
+ this.channelPressure(statusByteData.channel, message[1]);
727
+ break;
728
+
729
+ case messageTypes.systemExclusive:
730
+ this.systemExclusive(new IndexedByteArray(message.slice(1)));
731
+ break;
732
+
733
+ case messageTypes.reset:
734
+ this.stopAll(true);
735
+ this.resetControllers();
736
+ break;
737
+
738
+ default:
739
+ break;
740
+ }
741
+ }
742
+
743
+ /**
744
+ * @returns {number} the audioContext's current time
745
+ */
746
+ get currentTime()
747
+ {
748
+ return this.context.currentTime;
749
+ }
750
+
751
+ /**
752
+ * @returns {number} the current amount of voices playing
753
+ */
754
+ get voicesAmount()
755
+ {
756
+ return this._voicesAmount;
757
+ }
758
+
759
+ reverbateEverythingBecauseWhyNot()
760
+ {
761
+ for (let i = 0; i < this.channelsAmount; i++) {
762
+ this.controllerChange(i, midiControllers.effects1Depth, 127);
763
+ }
764
+ return "That's the spirit!";
765
+ }
766
+ }