spessasynth_lib 3.26.0 → 3.26.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.
@@ -0,0 +1,1033 @@
1
+ import { consoleColors } from "../utils/other.js";
2
+ import {
3
+ ALL_CHANNELS_OR_DIFFERENT_ACTION,
4
+ BasicMIDI,
5
+ channelConfiguration,
6
+ DEFAULT_PERCUSSION,
7
+ DEFAULT_SYNTH_MODE,
8
+ interpolationTypes,
9
+ masterParameterType,
10
+ messageTypes,
11
+ MIDI_CHANNEL_COUNT,
12
+ midiControllers,
13
+ SpessaSynthCoreUtils as util,
14
+ SynthesizerSnapshot,
15
+ VOICE_CAP
16
+ } from "spessasynth_core";
17
+ import { EventHandler } from "./synth_event_handler.js";
18
+ import { FancyChorus } from "./audio_effects/fancy_chorus.js";
19
+ import { getReverbProcessor } from "./audio_effects/reverb.js";
20
+ import { returnMessageType, workletMessageType } from "./worklet_message.js";
21
+ import { DEFAULT_SYNTH_CONFIG } from "./audio_effects/effects_config.js";
22
+ import { SoundfontManager } from "./synth_soundfont_manager.js";
23
+ import { WorkletKeyModifierManagerWrapper } from "./key_modifier_manager.js";
24
+ import { fillWithDefaults } from "../utils/fill_with_defaults.js";
25
+ import { DEFAULT_SEQUENCER_OPTIONS } from "../sequencer/default_sequencer_options.js";
26
+ import { WORKLET_PROCESSOR_NAME } from "./worklet_url.js";
27
+
28
+
29
+ /**
30
+ * synthesizer.js
31
+ * purpose: responds to midi messages and called functions, managing the channels and passing the messages to them
32
+ */
33
+
34
+ /**
35
+ * @typedef {Object} SynthMethodOptions
36
+ * @property {number} time - the audio context time when the event should execute, in seconds.
37
+ */
38
+
39
+ /**
40
+ * @type {SynthMethodOptions}
41
+ */
42
+ const DEFAULT_SYNTH_METHOD_OPTIONS = {
43
+ time: 0
44
+ };
45
+
46
+
47
+ // the "remote controller" of the worklet processor in the audio thread from the main thread
48
+ // noinspection JSUnusedGlobalSymbols
49
+ export class Synthetizer
50
+ {
51
+
52
+ /**
53
+ * Allows setting up custom event listeners for the synthesizer
54
+ * @type {EventHandler}
55
+ */
56
+ eventHandler = new EventHandler();
57
+
58
+ /**
59
+ * Synthesizer's parent AudioContext instance
60
+ * @type {BaseAudioContext}
61
+ */
62
+ context;
63
+
64
+ /**
65
+ * Synthesizer's output node
66
+ * @type {AudioNode}
67
+ */
68
+ targetNode;
69
+ /**
70
+ * @type {boolean}
71
+ * @private
72
+ */
73
+ _destroyed = false;
74
+
75
+ /**
76
+ * the new channels will have their audio sent to the modulated output by this constant.
77
+ * what does that mean?
78
+ * e.g., if outputsAmount is 16, then channel's 16 audio data will be sent to channel 0
79
+ * @type {number}
80
+ * @private
81
+ */
82
+ _outputsAmount = MIDI_CHANNEL_COUNT;
83
+
84
+ /**
85
+ * The current number of MIDI channels the synthesizer has
86
+ * @type {number}
87
+ */
88
+ channelsAmount = this._outputsAmount;
89
+
90
+ /**
91
+ * Synth's current channel properties
92
+ * @type {ChannelProperty[]}
93
+ */
94
+ channelProperties = [];
95
+
96
+ /**
97
+ * The current preset list
98
+ * @type {{presetName: string, bank: number, program: number}[]}
99
+ */
100
+ presetList = [];
101
+
102
+ /**
103
+ * Creates a new instance of the SpessaSynth synthesizer.
104
+ * @param targetNode {AudioNode}
105
+ * @param soundFontBuffer {ArrayBuffer} the soundfont file array buffer.
106
+ * @param enableEventSystem {boolean} enables the event system.
107
+ * Defaults to true.
108
+ * Disable only when you're rendering audio offline with no actions from the main thread
109
+ * @param startRenderingData {StartRenderingDataConfig} if it is set,
110
+ * starts playing this immediately and restores the values.
111
+ * @param synthConfig {SynthConfig} optional configuration for the synthesizer.
112
+ */
113
+ constructor(targetNode,
114
+ soundFontBuffer,
115
+ enableEventSystem = true,
116
+ startRenderingData = undefined,
117
+ synthConfig = DEFAULT_SYNTH_CONFIG)
118
+ {
119
+ util.SpessaSynthInfo("%cInitializing SpessaSynth synthesizer...", consoleColors.info);
120
+ this.context = targetNode.context;
121
+ this.targetNode = targetNode;
122
+
123
+ // ensure default values for options
124
+ enableEventSystem = enableEventSystem ?? true;
125
+ synthConfig = synthConfig ?? DEFAULT_SYNTH_CONFIG;
126
+
127
+ // initialize internal promise resolution
128
+ this._resolveWhenReady = undefined;
129
+ this.isReady = new Promise(resolve => this._resolveWhenReady = resolve);
130
+
131
+ // create initial channels
132
+ for (let i = 0; i < this.channelsAmount; i++)
133
+ {
134
+ this.addNewChannel(false);
135
+ }
136
+ this.channelProperties[DEFAULT_PERCUSSION].isDrum = true;
137
+
138
+ // determine output mode and channel configuration
139
+ const oneOutputMode = startRenderingData?.oneOutput ?? false;
140
+ let processorChannelCount = Array(this._outputsAmount + 2).fill(2);
141
+ let processorOutputsCount = this._outputsAmount + 2;
142
+ if (oneOutputMode)
143
+ {
144
+ processorOutputsCount = 1;
145
+ processorChannelCount = [32];
146
+ }
147
+
148
+ // initialize effects configuration
149
+ this.effectsConfig = fillWithDefaults(synthConfig, DEFAULT_SYNTH_CONFIG);
150
+
151
+ // process start rendering data
152
+ const sequencerRenderingData = {};
153
+ if (startRenderingData?.parsedMIDI !== undefined)
154
+ {
155
+ sequencerRenderingData.parsedMIDI = BasicMIDI.copyFrom(startRenderingData.parsedMIDI);
156
+ if (startRenderingData?.snapshot)
157
+ {
158
+ const snapshot = startRenderingData.snapshot;
159
+ if (snapshot?.effectsConfig !== undefined)
160
+ {
161
+ // overwrite effects configuration with the snapshot
162
+ this.effectsConfig = fillWithDefaults(snapshot.effectsConfig, DEFAULT_SYNTH_CONFIG);
163
+ // delete effects config as it cannot be cloned to the worklet (and does not need to be)
164
+ delete snapshot.effectsConfig;
165
+ }
166
+ sequencerRenderingData.snapshot = snapshot;
167
+ }
168
+ if (startRenderingData?.sequencerOptions)
169
+ {
170
+ // sequencer options
171
+ sequencerRenderingData.sequencerOptions = fillWithDefaults(
172
+ startRenderingData.sequencerOptions,
173
+ DEFAULT_SEQUENCER_OPTIONS
174
+ );
175
+ }
176
+
177
+ sequencerRenderingData.loopCount = startRenderingData?.loopCount ?? 0;
178
+ }
179
+
180
+ // create the audio worklet node
181
+ try
182
+ {
183
+ let workletConstructor = (synthConfig?.audioNodeCreators?.worklet) ??
184
+ ((context, name, options) =>
185
+ {
186
+ return new AudioWorkletNode(context, name, options);
187
+ });
188
+ this.worklet = workletConstructor(this.context, WORKLET_PROCESSOR_NAME, {
189
+ outputChannelCount: processorChannelCount,
190
+ numberOfOutputs: processorOutputsCount,
191
+ processorOptions: {
192
+ midiChannels: oneOutputMode ? 1 : this._outputsAmount,
193
+ soundfont: soundFontBuffer,
194
+ enableEventSystem: enableEventSystem,
195
+ startRenderingData: sequencerRenderingData
196
+ }
197
+ });
198
+ }
199
+ catch (e)
200
+ {
201
+ console.error(e);
202
+ throw new Error("Could not create the audioWorklet. Did you forget to addModule()?");
203
+ }
204
+
205
+ // set up message handling and managers
206
+ this.worklet.port.onmessage = e => this.handleMessage(e.data);
207
+ this.soundfontManager = new SoundfontManager(this);
208
+ this.keyModifierManager = new WorkletKeyModifierManagerWrapper(this);
209
+ this._snapshotCallback = undefined;
210
+ this.sequencerCallbackFunction = undefined;
211
+
212
+ // connect worklet outputs
213
+ if (oneOutputMode)
214
+ {
215
+ this.worklet.connect(targetNode, 0);
216
+ }
217
+ else
218
+ {
219
+ const reverbOn = this.effectsConfig?.reverbEnabled ?? true;
220
+ const chorusOn = this.effectsConfig?.chorusEnabled ?? true;
221
+ if (reverbOn)
222
+ {
223
+ const proc = getReverbProcessor(this.context, this.effectsConfig.reverbImpulseResponse);
224
+ this.reverbProcessor = proc.conv;
225
+ this.isReady = Promise.all([this.isReady, proc.promise]);
226
+ this.reverbProcessor.connect(targetNode);
227
+ this.worklet.connect(this.reverbProcessor, 0);
228
+ }
229
+ if (chorusOn)
230
+ {
231
+ this.chorusProcessor = new FancyChorus(targetNode, this.effectsConfig.chorusConfig);
232
+ this.worklet.connect(this.chorusProcessor.input, 1);
233
+ }
234
+ for (let i = 2; i < this.channelsAmount + 2; i++)
235
+ {
236
+ this.worklet.connect(targetNode, i);
237
+ }
238
+ }
239
+
240
+ // attach event handlers
241
+ this.eventHandler.addEvent("newchannel", `synth-new-channel-${Math.random()}`, () =>
242
+ {
243
+ this.channelsAmount++;
244
+ });
245
+ this.eventHandler.addEvent("presetlistchange", `synth-preset-list-change-${Math.random()}`, e =>
246
+ {
247
+ this.presetList = e;
248
+ });
249
+ }
250
+
251
+
252
+ /**
253
+ * @type {"gm"|"gm2"|"gs"|"xg"}
254
+ * @private
255
+ */
256
+ _midiSystem = DEFAULT_SYNTH_MODE;
257
+
258
+ /**
259
+ * The current MIDI system used by the synthesizer
260
+ * @returns {"gm"|"gm2"|"gs"|"xg"}
261
+ */
262
+ get midiSystem()
263
+ {
264
+ return this._midiSystem;
265
+ }
266
+
267
+ /**
268
+ * The current MIDI system used by the synthesizer
269
+ * @param value {"gm"|"gm2"|"gs"|"xg"}
270
+ */
271
+ set midiSystem(value)
272
+ {
273
+ this._midiSystem = value;
274
+ }
275
+
276
+ /**
277
+ * current voice amount
278
+ * @type {number}
279
+ * @private
280
+ */
281
+ _voicesAmount = 0;
282
+
283
+ /**
284
+ * @returns {number} the current number of voices playing.
285
+ */
286
+ get voicesAmount()
287
+ {
288
+ return this._voicesAmount;
289
+ }
290
+
291
+ /**
292
+ * For Black MIDI's - forces release time to 50 ms
293
+ * @type {boolean}
294
+ */
295
+ _highPerformanceMode = false;
296
+
297
+ get highPerformanceMode()
298
+ {
299
+ return this._highPerformanceMode;
300
+ }
301
+
302
+ /**
303
+ * For Black MIDI's - forces release time to 50 ms.
304
+ * @param {boolean} value
305
+ */
306
+ set highPerformanceMode(value)
307
+ {
308
+ this._highPerformanceMode = value;
309
+ this.post({
310
+ messageType: workletMessageType.highPerformanceMode,
311
+ messageData: value
312
+ });
313
+ }
314
+
315
+ /**
316
+ * @type {number}
317
+ * @private
318
+ */
319
+ _voiceCap = VOICE_CAP;
320
+
321
+ /**
322
+ * The maximum number of voices allowed at once.
323
+ * @returns {number}
324
+ */
325
+ get voiceCap()
326
+ {
327
+ return this._voiceCap;
328
+ }
329
+
330
+ /**
331
+ * The maximum number of voices allowed at once.
332
+ * @param value {number}
333
+ */
334
+ set voiceCap(value)
335
+ {
336
+ this._setMasterParam(masterParameterType.voicesCap, value);
337
+ this._voiceCap = value;
338
+ }
339
+
340
+ /**
341
+ * @returns {number} the audioContext's current time.
342
+ */
343
+ get currentTime()
344
+ {
345
+ return this.context.currentTime;
346
+ }
347
+
348
+ /**
349
+ * Sets the SpessaSynth's log level.
350
+ * @param enableInfo {boolean} - enable info (verbose)
351
+ * @param enableWarning {boolean} - enable warnings (unrecognized messages)
352
+ * @param enableGroup {boolean} - enable groups (to group a lot of logs)
353
+ * @param enableTable {boolean} - enable table (debug message)
354
+ */
355
+ setLogLevel(enableInfo, enableWarning, enableGroup, enableTable)
356
+ {
357
+ this.post({
358
+ channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION,
359
+ messageType: workletMessageType.setLogLevel,
360
+ messageData: [enableInfo, enableWarning, enableGroup, enableTable]
361
+ });
362
+ }
363
+
364
+ /**
365
+ * @param type {masterParameterType}
366
+ * @param data {any}
367
+ * @private
368
+ */
369
+ _setMasterParam(type, data)
370
+ {
371
+ this.post({
372
+ channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION,
373
+ messageType: workletMessageType.setMasterParameter,
374
+ messageData: [type, data]
375
+ });
376
+ }
377
+
378
+ /**
379
+ * Sets the interpolation type for the synthesizer:
380
+ * 0. - linear
381
+ * 1. - nearest neighbor
382
+ * 2. - cubic
383
+ * @param type {interpolationTypes}
384
+ */
385
+ setInterpolationType(type)
386
+ {
387
+ this._setMasterParam(masterParameterType.interpolationType, type);
388
+ }
389
+
390
+ /**
391
+ * Handles the messages received from the worklet.
392
+ * @param message {WorkletReturnMessage}
393
+ * @private
394
+ */
395
+ handleMessage(message)
396
+ {
397
+ const messageData = message.messageData;
398
+ switch (message.messageType)
399
+ {
400
+ case returnMessageType.channelPropertyChange:
401
+ /**
402
+ * @type {number}
403
+ */
404
+ const channelNumber = messageData[0];
405
+ /**
406
+ * @type {ChannelProperty}
407
+ */
408
+ const property = messageData[1];
409
+
410
+ this.channelProperties[channelNumber] = property;
411
+
412
+ this._voicesAmount = this.channelProperties.reduce((sum, voices) => sum + voices.voicesAmount, 0);
413
+ break;
414
+
415
+ case returnMessageType.eventCall:
416
+ this.eventHandler.callEvent(messageData.eventName, messageData.eventData);
417
+ break;
418
+
419
+ case returnMessageType.sequencerSpecific:
420
+ if (this.sequencerCallbackFunction)
421
+ {
422
+ this.sequencerCallbackFunction(messageData.messageType, messageData.messageData);
423
+ }
424
+ break;
425
+
426
+ case returnMessageType.masterParameterChange:
427
+ /**
428
+ * @type {masterParameterType}
429
+ */
430
+ const param = messageData[0];
431
+ const value = messageData[1];
432
+ switch (param)
433
+ {
434
+ default:
435
+ break;
436
+
437
+ case masterParameterType.midiSystem:
438
+ this._midiSystem = value;
439
+ break;
440
+ }
441
+ break;
442
+
443
+ case returnMessageType.synthesizerSnapshot:
444
+ if (this._snapshotCallback)
445
+ {
446
+ this._snapshotCallback(messageData);
447
+ }
448
+ break;
449
+
450
+ case returnMessageType.isFullyInitialized:
451
+ this._resolveWhenReady();
452
+ break;
453
+
454
+ case returnMessageType.soundfontError:
455
+ util.SpessaSynthWarn(new Error(messageData));
456
+ this.eventHandler.callEvent("soundfonterror", messageData);
457
+ break;
458
+ }
459
+ }
460
+
461
+ /**
462
+ * Gets a complete snapshot of the synthesizer, including controllers.
463
+ * @returns {Promise<SynthesizerSnapshot>}
464
+ */
465
+ async getSynthesizerSnapshot()
466
+ {
467
+ return new Promise(resolve =>
468
+ {
469
+ this._snapshotCallback = s =>
470
+ {
471
+ this._snapshotCallback = undefined;
472
+ s.effectsConfig = this.effectsConfig;
473
+ resolve(s);
474
+ };
475
+ this.post({
476
+ messageType: workletMessageType.requestSynthesizerSnapshot,
477
+ messageData: undefined,
478
+ channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION
479
+ });
480
+ });
481
+ }
482
+
483
+ /**
484
+ * Adds a new channel to the synthesizer.
485
+ * @param postMessage {boolean} leave at true, set to false only at initialization.
486
+ */
487
+ addNewChannel(postMessage = true)
488
+ {
489
+ this.channelProperties.push({
490
+ voicesAmount: 0,
491
+ pitchBend: 0,
492
+ pitchBendRangeSemitones: 0,
493
+ isMuted: false,
494
+ isDrum: false,
495
+ transposition: 0,
496
+ program: 0,
497
+ bank: this.channelsAmount % 16 === DEFAULT_PERCUSSION ? 128 : 0
498
+ });
499
+ if (!postMessage)
500
+ {
501
+ return;
502
+ }
503
+ this.post({
504
+ channelNumber: 0,
505
+ messageType: workletMessageType.addNewChannel,
506
+ messageData: null
507
+ });
508
+ }
509
+
510
+ /**
511
+ * @param channel {number}
512
+ * @param value {{delay: number, depth: number, rate: number}}
513
+ */
514
+ setVibrato(channel, value)
515
+ {
516
+ this.post({
517
+ channelNumber: channel,
518
+ messageType: workletMessageType.setChannelVibrato,
519
+ messageData: value
520
+ });
521
+ }
522
+
523
+ /**
524
+ * Connects the individual audio outputs to the given audio nodes. In the app, it's used by the renderer.
525
+ * @param audioNodes {AudioNode[]}
526
+ */
527
+ connectIndividualOutputs(audioNodes)
528
+ {
529
+ if (audioNodes.length !== this._outputsAmount)
530
+ {
531
+ throw new Error(`input nodes amount differs from the system's outputs amount!
532
+ Expected ${this._outputsAmount} got ${audioNodes.length}`);
533
+ }
534
+ for (let outputNumber = 0; outputNumber < this._outputsAmount; outputNumber++)
535
+ {
536
+ // + 2 because chorus and reverb come first!
537
+ this.worklet.connect(audioNodes[outputNumber], outputNumber + 2);
538
+ }
539
+ }
540
+
541
+ /**
542
+ * Disconnects the individual audio outputs to the given audio nodes. In the app, it's used by the renderer.
543
+ * @param audioNodes {AudioNode[]}
544
+ */
545
+ disconnectIndividualOutputs(audioNodes)
546
+ {
547
+ if (audioNodes.length !== this._outputsAmount)
548
+ {
549
+ throw new Error(`input nodes amount differs from the system's outputs amount!
550
+ Expected ${this._outputsAmount} got ${audioNodes.length}`);
551
+ }
552
+ for (let outputNumber = 0; outputNumber < this._outputsAmount; outputNumber++)
553
+ {
554
+ // + 2 because chorus and reverb come first!
555
+ this.worklet.disconnect(audioNodes[outputNumber], outputNumber + 2);
556
+ }
557
+ }
558
+
559
+ /*
560
+ * Disables the GS NRPN parameters like vibrato or drum key tuning.
561
+ */
562
+ disableGSNRPparams()
563
+ {
564
+ // rate -1 disables, see worklet_message.js line 9
565
+ // channel -1 is all
566
+ this.setVibrato(ALL_CHANNELS_OR_DIFFERENT_ACTION, { depth: 0, rate: -1, delay: 0 });
567
+ }
568
+
569
+ /**
570
+ * A message for debugging.
571
+ */
572
+ debugMessage()
573
+ {
574
+ util.SpessaSynthInfo(this);
575
+ this.post({
576
+ channelNumber: 0,
577
+ messageType: workletMessageType.debugMessage,
578
+ messageData: undefined
579
+ });
580
+ }
581
+
582
+ /**
583
+ * sends a raw MIDI message to the synthesizer.
584
+ * @param message {number[]|Uint8Array} the midi message, each number is a byte.
585
+ * @param channelOffset {number} the channel offset of the message.
586
+ * @param eventOptions {SynthMethodOptions} additional options for this command.
587
+ */
588
+ sendMessage(message, channelOffset = 0, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS)
589
+ {
590
+ this._sendInternal(message, channelOffset, false, eventOptions);
591
+ }
592
+
593
+ /**
594
+ * @param message {number[]|Uint8Array}
595
+ * @param offset {number}
596
+ * @param force {boolean}
597
+ * @param eventOptions {SynthMethodOptions}
598
+ * @private
599
+ */
600
+ _sendInternal(message, offset, force = false, eventOptions)
601
+ {
602
+ const opts = fillWithDefaults(eventOptions ?? {}, DEFAULT_SYNTH_METHOD_OPTIONS);
603
+ this.post({
604
+ messageType: workletMessageType.midiMessage,
605
+ messageData: [new Uint8Array(message), offset, force, opts]
606
+ });
607
+ }
608
+
609
+
610
+ /**
611
+ * Starts playing a note
612
+ * @param channel {number} usually 0-15: the channel to play the note.
613
+ * @param midiNote {number} 0-127 the key number of the note.
614
+ * @param velocity {number} 0-127 the velocity of the note (generally controls loudness).
615
+ * @param eventOptions {SynthMethodOptions} additional options for this command.
616
+ */
617
+ noteOn(channel, midiNote, velocity, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS)
618
+ {
619
+ const ch = channel % 16;
620
+ const offset = channel - ch;
621
+ midiNote %= 128;
622
+ velocity %= 128;
623
+ // check for legacy "enableDebugging"
624
+ if (eventOptions === true)
625
+ {
626
+ eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS;
627
+ }
628
+ this.sendMessage([messageTypes.noteOn | ch, midiNote, velocity], offset, eventOptions);
629
+ }
630
+
631
+ /**
632
+ * Stops playing a note.
633
+ * @param channel {number} usually 0-15: the channel of the note.
634
+ * @param midiNote {number} 0-127 the key number of the note.
635
+ * @param force {boolean} instantly kills the note if true.
636
+ * @param eventOptions {SynthMethodOptions} additional options for this command.
637
+ */
638
+ noteOff(channel, midiNote, force = false, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS)
639
+ {
640
+ midiNote %= 128;
641
+
642
+ const ch = channel % 16;
643
+ const offset = channel - ch;
644
+ this._sendInternal([messageTypes.noteOff | ch, midiNote], offset, force, eventOptions);
645
+ }
646
+
647
+ /**
648
+ * Stops all notes.
649
+ * @param force {boolean} if we should instantly kill the note, defaults to false.
650
+ */
651
+ stopAll(force = false)
652
+ {
653
+ this.post({
654
+ channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION,
655
+ messageType: workletMessageType.stopAll,
656
+ messageData: force ? 1 : 0
657
+ });
658
+
659
+ }
660
+
661
+ /**
662
+ * Changes the given controller
663
+ * @param channel {number} usually 0-15: the channel to change the controller.
664
+ * @param controllerNumber {number} 0-127 the MIDI CC number.
665
+ * @param controllerValue {number} 0-127 the controller value.
666
+ * @param force {boolean} forces the controller-change message, even if it's locked or gm system is set and the cc is bank select.
667
+ * @param eventOptions {SynthMethodOptions} additional options for this command.
668
+ */
669
+ controllerChange(channel, controllerNumber, controllerValue, force = false, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS)
670
+ {
671
+ if (controllerNumber > 127 || controllerNumber < 0)
672
+ {
673
+ throw new Error(`Invalid controller number: ${controllerNumber}`);
674
+ }
675
+ controllerValue = Math.floor(controllerValue) % 128;
676
+ controllerNumber = Math.floor(controllerNumber) % 128;
677
+ // controller change has its own message for the force property
678
+ const ch = channel % 16;
679
+ const offset = channel - ch;
680
+ this._sendInternal(
681
+ [messageTypes.controllerChange | ch, controllerNumber, controllerValue],
682
+ offset,
683
+ force,
684
+ eventOptions
685
+ );
686
+ }
687
+
688
+ /**
689
+ * Resets all controllers (for every channel)
690
+ */
691
+ resetControllers()
692
+ {
693
+ this.post({
694
+ channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION,
695
+ messageType: workletMessageType.ccReset,
696
+ messageData: undefined
697
+ });
698
+ }
699
+
700
+ /**
701
+ * Applies pressure to a given channel.
702
+ * @param channel {number} usually 0-15: the channel to change the controller.
703
+ * @param pressure {number} 0-127: the pressure to apply.
704
+ * @param eventOptions {SynthMethodOptions} additional options for this command.
705
+ */
706
+ channelPressure(channel, pressure, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS)
707
+ {
708
+ const ch = channel % 16;
709
+ const offset = channel - ch;
710
+ pressure %= 128;
711
+ this.sendMessage([messageTypes.channelPressure | ch, pressure], offset, eventOptions);
712
+ }
713
+
714
+ /**
715
+ * Applies pressure to a given note.
716
+ * @param channel {number} usually 0-15: the channel to change the controller.
717
+ * @param midiNote {number} 0-127: the MIDI note.
718
+ * @param pressure {number} 0-127: the pressure to apply.
719
+ * @param eventOptions {SynthMethodOptions} additional options for this command.
720
+ */
721
+ polyPressure(channel, midiNote, pressure, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS)
722
+ {
723
+ const ch = channel % 16;
724
+ const offset = channel - ch;
725
+ midiNote %= 128;
726
+ pressure %= 128;
727
+ this.sendMessage([messageTypes.polyPressure | ch, midiNote, pressure], offset, eventOptions);
728
+ }
729
+
730
+ /**
731
+ * Sets the pitch of the given channel.
732
+ * @param channel {number} usually 0-15: the channel to change pitch.
733
+ * @param MSB {number} SECOND byte of the MIDI pitchWheel message.
734
+ * @param LSB {number} FIRST byte of the MIDI pitchWheel message.
735
+ * @param eventOptions {SynthMethodOptions} additional options for this command.
736
+ */
737
+ pitchWheel(channel, MSB, LSB, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS)
738
+ {
739
+ const ch = channel % 16;
740
+ const offset = channel - ch;
741
+ this.sendMessage([messageTypes.pitchBend | ch, LSB, MSB], offset, eventOptions);
742
+ }
743
+
744
+ /**
745
+ * @param data {WorkletMessage}
746
+ */
747
+ post(data)
748
+ {
749
+ if (this._destroyed)
750
+ {
751
+ throw new Error("This synthesizer instance has been destroyed!");
752
+ }
753
+ this.worklet.port.postMessage(data);
754
+ }
755
+
756
+ /**
757
+ * Transposes the synthesizer's pitch by given the semitone amount (percussion channels don’t get affected).
758
+ * @param semitones {number} the semitones to transpose by.
759
+ * It can be a floating point number for more precision.
760
+ */
761
+ transpose(semitones)
762
+ {
763
+ this.transposeChannel(ALL_CHANNELS_OR_DIFFERENT_ACTION, semitones, false);
764
+ }
765
+
766
+ /**
767
+ * Transposes the channel by given number of semitones.
768
+ * @param channel {number} the channel number.
769
+ * @param semitones {number} the transposition of the channel, it can be a float.
770
+ * @param force {boolean} defaults to false, if true transposes the channel even if it's a drum channel.
771
+ */
772
+ transposeChannel(channel, semitones, force = false)
773
+ {
774
+ this.post({
775
+ channelNumber: channel,
776
+ messageType: workletMessageType.transpose,
777
+ messageData: [semitones, force]
778
+ });
779
+ }
780
+
781
+ /**
782
+ * Sets the main volume.
783
+ * @param volume {number} 0-1 the volume.
784
+ */
785
+ setMainVolume(volume)
786
+ {
787
+ this._setMasterParam(masterParameterType.mainVolume, volume);
788
+ }
789
+
790
+ /**
791
+ * Sets the master stereo panning.
792
+ * @param pan {number} (-1 to 1), the pan (-1 is left, 0 is middle, 1 is right)
793
+ */
794
+ setMasterPan(pan)
795
+ {
796
+ this._setMasterParam(masterParameterType.masterPan, pan);
797
+ }
798
+
799
+ /**
800
+ * Sets the channel's pitch bend range, in semitones
801
+ * @param channel {number} usually 0-15: the channel to change
802
+ * @param pitchBendRangeSemitones {number} the bend range in semitones
803
+ */
804
+ setPitchBendRange(channel, pitchBendRangeSemitones)
805
+ {
806
+ // set range
807
+ this.controllerChange(channel, midiControllers.RPNMsb, 0);
808
+ this.controllerChange(channel, midiControllers.dataEntryMsb, pitchBendRangeSemitones);
809
+
810
+ // reset rpn
811
+ this.controllerChange(channel, midiControllers.RPNMsb, 127);
812
+ this.controllerChange(channel, midiControllers.RPNLsb, 127);
813
+ this.controllerChange(channel, midiControllers.dataEntryMsb, 0);
814
+ }
815
+
816
+ /**
817
+ * Changes the patch for a given channel
818
+ * @param channel {number} usually 0-15: the channel to change
819
+ * @param programNumber {number} 0-127 the MIDI patch number
820
+ * defaults to false
821
+ */
822
+ programChange(channel, programNumber)
823
+ {
824
+ const ch = channel % 16;
825
+ const offset = channel - ch;
826
+ programNumber %= 128;
827
+ this.sendMessage([messageTypes.programChange | ch, programNumber], offset);
828
+ }
829
+
830
+ /**
831
+ * Overrides velocity on a given channel.
832
+ * @param channel {number} usually 0-15: the channel to change.
833
+ * @param velocity {number} 1-127, the velocity to use.
834
+ * 0 Disables this functionality
835
+ */
836
+ velocityOverride(channel, velocity)
837
+ {
838
+ const ch = channel % 16;
839
+ const offset = channel - ch;
840
+ this._sendInternal(
841
+ [messageTypes.controllerChange | ch, channelConfiguration.velocityOverride, velocity],
842
+ offset,
843
+ true,
844
+ DEFAULT_SYNTH_METHOD_OPTIONS
845
+ );
846
+ }
847
+
848
+ /**
849
+ * Causes the given midi channel to ignore controller messages for the given controller number.
850
+ * @param channel {number} usually 0-15: the channel to lock.
851
+ * @param controllerNumber {number} 0-127 MIDI CC number NOTE: -1 locks the preset.
852
+ * @param isLocked {boolean} true if locked, false if unlocked
853
+ */
854
+ lockController(channel, controllerNumber, isLocked)
855
+ {
856
+ this.post({
857
+ channelNumber: channel,
858
+ messageType: workletMessageType.lockController,
859
+ messageData: [controllerNumber, isLocked]
860
+ });
861
+ }
862
+
863
+ /**
864
+ * Mutes or unmutes the given channel.
865
+ * @param channel {number} usually 0-15: the channel to lock.
866
+ * @param isMuted {boolean} indicates if the channel is muted.
867
+ */
868
+ muteChannel(channel, isMuted)
869
+ {
870
+ this.post({
871
+ channelNumber: channel,
872
+ messageType: workletMessageType.muteChannel,
873
+ messageData: isMuted
874
+ });
875
+ }
876
+
877
+ /**
878
+ * Reloads the sounfont.
879
+ * THIS IS DEPRECATED!
880
+ * USE soundfontManager instead.
881
+ * @param soundFontBuffer {ArrayBuffer} the new soundfont file array buffer.
882
+ * @return {Promise<void>}
883
+ * @deprecated Use the soundfontManager property.
884
+ */
885
+ async reloadSoundFont(soundFontBuffer)
886
+ {
887
+ util.SpessaSynthWarn("reloadSoundFont is deprecated. Please use the soundfontManager property instead.");
888
+ await this.soundfontManager.reloadManager(soundFontBuffer);
889
+ }
890
+
891
+ /**
892
+ * Sends a MIDI Sysex message to the synthesizer.
893
+ * @param messageData {number[]|ArrayLike|Uint8Array} the message's data
894
+ * (excluding the F0 byte, but including the F7 at the end).
895
+ * @param channelOffset {number} channel offset for the system exclusive message, defaults to zero.
896
+ * @param eventOptions {SynthMethodOptions} additional options for this command.
897
+ */
898
+ systemExclusive(messageData, channelOffset = 0, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS)
899
+ {
900
+ this._sendInternal(
901
+ [messageTypes.systemExclusive, ...Array.from(messageData)],
902
+ channelOffset,
903
+ false,
904
+ eventOptions
905
+ );
906
+ }
907
+
908
+ // noinspection JSUnusedGlobalSymbols
909
+ /**
910
+ * Tune MIDI keys of a given program using the MIDI Tuning Standard.
911
+ * @param program {number} 0 - 127 the MIDI program number to use.
912
+ * @param tunings {{sourceKey: number, targetPitch: number}[]} - the keys and their tunings.
913
+ * TargetPitch of -1 sets the tuning for this key to be tuned regularly.
914
+ */
915
+ tuneKeys(program, tunings)
916
+ {
917
+ if (tunings.length > 127)
918
+ {
919
+ throw new Error("Too many tunings. Maximum allowed is 127.");
920
+ }
921
+ const systemExclusive = [
922
+ 0x7F, // real-time
923
+ 0x10, // device id
924
+ 0x08, // MIDI Tuning
925
+ 0x02, // note change
926
+ program, // tuning program number
927
+ tunings.length // number of changes
928
+ ];
929
+ for (const tuning of tunings)
930
+ {
931
+ systemExclusive.push(tuning.sourceKey); // [kk] MIDI Key number
932
+ if (tuning.targetPitch === -1)
933
+ {
934
+ // no change
935
+ systemExclusive.push(0x7F, 0x7F, 0x7F);
936
+ }
937
+ else
938
+ {
939
+ const midiNote = Math.floor(tuning.targetPitch);
940
+ const fraction = Math.floor((tuning.targetPitch - midiNote) / 0.000061);
941
+ systemExclusive.push(
942
+ midiNote,// frequency data byte 1
943
+ (fraction >> 7) & 0x7F, // frequency data byte 2
944
+ fraction & 0x7F // frequency data byte 3
945
+ );
946
+ }
947
+ }
948
+ systemExclusive.push(0xF7);
949
+ this.systemExclusive(systemExclusive);
950
+ }
951
+
952
+ /**
953
+ * Toggles drums on a given channel.
954
+ * @param channel {number}
955
+ * @param isDrum {boolean}
956
+ */
957
+ setDrums(channel, isDrum)
958
+ {
959
+ this.post({
960
+ channelNumber: channel,
961
+ messageType: workletMessageType.setDrums,
962
+ messageData: isDrum
963
+ });
964
+ }
965
+
966
+ /**
967
+ * Updates the reverb processor with a new impulse response.
968
+ * @param buffer {AudioBuffer} the new reverb impulse response.
969
+ */
970
+ setReverbResponse(buffer)
971
+ {
972
+ this.reverbProcessor.buffer = buffer;
973
+ this.effectsConfig.reverbImpulseResponse = buffer;
974
+ }
975
+
976
+ /**
977
+ * Updates the chorus processor parameters.
978
+ * @param config {ChorusConfig} the new chorus.
979
+ */
980
+ setChorusConfig(config)
981
+ {
982
+ this.worklet.disconnect(this.chorusProcessor.input);
983
+ this.chorusProcessor.delete();
984
+ delete this.chorusProcessor;
985
+ this.chorusProcessor = new FancyChorus(this.targetNode, config);
986
+ this.worklet.connect(this.chorusProcessor.input, 1);
987
+ this.effectsConfig.chorusConfig = config;
988
+ }
989
+
990
+ /**
991
+ * Changes the effects gain.
992
+ * @param reverbGain {number} the reverb gain, 0-1.
993
+ * @param chorusGain {number} the chorus gain, 0-1.
994
+ */
995
+ setEffectsGain(reverbGain, chorusGain)
996
+ {
997
+ // noinspection JSCheckFunctionSignatures
998
+ this.post({
999
+ messageType: workletMessageType.setEffectsGain,
1000
+ messageData: [reverbGain, chorusGain]
1001
+ });
1002
+ }
1003
+
1004
+ /**
1005
+ * Destroys the synthesizer instance.
1006
+ */
1007
+ destroy()
1008
+ {
1009
+ this.reverbProcessor.disconnect();
1010
+ this.chorusProcessor.delete();
1011
+ // noinspection JSCheckFunctionSignatures
1012
+ this.post({
1013
+ messageType: workletMessageType.destroyWorklet,
1014
+ messageData: undefined
1015
+ });
1016
+ this.worklet.disconnect();
1017
+ delete this.worklet;
1018
+ delete this.reverbProcessor;
1019
+ delete this.chorusProcessor;
1020
+ this._destroyed = true;
1021
+ }
1022
+
1023
+ // noinspection JSUnusedGlobalSymbols
1024
+ reverbateEverythingBecauseWhyNot()
1025
+ {
1026
+ for (let i = 0; i < this.channelsAmount; i++)
1027
+ {
1028
+ this.controllerChange(i, midiControllers.reverbDepth, 127);
1029
+ this.lockController(i, midiControllers.reverbDepth, true);
1030
+ }
1031
+ return "That's the spirit!";
1032
+ }
1033
+ }