diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index d2d612d4..99d3d405 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -36,21 +36,23 @@ + diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index e99d5fc3..774b3a47 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,6 +1,8 @@ \ No newline at end of file diff --git a/src/spessasynth_lib/README.md b/src/spessasynth_lib/README.md index 2b482ba5..cfed7f21 100644 --- a/src/spessasynth_lib/README.md +++ b/src/spessasynth_lib/README.md @@ -1,4 +1,5 @@ # spessasynth_lib + **A powerful soundfont/MIDI JavaScript library for the browsers.** ```shell @@ -6,19 +7,23 @@ npm install --save spessasynth_lib ``` ### [Project site (consider giving it a star!)](https://github.com/spessasus/SpessaSynth) + ### [Demo](https://spessasus.github.io/SpessaSynth) ### [Complete documentation](https://github.com/spessasus/SpessaSynth/wiki/Usage-As-Library) #### Basic example: play a single note + ```js -import {Synthetizer} from "spessasynth_lib" +import { Synthetizer } from "spessasynth_lib" + const sfont = await (await fetch("soundfont.sf3")).arrayBuffer(); const ctx = new AudioContext(); // make sure you copied the worklet processor! await ctx.audioWorklet.addModule("./worklet_processor.min.js"); const synth = new Synthetizer(ctx.destination, sfont); -document.getElementById("button").onclick = async () => { +document.getElementById("button").onclick = async () => +{ await ctx.resume(); synth.programChange(0, 48); // strings ensemble synth.noteOn(0, 52, 127); @@ -28,37 +33,55 @@ document.getElementById("button").onclick = async () => { ## Current Features ### Easy Integration + - **Modular design:** Easy integration into other projects (load what you need) -- **[Detailed documentation:](https://github.com/spessasus/SpessaSynth/wiki/Home)** With [examples!](https://github.com/spessasus/SpessaSynth/wiki/Usage-As-Library#examples) -- **Easy to Use:** basic setup is just [two lines of code!](https://github.com/spessasus/SpessaSynth/wiki/Usage-As-Library#minimal-setup) +- **[Detailed documentation:](https://github.com/spessasus/SpessaSynth/wiki/Home)** + With [examples!](https://github.com/spessasus/SpessaSynth/wiki/Usage-As-Library#examples) +- **Easy to Use:** basic setup is + just [two lines of code!](https://github.com/spessasus/SpessaSynth/wiki/Usage-As-Library#minimal-setup) - **No dependencies:** _batteries included!_ ### Powerful SoundFont Synthesizer + - Suitable for both **real-time** and **offline** synthesis - **Excellent SoundFont support:** - - **Generator Support** - - **Modulator Support:** *First (to my knowledge) JavaScript SoundFont synth with that feature!* - - **SoundFont3 Support:** Play compressed SoundFonts! - - **Experimental SF2Pack Support:** Play soundfonts compressed with BASSMIDI! (*Note: only works with vorbis compression*) - - **Can load very large SoundFonts:** up to 4GB! *Note: Only Firefox handles this well; Chromium has a hard-coded memory limit* + - **Generator Support** + - **Modulator Support:** *First (to my knowledge) JavaScript SoundFont synth with that feature!* + - **SoundFont3 Support:** Play compressed SoundFonts! + - **Experimental SF2Pack Support:** Play soundfonts compressed with BASSMIDI! (*Note: only works with vorbis + compression*) + - **Can load very large SoundFonts:** up to 4GB! *Note: Only Firefox handles this well; Chromium has a hard-coded + memory limit* - **Soundfont manager:** Stack multiple soundfonts! - **DLS Level 1 and 2 Support:** *internally converted to sf2* -- **Reverb and chorus support:** [customizable!](https://github.com/spessasus/SpessaSynth/wiki/Synthetizer-Class#effects-configuration-object) -- **Export audio files** using [OfflineAudioContext](https://developer.mozilla.org/en-US/docs/Web/API/OfflineAudioContext) -- **[Custom modulators for additional controllers](https://github.com/spessasus/SpessaSynth/wiki/Modulator-Class#default-modulators):** Why not? +- **Reverb and chorus support: + ** [customizable!](https://github.com/spessasus/SpessaSynth/wiki/Synthetizer-Class#effects-configuration-object) +- **Export audio files** + using [OfflineAudioContext](https://developer.mozilla.org/en-US/docs/Web/API/OfflineAudioContext) +- + * + +*[Custom modulators for additional controllers](https://github.com/spessasus/SpessaSynth/wiki/Modulator-Class#default-modulators): +** Why not? + - **Written using AudioWorklets:** - - Runs in a **separate thread** for maximum performance - - Supported by all modern browsers + - Runs in a **separate thread** for maximum performance + - Supported by all modern browsers - **Unlimited channel count:** Your CPU is the limit! - **Excellent MIDI Standards Support:** - - **MIDI Controller Support:** Default supported controllers [here](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#supported-controllers) - - **MIDI Tuning Standard Support:** [more info here](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#midi-tuning-standard) - - [Full **RPN** and limited **NRPN** support](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#supported-registered-parameters) - - Supports some [**Roland GS** and **Yamaha XG** system exclusives](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#supported-system-exclusives) + - **MIDI Controller Support:** Default supported + controllers [here](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#supported-controllers) + - **MIDI Tuning Standard Support: + ** [more info here](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#midi-tuning-standard) + - [Full **RPN** and limited **NRPN + ** support](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#supported-registered-parameters) + - Supports some [**Roland GS** and **Yamaha XG + ** system exclusives](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#supported-system-exclusives) - **High-performance mode:** Play Rush E! _note: may kill your browser ;)_ ### Built-in Powerful and Fast Sequencer + - **Supports MIDI formats 0, 1, and 2:** _note: format 2 support is experimental as it's very, very rare_ - **[Multi-Port MIDI](https://github.com/spessasus/SpessaSynth/wiki/About-Multi-Port) support:** More than 16 channels! - **Smart preloading:** Only preloads the samples used in the MIDI file for smooth playback (down to key and velocity!) @@ -68,41 +91,54 @@ document.getElementById("button").onclick = async () => { - **Loop points support:** Ensures seamless loops ### Read and Write SoundFont and MIDI Files with Ease + #### Read and write MIDI files + - **Smart name detection:** Handles incorrectly formatted and non-standard track names - **Raw name available:** Decode in any encoding! *(Kanji? No problem!)* - **Port detection during load time:** Manage ports and channels easily! - **Used channels on track:** Quickly determine which channels are used - **Key range detection:** Detect the key range of the MIDI -- **Easy MIDI editing:** Use [helper functions](https://github.com/spessasus/SpessaSynth/wiki/Writing-MIDI-Files#modifymidi) to modify the song to your needs! +- **Easy MIDI editing:** + Use [helper functions](https://github.com/spessasus/SpessaSynth/wiki/Writing-MIDI-Files#modifymidi) to modify the song + to your needs! - **Loop detection:** Automatically detects loops in MIDIs (e.g., from _Touhou Project_) - **First note detection:** Skip unnecessary silence at the start by jumping to the first note! - **[Write MIDI files from scratch](https://github.com/spessasus/SpessaSynth/wiki/Creating-MIDI-Files)** -- **Easy saving:** Save with just [one function!](https://github.com/spessasus/SpessaSynth/wiki/Writing-MIDI-Files#writemidifile) +- **Easy saving:** Save with + just [one function!](https://github.com/spessasus/SpessaSynth/wiki/Writing-MIDI-Files#writemidifile) #### Read and write [RMID files with embedded SF2 soundfonts](https://github.com/spessasus/sf2-rmidi-specification#readme) + - **[Level 4](https://github.com/spessasus/sf2-rmidi-specification#level-4) compliance:** Reads and writes *everything!* - **Compression and trimming support:** Reduce a MIDI file with a 1GB soundfont to **as small as 5MB**! - **DLS Version support:** The original legacy format with bank offset detection! - **Automatic bank shifting and validation:** Every soundfont *just works!* -- **Metadata support:** Add title, artist, album name and cover and more! And of course read them too! *(In any encoding!)* +- **Metadata support:** Add title, artist, album name and cover and more! And of course read them too! *(In any + encoding!)* - **Compatible with [Falcosoft Midi Player 6!](https://falcosoft.hu/softwares.html#midiplayer)** -- **Easy saving:** [As simple as saving a MIDI file!](https://github.com/spessasus/SpessaSynth/wiki/Writing-MIDI-Files#writermidi) +- **Easy saving: + ** [As simple as saving a MIDI file!](https://github.com/spessasus/SpessaSynth/wiki/Writing-MIDI-Files#writermidi) #### Read and write SoundFont2 files -- **Easy info access:** Just an [object of strings!](https://github.com/spessasus/SpessaSynth/wiki/SoundFont2-Class#soundfontinfo) + +- **Easy info access:** Just + an [object of strings!](https://github.com/spessasus/SpessaSynth/wiki/SoundFont2-Class#soundfontinfo) - **Smart trimming:** Trim the SoundFont to only include samples used in the MIDI *(down to key and velocity!)* - **sf3 conversion:** Compress SoundFont2 files to SoundFont3 with variable quality! - **Easy saving:** Also just [one function!](https://github.com/spessasus/SpessaSynth/wiki/SoundFont2-Class#write) #### Read and write SoundFont3 files + - Same features as SoundFont2 but with now with **Ogg Vorbis compression!** - **Variable compression quality:** You choose between file size and quality! - **Compression preserving:** Avoid decompressing and recompressing uncompressed samples for minimal quality loss! #### Read and play DLS Level 1 or 2 files + - Read DLS (DownLoadable Sounds) files as SF2 files! -- **Works like a normal soundfont:** *Saving it as sf2 is still [just one function!](https://github.com/spessasus/SpessaSynth/wiki/SoundFont2-Class#write)* +- **Works like a normal soundfont:** *Saving it as sf2 is + still [just one function!](https://github.com/spessasus/SpessaSynth/wiki/SoundFont2-Class#write)* - Converts articulators to both **modulators** and **generators**! - Works with both unsigned 8-bit samples and signed 16-bit samples! - **Covers special generator cases:** *such as modLfoToPitch*! @@ -110,12 +146,14 @@ document.getElementById("button").onclick = async () => { - Support built right into the synthesizer! ### Export MIDI as WAV + - Save the MIDI file as WAV audio! - **Metadata support:** *Embed metadata such as title, artist, album and more!* - **Cue points:** *Write MIDI loop points as cue points!* - **Loop multiple times:** *Render two (or more) loops into the file for seamless transitions!* -- *That's right, saving as WAV is also [just one function!](https://github.com/spessasus/SpessaSynth/wiki/Writing-Wave-Files#audiobuffertowav)* - +- *That's right, saving as WAV is + also [just one function!](https://github.com/spessasus/SpessaSynth/wiki/Writing-Wave-Files#audiobuffertowav)* # License + MIT License, except for the stbvorbis_sync.js in the `externals` folder which is licensed under the Apache-2.0 license. \ No newline at end of file diff --git a/src/spessasynth_lib/external_midi/README.md b/src/spessasynth_lib/external_midi/README.md index 3e0cd0c8..20e67ffa 100644 --- a/src/spessasynth_lib/external_midi/README.md +++ b/src/spessasynth_lib/external_midi/README.md @@ -1,3 +1,4 @@ ## This is the MIDI handling folder. + The code here is respnsible for dealing with MIDI Inputs and outputs and also for the WebMidiLink functionality. \ No newline at end of file diff --git a/src/spessasynth_lib/external_midi/midi_handler.js b/src/spessasynth_lib/external_midi/midi_handler.js index 659369f3..e3671e35 100644 --- a/src/spessasynth_lib/external_midi/midi_handler.js +++ b/src/spessasynth_lib/external_midi/midi_handler.js @@ -1,6 +1,6 @@ -import { Synthetizer } from '../synthetizer/synthetizer.js' -import { consoleColors } from '../utils/other.js'; -import { SpessaSynthInfo, SpessaSynthWarn } from '../utils/loggin.js' +import { Synthetizer } from "../synthetizer/synthetizer.js"; +import { consoleColors } from "../utils/other.js"; +import { SpessaSynthInfo, SpessaSynthWarn } from "../utils/loggin.js"; /** * midi_handler.js @@ -12,8 +12,9 @@ const NO_INPUT = null; export class MIDIDeviceHandler { constructor() - {} - + { + } + /** * @returns {Promise} if succeded */ @@ -27,7 +28,8 @@ export class MIDIDeviceHandler * @type {MIDIOutput} */ this.selectedOutput = NO_INPUT; - if(navigator.requestMIDIAccess) { + if (navigator.requestMIDIAccess) + { // prepare the midi access try { @@ -36,12 +38,12 @@ export class MIDIDeviceHandler this.outputs = response.outputs; SpessaSynthInfo("%cMIDI handler created!", consoleColors.recognized); return true; - } - catch (e) { + } catch (e) + { SpessaSynthWarn(`Could not get MIDI Devices:`, e); this.inputs = []; this.outputs = []; - return false + return false; } } else @@ -49,10 +51,10 @@ export class MIDIDeviceHandler SpessaSynthWarn("Web MIDI Api not supported!", consoleColors.unrecognized); this.inputs = []; this.outputs = []; - return false + return false; } } - + /** * Connects the sequencer to a given MIDI output port * @param output {MIDIOutput} @@ -62,11 +64,13 @@ export class MIDIDeviceHandler { this.selectedOutput = output; seq.connectMidiOutput(output); - SpessaSynthInfo(`%cPlaying MIDI to %c${output.name}`, + SpessaSynthInfo( + `%cPlaying MIDI to %c${output.name}`, consoleColors.info, - consoleColors.recognized); + consoleColors.recognized + ); } - + /** * Disconnects a midi output port from the sequencer * @param seq {Sequencer} @@ -75,10 +79,12 @@ export class MIDIDeviceHandler { this.selectedOutput = NO_INPUT; seq.connectMidiOutput(undefined); - SpessaSynthInfo("%cDisconnected from MIDI out.", - consoleColors.info); + SpessaSynthInfo( + "%cDisconnected from MIDI out.", + consoleColors.info + ); } - + /** * Connects a MIDI input to the synthesizer * @param input {MIDIInput} @@ -87,14 +93,17 @@ export class MIDIDeviceHandler connectDeviceToSynth(input, synth) { this.selectedInput = input; - input.onmidimessage = event => { + input.onmidimessage = event => + { synth.sendMessage(event.data); - } - SpessaSynthInfo(`%cListening for messages on %c${input.name}`, + }; + SpessaSynthInfo( + `%cListening for messages on %c${input.name}`, consoleColors.info, - consoleColors.recognized); + consoleColors.recognized + ); } - + /** * @param input {MIDIInput} */ @@ -102,15 +111,17 @@ export class MIDIDeviceHandler { this.selectedInput = NO_INPUT; input.onmidimessage = undefined; - SpessaSynthInfo(`%cDisconnected from %c${input.name}`, + SpessaSynthInfo( + `%cDisconnected from %c${input.name}`, consoleColors.info, - consoleColors.recognized); + consoleColors.recognized + ); } - + disconnectAllDevicesFromSynth() { this.selectedInput = NO_INPUT; - for(const i of this.inputs) + for (const i of this.inputs) { i[1].onmidimessage = undefined; } diff --git a/src/spessasynth_lib/external_midi/web_midi_link.js b/src/spessasynth_lib/external_midi/web_midi_link.js index 229faa42..476b68e8 100644 --- a/src/spessasynth_lib/external_midi/web_midi_link.js +++ b/src/spessasynth_lib/external_midi/web_midi_link.js @@ -1,6 +1,6 @@ -import { Synthetizer } from '../synthetizer/synthetizer.js' -import { consoleColors } from '../utils/other.js' -import { SpessaSynthInfo } from '../utils/loggin.js' +import { Synthetizer } from "../synthetizer/synthetizer.js"; +import { consoleColors } from "../utils/other.js"; +import { SpessaSynthInfo } from "../utils/loggin.js"; /** * web_midi_link.js @@ -15,28 +15,29 @@ export class WebMidiLinkHandler */ constructor(synth) { - - window.addEventListener("message", msg => { - if(typeof msg.data !== "string") + + window.addEventListener("message", msg => + { + if (typeof msg.data !== "string") { - return + return; } /** * @type {string[]} */ const data = msg.data.split(","); - if(data[0] !== "midi") + if (data[0] !== "midi") { return; } - + data.shift(); // remove MIDI - + const midiData = data.map(byte => parseInt(byte, 16)); - + synth.sendMessage(midiData); }); - + SpessaSynthInfo("%cWeb MIDI Link handler created!", consoleColors.recognized); } } \ No newline at end of file diff --git a/src/spessasynth_lib/externals/stbvorbis_sync/@types/stbvorbis_sync.d.ts b/src/spessasynth_lib/externals/stbvorbis_sync/@types/stbvorbis_sync.d.ts index 7b581b50..3109d804 100644 --- a/src/spessasynth_lib/externals/stbvorbis_sync/@types/stbvorbis_sync.d.ts +++ b/src/spessasynth_lib/externals/stbvorbis_sync/@types/stbvorbis_sync.d.ts @@ -1,10 +1,10 @@ declare type DecodedData = -{ - data: Float32Array[], - error: string|null, - sampleRate: number, - eof: boolean -} + { + data: Float32Array[], + error: string | null, + sampleRate: number, + eof: boolean + } declare const stbvorbis: { decode(buffer: ArrayBuffer): DecodedData diff --git a/src/spessasynth_lib/midi_parser/README.md b/src/spessasynth_lib/midi_parser/README.md index 934720d9..37f8ce94 100644 --- a/src/spessasynth_lib/midi_parser/README.md +++ b/src/spessasynth_lib/midi_parser/README.md @@ -1,3 +1,4 @@ ## This is the MIDI file parsing folder. + The code here is responsible for parsing the MIDI files and interpreting the messsages. All the events are defined in the `midi_message.js` file. \ No newline at end of file diff --git a/src/spessasynth_lib/midi_parser/basic_midi.js b/src/spessasynth_lib/midi_parser/basic_midi.js index adb8df83..dc0ccab5 100644 --- a/src/spessasynth_lib/midi_parser/basic_midi.js +++ b/src/spessasynth_lib/midi_parser/basic_midi.js @@ -16,97 +16,97 @@ export class BasicMIDI * The tempo changes in the sequence, ordered from last to first * @type {{ticks: number, tempo: number}[]} */ - this.tempoChanges = [{ticks: 0, tempo: 120}]; + this.tempoChanges = [{ ticks: 0, tempo: 120 }]; /** * Contains the copyright strings * @type {string} */ this.copyright = ""; - + /** * The amount of tracks in the sequence * @type {number} */ this.tracksAmount = 0; - + /** * The lyrics of the sequence as binary chunks * @type {Uint8Array[]} */ this.lyrics = []; - + /** * First note on of the MIDI file * @type {number} */ this.firstNoteOn = 0; - + /** * The MIDI's key range * @type {{min: number, max: number}} */ this.keyRange = { min: 0, max: 127 }; - + /** * The last voice (note on, off, cc change etc.) event tick * @type {number} */ this.lastVoiceEventTick = 0; - + /** * Midi port numbers for each track * @type {number[]} */ this.midiPorts = [0]; - + /** * Channel offsets for each port, using the SpessaSynth method * @type {number[]} */ this.midiPortChannelOffsets = [0]; - + /** * All channels that each track uses * @type {Set[]} */ this.usedChannelsOnTrack = []; - + /** * The loop points (in ticks) of the sequence * @type {{start: number, end: number}} */ this.loop = { start: 0, end: 0 }; - + /** * The sequence's name * @type {string} */ this.midiName = ""; - + /** * The file name of the sequence, if provided in the MIDI class * @type {string} */ this.fileName = ""; - + /** * The raw, encoded MIDI name. * @type {Uint8Array} */ this.rawMidiName = undefined; - + /** * The MIDI's embedded soundfont * @type {ArrayBuffer|undefined} */ this.embeddedSoundFont = undefined; - + /** * The MIDI file's format * @type {number} */ this.format = 0; - + /** * The RMID Info data if RMID, otherwise undefined * @type {Object} @@ -117,7 +117,7 @@ export class BasicMIDI * @type {number} */ this.bankOffset = 0; - + /** * The actual track data of the MIDI file * @type {MidiMessage[][]} @@ -132,9 +132,10 @@ export class BasicMIDI * @param mid {BasicMIDI} the MIDI * @returns {number} time in seconds */ -export function MIDIticksToSeconds(ticks, mid) { +export function MIDIticksToSeconds(ticks, mid) +{ let totalSeconds = 0; - + while (ticks > 0) { // tempo changes are reversed so the first element is the last tempo change @@ -142,12 +143,12 @@ export function MIDIticksToSeconds(ticks, mid) { // (always at tick 0 and tempo 120) // find the last tempo change that has occurred let tempo = mid.tempoChanges.find(v => v.ticks < ticks); - + // calculate the difference and tempo time let timeSinceLastTempo = ticks - tempo.ticks; totalSeconds += (timeSinceLastTempo * 60) / (tempo.tempo * mid.timeDivision); ticks -= timeSinceLastTempo; } - + return totalSeconds; } \ No newline at end of file diff --git a/src/spessasynth_lib/midi_parser/midi_builder.js b/src/spessasynth_lib/midi_parser/midi_builder.js index 60b5b06a..c111c7e6 100644 --- a/src/spessasynth_lib/midi_parser/midi_builder.js +++ b/src/spessasynth_lib/midi_parser/midi_builder.js @@ -1,8 +1,8 @@ -import { BasicMIDI, MIDIticksToSeconds } from './basic_midi.js' -import { messageTypes, MidiMessage } from './midi_message.js' -import { IndexedByteArray } from '../utils/indexed_array.js' -import { readBytesAsUintBigEndian } from '../utils/byte_functions/big_endian.js' -import { SpessaSynthWarn } from '../utils/loggin.js' +import { BasicMIDI, MIDIticksToSeconds } from "./basic_midi.js"; +import { messageTypes, MidiMessage } from "./midi_message.js"; +import { IndexedByteArray } from "../utils/indexed_array.js"; +import { readBytesAsUintBigEndian } from "../utils/byte_functions/big_endian.js"; +import { SpessaSynthWarn } from "../utils/loggin.js"; export class MIDIBuilder extends BasicMIDI { @@ -18,39 +18,39 @@ export class MIDIBuilder extends BasicMIDI this.midiName = name; this.encoder = new TextEncoder(); this.rawMidiName = this.encoder.encode(name); - + // create the first track with the file name this.addNewTrack(name); this.addSetTempo(0, initialTempo); } - + /** * Updates all internal values */ flush() { - + // find first note on const firstNoteOns = []; - for(const t of this.tracks) + for (const t of this.tracks) { // sost the track by ticks t.sort((e1, e2) => e1.ticks - e2.ticks); const firstNoteOn = t.find(e => (e.messageStatusByte & 0xF0) === messageTypes.noteOn); - if(firstNoteOn) + if (firstNoteOn) { firstNoteOns.push(firstNoteOn.ticks); } } this.firstNoteOn = Math.min(...firstNoteOns); - + // find tempo changes // and used channels on tracks // and midi ports // and last voice event tick // and loop - this.lastVoiceEventTick = 0 - this.tempoChanges = [{ticks: 0, tempo: 120}]; + this.lastVoiceEventTick = 0; + this.tempoChanges = [{ ticks: 0, tempo: 120 }]; this.midiPorts = []; this.midiPortChannelOffsets = []; let portOffset = 0; @@ -58,60 +58,63 @@ export class MIDIBuilder extends BasicMIDI * @type {Set[]} */ this.usedChannelsOnTrack = this.tracks.map(() => new Set()); - this.tracks.forEach((t, trackNum) => { + this.tracks.forEach((t, trackNum) => + { this.midiPorts.push(-1); - t.forEach(e => { + t.forEach(e => + { // last voice event tick - if(e.messageStatusByte >= 0x80 && e.messageStatusByte < 0xF0) + if (e.messageStatusByte >= 0x80 && e.messageStatusByte < 0xF0) { - if(e.ticks > this.lastVoiceEventTick) + if (e.ticks > this.lastVoiceEventTick) { this.lastVoiceEventTick = e.ticks; } } - + // tempo, used channels, port - if(e.messageStatusByte === messageTypes.setTempo) + if (e.messageStatusByte === messageTypes.setTempo) { this.tempoChanges.push({ ticks: e.ticks, - tempo: 60000000 / readBytesAsUintBigEndian(e.messageData, 3) + tempo: 60000000 / readBytesAsUintBigEndian( + e.messageData, + 3 + ) }); } - else - if((e.messageStatusByte & 0xF0) === messageTypes.noteOn) + else if ((e.messageStatusByte & 0xF0) === messageTypes.noteOn) { this.usedChannelsOnTrack[trackNum].add(e.messageData[0]); } - else - if(e.messageStatusByte === messageTypes.midiPort) + else if (e.messageStatusByte === messageTypes.midiPort) { const port = e.messageData[0]; this.midiPorts[trackNum] = port; - if(this.midiPortChannelOffsets[port] === undefined) + if (this.midiPortChannelOffsets[port] === undefined) { this.midiPortChannelOffsets[port] = portOffset; portOffset += 16; } } - }) + }); }); - - this.loop = {start: this.firstNoteOn, end: this.lastVoiceEventTick}; - + + this.loop = { start: this.firstNoteOn, end: this.lastVoiceEventTick }; + // reverse tempo and compute duration this.tempoChanges.reverse(); this.duration = MIDIticksToSeconds(this.lastVoiceEventTick, this); - + // fix midi ports: // midi tracks without ports will have a value of -1 // if all ports have a value of -1, set it to 0, otherwise take the first midi port and replace all -1 with it // why do this? some midis (for some reason) specify all channels to port 1 or else, but leave the conductor track with no port pref. // this spessasynth to reserve the first 16 channels for the conductor track (which doesn't play anything) and use additional 16 for the actual ports. let defaultP = 0; - for(let port of this.midiPorts) + for (let port of this.midiPorts) { - if(port !== -1) + if (port !== -1) { defaultP = port; break; @@ -119,12 +122,12 @@ export class MIDIBuilder extends BasicMIDI } this.midiPorts = this.midiPorts.map(port => port === -1 ? defaultP : port); // add dummy port if empty - if(this.midiPortChannelOffsets.length === 0) + if (this.midiPortChannelOffsets.length === 0) { this.midiPortChannelOffsets = [0]; } } - + /** * Adds a new "set tempo" message * @param ticks {number} the tick number of the event @@ -133,17 +136,17 @@ export class MIDIBuilder extends BasicMIDI addSetTempo(ticks, tempo) { const array = new IndexedByteArray(3); - + tempo = 60000000 / tempo; - + // Extract each byte in big-endian order array[0] = (tempo >> 16) & 0xFF; array[1] = (tempo >> 8) & 0xFF; array[2] = tempo & 0xFF; - + this.addEvent(ticks, 0, messageTypes.setTempo, array); } - + /** * Adds a new MIDI track * @param name {string} the new track's name @@ -152,7 +155,7 @@ export class MIDIBuilder extends BasicMIDI addNewTrack(name, port = 0) { this.tracksAmount++; - if(this.tracksAmount > 1) + if (this.tracksAmount > 1) { this.format = 1; } @@ -163,7 +166,7 @@ export class MIDIBuilder extends BasicMIDI this.addEvent(0, this.tracksAmount - 1, messageTypes.trackName, this.encoder.encode(name)); this.addEvent(0, this.tracksAmount - 1, messageTypes.midiPort, [port]); } - + /** * Adds a new MIDI Event * @param ticks {number} the tick time of the event @@ -173,11 +176,11 @@ export class MIDIBuilder extends BasicMIDI */ addEvent(ticks, track, event, eventData) { - if(!this.tracks[track]) + if (!this.tracks[track]) { throw new Error(`Track ${track} does not exist. Add it via addTrack method.`); } - if(event === messageTypes.endOfTrack) + if (event === messageTypes.endOfTrack) { SpessaSynthWarn("The EndOfTrack is added automatically. Ignoring!"); return; @@ -196,7 +199,7 @@ export class MIDIBuilder extends BasicMIDI new IndexedByteArray(0) )); } - + /** * Adds a new Note On event * @param ticks {number} the tick time of the event @@ -217,7 +220,7 @@ export class MIDIBuilder extends BasicMIDI [midiNote, velocity] ); } - + /** * Adds a new Note Off event * @param ticks {number} the tick time of the event @@ -236,7 +239,7 @@ export class MIDIBuilder extends BasicMIDI [midiNote, 64] ); } - + /** * Adds a new Controller Change event * @param ticks {number} the tick time of the event @@ -257,7 +260,7 @@ export class MIDIBuilder extends BasicMIDI [controllerNumber, controllerValue] ); } - + /** * Adds a new Pitch Wheel event * @param ticks {number} the tick time of the event diff --git a/src/spessasynth_lib/midi_parser/midi_data.js b/src/spessasynth_lib/midi_parser/midi_data.js index 08167913..ea6bae99 100644 --- a/src/spessasynth_lib/midi_parser/midi_data.js +++ b/src/spessasynth_lib/midi_parser/midi_data.js @@ -29,81 +29,81 @@ export class MidiData * @type {string} */ this.copyright = midi.copyright; - + /** * The amount of tracks in the sequence * @type {number} */ this.tracksAmount = midi.tracksAmount; - + /** * The lyrics of the sequence as binary chunks * @type {Uint8Array[]} */ this.lyrics = midi.lyrics; - + this.firstNoteOn = midi.firstNoteOn; - + /** * The MIDI's key range * @type {{min: number, max: number}} */ this.keyRange = midi.keyRange; - + /** * The last voice (note on, off, cc change etc.) event tick * @type {number} */ this.lastVoiceEventTick = midi.lastVoiceEventTick; - + /** * Midi port numbers for each track * @type {number[]} */ this.midiPorts = midi.midiPorts; - + /** * Channel offsets for each port, using the SpessaSynth method * @type {number[]} */ this.midiPortChannelOffsets = midi.midiPortChannelOffsets; - + /** * All channels that each track uses * @type {Set[]} */ this.usedChannelsOnTrack = midi.usedChannelsOnTrack; - + /** * The loop points (in ticks) of the sequence * @type {{start: number, end: number}} */ this.loop = midi.loop; - + /** * The sequence's name * @type {string} */ this.midiName = midi.midiName; - + /** * The file name of the sequence, if provided in the MIDI class * @type {string} */ this.fileName = midi.fileName; - + /** * The raw, encoded MIDI name. * @type {Uint8Array} */ this.rawMidiName = midi.rawMidiName; - + /** * Indicates if the midi has an embedded soundfont * @type {boolean} */ this.isEmbedded = midi.embeddedSoundFont !== undefined; - + /** * The RMID Info data if RMID, otherwise undefined * @type {Object} @@ -128,20 +128,20 @@ export const DUMMY_MIDI_DATA = { start: 0, end: 123456 }, - + lastVoiceEventTick: 123456, lyrics: [], copyright: "", midiPorts: [], midiPortChannelOffsets: [], tracksAmount: 0, - tempoChanges: [{ticks: 0, tempo: 120}], + tempoChanges: [{ ticks: 0, tempo: 120 }], fileName: "NOT_LOADED.mid", midiName: "Loading...", rawMidiName: new Uint8Array([76, 111, 97, 100, 105, 110, 103, 46, 46, 46]), // "Loading..." usedChannelsOnTrack: [], timeDivision: 0, - keyRange: {min: 0, max: 127}, + keyRange: { min: 0, max: 127 }, isEmbedded: false, RMIDInfo: undefined, bankOffset: 0 diff --git a/src/spessasynth_lib/midi_parser/midi_editor.js b/src/spessasynth_lib/midi_parser/midi_editor.js index 2e574b03..02ff44a6 100644 --- a/src/spessasynth_lib/midi_parser/midi_editor.js +++ b/src/spessasynth_lib/midi_parser/midi_editor.js @@ -1,9 +1,9 @@ -import { messageTypes, midiControllers, MidiMessage } from './midi_message.js' -import { IndexedByteArray } from '../utils/indexed_array.js' -import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn } from '../utils/loggin.js' -import { consoleColors } from '../utils/other.js' -import { DEFAULT_PERCUSSION } from '../synthetizer/synthetizer.js' -import { customControllers } from '../synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js' +import { messageTypes, midiControllers, MidiMessage } from "./midi_message.js"; +import { IndexedByteArray } from "../utils/indexed_array.js"; +import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn } from "../utils/loggin.js"; +import { consoleColors } from "../utils/other.js"; +import { DEFAULT_PERCUSSION } from "../synthetizer/synthetizer.js"; +import { customControllers } from "../synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js"; /** * @param ticks {number} @@ -24,7 +24,7 @@ export function getGsOn(ticks) 0x7F, // GS Change } 0x00, // turn on } Data 0x41, // checksum - 0xF7, // end of exclusive + 0xF7 // end of exclusive ]) ); } @@ -55,8 +55,8 @@ function getDrumChange(channel, ticks) 0x40, // System parameter } chanAddress, // Channel parameter } Address 0x15, // Drum change } - 0x01, // Is Drums } Data - ] + 0x01 // Is Drums } Data + ]; // calculate checksum // https://cdn.roland.com/assets/media/pdf/F-20_MIDI_Imple_e01_W.pdf section 4 const sum = 0x40 + chanAddress + 0x15 + 0x01; @@ -106,33 +106,38 @@ export function modifyMIDI( * @param channel {number} * @param port {number} */ - const clearChannelMessages = (channel, port) => { - midi.tracks.forEach((track, trackNum) => { - if(midi.midiPorts[trackNum] !== port) + const clearChannelMessages = (channel, port) => + { + midi.tracks.forEach((track, trackNum) => + { + if (midi.midiPorts[trackNum] !== port) { return; } - for(let i = track.length - 1; i >= 0; i--) // iterate in reverse to not mess up indexes + for (let i = track.length - 1; i >= 0; i--) // iterate in reverse to not mess up indexes { - if(track[i].messageStatusByte >= 0x80 && track[i].messageStatusByte < 0xF0) // do not clear sysexes + if (track[i].messageStatusByte >= 0x80 && track[i].messageStatusByte < 0xF0) // do not clear sysexes { - if((track[i].messageStatusByte & 0xF) === channel) + if ((track[i].messageStatusByte & 0xF) === channel) { track.splice(i, 1); } } } }); - } - desiredChannelsToClear.forEach(c => { + }; + desiredChannelsToClear.forEach(c => + { const channel = c % 16; const offset = c - channel; const port = midi.midiPortChannelOffsets.findIndex(o => o === offset); clearChannelMessages(channel, port); - SpessaSynthInfo(`%cRemoving channel %c${c}%c!`, + SpessaSynthInfo( + `%cRemoving channel %c${c}%c!`, consoleColors.info, consoleColors.recognized, - consoleColors.info); + consoleColors.info + ); }); let addedGs = false; let midiSystem = "gs"; @@ -153,18 +158,20 @@ export function modifyMIDI( * }[]} */ const programChanges = []; - midi.tracks.forEach((track, trackNum) => { - track.forEach(message => { + midi.tracks.forEach((track, trackNum) => + { + track.forEach(message => + { const status = message.messageStatusByte & 0xF0; - if(status === messageTypes.controllerChange) + if (status === messageTypes.controllerChange) { ccChanges.push({ track: trackNum, message: message, channel: message.messageStatusByte & 0xF - }) + }); } - else if(status === messageTypes.programChange) + else if (status === messageTypes.programChange) { programChanges.push({ track: trackNum, @@ -172,10 +179,10 @@ export function modifyMIDI( channel: message.messageStatusByte & 0xF }); } - else if(message.messageStatusByte === messageTypes.systemExclusive) + else if (message.messageStatusByte === messageTypes.systemExclusive) { // check for xg - if( + if ( message.messageData[0] === 0x43 && // Yamaha message.messageData[2] === 0x4C && // XG ON message.messageData[5] === 0x7E && @@ -187,7 +194,7 @@ export function modifyMIDI( addedGs = true; // flag as true so gs won't get added } else - // check for xg program change + // check for xg program change if ( message.messageData[0] === 0x43 // yamaha && message.messageData[2] === 0x4C // XG @@ -202,9 +209,9 @@ export function modifyMIDI( }); } } - }) + }); }); - + /** * @param chan {number} * @param port {number} @@ -213,13 +220,15 @@ export function modifyMIDI( * First voice otherwise, because MP6 doesn't like program changes after cc changes in embedded midis * @return {{index: number, track: number}[]} */ - const getFirstVoiceForChannel = (chan, port, searchForNoteOn) => { + const getFirstVoiceForChannel = (chan, port, searchForNoteOn) => + { return midi.tracks - .reduce((noteOns, track, trackNum) => { - if(midi.usedChannelsOnTrack[trackNum].has(chan) && midi.midiPorts[trackNum] === port) + .reduce((noteOns, track, trackNum) => + { + if (midi.usedChannelsOnTrack[trackNum].has(chan) && midi.midiPorts[trackNum] === port) { let eventIndex; - if(searchForNoteOn) + if (searchForNoteOn) { eventIndex = track.findIndex(event => // event is a noteon @@ -243,7 +252,7 @@ export function modifyMIDI( ) ); } - if(eventIndex !== -1) + if (eventIndex !== -1) { noteOns.push({ index: eventIndex, @@ -253,30 +262,32 @@ export function modifyMIDI( } return noteOns; }, []); - } - - + }; + + /** * @param channel {number} * @param port {number} * @param cc {number} */ - const clearControllers = (channel, port, cc,) => { + const clearControllers = (channel, port, cc) => + { const thisCcChanges = ccChanges.filter(m => m.channel === channel && m.message.messageData[0] === cc && midi.midiPorts[m.track] === port); // delete - for(let i = 0; i < thisCcChanges.length; i++) + for (let i = 0; i < thisCcChanges.length; i++) { // remove const e = thisCcChanges[i]; midi.tracks[e.track].splice(midi.tracks[e.track].indexOf(e.message), 1); ccChanges.splice(ccChanges.indexOf(e), 1); } - - } - desiredControllerChanges.forEach(desiredChange => { + + }; + desiredControllerChanges.forEach(desiredChange => + { const channel = desiredChange.channel; const midiChannel = channel % 16; const offset = channel - midiChannel; @@ -286,7 +297,8 @@ export function modifyMIDI( // the controller is locked. Clear all controllers clearControllers(midiChannel, port, ccNumber); // since we've removed all ccs, we need to add the first one. - SpessaSynthInfo(`%cNo controller %c${ccNumber}%c on channel %c${channel}%c found. Adding it!`, + SpessaSynthInfo( + `%cNo controller %c${ccNumber}%c on channel %c${channel}%c found. Adding it!`, consoleColors.info, consoleColors.unrecognized, consoleColors.info, @@ -297,7 +309,7 @@ export function modifyMIDI( * @type {{index: number, track: number}[]} */ const firstNoteOnForTrack = getFirstVoiceForChannel(midiChannel, port, true); - if(firstNoteOnForTrack.length === 0) + if (firstNoteOnForTrack.length === 0) { SpessaSynthWarn("Program change but no notes... ignoring!"); return; @@ -305,37 +317,44 @@ export function modifyMIDI( const firstNoteOn = firstNoteOnForTrack.reduce((first, current) => midi.tracks[current.track][current.index].ticks < midi.tracks[first.track][first.index].ticks ? current : first); // prepend with controller change - const ccChange = getControllerChange(midiChannel, ccNumber, targetValue, midi.tracks[firstNoteOn.track][firstNoteOn.index].ticks); + const ccChange = getControllerChange( + midiChannel, + ccNumber, + targetValue, + midi.tracks[firstNoteOn.track][firstNoteOn.index].ticks + ); midi.tracks[firstNoteOn.track].splice(firstNoteOn.index, 0, ccChange); }); - - desiredProgramChanges.forEach(change => { + + desiredProgramChanges.forEach(change => + { const midiChannel = change.channel % 16; const offset = change.channel - midiChannel; const port = midi.midiPortChannelOffsets.findIndex(o => o === offset); let desiredBank = change.isDrum ? 0 : change.bank; const desiredProgram = change.program; - + // get the program changes that are relevant for this channel (and port) const thisProgramChanges = programChanges.filter(c => midi.midiPorts[c.track] === port && c.channel === midiChannel); - - + + // clear bank selects clearControllers(midiChannel, port, midiControllers.bankSelect); clearControllers(midiChannel, port, midiControllers.lsbForControl0BankSelect); - + // if drums or the program uses bank select, flag as gs - if((change.isDrum || desiredBank > 0) && !addedGs) + if ((change.isDrum || desiredBank > 0) && !addedGs) { // make sure that GS is on // GS on: F0 41 10 42 12 40 00 7F 00 41 F7 - midi.tracks.forEach(track => { - for(let eventIndex = 0; eventIndex < track.length; eventIndex++) + midi.tracks.forEach(track => + { + for (let eventIndex = 0; eventIndex < track.length; eventIndex++) { const event = track[eventIndex]; - if(event.messageStatusByte === messageTypes.systemExclusive) + if (event.messageStatusByte === messageTypes.systemExclusive) { - if( + if ( event.messageData[0] === 0x41 // roland && event.messageData[2] === 0x42 // GS && event.messageData[6] === 0x7F // Mode set @@ -343,47 +362,58 @@ export function modifyMIDI( { // thats a GS on, we're done here addedGs = true; - SpessaSynthInfo("%cGS on detected!", consoleColors.recognized); + SpessaSynthInfo( + "%cGS on detected!", + consoleColors.recognized + ); break; } - else if( + else if ( event.messageData[0] === 0x7E // non realtime && event.messageData[2] === 0x09 // gm system ) { // thats a GM/2 system change, remove it! - SpessaSynthInfo("%cGM/2 on detected, removing!", consoleColors.info); + SpessaSynthInfo( + "%cGM/2 on detected, removing!", + consoleColors.info + ); track.splice(eventIndex, 1); // adjust program and bank changes eventIndex--; } } } - + }); - if(!addedGs) + if (!addedGs) { // gs is not on, add it on the first track at index 0 (or 1 if track name is first) let index = 0; - if(midi.tracks[0][0].messageStatusByte === messageTypes.trackName) + if (midi.tracks[0][0].messageStatusByte === messageTypes.trackName) + { index++; + } midi.tracks[0].splice(index, 0, getGsOn(0)); SpessaSynthInfo("%cGS on not detected. Adding it.", consoleColors.info); addedGs = true; } } - + // remove all program changes - for(const change of thisProgramChanges) + for (const change of thisProgramChanges) { - midi.tracks[change.track].splice(midi.tracks[change.track].indexOf(change.message), 1); + midi.tracks[change.track].splice( + midi.tracks[change.track].indexOf(change.message), + 1 + ); } /** * Find the first voice message * @type {{index: number, track: number}[]} */ const firstVoiceForTrack = getFirstVoiceForChannel(midiChannel, port, offset > 0); - if(firstVoiceForTrack.length === 0) + if (firstVoiceForTrack.length === 0) { SpessaSynthWarn("Program change but no notes... ignoring!"); return; @@ -394,23 +424,29 @@ export function modifyMIDI( // get the index and ticks let firstIndex = firstVoice.index; const ticks = midi.tracks[firstVoice.track][firstVoice.index].ticks; - + // add drums if needed - if(change.isDrum) + if (change.isDrum) { // do not add gs drum change on drum channel - if(midiSystem === "gs" && midiChannel !== DEFAULT_PERCUSSION) + if (midiSystem === "gs" && midiChannel !== DEFAULT_PERCUSSION) { - SpessaSynthInfo(`%cAdding GS Drum change on track %c${firstVoice.track}`, + SpessaSynthInfo( + `%cAdding GS Drum change on track %c${firstVoice.track}`, consoleColors.recognized, consoleColors.value ); - midi.tracks[firstVoice.track].splice(firstIndex, 0, getDrumChange(change.channel, ticks)); + midi.tracks[firstVoice.track].splice( + firstIndex, + 0, + getDrumChange(change.channel, ticks) + ); firstIndex++; } - else if(midiSystem === "xg") + else if (midiSystem === "xg") { - SpessaSynthInfo(`%cAdding XG Drum change on track %c${firstVoice.track}`, + SpessaSynthInfo( + `%cAdding XG Drum change on track %c${firstVoice.track}`, consoleColors.recognized, consoleColors.value ); @@ -418,20 +454,27 @@ export function modifyMIDI( desiredBank = 127; } } - - SpessaSynthInfo(`%cSetting %c${change.channel}%c to %c${desiredBank}:${desiredProgram}%c. Track num: %c${firstVoice.track}`, + + SpessaSynthInfo( + `%cSetting %c${change.channel}%c to %c${desiredBank}:${desiredProgram}%c. Track num: %c${firstVoice.track}`, consoleColors.info, consoleColors.recognized, consoleColors.info, consoleColors.recognized, consoleColors.info, - consoleColors.recognized); - + consoleColors.recognized + ); + // add bank - const bankChange = getControllerChange(midiChannel, midiControllers.bankSelect, desiredBank, ticks); + const bankChange = getControllerChange( + midiChannel, + midiControllers.bankSelect, + desiredBank, + ticks + ); midi.tracks[firstVoice.track].splice(firstIndex, 0, bankChange); firstIndex++; - + // add program change const programChange = new MidiMessage( ticks, @@ -442,50 +485,62 @@ export function modifyMIDI( ); midi.tracks[firstVoice.track].splice(firstIndex, 0, programChange); }); - + // transpose channels - for(const transpose of desiredChannelsToTranspose) + for (const transpose of desiredChannelsToTranspose) { const midiChannel = transpose.channel % 16; const port = Math.floor(transpose.channel / 16); const keyShift = Math.trunc(transpose.keyShift); const fineTune = transpose.keyShift - keyShift; - SpessaSynthInfo(`%cTransposing channel %c${transpose.channel}%c by %c${transpose.keyShift}%c semitones`, + SpessaSynthInfo( + `%cTransposing channel %c${transpose.channel}%c by %c${transpose.keyShift}%c semitones`, consoleColors.info, consoleColors.recognized, consoleColors.info, consoleColors.value, - consoleColors.info); - if(keyShift !== 0) + consoleColors.info + ); + if (keyShift !== 0) { - midi.tracks.forEach((track, trackNum) => { + midi.tracks.forEach((track, trackNum) => + { if ( midi.midiPorts[trackNum] !== port || !midi.usedChannelsOnTrack[trackNum].has(midiChannel) - ) { + ) + { return; } const onStatus = messageTypes.noteOn | midiChannel; const offStatus = messageTypes.noteOff | midiChannel; const polyStatus = messageTypes.polyPressure | midiChannel; - track.forEach(event => { + track.forEach(event => + { if ( - event.messageStatusByte !== onStatus && + event.messageStatusByte !== onStatus && event.messageStatusByte !== offStatus && event.messageStatusByte !== polyStatus - ) { + ) + { return; } - event.messageData[0] = Math.max(0, Math.min(127, event.messageData[0] + keyShift)); - }) + event.messageData[0] = Math.max( + 0, + Math.min( + 127, + event.messageData[0] + keyShift + ) + ); + }); }); } - - if(fineTune !== 0) + + if (fineTune !== 0) { // find the first track that uses this channel const track = midi.tracks.find((t, tNum) => midi.usedChannelsOnTrack[tNum].has(transpose.channel)); - if(track === undefined) + if (track === undefined) { SpessaSynthWarn(`Channel ${transpose.channel} unused but transpose requested???`); continue; @@ -493,7 +548,7 @@ export function modifyMIDI( // find first noteon for this channel const noteOn = messageTypes.noteOn | (transpose.channel % 16); const noteIndex = track.findIndex(n => n.messageStatusByte === noteOn); - if(noteIndex === -1) + if (noteIndex === -1) { SpessaSynthWarn(`No notes on channel ${transpose.channel} but transpose requested???`); continue; @@ -505,14 +560,22 @@ export function modifyMIDI( const ccChange = messageTypes.controllerChange | (transpose.channel % 16); const rpnCoarse = new MidiMessage(ticks, ccChange, new IndexedByteArray([midiControllers.RPNMsb, 0])); const rpnFine = new MidiMessage(ticks, ccChange, new IndexedByteArray([midiControllers.RPNLsb, 1])); - const deCoarse = new MidiMessage(ticks, ccChange, new IndexedByteArray([midiControllers.dataEntryMsb, centsCoarse])); - const deFine = new MidiMessage(ticks, ccChange, new IndexedByteArray([midiControllers.lsbForControl6DataEntry, 0])); + const deCoarse = new MidiMessage( + ticks, + ccChange, + new IndexedByteArray([midiControllers.dataEntryMsb, centsCoarse]) + ); + const deFine = new MidiMessage( + ticks, + ccChange, + new IndexedByteArray([midiControllers.lsbForControl6DataEntry, 0]) + ); // add in reverse track.splice(noteIndex, 0, deFine); track.splice(noteIndex, 0, deCoarse); track.splice(noteIndex, 0, rpnFine); track.splice(noteIndex, 0, rpnCoarse); - + } } SpessaSynthGroupEnd(); @@ -554,21 +617,22 @@ export function applySnapshotToMIDI(midi, snapshot) * }[]} */ const controllerChanges = []; - snapshot.channelSnapshots.forEach((channel, channelNumber) => { - if(channel.isMuted) + snapshot.channelSnapshots.forEach((channel, channelNumber) => + { + if (channel.isMuted) { channelsToClear.push(channelNumber); return; } const transposeFloat = channel.channelTransposeKeyShift + channel.customControllers[customControllers.channelTransposeFine] / 100; - if(transposeFloat !== 0) + if (transposeFloat !== 0) { channelsToTranspose.push({ channel: channelNumber, - keyShift: transposeFloat, + keyShift: transposeFloat }); } - if(channel.lockPreset) + if (channel.lockPreset) { programChanges.push({ channel: channelNumber, @@ -578,8 +642,9 @@ export function applySnapshotToMIDI(midi, snapshot) }); } // check for locked controllers and change them appropriately - channel.lockedControllers.forEach((l, ccNumber) => { - if(!l || ccNumber > 127 || ccNumber === midiControllers.bankSelect) + channel.lockedControllers.forEach((l, ccNumber) => + { + if (!l || ccNumber > 127 || ccNumber === midiControllers.bankSelect) { return; } diff --git a/src/spessasynth_lib/midi_parser/midi_loader.js b/src/spessasynth_lib/midi_parser/midi_loader.js index e2cb730c..9be675b8 100644 --- a/src/spessasynth_lib/midi_parser/midi_loader.js +++ b/src/spessasynth_lib/midi_parser/midi_loader.js @@ -1,17 +1,18 @@ -import { dataBytesAmount, getChannel, messageTypes, MidiMessage } from './midi_message.js' -import { IndexedByteArray } from '../utils/indexed_array.js' -import { consoleColors, formatTitle } from '../utils/other.js' -import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn } from '../utils/loggin.js' -import { readRIFFChunk } from '../soundfont/basic_soundfont/riff_chunk.js' -import { readVariableLengthQuantity } from '../utils/byte_functions/variable_length_quantity.js' -import { readBytesAsUintBigEndian } from '../utils/byte_functions/big_endian.js' -import { readBytesAsString } from '../utils/byte_functions/string.js' -import { readLittleEndian } from '../utils/byte_functions/little_endian.js' -import { RMIDINFOChunks } from './rmidi_writer.js' -import { BasicMIDI, MIDIticksToSeconds } from './basic_midi.js' +import { dataBytesAmount, getChannel, messageTypes, MidiMessage } from "./midi_message.js"; +import { IndexedByteArray } from "../utils/indexed_array.js"; +import { consoleColors, formatTitle } from "../utils/other.js"; +import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn } from "../utils/loggin.js"; +import { readRIFFChunk } from "../soundfont/basic_soundfont/riff_chunk.js"; +import { readVariableLengthQuantity } from "../utils/byte_functions/variable_length_quantity.js"; +import { readBytesAsUintBigEndian } from "../utils/byte_functions/big_endian.js"; +import { readBytesAsString } from "../utils/byte_functions/string.js"; +import { readLittleEndian } from "../utils/byte_functions/little_endian.js"; +import { RMIDINFOChunks } from "./rmidi_writer.js"; +import { BasicMIDI, MIDIticksToSeconds } from "./basic_midi.js"; -const GS_TEXT_HEADER = new Uint8Array([0x41, 0x10, 0x45, 0x12, 0x10, 0x00, 0x00]); +const GS_TEXT_HEADER = new Uint8Array([0x41, 0x10, 0x45, 0x12, 0x10, 0x00, 0x00]); + /** * midi_loader.js * purpose: parses a midi file for the seqyencer, including things like marker or CC 2/4 loop detection, copyright detection etc. @@ -23,51 +24,51 @@ class MIDI extends BasicMIDI * @param arrayBuffer {ArrayBuffer} * @param fileName {string} optional, replaces the decoded title if empty */ - constructor(arrayBuffer, fileName="") + constructor(arrayBuffer, fileName = "") { super(); SpessaSynthGroupCollapsed(`%cParsing MIDI File...`, consoleColors.info); const binaryData = new IndexedByteArray(arrayBuffer); let fileByteArray; - + // check for rmid let copyrightDetected = false; - + let nameDetected = false; - + let DLSRMID = false; - + const initialString = readBytesAsString(binaryData, 4); binaryData.currentIndex -= 4; - if(initialString === "RIFF") + if (initialString === "RIFF") { // possibly an RMID file (https://github.com/spessasus/sf2-rmidi-specification#readme) // skip size binaryData.currentIndex += 8; const rmid = readBytesAsString(binaryData, 4, undefined, false); - if(rmid !== "RMID") + if (rmid !== "RMID") { SpessaSynthGroupEnd(); throw new SyntaxError(`Invalid RMIDI Header! Expected "RMID", got "${rmid}"`); } const riff = readRIFFChunk(binaryData); - if(riff.header !== 'data') + if (riff.header !== "data") { SpessaSynthGroupEnd(); throw new SyntaxError(`Invalid RMIDI Chunk header! Expected "data", got "${rmid}"`); } // this is an rmid, load the midi into array for parsing fileByteArray = riff.chunkData; - + // keep loading chunks until we get sfbk - while(binaryData.currentIndex <= binaryData.length) + while (binaryData.currentIndex <= binaryData.length) { const startIndex = binaryData.currentIndex; const currentChunk = readRIFFChunk(binaryData, true); - if(currentChunk.header === "RIFF") + if (currentChunk.header === "RIFF") { const type = readBytesAsString(currentChunk.chunkData, 4).toLowerCase(); - if(type === "sfbk" || type === "sfpk" || type === "dls ") + if (type === "sfbk" || type === "sfpk" || type === "dls ") { SpessaSynthInfo("%cFound embedded soundfont!", consoleColors.recognized); this.embeddedSoundFont = binaryData.slice(startIndex, startIndex + currentChunk.size).buffer; @@ -76,54 +77,64 @@ class MIDI extends BasicMIDI { SpessaSynthWarn(`Unknown RIFF chunk: "${type}"`); } - if(type === "dls ") + if (type === "dls ") { // assume bank offset of 0 by default. If we find any bank selects, then the offset is 1. DLSRMID = true; } } - else if(currentChunk.header === "LIST") + else if (currentChunk.header === "LIST") { const type = readBytesAsString(currentChunk.chunkData, 4); - if(type === "INFO") + if (type === "INFO") { SpessaSynthInfo("%cFound RMIDI INFO chunk!", consoleColors.recognized); this.RMIDInfo = {}; - while(currentChunk.chunkData.currentIndex <= currentChunk.size) + while (currentChunk.chunkData.currentIndex <= currentChunk.size) { const infoChunk = readRIFFChunk(currentChunk.chunkData, true); this.RMIDInfo[infoChunk.header] = infoChunk.chunkData; } - if(this.RMIDInfo['ICOP']) + if (this.RMIDInfo["ICOP"]) { copyrightDetected = true; - this.copyright = readBytesAsString(this.RMIDInfo['ICOP'], this.RMIDInfo['ICOP'].length, undefined, false).replaceAll("\n", " "); + this.copyright = readBytesAsString( + this.RMIDInfo["ICOP"], + this.RMIDInfo["ICOP"].length, + undefined, + false + ).replaceAll("\n", " "); } - if(this.RMIDInfo['INAM']) + if (this.RMIDInfo["INAM"]) { this.rawMidiName = this.RMIDInfo[RMIDINFOChunks.name]; - this.midiName = readBytesAsString(this.rawMidiName, this.rawMidiName.length, undefined, false).replaceAll("\n", " "); + this.midiName = readBytesAsString( + this.rawMidiName, + this.rawMidiName.length, + undefined, + false + ).replaceAll("\n", " "); nameDetected = true; } // these can be used interchangeably - if(this.RMIDInfo['IALB'] && !this.RMIDInfo['IPRD']) + if (this.RMIDInfo["IALB"] && !this.RMIDInfo["IPRD"]) { - this.RMIDInfo['IPRD'] = this.RMIDInfo['IALB']; + this.RMIDInfo["IPRD"] = this.RMIDInfo["IALB"]; } - if(this.RMIDInfo['PRD'] && !this.RMIDInfo['IALB']) + if (this.RMIDInfo["PRD"] && !this.RMIDInfo["IALB"]) { - this.RMIDInfo['IALB'] = this.RMIDInfo['IPRD']; + this.RMIDInfo["IALB"] = this.RMIDInfo["IPRD"]; } this.bankOffset = 1; // defaults to 1 - if(this.RMIDInfo[RMIDINFOChunks.bankOffset]) + if (this.RMIDInfo[RMIDINFOChunks.bankOffset]) { this.bankOffset = readLittleEndian(this.RMIDInfo[RMIDINFOChunks.bankOffset], 2); } } } } - - if(DLSRMID) + + if (DLSRMID) { // assume bank offset of 0 by default. If we find any bank selects, then the offset is 1. this.bankOffset = 0; @@ -134,37 +145,37 @@ class MIDI extends BasicMIDI fileByteArray = binaryData; } const headerChunk = this.readMIDIChunk(fileByteArray); - if(headerChunk.type !== "MThd") + if (headerChunk.type !== "MThd") { SpessaSynthGroupEnd(); throw new SyntaxError(`Invalid MIDI Header! Expected "MThd", got "${headerChunk.type}"`); } - - if(headerChunk.size !== 6) + + if (headerChunk.size !== 6) { SpessaSynthGroupEnd(); throw new RangeError(`Invalid MIDI header chunk size! Expected 6, got ${headerChunk.size}`); } - + // format this.format = readBytesAsUintBigEndian(headerChunk.data, 2); // tracks count this.tracksAmount = readBytesAsUintBigEndian(headerChunk.data, 2); // time division this.timeDivision = readBytesAsUintBigEndian(headerChunk.data, 2); - + /** * The MIDI's key range * @type {{min: number, max: number}} */ - this.keyRange = {min: 127, max: 0}; - + this.keyRange = { min: 127, max: 0 }; + /** * Contains the lyrics as binary chunks * @type {Uint8Array[]} */ this.lyrics = []; - + /** * Contains all the tempo changes in the file. (Ordered from last to first) * @type {{ @@ -172,38 +183,38 @@ class MIDI extends BasicMIDI * tempo: number * }[]} */ - this.tempoChanges = [{ticks: 0, tempo: 120}]; - + this.tempoChanges = [{ ticks: 0, tempo: 120 }]; + let loopStart = null; let loopEnd = null; - + this.lastVoiceEventTick = 0; - + /** * Midi port numbers for each tracks * @type {number[]} */ this.midiPorts = []; - - let portOffset = 0 + + let portOffset = 0; /** * Channel offsets for each port, using the SpessaSynth method * @type {number[]} */ this.midiPortChannelOffsets = []; - + /** * All channels that each track uses. Note: these channels range from 0 to 15, excluding the port offsets! * @type {Set[]} */ this.usedChannelsOnTrack = []; - + /** * Read all the tracks * @type {MidiMessage[][]} */ this.tracks = []; - for(let i = 0; i < this.tracksAmount; i++) + for (let i = 0; i < this.tracksAmount; i++) { /** * @type {MidiMessage[]} @@ -212,40 +223,40 @@ class MIDI extends BasicMIDI const trackChunk = this.readMIDIChunk(fileByteArray); const usedChannels = new Set(); this.midiPorts.push(-1); - - if(trackChunk.type !== "MTrk") + + if (trackChunk.type !== "MTrk") { SpessaSynthGroupEnd(); throw new SyntaxError(`Invalid track header! Expected "MTrk" got "${trackChunk.type}"`); } - + /** * MIDI running byte * @type {number} */ let runningByte = undefined; - + let totalTicks = 0; // format 2 plays sequentially - if(this.format === 2 && i > 0) + if (this.format === 2 && i > 0) { totalTicks += this.tracks[i - 1][this.tracks[i - 1].length - 1].ticks; } // loop until we reach the end of track - while(trackChunk.data.currentIndex < trackChunk.size) + while (trackChunk.data.currentIndex < trackChunk.size) { totalTicks += readVariableLengthQuantity(trackChunk.data); - + // check if the status byte is valid (IE. larger than 127) const statusByteCheck = trackChunk.data[trackChunk.data.currentIndex]; - + let statusByte; // if we have a running byte and the status byte isn't valid - if(runningByte !== undefined && statusByteCheck < 0x80) + if (runningByte !== undefined && statusByteCheck < 0x80) { statusByte = runningByte; } - else if(!runningByte && statusByteCheck < 0x80) + else if (!runningByte && statusByteCheck < 0x80) { // if we don't have a running byte and the status byte isn't valid, it's an error. SpessaSynthGroupEnd(); @@ -257,63 +268,66 @@ class MIDI extends BasicMIDI statusByte = trackChunk.data[trackChunk.data.currentIndex++]; } const statusByteChannel = getChannel(statusByte); - + let eventDataLength; - + // determine the message's length; - switch(statusByteChannel) + switch (statusByteChannel) { case -1: // system common/realtime (no length) eventDataLength = 0; break; - + case -2: // meta (the next is the actual status byte) statusByte = trackChunk.data[trackChunk.data.currentIndex++]; eventDataLength = readVariableLengthQuantity(trackChunk.data); break; - + case -3: // sysex eventDataLength = readVariableLengthQuantity(trackChunk.data); break; - + default: // voice message // get the midi message length - if(totalTicks > this.lastVoiceEventTick) + if (totalTicks > this.lastVoiceEventTick) { this.lastVoiceEventTick = totalTicks; } eventDataLength = dataBytesAmount[statusByte >> 4]; - if((statusByte & 0xF0) === messageTypes.noteOn) + if ((statusByte & 0xF0) === messageTypes.noteOn) { usedChannels.add(statusByteChannel); - const note = trackChunk.data[trackChunk.data.currentIndex] + const note = trackChunk.data[trackChunk.data.currentIndex]; this.keyRange.min = Math.min(this.keyRange.min, note); this.keyRange.max = Math.max(this.keyRange.max, note); } - + // save the status byte runningByte = statusByte; break; } - + // put the event data into the array const eventData = new IndexedByteArray(eventDataLength); - const messageData = trackChunk.data.slice(trackChunk.data.currentIndex, trackChunk.data.currentIndex + eventDataLength); + const messageData = trackChunk.data.slice( + trackChunk.data.currentIndex, + trackChunk.data.currentIndex + eventDataLength + ); trackChunk.data.currentIndex += eventDataLength; eventData.set(messageData, 0); - + const message = new MidiMessage(totalTicks, statusByte, eventData); track.push(message); - - switch(statusByteChannel) + + switch (statusByteChannel) { case -2: // since this is a meta message - switch(statusByte) + switch (statusByte) { case messageTypes.setTempo: // add the tempo change @@ -322,7 +336,7 @@ class MIDI extends BasicMIDI tempo: 60000000 / readBytesAsUintBigEndian(messageData, 3) }); break; - + case messageTypes.marker: // check for loop markers const text = readBytesAsString(eventData, eventData.length).trim().toLowerCase(); @@ -330,45 +344,50 @@ class MIDI extends BasicMIDI { default: break; - + case "start": case "loopstart": loopStart = totalTicks; break; - + case "loopend": loopEnd = totalTicks; } eventData.currentIndex = 0; break; - + case messageTypes.midiPort: const port = eventData[0]; this.midiPorts[i] = port; - if(this.midiPortChannelOffsets[port] === undefined) + if (this.midiPortChannelOffsets[port] === undefined) { this.midiPortChannelOffsets[port] = portOffset; portOffset += 16; } break; - + case messageTypes.copyright: - if(!copyrightDetected) + if (!copyrightDetected) { - this.copyright += readBytesAsString(eventData, eventData.length, undefined, false) + "\n"; + this.copyright += readBytesAsString( + eventData, + eventData.length, + undefined, + false + ) + "\n"; } break; - + case messageTypes.lyric: this.lyrics.push(eventData); } break; - + case -3: // since this is a sysex message // check for embedded copyright (roland SC display sysex) http://www.bandtrax.com.au/sysex.htm // header goes like this: 41 10 45 12 10 00 00 - if(eventData.slice(0, 7).every((n, i) => GS_TEXT_HEADER[i] === n)) + if (eventData.slice(0, 7).every((n, i) => GS_TEXT_HEADER[i] === n)) { /** * @type {IndexedByteArray} @@ -376,28 +395,30 @@ class MIDI extends BasicMIDI const cutText = eventData.slice(7, messageData.length - 3); const decoded = readBytesAsString(cutText, cutText.length) + "\n"; this.copyright += decoded; - SpessaSynthInfo(`%cDecoded Roland SC message! %c${decoded}`, + SpessaSynthInfo( + `%cDecoded Roland SC message! %c${decoded}`, consoleColors.recognized, - consoleColors.value) + consoleColors.value + ); } break; - - + + default: // since this is a voice message // check for loop (CC 2/4) - if((statusByte & 0xF0) === messageTypes.controllerChange) + if ((statusByte & 0xF0) === messageTypes.controllerChange) { - switch(eventData[0]) + switch (eventData[0]) { case 2: case 116: loopStart = totalTicks; break; - + case 4: case 117: - if(loopEnd === null) + if (loopEnd === null) { loopEnd = totalTicks; } @@ -407,13 +428,15 @@ class MIDI extends BasicMIDI loopEnd = 0; } break; - + case 0: // check RMID - if(DLSRMID && eventData[1] !== 0 && eventData[1] !== 127) + if (DLSRMID && eventData[1] !== 0 && eventData[1] !== 127) { - SpessaSynthInfo("%cDLS RMIDI with offset 1 detected!", - consoleColors.recognized); + SpessaSynthInfo( + "%cDLS RMIDI with offset 1 detected!", + consoleColors.recognized + ); this.bankOffset = 1; } } @@ -422,38 +445,45 @@ class MIDI extends BasicMIDI } this.tracks.push(track); this.usedChannelsOnTrack.push(usedChannels); - SpessaSynthInfo(`%cParsed %c${this.tracks.length}%c / %c${this.tracksAmount}`, + SpessaSynthInfo( + `%cParsed %c${this.tracks.length}%c / %c${this.tracksAmount}`, consoleColors.info, consoleColors.value, consoleColors.info, - consoleColors.value); + consoleColors.value + ); } - - SpessaSynthInfo(`%cAll tracks parsed correctly!`, - consoleColors.recognized); - - SpessaSynthGroupCollapsed(`%cCorrecting loops, ports and detecting notes...`, - consoleColors.info) - + + SpessaSynthInfo( + `%cAll tracks parsed correctly!`, + consoleColors.recognized + ); + + SpessaSynthGroupCollapsed( + `%cCorrecting loops, ports and detecting notes...`, + consoleColors.info + ); + const firstNoteOns = []; - for(const t of this.tracks) + for (const t of this.tracks) { const firstNoteOn = t.find(e => (e.messageStatusByte & 0xF0) === messageTypes.noteOn); - if(firstNoteOn) + if (firstNoteOn) { firstNoteOns.push(firstNoteOn.ticks); } } this.firstNoteOn = Math.min(...firstNoteOns); - - SpessaSynthInfo(`%cFirst note-on detected at: %c${this.firstNoteOn}%c ticks!`, + + SpessaSynthInfo( + `%cFirst note-on detected at: %c${this.firstNoteOn}%c ticks!`, consoleColors.info, consoleColors.recognized, - consoleColors.info); - - + consoleColors.info + ); + - if(loopStart !== null && loopEnd === null) + if (loopStart !== null && loopEnd === null) { // not a loop loopStart = this.firstNoteOn; @@ -465,33 +495,36 @@ class MIDI extends BasicMIDI { loopStart = this.firstNoteOn; } - - if (loopEnd === null || loopEnd === 0) { + + if (loopEnd === null || loopEnd === 0) + { loopEnd = this.lastVoiceEventTick; } } - + /** * * @type {{start: number, end: number}} */ - this.loop = {start: loopStart, end: loopEnd}; - - SpessaSynthInfo(`%cLoop points: start: %c${this.loop.start}%c end: %c${this.loop.end}`, + this.loop = { start: loopStart, end: loopEnd }; + + SpessaSynthInfo( + `%cLoop points: start: %c${this.loop.start}%c end: %c${this.loop.end}`, consoleColors.info, consoleColors.recognized, consoleColors.info, - consoleColors.recognized); - + consoleColors.recognized + ); + // fix midi ports: // midi tracks without ports will have a value of -1 // if all ports have a value of -1, set it to 0, otherwise take the first midi port and replace all -1 with it // why do this? some midis (for some reason) specify all channels to port 1 or else, but leave the conductor track with no port pref. // this spessasynth to reserve the first 16 channels for the conductor track (which doesn't play anything) and use additional 16 for the actual ports. let defaultPort = 0; - for(let port of this.midiPorts) + for (let port of this.midiPorts) { - if(port !== -1) + if (port !== -1) { defaultPort = port; break; @@ -499,11 +532,11 @@ class MIDI extends BasicMIDI } this.midiPorts = this.midiPorts.map(port => port === -1 ? defaultPort : port); // add dummy port if empty - if(this.midiPortChannelOffsets.length === 0) + if (this.midiPortChannelOffsets.length === 0) { this.midiPortChannelOffsets = [0]; } - if(this.midiPortChannelOffsets.length < 2) + if (this.midiPortChannelOffsets.length < 2) { SpessaSynthInfo(`%cNo additional MIDI Ports detected.`, consoleColors.info); } @@ -511,22 +544,22 @@ class MIDI extends BasicMIDI { SpessaSynthInfo(`%cMIDI Ports detected!`, consoleColors.recognized); } - + // midi name - if(!nameDetected) + if (!nameDetected) { if (this.tracks.length > 1) { // if more than 1 track and the first track has no notes, just find the first trackName in the first track if ( this.tracks[0].find( - message => message.messageStatusByte >= messageTypes.noteOn - && - message.messageStatusByte < messageTypes.polyPressure + message => message.messageStatusByte >= messageTypes.noteOn + && + message.messageStatusByte < messageTypes.polyPressure ) === undefined ) { - + let name = this.tracks[0].find(message => message.messageStatusByte === messageTypes.trackName); if (name) { @@ -546,46 +579,52 @@ class MIDI extends BasicMIDI } } } - + this.fileName = fileName; this.midiName = this.midiName.trim(); // if midiName is "", use the file name - if(this.midiName.length === 0) + if (this.midiName.length === 0) { - SpessaSynthInfo(`%cNo name detected. Using the alt name!`, - consoleColors.info); + SpessaSynthInfo( + `%cNo name detected. Using the alt name!`, + consoleColors.info + ); this.midiName = formatTitle(fileName); // encode it too this.rawMidiName = new Uint8Array(this.midiName.length); - for(let i = 0; i < this.midiName.length; i++) + for (let i = 0; i < this.midiName.length; i++) { this.rawMidiName[i] = this.midiName.charCodeAt(i); } } else { - SpessaSynthInfo(`%cMIDI Name detected! %c"${this.midiName}"`, + SpessaSynthInfo( + `%cMIDI Name detected! %c"${this.midiName}"`, consoleColors.info, - consoleColors.recognized) + consoleColors.recognized + ); } // reverse the tempo changes this.tempoChanges.reverse(); - + /** * The total playback time, in seconds * @type {number} */ this.duration = MIDIticksToSeconds(this.lastVoiceEventTick, this); - + SpessaSynthGroupEnd(); - SpessaSynthInfo(`%cMIDI file parsed. Total tick time: %c${this.lastVoiceEventTick}%c, total seconds time: %c${this.duration}`, + SpessaSynthInfo( + `%cMIDI file parsed. Total tick time: %c${this.lastVoiceEventTick}%c, total seconds time: %c${this.duration}`, consoleColors.info, consoleColors.recognized, consoleColors.info, - consoleColors.recognized); + consoleColors.recognized + ); SpessaSynthGroupEnd(); } - + /** * @param fileByteArray {IndexedByteArray} * @returns {{type: string, size: number, data: IndexedByteArray}} @@ -605,4 +644,5 @@ class MIDI extends BasicMIDI return chunk; } } -export { MIDI } \ No newline at end of file + +export { MIDI }; \ No newline at end of file diff --git a/src/spessasynth_lib/midi_parser/midi_message.js b/src/spessasynth_lib/midi_parser/midi_message.js index d6b52841..32ad76b1 100644 --- a/src/spessasynth_lib/midi_parser/midi_message.js +++ b/src/spessasynth_lib/midi_parser/midi_message.js @@ -1,4 +1,4 @@ -import {IndexedByteArray} from "../utils/indexed_array.js"; +import { IndexedByteArray } from "../utils/indexed_array.js"; /** * midi_message.js @@ -12,7 +12,8 @@ export class MidiMessage * @param byte {number} the message status byte * @param data {IndexedByteArray} */ - constructor(ticks, byte, data) { + constructor(ticks, byte, data) + { // absolute ticks from the start this.ticks = ticks; // message status byte (for meta it's the second byte) @@ -29,13 +30,15 @@ export class MidiMessage * @param statusByte * @returns {number} channel is -1 for system messages -2 for meta and -3 for sysex */ -export function getChannel(statusByte) { +export function getChannel(statusByte) +{ const eventType = statusByte & 0xF0; const channel = statusByte & 0x0F; - + let resultChannel = channel; - - switch (eventType) { + + switch (eventType) + { // midi (and meta and sysex headers) case 0x80: case 0x90: @@ -45,13 +48,14 @@ export function getChannel(statusByte) { case 0xD0: case 0xE0: break; - + case 0xF0: - switch (channel) { + switch (channel) + { case 0x0: resultChannel = -3; break; - + case 0x1: case 0x2: case 0x3: @@ -68,17 +72,17 @@ export function getChannel(statusByte) { case 0xE: resultChannel = -1; break; - + case 0xF: resultChannel = -2; break; } break; - + default: resultChannel = -1; } - + return resultChannel; } @@ -127,18 +131,20 @@ export const messageTypes = { * @param statusByte {number} the status byte * @returns {{channel: number, status: number}} channel will be -1 for sysex and meta */ -export function getEvent(statusByte) { +export function getEvent(statusByte) +{ const status = statusByte & 0xF0; const channel = statusByte & 0x0F; - + let eventChannel = -1; let eventStatus = statusByte; - - if (status >= 0x80 && status <= 0xE0) { + + if (status >= 0x80 && status <= 0xE0) + { eventChannel = channel; eventStatus = status; } - + return { status: eventStatus, channel: eventChannel diff --git a/src/spessasynth_lib/midi_parser/midi_writer.js b/src/spessasynth_lib/midi_parser/midi_writer.js index 79c3b7f4..16261818 100644 --- a/src/spessasynth_lib/midi_parser/midi_writer.js +++ b/src/spessasynth_lib/midi_parser/midi_writer.js @@ -1,6 +1,6 @@ -import { messageTypes } from './midi_message.js' -import { writeVariableLengthQuantity } from '../utils/byte_functions/variable_length_quantity.js' -import { writeBytesAsUintBigEndian } from '../utils/byte_functions/big_endian.js' +import { messageTypes } from "./midi_message.js"; +import { writeVariableLengthQuantity } from "../utils/byte_functions/variable_length_quantity.js"; +import { writeBytesAsUintBigEndian } from "../utils/byte_functions/big_endian.js"; /** * Exports the midi as a .mid file @@ -13,12 +13,12 @@ export function writeMIDIFile(midi) * @type {Uint8Array[]} */ const binaryTrackData = []; - for(const track of midi.tracks) + for (const track of midi.tracks) { const binaryTrack = []; let currentTick = 0; let runningByte = undefined; - for(const event of track) + for (const event of track) { // ticks stored in MIDI are absolute, but .mid wants relative. Convert them here. const deltaTicks = event.ticks - currentTick; @@ -27,13 +27,13 @@ export function writeMIDIFile(midi) */ let messageData; // determine the message - if(event.messageStatusByte <= messageTypes.keySignature || event.messageStatusByte === messageTypes.sequenceSpecific) + if (event.messageStatusByte <= messageTypes.keySignature || event.messageStatusByte === messageTypes.sequenceSpecific) { // this is a meta message // syntax is FF messageData = [0xff, event.messageStatusByte, ...writeVariableLengthQuantity(event.messageData.length), ...event.messageData]; } - else if(event.messageStatusByte === messageTypes.systemExclusive) + else if (event.messageStatusByte === messageTypes.systemExclusive) { // this is a system exclusive message // syntax is F0 @@ -43,7 +43,7 @@ export function writeMIDIFile(midi) { // this is a midi message messageData = []; - if(runningByte !== event.messageStatusByte) + if (runningByte !== event.messageStatusByte) { // running byte was not the byte we want. Add the byte here. runningByte = event.messageStatusByte; @@ -61,19 +61,19 @@ export function writeMIDIFile(midi) } binaryTrackData.push(new Uint8Array(binaryTrack)); } - + /** * @param text {string} * @param arr {number[]} */ function writeText(text, arr) { - for(let i = 0; i < text.length; i++) + for (let i = 0; i < text.length; i++) { arr.push(text.charCodeAt(i)); } } - + // write the file const binaryData = []; // write header @@ -82,9 +82,9 @@ export function writeMIDIFile(midi) binaryData.push(0, midi.format); // format binaryData.push(...writeBytesAsUintBigEndian(midi.tracksAmount, 2)); // num tracks binaryData.push(...writeBytesAsUintBigEndian(midi.timeDivision, 2)); // time division - + // write tracks - for(const track of binaryTrackData) + for (const track of binaryTrackData) { // write track header writeText("MTrk", binaryData); // MTrk diff --git a/src/spessasynth_lib/midi_parser/rmidi_writer.js b/src/spessasynth_lib/midi_parser/rmidi_writer.js index 2c5410e5..a247c636 100644 --- a/src/spessasynth_lib/midi_parser/rmidi_writer.js +++ b/src/spessasynth_lib/midi_parser/rmidi_writer.js @@ -1,13 +1,14 @@ -import { combineArrays, IndexedByteArray } from '../utils/indexed_array.js' -import { writeMIDIFile } from './midi_writer.js' -import { writeRIFFOddSize } from '../soundfont/basic_soundfont/riff_chunk.js' -import { getStringBytes } from '../utils/byte_functions/string.js' -import { messageTypes, midiControllers, MidiMessage } from './midi_message.js' -import { DEFAULT_PERCUSSION } from '../synthetizer/synthetizer.js' -import { getGsOn } from './midi_editor.js' -import { SpessaSynthGroup, SpessaSynthGroupEnd, SpessaSynthInfo } from '../utils/loggin.js' -import { consoleColors } from '../utils/other.js' -import { writeLittleEndian } from '../utils/byte_functions/little_endian.js' +import { combineArrays, IndexedByteArray } from "../utils/indexed_array.js"; +import { writeMIDIFile } from "./midi_writer.js"; +import { writeRIFFOddSize } from "../soundfont/basic_soundfont/riff_chunk.js"; +import { getStringBytes } from "../utils/byte_functions/string.js"; +import { messageTypes, midiControllers, MidiMessage } from "./midi_message.js"; +import { DEFAULT_PERCUSSION } from "../synthetizer/synthetizer.js"; +import { getGsOn } from "./midi_editor.js"; +import { SpessaSynthGroup, SpessaSynthGroupEnd, SpessaSynthInfo } from "../utils/loggin.js"; +import { consoleColors } from "../utils/other.js"; +import { writeLittleEndian } from "../utils/byte_functions/little_endian.js"; + /** * @enum {string} */ @@ -25,7 +26,7 @@ export const RMIDINFOChunks = { encoding: "IENC", midiEncoding: "MENC", bankOffset: "DBNK" -} +}; const FORCED_ENCODING = "utf-8"; const DEFAULT_COPYRIGHT = "Created using SpessaSynth"; @@ -66,14 +67,16 @@ export function writeRMIDI( ) { SpessaSynthGroup("%cWriting the RMIDI File...", consoleColors.info); - SpessaSynthInfo(`%cConfiguration: Bank offset: %c${bankOffset}%c, encoding: %c${encoding}`, + SpessaSynthInfo( + `%cConfiguration: Bank offset: %c${bankOffset}%c, encoding: %c${encoding}`, consoleColors.info, consoleColors.value, consoleColors.info, - consoleColors.value); + consoleColors.value + ); SpessaSynthInfo("metadata", metadata); SpessaSynthInfo("Initial bank offset", mid.bankOffset); - if(correctBankOffset) + if (correctBankOffset) { // add offset to bank. // See https://github.com/spessasus/sf2-rmidi-specification#readme @@ -91,22 +94,26 @@ export function writeRMIDI( */ const eventIndexes = Array(mid.tracks.length).fill(0); let remainingTracks = mid.tracks.length; - - function findFirstEventIndex() { + + function findFirstEventIndex() + { let index = 0; let ticks = Infinity; - mid.tracks.forEach((track, i) => { - if (eventIndexes[i] >= track.length) { + mid.tracks.forEach((track, i) => + { + if (eventIndexes[i] >= track.length) + { return; } - if (track[eventIndexes[i]].ticks < ticks) { + if (track[eventIndexes[i]].ticks < ticks) + { index = i; ticks = track[eventIndexes[i]].ticks; } }); return index; } - + // it copies midiPorts everywhere else, but here 0 works so DO NOT CHANGE!!!!!!! const ports = Array(mid.tracks.length).fill(0); const channelsAmount = 16 + mid.midiPortChannelOffsets.reduce((max, cur) => cur > max ? cur : max); @@ -119,26 +126,30 @@ export function writeRMIDI( * }[]} */ const channelsInfo = []; - for (let i = 0; i < channelsAmount; i++) { + for (let i = 0; i < channelsAmount; i++) + { channelsInfo.push({ program: 0, drums: i % 16 === DEFAULT_PERCUSSION, // drums appear on 9 every 16 channels, lastBank: undefined, - hasBankSelect: false, + hasBankSelect: false }); } - while (remainingTracks > 0) { + while (remainingTracks > 0) + { let trackNum = findFirstEventIndex(); const track = mid.tracks[trackNum]; - if (eventIndexes[trackNum] >= track.length) { + if (eventIndexes[trackNum] >= track.length) + { remainingTracks--; continue; } const e = track[eventIndexes[trackNum]]; eventIndexes[trackNum]++; - + let portOffset = mid.midiPortChannelOffsets[ports[trackNum]]; - if (e.messageStatusByte === messageTypes.midiPort) { + if (e.messageStatusByte === messageTypes.midiPort) + { ports[trackNum] = e.messageData[0]; continue; } @@ -147,11 +158,13 @@ export function writeRMIDI( status !== messageTypes.controllerChange && status !== messageTypes.programChange && status !== messageTypes.systemExclusive - ) { - continue + ) + { + continue; } - - if (status === messageTypes.systemExclusive) { + + if (status === messageTypes.systemExclusive) + { // check for drum sysex if ( e.messageData[0] !== 0x41 || // roland @@ -160,25 +173,31 @@ export function writeRMIDI( e.messageData[4] !== 0x40 || // system parameter (e.messageData[5] & 0x10) === 0 || // part parameter e.messageData[6] !== 0x15 // drum part - ) { + ) + { // check for XG if ( e.messageData[0] === 0x43 && // yamaha e.messageData[2] === 0x4C && // sXG ON e.messageData[5] === 0x7E && e.messageData[6] === 0x00 - ) { + ) + { system = "xg"; - } else if ( + } + else if ( e.messageData[0] === 0x41 // roland && e.messageData[2] === 0x42 // GS && e.messageData[6] === 0x7F // Mode set - ) { + ) + { system = "gs"; - } else if ( + } + else if ( e.messageData[0] === 0x7E // non realtime && e.messageData[2] === 0x09 // gm system - ) { + ) + { system = "gm"; unwantedSystems.push({ tNum: trackNum, @@ -191,19 +210,25 @@ export function writeRMIDI( channelsInfo[sysexChannel].drums = !!(e.messageData[7] > 0 && e.messageData[5] >> 4); continue; } - + // program change const chNum = (e.messageStatusByte & 0xF) + portOffset; const channel = channelsInfo[chNum]; - if (status === messageTypes.programChange) { + if (status === messageTypes.programChange) + { // check if the preset for this program exists - if (channel.drums) { - if (soundfont.presets.findIndex(p => p.program === e.messageData[0] && p.bank === 128) === -1) { + if (channel.drums) + { + if (soundfont.presets.findIndex(p => p.program === e.messageData[0] && p.bank === 128) === -1) + { // doesn't exist. pick any preset that has the 128 bank. e.messageData[0] = soundfont.presets.find(p => p.bank === 128)?.program || 0; } - } else { - if (soundfont.presets.findIndex(p => p.program === e.messageData[0] && p.bank !== 128) === -1) { + } + else + { + if (soundfont.presets.findIndex(p => p.program === e.messageData[0] && p.bank !== 128) === -1) + { // doesn't exist. pick any preset that does not have the 128 bank. e.messageData[0] = soundfont.presets.find(p => p.bank !== 128)?.program || 0; } @@ -212,51 +237,64 @@ export function writeRMIDI( // check if this preset exists for program and bank const realBank = Math.max(0, channel.lastBank?.messageData[1] - mid.bankOffset); // make sure to take the previous bank offset into account const bank = channel.drums ? 128 : realBank; - if (channel.lastBank === undefined) { + if (channel.lastBank === undefined) + { continue; } - if (system === "xg" && channel.drums) { + if (system === "xg" && channel.drums) + { // drums override: set bank to 127 channelsInfo[chNum].lastBank.messageData[1] = 127; } - - if (soundfont.presets.findIndex(p => p.bank === bank && p.program === e.messageData[0]) === -1) { + + if (soundfont.presets.findIndex(p => p.bank === bank && p.program === e.messageData[0]) === -1) + { // no preset with this bank. find this program with any bank const targetBank = (soundfont.presets.find(p => p.program === e.messageData[0])?.bank + bankOffset) || bankOffset; channel.lastBank.messageData[1] = targetBank; - SpessaSynthInfo(`%cNo preset %c${bank}:${e.messageData[0]}%c. Changing bank to ${targetBank}.`, + SpessaSynthInfo( + `%cNo preset %c${bank}:${e.messageData[0]}%c. Changing bank to ${targetBank}.`, consoleColors.info, consoleColors.recognized, - consoleColors.info); - } else { + consoleColors.info + ); + } + else + { // there is a preset with this bank. add offset. For drums add the normal offset. let drumBank = system === "xg" ? 127 : 0; const newBank = (bank === 128 ? drumBank : realBank) + bankOffset; channel.lastBank.messageData[1] = newBank; - SpessaSynthInfo(`%cPreset %c${bank}:${e.messageData[0]}%c exists. Changing bank to ${newBank}.`, + SpessaSynthInfo( + `%cPreset %c${bank}:${e.messageData[0]}%c exists. Changing bank to ${newBank}.`, consoleColors.info, consoleColors.recognized, - consoleColors.info); + consoleColors.info + ); } continue; } // we only care about bank select - if (e.messageData[0] !== midiControllers.bankSelect) { + if (e.messageData[0] !== midiControllers.bankSelect) + { continue; } // bank select channel.hasBankSelect = true; - if (system === "xg") { + if (system === "xg") + { // check for xg drums channel.drums = e.messageData[1] === 120 || e.messageData[1] === 126 || e.messageData[1] === 127; } channel.lastBank = e; } - + // add missing bank selects // add all bank selects that are missing for this track - channelsInfo.forEach((has, ch) => { - if (has.hasBankSelect === true) { + channelsInfo.forEach((has, ch) => + { + if (has.hasBankSelect === true) + { return; } // find first program change (for the given channel) @@ -265,18 +303,22 @@ export function writeRMIDI( // find track with this channel being used const portOffset = Math.floor(ch / 16) * 16; const port = mid.midiPortChannelOffsets.indexOf(portOffset); - const track = mid.tracks.find((t, tNum) => mid.midiPorts[tNum] === port && mid.usedChannelsOnTrack[tNum].has(midiChannel)); - if (track === undefined) { + const track = mid.tracks.find((t, tNum) => mid.midiPorts[tNum] === port && mid.usedChannelsOnTrack[tNum].has( + midiChannel)); + if (track === undefined) + { // this channel is not used at all return; } let indexToAdd = track.findIndex(e => e.messageStatusByte === status); - if (indexToAdd === -1) { + if (indexToAdd === -1) + { // no program change... // add programs if they are missing from the track // (need them to activate bank 1 for the embedded sfont) const programIndex = track.findIndex(e => (e.messageStatusByte > 0x80 && e.messageStatusByte < 0xF0) && (e.messageStatusByte & 0xF) === midiChannel); - if (programIndex === -1) { + if (programIndex === -1) + { // no voices??? skip return; } @@ -289,31 +331,40 @@ export function writeRMIDI( )); indexToAdd = programIndex; } - SpessaSynthInfo(`%cAdding bank select for %c${ch}`, + SpessaSynthInfo( + `%cAdding bank select for %c${ch}`, consoleColors.info, - consoleColors.recognized) + consoleColors.recognized + ); const ticks = track[indexToAdd].ticks; - const targetBank = (soundfont.getPreset(0, has.program)?.bank + bankOffset) || bankOffset; + const targetBank = (soundfont.getPreset( + 0, + has.program + )?.bank + bankOffset) || bankOffset; track.splice(indexToAdd, 0, new MidiMessage( ticks, messageTypes.controllerChange | midiChannel, new IndexedByteArray([midiControllers.bankSelect, targetBank]) )); }); - + // make sure to put xg if gm - if (system !== "gs" && system !== "xg") { - for (const m of unwantedSystems) { + if (system !== "gs" && system !== "xg") + { + for (const m of unwantedSystems) + { mid.tracks[m.tNum].splice(mid.tracks[m.tNum].indexOf(m.e), 1); } let index = 0; if (mid.tracks[0][0].messageStatusByte === messageTypes.trackName) + { index++; + } mid.tracks[0].splice(index, 0, getGsOn(0)); } } const newMid = new IndexedByteArray(writeMIDIFile(mid).buffer); - + // infodata for RMID /** * @type {Uint8Array[]} @@ -325,9 +376,9 @@ export function writeRMIDI( writeRIFFOddSize(RMIDINFOChunks.software, encoder.encode("SpessaSynth"), true) ); // name - if(metadata.name !== undefined) + if (metadata.name !== undefined) { - + infoContent.push( writeRIFFOddSize(RMIDINFOChunks.name, encoder.encode(metadata.name), true) ); @@ -340,7 +391,7 @@ export function writeRMIDI( ); } // creation date - if(metadata.creationDate !== undefined) + if (metadata.creationDate !== undefined) { encoding = FORCED_ENCODING; infoContent.push( @@ -350,19 +401,19 @@ export function writeRMIDI( else { const today = new Date().toLocaleString(undefined, { - weekday: "long", - year: 'numeric', - month: "long", - day: "numeric", - hour: "numeric", - minute: "numeric" - }); + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + hour: "numeric", + minute: "numeric" + }); infoContent.push( writeRIFFOddSize(RMIDINFOChunks.creationDate, getStringBytes(today), true) ); } // comment - if(metadata.comment !== undefined) + if (metadata.comment !== undefined) { encoding = FORCED_ENCODING; infoContent.push( @@ -370,14 +421,14 @@ export function writeRMIDI( ); } // engineer - if(metadata.engineer !== undefined) + if (metadata.engineer !== undefined) { infoContent.push( writeRIFFOddSize(RMIDINFOChunks.engineer, encoder.encode(metadata.engineer), true) - ) + ); } // album - if(metadata.album !== undefined) + if (metadata.album !== undefined) { encoding = FORCED_ENCODING; infoContent.push( @@ -385,7 +436,7 @@ export function writeRMIDI( ); } // artist - if(metadata.artist !== undefined) + if (metadata.artist !== undefined) { encoding = FORCED_ENCODING; infoContent.push( @@ -393,7 +444,7 @@ export function writeRMIDI( ); } // genre - if(metadata.genre !== undefined) + if (metadata.genre !== undefined) { encoding = FORCED_ENCODING; infoContent.push( @@ -401,14 +452,14 @@ export function writeRMIDI( ); } // picture - if(metadata.picture !== undefined) + if (metadata.picture !== undefined) { infoContent.push( writeRIFFOddSize(RMIDINFOChunks.picture, new Uint8Array(metadata.picture)) ); } // copyright - if(metadata.copyright !== undefined) + if (metadata.copyright !== undefined) { encoding = FORCED_ENCODING; infoContent.push( @@ -423,13 +474,13 @@ export function writeRMIDI( writeRIFFOddSize(RMIDINFOChunks.copyright, getStringBytes(copyright)) ); } - + // bank offset const DBNK = new IndexedByteArray(2); writeLittleEndian(DBNK, bankOffset, 2); infoContent.push(writeRIFFOddSize(RMIDINFOChunks.bankOffset, DBNK)); // midi encoding - if(metadata.midiEncoding !== undefined) + if (metadata.midiEncoding !== undefined) { infoContent.push( writeRIFFOddSize(RMIDINFOChunks.midiEncoding, encoder.encode(metadata.midiEncoding)) @@ -438,7 +489,7 @@ export function writeRMIDI( } // encoding infoContent.push(writeRIFFOddSize(RMIDINFOChunks.encoding, getStringBytes(encoding))); - + // combine and write out const infodata = combineArrays(infoContent); const rmiddata = combineArrays([ @@ -453,7 +504,7 @@ export function writeRMIDI( ), soundfontBinary ]); - SpessaSynthInfo("%cFinished!", consoleColors.info) + SpessaSynthInfo("%cFinished!", consoleColors.info); SpessaSynthGroupEnd(); return writeRIFFOddSize( "RIFF", diff --git a/src/spessasynth_lib/midi_parser/used_keys_loaded.js b/src/spessasynth_lib/midi_parser/used_keys_loaded.js index b9eef71f..e658cae8 100644 --- a/src/spessasynth_lib/midi_parser/used_keys_loaded.js +++ b/src/spessasynth_lib/midi_parser/used_keys_loaded.js @@ -1,7 +1,7 @@ -import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from '../utils/loggin.js' -import { consoleColors } from '../utils/other.js' -import { DEFAULT_PERCUSSION } from '../synthetizer/synthetizer.js' -import { messageTypes, midiControllers } from './midi_message.js' +import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from "../utils/loggin.js"; +import { consoleColors } from "../utils/other.js"; +import { DEFAULT_PERCUSSION } from "../synthetizer/synthetizer.js"; +import { messageTypes, midiControllers } from "./midi_message.js"; /** * @param mid {BasicMIDI} @@ -10,24 +10,27 @@ import { messageTypes, midiControllers } from './midi_message.js' */ export function getUsedProgramsAndKeys(mid, soundfont) { - SpessaSynthGroupCollapsed("%cSearching for all used programs and keys...", - consoleColors.info); + SpessaSynthGroupCollapsed( + "%cSearching for all used programs and keys...", + consoleColors.info + ); // find every bank:program combo and every key:velocity for each. Make sure to care about ports and drums - const channelsAmount = 16 + mid.midiPortChannelOffsets.reduce((max, cur) => cur > max ? cur: max); + const channelsAmount = 16 + mid.midiPortChannelOffsets.reduce((max, cur) => cur > max ? cur : max); /** * @type {{program: number, bank: number, drums: boolean, string: string}[]} */ const channelPresets = []; - for (let i = 0; i < channelsAmount; i++) { + for (let i = 0; i < channelsAmount; i++) + { const bank = i % 16 === DEFAULT_PERCUSSION ? 128 : 0; channelPresets.push({ program: 0, bank: bank, drums: i % 16 === DEFAULT_PERCUSSION, // drums appear on 9 every 16 channels, - string: `${bank}:0`, + string: `${bank}:0` }); } - + function updateString(ch) { // check if this exists in the soundfont @@ -35,37 +38,42 @@ export function getUsedProgramsAndKeys(mid, soundfont) ch.bank = exists.bank; ch.program = exists.program; ch.string = ch.bank + ":" + ch.program; - if(!usedProgramsAndKeys[ch.string]) + if (!usedProgramsAndKeys[ch.string]) { - SpessaSynthInfo(`%cDetected a new preset: %c${ch.string}`, + SpessaSynthInfo( + `%cDetected a new preset: %c${ch.string}`, consoleColors.info, - consoleColors.recognized); + consoleColors.recognized + ); usedProgramsAndKeys[ch.string] = new Set(); } } + /** * find all programs used and key-velocity combos in them * bank:program each has a set of midiNote-velocity * @type {Object>} */ const usedProgramsAndKeys = {}; - + /** * indexes for tracks * @type {number[]} */ const eventIndexes = Array(mid.tracks.length).fill(0); let remainingTracks = mid.tracks.length; + function findFirstEventIndex() { let index = 0; let ticks = Infinity; - mid.tracks.forEach((track, i) => { - if(eventIndexes[i] >= track.length) + mid.tracks.forEach((track, i) => + { + if (eventIndexes[i] >= track.length) { return; } - if(track[eventIndexes[i]].ticks < ticks) + if (track[eventIndexes[i]].ticks < ticks) { index = i; ticks = track[eventIndexes[i]].ticks; @@ -73,28 +81,29 @@ export function getUsedProgramsAndKeys(mid, soundfont) }); return index; } + const ports = mid.midiPorts.slice(); // check for xg let system = "gs"; - while(remainingTracks > 0) + while (remainingTracks > 0) { let trackNum = findFirstEventIndex(); const track = mid.tracks[trackNum]; - if(eventIndexes[trackNum] >= track.length) + if (eventIndexes[trackNum] >= track.length) { remainingTracks--; continue; } const event = track[eventIndexes[trackNum]]; eventIndexes[trackNum]++; - - if(event.messageStatusByte === messageTypes.midiPort) + + if (event.messageStatusByte === messageTypes.midiPort) { ports[trackNum] = event.messageData[0]; continue; } const status = event.messageStatusByte & 0xF0; - if( + if ( status !== messageTypes.noteOn && status !== messageTypes.controllerChange && status !== messageTypes.programChange && @@ -105,31 +114,31 @@ export function getUsedProgramsAndKeys(mid, soundfont) } const channel = (event.messageStatusByte & 0xF) + mid.midiPortChannelOffsets[ports[trackNum]] || 0; let ch = channelPresets[channel]; - switch(status) + switch (status) { case messageTypes.programChange: ch.program = event.messageData[0]; updateString(ch); break; - + case messageTypes.controllerChange: - if(event.messageData[0] !== midiControllers.bankSelect) + if (event.messageData[0] !== midiControllers.bankSelect) { // we only care about bank select continue; } - if(system === "gs" && ch.drums) + if (system === "gs" && ch.drums) { // gs drums get changed via sysex, ignore here continue; } const bank = event.messageData[1]; const realBank = Math.max(0, bank - mid.bankOffset); - if(system === "xg") + if (system === "xg") { // check for xg drums const drumsBool = bank === 120 || bank === 126 || bank === 127; - if(drumsBool !== ch.drums) + if (drumsBool !== ch.drums) { // drum change is a program change ch.drums = drumsBool; @@ -145,9 +154,9 @@ export function getUsedProgramsAndKeys(mid, soundfont) channelPresets[channel].bank = realBank; // do not update the data, bank change doesnt change the preset break; - + case messageTypes.noteOn: - if(event.messageData[1] === 0) + if (event.messageData[1] === 0) { // that's a note off continue; @@ -155,21 +164,21 @@ export function getUsedProgramsAndKeys(mid, soundfont) updateString(ch); usedProgramsAndKeys[ch.string].add(`${event.messageData[0]}-${event.messageData[1]}`); break; - + case messageTypes.systemExclusive: // check for drum sysex - if( + if ( event.messageData[0] !== 0x41 || // roland event.messageData[2] !== 0x42 || // GS event.messageData[3] !== 0x12 || // GS event.messageData[4] !== 0x40 || // system parameter - (event.messageData[5] & 0x10 ) === 0 || // part parameter + (event.messageData[5] & 0x10) === 0 || // part parameter event.messageData[6] !== 0x15 // drum pars - + ) { // check for XG - if( + if ( event.messageData[0] === 0x43 && // yamaha event.messageData[2] === 0x4C && // sXG ON event.messageData[5] === 0x7E && @@ -187,16 +196,18 @@ export function getUsedProgramsAndKeys(mid, soundfont) ch.bank = isDrum ? 128 : 0; updateString(ch); break; - + } } - for(const key of Object.keys(usedProgramsAndKeys)) + for (const key of Object.keys(usedProgramsAndKeys)) { - if(usedProgramsAndKeys[key].size === 0) + if (usedProgramsAndKeys[key].size === 0) { - SpessaSynthInfo(`%cDetected change but no keys for %c${key}`, + SpessaSynthInfo( + `%cDetected change but no keys for %c${key}`, consoleColors.info, - consoleColors.value) + consoleColors.value + ); delete usedProgramsAndKeys[key]; } } diff --git a/src/spessasynth_lib/sequencer/README.md b/src/spessasynth_lib/sequencer/README.md index 404ae5c9..bfeb9c0b 100644 --- a/src/spessasynth_lib/sequencer/README.md +++ b/src/spessasynth_lib/sequencer/README.md @@ -1,8 +1,11 @@ ## This is the sequencer's folder. + The code here is responsible for playing back the parsed MIDI sequence with the synthesizer. ### Message protocol: + #### Message structure + ```js const message = { messageType: number, // WorkletSequencerMessageType @@ -11,13 +14,17 @@ const message = { ``` #### To worklet -Sequencer uses `Synthetizer`'s `post` method to post a message with `messageData` set to `workletMessageType.sequencerSpecific`. + +Sequencer uses `Synthetizer`'s `post` method to post a message with `messageData` set to +`workletMessageType.sequencerSpecific`. The `messageData` is set to the sequencer's message. #### From worklet -`WorkletSequencer` uses `SpessaSynthProcessor`'s post to send a message with `messageData` set to `returnMessageType.sequencerSpecific`. -The `messageData` is set to the sequencer's return message. +`WorkletSequencer` uses `SpessaSynthProcessor`'s post to send a message with `messageData` set to +`returnMessageType.sequencerSpecific`. +The `messageData` is set to the sequencer's return message. ### Process tick + `processTick` is called every time the `process` method is called via `SpessaSynthProcessor.processTickCallback`. diff --git a/src/spessasynth_lib/sequencer/sequencer.js b/src/spessasynth_lib/sequencer/sequencer.js index 6f17c84a..8db98a18 100644 --- a/src/spessasynth_lib/sequencer/sequencer.js +++ b/src/spessasynth_lib/sequencer/sequencer.js @@ -1,13 +1,13 @@ -import { MIDI } from '../midi_parser/midi_loader.js' -import { Synthetizer } from '../synthetizer/synthetizer.js' -import { messageTypes } from '../midi_parser/midi_message.js' -import { workletMessageType } from '../synthetizer/worklet_system/message_protocol/worklet_message.js' +import { MIDI } from "../midi_parser/midi_loader.js"; +import { Synthetizer } from "../synthetizer/synthetizer.js"; +import { messageTypes } from "../midi_parser/midi_message.js"; +import { workletMessageType } from "../synthetizer/worklet_system/message_protocol/worklet_message.js"; import { WorkletSequencerMessageType, - WorkletSequencerReturnMessageType, -} from './worklet_sequencer/sequencer_message.js' -import { SpessaSynthWarn } from '../utils/loggin.js' -import { DUMMY_MIDI_DATA, MidiData } from '../midi_parser/midi_data.js' + WorkletSequencerReturnMessageType +} from "./worklet_sequencer/sequencer_message.js"; +import { SpessaSynthWarn } from "../utils/loggin.js"; +import { DUMMY_MIDI_DATA, MidiData } from "../midi_parser/midi_data.js"; /** * sequencer.js @@ -33,10 +33,44 @@ import { DUMMY_MIDI_DATA, MidiData } from '../midi_parser/midi_data.js' * @type {SequencerOptions} */ const DEFAULT_OPTIONS = { - skipToFirstNoteOn: true, -} + skipToFirstNoteOn: true +}; + export class Sequencer { + /** + * Executes when MIDI parsing has an error. + * @type {function(string)} + */ + onError; + /** + * The sequence's data, except for the track data. + * @type {MidiData} + */ + midiData; + /** + * @type {Object} + * @private + */ + onSongChange = {}; + /** + * Fires on text event + * @param data {Uint8Array} the data text + * @param type {number} the status byte of the message (the meta status byte) + */ + onTextEvent; + /** + * Fires when CurrentTime changes + * @type {Object} the time that was changed to + * @private + */ + onTimeChange = {}; + /** + * @type {Object} + * @private + */ + onSongEnded = {}; + /** * Creates a new Midi sequencer for playing back MIDI files * @param midiBinaries {MIDIFile[]} List of the buffers of the MIDI files @@ -48,66 +82,66 @@ export class Sequencer this.ignoreEvents = false; this.synth = synth; this.highResTimeOffset = 0; - + /** * Absolute playback startTime, bases on the synth's time * @type {number} */ this.absoluteStartTime = this.synth.currentTime; - + /** * @type {function(MIDI)} * @private */ this._getMIDIResolve = undefined; - + /** * Controls the playback's rate * @type {number} */ this._playbackRate = 1; - + this.songIndex = 0; - + /** * Indicates if the current midiData property has dummy data in it (not yet loaded) * @type {boolean} */ this.hasDummyData = true; - + this._loop = true; - + /** * Indicates whether the sequencer has finished playing a sequence * @type {boolean} */ this.isFinished = false; - + /** * The current sequence's length, in seconds * @type {number} */ this.duration = 0; - + this.synth.sequencerCallbackFunction = this._handleMessage.bind(this); - + /** * @type {boolean} * @private */ this._skipToFirstNoteOn = options?.skipToFirstNoteOn ?? true; - - if(this._skipToFirstNoteOn === false) + + if (this._skipToFirstNoteOn === false) { // setter sends message this._sendMessage(WorkletSequencerMessageType.setSkipToFirstNote, false); } - + this.loadNewSongList(midiBinaries); - - window.addEventListener("beforeunload", this.resetMIDIOut.bind(this)) + + window.addEventListener("beforeunload", this.resetMIDIOut.bind(this)); } - + /** * Indicates if the sequencer should skip to first note on * @return {boolean} @@ -116,7 +150,7 @@ export class Sequencer { return this._skipToFirstNoteOn; } - + /** * Indicates if the sequencer should skip to first note on * @param val {boolean} @@ -126,10 +160,129 @@ export class Sequencer this._skipToFirstNoteOn = val; this._sendMessage(WorkletSequencerMessageType.setSkipToFirstNote, this._skipToFirstNoteOn); } - + + /** + * @returns {number} Current playback time, in seconds + */ + get currentTime() + { + // return the paused time if it's set to something other than undefined + if (this.pausedTime) + { + return this.pausedTime; + } + + return (this.synth.currentTime - this.absoluteStartTime) * this._playbackRate; + } + + set currentTime(time) + { + this.unpause(); + this._sendMessage(WorkletSequencerMessageType.setTime, time); + } + + get loop() + { + return this._loop; + } + + set loop(value) + { + this._sendMessage(WorkletSequencerMessageType.setLoop, value); + this._loop = value; + } + + /** + * Use for visualization as it's not affected by the audioContext stutter + * @returns {number} + */ + get currentHighResolutionTime() + { + if (this.pausedTime) + { + return this.pausedTime; + } + const highResTimeOffset = this.highResTimeOffset; + const absoluteStartTime = this.absoluteStartTime; + + // sync performance.now to current time + const performanceElapsedTime = ((performance.now() / 1000) - absoluteStartTime) * this._playbackRate; + + let currentPerformanceTime = highResTimeOffset + performanceElapsedTime; + const currentAudioTime = this.currentTime; + + const smoothingFactor = 0.01 * this._playbackRate; + + // diff times smoothing factor + const timeDifference = currentAudioTime - currentPerformanceTime; + this.highResTimeOffset += timeDifference * smoothingFactor; + + // return a smoothed performance time + currentPerformanceTime = this.highResTimeOffset + performanceElapsedTime; + return currentPerformanceTime; + } + + /** + * @returns {number} + */ + get playbackRate() + { + return this._playbackRate; + } + + /** + * @param value {number} + */ + set playbackRate(value) + { + this._sendMessage(WorkletSequencerMessageType.setPlaybackRate, value); + this.highResTimeOffset *= (value / this._playbackRate); + this._playbackRate = value; + } + + /** + * true if paused, false if playing or stopped + * @returns {boolean} + */ + get paused() + { + return this.pausedTime !== undefined; + } + + /** + * Adds a new event that gets called when the song changes + * @param callback {function(MidiData)} + * @param id {string} must be unique + */ + addOnSongChangeEvent(callback, id) + { + this.onSongChange[id] = callback; + callback(this.midiData); + } + + /** + * Adds a new event that gets called when the song ends + * @param callback {function} + * @param id {string} must be unique + */ + addOnSongEndedEvent(callback, id) + { + this.onSongEnded[id] = callback; + } + + /** + * Adds a new event that gets called when the time changes + * @param callback {function(number)} the new time, in seconds + * @param id {string} must be unique + */ + addOnTimeChangeEvent(callback, id) + { + this.onTimeChange[id] = callback; + } + resetMIDIOut() { - if(!this.MIDIout) + if (!this.MIDIout) { return; } @@ -140,18 +293,7 @@ export class Sequencer } this.MIDIout.send([messageTypes.reset]); // reset } - - set loop(value) - { - this._sendMessage(WorkletSequencerMessageType.setLoop, value); - this._loop = value; - } - - get loop() - { - return this._loop; - } - + /** * @param messageType {WorkletSequencerMessageType} * @param messageData {any} @@ -168,13 +310,17 @@ export class Sequencer } }); } - - /** - * Executes when MIDI parsing has an error. - * @type {function(string)} - */ - onError; - + + nextSong() + { + this._sendMessage(WorkletSequencerMessageType.changeSong, true); + } + + previousSong() + { + this._sendMessage(WorkletSequencerMessageType.changeSong, false); + } + /** * @param {WorkletSequencerReturnMessageType} messageType * @param {any} messageData @@ -182,7 +328,7 @@ export class Sequencer */ _handleMessage(messageType, messageData) { - if(this.ignoreEvents) + if (this.ignoreEvents) { return; } @@ -190,20 +336,22 @@ export class Sequencer { default: break; - + case WorkletSequencerReturnMessageType.midiEvent: /** * @type {number[]} */ let midiEventData = messageData; - if (this.MIDIout) { - if (midiEventData[0] >= 0x80) { + if (this.MIDIout) + { + if (midiEventData[0] >= 0x80) + { this.MIDIout.send(midiEventData); return; } } break; - + case WorkletSequencerReturnMessageType.songChange: /** * messageData is expected to be {MidiData} @@ -218,17 +366,18 @@ export class Sequencer Object.entries(this.onSongChange).forEach((callback) => callback[1](songChangeData)); this.unpause(); break; - + case WorkletSequencerReturnMessageType.textEvent: /** * @type {[Uint8Array, number]} */ let textEventData = messageData; - if (this.onTextEvent) { + if (this.onTextEvent) + { this.onTextEvent(textEventData[0], textEventData[1]); } break; - + case WorkletSequencerReturnMessageType.timeChange: // message data is absolute time const time = this.synth.currentTime - messageData; @@ -236,18 +385,18 @@ export class Sequencer this.unpause(); this._recalculateStartTime(time); break; - + case WorkletSequencerReturnMessageType.pause: this.pausedTime = this.currentTime; this.isFinished = messageData; - if(this.isFinished) + if (this.isFinished) { Object.entries(this.onSongEnded).forEach((callback) => callback[1]()); } break; - + case WorkletSequencerReturnMessageType.midiError: - if(this.onError) + if (this.onError) { this.onError(messageData); } @@ -256,76 +405,37 @@ export class Sequencer throw new Error(messageData); } return; - + case WorkletSequencerReturnMessageType.getMIDI: - if(this._getMIDIResolve) + if (this._getMIDIResolve) { this._getMIDIResolve(messageData); } } } - - /** - * @param value {number} - */ - set playbackRate(value) - { - this._sendMessage(WorkletSequencerMessageType.setPlaybackRate, value); - this.highResTimeOffset *= (value / this._playbackRate); - this._playbackRate = value; - } - - /** - * @returns {number} - */ - get playbackRate() - { - return this._playbackRate; - } - - /** - * Adds a new event that gets called when the song changes - * @param callback {function(MidiData)} - * @param id {string} must be unique - */ - addOnSongChangeEvent(callback, id) - { - this.onSongChange[id] = callback; - callback(this.midiData); - } - - + /** - * Adds a new event that gets called when the song ends - * @param callback {function} - * @param id {string} must be unique - */ - addOnSongEndedEvent(callback, id) - { - this.onSongEnded[id] = callback; - } - - /** - * Adds a new event that gets called when the time changes - * @param callback {function(number)} the new time, in seconds - * @param id {string} must be unique + * @param time + * @private */ - addOnTimeChangeEvent(callback, id) + _recalculateStartTime(time) { - this.onTimeChange[id] = callback; + this.absoluteStartTime = this.synth.currentTime - time / this._playbackRate; + this.highResTimeOffset = (this.synth.currentTime - (performance.now() / 1000)) * this._playbackRate; } - + /** * @returns {Promise} */ async getMIDI() { - return new Promise(resolve => { + return new Promise(resolve => + { this._getMIDIResolve = resolve; this._sendMessage(WorkletSequencerMessageType.getMIDI, undefined); }); } - + /** * @param midiBuffers {MIDIFile[]} */ @@ -339,81 +449,12 @@ export class Sequencer this._sendMessage(WorkletSequencerMessageType.loadNewSongList, midiBuffers); this.songIndex = 0; this.songsAmount = midiBuffers.length; - if(this.songsAmount > 1) + if (this.songsAmount > 1) { this.loop = false; } } - - nextSong() - { - this._sendMessage(WorkletSequencerMessageType.changeSong, true); - } - - previousSong() - { - this._sendMessage(WorkletSequencerMessageType.changeSong, false); - } - - /** - * @returns {number} Current playback time, in seconds - */ - get currentTime() - { - // return the paused time if it's set to something other than undefined - if(this.pausedTime) - { - return this.pausedTime; - } - - return (this.synth.currentTime - this.absoluteStartTime) * this._playbackRate; - } - - /** - * @param time - * @private - */ - _recalculateStartTime(time) - { - this.absoluteStartTime = this.synth.currentTime - time / this._playbackRate; - this.highResTimeOffset = (this.synth.currentTime - (performance.now() / 1000)) * this._playbackRate; - } - - /** - * Use for visualization as it's not affected by the audioContext stutter - * @returns {number} - */ - get currentHighResolutionTime() { - if (this.pausedTime) { - return this.pausedTime; - } - const highResTimeOffset = this.highResTimeOffset; - const absoluteStartTime = this.absoluteStartTime; - - // sync performance.now to current time - const performanceElapsedTime = ((performance.now() / 1000) - absoluteStartTime) * this._playbackRate; - - let currentPerformanceTime = highResTimeOffset + performanceElapsedTime; - const currentAudioTime = this.currentTime; - - const smoothingFactor = 0.01 * this._playbackRate; - - // diff times smoothing factor - const timeDifference = currentAudioTime - currentPerformanceTime; - this.highResTimeOffset += timeDifference * smoothingFactor; - - // return a smoothed performance time - currentPerformanceTime = this.highResTimeOffset + performanceElapsedTime; - return currentPerformanceTime; - } - - - set currentTime(time) - { - this.unpause() - this._sendMessage(WorkletSequencerMessageType.setTime, time); - } - + /** * @param output {MIDIOutput} */ @@ -424,13 +465,13 @@ export class Sequencer this._sendMessage(WorkletSequencerMessageType.changeMIDIMessageSending, output !== undefined); this.currentTime -= 0.1; } - + /** * Pauses the playback */ pause() { - if(this.paused) + if (this.paused) { SpessaSynthWarn("Already paused"); return; @@ -438,37 +479,28 @@ export class Sequencer this.pausedTime = this.currentTime; this._sendMessage(WorkletSequencerMessageType.pause); } - + unpause() { this.pausedTime = undefined; this.isFinished = false; } - - /** - * true if paused, false if playing or stopped - * @returns {boolean} - */ - get paused() - { - return this.pausedTime !== undefined; - } - + /** * Starts the playback * @param resetTime {boolean} If true, time is set to 0s */ play(resetTime = false) { - if(this.isFinished) + if (this.isFinished) { resetTime = true; } this._recalculateStartTime(this.pausedTime || 0); - this.unpause() + this.unpause(); this._sendMessage(WorkletSequencerMessageType.play, resetTime); } - + /** * Stops the playback */ @@ -476,36 +508,4 @@ export class Sequencer { this._sendMessage(WorkletSequencerMessageType.stop); } - - /** - * The sequence's data, except for the track data. - * @type {MidiData} - */ - midiData; - - /** - * @type {Object} - * @private - */ - onSongChange = {}; - - /** - * Fires on text event - * @param data {Uint8Array} the data text - * @param type {number} the status byte of the message (the meta status byte) - */ - onTextEvent; - - /** - * Fires when CurrentTime changes - * @type {Object} the time that was changed to - * @private - */ - onTimeChange = {}; - - /** - * @type {Object} - * @private - */ - onSongEnded = {}; } \ No newline at end of file diff --git a/src/spessasynth_lib/sequencer/worklet_sequencer/events.js b/src/spessasynth_lib/sequencer/worklet_sequencer/events.js index 9727c873..7650e808 100644 --- a/src/spessasynth_lib/sequencer/worklet_sequencer/events.js +++ b/src/spessasynth_lib/sequencer/worklet_sequencer/events.js @@ -1,7 +1,7 @@ -import { returnMessageType } from '../../synthetizer/worklet_system/message_protocol/worklet_message.js' -import { WorkletSequencerMessageType, WorkletSequencerReturnMessageType } from './sequencer_message.js' -import { messageTypes, midiControllers } from '../../midi_parser/midi_message.js' -import { MIDI_CHANNEL_COUNT } from '../../synthetizer/synthetizer.js' +import { returnMessageType } from "../../synthetizer/worklet_system/message_protocol/worklet_message.js"; +import { WorkletSequencerMessageType, WorkletSequencerReturnMessageType } from "./sequencer_message.js"; +import { messageTypes, midiControllers } from "../../midi_parser/midi_message.js"; +import { MIDI_CHANNEL_COUNT } from "../../synthetizer/synthetizer.js"; /** * @param messageType {WorkletSequencerMessageType} @@ -14,39 +14,39 @@ export function processMessage(messageType, messageData) { default: break; - + case WorkletSequencerMessageType.loadNewSongList: this.loadNewSongList(messageData); break; - + case WorkletSequencerMessageType.pause: this.pause(); break; - + case WorkletSequencerMessageType.play: this.play(messageData); break; - + case WorkletSequencerMessageType.stop: this.stop(); break; - + case WorkletSequencerMessageType.setTime: this.currentTime = messageData; break; - + case WorkletSequencerMessageType.changeMIDIMessageSending: this.sendMIDIMessages = messageData; break; - + case WorkletSequencerMessageType.setPlaybackRate: this.playbackRate = messageData; break; - + case WorkletSequencerMessageType.setLoop: this.loop = messageData; break; - + case WorkletSequencerMessageType.changeSong: if (messageData) { @@ -57,11 +57,11 @@ export function processMessage(messageType, messageData) this.previousSong(); } break; - + case WorkletSequencerMessageType.getMIDI: this.post(WorkletSequencerReturnMessageType.getMIDI, this.midiData); break; - + case WorkletSequencerMessageType.setSkipToFirstNote: this._skipToFirstNoteOn = messageData; break; @@ -76,7 +76,7 @@ export function processMessage(messageType, messageData) */ export function post(messageType, messageData = undefined) { - if(!this.synth.enableEventSystem) + if (!this.synth.enableEventSystem) { return; } @@ -86,7 +86,7 @@ export function post(messageType, messageData = undefined) messageType: messageType, messageData: messageData } - }) + }); } /** @@ -107,11 +107,11 @@ export function sendMIDIMessage(message) export function sendMIDICC(channel, type, value) { channel %= 16; - if(!this.sendMIDIMessages) + if (!this.sendMIDIMessages) { return; } - this.sendMIDIMessage([messageTypes.controllerChange | channel, type, value]) + this.sendMIDIMessage([messageTypes.controllerChange | channel, type, value]); } /** @@ -122,7 +122,7 @@ export function sendMIDICC(channel, type, value) export function sendMIDIProgramChange(channel, program) { channel %= 16; - if(!this.sendMIDIMessages) + if (!this.sendMIDIMessages) { return; } @@ -139,7 +139,7 @@ export function sendMIDIProgramChange(channel, program) export function sendMIDIPitchWheel(channel, MSB, LSB) { channel %= 16; - if(!this.sendMIDIMessages) + if (!this.sendMIDIMessages) { return; } @@ -151,12 +151,12 @@ export function sendMIDIPitchWheel(channel, MSB, LSB) */ export function sendMIDIReset() { - if(!this.sendMIDIMessages) + if (!this.sendMIDIMessages) { return; } this.sendMIDIMessage([messageTypes.reset]); - for(let ch = 0; ch < MIDI_CHANNEL_COUNT; ch++) + for (let ch = 0; ch < MIDI_CHANNEL_COUNT; ch++) { this.sendMIDIMessage([messageTypes.controllerChange | ch, midiControllers.allSoundOff, 0]); this.sendMIDIMessage([messageTypes.controllerChange | ch, midiControllers.resetAllControllers, 0]); diff --git a/src/spessasynth_lib/sequencer/worklet_sequencer/play.js b/src/spessasynth_lib/sequencer/worklet_sequencer/play.js index f0c3ff59..333c9b0b 100644 --- a/src/spessasynth_lib/sequencer/worklet_sequencer/play.js +++ b/src/spessasynth_lib/sequencer/worklet_sequencer/play.js @@ -1,6 +1,6 @@ -import { getEvent, messageTypes, midiControllers } from '../../midi_parser/midi_message.js' -import { WorkletSequencerReturnMessageType } from './sequencer_message.js' -import { MIDIticksToSeconds } from '../../midi_parser/basic_midi.js' +import { getEvent, messageTypes, midiControllers } from "../../midi_parser/midi_message.js"; +import { WorkletSequencerReturnMessageType } from "./sequencer_message.js"; +import { MIDIticksToSeconds } from "../../midi_parser/basic_midi.js"; // an array with preset default values @@ -28,14 +28,14 @@ export function _playTo(time, ticks = undefined) this.synth.resetAllControllers(); this.sendMIDIReset(); this._resetTimers(); - + const channelsToSave = this.synth.workletProcessorChannels.length; /** * save pitch bends here and send them only after * @type {number[]} */ const pitchBends = Array(channelsToSave).fill(8192); - + /** * Save programs here and send them only after * @type {{program: number, bank: number, actualBank: number}[]} @@ -46,25 +46,25 @@ export function _playTo(time, ticks = undefined) programs.push({ program: -1, bank: 0, - actualBank: 0, + actualBank: 0 }); } - + const isCCNonSkippable = controllerNumber => ( - controllerNumber === midiControllers.dataDecrement || - controllerNumber === midiControllers.dataIncrement || - controllerNumber === midiControllers.dataEntryMsb || - controllerNumber === midiControllers.dataDecrement || + controllerNumber === midiControllers.dataDecrement || + controllerNumber === midiControllers.dataIncrement || + controllerNumber === midiControllers.dataEntryMsb || + controllerNumber === midiControllers.dataDecrement || controllerNumber === midiControllers.lsbForControl6DataEntry || - controllerNumber === midiControllers.RPNLsb || - controllerNumber === midiControllers.RPNMsb || - controllerNumber === midiControllers.NRPNLsb || - controllerNumber === midiControllers.NRPNMsb || - controllerNumber === midiControllers.bankSelect || - controllerNumber === midiControllers.lsbForControl0BankSelect|| + controllerNumber === midiControllers.RPNLsb || + controllerNumber === midiControllers.RPNMsb || + controllerNumber === midiControllers.NRPNLsb || + controllerNumber === midiControllers.NRPNMsb || + controllerNumber === midiControllers.bankSelect || + controllerNumber === midiControllers.lsbForControl0BankSelect || controllerNumber === midiControllers.resetAllControllers ); - + /** * Save controllers here and send them only after * @type {number[][]} @@ -74,63 +74,63 @@ export function _playTo(time, ticks = undefined) { savedControllers.push(Array.from(defaultControllerArray)); } - - while(true) + + while (true) { // find next event let trackIndex = this._findFirstEventIndex(); let event = this.tracks[trackIndex][this.eventIndex[trackIndex]]; - if(ticks !== undefined) + if (ticks !== undefined) { - if(event.ticks >= ticks) + if (event.ticks >= ticks) { break; } } else { - if(this.playedTime >= time) + if (this.playedTime >= time) { break; } } - + // skip note ons const info = getEvent(event.messageStatusByte); // Keep in mind midi ports to determine channel!! const channel = info.channel + (this.midiPortChannelOffsets[this.midiPorts[trackIndex]] || 0); - switch(info.status) + switch (info.status) { // skip note messages case messageTypes.noteOn: case messageTypes.noteOff: case messageTypes.keySignature: break; - + // skip pitch bend case messageTypes.pitchBend: pitchBends[channel] = event.messageData[1] << 7 | event.messageData[0]; break; - + case messageTypes.programChange: const p = programs[channel]; p.program = event.messageData[0]; p.actualBank = p.bank; break; - + case messageTypes.controllerChange: // do not skip data entries const controllerNumber = event.messageData[0]; - if(isCCNonSkippable(controllerNumber)) + if (isCCNonSkippable(controllerNumber)) { let ccV = event.messageData[1]; - if(controllerNumber === midiControllers.bankSelect) + if (controllerNumber === midiControllers.bankSelect) { // add the bank to saved programs[channel].bank = ccV; break; } - if(this.sendMIDIMessages) + if (this.sendMIDIMessages) { this.sendMIDICC(channel, controllerNumber, ccV); } @@ -141,53 +141,59 @@ export function _playTo(time, ticks = undefined) } else { - if(savedControllers[channel] === undefined) + if (savedControllers[channel] === undefined) { savedControllers[channel] = Array.from(defaultControllerArray); } savedControllers[channel][controllerNumber] = event.messageData[1]; } break; - + default: this._processEvent(event, trackIndex); break; } - + this.eventIndex[trackIndex]++; // find next event trackIndex = this._findFirstEventIndex(); let nextEvent = this.tracks[trackIndex][this.eventIndex[trackIndex]]; - if(nextEvent === undefined) + if (nextEvent === undefined) { this.stop(); return false; } this.playedTime += this.oneTickToSeconds * (nextEvent.ticks - event.ticks); } - + // restoring saved controllers - if(this.sendMIDIMessages) + if (this.sendMIDIMessages) { for (let channelNumber = 0; channelNumber < channelsToSave; channelNumber++) { // restore pitch bends - if(pitchBends[channelNumber] !== undefined) + if (pitchBends[channelNumber] !== undefined) { - this.sendMIDIPitchWheel(channelNumber, pitchBends[channelNumber] >> 7, pitchBends[channelNumber] & 0x7F); + this.sendMIDIPitchWheel( + channelNumber, + pitchBends[channelNumber] >> 7, + pitchBends[channelNumber] & 0x7F + ); } - if(savedControllers[channelNumber] !== undefined) + if (savedControllers[channelNumber] !== undefined) { // every controller that has changed - savedControllers[channelNumber].forEach((value, index) => { - if(value !== defaultControllerArray[index] && !isCCNonSkippable(index)) + savedControllers[channelNumber].forEach((value, index) => + { + if (value !== defaultControllerArray[index] && !isCCNonSkippable( + index)) { this.sendMIDICC(channelNumber, index, value); } - }) + }); } // restore programs - if(programs[channelNumber].program >= 0 && programs[channelNumber].actualBank >= 0) + if (programs[channelNumber].program >= 0 && programs[channelNumber].actualBank >= 0) { const bank = programs[channelNumber].actualBank; this.sendMIDICC(channelNumber, midiControllers.bankSelect, bank); @@ -201,22 +207,28 @@ export function _playTo(time, ticks = undefined) for (let channelNumber = 0; channelNumber < channelsToSave; channelNumber++) { // restore pitch bends - if(pitchBends[channelNumber] !== undefined) + if (pitchBends[channelNumber] !== undefined) { this.synth.pitchWheel(channelNumber, pitchBends[channelNumber] >> 7, pitchBends[channelNumber] & 0x7F); } - if(savedControllers[channelNumber] !== undefined) + if (savedControllers[channelNumber] !== undefined) { // every controller that has changed - savedControllers[channelNumber].forEach((value, index) => { - if(value !== defaultControllerArray[index] && !isCCNonSkippable(index)) + savedControllers[channelNumber].forEach((value, index) => + { + if (value !== defaultControllerArray[index] && !isCCNonSkippable( + index)) { - this.synth.controllerChange(channelNumber, index, value); + this.synth.controllerChange( + channelNumber, + index, + value + ); } - }) + }); } // restore programs - if(programs[channelNumber].program >= 0 && programs[channelNumber].actualBank >= 0) + if (programs[channelNumber].program >= 0 && programs[channelNumber].actualBank >= 0) { const bank = programs[channelNumber].actualBank; this.synth.controllerChange(channelNumber, midiControllers.bankSelect, bank); @@ -234,34 +246,35 @@ export function _playTo(time, ticks = undefined) */ export function play(resetTime = false) { - if(this.midiData === undefined) + if (this.midiData === undefined) { return; } - + // reset the time if necesarry - if(resetTime) + if (resetTime) { this.currentTime = 0; return; } - - if(this.currentTime >= this.duration) + + if (this.currentTime >= this.duration) { this.currentTime = 0; return; } - + // unpause if paused - if(this.paused) + if (this.paused) { // adjust the start time - this._recalculateStartTime(this.pausedTime) + this._recalculateStartTime(this.pausedTime); this.pausedTime = undefined; } - if(!this.sendMIDIMessages) + if (!this.sendMIDIMessages) { - this.playingNotes.forEach(n => { + this.playingNotes.forEach(n => + { this.synth.noteOn(n.channel, n.midiNote, n.velocity, false, true); }); } @@ -283,7 +296,7 @@ export function setTimeTicks(ticks) ); const isNotFinished = this._playTo(0, ticks); this._recalculateStartTime(this.playedTime); - if(!isNotFinished) + if (!isNotFinished) { return; } diff --git a/src/spessasynth_lib/sequencer/worklet_sequencer/process_event.js b/src/spessasynth_lib/sequencer/worklet_sequencer/process_event.js index 540f7a16..97954b9f 100644 --- a/src/spessasynth_lib/sequencer/worklet_sequencer/process_event.js +++ b/src/spessasynth_lib/sequencer/worklet_sequencer/process_event.js @@ -1,9 +1,9 @@ -import { getEvent, messageTypes } from '../../midi_parser/midi_message.js' -import { WorkletSequencerReturnMessageType } from './sequencer_message.js' -import { consoleColors } from '../../utils/other.js' -import { SpessaSynthWarn } from '../../utils/loggin.js' -import { readBytesAsUintBigEndian } from '../../utils/byte_functions/big_endian.js' -import { DEFAULT_PERCUSSION } from '../../synthetizer/synthetizer.js' +import { getEvent, messageTypes } from "../../midi_parser/midi_message.js"; +import { WorkletSequencerReturnMessageType } from "./sequencer_message.js"; +import { consoleColors } from "../../utils/other.js"; +import { SpessaSynthWarn } from "../../utils/loggin.js"; +import { readBytesAsUintBigEndian } from "../../utils/byte_functions/big_endian.js"; +import { DEFAULT_PERCUSSION } from "../../synthetizer/synthetizer.js"; /** * Processes a single event @@ -14,23 +14,27 @@ import { DEFAULT_PERCUSSION } from '../../synthetizer/synthetizer.js' */ export function _processEvent(event, trackIndex) { - if(this.ignoreEvents) return; - if(this.sendMIDIMessages) + if (this.ignoreEvents) { - if(event.messageStatusByte >= 0x80) + return; + } + if (this.sendMIDIMessages) + { + if (event.messageStatusByte >= 0x80) { this.sendMIDIMessage([event.messageStatusByte, ...event.messageData]); return; } } const statusByteData = getEvent(event.messageStatusByte); - const offset = this.midiPortChannelOffsets[this.midiPorts[trackIndex]] || 0 + const offset = this.midiPortChannelOffsets[this.midiPorts[trackIndex]] || 0; statusByteData.channel += offset; // process the event - switch (statusByteData.status) { + switch (statusByteData.status) + { case messageTypes.noteOn: const velocity = event.messageData[1]; - if(velocity > 0) + if (velocity > 0) { this.synth.noteOn(statusByteData.channel, event.messageData[0], velocity); this.playingNotes.push({ @@ -44,56 +48,56 @@ export function _processEvent(event, trackIndex) this.synth.noteOff(statusByteData.channel, event.messageData[0]); const toDelete = this.playingNotes.findIndex(n => n.midiNote === event.messageData[0] && n.channel === statusByteData.channel); - if(toDelete !== -1) + if (toDelete !== -1) { this.playingNotes.splice(toDelete, 1); } } break; - + case messageTypes.noteOff: this.synth.noteOff(statusByteData.channel, event.messageData[0]); const toDelete = this.playingNotes.findIndex(n => n.midiNote === event.messageData[0] && n.channel === statusByteData.channel); - if(toDelete !== -1) + if (toDelete !== -1) { this.playingNotes.splice(toDelete, 1); } break; - + case messageTypes.pitchBend: this.synth.pitchWheel(statusByteData.channel, event.messageData[1], event.messageData[0]); break; - + case messageTypes.controllerChange: this.synth.controllerChange(statusByteData.channel, event.messageData[0], event.messageData[1]); break; - + case messageTypes.programChange: this.synth.programChange(statusByteData.channel, event.messageData[0]); break; - + case messageTypes.polyPressure: this.synth.polyPressure(statusByteData.channel, event.messageData[0], event.messageData[1]); break; - + case messageTypes.channelPressure: this.synth.channelPressure(statusByteData.channel, event.messageData[0]); break; - + case messageTypes.systemExclusive: this.synth.systemExclusive(event.messageData, offset); break; - + case messageTypes.setTempo: this.oneTickToSeconds = 60 / (getTempo(event) * this.midiData.timeDivision); - if(this.oneTickToSeconds === 0) + if (this.oneTickToSeconds === 0) { this.oneTickToSeconds = 60 / (120 * this.midiData.timeDivision); SpessaSynthWarn("invalid tempo! falling back to 120 BPM"); } break; - + // recongized but ignored case messageTypes.timeSignature: case messageTypes.endOfTrack: @@ -104,7 +108,7 @@ export function _processEvent(event, trackIndex) case messageTypes.sequenceNumber: case messageTypes.sequenceSpecific: break; - + case messageTypes.text: case messageTypes.lyric: case messageTypes.copyright: @@ -113,24 +117,27 @@ export function _processEvent(event, trackIndex) case messageTypes.cuePoint: case messageTypes.instrumentName: case messageTypes.programName: - this.post(WorkletSequencerReturnMessageType.textEvent, [event.messageData, statusByteData.status]) + this.post(WorkletSequencerReturnMessageType.textEvent, [event.messageData, statusByteData.status]); break; - + case messageTypes.midiPort: this.assignMIDIPort(trackIndex, event.messageData[0]); break; - + case messageTypes.reset: this.synth.stopAllChannels(); this.synth.resetAllControllers(); break; - + default: - SpessaSynthWarn(`%cUnrecognized Event: %c${event.messageStatusByte}%c status byte: %c${Object.keys(messageTypes).find(k => messageTypes[k] === statusByteData.status)}`, + SpessaSynthWarn( + `%cUnrecognized Event: %c${event.messageStatusByte}%c status byte: %c${Object.keys( + messageTypes).find(k => messageTypes[k] === statusByteData.status)}`, consoleColors.warn, consoleColors.unrecognized, consoleColors.warn, - consoleColors.value); + consoleColors.value + ); break; } } @@ -145,7 +152,7 @@ export function _addNewMidiPort() for (let i = 0; i < 16; i++) { this.synth.createWorkletChannel(true); - if(i === DEFAULT_PERCUSSION) + if (i === DEFAULT_PERCUSSION) { this.synth.setDrums(this.synth.workletProcessorChannels.length - 1, true); } diff --git a/src/spessasynth_lib/sequencer/worklet_sequencer/process_tick.js b/src/spessasynth_lib/sequencer/worklet_sequencer/process_tick.js index 27af453b..8c835988 100644 --- a/src/spessasynth_lib/sequencer/worklet_sequencer/process_tick.js +++ b/src/spessasynth_lib/sequencer/worklet_sequencer/process_tick.js @@ -6,28 +6,28 @@ export function _processTick() { let current = this.currentTime; - while(this.playedTime < current) + while (this.playedTime < current) { // find next event let trackIndex = this._findFirstEventIndex(); let event = this.tracks[trackIndex][this.eventIndex[trackIndex]]; this._processEvent(event, trackIndex); - + this.eventIndex[trackIndex]++; - + // find next event trackIndex = this._findFirstEventIndex(); - if(this.tracks[trackIndex].length <= this.eventIndex[trackIndex]) + if (this.tracks[trackIndex].length <= this.eventIndex[trackIndex]) { // song has ended - if(this.loop) + if (this.loop) { this.setTimeTicks(this.midiData.loop.start); return; } this.eventIndex[trackIndex]--; this.pause(true); - if(this.songs.length > 1) + if (this.songs.length > 1) { this.nextSong(); } @@ -35,18 +35,18 @@ export function _processTick() } let eventNext = this.tracks[trackIndex][this.eventIndex[trackIndex]]; this.playedTime += this.oneTickToSeconds * (eventNext.ticks - event.ticks); - + // loop - if((this.midiData.loop.end <= event.ticks) && this.loop && this.currentLoopCount > 0) + if ((this.midiData.loop.end <= event.ticks) && this.loop && this.currentLoopCount > 0) { this.currentLoopCount--; this.setTimeTicks(this.midiData.loop.start); return; } // if song has ended - else if(current >= this.duration) + else if (current >= this.duration) { - if(this.loop && this.currentLoopCount > 0) + if (this.loop && this.currentLoopCount > 0) { this.currentLoopCount--; this.setTimeTicks(this.midiData.loop.start); @@ -54,7 +54,7 @@ export function _processTick() } this.eventIndex[trackIndex]--; this.pause(true); - if(this.songs.length > 1) + if (this.songs.length > 1) { this.nextSong(); } @@ -72,12 +72,13 @@ export function _findFirstEventIndex() { let index = 0; let ticks = Infinity; - this.tracks.forEach((track, i) => { - if(this.eventIndex[i] >= track.length) + this.tracks.forEach((track, i) => + { + if (this.eventIndex[i] >= track.length) { return; } - if(track[this.eventIndex[i]].ticks < ticks) + if (track[this.eventIndex[i]].ticks < ticks) { index = i; ticks = track[this.eventIndex[i]].ticks; diff --git a/src/spessasynth_lib/sequencer/worklet_sequencer/sequencer_message.js b/src/spessasynth_lib/sequencer/worklet_sequencer/sequencer_message.js index ce6bf10b..12579329 100644 --- a/src/spessasynth_lib/sequencer/worklet_sequencer/sequencer_message.js +++ b/src/spessasynth_lib/sequencer/worklet_sequencer/sequencer_message.js @@ -24,7 +24,7 @@ export const WorkletSequencerMessageType = { changeSong: 8, getMIDI: 9, setSkipToFirstNote: 10 -} +}; /** * @@ -37,5 +37,5 @@ export const WorkletSequencerReturnMessageType = { timeChange: 3, // newAbsoluteTime pause: 4, // no data getMIDI: 5, // midiData - midiError: 6, // errorMSG -} \ No newline at end of file + midiError: 6 // errorMSG +}; \ No newline at end of file diff --git a/src/spessasynth_lib/sequencer/worklet_sequencer/song_control.js b/src/spessasynth_lib/sequencer/worklet_sequencer/song_control.js index 269edaaa..4bac886f 100644 --- a/src/spessasynth_lib/sequencer/worklet_sequencer/song_control.js +++ b/src/spessasynth_lib/sequencer/worklet_sequencer/song_control.js @@ -1,10 +1,15 @@ -import { WorkletSequencerReturnMessageType } from './sequencer_message.js' -import { consoleColors, formatTime } from '../../utils/other.js' -import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn } from '../../utils/loggin.js' -import { MidiData } from '../../midi_parser/midi_data.js' -import { MIDI } from '../../midi_parser/midi_loader.js' -import { getUsedProgramsAndKeys } from '../../midi_parser/used_keys_loaded.js' -import { MIDIticksToSeconds } from '../../midi_parser/basic_midi.js' +import { WorkletSequencerReturnMessageType } from "./sequencer_message.js"; +import { consoleColors, formatTime } from "../../utils/other.js"; +import { + SpessaSynthGroupCollapsed, + SpessaSynthGroupEnd, + SpessaSynthInfo, + SpessaSynthWarn +} from "../../utils/loggin.js"; +import { MidiData } from "../../midi_parser/midi_data.js"; +import { MIDI } from "../../midi_parser/midi_loader.js"; +import { getUsedProgramsAndKeys } from "../../midi_parser/used_keys_loaded.js"; +import { MIDIticksToSeconds } from "../../midi_parser/basic_midi.js"; /** * @param trackNum {number} @@ -14,22 +19,22 @@ import { MIDIticksToSeconds } from '../../midi_parser/basic_midi.js' export function assignMIDIPort(trackNum, port) { // assign new 16 channels if the port is not occupied yet - if(this.midiPortChannelOffset === 0) + if (this.midiPortChannelOffset === 0) { this.midiPortChannelOffset += 16; this.midiPortChannelOffsets[port] = 0; } - - if(this.midiPortChannelOffsets[port] === undefined) + + if (this.midiPortChannelOffsets[port] === undefined) { - if(this.synth.workletProcessorChannels.length < this.midiPortChannelOffset + 15) + if (this.synth.workletProcessorChannels.length < this.midiPortChannelOffset + 15) { this._addNewMidiPort(); } this.midiPortChannelOffsets[port] = this.midiPortChannelOffset; this.midiPortChannelOffset += 16; } - + this.midiPorts[trackNum] = port; } @@ -45,25 +50,25 @@ export function loadNewSequence(parsedMidi) { throw "No tracks supplied!"; } - - this.oneTickToSeconds = 60 / (120 * parsedMidi.timeDivision) - + + this.oneTickToSeconds = 60 / (120 * parsedMidi.timeDivision); + /** * @type {BasicMIDI} */ this.midiData = parsedMidi; - + this.currentLoopCount = this.loopCount; - + // check for embedded soundfont - if(this.midiData.embeddedSoundFont !== undefined) + if (this.midiData.embeddedSoundFont !== undefined) { SpessaSynthInfo("%cEmbedded soundfont detected! Using it.", consoleColors.recognized); this.synth.setEmbeddedSoundFont(this.midiData.embeddedSoundFont, this.midiData.bankOffset); } else { - if(this.synth.overrideSoundfont) + if (this.synth.overrideSoundfont) { // clean up the embedded soundfont this.synth.clearSoundFont(true, true); @@ -76,35 +81,39 @@ export function loadNewSequence(parsedMidi) const bank = parseInt(programBank.split(":")[0]); const program = parseInt(programBank.split(":")[1]); const preset = this.synth.getPreset(bank, program); - SpessaSynthInfo(`%cPreloading used samples on %c${preset.presetName}%c...`, + SpessaSynthInfo( + `%cPreloading used samples on %c${preset.presetName}%c...`, consoleColors.info, consoleColors.recognized, - consoleColors.info) - for (const combo of combos) { + consoleColors.info + ); + for (const combo of combos) + { const split = combo.split("-"); preset.preloadSpecific(parseInt(split[0]), parseInt(split[1])); } } SpessaSynthGroupEnd(); } - + /** * the midi track data * @type {MidiMessage[][]} */ this.tracks = this.midiData.tracks; - + // copy over the port data this.midiPorts = this.midiData.midiPorts; - + // clear last port data this.midiPortChannelOffset = 0; this.midiPortChannelOffsets = {}; // assign port offsets - this.midiData.midiPorts.forEach((port, trackIndex) => { + this.midiData.midiPorts.forEach((port, trackIndex) => + { this.assignMIDIPort(trackIndex, port); }); - + /** * Same as Audio.duration (seconds) * @type {number} @@ -112,14 +121,16 @@ export function loadNewSequence(parsedMidi) this.duration = this.midiData.duration; this.firstNoteTime = MIDIticksToSeconds(this.midiData.firstNoteOn, this.midiData); SpessaSynthInfo(`%cTotal song time: ${formatTime(Math.ceil(this.duration)).time}`, consoleColors.recognized); - + this.post(WorkletSequencerReturnMessageType.songChange, [new MidiData(this.midiData), this.songIndex]); - + this.synth.resetAllControllers(); - if(this.duration <= 1) + if (this.duration <= 1) { - SpessaSynthWarn(`%cVery short song: (${formatTime(Math.round(this.duration)).time}). Disabling loop!`, - consoleColors.warn); + SpessaSynthWarn( + `%cVery short song: (${formatTime(Math.round(this.duration)).time}). Disabling loop!`, + consoleColors.warn + ); this.loop = false; } this.play(true); @@ -135,8 +146,9 @@ export function loadNewSongList(midiBuffers) * parse the MIDIs (only the array buffers, MIDI is unchanged) * @type {BasicMIDI[]} */ - this.songs = midiBuffers.reduce((mids, b) => { - if(b.duration) + this.songs = midiBuffers.reduce((mids, b) => + { + if (b.duration) { mids.push(b); return mids; @@ -144,20 +156,19 @@ export function loadNewSongList(midiBuffers) try { mids.push(new MIDI(b.binary, b.altName || "")); - } - catch (e) + } catch (e) { this.post(WorkletSequencerReturnMessageType.midiError, e.message); return mids; } return mids; }, []); - if(this.songs.length < 1) + if (this.songs.length < 1) { return; } this.songIndex = 0; - if(this.songs.length > 1) + if (this.songs.length > 1) { this.loop = false; } @@ -169,7 +180,7 @@ export function loadNewSongList(midiBuffers) */ export function nextSong() { - if(this.songs.length === 1) + if (this.songs.length === 1) { this.currentTime = 0; return; @@ -184,13 +195,13 @@ export function nextSong() */ export function previousSong() { - if(this.songs.length === 1) + if (this.songs.length === 1) { this.currentTime = 0; return; } this.songIndex--; - if(this.songIndex < 0) + if (this.songIndex < 0) { this.songIndex = this.songs.length - 1; } diff --git a/src/spessasynth_lib/sequencer/worklet_sequencer/worklet_sequencer.js b/src/spessasynth_lib/sequencer/worklet_sequencer/worklet_sequencer.js index ac81e844..a207972f 100644 --- a/src/spessasynth_lib/sequencer/worklet_sequencer/worklet_sequencer.js +++ b/src/spessasynth_lib/sequencer/worklet_sequencer/worklet_sequencer.js @@ -1,9 +1,9 @@ -import { WorkletSequencerReturnMessageType } from './sequencer_message.js' -import { _addNewMidiPort, _processEvent } from './process_event.js' -import { _findFirstEventIndex, _processTick } from './process_tick.js' -import { assignMIDIPort, loadNewSequence, loadNewSongList, nextSong, previousSong } from './song_control.js' -import { _playTo, _recalculateStartTime, play, setTimeTicks } from './play.js' -import { messageTypes, midiControllers } from '../../midi_parser/midi_message.js' +import { WorkletSequencerReturnMessageType } from "./sequencer_message.js"; +import { _addNewMidiPort, _processEvent } from "./process_event.js"; +import { _findFirstEventIndex, _processTick } from "./process_tick.js"; +import { assignMIDIPort, loadNewSequence, loadNewSongList, nextSong, previousSong } from "./song_control.js"; +import { _playTo, _recalculateStartTime, play, setTimeTicks } from "./play.js"; +import { messageTypes, midiControllers } from "../../midi_parser/midi_message.js"; import { post, processMessage, @@ -11,10 +11,10 @@ import { sendMIDIMessage, sendMIDIPitchWheel, sendMIDIProgramChange, - sendMIDIReset, -} from './events.js' -import { SpessaSynthWarn } from '../../utils/loggin.js' -import { MIDI_CHANNEL_COUNT } from '../../synthetizer/synthetizer.js' + sendMIDIReset +} from "./events.js"; +import { SpessaSynthWarn } from "../../utils/loggin.js"; +import { MIDI_CHANNEL_COUNT } from "../../synthetizer/synthetizer.js"; class WorkletSequencer { @@ -25,47 +25,47 @@ class WorkletSequencer { this.synth = spessasynthProcessor; this.ignoreEvents = false; - + /** * If the event should instead be sent back to the main thread instead of synth * @type {boolean} */ this.sendMIDIMessages = false; - + this.loopCount = Infinity; this.currentLoopCount = this.loopCount; - + // event's number in this.events /** * @type {number[]} */ this.eventIndex = []; this.songIndex = 0; - + // tracks the time that we have already played /** * @type {number} */ this.playedTime = 0; - + /** * The (relative) time when the sequencer was paused. If it's not paused then it's undefined. * @type {number} */ this.pausedTime = undefined; - + /** * Absolute playback startTime, bases on the synth's time * @type {number} */ this.absoluteStartTime = currentTime; - + /** * Controls the playback's rate * @type {number} */ this._playbackRate = 1; - + /** * Currently playing notes (for pausing and resuming) * @type {{ @@ -75,37 +75,37 @@ class WorkletSequencer * }[]} */ this.playingNotes = []; - + // controls if the sequencer loops (defaults to true) this.loop = true; - + /** * the current track data * @type {BasicMIDI} */ this.midiData = undefined; - + /** * midi port number for the corresponding track * @type {number[]} */ this.midiPorts = []; - + this.midiPortChannelOffset = 0; - + /** * midi port: channel offset * @type {Object} */ this.midiPortChannelOffsets = {}; - + /** * @type {boolean} * @private */ this._skipToFirstNoteOn = true; } - + /** * @param value {number} */ @@ -115,24 +115,24 @@ class WorkletSequencer this._playbackRate = value; this.currentTime = time; } - + get currentTime() { // return the paused time if it's set to something other than undefined - if(this.pausedTime) + if (this.pausedTime) { return this.pausedTime; } - + return (currentTime - this.absoluteStartTime) * this._playbackRate; } - + set currentTime(time) { - if(time > this.duration || time < 0) + if (time > this.duration || time < 0) { // time is 0 - if(this._skipToFirstNoteOn) + if (this._skipToFirstNoteOn) { this.setTimeTicks(this.midiData.firstNoteOn - 1); } @@ -142,9 +142,9 @@ class WorkletSequencer } return; } - if(this._skipToFirstNoteOn) + if (this._skipToFirstNoteOn) { - if(time < this.firstNoteTime) + if (time < this.firstNoteTime) { this.setTimeTicks(this.midiData.firstNoteOn - 1); return; @@ -156,20 +156,29 @@ class WorkletSequencer this.post(WorkletSequencerReturnMessageType.timeChange, currentTime - time); const isNotFinished = this._playTo(time); this._recalculateStartTime(time); - if(!isNotFinished) + if (!isNotFinished) { return; } this.play(); } - + + /** + * true if paused, false if playing or stopped + * @returns {boolean} + */ + get paused() + { + return this.pausedTime !== undefined; + } + /** * Pauses the playback * @param isFinished {boolean} */ pause(isFinished = false) { - if(this.paused) + if (this.paused) { SpessaSynthWarn("Already paused"); return; @@ -178,22 +187,22 @@ class WorkletSequencer this.stop(); this.post(WorkletSequencerReturnMessageType.pause, isFinished); } - + /** * Stops the playback */ stop() { - this.clearProcessHandler() + this.clearProcessHandler(); // disable sustain for (let i = 0; i < 16; i++) { this.synth.controllerChange(i, midiControllers.sustainPedal, 0); } this.synth.stopAllChannels(); - if(this.sendMIDIMessages) + if (this.sendMIDIMessages) { - for(let note of this.playingNotes) + for (let note of this.playingNotes) { this.sendMIDIMessage([messageTypes.noteOff | (note.channel % 16), note.midiNote]); } @@ -204,27 +213,18 @@ class WorkletSequencer } } } - + _resetTimers() { - this.playedTime = 0 + this.playedTime = 0; this.eventIndex = Array(this.tracks.length).fill(0); } - - /** - * true if paused, false if playing or stopped - * @returns {boolean} - */ - get paused() - { - return this.pausedTime !== undefined; - } - + setProcessHandler() { this.synth.processTickCallback = this._processTick.bind(this); } - + clearProcessHandler() { this.synth.processTickCallback = undefined; @@ -257,4 +257,4 @@ WorkletSequencer.prototype._playTo = _playTo; WorkletSequencer.prototype.setTimeTicks = setTimeTicks; WorkletSequencer.prototype._recalculateStartTime = _recalculateStartTime; -export { WorkletSequencer } \ No newline at end of file +export { WorkletSequencer }; \ No newline at end of file diff --git a/src/spessasynth_lib/soundfont/README.md b/src/spessasynth_lib/soundfont/README.md index 36c64687..461416c8 100644 --- a/src/spessasynth_lib/soundfont/README.md +++ b/src/spessasynth_lib/soundfont/README.md @@ -1,6 +1,7 @@ ## This is the SoundFont2 parsing library. + The code here is responsible for parsing the SoundFont2 file and -providing an easy way to get the data out. +providing an easy way to get the data out. Default modulators are also stored here (in `modulators.js`) `basic_soundfont` folder contains the classes that represent the soundfont file. diff --git a/src/spessasynth_lib/soundfont/basic_soundfont/basic_instrument.js b/src/spessasynth_lib/soundfont/basic_soundfont/basic_instrument.js index 279b26f3..f3ff5d4b 100644 --- a/src/spessasynth_lib/soundfont/basic_soundfont/basic_instrument.js +++ b/src/spessasynth_lib/soundfont/basic_soundfont/basic_instrument.js @@ -14,39 +14,39 @@ export class BasicInstrument this.instrumentZones = []; this._useCount = 0; } - + + /** + * @returns {number} + */ + get useCount() + { + return this._useCount; + } + addUseCount() { this._useCount++; this.instrumentZones.forEach(z => z.useCount++); } - + removeUseCount() { this._useCount--; - for(let i = 0; i < this.instrumentZones.length; i++) + for (let i = 0; i < this.instrumentZones.length; i++) { - if(this.safeDeleteZone(i)) + if (this.safeDeleteZone(i)) { i--; } } } - - /** - * @returns {number} - */ - get useCount() - { - return this._useCount; - } - + deleteInstrument() { this.instrumentZones.forEach(z => z.deleteZone()); this.instrumentZones.length = 0; } - + /** * @param index {number} * @returns {boolean} is the zone has been deleted @@ -54,14 +54,14 @@ export class BasicInstrument safeDeleteZone(index) { this.instrumentZones[index].useCount--; - if(this.instrumentZones[index].useCount < 1) + if (this.instrumentZones[index].useCount < 1) { this.deleteZone(index); return true; } return false; } - + /** * @param index {number} */ diff --git a/src/spessasynth_lib/soundfont/basic_soundfont/basic_preset.js b/src/spessasynth_lib/soundfont/basic_soundfont/basic_preset.js index dbccf811..6db76b05 100644 --- a/src/spessasynth_lib/soundfont/basic_soundfont/basic_preset.js +++ b/src/spessasynth_lib/soundfont/basic_soundfont/basic_preset.js @@ -32,29 +32,29 @@ export class BasicPreset * @type {number} */ this.bank = 0; - + /** * The preset's zones * @type {BasicPresetZone[]} */ this.presetZones = []; - + /** * SampleID offset for this preset * @type {number} */ this.sampleIDOffset = 0; - + /** * Stores already found getSamplesAndGenerators for reuse * @type {SampleAndGenerators[][][]} */ this.foundSamplesAndGenerators = []; - for(let i = 0; i < 128; i++) + for (let i = 0; i < 128; i++) { this.foundSamplesAndGenerators[i] = []; } - + /** * unused metadata * @type {number} @@ -70,20 +70,20 @@ export class BasicPreset * @type {number} */ this.morphology = 0; - + /** * Default modulators * @type {Modulator[]} */ this.defaultModulators = modulators; } - + deletePreset() { this.presetZones.forEach(z => z.deleteZone()); this.presetZones.length = 0; } - + /** * @param index {number} */ @@ -92,7 +92,7 @@ export class BasicPreset this.presetZones[index].deleteZone(); this.presetZones.splice(index, 1); } - + /** * Preloads all samples (async) */ @@ -102,16 +102,17 @@ export class BasicPreset { for (let velocity = 0; velocity < 128; velocity++) { - this.getSamplesAndGenerators(key, velocity).forEach(samandgen => { - if(!samandgen.sample.isSampleLoaded) + this.getSamplesAndGenerators(key, velocity).forEach(samandgen => + { + if (!samandgen.sample.isSampleLoaded) { samandgen.sample.getAudioData(); } - }) + }); } } } - + /** * Preloads a specific key/velocity combo * @param key {number} @@ -119,14 +120,15 @@ export class BasicPreset */ preloadSpecific(key, velocity) { - this.getSamplesAndGenerators(key, velocity).forEach(samandgen => { - if(!samandgen.sample.isSampleLoaded) + this.getSamplesAndGenerators(key, velocity).forEach(samandgen => + { + if (!samandgen.sample.isSampleLoaded) { samandgen.sample.getAudioData(); } - }) + }); } - + /** * Returns generatorTranslator and generators for given note * @param midiNote {number} @@ -136,21 +138,21 @@ export class BasicPreset getSamplesAndGenerators(midiNote, velocity) { const memorized = this.foundSamplesAndGenerators[midiNote][velocity]; - if(memorized) + if (memorized) { return memorized; } - - if(this.presetZones.length < 1) + + if (this.presetZones.length < 1) { return []; } - + function isInRange(min, max, number) { return number >= min && number <= max; } - + /** * @param main {Generator[]} * @param adder {Generator[]} @@ -159,7 +161,7 @@ export class BasicPreset { main.push(...adder.filter(g => !main.find(mg => mg.generatorType === g.generatorType))); } - + /** * @param main {Modulator[]} * @param adder {Modulator[]} @@ -168,31 +170,39 @@ export class BasicPreset { main.push(...adder.filter(m => !main.find(mm => Modulator.isIdentical(m, mm)))); } - + /** * @type {SampleAndGenerators[]} */ let parsedGeneratorsAndSamples = []; - + /** * global zone is always first, so it or nothing * @type {Generator[]} */ let globalPresetGenerators = this.presetZones[0].isGlobal ? [...this.presetZones[0].generators] : []; - + let globalPresetModulators = this.presetZones[0].isGlobal ? [...this.presetZones[0].modulators] : []; - + // find the preset zones in range let presetZonesInRange = this.presetZones.filter(currentZone => ( - isInRange(currentZone.keyRange.min, currentZone.keyRange.max, midiNote) + isInRange( + currentZone.keyRange.min, + currentZone.keyRange.max, + midiNote + ) && - isInRange(currentZone.velRange.min, currentZone.velRange.max, velocity) + isInRange( + currentZone.velRange.min, + currentZone.velRange.max, + velocity + ) ) && !currentZone.isGlobal); - + presetZonesInRange.forEach(zone => { - if(zone.instrument.instrumentZones.length < 1) + if (zone.instrument.instrumentZones.length < 1) { return; } @@ -204,70 +214,92 @@ export class BasicPreset */ let globalInstrumentGenerators = zone.instrument.instrumentZones[0].isGlobal ? [...zone.instrument.instrumentZones[0].generators] : []; let globalInstrumentModulators = zone.instrument.instrumentZones[0].isGlobal ? [...zone.instrument.instrumentZones[0].modulators] : []; - + let instrumentZonesInRange = zone.instrument.instrumentZones .filter(currentZone => ( - isInRange(currentZone.keyRange.min, + isInRange( + currentZone.keyRange.min, currentZone.keyRange.max, - midiNote) + midiNote + ) && - isInRange(currentZone.velRange.min, + isInRange( + currentZone.velRange.min, currentZone.velRange.max, - velocity) + velocity + ) ) && !currentZone.isGlobal ); - + instrumentZonesInRange.forEach(instrumentZone => { let instrumentGenerators = [...instrumentZone.generators]; let instrumentModulators = [...instrumentZone.modulators]; - - addUnique(presetGenerators, globalPresetGenerators); + + addUnique( + presetGenerators, + globalPresetGenerators + ); // add the unique global preset generators (local replace global( - - + + // add the unique global instrument generators (local replace global) - addUnique(instrumentGenerators, globalInstrumentGenerators); - - addUniqueMods(presetModulators, globalPresetModulators); - addUniqueMods(instrumentModulators, globalInstrumentModulators); - + addUnique( + instrumentGenerators, + globalInstrumentGenerators + ); + + addUniqueMods( + presetModulators, + globalPresetModulators + ); + addUniqueMods( + instrumentModulators, + globalInstrumentModulators + ); + // default mods - addUniqueMods(instrumentModulators, this.defaultModulators); - + addUniqueMods( + instrumentModulators, + this.defaultModulators + ); + /** * sum preset modulators to instruments (amount) sf spec page 54 * @type {Modulator[]} */ const finalModulatorList = [...instrumentModulators]; - for(let i = 0; i < presetModulators.length; i++) + for (let i = 0; i < presetModulators.length; i++) { let mod = presetModulators[i]; - const identicalInstrumentModulator = finalModulatorList.findIndex(m => Modulator.isIdentical(mod, m)); - if(identicalInstrumentModulator !== -1) + const identicalInstrumentModulator = finalModulatorList.findIndex( + m => Modulator.isIdentical(mod, m)); + if (identicalInstrumentModulator !== -1) { // sum the amounts (this makes a new modulator because otherwise it would overwrite the one in the soundfont!!! - finalModulatorList[identicalInstrumentModulator] = finalModulatorList[identicalInstrumentModulator].sumTransform(mod); + finalModulatorList[identicalInstrumentModulator] = finalModulatorList[identicalInstrumentModulator].sumTransform( + mod); } else { finalModulatorList.push(mod); } } - - + + // combine both generators and add to the final result parsedGeneratorsAndSamples.push({ instrumentGenerators: instrumentGenerators, presetGenerators: presetGenerators, modulators: finalModulatorList, sample: instrumentZone.sample, - sampleID: instrumentZone.generators.find(g => g.generatorType === generatorTypes.sampleID).generatorValue + sampleID: instrumentZone.generators.find( + g => g.generatorType === generatorTypes.sampleID).generatorValue }); }); }); - + // save and return this.foundSamplesAndGenerators[midiNote][velocity] = parsedGeneratorsAndSamples; return parsedGeneratorsAndSamples; diff --git a/src/spessasynth_lib/soundfont/basic_soundfont/basic_sample.js b/src/spessasynth_lib/soundfont/basic_soundfont/basic_sample.js index 04c5cea5..e6cd6fe4 100644 --- a/src/spessasynth_lib/soundfont/basic_soundfont/basic_sample.js +++ b/src/spessasynth_lib/soundfont/basic_soundfont/basic_sample.js @@ -3,9 +3,10 @@ * purpose: parses soundfont samples, resamples if needed. * loads sample data, handles async loading of sf3 compressed samples */ -import { SpessaSynthWarn } from '../../utils/loggin.js' +import { SpessaSynthWarn } from "../../utils/loggin.js"; -export class BasicSample { +export class BasicSample +{ /** * The basic representation of a soundfont sample * @param sampleName {string} @@ -25,108 +26,114 @@ export class BasicSample { sampleLink, sampleType, loopStart, - loopEnd, - ) { + loopEnd + ) + { /** * Sample's name * @type {string} */ - this.sampleName = sampleName + this.sampleName = sampleName; /** * Sample rate in Hz * @type {number} */ - this.sampleRate = sampleRate + this.sampleRate = sampleRate; /** * Original pitch of the sample as a MIDI note number * @type {number} */ - this.samplePitch = samplePitch + this.samplePitch = samplePitch; /** * Pitch correction, in cents. Can be negative * @type {number} */ - this.samplePitchCorrection = samplePitchCorrection + this.samplePitchCorrection = samplePitchCorrection; /** * Sample link, currently unused. * @type {number} */ - this.sampleLink = sampleLink + this.sampleLink = sampleLink; /** * Type of the sample, an enum * @type {number} */ - this.sampleType = sampleType + this.sampleType = sampleType; /** * Relative to start of the sample in sample points * @type {number} */ - this.sampleLoopStartIndex = loopStart + this.sampleLoopStartIndex = loopStart; /** * Relative to start of the sample in sample points * @type {number} */ this.sampleLoopEndIndex = loopEnd; - + /** * Indicates if the sample is compressed * @type {boolean} */ - this.isCompressed = (sampleType & 0x10) > 0 - + this.isCompressed = (sampleType & 0x10) > 0; + /** * The compressed sample data if it was compressed by spessasynth * @type {Uint8Array} */ this.compressedData = undefined; - + /** * The sample's use count * @type {number} */ this.useCount = 0; } - + /** * @returns {Uint8Array|IndexedByteArray} */ getRawData() { - const e = new Error('Not implemented') - e.name = 'NotImplementedError' - throw e + const e = new Error("Not implemented"); + e.name = "NotImplementedError"; + throw e; } - + /** * @param quality {number} * @param encodeVorbis {EncodeVorbisFunction} */ - compressSample(quality, encodeVorbis) { + compressSample(quality, encodeVorbis) + { // no need to compress - if (this.isCompressed) { - return + if (this.isCompressed) + { + return; } // compress, always mono! - try { - this.compressedData = encodeVorbis([this.getAudioData()], 1, this.sampleRate, quality) + try + { + this.compressedData = encodeVorbis([this.getAudioData()], 1, this.sampleRate, quality); // flag as compressed - this.sampleType |= 0x10 - this.isCompressed = true - } catch (e) { - SpessaSynthWarn(`Failed to compress ${this.sampleName}. Leaving as uncompressed!`) - this.isCompressed = false - this.compressedData = undefined - this.sampleType &= -17 + this.sampleType |= 0x10; + this.isCompressed = true; + } catch (e) + { + SpessaSynthWarn(`Failed to compress ${this.sampleName}. Leaving as uncompressed!`); + this.isCompressed = false; + this.compressedData = undefined; + this.sampleType &= -17; } - + } - + /** * @returns {Float32Array} */ - getAudioData() { - const e = new Error('Not implemented') - e.name = 'NotImplementedError' - throw e + getAudioData() + { + const e = new Error("Not implemented"); + e.name = "NotImplementedError"; + throw e; } } \ No newline at end of file diff --git a/src/spessasynth_lib/soundfont/basic_soundfont/basic_soundfont.js b/src/spessasynth_lib/soundfont/basic_soundfont/basic_soundfont.js index a74db49d..2eeff9a3 100644 --- a/src/spessasynth_lib/soundfont/basic_soundfont/basic_soundfont.js +++ b/src/spessasynth_lib/soundfont/basic_soundfont/basic_soundfont.js @@ -1,8 +1,7 @@ -import { SpessaSynthWarn } from '../../utils/loggin.js' -import { consoleColors } from '../../utils/other.js' -import { write } from './write_sf2/write.js' -import { defaultModulators } from "./modulator.js"; -import { Modulator } from "./modulator.js"; +import { SpessaSynthWarn } from "../../utils/loggin.js"; +import { consoleColors } from "../../utils/other.js"; +import { write } from "./write_sf2/write.js"; +import { defaultModulators, Modulator } from "./modulator.js"; class BasicSoundFont { @@ -17,56 +16,89 @@ class BasicSoundFont * @type {Object} */ this.soundFontInfo = {}; - + /** * The soundfont's presets * @type {BasicPreset[]} */ - this.presets = [] - + this.presets = []; + /** * The soundfont's samples * @type {BasicSample[]} */ this.samples = []; - + /** * The soundfont's instruments * @type {BasicInstrument[]} */ this.instruments = []; - + /** * Soundfont's default modulatorss * @type {Modulator[]} */ this.defaultModulators = defaultModulators.map(m => Modulator.copy(m)); - - if(data?.presets) + + if (data?.presets) { this.presets.push(...data.presets); this.soundFontInfo = data.info; } } - + + /** + * Merges soundfonts with the given order. Keep in mind that the info read is copied from the first one + * @param soundfonts {...BasicSoundFont} the soundfonts to merge, the first overwrites the last + * @returns {BasicSoundFont} + */ + static mergeSoundfonts(...soundfonts) + { + const mainSf = soundfonts.shift(); + const presets = mainSf.presets; + while (soundfonts.length) + { + const newPresets = soundfonts.shift().presets; + newPresets.forEach(newPreset => + { + if ( + presets.find(existingPreset => existingPreset.bank === newPreset.bank && existingPreset.program === newPreset.program) === undefined + ) + { + presets.push(newPreset); + } + }); + } + + return new BasicSoundFont({ presets: presets, info: mainSf.soundFontInfo }); + } + removeUnusedElements() { - this.instruments.forEach(i => { - if(i.useCount < 1) + this.instruments.forEach(i => + { + if (i.useCount < 1) { - i.instrumentZones.forEach(z => {if(!z.isGlobal) z.sample.useCount--}); + i.instrumentZones.forEach(z => + { + if (!z.isGlobal) + { + z.sample.useCount--; + } + }); } - }) + }); this.instruments = this.instruments.filter(i => i.useCount > 0); this.samples = this.samples.filter(s => s.useCount > 0); } - + /** * @param instrument {BasicInstrument} */ deleteInstrument(instrument) { - if(instrument.useCount > 0) + if (instrument.useCount > 0) { throw new Error(`Cannot delete an instrument that has ${instrument.useCount} usages.`); } @@ -74,30 +106,39 @@ class BasicSoundFont instrument.deleteInstrument(); this.removeUnusedElements(); } - + + /** + * @param preset {BasicPreset} + */ + deletePreset(preset) + { + preset.deletePreset(); + this.presets.splice(this.presets.indexOf(preset), 1); + this.removeUnusedElements(); + } + /** * @param sample {BasicSample} */ deleteSample(sample) { - if(sample.useCount > 0) + if (sample.useCount > 0) { throw new Error(`Cannot delete sample that has ${sample.useCount} usages.`); } this.samples.splice(this.samples.indexOf(sample), 1); this.removeUnusedElements(); } - + /** - * @param preset {BasicPreset} + * To avoid overlapping on multiple desfonts + * @param offset {number} */ - deletePreset(preset) + setSampleIDOffset(offset) { - preset.deletePreset(); - this.presets.splice(this.presets.indexOf(preset), 1); - this.removeUnusedElements(); + this.presets.forEach(p => p.sampleIDOffset = offset); } - + /** * Get the appropriate preset, undefined if not foun d * @param bankNr {number} @@ -107,32 +148,23 @@ class BasicSoundFont */ getPresetNoFallback(bankNr, programNr, fallbackToProgram = false) { - const p = this.presets.find(p => p.bank === bankNr && p.program === programNr); - if(p) + const p = this.presets.find(p => p.bank === bankNr && p.program === programNr); + if (p) { return p; } - if(fallbackToProgram === false) + if (fallbackToProgram === false) { return undefined; } - if(bankNr === 128) + if (bankNr === 128) { // any drum preset return this.presets.find(p => p.bank === 128); } return this.presets.find(p => p.program === programNr); } - - /** - * To avoid overlapping on multiple desfonts - * @param offset {number} - */ - setSampleIDOffset(offset) - { - this.presets.forEach(p => p.sampleIDOffset = offset); - } - + /** * Get the appropriate preset * @param bankNr {number} @@ -146,11 +178,11 @@ class BasicSoundFont if (!preset) { // no match... - if(bankNr === 128) + if (bankNr === 128) { // drum preset: find any preset with bank 128 preset = this.presets.find(p => p.bank === 128 && p.program === programNr); - if(!preset) + if (!preset) { preset = this.presets.find(p => p.bank === 128); } @@ -160,22 +192,24 @@ class BasicSoundFont // non drum preset: find any preset with the given program that is not a drum preset preset = this.presets.find(p => p.program === programNr && p.bank !== 128); } - if(preset) + if (preset) { - SpessaSynthWarn(`%cPreset ${bankNr}.${programNr} not found. Replaced with %c${preset.presetName} (${preset.bank}.${preset.program})`, + SpessaSynthWarn( + `%cPreset ${bankNr}.${programNr} not found. Replaced with %c${preset.presetName} (${preset.bank}.${preset.program})`, consoleColors.warn, - consoleColors.recognized); + consoleColors.recognized + ); } } // no preset, use the first one available - if(!preset) + if (!preset) { SpessaSynthWarn(`Preset ${programNr} not found. Defaulting to`, this.presets[0].presetName); preset = this.presets[0]; } return preset; } - + /** * gets preset by name * @param presetName {string} @@ -184,41 +218,15 @@ class BasicSoundFont getPresetByName(presetName) { let preset = this.presets.find(p => p.presetName === presetName); - if(!preset) + if (!preset) { SpessaSynthWarn("Preset not found. Defaulting to:", this.presets[0].presetName); preset = this.presets[0]; } return preset; } - - - /** - * Merges soundfonts with the given order. Keep in mind that the info read is copied from the first one - * @param soundfonts {...BasicSoundFont} the soundfonts to merge, the first overwrites the last - * @returns {BasicSoundFont} - */ - static mergeSoundfonts(...soundfonts) - { - const mainSf = soundfonts.shift(); - const presets = mainSf.presets; - while(soundfonts.length) - { - const newPresets = soundfonts.shift().presets; - newPresets.forEach(newPreset => { - if( - presets.find(existingPreset => existingPreset.bank === newPreset.bank && existingPreset.program === newPreset.program) === undefined - ) - { - presets.push(newPreset); - } - }) - } - - return new BasicSoundFont({presets: presets, info: mainSf.soundFontInfo}); - } } BasicSoundFont.prototype.write = write; -export { BasicSoundFont } \ No newline at end of file +export { BasicSoundFont }; \ No newline at end of file diff --git a/src/spessasynth_lib/soundfont/basic_soundfont/basic_zone.js b/src/spessasynth_lib/soundfont/basic_soundfont/basic_zone.js index 0eb28bca..86108a65 100644 --- a/src/spessasynth_lib/soundfont/basic_soundfont/basic_zone.js +++ b/src/spessasynth_lib/soundfont/basic_soundfont/basic_zone.js @@ -11,25 +11,25 @@ export class BasicZone * @type {SoundFontRange} */ velRange = { min: 0, max: 127 }; - + /** * The zone's key range * @type {SoundFontRange} */ keyRange = { min: 0, max: 127 }; - + /** * Indicates if the zone is global * @type {boolean} */ isGlobal = false; - + /** * The zone's generators * @type {Generator[]} */ generators = []; - + /** * The zone's modulators * @type {Modulator[]} diff --git a/src/spessasynth_lib/soundfont/basic_soundfont/basic_zones.js b/src/spessasynth_lib/soundfont/basic_soundfont/basic_zones.js index fa81a05c..747c9896 100644 --- a/src/spessasynth_lib/soundfont/basic_soundfont/basic_zones.js +++ b/src/spessasynth_lib/soundfont/basic_soundfont/basic_zones.js @@ -1,4 +1,4 @@ -import { BasicZone } from './basic_zone.js' +import { BasicZone } from "./basic_zone.js"; export class BasicInstrumentZone extends BasicZone { @@ -12,15 +12,15 @@ export class BasicInstrumentZone extends BasicZone * @type {number} */ useCount = 0; - + deleteZone() { - this.useCount-- + this.useCount--; if (this.isGlobal) { - return + return; } - this.sample.useCount-- + this.sample.useCount--; } } @@ -30,11 +30,11 @@ export class BasicPresetZone extends BasicZone * Zone's instrument. Undefined if global * @type {BasicInstrument|undefined} */ - instrument = undefined - + instrument = undefined; + deleteZone() { - if(this.isGlobal) + if (this.isGlobal) { return; } diff --git a/src/spessasynth_lib/soundfont/basic_soundfont/generator.js b/src/spessasynth_lib/soundfont/basic_soundfont/generator.js index 50a94d42..09f6ba52 100644 --- a/src/spessasynth_lib/soundfont/basic_soundfont/generator.js +++ b/src/spessasynth_lib/soundfont/basic_soundfont/generator.js @@ -140,6 +140,17 @@ generatorLimits[generatorTypes.overridingRootKey] = { min: 0 - 1, max: 127, def: export class Generator { + /** + * The generator's enum number + * @type {generatorTypes|number} + */ + generatorType = generatorTypes.INVALID; + /** + * The generator's 16-bit value + * @type {number} + */ + generatorValue = 0; + /** * Constructs a new generator * @param type {generatorTypes|number} @@ -159,17 +170,6 @@ export class Generator this.generatorValue = Math.max(lim.min, Math.min(lim.max, this.generatorValue)); } } - - /** - * The generator's enum number - * @type {generatorTypes|number} - */ - generatorType = generatorTypes.INVALID; - /** - * The generator's 16-bit value - * @type {number} - */ - generatorValue = 0; } /** @@ -190,16 +190,16 @@ export function addAndClampGenerator(generatorType, presetGens, instrumentGens) { presetValue = presetGen.generatorValue; } - + let instruGen = instrumentGens.find(g => g.generatorType === generatorType); let instruValue = limits.def; if (instruGen) { instruValue = instruGen.generatorValue; } - + let value = instruValue + presetValue; - + // special case, intial attenuation. // Shall get clamped in the volume envelope, // so the modulators can be affected by negative generators (the "Brass" patch was problematic...) @@ -207,6 +207,6 @@ export function addAndClampGenerator(generatorType, presetGens, instrumentGens) { return value; } - + return Math.max(limits.min, Math.min(limits.max, value)); } \ No newline at end of file diff --git a/src/spessasynth_lib/soundfont/basic_soundfont/modulator.js b/src/spessasynth_lib/soundfont/basic_soundfont/modulator.js index 475ed2b9..961e2e99 100644 --- a/src/spessasynth_lib/soundfont/basic_soundfont/modulator.js +++ b/src/spessasynth_lib/soundfont/basic_soundfont/modulator.js @@ -15,7 +15,7 @@ export const modulatorSources = { pitchWheel: 14, pitchWheelRange: 16, link: 127 - + }; export const modulatorCurveTypes = { linear: 0, @@ -26,6 +26,12 @@ export const modulatorCurveTypes = { export class Modulator { + /** + * The current computed value of this modulator + * @type {number} + */ + currentValue = 0; + /** * Creates a modulator * @param params {{srcEnum: number, secSrcEnum: number, dest: generatorTypes, amt: number, transform: number}} @@ -40,36 +46,30 @@ export class Modulator this.secondarySourceEnum = params.secSrcEnum; this.transformAmount = params.amt; this.transformType = params.transform; - - + + if (this.modulatorDestination > 58) { this.modulatorDestination = generatorTypes.INVALID; // flag as invalid (for linked ones) } - + // decode the source this.sourcePolarity = this.sourceEnum >> 9 & 1; this.sourceDirection = this.sourceEnum >> 8 & 1; this.sourceUsesCC = this.sourceEnum >> 7 & 1; this.sourceIndex = this.sourceEnum & 127; this.sourceCurveType = this.sourceEnum >> 10 & 3; - + // decode the secondary source this.secSrcPolarity = this.secondarySourceEnum >> 9 & 1; this.secSrcDirection = this.secondarySourceEnum >> 8 & 1; this.secSrcUsesCC = this.secondarySourceEnum >> 7 & 1; this.secSrcIndex = this.secondarySourceEnum & 127; this.secSrcCurveType = this.secondarySourceEnum >> 10 & 3; - + //this.precomputeModulatorTransform(); } - - /** - * The current computed value of this modulator - * @type {number} - */ - currentValue = 0; - + /** * @param modulator {Modulator} * @returns {Modulator} @@ -77,14 +77,14 @@ export class Modulator static copy(modulator) { return new Modulator({ - srcEnum: modulator.sourceEnum, - secSrcEnum: modulator.secondarySourceEnum, - transform: modulator.transformType, - amt: modulator.transformAmount, - dest: modulator.modulatorDestination - }); + srcEnum: modulator.sourceEnum, + secSrcEnum: modulator.secondarySourceEnum, + transform: modulator.transformType, + amt: modulator.transformAmount, + dest: modulator.modulatorDestination + }); } - + /** * @param mod1 {Modulator} * @param mod2 {Modulator} @@ -97,7 +97,7 @@ export class Modulator && (mod1.secondarySourceEnum === mod2.secondarySourceEnum) && (mod1.transformType === mod2.transformType); } - + /** * Sums transform and creates a NEW modulator * @param modulator {Modulator} @@ -106,14 +106,14 @@ export class Modulator sumTransform(modulator) { return new Modulator({ - srcEnum: this.sourceEnum, - secSrcEnum: this.secondarySourceEnum, - dest: this.modulatorDestination, - transform: this.transformType, - amt: this.transformAmount + modulator.transformAmount - }); + srcEnum: this.sourceEnum, + secSrcEnum: this.secondarySourceEnum, + dest: this.modulatorDestination, + transform: this.transformType, + amt: this.transformAmount + modulator.transformAmount + }); } - + /** * @returns {string} */ @@ -123,25 +123,27 @@ export class Modulator { return Object.keys(object).find(key => object[key] === value); } - + let sourceString = getKeyByValue(modulatorCurveTypes, this.sourceCurveType); sourceString += this.sourcePolarity === 0 ? " unipolar " : " bipolar "; sourceString += this.sourceDirection === 0 ? "forwards " : "backwards "; if (this.sourceUsesCC) { sourceString += getKeyByValue(midiControllers, this.sourceIndex); - } else + } + else { sourceString += getKeyByValue(modulatorSources, this.sourceIndex); } - + let secSrcString = getKeyByValue(modulatorCurveTypes, this.secSrcCurveType); secSrcString += this.secSrcPolarity === 0 ? " unipolar " : " bipolar "; secSrcString += this.secSrcCurveType === 0 ? "forwards " : "backwards "; if (this.secSrcUsesCC) { secSrcString += getKeyByValue(midiControllers, this.secSrcIndex); - } else + } + else { secSrcString += getKeyByValue(modulatorSources, this.secSrcIndex); } @@ -166,90 +168,116 @@ export function getModSourceEnum(curveType, polarity, direction, isCC, index) export const defaultModulators = [ // vel to attenuation new Modulator({ - srcEnum: getModSourceEnum(DEFAULT_ATTENUATION_MOD_CURVE_TYPE, 0, 1, 0, modulatorSources.noteOnVelocity), - dest: generatorTypes.initialAttenuation, - amt: DEFAULT_ATTENUATION_MOD_AMOUNT, - secSrcEnum: 0x0, - transform: 0}), - + srcEnum: getModSourceEnum( + DEFAULT_ATTENUATION_MOD_CURVE_TYPE, + 0, + 1, + 0, + modulatorSources.noteOnVelocity + ), + dest: generatorTypes.initialAttenuation, + amt: DEFAULT_ATTENUATION_MOD_AMOUNT, + secSrcEnum: 0x0, + transform: 0 + }), + // mod wheel to vibrato - new Modulator({srcEnum: 0x0081, dest: generatorTypes.vibLfoToPitch, amt: 50, secSrcEnum: 0x0, transform: 0}), - + new Modulator({ srcEnum: 0x0081, dest: generatorTypes.vibLfoToPitch, amt: 50, secSrcEnum: 0x0, transform: 0 }), + // vol to attenuation new Modulator({ - srcEnum: getModSourceEnum(DEFAULT_ATTENUATION_MOD_CURVE_TYPE, 0, 1, 1, midiControllers.mainVolume), - dest: generatorTypes.initialAttenuation, - amt: DEFAULT_ATTENUATION_MOD_AMOUNT, - secSrcEnum: 0x0, - transform: 0}), - + srcEnum: getModSourceEnum( + DEFAULT_ATTENUATION_MOD_CURVE_TYPE, + 0, + 1, + 1, + midiControllers.mainVolume + ), + dest: generatorTypes.initialAttenuation, + amt: DEFAULT_ATTENUATION_MOD_AMOUNT, + secSrcEnum: 0x0, + transform: 0 + }), + // channel pressure to vibrato - new Modulator({srcEnum: 0x000D, dest: generatorTypes.vibLfoToPitch, amt: 50, secSrcEnum: 0x0, transform: 0}), - + new Modulator({ srcEnum: 0x000D, dest: generatorTypes.vibLfoToPitch, amt: 50, secSrcEnum: 0x0, transform: 0 }), + // pitch wheel to tuning - new Modulator({srcEnum: 0x020E, dest: generatorTypes.fineTune, amt: 12700, secSrcEnum: 0x0010, transform: 0}), - + new Modulator({ srcEnum: 0x020E, dest: generatorTypes.fineTune, amt: 12700, secSrcEnum: 0x0010, transform: 0 }), + // pan to uhh, pan - new Modulator({srcEnum: 0x028A, dest: generatorTypes.pan, amt: 1000, secSrcEnum: 0x0, transform: 0}), - + new Modulator({ srcEnum: 0x028A, dest: generatorTypes.pan, amt: 1000, secSrcEnum: 0x0, transform: 0 }), + // expression to attenuation new Modulator({ - srcEnum: getModSourceEnum(DEFAULT_ATTENUATION_MOD_CURVE_TYPE, 0, 1, 1, midiControllers.expressionController), - dest: generatorTypes.initialAttenuation, - amt: DEFAULT_ATTENUATION_MOD_AMOUNT, - secSrcEnum: 0x0, - transform: 0 - }), - + srcEnum: getModSourceEnum( + DEFAULT_ATTENUATION_MOD_CURVE_TYPE, + 0, + 1, + 1, + midiControllers.expressionController + ), + dest: generatorTypes.initialAttenuation, + amt: DEFAULT_ATTENUATION_MOD_AMOUNT, + secSrcEnum: 0x0, + transform: 0 + }), + // reverb effects to send - new Modulator({srcEnum: 0x00DB, dest: generatorTypes.reverbEffectsSend, amt: 750, secSrcEnum: 0x0, transform: 0}), - + new Modulator({ srcEnum: 0x00DB, dest: generatorTypes.reverbEffectsSend, amt: 750, secSrcEnum: 0x0, transform: 0 }), + // chorus effects to send - new Modulator({srcEnum: 0x00DD, dest: generatorTypes.chorusEffectsSend, amt: 750, secSrcEnum: 0x0, transform: 0}), - + new Modulator({ srcEnum: 0x00DD, dest: generatorTypes.chorusEffectsSend, amt: 750, secSrcEnum: 0x0, transform: 0 }), + // custom modulators heck yeah // poly pressure to vibrato new Modulator({ - srcEnum: getModSourceEnum(modulatorCurveTypes.linear, 0, 0, 0, modulatorSources.polyPressure), - dest: generatorTypes.vibLfoToPitch, - amt: 50, - secSrcEnum: 0x0, - transform: 0 - }), - + srcEnum: getModSourceEnum(modulatorCurveTypes.linear, 0, 0, 0, modulatorSources.polyPressure), + dest: generatorTypes.vibLfoToPitch, + amt: 50, + secSrcEnum: 0x0, + transform: 0 + }), + // cc 92 (tremolo) to modLFO volume new Modulator({ - srcEnum: getModSourceEnum(modulatorCurveTypes.linear, 0, 0, 1, midiControllers.effects2Depth), /*linear forward unipolar cc 92 */ - dest: generatorTypes.modLfoToVolume, - amt: 24, - secSrcEnum: 0x0, // no controller - transform: 0 - }), - + srcEnum: getModSourceEnum(modulatorCurveTypes.linear, 0, 0, 1, midiControllers.effects2Depth), /*linear forward unipolar cc 92 */ + dest: generatorTypes.modLfoToVolume, + amt: 24, + secSrcEnum: 0x0, // no controller + transform: 0 + }), + // cc 72 (release time) to volEnv release new Modulator({ - srcEnum: getModSourceEnum(modulatorCurveTypes.linear, 1, 0, 1, midiControllers.releaseTime), // linear forward bipolar cc 72 - dest: generatorTypes.releaseVolEnv, - amt: 1200, - secSrcEnum: 0x0, // no controller - transform: 0 - }), - + srcEnum: getModSourceEnum(modulatorCurveTypes.linear, 1, 0, 1, midiControllers.releaseTime), // linear forward bipolar cc 72 + dest: generatorTypes.releaseVolEnv, + amt: 1200, + secSrcEnum: 0x0, // no controller + transform: 0 + }), + // cc 74 (brightness) to filterFc new Modulator({ - srcEnum: getModSourceEnum(modulatorCurveTypes.linear, 1, 0 , 1, midiControllers.brightness), // linear forwards bipolar cc 74 - dest: generatorTypes.initialFilterFc, - amt: 4000, - secSrcEnum: 0x0, // no controller - transform: 0 - }), - + srcEnum: getModSourceEnum(modulatorCurveTypes.linear, 1, 0, 1, midiControllers.brightness), // linear forwards bipolar cc 74 + dest: generatorTypes.initialFilterFc, + amt: 4000, + secSrcEnum: 0x0, // no controller + transform: 0 + }), + // cc 71 (filter q) to filterq new Modulator({ - srcEnum: getModSourceEnum(modulatorCurveTypes.linear, 1, 0 , 1, midiControllers.timbreHarmonicContent), // linear forwards bipolar cc 74 - dest: generatorTypes.initialFilterQ, - amt: 250, - secSrcEnum: 0x0, // no controller - transform: 0 - }) + srcEnum: getModSourceEnum( + modulatorCurveTypes.linear, + 1, + 0, + 1, + midiControllers.timbreHarmonicContent + ), // linear forwards bipolar cc 74 + dest: generatorTypes.initialFilterQ, + amt: 250, + secSrcEnum: 0x0, // no controller + transform: 0 + }) ]; \ No newline at end of file diff --git a/src/spessasynth_lib/soundfont/basic_soundfont/riff_chunk.js b/src/spessasynth_lib/soundfont/basic_soundfont/riff_chunk.js index 52c9c898..8669050f 100644 --- a/src/spessasynth_lib/soundfont/basic_soundfont/riff_chunk.js +++ b/src/spessasynth_lib/soundfont/basic_soundfont/riff_chunk.js @@ -1,6 +1,6 @@ -import { IndexedByteArray } from '../../utils/indexed_array.js' -import { readLittleEndian, writeDword } from '../../utils/byte_functions/little_endian.js' -import { readBytesAsString, writeStringAsBytes } from '../../utils/byte_functions/string.js' +import { IndexedByteArray } from "../../utils/indexed_array.js"; +import { readLittleEndian, writeDword } from "../../utils/byte_functions/little_endian.js"; +import { readBytesAsString, writeStringAsBytes } from "../../utils/byte_functions/string.js"; /** * riff_chunk.js @@ -16,12 +16,13 @@ export class RiffChunk * @param size {number} * @param data {IndexedByteArray} */ - constructor(header, size, data) { + constructor(header, size, data) + { this.header = header; this.size = size; this.chunkData = data; } - + } /** @@ -30,29 +31,30 @@ export class RiffChunk * @param forceShift {boolean} * @returns {RiffChunk} */ -export function readRIFFChunk(dataArray, readData = true, forceShift = false) { - let header = readBytesAsString(dataArray, 4) - - let size = readLittleEndian(dataArray, 4) - let chunkData = undefined +export function readRIFFChunk(dataArray, readData = true, forceShift = false) +{ + let header = readBytesAsString(dataArray, 4); + + let size = readLittleEndian(dataArray, 4); + let chunkData = undefined; if (readData) { - chunkData = new IndexedByteArray(dataArray.buffer.slice(dataArray.currentIndex, dataArray.currentIndex + size)) + chunkData = new IndexedByteArray(dataArray.buffer.slice(dataArray.currentIndex, dataArray.currentIndex + size)); } - if(readData || forceShift) + if (readData || forceShift) { dataArray.currentIndex += size; } - - if(size % 2 !== 0) + + if (size % 2 !== 0) { - if(dataArray[dataArray.currentIndex] === 0) + if (dataArray[dataArray.currentIndex] === 0) { dataArray.currentIndex++; } } - - return new RiffChunk(header, size, chunkData) + + return new RiffChunk(header, size, chunkData); } /** @@ -63,17 +65,17 @@ export function readRIFFChunk(dataArray, readData = true, forceShift = false) { export function writeRIFFChunk(chunk, prepend = undefined) { let size = 8 + chunk.size; - if(chunk.size % 2 !== 0) + if (chunk.size % 2 !== 0) { size++; } - if(prepend) + if (prepend) { size += prepend.length; } const array = new IndexedByteArray(size); // prepend data (for example type before the read) - if(prepend) + if (prepend) { array.set(prepend, array.currentIndex); array.currentIndex += prepend.length; @@ -95,14 +97,14 @@ export function writeRIFFChunk(chunk, prepend = undefined) */ export function writeRIFFOddSize(header, data, addZeroByte = false) { - if(addZeroByte) + if (addZeroByte) { const tempData = new Uint8Array(data.length + 1); tempData.set(data, 0); data = tempData; } let finalSize = 8 + data.length; - if(finalSize % 2 !== 0) + if (finalSize % 2 !== 0) { finalSize++; } @@ -120,8 +122,9 @@ export function writeRIFFOddSize(header, data, addZeroByte = false) */ export function findRIFFListType(collection, type) { - return collection.find(c => { - if(c.header !== "LIST") + return collection.find(c => + { + if (c.header !== "LIST") { return false; } diff --git a/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/ibag.js b/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/ibag.js index 107bfc29..2c65bd30 100644 --- a/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/ibag.js +++ b/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/ibag.js @@ -1,6 +1,6 @@ -import { IndexedByteArray } from '../../../utils/indexed_array.js' -import { writeWord } from '../../../utils/byte_functions/little_endian.js' -import { RiffChunk, writeRIFFChunk } from '../riff_chunk.js' +import { IndexedByteArray } from "../../../utils/indexed_array.js"; +import { writeWord } from "../../../utils/byte_functions/little_endian.js"; +import { RiffChunk, writeRIFFChunk } from "../riff_chunk.js"; /** * @this {BasicSoundFont} @@ -14,10 +14,10 @@ export function getIBAG() let zoneID = 0; let generatorIndex = 0; let modulatorIndex = 0; - for(const inst of this.instruments) + for (const inst of this.instruments) { inst.instrumentZoneIndex = zoneID; - for(const ibag of inst.instrumentZones) + for (const ibag of inst.instrumentZones) { ibag.zoneID = zoneID; writeWord(ibagdata, generatorIndex); @@ -30,7 +30,7 @@ export function getIBAG() // write the terminal IBAG writeWord(ibagdata, generatorIndex); writeWord(ibagdata, modulatorIndex); - + return writeRIFFChunk(new RiffChunk( "ibag", ibagdata.length, diff --git a/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/igen.js b/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/igen.js index abbd3f47..5abe9068 100644 --- a/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/igen.js +++ b/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/igen.js @@ -1,6 +1,6 @@ -import { writeDword, writeWord } from '../../../utils/byte_functions/little_endian.js' -import { IndexedByteArray } from '../../../utils/indexed_array.js' -import { RiffChunk, writeRIFFChunk } from '../riff_chunk.js' +import { writeDword, writeWord } from "../../../utils/byte_functions/little_endian.js"; +import { IndexedByteArray } from "../../../utils/indexed_array.js"; +import { RiffChunk, writeRIFFChunk } from "../riff_chunk.js"; import { generatorTypes } from "../generator.js"; @@ -12,9 +12,10 @@ export function getIGEN() { // go through all instruments -> zones and write generators sequentially (add 4 for terminal) let igensize = 4; - for(const inst of this.instruments) + for (const inst of this.instruments) { - igensize += inst.instrumentZones.reduce((sum, z) => { + igensize += inst.instrumentZones.reduce((sum, z) => + { // clear sample and range generators before derermining the size z.generators = z.generators.filter(g => g.generatorType !== generatorTypes.sampleID && @@ -23,21 +24,21 @@ export function getIGEN() ); // add sample and ranges if needed // unshift vel then key ( to make key first) and instrument is last - if(z.velRange.max !== 127 || z.velRange.min !== 0) + if (z.velRange.max !== 127 || z.velRange.min !== 0) { z.generators.unshift({ generatorType: generatorTypes.velRange, generatorValue: z.velRange.max << 8 | z.velRange.min }); } - if(z.keyRange.max !== 127 || z.keyRange.min !== 0) + if (z.keyRange.max !== 127 || z.keyRange.min !== 0) { z.generators.unshift({ generatorType: generatorTypes.keyRange, generatorValue: z.keyRange.max << 8 | z.keyRange.min }); } - if(!z.isGlobal) + if (!z.isGlobal) { // write sample z.generators.push({ @@ -50,7 +51,7 @@ export function getIGEN() } const igendata = new IndexedByteArray(igensize); let igenIndex = 0; - for(const instrument of this.instruments) + for (const instrument of this.instruments) { for (const instrumentZone of instrument.instrumentZones) { @@ -67,7 +68,7 @@ export function getIGEN() } // terminal generator, is zero writeDword(igendata, 0); - + return writeRIFFChunk(new RiffChunk( "igen", igendata.length, diff --git a/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/imod.js b/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/imod.js index 37547c78..db3a3e71 100644 --- a/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/imod.js +++ b/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/imod.js @@ -1,6 +1,6 @@ -import { IndexedByteArray } from '../../../utils/indexed_array.js' -import { writeLittleEndian, writeWord } from '../../../utils/byte_functions/little_endian.js' -import { RiffChunk, writeRIFFChunk } from '../riff_chunk.js' +import { IndexedByteArray } from "../../../utils/indexed_array.js"; +import { writeLittleEndian, writeWord } from "../../../utils/byte_functions/little_endian.js"; +import { RiffChunk, writeRIFFChunk } from "../riff_chunk.js"; /** * @this {BasicSoundFont} @@ -11,13 +11,13 @@ export function getIMOD() // very similar to igen // go through all instruments -> zones and write modulators sequentially let imodsize = 10; - for(const inst of this.instruments) + for (const inst of this.instruments) { imodsize += inst.instrumentZones.reduce((sum, z) => z.modulators.length * 10 + sum, 0); } const imoddata = new IndexedByteArray(imodsize); let imodIndex = 0; - for(const inst of this.instruments) + for (const inst of this.instruments) { for (const ibag of inst.instrumentZones) { @@ -34,10 +34,10 @@ export function getIMOD() } } } - + // terminal modulator, is zero writeLittleEndian(imoddata, 0, 10); - + return writeRIFFChunk(new RiffChunk( "imod", imoddata.length, diff --git a/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/inst.js b/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/inst.js index 07a6f620..88db63c8 100644 --- a/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/inst.js +++ b/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/inst.js @@ -1,7 +1,7 @@ -import { IndexedByteArray } from '../../../utils/indexed_array.js' -import { writeStringAsBytes } from '../../../utils/byte_functions/string.js' -import { writeWord } from '../../../utils/byte_functions/little_endian.js' -import { RiffChunk, writeRIFFChunk } from '../riff_chunk.js' +import { IndexedByteArray } from "../../../utils/indexed_array.js"; +import { writeStringAsBytes } from "../../../utils/byte_functions/string.js"; +import { writeWord } from "../../../utils/byte_functions/little_endian.js"; +import { RiffChunk, writeRIFFChunk } from "../riff_chunk.js"; /** * @this {BasicSoundFont} @@ -14,7 +14,7 @@ export function getINST() // the instrument start index is adjusted in ibag, simply write it here let instrumentStart = 0; let instrumentID = 0; - for(const inst of this.instruments) + for (const inst of this.instruments) { writeStringAsBytes(instdata, inst.instrumentName, 20); writeWord(instdata, instrumentStart); @@ -25,7 +25,7 @@ export function getINST() // write EOI writeStringAsBytes(instdata, "EOI", 20); writeWord(instdata, instrumentStart); - + return writeRIFFChunk(new RiffChunk( "inst", instdata.length, diff --git a/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/pbag.js b/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/pbag.js index f40d7476..a6488b70 100644 --- a/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/pbag.js +++ b/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/pbag.js @@ -1,6 +1,6 @@ -import { IndexedByteArray } from '../../../utils/indexed_array.js' -import { writeWord } from '../../../utils/byte_functions/little_endian.js' -import { RiffChunk, writeRIFFChunk } from '../riff_chunk.js' +import { IndexedByteArray } from "../../../utils/indexed_array.js"; +import { writeWord } from "../../../utils/byte_functions/little_endian.js"; +import { RiffChunk, writeRIFFChunk } from "../riff_chunk.js"; /** * @this {BasicSoundFont} @@ -14,10 +14,10 @@ export function getPBAG() let zoneID = 0; let generatorIndex = 0; let modulatorIndex = 0; - for(const preset of this.presets) + for (const preset of this.presets) { preset.presetZoneStartIndex = zoneID; - for(const pbag of preset.presetZones) + for (const pbag of preset.presetZones) { pbag.zoneID = zoneID; writeWord(pbagdata, generatorIndex); @@ -30,7 +30,7 @@ export function getPBAG() // write the terminal PBAG writeWord(pbagdata, generatorIndex); writeWord(pbagdata, modulatorIndex); - + return writeRIFFChunk(new RiffChunk( "pbag", pbagdata.length, diff --git a/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/pgen.js b/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/pgen.js index cdf10d2e..43aefe4d 100644 --- a/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/pgen.js +++ b/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/pgen.js @@ -1,6 +1,6 @@ -import { writeWord } from '../../../utils/byte_functions/little_endian.js' -import { IndexedByteArray } from '../../../utils/indexed_array.js' -import { RiffChunk, writeRIFFChunk } from '../riff_chunk.js' +import { writeWord } from "../../../utils/byte_functions/little_endian.js"; +import { IndexedByteArray } from "../../../utils/indexed_array.js"; +import { RiffChunk, writeRIFFChunk } from "../riff_chunk.js"; import { generatorTypes } from "../generator.js"; @@ -13,9 +13,10 @@ export function getPGEN() // almost identical to igen, except correct instrument instead of sample gen // go through all preset zones and write generators sequentially (add 4 for terminal) let pgensize = 4; - for(const preset of this.presets) + for (const preset of this.presets) { - pgensize += preset.presetZones.reduce((size, z) => { + pgensize += preset.presetZones.reduce((size, z) => + { // clear instrument and range generators before derermining the size z.generators = z.generators.filter(g => g.generatorType !== generatorTypes.instrument && @@ -23,21 +24,21 @@ export function getPGEN() g.generatorType !== generatorTypes.velRange ); // unshift vel then key and instrument is last - if(z.velRange.max !== 127 || z.velRange.min !== 0) + if (z.velRange.max !== 127 || z.velRange.min !== 0) { z.generators.unshift({ generatorType: generatorTypes.velRange, generatorValue: z.velRange.max << 8 | z.velRange.min }); } - if(z.keyRange.max !== 127 || z.keyRange.min !== 0) + if (z.keyRange.max !== 127 || z.keyRange.min !== 0) { z.generators.unshift({ generatorType: generatorTypes.keyRange, generatorValue: z.keyRange.max << 8 | z.keyRange.min }); } - if(!z.isGlobal) + if (!z.isGlobal) { // write instrument z.generators.push({ @@ -69,7 +70,7 @@ export function getPGEN() // terminal generator, is zero writeWord(pgendata, 0); writeWord(pgendata, 0); - + return writeRIFFChunk(new RiffChunk( "pgen", pgendata.length, diff --git a/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/phdr.js b/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/phdr.js index fb35e0c9..38556079 100644 --- a/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/phdr.js +++ b/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/phdr.js @@ -1,7 +1,7 @@ -import { IndexedByteArray } from '../../../utils/indexed_array.js' -import { writeStringAsBytes } from '../../../utils/byte_functions/string.js' -import { writeDword, writeWord } from '../../../utils/byte_functions/little_endian.js' -import { RiffChunk, writeRIFFChunk } from '../riff_chunk.js' +import { IndexedByteArray } from "../../../utils/indexed_array.js"; +import { writeStringAsBytes } from "../../../utils/byte_functions/string.js"; +import { writeDword, writeWord } from "../../../utils/byte_functions/little_endian.js"; +import { RiffChunk, writeRIFFChunk } from "../riff_chunk.js"; /** * @this {BasicSoundFont} @@ -33,7 +33,7 @@ export function getPHDR() writeDword(phdrdata, 0); // library writeDword(phdrdata, 0); // genre writeDword(phdrdata, 0); // morphology - + return writeRIFFChunk(new RiffChunk( "phdr", phdrdata.length, diff --git a/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/pmod.js b/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/pmod.js index 710f7b0d..df1b30a5 100644 --- a/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/pmod.js +++ b/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/pmod.js @@ -1,6 +1,6 @@ -import { IndexedByteArray } from '../../../utils/indexed_array.js' -import { writeLittleEndian, writeWord } from '../../../utils/byte_functions/little_endian.js' -import { RiffChunk, writeRIFFChunk } from '../riff_chunk.js' +import { IndexedByteArray } from "../../../utils/indexed_array.js"; +import { writeLittleEndian, writeWord } from "../../../utils/byte_functions/little_endian.js"; +import { RiffChunk, writeRIFFChunk } from "../riff_chunk.js"; /** * @this {BasicSoundFont} @@ -11,13 +11,13 @@ export function getPMOD() // very similar to imod // go through all presets -> zones and write modulators sequentially let pmodsize = 10; - for(const preset of this.presets) + for (const preset of this.presets) { pmodsize += preset.presetZones.reduce((sum, z) => z.modulators.length * 10 + sum, 0); } const pmoddata = new IndexedByteArray(pmodsize); let pmodIndex = 0; - for(const preset of this.presets) + for (const preset of this.presets) { for (const pbag of preset.presetZones) { @@ -34,10 +34,10 @@ export function getPMOD() } } } - + // terminal modulator, is zero writeLittleEndian(pmoddata, 0, 10); - + return writeRIFFChunk(new RiffChunk( "pmod", pmoddata.length, diff --git a/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/sdta.js b/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/sdta.js index d5e15531..29209a17 100644 --- a/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/sdta.js +++ b/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/sdta.js @@ -1,7 +1,7 @@ -import { RiffChunk, writeRIFFChunk } from '../riff_chunk.js' -import { IndexedByteArray } from '../../../utils/indexed_array.js' -import { SpessaSynthInfo } from '../../../utils/loggin.js' -import { consoleColors } from '../../../utils/other.js' +import { RiffChunk, writeRIFFChunk } from "../riff_chunk.js"; +import { IndexedByteArray } from "../../../utils/indexed_array.js"; +import { SpessaSynthInfo } from "../../../utils/loggin.js"; +import { consoleColors } from "../../../utils/other.js"; /** * @this {BasicSoundFont} @@ -16,30 +16,35 @@ export function getSDTA(smplStartOffsets, smplEndOffsets, compress, quality, vor { // write smpl: write int16 data of each sample linearly // get size (calling getAudioData twice doesn't matter since it gets cached) - const sampleDatas = this.samples.map((s, i) => { - if(compress) + const sampleDatas = this.samples.map((s, i) => + { + if (compress) { s.compressSample(quality, vorbisFunc); } - const r= s.getRawData(); - SpessaSynthInfo(`%cEncoded sample %c${i}. ${s.sampleName}%c of %c${this.samples.length}`, + const r = s.getRawData(); + SpessaSynthInfo( + `%cEncoded sample %c${i}. ${s.sampleName}%c of %c${this.samples.length}`, consoleColors.info, consoleColors.recognized, consoleColors.info, - consoleColors.recognized); + consoleColors.recognized + ); return r; }); - const smplSize = this.samples.reduce((total, s, i) => { - return total + sampleDatas[i].length + 46; + const smplSize = this.samples.reduce((total, s, i) => + { + return total + sampleDatas[i].length + 46; }, 0); const smplData = new IndexedByteArray(smplSize); // resample to int16 and write out - this.samples.forEach((sample, i) => { + this.samples.forEach((sample, i) => + { const data = sampleDatas[i]; let startOffset; let endOffset; let jump = data.length; - if(sample.isCompressed) + if (sample.isCompressed) { // sf3 offset is in bytes startOffset = smplData.currentIndex; @@ -57,13 +62,13 @@ export function getSDTA(smplStartOffsets, smplEndOffsets, compress, quality, vor smplData.currentIndex += jump; smplEndOffsets.push(endOffset); }); - + const smplChunk = writeRIFFChunk(new RiffChunk( "smpl", smplData.length, smplData ), new IndexedByteArray([115, 100, 116, 97])); // `sdta` - + return writeRIFFChunk(new RiffChunk( "LIST", smplChunk.length, diff --git a/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/shdr.js b/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/shdr.js index f060170f..056dcb0a 100644 --- a/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/shdr.js +++ b/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/shdr.js @@ -1,7 +1,7 @@ -import { IndexedByteArray } from '../../../utils/indexed_array.js' -import { writeStringAsBytes } from '../../../utils/byte_functions/string.js' -import { writeDword, writeWord } from '../../../utils/byte_functions/little_endian.js' -import { RiffChunk, writeRIFFChunk } from '../riff_chunk.js' +import { IndexedByteArray } from "../../../utils/indexed_array.js"; +import { writeStringAsBytes } from "../../../utils/byte_functions/string.js"; +import { writeDword, writeWord } from "../../../utils/byte_functions/little_endian.js"; +import { RiffChunk, writeRIFFChunk } from "../riff_chunk.js"; /** * @this {BasicSoundFont} @@ -12,8 +12,9 @@ import { RiffChunk, writeRIFFChunk } from '../riff_chunk.js' export function getSHDR(smplStartOffsets, smplEndOffsets) { const sampleLength = 46; - const shdrData = new IndexedByteArray(sampleLength * (this.samples.length + 1 )); // +1 because EOP - this.samples.forEach((sample, index) => { + const shdrData = new IndexedByteArray(sampleLength * (this.samples.length + 1)); // +1 because EOP + this.samples.forEach((sample, index) => + { // sample name writeStringAsBytes(shdrData, sample.sampleName, 20); // start offset @@ -25,7 +26,7 @@ export function getSHDR(smplStartOffsets, smplEndOffsets) // loop is stored as relative in sample points, change it to absolute sample points here let loopStart = sample.sampleLoopStartIndex + dwStart; let loopEnd = sample.sampleLoopEndIndex + dwStart; - if(sample.isCompressed) + if (sample.isCompressed) { // https://github.com/FluidSynth/fluidsynth/wiki/SoundFont3Format loopStart -= dwStart; @@ -43,7 +44,7 @@ export function getSHDR(smplStartOffsets, smplEndOffsets) // sample type: write raw because we simply copy compressed samples writeWord(shdrData, sample.sampleType); }); - + // write EOS and zero everything else writeStringAsBytes(shdrData, "EOS", sampleLength); return writeRIFFChunk(new RiffChunk( diff --git a/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/soundfont_trimmer.js b/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/soundfont_trimmer.js index cff520fd..2b0426d8 100644 --- a/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/soundfont_trimmer.js +++ b/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/soundfont_trimmer.js @@ -1,11 +1,11 @@ -import { consoleColors } from '../../../utils/other.js' +import { consoleColors } from "../../../utils/other.js"; import { SpessaSynthGroup, SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo -} from '../../../utils/loggin.js' -import { getUsedProgramsAndKeys } from '../../../midi_parser/used_keys_loaded.js' +} from "../../../utils/loggin.js"; +import { getUsedProgramsAndKeys } from "../../../midi_parser/used_keys_loaded.js"; /** * @param soundfont {BasicSoundFont} @@ -25,16 +25,16 @@ export function trimSoundfont(soundfont, mid) for (let iZoneIndex = 0; iZoneIndex < instrument.instrumentZones.length; iZoneIndex++) { const iZone = instrument.instrumentZones[iZoneIndex]; - if(iZone.isGlobal) + if (iZone.isGlobal) { continue; } const iKeyRange = iZone.keyRange; const iVelRange = iZone.velRange; let isIZoneUsed = false; - for(const iCombo of combos) + for (const iCombo of combos) { - if( + if ( (iCombo.key >= iKeyRange.min && iCombo.key <= iKeyRange.max) && (iCombo.velocity >= iVelRange.min && iCombo.velocity <= iVelRange.max) ) @@ -43,38 +43,46 @@ export function trimSoundfont(soundfont, mid) break; } } - if(!isIZoneUsed) + if (!isIZoneUsed) { - SpessaSynthInfo(`%c${iZone.sample.sampleName} %cremoved from %c${instrument.instrumentName}%c. Use count: %c${iZone.useCount - 1}`, + SpessaSynthInfo( + `%c${iZone.sample.sampleName} %cremoved from %c${instrument.instrumentName}%c. Use count: %c${iZone.useCount - 1}`, consoleColors.recognized, consoleColors.info, consoleColors.recognized, consoleColors.info, - consoleColors.recognized); - if(instrument.safeDeleteZone(iZoneIndex)) + consoleColors.recognized + ); + if (instrument.safeDeleteZone(iZoneIndex)) { trimmedIZones++; iZoneIndex--; - SpessaSynthInfo(`%c${iZone.sample.sampleName} %cdeleted`, + SpessaSynthInfo( + `%c${iZone.sample.sampleName} %cdeleted`, consoleColors.recognized, - consoleColors.info) + consoleColors.info + ); } - if(iZone.sample.useCount < 1) + if (iZone.sample.useCount < 1) { soundfont.deleteSample(iZone.sample); } } - + } return trimmedIZones; } - - SpessaSynthGroup("%cTrimming soundfont...", - consoleColors.info); + + SpessaSynthGroup( + "%cTrimming soundfont...", + consoleColors.info + ); const usedProgramsAndKeys = getUsedProgramsAndKeys(mid, soundfont); - - SpessaSynthGroupCollapsed("%cModifying soundfont...", - consoleColors.info); + + SpessaSynthGroupCollapsed( + "%cModifying soundfont...", + consoleColors.info + ); SpessaSynthInfo("Detected keys for midi:", usedProgramsAndKeys); // modify the soundfont to only include programs and samples that are used for (let presetIndex = 0; presetIndex < soundfont.presets.length; presetIndex++) @@ -82,9 +90,10 @@ export function trimSoundfont(soundfont, mid) const p = soundfont.presets[presetIndex]; const string = p.bank + ":" + p.program; const used = usedProgramsAndKeys[string]; - if(used === undefined) + if (used === undefined) { - SpessaSynthInfo(`%cDeleting preset %c${p.presetName}%c and its zones`, + SpessaSynthInfo( + `%cDeleting preset %c${p.presetName}%c and its zones`, consoleColors.info, consoleColors.recognized, consoleColors.info @@ -94,23 +103,26 @@ export function trimSoundfont(soundfont, mid) } else { - const combos = [...used].map(s => { + const combos = [...used].map(s => + { const split = s.split("-"); return { key: parseInt(split[0]), velocity: parseInt(split[1]) - } + }; }); - SpessaSynthGroupCollapsed(`%cTrimming %c${p.presetName}`, + SpessaSynthGroupCollapsed( + `%cTrimming %c${p.presetName}`, consoleColors.info, - consoleColors.recognized); - SpessaSynthInfo(`Keys for ${p.presetName}:`, combos) + consoleColors.recognized + ); + SpessaSynthInfo(`Keys for ${p.presetName}:`, combos); let trimmedZones = 0; // clean the preset to only use zones that are used for (let zoneIndex = 0; zoneIndex < p.presetZones.length; zoneIndex++) { const zone = p.presetZones[zoneIndex]; - if(zone.isGlobal) + if (zone.isGlobal) { continue; } @@ -118,9 +130,9 @@ export function trimSoundfont(soundfont, mid) const velRange = zone.velRange; // check if any of the combos matches the zone let isZoneUsed = false; - for(const combo of combos) + for (const combo of combos) { - if( + if ( (combo.key >= keyRange.min && combo.key <= keyRange.max) && (combo.velocity >= velRange.min && combo.velocity <= velRange.max) ) @@ -128,7 +140,8 @@ export function trimSoundfont(soundfont, mid) // zone is used, trim the instrument zones isZoneUsed = true; const trimmedIZones = trimInstrumentZones(zone.instrument, combos); - SpessaSynthInfo(`%cTrimmed off %c${trimmedIZones}%c zones from %c${zone.instrument.instrumentName}`, + SpessaSynthInfo( + `%cTrimmed off %c${trimmedIZones}%c zones from %c${zone.instrument.instrumentName}`, consoleColors.info, consoleColors.recognized, consoleColors.info, @@ -137,18 +150,19 @@ export function trimSoundfont(soundfont, mid) break; } } - if(!isZoneUsed) + if (!isZoneUsed) { trimmedZones++; p.deleteZone(zoneIndex); - if(zone.instrument.useCount < 1) + if (zone.instrument.useCount < 1) { soundfont.deleteInstrument(zone.instrument); } zoneIndex--; } } - SpessaSynthInfo(`%cTrimmed off %c${trimmedZones}%c zones from %c${p.presetName}`, + SpessaSynthInfo( + `%cTrimmed off %c${trimmedZones}%c zones from %c${p.presetName}`, consoleColors.info, consoleColors.recognized, consoleColors.info, @@ -158,12 +172,14 @@ export function trimSoundfont(soundfont, mid) } } soundfont.removeUnusedElements(); - - soundfont.soundFontInfo['ICMT'] = `NOTE: This soundfont was trimmed by SpessaSynth to only contain presets used in "${mid.midiName}"\n\n` - + soundfont.soundFontInfo['ICMT']; - - SpessaSynthInfo("%cSoundfont modified!", - consoleColors.recognized) + + soundfont.soundFontInfo["ICMT"] = `NOTE: This soundfont was trimmed by SpessaSynth to only contain presets used in "${mid.midiName}"\n\n` + + soundfont.soundFontInfo["ICMT"]; + + SpessaSynthInfo( + "%cSoundfont modified!", + consoleColors.recognized + ); SpessaSynthGroupEnd(); SpessaSynthGroupEnd(); } \ No newline at end of file diff --git a/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/write.js b/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/write.js index 37942217..97e89b13 100644 --- a/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/write.js +++ b/src/spessasynth_lib/soundfont/basic_soundfont/write_sf2/write.js @@ -1,23 +1,19 @@ -import { combineArrays, IndexedByteArray } from '../../../utils/indexed_array.js' -import { RiffChunk, writeRIFFChunk } from '../riff_chunk.js' -import { writeStringAsBytes } from '../../../utils/byte_functions/string.js' -import { consoleColors } from '../../../utils/other.js' -import { getIGEN } from './igen.js' -import { getSDTA } from './sdta.js' -import { getSHDR } from './shdr.js' -import { getIMOD } from './imod.js' -import { getIBAG } from './ibag.js' -import { getINST } from './inst.js' -import { getPGEN } from './pgen.js' -import { getPMOD } from './pmod.js' -import { getPBAG } from './pbag.js' -import { getPHDR } from './phdr.js' -import { writeWord } from '../../../utils/byte_functions/little_endian.js' -import { - SpessaSynthGroupCollapsed, - SpessaSynthGroupEnd, - SpessaSynthInfo -} from '../../../utils/loggin.js' +import { combineArrays, IndexedByteArray } from "../../../utils/indexed_array.js"; +import { RiffChunk, writeRIFFChunk } from "../riff_chunk.js"; +import { writeStringAsBytes } from "../../../utils/byte_functions/string.js"; +import { consoleColors } from "../../../utils/other.js"; +import { getIGEN } from "./igen.js"; +import { getSDTA } from "./sdta.js"; +import { getSHDR } from "./shdr.js"; +import { getIMOD } from "./imod.js"; +import { getIBAG } from "./ibag.js"; +import { getINST } from "./inst.js"; +import { getPGEN } from "./pgen.js"; +import { getPMOD } from "./pmod.js"; +import { getPBAG } from "./pbag.js"; +import { getPHDR } from "./phdr.js"; +import { writeWord } from "../../../utils/byte_functions/little_endian.js"; +import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from "../../../utils/loggin.js"; /** * @typedef {Object} SoundFont2WriteOptions * @property {boolean} compress - if the soundfont should be compressed with the ogg vorbis codec @@ -32,7 +28,7 @@ const DEFAULT_WRITE_OPTIONS = { compress: false, compressionQuality: 0.5, compressionFunction: undefined -} +}; /** * Write the soundfont as an .sf2 file. This method is DESTRUCTIVE @@ -42,39 +38,45 @@ const DEFAULT_WRITE_OPTIONS = { */ export function write(options = DEFAULT_WRITE_OPTIONS) { - if(options.compress) + if (options.compress) { - if(typeof options.compressionFunction !== "function") + if (typeof options.compressionFunction !== "function") { throw new TypeError("No compression function supplied but compression enabled."); } } - SpessaSynthGroupCollapsed("%cSaving soundfont...", - consoleColors.info); - SpessaSynthInfo(`%cCompression: %c${options?.compress || "false"}%c quality: %c${options?.compressionQuality || "none"}`, + SpessaSynthGroupCollapsed( + "%cSaving soundfont...", + consoleColors.info + ); + SpessaSynthInfo( + `%cCompression: %c${options?.compress || "false"}%c quality: %c${options?.compressionQuality || "none"}`, consoleColors.info, consoleColors.recognized, consoleColors.info, - consoleColors.recognized) - SpessaSynthInfo("%cWriting INFO...", - consoleColors.info); + consoleColors.recognized + ); + SpessaSynthInfo( + "%cWriting INFO...", + consoleColors.info + ); /** * Write INFO * @type {IndexedByteArray[]} */ const infoArrays = []; this.soundFontInfo["ISFT"] = "SpessaSynth"; // ( ͡° ͜ʖ ͡°) - if(options?.compress) + if (options?.compress) { this.soundFontInfo["ifil"] = "3.0"; // set version to 3 } - + for (const [type, data] of Object.entries(this.soundFontInfo)) { - if(type === "ifil" || type === "iver") + if (type === "ifil" || type === "iver") { - const major= parseInt(data.split(".")[0]); - const minor= parseInt(data.split(".")[1]); + const major = parseInt(data.split(".")[0]); + const minor = parseInt(data.split(".")[1]); const ckdata = new IndexedByteArray(4); writeWord(ckdata, major); writeWord(ckdata, minor); @@ -84,8 +86,7 @@ export function write(options = DEFAULT_WRITE_OPTIONS) ckdata ))); } - else - if(type === "DMOD") + else if (type === "DMOD") { infoArrays.push(writeRIFFChunk(new RiffChunk( type, @@ -109,44 +110,71 @@ export function write(options = DEFAULT_WRITE_OPTIONS) ...infoArrays ]); const infoChunk = writeRIFFChunk(new RiffChunk("LIST", combined.length, combined)); - - SpessaSynthInfo("%cWriting SDTA...", - consoleColors.info); + + SpessaSynthInfo( + "%cWriting SDTA...", + consoleColors.info + ); // write sdata const smplStartOffsets = []; const smplEndOffsets = []; - const sdtaChunk = getSDTA.call(this, smplStartOffsets, smplEndOffsets, options?.compress, options?.compressionQuality || 0.5, options.compressionFunction); - - SpessaSynthInfo("%cWriting PDTA...", - consoleColors.info); + const sdtaChunk = getSDTA.call( + this, + smplStartOffsets, + smplEndOffsets, + options?.compress, + options?.compressionQuality || 0.5, + options.compressionFunction + ); + + SpessaSynthInfo( + "%cWriting PDTA...", + consoleColors.info + ); // write pdata // go in reverse so the indexes are correct // instruments - SpessaSynthInfo("%cWriting SHDR...", - consoleColors.info); + SpessaSynthInfo( + "%cWriting SHDR...", + consoleColors.info + ); const shdrChunk = getSHDR.call(this, smplStartOffsets, smplEndOffsets); - SpessaSynthInfo("%cWriting IGEN...", - consoleColors.info); + SpessaSynthInfo( + "%cWriting IGEN...", + consoleColors.info + ); const igenChunk = getIGEN.call(this); - SpessaSynthInfo("%cWriting IMOD...", - consoleColors.info); + SpessaSynthInfo( + "%cWriting IMOD...", + consoleColors.info + ); const imodChunk = getIMOD.call(this); - SpessaSynthInfo("%cWriting IBAG...", - consoleColors.info); + SpessaSynthInfo( + "%cWriting IBAG...", + consoleColors.info + ); const ibagChunk = getIBAG.call(this); - SpessaSynthInfo("%cWriting INST...", - consoleColors.info); + SpessaSynthInfo( + "%cWriting INST...", + consoleColors.info + ); const instChunk = getINST.call(this); // presets const pgenChunk = getPGEN.call(this); - SpessaSynthInfo("%cWriting PMOD...", - consoleColors.info); + SpessaSynthInfo( + "%cWriting PMOD...", + consoleColors.info + ); const pmodChunk = getPMOD.call(this); - SpessaSynthInfo("%cWriting PBAG...", - consoleColors.info); + SpessaSynthInfo( + "%cWriting PBAG...", + consoleColors.info + ); const pbagChunk = getPBAG.call(this); - SpessaSynthInfo("%cWriting PHDR...", - consoleColors.info); + SpessaSynthInfo( + "%cWriting PHDR...", + consoleColors.info + ); const phdrChunk = getPHDR.call(this); // combine in the sfspec order const pdtadata = combineArrays([ @@ -166,8 +194,10 @@ export function write(options = DEFAULT_WRITE_OPTIONS) pdtadata.length, pdtadata )); - SpessaSynthInfo("%cWriting the output file...", - consoleColors.info); + SpessaSynthInfo( + "%cWriting the output file...", + consoleColors.info + ); // finally, combine everything const riffdata = combineArrays([ new IndexedByteArray([115, 102, 98, 107]), // "sfbk" @@ -175,15 +205,17 @@ export function write(options = DEFAULT_WRITE_OPTIONS) sdtaChunk, pdtaChunk ]); - + const main = writeRIFFChunk(new RiffChunk( "RIFF", riffdata.length, riffdata )); - SpessaSynthInfo(`%cSaved succesfully! Final file size: %c${main.length}`, + SpessaSynthInfo( + `%cSaved succesfully! Final file size: %c${main.length}`, consoleColors.info, - consoleColors.recognized) + consoleColors.recognized + ); SpessaSynthGroupEnd(); return main; } \ No newline at end of file diff --git a/src/spessasynth_lib/soundfont/dls/articulator_converter.js b/src/spessasynth_lib/soundfont/dls/articulator_converter.js index 9e61edec..6a96af0e 100644 --- a/src/spessasynth_lib/soundfont/dls/articulator_converter.js +++ b/src/spessasynth_lib/soundfont/dls/articulator_converter.js @@ -1,10 +1,9 @@ -import { DLSSources } from './dls_sources.js' -import { getModSourceEnum } from "../basic_soundfont/modulator.js"; -import { midiControllers } from '../../midi_parser/midi_message.js' -import { DLSDestinations } from './dls_destinations.js' +import { DLSSources } from "./dls_sources.js"; +import { getModSourceEnum, Modulator, modulatorCurveTypes, modulatorSources } from "../basic_soundfont/modulator.js"; +import { midiControllers } from "../../midi_parser/midi_message.js"; +import { DLSDestinations } from "./dls_destinations.js"; import { generatorTypes } from "../basic_soundfont/generator.js"; -import { Modulator, modulatorCurveTypes, modulatorSources } from "../basic_soundfont/modulator.js"; /** * @param source {number} @@ -14,7 +13,7 @@ function getSF2SourceFromDLS(source) { let sourceEnum = undefined; let isCC = false; - switch(source) + switch (source) { default: case DLSSources.modLfo: @@ -23,7 +22,7 @@ function getSF2SourceFromDLS(source) case DLSSources.fineTune: case DLSSources.modEnv: return undefined; // cannot be this in sf2 - + case DLSSources.keyNum: sourceEnum = modulatorSources.noteOnKeyNum; break; @@ -70,11 +69,11 @@ function getSF2SourceFromDLS(source) sourceEnum = modulatorSources.pitchWheelRange; break; } - if(sourceEnum === undefined) + if (sourceEnum === undefined) { - throw `not known?? ${source}` + throw `not known?? ${source}`; } - return {enum: sourceEnum, isCC: isCC}; + return { enum: sourceEnum, isCC: isCC }; } /** @@ -92,12 +91,12 @@ function getSF2GeneratorFromDLS(destination, amount) case DLSDestinations.pan: return generatorTypes.pan; case DLSDestinations.gain: - return {gen: generatorTypes.initialAttenuation, newAmount: amount * -1}; + return { gen: generatorTypes.initialAttenuation, newAmount: amount * -1 }; case DLSDestinations.pitch: return generatorTypes.fineTune; case DLSDestinations.keyNum: return generatorTypes.overridingRootKey; - + // vol env case DLSDestinations.volEnvDelay: return generatorTypes.delayVolEnv; @@ -108,10 +107,10 @@ function getSF2GeneratorFromDLS(destination, amount) case DLSDestinations.volEnvDecay: return generatorTypes.decayVolEnv; case DLSDestinations.volEnvSustain: - return {gen: generatorTypes.sustainVolEnv, newAmount: 1000 - amount}; + return { gen: generatorTypes.sustainVolEnv, newAmount: 1000 - amount }; case DLSDestinations.volEnvRelease: return generatorTypes.releaseVolEnv; - + // mod env case DLSDestinations.modEnvDelay: return generatorTypes.delayModEnv; @@ -122,10 +121,10 @@ function getSF2GeneratorFromDLS(destination, amount) case DLSDestinations.modEnvDecay: return generatorTypes.decayModEnv; case DLSDestinations.modEnvSustain: - return {gen: generatorTypes.sustainModEnv, newAmount: (1000 - amount) / 10}; + return { gen: generatorTypes.sustainModEnv, newAmount: (1000 - amount) / 10 }; case DLSDestinations.modEnvRelease: return generatorTypes.releaseModEnv; - + case DLSDestinations.filterCutoff: return generatorTypes.initialFilterFc; case DLSDestinations.filterQ: @@ -145,32 +144,27 @@ function getSF2GeneratorFromDLS(destination, amount) */ function checkForSpecialDLSCombo(source, destination) { - if(source === DLSSources.vibratoLfo && destination === DLSDestinations.pitch) + if (source === DLSSources.vibratoLfo && destination === DLSDestinations.pitch) { return generatorTypes.vibLfoToPitch; } - else - if(source === DLSSources.modLfo && destination === DLSDestinations.pitch) + else if (source === DLSSources.modLfo && destination === DLSDestinations.pitch) { return generatorTypes.modLfoToPitch; } - else - if(source === DLSSources.modLfo && destination === DLSDestinations.filterCutoff) + else if (source === DLSSources.modLfo && destination === DLSDestinations.filterCutoff) { return generatorTypes.modLfoToFilterFc; } - else - if(source === DLSSources.modLfo && destination === DLSDestinations.gain) + else if (source === DLSSources.modLfo && destination === DLSDestinations.gain) { return generatorTypes.modLfoToVolume; } - else - if(source === DLSSources.modEnv && destination === DLSDestinations.filterCutoff) + else if (source === DLSSources.modEnv && destination === DLSDestinations.filterCutoff) { return generatorTypes.modEnvToFilterFc; } - else - if(source === DLSSources.modEnv && destination === DLSDestinations.pitch) + else if (source === DLSSources.modEnv && destination === DLSDestinations.pitch) { return generatorTypes.modEnvToPitch; } @@ -205,14 +199,14 @@ export function getSF2ModulatorFromArticulator( /** * @type {{enum: number, isCC: boolean}} */ - let sf2Source + let sf2Source; let swapSources = false; let isSourceNoController = false; - if(specialDestination === undefined) + if (specialDestination === undefined) { // determine destination const sf2GenDestination = getSF2GeneratorFromDLS(destination, value); - if(sf2GenDestination === undefined) + if (sf2GenDestination === undefined) { // cannot be a valid modulator return undefined; @@ -221,13 +215,13 @@ export function getSF2ModulatorFromArticulator( * @type {generatorTypes} */ destinationGenerator = sf2GenDestination; - if(sf2GenDestination.newAmount !== undefined) + if (sf2GenDestination.newAmount !== undefined) { value = sf2GenDestination.newAmount; destinationGenerator = sf2GenDestination.gen; } sf2Source = getSF2SourceFromDLS(source); - if(sf2Source === undefined) + if (sf2Source === undefined) { // cannot be a valid modulator return undefined; @@ -237,19 +231,19 @@ export function getSF2ModulatorFromArticulator( { destinationGenerator = specialDestination; swapSources = true; - sf2Source = {enum: modulatorSources.noController, isCC: false}; + sf2Source = { enum: modulatorSources.noController, isCC: false }; isSourceNoController = true; } let sf2SecondSource = getSF2SourceFromDLS(control); - if(sf2SecondSource === undefined) + if (sf2SecondSource === undefined) { // cannot be a valid modulator return undefined; } - + // get transforms and final enums let sourceEnumFinal; - if(isSourceNoController) + if (isSourceNoController) { // we force it into this state because before it was some strange value, // like vibrato lfo bipolar for example @@ -263,14 +257,14 @@ export function getSF2ModulatorFromArticulator( const outputTransform = transform & 0b1111; // source curve type maps to desfont curve type in section 2.10, table 9 let sourceTransform = (transform >> 10) & 0b1111; - if(sourceTransform === modulatorCurveTypes.linear && outputTransform !== modulatorCurveTypes.linear) + if (sourceTransform === modulatorCurveTypes.linear && outputTransform !== modulatorCurveTypes.linear) { sourceTransform = outputTransform; } const sourceIsBipolar = (transform >> 14) & 1; let sourceIsNegative = (transform >> 15) & 1; // special case: for attenuation, invert source - if(destinationGenerator === generatorTypes.initialAttenuation) + if (destinationGenerator === generatorTypes.initialAttenuation) { sourceIsNegative = !sourceIsNegative; } @@ -282,7 +276,7 @@ export function getSF2ModulatorFromArticulator( sf2Source.enum ); } - + const secSourceTransform = (transform >> 4) & 0b1111; const secSourceIsBipolar = (transform >> 8) & 1; const secSourceIsNegative = transform >> 9 & 1; @@ -293,14 +287,14 @@ export function getSF2ModulatorFromArticulator( sf2SecondSource.isCC, sf2SecondSource.enum ); - - if(swapSources) + + if (swapSources) { const temp = secSourceEnumFinal; secSourceEnumFinal = sourceEnumFinal; sourceEnumFinal = temp; } - + // return the modulator! return new Modulator({ srcEnum: sourceEnumFinal, @@ -309,5 +303,5 @@ export function getSF2ModulatorFromArticulator( transform: 0x0, amt: value }); - + } \ No newline at end of file diff --git a/src/spessasynth_lib/soundfont/dls/dls_destinations.js b/src/spessasynth_lib/soundfont/dls/dls_destinations.js index 824d456d..b624460e 100644 --- a/src/spessasynth_lib/soundfont/dls/dls_destinations.js +++ b/src/spessasynth_lib/soundfont/dls/dls_destinations.js @@ -12,27 +12,27 @@ export const DLSDestinations = { // nuh uh, the channel controllers are not supported!!!! chorusSend: 0x80, reverbSend: 0x81, - + modLfoFreq: 0x104, modLfoDelay: 0x105, - + vibLfoFreq: 0x114, vibLfoDelay: 0x115, - + volEnvAttack: 0x206, volEnvDecay: 0x207, volEnvRelease: 0x209, volEnvSustain: 0x20a, volEnvDelay: 0x20b, volEnvHold: 0x20c, - + modEnvAttack: 0x30a, modEnvDecay: 0x30b, modEnvRelease: 0x30d, modEnvSustain: 0x30e, modEnvDelay: 0x30f, modEnvHold: 0x310, - + filterCutoff: 0x500, filterQ: 0x501 -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/spessasynth_lib/soundfont/dls/dls_preset.js b/src/spessasynth_lib/soundfont/dls/dls_preset.js index 8d98c3b2..47bd586c 100644 --- a/src/spessasynth_lib/soundfont/dls/dls_preset.js +++ b/src/spessasynth_lib/soundfont/dls/dls_preset.js @@ -1,6 +1,6 @@ -import { BasicPreset } from '../basic_soundfont/basic_preset.js' -import { BasicPresetZone } from '../basic_soundfont/basic_zones.js' -import { BasicInstrument } from '../basic_soundfont/basic_instrument.js' +import { BasicPreset } from "../basic_soundfont/basic_preset.js"; +import { BasicPresetZone } from "../basic_soundfont/basic_zones.js"; +import { BasicInstrument } from "../basic_soundfont/basic_instrument.js"; import { defaultModulators } from "../basic_soundfont/modulator.js"; export class DLSPreset extends BasicPreset @@ -17,18 +17,18 @@ export class DLSPreset extends BasicPreset this.program = ulInstrument & 127; this.bank = (ulBank >> 8) & 127; const isDrums = ulBank >> 31; - if(isDrums) + if (isDrums) { // soundfont bank is 128 so we change it here this.bank = 128; } - + this.DLSInstrument = new BasicInstrument(); this.DLSInstrument.addUseCount(); - + const zone = new BasicPresetZone(); zone.instrument = this.DLSInstrument; - + this.presetZones = [zone]; } } \ No newline at end of file diff --git a/src/spessasynth_lib/soundfont/dls/dls_sample.js b/src/spessasynth_lib/soundfont/dls/dls_sample.js index 2c160268..df104ffd 100644 --- a/src/spessasynth_lib/soundfont/dls/dls_sample.js +++ b/src/spessasynth_lib/soundfont/dls/dls_sample.js @@ -1,7 +1,17 @@ -import { BasicSample } from '../basic_soundfont/basic_sample.js' +import { BasicSample } from "../basic_soundfont/basic_sample.js"; export class DLSSample extends BasicSample { + /** + * in decibels of attenuation, WITHOUT EMU CORRECTION + * @type {number} + */ + sampleDbAttenuation; + /** + * @type {Float32Array} + */ + sampleData; + /** * @param name {string} * @param rate {number} @@ -36,30 +46,19 @@ export class DLSSample extends BasicSample this.sampleData = data; this.sampleDbAttenuation = sampleDbAttenuation; } - + getAudioData() { return this.sampleData; } - - /** - * in decibels of attenuation, WITHOUT EMU CORRECTION - * @type {number} - */ - sampleDbAttenuation; - - /** - * @type {Float32Array} - */ - sampleData; - + getRawData() { - if(this.isCompressed) + if (this.isCompressed) { if (!this.compressedData) { - throw new Error("Compressed but no data??") + throw new Error("Compressed but no data??"); } return this.compressedData; } diff --git a/src/spessasynth_lib/soundfont/dls/dls_soundfont.js b/src/spessasynth_lib/soundfont/dls/dls_soundfont.js index cfcb3407..610e4af2 100644 --- a/src/spessasynth_lib/soundfont/dls/dls_soundfont.js +++ b/src/spessasynth_lib/soundfont/dls/dls_soundfont.js @@ -1,15 +1,15 @@ -import { BasicSoundFont } from '../basic_soundfont/basic_soundfont.js' -import { IndexedByteArray } from '../../utils/indexed_array.js' -import { SpessaSynthGroup, SpessaSynthGroupEnd, SpessaSynthInfo } from '../../utils/loggin.js' -import { consoleColors } from '../../utils/other.js' -import { findRIFFListType, readRIFFChunk } from '../basic_soundfont/riff_chunk.js' -import { readBytesAsString } from '../../utils/byte_functions/string.js' -import { readLittleEndian } from '../../utils/byte_functions/little_endian.js' -import { readDLSInstrumentList } from './read_instrument_list.js' -import { readDLSInstrument } from './read_instrument.js' -import { readLart } from './read_lart.js' -import { readRegion } from './read_region.js' -import { readDLSSamples } from './read_samples.js' +import { BasicSoundFont } from "../basic_soundfont/basic_soundfont.js"; +import { IndexedByteArray } from "../../utils/indexed_array.js"; +import { SpessaSynthGroup, SpessaSynthGroupEnd, SpessaSynthInfo } from "../../utils/loggin.js"; +import { consoleColors } from "../../utils/other.js"; +import { findRIFFListType, readRIFFChunk } from "../basic_soundfont/riff_chunk.js"; +import { readBytesAsString } from "../../utils/byte_functions/string.js"; +import { readLittleEndian } from "../../utils/byte_functions/little_endian.js"; +import { readDLSInstrumentList } from "./read_instrument_list.js"; +import { readDLSInstrument } from "./read_instrument.js"; +import { readLart } from "./read_lart.js"; +import { readRegion } from "./read_region.js"; +import { readDLSSamples } from "./read_samples.js"; class DLSSoundFont extends BasicSoundFont { @@ -22,91 +22,96 @@ class DLSSoundFont extends BasicSoundFont super(); this.dataArray = new IndexedByteArray(buffer); SpessaSynthGroup("%cParsing DLS...", consoleColors.info); - if(!this.dataArray) + if (!this.dataArray) { SpessaSynthGroupEnd(); throw new TypeError("No data!"); } - + // read the main chunk let firstChunk = readRIFFChunk(this.dataArray, false); this.verifyHeader(firstChunk, "riff"); - this.verifyText(readBytesAsString(this.dataArray,4).toLowerCase(), "dls "); - + this.verifyText(readBytesAsString(this.dataArray, 4).toLowerCase(), "dls "); + /** * Read list * @type {RiffChunk[]} */ const chunks = []; - while(this.dataArray.currentIndex < this.dataArray.length) + while (this.dataArray.currentIndex < this.dataArray.length) { chunks.push(readRIFFChunk(this.dataArray)); } - + // mandatory this.soundFontInfo["ifil"] = "2.1"; // always for dls this.soundFontInfo["isng"] = "EMU8000"; - + // set some defaults this.soundFontInfo["INAM"] = "Unnamed DLS"; this.soundFontInfo["IENG"] = "Unknown"; this.soundFontInfo["IPRD"] = "SpessaSynth DLS"; - this.soundFontInfo["ICRD"] = new Date().toDateString(); - + this.soundFontInfo["ICRD"] = new Date().toDateString(); + // read info const infoChunk = findRIFFListType(chunks, "INFO"); - if(infoChunk) + if (infoChunk) { - while(infoChunk.chunkData.currentIndex < infoChunk.chunkData.length) + while (infoChunk.chunkData.currentIndex < infoChunk.chunkData.length) { const infoPart = readRIFFChunk(infoChunk.chunkData); this.soundFontInfo[infoPart.header] = readBytesAsString(infoPart.chunkData, infoPart.size); } } this.soundFontInfo["ICMT"] = (this.soundFontInfo["ICMT"] || "(No description)") + "\nConverted from DLS to SF2 with SpessaSynth"; - if(this.soundFontInfo["ISBJ"]) + if (this.soundFontInfo["ISBJ"]) { // merge it this.soundFontInfo["ICMT"] += "\n" + this.soundFontInfo["ISBJ"]; delete this.soundFontInfo["ISBJ"]; } - - for(const [info, value] of Object.entries(this.soundFontInfo)) + + for (const [info, value] of Object.entries(this.soundFontInfo)) { - SpessaSynthInfo(`%c"${info}": %c"${value}"`, + SpessaSynthInfo( + `%c"${info}": %c"${value}"`, consoleColors.info, - consoleColors.recognized); + consoleColors.recognized + ); } - + // read "colh" let colhChunk = chunks.find(c => c.header === "colh"); - if(!colhChunk) + if (!colhChunk) { SpessaSynthGroupEnd(); throw new Error("No colh chunk!"); } this.instrumentAmount = readLittleEndian(colhChunk.chunkData, 4); - SpessaSynthInfo(`%cInstruments amount: %c${this.instrumentAmount}`, + SpessaSynthInfo( + `%cInstruments amount: %c${this.instrumentAmount}`, consoleColors.info, - consoleColors.recognized); - + consoleColors.recognized + ); + // read wave list - let waveListChunk = findRIFFListType(chunks , "wvpl"); + let waveListChunk = findRIFFListType(chunks, "wvpl"); this.readDLSSamples(waveListChunk); - + // read instrument list let instrumentListChunk = findRIFFListType(chunks, "lins"); - if(!instrumentListChunk) + if (!instrumentListChunk) { SpessaSynthGroupEnd(); throw new Error("No lins chunk!"); } this.readDLSInstrumentList(instrumentListChunk); - + // sort presets this.presets.sort((a, b) => (a.program - b.program) + (a.bank - b.bank)); - - SpessaSynthInfo(`%cParsing finished! %c"${this.soundFontInfo["INAM"] || "UNNAMED"}"%c has %c${this.presets.length} %cpresets, + + SpessaSynthInfo( + `%cParsing finished! %c"${this.soundFontInfo["INAM"] || "UNNAMED"}"%c has %c${this.presets.length} %cpresets, %c${this.instruments.length}%c instruments and %c${this.samples.length}%c samples.`, consoleColors.info, consoleColors.recognized, @@ -116,40 +121,42 @@ class DLSSoundFont extends BasicSoundFont consoleColors.recognized, consoleColors.info, consoleColors.recognized, - consoleColors.info); + consoleColors.info + ); SpessaSynthGroupEnd(); } - + /** * @param chunk {RiffChunk} * @param expected {string} */ verifyHeader(chunk, expected) { - if(chunk.header.toLowerCase() !== expected.toLowerCase()) + if (chunk.header.toLowerCase() !== expected.toLowerCase()) { SpessaSynthGroupEnd(); throw new SyntaxError(`Invalid DLS chunk header! Expected "${expected.toLowerCase()}" got "${chunk.header.toLowerCase()}"`); } } - + /** * @param text {string} * @param expected {string} */ verifyText(text, expected) { - if(text.toLowerCase() !== expected.toLowerCase()) + if (text.toLowerCase() !== expected.toLowerCase()) { SpessaSynthGroupEnd(); throw new SyntaxError(`Invalid DLS soundfont! Expected "${expected.toLowerCase()}" got "${text.toLowerCase()}"`); } } } + DLSSoundFont.prototype.readDLSInstrumentList = readDLSInstrumentList; DLSSoundFont.prototype.readDLSInstrument = readDLSInstrument; DLSSoundFont.prototype.readRegion = readRegion; DLSSoundFont.prototype.readLart = readLart; DLSSoundFont.prototype.readDLSSamples = readDLSSamples; -export {DLSSoundFont} \ No newline at end of file +export { DLSSoundFont }; \ No newline at end of file diff --git a/src/spessasynth_lib/soundfont/dls/dls_sources.js b/src/spessasynth_lib/soundfont/dls/dls_sources.js index ba9a0a07..728fa59e 100644 --- a/src/spessasynth_lib/soundfont/dls/dls_sources.js +++ b/src/spessasynth_lib/soundfont/dls/dls_sources.js @@ -12,15 +12,15 @@ export const DLSSources = { polyPressure: 0x7, channelPressure: 0x8, vibratoLfo: 0x9, - + modulationWheel: 0x81, volume: 0x87, pan: 0x8a, expression: 0x8b, chorus: 0xdb, reverb: 0xdd, - + pitchWheelRange: 0x100, fineTune: 0x101, coarseTune: 0x102 -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/spessasynth_lib/soundfont/dls/dls_zone.js b/src/spessasynth_lib/soundfont/dls/dls_zone.js index 01cfd439..d620b512 100644 --- a/src/spessasynth_lib/soundfont/dls/dls_zone.js +++ b/src/spessasynth_lib/soundfont/dls/dls_zone.js @@ -1,4 +1,4 @@ -import { BasicInstrumentZone } from '../basic_soundfont/basic_zones.js' +import { BasicInstrumentZone } from "../basic_soundfont/basic_zones.js"; import { Generator, generatorTypes } from "../basic_soundfont/generator.js"; export class DLSZone extends BasicInstrumentZone @@ -14,7 +14,7 @@ export class DLSZone extends BasicInstrumentZone this.velRange = velRange; this.isGlobal = true; } - + /** * @param attenuationCb {number} with EMU correction * @param loopingMode {number} the sfont one @@ -31,56 +31,56 @@ export class DLSZone extends BasicInstrumentZone sampleKey, sample, sampleID, - samplePitchCorrection, + samplePitchCorrection ) { - if(loopingMode !== 0) + if (loopingMode !== 0) { this.generators.push(new Generator(generatorTypes.sampleModes, loopingMode)); } this.generators.push(new Generator(generatorTypes.initialAttenuation, attenuationCb)); this.isGlobal = false; - + // correct tuning if needed samplePitchCorrection -= sample.samplePitchCorrection; const coarseTune = Math.trunc(samplePitchCorrection / 100); - if(coarseTune !== 0) + if (coarseTune !== 0) { this.generators.push(new Generator(generatorTypes.coarseTune, coarseTune)); } const fineTune = samplePitchCorrection - (coarseTune * 100); - if(fineTune !== 0) + if (fineTune !== 0) { this.generators.push(new Generator(generatorTypes.fineTune, fineTune)); } - + // correct loop if needed const diffStart = loop.start - sample.sampleLoopStartIndex; const diffEnd = loop.end - sample.sampleLoopEndIndex; - if(diffStart !== 0) + if (diffStart !== 0) { const fine = diffStart % 32768; this.generators.push(new Generator(generatorTypes.startloopAddrsOffset, fine)); // coarse generator uses 32768 samples per step const coarse = Math.trunc(diffStart / 32768); - if(coarse !== 0) + if (coarse !== 0) { this.generators.push(new Generator(generatorTypes.startloopAddrsCoarseOffset, coarse)); } } - if(diffEnd !== 0) + if (diffEnd !== 0) { const fine = diffEnd % 32768; this.generators.push(new Generator(generatorTypes.endloopAddrsOffset, fine)); // coarse generator uses 32768 samples per step const coarse = Math.trunc(diffEnd / 32768); - if(coarse !== 0) + if (coarse !== 0) { this.generators.push(new Generator(generatorTypes.endloopAddrsCoarseOffset, coarse)); } } // correct key if needed - if(sampleKey !== sample.samplePitch) + if (sampleKey !== sample.samplePitch) { this.generators.push(new Generator(generatorTypes.overridingRootKey, sampleKey)); } diff --git a/src/spessasynth_lib/soundfont/dls/read_articulation.js b/src/spessasynth_lib/soundfont/dls/read_articulation.js index d348ef30..9db70330 100644 --- a/src/spessasynth_lib/soundfont/dls/read_articulation.js +++ b/src/spessasynth_lib/soundfont/dls/read_articulation.js @@ -1,9 +1,9 @@ -import { readLittleEndian } from '../../utils/byte_functions/little_endian.js' -import { DLSDestinations } from './dls_destinations.js' -import { DLSSources } from './dls_sources.js' -import { getSF2ModulatorFromArticulator } from './articulator_converter.js' -import { SpessaSynthInfo, SpessaSynthWarn } from '../../utils/loggin.js' -import { consoleColors } from '../../utils/other.js' +import { readLittleEndian } from "../../utils/byte_functions/little_endian.js"; +import { DLSDestinations } from "./dls_destinations.js"; +import { DLSSources } from "./dls_sources.js"; +import { getSF2ModulatorFromArticulator } from "./articulator_converter.js"; +import { SpessaSynthInfo, SpessaSynthWarn } from "../../utils/loggin.js"; +import { consoleColors } from "../../utils/other.js"; import { Generator, generatorTypes } from "../basic_soundfont/generator.js"; import { Modulator } from "../basic_soundfont/modulator.js"; @@ -45,7 +45,8 @@ function modulatorConverterDebug( consoleColors.recognized, consoleColors.info, consoleColors.recognized, - consoleColors.info); + consoleColors.info + ); } /** @@ -65,7 +66,7 @@ export function readArticulation(chunk, disableVibrato) * @type {Modulator[]} */ const modulators = []; - + // cbSize (ignore) readLittleEndian(artData, 4); const connectionsAmount = readLittleEndian(artData, 4); @@ -78,17 +79,17 @@ export function readArticulation(chunk, disableVibrato) const transform = readLittleEndian(artData, 2); const scale = readLittleEndian(artData, 4) | 0; const value = scale >> 16; // convert it to 16 bit as soundfont uses that - + // modulatorConverterDebug(source, control, destination, value, transform); // interpret this somehow... // if source and control are both zero, it's a generator - if(source === 0 && control === 0 && transform === 0) + if (source === 0 && control === 0 && transform === 0) { /** * @type {Generator} */ let generator; - switch(destination) + switch (destination) { case DLSDestinations.pan: generator = new Generator(generatorTypes.pan, value); // turn percent into tenths of percent @@ -102,7 +103,7 @@ export function readArticulation(chunk, disableVibrato) case DLSDestinations.filterQ: generator = new Generator(generatorTypes.initialFilterQ, value); break; - + // mod lfo raw values it seems case DLSDestinations.modLfoFreq: generator = new Generator(generatorTypes.freqModLFO, value); @@ -116,7 +117,7 @@ export function readArticulation(chunk, disableVibrato) case DLSDestinations.vibLfoDelay: generator = new Generator(generatorTypes.delayVibLFO, value); break; - + // vol env: all times are timecents like sf2 case DLSDestinations.volEnvDelay: generator = new Generator(generatorTypes.delayVolEnv, value); @@ -138,7 +139,7 @@ export function readArticulation(chunk, disableVibrato) const sustainDb = (1000 - value) / 10; generator = new Generator(generatorTypes.sustainVolEnv, sustainDb * 10); break; - + // mod env case DLSDestinations.modEnvDelay: generator = new Generator(generatorTypes.delayModEnv, value); @@ -160,7 +161,7 @@ export function readArticulation(chunk, disableVibrato) const percentageSustain = 1000 - value; generator = new Generator(generatorTypes.sustainModEnv, percentageSustain); break; - + case DLSDestinations.reverbSend: generator = new Generator(generatorTypes.reverbEffectsSend, value); break; @@ -175,56 +176,56 @@ export function readArticulation(chunk, disableVibrato) generators.push(new Generator(generatorTypes.coarseTune, semi)); break; } - if(generator) + if (generator) { generators.push(generator); } } else - // if not, modulator?? + // if not, modulator?? { let isGenerator = true; // a few special cases which are generators: - if(control === DLSSources.none) + if (control === DLSSources.none) { // mod lfo to pitch - if(source === DLSSources.modLfo && destination === DLSDestinations.pitch) + if (source === DLSSources.modLfo && destination === DLSDestinations.pitch) { generators.push(new Generator(generatorTypes.modLfoToPitch, value)); } else - // mod lfo to volume - if(source === DLSSources.modLfo && destination === DLSDestinations.gain) + // mod lfo to volume + if (source === DLSSources.modLfo && destination === DLSDestinations.gain) { generators.push(new Generator(generatorTypes.modLfoToVolume, value)); } else - // mod lfo to filter - if(source === DLSSources.modLfo && destination === DLSDestinations.filterCutoff) + // mod lfo to filter + if (source === DLSSources.modLfo && destination === DLSDestinations.filterCutoff) { generators.push(new Generator(generatorTypes.modLfoToFilterFc, value)); } else - // vib lfo to pitch - if(source === DLSSources.vibratoLfo && destination === DLSDestinations.pitch) + // vib lfo to pitch + if (source === DLSSources.vibratoLfo && destination === DLSDestinations.pitch) { generators.push(new Generator(generatorTypes.vibLfoToPitch, value)); } else - // mod env to pitch - if(source === DLSSources.modEnv && destination === DLSDestinations.pitch) + // mod env to pitch + if (source === DLSSources.modEnv && destination === DLSDestinations.pitch) { generators.push(new Generator(generatorTypes.modEnvToPitch, value)); } else - // mod env to filter - if(source === DLSSources.modEnv && destination === DLSDestinations.filterCutoff) + // mod env to filter + if (source === DLSSources.modEnv && destination === DLSDestinations.filterCutoff) { generators.push(new Generator(generatorTypes.modEnvToFilterFc, value)); } else - // key to vol env hold - if(source === DLSSources.keyNum && destination === DLSDestinations.volEnvHold) + // key to vol env hold + if (source === DLSSources.keyNum && destination === DLSDestinations.volEnvHold) { // according to viena and another strange (with modulators) rendition of gm.dls in sf2, // it shall be divided by -128 @@ -232,13 +233,17 @@ export function readArticulation(chunk, disableVibrato) // real + (60 / 128) * scale generators.push(new Generator(generatorTypes.keyNumToVolEnvHold, value / -128)); const correction = Math.round((60 / 128) * value); - generators.forEach(g => { - if(g.generatorType === generatorTypes.holdVolEnv) g.generatorValue += correction; + generators.forEach(g => + { + if (g.generatorType === generatorTypes.holdVolEnv) + { + g.generatorValue += correction; + } }); } else - // key to vol env decay - if(source === DLSSources.keyNum && destination === DLSDestinations.volEnvDecay) + // key to vol env decay + if (source === DLSSources.keyNum && destination === DLSDestinations.volEnvDecay) { // according to viena and another strange (with modulators) rendition of gm.dls in sf2, // it shall be divided by -128 @@ -246,13 +251,17 @@ export function readArticulation(chunk, disableVibrato) // real + (60 / 128) * scale generators.push(new Generator(generatorTypes.keyNumToVolEnvDecay, value / -128)); const correction = Math.round((60 / 128) * value); - generators.forEach(g => { - if(g.generatorType === generatorTypes.decayVolEnv) g.generatorValue += correction; + generators.forEach(g => + { + if (g.generatorType === generatorTypes.decayVolEnv) + { + g.generatorValue += correction; + } }); } else - // key to mod env hold - if(source === DLSSources.keyNum && destination === DLSDestinations.modEnvHold) + // key to mod env hold + if (source === DLSSources.keyNum && destination === DLSDestinations.modEnvHold) { // according to viena and another strange (with modulators) rendition of gm.dls in sf2, // it shall be divided by -128 @@ -260,13 +269,17 @@ export function readArticulation(chunk, disableVibrato) // real + (60 / 128) * scale generators.push(new Generator(generatorTypes.keyNumToModEnvHold, value / -128)); const correction = Math.round((60 / 128) * value); - generators.forEach(g => { - if(g.generatorType === generatorTypes.holdModEnv) g.generatorValue += correction; + generators.forEach(g => + { + if (g.generatorType === generatorTypes.holdModEnv) + { + g.generatorValue += correction; + } }); } else - // key to mod env decay - if(source === DLSSources.keyNum && destination === DLSDestinations.modEnvDecay) + // key to mod env decay + if (source === DLSSources.keyNum && destination === DLSDestinations.modEnvDecay) { // according to viena and another strange (with modulators) rendition of gm.dls in sf2, // it shall be divided by -128 @@ -274,21 +287,25 @@ export function readArticulation(chunk, disableVibrato) // real + (60 / 128) * scale generators.push(new Generator(generatorTypes.keyNumToModEnvDecay, value / -128)); const correction = Math.round((60 / 128) * value); - generators.forEach(g => { - if(g.generatorType === generatorTypes.decayModEnv) g.generatorValue += correction; + generators.forEach(g => + { + if (g.generatorType === generatorTypes.decayModEnv) + { + g.generatorValue += correction; + } }); } else { isGenerator = false; } - + } else { isGenerator = false; } - if(isGenerator === false) + if (isGenerator === false) { // UNCOMMENT TO ENABLE DEBUG // modulatorConverterDebug(source, control, destination, value, transform) @@ -300,23 +317,23 @@ export function readArticulation(chunk, disableVibrato) transform, value ); - if(mod) + if (mod) { // some articulators cannot be turned into modulators, that's why this check is a thing modulators.push(mod); - SpessaSynthInfo("%cSucceeded converting to SF2 Modulator!", consoleColors.recognized) + SpessaSynthInfo("%cSucceeded converting to SF2 Modulator!", consoleColors.recognized); } else { - SpessaSynthWarn("Failed converting to SF2 Modulator!") + SpessaSynthWarn("Failed converting to SF2 Modulator!"); } } } } - + // override reverb and chorus with 1000 instead of 200 (if not overriden) // reverb - if(modulators.find(m => m.modulatorDestination === generatorTypes.reverbEffectsSend) === undefined) + if (modulators.find(m => m.modulatorDestination === generatorTypes.reverbEffectsSend) === undefined) { modulators.push(new Modulator({ srcEnum: 0x00DB, @@ -327,7 +344,7 @@ export function readArticulation(chunk, disableVibrato) })); } // chorus - if(modulators.find(m => m.modulatorDestination === generatorTypes.chorusEffectsSend) === undefined) + if (modulators.find(m => m.modulatorDestination === generatorTypes.chorusEffectsSend) === undefined) { modulators.push(new Modulator({ srcEnum: 0x00DD, @@ -337,9 +354,9 @@ export function readArticulation(chunk, disableVibrato) transform: 0 })); } - + // it seems that dls 1 does not have vibrato lfo, so we shall disable it - if(disableVibrato) + if (disableVibrato) { modulators.push( // mod to vib @@ -360,6 +377,6 @@ export function readArticulation(chunk, disableVibrato) }) ); } - - return {modulators: modulators, generators: generators} + + return { modulators: modulators, generators: generators }; } \ No newline at end of file diff --git a/src/spessasynth_lib/soundfont/dls/read_instrument.js b/src/spessasynth_lib/soundfont/dls/read_instrument.js index dba19d0e..cff0b72b 100644 --- a/src/spessasynth_lib/soundfont/dls/read_instrument.js +++ b/src/spessasynth_lib/soundfont/dls/read_instrument.js @@ -1,10 +1,10 @@ -import { readBytesAsString } from '../../utils/byte_functions/string.js' -import { readLittleEndian } from '../../utils/byte_functions/little_endian.js' -import { DLSPreset } from './dls_preset.js' -import { findRIFFListType, readRIFFChunk } from '../basic_soundfont/riff_chunk.js' -import { SpessaSynthGroup, SpessaSynthGroupEnd } from '../../utils/loggin.js' -import { BasicInstrumentZone } from '../basic_soundfont/basic_zones.js' -import { consoleColors } from '../../utils/other.js' +import { readBytesAsString } from "../../utils/byte_functions/string.js"; +import { readLittleEndian } from "../../utils/byte_functions/little_endian.js"; +import { DLSPreset } from "./dls_preset.js"; +import { findRIFFListType, readRIFFChunk } from "../basic_soundfont/riff_chunk.js"; +import { SpessaSynthGroup, SpessaSynthGroupEnd } from "../../utils/loggin.js"; +import { BasicInstrumentZone } from "../basic_soundfont/basic_zones.js"; +import { consoleColors } from "../../utils/other.js"; /** * @this {DLSSoundFont} @@ -18,32 +18,32 @@ export function readDLSInstrument(chunk) * @type {RiffChunk[]} */ const chunks = []; - while(chunk.chunkData.length > chunk.chunkData.currentIndex) + while (chunk.chunkData.length > chunk.chunkData.currentIndex) { chunks.push(readRIFFChunk(chunk.chunkData)); } - - + + const instrumentHeader = chunks.find(c => c.header === "insh"); - if(!instrumentHeader) + if (!instrumentHeader) { SpessaSynthGroupEnd(); throw new Error("No instrument header!"); } - + // read instrument header const regions = readLittleEndian(instrumentHeader.chunkData, 4); const ulBank = readLittleEndian(instrumentHeader.chunkData, 4); const ulInstrument = readLittleEndian(instrumentHeader.chunkData, 4); const preset = new DLSPreset(ulBank, ulInstrument); - + // read preset name in INFO let presetName = "unnamedPreset"; const infoChunk = findRIFFListType(chunks, "INFO"); - if(infoChunk) + if (infoChunk) { let info = readRIFFChunk(infoChunk.chunkData); - while(info.header !== "INAM") + while (info.header !== "INAM") { info = readRIFFChunk(infoChunk.chunkData); } @@ -51,49 +51,51 @@ export function readDLSInstrument(chunk) } preset.presetName = presetName; preset.DLSInstrument.instrumentName = presetName; - SpessaSynthGroup(`%cParsing %c"${presetName}"%c...`, + SpessaSynthGroup( + `%cParsing %c"${presetName}"%c...`, consoleColors.info, consoleColors.recognized, - consoleColors.info); - + consoleColors.info + ); + // list of regions const regionListChunk = findRIFFListType(chunks, "lrgn"); - if(!regionListChunk) + if (!regionListChunk) { SpessaSynthGroupEnd(); throw new Error("No region list!"); } - + // global articulation: essentially global zone const globalZone = new BasicInstrumentZone(); globalZone.isGlobal = true; - + // read articulators const globalLart = findRIFFListType(chunks, "lart"); const globalLar2 = findRIFFListType(chunks, "lar2"); this.readLart(globalLart, globalLar2, globalZone); preset.DLSInstrument.instrumentZones.push(globalZone); - + // read regions for (let i = 0; i < regions; i++) { const chunk = readRIFFChunk(regionListChunk.chunkData); this.verifyHeader(chunk, "LIST"); const type = readBytesAsString(chunk.chunkData, 4); - if(type !== "rgn " && type !== "rgn2") + if (type !== "rgn " && type !== "rgn2") { SpessaSynthGroupEnd(); throw new SyntaxError(`Invalid DLS region! Expected "rgn " or "rgn2" got "${type}"`); } - - + + const zone = this.readRegion(chunk); - if(zone) + if (zone) { preset.DLSInstrument.instrumentZones.push(zone); } } - + this.presets.push(preset); this.instruments.push(preset.DLSInstrument); SpessaSynthGroupEnd(); diff --git a/src/spessasynth_lib/soundfont/dls/read_instrument_list.js b/src/spessasynth_lib/soundfont/dls/read_instrument_list.js index c4f287f4..a0f8ef3d 100644 --- a/src/spessasynth_lib/soundfont/dls/read_instrument_list.js +++ b/src/spessasynth_lib/soundfont/dls/read_instrument_list.js @@ -1,6 +1,6 @@ -import { readRIFFChunk } from '../basic_soundfont/riff_chunk.js' -import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd } from '../../utils/loggin.js' -import { consoleColors } from '../../utils/other.js' +import { readRIFFChunk } from "../basic_soundfont/riff_chunk.js"; +import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd } from "../../utils/loggin.js"; +import { consoleColors } from "../../utils/other.js"; /** * @this {DLSSoundFont} @@ -9,7 +9,7 @@ import { consoleColors } from '../../utils/other.js' export function readDLSInstrumentList(instrumentListChunk) { SpessaSynthGroupCollapsed("%cLoading instruments...", consoleColors.info); - for(let i = 0; i < this.instrumentAmount; i++) + for (let i = 0; i < this.instrumentAmount; i++) { this.readDLSInstrument(readRIFFChunk(instrumentListChunk.chunkData)); } diff --git a/src/spessasynth_lib/soundfont/dls/read_lart.js b/src/spessasynth_lib/soundfont/dls/read_lart.js index 382b5069..8da5f3a5 100644 --- a/src/spessasynth_lib/soundfont/dls/read_lart.js +++ b/src/spessasynth_lib/soundfont/dls/read_lart.js @@ -1,5 +1,5 @@ -import { readRIFFChunk } from '../basic_soundfont/riff_chunk.js' -import { readArticulation } from './read_articulation.js' +import { readRIFFChunk } from "../basic_soundfont/riff_chunk.js"; +import { readArticulation } from "./read_articulation.js"; /** * @param lartChunk {RiffChunk|undefined} @@ -9,9 +9,9 @@ import { readArticulation } from './read_articulation.js' */ export function readLart(lartChunk, lar2Chunk, zone) { - if(lartChunk) + if (lartChunk) { - while(lartChunk.chunkData.currentIndex < lartChunk.chunkData.length) + while (lartChunk.chunkData.currentIndex < lartChunk.chunkData.length) { const art1 = readRIFFChunk(lartChunk.chunkData); this.verifyHeader(art1, "art1"); @@ -20,10 +20,10 @@ export function readLart(lartChunk, lar2Chunk, zone) zone.modulators.push(...modsAndGens.modulators); } } - - if(lar2Chunk) + + if (lar2Chunk) { - while(lar2Chunk.chunkData.currentIndex < lar2Chunk.chunkData.length) + while (lar2Chunk.chunkData.currentIndex < lar2Chunk.chunkData.length) { const art2 = readRIFFChunk(lar2Chunk.chunkData); this.verifyHeader(art2, "art2"); diff --git a/src/spessasynth_lib/soundfont/dls/read_region.js b/src/spessasynth_lib/soundfont/dls/read_region.js index 222bed71..fab24dd2 100644 --- a/src/spessasynth_lib/soundfont/dls/read_region.js +++ b/src/spessasynth_lib/soundfont/dls/read_region.js @@ -1,6 +1,6 @@ -import { readLittleEndian, signedInt16 } from '../../utils/byte_functions/little_endian.js' -import { findRIFFListType, readRIFFChunk } from '../basic_soundfont/riff_chunk.js' -import { DLSZone } from './dls_zone.js' +import { readLittleEndian, signedInt16 } from "../../utils/byte_functions/little_endian.js"; +import { findRIFFListType, readRIFFChunk } from "../basic_soundfont/riff_chunk.js"; +import { DLSZone } from "./dls_zone.js"; import { Generator, generatorTypes } from "../basic_soundfont/generator.js"; /** @@ -11,17 +11,17 @@ import { Generator, generatorTypes } from "../basic_soundfont/generator.js"; export function readRegion(chunk) { // regions are essentially instrument zones - + /** * read chunks in the region * @type {RiffChunk[]} */ const regionChunks = []; - while(chunk.chunkData.length > chunk.chunkData.currentIndex) + while (chunk.chunkData.length > chunk.chunkData.currentIndex) { regionChunks.push(readRIFFChunk(chunk.chunkData)); } - + // region header const regionHeader = regionChunks.find(c => c.header === "rgnh"); // key range @@ -30,54 +30,54 @@ export function readRegion(chunk) // vel range const velMin = readLittleEndian(regionHeader.chunkData, 2); const velMax = readLittleEndian(regionHeader.chunkData, 2); - + const zone = new DLSZone( - {min: keyMin, max: keyMax}, - {min: velMin, max: velMax} - ) - + { min: keyMin, max: keyMax }, + { min: velMin, max: velMax } + ); + // fusOptions: no idea about that one??? readLittleEndian(regionHeader.chunkData, 2); - + // keyGroup: essentially exclusive class const exclusive = readLittleEndian(regionHeader.chunkData, 2); - if(exclusive !== 0) + if (exclusive !== 0) { zone.generators.push(new Generator(generatorTypes.exclusiveClass, exclusive)); } - + // lart const lart = findRIFFListType(regionChunks, "lart"); const lar2 = findRIFFListType(regionChunks, "lar2"); this.readLart(lart, lar2, zone); - + // wsmpl: wave sample chunk zone.isGlobal = false; const waveSampleChunk = regionChunks.find(c => c.header === "wsmp"); // cbSize readLittleEndian(waveSampleChunk.chunkData, 4); const originalKey = readLittleEndian(waveSampleChunk.chunkData, 2); - + // sFineTune const pitchCorrection = signedInt16( waveSampleChunk.chunkData[waveSampleChunk.chunkData.currentIndex++], waveSampleChunk.chunkData[waveSampleChunk.chunkData.currentIndex++] ); - + // gain correction: Each unit of gain represents 1/655360 dB // it is set after linking the sample const gainCorrection = readLittleEndian(waveSampleChunk.chunkData, 4); // convert to signed and turn into attenuation (invert) const dbCorrection = (gainCorrection | 0) / -655360; - + // skip options readLittleEndian(waveSampleChunk.chunkData, 4); - + // read loop count (always one or zero) const loopsAmount = readLittleEndian(waveSampleChunk.chunkData, 4); let loopingMode; - const loop = {start: 0, end: 0}; - if(loopsAmount === 0) + const loop = { start: 0, end: 0 }; + if (loopsAmount === 0) { // no loop loopingMode = 0; @@ -88,7 +88,7 @@ export function readRegion(chunk) readLittleEndian(waveSampleChunk.chunkData, 4); // loop type: loop normally or loop until release (like soundfont) const loopType = readLittleEndian(waveSampleChunk.chunkData, 4); // why is it long??? - if(loopType === 0) + if (loopType === 0) { loopingMode = 1; } @@ -100,15 +100,15 @@ export function readRegion(chunk) const loopLength = readLittleEndian(waveSampleChunk.chunkData, 4); loop.end = loop.start + loopLength; } - + // wave link const waveLinkChunk = regionChunks.find(c => c.header === "wlnk"); - if(waveLinkChunk === undefined) + if (waveLinkChunk === undefined) { // no wave link = no sample. What? Why is it even here then???? return undefined; } - + // flags readLittleEndian(waveLinkChunk.chunkData, 2); // phasse group @@ -121,22 +121,23 @@ export function readRegion(chunk) * @type {DLSSample} */ const sample = this.samples[sampleID]; - if(sample === undefined) + if (sample === undefined) { throw new Error("Invalid sample ID!"); } - + // this correction overrides the sample gain correction const actualDbCorrection = dbCorrection || sample.sampleDbAttenuation; // convert to centibels const attenuation = (actualDbCorrection * 10) / 0.4; // make sure to apply EMU correction - + zone.setWavesample( attenuation, loopingMode, loop, originalKey, sample, sampleID, - pitchCorrection); + pitchCorrection + ); return zone; } \ No newline at end of file diff --git a/src/spessasynth_lib/soundfont/dls/read_samples.js b/src/spessasynth_lib/soundfont/dls/read_samples.js index 47aa9ab7..5af8e434 100644 --- a/src/spessasynth_lib/soundfont/dls/read_samples.js +++ b/src/spessasynth_lib/soundfont/dls/read_samples.js @@ -1,13 +1,14 @@ -import { findRIFFListType, readRIFFChunk } from '../basic_soundfont/riff_chunk.js' -import { readBytesAsString } from '../../utils/byte_functions/string.js' +import { findRIFFListType, readRIFFChunk } from "../basic_soundfont/riff_chunk.js"; +import { readBytesAsString } from "../../utils/byte_functions/string.js"; import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, - SpessaSynthInfo, SpessaSynthWarn, -} from '../../utils/loggin.js' -import { consoleColors } from '../../utils/other.js' -import { readLittleEndian, signedInt16 } from '../../utils/byte_functions/little_endian.js' -import { DLSSample } from './dls_sample.js' + SpessaSynthInfo, + SpessaSynthWarn +} from "../../utils/loggin.js"; +import { consoleColors } from "../../utils/other.js"; +import { readLittleEndian, signedInt16 } from "../../utils/byte_functions/little_endian.js"; +import { DLSSample } from "./dls_sample.js"; /** * @this {DLSSoundFont} @@ -15,36 +16,38 @@ import { DLSSample } from './dls_sample.js' */ export function readDLSSamples(waveListChunk) { - SpessaSynthGroupCollapsed("%cLoading Wave samples...", - consoleColors.recognized); + SpessaSynthGroupCollapsed( + "%cLoading Wave samples...", + consoleColors.recognized + ); let sampleID = 0; - while(waveListChunk.chunkData.currentIndex < waveListChunk.chunkData.length) + while (waveListChunk.chunkData.currentIndex < waveListChunk.chunkData.length) { const waveChunk = readRIFFChunk(waveListChunk.chunkData); this.verifyHeader(waveChunk, "LIST"); this.verifyText(readBytesAsString(waveChunk.chunkData, 4), "wave"); - + /** * @type {RiffChunk[]} */ const waveChunks = []; - while(waveChunk.chunkData.currentIndex < waveChunk.chunkData.length) + while (waveChunk.chunkData.currentIndex < waveChunk.chunkData.length) { waveChunks.push(readRIFFChunk(waveChunk.chunkData)); } - + const fmtChunk = waveChunks.find(c => c.header === "fmt "); - if(!fmtChunk) + if (!fmtChunk) { throw new Error("No fmt chunk in the wave file!"); } const waveFormat = readLittleEndian(fmtChunk.chunkData, 2); - if(waveFormat !== 1) + if (waveFormat !== 1) { throw new Error("Only PCM format in WAVE is supported."); } const channelsAmount = readLittleEndian(fmtChunk.chunkData, 2); - if(channelsAmount !== 1) + if (channelsAmount !== 1) { throw new Error("Only mono samples are supported."); } @@ -56,13 +59,13 @@ export function readDLSSamples(waveListChunk) // it's bits per sample because one channel const wBitsPerSample = readLittleEndian(fmtChunk.chunkData, 2); const bytesPerSample = wBitsPerSample / 8; - + const maxSampleValue = Math.pow(2, bytesPerSample * 8 - 1); // Max value for the sample const maxUnsigned = Math.pow(2, bytesPerSample * 8); - + let normalizationFactor; let isUnsigned = false; - + if (wBitsPerSample === 8) { normalizationFactor = 255; // For 8-bit normalize from 0-255 @@ -74,7 +77,7 @@ export function readDLSSamples(waveListChunk) } // read the data const dataChunk = waveChunks.find(c => c.header === "data"); - if(!dataChunk) + if (!dataChunk) { throw new Error("No data chunk in the wave chunk!"); } @@ -100,17 +103,17 @@ export function readDLSSamples(waveListChunk) sampleData[i] = sample / normalizationFactor; } } - + // sane defaults let sampleKey = 60; let samplePitch = 0; let sampleLoopStart = 0; let sampleLoopEnd = sampleData.length - 1; let sampleDbAttenuation = 0; - + // read wsmp - const wsmpChunk = waveChunks.find(c => c.header === "wsmp") - if(wsmpChunk) + const wsmpChunk = waveChunks.find(c => c.header === "wsmp"); + if (wsmpChunk) { // skip cbsize readLittleEndian(wsmpChunk.chunkData, 4); @@ -128,7 +131,7 @@ export function readDLSSamples(waveListChunk) // no idea about ful options readLittleEndian(wsmpChunk.chunkData, 4); const loopsAmount = readLittleEndian(wsmpChunk.chunkData, 4); - if(loopsAmount === 1) + if (loopsAmount === 1) { // skip size and type readLittleEndian(wsmpChunk.chunkData, 8); @@ -139,25 +142,25 @@ export function readDLSSamples(waveListChunk) } else { - SpessaSynthWarn("No wsmp chunk in wave... using sane defaults.") + SpessaSynthWarn("No wsmp chunk in wave... using sane defaults."); } - + // read sample name const waveInfo = findRIFFListType(waveChunks, "INFO"); let sampleName = `Unnamed ${sampleID}`; - if(waveInfo) + if (waveInfo) { let infoChunk = readRIFFChunk(waveInfo.chunkData); - while(infoChunk.header !== "INAM" && waveInfo.chunkData.currentIndex < waveInfo.chunkData.length) + while (infoChunk.header !== "INAM" && waveInfo.chunkData.currentIndex < waveInfo.chunkData.length) { infoChunk = readRIFFChunk(waveInfo.chunkData); } - if(infoChunk.header === "INAM") + if (infoChunk.header === "INAM") { sampleName = readBytesAsString(infoChunk.chunkData, infoChunk.size).trim(); } } - + this.samples.push(new DLSSample( sampleName, sampleRate, @@ -168,11 +171,13 @@ export function readDLSSamples(waveListChunk) sampleData, sampleDbAttenuation )); - + sampleID++; - SpessaSynthInfo(`%cLoaded sample %c${sampleName}`, + SpessaSynthInfo( + `%cLoaded sample %c${sampleName}`, consoleColors.info, - consoleColors.recognized); + consoleColors.recognized + ); } SpessaSynthGroupEnd(); } \ No newline at end of file diff --git a/src/spessasynth_lib/soundfont/load_soundfont.js b/src/spessasynth_lib/soundfont/load_soundfont.js index eec4c90a..74553cdd 100644 --- a/src/spessasynth_lib/soundfont/load_soundfont.js +++ b/src/spessasynth_lib/soundfont/load_soundfont.js @@ -1,7 +1,7 @@ -import { IndexedByteArray } from '../utils/indexed_array.js' -import { readBytesAsString } from '../utils/byte_functions/string.js' -import { DLSSoundFont } from './dls/dls_soundfont.js' -import { SoundFont2 } from './read_sf2/soundfont.js' +import { IndexedByteArray } from "../utils/indexed_array.js"; +import { readBytesAsString } from "../utils/byte_functions/string.js"; +import { DLSSoundFont } from "./dls/dls_soundfont.js"; +import { SoundFont2 } from "./read_sf2/soundfont.js"; /** * Loads a soundfont file @@ -13,7 +13,7 @@ export function loadSoundFont(buffer) const check = buffer.slice(8, 12); const a = new IndexedByteArray(check); const id = readBytesAsString(a, 4, undefined, false).toLowerCase(); - if(id === "dls ") + if (id === "dls ") { return new DLSSoundFont(buffer); } diff --git a/src/spessasynth_lib/soundfont/read_sf2/generators.js b/src/spessasynth_lib/soundfont/read_sf2/generators.js index 336a9b31..050d9424 100644 --- a/src/spessasynth_lib/soundfont/read_sf2/generators.js +++ b/src/spessasynth_lib/soundfont/read_sf2/generators.js @@ -33,11 +33,11 @@ export class ReadGenerator extends Generator export function readGenerators(generatorChunk) { let gens = []; - while(generatorChunk.chunkData.length > generatorChunk.chunkData.currentIndex) + while (generatorChunk.chunkData.length > generatorChunk.chunkData.currentIndex) { gens.push(new ReadGenerator(generatorChunk.chunkData)); } - if(gens.length > 1) + if (gens.length > 1) { // remove terminal gens.pop(); diff --git a/src/spessasynth_lib/soundfont/read_sf2/instruments.js b/src/spessasynth_lib/soundfont/read_sf2/instruments.js index 0aa4f612..258be8e6 100644 --- a/src/spessasynth_lib/soundfont/read_sf2/instruments.js +++ b/src/spessasynth_lib/soundfont/read_sf2/instruments.js @@ -1,8 +1,8 @@ -import {RiffChunk} from "../basic_soundfont/riff_chunk.js"; -import {InstrumentZone} from "./zones.js"; -import {readLittleEndian} from "../../utils/byte_functions/little_endian.js"; -import { readBytesAsString } from '../../utils/byte_functions/string.js' -import { BasicInstrument } from '../basic_soundfont/basic_instrument.js' +import { RiffChunk } from "../basic_soundfont/riff_chunk.js"; +import { InstrumentZone } from "./zones.js"; +import { readLittleEndian } from "../../utils/byte_functions/little_endian.js"; +import { readBytesAsString } from "../../utils/byte_functions/string.js"; +import { BasicInstrument } from "../basic_soundfont/basic_instrument.js"; /** * instrument.js @@ -22,7 +22,7 @@ export class Instrument extends BasicInstrument this.instrumentZoneIndex = readLittleEndian(instrumentChunk.chunkData, 2); this.instrumentZonesAmount = 0; } - + /** * Loads all the instrument zones, given the amount * @param amount {number} @@ -31,7 +31,7 @@ export class Instrument extends BasicInstrument getInstrumentZones(amount, zones) { this.instrumentZonesAmount = amount; - for(let i = this.instrumentZoneIndex; i < this.instrumentZonesAmount + this.instrumentZoneIndex; i++) + for (let i = this.instrumentZoneIndex; i < this.instrumentZonesAmount + this.instrumentZoneIndex; i++) { this.instrumentZones.push(zones[i]); } @@ -47,17 +47,17 @@ export class Instrument extends BasicInstrument export function readInstruments(instrumentChunk, instrumentZones) { let instruments = []; - while(instrumentChunk.chunkData.length > instrumentChunk.chunkData.currentIndex) + while (instrumentChunk.chunkData.length > instrumentChunk.chunkData.currentIndex) { let instrument = new Instrument(instrumentChunk); - if(instruments.length > 0) + if (instruments.length > 0) { let instrumentsAmount = instrument.instrumentZoneIndex - instruments[instruments.length - 1].instrumentZoneIndex; instruments[instruments.length - 1].getInstrumentZones(instrumentsAmount, instrumentZones); } instruments.push(instrument); } - if(instruments.length > 1) + if (instruments.length > 1) { // remove EOI instruments.pop(); diff --git a/src/spessasynth_lib/soundfont/read_sf2/modulators.js b/src/spessasynth_lib/soundfont/read_sf2/modulators.js index 9ccad594..f5c5b3d5 100644 --- a/src/spessasynth_lib/soundfont/read_sf2/modulators.js +++ b/src/spessasynth_lib/soundfont/read_sf2/modulators.js @@ -1,8 +1,6 @@ import { readLittleEndian, signedInt16 } from "../../utils/byte_functions/little_endian.js"; import { IndexedByteArray } from "../../utils/indexed_array.js"; -import { - Modulator, -} from "../basic_soundfont/modulator.js"; +import { Modulator } from "../basic_soundfont/modulator.js"; export class ReadModulator extends Modulator @@ -31,7 +29,7 @@ export class ReadModulator extends Modulator export function readModulators(modulatorChunk) { let gens = []; - while(modulatorChunk.chunkData.length > modulatorChunk.chunkData.currentIndex) + while (modulatorChunk.chunkData.length > modulatorChunk.chunkData.currentIndex) { gens.push(new ReadModulator(modulatorChunk.chunkData)); } diff --git a/src/spessasynth_lib/soundfont/read_sf2/presets.js b/src/spessasynth_lib/soundfont/read_sf2/presets.js index cadf0e45..d5be4e07 100644 --- a/src/spessasynth_lib/soundfont/read_sf2/presets.js +++ b/src/spessasynth_lib/soundfont/read_sf2/presets.js @@ -1,8 +1,8 @@ -import {RiffChunk} from "../basic_soundfont/riff_chunk.js"; -import {PresetZone} from "./zones.js"; -import {readLittleEndian} from "../../utils/byte_functions/little_endian.js"; -import { readBytesAsString } from '../../utils/byte_functions/string.js' -import { BasicPreset } from '../basic_soundfont/basic_preset.js' +import { RiffChunk } from "../basic_soundfont/riff_chunk.js"; +import { PresetZone } from "./zones.js"; +import { readLittleEndian } from "../../utils/byte_functions/little_endian.js"; +import { readBytesAsString } from "../../utils/byte_functions/string.js"; +import { BasicPreset } from "../basic_soundfont/basic_preset.js"; /** * parses soundfont presets, also includes function for getting the generators and samples from midi note and velocity @@ -21,18 +21,18 @@ export class Preset extends BasicPreset this.presetName = readBytesAsString(presetChunk.chunkData, 20) .trim() .replace(/\d{3}:\d{3}/, ""); // remove those pesky "000:001" - + this.program = readLittleEndian(presetChunk.chunkData, 2); this.bank = readLittleEndian(presetChunk.chunkData, 2); this.presetZoneStartIndex = readLittleEndian(presetChunk.chunkData, 2); - + // read the dwords this.library = readLittleEndian(presetChunk.chunkData, 4); this.genre = readLittleEndian(presetChunk.chunkData, 4); this.morphology = readLittleEndian(presetChunk.chunkData, 4); this.presetZonesAmount = 0; } - + /** * Loads all the preset zones, given the amount * @param amount {number} @@ -61,10 +61,10 @@ export function readPresets(presetChunk, presetZones, defaultModulators) * @type {Preset[]} */ let presets = []; - while(presetChunk.chunkData.length > presetChunk.chunkData.currentIndex) + while (presetChunk.chunkData.length > presetChunk.chunkData.currentIndex) { let preset = new Preset(presetChunk, defaultModulators); - if(presets.length > 0) + if (presets.length > 0) { let presetZonesAmount = preset.presetZoneStartIndex - presets[presets.length - 1].presetZoneStartIndex; presets[presets.length - 1].getPresetZones(presetZonesAmount, presetZones); diff --git a/src/spessasynth_lib/soundfont/read_sf2/samples.js b/src/spessasynth_lib/soundfont/read_sf2/samples.js index 02d27bac..93d09dd4 100644 --- a/src/spessasynth_lib/soundfont/read_sf2/samples.js +++ b/src/spessasynth_lib/soundfont/read_sf2/samples.js @@ -1,10 +1,10 @@ -import { RiffChunk } from '../basic_soundfont/riff_chunk.js' -import { IndexedByteArray } from '../../utils/indexed_array.js' -import { readLittleEndian, signedInt8 } from '../../utils/byte_functions/little_endian.js' -import { stbvorbis } from '../../externals/stbvorbis_sync/stbvorbis_sync.min.js' -import { SpessaSynthWarn } from '../../utils/loggin.js' -import { readBytesAsString } from '../../utils/byte_functions/string.js' -import { BasicSample } from '../basic_soundfont/basic_sample.js' +import { RiffChunk } from "../basic_soundfont/riff_chunk.js"; +import { IndexedByteArray } from "../../utils/indexed_array.js"; +import { readLittleEndian, signedInt8 } from "../../utils/byte_functions/little_endian.js"; +import { stbvorbis } from "../../externals/stbvorbis_sync/stbvorbis_sync.min.js"; +import { SpessaSynthWarn } from "../../utils/loggin.js"; +import { readBytesAsString } from "../../utils/byte_functions/string.js"; +import { BasicSample } from "../basic_soundfont/basic_sample.js"; export class LoadedSample extends BasicSample { @@ -37,8 +37,8 @@ export class LoadedSample extends BasicSample sampleType, smplArr, sampleIndex, - isDataRaw, - ) + isDataRaw + ) { super( sampleName, @@ -49,8 +49,8 @@ export class LoadedSample extends BasicSample sampleType, sampleLoopStartIndex - (sampleStartIndex / 2), sampleLoopEndIndex - (sampleStartIndex / 2) - ); - this.sampleName = sampleName + ); + this.sampleName = sampleName; // in bytes this.sampleStartIndex = sampleStartIndex; this.sampleEndIndex = sampleEndIndex; @@ -60,7 +60,7 @@ export class LoadedSample extends BasicSample this.sampleLength = this.sampleEndIndex - this.sampleStartIndex; this.sampleDataArray = smplArr; this.sampleData = new Float32Array(0); - if(this.isCompressed) + if (this.isCompressed) { // correct loop points this.sampleLoopStartIndex += this.sampleStartIndex / 2; @@ -69,7 +69,7 @@ export class LoadedSample extends BasicSample } this.isDataRaw = isDataRaw; } - + /** * Get raw data, whether it's compressed or not as we simply write it to the file * @return {Uint8Array} @@ -77,9 +77,9 @@ export class LoadedSample extends BasicSample getRawData() { const smplArr = this.sampleDataArray; - if(this.isCompressed) + if (this.isCompressed) { - if(this.compressedData) + if (this.compressedData) { return this.compressedData; } @@ -88,15 +88,15 @@ export class LoadedSample extends BasicSample } else { - if(!this.isDataRaw) + if (!this.isDataRaw) { throw new Error("Writing SF2Pack samples is not supported."); } const dataStartIndex = smplArr.currentIndex; - return smplArr.slice(dataStartIndex + this.sampleStartIndex, dataStartIndex + this.sampleEndIndex) + return smplArr.slice(dataStartIndex + this.sampleStartIndex, dataStartIndex + this.sampleEndIndex); } } - + /** * Decode binary vorbis into a float32 pcm */ @@ -119,7 +119,7 @@ export class LoadedSample extends BasicSample const vorbis = stbvorbis.decode(buff.buffer); this.sampleData = vorbis.data[0]; } - + /** * Loads the audio data and stores it for reuse * @returns {Float32Array} The audioData @@ -134,15 +134,15 @@ export class LoadedSample extends BasicSample // eos, do not do anything return new Float32Array(1); } - - if(this.isCompressed) + + if (this.isCompressed) { // if compressed, decode this.decodeVorbis(); this.isSampleLoaded = true; return this.sampleData; } - else if(!this.isDataRaw) + else if (!this.isDataRaw) { return this.getUncompressedReadyData(); } @@ -150,18 +150,18 @@ export class LoadedSample extends BasicSample } return this.sampleData; } - + /** * @returns {Float32Array} */ loadUncompressedData() { - if(this.isCompressed) + if (this.isCompressed) { SpessaSynthWarn("Trying to load a compressed sample via loadUncompressedData()... aborting!"); return new Float32Array(0); } - + // read the sample data let audioData = new Float32Array(this.sampleLength / 2); const dataStartIndex = this.sampleDataArray.currentIndex; @@ -169,18 +169,18 @@ export class LoadedSample extends BasicSample this.sampleDataArray.slice(dataStartIndex + this.sampleStartIndex, dataStartIndex + this.sampleEndIndex) .buffer ); - + // convert to float - for(let i = 0; i < convertedSigned16.length; i++) + for (let i = 0; i < convertedSigned16.length; i++) { audioData[i] = convertedSigned16[i] / 32768; } - + this.sampleData = audioData; this.isSampleLoaded = true; return audioData; } - + /** * @returns {Float32Array} */ @@ -211,7 +211,7 @@ export function readSamples(sampleHeadersChunk, smplChunkData, isSmplDataRaw = t */ let samples = []; let index = 0; - while(sampleHeadersChunk.chunkData.length > sampleHeadersChunk.chunkData.currentIndex) + while (sampleHeadersChunk.chunkData.length > sampleHeadersChunk.chunkData.currentIndex) { const sample = readSample(index, sampleHeadersChunk.chunkData, smplChunkData, isSmplDataRaw); samples.push(sample); @@ -233,45 +233,46 @@ export function readSamples(sampleHeadersChunk, smplChunkData, isSmplDataRaw = t * @param isDataRaw {boolean} true means binary 16 bit data, false means float32 * @returns {LoadedSample} */ -function readSample(index, sampleHeaderData, smplArrayData, isDataRaw) { - +function readSample(index, sampleHeaderData, smplArrayData, isDataRaw) +{ + // read the sample name let sampleName = readBytesAsString(sampleHeaderData, 20); - + // read the sample start index let sampleStartIndex = readLittleEndian(sampleHeaderData, 4) * 2; - + // read the sample end index let sampleEndIndex = readLittleEndian(sampleHeaderData, 4) * 2; - + // read the sample looping start index let sampleLoopStartIndex = readLittleEndian(sampleHeaderData, 4); - + // read the sample looping end index let sampleLoopEndIndex = readLittleEndian(sampleHeaderData, 4); - + // read the sample rate let sampleRate = readLittleEndian(sampleHeaderData, 4); - + // read the original sample pitch let samplePitch = sampleHeaderData[sampleHeaderData.currentIndex++]; - if(samplePitch === 255) + if (samplePitch === 255) { // if it's 255, then default to 60 samplePitch = 60; } - + // readt the sample pitch correction let samplePitchCorrection = signedInt8(sampleHeaderData[sampleHeaderData.currentIndex++]); - - + + // read the link to the other channel let sampleLink = readLittleEndian(sampleHeaderData, 2); let sampleType = readLittleEndian(sampleHeaderData, 2); - - - - return new LoadedSample(sampleName, + + + return new LoadedSample( + sampleName, sampleStartIndex, sampleEndIndex, sampleLoopStartIndex, @@ -283,5 +284,6 @@ function readSample(index, sampleHeaderData, smplArrayData, isDataRaw) { sampleType, smplArrayData, index, - isDataRaw); + isDataRaw + ); } \ No newline at end of file diff --git a/src/spessasynth_lib/soundfont/read_sf2/soundfont.js b/src/spessasynth_lib/soundfont/read_sf2/soundfont.js index 1ac0446b..a3abfe08 100644 --- a/src/spessasynth_lib/soundfont/read_sf2/soundfont.js +++ b/src/spessasynth_lib/soundfont/read_sf2/soundfont.js @@ -1,17 +1,17 @@ -import { IndexedByteArray } from '../../utils/indexed_array.js' +import { IndexedByteArray } from "../../utils/indexed_array.js"; import { readSamples } from "./samples.js"; -import { readLittleEndian } from '../../utils/byte_functions/little_endian.js' -import { readGenerators } from './generators.js' -import { readInstrumentZones, InstrumentZone, readPresetZones } from "./zones.js"; +import { readLittleEndian } from "../../utils/byte_functions/little_endian.js"; +import { readGenerators } from "./generators.js"; +import { InstrumentZone, readInstrumentZones, readPresetZones } from "./zones.js"; import { readPresets } from "./presets.js"; import { readInstruments } from "./instruments.js"; import { readModulators } from "./modulators.js"; -import { readRIFFChunk, RiffChunk } from '../basic_soundfont/riff_chunk.js' -import { consoleColors } from '../../utils/other.js' -import { SpessaSynthGroup, SpessaSynthGroupEnd, SpessaSynthInfo } from '../../utils/loggin.js' -import { readBytesAsString } from '../../utils/byte_functions/string.js' +import { readRIFFChunk, RiffChunk } from "../basic_soundfont/riff_chunk.js"; +import { consoleColors } from "../../utils/other.js"; +import { SpessaSynthGroup, SpessaSynthGroupEnd, SpessaSynthInfo } from "../../utils/loggin.js"; +import { readBytesAsString } from "../../utils/byte_functions/string.js"; import { stbvorbis } from "../../externals/stbvorbis_sync/stbvorbis_sync.min.js"; -import { BasicSoundFont } from '../basic_soundfont/basic_soundfont.js' +import { BasicSoundFont } from "../basic_soundfont/basic_soundfont.js"; import { Generator } from "../basic_soundfont/generator.js"; import { Modulator } from "../basic_soundfont/modulator.js"; @@ -30,24 +30,24 @@ export class SoundFont2 extends BasicSoundFont constructor(arrayBuffer, warnDeprecated = true) { super(); - if(warnDeprecated) + if (warnDeprecated) { console.warn("Using the constructor directly is deprecated. Use loadSoundFont instead."); } this.dataArray = new IndexedByteArray(arrayBuffer); SpessaSynthGroup("%cParsing SoundFont...", consoleColors.info); - if(!this.dataArray) + if (!this.dataArray) { SpessaSynthGroupEnd(); throw new TypeError("No data!"); } - + // read the main read let firstChunk = readRIFFChunk(this.dataArray, false); this.verifyHeader(firstChunk, "riff"); - - const type = readBytesAsString(this.dataArray,4).toLowerCase(); - if(type !== "sfbk" && type !== "sfpk") + + const type = readBytesAsString(this.dataArray, 4).toLowerCase(); + if (type !== "sfbk" && type !== "sfpk") { SpessaSynthGroupEnd(); throw new SyntaxError(`Invalid soundFont! Expected "sfbk" or "sfpk" got "${type}"`); @@ -58,13 +58,13 @@ export class SoundFont2 extends BasicSoundFont and the only other difference is that the main chunk isn't "sfbk" but rather "sfpk" */ const isSF2Pack = type === "sfpk"; - + // INFO let infoChunk = readRIFFChunk(this.dataArray); this.verifyHeader(infoChunk, "list"); readBytesAsString(infoChunk.chunkData, 4); - - while(infoChunk.chunkData.length > infoChunk.chunkData.currentIndex) + + while (infoChunk.chunkData.length > infoChunk.chunkData.currentIndex) { let chunk = readRIFFChunk(infoChunk.chunkData); let text; @@ -76,12 +76,12 @@ export class SoundFont2 extends BasicSoundFont text = `${readLittleEndian(chunk.chunkData, 2)}.${readLittleEndian(chunk.chunkData, 2)}`; this.soundFontInfo[chunk.header] = text; break; - + case "icmt": text = readBytesAsString(chunk.chunkData, chunk.chunkData.length, undefined, false); this.soundFontInfo[chunk.header] = text; break; - + // dmod: default modulators case "dmod": const newModulators = readModulators(chunk); @@ -89,29 +89,34 @@ export class SoundFont2 extends BasicSoundFont text = `Modulators: ${newModulators.length}`; // override default modulators const oldDefaults = this.defaultModulators; - + this.defaultModulators = newModulators; - this.defaultModulators.push(...oldDefaults.filter(m => !this.defaultModulators.find(mm => Modulator.isIdentical(m, mm)))); + this.defaultModulators.push(...oldDefaults.filter(m => !this.defaultModulators.find(mm => Modulator.isIdentical( + m, + mm + )))); this.soundFontInfo[chunk.header] = chunk.chunkData; break; - + default: text = readBytesAsString(chunk.chunkData, chunk.chunkData.length); this.soundFontInfo[chunk.header] = text; } - - SpessaSynthInfo(`%c"${chunk.header}": %c"${text}"`, + + SpessaSynthInfo( + `%c"${chunk.header}": %c"${text}"`, consoleColors.info, - consoleColors.recognized); + consoleColors.recognized + ); } - + // SDTA const sdtaChunk = readRIFFChunk(this.dataArray, false); - this.verifyHeader(sdtaChunk, "list") + this.verifyHeader(sdtaChunk, "list"); this.verifyText(readBytesAsString(this.dataArray, 4), "sdta"); - + // smpl - SpessaSynthInfo("%cVerifying smpl chunk...", consoleColors.warn) + SpessaSynthInfo("%cVerifying smpl chunk...", consoleColors.warn); let sampleDataChunk = readRIFFChunk(this.dataArray, false); this.verifyHeader(sampleDataChunk, "smpl"); /** @@ -119,25 +124,31 @@ export class SoundFont2 extends BasicSoundFont */ let sampleData; // SF2Pack: the entire data is compressed - if(isSF2Pack) + if (isSF2Pack) { - SpessaSynthInfo("%cSF2Pack detected, attempting to decode the smpl chunk...", - consoleColors.info); + SpessaSynthInfo( + "%cSF2Pack detected, attempting to decode the smpl chunk...", + consoleColors.info + ); try { /** * @type {Float32Array} */ - sampleData = stbvorbis.decode(this.dataArray.buffer.slice(this.dataArray.currentIndex, this.dataArray.currentIndex + sdtaChunk.size - 12)).data[0]; - } - catch (e) + sampleData = stbvorbis.decode(this.dataArray.buffer.slice( + this.dataArray.currentIndex, + this.dataArray.currentIndex + sdtaChunk.size - 12 + )).data[0]; + } catch (e) { SpessaSynthGroupEnd(); throw new Error(`SF2Pack Ogg Vorbis decode error: ${e}`); } - SpessaSynthInfo(`%cDecoded the smpl chunk! Length: %c${sampleData.length}`, + SpessaSynthInfo( + `%cDecoded the smpl chunk! Length: %c${sampleData.length}`, consoleColors.info, - consoleColors.value); + consoleColors.value + ); } else { @@ -147,94 +158,99 @@ export class SoundFont2 extends BasicSoundFont sampleData = this.dataArray; this.sampleDataStartIndex = this.dataArray.currentIndex; } - - SpessaSynthInfo(`%cSkipping sample chunk, length: %c${sdtaChunk.size - 12}`, + + SpessaSynthInfo( + `%cSkipping sample chunk, length: %c${sdtaChunk.size - 12}`, consoleColors.info, - consoleColors.value); + consoleColors.value + ); this.dataArray.currentIndex += sdtaChunk.size - 12; - + // PDTA - SpessaSynthInfo("%cLoading preset data chunk...", consoleColors.warn) + SpessaSynthInfo("%cLoading preset data chunk...", consoleColors.warn); let presetChunk = readRIFFChunk(this.dataArray); this.verifyHeader(presetChunk, "list"); readBytesAsString(presetChunk.chunkData, 4); - + // read the hydra chunks const presetHeadersChunk = readRIFFChunk(presetChunk.chunkData); this.verifyHeader(presetHeadersChunk, "phdr"); - + const presetZonesChunk = readRIFFChunk(presetChunk.chunkData); this.verifyHeader(presetZonesChunk, "pbag"); - + const presetModulatorsChunk = readRIFFChunk(presetChunk.chunkData); this.verifyHeader(presetModulatorsChunk, "pmod"); - + const presetGeneratorsChunk = readRIFFChunk(presetChunk.chunkData); this.verifyHeader(presetGeneratorsChunk, "pgen"); - + const presetInstrumentsChunk = readRIFFChunk(presetChunk.chunkData); this.verifyHeader(presetInstrumentsChunk, "inst"); - + const presetInstrumentZonesChunk = readRIFFChunk(presetChunk.chunkData); this.verifyHeader(presetInstrumentZonesChunk, "ibag"); - + const presetInstrumentModulatorsChunk = readRIFFChunk(presetChunk.chunkData); this.verifyHeader(presetInstrumentModulatorsChunk, "imod"); - + const presetInstrumentGeneratorsChunk = readRIFFChunk(presetChunk.chunkData); this.verifyHeader(presetInstrumentGeneratorsChunk, "igen"); - + const presetSamplesChunk = readRIFFChunk(presetChunk.chunkData); this.verifyHeader(presetSamplesChunk, "shdr"); - + /** * read all the samples * (the current index points to start of the smpl read) */ this.dataArray.currentIndex = this.sampleDataStartIndex; this.samples.push(...readSamples(presetSamplesChunk, sampleData, !isSF2Pack)); - + /** * read all the instrument generators * @type {Generator[]} */ let instrumentGenerators = readGenerators(presetInstrumentGeneratorsChunk); - + /** * read all the instrument modulators * @type {Modulator[]} */ let instrumentModulators = readModulators(presetInstrumentModulatorsChunk); - + /** * read all the instrument zones * @type {InstrumentZone[]} */ - let instrumentZones = readInstrumentZones(presetInstrumentZonesChunk, + let instrumentZones = readInstrumentZones( + presetInstrumentZonesChunk, instrumentGenerators, instrumentModulators, - this.samples); - + this.samples + ); + this.instruments = readInstruments(presetInstrumentsChunk, instrumentZones); - + /** * read all the preset generators * @type {Generator[]} */ let presetGenerators = readGenerators(presetGeneratorsChunk); - + /** * Read all the preset modulatorrs * @type {Modulator[]} */ let presetModulators = readModulators(presetModulatorsChunk); - + let presetZones = readPresetZones(presetZonesChunk, presetGenerators, presetModulators, this.instruments); - + this.presets.push(...readPresets(presetHeadersChunk, presetZones, this.defaultModulators)); this.presets.sort((a, b) => (a.program - b.program) + (a.bank - b.bank)); // preload the first preset - SpessaSynthInfo(`%cParsing finished! %c"${this.soundFontInfo["INAM"]}"%c has %c${this.presets.length} %cpresets, + SpessaSynthInfo( + `%cParsing finished! %c"${this.soundFontInfo["INAM"]}"%c has %c${this.presets.length} %cpresets, %c${this.instruments.length}%c instruments and %c${this.samples.length}%c samples.`, consoleColors.info, consoleColors.recognized, @@ -244,35 +260,36 @@ export class SoundFont2 extends BasicSoundFont consoleColors.recognized, consoleColors.info, consoleColors.recognized, - consoleColors.info); + consoleColors.info + ); SpessaSynthGroupEnd(); - - if(isSF2Pack) + + if (isSF2Pack) { delete this.dataArray; } } - + /** * @param chunk {RiffChunk} * @param expected {string} */ verifyHeader(chunk, expected) { - if(chunk.header.toLowerCase() !== expected.toLowerCase()) + if (chunk.header.toLowerCase() !== expected.toLowerCase()) { SpessaSynthGroupEnd(); throw new SyntaxError(`Invalid chunk header! Expected "${expected.toLowerCase()}" got "${chunk.header.toLowerCase()}"`); } } - + /** * @param text {string} * @param expected {string} */ verifyText(text, expected) { - if(text.toLowerCase() !== expected.toLowerCase()) + if (text.toLowerCase() !== expected.toLowerCase()) { SpessaSynthGroupEnd(); throw new SyntaxError(`Invalid soundFont! Expected "${expected.toLowerCase()}" got "${text.toLowerCase()}"`); diff --git a/src/spessasynth_lib/soundfont/read_sf2/zones.js b/src/spessasynth_lib/soundfont/read_sf2/zones.js index 26ca8a37..efde2d68 100644 --- a/src/spessasynth_lib/soundfont/read_sf2/zones.js +++ b/src/spessasynth_lib/soundfont/read_sf2/zones.js @@ -1,8 +1,8 @@ -import {readLittleEndian} from "../../utils/byte_functions/little_endian.js"; -import {IndexedByteArray} from "../../utils/indexed_array.js"; -import {RiffChunk} from "../basic_soundfont/riff_chunk.js"; -import {Instrument} from "./instruments.js"; -import { BasicInstrumentZone, BasicPresetZone } from '../basic_soundfont/basic_zones.js' +import { readLittleEndian } from "../../utils/byte_functions/little_endian.js"; +import { IndexedByteArray } from "../../utils/indexed_array.js"; +import { RiffChunk } from "../basic_soundfont/riff_chunk.js"; +import { Instrument } from "./instruments.js"; +import { BasicInstrumentZone, BasicPresetZone } from "../basic_soundfont/basic_zones.js"; import { Generator, generatorTypes } from "../basic_soundfont/generator.js"; import { Modulator } from "../basic_soundfont/modulator.js"; @@ -26,37 +26,37 @@ export class InstrumentZone extends BasicInstrumentZone this.generatorZoneSize = 0; this.isGlobal = true; } - + setZoneSize(modulatorZoneSize, generatorZoneSize) { this.modulatorZoneSize = modulatorZoneSize; this.generatorZoneSize = generatorZoneSize; } - + /** * grab the generators * @param generators {Generator[]} */ getGenerators(generators) { - for(let i = this.generatorZoneStartIndex; i < this.generatorZoneStartIndex + this.generatorZoneSize; i++) + for (let i = this.generatorZoneStartIndex; i < this.generatorZoneStartIndex + this.generatorZoneSize; i++) { this.generators.push(generators[i]); } } - + /** * grab the modulators * @param modulators {Modulator[]} */ getModulators(modulators) { - for(let i = this.modulatorZoneStartIndex; i < this.modulatorZoneStartIndex + this.modulatorZoneSize; i++) + for (let i = this.modulatorZoneStartIndex; i < this.modulatorZoneStartIndex + this.modulatorZoneSize; i++) { this.modulators.push(modulators[i]); } } - + /** * Loads the zone's sample * @param samples {BasicSample[]} @@ -71,27 +71,27 @@ export class InstrumentZone extends BasicInstrumentZone this.sample.useCount++; } } - + /** * Reads the keyRange of the zone */ getKeyRange() { let range = this.generators.find(g => g.generatorType === generatorTypes.keyRange); - if(range) + if (range) { this.keyRange.min = range.generatorValue & 0x7F; this.keyRange.max = (range.generatorValue >> 8) & 0x7F; } } - + /** * reads the velolicty range of the zone */ getVelRange() { let range = this.generators.find(g => g.generatorType === generatorTypes.velRange); - if(range) + if (range) { this.velRange.min = range.generatorValue & 0x7F; this.velRange.max = (range.generatorValue >> 8) & 0x7F; @@ -113,10 +113,10 @@ export function readInstrumentZones(zonesChunk, instrumentGenerators, instrument * @type {InstrumentZone[]} */ let zones = []; - while(zonesChunk.chunkData.length > zonesChunk.chunkData.currentIndex) + while (zonesChunk.chunkData.length > zonesChunk.chunkData.currentIndex) { let zone = new InstrumentZone(zonesChunk.chunkData); - if(zones.length > 0) + if (zones.length > 0) { let modulatorZoneSize = zone.modulatorZoneStartIndex - zones[zones.length - 1].modulatorZoneStartIndex; let generatorZoneSize = zone.generatorZoneStartIndex - zones[zones.length - 1].generatorZoneStartIndex; @@ -129,7 +129,7 @@ export function readInstrumentZones(zonesChunk, instrumentGenerators, instrument } zones.push(zone); } - if(zones.length > 1) + if (zones.length > 1) { // remove terminal zones.pop(); @@ -152,37 +152,37 @@ export class PresetZone extends BasicPresetZone this.generatorZoneSize = 0; this.isGlobal = true; } - + setZoneSize(modulatorZoneSize, generatorZoneSize) { this.modulatorZoneSize = modulatorZoneSize; this.generatorZoneSize = generatorZoneSize; } - + /** * grab the generators * @param generators {Generator[]} */ getGenerators(generators) { - for(let i = this.generatorZoneStartIndex; i < this.generatorZoneStartIndex + this.generatorZoneSize; i++) + for (let i = this.generatorZoneStartIndex; i < this.generatorZoneStartIndex + this.generatorZoneSize; i++) { this.generators.push(generators[i]); } } - + /** * grab the modulators * @param modulators {Modulator[]} */ getModulators(modulators) { - for(let i = this.modulatorZoneStartIndex; i < this.modulatorZoneStartIndex + this.modulatorZoneSize; i++) + for (let i = this.modulatorZoneStartIndex; i < this.modulatorZoneStartIndex + this.modulatorZoneSize; i++) { this.modulators.push(modulators[i]); } } - + /** * grab the instrument * @param instruments {Instrument[]} @@ -190,34 +190,34 @@ export class PresetZone extends BasicPresetZone getInstrument(instruments) { let instrumentID = this.generators.find(g => g.generatorType === generatorTypes.instrument); - if(instrumentID) + if (instrumentID) { this.instrument = instruments[instrumentID.generatorValue]; this.instrument.addUseCount(); this.isGlobal = false; } } - + /** * Reads the keyRange of the zone */ getKeyRange() { let range = this.generators.find(g => g.generatorType === generatorTypes.keyRange); - if(range) + if (range) { this.keyRange.min = range.generatorValue & 0x7F; this.keyRange.max = (range.generatorValue >> 8) & 0x7F; } } - + /** * reads the velolicty range of the zone */ getVelRange() { let range = this.generators.find(g => g.generatorType === generatorTypes.velRange); - if(range) + if (range) { this.velRange.min = range.generatorValue & 0x7F; this.velRange.max = (range.generatorValue >> 8) & 0x7F; @@ -239,10 +239,10 @@ export function readPresetZones(zonesChunk, presetGenerators, presetModulators, * @type {PresetZone[]} */ let zones = []; - while(zonesChunk.chunkData.length > zonesChunk.chunkData.currentIndex) + while (zonesChunk.chunkData.length > zonesChunk.chunkData.currentIndex) { let zone = new PresetZone(zonesChunk.chunkData); - if(zones.length > 0) + if (zones.length > 0) { let modulatorZoneSize = zone.modulatorZoneStartIndex - zones[zones.length - 1].modulatorZoneStartIndex; let generatorZoneSize = zone.generatorZoneStartIndex - zones[zones.length - 1].generatorZoneStartIndex; @@ -255,7 +255,7 @@ export function readPresetZones(zonesChunk, presetGenerators, presetModulators, } zones.push(zone); } - if(zones.length > 1) + if (zones.length > 1) { // remove terminal zones.pop(); diff --git a/src/spessasynth_lib/synthetizer/README.md b/src/spessasynth_lib/synthetizer/README.md index 8c9e0206..b61fa013 100644 --- a/src/spessasynth_lib/synthetizer/README.md +++ b/src/spessasynth_lib/synthetizer/README.md @@ -1,6 +1,8 @@ ## This is the main synthesizer folder. -The code here is responsible for making the actual sound. + +The code here is responsible for making the actual sound. This is the heart of the SpessaSynth library. + - `worklet_system` - the current synthesis system with AudioWorklets. `worklet_processor.min.js` - the minified worklet processor code to import. \ No newline at end of file diff --git a/src/spessasynth_lib/synthetizer/audio_effects/effects_config.js b/src/spessasynth_lib/synthetizer/audio_effects/effects_config.js index 9b598dca..944df839 100644 --- a/src/spessasynth_lib/synthetizer/audio_effects/effects_config.js +++ b/src/spessasynth_lib/synthetizer/audio_effects/effects_config.js @@ -1,4 +1,4 @@ -import { DEFAULT_CHORUS_CONFIG } from './fancy_chorus.js' +import { DEFAULT_CHORUS_CONFIG } from "./fancy_chorus.js"; /** * @typedef {Object} EffectsConfig @@ -15,7 +15,7 @@ import { DEFAULT_CHORUS_CONFIG } from './fancy_chorus.js' export const DEFAULT_EFFECTS_CONFIG = { chorusEnabled: true, chorusConfig: DEFAULT_CHORUS_CONFIG, - + reverbEnabled: true, reverbImpulseResponse: undefined // will load the integrated one -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/spessasynth_lib/synthetizer/audio_effects/fancy_chorus.js b/src/spessasynth_lib/synthetizer/audio_effects/fancy_chorus.js index 32f42aa8..7bce25e0 100644 --- a/src/spessasynth_lib/synthetizer/audio_effects/fancy_chorus.js +++ b/src/spessasynth_lib/synthetizer/audio_effects/fancy_chorus.js @@ -48,17 +48,18 @@ export class FancyChorus * @param output {AudioNode} * @param config {ChorusConfig} */ - constructor(output, config = DEFAULT_CHORUS_CONFIG) { + constructor(output, config = DEFAULT_CHORUS_CONFIG) + { const context = output.context; - + this.input = new ChannelSplitterNode(context, { numberOfOutputs: 2 }); - + const merger = new ChannelMergerNode(context, { numberOfInputs: 2 }); - + /** * @type {ChorusNode[]} */ @@ -69,18 +70,37 @@ export class FancyChorus const chorusNodesRight = []; let freq = config.oscillatorFrequency; let delay = config.defaultDelay; - for (let i = 0; i < config.nodesAmount; i++) { + for (let i = 0; i < config.nodesAmount; i++) + { // left node - this.createChorusNode(freq, delay - config.stereoDifference, chorusNodesLeft, 0, merger, 0, context, config); + this.createChorusNode( + freq, + delay - config.stereoDifference, + chorusNodesLeft, + 0, + merger, + 0, + context, + config + ); // right node - this.createChorusNode(freq, delay + config.stereoDifference, chorusNodesRight, 1, merger, 1, context, config); + this.createChorusNode( + freq, + delay + config.stereoDifference, + chorusNodesRight, + 1, + merger, + 1, + context, + config + ); freq += config.oscillatorFrequencyVariation; delay += config.delayVariation; } - + merger.connect(output); } - + /** * @param freq {number} * @param delay {number} @@ -94,7 +114,7 @@ export class FancyChorus createChorusNode(freq, delay, list, input, output, outputNum, context, config) { const oscillator = new OscillatorNode(context, { - type: 'sine', + type: "sine", frequency: freq }); const gainNode = new GainNode(context, { @@ -103,14 +123,14 @@ export class FancyChorus const delayNode = new DelayNode(context, { delayTime: delay }); - + oscillator.connect(gainNode); gainNode.connect(delayNode.delayTime); oscillator.start(context.currentTime + delay); - + this.input.connect(delayNode, input); delayNode.connect(output, 0, outputNum); - + list.push({ oscillator: oscillator, oscillatorGain: gainNode, diff --git a/src/spessasynth_lib/synthetizer/audio_effects/reverb.js b/src/spessasynth_lib/synthetizer/audio_effects/reverb.js index 000baae1..60693690 100644 --- a/src/spessasynth_lib/synthetizer/audio_effects/reverb.js +++ b/src/spessasynth_lib/synthetizer/audio_effects/reverb.js @@ -7,7 +7,7 @@ export function getReverbProcessor(context, reverbBuffer = undefined) { const convolver = new ConvolverNode(context); - if(reverbBuffer) + if (reverbBuffer) { convolver.buffer = reverbBuffer; } @@ -15,7 +15,8 @@ export function getReverbProcessor(context, reverbBuffer = undefined) { // resolve relative url const impulseURL = new URL("impulse_response_2.flac", import.meta.url); - fetch(impulseURL).then(async response => { + fetch(impulseURL).then(async response => + { const data = await response.arrayBuffer(); convolver.buffer = await context.decodeAudioData(data); }); diff --git a/src/spessasynth_lib/synthetizer/synth_event_handler.js b/src/spessasynth_lib/synthetizer/synth_event_handler.js index 467c2bfe..bf21ffd0 100644 --- a/src/spessasynth_lib/synthetizer/synth_event_handler.js +++ b/src/spessasynth_lib/synthetizer/synth_event_handler.js @@ -97,7 +97,8 @@ export class EventHandler /** * A new synthesizer event handler */ - constructor() { + constructor() + { /** * The main list of events * @type {Object>} @@ -119,7 +120,7 @@ export class EventHandler "soundfonterror": {} }; } - + /** * Adds a new event listener * @param name {EventTypes} @@ -130,7 +131,7 @@ export class EventHandler { this.events[name][id] = callback; } - + /** * Removes an event listener * @param name {EventTypes} @@ -140,7 +141,7 @@ export class EventHandler { delete this.events[name][id]; } - + /** * Calls the given event * @param name {EventTypes} @@ -148,7 +149,7 @@ export class EventHandler */ callEvent(name, eventData) { - if(this.events[name]) + if (this.events[name]) { Object.values(this.events[name]).forEach(ev => ev(eventData)); } diff --git a/src/spessasynth_lib/synthetizer/synth_soundfont_manager.js b/src/spessasynth_lib/synthetizer/synth_soundfont_manager.js index c9f8a742..86ffb50e 100644 --- a/src/spessasynth_lib/synthetizer/synth_soundfont_manager.js +++ b/src/spessasynth_lib/synthetizer/synth_soundfont_manager.js @@ -1,8 +1,8 @@ -import { workletMessageType } from './worklet_system/message_protocol/worklet_message.js' +import { workletMessageType } from "./worklet_system/message_protocol/worklet_message.js"; import { WorkletSoundfontManagerMessageType -} from './worklet_system/worklet_methods/worklet_soundfont_manager/sfman_message.js' -import { SpessaSynthWarn } from '../utils/loggin.js' +} from "./worklet_system/worklet_methods/worklet_soundfont_manager/sfman_message.js"; +import { SpessaSynthWarn } from "../utils/loggin.js"; export class SoundfontManager { @@ -23,7 +23,7 @@ export class SoundfontManager id: "main", bankOffset: 0 }]; - + /** * @type {MessagePort} * @private @@ -31,7 +31,7 @@ export class SoundfontManager this._port = synth.worklet.port; this.synth = synth; } - + /** * @private * @param type {WorkletSoundfontManagerMessageType} @@ -47,7 +47,7 @@ export class SoundfontManager ] }); } - + /** * Adds a new soundfont buffer with a given ID * @param soundfontBuffer {ArrayBuffer} - the soundfont's buffer @@ -56,7 +56,7 @@ export class SoundfontManager */ async addNewSoundFont(soundfontBuffer, id, bankOffset = 0) { - if(this.soundfontList.find(s => s.id === id) !== undefined) + if (this.soundfontList.find(s => s.id === id) !== undefined) { throw new Error("Cannot overwrite the existing soundfont. Use soundfontManager.delete(id) instead."); } @@ -67,26 +67,26 @@ export class SoundfontManager bankOffset: bankOffset }); } - + /** * Deletes a soundfont with the given ID * @param id {string} - the soundfont to delete */ deleteSoundFont(id) { - if(this.soundfontList.length === 0) + if (this.soundfontList.length === 0) { SpessaSynthWarn("1 soundfont left. Aborting!"); return; } - if(this.soundfontList.findIndex(s => s.id === id) === -1) + if (this.soundfontList.findIndex(s => s.id === id) === -1) { SpessaSynthWarn(`No soundfont with id of "${id}" found. Aborting!`); return; } this._sendToWorklet(WorkletSoundfontManagerMessageType.deleteSoundFont, id); } - + /** * Rearranges the soundfonts in a given order * @param newList {string[]} the order of soundfonts, a list of identifiers, first overwrites second @@ -98,7 +98,7 @@ export class SoundfontManager newList.indexOf(a.id) - newList.indexOf(b.id) ); } - + /** * DELETES ALL SOUNDFONTS!! and creates a new one with id "main" * @param newBuffer {ArrayBuffer} diff --git a/src/spessasynth_lib/synthetizer/synthetizer.js b/src/spessasynth_lib/synthetizer/synthetizer.js index d4f094b5..6e9115e6 100644 --- a/src/spessasynth_lib/synthetizer/synthetizer.js +++ b/src/spessasynth_lib/synthetizer/synthetizer.js @@ -1,18 +1,19 @@ -import { IndexedByteArray } from '../utils/indexed_array.js' -import { consoleColors } from '../utils/other.js' -import { getEvent, messageTypes, midiControllers } from '../midi_parser/midi_message.js' -import { EventHandler } from './synth_event_handler.js' -import { FancyChorus } from './audio_effects/fancy_chorus.js' -import { getReverbProcessor } from './audio_effects/reverb.js' +import { IndexedByteArray } from "../utils/indexed_array.js"; +import { consoleColors } from "../utils/other.js"; +import { getEvent, messageTypes, midiControllers } from "../midi_parser/midi_message.js"; +import { EventHandler } from "./synth_event_handler.js"; +import { FancyChorus } from "./audio_effects/fancy_chorus.js"; +import { getReverbProcessor } from "./audio_effects/reverb.js"; import { - ALL_CHANNELS_OR_DIFFERENT_ACTION, masterParameterType, + ALL_CHANNELS_OR_DIFFERENT_ACTION, + masterParameterType, returnMessageType, - workletMessageType, -} from './worklet_system/message_protocol/worklet_message.js' -import { SpessaSynthInfo, SpessaSynthWarn } from '../utils/loggin.js' -import { DEFAULT_EFFECTS_CONFIG } from './audio_effects/effects_config.js' -import { SoundfontManager } from './synth_soundfont_manager.js' -import { channelConfiguration } from './worklet_system/worklet_utilities/worklet_processor_channel.js' + workletMessageType +} from "./worklet_system/message_protocol/worklet_message.js"; +import { SpessaSynthInfo, SpessaSynthWarn } from "../utils/loggin.js"; +import { DEFAULT_EFFECTS_CONFIG } from "./audio_effects/effects_config.js"; +import { SoundfontManager } from "./synth_soundfont_manager.js"; +import { channelConfiguration } from "./worklet_system/worklet_utilities/worklet_processor_channel.js"; /** @@ -44,7 +45,8 @@ export const DEFAULT_SYNTH_MODE = "gs"; * @param startRenderingData {StartRenderingDataConfig} if set, starts playing this immediately and restores the values * @param effectsConfig {EffectsConfig} optional configuration for the audio effects. */ -export class Synthetizer { +export class Synthetizer +{ /** * Creates a new instance of the SpessaSynth synthesizer * @param targetNode {AudioNode} @@ -53,23 +55,24 @@ export class Synthetizer { * @param startRenderingData {StartRenderingDataConfig} if set, starts playing this immediately and restores the values * @param effectsConfig {EffectsConfig} optional configuration for the audio effects. */ - constructor(targetNode, - soundFontBuffer, - enableEventSystem = true, - startRenderingData = undefined, - effectsConfig = DEFAULT_EFFECTS_CONFIG) { + constructor(targetNode, + soundFontBuffer, + enableEventSystem = true, + startRenderingData = undefined, + effectsConfig = DEFAULT_EFFECTS_CONFIG) + { SpessaSynthInfo("%cInitializing SpessaSynth synthesizer...", consoleColors.info); this.context = targetNode.context; const oneOutputMode = startRenderingData?.oneOutput === true; - + /** * Allows to set up custom event listeners for the synthesizer * @type {EventHandler} */ this.eventHandler = new EventHandler(); - + this._voiceCap = VOICE_CAP; - + /** * the new channels will have their audio sent to the moduled output by this constant. * what does that mean? e.g. if outputsAmount is 16, then channel's 16 audio will be sent to channel 0 @@ -77,25 +80,25 @@ export class Synthetizer { * @private */ this._outputsAmount = MIDI_CHANNEL_COUNT; - + /** * the amount of midi channels * @type {number} */ this.channelsAmount = this._outputsAmount; - + /** * @type {function} */ this.resolveWhenReady = undefined; - + /** * Indicates if the synth is fully ready * @type {Promise} */ this.isReady = new Promise(resolve => this.resolveWhenReady = resolve); - - + + /** * individual channel voices amount * @type {ChannelProperty[]} @@ -107,24 +110,25 @@ export class Synthetizer { } this.channelProperties[DEFAULT_PERCUSSION].isDrum = true; this._voicesAmount = 0; - + /** * For Black MIDI's - forces release time to 50ms * @type {boolean} */ this._highPerformanceMode = false; - + // create a worklet processor let processorChannelCount = Array(this._outputsAmount + 2).fill(2); - let processorOutputsCount = this._outputsAmount + 2; - if(oneOutputMode) + let processorOutputsCount = this._outputsAmount + 2; + if (oneOutputMode) { processorOutputsCount = 1; processorChannelCount = [32]; } - + // first two outputs: reverb, chorsu, the others are the channel outputs - try { + try + { this.worklet = new AudioWorkletNode(this.context, WORKLET_PROCESSOR_NAME, { outputChannelCount: processorChannelCount, numberOfOutputs: processorOutputsCount, @@ -135,12 +139,11 @@ export class Synthetizer { startRenderingData: startRenderingData } }); - } - catch (e) + } catch (e) { throw new Error("Could not create the audioWorklet. Did you forget to addModule()?"); } - + /** * @typedef {Object} PresetListElement * @property {string} presetName @@ -149,43 +152,43 @@ export class Synthetizer { * * used in "presetlistchange" event */ - + // worklet sends us some data back this.worklet.port.onmessage = e => this.handleMessage(e.data); - + /** * The synth's soundfont manager * @type {SoundfontManager} */ this.soundfontManager = new SoundfontManager(this); - + /** * @type {function(SynthesizerSnapshot)} * @private */ this._snapshotCallback = undefined; - + /** * for the worklet sequencer's messages * @type {function(WorkletSequencerReturnMessageType, any)} */ this.sequencerCallbackFunction = undefined; - + // add reverb - if(effectsConfig.reverbEnabled && !oneOutputMode) + if (effectsConfig.reverbEnabled && !oneOutputMode) { this.reverbProcessor = getReverbProcessor(this.context, effectsConfig.reverbImpulseResponse); this.reverbProcessor.connect(targetNode); this.worklet.connect(this.reverbProcessor, 0); } - - if(effectsConfig.chorusEnabled && !oneOutputMode) + + if (effectsConfig.chorusEnabled && !oneOutputMode) { this.chorusProcessor = new FancyChorus(targetNode, effectsConfig.chorusConfig); this.worklet.connect(this.chorusProcessor.input, 1); } - - if(oneOutputMode) + + if (oneOutputMode) { // one output mode: one output (duh) this.worklet.connect(targetNode, 0); @@ -198,13 +201,14 @@ export class Synthetizer { this.worklet.connect(targetNode, i); } } - + // attach newchannel to keep track of channels count - this.eventHandler.addEvent("newchannel", "synth-new-channel", () => { + this.eventHandler.addEvent("newchannel", "synth-new-channel", () => + { this.channelsAmount++; }); } - + /** * The maximum amount of voices allowed at once * @returns {number} @@ -213,7 +217,7 @@ export class Synthetizer { { return this._voiceCap; } - + /** * The maximum amount of voices allowed at once * @param value {number} @@ -223,7 +227,12 @@ export class Synthetizer { this._setMasterParam(masterParameterType.voicesCap, value); this._voiceCap = value; } - + + get highPerformanceMode() + { + return this._highPerformanceMode; + } + /** * For Black MIDI's - forces release time to 50ms * @param {boolean} value @@ -231,14 +240,25 @@ export class Synthetizer { set highPerformanceMode(value) { this._highPerformanceMode = value; - + } - - get highPerformanceMode() + + /** + * @returns {number} the audioContext's current time + */ + get currentTime() { - return this._highPerformanceMode; + return this.context.currentTime; } - + + /** + * @returns {number} the current amount of voices playing + */ + get voicesAmount() + { + return this._voicesAmount; + } + /** * Sets the SpessaSynth's log level * @param enableInfo {boolean} - enable info (verbose) @@ -254,7 +274,7 @@ export class Synthetizer { messageData: [enableInfo, enableWarning, enableGroup, enableTable] }); } - + /** * @param type {masterParameterType} * @param data {any} @@ -266,9 +286,9 @@ export class Synthetizer { channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION, messageType: workletMessageType.setMasterParameter, messageData: [type, data] - }) + }); } - + /** * Sets the interpolation type for the synthesizer: * 0 - linear @@ -279,7 +299,7 @@ export class Synthetizer { { this._setMasterParam(masterParameterType.interpolationType, type); } - + /** * Handles the messages received from the worklet * @param message {WorkletReturnMessage} @@ -295,47 +315,49 @@ export class Synthetizer { * @type {ChannelProperty[]} */ this.channelProperties = messageData; - + this._voicesAmount = this.channelProperties.reduce((sum, voices) => sum + voices.voicesAmount, 0); break; - + case returnMessageType.eventCall: this.eventHandler.callEvent(messageData.eventName, messageData.eventData); break; - + case returnMessageType.sequencerSpecific: - if(this.sequencerCallbackFunction) + if (this.sequencerCallbackFunction) { this.sequencerCallbackFunction(messageData.messageType, messageData.messageData); } break; - + case returnMessageType.synthesizerSnapshot: - if(this._snapshotCallback) + if (this._snapshotCallback) { this._snapshotCallback(messageData); } break; - + case returnMessageType.ready: this.resolveWhenReady(); break; - + case returnMessageType.soundfontError: SpessaSynthWarn(new Error(messageData)); this.eventHandler.callEvent("soundfonterror", messageData); break; } } - + /** * Gets a complete snapshot of the synthesizer, including controllers * @returns {Promise} */ async getSynthesizerSnapshot() { - return new Promise(resolve => { - this._snapshotCallback = s => { + return new Promise(resolve => + { + this._snapshotCallback = s => + { this._snapshotCallback = undefined; resolve(s); }; @@ -346,7 +368,7 @@ export class Synthetizer { }); }); } - + /** * Adds a new channel to the synthesizer * @param postMessage {boolean} leave at true, set to false only at initialization @@ -360,7 +382,7 @@ export class Synthetizer { isMuted: false, isDrum: false }); - if(!postMessage) + if (!postMessage) { return; } @@ -370,7 +392,7 @@ export class Synthetizer { messageData: null }); } - + /** * @param channel {number} * @param value {{delay: number, depth: number, rate: number}} @@ -383,24 +405,25 @@ export class Synthetizer { messageData: value }); } - + /** * Connects the individual audio outputs to the given audio nodes. In the app it's used by the renderer. * @param audioNodes {AudioNode[]} */ connectIndividualOutputs(audioNodes) { - if(audioNodes.length !== this._outputsAmount) + if (audioNodes.length !== this._outputsAmount) { throw new Error(`input nodes amount differs from the system's outputs amount! Expected ${this._outputsAmount} got ${audioNodes.length}`); } - for (let outputNumber = 0; outputNumber < this._outputsAmount; outputNumber++) { + for (let outputNumber = 0; outputNumber < this._outputsAmount; outputNumber++) + { // + 2 because chorus and reverb come first! this.worklet.connect(audioNodes[outputNumber], outputNumber + 2); } } - + /* * Disables the GS NRPN parameters like vibrato or drum key tuning */ @@ -408,9 +431,9 @@ export class Synthetizer { { // rate -1 disables, see worklet_message.js line 9 // channel -1 is all - this.setVibrato(ALL_CHANNELS_OR_DIFFERENT_ACTION, {depth: 0, rate: -1, delay: 0}); + this.setVibrato(ALL_CHANNELS_OR_DIFFERENT_ACTION, { depth: 0, rate: -1, delay: 0 }); } - + /** * A message for debugging */ @@ -423,7 +446,7 @@ export class Synthetizer { messageData: undefined }); } - + /** * Starts playing a note * @param channel {number} usually 0-15: the channel to play the note @@ -431,22 +454,24 @@ export class Synthetizer { * @param velocity {number} 0-127 the velocity of the note (generally controls loudness) * @param enableDebugging {boolean} set to true to log technical details to console */ - noteOn(channel, midiNote, velocity, enableDebugging = false) { + noteOn(channel, midiNote, velocity, enableDebugging = false) + { this.post({ channelNumber: channel, messageType: workletMessageType.noteOn, messageData: [midiNote, velocity, enableDebugging] }); } - + /** * Stops playing a note * @param channel {number} usually 0-15: the channel of the note * @param midiNote {number} 0-127 the key number of the note * @param force {boolean} instantly kills the note if true */ - noteOff(channel, midiNote, force = false) { - if(force) + noteOff(channel, midiNote, force = false) + { + if (force) { this.post({ channelNumber: channel, @@ -454,7 +479,8 @@ export class Synthetizer { messageData: midiNote }); } - else { + else + { this.post({ channelNumber: channel, messageType: workletMessageType.noteOff, @@ -462,20 +488,21 @@ export class Synthetizer { }); } } - + /** * Stops all notes * @param force {boolean} if we should instantly kill the note, defaults to false */ - stopAll(force=false) { + stopAll(force = false) + { this.post({ channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION, messageType: workletMessageType.stopAll, messageData: force ? 1 : 0 }); - + } - + /** * Changes the given controller * @param channel {number} usually 0-15: the channel to change the controller @@ -483,9 +510,12 @@ export class Synthetizer { * @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, force=false) + controllerChange(channel, controllerNumber, controllerValue, force = false) { - if(controllerNumber > 127 || controllerNumber < 0) throw new Error(`Invalid controller number: ${controllerNumber}`); + if (controllerNumber > 127 || controllerNumber < 0) + { + throw new Error(`Invalid controller number: ${controllerNumber}`); + } controllerValue = Math.floor(controllerValue); controllerNumber = Math.floor(controllerNumber); this.post({ @@ -494,7 +524,7 @@ export class Synthetizer { messageData: [controllerNumber, controllerValue, force] }); } - + /** * Resets all controllers (for every channel) */ @@ -504,9 +534,9 @@ export class Synthetizer { channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION, messageType: workletMessageType.ccReset, messageData: undefined - }) + }); } - + /** * Applies pressure to a given channel * @param channel {number} usually 0-15: the channel to change the controller @@ -520,7 +550,7 @@ export class Synthetizer { messageData: pressure }); } - + /** * Applies pressure to a given note * @param channel {number} usually 0-15: the channel to change the controller @@ -535,7 +565,7 @@ export class Synthetizer { messageData: [midiNote, pressure] }); } - + /** * @param data {WorkletMessage} */ @@ -543,7 +573,7 @@ export class Synthetizer { { this.worklet.port.postMessage(data); } - + /** * Sets the pitch of the given channel * @param channel {number} usually 0-15: the channel to change pitch @@ -555,10 +585,10 @@ export class Synthetizer { this.post({ channelNumber: channel, messageType: workletMessageType.pitchWheel, - messageData: [MSB, LSB], + messageData: [MSB, LSB] }); } - + /** * Transposes the synthetizer's pitch by given semitones amount (percussion channels do not get affected) * @param semitones {number} the semitones to transpose by. Can be a floating point number for more precision @@ -567,14 +597,14 @@ export class Synthetizer { { this.transposeChannel(ALL_CHANNELS_OR_DIFFERENT_ACTION, semitones, false); } - + /** * Transposes the channel by given amount of semitones * @param channel {number} the channel number * @param semitones {number} the transposition of the channel, can be a float * @param force {boolean} defaults to false, if true transposes the channel even if it's a drum channel */ - transposeChannel(channel, semitones, force=false) + transposeChannel(channel, semitones, force = false) { this.post({ channelNumber: channel, @@ -582,7 +612,7 @@ export class Synthetizer { messageData: [semitones, force] }); } - + /** * Sets the main volume * @param volume {number} 0-1 the volume @@ -591,7 +621,7 @@ export class Synthetizer { { this._setMasterParam(masterParameterType.mainVolume, volume); } - + /** * Sets the master stereo panning * @param pan {number} -1 to 1, the pan (-1 is left, 0 is midde, 1 is right) @@ -600,7 +630,7 @@ export class Synthetizer { { this._setMasterParam(masterParameterType.masterPan, pan); } - + /** * Sets the channel's pitch bend range, in semitones * @param channel {number} usually 0-15: the channel to change @@ -611,28 +641,28 @@ export class Synthetizer { // set range this.controllerChange(channel, midiControllers.RPNMsb, 0); this.controllerChange(channel, midiControllers.dataEntryMsb, pitchBendRangeSemitones); - + // reset rpn this.controllerChange(channel, midiControllers.RPNMsb, 127); this.controllerChange(channel, midiControllers.RPNLsb, 127); this.controllerChange(channel, midiControllers.dataEntryMsb, 0); } - + /** * Changes the patch for a given channel * @param channel {number} usually 0-15: the channel to change * @param programNumber {number} 0-127 the MIDI patch number * @param userChange {boolean} indicates if the program change has been called by user. defaults to false */ - programChange(channel, programNumber, userChange=false) + programChange(channel, programNumber, userChange = false) { this.post({ channelNumber: channel, messageType: workletMessageType.programChange, messageData: [programNumber, userChange] - }) + }); } - + /** * Overrides velocity on a given channel * @param channel {number} usually 0-15: the channel to change @@ -645,9 +675,9 @@ export class Synthetizer { channelNumber: channel, messageType: workletMessageType.ccChange, messageData: [channelConfiguration.velocityOverride, velocity, true] - }) + }); } - + /** * Causes the given midi channel to ignore controller messages for the given controller number * @param channel {number} usually 0-15: the channel to lock @@ -662,7 +692,7 @@ export class Synthetizer { messageData: [controllerNumber, isLocked] }); } - + /** * Mutes or unmutes the given channel * @param channel {number} usually 0-15: the channel to lock @@ -676,7 +706,7 @@ export class Synthetizer { messageData: isMuted }); } - + /** * Reloads the sounfont. * THIS IS DEPRECATED! @@ -687,10 +717,10 @@ export class Synthetizer { */ async reloadSoundFont(soundFontBuffer) { - SpessaSynthWarn("reloadSoundFont is deprecated. Please use the soundfontManager property instead.") + SpessaSynthWarn("reloadSoundFont is deprecated. Please use the soundfontManager property instead."); await this.soundfontManager.reloadManager(soundFontBuffer); } - + /** * Sends a MIDI Sysex message to the synthesizer * @param messageData {IndexedByteArray} the message's data (excluding the F0 byte, but including the F7 at the end) @@ -703,7 +733,7 @@ export class Synthetizer { messageData: Array.from(messageData) }); } - + /** * Toggles drums on a given channel * @param channel {number} @@ -717,7 +747,7 @@ export class Synthetizer { messageData: isDrum }); } - + /** * sends a raw MIDI message to the synthesizer * @param message {ArrayLike} the midi message, each number is a byte @@ -726,14 +756,15 @@ export class Synthetizer { { // discard as soon as possible if high perf const statusByteData = getEvent(message[0]); - - + + // process the event switch (statusByteData.status) { case messageTypes.noteOn: const velocity = message[2]; - if(velocity > 0) { + if (velocity > 0) + { this.noteOn(statusByteData.channel, message[1], velocity); } else @@ -741,64 +772,49 @@ export class Synthetizer { this.noteOff(statusByteData.channel, message[1]); } break; - + case messageTypes.noteOff: this.noteOff(statusByteData.channel, message[1]); break; - + case messageTypes.pitchBend: this.pitchWheel(statusByteData.channel, message[2], message[1]); break; - + case messageTypes.controllerChange: this.controllerChange(statusByteData.channel, message[1], message[2]); break; - + case messageTypes.programChange: this.programChange(statusByteData.channel, message[1]); break; - + case messageTypes.polyPressure: this.polyPressure(statusByteData.channel, message[0], message[1]); break; - + case messageTypes.channelPressure: this.channelPressure(statusByteData.channel, message[1]); break; - + case messageTypes.systemExclusive: this.systemExclusive(new IndexedByteArray(message.slice(1))); break; - + case messageTypes.reset: this.stopAll(true); this.resetControllers(); break; - + default: break; } } - - /** - * @returns {number} the audioContext's current time - */ - get currentTime() - { - return this.context.currentTime; - } - - /** - * @returns {number} the current amount of voices playing - */ - get voicesAmount() - { - return this._voicesAmount; - } - + reverbateEverythingBecauseWhyNot() { - for (let i = 0; i < this.channelsAmount; i++) { + for (let i = 0; i < this.channelsAmount; i++) + { this.controllerChange(i, midiControllers.effects1Depth, 127); } return "That's the spirit!"; diff --git a/src/spessasynth_lib/synthetizer/worklet_system/README.md b/src/spessasynth_lib/synthetizer/worklet_system/README.md index 88136320..22f1df3f 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/README.md +++ b/src/spessasynth_lib/synthetizer/worklet_system/README.md @@ -1,6 +1,10 @@ ## This the worklet system synthesis folder. + The code here is responsible for a single midi channel, synthesizing the sound to it. + - `worklet_methods` contains the methods for the `main_processor.js` -- `worklet_utilities` contains the various digital signal processing functions such as the wavetable oscillator, low pass filter, etc. +- `worklet_utilities` contains the various digital signal processing functions such as the wavetable oscillator, low + pass filter, etc. -`minify_processor.js` uses esbuild to minify the processor code. Importing this instead of `worklet_processor.js` is recommended. \ No newline at end of file +`minify_processor.js` uses esbuild to minify the processor code. Importing this instead of `worklet_processor.js` is +recommended. \ No newline at end of file diff --git a/src/spessasynth_lib/synthetizer/worklet_system/main_processor.js b/src/spessasynth_lib/synthetizer/worklet_system/main_processor.js index 48a261ac..86cfd4a7 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/main_processor.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/main_processor.js @@ -1,47 +1,55 @@ // noinspection JSUnresolvedReference -import { DEFAULT_PERCUSSION, DEFAULT_SYNTH_MODE, VOICE_CAP } from '../synthetizer.js' -import { WorkletSequencer } from '../../sequencer/worklet_sequencer/worklet_sequencer.js' -import { SpessaSynthInfo } from '../../utils/loggin.js' -import { consoleColors } from '../../utils/other.js' -import { PAN_SMOOTHING_FACTOR, releaseVoice, renderVoice, voiceKilling } from './worklet_methods/voice_control.js' -import { ALL_CHANNELS_OR_DIFFERENT_ACTION, returnMessageType } from './message_protocol/worklet_message.js' -import { stbvorbis } from '../../externals/stbvorbis_sync/stbvorbis_sync.min.js' -import { VOLUME_ENVELOPE_SMOOTHING_FACTOR } from './worklet_utilities/volume_envelope.js' -import { handleMessage } from './message_protocol/handle_message.js' -import { callEvent, sendChannelProperties } from './message_protocol/message_sending.js' -import { systemExclusive } from './worklet_methods/system_exclusive.js' -import { noteOn } from './worklet_methods/note_on.js' -import { killNote, noteOff, stopAll, stopAllChannels } from './worklet_methods/note_off.js' +import { DEFAULT_PERCUSSION, DEFAULT_SYNTH_MODE, VOICE_CAP } from "../synthetizer.js"; +import { WorkletSequencer } from "../../sequencer/worklet_sequencer/worklet_sequencer.js"; +import { SpessaSynthInfo } from "../../utils/loggin.js"; +import { consoleColors } from "../../utils/other.js"; +import { PAN_SMOOTHING_FACTOR, releaseVoice, renderVoice, voiceKilling } from "./worklet_methods/voice_control.js"; +import { ALL_CHANNELS_OR_DIFFERENT_ACTION, returnMessageType } from "./message_protocol/worklet_message.js"; +import { stbvorbis } from "../../externals/stbvorbis_sync/stbvorbis_sync.min.js"; +import { VOLUME_ENVELOPE_SMOOTHING_FACTOR } from "./worklet_utilities/volume_envelope.js"; +import { handleMessage } from "./message_protocol/handle_message.js"; +import { callEvent, sendChannelProperties } from "./message_protocol/message_sending.js"; +import { systemExclusive } from "./worklet_methods/system_exclusive.js"; +import { noteOn } from "./worklet_methods/note_on.js"; +import { killNote, noteOff, stopAll, stopAllChannels } from "./worklet_methods/note_off.js"; import { - channelPressure, pitchWheel, - polyPressure, setChannelTuning, setChannelTuningSemitones, setMasterTuning, setModulationDepth, setOctaveTuning, + channelPressure, + pitchWheel, + polyPressure, + setChannelTuning, + setChannelTuningSemitones, + setMasterTuning, + setModulationDepth, + setOctaveTuning, transposeAllChannels, - transposeChannel, -} from './worklet_methods/tuning_control.js' + transposeChannel +} from "./worklet_methods/tuning_control.js"; import { controllerChange, muteChannel, setMasterGain, setMasterPan, - setMIDIVolume, -} from './worklet_methods/controller_control.js' -import { disableAndLockGSNRPN, setVibrato } from './worklet_methods/vibrato_control.js' -import { dataEntryCoarse, dataEntryFine } from './worklet_methods/data_entry.js' -import { createWorkletChannel } from './worklet_utilities/worklet_processor_channel.js' -import { resetAllControllers, resetControllers, resetParameters } from './worklet_methods/reset_controllers.js' + setMIDIVolume +} from "./worklet_methods/controller_control.js"; +import { disableAndLockGSNRPN, setVibrato } from "./worklet_methods/vibrato_control.js"; +import { dataEntryCoarse, dataEntryFine } from "./worklet_methods/data_entry.js"; +import { createWorkletChannel } from "./worklet_utilities/worklet_processor_channel.js"; +import { resetAllControllers, resetControllers, resetParameters } from "./worklet_methods/reset_controllers.js"; import { clearSoundFont, getPreset, programChange, - reloadSoundFont, sendPresetList, - setDrums, setEmbeddedSoundFont, - setPreset, -} from './worklet_methods/program_control.js' -import { applySynthesizerSnapshot, sendSynthesizerSnapshot } from './worklet_methods/snapshot.js' -import { WorkletSoundfontManager } from './worklet_methods/worklet_soundfont_manager/worklet_soundfont_manager.js' -import { interpolationTypes } from './worklet_utilities/wavetable_oscillator.js' -import { getWorkletVoices } from './worklet_utilities/worklet_voice.js' + reloadSoundFont, + sendPresetList, + setDrums, + setEmbeddedSoundFont, + setPreset +} from "./worklet_methods/program_control.js"; +import { applySynthesizerSnapshot, sendSynthesizerSnapshot } from "./worklet_methods/snapshot.js"; +import { WorkletSoundfontManager } from "./worklet_methods/worklet_soundfont_manager/worklet_soundfont_manager.js"; +import { interpolationTypes } from "./worklet_utilities/wavetable_oscillator.js"; +import { getWorkletVoices } from "./worklet_utilities/worklet_voice.js"; /** @@ -73,30 +81,30 @@ class SpessaSynthProcessor extends AudioWorkletProcessor super(); this.oneOutputMode = options.processorOptions?.startRenderingData?.oneOutput === true; this._outputsAmount = this.oneOutputMode ? 1 : options.processorOptions.midiChannels; - + this.enableEventSystem = options.processorOptions.enableEventSystem; - + /** * Synth's device id: -1 means all * @type {number} */ this.deviceID = ALL_CHANNELS_OR_DIFFERENT_ACTION; - + /** * Interpolation type used * @type {interpolationTypes} */ this.interpolationType = interpolationTypes.fourthOrder; - + /** * @type {function} */ this.processTickCallback = undefined; - + this.sequencer = new WorkletSequencer(this); - + this.transposition = 0; - + /** * this.tunings[program][key] = tuning * @type {MTSProgramTuning[]} @@ -106,27 +114,27 @@ class SpessaSynthProcessor extends AudioWorkletProcessor { this.tunings.push([]); } - + /** * Bank offset for things like embedded RMIDIS. Added for every program change * @type {number} */ this.soundfontBankOffset = 0; - + /** * The volume gain, set by user * @type {number} */ this.masterGain = SYNTHESIZER_GAIN; - + this.midiVolume = 1; - + /** * Maximum number of voices allowed at once * @type {number} */ this.voiceCap = VOICE_CAP; - + /** * -1 to 1 * @type {number} @@ -137,15 +145,15 @@ class SpessaSynthProcessor extends AudioWorkletProcessor * @type {number} */ this.panLeft = 0.5 * this.currentGain; - + this.highPerformanceMode = false; - + /** * Overrides the main soundfont (embedded for example * @type {BasicSoundFont} */ this.overrideSoundfont = undefined; - + /** * the pan of the right channel * @type {number} @@ -156,9 +164,11 @@ class SpessaSynthProcessor extends AudioWorkletProcessor /** * @type {WorkletSoundfontManager} */ - this.soundfontManager = new WorkletSoundfontManager(options.processorOptions.soundfont, this.postReady.bind(this)); - } - catch (e) + this.soundfontManager = new WorkletSoundfontManager( + options.processorOptions.soundfont, + this.postReady.bind(this) + ); + } catch (e) { this.post({ messageType: returnMessageType.soundfontError, @@ -167,10 +177,10 @@ class SpessaSynthProcessor extends AudioWorkletProcessor throw e; } this.sendPresetList(); - + this.defaultPreset = this.getPreset(0, 0); this.drumPreset = this.getPreset(128, 0); - + /** * contains all the channels with their voices on the processor size * @type {WorkletProcessorChannel[]} @@ -180,14 +190,14 @@ class SpessaSynthProcessor extends AudioWorkletProcessor { this.createWorkletChannel(false); } - + this.workletProcessorChannels[DEFAULT_PERCUSSION].preset = this.drumPreset; this.workletProcessorChannels[DEFAULT_PERCUSSION].drumChannel = true; - + // these smoothing factors were tested on 44100Hz, adjust them here this.volumeEnvelopeSmoothingFactor = VOLUME_ENVELOPE_SMOOTHING_FACTOR * (sampleRate / 44100); this.panSmoothingFactor = PAN_SMOOTHING_FACTOR * (sampleRate / 44100); - + /** * Controls the system * @typedef {"gm"|"gm2"|"gs"|"xg"} SynthSystem @@ -196,31 +206,31 @@ class SpessaSynthProcessor extends AudioWorkletProcessor * @type {SynthSystem} */ this.system = DEFAULT_SYNTH_MODE; - + this.totalVoicesAmount = 0; - + /** * The snapshot that synth was restored from * @type {SynthesizerSnapshot|undefined} * @private */ this._snapshot = options.processorOptions?.startRenderingData?.snapshot; - + this.port.onmessage = e => this.handleMessage(e.data); - + // if sent, start rendering - if(options.processorOptions.startRenderingData) + if (options.processorOptions.startRenderingData) { if (this._snapshot !== undefined) { this.applySynthesizerSnapshot(this._snapshot); this.resetAllControllers(); } - - SpessaSynthInfo("%cRendering enabled! Starting render.", consoleColors.info) + + SpessaSynthInfo("%cRendering enabled! Starting render.", consoleColors.info); if (options.processorOptions.startRenderingData.parsedMIDI) { - if(options.processorOptions.startRenderingData?.loopCount !== undefined) + if (options.processorOptions.startRenderingData?.loopCount !== undefined) { this.sequencer.loopCount = options.processorOptions.startRenderingData?.loopCount; this.sequencer.loop = true; @@ -232,28 +242,37 @@ class SpessaSynthProcessor extends AudioWorkletProcessor this.sequencer.loadNewSongList([options.processorOptions.startRenderingData.parsedMIDI]); } } - - stbvorbis.isInitialized.then(() => { + + stbvorbis.isInitialized.then(() => + { this.postReady(); SpessaSynthInfo("%cSpessaSynth is ready!", consoleColors.recognized); }); } - + + /** + * @returns {number} + */ + get currentGain() + { + return this.masterGain * this.midiVolume; + } + /** * @param data {WorkletReturnMessage} */ post(data) { - if(!this.enableEventSystem) + if (!this.enableEventSystem) { return; } this.port.postMessage(data); } - + postReady() { - if(!this.enableEventSystem) + if (!this.enableEventSystem) { return; } @@ -262,15 +281,7 @@ class SpessaSynthProcessor extends AudioWorkletProcessor messageData: undefined }); } - - /** - * @returns {number} - */ - get currentGain() - { - return this.masterGain * this.midiVolume; - } - + debugMessage() { SpessaSynthInfo({ @@ -280,7 +291,7 @@ class SpessaSynthProcessor extends AudioWorkletProcessor dumpedSamples: this.workletDumpedSamplesList }); } - + // noinspection JSUnusedGlobalSymbols /** * Syntesizes the voice to buffers @@ -288,16 +299,18 @@ class SpessaSynthProcessor extends AudioWorkletProcessor * @param outputs {Float32Array[][]} the outputs to write to, only the first 2 channels are populated * @returns {boolean} true */ - process(inputs, outputs) { - if(this.processTickCallback) + process(inputs, outputs) + { + if (this.processTickCallback) { this.processTickCallback(); } - + // for every channel let totalCurrentVoices = 0; - this.workletProcessorChannels.forEach((channel, index) => { - if(channel.voices.length < 1 || channel.isMuted) + this.workletProcessorChannels.forEach((channel, index) => + { + if (channel.voices.length < 1 || channel.isMuted) { // skip the channels return; @@ -307,7 +320,7 @@ class SpessaSynthProcessor extends AudioWorkletProcessor let outputRight; let reverbChannels; let chorusChannels; - if(this.oneOutputMode) + if (this.oneOutputMode) { // first output only const output = outputs[0]; @@ -325,14 +338,15 @@ class SpessaSynthProcessor extends AudioWorkletProcessor reverbChannels = outputs[0]; chorusChannels = outputs[1]; } - + const tempV = channel.voices; - + // reset voices channel.voices = []; - + // for every voice - tempV.forEach(v => { + tempV.forEach(v => + { // render voice this.renderVoice( channel, @@ -341,18 +355,18 @@ class SpessaSynthProcessor extends AudioWorkletProcessor reverbChannels, chorusChannels ); - if(!v.finished) + if (!v.finished) { // if not finished, add it back channel.voices.push(v); } }); - + totalCurrentVoices += channel.voices.length; }); - + // if voice count changed, update voice amount - if(totalCurrentVoices !== this.totalVoicesAmount) + if (totalCurrentVoices !== this.totalVoicesAmount) { this.totalVoicesAmount = totalCurrentVoices; this.sendChannelProperties(); @@ -430,4 +444,4 @@ SpessaSynthProcessor.prototype.sendPresetList = sendPresetList; SpessaSynthProcessor.prototype.sendSynthesizerSnapshot = sendSynthesizerSnapshot; SpessaSynthProcessor.prototype.applySynthesizerSnapshot = applySynthesizerSnapshot; -export { SpessaSynthProcessor } \ No newline at end of file +export { SpessaSynthProcessor }; \ No newline at end of file diff --git a/src/spessasynth_lib/synthetizer/worklet_system/message_protocol/handle_message.js b/src/spessasynth_lib/synthetizer/worklet_system/message_protocol/handle_message.js index b99c9284..95d59d57 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/message_protocol/handle_message.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/message_protocol/handle_message.js @@ -1,5 +1,5 @@ -import { ALL_CHANNELS_OR_DIFFERENT_ACTION, masterParameterType, workletMessageType } from './worklet_message.js' -import { SpessaSynthLogging, SpessaSynthWarn } from '../../../utils/loggin.js' +import { ALL_CHANNELS_OR_DIFFERENT_ACTION, masterParameterType, workletMessageType } from "./worklet_message.js"; +import { SpessaSynthLogging, SpessaSynthWarn } from "../../../utils/loggin.js"; /** * @this {SpessaSynthProcessor} @@ -13,50 +13,51 @@ export function handleMessage(message) * @type {WorkletProcessorChannel} */ let channelObject = {}; - if(channel >= 0) + if (channel >= 0) { channelObject = this.workletProcessorChannels[channel]; } - switch (message.messageType) { + switch (message.messageType) + { case workletMessageType.noteOn: this.noteOn(channel, data[0], data[1], data[2]); break; - + case workletMessageType.noteOff: this.noteOff(channel, data); break; - + case workletMessageType.pitchWheel: this.pitchWheel(channel, data[0], data[1]); break; - + case workletMessageType.ccChange: this.controllerChange(channel, data[0], data[1], data[2]); break; - + case workletMessageType.customcCcChange: // custom controller change channelObject.customControllers[data[0]] = data[1]; break; - + case workletMessageType.killNote: this.killNote(channel, data); break; - + case workletMessageType.programChange: this.programChange(channel, data[0], data[1]); break; - + case workletMessageType.channelPressure: this.channelPressure(channel, data); break; - + case workletMessageType.polyPressure: this.polyPressure(channel, data[0], data[1]); break; - + case workletMessageType.ccReset: - if(channel === ALL_CHANNELS_OR_DIFFERENT_ACTION) + if (channel === ALL_CHANNELS_OR_DIFFERENT_ACTION) { this.resetAllControllers(); } @@ -65,17 +66,17 @@ export function handleMessage(message) this.resetControllers(channel); } break; - + case workletMessageType.systemExclusive: this.systemExclusive(data); break; - + case workletMessageType.setChannelVibrato: - if(channel === ALL_CHANNELS_OR_DIFFERENT_ACTION) + if (channel === ALL_CHANNELS_OR_DIFFERENT_ACTION) { for (let i = 0; i < this.workletProcessorChannels.length; i++) { - if(data.rate === -1) + if (data.rate === -1) { this.disableAndLockGSNRPN(i); } @@ -85,8 +86,7 @@ export function handleMessage(message) } } } - else - if(data.rate === -1) + else if (data.rate === -1) { this.disableAndLockGSNRPN(channel); } @@ -95,9 +95,9 @@ export function handleMessage(message) this.setVibrato(channel, data.depth, data.rate, data.delay); } break; - + case workletMessageType.stopAll: - if(channel === ALL_CHANNELS_OR_DIFFERENT_ACTION) + if (channel === ALL_CHANNELS_OR_DIFFERENT_ACTION) { this.stopAllChannels(data === 1); } @@ -106,23 +106,23 @@ export function handleMessage(message) this.stopAll(channel, data === 1); } break; - + case workletMessageType.killNotes: this.voiceKilling(data); break; - + case workletMessageType.muteChannel: this.muteChannel(channel, data); break; - + case workletMessageType.addNewChannel: this.createWorkletChannel(true); break; - + case workletMessageType.debugMessage: this.debugMessage(); break; - + case workletMessageType.setMasterParameter: /** * @type {masterParameterType} @@ -134,27 +134,27 @@ export function handleMessage(message) case masterParameterType.masterPan: this.setMasterPan(value); break; - + case masterParameterType.mainVolume: this.setMasterGain(value); break; - + case masterParameterType.voicesCap: this.voiceCap = value; break; - + case masterParameterType.interpolationType: this.interpolationType = value; break; } break; - + case workletMessageType.setDrums: this.setDrums(channel, data); break; - + case workletMessageType.transpose: - if(channel === ALL_CHANNELS_OR_DIFFERENT_ACTION) + if (channel === ALL_CHANNELS_OR_DIFFERENT_ACTION) { this.transposeAllChannels(data[0], data[1]); } @@ -163,13 +163,13 @@ export function handleMessage(message) this.transposeChannel(channel, data[0], data[1]); } break; - + case workletMessageType.highPerformanceMode: this.highPerformanceMode = data; break; - + case workletMessageType.lockController: - if(data[0] === ALL_CHANNELS_OR_DIFFERENT_ACTION) + if (data[0] === ALL_CHANNELS_OR_DIFFERENT_ACTION) { channelObject.lockPreset = data[1]; } @@ -178,24 +178,24 @@ export function handleMessage(message) channelObject.lockedControllers[data[0]] = data[1]; } break; - + case workletMessageType.sequencerSpecific: this.sequencer.processMessage(data.messageType, data.messageData); break; - + case workletMessageType.soundFontManager: this.soundfontManager.handleMessage(data[0], data[1]); this.clearSoundFont(true, false); break; - + case workletMessageType.requestSynthesizerSnapshot: this.sendSynthesizerSnapshot(); break; - + case workletMessageType.setLogLevel: SpessaSynthLogging(data[0], data[1], data[2], data[3]); break; - + default: SpessaSynthWarn("Unrecognized event:", data); break; diff --git a/src/spessasynth_lib/synthetizer/worklet_system/message_protocol/message_sending.js b/src/spessasynth_lib/synthetizer/worklet_system/message_protocol/message_sending.js index db3f7950..e36baf48 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/message_protocol/message_sending.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/message_protocol/message_sending.js @@ -1,5 +1,5 @@ -import { returnMessageType } from './worklet_message.js' -import { NON_CC_INDEX_OFFSET } from '../worklet_utilities/worklet_processor_channel.js' +import { returnMessageType } from "./worklet_message.js"; +import { NON_CC_INDEX_OFFSET } from "../worklet_utilities/worklet_processor_channel.js"; import { modulatorSources } from "../../../soundfont/basic_soundfont/modulator.js"; @@ -11,7 +11,7 @@ import { modulatorSources } from "../../../soundfont/basic_soundfont/modulator.j */ export function callEvent(eventName, eventData) { - if(!this.enableEventSystem) + if (!this.enableEventSystem) { return; } @@ -21,7 +21,7 @@ export function callEvent(eventName, eventData) eventName: eventName, eventData: eventData } - }) + }); } /** @@ -38,14 +38,15 @@ export function callEvent(eventName, eventData) */ export function sendChannelProperties() { - if(!this.enableEventSystem) + if (!this.enableEventSystem) { return; } /** * @type {ChannelProperty[]} */ - const data = this.workletProcessorChannels.map(c => { + const data = this.workletProcessorChannels.map(c => + { const range = (c.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheelRange] >> 7) + (c.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheelRange] & 0x7F) / 127; return { voicesAmount: c.voices.length, @@ -53,7 +54,7 @@ export function sendChannelProperties() pitchBendRangeSemitones: range, isMuted: c.isMuted, isDrum: c.drumChannel - } + }; }); this.post({ messageType: returnMessageType.channelProperties, diff --git a/src/spessasynth_lib/synthetizer/worklet_system/message_protocol/worklet_message.js b/src/spessasynth_lib/synthetizer/worklet_system/message_protocol/worklet_message.js index 9380112f..c3840a23 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/message_protocol/worklet_message.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/message_protocol/worklet_message.js @@ -29,32 +29,32 @@ * @property {number} setLogLevel - 25 -> [enableInfo, enableWarning, enableGroup, enableTable] */ export const workletMessageType = { - noteOff: 0, - noteOn: 1, - ccChange: 2, - programChange: 3, - channelPressure: 4, - polyPressure: 5, - killNote: 6, - ccReset: 7, - setChannelVibrato: 8, - soundFontManager: 9, - stopAll: 10, - killNotes: 11, - muteChannel: 12, - addNewChannel: 13, - customcCcChange: 14, - debugMessage: 15, - systemExclusive: 16, - setMasterParameter: 17, - setDrums: 18, - pitchWheel: 19, - transpose: 20, - highPerformanceMode: 21, - lockController: 22, - sequencerSpecific: 23, + noteOff: 0, + noteOn: 1, + ccChange: 2, + programChange: 3, + channelPressure: 4, + polyPressure: 5, + killNote: 6, + ccReset: 7, + setChannelVibrato: 8, + soundFontManager: 9, + stopAll: 10, + killNotes: 11, + muteChannel: 12, + addNewChannel: 13, + customcCcChange: 14, + debugMessage: 15, + systemExclusive: 16, + setMasterParameter: 17, + setDrums: 18, + pitchWheel: 19, + transpose: 20, + highPerformanceMode: 21, + lockController: 22, + sequencerSpecific: 23, requestSynthesizerSnapshot: 24, - setLogLevel: 25 + setLogLevel: 25 }; /** @@ -65,7 +65,7 @@ export const masterParameterType = { masterPan: 1, voicesCap: 2, interpolationType: 3 -} +}; export const ALL_CHANNELS_OR_DIFFERENT_ACTION = -1; @@ -121,5 +121,5 @@ export const returnMessageType = { synthesizerSnapshot: 4, ready: 5, soundfontError: 6, - identify: 7, -} \ No newline at end of file + identify: 7 +}; \ No newline at end of file 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 3c6965f7..fa6d9d35 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 @@ -1,10 +1,10 @@ -import { consoleColors } from '../../../utils/other.js' -import { midiControllers } from '../../../midi_parser/midi_message.js' -import { channelConfiguration, dataEntryStates } from '../worklet_utilities/worklet_processor_channel.js' -import { computeModulators } from '../worklet_utilities/worklet_modulator.js' -import { SpessaSynthInfo, SpessaSynthWarn } from '../../../utils/loggin.js' -import { SYNTHESIZER_GAIN } from '../main_processor.js' -import { DEFAULT_PERCUSSION } from '../../synthetizer.js' +import { consoleColors } from "../../../utils/other.js"; +import { midiControllers } from "../../../midi_parser/midi_message.js"; +import { channelConfiguration, dataEntryStates } from "../worklet_utilities/worklet_processor_channel.js"; +import { computeModulators } from "../worklet_utilities/worklet_modulator.js"; +import { SpessaSynthInfo, SpessaSynthWarn } from "../../../utils/loggin.js"; +import { SYNTHESIZER_GAIN } from "../main_processor.js"; +import { DEFAULT_PERCUSSION } from "../../synthetizer.js"; /** * @param channel {number} @@ -19,34 +19,37 @@ export function controllerChange(channel, controllerNumber, controllerValue, for * @type {WorkletProcessorChannel} */ const channelObject = this.workletProcessorChannels[channel]; - if(channelObject === undefined) + if (channelObject === undefined) { SpessaSynthWarn(`Trying to access channel ${channel} which does not exist... ignoring!`); return; } - if(controllerNumber > 127) + if (controllerNumber > 127) { // channel configuration. force must be set to true - if(!force) return; + if (!force) + { + return; + } switch (controllerNumber) { default: return; - + case channelConfiguration.velocityOverride: channelObject.velocityOverride = controllerValue; } } // lsb controller values: append them as the lower nibble of the 14 bit value // excluding bank select and data entry as it's handled separately - if( + if ( controllerNumber >= midiControllers.lsbForControl1ModulationWheel && controllerNumber <= midiControllers.lsbForControl13EffectControl2 && controllerNumber !== midiControllers.lsbForControl6DataEntry ) { const actualCCNum = controllerNumber - 32; - if(channelObject.lockedControllers[actualCCNum]) + if (channelObject.lockedControllers[actualCCNum]) { return; } @@ -59,22 +62,26 @@ export function controllerChange(channel, controllerNumber, controllerValue, for case midiControllers.allNotesOff: this.stopAll(channel); break; - + case midiControllers.allSoundOff: this.stopAll(channel, true); break; - + // special case: bank select case midiControllers.bankSelect: let bankNr = controllerValue; - if(!force) + if (!force) { - switch (this.system) { + switch (this.system) + { case "gm": // gm ignores bank select - SpessaSynthInfo(`%cIgnoring the Bank Select (${controllerValue}), as the synth is in GM mode.`, consoleColors.info); + SpessaSynthInfo( + `%cIgnoring the Bank Select (${controllerValue}), as the synth is in GM mode.`, + consoleColors.info + ); return; - + case "xg": // for xg, if msb is 120, 126 or 127, then it's drums if (bankNr === 120 || bankNr === 126 || bankNr === 127) @@ -84,13 +91,13 @@ export function controllerChange(channel, controllerNumber, controllerValue, for else { // drums shall not be disabled on channel 9 - if(channel % 16 !== DEFAULT_PERCUSSION) + if (channel % 16 !== DEFAULT_PERCUSSION) { this.setDrums(channel, false); } } break; - + case "gm2": if (bankNr === 120) { @@ -101,7 +108,7 @@ export function controllerChange(channel, controllerNumber, controllerValue, for }); } } - + if (channelObject.drumChannel) { // 128 for percussion channel @@ -113,63 +120,62 @@ export function controllerChange(channel, controllerNumber, controllerValue, for bankNr = channelObject.midiControllers[midiControllers.bankSelect]; } } - + channelObject.midiControllers[midiControllers.bankSelect] = bankNr; break; - + case midiControllers.lsbForControl0BankSelect: - if(this.system === 'xg') + if (this.system === "xg") { - if(!channelObject.drumChannel) + if (!channelObject.drumChannel) { // some soundfonts use 127 as drums and // if it's not marked as drums by bank MSB (line 47), then we DO NOT want the drums! - if(controllerValue !== 127) + if (controllerValue !== 127) { channelObject.midiControllers[midiControllers.bankSelect] = controllerValue; } } } - else - if(this.system === "gm2") + else if (this.system === "gm2") { channelObject.midiControllers[midiControllers.bankSelect] = controllerValue; } break; - + // check for RPN and NPRN and data entry case midiControllers.RPNLsb: channelObject.RPValue = channelObject.RPValue << 7 | controllerValue; channelObject.dataEntryState = dataEntryStates.RPFine; break; - + case midiControllers.RPNMsb: channelObject.RPValue = controllerValue; channelObject.dataEntryState = dataEntryStates.RPCoarse; break; - + case midiControllers.NRPNMsb: channelObject.NRPCoarse = controllerValue; channelObject.dataEntryState = dataEntryStates.NRPCoarse; break; - + case midiControllers.NRPNLsb: channelObject.NRPFine = controllerValue; channelObject.dataEntryState = dataEntryStates.NRPFine; break; - + case midiControllers.dataEntryMsb: this.dataEntryCoarse(channel, controllerValue); break; - + case midiControllers.lsbForControl6DataEntry: this.dataEntryFine(channel, controllerValue); break; - + case midiControllers.resetAllControllers: this.resetControllers(channel); break; - + case midiControllers.sustainPedal: if (controllerValue >= 64) { @@ -178,16 +184,17 @@ export function controllerChange(channel, controllerNumber, controllerValue, for else { channelObject.holdPedal = false; - channelObject.sustainedVoices.forEach(v => { - this.releaseVoice(v) + channelObject.sustainedVoices.forEach(v => + { + this.releaseVoice(v); }); channelObject.sustainedVoices = []; } break; - + // default: apply the controller to the table default: - if(channelObject.lockedControllers[controllerNumber]) + if (channelObject.lockedControllers[controllerNumber]) { return; } @@ -242,7 +249,7 @@ export function setMasterPan(pan) */ export function muteChannel(channel, isMuted) { - if(isMuted) + if (isMuted) { this.stopAll(channel, true); } diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/data_entry.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/data_entry.js index c3b57783..43cf4021 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/data_entry.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/data_entry.js @@ -1,11 +1,11 @@ -import { consoleColors } from '../../../utils/other.js' -import { midiControllers } from '../../../midi_parser/midi_message.js' +import { consoleColors } from "../../../utils/other.js"; +import { midiControllers } from "../../../midi_parser/midi_message.js"; import { customControllers, dataEntryStates, - NON_CC_INDEX_OFFSET, -} from '../worklet_utilities/worklet_processor_channel.js' -import { SpessaSynthInfo, SpessaSynthWarn } from '../../../utils/loggin.js' + NON_CC_INDEX_OFFSET +} from "../worklet_utilities/worklet_processor_channel.js"; +import { SpessaSynthInfo, SpessaSynthWarn } from "../../../utils/loggin.js"; import { modulatorSources } from "../../../soundfont/basic_soundfont/modulator.js"; /** @@ -23,141 +23,156 @@ export function dataEntryCoarse(channel, dataValue) const channelObject = this.workletProcessorChannels[channel]; let addDefaultVibrato = () => { - if(channelObject.channelVibrato.delay === 0 && channelObject.channelVibrato.rate === 0 && channelObject.channelVibrato.depth === 0) + if (channelObject.channelVibrato.delay === 0 && channelObject.channelVibrato.rate === 0 && channelObject.channelVibrato.depth === 0) { channelObject.channelVibrato.depth = 50; channelObject.channelVibrato.rate = 8; channelObject.channelVibrato.delay = 0.6; } - } - switch(channelObject.dataEntryState) + }; + switch (channelObject.dataEntryState) { default: case dataEntryStates.Idle: break; - + // https://cdn.roland.com/assets/media/pdf/SC-88PRO_OM.pdf // http://hummer.stanford.edu/sig/doc/classes/MidiOutput/rpn.html case dataEntryStates.NRPFine: - if(this.system !== "gs") + if (this.system !== "gs") { return; } - if(channelObject.lockGSNRPNParams) + if (channelObject.lockGSNRPNParams) { return; } - switch(channelObject.NRPCoarse) + switch (channelObject.NRPCoarse) { default: - if(dataValue === 64) + if (dataValue === 64) { // default value return; } SpessaSynthWarn( - `%cUnrecognized NRPN for %c${channel}%c: %c(0x${channelObject.NRPCoarse.toString(16).toUpperCase()} 0x${channelObject.NRPFine.toString(16).toUpperCase()})%c data value: %c${dataValue}`, + `%cUnrecognized NRPN for %c${channel}%c: %c(0x${channelObject.NRPCoarse.toString(16) + .toUpperCase()} 0x${channelObject.NRPFine.toString( + 16).toUpperCase()})%c data value: %c${dataValue}`, consoleColors.warn, consoleColors.recognized, consoleColors.warn, consoleColors.unrecognized, consoleColors.warn, - consoleColors.value); + consoleColors.value + ); break; - + // part parameters: vibrato, cutoff case 0x01: - switch(channelObject.NRPFine) + switch (channelObject.NRPFine) { default: - if(dataValue === 64) + if (dataValue === 64) { // default value return; } SpessaSynthWarn( - `%cUnrecognized NRPN for %c${channel}%c: %c(0x${channelObject.NRPCoarse.toString(16)} 0x${channelObject.NRPFine.toString(16)})%c data value: %c${dataValue}`, + `%cUnrecognized NRPN for %c${channel}%c: %c(0x${channelObject.NRPCoarse.toString(16)} 0x${channelObject.NRPFine.toString( + 16)})%c data value: %c${dataValue}`, consoleColors.warn, consoleColors.recognized, consoleColors.warn, consoleColors.unrecognized, consoleColors.warn, - consoleColors.value); + consoleColors.value + ); break; - + // vibrato rate case 0x08: - if(dataValue === 64) + if (dataValue === 64) { return; } addDefaultVibrato(); channelObject.channelVibrato.rate = (dataValue / 64) * 8; - SpessaSynthInfo(`%cVibrato rate for %c${channel}%c is now set to %c${dataValue} = ${channelObject.channelVibrato.rate}%cHz.`, + SpessaSynthInfo( + `%cVibrato rate for %c${channel}%c is now set to %c${dataValue} = ${channelObject.channelVibrato.rate}%cHz.`, consoleColors.info, consoleColors.recognized, consoleColors.info, consoleColors.value, - consoleColors.info); + consoleColors.info + ); break; - + // vibrato depth case 0x09: - if(dataValue === 64) + if (dataValue === 64) { return; } addDefaultVibrato(); channelObject.channelVibrato.depth = dataValue / 2; - SpessaSynthInfo(`%cVibrato depth for %c${channel}%c is now set to %c${dataValue} = ${channelObject.channelVibrato.depth}%c cents range of detune.`, + SpessaSynthInfo( + `%cVibrato depth for %c${channel}%c is now set to %c${dataValue} = ${channelObject.channelVibrato.depth}%c cents range of detune.`, consoleColors.info, consoleColors.recognized, consoleColors.info, consoleColors.value, - consoleColors.info); + consoleColors.info + ); break; - + // vibrato delay case 0x0A: - if(dataValue === 64) + if (dataValue === 64) { return; } addDefaultVibrato(); channelObject.channelVibrato.delay = (dataValue / 64) / 3; - SpessaSynthInfo(`%cVibrato delay for %c${channel}%c is now set to %c${dataValue} = ${channelObject.channelVibrato.delay}%c seconds.`, + SpessaSynthInfo( + `%cVibrato delay for %c${channel}%c is now set to %c${dataValue} = ${channelObject.channelVibrato.delay}%c seconds.`, consoleColors.info, consoleColors.recognized, consoleColors.info, consoleColors.value, - consoleColors.info); + consoleColors.info + ); break; - + // filter cutoff case 0x20: // affect the "brightness" controller as we have a default modulator that controls it const ccValue = dataValue; - this.controllerChange(channel, midiControllers.brightness, dataValue) - SpessaSynthInfo(`%cFilter cutoff for %c${channel}%c is now set to %c${ccValue}`, + this.controllerChange(channel, midiControllers.brightness, dataValue); + SpessaSynthInfo( + `%cFilter cutoff for %c${channel}%c is now set to %c${ccValue}`, consoleColors.info, consoleColors.recognized, consoleColors.info, - consoleColors.value); + consoleColors.value + ); } break; - + // drum key tuning case 0x18: // fine is the key number and data value is the semitone change const semitones = dataValue - 64; channelObject.keyCentTuning[channelObject.NRPFine] = semitones * 100; - SpessaSynthInfo(`%cGS drum key tuning. MIDI note: %c${channelObject.NRPFine}%c semitones: %c${semitones}`, + SpessaSynthInfo( + `%cGS drum key tuning. MIDI note: %c${channelObject.NRPFine}%c semitones: %c${semitones}`, consoleColors.info, consoleColors.recognized, consoleColors.info, - consoleColors.value) + consoleColors.value + ); break; - + // drum reverb case 0x1D: const reverb = dataValue; @@ -167,14 +182,15 @@ export function dataEntryCoarse(channel, dataValue) consoleColors.info, consoleColors.recognized, consoleColors.info, - consoleColors.value); + consoleColors.value + ); break; } break; - + case dataEntryStates.RPCoarse: case dataEntryStates.RPFine: - switch(channelObject.RPValue) + switch (channelObject.RPValue) { default: SpessaSynthWarn( @@ -184,41 +200,44 @@ export function dataEntryCoarse(channel, dataValue) consoleColors.warn, consoleColors.unrecognized, consoleColors.warn, - consoleColors.value); + consoleColors.value + ); break; - + // pitch bend range case 0x0000: channelObject.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheelRange] = dataValue << 7; - SpessaSynthInfo(`%cChannel ${channel} bend range. Semitones: %c${dataValue}`, + SpessaSynthInfo( + `%cChannel ${channel} bend range. Semitones: %c${dataValue}`, consoleColors.info, - consoleColors.value); + consoleColors.value + ); break; - + // coarse tuning case 0x0002: // semitones this.setChannelTuningSemitones(channel, dataValue - 64); break; - + // fine tuning case 0x0001: // note: this will not work properly unless the lsb is sent! // here we store the raw value to then adjust in fine this.setChannelTuning(channel, (dataValue - 64), false); break; - + // modulation depth case 0x0005: this.setModulationDepth(channel, dataValue * 100); - break - + break; + case 0x3FFF: this.resetParameters(channel); break; - + } - + } } @@ -236,27 +255,29 @@ export function dataEntryFine(channel, dataValue) { default: break; - + case dataEntryStates.RPCoarse: case dataEntryStates.RPFine: - switch(channelObject.RPValue) + switch (channelObject.RPValue) { default: break; - + // pitch bend range fine tune case 0x0000: - if(dataValue === 0) + if (dataValue === 0) { break; } channelObject.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheelRange] |= dataValue; // 14 bit value so upper 7 are coarse and lower 7 are fine! const actualTune = (channelObject.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheelRange] >> 7) + dataValue / 127; - SpessaSynthInfo(`%cChannel ${channel} bend range. Semitones: %c${actualTune}`, + SpessaSynthInfo( + `%cChannel ${channel} bend range. Semitones: %c${actualTune}`, consoleColors.info, - consoleColors.value); + consoleColors.value + ); break; - + // fine tuning case 0x0001: // grab the data and shift @@ -264,19 +285,19 @@ export function dataEntryFine(channel, dataValue) const finalTuning = (coarse << 7) | dataValue; this.setChannelTuning(channel, finalTuning * 0.01220703125); // multiply by 8192 / 100 (cent increment) break; - + // modulation depth case 0x0005: const currentModulationDepthCents = channelObject.customControllers[customControllers.modulationMultiplier] * 50; let cents = currentModulationDepthCents + (dataValue / 128) * 100; this.setModulationDepth(channel, cents); - break - + break; + case 0x3FFF: this.resetParameters(channel); break; - + } - + } } \ No newline at end of file diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/note_off.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/note_off.js index 5b7a5d38..ebb96a72 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/note_off.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/note_off.js @@ -1,5 +1,5 @@ -import { consoleColors } from '../../../utils/other.js' -import { SpessaSynthInfo, SpessaSynthWarn } from '../../../utils/loggin.js' +import { consoleColors } from "../../../utils/other.js"; +import { SpessaSynthInfo, SpessaSynthWarn } from "../../../utils/loggin.js"; import { generatorTypes } from "../../../soundfont/basic_soundfont/generator.js"; /** @@ -10,38 +10,40 @@ import { generatorTypes } from "../../../soundfont/basic_soundfont/generator.js" */ export function noteOff(channel, midiNote) { - if(midiNote > 127 || midiNote < 0) + if (midiNote > 127 || midiNote < 0) { SpessaSynthWarn(`Received a noteOn for note`, midiNote, "Ignoring."); return; } - + let actualNote = midiNote + this.workletProcessorChannels[channel].channelTransposeKeyShift; const program = this.workletProcessorChannels[channel].preset.program; - if(this.tunings[program]?.[midiNote]?.midiNote >= 0) + if (this.tunings[program]?.[midiNote]?.midiNote >= 0) { actualNote = this.tunings[program]?.[midiNote].midiNote; } - + // if high performance mode, kill notes instead of stopping them - if(this.highPerformanceMode) + if (this.highPerformanceMode) { // if the channel is percussion channel, do not kill the notes - if(!this.workletProcessorChannels[channel].drumChannel) + if (!this.workletProcessorChannels[channel].drumChannel) { this.killNote(channel, actualNote); return; } } - + const channelVoices = this.workletProcessorChannels[channel].voices; - channelVoices.forEach(v => { - if(v.midiNote !== actualNote || v.isInRelease === true) + channelVoices.forEach(v => + { + if (v.midiNote !== actualNote || v.isInRelease === true) { return; } // if hold pedal, move to sustain - if(this.workletProcessorChannels[channel].holdPedal) { + if (this.workletProcessorChannels[channel].holdPedal) + { this.workletProcessorChannels[channel].sustainedVoices.push(v); } else @@ -63,8 +65,9 @@ export function noteOff(channel, midiNote) */ export function killNote(channel, midiNote) { - this.workletProcessorChannels[channel].voices.forEach(v => { - if(v.midiNote !== midiNote) + this.workletProcessorChannels[channel].voices.forEach(v => + { + if (v.midiNote !== midiNote) { return; } @@ -82,7 +85,7 @@ export function killNote(channel, midiNote) export function stopAll(channel, force = false) { const channelVoices = this.workletProcessorChannels[channel].voices; - if(force) + if (force) { // force stop all channelVoices.length = 0; @@ -91,13 +94,18 @@ export function stopAll(channel, force = false) } else { - channelVoices.forEach(v => { - if(v.isInRelease) return; + channelVoices.forEach(v => + { + if (v.isInRelease) + { + return; + } this.releaseVoice(v); }); - this.workletProcessorChannels[channel].sustainedVoices.forEach(v => { + this.workletProcessorChannels[channel].sustainedVoices.forEach(v => + { this.releaseVoice(v); - }) + }); } } @@ -108,7 +116,8 @@ export function stopAll(channel, force = false) export function stopAllChannels(force = false) { SpessaSynthInfo("%cStop all received!", consoleColors.info); - for (let i = 0; i < this.workletProcessorChannels.length; i++) { + for (let i = 0; i < this.workletProcessorChannels.length; i++) + { this.stopAll(i, force); } this.callEvent("stopall", undefined); diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/note_on.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/note_on.js index 76a13487..727a0ce4 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/note_on.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/note_on.js @@ -1,6 +1,6 @@ -import { computeModulators } from '../worklet_utilities/worklet_modulator.js' -import { WorkletVolumeEnvelope } from '../worklet_utilities/volume_envelope.js' -import { WorkletModulationEnvelope } from '../worklet_utilities/modulation_envelope.js' +import { computeModulators } from "../worklet_utilities/worklet_modulator.js"; +import { WorkletVolumeEnvelope } from "../worklet_utilities/volume_envelope.js"; +import { WorkletModulationEnvelope } from "../worklet_utilities/modulation_envelope.js"; import { generatorTypes } from "../../../soundfont/basic_soundfont/generator.js"; /** @@ -20,8 +20,8 @@ export function noteOn(channel, midiNote, velocity, enableDebugging = false, sen this.noteOff(channel, midiNote); return; } - - const channelObject = this.workletProcessorChannels[channel] + + const channelObject = this.workletProcessorChannels[channel]; if ( (this.highPerformanceMode && this.totalVoicesAmount > 200 && velocity < 40) || (this.highPerformanceMode && velocity < 10) || @@ -30,25 +30,25 @@ export function noteOn(channel, midiNote, velocity, enableDebugging = false, sen { return; } - + let sentMidiNote = midiNote + channelObject.channelTransposeKeyShift; - - if(midiNote > 127 || midiNote < 0) + + if (midiNote > 127 || midiNote < 0) { return; } const program = channelObject.preset.program; - if(this.tunings[program]?.[midiNote]?.midiNote >= 0) + if (this.tunings[program]?.[midiNote]?.midiNote >= 0) { sentMidiNote = this.tunings[program]?.[midiNote].midiNote; } - + // velocity override - if(channelObject.velocityOverride > 0) + if (channelObject.velocityOverride > 0) { velocity = channelObject.velocityOverride; } - + // get voices const voices = this.getWorkletVoices( channel, @@ -58,16 +58,18 @@ export function noteOn(channel, midiNote, velocity, enableDebugging = false, sen startTime, enableDebugging ); - + // add voices and exclusive class apply const channelVoices = channelObject.voices; - voices.forEach(voice => { + voices.forEach(voice => + { const exclusive = voice.generators[generatorTypes.exclusiveClass]; - if(exclusive !== 0) + if (exclusive !== 0) { // kill all voices with the same exclusive class - channelVoices.forEach(v => { - if(v.generators[generatorTypes.exclusiveClass] === exclusive) + channelVoices.forEach(v => + { + if (v.generators[generatorTypes.exclusiveClass] === exclusive) { this.releaseVoice(v); v.modulatedGenerators[generatorTypes.releaseVolEnv] = -7000; // make the release nearly instant @@ -75,7 +77,7 @@ export function noteOn(channel, midiNote, velocity, enableDebugging = false, sen WorkletVolumeEnvelope.recalculate(v); WorkletModulationEnvelope.recalculate(v); } - }) + }); } // compute all modulators computeModulators(voice, channelObject.midiControllers); @@ -87,12 +89,12 @@ export function noteOn(channel, midiNote, velocity, enableDebugging = false, sen const sm = voice.sample; // apply them const clamp = num => Math.max(0, Math.min(sm.sampleData.length - 1, num)); - sm.cursor = clamp( sm.cursor + cursorStartOffset); + sm.cursor = clamp(sm.cursor + cursorStartOffset); sm.end = clamp(sm.end + endOffset); sm.loopStart = clamp(sm.loopStart + loopStartOffset); sm.loopEnd = clamp(sm.loopEnd + loopEndOffset); // swap loops if needed - if(sm.loopEnd < sm.loopStart) + if (sm.loopEnd < sm.loopStart) { const temp = sm.loopStart; sm.loopStart = sm.loopEnd; @@ -107,23 +109,26 @@ export function noteOn(channel, midiNote, velocity, enableDebugging = false, sen // as it's interpolated (we don't want 0 attenuation for even a split second) voice.volumeEnvelope.attenuation = voice.volumeEnvelope.attenuationTarget; // set initial pan to avoid split second changing from middle to the correct value - voice.currentPan = ((Math.max(-500, Math.min(500, voice.modulatedGenerators[generatorTypes.pan] )) + 500) / 1000) // 0 to 1 + voice.currentPan = ((Math.max( + -500, + Math.min(500, voice.modulatedGenerators[generatorTypes.pan]) + ) + 500) / 1000); // 0 to 1 }); - + this.totalVoicesAmount += voices.length; // cap the voices - if(this.totalVoicesAmount > this.voiceCap) + if (this.totalVoicesAmount > this.voiceCap) { this.voiceKilling(voices.length); } channelVoices.push(...voices); - if(sendEvent) + if (sendEvent) { this.sendChannelProperties(); this.callEvent("noteon", { midiNote: midiNote, channel: channel, - velocity: velocity, + velocity: velocity }); } } \ No newline at end of file diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/program_control.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/program_control.js index 3fbebe03..08d75160 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/program_control.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/program_control.js @@ -1,8 +1,8 @@ -import { midiControllers } from '../../../midi_parser/midi_message.js' -import { returnMessageType } from '../message_protocol/worklet_message.js' -import { SpessaSynthInfo, SpessaSynthWarn } from '../../../utils/loggin.js' -import { consoleColors } from '../../../utils/other.js' -import { loadSoundFont } from '../../../soundfont/load_soundfont.js' +import { midiControllers } from "../../../midi_parser/midi_message.js"; +import { returnMessageType } from "../message_protocol/worklet_message.js"; +import { SpessaSynthInfo, SpessaSynthWarn } from "../../../utils/loggin.js"; +import { consoleColors } from "../../../utils/other.js"; +import { loadSoundFont } from "../../../soundfont/load_soundfont.js"; /** * executes a program change @@ -11,18 +11,18 @@ import { loadSoundFont } from '../../../soundfont/load_soundfont.js' * @param userChange {boolean} * @this {SpessaSynthProcessor} */ -export function programChange(channel, programNumber, userChange=false) +export function programChange(channel, programNumber, userChange = false) { /** * @type {WorkletProcessorChannel} */ const channelObject = this.workletProcessorChannels[channel]; - if(channelObject === undefined) + if (channelObject === undefined) { SpessaSynthWarn(`Trying to access channel ${channel} which does not exist... ignoring!`); return; } - if(channelObject.lockPreset) + if (channelObject.lockPreset) { return; } @@ -30,13 +30,13 @@ export function programChange(channel, programNumber, userChange=false) const bank = channelObject.drumChannel ? 128 : channelObject.midiControllers[midiControllers.bankSelect]; let sentBank; let preset; - + // check if override - if(this.overrideSoundfont) + if (this.overrideSoundfont) { const bankWithOffset = bank === 128 ? 128 : bank - this.soundfontBankOffset; const p = this.overrideSoundfont.getPresetNoFallback(bankWithOffset, programNumber); - if(p) + if (p) { sentBank = bank; preset = p; @@ -56,7 +56,7 @@ export function programChange(channel, programNumber, userChange=false) channelObject.presetUsesOverride = false; } this.setPreset(channel, preset); - this.callEvent("programchange",{ + this.callEvent("programchange", { channel: channel, program: preset.program, bank: sentBank, @@ -72,12 +72,12 @@ export function programChange(channel, programNumber, userChange=false) */ export function getPreset(bank, program) { - if(this.overrideSoundfont) + if (this.overrideSoundfont) { // if overriden soundfont const bankWithOffset = bank === 128 ? 128 : bank - this.soundfontBankOffset; const preset = this.overrideSoundfont.getPresetNoFallback(bankWithOffset, program); - if(preset) + if (preset) { return preset; } @@ -86,7 +86,6 @@ export function getPreset(bank, program) } - /** * @param channel {number} * @param preset {BasicPreset} @@ -94,13 +93,13 @@ export function getPreset(bank, program) */ export function setPreset(channel, preset) { - if(this.workletProcessorChannels[channel].lockPreset) + if (this.workletProcessorChannels[channel].lockPreset) { return; } delete this.workletProcessorChannels[channel].preset; this.workletProcessorChannels[channel].preset = preset; - + // reset cached voices this.workletProcessorChannels[channel].cachedVoices = []; for (let i = 0; i < 128; i++) @@ -118,15 +117,15 @@ export function setPreset(channel, preset) export function setDrums(channel, isDrum) { const channelObject = this.workletProcessorChannels[channel]; - if(channelObject.lockPreset) + if (channelObject.lockPreset) { return; } - if(channelObject.drumChannel === isDrum) + if (channelObject.drumChannel === isDrum) { return; } - if(isDrum) + if (isDrum) { // clear transpose channelObject.channelTransposeKeyShift = 0; @@ -136,10 +135,16 @@ export function setDrums(channel, isDrum) else { channelObject.drumChannel = false; - this.setPreset(channel, this.getPreset(channelObject.midiControllers[midiControllers.bankSelect], channelObject.preset.program)); + this.setPreset( + channel, + this.getPreset( + channelObject.midiControllers[midiControllers.bankSelect], + channelObject.preset.program + ) + ); } channelObject.presetUsesOverride = false; - this.callEvent("drumchange",{ + this.callEvent("drumchange", { channel: channel, isDrumChannel: channelObject.drumChannel }); @@ -155,18 +160,23 @@ export function sendPresetList() * @type {{bank: number, presetName: string, program: number}[]} */ const mainFont = this.soundfontManager.getPresetList(); - if(this.overrideSoundfont !== undefined) + if (this.overrideSoundfont !== undefined) { - this.overrideSoundfont.presets.forEach(p => { + this.overrideSoundfont.presets.forEach(p => + { const bankCheck = p.bank === 128 ? 128 : p.bank + this.soundfontBankOffset; const exists = mainFont.find(pr => pr.bank === bankCheck && pr.program === p.program); - if(exists !== undefined) + if (exists !== undefined) { exists.presetName = p.presetName; } else { - mainFont.push({presetName: p.presetName, bank: bankCheck, program: p.program}); + mainFont.push({ + presetName: p.presetName, + bank: bankCheck, + program: p.program + }); } }); } @@ -181,15 +191,15 @@ export function sendPresetList() export function clearSoundFont(sendPresets = true, clearOverride = true) { this.stopAllChannels(true); - if(clearOverride) + if (clearOverride) { delete this.overrideSoundfont; this.overrideSoundfont = undefined; } this.defaultPreset = this.getPreset(0, 0); this.drumPreset = this.getPreset(128, 0); - - for(let i = 0; i < this.workletProcessorChannels.length; i++) + + for (let i = 0; i < this.workletProcessorChannels.length; i++) { const channelObject = this.workletProcessorChannels[i]; channelObject.cachedVoices = []; @@ -197,13 +207,13 @@ export function clearSoundFont(sendPresets = true, clearOverride = true) { channelObject.cachedVoices.push([]); } - if(!clearOverride) + if (!clearOverride) { channelObject.lockPreset = false; } this.programChange(i, channelObject.preset.program); } - if(sendPresets) + if (sendPresets) { this.sendPresetList(); } @@ -219,18 +229,17 @@ export function reloadSoundFont(buffer, isOverride = false) this.clearSoundFont(false, isOverride); try { - if(isOverride) + if (isOverride) { this.overrideSoundfont = loadSoundFont(buffer); // assign sample offset - this.overrideSoundfont.setSampleIDOffset(this.soundfontManager.totalSoundfontOffset) + this.overrideSoundfont.setSampleIDOffset(this.soundfontManager.totalSoundfontOffset); } else { this.soundfontManager.reloadManager(buffer); } - } - catch (e) + } catch (e) { this.post({ messageType: returnMessageType.soundfontError, @@ -240,10 +249,11 @@ export function reloadSoundFont(buffer, isOverride = false) } this.defaultPreset = this.getPreset(0, 0); this.drumPreset = this.getPreset(128, 0); - this.workletProcessorChannels.forEach((c, cNum) => { + this.workletProcessorChannels.forEach((c, cNum) => + { this.programChange(cNum, c.preset.program); }); - this.post({messageType: returnMessageType.ready, messageData: undefined}); + this.post({ messageType: returnMessageType.ready, messageData: undefined }); this.sendPresetList(); SpessaSynthInfo("%cSpessaSynth is ready!", consoleColors.recognized); } @@ -261,9 +271,9 @@ export function setEmbeddedSoundFont(font, offset) this.reloadSoundFont(font, true); // preload all samples this.overrideSoundfont.samples.forEach(s => s.getAudioData()); - + // apply snapshot again if applicable - if(this._snapshot !== undefined) + if (this._snapshot !== undefined) { this.applySynthesizerSnapshot(this._snapshot); this.resetAllControllers(); diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/reset_controllers.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/reset_controllers.js index 958a1182..ab058335 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/reset_controllers.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/reset_controllers.js @@ -1,34 +1,38 @@ -import { consoleColors } from '../../../utils/other.js' -import { midiControllers } from '../../../midi_parser/midi_message.js' -import { DEFAULT_PERCUSSION, DEFAULT_SYNTH_MODE } from '../../synthetizer.js' +import { consoleColors } from "../../../utils/other.js"; +import { midiControllers } from "../../../midi_parser/midi_message.js"; +import { DEFAULT_PERCUSSION, DEFAULT_SYNTH_MODE } from "../../synthetizer.js"; import { customControllers, - customResetArray, dataEntryStates, + customResetArray, + dataEntryStates, NON_CC_INDEX_OFFSET, - resetArray, -} from '../worklet_utilities/worklet_processor_channel.js' -import { SpessaSynthInfo } from '../../../utils/loggin.js' + resetArray +} from "../worklet_utilities/worklet_processor_channel.js"; +import { SpessaSynthInfo } from "../../../utils/loggin.js"; import { modulatorSources } from "../../../soundfont/basic_soundfont/modulator.js"; /** * @this {SpessaSynthProcessor} * @param log {boolean} */ -export function resetAllControllers(log= true) +export function resetAllControllers(log = true) { - if (log) SpessaSynthInfo("%cResetting all controllers!", consoleColors.info); + if (log) + { + SpessaSynthInfo("%cResetting all controllers!", consoleColors.info); + } this.callEvent("allcontrollerreset", undefined); for (let channelNumber = 0; channelNumber < this.workletProcessorChannels.length; channelNumber++) { this.resetControllers(channelNumber); - + /** * @type {WorkletProcessorChannel} **/ const ch = this.workletProcessorChannels[channelNumber]; - + // if preset is unlocked, switch to non drums and call event - if(!ch.lockPreset) + if (!ch.lockPreset) { ch.presetUsesOverride = true; ch.midiControllers[midiControllers.bankSelect] = 0; @@ -56,9 +60,9 @@ export function resetAllControllers(log= true) this.callEvent("drumchange", { channel: channelNumber, isDrumChannel: ch.drumChannel - }) + }); } - + // call program change this.callEvent("programchange", { channel: channelNumber, @@ -66,10 +70,10 @@ export function resetAllControllers(log= true) bank: ch.preset.bank, userCalled: false }); - + let restoreControllerValueEvent = ccNum => { - if(this.workletProcessorChannels[channelNumber].lockedControllers[ccNum]) + if (this.workletProcessorChannels[channelNumber].lockedControllers[ccNum]) { // was not reset so restore the value this.callEvent("controllerchange", { @@ -78,9 +82,9 @@ export function resetAllControllers(log= true) controllerValue: this.workletProcessorChannels[channelNumber].midiControllers[ccNum] >> 7 }); } - - } - + + }; + restoreControllerValueEvent(midiControllers.mainVolume); restoreControllerValueEvent(midiControllers.pan); restoreControllerValueEvent(midiControllers.expressionController); @@ -88,9 +92,9 @@ export function resetAllControllers(log= true) restoreControllerValueEvent(midiControllers.effects3Depth); restoreControllerValueEvent(midiControllers.effects1Depth); restoreControllerValueEvent(midiControllers.brightness); - + // restore pitch wheel - if(this.workletProcessorChannels[channelNumber].lockedControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel]) + if (this.workletProcessorChannels[channelNumber].lockedControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel]) { const val = this.workletProcessorChannels[channelNumber].midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel]; const msb = val >> 7; @@ -99,16 +103,16 @@ export function resetAllControllers(log= true) channel: channelNumber, MSB: msb, LSB: lsb - }) + }); } } this.tunings = []; this.tunings = []; - for (let i = 0; i < 127; i++) + for (let i = 0; 127 > i; i++) { this.tunings.push([]); } - + this.setMIDIVolume(1); this.system = DEFAULT_SYNTH_MODE; } @@ -125,41 +129,44 @@ export function resetControllers(channel) * get excluded (locked) cc numbers as locked ccs are unaffected by reset * @type {number[]} */ - const excludedCCs = channelObject.lockedControllers.reduce((lockedCCs, cc, ccNum) => { - if(cc) + const excludedCCs = channelObject.lockedControllers.reduce((lockedCCs, cc, ccNum) => + { + if (cc) { lockedCCs.push(ccNum); } return lockedCCs; }, []); // save excluded controllers as reset doesn't affect them - let excludedCCvalues = excludedCCs.map(ccNum => { + let excludedCCvalues = excludedCCs.map(ccNum => + { return { ccNum: ccNum, ccVal: channelObject.midiControllers[ccNum] - } + }; }); - + channelObject.channelOctaveTuning.fill(0); channelObject.keyCentTuning.fill(0); - + // reset the array channelObject.midiControllers.set(resetArray); - channelObject.channelVibrato = {rate: 0, depth: 0, delay: 0}; + channelObject.channelVibrato = { rate: 0, depth: 0, delay: 0 }; channelObject.holdPedal = false; - - excludedCCvalues.forEach((cc) => { + + excludedCCvalues.forEach((cc) => + { channelObject.midiControllers[cc.ccNum] = cc.ccVal; }); - + // reset custom controllers // special case: transpose does not get affected const transpose = channelObject.customControllers[customControllers.channelTransposeFine]; channelObject.customControllers.set(customResetArray); channelObject.customControllers[customControllers.channelTransposeFine] = transpose; - + this.resetParameters(channel); - + } /** @@ -169,7 +176,7 @@ export function resetControllers(channel) export function resetParameters(channel) { const channelObject = this.workletProcessorChannels[channel]; - + // reset parameters /** * @type {number} diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/snapshot.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/snapshot.js index 0f3376a4..f282aefa 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/snapshot.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/snapshot.js @@ -1,4 +1,3 @@ - /** * @typedef {Object} ChannelSnapshot - a snapshot of the channel. * @@ -35,10 +34,10 @@ * @property {number} transposition - the current synth transpositon in semitones. can be a float */ -import { returnMessageType } from '../message_protocol/worklet_message.js' -import { SpessaSynthInfo } from '../../../utils/loggin.js' -import { consoleColors } from '../../../utils/other.js' -import { midiControllers } from '../../../midi_parser/midi_message.js' +import { returnMessageType } from "../message_protocol/worklet_message.js"; +import { SpessaSynthInfo } from "../../../utils/loggin.js"; +import { consoleColors } from "../../../utils/other.js"; +import { midiControllers } from "../../../midi_parser/midi_message.js"; /** * sends a snapshot of the current controller values of the synth (used to copy that data to OfflineAudioContext when rendering) @@ -49,29 +48,30 @@ export function sendSynthesizerSnapshot() /** * @type {ChannelSnapshot[]} */ - const channelSnapshots = this.workletProcessorChannels.map(channel => { + const channelSnapshots = this.workletProcessorChannels.map(channel => + { return { program: channel.preset.program, bank: channel.preset.bank, lockPreset: channel.lockPreset, patchName: channel.preset.presetName, - + midiControllers: channel.midiControllers, lockedControllers: channel.lockedControllers, customControllers: channel.customControllers, - + channelVibrato: channel.channelVibrato, lockVibrato: channel.lockGSNRPNParams, - + channelTransposeKeyShift: channel.channelTransposeKeyShift, channelOctaveTuning: channel.channelOctaveTuning, keyCentTuning: channel.keyCentTuning, velocityOverride: channel.velocityOverride, isMuted: channel.isMuted, drumChannel: channel.drumChannel - } + }; }); - + /** * @type {SynthesizerSnapshot} */ @@ -83,7 +83,7 @@ export function sendSynthesizerSnapshot() system: this.system, interpolation: this.interpolationType }; - + this.post({ messageType: returnMessageType.synthesizerSnapshot, messageData: synthesizerSnapshot @@ -99,37 +99,38 @@ export function applySynthesizerSnapshot(snapshot) { // restore system this.system = snapshot.system; - + // restore pan and volume this.setMasterGain(snapshot.mainVolume); this.setMasterPan(snapshot.pan); this.transposeAllChannels(snapshot.transposition); this.interpolationType = snapshot.interpolation; - + // add channels if more needed - while(this.workletProcessorChannels.length < snapshot.channelSnapshots.length) + while (this.workletProcessorChannels.length < snapshot.channelSnapshots.length) { this.createWorkletChannel(); } - + // restore cahnnels - snapshot.channelSnapshots.forEach((channelSnapshot, index) => { + snapshot.channelSnapshots.forEach((channelSnapshot, index) => + { const channelObject = this.workletProcessorChannels[index]; this.muteChannel(index, channelSnapshot.isMuted); this.setDrums(index, channelSnapshot.drumChannel); - + // restore controllers channelObject.midiControllers = channelSnapshot.midiControllers; channelObject.lockedControllers = channelSnapshot.lockedControllers; channelObject.customControllers = channelSnapshot.customControllers; - + // restore vibrato and transpose channelObject.channelVibrato = channelSnapshot.channelVibrato; channelObject.lockGSNRPNParams = channelSnapshot.lockVibrato; channelObject.channelTransposeKeyShift = channelSnapshot.channelTransposeKeyShift; channelObject.channelOctaveTuning = channelSnapshot.channelOctaveTuning; channelObject.velocityOverride = channelSnapshot.velocityOverride; - + // restore preset and lock channelObject.lockPreset = false; channelObject.midiControllers[midiControllers.bankSelect] = channelSnapshot.bank; diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/system_exclusive.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/system_exclusive.js index ae960a83..95831634 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/system_exclusive.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/system_exclusive.js @@ -1,7 +1,7 @@ -import { arrayToHexString, consoleColors } from '../../../utils/other.js' -import { SpessaSynthInfo, SpessaSynthWarn } from '../../../utils/loggin.js' -import { midiControllers } from '../../../midi_parser/midi_message.js' -import { ALL_CHANNELS_OR_DIFFERENT_ACTION } from '../message_protocol/worklet_message.js' +import { arrayToHexString, consoleColors } from "../../../utils/other.js"; +import { SpessaSynthInfo, SpessaSynthWarn } from "../../../utils/loggin.js"; +import { midiControllers } from "../../../midi_parser/midi_message.js"; +import { ALL_CHANNELS_OR_DIFFERENT_ACTION } from "../message_protocol/worklet_message.js"; /** * KeyNum: tuning @@ -25,13 +25,13 @@ function getTuning(byte1, byte2, byte3) { const midiNote = byte1; const fraction = (byte2 << 7) | byte3; // Combine byte2 and byte3 into a 14-bit number - + // no change if (byte1 === 0x7F && byte2 === 0x7F && byte3 === 0x7F) { return { midiNote: -1, centTuning: null }; } - + // calculate cent tuning return { midiNote: midiNote, centTuning: fraction * 0.0061 }; } @@ -46,9 +46,9 @@ function getTuning(byte1, byte2, byte3) export function systemExclusive(messageData, channelOffset = 0) { const type = messageData[0]; - if(this.deviceID !== ALL_CHANNELS_OR_DIFFERENT_ACTION && messageData[1] !== 0x7F) + if (this.deviceID !== ALL_CHANNELS_OR_DIFFERENT_ACTION && messageData[1] !== 0x7F) { - if(this.deviceID !== messageData[1]) + if (this.deviceID !== messageData[1]) { // not our device ID return; @@ -57,79 +57,90 @@ export function systemExclusive(messageData, channelOffset = 0) switch (type) { default: - SpessaSynthWarn(`%cUnrecognized SysEx: %c${arrayToHexString(messageData)}`, + SpessaSynthWarn( + `%cUnrecognized SysEx: %c${arrayToHexString(messageData)}`, consoleColors.warn, - consoleColors.unrecognized); + consoleColors.unrecognized + ); break; - + // non realtime case 0x7E: case 0x7F: - switch(messageData[2]) + switch (messageData[2]) { case 0x04: - let cents + let cents; // device control - switch(messageData[3]) + switch (messageData[3]) { case 0x01: // main volume const vol = messageData[5] << 7 | messageData[4]; this.setMIDIVolume(vol / 16384); - SpessaSynthInfo(`%cMaster Volume. Volume: %c${vol}`, + SpessaSynthInfo( + `%cMaster Volume. Volume: %c${vol}`, consoleColors.info, - consoleColors.value); + consoleColors.value + ); break; - + case 0x02: // main balance // midi spec page 62 const balance = messageData[5] << 7 | messageData[4]; const pan = (balance - 8192) / 8192; this.setMasterPan(pan); - SpessaSynthInfo(`%cMaster Pan. Pan: %c${pan}`, + SpessaSynthInfo( + `%cMaster Pan. Pan: %c${pan}`, consoleColors.info, - consoleColors.value); + consoleColors.value + ); break; - - + + case 0x03: // fine tuning const tuningValue = ((messageData[5] << 7) | messageData[6]) - 8192; cents = Math.floor(tuningValue / 81.92); // [-100;+99] cents range this.setMasterTuning(cents); - SpessaSynthInfo(`%cMaster Fine Tuning. Cents: %c${cents}`, + SpessaSynthInfo( + `%cMaster Fine Tuning. Cents: %c${cents}`, consoleColors.info, - consoleColors.value); + consoleColors.value + ); break; - + case 0x04: // coarse tuning // lsb is ignored const semitones = messageData[5] - 64; cents = semitones * 100; this.setMasterTuning(cents); - SpessaSynthInfo(`%cMaster Coarse Tuning. Cents: %c${cents}`, + SpessaSynthInfo( + `%cMaster Coarse Tuning. Cents: %c${cents}`, consoleColors.info, - consoleColors.value) + consoleColors.value + ); break; - + default: SpessaSynthWarn( `%cUnrecognized MIDI Device Control Real-time message: %c${arrayToHexString(messageData)}`, consoleColors.warn, - consoleColors.unrecognized); + consoleColors.unrecognized + ); } break; - + case 0x09: // gm system related - if(messageData[3] === 0x01) + if (messageData[3] === 0x01) { SpessaSynthInfo("%cGM system on", consoleColors.info); this.system = "gm"; } - else if(messageData[3] === 0x03) + else if (messageData[3] === 0x03) { SpessaSynthInfo("%cGM2 system on", consoleColors.info); this.system = "gm2"; @@ -140,18 +151,18 @@ export function systemExclusive(messageData, channelOffset = 0) this.system = "gs"; } break; - + // MIDI Tuning standard // https://midi.org/midi-tuning-updated-specification case 0x08: - switch(messageData[3]) + switch (messageData[3]) { // single note change // single note change bank case 0x02: case 0x07: let currentMessageIndex = 4; - if(messageData[3] === 0x07) + if (messageData[3] === 0x07) { // skip bank currentMessageIndex++; @@ -165,16 +176,18 @@ export function systemExclusive(messageData, channelOffset = 0) this.tunings[tuningProgram][messageData[currentMessageIndex++]] = getTuning( messageData[currentMessageIndex++], messageData[currentMessageIndex++], - messageData[currentMessageIndex++], + messageData[currentMessageIndex++] ); } - SpessaSynthInfo(`%cSingle Note Tuning. Program: %c${tuningProgram}%c Keys affected: ${numberOfChanges}`, + SpessaSynthInfo( + `%cSingle Note Tuning. Program: %c${tuningProgram}%c Keys affected: ${numberOfChanges}`, consoleColors.info, consoleColors.recognized, consoleColors.info, - consoleColors.recognized) + consoleColors.recognized + ); break; - + // octave tuning (1 byte) // and octave tuning (2 bytes) case 0x09: @@ -182,7 +195,7 @@ export function systemExclusive(messageData, channelOffset = 0) // get tuning: const newOctaveTuning = new Int8Array(12); // start from bit 7 - if(messageData[3] === 0x08) + if (messageData[3] === 0x08) { // 1 byte tuning: 0 is -64 cents, 64 is 0, 127 is +63 for (let i = 0; i < 12; i++) @@ -201,89 +214,97 @@ export function systemExclusive(messageData, channelOffset = 0) } // apply to channels (ordered from 0) // bit 1: 14 and 15 - if((messageData[4] & 1) === 1) + if ((messageData[4] & 1) === 1) { this.setOctaveTuning(14 + channelOffset, newOctaveTuning); } - if(((messageData[4] >> 1) & 1) === 1) + if (((messageData[4] >> 1) & 1) === 1) { this.setOctaveTuning(15 + channelOffset, newOctaveTuning); } - + // bit 2: channels 7 to 13 for (let i = 0; i < 7; i++) { const bit = (messageData[5] >> i) & 1; - if(bit === 1) + if (bit === 1) { this.setOctaveTuning(7 + i + channelOffset, newOctaveTuning); } } - + // bit 3: channels 0 to 16 for (let i = 0; i < 7; i++) { const bit = (messageData[6] >> i) & 1; - if(bit === 1) + if (bit === 1) { this.setOctaveTuning(i + channelOffset, newOctaveTuning); } } - - SpessaSynthInfo(`%cMIDI Octave Scale ${ - messageData[3] === 0x08 ? "(1 byte)" : "(2 bytes)" + + SpessaSynthInfo( + `%cMIDI Octave Scale ${ + messageData[3] === 0x08 ? "(1 byte)" : "(2 bytes)" } tuning via Tuning: %c${newOctaveTuning.join(" ")}`, consoleColors.info, - consoleColors.value); + consoleColors.value + ); break; - + default: SpessaSynthWarn( `%cUnrecognized MIDI Tuning standard message: %c${arrayToHexString(messageData)}`, consoleColors.warn, - consoleColors.unrecognized) + consoleColors.unrecognized + ); break; } break; - + default: SpessaSynthWarn( `%cUnrecognized MIDI Realtime/non realtime message: %c${arrayToHexString(messageData)}`, consoleColors.warn, - consoleColors.unrecognized) - + consoleColors.unrecognized + ); + } break; - + // this is a roland sysex // http://www.bandtrax.com.au/sysex.htm // https://cdn.roland.com/assets/media/pdf/AT-20R_30R_MI.pdf case 0x41: - function notRecognized() - { - // this is some other GS sysex... - SpessaSynthWarn(`%cUnrecognized Roland %cGS %cSysEx: %c${arrayToHexString(messageData)}`, - consoleColors.warn, - consoleColors.recognized, - consoleColors.warn, - consoleColors.unrecognized); - } - if(messageData[2] === 0x42 && messageData[3] === 0x12) + + function notRecognized() + { + // this is some other GS sysex... + SpessaSynthWarn( + `%cUnrecognized Roland %cGS %cSysEx: %c${arrayToHexString(messageData)}`, + consoleColors.warn, + consoleColors.recognized, + consoleColors.warn, + consoleColors.unrecognized + ); + } + + if (messageData[2] === 0x42 && messageData[3] === 0x12) { // this is a GS sysex // messageData[5] and [6] is the system parameter, messageData[7] is the value const messageValue = messageData[7]; - if(messageData[6] === 0x7F) + if (messageData[6] === 0x7F) { // GS mode set - if(messageValue === 0x00) + if (messageValue === 0x00) { // this is a GS reset SpessaSynthInfo("%cGS Reset received!", consoleColors.info); this.resetAllControllers(false); this.system = "gs"; } - else if(messageValue === 0x7F) + else if (messageValue === 0x7F) { // GS mode off SpessaSynthInfo("%cGS system off, switching to GM2", consoleColors.info); @@ -292,11 +313,10 @@ export function systemExclusive(messageData, channelOffset = 0) } return; } - else - if(messageData[4] === 0x40) + else if (messageData[4] === 0x40) { // this is a system parameter - if((messageData[5] & 0x10) > 0) + if ((messageData[5] & 0x10) > 0) { // this is an individual part (channel) parameter // determine the channel 0 means channel 10 (default), 1 means 1 etc. @@ -308,7 +328,7 @@ export function systemExclusive(messageData, channelOffset = 0) // this is some other GS sysex... notRecognized(); break; - + case 0x15: // this is the Use for Drum Part sysex (multiple drums) const isDrums = messageValue > 0 && messageData[5] >> 4; // if set to other than 0, is a drum channel @@ -323,43 +343,47 @@ export function systemExclusive(messageData, channelOffset = 0) consoleColors.value, consoleColors.recognized, consoleColors.info, - consoleColors.value); + consoleColors.value + ); return; - + case 0x16: // this is the pitch key shift sysex const keyShift = messageValue - 64; this.transposeChannel(channel, keyShift); - SpessaSynthInfo(`%cChannel %c${channel}%c pitch shift. Semitones %c${keyShift}%c, with %c${arrayToHexString(messageData)}`, + SpessaSynthInfo( + `%cChannel %c${channel}%c pitch shift. Semitones %c${keyShift}%c, with %c${arrayToHexString( + messageData)}`, consoleColors.info, consoleColors.recognized, consoleColors.info, consoleColors.value, consoleColors.info, - consoleColors.value); + consoleColors.value + ); return; - + // pan position case 0x1C: // 0 is random let panpot = messageValue; - if(panpot === 0) + if (panpot === 0) { - panpot = Math.floor(Math.random() * 128) + panpot = Math.floor(Math.random() * 128); } this.controllerChange(channel, midiControllers.pan, panpot); break; - + // chorus send case 0x21: this.controllerChange(channel, midiControllers.effects3Depth, messageValue); break; - + // reverb send case 0x22: this.controllerChange(channel, midiControllers.effects1Depth, messageValue); break; - + case 0x40: case 0x41: case 0x42: @@ -382,53 +406,63 @@ export function systemExclusive(messageData, channelOffset = 0) } this.setOctaveTuning(channel, newTuning); const cents = messageValue - 64; - SpessaSynthInfo(`%cChannel %c${channel}%c octave scale tuning. Cents %c${newTuning.join(" ")}%c, with %c${arrayToHexString(messageData)}`, + SpessaSynthInfo( + `%cChannel %c${channel}%c octave scale tuning. Cents %c${newTuning.join( + " ")}%c, with %c${arrayToHexString(messageData)}`, consoleColors.info, consoleColors.recognized, consoleColors.info, consoleColors.value, consoleColors.info, - consoleColors.value); + consoleColors.value + ); this.setChannelTuning(channel, cents); - break; + break; } return; } else - // this is a global system parameter - if(messageData[5] === 0x00 && messageData[6] === 0x06) + // this is a global system parameter + if (messageData[5] === 0x00 && messageData[6] === 0x06) { // roland master pan - SpessaSynthInfo(`%cRoland GS Master Pan set to: %c${messageValue}%c with: %c${arrayToHexString(messageData)}`, + SpessaSynthInfo( + `%cRoland GS Master Pan set to: %c${messageValue}%c with: %c${arrayToHexString( + messageData)}`, consoleColors.info, consoleColors.value, consoleColors.info, - consoleColors.value); + consoleColors.value + ); this.setMasterPan((messageValue - 64) / 64); return; } - else - if(messageData[5] === 0x00 && messageData[6] === 0x05) + else if (messageData[5] === 0x00 && messageData[6] === 0x05) { // roland master key shift (transpose) const transpose = messageValue - 64; - SpessaSynthInfo(`%cRoland GS Master Key-Shift set to: %c${transpose}%c with: %c${arrayToHexString(messageData)}`, + SpessaSynthInfo( + `%cRoland GS Master Key-Shift set to: %c${transpose}%c with: %c${arrayToHexString( + messageData)}`, consoleColors.info, consoleColors.value, consoleColors.info, - consoleColors.value); + consoleColors.value + ); this.setMasterTuning(transpose * 100); return; } - else - if(messageData[5] === 0x00 && messageData[6] === 0x04) + else if (messageData[5] === 0x00 && messageData[6] === 0x04) { // roland GS master volume - SpessaSynthInfo(`%cRoland GS Master Volume set to: %c${messageValue}%c with: %c${arrayToHexString(messageData)}`, + SpessaSynthInfo( + `%cRoland GS Master Volume set to: %c${messageValue}%c with: %c${arrayToHexString( + messageData)}`, consoleColors.info, consoleColors.value, consoleColors.info, - consoleColors.value); + consoleColors.value + ); this.setMIDIVolume(messageValue / 127); return; } @@ -437,35 +471,39 @@ export function systemExclusive(messageData, channelOffset = 0) notRecognized(); return; } - else - if(messageData[2] === 0x16 && messageData[3] === 0x12 && messageData[4] === 0x10) + else if (messageData[2] === 0x16 && messageData[3] === 0x12 && messageData[4] === 0x10) { // this is a roland master volume message this.setMIDIVolume(messageData[7] / 100); - SpessaSynthInfo(`%cRoland Master Volume control set to: %c${messageData[7]}%c via: %c${arrayToHexString(messageData)}`, + SpessaSynthInfo( + `%cRoland Master Volume control set to: %c${messageData[7]}%c via: %c${arrayToHexString( + messageData)}`, consoleColors.info, consoleColors.value, consoleColors.info, - consoleColors.value); + consoleColors.value + ); return; } else { // this is something else... - SpessaSynthWarn(`%cUnrecognized Roland SysEx: %c${arrayToHexString(messageData)}`, + SpessaSynthWarn( + `%cUnrecognized Roland SysEx: %c${arrayToHexString(messageData)}`, consoleColors.warn, - consoleColors.unrecognized); + consoleColors.unrecognized + ); return; } - + // yamaha // http://www.studio4all.de/htmle/main91.html case 0x43: // XG sysex - if(messageData[2] === 0x4C) + if (messageData[2] === 0x4C) { // XG system parameter - if(messageData[3] === 0x00 && messageData[4] === 0x00) + if (messageData[3] === 0x00 && messageData[4] === 0x00) { switch (messageData[5]) { @@ -473,20 +511,24 @@ export function systemExclusive(messageData, channelOffset = 0) case 0x04: const vol = messageData[6]; this.setMIDIVolume(vol / 127); - SpessaSynthInfo(`%cXG master volume. Volume: %c${vol}`, + SpessaSynthInfo( + `%cXG master volume. Volume: %c${vol}`, consoleColors.info, - consoleColors.recognized); + consoleColors.recognized + ); break; - + // master transpose case 0x06: const transpose = messageData[6] - 64; this.transposeAllChannels(transpose); - SpessaSynthInfo(`%cXG master transpose. Volume: %c${transpose}`, + SpessaSynthInfo( + `%cXG master transpose. Volume: %c${transpose}`, consoleColors.info, - consoleColors.recognized); + consoleColors.recognized + ); break; - + // XG on case 0x7E: SpessaSynthInfo("%cXG system on", consoleColors.info); @@ -496,15 +538,15 @@ export function systemExclusive(messageData, channelOffset = 0) } } else - // XG part parameter - if(messageData[3] === 0x08) + // XG part parameter + if (messageData[3] === 0x08) { - if(this.system !== "xg") + if (this.system !== "xg") { return; } const channel = messageData[4] + channelOffset; - if(channel >= this.workletProcessorChannels.length) + if (channel >= this.workletProcessorChannels.length) { // invalid channel return; @@ -516,80 +558,86 @@ export function systemExclusive(messageData, channelOffset = 0) case 0x01: this.controllerChange(channel, midiControllers.bankSelect, value); break; - + // bank select lsb case 0x02: this.controllerChange(channel, midiControllers.lsbForControl0BankSelect, value); break; - + // program change case 0x03: this.programChange(channel, value); break; - + // note shift case 0x08: const chan = this.workletProcessorChannels[channel]; - if(chan.drumChannel) + if (chan.drumChannel) { return; } const semitones = value - 64; chan.channelTransposeKeyShift = semitones; break; - + // volume case 0x0B: this.controllerChange(channel, midiControllers.mainVolume, value); break; - + // panpot case 0x0E: let pan = value; - if(pan === 0) + if (pan === 0) { // 0 means random pan = Math.floor(Math.random() * 127); } this.controllerChange(channel, midiControllers.pan, pan); break; - + // reverb case 0x13: this.controllerChange(channel, midiControllers.effects1Depth, value); break; - + // chorus case 0x12: this.controllerChange(channel, midiControllers.effects3Depth, value); break; - + default: - SpessaSynthWarn(`%cUnrecognized Yamaha XG Part Setup: %c${messageData[5].toString(16).toUpperCase()}`, + SpessaSynthWarn( + `%cUnrecognized Yamaha XG Part Setup: %c${messageData[5].toString(16) + .toUpperCase()}`, consoleColors.warn, - consoleColors.unrecognized); + consoleColors.unrecognized + ); } } - else - if(this.system === "xg") + else if (this.system === "xg") { - SpessaSynthWarn(`%cUnrecognized Yamaha XG SysEx: %c${arrayToHexString(messageData)}`, + SpessaSynthWarn( + `%cUnrecognized Yamaha XG SysEx: %c${arrayToHexString(messageData)}`, consoleColors.warn, - consoleColors.unrecognized); + consoleColors.unrecognized + ); } - + } else { - if(this.system === "xg") + if (this.system === "xg") { - SpessaSynthWarn(`%cUnrecognized Yamaha SysEx: %c${arrayToHexString(messageData)}`, + SpessaSynthWarn( + `%cUnrecognized Yamaha SysEx: %c${arrayToHexString(messageData)}`, consoleColors.warn, - consoleColors.unrecognized); + consoleColors.unrecognized + ); } } break; - - + + } } \ No newline at end of file diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control.js index 8491235f..daa5997f 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control.js @@ -1,7 +1,7 @@ -import { customControllers, NON_CC_INDEX_OFFSET } from '../worklet_utilities/worklet_processor_channel.js' -import { consoleColors } from '../../../utils/other.js' -import { computeModulators } from '../worklet_utilities/worklet_modulator.js' -import { SpessaSynthInfo } from '../../../utils/loggin.js' +import { customControllers, NON_CC_INDEX_OFFSET } from "../worklet_utilities/worklet_processor_channel.js"; +import { consoleColors } from "../../../utils/other.js"; +import { computeModulators } from "../worklet_utilities/worklet_modulator.js"; +import { SpessaSynthInfo } from "../../../utils/loggin.js"; import { modulatorSources } from "../../../soundfont/basic_soundfont/modulator.js"; /** @@ -27,23 +27,23 @@ export function transposeAllChannels(semitones, force = false) * @param semitones {number} Can be float * @param force {boolean} defaults to false, if true transposes the channel even if it's a drum channel */ -export function transposeChannel(channel, semitones, force=false) +export function transposeChannel(channel, semitones, force = false) { const channelObject = this.workletProcessorChannels[channel]; - if(!channelObject.drumChannel) + if (!channelObject.drumChannel) { semitones += this.transposition; } const keyShift = Math.trunc(semitones); const currentTranspose = channelObject.channelTransposeKeyShift + channelObject.customControllers[customControllers.channelTransposeFine] / 100; - if( + if ( (channelObject.drumChannel && !force) || semitones === currentTranspose ) { return; } - if(keyShift !== channelObject.channelTransposeKeyShift) + if (keyShift !== channelObject.channelTransposeKeyShift) { this.stopAll(channel, false); } @@ -64,13 +64,15 @@ export function setChannelTuning(channel, cents, log = true) const channelObject = this.workletProcessorChannels[channel]; cents = Math.round(cents); channelObject.customControllers[customControllers.channelTuning] = cents; - if(!log) + if (!log) { return; } - SpessaSynthInfo(`%cChannel ${channel} fine tuning. Cents: %c${cents}`, + SpessaSynthInfo( + `%cChannel ${channel} fine tuning. Cents: %c${cents}`, consoleColors.info, - consoleColors.value); + consoleColors.value + ); } /** @@ -84,9 +86,11 @@ export function setChannelTuningSemitones(channel, semitones) const channelObject = this.workletProcessorChannels[channel]; semitones = Math.round(semitones); channelObject.customControllers[customControllers.channelTuningSemitones] = semitones; - SpessaSynthInfo(`%cChannel ${channel} coarse tuning. Semitones: %c${semitones}`, + SpessaSynthInfo( + `%cChannel ${channel} coarse tuning. Semitones: %c${semitones}`, consoleColors.info, - consoleColors.value); + consoleColors.value + ); } /** @@ -97,7 +101,8 @@ export function setChannelTuningSemitones(channel, semitones) export function setMasterTuning(cents) { cents = Math.round(cents); - for (let i = 0; i < this.workletProcessorChannels.length; i++) { + for (let i = 0; i < this.workletProcessorChannels.length; i++) + { this.workletProcessorChannels[i].customControllers[customControllers.masterTuning] = cents; } } @@ -111,9 +116,11 @@ export function setModulationDepth(channel, cents) { let channelObject = this.workletProcessorChannels[channel]; cents = Math.round(cents); - SpessaSynthInfo(`%cChannel ${channel} modulation depth. Cents: %c${cents}`, + SpessaSynthInfo( + `%cChannel ${channel} modulation depth. Cents: %c${cents}`, consoleColors.info, - consoleColors.value); + consoleColors.value + ); /* ============== IMPORTANT here we convert cents into a multiplier. @@ -135,7 +142,7 @@ export function setModulationDepth(channel, cents) */ export function pitchWheel(channel, MSB, LSB) { - if(this.workletProcessorChannels[channel].lockedControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel]) + if (this.workletProcessorChannels[channel].lockedControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel]) { return; } @@ -148,7 +155,12 @@ export function pitchWheel(channel, MSB, LSB) this.workletProcessorChannels[channel].midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel] = bend; this.workletProcessorChannels[channel].voices.forEach(v => // compute pitch modulators - computeModulators(v, this.workletProcessorChannels[channel].midiControllers, 0, modulatorSources.pitchWheel)); + computeModulators( + v, + this.workletProcessorChannels[channel].midiControllers, + 0, + modulatorSources.pitchWheel + )); this.sendChannelProperties(); } @@ -163,8 +175,13 @@ export function channelPressure(channel, pressure) const channelObject = this.workletProcessorChannels[channel]; channelObject.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.channelPressure] = pressure << 7; this.workletProcessorChannels[channel].voices.forEach(v => - computeModulators(v, channelObject.midiControllers, 0, modulatorSources.channelPressure)); - this.callEvent("channelpressure",{ + computeModulators( + v, + channelObject.midiControllers, + 0, + modulatorSources.channelPressure + )); + this.callEvent("channelpressure", { channel: channel, pressure: pressure }); @@ -179,13 +196,19 @@ export function channelPressure(channel, pressure) */ export function polyPressure(channel, midiNote, pressure) { - this.workletProcessorChannels[channel].voices.forEach(v => { - if(v.midiNote !== midiNote) + this.workletProcessorChannels[channel].voices.forEach(v => + { + if (v.midiNote !== midiNote) { return; } v.pressure = pressure; - computeModulators(v, this.workletProcessorChannels[channel].midiControllers, 0, modulatorSources.polyPressure); + computeModulators( + v, + this.workletProcessorChannels[channel].midiControllers, + 0, + modulatorSources.polyPressure + ); }); this.callEvent("polypressure", { channel: channel, @@ -202,7 +225,7 @@ export function polyPressure(channel, midiNote, pressure) */ export function setOctaveTuning(channel, tuning) { - if(tuning.length !== 12) + if (tuning.length !== 12) { throw new Error("Tuning is not the length of 12."); } diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/vibrato_control.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/vibrato_control.js index 66dcbc43..54fbca1b 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/vibrato_control.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/vibrato_control.js @@ -19,7 +19,7 @@ export function disableAndLockGSNRPN(channel) */ export function setVibrato(channel, depth, rate, delay) { - if(this.workletProcessorChannels[channel].lockGSNRPNParams) + if (this.workletProcessorChannels[channel].lockGSNRPNParams) { return; } diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/voice_control.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/voice_control.js index de036ba6..b9cbabbd 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/voice_control.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/voice_control.js @@ -1,22 +1,23 @@ -import { absCentsToHz, timecentsToSeconds } from '../worklet_utilities/unit_converter.js' -import { getLFOValue } from '../worklet_utilities/lfo.js' -import { customControllers } from '../worklet_utilities/worklet_processor_channel.js' -import { WorkletModulationEnvelope } from '../worklet_utilities/modulation_envelope.js' +import { absCentsToHz, timecentsToSeconds } from "../worklet_utilities/unit_converter.js"; +import { getLFOValue } from "../worklet_utilities/lfo.js"; +import { customControllers } from "../worklet_utilities/worklet_processor_channel.js"; +import { WorkletModulationEnvelope } from "../worklet_utilities/modulation_envelope.js"; import { getSampleCubic, getSampleLinear, getSampleNearest, - interpolationTypes, -} from '../worklet_utilities/wavetable_oscillator.js' -import { panVoice } from '../worklet_utilities/stereo_panner.js' -import { WorkletLowpassFilter } from '../worklet_utilities/lowpass_filter.js' -import { MIN_NOTE_LENGTH } from '../main_processor.js' -import { WorkletVolumeEnvelope } from '../worklet_utilities/volume_envelope.js' + interpolationTypes +} from "../worklet_utilities/wavetable_oscillator.js"; +import { panVoice } from "../worklet_utilities/stereo_panner.js"; +import { WorkletLowpassFilter } from "../worklet_utilities/lowpass_filter.js"; +import { MIN_NOTE_LENGTH } from "../main_processor.js"; +import { WorkletVolumeEnvelope } from "../worklet_utilities/volume_envelope.js"; import { generatorTypes } from "../../../soundfont/basic_soundfont/generator.js"; const HALF_PI = Math.PI / 2; export const PAN_SMOOTHING_FACTOR = 0.05; + /** * Renders a voice to the stereo output buffer * @param channel {WorkletProcessorChannel} the voice's channel @@ -36,7 +37,7 @@ export function renderVoice( ) { // check if release - if(!voice.isInRelease) + if (!voice.isInRelease) { // if not in release, check if the release time is if (currentTime >= voice.releaseStartTime) @@ -45,27 +46,27 @@ export function renderVoice( voice.isInRelease = true; WorkletVolumeEnvelope.startRelease(voice); WorkletModulationEnvelope.startRelease(voice); - if(voice.sample.loopingMode === 3) + if (voice.sample.loopingMode === 3) { voice.sample.isLooping = false; } } } - - + + // if the initial attenuation is more than 100dB, skip the voice (it's silent anyways) - if(voice.modulatedGenerators[generatorTypes.initialAttenuation] > 2500) + if (voice.modulatedGenerators[generatorTypes.initialAttenuation] > 2500) { - if(voice.isInRelease) + if (voice.isInRelease) { voice.finished = true; } return; } - + // TUNING let targetKey = voice.targetKey; - + // calculate tuning let cents = voice.modulatedGenerators[generatorTypes.fineTune] // soundfont fine tune + channel.customControllers[customControllers.channelTuning] // RPN channel fine tuning @@ -75,23 +76,23 @@ export function renderVoice( + channel.keyCentTuning[voice.midiNote]; // SysEx key tuning let semitones = voice.modulatedGenerators[generatorTypes.coarseTune] // soundfont coarse tuning + channel.customControllers[customControllers.channelTuningSemitones]; // RPN channel coarse tuning - + // midi tuning standard const tuning = this.tunings[channel.preset.program]?.[targetKey]; - if(tuning?.midiNote >= 0) + if (tuning?.midiNote >= 0) { // override key targetKey = tuning.midiNote; // add microtonal tuning cents += tuning.centTuning; } - + // calculate tuning by key using soundfont's scale tuning cents += (targetKey - voice.sample.rootKey) * voice.modulatedGenerators[generatorTypes.scaleTuning]; - + // vibrato LFO const vibratoDepth = voice.modulatedGenerators[generatorTypes.vibLfoToPitch]; - if(vibratoDepth !== 0) + if (vibratoDepth !== 0) { // calculate start time and lfo value const vibStart = voice.startTime + timecentsToSeconds(voice.modulatedGenerators[generatorTypes.delayVibLFO]); @@ -100,16 +101,16 @@ export function renderVoice( // use modulation multiplier (RPN modulation depth) cents += lfoVal * (vibratoDepth * channel.customControllers[customControllers.modulationMultiplier]); } - + // lowpass frequency let lowpassCents = voice.modulatedGenerators[generatorTypes.initialFilterFc]; - + // mod LFO const modPitchDepth = voice.modulatedGenerators[generatorTypes.modLfoToPitch]; const modVolDepth = voice.modulatedGenerators[generatorTypes.modLfoToVolume]; const modFilterDepth = voice.modulatedGenerators[generatorTypes.modLfoToFilterFc]; let modLfoCentibels = 0; - if(modPitchDepth + modFilterDepth + modVolDepth !== 0) + if (modPitchDepth + modFilterDepth + modVolDepth !== 0) { // calculate start time and lfo value const modStart = voice.startTime + timecentsToSeconds(voice.modulatedGenerators[generatorTypes.delayModLFO]); @@ -123,18 +124,22 @@ export function renderVoice( // lowpass frequency lowpassCents += modLfoValue * modFilterDepth; } - + // channel vibrato (GS NRPN) - if(channel.channelVibrato.depth > 0) + if (channel.channelVibrato.depth > 0) { // same as others - const channelVibrato = getLFOValue(voice.startTime + channel.channelVibrato.delay, channel.channelVibrato.rate, currentTime); - if(channelVibrato) + const channelVibrato = getLFOValue( + voice.startTime + channel.channelVibrato.delay, + channel.channelVibrato.rate, + currentTime + ); + if (channelVibrato) { cents += channelVibrato * channel.channelVibrato.depth; } } - + // mod env const modEnvPitchDepth = voice.modulatedGenerators[generatorTypes.modEnvToPitch]; const modEnvFilterDepth = voice.modulatedGenerators[generatorTypes.modEnvToFilterFc]; @@ -142,47 +147,47 @@ export function renderVoice( // apply values lowpassCents += modEnv * modEnvFilterDepth; cents += modEnv * modEnvPitchDepth; - + // finally calculate the playback rate const centsTotal = ~~(cents + semitones * 100); - if(centsTotal !== voice.currentTuningCents) + if (centsTotal !== voice.currentTuningCents) { voice.currentTuningCents = centsTotal; voice.currentTuningCalculated = Math.pow(2, centsTotal / 1200); } - + // PANNING - const pan = ((Math.max(-500, Math.min(500, voice.modulatedGenerators[generatorTypes.pan] )) + 500) / 1000) ; // 0 to 1 - + const pan = ((Math.max(-500, Math.min(500, voice.modulatedGenerators[generatorTypes.pan])) + 500) / 1000); // 0 to 1 + // SYNTHESIS const bufferOut = new Float32Array(outputLeft.length); - + // wavetable oscillator - switch(this.interpolationType) + switch (this.interpolationType) { case interpolationTypes.linear: default: getSampleLinear(voice, bufferOut); break; - + case interpolationTypes.nearestNeighbor: getSampleNearest(voice, bufferOut); break; - + case interpolationTypes.fourthOrder: getSampleCubic(voice, bufferOut); } - + // lowpass filter WorkletLowpassFilter.apply(voice, bufferOut, lowpassCents); - + // volenv WorkletVolumeEnvelope.apply(voice, bufferOut, modLfoCentibels, this.volumeEnvelopeSmoothingFactor); - + // pan the voice and write out voice.currentPan += (pan - voice.currentPan) * this.panSmoothingFactor; // smooth out pan to prevent clicking const panLeft = Math.cos(HALF_PI * voice.currentPan) * this.panLeft; - const panRight = Math.sin(HALF_PI * voice.currentPan) * this.panRight; + const panRight = Math.sin(HALF_PI * voice.currentPan) * this.panRight; // disable reverb and chorus in one output mode const reverb = this.oneOutputMode ? 0 : voice.modulatedGenerators[generatorTypes.reverbEffectsSend]; const chorus = this.oneOutputMode ? 0 : voice.modulatedGenerators[generatorTypes.chorusEffectsSend]; @@ -192,7 +197,8 @@ export function renderVoice( bufferOut, outputLeft, outputRight, reverbOutput, reverb, - chorusOutput, chorus); + chorusOutput, chorus + ); } @@ -204,12 +210,12 @@ export function renderVoice( function getPriority(channel, voice) { let priority = 0; - if(channel.drumChannel) + if (channel.drumChannel) { // important priority += 5; } - if(voice.isInRelease) + if (voice.isInRelease) { // not important priority -= 5; @@ -218,7 +224,7 @@ function getPriority(channel, voice) priority += voice.velocity / 25; // map to 0-5 // the newer, more important priority -= voice.volumeEnvelope.state; - if(voice.isInRelease) + if (voice.isInRelease) { priority -= 5; } @@ -244,11 +250,11 @@ export function voiceKilling(amount) } } } - + // Step 2: Sort voices by priority (ascending order) allVoices.sort((a, b) => a.priority - b.priority); const voicesToRemove = allVoices.slice(0, amount); - + for (const { channel, voice } of voicesToRemove) { const index = channel.voices.indexOf(voice); @@ -268,7 +274,7 @@ export function releaseVoice(voice) { voice.releaseStartTime = currentTime; // check if the note is shorter than the min note time, if so, extend it - if(voice.releaseStartTime - voice.startTime < MIN_NOTE_LENGTH) + if (voice.releaseStartTime - voice.startTime < MIN_NOTE_LENGTH) { voice.releaseStartTime = voice.startTime + MIN_NOTE_LENGTH; } diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/worklet_soundfont_manager/sfman_message.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/worklet_soundfont_manager/sfman_message.js index 8a75fb0e..18219058 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/worklet_soundfont_manager/sfman_message.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/worklet_soundfont_manager/sfman_message.js @@ -5,5 +5,5 @@ export const WorkletSoundfontManagerMessageType = { reloadSoundFont: 0, // buffer addNewSoundFont: 2, // [buffer, id, bankOffset] deleteSoundFont: 3, // id - rearrangeSoundFonts: 4, // newOrder // where string is the id -} \ No newline at end of file + rearrangeSoundFonts: 4 // newOrder // where string is the id +}; \ No newline at end of file diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/worklet_soundfont_manager/worklet_soundfont_manager.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/worklet_soundfont_manager/worklet_soundfont_manager.js index 415d893f..ce1fc4d6 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/worklet_soundfont_manager/worklet_soundfont_manager.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_methods/worklet_soundfont_manager/worklet_soundfont_manager.js @@ -1,6 +1,6 @@ -import { SpessaSynthWarn } from '../../../../utils/loggin.js' -import { WorkletSoundfontManagerMessageType } from './sfman_message.js' -import { loadSoundFont } from '../../../../soundfont/load_soundfont.js' +import { SpessaSynthWarn } from "../../../../utils/loggin.js"; +import { WorkletSoundfontManagerMessageType } from "./sfman_message.js"; +import { loadSoundFont } from "../../../../soundfont/load_soundfont.js"; /** * @typedef {Object} SoundFontType @@ -25,17 +25,18 @@ export class WorkletSoundfontManager this.totalSoundfontOffset = 0; this.reloadManager(initialSoundFontBuffer); } - + _assingSampleOffsets() { let offset = 0; - this.soundfontList.forEach(s => { + this.soundfontList.forEach(s => + { s.soundfont.setSampleIDOffset(offset); - offset += s.soundfont.samples.length + offset += s.soundfont.samples.length; }); this.totalSoundfontOffset = offset; } - + generatePresetList() { this._assingSampleOffsets(); @@ -54,10 +55,10 @@ export class WorkletSoundfontManager * @type {Set} */ const presets = new Set(); - for(const p of font.soundfont.presets) + for (const p of font.soundfont.presets) { const presetString = `${p.bank + font.bankOffset}-${p.program}`; - if(presets.has(presetString)) + if (presets.has(presetString)) { continue; } @@ -65,22 +66,22 @@ export class WorkletSoundfontManager presetList[presetString] = p.presetName; } } - + /** * @type {{bank: number, presetName: string, program: number}[]} */ this.presetList = []; - for(const [string, name] of Object.entries(presetList)) + for (const [string, name] of Object.entries(presetList)) { const pb = string.split("-"); this.presetList.push({ presetName: name, program: parseInt(pb[1]), bank: parseInt(pb[0]) - }) + }); } } - + /** * @param type {WorkletSoundfontManagerMessageType} * @param data {any} @@ -92,20 +93,20 @@ export class WorkletSoundfontManager case WorkletSoundfontManagerMessageType.addNewSoundFont: this.addNewSoundFont(data[0], data[1], data[2]); break; - + case WorkletSoundfontManagerMessageType.reloadSoundFont: this.reloadManager(data); break; - + case WorkletSoundfontManagerMessageType.deleteSoundFont: this.deleteSoundFont(data); break; - + case WorkletSoundfontManagerMessageType.rearrangeSoundFonts: this.rearrangeSoundFonts(data); } } - + /** * Get the final preset list * @returns {{bank: number, presetName: string, program: number}[]} @@ -114,14 +115,14 @@ export class WorkletSoundfontManager { return this.presetList.slice(); } - + /** * Clears all soundfonts and adds a new one * @param soundFontArrayBuffer {ArrayBuffer} */ reloadManager(soundFontArrayBuffer) { - const font = loadSoundFont(soundFontArrayBuffer); + const font = loadSoundFont(soundFontArrayBuffer); /** * All the soundfonts, ordered from the most important to the least. * @type {SoundFontType[]} @@ -135,16 +136,16 @@ export class WorkletSoundfontManager this.generatePresetList(); this.ready(); } - + deleteSoundFont(id) { - if(this.soundfontList.length === 0) + if (this.soundfontList.length === 0) { SpessaSynthWarn("1 soundfont left. Aborting!"); return; } const index = this.soundfontList.findIndex(s => s.id === id); - if(index === -1) + if (index === -1) { SpessaSynthWarn(`No soundfont with id of "${id}" found. Aborting!`); return; @@ -155,7 +156,7 @@ export class WorkletSoundfontManager this.soundfontList.splice(index, 1); this.generatePresetList(); } - + /** * Adds a new soundfont buffer with a given ID * @param buffer {ArrayBuffer} @@ -164,7 +165,7 @@ export class WorkletSoundfontManager */ addNewSoundFont(buffer, id, bankOffset) { - if(this.soundfontList.find(s => s.id === id) !== undefined) + if (this.soundfontList.find(s => s.id === id) !== undefined) { throw new Error("Cannot overwrite the existing soundfont. Use soundfontManager.delete(id) instead."); } @@ -176,7 +177,7 @@ export class WorkletSoundfontManager this.generatePresetList(); this.ready(); } - + /** * Rearranges the soundfonts * @param newList {string[]} the order of soundfonts, a list of strings, first overwrites second @@ -188,7 +189,7 @@ export class WorkletSoundfontManager ); this.generatePresetList(); } - + /** * Gets a given preset from the soundfont stack * @param bankNumber {number} @@ -197,27 +198,27 @@ export class WorkletSoundfontManager */ getPreset(bankNumber, programNumber) { - if(this.soundfontList.length < 1) + if (this.soundfontList.length < 1) { throw new Error("No soundfonts! This should never happen."); } - for(const sf of this.soundfontList) + for (const sf of this.soundfontList) { // check for the preset (with given offset) const preset = sf.soundfont.getPresetNoFallback(bankNumber - sf.bankOffset, programNumber); - if(preset !== undefined) + if (preset !== undefined) { return preset; } // if not found, advance to the next soundfont } // if none found, return the first correct preset found - if(bankNumber !== 128) + if (bankNumber !== 128) { - for(const sf of this.soundfontList) + for (const sf of this.soundfontList) { const preset = sf.soundfont.presets.find(p => p.program === programNumber); - if(preset) + if (preset) { return preset; } @@ -227,10 +228,10 @@ export class WorkletSoundfontManager } else { - for(const sf of this.soundfontList) + for (const sf of this.soundfontList) { const preset = sf.soundfont.presets.find(p => p.bank === 128); - if(preset) + if (preset) { return preset; } diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_processor.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_processor.js index 450247fd..41efa1c0 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_processor.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_processor.js @@ -1,7 +1,7 @@ -import { WORKLET_PROCESSOR_NAME } from '../synthetizer.js' -import { consoleColors } from '../../utils/other.js' -import { SpessaSynthProcessor } from './main_processor.js' -import { SpessaSynthInfo } from '../../utils/loggin.js' +import { WORKLET_PROCESSOR_NAME } from "../synthetizer.js"; +import { consoleColors } from "../../utils/other.js"; +import { SpessaSynthProcessor } from "./main_processor.js"; +import { SpessaSynthInfo } from "../../utils/loggin.js"; // noinspection JSUnresolvedReference diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/lfo.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/lfo.js index df1b97fd..c4405ac0 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/lfo.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/lfo.js @@ -10,14 +10,16 @@ * @param currentTime {number} seconds * @return {number} the value from -1 to 1 */ -export function getLFOValue(startTime, frequency, currentTime) { - if (currentTime < startTime) { +export function getLFOValue(startTime, frequency, currentTime) +{ + if (currentTime < startTime) + { return 0; } - + const xVal = (currentTime - startTime) / (1 / frequency) + 0.25; // offset by -0.25, otherwise we start at -1 and can have unexpected jump in pitch or lowpass (happened with Synth Strings 2) - + // triangle, not sine return Math.abs(xVal - (~~(xVal + 0.5))) * 4 - 1; } diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js index e4dafe71..bb61e676 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js @@ -1,4 +1,4 @@ -import { absCentsToHz, decibelAttenuationToGain } from './unit_converter.js' +import { absCentsToHz, decibelAttenuationToGain } from "./unit_converter.js"; import { generatorTypes } from "../../../soundfont/basic_soundfont/generator.js"; /** @@ -16,79 +16,79 @@ export class WorkletLowpassFilter * @type {number} */ a0 = 0; - + /** * Filter coefficient 2 * @type {number} */ a1 = 0; - + /** * Filter coefficient 3 * @type {number} */ a2 = 0; - + /** * Filter coefficient 4 * @type {number} */ a3 = 0; - + /** * Filter coefficient 5 * @type {number} */ a4 = 0; - + /** * Input history 1 * @type {number} */ x1 = 0; - + /** * Input history 2 * @type {number} */ x2 = 0; - + /** * Output history 1 * @type {number} */ y1 = 0; - + /** * Output history 2 * @type {number} */ y2 = 0; - + /** * Resonance in centibels * @type {number} */ reasonanceCb = 0; - + /** * Resonance gain * @type {number} */ reasonanceGain = 1; - + /** * Cutoff frequency in cents * @type {number} */ cutoffCents = 13500; - + /** * Cutoff frequency in Hz * @type {number} */ cutoffHz = 20000; - + /** * Applies a low-pass filter to the given buffer * @param voice {WorkletVoice} the voice we're working on @@ -97,20 +97,20 @@ export class WorkletLowpassFilter */ static apply(voice, outputBuffer, cutoffCents) { - if(cutoffCents > 13499 && voice.filter.reasonanceCb === 0) + if (cutoffCents > 13499 && voice.filter.reasonanceCb === 0) { return; // filter is open } - - const filter = voice.filter + + const filter = voice.filter; // check if the frequency has changed. if so, calculate new coefficients - if(filter.cutoffCents !== cutoffCents || filter.reasonanceCb !== voice.modulatedGenerators[generatorTypes.initialFilterQ]) + if (filter.cutoffCents !== cutoffCents || filter.reasonanceCb !== voice.modulatedGenerators[generatorTypes.initialFilterQ]) { filter.cutoffCents = cutoffCents; filter.reasonanceCb = voice.modulatedGenerators[generatorTypes.initialFilterQ]; WorkletLowpassFilter.calculateCoefficients(voice); } - + // filter the input for (let i = 0; i < outputBuffer.length; i++) { @@ -120,17 +120,17 @@ export class WorkletLowpassFilter + filter.a2 * filter.x2 - filter.a3 * filter.y1 - filter.a4 * filter.y2; - + // set buffer filter.x2 = filter.x1; filter.x1 = input; filter.y2 = filter.y1; filter.y1 = filtered; - + outputBuffer[i] = filtered; } } - + /** * @param voice {WorkletVoice} */ @@ -138,31 +138,31 @@ export class WorkletLowpassFilter { const filter = voice.filter; filter.cutoffHz = absCentsToHz(filter.cutoffCents); - + // fix cutoff on low frequencies (fluid_iir_filter.c line 392) filter.cutoffHz = Math.min(filter.cutoffHz, 0.45 * sampleRate); - + // adjust the filterQ (fluid_iir_filter.c line 204) const qDb = (filter.reasonanceCb / 10) - 3.01; filter.reasonanceGain = decibelAttenuationToGain(-1 * qDb); // -1 because it's attenuation and we don't want attenuation - + // reduce the gain by the Q factor (fluid_iir_filter.c line 250) const qGain = 1 / Math.sqrt(filter.reasonanceGain); - - + + // code is ported from https://github.com/sinshu/meltysynth/ to work with js. // I'm too dumb to understand the math behind this... let w = 2 * Math.PI * filter.cutoffHz / sampleRate; // we're in the audioworkletglobalscope so we can use sampleRate let cosw = Math.cos(w); let alpha = Math.sin(w) / (2 * filter.reasonanceGain); - + let b1 = (1 - cosw) * qGain; let b0 = b1 / 2; let b2 = b0; let a0 = 1 + alpha; let a1 = -2 * cosw; let a2 = 1 - alpha; - + // set coefficients filter.a0 = b0 / a0; filter.a1 = b1 / a0; diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js index d9fa9900..55737494 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js @@ -1,5 +1,5 @@ -import { timecentsToSeconds } from './unit_converter.js' -import { getModulatorCurveValue } from './modulator_curves.js' +import { timecentsToSeconds } from "./unit_converter.js"; +import { getModulatorCurveValue } from "./modulator_curves.js"; import { generatorTypes } from "../../../soundfont/basic_soundfont/generator.js"; import { modulatorCurveTypes } from "../../../soundfont/basic_soundfont/modulator.js"; @@ -11,7 +11,8 @@ const MODENV_PEAK = 1; // 1000 should be precise enough const CONVEX_ATTACK = new Float32Array(1000); -for (let i = 0; i < CONVEX_ATTACK.length; i++) { +for (let i = 0; i < CONVEX_ATTACK.length; i++) +{ // this makes the db linear ( i think CONVEX_ATTACK[i] = getModulatorCurveValue(0, modulatorCurveTypes.convex, i / 1000, 0); } @@ -28,25 +29,25 @@ export class WorkletModulationEnvelope * @type {number} */ decayDuration = 0; - + /** * The hold duration, in seconds * @type {number} */ holdDuration = 0; - + /** * Release duration, in seconds * @type {number} */ releaseDuration = 0; - + /** * The sustain level 0-1 * @type {number} */ sustainLevel = 0; - + /** * Delay phase end time in seconds, absolute (audio context time) * @type {number} @@ -67,19 +68,19 @@ export class WorkletModulationEnvelope * @type {number} */ decayEnd = 0; - + /** * The level of the envelope when the release phase starts * @type {number} */ releaseStartLevel = 0; - + /** * The current modulation envelope value * @type {number} */ currentValue = 0; - + /** * Starts the release phase in the envelope * @param voice {WorkletVoice} the voice this envelope belongs to @@ -89,39 +90,39 @@ export class WorkletModulationEnvelope voice.modulationEnvelope.releaseStartLevel = voice.modulationEnvelope.currentValue; WorkletModulationEnvelope.recalculate(voice); } - + /** * @param voice {WorkletVoice} the voice to recalculate */ static recalculate(voice) { const env = voice.modulationEnvelope; - + env.sustainLevel = 1 - (voice.modulatedGenerators[generatorTypes.sustainModEnv] / 1000); - + env.attackDuration = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.attackModEnv]); - + const decayKeyExcursionCents = ((60 - voice.midiNote) * voice.modulatedGenerators[generatorTypes.keyNumToModEnvDecay]); const decayTime = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.decayModEnv] + decayKeyExcursionCents); // according to the specification, the decay time is the time it takes to reach 0% from 100%. // calculate the time to reach actual sustain level // for example, sustain 0.6 will be 0.4 of the decay time env.decayDuration = decayTime * (1 - env.sustainLevel); - + const holdKeyExcursionCents = ((60 - voice.midiNote) * voice.modulatedGenerators[generatorTypes.keyNumToModEnvHold]); env.holdDuration = timecentsToSeconds(holdKeyExcursionCents + voice.modulatedGenerators[generatorTypes.holdModEnv]); - + const releaseTime = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.releaseModEnv]); // release time is from the full level to 0% // to get the actual time, multiply by the release start level env.releaseDuration = releaseTime * env.releaseStartLevel; - + env.delayEnd = voice.startTime + timecentsToSeconds(voice.modulatedGenerators[generatorTypes.delayModEnv]); env.attackEnd = env.delayEnd + env.attackDuration; env.holdEnd = env.attackEnd + env.holdDuration; env.decayEnd = env.holdEnd + env.decayDuration; } - + /** * Calculates the current modulation envelope value for the given time and voice * @param voice {WorkletVoice} the voice we're working on @@ -131,31 +132,34 @@ export class WorkletModulationEnvelope static getValue(voice, currentTime) { const env = voice.modulationEnvelope; - if(voice.isInRelease) + if (voice.isInRelease) { - if(env.releaseDuration < 0.001) + if (env.releaseDuration < 0.001) { // prevent lowpass bugs if release is instant return env.releaseStartLevel; } - return Math.max(0, (1 - (currentTime - voice.releaseStartTime) / env.releaseDuration) * env.releaseStartLevel); + return Math.max( + 0, + (1 - (currentTime - voice.releaseStartTime) / env.releaseDuration) * env.releaseStartLevel + ); } - - if(currentTime < env.delayEnd) + + if (currentTime < env.delayEnd) { env.currentValue = 0; // delay } - else if(currentTime < env.attackEnd) + else if (currentTime < env.attackEnd) { // modulation envelope uses convex curve for attack env.currentValue = CONVEX_ATTACK[~~((1 - (env.attackEnd - currentTime) / env.attackDuration) * 1000)]; } - else if(currentTime < env.holdEnd) + else if (currentTime < env.holdEnd) { // hold: stay at 1 env.currentValue = MODENV_PEAK; } - else if(currentTime < env.decayEnd) + else if (currentTime < env.decayEnd) { // decay: linear ramp from 1 to sustain level env.currentValue = (1 - (env.decayEnd - currentTime) / env.decayDuration) * (env.sustainLevel - MODENV_PEAK) + MODENV_PEAK; diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/modulator_curves.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/modulator_curves.js index 09322684..ffb86d3f 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/modulator_curves.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/modulator_curves.js @@ -19,7 +19,7 @@ concave[MOD_PRECOMPUTED_LENGTH - 1] = 1; convex[0] = 0; convex[MOD_PRECOMPUTED_LENGTH - 1] = 1; -for(let i = 1; i < MOD_PRECOMPUTED_LENGTH - 1; i++) +for (let i = 1; i < MOD_PRECOMPUTED_LENGTH - 1; i++) { let x = (-200 * 2 / 960) * Math.log(i / (MOD_PRECOMPUTED_LENGTH - 1)) / Math.LN10; convex[i] = 1 - x; @@ -34,48 +34,52 @@ for(let i = 1; i < MOD_PRECOMPUTED_LENGTH - 1; i++) * @param value {number} the linear value, 0 to 1 * @returns {number} the transformed value, 0 to 1 or -1 to 1 */ -export function getModulatorCurveValue(direction, curveType, value, polarity) { +export function getModulatorCurveValue(direction, curveType, value, polarity) +{ // inverse the value if needed - if(direction) + if (direction) { - value = 1 - value + value = 1 - value; } - switch (curveType) { + switch (curveType) + { case modulatorCurveTypes.linear: - if (polarity) { + if (polarity) + { // bipolar return value * 2 - 1; } return value; - + case modulatorCurveTypes.switch: // switch value = value > 0.5 ? 1 : 0; - if (polarity) { + if (polarity) + { // multiply return value * 2 - 1; } return value; - + case modulatorCurveTypes.concave: // look up the value - if(polarity) + if (polarity) { value = value * 2 - 1; - if(value < 0) + if (value < 0) { return 1 - concave[~~(value * -MOD_PRECOMPUTED_LENGTH)] - 1; } return concave[~~value * MOD_PRECOMPUTED_LENGTH]; } - return concave[~~(value * MOD_PRECOMPUTED_LENGTH)] - + return concave[~~(value * MOD_PRECOMPUTED_LENGTH)]; + case modulatorCurveTypes.convex: // look up the value - if(polarity) + if (polarity) { value = value * 2 - 1; - if(value < 0) + if (value < 0) { return 1 - convex[~~(value * -MOD_PRECOMPUTED_LENGTH)] - 1; } diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/stereo_panner.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/stereo_panner.js index b2995782..703137eb 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/stereo_panner.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/stereo_panner.js @@ -1,5 +1,6 @@ export const WORKLET_SYSTEM_REVERB_DIVIDER = 1300; export const WORKLET_SYSTEM_CHORUS_DIVIDER = 1300; + /** * stereo_panner.js * purpose: pans a given voice out to the stereo output and to the effects' outputs @@ -26,12 +27,12 @@ export function panVoice(gainLeft, chorus, chorusLevel) { - if(isNaN(inputBuffer[0])) + if (isNaN(inputBuffer[0])) { return; } - - if(reverbLevel > 0) + + if (reverbLevel > 0) { const reverbLeft = reverb[0]; const reverbRight = reverb[1]; @@ -46,8 +47,8 @@ export function panVoice(gainLeft, reverbRight[i] += reverbRightGain * inputBuffer[i]; } } - - if(chorusLevel > 0) + + if (chorusLevel > 0) { const chorusLeft = chorus[0]; const chorusRight = chorus[1]; @@ -62,16 +63,16 @@ export function panVoice(gainLeft, chorusRight[i] += chorusRightGain * inputBuffer[i]; } } - + // mix out the audio data - if(gainLeft > 0) + if (gainLeft > 0) { for (let i = 0; i < inputBuffer.length; i++) { outputLeft[i] += gainLeft * inputBuffer[i]; } } - if(gainRight > 0) + if (gainRight > 0) { for (let i = 0; i < inputBuffer.length; i++) { diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/unit_converter.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/unit_converter.js index e327053c..23b4deed 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/unit_converter.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/unit_converter.js @@ -8,7 +8,8 @@ const MIN_TIMECENT = -15000; const MAX_TIMECENT = 15000; const timecentLookupTable = new Float32Array(MAX_TIMECENT - MIN_TIMECENT + 1); -for (let i = 0; i < timecentLookupTable.length; i++) { +for (let i = 0; i < timecentLookupTable.length; i++) +{ const timecents = MIN_TIMECENT + i; timecentLookupTable[i] = Math.pow(2, timecents / 1200); } @@ -20,7 +21,10 @@ for (let i = 0; i < timecentLookupTable.length; i++) { */ export function timecentsToSeconds(timecents) { - if(timecents <= -32767) return 0; + if (timecents <= -32767) + { + return 0; + } return timecentLookupTable[timecents - MIN_TIMECENT]; } @@ -28,7 +32,8 @@ export function timecentsToSeconds(timecents) const MIN_ABS_CENT = -20000; // freqVibLfo const MAX_ABS_CENT = 16500; // filterFc const absoluteCentLookupTable = new Float32Array(MAX_ABS_CENT - MIN_ABS_CENT + 1); -for (let i = 0; i < absoluteCentLookupTable.length; i++) { +for (let i = 0; i < absoluteCentLookupTable.length; i++) +{ const absoluteCents = MIN_ABS_CENT + i; absoluteCentLookupTable[i] = 440 * Math.pow(2, (absoluteCents - 6900) / 1200); } @@ -40,7 +45,7 @@ for (let i = 0; i < absoluteCentLookupTable.length; i++) { */ export function absCentsToHz(cents) { - if(cents < MIN_ABS_CENT || cents > MAX_ABS_CENT) + if (cents < MIN_ABS_CENT || cents > MAX_ABS_CENT) { return 440 * Math.pow(2, (cents - 6900) / 1200); } @@ -51,7 +56,8 @@ export function absCentsToHz(cents) const MIN_DECIBELS = -1660; const MAX_DECIBELS = 1600; const decibelLookUpTable = new Float32Array((MAX_DECIBELS - MIN_DECIBELS) * 100 + 1); -for (let i = 0; i < decibelLookUpTable.length; i++) { +for (let i = 0; i < decibelLookUpTable.length; i++) +{ const decibels = (MIN_DECIBELS * 100 + i) / 100; decibelLookUpTable[i] = Math.pow(10, -decibels / 20); } diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/volume_envelope.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/volume_envelope.js index 8e1f6256..e3d7d7be 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/volume_envelope.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/volume_envelope.js @@ -1,4 +1,4 @@ -import { decibelAttenuationToGain, timecentsToSeconds } from './unit_converter.js' +import { decibelAttenuationToGain, timecentsToSeconds } from "./unit_converter.js"; import { generatorTypes } from "../../../soundfont/basic_soundfont/generator.js"; @@ -26,28 +26,11 @@ const PERCEIVED_GAIN_SILENCE = 0.000015; // can't go lower than that (see #50) export class WorkletVolumeEnvelope { - /** - * @param sampleRate {number} Hz - * @param initialDecay {number} cb - */ - constructor(sampleRate, initialDecay) - { - this.sampleRate = sampleRate; - /** - * if sustain stge is silent, - * then we can turn off the voice when it is silent. - * We can't do that with modulated as it can silence the volume and then raise it again and the voice must keep playing - * @type {boolean} - */ - this.canEndOnSilentSustain = initialDecay / 10 >= PERCEIVED_DB_SILENCE; - } - /** * The envelope's current time in samples * @type {number} */ currentSampleTime = 0; - /** * The sample rate in Hz * @type {number} @@ -78,7 +61,6 @@ export class WorkletVolumeEnvelope * @type {number} */ currentReleaseGain = 1; - /** * The attack duration in samples * @type {number} @@ -89,55 +71,63 @@ export class WorkletVolumeEnvelope * @type {number} */ decayDuration = 0; - /** * The release duration in samples * @type {number} */ releaseDuration = 0; - /** * The voice's absolute attenuation in dB * @type {number} */ attenuation = 0; - /** * The attenuation target, which the "attenuation" property is linearly interpolated towards * @type {number} */ attenuationTarget = 0; - /** * The voice's sustain amount in dB, relative to attenuation * @type {number} */ sustainDbRelative = 0; - /** * The time in samples to the end of delay stage, relative to start of the envelope * @type {number} */ delayEnd = 0; - /** * The time in samples to the end of attack stage, relative to start of the envelope * @type {number} */ attackEnd = 0; - /** * The time in samples to the end of hold stage, relative to start of the envelope * @type {number} */ holdEnd = 0; - /** * The time in samples to the end of decay stage, relative to start of the envelope * @type {number} */ decayEnd = 0; - + + /** + * @param sampleRate {number} Hz + * @param initialDecay {number} cb + */ + constructor(sampleRate, initialDecay) + { + this.sampleRate = sampleRate; + /** + * if sustain stge is silent, + * then we can turn off the voice when it is silent. + * We can't do that with modulated as it can silence the volume and then raise it again and the voice must keep playing + * @type {boolean} + */ + this.canEndOnSilentSustain = initialDecay / 10 >= PERCEIVED_DB_SILENCE; + } + /** * Starts the release phase in the envelope * @param voice {WorkletVoice} the voice this envelope belongs to @@ -148,7 +138,7 @@ export class WorkletVolumeEnvelope voice.volumeEnvelope.currentReleaseGain = decibelAttenuationToGain(voice.volumeEnvelope.currentAttenuationDb); WorkletVolumeEnvelope.recalculate(voice); } - + /** * Recalculates the envelope * @param voice {WorkletVoice} the voice this envelope belongs to @@ -159,15 +149,18 @@ export class WorkletVolumeEnvelope const timecentsToSamples = tc => { return Math.max(0, Math.floor(timecentsToSeconds(tc) * env.sampleRate)); - } + }; // calculate absolute times (they can change so we have to recalculate every time - env.attenuationTarget = Math.max(0, Math.min(voice.modulatedGenerators[generatorTypes.initialAttenuation], 1440)) / 10; // divide by ten to get decibels + env.attenuationTarget = Math.max( + 0, + Math.min(voice.modulatedGenerators[generatorTypes.initialAttenuation], 1440) + ) / 10; // divide by ten to get decibels env.sustainDbRelative = Math.min(DB_SILENCE, voice.modulatedGenerators[generatorTypes.sustainVolEnv] / 10); const sustainDb = Math.min(DB_SILENCE, env.sustainDbRelative); - + // calculate durations env.attackDuration = timecentsToSamples(voice.modulatedGenerators[generatorTypes.attackVolEnv]); - + // decay: sfspec page 35: the time is for change from attenuation to -100dB // therefore we need to calculate the real time // (changing from attenuation to sustain instead of -100dB) @@ -175,43 +168,43 @@ export class WorkletVolumeEnvelope const keyNumAddition = (60 - voice.targetKey) * voice.modulatedGenerators[generatorTypes.keyNumToVolEnvDecay]; const fraction = sustainDb / DB_SILENCE; env.decayDuration = timecentsToSamples(fullChange + keyNumAddition) * fraction; - + env.releaseDuration = timecentsToSamples(voice.modulatedGenerators[generatorTypes.releaseVolEnv]); - + // calculate absolute end times for the values env.delayEnd = timecentsToSamples(voice.modulatedGenerators[generatorTypes.delayVolEnv]); env.attackEnd = env.attackDuration + env.delayEnd; - + // make sure to take keyNumToVolEnvHold into account!!! const holdExcursion = (60 - voice.targetKey) * voice.modulatedGenerators[generatorTypes.keyNumToVolEnvHold]; env.holdEnd = timecentsToSamples(voice.modulatedGenerators[generatorTypes.holdVolEnv] + holdExcursion) - + env.attackEnd; - + + env.attackEnd; + env.decayEnd = env.decayDuration + env.holdEnd; - + // if this is the first recalculation and the voice has no attack or delay time, set current db to peak - if(env.state === 0 && env.attackEnd === 0) + if (env.state === 0 && env.attackEnd === 0) { // env.currentAttenuationDb = env.attenuationTarget; env.state = 2; } - + // check if voice is in release - if(voice.isInRelease) + if (voice.isInRelease) { // no interpolation this time: force update to actual attenuation and calculate release start from there //env.attenuation = Math.min(DB_SILENCE, env.attenuationTarget); const sustainDb = Math.max(0, Math.min(DB_SILENCE, env.sustainDbRelative)); const fraction = sustainDb / DB_SILENCE; env.decayDuration = timecentsToSamples(fullChange + keyNumAddition) * fraction; - + switch (env.state) { case 0: env.releaseStartDb = DB_SILENCE; break; - + case 1: // attack phase: get linear gain of the attack phase when release started // and turn it into db as we're ramping the db up linearly @@ -222,28 +215,28 @@ export class WorkletVolumeEnvelope // turn that into db env.releaseStartDb = 20 * Math.log10(elapsed) * -1; break; - + case 2: env.releaseStartDb = 0; break; - + case 3: env.releaseStartDb = (1 - (env.decayEnd - env.releaseStartTimeSamples) / env.decayDuration) * sustainDb; break; - + case 4: env.releaseStartDb = sustainDb; break; } env.releaseStartDb = Math.max(0, Math.min(env.releaseStartDb, DB_SILENCE)); - if(env.releaseStartDb >= PERCEIVED_DB_SILENCE) + if (env.releaseStartDb >= PERCEIVED_DB_SILENCE) { voice.finished = true; } env.currentReleaseGain = decibelAttenuationToGain(env.releaseStartDb); } } - + /** * Applies volume envelope gain to the given output buffer * @param voice {WorkletVoice} the voice we're working on @@ -252,18 +245,18 @@ export class WorkletVolumeEnvelope * @param smoothingFactor {number} the adjusted smoothing factor for the envelope * @description essentially we use approach of 100dB is silence, 0dB is peak, and always add attenuation to that (which is interpolated) */ - static apply(voice, audioBuffer, centibelOffset, smoothingFactor) + static apply(voice, audioBuffer, centibelOffset, smoothingFactor) { const env = voice.volumeEnvelope; let decibelOffset = centibelOffset / 10; - + const attenuationSmoothing = smoothingFactor; - + // RELEASE PHASE - if(voice.isInRelease) + if (voice.isInRelease) { let elapsedRelease = env.currentSampleTime - env.releaseStartTimeSamples; - if(elapsedRelease >= env.releaseDuration) + if (elapsedRelease >= env.releaseDuration) { for (let i = 0; i < audioBuffer.length; i++) { @@ -283,108 +276,108 @@ export class WorkletVolumeEnvelope env.currentSampleTime++; elapsedRelease++; } - - if(env.currentReleaseGain <= PERCEIVED_GAIN_SILENCE) + + if (env.currentReleaseGain <= PERCEIVED_GAIN_SILENCE) { voice.finished = true; } return; } - + let filledBuffer = 0; - switch(env.state) + switch (env.state) { case 0: // delay phase, no sound is produced - while(env.currentSampleTime < env.delayEnd) + while (env.currentSampleTime < env.delayEnd) { env.currentAttenuationDb = DB_SILENCE; audioBuffer[filledBuffer] = 0; - - env.currentSampleTime++ - if(++filledBuffer >= audioBuffer.length) + + env.currentSampleTime++; + if (++filledBuffer >= audioBuffer.length) { return; } } env.state++; // fallthrough - + case 1: // attack phase: ramp from 0 to attenuation - while(env.currentSampleTime < env.attackEnd) + while (env.currentSampleTime < env.attackEnd) { // attenuation interpolation env.attenuation += (env.attenuationTarget - env.attenuation) * attenuationSmoothing; - + // Special case: linear gain ramp instead of linear db ramp let linearAttenuation = 1 - (env.attackEnd - env.currentSampleTime) / env.attackDuration; // 0 to 1 audioBuffer[filledBuffer] *= linearAttenuation * decibelAttenuationToGain(env.attenuation + decibelOffset); // set current attenuation to peak as its invalid during this phase env.currentAttenuationDb = 0; - + env.currentSampleTime++; - if(++filledBuffer >= audioBuffer.length) + if (++filledBuffer >= audioBuffer.length) { return; } } env.state++; // fallthrough - + case 2: // hold/peak phase: stay at attenuation - while(env.currentSampleTime < env.holdEnd) + while (env.currentSampleTime < env.holdEnd) { // attenuation interpolation env.attenuation += (env.attenuationTarget - env.attenuation) * attenuationSmoothing; - + audioBuffer[filledBuffer] *= decibelAttenuationToGain(env.attenuation + decibelOffset); env.currentAttenuationDb = 0; - + env.currentSampleTime++; - if(++filledBuffer >= audioBuffer.length) + if (++filledBuffer >= audioBuffer.length) { return; } } env.state++; // fallthrough - + case 3: // decay phase: linear ramp from attenuation to sustain - while(env.currentSampleTime < env.decayEnd) + while (env.currentSampleTime < env.decayEnd) { // attenuation interpolation env.attenuation += (env.attenuationTarget - env.attenuation) * attenuationSmoothing; - + env.currentAttenuationDb = (1 - (env.decayEnd - env.currentSampleTime) / env.decayDuration) * env.sustainDbRelative; audioBuffer[filledBuffer] *= decibelAttenuationToGain(env.currentAttenuationDb + decibelOffset + env.attenuation); - + env.currentSampleTime++; - if(++filledBuffer >= audioBuffer.length) + if (++filledBuffer >= audioBuffer.length) { return; } } env.state++; // fallthrough - + case 4: - if(env.canEndOnSilentSustain && env.sustainDbRelative >= PERCEIVED_DB_SILENCE) + if (env.canEndOnSilentSustain && env.sustainDbRelative >= PERCEIVED_DB_SILENCE) { voice.finished = true; } // sustain phase: stay at sustain - while(true) + while (true) { // attenuation interpolation env.attenuation += (env.attenuationTarget - env.attenuation) * attenuationSmoothing; - + audioBuffer[filledBuffer] *= decibelAttenuationToGain(env.sustainDbRelative + decibelOffset + env.attenuation); env.currentAttenuationDb = env.sustainDbRelative; env.currentSampleTime++; - if(++filledBuffer >= audioBuffer.length) + if (++filledBuffer >= audioBuffer.length) { return; } diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js index 50a47bcc..41ccfd4b 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js @@ -10,8 +10,8 @@ export const interpolationTypes = { linear: 0, nearestNeighbor: 1, - fourthOrder: 2, -} + fourthOrder: 2 +}; /** @@ -24,64 +24,64 @@ export function getSampleLinear(voice, outputBuffer) const sample = voice.sample; let cur = sample.cursor; const sampleData = sample.sampleData; - - if(sample.isLooping) + + if (sample.isLooping) { const loopLength = sample.loopEnd - sample.loopStart; for (let i = 0; i < outputBuffer.length; i++) { // check for loop - while(cur >= sample.loopEnd) + while (cur >= sample.loopEnd) { cur -= loopLength; } - + // grab the 2 nearest points const floor = ~~cur; let ceil = floor + 1; - - while(ceil >= sample.loopEnd) + + while (ceil >= sample.loopEnd) { ceil -= loopLength; } - + const fraction = cur - floor; - + // grab the samples and interpolate const upper = sampleData[ceil]; const lower = sampleData[floor]; outputBuffer[i] = (lower + (upper - lower) * fraction); - + cur += sample.playbackStep * voice.currentTuningCalculated; } } else { - if(sample.loopingMode === 2 && !voice.isInRelease) + if (sample.loopingMode === 2 && !voice.isInRelease) { return; } for (let i = 0; i < outputBuffer.length; i++) { - + // linear interpolation const floor = ~~cur; const ceil = floor + 1; - + // flag the voice as finished if needed - if(ceil >= sample.end) + if (ceil >= sample.end) { voice.finished = true; return; } - + const fraction = cur - floor; - + // grab the samples and interpolate const upper = sampleData[ceil]; const lower = sampleData[floor]; outputBuffer[i] = (lower + (upper - lower) * fraction); - + cur += sample.playbackStep * voice.currentTuningCalculated; } } @@ -99,47 +99,47 @@ export function getSampleNearest(voice, outputBuffer) let cur = sample.cursor; const loopLength = sample.loopEnd - sample.loopStart; const sampleData = sample.sampleData; - if(voice.sample.isLooping) + if (voice.sample.isLooping) { for (let i = 0; i < outputBuffer.length; i++) { // check for loop - while(cur >= sample.loopEnd) + while (cur >= sample.loopEnd) { cur -= loopLength; } - + // grab the nearest neighbor let ceil = ~~cur + 1; - - while(ceil >= sample.loopEnd) + + while (ceil >= sample.loopEnd) { ceil -= loopLength; } - + outputBuffer[i] = sampleData[ceil]; cur += sample.playbackStep * voice.currentTuningCalculated; } } else { - if(sample.loopingMode === 2 && !voice.isInRelease) + if (sample.loopingMode === 2 && !voice.isInRelease) { return; } for (let i = 0; i < outputBuffer.length; i++) { - + // nearest neighbor const ceil = ~~cur + 1; - + // flag the voice as finished if needed - if(ceil >= sample.end) + if (ceil >= sample.end) { voice.finished = true; return; } - + //nearest neighbor (uncomment to use) outputBuffer[i] = sampleData[ceil]; cur += sample.playbackStep * voice.currentTuningCalculated; @@ -149,7 +149,6 @@ export function getSampleNearest(voice, outputBuffer) } - /** * Fills the output buffer with raw sample data using cubic interpolation * @param voice {WorkletVoice} the voice we're working on @@ -160,21 +159,21 @@ export function getSampleCubic(voice, outputBuffer) const sample = voice.sample; let cur = sample.cursor; const sampleData = sample.sampleData; - - if(sample.isLooping) + + if (sample.isLooping) { const loopLength = sample.loopEnd - sample.loopStart; for (let i = 0; i < outputBuffer.length; i++) { // check for loop - while(cur >= sample.loopEnd) + while (cur >= sample.loopEnd) { cur -= loopLength; } - + // math comes from // https://stackoverflow.com/questions/1125666/how-do-you-do-bicubic-or-other-non-linear-interpolation-of-re-sampled-audio-da - + // grab the 4 points const y0 = ~~cur; // point before the cursor. twice bitwise not is just a faster Math.floor let y1 = y0 + 1; // point after the cursor @@ -183,63 +182,76 @@ export function getSampleCubic(voice, outputBuffer) const t = cur - y0; // distance from y0 to cursor // y0 is not handled here // as it's math.floor of cur which is handled above - if(y1 >= sample.loopEnd) y1 -= loopLength; - if(y2 >= sample.loopEnd) y2 -= loopLength; - if(y3 >= sample.loopEnd) y3 -= loopLength; - + if (y1 >= sample.loopEnd) + { + y1 -= loopLength; + } + if (y2 >= sample.loopEnd) + { + y2 -= loopLength; + } + if (y3 >= sample.loopEnd) + { + y3 -= loopLength; + } + // grab the samples const x0 = sampleData[y0]; const x1 = sampleData[y1]; const x2 = sampleData[y2]; const x3 = sampleData[y3]; - + // interpolate // const c0 = x1 const c1 = 0.5 * (x2 - x0); const c2 = x0 - (2.5 * x1) + (2 * x2) - (0.5 * x3); const c3 = (0.5 * (x3 - x0)) + (1.5 * (x1 - x2)); outputBuffer[i] = (((((c3 * t) + c2) * t) + c1) * t) + x1; - - + + cur += sample.playbackStep * voice.currentTuningCalculated; } } else { - if(sample.loopingMode === 2 && !voice.isInRelease) + if (sample.loopingMode === 2 && !voice.isInRelease) { return; } for (let i = 0; i < outputBuffer.length; i++) { - + // math comes from // https://stackoverflow.com/questions/1125666/how-do-you-do-bicubic-or-other-non-linear-interpolation-of-re-sampled-audio-da - + // grab the 4 points const y0 = ~~cur; // point before the cursor. twice bitwise not is just a faster Math.floor let y1 = y0 + 1; // point after the cursor let y2 = y1 + 1; // point 1 after the cursor let y3 = y2 + 1; // point 2 after the cursor const t = cur - y0; // distance from y0 to cursor - + // flag as finished if needed - if(y1 >= sample.end || - y2 >= sample.end || - y3 >= sample.end) {voice.finished = true; return;} - + if (y1 >= sample.end || + y2 >= sample.end || + y3 >= sample.end) + { + voice.finished = true; + return; + } + // grab the samples const x0 = sampleData[y0]; const x1 = sampleData[y1]; const x2 = sampleData[y2]; const x3 = sampleData[y3]; - + // interpolate const c1 = 0.5 * (x2 - x0); const c2 = x0 - (2.5 * x1) + (2 * x2) - (0.5 * x3); const c3 = (0.5 * (x3 - x0)) + (1.5 * (x1 - x2)); outputBuffer[i] = (((((c3 * t) + c2) * t) + c1) * t) + x1; - + cur += sample.playbackStep * voice.currentTuningCalculated; } } diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js index 682577aa..228f7911 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js @@ -1,8 +1,7 @@ - -import { getModulatorCurveValue, MOD_PRECOMPUTED_LENGTH } from './modulator_curves.js' -import { NON_CC_INDEX_OFFSET } from './worklet_processor_channel.js' -import { WorkletVolumeEnvelope } from './volume_envelope.js' -import { WorkletModulationEnvelope } from './modulation_envelope.js' +import { getModulatorCurveValue, MOD_PRECOMPUTED_LENGTH } from "./modulator_curves.js"; +import { NON_CC_INDEX_OFFSET } from "./worklet_processor_channel.js"; +import { WorkletVolumeEnvelope } from "./volume_envelope.js"; +import { WorkletModulationEnvelope } from "./modulation_envelope.js"; import { generatorLimits, generatorTypes } from "../../../soundfont/basic_soundfont/generator.js"; import { Modulator, modulatorSources } from "../../../soundfont/basic_soundfont/modulator.js"; @@ -20,14 +19,14 @@ import { Modulator, modulatorSources } from "../../../soundfont/basic_soundfont/ */ export function computeWorkletModulator(controllerTable, modulator, voice) { - if(modulator.transformAmount === 0) + if (modulator.transformAmount === 0) { modulator.currentValue = 0; return 0; } // mapped to 0-16384 let rawSourceValue; - if(modulator.sourceUsesCC) + if (modulator.sourceUsesCC) { rawSourceValue = controllerTable[modulator.sourceIndex]; } @@ -39,31 +38,31 @@ export function computeWorkletModulator(controllerTable, modulator, voice) case modulatorSources.noController: rawSourceValue = 16383; // equals to 1 break; - + case modulatorSources.noteOnKeyNum: rawSourceValue = voice.midiNote << 7; break; - + case modulatorSources.noteOnVelocity: rawSourceValue = voice.velocity << 7; break; - + case modulatorSources.polyPressure: rawSourceValue = voice.pressure << 7; break; - + default: rawSourceValue = controllerTable[index]; // pitch bend and range are stored in the cc table break; } - + } - + const sourceValue = transforms[modulator.sourceCurveType][modulator.sourcePolarity][modulator.sourceDirection][rawSourceValue]; - + // mapped to 0-127 let rawSecondSrcValue; - if(modulator.secSrcUsesCC) + if (modulator.secSrcUsesCC) { rawSecondSrcValue = controllerTable[modulator.secSrcIndex]; } @@ -75,36 +74,36 @@ export function computeWorkletModulator(controllerTable, modulator, voice) case modulatorSources.noController: rawSecondSrcValue = 16383; // equals to 1 break; - + case modulatorSources.noteOnKeyNum: rawSecondSrcValue = voice.midiNote << 7; break; - + case modulatorSources.noteOnVelocity: rawSecondSrcValue = voice.velocity << 7; break; - + case modulatorSources.polyPressure: rawSecondSrcValue = voice.pressure << 7; break; - + default: rawSecondSrcValue = controllerTable[index]; // pitch bend and range are stored in the cc table } - + } const secondSrcValue = transforms[modulator.secSrcCurveType][modulator.secSrcPolarity][modulator.secSrcDirection][rawSecondSrcValue]; - - + + // compute the modulator let computedValue = sourceValue * secondSrcValue * modulator.transformAmount; - - if(modulator.transformType === 2) + + if (modulator.transformType === 2) { // abs value computedValue = Math.abs(computedValue); } - + modulator.currentValue = computedValue; return computedValue; } @@ -116,29 +115,38 @@ export function computeWorkletModulator(controllerTable, modulator, voice) * @param sourceUsesCC {number} what modulators should be computed, -1 means all, 0 means modulator source enum 1 means midi controller * @param sourceIndex {number} enum for the source */ -export function computeModulators(voice, controllerTable, sourceUsesCC = -1, sourceIndex = 0) { +export function computeModulators(voice, controllerTable, sourceUsesCC = -1, sourceIndex = 0) +{ const modulators = voice.modulators; const generators = voice.generators; const modulatedGenerators = voice.modulatedGenerators; - + // Modulation envelope is cheap to recalculate // why here and not at the bottom? // I dunno, seems to work fine WorkletModulationEnvelope.recalculate(voice); - + if (sourceUsesCC === -1) { // All modulators mode: compute all modulators modulatedGenerators.set(generators); - modulators.forEach(mod => { + modulators.forEach(mod => + { const limits = generatorLimits[mod.modulatorDestination]; - const newValue = modulatedGenerators[mod.modulatorDestination] + computeWorkletModulator(controllerTable, mod, voice); - modulatedGenerators[mod.modulatorDestination] = Math.max(limits.min, Math.min(newValue, limits.max)); + const newValue = modulatedGenerators[mod.modulatorDestination] + computeWorkletModulator( + controllerTable, + mod, + voice + ); + modulatedGenerators[mod.modulatorDestination] = Math.max( + limits.min, + Math.min(newValue, limits.max) + ); }); WorkletVolumeEnvelope.recalculate(voice); return; } - + // Optimized mode: calculate only modulators that use the given source const volenvNeedsRecalculation = new Set([ generatorTypes.initialAttenuation, @@ -151,14 +159,16 @@ export function computeModulators(voice, controllerTable, sourceUsesCC = -1, sou generatorTypes.keyNumToVolEnvHold, generatorTypes.keyNumToVolEnvDecay ]); - + const computedDestinations = new Set(); - - modulators.forEach(mod => { + + modulators.forEach(mod => + { if ( (mod.sourceUsesCC === sourceUsesCC && mod.sourceIndex === sourceIndex) || (mod.secSrcUsesCC === sourceUsesCC && mod.secSrcIndex === sourceIndex) - ) { + ) + { const destination = mod.modulatorDestination; if (!computedDestinations.has(destination)) { @@ -167,19 +177,23 @@ export function computeModulators(voice, controllerTable, sourceUsesCC = -1, sou // compute our modulator computeWorkletModulator(controllerTable, mod, voice); // sum the values of all modulators for this destination - modulators.forEach(m => { + modulators.forEach(m => + { if (m.modulatorDestination === destination) { const limits = generatorLimits[mod.modulatorDestination]; const newValue = modulatedGenerators[mod.modulatorDestination] + m.currentValue; - modulatedGenerators[mod.modulatorDestination] = Math.max(limits.min, Math.min(newValue, limits.max)); + modulatedGenerators[mod.modulatorDestination] = Math.max( + limits.min, + Math.min(newValue, limits.max) + ); } }); computedDestinations.add(destination); } } }); - + // Recalculate volume envelope if necessary if ([...computedDestinations].some(dest => volenvNeedsRecalculation.has(dest))) { @@ -194,7 +208,7 @@ export function computeModulators(voice, controllerTable, sourceUsesCC = -1, sou */ const transforms = []; -for(let curve = 0; curve < 4; curve++) +for (let curve = 0; curve < 4; curve++) { transforms[curve] = [ @@ -207,45 +221,54 @@ for(let curve = 0; curve < 4; curve++) new Float32Array(MOD_PRECOMPUTED_LENGTH) ] ]; - for (let i = 0; i < MOD_PRECOMPUTED_LENGTH; i++) { - + for (let i = 0; i < MOD_PRECOMPUTED_LENGTH; i++) + { + // polarity 0 dir 0 transforms[curve][0][0][i] = getModulatorCurveValue( 0, curve, i / MOD_PRECOMPUTED_LENGTH, - 0); - if (isNaN(transforms[curve][0][0][i])) { + 0 + ); + if (isNaN(transforms[curve][0][0][i])) + { transforms[curve][0][0][i] = 1; } - + // polarity 1 dir 0 transforms[curve][1][0][i] = getModulatorCurveValue( 0, curve, i / MOD_PRECOMPUTED_LENGTH, - 1); - if (isNaN(transforms[curve][1][0][i])) { + 1 + ); + if (isNaN(transforms[curve][1][0][i])) + { transforms[curve][1][0][i] = 1; } - + // polarity 0 dir 1 transforms[curve][0][1][i] = getModulatorCurveValue( 1, curve, i / MOD_PRECOMPUTED_LENGTH, - 0); - if (isNaN(transforms[curve][0][1][i])) { + 0 + ); + if (isNaN(transforms[curve][0][1][i])) + { transforms[curve][0][1][i] = 1; } - + // polarity 1 dir 1 transforms[curve][1][1][i] = getModulatorCurveValue( 1, curve, i / MOD_PRECOMPUTED_LENGTH, - 1); - if (isNaN(transforms[curve][1][1][i])) { + 1 + ); + if (isNaN(transforms[curve][1][1][i])) + { transforms[curve][1][1][i] = 1; } } diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js index 27245252..d09d40e9 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js @@ -1,6 +1,7 @@ -import { midiControllers } from '../../../midi_parser/midi_message.js' +import { midiControllers } from "../../../midi_parser/midi_message.js"; import { modulatorSources } from "../../../soundfont/basic_soundfont/modulator.js"; + /** * @typedef {Object} WorkletProcessorChannel * @property {Int16Array} midiControllers - array of MIDI controller values + the values used by modulators as source (pitch bend, bend range etc.) @@ -28,7 +29,7 @@ import { modulatorSources } from "../../../soundfont/basic_soundfont/modulator.j * @property {number} channelVibrato.depth - depth of the vibrato effect (cents) * @property {number} channelVibrato.delay - delay before the vibrato effect starts (seconds) * @property {number} channelVibrato.rate - rate of the vibrato oscillation (Hz) - + * @property {boolean} isMuted - indicates whether the channel is muted * @property {WorkletVoice[]} voices - array of voices currently active on the channel * @property {WorkletVoice[]} sustainedVoices - array of voices that are sustained on the channel @@ -48,31 +49,31 @@ export function createWorkletChannel(sendEvent = false) midiControllers: new Int16Array(CONTROLLER_TABLE_SIZE), lockedControllers: Array(CONTROLLER_TABLE_SIZE).fill(false), customControllers: new Float32Array(CUSTOM_CONTROLLER_TABLE_SIZE), - + NRPCoarse: 0, NRPFine: 0, RPValue: 0, dataEntryState: dataEntryStates.Idle, - + voices: [], sustainedVoices: [], cachedVoices: [], preset: this.defaultPreset, presetUsesOverride: false, - + channelTransposeKeyShift: 0, channelOctaveTuning: new Int8Array(12), keyCentTuning: new Int16Array(128), - channelVibrato: {delay: 0, depth: 0, rate: 0}, + channelVibrato: { delay: 0, depth: 0, rate: 0 }, velocityOverride: 0, - + lockGSNRPNParams: false, holdPedal: false, isMuted: false, drumChannel: false, - lockPreset: false, - - } + lockPreset: false + + }; for (let i = 0; i < 128; i++) { channel.cachedVoices.push([]); @@ -80,7 +81,7 @@ export function createWorkletChannel(sendEvent = false) this.workletProcessorChannels.push(channel); this.resetControllers(this.workletProcessorChannels.length - 1); this.sendChannelProperties(); - if(sendEvent) + if (sendEvent) { this.callEvent("newchannel", undefined); } @@ -119,8 +120,8 @@ export const customControllers = { channelTransposeFine: 1, // cents, only the decimal tuning, (e.g. transpose is 4.5, then shift by 4 keys + tune by 50 cents) modulationMultiplier: 2, // cents, set by moduldation depth RPN masterTuning: 3, // cents, set by system exclusive - channelTuningSemitones: 4, // semitones, for RPN coarse tuning -} + channelTuningSemitones: 4 // semitones, for RPN coarse tuning +}; export const CUSTOM_CONTROLLER_TABLE_SIZE = Object.keys(customControllers).length; export const customResetArray = new Float32Array(CUSTOM_CONTROLLER_TABLE_SIZE); customResetArray[customControllers.modulationMultiplier] = 1; @@ -130,5 +131,5 @@ customResetArray[customControllers.modulationMultiplier] = 1; * @enum {number} */ export const channelConfiguration = { - velocityOverride: 128, // overrides velocity for the given channel -} + velocityOverride: 128 // overrides velocity for the given channel +}; diff --git a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_voice.js b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_voice.js index 25b9e084..4372aa92 100644 --- a/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +++ b/src/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_voice.js @@ -6,82 +6,41 @@ class WorkletSample { - /** - * @param data {Float32Array} - * @param playbackStep {number} the playback step, a single increment - * @param cursorStart {number} the sample id which starts the playback - * @param rootKey {number} MIDI root key - * @param loopStart {number} loop start index - * @param loopEnd {number} loop end index - * @param endIndex {number} sample end index (for end offset) - * @param loopingMode {number} sample looping mode - */ - constructor( - data, - playbackStep, - cursorStart, - rootKey, - loopStart, - loopEnd, - endIndex, - loopingMode - ) - { - this.sampleData = data; - this.playbackStep = playbackStep; - this.cursor = cursorStart; - this.rootKey = rootKey; - this.loopStart = loopStart; - this.loopEnd = loopEnd; - this.end = endIndex; - this.loopingMode = loopingMode; - this.isLooping = this.loopingMode === 1 || this.loopingMode === 3 - } - - - /** * the sample's audio data * @type {Float32Array} */ sampleData; - /** * Current playback step (rate) * @type {number} */ playbackStep = 0; - /** * Current position in the sample * @type {number} */ cursor = 0; - /** * MIDI root key of the sample * @type {number} */ rootKey = 0; - /** * Start position of the loop * @type {number} */ loopStart = 0; - /** * End position of the loop * @type {number} */ loopEnd = 0; - /** * End position of the sample * @type {number} */ end = 0; - /** * Looping mode of the sample: * 0 - no loop @@ -91,19 +50,49 @@ class WorkletSample * @type {0|1|2|3} */ loopingMode = 0; - - /** * Indicates if the sample is currently looping * @type {boolean} */ isLooping = false; + + /** + * @param data {Float32Array} + * @param playbackStep {number} the playback step, a single increment + * @param cursorStart {number} the sample id which starts the playback + * @param rootKey {number} MIDI root key + * @param loopStart {number} loop start index + * @param loopEnd {number} loop end index + * @param endIndex {number} sample end index (for end offset) + * @param loopingMode {number} sample looping mode + */ + constructor( + data, + playbackStep, + cursorStart, + rootKey, + loopStart, + loopEnd, + endIndex, + loopingMode + ) + { + this.sampleData = data; + this.playbackStep = playbackStep; + this.cursor = cursorStart; + this.rootKey = rootKey; + this.loopStart = loopStart; + this.loopEnd = loopEnd; + this.end = endIndex; + this.loopingMode = loopingMode; + this.isLooping = this.loopingMode === 1 || this.loopingMode === 3; + } } -import { SpessaSynthTable, SpessaSynthWarn } from '../../../utils/loggin.js' -import { WorkletLowpassFilter } from './lowpass_filter.js' -import { WorkletVolumeEnvelope } from './volume_envelope.js' -import { WorkletModulationEnvelope } from './modulation_envelope.js' +import { SpessaSynthTable, SpessaSynthWarn } from "../../../utils/loggin.js"; +import { WorkletLowpassFilter } from "./lowpass_filter.js"; +import { WorkletVolumeEnvelope } from "./volume_envelope.js"; +import { WorkletModulationEnvelope } from "./modulation_envelope.js"; import { addAndClampGenerator, generatorTypes } from "../../../soundfont/basic_soundfont/generator.js"; import { Modulator } from "../../../soundfont/basic_soundfont/modulator.js"; @@ -123,157 +112,160 @@ class WorkletVoice { /** * Creates a new voice - * @param sampleRate {number} - * @param workletSample {WorkletSample} - * @param midiNote {number} - * @param velocity {number} - * @param channel {number} - * @param currentTime {number} - * @param targetKey {number} - * @param generators {Int16Array} - * @param modulators {Modulator[]} - */ - constructor( - sampleRate, - workletSample, - midiNote, - velocity, - channel, - currentTime, - targetKey, - generators, - modulators, - ) - { - this.sample = workletSample; - this.generators = generators; - this.modulatedGenerators = new Int16Array(generators); - this.modulators = modulators; - - this.velocity = velocity; - this.midiNote = midiNote; - this.channelNumber = channel; - this.startTime = currentTime; - this.targetKey = targetKey; - this.volumeEnvelope = new WorkletVolumeEnvelope(sampleRate, generators[generatorTypes.sustainVolEnv]); - } - /** - * Sample ID for voice. * @type {WorkletSample} */ sample; - + /** - * Lowpass filter applied to the voice. + * Lowpass filter appl. + the voice. * @type {WorkletLowpassFilter} */ filter = new WorkletLowpassFilter(); - + /** - * The unmodulated (constant) generators of the voice. + * The unmodulated (coied to) generators of the voice. * @type {Int16Array} */ generators; - + /** - * The voice's modulators. - * Grouped by the destination. + * The voice's modulatnstant * Grouped by the destination. * @type {Modulator[]} */ modulators = []; - + /** - * The generators modulated by the modulators. + * The generators moduors. + by the modulators. * @type {Int16Array} */ modulatedGenerators; - + /** - * Indicates if the voice has finished. + * Indicates if the volated s finished. * @type {boolean} */ finished = false; - + /** - * Indicates if the voice is in the release phase. + * Indicates if the voice ha in the release phase. * @type {boolean} */ isInRelease = false; - + /** - * MIDI channel number. - * @type {number} + * MIDI channel numberice is * @type {number} */ channelNumber = 0; - + /** - * Velocity of the note. + * Velocity of the not. * @type {number} */ velocity = 0; - + /** * MIDI note number. - * @type {number} + e. + @type {number} */ midiNote = 0; - + /** - * The pressure of the note. + * The pressure of the * * @type {number} */ pressure = 0; - + /** - * Target key for the note. - * @type {number} + * Target key for the note. * @type {number} */ targetKey = 0; - + /** - * Modulation envelope. + * Modulation envelopenote. * @type {WorkletModulationEnvelope} */ modulationEnvelope = new WorkletModulationEnvelope(); - + /** * Volume envelope. - * @type {WorkletVolumeEnvelope} + . + type {WorkletVolumeEnvelope} */ volumeEnvelope; - + /** - * Start time of the voice absolute. + * Start time of the v * @bsolute. * @type {number} */ startTime = 0; - + /** - * Start time of the release phase absolute. + * Start time of the roice a phase absolute. * @type {number} */ releaseStartTime = Infinity; - + /** - * Current tuning adjustment in cents. + * Current tuning adjuelease in cents. * @type {number} */ currentTuningCents = 0; - + /** - * Calculated tuning adjustment. + * Calculated tuning astmentent. * @type {number} */ currentTuningCalculated = 1; - + /** * From 0 to 1. - * @type {number} + *djustm {number} */ currentPan = 0.5; - + /** - * Copies a workletVoice instance + * Copies a workletVoi @type + * @param sampleRate {number} + * @param workletSample {WorkletSample} + * @param midiNote {number} + * @param velocity {number} + * @param channel {number} + * @param currentTime {number} + * @param targetKey {number} + * @param generators {Int16Array} + * @param modulators {Modulator[]} + */ + constructor( + sampleRate, + workletSample, + midiNote, + velocity, + channel, + currentTime, + targetKey, + generators, + modulators + ) + { + this.sample = workletSample; + this.generators = generators; + this.modulatedGenerators = new Int16Array(generators); + this.modulators = modulators; + + this.velocity = velocity; + this.midiNote = midiNote; + this.channelNumber = channel; + this.startTime = currentTime; + this.targetKey = targetKey; + this.volumeEnvelope = new WorkletVolumeEnvelope(sampleRate, generators[generatorTypes.sustainVolEnv]); + } + + /** + * Sample ID for voicece ins + tance * @param voice {WorkletVoice} * @param currentTime {number} * @returns WorkletVoice @@ -290,7 +282,7 @@ class WorkletVoice sampleToCopy.loopEnd, sampleToCopy.end, sampleToCopy.loopingMode - ) + ); return new WorkletVoice( voice.volumeEnvelope.sampleRate, sample, @@ -321,15 +313,15 @@ export function getWorkletVoices(channel, velocity, channelObject, currentTime, - debug=false) + debug = false) { /** * @type {WorkletVoice[]} */ let workletVoices; - + const cached = channelObject.cachedVoices[midiNote][velocity]; - if(cached !== undefined) + if (cached !== undefined) { return cached.map(v => WorkletVoice.copy(v, currentTime)); } @@ -340,89 +332,99 @@ export function getWorkletVoices(channel, * @returns {WorkletVoice[]} */ workletVoices = preset.getSamplesAndGenerators(midiNote, velocity) - .reduce((voices, sampleAndGenerators) => { - if(sampleAndGenerators.sample.sampleData === undefined) + .reduce((voices, sampleAndGenerators) => { - SpessaSynthWarn(`Discarding invalid sample: ${sampleAndGenerators.sample.sampleName}`); + if (sampleAndGenerators.sample.sampleData === undefined) + { + SpessaSynthWarn(`Discarding invalid sample: ${sampleAndGenerators.sample.sampleName}`); + return voices; + } + + // create the generator list + const generators = new Int16Array(60); + // apply and sum the gens + for (let i = 0; i < 60; i++) + { + generators[i] = addAndClampGenerator( + i, + sampleAndGenerators.presetGenerators, + sampleAndGenerators.instrumentGenerators + ); + } + + // !! EMU initial attenuation correction, multiply initial attenuation by 0.4 + generators[generatorTypes.initialAttenuation] = Math.floor(generators[generatorTypes.initialAttenuation] * 0.4); + + // key override + let rootKey = sampleAndGenerators.sample.samplePitch; + if (generators[generatorTypes.overridingRootKey] > -1) + { + rootKey = generators[generatorTypes.overridingRootKey]; + } + + let targetKey = midiNote; + if (generators[generatorTypes.keyNum] > -1) + { + targetKey = generators[generatorTypes.keyNum]; + } + + // determine looping mode now. if the loop is too small, disable + let loopStart = sampleAndGenerators.sample.sampleLoopStartIndex; + let loopEnd = sampleAndGenerators.sample.sampleLoopEndIndex; + let loopingMode = generators[generatorTypes.sampleModes]; + /** + * create the worklet sample + * offsets are calculated at note on time (to allow for modulation of them) + * @type {WorkletSample} + */ + const workletSample = new WorkletSample( + sampleAndGenerators.sample.getAudioData(), + (sampleAndGenerators.sample.sampleRate / sampleRate) * Math.pow( + 2, + sampleAndGenerators.sample.samplePitchCorrection / 1200 + ), // cent tuning + 0, + rootKey, + loopStart, + loopEnd, + Math.floor(sampleAndGenerators.sample.sampleData.length) - 1, + loopingMode + ); + // velocity override + if (generators[generatorTypes.velocity] > -1) + { + velocity = generators[generatorTypes.velocity]; + } + + if (debug) + { + SpessaSynthTable([{ + Sample: sampleAndGenerators.sample.sampleName, + Generators: generators, + Modulators: sampleAndGenerators.modulators.map(m => m.debugString()), + Velocity: velocity, + TargetKey: targetKey, + MidiNote: midiNote, + WorkletSample: workletSample + }]); + } + + + voices.push( + new WorkletVoice( + sampleRate, + workletSample, + midiNote, + velocity, + channel, + currentTime, + targetKey, + generators, + sampleAndGenerators.modulators.map(m => Modulator.copy(m)) + ) + ); return voices; - } - - // create the generator list - const generators = new Int16Array(60); - // apply and sum the gens - for (let i = 0; i < 60; i++) - { - generators[i] = addAndClampGenerator(i, sampleAndGenerators.presetGenerators, sampleAndGenerators.instrumentGenerators); - } - - // !! EMU initial attenuation correction, multiply initial attenuation by 0.4 - generators[generatorTypes.initialAttenuation] = Math.floor(generators[generatorTypes.initialAttenuation] * 0.4); - - // key override - let rootKey = sampleAndGenerators.sample.samplePitch; - if (generators[generatorTypes.overridingRootKey] > -1) { - rootKey = generators[generatorTypes.overridingRootKey]; - } - - let targetKey = midiNote; - if (generators[generatorTypes.keyNum] > -1) { - targetKey = generators[generatorTypes.keyNum]; - } - - // determine looping mode now. if the loop is too small, disable - let loopStart = sampleAndGenerators.sample.sampleLoopStartIndex; - let loopEnd = sampleAndGenerators.sample.sampleLoopEndIndex; - let loopingMode = generators[generatorTypes.sampleModes]; - /** - * create the worklet sample - * offsets are calculated at note on time (to allow for modulation of them) - * @type {WorkletSample} - */ - const workletSample = new WorkletSample( - sampleAndGenerators.sample.getAudioData(), - (sampleAndGenerators.sample.sampleRate / sampleRate) * Math.pow(2, sampleAndGenerators.sample.samplePitchCorrection / 1200), // cent tuning - 0, - rootKey, - loopStart, - loopEnd, - Math.floor( sampleAndGenerators.sample.sampleData.length) - 1, - loopingMode - ) - // velocity override - if (generators[generatorTypes.velocity] > -1) - { - velocity = generators[generatorTypes.velocity]; - } - - if(debug) - { - SpessaSynthTable([{ - Sample: sampleAndGenerators.sample.sampleName, - Generators: generators, - Modulators: sampleAndGenerators.modulators.map(m => m.debugString()), - Velocity: velocity, - TargetKey: targetKey, - MidiNote: midiNote, - WorkletSample: workletSample - }]); - } - - - voices.push( - new WorkletVoice( - sampleRate, - workletSample, - midiNote, - velocity, - channel, - currentTime, - targetKey, - generators, - sampleAndGenerators.modulators.map(m => Modulator.copy(m)) - ) - ); - return voices; - }, []); + }, []); // cache the voice channelObject.cachedVoices[midiNote][velocity] = workletVoices.map(v => WorkletVoice.copy(v, currentTime)); } diff --git a/src/spessasynth_lib/utils/README.md b/src/spessasynth_lib/utils/README.md index 285e1646..ee095c08 100644 --- a/src/spessasynth_lib/utils/README.md +++ b/src/spessasynth_lib/utils/README.md @@ -1,4 +1,5 @@ ## This is the utility folder. + There are various utilites here used by the SpessaSynth library. ### Note that the stbvorbis_sync.js is licensed under Apache-2.0. \ No newline at end of file diff --git a/src/spessasynth_lib/utils/buffer_to_wav.js b/src/spessasynth_lib/utils/buffer_to_wav.js index 666cbcda..4dc55246 100644 --- a/src/spessasynth_lib/utils/buffer_to_wav.js +++ b/src/spessasynth_lib/utils/buffer_to_wav.js @@ -6,10 +6,10 @@ * @property {string|undefined} genre - the song's genre */ -import { combineArrays, IndexedByteArray } from './indexed_array.js' -import { getStringBytes, writeStringAsBytes } from './byte_functions/string.js' -import { writeRIFFOddSize } from '../soundfont/basic_soundfont/riff_chunk.js' -import { writeLittleEndian } from './byte_functions/little_endian.js' +import { combineArrays, IndexedByteArray } from "./indexed_array.js"; +import { getStringBytes, writeStringAsBytes } from "./byte_functions/string.js"; +import { writeRIFFOddSize } from "../soundfont/basic_soundfont/riff_chunk.js"; +import { writeLittleEndian } from "./byte_functions/little_endian.js"; /** * @@ -20,44 +20,44 @@ import { writeLittleEndian } from './byte_functions/little_endian.js' * @param loop {{start: number, end: number}} loop start and end points in seconds. Undefined if no loop * @returns {Blob} */ -export function audioBufferToWav(audioBuffer, normalizeAudio = true, channelOffset = 0, metadata = {}, loop=undefined) +export function audioBufferToWav(audioBuffer, normalizeAudio = true, channelOffset = 0, metadata = {}, loop = undefined) { const channel1Data = audioBuffer.getChannelData(channelOffset); const channel2Data = audioBuffer.getChannelData(channelOffset + 1); const length = channel1Data.length; - + const bytesPerSample = 2; // 16-bit PCM - + // prepare INFO chunk let infoChunk = new IndexedByteArray(0); const infoOn = Object.keys(metadata).length > 0; // INFO chunk - if(infoOn) + if (infoOn) { const encoder = new TextEncoder(); const infoChunks = [ getStringBytes("INFO"), writeRIFFOddSize("ICMT", encoder.encode("Created with SpessaSynth"), true) ]; - if(metadata.artist) + if (metadata.artist) { infoChunks.push( writeRIFFOddSize("IART", encoder.encode(metadata.artist), true) ); } - if(metadata.album) + if (metadata.album) { infoChunks.push( writeRIFFOddSize("IPRD", encoder.encode(metadata.album), true) ); } - if(metadata.genre) + if (metadata.genre) { infoChunks.push( writeRIFFOddSize("IGNR", encoder.encode(metadata.genre), true) ); } - if(metadata.title) + if (metadata.title) { infoChunks.push( writeRIFFOddSize("INAM", encoder.encode(metadata.title), true) @@ -65,15 +65,15 @@ export function audioBufferToWav(audioBuffer, normalizeAudio = true, channelOffs } infoChunk = writeRIFFOddSize("LIST", combineArrays(infoChunks)); } - + // prepare CUE chunk let cueChunk = new IndexedByteArray(0); const cueOn = loop?.end !== undefined && loop?.start !== undefined; - if(cueOn) + if (cueOn) { const loopStartSamples = Math.floor(loop.start * audioBuffer.sampleRate); const loopEndSamples = Math.floor(loop.end * audioBuffer.sampleRate); - + const cueStart = new IndexedByteArray(24); writeLittleEndian(cueStart, 0, 4); // dwIdentifier writeLittleEndian(cueStart, 0, 4); // dwPosition @@ -81,7 +81,7 @@ export function audioBufferToWav(audioBuffer, normalizeAudio = true, channelOffs writeLittleEndian(cueStart, 0, 4); // chunkStart, always 0 writeLittleEndian(cueStart, 0, 4); // BlockStart, always 0 writeLittleEndian(cueStart, loopStartSamples, 4); // sampleOffset - + const cueEnd = new IndexedByteArray(24); writeLittleEndian(cueEnd, 1, 4); // dwIdentifier writeLittleEndian(cueEnd, 0, 4); // dwPosition @@ -89,7 +89,7 @@ export function audioBufferToWav(audioBuffer, normalizeAudio = true, channelOffs writeLittleEndian(cueEnd, 0, 4); // chunkStart, always 0 writeLittleEndian(cueEnd, 0, 4); // BlockStart, always 0 writeLittleEndian(cueEnd, loopEndSamples, 4); // sampleOffset - + const out = combineArrays([ new IndexedByteArray([2, 0, 0, 0]), // cue points count, cueStart, @@ -97,17 +97,20 @@ export function audioBufferToWav(audioBuffer, normalizeAudio = true, channelOffs ]); cueChunk = writeRIFFOddSize("cue ", out); } - + // Prepare the header const headerSize = 44; const dataSize = length * 2 * bytesPerSample; // 2 channels, 16-bit per channel const fileSize = headerSize + dataSize + infoChunk.length + cueChunk.length - 8; // total file size minus the first 8 bytes const header = new Uint8Array(headerSize); - + // 'RIFF' header.set([82, 73, 70, 70], 0); // file length - header.set(new Uint8Array([fileSize & 0xff, (fileSize >> 8) & 0xff, (fileSize >> 16) & 0xff, (fileSize >> 24) & 0xff]), 4); + header.set( + new Uint8Array([fileSize & 0xff, (fileSize >> 8) & 0xff, (fileSize >> 16) & 0xff, (fileSize >> 24) & 0xff]), + 4 + ); // 'WAVE' header.set([87, 65, 86, 69], 8); // 'fmt ' @@ -120,30 +123,40 @@ export function audioBufferToWav(audioBuffer, normalizeAudio = true, channelOffs header.set([2, 0], 22); // sample rate const sampleRate = audioBuffer.sampleRate; - header.set(new Uint8Array([sampleRate & 0xff, (sampleRate >> 8) & 0xff, (sampleRate >> 16) & 0xff, (sampleRate >> 24) & 0xff]), 24); + header.set( + new Uint8Array([sampleRate & 0xff, (sampleRate >> 8) & 0xff, (sampleRate >> 16) & 0xff, (sampleRate >> 24) & 0xff]), + 24 + ); // byte rate (sample rate * block align) const byteRate = sampleRate * 2 * bytesPerSample; // 2 channels, 16-bit per channel - header.set(new Uint8Array([byteRate & 0xff, (byteRate >> 8) & 0xff, (byteRate >> 16) & 0xff, (byteRate >> 24) & 0xff]), 28); + header.set( + new Uint8Array([byteRate & 0xff, (byteRate >> 8) & 0xff, (byteRate >> 16) & 0xff, (byteRate >> 24) & 0xff]), + 28 + ); // block align (channels * bytes per sample) header.set([4, 0], 32); // 2 channels * 16-bit per channel / 8 // bits per sample header.set([16, 0], 34); // 16-bit - + // data chunk identifier 'data' header.set([100, 97, 116, 97], 36); // data chunk length - header.set(new Uint8Array([dataSize & 0xff, (dataSize >> 8) & 0xff, (dataSize >> 16) & 0xff, (dataSize >> 24) & 0xff]), 40); - - let wavData = new Uint8Array(fileSize + 8); + header.set( + new Uint8Array([dataSize & 0xff, (dataSize >> 8) & 0xff, (dataSize >> 16) & 0xff, (dataSize >> 24) & 0xff]), + 40 + ); + + let wavData = new Uint8Array(fileSize + 8); let offset = headerSize; wavData.set(header, 0); - + // Interleave audio data (combine channels) let multiplier = 32767; - if(normalizeAudio) + if (normalizeAudio) { // find min and max values to prevent clipping when converting to 16 bits - const maxAbsValue = channel1Data.map((v, i) => Math.max(Math.abs(v), Math.abs(channel2Data[i]))).reduce( (a,b) => Math.max(a,b)) + const maxAbsValue = channel1Data.map((v, i) => Math.max(Math.abs(v), Math.abs(channel2Data[i]))) + .reduce((a, b) => Math.max(a, b)); multiplier = maxAbsValue > 0 ? (32767 / maxAbsValue) : 1; } for (let i = 0; i < length; i++) @@ -151,23 +164,23 @@ export function audioBufferToWav(audioBuffer, normalizeAudio = true, channelOffs // interleave both channels const sample1 = Math.min(32767, Math.max(-32768, channel1Data[i] * multiplier)); const sample2 = Math.min(32767, Math.max(-32768, channel2Data[i] * multiplier)); - + // convert to 16-bit wavData[offset++] = sample1 & 0xff; wavData[offset++] = (sample1 >> 8) & 0xff; wavData[offset++] = sample2 & 0xff; wavData[offset++] = (sample2 >> 8) & 0xff; } - - if(infoOn) + + if (infoOn) { wavData.set(infoChunk, offset); offset += infoChunk.length; } - if(cueOn) + if (cueOn) { wavData.set(cueChunk, offset); } - - return new Blob([wavData.buffer], { type: 'audio/wav' }); + + return new Blob([wavData.buffer], { type: "audio/wav" }); } diff --git a/src/spessasynth_lib/utils/byte_functions/big_endian.js b/src/spessasynth_lib/utils/byte_functions/big_endian.js index b63cf811..e1bf3762 100644 --- a/src/spessasynth_lib/utils/byte_functions/big_endian.js +++ b/src/spessasynth_lib/utils/byte_functions/big_endian.js @@ -4,12 +4,14 @@ * @param bytesAmount {number} * @returns {number} */ -export function readBytesAsUintBigEndian(dataArray, bytesAmount) { - let out = 0 - for (let i = 8 * (bytesAmount - 1); i >= 0; i -= 8) { - out |= (dataArray[dataArray.currentIndex++] << i) +export function readBytesAsUintBigEndian(dataArray, bytesAmount) +{ + let out = 0; + for (let i = 8 * (bytesAmount - 1); i >= 0; i -= 8) + { + out |= (dataArray[dataArray.currentIndex++] << i); } - return out >>> 0 + return out >>> 0; } /** @@ -17,12 +19,14 @@ export function readBytesAsUintBigEndian(dataArray, bytesAmount) { * @param bytesAmount {number} * @returns {number[]} */ -export function writeBytesAsUintBigEndian(number, bytesAmount) { - const bytes = new Array(bytesAmount).fill(0) - for (let i = bytesAmount - 1; i >= 0; i--) { - bytes[i] = number & 0xFF - number >>= 8 +export function writeBytesAsUintBigEndian(number, bytesAmount) +{ + const bytes = new Array(bytesAmount).fill(0); + for (let i = bytesAmount - 1; i >= 0; i--) + { + bytes[i] = number & 0xFF; + number >>= 8; } - - return bytes + + return bytes; } \ No newline at end of file diff --git a/src/spessasynth_lib/utils/byte_functions/little_endian.js b/src/spessasynth_lib/utils/byte_functions/little_endian.js index 53ff03fe..be27a911 100644 --- a/src/spessasynth_lib/utils/byte_functions/little_endian.js +++ b/src/spessasynth_lib/utils/byte_functions/little_endian.js @@ -4,9 +4,10 @@ * @param bytesAmount {number} * @returns {number} */ -export function readLittleEndian(dataArray, bytesAmount){ +export function readLittleEndian(dataArray, bytesAmount) +{ let out = 0; - for(let i = 0; i < bytesAmount; i++) + for (let i = 0; i < bytesAmount; i++) { out |= (dataArray[dataArray.currentIndex++] << i * 8); } @@ -22,7 +23,7 @@ export function readLittleEndian(dataArray, bytesAmount){ */ export function writeLittleEndian(dataArray, number, byteTarget) { - for(let i = 0; i < byteTarget; i++) + for (let i = 0; i < byteTarget; i++) { dataArray[dataArray.currentIndex++] = (number >> (i * 8)) & 0xFF; } @@ -52,9 +53,10 @@ export function writeDword(dataArray, dword) * @param byte2 {number} * @returns {number} */ -export function signedInt16(byte1, byte2){ +export function signedInt16(byte1, byte2) +{ let val = (byte2 << 8) | byte1; - if(val > 32767) + if (val > 32767) { return val - 65536; } @@ -65,8 +67,9 @@ export function signedInt16(byte1, byte2){ * @param byte {number} * @returns {number} */ -export function signedInt8(byte) { - if(byte > 127) +export function signedInt8(byte) +{ + if (byte > 127) { return byte - 256; } diff --git a/src/spessasynth_lib/utils/byte_functions/string.js b/src/spessasynth_lib/utils/byte_functions/string.js index 6b47527f..b4fb1a39 100644 --- a/src/spessasynth_lib/utils/byte_functions/string.js +++ b/src/spessasynth_lib/utils/byte_functions/string.js @@ -1,4 +1,4 @@ -import { IndexedByteArray } from '../indexed_array.js' +import { IndexedByteArray } from "../indexed_array.js"; /** * @param dataArray {IndexedByteArray} @@ -7,24 +7,25 @@ import { IndexedByteArray } from '../indexed_array.js' * @param trimEnd {boolean} if we should trim once we reach an invalid byte * @returns {string} */ -export function readBytesAsString(dataArray, bytes, encoding = undefined, trimEnd = true) { +export function readBytesAsString(dataArray, bytes, encoding = undefined, trimEnd = true) +{ if (!encoding) { - let finished = false - let string = '' + let finished = false; + let string = ""; for (let i = 0; i < bytes; i++) { - let byte = dataArray[dataArray.currentIndex++] + let byte = dataArray[dataArray.currentIndex++]; if (finished) { - continue + continue; } if (byte < 32 || byte > 127) { if (trimEnd) { - finished = true - continue + finished = true; + continue; } else { @@ -35,16 +36,16 @@ export function readBytesAsString(dataArray, bytes, encoding = undefined, trimEn } } } - string += String.fromCharCode(byte) + string += String.fromCharCode(byte); } - return string + return string; } else { - let byteBuffer = dataArray.slice(dataArray.currentIndex, dataArray.currentIndex + bytes) - dataArray.currentIndex += bytes - let decoder = new TextDecoder(encoding.replace(/[^\x20-\x7E]/g, '')) - return decoder.decode(byteBuffer.buffer) + let byteBuffer = dataArray.slice(dataArray.currentIndex, dataArray.currentIndex + bytes); + dataArray.currentIndex += bytes; + let decoder = new TextDecoder(encoding.replace(/[^\x20-\x7E]/g, "")); + return decoder.decode(byteBuffer.buffer); } } @@ -56,7 +57,7 @@ export function readBytesAsString(dataArray, bytes, encoding = undefined, trimEn export function getStringBytes(string, padLength = 0) { let len = string.length; - if(padLength > 0) + if (padLength > 0) { len = padLength; } @@ -73,9 +74,9 @@ export function getStringBytes(string, padLength = 0) */ export function writeStringAsBytes(outArray, string, padLength = 0) { - if(padLength > 0) + if (padLength > 0) { - if(string.length > padLength) + if (string.length > padLength) { string = string.slice(0, padLength); } @@ -84,14 +85,14 @@ export function writeStringAsBytes(outArray, string, padLength = 0) { outArray[outArray.currentIndex++] = string.charCodeAt(i); } - + // pad with zeros if needed - if(padLength > string.length) + if (padLength > string.length) { for (let i = 0; i < padLength - string.length; i++) { outArray[outArray.currentIndex++] = 0; } } - return outArray + return outArray; } \ No newline at end of file diff --git a/src/spessasynth_lib/utils/byte_functions/variable_length_quantity.js b/src/spessasynth_lib/utils/byte_functions/variable_length_quantity.js index 5e188a63..678ade24 100644 --- a/src/spessasynth_lib/utils/byte_functions/variable_length_quantity.js +++ b/src/spessasynth_lib/utils/byte_functions/variable_length_quantity.js @@ -3,19 +3,22 @@ * @param MIDIbyteArray {IndexedByteArray} * @returns {number} */ -export function readVariableLengthQuantity(MIDIbyteArray) { - let out = 0 - while (MIDIbyteArray) { - const byte = MIDIbyteArray[MIDIbyteArray.currentIndex++] +export function readVariableLengthQuantity(MIDIbyteArray) +{ + let out = 0; + while (MIDIbyteArray) + { + const byte = MIDIbyteArray[MIDIbyteArray.currentIndex++]; // extract the first 7 bytes - out = (out << 7) | (byte & 127) - + out = (out << 7) | (byte & 127); + // if the last byte isn't 1, stop reading - if ((byte >> 7) !== 1) { - break + if ((byte >> 7) !== 1) + { + break; } } - return out + return out; } /** @@ -23,15 +26,17 @@ export function readVariableLengthQuantity(MIDIbyteArray) { * @param number {number} * @returns {number[]} */ -export function writeVariableLengthQuantity(number) { +export function writeVariableLengthQuantity(number) +{ // Add the first byte - let bytes = [number & 127] - number >>= 7 - + let bytes = [number & 127]; + number >>= 7; + // Continue processing the remaining bytes - while (number > 0) { - bytes.unshift((number & 127) | 128) - number >>= 7 + while (number > 0) + { + bytes.unshift((number & 127) | 128); + number >>= 7; } - return bytes + return bytes; } \ No newline at end of file diff --git a/src/spessasynth_lib/utils/encode_vorbis.js b/src/spessasynth_lib/utils/encode_vorbis.js index d9eec202..c08f64fc 100644 --- a/src/spessasynth_lib/utils/encode_vorbis.js +++ b/src/spessasynth_lib/utils/encode_vorbis.js @@ -1,4 +1,4 @@ -import { libvorbis } from '../externals/libvorbis/OggVorbisEncoder.min.js' +import { libvorbis } from "../externals/libvorbis/OggVorbisEncoder.min.js"; /** * @typedef {function} EncodeVorbisFunction @@ -8,7 +8,7 @@ import { libvorbis } from '../externals/libvorbis/OggVorbisEncoder.min.js' * @param quality {number} -0.1 to 1 * @returns {Uint8Array} */ -export function encodeVorbis(channelAudioData, channels, sampleRate, quality) +export function encodeVorbis(channelAudioData, channels, sampleRate, quality) { // https://github.com/higuma/ogg-vorbis-encoder-js //libvorbis.init(); @@ -21,7 +21,7 @@ export function encodeVorbis(channelAudioData, channels, sampleRate, quality) const outLen = arrs.reduce((l, c) => l + c.length, 0); const out = new Uint8Array(outLen); let offset = 0; - for(const a of arrs) + for (const a of arrs) { out.set(a, offset); offset += a.length; diff --git a/src/spessasynth_lib/utils/indexed_array.js b/src/spessasynth_lib/utils/indexed_array.js index 44a19396..b7b79fa7 100644 --- a/src/spessasynth_lib/utils/indexed_array.js +++ b/src/spessasynth_lib/utils/indexed_array.js @@ -5,6 +5,12 @@ export class IndexedByteArray extends Uint8Array { + /** + * The current index of the array + * @type {number} + */ + currentIndex; + /** * Creates a new instance of an Uint8Array with a currentIndex property * @param args {any} same as for Uint8Array @@ -14,12 +20,6 @@ export class IndexedByteArray extends Uint8Array super(args); this.currentIndex = 0; } - - /** - * The current index of the array - * @type {number} - */ - currentIndex; } @@ -32,7 +32,7 @@ export function combineArrays(arrs) const length = arrs.reduce((sum, current) => sum + current.length, 0); const newArr = new IndexedByteArray(length); let offset = 0; - for(const arr of arrs) + for (const arr of arrs) { newArr.set(arr, offset); offset += arr.length; diff --git a/src/spessasynth_lib/utils/loggin.js b/src/spessasynth_lib/utils/loggin.js index b7b70416..e3e0dcc9 100644 --- a/src/spessasynth_lib/utils/loggin.js +++ b/src/spessasynth_lib/utils/loggin.js @@ -23,7 +23,7 @@ export function SpessaSynthLogging(enableInfo, enableWarn, enableGroup, enableTa */ export function SpessaSynthInfo(...message) { - if(ENABLE_INFO) + if (ENABLE_INFO) { console.info(...message); } @@ -34,7 +34,7 @@ export function SpessaSynthInfo(...message) */ export function SpessaSynthWarn(...message) { - if(ENABLE_WARN) + if (ENABLE_WARN) { console.warn(...message); } @@ -42,7 +42,7 @@ export function SpessaSynthWarn(...message) export function SpessaSynthTable(...args) { - if(ENABLE_TABLE) + if (ENABLE_TABLE) { console.table(...args); } @@ -53,7 +53,7 @@ export function SpessaSynthTable(...args) */ export function SpessaSynthGroup(...message) { - if(ENABLE_GROUP) + if (ENABLE_GROUP) { console.group(...message); } @@ -64,7 +64,7 @@ export function SpessaSynthGroup(...message) */ export function SpessaSynthGroupCollapsed(...message) { - if(ENABLE_GROUP) + if (ENABLE_GROUP) { console.groupCollapsed(...message); } @@ -72,7 +72,7 @@ export function SpessaSynthGroupCollapsed(...message) export function SpessaSynthGroupEnd() { - if(ENABLE_GROUP) + if (ENABLE_GROUP) { console.groupEnd(); } diff --git a/src/spessasynth_lib/utils/other.js b/src/spessasynth_lib/utils/other.js index 89f4653d..ad278709 100644 --- a/src/spessasynth_lib/utils/other.js +++ b/src/spessasynth_lib/utils/other.js @@ -8,11 +8,16 @@ * @param totalSeconds {number} time in seconds * @return {{seconds: number, minutes: number, time: string}} */ -export function formatTime(totalSeconds) { +export function formatTime(totalSeconds) +{ totalSeconds = Math.floor(totalSeconds); let minutes = Math.floor(totalSeconds / 60); let seconds = Math.round(totalSeconds - (minutes * 60)); - return {"minutes": minutes, "seconds": seconds, "time": `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`} + return { + "minutes": minutes, + "seconds": seconds, + "time": `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}` + }; } /** @@ -33,15 +38,17 @@ export function formatTitle(fileName) * @param arr {number[]} * @returns {string} */ -export function arrayToHexString(arr) { - let hexString = ''; - - for (let i = 0; i < arr.length; i++) { - const hex = arr[i].toString(16).padStart(2, '0').toUpperCase(); +export function arrayToHexString(arr) +{ + let hexString = ""; + + for (let i = 0; i < arr.length; i++) + { + const hex = arr[i].toString(16).padStart(2, "0").toUpperCase(); hexString += hex; - hexString += ' '; + hexString += " "; } - + return hexString; } @@ -51,4 +58,4 @@ export const consoleColors = { info: "color: aqua;", recognized: "color: lime", value: "color: yellow; background-color: black;" -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/website/README.md b/src/website/README.md index 4823d206..39699ccb 100644 --- a/src/website/README.md +++ b/src/website/README.md @@ -1,4 +1,5 @@ ## This is the website folder. + It contains the css, HTML and the frontend Javascript for the GUI, such as the renderer, keyboard, etc. @@ -15,6 +16,7 @@ so maybe give them a try! Note: All these are not documented in the wiki as they are not the part of the SpessaSynth library. You're on your own! -Note 2: Pretty much all of these require a SpessaSynth instance to work, so they can't be used for different synths without large modifications. +Note 2: Pretty much all of these require a SpessaSynth instance to work, so they can't be used for different synths +without large modifications. Note 3: use `minify_website.sh` to minify the code. \ No newline at end of file diff --git a/src/website/css/dls_to_sf2.css b/src/website/css/dls_to_sf2.css index ace1df35..89c24d4d 100644 --- a/src/website/css/dls_to_sf2.css +++ b/src/website/css/dls_to_sf2.css @@ -7,27 +7,27 @@ color: #222; } -a{ +a { color: #546fff !important; } -.box{ +.box { padding: 2rem; margin: 0.5rem; border-radius: 1rem; } -.button_box{ +.button_box { display: flex; flex-direction: column; align-items: center; } -.wide_box{ +.wide_box { min-width: 80% !important; } -.flexy_box{ +.flexy_box { padding: 0.5rem !important; display: flex; flex-direction: column; @@ -35,15 +35,15 @@ a{ border: solid 0.1rem black; } -.hidden{ +.hidden { display: none; } -#dls_upload{ +#dls_upload { display: none; } -label{ +label { border: solid black 0.1rem; cursor: pointer; padding: 1rem; @@ -53,11 +53,11 @@ label{ margin: 0.5rem; } -label:active{ +label:active { transform: scale(0.9); } -body{ +body { background: #eee; display: flex; justify-content: center; diff --git a/src/website/css/keyboard/key.css b/src/website/css/keyboard/key.css index c3b604ae..519cfe8f 100644 --- a/src/website/css/keyboard/key.css +++ b/src/website/css/keyboard/key.css @@ -1,5 +1,4 @@ -#keyboard .key -{ +#keyboard .key { -webkit-user-select: none; user-select: none; touch-action: none; @@ -11,11 +10,11 @@ transform-origin: center top; --pressed-transform-skew: 0.0007; --pressed-transform: matrix3d( - 1,0,0, - 0,0,1, - 0,var(--pressed-transform-skew),0, - 0,1,0, - 0,0,0, + 1, 0, 0, + 0, 0, 1, + 0, var(--pressed-transform-skew), 0, + 0, 1, 0, + 0, 0, 0, 1); cursor: default; @@ -34,16 +33,15 @@ SIDEWAYS KEYS instead of "pressing" from top center, press and scale from center right! */ -#keyboard.sideways .key{ +#keyboard.sideways .key { border-radius: var(--key-border-radius) 0 0 var(--key-border-radius) !important; transform-origin: right center !important; --pressed-transform: matrix3d( - 1,0,0, - calc(var(--pressed-transform-skew) * -1),0,1, - 0,0,0, - 0,1,0, - 0,0,0 - ,1) !important; + 1, 0, 0, + calc(var(--pressed-transform-skew) * -1), 0, 1, + 0, 0, 0, + 0, 1, 0, + 0, 0, 0, 1) !important; --sharp-transform: scale(0.7, 1) !important; @@ -53,45 +51,45 @@ instead of "pressing" from top center, press and scale from center right! } -#keyboard .flat_key{ +#keyboard .flat_key { background: linear-gradient(90deg, #bbb, white); z-index: 1; } -#keyboard .flat_dark_key{ +#keyboard .flat_dark_key { background: linear-gradient(100deg, #111, #000); } -#keyboard .sharp_key{ +#keyboard .sharp_key { transform: var(--sharp-transform); z-index: 10; background: linear-gradient(140deg, #222, black); } -#keyboard .flat_key.between_sharps{ +#keyboard .flat_key.between_sharps { transform: var(--flat-between-transform); } -#keyboard .flat_key.left_sharp{ +#keyboard .flat_key.left_sharp { transform: var(--flat-left-transform); } -#keyboard .flat_key.right_sharp{ +#keyboard .flat_key.right_sharp { transform: var(--flat-right-transform); } -.sharp_key.pressed{ +.sharp_key.pressed { transform: var(--sharp-transform) var(--pressed-transform) !important; } -.flat_key.between_sharps.pressed{ +.flat_key.between_sharps.pressed { transform: var(--flat-between-transform) var(--pressed-transform) !important; } -.flat_key.left_sharp.pressed{ +.flat_key.left_sharp.pressed { transform: var(--flat-left-transform) var(--pressed-transform) !important; } -.flat_key.right_sharp.pressed{ +.flat_key.right_sharp.pressed { transform: var(--flat-right-transform) var(--pressed-transform) !important; } \ No newline at end of file diff --git a/src/website/css/keyboard/keyboard.css b/src/website/css/keyboard/keyboard.css index c7fc0f62..4637fcc8 100644 --- a/src/website/css/keyboard/keyboard.css +++ b/src/website/css/keyboard/keyboard.css @@ -15,11 +15,11 @@ --key-border-radius: 0.4vmin; } -#keyboard.mode_transform{ +#keyboard.mode_transform { transform: translateY(250%); } -#keyboard.sideways{ +#keyboard.sideways { min-height: unset; height: 100%; width: unset; diff --git a/src/website/css/keyboard_canvas_wrapper.css b/src/website/css/keyboard_canvas_wrapper.css index 493c8e91..82db9e55 100644 --- a/src/website/css/keyboard_canvas_wrapper.css +++ b/src/website/css/keyboard_canvas_wrapper.css @@ -1,4 +1,4 @@ -#keyboard_canvas_wrapper{ +#keyboard_canvas_wrapper { display: flex; flex-direction: column; flex: 1; @@ -7,18 +7,18 @@ transition: var(--music-mode-transition) transform; } -#keyboard_canvas_wrapper.out_animation{ +#keyboard_canvas_wrapper.out_animation { transform: translateX(-100%); } -#keyboard_canvas_wrapper.upwards{ +#keyboard_canvas_wrapper.upwards { flex-direction: column-reverse; } -#keyboard_canvas_wrapper.right_to_left{ +#keyboard_canvas_wrapper.right_to_left { flex-direction: row; } -#keyboard_canvas_wrapper.left_to_right{ +#keyboard_canvas_wrapper.left_to_right { flex-direction: row-reverse; } \ No newline at end of file diff --git a/src/website/css/music_mode_ui.css b/src/website/css/music_mode_ui.css index 174b0c73..f74c09c0 100644 --- a/src/website/css/music_mode_ui.css +++ b/src/website/css/music_mode_ui.css @@ -1,5 +1,4 @@ -#player_info -{ +#player_info { width: 100%; display: none; flex: 1; @@ -9,7 +8,7 @@ transition: var(--music-mode-transition) transform; } -#player_info_background_image{ +#player_info_background_image { width: 100%; height: 100%; position: absolute; @@ -23,13 +22,13 @@ left: 0; } -.player_info_note_icon img{ +.player_info_note_icon img { max-width: 100%; border-radius: 1rem; width: 20rem; } -.player_info_wrapper{ +.player_info_wrapper { display: flex; border-radius: 1rem; align-items: center; @@ -39,7 +38,7 @@ justify-content: center; } -.player_info_wrapper.light_mode{ +.player_info_wrapper.light_mode { backdrop-filter: brightness(1.8) blur(50px) !important; } @@ -47,11 +46,11 @@ margin: 1rem; } -#player_info_detail{ +#player_info_detail { white-space: preserve; } -.player_info_detail_element{ +.player_info_detail_element { display: flex; flex-wrap: nowrap; align-items: center; @@ -62,15 +61,15 @@ display: inline-block; } -.player_info_detail_element b{ +.player_info_detail_element b { margin-right: 1ch; } -.player_info_detail_element.hidden{ +.player_info_detail_element.hidden { display: none; } -.player_info_details_wrapper{ +.player_info_details_wrapper { max-width: 100%; padding: 1rem; display: flex; @@ -83,7 +82,7 @@ white-space: nowrap; } -.marquee span{ +.marquee span { white-space: nowrap; padding-left: 100%; animation: 15s linear infinite marquee; @@ -93,15 +92,15 @@ 0% { transform: translateX(0); } - 100%{ + 100% { transform: translateX(-100%); } } -.player_info_wrapper *{ +.player_info_wrapper * { text-align: center; } -.player_info_show{ +.player_info_show { transform: translateX(0%) !important; } \ No newline at end of file diff --git a/src/website/css/note_canvas.css b/src/website/css/note_canvas.css index 3fe87356..def1fc60 100644 --- a/src/website/css/note_canvas.css +++ b/src/website/css/note_canvas.css @@ -7,12 +7,12 @@ z-index: 1; } -#note_canvas.sideways{ +#note_canvas.sideways { height: 100% !important; width: unset !important; min-height: unset !important; } -#note_canvas.light_mode{ +#note_canvas.light_mode { background: linear-gradient(45deg, var(--top-buttons-color-start), var(--top-buttons-color-end)); } \ No newline at end of file diff --git a/src/website/css/notification/buttons.css b/src/website/css/notification/buttons.css index 818a566c..e97a3a59 100644 --- a/src/website/css/notification/buttons.css +++ b/src/website/css/notification/buttons.css @@ -13,11 +13,11 @@ .notification button:active, -.notification_file_button:active{ +.notification_file_button:active { transform: scale(var(--active-scale)); } -.notification .green_button{ +.notification .green_button { color: green; border-color: green; } \ No newline at end of file diff --git a/src/website/css/notification/inputs.css b/src/website/css/notification/inputs.css index e033a13c..f9fe4181 100644 --- a/src/website/css/notification/inputs.css +++ b/src/website/css/notification/inputs.css @@ -1,5 +1,4 @@ -.notification_input_wrapper -{ +.notification_input_wrapper { display: flex; justify-content: space-between; align-items: center; @@ -13,7 +12,7 @@ input[type='number']::-webkit-inner-spin-button { } .notification input[type='number'], -.notification input[type='text']{ +.notification input[type='text'] { display: block; background: var(--top-buttons-color); border: solid 1px #333; @@ -28,20 +27,20 @@ input[type='number']::-webkit-inner-spin-button { width: auto !important; } -.notification_slider_wrapper{ +.notification_slider_wrapper { display: flex; justify-content: space-between; margin: 0.5rem; } -.notification_slider_wrapper label{ +.notification_slider_wrapper label { margin-right: 2rem; } -.notification_slider_wrapper .settings_visual_wrapper{ +.notification_slider_wrapper .settings_visual_wrapper { margin: 0 !important; } -.notification_slider_wrapper .settings_slider_wrapper{ +.notification_slider_wrapper .settings_slider_wrapper { width: auto !important; } \ No newline at end of file diff --git a/src/website/css/notification/notification.css b/src/website/css/notification/notification.css index fa6d501e..4c733bd1 100644 --- a/src/website/css/notification/notification.css +++ b/src/website/css/notification/notification.css @@ -4,7 +4,7 @@ @import "progresses.css"; @import "toggles.css"; -.notification_field{ +.notification_field { position: absolute; padding: 0; width: 100%; @@ -39,7 +39,7 @@ max-width: 100%; } -.notification.drop{ +.notification.drop { opacity: 1; transform: translate(-50%, 0); } @@ -52,13 +52,13 @@ align-items: center; } -.notification h2{ +.notification h2 { text-align: start; line-height: 2rem; margin: 1rem; } -.notification_content{ +.notification_content { margin: 1rem; min-width: 90%; display: flex; @@ -67,13 +67,13 @@ justify-content: center; } -.notification .close_btn{ +.notification .close_btn { text-align: end; font-weight: bolder; font-size: 2rem; margin-right: 1rem; } -.notification .close_btn:hover{ +.notification .close_btn:hover { cursor: pointer; } \ No newline at end of file diff --git a/src/website/css/notification/progresses.css b/src/website/css/notification/progresses.css index 73272b4e..deb59eb0 100644 --- a/src/website/css/notification/progresses.css +++ b/src/website/css/notification/progresses.css @@ -1,4 +1,4 @@ -.notification .notification_progress_background{ +.notification .notification_progress_background { height: 1rem; margin: 1rem; background: var(--track-color); @@ -7,7 +7,7 @@ box-sizing: content-box; /*HERESY*/ } -.notification .notification_progress{ +.notification .notification_progress { height: 100%; width: 0; background: var(--primary-color); diff --git a/src/website/css/notification/texts.css b/src/website/css/notification/texts.css index 32655048..07c750ca 100644 --- a/src/website/css/notification/texts.css +++ b/src/website/css/notification/texts.css @@ -1,8 +1,8 @@ -.notification p{ +.notification p { margin: 1rem; font-size: var(--notification-font-size); } -.notification label{ +.notification label { font-size: var(--notification-font-size); } \ No newline at end of file diff --git a/src/website/css/notification/toggles.css b/src/website/css/notification/toggles.css index 51d53d3f..f8f42579 100644 --- a/src/website/css/notification/toggles.css +++ b/src/website/css/notification/toggles.css @@ -1,4 +1,4 @@ -.notification_switch_wrapper{ +.notification_switch_wrapper { display: flex; justify-content: space-between; align-items: center; @@ -50,7 +50,7 @@ box-shadow: 0 0 0.125rem rgba(0, 0, 0, 0.2); } -.notification_switch_slider:hover::before{ +.notification_switch_slider:hover::before { border: solid var(--border-color) 1px; filter: brightness(1.2); } diff --git a/src/website/css/sequencer_ui.css b/src/website/css/sequencer_ui.css index 5c146029..c1c4be71 100644 --- a/src/website/css/sequencer_ui.css +++ b/src/website/css/sequencer_ui.css @@ -1,5 +1,4 @@ -#sequencer_controls -{ +#sequencer_controls { width: 80%; position: relative; margin: auto auto 2px; @@ -8,8 +7,7 @@ --sequi-border-radius: var(--primary-border-radius); } -#sequencer_controls #note_progress -{ +#sequencer_controls #note_progress { border-radius: var(--progress-bar-height); background: linear-gradient(185deg, #306, var(--primary-color)); height: 100%; @@ -19,12 +17,11 @@ } -#sequencer_controls .note_progress_light{ +#sequencer_controls .note_progress_light { filter: brightness(3); } -#sequencer_controls #note_time -{ +#sequencer_controls #note_time { position: relative; font-size: calc(var(--progress-bar-height) * 0.8); line-height: var(--progress-bar-height); @@ -34,27 +31,25 @@ width: 100%; } -#sequencer_controls #note_time:hover{ +#sequencer_controls #note_time:hover { cursor: pointer; } -#sequencer_controls .control_buttons -{ +#sequencer_controls .control_buttons { position: relative; display: inline-block; transition: all 0.1s ease; } -#sequencer_controls .control_buttons:active{ +#sequencer_controls .control_buttons:active { transform: scale(var(--active-scale)); } -#sequencer_controls .control_buttons:hover{ +#sequencer_controls .control_buttons:hover { cursor: pointer; } -#sequencer_controls #note_progress_background -{ +#sequencer_controls #note_progress_background { border-radius: var(--progress-bar-height); background: linear-gradient(90deg, #454545, #343434); height: var(--progress-bar-height); @@ -63,14 +58,13 @@ overflow: hidden; } -#sequencer_controls .note_progress_background_light -{ +#sequencer_controls .note_progress_background_light { background: linear-gradient(90deg, #ddd, #bbb) !important; } -#sequencer_controls .lyrics{ +#sequencer_controls .lyrics { position: fixed; - top:0; + top: 0; right: 0; width: 30em; min-width: 30%; @@ -86,13 +80,13 @@ border-radius: var(--sequi-border-radius); } -.lyrics_title_wrapper{ +.lyrics_title_wrapper { background: transparent; top: 0; border-radius: var(--sequi-border-radius); } -.lyrics_selector{ +.lyrics_selector { width: 100%; border: none; font-size: 1.3rem; @@ -100,16 +94,16 @@ background: transparent; } -.lyrics_selector option{ +.lyrics_selector option { background: black; } -.lyrics_show{ +.lyrics_show { transform: scaleX(1) !important; visibility: visible !important; } -.lyrics_text{ +.lyrics_text { scroll-behavior: smooth; overflow-y: auto; max-height: 80%; @@ -132,22 +126,22 @@ font-size: 1rem; } -.lyrics details{ +.lyrics details { overflow: scroll; } -.lyrics details summary{ +.lyrics details summary { position: fixed; width: 100%; text-align: center; } -.lyrics details div{ +.lyrics details div { margin-top: 2rem; line-height: 2rem; } .lyrics details pre, -.lyrics details i{ +.lyrics details i { display: inline; } \ No newline at end of file diff --git a/src/website/css/settings/buttons.css b/src/website/css/settings/buttons.css index b0c79b4c..68ce9b03 100644 --- a/src/website/css/settings/buttons.css +++ b/src/website/css/settings/buttons.css @@ -1,4 +1,4 @@ -.seamless_button{ +.seamless_button { font-size: larger; background: transparent; margin: auto; @@ -7,20 +7,18 @@ user-select: none; } -.settings_button -{ +.settings_button { display: flex; align-items: center; justify-content: center; } -.settings_button span{ +.settings_button span { text-align: end; font-size: larger; } -.seamless_button:hover -{ +.seamless_button:hover { cursor: pointer; text-shadow: 0 0 5px white; } @@ -36,6 +34,6 @@ height: 0; /*this sets no borders which is nice*/ } -.settings_button:hover .gear{ +.settings_button:hover .gear { transform: rotate(45deg); } \ No newline at end of file diff --git a/src/website/css/settings/groups.css b/src/website/css/settings/groups.css index 511824b2..31b618b8 100644 --- a/src/website/css/settings/groups.css +++ b/src/website/css/settings/groups.css @@ -1,10 +1,10 @@ -.settings_groups_parent{ +.settings_groups_parent { display: flex; flex-wrap: wrap; justify-content: space-evenly; } -.settings_group{ +.settings_group { border-radius: var(--settings-border-radius); border: solid #333 1px; margin: 1em; @@ -13,12 +13,12 @@ transition: 0.2s; } -.settings_group:hover{ +.settings_group:hover { filter: brightness(1.2); box-shadow: 0 0 5px var(--top-buttons-color-end); } -.settings_group label, .settings_group p{ +.settings_group label, .settings_group p { margin-top: 1em; display: block; } \ No newline at end of file diff --git a/src/website/css/settings/settings.css b/src/website/css/settings/settings.css index 9d9d4e43..8cd89302 100644 --- a/src/website/css/settings/settings.css +++ b/src/website/css/settings/settings.css @@ -3,7 +3,7 @@ @import "groups.css"; @import "switches.css"; -.settings_menu{ +.settings_menu { --settings-border-radius: 1rem; position: absolute; top: 100%; @@ -21,7 +21,7 @@ border-radius: 0 0 var(--settings-border-radius) var(--settings-border-radius); } -.settings_menu_show{ +.settings_menu_show { display: block !important; transform: scaleX(1) !important; } @@ -31,7 +31,7 @@ user-select: none; } -.settings_menu select{ +.settings_menu select { background: transparent; border: solid 1px transparent; /*to keep the same size when adding one*/ font-size: larger; @@ -39,13 +39,13 @@ padding: .5rem; } -.settings_menu select option{ +.settings_menu select option { background: black; color: white; border: none; } -.settings_menu select:hover{ +.settings_menu select:hover { cursor: pointer; text-shadow: 0 0 5px white; border: 1px solid var(--top-buttons-color-end); diff --git a/src/website/css/settings/sliders.css b/src/website/css/settings/sliders.css index 32aae128..b4b06d19 100644 --- a/src/website/css/settings/sliders.css +++ b/src/website/css/settings/sliders.css @@ -1,4 +1,4 @@ -.settings_slider_wrapper{ +.settings_slider_wrapper { display: flex; align-items: center; width: 100%; @@ -11,7 +11,7 @@ --slider-border-thickness: 1px; } -.settings_visual_wrapper{ +.settings_visual_wrapper { --visual-width: 0%; display: flex; position: relative; @@ -25,7 +25,7 @@ border-radius: var(--track-height); } -.settings_visual_wrapper:has(.settings_slider:active){ +.settings_visual_wrapper:has(.settings_slider:active) { filter: brightness(var(--active-brightness)); } @@ -39,7 +39,7 @@ position: relative; } -.settings_slider:hover{ +.settings_slider:hover { filter: brightness(1.2); } @@ -47,7 +47,7 @@ outline: none; } -.settings_slider + span{ +.settings_slider + span { margin-bottom: 1rem; } @@ -56,7 +56,7 @@ text-align: center; } -.settings_slider_progress{ +.settings_slider_progress { width: calc(var(--visual-width) + 2 * var(--slider-border-thickness)); position: absolute; background: var(--primary-color); @@ -67,7 +67,7 @@ left: calc(-1 * var(--slider-border-thickness)); } -.settings_slider_thumb{ +.settings_slider_thumb { border: solid var(--slider-border-thickness) var(--track-border-color); background: var(--track-color); height: var(--thumb-size); @@ -78,11 +78,11 @@ left: calc(var(--visual-width) - var(--thumb-size) / 2); } -.settings_slider_wrapper:hover .settings_slider_thumb{ +.settings_slider_wrapper:hover .settings_slider_thumb { border-color: var(--primary-color); } .settings_slider_transition .settings_slider_thumb, -.settings_slider_transition .settings_slider_progress{ +.settings_slider_transition .settings_slider_progress { transition: all 0.2s ease; } \ No newline at end of file diff --git a/src/website/css/settings/switches.css b/src/website/css/settings/switches.css index 48704531..d585cea8 100644 --- a/src/website/css/settings/switches.css +++ b/src/website/css/settings/switches.css @@ -1,10 +1,10 @@ -.switch_label{ +.switch_label { display: flex; justify-content: space-between; margin-top: 1em; } -.switch_label label{ +.switch_label label { margin-right: 1em; font-size: large; } @@ -54,7 +54,7 @@ box-shadow: 0 0 0.125rem rgba(0, 0, 0, 0.2); } -.switch_slider:hover::before{ +.switch_slider:hover::before { border: solid var(--border-color) 1px; filter: brightness(1.2); } @@ -68,10 +68,10 @@ input:checked + .switch_slider::before { transform: translate(calc(var(--track-width) - var(--thumb-size) * 0.66), -50%); } -.switch:active .switch_slider::before{ +.switch:active .switch_slider::before { transform: translate(calc(var(--track-width) - var(--thumb-size) * 1.2), -50%); } -.switch:active input:checked + .switch_slider::before{ +.switch:active input:checked + .switch_slider::before { transform: translate(calc(var(--track-width) - var(--thumb-size) * 0.95), -50%); } \ No newline at end of file diff --git a/src/website/css/soundfont_mixer.css b/src/website/css/soundfont_mixer.css index e40d08ce..ca908b08 100644 --- a/src/website/css/soundfont_mixer.css +++ b/src/website/css/soundfont_mixer.css @@ -1,4 +1,4 @@ -.soundfont_mixer{ +.soundfont_mixer { top: 100%; position: absolute; background: var(--top-color); @@ -10,13 +10,13 @@ min-width: fit-content; } -.soundfont_mixer_list{ +.soundfont_mixer_list { margin: 1em; border-radius: 0.3em; - min-height: 4em ; + min-height: 4em; } -.soundfont_mixer .soundfont_entry{ +.soundfont_mixer .soundfont_entry { background: var(--top-buttons-color); margin: 0.2em; border-radius: var(--primary-border-radius); @@ -27,19 +27,19 @@ justify-content: center; } -.soundfont_mixer .soundfont_entry_wrapper{ +.soundfont_mixer .soundfont_entry_wrapper { display: flex; justify-content: space-between; } -.soundfont_mixer .soundfont_entry_button{ +.soundfont_mixer .soundfont_entry_button { } -.soundfont_mixer .soundfont_entry_button:hover{ +.soundfont_mixer .soundfont_entry_button:hover { cursor: pointer; } -.soundfont_mixer .action_buttons_wrapper{ +.soundfont_mixer .action_buttons_wrapper { display: flex; justify-content: space-between; } \ No newline at end of file diff --git a/src/website/css/style.css b/src/website/css/style.css index 6cbaef8f..2d7a3110 100644 --- a/src/website/css/style.css +++ b/src/website/css/style.css @@ -1,5 +1,6 @@ @import "settings/settings.css"; -@import "keyboard/keyboard.css"; /*DO NOT CHANGE POSITION OF THIS IMPORT*/ +@import "keyboard/keyboard.css"; +/*DO NOT CHANGE POSITION OF THIS IMPORT*/ @import "sequencer_ui.css"; @import "music_mode_ui.css"; @import "synthesizer_ui/synthesizer_ui.css"; @@ -28,7 +29,7 @@ --active-scale: 0.9; /*thanks for this bezier mozilla devtools*/ - --bouncy-transition: cubic-bezier(.68,-0.55,.27,1.55); + --bouncy-transition: cubic-bezier(.68, -0.55, .27, 1.55); --music-mode-transition: 0.5s ease; @@ -40,7 +41,7 @@ box-sizing: border-box; } -pre{ +pre { font-family: monospace !important; } @@ -52,18 +53,18 @@ html, body { scrollbar-width: thin; } -body.load{ +body.load { transition: background 0.2s; } body.no_scroll, -html.no_scroll{ +html.no_scroll { max-height: 100% !important; height: 100% !important; overflow: hidden !important; } -.spessasynth_main{ +.spessasynth_main { display: flex; flex-direction: column; height: 100%; @@ -76,7 +77,7 @@ html.no_scroll{ --shadow-color: #000; } -.spessasynth_main.light_mode{ +.spessasynth_main.light_mode { --primary-color: #a93bff; --border-color: #510087; --track-color: #ccc; @@ -84,44 +85,44 @@ html.no_scroll{ --shadow-color: #fff; } -a{ +a { text-decoration: none; color: #546fff; } -::-webkit-scrollbar{ +::-webkit-scrollbar { background-color: #000; width: 0.3em; } -::-webkit-scrollbar-thumb{ +::-webkit-scrollbar-thumb { background-color: #777; border-radius: 50px; } /*Bottom*/ -.bottom_part{ +.bottom_part { margin-top: 5px; } -button{ +button { -webkit-user-select: none; user-select: none; } -.hidden{ +.hidden { display: none !important; } -.secret_video{ +.secret_video { position: absolute; width: 100%; left: 0; z-index: 0; } -.drop_prompt{ +.drop_prompt { flex-direction: column; display: flex; justify-content: center; @@ -155,11 +156,15 @@ button{ animation: spin 1s ease-in-out infinite; } -.loading.done{ +.loading.done { transform: translateY(-100%); } @keyframes spin { - 0% { transform: rotate(0deg); } - 100% {transform: rotate(360deg); } + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } } \ No newline at end of file diff --git a/src/website/css/synthesizer_ui/synthesizer_ui.css b/src/website/css/synthesizer_ui/synthesizer_ui.css index 5e5d26fc..2e0787a5 100644 --- a/src/website/css/synthesizer_ui/synthesizer_ui.css +++ b/src/website/css/synthesizer_ui/synthesizer_ui.css @@ -2,8 +2,7 @@ @import "voice_selector.css"; @import "synthui_button.css"; -#synthetizer_controls -{ +#synthetizer_controls { --voice-meter-height: 2.5em; --synthui-background: black; --synthui-border-radius: var(--primary-border-radius); @@ -15,7 +14,7 @@ --synthui-margin: 0.2em; } -.wrapper{ +.wrapper { height: 80%; padding: 2%; display: flex; @@ -23,7 +22,7 @@ justify-content: space-evenly; } -.controls_wrapper{ +.controls_wrapper { display: flex; flex-wrap: wrap; align-items: stretch; @@ -31,14 +30,12 @@ margin: 1em; } -.main_controller_element -{ +.main_controller_element { margin-left: 0.5em; margin-right: 0.5em; } -.controller_element -{ +.controller_element { position: relative; height: var(--voice-meter-height); flex: 1; @@ -46,7 +43,7 @@ margin: var(--synthui-margin); } -.synthui_controller{ +.synthui_controller { left: 0; display: none; position: absolute; @@ -60,28 +57,26 @@ padding: 0.5em; } -.synthui_controller_light -{ +.synthui_controller_light { background: linear-gradient(115deg, rgb(200, 200, 200), rgba(255, 255, 255), rgba(200, 200, 200)) !important; } -.synthui_controller_show{ +.synthui_controller_show { transform: scaleX(1) !important; } -.channel_controller{ +.channel_controller { display: flex; align-items: stretch; flex-wrap: wrap; transition: 0.2s ease; } -.channel_controller.no_voices{ +.channel_controller.no_voices { filter: brightness(0.8); } -.mute_button -{ +.mute_button { flex: 0; display: flex; justify-content: center; @@ -94,23 +89,23 @@ color: black; } -.mute_button svg{ +.mute_button svg { transition: transform 0.2s ease; } -.mute_button:active{ +.mute_button:active { filter: brightness(0.8); transform: scale(var(--active-scale)); } -.mute_button:hover{ +.mute_button:hover { cursor: pointer; } -.mute_button:hover svg{ +.mute_button:hover svg { transform: scale(1.1) rotate(5deg); } -.mute_button:hover path{ +.mute_button:hover path { filter: drop-shadow(0 0 1px currentColor); } \ No newline at end of file diff --git a/src/website/css/synthesizer_ui/synthui_button.css b/src/website/css/synthesizer_ui/synthui_button.css index cd1d1109..5651e097 100644 --- a/src/website/css/synthesizer_ui/synthui_button.css +++ b/src/website/css/synthesizer_ui/synthui_button.css @@ -1,5 +1,4 @@ -.synthui_button -{ +.synthui_button { background: #000; border: 1px #333 solid; margin: var(--synthui-margin); @@ -14,20 +13,20 @@ justify-content: center; transition: all 0.1s ease; } -.synthui_button:hover -{ + +.synthui_button:hover { cursor: pointer; background: #111; color: #fff; text-shadow: 0 0 5px white; } -.synthui_button:active{ +.synthui_button:active { filter: brightness(0.9); transform: scale(var(--active-scale)); } -.synthui_button_light{ +.synthui_button_light { border-radius: var(--synthui-border-radius); background: #fff; border: 1px #ccc solid; @@ -44,14 +43,13 @@ transition: all 0.1s ease; } -.synthui_button_light:hover -{ +.synthui_button_light:hover { cursor: pointer; color: #666; text-shadow: 0 0 5px #666; } -.synthui_button_light:active{ +.synthui_button_light:active { filter: brightness(0.9); transform: scale(var(--active-scale)); } \ No newline at end of file diff --git a/src/website/css/synthesizer_ui/voice_meter.css b/src/website/css/synthesizer_ui/voice_meter.css index 5ee1f6f2..7f0a20c3 100644 --- a/src/website/css/synthesizer_ui/voice_meter.css +++ b/src/website/css/synthesizer_ui/voice_meter.css @@ -1,4 +1,4 @@ -.voice_meter{ +.voice_meter { cursor: not-allowed; border-width: 1px; border-style: solid; @@ -7,11 +7,11 @@ overflow: hidden; } -.voice_meter.editable{ +.voice_meter.editable { cursor: e-resize; } -.voice_meter .voice_meter_bar{ +.voice_meter .voice_meter_bar { position: relative; display: block; height: 100%; @@ -20,15 +20,15 @@ border-radius: var(--notification-border-radius); } -.voice_meter_light_color{ +.voice_meter_light_color { filter: brightness(1.4); } -.voice_meter_bar_smooth{ +.voice_meter_bar_smooth { transition: width ease-in-out 100ms; } -.voice_meter .voice_meter_text{ +.voice_meter .voice_meter_text { position: absolute; z-index: 1; height: var(--voice-meter-height); @@ -44,10 +44,10 @@ text-overflow: ellipsis; } -.voice_meter .voice_meter_text_light{ +.voice_meter .voice_meter_text_light { color: black; } -.locked_meter{ +.locked_meter { color: red; } \ No newline at end of file diff --git a/src/website/css/synthesizer_ui/voice_selector.css b/src/website/css/synthesizer_ui/voice_selector.css index a5ec18f1..feafcdc4 100644 --- a/src/website/css/synthesizer_ui/voice_selector.css +++ b/src/website/css/synthesizer_ui/voice_selector.css @@ -1,4 +1,4 @@ -.voice_selector{ +.voice_selector { border: #777 1px solid; line-height: var(--voice-meter-height); background-color: var(--synthui-background); @@ -10,33 +10,33 @@ overflow: hidden; } -.voice_selector_light{ +.voice_selector_light { border: #111 1px solid; background: linear-gradient(170deg, #bbb, #fff); color: black; } -.voice_selector:hover{ +.voice_selector:hover { cursor: pointer; background: #111; color: #fff; text-shadow: 0 0 5px white; } -.voice_selector_light:hover{ +.voice_selector_light:hover { background: #fff; color: #444; } -.selector_options:hover{ +.selector_options:hover { display: block; } -.locked_selector{ +.locked_selector { color: red; } -.voice_reset{ +.voice_reset { min-width: var(--voice-meter-height); display: flex; justify-content: center; @@ -50,12 +50,12 @@ cursor: pointer; } -.voice_reset:active{ +.voice_reset:active { filter: brightness(0.8); transform: scale(var(--active-scale)); } -.voice_reset:hover svg{ +.voice_reset:hover svg { transform: scale(1.1) rotate(5deg); } @@ -63,7 +63,7 @@ color: black; } -.voice_selector_wrapper{ +.voice_selector_wrapper { position: fixed; z-index: calc(var(--top-index) - 20); backdrop-filter: brightness(0.9) blur(2px); @@ -89,12 +89,12 @@ max-height: 90%; } -.voice_selector_search_wrapper{ +.voice_selector_search_wrapper { display: flex; margin: 1rem; } -.voice_selector_window input{ +.voice_selector_window input { border: solid 1px #333; font-size: 1rem; margin: .1rem; @@ -105,14 +105,14 @@ flex: 1; } -.voice_selector_table_wrapper{ +.voice_selector_table_wrapper { max-height: 70vh; overflow-y: scroll; overflow-x: hidden; padding: 1rem; } -.voice_selector_table{ +.voice_selector_table { width: 100%; font-size: 1.1rem; padding: .5rem; @@ -120,18 +120,18 @@ } -.voice_selector_table th{ +.voice_selector_table th { filter: none !important; text-align: start; line-height: 2rem; } -.voice_selector_preset_name{ +.voice_selector_preset_name { text-align: end; display: block; } -.voice_selector_table tr{ +.voice_selector_table tr { transition: all 50ms ease; border-radius: var(--primary-border-radius); } @@ -141,6 +141,6 @@ cursor: pointer; } -.voice_selector_selected{ +.voice_selector_selected { background: var(--border-color) !important; } \ No newline at end of file diff --git a/src/website/css/top_part.css b/src/website/css/top_part.css index 089cbc37..7fe69e5d 100644 --- a/src/website/css/top_part.css +++ b/src/website/css/top_part.css @@ -1,6 +1,5 @@ /*Top*/ -.top_part -{ +.top_part { --top-part-border-radius: 1.5rem; position: relative; background: var(--top-color); @@ -22,7 +21,7 @@ -webkit-user-select: none; } -.top_part_hidden{ +.top_part_hidden { position: fixed; width: 100%; transform: scaleY(0); @@ -32,7 +31,7 @@ border-radius: 0 0 0 var(--top-part-border-radius); } -.top_part.synthui_shown{ +.top_part.synthui_shown { border-radius: 0; } @@ -40,8 +39,7 @@ input[type="file"] { display: none; } -#title -{ +#title { user-select: text; position: relative; z-index: 1; @@ -52,8 +50,7 @@ input[type="file"] { text-shadow: 0 0 5px var(--font-color); } -#progress_bar -{ +#progress_bar { background: #206; display: block; position: absolute; @@ -81,8 +78,7 @@ input[type="file"] { justify-content: space-around; } -.midi_and_sf_controller label -{ +.midi_and_sf_controller label { padding: 6px; border-radius: var(--primary-border-radius); cursor: pointer; @@ -95,19 +91,17 @@ input[type="file"] { transition: all 0.1s ease; } -.midi_and_sf_controller label:active{ +.midi_and_sf_controller label:active { filter: brightness(0.9); transform: scale(var(--active-scale)); } -#sf_selector option -{ +#sf_selector option { background: #000; text-align: center; } -#sf_selector -{ +#sf_selector { display: block; border: none; font-size: 1em; @@ -119,7 +113,7 @@ input[type="file"] { font-weight: bolder; } -.show_top_button{ +.show_top_button { background: var(--top-buttons-color); width: fit-content; padding: 0.1em 2em; @@ -137,11 +131,11 @@ input[type="file"] { cursor: pointer; } -.show_top_button:hover{ +.show_top_button:hover { filter: brightness(1.1); transform: scaleY(1.3); } -.show_top_button.shown{ +.show_top_button.shown { opacity: 1; } \ No newline at end of file diff --git a/src/website/dls_to_sf2_converter.html b/src/website/dls_to_sf2_converter.html index fd39b87d..6325a822 100644 --- a/src/website/dls_to_sf2_converter.html +++ b/src/website/dls_to_sf2_converter.html @@ -1,48 +1,50 @@ - - - + + + - - - - - + + + + + - - - + + + - - - + + + DLS to SF2 Online Converter - - + +

DLS to SF2 Soundfont Converter

- Convert a DLS soundfont into an SF2 one, online for free! Created by me using SpessaSynth + Convert a DLS soundfont into an SF2 one, online for free! Created by me using SpessaSynth

- + - + - +
- + \ No newline at end of file diff --git a/src/website/js/dls_to_sf2.js b/src/website/js/dls_to_sf2.js index 86206442..061d0005 100644 --- a/src/website/js/dls_to_sf2.js +++ b/src/website/js/dls_to_sf2.js @@ -1,45 +1,50 @@ -import { loadSoundFont } from '../../spessasynth_lib/soundfont/load_soundfont.js' +import { loadSoundFont } from "../../spessasynth_lib/soundfont/load_soundfont.js"; const message = document.getElementById("message"); -document.getElementById("dls_upload").oninput = e => { - if(!e.target.files) +document.getElementById("dls_upload").oninput = e => +{ + if (!e.target.files) { return; } const file = e.target.files[0]; - if(file.type.endsWith(".dls")) + if (file.type.endsWith(".dls")) { message.innerText = "Not a DLS file."; return; } document.getElementById("dls_upload_btn").innerText = file.name; message.innerText = "Loading..."; - setTimeout(async () => { + setTimeout(async () => + { let sfont; try { sfont = loadSoundFont(await file.arrayBuffer()); - } - catch (e) + } catch (e) { message.style.color = "red"; message.innerText = `Error: ${e.message}`; return; } document.getElementById("sf_info").classList.remove("hidden"); - document.getElementById("dls_name").innerText = sfont.soundFontInfo["INAM"] || "Unnamed" ; - document.getElementById("dls_description").innerText = (sfont.soundFontInfo["ICMT"] || "No description").replace("\nConverted from DLS to SF2 with SpessaSynth", ""); + document.getElementById("dls_name").innerText = sfont.soundFontInfo["INAM"] || "Unnamed"; + document.getElementById("dls_description").innerText = (sfont.soundFontInfo["ICMT"] || "No description").replace( + "\nConverted from DLS to SF2 with SpessaSynth", + "" + ); document.getElementById("dls_presets").innerText = sfont.presets.length; document.getElementById("dls_samples").innerText = sfont.samples.length; message.innerText = "Loaded!"; - + const convert = document.getElementById("convert"); convert.classList.remove("hidden"); - convert.innerText = `Convert ${file.name}` - - convert.onclick = () => { + convert.innerText = `Convert ${file.name}`; + + convert.onclick = () => + { const binary = sfont.write(); - const blob = new Blob([binary.buffer], {type: "audio/soundfont"}); + const blob = new Blob([binary.buffer], { type: "audio/soundfont" }); const url = URL.createObjectURL(blob); const name = file.name.replace("dls", "sf2"); const a = document.createElement("a"); @@ -52,6 +57,6 @@ document.getElementById("dls_upload").oninput = e => { down.appendChild(a); message.style.color = "green"; message.innerText = `Success!`; - } + }; }); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/website/js/locale/locale_files/README.md b/src/website/js/locale/locale_files/README.md index c605f9a8..100a800d 100644 --- a/src/website/js/locale/locale_files/README.md +++ b/src/website/js/locale/locale_files/README.md @@ -5,17 +5,20 @@ I welcome contributions from translators! To add a new locale, please follow these steps: 1. **Create a New Locale Folder** - - Create a new folder in this folder named `locale_[your language 2-letter ISO code]`. For example, for German, the folder name would be `locale_de`. + - Create a new folder in this folder named `locale_[your language 2-letter ISO code]`. For example, for German, the + folder name would be `locale_de`. 2. **Copy an Existing Locale** - Copy the contents of `locale_en` (or any other existing locale you want to translate from) into your new folder. 3. **Update `locale.js`** - Open `locale.js` in your new folder. - - Rename `export const localeEnglish` in `locale.js` to reflect your language. For example, `localeEnglish` would become `localeGerman` for German. + - Rename `export const localeEnglish` in `locale.js` to reflect your language. For example, `localeEnglish` would + become `localeGerman` for German. 4. **Translate!** - - Translate all the strings in the `locale.js` file and all `.js` files in the folders. Make sure to leave the object keys unchanged. + - Translate all the strings in the `locale.js` file and all `.js` files in the folders. Make sure to leave the + object keys unchanged. - You may add comments to indicate who translated the text, e.g., `// translated by: XYZ`. 5. **Update `locale_list.js`** @@ -23,9 +26,11 @@ I welcome contributions from translators! To add a new locale, please follow the - Add a new entry for your locale. For example, for German, add: `"de": localeGerman,`. 6. **Submit a Pull Request** - - After completing the translation and updates, create a pull request with your changes. Thank you for helping SpessaSynth! + - After completing the translation and updates, create a pull request with your changes. Thank you for helping + SpessaSynth! **If you have any questions about this guide or something is unclear, let me know by opening an issue!** -> **Note:** Strings containing placeholders, like `Channel {0}`, should keep the placeholders intact. They are used for formatting and should not be altered. +> **Note:** Strings containing placeholders, like `Channel {0}`, should keep the placeholders intact. They are used for +> formatting and should not be altered. diff --git a/src/website/js/locale/locale_files/locale_en/export_audio.js b/src/website/js/locale/locale_files/locale_en/export_audio.js index de4e340b..43fdb430 100644 --- a/src/website/js/locale/locale_files/locale_en/export_audio.js +++ b/src/website/js/locale/locale_files/locale_en/export_audio.js @@ -3,7 +3,7 @@ export const exportAudio = { title: "Save Audio", description: "Save the composition to various formats" }, - + formats: { title: "Choose format", formats: { @@ -17,13 +17,13 @@ export const exportAudio = { confirm: "Export", normalizeVolume: { title: "Normalize volume", - description: "Keep the volume at the same level, no matter how loud or quiet the MIDI is. Recommended.", + description: "Keep the volume at the same level, no matter how loud or quiet the MIDI is. Recommended." }, additionalTime: { title: "Additional time (s)", - description: "Additional time at the end of the song to allow for the sound to fade. (seconds)", + description: "Additional time at the end of the song to allow for the sound to fade. (seconds)" }, - + separateChannels: { title: "Separate channels", description: "Save each channel as a separate file. Useful for things like oscilloscope viewers. Note that this disables reverb and chorus.", @@ -40,23 +40,23 @@ export const exportAudio = { exportMessage: { message: "Exporting WAV audio...", estimated: "Remaining:", - convertWav: "Converting to wav...", + convertWav: "Converting to wav..." } }, - + midi: { button: { title: "MIDI (.mid)", description: "Export the MIDI file with the controller and instrument changes applied" } }, - + soundfont: { button: { title: "SoundFont (.sf2)", description: "Export a SoundFont2 file" }, - + options: { title: "SF export options", confirm: "Export", @@ -75,14 +75,14 @@ export const exportAudio = { } } }, - + rmidi: { button: { title: "Embedded MIDI (.rmi)", description: "Export the modified MIDI with the embedded trimmed soundfont as a single file. " + "Note that this format isn't widely supported" }, - + progress: { title: "Exporting embeded MIDI...", loading: "Loading Soundfont and MIDI...", @@ -91,7 +91,7 @@ export const exportAudio = { saving: "Saving RMIDI...", done: "Done!" }, - + options: { title: "RMIDI export options", confirm: "Export", @@ -105,7 +105,7 @@ export const exportAudio = { }, bankOffset: { title: "Bank offset", - description: "The bank offset of the file. Value of 0 is recommended. Only change if you know what you're doing.", + description: "The bank offset of the file. Value of 0 is recommended. Only change if you know what you're doing." }, adjust: { title: "Adjust MIDI", @@ -117,7 +117,7 @@ export const exportAudio = { metadata: { songTitle: { title: "Title:", - description: "The song's title", + description: "The song's title" }, album: { title: "Album:", @@ -129,7 +129,7 @@ export const exportAudio = { }, albumCover: { title: "Album cover:", - description: "The song's album cover", + description: "The song's album cover" }, creationDate: { title: "Created:", @@ -149,4 +149,4 @@ export const exportAudio = { } } } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/website/js/locale/locale_files/locale_en/locale.js b/src/website/js/locale/locale_files/locale_en/locale.js index db2f7cb9..98fc541f 100644 --- a/src/website/js/locale/locale_files/locale_en/locale.js +++ b/src/website/js/locale/locale_files/locale_en/locale.js @@ -1,8 +1,8 @@ -import { settingsLocale } from './settings/settings.js' -import { musicPlayerModeLocale } from './music_player_mode.js' -import { synthesizerControllerLocale } from './synthesizer_controller/synthesizer_controller.js' -import { sequencerControllerLocale } from './sequencer_controller.js' -import { exportAudio } from './export_audio.js' +import { settingsLocale } from "./settings/settings.js"; +import { musicPlayerModeLocale } from "./music_player_mode.js"; +import { synthesizerControllerLocale } from "./synthesizer_controller/synthesizer_controller.js"; +import { sequencerControllerLocale } from "./sequencer_controller.js"; +import { exportAudio } from "./export_audio.js"; /** * @@ -13,7 +13,7 @@ export const localeEnglish = { // title messsage titleMessage: "SpessaSynth: SoundFont2 Javascript Synthesizer", demoTitleMessage: "SpessaSynth: SoundFont2 Javascript Synthesizer Online Demo", - + synthInit: { genericLoading: "Loading...", loadingSoundfont: "Loading SoundFont...", @@ -23,22 +23,22 @@ export const localeEnglish = { noWebAudio: "Your browser does not support Web Audio.", done: "Ready!" }, - + // top bar buttons midiUploadButton: "Upload your MIDI files", - + exportAudio: exportAudio, - + yes: "Yes", no: "No", - - + + demoSoundfontUploadButton: "Upload the soundfont", demoGithubPage: "Project's page", demoSongButton: "Demo Song", credits: "Credits", dropPrompt: "Drop files here...", - + warnings: { outOfMemory: "Your browser ran out of memory. Consider using Firefox or SF3 soundfont instead. (see console for error).", noMidiSupport: "No MIDI ports detected, this functionality will be disabled.", @@ -47,14 +47,14 @@ export const localeEnglish = { }, hideTopBar: { title: "Hide top bar", - description: "Hide the top (title) bar to provide a more seamless experience", + description: "Hide the top (title) bar to provide a more seamless experience" }, - + convertDls: { title: "DLS Conversion", message: "Looks like you've uploaded a DLS file. Do you want to convert it to SF2?" }, - + // all translations split up musicPlayerMode: musicPlayerModeLocale, settings: settingsLocale, diff --git a/src/website/js/locale/locale_files/locale_en/sequencer_controller.js b/src/website/js/locale/locale_files/locale_en/sequencer_controller.js index 9b576239..d0afc48e 100644 --- a/src/website/js/locale/locale_files/locale_en/sequencer_controller.js +++ b/src/website/js/locale/locale_files/locale_en/sequencer_controller.js @@ -6,9 +6,9 @@ export const sequencerControllerLocale = { lyrics: { show: "Show lyrics", title: "Decoded text", - noLyrics : "No lyrics available...", + noLyrics: "No lyrics available...", otherText: { title: "Other text" } } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/website/js/locale/locale_files/locale_en/settings/keyboard_settings.js b/src/website/js/locale/locale_files/locale_en/settings/keyboard_settings.js index 2e85ffe2..e6e8589c 100644 --- a/src/website/js/locale/locale_files/locale_en/settings/keyboard_settings.js +++ b/src/website/js/locale/locale_files/locale_en/settings/keyboard_settings.js @@ -1,30 +1,30 @@ export const keyboardSettingsLocale = { title: "MIDI Keyboard settings", - + selectedChannel: { title: "Selected channel", description: "The channel keyboard sends messages to", channelOption: "Channel {0}" }, - + keyboardSize: { title: "Keyboard size", description: "The range of keys shown on the keyboard. Adjusts the MIDI note size accordingly", - + full: "128 keys (full)", piano: "88 keys (piano)", fiveOctaves: "5 octaves", useSongKeyRange: "Use song's key range", - twoOctaves: "Two octaves", + twoOctaves: "Two octaves" }, - + toggleTheme: { title: "Use dark theme", - description: "Use the dark MIDI keyboard theme", + description: "Use the dark MIDI keyboard theme" }, - + show: { title: "Show", description: "Show/hide MIDI keyboard" } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/website/js/locale/locale_files/locale_en/settings/midi_settings.js b/src/website/js/locale/locale_files/locale_en/settings/midi_settings.js index 1b65a01a..0f808985 100644 --- a/src/website/js/locale/locale_files/locale_en/settings/midi_settings.js +++ b/src/website/js/locale/locale_files/locale_en/settings/midi_settings.js @@ -1,15 +1,15 @@ export const midiSettingsLocale = { title: "MIDI settings", - + midiInput: { title: "MIDI input", description: "The port to listen on for MIDI messages", disabled: "Disabled" }, - + midiOutput: { title: "MIDI output", description: "The port to play the MIDI file to", disabled: "Use SpessaSynth" } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/website/js/locale/locale_files/locale_en/settings/renderer_settings.js b/src/website/js/locale/locale_files/locale_en/settings/renderer_settings.js index 78f6c23f..6b910542 100644 --- a/src/website/js/locale/locale_files/locale_en/settings/renderer_settings.js +++ b/src/website/js/locale/locale_files/locale_en/settings/renderer_settings.js @@ -2,46 +2,46 @@ export const rendererSettingsLocale = { title: "Renderer settings", noteFallingTime: { title: "Note falling time (miliseconds)", - description: "How fast the notes fall (visually)", + description: "How fast the notes fall (visually)" }, - + waveformThickness: { title: "Waveform line thickness (px)", description: "How thick the waveform lines are" }, - + waveformSampleSize: { title: "Waveform sample size", description: "How detailed the waveforms are (Note: high values might impact performance)" }, - + waveformAmplifier: { title: "Waveform amplifier", description: "How vibrant the waveforms are" }, - + toggleWaveformsRendering: { title: "Enable waveforms rendering", description: "Enable rendering the channel waveforms (colorful lines showing audio)" }, - + toggleNotesRendering: { title: "Enable notes rendering", - description: "Enable rendering of the falling notes when playing a MIDI file", + description: "Enable rendering of the falling notes when playing a MIDI file" }, - + toggleDrawingActiveNotes: { title: "Enable drawing active notes", description: "Enable notes lighting up and glowing when they get pressed" }, - + toggleDrawingVisualPitch: { title: "Enable drawing visual pitch", description: "Enable 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 +}; \ No newline at end of file diff --git a/src/website/js/locale/locale_files/locale_en/settings/settings.js b/src/website/js/locale/locale_files/locale_en/settings/settings.js index fdc8bdd0..f4cf5b36 100644 --- a/src/website/js/locale/locale_files/locale_en/settings/settings.js +++ b/src/website/js/locale/locale_files/locale_en/settings/settings.js @@ -1,6 +1,6 @@ -import { rendererSettingsLocale } from './renderer_settings.js' -import { keyboardSettingsLocale } from './keyboard_settings.js' -import { midiSettingsLocale } from './midi_settings.js' +import { rendererSettingsLocale } from "./renderer_settings.js"; +import { keyboardSettingsLocale } from "./keyboard_settings.js"; +import { midiSettingsLocale } from "./midi_settings.js"; /** * @type {CompleteSettingsLocale} @@ -8,24 +8,24 @@ import { midiSettingsLocale } from './midi_settings.js' export const settingsLocale = { toggleButton: "Settings", mainTitle: "Program settings", - + rendererSettings: rendererSettingsLocale, keyboardSettings: keyboardSettingsLocale, midiSettings: midiSettingsLocale, - + interfaceSettings: { title: "Interface settings", - + toggleTheme: { title: "Use dark theme", description: "Enable the dark theme for the interface" }, - + selectLanguage: { title: "Language", description: "Change the program language" }, - + layoutDirection: { title: "Layout direction", description: "The layout direction of the renderer and keyboard", @@ -33,7 +33,7 @@ export const settingsLocale = { downwards: "Downwards", upwards: "Upwards", leftToRight: "Left to right", - rightToLeft: "Right to left", + rightToLeft: "Right to left" } } } diff --git a/src/website/js/locale/locale_files/locale_en/synthesizer_controller/channel_controller.js b/src/website/js/locale/locale_files/locale_en/synthesizer_controller/channel_controller.js index 44b4c133..e74f0b61 100644 --- a/src/website/js/locale/locale_files/locale_en/synthesizer_controller/channel_controller.js +++ b/src/website/js/locale/locale_files/locale_en/synthesizer_controller/channel_controller.js @@ -3,71 +3,71 @@ export const channelControllerLocale = { title: "Voices: ", description: "The current amount of voices playing on channel {0}" }, - + pitchBendMeter: { title: "Pitch: ", description: "The current pitch bend applied to channel {0}" }, - + panMeter: { title: "Pan: ", description: "The current stereo panning applied to channel {0} (right-click to lock)" }, - + expressionMeter: { title: "Expression: ", description: "The current expression (loudness) of channel {0} (right-click to lock)" }, - + volumeMeter: { title: "Volume: ", description: "The current volume of channel {0} (right-click to lock)" }, - + modulationWheelMeter: { title: "Mod wheel: ", description: "The current modulation (usually vibrato) depth of channel {0} (right-click to lock)" }, - + chorusMeter: { title: "Chorus: ", description: "The current level of chorus effect applied to channel {0} (right-click to lock)" }, - + reverbMeter: { title: "Reverb: ", description: "The current level of reverb effect applied to channel {0} (right-click to lock)" }, - + filterMeter: { title: "Filter: ", description: "The current level of low-pass filter cutoff applied to channel {0} (right-click to lock)" }, - + transposeMeter: { title: "Transpose: ", description: "The current transposition (key shift) of channel {0}" }, - + presetSelector: { description: "Change the patch (instrument) channel {0} is using", selectionPrompt: "Change instrument for channel {0}", searchPrompt: "Search..." }, - + presetReset: { description: "Unlock channel {0} to allow program changes" }, - + soloButton: { description: "Solo on channel {0}" }, - + muteButton: { description: "Mute/unmute channel {0}" }, - + drumToggleButton: { description: "Toggle drums on channel {0}" } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/website/js/locale/locale_files/locale_en/synthesizer_controller/synthesizer_controller.js b/src/website/js/locale/locale_files/locale_en/synthesizer_controller/synthesizer_controller.js index 53bee81a..31f3472c 100644 --- a/src/website/js/locale/locale_files/locale_en/synthesizer_controller/synthesizer_controller.js +++ b/src/website/js/locale/locale_files/locale_en/synthesizer_controller/synthesizer_controller.js @@ -1,4 +1,4 @@ -import { channelControllerLocale } from './channel_controller.js' +import { channelControllerLocale } from "./channel_controller.js"; /** * @@ -9,60 +9,60 @@ export const synthesizerControllerLocale = { title: "Synthesizer controller", description: "Show the synthesizer controller" }, - + // meters mainVoiceMeter: { title: "Voices: ", - description: "The total amount of voices currently playing", + description: "The total amount of voices currently playing" }, - + mainVolumeMeter: { title: "Volume: ", description: "The current master volume of the synthesizer" }, - + mainPanMeter: { title: "Pan: ", description: "The current master stereo panning of the synthesizer" }, - + mainTransposeMeter: { title: "Transpose: ", description: "Transposes the synthesizer (in semitones or keys)" }, - + // buttons midiPanic: { title: "MIDI Panic", description: "Stops all voices immediately" }, - + systemReset: { title: "System reset", description: "Resets all controllers to their default values" }, - + blackMidiMode: { title: "Black MIDI mode", description: "Toggles the High Performance Mode, simplifying the look and killing the notes faster" }, - + disableCustomVibrato: { title: "Disable custom vibrato", description: "Disables the custom (NRPN) Vibrato permamently. Reload the website to reenable it" }, - + helpButton: { title: "Help", description: "Opens an external website with the usage guide" }, - + interpolation: { description: "Select the synthesizer's interpolation method", linear: "Linear Interpolation", nearestNeighbor: "Nearest neighbor", cubic: "Cubic Interpolation" }, - + channelController: channelControllerLocale -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/website/js/locale/locale_files/locale_ja/export_audio.js b/src/website/js/locale/locale_files/locale_ja/export_audio.js index 6a487570..2b8aa1c5 100644 --- a/src/website/js/locale/locale_files/locale_ja/export_audio.js +++ b/src/website/js/locale/locale_files/locale_ja/export_audio.js @@ -3,7 +3,7 @@ export const exportAudio = { title: "音声を保存", description: "音声をWAV、MIDI、SF2、またはRMIファイルとして保存" }, - + formats: { title: "フォーマットを選択", formats: { @@ -17,13 +17,13 @@ export const exportAudio = { confirm: "エクスポート", normalizeVolume: { title: "音量の正規化", - description: "MIDIの音量にかかわらず、音量を一定に保ちます。推奨設定です。", + description: "MIDIの音量にかかわらず、音量を一定に保ちます。推奨設定です。" }, additionalTime: { title: "追加時間(秒)", - description: "音がフェードアウトするために曲の最後に追加する時間です。(秒)", + description: "音がフェードアウトするために曲の最後に追加する時間です。(秒)" }, - + separateChannels: { title: "チャンネルを分割", description: "各チャンネルを別々のファイルとして保存します。オシロスコープビューアなどに便利です。このオプションを使用するとリバーブやコーラスが無効になります。", @@ -40,23 +40,23 @@ export const exportAudio = { exportMessage: { message: "WAV音声をエクスポートしています...", estimated: "残り時間:", - convertWav: "WAVに変換中...", + convertWav: "WAVに変換中..." } }, - + midi: { button: { title: "MIDI (.mid)", description: "コントローラーと楽器の変更が適用されたMIDIファイルをエクスポートします" } }, - + soundfont: { button: { title: "サウンドフォント (.sf2)", description: "SoundFont2ファイルをエクスポートします" }, - + options: { title: "SFエクスポートオプション", confirm: "エクスポート", @@ -75,14 +75,14 @@ export const exportAudio = { } } }, - + rmidi: { button: { title: "埋め込みMIDI (.rmi)", description: "変更されたMIDIとトリミングされたサウンドフォントを1つのファイルに埋め込んでエクスポートします。" + "この形式は広くサポートされていないことに注意してください" }, - + progress: { title: "埋め込まれたMIDIをエクスポート中...", loading: "サウンドフォントとMIDIを読み込み中...", @@ -91,7 +91,7 @@ export const exportAudio = { saving: "RMIDIを保存中...", done: "完了しました!" }, - + options: { title: "RMIDIエクスポートオプション", confirm: "エクスポート", @@ -105,7 +105,7 @@ export const exportAudio = { }, bankOffset: { title: "バンクオフセット", - description: "ファイルのバンクオフセットです。0の値が推奨されます。変更は慎重に行ってください。", + description: "ファイルのバンクオフセットです。0の値が推奨されます。変更は慎重に行ってください。" }, adjust: { title: "MIDIを調整", @@ -148,6 +148,6 @@ export const exportAudio = { description: "曲の長さ" } } - + } -} +}; diff --git a/src/website/js/locale/locale_files/locale_ja/locale.js b/src/website/js/locale/locale_files/locale_ja/locale.js index fa14259c..1491db69 100644 --- a/src/website/js/locale/locale_files/locale_ja/locale.js +++ b/src/website/js/locale/locale_files/locale_ja/locale.js @@ -1,8 +1,8 @@ -import { settingsLocale } from './settings/settings.js' -import { musicPlayerModeLocale } from './music_player_mode.js' -import { synthesizerControllerLocale } from './synthesizer_controller/synthesizer_controller.js' -import { sequencerControllerLocale } from './sequencer_controller.js' -import { exportAudio } from './export_audio.js' +import { settingsLocale } from "./settings/settings.js"; +import { musicPlayerModeLocale } from "./music_player_mode.js"; +import { synthesizerControllerLocale } from "./synthesizer_controller/synthesizer_controller.js"; +import { sequencerControllerLocale } from "./sequencer_controller.js"; +import { exportAudio } from "./export_audio.js"; /** * @@ -13,7 +13,7 @@ export const localeJapanese = { // title messsage titleMessage: "SpessaSynth: SoundFont2 Javascript シンセサイザー", demoTitleMessage: "SpessaSynth: SoundFont2 Javascript シンセサイザー オンラインデモ", - + synthInit: { genericLoading: "読み込み中...", loadingSoundfont: "サウンドフォントを読み込んでいます...", @@ -23,43 +23,43 @@ export const localeJapanese = { noWebAudio: "お使いのブラウザはWeb Audioをサポートしていません。", done: "準備完了!" }, - + // top bar buttons midiUploadButton: "MIDIファイルをアップロード", - + exportAudio: exportAudio, - + yes: "はい", no: "いいえ", - - + + demoSoundfontUploadButton: "サウンドフォントをアップロード", demoGithubPage: "プロジェクトのページ", demoSongButton: "デモソング", credits: "クリエイター", dropPrompt: "ここにファイルをドロップ...", - + warnings: { noMidiSupport: "このブラウザはMIDI入力をサポートしていないため、この機能は利用できません。ChromeまたはFirefoxを使用することを検討してください。", outOfMemory: "ブラウザのメモリが不足しました。FirefoxやSF3サウンドフォントの使用を検討してください。\n\n(エラーについてはコンソールを参照してください)。", chromeMobile: "SpessaSynthはChrome Mobileでの動作が良くありません。\n\n代わりにFirefox Androidを使用することを検討してください。", warning: "注意" }, - + hideTopBar: { title: "トップバーを隠す", - description: "トップ(タイトル)バーを隠して、よりシームレスな体験を提供します", + description: "トップ(タイトル)バーを隠して、よりシームレスな体験を提供します" }, - + convertDls: { title: "DLS変換", message: "DLSファイルがアップロードされたようです。これをSF2に変換しますか?" }, - - + + // all translations split up musicPlayerMode: musicPlayerModeLocale, settings: settingsLocale, synthesizerController: synthesizerControllerLocale, sequencerController: sequencerControllerLocale -} +}; diff --git a/src/website/js/locale/locale_files/locale_ja/sequencer_controller.js b/src/website/js/locale/locale_files/locale_ja/sequencer_controller.js index 0ef52868..5ebd2fc9 100644 --- a/src/website/js/locale/locale_files/locale_ja/sequencer_controller.js +++ b/src/website/js/locale/locale_files/locale_ja/sequencer_controller.js @@ -11,4 +11,4 @@ export const sequencerControllerLocale = { title: "その他のテキスト" } } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/website/js/locale/locale_files/locale_ja/settings/keyboard_settings.js b/src/website/js/locale/locale_files/locale_ja/settings/keyboard_settings.js index a0ca40b0..e8887c5a 100644 --- a/src/website/js/locale/locale_files/locale_ja/settings/keyboard_settings.js +++ b/src/website/js/locale/locale_files/locale_ja/settings/keyboard_settings.js @@ -1,30 +1,30 @@ export const keyboardSettingsLocale = { title: "キーボード設定", - + selectedChannel: { title: "選択されたチャンネル", description: "キーボードがメッセージを送信するチャンネル", channelOption: "チャンネル {0}" }, - + keyboardSize: { title: "キーボードサイズ", description: "キーボードに表示されるキーの範囲。MIDIノートのサイズに応じて調整されます", - + full: "128キー(全体)", piano: "88キー(ピアノ)", fiveOctaves: "5オクターブ", useSongKeyRange: "曲のキー範囲を使用", twoOctaves: "オクターブ" }, - + toggleTheme: { title: "テーマを切り替え", - description: "キーボードのテーマを切り替えます", + description: "キーボードのテーマを切り替えます" }, - + show: { title: "表示", description: "MIDIキーボードを表示/非表示" } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/website/js/locale/locale_files/locale_ja/settings/midi_settings.js b/src/website/js/locale/locale_files/locale_ja/settings/midi_settings.js index 442b135d..36db6ba7 100644 --- a/src/website/js/locale/locale_files/locale_ja/settings/midi_settings.js +++ b/src/website/js/locale/locale_files/locale_ja/settings/midi_settings.js @@ -1,15 +1,15 @@ export const midiSettingsLocale = { title: "MIDI設定", - + midiInput: { title: "MIDI入力", description: "MIDIメッセージを受信するポート", disabled: "無効" }, - + midiOutput: { title: "MIDI出力", description: "MIDIファイルを再生するポート", disabled: "SpessaSynthを使用" } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/website/js/locale/locale_files/locale_ja/settings/renderer_settings.js b/src/website/js/locale/locale_files/locale_ja/settings/renderer_settings.js index bcdf3cf7..d3b48df7 100644 --- a/src/website/js/locale/locale_files/locale_ja/settings/renderer_settings.js +++ b/src/website/js/locale/locale_files/locale_ja/settings/renderer_settings.js @@ -2,46 +2,46 @@ export const rendererSettingsLocale = { title: "レンダラー設定", noteFallingTime: { title: "ノートの落下時間(ミリ秒)", - description: "ノートが落ちる速さ(視覚的に)", + description: "ノートが落ちる速さ(視覚的に)" }, - + waveformThickness: { title: "波形の線の太さ(ピクセル)", description: "波形の線の太さ" }, - + waveformSampleSize: { title: "波形のサンプルサイズ", description: "波形の詳細度(注:高い値はパフォーマンスに影響を与える可能性があります)" }, - + waveformAmplifier: { title: "波形の増幅器", description: "波形の鮮やかさ" }, - + toggleWaveformsRendering: { title: "波形レンダリングの切り替え", description: "チャンネル波形のレンダリングを切り替えます(オーディオを表示するカラフルな線)" }, - + toggleNotesRendering: { title: "ノートレンダリングの切り替え", - description: "MIDIファイルを再生する際の落下ノートのレンダリングを切り替えます", + description: "MIDIファイルを再生する際の落下ノートのレンダリングを切り替えます" }, - + toggleDrawingActiveNotes: { title: "アクティブノートの描画を切り替え", description: "ノートが押されたときに光り輝く描画を切り替えます" }, - + toggleDrawingVisualPitch: { title: "ビジュアルピッチ描画の切り替え", description: "ピッチホイールが適用されたときにノートが左右にスライドする描画を切り替えます" }, - + toggleStabilizeWaveforms: { title: "波形を安定させる", description: "オーディオ波形を安定させる設定を切り替え、波形を固定します。" } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/website/js/locale/locale_files/locale_ja/settings/settings.js b/src/website/js/locale/locale_files/locale_ja/settings/settings.js index 1bf56b13..9bebe624 100644 --- a/src/website/js/locale/locale_files/locale_ja/settings/settings.js +++ b/src/website/js/locale/locale_files/locale_ja/settings/settings.js @@ -1,6 +1,6 @@ -import { rendererSettingsLocale } from './renderer_settings.js' -import { keyboardSettingsLocale } from './keyboard_settings.js' -import { midiSettingsLocale } from './midi_settings.js' +import { rendererSettingsLocale } from "./renderer_settings.js"; +import { keyboardSettingsLocale } from "./keyboard_settings.js"; +import { midiSettingsLocale } from "./midi_settings.js"; /** * @type {CompleteSettingsLocale} @@ -8,24 +8,24 @@ import { midiSettingsLocale } from './midi_settings.js' export const settingsLocale = { toggleButton: "設定", mainTitle: "プログラム設定", - + rendererSettings: rendererSettingsLocale, keyboardSettings: keyboardSettingsLocale, midiSettings: midiSettingsLocale, - + interfaceSettings: { title: "インターフェース設定", - + toggleTheme: { title: "テーマを切り替え", description: "プログラムのテーマを切り替えます" }, - + selectLanguage: { title: "言語", description: "プログラムの言語を変更します" }, - + layoutDirection: { title: "レイアウトの方向", description: "レンダラーとキーボードのレイアウト方向", diff --git a/src/website/js/locale/locale_files/locale_ja/synthesizer_controller/channel_controller.js b/src/website/js/locale/locale_files/locale_ja/synthesizer_controller/channel_controller.js index 87f7fc32..1dd24128 100644 --- a/src/website/js/locale/locale_files/locale_ja/synthesizer_controller/channel_controller.js +++ b/src/website/js/locale/locale_files/locale_ja/synthesizer_controller/channel_controller.js @@ -3,72 +3,72 @@ export const channelControllerLocale = { title: "ボイス: ", description: "チャンネル {0} で再生中のボイスの現在の数" }, - + pitchBendMeter: { title: "ピッチ: ", description: "チャンネル {0} に適用されている現在のピッチベンド" }, - + panMeter: { title: "パン: ", description: "チャンネル {0} に適用されている現在のステレオパンニング(右クリックでロック)" }, - + expressionMeter: { title: "エクスプレッション: ", description: "チャンネル {0} の現在の表現(音量)(右クリックでロック)" }, - + volumeMeter: { title: "ボリューム: ", description: "チャンネル {0} の現在の音量(右クリックでロック)" }, - + modulationWheelMeter: { title: "モジュレーションホイール: ", description: "チャンネル {0} の現在のモジュレーション(通常はビブラート)の深さ(右クリックでロック)" }, - + chorusMeter: { title: "コーラス: ", description: "チャンネル {0} に適用されている現在のコーラスエフェクトのレベル(右クリックでロック)" }, - + reverbMeter: { title: "リバーブ: ", description: "チャンネル {0} に適用されている現在のリバーブエフェクトのレベル(右クリックでロック)" }, - + filterMeter: { title: "フィルター: ", description: "チャンネル {0} に適用されているローパスフィルターのカットオフの現在のレベル (右クリックでロック)" }, - - -transposeMeter: { + + + transposeMeter: { title: "トランスポーズ: ", description: "チャンネル {0} の現在の移調(キーシフト)" }, - + presetSelector: { description: "チャンネル {0} が使用するパッチ(楽器)を変更", selectionPrompt: "チャンネル {0} の楽器を変更する", searchPrompt: "検索..." }, - + presetReset: { description: "プログラム変更を許可するためにチャンネル {0} のロックを解除" }, - + soloButton: { description: "チャンネル {0} を単独再生" }, - + muteButton: { description: "チャンネル {0} をミュート/ミュート解除" }, - + drumToggleButton: { description: "チャンネル {0} でドラムを切り替え" } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/website/js/locale/locale_files/locale_ja/synthesizer_controller/synthesizer_controller.js b/src/website/js/locale/locale_files/locale_ja/synthesizer_controller/synthesizer_controller.js index 1aae65e7..b5ddd34b 100644 --- a/src/website/js/locale/locale_files/locale_ja/synthesizer_controller/synthesizer_controller.js +++ b/src/website/js/locale/locale_files/locale_ja/synthesizer_controller/synthesizer_controller.js @@ -1,4 +1,4 @@ -import { channelControllerLocale } from './channel_controller.js' +import { channelControllerLocale } from "./channel_controller.js"; /** * @@ -9,59 +9,59 @@ export const synthesizerControllerLocale = { title: "シンセサイザーコントローラー", description: "シンセサイザーコントローラーを表示" }, - + // meters mainVoiceMeter: { title: "ボイス: ", - description: "現在再生中のボイスの総数", + description: "現在再生中のボイスの総数" }, - + mainVolumeMeter: { title: "ボリューム: ", description: "シンセサイザーの現在のマスターボリューム" }, - + mainPanMeter: { title: "パン: ", description: "シンセサイザーの現在のマスターステレオパンニング" }, - + mainTransposeMeter: { title: "トランスポーズ: ", description: "シンセサイザーを移調します(セミトーンまたはキー)" }, - + // buttons midiPanic: { title: "MIDIパニック", description: "すべてのボイスを即座に停止" }, - + systemReset: { title: "システムリセット", description: "すべてのコントローラーをデフォルト値にリセット" }, - + blackMidiMode: { title: "ブラックMIDIモード", description: "高性能モードを切り替え、見た目を簡素化し、ノートを速く消去" }, - + disableCustomVibrato: { title: "カスタムビブラートを無効化", description: "カスタム(NRPN)ビブラートを永久に無効化。再度有効化するにはウェブサイトをリロード" }, - + helpButton: { title: "ヘルプ", description: "使用ガイドを表示します" }, - + interpolation: { description: "シンセサイザーの補間方法を選択", linear: "リニア", nearestNeighbor: "なし" }, - + channelController: channelControllerLocale -} +}; diff --git a/src/website/js/locale/locale_files/locale_list.js b/src/website/js/locale/locale_files/locale_list.js index db8195c0..b70f37a6 100644 --- a/src/website/js/locale/locale_files/locale_list.js +++ b/src/website/js/locale/locale_files/locale_list.js @@ -1,6 +1,6 @@ -import { localeEnglish } from './locale_en/locale.js' -import { localePolish } from './locale_pl/locale.js' -import { localeJapanese } from './locale_ja/locale.js' +import { localeEnglish } from "./locale_en/locale.js"; +import { localePolish } from "./locale_pl/locale.js"; +import { localeJapanese } from "./locale_ja/locale.js"; export const DEFAULT_LOCALE = "en"; /** @@ -10,7 +10,7 @@ export const localeList = { "en": localeEnglish, "pl": localePolish, "ja": localeJapanese -} +}; /** * @typedef { * "en" diff --git a/src/website/js/locale/locale_files/locale_pl/export_audio.js b/src/website/js/locale/locale_files/locale_pl/export_audio.js index 348e505f..625cae55 100644 --- a/src/website/js/locale/locale_files/locale_pl/export_audio.js +++ b/src/website/js/locale/locale_files/locale_pl/export_audio.js @@ -3,7 +3,7 @@ export const exportAudio = { title: "Zapisz", description: "Zapisz w różnych formatach" }, - + formats: { title: "Wybierz format", formats: { @@ -17,11 +17,11 @@ export const exportAudio = { confirm: "Eksportuj", normalizeVolume: { title: "Normalizuj głośność", - description: "Eksportuj audio z taką samą głośnością, niezależnie od głośności MIDI.", + description: "Eksportuj audio z taką samą głośnością, niezależnie od głośności MIDI." }, additionalTime: { title: "Dodatkowy czas (s)", - description: "Dodatkowy czas na końcu utworu aby pozwolić na wyciszenie się dźwięku. (sekundy)", + description: "Dodatkowy czas na końcu utworu aby pozwolić na wyciszenie się dźwięku. (sekundy)" }, separateChannels: { title: "Rozdziel kanały", @@ -42,20 +42,20 @@ export const exportAudio = { convertWav: "Konwertowanie do wav..." } }, - + midi: { button: { title: "MIDI (.mid)", description: "Eksportuj plik MIDI wraz ze zmianami instrumentów i kontrolerów" } }, - + soundfont: { button: { title: "SoundFont (.sf2)", description: "Eksportuj SoundFont" }, - + options: { title: "Opcje eksportu soundfonta", confirm: "Eksportuj", @@ -74,14 +74,14 @@ export const exportAudio = { } } }, - + rmidi: { button: { title: "Osadzone MIDI (.rmi)", description: "Eksportuj zmodyfikowane MIDI wraz ze zmniejszonym soundfontem jako jeden plik. " + "Uwaga: ten format nie jest szeroko wspierany" }, - + progress: { title: "Exportowanie osadzonego MIDI...", loading: "Wczytywanie soundfonta i MIDI...", @@ -90,7 +90,7 @@ export const exportAudio = { saving: "Zapisywanie RMIDI...", done: "Gotowe!" }, - + options: { title: "Opcje eksportu RMIDI", confirm: "Eksportuj", @@ -104,20 +104,20 @@ export const exportAudio = { }, bankOffset: { title: "Przesunięcie banku", - description: "Przesunięcie banku w pliku. Zalecane 0. Zmień tylko jeśli wiesz co robisz.", + description: "Przesunięcie banku w pliku. Zalecane 0. Zmień tylko jeśli wiesz co robisz." }, adjust: { title: "Dostosuj MIDI", description: "Dostosuj MIDI do SoundFonta. Pozostaw włączone, chyba że wiesz co robisz." } - + } } }, metadata: { songTitle: { title: "Tytuł:", - description: "Tytuł utworu", + description: "Tytuł utworu" }, album: { title: "Album:", @@ -129,7 +129,7 @@ export const exportAudio = { }, albumCover: { title: "Okładka albumu:", - description: "Okładka albumu utworu", + description: "Okładka albumu utworu" }, creationDate: { title: "Stworzono:", @@ -149,4 +149,4 @@ export const exportAudio = { } } } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/website/js/locale/locale_files/locale_pl/locale.js b/src/website/js/locale/locale_files/locale_pl/locale.js index 3dc1fa0b..0bc30a6c 100644 --- a/src/website/js/locale/locale_files/locale_pl/locale.js +++ b/src/website/js/locale/locale_files/locale_pl/locale.js @@ -1,8 +1,8 @@ -import { settingsLocale } from './settings/settings.js' -import { musicPlayerModeLocale } from './music_player_mode.js' -import { synthesizerControllerLocale } from './synthesizer_controller/synthesizer_controller.js' -import { sequencerControllerLocale } from './sequencer_controller.js' -import { exportAudio } from './export_audio.js' +import { settingsLocale } from "./settings/settings.js"; +import { musicPlayerModeLocale } from "./music_player_mode.js"; +import { synthesizerControllerLocale } from "./synthesizer_controller/synthesizer_controller.js"; +import { sequencerControllerLocale } from "./sequencer_controller.js"; +import { exportAudio } from "./export_audio.js"; /** * @@ -13,7 +13,7 @@ export const localePolish = { // title messsage titleMessage: "SpessaSynth: JavaScriptowy Syntezator SoundFont2", demoTitleMessage: "SpessaSynth: JavaScriptowy Syntezator SoundFont2 Wersja Demo", - + synthInit: { genericLoading: "Wczytywanie...", loadingSoundfont: "Wczytywanie SoundFonta...", @@ -23,42 +23,42 @@ export const localePolish = { noWebAudio: "Twoja przeglądarka nie wspiera Web Audio.", done: "Gotowe!" }, - + // top bar buttons midiUploadButton: "Wgraj Twoje pliki MIDI", midiRenderButton: { title: "Eksportuj audio", description: "Zapisz audio do pliku WAV lub MIDI" }, - + exportAudio: exportAudio, - + yes: "Tak", no: "Nie", - + demoSoundfontUploadButton: "Wgraj SoundFonta", demoGithubPage: "Strona projektu", demoSongButton: "Piosenka demo", credits: "Twórcy", dropPrompt: "Upuść pliki tutaj...", - + warnings: { outOfMemory: "Twojej przeglądarce skończyła się pamięć. Rozważ użycie Firefoxa albo plików SF3. (Zobacz błąd w konsoli)", noMidiSupport: "Nie wykryto MIDI. Korzystanie z portów MIDI nie będzie dostępne.", chromeMobile: "SpessaSynth działa wolno na Chromie na telefon. Rozważ użycie Firefoxa Android.", warning: "Uwaga" }, - + hideTopBar: { title: "Ukryj górny pasek", - description: "Ukryj pasek tytułowy w celu poprawy widoczności na pionowych ekranach", + description: "Ukryj pasek tytułowy w celu poprawy widoczności na pionowych ekranach" }, - + convertDls: { title: "Konwersja DLS", message: "Wygląda na to, że wgrałeś plik DLS. Czy chcesz przekonwertować go do SF2?" }, - + // all translations split up musicPlayerMode: musicPlayerModeLocale, settings: settingsLocale, diff --git a/src/website/js/locale/locale_files/locale_pl/sequencer_controller.js b/src/website/js/locale/locale_files/locale_pl/sequencer_controller.js index 6ffd148e..c55af990 100644 --- a/src/website/js/locale/locale_files/locale_pl/sequencer_controller.js +++ b/src/website/js/locale/locale_files/locale_pl/sequencer_controller.js @@ -11,4 +11,4 @@ export const sequencerControllerLocale = { title: "Inny tekst" } } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/website/js/locale/locale_files/locale_pl/settings/keyboard_settings.js b/src/website/js/locale/locale_files/locale_pl/settings/keyboard_settings.js index 910823b0..33de9aa9 100644 --- a/src/website/js/locale/locale_files/locale_pl/settings/keyboard_settings.js +++ b/src/website/js/locale/locale_files/locale_pl/settings/keyboard_settings.js @@ -1,30 +1,30 @@ export const keyboardSettingsLocale = { title: "Ustawienia pianina", - + selectedChannel: { title: "Wybrany kanał", description: "Kanał, do którego będzie podłączone pianino", channelOption: "Kanał {0}" }, - + keyboardSize: { title: "Rozmiar pianina", description: "Zakres klawiszy widocznych na pianine. Dostosowuje również szerokość wizualizowanych nut", - + full: "128 klawiszy (pełen zakres)", piano: "88 klawiszy (fortepian)", fiveOctaves: "5 oktaw", twoOctaves: "Dwie oktawy", - useSongKeyRange: "Użyj zakresu utworu", + useSongKeyRange: "Użyj zakresu utworu" }, - + toggleTheme: { title: "Włącz ciemny motyw", - description: "Włącz ciemny motyw wbudowanego pianina", + description: "Włącz ciemny motyw wbudowanego pianina" }, - + show: { title: "Pokaż", description: "Pokaż/ukryj pianino" } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/website/js/locale/locale_files/locale_pl/settings/midi_settings.js b/src/website/js/locale/locale_files/locale_pl/settings/midi_settings.js index 2b8dfe44..567ec1b9 100644 --- a/src/website/js/locale/locale_files/locale_pl/settings/midi_settings.js +++ b/src/website/js/locale/locale_files/locale_pl/settings/midi_settings.js @@ -1,15 +1,15 @@ export const midiSettingsLocale = { title: "Ustawienia MIDI", - + midiInput: { title: "Wejście MIDI", description: "Port MIDI, który będzie nasłuchiwany", disabled: "Wyłączony" }, - + midiOutput: { title: "Wyjście MIDI", description: "Port MIDI, do którego będzie grany utwór", disabled: "Użyj SpessaSynth" } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/website/js/locale/locale_files/locale_pl/settings/renderer_settings.js b/src/website/js/locale/locale_files/locale_pl/settings/renderer_settings.js index 42f0acd1..bef6956e 100644 --- a/src/website/js/locale/locale_files/locale_pl/settings/renderer_settings.js +++ b/src/website/js/locale/locale_files/locale_pl/settings/renderer_settings.js @@ -2,46 +2,46 @@ export const rendererSettingsLocale = { title: "Ustawienia wizualizacji", noteFallingTime: { title: "Czas spadania nut (ms)", - description: "Jak szybko spadają z góry nuty (w milisekundach)", + description: "Jak szybko spadają z góry nuty (w milisekundach)" }, - + waveformThickness: { title: "Grubość lini fal (px)", description: "Jak grube są linie fal dźwiękowych" }, - + waveformSampleSize: { title: "Rozmiar próbki fali", description: "Jak szczegółowe są linei fal dźwiękowcyh (Uwaga: wysokie wartości mogą pogorszyć wydajność)" }, - + waveformAmplifier: { title: "Wzmacniasz fal", description: "Jak 'żywe' są fale. Kontroluje ich amplitudę" }, - + toggleWaveformsRendering: { title: "Włącz rysowanie fal", description: "Włącz rysowanie fal dźwiękowych (16-tu kolorowych linii z tyłu)" }, - + toggleNotesRendering: { title: "Włącz rysowanie nut", - description: "Włącz rysowanie spadających nut podczas odtwarzania pliku MIDI", + description: "Włącz rysowanie spadających nut podczas odtwarzania pliku MIDI" }, - + toggleDrawingActiveNotes: { title: "Włącz rysowanie aktywnych nut", description: "Włącz efekt podświetlania się nut przy aktywacji" }, - + toggleDrawingVisualPitch: { title: "Włącz wizualizację wysokości tonu", description: "Włącz przesuwanie nut w lewo lub w prawo gdy wysokość nut jest zmieniana" }, - + toggleStabilizeWaveforms: { title: "Włącz stabilizację fal", description: "Włącz stabilizowanie fal dźwiękowych" } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/website/js/locale/locale_files/locale_pl/settings/settings.js b/src/website/js/locale/locale_files/locale_pl/settings/settings.js index aa7d1e31..f508fdd3 100644 --- a/src/website/js/locale/locale_files/locale_pl/settings/settings.js +++ b/src/website/js/locale/locale_files/locale_pl/settings/settings.js @@ -1,6 +1,6 @@ -import { rendererSettingsLocale } from './renderer_settings.js' -import { keyboardSettingsLocale } from './keyboard_settings.js' -import { midiSettingsLocale } from './midi_settings.js' +import { rendererSettingsLocale } from "./renderer_settings.js"; +import { keyboardSettingsLocale } from "./keyboard_settings.js"; +import { midiSettingsLocale } from "./midi_settings.js"; /** * @@ -9,24 +9,24 @@ import { midiSettingsLocale } from './midi_settings.js' export const settingsLocale = { toggleButton: "Ustawienia", mainTitle: "Ustawienia programu", - + rendererSettings: rendererSettingsLocale, keyboardSettings: keyboardSettingsLocale, midiSettings: midiSettingsLocale, - + interfaceSettings: { title: "Ustawienia interfejsu", - + toggleTheme: { title: "Włącz ciemny motyw", description: "Włącz ciemny motyw programu" }, - + selectLanguage: { title: "Język", description: "Zmień język programu" }, - + layoutDirection: { title: "Układ", description: "Kierunek układu wizualizacji i pianina", @@ -34,7 +34,7 @@ export const settingsLocale = { downwards: "W dół", upwards: "W górę", leftToRight: "Od lewej do prawej", - rightToLeft: "Od prawej do lewej", + rightToLeft: "Od prawej do lewej" } } } diff --git a/src/website/js/locale/locale_files/locale_pl/synthesizer_controller/channel_controller.js b/src/website/js/locale/locale_files/locale_pl/synthesizer_controller/channel_controller.js index 57cc3fab..b976defa 100644 --- a/src/website/js/locale/locale_files/locale_pl/synthesizer_controller/channel_controller.js +++ b/src/website/js/locale/locale_files/locale_pl/synthesizer_controller/channel_controller.js @@ -3,71 +3,71 @@ export const channelControllerLocale = { title: "Dźwięki: ", description: "Aktualna ilość dźwięków na kanale {0}" }, - + pitchBendMeter: { title: "Wysokość: ", description: "Aktualna wysokość tonu na kanale {0}" }, - + panMeter: { title: "Stereo: ", description: "Aktualny efekt stereo na kanale {0} (kliknij prawym aby zablokować)" }, - + expressionMeter: { title: "Ekspresja: ", description: "Aktualna ekspresja (głośnośc) kanału {0} (kliknij prawym aby zablokować)" }, - + volumeMeter: { title: "Głośność: ", description: "Aktualna głośność kanału {0} (kliknij prawym aby zablokować)" }, - + modulationWheelMeter: { title: "Modulacja: ", description: "Aktualna głębokość modulacji (zazwyczaj vibrato) kanału {0} (kliknij prawym aby zablokować)" }, - + chorusMeter: { title: "Chór: ", description: "Aktualny efekt chóru na kanale {0} (kliknij prawym aby zablokować)" }, - + reverbMeter: { title: "Pogłos: ", description: "Aktualny efekt pogłosu na kanale {0} (kliknij prawym aby zablokować)" }, - + filterMeter: { title: "Filtr: ", description: "Aktualny poziom filtra niskopasmowego na kanale {0} (kliknij prawym aby zablokować)" }, - + transposeMeter: { title: "Transpozycja: ", description: "Aktualna transpozycja (przesunięcie klawiszy) kanału {0}" }, - + presetSelector: { description: "Zmień patch (instrument), którego używa kanał {0}", selectionPrompt: "Zmień instrument kanału {0}", searchPrompt: "Wyszukaj..." }, - + presetReset: { description: "Odblokuj kanał {0}, aby program mógł go zmieniać" }, - + soloButton: { description: "Solo na kanale {0}" }, - + muteButton: { description: "Wycisz/odcisz kanał {0}" }, - + drumToggleButton: { description: "Przełącz perkusję na kanale {0}" } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/website/js/locale/locale_files/locale_pl/synthesizer_controller/synthesizer_controller.js b/src/website/js/locale/locale_files/locale_pl/synthesizer_controller/synthesizer_controller.js index 89be5f53..744ddb3f 100644 --- a/src/website/js/locale/locale_files/locale_pl/synthesizer_controller/synthesizer_controller.js +++ b/src/website/js/locale/locale_files/locale_pl/synthesizer_controller/synthesizer_controller.js @@ -1,4 +1,4 @@ -import { channelControllerLocale } from './channel_controller.js' +import { channelControllerLocale } from "./channel_controller.js"; /** * @type {{systemReset: {description: string, title: string}, disableCustomVibrato: {description: string, title: string}, mainTransposeMeter: {description: string, title: string}, mainVoiceMeter: {description: string, title: string}, midiPanic: {description: string, title: string}, mainPanMeter: {description: string, title: string}, mainVolumeMeter: {description: string, title: string}, toggleButton: {description: string, title: string}, channelController: {transposeMeter: {description: string, title: string}, voiceMeter: {description: string, title: string}, modulationWheelMeter: {description: string, title: string}, expressionMeter: {description: string, title: string}, panMeter: {description: string, title: string}, presetSelector: {description: string}, presetReset: {description: string}, pitchBendMeter: {description: string, title: string}, reverbMeter: {description: string, title: string}, volumeMeter: {description: string, title: string}, drumToggleButton: {description: string}, muteButton: {description: string}, chorusMeter: {description: string, title: string}}, blackMidiMode: {description: string, title: string}}} @@ -8,60 +8,60 @@ export const synthesizerControllerLocale = { title: "Kontroler syntezatora", description: "Pokaż kontroler syntezatora" }, - + // meters mainVoiceMeter: { title: "Dźwięki: ", - description: "Całkowita ilość aktualnie odtwarzanych dźwięków", + description: "Całkowita ilość aktualnie odtwarzanych dźwięków" }, - + mainVolumeMeter: { title: "Głośność: ", description: "Aktualna głośność syntezatora" }, - + mainPanMeter: { title: "Stereo: ", description: "Aktualna pozycja stereo syntezatora" }, - + mainTransposeMeter: { title: "Transpozycja: ", description: "Transpozycjonuje syntezator (w semitonach)" }, - + // buttons midiPanic: { title: "MIDI Panic", description: "Zatrzymuje wszystkie dźwięki" }, - + systemReset: { title: "Reset systemu", description: "Resetuje wszystkie kontroleru do ich domyślnych wartości" }, - + blackMidiMode: { title: "Tryb black MIDI", description: "Przełącza tryb wysokiej wydajności, upraszczając wygląd i pogarszając jakość dźwięku" }, - + disableCustomVibrato: { title: "Wyłącz niestandardowe vibrato", description: "Wyłącza niestandardowe (NRPN) vibrato. Wymaga przeładowania strony aby je ponownie włączyć" }, - + helpButton: { title: "Pomoc", description: "Pokaż instrukcję obsługi" }, - + interpolation: { description: "Wybierz metodę interpolacji", linear: "Interpolacja liniowa", nearestNeighbor: "Najbliższy sąsiad", cubic: "Interpolacja Sześcienna" }, - + channelController: channelControllerLocale -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/website/js/locale/locale_manager.js b/src/website/js/locale/locale_manager.js index 598df6c0..2afd7a33 100644 --- a/src/website/js/locale/locale_manager.js +++ b/src/website/js/locale/locale_manager.js @@ -7,26 +7,33 @@ * isEdited: boolean * }} PropertyType */ -import { SpessaSynthInfo, SpessaSynthWarn } from '../../../spessasynth_lib/utils/loggin.js' -import { DEFAULT_LOCALE, localeList } from './locale_files/locale_list.js' +import { SpessaSynthInfo, SpessaSynthWarn } from "../../../spessasynth_lib/utils/loggin.js"; +import { DEFAULT_LOCALE, localeList } from "./locale_files/locale_list.js"; export class LocaleManager { + /** + * calls it when the locale has changed (no arguments) + * @type {function()[]} + */ + onLocaleChanged = []; + /** * Creates a new locale manager, responsible for managing and binding text values, then changing them when the locale changes * @param initialLocale {LocaleList} */ - constructor(initialLocale) { + constructor(initialLocale) + { /** * @type {CompleteLocaleTypedef} */ this.locale = localeList[initialLocale] || localeList[DEFAULT_LOCALE]; - + /** * @type {CompleteLocaleTypedef} */ this.fallbackLocale = localeList[DEFAULT_LOCALE]; - + /** * @type {LocaleList} */ @@ -38,7 +45,7 @@ export class LocaleManager */ this._boundObjectProperties = []; } - + /** * Resolves and gets a the localized string for the current path * @param localePath {string} The locale path to the text, written as JS object path, starts with "locale." @@ -48,13 +55,13 @@ export class LocaleManager getLocaleString(localePath, formattingArguments = []) { const locale = this._resolveLocalePath(localePath); - if(formattingArguments.length > 0) + if (formattingArguments.length > 0) { return this._formatLocale(locale, formattingArguments); } return locale; } - + /** * @param property {PropertyType} * @private @@ -62,18 +69,18 @@ export class LocaleManager _applyPropertyInternal(property) { // if edited, skip - if(property.isEdited) + if (property.isEdited) { return; } let textValue = this._resolveLocalePath(property.localePath); - if(property.formattingArguments.length > 0) + if (property.formattingArguments.length > 0) { textValue = this._formatLocale(textValue, property.formattingArguments); } property.object[property.propertyName] = textValue; } - + /** * Checks if the property has changed and flags it as edited * @param property {PropertyType} @@ -83,22 +90,16 @@ export class LocaleManager { // get the text value let textValue = this._resolveLocalePath(property.localePath); - if(property.formattingArguments.length > 0) + if (property.formattingArguments.length > 0) { textValue = this._formatLocale(textValue, property.formattingArguments); } - if(property.object[property.propertyName] !== textValue) + if (property.object[property.propertyName] !== textValue) { property.isEdited = true; } } - - /** - * calls it when the locale has changed (no arguments) - * @type {function()[]} - */ - onLocaleChanged = []; - + /** * replaces strings like "{0}" with the given arguments * @param template {string} the preformatted string @@ -108,11 +109,12 @@ export class LocaleManager */ _formatLocale(template, values) { - return template.replace(/{(\d+)}/g, (match, number) => { - return typeof values[number] !== 'undefined' ? values[number] : match; + return template.replace(/{(\d+)}/g, (match, number) => + { + return typeof values[number] !== "undefined" ? values[number] : match; }); } - + /** * Binds a given object's property to a locale path and applies it * @param object {HTMLElement} the object that holds the bound property @@ -138,7 +140,7 @@ export class LocaleManager // add to bound properties list this._boundObjectProperties.push(property); } - + /** * Resolves the locale path to get the string value from the locale object * @param path {string} The locale path to the text, written as JS object path, starts with "locale." @@ -152,13 +154,13 @@ export class LocaleManager { throw new Error(`Invalid locale path: ${path} (it should start with "locale.")`); } - - const parts = path.split('.'); - + + const parts = path.split("."); + /** * Traverse the locale object to get the value * @type {Object|string} - */ + */ let current = fallback ? this.fallbackLocale : this.locale; for (let i = 1; i < parts.length; i++) // Start from 1 to skip "locale" { @@ -168,9 +170,10 @@ export class LocaleManager } else { - if(fallback) + if (fallback) { - throw new Error(`Invalid locale path: ${path}: part "${parts[i]}" does not exist. Available paths: ${Object.keys(current).join(", ")}`); + throw new Error(`Invalid locale path: ${path}: part "${parts[i]}" does not exist. Available paths: ${Object.keys( + current).join(", ")}`); } else { @@ -178,16 +181,16 @@ export class LocaleManager } } } - + // Check if the final resolved value is a string - if (typeof current !== 'string') + if (typeof current !== "string") { throw new Error(`Invalid locale path: ${path}: value is not a string. Perhaps the path is incomplete`); } - + return current; } - + /** * Changes the global locale and all bound text * @param newLocale {LocaleList} @@ -200,23 +203,25 @@ export class LocaleManager * @type {CompleteLocaleTypedef} */ const newLocaleObject = localeList[newLocale]; - if(!newLocaleObject) + if (!newLocaleObject) { SpessaSynthWarn(`Locale ${newLocale} not found. Not switching.`); return; } this.localeCode = newLocale; - SpessaSynthInfo("Changing locale to", newLocaleObject.localeName) - if(!force) + SpessaSynthInfo("Changing locale to", newLocaleObject.localeName); + if (!force) { // check if the property has been changed to something else. If so, don't change it back. - this._boundObjectProperties.forEach(property => { + this._boundObjectProperties.forEach(property => + { this._validatePropertyIntegrity(property); }); } this.locale = newLocaleObject; // apply the new locale to bound elements - this._boundObjectProperties.forEach(property => { + this._boundObjectProperties.forEach(property => + { this._applyPropertyInternal(property); }); this.onLocaleChanged.forEach(l => l()); diff --git a/src/website/js/main/demo_main.js b/src/website/js/main/demo_main.js index 3bf9f15f..30457991 100644 --- a/src/website/js/main/demo_main.js +++ b/src/website/js/main/demo_main.js @@ -1,12 +1,12 @@ -"use strict" +"use strict"; -import { Manager } from '../manager/manager.js' -import { SpessaSynthInfo, SpessaSynthWarn } from '../../../spessasynth_lib/utils/loggin.js' -import { isMobile } from '../utils/is_mobile.js' -import { getCheckSvg, getExclamationSvg, getHourglassSvg } from '../utils/icons.js' -import { closeNotification, showNotification } from '../notification/notification.js' -import { ANIMATION_REFLOW_TIME } from '../utils/animation_utils.js' -import { LocaleManager } from '../locale/locale_manager.js' +import { Manager } from "../manager/manager.js"; +import { SpessaSynthInfo, SpessaSynthWarn } from "../../../spessasynth_lib/utils/loggin.js"; +import { isMobile } from "../utils/is_mobile.js"; +import { getCheckSvg, getExclamationSvg, getHourglassSvg } from "../utils/icons.js"; +import { closeNotification, showNotification } from "../notification/notification.js"; +import { ANIMATION_REFLOW_TIME } from "../utils/animation_utils.js"; +import { LocaleManager } from "../locale/locale_manager.js"; /** * demo_main.js @@ -38,19 +38,23 @@ window.SPESSASYNTH_VERSION = r["version"]; // IndexedDB stuff const dbName = "spessasynth-db"; const objectStoreName = "soundFontStore"; + /** * @param callback {function(IDBDatabase)} */ -function initDatabase(callback) { +function initDatabase(callback) +{ const request = indexedDB.open(dbName, 1); - - - request.onsuccess = () => { + + + request.onsuccess = () => + { const db = request.result; callback(db); }; - - request.onupgradeneeded = (event) => { + + request.onupgradeneeded = (event) => + { const db = event.target.result; db.createObjectStore(objectStoreName, { keyPath: "id" }); }; @@ -61,30 +65,34 @@ function initDatabase(callback) { */ async function loadLastSoundFontFromDatabase() { - return await new Promise(resolve => { + return await new Promise(resolve => + { // fetch from db - initDatabase(db => { + initDatabase(db => + { const transaction = db.transaction([objectStoreName], "readonly"); const objectStore = transaction.objectStore(objectStoreName); const request = objectStore.get("buffer"); - - request.onerror = e => { + + request.onerror = e => + { console.error("Database error"); console.error(e); resolve(undefined); - } - - request.onsuccess = async () => { + }; + + request.onsuccess = async () => + { const result = request.result; - if(!result) + if (!result) { resolve(undefined); return; } resolve(result.data); - } - }) - }) + }; + }); + }); } function changeIcon(html, disableAnimation = true) @@ -99,20 +107,23 @@ function changeIcon(html, disableAnimation = true) */ async function saveSoundFontToIndexedDB(arr) { - initDatabase(db => { + initDatabase(db => + { const transaction = db.transaction([objectStoreName], "readwrite"); const objectStore = transaction.objectStore(objectStoreName); - try { + try + { const request = objectStore.put({ id: "buffer", data: arr }); - request.onsuccess = () => { + request.onsuccess = () => + { SpessaSynthInfo("SoundFont stored successfully"); }; - - request.onerror = e => { - console.error("Error saving soundfont", e) - } - } - catch (e) + + request.onerror = e => + { + console.error("Error saving soundfont", e); + }; + } catch (e) { SpessaSynthWarn("Failed saving soundfont:", e); } @@ -128,13 +139,12 @@ async function demoInit(initLocale) { const context = window.AudioContext || window.webkitAudioContext; window.audioContextMain = new context({ sampleRate: SAMPLE_RATE }); - } - catch (e) + } catch (e) { changeIcon(getExclamationSvg(256)); loadingMessage.textContent = localeManager.getLocaleString("locale.synthInit.noWebAudio"); throw e; - + } loadingMessage.textContent = localeManager.getLocaleString("locale.synthInit.loadingSoundfont"); let soundFontBuffer = await loadLastSoundFontFromDatabase(); @@ -148,7 +158,7 @@ async function demoInit(initLocale) loadingMessage.textContent = sFontLoadMessage; soundFontBuffer = await fetchFont(`soundfonts/${SF_NAME}`, percent => { - loadingMessage.textContent =`${sFontLoadMessage} ${percent}%`; + loadingMessage.textContent = `${sFontLoadMessage} ${percent}%`; }); progressBar.style.width = "0"; } @@ -157,72 +167,78 @@ async function demoInit(initLocale) SpessaSynthInfo("Loaded the soundfont from the database succesfully"); } window.soundFontParser = soundFontBuffer; - if(!loadedFromDb) + if (!loadedFromDb) { loadingMessage.textContent = localeManager.getLocaleString("locale.synthInit.savingSoundfont"); await saveSoundFontToIndexedDB(soundFontBuffer); } - - if(window.audioContextMain.state !== "running") + + if (window.audioContextMain.state !== "running") { - document.addEventListener("mousedown", () => { - if(window.audioContextMain.state !== "running") { + document.addEventListener("mousedown", () => + { + if (window.audioContextMain.state !== "running") + { window.audioContextMain.resume().then(); } - - }) + + }); } - + // prepare midi interface loadingMessage.textContent = localeManager.getLocaleString("locale.synthInit.startingSynthesizer"); window.manager = new Manager(audioContextMain, soundFontParser, localeManager); - window.manager.sfError = e => { + window.manager.sfError = e => + { changeIcon(getExclamationSvg(256)); - if(loadedFromDb) + if (loadedFromDb) { - SpessaSynthWarn("Invalid soundfont in the database. Resetting.") + SpessaSynthWarn("Invalid soundfont in the database. Resetting."); // restore to default - initDatabase(db => { + initDatabase(db => + { const transaction = db.transaction([objectStoreName], "readwrite"); const objectStore = transaction.objectStore(objectStoreName); const request = objectStore.delete("buffer"); - request.onsuccess = () => { + request.onsuccess = () => + { location.reload(); - } + }; }); - + } else { titleMessage.innerHTML = `Error parsing soundfont:
${e}
`; } loadingMessage.innerHTML = `Error parsing soundfont:
${e}
`; - } + }; await manager.ready; - - if(fileInput.files[0]) + + if (fileInput.files[0]) { await startMidi(fileInput.files); } else { fileInput.onclick = undefined; - fileInput.onchange = () => { - if(fileInput.files[0]) + fileInput.onchange = () => + { + if (fileInput.files[0]) { startMidi(fileInput.files).then(); } - } + }; } - - changeIcon(getCheckSvg(256)) + + changeIcon(getCheckSvg(256)); loadingMessage.textContent = localeManager.getLocaleString("locale.synthInit.done"); } async function fetchFont(url, callback) { let response = await fetch(url); - if(!response.ok) + if (!response.ok) { titleMessage.innerText = "Error downloading soundfont!"; throw response; @@ -232,16 +248,18 @@ async function fetchFont(url, callback) let done = false; let dataArray = new Uint8Array(parseInt(size)); let offset = 0; - do{ + do + { let readData = await reader.read(); - if(readData.value) { + if (readData.value) + { dataArray.set(readData.value, offset); offset += readData.value.length; } done = readData.done; let percent = Math.round((offset / size) * 100); callback(percent); - }while(!done); + } while (!done); return dataArray.buffer; } @@ -253,9 +271,9 @@ async function fetchFont(url, callback) */ async function startMidi(midiFiles) { - demoSongButton.style.display = "none" + demoSongButton.style.display = "none"; let fName; - if(midiFiles[0].name.length > 20) + if (midiFiles[0].name.length > 20) { fName = midiFiles[0].name.substring(0, 21) + "..."; } @@ -263,7 +281,7 @@ async function startMidi(midiFiles) { fName = midiFiles[0].name; } - if(midiFiles.length > 1) + if (midiFiles.length > 1) { fName += ` and ${midiFiles.length - 1} others`; } @@ -273,24 +291,24 @@ async function startMidi(midiFiles) * @type {MIDIFile[]} */ const parsed = []; - for(const file of midiFiles) + for (const file of midiFiles) { parsed.push({ binary: await file.arrayBuffer(), altName: file.name - }) + }); } manager.synth.setLogLevel(false, false, false, false); - if(manager.seq) + if (manager.seq) { manager.seq.loadNewSongList(parsed); - + } else { manager.play(parsed); } - + exportButton.style.display = "flex"; exportButton.onclick = window.manager.exportSong.bind(window.manager); } @@ -304,7 +322,7 @@ async function startMidi(midiFiles) function saveSettings(settingsData) { localStorage.setItem("spessasynth-settings", JSON.stringify(settingsData)); - SpessaSynthInfo("saved as", settingsData) + SpessaSynthInfo("saved as", settingsData); } // INIT STARTS HERE @@ -314,19 +332,20 @@ window.saveSettings = saveSettings; // load saved settings const saved = JSON.parse(localStorage.getItem("spessasynth-settings")); -if(saved !== null) +if (saved !== null) { /** * reads the settings * @type {Promise} */ - window.savedSettings = new Promise(resolve => { + window.savedSettings = new Promise(resolve => + { resolve(saved); }); } let initLocale; // get locale from saved settings or browser: "en-US" will turn into just "en" -if(saved && saved.interface && saved.interface.language) +if (saved && saved.interface && saved.interface.language) { initLocale = ((await savedSettings).interface.language) || navigator.language.split("-")[0].toLowerCase(); } @@ -352,55 +371,63 @@ async function playDemoSong(fileName) await startMidi([r]); } -demoInit(initLocale).then(() => { +demoInit(initLocale).then(() => +{ document.getElementById("sf_upload").style.display = "flex"; document.getElementById("file_upload").style.display = "flex"; - loading.classList.add("done") + loading.classList.add("done"); document.documentElement.classList.add("no_scroll"); document.body.classList.add("no_scroll"); - setTimeout(() => { + setTimeout(() => + { loading.style.display = "none"; document.body.classList.remove("no_scroll"); document.documentElement.classList.remove("no_scroll"); - + // check for chrome android - if(isMobile) + if (isMobile) { - if(window.chrome) + if (window.chrome) { showNotification( - window.manager.localeManager.getLocaleString("locale.warnings.warning"), + window.manager.localeManager.getLocaleString( + "locale.warnings.warning"), [{ type: "text", - textContent: window.manager.localeManager.getLocaleString("locale.warnings.chromeMobile"), + textContent: window.manager.localeManager.getLocaleString( + "locale.warnings.chromeMobile") }], 7 ); } } - }, 1000) + }, 1000); /** * @param e {{target: HTMLInputElement}} */ - sfInput.onchange = e => { - if (!e.target.files[0]) { + sfInput.onchange = e => + { + if (!e.target.files[0]) + { return; } /** * @type {File} */ const file = e.target.files[0]; - - if(window.manager.seq) + + if (window.manager.seq) { - window.manager.seq.pause() + window.manager.seq.pause(); } document.getElementById("sf_upload").firstElementChild.innerText = file.name; loading.style.display = ""; - setTimeout(async () => { + setTimeout(async () => + { loading.classList.remove("done"); changeIcon(getHourglassSvg(256), false); - loadingMessage.textContent = window.manager.localeManager.getLocaleString("locale.synthInit.loadingSoundfont"); + loadingMessage.textContent = window.manager.localeManager.getLocaleString( + "locale.synthInit.loadingSoundfont"); const parseStart = performance.now() / 1000; // parse the soundfont let soundFontBuffer; @@ -408,50 +435,57 @@ demoInit(initLocale).then(() => { { soundFontBuffer = await file.arrayBuffer(); window.soundFontParser = soundFontBuffer; - } - catch (e) + } catch (e) { - loadingMessage.textContent = window.manager.localeManager.getLocaleString("locale.warnings.outOfMemory"); + loadingMessage.textContent = window.manager.localeManager.getLocaleString( + "locale.warnings.outOfMemory"); changeIcon(getExclamationSvg(256)); showNotification( manager.localeManager.getLocaleString("locale.warnings.warning"), [{ type: "text", - textContent: window.manager.localeManager.getLocaleString("locale.warnings.outOfMemory"), + textContent: window.manager.localeManager.getLocaleString( + "locale.warnings.outOfMemory") }] ); throw e; } - window.manager.sfError = e => { + window.manager.sfError = e => + { loadingMessage.innerHTML = `Error parsing soundfont:
${e}
`; changeIcon(getExclamationSvg(256)); console.error(e); - } - loadingMessage.textContent = window.manager.localeManager.getLocaleString("locale.synthInit.startingSynthesizer"); + }; + loadingMessage.textContent = window.manager.localeManager.getLocaleString( + "locale.synthInit.startingSynthesizer"); await window.manager.reloadSf(soundFontBuffer); - if(window.manager.seq) + if (window.manager.seq) { window.manager.seq.currentTime -= 0.1; } - loadingMessage.textContent = window.manager.localeManager.getLocaleString("locale.synthInit.savingSoundfont"); + loadingMessage.textContent = window.manager.localeManager.getLocaleString( + "locale.synthInit.savingSoundfont"); await saveSoundFontToIndexedDB(soundFontBuffer); // wait to make sure that the animation has finished const elapsed = (performance.now() / 1000) - parseStart; await new Promise(r => setTimeout(r, 1000 - elapsed)); // DONE - changeIcon(getCheckSvg(256)) - loadingMessage.textContent = window.manager.localeManager.getLocaleString("locale.synthInit.done"); + changeIcon(getCheckSvg(256)); + loadingMessage.textContent = window.manager.localeManager.getLocaleString( + "locale.synthInit.done"); loading.classList.add("done"); document.documentElement.classList.add("no_scroll"); document.body.classList.add("no_scroll"); - setTimeout(() => { + setTimeout(() => + { loading.style.display = "none"; document.body.classList.remove("no_scroll"); document.documentElement.classList.remove("no_scroll"); - }, 1000) + }, 1000); }, ANIMATION_REFLOW_TIME); - } - demoSongButton.onclick = async () => { + }; + demoSongButton.onclick = async () => + { /** * @type {NotificationContent[]} */ @@ -459,20 +493,24 @@ demoInit(initLocale).then(() => { { type: "button", textContent: window.manager.localeManager.getLocaleString("locale.credits"), - onClick: () => { + onClick: () => + { window.open("https://github.com/spessasus/spessasynth-demo-songs#readme"); } }, { type: "button", textContent: "Bundled SoundFont Credits", - onClick: () => { + onClick: () => + { window.open("https://schristiancollins.com/generaluser.php"); } } ]; - titleMessage.textContent = window.manager.localeManager.getLocaleString("locale.synthInit.genericLoading"); - const songs = await ((await fetch("https://spessasus.github.io/spessasynth-demo-songs/demo_song_list.json")).text()); + titleMessage.textContent = window.manager.localeManager.getLocaleString( + "locale.synthInit.genericLoading"); + const songs = await ((await fetch( + "https://spessasus.github.io/spessasynth-demo-songs/demo_song_list.json")).text()); /** * @type {{ * name: string, @@ -480,24 +518,25 @@ demoInit(initLocale).then(() => { * }[]} */ const songsJSON = JSON.parse(songs); - for(const song of songsJSON) + for (const song of songsJSON) { contents.push({ type: "button", textContent: song.name, - onClick:async n => { + onClick: async n => + { closeNotification(n.id); await playDemoSong(song.fileName); } - },) + }); } - + showNotification( window.manager.localeManager.getLocaleString("locale.demoSongButton"), contents, 999999, true, undefined - ) + ); }; }); diff --git a/src/website/js/main/local_main.js b/src/website/js/main/local_main.js index 26ed3e4e..1e1c151e 100644 --- a/src/website/js/main/local_main.js +++ b/src/website/js/main/local_main.js @@ -1,9 +1,9 @@ -"use strict" +"use strict"; -import {Manager} from "../manager/manager.js"; -import { showNotification } from '../notification/notification.js' -import { LocaleManager } from '../locale/locale_manager.js' -import { SpessaSynthLogging } from '../../../spessasynth_lib/utils/loggin.js' +import { Manager } from "../manager/manager.js"; +import { showNotification } from "../notification/notification.js"; +import { LocaleManager } from "../locale/locale_manager.js"; +import { SpessaSynthLogging } from "../../../spessasynth_lib/utils/loggin.js"; /** * local_main.js @@ -50,7 +50,7 @@ window.SPESSASYNTH_VERSION = r; async function fetchFont(fileName, callback) { let response = await fetch(`${fileName}`); - if(!response.ok) + if (!response.ok) { titleMessage.innerText = "Error downloading soundfont!"; throw response; @@ -59,34 +59,38 @@ async function fetchFont(fileName, callback) let reader = await (await response.body).getReader(); let done = false; let dataArray; - try { + try + { dataArray = new Uint8Array(parseInt(size)); - } - catch (e) + } catch (e) { let message = `Your browser ran out of memory. Consider using Firefox or SF3 soundfont instead

(see console for error)`; - if(window.manager) + if (window.manager) { message = manager.localeManager.getLocaleString("locale.warnings.outOfMemory"); } - showNotification("Warning", + showNotification( + "Warning", [{ type: "text", textContent: message - }]); + }] + ); throw e; } let offset = 0; - do{ + do + { let readData = await reader.read(); - if(readData.value) { + if (readData.value) + { dataArray.set(readData.value, offset); offset += readData.value.length; } done = readData.done; let percent = Math.round((offset / size) * 100); callback(percent); - }while(!done); + } while (!done); return dataArray.buffer; } @@ -95,14 +99,14 @@ async function fetchFont(fileName, callback) */ async function startMidi(midiFiles) { - if(!synthReady) + if (!synthReady) { setTimeout(() => startMidi(midiFiles), 100); return; } await manager.ready; let fName; - if(midiFiles[0].name.length > 20) + if (midiFiles[0].name.length > 20) { fName = midiFiles[0].name.substring(0, 21) + "..."; } @@ -110,7 +114,7 @@ async function startMidi(midiFiles) { fName = midiFiles[0].name; } - if(midiFiles.length > 1) + if (midiFiles.length > 1) { fName += ` and ${midiFiles.length - 1} others`; } @@ -120,21 +124,22 @@ async function startMidi(midiFiles) * @type {MIDIFile[]} */ const parsed = []; - for(const file of midiFiles) + for (const file of midiFiles) { parsed.push({ binary: await file.arrayBuffer(), altName: file.name }); } - + titleMessage.style.fontStyle = "italic"; - - if(manager.seq) + + if (manager.seq) { manager.seq.loadNewSongList(parsed); } - else { + else + { manager.play(parsed); } exportButton.style.display = "flex"; @@ -149,15 +154,15 @@ async function replaceFont(fontName) { async function replaceSf() { - + // prompt the user to click if needed - if(!window.audioContextMain) + if (!window.audioContextMain) { titleMessage.innerText = "Press anywhere to start the app"; return; } - - if(!window.manager) + + if (!window.manager) { // prepare the manager window.manager = new Manager(audioContextMain, soundFontParser, localeManager); @@ -168,12 +173,12 @@ async function replaceFont(fontName) } else { - if(window.manager.seq) + if (window.manager.seq) { window.manager.seq.pause(); } await window.manager.reloadSf(window.soundFontParser); - if(window.manager.seq) + if (window.manager.seq) { // resets controllers window.manager.seq.currentTime -= 0.1; @@ -181,21 +186,25 @@ async function replaceFont(fontName) } synthReady = true; } - - if(window.loadedSoundfonts.find(sf => sf.name === fontName)) + + if (window.loadedSoundfonts.find(sf => sf.name === fontName)) { window.soundFontParser = window.loadedSoundfonts.find(sf => sf.name === fontName).sf; await replaceSf(); return; } titleMessage.innerText = "Downloading soundfont..."; - const data = await fetchFont(fontName, percent => progressBar.style.width = `${(percent / 100) * titleMessage.offsetWidth}px`); - + const data = await fetchFont( + fontName, + percent => progressBar.style.width = `${(percent / 100) * titleMessage.offsetWidth}px` + ); + titleMessage.innerText = "Parsing soundfont..."; - setTimeout(() => { + setTimeout(() => + { window.soundFontParser = data; progressBar.style.width = "0"; - window.loadedSoundfonts.push({name: fontName, sf: window.soundFontParser}) + window.loadedSoundfonts.push({ name: fontName, sf: window.soundFontParser }); replaceSf(); }); titleMessage.innerText = window.TITLE; @@ -204,27 +213,27 @@ async function replaceFont(fontName) document.body.onclick = async () => { // user has clicked, we can create the js - if(!window.audioContextMain) + if (!window.audioContextMain) { - if(navigator.mediaSession) + if (navigator.mediaSession) { navigator.mediaSession.playbackState = "playing"; } const context = window.AudioContext || window.webkitAudioContext; - window.audioContextMain = new context({sampleRate: SAMPLE_RATE}); - if(window.soundFontParser) + window.audioContextMain = new context({ sampleRate: SAMPLE_RATE }); + if (window.soundFontParser) { // prepare midi interface window.manager = new Manager(audioContextMain, soundFontParser, localeManager); - window.TITLE = window.manager.localeManager.getLocaleString("locale.titleMessage") - titleMessage.innerText = "Initializing..." + window.TITLE = window.manager.localeManager.getLocaleString("locale.titleMessage"); + titleMessage.innerText = "Initializing..."; await manager.ready; manager.synth.setLogLevel(true, true, true, true); synthReady = true; } } document.body.onclick = null; -} +}; /** * @type {{name: string, size: number}[]} @@ -234,8 +243,9 @@ let soundFonts = []; const localeManager = new LocaleManager(navigator.language.split("-")[0].toLowerCase()); // load the list of soundfonts -fetch("soundfonts").then(async r => { - if(!r.ok) +fetch("soundfonts").then(async r => +{ + if (!r.ok) { titleMessage.innerText = "Error fetching soundfonts!"; throw r.statusText; @@ -244,48 +254,52 @@ fetch("soundfonts").then(async r => { * @type {HTMLSelectElement} */ const sfSelector = document.getElementById("sf_selector"); - + soundFonts = JSON.parse(await r.text()); - for(let sf of soundFonts) + for (let sf of soundFonts) { const option = document.createElement("option"); option.value = sf.name; - let displayName = sf.name - if(displayName.length > 29) + let displayName = sf.name; + if (displayName.length > 29) { displayName = displayName.substring(0, 30) + "..."; } option.innerText = displayName; sfSelector.appendChild(option); } - - sfSelector.onchange = () => { + + sfSelector.onchange = () => + { sfSelector.blur(); fetch(`/setlastsf2?sfname=${encodeURIComponent(sfSelector.value)}`); - if(window.manager.seq) + if (window.manager.seq) { window.manager.seq.pause(); } replaceFont(sfSelector.value); - - if(window.manager.seq) + + if (window.manager.seq) { titleMessage.innerText = window.manager.seq.midiData.midiName || window.TITLE; } - - } - + + }; + // fetch the first sf2 await replaceFont(soundFonts[0].name); - + // start midi if already uploaded - if(fileInput.files[0]) { + if (fileInput.files[0]) + { await startMidi(fileInput.files); } - + // and add the event listener - fileInput.onchange = async () => { - if (!fileInput.files[0]) { + fileInput.onchange = async () => + { + if (!fileInput.files[0]) + { return; } await startMidi(fileInput.files); @@ -315,9 +329,11 @@ window.saveSettings = saveSettings; * reads the settings * @type {Promise} */ -window.savedSettings = new Promise(resolve => { - fetch("/getsettings").then(response => response.json().then( - parsedSettings => { +window.savedSettings = new Promise(resolve => +{ + fetch("/getsettings").then(response => response.json().then( + parsedSettings => + { resolve(parsedSettings); })); }); diff --git a/src/website/js/manager/export_audio.js b/src/website/js/manager/export_audio.js index 540044fb..f0bb279b 100644 --- a/src/website/js/manager/export_audio.js +++ b/src/website/js/manager/export_audio.js @@ -1,10 +1,10 @@ -import { closeNotification, showNotification } from '../notification/notification.js' -import { Synthetizer } from '../../../spessasynth_lib/synthetizer/synthetizer.js' -import { formatTime } from '../../../spessasynth_lib/utils/other.js' -import { audioBufferToWav } from '../../../spessasynth_lib/utils/buffer_to_wav.js' -import { WORKLET_URL_ABSOLUTE } from '../../../spessasynth_lib/synthetizer/worklet_url.js' -import { ANIMATION_REFLOW_TIME } from '../utils/animation_utils.js' -import { MIDIticksToSeconds } from '../../../spessasynth_lib/midi_parser/basic_midi.js' +import { closeNotification, showNotification } from "../notification/notification.js"; +import { Synthetizer } from "../../../spessasynth_lib/synthetizer/synthetizer.js"; +import { formatTime } from "../../../spessasynth_lib/utils/other.js"; +import { audioBufferToWav } from "../../../spessasynth_lib/utils/buffer_to_wav.js"; +import { WORKLET_URL_ABSOLUTE } from "../../../spessasynth_lib/synthetizer/worklet_url.js"; +import { ANIMATION_REFLOW_TIME } from "../utils/animation_utils.js"; +import { MIDIticksToSeconds } from "../../../spessasynth_lib/midi_parser/basic_midi.js"; const RENDER_AUDIO_TIME_INTERVAL = 1000; @@ -21,19 +21,19 @@ const RENDER_AUDIO_TIME_INTERVAL = 1000; export async function _doExportAudioData(normalizeAudio = true, additionalTime = 2, separateChannels = false, meta = {}, loopCount = 0) { this.isExporting = true; - if(!this.seq) + if (!this.seq) { throw new Error("No sequencer active"); } // get locales const exportingMessage = manager.localeManager.getLocaleString(`locale.exportAudio.formats.formats.wav.exportMessage.message`); const estimatedMessage = manager.localeManager.getLocaleString(`locale.exportAudio.formats.formats.wav.exportMessage.estimated`); - const loadingMessage = manager.localeManager.getLocaleString(`locale.synthInit.genericLoading`); + const loadingMessage = manager.localeManager.getLocaleString(`locale.synthInit.genericLoading`); const notification = showNotification( exportingMessage, [ - { type: 'text', textContent: loadingMessage }, - { type: 'progress' } + { type: "text", textContent: loadingMessage }, + { type: "progress" } ], 9999999, false @@ -44,9 +44,9 @@ export async function _doExportAudioData(normalizeAudio = true, additionalTime = let loopDuration = loopEndAbsolute - loopStartAbsolute; const duration = parsedMid.duration + additionalTime + loopDuration * loopCount; const sampleRate = this.context.sampleRate; - + let sampleDuration = sampleRate * duration; - + // prepare audio context const offline = new OfflineAudioContext({ numberOfChannels: separateChannels ? 32 : 2, @@ -54,13 +54,13 @@ export async function _doExportAudioData(normalizeAudio = true, additionalTime = length: sampleDuration }); await offline.audioWorklet.addModule(new URL("../../spessasynth_lib/" + WORKLET_URL_ABSOLUTE, import.meta.url)); - + /** * take snapshot of the real synth * @type {SynthesizerSnapshot} */ const snapshot = await this.synth.getSynthesizerSnapshot(); - + const soundfont = this.soundFont; /** * Prepare synthesizer @@ -80,8 +80,7 @@ export async function _doExportAudioData(normalizeAudio = true, additionalTime = chorusConfig: undefined, reverbImpulseResponse: this.impulseResponse }); - } - catch (e) + } catch (e) { showNotification( this.localeManager.getLocaleString("locale.warnings.warning"), @@ -89,19 +88,20 @@ export async function _doExportAudioData(normalizeAudio = true, additionalTime = type: "text", textContent: this.localeManager.getLocaleString("locale.warnings.outOfMemory") }] - ) + ); throw e; } - + const detailMessage = notification.div.getElementsByTagName("p")[0]; const progressDiv = notification.div.getElementsByClassName("notification_progress")[0]; - + const RATI_SECONDS = RENDER_AUDIO_TIME_INTERVAL / 1000; let rendered = synth.currentTime; let estimatedTime = duration; const smoothingFactor = 0.1; // for smoothing estimated time - - const interval = setInterval(() => { + + const interval = setInterval(() => + { // calculate estimated time let hasRendered = synth.currentTime - rendered; rendered = synth.currentTime; @@ -115,28 +115,29 @@ export async function _doExportAudioData(normalizeAudio = true, additionalTime = } // smooth out estimated using exponential moving average estimatedTime = smoothingFactor * estimated + (1 - smoothingFactor) * estimatedTime; - detailMessage.innerText = `${estimatedMessage} ${formatTime(estimatedTime).time}` + detailMessage.innerText = `${estimatedMessage} ${formatTime(estimatedTime).time}`; }, RENDER_AUDIO_TIME_INTERVAL); - + const buf = await offline.startRendering(); progressDiv.style.width = "100%"; // clear intervals and save file clearInterval(interval); - detailMessage.innerText = this.localeManager.getLocaleString("locale.exportAudio.formats.formats.wav.exportMessage.convertWav"); + detailMessage.innerText = this.localeManager.getLocaleString( + "locale.exportAudio.formats.formats.wav.exportMessage.convertWav"); // let the browser show await new Promise(r => setTimeout(r, ANIMATION_REFLOW_TIME)); - if(!separateChannels) + if (!separateChannels) { const startOffset = MIDIticksToSeconds(parsedMid.firstNoteOn, parsedMid); const loopStart = loopStartAbsolute - startOffset; const loopEnd = loopEndAbsolute - startOffset; let loop = undefined; - if(loopCount === 0) + if (loopCount === 0) { - loop = {start: loopStart, end: loopEnd}; + loop = { start: loopStart, end: loopEnd }; } const wav = audioBufferToWav(buf, normalizeAudio, 0, meta, loop); - this.saveBlob(wav, `${this.seqUI.currentSongTitle || 'unnamed_song'}.wav`); + this.saveBlob(wav, `${this.seqUI.currentSongTitle || "unnamed_song"}.wav`); } else { @@ -146,7 +147,7 @@ export async function _doExportAudioData(normalizeAudio = true, additionalTime = */ const content = []; const usedChannels = new Set(); - for(const p of parsedMid.usedChannelsOnTrack) + for (const p of parsedMid.usedChannelsOnTrack) { p.forEach(c => usedChannels.add(c)); } @@ -156,25 +157,27 @@ export async function _doExportAudioData(normalizeAudio = true, additionalTime = let muted = true; for (let j = i; j < snapshot.channelSnapshots.length; j += 16) { - if(!snapshot.channelSnapshots[j].isMuted) + if (!snapshot.channelSnapshots[j].isMuted) { muted = false; break; } } - if(!usedChannels.has(i) || muted) + if (!usedChannels.has(i) || muted) { continue; } content.push({ type: "button", textContent: this.localeManager.getLocaleString(separatePath + "save", [i + 1]), - onClick: async (n, target) => { - + onClick: async (n, target) => + { + const text = target.textContent; - target.textContent = this.localeManager.getLocaleString("locale.exportAudio.formats.formats.wav.exportMessage.convertWav"); + target.textContent = this.localeManager.getLocaleString( + "locale.exportAudio.formats.formats.wav.exportMessage.convertWav"); await new Promise(r => setTimeout(r, ANIMATION_REFLOW_TIME)); - + const audioOut = audioBufferToWav(buf, false, i * 2); const fileName = `${i + 1} - ${snapshot.channelSnapshots[i].patchName}.wav`; this.saveBlob(audioOut, fileName); @@ -195,7 +198,7 @@ export async function _doExportAudioData(normalizeAudio = true, additionalTime = flexDirection: "row" } ); - n.div.style.width = '30rem' + n.div.style.width = "30rem"; } closeNotification(notification.id); this.isExporting = false; @@ -208,18 +211,20 @@ export async function _doExportAudioData(normalizeAudio = true, additionalTime = */ export async function _exportAudioData() { - if(this.isExporting) + if (this.isExporting) { return; } const wavPath = `locale.exportAudio.formats.formats.wav.options.`; const metadataPath = "locale.exportAudio.formats.metadata."; - const verifyDecode = (type, def, decoder) => { - return this.seq.midiData.RMIDInfo?.[type] === undefined ? def : decoder.decode(this.seq.midiData.RMIDInfo?.[type]).replace(/\0$/, '') - } + const verifyDecode = (type, def, decoder) => + { + return this.seq.midiData.RMIDInfo?.[type] === undefined ? def : decoder.decode(this.seq.midiData.RMIDInfo?.[type]) + .replace(/\0$/, ""); + }; const encoding = verifyDecode("IENC", "ascii", new TextDecoder()); const decoder = new TextDecoder(encoding); - + const startAlbum = verifyDecode("IPRD", "", decoder); const startArtist = verifyDecode("IART", "", decoder); const startGenre = verifyDecode("IGNR", "", decoder); @@ -299,7 +304,8 @@ export async function _exportAudioData() { type: "button", textContent: this.localeManager.getLocaleString(wavPath + "confirm"), - onClick: n => { + onClick: n => + { closeNotification(n.id); const normalizeVolume = n.div.querySelector("input[normalize-volume-toggle]").checked; const additionalTime = n.div.querySelector("input[additional-time]").value; @@ -316,14 +322,20 @@ export async function _exportAudioData() artist: artist.length > 0 ? artist : undefined, album: album.length > 0 ? album : undefined, title: title.length > 0 ? title : undefined, - genre: genre.length > 0 ? genre : undefined, - } - - this._doExportAudioData(normalizeVolume, parseInt(additionalTime), separateChannels, metadata, parseInt(loopCount)); + genre: genre.length > 0 ? genre : undefined + }; + + this._doExportAudioData( + normalizeVolume, + parseInt(additionalTime), + separateChannels, + metadata, + parseInt(loopCount) + ); } } ]; - + /** * @type {NotificationContent[]} */ diff --git a/src/website/js/manager/export_midi.js b/src/website/js/manager/export_midi.js index fd122619..2aee7e9e 100644 --- a/src/website/js/manager/export_midi.js +++ b/src/website/js/manager/export_midi.js @@ -1,5 +1,5 @@ -import { applySnapshotToMIDI } from '../../../spessasynth_lib/midi_parser/midi_editor.js' -import { writeMIDIFile } from '../../../spessasynth_lib/midi_parser/midi_writer.js' +import { applySnapshotToMIDI } from "../../../spessasynth_lib/midi_parser/midi_editor.js"; +import { writeMIDIFile } from "../../../spessasynth_lib/midi_parser/midi_writer.js"; /** * Changes the MIDI according to locked controllers and programs and exports it as a file @@ -13,5 +13,5 @@ export async function exportMidi() // export modified midi and write out const file = writeMIDIFile(mid); const blob = new Blob([file], { type: "audio/mid" }); - this.saveBlob(blob, `${this.seqUI.currentSongTitle || "unnamed_song"}.mid`) + this.saveBlob(blob, `${this.seqUI.currentSongTitle || "unnamed_song"}.mid`); } \ No newline at end of file diff --git a/src/website/js/manager/export_rmidi.js b/src/website/js/manager/export_rmidi.js index 1987e71b..3783c307 100644 --- a/src/website/js/manager/export_rmidi.js +++ b/src/website/js/manager/export_rmidi.js @@ -1,11 +1,11 @@ -import { trimSoundfont } from '../../../spessasynth_lib/soundfont/basic_soundfont/write_sf2/soundfont_trimmer.js' -import { applySnapshotToMIDI } from '../../../spessasynth_lib/midi_parser/midi_editor.js' -import { closeNotification, showNotification } from '../notification/notification.js' -import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd } from '../../../spessasynth_lib/utils/loggin.js' -import { consoleColors } from '../../../spessasynth_lib/utils/other.js' -import { writeRMIDI } from '../../../spessasynth_lib/midi_parser/rmidi_writer.js' -import { ANIMATION_REFLOW_TIME } from '../utils/animation_utils.js' -import { loadSoundFont } from '../../../spessasynth_lib/soundfont/load_soundfont.js' +import { trimSoundfont } from "../../../spessasynth_lib/soundfont/basic_soundfont/write_sf2/soundfont_trimmer.js"; +import { applySnapshotToMIDI } from "../../../spessasynth_lib/midi_parser/midi_editor.js"; +import { closeNotification, showNotification } from "../notification/notification.js"; +import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd } from "../../../spessasynth_lib/utils/loggin.js"; +import { consoleColors } from "../../../spessasynth_lib/utils/other.js"; +import { writeRMIDI } from "../../../spessasynth_lib/midi_parser/rmidi_writer.js"; +import { ANIMATION_REFLOW_TIME } from "../utils/animation_utils.js"; +import { loadSoundFont } from "../../../spessasynth_lib/soundfont/load_soundfont.js"; /** * @this {Manager} @@ -20,17 +20,23 @@ export async function _exportRMIDI() * @param decoder {TextDecoder} * @return {string} */ - const verifyDecode = (type, def, decoder) => { - return this.seq.midiData.RMIDInfo?.[type] === undefined ? def : decoder.decode(this.seq.midiData.RMIDInfo?.[type]).replace(/\0$/, '') - } + const verifyDecode = (type, def, decoder) => + { + return this.seq.midiData.RMIDInfo?.[type] === undefined ? def : decoder.decode(this.seq.midiData.RMIDInfo?.[type]) + .replace(/\0$/, ""); + }; const encoding = verifyDecode("IENC", "ascii", new TextDecoder()); const decoder = new TextDecoder(encoding); - + const startAlbum = verifyDecode("IPRD", "", decoder); const startArtist = verifyDecode("IART", "", decoder); const startGenre = verifyDecode("IGNR", "", decoder); - const startComment = verifyDecode("ICMT", "Created using SpessaSynth: https://spessasus.github.io/SpessaSynth", decoder); - + const startComment = verifyDecode( + "ICMT", + "Created using SpessaSynth: https://spessasus.github.io/SpessaSynth", + decoder + ); + const path = "locale.exportAudio.formats.formats.rmidi.options."; const metadataPath = "locale.exportAudio.formats.metadata."; const n = showNotification( @@ -127,7 +133,8 @@ export async function _exportRMIDI() { type: "button", textContent: this.localeManager.getLocaleString(path + "confirm"), - onClick: async n => { + onClick: async n => + { const compressed = n.div.querySelector("input[compress-toggle='1']").checked; const quality = parseInt(n.div.querySelector("input[type='range']").value) / 10; const album = n.div.querySelector("input[name='album']").value; @@ -137,15 +144,17 @@ export async function _exportRMIDI() const genre = n.div.querySelector("input[name='genre']").value; const bankOffset = parseInt(n.div.querySelector("input[name='bank_offset']").value); const adjust = n.div.querySelector("input[name='adjust']").checked; - + /** * @type {File} */ const picture = n.div.querySelector("input[type='file']")?.files[0]; closeNotification(n.id); - - SpessaSynthGroupCollapsed("%cExporting RMIDI...", - consoleColors.info); + + SpessaSynthGroupCollapsed( + "%cExporting RMIDI...", + consoleColors.info + ); const localePath = "locale.exportAudio.formats.formats.rmidi.progress."; const notification = showNotification( this.localeManager.getLocaleString(localePath + "title"), @@ -164,35 +173,35 @@ export async function _exportRMIDI() const message = notification.div.getElementsByClassName("export_rmidi_message")[0]; const mid = await this.seq.getMIDI(); const font = loadSoundFont(mid.embeddedSoundFont || this.soundFont); - + message.textContent = this.localeManager.getLocaleString(localePath + "modifyingMIDI"); await new Promise(r => setTimeout(r, ANIMATION_REFLOW_TIME)); - + applySnapshotToMIDI(mid, await this.synth.getSynthesizerSnapshot()); - + message.textContent = this.localeManager.getLocaleString(localePath + "modifyingSoundfont"); await new Promise(r => setTimeout(r, ANIMATION_REFLOW_TIME)); - + trimSoundfont(font, mid); const newFont = font.write({ compress: compressed, compressionQuality: quality, compressionFunction: this.compressionFunc }); - + message.textContent = this.localeManager.getLocaleString(localePath + "saving"); await new Promise(r => setTimeout(r, ANIMATION_REFLOW_TIME)); - + let fileBuffer = undefined; - if(picture?.type.split("/")[0] === "image") + if (picture?.type.split("/")[0] === "image") { fileBuffer = await picture.arrayBuffer(); } - else if(mid.RMIDInfo?.["IPIC"] !== undefined) + else if (mid.RMIDInfo?.["IPIC"] !== undefined) { fileBuffer = mid.RMIDInfo["IPIC"].buffer; } - + const rmidBinary = writeRMIDI( newFont, mid, @@ -211,7 +220,7 @@ export async function _exportRMIDI() }, adjust ); - const blob = new Blob([rmidBinary.buffer], {type: "audio/rmid"}) + const blob = new Blob([rmidBinary.buffer], { type: "audio/rmid" }); this.saveBlob(blob, `${songTitle || "unnamed_song"}.rmi`); message.textContent = this.localeManager.getLocaleString(localePath + "done"); closeNotification(notification.id); @@ -224,10 +233,11 @@ export async function _exportRMIDI() this.localeManager ); const input = n.div.querySelector("input[type='file']"); - input.oninput = () => { - if(input.files[0]) + input.oninput = () => + { + if (input.files[0]) { input.parentElement.firstChild.textContent = input.files[0].name; } - } + }; } \ No newline at end of file diff --git a/src/website/js/manager/export_song.js b/src/website/js/manager/export_song.js index da2d51ef..26cee1cf 100644 --- a/src/website/js/manager/export_song.js +++ b/src/website/js/manager/export_song.js @@ -1,4 +1,4 @@ -import { closeNotification, showNotification } from '../notification/notification.js' +import { closeNotification, showNotification } from "../notification/notification.js"; /** * @this {Manager} @@ -13,7 +13,8 @@ export async function exportSong() { type: "button", translatePathTitle: path + "formats.wav.button", - onClick: n => { + onClick: n => + { closeNotification(n.id); this._exportAudioData(); } @@ -21,7 +22,8 @@ export async function exportSong() { type: "button", translatePathTitle: path + "formats.midi.button", - onClick: n => { + onClick: n => + { closeNotification(n.id); this.exportMidi(); } @@ -29,13 +31,13 @@ export async function exportSong() { type: "button", translatePathTitle: path + "formats.soundfont.button", - onClick: n => { + onClick: n => + { closeNotification(n.id); try { this._exportSoundfont(); - } - catch (e) + } catch (e) { showNotification( "Warning", @@ -50,13 +52,13 @@ export async function exportSong() { type: "button", translatePathTitle: path + "formats.rmidi.button", - onClick: n => { + onClick: n => + { closeNotification(n.id); try { this._exportRMIDI(); - } - catch (e) + } catch (e) { showNotification( "Warning", @@ -77,5 +79,5 @@ export async function exportSong() flexWrap: "wrap", justifyContent: "center" } - ) + ); } \ No newline at end of file diff --git a/src/website/js/manager/export_soundfont.js b/src/website/js/manager/export_soundfont.js index 7a5a3dbd..37ad2663 100644 --- a/src/website/js/manager/export_soundfont.js +++ b/src/website/js/manager/export_soundfont.js @@ -1,12 +1,9 @@ -import { applySnapshotToMIDI } from '../../../spessasynth_lib/midi_parser/midi_editor.js' -import { - SpessaSynthGroup, - SpessaSynthGroupEnd, -} from '../../../spessasynth_lib/utils/loggin.js' -import { consoleColors } from '../../../spessasynth_lib/utils/other.js' -import { trimSoundfont } from '../../../spessasynth_lib/soundfont/basic_soundfont/write_sf2/soundfont_trimmer.js' -import { closeNotification, showNotification } from '../notification/notification.js' -import { loadSoundFont } from '../../../spessasynth_lib/soundfont/load_soundfont.js' +import { applySnapshotToMIDI } from "../../../spessasynth_lib/midi_parser/midi_editor.js"; +import { SpessaSynthGroup, SpessaSynthGroupEnd } from "../../../spessasynth_lib/utils/loggin.js"; +import { consoleColors } from "../../../spessasynth_lib/utils/other.js"; +import { trimSoundfont } from "../../../spessasynth_lib/soundfont/basic_soundfont/write_sf2/soundfont_trimmer.js"; +import { closeNotification, showNotification } from "../notification/notification.js"; +import { loadSoundFont } from "../../../spessasynth_lib/soundfont/load_soundfont.js"; /** * @this {Manager} @@ -31,7 +28,7 @@ export async function _exportSoundfont() type: "toggle", translatePathTitle: path + "compress", attributes: { - "compress-toggle": "1", + "compress-toggle": "1" } }, { @@ -46,17 +43,20 @@ export async function _exportSoundfont() { type: "button", textContent: this.localeManager.getLocaleString(path + "confirm"), - onClick: async n => { + onClick: async n => + { const trimmed = n.div.querySelector("input[trim-toggle='1']").checked; const compressed = n.div.querySelector("input[compress-toggle='1']").checked; const quality = parseInt(n.div.querySelector("input[type='range']").value) / 10; closeNotification(n.id); - SpessaSynthGroup("%cExporting minified soundfont...", - consoleColors.info); + SpessaSynthGroup( + "%cExporting minified soundfont...", + consoleColors.info + ); const mid = await this.seq.getMIDI(); const soundfont = loadSoundFont(mid.embeddedSoundFont || this.soundFont); applySnapshotToMIDI(mid, await this.synth.getSynthesizerSnapshot()); - if(trimmed) + if (trimmed) { trimSoundfont(soundfont, mid); } @@ -65,9 +65,9 @@ export async function _exportSoundfont() compressionQuality: quality, compressionFunction: this.compressionFunc }); - const blob = new Blob([binary.buffer], {type: "audio/soundfont"}); + const blob = new Blob([binary.buffer], { type: "audio/soundfont" }); let extension = soundfont.soundFontInfo["ifil"].split(".")[0] === "3" ? "sf3" : "sf2"; - this.saveBlob(blob, `${soundfont.soundFontInfo['INAM'] || "unnamed"}.${extension}`); + this.saveBlob(blob, `${soundfont.soundFontInfo["INAM"] || "unnamed"}.${extension}`); SpessaSynthGroupEnd(); } } diff --git a/src/website/js/manager/manager.js b/src/website/js/manager/manager.js index a3416b8f..e7d26043 100644 --- a/src/website/js/manager/manager.js +++ b/src/website/js/manager/manager.js @@ -1,62 +1,66 @@ -import { MidiKeyboard } from '../midi_keyboard/midi_keyboard.js' -import { Synthetizer } from '../../../spessasynth_lib/synthetizer/synthetizer.js' -import { Renderer } from '../renderer/renderer.js' - -import { SequencerUI } from '../sequencer_ui/sequencer_ui.js' -import { SynthetizerUI } from '../synthesizer_ui/synthetizer_ui.js' -import { MIDIDeviceHandler } from '../../../spessasynth_lib/external_midi/midi_handler.js' -import { WebMidiLinkHandler } from '../../../spessasynth_lib/external_midi/web_midi_link.js' -import { Sequencer } from '../../../spessasynth_lib/sequencer/sequencer.js' -import { SpessaSynthSettings } from '../settings_ui/settings.js' -import { MusicModeUI } from '../music_mode_ui/music_mode_ui.js' -import { LocaleManager } from '../locale/locale_manager.js' -import { isMobile } from '../utils/is_mobile.js' -import { SpessaSynthInfo } from '../../../spessasynth_lib/utils/loggin.js' -import { keybinds } from '../utils/keybinds.js' -import { _doExportAudioData, _exportAudioData } from './export_audio.js' -import { exportMidi } from './export_midi.js' -import { _exportSoundfont } from './export_soundfont.js' -import { exportSong } from './export_song.js' -import { _exportRMIDI } from './export_rmidi.js' -import { WORKLET_URL_ABSOLUTE } from '../../../spessasynth_lib/synthetizer/worklet_url.js' -import { encodeVorbis } from '../../../spessasynth_lib/utils/encode_vorbis.js' -import { loadSoundFont } from '../../../spessasynth_lib/soundfont/load_soundfont.js' -import { readBytesAsString } from '../../../spessasynth_lib/utils/byte_functions/string.js' -import { IndexedByteArray } from '../../../spessasynth_lib/utils/indexed_array.js' -import { closeNotification, showNotification } from '../notification/notification.js' -import { DropFileHandler } from '../utils/drop_file_handler.js' +import { MidiKeyboard } from "../midi_keyboard/midi_keyboard.js"; +import { Synthetizer } from "../../../spessasynth_lib/synthetizer/synthetizer.js"; +import { Renderer } from "../renderer/renderer.js"; + +import { SequencerUI } from "../sequencer_ui/sequencer_ui.js"; +import { SynthetizerUI } from "../synthesizer_ui/synthetizer_ui.js"; +import { MIDIDeviceHandler } from "../../../spessasynth_lib/external_midi/midi_handler.js"; +import { WebMidiLinkHandler } from "../../../spessasynth_lib/external_midi/web_midi_link.js"; +import { Sequencer } from "../../../spessasynth_lib/sequencer/sequencer.js"; +import { SpessaSynthSettings } from "../settings_ui/settings.js"; +import { MusicModeUI } from "../music_mode_ui/music_mode_ui.js"; +import { LocaleManager } from "../locale/locale_manager.js"; +import { isMobile } from "../utils/is_mobile.js"; +import { SpessaSynthInfo } from "../../../spessasynth_lib/utils/loggin.js"; +import { keybinds } from "../utils/keybinds.js"; +import { _doExportAudioData, _exportAudioData } from "./export_audio.js"; +import { exportMidi } from "./export_midi.js"; +import { _exportSoundfont } from "./export_soundfont.js"; +import { exportSong } from "./export_song.js"; +import { _exportRMIDI } from "./export_rmidi.js"; +import { WORKLET_URL_ABSOLUTE } from "../../../spessasynth_lib/synthetizer/worklet_url.js"; +import { encodeVorbis } from "../../../spessasynth_lib/utils/encode_vorbis.js"; +import { loadSoundFont } from "../../../spessasynth_lib/soundfont/load_soundfont.js"; +import { readBytesAsString } from "../../../spessasynth_lib/utils/byte_functions/string.js"; +import { IndexedByteArray } from "../../../spessasynth_lib/utils/indexed_array.js"; +import { closeNotification, showNotification } from "../notification/notification.js"; +import { DropFileHandler } from "../utils/drop_file_handler.js"; // this enables transitions on body because if we enable them on load, it flashbangs us with white document.body.classList.add("load"); /** -* manager.js -* purpose: connects every element of spessasynth together -*/ + * manager.js + * purpose: connects every element of spessasynth together + */ const ENABLE_DEBUG = false; class Manager { channelColors = [ - 'rgba(255, 99, 71, 1)', // tomato - 'rgba(255, 165, 0, 1)', // orange - 'rgba(255, 215, 0, 1)', // gold - 'rgba(50, 205, 50, 1)', // limegreen - 'rgba(60, 179, 113, 1)', // mediumseagreen - 'rgba(0, 128, 0, 1)', // green - 'rgba(0, 191, 255, 1)', // deepskyblue - 'rgba(65, 105, 225, 1)', // royalblue - 'rgba(138, 43, 226, 1)', // blueviolet - 'rgba(50, 120, 125, 1)', //'rgba(218, 112, 214, 1)', // percission color - 'rgba(255, 0, 255, 1)', // magenta - 'rgba(255, 20, 147, 1)', // deeppink - 'rgba(218, 112, 214, 1)', // orchid - 'rgba(240, 128, 128, 1)', // lightcoral - 'rgba(255, 192, 203, 1)', // pink - 'rgba(255, 255, 0, 1)' // yellow + "rgba(255, 99, 71, 1)", // tomato + "rgba(255, 165, 0, 1)", // orange + "rgba(255, 215, 0, 1)", // gold + "rgba(50, 205, 50, 1)", // limegreen + "rgba(60, 179, 113, 1)", // mediumseagreen + "rgba(0, 128, 0, 1)", // green + "rgba(0, 191, 255, 1)", // deepskyblue + "rgba(65, 105, 225, 1)", // royalblue + "rgba(138, 43, 226, 1)", // blueviolet + "rgba(50, 120, 125, 1)", //'rgba(218, 112, 214, 1)', // percission color + "rgba(255, 0, 255, 1)", // magenta + "rgba(255, 20, 147, 1)", // deeppink + "rgba(218, 112, 214, 1)", // orchid + "rgba(240, 128, 128, 1)", // lightcoral + "rgba(255, 192, 203, 1)", // pink + "rgba(255, 255, 0, 1)" // yellow ]; - + /** + * @type {function(string)} + */ + sfError; + /** * Creates a new midi user interface. * @param context {AudioContext} @@ -71,11 +75,12 @@ class Manager this.compressionFunc = encodeVorbis; let solve; this.ready = new Promise(resolve => solve = resolve); - this.initializeContext(context, soundFontBuffer).then(() => { + this.initializeContext(context, soundFontBuffer).then(() => + { solve(); }); } - + saveBlob(blob, name) { const url = URL.createObjectURL(blob); @@ -88,12 +93,7 @@ class Manager a.click(); SpessaSynthInfo(a); } - - /** - * @type {function(string)} - */ - sfError; - + /** * @param context {BaseAudioContext} * @param soundFont {ArrayBuffer} @@ -101,54 +101,65 @@ class Manager */ async initializeContext(context, soundFont) { - - if(!context.audioWorklet) + + if (!context.audioWorklet) { - alert("Audio worklet is not supported on your browser. Sorry!") - throw "Not supported." + alert("Audio worklet is not supported on your browser. Sorry!"); + throw "Not supported."; } - + // bind every element with translate-path to translation - for(const element of document.querySelectorAll("*[translate-path]")) + for (const element of document.querySelectorAll("*[translate-path]")) { this.localeManager.bindObjectProperty(element, "innerText", element.getAttribute("translate-path")); } - + // same with title - for(const element of document.querySelectorAll("*[translate-path-title]")) + for (const element of document.querySelectorAll("*[translate-path-title]")) { - this.localeManager.bindObjectProperty(element, "innerText", element.getAttribute("translate-path-title") + ".title"); - this.localeManager.bindObjectProperty(element, "title", element.getAttribute("translate-path-title") + ".description"); + this.localeManager.bindObjectProperty( + element, + "innerText", + element.getAttribute("translate-path-title") + ".title" + ); + this.localeManager.bindObjectProperty( + element, + "title", + element.getAttribute("translate-path-title") + ".description" + ); } - + const DEBUG_PATH = "synthetizer/worklet_system/worklet_processor.js"; const WORKLET_PATH = ENABLE_DEBUG ? DEBUG_PATH : WORKLET_URL_ABSOLUTE; - if(ENABLE_DEBUG) + if (ENABLE_DEBUG) { console.warn("DEBUG ENABLED! DEBUGGING ENABLED!!"); } - - if(context.audioWorklet) + + if (context.audioWorklet) { - await context.audioWorklet.addModule(new URL ("../../spessasynth_lib/" + WORKLET_PATH, import.meta.url)); + await context.audioWorklet.addModule(new URL("../../spessasynth_lib/" + WORKLET_PATH, import.meta.url)); } /** * set up soundfont * @type {ArrayBuffer} */ this.soundFont = soundFont; - + // set up buffer here (if we let spessasynth use the default buffer, there's no reverb for the first second.) - const impulseURL = new URL("../../spessasynth_lib/synthetizer/audio_effects/impulse_response_2.flac", import.meta.url); - const response = await fetch(impulseURL) + const impulseURL = new URL( + "../../spessasynth_lib/synthetizer/audio_effects/impulse_response_2.flac", + import.meta.url + ); + const response = await fetch(impulseURL); const data = await response.arrayBuffer(); this.impulseResponse = await context.decodeAudioData(data); - + this.audioDelay = new DelayNode(context, { delayTime: 0 }); this.audioDelay.connect(context.destination); - + // set up synthetizer this.synth = new Synthetizer( this.audioDelay, @@ -160,46 +171,55 @@ class Manager chorusConfig: undefined, reverbImpulseResponse: this.impulseResponse, reverbEnabled: true - }); - this.synth.eventHandler.addEvent("soundfonterror", "manager-sf-error", e => { - if(this.sfError) + } + ); + this.synth.eventHandler.addEvent("soundfonterror", "manager-sf-error", e => + { + if (this.sfError) { this.sfError(e); } }); await this.synth.isReady; - + // set up midi access this.midHandler = new MIDIDeviceHandler(); - + // set up web midi link this.wml = new WebMidiLinkHandler(this.synth); - + // set up keyboard this.keyboard = new MidiKeyboard(this.channelColors, this.synth); - + /** * set up renderer * @type {HTMLCanvasElement} */ const canvas = document.getElementById("note_canvas"); - + canvas.width = window.innerWidth * window.devicePixelRatio; canvas.height = window.innerHeight * window.devicePixelRatio; - - this.renderer = new Renderer(this.channelColors, this.synth, canvas, this.audioDelay, window.SPESSASYNTH_VERSION); + + this.renderer = new Renderer( + this.channelColors, + this.synth, + canvas, + this.audioDelay, + window.SPESSASYNTH_VERSION + ); this.renderer.render(true); - + let titleSwappedWithSettings = false; - const checkResize = () => { + const checkResize = () => + { canvas.width = window.innerWidth * window.devicePixelRatio; canvas.height = window.innerHeight * window.devicePixelRatio; this.renderer.computeColors(); - if(isMobile) + if (isMobile) { - if(window.innerWidth / window.innerHeight > 1) + if (window.innerWidth / window.innerHeight > 1) { - if(!titleSwappedWithSettings) + if (!titleSwappedWithSettings) { const title = document.getElementById("title_wrapper"); const settings = document.getElementById("settings_div"); @@ -207,38 +227,42 @@ class Manager title.parentElement.insertBefore(settings, title); } } - else if(titleSwappedWithSettings) + else if (titleSwappedWithSettings) { const title = document.getElementById("title_wrapper"); const settings = document.getElementById("settings_div"); titleSwappedWithSettings = false; title.parentElement.insertBefore(title, settings); } - + } this.renderer.render(false, true); - } + }; checkResize(); window.addEventListener("resize", checkResize.bind(this)); - window.addEventListener("orientationchange", checkResize.bind(this)) - + window.addEventListener("orientationchange", checkResize.bind(this)); + // if on mobile, switch to a 2 octave keyboard - if(isMobile) + if (isMobile) { - this.renderer.keyRange = {min: 48, max: 72}; - this.keyboard.setKeyRange({min: 48, max: 72}, false); + this.renderer.keyRange = { min: 48, max: 72 }; + this.keyboard.setKeyRange({ min: 48, max: 72 }, false); } - + // set up synth UI - this.synthUI = new SynthetizerUI(this.channelColors, document.getElementById("synthetizer_controls"), this.localeManager); + this.synthUI = new SynthetizerUI( + this.channelColors, + document.getElementById("synthetizer_controls"), + this.localeManager + ); this.synthUI.connectSynth(this.synth); - + // create an UI for music player mode this.playerUI = new MusicModeUI(document.getElementById("player_info"), this.localeManager); - + // create an UI for sequencer this.seqUI = new SequencerUI(document.getElementById("sequencer_controls"), this.localeManager, this.playerUI); - + // set up settings UI this.settingsUI = new SpessaSynthSettings( document.getElementById("settings_div"), @@ -248,38 +272,47 @@ class Manager this.keyboard, this.midHandler, this.playerUI, - this.localeManager); - + this.localeManager + ); + // set up drop file handler - this.dropFileHandler = new DropFileHandler((data) => { - this.play([{binary: data.buf, altName: data.name}]); - if(data.name.length > 20) + this.dropFileHandler = new DropFileHandler((data) => + { + this.play([{ binary: data.buf, altName: data.name }]); + if (data.name.length > 20) { data.name = data.name.substring(0, 21) + "..."; } document.getElementById("file_upload").textContent = data.name; - }, buf => { + }, buf => + { this.reloadSf(buf); }); - + // set up soundfont mixer (unfinished) //this.soundFontMixer = new SoundFontMixer(document.getElementsByClassName("midi_and_sf_controller")[0], this.synth, this.synthUI); //this.soundFontMixer.soundFontChange(soundFont); - + // add keypresses - document.addEventListener("keydown", e => { - switch (e.key.toLowerCase()) { + document.addEventListener("keydown", e => + { + switch (e.key.toLowerCase()) + { case keybinds.cinematicMode: - if(this.seq) + if (this.seq) { this.seq.pause(); } - const response = window.prompt("Cinematic mode activated!\n Paste the link to the image for canvas (leave blank to disable)", ""); - if(this.seq) + const response = window.prompt( + "Cinematic mode activated!\n Paste the link to the image for canvas (leave blank to disable)", + "" + ); + if (this.seq) { this.seq.play(); } - if (response === null) { + if (response === null) + { return; } canvas.style.background = `linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)), center center / cover url("${response}")`; @@ -287,15 +320,19 @@ class Manager document.getElementsByClassName("bottom_part")[0].style.display = "none"; document.body.requestFullscreen().then(); break; - + case keybinds.videoMode: - if(this.seq) + if (this.seq) { this.seq.pause(); } - const videoSource = window.prompt("Video mode!\n Paste the link to the video source (leave blank to disable)\n" + - "Note: the video will be available in console as 'video'", ""); - if (videoSource === null) { + const videoSource = window.prompt( + "Video mode!\n Paste the link to the video source (leave blank to disable)\n" + + "Note: the video will be available in console as 'video'", + "" + ); + if (videoSource === null) + { return; } /** @@ -307,16 +344,17 @@ class Manager canvas.parentElement.appendChild(video); video.play(); window.video = video; - if(this.seq) + if (this.seq) { video.currentTime = parseFloat(window.prompt("Video offset to sync to midi, in seconds.", "0")); video.play(); this.seq.currentTime = 0; } - document.addEventListener("keydown", e => { - if(e.key === " ") + document.addEventListener("keydown", e => + { + if (e.key === " ") { - if(video.paused) + if (video.paused) { video.play(); } @@ -325,21 +363,21 @@ class Manager video.pause(); } } - }) - + }); + break; } }); this.renderer.render(false, true); } - + doDLSCheck() { - if(window.isLocalEdition !== true) + if (window.isLocalEdition !== true) { const text = this.soundFont.slice(8, 12); const header = readBytesAsString(new IndexedByteArray(text), 4); - if(header.toLowerCase() === "dls ") + if (header.toLowerCase() === "dls ") { showNotification( this.localeManager.getLocaleString("locale.convertDls.title"), @@ -351,7 +389,8 @@ class Manager { type: "button", textContent: this.localeManager.getLocaleString("locale.yes"), - onClick: n => { + onClick: n => + { closeNotification(n.id); this.downloadDesfont(); } @@ -359,15 +398,18 @@ class Manager { type: "button", textContent: this.localeManager.getLocaleString("locale.no"), - onClick: n => { closeNotification(n.id) } + onClick: n => + { + closeNotification(n.id); + } } ], 99999999 - ) + ); } } } - + /** * @param sf {ArrayBuffer} */ @@ -376,11 +418,12 @@ class Manager //this.soundFontMixer.soundFontChange(sf); await this.synth.soundfontManager.reloadManager(sf); this.soundFont = sf; - setTimeout(() => { - this.doDLSCheck() + setTimeout(() => + { + this.doDLSCheck(); }, 3000); } - + /** * starts playing and rendering the midi file * @param parsedMidi {MIDIFile[]} @@ -391,48 +434,50 @@ class Manager { return; } - - if(this.seq) + + if (this.seq) { this.seq.loadNewSongList(parsedMidi); return; } - + // create a new sequencer this.seq = new Sequencer(parsedMidi, this.synth); - - this.seq.onError = e => { + + this.seq.onError = e => + { document.getElementById("title").textContent = e; - } - + }; + // connect to the UI this.seqUI.connectSequencer(this.seq); - + // connect to the Player UI this.playerUI.connectSequencer(this.seq); - + // connect to the renderer; this.renderer.connectSequencer(this.seq); - + // connect to settings this.settingsUI.addSequencer(this.seq); - + // play the midi this.seq.play(true); } - + downloadDesfont() { const soundfont = loadSoundFont(this.soundFont); const binary = soundfont.write(); - const blob = new Blob([binary.buffer], {type: "audio/soundfont"}); + const blob = new Blob([binary.buffer], { type: "audio/soundfont" }); this.saveBlob(blob, `${soundfont.soundFontInfo["INAM"]}.sf2`); } } + Manager.prototype.exportSong = exportSong; Manager.prototype._exportAudioData = _exportAudioData; Manager.prototype._doExportAudioData = _doExportAudioData; Manager.prototype.exportMidi = exportMidi; Manager.prototype._exportSoundfont = _exportSoundfont; Manager.prototype._exportRMIDI = _exportRMIDI; -export { Manager } \ No newline at end of file +export { Manager }; \ No newline at end of file diff --git a/src/website/js/midi_keyboard/midi_keyboard.js b/src/website/js/midi_keyboard/midi_keyboard.js index 5f6c506c..5d00bd52 100644 --- a/src/website/js/midi_keyboard/midi_keyboard.js +++ b/src/website/js/midi_keyboard/midi_keyboard.js @@ -1,7 +1,7 @@ -import {Synthetizer} from "../../../spessasynth_lib/synthetizer/synthetizer.js"; -import { midiControllers } from '../../../spessasynth_lib/midi_parser/midi_message.js' -import { _handlePointers } from './pointer_handling.js' -import { ANIMATION_REFLOW_TIME } from '../utils/animation_utils.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"; +import { ANIMATION_REFLOW_TIME } from "../utils/animation_utils.js"; /** * midi_keyboard.js @@ -17,7 +17,8 @@ class MidiKeyboard * @param channelColors {Array} * @param synth {Synthetizer} */ - constructor(channelColors, synth) { + constructor(channelColors, synth) + { this.mouseHeld = false; /** * @type {Set} @@ -30,7 +31,7 @@ class MidiKeyboard this.enableDebugging = false; this.sizeChangeAnimationId = -1; this.modeChangeAnimationId = -1; - + /** * @type {{min: number, max: number}} * @private @@ -39,52 +40,58 @@ class MidiKeyboard min: 0, max: 127 }; - + // hold pedal on - document.addEventListener("keydown", e =>{ - if(e.key === "Shift") + document.addEventListener("keydown", e => + { + if (e.key === "Shift") { this.synth.controllerChange(this.channel, midiControllers.sustainPedal, 127); this.keyboard.style.filter = "brightness(0.5)"; } }); - + // hold pedal off - document.addEventListener("keyup", e => { - if(e.key === "Shift") + document.addEventListener("keyup", e => + { + if (e.key === "Shift") { this.synth.controllerChange(this.channel, midiControllers.sustainPedal, 0); this.keyboard.style.filter = ""; } }); - + this.synth = synth; this.channel = 0; - + this.channelColors = channelColors; /** * @type {boolean} * @private */ this._shown = true; - + this._createKeyboard(); - + // connect the synth to keyboard - this.synth.eventHandler.addEvent("noteon", "keyboard-note-on", e => { + this.synth.eventHandler.addEvent("noteon", "keyboard-note-on", e => + { this.pressNote(e.midiNote, e.channel, e.velocity); }); - - this.synth.eventHandler.addEvent("noteoff", "keyboard-note-off", e => { + + this.synth.eventHandler.addEvent("noteoff", "keyboard-note-off", e => + { this.releaseNote(e.midiNote, e.channel); }); - - this.synth.eventHandler.addEvent("stopall", "keyboard-stop-all", () => { + + this.synth.eventHandler.addEvent("stopall", "keyboard-stop-all", () => + { this.clearNotes(); }); - - this.synth.eventHandler.addEvent("mutechannel", "keyboard-mute-channel", e => { - if(e.isMuted) + + this.synth.eventHandler.addEvent("mutechannel", "keyboard-mute-channel", e => + { + if (e.isMuted) { for (let i = 0; i < 128; i++) { @@ -93,13 +100,18 @@ class MidiKeyboard } }); } - + + get shown() + { + return this._shown; + } + /** * @param val {boolean} */ set shown(val) { - if(val === true) + if (val === true) { this.keyboard.style.display = ""; } @@ -109,12 +121,38 @@ class MidiKeyboard } this._shown = val; } - - get shown() + + /** + * The range of displayed MIDI keys + * @returns {{min: number, max: number}} + */ + get keyRange() { - return this._shown; + return this._keyRange; } - + + /** + * The range of displayed MIDI keys + * @param value {{min: number, max: number}} + */ + set keyRange(value) + { + if (value.max === undefined || value.min === undefined) + { + throw new TypeError("No min or max property!"); + } + if (value.min > value.max) + { + let temp = value.min; + value.min = value.max; + value.max = temp; + } + value.min = Math.max(0, value.min); + value.max = Math.min(127, value.max); + this.setKeyRange(value, true); + + } + /** * @private */ @@ -125,35 +163,36 @@ class MidiKeyboard */ this.keyboard = document.getElementById("keyboard"); this.keyboard.innerHTML = ""; - + /** * * @type {HTMLDivElement[]} */ this.keys = []; - + /** * @type {string[][]} */ this.keyColors = []; // create keyboard - for (let midiNote = this._keyRange.min; midiNote < this._keyRange.max + 1; midiNote++) { - + for (let midiNote = this._keyRange.min; midiNote < this._keyRange.max + 1; midiNote++) + { + const keyElement = this._createKey(midiNote); this.keyColors.push([]); this.keyboard.appendChild(keyElement); this.keys.push(keyElement); } - + this._handlePointers(); - - if(this.mode === "dark") + + if (this.mode === "dark") { - this.mode = "light" + this.mode = "light"; this.toggleMode(false); } } - + /** * @param midiNote {number} * @returns {HTMLDivElement} @@ -161,17 +200,19 @@ class MidiKeyboard */ _createKey(midiNote) { - function isBlackNoteNumber(noteNumber) { + function isBlackNoteNumber(noteNumber) + { let pitchClass = noteNumber % 12; return pitchClass === 1 || pitchClass === 3 || pitchClass === 6 || pitchClass === 8 || pitchClass === 10; } + let keyElement = document.createElement("div"); keyElement.classList.add("key"); keyElement.id = `note${midiNote}`; - - + + let isBlack = isBlackNoteNumber(midiNote); - if(isBlack) + if (isBlack) { // short note keyElement.classList.add("sharp_key"); @@ -182,35 +223,36 @@ class MidiKeyboard keyElement.classList.add("flat_key"); let blackNoteLeft = false; let blackNoteRight = false; - if(midiNote >= 0) + if (midiNote >= 0) { blackNoteLeft = isBlackNoteNumber(midiNote - 1); } - if(midiNote < 127) { + if (midiNote < 127) + { blackNoteRight = isBlackNoteNumber(midiNote + 1); } - - if(blackNoteRight && blackNoteLeft) + + if (blackNoteRight && blackNoteLeft) { keyElement.classList.add("between_sharps"); } - else if(blackNoteLeft) + else if (blackNoteLeft) { keyElement.classList.add("left_sharp"); } - else if(blackNoteRight) + else if (blackNoteRight) { keyElement.classList.add("right_sharp"); } - - + + } return keyElement; } - + toggleMode(animate = true) { - if(this.mode === "light") + if (this.mode === "light") { this.mode = "dark"; } @@ -218,29 +260,32 @@ class MidiKeyboard { this.mode = "light"; } - if(!animate) + if (!animate) { - this.keys.forEach(k => { - if(k.classList.contains("flat_key")) + this.keys.forEach(k => + { + if (k.classList.contains("flat_key")) { k.classList.toggle("flat_dark_key"); } }); return; } - if(this.modeChangeAnimationId) + if (this.modeChangeAnimationId) { clearTimeout(this.modeChangeAnimationId); } this.keyboard.classList.add("mode_transform"); const disableScroll = document.body.scrollHeight <= window.innerHeight; - if(disableScroll) + if (disableScroll) { document.body.classList.add("no_scroll"); } - this.modeChangeAnimationId = setTimeout(() => { - this.keys.forEach(k => { - if(k.classList.contains("flat_key")) + this.modeChangeAnimationId = setTimeout(() => + { + this.keys.forEach(k => + { + if (k.classList.contains("flat_key")) { k.classList.toggle("flat_dark_key"); } @@ -250,46 +295,15 @@ class MidiKeyboard setTimeout(() => document.body.classList.remove("no_scroll"), 500); }, 500); } - - /** - * The range of displayed MIDI keys - * @returns {{min: number, max: number}} - */ - get keyRange() - { - return this._keyRange; - } - - /** - * The range of displayed MIDI keys - * @param value {{min: number, max: number}} - */ - set keyRange(value) - { - if(value.max === undefined || value.min === undefined) - { - throw new TypeError("No min or max property!"); - } - if(value.min > value.max) - { - let temp = value.min; - value.min = value.max; - value.max = temp; - } - value.min = Math.max(0, value.min); - value.max = Math.min(127, value.max); - this.setKeyRange(value, true); - - } - + /** * @param range {{min: number, max: number}} * @param animate {boolean} */ setKeyRange(range, animate = true) { - const diff = Math.abs(range.max - range.min) - if(diff < 12) + const diff = Math.abs(range.max - range.min); + if (diff < 12) { range.min -= 6; range.max = range.min + 12; @@ -304,39 +318,40 @@ class MidiKeyboard * @type {CSSStyleRule} */ let keyRule; - for(const rule of rules) + for (const rule of rules) { - if(rule.selectorText === "#keyboard .key") + if (rule.selectorText === "#keyboard .key") { keyRule = rule; break; } } keyRule.style.setProperty("--pressed-transform-skew", `${0.0008 / (newHeight / 7)}`); - if(animate) + if (animate) { - if(this.sizeChangeAnimationId) + if (this.sizeChangeAnimationId) { clearTimeout(this.sizeChangeAnimationId); } // do a cool animation // get height ratio for anumation const computedStyle = getComputedStyle(this.keyboard); - const currentHeight = parseFloat(computedStyle.getPropertyValue("--current-min-height").replace(/[^\d.]+/g, "")); + const currentHeight = parseFloat(computedStyle.getPropertyValue("--current-min-height") + .replace(/[^\d.]+/g, "")); const currentHeightPx = this.keyboard.getBoundingClientRect().height; const heightRatio = newHeight / currentHeight; const heightDifferencePx = currentHeightPx * heightRatio - currentHeightPx; - + // get key shift ratio for anumation const currentCenterKey = (this._keyRange.min + this._keyRange.max) / 2; const newCenterKey = (range.min + range.max) / 2; - + this._keyRange = range; - + // get key width for calculation const keyWidth = this.keys.find(k => k.classList.contains("sharp_key")).getBoundingClientRect().width; const pixelShift = (currentCenterKey - newCenterKey) * keyWidth; - + // get the new border radius const currentBorderRadius = parseFloat( computedStyle @@ -346,14 +361,18 @@ class MidiKeyboard // add margin so the keyboard takes up the new amount of space this.keyboard.style.marginTop = `${heightDifferencePx}px`; this.keyboard.style.transition = ""; - + // being the transition this.keyboard.style.transform = `scale(${heightRatio}) translateX(${pixelShift}px)`; this.keyboard.style.setProperty("--key-border-radius", `${currentBorderRadius / heightRatio}vmin`); - + // animation end - this.sizeChangeAnimationId = setTimeout(() => { - this.keyboard.style.setProperty("--current-min-height", `${newHeight}`); + this.sizeChangeAnimationId = setTimeout(() => + { + this.keyboard.style.setProperty( + "--current-min-height", + `${newHeight}` + ); // restore values and disable transition this.keyboard.style.transition = "none"; this.keyboard.style.transform = ""; @@ -362,7 +381,10 @@ class MidiKeyboard // update size this._createKeyboard(); // restore transition - setTimeout(() => this.keyboard.style.transition = "", ANIMATION_REFLOW_TIME); + setTimeout( + () => this.keyboard.style.transition = "", + ANIMATION_REFLOW_TIME + ); }, 500); } else @@ -372,7 +394,7 @@ class MidiKeyboard this._createKeyboard(); } } - + /** * Selects the channel from synth * @param channel {number} 0-15 @@ -381,7 +403,7 @@ class MidiKeyboard { this.channel = channel; } - + /** * presses a midi note visually * @param midiNote {number} 0-127 @@ -391,22 +413,23 @@ class MidiKeyboard pressNote(midiNote, channel, velocity) { let key = this.keys[midiNote - this._keyRange.min]; - if(key === undefined) + if (key === undefined) { return; } key.classList.add("pressed"); - + let isSharp = key.classList.contains("sharp_key"); let brightness = velocity / 127; let rgbaValues = this.channelColors[channel % 16].match(/\d+(\.\d+)?/g).map(parseFloat); - + // multiply the rgb values by brightness let color; - if (!isSharp && this.mode === "light") { + 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 color = `rgba(${newRGBValues.join(", ")}, ${rgbaValues[3]})`; } @@ -414,12 +437,12 @@ class MidiKeyboard { // multiply the rgb values let newRGBValues = rgbaValues.slice(0, 3).map(value => value * brightness); - + // create the new color color = `rgba(${newRGBValues.join(", ")}, ${rgbaValues[3]})`; } key.style.background = color; - if(this.mode === "dark") + if (this.mode === "dark") { const spread = GLOW_PX * brightness; key.style.boxShadow = `${color} 0px 0px ${spread}px ${spread / 5}px`; @@ -429,7 +452,7 @@ class MidiKeyboard */ this.keyColors[midiNote - this._keyRange.min].push(this.channelColors[channel % 16]); } - + /** * @param midiNote {number} 0-127 * @param channel {number} 0-15 @@ -437,49 +460,51 @@ class MidiKeyboard releaseNote(midiNote, channel) { let key = this.keys[midiNote - this._keyRange.min]; - if(key === undefined) + if (key === undefined) { return; } - + channel %= this.channelColors.length; - + /** * @type {string[]} */ let pressedColors = this.keyColors[midiNote - this._keyRange.min]; - if(!pressedColors) + if (!pressedColors) { return; } const colorIndex = pressedColors.findLastIndex(v => v === this.channelColors[channel]); - if(colorIndex === -1) + if (colorIndex === -1) { return; } pressedColors.splice(colorIndex, 1); key.style.background = pressedColors[pressedColors.length - 1]; - if(this.mode === "dark") + if (this.mode === "dark") { key.style.boxShadow = `0px 0px ${GLOW_PX}px ${pressedColors[pressedColors.length - 1]}`; } - if(pressedColors.length < 1) + if (pressedColors.length < 1) { key.classList.remove("pressed"); key.style.background = ""; key.style.boxShadow = ""; } } - + clearNotes() { - this.keys.forEach((key, index) => { + this.keys.forEach((key, index) => + { key.classList.remove("pressed"); key.style.background = ""; key.style.boxShadow = ""; this.keyColors[index] = []; - }) + }); } } + 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 index 23fbb35e..2026c119 100644 --- a/src/website/js/midi_keyboard/pointer_handling.js +++ b/src/website/js/midi_keyboard/pointer_handling.js @@ -1,4 +1,4 @@ -import { isMobile } from '../utils/is_mobile.js' +import { isMobile } from "../utils/is_mobile.js"; /** * @this {MidiKeyboard} @@ -7,17 +7,19 @@ import { isMobile } from '../utils/is_mobile.js' export function _handlePointers() { // POINTER HANDLING - const userNoteOff = note => { - this.pressedKeys.delete(note) + const userNoteOff = note => + { + this.pressedKeys.delete(note); this.releaseNote(note, this.channel); this.synth.noteOff(this.channel, note); - } - + }; + /** * @param note {number} * @param touch {Touch|MouseEvent} */ - const userNoteOn = (note, touch) => { + const userNoteOn = (note, touch) => + { // user note on let velocity; if (isMobile) @@ -30,7 +32,7 @@ export function _handlePointers() // determine velocity. lower = more velocity const keyElement = this.keys[0]; // all keys have the same top const rect = keyElement.getBoundingClientRect(); - if(this.keyboard.classList.contains("sideways")) + if (this.keyboard.classList.contains("sideways")) { const relativeMouseX = touch.clientX - rect.left; const keyWidth = rect.width; @@ -45,12 +47,13 @@ export function _handlePointers() } } this.synth.noteOn(this.channel, note, velocity, this.enableDebugging); - } - + }; + /** * @param e {MouseEvent|TouchEvent} */ - const moveHandler = e => { + const moveHandler = e => + { // all currently pressed keys are stored in this.pressedKeys /** * @type {Touch[]|MouseEvent[]} @@ -60,11 +63,12 @@ export function _handlePointers() * @type {Set} */ const currentlyTouchedKeys = new Set(); - touches.forEach(touch => { + 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)) + if (isNaN(midiNote) || midiNote < 0 || this.pressedKeys.has(midiNote)) { // pressed outside of bounds or already pressed return; @@ -72,37 +76,47 @@ export function _handlePointers() this.pressedKeys.add(midiNote); userNoteOn(midiNote, touch); }); - this.pressedKeys.forEach(key => { - if(!currentlyTouchedKeys.has(key)) + this.pressedKeys.forEach(key => + { + if (!currentlyTouchedKeys.has(key)) { userNoteOff(key); } }); }; - + // mouse - if(!isMobile) + if (!isMobile) { - document.addEventListener("mousedown", e => { + document.addEventListener("mousedown", e => + { this.mouseHeld = true; moveHandler(e); }); - document.addEventListener("mouseup", () => { + document.addEventListener("mouseup", () => + { this.mouseHeld = false; - this.pressedKeys.forEach(key => { + this.pressedKeys.forEach(key => + { userNoteOff(key); }); }); - this.keyboard.onmousemove = e => { - if(this.mouseHeld) moveHandler(e); + this.keyboard.onmousemove = e => + { + if (this.mouseHeld) + { + moveHandler(e); + } }; - this.keyboard.onmouseleave = () => { - this.pressedKeys.forEach(key => { + this.keyboard.onmouseleave = () => + { + this.pressedKeys.forEach(key => + { userNoteOff(key); }); - } + }; } - + // touch this.keyboard.ontouchstart = moveHandler.bind(this); this.keyboard.ontouchend = moveHandler.bind(this); diff --git a/src/website/js/music_mode_ui/music_mode_ui.js b/src/website/js/music_mode_ui/music_mode_ui.js index 5ec84c0a..7cda9056 100644 --- a/src/website/js/music_mode_ui/music_mode_ui.js +++ b/src/website/js/music_mode_ui/music_mode_ui.js @@ -1,6 +1,6 @@ -import { getDoubleNoteSvg } from '../utils/icons.js' -import { formatTime } from '../../../spessasynth_lib/utils/other.js' -import { ANIMATION_REFLOW_TIME } from '../utils/animation_utils.js' +import { getDoubleNoteSvg } from "../utils/icons.js"; +import { formatTime } from "../../../spessasynth_lib/utils/other.js"; +import { ANIMATION_REFLOW_TIME } from "../utils/animation_utils.js"; /** * music_mode_ui.js @@ -10,13 +10,15 @@ import { ANIMATION_REFLOW_TIME } from '../utils/animation_utils.js' const TRANSITION_TIME = 0.5; -export class MusicModeUI { +export class MusicModeUI +{ /** * Creates a new class for displaying information about the current file. * @param element {HTMLElement} * @param localeManager {LocaleManager} */ - constructor(element, localeManager) { + constructor(element, localeManager) + { this.mainDiv = element; // load html this.mainDiv.innerHTML = ` @@ -63,7 +65,7 @@ export class MusicModeUI { `; - + // apply locale bindings for (const el of this.mainDiv.querySelectorAll("*[translate-path]")) { @@ -78,12 +80,12 @@ export class MusicModeUI { this.visible = false; this.locale = localeManager; } - + toggleDarkMode() { this.mainDiv.getElementsByClassName("player_info_wrapper")[0].classList.toggle("light_mode"); } - + /** * @param title {string} */ @@ -92,14 +94,15 @@ export class MusicModeUI { // get the title document.getElementById("player_info_title").textContent = title; } - + /** * @param seq {Sequencer} */ connectSequencer(seq) { this.seq = seq; - this.seq.addOnSongChangeEvent(mid => { + this.seq.addOnSongChangeEvent(mid => + { // use file name if no copyright detected const midcopy = mid.copyright.replaceAll("\n", ""); /** @@ -107,17 +110,18 @@ export class MusicModeUI { * @param text {string} * @param enableMarquee {boolean} */ - const setInfoText = (id, text, enableMarquee = true) => { + const setInfoText = (id, text, enableMarquee = true) => + { const el = document.getElementById(id); - if(text.length > 0) + if (text.length > 0) { el.parentElement.classList.remove("hidden"); el.innerHTML = ""; // add scroll if needed - if(text.length > 30 && enableMarquee) + if (text.length > 30 && enableMarquee) { el.classList.add("marquee"); - + const textWrap = document.createElement("span"); textWrap.textContent = text; el.appendChild(textWrap); @@ -131,15 +135,15 @@ export class MusicModeUI { { el.parentElement.classList.add("hidden"); } - } + }; // copyright setInfoText("player_info_detail", midcopy); // time setInfoText("player_info_time", formatTime(this.seq.duration).time); - + // file name setInfoText("player_info_file_name", mid.fileName, false); - + // embedded things // add album and artist meta /** @@ -149,25 +153,35 @@ export class MusicModeUI { * @param prepend {string} * @return {string} */ - const verifyDecode = (type, def, decoder, prepend = "") => { - return this.seq.midiData.RMIDInfo?.[type] === undefined ? def : prepend + decoder.decode(this.seq.midiData.RMIDInfo?.[type]).replace(/\0$/, '') - } + const verifyDecode = (type, def, decoder, prepend = "") => + { + return this.seq.midiData.RMIDInfo?.[type] === undefined ? def : prepend + decoder.decode( + this.seq.midiData.RMIDInfo?.[type]).replace(/\0$/, ""); + }; // initialize decoder let encoding = verifyDecode("IENC", "ascii", new TextDecoder()); const decoder = new TextDecoder(encoding); - + // artist, album, creation date - setInfoText("player_info_album", verifyDecode('IPRD', "", decoder)); - setInfoText("player_info_artist", verifyDecode('IART', "", decoder)); - setInfoText("player_info_genre", verifyDecode('IGNR', "", decoder)); - setInfoText("player_info_creation", verifyDecode('ICRD', "", decoder) + verifyDecode('ICRT', "", decoder, "\n")); - setInfoText("player_info_comment", verifyDecode('ICMT', "", decoder)); - + setInfoText("player_info_album", verifyDecode("IPRD", "", decoder)); + setInfoText("player_info_artist", verifyDecode("IART", "", decoder)); + setInfoText("player_info_genre", verifyDecode("IGNR", "", decoder)); + setInfoText( + "player_info_creation", + verifyDecode("ICRD", "", decoder) + verifyDecode( + "ICRT", + "", + decoder, + "\n" + ) + ); + setInfoText("player_info_comment", verifyDecode("ICMT", "", decoder)); + // image const svg = this.mainDiv.getElementsByTagName("svg")[0]; const img = this.mainDiv.getElementsByTagName("img")[0]; const bg = document.getElementById("player_info_background_image"); - if(!mid.isEmbedded) + if (!mid.isEmbedded) { svg.style.display = ""; img.style.display = "none"; @@ -175,7 +189,7 @@ export class MusicModeUI { return; } // add album cover if available - if(mid.RMIDInfo["IPIC"] === undefined) + if (mid.RMIDInfo["IPIC"] === undefined) { svg.style.display = ""; img.style.display = "none"; @@ -190,30 +204,30 @@ export class MusicModeUI { bg.style.setProperty("--bg-image", `url('${url}')`); }, "player-js-song-change"); } - + /** * @param visible {boolean} * @param keyboardCanvasWrapper {HTMLDivElement} */ setVisibility(visible, keyboardCanvasWrapper) { - if(visible === this.visible) + if (visible === this.visible) { return; } this.visible = visible; - if(this.timeoutId) + if (this.timeoutId) { clearTimeout(this.timeoutId); } const playerDiv = this.mainDiv; - if(visible) + if (visible) { // PREPARATION // renderer and keyboard keyboardCanvasWrapper.classList.add("out_animation"); this.savedCKWrapperHeight = keyboardCanvasWrapper.clientHeight; - + // music mode // hacky: get position of the wrapper and temporarily set to absolute (set to normal after finish) const playerHeight = keyboardCanvasWrapper.clientHeight; @@ -222,23 +236,25 @@ export class MusicModeUI { playerDiv.style.top = `${playerTop}px`; playerDiv.style.height = `${playerHeight}px`; playerDiv.style.display = "flex"; - + // START - setTimeout(() => { + setTimeout(() => + { playerDiv.classList.add("player_info_show"); document.body.style.overflow = "hidden"; }, ANIMATION_REFLOW_TIME); - + // FINISH - this.timeoutId = setTimeout(async () => { + this.timeoutId = setTimeout(async () => + { keyboardCanvasWrapper.style.display = "none"; - + playerDiv.style.position = ""; playerDiv.style.top = ""; playerDiv.style.height = ""; - + document.body.style.overflow = ""; - }, TRANSITION_TIME * 1000) + }, TRANSITION_TIME * 1000); } else { @@ -250,24 +266,26 @@ export class MusicModeUI { keyboardCanvasWrapper.style.position = "absolute"; keyboardCanvasWrapper.style.top = `${rootTop}px`; keyboardCanvasWrapper.style.height = `${this.savedCKWrapperHeight}px`; - + // music mode playerDiv.classList.remove("player_info_show"); - + // START - setTimeout(() => { + setTimeout(() => + { keyboardCanvasWrapper.classList.remove("out_animation"); document.body.style.overflow = "hidden"; }, ANIMATION_REFLOW_TIME); - + // FINISH - this.timeoutId = setTimeout(() => { + this.timeoutId = setTimeout(() => + { playerDiv.style.display = "none"; - + keyboardCanvasWrapper.style.position = ""; keyboardCanvasWrapper.style.top = ""; keyboardCanvasWrapper.style.height = ""; - + document.body.style.overflow = ""; }, TRANSITION_TIME * 1000); } diff --git a/src/website/js/notification/get_content.js b/src/website/js/notification/get_content.js index 2e0e39d6..62087ec1 100644 --- a/src/website/js/notification/get_content.js +++ b/src/website/js/notification/get_content.js @@ -1,4 +1,4 @@ -import { createSlider } from '../settings_ui/sliders.js' +import { createSlider } from "../settings_ui/sliders.js"; /** * @param el {HTMLElement} @@ -7,13 +7,13 @@ import { createSlider } from '../settings_ui/sliders.js' */ function applyTextContent(el, content, locale) { - if(content.textContent) + if (content.textContent) { el.textContent = content.textContent; } - if(content.translatePathTitle) + if (content.translatePathTitle) { - if(!locale) + if (!locale) { throw new Error("Translate path title provided but no locale provided."); } @@ -29,20 +29,20 @@ function applyTextContent(el, content, locale) */ export function getContent(content, locale) { - switch(content.type) + switch (content.type) { case "button": const btn = document.createElement("button"); applyTextContent(btn, content, locale); - applyAttributes(content, [btn]) + applyAttributes(content, [btn]); return btn; - + case "text": const p = document.createElement("p"); applyTextContent(p, content, locale); applyAttributes(content, [p]); return p; - + case "input": const inputWrapper = document.createElement("div"); inputWrapper.classList.add("notification_input_wrapper"); @@ -51,12 +51,12 @@ export function getContent(content, locale) input.addEventListener("keydown", e => e.stopPropagation()); const inputLabel = document.createElement("label"); applyTextContent(inputLabel, content, locale); - + applyAttributes(content, [input, inputLabel]); inputWrapper.append(inputLabel); inputWrapper.appendChild(input); return inputWrapper; - + case "file": const fileWrapper = document.createElement("label"); fileWrapper.classList.add("notification_input_wrapper"); @@ -65,16 +65,16 @@ export function getContent(content, locale) const fileButton = document.createElement("label"); fileButton.classList.add("notification_file_button"); applyTextContent(fileButton, content, locale); - + const fileLabel = document.createElement("label"); applyTextContent(fileLabel, content, locale); - + applyAttributes(content, [fileButton, file, fileLabel]); fileButton.appendChild(file); fileWrapper.append(fileLabel); fileWrapper.appendChild(fileButton); return fileWrapper; - + case "progress": const background = document.createElement("div"); background.classList.add("notification_progress_background"); @@ -83,10 +83,10 @@ export function getContent(content, locale) applyAttributes(content, [progress, background]); background.appendChild(progress); return background; - + case "toggle": return getSwitch(content, locale); - + case "range": const range = document.createElement("input"); range.type = "range"; @@ -99,7 +99,7 @@ export function getContent(content, locale) wrapper.appendChild(label); wrapper.appendChild(slider); return wrapper; - + } } @@ -109,11 +109,11 @@ export function getContent(content, locale) */ function applyAttributes(content, elements) { - if(content.attributes) + if (content.attributes) { - for(const [key, value] of Object.entries(content.attributes)) + for (const [key, value] of Object.entries(content.attributes)) { - for(const element of elements) + for (const element of elements) { element.setAttribute(key, value); } @@ -130,21 +130,21 @@ function getSwitch(content, locale) { const switchWrapper = document.createElement("label"); switchWrapper.classList.add("notification_switch_wrapper"); - const toggleText= document.createElement("label"); + const toggleText = document.createElement("label"); applyTextContent(toggleText, content, locale); - + const toggleInput = document.createElement("input"); toggleInput.type = "checkbox"; applyAttributes(content, [toggleText, toggleInput]); - + const toggle = document.createElement("div"); toggle.classList.add("notification_switch"); toggle.appendChild(toggleInput); - + const slider = document.createElement("div"); slider.classList.add("notification_switch_slider"); toggle.appendChild(slider); - + switchWrapper.appendChild(toggleText); switchWrapper.appendChild(toggle); return switchWrapper; diff --git a/src/website/js/notification/notification.js b/src/website/js/notification/notification.js index 3fb2b39d..5e740912 100644 --- a/src/website/js/notification/notification.js +++ b/src/website/js/notification/notification.js @@ -1,5 +1,5 @@ -import { getContent } from './get_content.js' -import { ANIMATION_REFLOW_TIME } from '../utils/animation_utils.js' +import { getContent } from "./get_content.js"; +import { ANIMATION_REFLOW_TIME } from "../utils/animation_utils.js"; const NOTIFICATION_TIME = 13; @@ -49,7 +49,7 @@ export function showNotification( { const notification = document.createElement("div"); const notificationID = notificationCounter++; - + notification.classList.add("notification"); notification.innerHTML = `
@@ -58,27 +58,28 @@ export function showNotification(
`; const contentWrapper = document.createElement("div"); contentWrapper.classList.add("notification_content"); - if(contentStyling) + if (contentStyling) { - for(const [key, value] of Object.entries(contentStyling)) + for (const [key, value] of Object.entries(contentStyling)) { contentWrapper.style[key] = value; } } notification.appendChild(contentWrapper); - for(const content of contents) + for (const content of contents) { const element = getContent(content, locale); - if(content.onClick) + if (content.onClick) { - element.onclick = () => content.onClick({div: notification, id: notificationID}, element); + element.onclick = () => content.onClick({ div: notification, id: notificationID }, element); } contentWrapper.appendChild(element); } - - if(allowClosing) + + if (allowClosing) { - notification.getElementsByClassName("close_btn")[0].onclick = () => { + notification.getElementsByClassName("close_btn")[0].onclick = () => + { closeNotification(notificationID); }; } @@ -86,18 +87,20 @@ export function showNotification( { notification.getElementsByClassName("close_btn")[0].style.display = "none"; } - - setTimeout(() => { + + setTimeout(() => + { notification.classList.add("drop"); - }, ANIMATION_REFLOW_TIME) - const timeoutID = setTimeout(() => { + }, ANIMATION_REFLOW_TIME); + const timeoutID = setTimeout(() => + { closeNotification(notificationID); }, time * 1000 + ANIMATION_REFLOW_TIME); document.getElementsByClassName("notification_field")[0].appendChild(notification); notifications[notificationID] = { div: notification, timeout: timeoutID - } + }; return { div: notification, id: notificationID @@ -111,8 +114,8 @@ export function closeNotification(id) { const notification = notifications[id].div; clearTimeout(notifications[id].timeout); - notification.classList.remove("drop") + notification.classList.remove("drop"); setTimeout(() => notification.parentElement.removeChild(notification), 500); notifications[id] = undefined; - + } \ No newline at end of file diff --git a/src/website/js/renderer/calculate_note_times.js b/src/website/js/renderer/calculate_note_times.js index 34505587..e3493f0d 100644 --- a/src/website/js/renderer/calculate_note_times.js +++ b/src/website/js/renderer/calculate_note_times.js @@ -1,8 +1,8 @@ -import { IndexedByteArray } from '../../../spessasynth_lib/utils/indexed_array.js' -import { SpessaSynthInfo } from '../../../spessasynth_lib/utils/loggin.js' -import { consoleColors } from '../../../spessasynth_lib/utils/other.js' -import { DEFAULT_PERCUSSION } from '../../../spessasynth_lib/synthetizer/synthetizer.js' -import { readBytesAsUintBigEndian } from '../../../spessasynth_lib/utils/byte_functions/big_endian.js' +import { IndexedByteArray } from "../../../spessasynth_lib/utils/indexed_array.js"; +import { SpessaSynthInfo } from "../../../spessasynth_lib/utils/loggin.js"; +import { consoleColors } from "../../../spessasynth_lib/utils/other.js"; +import { DEFAULT_PERCUSSION } from "../../../spessasynth_lib/synthetizer/synthetizer.js"; +import { readBytesAsUintBigEndian } from "../../../spessasynth_lib/utils/byte_functions/big_endian.js"; const MIN_NOTE_TIME = 0.02; @@ -12,7 +12,7 @@ const MIN_NOTE_TIME = 0.02; */ export function calculateNoteTimes(midi) { - + /** * gets tempo from the midi message * @param event {MidiMessage} @@ -25,7 +25,7 @@ export function calculateNoteTimes(midi) event.messageData.currentIndex = 0; return 60000000 / readBytesAsUintBigEndian(event.messageData, 3); } - + /** * an array of 16 arrays (channels) and the notes are stored there * @typedef {{ @@ -40,7 +40,7 @@ export function calculateNoteTimes(midi) * renderStartIndex: number * }[]} NoteTimes */ - + /** * @type {NoteTimes} */ @@ -49,46 +49,49 @@ export function calculateNoteTimes(midi) const trackData = midi.tracks; let events = trackData.flat(); events.sort((e1, e2) => e1.ticks - e2.ticks); - + for (let i = 0; i < 16; i++) { - noteTimes.push({renderStartIndex: 0, notes: []}); + noteTimes.push({ renderStartIndex: 0, notes: [] }); } let elapsedTime = 0; let oneTickToSeconds = 60 / (120 * midi.timeDivision); let eventIndex = 0; let unfinished = 0; - while(eventIndex < events.length) + while (eventIndex < events.length) { const event = events[eventIndex]; - + const status = event.messageStatusByte >> 4; const channel = event.messageStatusByte & 0x0F; - + // note off - if(status === 0x8) + if (status === 0x8) { - const note = noteTimes[channel].notes.findLast(n => n.midiNote === event.messageData[0] && n.length === -1) - if(note) { + const note = noteTimes[channel].notes.findLast(n => n.midiNote === event.messageData[0] && n.length === -1); + if (note) + { const time = elapsedTime - note.start; note.length = (time < MIN_NOTE_TIME && channel === DEFAULT_PERCUSSION ? MIN_NOTE_TIME : time); } unfinished--; } // note on - else if(status === 0x9) + else if (status === 0x9) { - if(event.messageData[1] === 0) + if (event.messageData[1] === 0) { // nevermind, its note off - const note = noteTimes[channel].notes.findLast(n => n.midiNote === event.messageData[0] && n.length === -1) - if(note) { + const note = noteTimes[channel].notes.findLast(n => n.midiNote === event.messageData[0] && n.length === -1); + if (note) + { const time = elapsedTime - note.start; note.length = (time < MIN_NOTE_TIME && channel === DEFAULT_PERCUSSION ? MIN_NOTE_TIME : time); } unfinished--; } - else { + else + { noteTimes[event.messageStatusByte & 0x0F].notes.push({ midiNote: event.messageData[0], start: elapsedTime, @@ -99,18 +102,21 @@ export function calculateNoteTimes(midi) } } // set tempo - else if(event.messageStatusByte === 0x51) + else if (event.messageStatusByte === 0x51) { oneTickToSeconds = 60 / (getTempo(event) * midi.timeDivision); } - - if(++eventIndex >= events.length) break; - + + if (++eventIndex >= events.length) + { + break; + } + elapsedTime += oneTickToSeconds * (events[eventIndex].ticks - event.ticks); } - + // finish the unfinished notes - if(unfinished > 0) + if (unfinished > 0) { // for every channel, for every note that is unfinished (has -1 length) noteTimes.forEach((channel, channelNumber) => @@ -119,7 +125,7 @@ export function calculateNoteTimes(midi) const time = elapsedTime - note.start; note.length = (time < MIN_NOTE_TIME && channelNumber === DEFAULT_PERCUSSION ? MIN_NOTE_TIME : time); }) - ) + ); } this.noteTimes = noteTimes; SpessaSynthInfo(`%cFinished loading note times and ready to render the sequence!`, consoleColors.info); diff --git a/src/website/js/renderer/channel_analysers.js b/src/website/js/renderer/channel_analysers.js index 85a00bb3..f0a7f6be 100644 --- a/src/website/js/renderer/channel_analysers.js +++ b/src/website/js/renderer/channel_analysers.js @@ -1,6 +1,6 @@ -import { SpessaSynthInfo } from '../../../spessasynth_lib/utils/loggin.js' -import { consoleColors } from '../../../spessasynth_lib/utils/other.js' -import { STABILIZE_WAVEFORMS_FFT_MULTIPLIER } from './render_waveforms.js' +import { SpessaSynthInfo } from "../../../spessasynth_lib/utils/loggin.js"; +import { consoleColors } from "../../../spessasynth_lib/utils/other.js"; +import { STABILIZE_WAVEFORMS_FFT_MULTIPLIER } from "./render_waveforms.js"; /** * @param synth {Synthetizer} @@ -9,13 +9,13 @@ import { STABILIZE_WAVEFORMS_FFT_MULTIPLIER } from './render_waveforms.js' export function createChannelAnalysers(synth) { // disconnect the analysers from earlier - for(const analyser of this.channelAnalysers) + for (const analyser of this.channelAnalysers) { analyser.disconnect(); this.channelAnalysers.splice(0, 1); } this.channelAnalysers = []; - for(let i = 0; i < synth.channelsAmount; i++) + for (let i = 0; i < synth.channelsAmount; i++) { // create the analyser const analyser = new AnalyserNode(synth.context, { @@ -24,8 +24,9 @@ export function createChannelAnalysers(synth) }); this.channelAnalysers.push(analyser); } - - synth.eventHandler.addEvent("mutechannel", "renderer-mute-channel", eventData => { + + synth.eventHandler.addEvent("mutechannel", "renderer-mute-channel", eventData => + { this.renderChannels[eventData.channel] = !eventData.isMuted; }); this.updateFftSize(); @@ -43,11 +44,11 @@ export function updateFftSize() const mul = drum ? STABILIZE_WAVEFORMS_FFT_MULTIPLIER / 2 : STABILIZE_WAVEFORMS_FFT_MULTIPLIER; const fftSize = Math.min(32768, this._stabilizeWaveforms ? fft * mul : fft); this.channelAnalysers[i].fftSize = fftSize; - if(!drum) + if (!drum) { // calculate delay: // 16384 fft size = 0.1 s - if(fftSize > 4096) + if (fftSize > 4096) { this.delayNode.delayTime.value = fftSize / this.synth.context.sampleRate / 2; } @@ -67,10 +68,11 @@ export function updateFftSize() export function connectChannelAnalysers(synth) { synth.connectIndividualOutputs(this.channelAnalysers); - - + + // connect for drum change - synth.eventHandler.addEvent("drumchange", "renderer-drum-change", () => { + synth.eventHandler.addEvent("drumchange", "renderer-drum-change", () => + { this.updateFftSize(); }); } @@ -80,7 +82,8 @@ export function connectChannelAnalysers(synth) */ export function disconnectChannelAnalysers() { - for (const channelAnalyser of this.channelAnalysers) { + for (const channelAnalyser of this.channelAnalysers) + { channelAnalyser.disconnect(); } SpessaSynthInfo("%cAnalysers disconnected!", consoleColors.recognized); diff --git a/src/website/js/renderer/compute_note_positions.js b/src/website/js/renderer/compute_note_positions.js index 6e9909a7..22969c0f 100644 --- a/src/website/js/renderer/compute_note_positions.js +++ b/src/website/js/renderer/compute_note_positions.js @@ -1,4 +1,4 @@ -import { MAX_NOTES, MIN_NOTE_HEIGHT_PX, NOTE_MARGIN, PRESSED_EFFECT_TIME, STROKE_THICKNESS } from './renderer.js' +import { MAX_NOTES, MIN_NOTE_HEIGHT_PX, NOTE_MARGIN, PRESSED_EFFECT_TIME, STROKE_THICKNESS } from "./renderer.js"; /** * @param renderImmediately {boolean} @@ -9,16 +9,16 @@ export function computeNotePositions(renderImmediately = false) { // math this.notesOnScreen = 0; - + const canvasWidth = this.sideways ? this.canvas.height : this.canvas.width; const canvasHeight = this.sideways ? this.canvas.width : this.canvas.height; const keysAmount = this.keyRange.max - this.keyRange.min; const keyStep = canvasWidth / (keysAmount + 1); // add one because it works const noteWidth = keyStep - (NOTE_MARGIN * 2); - + const fallingTime = this.noteFallingTimeMs / 1000; const afterTime = this.noteAfterTriggerTimeMs / 1000; - + const currentSeqTime = this.seq.currentHighResolutionTime; const currentStartTime = currentSeqTime - afterTime; const fallingTimeSeconds = fallingTime + afterTime; @@ -29,9 +29,10 @@ export function computeNotePositions(renderImmediately = false) * @type {number[]} */ const pitchBendXShift = []; - this.synth.channelProperties.forEach(channel => { + this.synth.channelProperties.forEach(channel => + { // pitch range * (bend - 8192) / 8192)) * key width - if(this.showVisualPitch) + if (this.showVisualPitch) { const bend = channel.pitchBend - 8192 + this.visualPitchBendOffset; // -8192 to 8192 pitchBendXShift.push((channel.pitchBendRangeSemitones * ((bend / 8192) * keyStep))); @@ -40,45 +41,51 @@ export function computeNotePositions(renderImmediately = false) { pitchBendXShift.push(0); } - }) + }); /** * @type {NoteToRender[]} */ const notesToDraw = []; - this.noteTimes.forEach((channel, channelNumder) => { - - if(channel.renderStartIndex >= channel.notes.length || !this.renderChannels[channelNumder]) return; - + this.noteTimes.forEach((channel, channelNumder) => + { + + if (channel.renderStartIndex >= channel.notes.length || !this.renderChannels[channelNumder]) + { + return; + } + let noteIndex = channel.renderStartIndex; const notes = channel.notes; let note = notes[noteIndex]; - + let firstNoteIndex = -1; // while the note start is in range - while(note.start <= currentEndTime){ + while (note.start <= currentEndTime) + { noteIndex++; // cap notes - if(this.notesOnScreen > MAX_NOTES) + if (this.notesOnScreen > MAX_NOTES) { break; } - - const noteSum = note.start + note.length - + + const noteSum = note.start + note.length; + // if the note is out of range, append the render start index - if(noteSum > currentStartTime && note.length > 0) { + if (noteSum > currentStartTime && note.length > 0) + { let noteHeight = ((note.length / fallingTimeSeconds) * canvasHeight) - (NOTE_MARGIN * 2); - + // height less than that can be ommitted (come on) - if(this.notesOnScreen < 1000 || noteHeight > minNoteHeight) + if (this.notesOnScreen < 1000 || noteHeight > minNoteHeight) { - if(firstNoteIndex === -1) + if (firstNoteIndex === -1) { firstNoteIndex = noteIndex - 1; } - const position = (((note.start - currentStartTime) / fallingTimeSeconds) * canvasHeight); + const position = (((note.start - currentStartTime) / fallingTimeSeconds) * canvasHeight); let noteY; - if(this._notesFall) + if (this._notesFall) { noteY = canvasHeight - noteHeight - position + NOTE_MARGIN; } @@ -86,11 +93,11 @@ export function computeNotePositions(renderImmediately = false) { noteY = position + NOTE_MARGIN; } - + // if the note out of range, skip - if(note.midiNote < this.keyRange.min || note.midiNote > this.keyRange.max) + if (note.midiNote < this.keyRange.min || note.midiNote > this.keyRange.max) { - if(noteIndex >= notes.length) + if (noteIndex >= notes.length) { break; } @@ -99,15 +106,15 @@ export function computeNotePositions(renderImmediately = false) } const correctedNote = note.midiNote - this.keyRange.min; let noteX = keyStep * correctedNote + NOTE_MARGIN; - + let finalX, finalY, finalWidth, finalHeight; - if(this.sideways) + if (this.sideways) { // add noinspection since we want to inverse positons // noinspection JSSuspiciousNameCombination finalX = noteY; // noinspection JSSuspiciousNameCombination - finalY = noteX + finalY = noteX; // noinspection JSSuspiciousNameCombination finalHeight = noteWidth; // noinspection JSSuspiciousNameCombination @@ -121,17 +128,19 @@ export function computeNotePositions(renderImmediately = false) finalWidth = noteWidth; finalHeight = noteHeight; } - + this.notesOnScreen++; // draw the notes - if(renderImmediately) + if (renderImmediately) { // draw the notes right away, we don't care about the order this.drawingContext.fillStyle = this.plainColors[channelNumder]; - this.drawingContext.fillRect(finalX + STROKE_THICKNESS + NOTE_MARGIN, + this.drawingContext.fillRect( + finalX + STROKE_THICKNESS + NOTE_MARGIN, finalY + STROKE_THICKNESS, finalWidth - (STROKE_THICKNESS * 2), - finalHeight - (STROKE_THICKNESS * 2)); + finalHeight - (STROKE_THICKNESS * 2) + ); } else { @@ -142,9 +151,9 @@ export function computeNotePositions(renderImmediately = false) if ((note.start > currentSeqTime || noteSum < currentSeqTime)) { // this note is not presed - if(this.sideways) + if (this.sideways) { - if(this.drawActiveNotes) + if (this.drawActiveNotes) { color = this.sidewaysDarkerColors[channelNumder]; } @@ -153,7 +162,7 @@ export function computeNotePositions(renderImmediately = false) color = this.sidewaysChannelColors[channelNumder]; } } - else if(this.drawActiveNotes) + else if (this.drawActiveNotes) { color = this.darkerColors[channelNumder]; } @@ -170,15 +179,15 @@ export function computeNotePositions(renderImmediately = false) pressedProgress: 0, // not pressed velocity: note.velocity, // VELOCITY IS MAPPED FROM 0 TO 1!!!! // if we ignore drawing active notes, draw those with regular colors - color: color, - }) + color: color + }); } else { // this note is pressed - if(this.sideways) + if (this.sideways) { - if(this.showVisualPitch) + if (this.showVisualPitch) { finalY += pitchBendXShift[channelNumder]; } @@ -186,7 +195,7 @@ export function computeNotePositions(renderImmediately = false) } else { - if(this.showVisualPitch) + if (this.showVisualPitch) { finalX += pitchBendXShift[channelNumder]; } @@ -194,7 +203,7 @@ export function computeNotePositions(renderImmediately = false) } // determine for how long the note has been pressed let noteProgress; - if(this.drawActiveNotes) + if (this.drawActiveNotes) { noteProgress = 1 + (note.start - currentSeqTime) / (note.length * PRESSED_EFFECT_TIME); } @@ -212,20 +221,23 @@ export function computeNotePositions(renderImmediately = false) pressedProgress: noteProgress, velocity: note.velocity, color: color - }) + }); } } } } - - if(noteIndex >= notes.length) + + if (noteIndex >= notes.length) { break; } - + note = notes[noteIndex]; } - if(firstNoteIndex > -1) channel.renderStartIndex = firstNoteIndex; + if (firstNoteIndex > -1) + { + channel.renderStartIndex = firstNoteIndex; + } }); // sort the notes from shortest to longest (draw order) notesToDraw.sort((n1, n2) => n2.height - n1.height); diff --git a/src/website/js/renderer/connect_sequencer.js b/src/website/js/renderer/connect_sequencer.js index 1b5acda7..4f37d561 100644 --- a/src/website/js/renderer/connect_sequencer.js +++ b/src/website/js/renderer/connect_sequencer.js @@ -6,11 +6,12 @@ export function connectSequencer(sequencer) { this.seq = sequencer; this.seq.addOnTimeChangeEvent(() => this.resetIndexes(), "renderer-time-change"); - - this.seq.addOnSongChangeEvent(async mid => { + + this.seq.addOnSongChangeEvent(async mid => + { this.calculateNoteTimes(await this.seq.getMIDI()); this.resetIndexes(); - if(mid.RMIDInfo?.["IPIC"] !== undefined) + if (mid.RMIDInfo?.["IPIC"] !== undefined) { const blob = new Blob([mid.RMIDInfo?.["IPIC"].buffer]); const url = URL.createObjectURL(blob); @@ -21,7 +22,7 @@ export function connectSequencer(sequencer) { this.canvas.style.background = ""; } - }, "renderer-song-change") + }, "renderer-song-change"); } /** @@ -29,10 +30,10 @@ export function connectSequencer(sequencer) */ export function resetIndexes() { - if(!this.noteTimes) + if (!this.noteTimes) { return; } - + this.noteTimes.forEach(n => n.renderStartIndex = 0); } \ No newline at end of file diff --git a/src/website/js/renderer/draw_notes.js b/src/website/js/renderer/draw_notes.js index 5e53bbdf..a415aeb3 100644 --- a/src/website/js/renderer/draw_notes.js +++ b/src/website/js/renderer/draw_notes.js @@ -14,37 +14,49 @@ const PRESSED_EFFECT_OPACITY = 0.5; export function drawNotes(notesToDraw, drawingContext, sideways) { // render the pressed effect first - notesToDraw.forEach(n => { - if(n.pressedProgress === 0) + notesToDraw.forEach(n => + { + if (n.pressedProgress === 0) { return; } drawingContext.fillStyle = n.color; const effectStrength = n.pressedProgress * n.velocity; drawingContext.globalAlpha = PRESSED_EFFECT_OPACITY * effectStrength; - if(sideways) + if (sideways) { - drawingContext.fillRect(n.xPos, n.yPos - n.height * effectStrength, n.width, n.height * (effectStrength * 2 + 1)); + drawingContext.fillRect( + n.xPos, + n.yPos - n.height * effectStrength, + n.width, + n.height * (effectStrength * 2 + 1) + ); drawingContext.globalAlpha = 1; return; } - drawingContext.fillRect(n.xPos - n.width * effectStrength, n.yPos, n.width * (effectStrength * 2 + 1), n.height); + drawingContext.fillRect( + n.xPos - n.width * effectStrength, + n.yPos, + n.width * (effectStrength * 2 + 1), + n.height + ); drawingContext.globalAlpha = 1; - }) - - notesToDraw.forEach(n => { + }); + + notesToDraw.forEach(n => + { // save and change color drawingContext.fillStyle = n.color; drawingContext.save(); - + // draw the rectangle drawingContext.translate(n.xPos, n.yPos); drawingContext.fillRect(0, 0, n.width, n.height); drawingContext.restore(); - + // draw the outline drawingContext.strokeStyle = STROKE_COLOR; drawingContext.lineWidth = n.stroke; drawingContext.strokeRect(n.xPos, n.yPos, n.width, n.height); - }) + }); } \ No newline at end of file diff --git a/src/website/js/renderer/render.js b/src/website/js/renderer/render.js index 12d14536..5bc806f2 100644 --- a/src/website/js/renderer/render.js +++ b/src/website/js/renderer/render.js @@ -1,7 +1,8 @@ -import { FONT_SIZE } from './renderer.js' -import { drawNotes } from './draw_notes.js' +import { FONT_SIZE } from "./renderer.js"; +import { drawNotes } from "./draw_notes.js"; let hasRenderedNoVoices = false; + /** * Renders a single frame * @this {Renderer} @@ -13,7 +14,7 @@ export function render(auto = true, force = false) let nothingToDo = (this.seq === undefined || this?.seq?.paused === true) && this.synth.voicesAmount === 0 && !force; if (!this.renderBool || nothingToDo) { - if(hasRenderedNoVoices) + if (hasRenderedNoVoices) { // no frames shall be drawn. Redo! if (auto) @@ -31,18 +32,18 @@ export function render(auto = true, force = false) { hasRenderedNoVoices = false; } - + if (auto) { this.drawingContext.clearRect(0, 0, this.canvas.width, this.canvas.height); } - + if (this.renderAnalysers && !this.synth.highPerformanceMode) { // draw the individual analysers this.renderWaveforms(); } - + if (this.renderNotes && this.noteTimes) { /** @@ -50,19 +51,19 @@ export function render(auto = true, force = false) * @type {NoteToRender[]} */ let notesToDraw = this.computeNotePositions(this.synth.highPerformanceMode); - + // draw the notes from longest to shortest (non black midi mode) - if(!this.synth.highPerformanceMode) + if (!this.synth.highPerformanceMode) { drawNotes(notesToDraw, this.drawingContext, this.sideways); } } - + // calculate fps let timeSinceLastFrame = performance.now() - this.frameTimeStart; this.frameTimeStart = performance.now(); let fps = 1000 / timeSinceLastFrame; - + // draw note count and fps this.drawingContext.textBaseline = "hanging"; this.drawingContext.textAlign = "end"; @@ -72,13 +73,13 @@ export function render(auto = true, force = false) this.drawingContext.fillText(`${this.notesOnScreen} notes`, this.canvas.width, FONT_SIZE * 2 + 5); this.drawingContext.fillText(this.version, this.canvas.width, 5); this.drawingContext.fillText(Math.round(fps).toString() + " FPS", this.canvas.width, FONT_SIZE + 5); - - - if(this.onRender) + + + if (this.onRender) { this.onRender(); } - if(auto) + if (auto) { requestAnimationFrame(this.render.bind(this)); } diff --git a/src/website/js/renderer/render_waveforms.js b/src/website/js/renderer/render_waveforms.js index a6e20373..6c3a6883 100644 --- a/src/website/js/renderer/render_waveforms.js +++ b/src/website/js/renderer/render_waveforms.js @@ -6,9 +6,10 @@ export const STABILIZE_WAVEFORMS_FFT_MULTIPLIER = 4; export function renderWaveforms() { const waveWidth = this.canvas.width / 4; - const waveHeight = this.canvas.height / 4 + const waveHeight = this.canvas.height / 4; // draw all 16 channel waveforms in a 4x4 pattern - this.channelAnalysers.forEach((analyser, channelNumber) => { + this.channelAnalysers.forEach((analyser, channelNumber) => + { const x = channelNumber % 4; const y = Math.floor(channelNumber / 4); // if no voices, skip @@ -16,17 +17,17 @@ export function renderWaveforms() for (let i = channelNumber; i < this.synth.channelProperties.length; i += this.channelAnalysers.length) { // check every channel that is connected, because can be more outputs than just 16!!! (for example channel 17 also outputs to analyser 1) - if(this.synth.channelProperties[i].voicesAmount > 0) + if (this.synth.channelProperties[i].voicesAmount > 0) { voicesPlaying = true; break; } } - if(!voicesPlaying) + if (!voicesPlaying) { // draw a straight line const waveWidth = this.canvas.width / 4; - const waveHeight = this.canvas.height / 4 + const waveHeight = this.canvas.height / 4; const relativeX = waveWidth * x; const relativeY = waveHeight * y + waveHeight / 2; this.drawingContext.lineWidth = this.lineThickness; @@ -37,23 +38,23 @@ export function renderWaveforms() this.drawingContext.stroke(); return; } - + const waveform = new Float32Array(analyser.frequencyBinCount); analyser.getFloatTimeDomainData(waveform); - + const relativeX = waveWidth * x; const relativeY = waveHeight * y + waveHeight / 2; const multiplier = this.waveMultiplier * waveHeight; - + // draw this.drawingContext.lineWidth = this.lineThickness; this.drawingContext.strokeStyle = this.channelColors[channelNumber]; this.drawingContext.beginPath(); - if(this._stabilizeWaveforms) + if (this._stabilizeWaveforms) { let length = waveform.length / STABILIZE_WAVEFORMS_FFT_MULTIPLIER; const step = waveWidth / length; - + // Oscilloscope triggering const halfLength = Math.floor(length / 2); // start searchin from half the length @@ -73,24 +74,26 @@ export function renderWaveforms() { this.drawingContext.lineTo( xPos, - relativeY + waveform[i] * multiplier); + relativeY + waveform[i] * multiplier + ); xPos += step; } } else { const step = waveWidth / waveform.length; - + let xPos = relativeX; for (let i = 0; i < waveform.length; i++) { this.drawingContext.lineTo( xPos, - relativeY + waveform[i] * multiplier); + relativeY + waveform[i] * multiplier + ); xPos += step; } } - + this.drawingContext.stroke(); channelNumber++; }); diff --git a/src/website/js/renderer/renderer.js b/src/website/js/renderer/renderer.js index f2c27ee4..dadafab9 100644 --- a/src/website/js/renderer/renderer.js +++ b/src/website/js/renderer/renderer.js @@ -1,16 +1,16 @@ -import {Synthetizer} from "../../../spessasynth_lib/synthetizer/synthetizer.js"; -import { calculateRGB } from '../utils/calculate_rgb.js' -import { render } from './render.js' -import { computeNotePositions } from './compute_note_positions.js' +import { Synthetizer } from "../../../spessasynth_lib/synthetizer/synthetizer.js"; +import { calculateRGB } from "../utils/calculate_rgb.js"; +import { render } from "./render.js"; +import { computeNotePositions } from "./compute_note_positions.js"; import { connectChannelAnalysers, createChannelAnalysers, disconnectChannelAnalysers, - updateFftSize, -} from './channel_analysers.js' -import { connectSequencer, resetIndexes } from './connect_sequencer.js' -import { renderWaveforms} from './render_waveforms.js' -import { calculateNoteTimes } from './calculate_note_times.js' + updateFftSize +} from "./channel_analysers.js"; +import { connectSequencer, resetIndexes } from "./connect_sequencer.js"; +import { renderWaveforms } from "./render_waveforms.js"; +import { calculateNoteTimes } from "./calculate_note_times.js"; /** * renderer.js @@ -51,6 +51,12 @@ export const MAX_NOTES = 81572; class Renderer { + /** + * called after a frame is rendered + * @type {function} + */ + onRender; + /** * Creates a new midi renderer for rendering notes visually. * @param channelColors {Array} @@ -76,28 +82,28 @@ class Renderer min: 0, max: 127 }; - + this.version = "v" + version; - + /** * adds this to the synth's visual pitch in position caluclation * @type {number} */ this.visualPitchBendOffset = 0; - + this.lineThickness = ANALYSER_STROKE; this._normalAnalyserFft = CHANNEL_ANALYSER_FFT; this._drumAnalyserFft = DRUMS_ANALYSER_FFT; this.waveMultiplier = WAVE_MULTIPLIER; - + /** * @type {boolean} * @private */ this._notesFall = true; this.sideways = false; - - + + // booleans this._renderBool = true; this.renderAnalysers = true; @@ -109,28 +115,28 @@ class Renderer * @type {boolean[]} */ this.renderChannels = Array(16).fill(true); - + /** * canvas * @type {HTMLCanvasElement} */ this.canvas = canvas; - + /** * @type {CanvasRenderingContext2D} */ this.drawingContext = this.canvas.getContext("2d"); - + // note colors this.plainColors = channelColors; - - this.computeColors() - + + this.computeColors(); + // synth and analysers this.synth = synth; this.delayNode = delayNode; this.notesOnScreen = 0; - + /** * @type {AnalyserNode[]} */ @@ -138,12 +144,12 @@ class Renderer this.createChannelAnalysers(synth); this.connectChannelAnalysers(synth); } - + get stabilizeWaveforms() { return this._stabilizeWaveforms; } - + /** * * @param val {boolean} @@ -153,15 +159,7 @@ class Renderer this._stabilizeWaveforms = val; this.updateFftSize(); } - - /** - * @param val {"down"|"up"} - */ - set direction(val) - { - this._notesFall = val === "down"; - } - + /** * @returns {"down"|"up"} */ @@ -169,84 +167,46 @@ class Renderer { return this._notesFall ? "down" : "up"; } - - computeColors() - { - this.channelColors = this.plainColors.map(c => { - const gradient = this.drawingContext.createLinearGradient(0, 0, - this.canvas.width / 128, 0); - gradient.addColorStop(0, calculateRGB(c, v => v * GRADIENT_DARKEN)); // darker color - gradient.addColorStop(1, c); // brighter color - return gradient; - }); - this.darkerColors = this.plainColors.map(c => { - const gradient = this.drawingContext.createLinearGradient(0, 0, - this.canvas.width / 128, 0); - - gradient.addColorStop(0, calculateRGB(c, v => v * GRADIENT_DARKEN * DARKER_MULTIPLIER)); // darker color - gradient.addColorStop(1, calculateRGB(c, v => v * DARKER_MULTIPLIER)); // brighter color - return gradient; - }); - - this.sidewaysChannelColors = this.plainColors.map(c => { - const gradient = this.drawingContext.createLinearGradient(0, 0, - 0, this.canvas.width / 128); - gradient.addColorStop(0, calculateRGB(c, v => v * GRADIENT_DARKEN)); // darker color - gradient.addColorStop(1, c); // brighter color - return gradient; - }); - this.sidewaysDarkerColors = this.plainColors.map(c => { - const gradient = this.drawingContext.createLinearGradient(0, 0, - 0, this.canvas.width / 128); - - gradient.addColorStop(0, calculateRGB(c, v => v * GRADIENT_DARKEN * DARKER_MULTIPLIER)); // darker color - gradient.addColorStop(1, calculateRGB(c, v => v * DARKER_MULTIPLIER)); // brighter color - return gradient; - }); - } - - toggleDarkMode() - { - this.canvas.classList.toggle("light_mode"); - } - + /** - * called after a frame is rendered - * @type {function} + * @param val {"down"|"up"} */ - onRender; - + set direction(val) + { + this._notesFall = val === "down"; + } + get normalAnalyserFft() { - return this._normalAnalyserFft + return this._normalAnalyserFft; } - + set normalAnalyserFft(value) { this._normalAnalyserFft = value; this.updateFftSize(); } - + get drumAnalyserFft() { return this._drumAnalyserFft; } - + set drumAnalyserFft(value) { this._drumAnalyserFft = value; this.updateFftSize(); } - + get renderBool() { return this._renderBool; } - + set renderBool(value) { this._renderBool = value; - if(value === true) + if (value === true) { this.connectChannelAnalysers(this.synth); } @@ -255,7 +215,7 @@ class Renderer this.disconnectChannelAnalysers(); } } - + /** * The range of displayed MIDI keys * @returns {{min: number, max: number}} @@ -264,18 +224,18 @@ class Renderer { return this._keyRange; } - + /** * The range of displayed MIDI keys * @param value {{min: number, max: number}} */ set keyRange(value) { - if(value.max === undefined || value.min === undefined) + if (value.max === undefined || value.min === undefined) { throw new TypeError("No min or max property!"); } - if(value.min > value.max) + if (value.min > value.max) { let temp = value.min; value.min = value.max; @@ -285,6 +245,96 @@ class Renderer value.max = Math.min(127, value.max); this._keyRange = value; } + + toggleDarkMode() + { + this.canvas.classList.toggle("light_mode"); + } + + computeColors() + { + this.channelColors = this.plainColors.map(c => + { + const gradient = this.drawingContext.createLinearGradient( + 0, + 0, + this.canvas.width / 128, + 0 + ); + gradient.addColorStop( + 0, + calculateRGB(c, v => v * GRADIENT_DARKEN) + ); // darker color + gradient.addColorStop(1, c); // brighter color + return gradient; + }); + this.darkerColors = this.plainColors.map(c => + { + const gradient = this.drawingContext.createLinearGradient( + 0, + 0, + this.canvas.width / 128, + 0 + ); + + gradient.addColorStop( + 0, + calculateRGB( + c, + v => v * GRADIENT_DARKEN * DARKER_MULTIPLIER + ) + ); // darker color + gradient.addColorStop( + 1, + calculateRGB(c, v => v * DARKER_MULTIPLIER) + ); // brighter color + return gradient; + }); + + this.sidewaysChannelColors = this.plainColors.map(c => + { + const gradient = this.drawingContext.createLinearGradient( + 0, + 0, + 0, + this.canvas.width / 128 + ); + gradient.addColorStop( + 0, + calculateRGB( + c, + v => v * GRADIENT_DARKEN + ) + ); // darker color + gradient.addColorStop(1, c); // brighter color + return gradient; + }); + this.sidewaysDarkerColors = this.plainColors.map(c => + { + const gradient = this.drawingContext.createLinearGradient( + 0, + 0, + 0, + this.canvas.width / 128 + ); + + gradient.addColorStop( + 0, + calculateRGB( + c, + v => v * GRADIENT_DARKEN * DARKER_MULTIPLIER + ) + ); // darker color + gradient.addColorStop( + 1, + calculateRGB( + c, + v => v * DARKER_MULTIPLIER + ) + ); // brighter color + return gradient; + }); + } } Renderer.prototype.render = render; @@ -301,4 +351,4 @@ Renderer.prototype.resetIndexes = resetIndexes; Renderer.prototype.renderWaveforms = renderWaveforms; -export { Renderer } \ No newline at end of file +export { Renderer }; \ No newline at end of file diff --git a/src/website/js/sequencer_ui/lyrics.js b/src/website/js/sequencer_ui/lyrics.js index 6cfcf4ec..cfd610fa 100644 --- a/src/website/js/sequencer_ui/lyrics.js +++ b/src/website/js/sequencer_ui/lyrics.js @@ -1,5 +1,5 @@ -import { supportedEncodings } from '../utils/encodings.js' -import { messageTypes } from '../../../spessasynth_lib/midi_parser/midi_message.js' +import { supportedEncodings } from "../utils/encodings.js"; +import { messageTypes } from "../../../spessasynth_lib/midi_parser/midi_message.js"; const ACTUAL_FONT_SIZE = parseFloat(getComputedStyle(document.body).fontSize); @@ -24,25 +24,26 @@ export function createLyrics() */ this.lyricsElement = {}; // main div - const mainLyricsDiv = document.createElement("div"); + const mainLyricsDiv = document.createElement("div"); mainLyricsDiv.classList.add("lyrics"); - + // title wrapper const titleWrapper = document.createElement("div"); titleWrapper.classList.add("lyrics_title_wrapper"); mainLyricsDiv.append(titleWrapper); this.lyricsElement.titleWrapper = titleWrapper; - + // title const lyricsTitle = document.createElement("h2"); this.locale.bindObjectProperty(lyricsTitle, "textContent", "locale.sequencerController.lyrics.title"); lyricsTitle.classList.add("lyrics_title"); titleWrapper.appendChild(lyricsTitle); this.lyricsElement.title = lyricsTitle; - + // encoding selector const encodingSelector = document.createElement("select"); - supportedEncodings.forEach(encoding => { + supportedEncodings.forEach(encoding => + { const option = document.createElement("option"); option.innerText = encoding; option.value = encoding; @@ -53,20 +54,20 @@ export function createLyrics() encodingSelector.classList.add("lyrics_selector"); this.encodingSelector = encodingSelector; titleWrapper.appendChild(encodingSelector); - + // the actual text const text = document.createElement("p"); text.classList.add("lyrics_text"); mainLyricsDiv.appendChild(text); - + const currentLyrics = document.createElement("span"); currentLyrics.classList.add("lyrics_text_highlight"); text.appendChild(currentLyrics); - + const allLyrics = document.createElement("span"); allLyrics.classList.add("lyrics_text_gray"); text.appendChild(allLyrics); - + // display for other texts const otherTextWrapper = document.createElement("details"); const sum = document.createElement("summary"); @@ -76,7 +77,7 @@ export function createLyrics() otherText.innerText = ""; otherTextWrapper.appendChild(otherText); mainLyricsDiv.appendChild(otherTextWrapper); - + this.lyricsElement.text = { highlight: currentLyrics, gray: allLyrics, @@ -94,7 +95,7 @@ export function createLyrics() */ export function setLyricsText(text) { - + const highlight = this.lyricsElement.text.highlight; const gray = this.lyricsElement.text.gray; gray.innerText = this.currentLyricsString.replace(text, ""); @@ -108,11 +109,14 @@ export function setLyricsText(text) export function updateOtherTextEvents() { let text = ""; - for(const raw of this.rawOtherTextEvents) + for (const raw of this.rawOtherTextEvents) { - text +=`
${Object.keys(messageTypes)
+        text += `
${Object.keys(messageTypes)
             .find(k => messageTypes[k] === raw.type)
-            .replace(/([a-z])([A-Z])/g, '$1 $2')}:
${this.decodeTextFix(raw.data.buffer)}

`; + .replace( + /([a-z])([A-Z])/g, + "$1 $2" + )}:
${this.decodeTextFix(raw.data.buffer)}

`; } this.lyricsElement.text.other.innerHTML = text; } \ No newline at end of file diff --git a/src/website/js/sequencer_ui/sequencer_ui.js b/src/website/js/sequencer_ui/sequencer_ui.js index 4042b816..11cb77b1 100644 --- a/src/website/js/sequencer_ui/sequencer_ui.js +++ b/src/website/js/sequencer_ui/sequencer_ui.js @@ -1,13 +1,13 @@ -import { Sequencer } from '../../../spessasynth_lib/sequencer/sequencer.js' -import { formatTime } from '../../../spessasynth_lib/utils/other.js' -import { supportedEncodings } from '../utils/encodings.js' -import { getBackwardSvg, getForwardSvg, getLoopSvg, getPauseSvg, getPlaySvg, getTextSvg } from '../utils/icons.js' -import { messageTypes } from '../../../spessasynth_lib/midi_parser/midi_message.js' -import { getSeqUIButton } from './sequi_button.js' -import { keybinds } from '../utils/keybinds.js' -import { createNavigatorHandler, updateTitleAndMediaStatus } from './title_and_media_status.js' -import { createLyrics, setLyricsText, updateOtherTextEvents } from './lyrics.js' -import { RMIDINFOChunks } from '../../../spessasynth_lib/midi_parser/rmidi_writer.js' +import { Sequencer } from "../../../spessasynth_lib/sequencer/sequencer.js"; +import { formatTime } from "../../../spessasynth_lib/utils/other.js"; +import { supportedEncodings } from "../utils/encodings.js"; +import { getBackwardSvg, getForwardSvg, getLoopSvg, getPauseSvg, getPlaySvg, getTextSvg } from "../utils/icons.js"; +import { messageTypes } from "../../../spessasynth_lib/midi_parser/midi_message.js"; +import { getSeqUIButton } from "./sequi_button.js"; +import { keybinds } from "../utils/keybinds.js"; +import { createNavigatorHandler, updateTitleAndMediaStatus } from "./title_and_media_status.js"; +import { createLyrics, setLyricsText, updateOtherTextEvents } from "./lyrics.js"; +import { RMIDINFOChunks } from "../../../spessasynth_lib/midi_parser/rmidi_writer.js"; /** * sequencer_ui.js @@ -59,10 +59,10 @@ class SequencerUI this.currentLyricsString = ""; this.musicModeUI = musicMode; } - + toggleDarkMode() { - if(this.mode === "dark") + if (this.mode === "dark") { this.mode = "light"; this.iconColor = ICON_COLOR_L; @@ -74,7 +74,7 @@ class SequencerUI this.iconColor = ICON_COLOR; this.iconDisabledColor = ICON_DISABLED_COLOR; } - if(!this.seq) + if (!this.seq) { this.requiresThemeUpdate = true; return; @@ -85,53 +85,53 @@ class SequencerUI this.lyricsElement.titleWrapper.classList.toggle("lyrics_light"); this.lyricsElement.selector.classList.toggle("lyrics_light"); } - + seqPlay(sendPlay = true) { - if(sendPlay) + if (sendPlay) { this.seq.play(); } this.playPause.innerHTML = getPauseSvg(ICON_SIZE); this.createNavigatorHandler(); this.updateTitleAndMediaStatus(); - if(!navigator.mediaSession) + if (!navigator.mediaSession) { return; } navigator.mediaSession.playbackState = "playing"; } - + seqPause(sendPause = true) { - if(sendPause) + if (sendPause) { this.seq.pause(); } this.playPause.innerHTML = getPlaySvg(ICON_SIZE); this.createNavigatorHandler(); this.updateTitleAndMediaStatus(); - if(!navigator.mediaSession) + if (!navigator.mediaSession) { return; } navigator.mediaSession.playbackState = "paused"; } - + switchToNextSong() { this.seq.nextSong(); this.createNavigatorHandler(); this.updateTitleAndMediaStatus(); } - + switchToPreviousSong() { this.seq.previousSong(); this.createNavigatorHandler(); this.updateTitleAndMediaStatus(); } - + /** * @param text {ArrayBuffer} * @param useInfoEncoding {boolean} @@ -140,17 +140,16 @@ class SequencerUI decodeTextFix(text, useInfoEncoding = false) { let encodingIndex = 0; - while(true) + while (true) { try { - if(useInfoEncoding) + if (useInfoEncoding) { - + } return this.decoder.decode(text); - } - catch (e) + } catch (e) { encodingIndex++; this.changeEncoding(supportedEncodings[encodingIndex]); @@ -158,7 +157,7 @@ class SequencerUI } } } - + /** * * @param sequencer {Sequencer} the sequencer to be used @@ -170,14 +169,15 @@ class SequencerUI this.setSliderInterval(); this.createNavigatorHandler(); this.updateTitleAndMediaStatus(); - - this.seq.onTextEvent = (data, type) => { + + this.seq.onTextEvent = (data, type) => + { const text = this.decodeTextFix(data.buffer); switch (type) { default: return; - + case messageTypes.text: case messageTypes.copyright: case messageTypes.cuePoint: @@ -185,38 +185,43 @@ class SequencerUI case messageTypes.instrumentName: case messageTypes.programName: case messageTypes.marker: - this.rawOtherTextEvents.push({type: type, data: data}); + this.rawOtherTextEvents.push({ type: type, data: data }); this.requiresTextUpdate = true; return; - + case messageTypes.lyric: this.text += text; this.rawLyrics.push(...data); this.setLyricsText(this.text); break; } - } - - this.seq.addOnTimeChangeEvent(() => { + }; + + this.seq.addOnTimeChangeEvent(() => + { this.text = ""; this.rawLyrics = []; this.seqPlay(false); }, "sequi-time-change"); - - this.seq.addOnSongChangeEvent(data => { + + this.seq.addOnSongChangeEvent(data => + { this.createNavigatorHandler(); this.updateTitleAndMediaStatus(); this.seqPlay(false); // disable loop if more than 1 song - if(this.seq.songsAmount > 1) + if (this.seq.songsAmount > 1) { this.seq.loop = false; - this.loopButton.firstElementChild.setAttribute("fill", this.iconDisabledColor); + this.loopButton.firstElementChild.setAttribute( + "fill", + this.iconDisabledColor + ); } - + // use encoding suggested by the rmidi if available this.hasInfoDecoding = this.seq.midiData.RMIDInfo?.[RMIDINFOChunks.encoding] !== undefined; - if(data.isEmbedded) + if (data.isEmbedded) { /** * @param type {string} @@ -225,20 +230,26 @@ class SequencerUI * @param prepend {string} * @return {string} */ - const verifyDecode = (type, def, decoder, prepend = "") => { - return this.seq.midiData.RMIDInfo?.[type] === undefined ? def : prepend + decoder.decode(this.seq.midiData.RMIDInfo?.[type]).replace(/\0$/, '') - } + const verifyDecode = (type, def, decoder, prepend = "") => + { + return this.seq.midiData.RMIDInfo?.[type] === undefined ? def : prepend + decoder.decode( + this.seq.midiData.RMIDInfo?.[type]).replace(/\0$/, ""); + }; const dec = new TextDecoder(); - const midiEncoding = verifyDecode(RMIDINFOChunks.midiEncoding, this.encoding, dec); + const midiEncoding = verifyDecode( + RMIDINFOChunks.midiEncoding, + this.encoding, + dec + ); const infoEncoding = verifyDecode(RMIDINFOChunks.encoding, "utf-8", dec); this.infoDecoder = new TextDecoder(infoEncoding); this.changeEncoding(midiEncoding); } }, "sequi-song-change"); - - if(this.requiresThemeUpdate) + + if (this.requiresThemeUpdate) { - if(this.mode === "light") + if (this.mode === "light") { // change to dark and then switch this.mode = "dark"; @@ -247,12 +258,12 @@ class SequencerUI // otherwise we're already dark } } - + changeEncoding(encoding) { this.encoding = encoding; this.decoder = new TextDecoder(encoding); - if(!this.hasInfoDecoding) + if (!this.hasInfoDecoding) { this.infoDecoder = new TextDecoder(encoding); } @@ -262,146 +273,168 @@ class SequencerUI this.updateTitleAndMediaStatus(false); this.setLyricsText(this.text); } - + createControls() { // time this.progressTime = document.createElement("p"); this.progressTime.id = "note_time"; // it'll always be on top - this.progressTime.onclick = event => { + this.progressTime.onclick = event => + { event.preventDefault(); const barPosition = progressBarBg.getBoundingClientRect(); const x = event.clientX - barPosition.left; const width = barPosition.width; - + this.seq.currentTime = (x / width) * this.seq.duration; playPauseButton.innerHTML = getPauseSvg(ICON_SIZE); }; - + this.createLyrics(); - - + + // background bar const progressBarBg = document.createElement("div"); progressBarBg.id = "note_progress_background"; this.progressBarBackground = progressBarBg; - - + + // foreground bar this.progressBar = document.createElement("div"); this.progressBar.id = "note_progress"; this.progressBar.min = (0).toString(); this.progressBar.max = this.seq.duration.toString(); - - + + // control buttons const controlsDiv = document.createElement("div"); - - + + // play pause - const playPauseButton = getSeqUIButton("Play/Pause", - getPauseSvg(ICON_SIZE)); + const playPauseButton = getSeqUIButton( + "Play/Pause", + getPauseSvg(ICON_SIZE) + ); this.playPause = playPauseButton; this.locale.bindObjectProperty(playPauseButton, "title", "locale.sequencerController.playPause"); - const togglePlayback = () => { - if(this.seq.paused) + const togglePlayback = () => + { + if (this.seq.paused) { this.seqPlay(); } else { - this.seqPause() + this.seqPause(); } - } + }; playPauseButton.onclick = togglePlayback; - - + + // previous song button - const previousSongButton = getSeqUIButton("Previous song", - getBackwardSvg(ICON_SIZE)); + const previousSongButton = getSeqUIButton( + "Previous song", + getBackwardSvg(ICON_SIZE) + ); this.locale.bindObjectProperty(previousSongButton, "title", "locale.sequencerController.previousSong"); previousSongButton.onclick = () => this.switchToPreviousSong(); - + // next song button - const nextSongButton = getSeqUIButton("Next song", - getForwardSvg(ICON_SIZE)); + const nextSongButton = getSeqUIButton( + "Next song", + getForwardSvg(ICON_SIZE) + ); this.locale.bindObjectProperty(nextSongButton, "title", "locale.sequencerController.nextSong"); nextSongButton.onclick = () => this.switchToNextSong(); - + // loop button - const loopButton = getSeqUIButton("Loop this", - getLoopSvg(ICON_SIZE)); + const loopButton = getSeqUIButton( + "Loop this", + getLoopSvg(ICON_SIZE) + ); this.locale.bindObjectProperty(loopButton, "title", "locale.sequencerController.loopThis"); - const toggleLoop = () => { - if(this.seq.loop) + const toggleLoop = () => + { + if (this.seq.loop) { this.seq.loop = false; } else { this.seq.loop = true; - if(this.seq.currentTime >= this.seq.duration) + if (this.seq.currentTime >= this.seq.duration) { this.seq.currentTime = 0; } } - loopButton.firstElementChild.setAttribute("fill", (this.seq.loop ? this.iconColor : this.iconDisabledColor)); - } + loopButton.firstElementChild.setAttribute( + "fill", + (this.seq.loop ? this.iconColor : this.iconDisabledColor) + ); + }; loopButton.onclick = toggleLoop; this.loopButton = loopButton; - - + + // show text button - const textButton = getSeqUIButton("Show lyrics", - getTextSvg(ICON_SIZE)); + const textButton = getSeqUIButton( + "Show lyrics", + getTextSvg(ICON_SIZE) + ); this.locale.bindObjectProperty(textButton, "title", "locale.sequencerController.lyrics.show"); textButton.firstElementChild.setAttribute("fill", this.iconDisabledColor); // defaults to disabled - const toggleLyrics = () => { + const toggleLyrics = () => + { this.lyricsElement.mainDiv.classList.toggle("lyrics_show"); - textButton.firstElementChild.setAttribute("fill", (this.lyricsElement.mainDiv.classList.contains("lyrics_show") ? this.iconColor : this.iconDisabledColor)); - } + textButton.firstElementChild.setAttribute( + "fill", + (this.lyricsElement.mainDiv.classList.contains("lyrics_show") ? this.iconColor : this.iconDisabledColor) + ); + }; textButton.onclick = toggleLyrics; - + // keyboard control - document.addEventListener("keydown", event => { - switch(event.key.toLowerCase()) + document.addEventListener("keydown", event => + { + switch (event.key.toLowerCase()) { case keybinds.playPause: event.preventDefault(); togglePlayback(); break; - + case keybinds.toggleLoop: event.preventDefault(); toggleLoop(); break; - + case keybinds.toggleLyrics: event.preventDefault(); toggleLyrics(); break; - + default: break; } - }) - + }); + // add everything controlsDiv.appendChild(previousSongButton); // |< controlsDiv.appendChild(loopButton); // () controlsDiv.appendChild(playPauseButton); // || controlsDiv.appendChild(textButton); // == controlsDiv.appendChild(nextSongButton); // >| - + this.controls.appendChild(progressBarBg); progressBarBg.appendChild(this.progressBar); this.controls.appendChild(this.progressTime); this.controls.appendChild(controlsDiv); - + // add number and arrow controls - document.addEventListener("keydown", e => { - + document.addEventListener("keydown", e => + { + switch (e.key.toLowerCase()) { case keybinds.seekBackwards: @@ -409,27 +442,27 @@ class SequencerUI this.seq.currentTime -= 5; playPauseButton.innerHTML = getPauseSvg(ICON_SIZE); break; - + case keybinds.seekForwards: e.preventDefault(); this.seq.currentTime += 5; playPauseButton.innerHTML = getPauseSvg(ICON_SIZE); break; - + case keybinds.previousSong: this.switchToPreviousSong(); break; - + case keybinds.nextSong: this.switchToNextSong(); break; - + default: - if(!isNaN(parseFloat(e.key))) + if (!isNaN(parseFloat(e.key))) { e.preventDefault(); const num = parseInt(e.key); - if(0 <= num && num <= 9) + if (0 <= num && num <= 9) { this.seq.currentTime = this.seq.duration * (num / 10); playPauseButton.innerHTML = getPauseSvg(ICON_SIZE); @@ -437,27 +470,28 @@ class SequencerUI } break; } - }) + }); } - + _updateInterval() { 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.requiresTextUpdate) + if (this.requiresTextUpdate) { this.updateOtherTextEvents(); this.requiresTextUpdate = false; } } - + setSliderInterval() { setInterval(this._updateInterval.bind(this), 100); } } + SequencerUI.prototype.createNavigatorHandler = createNavigatorHandler; SequencerUI.prototype.updateTitleAndMediaStatus = updateTitleAndMediaStatus; @@ -465,4 +499,4 @@ SequencerUI.prototype.createLyrics = createLyrics; SequencerUI.prototype.setLyricsText = setLyricsText; SequencerUI.prototype.updateOtherTextEvents = updateOtherTextEvents; -export { SequencerUI } \ No newline at end of file +export { SequencerUI }; \ No newline at end of file diff --git a/src/website/js/sequencer_ui/title_and_media_status.js b/src/website/js/sequencer_ui/title_and_media_status.js index d843060f..28845a9e 100644 --- a/src/website/js/sequencer_ui/title_and_media_status.js +++ b/src/website/js/sequencer_ui/title_and_media_status.js @@ -1,46 +1,54 @@ -import { formatTitle } from '../../../spessasynth_lib/utils/other.js' +import { formatTitle } from "../../../spessasynth_lib/utils/other.js"; /** * @this {SequencerUI} */ export function createNavigatorHandler() { - if(!navigator.mediaSession) + if (!navigator.mediaSession) { return; } - + navigator.mediaSession.metadata = new MediaMetadata({ title: this.currentSongTitle, artist: "SpessaSynth" }); - - navigator.mediaSession.setActionHandler("play", () => { + + navigator.mediaSession.setActionHandler("play", () => + { this.seqPlay(); }); - navigator.mediaSession.setActionHandler("pause", () => { + navigator.mediaSession.setActionHandler("pause", () => + { this.seqPause(); }); - navigator.mediaSession.setActionHandler("stop", () => { + navigator.mediaSession.setActionHandler("stop", () => + { this.seq.currentTime = 0; this.seqPause(); }); - navigator.mediaSession.setActionHandler("seekbackward", e => { + navigator.mediaSession.setActionHandler("seekbackward", e => + { this.seq.currentTime -= e.seekOffset || 10; }); - navigator.mediaSession.setActionHandler("seekforward", e => { + navigator.mediaSession.setActionHandler("seekforward", e => + { this.seq.currentTime += e.seekOffset || 10; }); - navigator.mediaSession.setActionHandler("seekto", e => { - this.seq.currentTime = e.seekTime + navigator.mediaSession.setActionHandler("seekto", e => + { + this.seq.currentTime = e.seekTime; }); - navigator.mediaSession.setActionHandler("previoustrack", () => { + navigator.mediaSession.setActionHandler("previoustrack", () => + { this.switchToPreviousSong(); }); - navigator.mediaSession.setActionHandler("nexttrack", () => { + navigator.mediaSession.setActionHandler("nexttrack", () => + { this.switchToNextSong(); }); - + navigator.mediaSession.playbackState = "playing"; } @@ -50,29 +58,30 @@ export function createNavigatorHandler() */ export function updateTitleAndMediaStatus(cleanOtherTextEvents = true) { - if(this.seq?.hasDummyData === true) + if (this.seq?.hasDummyData === true) { this.currentSongTitle = this.locale.getLocaleString("locale.synthInit.genericLoading"); } else { - const text = this.infoDecoder.decode(this.seq.midiData.rawMidiName.buffer).replace(/\0$/, ''); + const text = this.infoDecoder.decode(this.seq.midiData.rawMidiName.buffer).replace(/\0$/, ""); this.currentSongTitle = formatTitle(text); } - if(this.seq.midiData) + if (this.seq.midiData) { // combine lyrics into one binary array const lyricsArray = this.seq.midiData.lyrics; this.currentLyrics = new Uint8Array(lyricsArray.reduce((sum, cur) => sum + cur.length, 0)); let offset = 0; - for(const lyr of lyricsArray) + for (const lyr of lyricsArray) { this.currentLyrics.set(lyr, offset); offset += lyr.length; } - this.currentLyricsString = this.decodeTextFix(this.currentLyrics.buffer) || this.locale.getLocaleString("locale.sequencerController.lyrics.noLyrics"); + this.currentLyricsString = this.decodeTextFix(this.currentLyrics.buffer) || this.locale.getLocaleString( + "locale.sequencerController.lyrics.noLyrics"); this.setLyricsText(""); - if(cleanOtherTextEvents) + if (cleanOtherTextEvents) { this.rawOtherTextEvents = []; } @@ -80,20 +89,20 @@ export function updateTitleAndMediaStatus(cleanOtherTextEvents = true) document.getElementById("title").innerText = this.currentSongTitle; document.title = this.currentSongTitle + " - SpessaSynth"; this.musicModeUI.setTitle(this.currentSongTitle); - - if(!navigator.mediaSession) + + if (!navigator.mediaSession) { return; } - try { + try + { navigator.mediaSession.setPositionState({ duration: this.seq.duration, playbackRate: this.seq.playbackRate, position: this.seq.currentTime }); - } - catch(e) + } catch (e) { - + } } \ No newline at end of file diff --git a/src/website/js/settings_ui/handlers/interface_handler.js b/src/website/js/settings_ui/handlers/interface_handler.js index c033fb68..7016287b 100644 --- a/src/website/js/settings_ui/handlers/interface_handler.js +++ b/src/website/js/settings_ui/handlers/interface_handler.js @@ -1,5 +1,3 @@ - - /** * @this {SpessaSynthSettings} * @private @@ -7,29 +5,32 @@ export function _createInterfaceSettingsHandler() { const button = this.htmlControls.interface.themeSelector; - button.onclick = () => { + 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)) + for (const [code, locale] of Object.entries(this.locales)) { const option = document.createElement("option"); option.value = code; - option.textContent = locale.localeName + option.textContent = locale.localeName; select.appendChild(option); } - select.onchange = () => { + select.onchange = () => + { this.locale.changeGlobalLocale(select.value); this._saveSettings(); - } + }; const layoutSelect = this.htmlControls.interface.layoutSelector; - layoutSelect.onchange = () => { + layoutSelect.onchange = () => + { this._changeLayout(layoutSelect.value); this._saveSettings(); layoutSelect.blur(); - } + }; } /** @@ -44,44 +45,44 @@ export function _changeLayout(layout) const keyboard = document.getElementById("keyboard"); switch (layout) { - case 'downwards': + case "downwards": wrapper.classList.remove("upwards"); wrapper.classList.remove("left_to_right"); wrapper.classList.remove("right_to_left"); - + canvas.classList.remove("sideways"); keyboard.classList.remove("sideways"); this.renderer.direction = "down"; this.renderer.sideways = false; break; - - case 'upwards': + + case "upwards": wrapper.classList.add("upwards"); wrapper.classList.remove("left_to_right"); wrapper.classList.remove("right_to_left"); - + canvas.classList.remove("sideways"); keyboard.classList.remove("sideways"); this.renderer.direction = "up"; this.renderer.sideways = false; break; - - case 'left': + + case "left": wrapper.classList.remove("upwards"); wrapper.classList.add("left_to_right"); wrapper.classList.remove("right_to_left"); - + canvas.classList.add("sideways"); keyboard.classList.add("sideways"); this.renderer.direction = "up"; this.renderer.sideways = true; break; - - case 'right': + + case "right": wrapper.classList.remove("upwards"); wrapper.classList.remove("left_to_right"); wrapper.classList.add("right_to_left"); - + canvas.classList.add("sideways"); keyboard.classList.add("sideways"); this.renderer.direction = "down"; diff --git a/src/website/js/settings_ui/handlers/keyboard_handler.js b/src/website/js/settings_ui/handlers/keyboard_handler.js index 168caec5..3cc34aca 100644 --- a/src/website/js/settings_ui/handlers/keyboard_handler.js +++ b/src/website/js/settings_ui/handlers/keyboard_handler.js @@ -1,4 +1,4 @@ -export const USE_MIDI_RANGE = 'midi range'; +export const USE_MIDI_RANGE = "midi range"; /** * The channel colors are taken from synthui @@ -8,45 +8,53 @@ export const USE_MIDI_RANGE = 'midi range'; * @this {SpessaSynthSettings} * @private */ -export function _createKeyboardHandler( keyboard, synthui, renderer) +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]); - + 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 { + keyboardControls.channelSelector.onchange = () => + { keyboard.selectChannel(parseInt(keyboardControls.channelSelector.value)); - } - - keyboardControls.sizeSelector.onchange = () => { - if(this.musicMode.visible) + }; + + keyboardControls.sizeSelector.onchange = () => + { + if (this.musicMode.visible) { this.musicMode.setVisibility(false, document.getElementById("keyboard_canvas_wrapper")); - setTimeout(() => { - if(keyboardControls.sizeSelector.value === USE_MIDI_RANGE) + setTimeout(() => + { + if (keyboardControls.sizeSelector.value === USE_MIDI_RANGE) { this.autoKeyRange = true; - if(this?.sequi?.seq) + if (this?.sequi?.seq) { keyboard.keyRange = this.sequi.seq.midiData.keyRange; renderer.keyRange = this.sequi.seq.midiData.keyRange; @@ -62,10 +70,10 @@ export function _createKeyboardHandler( keyboard, synthui, renderer) }, 600); return; } - if(keyboardControls.sizeSelector.value === USE_MIDI_RANGE) + if (keyboardControls.sizeSelector.value === USE_MIDI_RANGE) { this.autoKeyRange = true; - if(this?.sequi?.seq) + if (this?.sequi?.seq) { keyboard.keyRange = this.sequi.seq.midiData.keyRange; renderer.keyRange = this.sequi.seq.midiData.keyRange; @@ -78,70 +86,77 @@ export function _createKeyboardHandler( keyboard, synthui, renderer) renderer.keyRange = this.keyboardSizes[keyboardControls.sizeSelector.value]; } this._saveSettings(); - } - + }; + /** * @param seq {Sequencer} */ - this.addSequencer = seq => { - seq.addOnSongChangeEvent(mid => { + this.addSequencer = seq => + { + seq.addOnSongChangeEvent(mid => + { if (this.autoKeyRange) { keyboard.keyRange = mid.keyRange; renderer.keyRange = mid.keyRange; } - if(mid.RMIDInfo?.["IPIC"] !== undefined) + if (mid.RMIDInfo?.["IPIC"] !== undefined) { // switch to music mode if picture available - if(this.musicMode.visible === false) + if (this.musicMode.visible === false) { this.toggleMusicPlayerMode().then(); } } }, "settings-keyboard-handler-song-change"); - } - + }; + // listen for new channels - synthui.synth.eventHandler.addEvent("newchannel", "settings-new-channel", () => { + synthui.synth.eventHandler.addEvent("newchannel", "settings-new-channel", () => + { 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) + synthui.synth.eventHandler.addEvent("programchange", "settings-keyboard-program-change", e => + { + if (e.userCalled) { keyboard.selectChannel(e.channel); keyboardControls.channelSelector.value = e.channel; } }); - + // QoL: change selected channel if the given channel is muted - synthui.synth.eventHandler.addEvent("mutechannel", "settings-keuboard-mute-channel", e => { - if(e.isMuted) + synthui.synth.eventHandler.addEvent("mutechannel", "settings-keuboard-mute-channel", e => + { + if (e.isMuted) { - if(e.channel === keyboard.channel) + if (e.channel === keyboard.channel) { // find the first non selected channel let channelNumber = 0; - while(synthui.synth.channelProperties[channelNumber].isMuted) + while (synthui.synth.channelProperties[channelNumber].isMuted) { channelNumber++; } - if(channelNumber < synthui.synth.channelsAmount) + if (channelNumber < synthui.synth.channelsAmount) { keyboard.selectChannel(channelNumber); keyboardControls.channelSelector.value = channelNumber; } } } - }) - + }); + // dark mode toggle - keyboardControls.modeSelector.onclick = () => { - if(this.musicMode.visible) + keyboardControls.modeSelector.onclick = () => + { + if (this.musicMode.visible) { this.musicMode.setVisibility(false, document.getElementById("keyboard_canvas_wrapper")); - setTimeout(() => { + setTimeout(() => + { keyboard.toggleMode(); this._saveSettings(); }, 600); @@ -149,12 +164,13 @@ export function _createKeyboardHandler( keyboard, synthui, renderer) } keyboard.toggleMode(); this._saveSettings(); - } - + }; + // keyboard show toggle - keyboardControls.showSelector.onclick = () => { + keyboardControls.showSelector.onclick = () => + { keyboard.shown = !keyboard.shown; 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 index bbc4e522..e6b48396 100644 --- a/src/website/js/settings_ui/handlers/midi_handler.js +++ b/src/website/js/settings_ui/handlers/midi_handler.js @@ -1,5 +1,5 @@ -import { isMobile } from '../../utils/is_mobile.js' -import { showNotification } from '../../notification/notification.js' +import { isMobile } from "../../utils/is_mobile.js"; +import { showNotification } from "../../notification/notification.js"; /** * @param handler {MIDIDeviceHandler} @@ -10,21 +10,23 @@ import { showNotification } from '../../notification/notification.js' */ export function _createMidiSettingsHandler(handler, sequi, synthui) { - handler.createMIDIDeviceHandler().then(success => { - if(success) + handler.createMIDIDeviceHandler().then(success => + { + if (success) { this._createMidiInputHandler(handler, synthui.synth); this._createMidiOutputHandler(handler, sequi); } else { - if(!isMobile) + if (!isMobile) { showNotification( this.locale.getLocaleString("locale.warnings.warning"), [{ type: "text", - textContent: this.locale.getLocaleString("locale.warnings.noMidiSupport") + textContent: this.locale.getLocaleString( + "locale.warnings.noMidiSupport") }] ); } @@ -42,21 +44,22 @@ export function _createMidiSettingsHandler(handler, sequi, synthui) export function _createMidiInputHandler(handler, synth) { // input selector - if(handler.inputs.length < 1) + if (handler.inputs.length < 1) { return; } // no device const select = this.htmlControls.midi.inputSelector; - for(const input of handler.inputs) + 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") + select.onchange = () => + { + if (select.value === "-1") { handler.disconnectAllDevicesFromSynth(); } @@ -65,7 +68,7 @@ export function _createMidiInputHandler(handler, synth) handler.connectDeviceToSynth(handler.inputs.get(select.value), synth); } this._saveSettings(); - } + }; } /** @@ -77,32 +80,34 @@ export function _createMidiInputHandler(handler, synth) */ export function _createMidiOutputHandler(handler, sequi) { - if(!handler.outputs) + if (!handler.outputs) { - setTimeout(() => { + setTimeout(() => + { this._createMidiOutputHandler(handler, sequi); }, 1000); return; } - if(handler.outputs.length < 1) + if (handler.outputs.length < 1) { return; } const select = this.htmlControls.midi.outputSelector; - for(const output of handler.outputs) + 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) + + select.onchange = () => + { + if (!sequi.seq) { return; } - if(select.value === "-1") + if (select.value === "-1") { handler.disconnectSeqFromMIDI(sequi.seq); } @@ -111,5 +116,5 @@ export function _createMidiOutputHandler(handler, sequi) 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 index 44dc7d4f..d1700d6d 100644 --- a/src/website/js/settings_ui/handlers/renderer_handler.js +++ b/src/website/js/settings_ui/handlers/renderer_handler.js @@ -1,4 +1,4 @@ -import { getSpan } from '../sliders.js' +import { getSpan } from "../sliders.js"; /** * @param renderer {Renderer} @@ -8,66 +8,87 @@ import { getSpan } from '../sliders.js' export function _createRendererHandler(renderer) { const rendererControls = this.htmlControls.renderer; - + // note falling time - rendererControls.noteTimeSlider.addEventListener("input", () => { + rendererControls.noteTimeSlider.addEventListener("input", () => + { renderer.noteFallingTimeMs = rendererControls.noteTimeSlider.value; - getSpan(rendererControls.noteTimeSlider).innerText = `${rendererControls.noteTimeSlider.value}ms` + getSpan(rendererControls.noteTimeSlider).innerText = `${rendererControls.noteTimeSlider.value}ms`; }); // bind to onchange instead of oniinput to prevent spam - rendererControls.noteTimeSlider.onchange = () => { this._saveSettings(); } - + rendererControls.noteTimeSlider.onchange = () => + { + this._saveSettings(); + }; + // waveform line thickness - rendererControls.analyserThicknessSlider.addEventListener("input", () => { + rendererControls.analyserThicknessSlider.addEventListener("input", () => + { renderer.lineThickness = parseInt(rendererControls.analyserThicknessSlider.value); getSpan(rendererControls.analyserThicknessSlider).innerText = `${rendererControls.analyserThicknessSlider.value}px`; }); - rendererControls.analyserThicknessSlider.onchange = () => { this._saveSettings(); } - + rendererControls.analyserThicknessSlider.onchange = () => + { + this._saveSettings(); + }; + // fft size (sample size) - rendererControls.analyserFftSlider.addEventListener("input", () => { + rendererControls.analyserFftSlider.addEventListener("input", () => + { 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(); getSpan(rendererControls.analyserFftSlider).innerText = `${value}`; }); - rendererControls.analyserFftSlider.onchange = () => { this._saveSettings(); } - + rendererControls.analyserFftSlider.onchange = () => + { + this._saveSettings(); + }; + // wave multiplier - rendererControls.waveMultiplierSlizer.addEventListener("input", () => { + rendererControls.waveMultiplierSlizer.addEventListener("input", () => + { renderer.waveMultiplier = parseInt(rendererControls.waveMultiplierSlizer.value); getSpan(rendererControls.waveMultiplierSlizer).innerText = rendererControls.waveMultiplierSlizer.value; }); - rendererControls.waveMultiplierSlizer.onchange = () => { this._saveSettings(); } - + rendererControls.waveMultiplierSlizer.onchange = () => + { + this._saveSettings(); + }; + // render waveforms - rendererControls.analyserToggler.onclick = () => { + rendererControls.analyserToggler.onclick = () => + { renderer.renderAnalysers = !renderer.renderAnalysers; - this._saveSettings() + this._saveSettings(); }; - + // render notes - rendererControls.noteToggler.onclick = () => { + rendererControls.noteToggler.onclick = () => + { renderer.renderNotes = !renderer.renderNotes; - this._saveSettings() + this._saveSettings(); }; - + // render active notes effect - rendererControls.activeNoteToggler.onclick = () => { + rendererControls.activeNoteToggler.onclick = () => + { renderer.drawActiveNotes = !renderer.drawActiveNotes; - this._saveSettings() + this._saveSettings(); }; - + // show visual pitch - rendererControls.visualPitchToggler.onclick = () => { + rendererControls.visualPitchToggler.onclick = () => + { renderer.showVisualPitch = !renderer.showVisualPitch; this._saveSettings(); }; - + // stabilize waveforms - rendererControls.stabilizeWaveformsToggler.onclick = () => { + rendererControls.stabilizeWaveformsToggler.onclick = () => + { renderer.stabilizeWaveforms = !renderer.stabilizeWaveforms; this._saveSettings(); - } + }; } \ No newline at end of file diff --git a/src/website/js/settings_ui/handlers/toggle_dark_mode.js b/src/website/js/settings_ui/handlers/toggle_dark_mode.js index 44dbc6a3..43705a7b 100644 --- a/src/website/js/settings_ui/handlers/toggle_dark_mode.js +++ b/src/website/js/settings_ui/handlers/toggle_dark_mode.js @@ -31,7 +31,7 @@ const TRANSITION_TIME = 0.2; */ export function _toggleDarkMode() { - if(this.mode === "dark") + if (this.mode === "dark") { this.mode = "light"; this.renderer.drawActiveNotes = false; @@ -40,38 +40,38 @@ export function _toggleDarkMode() { this.renderer.drawActiveNotes = true; this.mode = "dark"; - + } this.renderer.toggleDarkMode(); this.synthui.toggleDarkMode(); - this.sequi.toggleDarkMode() + this.sequi.toggleDarkMode(); this.musicMode.toggleDarkMode(); - + document.getElementsByClassName("spessasynth_main")[0].classList.toggle("light_mode"); - + // 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) + for (let rule of rules) { - if(rule.selectorText === "*") + if (rule.selectorText === "*") { - if(this.mode === "dark") + if (this.mode === "dark") { // dark mode transitionColor(FC_LIGHT, FC_DARK, TRANSITION_TIME, rule, "--font-color"); - + transitionColor(TBC_LIGHT.start, TBC_DARK.start, TRANSITION_TIME, rule, "--top-buttons-color-start"); transitionColor(TBC_LIGHT.end, TBC_DARK.end, TRANSITION_TIME, rule, "--top-buttons-color-end"); - + transitionColor(TC_LIGHT.start, TC_DARK.start, TRANSITION_TIME, rule, "--top-color-start"); transitionColor(TC_LIGHT.end, TC_DARK.end, TRANSITION_TIME, rule, "--top-color-end"); } @@ -79,10 +79,10 @@ export function _toggleDarkMode() { // light mode transitionColor(FC_DARK, FC_LIGHT, TRANSITION_TIME, rule, "--font-color"); - + transitionColor(TBC_DARK.start, TBC_LIGHT.start, TRANSITION_TIME, rule, "--top-buttons-color-start"); transitionColor(TBC_DARK.end, TBC_LIGHT.end, TRANSITION_TIME, rule, "--top-buttons-color-end"); - + transitionColor(TC_DARK.start, TC_LIGHT.start, TRANSITION_TIME, rule, "--top-color-start"); transitionColor(TC_DARK.end, TC_LIGHT.end, TRANSITION_TIME, rule, "--top-color-end"); } @@ -96,6 +96,7 @@ export function _toggleDarkMode() * @type {Object} */ let intervals = {}; + /** * @param initialColor {string} hex * @param targetColor {string} hex @@ -105,11 +106,12 @@ let intervals = {}; */ function transitionColor(initialColor, targetColor, duration, cssRule, propertyName) { - if(intervals[propertyName]) + if (intervals[propertyName]) { clearInterval(intervals[propertyName]); intervals[propertyName] = undefined; } + /** * @param hex {string} * @return {{r: number, b: number, g: number}} @@ -128,7 +130,7 @@ function transitionColor(initialColor, targetColor, duration, cssRule, propertyN b: num & 255 }; } - + /** * @param start {number} * @param end {number} @@ -139,31 +141,31 @@ function transitionColor(initialColor, targetColor, duration, cssRule, propertyN { return start + ((end - start) * progress); } - + // Parse initial and target colors const startColor = hexToRgb(initialColor); const endColor = hexToRgb(targetColor); - + const startTime = performance.now() / 1000; - + function step() { const currentTime = performance.now() / 1000; const elapsedTime = currentTime - startTime; const progress = Math.min(elapsedTime / duration, 1); - + const r = Math.round(interpolate(startColor.r, endColor.r, progress)); const g = Math.round(interpolate(startColor.g, endColor.g, progress)); const b = Math.round(interpolate(startColor.b, endColor.b, progress)); - + cssRule.style.setProperty(propertyName, `rgb(${r}, ${g}, ${b})`); - + if (progress >= 1) { clearInterval(intervals[propertyName]); intervals[propertyName] = undefined; } } - + intervals[propertyName] = setInterval(step, 1000 / 60); // 60 FPS should be enough } \ 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 3dfaf022..b8144654 100644 --- a/src/website/js/settings_ui/saving/load_settings.js +++ b/src/website/js/settings_ui/saving/load_settings.js @@ -1,6 +1,6 @@ -import { SpessaSynthInfo } from '../../../../spessasynth_lib/utils/loggin.js' -import { getSpan } from '../sliders.js' -import { USE_MIDI_RANGE } from '../handlers/keyboard_handler.js' +import { SpessaSynthInfo } from "../../../../spessasynth_lib/utils/loggin.js"; +import { getSpan } from "../sliders.js"; +import { USE_MIDI_RANGE } from "../handlers/keyboard_handler.js"; /** * @private @@ -12,14 +12,14 @@ export async function _loadSettings() * @type {SavedSettings} */ const savedSettings = await window.savedSettings; - - if(!savedSettings.interface) + + if (!savedSettings.interface) { return; } - - SpessaSynthInfo("Loading saved settings...", savedSettings) - + + SpessaSynthInfo("Loading saved settings...", savedSettings); + // renderer const rendererControls = this.htmlControls.renderer; const renderer = this.renderer; @@ -27,66 +27,66 @@ export async function _loadSettings() // note falling time renderer.noteFallingTimeMs = rendererValues.noteFallingTimeMs; rendererControls.noteTimeSlider.value = rendererValues.noteFallingTimeMs; - rendererControls.noteTimeSlider.dispatchEvent(new Event('input')); - getSpan(rendererControls.noteTimeSlider).innerText = `${rendererValues.noteFallingTimeMs}ms` - + rendererControls.noteTimeSlider.dispatchEvent(new Event("input")); + getSpan(rendererControls.noteTimeSlider).innerText = `${rendererValues.noteFallingTimeMs}ms`; + // waveform line thickness rendererControls.analyserThicknessSlider.value = rendererValues.waveformThickness; - rendererControls.analyserThicknessSlider.dispatchEvent(new Event('input')); + rendererControls.analyserThicknessSlider.dispatchEvent(new Event("input")); renderer.lineThickness = rendererValues.waveformThickness; getSpan(rendererControls.analyserThicknessSlider).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); - rendererControls.analyserFftSlider.dispatchEvent(new Event('input')); + rendererControls.analyserFftSlider.dispatchEvent(new Event("input")); renderer.normalAnalyserFft = value; renderer.drumAnalyserFft = Math.pow(2, Math.min(15, Math.log2(value) + 2)); renderer.updateFftSize(); getSpan(rendererControls.analyserFftSlider).innerText = `${value}`; - + // wave multiplier renderer.waveMultiplier = rendererValues.amplifier; rendererControls.waveMultiplierSlizer.value = rendererValues.amplifier; - rendererControls.waveMultiplierSlizer.dispatchEvent(new Event('input')); + rendererControls.waveMultiplierSlizer.dispatchEvent(new Event("input")); getSpan(rendererControls.waveMultiplierSlizer).innerText = rendererValues.amplifier.toString(); - + // render waveforms let controls = this.htmlControls.renderer; renderer.renderAnalysers = rendererValues.renderWaveforms; controls.analyserToggler.checked = rendererValues.renderWaveforms; - + // render notes renderer.renderNotes = rendererValues.renderNotes; controls.noteToggler.checked = rendererValues.renderNotes; - + // render active notes effect renderer.drawActiveNotes = rendererValues.drawActiveNotes; controls.activeNoteToggler.checked = rendererValues.drawActiveNotes; - + // show visual pitch renderer.showVisualPitch = rendererValues.showVisualPitch; controls.visualPitchToggler.checked = rendererValues.showVisualPitch; - + // stabilize waveforms renderer.stabilizeWaveforms = rendererValues.stabilizeWaveforms; controls.stabilizeWaveformsToggler.checked = 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.setKeyRange(keyboardValues.keyRange, false); // find the correct option for the size - if(keyboardValues.autoRange) + if (keyboardValues.autoRange) { keyboardControls.sizeSelector.value = USE_MIDI_RANGE; this.autoKeyRange = true; @@ -98,27 +98,28 @@ export async function _loadSettings() .find(size => this.keyboardSizes[size].min === keyboardValues.keyRange.min && this.keyboardSizes[size].max === keyboardValues.keyRange.max); } // keyboard theme - if(keyboardValues.mode === "dark") + if (keyboardValues.mode === "dark") { keyboard.toggleMode(false); this.htmlControls.keyboard.modeSelector.checked = true; } // keyboard show - if(keyboardValues.show === false) + if (keyboardValues.show === false) { keyboard.shown = false; this.htmlControls.keyboard.showSelector.checked = false; } - - + + // interface this.locale.changeGlobalLocale(savedSettings.interface.language, true); - + // using set timeout here fixes it for some reason - setTimeout(() => { + setTimeout(() => + { this.htmlControls.interface.languageSelector.value = savedSettings.interface.language; - }, 100); - if(savedSettings.interface.mode === "light") + }, 100); + if (savedSettings.interface.mode === "light") { this._toggleDarkMode(); this.htmlControls.interface.themeSelector.checked = false; @@ -127,7 +128,7 @@ export async function _loadSettings() { this.htmlControls.interface.themeSelector.checked = true; } - + this.htmlControls.interface.layoutSelector.value = savedSettings.interface.layout || "downwards"; this._changeLayout(savedSettings.interface.layout || "downwards"); } \ 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 index d4f75e77..02471b7b 100644 --- a/src/website/js/settings_ui/saving/save_settings.js +++ b/src/website/js/settings_ui/saving/save_settings.js @@ -5,7 +5,7 @@ */ export function _saveSettings() { - if(window.saveSettings) + if (window.saveSettings) { window.saveSettings(this._serializeSettings()); } diff --git a/src/website/js/settings_ui/saving/serialize_settings.js b/src/website/js/settings_ui/saving/serialize_settings.js index 3d2f2545..b0a43381 100644 --- a/src/website/js/settings_ui/saving/serialize_settings.js +++ b/src/website/js/settings_ui/saving/serialize_settings.js @@ -1,4 +1,4 @@ -import { USE_MIDI_RANGE } from '../handlers/keyboard_handler.js' +import { USE_MIDI_RANGE } from "../handlers/keyboard_handler.js"; /** * Serializes settings into a nice object @@ -14,14 +14,14 @@ export function _serializeSettings() waveformThickness: this.renderer.lineThickness, sampleSize: this.renderer.normalAnalyserFft, amplifier: this.renderer.waveMultiplier, - renderWaveforms: this.renderer.renderAnalysers, + renderWaveforms: this.renderer.renderAnalysers, 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, @@ -29,16 +29,16 @@ export function _serializeSettings() autoRange: this.htmlControls.keyboard.sizeSelector.value === USE_MIDI_RANGE, show: this.htmlControls.keyboard.showSelector.checked === true }, - + midi: { input: this.midiDeviceHandler.selectedInput === null ? null : this.midiDeviceHandler.selectedInput.name, - output: this.midiDeviceHandler.selectedOutput === null ? null: this.midiDeviceHandler.selectedOutput.name + output: this.midiDeviceHandler.selectedOutput === null ? null : this.midiDeviceHandler.selectedOutput.name }, - + interface: { mode: this.mode, language: this.htmlControls.interface.languageSelector.value, - layout: this.htmlControls.interface.layoutSelector.value, + layout: this.htmlControls.interface.layoutSelector.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 0a442212..3788bc2a 100644 --- a/src/website/js/settings_ui/settings.js +++ b/src/website/js/settings_ui/settings.js @@ -1,24 +1,25 @@ -import { settingsHtml } from './settings_html.js' -import { getDownArrowSvg, getGearSvg } from '../utils/icons.js' -import { _loadSettings } from './saving/load_settings.js' -import { _saveSettings } from './saving/save_settings.js' -import { _serializeSettings } from './saving/serialize_settings.js' -import { _changeLayout, _createInterfaceSettingsHandler } from './handlers/interface_handler.js' -import { _toggleDarkMode } from './handlers/toggle_dark_mode.js' -import { _createRendererHandler } from './handlers/renderer_handler.js' +import { settingsHtml } from "./settings_html.js"; +import { getDownArrowSvg, getGearSvg } from "../utils/icons.js"; +import { _loadSettings } from "./saving/load_settings.js"; +import { _saveSettings } from "./saving/save_settings.js"; +import { _serializeSettings } from "./saving/serialize_settings.js"; +import { _changeLayout, _createInterfaceSettingsHandler } from "./handlers/interface_handler.js"; +import { _toggleDarkMode } from "./handlers/toggle_dark_mode.js"; +import { _createRendererHandler } from "./handlers/renderer_handler.js"; import { _createMidiInputHandler, _createMidiOutputHandler, - _createMidiSettingsHandler, -} from './handlers/midi_handler.js' -import { _createKeyboardHandler } from './handlers/keyboard_handler.js' -import { localeList } from '../locale/locale_files/locale_list.js' -import { keybinds } from '../utils/keybinds.js' -import { handleSliders } from './sliders.js' -import { ANIMATION_REFLOW_TIME } from '../utils/animation_utils.js' + _createMidiSettingsHandler +} from "./handlers/midi_handler.js"; +import { _createKeyboardHandler } from "./handlers/keyboard_handler.js"; +import { localeList } from "../locale/locale_files/locale_list.js"; +import { keybinds } from "../utils/keybinds.js"; +import { handleSliders } from "./sliders.js"; +import { ANIMATION_REFLOW_TIME } from "../utils/animation_utils.js"; const TRANSITION_TIME = 0.2; + /** * settings.js * purpose: manages the gui settings, controlling things like render settings, light mode etc. @@ -26,6 +27,12 @@ const TRANSITION_TIME = 0.2; class SpessaSynthSettings { + /** + * @type {function} + * @param {Sequencer} seq + */ + addSequencer; + /** * Creates a new instance of synthetizer UI * @param settingsWrapper {HTMLElement} the element to create the settings in @@ -48,7 +55,7 @@ class SpessaSynthSettings { this.mode = "dark"; this.autoKeyRange = false; - + this.renderer = renderer; this.midiKeyboard = midiKeyboard; this.midiDeviceHandler = midiDeviceHandler; @@ -56,7 +63,7 @@ class SpessaSynthSettings this.sequi = sequi; this.locale = localeManager; this.musicMode = playerInfo; - + this.locales = localeList; this.keyboardSizes = { "full": { min: 0, max: 127 }, @@ -64,7 +71,7 @@ class SpessaSynthSettings "5 octaves": { min: 36, max: 96 }, "two octaves": { min: 53, max: 77 } }; - + /** * @type {HTMLElement} */ @@ -73,64 +80,66 @@ class SpessaSynthSettings settingsButton.classList.add("seamless_button"); settingsButton.classList.add("settings_button"); settingsWrapper.appendChild(settingsButton); - + 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); - + // add svg to show top button - const showTopButton = document.getElementsByClassName("show_top_button")[0]; + const showTopButton = document.getElementsByClassName("show_top_button")[0]; showTopButton.innerHTML = getDownArrowSvg(20); - - let text = document.createElement('span'); + + let text = document.createElement("span"); this.locale.bindObjectProperty(text, "innerText", "locale.settings.toggleButton"); settingsButton.appendChild(text); - - let gear = document.createElement('div'); + + let gear = document.createElement("div"); gear.innerHTML = getGearSvg(24); gear.classList.add("gear"); settingsButton.appendChild(gear); - + 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); - + musicModeButton.onclick = this.toggleMusicPlayerMode.bind(this); - + hideTopButton.onclick = this.hideTopPart.bind(this); - + this.hideOnDocClick = true; // stop propagation to disable hide on click outside - this.mainDiv.onclick = () => { + this.mainDiv.onclick = () => + { this.hideOnDocClick = false; }; - + // hide if clicked outside - document.addEventListener("click", () => { - if(!this.hideOnDocClick) + document.addEventListener("click", () => + { + if (!this.hideOnDocClick) { this.hideOnDocClick = true; return; } this.setVisibility(false); - }) - + }); + // load the html this.mainDiv.innerHTML = settingsHtml; // load input type range handleSliders(this.mainDiv); - + // bind all translations to the html for (const element of this.mainDiv.querySelectorAll("*[translate-path]")) { @@ -141,55 +150,58 @@ class SpessaSynthSettings { const path = element.getAttribute("translate-path-title"); // translate-path-title: apply to both innerText and title, by adding .title and .description respectively - this.locale.bindObjectProperty(element, "textContent", path + ".title"); + this.locale.bindObjectProperty(element, "textContent", path + ".title"); this.locale.bindObjectProperty(element, "title", path + ".description"); } - + this.getHtmlControls(); - - + + // key bind is "R" - document.addEventListener("keydown", e => { + document.addEventListener("keydown", e => + { switch (e.key.toLowerCase()) { case keybinds.settingsShow: this.setVisibility(!this.visible); break; - + // hide when synth controller shown case keybinds.synthesizerUIShow: this.setVisibility(false); } - }) - + }); + // if window.savedSettings exists, load it - if(window.savedSettings) + if (window.savedSettings) { - this._loadSettings().then(() => { - this.createHandlers(renderer, midiKeyboard, midiDeviceHandler, sequi, sythui) + this._loadSettings().then(() => + { + this.createHandlers(renderer, midiKeyboard, midiDeviceHandler, sequi, sythui); }); } else { - this.createHandlers(renderer, midiKeyboard, midiDeviceHandler, sequi, sythui) + this.createHandlers(renderer, midiKeyboard, midiDeviceHandler, sequi, sythui); } - + this.topPartVisible = true; - let fullscreen = false - + let fullscreen = false; + // detect fullscreen (even f11) - window.addEventListener("resize", () => { + window.addEventListener("resize", () => + { let maxHeight = window.screen.height, maxWidth = window.screen.width, curHeight = window.outerHeight, curWidth = window.outerWidth; - + let screen; screen = maxWidth === curWidth && maxHeight === curHeight; - if(screen !== fullscreen) + if (screen !== fullscreen) { fullscreen = screen; - if(screen) + if (screen) { this.hideTopPart(); } @@ -199,9 +211,10 @@ class SpessaSynthSettings } } }); - - document.addEventListener("fullscreenchange", () => { - if(document.fullscreenElement === null) + + document.addEventListener("fullscreenchange", () => + { + if (document.fullscreenElement === null) { this.showTopPart(); } @@ -209,45 +222,40 @@ class SpessaSynthSettings { this.hideTopPart(); } - }) + }); } - - /** - * @type {function} - * @param {Sequencer} seq - */ - addSequencer; - + async toggleMusicPlayerMode() { - if(this.musicMode.visible === false) + if (this.musicMode.visible === false) { this.hideTopPart(); } this.musicMode.setVisibility(!this.musicMode.visible, document.getElementById("keyboard_canvas_wrapper")); this.renderer.renderBool = !this.musicMode.visible; } - + showTopPart() { - if(this.topPartVisible === true) + if (this.topPartVisible === true) { return; } this.topPartVisible = true; const topPart = document.getElementsByClassName("top_part")[0]; - const showTopButton = document.getElementsByClassName("show_top_button")[0]; + const showTopButton = document.getElementsByClassName("show_top_button")[0]; topPart.style.display = ""; - setTimeout(() => { + setTimeout(() => + { topPart.classList.remove("top_part_hidden"); }, ANIMATION_REFLOW_TIME); showTopButton.classList.remove("shown"); showTopButton.style.display = "none"; } - + hideTopPart() { - if(this.topPartVisible === false) + if (this.topPartVisible === false) { return; } @@ -255,33 +263,36 @@ class SpessaSynthSettings // hide top const topPart = document.getElementsByClassName("top_part")[0]; topPart.classList.add("top_part_hidden"); - setTimeout(() => { + setTimeout(() => + { topPart.style.display = "none"; }, 200); - + // show button to get it back - const showTopButton = document.getElementsByClassName("show_top_button")[0]; + const showTopButton = document.getElementsByClassName("show_top_button")[0]; showTopButton.style.display = "flex"; - setTimeout(() => { + setTimeout(() => + { showTopButton.classList.add("shown"); }, ANIMATION_REFLOW_TIME); - + showTopButton.onclick = this.showTopPart.bind(this); } - + /** * @param visible {boolean} */ setVisibility(visible) { - if(this.animationId) + if (this.animationId) { clearTimeout(this.animationId); } - if(visible) + if (visible) { this.mainDiv.style.display = "block"; - setTimeout(() => { + setTimeout(() => + { document.getElementsByClassName("top_part")[0].classList.add("settings_shown"); this.mainDiv.classList.add("settings_menu_show"); }, ANIMATION_REFLOW_TIME); @@ -291,31 +302,34 @@ class SpessaSynthSettings { document.getElementsByClassName("top_part")[0].classList.remove("settings_shown"); this.mainDiv.classList.remove("settings_menu_show"); - this.animationId = setTimeout(() => { + this.animationId = setTimeout(() => + { this.mainDiv.style.display = "none"; }, TRANSITION_TIME * 1000); } this.visible = visible; } - + createHandlers(renderer, midiKeyboard, midiDeviceHandler, sequi, sythui) { // create handlers for all settings this._createRendererHandler(renderer); - + this._createMidiSettingsHandler( midiDeviceHandler, sequi, - sythui); - - this._createKeyboardHandler(midiKeyboard, + sythui + ); + + this._createKeyboardHandler( + midiKeyboard, sythui, renderer ); - + this._createInterfaceSettingsHandler(); } - + getHtmlControls() { // get the html controllers @@ -323,36 +337,37 @@ class SpessaSynthSettings renderer: { noteTimeSlider: document.getElementById("note_time_slider"), analyserToggler: document.getElementById("analyser_toggler"), - noteToggler: document.getElementById("note_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"), + waveMultiplierSlizer: document.getElementById("wave_multiplier_slider") }, - + keyboard: { channelSelector: document.getElementById("channel_selector"), modeSelector: document.getElementById("mode_selector"), sizeSelector: document.getElementById("keyboard_size_selector"), showSelector: document.getElementById("keyboard_show") }, - + 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"), layoutSelector: document.getElementById("layout_selector") } - } + }; } } + SpessaSynthSettings.prototype._toggleDarkMode = _toggleDarkMode; SpessaSynthSettings.prototype._createInterfaceSettingsHandler = _createInterfaceSettingsHandler; SpessaSynthSettings.prototype._changeLayout = _changeLayout; @@ -367,4 +382,4 @@ SpessaSynthSettings.prototype._loadSettings = _loadSettings; SpessaSynthSettings.prototype._serializeSettings = _serializeSettings; SpessaSynthSettings.prototype._saveSettings = _saveSettings; -export { SpessaSynthSettings } \ No newline at end of file +export { SpessaSynthSettings }; \ 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 74d23b89..2dfa79c5 100644 --- a/src/website/js/settings_ui/settings_html.js +++ b/src/website/js/settings_ui/settings_html.js @@ -2,7 +2,7 @@ * settings_html.js * purpose: the inner html for the settings element */ -import { USE_MIDI_RANGE } from './handlers/keyboard_handler.js' +import { USE_MIDI_RANGE } from "./handlers/keyboard_handler.js"; // translate-path: only innerText: translate-path-title: inner text by adding .title and title by adding .description export const settingsHtml = ` diff --git a/src/website/js/settings_ui/sliders.js b/src/website/js/settings_ui/sliders.js index afde32ca..48a91ae8 100644 --- a/src/website/js/settings_ui/sliders.js +++ b/src/website/js/settings_ui/sliders.js @@ -16,11 +16,11 @@ export function handleSliders(div) * @type {HTMLCollectionOf} */ const inputs = div.getElementsByTagName("spessarange"); - for(const input of inputs) + for (const input of inputs) { input.parentElement.insertBefore(createSlider(input, true), input); } - while(inputs.length > 0) + while (inputs.length > 0) { inputs[0].parentNode.removeChild(inputs[0]); } @@ -32,7 +32,7 @@ export function createSlider(input, showSpan = true) const mainWrapper = document.createElement("div"); mainWrapper.classList.add("settings_slider_wrapper"); // copy over values to the actual input - const min = input.getAttribute( "min"); + const min = input.getAttribute("min"); const max = input.getAttribute("max"); const current = input.getAttribute("value"); const units = input.getAttribute("units"); @@ -44,33 +44,34 @@ export function createSlider(input, showSpan = true) htmlInput.min = min; htmlInput.max = max; htmlInput.value = current; - - - let span - if(showSpan) + + + let span; + if (showSpan) { span = document.createElement("span"); span.textContent = current + units; } - + // visual wrapper wraps the input, thumb and progress const visualWrapper = document.createElement("div"); visualWrapper.classList.add("settings_visual_wrapper"); - + const progressBar = document.createElement("div"); progressBar.classList.add("settings_slider_progress"); visualWrapper.appendChild(progressBar); - + const thumb = document.createElement("div"); thumb.classList.add("settings_slider_thumb"); visualWrapper.appendChild(thumb); visualWrapper.appendChild(htmlInput); - - htmlInput.addEventListener("input", () => { + + htmlInput.addEventListener("input", () => + { // calculate the difference between values, if larger than 5%, enable transition const val = parseInt(visualWrapper.style.getPropertyValue("--visual-width").replace("%", "")); const newVal = Math.round((htmlInput.value - htmlInput.min) / (htmlInput.max - htmlInput.min) * 100); - if(Math.abs((val - newVal) / 100) > 0.05) + if (Math.abs((val - newVal) / 100) > 0.05) { visualWrapper.classList.add("settings_slider_transition"); } @@ -81,9 +82,12 @@ export function createSlider(input, showSpan = true) // apply the width visualWrapper.style.setProperty("--visual-width", `${newVal}%`); }); - visualWrapper.style.setProperty("--visual-width", `${(htmlInput.value - htmlInput.min) / (htmlInput.max - htmlInput.min) * 100}%`); + visualWrapper.style.setProperty( + "--visual-width", + `${(htmlInput.value - htmlInput.min) / (htmlInput.max - htmlInput.min) * 100}%` + ); mainWrapper.appendChild(visualWrapper); - if(showSpan) + if (showSpan) { mainWrapper.appendChild(span); } diff --git a/src/website/js/synthesizer_ui/methods/create_channel_controller.js b/src/website/js/synthesizer_ui/methods/create_channel_controller.js index 7de65fe9..00aa489b 100644 --- a/src/website/js/synthesizer_ui/methods/create_channel_controller.js +++ b/src/website/js/synthesizer_ui/methods/create_channel_controller.js @@ -18,25 +18,18 @@ * }} ChannelController */ -import { Meter } from './synthui_meter.js' -import { LOCALE_PATH } from '../synthetizer_ui.js' -import { midiControllers } from '../../../../spessasynth_lib/midi_parser/midi_message.js' -import { - getDrumsSvg, - getEmptyMicSvg, - getMicSvg, - getMuteSvg, - getNoteSvg, - getVolumeSvg, -} from '../../utils/icons.js' -import { DEFAULT_PERCUSSION } from '../../../../spessasynth_lib/synthetizer/synthetizer.js' -import { Selector } from './synthui_selector.js' +import { Meter } from "./synthui_meter.js"; +import { LOCALE_PATH } from "../synthetizer_ui.js"; +import { midiControllers } from "../../../../spessasynth_lib/midi_parser/midi_message.js"; +import { getDrumsSvg, getEmptyMicSvg, getMicSvg, getMuteSvg, getNoteSvg, getVolumeSvg } from "../../utils/icons.js"; +import { DEFAULT_PERCUSSION } from "../../../../spessasynth_lib/synthetizer/synthetizer.js"; +import { Selector } from "./synthui_selector.js"; import { ALL_CHANNELS_OR_DIFFERENT_ACTION -} from '../../../../spessasynth_lib/synthetizer/worklet_system/message_protocol/worklet_message.js' +} from "../../../../spessasynth_lib/synthetizer/worklet_system/message_protocol/worklet_message.js"; import { NON_CC_INDEX_OFFSET -} from '../../../../spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js' +} from "../../../../spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js"; import { modulatorSources } from "../../../../spessasynth_lib/soundfont/basic_soundfont/modulator.js"; @@ -57,53 +50,75 @@ export function createChannelController(channelNumber) // controller const controller = document.createElement("div"); controller.classList.add("channel_controller"); - + // voice meter - const voiceMeter = new Meter(this.channelColors[channelNumber % this.channelColors.length], + const voiceMeter = new Meter( + this.channelColors[channelNumber % this.channelColors.length], LOCALE_PATH + "channelController.voiceMeter", this.locale, [channelNumber + 1], 0, - 100); + 100 + ); voiceMeter.bar.classList.add("voice_meter_bar_smooth"); controller.appendChild(voiceMeter.div); - + // pitch wheel - const pitchWheel = new Meter(this.channelColors[channelNumber % this.channelColors.length], + const pitchWheel = new Meter( + this.channelColors[channelNumber % this.channelColors.length], LOCALE_PATH + "channelController.pitchBendMeter", this.locale, [channelNumber + 1], -8192, 8191, true, - val => { + val => + { const meterLocked = pitchWheel.isLocked; - if(meterLocked) + if (meterLocked) { - this.synth.lockController(channelNumber, NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel, false); + this.synth.lockController( + channelNumber, + NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel, + false + ); } val = Math.round(val) + 8192; // get bend values const msb = val >> 7; const lsb = val & 0x7F; this.synth.pitchWheel(channelNumber, msb, lsb); - if(meterLocked) + if (meterLocked) { - this.synth.lockController(channelNumber, NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel, true); + this.synth.lockController( + channelNumber, + NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel, + true + ); } }, - () => this.synth.lockController(channelNumber, NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel, true), - () => this.synth.lockController(channelNumber, NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel, false)); + () => this.synth.lockController( + channelNumber, + NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel, + true + ), + () => this.synth.lockController( + channelNumber, + NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel, + false + ) + ); pitchWheel.update(0); controller.appendChild(pitchWheel.div); - + /** * @param cc {number} * @param val {number} * @param meter {Meter} */ - let changeCCUserFunction = (cc, val, meter) => { - if(meter.isLocked) + let changeCCUserFunction = (cc, val, meter) => + { + if (meter.isLocked) { this.synth.lockController(channelNumber, cc, false); this.synth.controllerChange(channelNumber, cc, val); @@ -113,15 +128,16 @@ export function createChannelController(channelNumber) { this.synth.controllerChange(channelNumber, cc, val); } - } - + }; + /** * @param ccNum {number} * @param localePath {string} * @param defaultValue {number} * @returns {Meter} */ - const createCCMeterHelper = (ccNum, localePath, defaultValue) => { + const createCCMeterHelper = (ccNum, localePath, defaultValue) => + { const meter = new Meter( this.channelColors[channelNumber % this.channelColors.length], LOCALE_PATH + localePath, @@ -136,59 +152,71 @@ export function createChannelController(channelNumber) ); meter.update(defaultValue); return meter; - } - + }; + // pan controller const pan = createCCMeterHelper(midiControllers.pan, "channelController.panMeter", 64); controller.appendChild(pan.div); - + // expression controller - const expression = createCCMeterHelper(midiControllers.expressionController, "channelController.expressionMeter", 127); + const expression = createCCMeterHelper( + midiControllers.expressionController, + "channelController.expressionMeter", + 127 + ); controller.appendChild(expression.div); - + // volume controller const volume = createCCMeterHelper(midiControllers.mainVolume, "channelController.volumeMeter", 100); controller.appendChild(volume.div); - + // modulation wheel - const modulation = createCCMeterHelper(midiControllers.modulationWheel, "channelController.modulationWheelMeter", 0); + const modulation = createCCMeterHelper( + midiControllers.modulationWheel, + "channelController.modulationWheelMeter", + 0 + ); controller.appendChild(modulation.div); - + // chorus const chorus = createCCMeterHelper(midiControllers.effects3Depth, "channelController.chorusMeter", 0); controller.appendChild(chorus.div); - + // reverb const reverb = createCCMeterHelper(midiControllers.effects1Depth, "channelController.reverbMeter", 0); controller.appendChild(reverb.div); - + // brightness const brightness = createCCMeterHelper(midiControllers.brightness, "channelController.filterMeter", 64); controller.appendChild(brightness.div); - + // transpose is not a cc, add it manually - const transpose = new Meter(this.channelColors[channelNumber % this.channelColors.length], + const transpose = new Meter( + this.channelColors[channelNumber % this.channelColors.length], LOCALE_PATH + "channelController.transposeMeter", this.locale, [channelNumber + 1], -36, 36, true, - val => { + val => + { val = Math.round(val); this.synth.transposeChannel(channelNumber, val, true); transpose.update(val); - }); + } + ); transpose.update(0); controller.appendChild(transpose.div); - + // preset controller const presetSelector = new Selector( ([]), // empty for now this.locale, LOCALE_PATH + "channelController.presetSelector", [channelNumber + 1], - async presetName => { + async presetName => + { const data = presetName.split(":"); this.synth.lockController(channelNumber, ALL_CHANNELS_OR_DIFFERENT_ACTION, false); this.synth.controllerChange(channelNumber, midiControllers.bankSelect, parseInt(data[0]), true); @@ -199,16 +227,22 @@ export function createChannelController(channelNumber) locked => this.synth.lockController(channelNumber, ALL_CHANNELS_OR_DIFFERENT_ACTION, locked) ); controller.appendChild(presetSelector.mainButton); - + // solo button const soloButton = document.createElement("div"); soloButton.innerHTML = getEmptyMicSvg(ICON_SIZE); - this.locale.bindObjectProperty(soloButton, "title", LOCALE_PATH + "channelController.soloButton.description", [channelNumber + 1]); + this.locale.bindObjectProperty( + soloButton, + "title", + LOCALE_PATH + "channelController.soloButton.description", + [channelNumber + 1] + ); soloButton.classList.add("controller_element"); soloButton.classList.add("mute_button"); - soloButton.onclick = () => { + soloButton.onclick = () => + { // toggle solo - if(this.soloChannels.has(channelNumber)) + if (this.soloChannels.has(channelNumber)) { this.soloChannels.delete(channelNumber); } @@ -216,7 +250,7 @@ export function createChannelController(channelNumber) { this.soloChannels.add(channelNumber); } - if(this.soloChannels.size === 0 || this.soloChannels.size >= this.synth.channelsAmount) + if (this.soloChannels.size === 0 || this.soloChannels.size >= this.synth.channelsAmount) { // no channels or all channels are soloed, unmute everything for (let i = 0; i < this.synth.channelsAmount; i++) @@ -224,7 +258,7 @@ export function createChannelController(channelNumber) this.controllers[i].soloButton.innerHTML = getEmptyMicSvg(ICON_SIZE); this.synth.muteChannel(i, this.controllers[i].muteButton.hasAttribute("is_muted")); } - if(this.soloChannels.size >= this.synth.channelsAmount) + if (this.soloChannels.size >= this.synth.channelsAmount) { // all channels are soloed, return to normal this.soloChannels.clear(); @@ -234,7 +268,7 @@ export function createChannelController(channelNumber) // unmute every solo channel and mute others for (let i = 0; i < this.synth.channelsAmount; i++) { - if(this.soloChannels.has(i)) + if (this.soloChannels.has(i)) { this.controllers[i].soloButton.innerHTML = getMicSvg(ICON_SIZE); this.synth.muteChannel(i, this.controllers[i].muteButton.hasAttribute("is_muted")); @@ -245,24 +279,30 @@ export function createChannelController(channelNumber) this.synth.muteChannel(i, true); } } - } + }; controller.appendChild(soloButton); - + // mute button const muteButton = document.createElement("div"); muteButton.innerHTML = getVolumeSvg(ICON_SIZE); - this.locale.bindObjectProperty(muteButton, "title", LOCALE_PATH + "channelController.muteButton.description", [channelNumber + 1]); + this.locale.bindObjectProperty( + muteButton, + "title", + LOCALE_PATH + "channelController.muteButton.description", + [channelNumber + 1] + ); muteButton.classList.add("controller_element"); muteButton.classList.add("mute_button"); - muteButton.onclick = () => { - if(muteButton.hasAttribute("is_muted")) + muteButton.onclick = () => + { + if (muteButton.hasAttribute("is_muted")) { // unmute muteButton.removeAttribute("is_muted"); const canBeUnmuted = this.soloChannels.size === 0 || this.soloChannels.has(channelNumber); this.synth.muteChannel(channelNumber, !canBeUnmuted); muteButton.innerHTML = getVolumeSvg(ICON_SIZE); - + } else { @@ -271,21 +311,27 @@ export function createChannelController(channelNumber) muteButton.setAttribute("is_muted", "true"); muteButton.innerHTML = getMuteSvg(ICON_SIZE); } - } - + }; + controller.appendChild(muteButton); - + // drums toggle const drumsToggle = document.createElement("div"); drumsToggle.innerHTML = channelNumber === DEFAULT_PERCUSSION ? getDrumsSvg(ICON_SIZE) : getNoteSvg(ICON_SIZE); - this.locale.bindObjectProperty(drumsToggle, "title", LOCALE_PATH + "channelController.drumToggleButton.description", [channelNumber + 1]); + this.locale.bindObjectProperty( + drumsToggle, + "title", + LOCALE_PATH + "channelController.drumToggleButton.description", + [channelNumber + 1] + ); drumsToggle.classList.add("controller_element"); drumsToggle.classList.add("mute_button"); - drumsToggle.onclick = () => { + drumsToggle.onclick = () => + { this.synth.setDrums(channelNumber, !this.synth.channelProperties[channelNumber].isDrum); - } + }; controller.appendChild(drumsToggle); - + return { controller: controller, voiceMeter: voiceMeter, @@ -303,7 +349,7 @@ export function createChannelController(channelNumber) muteButton: muteButton, transpose: transpose }; - + } /** @@ -312,7 +358,7 @@ export function createChannelController(channelNumber) export function createChannelControllers() { const dropdownDiv = this.uiDiv.getElementsByClassName("synthui_controller")[0]; - + /** * @type {ChannelController[]} */ @@ -323,9 +369,9 @@ export function createChannelControllers() this.controllers.push(controller); dropdownDiv.appendChild(controller.controller); } - + this.setEventListeners(); - + setInterval(this.updateVoicesAmount.bind(this), 100); this.hideControllers(); } \ No newline at end of file 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 3c27c0af..50189ff2 100644 --- a/src/website/js/synthesizer_ui/methods/create_main_controller.js +++ b/src/website/js/synthesizer_ui/methods/create_main_controller.js @@ -1,11 +1,11 @@ -import { Meter } from './synthui_meter.js' -import { VOICE_CAP } from '../../../../spessasynth_lib/synthetizer/synthetizer.js' -import { LOCALE_PATH } from '../synthetizer_ui.js' +import { Meter } from "./synthui_meter.js"; +import { VOICE_CAP } from "../../../../spessasynth_lib/synthetizer/synthetizer.js"; +import { LOCALE_PATH } from "../synthetizer_ui.js"; import { ALL_CHANNELS_OR_DIFFERENT_ACTION -} from '../../../../spessasynth_lib/synthetizer/worklet_system/message_protocol/worklet_message.js' -import { getEmptyMicSvg, getVolumeSvg } from '../../utils/icons.js' -import { ICON_SIZE } from './create_channel_controller.js' +} from "../../../../spessasynth_lib/synthetizer/worklet_system/message_protocol/worklet_message.js"; +import { getEmptyMicSvg, getVolumeSvg } from "../../utils/icons.js"; +import { ICON_SIZE } from "./create_channel_controller.js"; /** * @this {SynthetizerUI} @@ -15,110 +15,146 @@ export function createMainSynthController() // controls wrapper let controlsWrapper = document.createElement("div"); controlsWrapper.classList.add("controls_wrapper"); - + /** * Voice meter * @type {Meter} */ - this.voiceMeter = new Meter("", + this.voiceMeter = new Meter( + "", LOCALE_PATH + "mainVoiceMeter", this.locale, [], 0, - VOICE_CAP); + VOICE_CAP + ); this.voiceMeter.bar.classList.add("voice_meter_bar_smooth"); this.voiceMeter.div.classList.add("main_controller_element"); - + /** * Volume controller * @type {Meter} */ - this.volumeController = new Meter("", + this.volumeController = new Meter( + "", LOCALE_PATH + "mainVolumeMeter", this.locale, [], 0, 200, true, - v => { + v => + { this.synth.setMainVolume(Math.round(v) / 100); this.volumeController.update(v); - }); + } + ); this.volumeController.bar.classList.add("voice_meter_bar_smooth"); this.volumeController.div.classList.add("main_controller_element"); this.volumeController.update(100); - + /** * Pan controller * @type {Meter} */ - this.panController = new Meter("", + this.panController = new Meter( + "", LOCALE_PATH + "mainPanMeter", this.locale, [], -1, 1, true, - v => { + v => + { // use roland gs master pan this.synth.setMasterPan(v); this.panController.update(v); - }); + } + ); this.panController.bar.classList.add("voice_meter_bar_smooth"); this.panController.div.classList.add("main_controller_element"); this.panController.update(0); - + /** * Transpose controller * @type {Meter} */ - this.transposeController = new Meter("", + this.transposeController = new Meter( + "", LOCALE_PATH + "mainTransposeMeter", this.locale, [], -12, 12, true, - v => { + v => + { // limit to half semitone precision - this.synth.transpose(Math.round(v * 2 ) / 2); - this.transposeController.update(Math.round(v * 2) / 2) - }); + this.synth.transpose(Math.round(v * 2) / 2); + this.transposeController.update(Math.round(v * 2) / 2); + } + ); this.transposeController.bar.classList.add("voice_meter_bar_smooth"); this.transposeController.div.classList.add("main_controller_element"); this.transposeController.update(0); - + // note killer let midiPanicButton = document.createElement("button"); this.locale.bindObjectProperty(midiPanicButton, "textContent", LOCALE_PATH + "midiPanic.title"); this.locale.bindObjectProperty(midiPanicButton, "title", LOCALE_PATH + "midiPanic.description"); - + midiPanicButton.classList.add("synthui_button"); midiPanicButton.classList.add("main_controller_element"); midiPanicButton.onclick = () => this.synth.stopAll(true); - + // system reset button let resetCCButton = document.createElement("button"); this.locale.bindObjectProperty(resetCCButton, "textContent", LOCALE_PATH + "systemReset.title"); this.locale.bindObjectProperty(resetCCButton, "title", LOCALE_PATH + "systemReset.description"); - + resetCCButton.classList.add("synthui_button"); resetCCButton.classList.add("main_controller_element"); - resetCCButton.onclick = () => { + resetCCButton.onclick = () => + { // unlock everything this.controllers.forEach((channel, number) => { // CCs - if(channel.pitchWheel.isLocked) channel.pitchWheel.lockMeter(); - if(channel.pan.isLocked) channel.pan.lockMeter(); - if(channel.expression.isLocked) channel.expression.lockMeter(); - if(channel.volume.isLocked) channel.volume.lockMeter(); - if(channel.mod.isLocked) channel.mod.lockMeter(); - if(channel.chorus.isLocked) channel.chorus.lockMeter(); - if(channel.reverb.isLocked) channel.reverb.lockMeter(); - if(channel.brightness.isLocked) channel.brightness.lockMeter(); + if (channel.pitchWheel.isLocked) + { + channel.pitchWheel.lockMeter(); + } + if (channel.pan.isLocked) + { + channel.pan.lockMeter(); + } + if (channel.expression.isLocked) + { + channel.expression.lockMeter(); + } + if (channel.volume.isLocked) + { + channel.volume.lockMeter(); + } + if (channel.mod.isLocked) + { + channel.mod.lockMeter(); + } + if (channel.chorus.isLocked) + { + channel.chorus.lockMeter(); + } + if (channel.reverb.isLocked) + { + channel.reverb.lockMeter(); + } + if (channel.brightness.isLocked) + { + channel.brightness.lockMeter(); + } // program - if(channel.preset.mainButton.classList.contains("locked_selector")) + if (channel.preset.mainButton.classList.contains("locked_selector")) { this.synth.lockController(number, ALL_CHANNELS_OR_DIFFERENT_ACTION, false); channel.preset.mainButton.classList.remove("locked_selector"); @@ -126,41 +162,42 @@ export function createMainSynthController() // transpose this.synth.transposeChannel(number, 0, true); channel.transpose.update(0); - + // mute/solo channel.soloButton.innerHTML = getEmptyMicSvg(ICON_SIZE); channel.muteButton.innerHTML = getVolumeSvg(ICON_SIZE); this.synth.muteChannel(number, false); - + }); this.synth.resetControllers(); }; - - - + + // black midi mode toggle const highPerfToggle = document.createElement("button"); this.locale.bindObjectProperty(highPerfToggle, "textContent", LOCALE_PATH + "blackMidiMode.title"); this.locale.bindObjectProperty(highPerfToggle, "title", LOCALE_PATH + "blackMidiMode.description"); - + highPerfToggle.classList.add("synthui_button"); highPerfToggle.classList.add("main_controller_element"); - highPerfToggle.onclick = () => { + highPerfToggle.onclick = () => + { this.synth.highPerformanceMode = !this.synth.highPerformanceMode; - } - + }; + // vibrato reset const vibratoReset = document.createElement("button"); this.locale.bindObjectProperty(vibratoReset, "textContent", LOCALE_PATH + "disableCustomVibrato.title"); this.locale.bindObjectProperty(vibratoReset, "title", LOCALE_PATH + "disableCustomVibrato.description"); - + vibratoReset.classList.add("synthui_button"); vibratoReset.classList.add("main_controller_element"); - vibratoReset.onclick = () => { + vibratoReset.onclick = () => + { this.synth.disableGSNRPparams(); vibratoReset.parentNode.removeChild(vibratoReset); - } - + }; + // help button const helpButton = document.createElement("a"); helpButton.href = "https://github.com/spessasus/SpessaSynth/wiki/How-To-Use-App#synthesizer-controller"; @@ -169,7 +206,7 @@ export function createMainSynthController() helpButton.classList.add("synthui_button"); this.locale.bindObjectProperty(helpButton, "textContent", LOCALE_PATH + "helpButton.title"); this.locale.bindObjectProperty(helpButton, "title", LOCALE_PATH + "helpButton.description"); - + /** * interpolation type * @type {HTMLSelectElement} @@ -178,7 +215,7 @@ export function createMainSynthController() interpolation.classList.add("main_controller_element"); interpolation.classList.add("synthui_button"); this.locale.bindObjectProperty(interpolation, "title", LOCALE_PATH + "interpolation.description"); - + // interpolation types { /** @@ -189,7 +226,7 @@ export function createMainSynthController() linear.value = "0"; this.locale.bindObjectProperty(linear, "textContent", LOCALE_PATH + "interpolation.linear"); interpolation.appendChild(linear); - + /** * nearest neighbor * @type {HTMLOptionElement} @@ -198,7 +235,7 @@ export function createMainSynthController() nearest.value = "1"; this.locale.bindObjectProperty(nearest, "textContent", LOCALE_PATH + "interpolation.nearestNeighbor"); interpolation.appendChild(nearest); - + /** * cubic (default) * @type {HTMLOptionElement} @@ -208,27 +245,29 @@ export function createMainSynthController() cubic.selected = true; this.locale.bindObjectProperty(cubic, "textContent", LOCALE_PATH + "interpolation.cubic"); interpolation.appendChild(cubic); - - interpolation.onchange = () => { + + interpolation.onchange = () => + { this.synth.setInterpolationType(parseInt(interpolation.value)); - } + }; } - + // main controller let controller = document.createElement("div"); controller.classList.add("synthui_controller"); this.uiDiv.appendChild(controller); - + // channel controller shower let showControllerButton = document.createElement("button"); this.locale.bindObjectProperty(showControllerButton, "textContent", LOCALE_PATH + "toggleButton.title"); this.locale.bindObjectProperty(showControllerButton, "title", LOCALE_PATH + "toggleButton.description"); showControllerButton.classList.add("synthui_button"); - showControllerButton.onclick = () => { + showControllerButton.onclick = () => + { this.hideOnDocClick = false; this.toggleVisibility(); - } - + }; + // meters controlsWrapper.appendChild(this.volumeController.div); controlsWrapper.appendChild(this.panController.div); @@ -240,7 +279,7 @@ export function createMainSynthController() controlsWrapper.appendChild(vibratoReset); controlsWrapper.appendChild(helpButton); controlsWrapper.appendChild(interpolation); - + /** * @type {Meter[]} */ @@ -248,7 +287,7 @@ export function createMainSynthController() this.volumeController, this.panController, this.transposeController, - this.voiceMeter, + this.voiceMeter ]; /** * @type {HTMLElement[]} @@ -270,8 +309,9 @@ export function createMainSynthController() // stop propagation to not hide this.mainControllerDiv.onclick = e => e.stopPropagation(); // hide if clicked outside - document.addEventListener("click", () => { - if(!this.hideOnDocClick) + document.addEventListener("click", () => + { + if (!this.hideOnDocClick) { this.hideOnDocClick = true; return; @@ -279,5 +319,5 @@ export function createMainSynthController() controller.classList.remove("synthui_controller_show"); this.isShown = false; this.hideControllers(); - }) + }); } \ No newline at end of file diff --git a/src/website/js/synthesizer_ui/methods/hide_show_controllers.js b/src/website/js/synthesizer_ui/methods/hide_show_controllers.js index f07880e2..458d1d00 100644 --- a/src/website/js/synthesizer_ui/methods/hide_show_controllers.js +++ b/src/website/js/synthesizer_ui/methods/hide_show_controllers.js @@ -3,7 +3,8 @@ */ export function hideControllers() { - this.controllers.forEach(c => { + this.controllers.forEach(c => + { c.voiceMeter.hide(); c.pitchWheel.hide(); c.pan.hide(); @@ -14,7 +15,7 @@ export function hideControllers() c.reverb.hide(); c.brightness.hide(); c.preset.hide(); - }) + }); } /** @@ -22,7 +23,8 @@ export function hideControllers() */ export function showControllers() { - this.controllers.forEach(c => { + this.controllers.forEach(c => + { c.voiceMeter.show(); c.pitchWheel.show(); c.pan.show(); @@ -33,5 +35,5 @@ export function showControllers() c.reverb.show(); c.brightness.show(); c.preset.show(); - }) + }); } \ No newline at end of file diff --git a/src/website/js/synthesizer_ui/methods/set_event_listeners.js b/src/website/js/synthesizer_ui/methods/set_event_listeners.js index f43680df..9306afd0 100644 --- a/src/website/js/synthesizer_ui/methods/set_event_listeners.js +++ b/src/website/js/synthesizer_ui/methods/set_event_listeners.js @@ -1,5 +1,5 @@ -import { midiControllers } from '../../../../spessasynth_lib/midi_parser/midi_message.js' -import { getDrumsSvg, getNoteSvg } from '../../utils/icons.js' +import { midiControllers } from "../../../../spessasynth_lib/midi_parser/midi_message.js"; +import { getDrumsSvg, getNoteSvg } from "../../utils/icons.js"; /** * @this {SynthetizerUI} @@ -12,9 +12,11 @@ export function setEventListeners() { this.controllers[e.channel].preset.set(`${e.bank}:${e.program}`); }); - - this.synth.eventHandler.addEvent("allcontrollerreset", "synthui-all-controller-reset", () => { - for (const controller of this.controllers) { + + this.synth.eventHandler.addEvent("allcontrollerreset", "synthui-all-controller-reset", () => + { + for (const controller of this.controllers) + { controller.pan.update(64); controller.mod.update(0); controller.chorus.update(0); @@ -25,8 +27,9 @@ export function setEventListeners() controller.brightness.update(64); } }); - - this.synth.eventHandler.addEvent("controllerchange", "synthui-controller-change",e => { + + this.synth.eventHandler.addEvent("controllerchange", "synthui-controller-change", e => + { const controller = e.controllerNumber; const channel = e.channel; const value = e.controllerValue; @@ -34,55 +37,58 @@ export function setEventListeners() { default: break; - + case midiControllers.expressionController: // expression this.controllers[channel].expression.update(value); break; - + case midiControllers.mainVolume: // volume this.controllers[channel].volume.update(value); break; - + case midiControllers.pan: // pan this.controllers[channel].pan.update(value); break; - + case midiControllers.modulationWheel: // mod wheel this.controllers[channel].mod.update(value); break; - + case midiControllers.effects3Depth: // chorus this.controllers[channel].chorus.update(value); break; - + case midiControllers.effects1Depth: // reverb this.controllers[channel].reverb.update(value); break; - + case midiControllers.brightness: // brightness this.controllers[channel].brightness.update(value); } }); - - this.synth.eventHandler.addEvent("pitchwheel", "synthui-pitch-wheel", e => { + + this.synth.eventHandler.addEvent("pitchwheel", "synthui-pitch-wheel", e => + { const val = (e.MSB << 7) | e.LSB; // pitch wheel this.controllers[e.channel].pitchWheel.update(val - 8192); }); - - this.synth.eventHandler.addEvent("drumchange", "synthui-drum-change", e => { + + this.synth.eventHandler.addEvent("drumchange", "synthui-drum-change", e => + { this.controllers[e.channel].drumsToggle.innerHTML = (e.isDrumChannel ? getDrumsSvg(32) : getNoteSvg(32)); this.controllers[e.channel].preset.reload(e.isDrumChannel ? this.percussionList : this.instrumentList); }); - - this.synth.eventHandler.addEvent("newchannel", "synthui-new-channel", () => { + + this.synth.eventHandler.addEvent("newchannel", "synthui-new-channel", () => + { const controller = this.createChannelController(this.controllers.length); this.controllers.push(controller); dropdownDiv.appendChild(controller.controller); diff --git a/src/website/js/synthesizer_ui/methods/synthui_meter.js b/src/website/js/synthesizer_ui/methods/synthui_meter.js index 1fbecf85..60f346d8 100644 --- a/src/website/js/synthesizer_ui/methods/synthui_meter.js +++ b/src/website/js/synthesizer_ui/methods/synthui_meter.js @@ -28,7 +28,7 @@ export class Meter localeArgs, min = 0, max = 100, - editable=false, + editable = false, editCallback = undefined, lockCallback = undefined, unlockCallback = undefined) @@ -43,19 +43,19 @@ export class Meter this.isLocked = false; this.lockCallback = lockCallback; this.unlockCallback = unlockCallback; - + /** * @type {HTMLDivElement} */ this.div = document.createElement("div"); this.div.classList.add("voice_meter"); this.div.classList.add("controller_element"); - if(color !== "none" && color !== "") + if (color !== "none" && color !== "") { this.div.style.borderColor = color; } - locale.bindObjectProperty(this.div, "title", localePath + ".description", localeArgs); - + locale.bindObjectProperty(this.div, "title", localePath + ".description", localeArgs); + /** * @type {HTMLDivElement} */ @@ -63,24 +63,27 @@ export class Meter this.bar.classList.add("voice_meter_bar"); this.bar.style.background = color; this.div.appendChild(this.bar); - + /** * @type {HTMLParagraphElement} */ this.text = document.createElement("p"); this.text.classList.add("voice_meter_text"); this.div.appendChild(this.text); - + this.isActive = false; - - if(editable) + + if (editable) { - if(editCallback === undefined) { + if (editCallback === undefined) + { throw new Error("No editable function given!"); } - this.div.onmousedown = e => { + this.div.onmousedown = e => + { e.preventDefault(); - if(e.button === 0) { + if (e.button === 0) + { // left mouse button: adjust value this.isActive = true; } @@ -89,9 +92,10 @@ export class Meter // other, lock it this.lockMeter(); } - } - this.div.onmousemove = e => { - if(!this.isActive) + }; + this.div.onmousemove = e => + { + if (!this.isActive) { return; } @@ -99,76 +103,82 @@ export class Meter const relativeLeft = bounds.left; const width = bounds.width; const relative = e.clientX - relativeLeft; - const percentage = Math.max(0, Math.min(1, relative / width)); + const percentage = Math.max(0, Math.min(1, relative / width)); editCallback(percentage * (max - min) + min); }; this.div.onmouseup = () => this.isActive = false; - this.div.onmouseleave = e => { + this.div.onmouseleave = e => + { this.div.onmousemove(e); this.isActive = false; - } - + }; + // QoL - this.text.oncontextmenu = e => { + this.text.oncontextmenu = e => + { e.preventDefault(); }; - + // add mobile - this.div.onclick = e => { + this.div.onclick = e => + { e.preventDefault(); this.isActive = true; this.div.onmousemove(e); this.isActive = false; - } + }; this.div.classList.add("editable"); } } - + lockMeter() { - if(this.lockCallback === undefined) + if (this.lockCallback === undefined) { // no callback, it can't be locked return; } - if(this.isLocked) { + if (this.isLocked) + { this.text.classList.remove("locked_meter"); this.unlockCallback(); } - else { + else + { this.text.classList.add("locked_meter"); this.lockCallback(); } this.isLocked = !this.isLocked; } - - toggleMode(updateColor=false) + + toggleMode(updateColor = false) { - if(updateColor) + if (updateColor) { this.bar.classList.toggle("voice_meter_light_color"); this.div.classList.toggle("voice_meter_light_color"); } this.text.classList.toggle("voice_meter_text_light"); } - + show() { this.isShown = true; - if(!this.isVisualValueSet) { + if (!this.isVisualValueSet) + { const percentage = Math.max(0, Math.min((this.currentValue - this.min) / (this.max - this.min), 1)); this.bar.style.width = `${percentage * 100}%`; this.text.textContent = this.meterText + (Math.round(this.currentValue * 100) / 100).toString(); this.isVisualValueSet = true; } } - + hide() { this.isShown = false; } - - + + /** * Updates a given meter to a given value * @param value {number} @@ -176,12 +186,13 @@ export class Meter */ update(value, force = false) { - if(value === this.currentValue && force === false) + if (value === this.currentValue && force === false) { return; } this.currentValue = value; - if(this.isShown) { + if (this.isShown) + { const percentage = Math.max(0, Math.min((value - this.min) / (this.max - this.min), 1)); this.bar.style.width = `${percentage * 100}%`; this.text.textContent = this.meterText + (Math.round(value * 100) / 100).toString(); diff --git a/src/website/js/synthesizer_ui/methods/synthui_selector.js b/src/website/js/synthesizer_ui/methods/synthui_selector.js index 8afdeaf0..a9f7ef38 100644 --- a/src/website/js/synthesizer_ui/methods/synthui_selector.js +++ b/src/website/js/synthesizer_ui/methods/synthui_selector.js @@ -1,7 +1,7 @@ -import { midiPatchNames } from '../../utils/patch_names.js' -import { getLockSVG, getUnlockSVG } from '../../utils/icons.js' -import { LOCALE_PATH } from '../synthetizer_ui.js' -import { ICON_SIZE } from './create_channel_controller.js' +import { midiPatchNames } from "../../utils/patch_names.js"; +import { getLockSVG, getUnlockSVG } from "../../utils/icons.js"; +import { LOCALE_PATH } from "../synthetizer_ui.js"; +import { ICON_SIZE } from "./create_channel_controller.js"; /** * syntui_selector.js @@ -31,22 +31,28 @@ export class Selector /** * @type {{name: string, program: number, bank: number, stringified: string}[]} */ - this.elements = elements.map(e => { + this.elements = elements.map(e => + { return { name: e.name, program: e.program, bank: e.bank, - stringified: `${e.bank.toString().padStart(3, "0")}:${e.program.toString().padStart(3, "0")} ${e.name}` - } + stringified: `${e.bank.toString().padStart(3, "0")}:${e.program.toString() + .padStart( + 3, + "0" + )} ${e.name}` + }; }); - if(this.elements.length > 0) { + if (this.elements.length > 0) + { this.value = `${this.elements[0].bank}:${this.elements[0].program}`; } else { this.value = ""; } - + /** * @type {HTMLSelectElement} */ @@ -57,21 +63,22 @@ export class Selector this.locale = locale; this.localePath = descriptionPath; this.localeArgs = descriptionArgs; - + this.reload(); - - this.mainButton.onclick = () => { + + this.mainButton.onclick = () => + { this.showSelectionMenu(); - } - + }; + this.editCallback = editCallback; - + this.selectionMenu = undefined; this.lockCallback = lockCallback; this.locked = false; this.isWindowShown = false; } - + showSelectionMenu() { /** @@ -83,42 +90,49 @@ export class Selector document.getElementsByClassName("spessasynth_main")[0].appendChild(this.selectionMenu); const selectionWindow = document.createElement("div"); selectionWindow.classList.add("voice_selector_window"); - + // add title - const selectionTitle = document.createElement('h2'); + const selectionTitle = document.createElement("h2"); this.locale.bindObjectProperty( selectionTitle, "textContent", this.localePath + ".selectionPrompt", - this.localeArgs); + this.localeArgs + ); selectionWindow.appendChild(selectionTitle); - + // add search wrapper const searchWrapper = document.createElement("div"); searchWrapper.classList.add("voice_selector_search_wrapper"); selectionWindow.appendChild(searchWrapper); - + // search input const searchInput = document.createElement("input"); searchInput.type = "text"; this.locale.bindObjectProperty(searchInput, "placeholder", this.localePath + ".searchPrompt"); searchWrapper.appendChild(searchInput); searchInput.onkeydown = e => e.stopPropagation(); - + // preset lock button const presetLock = document.createElement("div"); presetLock.innerHTML = this.locked ? getLockSVG(ICON_SIZE) : getUnlockSVG(ICON_SIZE); - this.locale.bindObjectProperty(presetLock, "title", LOCALE_PATH + "channelController.presetReset.description", this.localeArgs); + this.locale.bindObjectProperty( + presetLock, + "title", + LOCALE_PATH + "channelController.presetReset.description", + this.localeArgs + ); presetLock.classList.add("voice_reset"); - if(this.mainButton.classList.contains("voice_selector_light")) + if (this.mainButton.classList.contains("voice_selector_light")) { presetLock.classList.add("voice_reset_light"); } - presetLock.onclick = () => { + presetLock.onclick = () => + { this.locked = !this.locked; this.lockCallback(this.locked); this.mainButton.classList.toggle("locked_selector"); - if(this.locked) + if (this.locked) { presetLock.innerHTML = getLockSVG(ICON_SIZE); } @@ -126,40 +140,46 @@ export class Selector { presetLock.innerHTML = getUnlockSVG(ICON_SIZE); } - } + }; searchWrapper.appendChild(presetLock); this.presetLock = presetLock; - + // add the table, wrapper first const tableWrapper = document.createElement("div"); tableWrapper.classList.add("voice_selector_table_wrapper"); selectionWindow.appendChild(tableWrapper); - + // add the table this.generateTable(tableWrapper, this.elements); - + // add search function - searchInput.oninput = e => { + searchInput.oninput = e => + { e.stopPropagation(); const text = searchInput.value; const filtered = this.elements.filter(e => e.stringified.search(new RegExp(text, "i")) >= 0); - if(filtered.length === this.elements.length) return; + if (filtered.length === this.elements.length) + { + return; + } tableWrapper.replaceChildren(); this.generateTable(tableWrapper, filtered); - } - - - selectionWindow.onclick = e => { + }; + + + selectionWindow.onclick = e => + { e.stopPropagation(); - } + }; this.selectionMenu.appendChild(selectionWindow); - this.selectionMenu.onclick = e => { + this.selectionMenu.onclick = e => + { e.stopPropagation(); this.hideSelectionMenu(); - } + }; this.isWindowShown = true; } - + /** * Generates the instrument table for displaying * @param wrapper {Element} the wrapper @@ -169,31 +189,33 @@ export class Selector { const table = document.createElement("table"); table.classList.add("voice_selector_table"); - + const selectedBank = parseInt(this.value.split(":")[0]); const selectedProgram = parseInt(this.value.split(":")[1]); - + let lastProgram = -20; - for(const preset of elements) + for (const preset of elements) { const row = document.createElement("tr"); const program = preset.program; - - if(program === selectedProgram && preset.bank === selectedBank) + + if (program === selectedProgram && preset.bank === selectedBank) { row.classList.add("voice_selector_selected"); - setTimeout(() => { + setTimeout(() => + { row.scrollIntoView({ behavior: "instant", block: "center", inline: "center" - }) + }); }, 20); } - - row.onclick = () => { + + row.onclick = () => + { const newVal = `${preset.bank}:${program}`; - if(this.value === newVal) + if (this.value === newVal) { this.hideSelectionMenu(); return; @@ -202,14 +224,14 @@ export class Selector this.locked = true; this.presetLock.innerHTML = getLockSVG(ICON_SIZE); this.hideSelectionMenu(); - } - + }; + // create a new group - if(program !== lastProgram) + if (program !== lastProgram) { lastProgram = program; // create the header (not for drums - if(preset.bank !== 128) + if (preset.bank !== 128) { const headerRow = document.createElement("tr"); const header = document.createElement("th"); @@ -221,82 +243,92 @@ export class Selector } const programText = `${preset.program.toString().padStart(3, "0")}`; const bankText = `${preset.bank.toString().padStart(3, "0")}`; - + const presetName = document.createElement("td"); presetName.classList.add("voice_selector_preset_name"); presetName.textContent = preset.name; - + const presetProgram = document.createElement("td"); presetName.classList.add("voice_selector_preset_program"); presetProgram.textContent = programText; - + const presetBank = document.createElement("td"); presetName.classList.add("voice_selector_preset_program"); presetBank.textContent = bankText; - + row.appendChild(presetBank); row.appendChild(presetProgram); row.appendChild(presetName); table.appendChild(row); } wrapper.appendChild(table); - + } - + hideSelectionMenu() { document.getElementsByClassName("spessasynth_main")[0].removeChild(this.selectionMenu); this.selectionMenu = undefined; this.isWindowShown = false; } - + toggleMode() { this.mainButton.classList.toggle("voice_selector_light"); } - + /** * @param elements {{name: string, program: number, bank: number}[]} */ - reload(elements= this.elements) + reload(elements = this.elements) { - this.elements = elements.map(e => { + this.elements = elements.map(e => + { return { name: e.name, program: e.program, bank: e.bank, - stringified: `${e.bank.toString().padStart(3, "0")}:${e.program.toString().padStart(3, "0")} ${e.name}` - } + stringified: `${e.bank.toString().padStart(3, "0")}:${e.program.toString() + .padStart( + 3, + "0" + )} ${e.name}` + }; }); - if(!this.isShown) + if (!this.isShown) { this.isReloaded = false; return; } this.isReloaded = true; - if(this.elements.length > 0) - this.mainButton.textContent = this.getString(`${this.elements[0].bank}:${this.value.split(":")[1]}`); + if (this.elements.length > 0) + { + this.mainButton.textContent = this.getString(`${this.elements[0].bank}:${this.value.split(":")[1]}`); + } } - + /** * @param value {string} */ set(value) { this.value = value; - if(this.isShown) + if (this.isShown) { - if(!this.isReloaded) + if (!this.isReloaded) { this.reload(); } this.mainButton.textContent = this.getString(this.value); - - if(this.isWindowShown) + + if (this.isWindowShown) { // remove the old selected class const oldSelected = this.selectionMenu.getElementsByClassName("voice_selector_selected")[0]; - if(oldSelected !== undefined) oldSelected.classList.remove("voice_selector_selected"); + if (oldSelected !== undefined) + { + oldSelected.classList.remove("voice_selector_selected"); + } /** * @type {HTMLTableElement} */ @@ -304,12 +336,15 @@ export class Selector // find the new selected class const selectedBank = parseInt(this.value.split(":")[0]); const selectedProgram = parseInt(this.value.split(":")[1]); - for(const row of table.rows) + for (const row of table.rows) { - if(row.cells.length === 1) continue; + if (row.cells.length === 1) + { + continue; + } const bank = parseInt(row.cells[0].textContent); const program = parseInt(row.cells[1].textContent); - if(bank === selectedBank && program === selectedProgram) + if (bank === selectedBank && program === selectedProgram) { row.classList.add("voice_selector_selected"); row.scrollIntoView({ @@ -322,7 +357,7 @@ export class Selector } } } - + /** * @param inputString {string} * @returns {string} @@ -333,20 +368,23 @@ export class Selector const bank = parseInt(split[0]); const program = parseInt(split[1]); const name = this.elements.find(e => e.bank === bank && e.program === program); - if(bank === 128 || this.elements.filter(e => e.program === program && e.bank !== 128).length < 2) return `${program}. ${name.name}`; + if (bank === 128 || this.elements.filter(e => e.program === program && e.bank !== 128).length < 2) + { + return `${program}. ${name.name}`; + } return `${bank}:${program} ${name.name}`; } - + show() { this.isShown = true; - if(!this.isReloaded) + if (!this.isReloaded) { this.reload(); } this.mainButton.textContent = this.getString(this.value); } - + hide() { this.isShown = false; diff --git a/src/website/js/synthesizer_ui/methods/toggle_dark_mode.js b/src/website/js/synthesizer_ui/methods/toggle_dark_mode.js index ccb2cef4..548537ce 100644 --- a/src/website/js/synthesizer_ui/methods/toggle_dark_mode.js +++ b/src/website/js/synthesizer_ui/methods/toggle_dark_mode.js @@ -4,16 +4,19 @@ export function toggleDarkMode() { this.mainControllerDiv.classList.toggle("synthui_controller_light"); - this.mainButtons.forEach(b => { + this.mainButtons.forEach(b => + { b.classList.toggle("synthui_button"); b.classList.toggle("synthui_button_light"); - }) - - this.mainMeters.forEach(meter => { + }); + + this.mainMeters.forEach(meter => + { meter.toggleMode(true); }); - - this.controllers.forEach(controller => { + + this.controllers.forEach(controller => + { controller.voiceMeter.toggleMode(); controller.pitchWheel.toggleMode(); controller.pan.toggleMode(); @@ -26,5 +29,5 @@ export function toggleDarkMode() controller.preset.toggleMode(); controller.drumsToggle.classList.toggle("mute_button_light"); controller.muteButton.classList.toggle("mute_button_light"); - }) + }); } \ No newline at end of file diff --git a/src/website/js/synthesizer_ui/synthetizer_ui.js b/src/website/js/synthesizer_ui/synthetizer_ui.js index 20c36da6..85509ff9 100644 --- a/src/website/js/synthesizer_ui/synthetizer_ui.js +++ b/src/website/js/synthesizer_ui/synthetizer_ui.js @@ -1,16 +1,15 @@ -import { - Synthetizer, -} from '../../../spessasynth_lib/synthetizer/synthetizer.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 '../utils/keybinds.js' -import { ANIMATION_REFLOW_TIME } from '../utils/animation_utils.js' +import { Synthetizer } from "../../../spessasynth_lib/synthetizer/synthetizer.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 "../utils/keybinds.js"; +import { ANIMATION_REFLOW_TIME } from "../utils/animation_utils.js"; export const LOCALE_PATH = "locale.synthesizerController."; + /** * synthesizer_ui.js * purpose: manages the graphical user interface for the synthesizer @@ -24,7 +23,8 @@ class SynthetizerUI * @param element {HTMLElement} the element to create synthui in * @param localeManager {LocaleManager} */ - constructor(colors, element, localeManager) { + constructor(colors, element, localeManager) + { this.channelColors = colors; const wrapper = element; this.uiDiv = document.createElement("div"); @@ -36,7 +36,7 @@ class SynthetizerUI this.locale = localeManager; this.hideOnDocClick = true; } - + /** * Connects the synth to UI * @param synth {Synthetizer} @@ -44,49 +44,54 @@ class SynthetizerUI connectSynth(synth) { this.synth = synth; - + this.getInstrumentList(); - + this.createMainSynthController(); this.createChannelControllers(); - - document.addEventListener("keydown", e => { + + document.addEventListener("keydown", e => + { switch (e.key.toLowerCase()) { case keybinds.synthesizerUIShow: e.preventDefault(); this.toggleVisibility(); break; - + // case keybinds.settingsShow: this.isShown = true; this.toggleVisibility(); break; - + case keybinds.blackMidiMode: e.preventDefault(); this.synth.highPerformanceMode = !this.synth.highPerformanceMode; break; - + case keybinds.midiPanic: e.preventDefault(); this.synth.stopAll(true); break; } - }) - + }); + // add event listener for locale change - this.locale.onLocaleChanged.push(() => { + this.locale.onLocaleChanged.push(() => + { // reload all meters // global meters this.voiceMeter.update(this.voiceMeter.currentValue, true); this.volumeController.update(this.volumeController.currentValue, true); this.panController.update(this.panController.currentValue, true); this.panController.update(this.panController.currentValue, true); - this.transposeController.update(this.transposeController.currentValue, true); + this.transposeController.update( + this.transposeController.currentValue, + true + ); // channel controller meters - for(const controller of this.controllers) + for (const controller of this.controllers) { controller.voiceMeter.update(controller.voiceMeter.currentValue, true); controller.pitchWheel.update(controller.pitchWheel.currentValue, true); @@ -99,24 +104,25 @@ class SynthetizerUI controller.brightness.update(controller.brightness.currentValue, true); controller.transpose.update(controller.transpose.currentValue, true); } - }) + }); } - + toggleVisibility() { - if(this.animationId !== -1) + if (this.animationId !== -1) { clearTimeout(this.animationId); } const controller = document.getElementsByClassName("synthui_controller")[0]; this.isShown = !this.isShown; - if(this.isShown) + if (this.isShown) { controller.style.display = "block"; document.getElementsByClassName("top_part")[0].classList.add("synthui_shown"); this.showControllers(); - - setTimeout(() => { + + setTimeout(() => + { controller.classList.add("synthui_controller_show"); }, ANIMATION_REFLOW_TIME); } @@ -125,21 +131,23 @@ class SynthetizerUI document.getElementsByClassName("top_part")[0].classList.remove("synthui_shown"); this.hideControllers(); controller.classList.remove("synthui_controller_show"); - this.animationId = setTimeout(() => { + this.animationId = setTimeout(() => + { controller.style.display = "none"; }, 200); } } - + updateVoicesAmount() { this.voiceMeter.update(this.synth.voicesAmount); - - this.controllers.forEach((controller, i) => { + + this.controllers.forEach((controller, i) => + { // update channel let voices = this.synth.channelProperties[i].voicesAmount; controller.voiceMeter.update(voices); - if(voices < 1 && this.synth.voicesAmount > 0) + if (voices < 1 && this.synth.voicesAmount > 0) { controller.controller.classList.add("no_voices"); } @@ -149,10 +157,11 @@ class SynthetizerUI } }); } - + getInstrumentList() { - this.synth.eventHandler.addEvent("presetlistchange", "synthui-preset-list-change", e => { + this.synth.eventHandler.addEvent("presetlistchange", "synthui-preset-list-change", e => + { /** * @type {PresetListElement[]} */ @@ -161,47 +170,51 @@ class SynthetizerUI * @type {{name: string, program: number, bank: number}[]} */ this.instrumentList = presetList.filter(p => p.bank !== 128) - .sort((a, b) => { - if(a.program === b.program) + .sort((a, b) => + { + if (a.program === b.program) { return a.bank - b.bank; } return a.program - b.program; }) - .map(p => { + .map(p => + { return { name: p.presetName, bank: p.bank, program: p.program }; }); - + /** * @type {{name: string, program: number, bank: number}[]} */ this.percussionList = presetList.filter(p => p.bank === 128) .sort((a, b) => a.program - b.program) - .map(p => { + .map(p => + { return { name: p.presetName, bank: p.bank, program: p.program }; }); - - if(this.percussionList.length === 0) + + if (this.percussionList.length === 0) { this.percussionList = this.instrumentList; } - else if(this.instrumentList.length === 0) + else if (this.instrumentList.length === 0) { this.instrumentList = this.percussionList; } - - this.controllers.forEach((controller, i) => { + + this.controllers.forEach((controller, i) => + { const list = this.synth.channelProperties[i].isDrum ? this.percussionList : this.instrumentList; controller.preset.reload(list); - controller.preset.set(`${list[0].bank}:${list[0].program}`) + controller.preset.set(`${list[0].bank}:${list[0].program}`); }); }); } @@ -217,4 +230,4 @@ SynthetizerUI.prototype.createMainSynthController = createMainSynthController; SynthetizerUI.prototype.setEventListeners = setEventListeners; -export { SynthetizerUI } \ No newline at end of file +export { SynthetizerUI }; \ No newline at end of file diff --git a/src/website/js/utils/calculate_rgb.js b/src/website/js/utils/calculate_rgb.js index 728d6a3f..2d43a8c7 100644 --- a/src/website/js/utils/calculate_rgb.js +++ b/src/website/js/utils/calculate_rgb.js @@ -6,6 +6,7 @@ */ export function calculateRGB(rgbString, operation) { - let rgbValues = rgbString.replace(/[^\d,]/g, '').split(','); - return `rgb(${operation(parseInt(rgbValues[0]))}, ${operation(parseInt(rgbValues[1]))}, ${operation(parseInt(rgbValues[2]))})`; + let rgbValues = rgbString.replace(/[^\d,]/g, "").split(","); + return `rgb(${operation(parseInt(rgbValues[0]))}, ${operation(parseInt(rgbValues[1]))}, ${operation(parseInt( + rgbValues[2]))})`; } \ No newline at end of file diff --git a/src/website/js/utils/drop_file_handler.js b/src/website/js/utils/drop_file_handler.js index 3baf113f..12705728 100644 --- a/src/website/js/utils/drop_file_handler.js +++ b/src/website/js/utils/drop_file_handler.js @@ -1,4 +1,3 @@ - export class DropFileHandler { /** @@ -9,34 +8,40 @@ export class DropFileHandler constructor(midiCallback, soundFontCallback) { const dragPrompt = document.getElementsByClassName("drop_prompt")[0]; - document.body.addEventListener("dragover", e => { + document.body.addEventListener("dragover", e => + { e.preventDefault(); dragPrompt.classList.remove("hidden"); }); - document.body.addEventListener("dragleave", () => { + document.body.addEventListener("dragleave", () => + { dragPrompt.classList.add("hidden"); }); - - document.body.addEventListener("drop", async e => { + + document.body.addEventListener("drop", async e => + { e.preventDefault(); dragPrompt.classList.add("hidden"); const file = e.dataTransfer.files[0]; - if(!file) return; - + if (!file) + { + return; + } + const name = file.name; const buf = await file.arrayBuffer(); // identify the file // check for RIFF const riff = buf.slice(0, 4); const decoder = new TextDecoder(); - if(decoder.decode(riff) === "RIFF") + if (decoder.decode(riff) === "RIFF") { // riff, check if RMID, otherwise soundfont const rmid = buf.slice(8, 12); - if(decoder.decode(rmid) === "RMID") + if (decoder.decode(rmid) === "RMID") { // RMID - midiCallback({buf: buf, name: name}); + midiCallback({ buf: buf, name: name }); return; } // soundfont @@ -44,8 +49,8 @@ export class DropFileHandler return; } // midi - midiCallback({buf: buf, name: name}); - - }) + midiCallback({ buf: buf, name: name }); + + }); } } \ No newline at end of file diff --git a/src/website/js/utils/encodings.js b/src/website/js/utils/encodings.js index 90f15fd1..02930082 100644 --- a/src/website/js/utils/encodings.js +++ b/src/website/js/utils/encodings.js @@ -36,5 +36,5 @@ export const supportedEncodings = [ "ISO-2022-JP", "EUC-KR", "Big5", - "GB18030", + "GB18030" ]; \ No newline at end of file diff --git a/src/website/js/utils/icons.js b/src/website/js/utils/icons.js index 88bea2b7..f15aaa25 100644 --- a/src/website/js/utils/icons.js +++ b/src/website/js/utils/icons.js @@ -10,8 +10,8 @@ */ export function getPlaySvg(size) { - return ` - + return ` + `; } @@ -21,8 +21,8 @@ export function getPlaySvg(size) */ export function getPauseSvg(size) { - return ` - + return ` + `; } @@ -32,19 +32,19 @@ export function getPauseSvg(size) */ export function getLoopSvg(size) { - return ` - - + c13.284,0,24.878-7.354,30.941-18.201L80.93,65.23C81.478,64.046,81.055,62.623,79.904,61.958z'/> `; } @@ -54,54 +54,54 @@ export function getLoopSvg(size) */ export function getTextSvg(size) { - return ` - + return ` + `; } export function getForwardSvg(size) { - return ` - + return ` + `; } export function getBackwardSvg(size) { - return ` - + return ` + `; } export function getVolumeSvg(size) { - return ` - - - + return ` + + + `; } export function getEmptyMicSvg(size) { - return ` - - + return ` + + `; } export function getMicSvg(size) { - return ` - - + return ` + + `; } export function getMuteSvg(size) { - return ` - + return ` + `; } @@ -131,68 +131,68 @@ export function getNoteSvg(size) export function getGearSvg(size) { - return ` - + return ` + `; } export function getDoubleNoteSvg(size) { - return ` - - - -` + return ` + + + +`; } export function getDownArrowSvg(size) { - return ` - + return ` + `; } export function getLockSVG(size) { - return ` - -` + return ` + +`; } export function getUnlockSVG(size) { - return ` - + return ` + `; } export function getSf2LogoSvg(size) { - return ` + return ` - - + + - + - + - + - - - - + + + + `; } export function getHourglassSvg(size) { - return ` - + return ` + `; } @@ -201,7 +201,7 @@ export function getExclamationSvg(size) return ` -` +`; } export function getCheckSvg(size) diff --git a/src/website/js/utils/is_mobile.js b/src/website/js/utils/is_mobile.js index e11c4c59..30389540 100644 --- a/src/website/js/utils/is_mobile.js +++ b/src/website/js/utils/is_mobile.js @@ -1,6 +1,18 @@ -function checkMobile() { +// noinspection RegExpRedundantEscape,JSDeprecatedSymbols,RegExpSingleCharAlternation + +function checkMobile() +{ let check = false; - (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera); + (function(a) + { + if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test( + a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( + a.substr(0, 4))) + { + check = true; + } + })(navigator.userAgent || navigator.vendor || window.opera); return check; } + export const isMobile = checkMobile(); \ No newline at end of file diff --git a/src/website/js/utils/keybinds.js b/src/website/js/utils/keybinds.js index 3e85ecab..7e3603f8 100644 --- a/src/website/js/utils/keybinds.js +++ b/src/website/js/utils/keybinds.js @@ -4,10 +4,10 @@ export const keybinds = { synthesizerUIShow: "s", settingsShow: "r", - + blackMidiMode: "b", midiPanic: "backspace", - + playPause: " ", toggleLoop: "l", toggleLyrics: "t", @@ -15,7 +15,7 @@ export const keybinds = { seekForwards: "arrowright", previousSong: "[", nextSong: "]", - + cinematicMode: "c", videoMode: "v" -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/website/local_edition_index.html b/src/website/local_edition_index.html index 7b766cb2..25e680f6 100644 --- a/src/website/local_edition_index.html +++ b/src/website/local_edition_index.html @@ -1,78 +1,85 @@ - + + - - - - + + + + - - - - - - - - + + + + + + + + - - - - - + + + + + - - - - + + + + - SpessaSynth Local Edition - - - - + SpessaSynth Local Edition + + + + - -
-
-
+
+
+
-
-
-

SpessaSynth: SoundFont2 Javascript Synthetizer

- -
- -
+
+
+

SpessaSynth: SoundFont2 Javascript Synthetizer

+ +
+ +
- + - + +
-
-
-
+
+
- -
+ +
-
-
+
+
-
- +
+ \ No newline at end of file diff --git a/src/website/server/open.js b/src/website/server/open.js index 1db63fee..af7d7272 100644 --- a/src/website/server/open.js +++ b/src/website/server/open.js @@ -1,19 +1,20 @@ -import child_process from 'node:child_process' +import child_process from "node:child_process"; export function openURL(url) { - switch (process.platform) { - case 'linux': + switch (process.platform) + { + case "linux": child_process.exec(`xdg-open ${url}`); break; - case 'win32': + case "win32": child_process.exec(`start "" ${url}`); break; - case 'darwin': + case "darwin": child_process.exec(`open "${url}"`); break; default: - console.log('Could not open the browser. Open the link below:'); + console.log("Could not open the browser. Open the link below:"); console.log(url); } } \ No newline at end of file diff --git a/src/website/server/serve.js b/src/website/server/serve.js index 3e87c4f0..58a4b95f 100644 --- a/src/website/server/serve.js +++ b/src/website/server/serve.js @@ -1,6 +1,7 @@ -import fs from 'fs'; -import path from 'path' -import { configPath, packageJSON, soundfontsPath } from './server.js' +import fs from "fs"; +import path from "path"; +import { configPath, packageJSON, soundfontsPath } from "./server.js"; + /** * @param res {ServerResponse} * @param path {string} @@ -8,31 +9,34 @@ import { configPath, packageJSON, soundfontsPath } from './server.js' export async function serveSfont(path, res) { const fileStream = fs.createReadStream(path); - + const size = fs.statSync(path).size; - - fileStream.on('error', (error) => { - if (error.code === 'ENOENT') + + fileStream.on("error", (error) => + { + if (error.code === "ENOENT") { res.writeHead(404); - res.end('Soundfont not found'); + res.end("Soundfont not found"); } else { res.writeHead(500); - res.end('Internal server error'); + res.end("Internal server error"); } }); - - fileStream.once('open', () => { - res.setHeader('Content-Type', 'application/octet-stream'); - res.setHeader('Content-Length', size); + + fileStream.once("open", () => + { + res.setHeader("Content-Type", "application/octet-stream"); + res.setHeader("Content-Length", size); fileStream.pipe(res); }); - fileStream.on("end", () => { + fileStream.on("end", () => + { fileStream.close(); res.end(); - }) + }); } /** @@ -41,11 +45,11 @@ export async function serveSfont(path, res) */ function isSoundFont(name) { - const fName = name.toLowerCase() - return fName.slice(-3) === 'sf2' || - fName.slice(-3) === 'sf3' || - fName.slice(-5) === 'sfogg' || - fName.slice(-3) === 'dls' + const fName = name.toLowerCase(); + return fName.slice(-3) === "sf2" || + fName.slice(-3) === "sf3" || + fName.slice(-5) === "sfogg" || + fName.slice(-3) === "dls"; } /** @@ -54,26 +58,27 @@ function isSoundFont(name) export function serveSfontList(res) { const fileNames = fs.readdirSync(soundfontsPath).filter(fName => isSoundFont(fName)); - - const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); - if (config['lastUsedSf2']) + + const config = JSON.parse(fs.readFileSync(configPath, "utf-8")); + if (config["lastUsedSf2"]) { - if (fileNames.includes(config['lastUsedSf2'])) + if (fileNames.includes(config["lastUsedSf2"])) { - fileNames.splice(fileNames.indexOf(config['lastUsedSf2']), 1); - fileNames.unshift(config['lastUsedSf2']); + fileNames.splice(fileNames.indexOf(config["lastUsedSf2"]), 1); + fileNames.unshift(config["lastUsedSf2"]); } } else { - config['lastUsedSf2'] = fileNames[0]; + config["lastUsedSf2"] = fileNames[0]; } - - const files = fileNames.map(file => { + + const files = fileNames.map(file => + { return { name: file }; }); - - res.writeHead(200, { 'Content-Type': 'application/json' }); + + res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(files)); } @@ -82,8 +87,8 @@ export function serveSfontList(res) */ export function serveSettings(res) { - const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); - res.writeHead(200, { 'Content-Type': 'application/json' }); + const config = JSON.parse(fs.readFileSync(configPath, "utf-8")); + res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify(config.settings || {})); } @@ -92,10 +97,10 @@ export function serveSettings(res) * @param filePath {string} * @param mimeType {string} */ -export function serveStaticFile(res, filePath, mimeType=undefined) +export function serveStaticFile(res, filePath, mimeType = undefined) { filePath = decodeURIComponent(filePath); - if( + if ( filePath.toLowerCase().endsWith(".sf3") || filePath.toLowerCase().endsWith(".sf2") || filePath.toLowerCase().endsWith(".sfogg") || @@ -105,13 +110,14 @@ export function serveStaticFile(res, filePath, mimeType=undefined) filePath = path.join(path.dirname(filePath), "../soundfonts", path.basename(filePath)); serveSfont(filePath, res).then(); return; - + } let file; - try { + try + { file = fs.readFileSync(filePath); - } - catch (e) { + } catch (e) + { res.writeHead(404); res.end(` @@ -125,13 +131,13 @@ export function serveStaticFile(res, filePath, mimeType=undefined) return; } let type = {}; - if(mimeType) + if (mimeType) { - type = { 'Content-Type': mimeType} + type = { "Content-Type": mimeType }; } - if(filePath.endsWith(".js")) + if (filePath.endsWith(".js")) { - type = { 'Content-Type': 'text/javascript'} + type = { "Content-Type": "text/javascript" }; } res.writeHead(200, type); res.end(file); @@ -143,8 +149,8 @@ export function serveStaticFile(res, filePath, mimeType=undefined) */ export function getVersion(res) { - const text = fs.readFileSync(packageJSON, 'utf-8'); + const text = fs.readFileSync(packageJSON, "utf-8"); const jason = JSON.parse(text); - res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.writeHead(200, { "Content-Type": "text/plain" }); res.end(jason.version); } \ No newline at end of file diff --git a/src/website/server/server.js b/src/website/server/server.js index 2123d710..79417ec0 100644 --- a/src/website/server/server.js +++ b/src/website/server/server.js @@ -1,70 +1,86 @@ -import http from 'http'; -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { getVersion, serveSettings, serveSfontList, serveStaticFile } from './serve.js' -import { openURL } from './open.js' +import http from "http"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import { getVersion, serveSettings, serveSfontList, serveStaticFile } from "./serve.js"; +import { openURL } from "./open.js"; let PORT = 8181; -const HOST = '0.0.0.0'; +const HOST = "0.0.0.0"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.join(path.dirname(__filename), "../../"); -export const configPath = path.join(__dirname, '/website/server/config.json'); -export const soundfontsPath = path.join(__dirname, '../soundfonts'); -export const packageJSON = path.join(__dirname, '../package.json'); +export const configPath = path.join(__dirname, "/website/server/config.json"); +export const soundfontsPath = path.join(__dirname, "../soundfonts"); +export const packageJSON = path.join(__dirname, "../package.json"); -fs.writeFile(configPath, '{}', { flag: 'wx' }, () => {}); +fs.writeFile(configPath, "{}", { flag: "wx" }, () => +{ +}); -const server = http.createServer((req, res) => { - switch(req.url.split('?')[0]) +const server = http.createServer((req, res) => +{ + switch (req.url.split("?")[0]) { case "/": - serveStaticFile(res, path.join(__dirname, 'website/local_edition_index.html'), 'text/html'); + serveStaticFile( + res, + path.join(__dirname, "website/local_edition_index.html"), + "text/html" + ); break; - + case "/soundfonts": serveSfontList(res); break; - + 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'); + 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"); break; - + case "/savesettings": - let body = ''; - req.on('data', chunk => { + let body = ""; + req.on("data", chunk => + { body += chunk.toString(); }); - req.on('end', () => { + req.on("end", () => + { const settings = JSON.parse(body); - const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + const config = JSON.parse(fs.readFileSync(configPath, "utf-8")); config.settings = settings; - - fs.writeFile(configPath, JSON.stringify(config), { flag: 'w' }, () => {}); - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('Settings saved'); + + fs.writeFile(configPath, JSON.stringify(config), { flag: "w" }, () => + { + }); + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("Settings saved"); }); break; - + case "/getsettings": serveSettings(res); break; - + case "/getversion": getVersion(res); break; - + default: serveStaticFile(res, path.join(__dirname, req.url)); } @@ -72,10 +88,12 @@ const server = http.createServer((req, res) => { // look for a port that isn't occupied let served = false; + function tryServer() { - server.listen(PORT, HOST, () => { - if(served) + server.listen(PORT, HOST, () => + { + if (served) { return; } @@ -83,10 +101,11 @@ function tryServer() let url = `http://localhost:${PORT}`; console.log(`Running on ${url}. A browser window should open.`); openURL(url); - }).on('error', e => { - if(e.code === 'EADDRINUSE') + }).on("error", e => + { + if (e.code === "EADDRINUSE") { - console.log(`Port ${PORT} seems to be occupied, trying again...`) + console.log(`Port ${PORT} seems to be occupied, trying again...`); PORT++; tryServer(); } @@ -96,4 +115,5 @@ function tryServer() } }); } + tryServer();