spessasynth_core
Version:
MIDI and SoundFont2/DLS library with no compromises
214 lines (191 loc) • 8.5 kB
JavaScript
import { VolumeEnvelope } from "../engine_components/volume_envelope.js";
import { ModulationEnvelope } from "../engine_components/modulation_envelope.js";
import { customControllers } from "../engine_components/controller_tables.js";
import { absCentsToHz, timecentsToSeconds } from "../engine_components/unit_converter.js";
import { getLFOValue } from "../engine_components/lfo.js";
import { WavetableOscillator } from "../engine_components/wavetable_oscillator.js";
import { LowpassFilter } from "../engine_components/lowpass_filter.js";
import { generatorTypes } from "../../../soundfont/basic_soundfont/generator_types.js";
/**
* Renders a voice to the stereo output buffer
* @param voice {Voice} the voice to render
* @param timeNow {number} current time in seconds
* @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
* @param startIndex {number}
* @param sampleCount {number}
* @this {MidiAudioChannel}
* @returns {boolean} true if the voice is finished
*/
export function renderVoice(
voice, timeNow,
outputLeft, outputRight,
reverbOutputLeft, reverbOutputRight,
chorusOutputLeft, chorusOutputRight,
startIndex, sampleCount
)
{
// 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;
VolumeEnvelope.startRelease(voice);
ModulationEnvelope.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];
// low pass excursion with LFO and mod envelope
let lowpassExcursion = 0;
let volumeExcursionCentibels = 0;
// vibrato LFO
const vibPitchDepth = voice.modulatedGenerators[generatorTypes.vibLfoToPitch];
const vibVolDepth = voice.modulatedGenerators[generatorTypes.vibLfoToVolume];
const vibFilterDepth = voice.modulatedGenerators[generatorTypes.vibLfoToFilterFc];
if (vibPitchDepth !== 0 || vibVolDepth !== 0 || vibFilterDepth !== 0)
{
// calculate start time and lfo value
const vibStart = voice.startTime + timecentsToSeconds(voice.modulatedGenerators[generatorTypes.delayVibLFO]);
const vibFreqHz = absCentsToHz(voice.modulatedGenerators[generatorTypes.freqVibLFO]);
const vibLfoValue = getLFOValue(vibStart, vibFreqHz, timeNow);
// use modulation multiplier (RPN modulation depth)
cents += vibLfoValue * (vibPitchDepth * this.customControllers[customControllers.modulationMultiplier]);
// vol env volume offset
// negate the lfo value because audigy starts with increase rather than decrease
volumeExcursionCentibels += -vibLfoValue * vibVolDepth;
// low pass frequency
lowpassExcursion += vibLfoValue * vibFilterDepth;
}
// mod LFO
const modPitchDepth = voice.modulatedGenerators[generatorTypes.modLfoToPitch];
const modVolDepth = voice.modulatedGenerators[generatorTypes.modLfoToVolume];
const modFilterDepth = voice.modulatedGenerators[generatorTypes.modLfoToFilterFc];
// 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]);
// vol env volume offset
// negate the lfo value because audigy starts with increase rather than decrease
volumeExcursionCentibels += -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 = ModulationEnvelope.getValue(voice, timeNow);
// apply values
lowpassExcursion += modEnv * modEnvFilterDepth;
cents += modEnv * modEnvPitchDepth;
}
// default resonant modulator: it does not affect the filter gain (neither XG nor GS did that)
volumeExcursionCentibels -= voice.resonanceOffset;
// 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(sampleCount);
// looping mode 2: start on release. process only volEnv
if (voice.sample.loopingMode === 2 && !voice.isInRelease)
{
VolumeEnvelope.apply(
voice,
bufferOut,
volumeExcursionCentibels,
this.synth.volumeEnvelopeSmoothingFactor
);
return voice.finished;
}
// wave table oscillator
WavetableOscillator.getSample(voice, bufferOut, this.synth.interpolationType);
// low pass filter
LowpassFilter.apply(voice, bufferOut, lowpassExcursion, this.synth.filterSmoothingFactor);
// vol env
VolumeEnvelope.apply(
voice,
bufferOut,
volumeExcursionCentibels,
this.synth.volumeEnvelopeSmoothingFactor
);
this.panAndMixVoice(
voice,
bufferOut,
outputLeft, outputRight,
reverbOutputLeft, reverbOutputRight,
chorusOutputLeft, chorusOutputRight,
startIndex
);
return voice.finished;
}