UNPKG

spessasynth_lib

Version:

MIDI and SoundFont2/DLS library with no compromises

197 lines (175 loc) 7.79 kB
import { WorkletVolumeEnvelope } from "../worklet_utilities/volume_envelope.js"; import { WorkletModulationEnvelope } from "../worklet_utilities/modulation_envelope.js"; import { generatorTypes } from "../../../soundfont/basic_soundfont/generator.js"; import { customControllers } from "../worklet_utilities/controller_tables.js"; import { absCentsToHz, timecentsToSeconds } from "../worklet_utilities/unit_converter.js"; import { getLFOValue } from "../worklet_utilities/lfo.js"; import { interpolationTypes, WavetableOscillator } from "../worklet_utilities/wavetable_oscillator.js"; import { WorkletLowpassFilter } from "../worklet_utilities/lowpass_filter.js"; /** * Renders a voice to the stereo output buffer * @param voice {WorkletVoice} the voice to render * @param outputLeft {Float32Array} the left output buffer * @param outputRight {Float32Array} the right output buffer * @param reverbOutputLeft {Float32Array} left output for reverb * @param reverbOutputRight {Float32Array} right output for reverb * @param chorusOutputLeft {Float32Array} left output for chorus * @param chorusOutputRight {Float32Array} right output for chorus * @this {WorkletProcessorChannel} * @returns {boolean} true if the voice is finished */ export function renderVoice( voice, outputLeft, outputRight, reverbOutputLeft, reverbOutputRight, chorusOutputLeft, chorusOutputRight ) { // avoid jetbrains errors const timeNow = currentTime; // check if release if (!voice.isInRelease) { // if not in release, check if the release time is if (timeNow >= voice.releaseStartTime) { // release the voice here voice.isInRelease = true; WorkletVolumeEnvelope.startRelease(voice); WorkletModulationEnvelope.startRelease(voice); if (voice.sample.loopingMode === 3) { voice.sample.isLooping = false; } } } // if the initial attenuation is more than 100dB, skip the voice (it's silent anyway) if (voice.modulatedGenerators[generatorTypes.initialAttenuation] > 2500) { if (voice.isInRelease) { voice.finished = true; } return voice.finished; } // TUNING let targetKey = voice.targetKey; // calculate tuning let cents = voice.modulatedGenerators[generatorTypes.fineTune] // soundfont fine tune + this.channelOctaveTuning[voice.midiNote] // MTS octave tuning + this.channelTuningCents; // channel tuning let semitones = voice.modulatedGenerators[generatorTypes.coarseTune]; // soundfont coarse tuning // midi tuning standard const tuning = this.synth.tunings[this.preset.program]?.[voice.realKey]; if (tuning !== undefined && tuning?.midiNote >= 0) { // override key targetKey = tuning.midiNote; // add micro-tonal tuning cents += tuning.centTuning; } // portamento if (voice.portamentoFromKey > -1) { // 0 to 1 const elapsed = Math.min((timeNow - voice.startTime) / voice.portamentoDuration, 1); const diff = targetKey - voice.portamentoFromKey; // zero progress means the pitch being in fromKey, full progress means the normal pitch semitones -= diff * (1 - elapsed); } // calculate tuning by key using soundfont's scale tuning cents += (targetKey - voice.sample.rootKey) * voice.modulatedGenerators[generatorTypes.scaleTuning]; // vibrato LFO const vibratoDepth = voice.modulatedGenerators[generatorTypes.vibLfoToPitch]; if (vibratoDepth !== 0) { // calculate start time and lfo value const vibStart = voice.startTime + timecentsToSeconds(voice.modulatedGenerators[generatorTypes.delayVibLFO]); const vibFreqHz = absCentsToHz(voice.modulatedGenerators[generatorTypes.freqVibLFO]); const lfoVal = getLFOValue(vibStart, vibFreqHz, timeNow); // use modulation multiplier (RPN modulation depth) cents += lfoVal * (vibratoDepth * this.customControllers[customControllers.modulationMultiplier]); } // low pass excursion with LFO and mod envelope let lowpassExcursion = 0; // mod LFO const modPitchDepth = voice.modulatedGenerators[generatorTypes.modLfoToPitch]; const modVolDepth = voice.modulatedGenerators[generatorTypes.modLfoToVolume]; const modFilterDepth = voice.modulatedGenerators[generatorTypes.modLfoToFilterFc]; let modLfoCentibels = 0; // don't compute mod lfo unless necessary if (modPitchDepth !== 0 || modFilterDepth !== 0 || modVolDepth !== 0) { // calculate start time and lfo value const modStart = voice.startTime + timecentsToSeconds(voice.modulatedGenerators[generatorTypes.delayModLFO]); const modFreqHz = absCentsToHz(voice.modulatedGenerators[generatorTypes.freqModLFO]); const modLfoValue = getLFOValue(modStart, modFreqHz, timeNow); // use modulation multiplier (RPN modulation depth) cents += modLfoValue * (modPitchDepth * this.customControllers[customControllers.modulationMultiplier]); // vole nv volume offset // negate the lfo value because audigy starts with increase rather than decrease modLfoCentibels = -modLfoValue * modVolDepth; // low pass frequency lowpassExcursion += modLfoValue * modFilterDepth; } // channel vibrato (GS NRPN) if (this.channelVibrato.depth > 0) { // same as others const channelVibrato = getLFOValue( voice.startTime + this.channelVibrato.delay, this.channelVibrato.rate, timeNow ); if (channelVibrato) { cents += channelVibrato * this.channelVibrato.depth; } } // mod env const modEnvPitchDepth = voice.modulatedGenerators[generatorTypes.modEnvToPitch]; const modEnvFilterDepth = voice.modulatedGenerators[generatorTypes.modEnvToFilterFc]; // don't compute mod env unless necessary if (modEnvFilterDepth !== 0 || modEnvPitchDepth !== 0) { const modEnv = WorkletModulationEnvelope.getValue(voice, timeNow); // apply values lowpassExcursion += modEnv * modEnvFilterDepth; cents += modEnv * modEnvPitchDepth; } // finally, calculate the playback rate const centsTotal = ~~(cents + semitones * 100); if (centsTotal !== voice.currentTuningCents) { voice.currentTuningCents = centsTotal; voice.currentTuningCalculated = Math.pow(2, centsTotal / 1200); } // SYNTHESIS const bufferOut = new Float32Array(outputLeft.length); // wave table oscillator switch (this.synth.interpolationType) { case interpolationTypes.fourthOrder: WavetableOscillator.getSampleCubic(voice, bufferOut); break; case interpolationTypes.linear: default: WavetableOscillator.getSampleLinear(voice, bufferOut); break; case interpolationTypes.nearestNeighbor: WavetableOscillator.getSampleNearest(voice, bufferOut); break; } // low pass filter WorkletLowpassFilter.apply(voice, bufferOut, lowpassExcursion, this.synth.filterSmoothingFactor); // vol env WorkletVolumeEnvelope.apply(voice, bufferOut, modLfoCentibels, this.synth.volumeEnvelopeSmoothingFactor); this.panVoice( voice, bufferOut, outputLeft, outputRight, reverbOutputLeft, reverbOutputRight, chorusOutputLeft, chorusOutputRight ); return voice.finished; }