diff --git a/README.md b/README.md index 4a62d22a..681939e7 100644 --- a/README.md +++ b/README.md @@ -65,4 +65,11 @@ The program is divided into parts: - Vibrato LFO (freq, depth and delay) **Including the Mod wheel support!** - Scale tuning, fine tune and coarse tune - exclusive class (although sometimes broken) -- pan \ No newline at end of file +- pan + +#### todo +- make the worklet system work +- make the worklet system perform good +- implement the worklet system +- port the worklet system to emscripten (maybe) +- reverb that actually runs well \ No newline at end of file diff --git a/src/spessasynth_lib/soundfont/chunk/modulators.js b/src/spessasynth_lib/soundfont/chunk/modulators.js index bedecb9b..43915244 100644 --- a/src/spessasynth_lib/soundfont/chunk/modulators.js +++ b/src/spessasynth_lib/soundfont/chunk/modulators.js @@ -1,9 +1,5 @@ import {signedInt16, readByte, readBytesAsUintLittleEndian} from "../../utils/byte_functions.js"; import { ShiftableByteArray } from '../../utils/shiftable_array.js'; -import { - getModulatorValue, - MOD_PRECOMPUTED_LENGTH, -} from '../../synthetizer/worklet_channel/worklet_utilities/modulator_curves.js' import { generatorTypes } from './generators.js' import { consoleColors } from '../../utils/other.js' @@ -47,13 +43,13 @@ export class Modulator{ this.modulatorSource = dataArray.srcEnum; this.modulatorDestination = dataArray.dest; this.modulationSecondarySrc = dataArray.secSrcEnum; - this.modulationAmount = dataArray.amt; + this.transformAmount = dataArray.amt; this.transformType = dataArray.transform; } else { this.modulatorSource = readBytesAsUintLittleEndian(dataArray, 2); this.modulatorDestination = readBytesAsUintLittleEndian(dataArray, 2); - this.modulationAmount = signedInt16(readByte(dataArray), readByte(dataArray)); + this.transformAmount = signedInt16(readByte(dataArray), readByte(dataArray)); this.modulationSecondarySrc = readBytesAsUintLittleEndian(dataArray, 2); this.transformType = readBytesAsUintLittleEndian(dataArray, 2); } @@ -64,84 +60,20 @@ export class Modulator{ } // decode the source - this.sourceIsBipolar = this.modulatorSource >> 9 & 1; + this.sourcePolarity = this.modulatorSource >> 9 & 1; this.sourceDirection = this.modulatorSource >> 8 & 1; this.sourceUsesCC = this.modulatorSource >> 7 & 1; this.sourceIndex = this.modulatorSource & 127; this.sourceCurveType = this.modulatorSource >> 10 & 3; // decode the secondary source - this.secSrcIsBipolar = this.modulationSecondarySrc >> 9 & 1; + this.secSrcPolarity = this.modulationSecondarySrc >> 9 & 1; this.secSrcDirection = this.modulationSecondarySrc >> 8 & 1; this.secSrcUsesCC = this.modulationSecondarySrc >> 7 & 1; this.secSrcIndex = this.modulationSecondarySrc & 127; this.secSrcCurveType = this.modulationSecondarySrc >> 10 & 3; - this.precomputeModulatorTransform(); - } - - // precompute the values on sf load - precomputeModulatorTransform() - { - this.sourceTransformed = new Float32Array(MOD_PRECOMPUTED_LENGTH); - this.secondarySrcTransformed = new Float32Array(MOD_PRECOMPUTED_LENGTH); - - if(this.modulationAmount < 1) - { - return; - } - - - // 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; - } + //this.precomputeModulatorTransform(); } } diff --git a/src/spessasynth_lib/soundfont/chunk/presets.js b/src/spessasynth_lib/soundfont/chunk/presets.js index e576d58e..83e0bbde 100644 --- a/src/spessasynth_lib/soundfont/chunk/presets.js +++ b/src/spessasynth_lib/soundfont/chunk/presets.js @@ -182,7 +182,7 @@ export class Preset { if(identicalInstrumentModulator) { // sum the amounts - identicalInstrumentModulator.modulationAmount += mod.modulationAmount; + identicalInstrumentModulator.modulationAmount += mod.transformAmount; } else { diff --git a/src/spessasynth_lib/synthetizer/buffer_voice/generator_translator.js b/src/spessasynth_lib/synthetizer/native_system/generator_translator.js similarity index 100% rename from src/spessasynth_lib/synthetizer/buffer_voice/generator_translator.js rename to src/spessasynth_lib/synthetizer/native_system/generator_translator.js diff --git a/src/spessasynth_lib/synthetizer/buffer_voice/midi_channel.js b/src/spessasynth_lib/synthetizer/native_system/midi_channel.js similarity index 98% rename from src/spessasynth_lib/synthetizer/buffer_voice/midi_channel.js rename to src/spessasynth_lib/synthetizer/native_system/midi_channel.js index abfba01c..a552b73f 100644 --- a/src/spessasynth_lib/synthetizer/buffer_voice/midi_channel.js +++ b/src/spessasynth_lib/synthetizer/native_system/midi_channel.js @@ -1,4 +1,4 @@ -import {Voice} from "./voice.js"; +import {VoiceGroup} from "./voice_group.js"; import {Preset} from "../../soundfont/chunk/presets.js"; import { consoleColors } from '../../utils/other.js' import { midiControllers } from '../../midi_parser/midi_message.js' @@ -71,12 +71,12 @@ export class MidiChannel { /** * Current playing notes - * @type {Voice[]} + * @type {VoiceGroup[]} */ this.playingNotes = []; /** * Notes that are stopping and are about to get deleted - * @type {Voice[]} + * @type {VoiceGroup[]} */ this.stoppingNotes = []; @@ -250,7 +250,7 @@ export class MidiChannel { this.notes.add(midiNote); this.receivedNotes.add(midiNote); - let note = new Voice(midiNote, velocity, this.panner, this.preset, this.vibrato, this.channelTuningRatio, this.modulation); + let note = new VoiceGroup(midiNote, velocity, this.panner, this.preset, this.vibrato, this.channelTuningRatio, this.modulation); let exclusives = note.startNote(debugInfo); const bendRatio = (this.pitchBend / 8192) * this.channelPitchBendRange; diff --git a/src/spessasynth_lib/synthetizer/buffer_voice/synthesis_model.js b/src/spessasynth_lib/synthetizer/native_system/voice.js similarity index 98% rename from src/spessasynth_lib/synthetizer/buffer_voice/synthesis_model.js rename to src/spessasynth_lib/synthetizer/native_system/voice.js index bd616a61..11cbe466 100644 --- a/src/spessasynth_lib/synthetizer/buffer_voice/synthesis_model.js +++ b/src/spessasynth_lib/synthetizer/native_system/voice.js @@ -1,6 +1,6 @@ import { GeneratorTranslator } from './generator_translator.js'; -export class SynthesisModel +export class Voice { /** * Creates a new instance of a single sample @@ -240,7 +240,7 @@ export class SynthesisModel if(!this.wavetableOscillator.loop) { // if not looping, return the sample length - return this.synthesisOptions.sample.sampleLengthSeconds; + return Math.min(this.synthesisOptions.sample.sampleLengthSeconds, this.volEnv.releaseTime); } return this.volEnv.releaseTime; } diff --git a/src/spessasynth_lib/synthetizer/buffer_voice/voice.js b/src/spessasynth_lib/synthetizer/native_system/voice_group.js similarity index 96% rename from src/spessasynth_lib/synthetizer/buffer_voice/voice.js rename to src/spessasynth_lib/synthetizer/native_system/voice_group.js index 3807812d..a88a3b79 100644 --- a/src/spessasynth_lib/synthetizer/buffer_voice/voice.js +++ b/src/spessasynth_lib/synthetizer/native_system/voice_group.js @@ -1,10 +1,10 @@ import {Preset} from "../../soundfont/chunk/presets.js"; -import { SynthesisModel } from './synthesis_model.js'; +import { Voice } from './voice.js'; -export class Voice +export class VoiceGroup { /** - * Create a note + * Create a note (a group of voices) * @param midiNote {number} * @param targetVelocity {number} * @param node {AudioNode} @@ -43,10 +43,10 @@ export class Voice this.exclusives = new Set(); /** - * @type {SynthesisModel[]} + * @type {Voice[]} */ this.sampleNodes = samples.map(samAndGen => { - const sm = new SynthesisModel( + const sm = new Voice( samAndGen, midiNote, node, diff --git a/src/spessasynth_lib/synthetizer/synthetizer.js b/src/spessasynth_lib/synthetizer/synthetizer.js index 4ad466e7..509b1324 100644 --- a/src/spessasynth_lib/synthetizer/synthetizer.js +++ b/src/spessasynth_lib/synthetizer/synthetizer.js @@ -1,9 +1,9 @@ -import {MidiChannel} from "./buffer_voice/midi_channel.js"; +import {MidiChannel} from "./native_system/midi_channel.js"; import {SoundFont2} from "../soundfont/soundfont_parser.js"; import {ShiftableByteArray} from "../utils/shiftable_array.js"; import { arrayToHexString, consoleColors } from '../utils/other.js'; import { midiControllers } from '../midi_parser/midi_message.js' -import { WorkletChannel } from './worklet_channel/worklet_channel.js' +import { WorkletChannel } from './worklet_system/worklet_channel.js' import { EventHandler } from '../utils/event_handler.js' // i mean come on @@ -105,11 +105,14 @@ export class Synthetizer { } /* - * Prevents any further changes to the vibrato via NRPN messages + * Prevents any further changes to the vibrato via NRPN messages and resets it to none */ - lockChannelVibrato() + lockAndResetChannelVibrato() { - this.midiChannels.forEach(c => c.lockVibrato = true); + this.midiChannels.forEach(c => { + c.lockVibrato = true; + c.vibrato = {depth: 0, rate: 0, delay: 0}; + }); } /** @@ -142,15 +145,9 @@ export class Synthetizer { stopAll(force=false) { console.log("%cStop all received!", consoleColors.info); for (let channel of this.midiChannels) { - for(const note of channel.notes) - { - this.eventHandler.callEvent("noteoff", { - midiNote: note, - channel: channel.channelNumber - 1 - }); - } channel.stopAll(force); } + this.eventHandler.callEvent("stopall", {}); } /** diff --git a/src/spessasynth_lib/synthetizer/worklet_channel/worklet_utilities/volume_envelope.js b/src/spessasynth_lib/synthetizer/worklet_channel/worklet_utilities/volume_envelope.js deleted file mode 100644 index b740a95d..00000000 --- a/src/spessasynth_lib/synthetizer/worklet_channel/worklet_utilities/volume_envelope.js +++ /dev/null @@ -1,77 +0,0 @@ -import { MIN_AUDIBLE_GAIN } from '../channel_processor.js'; -import { timecentsToSeconds } from './unit_converter.js' -import { getModulated } from './worklet_modulator.js' -import { generatorTypes } from '../../../soundfont/chunk/generators.js' - -export const volumeEnvelopePhases = { - delay: 0, - attack: 1, - hold: 2, - decay: 3, - release: 4 -} - -const releaseExpoLookupTable = new Float32Array(1001); -for (let i = 0; i < 1001; i++) { - releaseExpoLookupTable[i] = Math.pow(0.000001, (i / 1000)); -} - -/** - * @param releaseTime {number} the length of release phase - * @param elapsed {number} the amount of seconds passed since the release start - * @returns {Number} - */ -export function getVolEnvReleaseMultiplier(releaseTime, elapsed) -{ - const gain = releaseExpoLookupTable[Math.trunc((elapsed / releaseTime) * 1000)]; - //const gain = (1 - elapsed / (releaseTime * 0.2)) - return gain > MIN_AUDIBLE_GAIN ? gain : -1; -} - -/** - * @param delay {number} seconds - * @param attack {number} seconds - * @param peak {number} gain - * @param hold {number} seconds - * @param sustain {number} gain - * @param decay {number} seconds - * @param startTime {number} seconds - * @param currentTime {number} seconds - * @returns {number} the gain or -1 if inaudible - */ -export function getVolumeEnvelopeValue(delay, attack, peak, hold, sustain, decay, startTime, currentTime) { - const attackStart = startTime + delay; - const attackEnd = attackStart + attack; - const holdEnd = attackEnd + hold; - const decayEnd = holdEnd + decay; - - // delay time - if (currentTime < attackStart) { - return 0; - } - // attack time - else if (currentTime < attackEnd) { - // linear - return ((currentTime - attackStart) / attack) * peak; - } - // hold time - else if (currentTime < holdEnd) { - return peak; - } - // decay time - else if (currentTime < decayEnd && (peak !== sustain)) { - // exponential - const gain = releaseExpoLookupTable[Math.trunc(((currentTime - holdEnd) / decay) * 1000)] * (peak - sustain) + sustain - if (gain < MIN_AUDIBLE_GAIN) { - return -0.001; - } - return gain; - } - // sustain - else { - if (sustain < MIN_AUDIBLE_GAIN) { - return -0.001; - } - return sustain; - } -} \ No newline at end of file diff --git a/src/spessasynth_lib/synthetizer/worklet_channel/worklet_utilities/wavetable_oscillator.js b/src/spessasynth_lib/synthetizer/worklet_channel/worklet_utilities/wavetable_oscillator.js deleted file mode 100644 index 143a6f10..00000000 --- a/src/spessasynth_lib/synthetizer/worklet_channel/worklet_utilities/wavetable_oscillator.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @param voice {WorkletVoice} - * @param sampleData {Float32Array} - * @param playbackRate {number} - * @returns {number} - */ -export function getOscillatorValue(voice, sampleData, playbackRate) -{ - const cur = voice.sample.cursor; - - // linear interpolation - const floor = ~~cur; - const ceil = floor + 1; - const fraction = cur - floor; - - // grab the samples and interpolate - const upper = sampleData[ceil]; - const lower = sampleData[floor]; - - // advance the sample - voice.sample.cursor += voice.sample.playbackStep * playbackRate; - - if((voice.sample.loopingMode === 1) || (voice.sample.loopingMode === 3 && !voice.isInRelease)) - { - if (voice.sample.cursor > voice.sample.loopEnd) { - voice.sample.cursor -= voice.sample.loopEnd - voice.sample.loopStart; - } - } - else - { - // if not looping, flag the voice as finished - if(ceil >= voice.sample.end) - { - voice.finished = true; - return 0; - } - } - - return (lower + (upper - lower) * fraction); -} \ No newline at end of file diff --git a/src/spessasynth_lib/synthetizer/worklet_channel/worklet_utilities/worklet_modulator.js b/src/spessasynth_lib/synthetizer/worklet_channel/worklet_utilities/worklet_modulator.js deleted file mode 100644 index 364db65a..00000000 --- a/src/spessasynth_lib/synthetizer/worklet_channel/worklet_utilities/worklet_modulator.js +++ /dev/null @@ -1,132 +0,0 @@ -import { NON_CC_INDEX_OFFSET } from '../worklet_channel.js' -import { modulatorSources } from '../../../soundfont/chunk/modulators.js' - -/** - * @typedef {{ - * transformAmount: number, - * transformType: 0|2, - * - * sourceTransformed: Float32Array - * sourceIndex: number, - * sourceUsesCC: number, - * - * secondarySrcTransformed: Float32Array, - * secondarySrcIndex: number, - * secondarySrcUsesCC: number - * }} WorkletModulator - */ - -/** - * - * @param controllerTable {Int16Array} all midi controllers as 14bit values + the non controller indexes, starting at 128 - * @param modulator {WorkletModulator} - * @param midiNote {number} - * @param velocity {number} - * @returns {number} the computed value - */ -export function computeWorkletModulator(controllerTable, modulator, midiNote, velocity) -{ - if(modulator.transformAmount === 0) - { - return 0; - } - // mapped to 0-16384 - let rawSourceValue = 0; - if(modulator.sourceUsesCC) - { - rawSourceValue = controllerTable[modulator.sourceIndex]; - } - else - { - const index = modulator.sourceIndex + NON_CC_INDEX_OFFSET; - switch (modulator.sourceIndex) - { - case modulatorSources.noController: - return 0;// fluid_mod.c line 374 (0 times secondary times amount is still zero) - - case modulatorSources.noteOnKeyNum: - rawSourceValue = midiNote << 7; - break; - - case modulatorSources.noteOnVelocity: - case modulatorSources.polyPressure: - rawSourceValue = velocity << 7; - break; - - default: - rawSourceValue = controllerTable[index]; // use the 7 bit value - break; - } - - } - - const sourceValue = modulator.sourceTransformed[rawSourceValue]; - - // mapped to 0-127 - let rawSecondSrcValue; - if(modulator.secondarySrcUsesCC) - { - rawSecondSrcValue = controllerTable[modulator.secondarySrcTransformed]; - } - else - { - const index = modulator.secondarySrcIndex + NON_CC_INDEX_OFFSET; - switch (modulator.secondarySrcIndex) - { - case modulatorSources.noController: - rawSecondSrcValue = 16383;// fluid_mod.c line 376 - break; - - case modulatorSources.noteOnKeyNum: - rawSecondSrcValue = midiNote << 7; - break; - - case modulatorSources.noteOnVelocity: - case modulatorSources.polyPressure: - rawSecondSrcValue = velocity << 7; - break; - - default: - rawSecondSrcValue = controllerTable[index]; - } - - } - const secondSrcValue = modulator.secondarySrcTransformed[rawSecondSrcValue]; - - - // compute the modulator - const computedValue = sourceValue * secondSrcValue * modulator.transformAmount; - - if(modulator.transformType === 2) - { - // abs value - return Math.abs(computedValue); - } - return computedValue; -} - -/** - * @param voice {WorkletVoice} - * @param generatorType {number} - * @param controllerTable {Int16Array} - * @returns {number} the computed number - */ -export function getModulated(voice, generatorType, controllerTable) { - const modLen = voice.modulators[generatorType].length; - if (modLen < 1) { - // if no mods, just return gen - return voice.generators[generatorType]; - } - else if(modLen === 1) - { - return voice.generators[generatorType] + computeWorkletModulator(controllerTable, voice.modulators[generatorType][0], voice.midiNote, voice.velocity) - } - else { - // if mods, sum them - let sum = voice.generators[generatorType]; - for (let i = 0; i < modLen; i++) { - sum += computeWorkletModulator(controllerTable, voice.modulators[generatorType][i], voice.midiNote, voice.velocity); - } - return sum; - } -} \ No newline at end of file diff --git a/src/spessasynth_lib/synthetizer/worklet_channel/channel_processor.js b/src/spessasynth_lib/synthetizer/worklet_system/channel_processor.js similarity index 56% rename from src/spessasynth_lib/synthetizer/worklet_channel/channel_processor.js rename to src/spessasynth_lib/synthetizer/worklet_system/channel_processor.js index 4526f2f8..801f7700 100644 --- a/src/spessasynth_lib/synthetizer/worklet_channel/channel_processor.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/channel_processor.js @@ -1,33 +1,47 @@ import { NON_CC_INDEX_OFFSET, workletMessageType } from './worklet_channel.js'; import { midiControllers } from '../../midi_parser/midi_message.js'; import { generatorTypes } from '../../soundfont/chunk/generators.js'; -import { getOscillatorValue } from './worklet_utilities/wavetable_oscillator.js'; +import { getOscillatorData } from './worklet_utilities/wavetable_oscillator.js' import { modulatorSources } from '../../soundfont/chunk/modulators.js'; -import { getModulated } from './worklet_utilities/worklet_modulator.js' -import { - getVolEnvReleaseMultiplier, - getVolumeEnvelopeValue, -} from './worklet_utilities/volume_envelope.js' +import { computeModulators } from './worklet_utilities/worklet_modulator.js' import { absCentsToHz, - decibelAttenuationToGain, - HALF_PI, timecentsToSeconds, } from './worklet_utilities/unit_converter.js' 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' export const MIN_AUDIBLE_GAIN = 0.0001; +// 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; +resetArray[midiControllers.expressionController] = 127 << 7; +resetArray[midiControllers.pan] = 64 << 7; + +resetArray[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel] = 8192; +resetArray[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheelRange] = 2 << 7; +resetArray[NON_CC_INDEX_OFFSET + modulatorSources.channelPressure] = 127 << 7; +resetArray[NON_CC_INDEX_OFFSET + modulatorSources.channelTuning] = 0; + class ChannelProcessor extends AudioWorkletProcessor { constructor() { super(); + /** + * Contains all controllers + other "not controllers" like pitch bend + * @type {Int16Array} + */ + this.midiControllers = new Int16Array(146); + /** * @type {Object} */ this.samples = {}; + // in seconds, time between two samples (very, very short) this.sampleTime = 1 / sampleRate; this.resetControllers(); @@ -63,6 +77,7 @@ class ChannelProcessor extends AudioWorkletProcessor { } v.releaseStartTime = currentTime; v.isInRelease = true; + v.releaseStartDb = v.currentAttenuationDb; }); break; @@ -77,6 +92,7 @@ class ChannelProcessor extends AudioWorkletProcessor { { this.voices = this.voices.filter(v => v.generators[generatorTypes.exclusiveClass] !== exclusive); } + computeModulators(voice, this.midiControllers); }) this.voices.push(...data); break; @@ -91,6 +107,7 @@ class ChannelProcessor extends AudioWorkletProcessor { case workletMessageType.ccChange: this.midiControllers[data[0]] = data[1]; + this.voices.forEach(v => computeModulators(v, this.midiControllers)); break; case workletMessageType.setChannelVibrato: @@ -99,6 +116,11 @@ class ChannelProcessor extends AudioWorkletProcessor { case workletMessageType.clearCache: this.samples = []; + break; + + case workletMessageType.stopAll: + this.voices = []; + break; } } } @@ -109,6 +131,10 @@ class ChannelProcessor extends AudioWorkletProcessor { * @returns {boolean} */ process(inputs, outputs) { + if(this.voices.length < 1) + { + return true; + } const channels = outputs[0]; const tempV = this.voices; this.voices = []; @@ -136,24 +162,22 @@ class ChannelProcessor extends AudioWorkletProcessor { return; } - - // MODULATORS are computed in getModulated if needed. - // TUNING // 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); + let cents = voice.modulatedGenerators[generatorTypes.fineTune] + + this.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.channelTuning]; + let semitones = voice.modulatedGenerators[generatorTypes.coarseTune]; // calculate tuning by key - cents += (voice.targetKey - voice.sample.rootKey) * getModulated(voice, generatorTypes.scaleTuning, this.midiControllers); + cents += (voice.targetKey - voice.sample.rootKey) * voice.modulatedGenerators[generatorTypes.scaleTuning]; // vibrato LFO - const vibratoDepth = getModulated(voice, generatorTypes.vibLfoToPitch, this.midiControllers); + const vibratoDepth = voice.modulatedGenerators[generatorTypes.vibLfoToPitch]; if(vibratoDepth > 0) { - const vibStart = voice.startTime + timecentsToSeconds(getModulated(voice, generatorTypes.delayVibLFO, this.midiControllers)); - const vibFreqHz = absCentsToHz(getModulated(voice, generatorTypes.freqVibLFO, this.midiControllers)); + const vibStart = voice.startTime + timecentsToSeconds(voice.modulatedGenerators[generatorTypes.delayVibLFO]); + const vibFreqHz = absCentsToHz(voice.modulatedGenerators[generatorTypes.freqVibLFO]); const lfoVal = getLFOValue(vibStart, vibFreqHz, currentTime); if(lfoVal) { @@ -162,13 +186,13 @@ class ChannelProcessor extends AudioWorkletProcessor { } // mod LFO - const modPitchDepth = getModulated(voice, generatorTypes.modLfoToPitch, this.midiControllers); - const modVolDepth = getModulated(voice, generatorTypes.modLfoToVolume, this.midiControllers); + const modPitchDepth = voice.modulatedGenerators[generatorTypes.modLfoToPitch]; + const modVolDepth = voice.modulatedGenerators[generatorTypes.modLfoToVolume]; let modLfoCentibels = 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)); + 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); @@ -189,31 +213,14 @@ class ChannelProcessor extends AudioWorkletProcessor { // finally calculate the playback rate const playbackRate = Math.pow(2,(cents / 100 + semitones) / 12); - // VOLUME ENVELOPE - let attenuation, sustain, delay, attack, hold, decay, release; - attenuation = decibelAttenuationToGain((getModulated(voice, generatorTypes.initialAttenuation, this.midiControllers) / 25) + modLfoCentibels); - if(voice.isInRelease) - { - release = timecentsToSeconds(getModulated(voice, generatorTypes.releaseVolEnv, this.midiControllers)); - } - else { - sustain = attenuation * decibelAttenuationToGain(getModulated(voice, generatorTypes.sustainVolEnv, this.midiControllers) / 10); - delay = timecentsToSeconds(getModulated(voice, generatorTypes.delayVolEnv, this.midiControllers)); - attack = timecentsToSeconds(getModulated(voice, generatorTypes.attackVolEnv, this.midiControllers)); - hold = timecentsToSeconds(getModulated(voice, generatorTypes.holdVolEnv, this.midiControllers) + ((60 - voice.midiNote) * getModulated(voice, generatorTypes.keyNumToVolEnvHold, this.midiControllers))); - decay = timecentsToSeconds(getModulated(voice, generatorTypes.decayVolEnv, this.midiControllers) + ((60 - voice.midiNote) * getModulated(voice, generatorTypes.keyNumToVolEnvDecay, this.midiControllers))); - } - // 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); - const panRight = Math.sin(HALF_PI * pan); + const pan = ( (Math.max(-500, Math.min(500, voice.modulatedGenerators[generatorTypes.pan] )) + 500) / 1000) ; // 0 to 1 // LOWPASS - // const filterQ = getModulated(voice, generatorTypes.initialFilterQ, this.midiControllers) - 3.01; // polyphone???? + // const filterQ = voice.modulatedGenerators[generatorTypes.initialFilterQ] - 3.01; // polyphone???? // const filterQgain = Math.pow(10, filterQ / 20); - // const filterFcHz = absCentsToHz(getModulated(voice, generatorTypes.initialFilterFc, this.midiControllers)); + // const filterFcHz = absCentsToHz(voice.modulatedGenerators[generatorTypes.initialFilterFc]); // // calculate coefficients // const theta = 2 * Math.PI * filterFcHz / sampleRate; // let a0, a1, a2, b1, b2; @@ -249,62 +256,62 @@ class ChannelProcessor extends AudioWorkletProcessor { // } // SYNTHESIS - let actualTime = currentTime; - for (let outputSampleIndex = 0; outputSampleIndex < outputLeft.length; outputSampleIndex++) { - - // Read the sample - let sample = getOscillatorValue( - voice, - this.samples[voice.sample.sampleID], - playbackRate - ); - - // apply the volenv - if(voice.isInRelease) - { - voice.volEnvGain = attenuation * getVolEnvReleaseMultiplier(release, actualTime - voice.releaseStartTime); - } - else { - voice.currentGain = getVolumeEnvelopeValue( - delay, - attack, - attenuation, - hold, - sustain, - decay, - voice.startTime, - actualTime); - - voice.volEnvGain = voice.currentGain; - } - if(voice.volEnvGain < 0) - { - voice.finished = true; - return; - } - - sample *= voice.volEnvGain; - - // pan the voice and write out - outputLeft[outputSampleIndex] += sample * panLeft; - outputRight[outputSampleIndex] += sample * panRight; - - actualTime += this.sampleTime; - } + const bufferOut = new Float32Array(outputLeft.length); + + // wavetable oscillator + getOscillatorData(voice, this.samples[voice.sample.sampleID], playbackRate, bufferOut); + + // volenv + applyVolumeEnvelope(voice, bufferOut, currentTime, modLfoCentibels, this.sampleTime); + + // pan the voice and write out + panVoice(pan, bufferOut, outputLeft, outputRight); + + // apply the volEnv + // for (let outputSampleIndex = 0; outputSampleIndex < outputLeft.length; outputSampleIndex++) { + // + // // Read the sample + // let sample = getOscillatorValue( + // voice, + // this.samples[voice.sample.sampleID], + // playbackRate + // ); + // + // // apply the volenv + // if(voice.isInRelease) + // { + // voice.volEnvGain = attenuation * getVolEnvReleaseMultiplier(release, actualTime - voice.releaseStartTime); + // } + // else { + // voice.currentGain = getVolumeEnvelopeValue( + // delay, + // attack, + // attenuation, + // hold, + // sustain, + // decay, + // voice.startTime, + // actualTime); + // + // voice.volEnvGain = voice.currentGain; + // } + // if(voice.volEnvGain < 0) + // { + // voice.finished = true; + // return; + // } + // + // sample *= voice.volEnvGain; + // + // + // + // actualTime += this.sampleTime; + // } } resetControllers() { - // Create an Int16Array with 127 elements - this.midiControllers = new Int16Array(146); - this.midiControllers[midiControllers.mainVolume] = 100 << 7; - this.midiControllers[midiControllers.expressionController] = 127 << 7; - this.midiControllers[midiControllers.pan] = 64 << 7; - - this.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel] = 8192; - this.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheelRange] = 2 << 7; - this.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.channelPressure] = 127 << 7; - this.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.channelTuning] = 0; + this.midiControllers.set(resetArray); } } diff --git a/src/spessasynth_lib/synthetizer/worklet_channel/generator_handler.js b/src/spessasynth_lib/synthetizer/worklet_system/generator_handler.js similarity index 100% rename from src/spessasynth_lib/synthetizer/worklet_channel/generator_handler.js rename to src/spessasynth_lib/synthetizer/worklet_system/generator_handler.js diff --git a/src/spessasynth_lib/synthetizer/worklet_channel/voice2.js b/src/spessasynth_lib/synthetizer/worklet_system/voice2.js similarity index 98% rename from src/spessasynth_lib/synthetizer/worklet_channel/voice2.js rename to src/spessasynth_lib/synthetizer/worklet_system/voice2.js index 386b2a41..293ff624 100644 --- a/src/spessasynth_lib/synthetizer/worklet_channel/voice2.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/voice2.js @@ -21,7 +21,7 @@ export class Voice2 extends AudioWorkletNode{ * @param tuningRatio {number} the note's initial tuning ratio */ constructor(midiNote, targetVelocity, node, preset, vibratoOptions, tuningRatio) { - super(node.context,"worklet_channel-processor" , { + super(node.context,"worklet_system-processor" , { outputChannelCount: [2] }); this.midiNote = midiNote; diff --git a/src/spessasynth_lib/synthetizer/worklet_channel/worklet_channel.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_channel.js similarity index 92% rename from src/spessasynth_lib/synthetizer/worklet_channel/worklet_channel.js rename to src/spessasynth_lib/synthetizer/worklet_system/worklet_channel.js index 6330fbb7..7afdd9ee 100644 --- a/src/spessasynth_lib/synthetizer/worklet_channel/worklet_channel.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_channel.js @@ -12,12 +12,13 @@ * @typedef {{ * sample: WorkletSample, * generators: Int16Array, - * modulators: WorkletModulator[][], + * modulators: Modulator[], + * modulatedGenerators: Int16Array, * finished: boolean, * isInRelease: boolean, * velocity: number, - * currentGain: number, - * volEnvGain: number, + * currentAttenuationDb: number, + * releaseStartDb: number, * startTime: number, * midiNote: number, * releaseStartTime: number, @@ -54,11 +55,12 @@ export const workletMessageType = { ccReset: 5, setChannelVibrato: 6, clearCache: 7, + stopAll: 8 }; /** * @typedef {{ - * messageType: 0|1|2|3|4|5|6|7, + * messageType: 0|1|2|3|4|5|6|7|8, * messageData: ( * number[] * |WorkletVoice[] @@ -76,6 +78,7 @@ export const workletMessageType = { * 5 - controllers reset * 6 - channel vibrato -> {frequencyHz: number, depthCents: number, delaySeconds: number} * 7 - clear cached samples + * 8 - stop all notes */ @@ -343,42 +346,20 @@ export class WorkletChannel { velocity = generators[generatorTypes.velocity]; } - /** - * grouped by destination - * @type {WorkletModulator[][]} - */ - const modulators = [] - for (let i = 0; i < 60; i++) { - modulators.push([]); - } - sampleAndGenerators.modulators.forEach(mod => { - modulators[mod.modulatorDestination].push({ - transformAmount: mod.modulationAmount, - transformType: mod.transformType, - - sourceIndex: mod.sourceIndex, - sourceUsesCC: mod.sourceUsesCC, - sourceTransformed: mod.sourceTransformed, - - secondarySrcIndex: mod.secSrcIndex, - secondarySrcUsesCC: mod.secSrcUsesCC, - secondarySrcTransformed: mod.secondarySrcTransformed - }); - }); - this.actualVoices.push(midiNote); return { generators: generators, + modulatedGenerators: new Int16Array(60), sample: workletSample, - modulators: modulators, + modulators: sampleAndGenerators.modulators, finished: false, velocity: velocity, - currentGain: 0, - volEnvGain: 0, + currentAttenuationDb: 100, + releaseStartDb: 0, midiNote: midiNote, startTime: this.ctx.currentTime, isInRelease: false, - releaseStartTime: 0, + releaseStartTime: -1, targetKey: targetKey, }; @@ -601,10 +582,10 @@ export class WorkletChannel { // semitones this.channelTuningSemitones = dataValue - 64; console.log("tuning", this.channelTuningSemitones, "for", this.channelNumber); - this.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.channelTuning] = this.channelTuningSemitones + this.channelTranspose << 7; + this.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.channelTuning] = (this.channelTuningSemitones + this.channelTranspose) * 100; this.post({ messageType: workletMessageType.ccChange, - messageData: [NON_CC_INDEX_OFFSET + modulatorSources.channelTuning, this.channelTuningSemitones + this.channelTranspose << 7] + messageData: [NON_CC_INDEX_OFFSET + modulatorSources.channelTuning, (this.channelTuningSemitones + this.channelTranspose) * 100] }); break; @@ -619,10 +600,12 @@ export class WorkletChannel { stopAll() { - for(let midiNote = 0; midiNote < 128; midiNote++) - { - this.stopNote(midiNote); - } + this.post({ + messageType: workletMessageType.stopAll, + messageData: 0 + }); + this.actualVoices = []; + this.notes = new Set(); } transposeChannel(semitones) @@ -632,20 +615,25 @@ export class WorkletChannel { return; } this.channelTranspose = semitones; - this.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.channelTuning] = this.channelTuningSemitones + this.channelTranspose << 7; + this.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.channelTuning] = (this.channelTuningSemitones + this.channelTranspose) * 100; this.post({ messageType: workletMessageType.ccChange, - messageData: [NON_CC_INDEX_OFFSET + modulatorSources.channelTuning, (this.channelTuningSemitones + this.channelTranspose) << 7] + messageData: [NON_CC_INDEX_OFFSET + modulatorSources.channelTuning, (this.channelTuningSemitones + this.channelTranspose) * 100] }); } resetControllers() { this.holdPedal = false; + this.chorus.setChorusLevel(0); this.vibrato = {depth: 0, rate: 0, delay: 0}; this.resetParameters(); + this.post({ + messageType: workletMessageType.ccReset, + messageData: 0 + }); } resetParameters() diff --git a/src/spessasynth_lib/synthetizer/worklet_channel/worklet_utilities/lfo.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/lfo.js similarity index 100% rename from src/spessasynth_lib/synthetizer/worklet_channel/worklet_utilities/lfo.js rename to src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/lfo.js diff --git a/src/spessasynth_lib/synthetizer/worklet_channel/worklet_utilities/modulator_curves.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/modulator_curves.js similarity index 100% rename from src/spessasynth_lib/synthetizer/worklet_channel/worklet_utilities/modulator_curves.js rename to src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/modulator_curves.js diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/stereo_panner.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/stereo_panner.js new file mode 100644 index 00000000..9f6d1a96 --- /dev/null +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/stereo_panner.js @@ -0,0 +1,17 @@ +import { HALF_PI } from './unit_converter.js' + +/** + * @param pan {number} 0-1 + * @param inputBuffer {Float32Array} + * @param outputLeft {Float32Array} + * @param outputRight {Float32Array} + */ +export function panVoice(pan, inputBuffer, outputLeft, outputRight) +{ + const panLeft = Math.cos(HALF_PI * pan); + const panRight = Math.sin(HALF_PI * pan); + for (let i = 0; i < inputBuffer.length; i++) { + outputLeft[i] += panLeft * inputBuffer[i]; + outputRight[i] += panRight * inputBuffer[i]; + } +} \ No newline at end of file diff --git a/src/spessasynth_lib/synthetizer/worklet_channel/worklet_utilities/unit_converter.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/unit_converter.js similarity index 96% rename from src/spessasynth_lib/synthetizer/worklet_channel/worklet_utilities/unit_converter.js rename to src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/unit_converter.js index d84f2408..ae7f47db 100644 --- a/src/spessasynth_lib/synthetizer/worklet_channel/worklet_utilities/unit_converter.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/unit_converter.js @@ -3,7 +3,7 @@ const MIN_TIMECENT = -12000; const MAX_TIMECENT = 8000; const timecentLookupTable = new Float32Array(MAX_TIMECENT - MIN_TIMECENT + 1); -for (let i = 0; i < timecentLookupTable.length; i++) { +for (let i = 1; i < timecentLookupTable.length; i++) { const timecents = MIN_TIMECENT + i; timecentLookupTable[i] = Math.pow(2, timecents / 1200); } diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/volume_envelope.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/volume_envelope.js new file mode 100644 index 00000000..92a541d4 --- /dev/null +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/volume_envelope.js @@ -0,0 +1,146 @@ +import { MIN_AUDIBLE_GAIN } from '../channel_processor.js'; +import { decibelAttenuationToGain, timecentsToSeconds } from './unit_converter.js' +import { generatorTypes } from '../../../soundfont/chunk/generators.js' + +const DB_SILENCE = 100; + +/** + * @param voice {WorkletVoice} + * @param audioBuffer {Float32Array} + * @param currentTime {number} + * @param centibelOffset {number} + * @param sampleTime {number} single sample time, usually 1 / 44100 of a second + */ +export function applyVolumeEnvelope(voice, audioBuffer, currentTime, centibelOffset, sampleTime) +{ + // calculate values + let decibelOffset = centibelOffset * 10; + + // calculate env times + let attack = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.attackVolEnv]); + let decay = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.decayVolEnv]); + + // calculate absolute times + let attenuation = voice.modulatedGenerators[generatorTypes.initialAttenuation] / 25 + decibelOffset; + let release = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.releaseVolEnv]); + let sustain = attenuation + voice.modulatedGenerators[generatorTypes.sustainVolEnv] / 10; + let delayEnd = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.delayVolEnv]) + voice.startTime; + let attackEnd = attack + delayEnd; + let holdEnd = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.holdVolEnv] + ((60 - voice.midiNote) * voice.modulatedGenerators[generatorTypes.keyNumToVolEnvHold])) + attackEnd; + let decayEnd = decay + ((60 - voice.midiNote) * voice.modulatedGenerators[generatorTypes.keyNumToVolEnvDecay]) + holdEnd; + + if(voice.isInRelease) + { + let elapsedRelease = currentTime - voice.releaseStartTime; + let dbDifference = DB_SILENCE - voice.releaseStartDb; + let db; + for (let i = 0; i < audioBuffer.length; i++) { + db = (elapsedRelease / release) * dbDifference + voice.releaseStartDb + decibelOffset; + audioBuffer[i] = decibelAttenuationToGain(db) * audioBuffer[i]; + elapsedRelease += sampleTime; + } + + if(db >= DB_SILENCE) + { + voice.finished = true; + } + return; + } + let currentFrameTime = currentTime; + let dbAttenuation; + for (let i = 0; i < audioBuffer.length; i++) { + if(currentFrameTime < delayEnd) + { + // we're in the delay phase + dbAttenuation = DB_SILENCE; + } + else if(currentFrameTime < attackEnd) + { + // we're in the attack pahse + dbAttenuation = ((attackEnd - currentFrameTime) / attack) * (DB_SILENCE - attenuation) + attenuation; + } + else if(currentFrameTime < holdEnd) + { + dbAttenuation = attenuation; + } + else if(currentFrameTime < decayEnd) + { + // we're in the decay phase + dbAttenuation = (1 - (decayEnd - currentFrameTime) / decay) * (sustain - attenuation) + attenuation; + } + else + { + dbAttenuation = sustain; + } + + // apply gain and advance the time + audioBuffer[i] = audioBuffer[i] * decibelAttenuationToGain(dbAttenuation); + currentFrameTime += sampleTime; + } + voice.currentAttenuationDb = dbAttenuation; +} + +const releaseExpoLookupTable = new Float32Array(1001); +for (let i = 0; i < 1001; i++) { + releaseExpoLookupTable[i] = Math.pow(0.000001, (i / 1000)); +} + +/** + * @param releaseTime {number} the length of release phase + * @param elapsed {number} the amount of seconds passed since the release start + * @returns {Number} + */ +export function getVolEnvReleaseMultiplier(releaseTime, elapsed) +{ + const gain = releaseExpoLookupTable[Math.trunc((elapsed / releaseTime) * 1000)]; + //const gain = (1 - elapsed / (releaseTime * 0.2)) + return gain > MIN_AUDIBLE_GAIN ? gain : -1; +} + +/** + * @param delay {number} seconds + * @param attack {number} seconds + * @param peak {number} gain + * @param hold {number} seconds + * @param sustain {number} gain + * @param decay {number} seconds + * @param startTime {number} seconds + * @param currentTime {number} seconds + * @returns {number} the gain or -1 if inaudible + */ +export function getVolumeEnvelopeValue(delay, attack, peak, hold, sustain, decay, startTime, currentTime) { + const attackStart = startTime + delay; + const attackEnd = attackStart + attack; + const holdEnd = attackEnd + hold; + const decayEnd = holdEnd + decay; + + // delay time + if (currentTime < attackStart) { + return 0; + } + // attack time + else if (currentTime < attackEnd) { + // linear + return ((currentTime - attackStart) / attack) * peak; + } + // hold time + else if (currentTime < holdEnd) { + return peak; + } + // decay time + else if (currentTime < decayEnd && (peak !== sustain)) { + // exponential + const gain = releaseExpoLookupTable[Math.trunc(((currentTime - holdEnd) / decay) * 1000)] * (peak - sustain) + sustain + if (gain < MIN_AUDIBLE_GAIN) { + return -0.001; + } + return gain; + } + // sustain + else { + if (sustain < MIN_AUDIBLE_GAIN) { + return -0.001; + } + return sustain; + } +} \ No newline at end of file diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js new file mode 100644 index 00000000..b32d6c61 --- /dev/null +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js @@ -0,0 +1,66 @@ +/** + * @param voice {WorkletVoice} + * @param sampleData {Float32Array} + * @param playbackRate {number} + * @param outputBuffer {Float32Array} + */ +export function getOscillatorData(voice, sampleData, playbackRate, outputBuffer) +{ + let cur = voice.sample.cursor; + const loop = (voice.sample.loopingMode === 1) || (voice.sample.loopingMode === 3 && !voice.isInRelease); + const loopLength = voice.sample.loopEnd - voice.sample.loopStart; + + if(loop) + { + for (let i = 0; i < outputBuffer.length; i++) { + // check for loop + if (cur > voice.sample.loopEnd) { + cur -= loopLength; + } + + // grab the 2 nearest points + const floor = ~~cur; + let ceil = floor + 1; + + if(ceil > voice.sample.loopEnd) + { + ceil -= loopLength; + } + + const fraction = cur - floor; + + // grab the samples and interpolate + const upper = sampleData[ceil]; + const lower = sampleData[floor]; + outputBuffer[i] = (lower + (upper - lower) * fraction); + + cur += voice.sample.playbackStep * playbackRate; + } + } + else + { + for (let i = 0; i < outputBuffer.length; i++) { + + // linear interpolation + const floor = ~~cur; + const ceil = floor + 1; + + // flag the voice as finished if needed + if(ceil >= voice.sample.end) + { + voice.finished = true; + return; + } + + const fraction = cur - floor; + + // grab the samples and interpolate + const upper = sampleData[ceil]; + const lower = sampleData[floor]; + outputBuffer[i] = (lower + (upper - lower) * fraction); + + cur += voice.sample.playbackStep * playbackRate; + } + } + voice.sample.cursor = cur; +} \ No newline at end of file diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js new file mode 100644 index 00000000..f404a108 --- /dev/null +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js @@ -0,0 +1,176 @@ +import { NON_CC_INDEX_OFFSET } from '../worklet_channel.js' +import { modulatorSources } from '../../../soundfont/chunk/modulators.js' +import { getModulatorValue, MOD_PRECOMPUTED_LENGTH } from './modulator_curves.js' + +/** + * + * @param controllerTable {Int16Array} all midi controllers as 14bit values + the non controller indexes, starting at 128 + * @param modulator {Modulator} + * @param midiNote {number} + * @param velocity {number} + * @returns {number} the computed value + */ +export function computeWorkletModulator(controllerTable, modulator, midiNote, velocity) +{ + if(modulator.transformAmount === 0) + { + return 0; + } + // mapped to 0-16384 + let rawSourceValue = 0; + if(modulator.sourceUsesCC) + { + rawSourceValue = controllerTable[modulator.sourceIndex]; + } + else + { + const index = modulator.sourceIndex + NON_CC_INDEX_OFFSET; + switch (modulator.sourceIndex) + { + case modulatorSources.noController: + return 0;// fluid_mod.c line 374 (0 times secondary times amount is still zero) + + case modulatorSources.noteOnKeyNum: + rawSourceValue = midiNote << 7; + break; + + case modulatorSources.noteOnVelocity: + case modulatorSources.polyPressure: + rawSourceValue = velocity << 7; + break; + + default: + rawSourceValue = controllerTable[index]; // use the 7 bit value + break; + } + + } + + const sourceValue = transforms[modulator.sourceCurveType][modulator.sourcePolarity][modulator.sourceDirection][rawSourceValue]; + + // mapped to 0-127 + let rawSecondSrcValue; + if(modulator.secSrcUsesCC) + { + rawSecondSrcValue = controllerTable[modulator.secSrcIndex]; + } + else + { + const index = modulator.secSrcIndex + NON_CC_INDEX_OFFSET; + switch (modulator.secSrcIndex) + { + case modulatorSources.noController: + rawSecondSrcValue = 16383;// fluid_mod.c line 376 + break; + + case modulatorSources.noteOnKeyNum: + rawSecondSrcValue = midiNote << 7; + break; + + case modulatorSources.noteOnVelocity: + case modulatorSources.polyPressure: + rawSecondSrcValue = velocity << 7; + break; + + default: + rawSecondSrcValue = controllerTable[index]; + } + + } + const secondSrcValue = transforms[modulator.secSrcCurveType][modulator.secSrcPolarity][modulator.secSrcDirection][rawSecondSrcValue]; + + + // compute the modulator + const computedValue = sourceValue * secondSrcValue * modulator.transformAmount; + + if(modulator.transformType === 2) + { + // abs value + return Math.abs(computedValue); + } + return computedValue; +} + +/** + * @param voice {WorkletVoice} + * @param controllerTable {Int16Array} + */ +export function computeModulators(voice, controllerTable) +{ + voice.modulatedGenerators.set(voice.generators); + voice.modulators.forEach(mod => { + voice.modulatedGenerators[mod.modulatorDestination] += computeWorkletModulator(controllerTable, mod, voice.midiNote, voice.velocity); + }); +} + +/** + * @param voice {WorkletVoice} + * @param generatorType {number} + * @returns {number} the computed number + */ +// export function getModulated(voice, generatorType) { +// return voice.modulatedGenerators[generatorType]; +// } + +/** + * as follows: transforms[curveType][polarity][direction] is an array + * @type {Float32Array[][][]} + */ +const transforms = []; + +for(let curve = 0; curve < 4; curve++) +{ + transforms[curve] = + [ + [ + new Float32Array(MOD_PRECOMPUTED_LENGTH), + new Float32Array(MOD_PRECOMPUTED_LENGTH) + ], + [ + new Float32Array(MOD_PRECOMPUTED_LENGTH), + new Float32Array(MOD_PRECOMPUTED_LENGTH) + ] + ]; + for (let i = 0; i < MOD_PRECOMPUTED_LENGTH; i++) { + + // polarity 0 dir 0 + transforms[curve][0][0][i] = getModulatorValue( + 0, + curve, + i / MOD_PRECOMPUTED_LENGTH, + 0); + if (isNaN(transforms[curve][0][0][i])) { + transforms[curve][0][0][i] = 1; + } + + // polarity 1 dir 0 + transforms[curve][1][0][i] = getModulatorValue( + 0, + curve, + i / MOD_PRECOMPUTED_LENGTH, + 1); + if (isNaN(transforms[curve][1][0][i])) { + transforms[curve][1][0][i] = 1; + } + + // polarity 0 dir 1 + transforms[curve][0][1][i] = getModulatorValue( + 1, + curve, + i / MOD_PRECOMPUTED_LENGTH, + 0); + if (isNaN(transforms[curve][0][1][i])) { + transforms[curve][0][1][i] = 1; + } + + // polarity 1 dir 1 + transforms[curve][1][1][i] = getModulatorValue( + 1, + curve, + i / MOD_PRECOMPUTED_LENGTH, + 1); + if (isNaN(transforms[curve][1][1][i])) { + transforms[curve][1][1][i] = 1; + } + } +} \ No newline at end of file diff --git a/src/spessasynth_lib/utils/event_handler.js b/src/spessasynth_lib/utils/event_handler.js index 451d24c2..f5d9c954 100644 --- a/src/spessasynth_lib/utils/event_handler.js +++ b/src/spessasynth_lib/utils/event_handler.js @@ -5,7 +5,8 @@ * "pitchwheel"| * "controllerchange"| * "programchange"| - * "drumchange"} EventTypes + * "drumchange"| + * "stopall"} EventTypes */ export class EventHandler { diff --git a/src/spessasynth_lib/utils/other.js b/src/spessasynth_lib/utils/other.js index cbab50b8..3142864b 100644 --- a/src/spessasynth_lib/utils/other.js +++ b/src/spessasynth_lib/utils/other.js @@ -100,7 +100,7 @@ export const midiPatchNames = [ "Synth Strings 1", "Synth Strings 2", "Choir Aahs", - "Voice Oohs", + "VoiceGroup Oohs", "Synth Choir", "Orchestra Hit", "Trumpet", diff --git a/src/website/manager.js b/src/website/manager.js index 20bf7239..e8aedbb2 100644 --- a/src/website/manager.js +++ b/src/website/manager.js @@ -43,9 +43,9 @@ export class Manager { async initializeContext(context, soundFont) { if(context.audioWorklet) { try { - await context.audioWorklet.addModule("/spessasynth_lib/synthetizer/worklet_channel/channel_processor.js"); + await context.audioWorklet.addModule("/spessasynth_lib/synthetizer/worklet_system/channel_processor.js"); } catch (e) { - await context.audioWorklet.addModule("src/spessasynth_lib/synthetizer/worklet_channel/channel_processor.js"); + await context.audioWorklet.addModule("src/spessasynth_lib/synthetizer/worklet_system/channel_processor.js"); } } // set up soundfont diff --git a/src/website/ui/midi_keyboard.js b/src/website/ui/midi_keyboard.js index ad13e8a7..3a279abe 100644 --- a/src/website/ui/midi_keyboard.js +++ b/src/website/ui/midi_keyboard.js @@ -3,7 +3,7 @@ import { MIDIDeviceHandler } from '../../spessasynth_lib/midi_handler/midi_handl import { midiControllers } from '../../spessasynth_lib/midi_parser/midi_message.js' const KEYBOARD_VELOCITY = 126; -const GLOW_PX = 50; +const GLOW_PX = 75; export class MidiKeyboard { @@ -142,7 +142,7 @@ export class MidiKeyboard } - this.keyColors.push([keyElement.style.background]); + this.keyColors.push([]); this.keyboard.appendChild(keyElement); this.keys.push(keyElement); } @@ -212,8 +212,9 @@ export class MidiKeyboard this.synth.eventHandler.addEvent("noteoff", e => { this.releaseNote(e.midiNote, e.channel); }) - //this.synth.onNoteOn.push((note, chan, vel, vol, exp) => this.pressNote(note, chan, vel, vol, exp)); - //this.synth.onNoteOff.push((note, chan) => this.releaseNote(note, chan)); + this.synth.eventHandler.addEvent("stopall", () => { + this.clearNotes(); + }) } toggleMode() @@ -336,19 +337,27 @@ export class MidiKeyboard { return; } - if(pressedColors.length > 1) { - pressedColors.splice(pressedColors.findLastIndex(v => v === this.channelColors[channel]), 1); - key.style.background = pressedColors[pressedColors.length - 1]; - if(this.mode === "dark") - { - key.style.boxShadow = `0px 0px ${GLOW_PX}px ${pressedColors[pressedColors.length - 1]}`; - } + pressedColors.splice(pressedColors.findLastIndex(v => v === this.channelColors[channel]), 1); + key.style.background = pressedColors[pressedColors.length - 1]; + if(this.mode === "dark") + { + key.style.boxShadow = `0px 0px ${GLOW_PX}px ${pressedColors[pressedColors.length - 1]}`; } - if(pressedColors.length === 1) + if(pressedColors.length < 1) { key.classList.remove("pressed"); key.style.background = ""; key.style.boxShadow = ""; } } + + clearNotes() + { + this.keys.forEach((key, index) => { + key.classList.remove("pressed"); + key.style.background = ""; + key.style.boxShadow = ""; + this.keyColors[index] = []; + }) + } } \ No newline at end of file diff --git a/src/website/ui/sequencer_ui/sequencer_ui.js b/src/website/ui/sequencer_ui/sequencer_ui.js index 38823b02..b4f04dbe 100644 --- a/src/website/ui/sequencer_ui/sequencer_ui.js +++ b/src/website/ui/sequencer_ui/sequencer_ui.js @@ -369,6 +369,7 @@ export class SequencerUI{ updateTitleAndMediaStatus() { document.getElementById("title").innerText = this.titles[this.seq.songIndex]; + document.title = this.titles[this.seq.songIndex] + " - SpessaSynth" navigator.mediaSession.setPositionState({ duration: this.seq.duration, diff --git a/src/website/ui/synthesizer_ui/synthetizer_ui.js b/src/website/ui/synthesizer_ui/synthetizer_ui.js index e52c0bb1..c09024f0 100644 --- a/src/website/ui/synthesizer_ui/synthetizer_ui.js +++ b/src/website/ui/synthesizer_ui/synthetizer_ui.js @@ -1,5 +1,5 @@ import { DEFAULT_GAIN, Synthetizer } from '../../../spessasynth_lib/synthetizer/synthetizer.js' -import {MidiChannel} from "../../../spessasynth_lib/synthetizer/buffer_voice/midi_channel.js"; +import {MidiChannel} from "../../../spessasynth_lib/synthetizer/native_system/midi_channel.js"; import { getLoopSvg, getMuteSvg, getVolumeSvg } from '../icons.js' import { ShiftableByteArray } from '../../../spessasynth_lib/utils/shiftable_array.js'; import { Meter } from './synthui_meter.js' @@ -31,7 +31,7 @@ export class SynthetizerUI createMainSynthController() { /** - * Voice meter + * VoiceGroup meter * @type {Meter} */ this.voiceMeter = new Meter("#206", "Voices: ", 0, this.synth.voiceCap);