|
|
import { consoleColors } from "../utils/other.js"; |
|
|
import { messageTypes, midiControllers } from "../midi_parser/midi_message.js"; |
|
|
import { EventHandler } from "./synth_event_handler.js"; |
|
|
import { FancyChorus } from "./audio_effects/fancy_chorus.js"; |
|
|
import { getReverbProcessor } from "./audio_effects/reverb.js"; |
|
|
import { |
|
|
ALL_CHANNELS_OR_DIFFERENT_ACTION, |
|
|
masterParameterType, |
|
|
returnMessageType, |
|
|
workletMessageType |
|
|
} from "./worklet_system/message_protocol/worklet_message.js"; |
|
|
import { SpessaSynthInfo, SpessaSynthWarn } from "../utils/loggin.js"; |
|
|
import { DEFAULT_SYNTH_CONFIG } from "./audio_effects/effects_config.js"; |
|
|
import { SoundfontManager } from "./synth_soundfont_manager.js"; |
|
|
import { KeyModifierManager } from "./key_modifier_manager.js"; |
|
|
import { channelConfiguration } from "./worklet_system/worklet_utilities/controller_tables.js"; |
|
|
import { |
|
|
DEFAULT_PERCUSSION, |
|
|
DEFAULT_SYNTH_MODE, |
|
|
MIDI_CHANNEL_COUNT, |
|
|
VOICE_CAP, |
|
|
WORKLET_PROCESSOR_NAME |
|
|
} from "./synth_constants.js"; |
|
|
import { BasicMIDI } from "../midi_parser/basic_midi.js"; |
|
|
import { fillWithDefaults } from "../utils/fill_with_defaults.js"; |
|
|
import { DEFAULT_SEQUENCER_OPTIONS } from "../sequencer/default_sequencer_options.js"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const DEFAULT_SYNTH_METHOD_OPTIONS = { |
|
|
time: 0 |
|
|
}; |
|
|
|
|
|
|
|
|
export class Synthetizer |
|
|
{ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
eventHandler = new EventHandler(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
context; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
targetNode; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_destroyed = false; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_outputsAmount = MIDI_CHANNEL_COUNT; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
channelsAmount = this._outputsAmount; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
channelProperties = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
presetList = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(targetNode, |
|
|
soundFontBuffer, |
|
|
enableEventSystem = true, |
|
|
startRenderingData = undefined, |
|
|
synthConfig = DEFAULT_SYNTH_CONFIG) |
|
|
{ |
|
|
SpessaSynthInfo("%cInitializing SpessaSynth synthesizer...", consoleColors.info); |
|
|
this.context = targetNode.context; |
|
|
this.targetNode = targetNode; |
|
|
|
|
|
|
|
|
enableEventSystem = enableEventSystem ?? true; |
|
|
synthConfig = synthConfig ?? DEFAULT_SYNTH_CONFIG; |
|
|
|
|
|
|
|
|
this._resolveWhenReady = undefined; |
|
|
this.isReady = new Promise(resolve => this._resolveWhenReady = resolve); |
|
|
|
|
|
|
|
|
for (let i = 0; i < this.channelsAmount; i++) |
|
|
{ |
|
|
this.addNewChannel(false); |
|
|
} |
|
|
this.channelProperties[DEFAULT_PERCUSSION].isDrum = true; |
|
|
|
|
|
|
|
|
const oneOutputMode = startRenderingData?.oneOutput ?? false; |
|
|
let processorChannelCount = Array(this._outputsAmount + 2).fill(2); |
|
|
let processorOutputsCount = this._outputsAmount + 2; |
|
|
if (oneOutputMode) |
|
|
{ |
|
|
processorOutputsCount = 1; |
|
|
processorChannelCount = [32]; |
|
|
} |
|
|
|
|
|
|
|
|
this.effectsConfig = fillWithDefaults(synthConfig, DEFAULT_SYNTH_CONFIG); |
|
|
|
|
|
|
|
|
const sequencerRenderingData = {}; |
|
|
if (startRenderingData?.parsedMIDI !== undefined) |
|
|
{ |
|
|
sequencerRenderingData.parsedMIDI = BasicMIDI.copyFrom(startRenderingData.parsedMIDI); |
|
|
if (startRenderingData?.snapshot) |
|
|
{ |
|
|
const snapshot = startRenderingData.snapshot; |
|
|
if (snapshot?.effectsConfig !== undefined) |
|
|
{ |
|
|
|
|
|
this.effectsConfig = fillWithDefaults(snapshot.effectsConfig, DEFAULT_SYNTH_CONFIG); |
|
|
|
|
|
delete snapshot.effectsConfig; |
|
|
} |
|
|
sequencerRenderingData.snapshot = snapshot; |
|
|
} |
|
|
if (startRenderingData?.sequencerOptions) |
|
|
{ |
|
|
|
|
|
sequencerRenderingData.sequencerOptions = fillWithDefaults( |
|
|
startRenderingData.sequencerOptions, |
|
|
DEFAULT_SEQUENCER_OPTIONS |
|
|
); |
|
|
} |
|
|
|
|
|
sequencerRenderingData.loopCount = startRenderingData?.loopCount ?? 0; |
|
|
} |
|
|
|
|
|
|
|
|
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()?"); |
|
|
} |
|
|
|
|
|
|
|
|
this.worklet.port.onmessage = e => this.handleMessage(e.data); |
|
|
this.soundfontManager = new SoundfontManager(this); |
|
|
this.keyModifierManager = new KeyModifierManager(this); |
|
|
this._snapshotCallback = undefined; |
|
|
this.sequencerCallbackFunction = undefined; |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_midiSystem = DEFAULT_SYNTH_MODE; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get midiSystem() |
|
|
{ |
|
|
return this._midiSystem; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
set midiSystem(value) |
|
|
{ |
|
|
this._midiSystem = value; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_voicesAmount = 0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get voicesAmount() |
|
|
{ |
|
|
return this._voicesAmount; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_highPerformanceMode = false; |
|
|
|
|
|
get highPerformanceMode() |
|
|
{ |
|
|
return this._highPerformanceMode; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
set highPerformanceMode(value) |
|
|
{ |
|
|
this._highPerformanceMode = value; |
|
|
this.post({ |
|
|
messageType: workletMessageType.highPerformanceMode, |
|
|
messageData: value |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_voiceCap = VOICE_CAP; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get voiceCap() |
|
|
{ |
|
|
return this._voiceCap; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
set voiceCap(value) |
|
|
{ |
|
|
this._setMasterParam(masterParameterType.voicesCap, value); |
|
|
this._voiceCap = value; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get currentTime() |
|
|
{ |
|
|
return this.context.currentTime; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setLogLevel(enableInfo, enableWarning, enableGroup, enableTable) |
|
|
{ |
|
|
this.post({ |
|
|
channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION, |
|
|
messageType: workletMessageType.setLogLevel, |
|
|
messageData: [enableInfo, enableWarning, enableGroup, enableTable] |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_setMasterParam(type, data) |
|
|
{ |
|
|
this.post({ |
|
|
channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION, |
|
|
messageType: workletMessageType.setMasterParameter, |
|
|
messageData: [type, data] |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setInterpolationType(type) |
|
|
{ |
|
|
this._setMasterParam(masterParameterType.interpolationType, type); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
handleMessage(message) |
|
|
{ |
|
|
const messageData = message.messageData; |
|
|
switch (message.messageType) |
|
|
{ |
|
|
case returnMessageType.channelPropertyChange: |
|
|
|
|
|
|
|
|
|
|
|
const channelNumber = messageData[0]; |
|
|
|
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
|
|
|
|
|
|
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: |
|
|
SpessaSynthWarn(new Error(messageData)); |
|
|
this.eventHandler.callEvent("soundfonterror", messageData); |
|
|
break; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setVibrato(channel, value) |
|
|
{ |
|
|
this.post({ |
|
|
channelNumber: channel, |
|
|
messageType: workletMessageType.setChannelVibrato, |
|
|
messageData: value |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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++) |
|
|
{ |
|
|
|
|
|
this.worklet.connect(audioNodes[outputNumber], outputNumber + 2); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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++) |
|
|
{ |
|
|
|
|
|
this.worklet.disconnect(audioNodes[outputNumber], outputNumber + 2); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
disableGSNRPparams() |
|
|
{ |
|
|
|
|
|
|
|
|
this.setVibrato(ALL_CHANNELS_OR_DIFFERENT_ACTION, { depth: 0, rate: -1, delay: 0 }); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
debugMessage() |
|
|
{ |
|
|
SpessaSynthInfo(this); |
|
|
this.post({ |
|
|
channelNumber: 0, |
|
|
messageType: workletMessageType.debugMessage, |
|
|
messageData: undefined |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sendMessage(message, channelOffset = 0, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS) |
|
|
{ |
|
|
this._sendInternal(message, channelOffset, false, eventOptions); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_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] |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
noteOn(channel, midiNote, velocity, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS) |
|
|
{ |
|
|
const ch = channel % 16; |
|
|
const offset = channel - ch; |
|
|
midiNote %= 128; |
|
|
velocity %= 128; |
|
|
|
|
|
if (eventOptions === true) |
|
|
{ |
|
|
eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS; |
|
|
} |
|
|
this.sendMessage([messageTypes.noteOn | ch, midiNote, velocity], offset, eventOptions); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
stopAll(force = false) |
|
|
{ |
|
|
this.post({ |
|
|
channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION, |
|
|
messageType: workletMessageType.stopAll, |
|
|
messageData: force ? 1 : 0 |
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
const ch = channel % 16; |
|
|
const offset = channel - ch; |
|
|
this._sendInternal( |
|
|
[messageTypes.controllerChange | ch, controllerNumber, controllerValue], |
|
|
offset, |
|
|
force, |
|
|
eventOptions |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
resetControllers() |
|
|
{ |
|
|
this.post({ |
|
|
channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION, |
|
|
messageType: workletMessageType.ccReset, |
|
|
messageData: undefined |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
post(data) |
|
|
{ |
|
|
if (this._destroyed) |
|
|
{ |
|
|
throw new Error("This synthesizer instance has been destroyed!"); |
|
|
} |
|
|
this.worklet.port.postMessage(data); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
transpose(semitones) |
|
|
{ |
|
|
this.transposeChannel(ALL_CHANNELS_OR_DIFFERENT_ACTION, semitones, false); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
transposeChannel(channel, semitones, force = false) |
|
|
{ |
|
|
this.post({ |
|
|
channelNumber: channel, |
|
|
messageType: workletMessageType.transpose, |
|
|
messageData: [semitones, force] |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setMainVolume(volume) |
|
|
{ |
|
|
this._setMasterParam(masterParameterType.mainVolume, volume); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setMasterPan(pan) |
|
|
{ |
|
|
this._setMasterParam(masterParameterType.masterPan, pan); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setPitchBendRange(channel, pitchBendRangeSemitones) |
|
|
{ |
|
|
|
|
|
this.controllerChange(channel, midiControllers.RPNMsb, 0); |
|
|
this.controllerChange(channel, midiControllers.dataEntryMsb, pitchBendRangeSemitones); |
|
|
|
|
|
|
|
|
this.controllerChange(channel, midiControllers.RPNMsb, 127); |
|
|
this.controllerChange(channel, midiControllers.RPNLsb, 127); |
|
|
this.controllerChange(channel, midiControllers.dataEntryMsb, 0); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
programChange(channel, programNumber) |
|
|
{ |
|
|
const ch = channel % 16; |
|
|
const offset = channel - ch; |
|
|
programNumber %= 128; |
|
|
this.sendMessage([messageTypes.programChange | ch, programNumber], offset); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lockController(channel, controllerNumber, isLocked) |
|
|
{ |
|
|
this.post({ |
|
|
channelNumber: channel, |
|
|
messageType: workletMessageType.lockController, |
|
|
messageData: [controllerNumber, isLocked] |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
muteChannel(channel, isMuted) |
|
|
{ |
|
|
this.post({ |
|
|
channelNumber: channel, |
|
|
messageType: workletMessageType.muteChannel, |
|
|
messageData: isMuted |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async reloadSoundFont(soundFontBuffer) |
|
|
{ |
|
|
SpessaSynthWarn("reloadSoundFont is deprecated. Please use the soundfontManager property instead."); |
|
|
await this.soundfontManager.reloadManager(soundFontBuffer); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
systemExclusive(messageData, channelOffset = 0, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS) |
|
|
{ |
|
|
this._sendInternal( |
|
|
[messageTypes.systemExclusive, ...Array.from(messageData)], |
|
|
channelOffset, |
|
|
false, |
|
|
eventOptions |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tuneKeys(program, tunings) |
|
|
{ |
|
|
if (tunings.length > 127) |
|
|
{ |
|
|
throw new Error("Too many tunings. Maximum allowed is 127."); |
|
|
} |
|
|
const systemExclusive = [ |
|
|
0x7F, |
|
|
0x10, |
|
|
0x08, |
|
|
0x02, |
|
|
program, |
|
|
tunings.length |
|
|
]; |
|
|
for (const tuning of tunings) |
|
|
{ |
|
|
systemExclusive.push(tuning.sourceKey); |
|
|
if (tuning.targetPitch === -1) |
|
|
{ |
|
|
|
|
|
systemExclusive.push(0x7F, 0x7F, 0x7F); |
|
|
} |
|
|
else |
|
|
{ |
|
|
const midiNote = Math.floor(tuning.targetPitch); |
|
|
const fraction = Math.floor((tuning.targetPitch - midiNote) / 0.000061); |
|
|
systemExclusive.push( |
|
|
midiNote, |
|
|
(fraction >> 7) & 0x7F, |
|
|
fraction & 0x7F |
|
|
); |
|
|
} |
|
|
} |
|
|
systemExclusive.push(0xF7); |
|
|
this.systemExclusive(systemExclusive); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setDrums(channel, isDrum) |
|
|
{ |
|
|
this.post({ |
|
|
channelNumber: channel, |
|
|
messageType: workletMessageType.setDrums, |
|
|
messageData: isDrum |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setReverbResponse(buffer) |
|
|
{ |
|
|
this.reverbProcessor.buffer = buffer; |
|
|
this.effectsConfig.reverbImpulseResponse = buffer; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setEffectsGain(reverbGain, chorusGain) |
|
|
{ |
|
|
|
|
|
this.post({ |
|
|
messageType: workletMessageType.setEffectsGain, |
|
|
messageData: [reverbGain, chorusGain] |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
destroy() |
|
|
{ |
|
|
this.reverbProcessor.disconnect(); |
|
|
this.chorusProcessor.delete(); |
|
|
|
|
|
this.post({ |
|
|
messageType: workletMessageType.destroyWorklet, |
|
|
messageData: undefined |
|
|
}); |
|
|
this.worklet.disconnect(); |
|
|
delete this.worklet; |
|
|
delete this.reverbProcessor; |
|
|
delete this.chorusProcessor; |
|
|
this._destroyed = true; |
|
|
} |
|
|
|
|
|
|
|
|
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!"; |
|
|
} |
|
|
} |