UNPKG

spessasynth_core

Version:

MIDI and SoundFont2/DLS library with no compromises

525 lines (479 loc) 15.3 kB
/** * voice.js * purpose: prepares Voices from sample and generator data */ import { MIN_EXCLUSIVE_LENGTH, MIN_NOTE_LENGTH } from "../main_processor.js"; import { SpessaSynthWarn } from "../../../utils/loggin.js"; import { LowpassFilter } from "./lowpass_filter.js"; import { VolumeEnvelope } from "./volume_envelope.js"; import { ModulationEnvelope } from "./modulation_envelope.js"; import { addAndClampGenerator } from "../../../soundfont/basic_soundfont/generator.js"; import { Modulator } from "../../../soundfont/basic_soundfont/modulator.js"; import { GENERATORS_AMOUNT, generatorTypes } from "../../../soundfont/basic_soundfont/generator_types.js"; const EXCLUSIVE_CUTOFF_TIME = -2320; const EXCLUSIVE_MOD_CUTOFF_TIME = -1130; // less because filter shenanigans class AudioSample { /** * the sample's audio data * @type {Float32Array} */ sampleData; /** * Current playback step (rate) * @type {number} */ playbackStep = 0; /** * Current position in the sample * @type {number} */ cursor = 0; /** * MIDI root key of the sample * @type {number} */ rootKey = 0; /** * Start position of the loop * @type {number} */ loopStart = 0; /** * End position of the loop * @type {number} */ loopEnd = 0; /** * End position of the sample * @type {number} */ end = 0; /** * Looping mode of the sample: * 0 - no loop * 1 - loop * 2 - UNOFFICIAL: polyphone 2.4 added start on release * 3 - loop then play when released * @type {0|1|2|3} */ loopingMode = 0; /** * Indicates if the sample is currently looping * @type {boolean} */ isLooping = false; /** * @param data {Float32Array} * @param playbackStep {number} the playback step, a single increment * @param cursorStart {number} the sample id which starts the playback * @param rootKey {number} MIDI root key * @param loopStart {number} loop start index * @param loopEnd {number} loop end index * @param endIndex {number} sample end index (for end offset) * @param loopingMode {number} sample looping mode */ constructor( data, playbackStep, cursorStart, rootKey, loopStart, loopEnd, endIndex, loopingMode ) { this.sampleData = data; this.playbackStep = playbackStep; this.cursor = cursorStart; this.rootKey = rootKey; this.loopStart = loopStart; this.loopEnd = loopEnd; this.end = endIndex; this.loopingMode = loopingMode; this.isLooping = this.loopingMode === 1 || this.loopingMode === 3; } } /** * Voice represents a single instance of the * SoundFont2 synthesis model. * That is: * A wavetable oscillator (sample) * A volume envelope (volumeEnvelope) * A modulation envelope (modulationEnvelope) * Generators (generators and modulatedGenerators) * Modulators (modulators) * And MIDI params such as channel, MIDI note, velocity */ class Voice { /** * The sample of the voice. * @type {AudioSample} */ sample; /** * Lowpass filter applied to the voice. * @type {LowpassFilter} */ filter; /** * Linear gain of the voice. Used with Key Modifiers. * @type {number} */ gain = 1; /** * The unmodulated (copied to) generators of the voice. * @type {Int16Array} */ generators; /** * The voice's modulators. * @type {Modulator[]} */ modulators = []; /** * Resonance offset, it is affected by the default resonant modulator * @type {number} */ resonanceOffset = 0; /** * The generators in real-time, affected by modulators. * This is used during rendering. * @type {Int16Array} */ modulatedGenerators; /** * Indicates if the voice is finished. * @type {boolean} */ finished = false; /** * Indicates if the voice is in the release phase. * @type {boolean} */ isInRelease = false; /** * Velocity of the note. * @type {number} */ velocity = 0; /** * MIDI note number. * @type {number} */ midiNote = 0; /** * The pressure of the voice * @type {number} */ pressure = 0; /** * Target key for the note. * @type {number} */ targetKey = 0; /** * Modulation envelope. * @type {ModulationEnvelope} */ modulationEnvelope = new ModulationEnvelope(); /** * Volume envelope. * @type {VolumeEnvelope} */ volumeEnvelope; /** * Start time of the voice, absolute. * @type {number} */ startTime = 0; /** * Start time of the release phase, absolute. * @type {number} */ releaseStartTime = Infinity; /** * Current tuning in cents. * @type {number} */ currentTuningCents = 0; /** * Current calculated tuning. (as in ratio) * @type {number} */ currentTuningCalculated = 1; /** * From -500 to 500. * @param {number} */ currentPan = 0; /** * If MIDI Tuning Standard is already applied (at note-on time), * this will be used to take the values at real-time tuning as "midiNote" * property contains the tuned number. * see #29 comment by @paulikaro * @type {number} */ realKey; /** * @type {number} Initial key to glide from, MIDI Note number. If -1, the portamento is OFF. */ portamentoFromKey = -1; /** * Duration of the linear glide, in seconds. * @type {number} */ portamentoDuration = 0; /** * From -500 to 500, where zero means disabled (use the channel pan). Used for random pan. * @type {number} */ overridePan = 0; /** * Exclusive class number for hi-hats etc. * @type {number} */ exclusiveClass = 0; /** * Creates a Voice * @param sampleRate {number} * @param audioSample {AudioSample} * @param midiNote {number} * @param velocity {number} * @param currentTime {number} * @param targetKey {number} * @param realKey {number} * @param generators {Int16Array} * @param modulators {Modulator[]} */ constructor( sampleRate, audioSample, midiNote, velocity, currentTime, targetKey, realKey, generators, modulators ) { this.sample = audioSample; this.generators = generators; this.exclusiveClass = this.generators[generatorTypes.exclusiveClass]; this.modulatedGenerators = new Int16Array(generators); this.modulators = modulators; this.filter = new LowpassFilter(sampleRate); this.velocity = velocity; this.midiNote = midiNote; this.startTime = currentTime; this.targetKey = targetKey; this.realKey = realKey; this.volumeEnvelope = new VolumeEnvelope(sampleRate, generators[generatorTypes.sustainVolEnv]); } /** * copies the voice * @param voice {Voice} * @param currentTime {number} * @param realKey {number} * @returns Voice */ static copy(voice, currentTime, realKey) { const sampleToCopy = voice.sample; const sample = new AudioSample( sampleToCopy.sampleData, sampleToCopy.playbackStep, sampleToCopy.cursor, sampleToCopy.rootKey, sampleToCopy.loopStart, sampleToCopy.loopEnd, sampleToCopy.end, sampleToCopy.loopingMode ); return new Voice( voice.volumeEnvelope.sampleRate, sample, voice.midiNote, voice.velocity, currentTime, voice.targetKey, realKey, new Int16Array(voice.generators), voice.modulators.map(m => Modulator.copy(m)) ); } /** * Releases the voice as exclusiveClass * @param currentTime {number} */ exclusiveRelease(currentTime) { this.release(currentTime, MIN_EXCLUSIVE_LENGTH); this.modulatedGenerators[generatorTypes.releaseVolEnv] = EXCLUSIVE_CUTOFF_TIME; // make the release nearly instant this.modulatedGenerators[generatorTypes.releaseModEnv] = EXCLUSIVE_MOD_CUTOFF_TIME; VolumeEnvelope.recalculate(this); ModulationEnvelope.recalculate(this); } /** * Stops the voice * @param currentTime {number} * @param minNoteLength {number} minimum note length in seconds */ release(currentTime, minNoteLength = MIN_NOTE_LENGTH) { this.releaseStartTime = currentTime; // check if the note is shorter than the min note time, if so, extend it if (this.releaseStartTime - this.startTime < minNoteLength) { this.releaseStartTime = this.startTime + minNoteLength; } } } /** * @param preset {BasicPreset} the preset to get voices for * @param bank {number} the bank to cache the voices in * @param program {number} program to cache the voices in * @param midiNote {number} the MIDI note to use * @param velocity {number} the velocity to use * @param realKey {number} the real MIDI note if the "midiNote" was changed by MIDI Tuning Standard * @this {SpessaSynthProcessor} * @returns {Voice[]} output is an array of Voices */ export function getVoicesForPreset(preset, bank, program, midiNote, velocity, realKey) { /** * @type {Voice[]} */ const voices = preset.getSamplesAndGenerators(midiNote, velocity) .reduce((voices, sampleAndGenerators) => { if (sampleAndGenerators.sample.getAudioData() === undefined) { SpessaSynthWarn(`Discarding invalid sample: ${sampleAndGenerators.sample.sampleName}`); return voices; } // create the generator list const generators = new Int16Array(GENERATORS_AMOUNT); // apply and sum the gens for (let i = 0; i < 60; i++) { generators[i] = addAndClampGenerator( i, sampleAndGenerators.presetGenerators, sampleAndGenerators.instrumentGenerators ); } // EMU initial attenuation correction, multiply initial attenuation by 0.4! // all EMU sound cards have this quirk, and all sf2 editors and players emulate it too generators[generatorTypes.initialAttenuation] = Math.floor(generators[generatorTypes.initialAttenuation] * 0.4); // key override let rootKey = sampleAndGenerators.sample.samplePitch; if (generators[generatorTypes.overridingRootKey] > -1) { rootKey = generators[generatorTypes.overridingRootKey]; } let targetKey = midiNote; if (generators[generatorTypes.keyNum] > -1) { targetKey = generators[generatorTypes.keyNum]; } // determine looping mode now. if the loop is too small, disable let loopStart = sampleAndGenerators.sample.sampleLoopStartIndex; let loopEnd = sampleAndGenerators.sample.sampleLoopEndIndex; let loopingMode = generators[generatorTypes.sampleModes]; /** * create the sample * offsets are calculated at note on time (to allow for modulation of them) * @type {AudioSample} */ const audioSample = new AudioSample( sampleAndGenerators.sample.sampleData, (sampleAndGenerators.sample.sampleRate / this.sampleRate) * Math.pow( 2, sampleAndGenerators.sample.samplePitchCorrection / 1200 ), // cent tuning 0, rootKey, loopStart, loopEnd, Math.floor(sampleAndGenerators.sample.sampleData.length) - 1, loopingMode ); // velocity override if (generators[generatorTypes.velocity] > -1) { velocity = generators[generatorTypes.velocity]; } // uncomment to print debug info // SpessaSynthTable([{ // Sample: sampleAndGenerators.sample.sampleName, // Generators: generators, // Modulators: sampleAndGenerators.modulators.map(m => Modulator.debugString(m)), // Velocity: velocity, // TargetKey: targetKey, // MidiNote: midiNote, // AudioSample: audioSample // }]); voices.push( new Voice( this.sampleRate, audioSample, midiNote, velocity, this.currentSynthTime, targetKey, realKey, generators, sampleAndGenerators.modulators.map(m => Modulator.copy(m)) ) ); return voices; }, []); // cache the voice this.setCachedVoice(bank, program, midiNote, velocity, voices); return voices.map(v => Voice.copy(v, this.currentSynthTime, realKey)); } /** * @param channel {number} a hint for the processor to recalculate sample cursors when sample dumping * @param midiNote {number} the MIDI note to use * @param velocity {number} the velocity to use * @param realKey {number} the real MIDI note if the "midiNote" was changed by MIDI Tuning Standard * @this {SpessaSynthProcessor} * @returns {Voice[]} output is an array of Voices */ export function getVoices(channel, midiNote, velocity, realKey) { const channelObject = this.midiAudioChannels[channel]; // override patch const overridePatch = this.keyModifierManager.hasOverridePatch(channel, midiNote); let bank = channelObject.getBankSelect(); let preset = channelObject.preset; if (!preset) { SpessaSynthWarn(`No preset for channel ${channel}!`); return []; } let program = preset.program; if (overridePatch) { const override = this.keyModifierManager.getPatch(channel, midiNote); bank = override.bank; program = override.program; } const cached = this.getCachedVoice(bank, program, midiNote, velocity); // if cached, return it! if (cached !== undefined) { return cached.map(v => Voice.copy(v, this.currentSynthTime, realKey)); } // not cached... if (overridePatch) { preset = this.getPreset(bank, program); } return this.getVoicesForPreset(preset, bank, program, midiNote, velocity, realKey); }