Skip to content

Commit

Permalink
Some improvements to the current synthesis engine
Browse files Browse the repository at this point in the history
*sigh* i guess i really need to go emscripten if i want to get the worklet working :/
  • Loading branch information
spessasus committed Sep 17, 2023
1 parent d100f85 commit ab0bdc9
Show file tree
Hide file tree
Showing 14 changed files with 188 additions and 132 deletions.
7 changes: 5 additions & 2 deletions src/spessasynth_lib/sequencer/sequencer.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {MIDI} from "../midi_parser/midi_loader.js";
import { DEFAULT_PERCUSSION, Synthetizer } from '../synthetizer/synthetizer.js';
import {getEvent, messageTypes, MidiMessage} from "../midi_parser/midi_message.js";
import { getEvent, messageTypes, midiControllers, MidiMessage } from '../midi_parser/midi_message.js'
import { consoleColors, formatTime } from '../utils/other.js'
import {readBytesAsUintBigEndian} from "../utils/byte_functions.js";

Expand Down Expand Up @@ -599,7 +599,10 @@ export class Sequencer {
{
clearInterval(this.playbackInterval);
this.playbackInterval = undefined;
this.synth.stopAll(true);
for (let i = 0; i < 16; i++) {
this.synth.controllerChange(i, midiControllers.sustainPedal, 0);
}
this.synth.stopAll();
if(this.MIDIout)
{
for (let c = 0; c < 16; c++)
Expand Down
84 changes: 62 additions & 22 deletions src/spessasynth_lib/soundfont/chunk/modulators.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ export const modulatorCurveTypes = {
switch: 3
}

/**
*
* type, polarity, direction
* @type {Float32Array[][][]}
*/
export const precomputedTransforms = [];
for (let i = 0; i < 4; i++) {
precomputedTransforms.push([[], []]);
}

export class Modulator{
/**
* Creates a modulator
Expand Down Expand Up @@ -81,37 +91,67 @@ export class Modulator{
return;
}

for (let i = 0; i < MOD_PRECOMPUTED_LENGTH; i++) {
this.sourceTransformed[i] = getModulatorValue(
this.sourceDirection,
this.sourceCurveType,
i / MOD_PRECOMPUTED_LENGTH,
this.sourceIsBipolar);

this.secondarySrcTransformed[i] = getModulatorValue(
this.secSrcDirection,
this.secSrcCurveType,
i / MOD_PRECOMPUTED_LENGTH,
this.secSrcIsBipolar);
if(isNaN(this.sourceTransformed[i]))
{
this.sourceTransformed[i] = 1;
}
if(isNaN(this.secondarySrcTransformed[i]))
{
this.secondarySrcTransformed[i] = 1;

// read the cached table
let sourceCached = false;
if(precomputedTransforms[this.sourceCurveType][this.sourceIsBipolar][this.sourceDirection])
{
this.sourceTransformed = new Float32Array(precomputedTransforms[this.sourceCurveType][this.sourceIsBipolar][this.sourceDirection]);
sourceCached = true;
}

let secondarySourceCached = false;
if(precomputedTransforms[this.secSrcCurveType][this.secSrcIsBipolar][this.secSrcDirection])
{
this.secondarySrcTransformed = new Float32Array(precomputedTransforms[this.secSrcCurveType][this.secSrcIsBipolar][this.secSrcDirection]);
secondarySourceCached = true;
}

if(!secondarySourceCached || !sourceCached) {
for (let i = 0; i < MOD_PRECOMPUTED_LENGTH; i++) {
if (!sourceCached) {
this.sourceTransformed[i] = getModulatorValue(
this.sourceDirection,
this.sourceCurveType,
i / MOD_PRECOMPUTED_LENGTH,
this.sourceIsBipolar);
if (isNaN(this.sourceTransformed[i])) {
this.sourceTransformed[i] = 1;
}
}

if (!secondarySourceCached) {
this.secondarySrcTransformed[i] = getModulatorValue(
this.secSrcDirection,
this.secSrcCurveType,
i / MOD_PRECOMPUTED_LENGTH,
this.secSrcIsBipolar);
if (isNaN(this.secondarySrcTransformed[i])) {
this.secondarySrcTransformed[i] = 1;
}
}
}
}

if(!sourceCached)
{
precomputedTransforms[this.sourceCurveType][this.sourceIsBipolar][this.sourceDirection] = this.sourceTransformed;
}

if(!secondarySourceCached)
{
precomputedTransforms[this.secSrcCurveType][this.secSrcIsBipolar][this.secSrcDirection] = this.secondarySrcTransformed;
}
}
}

export const defaultModulators = [
new Modulator({srcEnum: 0x0502, dest: generatorTypes.initialAttenuation, amt: 1440, secSrcEnum: 0x0, transform: 0}), // vel to attenuation
new Modulator({srcEnum: 0x0502, dest: generatorTypes.initialAttenuation, amt: 960, secSrcEnum: 0x0, transform: 0}), // vel to attenuation
new Modulator({srcEnum: 0x0081, dest: generatorTypes.vibLfoToPitch, amt: 50, secSrcEnum: 0x0, transform: 0}), // mod to vibrato
new Modulator({srcEnum: 0x0587, dest: generatorTypes.initialAttenuation, amt: 1440, secSrcEnum: 0x0, transform: 0}), // vol to attenuation
new Modulator({srcEnum: 0x0587, dest: generatorTypes.initialAttenuation, amt: 960, secSrcEnum: 0x0, transform: 0}), // vol to attenuation
new Modulator({srcEnum: 0x020E, dest: generatorTypes.fineTune, amt: 12700, secSrcEnum: 0x0010, transform: 0}), // pitch to tuning
new Modulator({srcEnum: 0x028A, dest: generatorTypes.pan, amt: 1000, secSrcEnum: 0x0, transform: 0}), // pan to uhh, pan
new Modulator({srcEnum: 0x058B, dest: generatorTypes.initialAttenuation, amt: 1440, secSrcEnum: 0x0, transform: 0}) // expression to attenuation
new Modulator({srcEnum: 0x058B, dest: generatorTypes.initialAttenuation, amt: 960, secSrcEnum: 0x0, transform: 0}) // expression to attenuation
]

console.log("%cDefault Modulators:", consoleColors.recognized, defaultModulators)
Expand Down
1 change: 1 addition & 0 deletions src/spessasynth_lib/soundfont/chunk/samples.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export class Sample {
this.sampleLength = this.sampleEndIndex - this.sampleStartIndex;
this.indexRatio = 1;
this.sampleDataArray = smplArr;
this.sampleLengthSeconds = this.sampleLength / (this.sampleRate * 2);

if (this.sampleLength < 1 || this.sampleName.substring(0, 3).toLowerCase() === "eos") {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,9 @@ export class GeneratorTranslator {
const decayTime = this.timecentsToSeconds(this.decayTime + ((60 - this.midiNote) * this.keyToVolDecay));
const sustainLevel = this._getSustainLevel() * velocityGain;
let releaseTime = this.timecentsToSeconds(this.releaseTime);
if(releaseTime > 5)
if (releaseTime > 15)
{
releaseTime = 5;
releaseTime = 15;
}
return {
attenuation: attenuation,
Expand Down
44 changes: 21 additions & 23 deletions src/spessasynth_lib/synthetizer/buffer_voice/midi_channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ export class MidiChannel {

get voicesAmount()
{
return this.notes.size;
return this.playingNotes.reduce((amt, voice) => amt + voice.sampleNodes.length, 0) + this.stoppingNotes.reduce((amt, voice) => amt + voice.sampleNodes.length, 0);
}

setVolume(volume) {
Expand Down Expand Up @@ -477,34 +477,32 @@ export class MidiChannel {
return;
}

let notes = this.playingNotes.filter(n => n.midiNote === midiNote);
if(notes.length < 1)
let note = this.playingNotes.find(n => n.midiNote === midiNote);
if(!note)
{
return
}
for(let note of notes) {

// add note as a fading one
this.stoppingNotes.push(note);
// add note as a fading one
this.stoppingNotes.push(note);

// and remove it from the main array
this.playingNotes.splice(this.playingNotes.indexOf(note), 1);
// and remove it from the main array
this.playingNotes.splice(this.playingNotes.indexOf(note), 1);

if(highPerf)
{
note.killNote().then(() => {
this.notes.delete(midiNote);
note.disconnectNote();
delete this.stoppingNotes.splice(this.stoppingNotes.indexOf(note), 1);
});
}
else {
note.stopNote().then(() => {
this.notes.delete(midiNote);
note.disconnectNote();
delete this.stoppingNotes.splice(this.stoppingNotes.indexOf(note), 1);
});
}
if(highPerf)
{
note.killNote().then(() => {
this.notes.delete(midiNote);
note.disconnectNote();
delete this.stoppingNotes.splice(this.stoppingNotes.indexOf(note), 1);
});
}
else {
note.stopNote().then(() => {
this.notes.delete(midiNote);
note.disconnectNote();
delete this.stoppingNotes.splice(this.stoppingNotes.indexOf(note), 1);
});
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ export class SynthesisModel
this.wavetableOscillator.start();
}

/**
* @returns {number} the release time
*/
stop()
{
// looping mode 3
Expand Down Expand Up @@ -230,6 +233,12 @@ export class SynthesisModel
this.lowpassFilter.frequency.setValueAtTime(this.lowpassFilter.frequency.value, this.now);
this.lowpassFilter.frequency.linearRampToValueAtTime(this.filEnv.endHz, this.now + this.filEnv.releaseTime);
}

if(this.volEnv.releaseTime > this.synthesisOptions.sample.sampleLengthSeconds && !this.wavetableOscillator.loop)
{
return this.synthesisOptions.sample.sampleLengthSeconds;
}
return this.volEnv.releaseTime;
}

disconnect()
Expand Down
5 changes: 1 addition & 4 deletions src/spessasynth_lib/synthetizer/buffer_voice/voice.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,7 @@ export class Voice
*/
async stopNote(){
// find the longest release time
let maxFadeout = Math.max(...this.sampleNodes.map(s => s.volEnv.releaseTime));

// stop every sample
this.sampleNodes.forEach(s => s.stop());
let maxFadeout = Math.max(...this.sampleNodes.map(s => s.stop()));

// so .then() can be added to delete the note after it finished
await new Promise(r => setTimeout(r, maxFadeout * 1000));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,12 @@ class ChannelProcessor extends AudioWorkletProcessor {
// note off
case workletMessageType.noteOff:
this.voices.forEach(v => {
if(v.midiNote === data)
if(v.midiNote !== data)
{
v.isInRelease = true;
v.releaseStartTime = currentTime;
return;
}
v.releaseStartTime = currentTime;
v.isInRelease = true;
});
break;

Expand Down Expand Up @@ -139,19 +140,13 @@ class ChannelProcessor extends AudioWorkletProcessor {
// MODULATORS are computed in getModulated if needed.

// TUNING
// get the root key
let key = voice.sample.rootKey;
const overrideKey = getModulated(voice, generatorTypes.overridingRootKey, this.midiControllers);
if(overrideKey !== -1)
{
key = overrideKey;
}

// calculate tuning
let cents = getModulated(voice, generatorTypes.fineTune, this.midiControllers);
let semitones = getModulated(voice, generatorTypes.coarseTune, this.midiControllers) + parseFloat(this.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.channelTuning] >> 7);

// calculate tuning by key
cents += (voice.midiNote - key) * getModulated(voice, generatorTypes.scaleTuning, this.midiControllers);
cents += (voice.targetKey - voice.sample.rootKey) * getModulated(voice, generatorTypes.scaleTuning, this.midiControllers);

// vibrato LFO
const vibratoDepth = getModulated(voice, generatorTypes.vibLfoToPitch, this.midiControllers);
Expand All @@ -170,7 +165,7 @@ class ChannelProcessor extends AudioWorkletProcessor {
const modPitchDepth = getModulated(voice, generatorTypes.modLfoToPitch, this.midiControllers);
const modVolDepth = getModulated(voice, generatorTypes.modLfoToVolume, this.midiControllers);
let modLfoCentibels = 0;
if(modPitchDepth + modVolDepth > 0)
if(modPitchDepth > 0 || modVolDepth > 0)
{
const modStart = voice.startTime + timecentsToSeconds(getModulated(voice, generatorTypes.delayModLFO, this.midiControllers));
const modFreqHz = absCentsToHz(getModulated(voice, generatorTypes.freqModLFO, this.midiControllers));
Expand Down Expand Up @@ -209,20 +204,6 @@ class ChannelProcessor extends AudioWorkletProcessor {
decay = timecentsToSeconds(getModulated(voice, generatorTypes.decayVolEnv, this.midiControllers) + ((60 - voice.midiNote) * getModulated(voice, generatorTypes.keyNumToVolEnvDecay, this.midiControllers)));
}

// WAVETABLE OSCILLATOR
// get offsets
let loopStart = voice.sample.loopStart + getModulated(voice, generatorTypes.startloopAddrsOffset, this.midiControllers) + (getModulated(voice, generatorTypes.startloopAddrsCoarseOffset, this.midiControllers) * 32768);
let loopEnd = voice.sample.loopEnd + getModulated(voice, generatorTypes.endloopAddrsOffset, this.midiControllers) + (getModulated(voice, generatorTypes.endloopAddrsCoarseOffset, this.midiControllers) * 32768);
const startOffset = getModulated(voice, generatorTypes.startAddrsOffset, this.midiControllers) + (getModulated(voice, generatorTypes.startAddrsCoarseOffset, this.midiControllers) * 32768);
const endIndex = getModulated(voice, generatorTypes.endAddrOffset, this.midiControllers) + (getModulated(voice, generatorTypes.endAddrsCoarseOffset, this.midiControllers) * 32768) + this.samples[voice.sample.sampleID].length;
const mode = getModulated(voice, generatorTypes.sampleModes, this.midiControllers);
const loop = mode === 1 || (mode === 3 && !voice.isInRelease);
if(loopStart >= loopEnd)
{
loopStart = voice.sample.loopStart;
loopEnd = voice.sample.loopEnd;
}

// PANNING
const pan = ( (Math.max(-500, Math.min(500, getModulated(voice, generatorTypes.pan, this.midiControllers) )) + 500) / 1000) ; // 0 to 1
const panLeft = Math.cos(HALF_PI * pan);
Expand All @@ -233,14 +214,10 @@ class ChannelProcessor extends AudioWorkletProcessor {
for (let outputSampleIndex = 0; outputSampleIndex < outputLeft.length; outputSampleIndex++) {

// Read the sample
let sample = getOscillatorValue(voice,
let sample = getOscillatorValue(
voice,
this.samples[voice.sample.sampleID],
playbackRate,
startOffset,
endIndex,
loop,
loopStart,
loopEnd
playbackRate
);

// apply the volenv
Expand Down
Loading

0 comments on commit ab0bdc9

Please sign in to comment.