Skip to content

Commit

Permalink
The Worklet update: Everything
Browse files Browse the repository at this point in the history
- fixed envelope
- added lowpass filter
- added modenv
- full modenv support
now i need to port this to wasm and it will be done (i hope)
  • Loading branch information
spessasus committed Sep 30, 2023
1 parent 3f16337 commit 0771550
Show file tree
Hide file tree
Showing 13 changed files with 392 additions and 214 deletions.
4 changes: 2 additions & 2 deletions src/spessasynth_lib/soundfont/chunk/generators.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ generatorLimits[generatorTypes.attackModEnv] = {min: -12000, max: 8000, def: -12
generatorLimits[generatorTypes.holdModEnv] = {min: -12000, max: 5000, def: -12000};
generatorLimits[generatorTypes.decayModEnv] = {min: -12000, max: 8000, def: -12000};
generatorLimits[generatorTypes.sustainModEnv] = {min: 0, max: 1000, def: 0};
generatorLimits[generatorTypes.releaseModEnv] = {min: -12000, max: 8000, def: -12000};
generatorLimits[generatorTypes.releaseModEnv] = {min: -12000, max: 8000, def: -7200};
// keynum to mod env
generatorLimits[generatorTypes.keyNumToModEnvHold] = {min: -1200, max: 1200, def: 0};
generatorLimits[generatorTypes.keyNumToModEnvDecay] = {min: -1200, max: 1200, def: 0};
Expand All @@ -120,7 +120,7 @@ generatorLimits[generatorTypes.attackVolEnv] = {min: -12000, max: 8000, def: -12
generatorLimits[generatorTypes.holdVolEnv] = {min: -12000, max: 5000, def: -12000};
generatorLimits[generatorTypes.decayVolEnv] = {min: -12000, max: 8000, def: -12000};
generatorLimits[generatorTypes.sustainVolEnv] = {min: 0, max: 1440, def: 0};
generatorLimits[generatorTypes.releaseVolEnv] = {min: -7200, max: 8000, def: -7200}; // prevent clicks
generatorLimits[generatorTypes.releaseVolEnv] = {min: -7200, max: 8000, def: -12000}; // prevent clicks
// keynum to vol env
generatorLimits[generatorTypes.keyNumToVolEnvHold] = {min: -1200, max: 1200, def: 0};
generatorLimits[generatorTypes.keyNumToVolEnvDecay] = {min: -1200, max: 1200, def: 0};
Expand Down
1 change: 1 addition & 0 deletions src/spessasynth_lib/soundfont/chunk/modulators.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const modulatorSources = {
pitchWheel: 14,
pitchWheelRange: 16,
channelTuning: 17,
channelTranspose: 18,
link: 127
}

Expand Down
2 changes: 1 addition & 1 deletion src/spessasynth_lib/synthetizer/synthetizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { WorkletChannel } from './worklet_system/worklet_channel.js'
import { EventHandler } from '../utils/event_handler.js'

// i mean come on
const VOICES_CAP = 800;
const VOICES_CAP = 2000;

export const DEFAULT_GAIN = 0.5;
export const DEFAULT_PERCUSSION = 9;
Expand Down
147 changes: 92 additions & 55 deletions src/spessasynth_lib/synthetizer/worklet_system/channel_processor.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@ import { getLFOValue } from './worklet_utilities/lfo.js';
import { consoleColors } from '../../utils/other.js'
import { panVoice } from './worklet_utilities/stereo_panner.js'
import { applyVolumeEnvelope } from './worklet_utilities/volume_envelope.js'
import { applyLowpassFilter } from './worklet_utilities/lowpass_filter.js'
import { getModEnvValue } from './worklet_utilities/modulation_envelope.js'

export const MIN_AUDIBLE_GAIN = 0.0001;

const CONTROLLER_TABLE_SIZE = 147;

const BLOCK_SIZE = 128;

// an array with preset default values so we can quickly use set() to reset the controllers
const resetArray = new Int16Array(146);
resetArray[midiControllers.mainVolume] = 100 << 7;
Expand All @@ -34,7 +40,9 @@ class ChannelProcessor extends AudioWorkletProcessor {
* Contains all controllers + other "not controllers" like pitch bend
* @type {Int16Array}
*/
this.midiControllers = new Int16Array(146);
this.midiControllers = new Int16Array(CONTROLLER_TABLE_SIZE);

this.emptyOutBuffer = new Float32Array(BLOCK_SIZE).buffer;

/**
* @type {Object<number, Float32Array>}
Expand Down Expand Up @@ -71,30 +79,48 @@ class ChannelProcessor extends AudioWorkletProcessor {
// note off
case workletMessageType.noteOff:
this.voices.forEach(v => {
if(v.midiNote !== data)
if(v.midiNote !== data || v.isInRelease === true)
{
return;
}
v.releaseStartTime = currentTime;
v.isInRelease = true;
v.releaseStartDb = v.currentAttenuationDb;
this.releaseVoice(v);
});
break;

case workletMessageType.killNote:
this.voices = this.voices.filter(v => v.midiNote !== data);
this.port.postMessage(this.voices.length);
break;

case workletMessageType.noteOn:
data.forEach(voice => {
const exclusive = voice.generators[generatorTypes.exclusiveClass];
if(exclusive !== 0)
{
this.voices = this.voices.filter(v => v.generators[generatorTypes.exclusiveClass] !== exclusive);
this.voices.forEach(v => {
if(v.generators[generatorTypes.exclusiveClass] === exclusive)
{
this.releaseVoice(v);
v.generators[generatorTypes.releaseVolEnv] = -12000; // make the release nearly instant
computeModulators(v, this.midiControllers);
}
})
//this.voices = this.voices.filter(v => v.generators[generatorTypes.exclusiveClass] !== exclusive);
}
computeModulators(voice, this.midiControllers);

// if both delay + attack are less than -23999, instantly ramp to attenuation (attack and delay are essentially 0)
if(voice.modulatedGenerators[generatorTypes.delayVolEnv] + voice.modulatedGenerators[generatorTypes.attackVolEnv] < -23999)
{
voice.currentAttenuationDb = voice.modulatedGenerators[generatorTypes.initialAttenuation] / 25;
}
else
{
voice.currentAttenuationDb = 100;
}
})
this.voices.push(...data);
this.port.postMessage(this.voices.length);
break;

case workletMessageType.sampleDump:
Expand All @@ -119,12 +145,35 @@ class ChannelProcessor extends AudioWorkletProcessor {
break;

case workletMessageType.stopAll:
this.voices = [];
if(data === 1)
{
// force stop all
this.voices = [];
this.port.postMessage(0);
}
else
{
this.voices.forEach(v => {
if(v.isInRelease) return;
this.releaseVoice(v)
});
}
break;
}
}
}

/**
* @param voice {WorkletVoice}
*/
releaseVoice(voice)
{
voice.releaseStartTime = currentTime;
voice.isInRelease = true;
voice.releaseStartDb = voice.currentAttenuationDb;
voice.releaseStartModEnv = voice.currentModEnvValue;
}

/**
* @param inputs {Float32Array[][]}
* @param outputs {Float32Array[][]}
Expand All @@ -146,6 +195,10 @@ class ChannelProcessor extends AudioWorkletProcessor {
}
});

if(tempV.length !== this.voices.length) {
this.port.postMessage(this.voices.length);
}

return true;
}

Expand All @@ -166,7 +219,8 @@ class ChannelProcessor extends AudioWorkletProcessor {

// calculate tuning
let cents = voice.modulatedGenerators[generatorTypes.fineTune]
+ this.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.channelTuning];
+ this.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.channelTuning]
+ this.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.channelTranspose];
let semitones = voice.modulatedGenerators[generatorTypes.coarseTune];

// calculate tuning by key
Expand All @@ -185,19 +239,22 @@ class ChannelProcessor extends AudioWorkletProcessor {
}
}

// lowpass frequency
let lowpassCents = voice.modulatedGenerators[generatorTypes.initialFilterFc];

// mod LFO
const modPitchDepth = voice.modulatedGenerators[generatorTypes.modLfoToPitch];
const modVolDepth = voice.modulatedGenerators[generatorTypes.modLfoToVolume];
const modFilterDepth = voice.modulatedGenerators[generatorTypes.modLfoToFilterFc];
let modLfoCentibels = 0;
if(modPitchDepth > 0 || modVolDepth > 0)
if(modPitchDepth + modFilterDepth + modVolDepth > 0)
{
const modStart = voice.startTime + timecentsToSeconds(voice.modulatedGenerators[generatorTypes.delayModLFO]);
const modFreqHz = absCentsToHz(voice.modulatedGenerators[generatorTypes.freqModLFO]);
const modLfo = getLFOValue(modStart, modFreqHz, currentTime);
if(modLfo) {
cents += (modLfo * modPitchDepth);
modLfoCentibels = (modLfo * modVolDepth) / 10
}
const modLfoValue = getLFOValue(modStart, modFreqHz, currentTime);
cents += modLfoValue * modPitchDepth;
modLfoCentibels = modLfoValue * modVolDepth;
lowpassCents += modLfoValue * modFilterDepth;
}

// channel vibrato (GS NRPN)
Expand All @@ -210,56 +267,33 @@ class ChannelProcessor extends AudioWorkletProcessor {
}
}

// mod env
const modEnvPitchDepth = voice.modulatedGenerators[generatorTypes.modEnvToPitch];
const modEnvFilterDepth = voice.modulatedGenerators[generatorTypes.modEnvToFilterFc];
const modEnv = getModEnvValue(voice, currentTime);
lowpassCents += modEnv * modEnvFilterDepth;
cents += modEnv * modEnvPitchDepth;

// finally calculate the playback rate
const playbackRate = Math.pow(2,(cents / 100 + semitones) / 12);
const centsTotal = ~~(cents + semitones * 100);
if(centsTotal !== voice.currentTuningCents)
{
voice.currentTuningCents = centsTotal;
voice.currentTuningCalculated = Math.pow(2, centsTotal / 1200);
}

// PANNING
const pan = ( (Math.max(-500, Math.min(500, voice.modulatedGenerators[generatorTypes.pan] )) + 500) / 1000) ; // 0 to 1


// LOWPASS
// const filterQ = voice.modulatedGenerators[generatorTypes.initialFilterQ] - 3.01; // polyphone????
// const filterQgain = Math.pow(10, filterQ / 20);
// const filterFcHz = absCentsToHz(voice.modulatedGenerators[generatorTypes.initialFilterFc]);
// // calculate coefficients
// const theta = 2 * Math.PI * filterFcHz / sampleRate;
// let a0, a1, a2, b1, b2;
// if (filterQgain <= 0)
// {
// a0 = 1;
// a1 = 0;
// a2 = 0;
// b1 = 0;
// b2 = 0;
// }
// else
// {
// const dTmp = Math.sin(theta) / (2 * filterQgain);
// if (dTmp <= -1.0)
// {
// a0 = 1;
// a1 = 0;
// a2 = 0;
// b1 = 0;
// b2 = 0;
// }
// else
// {
// const beta = 0.5 * (1 - dTmp) / (1 + dTmp);
// const gamma = (0.5 + beta) * Math.cos(theta);
// a0 = (0.5 + beta - gamma) / 2;
// a1 = 2 * a0;
// a2 = a0;
// b1 = -2 * gamma;
// b2 = 2 * beta;
// }
// }

// SYNTHESIS
const bufferOut = new Float32Array(outputLeft.length);
const bufferOut = new Float32Array(this.emptyOutBuffer);

// wavetable oscillator
getOscillatorData(voice, this.samples[voice.sample.sampleID], playbackRate, bufferOut);
getOscillatorData(voice, this.samples[voice.sample.sampleID], bufferOut);

// lowpass filter
applyLowpassFilter(voice, bufferOut, lowpassCents);

// volenv
applyVolumeEnvelope(voice, bufferOut, currentTime, modLfoCentibels, this.sampleTime);
Expand Down Expand Up @@ -311,7 +345,10 @@ class ChannelProcessor extends AudioWorkletProcessor {

resetControllers()
{
// transpose does not get affected
const transpose = this.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.channelTranspose];
this.midiControllers.set(resetArray);
this.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.channelTranspose] = transpose;
}

}
Expand Down
Loading

0 comments on commit 0771550

Please sign in to comment.