Skip to content

Commit

Permalink
Volume envelope rework:
Browse files Browse the repository at this point in the history
vol env now uses samples instead of seconds
code cleanup and optimizations for mod env
fixed decay times for both: now relative to sustain level like the spec says
  • Loading branch information
spessasus committed Aug 30, 2024
1 parent 5533681 commit 1d347f1
Show file tree
Hide file tree
Showing 16 changed files with 497 additions and 313 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "SpessaSynth",
"version": "3.20.2",
"version": "3.20.3",
"type": "module",
"scripts": {
"start": "node src/website/server/server.js"
Expand Down
4 changes: 1 addition & 3 deletions src/spessasynth_lib/sequencer/worklet_sequencer/play.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,10 +264,8 @@ export function play(resetTime = false)
}
if(!this.sendMIDIMessages)
{
const time = this.currentTime;
this.playingNotes.forEach(n => {
const timeOffset = n.startTime - time;
this.synth.noteOn(n.channel, n.midiNote, n.velocity, false, true, currentTime + timeOffset);
this.synth.noteOn(n.channel, n.midiNote, n.velocity, false, true);
});
}
this.setProcessHandler();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ export function _processEvent(event, trackIndex)
this.playingNotes.push({
midiNote: event.messageData[0],
channel: statusByteData.channel,
velocity: velocity,
startTime: this.currentTime
velocity: velocity
});
}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ class WorkletSequencer
* @type {{
* midiNote: number,
* channel: number,
* velocity: number,
* startTime: number
* velocity: number
* }[]}
*/
this.playingNotes = [];
Expand Down
18 changes: 9 additions & 9 deletions src/spessasynth_lib/synthetizer/worklet_processor.min.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { getWorkletVoices } from '../worklet_utilities/worklet_voice.js'
import { generatorTypes } from '../../../soundfont/read_sf2/generators.js'
import { computeModulators } from '../worklet_utilities/worklet_modulator.js'
import { recalculateVolumeEnvelope } from '../worklet_utilities/volume_envelope.js'
import { WorkletVolumeEnvelope } from '../worklet_utilities/volume_envelope.js'
import { WorkletModulationEnvelope } from '../worklet_utilities/modulation_envelope.js'

/**
* Append the voices
Expand Down Expand Up @@ -72,7 +73,8 @@ export function noteOn(channel, midiNote, velocity, enableDebugging = false, sen
this.releaseVoice(v);
v.modulatedGenerators[generatorTypes.releaseVolEnv] = -7000; // make the release nearly instant
v.modulatedGenerators[generatorTypes.releaseModEnv] = -7000;
recalculateVolumeEnvelope(v);
WorkletVolumeEnvelope.recalculate(v);
WorkletModulationEnvelope.recalculate(v);
}
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { generatorTypes } from '../../../soundfont/read_sf2/generators.js'
import { absCentsToHz, decibelAttenuationToGain, timecentsToSeconds } from '../worklet_utilities/unit_converter.js'
import { getLFOValue } from '../worklet_utilities/lfo.js'
import { customControllers } from '../worklet_utilities/worklet_processor_channel.js'
import { getModEnvValue } from '../worklet_utilities/modulation_envelope.js'
import { WorkletModulationEnvelope } from '../worklet_utilities/modulation_envelope.js'
import { getOscillatorData } from '../worklet_utilities/wavetable_oscillator.js'
import { panVoice } from '../worklet_utilities/stereo_panner.js'
import { applyVolumeEnvelope, recalculateVolumeEnvelope } from '../worklet_utilities/volume_envelope.js'
import { applyLowpassFilter } from '../worklet_utilities/lowpass_filter.js'
import { MIN_NOTE_LENGTH } from '../main_processor.js'
import { WorkletVolumeEnvelope } from '../worklet_utilities/volume_envelope.js'


const HALF_PI = Math.PI / 2;
Expand Down Expand Up @@ -36,10 +36,10 @@ export function renderVoice(
// if not in release, check if the release time is
if (currentTime >= voice.releaseStartTime)
{
voice.releaseStartModEnv = voice.currentModEnvValue;

voice.isInRelease = true;
recalculateVolumeEnvelope(voice);
voice.volumeEnvelope.currentReleaseGain = decibelAttenuationToGain(voice.volumeEnvelope.currentAttenuationDb);
WorkletVolumeEnvelope.startRelease(voice);
WorkletModulationEnvelope.startRelease(voice);
}
}

Expand Down Expand Up @@ -127,7 +127,7 @@ export function renderVoice(
// mod env
const modEnvPitchDepth = voice.modulatedGenerators[generatorTypes.modEnvToPitch];
const modEnvFilterDepth = voice.modulatedGenerators[generatorTypes.modEnvToFilterFc];
const modEnv = getModEnvValue(voice, currentTime);
const modEnv = WorkletModulationEnvelope.getValue(voice, currentTime);
// apply values
lowpassCents += modEnv * modEnvFilterDepth;
cents += modEnv * modEnvPitchDepth;
Expand All @@ -153,7 +153,7 @@ export function renderVoice(
applyLowpassFilter(voice, bufferOut, lowpassCents);

// volenv
applyVolumeEnvelope(voice, bufferOut, currentTime, modLfoCentibels, this.sampleTime, this.volumeEnvelopeSmoothingFactor);
WorkletVolumeEnvelope.apply(voice, bufferOut, modLfoCentibels, this.volumeEnvelopeSmoothingFactor);

// pan the voice and write out
voice.currentPan += (pan - voice.currentPan) * this.panSmoothingFactor; // smooth out pan to prevent clicking
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { modulatorCurveTypes } from '../../../soundfont/read_sf2/modulators.js'
* modulation_envelope.js
* purpose: calculates the modulation envelope for the given voice
*/
const PEAK = 1;
const MODENV_PEAK = 1;

// 1000 should be precise enough
const CONVEX_ATTACK = new Float32Array(1000);
Expand All @@ -16,58 +16,156 @@ for (let i = 0; i < CONVEX_ATTACK.length; i++) {
CONVEX_ATTACK[i] = getModulatorCurveValue(0, modulatorCurveTypes.convex, i / 1000, 0);
}

/**
* Calculates the current modulation envelope value for the given time and voice
* @param voice {WorkletVoice} the voice we're working on
* @param currentTime {number} in seconds
* @returns {number} modenv value, from 0 to 1
*/
export function getModEnvValue(voice, currentTime)
export class WorkletModulationEnvelope
{
// calculate env times
let attack = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.attackModEnv]);
let decay = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.decayModEnv] + ((60 - voice.midiNote) * voice.modulatedGenerators[generatorTypes.keyNumToModEnvDecay]));
let hold = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.holdModEnv] + ((60 - voice.midiNote) * voice.modulatedGenerators[generatorTypes.keyNumToModEnvHold]));
/**
* The attack duration, in seconds
* @type {number}
*/
attackDuration = 0;
/**
* The decay duration, in seconds
* @type {number}
*/
decayDuration = 0;

// calculate absolute times
if(voice.isInRelease && voice.releaseStartTime < currentTime)
{
let release = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.releaseModEnv]);
if(voice.modulatedGenerators[generatorTypes.releaseModEnv] < -7199)
{
// prevent lowpass bugs if release is instant
return voice.releaseStartModEnv;
}
return (1 - (currentTime - voice.releaseStartTime) / release) * voice.releaseStartModEnv;
}
/**
* The hold duration, in seconds
* @type {number}
*/
holdDuration = 0;

let sustain = 1 - (voice.modulatedGenerators[generatorTypes.sustainModEnv] / 1000);
let delayEnd = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.delayModEnv]) + voice.startTime;
let attackEnd = attack + delayEnd;
let holdEnd = hold + attackEnd;
let decayEnd = decay + holdEnd;
/**
* Release duration, in seconds
* @type {number}
*/
releaseDuration = 0;

let modEnvVal
if(currentTime < delayEnd)
{
modEnvVal = 0; // delay
}
else if(currentTime < attackEnd)
{
modEnvVal = CONVEX_ATTACK[~~((1 - (attackEnd - currentTime) / attack) * 1000)]; // convex attack
}
else if(currentTime < holdEnd)
/**
* The sustain level 0-1
* @type {number}
*/
sustainLevel = 0;

/**
* Delay phase end time in seconds, absolute (audio context time)
* @type {number}
*/
delayEnd = 0;
/**
* Attack phase end time in seconds, absolute (audio context time)
* @type {number}
*/
attackEnd = 0;
/**
* Hold phase end time in seconds, absolute (audio context time)
* @type {number}
*/
holdEnd = 0;
/**
* Decay phase end time in seconds, absolute (audio context time)
* @type {number}
*/
decayEnd = 0;

/**
* The level of the envelope when the release phase starts
* @type {number}
*/
releaseStartLevel = 0;

/**
* The current modulation envelope value
* @type {number}
*/
currentValue = 0;

/**
* Starts the release phase in the envelope
* @param voice {WorkletVoice} the voice this envelope belongs to
*/
static startRelease(voice)
{
modEnvVal = PEAK; // peak
voice.modulationEnvelope.releaseStartLevel = voice.modulationEnvelope.currentValue;
WorkletModulationEnvelope.recalculate(voice);
}
else if(currentTime < decayEnd)

/**
* @param voice {WorkletVoice} the voice to recalculate
*/
static recalculate(voice)
{
modEnvVal = (1 - (decayEnd - currentTime) / decay) * (sustain - PEAK) + PEAK; // decay
const env = voice.modulationEnvelope;

env.sustainLevel = 1 - (voice.modulatedGenerators[generatorTypes.sustainModEnv] / 1000);

env.attackDuration = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.attackModEnv]);

const decayKeyExcursionCents = ((60 - voice.midiNote) * voice.modulatedGenerators[generatorTypes.keyNumToModEnvDecay]);
const decayTime = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.decayModEnv] + decayKeyExcursionCents);
// according to the specification, the decay time is the time it takes to reach 0% from 100%.
// calculate the time to reach actual sustain level
// for example, sustain 0.6 will be 0.4 of the decay time
env.decayDuration = decayTime * (1 - env.sustainLevel);

const holdKeyExcursionCents = ((60 - voice.midiNote) * voice.modulatedGenerators[generatorTypes.keyNumToModEnvHold]);
env.holdDuration = timecentsToSeconds(holdKeyExcursionCents + voice.modulatedGenerators[generatorTypes.holdModEnv]);

const releaseTime = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.releaseVolEnv]);
// release time is from the full level to 0%
// to get the actual time, multiply by the release start level
env.releaseDuration = releaseTime * env.releaseStartLevel;

env.delayEnd = voice.startTime + timecentsToSeconds(voice.modulatedGenerators[generatorTypes.delayModEnv]);
env.attackEnd = env.delayEnd + env.attackDuration;
env.holdEnd = env.attackEnd + env.holdDuration;
env.decayEnd = env.holdEnd + env.decayDuration;
}
else

/**
* FIXME: GeneralUserGS 808 toms sound incorrect
* Calculates the current modulation envelope value for the given time and voice
* @param voice {WorkletVoice} the voice we're working on
* @param currentTime {number} in seconds
* @returns {number} modenv value, from 0 to 1
*/
static getValue(voice, currentTime)
{
modEnvVal = sustain; // sustain
const env = voice.modulationEnvelope;
if(voice.isInRelease)
{
if(voice.modulatedGenerators[generatorTypes.releaseModEnv] < -7199)
{
// prevent lowpass bugs if release is instant
return env.releaseStartLevel;
}
return Math.max(0, (1 - (currentTime - voice.releaseStartTime) / env.releaseDuration) * env.releaseStartLevel);
}

if(currentTime < env.delayEnd)
{
env.currentValue = 0; // delay
}
else if(currentTime < env.attackEnd)
{
// modulation envelope uses convex curve for attack
env.currentValue = CONVEX_ATTACK[~~((1 - (env.attackEnd - currentTime) / env.attackDuration) * 1000)];
}
else if(currentTime < env.holdEnd)
{
// hold: stay at 1
env.currentValue = MODENV_PEAK;
}
else if(currentTime < env.decayEnd)
{
// decay: linear ramp from 1 to sustain level
env.currentValue = (1 - (env.decayEnd - currentTime) / env.decayDuration) * (env.sustainLevel - MODENV_PEAK) + MODENV_PEAK;
}
else
{
// sustain: stay at sustain level
env.currentValue = env.sustainLevel;
}
return env.currentValue;
}
voice.currentModEnvValue = modEnvVal;
return modEnvVal;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const WORKLET_SYSTEM_REVERB_DIVIDER = 500;
export const WORKLET_SYSTEM_CHORUS_DIVIDER = 500;
export const WORKLET_SYSTEM_REVERB_DIVIDER = 800;
export const WORKLET_SYSTEM_CHORUS_DIVIDER = 800;
/**
* stereo_panner.js
* purpose: pans a given voice out to the stereo output and to the effects' outputs
Expand Down
Loading

0 comments on commit 1d347f1

Please sign in to comment.