UNPKG

spessasynth_lib

Version:

MIDI and SoundFont2/DLS library for the browsers with no compromises

1,033 lines (942 loc) 34.4 kB
import { consoleColors } from "../utils/other.js"; import { ALL_CHANNELS_OR_DIFFERENT_ACTION, BasicMIDI, channelConfiguration, DEFAULT_PERCUSSION, DEFAULT_SYNTH_MODE, interpolationTypes, masterParameterType, messageTypes, MIDI_CHANNEL_COUNT, midiControllers, SpessaSynthCoreUtils as util, SynthesizerSnapshot, VOICE_CAP } from "spessasynth_core"; import { EventHandler } from "./synth_event_handler.js"; import { FancyChorus } from "./audio_effects/fancy_chorus.js"; import { getReverbProcessor } from "./audio_effects/reverb.js"; import { returnMessageType, workletMessageType } from "./worklet_message.js"; import { DEFAULT_SYNTH_CONFIG } from "./audio_effects/effects_config.js"; import { SoundfontManager } from "./synth_soundfont_manager.js"; import { WorkletKeyModifierManagerWrapper } from "./key_modifier_manager.js"; import { fillWithDefaults } from "../utils/fill_with_defaults.js"; import { DEFAULT_SEQUENCER_OPTIONS } from "../sequencer/default_sequencer_options.js"; import { WORKLET_PROCESSOR_NAME } from "./worklet_url.js"; /** * synthesizer.js * purpose: responds to midi messages and called functions, managing the channels and passing the messages to them */ /** * @typedef {Object} SynthMethodOptions * @property {number} time - the audio context time when the event should execute, in seconds. */ /** * @type {SynthMethodOptions} */ const DEFAULT_SYNTH_METHOD_OPTIONS = { time: 0 }; // the "remote controller" of the worklet processor in the audio thread from the main thread // noinspection JSUnusedGlobalSymbols export class Synthetizer { /** * Allows setting up custom event listeners for the synthesizer * @type {EventHandler} */ eventHandler = new EventHandler(); /** * Synthesizer's parent AudioContext instance * @type {BaseAudioContext} */ context; /** * Synthesizer's output node * @type {AudioNode} */ targetNode; /** * @type {boolean} * @private */ _destroyed = false; /** * the new channels will have their audio sent to the modulated output by this constant. * what does that mean? * e.g., if outputsAmount is 16, then channel's 16 audio data will be sent to channel 0 * @type {number} * @private */ _outputsAmount = MIDI_CHANNEL_COUNT; /** * The current number of MIDI channels the synthesizer has * @type {number} */ channelsAmount = this._outputsAmount; /** * Synth's current channel properties * @type {ChannelProperty[]} */ channelProperties = []; /** * The current preset list * @type {{presetName: string, bank: number, program: number}[]} */ presetList = []; /** * Creates a new instance of the SpessaSynth synthesizer. * @param targetNode {AudioNode} * @param soundFontBuffer {ArrayBuffer} the soundfont file array buffer. * @param enableEventSystem {boolean} enables the event system. * Defaults to true. * Disable only when you're rendering audio offline with no actions from the main thread * @param startRenderingData {StartRenderingDataConfig} if it is set, * starts playing this immediately and restores the values. * @param synthConfig {SynthConfig} optional configuration for the synthesizer. */ constructor(targetNode, soundFontBuffer, enableEventSystem = true, startRenderingData = undefined, synthConfig = DEFAULT_SYNTH_CONFIG) { util.SpessaSynthInfo("%cInitializing SpessaSynth synthesizer...", consoleColors.info); this.context = targetNode.context; this.targetNode = targetNode; // ensure default values for options enableEventSystem = enableEventSystem ?? true; synthConfig = synthConfig ?? DEFAULT_SYNTH_CONFIG; // initialize internal promise resolution this._resolveWhenReady = undefined; this.isReady = new Promise(resolve => this._resolveWhenReady = resolve); // create initial channels for (let i = 0; i < this.channelsAmount; i++) { this.addNewChannel(false); } this.channelProperties[DEFAULT_PERCUSSION].isDrum = true; // determine output mode and channel configuration const oneOutputMode = startRenderingData?.oneOutput ?? false; let processorChannelCount = Array(this._outputsAmount + 2).fill(2); let processorOutputsCount = this._outputsAmount + 2; if (oneOutputMode) { processorOutputsCount = 1; processorChannelCount = [32]; } // initialize effects configuration this.effectsConfig = fillWithDefaults(synthConfig, DEFAULT_SYNTH_CONFIG); // process start rendering data const sequencerRenderingData = {}; if (startRenderingData?.parsedMIDI !== undefined) { sequencerRenderingData.parsedMIDI = BasicMIDI.copyFrom(startRenderingData.parsedMIDI); if (startRenderingData?.snapshot) { const snapshot = startRenderingData.snapshot; if (snapshot?.effectsConfig !== undefined) { // overwrite effects configuration with the snapshot this.effectsConfig = fillWithDefaults(snapshot.effectsConfig, DEFAULT_SYNTH_CONFIG); // delete effects config as it cannot be cloned to the worklet (and does not need to be) delete snapshot.effectsConfig; } sequencerRenderingData.snapshot = snapshot; } if (startRenderingData?.sequencerOptions) { // sequencer options sequencerRenderingData.sequencerOptions = fillWithDefaults( startRenderingData.sequencerOptions, DEFAULT_SEQUENCER_OPTIONS ); } sequencerRenderingData.loopCount = startRenderingData?.loopCount ?? 0; } // create the audio worklet node try { let workletConstructor = (synthConfig?.audioNodeCreators?.worklet) ?? ((context, name, options) => { return new AudioWorkletNode(context, name, options); }); this.worklet = workletConstructor(this.context, WORKLET_PROCESSOR_NAME, { outputChannelCount: processorChannelCount, numberOfOutputs: processorOutputsCount, processorOptions: { midiChannels: oneOutputMode ? 1 : this._outputsAmount, soundfont: soundFontBuffer, enableEventSystem: enableEventSystem, startRenderingData: sequencerRenderingData } }); } catch (e) { console.error(e); throw new Error("Could not create the audioWorklet. Did you forget to addModule()?"); } // set up message handling and managers this.worklet.port.onmessage = e => this.handleMessage(e.data); this.soundfontManager = new SoundfontManager(this); this.keyModifierManager = new WorkletKeyModifierManagerWrapper(this); this._snapshotCallback = undefined; this.sequencerCallbackFunction = undefined; // connect worklet outputs if (oneOutputMode) { this.worklet.connect(targetNode, 0); } else { const reverbOn = this.effectsConfig?.reverbEnabled ?? true; const chorusOn = this.effectsConfig?.chorusEnabled ?? true; if (reverbOn) { const proc = getReverbProcessor(this.context, this.effectsConfig.reverbImpulseResponse); this.reverbProcessor = proc.conv; this.isReady = Promise.all([this.isReady, proc.promise]); this.reverbProcessor.connect(targetNode); this.worklet.connect(this.reverbProcessor, 0); } if (chorusOn) { this.chorusProcessor = new FancyChorus(targetNode, this.effectsConfig.chorusConfig); this.worklet.connect(this.chorusProcessor.input, 1); } for (let i = 2; i < this.channelsAmount + 2; i++) { this.worklet.connect(targetNode, i); } } // attach event handlers this.eventHandler.addEvent("newchannel", `synth-new-channel-${Math.random()}`, () => { this.channelsAmount++; }); this.eventHandler.addEvent("presetlistchange", `synth-preset-list-change-${Math.random()}`, e => { this.presetList = e; }); } /** * @type {"gm"|"gm2"|"gs"|"xg"} * @private */ _midiSystem = DEFAULT_SYNTH_MODE; /** * The current MIDI system used by the synthesizer * @returns {"gm"|"gm2"|"gs"|"xg"} */ get midiSystem() { return this._midiSystem; } /** * The current MIDI system used by the synthesizer * @param value {"gm"|"gm2"|"gs"|"xg"} */ set midiSystem(value) { this._midiSystem = value; } /** * current voice amount * @type {number} * @private */ _voicesAmount = 0; /** * @returns {number} the current number of voices playing. */ get voicesAmount() { return this._voicesAmount; } /** * For Black MIDI's - forces release time to 50 ms * @type {boolean} */ _highPerformanceMode = false; get highPerformanceMode() { return this._highPerformanceMode; } /** * For Black MIDI's - forces release time to 50 ms. * @param {boolean} value */ set highPerformanceMode(value) { this._highPerformanceMode = value; this.post({ messageType: workletMessageType.highPerformanceMode, messageData: value }); } /** * @type {number} * @private */ _voiceCap = VOICE_CAP; /** * The maximum number of voices allowed at once. * @returns {number} */ get voiceCap() { return this._voiceCap; } /** * The maximum number of voices allowed at once. * @param value {number} */ set voiceCap(value) { this._setMasterParam(masterParameterType.voicesCap, value); this._voiceCap = value; } /** * @returns {number} the audioContext's current time. */ get currentTime() { return this.context.currentTime; } /** * Sets the SpessaSynth's log level. * @param enableInfo {boolean} - enable info (verbose) * @param enableWarning {boolean} - enable warnings (unrecognized messages) * @param enableGroup {boolean} - enable groups (to group a lot of logs) * @param enableTable {boolean} - enable table (debug message) */ setLogLevel(enableInfo, enableWarning, enableGroup, enableTable) { this.post({ channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION, messageType: workletMessageType.setLogLevel, messageData: [enableInfo, enableWarning, enableGroup, enableTable] }); } /** * @param type {masterParameterType} * @param data {any} * @private */ _setMasterParam(type, data) { this.post({ channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION, messageType: workletMessageType.setMasterParameter, messageData: [type, data] }); } /** * Sets the interpolation type for the synthesizer: * 0. - linear * 1. - nearest neighbor * 2. - cubic * @param type {interpolationTypes} */ setInterpolationType(type) { this._setMasterParam(masterParameterType.interpolationType, type); } /** * Handles the messages received from the worklet. * @param message {WorkletReturnMessage} * @private */ handleMessage(message) { const messageData = message.messageData; switch (message.messageType) { case returnMessageType.channelPropertyChange: /** * @type {number} */ const channelNumber = messageData[0]; /** * @type {ChannelProperty} */ const property = messageData[1]; this.channelProperties[channelNumber] = property; this._voicesAmount = this.channelProperties.reduce((sum, voices) => sum + voices.voicesAmount, 0); break; case returnMessageType.eventCall: this.eventHandler.callEvent(messageData.eventName, messageData.eventData); break; case returnMessageType.sequencerSpecific: if (this.sequencerCallbackFunction) { this.sequencerCallbackFunction(messageData.messageType, messageData.messageData); } break; case returnMessageType.masterParameterChange: /** * @type {masterParameterType} */ const param = messageData[0]; const value = messageData[1]; switch (param) { default: break; case masterParameterType.midiSystem: this._midiSystem = value; break; } break; case returnMessageType.synthesizerSnapshot: if (this._snapshotCallback) { this._snapshotCallback(messageData); } break; case returnMessageType.isFullyInitialized: this._resolveWhenReady(); break; case returnMessageType.soundfontError: util.SpessaSynthWarn(new Error(messageData)); this.eventHandler.callEvent("soundfonterror", messageData); break; } } /** * Gets a complete snapshot of the synthesizer, including controllers. * @returns {Promise<SynthesizerSnapshot>} */ async getSynthesizerSnapshot() { return new Promise(resolve => { this._snapshotCallback = s => { this._snapshotCallback = undefined; s.effectsConfig = this.effectsConfig; resolve(s); }; this.post({ messageType: workletMessageType.requestSynthesizerSnapshot, messageData: undefined, channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION }); }); } /** * Adds a new channel to the synthesizer. * @param postMessage {boolean} leave at true, set to false only at initialization. */ addNewChannel(postMessage = true) { this.channelProperties.push({ voicesAmount: 0, pitchBend: 0, pitchBendRangeSemitones: 0, isMuted: false, isDrum: false, transposition: 0, program: 0, bank: this.channelsAmount % 16 === DEFAULT_PERCUSSION ? 128 : 0 }); if (!postMessage) { return; } this.post({ channelNumber: 0, messageType: workletMessageType.addNewChannel, messageData: null }); } /** * @param channel {number} * @param value {{delay: number, depth: number, rate: number}} */ setVibrato(channel, value) { this.post({ channelNumber: channel, messageType: workletMessageType.setChannelVibrato, messageData: value }); } /** * Connects the individual audio outputs to the given audio nodes. In the app, it's used by the renderer. * @param audioNodes {AudioNode[]} */ connectIndividualOutputs(audioNodes) { if (audioNodes.length !== this._outputsAmount) { throw new Error(`input nodes amount differs from the system's outputs amount! Expected ${this._outputsAmount} got ${audioNodes.length}`); } for (let outputNumber = 0; outputNumber < this._outputsAmount; outputNumber++) { // + 2 because chorus and reverb come first! this.worklet.connect(audioNodes[outputNumber], outputNumber + 2); } } /** * Disconnects the individual audio outputs to the given audio nodes. In the app, it's used by the renderer. * @param audioNodes {AudioNode[]} */ disconnectIndividualOutputs(audioNodes) { if (audioNodes.length !== this._outputsAmount) { throw new Error(`input nodes amount differs from the system's outputs amount! Expected ${this._outputsAmount} got ${audioNodes.length}`); } for (let outputNumber = 0; outputNumber < this._outputsAmount; outputNumber++) { // + 2 because chorus and reverb come first! this.worklet.disconnect(audioNodes[outputNumber], outputNumber + 2); } } /* * Disables the GS NRPN parameters like vibrato or drum key tuning. */ disableGSNRPparams() { // rate -1 disables, see worklet_message.js line 9 // channel -1 is all this.setVibrato(ALL_CHANNELS_OR_DIFFERENT_ACTION, { depth: 0, rate: -1, delay: 0 }); } /** * A message for debugging. */ debugMessage() { util.SpessaSynthInfo(this); this.post({ channelNumber: 0, messageType: workletMessageType.debugMessage, messageData: undefined }); } /** * sends a raw MIDI message to the synthesizer. * @param message {number[]|Uint8Array} the midi message, each number is a byte. * @param channelOffset {number} the channel offset of the message. * @param eventOptions {SynthMethodOptions} additional options for this command. */ sendMessage(message, channelOffset = 0, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS) { this._sendInternal(message, channelOffset, false, eventOptions); } /** * @param message {number[]|Uint8Array} * @param offset {number} * @param force {boolean} * @param eventOptions {SynthMethodOptions} * @private */ _sendInternal(message, offset, force = false, eventOptions) { const opts = fillWithDefaults(eventOptions ?? {}, DEFAULT_SYNTH_METHOD_OPTIONS); this.post({ messageType: workletMessageType.midiMessage, messageData: [new Uint8Array(message), offset, force, opts] }); } /** * Starts playing a note * @param channel {number} usually 0-15: the channel to play the note. * @param midiNote {number} 0-127 the key number of the note. * @param velocity {number} 0-127 the velocity of the note (generally controls loudness). * @param eventOptions {SynthMethodOptions} additional options for this command. */ noteOn(channel, midiNote, velocity, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS) { const ch = channel % 16; const offset = channel - ch; midiNote %= 128; velocity %= 128; // check for legacy "enableDebugging" if (eventOptions === true) { eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS; } this.sendMessage([messageTypes.noteOn | ch, midiNote, velocity], offset, eventOptions); } /** * Stops playing a note. * @param channel {number} usually 0-15: the channel of the note. * @param midiNote {number} 0-127 the key number of the note. * @param force {boolean} instantly kills the note if true. * @param eventOptions {SynthMethodOptions} additional options for this command. */ noteOff(channel, midiNote, force = false, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS) { midiNote %= 128; const ch = channel % 16; const offset = channel - ch; this._sendInternal([messageTypes.noteOff | ch, midiNote], offset, force, eventOptions); } /** * Stops all notes. * @param force {boolean} if we should instantly kill the note, defaults to false. */ stopAll(force = false) { this.post({ channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION, messageType: workletMessageType.stopAll, messageData: force ? 1 : 0 }); } /** * Changes the given controller * @param channel {number} usually 0-15: the channel to change the controller. * @param controllerNumber {number} 0-127 the MIDI CC number. * @param controllerValue {number} 0-127 the controller value. * @param force {boolean} forces the controller-change message, even if it's locked or gm system is set and the cc is bank select. * @param eventOptions {SynthMethodOptions} additional options for this command. */ controllerChange(channel, controllerNumber, controllerValue, force = false, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS) { if (controllerNumber > 127 || controllerNumber < 0) { throw new Error(`Invalid controller number: ${controllerNumber}`); } controllerValue = Math.floor(controllerValue) % 128; controllerNumber = Math.floor(controllerNumber) % 128; // controller change has its own message for the force property const ch = channel % 16; const offset = channel - ch; this._sendInternal( [messageTypes.controllerChange | ch, controllerNumber, controllerValue], offset, force, eventOptions ); } /** * Resets all controllers (for every channel) */ resetControllers() { this.post({ channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION, messageType: workletMessageType.ccReset, messageData: undefined }); } /** * Applies pressure to a given channel. * @param channel {number} usually 0-15: the channel to change the controller. * @param pressure {number} 0-127: the pressure to apply. * @param eventOptions {SynthMethodOptions} additional options for this command. */ channelPressure(channel, pressure, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS) { const ch = channel % 16; const offset = channel - ch; pressure %= 128; this.sendMessage([messageTypes.channelPressure | ch, pressure], offset, eventOptions); } /** * Applies pressure to a given note. * @param channel {number} usually 0-15: the channel to change the controller. * @param midiNote {number} 0-127: the MIDI note. * @param pressure {number} 0-127: the pressure to apply. * @param eventOptions {SynthMethodOptions} additional options for this command. */ polyPressure(channel, midiNote, pressure, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS) { const ch = channel % 16; const offset = channel - ch; midiNote %= 128; pressure %= 128; this.sendMessage([messageTypes.polyPressure | ch, midiNote, pressure], offset, eventOptions); } /** * Sets the pitch of the given channel. * @param channel {number} usually 0-15: the channel to change pitch. * @param MSB {number} SECOND byte of the MIDI pitchWheel message. * @param LSB {number} FIRST byte of the MIDI pitchWheel message. * @param eventOptions {SynthMethodOptions} additional options for this command. */ pitchWheel(channel, MSB, LSB, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS) { const ch = channel % 16; const offset = channel - ch; this.sendMessage([messageTypes.pitchBend | ch, LSB, MSB], offset, eventOptions); } /** * @param data {WorkletMessage} */ post(data) { if (this._destroyed) { throw new Error("This synthesizer instance has been destroyed!"); } this.worklet.port.postMessage(data); } /** * Transposes the synthesizer's pitch by given the semitone amount (percussion channels don’t get affected). * @param semitones {number} the semitones to transpose by. * It can be a floating point number for more precision. */ transpose(semitones) { this.transposeChannel(ALL_CHANNELS_OR_DIFFERENT_ACTION, semitones, false); } /** * Transposes the channel by given number of semitones. * @param channel {number} the channel number. * @param semitones {number} the transposition of the channel, it can be a float. * @param force {boolean} defaults to false, if true transposes the channel even if it's a drum channel. */ transposeChannel(channel, semitones, force = false) { this.post({ channelNumber: channel, messageType: workletMessageType.transpose, messageData: [semitones, force] }); } /** * Sets the main volume. * @param volume {number} 0-1 the volume. */ setMainVolume(volume) { this._setMasterParam(masterParameterType.mainVolume, volume); } /** * Sets the master stereo panning. * @param pan {number} (-1 to 1), the pan (-1 is left, 0 is middle, 1 is right) */ setMasterPan(pan) { this._setMasterParam(masterParameterType.masterPan, pan); } /** * Sets the channel's pitch bend range, in semitones * @param channel {number} usually 0-15: the channel to change * @param pitchBendRangeSemitones {number} the bend range in semitones */ setPitchBendRange(channel, pitchBendRangeSemitones) { // set range this.controllerChange(channel, midiControllers.RPNMsb, 0); this.controllerChange(channel, midiControllers.dataEntryMsb, pitchBendRangeSemitones); // reset rpn this.controllerChange(channel, midiControllers.RPNMsb, 127); this.controllerChange(channel, midiControllers.RPNLsb, 127); this.controllerChange(channel, midiControllers.dataEntryMsb, 0); } /** * Changes the patch for a given channel * @param channel {number} usually 0-15: the channel to change * @param programNumber {number} 0-127 the MIDI patch number * defaults to false */ programChange(channel, programNumber) { const ch = channel % 16; const offset = channel - ch; programNumber %= 128; this.sendMessage([messageTypes.programChange | ch, programNumber], offset); } /** * Overrides velocity on a given channel. * @param channel {number} usually 0-15: the channel to change. * @param velocity {number} 1-127, the velocity to use. * 0 Disables this functionality */ velocityOverride(channel, velocity) { const ch = channel % 16; const offset = channel - ch; this._sendInternal( [messageTypes.controllerChange | ch, channelConfiguration.velocityOverride, velocity], offset, true, DEFAULT_SYNTH_METHOD_OPTIONS ); } /** * Causes the given midi channel to ignore controller messages for the given controller number. * @param channel {number} usually 0-15: the channel to lock. * @param controllerNumber {number} 0-127 MIDI CC number NOTE: -1 locks the preset. * @param isLocked {boolean} true if locked, false if unlocked */ lockController(channel, controllerNumber, isLocked) { this.post({ channelNumber: channel, messageType: workletMessageType.lockController, messageData: [controllerNumber, isLocked] }); } /** * Mutes or unmutes the given channel. * @param channel {number} usually 0-15: the channel to lock. * @param isMuted {boolean} indicates if the channel is muted. */ muteChannel(channel, isMuted) { this.post({ channelNumber: channel, messageType: workletMessageType.muteChannel, messageData: isMuted }); } /** * Reloads the soundfont. * THIS IS DEPRECATED! * USE soundfontManager instead. * @param soundFontBuffer {ArrayBuffer} the new soundfont file array buffer. * @return {Promise<void>} * @deprecated Use the soundfontManager property. */ async reloadSoundFont(soundFontBuffer) { util.SpessaSynthWarn("reloadSoundFont is deprecated. Please use the soundfontManager property instead."); await this.soundfontManager.reloadManager(soundFontBuffer); } /** * Sends a MIDI Sysex message to the synthesizer. * @param messageData {number[]|ArrayLike|Uint8Array} the message's data * (excluding the F0 byte, but including the F7 at the end). * @param channelOffset {number} channel offset for the system exclusive message, defaults to zero. * @param eventOptions {SynthMethodOptions} additional options for this command. */ systemExclusive(messageData, channelOffset = 0, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS) { this._sendInternal( [messageTypes.systemExclusive, ...Array.from(messageData)], channelOffset, false, eventOptions ); } // noinspection JSUnusedGlobalSymbols /** * Tune MIDI keys of a given program using the MIDI Tuning Standard. * @param program {number} 0 - 127 the MIDI program number to use. * @param tunings {{sourceKey: number, targetPitch: number}[]} - the keys and their tunings. * TargetPitch of -1 sets the tuning for this key to be tuned regularly. */ tuneKeys(program, tunings) { if (tunings.length > 127) { throw new Error("Too many tunings. Maximum allowed is 127."); } const systemExclusive = [ 0x7F, // real-time 0x10, // device id 0x08, // MIDI Tuning 0x02, // note change program, // tuning program number tunings.length // number of changes ]; for (const tuning of tunings) { systemExclusive.push(tuning.sourceKey); // [kk] MIDI Key number if (tuning.targetPitch === -1) { // no change systemExclusive.push(0x7F, 0x7F, 0x7F); } else { const midiNote = Math.floor(tuning.targetPitch); const fraction = Math.floor((tuning.targetPitch - midiNote) / 0.000061); systemExclusive.push( midiNote,// frequency data byte 1 (fraction >> 7) & 0x7F, // frequency data byte 2 fraction & 0x7F // frequency data byte 3 ); } } systemExclusive.push(0xF7); this.systemExclusive(systemExclusive); } /** * Toggles drums on a given channel. * @param channel {number} * @param isDrum {boolean} */ setDrums(channel, isDrum) { this.post({ channelNumber: channel, messageType: workletMessageType.setDrums, messageData: isDrum }); } /** * Updates the reverb processor with a new impulse response. * @param buffer {AudioBuffer} the new reverb impulse response. */ setReverbResponse(buffer) { this.reverbProcessor.buffer = buffer; this.effectsConfig.reverbImpulseResponse = buffer; } /** * Updates the chorus processor parameters. * @param config {ChorusConfig} the new chorus. */ setChorusConfig(config) { this.worklet.disconnect(this.chorusProcessor.input); this.chorusProcessor.delete(); delete this.chorusProcessor; this.chorusProcessor = new FancyChorus(this.targetNode, config); this.worklet.connect(this.chorusProcessor.input, 1); this.effectsConfig.chorusConfig = config; } /** * Changes the effects gain. * @param reverbGain {number} the reverb gain, 0-1. * @param chorusGain {number} the chorus gain, 0-1. */ setEffectsGain(reverbGain, chorusGain) { // noinspection JSCheckFunctionSignatures this.post({ messageType: workletMessageType.setEffectsGain, messageData: [reverbGain, chorusGain] }); } /** * Destroys the synthesizer instance. */ destroy() { this.reverbProcessor.disconnect(); this.chorusProcessor.delete(); // noinspection JSCheckFunctionSignatures this.post({ messageType: workletMessageType.destroyWorklet, messageData: undefined }); this.worklet.disconnect(); delete this.worklet; delete this.reverbProcessor; delete this.chorusProcessor; this._destroyed = true; } // noinspection JSUnusedGlobalSymbols reverbateEverythingBecauseWhyNot() { for (let i = 0; i < this.channelsAmount; i++) { this.controllerChange(i, midiControllers.reverbDepth, 127); this.lockController(i, midiControllers.reverbDepth, true); } return "That's the spirit!"; } }