Skip to content

Commit

Permalink
Fix clicking in volenv
Browse files Browse the repository at this point in the history
volenv rework btw: no interpolation of gain, only initialAttenuation is now interpolated
add version display
  • Loading branch information
spessasus committed Sep 17, 2024
1 parent 1e9e89a commit 07166f3
Show file tree
Hide file tree
Showing 12 changed files with 143 additions and 96 deletions.
16 changes: 8 additions & 8 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
Expand Up @@ -72,6 +72,7 @@ export function noteOn(channel, midiNote, velocity, enableDebugging = false, sen
}
// compute all modulators
computeModulators(voice, channelObject.midiControllers);
WorkletVolumeEnvelope.intialize(voice);
// set initial pan to avoid split second changing from middle to the correct value
voice.currentPan = ((Math.max(-500, Math.min(500, voice.modulatedGenerators[generatorTypes.pan] )) + 500) / 1000) // 0 to 1
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { generatorTypes } from '../../../soundfont/read_sf2/generators.js'
export const VOLUME_ENVELOPE_SMOOTHING_FACTOR = 0.001;

const DB_SILENCE = 100;
const PERCEIVED_DB_SILENCE = 70;
const PERCEIVED_DB_SILENCE = 96;

/**
* VOL ENV STATES:
Expand Down Expand Up @@ -92,10 +92,16 @@ export class WorkletVolumeEnvelope
attenuation = 0;

/**
* The voice's sustain amount in dB, absolute
* The attenuation target, which the "attenuation" property is linearly interpolated towards
* @type {number}
*/
sustainDb = 0;
attenuationTarget = 0;

/**
* The voice's sustain amount in dB, relative to attenuation
* @type {number}
*/
sustainDbRelative = 0;

/**
* The time in samples to the end of delay stage, relative to start of the envelope
Expand Down Expand Up @@ -132,20 +138,36 @@ export class WorkletVolumeEnvelope
WorkletVolumeEnvelope.recalculate(voice);
}

/**
* Initializes a volume envelope
* @param voice {WorkletVoice}
*/
static intialize(voice)
{
WorkletVolumeEnvelope.recalculate(voice, true);
voice.volumeEnvelope.attenuation = voice.volumeEnvelope.attenuationTarget;
}

/**
* Recalculates the envelope
* @param voice {WorkletVoice} the voice this envelope belongs to
* @param setupInterpolated {boolean} if we should initialize the interpolated values (attenuation and sustain)
*/
static recalculate(voice)
static recalculate(voice, setupInterpolated = false)
{
const env = voice.volumeEnvelope;
const timecentsToSamples = tc =>
{
return Math.floor(timecentsToSeconds(tc) * env.sampleRate);
}
// calculate absolute times (they can change so we have to recalculate every time
env.attenuation = Math.max(0, Math.min(voice.modulatedGenerators[generatorTypes.initialAttenuation], 1440)) / 10; // divide by ten to get decibelts
env.sustainDb = Math.min(100, voice.volumeEnvelope.attenuation + voice.modulatedGenerators[generatorTypes.sustainVolEnv] / 10);
env.attenuationTarget = Math.max(0, Math.min(voice.modulatedGenerators[generatorTypes.initialAttenuation], 1440)) / 10; // divide by ten to get decibels
env.sustainDbRelative = Math.min(100, voice.modulatedGenerators[generatorTypes.sustainVolEnv] / 10);
if(setupInterpolated)
{
env.attenuation = env.attenuationTarget;
}
const sustainDb = Math.min(100, env.sustainDbRelative + env.attenuation);

// calculate durations
env.attackDuration = timecentsToSamples(voice.modulatedGenerators[generatorTypes.attackVolEnv]);
Expand All @@ -155,7 +177,7 @@ export class WorkletVolumeEnvelope
// (changing from attenuation to sustain instead of -100dB)
const fullChange = voice.modulatedGenerators[generatorTypes.decayVolEnv];
const keyNumAddition = (60 - voice.targetKey) * voice.modulatedGenerators[generatorTypes.keyNumToVolEnvDecay];
const fraction = (env.sustainDb - env.attenuation) / 100;
const fraction = (sustainDb - env.attenuation) / 100;
env.decayDuration = timecentsToSamples(fullChange + keyNumAddition) * fraction;

env.releaseDuration = timecentsToSamples(voice.modulatedGenerators[generatorTypes.releaseVolEnv]);
Expand All @@ -175,13 +197,14 @@ export class WorkletVolumeEnvelope
// if this is the first recalculation and the voice has no attack or delay time, set current db to peak
if(env.state === 0 && env.attackEnd === 0)
{
env.currentAttenuationDb = env.attenuation;
env.currentAttenuationDb = env.attenuationTarget;
env.state = 2;
}

// check if voice is in release
if(voice.isInRelease)
{
const sustainDb = Math.min(100, env.sustainDbRelative + env.attenuation);
switch (env.state)
{
case 0:
Expand All @@ -206,11 +229,11 @@ export class WorkletVolumeEnvelope
break;

case 3:
env.releaseStartDb = (1 - (env.decayEnd - env.releaseStartTimeSamples) / env.decayDuration) * (env.sustainDb - env.attenuation) + env.attenuation;
env.releaseStartDb = (1 - (env.decayEnd - env.releaseStartTimeSamples) / env.decayDuration) * (sustainDb - env.attenuation) + env.attenuation;
break;

case 4:
env.releaseStartDb = env.sustainDb;
env.releaseStartDb = sustainDb;
break;

default:
Expand All @@ -224,20 +247,6 @@ export class WorkletVolumeEnvelope
}
}

/**
* Gets interpolated gain
* @param env {WorkletVolumeEnvelope}
* @param attenuationDb {number} in decibels
* @param smoothingFactor {number}
* @returns {number} the gain value
*/
static getInterpolatedGain(env, attenuationDb, smoothingFactor)
{
// interpolate attenuation to prevent clicking
env.currentAttenuationDb += (attenuationDb - env.currentAttenuationDb) * smoothingFactor;
return decibelAttenuationToGain(env.currentAttenuationDb);
}

/**
* Applies volume envelope gain to the given output buffer
* @param voice {WorkletVoice} the voice we're working on
Expand All @@ -249,6 +258,8 @@ export class WorkletVolumeEnvelope
{
const env = voice.volumeEnvelope;
let decibelOffset = centibelOffset / 10;

const attenuationSmoothing = smoothingFactor;

// RELEASE PHASE
if(voice.isInRelease)
Expand Down Expand Up @@ -308,6 +319,9 @@ export class WorkletVolumeEnvelope
// attack phase: ramp from 0 to attenuation
while(env.currentSampleTime < env.attackEnd)
{
// attenuation interpolation
env.attenuation += (env.attenuationTarget - env.attenuation) * attenuationSmoothing;

// Special case: linear gain ramp instead of linear db ramp
let linearAttenuation = 1 - (env.attackEnd - env.currentSampleTime) / env.attackDuration; // 0 to 1
audioBuffer[filledBuffer] *= linearAttenuation * decibelAttenuationToGain(env.attenuation + decibelOffset)
Expand All @@ -328,7 +342,11 @@ export class WorkletVolumeEnvelope
// hold/peak phase: stay at attenuation
while(env.currentSampleTime < env.holdEnd)
{
audioBuffer[filledBuffer] *= WorkletVolumeEnvelope.getInterpolatedGain(env, env.attenuation + decibelOffset, smoothingFactor);
// attenuation interpolation
env.attenuation += (env.attenuationTarget - env.attenuation) * attenuationSmoothing;

audioBuffer[filledBuffer] *= decibelAttenuationToGain(env.attenuation + decibelOffset);
env.currentAttenuationDb = env.attenuation;

env.currentSampleTime++;
if(++filledBuffer >= audioBuffer.length)
Expand All @@ -341,11 +359,14 @@ export class WorkletVolumeEnvelope

case 3:
// decay phase: linear ramp from attenuation to sustain
const dbDifference = env.sustainDb - env.attenuation;
while(env.currentSampleTime < env.decayEnd)
{
const newAttenuation = (1 - (env.decayEnd - env.currentSampleTime) / env.decayDuration) * dbDifference + env.attenuation;
audioBuffer[filledBuffer] *= WorkletVolumeEnvelope.getInterpolatedGain(env, newAttenuation + decibelOffset, smoothingFactor);
// attenuation interpolation
env.attenuation += (env.attenuationTarget - env.attenuation) * attenuationSmoothing;

const sustainDb = Math.min(100, env.sustainDbRelative + env.attenuation);
env.currentAttenuationDb = (1 - (env.decayEnd - env.currentSampleTime) / env.decayDuration) * (sustainDb - env.attenuation) + env.attenuation;
audioBuffer[filledBuffer] *= decibelAttenuationToGain(env.currentAttenuationDb + decibelOffset);

env.currentSampleTime++;
if(++filledBuffer >= audioBuffer.length)
Expand All @@ -358,24 +379,20 @@ export class WorkletVolumeEnvelope

case 4:
// sustain phase: stay at sustain
if(env.sustainDb > PERCEIVED_DB_SILENCE)
{
while(filledBuffer < audioBuffer.length)
{
audioBuffer[filledBuffer++] = 0;
}
return;
}
while(true)
{
audioBuffer[filledBuffer] *= WorkletVolumeEnvelope.getInterpolatedGain(env, env.sustainDb + decibelOffset, smoothingFactor);
// attenuation interpolation
env.attenuation += (env.attenuationTarget - env.attenuation) * attenuationSmoothing;
const sustainDb = Math.min(100, env.sustainDbRelative + env.attenuation);

audioBuffer[filledBuffer] *= decibelAttenuationToGain(sustainDb + decibelOffset);
env.currentAttenuationDb = sustainDb;
env.currentSampleTime++;
if(++filledBuffer >= audioBuffer.length)
{
return;
}
}

}
}
}
5 changes: 4 additions & 1 deletion src/website/demo_main.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ const exportButton = document.getElementById("export_button");
const loading = document.getElementsByClassName("loading")[0];
const loadingMessage = document.getElementById("loading_message");

// load version
const r = await (await fetch("package.json")).json();
window.SPESSASYNTH_VERSION = r["version"];

// IndexedDB stuff
const dbName = "spessasynth-db";
const objectStoreName = "soundFontStore";
Expand Down Expand Up @@ -68,7 +72,6 @@ async function loadLastSoundFontFromDatabase()
console.error("Database error");
console.error(e);
resolve(undefined);
return;
}

request.onsuccess = async () => {
Expand Down
9 changes: 6 additions & 3 deletions src/website/js/renderer/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,14 @@ export function render(auto = true, force = false)
// draw note count and fps
this.drawingContext.textBaseline = "hanging";
this.drawingContext.textAlign = "end";
this.drawingContext.font = `${FONT_SIZE}px Verdana`;
this.drawingContext.font = `${FONT_SIZE}px system-ui`;
this.drawingContext.fillStyle = "white";
this.drawingContext.strokeStyle = "white";
this.drawingContext.fillText(`${this.notesOnScreen} notes`, this.canvas.width, FONT_SIZE + 5);
this.drawingContext.fillText(Math.round(fps).toString() + " FPS", this.canvas.width, 5);
this.drawingContext.fillText(`${this.notesOnScreen} notes`, this.canvas.width, FONT_SIZE * 2 + 5);
this.drawingContext.fillText(this.version, this.canvas.width, 5);
this.drawingContext.fillText(Math.round(fps).toString() + " FPS", this.canvas.width, FONT_SIZE + 5);


if(this.onRender)
{
this.onRender();
Expand Down
7 changes: 5 additions & 2 deletions src/website/js/renderer/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const DARKER_MULTIPLIER = 0.6;
const GRADIENT_DARKEN = 0.5;
export const STROKE_THICKNESS = 1;
export const NOTE_MARGIN = 1;
export const FONT_SIZE = 16;
export const FONT_SIZE = 12;
export const PRESSED_EFFECT_TIME = 0.6;

// limits
Expand All @@ -57,8 +57,9 @@ class Renderer
* @param synth {Synthetizer}
* @param canvas {HTMLCanvasElement}
* @param delayNode {DelayNode} used for correcting time for large fft sizes
* @param version {string}
*/
constructor(channelColors, synth, canvas, delayNode)
constructor(channelColors, synth, canvas, delayNode, version = "")
{
// variables
/**
Expand All @@ -76,6 +77,8 @@ class Renderer
max: 127
};

this.version = "v" + version;

/**
* adds this to the synth's visual pitch in position caluclation
* @type {number}
Expand Down
3 changes: 3 additions & 0 deletions src/website/local_main.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ let synthReady = false;
*/
window.loadedSoundfonts = [];

const r = await (await fetch("/getversion")).text();
window.SPESSASYNTH_VERSION = r;

/**
* @param fileName {string}
* @param callback {function(number)}
Expand Down
2 changes: 1 addition & 1 deletion src/website/manager/manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ class Manager
canvas.width = window.innerWidth * window.devicePixelRatio;
canvas.height = window.innerHeight * window.devicePixelRatio;

this.renderer = new Renderer(this.channelColors, this.synth, canvas, this.audioDelay);
this.renderer = new Renderer(this.channelColors, this.synth, canvas, this.audioDelay, window.SPESSASYNTH_VERSION);
this.renderer.render(true);

let titleSwappedWithSettings = false;
Expand Down
Loading

0 comments on commit 07166f3

Please sign in to comment.