From d99ccee329b4b27335e6978a9f516456e1bdfb98 Mon Sep 17 00:00:00 2001 From: spessasus <95608008+spessasus@users.noreply.github.com> Date: Thu, 21 Sep 2023 00:41:41 +0200 Subject: [PATCH] Added mute functionality and some small imrovements --- .../synthetizer/buffer_voice/midi_channel.js | 42 +++++++---- .../synthetizer/synthetizer.js | 57 +++++++++------ .../worklet_channel/channel_processor.js | 72 +++++++++---------- .../worklet_channel/worklet_channel.js | 16 ++++- src/website/css/synthesizer_ui.css | 14 ++++ src/website/ui/icons.js | 16 +++++ .../ui/synthesizer_ui/synthetizer_ui.js | 21 +++++- 7 files changed, 165 insertions(+), 73 deletions(-) diff --git a/src/spessasynth_lib/synthetizer/buffer_voice/midi_channel.js b/src/spessasynth_lib/synthetizer/buffer_voice/midi_channel.js index ce059edd..9094c9e6 100644 --- a/src/spessasynth_lib/synthetizer/buffer_voice/midi_channel.js +++ b/src/spessasynth_lib/synthetizer/buffer_voice/midi_channel.js @@ -30,6 +30,7 @@ export class MidiChannel { this.outputNode = targetNode; this.channelNumber = channelNumber this.percussionChannel = percussionChannel; + this.defaultGain = CHANNEL_LOUDNESS; this.preset = defaultPreset; this.bank = this.preset.bank; @@ -101,6 +102,7 @@ export class MidiChannel { * @type {boolean} */ this.lockPreset = false; + this.lockVibrato = false; } /** @@ -233,7 +235,7 @@ export class MidiChannel { { val = Math.min(1, val); this.channelExpression = val; - this.gainController.gain.value = this.getGain(); + this.updateGain(); } /** @@ -242,16 +244,17 @@ export class MidiChannel { * @param debugInfo {boolean} for debugging set to true */ playNote(midiNote, velocity, debugInfo = false) { - if(!velocity) - { - throw "No velocity given!"; - } if (velocity === 0) { // stop if velocity 0 this.stopNote(midiNote); return; } + if(this.defaultGain === 0) + { + return; + } + this.notes.add(midiNote); this.receivedNotes.add(midiNote); let note = new Voice(midiNote, velocity, this.panner, this.preset, this.vibrato, this.channelTuningRatio, this.modulation); @@ -330,7 +333,7 @@ export class MidiChannel { setVolume(volume) { volume = Math.min(127, volume); this.channelVolume = volume / 127; - this.gainController.gain.value = this.getGain(); + this.updateGain(); } setRPCoarse(value) @@ -387,6 +390,10 @@ export class MidiChannel { break; case 1: + if(this.lockVibrato) + { + return; + } switch(this.NRPFine) { default: @@ -472,12 +479,20 @@ export class MidiChannel { } } + updateGain(){ + this.gainController.gain.value = this.defaultGain * this.channelVolume * this.channelExpression; + } - /** - * @returns {number} - */ - getGain(){ - return CHANNEL_LOUDNESS * this.channelVolume * this.channelExpression; + muteChannel() + { + this.defaultGain = 0; + this.updateGain(); + } + + unmuteChannel() + { + this.defaultGain = CHANNEL_LOUDNESS; + this.updateGain(); } /** @@ -533,6 +548,9 @@ export class MidiChannel { { this.stopNote(midiNote); } + this.playingNotes.forEach(n => { + this.stopNote(n.midiNote); + }); if(force) { this.stoppingNotes.forEach(n => { @@ -549,7 +567,7 @@ export class MidiChannel { this.channelTuningRatio = 1; this.channelPitchBendRange = 2; this.holdPedal = false; - this.gainController.gain.value = this.getGain(); + this.updateGain(); this.chorusController.gain.value = 0; this.panner.pan.value = 0; this.pitchBend = 0; diff --git a/src/spessasynth_lib/synthetizer/synthetizer.js b/src/spessasynth_lib/synthetizer/synthetizer.js index fe9e3c9d..bc2036f1 100644 --- a/src/spessasynth_lib/synthetizer/synthetizer.js +++ b/src/spessasynth_lib/synthetizer/synthetizer.js @@ -13,6 +13,7 @@ export const DEFAULT_PERCUSSION = 9; export class Synthetizer { /** + * Creates a new instance of the SpessaSynth synthesizer * @param targetNode {AudioNode} * @param soundFont {SoundFont2} */ @@ -68,10 +69,10 @@ export class Synthetizer { } /** - * MIDI noteOn Event - * @param channel {number} 0-15 - * @param midiNote {number} 0-127 - * @param velocity {number} 0-127 + * Starts playing a note + * @param channel {number} 0-15 the channel to play the note + * @param midiNote {number} 0-127 the key number of the note + * @param velocity {number} 0-127 the velocity of the note (generally controls loudness) * @param enableDebugging {boolean} set to true to log stuff to console */ noteOn(channel, midiNote, velocity, enableDebugging = false) { @@ -103,10 +104,18 @@ export class Synthetizer { } } + /* + * Prevents any further changes to the vibrato via NRPN messages + */ + lockChannelVibrato() + { + this.midiChannels.forEach(c => c.lockVibrato = true); + } + /** - * MIDI noteOff event - * @param channel {number} 0-15 - * @param midiNote {number} 0-127 + * Stops playing a note + * @param channel {number} 0-15 the channel of the note + * @param midiNote {number} 0-127 the key number of the note */ noteOff(channel, midiNote) { if(midiNote > 127 || midiNote < 0) @@ -161,9 +170,9 @@ export class Synthetizer { /** * Changes the given controller - * @param channel {number} 0-15 - * @param controllerNumber {number} 0-127 - * @param controllerValue {number} 0-127 + * @param channel {number} 0-15 the channel to change the controller + * @param controllerNumber {number} 0-127 the MIDI CC number + * @param controllerValue {number} 0-127 the controller value */ controllerChange(channel, controllerNumber, controllerValue) { @@ -219,7 +228,7 @@ export class Synthetizer { } /** - * Resets all controllers + * Resets all controllers (for every channel) */ resetControllers() { @@ -260,10 +269,10 @@ export class Synthetizer { } /** - * Sets the pitch - * @param channel {number} 0-16 - * @param MSB {number} SECOND byte - * @param LSB {number} FIRST byte + * Sets the pitch of the given channel + * @param channel {number} 0-16 the channel to change pitch + * @param MSB {number} SECOND byte of the MIDI pitchWheel message + * @param LSB {number} FIRST byte of the MIDI pitchWheel message */ pitchWheel(channel, MSB, LSB) { @@ -276,7 +285,7 @@ export class Synthetizer { /** * Transposes the synthetizer's pitch by given semitones amount (percussion channels do not get affected) - * @param semitones {number} + * @param semitones {number} the semitones to transpose by. Can be a floating point number for more precision */ transpose(semitones) { @@ -285,7 +294,7 @@ export class Synthetizer { /** * Sets the main volume - * @param volume {number} 0-1 + * @param volume {number} 0-1 the volume */ setMainVolume(volume) { @@ -311,8 +320,9 @@ export class Synthetizer { onPitchWheel; /** - * @param channel {number} 0-15 - * @param programNumber {number} 0-127 + * Changes the patch for a given channel + * @param channel {number} 0-15 the channel to change + * @param programNumber {number} 0-127 the MIDI patch number */ programChange(channel, programNumber) { @@ -330,6 +340,9 @@ export class Synthetizer { } } + /** + * Call after replacing synth.soundFont + */ reloadSoundFont() { this.defaultPreset = this.soundFont.getPreset(0, 0); @@ -346,8 +359,8 @@ export class Synthetizer { } /** - * Sends a sysex - * @param messageData {ShiftableByteArray} the message's data (after F0) + * Sends a MIDI Sysex message + * @param messageData {ShiftableByteArray} the message's data (excluding the F0 byte, but including the F7 at the end) */ systemExclusive(messageData) { @@ -493,7 +506,7 @@ export class Synthetizer { } /** - * @returns {number} + * @returns {number} the audioContext's current time */ get currentTime() { diff --git a/src/spessasynth_lib/synthetizer/worklet_channel/channel_processor.js b/src/spessasynth_lib/synthetizer/worklet_channel/channel_processor.js index e26e4841..4526f2f8 100644 --- a/src/spessasynth_lib/synthetizer/worklet_channel/channel_processor.js +++ b/src/spessasynth_lib/synthetizer/worklet_channel/channel_processor.js @@ -211,42 +211,42 @@ class ChannelProcessor extends AudioWorkletProcessor { // LOWPASS - const filterQ = getModulated(voice, generatorTypes.initialFilterQ, this.midiControllers) - 3.01; // polyphone???? - const filterQgain = Math.pow(10, filterQ / 20); - const filterFcHz = absCentsToHz(getModulated(voice, generatorTypes.initialFilterFc, this.midiControllers)); - // 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; - } - } + // const filterQ = getModulated(voice, generatorTypes.initialFilterQ, this.midiControllers) - 3.01; // polyphone???? + // const filterQgain = Math.pow(10, filterQ / 20); + // const filterFcHz = absCentsToHz(getModulated(voice, generatorTypes.initialFilterFc, this.midiControllers)); + // // 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 let actualTime = currentTime; diff --git a/src/spessasynth_lib/synthetizer/worklet_channel/worklet_channel.js b/src/spessasynth_lib/synthetizer/worklet_channel/worklet_channel.js index 43cc7c32..222b4014 100644 --- a/src/spessasynth_lib/synthetizer/worklet_channel/worklet_channel.js +++ b/src/spessasynth_lib/synthetizer/worklet_channel/worklet_channel.js @@ -30,7 +30,6 @@ import { consoleColors } from '../../utils/other.js' import { modulatorSources } from '../../soundfont/chunk/modulators.js' import { midiControllers } from '../../midi_parser/midi_message.js' import { addAndClampGenerator, generatorTypes } from '../../soundfont/chunk/generators.js' - const CHANNEL_GAIN = 0.5; export const NON_CC_INDEX_OFFSET = 128; @@ -160,6 +159,7 @@ export class WorkletChannel { * @type {boolean} */ this.lockPreset = false; + this.lockVibrato = false; } /** @@ -170,6 +170,16 @@ export class WorkletChannel { this.worklet.port.postMessage(data); } + muteChannel() + { + this.gainController.gain.value = 0; + } + + unmuteChannel() + { + this.gainController.gain.value = CHANNEL_GAIN; + } + /** * @param cc {number} * @param val {number} @@ -493,6 +503,10 @@ export class WorkletChannel { // vibrato rate case 8: + if(this.lockVibrato) + { + return; + } if(dataValue === 64) { return; diff --git a/src/website/css/synthesizer_ui.css b/src/website/css/synthesizer_ui.css index acc89798..6568f4fa 100644 --- a/src/website/css/synthesizer_ui.css +++ b/src/website/css/synthesizer_ui.css @@ -115,6 +115,20 @@ display: block; } +.mute_button +{ + flex: 0; + display: flex; + justify-content: center; + align-items: center; + border: #777 1px solid; + min-width: var(--voice-meter-height); +} + +.mute_button:hover{ + cursor: pointer; +} + .voice_reset{ flex: 0; min-width: 0; diff --git a/src/website/ui/icons.js b/src/website/ui/icons.js index c868b725..b3887c2a 100644 --- a/src/website/ui/icons.js +++ b/src/website/ui/icons.js @@ -65,4 +65,20 @@ export function getBackwardSvg(size) return ` `; +} + +export function getVolumeSvg(size) +{ + return ` + + + +`; +} + +export function getMuteSvg(size) +{ + return ` + +`; } \ No newline at end of file diff --git a/src/website/ui/synthesizer_ui/synthetizer_ui.js b/src/website/ui/synthesizer_ui/synthetizer_ui.js index 3125b4f5..46f85928 100644 --- a/src/website/ui/synthesizer_ui/synthetizer_ui.js +++ b/src/website/ui/synthesizer_ui/synthetizer_ui.js @@ -1,6 +1,6 @@ import { DEFAULT_GAIN, Synthetizer } from '../../../spessasynth_lib/synthetizer/synthetizer.js' import {MidiChannel} from "../../../spessasynth_lib/synthetizer/buffer_voice/midi_channel.js"; -import { getLoopSvg } from '../icons.js'; +import { getLoopSvg, getMuteSvg, getVolumeSvg } from '../icons.js' import { ShiftableByteArray } from '../../../spessasynth_lib/utils/shiftable_array.js'; import { Meter } from './synthui_meter.js' import { midiPatchNames } from '../../../spessasynth_lib/utils/other.js' @@ -324,9 +324,26 @@ export class SynthetizerUI this.synth.midiChannels[channelNumber].lockPreset = false; presetSelector.classList.remove("locked_selector"); } - controller.appendChild(presetReset); + // mute button + const muteButton = document.createElement("div"); + muteButton.innerHTML = getVolumeSvg(32); + muteButton.classList.add("controller_element"); + muteButton.classList.add("mute_button"); + muteButton.onclick = () => { + if(this.synth.midiChannels[channelNumber].gainController.gain.value === 0) + { + this.synth.midiChannels[channelNumber].unmuteChannel(); + muteButton.innerHTML = getVolumeSvg(32); + } + else + { + this.synth.midiChannels[channelNumber].muteChannel(); + muteButton.innerHTML = getMuteSvg(32); + } + } + controller.appendChild(muteButton); return { controller: controller,