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.
- package/external_midi/README.md +4 -0
- package/external_midi/midi_handler.js +130 -0
- package/external_midi/web_midi_link.js +43 -0
- package/package.json +5 -2
- package/sequencer/README.md +32 -0
- package/sequencer/default_sequencer_options.js +8 -0
- package/sequencer/midi_data.js +63 -0
- package/sequencer/sequencer.js +808 -0
- package/sequencer/sequencer_message.js +53 -0
- package/synthetizer/README.md +38 -0
- package/synthetizer/audio_effects/effects_config.js +25 -0
- package/synthetizer/audio_effects/fancy_chorus.js +162 -0
- package/synthetizer/audio_effects/rb_compressed.min.js +1 -0
- package/synthetizer/audio_effects/reverb.js +35 -0
- package/synthetizer/audio_effects/reverb_as_binary.js +18 -0
- package/synthetizer/key_modifier_manager.js +113 -0
- package/synthetizer/sfman_message.js +9 -0
- package/synthetizer/synth_event_handler.js +217 -0
- package/synthetizer/synth_soundfont_manager.js +112 -0
- package/synthetizer/synthetizer.js +1033 -0
- package/synthetizer/worklet_message.js +120 -0
- package/synthetizer/worklet_processor.js +637 -0
- package/synthetizer/worklet_processor.min.js +22 -0
- package/synthetizer/worklet_processor.min.js.map +7 -0
- package/synthetizer/worklet_url.js +18 -0
- package/utils/buffer_to_wav.js +28 -0
- package/utils/fill_with_defaults.js +21 -0
- package/utils/other.js +11 -0
|
@@ -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
|
+
}
|