diff --git a/package-lock.json b/package-lock.json index 9fa76cda..6152d133 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "SpessaSynth", - "version": "3.2.17", + "version": "3.2.27", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "SpessaSynth", - "version": "3.2.17", + "version": "3.2.27", "hasInstallScript": true } } diff --git a/package.json b/package.json index 31420045..0db0ab70 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "SpessaSynth", - "version": "3.2.25", + "version": "3.2.27", "type": "module", "scripts": { "start": "node src/website/server/server.js", diff --git a/src/website/css/keyboard.css b/src/website/css/keyboard.css index bcb88400..1df16d4f 100644 --- a/src/website/css/keyboard.css +++ b/src/website/css/keyboard.css @@ -10,6 +10,7 @@ background: black; width: 100%; transition: var(--music-mode-transition) transform; + touch-action: none; } #keyboard.out_animation{ @@ -20,6 +21,7 @@ { -webkit-user-select: none; user-select: none; + touch-action: none; flex: 1; transition: transform 0.1s ease; diff --git a/src/website/css/synthesizer_ui/synthesizer_ui.css b/src/website/css/synthesizer_ui/synthesizer_ui.css index 3797330c..f4d19712 100644 --- a/src/website/css/synthesizer_ui/synthesizer_ui.css +++ b/src/website/css/synthesizer_ui/synthesizer_ui.css @@ -68,10 +68,11 @@ display: flex; align-items: stretch; flex-wrap: wrap; + transition: 0.2s ease; } -.no_voices { - filter: brightness(80%); +.channel_controller.no_voices{ + filter: brightness(0.8); } .mute_button diff --git a/src/website/js/keybinds.js b/src/website/js/keybinds.js new file mode 100644 index 00000000..3e85ecab --- /dev/null +++ b/src/website/js/keybinds.js @@ -0,0 +1,21 @@ +/** + * @enum {string} + */ +export const keybinds = { + synthesizerUIShow: "s", + settingsShow: "r", + + blackMidiMode: "b", + midiPanic: "backspace", + + playPause: " ", + toggleLoop: "l", + toggleLyrics: "t", + seekBackwards: "arrowleft", + seekForwards: "arrowright", + previousSong: "[", + nextSong: "]", + + cinematicMode: "c", + videoMode: "v" +} \ No newline at end of file diff --git a/src/website/js/midi_keyboard.js b/src/website/js/midi_keyboard/midi_keyboard.js similarity index 73% rename from src/website/js/midi_keyboard.js rename to src/website/js/midi_keyboard/midi_keyboard.js index 97838354..5725c88f 100644 --- a/src/website/js/midi_keyboard.js +++ b/src/website/js/midi_keyboard/midi_keyboard.js @@ -1,6 +1,6 @@ -import {Synthetizer} from "../../spessasynth_lib/synthetizer/synthetizer.js"; -import { midiControllers } from '../../spessasynth_lib/midi_parser/midi_message.js' -import { isMobile } from './utils/is_mobile.js' +import {Synthetizer} from "../../../spessasynth_lib/synthetizer/synthetizer.js"; +import { midiControllers } from '../../../spessasynth_lib/midi_parser/midi_message.js' +import { _handlePointers } from './pointer_handling.js' /** * midi_keyboard.js @@ -9,7 +9,7 @@ import { isMobile } from './utils/is_mobile.js' const GLOW_PX = 150; -export class MidiKeyboard +class MidiKeyboard { /** * Creates a new midi keyboard(keyboard) @@ -18,13 +18,15 @@ export class MidiKeyboard */ constructor(channelColors, synth) { this.mouseHeld = false; - this.lastKeyPressed = -1; - this.heldKeys = []; + /** + * @type {Set} + */ + this.pressedKeys = new Set(); /** * @type {"light"|"dark"} */ this.mode = "light"; - this.enableDebugging = true; + this.enableDebugging = false; /** * @type {{min: number, max: number}} @@ -35,20 +37,6 @@ export class MidiKeyboard max: 127 }; - document.onpointerdown = () => { - this.mouseHeld = true; - } - document.onpointerup = () => { - this.mouseHeld = false; - this.lastKeyPressed = -1; - for(let key of this.heldKeys) - { - // user note off - this.releaseNote(key, this.channel); - this.synth.noteOff(this.channel, key); - } - } - // hold pedal on document.addEventListener("keydown", e =>{ if(e.key === "Shift") @@ -126,77 +114,7 @@ export class MidiKeyboard this.keys.push(keyElement); } - /** - * @param keyElement {HTMLDivElement} - * @param e {PointerEvent} - */ - const noteOnHandler = (keyElement, e) => { - if (!this.mouseHeld) - { - return; - } - - e.stopPropagation(); - e.preventDefault(); - const midiNote = parseInt(keyElement.id.replace("note", "")); - - if(this.lastKeyPressed === midiNote || isNaN(midiNote)) - { - return; - } - - if(this.lastKeyPressed !== -1) - { - // user note off - this.heldKeys.splice(this.heldKeys.indexOf(this.lastKeyPressed), 1); - this.releaseNote(this.lastKeyPressed, this.channel); - this.synth.noteOff(this.channel, this.lastKeyPressed); - } - - this.lastKeyPressed = midiNote; - - // user note on - if (!this.heldKeys.includes(midiNote)) - { - this.heldKeys.push(midiNote); - } - - let velocity ; - if (isMobile) - { - // ignore precise key velocity on mobile (keys are too small anyways) - velocity = 127; - } - else - { - // determine velocity. lower = more velocity - const rect = keyElement.getBoundingClientRect(); - const relativeY = e.clientY; // Handle both mouse and touch events - const relativeMouseY = relativeY - rect.top; - const keyHeight = rect.height; - velocity = Math.floor(relativeMouseY / keyHeight * 127); - } - this.synth.noteOn(this.channel, midiNote, velocity, this.enableDebugging); - }; - - // POINTER HANDLING - this.keyboard.onpointerdown = e => { - this.mouseHeld = true; - noteOnHandler(document.elementFromPoint(e.clientX, e.clientY), e); - } - - this.keyboard.onpointermove = e => { - noteOnHandler(document.elementFromPoint(e.clientX, e.clientY), e); - }; - - this.keyboard.onpointerleave = () => { - const midiNote = this.lastKeyPressed; - // user note off - this.heldKeys.splice(this.heldKeys.indexOf(midiNote), 1); - this.releaseNote(midiNote, this.channel); - this.synth.noteOff(this.channel, midiNote); - this.lastKeyPressed = -1; - }; + this._handlePointers(); } /** @@ -412,4 +330,6 @@ export class MidiKeyboard this.keyColors[index] = []; }) } -} \ No newline at end of file +} +MidiKeyboard.prototype._handlePointers = _handlePointers; +export { MidiKeyboard }; \ No newline at end of file diff --git a/src/website/js/midi_keyboard/pointer_handling.js b/src/website/js/midi_keyboard/pointer_handling.js new file mode 100644 index 00000000..27e48a08 --- /dev/null +++ b/src/website/js/midi_keyboard/pointer_handling.js @@ -0,0 +1,103 @@ +import { isMobile } from '../utils/is_mobile.js' + +/** + * @this {MidiKeyboard} + * @private + */ +export function _handlePointers() +{ + // POINTER HANDLING + const userNoteOff = note => { + this.pressedKeys.delete(note) + this.releaseNote(note, this.channel); + this.synth.noteOff(this.channel, note); + } + + const userNoteOn = (note, clientY) => { + // user note on + this.pressedKeys.add(note); + + let velocity; + if (isMobile) + { + // ignore precise key velocity on mobile (keys are too small anyways) + velocity = 127; + } + else + { + // determine velocity. lower = more velocity + const keyElement = this.keys[note]; + const rect = keyElement.getBoundingClientRect(); + // Handle both mouse and touch events + const relativeMouseY = clientY - rect.top; + const keyHeight = rect.height; + velocity = Math.floor(relativeMouseY / keyHeight * 127); + } + this.synth.noteOn(this.channel, note, velocity, this.enableDebugging); + } + + /** + * @param e {MouseEvent|TouchEvent} + */ + const moveHandler = e => { + // all currently pressed keys are stored in this.pressedKeys + /** + * @type {Touch[]|MouseEvent[]} + */ + const touches = e.touches ? Array.from(e.touches) : [e]; + /** + * @type {Set} + */ + const currentlyTouchedKeys = new Set(); + touches.forEach(touch => { + const targetKey = document.elementFromPoint(touch.clientX, touch.clientY); + const midiNote = parseInt(targetKey.id.replace("note", "")); + currentlyTouchedKeys.add(midiNote); + if(isNaN(midiNote) || midiNote < 0 || this.pressedKeys.has(midiNote)) + { + // pressed outside of bounds or already pressed + return; + } + userNoteOn(midiNote, touch.clientY); + }); + this.pressedKeys.forEach(key => { + if(!currentlyTouchedKeys.has(key)) + { + userNoteOff(key); + } + }); + }; + + // mouse + document.addEventListener("mousedown", e => { + this.mouseHeld = true; + moveHandler(e); + }); + document.addEventListener("mouseup", () => { + this.mouseHeld = false; + this.pressedKeys.forEach(key => { + userNoteOff(key); + }); + }); + this.keyboard.onmousemove = e => { + if(this.mouseHeld) moveHandler(e); + }; + this.keyboard.onmouseleave = () => { + this.pressedKeys.forEach(key => { + userNoteOff(key); + }); + } + + // touch + this.keyboard.ontouchstart = e => { + moveHandler(e); + }; + this.keyboard.ontouchend = () => { + this.pressedKeys.forEach(key => { + userNoteOff(key); + }); + }; + this.keyboard.addEventListener("touchmove", e => { + moveHandler(e); + }); +} \ No newline at end of file diff --git a/src/website/js/notification.js b/src/website/js/notification.js index 0dba84e8..16f31428 100644 --- a/src/website/js/notification.js +++ b/src/website/js/notification.js @@ -1,4 +1,4 @@ -const NOTIFICATION_TIME = 7000; +const NOTIFICATION_TIME = 13000; /** * @param title {string} * @param message {string} diff --git a/src/website/js/sequencer_ui/sequencer_ui.js b/src/website/js/sequencer_ui/sequencer_ui.js index e9d71cac..f729140f 100644 --- a/src/website/js/sequencer_ui/sequencer_ui.js +++ b/src/website/js/sequencer_ui/sequencer_ui.js @@ -4,6 +4,7 @@ import { supportedEncodings } from '../utils/encodings.js' import { getBackwardSvg, getForwardSvg, getLoopSvg, getPauseSvg, getPlaySvg, getTextSvg } from '../icons.js' import { messageTypes } from '../../../spessasynth_lib/midi_parser/midi_message.js' import { getSeqUIButton } from './sequi_button.js' +import { keybinds } from '../keybinds.js' /** * sequencer_ui.js @@ -398,17 +399,17 @@ export class SequencerUI document.addEventListener("keypress", event => { switch(event.key.toLowerCase()) { - case " ": + case keybinds.playPause: event.preventDefault(); togglePlayback(); break; - case "l": + case keybinds.toggleLoop: event.preventDefault(); toggleLoop(); break; - case "t": + case keybinds.toggleLyrics: event.preventDefault(); toggleLyrics(); break; @@ -435,23 +436,23 @@ export class SequencerUI switch (e.key.toLowerCase()) { - case "arrowleft": + case keybinds.seekBackwards: e.preventDefault(); this.seq.currentTime -= 5; playPauseButton.innerHTML = getPauseSvg(ICON_SIZE); break; - case "arrowright": + case keybinds.seekForwards: e.preventDefault(); this.seq.currentTime += 5; playPauseButton.innerHTML = getPauseSvg(ICON_SIZE); break; - case "[": + case keybinds.previousSong: this.switchToPreviousSong(); break; - case "]": + case keybinds.nextSong: this.switchToNextSong(); break; diff --git a/src/website/js/settings_ui/handlers/keyboard_handler.js b/src/website/js/settings_ui/handlers/keyboard_handler.js index b4baef84..6e44145e 100644 --- a/src/website/js/settings_ui/handlers/keyboard_handler.js +++ b/src/website/js/settings_ui/handlers/keyboard_handler.js @@ -47,9 +47,18 @@ export function _createKeyboardHandler( keyboard, synthui, renderer) createChannel(); }); + // QoL: change keyboard channel to the changed one when user changed it: adjust selector here + synthui.synth.eventHandler.addEvent("programchange", "settings-keyboard-program-change", e => { + if(e.userCalled) + { + keyboardControls.channelSelector.value = e.channel; + } + }) + // dark mode toggle keyboardControls.modeSelector.onclick = () => { keyboard.toggleMode(); 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 index 84feed30..6c6e6d59 100644 --- a/src/website/js/settings_ui/saving/load_settings.js +++ b/src/website/js/settings_ui/saving/load_settings.js @@ -93,7 +93,7 @@ export async function _loadSettings() // interface if(savedSettings.interface.language !== this.locale.localeCode) { - this.locale.changeGlobalLocale(savedSettings.interface.language); + this.locale.changeGlobalLocale(savedSettings.interface.language, true); // using set timeout here fixes it for some reason setTimeout(() => { @@ -103,6 +103,7 @@ export async function _loadSettings() if(savedSettings.interface.mode === "light") { this._toggleDarkMode(); + this.htmlControls.interface.themeSelector.checked = false; } else { diff --git a/src/website/js/settings_ui/settings.js b/src/website/js/settings_ui/settings.js index 24f792b1..fefc9bf5 100644 --- a/src/website/js/settings_ui/settings.js +++ b/src/website/js/settings_ui/settings.js @@ -13,6 +13,7 @@ import { } from './handlers/midi_handler.js' import { _createKeyboardHandler } from './handlers/keyboard_handler.js' import { localeList } from '../../locale/locale_files/locale_list.js' +import { keybinds } from '../keybinds.js' const TRANSITION_TIME = 0.2; @@ -94,6 +95,7 @@ class SpessaSynthSettings this.mainDiv = document.createElement("div"); this.mainDiv.classList.add("settings_menu"); this.visible = false; + this.animationId = -1; settingsButton.onclick = () => this.setVisibility(!this.visible); settingsWrapper.appendChild(this.mainDiv); @@ -139,9 +141,15 @@ class SpessaSynthSettings // key bind is "R" document.addEventListener("keydown", e => { - if(e.key.toLowerCase() === "r") + switch (e.key.toLowerCase()) { - this.setVisibility(!this.visible); + case keybinds.settingsShow: + this.setVisibility(!this.visible); + break; + + // hide when synth controller shown + case keybinds.synthesizerUIShow: + this.setVisibility(false); } }) @@ -196,6 +204,10 @@ class SpessaSynthSettings */ setVisibility(visible) { + if(this.animationId) + { + clearTimeout(this.animationId); + } if(visible) { this.mainDiv.style.display = "block"; @@ -209,7 +221,7 @@ class SpessaSynthSettings { document.getElementsByClassName("top_part")[0].classList.remove("settings_shown"); this.mainDiv.classList.remove("settings_menu_show"); - setTimeout(() => { + this.animationId = setTimeout(() => { this.mainDiv.style.display = "none"; }, TRANSITION_TIME * 1000); } diff --git a/src/website/js/synthesizer_ui/methods/create_main_controller.js b/src/website/js/synthesizer_ui/methods/create_main_controller.js index 14ba5774..57e50734 100644 --- a/src/website/js/synthesizer_ui/methods/create_main_controller.js +++ b/src/website/js/synthesizer_ui/methods/create_main_controller.js @@ -106,29 +106,7 @@ export function createMainSynthController() showControllerButton.classList.add("synthui_button"); showControllerButton.onclick = () => { this.hideOnDocClick = false; - - this.isShown = !this.isShown; - if(this.isShown) - { - controlsWrapper.classList.add("controls_wrapper_show"); - controller.style.display = "block"; - document.getElementsByClassName("top_part")[0].classList.add("synthui_shown"); - this.showControllers(); - - setTimeout(() => { - controller.classList.add("synthui_controller_show"); - }, 10); - } - else - { - document.getElementsByClassName("top_part")[0].classList.remove("synthui_shown"); - this.hideControllers(); - controller.classList.remove("synthui_controller_show"); - setTimeout(() => { - controlsWrapper.classList.remove("controls_wrapper_show"); - controller.style.display = "none"; - }, 200); - } + this.toggleVisibility(); } // black midi mode toggle diff --git a/src/website/js/synthesizer_ui/synthetizer_ui.js b/src/website/js/synthesizer_ui/synthetizer_ui.js index 697618f6..841f282a 100644 --- a/src/website/js/synthesizer_ui/synthetizer_ui.js +++ b/src/website/js/synthesizer_ui/synthetizer_ui.js @@ -1,15 +1,12 @@ import { Synthetizer, - VOICE_CAP, } from '../../../spessasynth_lib/synthetizer/synthetizer.js' -import { getDrumsSvg, getNoteSvg } from '../icons.js' -import { Meter } from './methods/synthui_meter.js' -import { midiControllers } from '../../../spessasynth_lib/midi_parser/midi_message.js' import { hideControllers, showControllers } from './methods/hide_show_controllers.js' import { toggleDarkMode } from './methods/toggle_dark_mode.js' import { createChannelController, createChannelControllers } from './methods/create_channel_controller.js' import { createMainSynthController } from './methods/create_main_controller.js' import { setEventListeners } from './methods/set_event_listeners.js' +import { keybinds } from '../keybinds.js' export const LOCALE_PATH = "locale.synthesizerController."; @@ -34,8 +31,8 @@ class SynthetizerUI wrapper.appendChild(this.uiDiv); this.uiDiv.style.visibility = "visible"; this.isShown = false; + this.animationId = -1; this.locale = localeManager; - this.hideOnDocClick = true; } @@ -55,28 +52,23 @@ class SynthetizerUI document.addEventListener("keydown", e => { switch (e.key.toLowerCase()) { - case "s": + case keybinds.synthesizerUIShow: e.preventDefault(); - const controller = this.uiDiv.getElementsByClassName("synthui_controller")[0]; - controller.classList.toggle("synthui_controller_show"); - controller.getElementsByClassName("controls_wrapper")[0].classList.toggle("controls_wrapper_show"); - this.isShown = !this.isShown; - if(this.isShown) - { - this.showControllers(); - } - else - { - this.hideControllers() - } + this.toggleVisibility(); break; - case "b": + // + case keybinds.settingsShow: + this.isShown = true; + this.toggleVisibility(); + break; + + case keybinds.blackMidiMode: e.preventDefault(); this.synth.highPerformanceMode = !this.synth.highPerformanceMode; break; - case "backspace": + case keybinds.midiPanic: e.preventDefault(); this.synth.stopAll(true); break; @@ -108,6 +100,38 @@ class SynthetizerUI }) } + toggleVisibility() + { + if(this.animationId !== -1) + { + clearTimeout(this.animationId); + } + const controller = document.getElementsByClassName("synthui_controller")[0]; + const controlsWrapper = controller.getElementsByClassName("controls_wrapper")[0]; + this.isShown = !this.isShown; + if(this.isShown) + { + controlsWrapper.classList.add("controls_wrapper_show"); + controller.style.display = "block"; + document.getElementsByClassName("top_part")[0].classList.add("synthui_shown"); + this.showControllers(); + + setTimeout(() => { + controller.classList.add("synthui_controller_show"); + }, 10); + } + else + { + document.getElementsByClassName("top_part")[0].classList.remove("synthui_shown"); + this.hideControllers(); + controller.classList.remove("synthui_controller_show"); + this.animationId = setTimeout(() => { + controlsWrapper.classList.remove("controls_wrapper_show"); + controller.style.display = "none"; + }, 200); + } + } + updateVoicesAmount() { this.voiceMeter.update(this.synth.voicesAmount); diff --git a/src/website/locale/locale_manager.js b/src/website/locale/locale_manager.js index 6d8f3a4f..5720d4b7 100644 --- a/src/website/locale/locale_manager.js +++ b/src/website/locale/locale_manager.js @@ -78,6 +78,7 @@ export class LocaleManager } if(property.object[property.propertyName] !== textValue) { + console.log("edited. Expected", textValue, "got", property.object[property.propertyName]); property.isEdited = true; } } @@ -171,8 +172,9 @@ export class LocaleManager /** * Changes the global locale and all bound text * @param newLocale {LocaleList} + * @param force {boolean} if the locale should be applied even to changed values */ - changeGlobalLocale(newLocale) + changeGlobalLocale(newLocale, force = false) { /** * @type {CompleteLocaleTypedef} @@ -185,10 +187,13 @@ export class LocaleManager } this.localeCode = newLocale; SpessaSynthInfo("Changing locale to", newLocaleObject.localeName) - // check if the property has been changed to something else. If so, don't change it back. - this._boundObjectProperties.forEach(property => { - this._validatePropertyIntegrity(property); - }) + if(!force) + { + // check if the property has been changed to something else. If so, don't change it back. + this._boundObjectProperties.forEach(property => { + this._validatePropertyIntegrity(property); + }); + } this.locale = newLocaleObject; // apply the new locale to bound elements this._boundObjectProperties.forEach(property => { diff --git a/src/website/main.js b/src/website/main.js index 68ecf00e..780abea9 100644 --- a/src/website/main.js +++ b/src/website/main.js @@ -11,8 +11,6 @@ import { showNotification } from './js/notification.js' * main.js * purpose: main script for the local edition, loads the soundfont and passes it to the manager.js, reloads soundfonts when needed and saves the settings */ - -const TITLE = "SpessaSynth: SoundFont2 Javascript Synthesizer"; const SAMPLE_RATE = 44100; /** @@ -208,10 +206,9 @@ async function replaceFont(fontName) if(!window.manager) { // prepare the manager window.manager = new Manager(audioContextMain, soundFontParser); - const t = titleMessage.innerText; + window.TITLE = window.manager.localeManager.getLocaleString("locale.titleMessage"); titleMessage.innerText = "Initializing..."; await manager.ready; - titleMessage.innerText = t; } else { @@ -227,7 +224,6 @@ async function replaceFont(fontName) } } synthReady = true; - titleMessage.innerText = TITLE; } if(window.loadedSoundfonts.find(sf => sf.name === fontName)) @@ -262,10 +258,9 @@ document.body.onclick = async () => if(window.soundFontParser) { // prepare midi interface window.manager = new Manager(audioContextMain, soundFontParser); - const t = titleMessage.innerText; + window.TITLE = window.manager.localeManager.getLocaleString("locale.titleMessage") titleMessage.innerText = "Initializing..." await manager.ready; - titleMessage.innerText = t; synthReady = true; } } @@ -310,7 +305,7 @@ fetch("soundfonts").then(async r => { if(window.manager.seq) { - titleMessage.innerText = window.manager.seq.midiData.midiName || TITLE; + titleMessage.innerText = window.manager.seq.midiData.midiName || window.TITLE; } } diff --git a/src/website/manager.js b/src/website/manager.js index eeed3919..0434c99e 100644 --- a/src/website/manager.js +++ b/src/website/manager.js @@ -1,4 +1,4 @@ -import { MidiKeyboard } from './js/midi_keyboard.js' +import { MidiKeyboard } from './js/midi_keyboard/midi_keyboard.js' import { Synthetizer } from '../spessasynth_lib/synthetizer/synthetizer.js' import { Renderer } from './js/renderer/renderer.js' import { MIDI } from '../spessasynth_lib/midi_parser/midi_loader.js' @@ -15,6 +15,7 @@ import { LocaleManager } from './locale/locale_manager.js' import { isMobile } from './js/utils/is_mobile.js' import { SpessaSynthInfo } from '../spessasynth_lib/utils/loggin.js' import { showNotification } from './js/notification.js' +import { keybinds } from './js/keybinds.js' const RENDER_AUDIO_TIME_INTERVAL = 500; @@ -249,7 +250,7 @@ document.body.classList.add("load"); // add keypresses canvas.addEventListener("keypress", e => { switch (e.key.toLowerCase()) { - case "c": + case keybinds.cinematicMode: if(this.seq) { this.seq.pause(); @@ -268,7 +269,7 @@ document.body.classList.add("load"); document.body.requestFullscreen().then(); break; - case "v": + case keybinds.videoMode: if(this.seq) { this.seq.pause(); diff --git a/src/website/server/serve.js b/src/website/server/serve.js index 973c51e8..5d1a405e 100644 --- a/src/website/server/serve.js +++ b/src/website/server/serve.js @@ -1,5 +1,6 @@ import fs from 'fs'; import path from 'path' +import { configPath, soundfontsPath } from './server.js' /** * @param res {ServerResponse} * @param path {string} @@ -34,6 +35,47 @@ export async function serveSfont(path, res) }) } +/** + * @param res {ServerResponse} + */ +export function serveSfontList(res) +{ + const fileNames = fs.readdirSync(soundfontsPath).filter(fName => { + return fName.slice(-3).toLowerCase() === 'sf2' || fName.slice(-3).toLowerCase() === 'sf3'; + }); + + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + if (config['lastUsedSf2']) + { + if (fileNames.includes(config['lastUsedSf2'])) + { + fileNames.splice(fileNames.indexOf(config['lastUsedSf2']), 1); + fileNames.unshift(config['lastUsedSf2']); + } + } + else + { + config['lastUsedSf2'] = fileNames[0]; + } + + const files = fileNames.map(file => { + return { name: file }; + }); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(files)); +} + +/** + * @param res {ServerResponse} + */ +export function serveSettings(res) +{ + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(config.settings || {})); +} + /** * @param res {ServerResponse} * @param filePath {string} diff --git a/src/website/server/server.js b/src/website/server/server.js index acf072e8..cd2a6bc3 100644 --- a/src/website/server/server.js +++ b/src/website/server/server.js @@ -2,108 +2,93 @@ import http from 'http'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; -import { serveStaticFile } from './serve.js' +import { serveSettings, serveSfontList, serveStaticFile } from './serve.js' import { openURL } from './open.js' -const PORT = 8181; +let PORT = 8181; const HOST = '0.0.0.0'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.join(path.dirname(__filename), "../../"); -const configPath = path.join(__dirname, '/website/server/config.json'); -const soundfontsPath = path.join(__dirname, '../soundfonts'); +export const configPath = path.join(__dirname, '/website/server/config.json'); +export const soundfontsPath = path.join(__dirname, '../soundfonts'); fs.writeFile(configPath, '{}', { flag: 'wx' }, () => {}); const server = http.createServer((req, res) => { - // main page - if (req.method === 'GET' && req.url === '/') + switch(req.url.split('?')[0]) { - serveStaticFile(res, path.join(__dirname, 'website/local_edition_index.html'), 'text/html'); - } - // soundfont list - else if (req.method === 'GET' && req.url.startsWith('/soundfonts')) - { - const fileNames = fs.readdirSync(soundfontsPath).filter(fName => { - return fName.slice(-3).toLowerCase() === 'sf2' || fName.slice(-3).toLowerCase() === 'sf3'; - }); - - const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); - if (config['lastUsedSf2']) - { - if (fileNames.includes(config['lastUsedSf2'])) - { - fileNames.splice(fileNames.indexOf(config['lastUsedSf2']), 1); - fileNames.unshift(config['lastUsedSf2']); - } - } - else - { - config['lastUsedSf2'] = fileNames[0]; - } + case "/": + serveStaticFile(res, path.join(__dirname, 'website/local_edition_index.html'), 'text/html'); + break; - const files = fileNames.map(file => { - return { name: file }; - }); + case "/soundfonts": + serveSfontList(res); + break; - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(files)); - } - // set last used soundfont - else if (req.method === 'GET' && req.url.startsWith('/setlastsf2')) - { - const urlParams = new URL(req.url, `http://${req.headers.host}`).searchParams; - const sfname = urlParams.get('sfname'); + case "/setlastsf2": + const urlParams = new URL(req.url, `http://${req.headers.host}`).searchParams; + const sfname = urlParams.get('sfname'); - const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); - config['lastUsedSf2'] = sfname; - - fs.writeFile(configPath, JSON.stringify(config), { flag: 'w' }, () => {}); - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('Soundfont updated'); - } - // save settings - else if (req.method === 'POST' && req.url === '/savesettings') - { - let body = ''; - req.on('data', chunk => { - body += chunk.toString(); - }); - req.on('end', () => { - const settings = JSON.parse(body); const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); - config.settings = settings; + config['lastUsedSf2'] = sfname; fs.writeFile(configPath, JSON.stringify(config), { flag: 'w' }, () => {}); res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('Settings saved'); - }); - } - // get settings - else if (req.method === 'GET' && req.url === '/getsettings') - { - const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(config.settings || {})); - } - // serve a static file - else if (req.method === 'GET') - { - serveStaticFile(res, path.join(__dirname, req.url)); - } - // no idea - else - { - res.writeHead(404, { 'Content-Type': 'text/plain' }); - res.end('404 Not Found'); - } -}); + res.end('Soundfont updated'); + break; + + case "/savesettings": + let body = ''; + req.on('data', chunk => { + body += chunk.toString(); + }); + req.on('end', () => { + const settings = JSON.parse(body); + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + config.settings = settings; -server.listen(PORT, HOST, () => { - let url = `http://localhost:${PORT}`; + fs.writeFile(configPath, JSON.stringify(config), { flag: 'w' }, () => {}); + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Settings saved'); + }); + break; - console.log(`Running on ${url}. A browser window should open.`); - openURL(url); + case "/getsettings": + serveSettings(res); + break; + + default: + serveStaticFile(res, path.join(__dirname, req.url)); + } }); + +// look for a port that isn't occupied +let served = false; +function tryServer() +{ + server.listen(PORT, HOST, () => { + if(served) + { + return; + } + served = true; + let url = `http://localhost:${PORT}`; + console.log(`Running on ${url}. A browser window should open.`); + openURL(url); + }).on('error', e => { + if(e.code === 'EADDRINUSE') + { + console.log(`Port ${PORT} seems to be occupied, trying again...`) + PORT++; + tryServer(); + } + else + { + throw e; + } + }); +} +tryServer();