|
|
import { WorkletSequencer } from "../../sequencer/worklet_sequencer/worklet_sequencer.js"; |
|
|
import { SpessaSynthInfo } from "../../utils/loggin.js"; |
|
|
import { consoleColors } from "../../utils/other.js"; |
|
|
import { voiceKilling } from "./worklet_methods/stopping_notes/voice_killing.js"; |
|
|
import { |
|
|
ALL_CHANNELS_OR_DIFFERENT_ACTION, |
|
|
masterParameterType, |
|
|
returnMessageType |
|
|
} from "./message_protocol/worklet_message.js"; |
|
|
import { stbvorbis } from "../../externals/stbvorbis_sync/stbvorbis_sync.min.js"; |
|
|
import { VOLUME_ENVELOPE_SMOOTHING_FACTOR } from "./worklet_utilities/volume_envelope.js"; |
|
|
import { handleMessage } from "./message_protocol/handle_message.js"; |
|
|
import { callEvent } from "./message_protocol/message_sending.js"; |
|
|
import { systemExclusive } from "./worklet_methods/system_exclusive.js"; |
|
|
import { setMasterGain, setMasterPan, setMIDIVolume } from "./worklet_methods/controller_control/master_parameters.js"; |
|
|
import { resetAllControllers } from "./worklet_methods/controller_control/reset_controllers.js"; |
|
|
import { WorkletSoundfontManager } from "./worklet_methods/worklet_soundfont_manager/worklet_soundfont_manager.js"; |
|
|
import { interpolationTypes } from "./worklet_utilities/wavetable_oscillator.js"; |
|
|
import { WorkletKeyModifierManager } from "./worklet_methods/worklet_key_modifier.js"; |
|
|
import { getWorkletVoices } from "./worklet_utilities/worklet_voice.js"; |
|
|
import { PAN_SMOOTHING_FACTOR } from "./worklet_utilities/stereo_panner.js"; |
|
|
import { stopAllChannels } from "./worklet_methods/stopping_notes/stop_all_channels.js"; |
|
|
import { setEmbeddedSoundFont } from "./worklet_methods/soundfont_management/set_embedded_sound_font.js"; |
|
|
import { reloadSoundFont } from "./worklet_methods/soundfont_management/reload_sound_font.js"; |
|
|
import { clearSoundFont } from "./worklet_methods/soundfont_management/clear_sound_font.js"; |
|
|
import { sendPresetList } from "./worklet_methods/soundfont_management/send_preset_list.js"; |
|
|
import { getPreset } from "./worklet_methods/soundfont_management/get_preset.js"; |
|
|
import { transposeAllChannels } from "./worklet_methods/tuning_control/transpose_all_channels.js"; |
|
|
import { setMasterTuning } from "./worklet_methods/tuning_control/set_master_tuning.js"; |
|
|
import { sendSynthesizerSnapshot } from "./snapshot/send_synthesizer_snapshot.js"; |
|
|
import { applySynthesizerSnapshot } from "./snapshot/apply_synthesizer_snapshot.js"; |
|
|
import { createWorkletChannel } from "./worklet_methods/create_worklet_channel.js"; |
|
|
import { FILTER_SMOOTHING_FACTOR } from "./worklet_utilities/lowpass_filter.js"; |
|
|
import { DEFAULT_PERCUSSION, DEFAULT_SYNTH_MODE, VOICE_CAP } from "../synth_constants.js"; |
|
|
import { fillWithDefaults } from "../../utils/fill_with_defaults.js"; |
|
|
import { DEFAULT_SEQUENCER_OPTIONS } from "../../sequencer/default_sequencer_options.js"; |
|
|
import { getEvent, messageTypes } from "../../midi_parser/midi_message.js"; |
|
|
import { IndexedByteArray } from "../../utils/indexed_array.js"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const MIN_NOTE_LENGTH = 0.03; |
|
|
|
|
|
export const MIN_EXCLUSIVE_LENGTH = 0.07; |
|
|
|
|
|
export const SYNTHESIZER_GAIN = 1.0; |
|
|
|
|
|
|
|
|
|
|
|
class SpessaSynthProcessor extends AudioWorkletProcessor |
|
|
{ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cachedVoices = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alive = true; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
deviceID = ALL_CHANNELS_OR_DIFFERENT_ACTION; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
eventQueue = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
interpolationType = interpolationTypes.fourthOrder; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sequencer = new WorkletSequencer(this); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
transposition = 0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tunings = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
soundfontBankOffset = 0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
masterGain = SYNTHESIZER_GAIN; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
midiVolume = 1; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
reverbGain = 1; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
chorusGain = 1; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
voiceCap = VOICE_CAP; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pan = 0.0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
panLeft = 0.5; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
panRight = 0.5; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
highPerformanceMode = false; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
keyModifierManager = new WorkletKeyModifierManager(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
overrideSoundfont = undefined; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
workletProcessorChannels = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
system = DEFAULT_SYNTH_MODE; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
totalVoicesAmount = 0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
defaultPreset; |
|
|
|
|
|
defaultPresetUsesOverride = false; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
drumPreset; |
|
|
|
|
|
defaultDrumsUsesOverride = false; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
processorInitialized = stbvorbis.isInitialized; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(options) |
|
|
{ |
|
|
super(); |
|
|
this.midiOutputsCount = options.processorOptions.midiChannels; |
|
|
let initialChannelCount = this.midiOutputsCount; |
|
|
this.oneOutputMode = this.midiOutputsCount === 1; |
|
|
if (this.oneOutputMode) |
|
|
{ |
|
|
initialChannelCount = 16; |
|
|
} |
|
|
|
|
|
this.enableEventSystem = options.processorOptions.enableEventSystem; |
|
|
|
|
|
|
|
|
for (let i = 0; i < 127; i++) |
|
|
{ |
|
|
this.tunings.push([]); |
|
|
} |
|
|
|
|
|
try |
|
|
{ |
|
|
|
|
|
|
|
|
|
|
|
this.soundfontManager = new WorkletSoundfontManager( |
|
|
options.processorOptions.soundfont, |
|
|
this.postReady.bind(this) |
|
|
); |
|
|
} |
|
|
catch (e) |
|
|
{ |
|
|
this.post({ |
|
|
messageType: returnMessageType.soundfontError, |
|
|
messageData: e |
|
|
}); |
|
|
throw e; |
|
|
} |
|
|
this.sendPresetList(); |
|
|
|
|
|
this.getDefaultPresets(); |
|
|
|
|
|
|
|
|
for (let i = 0; i < initialChannelCount; i++) |
|
|
{ |
|
|
this.createWorkletChannel(false); |
|
|
} |
|
|
|
|
|
this.workletProcessorChannels[DEFAULT_PERCUSSION].preset = this.drumPreset; |
|
|
this.workletProcessorChannels[DEFAULT_PERCUSSION].drumChannel = true; |
|
|
|
|
|
|
|
|
this.volumeEnvelopeSmoothingFactor = VOLUME_ENVELOPE_SMOOTHING_FACTOR * (44100 / sampleRate); |
|
|
this.panSmoothingFactor = PAN_SMOOTHING_FACTOR * (44100 / sampleRate); |
|
|
this.filterSmoothingFactor = FILTER_SMOOTHING_FACTOR * (44100 / sampleRate); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this._snapshot = options.processorOptions?.startRenderingData?.snapshot; |
|
|
|
|
|
this.port.onmessage = e => this.handleMessage(e.data); |
|
|
|
|
|
|
|
|
if (options.processorOptions.startRenderingData) |
|
|
{ |
|
|
if (this._snapshot !== undefined) |
|
|
{ |
|
|
this.applySynthesizerSnapshot(this._snapshot); |
|
|
this.resetAllControllers(); |
|
|
} |
|
|
|
|
|
SpessaSynthInfo("%cRendering enabled! Starting render.", consoleColors.info); |
|
|
if (options.processorOptions.startRenderingData.parsedMIDI) |
|
|
{ |
|
|
if (options.processorOptions.startRenderingData?.loopCount !== undefined) |
|
|
{ |
|
|
this.sequencer.loopCount = options.processorOptions.startRenderingData?.loopCount; |
|
|
this.sequencer.loop = true; |
|
|
} |
|
|
else |
|
|
{ |
|
|
this.sequencer.loop = false; |
|
|
} |
|
|
|
|
|
this.voiceCap = Infinity; |
|
|
this.processorInitialized.then(() => |
|
|
{ |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const seqOptions = fillWithDefaults( |
|
|
options.processorOptions.startRenderingData.sequencerOptions, |
|
|
DEFAULT_SEQUENCER_OPTIONS |
|
|
); |
|
|
this.sequencer.skipToFirstNoteOn = seqOptions.skipToFirstNoteOn; |
|
|
this.sequencer.preservePlaybackState = seqOptions.preservePlaybackState; |
|
|
|
|
|
this.sequencer.loadNewSongList([options.processorOptions.startRenderingData.parsedMIDI]); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
this.postReady(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
get currentGain() |
|
|
{ |
|
|
return this.masterGain * this.midiVolume; |
|
|
} |
|
|
|
|
|
getDefaultPresets() |
|
|
{ |
|
|
|
|
|
const sys = this.system; |
|
|
this.system = "xg"; |
|
|
this.defaultPreset = this.getPreset(0, 0); |
|
|
this.defaultPresetUsesOverride = this.overrideSoundfont?.presets?.indexOf(this.defaultPreset) >= 0; |
|
|
this.system = sys; |
|
|
this.drumPreset = this.getPreset(128, 0); |
|
|
this.defaultDrumsUsesOverride = this.overrideSoundfont?.presets?.indexOf(this.drumPreset) >= 0; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setSystem(value) |
|
|
{ |
|
|
this.system = value; |
|
|
this.post({ |
|
|
messageType: returnMessageType.masterParameterChange, |
|
|
messageData: [masterParameterType.midiSystem, this.system] |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getCachedVoice(bank, program, midiNote, velocity) |
|
|
{ |
|
|
return this.cachedVoices?.[bank]?.[program]?.[midiNote]?.[velocity]; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setCachedVoice(bank, program, midiNote, velocity, voices) |
|
|
{ |
|
|
|
|
|
if (!this.cachedVoices) |
|
|
{ |
|
|
this.cachedVoices = []; |
|
|
} |
|
|
if (!this.cachedVoices[bank]) |
|
|
{ |
|
|
this.cachedVoices[bank] = []; |
|
|
} |
|
|
if (!this.cachedVoices[bank][program]) |
|
|
{ |
|
|
this.cachedVoices[bank][program] = []; |
|
|
} |
|
|
if (!this.cachedVoices[bank][program][midiNote]) |
|
|
{ |
|
|
this.cachedVoices[bank][program][midiNote] = []; |
|
|
} |
|
|
|
|
|
|
|
|
this.cachedVoices[bank][program][midiNote][velocity] = voices; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
post(data) |
|
|
{ |
|
|
if (!this.enableEventSystem) |
|
|
{ |
|
|
return; |
|
|
} |
|
|
this.port.postMessage(data); |
|
|
} |
|
|
|
|
|
postReady() |
|
|
{ |
|
|
|
|
|
this.processorInitialized.then(() => |
|
|
{ |
|
|
|
|
|
this.port.postMessage({ |
|
|
messageType: returnMessageType.isFullyInitialized, |
|
|
messageData: undefined |
|
|
}); |
|
|
SpessaSynthInfo("%cSpessaSynth is ready!", consoleColors.recognized); |
|
|
}); |
|
|
} |
|
|
|
|
|
debugMessage() |
|
|
{ |
|
|
SpessaSynthInfo({ |
|
|
channels: this.workletProcessorChannels, |
|
|
voicesAmount: this.totalVoicesAmount, |
|
|
dumpedSamples: this.workletDumpedSamplesList |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
process(inputs, outputs) |
|
|
{ |
|
|
if (!this.alive) |
|
|
{ |
|
|
return false; |
|
|
} |
|
|
|
|
|
this.sequencer.processTick(); |
|
|
|
|
|
|
|
|
const time = currentTime; |
|
|
while (this.eventQueue[0]?.time <= time) |
|
|
{ |
|
|
this.eventQueue.shift().callback(); |
|
|
} |
|
|
|
|
|
|
|
|
this.totalVoicesAmount = 0; |
|
|
this.workletProcessorChannels.forEach((channel, index) => |
|
|
{ |
|
|
if (channel.voices.length < 1 || channel.isMuted) |
|
|
{ |
|
|
|
|
|
return; |
|
|
} |
|
|
let voiceCount = channel.voices.length; |
|
|
let outputIndex; |
|
|
let outputL; |
|
|
let outputR; |
|
|
let reverbL; |
|
|
let reverbR; |
|
|
let chorusL; |
|
|
let chorusR; |
|
|
|
|
|
if (this.oneOutputMode) |
|
|
{ |
|
|
|
|
|
const output = outputs[0]; |
|
|
|
|
|
outputIndex = (index % 16) * 2; |
|
|
outputL = output[outputIndex]; |
|
|
outputR = output[outputIndex + 1]; |
|
|
} |
|
|
else |
|
|
{ |
|
|
|
|
|
outputIndex = (index % this.midiOutputsCount) + 2; |
|
|
outputL = outputs[outputIndex][0]; |
|
|
outputR = outputs[outputIndex][1]; |
|
|
reverbL = outputs[0][0]; |
|
|
reverbR = outputs[0][1]; |
|
|
chorusL = outputs[1][0]; |
|
|
chorusR = outputs[1][1]; |
|
|
} |
|
|
|
|
|
|
|
|
channel.renderAudio( |
|
|
outputL, outputR, |
|
|
reverbL, reverbR, |
|
|
chorusL, chorusR |
|
|
); |
|
|
|
|
|
this.totalVoicesAmount += channel.voices.length; |
|
|
|
|
|
if (channel.voices.length !== voiceCount) |
|
|
{ |
|
|
channel.sendChannelProperty(); |
|
|
} |
|
|
}); |
|
|
|
|
|
return true; |
|
|
} |
|
|
|
|
|
destroyWorkletProcessor() |
|
|
{ |
|
|
this.alive = false; |
|
|
this.workletProcessorChannels.forEach(c => |
|
|
{ |
|
|
delete c.midiControllers; |
|
|
delete c.voices; |
|
|
delete c.sustainedVoices; |
|
|
delete c.lockedControllers; |
|
|
delete c.preset; |
|
|
delete c.customControllers; |
|
|
}); |
|
|
delete this.cachedVoices; |
|
|
delete this.workletProcessorChannels; |
|
|
delete this.sequencer.midiData; |
|
|
delete this.sequencer; |
|
|
this.soundfontManager.destroyManager(); |
|
|
delete this.soundfontManager; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
controllerChange(channel, controllerNumber, controllerValue, force = false) |
|
|
{ |
|
|
this.workletProcessorChannels[channel].controllerChange(controllerNumber, controllerValue, force); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
noteOn(channel, midiNote, velocity) |
|
|
{ |
|
|
this.workletProcessorChannels[channel].noteOn(midiNote, velocity); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
noteOff(channel, midiNote) |
|
|
{ |
|
|
this.workletProcessorChannels[channel].noteOff(midiNote); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
polyPressure(channel, midiNote, pressure) |
|
|
{ |
|
|
this.workletProcessorChannels[channel].polyPressure(midiNote, pressure); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
channelPressure(channel, pressure) |
|
|
{ |
|
|
this.workletProcessorChannels[channel].channelPressure(pressure); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pitchWheel(channel, MSB, LSB) |
|
|
{ |
|
|
this.workletProcessorChannels[channel].pitchWheel(MSB, LSB); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
programChange(channel, programNumber) |
|
|
{ |
|
|
this.workletProcessorChannels[channel].programChange(programNumber); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
processMessage(message, channelOffset, force, options) |
|
|
{ |
|
|
const call = () => |
|
|
{ |
|
|
const statusByteData = getEvent(message[0]); |
|
|
|
|
|
const channel = statusByteData.channel + channelOffset; |
|
|
|
|
|
switch (statusByteData.status) |
|
|
{ |
|
|
case messageTypes.noteOn: |
|
|
const velocity = message[2]; |
|
|
if (velocity > 0) |
|
|
{ |
|
|
this.noteOn(channel, message[1], velocity); |
|
|
} |
|
|
else |
|
|
{ |
|
|
this.noteOff(channel, message[1]); |
|
|
} |
|
|
break; |
|
|
|
|
|
case messageTypes.noteOff: |
|
|
if (force) |
|
|
{ |
|
|
this.workletProcessorChannels[channel].killNote(message[1]); |
|
|
} |
|
|
else |
|
|
{ |
|
|
this.noteOff(channel, message[1]); |
|
|
} |
|
|
break; |
|
|
|
|
|
case messageTypes.pitchBend: |
|
|
this.pitchWheel(channel, message[2], message[1]); |
|
|
break; |
|
|
|
|
|
case messageTypes.controllerChange: |
|
|
this.controllerChange(channel, message[1], message[2], force); |
|
|
break; |
|
|
|
|
|
case messageTypes.programChange: |
|
|
this.programChange(channel, message[1]); |
|
|
break; |
|
|
|
|
|
case messageTypes.polyPressure: |
|
|
this.polyPressure(channel, message[0], message[1]); |
|
|
break; |
|
|
|
|
|
case messageTypes.channelPressure: |
|
|
this.channelPressure(channel, message[1]); |
|
|
break; |
|
|
|
|
|
case messageTypes.systemExclusive: |
|
|
this.systemExclusive(new IndexedByteArray(message.slice(1)), channelOffset); |
|
|
break; |
|
|
|
|
|
case messageTypes.reset: |
|
|
this.stopAllChannels(true); |
|
|
this.resetAllControllers(); |
|
|
break; |
|
|
|
|
|
default: |
|
|
break; |
|
|
} |
|
|
}; |
|
|
|
|
|
const time = options.time; |
|
|
if (time > currentTime) |
|
|
{ |
|
|
this.eventQueue.push({ |
|
|
callback: call.bind(this), |
|
|
time: time |
|
|
}); |
|
|
this.eventQueue.sort((e1, e2) => e1.time - e2.time); |
|
|
} |
|
|
else |
|
|
{ |
|
|
call(); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
SpessaSynthProcessor.prototype.voiceKilling = voiceKilling; |
|
|
SpessaSynthProcessor.prototype.getWorkletVoices = getWorkletVoices; |
|
|
|
|
|
|
|
|
SpessaSynthProcessor.prototype.handleMessage = handleMessage; |
|
|
SpessaSynthProcessor.prototype.callEvent = callEvent; |
|
|
|
|
|
|
|
|
SpessaSynthProcessor.prototype.systemExclusive = systemExclusive; |
|
|
|
|
|
|
|
|
SpessaSynthProcessor.prototype.stopAllChannels = stopAllChannels; |
|
|
SpessaSynthProcessor.prototype.createWorkletChannel = createWorkletChannel; |
|
|
SpessaSynthProcessor.prototype.resetAllControllers = resetAllControllers; |
|
|
|
|
|
|
|
|
SpessaSynthProcessor.prototype.setMasterGain = setMasterGain; |
|
|
SpessaSynthProcessor.prototype.setMasterPan = setMasterPan; |
|
|
SpessaSynthProcessor.prototype.setMIDIVolume = setMIDIVolume; |
|
|
|
|
|
|
|
|
SpessaSynthProcessor.prototype.transposeAllChannels = transposeAllChannels; |
|
|
SpessaSynthProcessor.prototype.setMasterTuning = setMasterTuning; |
|
|
|
|
|
|
|
|
SpessaSynthProcessor.prototype.getPreset = getPreset; |
|
|
SpessaSynthProcessor.prototype.reloadSoundFont = reloadSoundFont; |
|
|
SpessaSynthProcessor.prototype.clearSoundFont = clearSoundFont; |
|
|
SpessaSynthProcessor.prototype.setEmbeddedSoundFont = setEmbeddedSoundFont; |
|
|
SpessaSynthProcessor.prototype.sendPresetList = sendPresetList; |
|
|
|
|
|
|
|
|
SpessaSynthProcessor.prototype.sendSynthesizerSnapshot = sendSynthesizerSnapshot; |
|
|
SpessaSynthProcessor.prototype.applySynthesizerSnapshot = applySynthesizerSnapshot; |
|
|
|
|
|
export { SpessaSynthProcessor }; |