spessasynth_core
Version:
MIDI and SoundFont2/DLS library with no compromises
220 lines (199 loc) • 7.74 kB
JavaScript
import { midiControllers } from "../../../midi/midi_message.js";
import { portamentoTimeToSeconds } from "./portamento_time.js";
import { customControllers } from "../engine_components/controller_tables.js";
import { Modulator } from "../../../soundfont/basic_soundfont/modulator.js";
import { GENERATOR_OVERRIDE_NO_CHANGE_VALUE } from "../../synth_constants.js";
import { generatorTypes } from "../../../soundfont/basic_soundfont/generator_types.js";
import { SpessaSynthWarn } from "../../../utils/loggin.js";
/**
* sends a "MIDI Note on message"
* @param midiNote {number}
* @param velocity {number}
* @this {MidiAudioChannel}
*/
export function noteOn(midiNote, velocity)
{
if (velocity < 1)
{
this.noteOff(midiNote);
return;
}
velocity = Math.min(127, velocity);
if (
(this.synth.highPerformanceMode && this.synth.totalVoicesAmount > 200 && velocity < 40) ||
(this.synth.highPerformanceMode && velocity < 10) ||
(this.isMuted)
)
{
return;
}
if (!this.preset)
{
SpessaSynthWarn(`No preset for channel ${this.channelNumber}!`);
return;
}
const realKey = midiNote + this.channelTransposeKeyShift + this.customControllers[customControllers.channelKeyShift];
let internalMidiNote = realKey;
if (realKey > 127 || realKey < 0)
{
return;
}
const program = this.preset?.program;
const tune = this.synth.tunings[program]?.[realKey]?.midiNote;
if (tune >= 0)
{
internalMidiNote = tune;
}
// velocity override
if (this.velocityOverride > 0)
{
velocity = this.velocityOverride;
}
// monophonic retrigger
if (this.synth._monophonicRetriggerMode)
{
this.killNote(midiNote, -7200);
}
// key velocity override
const keyVel = this.synth.keyModifierManager.getVelocity(this.channelNumber, realKey);
if (keyVel > -1)
{
velocity = keyVel;
}
// gain
const voiceGain = this.synth.keyModifierManager.getGain(this.channelNumber, realKey);
// portamento
let portamentoFromKey = -1;
let portamentoDuration = 0;
// note: the 14-bit value needs to go down to 7-bit
const portamentoTime = this.midiControllers[midiControllers.portamentoTime] >> 7;
const control = this.midiControllers[midiControllers.portamentoControl];
const currentFromKey = control >> 7;
if (
!this.drumChannel && // no portamento on drum channel
currentFromKey !== internalMidiNote && // if the same note, there's no portamento
this.midiControllers[midiControllers.portamentoOnOff] >= 8192 && // (64 << 7)
portamentoTime > 0 // 0 duration is no portamento
)
{
// a value of one means the initial portamento
if (control !== 1)
{
const diff = Math.abs(internalMidiNote - currentFromKey);
portamentoDuration = portamentoTimeToSeconds(portamentoTime, diff);
portamentoFromKey = currentFromKey;
}
// set portamento control to previous value
this.controllerChange(midiControllers.portamentoControl, internalMidiNote);
}
// get voices
const voices = this.synth.getVoices(
this.channelNumber,
internalMidiNote,
velocity,
realKey
);
// zero means disabled
let panOverride = 0;
if (this.randomPan)
{
// the range is -500 to 500
panOverride = Math.round(Math.random() * 1000 - 500);
}
// add voices
const channelVoices = this.voices;
voices.forEach(voice =>
{
// apply portamento
voice.portamentoFromKey = portamentoFromKey;
voice.portamentoDuration = portamentoDuration;
// apply pan override
voice.overridePan = panOverride;
// apply gain override
voice.gain = voiceGain;
// dynamic modulators (if none, this won't iterate over anything)
this.sysExModulators.modulatorList.forEach(m =>
{
const mod = m.mod;
const existingModIndex = voice.modulators.findIndex(voiceMod => Modulator.isIdentical(voiceMod, mod));
// replace or add
if (existingModIndex !== -1)
{
voice.modulators[existingModIndex] = Modulator.copy(mod);
}
else
{
voice.modulators.push(Modulator.copy(mod));
}
});
// apply generator override
if (this.generatorOverridesEnabled)
{
this.generatorOverrides.forEach((overrideValue, generatorType) =>
{
if (overrideValue === GENERATOR_OVERRIDE_NO_CHANGE_VALUE)
{
return;
}
voice.generators[generatorType] = overrideValue;
});
}
// apply exclusive class
const exclusive = voice.exclusiveClass;
if (exclusive !== 0)
{
// kill all voices with the same exclusive class
channelVoices.forEach(v =>
{
if (v.exclusiveClass === exclusive)
{
v.exclusiveRelease(this.synth.currentSynthTime);
}
});
}
// compute all modulators
this.computeModulators(voice);
// modulate sample offsets (these are not real time)
const cursorStartOffset = voice.modulatedGenerators[generatorTypes.startAddrsOffset] + voice.modulatedGenerators[generatorTypes.startAddrsCoarseOffset] * 32768;
const endOffset = voice.modulatedGenerators[generatorTypes.endAddrOffset] + voice.modulatedGenerators[generatorTypes.endAddrsCoarseOffset] * 32768;
const loopStartOffset = voice.modulatedGenerators[generatorTypes.startloopAddrsOffset] + voice.modulatedGenerators[generatorTypes.startloopAddrsCoarseOffset] * 32768;
const loopEndOffset = voice.modulatedGenerators[generatorTypes.endloopAddrsOffset] + voice.modulatedGenerators[generatorTypes.endloopAddrsCoarseOffset] * 32768;
const sm = voice.sample;
// apply them
const clamp = num => Math.max(0, Math.min(sm.sampleData.length - 1, num));
sm.cursor = clamp(sm.cursor + cursorStartOffset);
sm.end = clamp(sm.end + endOffset);
sm.loopStart = clamp(sm.loopStart + loopStartOffset);
sm.loopEnd = clamp(sm.loopEnd + loopEndOffset);
// swap loops if needed
if (sm.loopEnd < sm.loopStart)
{
const temp = sm.loopStart;
sm.loopStart = sm.loopEnd;
sm.loopEnd = temp;
}
if (sm.loopEnd - sm.loopStart < 1)
{
sm.loopingMode = 0;
sm.isLooping = false;
}
// set the current attenuation to target,
// as it's interpolated (we don't want 0 attenuation for even a split second)
voice.volumeEnvelope.attenuation = voice.volumeEnvelope.attenuationTargetGain;
// 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 to 500
});
this.synth.totalVoicesAmount += voices.length;
// cap the voices
if (this.synth.totalVoicesAmount > this.synth.voiceCap)
{
this.synth.voiceKilling(voices.length);
}
channelVoices.push(...voices);
this.sendChannelProperty();
this.synth.callEvent("noteon", {
midiNote: midiNote,
channel: this.channelNumber,
velocity: velocity
});
}