From a0a88a323f30f22eec9d4812ac714e521da0bd57 Mon Sep 17 00:00:00 2001 From: spessasus Date: Sun, 23 Jun 2024 14:43:17 +0200 Subject: [PATCH] add stabilizing waveforms off by default bugfixes settings rework --- package.json | 2 +- .../synthetizer/synthetizer.js | 5 +- .../worklet_methods/controller_control.js | 75 ++- .../worklet_methods/handle_message.js | 2 +- .../worklet_utilities/worklet_message.js | 2 +- src/website/js/renderer/render_waveforms.js | 39 +- src/website/js/renderer/renderer.js | 1 + .../settings_ui/handlers/interface_handler.js | 68 +++ .../settings_ui/handlers/keyboard_handler.js | 55 ++ .../js/settings_ui/handlers/midi_handler.js | 102 ++++ .../settings_ui/handlers/renderer_handler.js | 71 +++ .../js/settings_ui/saving/load_settings.js | 97 ++++ .../js/settings_ui/saving/save_settings.js | 12 + .../saving/saved_settings_typedef.js | 35 ++ .../settings_ui/saving/serialize_settings.js | 39 ++ src/website/js/settings_ui/settings.js | 531 ++---------------- src/website/js/settings_ui/settings_html.js | 2 + .../js/synthesizer_ui/synthetizer_ui.js | 4 +- .../locale_en/settings/renderer_settings.js | 5 + .../locale_ja/settings/renderer_settings.js | 5 + .../locale_pl/settings/renderer_settings.js | 5 + 21 files changed, 619 insertions(+), 538 deletions(-) create mode 100644 src/website/js/settings_ui/handlers/interface_handler.js create mode 100644 src/website/js/settings_ui/handlers/keyboard_handler.js create mode 100644 src/website/js/settings_ui/handlers/midi_handler.js create mode 100644 src/website/js/settings_ui/handlers/renderer_handler.js create mode 100644 src/website/js/settings_ui/saving/load_settings.js create mode 100644 src/website/js/settings_ui/saving/save_settings.js create mode 100644 src/website/js/settings_ui/saving/saved_settings_typedef.js create mode 100644 src/website/js/settings_ui/saving/serialize_settings.js diff --git a/package.json b/package.json index 84aec528..97f9a059 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "SpessaSynth", - "version": "3.1.0", + "version": "3.2.1", "dependencies": { "@types/webaudioapi": "^0.0.27", "express": "^4.18.2", diff --git a/src/spessasynth_lib/synthetizer/synthetizer.js b/src/spessasynth_lib/synthetizer/synthetizer.js index 6dd6b446..0320f723 100644 --- a/src/spessasynth_lib/synthetizer/synthetizer.js +++ b/src/spessasynth_lib/synthetizer/synthetizer.js @@ -356,15 +356,16 @@ export class Synthetizer { * @param channel {number} usually 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 + * @param force {boolean} forces the controller change, even if it's locked or gm system is set and the cc is bank select */ - controllerChange(channel, controllerNumber, controllerValue) + controllerChange(channel, controllerNumber, controllerValue, force=false) { controllerValue = Math.floor(controllerValue); controllerNumber = Math.floor(controllerNumber); this.post({ channelNumber: channel, messageType: workletMessageType.ccChange, - messageData: [controllerNumber, controllerValue] + messageData: [controllerNumber, controllerValue, force] }); } diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/controller_control.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/controller_control.js index 15125102..8d0615ec 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/controller_control.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/controller_control.js @@ -15,9 +15,10 @@ import { SYNTHESIZER_GAIN } from '../worklet_utilities/main_processor.js' * @param channel {number} * @param controllerNumber {number} * @param controllerValue {number} + * @param force {boolean} * @this {SpessaSynthProcessor} */ -export function controllerChange(channel, controllerNumber, controllerValue) +export function controllerChange(channel, controllerNumber, controllerValue, force = false) { /** * @type {WorkletProcessorChannel} @@ -34,45 +35,43 @@ export function controllerChange(channel, controllerNumber, controllerValue) case midiControllers.bankSelect: let bankNr = controllerValue; - switch (this.system) + if(!force) { - case "gm": - // gm ignores bank select - SpessaSynthInfo(`%cIgnoring the Bank Select (${controllerValue}), as the synth is in GM mode.`, consoleColors.info); - return; - - case "xg": - // for xg, if msb is 127, then it's drums - if (bankNr === 127) - { - channelObject.drumChannel = true; - this.callEvent("drumchange", { - channel: channel, - isDrumChannel: true - }); - } - break; - - case "gm2": - if(bankNr === 120) - { - channelObject.drumChannel = true; - this.callEvent("drumchange", { - channel: channel, - isDrumChannel: true - }); - } - } + switch (this.system) { + case "gm": + // gm ignores bank select + SpessaSynthInfo(`%cIgnoring the Bank Select (${controllerValue}), as the synth is in GM mode.`, consoleColors.info); + return; + + case "xg": + // for xg, if msb is 127, then it's drums + if (bankNr === 127) { + channelObject.drumChannel = true; + this.callEvent("drumchange", { + channel: channel, + isDrumChannel: true + }); + } + break; + + case "gm2": + if (bankNr === 120) { + channelObject.drumChannel = true; + this.callEvent("drumchange", { + channel: channel, + isDrumChannel: true + }); + } + } - if(channelObject.drumChannel) - { - // 128 for percussion channel - bankNr = 128; - } - if(bankNr === 128 && !channelObject.drumChannel) - { - // if channel is not for percussion, default to bank current - bankNr = channelObject.midiControllers[midiControllers.bankSelect]; + if (channelObject.drumChannel) { + // 128 for percussion channel + bankNr = 128; + } + if (bankNr === 128 && !channelObject.drumChannel) { + // if channel is not for percussion, default to bank current + bankNr = channelObject.midiControllers[midiControllers.bankSelect]; + } } channelObject.midiControllers[midiControllers.bankSelect] = bankNr; diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/handle_message.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/handle_message.js index c39a83b6..3a08e2f2 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/handle_message.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/handle_message.js @@ -31,7 +31,7 @@ export function handleMessage(message) break; case workletMessageType.ccChange: - this.controllerChange(channel, data[0], data[1]); + this.controllerChange(channel, data[0], data[1], data[2]); break; case workletMessageType.customcCcChange: diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_message.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_message.js index 7f81bcb9..63722a95 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_message.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_message.js @@ -2,7 +2,7 @@ * @enum {number} * @property {number} noteOff - 0 -> midiNote: Every message needs a channel number (if not relevant or all, set to -1) * @property {number} noteOn - 1 -> [midiNote, velocity, enableDebugging] - * @property {number} ccChange - 2 -> [ccNumber, ccValue] + * @property {number} ccChange - 2 -> [ccNumber, ccValue, force] * @property {number} programChange - 3 -> [programNumber, userChange] * @property {number} killNote - 4 -> midiNote * @property {number} ccReset - 5 -> (no data) note: if channel is -1 then reset all channels diff --git a/src/website/js/renderer/render_waveforms.js b/src/website/js/renderer/render_waveforms.js index 6fc4281d..fd926bd2 100644 --- a/src/website/js/renderer/render_waveforms.js +++ b/src/website/js/renderer/render_waveforms.js @@ -47,16 +47,39 @@ export function renderWaveforms() this.drawingContext.lineWidth = this.lineThickness; this.drawingContext.strokeStyle = this.channelColors[channelNumber]; this.drawingContext.beginPath(); + if(this.stabilizeWaveforms) + { + const length = waveform.length / 2; + // Oscilloscope triggering + let triggerPoint = 0; + const threshold = 0; // Adjust this if necessary + for (let i = 1; i < waveform.length; i++) { + if (waveform[i - 1] < threshold && waveform[i] >= threshold) { + triggerPoint = i; + break; + } + } + const step = waveWidth / length; - const step = waveWidth / waveform.length; - - let xPos = relativeX; + let xPos = relativeX; + for (let i = triggerPoint; i < triggerPoint + length; i++) { + this.drawingContext.lineTo( + xPos, + relativeY + waveform[i] * multiplier); + xPos += step; + } + } + else + { + const step = waveWidth / waveform.length; - for (let i = 0; i < waveform.length; i++) { - this.drawingContext.lineTo( - xPos, - relativeY + waveform[i] * multiplier); - xPos += step; + let xPos = relativeX; + for (let i = 0; i < waveform.length; i++) { + this.drawingContext.lineTo( + xPos, + relativeY + waveform[i] * multiplier); + xPos += step; + } } this.drawingContext.stroke(); diff --git a/src/website/js/renderer/renderer.js b/src/website/js/renderer/renderer.js index af455c3a..3c40f842 100644 --- a/src/website/js/renderer/renderer.js +++ b/src/website/js/renderer/renderer.js @@ -89,6 +89,7 @@ class Renderer this.renderNotes = true; this.drawActiveNotes = true; this.showVisualPitch = true; + this.stabilizeWaveforms = false; /** * @type {boolean[]} */ diff --git a/src/website/js/settings_ui/handlers/interface_handler.js b/src/website/js/settings_ui/handlers/interface_handler.js new file mode 100644 index 00000000..010c712d --- /dev/null +++ b/src/website/js/settings_ui/handlers/interface_handler.js @@ -0,0 +1,68 @@ +/** + * @this {Settings} + * @private + */ +export function _toggleDarkMode() +{ + if(this.mode === "dark") + { + this.mode = "light"; + this.renderer.drawActiveNotes = false; + } + else + { + this.renderer.drawActiveNotes = true; + this.mode = "dark"; + + } + this.renderer.toggleDarkMode(); + this.synthui.toggleDarkMode(); + this.sequi.toggleDarkMode() + + // top part + document.getElementsByClassName("top_part")[0].classList.toggle("top_part_light"); + + // settings + this.mainDiv.classList.toggle("settings_menu_light"); + + // rest + // things get hacky here: change the global (*) --font-color to black: + // find the star rule + const rules = document.styleSheets[0].cssRules; + for(let rule of rules) + { + if(rule.selectorText === "*") + { + rule.style.setProperty("--font-color", this.mode === "dark" ? "#eee" : "#333"); + rule.style.setProperty("--top-buttons-color", this.mode === "dark" ? "linear-gradient(201deg, #222, #333)" : "linear-gradient(270deg, #ddd, #fff)"); + break; + } + } + document.body.style.background = this.mode === "dark" ? "black" : "white"; +} + +/** + * @this {Settings} + * @private + */ +export function _createInterfaceSettingsHandler() +{ + const button = this.htmlControls.interface.themeSelector; + button.onclick = () => { + this._toggleDarkMode(); + this._saveSettings(); + } + const select = this.htmlControls.interface.languageSelector; + // load up the languages + for(const [code, locale] of Object.entries(this.locales)) + { + const option = document.createElement("option"); + option.value = code; + option.textContent = locale.localeName + select.appendChild(option); + } + select.onchange = () => { + this.locale.changeGlobalLocale(this.locales[select.value]); + this._saveSettings(); + } +} \ No newline at end of file diff --git a/src/website/js/settings_ui/handlers/keyboard_handler.js b/src/website/js/settings_ui/handlers/keyboard_handler.js new file mode 100644 index 00000000..cd912890 --- /dev/null +++ b/src/website/js/settings_ui/handlers/keyboard_handler.js @@ -0,0 +1,55 @@ +/** + * The channel colors are taken from synthui + * @param keyboard {MidiKeyboard} + * @param synthui {SynthetizerUI} + * @param renderer {Renderer} + * @this {Settings} + * @private + */ +export function _createKeyboardHandler( keyboard, synthui, renderer) +{ + let channelNumber = 0; + + const keyboardControls = this.htmlControls.keyboard; + + const createChannel = () => + { + const option = document.createElement("option"); + + option.value = channelNumber.toString(); + // Channel: {0} gets formatred to channel number + this.locale.bindObjectProperty(option, "textContent", "locale.settings.keyboardSettings.selectedChannel.channelOption", [channelNumber + 1]); + + option.style.background = synthui.channelColors[channelNumber % synthui.channelColors.length]; + option.style.color = "rgb(0, 0, 0)"; + + keyboardControls.channelSelector.appendChild(option); + channelNumber++; + } + + // create the initial synth channels+ + for (let i = 0; i { + keyboard.selectChannel(parseInt(keyboardControls.channelSelector.value)); + } + + keyboardControls.sizeSelector.onchange = () => { + keyboard.keyRange = this.keyboardSizes[keyboardControls.sizeSelector.value]; + renderer.keyRange = this.keyboardSizes[keyboardControls.sizeSelector.value]; + this._saveSettings(); + } + + // listen for new channels + synthui.synth.eventHandler.addEvent("newchannel", "settings-new-channel", () => { + createChannel(); + }); + + // dark mode toggle + keyboardControls.modeSelector.onclick = () => { + keyboard.toggleMode(); + this._saveSettings(); + } +} \ No newline at end of file diff --git a/src/website/js/settings_ui/handlers/midi_handler.js b/src/website/js/settings_ui/handlers/midi_handler.js new file mode 100644 index 00000000..ff369964 --- /dev/null +++ b/src/website/js/settings_ui/handlers/midi_handler.js @@ -0,0 +1,102 @@ +/** + * @param handler {MIDIDeviceHandler} + * @param sequi {SequencerUI} + * @param synthui {SynthetizerUI} + * @this {Settings} + * @private + */ +export function _createMidiSettingsHandler(handler, sequi, synthui) +{ + handler.createMIDIDeviceHandler().then(success => { + if(success) + { + this._createMidiInputHandler(handler, synthui.synth); + this._createMidiOutputHandler(handler, sequi); + } + else + { + document.getElementById("midi_settings").style.display = "none"; + } + }); +} + +/** + * @param handler {MIDIDeviceHandler} + * @param synth {Synthetizer} + * @this {Settings} + * @private + */ +export function _createMidiInputHandler(handler, synth) +{ + // input selector + if(handler.inputs.length < 1) + { + return; + } + // no device + const select = this.htmlControls.midi.inputSelector; + for(const input of handler.inputs) + { + const option = document.createElement("option"); + option.value = input[0]; + option.innerText = input[1].name; + select.appendChild(option); + } + select.onchange = () => { + if(select.value === "-1") + { + handler.disconnectAllDevicesFromSynth(); + } + else + { + handler.connectDeviceToSynth(handler.inputs.get(select.value), synth); + } + this._saveSettings(); + } +} + +/** + * note that using sequi allows us to obtain the sequencer after it has been created + * @param handler {MIDIDeviceHandler} + * @param sequi {SequencerUI} + * @this {Settings} + * @private + */ +export function _createMidiOutputHandler(handler, sequi) +{ + if(!handler.outputs) + { + setTimeout(() => { + this._createMidiOutputHandler(handler, sequi); + }, 1000); + return; + } + if(handler.outputs.length < 1) + { + return; + } + const select = this.htmlControls.midi.outputSelector; + for(const output of handler.outputs) + { + const option = document.createElement("option"); + option.value = output[0]; + option.innerText = output[1].name; + select.appendChild(option); + } + + select.onchange = () => { + if(!sequi.seq) + { + return; + } + if(select.value === "-1") + { + handler.disconnectSeqFromMIDI(sequi.seq); + } + else + { + handler.connectMIDIOutputToSeq(handler.outputs.get(select.value), sequi.seq); + } + this._saveSettings(); + } +} \ No newline at end of file diff --git a/src/website/js/settings_ui/handlers/renderer_handler.js b/src/website/js/settings_ui/handlers/renderer_handler.js new file mode 100644 index 00000000..b7b2be4a --- /dev/null +++ b/src/website/js/settings_ui/handlers/renderer_handler.js @@ -0,0 +1,71 @@ +/** + * @param renderer {Renderer} + * @this {Settings} + * @private + */ +export function _createRendererHandler(renderer) +{ + const rendererControls = this.htmlControls.renderer; + + // note falling time + rendererControls.noteTimeSlider.oninput = () => { + renderer.noteFallingTimeMs = rendererControls.noteTimeSlider.value; + rendererControls.noteTimeSlider.nextElementSibling.innerText = `${rendererControls.noteTimeSlider.value}ms` + } + // bind to onchange instead of oniinput to prevent spam + rendererControls.noteTimeSlider.onchange = () => { this._saveSettings(); } + + // waveform line thickness + rendererControls.analyserThicknessSlider.oninput = () => { + renderer.lineThickness = parseInt(rendererControls.analyserThicknessSlider.value); + rendererControls.analyserThicknessSlider.nextElementSibling.innerText = `${rendererControls.analyserThicknessSlider.value}px`; + } + rendererControls.analyserThicknessSlider.onchange = () => { this._saveSettings(); } + + // fft size (sample size) + rendererControls.analyserFftSlider.oninput = () => { + let value = Math.pow(2, parseInt(rendererControls.analyserFftSlider.value)); + renderer.normalAnalyserFft = value; + renderer.drumAnalyserFft = Math.pow(2, Math.min(15, parseInt(rendererControls.analyserFftSlider.value) + 2)); + renderer.updateFftSize(); + rendererControls.analyserFftSlider.nextElementSibling.innerText = `${value}`; + } + rendererControls.analyserFftSlider.onchange = () => { this._saveSettings(); } + + // wave multiplier + rendererControls.waveMultiplierSlizer.oninput = () => { + renderer.waveMultiplier = parseInt(rendererControls.waveMultiplierSlizer.value); + rendererControls.waveMultiplierSlizer.nextElementSibling.innerText = rendererControls.waveMultiplierSlizer.value; + } + rendererControls.waveMultiplierSlizer.onchange = () => { this._saveSettings(); } + + // render waveforms + rendererControls.analyserToggler.onclick = () => { + renderer.renderAnalysers = !renderer.renderAnalysers; + this._saveSettings() + }; + + // render notes + rendererControls.noteToggler.onclick = () => { + renderer.renderNotes = !renderer.renderNotes; + this._saveSettings() + }; + + // render active notes effect + rendererControls.activeNoteToggler.onclick = () => { + renderer.drawActiveNotes = !renderer.drawActiveNotes; + this._saveSettings() + }; + + // show visual pitch + rendererControls.visualPitchToggler.onclick = () => { + renderer.showVisualPitch = !renderer.showVisualPitch; + this._saveSettings(); + }; + + // stabilize waveforms + rendererControls.stabilizeWaveformsToggler.onclick = () => { + renderer.stabilizeWaveforms = !renderer.stabilizeWaveforms; + this._saveSettings(); + } +} \ No newline at end of file diff --git a/src/website/js/settings_ui/saving/load_settings.js b/src/website/js/settings_ui/saving/load_settings.js new file mode 100644 index 00000000..0136192e --- /dev/null +++ b/src/website/js/settings_ui/saving/load_settings.js @@ -0,0 +1,97 @@ +import { SpessaSynthInfo } from '../../../../spessasynth_lib/utils/loggin.js' +import { DEFAULT_LOCALE } from '../../../locale/locale_files/locale_list.js' + +/** + * @private + * @this {Settings} + */ +export async function _loadSettings() +{ + /** + * @type {SavedSettings} + */ + const savedSettings = await window.savedSettings; + + if(!savedSettings.interface) + { + return; + } + + SpessaSynthInfo("Loading saved settings...", savedSettings) + + // renderer + const rendererControls = this.htmlControls.renderer; + const renderer = this.renderer; + const rendererValues = savedSettings.renderer; + // note falling time + renderer.noteFallingTimeMs = rendererValues.noteFallingTimeMs; + rendererControls.noteTimeSlider.value = rendererValues.noteFallingTimeMs; + rendererControls.noteTimeSlider.nextElementSibling.innerText = `${rendererValues.noteFallingTimeMs}ms` + + // waveform line thickness + rendererControls.analyserThicknessSlider.value = rendererValues.waveformThickness + renderer.lineThickness = rendererValues.waveformThickness; + rendererControls.analyserThicknessSlider.nextElementSibling.innerText = `${rendererValues.waveformThickness}px`; + + // fft size (sample size) + let value = rendererValues.sampleSize; + // Math.pow(2, parseInt(rendererControls.analyserFftSlider.value)); we need to invert this + rendererControls.analyserFftSlider.value = Math.log2(value); + renderer.normalAnalyserFft = value; + renderer.drumAnalyserFft = Math.pow(2, Math.min(15, Math.log2(value) + 2)); + renderer.updateFftSize(); + rendererControls.analyserFftSlider.nextElementSibling.innerText = `${value}`; + + // wave multiplier + renderer.waveMultiplier = rendererValues.amplifier; + rendererControls.waveMultiplierSlizer.value = rendererValues.amplifier; + rendererControls.waveMultiplierSlizer.nextElementSibling.innerText = rendererValues.amplifier; + + // render waveforms + renderer.renderAnalysers = rendererValues.renderWaveforms; + + // render notes + renderer.renderNotes = rendererValues.renderNotes; + + // render active notes effect + renderer.drawActiveNotes = rendererValues.drawActiveNotes; + + // show visual pitch + renderer.showVisualPitch = rendererValues.showVisualPitch; + + // stabilize waveforms + renderer.stabilizeWaveforms = rendererValues.stabilizeWaveforms; + + // keyboard size + renderer.keyRange = rendererValues.keyRange; + + // keyboard + const keyboardControls = this.htmlControls.keyboard; + const keyboard = this.midiKeyboard; + const keyboardValues = savedSettings.keyboard; + + // removed selected channel because it's not something you want to save + + // keyboard size + keyboard.keyRange = keyboardValues.keyRange; + // find the correct option for the size + keyboardControls.sizeSelector.value = Object.keys(this.keyboardSizes) + .find(size => this.keyboardSizes[size].min === keyboardValues.keyRange.min && this.keyboardSizes[size].max === keyboardValues.keyRange.max); + // keyboard theme + if(keyboardValues.mode === "dark") + { + keyboard.toggleMode(); + } + + + // interface + if(savedSettings.interface.language !== DEFAULT_LOCALE) + { + this.locale.changeGlobalLocale(this.locales[savedSettings.interface.language]); + this.htmlControls.interface.languageSelector.value = savedSettings.interface.language; + } + if(savedSettings.interface.mode === "light") + { + this._toggleDarkMode(); + } +} \ No newline at end of file diff --git a/src/website/js/settings_ui/saving/save_settings.js b/src/website/js/settings_ui/saving/save_settings.js new file mode 100644 index 00000000..348bf262 --- /dev/null +++ b/src/website/js/settings_ui/saving/save_settings.js @@ -0,0 +1,12 @@ +// if window.saveSettings function is exposed, call it with _serializeSettings +/** + * @this {Settings} + * @private + */ +export function _saveSettings() +{ + if(window.saveSettings) + { + window.saveSettings(this._serializeSettings()); + } +} \ No newline at end of file diff --git a/src/website/js/settings_ui/saving/saved_settings_typedef.js b/src/website/js/settings_ui/saving/saved_settings_typedef.js new file mode 100644 index 00000000..459398c1 --- /dev/null +++ b/src/website/js/settings_ui/saving/saved_settings_typedef.js @@ -0,0 +1,35 @@ +/** + * @typedef {{ + * keyboard: { + * keyRange: { + * min: number, + * max: number + * }, + * mode: ("light" | "dark"), + * selectedChannel: number + * }, + * renderer: { + * renderNotes: boolean, + * keyRange: { + * min: number, + * max: number + * }, + * noteFallingTimeMs: number, + * renderWaveforms: boolean, + * drawActiveNotes: boolean, + * stabilizeWaveforms: boolean, + * amplifier: number, + * showVisualPitch: boolean, + * sampleSize: number, + * waveformThickness: number + * }, + * midi: { + * output: (null | string), + * input: (null | string) + * }, + * interface: { + * mode: ("light" | "dark"), + * language: string + * } + * }} SavedSettings + */ \ No newline at end of file diff --git a/src/website/js/settings_ui/saving/serialize_settings.js b/src/website/js/settings_ui/saving/serialize_settings.js new file mode 100644 index 00000000..1df5622f --- /dev/null +++ b/src/website/js/settings_ui/saving/serialize_settings.js @@ -0,0 +1,39 @@ +/** + * Serializes settings into a nice object + * @private + * @returns {SavedSettings} + * @this {Settings} + */ +export function _serializeSettings() +{ + return { + renderer: { + noteFallingTimeMs: this.renderer.noteFallingTimeMs, + waveformThickness: this.renderer.lineThickness, + sampleSize: this.renderer.normalAnalyserFft, + amplifier: this.renderer.waveMultiplier, + renderWaveforms: this.renderer.renderNotes, + renderNotes: this.renderer.renderNotes, + drawActiveNotes: this.renderer.drawActiveNotes, + showVisualPitch: this.renderer.showVisualPitch, + stabilizeWaveforms: this.renderer.stabilizeWaveforms, + keyRange: this.renderer.keyRange + }, + + keyboard: { + selectedChannel: this.midiKeyboard.channel, + keyRange: this.midiKeyboard.keyRange, + mode: this.midiKeyboard.mode + }, + + midi: { + input: this.midiDeviceHandler.selectedInput === null ? null : this.midiDeviceHandler.selectedInput.name, + output: this.midiDeviceHandler.selectedOutput === null ? null: this.midiDeviceHandler.selectedOutput.name + }, + + interface: { + mode: this.mode, + language: this.htmlControls.interface.languageSelector.value + } + } +} \ No newline at end of file diff --git a/src/website/js/settings_ui/settings.js b/src/website/js/settings_ui/settings.js index ebd7f62c..c55d246c 100644 --- a/src/website/js/settings_ui/settings.js +++ b/src/website/js/settings_ui/settings.js @@ -1,49 +1,23 @@ import { settingsHtml } from './settings_html.js' import { getDownArrowSvg, getGearSvg } from '../icons.js' -import { DEFAULT_LOCALE } from '../../locale/locale_files/locale_list.js' -import { SpessaSynthInfo } from '../../../spessasynth_lib/utils/loggin.js' +import { _loadSettings } from './saving/load_settings.js' +import { _saveSettings } from './saving/save_settings.js' +import { _serializeSettings } from './saving/serialize_settings.js' +import { _createInterfaceSettingsHandler, _toggleDarkMode } from './handlers/interface_handler.js' +import { _createRendererHandler } from './handlers/renderer_handler.js' +import { + _createMidiInputHandler, + _createMidiOutputHandler, + _createMidiSettingsHandler, +} from './handlers/midi_handler.js' +import { _createKeyboardHandler } from './handlers/keyboard_handler.js' /** * settings.js * purpose: manages the gui settings, controlling things like render settings, light mode etc. */ -/** - * @typedef {{ - * keyboard: { - * keyRange: { - * min: number, - * max: number - * }, - * mode: ("light" | "dark"), - * selectedChannel: number - * }, - * renderer: { - * renderNotes: boolean, - * keyRange: { - * min: number, - * max: number - * }, - * noteFallingTimeMs: number, - * renderWaveforms: boolean, - * drawActiveNotes: boolean, - * amplifier: number, - * showVisualPitch: boolean, - * sampleSize: number, - * waveformThickness: number - * }, - * midi: { - * output: (null | string), - * input: (null | string) - * }, - * interface: { - * mode: ("light" | "dark"), - * language: string - * } - * }} SavedSettings - */ - -export class Settings +class Settings { /** * Creates a new instance of synthetizer UI @@ -88,28 +62,23 @@ export class Settings const musicModeButton = document.createElement("div"); musicModeButton.classList.add("seamless_button"); - this.locale.bindObjectProperty(musicModeButton, "innerText", "locale.musicPlayerMode.toggleButton.title"); this.locale.bindObjectProperty(musicModeButton, "title", "locale.musicPlayerMode.toggleButton.description"); - settingsWrapper.appendChild(musicModeButton); const hideTopButton = document.createElement("div"); hideTopButton.classList.add("seamless_button"); - this.locale.bindObjectProperty(hideTopButton, "innerText", "locale.hideTopBar.title"); this.locale.bindObjectProperty(hideTopButton, "title", "locale.hideTopBar.description"); - settingsWrapper.appendChild(hideTopButton); let text = document.createElement('span'); this.locale.bindObjectProperty(text, "innerText", "locale.settings.toggleButton"); + settingsButton.appendChild(text); let gear = document.createElement('div'); gear.innerHTML = getGearSvg(32); gear.classList.add("gear"); - - settingsButton.appendChild(text); settingsButton.appendChild(gear); this.mainDiv = document.createElement("div"); @@ -168,36 +137,7 @@ export class Settings this.locale.bindObjectProperty(element, "title", path + ".description"); } - // get the html controllers - this.htmlControls = { - renderer: { - noteTimeSlider: document.getElementById("note_time_slider"), - analyserToggler: document.getElementById("analyser_toggler"), - noteToggler: document.getElementById("note_toggler"), - activeNoteToggler: document.getElementById("active_note_toggler"), - visualPitchToggler: document.getElementById("visual_pitch_toggler"), - - analyserThicknessSlider: document.getElementById("analyser_thickness_slider"), - analyserFftSlider: document.getElementById("analyser_fft_slider"), - waveMultiplierSlizer: document.getElementById("wave_multiplier_slider"), - }, - - keyboard: { - channelSelector: document.getElementById("channel_selector"), - modeSelector: document.getElementById("mode_selector"), - sizeSelector: document.getElementById("keyboard_size_selector"), - }, - - midi: { - outputSelector: document.getElementById("midi_output_selector"), - inputSelector: document.getElementById("midi_input_selector") - }, - - interface: { - themeSelector: document.getElementById("toggle_mode_button"), - languageSelector: document.getElementById("language_selector") - } - } + this.getHtmlControls(); // create handlers for all settings this._createRendererHandler(renderer); @@ -229,431 +169,52 @@ export class Settings } } - /** - * @private - */ - async _loadSettings() - { - /** - * @type {SavedSettings} - */ - const savedSettings = await window.savedSettings; - - if(!savedSettings.interface) - { - return; - } - - SpessaSynthInfo("Loading saved settings...", savedSettings) - - // renderer - const rendererControls = this.htmlControls.renderer; - const renderer = this.renderer; - const rendererValues = savedSettings.renderer; - // note falling time - renderer.noteFallingTimeMs = rendererValues.noteFallingTimeMs; - rendererControls.noteTimeSlider.value = rendererValues.noteFallingTimeMs; - rendererControls.noteTimeSlider.nextElementSibling.innerText = `${rendererValues.noteFallingTimeMs}ms` - - // waveform line thickness - rendererControls.analyserThicknessSlider.value = rendererValues.waveformThickness - renderer.lineThickness = rendererValues.waveformThickness; - rendererControls.analyserThicknessSlider.nextElementSibling.innerText = `${rendererValues.waveformThickness}px`; - - // fft size (sample size) - let value = rendererValues.sampleSize; - // Math.pow(2, parseInt(rendererControls.analyserFftSlider.value)); we need to invert this - rendererControls.analyserFftSlider.value = Math.log2(value); - renderer.normalAnalyserFft = value; - renderer.drumAnalyserFft = Math.pow(2, Math.min(15, Math.log2(value) + 2)); - renderer.updateFftSize(); - rendererControls.analyserFftSlider.nextElementSibling.innerText = `${value}`; - - // wave multiplier - renderer.waveMultiplier = rendererValues.amplifier; - rendererControls.waveMultiplierSlizer.value = rendererValues.amplifier; - rendererControls.waveMultiplierSlizer.nextElementSibling.innerText = rendererValues.amplifier; - - // render waveforms - renderer.renderAnalysers = rendererValues.renderWaveforms; - - // render notes - renderer.renderNotes = rendererValues.renderNotes; - - // render active notes effect - renderer.drawActiveNotes = rendererValues.drawActiveNotes; - - // show visual pitch - renderer.showVisualPitch = rendererValues.showVisualPitch; - - // keyboard size - renderer.keyRange = rendererValues.keyRange; - - // keyboard - const keyboardControls = this.htmlControls.keyboard; - const keyboard = this.midiKeyboard; - const keyboardValues = savedSettings.keyboard; - - // removed selected channel because it's not something you want to save - - // keyboard size - keyboard.keyRange = keyboardValues.keyRange; - // find the correct option for the size - keyboardControls.sizeSelector.value = Object.keys(this.keyboardSizes) - .find(size => this.keyboardSizes[size].min === keyboardValues.keyRange.min && this.keyboardSizes[size].max === keyboardValues.keyRange.max); - // keyboard theme - if(keyboardValues.mode === "dark") - { - keyboard.toggleMode(); - } - - - // interface - if(savedSettings.interface.language !== DEFAULT_LOCALE) - { - this.locale.changeGlobalLocale(this.locales[savedSettings.interface.language]); - this.htmlControls.interface.languageSelector.value = savedSettings.interface.language; - } - if(savedSettings.interface.mode === "light") - { - this._toggleDarkMode(); - } - } - - - - // if window.saveSettings function is exposed, call it with _serializeSettings - _saveSettings() - { - if(window.saveSettings) - { - window.saveSettings(this._serializeSettings()); - } - } - - /** - * Serializes settings into a nice object - * @private - * @returns {SavedSettings} - */ - _serializeSettings() + getHtmlControls() { - return { + // get the html controllers + this.htmlControls = { renderer: { - noteFallingTimeMs: this.renderer.noteFallingTimeMs, - waveformThickness: this.renderer.lineThickness, - sampleSize: this.renderer.normalAnalyserFft, - amplifier: this.renderer.waveMultiplier, - renderWaveforms: this.renderer.renderNotes, - renderNotes: this.renderer.renderNotes, - drawActiveNotes: this.renderer.drawActiveNotes, - showVisualPitch: this.renderer.showVisualPitch, - keyRange: this.renderer.keyRange + noteTimeSlider: document.getElementById("note_time_slider"), + analyserToggler: document.getElementById("analyser_toggler"), + noteToggler: document.getElementById("note_toggler"), + activeNoteToggler: document.getElementById("active_note_toggler"), + visualPitchToggler: document.getElementById("visual_pitch_toggler"), + stabilizeWaveformsToggler: document.getElementById("stabilize_waveforms_toggler"), + + analyserThicknessSlider: document.getElementById("analyser_thickness_slider"), + analyserFftSlider: document.getElementById("analyser_fft_slider"), + waveMultiplierSlizer: document.getElementById("wave_multiplier_slider"), }, keyboard: { - selectedChannel: this.midiKeyboard.channel, - keyRange: this.midiKeyboard.keyRange, - mode: this.midiKeyboard.mode + channelSelector: document.getElementById("channel_selector"), + modeSelector: document.getElementById("mode_selector"), + sizeSelector: document.getElementById("keyboard_size_selector"), }, midi: { - input: this.midiDeviceHandler.selectedInput === null ? null : this.midiDeviceHandler.selectedInput.name, - output: this.midiDeviceHandler.selectedOutput === null ? null: this.midiDeviceHandler.selectedOutput.name + outputSelector: document.getElementById("midi_output_selector"), + inputSelector: document.getElementById("midi_input_selector") }, interface: { - mode: this.mode, - language: this.htmlControls.interface.languageSelector.value - } - } - } - - /** - * - * @private - */ - _toggleDarkMode() - { - if(this.mode === "dark") - { - this.mode = "light"; - this.renderer.drawActiveNotes = false; - } - else - { - this.renderer.drawActiveNotes = true; - this.mode = "dark"; - - } - this.renderer.toggleDarkMode(); - this.synthui.toggleDarkMode(); - this.sequi.toggleDarkMode() - - // top part - document.getElementsByClassName("top_part")[0].classList.toggle("top_part_light"); - - // settings - this.mainDiv.classList.toggle("settings_menu_light"); - - // rest - // things get hacky here: change the global (*) --font-color to black: - // find the star rule - const rules = document.styleSheets[0].cssRules; - for(let rule of rules) - { - if(rule.selectorText === "*") - { - rule.style.setProperty("--font-color", this.mode === "dark" ? "#eee" : "#333"); - rule.style.setProperty("--top-buttons-color", this.mode === "dark" ? "linear-gradient(201deg, #222, #333)" : "linear-gradient(270deg, #ddd, #fff)"); - break; - } - } - document.body.style.background = this.mode === "dark" ? "black" : "white"; - } - - /** - * @private - */ - _createInterfaceSettingsHandler() - { - const button = this.htmlControls.interface.themeSelector; - button.onclick = () => { - this._toggleDarkMode(); - this._saveSettings(); - } - const select = this.htmlControls.interface.languageSelector; - // load up the languages - for(const [code, locale] of Object.entries(this.locales)) - { - const option = document.createElement("option"); - option.value = code; - option.textContent = locale.localeName - select.appendChild(option); - } - select.onchange = () => { - this.locale.changeGlobalLocale(this.locales[select.value]); - this._saveSettings(); - } - } - - /** - * @param handler {MIDIDeviceHandler} - * @param sequi {SequencerUI} - * @param synthui {SynthetizerUI} - * @private - */ - _createMidiSettingsHandler(handler, sequi, synthui) - { - handler.createMIDIDeviceHandler().then(success => { - if(success) - { - this._createMidiInputHandler(handler, synthui.synth); - this._createMidiOutputHandler(handler, sequi); - } - else - { - document.getElementById("midi_settings").style.display = "none"; - } - }); - } - - /** - * @param handler {MIDIDeviceHandler} - * @param synth {Synthetizer} - * @private - */ - _createMidiInputHandler(handler, synth) - { - // input selector - if(handler.inputs.length < 1) - { - return; - } - // no device - const select = this.htmlControls.midi.inputSelector; - for(const input of handler.inputs) - { - const option = document.createElement("option"); - option.value = input[0]; - option.innerText = input[1].name; - select.appendChild(option); - } - select.onchange = () => { - if(select.value === "-1") - { - handler.disconnectAllDevicesFromSynth(); - } - else - { - handler.connectDeviceToSynth(handler.inputs.get(select.value), synth); - } - this._saveSettings(); - } - } - - /** - * note that using sequi allows us to obtain the sequencer after it has been created - * @param handler {MIDIDeviceHandler} - * @param sequi {SequencerUI} - * @private - */ - _createMidiOutputHandler(handler, sequi) - { - if(!handler.outputs) - { - setTimeout(() => { - this._createMidiOutputHandler(handler, sequi); - }, 1000); - return; - } - if(handler.outputs.length < 1) - { - return; - } - const select = this.htmlControls.midi.outputSelector; - for(const output of handler.outputs) - { - const option = document.createElement("option"); - option.value = output[0]; - option.innerText = output[1].name; - select.appendChild(option); - } - - select.onchange = () => { - if(!sequi.seq) - { - return; - } - if(select.value === "-1") - { - handler.disconnectSeqFromMIDI(sequi.seq); - } - else - { - handler.connectMIDIOutputToSeq(handler.outputs.get(select.value), sequi.seq); + themeSelector: document.getElementById("toggle_mode_button"), + languageSelector: document.getElementById("language_selector") } - this._saveSettings(); } } +} +Settings.prototype._toggleDarkMode = _toggleDarkMode; +Settings.prototype._createInterfaceSettingsHandler = _createInterfaceSettingsHandler; +Settings.prototype._createRendererHandler = _createRendererHandler; - /** - * The channel colors are taken from synthui - * @param keyboard {MidiKeyboard} - * @param synthui {SynthetizerUI} - * @param renderer {Renderer} - * @private - */ - _createKeyboardHandler( keyboard, synthui, renderer) - { - let channelNumber = 0; - - const keyboardControls = this.htmlControls.keyboard; - - const createChannel = () => - { - const option = document.createElement("option"); +Settings.prototype._createMidiSettingsHandler = _createMidiSettingsHandler; +Settings.prototype._createMidiInputHandler = _createMidiInputHandler; +Settings.prototype._createMidiOutputHandler = _createMidiOutputHandler; +Settings.prototype._createKeyboardHandler = _createKeyboardHandler; - option.value = channelNumber.toString(); - // Channel: {0} gets formatred to channel number - this.locale.bindObjectProperty(option, "textContent", "locale.settings.keyboardSettings.selectedChannel.channelOption", [channelNumber + 1]); - - option.style.background = synthui.channelColors[channelNumber % synthui.channelColors.length]; - option.style.color = "rgb(0, 0, 0)"; - - keyboardControls.channelSelector.appendChild(option); - channelNumber++; - } - - // create the initial synth channels+ - for (let i = 0; i { - keyboard.selectChannel(parseInt(keyboardControls.channelSelector.value)); - } - - keyboardControls.sizeSelector.onchange = () => { - keyboard.keyRange = this.keyboardSizes[keyboardControls.sizeSelector.value]; - renderer.keyRange = this.keyboardSizes[keyboardControls.sizeSelector.value]; - this._saveSettings(); - } +Settings.prototype._loadSettings = _loadSettings; +Settings.prototype._serializeSettings = _serializeSettings; +Settings.prototype._saveSettings = _saveSettings; - // listen for new channels - synthui.synth.eventHandler.addEvent("newchannel", "settings-new-channel", () => { - createChannel(); - }); - - // dark mode toggle - keyboardControls.modeSelector.onclick = () => { - keyboard.toggleMode(); - this._saveSettings(); - } - } - - /** - * @param renderer {Renderer} - * @private - */ - _createRendererHandler(renderer) - { - const rendererControls = this.htmlControls.renderer; - - // note falling time - rendererControls.noteTimeSlider.oninput = () => { - renderer.noteFallingTimeMs = rendererControls.noteTimeSlider.value; - rendererControls.noteTimeSlider.nextElementSibling.innerText = `${rendererControls.noteTimeSlider.value}ms` - } - // bind to onchange instead of oniinput to prevent spam - rendererControls.noteTimeSlider.onchange = () => { this._saveSettings(); } - - // waveform line thickness - rendererControls.analyserThicknessSlider.oninput = () => { - renderer.lineThickness = parseInt(rendererControls.analyserThicknessSlider.value); - rendererControls.analyserThicknessSlider.nextElementSibling.innerText = `${rendererControls.analyserThicknessSlider.value}px`; - } - rendererControls.analyserThicknessSlider.onchange = () => { this._saveSettings(); } - - // fft size (sample size) - rendererControls.analyserFftSlider.oninput = () => { - let value = Math.pow(2, parseInt(rendererControls.analyserFftSlider.value)); - renderer.normalAnalyserFft = value; - renderer.drumAnalyserFft = Math.pow(2, Math.min(15, parseInt(rendererControls.analyserFftSlider.value) + 2)); - renderer.updateFftSize(); - rendererControls.analyserFftSlider.nextElementSibling.innerText = `${value}`; - } - rendererControls.analyserFftSlider.onchange = () => { this._saveSettings(); } - - // wave multiplier - rendererControls.waveMultiplierSlizer.oninput = () => { - renderer.waveMultiplier = parseInt(rendererControls.waveMultiplierSlizer.value); - rendererControls.waveMultiplierSlizer.nextElementSibling.innerText = rendererControls.waveMultiplierSlizer.value; - } - rendererControls.waveMultiplierSlizer.onchange = () => { this._saveSettings(); } - - // render waveforms - rendererControls.analyserToggler.onclick = () => { - renderer.renderAnalysers = !renderer.renderAnalysers; - this._saveSettings() - }; - - // render notes - rendererControls.noteToggler.onclick = () => { - renderer.renderNotes = !renderer.renderNotes; - this._saveSettings() - }; - - // render active notes effect - rendererControls.activeNoteToggler.onclick = () => { - renderer.drawActiveNotes = !renderer.drawActiveNotes; - this._saveSettings() - }; - - // show visual pitch - rendererControls.visualPitchToggler.onclick = () => { - renderer.showVisualPitch = !renderer.showVisualPitch; - this._saveSettings(); - }; - } -} \ No newline at end of file +export {Settings} \ No newline at end of file diff --git a/src/website/js/settings_ui/settings_html.js b/src/website/js/settings_ui/settings_html.js index b9fa6f10..2badba79 100644 --- a/src/website/js/settings_ui/settings_html.js +++ b/src/website/js/settings_ui/settings_html.js @@ -45,6 +45,8 @@ export const settingsHtml = `
+
+ diff --git a/src/website/js/synthesizer_ui/synthetizer_ui.js b/src/website/js/synthesizer_ui/synthetizer_ui.js index 87a94cc7..2d26bab0 100644 --- a/src/website/js/synthesizer_ui/synthetizer_ui.js +++ b/src/website/js/synthesizer_ui/synthetizer_ui.js @@ -545,10 +545,10 @@ export class SynthetizerUI this.locale, LOCALE_PATH + "channelController.presetSelector.description", [channelNumber + 1], - presetName => { + async presetName => { const data = JSON.parse(presetName); this.synth.lockController(channelNumber, ALL_CHANNELS_OR_DIFFERENT_ACTION, false); - this.synth.controllerChange(channelNumber, midiControllers.bankSelect, data[0]); + this.synth.controllerChange(channelNumber, midiControllers.bankSelect, data[0], true); this.synth.programChange(channelNumber, data[1], true); presetSelector.mainDiv.classList.add("locked_selector"); this.synth.lockController(channelNumber, ALL_CHANNELS_OR_DIFFERENT_ACTION, true); diff --git a/src/website/locale/locale_files/locale_en/settings/renderer_settings.js b/src/website/locale/locale_files/locale_en/settings/renderer_settings.js index 2269de5e..b171cb21 100644 --- a/src/website/locale/locale_files/locale_en/settings/renderer_settings.js +++ b/src/website/locale/locale_files/locale_en/settings/renderer_settings.js @@ -38,5 +38,10 @@ export const rendererSettingsLocale = { toggleDrawingVisualPitch: { title: "Toggle drawing visual pitch", description: "Toggle notes sliding left or right when the pitch wheel is applied" + }, + + toggleStabilizeWaveforms: { + title: "Stabilize waveforms", + description: "Enable oscilloscope triggering" } } \ No newline at end of file diff --git a/src/website/locale/locale_files/locale_ja/settings/renderer_settings.js b/src/website/locale/locale_files/locale_ja/settings/renderer_settings.js index 1aec5688..bcdf3cf7 100644 --- a/src/website/locale/locale_files/locale_ja/settings/renderer_settings.js +++ b/src/website/locale/locale_files/locale_ja/settings/renderer_settings.js @@ -38,5 +38,10 @@ export const rendererSettingsLocale = { toggleDrawingVisualPitch: { title: "ビジュアルピッチ描画の切り替え", description: "ピッチホイールが適用されたときにノートが左右にスライドする描画を切り替えます" + }, + + toggleStabilizeWaveforms: { + title: "波形を安定させる", + description: "オーディオ波形を安定させる設定を切り替え、波形を固定します。" } } \ No newline at end of file diff --git a/src/website/locale/locale_files/locale_pl/settings/renderer_settings.js b/src/website/locale/locale_files/locale_pl/settings/renderer_settings.js index 7792f3e9..9c71a167 100644 --- a/src/website/locale/locale_files/locale_pl/settings/renderer_settings.js +++ b/src/website/locale/locale_files/locale_pl/settings/renderer_settings.js @@ -38,5 +38,10 @@ export const rendererSettingsLocale = { toggleDrawingVisualPitch: { title: "Przełącz wizualizację wysokości tonu", description: "Przełącz przesuwanie nut w lewo lub w prawo gdy wysokość nut jest zmieniana" + }, + + toggleStabilizeWaveforms: { + title: "Przełącz stabilizację fal", + description: "Przełącz stabilizowanie fal dźwiękowych" } } \ No newline at end of file