diff --git a/index.html b/index.html index cc42a433..681f4135 100644 --- a/index.html +++ b/index.html @@ -189,7 +189,7 @@

SpessaSynth: Online Demo

} else { - title = midiFiles[i].name; + title = midiFiles[i].name.replace(".mid", ""); } titles.push(title); } diff --git a/src/spessasynth_lib/synthetizer/buffer_voice/voice.js b/src/spessasynth_lib/synthetizer/buffer_voice/voice.js index c7afdb48..2dc62ada 100644 --- a/src/spessasynth_lib/synthetizer/buffer_voice/voice.js +++ b/src/spessasynth_lib/synthetizer/buffer_voice/voice.js @@ -1,5 +1,9 @@ +/** + * @typedef {{}} + */ + import {Preset} from "../../soundfont/chunk/presets.js"; -import { SynthesisModel } from './synthesis_model.js' +import { SynthesisModel } from './synthesis_model.js'; export class Voice { @@ -64,6 +68,11 @@ export class Voice }); } + /** + * @type {[]} + */ + static cachedVoices = []; + /** * @param debug {boolean} diff --git a/src/spessasynth_lib/synthetizer/synthetizer.js b/src/spessasynth_lib/synthetizer/synthetizer.js index 636bbd37..4ad466e7 100644 --- a/src/spessasynth_lib/synthetizer/synthetizer.js +++ b/src/spessasynth_lib/synthetizer/synthetizer.js @@ -4,9 +4,10 @@ import {ShiftableByteArray} from "../utils/shiftable_array.js"; import { arrayToHexString, consoleColors } from '../utils/other.js'; import { midiControllers } from '../midi_parser/midi_message.js' import { WorkletChannel } from './worklet_channel/worklet_channel.js' +import { EventHandler } from '../utils/event_handler.js' // i mean come on -const VOICES_CAP = 2137; +const VOICES_CAP = 800; export const DEFAULT_GAIN = 0.5; export const DEFAULT_PERCUSSION = 9; @@ -23,6 +24,8 @@ export class Synthetizer { this.soundFont = soundFont; this.context = targetNode.context; + this.eventHandler = new EventHandler(); + this.volumeController = new GainNode(targetNode.context, { gain: DEFAULT_GAIN }); @@ -92,9 +95,13 @@ export class Synthetizer { let chan = this.midiChannels[channel]; chan.playNote(midiNote, velocity, enableDebugging); - if(this.onNoteOn.length) { - this.callEvent(this.onNoteOn, [midiNote, channel, velocity, chan.channelVolume, chan.channelExpression]); - } + this.eventHandler.callEvent("noteon", { + midiNote: midiNote, + channel: channel, + velocity: velocity, + channelVolume: chan.channelVolume, + channelExpression: chan.channelExpression, + }); } /* @@ -116,9 +123,10 @@ export class Synthetizer { console.warn(`Received a noteOn for note`, midiNote, "Ignoring."); return; } - if(this.onNoteOff) { - this.callEvent(this.onNoteOff, [midiNote, channel]); - } + this.eventHandler.callEvent("noteoff", { + midiNote: midiNote, + channel: channel + }); if(this.highPerformanceMode) { this.midiChannels[channel].stopNote(midiNote, true); @@ -127,34 +135,6 @@ export class Synthetizer { this.midiChannels[channel].stopNote(midiNote); } - /** - * @param event {function[]} - * @param args {number[]} - */ - callEvent(event, args) - { - event.forEach(f => f(...args)) - } - - /** - * Plays when the midi note goes on - * @type {function[]} - * @param midiNote {number} 0-127 - * @param channel {number} 0-15 - * @param velocity {number} 0-127 - * @param volume {number} 0-1 - * @param expression {number} 0-1 - */ - onNoteOn = []; - - /** - * Plays when the midi note goes off - * @type {function[]} - * @param midiNote {number} 0-127 - * @param channel {number} 0-15 - */ - onNoteOff = []; - /** * Stops all notes * @param force {boolean} if we should instantly kill the note, defaults to false @@ -162,12 +142,12 @@ export class Synthetizer { stopAll(force=false) { console.log("%cStop all received!", consoleColors.info); for (let channel of this.midiChannels) { - if(this.onNoteOff) + for(const note of channel.notes) { - for(const note of channel.notes) - { - this.callEvent(this.onNoteOff, [note, channel.channelNumber - 1]); - } + this.eventHandler.callEvent("noteoff", { + midiNote: note, + channel: channel.channelNumber - 1 + }); } channel.stopAll(force); } @@ -216,8 +196,16 @@ export class Synthetizer { if(this.midiChannels[channel].bank === 127) { this.midiChannels[channel].percussionChannel = true; + this.eventHandler.callEvent("drumchange",{ + channel: channel, + isDrumChannel: true + }); } this.midiChannels[channel].bank = controllerValue; + this.eventHandler.callEvent("drumchange",{ + channel: channel, + isDrumChannel: false + }); } break; @@ -226,10 +214,11 @@ export class Synthetizer { this.midiChannels[channel].controllerChange(controllerNumber, controllerValue); break; } - if(this.onControllerChange) - { - this.callEvent(this.onControllerChange, [channel, controllerNumber, controllerValue]); - } + this.eventHandler.callEvent("controllerchange", { + channel: channel, + controllerNumber: controllerNumber, + controllerValue: controllerValue + }); } /** @@ -242,33 +231,37 @@ export class Synthetizer { { // reset ch.resetControllers(); - ch.percussionChannel = false; ch.bank = 0; - ch.setPreset(this.defaultPreset); + if(ch.channelNumber - 1 === DEFAULT_PERCUSSION) { + ch.setPreset(this.percussionPreset); + ch.percussionChannel = true; + this.eventHandler.callEvent("drumchange",{ + channel: ch.channelNumber - 1, + isDrumChannel: true + }); + } + else + { + ch.percussionChannel = false; + ch.setPreset(this.defaultPreset); + this.eventHandler.callEvent("drumchange",{ + channel: ch.channelNumber - 1, + isDrumChannel: false + }); + } // call all the event listeners const chNr = ch.channelNumber - 1; - if(this.onProgramChange.length) - { - this.callEvent(this.onProgramChange, [chNr, chNr === DEFAULT_PERCUSSION ? this.percussionPreset : this.defaultPreset]); - } - if(this.onControllerChange.length) - { - this.callEvent(this.onControllerChange, [chNr, midiControllers.mainVolume, 100]); - this.callEvent(this.onControllerChange, [chNr, midiControllers.pan, 64]); - this.callEvent(this.onControllerChange, [chNr, midiControllers.expressionController, 127]); - this.callEvent(this.onControllerChange, [chNr, midiControllers.modulationWheel, 0]); - this.callEvent(this.onControllerChange, [chNr, midiControllers.effects3Depth, 0]); + this.eventHandler.callEvent("programchange", {channel: chNr, preset: ch.preset}) - } - if(this.onPitchWheel.length) - { - this.callEvent(this.onPitchWheel, [chNr, 64, 0]); - } - } + this.eventHandler.callEvent("controllerchange", {channel: chNr, controllerNumber: midiControllers.mainVolume, controllerValue: 100}); + this.eventHandler.callEvent("controllerchange", {channel: chNr, controllerNumber: midiControllers.pan, controllerValue: 64}); + this.eventHandler.callEvent("controllerchange", {channel: chNr, controllerNumber: midiControllers.expressionController, controllerValue: 127}); + this.eventHandler.callEvent("controllerchange", {channel: chNr, controllerNumber: midiControllers.modulationWheel, controllerValue: 0}); + this.eventHandler.callEvent("controllerchange", {channel: chNr, controllerNumber: midiControllers.effects3Depth, controllerValue: 0}); - this.midiChannels[DEFAULT_PERCUSSION].percussionChannel = true; - this.midiChannels[DEFAULT_PERCUSSION].setPreset(this.percussionPreset); + this.eventHandler.callEvent("pitchwheel", {channel: chNr, MSB: 64, LSB: 0}) + } this.system = "gm2"; this.volumeController.gain.value = DEFAULT_GAIN; this.panController.pan.value = 0; @@ -283,10 +276,11 @@ export class Synthetizer { pitchWheel(channel, MSB, LSB) { this.midiChannels[channel].setPitchBend(MSB, LSB); - if(this.onPitchWheel.length) - { - this.callEvent(this.onPitchWheel, [channel, MSB, LSB]); - } + this.eventHandler.callEvent("pitchwheel", { + channel: channel, + MSB: MSB, + LSB: LSB + }); } /** @@ -307,27 +301,6 @@ export class Synthetizer { this.volumeController.gain.value = volume * DEFAULT_GAIN; } - /** - * Calls on program change(channel number, preset) - * @type {function(number, Preset)[]} - */ - onProgramChange = []; - - /** - * Calls on controller change(channel number, cc, controller value) - * @param channel {number} 0-16 - * @param controllerNumber {number} 0-127 - * @param controllerValue {number} 0-127 - * @type {function[]} - */ - onControllerChange = []; - - /** - * Calls on pitch wheel change (channel, msb, lsb) - * @type {function(number, number, number)[]} - */ - onPitchWheel = []; - /** * Changes the patch for a given channel * @param channel {number} 0-15 the channel to change @@ -342,11 +315,10 @@ export class Synthetizer { // find the preset let preset = this.soundFont.getPreset(bank, programNumber); channelObj.setPreset(preset); - // console.log("changing channel", channel, "to bank:", channelObj.bank, - // "preset:", programNumber, preset.presetName); - if(this.onProgramChange) { - this.callEvent(this.onProgramChange, [channel, preset]); - } + this.eventHandler.callEvent("programchange", { + channel: channel, + preset: preset + }); } /** @@ -452,6 +424,11 @@ export class Synthetizer { consoleColors.recognized, consoleColors.info, consoleColors.value); + + this.eventHandler.callEvent("drumchange",{ + channel: channel, + isDrumChannel: this.midiChannels[channel].percussionChannel + }); } else if(messageData[4] === 0x40 && messageData[6] === 0x06 && messageData[5] === 0x00) diff --git a/src/spessasynth_lib/utils/event_handler.js b/src/spessasynth_lib/utils/event_handler.js new file mode 100644 index 00000000..451d24c2 --- /dev/null +++ b/src/spessasynth_lib/utils/event_handler.js @@ -0,0 +1,62 @@ +/** + * @typedef { + * "noteon"| + * "noteoff"| + * "pitchwheel"| + * "controllerchange"| + * "programchange"| + * "drumchange"} EventTypes + */ +export class EventHandler +{ + /** + * A new synthesizer event handler + */ + constructor() { + /** + * The main list of events + * @type {Object} + */ + this.events = {}; + } + + /** + * Adds a new event listener + * @param name {EventTypes} + * @param callback {function(Object)} + */ + addEvent(name, callback) + { + if(this.events[name]) + { + this.events[name].push(callback); + } + else + { + this.events[name] = [callback]; + } + } + + /** + * Removes an event listener + * @param name {EventTypes} + * @param callback {function(Object)} + */ + removeEvent(name, callback) + { + this.events[name].splice(this.events[name].findIndex(c => c === callback), 1); + } + + /** + * Calls the given event + * @param name {EventTypes} + * @param eventData {Object} + */ + callEvent(name, eventData) + { + if(this.events[name]) + { + this.events[name].forEach(ev => ev(eventData)); + } + } +} \ No newline at end of file diff --git a/src/website/css/keyboard.css b/src/website/css/keyboard.css index f4eb304b..bc3e876c 100644 --- a/src/website/css/keyboard.css +++ b/src/website/css/keyboard.css @@ -23,6 +23,10 @@ --flat-half-translate: 18%; } +#keyboard .flat_dark_key{ + background: linear-gradient(262deg, #111, #000); +} + #keyboard .sharp_key{ --sharp-transform: scale(1, 0.7); transform: var(--sharp-transform); diff --git a/src/website/css/keyboard_ui.css b/src/website/css/keyboard_ui.css index aa2267cb..f99e1506 100644 --- a/src/website/css/keyboard_ui.css +++ b/src/website/css/keyboard_ui.css @@ -17,4 +17,25 @@ background: var(--top-color); text-align: center; margin: auto; -} \ No newline at end of file +} + +.kebui_button +{ + background: #000; + border: 1px #333 solid; + min-height: 2em; + border-radius: 5px; + font-size: 16px; + display: block; + white-space: nowrap; + color: var(--font-color); + margin: auto; +} + +.kebui_button:hover +{ + cursor: pointer; + background: #111; + color: #fff; + text-shadow: 0 0 5px white; +} diff --git a/src/website/css/sequencer_ui.css b/src/website/css/sequencer_ui.css index 5c7880d7..f0570c1f 100644 --- a/src/website/css/sequencer_ui.css +++ b/src/website/css/sequencer_ui.css @@ -23,7 +23,7 @@ z-index: 5; font-size: var(--progress-bar-height); text-align: center; - color: #ccc; + color: var(--font-color); margin: auto; width: 100%; } diff --git a/src/website/css/style.css b/src/website/css/style.css index 5cfd2be7..8dd2629b 100644 --- a/src/website/css/style.css +++ b/src/website/css/style.css @@ -4,8 +4,9 @@ } * { + --font-color: #ccc; font-family: "Sans", serif; - color: #ccc; + color: var(--font-color); text-align: center; margin: 0; } @@ -66,6 +67,7 @@ input[type="file"] { line-height: 100%; font-weight: lighter; font-size: 2.1em; + text-shadow: 0 0 5px var(--font-color); } #progress_bar diff --git a/src/website/css/synthesizer_ui.css b/src/website/css/synthesizer_ui.css index 6568f4fa..b1d6a0eb 100644 --- a/src/website/css/synthesizer_ui.css +++ b/src/website/css/synthesizer_ui.css @@ -127,6 +127,7 @@ .mute_button:hover{ cursor: pointer; + } .voice_reset{ @@ -165,12 +166,12 @@ display: block; min-height: var(--voice-meter-height); white-space: nowrap; - color: #ccc; - padding: 9px; + color: var(--font-color); } .synthui_button:hover { cursor: pointer; background: #111; color: #fff; + text-shadow: 0 0 5px white; } diff --git a/src/website/main.js b/src/website/main.js index 4939369f..0b289306 100644 --- a/src/website/main.js +++ b/src/website/main.js @@ -110,7 +110,7 @@ async function startMidi(midiFiles) } else { - title = midiFiles[i].name; + title = midiFiles[i].name.replace(".mid", ""); } titles.push(title); } @@ -192,7 +192,9 @@ document.body.onclick = () => { // user has clicked, we can create the ui if(!window.audioContextMain) { - window.audioContextMain = new AudioContext({sampleRate: 44100}); + navigator.mediaSession.playbackState = "playing"; + window.audioContextMain = new AudioContext({sampleRate: 44100, + latencyHint: "playback"}); if(window.soundFontParser) { titleMessage.innerText = TITLE; // prepare midi interface diff --git a/src/website/manager.js b/src/website/manager.js index d043703c..20bf7239 100644 --- a/src/website/manager.js +++ b/src/website/manager.js @@ -38,15 +38,15 @@ export class Manager { constructor(context, soundFont) { this.context = context; this.initializeContext(context, soundFont).then(); - } async initializeContext(context, soundFont) { - try { - await context.audioWorklet.addModule("/spessasynth_lib/synthetizer/worklet_channel/channel_processor.js"); - } - catch (e) { - await context.audioWorklet.addModule("src/spessasynth_lib/synthetizer/worklet_channel/channel_processor.js"); + if(context.audioWorklet) { + try { + await context.audioWorklet.addModule("/spessasynth_lib/synthetizer/worklet_channel/channel_processor.js"); + } catch (e) { + await context.audioWorklet.addModule("src/spessasynth_lib/synthetizer/worklet_channel/channel_processor.js"); + } } // set up soundfont this.soundFont = soundFont; diff --git a/src/website/sequence_recorder/sequence_recorder.js b/src/website/sequence_recorder/sequence_recorder.js index 0da3d71c..8868969c 100644 --- a/src/website/sequence_recorder/sequence_recorder.js +++ b/src/website/sequence_recorder/sequence_recorder.js @@ -25,30 +25,49 @@ export class SequenceRecorder { this.targetChannel = desiredChannel; // connect to synth - this.synth.onNoteOn.push(this.noteOn.bind(this)); - this.nOnI = this.synth.onNoteOn.length - 1; // note on index - this.synth.onNoteOff.push(this.noteOff.bind(this)); - this.nOffI = this.synth.onNoteOn.length - 1; // note off index - this.synth.onControllerChange.push(this.controllerChange.bind(this)); - this.cCI = this.synth.onNoteOn.length - 1; // controller change index - this.synth.onProgramChange.push(this.programChange.bind(this)); - this.pCI = this.synth.onNoteOn.length - 1; // program change index - this.synth.onPitchWheel.push(this.pitchWheel.bind(this)); - this.pWI = this.synth.onNoteOn.length - 1; // pitch wheel index + this.synth.eventHandler.addEvent("noteon", e => { + this.noteOn(e.midiNote, e.channel, e.velocity); + }); + this.synth.eventHandler.addEvent("noteoff", e => { + this.noteOff(e.midiNote, e.channel); + }); + this.synth.eventHandler.addEvent("controllerchange", e => { + this.controllerChange(e.channel, e.controllerNumber, e.controllerValue); + }); + this.synth.eventHandler.addEvent("programchange", e => { + this.programChange(e.channel, e.preset); + }); + this.synth.eventHandler.addEvent("controllerchange", e => { + this.controllerChange(e.channel, e.controllerNumber, e.controllerValue); + }); + //this.synth.onNoteOn.push(this.noteOn.bind(this)); + //this.nOnI = this.synth.onNoteOn.length - 1; // note on index + //this.synth.onNoteOff.push(this.noteOff.bind(this)); + // this.nOffI = this.synth.onNoteOn.length - 1; // note off index + // this.synth.onControllerChange.push(this.controllerChange.bind(this)); + // this.cCI = this.synth.onNoteOn.length - 1; // controller change index + // this.synth.onProgramChange.push(this.programChange.bind(this)); + // this.pCI = this.synth.onNoteOn.length - 1; // program change index + // this.synth.onPitchWheel.push(this.pitchWheel.bind(this)); + // this.pWI = this.synth.onNoteOn.length - 1; // pitch wheel index } stopRecording() { - this.synth.onNoteOff.splice(this.nOffI, 1); - this.synth.onNoteOn.splice(this.nOnI, 1); - this.synth.onControllerChange.splice(this.cCI, 1); - this.synth.onProgramChange.splice(this.pCI, 1); - this.synth.onPitchWheel.splice(this.pWI, 1); - // this.synth.onNoteOff = this.synth.onNoteOff.filter(e => e !== this.noteOff.bind(this)); - // this.synth.onNoteOn = this.synth.onNoteOn.filter(e => e !== this.noteOn.bind(this)); - // this.synth.onControllerChange = this.synth.onControllerChange.filter(e => e !== this.controllerChange.bind(this)); - // this.synth.onProgramChange = this.synth.onProgramChange.filter(e => e !== this.programChange.bind(this)); - // this.synth.onPitchWheel = this.synth.onPitchWheel.filter(e => e !== this.pitchWheel.bind(this)); + this.synth.eventHandler.removeEvent("noteon", e => { + this.noteOn(e.midiNote, e.channel, e.velocity); + }); + this.synth.eventHandler.removeEvent("noteoff", e => { + this.noteOff(e.midiNote, e.channel); + }); + this.synth.eventHandler.removeEvent("controllerchange", e => { + this.controllerChange(e.channel, e.controllerNumber, e.controllerValue); + }); + // this.synth.onNoteOff.splice(this.nOffI, 1); + // this.synth.onNoteOn.splice(this.nOnI, 1); + // this.synth.onControllerChange.splice(this.cCI, 1); + // this.synth.onProgramChange.splice(this.pCI, 1); + // this.synth.onPitchWheel.splice(this.pWI, 1); } getTime() diff --git a/src/website/ui/midi_keyboard.js b/src/website/ui/midi_keyboard.js index a8373ccd..5d52de24 100644 --- a/src/website/ui/midi_keyboard.js +++ b/src/website/ui/midi_keyboard.js @@ -3,6 +3,7 @@ import { MIDIDeviceHandler } from '../../spessasynth_lib/midi_handler/midi_handl import { midiControllers } from '../../spessasynth_lib/midi_parser/midi_message.js' const KEYBOARD_VELOCITY = 126; +const GLOW_PX = 50; export class MidiKeyboard { @@ -15,6 +16,10 @@ export class MidiKeyboard constructor(channelColors, synth, handler) { this.mouseHeld = false; this.heldKeys = []; + /** + * @type {"light"|"dark"} + */ + this.mode = "light"; document.onmousedown = () => this.mouseHeld = true; document.onmouseup = () => { @@ -22,7 +27,7 @@ export class MidiKeyboard for(let key of this.heldKeys) { // user note off - this.releaseNote(key); + this.releaseNote(key, this.channel); this.synth.noteOff(this.channel, key); } } @@ -98,7 +103,7 @@ export class MidiKeyboard keyElement.onmouseout = () => { // user note off this.heldKeys.splice(this.heldKeys.indexOf(midiNote), 1); - this.releaseNote(midiNote); + this.releaseNote(midiNote, this.channel); this.synth.noteOff(this.channel, midiNote); }; keyElement.onmouseleave = keyElement.onmouseup; @@ -142,8 +147,16 @@ export class MidiKeyboard this.keys.push(keyElement); } - // channel selector this.selectorMenu = document.getElementById("keyboard_selector"); + // dark mode toggle + const modeToggler = document.createElement("button"); + modeToggler.innerText = "Toggle Dark Keyboard"; + modeToggler.classList.add("kebui_button"); + modeToggler.onclick = this.toggleMode.bind(this); + + this.selectorMenu.appendChild(modeToggler); + + // channel selector const channelSelector = document.createElement("select"); let channelNumber = 0; @@ -193,8 +206,32 @@ export class MidiKeyboard }); // connect the synth to keyboard - this.synth.onNoteOn.push((note, chan, vel, vol, exp) => this.pressNote(note, chan, vel, vol, exp)); - this.synth.onNoteOff.push(note => this.releaseNote(note)); + this.synth.eventHandler.addEvent("noteon", e => { + this.pressNote(e.midiNote, e.channel, e.velocity, e.channelVolume, e.channelExpression); + }); + this.synth.eventHandler.addEvent("noteoff", e => { + this.releaseNote(e.midiNote, e.channel); + }) + //this.synth.onNoteOn.push((note, chan, vel, vol, exp) => this.pressNote(note, chan, vel, vol, exp)); + //this.synth.onNoteOff.push((note, chan) => this.releaseNote(note, chan)); + } + + toggleMode() + { + if(this.mode === "light") + { + this.mode = "dark"; + } + else + { + this.mode = "light"; + } + this.keys.forEach(k => { + if(k.classList.contains("flat_key")) + { + k.classList.toggle("flat_dark_key"); + } + }) } createMIDIOutputSelector(seq) @@ -252,12 +289,13 @@ export class MidiKeyboard let rgbaValues = this.channelColors[channel].match(/\d+(\.\d+)?/g).map(parseFloat); // multiply the rgb values by brightness - if (!isSharp) { + let color; + if (!isSharp && this.mode === "light") { // multiply the rgb values let newRGBValues = rgbaValues.slice(0, 3).map(value => 255 - (255 - value) * brightness); // create the new color - key.style.background = `rgba(${newRGBValues.join(", ")}, ${rgbaValues[3]})`; + color = `rgba(${newRGBValues.join(", ")}, ${rgbaValues[3]})`; } else { @@ -265,7 +303,12 @@ export class MidiKeyboard let newRGBValues = rgbaValues.slice(0, 3).map(value => value * brightness); // create the new color - key.style.background = `rgba(${newRGBValues.join(", ")}, ${rgbaValues[3]})`; + color = `rgba(${newRGBValues.join(", ")}, ${rgbaValues[3]})`; + } + key.style.background = color; + if(this.mode === "dark") + { + key.style.boxShadow = `0px 0px ${GLOW_PX * brightness}px ${color}`; } /** * @type {string[]} @@ -273,7 +316,11 @@ export class MidiKeyboard this.keyColors[midiNote].push(this.channelColors[channel]); } - releaseNote(midiNote) + /** + * @param midiNote {number} 0-127 + * @param channel {number} 0-15 + */ + releaseNote(midiNote, channel) { if(midiNote > 127 || midiNote < 0) { @@ -290,13 +337,18 @@ export class MidiKeyboard return; } if(pressedColors.length > 1) { - pressedColors.pop(); + pressedColors.splice(pressedColors.findLastIndex(v => v === this.channelColors[channel]), 1); key.style.background = pressedColors[pressedColors.length - 1]; + if(this.mode === "dark") + { + key.style.boxShadow = `0px 0px ${GLOW_PX}px ${pressedColors[pressedColors.length - 1]}`; + } } if(pressedColors.length === 1) { key.classList.remove("pressed"); key.style.background = ""; + key.style.boxShadow = ""; } } } \ No newline at end of file diff --git a/src/website/ui/sequencer_ui/sequencer_ui.js b/src/website/ui/sequencer_ui/sequencer_ui.js index 9cadc741..38823b02 100644 --- a/src/website/ui/sequencer_ui/sequencer_ui.js +++ b/src/website/ui/sequencer_ui/sequencer_ui.js @@ -1,4 +1,4 @@ -import {Sequencer} from "../../../spessasynth_lib/sequencer/sequencer.js"; +import { Sequencer } from '../../../spessasynth_lib/sequencer/sequencer.js' import { formatTime, supportedEncodings } from '../../../spessasynth_lib/utils/other.js' import { getBackwardSvg, getForwardSvg, getLoopSvg, getPauseSvg, getPlaySvg, getTextSvg } from '../icons.js' import { messageTypes } from '../../../spessasynth_lib/midi_parser/midi_message.js' @@ -23,9 +23,74 @@ export class SequencerUI{ this.text = ""; this.rawText = []; this.titles = [""]; - this.titleIndex = 0; } + createNavigatorHandler() + { + + navigator.mediaSession.metadata = new MediaMetadata({ + title: this.titles[this.seq.songIndex], + artist: "SpessaSynth" + }); + + navigator.mediaSession.setActionHandler("play", () => { + this.seqPlay(); + }); + navigator.mediaSession.setActionHandler("pause", () => { + this.seqPause(); + }); + navigator.mediaSession.setActionHandler("stop", () => { + this.seq.currentTime = 0; + this.seqPause(); + }); + navigator.mediaSession.setActionHandler("seekbackward", e => { + this.seq.currentTime -= e.seekOffset || 10; + }); + navigator.mediaSession.setActionHandler("seekforward", e => { + this.seq.currentTime += e.seekOffset || 10; + }); + navigator.mediaSession.setActionHandler("seekto", e => { + this.seq.currentTime = e.seekTime + }); + navigator.mediaSession.setActionHandler("previoustrack", () => { + this.switchToPreviousSong(); + }); + navigator.mediaSession.setActionHandler("nexttrack", () => { + this.switchToNextSong(); + }); + + navigator.mediaSession.playbackState = "playing"; + } + + seqPlay() + { + this.seq.play(); + this.createNavigatorHandler(); + this.updateTitleAndMediaStatus(); + navigator.mediaSession.playbackState = "playing"; + } + + seqPause() + { + this.seq.pause(); + this.createNavigatorHandler(); + this.updateTitleAndMediaStatus(); + navigator.mediaSession.playbackState = "paused"; + } + + switchToNextSong() + { + this.seq.nextSong(); + this.createNavigatorHandler(); + this.updateTitleAndMediaStatus(); + } + + switchToPreviousSong() + { + this.seq.previousSong(); + this.createNavigatorHandler(); + this.updateTitleAndMediaStatus(); + } /** * @param songTitles {string[]} @@ -33,7 +98,8 @@ export class SequencerUI{ setSongTitles(songTitles) { this.titles = songTitles; - this.titleIndex = 0; + this.createNavigatorHandler(); + this.updateTitleAndMediaStatus(); } /** @@ -45,6 +111,8 @@ export class SequencerUI{ this.seq = sequencer; this.createControls(); this.setSliderInterval(); + this.createNavigatorHandler(); + this.updateTitleAndMediaStatus(); this.seq.onTextEvent = (data, type) => { let end = ""; @@ -113,20 +181,15 @@ export class SequencerUI{ * }} */ this.lyricsElement = {}; - // main div const mainLyricsDiv = document.createElement("div"); mainLyricsDiv.classList.add("lyrics"); - - // title const lyricsTitle = document.createElement("h2"); lyricsTitle.innerText = "Decoded Text"; lyricsTitle.classList.add("lyrics_title"); mainLyricsDiv.appendChild(lyricsTitle); this.lyricsElement.title = lyricsTitle; - - // encoding selector const encodingSelector = document.createElement("select"); supportedEncodings.forEach(encoding => { @@ -139,8 +202,6 @@ export class SequencerUI{ encodingSelector.onchange = () => this.changeEncoding(encodingSelector.value); encodingSelector.classList.add("lyrics_selector"); mainLyricsDiv.appendChild(encodingSelector); - - // the actual text const text = document.createElement("p"); text.classList.add("lyrics_text"); @@ -175,12 +236,12 @@ export class SequencerUI{ const togglePlayback = () => { if(this.seq.paused) { - this.seq.play(); playPauseButton.innerHTML = getPauseSvg(ICON_SIZE); + this.seqPlay(); } else { - this.seq.pause(); + this.seqPause() playPauseButton.innerHTML = getPlaySvg(ICON_SIZE); } } @@ -190,12 +251,12 @@ export class SequencerUI{ // previous song button const previousSongButton = getSeqUIButton("Previous song", getBackwardSvg(ICON_SIZE)); - previousSongButton.onclick = () => this.seq.previousSong(); + previousSongButton.onclick = () => this.switchToPreviousSong(); // next song button const nextSongButton = getSeqUIButton("Next song", getForwardSvg(ICON_SIZE)); - nextSongButton.onclick = () => this.seq.nextSong(); + nextSongButton.onclick = () => this.switchToNextSong(); // loop button const loopButton = getSeqUIButton("Loop this", @@ -282,11 +343,11 @@ export class SequencerUI{ break; case "[": - this.seq.previousSong(); + this.switchToPreviousSong(); break; case "]": - this.seq.nextSong(); + this.switchToNextSong(); break; default: @@ -305,17 +366,24 @@ export class SequencerUI{ }) } + updateTitleAndMediaStatus() + { + document.getElementById("title").innerText = this.titles[this.seq.songIndex]; + + navigator.mediaSession.setPositionState({ + duration: this.seq.duration, + playbackRate: this.seq.playbackRate, + position: this.seq.currentTime + }); + } + setSliderInterval(){ setInterval(() => { + //this.updateTitleAndMediaStatus(); this.progressBar.style.width = `${(this.seq.currentTime / this.seq.duration) * 100}%`; const time = formatTime(this.seq.currentTime); const total = formatTime(this.seq.duration); this.progressTime.innerText = `${time.time} / ${total.time}`; - if(this.seq.songIndex !== this.titleIndex) - { - document.getElementById("title").innerText = this.titles[this.seq.songIndex]; - this.titleIndex = this.seq.songIndex; - } }, 100); } } \ 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 236a204b..e52c0bb1 100644 --- a/src/website/ui/synthesizer_ui/synthetizer_ui.js +++ b/src/website/ui/synthesizer_ui/synthetizer_ui.js @@ -8,7 +8,6 @@ import { midiControllers } from '../../../spessasynth_lib/midi_parser/midi_messa import { SequencePlayer } from '../../sequence_recorder/sequence_player.js' import { SequenceRecorder } from '../../sequence_recorder/sequence_recorder.js' -const MAX_VOICE_METER = 400; export class SynthetizerUI { /** @@ -35,7 +34,7 @@ export class SynthetizerUI * Voice meter * @type {Meter} */ - this.voiceMeter = new Meter("#206", "Voices: ", 0, MAX_VOICE_METER); + this.voiceMeter = new Meter("#206", "Voices: ", 0, this.synth.voiceCap); this.voiceMeter.bar.classList.add("voice_meter_bar_smooth"); /** @@ -185,15 +184,20 @@ export class SynthetizerUI num++; } - this.synth.onProgramChange.push((channel, p) => { - if(this.synth.midiChannels[channel].lockPreset) + // add event listeners + this.synth.eventHandler.addEvent("programchange", e => + { + if(this.synth.midiChannels[e.channel].lockPreset) { return; } - this.controllers[channel].preset.value = JSON.stringify([p.bank, p.program]); + this.controllers[e.channel].preset.value = JSON.stringify([e.preset.bank, e.preset.program]); }); - this.synth.onControllerChange.push((channel, controller, value) => { + this.synth.eventHandler.addEvent("controllerchange", e => { + const controller = e.controllerNumber; + const channel = e.channel; + const value = e.controllerValue; switch (controller) { default: @@ -226,10 +230,18 @@ export class SynthetizerUI } }); - this.synth.onPitchWheel.push((channel, MSB, LSB) => { - const val = (MSB << 7) | LSB; + this.synth.eventHandler.addEvent("pitchwheel", e => { + const val = (e.MSB << 7) | e.LSB; // pitch wheel - this.controllers[channel].pitchWheel.update(val - 8192); + this.controllers[e.channel].pitchWheel.update(val - 8192); + }); + + this.synth.eventHandler.addEvent("drumchange", e => { + if(this.synth.midiChannels[e.channel].lockPreset) + { + return; + } + this.reloadSelector(this.controllers[e.channel].preset, e.isDrumChannel ? this.percussionList : this.instrumentList); }); setInterval(this.updateVoicesAmount.bind(this), 100);