diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..5731dade63692ea6b22efe9e64998e93f8a9ee4e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +spessasynth_lib/soundfonts/GeneralUserGS.sf3 filter=lfs diff=lfs merge=lfs -text diff --git a/394.mid b/394.mid new file mode 100644 index 0000000000000000000000000000000000000000..f199613373382465f0366f25f234a772bd6695be Binary files /dev/null and b/394.mid differ diff --git a/advanced_demo.js b/advanced_demo.js new file mode 100644 index 0000000000000000000000000000000000000000..43a98ff730e61f8b3d0672dd617a4919bf82697d --- /dev/null +++ b/advanced_demo.js @@ -0,0 +1,109 @@ + + + + + + + + + + + + +// import the modules +import { WORKLET_URL_ABSOLUTE } from "./spessasynth_lib/synthetizer/worklet_url.js"; +import { Sequencer } from "./spessasynth_lib/sequencer/sequencer.js"; +import { Synthetizer } from "./spessasynth_lib/synthetizer/synthetizer.js"; + +// load the soundfont +fetch("./spessasynth_lib/soundfonts/GeneralUserGS.sf3").then(async response => +{ + // load the soundfont into an array buffer + let soundFontBuffer = await response.arrayBuffer(); + document.getElementById("message").innerText = "SoundFont has been loaded!"; + + // create the context and add audio worklet + const context = new AudioContext(); + await context.audioWorklet.addModule(new URL("./spessasynth_lib/" + WORKLET_URL_ABSOLUTE, import.meta.url)); + const synth = new Synthetizer(context.destination, soundFontBuffer); // create the synthetizer + let seq; + + // add an event listener for the file inout + document.getElementById("midi_input").addEventListener("change", async event => + { + // check if any files are added + if (!event.target.files[0]) + { + return; + } + // resume the context if paused + await context.resume(); + // parse all the files + const parsedSongs = []; + for (let file of event.target.files) + { + const buffer = await file.arrayBuffer(); + parsedSongs.push({ + binary: buffer, // binary: the binary data of the file + altName: file.name // altName: the fallback name if the MIDI doesn't have one. Here we set it to the file name + }); + } + if (seq === undefined) + { + seq = new Sequencer(parsedSongs, synth); // create the sequencer with the parsed midis + seq.play(); // play the midi + } + else + { + seq.loadNewSongList(parsedSongs); // the sequencer is already created, no need to create a new one. + } + seq.loop = false; // the sequencer loops a single song by default + + // make the slider move with the song + let slider = document.getElementById("progress"); + setInterval(() => + { + // slider ranges from 0 to 1000 + slider.value = (seq.currentTime / seq.duration) * 1000; + }, 100); + + // on song change, show the name + seq.addOnSongChangeEvent(e => + { + document.getElementById("message").innerText = "Now playing: " + e.midiName; + }, "example-time-change"); // make sure to add a unique id! + + // add time adjustment + slider.onchange = () => + { + // calculate the time + seq.currentTime = (slider.value / 1000) * seq.duration; // switch the time (the sequencer adjusts automatically) + }; + + // add button controls + document.getElementById("previous").onclick = () => + { + seq.previousSong(); // go back by one song + }; + + // on pause click + document.getElementById("pause").onclick = () => + { + if (seq.paused) + { + document.getElementById("pause").innerText = "Pause"; + seq.play(); // resume + } + else + { + document.getElementById("pause").innerText = "Resume"; + seq.pause(); // pause + + } + }; + document.getElementById("next").onclick = () => + { + seq.nextSong(); // go to the next song + }; + }); +}); \ No newline at end of file diff --git a/examples.css b/examples.css new file mode 100644 index 0000000000000000000000000000000000000000..0dc1583264ea6f1466702745bfa1b89709009db3 --- /dev/null +++ b/examples.css @@ -0,0 +1,66 @@ +.txtlogee{ + text-align: center; + text-transform: uppercase !important; + } + + .playcax{ + text-align: center; + margin: 15px 0px; + } + + * { + font-family: "Noto Sans Light", "Open Sans Light", sans-serif; + color: #ccc; + font-size: 1.1rem; + } + + button { + background: #444; + border: solid #555; + padding: 10px; + border-radius: 1rem; + cursor: pointer; + margin: 0.3rem; + display: inline-block; + text-transform: uppercase !important; + } + + button:hover { + filter: brightness(1.1); + } + + button:active { + transform: scale(0.9); + } + + canvas { + width: 100%; + min-height: 0; + max-height: 40vh; + } + + input[type="range"] { + width: 100%; + } + + body { + background: #111; + display: flex; + flex-direction: column; + align-items: center; + max-height: 100vh; + margin: 50px; + } + + .example_content { + width: 80%; + background: #333; + padding: 2em; + margin: 1rem; + /*border-radius: 1rem;*/ + box-shadow: black 0 0 15px; + /*display: flex;*/ + flex-direction: column; + align-items: center; + } + \ No newline at end of file diff --git a/index - Copia.html b/index - Copia.html new file mode 100644 index 0000000000000000000000000000000000000000..e675b2a6786ee1e71312ab75a248b89394ed2804 --- /dev/null +++ b/index - Copia.html @@ -0,0 +1,32 @@ + + + + + + + + SpessaSynth advanced example + + + + + + +
+

Please wait for the soundFont to load.

+ +

+ +
+ + + + + + + +
+ + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000000000000000000000000000000000000..e675b2a6786ee1e71312ab75a248b89394ed2804 --- /dev/null +++ b/index.html @@ -0,0 +1,32 @@ + + + + + + + + SpessaSynth advanced example + + + + + + +
+

Please wait for the soundFont to load.

+ +

+ +
+ + + + + + + +
+ + + \ No newline at end of file diff --git a/spessasynth_lib/external_midi/README.md b/spessasynth_lib/external_midi/README.md new file mode 100644 index 0000000000000000000000000000000000000000..20e67ffa9f2e4f834ec87f27010235373381baa8 --- /dev/null +++ b/spessasynth_lib/external_midi/README.md @@ -0,0 +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/spessasynth_lib/external_midi/midi_handler.js b/spessasynth_lib/external_midi/midi_handler.js new file mode 100644 index 0000000000000000000000000000000000000000..b996028bfd07b3ee24e9a3b918b68c2bfe4a33d3 --- /dev/null +++ b/spessasynth_lib/external_midi/midi_handler.js @@ -0,0 +1,130 @@ +import { Synthetizer } from "../synthetizer/synthetizer.js"; +import { consoleColors } from "../utils/other.js"; +import { SpessaSynthInfo, SpessaSynthWarn } from "../utils/loggin.js"; + +/** + * midi_handler.js + * purpose: handles the connection between MIDI devices and synthesizer/sequencer via Web MIDI API + */ + +const NO_INPUT = null; + +export class MIDIDeviceHandler +{ + constructor() + { + } + + /** + * @returns {Promise} if succeded + */ + async createMIDIDeviceHandler() + { + /** + * @type {MIDIInput} + */ + this.selectedInput = NO_INPUT; + /** + * @type {MIDIOutput} + */ + this.selectedOutput = NO_INPUT; + if (navigator.requestMIDIAccess) + { + // prepare the midi access + try + { + const response = await navigator.requestMIDIAccess({ sysex: true, software: true }); + this.inputs = response.inputs; + this.outputs = response.outputs; + SpessaSynthInfo("%cMIDI handler created!", consoleColors.recognized); + return true; + } + catch (e) + { + SpessaSynthWarn(`Could not get MIDI Devices:`, e); + this.inputs = []; + this.outputs = []; + return false; + } + } + else + { + SpessaSynthWarn("Web MIDI Api not supported!", consoleColors.unrecognized); + this.inputs = []; + this.outputs = []; + return false; + } + } + + /** + * Connects the sequencer to a given MIDI output port + * @param output {MIDIOutput} + * @param seq {Sequencer} + */ + connectMIDIOutputToSeq(output, seq) + { + this.selectedOutput = output; + seq.connectMidiOutput(output); + SpessaSynthInfo( + `%cPlaying MIDI to %c${output.name}`, + consoleColors.info, + consoleColors.recognized + ); + } + + /** + * Disconnects a midi output port from the sequencer + * @param seq {Sequencer} + */ + disconnectSeqFromMIDI(seq) + { + this.selectedOutput = NO_INPUT; + seq.connectMidiOutput(undefined); + SpessaSynthInfo( + "%cDisconnected from MIDI out.", + consoleColors.info + ); + } + + /** + * Connects a MIDI input to the synthesizer + * @param input {MIDIInput} + * @param synth {Synthetizer} + */ + connectDeviceToSynth(input, synth) + { + this.selectedInput = input; + input.onmidimessage = event => + { + synth.sendMessage(event.data); + }; + SpessaSynthInfo( + `%cListening for messages on %c${input.name}`, + consoleColors.info, + consoleColors.recognized + ); + } + + /** + * @param input {MIDIInput} + */ + disconnectDeviceFromSynth(input) + { + this.selectedInput = NO_INPUT; + input.onmidimessage = undefined; + SpessaSynthInfo( + `%cDisconnected from %c${input.name}`, + consoleColors.info, + consoleColors.recognized + ); + } + + disconnectAllDevicesFromSynth() + { + this.selectedInput = NO_INPUT; + for (const i of this.inputs) + { + i[1].onmidimessage = undefined; + } + } +} \ No newline at end of file diff --git a/spessasynth_lib/external_midi/web_midi_link.js b/spessasynth_lib/external_midi/web_midi_link.js new file mode 100644 index 0000000000000000000000000000000000000000..7ece7f31977837f687d6275657c9c0f3f4693245 --- /dev/null +++ b/spessasynth_lib/external_midi/web_midi_link.js @@ -0,0 +1,43 @@ +import { Synthetizer } from "../synthetizer/synthetizer.js"; +import { consoleColors } from "../utils/other.js"; +import { SpessaSynthInfo } from "../utils/loggin.js"; + +/** + * web_midi_link.js + * purpose: handles the web midi link connection to the synthesizer + * https://www.g200kg.com/en/docs/webmidilink/ + */ + +export class WebMIDILinkHandler +{ + /** + * @param synth {Synthetizer} the synth to play to + */ + constructor(synth) + { + + window.addEventListener("message", msg => + { + if (typeof msg.data !== "string") + { + return; + } + /** + * @type {string[]} + */ + const data = msg.data.split(","); + 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/spessasynth_lib/externals/fflate/LICENSE b/spessasynth_lib/externals/fflate/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..e84f0d6caca033c62946025c0237a499459af698 --- /dev/null +++ b/spessasynth_lib/externals/fflate/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Arjun Barrett + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/spessasynth_lib/externals/fflate/fflate.min.js b/spessasynth_lib/externals/fflate/fflate.min.js new file mode 100644 index 0000000000000000000000000000000000000000..db85cbdc9262f207ef2960c10a9b8891107f8ea8 --- /dev/null +++ b/spessasynth_lib/externals/fflate/fflate.min.js @@ -0,0 +1 @@ +let tr;(()=>{var l=Uint8Array,T=Uint16Array,ur=Int32Array,W=new l([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0]),X=new l([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0]),wr=new l([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),Y=function(r,a){for(var e=new T(31),f=0;f<31;++f)e[f]=a+=1<>1|(i&21845)<<1,c=(c&52428)>>2|(c&13107)<<2,c=(c&61680)>>4|(c&3855)<<4,_[i]=((c&65280)>>8|(c&255)<<8)>>1;var c,i,A=function(r,a,e){for(var f=r.length,v=0,g=new T(a);v>m]=U}else for(b=new T(f),v=0;v>15-r[v]);return b},M=new l(288);for(i=0;i<144;++i)M[i]=8;var i;for(i=144;i<256;++i)M[i]=9;var i;for(i=256;i<280;++i)M[i]=7;var i;for(i=280;i<288;++i)M[i]=8;var i,L=new l(32);for(i=0;i<32;++i)L[i]=5;var i,gr=A(M,9,1),br=A(L,5,1),q=function(r){for(var a=r[0],e=1;ea&&(a=r[e]);return a},u=function(r,a,e){var f=a/8|0;return(r[f]|r[f+1]<<8)>>(a&7)&e},C=function(r,a){var e=a/8|0;return(r[e]|r[e+1]<<8|r[e+2]<<16)>>(a&7)},kr=function(r){return(r+7)/8|0},xr=function(r,a,e){return(a==null||a<0)&&(a=0),(e==null||e>r.length)&&(e=r.length),new l(r.subarray(a,e))},yr=["unexpected EOF","invalid block type","invalid length/literal","invalid distance","stream finished","no stream handler",,"no callback","invalid UTF-8 data","extra field too long","date not in range 1980-2099","filename too long","stream finishing","invalid zip data"],h=function(r,a,e){var f=new Error(a||yr[r]);if(f.code=r,Error.captureStackTrace&&Error.captureStackTrace(f,h),!e)throw f;return f},Sr=function(r,a,e,f){var v=r.length,g=f?f.length:0;if(!v||a.f&&!a.l)return e||new l(0);var k=!e,b=k||a.i!=2,m=a.i;k&&(e=new l(v*3));var U=function(fr){var or=e.length;if(fr>or){var lr=new l(Math.max(or*2,fr));lr.set(e),e=lr}},x=a.f||0,n=a.p||0,o=a.b||0,S=a.l,I=a.d,z=a.m,D=a.n,G=v*8;do{if(!S){x=u(r,n,1);var H=u(r,n+1,3);if(n+=3,H)if(H==1)S=gr,I=br,z=9,D=5;else if(H==2){var N=u(r,n,31)+257,s=u(r,n+10,15)+4,d=N+u(r,n+5,31)+1;n+=14;for(var F=new l(d),P=new l(19),t=0;t>4;if(w<16)F[t++]=w;else{var E=0,O=0;for(w==16?(O=3+u(r,n,3),n+=2,E=F[t-1]):w==17?(O=3+u(r,n,7),n+=3):w==18&&(O=11+u(r,n,127),n+=7);O--;)F[t++]=E}}var er=F.subarray(0,N),y=F.subarray(N);z=q(er),D=q(y),S=A(er,z,1),I=A(y,D,1)}else h(1);else{var w=kr(n)+4,J=r[w-4]|r[w-3]<<8,K=w+J;if(K>v){m&&h(0);break}b&&U(o+J),e.set(r.subarray(w,K),o),a.b=o+=J,a.p=n=K*8,a.f=x;continue}if(n>G){m&&h(0);break}}b&&U(o+131072);for(var Ur=(1<>4;if(n+=E&15,n>G){m&&h(0);break}if(E||h(2),p<256)e[o++]=p;else if(p==256){Q=n,S=null;break}else{var nr=p-254;if(p>264){var t=p-257,B=W[t];nr=u(r,n,(1<>4;R||h(3),n+=R&15;var y=hr[V];if(V>3){var B=X[V];y+=C(r,n)&(1<G){m&&h(0);break}b&&U(o+131072);var vr=o+nr;if(o +} \ No newline at end of file diff --git a/spessasynth_lib/externals/stbvorbis_sync/LICENSE b/spessasynth_lib/externals/stbvorbis_sync/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..e09346158473de0a62027512fc42dc16acb42e73 --- /dev/null +++ b/spessasynth_lib/externals/stbvorbis_sync/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/spessasynth_lib/externals/stbvorbis_sync/NOTICE b/spessasynth_lib/externals/stbvorbis_sync/NOTICE new file mode 100644 index 0000000000000000000000000000000000000000..6c34db4d627a3f0e181d72f2c0b5b78a91665606 --- /dev/null +++ b/spessasynth_lib/externals/stbvorbis_sync/NOTICE @@ -0,0 +1,6 @@ +LICENSE is for stbvorbis_sync.js which is licensed under Apache-2.0 + +Modifications made to stbvorbis_sync.js +1. minified the code +2. added types declaration +3. changed the not initialized error message \ No newline at end of file diff --git a/spessasynth_lib/externals/stbvorbis_sync/stbvorbis_sync.min.js b/spessasynth_lib/externals/stbvorbis_sync/stbvorbis_sync.min.js new file mode 100644 index 0000000000000000000000000000000000000000..caf38055c4593dd1a4a2ce04db6d68df5de26b40 --- /dev/null +++ b/spessasynth_lib/externals/stbvorbis_sync/stbvorbis_sync.min.js @@ -0,0 +1 @@ +export var stbvorbis=void 0!==stbvorbis?stbvorbis:{};let isReady=!1,readySolver;stbvorbis.isInitialized=new Promise(A=>readySolver=A);var atob=function(A){var I,g,B,E,Q,C,i,h="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",o="",G=0;A=A.replace(/[^A-Za-z0-9\+\/\=]/g,"");do E=h.indexOf(A.charAt(G++)),Q=h.indexOf(A.charAt(G++)),C=h.indexOf(A.charAt(G++)),i=h.indexOf(A.charAt(G++)),I=E<<2|Q>>4,g=(15&Q)<<4|C>>2,B=(3&C)<<6|i,o+=String.fromCharCode(I),64!==C&&(o+=String.fromCharCode(g)),64!==i&&(o+=String.fromCharCode(B));while(G1&&($.thisProgram=process.argv[1].replace(/\\/g,"/")),$.arguments=process.argv.slice(2),"undefined"!=typeof module&&(/undefined!=$/),process.on("uncaughtException",function(A){if(!(A instanceof II))throw A}),process.on("unhandledRejection",function(A,I){process.exit(1)}),$.quit=function(A){process.exit(A)},$.inspect=function(){return"[Emscripten Module object]"}):r?("undefined"!=typeof read&&($.read=function A(I){return read(I)}),$.readBinary=function A(I){var g;return"function"==typeof readbuffer?new Uint8Array(readbuffer(I)):(_("object"==typeof(g=read(I,"binary"))),g)},"undefined"!=typeof scriptArgs?$.arguments=scriptArgs:"undefined"!=typeof arguments&&($.arguments=arguments),"function"==typeof quit&&($.quit=function(A){quit(A)})):(t||k)&&(t?document.currentScript&&(Y=document.currentScript.src):Y=self.location.href,Y=0!==Y.indexOf("blob:")?Y.split("/").slice(0,-1).join("/")+"/":"",$.read=function A(I){var g=new XMLHttpRequest;return g.open("GET",I,!1),g.send(null),g.responseText},k&&($.readBinary=function A(I){var g=new XMLHttpRequest;return g.open("GET",I,!1),g.responseType="arraybuffer",g.send(null),new Uint8Array(g.response)}),$.readAsync=function A(I,g,B){var E=new XMLHttpRequest;E.open("GET",I,!0),E.responseType="arraybuffer",E.onload=function A(){if(200==E.status||0==E.status&&E.response){g(E.response);return}B()},E.onerror=B,E.send(null)},$.setWindowTitle=function(A){document.title=A});var f=$.print||("undefined"!=typeof console?console.log.bind(console):"undefined"!=typeof print?print:null),H=$.printErr||("undefined"!=typeof printErr?printErr:"undefined"!=typeof console&&console.warn.bind(console)||f);for(A in e)e.hasOwnProperty(A)&&($[A]=e[A]);function L(A){var I=S;return S=S+A+15&-16,I}function M(A){var I=h[c>>2],g=I+A+15&-16;return(h[c>>2]=g,g>=AN&&!Ae())?(h[c>>2]=I,0):I}function d(A,I){return I||(I=16),A=Math.ceil(A/I)*I}function q(A){switch(A){case"i1":case"i8":return 1;case"i16":return 2;case"i32":case"float":return 4;case"i64":case"double":return 8;default:if("*"===A[A.length-1])return 4;if("i"!==A[0])return 0;var I=parseInt(A.substr(1));return _(I%8==0),I/8}}function K(A){K.shown||(K.shown={}),K.shown[A]||(K.shown[A]=1,H(A))}e=void 0;var l={"f64-rem":function(A,I){return A%I},debugger:function(){}},u=[];function b(A,I){for(var g=0,B=g;B>>0)+4294967296*+(I>>>0):+(A>>>0)+4294967296*+(0|I)}function V(A,I,g){return g&&g.length?$["dynCall_"+A].apply(null,[I].concat(g)):$["dynCall_"+A].call(null,I)}var p=0,W=0;function _(A,I){A||IE("Assertion failed: "+I)}function T(A){var I=$["_"+A];return _(I,"Cannot call unknown function "+A+", make sure it is exported"),I}var v={stackSave:function(){IA()},stackRestore:function(){A9()},arrayToC:function(A){var I,g,B=A5(A.length);return I=A,g=B,E.set(I,g),B},stringToC:function(A){var I=0;if(null!=A&&0!==A){var g=(A.length<<2)+1;I=A5(g),Ai(A,I,g)}return I}},O={string:v.stringToC,array:v.arrayToC};function j(A,I,g,B,E){var Q=T(A),C=[],i=0;if(B)for(var h=0;h>0]=I;break;case"i16":C[A>>1]=I;break;case"i32":h[A>>2]=I;break;case"i64":tempI64=[I>>>0,+Ax(tempDouble=I)>=1?tempDouble>0?(0|Ap(+A6(tempDouble/4294967296),4294967295))>>>0:~~+AV((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0,],h[A>>2]=tempI64[0],h[A+4>>2]=tempI64[1];break;case"float":G[A>>2]=I;break;case"double":D[A>>3]=I;break;default:IE("invalid type for setValue: "+g)}}function z(A,I,g){switch("*"===(I=I||"i8").charAt(I.length-1)&&(I="i32"),I){case"i1":case"i8":return E[A>>0];case"i16":return C[A>>1];case"i32":case"i64":return h[A>>2];case"float":return G[A>>2];case"double":return D[A>>3];default:IE("invalid type for getValue: "+I)}return null}function AA(A,I,g,B){"number"==typeof A?(i=!0,o=A):(i=!1,o=A.length);var C="string"==typeof I?I:null;if(G=4==g?B:["function"==typeof A8?A8:L,A5,L,M,][void 0===g?2:g](Math.max(o,C?1:I.length)),i){for(B=G,_((3&G)==0),D=G+(-4&o);B>2]=0;for(D=G+o;B>0]=0;return G}if("i8"===C)return A.subarray||A.slice?Q.set(A,G):Q.set(new Uint8Array(A),G),G;for(var i,o,G,D,a,S,F,R=0;R>0],(0!=B||I)&&(i++,!I||i!=I););I||(I=i);var h="";if(C<128){for(;I>0;)E=String.fromCharCode.apply(String,Q.subarray(A,A+Math.min(I,1024))),h=h?h+E:E,A+=1024,I-=1024;return h}return g=A,function A(I,g){for(var B=g;I[B];)++B;if(B-g>16&&I.subarray&&AQ)return AQ.decode(I.subarray(g,B));for(var E,Q,C,i,h,o,G="";;){if(!(E=I[g++]))return G;if(!(128&E)){G+=String.fromCharCode(E);continue}if(Q=63&I[g++],(224&E)==192){G+=String.fromCharCode((31&E)<<6|Q);continue}if(C=63&I[g++],(240&E)==224?E=(15&E)<<12|Q<<6|C:(i=63&I[g++],(248&E)==240?E=(7&E)<<18|Q<<12|C<<6|i:(h=63&I[g++],E=(252&E)==248?(3&E)<<24|Q<<18|C<<12|i<<6|h:(1&E)<<30|Q<<24|C<<18|i<<12|h<<6|(o=63&I[g++]))),E<65536)G+=String.fromCharCode(E);else{var D=E-65536;G+=String.fromCharCode(55296|D>>10,56320|1023&D)}}}(Q,g)}function AB(A){for(var I="";;){var g=E[A++>>0];if(!g)return I;I+=String.fromCharCode(g)}}function AE(A,I){return function A(I,g,B){for(var Q=0;Q>0]=I.charCodeAt(Q);B||(E[g>>0]=0)}(A,I,!1)}var AQ="undefined"!=typeof TextDecoder?new TextDecoder("utf8"):void 0;function AC(A,I,g,B){if(!(B>0))return 0;for(var E=g,Q=g+B-1,C=0;C=55296&&i<=57343&&(i=65536+((1023&i)<<10)|1023&A.charCodeAt(++C)),i<=127){if(g>=Q)break;I[g++]=i}else if(i<=2047){if(g+1>=Q)break;I[g++]=192|i>>6,I[g++]=128|63&i}else if(i<=65535){if(g+2>=Q)break;I[g++]=224|i>>12,I[g++]=128|i>>6&63,I[g++]=128|63&i}else if(i<=2097151){if(g+3>=Q)break;I[g++]=240|i>>18,I[g++]=128|i>>12&63,I[g++]=128|i>>6&63,I[g++]=128|63&i}else if(i<=67108863){if(g+4>=Q)break;I[g++]=248|i>>24,I[g++]=128|i>>18&63,I[g++]=128|i>>12&63,I[g++]=128|i>>6&63,I[g++]=128|63&i}else{if(g+5>=Q)break;I[g++]=252|i>>30,I[g++]=128|i>>24&63,I[g++]=128|i>>18&63,I[g++]=128|i>>12&63,I[g++]=128|i>>6&63,I[g++]=128|63&i}}return I[g]=0,g-E}function Ai(A,I,g){return AC(A,Q,I,g)}function Ah(A){for(var I=0,g=0;g=55296&&B<=57343&&(B=65536+((1023&B)<<10)|1023&A.charCodeAt(++g)),B<=127?++I:B<=2047?I+=2:B<=65535?I+=3:B<=2097151?I+=4:B<=67108863?I+=5:I+=6}return I}var Ao="undefined"!=typeof TextDecoder?new TextDecoder("utf-16le"):void 0;function AG(A){for(var I=A,g=I>>1;C[g];)++g;if((I=g<<1)-A>32&&Ao)return Ao.decode(Q.subarray(A,I));for(var B=0,E="";;){var i=C[A+2*B>>1];if(0==i)return E;++B,E+=String.fromCharCode(i)}}function AD(A,I,g){if(void 0===g&&(g=2147483647),g<2)return 0;for(var B=I,E=(g-=2)<2*A.length?g/2:A.length,Q=0;Q>1]=i,I+=2}return C[I>>1]=0,I-B}function Aa(A){return 2*A.length}function AS(A){for(var I=0,g="";;){var B=h[A+4*I>>2];if(0==B)return g;if(++I,B>=65536){var E=B-65536;g+=String.fromCharCode(55296|E>>10,56320|1023&E)}else g+=String.fromCharCode(B)}}function AF(A,I,g){if(void 0===g&&(g=2147483647),g<4)return 0;for(var B=I,E=B+g-4,Q=0;Q=55296&&C<=57343&&(C=65536+((1023&C)<<10)|1023&A.charCodeAt(++Q)),h[I>>2]=C,(I+=4)+4>E)break}return h[I>>2]=0,I-B}function AR(A){for(var I=0,g=0;g=55296&&B<=57343&&++g,I+=4}return I}function As(A){var I=Ah(A)+1,g=A8(I);return g&&AC(A,E,g,I),g}function Aw(A){var I=Ah(A)+1,g=A5(I);return AC(A,E,g,I),g}function Ay(A){return A}function Ac(){var A,I=function A(){var I=Error();if(!I.stack){try{throw Error(0)}catch(g){I=g}if(!I.stack)return"(no stack trace available)"}return I.stack.toString()}();return $.extraStackTrace&&(I+="\n"+$.extraStackTrace()),(A=I).replace(/__Z[\w\d_]+/g,function(A){var I,g=I=A;return A===g?A:A+" ["+g+"]"})}function An(A,I){return A%I>0&&(A+=I-A%I),A}function AU(A){$.buffer=B=A}function A$(){$.HEAP8=E=new Int8Array(B),$.HEAP16=C=new Int16Array(B),$.HEAP32=h=new Int32Array(B),$.HEAPU8=Q=new Uint8Array(B),$.HEAPU16=i=new Uint16Array(B),$.HEAPU32=o=new Uint32Array(B),$.HEAPF32=G=new Float32Array(B),$.HEAPF64=D=new Float64Array(B)}function Ae(){var A=$.usingWasm?65536:16777216,I=2147483648-A;if(h[c>>2]>I)return!1;var g=AN;for(AN=Math.max(AN,16777216);AN>2];)AN=AN<=536870912?An(2*AN,A):Math.min(An((3*AN+2147483648)/4,A),I);var B=$.reallocBuffer(AN);return B&&B.byteLength==AN?(AU(B),A$(),!0):(AN=g,!1)}a=S=R=s=w=y=c=0,F=!1,$.reallocBuffer||($.reallocBuffer=function(A){try{if(ArrayBuffer.transfer)I=ArrayBuffer.transfer(B,A);else{var I,g=E;I=new ArrayBuffer(A),new Int8Array(I).set(g)}}catch(Q){return!1}return!!Az(I)&&I});try{(n=Function.prototype.call.bind(Object.getOwnPropertyDescriptor(ArrayBuffer.prototype,"byteLength").get))(new ArrayBuffer(4))}catch(At){n=function(A){return A.byteLength}}var Ak=$.TOTAL_STACK||5242880,AN=$.TOTAL_MEMORY||16777216;function Ar(){return AN}function AY(A){for(;A.length>0;){var I=A.shift();if("function"==typeof I){I();continue}var g=I.func;"number"==typeof g?void 0===I.arg?$.dynCall_v(g):$.dynCall_vi(g,I.arg):g(void 0===I.arg?null:I.arg)}}AN=0?A:I<=32?2*Math.abs(1<=B&&(I<=32||A>B)&&(A=-2*B+A),A}var Ax=Math.abs,AV=Math.ceil,A6=Math.floor,Ap=Math.min,A7=0,A1=null,AW=null;function A_(A){return A}$.preloadedImages={},$.preloadedAudios={};var AT="data:application/octet-stream;base64,";function A2(A){return String.prototype.startsWith?A.startsWith(AT):0===A.indexOf(AT)}!function A(){var I="main.wast",g="main.wasm",B="main.temp.asm.js";A2(I)||(I=J(I)),A2(g)||(g=J(g)),A2(B)||(B=J(B));var E={global:null,env:null,asm2wasm:l,parent:$},Q=null;function C(A){return A}function i(){try{if($.wasmBinary)return new Uint8Array($.wasmBinary);if($.readBinary)return $.readBinary(g);throw"both async and sync fetching of the wasm failed"}catch(A){IE(A)}}$.asmPreload=$.asm;var h=$.reallocBuffer,o=function(A){A=An(A,$.usingWasm?65536:16777216);var I=$.buffer.byteLength;if($.usingWasm)try{var g=$.wasmMemory.grow((A-I)/65536);if(-1!==g)return $.buffer=$.wasmMemory.buffer;return null}catch(B){return null}};$.reallocBuffer=function(A){return"asmjs"===G?h(A):o(A)};var G="";$.asm=function(A,I,B){var C;if(!(I=C=I).table){var h,o=$.wasmTableSize;void 0===o&&(o=1024);var G=$.wasmMaxTableSize;"object"==typeof WebAssembly&&"function"==typeof WebAssembly.Table?void 0!==G?I.table=new WebAssembly.Table({initial:o,maximum:G,element:"anyfunc"}):I.table=new WebAssembly.Table({initial:o,element:"anyfunc"}):I.table=Array(o),$.wasmTable=I.table}return I.memoryBase||(I.memoryBase=$.STATIC_BASE),I.tableBase||(I.tableBase=0),h=function A(I,B,C){if("object"!=typeof WebAssembly)return H("no native wasm support detected"),!1;if(!($.wasmMemory instanceof WebAssembly.Memory))return H("no native wasm Memory in use"),!1;function h(A,I){if((Q=A.exports).memory){var g,B,E;g=Q.memory,B=$.buffer,g.byteLength0?g:Ah(A)+1,E=Array(B),Q=AC(A,E,0,E.length);return I&&(E.length=Q),E}function A4(A){for(var I=[],g=0;g255&&(B&=255),I.push(String.fromCharCode(B))}return I.join("")}S+=16,c=L(4),w=(R=s=d(S))+Ak,y=d(w),h[c>>2]=y,F=!0,$.wasmTableSize=4,$.wasmMaxTableSize=4,$.asmGlobalArg={},$.asmLibraryArg={abort:IE,assert:_,enlargeMemory:Ae,getTotalMemory:Ar,abortOnCannotGrowMemory:function A(){IE("Cannot enlarge memory arrays. Either (1) compile with -s TOTAL_MEMORY=X with X higher than the current value "+AN+", (2) compile with -s ALLOW_MEMORY_GROWTH=1 which allows increasing the size at runtime, or (3) if you want malloc to return NULL (0) instead of this abort, compile with -s ABORTING_MALLOC=0 ")},invoke_iii:function A(I,g,B){var E=IA();try{return $.dynCall_iii(I,g,B)}catch(Q){if(A9(E),"number"!=typeof Q&&"longjmp"!==Q)throw Q;$.setThrew(1,0)}},___assert_fail:function A(I,g,B,E){IE("Assertion failed: "+Ag(I)+", at: "+[g?Ag(g):"unknown filename",B,E?Ag(E):"unknown function",])},___setErrNo:function A(I){return $.___errno_location&&(h[$.___errno_location()>>2]=I),I},_abort:function A(){$.abort()},_emscripten_memcpy_big:function A(I,g,B){return Q.set(Q.subarray(g,g+B),I),I},_llvm_floor_f64:A6,DYNAMICTOP_PTR:c,tempDoublePtr:Av,ABORT:p,STACKTOP:s,STACK_MAX:w};var A3=$.asm($.asmGlobalArg,$.asmLibraryArg,B);$.asm=A3,$.___errno_location=function(){return $.asm.___errno_location.apply(null,arguments)};var Az=$._emscripten_replace_memory=function(){return $.asm._emscripten_replace_memory.apply(null,arguments)};$._free=function(){return $.asm._free.apply(null,arguments)};var A8=$._malloc=function(){return $.asm._malloc.apply(null,arguments)};$._memcpy=function(){return $.asm._memcpy.apply(null,arguments)},$._memset=function(){return $.asm._memset.apply(null,arguments)},$._sbrk=function(){return $.asm._sbrk.apply(null,arguments)},$._stb_vorbis_js_channels=function(){return $.asm._stb_vorbis_js_channels.apply(null,arguments)},$._stb_vorbis_js_close=function(){return $.asm._stb_vorbis_js_close.apply(null,arguments)},$._stb_vorbis_js_decode=function(){return $.asm._stb_vorbis_js_decode.apply(null,arguments)},$._stb_vorbis_js_open=function(){return $.asm._stb_vorbis_js_open.apply(null,arguments)},$._stb_vorbis_js_sample_rate=function(){return $.asm._stb_vorbis_js_sample_rate.apply(null,arguments)},$.establishStackSpace=function(){return $.asm.establishStackSpace.apply(null,arguments)},$.getTempRet0=function(){return $.asm.getTempRet0.apply(null,arguments)},$.runPostSets=function(){return $.asm.runPostSets.apply(null,arguments)},$.setTempRet0=function(){return $.asm.setTempRet0.apply(null,arguments)},$.setThrew=function(){return $.asm.setThrew.apply(null,arguments)};var A5=$.stackAlloc=function(){return $.asm.stackAlloc.apply(null,arguments)},A9=$.stackRestore=function(){return $.asm.stackRestore.apply(null,arguments)},IA=$.stackSave=function(){return $.asm.stackSave.apply(null,arguments)};function II(A){this.name="ExitStatus",this.message="Program terminated with exit("+A+")",this.status=A}function Ig(A){if(A=A||$.arguments,!(A7>0))!function A(){if($.preRun)for("function"==typeof $.preRun&&($.preRun=[$.preRun]);$.preRun.length;)Aq($.preRun.shift());AY(AJ)}(),!(A7>0)&&($.calledRun||($.setStatus?($.setStatus("Running..."),setTimeout(function(){setTimeout(function(){$.setStatus("")},1),I()},1)):I()));function I(){!$.calledRun&&($.calledRun=!0,p||(A0||(A0=!0,AY(Af)),AY(AH),$.onRuntimeInitialized&&$.onRuntimeInitialized(),function A(){if($.postRun)for("function"==typeof $.postRun&&($.postRun=[$.postRun]);$.postRun.length;)Ab($.postRun.shift());AY(AM)}()))}}function IB(A,I){(!I||!$.noExitRuntime||0!==A)&&($.noExitRuntime||(p=!0,W=A,s=U,AY(AL),Ad=!0,$.onExit&&$.onExit(A)),$.quit(A,new II(A)))}function IE(A){throw $.onAbort&&$.onAbort(A),void 0!==A?(f(A),H(A),A=JSON.stringify(A)):A="",p=!0,W=1,"abort("+A+"). Build with -s ASSERTIONS=1 for more info."}if($.dynCall_iii=function(){return $.asm.dynCall_iii.apply(null,arguments)},$.asm=A3,$.ccall=j,$.cwrap=function A(I,g,B,E){var Q=(B=B||[]).every(function(A){return"number"===A});return"string"!==g&&Q&&!E?T(I):function(){return j(I,g,B,arguments,E)}},II.prototype=Error(),II.prototype.constructor=II,AW=function A(){$.calledRun||Ig(),$.calledRun||(AW=A)},$.run=Ig,$.abort=IE,$.preInit)for("function"==typeof $.preInit&&($.preInit=[$.preInit]);$.preInit.length>0;)$.preInit.pop()();$.noExitRuntime=!0,Ig(),$.onRuntimeInitialized=()=>{isReady=!0,readySolver()},stbvorbis.decode=function(A){return function A(I){if(!isReady)throw Error("SF3 decoder has not been initialized yet. Did you await synth.isReady?");var g={};function B(A){return new Int32Array($.HEAPU8.buffer,A,1)[0]}function E(A,I){var g=new ArrayBuffer(I*Float32Array.BYTES_PER_ELEMENT),B=new Float32Array(g);return B.set(new Float32Array($.HEAPU8.buffer,A,I)),B}g.open=$.cwrap("stb_vorbis_js_open","number",[]),g.close=$.cwrap("stb_vorbis_js_close","void",["number"]),g.channels=$.cwrap("stb_vorbis_js_channels","number",["number"]),g.sampleRate=$.cwrap("stb_vorbis_js_sample_rate","number",["number"]),g.decode=$.cwrap("stb_vorbis_js_decode","number",["number","number","number","number","number"]);var Q,C,i,h,o=g.open(),G=(Q=I,C=I.byteLength,i=$._malloc(C),(h=new Uint8Array($.HEAPU8.buffer,i,C)).set(new Uint8Array(Q,0,C)),h),D=$._malloc(4),a=$._malloc(4),S=g.decode(o,G.byteOffset,G.byteLength,D,a);if($._free(G.byteOffset),S<0)throw g.close(o),$._free(D),Error("stbvorbis decode failed: "+S);for(var F=g.channels(o),R=Array(F),s=new Int32Array($.HEAPU32.buffer,B(D),F),w=0;w [...track]); // Shallow copy of each track array + + return m; + } + + /** + * Parses internal MIDI values + * @protected + */ + _parseInternal() + { + SpessaSynthGroup( + "%cInterpreting MIDI events...", + consoleColors.info + ); + /** + * For karaoke files, text events starting with @T are considered titles, + * usually the first one is the title, and the latter is things such as "sequenced by" etc. + * @type {boolean} + */ + let karaokeHasTitle = false; + + this.keyRange = { max: 0, min: 127 }; + + /** + * Will be joined with "\n" to form the final string + * @type {string[]} + */ + let copyrightComponents = []; + let copyrightDetected = false; + if (typeof this.RMIDInfo["ICOP"] !== "undefined") + { + // if RMIDI has copyright info, don't try to detect one. + copyrightDetected = true; + } + + + let nameDetected = false; + if (typeof this.RMIDInfo["INAM"] !== "undefined") + { + // same as with copyright + nameDetected = true; + } + + // loop tracking + let loopStart = null; + let loopEnd = null; + + for (let i = 0; i < this.tracks.length; i++) + { + /** + * @type {MIDIMessage[]} + */ + const track = this.tracks[i]; + const usedChannels = new Set(); + let trackHasVoiceMessages = false; + + for (const e of track) + { + // check if it's a voice message + if (e.messageStatusByte >= 0x80 && e.messageStatusByte < 0xF0) + { + trackHasVoiceMessages = true; + // voice messages are 7-bit always + for (let j = 0; j < e.messageData.length; j++) + { + e.messageData[j] = Math.min(127, e.messageData[j]); + } + // last voice event tick + if (e.ticks > this.lastVoiceEventTick) + { + this.lastVoiceEventTick = e.ticks; + } + + // interpret the voice message + switch (e.messageStatusByte & 0xF0) + { + // cc change: loop points + case messageTypes.controllerChange: + switch (e.messageData[0]) + { + case 2: + case 116: + loopStart = e.ticks; + break; + + case 4: + case 117: + if (loopEnd === null) + { + loopEnd = e.ticks; + } + else + { + // this controller has occurred more than once; + // this means + // that it doesn't indicate the loop + loopEnd = 0; + } + break; + + case 0: + // check RMID + if (this.isDLSRMIDI && e.messageData[1] !== 0 && e.messageData[1] !== 127) + { + SpessaSynthInfo( + "%cDLS RMIDI with offset 1 detected!", + consoleColors.recognized + ); + this.bankOffset = 1; + } + } + break; + + // note on: used notes tracking and key range + case messageTypes.noteOn: + usedChannels.add(e.messageStatusByte & 0x0F); + const note = e.messageData[0]; + this.keyRange.min = Math.min(this.keyRange.min, note); + this.keyRange.max = Math.max(this.keyRange.max, note); + break; + } + } + e.messageData.currentIndex = 0; + const eventText = readBytesAsString(e.messageData, e.messageData.length); + e.messageData.currentIndex = 0; + // interpret the message + switch (e.messageStatusByte) + { + case messageTypes.setTempo: + // add the tempo change + e.messageData.currentIndex = 0; + this.tempoChanges.push({ + ticks: e.ticks, + tempo: 60000000 / readBytesAsUintBigEndian(e.messageData, 3) + }); + e.messageData.currentIndex = 0; + break; + + case messageTypes.marker: + // check for loop markers + const text = eventText.trim().toLowerCase(); + switch (text) + { + default: + break; + + case "start": + case "loopstart": + loopStart = e.ticks; + break; + + case "loopend": + loopEnd = e.ticks; + } + e.messageData.currentIndex = 0; + break; + + case messageTypes.copyright: + if (!copyrightDetected) + { + e.messageData.currentIndex = 0; + copyrightComponents.push(readBytesAsString( + e.messageData, + e.messageData.length, + undefined, + false + )); + e.messageData.currentIndex = 0; + } + break; + + case messageTypes.lyric: + // note here: .kar files sometimes just use... + // lyrics instead of text because why not (of course) + // perform the same check for @KMIDI KARAOKE FILE + if (eventText.trim().startsWith("@KMIDI KARAOKE FILE")) + { + this.isKaraokeFile = true; + SpessaSynthInfo("%cKaraoke MIDI detected!", consoleColors.recognized); + } + + if (this.isKaraokeFile) + { + // replace the type of the message with text + e.messageStatusByte = messageTypes.text; + } + else + { + // add lyrics like a regular midi file + this.lyrics.push(e.messageData); + this.lyricsTicks.push(e.ticks); + break; + } + + // kar: treat the same as text + // fallthrough + case messageTypes.text: + // possibly Soft Karaoke MIDI file + // it has a text event at the start of the file + // "@KMIDI KARAOKE FILE" + const checkedText = eventText.trim(); + if (checkedText.startsWith("@KMIDI KARAOKE FILE")) + { + this.isKaraokeFile = true; + + SpessaSynthInfo("%cKaraoke MIDI detected!", consoleColors.recognized); + } + else if (this.isKaraokeFile) + { + // check for @T (title) + // or @A because it is a title too sometimes? + // IDK it's strange + if (checkedText.startsWith("@T") || checkedText.startsWith("@A")) + { + if (!karaokeHasTitle) + { + this.midiName = checkedText.substring(2).trim(); + karaokeHasTitle = true; + nameDetected = true; + // encode to rawMidiName + this.rawMidiName = getStringBytes(this.midiName); + } + else + { + // append to copyright + copyrightComponents.push(checkedText.substring(2).trim()); + } + } + else if (checkedText[0] !== "@") + { + // non @: the lyrics + this.lyrics.push(sanitizeKarLyrics(e.messageData)); + this.lyricsTicks.push(e.ticks); + } + } + break; + + case messageTypes.trackName: + break; + } + } + // add used channels + this.usedChannelsOnTrack.push(usedChannels); + + // track name + this.trackNames[i] = ""; + const trackName = track.find(e => e.messageStatusByte === messageTypes.trackName); + if (trackName) + { + trackName.messageData.currentIndex = 0; + const name = readBytesAsString(trackName.messageData, trackName.messageData.length); + this.trackNames[i] = name; + // If the track has no voice messages, its "track name" event (if it has any) + // is some metadata. + // Add it to copyright + if (!trackHasVoiceMessages) + { + copyrightComponents.push(name); + } + } + } + + // reverse the tempo changes + this.tempoChanges.reverse(); + + SpessaSynthInfo( + `%cCorrecting loops, ports and detecting notes...`, + consoleColors.info + ); + + const firstNoteOns = []; + for (const t of this.tracks) + { + const firstNoteOn = t.find(e => (e.messageStatusByte & 0xF0) === messageTypes.noteOn); + if (firstNoteOn) + { + firstNoteOns.push(firstNoteOn.ticks); + } + } + this.firstNoteOn = Math.min(...firstNoteOns); + + SpessaSynthInfo( + `%cFirst note-on detected at: %c${this.firstNoteOn}%c ticks!`, + consoleColors.info, + consoleColors.recognized, + consoleColors.info + ); + + + if (loopStart !== null && loopEnd === null) + { + // not a loop + loopStart = this.firstNoteOn; + loopEnd = this.lastVoiceEventTick; + } + else + { + if (loopStart === null) + { + loopStart = this.firstNoteOn; + } + + 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}`, + consoleColors.info, + consoleColors.recognized, + consoleColors.info, + consoleColors.recognized + ); + + // determine ports + let portOffset = 0; + this.midiPorts = []; + this.midiPortChannelOffsets = []; + for (let trackNum = 0; trackNum < this.tracks.length; trackNum++) + { + this.midiPorts.push(-1); + if (this.usedChannelsOnTrack[trackNum].size === 0) + { + continue; + } + for (const e of this.tracks[trackNum]) + { + if (e.messageStatusByte !== messageTypes.midiPort) + { + continue; + } + const port = e.messageData[0]; + this.midiPorts[trackNum] = port; + if (this.midiPortChannelOffsets[port] === undefined) + { + this.midiPortChannelOffsets[port] = portOffset; + portOffset += 16; + } + } + } + + // 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 would we 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 the additional 16 for the actual ports. + let defaultPort = Infinity; + for (let port of this.midiPorts) + { + if (port !== -1) + { + if (defaultPort > port) + { + defaultPort = port; + } + } + } + if (defaultPort === Infinity) + { + defaultPort = 0; + } + this.midiPorts = this.midiPorts.map(port => port === -1 ? defaultPort : port); + // add fake port if empty + if (this.midiPortChannelOffsets.length === 0) + { + this.midiPortChannelOffsets = [0]; + } + if (this.midiPortChannelOffsets.length < 2) + { + SpessaSynthInfo(`%cNo additional MIDI Ports detected.`, consoleColors.info); + } + else + { + this.isMultiPort = true; + SpessaSynthInfo(`%cMIDI Ports detected!`, consoleColors.recognized); + } + + // midi name + 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 + ) === undefined + ) + { + + let name = this.tracks[0].find(message => message.messageStatusByte === messageTypes.trackName); + if (name) + { + this.rawMidiName = name.messageData; + name.messageData.currentIndex = 0; + this.midiName = readBytesAsString(name.messageData, name.messageData.length, undefined, false); + } + } + } + else + { + // if only 1 track, find the first "track name" event + let name = this.tracks[0].find(message => message.messageStatusByte === messageTypes.trackName); + if (name) + { + this.rawMidiName = name.messageData; + name.messageData.currentIndex = 0; + this.midiName = readBytesAsString(name.messageData, name.messageData.length, undefined, false); + } + } + } + + if (!copyrightDetected) + { + this.copyright = copyrightComponents + // trim and group newlines into one + .map(c => c.trim().replace(/(\r?\n)+/g, "\n")) + // remove empty strings + .filter(c => c.length > 0) + // join with newlines + .join("\n") || ""; + } + + this.midiName = this.midiName.trim(); + this.midiNameUsesFileName = false; + // if midiName is "", use the file name + if (this.midiName.length === 0) + { + SpessaSynthInfo( + `%cNo name detected. Using the alt name!`, + consoleColors.info + ); + this.midiName = formatTitle(this.fileName); + this.midiNameUsesFileName = true; + // encode it too + this.rawMidiName = new Uint8Array(this.midiName.length); + for (let i = 0; i < this.midiName.length; i++) + { + this.rawMidiName[i] = this.midiName.charCodeAt(i); + } + } + else + { + SpessaSynthInfo( + `%cMIDI Name detected! %c"${this.midiName}"`, + consoleColors.info, + consoleColors.recognized + ); + } + + // if the first event is not at 0 ticks, add a track name + // https://github.com/spessasus/SpessaSynth/issues/145 + if (!this.tracks.some(t => t[0].ticks === 0)) + { + const track = this.tracks[0]; + // can copy + track.unshift(new MIDIMessage( + 0, + messageTypes.trackName, + new IndexedByteArray(this.rawMidiName.buffer) + )); + } + + + /** + * The total playback time, in seconds + * @type {number} + */ + this.duration = this.MIDIticksToSeconds(this.lastVoiceEventTick); + + SpessaSynthInfo("%cSuccess!", consoleColors.recognized); + SpessaSynthGroupEnd(); + } + + /** + * Updates all internal values + */ + flush() + { + + for (const t of this.tracks) + { + // sort the track by ticks + t.sort((e1, e2) => e1.ticks - e2.ticks); + } + this._parseInternal(); + } +} + +BasicMIDI.prototype.writeMIDI = writeMIDI; +BasicMIDI.prototype.modifyMIDI = modifyMIDI; +BasicMIDI.prototype.applySnapshotToMIDI = applySnapshotToMIDI; +BasicMIDI.prototype.writeRMIDI = writeRMIDI; +BasicMIDI.prototype.getUsedProgramsAndKeys = getUsedProgramsAndKeys; + +export { BasicMIDI }; \ No newline at end of file diff --git a/spessasynth_lib/midi_parser/midi_builder.js b/spessasynth_lib/midi_parser/midi_builder.js new file mode 100644 index 0000000000000000000000000000000000000000..e3662b6dd2d349bcdeca8caf67273e8235fd77c2 --- /dev/null +++ b/spessasynth_lib/midi_parser/midi_builder.js @@ -0,0 +1,202 @@ +import { BasicMIDI } from "./basic_midi.js"; +import { messageTypes, MIDIMessage } from "./midi_message.js"; +import { IndexedByteArray } from "../utils/indexed_array.js"; +import { SpessaSynthWarn } from "../utils/loggin.js"; + +/** + * A class that helps to build a MIDI file from scratch. + */ +export class MIDIBuilder extends BasicMIDI +{ + /** + * @param name {string} The MIDI's name + * @param timeDivision {number} the file's time division + * @param initialTempo {number} the file's initial tempo + */ + constructor(name, timeDivision = 480, initialTempo = 120) + { + super(); + this.timeDivision = timeDivision; + 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); + } + + /** + * Adds a new Set Tempo event + * @param ticks {number} the tick number of the event + * @param tempo {number} the tempo in beats per minute (BPM) + */ + 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 + * @param port {number} the new track's port + */ + addNewTrack(name, port = 0) + { + this.tracksAmount++; + if (this.tracksAmount > 1) + { + this.format = 1; + } + this.tracks.push([]); + this.tracks[this.tracksAmount - 1].push( + new MIDIMessage(0, messageTypes.endOfTrack, new IndexedByteArray(0)) + ); + 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 + * @param track {number} the track number to use + * @param event {number} the MIDI event number + * @param eventData {Uint8Array|Iterable} the raw event data + */ + addEvent(ticks, track, event, eventData) + { + if (!this.tracks[track]) + { + throw new Error(`Track ${track} does not exist. Add it via addTrack method.`); + } + if (event === messageTypes.endOfTrack) + { + SpessaSynthWarn( + "The EndOfTrack is added automatically and does not influence the duration. Consider adding a voice event instead."); + return; + } + // remove the end of track + this.tracks[track].pop(); + this.tracks[track].push(new MIDIMessage( + ticks, + event, + new IndexedByteArray(eventData) + )); + // add the end of track + this.tracks[track].push(new MIDIMessage( + ticks, + messageTypes.endOfTrack, + new IndexedByteArray(0) + )); + } + + /** + * Adds a new Note On event + * @param ticks {number} the tick time of the event + * @param track {number} the track number to use + * @param channel {number} the channel to use + * @param midiNote {number} the midi note of the keypress + * @param velocity {number} the velocity of the keypress + */ + addNoteOn(ticks, track, channel, midiNote, velocity) + { + channel %= 16; + midiNote %= 128; + velocity %= 128; + this.addEvent( + ticks, + track, + messageTypes.noteOn | channel, + [midiNote, velocity] + ); + } + + /** + * Adds a new Note Off event + * @param ticks {number} the tick time of the event + * @param track {number} the track number to use + * @param channel {number} the channel to use + * @param midiNote {number} the midi note of the key release + */ + addNoteOff(ticks, track, channel, midiNote) + { + channel %= 16; + midiNote %= 128; + this.addEvent( + ticks, + track, + messageTypes.noteOff | channel, + [midiNote, 64] + ); + } + + /** + * Adds a new Program Change event + * @param ticks {number} the tick time of the event + * @param track {number} the track number to use + * @param channel {number} the channel to use + * @param programNumber {number} the MIDI program to use + */ + addProgramChange(ticks, track, channel, programNumber) + { + channel %= 16; + programNumber %= 128; + this.addEvent( + ticks, + track, + messageTypes.programChange | channel, + [programNumber] + ); + } + + /** + * Adds a new Controller Change event + * @param ticks {number} the tick time of the event + * @param track {number} the track number to use + * @param channel {number} the channel to use + * @param controllerNumber {number} the MIDI CC to use + * @param controllerValue {number} the new CC value + */ + addControllerChange(ticks, track, channel, controllerNumber, controllerValue) + { + channel %= 16; + controllerNumber %= 128; + controllerValue %= 128; + this.addEvent( + ticks, + track, + messageTypes.controllerChange | channel, + [controllerNumber, controllerValue] + ); + } + + /** + * Adds a new Pitch Wheel event + * @param ticks {number} the tick time of the event + * @param track {number} the track to use + * @param channel {number} the channel to use + * @param MSB {number} SECOND byte of the MIDI pitchWheel message + * @param LSB {number} FIRST byte of the MIDI pitchWheel message + */ + addPitchWheel(ticks, track, channel, MSB, LSB) + { + channel %= 16; + MSB %= 128; + LSB %= 128; + this.addEvent( + ticks, + track, + messageTypes.pitchBend | channel, + [LSB, MSB] + ); + } +} \ No newline at end of file diff --git a/spessasynth_lib/midi_parser/midi_data.js b/spessasynth_lib/midi_parser/midi_data.js new file mode 100644 index 0000000000000000000000000000000000000000..183b4d56fe103af819e76fc646fc43bec7da5d0d --- /dev/null +++ b/spessasynth_lib/midi_parser/midi_data.js @@ -0,0 +1,63 @@ +import { MIDISequenceData } from "./midi_sequence.js"; + +/** + * A simplified version of the MIDI, accessible at all times from the Sequencer. + * Use getMIDI() to get the actual sequence. + * This class contains all properties that MIDI does, except for tracks and the embedded soundfont. + */ +export class MIDIData extends MIDISequenceData +{ + + /** + * A boolean indicating if the MIDI file contains an embedded soundfont. + * If the embedded soundfont is undefined, this will be false. + * @type {boolean} + */ + isEmbedded = false; + + /** + * Constructor that copies data from a BasicMIDI instance. + * @param {BasicMIDI} midi - The BasicMIDI instance to copy data from. + */ + constructor(midi) + { + super(); + this._copyFromSequence(midi); + + // Set isEmbedded based on the presence of an embeddedSoundFont + this.isEmbedded = midi.embeddedSoundFont !== undefined; + } +} + + +/** + * Temporary MIDI data used when the MIDI is not loaded. + * @type {MIDIData} + */ +export const DUMMY_MIDI_DATA = { + duration: 99999, + firstNoteOn: 0, + loop: { + start: 0, + end: 123456 + }, + + lastVoiceEventTick: 123456, + lyrics: [], + copyright: "", + midiPorts: [], + midiPortChannelOffsets: [], + tracksAmount: 0, + 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 }, + isEmbedded: false, + RMIDInfo: {}, + bankOffset: 0, + midiNameUsesFileName: false, + format: 0 +}; \ No newline at end of file diff --git a/spessasynth_lib/midi_parser/midi_editor.js b/spessasynth_lib/midi_parser/midi_editor.js new file mode 100644 index 0000000000000000000000000000000000000000..1eaddc7c7505cd9f60e90bfdd0b58a261e9cfdfb --- /dev/null +++ b/spessasynth_lib/midi_parser/midi_editor.js @@ -0,0 +1,611 @@ +import { messageTypes, midiControllers, MIDIMessage } from "./midi_message.js"; +import { IndexedByteArray } from "../utils/indexed_array.js"; +import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from "../utils/loggin.js"; +import { consoleColors } from "../utils/other.js"; + +import { customControllers } from "../synthetizer/worklet_system/worklet_utilities/controller_tables.js"; +import { DEFAULT_PERCUSSION } from "../synthetizer/synth_constants.js"; +import { isGM2On, isGMOn, isGSOn, isXGOn } from "../utils/sysex_detector.js"; +import { isSystemXG, isXGDrums, XG_SFX_VOICE } from "../utils/xg_hacks.js"; + +/** + * @param ticks {number} + * @returns {MIDIMessage} + */ +export function getGsOn(ticks) +{ + return new MIDIMessage( + ticks, + messageTypes.systemExclusive, + new IndexedByteArray([ + 0x41, // Roland + 0x10, // Device ID (defaults to 16 on roland) + 0x42, // GS + 0x12, // Command ID (DT1) (whatever that means...) + 0x40, // System parameter - Address + 0x00, // Global parameter - Address + 0x7F, // GS Change - Address + 0x00, // turn on - Data + 0x41, // checksum + 0xF7 // end of exclusive + ]) + ); +} + +/** + * @param channel {number} + * @param cc {number} + * @param value {number} + * @param ticks {number} + * @returns {MIDIMessage} + */ +function getControllerChange(channel, cc, value, ticks) +{ + return new MIDIMessage( + ticks, + messageTypes.controllerChange | (channel % 16), + new IndexedByteArray([cc, value]) + ); +} + +/** + * @param channel {number} + * @param ticks {number} + * @returns {MIDIMessage} + */ +function getDrumChange(channel, ticks) +{ + const chanAddress = 0x10 | [1, 2, 3, 4, 5, 6, 7, 8, 0, 9, 10, 11, 12, 13, 14, 15][channel % 16]; + // excluding manufacturerID DeviceID and ModelID (and F7) + const sysexData = [ + 0x41, // Roland + 0x10, // Device ID (defaults to 16 on roland) + 0x42, // GS + 0x12, // Command ID (DT1) (whatever that means...) + 0x40, // System parameter } + chanAddress, // Channel parameter } Address + 0x15, // Drum change } + 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; + const checksum = 128 - (sum % 128); + // add system exclusive to enable drums + return new MIDIMessage( + ticks, + messageTypes.systemExclusive, + new IndexedByteArray([ + ...sysexData, + checksum, + 0xF7 + ]) + ); +} + +/** + * @typedef {Object} DesiredProgramChange + * @property {number} channel - The channel number. + * @property {number} program - The program number. + * @property {number} bank - The bank number. + * @property {boolean} isDrum - Indicates if the channel is a drum channel. + * If it is, then the bank number is ignored. + */ + +/** + * @typedef {Object} DesiredControllerChange + * @property {number} channel - The channel number. + * @property {number} controllerNumber - The MIDI controller number. + * @property {number} controllerValue - The new controller value. + */ + +/** + * @typedef {Object} DesiredChanneltranspose + * @property {number} channel - The channel number. + * @property {number} keyShift - The number of semitones to transpose. + * Note that this can use floating point numbers, + * which will be used to fine-tune the pitch in cents using RPN. + */ + + +/** + * Allows easy editing of the file by removing channels, changing programs, + * changing controllers and transposing channels. Note that this modifies the MIDI in-place. + * + * @this {BasicMIDI} + * @param {DesiredProgramChange[]} desiredProgramChanges - The programs to set on given channels. + * @param {DesiredControllerChange[]} desiredControllerChanges - The controllers to set on given channels. + * @param {number[]} desiredChannelsToClear - The channels to remove from the sequence. + * @param {DesiredChanneltranspose[]} desiredChannelsToTranspose - The channels to transpose. + */ +export function modifyMIDI( + desiredProgramChanges = [], + desiredControllerChanges = [], + desiredChannelsToClear = [], + desiredChannelsToTranspose = [] +) +{ + const midi = this; + SpessaSynthGroupCollapsed("%cApplying changes to the MIDI file...", consoleColors.info); + + SpessaSynthInfo("Desired program changes:", desiredProgramChanges); + SpessaSynthInfo("Desired CC changes:", desiredControllerChanges); + SpessaSynthInfo("Desired channels to clear:", desiredChannelsToClear); + SpessaSynthInfo("Desired channels to transpose:", desiredChannelsToTranspose); + + /** + * @type {Set} + */ + const channelsToChangeProgram = new Set(); + desiredProgramChanges.forEach(c => + { + channelsToChangeProgram.add(c.channel); + }); + + + // go through all events one by one + let system = "gs"; + let addedGs = false; + /** + * indexes for tracks + * @type {number[]} + */ + const eventIndexes = Array(midi.tracks.length).fill(0); + let remainingTracks = midi.tracks.length; + + function findFirstEventIndex() + { + let index = 0; + let ticks = Infinity; + midi.tracks.forEach((track, i) => + { + if (eventIndexes[i] >= track.length) + { + return; + } + 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! + /** + * midi port number for the corresponding track + * @type {number[]} + */ + const midiPorts = midi.midiPorts.slice(); + /** + * midi port: channel offset + * @type {Object} + */ + const midiPortChannelOffsets = {}; + let midiPortChannelOffset = 0; + + function assignMIDIPort(trackNum, port) + { + // do not assign ports to empty tracks + if (midi.usedChannelsOnTrack[trackNum].size === 0) + { + return; + } + + // assign new 16 channels if the port is not occupied yet + if (midiPortChannelOffset === 0) + { + midiPortChannelOffset += 16; + midiPortChannelOffsets[port] = 0; + } + + if (midiPortChannelOffsets[port] === undefined) + { + midiPortChannelOffsets[port] = midiPortChannelOffset; + midiPortChannelOffset += 16; + } + + midiPorts[trackNum] = port; + } + + // assign port offsets + midi.midiPorts.forEach((port, trackIndex) => + { + assignMIDIPort(trackIndex, port); + }); + + const channelsAmount = midiPortChannelOffset; + /** + * Tracks if the channel already had its first note on + * @type {boolean[]} + */ + const isFirstNoteOn = Array(channelsAmount).fill(true); + + /** + * MIDI key transpose + * @type {number[]} + */ + const coarseTranspose = Array(channelsAmount).fill(0); + /** + * RPN fine transpose + * @type {number[]} + */ + const fineTranspose = Array(channelsAmount).fill(0); + desiredChannelsToTranspose.forEach(transpose => + { + const coarse = Math.trunc(transpose.keyShift); + const fine = transpose.keyShift - coarse; + coarseTranspose[transpose.channel] = coarse; + fineTranspose[transpose.channel] = fine; + }); + + while (remainingTracks > 0) + { + let trackNum = findFirstEventIndex(); + const track = midi.tracks[trackNum]; + if (eventIndexes[trackNum] >= track.length) + { + remainingTracks--; + continue; + } + const index = eventIndexes[trackNum]++; + const e = track[index]; + + const deleteThisEvent = () => + { + track.splice(index, 1); + eventIndexes[trackNum]--; + }; + + /** + * @param e {MIDIMessage} + * @param offset{number} + */ + const addEventBefore = (e, offset = 0) => + { + track.splice(index + offset, 0, e); + eventIndexes[trackNum]++; + }; + + + let portOffset = midiPortChannelOffsets[midiPorts[trackNum]] || 0; + if (e.messageStatusByte === messageTypes.midiPort) + { + assignMIDIPort(trackNum, e.messageData[0]); + continue; + } + // don't clear meta + if (e.messageStatusByte <= messageTypes.sequenceSpecific && e.messageStatusByte >= messageTypes.sequenceNumber) + { + continue; + } + const status = e.messageStatusByte & 0xF0; + const midiChannel = e.messageStatusByte & 0xF; + const channel = midiChannel + portOffset; + // clear channel? + if (desiredChannelsToClear.indexOf(channel) !== -1) + { + deleteThisEvent(); + continue; + } + switch (status) + { + case messageTypes.noteOn: + // is it first? + if (isFirstNoteOn[channel]) + { + isFirstNoteOn[channel] = false; + // all right, so this is the first note on + // first: controllers + // because FSMP does not like program changes after cc changes in embedded midis + // and since we use splice, + // controllers get added first, then programs before them + // now add controllers + desiredControllerChanges.filter(c => c.channel === channel).forEach(change => + { + const ccChange = getControllerChange( + midiChannel, + change.controllerNumber, + change.controllerValue, + e.ticks + ); + addEventBefore(ccChange); + }); + const fineTune = fineTranspose[channel]; + + if (fineTune !== 0) + { + // add rpn + // 64 is the center, 96 = 50 cents up + const centsCoarse = (fineTune * 64) + 64; + const rpnCoarse = getControllerChange(midiChannel, midiControllers.RPNMsb, 0, e.ticks); + const rpnFine = getControllerChange(midiChannel, midiControllers.RPNLsb, 1, e.ticks); + const dataEntryCoarse = getControllerChange( + channel, + midiControllers.dataEntryMsb, + centsCoarse, + e.ticks + ); + const dataEntryFine = getControllerChange( + midiChannel, + midiControllers.lsbForControl6DataEntry, + 0, + e.ticks + ); + addEventBefore(dataEntryFine); + addEventBefore(dataEntryCoarse); + addEventBefore(rpnFine); + addEventBefore(rpnCoarse); + + } + + if (channelsToChangeProgram.has(channel)) + { + const change = desiredProgramChanges.find(c => c.channel === channel); + let desiredBank = Math.max(0, Math.min(change.bank, 127)); + const desiredProgram = change.program; + SpessaSynthInfo( + `%cSetting %c${change.channel}%c to %c${desiredBank}:${desiredProgram}%c. Track num: %c${trackNum}`, + consoleColors.info, + consoleColors.recognized, + consoleColors.info, + consoleColors.recognized, + consoleColors.info, + consoleColors.recognized + ); + + // note: this is in reverse. + // the output event order is: drums -> lsb -> msb -> program change + + // add program change + const programChange = new MIDIMessage( + e.ticks, + messageTypes.programChange | midiChannel, + new IndexedByteArray([ + desiredProgram + ]) + ); + addEventBefore(programChange); + + const addBank = (isLSB, v) => + { + const bankChange = getControllerChange( + midiChannel, + isLSB ? midiControllers.lsbForControl0BankSelect : midiControllers.bankSelect, + v, + e.ticks + ); + addEventBefore(bankChange); + }; + + // on xg, add lsb + if (isSystemXG(system)) + { + // xg drums: msb can be 120, 126 or 127 + if (change.isDrum) + { + SpessaSynthInfo( + `%cAdding XG Drum change on track %c${trackNum}`, + consoleColors.recognized, + consoleColors.value + ); + addBank(false, isXGDrums(desiredBank) ? desiredBank : 127); + addBank(true, 0); + } + else + { + // sfx voice is set via MSB + if (desiredBank === XG_SFX_VOICE) + { + addBank(false, XG_SFX_VOICE); + addBank(true, 0); + } + else + { + // add variation as LSB + addBank(false, 0); + addBank(true, desiredBank); + } + } + } + else + { + // add just msb + addBank(false, desiredBank); + + if (change.isDrum && midiChannel !== DEFAULT_PERCUSSION) + { + // add gs drum change + SpessaSynthInfo( + `%cAdding GS Drum change on track %c${trackNum}`, + consoleColors.recognized, + consoleColors.value + ); + addEventBefore(getDrumChange(midiChannel, e.ticks)); + } + } + } + } + // transpose key (for zero it won't change anyway) + e.messageData[0] += coarseTranspose[channel]; + break; + + case messageTypes.noteOff: + e.messageData[0] += coarseTranspose[channel]; + break; + + case messageTypes.programChange: + // do we delete it? + if (channelsToChangeProgram.has(channel)) + { + // this channel has program change. BEGONE! + deleteThisEvent(); + continue; + } + break; + + case messageTypes.controllerChange: + const ccNum = e.messageData[0]; + const changes = desiredControllerChanges.find(c => c.channel === channel && ccNum === c.controllerNumber); + if (changes !== undefined) + { + // this controller is locked, BEGONE CHANGE! + deleteThisEvent(); + continue; + } + // bank maybe? + if (ccNum === midiControllers.bankSelect || ccNum === midiControllers.lsbForControl0BankSelect) + { + if (channelsToChangeProgram.has(channel)) + { + // BEGONE! + deleteThisEvent(); + continue; + } + } + break; + + case messageTypes.systemExclusive: + // check for xg on + if (isXGOn(e)) + { + SpessaSynthInfo("%cXG system on detected", consoleColors.info); + system = "xg"; + addedGs = true; // flag as true so gs won't get added + } + else + // check for xg program change + if ( + e.messageData[0] === 0x43 // yamaha + && e.messageData[2] === 0x4C // XG + && e.messageData[3] === 0x08 // part parameter + && e.messageData[5] === 0x03 // program change + ) + { + // do we delete it? + if (channelsToChangeProgram.has(e.messageData[4] + portOffset)) + { + // this channel has program change. BEGONE! + deleteThisEvent(); + } + } + else + // check for GS on + if (isGSOn(e)) + { + // that's a GS on, we're done here + addedGs = true; + SpessaSynthInfo( + "%cGS on detected!", + consoleColors.recognized + ); + break; + } + else + // check for GM/2 on + if (isGMOn(e) || isGM2On(e)) + { + // that's a GM1 system change, remove it! + SpessaSynthInfo( + "%cGM/2 on detected, removing!", + consoleColors.info + ); + deleteThisEvent(); + addedGs = false; + } + } + } + // check for gs + if (!addedGs && desiredProgramChanges.length > 0) + { + // 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) + { + index++; + } + midi.tracks[0].splice(index, 0, getGsOn(0)); + SpessaSynthInfo("%cGS on not detected. Adding it.", consoleColors.info); + } + this.flush(); + SpessaSynthGroupEnd(); +} + +/** + * Modifies the sequence according to the locked presets and controllers in the given snapshot + * @this {BasicMIDI} + * @param snapshot {SynthesizerSnapshot} + */ +export function applySnapshotToMIDI(snapshot) +{ + /** + * @type {{ + * channel: number, + * keyShift: number + * }[]} + */ + const channelsToTranspose = []; + /** + * @type {number[]} + */ + const channelsToClear = []; + /** + * @type {{ + * channel: number, + * program: number, + * bank: number, + * isDrum: boolean + * }[]} + */ + const programChanges = []; + /** + * + * @type {{ + * channel: number, + * controllerNumber: number, + * controllerValue: number + * }[]} + */ + const controllerChanges = []; + snapshot.channelSnapshots.forEach((channel, channelNumber) => + { + if (channel.isMuted) + { + channelsToClear.push(channelNumber); + return; + } + const transposeFloat = channel.channelTransposeKeyShift + channel.customControllers[customControllers.channelTransposeFine] / 100; + if (transposeFloat !== 0) + { + channelsToTranspose.push({ + channel: channelNumber, + keyShift: transposeFloat + }); + } + if (channel.lockPreset) + { + programChanges.push({ + channel: channelNumber, + program: channel.program, + bank: channel.bank, + isDrum: channel.drumChannel + }); + } + // check for locked controllers and change them appropriately + channel.lockedControllers.forEach((l, ccNumber) => + { + if (!l || ccNumber > 127 || ccNumber === midiControllers.bankSelect) + { + return; + } + const targetValue = channel.midiControllers[ccNumber] >> 7; // channel controllers are stored as 14 bit values + controllerChanges.push({ + channel: channelNumber, + controllerNumber: ccNumber, + controllerValue: targetValue + }); + }); + }); + this.modifyMIDI(programChanges, controllerChanges, channelsToClear, channelsToTranspose); +} \ No newline at end of file diff --git a/spessasynth_lib/midi_parser/midi_loader.js b/spessasynth_lib/midi_parser/midi_loader.js new file mode 100644 index 0000000000000000000000000000000000000000..14f67a7228399a97da77daa5d0b451ff917b4736 --- /dev/null +++ b/spessasynth_lib/midi_parser/midi_loader.js @@ -0,0 +1,324 @@ +import { dataBytesAmount, getChannel, MIDIMessage } from "./midi_message.js"; +import { IndexedByteArray } from "../utils/indexed_array.js"; +import { consoleColors } 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 } from "./basic_midi.js"; +import { loadXMF } from "./xmf_loader.js"; + +/** + * midi_loader.js + * purpose: + * parses a midi file for the seqyencer, + * including things like marker or CC 2/4 loop detection, copyright detection, etc. + */ + +/** + * The MIDI class is a MIDI file parser that reads a MIDI file and extracts all the necessary information from it. + * Supported formats are .mid and .rmi files. + */ +class MIDI extends BasicMIDI +{ + /** + * Parses a given midi file + * @param arrayBuffer {ArrayBuffer} + * @param fileName {string} optional, replaces the decoded title if empty + */ + constructor(arrayBuffer, fileName = "") + { + super(); + SpessaSynthGroupCollapsed(`%cParsing MIDI File...`, consoleColors.info); + this.fileName = fileName; + const binaryData = new IndexedByteArray(arrayBuffer); + let fileByteArray; + + // check for rmid + const initialString = readBytesAsString(binaryData, 4); + binaryData.currentIndex -= 4; + 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") + { + SpessaSynthGroupEnd(); + throw new SyntaxError(`Invalid RMIDI Header! Expected "RMID", got "${rmid}"`); + } + const riff = readRIFFChunk(binaryData); + if (riff.header !== "data") + { + SpessaSynthGroupEnd(); + throw new SyntaxError(`Invalid RMIDI Chunk header! Expected "data", got "${rmid}"`); + } + // this is a rmid, load the midi into an array for parsing + fileByteArray = riff.chunkData; + + // keep loading chunks until we get the "SFBK" header + while (binaryData.currentIndex <= binaryData.length) + { + const startIndex = binaryData.currentIndex; + const currentChunk = readRIFFChunk(binaryData, true); + if (currentChunk.header === "RIFF") + { + const type = readBytesAsString(currentChunk.chunkData, 4).toLowerCase(); + if (type === "sfbk" || type === "sfpk" || type === "dls ") + { + SpessaSynthInfo("%cFound embedded soundfont!", consoleColors.recognized); + this.embeddedSoundFont = binaryData.slice(startIndex, startIndex + currentChunk.size).buffer; + } + else + { + SpessaSynthWarn(`Unknown RIFF chunk: "${type}"`); + } + if (type === "dls ") + { + // Assume bank offset of 0 by default. If we find any bank selects, then the offset is 1. + this.isDLSRMIDI = true; + } + } + else if (currentChunk.header === "LIST") + { + const type = readBytesAsString(currentChunk.chunkData, 4); + if (type === "INFO") + { + SpessaSynthInfo("%cFound RMIDI INFO chunk!", consoleColors.recognized); + this.RMIDInfo = {}; + while (currentChunk.chunkData.currentIndex <= currentChunk.size) + { + const infoChunk = readRIFFChunk(currentChunk.chunkData, true); + this.RMIDInfo[infoChunk.header] = infoChunk.chunkData; + } + if (this.RMIDInfo["ICOP"]) + { + // special case, overwrites the copyright components array + this.copyright = readBytesAsString( + this.RMIDInfo["ICOP"], + this.RMIDInfo["ICOP"].length, + undefined, + false + ).replaceAll("\n", " "); + } + if (this.RMIDInfo["INAM"]) + { + this.rawMidiName = this.RMIDInfo[RMIDINFOChunks.name]; + // noinspection JSCheckFunctionSignatures + this.midiName = readBytesAsString( + this.rawMidiName, + this.rawMidiName.length, + undefined, + false + ).replaceAll("\n", " "); + } + // these can be used interchangeably + if (this.RMIDInfo["IALB"] && !this.RMIDInfo["IPRD"]) + { + this.RMIDInfo["IPRD"] = this.RMIDInfo["IALB"]; + } + if (this.RMIDInfo["IPRD"] && !this.RMIDInfo["IALB"]) + { + this.RMIDInfo["IALB"] = this.RMIDInfo["IPRD"]; + } + this.bankOffset = 1; // defaults to 1 + if (this.RMIDInfo[RMIDINFOChunks.bankOffset]) + { + this.bankOffset = readLittleEndian(this.RMIDInfo[RMIDINFOChunks.bankOffset], 2); + } + } + } + } + + if (this.isDLSRMIDI) + { + // Assume bank offset of 0 by default. If we find any bank selects, then the offset is 1. + this.bankOffset = 0; + } + + // if no embedded bank, assume 0 + if (this.embeddedSoundFont === undefined) + { + this.bankOffset = 0; + } + } + else if (initialString === "XMF_") + { + // XMF file + fileByteArray = loadXMF(this, binaryData); + } + else + { + fileByteArray = binaryData; + } + const headerChunk = this._readMIDIChunk(fileByteArray); + if (headerChunk.type !== "MThd") + { + SpessaSynthGroupEnd(); + throw new SyntaxError(`Invalid MIDI Header! Expected "MThd", got "${headerChunk.type}"`); + } + + 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); + // read all the tracks + for (let i = 0; i < this.tracksAmount; i++) + { + /** + * @type {MIDIMessage[]} + */ + const track = []; + const trackChunk = this._readMIDIChunk(fileByteArray); + + 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) + { + 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) + { + 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) + { + statusByte = runningByte; + } + else + { // noinspection PointlessBooleanExpressionJS + if (runningByte === undefined && statusByteCheck < 0x80) + { + // if we don't have a running byte and the status byte isn't valid, it's an error. + SpessaSynthGroupEnd(); + throw new SyntaxError(`Unexpected byte with no running byte. (${statusByteCheck})`); + } + else + { + // if the status byte is valid, use that + statusByte = trackChunk.data[trackChunk.data.currentIndex++]; + } + } + const statusByteChannel = getChannel(statusByte); + + let eventDataLength; + + // determine the message's length; + 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 + // gets the midi message length + eventDataLength = dataBytesAmount[statusByte >> 4]; + // save the status byte + runningByte = statusByte; + break; + } + + // put the event data into the array + const eventData = new IndexedByteArray(eventDataLength); + eventData.set(trackChunk.data.slice( + trackChunk.data.currentIndex, + trackChunk.data.currentIndex + eventDataLength + ), 0); + const event = new MIDIMessage(totalTicks, statusByte, eventData); + track.push(event); + // advance the track chunk + trackChunk.data.currentIndex += eventDataLength; + } + this.tracks.push(track); + + SpessaSynthInfo( + `%cParsed %c${this.tracks.length}%c / %c${this.tracksAmount}`, + consoleColors.info, + consoleColors.value, + consoleColors.info, + consoleColors.value + ); + } + + SpessaSynthInfo( + `%cAll tracks parsed correctly!`, + consoleColors.recognized + ); + // parse the events + this._parseInternal(); + SpessaSynthGroupEnd(); + 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 + ); + } + + /** + * @param fileByteArray {IndexedByteArray} + * @returns {{type: string, size: number, data: IndexedByteArray}} + * @private + */ + _readMIDIChunk(fileByteArray) + { + const chunk = {}; + // type + chunk.type = readBytesAsString(fileByteArray, 4); + // size + chunk.size = readBytesAsUintBigEndian(fileByteArray, 4); + // data + chunk.data = new IndexedByteArray(chunk.size); + const dataSlice = fileByteArray.slice(fileByteArray.currentIndex, fileByteArray.currentIndex + chunk.size); + chunk.data.set(dataSlice, 0); + fileByteArray.currentIndex += chunk.size; + return chunk; + } +} + +export { MIDI }; \ No newline at end of file diff --git a/spessasynth_lib/midi_parser/midi_message.js b/spessasynth_lib/midi_parser/midi_message.js new file mode 100644 index 0000000000000000000000000000000000000000..3f30116b6e03d467c5c948939df8d17c4a29c30e --- /dev/null +++ b/spessasynth_lib/midi_parser/midi_message.js @@ -0,0 +1,254 @@ +import { IndexedByteArray } from "../utils/indexed_array.js"; + +/** + * midi_message.js + * purpose: contains enums for midi events and controllers and functions to parse them + */ + +export class MIDIMessage +{ + /** + * Absolute number of MIDI ticks from the start of the track. + * @type {number} + */ + ticks; + + /** + * The MIDI message status byte. Note that for meta events, it is the second byte. (not 0xFF) + * @type {number} + */ + messageStatusByte; + + /** + * Message's binary data + * @type {IndexedByteArray} + */ + messageData; + + /** + * @param ticks {number} + * @param byte {number} the message status byte + * @param data {IndexedByteArray} + */ + constructor(ticks, byte, data) + { + this.ticks = ticks; + this.messageStatusByte = byte; + this.messageData = data; + } +} + +/** + * Gets the status byte's channel + * @param statusByte + * @returns {number} channel is -1 for system messages -2 for meta and -3 for sysex + */ +export function getChannel(statusByte) +{ + const eventType = statusByte & 0xF0; + const channel = statusByte & 0x0F; + + let resultChannel = channel; + + switch (eventType) + { + // midi (and meta and sysex headers) + case 0x80: + case 0x90: + case 0xA0: + case 0xB0: + case 0xC0: + case 0xD0: + case 0xE0: + break; + + case 0xF0: + switch (channel) + { + case 0x0: + resultChannel = -3; + break; + + case 0x1: + case 0x2: + case 0x3: + case 0x4: + case 0x5: + case 0x6: + case 0x7: + case 0x8: + case 0x9: + case 0xA: + case 0xB: + case 0xC: + case 0xD: + case 0xE: + resultChannel = -1; + break; + + case 0xF: + resultChannel = -2; + break; + } + break; + + default: + resultChannel = -1; + } + + return resultChannel; +} + +// all the midi statuses dictionary +export const messageTypes = { + noteOff: 0x80, + noteOn: 0x90, + polyPressure: 0xA0, + controllerChange: 0xB0, + programChange: 0xC0, + channelPressure: 0xD0, + pitchBend: 0xE0, + systemExclusive: 0xF0, + timecode: 0xF1, + songPosition: 0xF2, + songSelect: 0xF3, + tuneRequest: 0xF6, + clock: 0xF8, + start: 0xFA, + continue: 0xFB, + stop: 0xFC, + activeSensing: 0xFE, + reset: 0xFF, + sequenceNumber: 0x00, + text: 0x01, + copyright: 0x02, + trackName: 0x03, + instrumentName: 0x04, + lyric: 0x05, + marker: 0x06, + cuePoint: 0x07, + programName: 0x08, + midiChannelPrefix: 0x20, + midiPort: 0x21, + endOfTrack: 0x2F, + setTempo: 0x51, + smpteOffset: 0x54, + timeSignature: 0x58, + keySignature: 0x59, + sequenceSpecific: 0x7F +}; + + +/** + * Gets the event's status and channel from the status byte + * @param statusByte {number} the status byte + * @returns {{channel: number, status: number}} channel will be -1 for sysex and meta + */ +export function getEvent(statusByte) +{ + const status = statusByte & 0xF0; + const channel = statusByte & 0x0F; + + let eventChannel = -1; + let eventStatus = statusByte; + + if (status >= 0x80 && status <= 0xE0) + { + eventChannel = channel; + eventStatus = status; + } + + return { + status: eventStatus, + channel: eventChannel + }; +} + + +/** + * @enum {number} + */ +export const midiControllers = { + bankSelect: 0, + modulationWheel: 1, + breathController: 2, + footController: 4, + portamentoTime: 5, + dataEntryMsb: 6, + mainVolume: 7, + balance: 8, + pan: 10, + expressionController: 11, + effectControl1: 12, + effectControl2: 13, + generalPurposeController1: 16, + generalPurposeController2: 17, + generalPurposeController3: 18, + generalPurposeController4: 19, + lsbForControl0BankSelect: 32, + lsbForControl1ModulationWheel: 33, + lsbForControl2BreathController: 34, + lsbForControl4FootController: 36, + lsbForControl5PortamentoTime: 37, + lsbForControl6DataEntry: 38, + lsbForControl7MainVolume: 39, + lsbForControl8Balance: 40, + lsbForControl10Pan: 42, + lsbForControl11ExpressionController: 43, + lsbForControl12EffectControl1: 44, + lsbForControl13EffectControl2: 45, + sustainPedal: 64, + portamentoOnOff: 65, + sostenutoPedal: 66, + softPedal: 67, + legatoFootswitch: 68, + hold2Pedal: 69, + soundVariation: 70, + filterResonance: 71, + releaseTime: 72, + attackTime: 73, + brightness: 74, + decayTime: 75, + vibratoRate: 76, + vibratoDepth: 77, + vibratoDelay: 78, + soundController10: 79, + generalPurposeController5: 80, + generalPurposeController6: 81, + generalPurposeController7: 82, + generalPurposeController8: 83, + portamentoControl: 84, + reverbDepth: 91, + tremoloDepth: 92, + chorusDepth: 93, + detuneDepth: 94, + phaserDepth: 95, + dataIncrement: 96, + dataDecrement: 97, + NRPNLsb: 98, + NRPNMsb: 99, + RPNLsb: 100, + RPNMsb: 101, + allSoundOff: 120, + resetAllControllers: 121, + localControlOnOff: 122, + allNotesOff: 123, + omniModeOff: 124, + omniModeOn: 125, + monoModeOn: 126, + polyModeOn: 127 +}; + + +/** + * @type {{"11": number, "12": number, "13": number, "14": number, "8": number, "9": number, "10": number}} + */ +export const dataBytesAmount = { + 0x8: 2, // note off + 0x9: 2, // note on + 0xA: 2, // note at + 0xB: 2, // cc change + 0xC: 1, // pg change + 0xD: 1, // channel after touch + 0xE: 2 // pitch wheel +}; \ No newline at end of file diff --git a/spessasynth_lib/midi_parser/midi_sequence.js b/spessasynth_lib/midi_parser/midi_sequence.js new file mode 100644 index 0000000000000000000000000000000000000000..2c5d907ec45dad614914818e927d870441dcf906 --- /dev/null +++ b/spessasynth_lib/midi_parser/midi_sequence.js @@ -0,0 +1,225 @@ +/** + * This is the base type for MIDI files. It contains all the "metadata" and information. + * It extends to: + * - BasicMIDI, which contains the actual track data of the MIDI file. Essentially the MIDI file itself. + * - MIDIData, which contains all properties that MIDI does, except for tracks and the embedded soundfont. + * MIDIData is the "shell" of the file which is available on the main thread at all times, containing the metadata. + */ +class MIDISequenceData +{ + /** + * The time division of the sequence, representing the number of ticks per beat. + * @type {number} + */ + timeDivision = 0; + + /** + * The duration of the sequence, in seconds. + * @type {number} + */ + duration = 0; + + /** + * The tempo changes in the sequence, ordered from the last change to the first. + * Each change is represented by an object with a tick position and a tempo value in beats per minute. + * @type {{ticks: number, tempo: number}[]} + */ + tempoChanges = [{ ticks: 0, tempo: 120 }]; + + /** + * A string containing the copyright information for the MIDI sequence if detected. + * @type {string} + */ + copyright = ""; + + /** + * The number of tracks in the MIDI sequence. + * @type {number} + */ + tracksAmount = 0; + + /** + * The track names in the MIDI file, an empty string if not set. + * @type {string[]} + */ + trackNames = []; + + /** + * An array containing the lyrics of the sequence, stored as binary chunks (Uint8Array). + * @type {Uint8Array[]} + */ + lyrics = []; + + /** + * An array of tick positions where lyrics events occur in the sequence. + * @type {number[]} + */ + lyricsTicks = []; + + /** + * The tick position of the first note-on event in the MIDI sequence. + * @type {number} + */ + firstNoteOn = 0; + + /** + * The MIDI key range used in the sequence, represented by a minimum and maximum note value. + * @type {{min: number, max: number}} + */ + keyRange = { min: 0, max: 127 }; + + /** + * The tick position of the last voice event (such as note-on, note-off, or control change) in the sequence. + * @type {number} + */ + lastVoiceEventTick = 0; + + /** + * An array of MIDI port numbers used by each track in the sequence. + * @type {number[]} + */ + midiPorts = [0]; + + /** + * An array of channel offsets for each MIDI port, using the SpessaSynth method. + * @type {number[]} + */ + midiPortChannelOffsets = [0]; + + /** + * A list of sets, where each set contains the MIDI channels used by each track in the sequence. + * @type {Set[]} + */ + usedChannelsOnTrack = []; + + /** + * The loop points (in ticks) of the sequence, including both start and end points. + * @type {{start: number, end: number}} + */ + loop = { start: 0, end: 0 }; + + /** + * The name of the MIDI sequence. + * @type {string} + */ + midiName = ""; + + /** + * A boolean indicating if the sequence's name is the same as the file name. + * @type {boolean} + */ + midiNameUsesFileName = false; + + /** + * The file name of the MIDI sequence, if provided during parsing. + * @type {string} + */ + fileName = ""; + + /** + * The raw, encoded MIDI name, represented as a Uint8Array. + * Useful when the MIDI file uses a different code page. + * @type {Uint8Array} + */ + rawMidiName; + + /** + * The format of the MIDI file, which can be 0, 1, or 2, indicating the type of the MIDI file. + * @type {number} + */ + format = 0; + + /** + * The RMID (Resource-Interchangeable MIDI) info data, if the file is RMID formatted. + * Otherwise, this field is undefined. + * Chunk type (e.g. "INAM"): Chunk data as a binary array. + * @type {Object} + */ + RMIDInfo = {}; + + /** + * The bank offset used for RMID files. + * @type {number} + */ + bankOffset = 0; + + /** + * If the MIDI file is a Soft Karaoke file (.kar), this flag is set to true. + * https://www.mixagesoftware.com/en/midikit/help/HTML/karaoke_formats.html + * @type {boolean} + */ + isKaraokeFile = false; + + /** + * Indicates if this file is a Multi-Port MIDI file. + * @type {boolean} + */ + isMultiPort = false; + + /** + * Converts ticks to time in seconds + * @param ticks {number} time in MIDI ticks + * @returns {number} time in seconds + */ + MIDIticksToSeconds(ticks) + { + let totalSeconds = 0; + + while (ticks > 0) + { + // tempo changes are reversed, so the first element is the last tempo change + // and the last element is the first tempo change + // (always at tick 0 and tempo 120) + // find the last tempo change that has occurred + let tempo = this.tempoChanges.find(v => v.ticks < ticks); + + // calculate the difference and tempo time + let timeSinceLastTempo = ticks - tempo.ticks; + totalSeconds += (timeSinceLastTempo * 60) / (tempo.tempo * this.timeDivision); + ticks -= timeSinceLastTempo; + } + + return totalSeconds; + } + + /** + * INTERNAL USE ONLY! + * DO NOT USE IN SPESSASYNTH_LIB + * @param sequence {MIDISequenceData} + * @protected + */ + _copyFromSequence(sequence) + { + // properties can be assigned + this.midiName = sequence.midiName; + this.midiNameUsesFileName = sequence.midiNameUsesFileName; + this.fileName = sequence.fileName; + this.timeDivision = sequence.timeDivision; + this.duration = sequence.duration; + this.copyright = sequence.copyright; + this.tracksAmount = sequence.tracksAmount; + this.firstNoteOn = sequence.firstNoteOn; + this.lastVoiceEventTick = sequence.lastVoiceEventTick; + this.format = sequence.format; + this.bankOffset = sequence.bankOffset; + this.isKaraokeFile = sequence.isKaraokeFile; + this.isMultiPort = sequence.isMultiPort; + + // copying arrays + this.tempoChanges = [...sequence.tempoChanges]; + this.lyrics = sequence.lyrics.map(arr => new Uint8Array(arr)); + this.lyricsTicks = [...sequence.lyricsTicks]; + this.midiPorts = [...sequence.midiPorts]; + this.trackNames = [...sequence.trackNames]; + this.midiPortChannelOffsets = [...sequence.midiPortChannelOffsets]; + this.usedChannelsOnTrack = sequence.usedChannelsOnTrack.map(set => new Set(set)); + this.rawMidiName = sequence.rawMidiName ? new Uint8Array(sequence.rawMidiName) : undefined; + + // copying objects + this.loop = { ...sequence.loop }; + this.keyRange = { ...sequence.keyRange }; + this.RMIDInfo = { ...sequence.RMIDInfo }; + } +} + +export { MIDISequenceData }; \ No newline at end of file diff --git a/spessasynth_lib/midi_parser/midi_writer.js b/spessasynth_lib/midi_parser/midi_writer.js new file mode 100644 index 0000000000000000000000000000000000000000..cfde786739a77fd488a6cd85b6cb0c464a1e672d --- /dev/null +++ b/spessasynth_lib/midi_parser/midi_writer.js @@ -0,0 +1,99 @@ +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 standard MIDI file + * @this {BasicMIDI} + */ +export function writeMIDI() +{ + const midi = this; + if (!midi.tracks) + { + throw new Error("MIDI has no tracks!"); + } + /** + * @type {Uint8Array[]} + */ + const binaryTrackData = []; + for (const track of midi.tracks) + { + const binaryTrack = []; + let currentTick = 0; + let runningByte = undefined; + for (const event of track) + { + // Ticks stored in MIDI are absolute, but SMF wants relative. Convert them here. + const deltaTicks = event.ticks - currentTick; + /** + * @type {number[]} + */ + let messageData; + // determine the message + if (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) + { + // this is a system exclusive message + // syntax is F0 + messageData = [0xf0, ...writeVariableLengthQuantity(event.messageData.length), ...event.messageData]; + } + else + { + // this is a midi message + messageData = []; + if (runningByte !== event.messageStatusByte) + { + // Running byte was not the byte we want. Add the byte here. + runningByte = event.messageStatusByte; + // add the status byte to the midi + messageData.push(event.messageStatusByte); + } + // add the data + messageData.push(...event.messageData); + } + // write VLQ + binaryTrack.push(...writeVariableLengthQuantity(deltaTicks)); + // write the message + binaryTrack.push(...messageData); + currentTick += deltaTicks; + } + binaryTrackData.push(new Uint8Array(binaryTrack)); + } + + /** + * @param text {string} + * @param arr {number[]} + */ + function writeText(text, arr) + { + for (let i = 0; i < text.length; i++) + { + arr.push(text.charCodeAt(i)); + } + } + + // write the file + const binaryData = []; + // write header + writeText("MThd", binaryData); // MThd + binaryData.push(...writeBytesAsUintBigEndian(6, 4)); // length + 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) + { + // write track header + writeText("MTrk", binaryData); // MTrk + binaryData.push(...writeBytesAsUintBigEndian(track.length, 4)); // length + binaryData.push(...track); // write data + } + return new Uint8Array(binaryData); +} \ No newline at end of file diff --git a/spessasynth_lib/midi_parser/rmidi_writer.js b/spessasynth_lib/midi_parser/rmidi_writer.js new file mode 100644 index 0000000000000000000000000000000000000000..df47857b763d79ea345e68207f2a97687ac6b2b7 --- /dev/null +++ b/spessasynth_lib/midi_parser/rmidi_writer.js @@ -0,0 +1,567 @@ +import { combineArrays, IndexedByteArray } from "../utils/indexed_array.js"; +import { writeRIFFOddSize } from "../soundfont/basic_soundfont/riff_chunk.js"; +import { getStringBytes, getStringBytesZero } from "../utils/byte_functions/string.js"; +import { messageTypes, midiControllers, MIDIMessage } from "./midi_message.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 { DEFAULT_PERCUSSION } from "../synthetizer/synth_constants.js"; +import { chooseBank, isSystemXG, parseBankSelect } from "../utils/xg_hacks.js"; +import { isGM2On, isGMOn, isGSDrumsOn, isGSOn, isXGOn } from "../utils/sysex_detector.js"; + +/** + * @enum {string} + */ +export const RMIDINFOChunks = { + name: "INAM", + album: "IPRD", + album2: "IALB", + artist: "IART", + genre: "IGNR", + picture: "IPIC", + copyright: "ICOP", + creationDate: "ICRD", + comment: "ICMT", + engineer: "IENG", + software: "ISFT", + encoding: "IENC", + midiEncoding: "MENC", + bankOffset: "DBNK" +}; + +const FORCED_ENCODING = "utf-8"; +const DEFAULT_COPYRIGHT = "Created using SpessaSynth"; + +/** + * @typedef {Object} RMIDMetadata + * @property {string|undefined} name - the name of the file + * @property {string|undefined} engineer - the engineer who worked on the file + * @property {string|undefined} artist - the artist + * @property {string|undefined} album - the album + * @property {string|undefined} genre - the genre of the song + * @property {ArrayBuffer|undefined} picture - the image for the file (album cover) + * @property {string|undefined} comment - the coment of the file + * @property {string|undefined} creationDate - the creation date of the file + * @property {string|undefined} copyright - the copyright of the file + * @property {string|unescape} midiEncoding - the encoding of the inner MIDI file + */ + +/** + * Writes an RMIDI file + * @this {BasicMIDI} + * @param soundfontBinary {Uint8Array} + * @param soundfont {BasicSoundBank} + * @param bankOffset {number} the bank offset for RMIDI + * @param encoding {string} the encoding of the RMIDI info chunk + * @param metadata {RMIDMetadata} the metadata of the file. Optional. If provided, the encoding is forced to utf-8/ + * @param correctBankOffset {boolean} + * @returns {IndexedByteArray} + */ +export function writeRMIDI( + soundfontBinary, + soundfont, + bankOffset = 0, + encoding = "Shift_JIS", + metadata = {}, + correctBankOffset = true +) +{ + const mid = this; + SpessaSynthGroup("%cWriting the RMIDI File...", consoleColors.info); + SpessaSynthInfo( + `%cConfiguration: Bank offset: %c${bankOffset}%c, encoding: %c${encoding}`, + consoleColors.info, + consoleColors.value, + consoleColors.info, + consoleColors.value + ); + SpessaSynthInfo("metadata", metadata); + SpessaSynthInfo("Initial bank offset", mid.bankOffset); + if (correctBankOffset) + { + // Add the offset to the bank. + // See https://github.com/spessasus/sf2-rmidi-specification#readme + // also fix presets that don't exist + // since midi player6 doesn't seem to default to 0 when non-existent... + let system = "gm"; + /** + * The unwanted system messages such as gm/gm2 on + * @type {{tNum: number, e: MIDIMessage}[]} + */ + let unwantedSystems = []; + /** + * 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) + { + return; + } + 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); + /** + * @type {{ + * program: number, + * drums: boolean, + * lastBank: MIDIMessage, + * lastBankLSB: MIDIMessage, + * hasBankSelect: boolean + * }[]} + */ + const channelsInfo = []; + 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, + lastBankLSB: undefined, + hasBankSelect: false + }); + } + while (remainingTracks > 0) + { + let trackNum = findFirstEventIndex(); + const track = mid.tracks[trackNum]; + 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) + { + ports[trackNum] = e.messageData[0]; + continue; + } + const status = e.messageStatusByte & 0xF0; + if ( + status !== messageTypes.controllerChange && + status !== messageTypes.programChange && + status !== messageTypes.systemExclusive + ) + { + continue; + } + + if (status === messageTypes.systemExclusive) + { + // check for drum sysex + if (!isGSDrumsOn(e)) + { + // check for XG + if (isXGOn(e)) + { + system = "xg"; + } + else if (isGSOn(e)) + { + system = "gs"; + } + else if (isGMOn(e)) + { + // we do not want gm1 + system = "gm"; + unwantedSystems.push({ + tNum: trackNum, + e: e + }); + } + else if (isGM2On(e)) + { + system = "gm2"; + } + continue; + } + const sysexChannel = [9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15][e.messageData[5] & 0x0F] + portOffset; + channelsInfo[sysexChannel].drums = !!(e.messageData[7] > 0 && e.messageData[5] >> 4); + continue; + } + + // program change + const chNum = (e.messageStatusByte & 0xF) + portOffset; + /** + * @type {{program: number, drums: boolean, lastBank: MIDIMessage, lastBankLSB: MIDIMessage, hasBankSelect: boolean}} + */ + const channel = channelsInfo[chNum]; + if (status === messageTypes.programChange) + { + const isXG = isSystemXG(system); + // check if the preset for this program exists + const initialProgram = e.messageData[0]; + if (channel.drums) + { + if (soundfont.presets.findIndex(p => p.program === initialProgram && p.isDrumPreset( + isXG, + true + )) === -1) + { + // doesn't exist. pick any preset that has bank 128. + e.messageData[0] = soundfont.presets.find(p => p.isDrumPreset(isXG))?.program || 0; + SpessaSynthInfo( + `%cNo drum preset %c${initialProgram}%c. Channel %c${chNum}%c. Changing program to ${e.messageData[0]}.`, + consoleColors.info, + consoleColors.unrecognized, + consoleColors.info, + consoleColors.recognized, + consoleColors.info + ); + } + } + else + { + if (soundfont.presets.findIndex(p => p.program === initialProgram && !p.isDrumPreset(isXG)) === -1) + { + // doesn't exist. pick any preset that does not have bank 128. + e.messageData[0] = soundfont.presets.find(p => !p.isDrumPreset(isXG))?.program || 0; + SpessaSynthInfo( + `%cNo preset %c${initialProgram}%c. Channel %c${chNum}%c. Changing program to ${e.messageData[0]}.`, + consoleColors.info, + consoleColors.unrecognized, + consoleColors.info, + consoleColors.recognized, + consoleColors.info + ); + } + } + channel.program = e.messageData[0]; + // 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 bankLSB = (channel?.lastBankLSB?.messageData[1] - mid.bankOffset) || 0; + if (channel.lastBank === undefined) + { + continue; + } + // adjust bank for XG + let bank = chooseBank(realBank, bankLSB, channel.drums, isXG); + 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; + if (channel?.lastBankLSB?.messageData) + { + channel.lastBankLSB.messageData[1] = targetBank; + } + SpessaSynthInfo( + `%cNo preset %c${bank}:${e.messageData[0]}%c. Channel %c${chNum}%c. Changing bank to ${targetBank}.`, + consoleColors.info, + consoleColors.unrecognized, + consoleColors.info, + consoleColors.recognized, + consoleColors.info + ); + } + else + { + // There is a preset with this bank. Add offset. For drums add the normal offset. + let drumBank = bank; + if (isSystemXG(system) && bank === 128) + { + bank = 127; + } + const newBank = (bank === 128 ? 128 : drumBank) + bankOffset; + channel.lastBank.messageData[1] = newBank; + if (channel?.lastBankLSB?.messageData && !channel.drums) + { + channel.lastBankLSB.messageData[1] = channel.lastBankLSB.messageData[1] - mid.bankOffset + bankOffset; + } + SpessaSynthInfo( + `%cPreset %c${bank}:${e.messageData[0]}%c exists. Channel %c${chNum}%c. Changing bank to ${newBank}.`, + consoleColors.info, + consoleColors.recognized, + consoleColors.info, + consoleColors.recognized, + consoleColors.info + ); + } + continue; + } + + // controller change + // we only care about bank-selects + const isLSB = e.messageData[0] === midiControllers.lsbForControl0BankSelect; + if (e.messageData[0] !== midiControllers.bankSelect && !isLSB) + { + continue; + } + // bank select + channel.hasBankSelect = true; + const bankNumber = e.messageData[1]; + // interpret + const intepretation = parseBankSelect( + channel?.lastBank?.messageData[1] || 0, + bankNumber, + system, + isLSB, + channel.drums, + chNum + ); + if (intepretation.drumsStatus === 2) + { + channel.drums = true; + } + else if (intepretation.drumsStatus === 1) + { + channel.drums = false; + } + if (isLSB) + { + channel.lastBankLSB = e; + } + else + { + 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) + { + return; + } + // find the first program change (for the given channel) + const midiChannel = ch % 16; + const status = messageTypes.programChange | midiChannel; + // 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) + { + // this channel is not used at all + return; + } + let indexToAdd = track.findIndex(e => e.messageStatusByte === status); + 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) + { + // no voices??? skip + return; + } + const programTicks = track[programIndex].ticks; + const targetProgram = soundfont.getPreset(0, 0).program; + track.splice(programIndex, 0, new MIDIMessage( + programTicks, + messageTypes.programChange | midiChannel, + new IndexedByteArray([targetProgram]) + )); + indexToAdd = programIndex; + } + SpessaSynthInfo( + `%cAdding bank select for %c${ch}`, + consoleColors.info, + consoleColors.recognized + ); + const ticks = track[indexToAdd].ticks; + const targetBank = (soundfont.getPreset( + 0, + has.program, + isSystemXG(system) + )?.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" && !isSystemXG(system)) + { + 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(mid.writeMIDI().buffer); + + // info data for RMID + /** + * @type {Uint8Array[]} + */ + const infoContent = [getStringBytes("INFO")]; + const encoder = new TextEncoder(); + // software (SpessaSynth) + infoContent.push( + writeRIFFOddSize(RMIDINFOChunks.software, encoder.encode("SpessaSynth"), true) + ); + // name + if (metadata.name !== undefined) + { + + infoContent.push( + writeRIFFOddSize(RMIDINFOChunks.name, encoder.encode(metadata.name), true) + ); + encoding = FORCED_ENCODING; + } + else + { + infoContent.push( + writeRIFFOddSize(RMIDINFOChunks.name, mid.rawMidiName, true) + ); + } + // creation date + if (metadata.creationDate !== undefined) + { + encoding = FORCED_ENCODING; + infoContent.push( + writeRIFFOddSize(RMIDINFOChunks.creationDate, encoder.encode(metadata.creationDate), true) + ); + } + else + { + const today = new Date().toLocaleString(undefined, { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + hour: "numeric", + minute: "numeric" + }); + infoContent.push( + writeRIFFOddSize(RMIDINFOChunks.creationDate, getStringBytesZero(today), true) + ); + } + // comment + if (metadata.comment !== undefined) + { + encoding = FORCED_ENCODING; + infoContent.push( + writeRIFFOddSize(RMIDINFOChunks.comment, encoder.encode(metadata.comment)) + ); + } + // engineer + if (metadata.engineer !== undefined) + { + infoContent.push( + writeRIFFOddSize(RMIDINFOChunks.engineer, encoder.encode(metadata.engineer), true) + ); + } + // album + if (metadata.album !== undefined) + { + // note that there are two album chunks: IPRD and IALB + encoding = FORCED_ENCODING; + infoContent.push( + writeRIFFOddSize(RMIDINFOChunks.album, encoder.encode(metadata.album), true) + ); + infoContent.push( + writeRIFFOddSize(RMIDINFOChunks.album2, encoder.encode(metadata.album), true) + ); + } + // artist + if (metadata.artist !== undefined) + { + encoding = FORCED_ENCODING; + infoContent.push( + writeRIFFOddSize(RMIDINFOChunks.artist, encoder.encode(metadata.artist), true) + ); + } + // genre + if (metadata.genre !== undefined) + { + encoding = FORCED_ENCODING; + infoContent.push( + writeRIFFOddSize(RMIDINFOChunks.genre, encoder.encode(metadata.genre), true) + ); + } + // picture + if (metadata.picture !== undefined) + { + infoContent.push( + writeRIFFOddSize(RMIDINFOChunks.picture, new Uint8Array(metadata.picture)) + ); + } + // copyright + if (metadata.copyright !== undefined) + { + encoding = FORCED_ENCODING; + infoContent.push( + writeRIFFOddSize(RMIDINFOChunks.copyright, encoder.encode(metadata.copyright), true) + ); + } + else + { + // use midi copyright if possible + const copyright = mid.copyright.length > 0 ? mid.copyright : DEFAULT_COPYRIGHT; + infoContent.push( + writeRIFFOddSize(RMIDINFOChunks.copyright, getStringBytesZero(copyright)) + ); + } + + // bank offset + const DBNK = new IndexedByteArray(2); + writeLittleEndian(DBNK, bankOffset, 2); + infoContent.push(writeRIFFOddSize(RMIDINFOChunks.bankOffset, DBNK)); + // midi encoding + if (metadata.midiEncoding !== undefined) + { + infoContent.push( + writeRIFFOddSize(RMIDINFOChunks.midiEncoding, encoder.encode(metadata.midiEncoding)) + ); + encoding = FORCED_ENCODING; + } + // encoding + infoContent.push(writeRIFFOddSize(RMIDINFOChunks.encoding, getStringBytesZero(encoding))); + + // combine and write out + const infodata = combineArrays(infoContent); + const rmiddata = combineArrays([ + getStringBytes("RMID"), + writeRIFFOddSize( + "data", + newMid + ), + writeRIFFOddSize( + "LIST", + infodata + ), + soundfontBinary + ]); + SpessaSynthInfo("%cFinished!", consoleColors.info); + SpessaSynthGroupEnd(); + return writeRIFFOddSize( + "RIFF", + rmiddata + ); +} \ No newline at end of file diff --git a/spessasynth_lib/midi_parser/used_keys_loaded.js b/spessasynth_lib/midi_parser/used_keys_loaded.js new file mode 100644 index 0000000000000000000000000000000000000000..50582a1c7a68c2d62e9e51b96a6c284f1e5f2f83 --- /dev/null +++ b/spessasynth_lib/midi_parser/used_keys_loaded.js @@ -0,0 +1,238 @@ +import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from "../utils/loggin.js"; +import { consoleColors } from "../utils/other.js"; +import { messageTypes, midiControllers } from "./midi_message.js"; +import { DEFAULT_PERCUSSION } from "../synthetizer/synth_constants.js"; +import { chooseBank, isSystemXG, parseBankSelect } from "../utils/xg_hacks.js"; +import { isGSDrumsOn, isXGOn } from "../utils/sysex_detector.js"; + +/** + * Gets the used programs and keys for this MIDI file with a given sound bank + * @this {BasicMIDI} + * @param soundfont {BasicSoundBank|WorkletSoundfontManager} - the sound bank + * @returns {Object>} + */ +export function getUsedProgramsAndKeys(soundfont) +{ + const mid = this; + 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); + /** + * @type {{program: number, bank: number, bankLSB: number, drums: boolean, string: string, actualBank: number}[]} + */ + const channelPresets = []; + for (let i = 0; i < channelsAmount; i++) + { + const bank = i % 16 === DEFAULT_PERCUSSION ? 128 : 0; + channelPresets.push({ + program: 0, + bank: bank, + bankLSB: 0, + actualBank: bank, + drums: i % 16 === DEFAULT_PERCUSSION, // drums appear on 9 every 16 channels, + string: `${bank}:0` + }); + } + + // check for xg + let system = "gs"; + + function updateString(ch) + { + const bank = chooseBank(ch.bank, ch.bankLSB, ch.drums, isSystemXG(system)); + // check if this exists in the soundfont + let exists = soundfont.getPreset(bank, ch.program, isSystemXG(system)); + ch.actualBank = exists.bank; + ch.program = exists.program; + ch.string = ch.actualBank + ":" + ch.program; + if (!usedProgramsAndKeys[ch.string]) + { + SpessaSynthInfo( + `%cDetected a new preset: %c${ch.string}`, + consoleColors.info, + 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) + { + return; + } + if (track[eventIndexes[i]].ticks < ticks) + { + index = i; + ticks = track[eventIndexes[i]].ticks; + } + }); + return index; + } + + const ports = mid.midiPorts.slice(); + // initialize + channelPresets.forEach(c => + { + updateString(c); + }); + while (remainingTracks > 0) + { + let trackNum = findFirstEventIndex(); + const track = mid.tracks[trackNum]; + if (eventIndexes[trackNum] >= track.length) + { + remainingTracks--; + continue; + } + const event = track[eventIndexes[trackNum]]; + eventIndexes[trackNum]++; + + if (event.messageStatusByte === messageTypes.midiPort) + { + ports[trackNum] = event.messageData[0]; + continue; + } + const status = event.messageStatusByte & 0xF0; + if ( + status !== messageTypes.noteOn && + status !== messageTypes.controllerChange && + status !== messageTypes.programChange && + status !== messageTypes.systemExclusive + ) + { + continue; + } + const channel = (event.messageStatusByte & 0xF) + mid.midiPortChannelOffsets[ports[trackNum]] || 0; + let ch = channelPresets[channel]; + switch (status) + { + case messageTypes.programChange: + ch.program = event.messageData[0]; + updateString(ch); + break; + + case messageTypes.controllerChange: + const isLSB = event.messageData[0] === midiControllers.lsbForControl0BankSelect; + if (event.messageData[0] !== midiControllers.bankSelect && !isLSB) + { + // we only care about bank select + continue; + } + 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 (isLSB) + { + ch.bankLSB = realBank; + } + else + { + ch.bank = realBank; + } + // interpret the bank + const intepretation = parseBankSelect( + ch.bank, + realBank, + system, + isLSB, + ch.drums, + channel + ); + switch (intepretation.drumsStatus) + { + case 0: + // no change + break; + + case 1: + // drums changed to off + // drum change is a program change + ch.drums = false; + updateString(ch); + break; + + case 2: + // drums changed to on + // drum change is a program change + ch.drums = true; + updateString(ch); + break; + } + // do not update the data, bank change doesn't change the preset + break; + + case messageTypes.noteOn: + if (event.messageData[1] === 0) + { + // that's a note off + continue; + } + usedProgramsAndKeys[ch.string].add(`${event.messageData[0]}-${event.messageData[1]}`); + break; + + case messageTypes.systemExclusive: + // check for drum sysex + if (!isGSDrumsOn(event)) + { + // check for XG + if (isXGOn(event)) + { + system = "xg"; + SpessaSynthInfo( + "%cXG on detected!", + consoleColors.recognized + ); + } + continue; + } + const sysexChannel = [9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15][event.messageData[5] & 0x0F] + mid.midiPortChannelOffsets[ports[trackNum]]; + const isDrum = !!(event.messageData[7] > 0 && event.messageData[5] >> 4); + ch = channelPresets[sysexChannel]; + ch.drums = isDrum; + updateString(ch); + break; + + } + } + for (const key of Object.keys(usedProgramsAndKeys)) + { + if (usedProgramsAndKeys[key].size === 0) + { + SpessaSynthInfo( + `%cDetected change but no keys for %c${key}`, + consoleColors.info, + consoleColors.value + ); + delete usedProgramsAndKeys[key]; + } + } + SpessaSynthGroupEnd(); + return usedProgramsAndKeys; +} \ No newline at end of file diff --git a/spessasynth_lib/midi_parser/xmf_loader.js b/spessasynth_lib/midi_parser/xmf_loader.js new file mode 100644 index 0000000000000000000000000000000000000000..4e6f0ef450b87c521a81e383d90ebb2be888a414 --- /dev/null +++ b/spessasynth_lib/midi_parser/xmf_loader.js @@ -0,0 +1,454 @@ +import { readBytesAsString } from "../utils/byte_functions/string.js"; +import { SpessaSynthGroup, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn } from "../utils/loggin.js"; +import { consoleColors } from "../utils/other.js"; +import { readBytesAsUintBigEndian } from "../utils/byte_functions/big_endian.js"; +import { readVariableLengthQuantity } from "../utils/byte_functions/variable_length_quantity.js"; +import { RMIDINFOChunks } from "./rmidi_writer.js"; +import { inflateSync } from "../externals/fflate/fflate.min.js"; +import { IndexedByteArray } from "../utils/indexed_array.js"; + +/** + * @enum {number} + */ +const metadataTypes = { + XMFFileType: 0, + nodeName: 1, + nodeIDNumber: 2, + resourceFormat: 3, + filenameOnDisk: 4, + filenameExtensionOnDisk: 5, + macOSFileTypeAndCreator: 6, + mimeType: 7, + title: 8, + copyrightNotice: 9, + comment: 10, + autoStart: 11, // Node Name of the FileNode containing the SMF image to autostart when the XMF file loads + preload: 12, // Used to preload specific SMF and DLS file images. + contentDescription: 13, // RP-42a (https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp42.pdf) + ID3Metadata: 14 // RP-47 (https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp47.pdf) +}; + +/** + * @enum {number} + */ +const referenceTypeIds = { + inLineResource: 1, + inFileResource: 2, + inFileNode: 3, + externalFile: 4, + externalXMF: 5, + XMFFileURIandNodeID: 6 +}; + +/** + * @enum {number} + */ +const resourceFormatIDs = { + StandardMIDIFile: 0, + StandardMIDIFileType1: 1, + DLS1: 2, + DLS2: 3, + DLS22: 4, + mobileDLS: 5 +}; + +/** + * @enum {number} + */ +const formatTypeIDs = { + standard: 0, + MMA: 1, + registered: 2, + nonRegistered: 3 +}; + + +/** + * @enum {number} + */ +const unpackerIDs = { + none: 0, + MMAUnpacker: 1, + registered: 2, + nonRegistered: 3 +}; + +class XMFNode +{ + /** + * @type {number} + */ + length; + /** + * 0 means it's a file node + * @type {number} + */ + itemCount; + /** + * @type {number} + */ + metadataLength; + + /** + * @type {Object} + */ + metadata = {}; + + /** + * @type {IndexedByteArray} + */ + nodeData; + + /** + * @type {XMFNode[]} + */ + innerNodes = []; + + packedContent = false; + + nodeUnpackers = []; + + + /** + * @type {"StandardMIDIFile"| + * "StandardMIDIFileType1"| + * "DLS1"| + * "DLS2"| + * "DLS22"| + * "mobileDLS"| + * "unknown"|"folder"} + */ + resourceFormat = "unknown"; + + /** + * @param binaryData {IndexedByteArray} + */ + constructor(binaryData) + { + let nodeStartIndex = binaryData.currentIndex; + this.length = readVariableLengthQuantity(binaryData); + this.itemCount = readVariableLengthQuantity(binaryData); + // header length + const headerLength = readVariableLengthQuantity(binaryData); + const readBytes = binaryData.currentIndex - nodeStartIndex; + + const remainingHeader = headerLength - readBytes; + const headerData = binaryData.slice( + binaryData.currentIndex, + binaryData.currentIndex + remainingHeader + ); + binaryData.currentIndex += remainingHeader; + + this.metadataLength = readVariableLengthQuantity(headerData); + + const metadataChunk = headerData.slice( + headerData.currentIndex, + headerData.currentIndex + this.metadataLength + ); + headerData.currentIndex += this.metadataLength; + + /** + * @type {metadataTypes|string|number} + */ + let fieldSpecifier; + let key; + while (metadataChunk.currentIndex < metadataChunk.length) + { + const firstSpecifierByte = metadataChunk[metadataChunk.currentIndex]; + if (firstSpecifierByte === 0) + { + metadataChunk.currentIndex++; + fieldSpecifier = readVariableLengthQuantity(metadataChunk); + if (Object.values(metadataTypes).indexOf(fieldSpecifier) === -1) + { + SpessaSynthWarn(`Unknown field specifier: ${fieldSpecifier}`); + key = `unknown_${fieldSpecifier}`; + } + else + { + key = Object.keys(metadataTypes).find(k => metadataTypes[k] === fieldSpecifier); + } + } + else + { + // this is the length of string + const stringLength = readVariableLengthQuantity(metadataChunk); + fieldSpecifier = readBytesAsString(metadataChunk, stringLength); + key = fieldSpecifier; + } + + const numberOfVersions = readVariableLengthQuantity(metadataChunk); + if (numberOfVersions === 0) + { + const dataLength = readVariableLengthQuantity(metadataChunk); + const contentsChunk = metadataChunk.slice( + metadataChunk.currentIndex, + metadataChunk.currentIndex + dataLength + ); + metadataChunk.currentIndex += dataLength; + const formatID = readVariableLengthQuantity(contentsChunk); + // text only + if (formatID < 4) + { + this.metadata[key] = readBytesAsString(contentsChunk, dataLength - 1); + } + else + { + this.metadata[key] = contentsChunk.slice(contentsChunk.currentIndex); + } + } + else + { + // throw new Error ("International content is not supported."); + // Skip the number of versions + SpessaSynthWarn(`International content: ${numberOfVersions}`); + // Length in bytes + // Skip the whole thing! + metadataChunk.currentIndex += readVariableLengthQuantity(metadataChunk); + } + } + + const unpackersStart = headerData.currentIndex; + const unpackersLength = readVariableLengthQuantity(headerData); + const unpackersData = headerData.slice(headerData.currentIndex, unpackersStart + unpackersLength); + headerData.currentIndex = unpackersStart + unpackersLength; + if (unpackersLength > 0) + { + this.packedContent = true; + while (unpackersData.currentIndex < unpackersLength) + { + const unpacker = {}; + unpacker.id = readVariableLengthQuantity(unpackersData); + switch (unpacker.id) + { + case unpackerIDs.nonRegistered: + case unpackerIDs.registered: + SpessaSynthGroupEnd(); + throw new Error(`Unsupported unpacker ID: ${unpacker.id}`); + + default: + SpessaSynthGroupEnd(); + throw new Error(`Unknown unpacker ID: ${unpacker.id}`); + + case unpackerIDs.none: + unpacker.standardID = readVariableLengthQuantity(unpackersData); + break; + + case unpackerIDs.MMAUnpacker: + let manufacturerID = unpackersData[unpackersData.currentIndex++]; + // one or three byte form, depending on if the first byte is zero + if (manufacturerID === 0) + { + manufacturerID <<= 8; + manufacturerID |= unpackersData[unpackersData.currentIndex++]; + manufacturerID <<= 8; + manufacturerID |= unpackersData[unpackersData.currentIndex++]; + } + const manufacturerInternalID = readVariableLengthQuantity(unpackersData); + unpacker.manufacturerID = manufacturerID; + unpacker.manufacturerInternalID = manufacturerInternalID; + break; + } + unpacker.decodedSize = readVariableLengthQuantity(unpackersData); + this.nodeUnpackers.push(unpacker); + } + } + binaryData.currentIndex = nodeStartIndex + headerLength; + /** + * @type {referenceTypeIds|number} + */ + this.referenceTypeID = readVariableLengthQuantity(binaryData); + this.nodeData = binaryData.slice(binaryData.currentIndex, nodeStartIndex + this.length); + binaryData.currentIndex = nodeStartIndex + this.length; + switch (this.referenceTypeID) + { + case referenceTypeIds.inLineResource: + break; + + case referenceTypeIds.externalXMF: + case referenceTypeIds.inFileNode: + case referenceTypeIds.XMFFileURIandNodeID: + case referenceTypeIds.externalFile: + case referenceTypeIds.inFileResource: + SpessaSynthGroupEnd(); + throw new Error(`Unsupported reference type: ${this.referenceTypeID}`); + + default: + SpessaSynthGroupEnd(); + throw new Error(`Unknown reference type: ${this.referenceTypeID}`); + } + + // read the data + if (this.isFile) + { + if (this.packedContent) + { + const compressed = this.nodeData.slice(2, this.nodeData.length); + SpessaSynthInfo( + `%cPacked content. Attemting to deflate. Target size: %c${this.nodeUnpackers[0].decodedSize}`, + consoleColors.warn, + consoleColors.value + ); + try + { + this.nodeData = new IndexedByteArray(inflateSync(compressed).buffer); + } + catch (e) + { + SpessaSynthGroupEnd(); + throw new Error(`Error unpacking XMF file contents: ${e.message}.`); + } + } + /** + * interpret the content + * @type {number[]} + */ + const resourceFormat = this.metadata["resourceFormat"]; + if (resourceFormat === undefined) + { + SpessaSynthWarn("No resource format for this file node!"); + } + else + { + const formatTypeID = resourceFormat[0]; + if (formatTypeID !== formatTypeIDs.standard) + { + SpessaSynthWarn(`Non-standard formatTypeID: ${resourceFormat}`); + this.resourceFormat = resourceFormat.toString(); + } + const resourceFormatID = resourceFormat[1]; + if (Object.values(resourceFormatIDs).indexOf(resourceFormatID) === -1) + { + SpessaSynthWarn(`Unrecognized resource format: ${resourceFormatID}`); + } + else + { + this.resourceFormat = Object.keys(resourceFormatIDs) + .find(k => resourceFormatIDs[k] === resourceFormatID); + } + } + } + else + { + // folder node + this.resourceFormat = "folder"; + while (this.nodeData.currentIndex < this.nodeData.length) + { + const nodeStartIndex = this.nodeData.currentIndex; + const nodeLength = readVariableLengthQuantity(this.nodeData); + const nodeData = this.nodeData.slice(nodeStartIndex, nodeStartIndex + nodeLength); + this.nodeData.currentIndex = nodeStartIndex + nodeLength; + this.innerNodes.push(new XMFNode(nodeData)); + } + } + } + + get isFile() + { + return this.itemCount === 0; + } +} + +/** + * @param midi {MIDI} + * @param binaryData {IndexedByteArray} + * @returns {IndexedByteArray} the file byte array + */ +export function loadXMF(midi, binaryData) +{ + midi.bankOffset = 0; + // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/xmf-v1a.pdf + // https://wiki.multimedia.cx/index.php?title=Extensible_Music_Format_(XMF) + const sanityCheck = readBytesAsString(binaryData, 4); + if (sanityCheck !== "XMF_") + { + SpessaSynthGroupEnd(); + throw new SyntaxError(`Invalid XMF Header! Expected "_XMF", got "${sanityCheck}"`); + } + + SpessaSynthGroup("%cParsing XMF file...", consoleColors.info); + const version = readBytesAsString(binaryData, 4); + SpessaSynthInfo( + `%cXMF version: %c${version}`, + consoleColors.info, consoleColors.recognized + ); + // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp43.pdf + // version 2.00 has additional bytes + if (version === "2.00") + { + const fileTypeId = readBytesAsUintBigEndian(binaryData, 4); + const fileTypeRevisionId = readBytesAsUintBigEndian(binaryData, 4); + SpessaSynthInfo( + `%cFile Type ID: %c${fileTypeId}%c, File Type Revision ID: %c${fileTypeRevisionId}`, + consoleColors.info, + consoleColors.recognized, + consoleColors.info, + consoleColors.recognized + ); + } + + // file length + readVariableLengthQuantity(binaryData); + + const metadataTableLength = readVariableLengthQuantity(binaryData); + // skip metadata + binaryData.currentIndex += metadataTableLength; + + // skip to tree root + binaryData.currentIndex = readVariableLengthQuantity(binaryData); + const rootNode = new XMFNode(binaryData); + /** + * @type {IndexedByteArray} + */ + let midiArray; + /** + * find the stuff we care about + * @param node {XMFNode} + */ + const searchNode = node => + { + const checkMeta = (xmf, rmid) => + { + if (node.metadata[xmf] !== undefined && typeof node.metadata[xmf] === "string") + { + midi.RMIDInfo[rmid] = node.metadata[xmf]; + } + }; + // meta + checkMeta("nodeName", RMIDINFOChunks.name); + checkMeta("title", RMIDINFOChunks.name); + checkMeta("copyrightNotice", RMIDINFOChunks.copyright); + checkMeta("comment", RMIDINFOChunks.comment); + if (node.isFile) + { + switch (node.resourceFormat) + { + default: + return; + case "DLS1": + case "DLS2": + case "DLS22": + case "mobileDLS": + SpessaSynthInfo("%cFound embedded DLS!", consoleColors.recognized); + midi.embeddedSoundFont = node.nodeData.buffer; + break; + + case "StandardMIDIFile": + case "StandardMIDIFileType1": + SpessaSynthInfo("%cFound embedded MIDI!", consoleColors.recognized); + midiArray = node.nodeData; + break; + } + } + else + { + for (const n of node.innerNodes) + { + searchNode(n); + } + } + }; + searchNode(rootNode); + SpessaSynthGroupEnd(); + return midiArray; +} \ No newline at end of file diff --git a/spessasynth_lib/sequencer/README.md b/spessasynth_lib/sequencer/README.md new file mode 100644 index 0000000000000000000000000000000000000000..bfeb9c0b17d43d3a79b93972a5bb9c31f12b85ff --- /dev/null +++ b/spessasynth_lib/sequencer/README.md @@ -0,0 +1,30 @@ +## 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 + messageData: any // any +} +``` + +#### To worklet + +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. + +### Process tick + +`processTick` is called every time the `process` method is called via `SpessaSynthProcessor.processTickCallback`. diff --git a/spessasynth_lib/sequencer/default_sequencer_options.js b/spessasynth_lib/sequencer/default_sequencer_options.js new file mode 100644 index 0000000000000000000000000000000000000000..af8237cc396b05a8432d87197088a9876631102a --- /dev/null +++ b/spessasynth_lib/sequencer/default_sequencer_options.js @@ -0,0 +1,8 @@ +/** + * @type {SequencerOptions} + */ +export const DEFAULT_SEQUENCER_OPTIONS = { + skipToFirstNoteOn: true, + autoPlay: true, + preservePlaybackState: false +}; \ No newline at end of file diff --git a/spessasynth_lib/sequencer/sequencer.js b/spessasynth_lib/sequencer/sequencer.js new file mode 100644 index 0000000000000000000000000000000000000000..809a8fa138b38bc4508d18dc5b9156f2cbfd5ae4 --- /dev/null +++ b/spessasynth_lib/sequencer/sequencer.js @@ -0,0 +1,804 @@ +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 { + SongChangeType, + 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"; +import { BasicMIDI } from "../midi_parser/basic_midi.js"; +import { readBytesAsUintBigEndian } from "../utils/byte_functions/big_endian.js"; +import { DEFAULT_SEQUENCER_OPTIONS } from "./default_sequencer_options.js"; + +/** + * sequencer.js + * purpose: plays back the midi file decoded by midi_loader.js, including support for multichannel midis + * (adding channels when more than one midi port is detected) + * note: this is the sequencer class that runs on the main thread + * and only communicates with the worklet sequencer which does the actual playback + */ + +/** + * @typedef MidFile {Object} + * @property {ArrayBuffer} binary - the binary data of the file. + * @property {string|undefined} altName - the alternative name for the file + */ + +/** + * @typedef {BasicMIDI|MidFile} MIDIFile + */ + +// noinspection JSUnusedGlobalSymbols +/** + * @typedef {Object} SequencerOptions + * @property {boolean|undefined} skipToFirstNoteOn - if true, the sequencer will skip to the first note + * @property {boolean|undefined} autoPlay - if true, the sequencer will automatically start playing the MIDI + * @property {boolean|unescape} preservePlaybackState - if true, + * the sequencer will stay paused when seeking or changing the playback rate + */ + +// noinspection JSUnusedGlobalSymbols +export class Sequencer +{ + /** + * Executes when MIDI parsing has an error. + * @type {function(Error)} + */ + onError; + + /** + * Fires on text event + * @type {Function} + * @param data {Uint8Array} the data text + * @param type {number} the status byte of the message (the meta-status byte) + * @param lyricsIndex {number} if the text is a lyric, the index of the lyric in midiData.lyrics, otherwise -1 + */ + onTextEvent; + + /** + * The current MIDI data, with the exclusion of the embedded sound bank and event data. + * @type {MIDIData} + */ + midiData; + + /** + * The current MIDI data for all songs, like the midiData property. + * @type {MIDIData[]} + */ + songListData = []; + + /** + * @type {Object} + * @private + */ + onSongChange = {}; + + /** + * Fires when CurrentTime changes + * @type {Object} the time that was changed to + * @private + */ + onTimeChange = {}; + + /** + * @type {Object} + * @private + */ + onSongEnded = {}; + + /** + * Fires on tempo change + * @type {Object} + */ + onTempoChange = {}; + + /** + * Fires on meta-event + * @type {Object} + */ + onMetaEvent = {}; + + /** + * Current song's tempo in BPM + * @type {number} + */ + currentTempo = 120; + /** + * Current song index + * @type {number} + */ + songIndex = 0; + /** + * @type {function(BasicMIDI)} + * @private + */ + _getMIDIResolve = undefined; + /** + * Indicates if the current midiData property has fake data in it (not yet loaded) + * @type {boolean} + */ + hasDummyData = true; + /** + * Indicates whether the sequencer has finished playing a sequence + * @type {boolean} + */ + isFinished = false; + /** + * The current sequence's length, in seconds + * @type {number} + */ + duration = 0; + + /** + * Indicates if the sequencer is paused. + * Paused if a number, undefined if playing + * @type {undefined|number} + * @private + */ + pausedTime = undefined; + + /** + * Creates a new Midi sequencer for playing back MIDI files + * @param midiBinaries {MIDIFile[]} List of the buffers of the MIDI files + * @param synth {Synthetizer} synth to send events to + * @param options {SequencerOptions} the sequencer's options + */ + constructor(midiBinaries, synth, options = DEFAULT_SEQUENCER_OPTIONS) + { + 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; + + this.synth.sequencerCallbackFunction = this._handleMessage.bind(this); + + /** + * @type {boolean} + * @private + */ + this._skipToFirstNoteOn = options?.skipToFirstNoteOn ?? true; + /** + * @type {boolean} + * @private + */ + this._preservePlaybackState = options?.preservePlaybackState ?? false; + + if (this._skipToFirstNoteOn === false) + { + // setter sends message + this._sendMessage(WorkletSequencerMessageType.setSkipToFirstNote, false); + } + + if (this._preservePlaybackState === true) + { + this._sendMessage(WorkletSequencerMessageType.setPreservePlaybackState, true); + } + + this.loadNewSongList(midiBinaries, options?.autoPlay ?? true); + + window.addEventListener("beforeunload", this.resetMIDIOut.bind(this)); + } + + /** + * Internal loop marker + * @type {boolean} + * @private + */ + _loop = true; + + /** + * Indicates if the sequencer is currently looping + * @returns {boolean} + */ + get loop() + { + return this._loop; + } + + set loop(value) + { + this._sendMessage(WorkletSequencerMessageType.setLoop, [value, this._loopsRemaining]); + this._loop = value; + } + + /** + * Internal loop count marker (-1 is infinite) + * @type {number} + * @private + */ + _loopsRemaining = -1; + + /** + * The current remaining number of loops. -1 means infinite looping + * @returns {number} + */ + get loopsRemaining() + { + return this._loopsRemaining; + } + + /** + * The current remaining number of loops. -1 means infinite looping + * @param val {number} + */ + set loopsRemaining(val) + { + this._loopsRemaining = val; + this._sendMessage(WorkletSequencerMessageType.setLoop, [this._loop, val]); + } + + /** + * Controls the playback's rate + * @type {number} + * @private + */ + _playbackRate = 1; + + /** + * @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; + } + + /** + * @type {boolean} + * @private + */ + _shuffleSongs = false; + + /** + * Indicates if the song order is random + * @returns {boolean} + */ + get shuffleSongs() + { + return this._shuffleSongs; + } + + /** + * Indicates if the song order is random + * @param value {boolean} + */ + set shuffleSongs(value) + { + this._shuffleSongs = value; + if (value) + { + this._sendMessage(WorkletSequencerMessageType.changeSong, [SongChangeType.shuffleOn]); + } + else + { + this._sendMessage(WorkletSequencerMessageType.changeSong, [SongChangeType.shuffleOff]); + } + } + + /** + * Indicates if the sequencer should skip to first note on + * @return {boolean} + */ + get skipToFirstNoteOn() + { + return this._skipToFirstNoteOn; + } + + /** + * Indicates if the sequencer should skip to first note on + * @param val {boolean} + */ + set skipToFirstNoteOn(val) + { + this._skipToFirstNoteOn = val; + this._sendMessage(WorkletSequencerMessageType.setSkipToFirstNote, this._skipToFirstNoteOn); + } + + /** + * if true, + * the sequencer will stay paused when seeking or changing the playback rate + * @returns {boolean} + */ + get preservePlaybackState() + { + return this._preservePlaybackState; + } + + /** + * if true, + * the sequencer will stay paused when seeking or changing the playback rate + * @param val {boolean} + */ + set preservePlaybackState(val) + { + this._preservePlaybackState = val; + this._sendMessage(WorkletSequencerMessageType.setPreservePlaybackState, val); + } + + /** + * @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 !== undefined) + { + return this.pausedTime; + } + + return (this.synth.currentTime - this.absoluteStartTime) * this._playbackRate; + } + + set currentTime(time) + { + if (!this._preservePlaybackState) + { + this.unpause(); + } + this._sendMessage(WorkletSequencerMessageType.setTime, time); + } + + /** + * Use for visualization as it's not affected by the audioContext stutter + * @returns {number} + */ + get currentHighResolutionTime() + { + if (this.pausedTime !== undefined) + { + 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; + } + + /** + * 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; + } + + /** + * 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; + } + + /** + * Adds a new event that gets called when the tempo changes + * @param callback {function(number)} the new tempo, in BPM + * @param id {string} must be unique + */ + addOnTempoChangeEvent(callback, id) + { + this.onTempoChange[id] = callback; + } + + /** + * Adds a new event that gets called when a meta-event occurs + * @param callback {function([number, Uint8Array, number, number])} the meta-event type, + * its data, the track number and MIDI ticks + * @param id {string} must be unique + */ + addOnMetaEvent(callback, id) + { + this.onMetaEvent[id] = callback; + } + + resetMIDIOut() + { + if (!this.MIDIout) + { + return; + } + for (let i = 0; i < 16; i++) + { + this.MIDIout.send([messageTypes.controllerChange | i, 120, 0]); // all notes off + this.MIDIout.send([messageTypes.controllerChange | i, 123, 0]); // all sound off + } + this.MIDIout.send([messageTypes.reset]); // reset + } + + /** + * @param messageType {WorkletSequencerMessageType} + * @param messageData {any} + * @private + */ + _sendMessage(messageType, messageData = undefined) + { + this.synth.post({ + channelNumber: -1, + messageType: workletMessageType.sequencerSpecific, + messageData: { + messageType: messageType, + messageData: messageData + } + }); + } + + /** + * Switch to the next song in the playlist + */ + nextSong() + { + this._sendMessage(WorkletSequencerMessageType.changeSong, [SongChangeType.forwards]); + } + + /** + * Switch to the previous song in the playlist + */ + previousSong() + { + this._sendMessage(WorkletSequencerMessageType.changeSong, [SongChangeType.backwards]); + } + + /** + * Sets the song index in the playlist + * @param index + */ + setSongIndex(index) + { + const clamped = Math.max(Math.min(this.songsAmount - 1, index), 0); + this._sendMessage(WorkletSequencerMessageType.changeSong, [SongChangeType.index, clamped]); + } + + /** + * @param type {Object} + * @param params {any} + * @private + */ + _callEvents(type, params) + { + for (const key in type) + { + const callback = type[key]; + try + { + callback(params); + } + catch (e) + { + SpessaSynthWarn(`Failed to execute callback for ${callback[0]}:`, e); + } + } + } + + /** + * @param {WorkletSequencerReturnMessageType} messageType + * @param {any} messageData + * @private + */ + _handleMessage(messageType, messageData) + { + if (this.ignoreEvents) + { + return; + } + switch (messageType) + { + case WorkletSequencerReturnMessageType.midiEvent: + /** + * @type {number[]} + */ + let midiEventData = messageData; + if (this.MIDIout) + { + if (midiEventData[0] >= 0x80) + { + this.MIDIout.send(midiEventData); + return; + } + } + break; + + case WorkletSequencerReturnMessageType.songChange: + this.songIndex = messageData[0]; + const songChangeData = this.songListData[this.songIndex]; + this.midiData = songChangeData; + this.hasDummyData = false; + this.absoluteStartTime = 0; + this.duration = this.midiData.duration; + this._callEvents(this.onSongChange, songChangeData); + // if is auto played, unpause + if (messageData[1] === true) + { + this.unpause(); + } + break; + + case WorkletSequencerReturnMessageType.timeChange: + // message data is absolute time + const time = this.synth.currentTime - messageData; + this._callEvents(this.onTimeChange, time); + this._recalculateStartTime(time); + if (this.paused && this._preservePlaybackState) + { + this.pausedTime = time; + } + else + { + this.unpause(); + } + break; + + case WorkletSequencerReturnMessageType.pause: + this.pausedTime = this.currentTime; + this.isFinished = messageData; + if (this.isFinished) + { + this._callEvents(this.onSongEnded, undefined); + } + break; + + case WorkletSequencerReturnMessageType.midiError: + if (this.onError) + { + this.onError(messageData); + } + else + { + throw new Error("Sequencer error: " + messageData); + } + return; + + case WorkletSequencerReturnMessageType.getMIDI: + if (this._getMIDIResolve) + { + this._getMIDIResolve(BasicMIDI.copyFrom(messageData)); + } + break; + + case WorkletSequencerReturnMessageType.metaEvent: + /** + * @type {MIDIMessage} + */ + const event = messageData[0]; + switch (event.messageStatusByte) + { + case messageTypes.setTempo: + event.messageData.currentIndex = 0; + const bpm = 60000000 / readBytesAsUintBigEndian(event.messageData, 3); + event.messageData.currentIndex = 0; + this.currentTempo = Math.round(bpm * 100) / 100; + if (this.onTempoChange) + { + this._callEvents(this.onTempoChange, this.currentTempo); + } + break; + + case messageTypes.text: + case messageTypes.lyric: + case messageTypes.copyright: + case messageTypes.trackName: + case messageTypes.marker: + case messageTypes.cuePoint: + case messageTypes.instrumentName: + case messageTypes.programName: + let lyricsIndex = -1; + if (event.messageStatusByte === messageTypes.lyric) + { + lyricsIndex = Math.min( + this.midiData.lyricsTicks.indexOf(event.ticks), + this.midiData.lyrics.length - 1 + ); + } + let sentStatus = event.messageStatusByte; + // if MIDI is a karaoke file, it uses the "text" event type or "lyrics" for lyrics (duh) + // why? + // because the MIDI standard is a messy pile of garbage, + // and it's not my fault that it's like this :( + // I'm just trying to make the best out of a bad situation. + // I'm sorry + // okay I should get back to work + // anyway, + // check for a karaoke file and change the status byte to "lyric" + // if it's a karaoke file + if (this.midiData.isKaraokeFile && ( + event.messageStatusByte === messageTypes.text || + event.messageStatusByte === messageTypes.lyric + )) + { + lyricsIndex = Math.min( + this.midiData.lyricsTicks.indexOf(event.ticks), + this.midiData.lyricsTicks.length + ); + sentStatus = messageTypes.lyric; + } + if (this.onTextEvent) + { + this.onTextEvent(event.messageData, sentStatus, lyricsIndex, event.ticks); + } + break; + } + this._callEvents(this.onMetaEvent, messageData); + break; + + case WorkletSequencerReturnMessageType.loopCountChange: + this._loopsRemaining = messageData; + if (this._loopsRemaining === 0) + { + this._loop = false; + } + break; + + case WorkletSequencerReturnMessageType.songListChange: + this.songListData = messageData; + break; + + default: + break; + } + } + + /** + * @param time + * @private + */ + _recalculateStartTime(time) + { + 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 => + { + this._getMIDIResolve = resolve; + this._sendMessage(WorkletSequencerMessageType.getMIDI, undefined); + }); + } + + /** + * Loads a new song list + * @param midiBuffers {MIDIFile[]} - the MIDI files to play + * @param autoPlay {boolean} - if true, the first sequence will automatically start playing + */ + loadNewSongList(midiBuffers, autoPlay = true) + { + this.pause(); + // add some fake data + this.midiData = DUMMY_MIDI_DATA; + this.hasDummyData = true; + this.duration = 99999; + /** + * sanitize MIDIs + * @type {({binary: ArrayBuffer, altName: string}|BasicMIDI)[]} + */ + const sanitizedMidis = midiBuffers.map(m => + { + if (m.binary !== undefined) + { + return m; + } + return BasicMIDI.copyFrom(m); + }); + this._sendMessage(WorkletSequencerMessageType.loadNewSongList, [sanitizedMidis, autoPlay]); + this.songIndex = 0; + this.songsAmount = midiBuffers.length; + if (this.songsAmount > 1) + { + this.loop = false; + } + if (autoPlay === false) + { + this.pausedTime = this.currentTime; + } + } + + /** + * @param output {MIDIOutput} + */ + connectMidiOutput(output) + { + this.resetMIDIOut(); + this.MIDIout = output; + this._sendMessage(WorkletSequencerMessageType.changeMIDIMessageSending, output !== undefined); + this.currentTime -= 0.1; + } + + /** + * Pauses the playback + */ + pause() + { + if (this.paused) + { + SpessaSynthWarn("Already paused"); + return; + } + this.pausedTime = this.currentTime; + this._sendMessage(WorkletSequencerMessageType.pause); + } + + unpause() + { + this.pausedTime = undefined; + this.isFinished = false; + } + + /** + * Starts the playback + * @param resetTime {boolean} If true, time is set to 0 s + */ + play(resetTime = false) + { + if (this.isFinished) + { + resetTime = true; + } + this._recalculateStartTime(this.pausedTime || 0); + this.unpause(); + this._sendMessage(WorkletSequencerMessageType.play, resetTime); + } + + /** + * Stops the playback + */ + stop() + { + this._sendMessage(WorkletSequencerMessageType.stop); + } +} \ No newline at end of file diff --git a/spessasynth_lib/sequencer/worklet_sequencer/events.js b/spessasynth_lib/sequencer/worklet_sequencer/events.js new file mode 100644 index 0000000000000000000000000000000000000000..93b28f133c3f0d13812d49b93ca4ec7637b8a229 --- /dev/null +++ b/spessasynth_lib/sequencer/worklet_sequencer/events.js @@ -0,0 +1,199 @@ +import { + ALL_CHANNELS_OR_DIFFERENT_ACTION, + returnMessageType +} from "../../synthetizer/worklet_system/message_protocol/worklet_message.js"; +import { SongChangeType, WorkletSequencerMessageType, WorkletSequencerReturnMessageType } from "./sequencer_message.js"; +import { messageTypes, midiControllers } from "../../midi_parser/midi_message.js"; + +import { MIDI_CHANNEL_COUNT } from "../../synthetizer/synth_constants.js"; + +/** + * @param messageType {WorkletSequencerMessageType} + * @param messageData {any} + * @this {WorkletSequencer} + */ +export function processMessage(messageType, messageData) +{ + switch (messageType) + { + default: + break; + + case WorkletSequencerMessageType.loadNewSongList: + this.loadNewSongList(messageData[0], messageData[1]); + 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: + const [loop, count] = messageData; + this.loop = loop; + if (count === ALL_CHANNELS_OR_DIFFERENT_ACTION) + { + this.loopCount = Infinity; + } + else + { + this.loopCount = count; + } + break; + + case WorkletSequencerMessageType.changeSong: + switch (messageData[0]) + { + case SongChangeType.forwards: + this.nextSong(); + break; + + case SongChangeType.backwards: + this.previousSong(); + break; + + case SongChangeType.shuffleOff: + this.shuffleMode = false; + this.songIndex = this.shuffledSongIndexes[this.songIndex]; + break; + + case SongChangeType.shuffleOn: + this.shuffleMode = true; + this.shuffleSongIndexes(); + this.songIndex = 0; + this.loadCurrentSong(); + break; + + case SongChangeType.index: + this.songIndex = messageData[1]; + this.loadCurrentSong(); + break; + } + break; + + case WorkletSequencerMessageType.getMIDI: + this.post(WorkletSequencerReturnMessageType.getMIDI, this.midiData); + break; + + case WorkletSequencerMessageType.setSkipToFirstNote: + this.skipToFirstNoteOn = messageData; + break; + + case WorkletSequencerMessageType.setPreservePlaybackState: + this.preservePlaybackState = messageData; + } +} + +/** + * + * @param messageType {WorkletSequencerReturnMessageType} + * @param messageData {any} + * @this {WorkletSequencer} + */ +export function post(messageType, messageData = undefined) +{ + if (!this.synth.enableEventSystem) + { + return; + } + this.synth.post({ + messageType: returnMessageType.sequencerSpecific, + messageData: { + messageType: messageType, + messageData: messageData + } + }); +} + +/** + * @param message {number[]} + * @this {WorkletSequencer} + */ +export function sendMIDIMessage(message) +{ + this.post(WorkletSequencerReturnMessageType.midiEvent, message); +} + +/** + * @this {WorkletSequencer} + * @param channel {number} + * @param type {number} + * @param value {number} + */ +export function sendMIDICC(channel, type, value) +{ + channel %= 16; + if (!this.sendMIDIMessages) + { + return; + } + this.sendMIDIMessage([messageTypes.controllerChange | channel, type, value]); +} + +/** + * @this {WorkletSequencer} + * @param channel {number} + * @param program {number} + */ +export function sendMIDIProgramChange(channel, program) +{ + channel %= 16; + if (!this.sendMIDIMessages) + { + return; + } + this.sendMIDIMessage([messageTypes.programChange | channel, program]); +} + +/** + * Sets the pitch of the given channel + * @this {WorkletSequencer} + * @param channel {number} usually 0-15: the channel to change pitch + * @param MSB {number} SECOND byte of the MIDI pitchWheel message + * @param LSB {number} FIRST byte of the MIDI pitchWheel message + */ +export function sendMIDIPitchWheel(channel, MSB, LSB) +{ + channel %= 16; + if (!this.sendMIDIMessages) + { + return; + } + this.sendMIDIMessage([messageTypes.pitchBend | channel, LSB, MSB]); +} + +/** + * @this {WorkletSequencer} + */ +export function sendMIDIReset() +{ + if (!this.sendMIDIMessages) + { + return; + } + this.sendMIDIMessage([messageTypes.reset]); + 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]); + } +} \ No newline at end of file diff --git a/spessasynth_lib/sequencer/worklet_sequencer/play.js b/spessasynth_lib/sequencer/worklet_sequencer/play.js new file mode 100644 index 0000000000000000000000000000000000000000..479e77f2914cd1a06b5d602d35b6e180a3f371f4 --- /dev/null +++ b/spessasynth_lib/sequencer/worklet_sequencer/play.js @@ -0,0 +1,355 @@ +import { getEvent, messageTypes, midiControllers } from "../../midi_parser/midi_message.js"; +import { WorkletSequencerReturnMessageType } from "./sequencer_message.js"; +import { resetArray } from "../../synthetizer/worklet_system/worklet_utilities/controller_tables.js"; +import { + nonResetableCCs +} from "../../synthetizer/worklet_system/worklet_methods/controller_control/reset_controllers.js"; + + +// an array with preset default values +const defaultControllerArray = resetArray.slice(0, 128); + +/** + * plays from start to the target time, excluding note messages (to get the synth to the correct state) + * @private + * @param time {number} in seconds + * @param ticks {number} optional MIDI ticks, when given is used instead of time + * @returns {boolean} true if the midi file is not finished + * @this {WorkletSequencer} + */ +export function _playTo(time, ticks = undefined) +{ + this.oneTickToSeconds = 60 / (120 * this.midiData.timeDivision); + // reset + 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}[]} + */ + const programs = []; + for (let i = 0; i < channelsToSave; i++) + { + programs.push({ + program: -1, + bank: 0, + actualBank: 0 + }); + } + + const isCCNonSkippable = controllerNumber => ( + 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.resetAllControllers + ); + + /** + * Save controllers here and send them only after + * @type {number[][]} + */ + const savedControllers = []; + for (let i = 0; i < channelsToSave; i++) + { + savedControllers.push(Array.from(defaultControllerArray)); + } + + /** + * RP-15 compliant reset + * https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf + * @param chan {number} + */ + function resetAllControlllers(chan) + { + // reset pitch bend + pitchBends[chan] = 8192; + if (savedControllers?.[chan] === undefined) + { + return; + } + for (let i = 0; i < defaultControllerArray.length; i++) + { + if (!nonResetableCCs.has(i)) + { + savedControllers[chan][i] = defaultControllerArray[i]; + } + } + } + + while (true) + { + // find the next event + let trackIndex = this._findFirstEventIndex(); + let event = this.tracks[trackIndex][this.eventIndex[trackIndex]]; + if (ticks !== undefined) + { + if (event.ticks >= ticks) + { + break; + } + } + else + { + if (this.playedTime >= time) + { + break; + } + } + + // skip note ons + const info = getEvent(event.messageStatusByte); + // Keep in mind midi ports to determine the channel! + const channel = info.channel + (this.midiPortChannelOffsets[this.midiPorts[trackIndex]] || 0); + switch (info.status) + { + // skip note messages + case messageTypes.noteOn: + // track portamento control as last note + if (savedControllers[channel] === undefined) + { + savedControllers[channel] = Array.from(defaultControllerArray); + } + savedControllers[channel][midiControllers.portamentoControl] = event.messageData[0]; + break; + + case messageTypes.noteOff: + break; + + // skip pitch bend + case messageTypes.pitchBend: + pitchBends[channel] = event.messageData[1] << 7 | event.messageData[0]; + break; + + case messageTypes.programChange: + // empty tracks cannot program change + if (this.midiData.isMultiPort && this.midiData.usedChannelsOnTrack[trackIndex].size === 0) + { + break; + } + const p = programs[channel]; + p.program = event.messageData[0]; + p.actualBank = p.bank; + break; + + case messageTypes.controllerChange: + // empty tracks cannot controller change + if (this.midiData.isMultiPort && this.midiData.usedChannelsOnTrack[trackIndex].size === 0) + { + break; + } + // do not skip data entries + const controllerNumber = event.messageData[0]; + if (isCCNonSkippable(controllerNumber)) + { + let ccV = event.messageData[1]; + if (controllerNumber === midiControllers.bankSelect) + { + // add the bank to be saved + programs[channel].bank = ccV; + break; + } + else if (controllerNumber === midiControllers.resetAllControllers) + { + resetAllControlllers(channel); + } + if (this.sendMIDIMessages) + { + this.sendMIDICC(channel, controllerNumber, ccV); + } + else + { + this.synth.controllerChange(channel, controllerNumber, ccV); + } + } + else + { + 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 the next event + trackIndex = this._findFirstEventIndex(); + let nextEvent = this.tracks[trackIndex][this.eventIndex[trackIndex]]; + if (nextEvent === undefined) + { + this.stop(); + return false; + } + this.playedTime += this.oneTickToSeconds * (nextEvent.ticks - event.ticks); + } + + // restoring saved controllers + if (this.sendMIDIMessages) + { + for (let channelNumber = 0; channelNumber < channelsToSave; channelNumber++) + { + // restore pitch bends + if (pitchBends[channelNumber] !== undefined) + { + this.sendMIDIPitchWheel( + channelNumber, + pitchBends[channelNumber] >> 7, + pitchBends[channelNumber] & 0x7F + ); + } + if (savedControllers[channelNumber] !== undefined) + { + // every controller that has changed + 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) + { + const bank = programs[channelNumber].actualBank; + this.sendMIDICC(channelNumber, midiControllers.bankSelect, bank); + this.sendMIDIProgramChange(channelNumber, programs[channelNumber].program); + } + } + } + else + { + // for all synth channels + for (let channelNumber = 0; channelNumber < channelsToSave; channelNumber++) + { + // restore pitch bends + if (pitchBends[channelNumber] !== undefined) + { + this.synth.pitchWheel(channelNumber, pitchBends[channelNumber] >> 7, pitchBends[channelNumber] & 0x7F); + } + if (savedControllers[channelNumber] !== undefined) + { + // every controller that has changed + savedControllers[channelNumber].forEach((value, index) => + { + if (value !== defaultControllerArray[index] && !isCCNonSkippable( + index)) + { + this.synth.controllerChange( + channelNumber, + index, + value + ); + } + }); + } + // restore programs + if (programs[channelNumber].program >= 0 && programs[channelNumber].actualBank >= 0) + { + const bank = programs[channelNumber].actualBank; + this.synth.controllerChange(channelNumber, midiControllers.bankSelect, bank); + this.synth.programChange(channelNumber, programs[channelNumber].program); + } + } + } + return true; +} + +/** + * Starts the playback + * @param resetTime {boolean} If true, time is set to 0 s + * @this {WorkletSequencer} + */ +export function play(resetTime = false) +{ + if (this.midiData === undefined) + { + return; + } + + // reset the time if necessary + if (resetTime) + { + this.pausedTime = undefined; + this.currentTime = 0; + return; + } + + if (this.currentTime >= this.duration) + { + this.pausedTime = undefined; + this.currentTime = 0; + return; + } + + // unpause if paused + if (this.paused) + { + // adjust the start time + this._recalculateStartTime(this.pausedTime); + this.pausedTime = undefined; + } + if (!this.sendMIDIMessages) + { + this.playingNotes.forEach(n => + { + this.synth.noteOn(n.channel, n.midiNote, n.velocity); + }); + } + this.setProcessHandler(); +} + +/** + * @this {WorkletSequencer} + * @param ticks {number} + */ +export function setTimeTicks(ticks) +{ + this.stop(); + this.playingNotes = []; + this.pausedTime = undefined; + this.post( + WorkletSequencerReturnMessageType.timeChange, + currentTime - this.midiData.MIDIticksToSeconds(ticks) + ); + const isNotFinished = this._playTo(0, ticks); + this._recalculateStartTime(this.playedTime); + if (!isNotFinished) + { + return; + } + this.play(); +} + +/** + * @param time + * @private + * @this {WorkletSequencer} + */ +export function _recalculateStartTime(time) +{ + this.absoluteStartTime = currentTime - time / this._playbackRate; +} \ No newline at end of file diff --git a/spessasynth_lib/sequencer/worklet_sequencer/process_event.js b/spessasynth_lib/sequencer/worklet_sequencer/process_event.js new file mode 100644 index 0000000000000000000000000000000000000000..55c93b398fe57208b7c53ed3cfd0f656f045a663 --- /dev/null +++ b/spessasynth_lib/sequencer/worklet_sequencer/process_event.js @@ -0,0 +1,169 @@ +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"; + +/** + * Processes a single event + * @param event {MIDIMessage} + * @param trackIndex {number} + * @this {WorkletSequencer} + * @private + */ +export function _processEvent(event, trackIndex) +{ + 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; + statusByteData.channel += offset; + // process the event + switch (statusByteData.status) + { + case messageTypes.noteOn: + const velocity = event.messageData[1]; + if (velocity > 0) + { + this.synth.noteOn(statusByteData.channel, event.messageData[0], velocity); + this.playingNotes.push({ + midiNote: event.messageData[0], + channel: statusByteData.channel, + velocity: velocity + }); + } + else + { + 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) + { + 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) + { + this.playingNotes.splice(toDelete, 1); + } + break; + + case messageTypes.pitchBend: + this.synth.pitchWheel(statusByteData.channel, event.messageData[1], event.messageData[0]); + break; + + case messageTypes.controllerChange: + // empty tracks cannot cc change + if (this.midiData.isMultiPort && this.midiData.usedChannelsOnTrack[trackIndex].size === 0) + { + return; + } + this.synth.controllerChange(statusByteData.channel, event.messageData[0], event.messageData[1]); + break; + + case messageTypes.programChange: + // empty tracks cannot program change + if (this.midiData.isMultiPort && this.midiData.usedChannelsOnTrack[trackIndex].size === 0) + { + return; + } + 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: + event.messageData.currentIndex = 0; + let tempoBPM = 60000000 / readBytesAsUintBigEndian(event.messageData, 3); + this.oneTickToSeconds = 60 / (tempoBPM * this.midiData.timeDivision); + if (this.oneTickToSeconds === 0) + { + this.oneTickToSeconds = 60 / (120 * this.midiData.timeDivision); + SpessaSynthWarn("invalid tempo! falling back to 120 BPM"); + tempoBPM = 120; + } + break; + + // recognized but ignored + case messageTypes.timeSignature: + case messageTypes.endOfTrack: + case messageTypes.midiChannelPrefix: + case messageTypes.songPosition: + case messageTypes.activeSensing: + case messageTypes.keySignature: + case messageTypes.sequenceNumber: + case messageTypes.sequenceSpecific: + case messageTypes.text: + case messageTypes.lyric: + case messageTypes.copyright: + case messageTypes.trackName: + case messageTypes.marker: + case messageTypes.cuePoint: + case messageTypes.instrumentName: + case messageTypes.programName: + 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)}`, + consoleColors.warn, + consoleColors.unrecognized, + consoleColors.warn, + consoleColors.value + ); + break; + } + if (statusByteData.status >= 0 && statusByteData.status < 0x80) + { + this.post( + WorkletSequencerReturnMessageType.metaEvent, + [event, trackIndex] + ); + } +} + +/** + * Adds 16 channels to the synth + * @this {WorkletSequencer} + * @private + */ +export function _addNewMidiPort() +{ + for (let i = 0; i < 16; i++) + { + this.synth.createWorkletChannel(true); + } +} \ No newline at end of file diff --git a/spessasynth_lib/sequencer/worklet_sequencer/process_tick.js b/spessasynth_lib/sequencer/worklet_sequencer/process_tick.js new file mode 100644 index 0000000000000000000000000000000000000000..8f656649aec3ce3e129bce85027ef420649d66a8 --- /dev/null +++ b/spessasynth_lib/sequencer/worklet_sequencer/process_tick.js @@ -0,0 +1,106 @@ +import { WorkletSequencerReturnMessageType } from "./sequencer_message.js"; + +/** + * Processes a single tick + * @this {WorkletSequencer} + */ +export function processTick() +{ + if (!this.isActive) + { + return; + } + let current = this.currentTime; + while (this.playedTime < current) + { + // find the next event + let trackIndex = this._findFirstEventIndex(); + let event = this.tracks[trackIndex][this.eventIndex[trackIndex]]; + this._processEvent(event, trackIndex); + + this.eventIndex[trackIndex]++; + + // find the next event + trackIndex = this._findFirstEventIndex(); + if (this.tracks[trackIndex].length <= this.eventIndex[trackIndex]) + { + // the song has ended + if (this.loop) + { + this.setTimeTicks(this.midiData.loop.start); + return; + } + this.eventIndex[trackIndex]--; + this.pause(true); + if (this.songs.length > 1) + { + this.nextSong(); + } + return; + } + let eventNext = this.tracks[trackIndex][this.eventIndex[trackIndex]]; + this.playedTime += this.oneTickToSeconds * (eventNext.ticks - event.ticks); + + const canLoop = this.loop && (this.loopCount > 0 || this.loopCount === -1); + + // if we reached loop.end + if ((this.midiData.loop.end <= event.ticks) && canLoop) + { + // loop + if (this.loopCount !== Infinity) + { + this.loopCount--; + this.post(WorkletSequencerReturnMessageType.loopCountChange, this.loopCount); + } + this.setTimeTicks(this.midiData.loop.start); + return; + } + // if the song has ended + else if (current >= this.duration) + { + if (canLoop) + { + // loop + if (this.loopCount !== Infinity) + { + this.loopCount--; + this.post(WorkletSequencerReturnMessageType.loopCountChange, this.loopCount); + } + this.setTimeTicks(this.midiData.loop.start); + return; + } + // stop the playback + this.eventIndex[trackIndex]--; + this.pause(true); + if (this.songs.length > 1) + { + this.nextSong(); + } + return; + } + } +} + + +/** + * @returns {number} the index of the first to the current played time + * @this {WorkletSequencer} + */ +export function _findFirstEventIndex() +{ + let index = 0; + let ticks = Infinity; + this.tracks.forEach((track, i) => + { + if (this.eventIndex[i] >= track.length) + { + return; + } + if (track[this.eventIndex[i]].ticks < ticks) + { + index = i; + ticks = track[this.eventIndex[i]].ticks; + } + }); + return index; +} \ No newline at end of file diff --git a/spessasynth_lib/sequencer/worklet_sequencer/sequencer_message.js b/spessasynth_lib/sequencer/worklet_sequencer/sequencer_message.js new file mode 100644 index 0000000000000000000000000000000000000000..3043f178065fd7f71acfcc9aa3eee358d5c5dae3 --- /dev/null +++ b/spessasynth_lib/sequencer/worklet_sequencer/sequencer_message.js @@ -0,0 +1,53 @@ +export const SongChangeType = { + backwards: 0, // no additional data + forwards: 1, // no additional data + shuffleOn: 2, // no additional data + shuffleOff: 3, // no additional data + index: 4 // songIndex +}; + +/** + * @enum {number} + * @property {number} loadNewSongList - 0 -> [...song] + * @property {number} pause - 1 -> isFinished + * @property {number} stop - 2 -> (no data) + * @property {number} play - 3 -> resetTime + * @property {number} setTime - 4 -> time + * @property {number} changeMIDIMessageSending - 5 -> sendMIDIMessages + * @property {number} setPlaybackRate - 6 -> playbackRate + * @property {number} setLoop - 7 -> [loop, count] + * @property {number} changeSong - 8 -> [changeType, data] + * @property {number} getMIDI - 9 -> (no data) + * @property {number} setSkipToFirstNote -10 -> skipToFirstNoteOn + * @property {number} setPreservePlaybackState -11 -> preservePlaybackState + */ +export const WorkletSequencerMessageType = { + loadNewSongList: 0, + pause: 1, + stop: 2, + play: 3, + setTime: 4, + changeMIDIMessageSending: 5, + setPlaybackRate: 6, + setLoop: 7, + changeSong: 8, + getMIDI: 9, + setSkipToFirstNote: 10, + setPreservePlaybackState: 11 +}; + +/** + * + * @enum {number} + */ +export const WorkletSequencerReturnMessageType = { + midiEvent: 0, // [...midiEventBytes] + songChange: 1, // [songIndex, isAutoPlayed] + timeChange: 2, // newAbsoluteTime + pause: 3, // no data + getMIDI: 4, // midiData + midiError: 5, // errorMSG + metaEvent: 6, // [event, trackNum] + loopCountChange: 7, // newLoopCount + songListChange: 8 // songListData +}; \ No newline at end of file diff --git a/spessasynth_lib/sequencer/worklet_sequencer/song_control.js b/spessasynth_lib/sequencer/worklet_sequencer/song_control.js new file mode 100644 index 0000000000000000000000000000000000000000..fe7ffe7c1329214d95d204ac9ef86096ecee8a47 --- /dev/null +++ b/spessasynth_lib/sequencer/worklet_sequencer/song_control.js @@ -0,0 +1,229 @@ +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 { BasicMIDI } from "../../midi_parser/basic_midi.js"; + + +/** + * @param trackNum {number} + * @param port {number} + * @this {WorkletSequencer} + */ +export function assignMIDIPort(trackNum, port) +{ + // do not assign ports to empty tracks + if (this.midiData.usedChannelsOnTrack[trackNum].size === 0) + { + return; + } + + // assign new 16 channels if the port is not occupied yet + if (this.midiPortChannelOffset === 0) + { + this.midiPortChannelOffset += 16; + this.midiPortChannelOffsets[port] = 0; + } + + if (this.midiPortChannelOffsets[port] === undefined) + { + if (this.synth.workletProcessorChannels.length < this.midiPortChannelOffset + 15) + { + this._addNewMidiPort(); + } + this.midiPortChannelOffsets[port] = this.midiPortChannelOffset; + this.midiPortChannelOffset += 16; + } + + this.midiPorts[trackNum] = port; +} + +/** + * Loads a new sequence + * @param parsedMidi {BasicMIDI} + * @param autoPlay {boolean} + * @this {WorkletSequencer} + */ +export function loadNewSequence(parsedMidi, autoPlay = true) +{ + this.stop(); + if (!parsedMidi.tracks) + { + throw new Error("This MIDI has no tracks!"); + } + + this.oneTickToSeconds = 60 / (120 * parsedMidi.timeDivision); + + /** + * @type {BasicMIDI} + */ + this.midiData = parsedMidi; + + // check for embedded soundfont + 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) + { + // clean up the embedded soundfont + this.synth.clearSoundFont(true, true); + } + SpessaSynthGroupCollapsed("%cPreloading samples...", consoleColors.info); + // smart preloading: load only samples used in the midi! + const used = this.midiData.getUsedProgramsAndKeys(this.synth.soundfontManager); + for (const [programBank, combos] of Object.entries(used)) + { + 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...`, + consoleColors.info, + consoleColors.recognized, + 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.slice(); + + // clear last port data + this.midiPortChannelOffset = 0; + this.midiPortChannelOffsets = {}; + // assign port offsets + this.midiData.midiPorts.forEach((port, trackIndex) => + { + this.assignMIDIPort(trackIndex, port); + }); + + /** + * Same as "audio.duration" property (seconds) + * @type {number} + */ + this.duration = this.midiData.duration; + this.firstNoteTime = this.midiData.MIDIticksToSeconds(this.midiData.firstNoteOn); + SpessaSynthInfo(`%cTotal song time: ${formatTime(Math.ceil(this.duration)).time}`, consoleColors.recognized); + + this.post(WorkletSequencerReturnMessageType.songChange, [this.songIndex, autoPlay]); + + if (this.duration <= 1) + { + SpessaSynthWarn( + `%cVery short song: (${formatTime(Math.round(this.duration)).time}). Disabling loop!`, + consoleColors.warn + ); + this.loop = false; + } + if (autoPlay) + { + this.play(true); + } + else + { + // this shall not play: play to the first note and then wait + const targetTime = this.skipToFirstNoteOn ? this.midiData.firstNoteOn - 1 : 0; + this.setTimeTicks(targetTime); + this.pause(); + } +} + +/** + * @param midiBuffers {MIDIFile[]} + * @param autoPlay {boolean} + * @this {WorkletSequencer} + */ +export function loadNewSongList(midiBuffers, autoPlay = true) +{ + /** + * parse the MIDIs (only the array buffers, MIDI is unchanged) + * @type {BasicMIDI[]} + */ + this.songs = midiBuffers.reduce((mids, b) => + { + if (b.duration) + { + mids.push(BasicMIDI.copyFrom(b)); + return mids; + } + try + { + mids.push(new MIDI(b.binary, b.altName || "")); + } + catch (e) + { + console.error(e); + this.post(WorkletSequencerReturnMessageType.midiError, e); + return mids; + } + return mids; + }, []); + if (this.songs.length < 1) + { + return; + } + this.songIndex = 0; + if (this.songs.length > 1) + { + this.loop = false; + } + this.shuffleSongIndexes(); + const midiDatas = this.songs.map(s => new MIDIData(s)); + this.post(WorkletSequencerReturnMessageType.songListChange, midiDatas); + this.loadCurrentSong(autoPlay); +} + +/** + * @this {WorkletSequencer} + */ +export function nextSong() +{ + if (this.songs.length === 1) + { + this.currentTime = 0; + return; + } + this.songIndex++; + this.songIndex %= this.songs.length; + this.loadCurrentSong(); +} + +/** + * @this {WorkletSequencer} + */ +export function previousSong() +{ + if (this.songs.length === 1) + { + this.currentTime = 0; + return; + } + this.songIndex--; + if (this.songIndex < 0) + { + this.songIndex = this.songs.length - 1; + } + this.loadCurrentSong(); +} \ No newline at end of file diff --git a/spessasynth_lib/sequencer/worklet_sequencer/worklet_sequencer.js b/spessasynth_lib/sequencer/worklet_sequencer/worklet_sequencer.js new file mode 100644 index 0000000000000000000000000000000000000000..92ba45a5836f7e252db5591043b02205c33d6de2 --- /dev/null +++ b/spessasynth_lib/sequencer/worklet_sequencer/worklet_sequencer.js @@ -0,0 +1,336 @@ +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, + sendMIDICC, + sendMIDIMessage, + sendMIDIPitchWheel, + sendMIDIProgramChange, + sendMIDIReset +} from "./events.js"; +import { SpessaSynthWarn } from "../../utils/loggin.js"; + +import { MIDI_CHANNEL_COUNT } from "../../synthetizer/synth_constants.js"; + +class WorkletSequencer +{ + /** + * All the sequencer's songs + * @type {BasicMIDI[]} + */ + songs = []; + + /** + * Current song index + * @type {number} + */ + songIndex = 0; + + /** + * shuffled song indexes + * @type {number[]} + */ + shuffledSongIndexes = []; + + /** + * the synth to use + * @type {SpessaSynthProcessor} + */ + synth; + + /** + * if the sequencer is active + * @type {boolean} + */ + isActive = false; + + /** + * If the event should instead be sent back to the main thread instead of synth + * @type {boolean} + */ + sendMIDIMessages = false; + + /** + * sequencer's loop count + * @type {number} + */ + loopCount = Infinity; + + /** + * event's number in this.events + * @type {number[]} + */ + eventIndex = []; + + /** + * tracks the time that has already been played + * @type {number} + */ + playedTime = 0; + + /** + * The (relative) time when the sequencer was paused. If it's not paused, then it's undefined. + * @type {number} + */ + pausedTime = undefined; + + /** + * Absolute playback startTime, bases on the synth's time + * @type {number} + */ + absoluteStartTime = currentTime; + /** + * Currently playing notes (for pausing and resuming) + * @type {{ + * midiNote: number, + * channel: number, + * velocity: number + * }[]} + */ + playingNotes = []; + + /** + * controls if the sequencer loops (defaults to true) + * @type {boolean} + */ + loop = true; + + /** + * controls if the songs are ordered randomly + * @type {boolean} + */ + shuffleMode = false; + + /** + * the current track data + * @type {BasicMIDI} + */ + midiData = undefined; + + /** + * midi port number for the corresponding track + * @type {number[]} + */ + midiPorts = []; + midiPortChannelOffset = 0; + /** + * stored as: + * Object + * @type {Object} + */ + midiPortChannelOffsets = {}; + + /** + * @type {boolean} + */ + skipToFirstNoteOn = true; + + /** + * If true, seq will stay paused when seeking or changing the playback rate + * @type {boolean} + */ + preservePlaybackState = false; + + /** + * @param spessasynthProcessor {SpessaSynthProcessor} + */ + constructor(spessasynthProcessor) + { + this.synth = spessasynthProcessor; + } + + /** + * Controls the playback's rate + * @type {number} + * @private + */ + _playbackRate = 1; + + /** + * @param value {number} + */ + set playbackRate(value) + { + const time = this.currentTime; + this._playbackRate = value; + this.currentTime = time; + } + + get currentTime() + { + // return the paused time if it's set to something other than undefined + if (this.pausedTime !== undefined) + { + return this.pausedTime; + } + + return (currentTime - this.absoluteStartTime) * this._playbackRate; + } + + set currentTime(time) + { + if (time > this.duration || time < 0) + { + // time is 0 + if (this.skipToFirstNoteOn) + { + this.setTimeTicks(this.midiData.firstNoteOn - 1); + } + else + { + this.setTimeTicks(0); + } + return; + } + if (this.skipToFirstNoteOn) + { + if (time < this.firstNoteTime) + { + this.setTimeTicks(this.midiData.firstNoteOn - 1); + return; + } + } + this.stop(); + this.playingNotes = []; + const wasPaused = this.paused && this.preservePlaybackState; + this.pausedTime = undefined; + this.post(WorkletSequencerReturnMessageType.timeChange, currentTime - time); + if (this.midiData.duration === 0) + { + SpessaSynthWarn("No duration!"); + this.post(WorkletSequencerReturnMessageType.pause, true); + return; + } + this._playTo(time); + this._recalculateStartTime(time); + if (wasPaused) + { + this.pause(); + } + else + { + 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) + { + SpessaSynthWarn("Already paused"); + return; + } + this.pausedTime = this.currentTime; + this.stop(); + this.post(WorkletSequencerReturnMessageType.pause, isFinished); + } + + /** + * Stops the playback + */ + stop() + { + this.clearProcessHandler(); + // disable sustain + for (let i = 0; i < 16; i++) + { + this.synth.controllerChange(i, midiControllers.sustainPedal, 0); + } + this.synth.stopAllChannels(); + if (this.sendMIDIMessages) + { + for (let note of this.playingNotes) + { + this.sendMIDIMessage([messageTypes.noteOff | (note.channel % 16), note.midiNote]); + } + for (let c = 0; c < MIDI_CHANNEL_COUNT; c++) + { + this.sendMIDICC(c, midiControllers.allNotesOff, 0); + } + } + } + + loadCurrentSong(autoPlay = true) + { + let index = this.songIndex; + if (this.shuffleMode) + { + index = this.shuffledSongIndexes[this.songIndex]; + } + this.loadNewSequence(this.songs[index], autoPlay); + } + + _resetTimers() + { + this.playedTime = 0; + this.eventIndex = Array(this.tracks.length).fill(0); + } + + setProcessHandler() + { + this.isActive = true; + } + + clearProcessHandler() + { + this.isActive = false; + } + + shuffleSongIndexes() + { + const indexes = this.songs.map((_, i) => i); + this.shuffledSongIndexes = []; + while (indexes.length > 0) + { + const index = indexes[Math.floor(Math.random() * indexes.length)]; + this.shuffledSongIndexes.push(index); + indexes.splice(indexes.indexOf(index), 1); + } + } +} + +// Web MIDI sending +WorkletSequencer.prototype.sendMIDIMessage = sendMIDIMessage; +WorkletSequencer.prototype.sendMIDIReset = sendMIDIReset; +WorkletSequencer.prototype.sendMIDICC = sendMIDICC; +WorkletSequencer.prototype.sendMIDIProgramChange = sendMIDIProgramChange; +WorkletSequencer.prototype.sendMIDIPitchWheel = sendMIDIPitchWheel; +WorkletSequencer.prototype.assignMIDIPort = assignMIDIPort; + +WorkletSequencer.prototype.post = post; +WorkletSequencer.prototype.processMessage = processMessage; + +WorkletSequencer.prototype._processEvent = _processEvent; +WorkletSequencer.prototype._addNewMidiPort = _addNewMidiPort; +WorkletSequencer.prototype.processTick = processTick; +WorkletSequencer.prototype._findFirstEventIndex = _findFirstEventIndex; + +WorkletSequencer.prototype.loadNewSequence = loadNewSequence; +WorkletSequencer.prototype.loadNewSongList = loadNewSongList; +WorkletSequencer.prototype.nextSong = nextSong; +WorkletSequencer.prototype.previousSong = previousSong; + +WorkletSequencer.prototype.play = play; +WorkletSequencer.prototype._playTo = _playTo; +WorkletSequencer.prototype.setTimeTicks = setTimeTicks; +WorkletSequencer.prototype._recalculateStartTime = _recalculateStartTime; + +export { WorkletSequencer }; \ No newline at end of file diff --git a/spessasynth_lib/soundfont/README.md b/spessasynth_lib/soundfont/README.md new file mode 100644 index 0000000000000000000000000000000000000000..461416c8c7649baa0184dcb922509dce1b73b286 --- /dev/null +++ b/spessasynth_lib/soundfont/README.md @@ -0,0 +1,13 @@ +## 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. +Default modulators are also stored here (in `modulators.js`) + +`basic_soundfont` folder contains the classes that represent the soundfont file. + +`read_sf2` folder contains the code for reading an `.sf2` file. + +`write` folder contains the code for writing out an `.sf2` file. + +`dls` folder contains the code for reading a `.dls` file (and converting in into a soundfont representation). \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/basic_instrument.js b/spessasynth_lib/soundfont/basic_soundfont/basic_instrument.js new file mode 100644 index 0000000000000000000000000000000000000000..d6d19c2c329364e666d3ce045215cfd0e1329d5c --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/basic_instrument.js @@ -0,0 +1,77 @@ +export class BasicInstrument +{ + /** + * The instrument's name + * @type {string} + */ + instrumentName = ""; + + /** + * The instrument's zones + * @type {BasicInstrumentZone[]} + */ + instrumentZones = []; + + /** + * Instrument's use count, used for trimming + * @type {number} + * @private + */ + _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++) + { + if (this.safeDeleteZone(i)) + { + i--; + } + } + } + + deleteInstrument() + { + this.instrumentZones.forEach(z => z.deleteZone()); + this.instrumentZones.length = 0; + } + + /** + * @param index {number} + * @returns {boolean} is the zone has been deleted + */ + safeDeleteZone(index) + { + this.instrumentZones[index].useCount--; + if (this.instrumentZones[index].useCount < 1) + { + this.deleteZone(index); + return true; + } + return false; + } + + /** + * @param index {number} + */ + deleteZone(index) + { + this.instrumentZones[index].deleteZone(); + this.instrumentZones.splice(index, 1); + } +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/basic_preset.js b/spessasynth_lib/soundfont/basic_soundfont/basic_preset.js new file mode 100644 index 0000000000000000000000000000000000000000..20842b8f8e9fa20caf2d3ab923fe30b0131ecb95 --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/basic_preset.js @@ -0,0 +1,336 @@ +/** + * @typedef {{ + * instrumentGenerators: Generator[], + * presetGenerators: Generator[], + * modulators: Modulator[], + * sample: BasicSample, + * sampleID: number, + * }} SampleAndGenerators + */ +import { generatorTypes } from "./generator.js"; +import { Modulator } from "./modulator.js"; +import { isXGDrums } from "../../utils/xg_hacks.js"; + +export class BasicPreset +{ + /** + * The parent soundbank instance + * Currently used for determining default modulators and XG status + * @type {BasicSoundBank} + */ + parentSoundBank; + + /** + * The preset's name + * @type {string} + */ + presetName = ""; + + /** + * The preset's MIDI program number + * @type {number} + */ + program = 0; + + /** + * The preset's MIDI bank number + * @type {number} + */ + bank = 0; + + /** + * The preset's zones + * @type {BasicPresetZone[]} + */ + presetZones = []; + + /** + * Stores already found getSamplesAndGenerators for reuse + * @type {SampleAndGenerators[][][]} + */ + foundSamplesAndGenerators = []; + + /** + * unused metadata + * @type {number} + */ + library = 0; + /** + * unused metadata + * @type {number} + */ + genre = 0; + /** + * unused metadata + * @type {number} + */ + morphology = 0; + + /** + * Creates a new preset representation + * @param parentSoundBank {BasicSoundBank} + */ + constructor(parentSoundBank) + { + this.parentSoundBank = parentSoundBank; + for (let i = 0; i < 128; i++) + { + this.foundSamplesAndGenerators[i] = []; + } + } + + /** + * @param allowXG {boolean} + * @param allowSFX {boolean} + * @returns {boolean} + */ + isDrumPreset(allowXG, allowSFX = false) + { + const xg = allowXG && this.parentSoundBank.isXGBank; + // sfx is not cool + return this.bank === 128 || ( + xg && + (isXGDrums(this.bank) && (this.bank !== 126 || allowSFX)) + ); + } + + deletePreset() + { + this.presetZones.forEach(z => z.deleteZone()); + this.presetZones.length = 0; + } + + /** + * @param index {number} + */ + deleteZone(index) + { + this.presetZones[index].deleteZone(); + this.presetZones.splice(index, 1); + } + + // noinspection JSUnusedGlobalSymbols + /** + * Preloads all samples (async) + */ + preload(keyMin, keyMax) + { + for (let key = keyMin; key < keyMax + 1; key++) + { + for (let velocity = 0; velocity < 128; velocity++) + { + this.getSamplesAndGenerators(key, velocity).forEach(samandgen => + { + if (!samandgen.sample.isSampleLoaded) + { + samandgen.sample.getAudioData(); + } + }); + } + } + } + + /** + * Preloads a specific key/velocity combo + * @param key {number} + * @param velocity {number} + */ + preloadSpecific(key, velocity) + { + this.getSamplesAndGenerators(key, velocity).forEach(samandgen => + { + if (!samandgen.sample.isSampleLoaded) + { + samandgen.sample.getAudioData(); + } + }); + } + + /** + * Returns generatorTranslator and generators for given note + * @param midiNote {number} + * @param velocity {number} + * @returns {SampleAndGenerators[]} + */ + getSamplesAndGenerators(midiNote, velocity) + { + const memorized = this.foundSamplesAndGenerators[midiNote][velocity]; + if (memorized) + { + return memorized; + } + + if (this.presetZones.length < 1) + { + return []; + } + + /** + * @param range {SoundFontRange} + * @param number {number} + * @returns {boolean} + */ + function isInRange(range, number) + { + return number >= range.min && number <= range.max; + } + + /** + * @param main {Generator[]} + * @param adder {Generator[]} + */ + function addUnique(main, adder) + { + main.push(...adder.filter(g => !main.find(mg => mg.generatorType === g.generatorType))); + } + + /** + * @param main {Modulator[]} + * @param adder {Modulator[]} + */ + function addUniqueMods(main, adder) + { + 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] : []; + + /** + * @type {Modulator[]} + */ + let globalPresetModulators = this.presetZones[0].isGlobal ? [...this.presetZones[0].modulators] : []; + const globalKeyRange = this.presetZones[0].isGlobal ? this.presetZones[0].keyRange : { min: 0, max: 127 }; + const globalVelRange = this.presetZones[0].isGlobal ? this.presetZones[0].velRange : { min: 0, max: 127 }; + + // find the preset zones in range + let presetZonesInRange = this.presetZones.filter(currentZone => + ( + isInRange( + currentZone.hasKeyRange ? currentZone.keyRange : globalKeyRange, + midiNote + ) + && + isInRange( + currentZone.hasVelRange ? currentZone.velRange : globalVelRange, + velocity + ) + ) && !currentZone.isGlobal); + + presetZonesInRange.forEach(zone => + { + // the global zone is already taken into account earlier + if (zone.instrument.instrumentZones.length < 1) + { + return; + } + let presetGenerators = zone.generators; + let presetModulators = zone.modulators; + const firstZone = zone.instrument.instrumentZones[0]; + /** + * global zone is always first, so it or nothing + * @type {Generator[]} + */ + let globalInstrumentGenerators = firstZone.isGlobal ? [...firstZone.generators] : []; + let globalInstrumentModulators = firstZone.isGlobal ? [...firstZone.modulators] : []; + const globalKeyRange = firstZone.isGlobal ? firstZone.keyRange : { min: 0, max: 127 }; + const globalVelRange = firstZone.isGlobal ? firstZone.velRange : { min: 0, max: 127 }; + + + let instrumentZonesInRange = zone.instrument.instrumentZones + .filter(currentZone => + ( + isInRange( + currentZone.hasKeyRange ? currentZone.keyRange : globalKeyRange, + midiNote + ) + && + isInRange( + currentZone.hasVelRange ? currentZone.velRange : globalVelRange, + velocity + ) + ) && !currentZone.isGlobal + ); + + instrumentZonesInRange.forEach(instrumentZone => + { + let instrumentGenerators = [...instrumentZone.generators]; + let instrumentModulators = [...instrumentZone.modulators]; + + 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 + ); + + // default mods + addUniqueMods( + instrumentModulators, + this.parentSoundBank.defaultModulators + ); + + /** + * sum preset modulators to instruments (amount) sf spec page 54 + * @type {Modulator[]} + */ + const finalModulatorList = [...instrumentModulators]; + for (let i = 0; i < presetModulators.length; i++) + { + let mod = presetModulators[i]; + 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); + } + 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 + }); + }); + }); + + // save and return + this.foundSamplesAndGenerators[midiNote][velocity] = parsedGeneratorsAndSamples; + return parsedGeneratorsAndSamples; + } +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/basic_sample.js b/spessasynth_lib/soundfont/basic_soundfont/basic_sample.js new file mode 100644 index 0000000000000000000000000000000000000000..597e424dfa3c737b3aadd1a2d933ec4356c0b508 --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/basic_sample.js @@ -0,0 +1,197 @@ +/** + * samples.js + * purpose: parses soundfont samples, resamples if needed. + * loads sample data, handles async loading of sf3 compressed samples + */ +import { SpessaSynthWarn } from "../../utils/loggin.js"; + +// should be reasonable for most cases +const RESAMPLE_RATE = 48000; + +export class BasicSample +{ + + /** + * The sample's name + * @type {string} + */ + sampleName; + + /** + * Sample rate in Hz + * @type {number} + */ + sampleRate; + + /** + * Original pitch of the sample as a MIDI note number + * @type {number} + */ + samplePitch; + + /** + * Pitch correction, in cents. Can be negative + * @type {number} + */ + samplePitchCorrection; + + /** + * Sample link, currently unused here + * @type {number} + */ + sampleLink; + + /** + * Type of the sample, currently only used for SF3 + * @type {number} + */ + sampleType; + + /** + * Relative to the start of the sample in sample points + * @type {number} + */ + sampleLoopStartIndex; + + /** + * Relative to the start of the sample in sample points + * @type {number} + */ + sampleLoopEndIndex; + + /** + * Indicates if the sample is compressed + * @type {boolean} + */ + isCompressed; + + /** + * The compressed sample data if it was compressed by spessasynth + * @type {Uint8Array} + */ + compressedData = undefined; + + /** + * The sample's use count + * @type {number} + */ + useCount = 0; + + /** + * The sample's audio data + * @type {Float32Array} + */ + sampleData = undefined; + + /** + * The basic representation of a soundfont sample + * @param sampleName {string} The sample's name + * @param sampleRate {number} The sample's rate in Hz + * @param samplePitch {number} The sample's pitch as a MIDI note number + * @param samplePitchCorrection {number} The sample's pitch correction in cents + * @param sampleLink {number} The sample's link, currently unused + * @param sampleType {number} The sample's type, an enum + * @param loopStart {number} The sample's loop start relative to the sample start in sample points + * @param loopEnd {number} The sample's loop end relative to the sample start in sample points + */ + constructor( + sampleName, + sampleRate, + samplePitch, + samplePitchCorrection, + sampleLink, + sampleType, + loopStart, + loopEnd + ) + { + this.sampleName = sampleName; + this.sampleRate = sampleRate; + this.samplePitch = samplePitch; + this.samplePitchCorrection = samplePitchCorrection; + this.sampleLink = sampleLink; + this.sampleType = sampleType; + this.sampleLoopStartIndex = loopStart; + this.sampleLoopEndIndex = loopEnd; + // https://github.com/FluidSynth/fluidsynth/wiki/SoundFont3Format + this.isCompressed = (sampleType & 0x10) > 0; + } + + + /** + * @returns {Uint8Array|IndexedByteArray} + */ + getRawData() + { + const uint8 = new Uint8Array(this.sampleData.length * 2); + for (let i = 0; i < this.sampleData.length; i++) + { + const sample = Math.floor(this.sampleData[i] * 32768); + uint8[i * 2] = sample & 0xFF; // lower byte + uint8[i * 2 + 1] = (sample >> 8) & 0xFF; // upper byte + } + return uint8; + } + + resampleData(newSampleRate) + { + let audioData = this.getAudioData(); + const ratio = newSampleRate / this.sampleRate; + const resampled = new Float32Array(Math.floor(audioData.length * ratio)); + for (let i = 0; i < resampled.length; i++) + { + resampled[i] = audioData[Math.floor(i * (1 / ratio))]; + } + audioData = resampled; + this.sampleRate = newSampleRate; + // adjust loop points + this.sampleLoopStartIndex = Math.floor(this.sampleLoopStartIndex * ratio); + this.sampleLoopEndIndex = Math.floor(this.sampleLoopEndIndex * ratio); + this.sampleData = audioData; + } + + /** + * @param quality {number} + * @param encodeVorbis {EncodeVorbisFunction} + */ + compressSample(quality, encodeVorbis) + { + // no need to compress + if (this.isCompressed) + { + return; + } + // compress, always mono! + try + { + // if the sample rate is too low or too high, resample + let audioData = this.getAudioData(); + if (this.sampleRate < 8000 || this.sampleRate > 96000) + { + this.resampleData(RESAMPLE_RATE); + audioData = this.getAudioData(); + } + this.compressedData = encodeVorbis([audioData], 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; + // flag as uncompressed + this.sampleType &= 0xEF; + } + + } + + /** + * @returns {Float32Array} + */ + getAudioData() + { + return this.sampleData; + } +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/basic_soundfont.js b/spessasynth_lib/soundfont/basic_soundfont/basic_soundfont.js new file mode 100644 index 0000000000000000000000000000000000000000..918ea6f8fbc1d26751803e4728bc8269cc47af08 --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/basic_soundfont.js @@ -0,0 +1,565 @@ +import { + SpessaSynthGroup, + SpessaSynthGroupCollapsed, + SpessaSynthGroupEnd, + SpessaSynthInfo, + SpessaSynthWarn +} from "../../utils/loggin.js"; +import { consoleColors } from "../../utils/other.js"; +import { write } from "./write_sf2/write.js"; +import { defaultModulators, Modulator } from "./modulator.js"; +import { writeDLS } from "./write_dls/write_dls.js"; +import { BasicSample } from "./basic_sample.js"; +import { BasicInstrumentZone, BasicPresetZone } from "./basic_zones.js"; +import { Generator, generatorTypes } from "./generator.js"; +import { BasicInstrument } from "./basic_instrument.js"; +import { BasicPreset } from "./basic_preset.js"; +import { isXGDrums } from "../../utils/xg_hacks.js"; + +class BasicSoundBank +{ + + /** + * Soundfont's info stored as name: value. ifil and iver are stored as string representation of float (e.g., 2.1) + * @type {Object} + */ + soundFontInfo = {}; + + /** + * The soundfont's presets + * @type {BasicPreset[]} + */ + presets = []; + + /** + * The soundfont's samples + * @type {BasicSample[]} + */ + samples = []; + + /** + * The soundfont's instruments + * @type {BasicInstrument[]} + */ + instruments = []; + + /** + * Soundfont's default modulatorss + * @type {Modulator[]} + */ + defaultModulators = defaultModulators.map(m => Modulator.copy(m)); + + /** + * Checks for XG drumsets and considers if this soundfont is XG. + * @type {boolean} + */ + isXGBank = false; + + /** + * Creates a new basic soundfont template + * @param data {undefined|{presets: BasicPreset[], info: Object}} + */ + constructor(data = undefined) + { + 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 {...BasicSoundBank} the soundfonts to merge, the first overwrites the last + * @returns {BasicSoundBank} + */ + static mergeSoundBanks(...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 BasicSoundBank({ presets: presets, info: mainSf.soundFontInfo }); + } + + /** + * Creates a simple soundfont with one saw wave preset. + * @returns {ArrayBufferLike} + */ + static getDummySoundfontFile() + { + const font = new BasicSoundBank(); + const sample = new BasicSample( + "Saw", + 44100, + 65, + 20, + 0, + 0, + 0, + 127 + ); + sample.sampleData = new Float32Array(128); + for (let i = 0; i < 128; i++) + { + sample.sampleData[i] = (i / 128) * 2 - 1; + } + font.samples.push(sample); + + const gZone = new BasicInstrumentZone(); + gZone.isGlobal = true; + gZone.generators.push(new Generator(generatorTypes.initialAttenuation, 375)); + gZone.generators.push(new Generator(generatorTypes.releaseVolEnv, -1000)); + gZone.generators.push(new Generator(generatorTypes.sampleModes, 1)); + + const zone1 = new BasicInstrumentZone(); + zone1.sample = sample; + + const zone2 = new BasicInstrumentZone(); + zone2.sample = sample; + zone2.generators.push(new Generator(generatorTypes.fineTune, -9)); + + + const inst = new BasicInstrument(); + inst.instrumentName = "Saw Wave"; + inst.instrumentZones.push(gZone); + inst.instrumentZones.push(zone1); + inst.instrumentZones.push(zone2); + font.instruments.push(inst); + + const pZone = new BasicPresetZone(); + pZone.instrument = inst; + + const preset = new BasicPreset(font); + preset.presetName = "Saw Wave"; + preset.presetZones.push(pZone); + font.presets.push(preset); + + font.soundFontInfo["ifil"] = "2.1"; + font.soundFontInfo["isng"] = "EMU8000"; + font.soundFontInfo["INAM"] = "Dummy"; + font._parseInternal(); + return font.write().buffer; + } + + /** + * parses the bank after loading is done + * @protected + */ + _parseInternal() + { + this.isXGBank = false; + // definitions for XG: + // at least one preset with bank 127, 126 or 120 + // MUST be a valid XG bank. + // allowed banks: (see XG specification) + // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 24, + // 25, 27, 28, 29, 30, 31, 32, 33, 40, 41, 48, 56, 57, 58, + // 64, 65, 66, 126, 127 + const allowedPrograms = new Set([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 24, + 25, 27, 28, 29, 30, 31, 32, 33, 40, 41, 48, 56, 57, 58, + 64, 65, 66, 126, 127 + ]); + for (const preset of this.presets) + { + if (isXGDrums(preset.bank)) + { + this.isXGBank = true; + if (!allowedPrograms.has(preset.program)) + { + // not valid! + this.isXGBank = false; + SpessaSynthInfo( + `%cThis bank is not valid XG. Preset %c${preset.bank}:${preset.program}%c is not a valid XG drum. XG mode will use presets on bank 128.`, + consoleColors.info, + consoleColors.value, + consoleColors.info + ); + break; + } + } + } + } + + /** + * Trims a sound bank to only contain samples in a given MIDI file + * @param mid {BasicMIDI} - the MIDI file + */ + trimSoundBank(mid) + { + const soundfont = this; + + /** + * @param instrument {Instrument} + * @param combos {{key: number, velocity: number}[]} + * @returns {number} + */ + function trimInstrumentZones(instrument, combos) + { + let trimmedIZones = 0; + for (let iZoneIndex = 0; iZoneIndex < instrument.instrumentZones.length; iZoneIndex++) + { + const iZone = instrument.instrumentZones[iZoneIndex]; + if (iZone.isGlobal) + { + continue; + } + const iKeyRange = iZone.keyRange; + const iVelRange = iZone.velRange; + let isIZoneUsed = false; + for (const iCombo of combos) + { + if ( + (iCombo.key >= iKeyRange.min && iCombo.key <= iKeyRange.max) && + (iCombo.velocity >= iVelRange.min && iCombo.velocity <= iVelRange.max) + ) + { + isIZoneUsed = true; + break; + } + } + if (!isIZoneUsed) + { + 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)) + { + trimmedIZones++; + iZoneIndex--; + SpessaSynthInfo( + `%c${iZone.sample.sampleName} %cdeleted`, + consoleColors.recognized, + consoleColors.info + ); + } + if (iZone.sample.useCount < 1) + { + soundfont.deleteSample(iZone.sample); + } + } + + } + return trimmedIZones; + } + + SpessaSynthGroup( + "%cTrimming soundfont...", + consoleColors.info + ); + const usedProgramsAndKeys = mid.getUsedProgramsAndKeys(soundfont); + + 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++) + { + const p = soundfont.presets[presetIndex]; + const string = p.bank + ":" + p.program; + const used = usedProgramsAndKeys[string]; + if (used === undefined) + { + SpessaSynthInfo( + `%cDeleting preset %c${p.presetName}%c and its zones`, + consoleColors.info, + consoleColors.recognized, + consoleColors.info + ); + soundfont.deletePreset(p); + presetIndex--; + } + else + { + const combos = [...used].map(s => + { + const split = s.split("-"); + return { + key: parseInt(split[0]), + velocity: parseInt(split[1]) + }; + }); + SpessaSynthGroupCollapsed( + `%cTrimming %c${p.presetName}`, + consoleColors.info, + 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) + { + continue; + } + const keyRange = zone.keyRange; + const velRange = zone.velRange; + // check if any of the combos matches the zone + let isZoneUsed = false; + for (const combo of combos) + { + if ( + (combo.key >= keyRange.min && combo.key <= keyRange.max) && + (combo.velocity >= velRange.min && combo.velocity <= velRange.max) + ) + { + // 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}`, + consoleColors.info, + consoleColors.recognized, + consoleColors.info, + consoleColors.recognized + ); + break; + } + } + if (!isZoneUsed) + { + trimmedZones++; + p.deleteZone(zoneIndex); + if (zone.instrument.useCount < 1) + { + soundfont.deleteInstrument(zone.instrument); + } + zoneIndex--; + } + } + SpessaSynthInfo( + `%cTrimmed off %c${trimmedZones}%c zones from %c${p.presetName}`, + consoleColors.info, + consoleColors.recognized, + consoleColors.info, + consoleColors.recognized + ); + SpessaSynthGroupEnd(); + } + } + 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 + ); + SpessaSynthGroupEnd(); + SpessaSynthGroupEnd(); + } + + removeUnusedElements() + { + this.instruments.forEach(i => + { + if (i.useCount < 1) + { + 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) + { + throw new Error(`Cannot delete an instrument that has ${instrument.useCount} usages.`); + } + this.instruments.splice(this.instruments.indexOf(instrument), 1); + 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) + { + throw new Error(`Cannot delete sample that has ${sample.useCount} usages.`); + } + this.samples.splice(this.samples.indexOf(sample), 1); + this.removeUnusedElements(); + } + + /** + * Get the appropriate preset, undefined if not found + * @param bankNr {number} + * @param programNr {number} + * @param allowXGDrums {boolean} if true, allows XG drum banks (120, 126 and 127) as drum preset + * @return {BasicPreset} + */ + getPresetNoFallback(bankNr, programNr, allowXGDrums = false) + { + const isDrum = bankNr === 128 || (allowXGDrums && isXGDrums(bankNr)); + // check for exact match + let p; + if (isDrum) + { + p = this.presets.find(p => p.bank === bankNr && p.isDrumPreset(allowXGDrums) && p.program === programNr); + } + else + { + p = this.presets.find(p => p.bank === bankNr && p.program === programNr); + } + if (p) + { + return p; + } + // no match... + if (isDrum) + { + if (allowXGDrums) + { + // try any drum preset with matching program? + const p = this.presets.find(p => p.isDrumPreset(allowXGDrums) && p.program === programNr); + if (p) + { + return p; + } + } + } + return undefined; + } + + /** + * Get the appropriate preset + * @param bankNr {number} + * @param programNr {number} + * @param allowXGDrums {boolean} if true, allows XG drum banks (120, 126 and 127) as drum preset + * @returns {BasicPreset} + */ + getPreset(bankNr, programNr, allowXGDrums = false) + { + const isDrums = bankNr === 128 || (allowXGDrums && isXGDrums(bankNr)); + // check for exact match + let preset; + // only allow drums if the preset is considered to be a drum preset + if (isDrums) + { + preset = this.presets.find(p => p.bank === bankNr && p.isDrumPreset(allowXGDrums) && p.program === programNr); + } + else + { + preset = this.presets.find(p => p.bank === bankNr && p.program === programNr); + } + if (preset) + { + return preset; + } + // no match... + if (isDrums) + { + // drum preset: find any preset with bank 128 + preset = this.presets.find(p => p.isDrumPreset(allowXGDrums) && p.program === programNr); + if (!preset) + { + // only allow 128, otherwise it would default to XG SFX + preset = this.presets.find(p => p.isDrumPreset(allowXGDrums)); + } + } + else + { + // 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.isDrumPreset(allowXGDrums)); + } + if (preset) + { + SpessaSynthWarn( + `%cPreset ${bankNr}.${programNr} not found. Replaced with %c${preset.presetName} (${preset.bank}.${preset.program})`, + consoleColors.warn, + consoleColors.recognized + ); + } + + // no preset, use the first one available + 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} + * @returns {BasicPreset} + */ + getPresetByName(presetName) + { + let preset = this.presets.find(p => p.presetName === presetName); + if (!preset) + { + SpessaSynthWarn("Preset not found. Defaulting to:", this.presets[0].presetName); + preset = this.presets[0]; + } + return preset; + } + + /** + * @param error {string} + */ + parsingError(error) + { + throw new Error(`SF parsing error: ${error} The file may be corrupted.`); + } + + destroySoundBank() + { + delete this.presets; + delete this.instruments; + delete this.samples; + } +} + +BasicSoundBank.prototype.write = write; +BasicSoundBank.prototype.writeDLS = writeDLS; + +export { BasicSoundBank }; \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/basic_zone.js b/spessasynth_lib/soundfont/basic_soundfont/basic_zone.js new file mode 100644 index 0000000000000000000000000000000000000000..7380b761021a1522645c3577ca311d844e254e18 --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/basic_zone.js @@ -0,0 +1,64 @@ +/** + * @typedef {Object} SoundFontRange + * @property {number} min - the minimum midi note + * @property {number} max - the maximum midi note + */ + +export class BasicZone +{ + /** + * The zone's velocity range + * min -1 means that it is a default value + * @type {SoundFontRange} + */ + velRange = { min: -1, max: 127 }; + + /** + * The zone's key range + * min -1 means that it is a default value + * @type {SoundFontRange} + */ + keyRange = { min: -1, 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[]} + */ + modulators = []; + + /** + * @returns {boolean} + */ + get hasKeyRange() + { + return this.keyRange.min !== -1; + } + + /** + * @returns {boolean} + */ + get hasVelRange() + { + return this.velRange.min !== -1; + } + + /** + * @param generatorType {generatorTypes} + * @param notFoundValue {number} + * @returns {number} + */ + getGeneratorValue(generatorType, notFoundValue) + { + return this.generators.find(g => g.generatorType === generatorType)?.generatorValue ?? notFoundValue; + } +} + diff --git a/spessasynth_lib/soundfont/basic_soundfont/basic_zones.js b/spessasynth_lib/soundfont/basic_soundfont/basic_zones.js new file mode 100644 index 0000000000000000000000000000000000000000..747c9896b4927959d6f84fc1f812ed476fc1ff94 --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/basic_zones.js @@ -0,0 +1,43 @@ +import { BasicZone } from "./basic_zone.js"; + +export class BasicInstrumentZone extends BasicZone +{ + /** + * Zone's sample. Undefined if global + * @type {BasicSample|undefined} + */ + sample = undefined; + /** + * The zone's use count + * @type {number} + */ + useCount = 0; + + deleteZone() + { + this.useCount--; + if (this.isGlobal) + { + return; + } + this.sample.useCount--; + } +} + +export class BasicPresetZone extends BasicZone +{ + /** + * Zone's instrument. Undefined if global + * @type {BasicInstrument|undefined} + */ + instrument = undefined; + + deleteZone() + { + if (this.isGlobal) + { + return; + } + this.instrument.removeUseCount(); + } +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/generator.js b/spessasynth_lib/soundfont/basic_soundfont/generator.js new file mode 100644 index 0000000000000000000000000000000000000000..c303e39bde37f8edcf5c1b36850d77abe1721025 --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/generator.js @@ -0,0 +1,220 @@ +/** + * @enum {number} + */ +export const generatorTypes = { + INVALID: -1, // invalid generator + startAddrsOffset: 0, // sample control - moves sample start point + endAddrOffset: 1, // sample control - moves sample end point + startloopAddrsOffset: 2, // loop control - moves loop start point + endloopAddrsOffset: 3, // loop control - moves loop end point + startAddrsCoarseOffset: 4, // sample control - moves sample start point in 32,768 increments + modLfoToPitch: 5, // pitch modulation - modulation lfo pitch modulation in cents + vibLfoToPitch: 6, // pitch modulation - vibrato lfo pitch modulation in cents + modEnvToPitch: 7, // pitch modulation - modulation envelope pitch modulation in cents + initialFilterFc: 8, // filter - lowpass filter cutoff in cents + initialFilterQ: 9, // filter - lowpass filter resonance + modLfoToFilterFc: 10, // filter modulation - modulation lfo lowpass filter cutoff in cents + modEnvToFilterFc: 11, // filter modulation - modulation envelope lowpass filter cutoff in cents + endAddrsCoarseOffset: 12, // ample control - move sample end point in 32,768 increments + modLfoToVolume: 13, // modulation lfo - volume (tremolo), where 100 = 10dB + unused1: 14, // unused + chorusEffectsSend: 15, // effect send - how much is sent to chorus 0 - 1000 + reverbEffectsSend: 16, // effect send - how much is sent to reverb 0 - 1000 + pan: 17, // panning - where -500 = left, 0 = center, 500 = right + unused2: 18, // unused + unused3: 19, // unused + unused4: 20, // unused + delayModLFO: 21, // mod lfo - delay for mod lfo to start from zero + freqModLFO: 22, // mod lfo - frequency of mod lfo, 0 = 8.176 Hz, units: f => 1200log2(f/8.176) + delayVibLFO: 23, // vib lfo - delay for vibrato lfo to start from zero + freqVibLFO: 24, // vib lfo - frequency of vibrato lfo, 0 = 8.176Hz, unit: f => 1200log2(f/8.176) + delayModEnv: 25, // mod env - 0 = 1 s decay till mod env starts + attackModEnv: 26, // mod env - attack of mod env + holdModEnv: 27, // mod env - hold of mod env + decayModEnv: 28, // mod env - decay of mod env + sustainModEnv: 29, // mod env - sustain of mod env + releaseModEnv: 30, // mod env - release of mod env + keyNumToModEnvHold: 31, // mod env - also modulating mod envelope hold with key number + keyNumToModEnvDecay: 32, // mod env - also modulating mod envelope decay with key number + delayVolEnv: 33, // vol env - delay of envelope from zero (weird scale) + attackVolEnv: 34, // vol env - attack of envelope + holdVolEnv: 35, // vol env - hold of envelope + decayVolEnv: 36, // vol env - decay of envelope + sustainVolEnv: 37, // vol env - sustain of envelope + releaseVolEnv: 38, // vol env - release of envelope + keyNumToVolEnvHold: 39, // vol env - key number to volume envelope hold + keyNumToVolEnvDecay: 40, // vol env - key number to volume envelope decay + instrument: 41, // zone - instrument index to use for preset zone + reserved1: 42, // reserved + keyRange: 43, // zone - key range for which preset / instrument zone is active + velRange: 44, // zone - velocity range for which preset / instrument zone is active + startloopAddrsCoarseOffset: 45, // sample control - moves sample loop start point in 32,768 increments + keyNum: 46, // zone - instrument only: always use this midi number (ignore what's pressed) + velocity: 47, // zone - instrument only: always use this velocity (ignore what's pressed) + initialAttenuation: 48, // zone - allows turning down the volume, 10 = -1dB + reserved2: 49, // reserved + endloopAddrsCoarseOffset: 50, // sample control - moves sample loop end point in 32,768 increments + coarseTune: 51, // tune - pitch offset in semitones + fineTune: 52, // tune - pitch offset in cents + sampleID: 53, // sample - instrument zone only: which sample to use + sampleModes: 54, // sample - 0 = no loop, 1 = loop, 2 = reserved, 3 = loop and play till the end in release phase + reserved3: 55, // reserved + scaleTuning: 56, // sample - the degree to which MIDI key number influences pitch, 100 = default + exclusiveClass: 57, // sample - = cut = choke group + overridingRootKey: 58, // sample - can override the sample's original pitch + unused5: 59, // unused + endOper: 60 // end marker +}; +/** + * @type {{min: number, max: number, def: number}[]} + */ +export const generatorLimits = []; +// offsets +generatorLimits[generatorTypes.startAddrsOffset] = { min: 0, max: 32768, def: 0 }; +generatorLimits[generatorTypes.endAddrOffset] = { min: -32768, max: 32768, def: 0 }; +generatorLimits[generatorTypes.startloopAddrsOffset] = { min: -32768, max: 32768, def: 0 }; +generatorLimits[generatorTypes.endloopAddrsOffset] = { min: -32768, max: 32768, def: 0 }; +generatorLimits[generatorTypes.startAddrsCoarseOffset] = { min: 0, max: 32768, def: 0 }; + +// pitch influence +generatorLimits[generatorTypes.modLfoToPitch] = { min: -12000, max: 12000, def: 0 }; +generatorLimits[generatorTypes.vibLfoToPitch] = { min: -12000, max: 12000, def: 0 }; +generatorLimits[generatorTypes.modEnvToPitch] = { min: -12000, max: 12000, def: 0 }; + +// lowpass +generatorLimits[generatorTypes.initialFilterFc] = { min: 1500, max: 13500, def: 13500 }; +generatorLimits[generatorTypes.initialFilterQ] = { min: 0, max: 960, def: 0 }; +generatorLimits[generatorTypes.modLfoToFilterFc] = { min: -12000, max: 12000, def: 0 }; +generatorLimits[generatorTypes.modEnvToFilterFc] = { min: -12000, max: 12000, def: 0 }; + +generatorLimits[generatorTypes.endAddrsCoarseOffset] = { min: -32768, max: 32768, def: 0 }; + +generatorLimits[generatorTypes.modLfoToVolume] = { min: -960, max: 960, def: 0 }; + +// effects, pan +generatorLimits[generatorTypes.chorusEffectsSend] = { min: 0, max: 1000, def: 0 }; +generatorLimits[generatorTypes.reverbEffectsSend] = { min: 0, max: 1000, def: 0 }; +generatorLimits[generatorTypes.pan] = { min: -500, max: 500, def: 0 }; + +// lfo +generatorLimits[generatorTypes.delayModLFO] = { min: -12000, max: 5000, def: -12000 }; +generatorLimits[generatorTypes.freqModLFO] = { min: -16000, max: 4500, def: 0 }; +generatorLimits[generatorTypes.delayVibLFO] = { min: -12000, max: 5000, def: -12000 }; +generatorLimits[generatorTypes.freqVibLFO] = { min: -16000, max: 4500, def: 0 }; + +// mod env +generatorLimits[generatorTypes.delayModEnv] = { min: -32768, max: 5000, def: -32768 }; // -32,768 indicates instant phase, +// this is done to prevent click at the start of filter modenv +generatorLimits[generatorTypes.attackModEnv] = { min: -32768, max: 8000, def: -32768 }; +generatorLimits[generatorTypes.holdModEnv] = { min: -12000, max: 5000, def: -12000 }; +generatorLimits[generatorTypes.decayModEnv] = { min: -12000, max: 8000, def: -12000 }; +generatorLimits[generatorTypes.sustainModEnv] = { min: 0, max: 1000, def: 0 }; +generatorLimits[generatorTypes.releaseModEnv] = { min: -7200, max: 8000, def: -12000 }; // min is set to -7200 to prevent lowpass clicks +// key num to mod env +generatorLimits[generatorTypes.keyNumToModEnvHold] = { min: -1200, max: 1200, def: 0 }; +generatorLimits[generatorTypes.keyNumToModEnvDecay] = { min: -1200, max: 1200, def: 0 }; + +// vol env +generatorLimits[generatorTypes.delayVolEnv] = { min: -12000, max: 5000, def: -12000 }; +generatorLimits[generatorTypes.attackVolEnv] = { min: -12000, max: 8000, def: -12000 }; +generatorLimits[generatorTypes.holdVolEnv] = { min: -12000, max: 5000, def: -12000 }; +generatorLimits[generatorTypes.decayVolEnv] = { min: -12000, max: 8000, def: -12000 }; +generatorLimits[generatorTypes.sustainVolEnv] = { min: 0, max: 1440, def: 0 }; +generatorLimits[generatorTypes.releaseVolEnv] = { min: -7200, max: 8000, def: -12000 }; // min is set to -7200 prevent clicks +// key num to vol env +generatorLimits[generatorTypes.keyNumToVolEnvHold] = { min: -1200, max: 1200, def: 0 }; +generatorLimits[generatorTypes.keyNumToVolEnvDecay] = { min: -1200, max: 1200, def: 0 }; + +generatorLimits[generatorTypes.startloopAddrsCoarseOffset] = { min: -32768, max: 32768, def: 0 }; +generatorLimits[generatorTypes.keyNum] = { min: -1, max: 127, def: -1 }; +generatorLimits[generatorTypes.velocity] = { min: -1, max: 127, def: -1 }; + +generatorLimits[generatorTypes.initialAttenuation] = { min: 0, max: 1440, def: 0 }; + +generatorLimits[generatorTypes.endloopAddrsCoarseOffset] = { min: -32768, max: 32768, def: 0 }; + +generatorLimits[generatorTypes.coarseTune] = { min: -120, max: 120, def: 0 }; +generatorLimits[generatorTypes.fineTune] = { min: -12700, max: 12700, def: 0 }; // this generator is used as initial pitch, hence this range +generatorLimits[generatorTypes.scaleTuning] = { min: 0, max: 1200, def: 100 }; +generatorLimits[generatorTypes.exclusiveClass] = { min: 0, max: 99999, def: 0 }; +generatorLimits[generatorTypes.overridingRootKey] = { min: 0 - 1, max: 127, def: -1 }; +generatorLimits[generatorTypes.sampleModes] = { min: 0, max: 3, def: 0 }; + +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} + * @param value {number} + * @param validate {boolean} + */ + constructor(type = generatorTypes.INVALID, value = 0, validate = true) + { + this.generatorType = type; + if (value === undefined) + { + throw new Error("No value provided."); + } + this.generatorValue = Math.round(value); + if (validate) + { + const lim = generatorLimits[type]; + + if (lim !== undefined) + { + this.generatorValue = Math.max(lim.min, Math.min(lim.max, this.generatorValue)); + } + } + } +} + +/** + * generator.js + * purpose: contains enums for generators, + * and their limis parses reads soundfont generators, sums them and applies limits + */ +/** + * @param generatorType {number} + * @param presetGens {Generator[]} + * @param instrumentGens {Generator[]} + */ +export function addAndClampGenerator(generatorType, presetGens, instrumentGens) +{ + const limits = generatorLimits[generatorType] || { min: 0, max: 32768, def: 0 }; + let presetGen = presetGens.find(g => g.generatorType === generatorType); + let presetValue = 0; + if (presetGen) + { + 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, initial attenuation. + // Shall get clamped in the volume envelope, + // so the modulators can be affected by negative generators (the "Brass" patch was problematic...) + if (generatorType === generatorTypes.initialAttenuation) + { + return value; + } + + return Math.max(limits.min, Math.min(limits.max, value)); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/modulator.js b/spessasynth_lib/soundfont/basic_soundfont/modulator.js new file mode 100644 index 0000000000000000000000000000000000000000..10875f56826b288beddd5a7eb80de82954991d9a --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/modulator.js @@ -0,0 +1,378 @@ +import { generatorTypes } from "./generator.js"; +import { midiControllers } from "../../midi_parser/midi_message.js"; + +/** + * modulators.js + * purpose: parses soundfont modulators and the source enums, also includes the default modulators list + **/ + +export const modulatorSources = { + noController: 0, + noteOnVelocity: 2, + noteOnKeyNum: 3, + polyPressure: 10, + channelPressure: 13, + pitchWheel: 14, + pitchWheelRange: 16, + link: 127 + +}; +export const modulatorCurveTypes = { + linear: 0, + concave: 1, + convex: 2, + switch: 3 +}; + +export class Modulator +{ + /** + * The current computed value of this modulator + * @type {number} + */ + currentValue = 0; + + /** + * The source enumeration for this modulator + * @type {number} + */ + sourceEnum; + + /** + * The secondary source enumeration for this modulator + * @type {number} + */ + secondarySourceEnum; + + /** + * The generator destination of this modulator + * @type {generatorTypes} + */ + modulatorDestination; + + /** + * The transform amount for this modulator + * @type {number} + */ + transformAmount; + + /** + * The transform type for this modulator + * @type {0|2} + */ + transformType; + + /** + * creates a modulator + * @param srcEnum {number} + * @param secSrcEnum {number} + * @param destination {generatorTypes|number} + * @param amount {number} + * @param transformType {number} + */ + constructor(srcEnum, secSrcEnum, destination, amount, transformType) + { + this.sourceEnum = srcEnum; + this.modulatorDestination = destination; + this.secondarySourceEnum = secSrcEnum; + this.transformAmount = amount; + this.transformType = transformType; + + + 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; + + /** + * Indicates if the given modulator is chorus or reverb effects modulator. + * This is done to simulate BASSMIDI effects behavior: + * - defaults to 1000 transform amount rather than 200 + * - values can be changed, but anything above 200 is 1000 + * (except for values above 1000, they are copied directly) + * - all values below are multiplied by 5 (200 * 5 = 1000) + * - still can be disabled if the soundfont has its own modulator curve + * - this fixes the very low amount of reverb by default and doesn't break soundfonts + * @type {boolean} + */ + this.isEffectModulator = + ( + this.sourceEnum === 0x00DB + || this.sourceEnum === 0x00DD + ) + && this.secondarySourceEnum === 0x0 + && ( + this.modulatorDestination === generatorTypes.reverbEffectsSend + || this.modulatorDestination === generatorTypes.chorusEffectsSend + ); + } + + /** + * @param modulator {Modulator} + * @returns {Modulator} + */ + static copy(modulator) + { + return new Modulator( + modulator.sourceEnum, + modulator.secondarySourceEnum, + modulator.modulatorDestination, + modulator.transformAmount, + modulator.transformType + ); + } + + /** + * @param mod1 {Modulator} + * @param mod2 {Modulator} + * @param checkAmount {boolean} + * @returns {boolean} + */ + static isIdentical(mod1, mod2, checkAmount = false) + { + return (mod1.sourceEnum === mod2.sourceEnum) + && (mod1.modulatorDestination === mod2.modulatorDestination) + && (mod1.secondarySourceEnum === mod2.secondarySourceEnum) + && (mod1.transformType === mod2.transformType) + && (!checkAmount || (mod1.transformAmount === mod2.transformAmount)); + } + + /** + * @param mod {Modulator} + * @returns {string} + */ + static debugString(mod) + { + function getKeyByValue(object, value) + { + return Object.keys(object).find(key => object[key] === value); + } + + let sourceString = getKeyByValue(modulatorCurveTypes, mod.sourceCurveType); + sourceString += mod.sourcePolarity === 0 ? " unipolar " : " bipolar "; + sourceString += mod.sourceDirection === 0 ? "forwards " : "backwards "; + if (mod.sourceUsesCC) + { + sourceString += getKeyByValue(midiControllers, mod.sourceIndex); + } + else + { + sourceString += getKeyByValue(modulatorSources, mod.sourceIndex); + } + + let secSrcString = getKeyByValue(modulatorCurveTypes, mod.secSrcCurveType); + secSrcString += mod.secSrcPolarity === 0 ? " unipolar " : " bipolar "; + secSrcString += mod.secSrcCurveType === 0 ? "forwards " : "backwards "; + if (mod.secSrcUsesCC) + { + secSrcString += getKeyByValue(midiControllers, mod.secSrcIndex); + } + else + { + secSrcString += getKeyByValue(modulatorSources, mod.secSrcIndex); + } + return `Modulator: + Source: ${sourceString} + Secondary source: ${secSrcString} + Destination: ${getKeyByValue(generatorTypes, mod.modulatorDestination)} + Trasform amount: ${mod.transformAmount} + Transform type: ${mod.transformType} + \n\n`; + } + + /** + * Sum transform and create a NEW modulator + * @param modulator {Modulator} + * @returns {Modulator} + */ + sumTransform(modulator) + { + return new Modulator( + this.sourceEnum, + this.secondarySourceEnum, + this.modulatorDestination, + this.transformAmount + modulator.transformAmount, + this.transformType + ); + } +} + +export const DEFAULT_ATTENUATION_MOD_AMOUNT = 960; +export const DEFAULT_ATTENUATION_MOD_CURVE_TYPE = modulatorCurveTypes.concave; + +export function getModSourceEnum(curveType, polarity, direction, isCC, index) +{ + return (curveType << 10) | (polarity << 9) | (direction << 8) | (isCC << 7) | index; +} + +const soundFontModulators = [ + // vel to attenuation + new Modulator( + getModSourceEnum( + DEFAULT_ATTENUATION_MOD_CURVE_TYPE, + 0, + 1, + 0, + modulatorSources.noteOnVelocity + ), + 0x0, + generatorTypes.initialAttenuation, + DEFAULT_ATTENUATION_MOD_AMOUNT, + 0 + ), + + // mod wheel to vibrato + new Modulator(0x0081, 0x0, generatorTypes.vibLfoToPitch, 50, 0), + + // vol to attenuation + new Modulator( + getModSourceEnum( + DEFAULT_ATTENUATION_MOD_CURVE_TYPE, + 0, + 1, + 1, + midiControllers.mainVolume + ), + 0x0, + generatorTypes.initialAttenuation, + DEFAULT_ATTENUATION_MOD_AMOUNT, + 0 + ), + + // channel pressure to vibrato + new Modulator(0x000D, 0x0, generatorTypes.vibLfoToPitch, 50, 0), + + // pitch wheel to tuning + new Modulator(0x020E, 0x0010, generatorTypes.fineTune, 12700, 0), + + // pan to uhh, pan + // amount is 500 instead of 1000, see #59 + new Modulator(0x028A, 0x0, generatorTypes.pan, 500, 0), + + // expression to attenuation + new Modulator( + getModSourceEnum( + DEFAULT_ATTENUATION_MOD_CURVE_TYPE, + 0, + 1, + 1, + midiControllers.expressionController + ), + 0x0, + generatorTypes.initialAttenuation, + DEFAULT_ATTENUATION_MOD_AMOUNT, + 0 + ), + + // reverb effects to send + new Modulator(0x00DB, 0x0, generatorTypes.reverbEffectsSend, 200, 0), + + // chorus effects to send + new Modulator(0x00DD, 0x0, generatorTypes.chorusEffectsSend, 200, 0) +]; + +const customModulators = [ + // custom modulators heck yeah + // poly pressure to vibrato + new Modulator( + getModSourceEnum(modulatorCurveTypes.linear, 0, 0, 0, modulatorSources.polyPressure), + 0x0, + generatorTypes.vibLfoToPitch, + 50, + 0 + ), + + // cc 92 (tremolo) to modLFO volume + new Modulator( + getModSourceEnum( + modulatorCurveTypes.linear, + 0, + 0, + 1, + midiControllers.tremoloDepth + ), /*linear forward unipolar cc 92 */ + 0x0, // no controller + generatorTypes.modLfoToVolume, + 24, + 0 + ), + + // cc 73 (attack time) to volEnv attack + new Modulator( + getModSourceEnum( + modulatorCurveTypes.convex, + 1, + 0, + 1, + midiControllers.attackTime + ), // linear forward bipolar cc 72 + 0x0, // no controller + generatorTypes.attackVolEnv, + 6000, + 0 + ), + + // cc 72 (release time) to volEnv release + new Modulator( + getModSourceEnum( + modulatorCurveTypes.linear, + 1, + 0, + 1, + midiControllers.releaseTime + ), // linear forward bipolar cc 72 + 0x0, // no controller + generatorTypes.releaseVolEnv, + 3600, + 0 + ), + + // cc 74 (brightness) to filterFc + new Modulator( + getModSourceEnum( + modulatorCurveTypes.linear, + 1, + 0, + 1, + midiControllers.brightness + ), // linear forwards bipolar cc 74 + 0x0, // no controller + generatorTypes.initialFilterFc, + 6000, + 0 + ), + + // cc 71 (filter Q) to filter Q + new Modulator( + getModSourceEnum( + modulatorCurveTypes.linear, + 1, + 0, + 1, + midiControllers.filterResonance + ), // linear forwards bipolar cc 74 + 0x0, // no controller + generatorTypes.initialFilterQ, + 250, + 0 + ) +]; + +/** + * @type {Modulator[]} + */ +export const defaultModulators = soundFontModulators.concat(customModulators); \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/riff_chunk.js b/spessasynth_lib/soundfont/basic_soundfont/riff_chunk.js new file mode 100644 index 0000000000000000000000000000000000000000..7a8b0d919e85636acaa36d8e9ce388de873c9a26 --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/riff_chunk.js @@ -0,0 +1,149 @@ +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 + * reads a riff read and stores it as a class + */ + +export class RiffChunk +{ + /** + * Creates a new riff read + * @constructor + * @param header {string} + * @param size {number} + * @param data {IndexedByteArray} + */ + constructor(header, size, data) + { + this.header = header; + this.size = size; + this.chunkData = data; + } + +} + +/** + * @param dataArray {IndexedByteArray} + * @param readData {boolean} + * @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; + if (readData) + { + chunkData = new IndexedByteArray(dataArray.buffer.slice(dataArray.currentIndex, dataArray.currentIndex + size)); + } + if (readData || forceShift) + { + dataArray.currentIndex += size; + } + + if (size % 2 !== 0) + { + if (dataArray[dataArray.currentIndex] === 0) + { + dataArray.currentIndex++; + } + } + + return new RiffChunk(header, size, chunkData); +} + +/** + * @param chunk {RiffChunk} + * @param prepend {IndexedByteArray} + * @returns {IndexedByteArray} + */ +export function writeRIFFChunk(chunk, prepend = undefined) +{ + let size = 8 + chunk.size; + if (chunk.size % 2 !== 0) + { + size++; + } + if (prepend) + { + size += prepend.length; + } + const array = new IndexedByteArray(size); + // prepend data (for example, type before the read) + if (prepend) + { + array.set(prepend, array.currentIndex); + array.currentIndex += prepend.length; + } + // write header + writeStringAsBytes(array, chunk.header); + // write size (excluding header and the size itself) and then prepend if specified + writeDword(array, size - 8 - (prepend?.length || 0)); + // write data + array.set(chunk.chunkData, array.currentIndex); + return array; +} + +/** + * @param header {string} + * @param data {Uint8Array} + * @param addZeroByte {Boolean} + * @param isList {boolean} + * @returns {IndexedByteArray} + */ +export function writeRIFFOddSize(header, data, addZeroByte = false, isList = false) +{ + if (addZeroByte) + { + const tempData = new Uint8Array(data.length + 1); + tempData.set(data); + data = tempData; + } + let offset = 8; + let finalSize = offset + data.length; + let writtenSize = data.length; + if (finalSize % 2 !== 0) + { + finalSize++; + } + let headerWritten = header; + if (isList) + { + finalSize += 4; + writtenSize += 4; + offset += 4; + headerWritten = "LIST"; + } + const outArray = new IndexedByteArray(finalSize); + writeStringAsBytes(outArray, headerWritten); + writeDword(outArray, writtenSize); + if (isList) + { + writeStringAsBytes(outArray, header); + } + outArray.set(data, offset); + return outArray; +} + +/** + * @param collection {RiffChunk[]} + * @param type {string} + * @returns {RiffChunk|undefined} + */ +export function findRIFFListType(collection, type) +{ + return collection.find(c => + { + if (c.header !== "LIST") + { + return false; + } + c.chunkData.currentIndex = 0; + return readBytesAsString(c.chunkData, 4) === type; + }); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/write_dls/art2.js b/spessasynth_lib/soundfont/basic_soundfont/write_dls/art2.js new file mode 100644 index 0000000000000000000000000000000000000000..6b44827217697a55656e4c836f580fe3a6ba41fc --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/write_dls/art2.js @@ -0,0 +1,173 @@ +import { getDLSArticulatorFromSf2Generator, getDLSArticulatorFromSf2Modulator } from "./modulator_converter.js"; +import { writeRIFFOddSize } from "../riff_chunk.js"; +import { combineArrays, IndexedByteArray } from "../../../utils/indexed_array.js"; +import { Generator, generatorTypes } from "../generator.js"; +import { writeDword } from "../../../utils/byte_functions/little_endian.js"; +import { consoleColors } from "../../../utils/other.js"; +import { SpessaSynthInfo, SpessaSynthWarn } from "../../../utils/loggin.js"; +import { Modulator } from "../modulator.js"; +import { + DEFAULT_DLS_CHORUS, + DEFAULT_DLS_REVERB, + DLS_1_NO_VIBRATO_MOD, + DLS_1_NO_VIBRATO_PRESSURE +} from "../../dls/dls_sources.js"; + +const invalidGeneratorTypes = new Set([ + generatorTypes.sampleModes, + generatorTypes.initialAttenuation, + generatorTypes.keyRange, + generatorTypes.velRange, + generatorTypes.sampleID, + generatorTypes.fineTune, + generatorTypes.coarseTune, + generatorTypes.startAddrsOffset, + generatorTypes.startAddrsCoarseOffset, + generatorTypes.endAddrOffset, + generatorTypes.endAddrsCoarseOffset, + generatorTypes.startloopAddrsOffset, + generatorTypes.startloopAddrsCoarseOffset, + generatorTypes.endloopAddrsOffset, + generatorTypes.endloopAddrsCoarseOffset, + generatorTypes.overridingRootKey, + generatorTypes.exclusiveClass +]); + +/** + * @param zone {BasicInstrumentZone} + * @returns {IndexedByteArray} + */ +export function writeArticulator(zone) +{ + + + // envelope generators are limited to 40 seconds + // in timecents, this is 1200 * log2(10) = 6386 + + for (let i = 0; i < zone.generators.length; i++) + { + const g = zone.generators[i]; + if ( + g.generatorType === generatorTypes.delayVolEnv || + g.generatorType === generatorTypes.attackVolEnv || + g.generatorType === generatorTypes.holdVolEnv || + g.generatorType === generatorTypes.decayVolEnv || + g.generatorType === generatorTypes.releaseVolEnv || + g.generatorType === generatorTypes.delayModEnv || + g.generatorType === generatorTypes.attackModEnv || + g.generatorType === generatorTypes.holdModEnv || + g.generatorType === generatorTypes.decayModEnv + ) + { + zone.generators[i] = new Generator(g.generatorType, Math.min(g.generatorValue, 6386), false); + } + } + + + // read_articulation.js: + // according to viena and another strange (with modulators) rendition of gm.dls in sf2, + // it shall be divided by -128, + // and a strange correction needs to be applied to the real value: + // real + (60 / 128) * scale + // we invert this here + for (let i = 0; i < zone.generators.length; i++) + { + const relativeGenerator = zone.generators[i]; + let absoluteCounterpart = undefined; + switch (relativeGenerator.generatorType) + { + default: + continue; + + case generatorTypes.keyNumToVolEnvDecay: + absoluteCounterpart = generatorTypes.decayVolEnv; + break; + case generatorTypes.keyNumToVolEnvHold: + absoluteCounterpart = generatorTypes.holdVolEnv; + break; + case generatorTypes.keyNumToModEnvDecay: + absoluteCounterpart = generatorTypes.decayModEnv; + break; + case generatorTypes.keyNumToModEnvHold: + absoluteCounterpart = generatorTypes.holdModEnv; + } + let absoluteGenerator = zone.generators.find(g => g.generatorType === absoluteCounterpart); + if (absoluteGenerator === undefined) + { + // there's no absolute generator here. + continue; + } + const dlsRelative = relativeGenerator.generatorValue * -128; + const subtraction = (60 / 128) * dlsRelative; + const newAbsolute = absoluteGenerator.generatorValue - subtraction; + + const iR = zone.generators.indexOf(relativeGenerator); + const iA = zone.generators.indexOf(absoluteGenerator); + zone.generators[iA] = + new Generator(absoluteCounterpart, newAbsolute, false); + zone.generators[iR] = + new Generator(relativeGenerator.generatorType, dlsRelative, false); + } + /** + * @type {Articulator[]} + */ + const generators = zone.generators.reduce((arrs, g) => + { + if (invalidGeneratorTypes.has(g.generatorType)) + { + return arrs; + } + const art = getDLSArticulatorFromSf2Generator(g); + if (art !== undefined) + { + arrs.push(art); + SpessaSynthInfo("%cSucceeded converting to DLS Articulator!", consoleColors.recognized); + + } + else + { + SpessaSynthWarn("Failed converting to DLS Articulator!"); + } + return arrs; + }, []); + /** + * @type {Articulator[]} + */ + const modulators = zone.modulators.reduce((arrs, m) => + { + // do not write the default DLS modulators + if ( + Modulator.isIdentical(m, DEFAULT_DLS_CHORUS, true) || + Modulator.isIdentical(m, DEFAULT_DLS_REVERB, true) || + Modulator.isIdentical(m, DLS_1_NO_VIBRATO_MOD, true) || + Modulator.isIdentical(m, DLS_1_NO_VIBRATO_PRESSURE, true) + ) + { + return arrs; + } + const art = getDLSArticulatorFromSf2Modulator(m); + if (art !== undefined) + { + arrs.push(art); + SpessaSynthInfo("%cSucceeded converting to DLS Articulator!", consoleColors.recognized); + + } + else + { + SpessaSynthWarn("Failed converting to DLS Articulator!"); + } + return arrs; + }, []); + generators.push(...modulators); + + const art2Data = new IndexedByteArray(8); + writeDword(art2Data, 8); // cbSize + writeDword(art2Data, generators.length); // cbConnectionBlocks + + + const out = generators.map(a => a.writeArticulator()); + return writeRIFFOddSize( + "art2", + combineArrays([art2Data, ...out]) + ); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/write_dls/articulator.js b/spessasynth_lib/soundfont/basic_soundfont/write_dls/articulator.js new file mode 100644 index 0000000000000000000000000000000000000000..9abb0b0d1b745e901673fee671d590d83f5ac253 --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/write_dls/articulator.js @@ -0,0 +1,49 @@ +import { IndexedByteArray } from "../../../utils/indexed_array.js"; +import { writeDword, writeWord } from "../../../utils/byte_functions/little_endian.js"; + +export class Articulator +{ + /** + * @type {DLSSources} + */ + source; + /** + * @type {DLSSources} + */ + control; + /** + * @type {DLSDestinations} + */ + destination; + /** + * @type {number} + */ + scale; + /** + * @type {number} + */ + transform; + + constructor(source, control, destination, scale, transform) + { + this.source = source; + this.control = control; + this.destination = destination; + this.scale = scale; + this.transform = transform; + } + + /** + * @returns {IndexedByteArray} + */ + writeArticulator() + { + const out = new IndexedByteArray(12); + writeWord(out, this.source); + writeWord(out, this.control); + writeWord(out, this.destination); + writeWord(out, this.transform); + writeDword(out, this.scale << 16); + return out; + } +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/write_dls/combine_zones.js b/spessasynth_lib/soundfont/basic_soundfont/write_dls/combine_zones.js new file mode 100644 index 0000000000000000000000000000000000000000..7352d277fcd8b3f7c0cc575353faf3c790eb3fbe --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/write_dls/combine_zones.js @@ -0,0 +1,400 @@ +import { Modulator } from "../modulator.js"; +import { BasicInstrumentZone } from "../basic_zones.js"; +import { Generator, generatorLimits, generatorTypes } from "../generator.js"; + +const notGlobalizedTypes = new Set([ + generatorTypes.velRange, + generatorTypes.keyRange, + generatorTypes.instrument, + generatorTypes.exclusiveClass, + generatorTypes.endOper, + generatorTypes.sampleModes, + generatorTypes.startloopAddrsOffset, + generatorTypes.startloopAddrsCoarseOffset, + generatorTypes.endloopAddrsOffset, + generatorTypes.endloopAddrsCoarseOffset, + generatorTypes.startAddrsOffset, + generatorTypes.startAddrsCoarseOffset, + generatorTypes.endAddrOffset, + generatorTypes.endAddrsCoarseOffset, + generatorTypes.initialAttenuation, // written into wsmp, there's no global wsmp + generatorTypes.fineTune, // written into wsmp, there's no global wsmp + generatorTypes.coarseTune, // written into wsmp, there's no global wsmp + generatorTypes.keyNumToVolEnvHold, // KEY TO SOMETHING: + generatorTypes.keyNumToVolEnvDecay,// cannot be globalized as they modify their respective generators + generatorTypes.keyNumToModEnvHold, // (for example, keyNumToVolEnvDecay modifies VolEnvDecay) + generatorTypes.keyNumToModEnvDecay +]); + +/** + * Combines preset zones + * @param preset {BasicPreset} + * @param globalize {boolean} + * @returns {BasicInstrumentZone[]} + */ +export function combineZones(preset, globalize = true) +{ + /** + * @param main {Generator[]} + * @param adder {Generator[]} + */ + function addUnique(main, adder) + { + main.push(...adder.filter(g => !main.find(mg => mg.generatorType === g.generatorType))); + } + + /** + * @param r1 {SoundFontRange} + * @param r2 {SoundFontRange} + * @returns {SoundFontRange} + */ + function subtractRanges(r1, r2) + { + return { min: Math.max(r1.min, r2.min), max: Math.min(r1.max, r2.max) }; + } + + /** + * @param main {Modulator[]} + * @param adder {Modulator[]} + */ + function addUniqueMods(main, adder) + { + main.push(...adder.filter(m => !main.find(mm => Modulator.isIdentical(m, mm)))); + } + + /** + * @type {BasicInstrumentZone[]} + */ + const finalZones = []; + + /** + * @type {Generator[]} + */ + const globalPresetGenerators = []; + /** + * @type {Modulator[]} + */ + const globalPresetModulators = []; + let globalPresetKeyRange = { min: 0, max: 127 }; + let globalPresetVelRange = { min: 0, max: 127 }; + + // find the global zone and apply ranges, generators, and modulators + const globalPresetZone = preset.presetZones.find(z => z.isGlobal); + if (globalPresetZone) + { + globalPresetGenerators.push(...globalPresetZone.generators); + globalPresetModulators.push(...globalPresetZone.modulators); + globalPresetKeyRange = globalPresetZone.keyRange; + globalPresetVelRange = globalPresetZone.velRange; + } + // for each non-global preset zone + for (const presetZone of preset.presetZones) + { + if (presetZone.isGlobal) + { + continue; + } + // use global ranges if not provided + let presetZoneKeyRange = presetZone.keyRange; + if (!presetZone.hasKeyRange) + { + presetZoneKeyRange = globalPresetKeyRange; + } + let presetZoneVelRange = presetZone.velRange; + if (!presetZone.hasVelRange) + { + presetZoneVelRange = globalPresetVelRange; + } + // add unique generators and modulators from the global zone + const presetGenerators = presetZone.generators.map(g => new Generator(g.generatorType, g.generatorValue)); + addUnique(presetGenerators, globalPresetGenerators); + const presetModulators = [...presetZone.modulators]; + addUniqueMods(presetModulators, globalPresetModulators); + + const iZones = presetZone.instrument.instrumentZones; + /** + * @type {Generator[]} + */ + const globalInstGenerators = []; + /** + * @type {Modulator[]} + */ + const globalInstModulators = []; + let globalInstKeyRange = { min: 0, max: 127 }; + let globalInstVelRange = { min: 0, max: 127 }; + const globalInstZone = iZones.find(z => z.isGlobal); + if (globalInstZone) + { + globalInstGenerators.push(...globalInstZone.generators); + globalInstModulators.push(...globalInstZone.modulators); + globalInstKeyRange = globalInstZone.keyRange; + globalInstVelRange = globalInstZone.velRange; + } + // for each non-global instrument zone + for (const instZone of iZones) + { + if (instZone.isGlobal) + { + continue; + } + // use global ranges if not provided + let instZoneKeyRange = instZone.keyRange; + if (!instZone.hasKeyRange) + { + instZoneKeyRange = globalInstKeyRange; + } + let instZoneVelRange = instZone.velRange; + if (!instZone.hasVelRange) + { + instZoneVelRange = globalInstVelRange; + } + instZoneKeyRange = subtractRanges(instZoneKeyRange, presetZoneKeyRange); + instZoneVelRange = subtractRanges(instZoneVelRange, presetZoneVelRange); + + // if either of the zones is out of range (i.e.m min larger than the max), + // then we discard that zone + if (instZoneKeyRange.max < instZoneKeyRange.min || instZoneVelRange.max < instZoneVelRange.min) + { + continue; + } + + // add unique generators and modulators from the global zone + const instGenerators = instZone.generators.map(g => new Generator(g.generatorType, g.generatorValue)); + addUnique(instGenerators, globalInstGenerators); + const instModulators = [...instZone.modulators]; + addUniqueMods(instModulators, globalInstModulators); + + /** + * sum preset modulators to instruments (amount) sf spec page 54 + * @type {Modulator[]} + */ + const finalModList = [...instModulators]; + for (const mod of presetModulators) + { + const identicalInstMod = finalModList.findIndex( + m => Modulator.isIdentical(mod, m)); + if (identicalInstMod !== -1) + { + // sum the amounts + // (this makes a new modulator + // because otherwise it would overwrite the one in the soundfont! + finalModList[identicalInstMod] = finalModList[identicalInstMod].sumTransform( + mod); + } + else + { + finalModList.push(mod); + } + } + + // clone the generators as the values are modified during DLS conversion (keyNumToSomething) + let finalGenList = instGenerators.map(g => new Generator(g.generatorType, g.generatorValue)); + for (const gen of presetGenerators) + { + if (gen.generatorType === generatorTypes.velRange || + gen.generatorType === generatorTypes.keyRange || + gen.generatorType === generatorTypes.instrument || + gen.generatorType === generatorTypes.endOper || + gen.generatorType === generatorTypes.sampleModes) + { + continue; + } + const identicalInstGen = instGenerators.findIndex(g => g.generatorType === gen.generatorType); + if (identicalInstGen !== -1) + { + // if exists, sum to that generator + const newAmount = finalGenList[identicalInstGen].generatorValue + gen.generatorValue; + finalGenList[identicalInstGen] = new Generator(gen.generatorType, newAmount); + } + else + { + // if not, sum to the default generator + const newAmount = generatorLimits[gen.generatorType].def + gen.generatorValue; + finalGenList.push(new Generator(gen.generatorType, newAmount)); + } + } + + // remove unwanted + finalGenList = finalGenList.filter(g => + g.generatorType !== generatorTypes.sampleID && + g.generatorType !== generatorTypes.keyRange && + g.generatorType !== generatorTypes.velRange && + g.generatorType !== generatorTypes.endOper && + g.generatorType !== generatorTypes.instrument && + g.generatorValue !== generatorLimits[g.generatorType].def + ); + + // create the zone and copy over values + const zone = new BasicInstrumentZone(); + zone.keyRange = instZoneKeyRange; + zone.velRange = instZoneVelRange; + if (zone.keyRange.min === 0 && zone.keyRange.max === 127) + { + zone.keyRange.min = -1; + } + if (zone.velRange.min === 0 && zone.velRange.max === 127) + { + zone.velRange.min = -1; + } + zone.isGlobal = false; + zone.sample = instZone.sample; + zone.generators = finalGenList; + zone.modulators = finalModList; + finalZones.push(zone); + } + } + + if (globalize) + { + // create a global zone and add repeating generators to it + // also modulators + const globalZone = new BasicInstrumentZone(); + globalZone.isGlobal = true; + // iterate over every type of generator + for (let checkedType = 0; checkedType < 58; checkedType++) + { + // not these though + if (notGlobalizedTypes.has(checkedType)) + { + continue; + } + /** + * @type {Object} + */ + let occurencesForValues = {}; + const defaultForChecked = generatorLimits[checkedType]?.def || 0; + occurencesForValues[defaultForChecked] = 0; + for (const z of finalZones) + { + const gen = z.generators.find(g => g.generatorType === checkedType); + if (gen) + { + const value = gen.generatorValue; + if (occurencesForValues[value] === undefined) + { + occurencesForValues[value] = 1; + } + else + { + occurencesForValues[value]++; + } + } + else + { + occurencesForValues[defaultForChecked]++; + } + + // if the checked type has the keyNumTo something generator set, it cannot be globalized. + let relativeCounterpart; + switch (checkedType) + { + default: + continue; + + case generatorTypes.decayVolEnv: + relativeCounterpart = generatorTypes.keyNumToVolEnvDecay; + break; + case generatorTypes.holdVolEnv: + relativeCounterpart = generatorTypes.keyNumToVolEnvHold; + break; + case generatorTypes.decayModEnv: + relativeCounterpart = generatorTypes.keyNumToModEnvDecay; + break; + case generatorTypes.holdModEnv: + relativeCounterpart = generatorTypes.keyNumToModEnvHold; + } + const relative = z.generators.find(g => g.generatorType === relativeCounterpart); + if (relative !== undefined) + { + occurencesForValues = {}; + break; + } + } + // if at least one occurrence, find the most used one and add it to global + if (Object.keys(occurencesForValues).length > 0) + { + // [value, occurrences] + const valueToGlobalize = Object.entries(occurencesForValues).reduce((max, curr) => + { + if (max[1] < curr[1]) + { + return curr; + } + return max; + }, [0, 0]); + const targetValue = parseInt(valueToGlobalize[0]); + + // if the global value is the default value just remove it, no need to add it + if (targetValue !== defaultForChecked) + { + globalZone.generators.push(new Generator(checkedType, targetValue)); + } + // remove from the zones + finalZones.forEach(z => + { + const gen = z.generators.findIndex(g => + g.generatorType === checkedType); + if (gen !== -1) + { + if (z.generators[gen].generatorValue === targetValue) + { + // That exact value exists. Since it's global now, remove it + z.generators.splice(gen, 1); + } + } + else + { + // That type does not exist at all here. + // Since we're globalizing, we need to add the default here. + if (targetValue !== defaultForChecked) + { + z.generators.push(new Generator(checkedType, defaultForChecked)); + } + } + }); + } + } + + // globalize only modulators that exist in all zones + const firstZone = finalZones.find(z => !z.isGlobal); + const modulators = firstZone.modulators.map(m => Modulator.copy(m)); + for (const checkedModulator of modulators) + { + let existsForAllZones = true; + for (const zone of finalZones) + { + if (zone.isGlobal || !existsForAllZones) + { + continue; + } + // check if that zone has an existing modulator + const mod = zone.modulators.find(m => Modulator.isIdentical(m, checkedModulator)); + if (!mod) + { + // does not exist for this zone, so it's not global. + existsForAllZones = false; + } + // exists. + + } + if (existsForAllZones === true) + { + globalZone.modulators.push(Modulator.copy(checkedModulator)); + // delete it from local zones. + for (const zone of finalZones) + { + const modulator = zone.modulators.find(m => Modulator.isIdentical(m, checkedModulator)); + // Check if the amount is correct. + // If so, delete it since it's global. + // If not, then it will simply override global as it's identical. + if (modulator.transformAmount === checkedModulator.transformAmount) + { + zone.modulators.splice(zone.modulators.indexOf(modulator), 1); + } + } + } + } + finalZones.splice(0, 0, globalZone); + } + return finalZones; +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/write_dls/ins.js b/spessasynth_lib/soundfont/basic_soundfont/write_dls/ins.js new file mode 100644 index 0000000000000000000000000000000000000000..f51f8b30502f477b6815df4514a6586b71fff53f --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/write_dls/ins.js @@ -0,0 +1,103 @@ +import { combineArrays, IndexedByteArray } from "../../../utils/indexed_array.js"; +import { combineZones } from "./combine_zones.js"; +import { writeRIFFOddSize } from "../riff_chunk.js"; +import { writeDword } from "../../../utils/byte_functions/little_endian.js"; +import { writeDLSRegion } from "./rgn2.js"; +import { getStringBytesZero } from "../../../utils/byte_functions/string.js"; +import { writeArticulator } from "./art2.js"; +import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd } from "../../../utils/loggin.js"; +import { consoleColors } from "../../../utils/other.js"; + +/** + * @this {BasicSoundBank} + * @param preset {BasicPreset} + * @returns {IndexedByteArray} + */ +export function writeIns(preset) +{ + SpessaSynthGroupCollapsed( + `%cWriting %c${preset.presetName}%c...`, + consoleColors.info, + consoleColors.recognized, + consoleColors.info + ); + // combine preset and instrument zones into a single instrument zone (region) list + const combined = combineZones(preset); + + const nonGlobalRegionsCount = combined.reduce((sum, z) => + { + if (!z.isGlobal) + { + return sum + 1; + } + return sum; + }, 0); + + // insh: instrument header + const inshData = new IndexedByteArray(12); + writeDword(inshData, nonGlobalRegionsCount); // cRegions + // bank MSB is in bits 8-14 + let ulBank = (preset.bank & 127) << 8; + // bit 32 means drums + if (preset.bank === 128) + { + ulBank |= (1 << 31); + } + writeDword(inshData, ulBank); // ulBank + writeDword(inshData, preset.program & 127); // ulInstrument + + const insh = writeRIFFOddSize( + "insh", + inshData + ); + + // write global zone + let lar2 = new IndexedByteArray(0); + const globalZone = combined.find(z => z.isGlobal === true); + if (globalZone) + { + const art2 = writeArticulator(globalZone); + lar2 = writeRIFFOddSize( + "lar2", + art2, + false, + true + ); + } + + // write the region list + const lrgnData = combineArrays(combined.reduce((arrs, z) => + { + if (!z.isGlobal) + { + arrs.push(writeDLSRegion.apply(this, [z, globalZone])); + } + return arrs; + }, [])); + const lrgn = writeRIFFOddSize( + "lrgn", + lrgnData, + false, + true + ); + + // writeINFO + const inam = writeRIFFOddSize( + "INAM", + getStringBytesZero(preset.presetName) + ); + const info = writeRIFFOddSize( + "INFO", + inam, + false, + true + ); + + SpessaSynthGroupEnd(); + return writeRIFFOddSize( + "ins ", + combineArrays([insh, lrgn, lar2, info]), + false, + true + ); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/write_dls/lins.js b/spessasynth_lib/soundfont/basic_soundfont/write_dls/lins.js new file mode 100644 index 0000000000000000000000000000000000000000..9f40077fa32e518f0d747ad79a3a97c827e771df --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/write_dls/lins.js @@ -0,0 +1,18 @@ +import { writeRIFFOddSize } from "../riff_chunk.js"; +import { combineArrays } from "../../../utils/indexed_array.js"; +import { writeIns } from "./ins.js"; + +/** + * @this {BasicSoundBank} + * @returns {IndexedByteArray} + */ +export function writeLins() +{ + const lins = combineArrays(this.presets.map(p => writeIns.apply(this, [p]))); + return writeRIFFOddSize( + "lins", + lins, + false, + true + ); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/write_dls/modulator_converter.js b/spessasynth_lib/soundfont/basic_soundfont/write_dls/modulator_converter.js new file mode 100644 index 0000000000000000000000000000000000000000..c23f08e7c0ce1218d96d9903c70492f31f0d61bc --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/write_dls/modulator_converter.js @@ -0,0 +1,330 @@ +import { midiControllers } from "../../../midi_parser/midi_message.js"; +import { DLSSources } from "../../dls/dls_sources.js"; +import { modulatorCurveTypes, modulatorSources } from "../modulator.js"; +import { generatorTypes } from "../generator.js"; +import { DLSDestinations } from "../../dls/dls_destinations.js"; +import { Articulator } from "./articulator.js"; +import { SpessaSynthWarn } from "../../../utils/loggin.js"; + + +/** + * @param cc {boolean} + * @param index {number} + * @returns {number|undefined} + */ +function getDLSSourceFromSf2Source(cc, index) +{ + if (cc) + { + switch (index) + { + default: + // DLS supports limited controllers + return undefined; + + case midiControllers.modulationWheel: + return DLSSources.modulationWheel; + case midiControllers.mainVolume: + return DLSSources.volume; + case midiControllers.pan: + return DLSSources.pan; + case midiControllers.expressionController: + return DLSSources.expression; + case midiControllers.chorusDepth: + return DLSSources.chorus; + case midiControllers.reverbDepth: + return DLSSources.reverb; + } + } + else + { + switch (index) + { + default: + // cannot be a DLS articulator + return undefined; + + case modulatorSources.noteOnKeyNum: + return DLSSources.keyNum; + case modulatorSources.noteOnVelocity: + return DLSSources.velocity; + case modulatorSources.noController: + return DLSSources.none; + case modulatorSources.polyPressure: + return DLSSources.polyPressure; + case modulatorSources.channelPressure: + return DLSSources.channelPressure; + case modulatorSources.pitchWheel: + return DLSSources.pitchWheel; + case modulatorSources.pitchWheelRange: + return DLSSources.pitchWheelRange; + } + } +} + +/** + * @param dest {number} + * @param amount {number} + * @returns {number|undefined|{dest: number, amount: number}} + */ +function getDLSDestinationFromSf2(dest, amount) +{ + switch (dest) + { + default: + return undefined; + + case generatorTypes.initialAttenuation: + // the amount does not get EMU corrected here, as this only applies to modulator attenuation + // the generator (affected) attenuation is handled in wsmp. + return { dest: DLSDestinations.gain, amount: -amount }; + case generatorTypes.fineTune: + return DLSDestinations.pitch; + case generatorTypes.pan: + return DLSDestinations.pan; + case generatorTypes.keyNum: + return DLSDestinations.keyNum; + + case generatorTypes.reverbEffectsSend: + return DLSDestinations.reverbSend; + case generatorTypes.chorusEffectsSend: + return DLSDestinations.chorusSend; + + case generatorTypes.freqModLFO: + return DLSDestinations.modLfoFreq; + case generatorTypes.delayModLFO: + return DLSDestinations.modLfoDelay; + + case generatorTypes.delayVibLFO: + return DLSDestinations.vibLfoDelay; + case generatorTypes.freqVibLFO: + return DLSDestinations.vibLfoFreq; + + case generatorTypes.delayVolEnv: + return DLSDestinations.volEnvDelay; + case generatorTypes.attackVolEnv: + return DLSDestinations.volEnvAttack; + case generatorTypes.holdVolEnv: + return DLSDestinations.volEnvHold; + case generatorTypes.decayVolEnv: + return DLSDestinations.volEnvDecay; + case generatorTypes.sustainVolEnv: + return { dest: DLSDestinations.volEnvSustain, amount: 1000 - amount }; + case generatorTypes.releaseVolEnv: + return DLSDestinations.volEnvRelease; + + case generatorTypes.delayModEnv: + return DLSDestinations.modEnvDelay; + case generatorTypes.attackModEnv: + return DLSDestinations.modEnvAttack; + case generatorTypes.holdModEnv: + return DLSDestinations.modEnvHold; + case generatorTypes.decayModEnv: + return DLSDestinations.modEnvDecay; + case generatorTypes.sustainModEnv: + return { dest: DLSDestinations.modEnvSustain, amount: 1000 - amount }; + case generatorTypes.releaseModEnv: + return DLSDestinations.modEnvRelease; + + case generatorTypes.initialFilterFc: + return DLSDestinations.filterCutoff; + case generatorTypes.initialFilterQ: + return DLSDestinations.filterQ; + } +} + +/** + * @param dest {number} + * @param amt {number} + * @returns {{source: DLSSources, dest: DLSDestinations, amt: number, isBipolar: boolean}|undefined} + */ +function checkSF2SpecialCombos(dest, amt) +{ + + switch (dest) + { + default: + return undefined; + // mod env + case generatorTypes.modEnvToFilterFc: + return { source: DLSSources.modEnv, dest: DLSDestinations.filterCutoff, amt: amt, isBipolar: false }; + case generatorTypes.modEnvToPitch: + return { source: DLSSources.modEnv, dest: DLSDestinations.pitch, amt: amt, isBipolar: false }; + + // mod lfo + case generatorTypes.modLfoToFilterFc: + return { source: DLSSources.modLfo, dest: DLSDestinations.filterCutoff, amt: amt, isBipolar: true }; + case generatorTypes.modLfoToVolume: + return { source: DLSSources.modLfo, dest: DLSDestinations.gain, amt: amt, isBipolar: true }; + case generatorTypes.modLfoToPitch: + return { source: DLSSources.modLfo, dest: DLSDestinations.pitch, amt: amt, isBipolar: true }; + + // vib lfo + case generatorTypes.vibLfoToPitch: + return { source: DLSSources.vibratoLfo, dest: DLSDestinations.pitch, amt: amt, isBipolar: true }; + + // key to something + case generatorTypes.keyNumToVolEnvHold: + return { + source: DLSSources.keyNum, + dest: DLSDestinations.volEnvHold, + amt: amt, + isBipolar: true + }; + case generatorTypes.keyNumToVolEnvDecay: + return { + source: DLSSources.keyNum, + dest: DLSDestinations.volEnvDecay, + amt: amt, + isBipolar: true + }; + case generatorTypes.keyNumToModEnvHold: + return { + source: DLSSources.keyNum, + dest: DLSDestinations.modEnvHold, + amt: amt, + isBipolar: true + }; + case generatorTypes.keyNumToModEnvDecay: + return { + source: DLSSources.keyNum, + dest: DLSDestinations.modEnvDecay, + amt: amt, + isBipolar: true + }; + + // Scale tuning is implemented in DLS via an articulator: + // keyNum to relative pitch at 12,800 cents. + // Change that to scale tuning * 128. + // Therefore, a regular scale is still 12,800, half is 6400, etc. + case generatorTypes.scaleTuning: + return { + source: DLSSources.keyNum, + dest: DLSDestinations.pitch, + amt: amt * 128, + isBipolar: false // according to table 4, this should be false. + }; + } +} + +/** + * @param gen {Generator} + * @returns {Articulator|undefined} + */ +export function getDLSArticulatorFromSf2Generator(gen) +{ + const dest = getDLSDestinationFromSf2(gen.generatorType, gen.generatorValue); + let destination = dest; + let source = 0; + let amount = gen.generatorValue; + if (dest?.amount !== undefined) + { + amount = dest.amount; + destination = dest.dest; + } + // check for special combo + const combo = checkSF2SpecialCombos(gen.generatorType, gen.generatorValue); + if (combo !== undefined) + { + amount = combo.amt; + destination = combo.dest; + source = combo.source; + } + else if (destination === undefined) + { + SpessaSynthWarn(`Invalid generator type: ${gen.generatorType}`); + return undefined; + } + return new Articulator( + source, + 0, + destination, + amount, + 0 + ); +} + + +/** + * @param mod {Modulator} + * @returns {Articulator|undefined} + */ +export function getDLSArticulatorFromSf2Modulator(mod) +{ + if (mod.transformType !== 0) + { + SpessaSynthWarn("Other transform types are not supported."); + return undefined; + } + let source = getDLSSourceFromSf2Source(mod.sourceUsesCC, mod.sourceIndex); + let sourceTransformType = mod.sourceCurveType; + let sourceBipolar = mod.sourcePolarity; + let sourceDirection = mod.sourceDirection; + if (source === undefined) + { + SpessaSynthWarn(`Invalid source: ${mod.sourceIndex}, CC: ${mod.sourceUsesCC}`); + return undefined; + } + // Attenuation is the opposite of gain. Invert. + if (mod.modulatorDestination === generatorTypes.initialAttenuation) + { + sourceDirection = sourceDirection === 1 ? 0 : 1; + } + let control = getDLSSourceFromSf2Source(mod.secSrcUsesCC, mod.secSrcIndex); + let controlTransformType = mod.secSrcCurveType; + let controlBipolar = mod.secSrcPolarity; + let controlDirection = mod.secSrcDirection; + if (control === undefined) + { + SpessaSynthWarn(`Invalid secondary source: ${mod.secSrcIndex}, CC: ${mod.secSrcUsesCC}`); + return undefined; + } + let dlsDestinationFromSf2 = getDLSDestinationFromSf2(mod.modulatorDestination, mod.transformAmount); + let destination = dlsDestinationFromSf2; + let amt = mod.transformAmount; + if (dlsDestinationFromSf2?.dest !== undefined) + { + destination = dlsDestinationFromSf2.dest; + amt = dlsDestinationFromSf2.amount; + } + const specialCombo = checkSF2SpecialCombos(mod.modulatorDestination, mod.transformAmount); + if (specialCombo !== undefined) + { + amt = specialCombo.amt; + // move the source to control + control = source; + controlTransformType = sourceTransformType; + controlBipolar = sourceBipolar; + controlDirection = sourceDirection; + + // set source as static as it's either: env, lfo or key num + sourceTransformType = modulatorCurveTypes.linear; + sourceBipolar = specialCombo.isBipolar ? 1 : 0; + sourceDirection = 0; + source = specialCombo.source; + destination = specialCombo.dest; + } + else if (destination === undefined) + { + SpessaSynthWarn(`Invalid destination: ${mod.modulatorDestination}`); + return undefined; + } + + // source curve type maps to a desfont curve type in section 2.10, table 9 + let transform = 0; + transform |= controlTransformType << 4; + transform |= controlBipolar << 8; + transform |= controlDirection << 9; + + // use the source curve in output transform + transform |= sourceTransformType; + transform |= sourceBipolar << 14; + transform |= sourceDirection << 15; + return new Articulator( + source, + control, + destination, + amt, + transform + ); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/write_dls/rgn2.js b/spessasynth_lib/soundfont/basic_soundfont/write_dls/rgn2.js new file mode 100644 index 0000000000000000000000000000000000000000..078e09cc02dd25a243095f1ca2b0270cc7fd2a0c --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/write_dls/rgn2.js @@ -0,0 +1,121 @@ +import { combineArrays, IndexedByteArray } from "../../../utils/indexed_array.js"; +import { writeDword, writeWord } from "../../../utils/byte_functions/little_endian.js"; +import { generatorTypes } from "../generator.js"; +import { writeRIFFOddSize } from "../riff_chunk.js"; +import { writeWavesample } from "./wsmp.js"; +import { writeArticulator } from "./art2.js"; + +/** + * @param zone {BasicInstrumentZone} + * @param globalZone {BasicInstrumentZone} + * @this {BasicSoundBank} + * @returns {IndexedByteArray} + */ +export function writeDLSRegion(zone, globalZone) +{ + // region header + const rgnhData = new IndexedByteArray(12); + // keyRange + writeWord(rgnhData, Math.max(zone.keyRange.min, 0)); + writeWord(rgnhData, zone.keyRange.max); + // velRange + writeWord(rgnhData, Math.max(zone.velRange.min, 0)); + writeWord(rgnhData, zone.velRange.max); + // fusOptions: 0 it seems + writeWord(rgnhData, 0); + // keyGroup (exclusive class) + const exclusive = zone.getGeneratorValue(generatorTypes.exclusiveClass, 0); + writeWord(rgnhData, exclusive); + // usLayer + writeWord(rgnhData, 0); + const rgnh = writeRIFFOddSize( + "rgnh", + rgnhData + ); + + let rootKey = zone.getGeneratorValue(generatorTypes.overridingRootKey, zone.sample.samplePitch); + + // a lot of soundfonts like to set scale tuning to 0 in drums and keep the key at 60 + // since we implement scale tuning via a dls articulator and fluid doesn't support these, + // change the root key here + const scaleTuning = zone.getGeneratorValue( + generatorTypes.scaleTuning, + globalZone.getGeneratorValue(generatorTypes.scaleTuning, 100) + ); + if (scaleTuning === 0 && zone.keyRange.max - zone.keyRange.min === 0) + { + rootKey = zone.keyRange.min; + } + + // wave sample (Wsmp) + const wsmp = writeWavesample( + zone.sample, + rootKey, + zone.getGeneratorValue( + generatorTypes.fineTune, + 0 + ) + zone.getGeneratorValue(generatorTypes.coarseTune, 0) * 100 + + zone.sample.samplePitchCorrection, + zone.getGeneratorValue(generatorTypes.initialAttenuation, 0), + // calculate loop with offsets + zone.sample.sampleLoopStartIndex + + zone.getGeneratorValue(generatorTypes.startloopAddrsOffset, 0) + + zone.getGeneratorValue(generatorTypes.startloopAddrsCoarseOffset, 0) * 32768, + zone.sample.sampleLoopEndIndex + + zone.getGeneratorValue(generatorTypes.endloopAddrsOffset, 0) + + zone.getGeneratorValue(generatorTypes.endloopAddrsCoarseOffset, 0) * 32768, + zone.getGeneratorValue(generatorTypes.sampleModes, 0) + ); + + // wave link (wlnk) + const wlnkData = new IndexedByteArray(12); + writeWord(wlnkData, 0); // fusOptions + writeWord(wlnkData, 0); // usPhaseGroup + // let sampleType = 0; + // switch (zone.sample.sampleType) + // { + // default: + // case 1: + // case 4: + // // mono/left + // sampleType = 0; + // break; + // + // case 2: + // // right + // sampleType = 1; + // } + // 1 means that the first bit is on so mono/left + writeDword(wlnkData, 1); // ulChannel + writeDword(wlnkData, this.samples.indexOf(zone.sample)); // ulTableIndex + const wlnk = writeRIFFOddSize( + "wlnk", + wlnkData + ); + + // art + let lar2 = new IndexedByteArray(0); + if (zone.modulators.length + zone.generators.length > 0) + { + const art2 = writeArticulator(zone); + + lar2 = writeRIFFOddSize( + "lar2", + art2, + false, + true + ); + } + + return writeRIFFOddSize( + "rgn2", + combineArrays([ + rgnh, + wsmp, + wlnk, + lar2 + ]), + false, + true + ); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/write_dls/wave.js b/spessasynth_lib/soundfont/basic_soundfont/write_dls/wave.js new file mode 100644 index 0000000000000000000000000000000000000000..a33eda05049cd3627fb8b832b2aa4945d1c0e5e9 --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/write_dls/wave.js @@ -0,0 +1,94 @@ +import { combineArrays, IndexedByteArray } from "../../../utils/indexed_array.js"; +import { writeDword, writeWord } from "../../../utils/byte_functions/little_endian.js"; +import { writeRIFFOddSize } from "../riff_chunk.js"; +import { writeWavesample } from "./wsmp.js"; +import { getStringBytesZero } from "../../../utils/byte_functions/string.js"; +import { SpessaSynthInfo } from "../../../utils/loggin.js"; +import { consoleColors } from "../../../utils/other.js"; + +/** + * @param sample {BasicSample} + * @returns {IndexedByteArray} + */ +export function writeDLSSample(sample) +{ + const fmtData = new IndexedByteArray(18); + writeWord(fmtData, 1); // wFormatTag + writeWord(fmtData, 1); // wChannels + writeDword(fmtData, sample.sampleRate); + writeDword(fmtData, sample.sampleRate * 2); // 16-bit samples + writeWord(fmtData, 2); // wBlockAlign + writeWord(fmtData, 16); // wBitsPerSample + const fmt = writeRIFFOddSize( + "fmt ", + fmtData + ); + let loop = 1; + if (sample.sampleLoopStartIndex + Math.abs(sample.getAudioData().length - sample.sampleLoopEndIndex) < 2) + { + loop = 0; + } + const wsmp = writeWavesample( + sample, + sample.samplePitch, + sample.samplePitchCorrection, + 0, + sample.sampleLoopStartIndex, + sample.sampleLoopEndIndex, + loop + ); + const audio = sample.getAudioData(); + let data; + // if sample is compressed, getRawData cannot be used + if (sample.isCompressed) + { + const data16 = new Int16Array(audio.length); + + for (let i = 0; i < audio.length; i++) + { + // 32,767, as 32,768 may cause overflow (because vorbis can go above 1 sometimes) + data16[i] = audio[i] * 32767; + } + + + data = writeRIFFOddSize( + "data", + new IndexedByteArray(data16.buffer) + ); + } + else + { + data = writeRIFFOddSize( + "data", + sample.getRawData() + ); + } + + const inam = writeRIFFOddSize( + "INAM", + getStringBytesZero(sample.sampleName) + ); + const info = writeRIFFOddSize( + "INFO", + inam, + false, + true + ); + SpessaSynthInfo( + `%cSaved %c${sample.sampleName}%c succesfully!`, + consoleColors.recognized, + consoleColors.value, + consoleColors.recognized + ); + return writeRIFFOddSize( + "wave", + combineArrays([ + fmt, + wsmp, + data, + info + ]), + false, + true + ); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/write_dls/write_dls.js b/spessasynth_lib/soundfont/basic_soundfont/write_dls/write_dls.js new file mode 100644 index 0000000000000000000000000000000000000000..d8f4326303ac58270034d509c3f7dec930b287ea --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/write_dls/write_dls.js @@ -0,0 +1,119 @@ +import { writeRIFFOddSize } from "../riff_chunk.js"; +import { writeDword } from "../../../utils/byte_functions/little_endian.js"; +import { combineArrays, IndexedByteArray } from "../../../utils/indexed_array.js"; +import { writeLins } from "./lins.js"; +import { getStringBytesZero, writeStringAsBytes } from "../../../utils/byte_functions/string.js"; +import { writeWavePool } from "./wvpl.js"; +import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from "../../../utils/loggin.js"; +import { consoleColors } from "../../../utils/other.js"; + +/** + * Write the soundfont as a .dls file. Experimental + * @this {BasicSoundBank} + * @returns {Uint8Array} + */ +export function writeDLS() +{ + SpessaSynthGroupCollapsed( + "%cSaving DLS...", + consoleColors.info + ); + // write colh + const colhNum = new IndexedByteArray(4); + writeDword(colhNum, this.presets.length); + const colh = writeRIFFOddSize( + "colh", + colhNum + ); + SpessaSynthGroupCollapsed( + "%cWriting instruments...", + consoleColors.info + ); + const lins = writeLins.apply(this); + SpessaSynthInfo( + "%cSuccess!", + consoleColors.recognized + ); + SpessaSynthGroupEnd(); + + SpessaSynthGroupCollapsed( + "%cWriting WAVE samples...", + consoleColors.info + ); + const wavepool = writeWavePool.apply(this); + const wvpl = wavepool.data; + const ptblOffsets = wavepool.indexes; + SpessaSynthInfo("%cSucceeded!", consoleColors.recognized); + SpessaSynthGroupEnd(); + + // write ptbl + const ptblData = new IndexedByteArray(8 + 4 * ptblOffsets.length); + writeDword(ptblData, 8); + writeDword(ptblData, ptblOffsets.length); + for (const offset of ptblOffsets) + { + writeDword(ptblData, offset); + } + const ptbl = writeRIFFOddSize( + "ptbl", + ptblData + ); + + this.soundFontInfo["ICMT"] = (this.soundFontInfo["ICMT"] || "Soundfont") + "\nConverted from SF2 to DLS using SpessaSynth"; + this.soundFontInfo["ISFT"] = "SpessaSynth"; + // write INFO + const infos = []; + for (const [info, data] of Object.entries(this.soundFontInfo)) + { + if ( + info !== "ICMT" && + info !== "INAM" && + info !== "ICRD" && + info !== "IENG" && + info !== "ICOP" && + info !== "ISFT" && + info !== "ISBJ" + ) + { + continue; + } + infos.push( + writeRIFFOddSize( + info, + getStringBytesZero(data), + true + ) + ); + } + const info = writeRIFFOddSize( + "INFO", + combineArrays(infos), + false, + true + ); + + const out = new IndexedByteArray( + colh.length + + lins.length + + ptbl.length + + wvpl.length + + info.length + + 4); + writeStringAsBytes(out, "DLS "); + out.set(combineArrays([ + colh, + lins, + ptbl, + wvpl, + info + ]), 4); + SpessaSynthInfo( + "%cSaved succesfully!", + consoleColors.recognized + ); + SpessaSynthGroupEnd(); + return writeRIFFOddSize( + "RIFF", + out + ); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/write_dls/wsmp.js b/spessasynth_lib/soundfont/basic_soundfont/write_dls/wsmp.js new file mode 100644 index 0000000000000000000000000000000000000000..4c6688ce132286d385bd0d593fa2900e7b45828e --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/write_dls/wsmp.js @@ -0,0 +1,78 @@ +import { writeDword, writeWord } from "../../../utils/byte_functions/little_endian.js"; +import { IndexedByteArray } from "../../../utils/indexed_array.js"; +import { writeRIFFOddSize } from "../riff_chunk.js"; + +const WSMP_SIZE = 20; + +/** + * @param sample {BasicSample} + * @param rootKey {number} + * @param tuning {number} + * @param attenuationCentibels {number} CENTIBELS, NO CORRECTION + * @param loopStart {number} + * @param loopEnd {number} + * @param loopingMode {number} + * @returns {IndexedByteArray} + */ +export function writeWavesample( + sample, + rootKey, + tuning, + attenuationCentibels, + loopStart, + loopEnd, + loopingMode) +{ + let loopCount = loopingMode === 0 ? 0 : 1; + const wsmpData = new IndexedByteArray(WSMP_SIZE + loopCount * 16); + writeDword(wsmpData, WSMP_SIZE); // cbSize + // usUnityNote (apply root pitch here) + writeWord(wsmpData, rootKey); + // sFineTune + writeWord(wsmpData, tuning); + + // gain correction, use InitialAttenuation, apply attenuation correction + const attenuationCb = attenuationCentibels * 0.4; + + // gain correction: Each unit of gain represents 1/655360 dB + const lGain = Math.floor(attenuationCb * -65536); + writeDword(wsmpData, lGain); + // fulOptions: has to be 2, according to all DLS files I have + writeDword(wsmpData, 2); + + const loopSize = loopEnd - loopStart; + let ulLoopType = 0; + switch (loopingMode) + { + default: + case 0: + // no loop + loopCount = 0; + break; + + case 1: + // loop + ulLoopType = 0; + loopCount = 1; + break; + + case 3: + // loop and release + ulLoopType = 1; + loopCount = 1; + } + + // cSampleLoops + writeDword(wsmpData, loopCount); + if (loopCount === 1) + { + writeDword(wsmpData, 16); // cbSize + writeDword(wsmpData, ulLoopType); + writeDword(wsmpData, loopStart); + writeDword(wsmpData, loopSize); + } + return writeRIFFOddSize( + "wsmp", + wsmpData + ); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/write_dls/wvpl.js b/spessasynth_lib/soundfont/basic_soundfont/write_dls/wvpl.js new file mode 100644 index 0000000000000000000000000000000000000000..d969b0f9fd3facebebd2e7d70ea6718cc9fb86e2 --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/write_dls/wvpl.js @@ -0,0 +1,32 @@ +import { writeDLSSample } from "./wave.js"; +import { writeRIFFOddSize } from "../riff_chunk.js"; +import { combineArrays } from "../../../utils/indexed_array.js"; + +/** + * @this {BasicSoundBank} + * @returns {{data: IndexedByteArray, indexes: number[] }} + */ +export function writeWavePool() +{ + let currentIndex = 0; + const offsets = []; + /** + * @type {IndexedByteArray[]} + */ + const samples = this.samples.map(s => + { + const out = writeDLSSample(s); + offsets.push(currentIndex); + currentIndex += out.length; + return out; + }); + return { + data: writeRIFFOddSize( + "wvpl", + combineArrays(samples), + false, + true + ), + indexes: offsets + }; +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/write_sf2/ibag.js b/spessasynth_lib/soundfont/basic_soundfont/write_sf2/ibag.js new file mode 100644 index 0000000000000000000000000000000000000000..461d6f494ac972b7ecdd44f8e5c20603aa982f69 --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/write_sf2/ibag.js @@ -0,0 +1,39 @@ +import { IndexedByteArray } from "../../../utils/indexed_array.js"; +import { writeWord } from "../../../utils/byte_functions/little_endian.js"; +import { RiffChunk, writeRIFFChunk } from "../riff_chunk.js"; + +/** + * @this {BasicSoundBank} + * @returns {IndexedByteArray} + */ +export function getIBAG() +{ + // write all ibag with their start indexes as they were changed in getIGEN() and getIMOD() + const ibagsize = this.instruments.reduce((sum, i) => i.instrumentZones.length * 4 + sum, 4); + const ibagdata = new IndexedByteArray(ibagsize); + let zoneID = 0; + let generatorIndex = 0; + let modulatorIndex = 0; + for (const inst of this.instruments) + { + inst.instrumentZoneIndex = zoneID; + for (const ibag of inst.instrumentZones) + { + ibag.zoneID = zoneID; + writeWord(ibagdata, generatorIndex); + writeWord(ibagdata, modulatorIndex); + generatorIndex += ibag.generators.length; + modulatorIndex += ibag.modulators.length; + zoneID++; + } + } + // write the terminal IBAG + writeWord(ibagdata, generatorIndex); + writeWord(ibagdata, modulatorIndex); + + return writeRIFFChunk(new RiffChunk( + "ibag", + ibagdata.length, + ibagdata + )); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/write_sf2/igen.js b/spessasynth_lib/soundfont/basic_soundfont/write_sf2/igen.js new file mode 100644 index 0000000000000000000000000000000000000000..7c97c0ddbe6a38061dd72597401c02b38d49522f --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/write_sf2/igen.js @@ -0,0 +1,80 @@ +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 { Generator, generatorTypes } from "../generator.js"; + +/** + * @this {BasicSoundBank} + * @returns {IndexedByteArray} + */ +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) + { + igensize += inst.instrumentZones.reduce((sum, z) => + { + // clear sample and range generators before determining the size + z.generators = z.generators.filter(g => + g.generatorType !== generatorTypes.sampleID && + g.generatorType !== generatorTypes.keyRange && + g.generatorType !== generatorTypes.velRange + ); + // add sample and ranges if necessary + // unshift vel then key (to make key first) and the instrument is last + if (z.velRange.max !== 127 || z.velRange.min !== 0) + { + z.generators.unshift(new Generator( + generatorTypes.velRange, + z.velRange.max << 8 | Math.max(z.velRange.min, 0), + false + )); + } + if (z.keyRange.max !== 127 || z.keyRange.min !== 0) + { + z.generators.unshift(new Generator( + generatorTypes.keyRange, + z.keyRange.max << 8 | Math.max(z.keyRange.min, 0), + false + )); + } + if (!z.isGlobal) + { + // write sample + z.generators.push(new Generator( + generatorTypes.sampleID, + this.samples.indexOf(z.sample), + false + )); + } + return z.generators.length * 4 + sum; + }, 0); + } + const igendata = new IndexedByteArray(igensize); + let igenIndex = 0; + for (const instrument of this.instruments) + { + for (const instrumentZone of instrument.instrumentZones) + { + // set the start index here + instrumentZone.generatorZoneStartIndex = igenIndex; + for (const gen of instrumentZone.generators) + { + // name is deceptive, it works on negatives + writeWord(igendata, gen.generatorType); + writeWord(igendata, gen.generatorValue); + igenIndex++; + } + } + } + // terminal generator, is zero + writeDword(igendata, 0); + + return writeRIFFChunk(new RiffChunk( + "igen", + igendata.length, + igendata + )); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/write_sf2/imod.js b/spessasynth_lib/soundfont/basic_soundfont/write_sf2/imod.js new file mode 100644 index 0000000000000000000000000000000000000000..535671ec2e1468b0479584c4772265c1b0caef3b --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/write_sf2/imod.js @@ -0,0 +1,46 @@ +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 {BasicSoundBank} + * @returns {IndexedByteArray} + */ +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) + { + 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 ibag of inst.instrumentZones) + { + // set the start index here + ibag.modulatorZoneStartIndex = imodIndex; + for (const mod of ibag.modulators) + { + writeWord(imoddata, mod.sourceEnum); + writeWord(imoddata, mod.modulatorDestination); + writeWord(imoddata, mod.transformAmount); + writeWord(imoddata, mod.secondarySourceEnum); + writeWord(imoddata, mod.transformType); + imodIndex++; + } + } + } + + // terminal modulator, is zero + writeLittleEndian(imoddata, 0, 10); + + return writeRIFFChunk(new RiffChunk( + "imod", + imoddata.length, + imoddata + )); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/write_sf2/inst.js b/spessasynth_lib/soundfont/basic_soundfont/write_sf2/inst.js new file mode 100644 index 0000000000000000000000000000000000000000..2612429d960aa9d016f12e4b69bbf11d5e398169 --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/write_sf2/inst.js @@ -0,0 +1,34 @@ +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 {BasicSoundBank} + * @returns {IndexedByteArray} + */ +export function getINST() +{ + const instsize = this.instruments.length * 22 + 22; + const instdata = new IndexedByteArray(instsize); + // the instrument start index is adjusted in ibag, write it here + let instrumentStart = 0; + let instrumentID = 0; + for (const inst of this.instruments) + { + writeStringAsBytes(instdata, inst.instrumentName, 20); + writeWord(instdata, instrumentStart); + instrumentStart += inst.instrumentZones.length; + inst.instrumentID = instrumentID; + instrumentID++; + } + // write EOI + writeStringAsBytes(instdata, "EOI", 20); + writeWord(instdata, instrumentStart); + + return writeRIFFChunk(new RiffChunk( + "inst", + instdata.length, + instdata + )); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/write_sf2/pbag.js b/spessasynth_lib/soundfont/basic_soundfont/write_sf2/pbag.js new file mode 100644 index 0000000000000000000000000000000000000000..778af84c3c63de84c8871197196be7cd74a845d1 --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/write_sf2/pbag.js @@ -0,0 +1,39 @@ +import { IndexedByteArray } from "../../../utils/indexed_array.js"; +import { writeWord } from "../../../utils/byte_functions/little_endian.js"; +import { RiffChunk, writeRIFFChunk } from "../riff_chunk.js"; + +/** + * @this {BasicSoundBank} + * @returns {IndexedByteArray} + */ +export function getPBAG() +{ + // write all pbag with their start indexes as they were changed in getPGEN() and getPMOD() + const pbagsize = this.presets.reduce((sum, i) => i.presetZones.length * 4 + sum, 4); + const pbagdata = new IndexedByteArray(pbagsize); + let zoneID = 0; + let generatorIndex = 0; + let modulatorIndex = 0; + for (const preset of this.presets) + { + preset.presetZoneStartIndex = zoneID; + for (const pbag of preset.presetZones) + { + pbag.zoneID = zoneID; + writeWord(pbagdata, generatorIndex); + writeWord(pbagdata, modulatorIndex); + generatorIndex += pbag.generators.length; + modulatorIndex += pbag.modulators.length; + zoneID++; + } + } + // write the terminal PBAG + writeWord(pbagdata, generatorIndex); + writeWord(pbagdata, modulatorIndex); + + return writeRIFFChunk(new RiffChunk( + "pbag", + pbagdata.length, + pbagdata + )); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/write_sf2/pgen.js b/spessasynth_lib/soundfont/basic_soundfont/write_sf2/pgen.js new file mode 100644 index 0000000000000000000000000000000000000000..2f4bbb2f06794d30d660d1edcbad01a88176ea85 --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/write_sf2/pgen.js @@ -0,0 +1,82 @@ +import { writeWord } from "../../../utils/byte_functions/little_endian.js"; +import { IndexedByteArray } from "../../../utils/indexed_array.js"; +import { RiffChunk, writeRIFFChunk } from "../riff_chunk.js"; + +import { Generator, generatorTypes } from "../generator.js"; + +/** + * @this {BasicSoundBank} + * @returns {IndexedByteArray} + */ +export function getPGEN() +{ + // almost identical to igen, except the correct instrument instead of sample gen + // goes through all preset zones and writes generators sequentially (add 4 for terminal) + let pgensize = 4; + for (const preset of this.presets) + { + pgensize += preset.presetZones.reduce((size, z) => + { + // clear instrument and range generators before determining the size + z.generators = z.generators.filter(g => + g.generatorType !== generatorTypes.instrument && + g.generatorType !== generatorTypes.keyRange && + g.generatorType !== generatorTypes.velRange + ); + // unshift vel then key and instrument is last + if (z.velRange.max !== 127 || z.velRange.min !== 0) + { + z.generators.unshift(new Generator( + generatorTypes.velRange, + z.velRange.max << 8 | Math.max(z.velRange.min, 0), + false + )); + } + if (z.keyRange.max !== 127 || z.keyRange.min !== 0) + { + z.generators.unshift(new Generator( + generatorTypes.keyRange, + z.keyRange.max << 8 | Math.max(z.keyRange.min, 0), + false + )); + } + if (!z.isGlobal) + { + // write the instrument + z.generators.push(new Generator( + generatorTypes.instrument, + this.instruments.indexOf(z.instrument), + false + )); + } + return z.generators.length * 4 + size; + }, 0); + } + const pgendata = new IndexedByteArray(pgensize); + let pgenIndex = 0; + for (const preset of this.presets) + { + for (const presetZone of preset.presetZones) + { + // set the start index here + presetZone.generatorZoneStartIndex = pgenIndex; + // write generators + for (const gen of presetZone.generators) + { + // name is deceptive, it works on negatives + writeWord(pgendata, gen.generatorType); + writeWord(pgendata, gen.generatorValue); + } + pgenIndex += presetZone.generators.length; + } + } + // terminal generator, is zero + writeWord(pgendata, 0); + writeWord(pgendata, 0); + + return writeRIFFChunk(new RiffChunk( + "pgen", + pgendata.length, + pgendata + )); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/write_sf2/phdr.js b/spessasynth_lib/soundfont/basic_soundfont/write_sf2/phdr.js new file mode 100644 index 0000000000000000000000000000000000000000..3d6f46e0a14d3ae3a6f12b9e3debb91525b9abad --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/write_sf2/phdr.js @@ -0,0 +1,42 @@ +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 {BasicSoundBank} + * @returns {IndexedByteArray} + */ +export function getPHDR() +{ + const phdrsize = this.presets.length * 38 + 38; + const phdrdata = new IndexedByteArray(phdrsize); + // the preset start is adjusted in pbag, this is only for the terminal preset index + let presetStart = 0; + for (const preset of this.presets) + { + writeStringAsBytes(phdrdata, preset.presetName, 20); + writeWord(phdrdata, preset.program); + writeWord(phdrdata, preset.bank); + writeWord(phdrdata, presetStart); + // 3 unused dword, spec says to keep em so we do + writeDword(phdrdata, preset.library); + writeDword(phdrdata, preset.genre); + writeDword(phdrdata, preset.morphology); + presetStart += preset.presetZones.length; + } + // write EOP + writeStringAsBytes(phdrdata, "EOP", 20); + writeWord(phdrdata, 0); // program + writeWord(phdrdata, 0); // bank + writeWord(phdrdata, presetStart); + writeDword(phdrdata, 0); // library + writeDword(phdrdata, 0); // genre + writeDword(phdrdata, 0); // morphology + + return writeRIFFChunk(new RiffChunk( + "phdr", + phdrdata.length, + phdrdata + )); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/write_sf2/pmod.js b/spessasynth_lib/soundfont/basic_soundfont/write_sf2/pmod.js new file mode 100644 index 0000000000000000000000000000000000000000..4432559858447517bbbff81e4f4039dd251bcc57 --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/write_sf2/pmod.js @@ -0,0 +1,46 @@ +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 {BasicSoundBank} + * @returns {IndexedByteArray} + */ +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) + { + 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 pbag of preset.presetZones) + { + // set the start index here + pbag.modulatorZoneStartIndex = pmodIndex; + for (const mod of pbag.modulators) + { + writeWord(pmoddata, mod.sourceEnum); + writeWord(pmoddata, mod.modulatorDestination); + writeWord(pmoddata, mod.transformAmount); + writeWord(pmoddata, mod.secondarySourceEnum); + writeWord(pmoddata, mod.transformType); + pmodIndex++; + } + } + } + + // terminal modulator, is zero + writeLittleEndian(pmoddata, 0, 10); + + return writeRIFFChunk(new RiffChunk( + "pmod", + pmoddata.length, + pmoddata + )); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/write_sf2/sdta.js b/spessasynth_lib/soundfont/basic_soundfont/write_sf2/sdta.js new file mode 100644 index 0000000000000000000000000000000000000000..8785daf99ef066b2587c28dd17afa5a9148f40dc --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/write_sf2/sdta.js @@ -0,0 +1,80 @@ +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 {BasicSoundBank} + * @param smplStartOffsets {number[]} + * @param smplEndOffsets {number[]} + * @param compress {boolean} + * @param quality {number} + * @param vorbisFunc {EncodeVorbisFunction} + * @returns {IndexedByteArray} + */ +export function getSDTA(smplStartOffsets, smplEndOffsets, compress, quality, vorbisFunc) +{ + // 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) + { + s.compressSample(quality, vorbisFunc); + } + const r = s.getRawData(); + SpessaSynthInfo( + `%cEncoded sample %c${i}. ${s.sampleName}%c of %c${this.samples.length}%c. Compressed: %c${s.isCompressed}%c.`, + consoleColors.info, + consoleColors.recognized, + consoleColors.info, + consoleColors.recognized, + consoleColors.info, + s.isCompressed ? consoleColors.recognized : consoleColors.unrecognized, + consoleColors.info + ); + return r; + }); + 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) => + { + const data = sampleDatas[i]; + let startOffset; + let endOffset; + let jump = data.length; + if (sample.isCompressed) + { + // sf3 offset is in bytes + startOffset = smplData.currentIndex; + endOffset = startOffset + data.length; + } + else + { + // sf2 in sample data points + startOffset = smplData.currentIndex / 2; + endOffset = startOffset + data.length / 2; + jump += 46; + } + smplStartOffsets.push(startOffset); + smplData.set(data, smplData.currentIndex); + 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, + smplChunk + )); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/write_sf2/shdr.js b/spessasynth_lib/soundfont/basic_soundfont/write_sf2/shdr.js new file mode 100644 index 0000000000000000000000000000000000000000..1cb6641cbfa9308c99095303d45cffb63cc0235a --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/write_sf2/shdr.js @@ -0,0 +1,55 @@ +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 {BasicSoundBank} + * @param smplStartOffsets {number[]} + * @param smplEndOffsets {number[]} + * @returns {IndexedByteArray} + */ +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) => + { + // sample name + writeStringAsBytes(shdrData, sample.sampleName, 20); + // start offset + const dwStart = smplStartOffsets[index]; + writeDword(shdrData, dwStart); + // end offset + const dwEnd = smplEndOffsets[index]; + writeDword(shdrData, dwEnd); + // 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) + { + // https://github.com/FluidSynth/fluidsynth/wiki/SoundFont3Format + loopStart -= dwStart; + loopEnd -= dwStart; + } + writeDword(shdrData, loopStart); + writeDword(shdrData, loopEnd); + // sample rate + writeDword(shdrData, sample.sampleRate); + // pitch and correction + shdrData[shdrData.currentIndex++] = sample.samplePitch; + shdrData[shdrData.currentIndex++] = sample.samplePitchCorrection; + // sample link + writeWord(shdrData, sample.sampleLink); + // 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( + "shdr", + shdrData.length, + shdrData + )); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/basic_soundfont/write_sf2/write.js b/spessasynth_lib/soundfont/basic_soundfont/write_sf2/write.js new file mode 100644 index 0000000000000000000000000000000000000000..425543706e663b9915612f86240a02df2e2ae570 --- /dev/null +++ b/spessasynth_lib/soundfont/basic_soundfont/write_sf2/write.js @@ -0,0 +1,222 @@ +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 + * @property {number} compressionQuality - the vorbis compression quality, from -0.1 to 1 + * @property {EncodeVorbisFunction|undefined} compressionFunction - the encode vorbis function. + * Can be undefined if not compressed. + */ + +/** + * @type {SoundFont2WriteOptions} + */ +const DEFAULT_WRITE_OPTIONS = { + compress: false, + compressionQuality: 0.5, + compressionFunction: undefined +}; + +/** + * Write the soundfont as an .sf2 file. This method is DESTRUCTIVE + * @this {BasicSoundBank} + * @param {SoundFont2WriteOptions} options + * @returns {Uint8Array} + */ +export function write(options = DEFAULT_WRITE_OPTIONS) +{ + if (options.compress) + { + 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"}`, + consoleColors.info, + consoleColors.recognized, + consoleColors.info, + consoleColors.recognized + ); + SpessaSynthInfo( + "%cWriting INFO...", + consoleColors.info + ); + /** + * Write INFO + * @type {IndexedByteArray[]} + */ + const infoArrays = []; + this.soundFontInfo["ISFT"] = "SpessaSynth"; // ( ͡° ͜ʖ ͡°) + 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") + { + const major = parseInt(data.split(".")[0]); + const minor = parseInt(data.split(".")[1]); + const ckdata = new IndexedByteArray(4); + writeWord(ckdata, major); + writeWord(ckdata, minor); + infoArrays.push(writeRIFFChunk(new RiffChunk( + type, + 4, + ckdata + ))); + } + else if (type === "DMOD") + { + infoArrays.push(writeRIFFChunk(new RiffChunk( + type, + data.length, + data + ))); + } + else + { + const arr = new IndexedByteArray(data.length); + writeStringAsBytes(arr, data); + infoArrays.push(writeRIFFChunk(new RiffChunk( + type, + data.length, + arr + ))); + } + } + const combined = combineArrays([ + new IndexedByteArray([73, 78, 70, 79]), // INFO + ...infoArrays + ]); + const infoChunk = writeRIFFChunk(new RiffChunk("LIST", combined.length, combined)); + + SpessaSynthInfo( + "%cWriting SDTA...", + consoleColors.info + ); + // write sdta + const smplStartOffsets = []; + const smplEndOffsets = []; + const sdtaChunk = getSDTA.call( + this, + smplStartOffsets, + smplEndOffsets, + options?.compress, + options?.compressionQuality ?? 0.5, + options.compressionFunction + ); + + SpessaSynthInfo( + "%cWriting PDTA...", + consoleColors.info + ); + // write pdta + // go in reverse so the indexes are correct + // instruments + SpessaSynthInfo( + "%cWriting SHDR...", + consoleColors.info + ); + const shdrChunk = getSHDR.call(this, smplStartOffsets, smplEndOffsets); + SpessaSynthInfo( + "%cWriting IGEN...", + consoleColors.info + ); + const igenChunk = getIGEN.call(this); + SpessaSynthInfo( + "%cWriting IMOD...", + consoleColors.info + ); + const imodChunk = getIMOD.call(this); + SpessaSynthInfo( + "%cWriting IBAG...", + consoleColors.info + ); + const ibagChunk = getIBAG.call(this); + SpessaSynthInfo( + "%cWriting INST...", + consoleColors.info + ); + const instChunk = getINST.call(this); + // presets + const pgenChunk = getPGEN.call(this); + SpessaSynthInfo( + "%cWriting PMOD...", + consoleColors.info + ); + const pmodChunk = getPMOD.call(this); + SpessaSynthInfo( + "%cWriting PBAG...", + consoleColors.info + ); + const pbagChunk = getPBAG.call(this); + SpessaSynthInfo( + "%cWriting PHDR...", + consoleColors.info + ); + const phdrChunk = getPHDR.call(this); + // combine in the sfspec order + const pdtadata = combineArrays([ + new IndexedByteArray([112, 100, 116, 97]), // "pdta" + phdrChunk, + pbagChunk, + pmodChunk, + pgenChunk, + instChunk, + ibagChunk, + imodChunk, + igenChunk, + shdrChunk + ]); + const pdtaChunk = writeRIFFChunk(new RiffChunk( + "LIST", + pdtadata.length, + pdtadata + )); + SpessaSynthInfo( + "%cWriting the output file...", + consoleColors.info + ); + // finally, combine everything + const riffdata = combineArrays([ + new IndexedByteArray([115, 102, 98, 107]), // "sfbk" + infoChunk, + sdtaChunk, + pdtaChunk + ]); + + const main = writeRIFFChunk(new RiffChunk( + "RIFF", + riffdata.length, + riffdata + )); + SpessaSynthInfo( + `%cSaved succesfully! Final file size: %c${main.length}`, + consoleColors.info, + consoleColors.recognized + ); + SpessaSynthGroupEnd(); + return main; +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/dls/articulator_converter.js b/spessasynth_lib/soundfont/dls/articulator_converter.js new file mode 100644 index 0000000000000000000000000000000000000000..aa8db593e5024f614da5b0e7000cc5ab96eb9cc1 --- /dev/null +++ b/spessasynth_lib/soundfont/dls/articulator_converter.js @@ -0,0 +1,396 @@ +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 { consoleColors } from "../../utils/other.js"; +import { SpessaSynthWarn } from "../../utils/loggin.js"; + +/** + * @param source {number} + * @returns {{enum: number, isCC: boolean}|undefined} + */ +function getSF2SourceFromDLS(source) +{ + let sourceEnum = undefined; + let isCC = false; + switch (source) + { + default: + case DLSSources.modLfo: + case DLSSources.vibratoLfo: + case DLSSources.coarseTune: + case DLSSources.fineTune: + case DLSSources.modEnv: + return undefined; // cannot be this in sf2 + + case DLSSources.keyNum: + sourceEnum = modulatorSources.noteOnKeyNum; + break; + case DLSSources.none: + sourceEnum = modulatorSources.noController; + break; + case DLSSources.modulationWheel: + sourceEnum = midiControllers.modulationWheel; + isCC = true; + break; + case DLSSources.pan: + sourceEnum = midiControllers.pan; + isCC = true; + break; + case DLSSources.reverb: + sourceEnum = midiControllers.reverbDepth; + isCC = true; + break; + case DLSSources.chorus: + sourceEnum = midiControllers.chorusDepth; + isCC = true; + break; + case DLSSources.expression: + sourceEnum = midiControllers.expressionController; + isCC = true; + break; + case DLSSources.volume: + sourceEnum = midiControllers.mainVolume; + isCC = true; + break; + case DLSSources.velocity: + sourceEnum = modulatorSources.noteOnVelocity; + break; + case DLSSources.polyPressure: + sourceEnum = modulatorSources.polyPressure; + break; + case DLSSources.channelPressure: + sourceEnum = modulatorSources.channelPressure; + break; + case DLSSources.pitchWheel: + sourceEnum = modulatorSources.pitchWheel; + break; + case DLSSources.pitchWheelRange: + sourceEnum = modulatorSources.pitchWheelRange; + break; + } + if (sourceEnum === undefined) + { + throw new Error(`Unknown DLS Source: ${source}`); + } + return { enum: sourceEnum, isCC: isCC }; +} + +/** + * @param destination {number} + * @param amount {number} + * @returns {generatorTypes|{gen: generatorTypes, newAmount: number}} // transform amount to sf2 units + */ +function getSF2GeneratorFromDLS(destination, amount) +{ + switch (destination) + { + default: + case DLSDestinations.none: + return undefined; + case DLSDestinations.pan: + return generatorTypes.pan; + case DLSDestinations.gain: + 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; + case DLSDestinations.volEnvAttack: + return generatorTypes.attackVolEnv; + case DLSDestinations.volEnvHold: + return generatorTypes.holdVolEnv; + case DLSDestinations.volEnvDecay: + return generatorTypes.decayVolEnv; + case DLSDestinations.volEnvSustain: + return { gen: generatorTypes.sustainVolEnv, newAmount: 1000 - amount }; + case DLSDestinations.volEnvRelease: + return generatorTypes.releaseVolEnv; + + // mod env + case DLSDestinations.modEnvDelay: + return generatorTypes.delayModEnv; + case DLSDestinations.modEnvAttack: + return generatorTypes.attackModEnv; + case DLSDestinations.modEnvHold: + return generatorTypes.holdModEnv; + case DLSDestinations.modEnvDecay: + return generatorTypes.decayModEnv; + case DLSDestinations.modEnvSustain: + return { gen: generatorTypes.sustainModEnv, newAmount: (1000 - amount) / 10 }; + case DLSDestinations.modEnvRelease: + return generatorTypes.releaseModEnv; + + case DLSDestinations.filterCutoff: + return generatorTypes.initialFilterFc; + case DLSDestinations.filterQ: + return generatorTypes.initialFilterQ; + case DLSDestinations.chorusSend: + return generatorTypes.chorusEffectsSend; + case DLSDestinations.reverbSend: + return generatorTypes.reverbEffectsSend; + + // lfo + case DLSDestinations.modLfoFreq: + return generatorTypes.freqModLFO; + case DLSDestinations.modLfoDelay: + return generatorTypes.delayModLFO; + case DLSDestinations.vibLfoFreq: + return generatorTypes.freqVibLFO; + case DLSDestinations.vibLfoDelay: + return generatorTypes.delayVibLFO; + } +} + +/** + * checks for combos such as mod lfo as source and pitch as destination which results in modLfoToPitch + * @param source {number} + * @param destination {number} + * @returns {generatorTypes} real destination + */ +function checkForSpecialDLSCombo(source, destination) +{ + if (source === DLSSources.vibratoLfo && destination === DLSDestinations.pitch) + { + // vibrato lfo to pitch + return generatorTypes.vibLfoToPitch; + } + else if (source === DLSSources.modLfo && destination === DLSDestinations.pitch) + { + // mod lfo to pitch + return generatorTypes.modLfoToPitch; + } + else if (source === DLSSources.modLfo && destination === DLSDestinations.filterCutoff) + { + // mod lfo to filter + return generatorTypes.modLfoToFilterFc; + } + else if (source === DLSSources.modLfo && destination === DLSDestinations.gain) + { + // mod lfo to volume + return generatorTypes.modLfoToVolume; + } + else if (source === DLSSources.modEnv && destination === DLSDestinations.filterCutoff) + { + // mod envelope to filter + return generatorTypes.modEnvToFilterFc; + } + else if (source === DLSSources.modEnv && destination === DLSDestinations.pitch) + { + // mod envelope to pitch + return generatorTypes.modEnvToPitch; + } + else + { + return undefined; + } +} + +// noinspection JSUnusedGlobalSymbols +/** + * @param source {number} + * @param control {number} + * @param destination {number} + * @param value {number} + * @param transform {number} + * @param msg {string} + */ +export function modulatorConverterDebug( + source, + control, + destination, + value, + transform, + msg = "Attempting to convert the following DLS Articulator to SF2 Modulator:" +) +{ + const type = Object.keys(DLSDestinations).find(k => DLSDestinations[k] === destination); + const srcType = Object.keys(DLSSources).find(k => DLSSources[k] === source); + const ctrlType = Object.keys(DLSSources).find(k => DLSSources[k] === control); + const typeString = type ? type : destination.toString(16); + const srcString = srcType ? srcType : source.toString(16); + const ctrlString = ctrlType ? ctrlType : control.toString(16); + console.debug( + `%c${msg} + Source: %c${srcString}%c + Control: %c${ctrlString}%c + Destination: %c${typeString}%c + Amount: %c${value}%c + Transform: %c${transform}%c...`, + consoleColors.info, + consoleColors.recognized, + consoleColors.info, + consoleColors.recognized, + consoleColors.info, + consoleColors.recognized, + consoleColors.info, + consoleColors.recognized, + consoleColors.info, + consoleColors.recognized, + consoleColors.info + ); +} + +/** + * @param source {number} + * @param control {number} + * @param destination {number} + * @param transform {number} + * @param value {number} + * @returns {Modulator|undefined} + */ +export function getSF2ModulatorFromArticulator( + source, + control, + destination, + transform, + value +) +{ + // modulatorConverterDebug( + // source, + // control, + // destination, + // value, + // transform + // ); + // check for special combinations + const specialDestination = checkForSpecialDLSCombo(source, destination); + /** + * @type {generatorTypes} + */ + let destinationGenerator; + /** + * @type {{enum: number, isCC: boolean}} + */ + let sf2Source; + let swapSources = false; + let isSourceNoController = false; + let newValue = value; + if (specialDestination === undefined) + { + // determine destination + const sf2GenDestination = getSF2GeneratorFromDLS(destination, value); + if (sf2GenDestination === undefined) + { + // cannot be a valid modulator + SpessaSynthWarn(`Invalid destination: ${destination}`); + return undefined; + } + /** + * @type {generatorTypes} + */ + destinationGenerator = sf2GenDestination; + if (sf2GenDestination.newAmount !== undefined) + { + newValue = sf2GenDestination.newAmount; + destinationGenerator = sf2GenDestination.gen; + } + sf2Source = getSF2SourceFromDLS(source); + if (sf2Source === undefined) + { + // cannot be a valid modulator + SpessaSynthWarn(`Invalid source: ${source}`); + return undefined; + } + } + else + { + destinationGenerator = specialDestination; + swapSources = true; + sf2Source = { enum: modulatorSources.noController, isCC: false }; + isSourceNoController = true; + } + let sf2SecondSource = getSF2SourceFromDLS(control); + if (sf2SecondSource === undefined) + { + // cannot be a valid modulator + SpessaSynthWarn(`Invalid control: ${control}`); + return undefined; + } + + // get transforms and final enums + let sourceEnumFinal; + if (isSourceNoController) + { + // we force it into this state because before it was some strange value, + // like vibrato lfo bipolar, for example, + // since we turn it into NoController -> vibLfoToPitch, + // the result is the same and bipolar controller is technically 0 + sourceEnumFinal = 0x0; + } + else + { + // output transform is ignored as it's not a thing in sfont format + // unless the curve type of source is linear, then output is copied + const outputTransform = transform & 0b1111; + // source curve type maps to a desfont curve type in section 2.10, table 9 + let sourceTransform = (transform >> 10) & 0b1111; + 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 (dls gain is the opposite of sf2 attenuation) + if (destinationGenerator === generatorTypes.initialAttenuation) + { + // if the value is negative, the source shall be negative! + // why? + // IDK, it makes it work with ROCK.RMI and NOKIA_S30.dls + if (value < 0) + { + sourceIsNegative = 1; + } + } + sourceEnumFinal = getModSourceEnum( + sourceTransform, + sourceIsBipolar, + sourceIsNegative, + sf2Source.isCC, + sf2Source.enum + ); + } + + // a corrupted rendition of gm.dls was found under + // https://sembiance.com/fileFormatSamples/audio/downloadableSoundBank/ + // which specifies a whopping -32,768 decibels of attenuation + if (destinationGenerator === generatorTypes.initialAttenuation) + { + newValue = Math.max(960, Math.min(0, newValue)); + } + + const secSourceTransform = (transform >> 4) & 0b1111; + const secSourceIsBipolar = (transform >> 8) & 1; + const secSourceIsNegative = transform >> 9 & 1; + let secSourceEnumFinal = getModSourceEnum( + secSourceTransform, + secSourceIsBipolar, + secSourceIsNegative, + sf2SecondSource.isCC, + sf2SecondSource.enum + ); + + if (swapSources) + { + const temp = secSourceEnumFinal; + secSourceEnumFinal = sourceEnumFinal; + sourceEnumFinal = temp; + } + + // return the modulator! + return new Modulator( + sourceEnumFinal, + secSourceEnumFinal, + destinationGenerator, + newValue, + 0x0 + ); + +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/dls/dls_destinations.js b/spessasynth_lib/soundfont/dls/dls_destinations.js new file mode 100644 index 0000000000000000000000000000000000000000..b47a6df2e2c40503356361c3754666c26c57cb0e --- /dev/null +++ b/spessasynth_lib/soundfont/dls/dls_destinations.js @@ -0,0 +1,38 @@ +/** + * + * @enum {number} + */ +export const DLSDestinations = { + none: 0x0, // no destination + gain: 0x1, // linear gain + reserved: 0x2, // reserved + pitch: 0x3, // pitch in cents + pan: 0x4, // pan 10ths of a percent + keyNum: 0x5, // MIDI key number + // nuh uh, the channel controllers are not supported! + chorusSend: 0x80, // chorus send level 10ths of a percent + reverbSend: 0x81, // reverb send level 10ths of a percent + + modLfoFreq: 0x104, // modulation LFO frequency + modLfoDelay: 0x105, // modulation LFO delay + + vibLfoFreq: 0x114, // vibrato LFO frequency + vibLfoDelay: 0x115, // vibrato LFO delay + + volEnvAttack: 0x206, // volume envelope attack + volEnvDecay: 0x207, // volume envelope decay + volEnvRelease: 0x209, // volume envelope release + volEnvSustain: 0x20a, // volume envelope sustain + volEnvDelay: 0x20b, // volume envelope delay + volEnvHold: 0x20c, // volume envelope hold + + modEnvAttack: 0x30a, // modulation envelope attack + modEnvDecay: 0x30b, // modulation envelope decay + modEnvRelease: 0x30d, // modulation envelope release + modEnvSustain: 0x30e, // modulation envelope sustain + modEnvDelay: 0x30f, // modulation envelope delay + modEnvHold: 0x310, // modulation envelope hold + + filterCutoff: 0x500, // low pass filter cutoff frequency + filterQ: 0x501 // low pass filter resonance +}; \ No newline at end of file diff --git a/spessasynth_lib/soundfont/dls/dls_preset.js b/spessasynth_lib/soundfont/dls/dls_preset.js new file mode 100644 index 0000000000000000000000000000000000000000..4c73dfe6ad681f9f2eccf9bfd30d77f0a74619a7 --- /dev/null +++ b/spessasynth_lib/soundfont/dls/dls_preset.js @@ -0,0 +1,44 @@ +import { BasicPreset } from "../basic_soundfont/basic_preset.js"; +import { BasicPresetZone } from "../basic_soundfont/basic_zones.js"; +import { BasicInstrument } from "../basic_soundfont/basic_instrument.js"; + +export class DLSPreset extends BasicPreset +{ + /** + * Creates a new DLS preset + * @param dls {BasicSoundBank} + * @param ulBank {number} + * @param ulInstrument {number} + */ + constructor(dls, ulBank, ulInstrument) + { + // use stock default modulators, dls won't ever have DMOD chunk + super(dls); + this.program = ulInstrument & 127; + const bankMSB = (ulBank >> 8) & 127; + const bankLSB = ulBank & 127; + // switch accordingly + if (bankMSB > 0) + { + this.bank = bankMSB; + } + else + { + this.bank = bankLSB; + } + const isDrums = ulBank >> 31; + 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/spessasynth_lib/soundfont/dls/dls_sample.js b/spessasynth_lib/soundfont/dls/dls_sample.js new file mode 100644 index 0000000000000000000000000000000000000000..717d313121adcdbf08958cb52fe366afae1d5f4f --- /dev/null +++ b/spessasynth_lib/soundfont/dls/dls_sample.js @@ -0,0 +1,75 @@ +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} + * @param pitch {number} + * @param pitchCorrection {number} + * @param loopStart {number} sample data points + * @param loopEnd {number} sample data points + * @param data {Float32Array} + * @param sampleDbAttenuation {number} in db + */ + constructor( + name, + rate, + pitch, + pitchCorrection, + loopStart, + loopEnd, + data, + sampleDbAttenuation + ) + { + super( + name, + rate, + pitch, + pitchCorrection, + 0, + 1, + loopStart, + loopEnd + ); + this.sampleData = data; + this.sampleDbAttenuation = sampleDbAttenuation; + } + + getAudioData() + { + return this.sampleData; + } + + getRawData() + { + if (this.isCompressed) + { + if (!this.compressedData) + { + throw new Error("Compressed but no data?? This shouldn't happen!!"); + } + return this.compressedData; + } + return super.getRawData(); + // const uint8 = new Uint8Array(this.sampleData.length * 2); + // for (let i = 0; i < this.sampleData.length; i++) + // { + // const sample = Math.floor(this.sampleData[i] * 32768); + // uint8[i * 2] = sample & 0xFF; // lower byte + // uint8[i * 2 + 1] = (sample >> 8) & 0xFF; // upper byte + // } + // return uint8; + } +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/dls/dls_soundfont.js b/spessasynth_lib/soundfont/dls/dls_soundfont.js new file mode 100644 index 0000000000000000000000000000000000000000..52c1593ad46e02d289ebf42d0f26a04a373b4d46 --- /dev/null +++ b/spessasynth_lib/soundfont/dls/dls_soundfont.js @@ -0,0 +1,186 @@ +import { BasicSoundBank } 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 BasicSoundBank +{ + /** + * Loads a new DLS (Downloadable sounds) soundfont + * @param buffer {ArrayBuffer} + */ + constructor(buffer) + { + super(); + this.dataArray = new IndexedByteArray(buffer); + SpessaSynthGroup("%cParsing DLS...", consoleColors.info); + if (!this.dataArray) + { + SpessaSynthGroupEnd(); + this.parsingError("No data provided!"); + } + + // read the main chunk + let firstChunk = readRIFFChunk(this.dataArray, false); + this.verifyHeader(firstChunk, "riff"); + this.verifyText(readBytesAsString(this.dataArray, 4).toLowerCase(), "dls "); + + /** + * Read the list + * @type {RiffChunk[]} + */ + const chunks = []; + 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(); + + // read info + const infoChunk = findRIFFListType(chunks, "INFO"); + if (infoChunk) + { + 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)"; + if (this.soundFontInfo["ISBJ"]) + { + // merge it + this.soundFontInfo["ICMT"] += "\n" + this.soundFontInfo["ISBJ"]; + delete this.soundFontInfo["ISBJ"]; + } + this.soundFontInfo["ICMT"] += "\nConverted from DLS to SF2 with SpessaSynth"; + + for (const [info, value] of Object.entries(this.soundFontInfo)) + { + SpessaSynthInfo( + `%c"${info}": %c"${value}"`, + consoleColors.info, + consoleColors.recognized + ); + } + + // read "colh" + let colhChunk = chunks.find(c => c.header === "colh"); + if (!colhChunk) + { + SpessaSynthGroupEnd(); + this.parsingError("No colh chunk!"); + } + this.instrumentAmount = readLittleEndian(colhChunk.chunkData, 4); + SpessaSynthInfo( + `%cInstruments amount: %c${this.instrumentAmount}`, + consoleColors.info, + consoleColors.recognized + ); + + // read the wave list + let waveListChunk = findRIFFListType(chunks, "wvpl"); + if (!waveListChunk) + { + SpessaSynthGroupEnd(); + this.parsingError("No wvpl chunk!"); + } + this.readDLSSamples(waveListChunk); + + // read the instrument list + let instrumentListChunk = findRIFFListType(chunks, "lins"); + if (!instrumentListChunk) + { + SpessaSynthGroupEnd(); + this.parsingError("No lins chunk!"); + } + this.readDLSInstrumentList(instrumentListChunk); + + // sort presets + this.presets.sort((a, b) => (a.program - b.program) + (a.bank - b.bank)); + this._parseInternal(); + 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, + consoleColors.info, + consoleColors.recognized, + consoleColors.info, + consoleColors.recognized, + consoleColors.info, + consoleColors.recognized, + consoleColors.info + ); + SpessaSynthGroupEnd(); + } + + /** + * @param chunk {RiffChunk} + * @param expected {string} + */ + verifyHeader(chunk, ...expected) + { + for (const expect of expected) + { + if (chunk.header.toLowerCase() === expect.toLowerCase()) + { + return; + } + } + SpessaSynthGroupEnd(); + this.parsingError(`Invalid DLS chunk header! Expected "${expected.toString()}" got "${chunk.header.toLowerCase()}"`); + } + + /** + * @param text {string} + * @param expected {string} + */ + verifyText(text, expected) + { + if (text.toLowerCase() !== expected.toLowerCase()) + { + SpessaSynthGroupEnd(); + this.parsingError(`FourCC error: Expected "${expected.toLowerCase()}" got "${text.toLowerCase()}"`); + } + } + + /** + * @param error {string} + */ + parsingError(error) + { + throw new Error(`DLS parse error: ${error} The file may be corrupted.`); + } + + destroySoundBank() + { + super.destroySoundBank(); + delete this.dataArray; + } +} + +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 diff --git a/spessasynth_lib/soundfont/dls/dls_sources.js b/spessasynth_lib/soundfont/dls/dls_sources.js new file mode 100644 index 0000000000000000000000000000000000000000..2837e67977393a25c10d8511a88eacb97afb125c --- /dev/null +++ b/spessasynth_lib/soundfont/dls/dls_sources.js @@ -0,0 +1,62 @@ +import { Modulator } from "../basic_soundfont/modulator.js"; +import { generatorTypes } from "../basic_soundfont/generator.js"; + +/** + * @enum {number} + */ +export const DLSSources = { + none: 0x0, + modLfo: 0x1, + velocity: 0x2, + keyNum: 0x3, + volEnv: 0x4, + modEnv: 0x5, + pitchWheel: 0x6, + polyPressure: 0x7, + channelPressure: 0x8, + vibratoLfo: 0x9, + + modulationWheel: 0x81, + volume: 0x87, + pan: 0x8a, + expression: 0x8b, + // note: these are flipped unintentionally in DLS2 table 9. Argh! + chorus: 0xdd, + reverb: 0xdb, + + pitchWheelRange: 0x100, + fineTune: 0x101, + coarseTune: 0x102 +}; + +export const DEFAULT_DLS_REVERB = new Modulator( + 0x00DB, + 0x0, + generatorTypes.reverbEffectsSend, + 1000, + 0 +); + +export const DEFAULT_DLS_CHORUS = new Modulator( + 0x00DD, + 0x0, + generatorTypes.chorusEffectsSend, + 1000, + 0 +); + +export const DLS_1_NO_VIBRATO_MOD = new Modulator( + 0x0081, + 0x0, + generatorTypes.vibLfoToPitch, + 0, + 0 +); + +export const DLS_1_NO_VIBRATO_PRESSURE = new Modulator( + 0x000D, + 0x0, + generatorTypes.vibLfoToPitch, + 0, + 0 +); \ No newline at end of file diff --git a/spessasynth_lib/soundfont/dls/dls_zone.js b/spessasynth_lib/soundfont/dls/dls_zone.js new file mode 100644 index 0000000000000000000000000000000000000000..2a02f5632957d06e9572e2e3423f471252d920c8 --- /dev/null +++ b/spessasynth_lib/soundfont/dls/dls_zone.js @@ -0,0 +1,95 @@ +import { BasicInstrumentZone } from "../basic_soundfont/basic_zones.js"; +import { Generator, generatorTypes } from "../basic_soundfont/generator.js"; + +export class DLSZone extends BasicInstrumentZone +{ + /** + * @param keyRange {SoundFontRange} + * @param velRange {SoundFontRange} + */ + constructor(keyRange, velRange) + { + super(); + this.keyRange = keyRange; + this.velRange = velRange; + this.isGlobal = true; + } + + /** + * @param attenuationCb {number} with EMU correction + * @param loopingMode {number} the sfont one + * @param loop {{start: number, end: number}} + * @param sampleKey {number} + * @param sample {BasicSample} + * @param sampleID {number} + * @param samplePitchCorrection {number} cents + */ + setWavesample( + attenuationCb, + loopingMode, + loop, + sampleKey, + sample, + sampleID, + samplePitchCorrection + ) + { + 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) + { + this.generators.push(new Generator(generatorTypes.coarseTune, coarseTune)); + } + const fineTune = samplePitchCorrection - (coarseTune * 100); + if (fineTune !== 0) + { + this.generators.push(new Generator(generatorTypes.fineTune, fineTune)); + } + + // correct loop if needed + if (loopingMode !== 0) + { + const diffStart = loop.start - sample.sampleLoopStartIndex; + const diffEnd = loop.end - sample.sampleLoopEndIndex; + 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) + { + this.generators.push(new Generator(generatorTypes.startloopAddrsCoarseOffset, coarse)); + } + } + 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) + { + this.generators.push(new Generator(generatorTypes.endloopAddrsCoarseOffset, coarse)); + } + } + } + // correct the key if needed + if (sampleKey !== sample.samplePitch) + { + this.generators.push(new Generator(generatorTypes.overridingRootKey, sampleKey)); + } + // add sample ID + this.generators.push(new Generator(generatorTypes.sampleID, sampleID)); + this.sample = sample; + sample.useCount++; + } +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/dls/read_articulation.js b/spessasynth_lib/soundfont/dls/read_articulation.js new file mode 100644 index 0000000000000000000000000000000000000000..47c1e7662526c6e688fd4e8b43e875e1918a270b --- /dev/null +++ b/spessasynth_lib/soundfont/dls/read_articulation.js @@ -0,0 +1,299 @@ +import { readLittleEndian } from "../../utils/byte_functions/little_endian.js"; +import { DLSDestinations } from "./dls_destinations.js"; +import { DLS_1_NO_VIBRATO_MOD, DLS_1_NO_VIBRATO_PRESSURE, 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"; + + +/** + * Reads the articulator chunk + * @param chunk {RiffChunk} + * @param disableVibrato {boolean} it seems that dls 1 does not have vibrato lfo, so we shall disable it + * @returns {{modulators: Modulator[], generators: Generator[]}} + */ +export function readArticulation(chunk, disableVibrato) +{ + const artData = chunk.chunkData; + /** + * @type {Generator[]} + */ + const generators = []; + /** + * @type {Modulator[]} + */ + const modulators = []; + + // cbSize (ignore) + readLittleEndian(artData, 4); + const connectionsAmount = readLittleEndian(artData, 4); + for (let i = 0; i < connectionsAmount; i++) + { + // read the block + const source = readLittleEndian(artData, 2); + const control = readLittleEndian(artData, 2); + const destination = readLittleEndian(artData, 2); + 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) + { + /** + * @type {Generator} + */ + let generator; + switch (destination) + { + case DLSDestinations.pan: + generator = new Generator(generatorTypes.pan, value); // turn percent into tenths of percent + break; + case DLSDestinations.gain: + generator = new Generator(generatorTypes.initialAttenuation, -value * 10 / 0.4); // turn to centibels and apply emu correction + break; + case DLSDestinations.filterCutoff: + generator = new Generator(generatorTypes.initialFilterFc, value); + break; + 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); + break; + case DLSDestinations.modLfoDelay: + generator = new Generator(generatorTypes.delayModLFO, value); + break; + case DLSDestinations.vibLfoFreq: + generator = new Generator(generatorTypes.freqVibLFO, value); + break; + 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); + break; + case DLSDestinations.volEnvAttack: + generator = new Generator(generatorTypes.attackVolEnv, value); + break; + case DLSDestinations.volEnvHold: + // do not validate because keyNumToSomething + generator = new Generator(generatorTypes.holdVolEnv, value, false); + break; + case DLSDestinations.volEnvDecay: + // do not validate because keyNumToSomething + generator = new Generator(generatorTypes.decayVolEnv, value, false); + break; + case DLSDestinations.volEnvRelease: + generator = new Generator(generatorTypes.releaseVolEnv, value); + break; + case DLSDestinations.volEnvSustain: + // gain seems to be (1000 - value) / 10 = sustain dB + const sustainCb = 1000 - value; + generator = new Generator(generatorTypes.sustainVolEnv, sustainCb); + break; + + // mod env + case DLSDestinations.modEnvDelay: + generator = new Generator(generatorTypes.delayModEnv, value); + break; + case DLSDestinations.modEnvAttack: + generator = new Generator(generatorTypes.attackModEnv, value); + break; + case DLSDestinations.modEnvHold: + // do not validate because keyNumToSomething + generator = new Generator(generatorTypes.holdModEnv, value, false); + break; + case DLSDestinations.modEnvDecay: + // do not validate because keyNumToSomething + generator = new Generator(generatorTypes.decayModEnv, value, false); + break; + case DLSDestinations.modEnvRelease: + generator = new Generator(generatorTypes.releaseModEnv, value); + break; + case DLSDestinations.modEnvSustain: + // dls uses 1%, desfont uses 0.1% + const percentageSustain = 1000 - value; + generator = new Generator(generatorTypes.sustainModEnv, percentageSustain); + break; + + case DLSDestinations.reverbSend: + generator = new Generator(generatorTypes.reverbEffectsSend, value); + break; + case DLSDestinations.chorusSend: + generator = new Generator(generatorTypes.chorusEffectsSend, value); + break; + case DLSDestinations.pitch: + // split it up + const semi = Math.floor(value / 100); + const cents = Math.floor(value - semi * 100); + generator = new Generator(generatorTypes.fineTune, cents); + generators.push(new Generator(generatorTypes.coarseTune, semi)); + break; + } + if (generator) + { + generators.push(generator); + } + } + else + // if not, modulator? + { + let isGenerator = true; + + const applyKeyToCorrection = (value, keyToGen, realGen) => + { + // according to viena and another strange (with modulators) rendition of gm.dls in sf2, + // it shall be divided by -128 + // and a strange correction needs to be applied to the real value: + // real + (60 / 128) * scale + const keyToGenValue = value / -128; + generators.push(new Generator(keyToGen, keyToGenValue)); + // airfont 340 fix + if (keyToGenValue <= 120) + { + const correction = Math.round((60 / 128) * value); + generators.forEach(g => + { + if (g.generatorType === realGen) + { + g.generatorValue += correction; + } + }); + } + }; + + // a few special cases which are generators: + if (control === DLSSources.none) + { + // mod lfo to 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) + { + generators.push(new Generator(generatorTypes.modLfoToVolume, value)); + } + else + // 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) + { + generators.push(new Generator(generatorTypes.vibLfoToPitch, value)); + } + else + // 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) + { + generators.push(new Generator(generatorTypes.modEnvToFilterFc, value)); + } + else + // scale tuning (key number to pitch) + if (source === DLSSources.keyNum && destination === DLSDestinations.pitch) + { + // this is just a soundfont generator, but the amount must be changed + // 12,800 means the regular scale (100) + generators.push(new Generator(generatorTypes.scaleTuning, value / 128)); + } + else + // key to vol env hold + if (source === DLSSources.keyNum && destination === DLSDestinations.volEnvHold) + { + applyKeyToCorrection(value, generatorTypes.keyNumToVolEnvHold, generatorTypes.holdVolEnv); + } + else + // key to vol env decay + if (source === DLSSources.keyNum && destination === DLSDestinations.volEnvDecay) + { + applyKeyToCorrection(value, generatorTypes.keyNumToVolEnvDecay, generatorTypes.decayVolEnv); + } + else + // key to mod env hold + if (source === DLSSources.keyNum && destination === DLSDestinations.modEnvHold) + { + applyKeyToCorrection(value, generatorTypes.keyNumToModEnvHold, generatorTypes.holdModEnv); + } + else + // key to mod env decay + if (source === DLSSources.keyNum && destination === DLSDestinations.modEnvDecay) + { + applyKeyToCorrection(value, generatorTypes.keyNumToModEnvDecay, generatorTypes.decayModEnv); + } + else + { + isGenerator = false; + } + + } + else + { + isGenerator = false; + } + if (isGenerator === false) + { + // UNCOMMENT TO ENABLE DEBUG + // modulatorConverterDebug(source, control, destination, value, transform) + // convert it to modulator + const mod = getSF2ModulatorFromArticulator( + source, + control, + destination, + transform, + value + ); + 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); + } + else + { + SpessaSynthWarn("Failed converting to SF2 Modulator!"); + } + } + } + } + + // it seems that dls 1 does not have vibrato lfo, so we shall disable it + if (disableVibrato) + { + modulators.push( + // mod to vib + Modulator.copy(DLS_1_NO_VIBRATO_MOD), + // press to vib + Modulator.copy(DLS_1_NO_VIBRATO_PRESSURE) + ); + } + + return { modulators: modulators, generators: generators }; +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/dls/read_instrument.js b/spessasynth_lib/soundfont/dls/read_instrument.js new file mode 100644 index 0000000000000000000000000000000000000000..6521cfc06250183e5e50e1b23a05cdd46349d107 --- /dev/null +++ b/spessasynth_lib/soundfont/dls/read_instrument.js @@ -0,0 +1,121 @@ +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 { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd } from "../../utils/loggin.js"; +import { BasicInstrumentZone } from "../basic_soundfont/basic_zones.js"; +import { consoleColors } from "../../utils/other.js"; +import { generatorLimits, generatorTypes } from "../basic_soundfont/generator.js"; +import { Modulator } from "../basic_soundfont/modulator.js"; +import { DEFAULT_DLS_CHORUS, DEFAULT_DLS_REVERB } from "./dls_sources.js"; + +/** + * @this {DLSSoundFont} + * @param chunk {RiffChunk} + */ +export function readDLSInstrument(chunk) +{ + this.verifyHeader(chunk, "LIST"); + this.verifyText(readBytesAsString(chunk.chunkData, 4), "ins "); + /** + * @type {RiffChunk[]} + */ + const chunks = []; + while (chunk.chunkData.length > chunk.chunkData.currentIndex) + { + chunks.push(readRIFFChunk(chunk.chunkData)); + } + + + const instrumentHeader = chunks.find(c => c.header === "insh"); + 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(this, ulBank, ulInstrument); + + // read preset name in INFO + let presetName = "unnamedPreset"; + const infoChunk = findRIFFListType(chunks, "INFO"); + if (infoChunk) + { + let info = readRIFFChunk(infoChunk.chunkData); + while (info.header !== "INAM") + { + info = readRIFFChunk(infoChunk.chunkData); + } + presetName = readBytesAsString(info.chunkData, info.chunkData.length).trim(); + } + preset.presetName = presetName; + preset.DLSInstrument.instrumentName = presetName; + SpessaSynthGroupCollapsed( + `%cParsing %c"${presetName}"%c...`, + consoleColors.info, + consoleColors.recognized, + consoleColors.info + ); + + // list of regions + const regionListChunk = findRIFFListType(chunks, "lrgn"); + 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"); + if (globalLar2 !== undefined || globalLart !== undefined) + { + this.readLart(globalLart, globalLar2, globalZone); + } + // remove generators with default values + globalZone.generators = globalZone.generators.filter(g => g.generatorValue !== generatorLimits[g.generatorType].def); + // override reverb and chorus with 1000 instead of 200 (if not override) + // reverb + if (globalZone.modulators.find(m => m.modulatorDestination === generatorTypes.reverbEffectsSend) === undefined) + { + globalZone.modulators.push(Modulator.copy(DEFAULT_DLS_REVERB)); + } + // chorus + if (globalZone.modulators.find(m => m.modulatorDestination === generatorTypes.chorusEffectsSend) === undefined) + { + globalZone.modulators.push(Modulator.copy(DEFAULT_DLS_CHORUS)); + } + 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") + { + SpessaSynthGroupEnd(); + this.parsingError(`Invalid DLS region! Expected "rgn " or "rgn2" got "${type}"`); + } + + + const zone = this.readRegion(chunk); + if (zone) + { + preset.DLSInstrument.instrumentZones.push(zone); + } + } + + this.presets.push(preset); + this.instruments.push(preset.DLSInstrument); + SpessaSynthGroupEnd(); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/dls/read_instrument_list.js b/spessasynth_lib/soundfont/dls/read_instrument_list.js new file mode 100644 index 0000000000000000000000000000000000000000..a0f8ef3d73e4dba3e1f08135af68911d22fd8412 --- /dev/null +++ b/spessasynth_lib/soundfont/dls/read_instrument_list.js @@ -0,0 +1,17 @@ +import { readRIFFChunk } from "../basic_soundfont/riff_chunk.js"; +import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd } from "../../utils/loggin.js"; +import { consoleColors } from "../../utils/other.js"; + +/** + * @this {DLSSoundFont} + * @param instrumentListChunk {RiffChunk} + */ +export function readDLSInstrumentList(instrumentListChunk) +{ + SpessaSynthGroupCollapsed("%cLoading instruments...", consoleColors.info); + for (let i = 0; i < this.instrumentAmount; i++) + { + this.readDLSInstrument(readRIFFChunk(instrumentListChunk.chunkData)); + } + SpessaSynthGroupEnd(); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/dls/read_lart.js b/spessasynth_lib/soundfont/dls/read_lart.js new file mode 100644 index 0000000000000000000000000000000000000000..d9036f42c48b220bbca18b1c77aa8a66ddfdcea6 --- /dev/null +++ b/spessasynth_lib/soundfont/dls/read_lart.js @@ -0,0 +1,35 @@ +import { readRIFFChunk } from "../basic_soundfont/riff_chunk.js"; +import { readArticulation } from "./read_articulation.js"; + +/** + * @param lartChunk {RiffChunk|undefined} + * @param lar2Chunk {RiffChunk|undefined} + * @param zone {BasicInstrumentZone} + * @this {DLSSoundFont} + */ +export function readLart(lartChunk, lar2Chunk, zone) +{ + if (lartChunk) + { + while (lartChunk.chunkData.currentIndex < lartChunk.chunkData.length) + { + const art1 = readRIFFChunk(lartChunk.chunkData); + this.verifyHeader(art1, "art1", "art2"); + const modsAndGens = readArticulation(art1, true); + zone.generators.push(...modsAndGens.generators); + zone.modulators.push(...modsAndGens.modulators); + } + } + + if (lar2Chunk) + { + while (lar2Chunk.chunkData.currentIndex < lar2Chunk.chunkData.length) + { + const art2 = readRIFFChunk(lar2Chunk.chunkData); + this.verifyHeader(art2, "art2", "art1"); + const modsAndGens = readArticulation(art2, false); + zone.generators.push(...modsAndGens.generators); + zone.modulators.push(...modsAndGens.modulators); + } + } +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/dls/read_region.js b/spessasynth_lib/soundfont/dls/read_region.js new file mode 100644 index 0000000000000000000000000000000000000000..7b5af83d113f229f0fc90a0ee9931a1b85063757 --- /dev/null +++ b/spessasynth_lib/soundfont/dls/read_region.js @@ -0,0 +1,152 @@ +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"; + +/** + * @this {DLSSoundFont} + * @param chunk {RiffChunk} + * @returns {DLSZone} + */ +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) + { + regionChunks.push(readRIFFChunk(chunk.chunkData)); + } + + // region header + const regionHeader = regionChunks.find(c => c.header === "rgnh"); + // key range + let keyMin = readLittleEndian(regionHeader.chunkData, 2); + let keyMax = readLittleEndian(regionHeader.chunkData, 2); + // vel range + let velMin = readLittleEndian(regionHeader.chunkData, 2); + let velMax = readLittleEndian(regionHeader.chunkData, 2); + + // a fix for not cool files + if (velMin === 0 && velMax === 0) + { + velMax = 127; + velMin = 0; + } + // cannot do the same to key zones sadly + + const zone = new DLSZone( + { 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) + { + zone.generators.push(new Generator(generatorTypes.exclusiveClass, exclusive)); + } + + // lart + const lart = findRIFFListType(regionChunks, "lart"); + const lar2 = findRIFFListType(regionChunks, "lar2"); + this.readLart(lart, lar2, zone); + + // wsmp: wave sample chunk + zone.isGlobal = false; + const waveSampleChunk = regionChunks.find(c => c.header === "wsmp"); + // cbSize + readLittleEndian(waveSampleChunk.chunkData, 4); + let originalKey = readLittleEndian(waveSampleChunk.chunkData, 2); + + // sFineTune + let 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) + { + // no loop + loopingMode = 0; + } + else + { + // ignore cbSize + 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) + { + loopingMode = 1; + } + else + { + loopingMode = 3; + } + loop.start = readLittleEndian(waveSampleChunk.chunkData, 4); + const loopLength = readLittleEndian(waveSampleChunk.chunkData, 4); + loop.end = loop.start + loopLength; + } + + // wave link + const waveLinkChunk = regionChunks.find(c => c.header === "wlnk"); + if (waveLinkChunk === undefined) + { + // No wave link means no sample. What? Why is it even here then? + return undefined; + } + + // flags + readLittleEndian(waveLinkChunk.chunkData, 2); + // phase group + readLittleEndian(waveLinkChunk.chunkData, 2); + // channel + readLittleEndian(waveLinkChunk.chunkData, 4); + // sampleID + const sampleID = readLittleEndian(waveLinkChunk.chunkData, 4); + // noinspection JSValidateTypes + /** + * @type {DLSSample} + */ + const sample = this.samples[sampleID]; + 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 + ); + return zone; +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/dls/read_samples.js b/spessasynth_lib/soundfont/dls/read_samples.js new file mode 100644 index 0000000000000000000000000000000000000000..a8ba5ede1756dd589bc2f991d42494c230813941 --- /dev/null +++ b/spessasynth_lib/soundfont/dls/read_samples.js @@ -0,0 +1,270 @@ +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"; + +const W_FORMAT_TAG = { + PCM: 0x01, + ALAW: 0x6 +}; + +/** + * @param dataChunk {RiffChunk} + * @param bytesPerSample {number} + * @returns {Float32Array} + */ +function readPCM(dataChunk, bytesPerSample) +{ + 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 (bytesPerSample === 1) + { + normalizationFactor = 255; // For 8-bit normalize from 0-255 + isUnsigned = true; + } + else + { + normalizationFactor = maxSampleValue; // For 16-bit normalize from -32,768 to 32,767 + } + const sampleLength = dataChunk.size / bytesPerSample; + const sampleData = new Float32Array(sampleLength); + for (let i = 0; i < sampleData.length; i++) + { + // read + let sample = readLittleEndian(dataChunk.chunkData, bytesPerSample); + // turn into signed + if (isUnsigned) + { + // normalize unsigned 8-bit sample + sampleData[i] = (sample / normalizationFactor) - 0.5; + } + else + { + // normalize signed 16-bit sample + if (sample >= maxSampleValue) + { + sample -= maxUnsigned; + } + sampleData[i] = sample / normalizationFactor; + } + } + return sampleData; +} + +/** + * @param dataChunk {RiffChunk} + * @param bytesPerSample {number} + * @returns {Float32Array} + */ +function readALAW(dataChunk, bytesPerSample) +{ + const sampleLength = dataChunk.size / bytesPerSample; + const sampleData = new Float32Array(sampleLength); + for (let i = 0; i < sampleData.length; i++) + { + // read + const input = readLittleEndian(dataChunk.chunkData, bytesPerSample); + + // https://en.wikipedia.org/wiki/G.711#A-law + // re-toggle toggled bits + let sample = input ^ 0x55; + + // remove sign bit + sample &= 0x7F; + + // extract exponent + let exponent = sample >> 4; + // extract mantissa + let mantissa = sample & 0xF; + if (exponent > 0) + { + mantissa += 16; // add leading '1', if exponent > 0 + } + + mantissa = (mantissa << 4) + 0x8; + if (exponent > 1) + { + mantissa = mantissa << (exponent - 1); + } + + const s16sample = input > 127 ? mantissa : -mantissa; + + // convert to float + sampleData[i] = s16sample / 32678; + } + return sampleData; +} + +/** + * @this {DLSSoundFont} + * @param waveListChunk {RiffChunk} + */ +export function readDLSSamples(waveListChunk) +{ + SpessaSynthGroupCollapsed( + "%cLoading Wave samples...", + consoleColors.recognized + ); + let sampleID = 0; + 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) + { + waveChunks.push(readRIFFChunk(waveChunk.chunkData)); + } + + const fmtChunk = waveChunks.find(c => c.header === "fmt "); + if (!fmtChunk) + { + throw new Error("No fmt chunk in the wave file!"); + } + // https://github.com/tpn/winsdk-10/blob/9b69fd26ac0c7d0b83d378dba01080e93349c2ed/Include/10.0.14393.0/shared/mmreg.h#L2108 + const waveFormat = readLittleEndian(fmtChunk.chunkData, 2); + const channelsAmount = readLittleEndian(fmtChunk.chunkData, 2); + if (channelsAmount !== 1) + { + throw new Error(`Only mono samples are supported. Fmt reports ${channelsAmount} channels`); + } + const sampleRate = readLittleEndian(fmtChunk.chunkData, 4); + // skip avg bytes + readLittleEndian(fmtChunk.chunkData, 4); + // blockAlign + readLittleEndian(fmtChunk.chunkData, 2); + // it's bits per sample because one channel + const wBitsPerSample = readLittleEndian(fmtChunk.chunkData, 2); + const bytesPerSample = wBitsPerSample / 8; + + // read the data + let failed = false; + const dataChunk = waveChunks.find(c => c.header === "data"); + if (!dataChunk) + { + this.parsingError("No data chunk in the WAVE chunk!"); + } + let sampleData; + switch (waveFormat) + { + default: + failed = true; + sampleData = new Float32Array(dataChunk.size / bytesPerSample); + break; + + case W_FORMAT_TAG.PCM: + sampleData = readPCM(dataChunk, bytesPerSample); + break; + + case W_FORMAT_TAG.ALAW: + sampleData = readALAW(dataChunk, bytesPerSample); + break; + + } + + // read sample name + const waveInfo = findRIFFListType(waveChunks, "INFO"); + let sampleName = `Unnamed ${sampleID}`; + if (waveInfo) + { + let infoChunk = readRIFFChunk(waveInfo.chunkData); + while (infoChunk.header !== "INAM" && waveInfo.chunkData.currentIndex < waveInfo.chunkData.length) + { + infoChunk = readRIFFChunk(waveInfo.chunkData); + } + if (infoChunk.header === "INAM") + { + sampleName = readBytesAsString(infoChunk.chunkData, infoChunk.size).trim(); + } + } + + // correct 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) + { + // skip cbsize + readLittleEndian(wsmpChunk.chunkData, 4); + sampleKey = readLittleEndian(wsmpChunk.chunkData, 2); + // section 1.14.2: Each relative pitch unit represents 1/65536 cents. + // but that doesn't seem true for this one: it's just cents. + samplePitch = signedInt16( + wsmpChunk.chunkData[wsmpChunk.chunkData.currentIndex++], + wsmpChunk.chunkData[wsmpChunk.chunkData.currentIndex++] + ); + + // pitch correction: convert hundreds to the root key + const samplePitchSemitones = Math.trunc(samplePitch / 100); + sampleKey += samplePitchSemitones; + samplePitch -= samplePitchSemitones * 100; + + + // gain is applied it manually here (literally multiplying the samples) + const gainCorrection = readLittleEndian(wsmpChunk.chunkData, 4); + // convert to signed and turn into decibels + sampleDbAttenuation = (gainCorrection | 0) / -655360; + // no idea about ful options + readLittleEndian(wsmpChunk.chunkData, 4); + const loopsAmount = readLittleEndian(wsmpChunk.chunkData, 4); + if (loopsAmount === 1) + { + // skip size and type + readLittleEndian(wsmpChunk.chunkData, 8); + sampleLoopStart = readLittleEndian(wsmpChunk.chunkData, 4); + const loopSize = readLittleEndian(wsmpChunk.chunkData, 4); + sampleLoopEnd = sampleLoopStart + loopSize; + } + } + else + { + SpessaSynthWarn("No wsmp chunk in wave... using sane defaults."); + } + + if (failed) + { + console.error(`Failed to load '${sampleName}': Unsupported format: (${waveFormat})`); + } + + this.samples.push(new DLSSample( + sampleName, + sampleRate, + sampleKey, + samplePitch, + sampleLoopStart, + sampleLoopEnd, + sampleData, + sampleDbAttenuation + )); + + + sampleID++; + SpessaSynthInfo( + `%cLoaded sample %c${sampleName}`, + consoleColors.info, + consoleColors.recognized + ); + } + SpessaSynthGroupEnd(); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/load_soundfont.js b/spessasynth_lib/soundfont/load_soundfont.js new file mode 100644 index 0000000000000000000000000000000000000000..27840d94f7e8e728988fb8c237bf8cc53f73636b --- /dev/null +++ b/spessasynth_lib/soundfont/load_soundfont.js @@ -0,0 +1,21 @@ +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 + * @param buffer {ArrayBuffer} + * @returns {BasicSoundBank} + */ +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 ") + { + return new DLSSoundFont(buffer); + } + return new SoundFont2(buffer, false); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/read_sf2/generators.js b/spessasynth_lib/soundfont/read_sf2/generators.js new file mode 100644 index 0000000000000000000000000000000000000000..050d9424b1a7d6227b056306c3d1f043200eff81 --- /dev/null +++ b/spessasynth_lib/soundfont/read_sf2/generators.js @@ -0,0 +1,46 @@ +import { IndexedByteArray } from "../../utils/indexed_array.js"; +import { RiffChunk } from "../basic_soundfont/riff_chunk.js"; +import { signedInt16 } from "../../utils/byte_functions/little_endian.js"; +import { Generator } from "../basic_soundfont/generator.js"; + + +export class ReadGenerator extends Generator +{ + /** + * Creates a generator + * @param dataArray {IndexedByteArray} + */ + constructor(dataArray) + { + super(); + // 4 bytes: + // type, type, type, value + const i = dataArray.currentIndex; + /** + * @type {generatorTypes|number} + */ + this.generatorType = (dataArray[i + 1] << 8) | dataArray[i]; + this.generatorValue = signedInt16(dataArray[i + 2], dataArray[i + 3]); + dataArray.currentIndex += 4; + } +} + +/** + * Reads the generator read + * @param generatorChunk {RiffChunk} + * @returns {Generator[]} + */ +export function readGenerators(generatorChunk) +{ + let gens = []; + while (generatorChunk.chunkData.length > generatorChunk.chunkData.currentIndex) + { + gens.push(new ReadGenerator(generatorChunk.chunkData)); + } + if (gens.length > 1) + { + // remove terminal + gens.pop(); + } + return gens; +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/read_sf2/instruments.js b/spessasynth_lib/soundfont/read_sf2/instruments.js new file mode 100644 index 0000000000000000000000000000000000000000..258be8e655f0bc44bd33cadb639f092f36b8e58b --- /dev/null +++ b/spessasynth_lib/soundfont/read_sf2/instruments.js @@ -0,0 +1,66 @@ +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 + * purpose: parses soundfont instrument and stores them as a class + */ + +export class Instrument extends BasicInstrument +{ + /** + * Creates an instrument + * @param instrumentChunk {RiffChunk} + */ + constructor(instrumentChunk) + { + super(); + this.instrumentName = readBytesAsString(instrumentChunk.chunkData, 20).trim(); + this.instrumentZoneIndex = readLittleEndian(instrumentChunk.chunkData, 2); + this.instrumentZonesAmount = 0; + } + + /** + * Loads all the instrument zones, given the amount + * @param amount {number} + * @param zones {InstrumentZone[]} + */ + getInstrumentZones(amount, zones) + { + this.instrumentZonesAmount = amount; + for (let i = this.instrumentZoneIndex; i < this.instrumentZonesAmount + this.instrumentZoneIndex; i++) + { + this.instrumentZones.push(zones[i]); + } + } +} + +/** + * Reads the instruments + * @param instrumentChunk {RiffChunk} + * @param instrumentZones {InstrumentZone[]} + * @returns {Instrument[]} + */ +export function readInstruments(instrumentChunk, instrumentZones) +{ + let instruments = []; + while (instrumentChunk.chunkData.length > instrumentChunk.chunkData.currentIndex) + { + let instrument = new Instrument(instrumentChunk); + 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) + { + // remove EOI + instruments.pop(); + } + return instruments; +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/read_sf2/modulators.js b/spessasynth_lib/soundfont/read_sf2/modulators.js new file mode 100644 index 0000000000000000000000000000000000000000..3883463b60a7b0aed9e7693fb309acf202f68eaf --- /dev/null +++ b/spessasynth_lib/soundfont/read_sf2/modulators.js @@ -0,0 +1,36 @@ +import { readLittleEndian, signedInt16 } from "../../utils/byte_functions/little_endian.js"; +import { IndexedByteArray } from "../../utils/indexed_array.js"; +import { Modulator } from "../basic_soundfont/modulator.js"; + + +export class ReadModulator extends Modulator +{ + /** + * Creates a modulator + * @param dataArray {IndexedByteArray} + */ + constructor(dataArray) + { + const srcEnum = readLittleEndian(dataArray, 2); + const destination = readLittleEndian(dataArray, 2); + const amount = signedInt16(dataArray[dataArray.currentIndex++], dataArray[dataArray.currentIndex++]); + const secSrcEnum = readLittleEndian(dataArray, 2); + const transformType = readLittleEndian(dataArray, 2); + super(srcEnum, secSrcEnum, destination, amount, transformType); + } +} + +/** + * Reads the modulator read + * @param modulatorChunk {RiffChunk} + * @returns {Modulator[]} + */ +export function readModulators(modulatorChunk) +{ + let gens = []; + while (modulatorChunk.chunkData.length > modulatorChunk.chunkData.currentIndex) + { + gens.push(new ReadModulator(modulatorChunk.chunkData)); + } + return gens; +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/read_sf2/presets.js b/spessasynth_lib/soundfont/read_sf2/presets.js new file mode 100644 index 0000000000000000000000000000000000000000..3dd80c8fd367f8bd01ddbdb96c7dd635df551f6b --- /dev/null +++ b/spessasynth_lib/soundfont/read_sf2/presets.js @@ -0,0 +1,80 @@ +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 + */ + +export class Preset extends BasicPreset +{ + /** + * Creates a preset + * @param presetChunk {RiffChunk} + * @param sf2 {BasicSoundBank} + */ + constructor(presetChunk, sf2) + { + super(sf2); + 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 dword + 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} + * @param zones {PresetZone[]} + */ + getPresetZones(amount, zones) + { + this.presetZonesAmount = amount; + for (let i = this.presetZoneStartIndex; i < this.presetZonesAmount + this.presetZoneStartIndex; i++) + { + this.presetZones.push(zones[i]); + } + } +} + +/** + * Reads the presets + * @param presetChunk {RiffChunk} + * @param presetZones {PresetZone[]} + * @param sf2 {BasicSoundBank} + * @returns {Preset[]} + */ +export function readPresets(presetChunk, presetZones, sf2) +{ + /** + * @type {Preset[]} + */ + let presets = []; + while (presetChunk.chunkData.length > presetChunk.chunkData.currentIndex) + { + let preset = new Preset(presetChunk, sf2); + if (presets.length > 0) + { + let presetZonesAmount = preset.presetZoneStartIndex - presets[presets.length - 1].presetZoneStartIndex; + presets[presets.length - 1].getPresetZones(presetZonesAmount, presetZones); + } + presets.push(preset); + } + // remove EOP + if (presets.length > 1) + { + presets.pop(); + } + return presets; +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/read_sf2/samples.js b/spessasynth_lib/soundfont/read_sf2/samples.js new file mode 100644 index 0000000000000000000000000000000000000000..87eb49974392f9792d5173c3f0751d61f5797620 --- /dev/null +++ b/spessasynth_lib/soundfont/read_sf2/samples.js @@ -0,0 +1,304 @@ +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 SoundFontSample extends BasicSample +{ + /** + * Creates a sample + * @param sampleName {string} + * @param sampleStartIndex {number} + * @param sampleEndIndex {number} + * @param sampleLoopStartIndex {number} + * @param sampleLoopEndIndex {number} + * @param sampleRate {number} + * @param samplePitch {number} + * @param samplePitchCorrection {number} + * @param sampleLink {number} + * @param sampleType {number} + * @param smplArr {IndexedByteArray|Float32Array} + * @param sampleIndex {number} initial sample index when loading the sfont + * @param isDataRaw {boolean} if false, the data is decoded as float32. + * Used for SF2Pack support + */ + constructor( + sampleName, + sampleStartIndex, + sampleEndIndex, + sampleLoopStartIndex, + sampleLoopEndIndex, + sampleRate, + samplePitch, + samplePitchCorrection, + sampleLink, + sampleType, + smplArr, + sampleIndex, + isDataRaw + ) + { + super( + sampleName, + sampleRate, + samplePitch, + samplePitchCorrection, + sampleLink, + sampleType, + sampleLoopStartIndex - (sampleStartIndex / 2), + sampleLoopEndIndex - (sampleStartIndex / 2) + ); + this.sampleName = sampleName; + // in bytes + this.sampleStartIndex = sampleStartIndex; + this.sampleEndIndex = sampleEndIndex; + this.isSampleLoaded = false; + this.sampleID = sampleIndex; + // in bytes + this.sampleLength = this.sampleEndIndex - this.sampleStartIndex; + this.sampleDataArray = smplArr; + this.sampleData = new Float32Array(0); + if (this.isCompressed) + { + // correct loop points + this.sampleLoopStartIndex += this.sampleStartIndex / 2; + this.sampleLoopEndIndex += this.sampleStartIndex / 2; + this.sampleLength = 99999999; // set to 999,999 before we decode it + } + this.isDataRaw = isDataRaw; + } + + /** + * Get raw data, whether it's compressed or not as we simply write it to the file + * @return {Uint8Array} either s16 or vorbis data + */ + getRawData() + { + const smplArr = this.sampleDataArray; + if (this.isCompressed) + { + if (this.compressedData) + { + return this.compressedData; + } + const smplStart = smplArr.currentIndex; + return smplArr.slice(this.sampleStartIndex / 2 + smplStart, this.sampleEndIndex / 2 + smplStart); + } + else + { + if (!this.isDataRaw) + { + // encode the f32 into s16 manually + super.getRawData(); + } + const dataStartIndex = smplArr.currentIndex; + return smplArr.slice(dataStartIndex + this.sampleStartIndex, dataStartIndex + this.sampleEndIndex); + } + } + + /** + * Decode binary vorbis into a float32 pcm + */ + decodeVorbis() + { + if (this.sampleLength < 1) + { + // eos, do not do anything + return; + } + // get the compressed byte stream + const smplArr = this.sampleDataArray; + const smplStart = smplArr.currentIndex; + const buff = smplArr.slice(this.sampleStartIndex / 2 + smplStart, this.sampleEndIndex / 2 + smplStart); + // reset array and being decoding + this.sampleData = new Float32Array(0); + try + { + /** + * @type {{data: Float32Array[], error: (string|null), sampleRate: number, eof: boolean}} + */ + const vorbis = stbvorbis.decode(buff.buffer); + this.sampleData = vorbis.data[0]; + if (this.sampleData === undefined) + { + SpessaSynthWarn(`Error decoding sample ${this.sampleName}: Vorbis decode returned undefined.`); + } + } + catch (e) + { + // do not error out, fill with silence + SpessaSynthWarn(`Error decoding sample ${this.sampleName}: ${e}`); + this.sampleData = new Float32Array(this.sampleLoopEndIndex + 1); + } + } + + /** + * Loads the audio data and stores it for reuse + * @returns {Float32Array} The audioData + */ + getAudioData() + { + if (!this.isSampleLoaded) + { + // start loading data if it is not loaded + if (this.sampleLength < 1) + { + SpessaSynthWarn(`Invalid sample ${this.sampleName}! Invalid length: ${this.sampleLength}`); + return new Float32Array(1); + } + + if (this.isCompressed) + { + // if compressed, decode + this.decodeVorbis(); + this.isSampleLoaded = true; + return this.sampleData; + } + else if (!this.isDataRaw) + { + return this.getUncompressedReadyData(); + } + return this.loadUncompressedData(); + } + return this.sampleData; + } + + /** + * @returns {Float32Array} + */ + loadUncompressedData() + { + 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; + let convertedSigned16 = new Int16Array( + this.sampleDataArray.slice(dataStartIndex + this.sampleStartIndex, dataStartIndex + this.sampleEndIndex) + .buffer + ); + + // convert to float + for (let i = 0; i < convertedSigned16.length; i++) + { + audioData[i] = convertedSigned16[i] / 32768; + } + + this.sampleData = audioData; + this.isSampleLoaded = true; + return audioData; + } + + /** + * @returns {Float32Array} + */ + getUncompressedReadyData() + { + /** + * read the sample data + * @type {Float32Array} + */ + let audioData = this.sampleDataArray.slice(this.sampleStartIndex / 2, this.sampleEndIndex / 2); + this.sampleData = audioData; + this.isSampleLoaded = true; + return audioData; + } +} + +/** + * Reads the generatorTranslator from the shdr read + * @param sampleHeadersChunk {RiffChunk} + * @param smplChunkData {IndexedByteArray|Float32Array} + * @param isSmplDataRaw {boolean} + * @returns {SoundFontSample[]} + */ +export function readSamples(sampleHeadersChunk, smplChunkData, isSmplDataRaw = true) +{ + /** + * @type {SoundFontSample[]} + */ + let samples = []; + let index = 0; + while (sampleHeadersChunk.chunkData.length > sampleHeadersChunk.chunkData.currentIndex) + { + const sample = readSample(index, sampleHeadersChunk.chunkData, smplChunkData, isSmplDataRaw); + samples.push(sample); + index++; + } + // remove EOS + if (samples.length > 1) + { + samples.pop(); + } + return samples; +} + +/** + * Reads it into a sample + * @param index {number} + * @param sampleHeaderData {IndexedByteArray} + * @param smplArrayData {IndexedByteArray|Float32Array} + * @param isDataRaw {boolean} true means binary 16-bit data, false means float32 + * @returns {SoundFontSample} + */ +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 it's 255, then default to 60 + samplePitch = 60; + } + + // read 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 SoundFontSample( + sampleName, + sampleStartIndex, + sampleEndIndex, + sampleLoopStartIndex, + sampleLoopEndIndex, + sampleRate, + samplePitch, + samplePitchCorrection, + sampleLink, + sampleType, + smplArrayData, + index, + isDataRaw + ); +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/read_sf2/soundfont.js b/spessasynth_lib/soundfont/read_sf2/soundfont.js new file mode 100644 index 0000000000000000000000000000000000000000..e27f6828067993ced4e95efb3b45dd8eb6b54c3c --- /dev/null +++ b/spessasynth_lib/soundfont/read_sf2/soundfont.js @@ -0,0 +1,305 @@ +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 { 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 { stbvorbis } from "../../externals/stbvorbis_sync/stbvorbis_sync.min.js"; +import { BasicSoundBank } from "../basic_soundfont/basic_soundfont.js"; +import { Generator } from "../basic_soundfont/generator.js"; +import { Modulator } from "../basic_soundfont/modulator.js"; + +/** + * soundfont.js + * purpose: parses a soundfont2 file + */ + +export class SoundFont2 extends BasicSoundBank +{ + /** + * Initializes a new SoundFont2 Parser and parses the given data array + * @param arrayBuffer {ArrayBuffer} + * @param warnDeprecated {boolean} + */ + constructor(arrayBuffer, warnDeprecated = true) + { + super(); + 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) + { + SpessaSynthGroupEnd(); + this.parsingError("No data provided!"); + } + + // 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") + { + SpessaSynthGroupEnd(); + throw new SyntaxError(`Invalid soundFont! Expected "sfbk" or "sfpk" got "${type}"`); + } + /* + Some SF2Pack description: + this is essentially sf2, but the entire smpl chunk is compressed (we only support Ogg Vorbis here) + 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) + { + let chunk = readRIFFChunk(infoChunk.chunkData); + let text; + // special cases + switch (chunk.header.toLowerCase()) + { + case "ifil": + case "iver": + 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); + newModulators.pop(); // remove the terminal record + 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.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}"`, + consoleColors.info, + consoleColors.recognized + ); + } + + // SDTA + const sdtaChunk = readRIFFChunk(this.dataArray, false); + this.verifyHeader(sdtaChunk, "list"); + this.verifyText(readBytesAsString(this.dataArray, 4), "sdta"); + + // smpl + SpessaSynthInfo("%cVerifying smpl chunk...", consoleColors.warn); + let sampleDataChunk = readRIFFChunk(this.dataArray, false); + this.verifyHeader(sampleDataChunk, "smpl"); + /** + * @type {IndexedByteArray|Float32Array} + */ + let sampleData; + // SF2Pack: the entire data is compressed + if (isSF2Pack) + { + 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) + { + SpessaSynthGroupEnd(); + throw new Error(`SF2Pack Ogg Vorbis decode error: ${e}`); + } + SpessaSynthInfo( + `%cDecoded the smpl chunk! Length: %c${sampleData.length}`, + consoleColors.info, + consoleColors.value + ); + } + else + { + /** + * @type {IndexedByteArray} + */ + sampleData = this.dataArray; + this.sampleDataStartIndex = this.dataArray.currentIndex; + } + + SpessaSynthInfo( + `%cSkipping sample chunk, length: %c${sdtaChunk.size - 12}`, + consoleColors.info, + consoleColors.value + ); + this.dataArray.currentIndex += sdtaChunk.size - 12; + + // PDTA + 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, + instrumentGenerators, + instrumentModulators, + 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)); + this.presets.sort((a, b) => (a.program - b.program) + (a.bank - b.bank)); + this._parseInternal(); + 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, + consoleColors.info, + consoleColors.recognized, + consoleColors.info, + consoleColors.recognized, + consoleColors.info, + consoleColors.recognized, + consoleColors.info + ); + SpessaSynthGroupEnd(); + + if (isSF2Pack) + { + delete this.dataArray; + } + } + + /** + * @param chunk {RiffChunk} + * @param expected {string} + */ + verifyHeader(chunk, expected) + { + if (chunk.header.toLowerCase() !== expected.toLowerCase()) + { + SpessaSynthGroupEnd(); + this.parsingError(`Invalid chunk header! Expected "${expected.toLowerCase()}" got "${chunk.header.toLowerCase()}"`); + } + } + + /** + * @param text {string} + * @param expected {string} + */ + verifyText(text, expected) + { + if (text.toLowerCase() !== expected.toLowerCase()) + { + SpessaSynthGroupEnd(); + this.parsingError(`Invalid FourCC: Expected "${expected.toLowerCase()}" got "${text.toLowerCase()}"\``); + } + } + + destroySoundBank() + { + super.destroySoundBank(); + delete this.dataArray; + } +} \ No newline at end of file diff --git a/spessasynth_lib/soundfont/read_sf2/zones.js b/spessasynth_lib/soundfont/read_sf2/zones.js new file mode 100644 index 0000000000000000000000000000000000000000..97fae0993c9da21ccf0a7c518e77d6d4699b6220 --- /dev/null +++ b/spessasynth_lib/soundfont/read_sf2/zones.js @@ -0,0 +1,263 @@ +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 { BasicInstrumentZone, BasicPresetZone } from "../basic_soundfont/basic_zones.js"; +import { Generator, generatorTypes } from "../basic_soundfont/generator.js"; +import { Modulator } from "../basic_soundfont/modulator.js"; + +/** + * zones.js + * purpose: reads instrumend and preset zones from soundfont and gets their respective samples and generators and modulators + */ + +export class InstrumentZone extends BasicInstrumentZone +{ + /** + * Creates a zone (instrument) + * @param dataArray {IndexedByteArray} + */ + constructor(dataArray) + { + super(); + this.generatorZoneStartIndex = readLittleEndian(dataArray, 2); + this.modulatorZoneStartIndex = readLittleEndian(dataArray, 2); + this.modulatorZoneSize = 0; + 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++) + { + this.generators.push(generators[i]); + } + } + + /** + * grab the modulators + * @param modulators {Modulator[]} + */ + getModulators(modulators) + { + for (let i = this.modulatorZoneStartIndex; i < this.modulatorZoneStartIndex + this.modulatorZoneSize; i++) + { + this.modulators.push(modulators[i]); + } + } + + /** + * Loads the zone's sample + * @param samples {BasicSample[]} + */ + getSample(samples) + { + let sampleID = this.generators.find(g => g.generatorType === generatorTypes.sampleID); + if (sampleID) + { + this.sample = samples[sampleID.generatorValue]; + this.isGlobal = false; + this.sample.useCount++; + } + } + + /** + * Reads the keyRange of the zone + */ + getKeyRange() + { + let range = this.generators.find(g => g.generatorType === generatorTypes.keyRange); + 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) + { + this.velRange.min = range.generatorValue & 0x7F; + this.velRange.max = (range.generatorValue >> 8) & 0x7F; + } + } +} + +/** + * Reads the given instrument zone read + * @param zonesChunk {RiffChunk} + * @param instrumentGenerators {Generator[]} + * @param instrumentModulators {Modulator[]} + * @param instrumentSamples {BasicSample[]} + * @returns {InstrumentZone[]} + */ +export function readInstrumentZones(zonesChunk, instrumentGenerators, instrumentModulators, instrumentSamples) +{ + /** + * @type {InstrumentZone[]} + */ + let zones = []; + while (zonesChunk.chunkData.length > zonesChunk.chunkData.currentIndex) + { + let zone = new InstrumentZone(zonesChunk.chunkData); + if (zones.length > 0) + { + let modulatorZoneSize = zone.modulatorZoneStartIndex - zones[zones.length - 1].modulatorZoneStartIndex; + let generatorZoneSize = zone.generatorZoneStartIndex - zones[zones.length - 1].generatorZoneStartIndex; + zones[zones.length - 1].setZoneSize(modulatorZoneSize, generatorZoneSize); + zones[zones.length - 1].getGenerators(instrumentGenerators); + zones[zones.length - 1].getModulators(instrumentModulators); + zones[zones.length - 1].getSample(instrumentSamples); + zones[zones.length - 1].getKeyRange(); + zones[zones.length - 1].getVelRange(); + } + zones.push(zone); + } + if (zones.length > 1) + { + // remove terminal + zones.pop(); + } + return zones; +} + +export class PresetZone extends BasicPresetZone +{ + /** + * Creates a zone (preset) + * @param dataArray {IndexedByteArray} + */ + constructor(dataArray) + { + super(); + this.generatorZoneStartIndex = readLittleEndian(dataArray, 2); + this.modulatorZoneStartIndex = readLittleEndian(dataArray, 2); + this.modulatorZoneSize = 0; + 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++) + { + this.generators.push(generators[i]); + } + } + + /** + * grab the modulators + * @param modulators {Modulator[]} + */ + getModulators(modulators) + { + for (let i = this.modulatorZoneStartIndex; i < this.modulatorZoneStartIndex + this.modulatorZoneSize; i++) + { + this.modulators.push(modulators[i]); + } + } + + /** + * grab the instrument + * @param instruments {BasicInstrument[]} + */ + getInstrument(instruments) + { + let instrumentID = this.generators.find(g => g.generatorType === generatorTypes.instrument); + 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) + { + 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) + { + this.velRange.min = range.generatorValue & 0x7F; + this.velRange.max = (range.generatorValue >> 8) & 0x7F; + } + } +} + +/** + * Reads the given preset zone read + * @param zonesChunk {RiffChunk} + * @param presetGenerators {Generator[]} + * @param instruments {BasicInstrument[]} + * @param presetModulators {Modulator[]} + * @returns {PresetZone[]} + */ +export function readPresetZones(zonesChunk, presetGenerators, presetModulators, instruments) +{ + /** + * @type {PresetZone[]} + */ + let zones = []; + while (zonesChunk.chunkData.length > zonesChunk.chunkData.currentIndex) + { + let zone = new PresetZone(zonesChunk.chunkData); + if (zones.length > 0) + { + let modulatorZoneSize = zone.modulatorZoneStartIndex - zones[zones.length - 1].modulatorZoneStartIndex; + let generatorZoneSize = zone.generatorZoneStartIndex - zones[zones.length - 1].generatorZoneStartIndex; + zones[zones.length - 1].setZoneSize(modulatorZoneSize, generatorZoneSize); + zones[zones.length - 1].getGenerators(presetGenerators); + zones[zones.length - 1].getModulators(presetModulators); + zones[zones.length - 1].getInstrument(instruments); + zones[zones.length - 1].getKeyRange(); + zones[zones.length - 1].getVelRange(); + } + zones.push(zone); + } + if (zones.length > 1) + { + // remove terminal + zones.pop(); + } + return zones; +} \ No newline at end of file diff --git a/spessasynth_lib/soundfonts/.gitignore b/spessasynth_lib/soundfonts/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..2fa8dbd13d6ae98bb591d22069e14346d65e0ebd --- /dev/null +++ b/spessasynth_lib/soundfonts/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!GeneralUserGS.sf3 \ No newline at end of file diff --git a/spessasynth_lib/soundfonts/GeneralUserGS.sf3 b/spessasynth_lib/soundfonts/GeneralUserGS.sf3 new file mode 100644 index 0000000000000000000000000000000000000000..43f3909530268eb24446e735034e77ac23a39f20 --- /dev/null +++ b/spessasynth_lib/soundfonts/GeneralUserGS.sf3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d7d2a5f9a48224181b354e63cab1619c3e2163ee879ab50d0a40700ede315eb +size 10580436 diff --git a/spessasynth_lib/synthetizer/README.md b/spessasynth_lib/synthetizer/README.md new file mode 100644 index 0000000000000000000000000000000000000000..b61fa013c008601f595b8002a39e4096ea7abdee --- /dev/null +++ b/spessasynth_lib/synthetizer/README.md @@ -0,0 +1,8 @@ +## This is the main synthesizer folder. + +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/spessasynth_lib/synthetizer/audio_effects/effects_config.js b/spessasynth_lib/synthetizer/audio_effects/effects_config.js new file mode 100644 index 0000000000000000000000000000000000000000..97fe756df385b6e98e443238f4ed6ab40ea0d267 --- /dev/null +++ b/spessasynth_lib/synthetizer/audio_effects/effects_config.js @@ -0,0 +1,25 @@ +import { DEFAULT_CHORUS_CONFIG } from "./fancy_chorus.js"; + +/** + * @typedef {Object} SynthConfig + * @property {boolean?} chorusEnabled - indicates if the chorus effect is enabled. + * @property {ChorusConfig?} chorusConfig - the configuration for chorus. Pass undefined to use defaults + * @property {boolean?} reverbEnabled - indicates if the reverb effect is enabled. + * @property {AudioBuffer?} reverbImpulseResponse - the impulse response for the reverb. Pass undefined to use defaults + * @property {{ + * worklet: function(context: object, name: string, options?: Object) + * }?} audioNodeCreators - custom audio node creation functions for Web Audio wrappers. + */ + + +/** + * @type {SynthConfig} + */ +export const DEFAULT_SYNTH_CONFIG = { + chorusEnabled: true, + chorusConfig: DEFAULT_CHORUS_CONFIG, + + reverbEnabled: true, + reverbImpulseResponse: undefined, // will load the integrated one + audioNodeCreators: undefined +}; \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/audio_effects/fancy_chorus.js b/spessasynth_lib/synthetizer/audio_effects/fancy_chorus.js new file mode 100644 index 0000000000000000000000000000000000000000..e26c51c293237354fe6d592e54bfd23c7e918ad0 --- /dev/null +++ b/spessasynth_lib/synthetizer/audio_effects/fancy_chorus.js @@ -0,0 +1,162 @@ +/** + * fancy_chorus.js + * purpose: creates a simple chorus effect node + */ + +/** + * @typedef {{ + * oscillator: OscillatorNode, + * oscillatorGain: GainNode, + * delay: DelayNode + * }} ChorusNode + */ + +/** + * @typedef {Object} ChorusConfig + * @property {number?} nodesAmount - the amount of delay nodes (for each channel) and the corresponding oscillators + * @property {number?} defaultDelay - the initial delay, in seconds + * @property {number?} delayVariation - the difference between delays in the delay nodes + * @property {number?} stereoDifference - the difference of delays between two channels (added to the left channel and subtracted from the right) + * @property {number?} oscillatorFrequency - the initial delay time oscillator frequency, in Hz. + * @property {number?} oscillatorFrequencyVariation - the difference between frequencies of oscillators, in Hz + * @property {number?} oscillatorGain - how much will oscillator alter the delay in delay nodes, in seconds + */ + +const NODES_AMOUNT = 4; +const DEFAULT_DELAY = 0.03; +const DELAY_VARIATION = 0.01; +const STEREO_DIFF = 0.02; + +const OSC_FREQ = 0.2; +const OSC_FREQ_VARIATION = 0.05; +const OSC_GAIN = 0.003; + +export const DEFAULT_CHORUS_CONFIG = { + nodesAmount: NODES_AMOUNT, + defaultDelay: DEFAULT_DELAY, + delayVariation: DELAY_VARIATION, + stereoDifference: STEREO_DIFF, + oscillatorFrequency: OSC_FREQ, + oscillatorFrequencyVariation: OSC_FREQ_VARIATION, + oscillatorGain: OSC_GAIN +}; + +export class FancyChorus +{ + /** + * Creates a fancy chorus effect + * @param output {AudioNode} + * @param config {ChorusConfig} + */ + constructor(output, config = DEFAULT_CHORUS_CONFIG) + { + const context = output.context; + + this.input = context.createChannelSplitter(2); + + const merger = context.createChannelMerger(2); + + /** + * @type {ChorusNode[]} + */ + const chorusNodesLeft = []; + /** + * @type {ChorusNode[]} + */ + const chorusNodesRight = []; + let freq = config.oscillatorFrequency; + let delay = config.defaultDelay; + for (let i = 0; i < config.nodesAmount; i++) + { + // left node + 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 + ); + freq += config.oscillatorFrequencyVariation; + delay += config.delayVariation; + } + + merger.connect(output); + this.merger = merger; + this.chorusLeft = chorusNodesLeft; + this.chorusRight = chorusNodesRight; + } + + delete() + { + this.input.disconnect(); + delete this.input; + this.merger.disconnect(); + delete this.merger; + for (const chorusLeftElement of this.chorusLeft) + { + chorusLeftElement.delay.disconnect(); + chorusLeftElement.oscillator.disconnect(); + chorusLeftElement.oscillatorGain.disconnect(); + delete chorusLeftElement.delay; + delete chorusLeftElement.oscillatorGain; + delete chorusLeftElement.oscillatorGain; + } + for (const chorusRightElement of this.chorusRight) + { + chorusRightElement.delay.disconnect(); + chorusRightElement.oscillator.disconnect(); + chorusRightElement.oscillatorGain.disconnect(); + delete chorusRightElement.delay; + delete chorusRightElement.oscillatorGain; + delete chorusRightElement.oscillatorGain; + } + } + + /** + * @param freq {number} + * @param delay {number} + * @param list {ChorusNode[]} + * @param input {number} + * @param output {AudioNode} + * @param outputNum {number} + * @param context {BaseAudioContext} + * @param config {ChorusConfig} + */ + createChorusNode(freq, delay, list, input, output, outputNum, context, config) + { + const oscillator = context.createOscillator(); + oscillator.type = "sine"; + oscillator.frequency.value = freq; + const gainNode = context.createGain(); + gainNode.gain.value = config.oscillatorGain; + const delayNode = context.createDelay(); + delayNode.delayTime.value = 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, + delay: delayNode + }); + } +} diff --git a/spessasynth_lib/synthetizer/audio_effects/rb_compressed.min.js b/spessasynth_lib/synthetizer/audio_effects/rb_compressed.min.js new file mode 100644 index 0000000000000000000000000000000000000000..74b63e437b0cb7680a6a09e9ffd7d631db28fea5 --- /dev/null +++ b/spessasynth_lib/synthetizer/audio_effects/rb_compressed.min.js @@ -0,0 +1 @@ +export const rbCompressed = `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` \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/audio_effects/reverb.js b/spessasynth_lib/synthetizer/audio_effects/reverb.js new file mode 100644 index 0000000000000000000000000000000000000000..f82bee642f9e0eabff20fe936a52efebe593b1ae --- /dev/null +++ b/spessasynth_lib/synthetizer/audio_effects/reverb.js @@ -0,0 +1,35 @@ +import { reverbBufferBinary } from "./reverb_as_binary.js"; + +/** + * Creates a reverb processor + * @param context {BaseAudioContext} + * @param reverbBuffer {AudioBuffer} + * @returns {{conv: ConvolverNode, promise: Promise}} + */ +export function getReverbProcessor(context, reverbBuffer = undefined) +{ + let solve; + /** + * @type {Promise} + */ + let promise = new Promise(r => solve = r); + const convolver = context.createConvolver(); + if (reverbBuffer) + { + convolver.buffer = reverbBuffer; + solve(); + } + else + { + // decode + promise = context.decodeAudioData(reverbBufferBinary.slice(0)); + promise.then(b => + { + convolver.buffer = b; + }); + } + return { + conv: convolver, + promise: promise + }; +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/audio_effects/reverb_as_binary.js b/spessasynth_lib/synthetizer/audio_effects/reverb_as_binary.js new file mode 100644 index 0000000000000000000000000000000000000000..7c134d1ba08208adc36d19fa9dc2252df2302d3d --- /dev/null +++ b/spessasynth_lib/synthetizer/audio_effects/reverb_as_binary.js @@ -0,0 +1,18 @@ +import { rbCompressed } from "./rb_compressed.min.js"; +import { inflateSync } from "../../externals/fflate/fflate.min.js"; + +// convert the base64 string to array buffer +const binaryString = atob(rbCompressed); +const binary = new Uint8Array(binaryString.length); +for (let i = 0; i < binaryString.length; i++) +{ + binary[i] = binaryString.charCodeAt(i); +} + + +/** + * the reverb is zlib compressed, decompress here + * @type {ArrayBuffer} + */ +const reverbBufferBinary = inflateSync(binary).buffer; +export { reverbBufferBinary }; \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/key_modifier_manager.js b/spessasynth_lib/synthetizer/key_modifier_manager.js new file mode 100644 index 0000000000000000000000000000000000000000..19a42b5be7447468c79549de5ddf3f41af7c1300 --- /dev/null +++ b/spessasynth_lib/synthetizer/key_modifier_manager.js @@ -0,0 +1,102 @@ +import { workletMessageType } from "./worklet_system/message_protocol/worklet_message.js"; +import { KeyModifier, workletKeyModifierMessageType } from "./worklet_system/worklet_methods/worklet_key_modifier.js"; + +export class KeyModifierManager +{ + /** + * @param synth {Synthetizer} + */ + constructor(synth) + { + this.synth = synth; + /** + * The velocity override mappings for MIDI keys + * @type {KeyModifier[][]} + * @private + */ + this._keyModifiers = []; + } + + /** + * @private + * @param type {workletKeyModifierMessageType} + * @param data {any} + */ + _sendToWorklet(type, data) + { + this.synth.post({ + messageType: workletMessageType.keyModifierManager, + messageData: [ + type, + data + ] + }); + } + + /** + * Modifies a single key + * @param channel {number} the channel affected. Usually 0-15 + * @param midiNote {number} the MIDI note to change. 0-127 + * @param options {{ + * velocity: number|undefined, + * patch: { + * bank: number, + * program: number + * }|undefined + * }} the key's modifiers + */ + addModifier(channel, midiNote, options) + { + const velocity = options?.velocity ?? -1; + const program = options?.patch?.program ?? -1; + const bank = options?.patch?.bank ?? -1; + const mod = new KeyModifier(velocity, bank, program); + if (this._keyModifiers[channel] === undefined) + { + this._keyModifiers[channel] = []; + } + this._keyModifiers[channel][midiNote] = mod; + this._sendToWorklet( + workletKeyModifierMessageType.addMapping, + [channel, midiNote, mod] + ); + } + + /** + * Gets a key modifier + * @param channel {number} the channel affected. Usually 0-15 + * @param midiNote {number} the MIDI note to change. 0-127 + * @returns {KeyModifier|undefined} + */ + getModifier(channel, midiNote) + { + return this._keyModifiers?.[channel]?.[midiNote]; + } + + /** + * Deletes a key modifier + * @param channel {number} the channel affected. Usually 0-15 + * @param midiNote {number} the MIDI note to change. 0-127 + */ + deleteModifier(channel, midiNote) + { + this._sendToWorklet( + workletKeyModifierMessageType.deleteMapping, + [channel, midiNote] + ); + if (this._keyModifiers[channel]?.[midiNote] === undefined) + { + return; + } + this._keyModifiers[channel][midiNote] = undefined; + } + + /** + * Clears ALL Modifiers + */ + clearModifiers() + { + this._sendToWorklet(workletKeyModifierMessageType.clearMappings, undefined); + this._keyModifiers = []; + } +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/synth_constants.js b/spessasynth_lib/synthetizer/synth_constants.js new file mode 100644 index 0000000000000000000000000000000000000000..862d8c14ce8e79d2192470ea90e083e33aa806f3 --- /dev/null +++ b/spessasynth_lib/synthetizer/synth_constants.js @@ -0,0 +1,15 @@ +/** + * @typedef {Object} StartRenderingDataConfig + * @property {BasicMIDI} parsedMIDI - the MIDI to render + * @property {SynthesizerSnapshot?} snapshot - the snapshot to apply + * @property {boolean?} oneOutput - if synth should use one output with 32 channels (2 audio channels for each midi channel). + * this disabled chorus and reverb. + * @property {number?} loopCount - the times to loop the song + * @property {SequencerOptions?} sequencerOptions - the options to pass to the sequencer + */ + +export const WORKLET_PROCESSOR_NAME = "spessasynth-worklet-system"; +export const VOICE_CAP = 350; +export const DEFAULT_PERCUSSION = 9; +export const MIDI_CHANNEL_COUNT = 16; +export const DEFAULT_SYNTH_MODE = "gs"; \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/synth_event_handler.js b/spessasynth_lib/synthetizer/synth_event_handler.js new file mode 100644 index 0000000000000000000000000000000000000000..380df00c6e1f33e9abbffe0885142313b7fe204e --- /dev/null +++ b/spessasynth_lib/synthetizer/synth_event_handler.js @@ -0,0 +1,214 @@ +/** + * synth_event_handler.js + * purpose: manages the synthesizer's event system, calling assinged functions when synthesizer requests dispatching the event + */ + +/** + * @typedef {Object} NoteOnCallback + * @property {number} midiNote - The MIDI note number. + * @property {number} channel - The MIDI channel number. + * @property {number} velocity - The velocity of the note. + */ + +/** + * @typedef {Object} NoteOffCallback + * @property {number} midiNote - The MIDI note number. + * @property {number} channel - The MIDI channel number. + */ + +/** + * @typedef {Object} DrumChangeCallback + * @property {number} channel - The MIDI channel number. + * @property {boolean} isDrumChannel - Indicates if the channel is a drum channel. + */ + +/** + * @typedef {Object} ProgramChangeCallback + * @property {number} channel - The MIDI channel number. + * @property {number} program - The program number. + * @property {number} bank - The bank number. + */ + +/** + * @typedef {Object} ControllerChangeCallback + * @property {number} channel - The MIDI channel number. + * @property {number} controllerNumber - The controller number. + * @property {number} controllerValue - The value of the controller. + */ + +/** + * @typedef {Object} MuteChannelCallback + * @property {number} channel - The MIDI channel number. + * @property {boolean} isMuted - Indicates if the channel is muted. + */ + +/** + * @typedef {Object} PresetListChangeCallbackSingle + * @property {string} presetName - The name of the preset. + * @property {number} bank - The bank number. + * @property {number} program - The program number. + */ + +/** + * @typedef {PresetListChangeCallbackSingle[]} PresetListChangeCallback - A list of preset objects. + */ + +/** + * @typedef {Object} SynthDisplayCallback + * @property {Uint8Array} displayData - The data to display. + * @property {SynthDisplayType} displayType - The type of display. + */ + +/** + * @typedef {Object} PitchWheelCallback + * @property {number} channel - The MIDI channel number. + * @property {number} MSB - The most significant byte of the pitch wheel value. + * @property {number} LSB - The least significant byte of the pitch wheel value. + */ + +/** + * @typedef {Object} ChannelPressureCallback + * @property {number} channel - The MIDI channel number. + * @property {number} pressure - The pressure value. + */ + +/** + * @typedef {Error} SoundfontErrorCallback - The error message for soundfont errors. + */ + +/** + * @typedef { + * NoteOnCallback | + * NoteOffCallback | + * DrumChangeCallback | + * ProgramChangeCallback | + * ControllerChangeCallback | + * MuteChannelCallback | + * PresetListChangeCallback | + * PitchWheelCallback | + * SoundfontErrorCallback | + * ChannelPressureCallback | + * SynthDisplayCallback | + * undefined + * } EventCallbackData + */ + +/** + * @typedef { + * "noteon"| + * "noteoff"| + * "pitchwheel"| + * "controllerchange"| + * "programchange"| + * "channelpressure"| + * "polypressure" | + * "drumchange"| + * "stopall"| + * "newchannel"| + * "mutechannel"| + * "presetlistchange"| + * "allcontrollerreset"| + * "soundfonterror"| + * "synthdisplay"} EventTypes + */ +export class EventHandler +{ + /** + * A new synthesizer event handler + */ + constructor() + { + /** + * The main list of events + * @type {Object>} + */ + this.events = { + "noteoff": {}, // called on note off message + "noteon": {}, // called on note on message + "pitchwheel": {}, // called on pitch wheel change + "controllerchange": {}, // called on controller change + "programchange": {}, // called on program change + "channelpressure": {}, // called on channel pressure message + "polypressure": {}, // called on poly pressure message + "drumchange": {}, // called when channel type changes + "stopall": {}, // called when synth receives stop all command + "newchannel": {}, // called when a new channel is created + "mutechannel": {}, // called when a channel is muted/unmuted + "presetlistchange": {}, // called when the preset list changes (soundfont gets reloaded) + "allcontrollerreset": {}, // called when all controllers are reset + "soundfonterror": {}, // called when a soundfont parsing error occurs + "synthdisplay": {} // called when there's a SysEx message to display some text + }; + + /** + * Set to 0 to disabled, otherwise in seconds + * @type {number} + */ + this.timeDelay = 0; + } + + /** + * Adds a new event listener + * @param name {EventTypes} + * @param id {string} the unique identifier for the event (to delete it + * @param callback {function(EventCallbackData)} + */ + addEvent(name, id, callback) + { + this.events[name][id] = callback; + } + + /** + * Removes an event listener + * @param name {EventTypes} + * @param id {string} + */ + removeEvent(name, id) + { + delete this.events[name][id]; + } + + /** + * Calls the given event + * @param name {EventTypes} + * @param eventData {EventCallbackData} + */ + callEvent(name, eventData) + { + if (this.events[name]) + { + if (this.timeDelay > 0) + { + setTimeout(() => + { + Object.values(this.events[name]).forEach(ev => + { + try + { + ev(eventData); + } + catch (e) + { + console.error(`Error while executing an event callback for ${name}:`, e); + } + }); + }, this.timeDelay * 1000); + } + else + { + Object.values(this.events[name]).forEach(ev => + { + try + { + ev(eventData); + } + catch (e) + { + console.error(`Error while executing an event callback for ${name}:`, e); + } + } + ); + } + } + } +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/synth_soundfont_manager.js b/spessasynth_lib/synthetizer/synth_soundfont_manager.js new file mode 100644 index 0000000000000000000000000000000000000000..00d5992e1009988c633c66201501056eccde2cb0 --- /dev/null +++ b/spessasynth_lib/synthetizer/synth_soundfont_manager.js @@ -0,0 +1,111 @@ +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"; + +export class SoundfontManager +{ + /** + * Creates a new instance of the soundfont manager + * @param synth {Synthetizer} + */ + constructor(synth) + { + /** + * The current list of soundfonts, in order from the most important to the least. + * @type {{ + * id: string, + * bankOffset: number + * }[]} + */ + this.soundfontList = [{ + id: "main", + bankOffset: 0 + }]; + + /** + * @type {MessagePort} + * @private + */ + this._port = synth.worklet.port; + this.synth = synth; + } + + /** + * @private + * @param type {WorkletSoundfontManagerMessageType} + * @param data {any} + */ + _sendToWorklet(type, data) + { + this._port.postMessage({ + messageType: workletMessageType.soundFontManager, + messageData: [ + type, + data + ] + }); + } + + /** + * Adds a new soundfont buffer with a given ID + * @param soundfontBuffer {ArrayBuffer} - the soundfont's buffer + * @param id {string} - the soundfont's unique identifier + * @param bankOffset {number} - the soundfont's bank offset. Default is 0 + */ + async addNewSoundFont(soundfontBuffer, id, bankOffset = 0) + { + if (this.soundfontList.find(s => s.id === id) !== undefined) + { + throw new Error("Cannot overwrite the existing soundfont. Use soundfontManager.delete(id) instead."); + } + this._sendToWorklet(WorkletSoundfontManagerMessageType.addNewSoundFont, [soundfontBuffer, id, bankOffset]); + await new Promise(r => this.synth._resolveWhenReady = r); + this.soundfontList.push({ + id: id, + bankOffset: bankOffset + }); + } + + /** + * Deletes a soundfont with the given ID + * @param id {string} - the soundfont to delete + */ + deleteSoundFont(id) + { + if (this.soundfontList.length === 0) + { + SpessaSynthWarn("1 soundfont left. Aborting!"); + return; + } + 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 + */ + rearrangeSoundFonts(newList) + { + this._sendToWorklet(WorkletSoundfontManagerMessageType.rearrangeSoundFonts, newList); + this.soundfontList.sort((a, b) => + newList.indexOf(a.id) - newList.indexOf(b.id) + ); + } + + /** + * DELETES ALL SOUNDFONTS!! and creates a new one with id "main" + * @param newBuffer {ArrayBuffer} + */ + async reloadManager(newBuffer) + { + this._sendToWorklet(WorkletSoundfontManagerMessageType.reloadSoundFont, newBuffer); + await new Promise(r => this.synth._resolveWhenReady = r); + } +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/synthetizer.js b/spessasynth_lib/synthetizer/synthetizer.js new file mode 100644 index 0000000000000000000000000000000000000000..c5fff63ae2a54219d752da2126c2e7df85006c42 --- /dev/null +++ b/spessasynth_lib/synthetizer/synthetizer.js @@ -0,0 +1,1031 @@ +import { consoleColors } from "../utils/other.js"; +import { 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, + returnMessageType, + workletMessageType +} from "./worklet_system/message_protocol/worklet_message.js"; +import { SpessaSynthInfo, SpessaSynthWarn } from "../utils/loggin.js"; +import { DEFAULT_SYNTH_CONFIG } from "./audio_effects/effects_config.js"; +import { SoundfontManager } from "./synth_soundfont_manager.js"; +import { KeyModifierManager } from "./key_modifier_manager.js"; +import { channelConfiguration } from "./worklet_system/worklet_utilities/controller_tables.js"; +import { + DEFAULT_PERCUSSION, + DEFAULT_SYNTH_MODE, + MIDI_CHANNEL_COUNT, + VOICE_CAP, + WORKLET_PROCESSOR_NAME +} from "./synth_constants.js"; +import { BasicMIDI } from "../midi_parser/basic_midi.js"; +import { fillWithDefaults } from "../utils/fill_with_defaults.js"; +import { DEFAULT_SEQUENCER_OPTIONS } from "../sequencer/default_sequencer_options.js"; + + +/** + * synthesizer.js + * purpose: responds to midi messages and called functions, managing the channels and passing the messages to them + */ + +/** + * @typedef {Object} SynthMethodOptions + * @property {number} time - the audio context time when the event should execute, in seconds. + */ + +/** + * @type {SynthMethodOptions} + */ +const DEFAULT_SYNTH_METHOD_OPTIONS = { + time: 0 +}; + + +export class Synthetizer +{ + + /** + * Allows setting up custom event listeners for the synthesizer + * @type {EventHandler} + */ + eventHandler = new EventHandler(); + + /** + * Synthesizer's parent AudioContext instance + * @type {BaseAudioContext} + */ + context; + + /** + * Synthesizer's output node + * @type {AudioNode} + */ + targetNode; + /** + * @type {boolean} + * @private + */ + _destroyed = false; + + /** + * 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 data will be sent to channel 0 + * @type {number} + * @private + */ + _outputsAmount = MIDI_CHANNEL_COUNT; + + /** + * The current number of MIDI channels the synthesizer has + * @type {number} + */ + channelsAmount = this._outputsAmount; + + /** + * Synth's current channel properties + * @type {ChannelProperty[]} + */ + channelProperties = []; + + /** + * The current preset list + * @type {{presetName: string, bank: number, program: number}[]} + */ + presetList = []; + + /** + * Creates a new instance of the SpessaSynth synthesizer. + * @param targetNode {AudioNode} + * @param soundFontBuffer {ArrayBuffer} the soundfont file array buffer. + * @param enableEventSystem {boolean} enables the event system. + * Defaults to true. + * Disable only when you're rendering audio offline with no actions from the main thread + * @param startRenderingData {StartRenderingDataConfig} if it is set, + * starts playing this immediately and restores the values. + * @param synthConfig {SynthConfig} optional configuration for the synthesizer. + */ + constructor(targetNode, + soundFontBuffer, + enableEventSystem = true, + startRenderingData = undefined, + synthConfig = DEFAULT_SYNTH_CONFIG) + { + SpessaSynthInfo("%cInitializing SpessaSynth synthesizer...", consoleColors.info); + this.context = targetNode.context; + this.targetNode = targetNode; + + // ensure default values for options + enableEventSystem = enableEventSystem ?? true; + synthConfig = synthConfig ?? DEFAULT_SYNTH_CONFIG; + + // initialize internal promise resolution + this._resolveWhenReady = undefined; + this.isReady = new Promise(resolve => this._resolveWhenReady = resolve); + + // create initial channels + for (let i = 0; i < this.channelsAmount; i++) + { + this.addNewChannel(false); + } + this.channelProperties[DEFAULT_PERCUSSION].isDrum = true; + + // determine output mode and channel configuration + const oneOutputMode = startRenderingData?.oneOutput ?? false; + let processorChannelCount = Array(this._outputsAmount + 2).fill(2); + let processorOutputsCount = this._outputsAmount + 2; + if (oneOutputMode) + { + processorOutputsCount = 1; + processorChannelCount = [32]; + } + + // initialize effects configuration + this.effectsConfig = fillWithDefaults(synthConfig, DEFAULT_SYNTH_CONFIG); + + // process start rendering data + const sequencerRenderingData = {}; + if (startRenderingData?.parsedMIDI !== undefined) + { + sequencerRenderingData.parsedMIDI = BasicMIDI.copyFrom(startRenderingData.parsedMIDI); + if (startRenderingData?.snapshot) + { + const snapshot = startRenderingData.snapshot; + if (snapshot?.effectsConfig !== undefined) + { + // overwrite effects configuration with the snapshot + this.effectsConfig = fillWithDefaults(snapshot.effectsConfig, DEFAULT_SYNTH_CONFIG); + // delete effects config as it cannot be cloned to the worklet (and does not need to be) + delete snapshot.effectsConfig; + } + sequencerRenderingData.snapshot = snapshot; + } + if (startRenderingData?.sequencerOptions) + { + // sequencer options + sequencerRenderingData.sequencerOptions = fillWithDefaults( + startRenderingData.sequencerOptions, + DEFAULT_SEQUENCER_OPTIONS + ); + } + + sequencerRenderingData.loopCount = startRenderingData?.loopCount ?? 0; + } + + // create the audio worklet node + try + { + let workletConstructor = (synthConfig?.audioNodeCreators?.worklet) ?? + ((context, name, options) => + { + return new AudioWorkletNode(context, name, options); + }); + this.worklet = workletConstructor(this.context, WORKLET_PROCESSOR_NAME, { + outputChannelCount: processorChannelCount, + numberOfOutputs: processorOutputsCount, + processorOptions: { + midiChannels: oneOutputMode ? 1 : this._outputsAmount, + soundfont: soundFontBuffer, + enableEventSystem: enableEventSystem, + startRenderingData: sequencerRenderingData + } + }); + } + catch (e) + { + console.error(e); + throw new Error("Could not create the audioWorklet. Did you forget to addModule()?"); + } + + // set up message handling and managers + this.worklet.port.onmessage = e => this.handleMessage(e.data); + this.soundfontManager = new SoundfontManager(this); + this.keyModifierManager = new KeyModifierManager(this); + this._snapshotCallback = undefined; + this.sequencerCallbackFunction = undefined; + + // connect worklet outputs + if (oneOutputMode) + { + this.worklet.connect(targetNode, 0); + } + else + { + const reverbOn = this.effectsConfig?.reverbEnabled ?? true; + const chorusOn = this.effectsConfig?.chorusEnabled ?? true; + if (reverbOn) + { + const proc = getReverbProcessor(this.context, this.effectsConfig.reverbImpulseResponse); + this.reverbProcessor = proc.conv; + this.isReady = Promise.all([this.isReady, proc.promise]); + this.reverbProcessor.connect(targetNode); + this.worklet.connect(this.reverbProcessor, 0); + } + if (chorusOn) + { + this.chorusProcessor = new FancyChorus(targetNode, this.effectsConfig.chorusConfig); + this.worklet.connect(this.chorusProcessor.input, 1); + } + for (let i = 2; i < this.channelsAmount + 2; i++) + { + this.worklet.connect(targetNode, i); + } + } + + // attach event handlers + this.eventHandler.addEvent("newchannel", `synth-new-channel-${Math.random()}`, () => + { + this.channelsAmount++; + }); + this.eventHandler.addEvent("presetlistchange", `synth-preset-list-change-${Math.random()}`, e => + { + this.presetList = e; + }); + } + + + /** + * @type {"gm"|"gm2"|"gs"|"xg"} + * @private + */ + _midiSystem = DEFAULT_SYNTH_MODE; + + /** + * The current MIDI system used by the synthesizer + * @returns {"gm"|"gm2"|"gs"|"xg"} + */ + get midiSystem() + { + return this._midiSystem; + } + + /** + * The current MIDI system used by the synthesizer + * @param value {"gm"|"gm2"|"gs"|"xg"} + */ + set midiSystem(value) + { + this._midiSystem = value; + } + + /** + * current voice amount + * @type {number} + * @private + */ + _voicesAmount = 0; + + /** + * @returns {number} the current number of voices playing. + */ + get voicesAmount() + { + return this._voicesAmount; + } + + /** + * For Black MIDI's - forces release time to 50 ms + * @type {boolean} + */ + _highPerformanceMode = false; + + get highPerformanceMode() + { + return this._highPerformanceMode; + } + + /** + * For Black MIDI's - forces release time to 50 ms. + * @param {boolean} value + */ + set highPerformanceMode(value) + { + this._highPerformanceMode = value; + this.post({ + messageType: workletMessageType.highPerformanceMode, + messageData: value + }); + } + + /** + * @type {number} + * @private + */ + _voiceCap = VOICE_CAP; + + /** + * The maximum number of voices allowed at once. + * @returns {number} + */ + get voiceCap() + { + return this._voiceCap; + } + + /** + * The maximum number of voices allowed at once. + * @param value {number} + */ + set voiceCap(value) + { + this._setMasterParam(masterParameterType.voicesCap, value); + this._voiceCap = value; + } + + /** + * @returns {number} the audioContext's current time. + */ + get currentTime() + { + return this.context.currentTime; + } + + /** + * Sets the SpessaSynth's log level. + * @param enableInfo {boolean} - enable info (verbose) + * @param enableWarning {boolean} - enable warnings (unrecognized messages) + * @param enableGroup {boolean} - enable groups (to group a lot of logs) + * @param enableTable {boolean} - enable table (debug message) + */ + setLogLevel(enableInfo, enableWarning, enableGroup, enableTable) + { + this.post({ + channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION, + messageType: workletMessageType.setLogLevel, + messageData: [enableInfo, enableWarning, enableGroup, enableTable] + }); + } + + /** + * @param type {masterParameterType} + * @param data {any} + * @private + */ + _setMasterParam(type, data) + { + this.post({ + channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION, + messageType: workletMessageType.setMasterParameter, + messageData: [type, data] + }); + } + + /** + * Sets the interpolation type for the synthesizer: + * 0. - linear + * 1. - nearest neighbor + * 2. - cubic + * @param type {interpolationTypes} + */ + setInterpolationType(type) + { + this._setMasterParam(masterParameterType.interpolationType, type); + } + + /** + * Handles the messages received from the worklet. + * @param message {WorkletReturnMessage} + * @private + */ + handleMessage(message) + { + const messageData = message.messageData; + switch (message.messageType) + { + case returnMessageType.channelPropertyChange: + /** + * @type {number} + */ + const channelNumber = messageData[0]; + /** + * @type {ChannelProperty} + */ + const property = messageData[1]; + + this.channelProperties[channelNumber] = property; + + 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) + { + this.sequencerCallbackFunction(messageData.messageType, messageData.messageData); + } + break; + + case returnMessageType.masterParameterChange: + /** + * @type {masterParameterType} + */ + const param = messageData[0]; + const value = messageData[1]; + switch (param) + { + default: + break; + + case masterParameterType.midiSystem: + this._midiSystem = value; + break; + } + break; + + case returnMessageType.synthesizerSnapshot: + if (this._snapshotCallback) + { + this._snapshotCallback(messageData); + } + break; + + case returnMessageType.isFullyInitialized: + 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 => + { + this._snapshotCallback = undefined; + s.effectsConfig = this.effectsConfig; + resolve(s); + }; + this.post({ + messageType: workletMessageType.requestSynthesizerSnapshot, + messageData: undefined, + channelNumber: ALL_CHANNELS_OR_DIFFERENT_ACTION + }); + }); + } + + /** + * Adds a new channel to the synthesizer. + * @param postMessage {boolean} leave at true, set to false only at initialization. + */ + addNewChannel(postMessage = true) + { + this.channelProperties.push({ + voicesAmount: 0, + pitchBend: 0, + pitchBendRangeSemitones: 0, + isMuted: false, + isDrum: false, + transposition: 0, + program: 0, + bank: this.channelsAmount % 16 === DEFAULT_PERCUSSION ? 128 : 0 + }); + if (!postMessage) + { + return; + } + this.post({ + channelNumber: 0, + messageType: workletMessageType.addNewChannel, + messageData: null + }); + } + + /** + * @param channel {number} + * @param value {{delay: number, depth: number, rate: number}} + */ + setVibrato(channel, value) + { + this.post({ + channelNumber: channel, + messageType: workletMessageType.setChannelVibrato, + 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) + { + 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++) + { + // + 2 because chorus and reverb come first! + this.worklet.connect(audioNodes[outputNumber], outputNumber + 2); + } + } + + /** + * Disconnects the individual audio outputs to the given audio nodes. In the app, it's used by the renderer. + * @param audioNodes {AudioNode[]} + */ + disconnectIndividualOutputs(audioNodes) + { + 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++) + { + // + 2 because chorus and reverb come first! + this.worklet.disconnect(audioNodes[outputNumber], outputNumber + 2); + } + } + + /* + * Disables the GS NRPN parameters like vibrato or drum key tuning. + */ + disableGSNRPparams() + { + // 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 }); + } + + /** + * A message for debugging. + */ + debugMessage() + { + SpessaSynthInfo(this); + this.post({ + channelNumber: 0, + messageType: workletMessageType.debugMessage, + messageData: undefined + }); + } + + /** + * sends a raw MIDI message to the synthesizer. + * @param message {number[]|Uint8Array} the midi message, each number is a byte. + * @param channelOffset {number} the channel offset of the message. + * @param eventOptions {SynthMethodOptions} additional options for this command. + */ + sendMessage(message, channelOffset = 0, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS) + { + this._sendInternal(message, channelOffset, false, eventOptions); + } + + /** + * @param message {number[]|Uint8Array} + * @param offset {number} + * @param force {boolean} + * @param eventOptions {SynthMethodOptions} + * @private + */ + _sendInternal(message, offset, force = false, eventOptions) + { + const opts = fillWithDefaults(eventOptions ?? {}, DEFAULT_SYNTH_METHOD_OPTIONS); + this.post({ + messageType: workletMessageType.midiMessage, + messageData: [new Uint8Array(message), offset, force, opts] + }); + } + + + /** + * Starts playing a note + * @param channel {number} usually 0-15: the channel to play the note. + * @param midiNote {number} 0-127 the key number of the note. + * @param velocity {number} 0-127 the velocity of the note (generally controls loudness). + * @param eventOptions {SynthMethodOptions} additional options for this command. + */ + noteOn(channel, midiNote, velocity, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS) + { + const ch = channel % 16; + const offset = channel - ch; + midiNote %= 128; + velocity %= 128; + // check for legacy "enableDebugging" + if (eventOptions === true) + { + eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS; + } + this.sendMessage([messageTypes.noteOn | ch, midiNote, velocity], offset, eventOptions); + } + + /** + * 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. + * @param eventOptions {SynthMethodOptions} additional options for this command. + */ + noteOff(channel, midiNote, force = false, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS) + { + midiNote %= 128; + + const ch = channel % 16; + const offset = channel - ch; + this._sendInternal([messageTypes.noteOff | ch, midiNote], offset, force, eventOptions); + } + + /** + * Stops all notes. + * @param force {boolean} if we should instantly kill the note, defaults to 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. + * @param controllerNumber {number} 0-127 the MIDI CC number. + * @param controllerValue {number} 0-127 the controller value. + * @param force {boolean} forces the controller-change message, even if it's locked or gm system is set and the cc is bank select. + * @param eventOptions {SynthMethodOptions} additional options for this command. + */ + controllerChange(channel, controllerNumber, controllerValue, force = false, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS) + { + if (controllerNumber > 127 || controllerNumber < 0) + { + throw new Error(`Invalid controller number: ${controllerNumber}`); + } + controllerValue = Math.floor(controllerValue) % 128; + controllerNumber = Math.floor(controllerNumber) % 128; + // controller change has its own message for the force property + const ch = channel % 16; + const offset = channel - ch; + this._sendInternal( + [messageTypes.controllerChange | ch, controllerNumber, controllerValue], + offset, + force, + eventOptions + ); + } + + /** + * Resets all controllers (for every channel) + */ + resetControllers() + { + this.post({ + 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. + * @param pressure {number} 0-127: the pressure to apply. + * @param eventOptions {SynthMethodOptions} additional options for this command. + */ + channelPressure(channel, pressure, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS) + { + const ch = channel % 16; + const offset = channel - ch; + pressure %= 128; + this.sendMessage([messageTypes.channelPressure | ch, pressure], offset, eventOptions); + } + + /** + * Applies pressure to a given note. + * @param channel {number} usually 0-15: the channel to change the controller. + * @param midiNote {number} 0-127: the MIDI note. + * @param pressure {number} 0-127: the pressure to apply. + * @param eventOptions {SynthMethodOptions} additional options for this command. + */ + polyPressure(channel, midiNote, pressure, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS) + { + const ch = channel % 16; + const offset = channel - ch; + midiNote %= 128; + pressure %= 128; + this.sendMessage([messageTypes.polyPressure | ch, midiNote, pressure], offset, eventOptions); + } + + /** + * Sets the pitch of the given channel. + * @param channel {number} usually 0-15: the channel to change pitch. + * @param MSB {number} SECOND byte of the MIDI pitchWheel message. + * @param LSB {number} FIRST byte of the MIDI pitchWheel message. + * @param eventOptions {SynthMethodOptions} additional options for this command. + */ + pitchWheel(channel, MSB, LSB, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS) + { + const ch = channel % 16; + const offset = channel - ch; + this.sendMessage([messageTypes.pitchBend | ch, LSB, MSB], offset, eventOptions); + } + + /** + * @param data {WorkletMessage} + */ + post(data) + { + if (this._destroyed) + { + throw new Error("This synthesizer instance has been destroyed!"); + } + this.worklet.port.postMessage(data); + } + + /** + * Transposes the synthetizer's pitch by given semitones amount (percussion channels don’t get affected). + * @param semitones {number} the semitones to transpose by. + * It can be a floating point number for more precision. + */ + transpose(semitones) + { + this.transposeChannel(ALL_CHANNELS_OR_DIFFERENT_ACTION, semitones, false); + } + + /** + * Transposes the channel by given number of semitones. + * @param channel {number} the channel number. + * @param semitones {number} the transposition of the channel, it 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) + { + this.post({ + channelNumber: channel, + messageType: workletMessageType.transpose, + messageData: [semitones, force] + }); + } + + /** + * Sets the main volume. + * @param volume {number} 0-1 the volume. + */ + setMainVolume(volume) + { + 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) + */ + setMasterPan(pan) + { + this._setMasterParam(masterParameterType.masterPan, pan); + } + + /** + * Sets the channel's pitch bend range, in semitones + * @param channel {number} usually 0-15: the channel to change + * @param pitchBendRangeSemitones {number} the bend range in semitones + */ + setPitchBendRange(channel, pitchBendRangeSemitones) + { + // 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 + * defaults to false + */ + programChange(channel, programNumber) + { + const ch = channel % 16; + const offset = channel - ch; + programNumber %= 128; + this.sendMessage([messageTypes.programChange | ch, programNumber], offset); + } + + /** + * Overrides velocity on a given channel. + * @param channel {number} usually 0-15: the channel to change. + * @param velocity {number} 1-127, the velocity to use. + * 0 Disables this functionality + */ + velocityOverride(channel, velocity) + { + const ch = channel % 16; + const offset = channel - ch; + this._sendInternal( + [messageTypes.controllerChange | ch, channelConfiguration.velocityOverride, velocity], + offset, + true, + DEFAULT_SYNTH_METHOD_OPTIONS + ); + } + + /** + * Causes the given midi channel to ignore controller messages for the given controller number. + * @param channel {number} usually 0-15: the channel to lock. + * @param controllerNumber {number} 0-127 MIDI CC number NOTE: -1 locks the preset. + * @param isLocked {boolean} true if locked, false if unlocked + */ + lockController(channel, controllerNumber, isLocked) + { + this.post({ + channelNumber: channel, + messageType: workletMessageType.lockController, + messageData: [controllerNumber, isLocked] + }); + } + + /** + * Mutes or unmutes the given channel. + * @param channel {number} usually 0-15: the channel to lock. + * @param isMuted {boolean} indicates if the channel is muted. + */ + muteChannel(channel, isMuted) + { + this.post({ + channelNumber: channel, + messageType: workletMessageType.muteChannel, + messageData: isMuted + }); + } + + /** + * Reloads the sounfont. + * THIS IS DEPRECATED! + * USE soundfontManager instead. + * @param soundFontBuffer {ArrayBuffer} the new soundfont file array buffer. + * @return {Promise} + * @deprecated Use the soundfontManager property. + */ + async reloadSoundFont(soundFontBuffer) + { + 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 {number[]|ArrayLike|Uint8Array} the message's data + * (excluding the F0 byte, but including the F7 at the end). + * @param channelOffset {number} channel offset for the system exclusive message, defaults to zero. + * @param eventOptions {SynthMethodOptions} additional options for this command. + */ + systemExclusive(messageData, channelOffset = 0, eventOptions = DEFAULT_SYNTH_METHOD_OPTIONS) + { + this._sendInternal( + [messageTypes.systemExclusive, ...Array.from(messageData)], + channelOffset, + false, + eventOptions + ); + } + + // noinspection JSUnusedGlobalSymbols + /** + * Tune MIDI keys of a given program using the MIDI Tuning Standard. + * @param program {number} 0 - 127 the MIDI program number to use. + * @param tunings {{sourceKey: number, targetPitch: number}[]} - the keys and their tunings. + * TargetPitch of -1 sets the tuning for this key to be tuned regularly. + */ + tuneKeys(program, tunings) + { + if (tunings.length > 127) + { + throw new Error("Too many tunings. Maximum allowed is 127."); + } + const systemExclusive = [ + 0x7F, // real-time + 0x10, // device id + 0x08, // MIDI Tuning + 0x02, // note change + program, // tuning program number + tunings.length // number of changes + ]; + for (const tuning of tunings) + { + systemExclusive.push(tuning.sourceKey); // [kk] MIDI Key number + if (tuning.targetPitch === -1) + { + // no change + systemExclusive.push(0x7F, 0x7F, 0x7F); + } + else + { + const midiNote = Math.floor(tuning.targetPitch); + const fraction = Math.floor((tuning.targetPitch - midiNote) / 0.000061); + systemExclusive.push( + midiNote,// frequency data byte 1 + (fraction >> 7) & 0x7F, // frequency data byte 2 + fraction & 0x7F // frequency data byte 3 + ); + } + } + systemExclusive.push(0xF7); + this.systemExclusive(systemExclusive); + } + + /** + * Toggles drums on a given channel. + * @param channel {number} + * @param isDrum {boolean} + */ + setDrums(channel, isDrum) + { + this.post({ + channelNumber: channel, + messageType: workletMessageType.setDrums, + messageData: isDrum + }); + } + + /** + * Updates the reverb processor with a new impulse response. + * @param buffer {AudioBuffer} the new reverb impulse response. + */ + setReverbResponse(buffer) + { + this.reverbProcessor.buffer = buffer; + this.effectsConfig.reverbImpulseResponse = buffer; + } + + /** + * Updates the chorus processor parameters. + * @param config {ChorusConfig} the new chorus. + */ + setChorusConfig(config) + { + this.worklet.disconnect(this.chorusProcessor.input); + this.chorusProcessor.delete(); + delete this.chorusProcessor; + this.chorusProcessor = new FancyChorus(this.targetNode, config); + this.worklet.connect(this.chorusProcessor.input, 1); + this.effectsConfig.chorusConfig = config; + } + + /** + * Changes the effects gain. + * @param reverbGain {number} the reverb gain, 0-1. + * @param chorusGain {number} the chorus gain, 0-1. + */ + setEffectsGain(reverbGain, chorusGain) + { + // noinspection JSCheckFunctionSignatures + this.post({ + messageType: workletMessageType.setEffectsGain, + messageData: [reverbGain, chorusGain] + }); + } + + /** + * Destroys the synthesizer instance. + */ + destroy() + { + this.reverbProcessor.disconnect(); + this.chorusProcessor.delete(); + // noinspection JSCheckFunctionSignatures + this.post({ + messageType: workletMessageType.destroyWorklet, + messageData: undefined + }); + this.worklet.disconnect(); + delete this.worklet; + delete this.reverbProcessor; + delete this.chorusProcessor; + this._destroyed = true; + } + + // noinspection JSUnusedGlobalSymbols + reverbateEverythingBecauseWhyNot() + { + for (let i = 0; i < this.channelsAmount; i++) + { + this.controllerChange(i, midiControllers.reverbDepth, 127); + this.lockController(i, midiControllers.reverbDepth, true); + } + return "That's the spirit!"; + } +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_processor.min.js b/spessasynth_lib/synthetizer/worklet_processor.min.js new file mode 100644 index 0000000000000000000000000000000000000000..a7f4e3d70df6273b145b944611bbe4b8828cc956 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_processor.min.js @@ -0,0 +1,21 @@ +var Fs=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(A,t)=>(typeof require<"u"?require:A)[t]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});function Hn(e){e=Math.floor(e);let A=Math.floor(e/60),t=Math.round(e-A*60);return{minutes:A,seconds:t,time:`${A.toString().padStart(2,"0")}:${t.toString().padStart(2,"0")}`}}function Rs(e){return e.trim().replaceAll(".mid","").replaceAll(".kar","").replaceAll(".rmi","").replaceAll(".xmf","").replaceAll(".mxmf","").replaceAll("_"," ").trim()}function HA(e){let A="";for(let t=0;ts+o.length,0),t=new b(A),n=0;for(let s of e)t.set(s,n),n+=s.length;return t}var VA=class{ticks;messageStatusByte;messageData;constructor(A,t,n){this.ticks=A,this.messageStatusByte=t,this.messageData=n}};function xs(e){let A=e&240,t=e&15,n=t;switch(A){case 128:case 144:case 160:case 176:case 192:case 208:case 224:break;case 240:switch(t){case 0:n=-3;break;case 1:case 2:case 3:case 4:case 5:case 6:case 7:case 8:case 9:case 10:case 11:case 12:case 13:case 14:n=-1;break;case 15:n=-2;break}break;default:n=-1}return n}var w={noteOff:128,noteOn:144,polyPressure:160,controllerChange:176,programChange:192,channelPressure:208,pitchBend:224,systemExclusive:240,timecode:241,songPosition:242,songSelect:243,tuneRequest:246,clock:248,start:250,continue:251,stop:252,activeSensing:254,reset:255,sequenceNumber:0,text:1,copyright:2,trackName:3,instrumentName:4,lyric:5,marker:6,cuePoint:7,programName:8,midiChannelPrefix:32,midiPort:33,endOfTrack:47,setTempo:81,smpteOffset:84,timeSignature:88,keySignature:89,sequenceSpecific:127};function at(e){let A=e&240,t=e&15,n=-1,s=e;return A>=128&&A<=224&&(n=t,s=A),{status:s,channel:n}}var y={bankSelect:0,modulationWheel:1,breathController:2,footController:4,portamentoTime:5,dataEntryMsb:6,mainVolume:7,balance:8,pan:10,expressionController:11,effectControl1:12,effectControl2:13,generalPurposeController1:16,generalPurposeController2:17,generalPurposeController3:18,generalPurposeController4:19,lsbForControl0BankSelect:32,lsbForControl1ModulationWheel:33,lsbForControl2BreathController:34,lsbForControl4FootController:36,lsbForControl5PortamentoTime:37,lsbForControl6DataEntry:38,lsbForControl7MainVolume:39,lsbForControl8Balance:40,lsbForControl10Pan:42,lsbForControl11ExpressionController:43,lsbForControl12EffectControl1:44,lsbForControl13EffectControl2:45,sustainPedal:64,portamentoOnOff:65,sostenutoPedal:66,softPedal:67,legatoFootswitch:68,hold2Pedal:69,soundVariation:70,filterResonance:71,releaseTime:72,attackTime:73,brightness:74,decayTime:75,vibratoRate:76,vibratoDepth:77,vibratoDelay:78,soundController10:79,generalPurposeController5:80,generalPurposeController6:81,generalPurposeController7:82,generalPurposeController8:83,portamentoControl:84,reverbDepth:91,tremoloDepth:92,chorusDepth:93,detuneDepth:94,phaserDepth:95,dataIncrement:96,dataDecrement:97,NRPNLsb:98,NRPNMsb:99,RPNLsb:100,RPNMsb:101,allSoundOff:120,resetAllControllers:121,localControlOnOff:122,allNotesOff:123,omniModeOff:124,omniModeOn:125,monoModeOn:126,polyModeOn:127},Ms={8:2,9:2,10:2,11:2,12:1,13:1,14:2};var Ns=!1,bs=!0,Xt=!1,gi=!0;function Ls(e,A,t,n){Ns=e,bs=A,Xt=t,gi=n}function p(...e){Ns&&console.info(...e)}function Y(...e){bs&&console.warn(...e)}function se(...e){Xt&&console.group(...e)}function pA(...e){Xt&&console.groupCollapsed(...e)}function P(){Xt&&console.groupEnd()}function oe(e,A){let t=0;for(let n=8*(A-1);n>=0;n-=8)t|=e[e.currentIndex++]<>>0}function Rt(e,A){let t=new Array(A).fill(0);for(let n=A-1;n>=0;n--)t[n]=e&255,e>>=8;return t}function Us(e,A){if(this.sendMIDIMessages&&e.messageStatusByte>=128){this.sendMIDIMessage([e.messageStatusByte,...e.messageData]);return}let t=at(e.messageStatusByte),n=this.midiPortChannelOffsets[this.midiPorts[A]]||0;switch(t.channel+=n,t.status){case w.noteOn:let s=e.messageData[1];if(s>0)this.synth.noteOn(t.channel,e.messageData[0],s),this.playingNotes.push({midiNote:e.messageData[0],channel:t.channel,velocity:s});else{this.synth.noteOff(t.channel,e.messageData[0]);let C=this.playingNotes.findIndex(i=>i.midiNote===e.messageData[0]&&i.channel===t.channel);C!==-1&&this.playingNotes.splice(C,1)}break;case w.noteOff:this.synth.noteOff(t.channel,e.messageData[0]);let o=this.playingNotes.findIndex(C=>C.midiNote===e.messageData[0]&&C.channel===t.channel);o!==-1&&this.playingNotes.splice(o,1);break;case w.pitchBend:this.synth.pitchWheel(t.channel,e.messageData[1],e.messageData[0]);break;case w.controllerChange:if(this.midiData.isMultiPort&&this.midiData.usedChannelsOnTrack[A].size===0)return;this.synth.controllerChange(t.channel,e.messageData[0],e.messageData[1]);break;case w.programChange:if(this.midiData.isMultiPort&&this.midiData.usedChannelsOnTrack[A].size===0)return;this.synth.programChange(t.channel,e.messageData[0]);break;case w.polyPressure:this.synth.polyPressure(t.channel,e.messageData[0],e.messageData[1]);break;case w.channelPressure:this.synth.channelPressure(t.channel,e.messageData[0]);break;case w.systemExclusive:this.synth.systemExclusive(e.messageData,n);break;case w.setTempo:e.messageData.currentIndex=0;let r=6e7/oe(e.messageData,3);this.oneTickToSeconds=60/(r*this.midiData.timeDivision),this.oneTickToSeconds===0&&(this.oneTickToSeconds=60/(120*this.midiData.timeDivision),Y("invalid tempo! falling back to 120 BPM"),r=120);break;case w.timeSignature:case w.endOfTrack:case w.midiChannelPrefix:case w.songPosition:case w.activeSensing:case w.keySignature:case w.sequenceNumber:case w.sequenceSpecific:case w.text:case w.lyric:case w.copyright:case w.trackName:case w.marker:case w.cuePoint:case w.instrumentName:case w.programName:break;case w.midiPort:this.assignMIDIPort(A,e.messageData[0]);break;case w.reset:this.synth.stopAllChannels(),this.synth.resetAllControllers();break;default:Y(`%cUnrecognized Event: %c${e.messageStatusByte}%c status byte: %c${Object.keys(w).find(C=>w[C]===t.status)}`,I.warn,I.unrecognized,I.warn,I.value);break}t.status>=0&&t.status<128&&this.post(UA.metaEvent,[e,A])}function Ts(){for(let e=0;e<16;e++)this.synth.createWorkletChannel(!0)}function vs(){if(!this.isActive)return;let e=this.currentTime;for(;this.playedTime1&&this.nextSong();return}let n=this.tracks[A][this.eventIndex[A]];this.playedTime+=this.oneTickToSeconds*(n.ticks-t.ticks);let s=this.loop&&(this.loopCount>0||this.loopCount===-1);if(this.midiData.loop.end<=t.ticks&&s){this.loopCount!==1/0&&(this.loopCount--,this.post(UA.loopCountChange,this.loopCount)),this.setTimeTicks(this.midiData.loop.start);return}else if(e>=this.duration){if(s){this.loopCount!==1/0&&(this.loopCount--,this.post(UA.loopCountChange,this.loopCount)),this.setTimeTicks(this.midiData.loop.start);return}this.eventIndex[A]--,this.pause(!0),this.songs.length>1&&this.nextSong();return}}}function Hs(){let e=0,A=1/0;return this.tracks.forEach((t,n)=>{this.eventIndex[n]>=t.length||t[this.eventIndex[n]].ticks0;){let n=this.tempoChanges.find(o=>o.ticksnew Uint8Array(t)),this.lyricsTicks=[...A.lyricsTicks],this.midiPorts=[...A.midiPorts],this.trackNames=[...A.trackNames],this.midiPortChannelOffsets=[...A.midiPortChannelOffsets],this.usedChannelsOnTrack=A.usedChannelsOnTrack.map(t=>new Set(t)),this.rawMidiName=A.rawMidiName?new Uint8Array(A.rawMidiName):void 0,this.loop={...A.loop},this.keyRange={...A.keyRange},this.RMIDInfo={...A.RMIDInfo}}};var Wt=class extends It{isEmbedded=!1;constructor(A){super(),this._copyFromSequence(A),this.isEmbedded=A.embeddedSoundFont!==void 0}},la={duration:99999,firstNoteOn:0,loop:{start:0,end:123456},lastVoiceEventTick:123456,lyrics:[],copyright:"",midiPorts:[],midiPortChannelOffsets:[],tracksAmount:0,tempoChanges:[{ticks:0,tempo:120}],fileName:"NOT_LOADED.mid",midiName:"Loading...",rawMidiName:new Uint8Array([76,111,97,100,105,110,103,46,46,46]),usedChannelsOnTrack:[],timeDivision:0,keyRange:{min:0,max:127},isEmbedded:!1,RMIDInfo:{},bankOffset:0,midiNameUsesFileName:!1,format:0};function N(e,A){let t=0;for(let n=0;n>>0}function Xe(e,A,t){for(let n=0;n>n*8&255}function H(e,A){e[e.currentIndex++]=A&255,e[e.currentIndex++]=A>>8}function AA(e,A){Xe(e,A,4)}function Ne(e,A){let t=A<<8|e;return t>32767?t-65536:t}function Ys(e){return e>127?e-256:e}function eA(e,A,t=void 0,n=!0){if(t){let s=e.slice(e.currentIndex,e.currentIndex+A);return e.currentIndex+=A,new TextDecoder(t.replace(/[^\x20-\x7E]/g,"")).decode(s.buffer)}else{let s=!1,o="";for(let r=0;r127)&&C!==10){if(n){s=!0;continue}else if(C===0){s=!0;continue}}o+=String.fromCharCode(C)}}return o}}function gt(e,A=0){let t=e.length;A>0&&(t=A);let n=new b(t);return TA(n,e,A),n}function fe(e){return gt(e,e.length+1)}function TA(e,A,t=0){t>0&&A.length>t&&(A=A.slice(0,t));for(let n=0;nA.length)for(let n=0;nt.header!=="LIST"?!1:(t.chunkData.currentIndex=0,eA(t.chunkData,4)===A))}function yA(e){let A=0;for(;e;){let t=e[e.currentIndex++];if(A=A<<7|t&127,t>>7!==1)break}return A}function _t(e){let A=[e&127];for(e>>=7;e>0;)A.unshift(e&127|128),e>>=7;return A}var a={INVALID:-1,startAddrsOffset:0,endAddrOffset:1,startloopAddrsOffset:2,endloopAddrsOffset:3,startAddrsCoarseOffset:4,modLfoToPitch:5,vibLfoToPitch:6,modEnvToPitch:7,initialFilterFc:8,initialFilterQ:9,modLfoToFilterFc:10,modEnvToFilterFc:11,endAddrsCoarseOffset:12,modLfoToVolume:13,unused1:14,chorusEffectsSend:15,reverbEffectsSend:16,pan:17,unused2:18,unused3:19,unused4:20,delayModLFO:21,freqModLFO:22,delayVibLFO:23,freqVibLFO:24,delayModEnv:25,attackModEnv:26,holdModEnv:27,decayModEnv:28,sustainModEnv:29,releaseModEnv:30,keyNumToModEnvHold:31,keyNumToModEnvDecay:32,delayVolEnv:33,attackVolEnv:34,holdVolEnv:35,decayVolEnv:36,sustainVolEnv:37,releaseVolEnv:38,keyNumToVolEnvHold:39,keyNumToVolEnvDecay:40,instrument:41,reserved1:42,keyRange:43,velRange:44,startloopAddrsCoarseOffset:45,keyNum:46,velocity:47,initialAttenuation:48,reserved2:49,endloopAddrsCoarseOffset:50,coarseTune:51,fineTune:52,sampleID:53,sampleModes:54,reserved3:55,scaleTuning:56,exclusiveClass:57,overridingRootKey:58,unused5:59,endOper:60},X=[];X[a.startAddrsOffset]={min:0,max:32768,def:0};X[a.endAddrOffset]={min:-32768,max:32768,def:0};X[a.startloopAddrsOffset]={min:-32768,max:32768,def:0};X[a.endloopAddrsOffset]={min:-32768,max:32768,def:0};X[a.startAddrsCoarseOffset]={min:0,max:32768,def:0};X[a.modLfoToPitch]={min:-12e3,max:12e3,def:0};X[a.vibLfoToPitch]={min:-12e3,max:12e3,def:0};X[a.modEnvToPitch]={min:-12e3,max:12e3,def:0};X[a.initialFilterFc]={min:1500,max:13500,def:13500};X[a.initialFilterQ]={min:0,max:960,def:0};X[a.modLfoToFilterFc]={min:-12e3,max:12e3,def:0};X[a.modEnvToFilterFc]={min:-12e3,max:12e3,def:0};X[a.endAddrsCoarseOffset]={min:-32768,max:32768,def:0};X[a.modLfoToVolume]={min:-960,max:960,def:0};X[a.chorusEffectsSend]={min:0,max:1e3,def:0};X[a.reverbEffectsSend]={min:0,max:1e3,def:0};X[a.pan]={min:-500,max:500,def:0};X[a.delayModLFO]={min:-12e3,max:5e3,def:-12e3};X[a.freqModLFO]={min:-16e3,max:4500,def:0};X[a.delayVibLFO]={min:-12e3,max:5e3,def:-12e3};X[a.freqVibLFO]={min:-16e3,max:4500,def:0};X[a.delayModEnv]={min:-32768,max:5e3,def:-32768};X[a.attackModEnv]={min:-32768,max:8e3,def:-32768};X[a.holdModEnv]={min:-12e3,max:5e3,def:-12e3};X[a.decayModEnv]={min:-12e3,max:8e3,def:-12e3};X[a.sustainModEnv]={min:0,max:1e3,def:0};X[a.releaseModEnv]={min:-7200,max:8e3,def:-12e3};X[a.keyNumToModEnvHold]={min:-1200,max:1200,def:0};X[a.keyNumToModEnvDecay]={min:-1200,max:1200,def:0};X[a.delayVolEnv]={min:-12e3,max:5e3,def:-12e3};X[a.attackVolEnv]={min:-12e3,max:8e3,def:-12e3};X[a.holdVolEnv]={min:-12e3,max:5e3,def:-12e3};X[a.decayVolEnv]={min:-12e3,max:8e3,def:-12e3};X[a.sustainVolEnv]={min:0,max:1440,def:0};X[a.releaseVolEnv]={min:-7200,max:8e3,def:-12e3};X[a.keyNumToVolEnvHold]={min:-1200,max:1200,def:0};X[a.keyNumToVolEnvDecay]={min:-1200,max:1200,def:0};X[a.startloopAddrsCoarseOffset]={min:-32768,max:32768,def:0};X[a.keyNum]={min:-1,max:127,def:-1};X[a.velocity]={min:-1,max:127,def:-1};X[a.initialAttenuation]={min:0,max:1440,def:0};X[a.endloopAddrsCoarseOffset]={min:-32768,max:32768,def:0};X[a.coarseTune]={min:-120,max:120,def:0};X[a.fineTune]={min:-12700,max:12700,def:0};X[a.scaleTuning]={min:0,max:1200,def:100};X[a.exclusiveClass]={min:0,max:99999,def:0};X[a.overridingRootKey]={min:-1,max:127,def:-1};X[a.sampleModes]={min:0,max:3,def:0};var U=class{generatorType=a.INVALID;generatorValue=0;constructor(A=a.INVALID,t=0,n=!0){if(this.generatorType=A,t===void 0)throw new Error("No value provided.");if(this.generatorValue=Math.round(t),n){let s=X[A];s!==void 0&&(this.generatorValue=Math.max(s.min,Math.min(s.max,this.generatorValue)),A===a.initialAttenuation&&console.log(this.generatorValue))}}};function Js(e,A,t){let n=X[e]||{min:0,max:32768,def:0},s=A.find(g=>g.generatorType===e),o=0;s&&(o=s.generatorValue);let r=t.find(g=>g.generatorType===e),C=n.def;r&&(C=r.generatorValue);let i=C+o;return e===a.initialAttenuation?i:Math.max(n.min,Math.min(n.max,i))}var j={noController:0,noteOnVelocity:2,noteOnKeyNum:3,polyPressure:10,channelPressure:13,pitchWheel:14,pitchWheelRange:16,link:127},GA={linear:0,concave:1,convex:2,switch:3},z=class e{currentValue=0;sourceEnum;secondarySourceEnum;modulatorDestination;transformAmount;transformType;constructor(A,t,n,s,o){this.sourceEnum=A,this.modulatorDestination=n,this.secondarySourceEnum=t,this.transformAmount=s,this.transformType=o,this.modulatorDestination>58&&(this.modulatorDestination=a.INVALID),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,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.isEffectModulator=(this.sourceEnum===219||this.sourceEnum===221)&&this.secondarySourceEnum===0&&(this.modulatorDestination===a.reverbEffectsSend||this.modulatorDestination===a.chorusEffectsSend)}static copy(A){return new e(A.sourceEnum,A.secondarySourceEnum,A.modulatorDestination,A.transformAmount,A.transformType)}static isIdentical(A,t,n=!1){return A.sourceEnum===t.sourceEnum&&A.modulatorDestination===t.modulatorDestination&&A.secondarySourceEnum===t.secondarySourceEnum&&A.transformType===t.transformType&&(!n||A.transformAmount===t.transformAmount)}static debugString(A){function t(o,r){return Object.keys(o).find(C=>o[C]===r)}let n=t(GA,A.sourceCurveType);n+=A.sourcePolarity===0?" unipolar ":" bipolar ",n+=A.sourceDirection===0?"forwards ":"backwards ",A.sourceUsesCC?n+=t(y,A.sourceIndex):n+=t(j,A.sourceIndex);let s=t(GA,A.secSrcCurveType);return s+=A.secSrcPolarity===0?" unipolar ":" bipolar ",s+=A.secSrcCurveType===0?"forwards ":"backwards ",A.secSrcUsesCC?s+=t(y,A.secSrcIndex):s+=t(j,A.secSrcIndex),`Modulator: + Source: ${n} + Secondary source: ${s} + Destination: ${t(a,A.modulatorDestination)} + Trasform amount: ${A.transformAmount} + Transform type: ${A.transformType} + + +`}sumTransform(A){return new e(this.sourceEnum,this.secondarySourceEnum,this.modulatorDestination,this.transformAmount+A.transformAmount,this.transformType)}},Yn=960,Jn=GA.concave;function re(e,A,t,n,s){return e<<10|A<<9|t<<8|n<<7|s}var Ci=[new z(re(Jn,0,1,0,j.noteOnVelocity),0,a.initialAttenuation,Yn,0),new z(129,0,a.vibLfoToPitch,50,0),new z(re(Jn,0,1,1,y.mainVolume),0,a.initialAttenuation,Yn,0),new z(13,0,a.vibLfoToPitch,50,0),new z(526,16,a.fineTune,12700,0),new z(650,0,a.pan,500,0),new z(re(Jn,0,1,1,y.expressionController),0,a.initialAttenuation,Yn,0),new z(219,0,a.reverbEffectsSend,200,0),new z(221,0,a.chorusEffectsSend,200,0)],Ei=[new z(re(GA.linear,0,0,0,j.polyPressure),0,a.vibLfoToPitch,50,0),new z(re(GA.linear,0,0,1,y.tremoloDepth),0,a.modLfoToVolume,24,0),new z(re(GA.convex,1,0,1,y.attackTime),0,a.attackVolEnv,6e3,0),new z(re(GA.linear,1,0,1,y.releaseTime),0,a.releaseVolEnv,3600,0),new z(re(GA.linear,1,0,1,y.brightness),0,a.initialFilterFc,6e3,0),new z(re(GA.linear,1,0,1,y.filterResonance),0,a.initialFilterQ,250,0)],Ks=Ci.concat(Ei);var xA=128,zt=147,be=new Int16Array(zt).fill(0),MA=(e,A)=>be[e]=A<<7;MA(y.mainVolume,100);MA(y.balance,64);MA(y.expressionController,127);MA(y.pan,64);MA(y.portamentoOnOff,127);MA(y.filterResonance,64);MA(y.releaseTime,64);MA(y.attackTime,64);MA(y.brightness,64);MA(y.decayTime,64);MA(y.vibratoRate,64);MA(y.vibratoDepth,64);MA(y.vibratoDelay,64);MA(y.generalPurposeController6,64);MA(y.generalPurposeController8,64);MA(y.RPNLsb,127);MA(y.RPNMsb,127);MA(y.NRPNLsb,127);MA(y.NRPNMsb,127);var jt=1;be[y.portamentoControl]=jt;MA(xA+j.pitchWheel,64);MA(xA+j.pitchWheelRange,2);var cA={channelTuning:0,channelTransposeFine:1,modulationMultiplier:2,masterTuning:3,channelTuningSemitones:4},Kn=Object.keys(cA).length,On=new Float32Array(Kn);On[cA.modulationMultiplier]=1;var YA={Idle:0,RPCoarse:1,RPFine:2,NRPCoarse:3,NRPFine:4,DataCoarse:5,DataFine:6},Os={velocityOverride:128};var qs="spessasynth-worklet-system";var $t="gs";function Ct(e){return e.messageData[0]===67&&e.messageData[2]===76&&e.messageData[5]===126&&e.messageData[6]===0}function An(e){return e.messageData[0]===65&&e.messageData[2]===66&&e.messageData[3]===18&&e.messageData[4]===64&&(e.messageData[5]&16)!==0&&e.messageData[6]===21}function en(e){return e.messageData[0]===65&&e.messageData[2]===66&&e.messageData[6]===127}function tn(e){return e.messageData[0]===126&&e.messageData[2]===9&&e.messageData[3]===1}function nn(e){return e.messageData[0]===126&&e.messageData[2]===9&&e.messageData[3]===3}var on=64,Ps=121;function Vs(e){return e==="gm2"?Ps:0}function jA(e){return e===120||e===126||e===127}function sn(e){return jA(e)||e===on||e===Ps}function Et(e,A,t,n,s,o){let r=e,C=0;if(n)FA(t)?sn(A)||(r=A):t==="gm2"&&(r=A);else{let i=!0;switch(t){case"gm":p(`%cIgnoring the Bank Select (${A}), as the synth is in GM mode.`,I.info),i=!1;break;case"xg":i=sn(A),jA(A)?C=2:o%16!==9&&(C=1);break;case"gm2":A===120?C=2:o%16!==9&&(C=1)}s&&(A=128),A===128&&!s&&(A=e),i&&(r=A)}return{newBank:r,drumsStatus:C}}function Bt(e,A,t,n){return n?t?jA(e)?e:128:sn(e)||A===0&&e!==0?e:sn(A)?0:A:t?128:e}function FA(e){return e==="gm2"||e==="xg"}function qn(e){return new VA(e,w.systemExclusive,new b([65,16,66,18,64,0,127,0,65,247]))}function ht(e,A,t,n){return new VA(n,w.controllerChange|e%16,new b([A,t]))}function Bi(e,A){let t=16|[1,2,3,4,5,6,7,8,0,9,10,11,12,13,14,15][e%16],n=[65,16,66,18,64,t,21,1],o=128-(64+t+21+1)%128;return new VA(A,w.systemExclusive,new b([...n,o,247]))}function Zs(e=[],A=[],t=[],n=[]){let s=this;pA("%cApplying changes to the MIDI file...",I.info),p("Desired program changes:",e),p("Desired CC changes:",A),p("Desired channels to clear:",t),p("Desired channels to transpose:",n);let o=new Set;e.forEach(x=>{o.add(x.channel)});let r="gs",C=!1,i=Array(s.tracks.length).fill(0),g=s.tracks.length;function c(){let x=0,G=1/0;return s.tracks.forEach((F,E)=>{i[E]>=F.length||F[i[E]].ticks{f(G,x)});let m=B,S=Array(m).fill(!0),M=Array(m).fill(0),D=Array(m).fill(0);for(n.forEach(x=>{let G=Math.trunc(x.keyShift),F=x.keyShift-G;M[x.channel]=G,D[x.channel]=F});g>0;){let x=c(),G=s.tracks[x];if(i[x]>=G.length){g--;continue}let F=i[x]++,E=G[F],L=()=>{G.splice(F,1),i[x]--},Z=(BA,sA=0)=>{G.splice(F+sA,0,BA),i[x]++},EA=d[h[x]]||0;if(E.messageStatusByte===w.midiPort){f(x,E.messageData[0]);continue}if(E.messageStatusByte<=w.sequenceSpecific&&E.messageStatusByte>=w.sequenceNumber)continue;let tA=E.messageStatusByte&240,T=E.messageStatusByte&15,$=T+EA;if(t.indexOf($)!==-1){L();continue}switch(tA){case w.noteOn:if(S[$]){S[$]=!1,A.filter(CA=>CA.channel===$).forEach(CA=>{let v=ht(T,CA.controllerNumber,CA.controllerValue,E.ticks);Z(v)});let oA=D[$];if(oA!==0){let CA=oA*64+64,v=ht(T,y.RPNMsb,0,E.ticks),J=ht(T,y.RPNLsb,1,E.ticks),W=ht($,y.dataEntryMsb,CA,E.ticks),O=ht(T,y.lsbForControl6DataEntry,0,E.ticks);Z(O),Z(W),Z(J),Z(v)}if(o.has($)){let CA=e.find(rA=>rA.channel===$),v=Math.max(0,Math.min(CA.bank,127)),J=CA.program;p(`%cSetting %c${CA.channel}%c to %c${v}:${J}%c. Track num: %c${x}`,I.info,I.recognized,I.info,I.recognized,I.info,I.recognized);let W=new VA(E.ticks,w.programChange|T,new b([J]));Z(W);let O=(rA,KA)=>{let Ie=ht(T,rA?y.lsbForControl0BankSelect:y.bankSelect,KA,E.ticks);Z(Ie)};FA(r)?CA.isDrum?(p(`%cAdding XG Drum change on track %c${x}`,I.recognized,I.value),O(!1,jA(v)?v:127),O(!0,0)):v===on?(O(!1,on),O(!0,0)):(O(!1,0),O(!0,v)):(O(!1,v),CA.isDrum&&T!==9&&(p(`%cAdding GS Drum change on track %c${x}`,I.recognized,I.value),Z(Bi(T,E.ticks))))}}E.messageData[0]+=M[$];break;case w.noteOff:E.messageData[0]+=M[$];break;case w.programChange:if(o.has($)){L();continue}break;case w.controllerChange:let BA=E.messageData[0];if(A.find(oA=>oA.channel===$&&BA===oA.controllerNumber)!==void 0){L();continue}if((BA===y.bankSelect||BA===y.lsbForControl0BankSelect)&&o.has($)){L();continue}break;case w.systemExclusive:if(Ct(E))p("%cXG system on detected",I.info),r="xg",C=!0;else if(E.messageData[0]===67&&E.messageData[2]===76&&E.messageData[3]===8&&E.messageData[5]===3)o.has(E.messageData[4]+EA)&&L();else if(en(E)){C=!0,p("%cGS on detected!",I.recognized);break}else(tn(E)||nn(E))&&(p("%cGM/2 on detected, removing!",I.info),L(),C=!1)}}if(!C&&e.length>0){let x=0;s.tracks[0][0].messageStatusByte===w.trackName&&x++,s.tracks[0].splice(x,0,qn(0)),p("%cGS on not detected. Adding it.",I.info)}this.flush(),P()}function Xs(e){let A=[],t=[],n=[],s=[];e.channelSnapshots.forEach((o,r)=>{if(o.isMuted){t.push(r);return}let C=o.channelTransposeKeyShift+o.customControllers[cA.channelTransposeFine]/100;C!==0&&A.push({channel:r,keyShift:C}),o.lockPreset&&n.push({channel:r,program:o.program,bank:o.bank,isDrum:o.drumChannel}),o.lockedControllers.forEach((i,g)=>{if(!i||g>127||g===y.bankSelect)return;let c=o.midiControllers[g]>>7;s.push({channel:r,controllerNumber:g,controllerValue:c})})}),this.modifyMIDI(n,s,t,A)}var uA={name:"INAM",album:"IPRD",album2:"IALB",artist:"IART",genre:"IGNR",picture:"IPIC",copyright:"ICOP",creationDate:"ICRD",comment:"ICMT",engineer:"IENG",software:"ISFT",encoding:"IENC",midiEncoding:"MENC",bankOffset:"DBNK"},Le="utf-8",hi="Created using SpessaSynth";function Ws(e,A,t=0,n="Shift_JIS",s={},o=!0){let r=this;if(se("%cWriting the RMIDI File...",I.info),p(`%cConfiguration: Bank offset: %c${t}%c, encoding: %c${n}`,I.info,I.value,I.info,I.value),p("metadata",s),p("Initial bank offset",r.bankOffset),o){let M=function(){let F=0,E=1/0;return r.tracks.forEach((L,Z)=>{m[Z]>=L.length||L[m[Z]].ticksE>F?E:F),G=[];for(let F=0;F0;){let F=M(),E=r.tracks[F];if(m[F]>=E.length){S--;continue}let L=E[m[F]];m[F]++;let Z=r.midiPortChannelOffsets[D[F]];if(L.messageStatusByte===w.midiPort){D[F]=L.messageData[0];continue}let EA=L.messageStatusByte&240;if(EA!==w.controllerChange&&EA!==w.programChange&&EA!==w.systemExclusive)continue;if(EA===w.systemExclusive){if(!An(L)){Ct(L)?B="xg":en(L)?B="gs":tn(L)?(B="gm",f.push({tNum:F,e:L})):nn(L)&&(B="gm2");continue}let oA=[9,0,1,2,3,4,5,6,7,8,10,11,12,13,14,15][L.messageData[5]&15]+Z;G[oA].drums=!!(L.messageData[7]>0&&L.messageData[5]>>4);continue}let tA=(L.messageStatusByte&15)+Z,T=G[tA];if(EA===w.programChange){let oA=FA(B),CA=L.messageData[0];T.drums?A.presets.findIndex(O=>O.program===CA&&O.isDrumPreset(oA,!0))===-1&&(L.messageData[0]=A.presets.find(O=>O.isDrumPreset(oA))?.program||0,p(`%cNo drum preset %c${CA}%c. Channel %c${tA}%c. Changing program to ${L.messageData[0]}.`,I.info,I.unrecognized,I.info,I.recognized,I.info)):A.presets.findIndex(O=>O.program===CA&&!O.isDrumPreset(oA))===-1&&(L.messageData[0]=A.presets.find(O=>!O.isDrumPreset(oA))?.program||0,p(`%cNo preset %c${CA}%c. Channel %c${tA}%c. Changing program to ${L.messageData[0]}.`,I.info,I.unrecognized,I.info,I.recognized,I.info)),T.program=L.messageData[0];let v=Math.max(0,T.lastBank?.messageData[1]-r.bankOffset),J=T?.lastBankLSB?.messageData[1]-r.bankOffset||0;if(T.lastBank===void 0)continue;let W=Bt(v,J,T.drums,oA);if(A.presets.findIndex(O=>O.bank===W&&O.program===L.messageData[0])===-1){let O=A.presets.find(rA=>rA.program===L.messageData[0])?.bank+t||t;T.lastBank.messageData[1]=O,T?.lastBankLSB?.messageData&&(T.lastBankLSB.messageData[1]=O),p(`%cNo preset %c${W}:${L.messageData[0]}%c. Channel %c${tA}%c. Changing bank to ${O}.`,I.info,I.unrecognized,I.info,I.recognized,I.info)}else{let O=W;FA(B)&&W===128&&(W=127);let rA=(W===128?128:O)+t;T.lastBank.messageData[1]=rA,T?.lastBankLSB?.messageData&&!T.drums&&(T.lastBankLSB.messageData[1]=T.lastBankLSB.messageData[1]-r.bankOffset+t),p(`%cPreset %c${W}:${L.messageData[0]}%c exists. Channel %c${tA}%c. Changing bank to ${rA}.`,I.info,I.recognized,I.info,I.recognized,I.info)}continue}let $=L.messageData[0]===y.lsbForControl0BankSelect;if(L.messageData[0]!==y.bankSelect&&!$)continue;T.hasBankSelect=!0;let BA=L.messageData[1],sA=Et(T?.lastBank?.messageData[1]||0,BA,B,$,T.drums,tA);sA.drumsStatus===2?T.drums=!0:sA.drumsStatus===1&&(T.drums=!1),$?T.lastBankLSB=L:T.lastBank=L}if(G.forEach((F,E)=>{if(F.hasBankSelect===!0)return;let L=E%16,Z=w.programChange|L,EA=Math.floor(E/16)*16,tA=r.midiPortChannelOffsets.indexOf(EA),T=r.tracks.find((oA,CA)=>r.midiPorts[CA]===tA&&r.usedChannelsOnTrack[CA].has(L));if(T===void 0)return;let $=T.findIndex(oA=>oA.messageStatusByte===Z);if($===-1){let oA=T.findIndex(J=>J.messageStatusByte>128&&J.messageStatusByte<240&&(J.messageStatusByte&15)===L);if(oA===-1)return;let CA=T[oA].ticks,v=A.getPreset(0,0).program;T.splice(oA,0,new VA(CA,w.programChange|L,new b([v]))),$=oA}p(`%cAdding bank select for %c${E}`,I.info,I.recognized);let BA=T[$].ticks,sA=A.getPreset(0,F.program,FA(B))?.bank+t||t;T.splice($,0,new VA(BA,w.controllerChange|L,new b([y.bankSelect,sA])))}),B!=="gs"&&!FA(B)){for(let E of f)r.tracks[E.tNum].splice(r.tracks[E.tNum].indexOf(E.e),1);let F=0;r.tracks[0][0].messageStatusByte===w.trackName&&F++,r.tracks[0].splice(F,0,qn(0))}}let C=new b(r.writeMIDI().buffer),i=[gt("INFO")],g=new TextEncoder;if(i.push(_(uA.software,g.encode("SpessaSynth"),!0)),s.name!==void 0?(i.push(_(uA.name,g.encode(s.name),!0)),n=Le):i.push(_(uA.name,r.rawMidiName,!0)),s.creationDate!==void 0)n=Le,i.push(_(uA.creationDate,g.encode(s.creationDate),!0));else{let B=new Date().toLocaleString(void 0,{weekday:"long",year:"numeric",month:"long",day:"numeric",hour:"numeric",minute:"numeric"});i.push(_(uA.creationDate,fe(B),!0))}if(s.comment!==void 0&&(n=Le,i.push(_(uA.comment,g.encode(s.comment)))),s.engineer!==void 0&&i.push(_(uA.engineer,g.encode(s.engineer),!0)),s.album!==void 0&&(n=Le,i.push(_(uA.album,g.encode(s.album),!0)),i.push(_(uA.album2,g.encode(s.album),!0))),s.artist!==void 0&&(n=Le,i.push(_(uA.artist,g.encode(s.artist),!0))),s.genre!==void 0&&(n=Le,i.push(_(uA.genre,g.encode(s.genre),!0))),s.picture!==void 0&&i.push(_(uA.picture,new Uint8Array(s.picture))),s.copyright!==void 0)n=Le,i.push(_(uA.copyright,g.encode(s.copyright),!0));else{let B=r.copyright.length>0?r.copyright:hi;i.push(_(uA.copyright,fe(B)))}let c=new b(2);Xe(c,t,2),i.push(_(uA.bankOffset,c)),s.midiEncoding!==void 0&&(i.push(_(uA.midiEncoding,g.encode(s.midiEncoding))),n=Le),i.push(_(uA.encoding,fe(n)));let h=wA(i),d=wA([gt("RMID"),_("data",C),_("LIST",h),e]);return p("%cFinished!",I.info),P(),_("RIFF",d)}function _s(){let e=this;if(!e.tracks)throw new Error("MIDI has no tracks!");let A=[];for(let s of e.tracks){let o=[],r=0,C;for(let i of s){let g=i.ticks-r,c;i.messageStatusByte<=w.sequenceSpecific?c=[255,i.messageStatusByte,..._t(i.messageData.length),...i.messageData]:i.messageStatusByte===w.systemExclusive?c=[240,..._t(i.messageData.length),...i.messageData]:(c=[],C!==i.messageStatusByte&&(C=i.messageStatusByte,c.push(i.messageStatusByte)),c.push(...i.messageData)),o.push(..._t(g)),o.push(...c),r+=g}A.push(new Uint8Array(o))}function t(s,o){for(let r=0;rd>h?d:h),n=[];for(let h=0;h{C[f]>=B.length||B[C[f]].ticks{o(h)});i>0;){let h=g(),d=A.tracks[h];if(C[h]>=d.length){i--;continue}let B=d[C[h]];if(C[h]++,B.messageStatusByte===w.midiPort){c[h]=B.messageData[0];continue}let f=B.messageStatusByte&240;if(f!==w.noteOn&&f!==w.controllerChange&&f!==w.programChange&&f!==w.systemExclusive)continue;let m=(B.messageStatusByte&15)+A.midiPortChannelOffsets[c[h]]||0,S=n[m];switch(f){case w.programChange:S.program=B.messageData[0],o(S);break;case w.controllerChange:let M=B.messageData[0]===y.lsbForControl0BankSelect;if(B.messageData[0]!==y.bankSelect&&!M||s==="gs"&&S.drums)continue;let D=B.messageData[1],x=Math.max(0,D-A.bankOffset);switch(M?S.bankLSB=x:S.bank=x,Et(S.bank,x,s,M,S.drums,m).drumsStatus){case 0:break;case 1:S.drums=!1,o(S);break;case 2:S.drums=!0,o(S);break}break;case w.noteOn:if(B.messageData[1]===0)continue;r[S.string].add(`${B.messageData[0]}-${B.messageData[1]}`);break;case w.systemExclusive:if(!An(B)){Ct(B)&&(s="xg",p("%cXG on detected!",I.recognized));continue}let F=[9,0,1,2,3,4,5,6,7,8,10,11,12,13,14,15][B.messageData[5]&15]+A.midiPortChannelOffsets[c[h]],E=!!(B.messageData[7]>0&&B.messageData[5]>>4);S=n[F],S.drums=E,o(S);break}}for(let h of Object.keys(r))r[h].size===0&&(p(`%cDetected change but no keys for %c${h}`,I.info,I.value),delete r[h]);return P(),r}var ge=class e extends It{embeddedSoundFont=void 0;tracks=[];isDLSRMIDI=!1;static copyFrom(A){let t=new e;return t._copyFromSequence(A),t.isDLSRMIDI=A.isDLSRMIDI,t.embeddedSoundFont=A.embeddedSoundFont?A.embeddedSoundFont.slice(0):void 0,t.tracks=A.tracks.map(n=>[...n]),t}_parseInternal(){se("%cInterpreting MIDI events...",I.info);let A=!1;this.keyRange={max:0,min:127};let t=[],n=!1;typeof this.RMIDInfo.ICOP<"u"&&(n=!0);let s=!1;typeof this.RMIDInfo.INAM<"u"&&(s=!0);let o=null,r=null;for(let c=0;c=128&&m.messageStatusByte<240){B=!0;for(let M=0;Mthis.lastVoiceEventTick&&(this.lastVoiceEventTick=m.ticks),m.messageStatusByte&240){case w.controllerChange:switch(m.messageData[0]){case 2:case 116:o=m.ticks;break;case 4:case 117:r===null?r=m.ticks:r=0;break;case 0:this.isDLSRMIDI&&m.messageData[1]!==0&&m.messageData[1]!==127&&(p("%cDLS RMIDI with offset 1 detected!",I.recognized),this.bankOffset=1)}break;case w.noteOn:d.add(m.messageStatusByte&15);let M=m.messageData[0];this.keyRange.min=Math.min(this.keyRange.min,M),this.keyRange.max=Math.max(this.keyRange.max,M);break}}m.messageData.currentIndex=0;let S=eA(m.messageData,m.messageData.length);switch(m.messageData.currentIndex=0,m.messageStatusByte){case w.setTempo:m.messageData.currentIndex=0,this.tempoChanges.push({ticks:m.ticks,tempo:6e7/oe(m.messageData,3)}),m.messageData.currentIndex=0;break;case w.marker:switch(S.trim().toLowerCase()){default:break;case"start":case"loopstart":o=m.ticks;break;case"loopend":r=m.ticks}m.messageData.currentIndex=0;break;case w.copyright:n||(m.messageData.currentIndex=0,t.push(eA(m.messageData,m.messageData.length,void 0,!1)),m.messageData.currentIndex=0);break;case w.lyric:if(S.trim().startsWith("@KMIDI KARAOKE FILE")&&(this.isKaraokeFile=!0,p("%cKaraoke MIDI detected!",I.recognized)),this.isKaraokeFile)m.messageStatusByte=w.text;else{this.lyrics.push(m.messageData),this.lyricsTicks.push(m.ticks);break}case w.text:let D=S.trim();D.startsWith("@KMIDI KARAOKE FILE")?(this.isKaraokeFile=!0,p("%cKaraoke MIDI detected!",I.recognized)):this.isKaraokeFile&&(D.startsWith("@T")||D.startsWith("@A")?A?t.push(D.substring(2).trim()):(this.midiName=D.substring(2).trim(),A=!0,s=!0,this.rawMidiName=gt(this.midiName)):D[0]!=="@"&&(this.lyrics.push(Gs(m.messageData)),this.lyricsTicks.push(m.ticks)));break;case w.trackName:break}}this.usedChannelsOnTrack.push(d),this.trackNames[c]="";let f=h.find(m=>m.messageStatusByte===w.trackName);if(f){f.messageData.currentIndex=0;let m=eA(f.messageData,f.messageData.length);this.trackNames[c]=m,B||t.push(m)}}this.tempoChanges.reverse(),p("%cCorrecting loops, ports and detecting notes...",I.info);let C=[];for(let c of this.tracks){let h=c.find(d=>(d.messageStatusByte&240)===w.noteOn);h&&C.push(h.ticks)}this.firstNoteOn=Math.min(...C),p(`%cFirst note-on detected at: %c${this.firstNoteOn}%c ticks!`,I.info,I.recognized,I.info),o!==null&&r===null?(o=this.firstNoteOn,r=this.lastVoiceEventTick):(o===null&&(o=this.firstNoteOn),(r===null||r===0)&&(r=this.lastVoiceEventTick)),this.loop={start:o,end:r},p(`%cLoop points: start: %c${this.loop.start}%c end: %c${this.loop.end}`,I.info,I.recognized,I.info,I.recognized);let i=0;this.midiPorts=[],this.midiPortChannelOffsets=[];for(let c=0;cc&&(g=c);if(g===1/0&&(g=0),this.midiPorts=this.midiPorts.map(c=>c===-1?g:c),this.midiPortChannelOffsets.length===0&&(this.midiPortChannelOffsets=[0]),this.midiPortChannelOffsets.length<2?p("%cNo additional MIDI Ports detected.",I.info):(this.isMultiPort=!0,p("%cMIDI Ports detected!",I.recognized)),!s)if(this.tracks.length>1){if(this.tracks[0].find(c=>c.messageStatusByte>=w.noteOn&&c.messageStatusByteh.messageStatusByte===w.trackName);c&&(this.rawMidiName=c.messageData,c.messageData.currentIndex=0,this.midiName=eA(c.messageData,c.messageData.length,void 0,!1))}}else{let c=this.tracks[0].find(h=>h.messageStatusByte===w.trackName);c&&(this.rawMidiName=c.messageData,c.messageData.currentIndex=0,this.midiName=eA(c.messageData,c.messageData.length,void 0,!1))}if(n||(this.copyright=t.map(c=>c.trim().replace(/(\r?\n)+/g,` +`)).filter(c=>c.length>0).join(` +`)||""),this.midiName=this.midiName.trim(),this.midiNameUsesFileName=!1,this.midiName.length===0){p("%cNo name detected. Using the alt name!",I.info),this.midiName=Rs(this.fileName),this.midiNameUsesFileName=!0,this.rawMidiName=new Uint8Array(this.midiName.length);for(let c=0;cc[0].ticks===0)||this.tracks[0].unshift(new VA(0,w.trackName,new b(this.rawMidiName.buffer))),this.duration=this.MIDIticksToSeconds(this.lastVoiceEventTick),p("%cSuccess!",I.recognized),P()}flush(){for(let A of this.tracks)A.sort((t,n)=>t.ticks-n.ticks);this._parseInternal()}};ge.prototype.writeMIDI=_s;ge.prototype.modifyMIDI=Zs;ge.prototype.applySnapshotToMIDI=Xs;ge.prototype.writeRMIDI=Ws;ge.prototype.getUsedProgramsAndKeys=zs;var Pn;(()=>{var e=Uint8Array,A=Uint16Array,t=Int32Array,n=new e([0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0]),s=new e([0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0]),o=new e([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),r=function(v,J){for(var W=new A(31),O=0;O<31;++O)W[O]=J+=1<>1|(D&21845)<<1,f=(f&52428)>>2|(f&13107)<<2,f=(f&61680)>>4|(f&3855)<<4,B[D]=((f&65280)>>8|(f&255)<<8)>>1;var f,D,m=function(v,J,W){for(var O=v.length,rA=0,KA=new A(J);rA>le]=$e}else for(ce=new A(O),rA=0;rA>15-v[rA]);return ce},S=new e(288);for(D=0;D<144;++D)S[D]=8;var D;for(D=144;D<256;++D)S[D]=9;var D;for(D=256;D<280;++D)S[D]=7;var D;for(D=280;D<288;++D)S[D]=8;var D,M=new e(32);for(D=0;D<32;++D)M[D]=5;var D,x=m(S,9,1),G=m(M,5,1),F=function(v){for(var J=v[0],W=1;WJ&&(J=v[W]);return J},E=function(v,J,W){var O=J/8|0;return(v[O]|v[O+1]<<8)>>(J&7)&W},L=function(v,J){var W=J/8|0;return(v[W]|v[W+1]<<8|v[W+2]<<16)>>(J&7)},Z=function(v){return(v+7)/8|0},EA=function(v,J,W){return(J==null||J<0)&&(J=0),(W==null||W>v.length)&&(W=v.length),new e(v.subarray(J,W))},tA=["unexpected EOF","invalid block type","invalid length/literal","invalid distance","stream finished","no stream handler",,"no callback","invalid UTF-8 data","extra field too long","date not in range 1980-2099","filename too long","stream finishing","invalid zip data"],T=function(v,J,W){var O=new Error(J||tA[v]);if(O.code=v,Error.captureStackTrace&&Error.captureStackTrace(O,T),!W)throw O;return O},$=function(v,J,W,O){var rA=v.length,KA=O?O.length:0;if(!rA||J.f&&!J.l)return W||new e(0);var Ie=!W,ce=Ie||J.i!=2,le=J.i;Ie&&(W=new e(rA*3));var $e=function(Ot){var Me=W.length;if(Ot>Me){var St=new e(Math.max(Me*2,Ot));St.set(W),W=St}},pe=J.f||0,aA=J.p||0,lA=J.b||0,Qe=J.l,XA=J.d,Re=J.m,Ge=J.n,ut=rA*8;do{if(!Qe){pe=E(v,aA,1);var At=E(v,aA+1,3);if(aA+=3,At)if(At==1)Qe=x,XA=G,Re=9,Ge=5;else if(At==2){var ft=E(v,aA,31)+257,kn=E(v,aA+10,15)+4,wn=ft+E(v,aA+5,31)+1;aA+=14;for(var et=new e(wn),xe=new e(19),WA=0;WA>4;if(OA<16)et[WA++]=OA;else{var ye=0,pt=0;for(OA==16?(pt=3+E(v,aA,3),aA+=2,ye=et[WA-1]):OA==17?(pt=3+E(v,aA,7),aA+=3):OA==18&&(pt=11+E(v,aA,127),aA+=7);pt--;)et[WA++]=ye}}var Rn=et.subarray(0,ft),Se=et.subarray(ft);Re=F(Rn),Ge=F(Se),Qe=m(Rn,Re,1),XA=m(Se,Ge,1)}else T(1);else{var OA=Z(aA)+4,Yt=v[OA-4]|v[OA-3]<<8,Jt=OA+Yt;if(Jt>rA){le&&T(0);break}ce&&$e(lA+Yt),W.set(v.subarray(OA,Jt),lA),J.b=lA+=Yt,J.p=aA=Jt*8,J.f=pe;continue}if(aA>ut){le&&T(0);break}}ce&&$e(lA+131072);for(var ds=(1<>4;if(aA+=ye&15,aA>ut){le&&T(0);break}if(ye||T(2),qe<256)W[lA++]=qe;else if(qe==256){Kt=aA,Qe=null;break}else{var Gn=qe-254;if(qe>264){var WA=qe-257,De=n[WA];Gn=E(v,aA,(1<>4;nt||T(3),aA+=nt&15;var Se=h[Pe];if(Pe>3){var De=s[Pe];Se+=L(v,aA)&(1<ut){le&&T(0);break}ce&&$e(lA+131072);var yt=lA+Gn;if(lAVn[m]===i);else{let m=yA(C);i=eA(C,m),g=i}let f=yA(C);if(f===0){let m=yA(C),S=C.slice(C.currentIndex,C.currentIndex+m);C.currentIndex+=m,yA(S)<4?this.metadata[g]=eA(S,m-1):this.metadata[g]=S.slice(S.currentIndex)}else Y(`International content: ${f}`),C.currentIndex+=yA(C)}let c=r.currentIndex,h=yA(r),d=r.slice(r.currentIndex,c+h);if(r.currentIndex=c+h,h>0)for(this.packedContent=!0;d.currentIndexZn[S]===m)}}else for(this.resourceFormat="folder";this.nodeData.currentIndex{let g=(c,h)=>{i.metadata[c]!==void 0&&typeof i.metadata[c]=="string"&&(e.RMIDInfo[h]=i.metadata[c])};if(g("nodeName",uA.name),g("title",uA.name),g("copyrightNotice",uA.copyright),g("comment",uA.comment),i.isFile)switch(i.resourceFormat){default:return;case"DLS1":case"DLS2":case"DLS22":case"mobileDLS":p("%cFound embedded DLS!",I.recognized),e.embeddedSoundFont=i.nodeData.buffer;break;case"StandardMIDIFile":case"StandardMIDIFileType1":p("%cFound embedded MIDI!",I.recognized),r=i.nodeData;break}else for(let c of i.innerNodes)C(c)};return C(o),P(),r}var an=class extends ge{constructor(A,t=""){super(),pA("%cParsing MIDI File...",I.info),this.fileName=t;let n=new b(A),s,o=eA(n,4);if(n.currentIndex-=4,o==="RIFF"){n.currentIndex+=8;let C=eA(n,4,void 0,!1);if(C!=="RMID")throw P(),new SyntaxError(`Invalid RMIDI Header! Expected "RMID", got "${C}"`);let i=IA(n);if(i.header!=="data")throw P(),new SyntaxError(`Invalid RMIDI Chunk header! Expected "data", got "${C}"`);for(s=i.chunkData;n.currentIndex<=n.length;){let g=n.currentIndex,c=IA(n,!0);if(c.header==="RIFF"){let h=eA(c.chunkData,4).toLowerCase();h==="sfbk"||h==="sfpk"||h==="dls "?(p("%cFound embedded soundfont!",I.recognized),this.embeddedSoundFont=n.slice(g,g+c.size).buffer):Y(`Unknown RIFF chunk: "${h}"`),h==="dls "&&(this.isDLSRMIDI=!0)}else if(c.header==="LIST"&&eA(c.chunkData,4)==="INFO"){for(p("%cFound RMIDI INFO chunk!",I.recognized),this.RMIDInfo={};c.chunkData.currentIndex<=c.size;){let d=IA(c.chunkData,!0);this.RMIDInfo[d.header]=d.chunkData}this.RMIDInfo.ICOP&&(this.copyright=eA(this.RMIDInfo.ICOP,this.RMIDInfo.ICOP.length,void 0,!1).replaceAll(` +`," ")),this.RMIDInfo.INAM&&(this.rawMidiName=this.RMIDInfo[uA.name],this.midiName=eA(this.rawMidiName,this.rawMidiName.length,void 0,!1).replaceAll(` +`," ")),this.RMIDInfo.IALB&&!this.RMIDInfo.IPRD&&(this.RMIDInfo.IPRD=this.RMIDInfo.IALB),this.RMIDInfo.IPRD&&!this.RMIDInfo.IALB&&(this.RMIDInfo.IALB=this.RMIDInfo.IPRD),this.bankOffset=1,this.RMIDInfo[uA.bankOffset]&&(this.bankOffset=N(this.RMIDInfo[uA.bankOffset],2))}}this.isDLSRMIDI&&(this.bankOffset=0),this.embeddedSoundFont===void 0&&(this.bankOffset=0)}else o==="XMF_"?s=js(this,n):s=n;let r=this._readMIDIChunk(s);if(r.type!=="MThd")throw P(),new SyntaxError(`Invalid MIDI Header! Expected "MThd", got "${r.type}"`);if(r.size!==6)throw P(),new RangeError(`Invalid MIDI header chunk size! Expected 6, got ${r.size}`);this.format=oe(r.data,2),this.tracksAmount=oe(r.data,2),this.timeDivision=oe(r.data,2);for(let C=0;C0&&(h+=this.tracks[C-1][this.tracks[C-1].length-1].ticks);g.data.currentIndex>4],c=B;break}let S=new b(m);S.set(g.data.slice(g.data.currentIndex,g.data.currentIndex+m),0);let M=new VA(h,B,S);i.push(M),g.data.currentIndex+=m}this.tracks.push(i),p(`%cParsed %c${this.tracks.length}%c / %c${this.tracksAmount}`,I.info,I.value,I.info,I.value)}p("%cAll tracks parsed correctly!",I.recognized),this._parseInternal(),P(),p(`%cMIDI file parsed. Total tick time: %c${this.lastVoiceEventTick}%c, total seconds time: %c${this.duration}`,I.info,I.recognized,I.info,I.recognized)}_readMIDIChunk(A){let t={};t.type=eA(A,4),t.size=oe(A,4),t.data=new b(t.size);let n=A.slice(A.currentIndex,A.currentIndex+t.size);return t.data.set(n,0),A.currentIndex+=t.size,t}};function $s(e,A){this.midiData.usedChannelsOnTrack[e].size!==0&&(this.midiPortChannelOffset===0&&(this.midiPortChannelOffset+=16,this.midiPortChannelOffsets[A]=0),this.midiPortChannelOffsets[A]===void 0&&(this.synth.workletProcessorChannels.length{this.assignMIDIPort(n,t)}),this.duration=this.midiData.duration,this.firstNoteTime=this.midiData.MIDIticksToSeconds(this.midiData.firstNoteOn),p(`%cTotal song time: ${Hn(Math.ceil(this.duration)).time}`,I.recognized),this.post(UA.songChange,[this.songIndex,A]),this.duration<=1&&(Y(`%cVery short song: (${Hn(Math.round(this.duration)).time}). Disabling loop!`,I.warn),this.loop=!1),A)this.play(!0);else{let t=this.skipToFirstNoteOn?this.midiData.firstNoteOn-1:0;this.setTimeTicks(t),this.pause()}}function eo(e,A=!0){if(this.songs=e.reduce((n,s)=>{if(s.duration)return n.push(ge.copyFrom(s)),n;try{n.push(new an(s.binary,s.altName||""))}catch(o){return console.error(o),this.post(UA.midiError,o),n}return n},[]),this.songs.length<1)return;this.songIndex=0,this.songs.length>1&&(this.loop=!1),this.shuffleSongIndexes();let t=this.songs.map(n=>new Wt(n));this.post(UA.songListChange,t),this.loadCurrentSong(A)}function to(){if(this.songs.length===1){this.currentTime=0;return}this.songIndex++,this.songIndex%=this.songs.length,this.loadCurrentSong()}function no(){if(this.songs.length===1){this.currentTime=0;return}this.songIndex--,this.songIndex<0&&(this.songIndex=this.songs.length-1),this.loadCurrentSong()}function so(e=!0){e&&p("%cResetting all controllers!",I.info),this.callEvent("allcontrollerreset",void 0),this.setSystem($t);for(let A=0;A>7});if(this.workletProcessorChannels[A].lockedControllers[xA+j.pitchWheel]===!1){let o=this.workletProcessorChannels[A].midiControllers[xA+j.pitchWheel],r=o>>7,C=o&127;this.callEvent("pitchwheel",{channel:A,MSB:r,LSB:C})}}this.tunings=[],this.tunings=[];for(let A=0;127>A;A++)this.tunings.push([]);this.setMIDIVolume(1)}function oo(){this.channelOctaveTuning.fill(0);for(let A=0;A>7):this.midiControllers[A]=t}this.channelVibrato={rate:0,depth:0,delay:0},this.holdPedal=!1,this.randomPan=!1;let e=this.customControllers[cA.channelTransposeFine];this.customControllers.set(On),this.setCustomController(cA.channelTransposeFine,e),this.resetParameters()}var Wn=new Set([y.bankSelect,y.lsbForControl0BankSelect,y.mainVolume,y.lsbForControl7MainVolume,y.pan,y.lsbForControl10Pan,y.reverbDepth,y.tremoloDepth,y.chorusDepth,y.detuneDepth,y.phaserDepth,y.soundVariation,y.filterResonance,y.releaseTime,y.attackTime,y.brightness,y.decayTime,y.vibratoRate,y.vibratoDepth,y.vibratoDelay,y.soundController10]);function ro(){this.channelOctaveTuning.fill(0),this.pitchWheel(64,0),this.channelVibrato={rate:0,depth:0,delay:0};for(let e=0;e<128;e++){let A=be[e];!Wn.has(e)&&A!==this.midiControllers[e]&&(e===y.portamentoControl?this.midiControllers[e]=jt:this.controllerChange(e,A>>7))}}function io(){this.dataEntryState=YA.Idle,p("%cResetting Registered and Non-Registered Parameters!",I.info)}var We=be.slice(0,128);function ao(e,A=void 0){this.oneTickToSeconds=60/(120*this.midiData.timeDivision),this.synth.resetAllControllers(),this.sendMIDIReset(),this._resetTimers();let t=this.synth.workletProcessorChannels.length,n=Array(t).fill(8192),s=[];for(let i=0;ii===y.dataDecrement||i===y.dataIncrement||i===y.dataEntryMsb||i===y.dataDecrement||i===y.lsbForControl6DataEntry||i===y.RPNLsb||i===y.RPNMsb||i===y.NRPNLsb||i===y.NRPNMsb||i===y.bankSelect||i===y.lsbForControl0BankSelect||i===y.resetAllControllers,r=[];for(let i=0;i=A)break}else if(this.playedTime>=e)break;let c=at(g.messageStatusByte),h=c.channel+(this.midiPortChannelOffsets[this.midiPorts[i]]||0);switch(c.status){case w.noteOn:r[h]===void 0&&(r[h]=Array.from(We)),r[h][y.portamentoControl]=g.messageData[0];break;case w.noteOff:break;case w.pitchBend:n[h]=g.messageData[1]<<7|g.messageData[0];break;case w.programChange:if(this.midiData.isMultiPort&&this.midiData.usedChannelsOnTrack[i].size===0)break;let B=s[h];B.program=g.messageData[0],B.actualBank=B.bank;break;case w.controllerChange:if(this.midiData.isMultiPort&&this.midiData.usedChannelsOnTrack[i].size===0)break;let f=g.messageData[0];if(o(f)){let m=g.messageData[1];if(f===y.bankSelect){s[h].bank=m;break}else f===y.resetAllControllers&&C(h);this.sendMIDIMessages?this.sendMIDICC(h,f,m):this.synth.controllerChange(h,f,m)}else r[h]===void 0&&(r[h]=Array.from(We)),r[h][f]=g.messageData[1];break;default:this._processEvent(g,i);break}this.eventIndex[i]++,i=this._findFirstEventIndex();let d=this.tracks[i][this.eventIndex[i]];if(d===void 0)return this.stop(),!1;this.playedTime+=this.oneTickToSeconds*(d.ticks-g.ticks)}if(this.sendMIDIMessages){for(let i=0;i>7,n[i]&127),r[i]!==void 0&&r[i].forEach((g,c)=>{g!==We[c]&&!o(c)&&this.sendMIDICC(i,c,g)}),s[i].program>=0&&s[i].actualBank>=0){let g=s[i].actualBank;this.sendMIDICC(i,y.bankSelect,g),this.sendMIDIProgramChange(i,s[i].program)}}else for(let i=0;i>7,n[i]&127),r[i]!==void 0&&r[i].forEach((g,c)=>{g!==We[c]&&!o(c)&&this.synth.controllerChange(i,c,g)}),s[i].program>=0&&s[i].actualBank>=0){let g=s[i].actualBank;this.synth.controllerChange(i,y.bankSelect,g),this.synth.programChange(i,s[i].program)}return!0}function Io(e=!1){if(this.midiData!==void 0){if(e){this.pausedTime=void 0,this.currentTime=0;return}if(this.currentTime>=this.duration){this.pausedTime=void 0,this.currentTime=0;return}this.paused&&(this._recalculateStartTime(this.pausedTime),this.pausedTime=void 0),this.sendMIDIMessages||this.playingNotes.forEach(A=>{this.synth.noteOn(A.channel,A.midiNote,A.velocity)}),this.setProcessHandler()}}function go(e){this.stop(),this.playingNotes=[],this.pausedTime=void 0,this.post(UA.timeChange,currentTime-this.midiData.MIDIticksToSeconds(e));let A=this._playTo(0,e);this._recalculateStartTime(this.playedTime),A&&this.play()}function Co(e){this.absoluteStartTime=currentTime-e/this._playbackRate}var RA={midiMessage:0,ccReset:7,setChannelVibrato:8,soundFontManager:9,stopAll:10,killNotes:11,muteChannel:12,addNewChannel:13,customcCcChange:14,debugMessage:15,setMasterParameter:17,setDrums:18,transpose:19,highPerformanceMode:20,lockController:21,sequencerSpecific:22,requestSynthesizerSnapshot:23,setLogLevel:24,keyModifierManager:25,setEffectsGain:26,destroyWorklet:27},Ue={mainVolume:0,masterPan:1,voicesCap:2,interpolationType:3,midiSystem:4},ie=-1,JA={channelPropertyChange:0,eventCall:1,masterParameterChange:2,sequencerSpecific:3,synthesizerSnapshot:4,isFullyInitialized:5,soundfontError:6};function Eo(e,A){switch(e){default:break;case zA.loadNewSongList:this.loadNewSongList(A[0],A[1]);break;case zA.pause:this.pause();break;case zA.play:this.play(A);break;case zA.stop:this.stop();break;case zA.setTime:this.currentTime=A;break;case zA.changeMIDIMessageSending:this.sendMIDIMessages=A;break;case zA.setPlaybackRate:this.playbackRate=A;break;case zA.setLoop:let[t,n]=A;this.loop=t,n===ie?this.loopCount=1/0:this.loopCount=n;break;case zA.changeSong:switch(A[0]){case it.forwards:this.nextSong();break;case it.backwards:this.previousSong();break;case it.shuffleOff:this.shuffleMode=!1,this.songIndex=this.shuffledSongIndexes[this.songIndex];break;case it.shuffleOn:this.shuffleMode=!0,this.shuffleSongIndexes(),this.songIndex=0,this.loadCurrentSong();break;case it.index:this.songIndex=A[1],this.loadCurrentSong();break}break;case zA.getMIDI:this.post(UA.getMIDI,this.midiData);break;case zA.setSkipToFirstNote:this.skipToFirstNoteOn=A;break;case zA.setPreservePlaybackState:this.preservePlaybackState=A}}function Bo(e,A=void 0){this.synth.enableEventSystem&&this.synth.post({messageType:JA.sequencerSpecific,messageData:{messageType:e,messageData:A}})}function ho(e){this.post(UA.midiEvent,e)}function co(e,A,t){e%=16,this.sendMIDIMessages&&this.sendMIDIMessage([w.controllerChange|e,A,t])}function lo(e,A){e%=16,this.sendMIDIMessages&&this.sendMIDIMessage([w.programChange|e,A])}function Qo(e,A,t){e%=16,this.sendMIDIMessages&&this.sendMIDIMessage([w.pitchBend|e,t,A])}function uo(){if(this.sendMIDIMessages){this.sendMIDIMessage([w.reset]);for(let e=0;e<16;e++)this.sendMIDIMessage([w.controllerChange|e,y.allSoundOff,0]),this.sendMIDIMessage([w.controllerChange|e,y.resetAllControllers,0])}}var DA=class{songs=[];songIndex=0;shuffledSongIndexes=[];synth;isActive=!1;sendMIDIMessages=!1;loopCount=1/0;eventIndex=[];playedTime=0;pausedTime=void 0;absoluteStartTime=currentTime;playingNotes=[];loop=!0;shuffleMode=!1;midiData=void 0;midiPorts=[];midiPortChannelOffset=0;midiPortChannelOffsets={};skipToFirstNoteOn=!0;preservePlaybackState=!1;constructor(A){this.synth=A}_playbackRate=1;set playbackRate(A){let t=this.currentTime;this._playbackRate=A,this.currentTime=t}get currentTime(){return this.pausedTime!==void 0?this.pausedTime:(currentTime-this.absoluteStartTime)*this._playbackRate}set currentTime(A){if(A>this.duration||A<0){this.skipToFirstNoteOn?this.setTimeTicks(this.midiData.firstNoteOn-1):this.setTimeTicks(0);return}if(this.skipToFirstNoteOn&&An);for(this.shuffledSongIndexes=[];A.length>0;){let t=A[Math.floor(Math.random()*A.length)];this.shuffledSongIndexes.push(t),A.splice(A.indexOf(t),1)}}};DA.prototype.sendMIDIMessage=ho;DA.prototype.sendMIDIReset=uo;DA.prototype.sendMIDICC=co;DA.prototype.sendMIDIProgramChange=lo;DA.prototype.sendMIDIPitchWheel=Qo;DA.prototype.assignMIDIPort=$s;DA.prototype.post=Bo;DA.prototype.processMessage=Eo;DA.prototype._processEvent=Us;DA.prototype._addNewMidiPort=Ts;DA.prototype.processTick=vs;DA.prototype._findFirstEventIndex=Hs;DA.prototype.loadNewSequence=Ao;DA.prototype.loadNewSongList=eo;DA.prototype.nextSong=to;DA.prototype.previousSong=no;DA.prototype.play=Io;DA.prototype._playTo=ao;DA.prototype.setTimeTicks=go;DA.prototype._recalculateStartTime=Co;function li(e,A){let t=0;return e.drumChannel&&(t+=5),A.isInRelease&&(t-=5),t+=A.velocity/25,t-=A.volumeEnvelope.state,A.isInRelease&&(t-=5),t-=A.volumeEnvelope.currentAttenuationDb/50,t}function fo(e){let A=[];for(let n of this.workletProcessorChannels)for(let s of n.voices)if(!s.finished){let o=li(n,s);A.push({channel:n,voice:s,priority:o})}A.sort((n,s)=>n.priority-s.priority);let t=A.slice(0,e);for(let{channel:n,voice:s}of t){let o=n.voices.indexOf(s);o>-1&&n.voices.splice(o,1)}}var me=me!==void 0?me:{},mo=!1,po;me.isInitialized=new Promise(e=>po=e);var Qi=function(e){var A,t,n,s,o,r,C,i="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",g="",c=0;e=e.replace(/[^A-Za-z0-9\+\/\=]/g,"");do s=i.indexOf(e.charAt(c++)),o=i.indexOf(e.charAt(c++)),r=i.indexOf(e.charAt(c++)),C=i.indexOf(e.charAt(c++)),A=s<<2|o>>4,t=(15&o)<<4|r>>2,n=(3&r)<<6|C,g+=String.fromCharCode(A),r!==64&&(g+=String.fromCharCode(t)),C!==64&&(g+=String.fromCharCode(n));while(c1&&(E.thisProgram=process.argv[1].replace(/\\/g,"/")),E.arguments=process.argv.slice(2),typeof module<"u",process.on("uncaughtException",function(l){if(!(l instanceof kt))throw l}),process.on("unhandledRejection",function(l,Q){process.exit(1)}),E.quit=function(l){process.exit(l)},E.inspect=function(){return"[Emscripten Module object]"}):T?(typeof read<"u"&&(E.read=function(Q){return read(Q)}),E.readBinary=function(Q){var u;return typeof readbuffer=="function"?new Uint8Array(readbuffer(Q)):(XA(typeof(u=read(Q,"binary"))=="object"),u)},typeof scriptArgs<"u"?E.arguments=scriptArgs:typeof arguments<"u"&&(E.arguments=arguments),typeof quit=="function"&&(E.quit=function(l){quit(l)})):(Z||EA)&&(Z?document.currentScript&&($=document.currentScript.src):$=self.location.href,$=$.indexOf("blob:")!==0?$.split("/").slice(0,-1).join("/")+"/":"",E.read=function(Q){var u=new XMLHttpRequest;return u.open("GET",Q,!1),u.send(null),u.responseText},EA&&(E.readBinary=function(Q){var u=new XMLHttpRequest;return u.open("GET",Q,!1),u.responseType="arraybuffer",u.send(null),new Uint8Array(u.response)}),E.readAsync=function(Q,u,k){var K=new XMLHttpRequest;K.open("GET",Q,!0),K.responseType="arraybuffer",K.onload=function(){if(K.status==200||K.status==0&&K.response){u(K.response);return}k()},K.onerror=k,K.send(null)},E.setWindowTitle=function(l){document.title=l});var sA=E.print||(typeof console<"u"?console.log.bind(console):typeof print<"u"?print:null),oA=E.printErr||(typeof printErr<"u"?printErr:typeof console<"u"&&console.warn.bind(console)||sA);for(e in L)L.hasOwnProperty(e)&&(E[e]=L[e]);function CA(l){var Q=B;return B=B+l+15&-16,Q}function v(l){var Q=i[x>>2],u=Q+l+15&-16;return i[x>>2]=u,u>=NA&&!yt()?(i[x>>2]=Q,0):Q}function J(l,Q){return Q||(Q=16),l=Math.ceil(l/Q)*Q}function W(l){switch(l){case"i1":case"i8":return 1;case"i16":return 2;case"i32":case"float":return 4;case"i64":case"double":return 8;default:if(l[l.length-1]==="*")return 4;if(l[0]!=="i")return 0;var Q=parseInt(l.substr(1));return XA(Q%8==0),Q/8}}function O(l){O.shown||(O.shown={}),O.shown[l]||(O.shown[l]=1,oA(l))}L=void 0;var rA={"f64-rem":function(l,Q){return l%Q},debugger:function(){}},KA=[];function Ie(l,Q){for(var u=0,k=u;k>>0)+4294967296*+(Q>>>0):+(l>>>0)+4294967296*+(0|Q)}function aA(l,Q,u){return u&&u.length?E["dynCall_"+l].apply(null,[Q].concat(u)):E["dynCall_"+l].call(null,Q)}var lA=0,Qe=0;function XA(l,Q){l||ke("Assertion failed: "+Q)}function Re(l){var Q=E["_"+l];return XA(Q,"Cannot call unknown function "+l+", make sure it is exported"),Q}var Ge={stackSave:function(){Ln()},stackRestore:function(){bn()},arrayToC:function(l){var Q,u,k=Vt(l.length);return Q=l,u=k,s.set(Q,u),k},stringToC:function(l){var Q=0;if(l!=null&&l!==0){var u=(l.length<<2)+1;Q=Vt(u),mt(l,Q,u)}return Q}},ut={string:Ge.stringToC,array:Ge.arrayToC};function At(l,Q,u,k,K){var iA=Re(l),gA=[],V=0;if(k)for(var LA=0;LA>0]=Q;break;case"i16":r[l>>1]=Q;break;case"i32":i[l>>2]=Q;break;case"i64":tempI64=[Q>>>0,+oi(tempDouble=Q)>=1?tempDouble>0?(0|ii(+ys(tempDouble/4294967296),4294967295))>>>0:~~+ri((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0],i[l>>2]=tempI64[0],i[l+4>>2]=tempI64[1];break;case"float":c[l>>2]=Q;break;case"double":h[l>>3]=Q;break;default:ke("invalid type for setValue: "+u)}}function kn(l,Q,u){switch((Q=Q||"i8").charAt(Q.length-1)==="*"&&(Q="i32"),Q){case"i1":case"i8":return s[l>>0];case"i16":return r[l>>1];case"i32":case"i64":return i[l>>2];case"float":return c[l>>2];case"double":return h[l>>3];default:ke("invalid type for getValue: "+Q)}return null}function wn(l,Q,u,k){typeof l=="number"?(iA=!0,gA=l):(iA=!1,gA=l.length);var K=typeof Q=="string"?Q:null;if(V=u==4?k:[typeof Pt=="function"?Pt:CA,Vt,CA,v][u===void 0?2:u](Math.max(gA,K?1:Q.length)),iA){for(k=V,XA((3&V)==0),LA=V+(-4&gA);k>2]=0;for(LA=V+gA;k>0]=0;return V}if(K==="i8")return l.subarray||l.slice?o.set(l,V):o.set(new Uint8Array(l),V),V;for(var iA,gA,V,LA,fA,mA,QA,nA=0;nA>0],(k!=0||Q)&&(gA++,!Q||gA!=Q););Q||(Q=gA);var V="";if(iA<128){for(;Q>0;)K=String.fromCharCode.apply(String,o.subarray(l,l+Math.min(Q,1024))),V=V?V+K:K,l+=1024,Q-=1024;return V}return u=l,function(fA,mA){for(var QA=mA;fA[QA];)++QA;if(QA-mA>16&&fA.subarray&&Ht)return Ht.decode(fA.subarray(mA,QA));for(var nA,PA,Ae,ee,te,Ze,ne="";;){if(!(nA=fA[mA++]))return ne;if(!(128&nA)){ne+=String.fromCharCode(nA);continue}if(PA=63&fA[mA++],(224&nA)==192){ne+=String.fromCharCode((31&nA)<<6|PA);continue}if(Ae=63&fA[mA++],(240&nA)==224?nA=(15&nA)<<12|PA<<6|Ae:(ee=63&fA[mA++],(248&nA)==240?nA=(7&nA)<<18|PA<<12|Ae<<6|ee:(te=63&fA[mA++],nA=(252&nA)==248?(3&nA)<<24|PA<<18|Ae<<12|ee<<6|te:(1&nA)<<30|PA<<24|Ae<<18|ee<<12|te<<6|(Ze=63&fA[mA++]))),nA<65536)ne+=String.fromCharCode(nA);else{var wt=nA-65536;ne+=String.fromCharCode(55296|wt>>10,56320|1023&wt)}}}(o,u)}function WA(l){for(var Q="";;){var u=s[l++>>0];if(!u)return Q;Q+=String.fromCharCode(u)}}function Fn(l,Q){return function(k,K,iA){for(var gA=0;gA>0]=k.charCodeAt(gA);iA||(s[K>>0]=0)}(l,Q,!1)}var Ht=typeof TextDecoder<"u"?new TextDecoder("utf8"):void 0;function tt(l,Q,u,k){if(!(k>0))return 0;for(var K=u,iA=u+k-1,gA=0;gA=55296&&V<=57343&&(V=65536+((1023&V)<<10)|1023&l.charCodeAt(++gA)),V<=127){if(u>=iA)break;Q[u++]=V}else if(V<=2047){if(u+1>=iA)break;Q[u++]=192|V>>6,Q[u++]=128|63&V}else if(V<=65535){if(u+2>=iA)break;Q[u++]=224|V>>12,Q[u++]=128|V>>6&63,Q[u++]=128|63&V}else if(V<=2097151){if(u+3>=iA)break;Q[u++]=240|V>>18,Q[u++]=128|V>>12&63,Q[u++]=128|V>>6&63,Q[u++]=128|63&V}else if(V<=67108863){if(u+4>=iA)break;Q[u++]=248|V>>24,Q[u++]=128|V>>18&63,Q[u++]=128|V>>12&63,Q[u++]=128|V>>6&63,Q[u++]=128|63&V}else{if(u+5>=iA)break;Q[u++]=252|V>>30,Q[u++]=128|V>>24&63,Q[u++]=128|V>>18&63,Q[u++]=128|V>>12&63,Q[u++]=128|V>>6&63,Q[u++]=128|63&V}}return Q[u]=0,u-K}function mt(l,Q,u){return tt(l,o,Q,u)}function OA(l){for(var Q=0,u=0;u=55296&&k<=57343&&(k=65536+((1023&k)<<10)|1023&l.charCodeAt(++u)),k<=127?++Q:k<=2047?Q+=2:k<=65535?Q+=3:k<=2097151?Q+=4:k<=67108863?Q+=5:Q+=6}return Q}var ye=typeof TextDecoder<"u"?new TextDecoder("utf-16le"):void 0;function pt(l){for(var Q=l,u=Q>>1;r[u];)++u;if((Q=u<<1)-l>32&&ye)return ye.decode(o.subarray(l,Q));for(var k=0,K="";;){var iA=r[l+2*k>>1];if(iA==0)return K;++k,K+=String.fromCharCode(iA)}}function Rn(l,Q,u){if(u===void 0&&(u=2147483647),u<2)return 0;for(var k=Q,K=(u-=2)<2*l.length?u/2:l.length,iA=0;iA>1]=gA,Q+=2}return r[Q>>1]=0,Q-k}function Se(l){return 2*l.length}function Yt(l){for(var Q=0,u="";;){var k=i[l+4*Q>>2];if(k==0)return u;if(++Q,k>=65536){var K=k-65536;u+=String.fromCharCode(55296|K>>10,56320|1023&K)}else u+=String.fromCharCode(k)}}function Jt(l,Q,u){if(u===void 0&&(u=2147483647),u<4)return 0;for(var k=Q,K=k+u-4,iA=0;iA=55296&&gA<=57343&&(gA=65536+((1023&gA)<<10)|1023&l.charCodeAt(++iA)),i[Q>>2]=gA,(Q+=4)+4>K)break}return i[Q>>2]=0,Q-k}function ds(l){for(var Q=0,u=0;u=55296&&k<=57343&&++u,Q+=4}return Q}function us(l){var Q=OA(l)+1,u=Pt(Q);return u&&tt(l,s,u,Q),u}function Kt(l){var Q=OA(l)+1,u=Vt(Q);return tt(l,s,u,Q),u}function qe(l){return l}function Gn(){var l,Q=function(){var k=Error();if(!k.stack){try{throw Error(0)}catch(K){k=K}if(!k.stack)return"(no stack trace available)"}return k.stack.toString()}();return E.extraStackTrace&&(Q+=` +`+E.extraStackTrace()),(l=Q).replace(/__Z[\w\d_]+/g,function(u){var k,K=k=u;return u===K?u:u+" ["+K+"]"})}function De(l,Q){return l%Q>0&&(l+=Q-l%Q),l}function nt(l){E.buffer=n=l}function Pe(){E.HEAP8=s=new Int8Array(n),E.HEAP16=r=new Int16Array(n),E.HEAP32=i=new Int32Array(n),E.HEAPU8=o=new Uint8Array(n),E.HEAPU16=C=new Uint16Array(n),E.HEAPU32=g=new Uint32Array(n),E.HEAPF32=c=new Float32Array(n),E.HEAPF64=h=new Float64Array(n)}function yt(){var l=E.usingWasm?65536:16777216,Q=2147483648-l;if(i[x>>2]>Q)return!1;var u=NA;for(NA=Math.max(NA,16777216);NA>2];)NA=NA<=536870912?De(2*NA,l):Math.min(De((3*NA+2147483648)/4,l),Q);var k=E.reallocBuffer(NA);return k&&k.byteLength==NA?(nt(k),Pe(),!0):(NA=u,!1)}d=B=m=S=M=D=x=0,f=!1,E.reallocBuffer||(E.reallocBuffer=function(l){try{if(ArrayBuffer.transfer)Q=ArrayBuffer.transfer(n,l);else{var Q,u=s;Q=new ArrayBuffer(l),new Int8Array(Q).set(u)}}catch{return!1}return!!ai(Q)&&Q});try{(G=Function.prototype.call.bind(Object.getOwnPropertyDescriptor(ArrayBuffer.prototype,"byteLength").get))(new ArrayBuffer(4))}catch{G=function(Q){return Q.byteLength}}var st=E.TOTAL_STACK||5242880,NA=E.TOTAL_MEMORY||16777216;function Ot(){return NA}function Me(l){for(;l.length>0;){var Q=l.shift();if(typeof Q=="function"){Q();continue}var u=Q.func;typeof u=="number"?Q.arg===void 0?E.dynCall_v(u):E.dynCall_vi(u,Q.arg):u(Q.arg===void 0?null:Q.arg)}}NA=0?l:Q<=32?2*Math.abs(1<=k&&(Q<=32||l>k)&&(l=-2*k+l),l}var oi=Math.abs,ri=Math.ceil,ys=Math.floor,ii=Math.min,Ve=0,Nn=null,Dt=null;function Oi(l){return l}E.preloadedImages={},E.preloadedAudios={};var Ss="data:application/octet-stream;base64,";function qt(l){return String.prototype.startsWith?l.startsWith(Ss):l.indexOf(Ss)===0}(function(){var Q="main.wast",u="main.wasm",k="main.temp.asm.js";qt(Q)||(Q=BA(Q)),qt(u)||(u=BA(u)),qt(k)||(k=BA(k));var K={global:null,env:null,asm2wasm:rA,parent:E},iA=null;function gA(QA){return QA}function V(){try{if(E.wasmBinary)return new Uint8Array(E.wasmBinary);if(E.readBinary)return E.readBinary(u);throw"both async and sync fetching of the wasm failed"}catch(QA){ke(QA)}}E.asmPreload=E.asm;var LA=E.reallocBuffer,fA=function(QA){QA=De(QA,E.usingWasm?65536:16777216);var nA=E.buffer.byteLength;if(E.usingWasm)try{var PA=E.wasmMemory.grow((QA-nA)/65536);return PA!==-1?E.buffer=E.wasmMemory.buffer:null}catch{return null}};E.reallocBuffer=function(QA){return mA==="asmjs"?LA(QA):fA(QA)};var mA="";E.asm=function(QA,nA,PA){var Ae;if(!(nA=Ae=nA).table){var ee,te=E.wasmTableSize;te===void 0&&(te=1024);var Ze=E.wasmMaxTableSize;typeof WebAssembly=="object"&&typeof WebAssembly.Table=="function"?Ze!==void 0?nA.table=new WebAssembly.Table({initial:te,maximum:Ze,element:"anyfunc"}):nA.table=new WebAssembly.Table({initial:te,element:"anyfunc"}):nA.table=Array(te),E.wasmTable=nA.table}return nA.memoryBase||(nA.memoryBase=E.STATIC_BASE),nA.tableBase||(nA.tableBase=0),ee=function(wt,ot,Tn){if(typeof WebAssembly!="object")return oA("no native wasm support detected"),!1;if(!(E.wasmMemory instanceof WebAssembly.Memory))return oA("no native wasm Memory in use"),!1;function Zt(de,ue){if((iA=de.exports).memory){var Ft,vn,ws;Ft=iA.memory,vn=E.buffer,Ft.byteLength0?u:OA(l)+1,K=Array(k),iA=tt(l,K,0,K.length);return Q&&(K.length=iA),K}function Zi(l){for(var Q=[],u=0;u255&&(k&=255),Q.push(String.fromCharCode(k))}return Q.join("")}B+=16,x=CA(4),M=(m=S=J(B))+st,D=J(M),i[x>>2]=D,f=!0,E.wasmTableSize=4,E.wasmMaxTableSize=4,E.asmGlobalArg={},E.asmLibraryArg={abort:ke,assert:XA,enlargeMemory:yt,getTotalMemory:Ot,abortOnCannotGrowMemory:function(){ke("Cannot enlarge memory arrays. Either (1) compile with -s TOTAL_MEMORY=X with X higher than the current value "+NA+", (2) compile with -s ALLOW_MEMORY_GROWTH=1 which allows increasing the size at runtime, or (3) if you want malloc to return NULL (0) instead of this abort, compile with -s ABORTING_MALLOC=0 ")},invoke_iii:function(Q,u,k){var K=Ln();try{return E.dynCall_iii(Q,u,k)}catch(iA){if(bn(K),typeof iA!="number"&&iA!=="longjmp")throw iA;E.setThrew(1,0)}},___assert_fail:function(Q,u,k,K){ke("Assertion failed: "+xe(Q)+", at: "+[u?xe(u):"unknown filename",k,K?xe(K):"unknown function"])},___setErrNo:function(Q){return E.___errno_location&&(i[E.___errno_location()>>2]=Q),Q},_abort:function(){E.abort()},_emscripten_memcpy_big:function(Q,u,k){return o.set(o.subarray(u,u+k),Q),Q},_llvm_floor_f64:ys,DYNAMICTOP_PTR:x,tempDoublePtr:_A,ABORT:lA,STACKTOP:S,STACK_MAX:M};var Ds=E.asm(E.asmGlobalArg,E.asmLibraryArg,n);E.asm=Ds,E.___errno_location=function(){return E.asm.___errno_location.apply(null,arguments)};var ai=E._emscripten_replace_memory=function(){return E.asm._emscripten_replace_memory.apply(null,arguments)};E._free=function(){return E.asm._free.apply(null,arguments)};var Pt=E._malloc=function(){return E.asm._malloc.apply(null,arguments)};E._memcpy=function(){return E.asm._memcpy.apply(null,arguments)},E._memset=function(){return E.asm._memset.apply(null,arguments)},E._sbrk=function(){return E.asm._sbrk.apply(null,arguments)},E._stb_vorbis_js_channels=function(){return E.asm._stb_vorbis_js_channels.apply(null,arguments)},E._stb_vorbis_js_close=function(){return E.asm._stb_vorbis_js_close.apply(null,arguments)},E._stb_vorbis_js_decode=function(){return E.asm._stb_vorbis_js_decode.apply(null,arguments)},E._stb_vorbis_js_open=function(){return E.asm._stb_vorbis_js_open.apply(null,arguments)},E._stb_vorbis_js_sample_rate=function(){return E.asm._stb_vorbis_js_sample_rate.apply(null,arguments)},E.establishStackSpace=function(){return E.asm.establishStackSpace.apply(null,arguments)},E.getTempRet0=function(){return E.asm.getTempRet0.apply(null,arguments)},E.runPostSets=function(){return E.asm.runPostSets.apply(null,arguments)},E.setTempRet0=function(){return E.asm.setTempRet0.apply(null,arguments)},E.setThrew=function(){return E.asm.setThrew.apply(null,arguments)};var Vt=E.stackAlloc=function(){return E.asm.stackAlloc.apply(null,arguments)},bn=E.stackRestore=function(){return E.asm.stackRestore.apply(null,arguments)},Ln=E.stackSave=function(){return E.asm.stackSave.apply(null,arguments)};function kt(l){this.name="ExitStatus",this.message="Program terminated with exit("+l+")",this.status=l}function Un(l){l=l||E.arguments,!(Ve>0)&&(function(){if(E.preRun)for(typeof E.preRun=="function"&&(E.preRun=[E.preRun]);E.preRun.length;)ni(E.preRun.shift());Me(St)}(),!(Ve>0)&&(E.calledRun||(E.setStatus?(E.setStatus("Running..."),setTimeout(function(){setTimeout(function(){E.setStatus("")},1),Q()},1)):Q())));function Q(){!E.calledRun&&(E.calledRun=!0,lA||(Mn||(Mn=!0,Me(xn)),Me(fs),E.onRuntimeInitialized&&E.onRuntimeInitialized(),function(){if(E.postRun)for(typeof E.postRun=="function"&&(E.postRun=[E.postRun]);E.postRun.length;)si(E.postRun.shift());Me(ps)}()))}}function Xi(l,Q){(!Q||!E.noExitRuntime||l!==0)&&(E.noExitRuntime||(lA=!0,Qe=l,S=F,Me(ms),ti=!0,E.onExit&&E.onExit(l)),E.quit(l,new kt(l)))}function ke(l){throw E.onAbort&&E.onAbort(l),l!==void 0?(sA(l),oA(l),l=JSON.stringify(l)):l="",lA=!0,Qe=1,"abort("+l+"). Build with -s ASSERTIONS=1 for more info."}if(E.dynCall_iii=function(){return E.asm.dynCall_iii.apply(null,arguments)},E.asm=Ds,E.ccall=At,E.cwrap=function(Q,u,k,K){var iA=(k=k||[]).every(function(gA){return gA==="number"});return u!=="string"&&iA&&!K?Re(Q):function(){return At(Q,u,k,arguments,K)}},kt.prototype=Error(),kt.prototype.constructor=kt,Dt=function l(){E.calledRun||Un(),E.calledRun||(Dt=l)},E.run=Un,E.abort=ke,E.preInit)for(typeof E.preInit=="function"&&(E.preInit=[E.preInit]);E.preInit.length>0;)E.preInit.pop()();E.noExitRuntime=!0,Un(),E.onRuntimeInitialized=()=>{mo=!0,po()},me.decode=function(l){return function(u){if(!mo)throw Error("SF3 decoder has not been initialized yet. Did you await synth.isReady?");var k={};function K(ot){return new Int32Array(E.HEAPU8.buffer,ot,1)[0]}function iA(ot,Tn){var Zt=new ArrayBuffer(Tn*Float32Array.BYTES_PER_ELEMENT),rt=new Float32Array(Zt);return rt.set(new Float32Array(E.HEAPU8.buffer,ot,Tn)),rt}k.open=E.cwrap("stb_vorbis_js_open","number",[]),k.close=E.cwrap("stb_vorbis_js_close","void",["number"]),k.channels=E.cwrap("stb_vorbis_js_channels","number",["number"]),k.sampleRate=E.cwrap("stb_vorbis_js_sample_rate","number",["number"]),k.decode=E.cwrap("stb_vorbis_js_decode","number",["number","number","number","number","number"]);var gA,V,LA,fA,mA=k.open(),QA=(gA=u,V=u.byteLength,LA=E._malloc(V),(fA=new Uint8Array(E.HEAPU8.buffer,LA,V)).set(new Uint8Array(gA,0,V)),fA),nA=E._malloc(4),PA=E._malloc(4),Ae=k.decode(mA,QA.byteOffset,QA.byteLength,nA,PA);if(E._free(QA.byteOffset),Ae<0)throw k.close(mA),E._free(nA),Error("stbvorbis decode failed: "+Ae);for(var ee=k.channels(mA),te=Array(ee),Ze=new Int32Array(E.HEAPU32.buffer,K(nA),ee),ne=0;neyo?440*Math.pow(2,(e-6900)/1200):jn[~~e-In]}var As=-1660,di=1600,$n=new Float32Array((di-As)*100+1);for(let e=0;e<$n.length;e++){let A=(As*100+e)/100;$n[e]=Math.pow(10,-A/20)}function ae(e){return $n[Math.floor((e-As)*100)]}var So=.01,$A=100,es=90,ui=15e-6,Ee=class e{currentSampleTime=0;sampleRate;currentAttenuationDb=$A;state=0;releaseStartDb=$A;releaseStartTimeSamples=0;currentReleaseGain=1;attackDuration=0;decayDuration=0;releaseDuration=0;attenuation=0;attenuationTargetGain=0;attenuationTarget=0;sustainDbRelative=0;delayEnd=0;attackEnd=0;holdEnd=0;decayEnd=0;constructor(A,t){this.sampleRate=A,this.canEndOnSilentSustain=t/10>=es}static startRelease(A){A.volumeEnvelope.releaseStartTimeSamples=A.volumeEnvelope.currentSampleTime,A.volumeEnvelope.currentReleaseGain=ae(A.volumeEnvelope.currentAttenuationDb),e.recalculate(A)}static recalculate(A){let t=A.volumeEnvelope,n=g=>Math.max(0,Math.floor(Ce(g)*t.sampleRate));t.attenuationTarget=Math.max(0,Math.min(A.modulatedGenerators[a.initialAttenuation],1440))/10,t.attenuationTargetGain=ae(t.attenuationTarget),t.sustainDbRelative=Math.min($A,A.modulatedGenerators[a.sustainVolEnv]/10);let s=Math.min($A,t.sustainDbRelative);t.attackDuration=n(A.modulatedGenerators[a.attackVolEnv]);let o=A.modulatedGenerators[a.decayVolEnv],r=(60-A.targetKey)*A.modulatedGenerators[a.keyNumToVolEnvDecay],C=s/$A;t.decayDuration=n(o+r)*C,t.releaseDuration=n(A.modulatedGenerators[a.releaseVolEnv]),t.delayEnd=n(A.modulatedGenerators[a.delayVolEnv]),t.attackEnd=t.attackDuration+t.delayEnd;let i=(60-A.targetKey)*A.modulatedGenerators[a.keyNumToVolEnvHold];if(t.holdEnd=n(A.modulatedGenerators[a.holdVolEnv]+i)+t.attackEnd,t.decayEnd=t.decayDuration+t.holdEnd,t.state===0&&t.attackEnd===0&&(t.state=2),A.isInRelease){let g=Math.max(0,Math.min($A,t.sustainDbRelative)),c=g/$A;switch(t.decayDuration=n(o+r)*c,t.state){case 0:t.releaseStartDb=$A;break;case 1:let d=1-(t.attackEnd-t.releaseStartTimeSamples)/t.attackDuration;t.releaseStartDb=20*Math.log10(d)*-1;break;case 2:t.releaseStartDb=0;break;case 3:t.releaseStartDb=(1-(t.decayEnd-t.releaseStartTimeSamples)/t.decayDuration)*g;break;case 4:t.releaseStartDb=g;break}t.releaseStartDb=Math.max(0,Math.min(t.releaseStartDb,$A)),t.releaseStartDb>=es&&(A.finished=!0),t.currentReleaseGain=ae(t.releaseStartDb);let h=($A-t.releaseStartDb)/$A;t.releaseDuration*=h}}static apply(A,t,n,s){let o=A.volumeEnvelope,r=n/10,C=s;if(A.isInRelease){let g=o.currentSampleTime-o.releaseStartTimeSamples;if(g>=o.releaseDuration){for(let h=0;h=t.length)return;o.state++;case 1:for(;o.currentSampleTime=t.length)return}o.state++;case 2:for(;o.currentSampleTime=t.length)return;o.state++;case 3:for(;o.currentSampleTime=t.length)return;o.state++;case 4:for(o.canEndOnSilentSustain&&o.sustainDbRelative>=es&&(A.finished=!0);;)if(o.attenuation+=(o.attenuationTargetGain-o.attenuation)*C,t[i]*=o.attenuation*ae(o.sustainDbRelative+r),o.currentAttenuationDb=o.sustainDbRelative,o.currentSampleTime++,++i>=t.length)return}}};function Do(e){let A=e.messageData,t=e.channelNumber,n;if(t>=0&&(n=this.workletProcessorChannels[t],n===void 0)){Y(`Trying to access channel ${t} which does not exist... ignoring!`);return}switch(e.messageType){case RA.midiMessage:this.processMessage(...A);break;case RA.customcCcChange:n.setCustomController(A[0],A[1]),n.updateChannelTuning();break;case RA.ccReset:t===ie?this.resetAllControllers():n.resetControllers();break;case RA.setChannelVibrato:if(t===ie)for(let r=0;r>1&1)===1&&this.workletProcessorChannels[15+A].setOctaveTuning(i);for(let g=0;g<7;g++)(e[5]>>g&1)===1&&this.workletProcessorChannels[7+g+A].setOctaveTuning(i);for(let g=0;g<7;g++)(e[6]>>g&1)===1&&this.workletProcessorChannels[g+A].setOctaveTuning(i);p(`%cMIDI Octave Scale ${e[3]===8?"(1 byte)":"(2 bytes)"} tuning via Tuning: %c${i.join(" ")}`,I.info,I.value);break;default:Y(`%cUnrecognized MIDI Tuning standard message: %c${HA(e)}`,I.warn,I.unrecognized);break}break;default:Y(`%cUnrecognized MIDI Realtime/non realtime message: %c${HA(e)}`,I.warn,I.unrecognized)}break;case 65:let n=function(){Y(`%cUnrecognized Roland %cGS %cSysEx: %c${HA(e)}`,I.warn,I.recognized,I.warn,I.unrecognized)};if(e[2]===66&&e[3]===18){let s=e[7];if(e[6]===127){s===0?(p("%cGS Reset received!",I.info),this.resetAllControllers(!1),this.setSystem("gs")):s===127&&(p("%cGS system off, switching to GM2",I.info),this.resetAllControllers(!1),this.setSystem("gm2"));return}else if(e[4]===64){if((e[5]&16)>0){let o=[9,0,1,2,3,4,5,6,7,8,10,11,12,13,14,15][e[5]&15]+A,r=this.workletProcessorChannels[o];switch(e[6]){default:n();break;case 21:let C=s>0&&e[5]>>4;r.setDrums(C),p(`%cChannel %c${o}%c ${C?"is now a drum channel":"now isn't a drum channel"}%c via: %c${HA(e)}`,I.info,I.value,I.recognized,I.info,I.value);return;case 22:let i=s-64;r.transposeChannel(i),p(`%cChannel %c${o}%c pitch shift. Semitones %c${i}%c, with %c${HA(e)}`,I.info,I.recognized,I.info,I.value,I.info,I.value);return;case 28:let g=s;g===0?(r.randomPan=!0,p(`%cRandom pan is set to %cON%c for %c${o}`,I.info,I.recognized,I.info,I.value)):(r.randomPan=!1,r.controllerChange(y.pan,g));break;case 33:r.controllerChange(y.chorusDepth,s);break;case 34:r.controllerChange(y.reverbDepth,s);break;case 64:case 65:case 66:case 67:case 68:case 69:case 70:case 71:case 72:case 73:case 74:case 75:let c=e.length-9,h=new Int8Array(12);for(let B=0;B=this.workletProcessorChannels.length)return;let o=this.workletProcessorChannels[s],r=e[6];switch(e[5]){case 1:o.controllerChange(y.bankSelect,r);break;case 2:o.controllerChange(y.lsbForControl0BankSelect,r);break;case 3:o.programChange(r);break;case 8:if(o.drumChannel)return;let C=r-64;o.channelTransposeKeyShift=C;break;case 11:o.controllerChange(y.mainVolume,r);break;case 14:let i=r;i===0?(o.randomPan=!0,p(`%cRandom pan is set to %cON%c for %c${s}`,I.info,I.recognized,I.info,I.value)):o.controllerChange(y.pan,i);break;case 19:o.controllerChange(y.reverbDepth,r);break;case 18:o.controllerChange(y.chorusDepth,r);break;default:Y(`%cUnrecognized Yamaha XG Part Setup: %c${e[5].toString(16).toUpperCase()}`,I.warn,I.unrecognized)}}else if(e[3]===6&&e[4]===0){let s=new Uint8Array(e.slice(5,e.length-1));this.callEvent("synthdisplay",{displayData:s,displayType:ts.XGText})}else FA(this.system)&&Y(`%cUnrecognized Yamaha XG SysEx: %c${HA(e)}`,I.warn,I.unrecognized);else FA(this.system)&&Y(`%cUnrecognized Yamaha SysEx: %c${HA(e)}`,I.warn,I.unrecognized);break}}function Fo(e){this.midiVolume=Math.pow(e,Math.E),this.setMasterPan(this.pan)}function Ro(e){this.masterGain=e*ns,this.setMasterPan(this.pan)}function Go(e){this.pan=e,e=e/2+.5,this.panLeft=1-e,this.panRight=e}var xt={reloadSoundFont:0,addNewSoundFont:2,deleteSoundFont:3,rearrangeSoundFonts:4};function xo(){let e=4;for(let n of this.instruments)e+=n.instrumentZones.reduce((s,o)=>(o.generators=o.generators.filter(r=>r.generatorType!==a.sampleID&&r.generatorType!==a.keyRange&&r.generatorType!==a.velRange),(o.velRange.max!==127||o.velRange.min!==0)&&o.generators.unshift(new U(a.velRange,o.velRange.max<<8|Math.max(o.velRange.min,0),!1)),(o.keyRange.max!==127||o.keyRange.min!==0)&&o.generators.unshift(new U(a.keyRange,o.keyRange.max<<8|Math.max(o.keyRange.min,0),!1)),o.isGlobal||o.generators.push(new U(a.sampleID,this.samples.indexOf(o.sample),!1)),o.generators.length*4+s),0);let A=new b(e),t=0;for(let n of this.instruments)for(let s of n.instrumentZones){s.generatorZoneStartIndex=t;for(let o of s.generators)H(A,o.generatorType),H(A,o.generatorValue),t++}return AA(A,0),dA(new hA("igen",A.length,A))}function Mo(e,A,t,n,s){let o=this.samples.map((g,c)=>{t&&g.compressSample(n,s);let h=g.getRawData();return p(`%cEncoded sample %c${c}. ${g.sampleName}%c of %c${this.samples.length}%c. Compressed: %c${g.isCompressed}%c.`,I.info,I.recognized,I.info,I.recognized,I.info,g.isCompressed?I.recognized:I.unrecognized,I.info),h}),r=this.samples.reduce((g,c,h)=>g+o[h].length+46,0),C=new b(r);this.samples.forEach((g,c)=>{let h=o[c],d,B,f=h.length;g.isCompressed?(d=C.currentIndex,B=d+h.length):(d=C.currentIndex/2,B=d+h.length/2,f+=46),e.push(d),C.set(h,C.currentIndex),C.currentIndex+=f,A.push(B)});let i=dA(new hA("smpl",C.length,C),new b([115,100,116,97]));return dA(new hA("LIST",i.length,i))}function No(e,A){let n=new b(46*(this.samples.length+1));return this.samples.forEach((s,o)=>{TA(n,s.sampleName,20);let r=e[o];AA(n,r);let C=A[o];AA(n,C);let i=s.sampleLoopStartIndex+r,g=s.sampleLoopEndIndex+r;s.isCompressed&&(i-=r,g-=r),AA(n,i),AA(n,g),AA(n,s.sampleRate),n[n.currentIndex++]=s.samplePitch,n[n.currentIndex++]=s.samplePitchCorrection,H(n,s.sampleLink),H(n,s.sampleType)}),TA(n,"EOS",46),dA(new hA("shdr",n.length,n))}function bo(){let e=10;for(let n of this.instruments)e+=n.instrumentZones.reduce((s,o)=>o.modulators.length*10+s,0);let A=new b(e),t=0;for(let n of this.instruments)for(let s of n.instrumentZones){s.modulatorZoneStartIndex=t;for(let o of s.modulators)H(A,o.sourceEnum),H(A,o.modulatorDestination),H(A,o.transformAmount),H(A,o.secondarySourceEnum),H(A,o.transformType),t++}return Xe(A,0,10),dA(new hA("imod",A.length,A))}function Lo(){let e=this.instruments.reduce((o,r)=>r.instrumentZones.length*4+o,4),A=new b(e),t=0,n=0,s=0;for(let o of this.instruments){o.instrumentZoneIndex=t;for(let r of o.instrumentZones)r.zoneID=t,H(A,n),H(A,s),n+=r.generators.length,s+=r.modulators.length,t++}return H(A,n),H(A,s),dA(new hA("ibag",A.length,A))}function Uo(){let e=this.instruments.length*22+22,A=new b(e),t=0,n=0;for(let s of this.instruments)TA(A,s.instrumentName,20),H(A,t),t+=s.instrumentZones.length,s.instrumentID=n,n++;return TA(A,"EOI",20),H(A,t),dA(new hA("inst",A.length,A))}function To(){let e=4;for(let n of this.presets)e+=n.presetZones.reduce((s,o)=>(o.generators=o.generators.filter(r=>r.generatorType!==a.instrument&&r.generatorType!==a.keyRange&&r.generatorType!==a.velRange),(o.velRange.max!==127||o.velRange.min!==0)&&o.generators.unshift(new U(a.velRange,o.velRange.max<<8|Math.max(o.velRange.min,0),!1)),(o.keyRange.max!==127||o.keyRange.min!==0)&&o.generators.unshift(new U(a.keyRange,o.keyRange.max<<8|Math.max(o.keyRange.min,0),!1)),o.isGlobal||o.generators.push(new U(a.instrument,this.instruments.indexOf(o.instrument),!1)),o.generators.length*4+s),0);let A=new b(e),t=0;for(let n of this.presets)for(let s of n.presetZones){s.generatorZoneStartIndex=t;for(let o of s.generators)H(A,o.generatorType),H(A,o.generatorValue);t+=s.generators.length}return H(A,0),H(A,0),dA(new hA("pgen",A.length,A))}function vo(){let e=10;for(let n of this.presets)e+=n.presetZones.reduce((s,o)=>o.modulators.length*10+s,0);let A=new b(e),t=0;for(let n of this.presets)for(let s of n.presetZones){s.modulatorZoneStartIndex=t;for(let o of s.modulators)H(A,o.sourceEnum),H(A,o.modulatorDestination),H(A,o.transformAmount),H(A,o.secondarySourceEnum),H(A,o.transformType),t++}return Xe(A,0,10),dA(new hA("pmod",A.length,A))}function Ho(){let e=this.presets.reduce((o,r)=>r.presetZones.length*4+o,4),A=new b(e),t=0,n=0,s=0;for(let o of this.presets){o.presetZoneStartIndex=t;for(let r of o.presetZones)r.zoneID=t,H(A,n),H(A,s),n+=r.generators.length,s+=r.modulators.length,t++}return H(A,n),H(A,s),dA(new hA("pbag",A.length,A))}function Yo(){let e=this.presets.length*38+38,A=new b(e),t=0;for(let n of this.presets)TA(A,n.presetName,20),H(A,n.program),H(A,n.bank),H(A,t),AA(A,n.library),AA(A,n.genre),AA(A,n.morphology),t+=n.presetZones.length;return TA(A,"EOP",20),H(A,0),H(A,0),H(A,t),AA(A,0),AA(A,0),AA(A,0),dA(new hA("phdr",A.length,A))}var mi={compress:!1,compressionQuality:.5,compressionFunction:void 0};function Jo(e=mi){if(e.compress&&typeof e.compressionFunction!="function")throw new TypeError("No compression function supplied but compression enabled.");pA("%cSaving soundfont...",I.info),p(`%cCompression: %c${e?.compress||"false"}%c quality: %c${e?.compressionQuality||"none"}`,I.info,I.recognized,I.info,I.recognized),p("%cWriting INFO...",I.info);let A=[];this.soundFontInfo.ISFT="SpessaSynth",e?.compress&&(this.soundFontInfo.ifil="3.0");for(let[G,F]of Object.entries(this.soundFontInfo))if(G==="ifil"||G==="iver"){let E=parseInt(F.split(".")[0]),L=parseInt(F.split(".")[1]),Z=new b(4);H(Z,E),H(Z,L),A.push(dA(new hA(G,4,Z)))}else if(G==="DMOD")A.push(dA(new hA(G,F.length,F)));else{let E=new b(F.length);TA(E,F),A.push(dA(new hA(G,F.length,E)))}let t=wA([new b([73,78,70,79]),...A]),n=dA(new hA("LIST",t.length,t));p("%cWriting SDTA...",I.info);let s=[],o=[],r=Mo.call(this,s,o,e?.compress,e?.compressionQuality??.5,e.compressionFunction);p("%cWriting PDTA...",I.info),p("%cWriting SHDR...",I.info);let C=No.call(this,s,o);p("%cWriting IGEN...",I.info);let i=xo.call(this);p("%cWriting IMOD...",I.info);let g=bo.call(this);p("%cWriting IBAG...",I.info);let c=Lo.call(this);p("%cWriting INST...",I.info);let h=Uo.call(this),d=To.call(this);p("%cWriting PMOD...",I.info);let B=vo.call(this);p("%cWriting PBAG...",I.info);let f=Ho.call(this);p("%cWriting PHDR...",I.info);let m=Yo.call(this),S=wA([new b([112,100,116,97]),m,f,B,d,h,c,g,i,C]),M=dA(new hA("LIST",S.length,S));p("%cWriting the output file...",I.info);let D=wA([new b([115,102,98,107]),n,r,M]),x=dA(new hA("RIFF",D.length,D));return p(`%cSaved succesfully! Final file size: %c${x.length}`,I.info,I.recognized),P(),x}var Mt=class{velRange={min:-1,max:127};keyRange={min:-1,max:127};isGlobal=!1;generators=[];modulators=[];get hasKeyRange(){return this.keyRange.min!==-1}get hasVelRange(){return this.velRange.min!==-1}getGeneratorValue(A,t){return this.generators.find(n=>n.generatorType===A)?.generatorValue??t}};var qA=class extends Mt{sample=void 0;useCount=0;deleteZone(){this.useCount--,!this.isGlobal&&this.sample.useCount--}},Te=class extends Mt{instrument=void 0;deleteZone(){this.isGlobal||this.instrument.removeUseCount()}};var pi=new Set([a.velRange,a.keyRange,a.instrument,a.exclusiveClass,a.endOper,a.sampleModes,a.startloopAddrsOffset,a.startloopAddrsCoarseOffset,a.endloopAddrsOffset,a.endloopAddrsCoarseOffset,a.startAddrsOffset,a.startAddrsCoarseOffset,a.endAddrOffset,a.endAddrsCoarseOffset,a.initialAttenuation,a.fineTune,a.coarseTune,a.keyNumToVolEnvHold,a.keyNumToVolEnvDecay,a.keyNumToModEnvHold,a.keyNumToModEnvDecay]);function Ko(e,A=!0){function t(h,d){h.push(...d.filter(B=>!h.find(f=>f.generatorType===B.generatorType)))}function n(h,d){return{min:Math.max(h.min,d.min),max:Math.min(h.max,d.max)}}function s(h,d){h.push(...d.filter(B=>!h.find(f=>z.isIdentical(B,f))))}let o=[],r=[],C=[],i={min:0,max:127},g={min:0,max:127},c=e.presetZones.find(h=>h.isGlobal);c&&(r.push(...c.generators),C.push(...c.modulators),i=c.keyRange,g=c.velRange);for(let h of e.presetZones){if(h.isGlobal)continue;let d=h.keyRange;h.hasKeyRange||(d=i);let B=h.velRange;h.hasVelRange||(B=g);let f=h.generators.map(E=>new U(E.generatorType,E.generatorValue));t(f,r);let m=[...h.modulators];s(m,C);let S=h.instrument.instrumentZones,M=[],D=[],x={min:0,max:127},G={min:0,max:127},F=S.find(E=>E.isGlobal);F&&(M.push(...F.generators),D.push(...F.modulators),x=F.keyRange,G=F.velRange);for(let E of S){if(E.isGlobal)continue;let L=E.keyRange;E.hasKeyRange||(L=x);let Z=E.velRange;if(E.hasVelRange||(Z=G),L=n(L,d),Z=n(Z,B),L.maxnew U(sA.generatorType,sA.generatorValue));t(EA,M);let tA=[...E.modulators];s(tA,D);let T=[...tA];for(let sA of m){let oA=T.findIndex(CA=>z.isIdentical(sA,CA));oA!==-1?T[oA]=T[oA].sumTransform(sA):T.push(sA)}let $=EA.map(sA=>new U(sA.generatorType,sA.generatorValue));for(let sA of f){if(sA.generatorType===a.velRange||sA.generatorType===a.keyRange||sA.generatorType===a.instrument||sA.generatorType===a.endOper||sA.generatorType===a.sampleModes)continue;let oA=EA.findIndex(CA=>CA.generatorType===sA.generatorType);if(oA!==-1){let CA=$[oA].generatorValue+sA.generatorValue;$[oA]=new U(sA.generatorType,CA)}else{let CA=X[sA.generatorType].def+sA.generatorValue;$.push(new U(sA.generatorType,CA))}}$=$.filter(sA=>sA.generatorType!==a.sampleID&&sA.generatorType!==a.keyRange&&sA.generatorType!==a.velRange&&sA.generatorType!==a.endOper&&sA.generatorType!==a.instrument&&sA.generatorValue!==X[sA.generatorType].def);let BA=new qA;BA.keyRange=L,BA.velRange=Z,BA.keyRange.min===0&&BA.keyRange.max===127&&(BA.keyRange.min=-1),BA.velRange.min===0&&BA.velRange.max===127&&(BA.velRange.min=-1),BA.isGlobal=!1,BA.sample=E.sample,BA.generators=$,BA.modulators=T,o.push(BA)}}if(A){let h=new qA;h.isGlobal=!0;for(let f=0;f<58;f++){if(pi.has(f))continue;let m={},S=X[f]?.def||0;m[S]=0;for(let M of o){let D=M.generators.find(F=>F.generatorType===f);if(D){let F=D.generatorValue;m[F]===void 0?m[F]=1:m[F]++}else m[S]++;let x;switch(f){default:continue;case a.decayVolEnv:x=a.keyNumToVolEnvDecay;break;case a.holdVolEnv:x=a.keyNumToVolEnvHold;break;case a.decayModEnv:x=a.keyNumToModEnvDecay;break;case a.holdModEnv:x=a.keyNumToModEnvHold}if(M.generators.find(F=>F.generatorType===x)!==void 0){m={};break}}if(Object.keys(m).length>0){let M=Object.entries(m).reduce((x,G)=>x[1]{let G=x.generators.findIndex(F=>F.generatorType===f);G!==-1?x.generators[G].generatorValue===D&&x.generators.splice(G,1):D!==S&&x.generators.push(new U(f,S))})}}let B=o.find(f=>!f.isGlobal).modulators.map(f=>z.copy(f));for(let f of B){let m=!0;for(let S of o){if(S.isGlobal||!m)continue;S.modulators.find(D=>z.isIdentical(D,f))||(m=!1)}if(m===!0){h.modulators.push(z.copy(f));for(let S of o){let M=S.modulators.find(D=>z.isIdentical(D,f));M.transformAmount===f.transformAmount&&S.modulators.splice(S.modulators.indexOf(M),1)}}}o.splice(0,0,h)}return o}var Oo=20;function gn(e,A,t,n,s,o,r){let C=r===0?0:1,i=new b(Oo+C*16);AA(i,Oo),H(i,A),H(i,t);let g=n*.4,c=Math.floor(g*-65536);AA(i,c),AA(i,2);let h=o-s,d=0;switch(r){default:case 0:C=0;break;case 1:d=0,C=1;break;case 3:d=1,C=1}return AA(i,C),C===1&&(AA(i,16),AA(i,d),AA(i,s),AA(i,h)),_("wsmp",i)}var q={none:0,modLfo:1,velocity:2,keyNum:3,volEnv:4,modEnv:5,pitchWheel:6,polyPressure:7,channelPressure:8,vibratoLfo:9,modulationWheel:129,volume:135,pan:138,expression:139,chorus:221,reverb:219,pitchWheelRange:256,fineTune:257,coarseTune:258},Cn=new z(219,0,a.reverbEffectsSend,1e3,0),En=new z(221,0,a.chorusEffectsSend,1e3,0),Bn=new z(129,0,a.vibLfoToPitch,0,0),hn=new z(13,0,a.vibLfoToPitch,0,0);var R={none:0,gain:1,reserved:2,pitch:3,pan:4,keyNum:5,chorusSend:128,reverbSend:129,modLfoFreq:260,modLfoDelay:261,vibLfoFreq:276,vibLfoDelay:277,volEnvAttack:518,volEnvDecay:519,volEnvRelease:521,volEnvSustain:522,volEnvDelay:523,volEnvHold:524,modEnvAttack:778,modEnvDecay:779,modEnvRelease:781,modEnvSustain:782,modEnvDelay:783,modEnvHold:784,filterCutoff:1280,filterQ:1281};var Nt=class{source;control;destination;scale;transform;constructor(A,t,n,s,o){this.source=A,this.control=t,this.destination=n,this.scale=s,this.transform=o}writeArticulator(){let A=new b(12);return H(A,this.source),H(A,this.control),H(A,this.destination),H(A,this.transform),AA(A,this.scale<<16),A}};function qo(e,A){if(e)switch(A){default:return;case y.modulationWheel:return q.modulationWheel;case y.mainVolume:return q.volume;case y.pan:return q.pan;case y.expressionController:return q.expression;case y.chorusDepth:return q.chorus;case y.reverbDepth:return q.reverb}else switch(A){default:return;case j.noteOnKeyNum:return q.keyNum;case j.noteOnVelocity:return q.velocity;case j.noController:return q.none;case j.polyPressure:return q.polyPressure;case j.channelPressure:return q.channelPressure;case j.pitchWheel:return q.pitchWheel;case j.pitchWheelRange:return q.pitchWheelRange}}function Po(e,A){switch(e){default:return;case a.initialAttenuation:return{dest:R.gain,amount:-A};case a.fineTune:return R.pitch;case a.pan:return R.pan;case a.keyNum:return R.keyNum;case a.reverbEffectsSend:return R.reverbSend;case a.chorusEffectsSend:return R.chorusSend;case a.freqModLFO:return R.modLfoFreq;case a.delayModLFO:return R.modLfoDelay;case a.delayVibLFO:return R.vibLfoDelay;case a.freqVibLFO:return R.vibLfoFreq;case a.delayVolEnv:return R.volEnvDelay;case a.attackVolEnv:return R.volEnvAttack;case a.holdVolEnv:return R.volEnvHold;case a.decayVolEnv:return R.volEnvDecay;case a.sustainVolEnv:return{dest:R.volEnvSustain,amount:1e3-A};case a.releaseVolEnv:return R.volEnvRelease;case a.delayModEnv:return R.modEnvDelay;case a.attackModEnv:return R.modEnvAttack;case a.holdModEnv:return R.modEnvHold;case a.decayModEnv:return R.modEnvDecay;case a.sustainModEnv:return{dest:R.modEnvSustain,amount:1e3-A};case a.releaseModEnv:return R.modEnvRelease;case a.initialFilterFc:return R.filterCutoff;case a.initialFilterQ:return R.filterQ}}function Vo(e,A){switch(e){default:return;case a.modEnvToFilterFc:return{source:q.modEnv,dest:R.filterCutoff,amt:A,isBipolar:!1};case a.modEnvToPitch:return{source:q.modEnv,dest:R.pitch,amt:A,isBipolar:!1};case a.modLfoToFilterFc:return{source:q.modLfo,dest:R.filterCutoff,amt:A,isBipolar:!0};case a.modLfoToVolume:return{source:q.modLfo,dest:R.gain,amt:A,isBipolar:!0};case a.modLfoToPitch:return{source:q.modLfo,dest:R.pitch,amt:A,isBipolar:!0};case a.vibLfoToPitch:return{source:q.vibratoLfo,dest:R.pitch,amt:A,isBipolar:!0};case a.keyNumToVolEnvHold:return{source:q.keyNum,dest:R.volEnvHold,amt:A,isBipolar:!0};case a.keyNumToVolEnvDecay:return{source:q.keyNum,dest:R.volEnvDecay,amt:A,isBipolar:!0};case a.keyNumToModEnvHold:return{source:q.keyNum,dest:R.modEnvHold,amt:A,isBipolar:!0};case a.keyNumToModEnvDecay:return{source:q.keyNum,dest:R.modEnvDecay,amt:A,isBipolar:!0};case a.scaleTuning:return{source:q.keyNum,dest:R.pitch,amt:A*128,isBipolar:!1}}}function Zo(e){let A=Po(e.generatorType,e.generatorValue),t=A,n=0,s=e.generatorValue;A?.amount!==void 0&&(s=A.amount,t=A.dest);let o=Vo(e.generatorType,e.generatorValue);if(o!==void 0)s=o.amt,t=o.dest,n=o.source;else if(t===void 0){Y(`Invalid generator type: ${e.generatorType}`);return}return new Nt(n,0,t,s,0)}function Xo(e){if(e.transformType!==0){Y("Other transform types are not supported.");return}let A=qo(e.sourceUsesCC,e.sourceIndex),t=e.sourceCurveType,n=e.sourcePolarity,s=e.sourceDirection;if(A===void 0){Y(`Invalid source: ${e.sourceIndex}, CC: ${e.sourceUsesCC}`);return}e.modulatorDestination===a.initialAttenuation&&(s=s===1?0:1);let o=qo(e.secSrcUsesCC,e.secSrcIndex),r=e.secSrcCurveType,C=e.secSrcPolarity,i=e.secSrcDirection;if(o===void 0){Y(`Invalid secondary source: ${e.secSrcIndex}, CC: ${e.secSrcUsesCC}`);return}let g=Po(e.modulatorDestination,e.transformAmount),c=g,h=e.transformAmount;g?.dest!==void 0&&(c=g.dest,h=g.amount);let d=Vo(e.modulatorDestination,e.transformAmount);if(d!==void 0)h=d.amt,o=A,r=t,C=n,i=s,t=GA.linear,n=d.isBipolar?1:0,s=0,A=d.source,c=d.dest;else if(c===void 0){Y(`Invalid destination: ${e.modulatorDestination}`);return}let B=0;return B|=r<<4,B|=C<<8,B|=i<<9,B|=t,B|=n<<14,B|=s<<15,new Nt(A,o,c,h,B)}var yi=new Set([a.sampleModes,a.initialAttenuation,a.keyRange,a.velRange,a.sampleID,a.fineTune,a.coarseTune,a.startAddrsOffset,a.startAddrsCoarseOffset,a.endAddrOffset,a.endAddrsCoarseOffset,a.startloopAddrsOffset,a.startloopAddrsCoarseOffset,a.endloopAddrsOffset,a.endloopAddrsCoarseOffset,a.overridingRootKey,a.exclusiveClass]);function cn(e){for(let o=0;of.generatorType===C);if(i===void 0)continue;let g=r.generatorValue*-128,c=60/128*g,h=i.generatorValue-c,d=e.generators.indexOf(r),B=e.generators.indexOf(i);e.generators[B]=new U(C,h,!1),e.generators[d]=new U(r.generatorType,g,!1)}let A=e.generators.reduce((o,r)=>{if(yi.has(r.generatorType))return o;let C=Zo(r);return C!==void 0?(o.push(C),p("%cSucceeded converting to DLS Articulator!",I.recognized)):Y("Failed converting to DLS Articulator!"),o},[]),t=e.modulators.reduce((o,r)=>{if(z.isIdentical(r,En,!0)||z.isIdentical(r,Cn,!0)||z.isIdentical(r,Bn,!0)||z.isIdentical(r,hn,!0))return o;let C=Xo(r);return C!==void 0?(o.push(C),p("%cSucceeded converting to DLS Articulator!",I.recognized)):Y("Failed converting to DLS Articulator!"),o},[]);A.push(...t);let n=new b(8);AA(n,8),AA(n,A.length);let s=A.map(o=>o.writeArticulator());return _("art2",wA([n,...s]))}function Wo(e,A){let t=new b(12);H(t,Math.max(e.keyRange.min,0)),H(t,e.keyRange.max),H(t,Math.max(e.velRange.min,0)),H(t,e.velRange.max),H(t,0);let n=e.getGeneratorValue(a.exclusiveClass,0);H(t,n),H(t,0);let s=_("rgnh",t),o=e.getGeneratorValue(a.overridingRootKey,e.sample.samplePitch);e.getGeneratorValue(a.scaleTuning,A.getGeneratorValue(a.scaleTuning,100))===0&&e.keyRange.max-e.keyRange.min===0&&(o=e.keyRange.min);let C=gn(e.sample,o,e.getGeneratorValue(a.fineTune,0)+e.getGeneratorValue(a.coarseTune,0)*100+e.sample.samplePitchCorrection,e.getGeneratorValue(a.initialAttenuation,0),e.sample.sampleLoopStartIndex+e.getGeneratorValue(a.startloopAddrsOffset,0)+e.getGeneratorValue(a.startloopAddrsCoarseOffset,0)*32768,e.sample.sampleLoopEndIndex+e.getGeneratorValue(a.endloopAddrsOffset,0)+e.getGeneratorValue(a.endloopAddrsCoarseOffset,0)*32768,e.getGeneratorValue(a.sampleModes,0)),i=new b(12);H(i,0),H(i,0),AA(i,1),AA(i,this.samples.indexOf(e.sample));let g=_("wlnk",i),c=new b(0);if(e.modulators.length+e.generators.length>0){let h=cn(e);c=_("lar2",h,!1,!0)}return _("rgn2",wA([s,C,g,c]),!1,!0)}function _o(e){pA(`%cWriting %c${e.presetName}%c...`,I.info,I.recognized,I.info);let A=Ko(e),t=A.reduce((d,B)=>B.isGlobal?d:d+1,0),n=new b(12);AA(n,t);let s=(e.bank&127)<<8;e.bank===128&&(s|=1<<31),AA(n,s),AA(n,e.program&127);let o=_("insh",n),r=new b(0),C=A.find(d=>d.isGlobal===!0);if(C){let d=cn(C);r=_("lar2",d,!1,!0)}let i=wA(A.reduce((d,B)=>(B.isGlobal||d.push(Wo.apply(this,[B,C])),d),[])),g=_("lrgn",i,!1,!0),c=_("INAM",fe(e.presetName)),h=_("INFO",c,!1,!0);return P(),_("ins ",wA([o,g,r,h]),!1,!0)}function zo(){let e=wA(this.presets.map(A=>_o.apply(this,[A])));return _("lins",e,!1,!0)}function jo(e){let A=new b(18);H(A,1),H(A,1),AA(A,e.sampleRate),AA(A,e.sampleRate*2),H(A,2),H(A,16);let t=_("fmt ",A),n=1;e.sampleLoopStartIndex+Math.abs(e.getAudioData().length-e.sampleLoopEndIndex)<2&&(n=0);let s=gn(e,e.samplePitch,e.samplePitchCorrection,0,e.sampleLoopStartIndex,e.sampleLoopEndIndex,n),o=e.getAudioData(),r;if(e.isCompressed){let g=new Int16Array(o.length);for(let c=0;c{let s=jo(n);return A.push(e),e+=s.length,s});return{data:_("wvpl",wA(t),!1,!0),indexes:A}}function Ar(){pA("%cSaving DLS...",I.info);let e=new b(4);AA(e,this.presets.length);let A=_("colh",e);pA("%cWriting instruments...",I.info);let t=zo.apply(this);p("%cSuccess!",I.recognized),P(),pA("%cWriting WAVE samples...",I.info);let n=$o.apply(this),s=n.data,o=n.indexes;p("%cSucceeded!",I.recognized),P();let r=new b(8+4*o.length);AA(r,8),AA(r,o.length);for(let h of o)AA(r,h);let C=_("ptbl",r);this.soundFontInfo.ICMT=(this.soundFontInfo.ICMT||"Soundfont")+` +Converted from SF2 to DLS using SpessaSynth`,this.soundFontInfo.ISFT="SpessaSynth";let i=[];for(let[h,d]of Object.entries(this.soundFontInfo))h!=="ICMT"&&h!=="INAM"&&h!=="ICRD"&&h!=="IENG"&&h!=="ICOP"&&h!=="ISFT"&&h!=="ISBJ"||i.push(_(h,fe(d),!0));let g=_("INFO",wA(i),!1,!0),c=new b(A.length+t.length+C.length+s.length+g.length+4);return TA(c,"DLS "),c.set(wA([A,t,C,s,g]),4),p("%cSaved succesfully!",I.recognized),P(),_("RIFF",c)}var Si=48e3,ve=class{sampleName;sampleRate;samplePitch;samplePitchCorrection;sampleLink;sampleType;sampleLoopStartIndex;sampleLoopEndIndex;isCompressed;compressedData=void 0;useCount=0;sampleData=void 0;constructor(A,t,n,s,o,r,C,i){this.sampleName=A,this.sampleRate=t,this.samplePitch=n,this.samplePitchCorrection=s,this.sampleLink=o,this.sampleType=r,this.sampleLoopStartIndex=C,this.sampleLoopEndIndex=i,this.isCompressed=(r&16)>0}getRawData(){let A=new Uint8Array(this.sampleData.length*2);for(let t=0;t>8&255}return A}resampleData(A){let t=this.getAudioData(),n=A/this.sampleRate,s=new Float32Array(Math.floor(t.length*n));for(let o=0;o96e3)&&(this.resampleData(Si),n=this.getAudioData()),this.compressedData=t([n],1,this.sampleRate,A),this.sampleType|=16,this.isCompressed=!0}catch{Y(`Failed to compress ${this.sampleName}. Leaving as uncompressed!`),this.isCompressed=!1,this.compressedData=void 0,this.sampleType&=239}}getAudioData(){return this.sampleData}};var He=class{instrumentName="";instrumentZones=[];_useCount=0;get useCount(){return this._useCount}addUseCount(){this._useCount++,this.instrumentZones.forEach(A=>A.useCount++)}removeUseCount(){this._useCount--;for(let A=0;AA.deleteZone()),this.instrumentZones.length=0}safeDeleteZone(A){return this.instrumentZones[A].useCount--,this.instrumentZones[A].useCount<1?(this.deleteZone(A),!0):!1}deleteZone(A){this.instrumentZones[A].deleteZone(),this.instrumentZones.splice(A,1)}};var Ye=class{parentSoundBank;presetName="";program=0;bank=0;presetZones=[];foundSamplesAndGenerators=[];library=0;genre=0;morphology=0;constructor(A){this.parentSoundBank=A;for(let t=0;t<128;t++)this.foundSamplesAndGenerators[t]=[]}isDrumPreset(A,t=!1){let n=A&&this.parentSoundBank.isXGBank;return this.bank===128||n&&jA(this.bank)&&(this.bank!==126||t)}deletePreset(){this.presetZones.forEach(A=>A.deleteZone()),this.presetZones.length=0}deleteZone(A){this.presetZones[A].deleteZone(),this.presetZones.splice(A,1)}preload(A,t){for(let n=A;n{o.sample.isSampleLoaded||o.sample.getAudioData()})}preloadSpecific(A,t){this.getSamplesAndGenerators(A,t).forEach(n=>{n.sample.isSampleLoaded||n.sample.getAudioData()})}getSamplesAndGenerators(A,t){let n=this.foundSamplesAndGenerators[A][t];if(n)return n;if(this.presetZones.length<1)return[];function s(B,f){return f>=B.min&&f<=B.max}function o(B,f){B.push(...f.filter(m=>!B.find(S=>S.generatorType===m.generatorType)))}function r(B,f){B.push(...f.filter(m=>!B.find(S=>z.isIdentical(m,S))))}let C=[],i=this.presetZones[0].isGlobal?[...this.presetZones[0].generators]:[],g=this.presetZones[0].isGlobal?[...this.presetZones[0].modulators]:[],c=this.presetZones[0].isGlobal?this.presetZones[0].keyRange:{min:0,max:127},h=this.presetZones[0].isGlobal?this.presetZones[0].velRange:{min:0,max:127};return this.presetZones.filter(B=>s(B.hasKeyRange?B.keyRange:c,A)&&s(B.hasVelRange?B.velRange:h,t)&&!B.isGlobal).forEach(B=>{if(B.instrument.instrumentZones.length<1)return;let f=B.generators,m=B.modulators,S=B.instrument.instrumentZones[0],M=S.isGlobal?[...S.generators]:[],D=S.isGlobal?[...S.modulators]:[],x=S.isGlobal?S.keyRange:{min:0,max:127},G=S.isGlobal?S.velRange:{min:0,max:127};B.instrument.instrumentZones.filter(E=>s(E.hasKeyRange?E.keyRange:x,A)&&s(E.hasVelRange?E.velRange:G,t)&&!E.isGlobal).forEach(E=>{let L=[...E.generators],Z=[...E.modulators];o(f,i),o(L,M),r(m,g),r(Z,D),r(Z,this.parentSoundBank.defaultModulators);let EA=[...Z];for(let tA=0;tAz.isIdentical(T,BA));$!==-1?EA[$]=EA[$].sumTransform(T):EA.push(T)}C.push({instrumentGenerators:L,presetGenerators:f,modulators:EA,sample:E.sample,sampleID:E.generators.find(tA=>tA.generatorType===a.sampleID).generatorValue})})}),this.foundSamplesAndGenerators[A][t]=C,C}};var Je=class e{soundFontInfo={};presets=[];samples=[];instruments=[];defaultModulators=Ks.map(A=>z.copy(A));isXGBank=!1;constructor(A=void 0){A?.presets&&(this.presets.push(...A.presets),this.soundFontInfo=A.info)}static mergeSoundBanks(...A){let t=A.shift(),n=t.presets;for(;A.length;)A.shift().presets.forEach(o=>{n.find(r=>r.bank===o.bank&&r.program===o.program)===void 0&&n.push(o)});return new e({presets:n,info:t.soundFontInfo})}static getDummySoundfontFile(){let A=new e,t=new ve("Saw",44100,65,20,0,0,0,127);t.sampleData=new Float32Array(128);for(let g=0;g<128;g++)t.sampleData[g]=g/128*2-1;A.samples.push(t);let n=new qA;n.isGlobal=!0,n.generators.push(new U(a.initialAttenuation,375)),n.generators.push(new U(a.releaseVolEnv,-1e3)),n.generators.push(new U(a.sampleModes,1));let s=new qA;s.sample=t;let o=new qA;o.sample=t,o.generators.push(new U(a.fineTune,-9));let r=new He;r.instrumentName="Saw Wave",r.instrumentZones.push(n),r.instrumentZones.push(s),r.instrumentZones.push(o),A.instruments.push(r);let C=new Te;C.instrument=r;let i=new Ye(A);return i.presetName="Saw Wave",i.presetZones.push(C),A.presets.push(i),A.soundFontInfo.ifil="2.1",A.soundFontInfo.isng="EMU8000",A.soundFontInfo.INAM="Dummy",A._parseInternal(),A.write().buffer}_parseInternal(){this.isXGBank=!1;let A=new Set([0,1,2,3,4,5,6,7,8,9,16,17,24,25,27,28,29,30,31,32,33,40,41,48,56,57,58,64,65,66,126,127]);for(let t of this.presets)if(jA(t.bank)&&(this.isXGBank=!0,!A.has(t.program))){this.isXGBank=!1,p(`%cThis bank is not valid XG. Preset %c${t.bank}:${t.program}%c is not a valid XG drum. XG mode will use presets on bank 128.`,I.info,I.value,I.info);break}}trimSoundBank(A){let t=this;function n(o,r){let C=0;for(let i=0;i=c.min&&B.key<=c.max&&B.velocity>=h.min&&B.velocity<=h.max){d=!0;break}d||(p(`%c${g.sample.sampleName} %cremoved from %c${o.instrumentName}%c. Use count: %c${g.useCount-1}`,I.recognized,I.info,I.recognized,I.info,I.recognized),o.safeDeleteZone(i)&&(C++,i--,p(`%c${g.sample.sampleName} %cdeleted`,I.recognized,I.info)),g.sample.useCount<1&&t.deleteSample(g.sample))}return C}se("%cTrimming soundfont...",I.info);let s=A.getUsedProgramsAndKeys(t);pA("%cModifying soundfont...",I.info),p("Detected keys for midi:",s);for(let o=0;o{let d=h.split("-");return{key:parseInt(d[0]),velocity:parseInt(d[1])}});pA(`%cTrimming %c${r.presetName}`,I.info,I.recognized),p(`Keys for ${r.presetName}:`,g);let c=0;for(let h=0;h=B.min&&S.key<=B.max&&S.velocity>=f.min&&S.velocity<=f.max){m=!0;let M=n(d.instrument,g);p(`%cTrimmed off %c${M}%c zones from %c${d.instrument.instrumentName}`,I.info,I.recognized,I.info,I.recognized);break}m||(c++,r.deleteZone(h),d.instrument.useCount<1&&t.deleteInstrument(d.instrument),h--)}p(`%cTrimmed off %c${c}%c zones from %c${r.presetName}`,I.info,I.recognized,I.info,I.recognized),P()}}t.removeUnusedElements(),t.soundFontInfo.ICMT=`NOTE: This soundfont was trimmed by SpessaSynth to only contain presets used in "${A.midiName}" + +`+t.soundFontInfo.ICMT,p("%cSoundfont modified!",I.recognized),P(),P()}removeUnusedElements(){this.instruments.forEach(A=>{A.useCount<1&&A.instrumentZones.forEach(t=>{t.isGlobal||t.sample.useCount--})}),this.instruments=this.instruments.filter(A=>A.useCount>0),this.samples=this.samples.filter(A=>A.useCount>0)}deleteInstrument(A){if(A.useCount>0)throw new Error(`Cannot delete an instrument that has ${A.useCount} usages.`);this.instruments.splice(this.instruments.indexOf(A),1),A.deleteInstrument(),this.removeUnusedElements()}deletePreset(A){A.deletePreset(),this.presets.splice(this.presets.indexOf(A),1),this.removeUnusedElements()}deleteSample(A){if(A.useCount>0)throw new Error(`Cannot delete sample that has ${A.useCount} usages.`);this.samples.splice(this.samples.indexOf(A),1),this.removeUnusedElements()}getPresetNoFallback(A,t,n=!1){let s=A===128||n&&jA(A),o;if(s?o=this.presets.find(r=>r.bank===A&&r.isDrumPreset(n)&&r.program===t):o=this.presets.find(r=>r.bank===A&&r.program===t),o)return o;if(s&&n){let r=this.presets.find(C=>C.isDrumPreset(n)&&C.program===t);if(r)return r}}getPreset(A,t,n=!1){let s=A===128||n&&jA(A),o;return s?o=this.presets.find(r=>r.bank===A&&r.isDrumPreset(n)&&r.program===t):o=this.presets.find(r=>r.bank===A&&r.program===t),o||(s?(o=this.presets.find(r=>r.isDrumPreset(n)&&r.program===t),o||(o=this.presets.find(r=>r.isDrumPreset(n)))):o=this.presets.find(r=>r.program===t&&!r.isDrumPreset(n)),o&&Y(`%cPreset ${A}.${t} not found. Replaced with %c${o.presetName} (${o.bank}.${o.program})`,I.warn,I.recognized),o||(Y(`Preset ${t} not found. Defaulting to`,this.presets[0].presetName),o=this.presets[0]),o)}getPresetByName(A){let t=this.presets.find(n=>n.presetName===A);return t||(Y("Preset not found. Defaulting to:",this.presets[0].presetName),t=this.presets[0]),t}parsingError(A){throw new Error(`SF parsing error: ${A} The file may be corrupted.`)}destroySoundBank(){delete this.presets,delete this.instruments,delete this.samples}};Je.prototype.write=Jo;Je.prototype.writeDLS=Ar;function er(e){pA("%cLoading instruments...",I.info);for(let A=0;A>8&127,o=t&127;s>0?this.bank=s:this.bank=o,t>>31&&(this.bank=128),this.DLSInstrument=new He,this.DLSInstrument.addUseCount();let C=new Te;C.instrument=this.DLSInstrument,this.presetZones=[C]}};function tr(e){this.verifyHeader(e,"LIST"),this.verifyText(eA(e.chunkData,4),"ins ");let A=[];for(;e.chunkData.length>e.chunkData.currentIndex;)A.push(IA(e.chunkData));let t=A.find(B=>B.header==="insh");if(!t)throw P(),new Error("No instrument header!");let n=N(t.chunkData,4),s=N(t.chunkData,4),o=N(t.chunkData,4),r=new ln(this,s,o),C="unnamedPreset",i=ZA(A,"INFO");if(i){let B=IA(i.chunkData);for(;B.header!=="INAM";)B=IA(i.chunkData);C=eA(B.chunkData,B.chunkData.length).trim()}r.presetName=C,r.DLSInstrument.instrumentName=C,pA(`%cParsing %c"${C}"%c...`,I.info,I.recognized,I.info);let g=ZA(A,"lrgn");if(!g)throw P(),new Error("No region list!");let c=new qA;c.isGlobal=!0;let h=ZA(A,"lart"),d=ZA(A,"lar2");(d!==void 0||h!==void 0)&&this.readLart(h,d,c),c.generators=c.generators.filter(B=>B.generatorValue!==X[B.generatorType].def),c.modulators.find(B=>B.modulatorDestination===a.reverbEffectsSend)===void 0&&c.modulators.push(z.copy(Cn)),c.modulators.find(B=>B.modulatorDestination===a.chorusEffectsSend)===void 0&&c.modulators.push(z.copy(En)),r.DLSInstrument.instrumentZones.push(c);for(let B=0;B>10&15;D===GA.linear&&M!==GA.linear&&(D=M);let x=n>>14&1,G=n>>15&1;r===a.initialAttenuation&&s<0&&(G=1),d=re(D,x,G,C.isCC,C.enum)}r===a.initialAttenuation&&(c=Math.max(960,Math.min(0,c)));let B=n>>4&15,f=n>>8&1,m=n>>9&1,S=re(B,f,m,h.isCC,h.enum);if(i){let M=S;S=d,d=M}return new z(d,S,r,c,0)}function ss(e,A){let t=e.chunkData,n=[],s=[];N(t,4);let o=N(t,4);for(let r=0;r>16;if(C===0&&i===0&&c===0){let B;switch(g){case R.pan:B=new U(a.pan,d);break;case R.gain:B=new U(a.initialAttenuation,-d*10/.4);break;case R.filterCutoff:B=new U(a.initialFilterFc,d);break;case R.filterQ:B=new U(a.initialFilterQ,d);break;case R.modLfoFreq:B=new U(a.freqModLFO,d);break;case R.modLfoDelay:B=new U(a.delayModLFO,d);break;case R.vibLfoFreq:B=new U(a.freqVibLFO,d);break;case R.vibLfoDelay:B=new U(a.delayVibLFO,d);break;case R.volEnvDelay:B=new U(a.delayVolEnv,d);break;case R.volEnvAttack:B=new U(a.attackVolEnv,d);break;case R.volEnvHold:B=new U(a.holdVolEnv,d,!1);break;case R.volEnvDecay:B=new U(a.decayVolEnv,d,!1);break;case R.volEnvRelease:B=new U(a.releaseVolEnv,d);break;case R.volEnvSustain:let f=1e3-d;B=new U(a.sustainVolEnv,f);break;case R.modEnvDelay:B=new U(a.delayModEnv,d);break;case R.modEnvAttack:B=new U(a.attackModEnv,d);break;case R.modEnvHold:B=new U(a.holdModEnv,d,!1);break;case R.modEnvDecay:B=new U(a.decayModEnv,d,!1);break;case R.modEnvRelease:B=new U(a.releaseModEnv,d);break;case R.modEnvSustain:let m=1e3-d;B=new U(a.sustainModEnv,m);break;case R.reverbSend:B=new U(a.reverbEffectsSend,d);break;case R.chorusSend:B=new U(a.chorusEffectsSend,d);break;case R.pitch:let S=Math.floor(d/100),M=Math.floor(d-S*100);B=new U(a.fineTune,M),n.push(new U(a.coarseTune,S));break}B&&n.push(B)}else{let B=!0,f=(m,S,M)=>{let D=m/-128;if(n.push(new U(S,D)),D<=120){let x=Math.round(.46875*m);n.forEach(G=>{G.generatorType===M&&(G.generatorValue+=x)})}};if(i===q.none?C===q.modLfo&&g===R.pitch?n.push(new U(a.modLfoToPitch,d)):C===q.modLfo&&g===R.gain?n.push(new U(a.modLfoToVolume,d)):C===q.modLfo&&g===R.filterCutoff?n.push(new U(a.modLfoToFilterFc,d)):C===q.vibratoLfo&&g===R.pitch?n.push(new U(a.vibLfoToPitch,d)):C===q.modEnv&&g===R.pitch?n.push(new U(a.modEnvToPitch,d)):C===q.modEnv&&g===R.filterCutoff?n.push(new U(a.modEnvToFilterFc,d)):C===q.keyNum&&g===R.pitch?n.push(new U(a.scaleTuning,d/128)):C===q.keyNum&&g===R.volEnvHold?f(d,a.keyNumToVolEnvHold,a.holdVolEnv):C===q.keyNum&&g===R.volEnvDecay?f(d,a.keyNumToVolEnvDecay,a.decayVolEnv):C===q.keyNum&&g===R.modEnvHold?f(d,a.keyNumToModEnvHold,a.holdModEnv):C===q.keyNum&&g===R.modEnvDecay?f(d,a.keyNumToModEnvDecay,a.decayModEnv):B=!1:B=!1,B===!1){let m=sr(C,i,g,c,d);m?(s.push(m),p("%cSucceeded converting to SF2 Modulator!",I.recognized)):Y("Failed converting to SF2 Modulator!")}}}return A&&s.push(z.copy(Bn),z.copy(hn)),{modulators:s,generators:n}}function or(e,A,t){if(e)for(;e.chunkData.currentIndexe.chunkData.currentIndex;)A.push(IA(e.chunkData));let t=A.find(Z=>Z.header==="rgnh"),n=N(t.chunkData,2),s=N(t.chunkData,2),o=N(t.chunkData,2),r=N(t.chunkData,2);o===0&&r===0&&(r=127,o=0);let C=new Qn({min:n,max:s},{min:o,max:r});N(t.chunkData,2);let i=N(t.chunkData,2);i!==0&&C.generators.push(new U(a.exclusiveClass,i));let g=ZA(A,"lart"),c=ZA(A,"lar2");this.readLart(g,c,C),C.isGlobal=!1;let h=A.find(Z=>Z.header==="wsmp");N(h.chunkData,4);let d=N(h.chunkData,2),B=Ne(h.chunkData[h.chunkData.currentIndex++],h.chunkData[h.chunkData.currentIndex++]),m=(N(h.chunkData,4)|0)/-655360;N(h.chunkData,4);let S=N(h.chunkData,4),M,D={start:0,end:0};if(S===0)M=0;else{N(h.chunkData,4),N(h.chunkData,4)===0?M=1:M=3,D.start=N(h.chunkData,4);let EA=N(h.chunkData,4);D.end=D.start+EA}let x=A.find(Z=>Z.header==="wlnk");if(x===void 0)return;N(x.chunkData,2),N(x.chunkData,2),N(x.chunkData,4);let G=N(x.chunkData,4),F=this.samples[G];if(F===void 0)throw new Error("Invalid sample ID!");let L=(m||F.sampleDbAttenuation)*10/.4;return C.setWavesample(L,M,D,d,F,G,B),C}var dn=class extends ve{sampleDbAttenuation;sampleData;constructor(A,t,n,s,o,r,C,i){super(A,t,n,s,0,1,o,r),this.sampleData=C,this.sampleDbAttenuation=i}getAudioData(){return this.sampleData}getRawData(){if(this.isCompressed){if(!this.compressedData)throw new Error("Compressed but no data?? This shouldn't happen!!");return this.compressedData}return super.getRawData()}};var ir={PCM:1,ALAW:6};function wi(e,A){let t=Math.pow(2,A*8-1),n=Math.pow(2,A*8),s,o=!1;A===1?(s=255,o=!0):s=t;let r=e.size/A,C=new Float32Array(r);for(let i=0;i=t&&(g-=n),C[i]=g/s)}return C}function Fi(e,A){let t=e.size/A,n=new Float32Array(t);for(let s=0;s>4,i=r&15;C>0&&(i+=16),i=(i<<4)+8,C>1&&(i=i<127?i:-i;n[s]=g/32678}return n}function ar(e){pA("%cLoading Wave samples...",I.recognized);let A=0;for(;e.chunkData.currentIndexF.header==="fmt ");if(!s)throw new Error("No fmt chunk in the wave file!");let o=N(s.chunkData,2),r=N(s.chunkData,2);if(r!==1)throw new Error(`Only mono samples are supported. Fmt reports ${r} channels`);let C=N(s.chunkData,4);N(s.chunkData,4),N(s.chunkData,2);let g=N(s.chunkData,2)/8,c=!1,h=n.find(F=>F.header==="data");h||this.parsingError("No data chunk in the WAVE chunk!");let d;switch(o){default:c=!0,d=new Float32Array(h.size/g);break;case ir.PCM:d=wi(h,g);break;case ir.ALAW:d=Fi(h,g);break}let B=ZA(n,"INFO"),f=`Unnamed ${A}`;if(B){let F=IA(B.chunkData);for(;F.header!=="INAM"&&B.chunkData.currentIndexF.header==="wsmp");if(G){N(G.chunkData,4),m=N(G.chunkData,2),S=Ne(G.chunkData[G.chunkData.currentIndex++],G.chunkData[G.chunkData.currentIndex++]);let F=Math.trunc(S/100);if(m+=F,S-=F*100,x=(N(G.chunkData,4)|0)/-655360,N(G.chunkData,4),N(G.chunkData,4)===1){N(G.chunkData,8),M=N(G.chunkData,4);let Z=N(G.chunkData,4);D=M+Z}}else Y("No wsmp chunk in wave... using sane defaults.");c&&console.error(`Failed to load '${f}': Unsupported format: (${o})`),this.samples.push(new dn(f,C,m,S,M,D,d,x)),A++,p(`%cLoaded sample %c${f}`,I.info,I.recognized)}P()}var we=class extends Je{constructor(A){super(),this.dataArray=new b(A),se("%cParsing DLS...",I.info),this.dataArray||(P(),this.parsingError("No data provided!"));let t=IA(this.dataArray,!1);this.verifyHeader(t,"riff"),this.verifyText(eA(this.dataArray,4).toLowerCase(),"dls ");let n=[];for(;this.dataArray.currentIndexi.header==="colh");o||(P(),this.parsingError("No colh chunk!")),this.instrumentAmount=N(o.chunkData,4),p(`%cInstruments amount: %c${this.instrumentAmount}`,I.info,I.recognized);let r=ZA(n,"wvpl");r||(P(),this.parsingError("No wvpl chunk!")),this.readDLSSamples(r);let C=ZA(n,"lins");C||(P(),this.parsingError("No lins chunk!")),this.readDLSInstrumentList(C),this.presets.sort((i,g)=>i.program-g.program+(i.bank-g.bank)),this._parseInternal(),p(`%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.`,I.info,I.recognized,I.info,I.recognized,I.info,I.recognized,I.info,I.recognized,I.info),P()}verifyHeader(A,...t){for(let n of t)if(A.header.toLowerCase()===n.toLowerCase())return;P(),this.parsingError(`Invalid DLS chunk header! Expected "${t.toString()}" got "${A.header.toLowerCase()}"`)}verifyText(A,t){A.toLowerCase()!==t.toLowerCase()&&(P(),this.parsingError(`FourCC error: Expected "${t.toLowerCase()}" got "${A.toLowerCase()}"`))}parsingError(A){throw new Error(`DLS parse error: ${A} The file may be corrupted.`)}destroySoundBank(){super.destroySoundBank(),delete this.dataArray}};we.prototype.readDLSInstrumentList=er;we.prototype.readDLSInstrument=tr;we.prototype.readRegion=rr;we.prototype.readLart=or;we.prototype.readDLSSamples=ar;var os=class extends ve{constructor(A,t,n,s,o,r,C,i,g,c,h,d,B){super(A,r,C,i,g,c,s-t/2,o-t/2),this.sampleName=A,this.sampleStartIndex=t,this.sampleEndIndex=n,this.isSampleLoaded=!1,this.sampleID=d,this.sampleLength=this.sampleEndIndex-this.sampleStartIndex,this.sampleDataArray=h,this.sampleData=new Float32Array(0),this.isCompressed&&(this.sampleLoopStartIndex+=this.sampleStartIndex/2,this.sampleLoopEndIndex+=this.sampleStartIndex/2,this.sampleLength=99999999),this.isDataRaw=B}getRawData(){let A=this.sampleDataArray;if(this.isCompressed){if(this.compressedData)return this.compressedData;let t=A.currentIndex;return A.slice(this.sampleStartIndex/2+t,this.sampleEndIndex/2+t)}else{this.isDataRaw||super.getRawData();let t=A.currentIndex;return A.slice(t+this.sampleStartIndex,t+this.sampleEndIndex)}}decodeVorbis(){if(this.sampleLength<1)return;let A=this.sampleDataArray,t=A.currentIndex,n=A.slice(this.sampleStartIndex/2+t,this.sampleEndIndex/2+t);this.sampleData=new Float32Array(0);try{let s=me.decode(n.buffer);this.sampleData=s.data[0],this.sampleData===void 0&&Y(`Error decoding sample ${this.sampleName}: Vorbis decode returned undefined.`)}catch(s){Y(`Error decoding sample ${this.sampleName}: ${s}`),this.sampleData=new Float32Array(this.sampleLoopEndIndex+1)}}getAudioData(){return this.isSampleLoaded?this.sampleData:this.sampleLength<1?(Y(`Invalid sample ${this.sampleName}! Invalid length: ${this.sampleLength}`),new Float32Array(1)):this.isCompressed?(this.decodeVorbis(),this.isSampleLoaded=!0,this.sampleData):this.isDataRaw?this.loadUncompressedData():this.getUncompressedReadyData()}loadUncompressedData(){if(this.isCompressed)return Y("Trying to load a compressed sample via loadUncompressedData()... aborting!"),new Float32Array(0);let A=new Float32Array(this.sampleLength/2),t=this.sampleDataArray.currentIndex,n=new Int16Array(this.sampleDataArray.slice(t+this.sampleStartIndex,t+this.sampleEndIndex).buffer);for(let s=0;se.chunkData.currentIndex;){let o=Ri(s,e.chunkData,A,t);n.push(o),s++}return n.length>1&&n.pop(),n}function Ri(e,A,t,n){let s=eA(A,20),o=N(A,4)*2,r=N(A,4)*2,C=N(A,4),i=N(A,4),g=N(A,4),c=A[A.currentIndex++];c===255&&(c=60);let h=Ys(A[A.currentIndex++]),d=N(A,2),B=N(A,2);return new os(s,o,r,C,i,g,c,h,d,B,t,e,n)}var rs=class extends U{constructor(A){super();let t=A.currentIndex;this.generatorType=A[t+1]<<8|A[t],this.generatorValue=Ne(A[t+2],A[t+3]),A.currentIndex+=4}};function is(e){let A=[];for(;e.chunkData.length>e.chunkData.currentIndex;)A.push(new rs(e.chunkData));return A.length>1&&A.pop(),A}var as=class extends qA{constructor(A){super(),this.generatorZoneStartIndex=N(A,2),this.modulatorZoneStartIndex=N(A,2),this.modulatorZoneSize=0,this.generatorZoneSize=0,this.isGlobal=!0}setZoneSize(A,t){this.modulatorZoneSize=A,this.generatorZoneSize=t}getGenerators(A){for(let t=this.generatorZoneStartIndex;tn.generatorType===a.sampleID);t&&(this.sample=A[t.generatorValue],this.isGlobal=!1,this.sample.useCount++)}getKeyRange(){let A=this.generators.find(t=>t.generatorType===a.keyRange);A&&(this.keyRange.min=A.generatorValue&127,this.keyRange.max=A.generatorValue>>8&127)}getVelRange(){let A=this.generators.find(t=>t.generatorType===a.velRange);A&&(this.velRange.min=A.generatorValue&127,this.velRange.max=A.generatorValue>>8&127)}};function gr(e,A,t,n){let s=[];for(;e.chunkData.length>e.chunkData.currentIndex;){let o=new as(e.chunkData);if(s.length>0){let r=o.modulatorZoneStartIndex-s[s.length-1].modulatorZoneStartIndex,C=o.generatorZoneStartIndex-s[s.length-1].generatorZoneStartIndex;s[s.length-1].setZoneSize(r,C),s[s.length-1].getGenerators(A),s[s.length-1].getModulators(t),s[s.length-1].getSample(n),s[s.length-1].getKeyRange(),s[s.length-1].getVelRange()}s.push(o)}return s.length>1&&s.pop(),s}var Is=class extends Te{constructor(A){super(),this.generatorZoneStartIndex=N(A,2),this.modulatorZoneStartIndex=N(A,2),this.modulatorZoneSize=0,this.generatorZoneSize=0,this.isGlobal=!0}setZoneSize(A,t){this.modulatorZoneSize=A,this.generatorZoneSize=t}getGenerators(A){for(let t=this.generatorZoneStartIndex;tn.generatorType===a.instrument);t&&(this.instrument=A[t.generatorValue],this.instrument.addUseCount(),this.isGlobal=!1)}getKeyRange(){let A=this.generators.find(t=>t.generatorType===a.keyRange);A&&(this.keyRange.min=A.generatorValue&127,this.keyRange.max=A.generatorValue>>8&127)}getVelRange(){let A=this.generators.find(t=>t.generatorType===a.velRange);A&&(this.velRange.min=A.generatorValue&127,this.velRange.max=A.generatorValue>>8&127)}};function Cr(e,A,t,n){let s=[];for(;e.chunkData.length>e.chunkData.currentIndex;){let o=new Is(e.chunkData);if(s.length>0){let r=o.modulatorZoneStartIndex-s[s.length-1].modulatorZoneStartIndex,C=o.generatorZoneStartIndex-s[s.length-1].generatorZoneStartIndex;s[s.length-1].setZoneSize(r,C),s[s.length-1].getGenerators(A),s[s.length-1].getModulators(t),s[s.length-1].getInstrument(n),s[s.length-1].getKeyRange(),s[s.length-1].getVelRange()}s.push(o)}return s.length>1&&s.pop(),s}var gs=class extends Ye{constructor(A,t){super(t),this.presetName=eA(A.chunkData,20).trim().replace(/\d{3}:\d{3}/,""),this.program=N(A.chunkData,2),this.bank=N(A.chunkData,2),this.presetZoneStartIndex=N(A.chunkData,2),this.library=N(A.chunkData,4),this.genre=N(A.chunkData,4),this.morphology=N(A.chunkData,4),this.presetZonesAmount=0}getPresetZones(A,t){this.presetZonesAmount=A;for(let n=this.presetZoneStartIndex;ne.chunkData.currentIndex;){let s=new gs(e,t);if(n.length>0){let o=s.presetZoneStartIndex-n[n.length-1].presetZoneStartIndex;n[n.length-1].getPresetZones(o,A)}n.push(s)}return n.length>1&&n.pop(),n}var Cs=class extends He{constructor(A){super(),this.instrumentName=eA(A.chunkData,20).trim(),this.instrumentZoneIndex=N(A.chunkData,2),this.instrumentZonesAmount=0}getInstrumentZones(A,t){this.instrumentZonesAmount=A;for(let n=this.instrumentZoneIndex;ne.chunkData.currentIndex;){let n=new Cs(e);if(t.length>0){let s=n.instrumentZoneIndex-t[t.length-1].instrumentZoneIndex;t[t.length-1].getInstrumentZones(s,A)}t.push(n)}return t.length>1&&t.pop(),t}var Es=class extends z{constructor(A){let t=N(A,2),n=N(A,2),s=Ne(A[A.currentIndex++],A[A.currentIndex++]),o=N(A,2),r=N(A,2);super(t,o,n,s,r)}};function un(e){let A=[];for(;e.chunkData.length>e.chunkData.currentIndex;)A.push(new Es(e.chunkData));return A}var fn=class extends Je{constructor(A,t=!0){super(),t&&console.warn("Using the constructor directly is deprecated. Use loadSoundFont instead."),this.dataArray=new b(A),se("%cParsing SoundFont...",I.info),this.dataArray||(P(),this.parsingError("No data provided!"));let n=IA(this.dataArray,!1);this.verifyHeader(n,"riff");let s=eA(this.dataArray,4).toLowerCase();if(s!=="sfbk"&&s!=="sfpk")throw P(),new SyntaxError(`Invalid soundFont! Expected "sfbk" or "sfpk" got "${s}"`);let o=s==="sfpk",r=IA(this.dataArray);for(this.verifyHeader(r,"list"),eA(r.chunkData,4);r.chunkData.length>r.chunkData.currentIndex;){let tA=IA(r.chunkData),T;switch(tA.header.toLowerCase()){case"ifil":case"iver":T=`${N(tA.chunkData,2)}.${N(tA.chunkData,2)}`,this.soundFontInfo[tA.header]=T;break;case"icmt":T=eA(tA.chunkData,tA.chunkData.length,void 0,!1),this.soundFontInfo[tA.header]=T;break;case"dmod":let $=un(tA);$.pop(),T=`Modulators: ${$.length}`;let BA=this.defaultModulators;this.defaultModulators=$,this.defaultModulators.push(...BA.filter(sA=>!this.defaultModulators.find(oA=>z.isIdentical(sA,oA)))),this.soundFontInfo[tA.header]=tA.chunkData;break;default:T=eA(tA.chunkData,tA.chunkData.length),this.soundFontInfo[tA.header]=T}p(`%c"${tA.header}": %c"${T}"`,I.info,I.recognized)}let C=IA(this.dataArray,!1);this.verifyHeader(C,"list"),this.verifyText(eA(this.dataArray,4),"sdta"),p("%cVerifying smpl chunk...",I.warn);let i=IA(this.dataArray,!1);this.verifyHeader(i,"smpl");let g;if(o){p("%cSF2Pack detected, attempting to decode the smpl chunk...",I.info);try{g=me.decode(this.dataArray.buffer.slice(this.dataArray.currentIndex,this.dataArray.currentIndex+C.size-12)).data[0]}catch(tA){throw P(),new Error(`SF2Pack Ogg Vorbis decode error: ${tA}`)}p(`%cDecoded the smpl chunk! Length: %c${g.length}`,I.info,I.value)}else g=this.dataArray,this.sampleDataStartIndex=this.dataArray.currentIndex;p(`%cSkipping sample chunk, length: %c${C.size-12}`,I.info,I.value),this.dataArray.currentIndex+=C.size-12,p("%cLoading preset data chunk...",I.warn);let c=IA(this.dataArray);this.verifyHeader(c,"list"),eA(c.chunkData,4);let h=IA(c.chunkData);this.verifyHeader(h,"phdr");let d=IA(c.chunkData);this.verifyHeader(d,"pbag");let B=IA(c.chunkData);this.verifyHeader(B,"pmod");let f=IA(c.chunkData);this.verifyHeader(f,"pgen");let m=IA(c.chunkData);this.verifyHeader(m,"inst");let S=IA(c.chunkData);this.verifyHeader(S,"ibag");let M=IA(c.chunkData);this.verifyHeader(M,"imod");let D=IA(c.chunkData);this.verifyHeader(D,"igen");let x=IA(c.chunkData);this.verifyHeader(x,"shdr"),this.dataArray.currentIndex=this.sampleDataStartIndex,this.samples.push(...Ir(x,g,!o));let G=is(D),F=un(M),E=gr(S,G,F,this.samples);this.instruments=Br(m,E);let L=is(f),Z=un(B),EA=Cr(d,L,Z,this.instruments);this.presets.push(...Er(h,EA,this)),this.presets.sort((tA,T)=>tA.program-T.program+(tA.bank-T.bank)),this._parseInternal(),p(`%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.`,I.info,I.recognized,I.info,I.recognized,I.info,I.recognized,I.info,I.recognized,I.info),P(),o&&delete this.dataArray}verifyHeader(A,t){A.header.toLowerCase()!==t.toLowerCase()&&(P(),this.parsingError(`Invalid chunk header! Expected "${t.toLowerCase()}" got "${A.header.toLowerCase()}"`))}verifyText(A,t){A.toLowerCase()!==t.toLowerCase()&&(P(),this.parsingError(`Invalid FourCC: Expected "${t.toLowerCase()}" got "${A.toLowerCase()}"\``))}destroySoundBank(){super.destroySoundBank(),delete this.dataArray}};function bt(e){let A=e.slice(8,12),t=new b(A);return eA(t,4,void 0,!1).toLowerCase()==="dls "?new we(e):new fn(e,!1)}var mn=class{constructor(A,t){this.ready=t,this.reloadManager(A)}generatePresetList(){let A={};for(let t=this.soundfontList.length-1;t>=0;t--){let n=this.soundfontList[t],s=new Set;for(let o of n.soundfont.presets){let r=`${o.bank+n.bankOffset}-${o.program}`;s.has(r)||(s.add(r),A[r]=o.presetName)}}this.presetList=[];for(let[t,n]of Object.entries(A)){let s=t.split("-");this.presetList.push({presetName:n,program:parseInt(s[1]),bank:parseInt(s[0])})}}handleMessage(A,t){switch(A){case xt.addNewSoundFont:this.addNewSoundFont(t[0],t[1],t[2]);break;case xt.reloadSoundFont:this.reloadManager(t);break;case xt.deleteSoundFont:this.deleteSoundFont(t);break;case xt.rearrangeSoundFonts:this.rearrangeSoundFonts(t)}}getPresetList(){return this.presetList.slice()}reloadManager(A){let t=bt(A);this.soundfontList=[],this.soundfontList.push({id:"main",bankOffset:0,soundfont:t}),this.generatePresetList(),this.ready()}deleteSoundFont(A){if(this.soundfontList.length===0){Y("1 soundfont left. Aborting!");return}let t=this.soundfontList.findIndex(n=>n.id===A);if(t===-1){Y(`No soundfont with id of "${A}" found. Aborting!`);return}delete this.soundfontList[t].soundfont.presets,delete this.soundfontList[t].soundfont.instruments,delete this.soundfontList[t].soundfont.samples,this.soundfontList.splice(t,1),this.generatePresetList()}addNewSoundFont(A,t,n){if(this.soundfontList.find(s=>s.id===t)!==void 0)throw new Error("Cannot overwrite the existing soundfont. Use soundfontManager.delete(id) instead.");this.soundfontList.push({id:t,soundfont:bt(A),bankOffset:n}),this.generatePresetList(),this.ready()}rearrangeSoundFonts(A){this.soundfontList.sort((t,n)=>A.indexOf(t.id)-A.indexOf(n.id)),this.generatePresetList()}getPreset(A,t,n=!1){if(this.soundfontList.length<1)throw new Error("No soundfonts! This should never happen.");for(let o of this.soundfontList){let r=o.soundfont.getPresetNoFallback(A-o.bankOffset,t,n);if(r!==void 0)return r}if(A===128||n&&jA(A)){for(let o of this.soundfontList){let r=o.soundfont.presets.find(i=>i.isDrumPreset(n)&&i.program===t);if(r)return r;let C=o.soundfont.presets.find(i=>i.isDrumPreset(n));if(C)return C}return this.soundfontList[0].soundfont.presets[0]}else{for(let o of this.soundfontList){let r=o.soundfont.presets.find(C=>C.program===t&&!C.isDrumPreset(n));if(r)return r}return this.soundfontList[0].soundfont.presets[0]}}destroyManager(){this.soundfontList.forEach(A=>{A.soundfont.destroySoundBank()}),delete this.soundfontList}};var Qt={linear:0,nearestNeighbor:1,fourthOrder:2},lt=class{static getSampleLinear(A,t){let n=A.sample,s=n.cursor,o=n.sampleData;if(n.isLooping){let r=n.loopEnd-n.loopStart;for(let C=0;C=n.loopEnd;)s-=r;let i=~~s,g=i+1;for(;g>=n.loopEnd;)g-=r;let c=s-i,h=o[g],d=o[i];t[C]=d+(h-d)*c,s+=n.playbackStep*A.currentTuningCalculated}}else{if(n.loopingMode===2&&!A.isInRelease)return;for(let r=0;r=n.end){A.finished=!0;return}let g=s-C,c=o[i],h=o[C];t[r]=h+(c-h)*g,s+=n.playbackStep*A.currentTuningCalculated}}A.sample.cursor=s}static getSampleNearest(A,t){let n=A.sample,s=n.cursor,o=n.loopEnd-n.loopStart,r=n.sampleData;if(A.sample.isLooping)for(let C=0;C=n.loopEnd;)s-=o;let i=~~s+1;for(;i>=n.loopEnd;)i-=o;t[C]=r[i],s+=n.playbackStep*A.currentTuningCalculated}else{if(n.loopingMode===2&&!A.isInRelease)return;for(let C=0;C=n.end){A.finished=!0;return}t[C]=r[i],s+=n.playbackStep*A.currentTuningCalculated}}n.cursor=s}static getSampleCubic(A,t){let n=A.sample,s=n.cursor,o=n.sampleData;if(n.isLooping){let r=n.loopEnd-n.loopStart;for(let C=0;C=n.loopEnd;)s-=r;let i=~~s,g=i+1,c=g+1,h=c+1,d=s-i;g>=n.loopEnd&&(g-=r),c>=n.loopEnd&&(c-=r),h>=n.loopEnd&&(h-=r);let B=o[i],f=o[g],m=o[c],S=o[h],M=.5*(m-B),D=B-2.5*f+2*m-.5*S,x=.5*(S-B)+1.5*(f-m);t[C]=((x*d+D)*d+M)*d+f,s+=n.playbackStep*A.currentTuningCalculated}}else{if(n.loopingMode===2&&!A.isInRelease)return;for(let r=0;r=n.end||g>=n.end||c>=n.end){A.finished=!0;return}let d=o[C],B=o[i],f=o[g],m=o[c],S=.5*(f-d),M=d-2.5*B+2*f-.5*m,D=.5*(m-d)+1.5*(B-f);t[r]=((D*h+M)*h+S)*h+B,s+=n.playbackStep*A.currentTuningCalculated}}A.sample.cursor=s}};var Bs={addMapping:0,deleteMapping:1,clearMappings:2},pn=class{_keyMappings=[];handleMessage(A,t){switch(A){default:return;case Bs.addMapping:this.addMapping(...t);break;case Bs.clearMappings:this.clearMappings();break;case Bs.deleteMapping:this.deleteMapping(...t)}}addMapping(A,t,n){this._keyMappings[A]===void 0&&(this._keyMappings[A]=[]),this._keyMappings[A][t]=n}deleteMapping(A,t){this._keyMappings[A]?.[t]!==void 0&&(this._keyMappings[A][t]=void 0)}clearMappings(){this._keyMappings=[]}setMappings(A){this._keyMappings=A}getMappings(){return this._keyMappings}getVelocity(A,t){let n=this._keyMappings[A]?.[t];return n?n.velocity:-1}hasOverridePatch(A,t){let n=this._keyMappings[A]?.[t]?.patch?.bank;return n!==void 0&&n>=0}getPatch(A,t){let n=this._keyMappings[A]?.[t];if(n)return n.patch;throw new Error("No modifier.")}};var hr=.1,Ke=class e{static cachedCoefficients=[];a0=0;a1=0;a2=0;a3=0;a4=0;x1=0;x2=0;y1=0;y2=0;resonanceCb=0;currentInitialFc=13500;lastTargetCutoff=1/0;initialized=!1;static apply(A,t,n,s){let o=A.modulatedGenerators[a.initialFilterFc],r=A.filter;r.initialized?r.currentInitialFc+=(o-r.currentInitialFc)*s:(r.initialized=!0,r.currentInitialFc=o);let C=r.currentInitialFc+n,i=A.modulatedGenerators[a.initialFilterQ];if(r.currentInitialFc>13499&&C>13499&&i===0){r.currentInitialFc=13500;return}(Math.abs(r.lastTargetCutoff-C)>1||r.resonanceCb!==i)&&(r.lastTargetCutoff=C,r.resonanceCb=i,e.calculateCoefficients(r,C));for(let g=0;g.5?1:0,n?t*2-1:t;case GA.concave:return n?(t=t*2-1,t<0?-Fe[~~(t*-bA)]:Fe[~~(t*bA)]):Fe[~~(t*bA)];case GA.convex:return n?(t=t*2-1,t<0?-_e[~~(t*-bA)]:_e[~~(t*bA)]):_e[~~(t*bA)]}}var cs=1,ls=new Float32Array(1e3);for(let e=0;ez.copy(o)))}exclusiveRelease(){this.release(Qr),this.modulatedGenerators[a.releaseVolEnv]=Gi,this.modulatedGenerators[a.releaseModEnv]=xi,Ee.recalculate(this),Be.recalculate(this)}release(A=lr){this.releaseStartTime=currentTime,this.releaseStartTime-this.startTimeLt.copy(d,n));let h=r.preset;return C&&(h=this.soundfontManager.getPreset(i,g,FA(this.system))),o=h.getSamplesAndGenerators(A,t).reduce((d,B)=>{if(B.sample.getAudioData()===void 0)return Y(`Discarding invalid sample: ${B.sample.sampleName}`),d;let f=new Int16Array(60);for(let F=0;F<60;F++)f[F]=Js(F,B.presetGenerators,B.instrumentGenerators);f[a.initialAttenuation]=Math.floor(f[a.initialAttenuation]*.4);let m=B.sample.samplePitch;f[a.overridingRootKey]>-1&&(m=f[a.overridingRootKey]);let S=A;f[a.keyNum]>-1&&(S=f[a.keyNum]);let M=B.sample.sampleLoopStartIndex,D=B.sample.sampleLoopEndIndex,x=f[a.sampleModes],G=new yn(B.sample.sampleData,B.sample.sampleRate/sampleRate*Math.pow(2,B.sample.samplePitchCorrection/1200),0,m,M,D,Math.floor(B.sample.sampleData.length)-1,x);return f[a.velocity]>-1&&(t=f[a.velocity]),d.push(new Lt(sampleRate,G,A,t,e,n,S,s,f,B.modulators.map(F=>z.copy(F)))),d},[]),this.setCachedVoice(i,g,A,t,o.map(d=>Lt.copy(d,n))),o}var ur=.05,Mi=4600,Ni=2e3,dr=Math.PI/2,Sn=-500,fr=500,Qs=fr-Sn,mr=new Float32Array(Qs+1),pr=new Float32Array(Qs+1);for(let e=Sn;e<=fr;e++){let A=(e-Sn)/Qs,t=e-Sn;mr[t]=Math.cos(dr*A),pr[t]=Math.sin(dr*A)}function yr(e,A,t,n,s,o,r,C){if(isNaN(A[0]))return;let i;e.overridePan?i=e.overridePan:(e.currentPan+=(e.modulatedGenerators[a.pan]-e.currentPan)*this.synth.panSmoothingFactor,i=e.currentPan);let g=this.synth.currentGain,c=~~(i+500),h=mr[c]*g*this.synth.panLeft,d=pr[c]*g*this.synth.panRight;if(!this.synth.oneOutputMode){let B=e.modulatedGenerators[a.reverbEffectsSend];if(B>0){let m=this.synth.reverbGain*g*(B/Mi);for(let S=0;S0){let m=this.synth.chorusGain*f/Ni,S=h*m,M=d*m;for(let D=0;D0)for(let B=0;B0)for(let B=0;Bt.getAudioData()),this._snapshot!==void 0&&(this.applySynthesizerSnapshot(this._snapshot),this.resetAllControllers())}function kr(e,A=!1){this.clearSoundFont(!1,A);try{A?this.overrideSoundfont=bt(e):this.soundfontManager.reloadManager(e)}catch(t){this.post({messageType:JA.soundfontError,messageData:t});return}this.getDefaultPresets(),this.workletProcessorChannels.forEach(t=>t.programChange(t.preset.program)),this.postReady(),this.sendPresetList(),p("%cSpessaSynth is ready!",I.recognized)}function wr(e=!0,A=!0){this.stopAllChannels(!0),A&&(delete this.overrideSoundfont,this.overrideSoundfont=void 0),this.getDefaultPresets(),this.cachedVoices=[];for(let t=0;t{let t=A.bank===128?128:A.bank+this.soundfontBankOffset,n=e.find(s=>s.bank===t&&s.program===A.program);n!==void 0?n.presetName=A.presetName:e.push({presetName:A.presetName,bank:t,program:A.program})}),this.processorInitialized.then(()=>{this.callEvent("presetlistchange",e)})}function Rr(e,A){if(this.overrideSoundfont){let t=e===128?128:e-this.soundfontBankOffset,n=this.overrideSoundfont.getPresetNoFallback(t,A,FA(this.system));if(n)return n}return this.soundfontManager.getPreset(e,A,FA(this.system))}function Gr(e,A=!1){this.transposition=0;for(let t=0;tUt.getChannelSnapshot(A,s)),t.keyMappings=A.keyModifierManager.getMappings(),t.mainVolume=A.midiVolume,t.pan=A.pan,t.system=A.system,t.interpolation=A.interpolationType,t.transposition=A.transposition,t.effectsConfig={},t}static applySnapshot(A,t){for(A.setSystem(t.system),A.setMasterGain(t.mainVolume),A.setMasterPan(t.pan),A.transposeAllChannels(t.transposition),A.interpolationType=t.interpolation,A.keyModifierManager.setMappings(t.keyMappings);A.workletProcessorChannels.length{Ut.applyChannelSnapshot(A,s,n)}),p("%cFinished restoring controllers!",I.info)}};function Mr(){this.post({messageType:JA.synthesizerSnapshot,messageData:dt.createSynthesizerSnapshot(this)})}function Nr(e){dt.applySnapshot(this,e),p("%cFinished applying snapshot!",I.info)}function Dn(e,A,t){if(t=e.releaseStartTime&&(e.isInRelease=!0,Ee.startRelease(e),Be.startRelease(e),e.sample.loopingMode===3&&(e.sample.isLooping=!1)),e.modulatedGenerators[a.initialAttenuation]>2500)return e.isInRelease&&(e.finished=!0),e.finished;let i=e.targetKey,g=e.modulatedGenerators[a.fineTune]+this.channelOctaveTuning[e.midiNote]+this.channelTuningCents,c=e.modulatedGenerators[a.coarseTune],h=this.synth.tunings[this.preset.program]?.[e.realKey];if(h!==void 0&&h?.midiNote>=0&&(i=h.midiNote,g+=h.centTuning),e.portamentoFromKey>-1){let E=Math.min((C-e.startTime)/e.portamentoDuration,1),L=i-e.portamentoFromKey;c-=L*(1-E)}g+=(i-e.sample.rootKey)*e.modulatedGenerators[a.scaleTuning];let d=e.modulatedGenerators[a.vibLfoToPitch];if(d!==0){let E=e.startTime+Ce(e.modulatedGenerators[a.delayVibLFO]),L=Gt(e.modulatedGenerators[a.freqVibLFO]),Z=Dn(E,L,C);g+=Z*(d*this.customControllers[cA.modulationMultiplier])}let B=0,f=e.modulatedGenerators[a.modLfoToPitch],m=e.modulatedGenerators[a.modLfoToVolume],S=e.modulatedGenerators[a.modLfoToFilterFc],M=0;if(f!==0||S!==0||m!==0){let E=e.startTime+Ce(e.modulatedGenerators[a.delayModLFO]),L=Gt(e.modulatedGenerators[a.freqModLFO]),Z=Dn(E,L,C);g+=Z*(f*this.customControllers[cA.modulationMultiplier]),M=-Z*m,B+=Z*S}if(this.channelVibrato.depth>0){let E=Dn(e.startTime+this.channelVibrato.delay,this.channelVibrato.rate,C);E&&(g+=E*this.channelVibrato.depth)}let D=e.modulatedGenerators[a.modEnvToPitch],x=e.modulatedGenerators[a.modEnvToFilterFc];if(x!==0||D!==0){let E=Be.getValue(e,C);B+=E*x,g+=E*D}let G=~~(g+c*100);G!==e.currentTuningCents&&(e.currentTuningCents=G,e.currentTuningCalculated=Math.pow(2,G/1200));let F=new Float32Array(A.length);switch(this.synth.interpolationType){case Qt.fourthOrder:lt.getSampleCubic(e,F);break;case Qt.linear:default:lt.getSampleLinear(e,F);break;case Qt.nearestNeighbor:lt.getSampleNearest(e,F);break}return Ke.apply(e,F,B,this.synth.filterSmoothingFactor),Ee.apply(e,F,M,this.synth.volumeEnvelopeSmoothingFactor),this.panVoice(e,F,A,t,n,s,o,r),e.finished}function Lr(e,A=-12e3){this.voices.forEach(t=>{t.realKey===e&&(t.modulatedGenerators[a.releaseVolEnv]=A,t.release())})}function Ur(e,A=!0){e=Math.round(e),this.setCustomController(cA.channelTuning,e),A&&p(`%cFine tuning for %c${this.channelNumber}%c is now set to %c${e}%c cents.`,I.info,I.recognized,I.info,I.value,I.info)}function Tr(e){e=Math.round(e),p(`%cChannel ${this.channelNumber} modulation depth. Cents: %c${e}`,I.info,I.value),this.setCustomController(cA.modulationMultiplier,e/50)}function vr(e){switch(this.dataEntryState){default:break;case YA.RPCoarse:case YA.RPFine:switch(this.midiControllers[y.RPNMsb]|this.midiControllers[y.RPNLsb]>>7){default:break;case 0:if(e===0)break;this.midiControllers[xA+j.pitchWheelRange]|=e;let t=(this.midiControllers[xA+j.pitchWheelRange]>>7)+e/128;p(`%cChannel ${this.channelNumber} bend range. Semitones: %c${t}`,I.info,I.value);break;case 1:let s=this.customControllers[cA.channelTuning]<<7|e;this.setTuning(s*.01220703125);break;case 5:let r=this.customControllers[cA.modulationMultiplier]*50+e/128*100;this.setModulationDepth(r);break;case 16383:this.resetParameters();break}}}var bi=1e3/200;function Hr(e,A,t){if(A.transformAmount===0)return A.currentValue=0,0;let n;if(A.sourceUsesCC)n=e[A.sourceIndex];else{let g=A.sourceIndex+xA;switch(A.sourceIndex){case j.noController:n=16383;break;case j.noteOnKeyNum:n=t.midiNote<<7;break;case j.noteOnVelocity:n=t.velocity<<7;break;case j.polyPressure:n=t.pressure<<7;break;default:n=e[g];break}}let s=je[A.sourceCurveType][A.sourcePolarity][A.sourceDirection][n],o;if(A.secSrcUsesCC)o=e[A.secSrcIndex];else{let g=A.secSrcIndex+xA;switch(A.secSrcIndex){case j.noController:o=16383;break;case j.noteOnKeyNum:o=t.midiNote<<7;break;case j.noteOnVelocity:o=t.velocity<<7;break;case j.polyPressure:o=t.pressure<<7;break;default:o=e[g]}}let r=je[A.secSrcCurveType][A.secSrcPolarity][A.secSrcDirection][o],C=A.transformAmount;A.isEffectModulator&&C<=1e3&&(C*=bi,C=Math.min(C,1e3));let i=s*r*C;return A.transformType===2&&(i=Math.abs(i)),A.currentValue=i,i}function he(e,A,t=-1,n=0){let s=e.modulators,o=e.generators,r=e.modulatedGenerators;if(t===-1){r.set(o),s.forEach(g=>{let c=X[g.modulatorDestination],h=r[g.modulatorDestination]+Hr(A,g,e);r[g.modulatorDestination]=Math.max(c.min,Math.min(h,c.max))}),Ee.recalculate(e),Be.recalculate(e);return}let C=new Set([a.initialAttenuation,a.delayVolEnv,a.attackVolEnv,a.holdVolEnv,a.decayVolEnv,a.sustainVolEnv,a.releaseVolEnv,a.keyNumToVolEnvHold,a.keyNumToVolEnvDecay]),i=new Set;s.forEach(g=>{if(g.sourceUsesCC===t&&g.sourceIndex===n||g.secSrcUsesCC===t&&g.secSrcIndex===n){let c=g.modulatorDestination;i.has(c)||(r[c]=o[c],Hr(A,g,e),s.forEach(h=>{if(h.modulatorDestination===c){let d=X[g.modulatorDestination],B=r[g.modulatorDestination]+h.currentValue;r[g.modulatorDestination]=Math.max(d.min,Math.min(B,d.max))}}),i.add(c))}}),[...i].some(g=>C.has(g))&&Ee.recalculate(e),Be.recalculate(e)}var je=[];for(let e=0;e<4;e++){je[e]=[[new Float32Array(bA),new Float32Array(bA)],[new Float32Array(bA),new Float32Array(bA)]];for(let A=0;A127){if(!t)return;switch(e){default:return;case Os.velocityOverride:this.velocityOverride=A}}if(e>=y.lsbForControl1ModulationWheel&&e<=y.lsbForControl13EffectControl2&&e!==y.lsbForControl6DataEntry){let n=e-32;if(this.lockedControllers[n])return;this.midiControllers[n]=this.midiControllers[n]&16256|A&127,this.voices.forEach(s=>he(s,this.midiControllers,1,n))}if(!this.lockedControllers[e]){switch(this.midiControllers[e]=A<<7,e){case y.allNotesOff:this.stopAllNotes();break;case y.allSoundOff:this.stopAllNotes(!0);break;case y.bankSelect:this.setBankSelect(A);break;case y.lsbForControl0BankSelect:this.setBankSelect(A,!0);break;case y.RPNLsb:this.dataEntryState=YA.RPFine;break;case y.RPNMsb:this.dataEntryState=YA.RPCoarse;break;case y.NRPNMsb:this.dataEntryState=YA.NRPCoarse;break;case y.NRPNLsb:this.dataEntryState=YA.NRPFine;break;case y.dataEntryMsb:this.dataEntryCoarse(A);break;case y.lsbForControl6DataEntry:this.dataEntryFine(A);break;case y.resetAllControllers:this.resetControllersRP15Compliant();break;case y.sustainPedal:A>=64?this.holdPedal=!0:(this.holdPedal=!1,this.sustainedVoices.forEach(n=>{n.release()}),this.sustainedVoices=[]);break;default:this.voices.forEach(n=>he(n,this.midiControllers,1,e));break}this.synth.callEvent("controllerchange",{channel:this.channelNumber,controllerNumber:e,controllerValue:A})}}function Jr(e=!1){e?(this.voices.length=0,this.sustainedVoices.length=0,this.sendChannelProperty()):(this.voices.forEach(A=>{A.isInRelease||A.release()}),this.sustainedVoices.forEach(A=>{A.release()}))}function Kr(e){e&&this.stopAllNotes(!0),this.isMuted=e,this.sendChannelProperty(),this.synth.callEvent("mutechannel",{channel:this.channelNumber,isMuted:e})}function Or(e,A=!1){this.drumChannel||(e+=this.synth.transposition);let t=Math.trunc(e),n=this.channelTransposeKeyShift+this.customControllers[cA.channelTransposeFine]/100;this.drumChannel&&!A||e===n||(t!==this.channelTransposeKeyShift&&this.controllerChange(y.allNotesOff,127),this.channelTransposeKeyShift=t,this.setCustomController(cA.channelTransposeFine,(e-t)*100),this.sendChannelProperty())}var Tt={pitchBendRange:0,fineTuning:1,coarseTuning:2,modulationDepth:5,resetParameters:16383},Oe={partParameter:1,vibratoRate:8,vibratoDepth:9,vibratoDelay:10,EGAttackTime:100,EGReleaseTime:102,TVFFilterCutoff:32,drumReverb:29};function qr(e){let A=()=>{this.channelVibrato.delay===0&&this.channelVibrato.rate===0&&this.channelVibrato.depth===0&&(this.channelVibrato.depth=50,this.channelVibrato.rate=8,this.channelVibrato.delay=.6)},t=(n,s,o)=>{o.length>0&&(o=" "+o),p(`%c${n} for %c${this.channelNumber}%c is now set to %c${s}%c${o}.`,I.info,I.recognized,I.info,I.value,I.info)};switch(this.dataEntryState){default:case YA.Idle:break;case YA.NRPFine:if(this.lockGSNRPNParams)return;let n=this.midiControllers[y.NRPNMsb]>>7,s=this.midiControllers[y.NRPNLsb]>>7;switch(n){default:if(e===64)return;Y(`%cUnrecognized NRPN for %c${this.channelNumber}%c: %c(0x${s.toString(16).toUpperCase()} 0x${s.toString(16).toUpperCase()})%c data value: %c${e}`,I.warn,I.recognized,I.warn,I.unrecognized,I.warn,I.value);break;case Oe.partParameter:switch(s){default:if(e===64)return;Y(`%cUnrecognized NRPN for %c${this.channelNumber}%c: %c(0x${n.toString(16)} 0x${s.toString(16)})%c data value: %c${e}`,I.warn,I.recognized,I.warn,I.unrecognized,I.warn,I.value);break;case Oe.vibratoRate:if(e===64)return;A(),this.channelVibrato.rate=e/64*8,t("Vibrato rate",`${e} = ${this.channelVibrato.rate}`,"Hz");break;case Oe.vibratoDepth:if(e===64)return;A(),this.channelVibrato.depth=e/2,t("Vibrato depth",`${e} = ${this.channelVibrato.depth}`,"cents of detune");break;case Oe.vibratoDelay:if(e===64)return;A(),this.channelVibrato.delay=e/64/3,t("Vibrato delay",`${e} = ${this.channelVibrato.delay}`,"seconds");break;case Oe.TVFFilterCutoff:this.controllerChange(y.brightness,e),t("Filter cutoff",e.toString(),"");break;case Oe.EGAttackTime:this.controllerChange(y.attackTime,e),t("EG attack time",e.toString(),"");break;case Oe.EGReleaseTime:this.controllerChange(y.releaseTime,e),t("EG release time",e.toString(),"");break}break;case Oe.drumReverb:let r=e;this.controllerChange(y.reverbDepth,r),t("GS Drum reverb",r.toString(),"percent");break}break;case YA.RPCoarse:case YA.RPFine:let o=this.midiControllers[y.RPNMsb]|this.midiControllers[y.RPNLsb]>>7;switch(o){default:Y(`%cUnrecognized RPN for %c${this.channelNumber}%c: %c(0x${o.toString(16)})%c data value: %c${e}`,I.warn,I.recognized,I.warn,I.unrecognized,I.warn,I.value);break;case Tt.pitchBendRange:this.midiControllers[xA+j.pitchWheelRange]=e<<7,t("Pitch bend range",e.toString(),"semitones");break;case Tt.coarseTuning:let r=e-64;this.setCustomController(cA.channelTuningSemitones,r),t("Coarse tuning",r.toString(),"semitones");break;case Tt.fineTuning:this.setTuning(e-64,!1);break;case Tt.modulationDepth:this.setModulationDepth(e*100);break;case Tt.resetParameters:this.resetParameters();break}}}var vt={0:0,1:.006,2:.023,4:.05,8:.11,16:.25,32:.5,64:2.06,80:4.2,96:8.4,112:19.5,116:26.7,120:40,124:80,127:480};function Li(e){if(vt[e]!==void 0)return vt[e];let A=null,t=null;for(let n of Object.keys(vt))n=parseInt(n),nA)&&(A=n),n>e&&(t===null||n200&&A<40||this.synth.highPerformanceMode&&A<10||this.isMuted)return;let t=e+this.channelTransposeKeyShift,n=t;if(t>127||t<0)return;let s=this.preset.program;this.synth.tunings[s]?.[t]?.midiNote>=0&&(n=this.synth.tunings[s]?.[t].midiNote),this.velocityOverride>0&&(A=this.velocityOverride);let o=this.synth.keyModifierManager.getVelocity(this.channelNumber,t);o>-1&&(A=o);let r=-1,C=0,i=this.midiControllers[y.portamentoTime]>>7,g=this.midiControllers[y.portamentoControl],c=g>>7;if(!this.drumChannel&&c!==n&&this.midiControllers[y.portamentoOnOff]>=8192&&i>0){if(g!==1){let f=Math.abs(n-c);C=Pr(i,f),r=c}this.controllerChange(y.portamentoControl,n)}let h=this.synth.getWorkletVoices(this.channelNumber,n,A,currentTime,t),d=0;this.randomPan&&(d=Math.round(Math.random()*1e3-500));let B=this.voices;h.forEach(f=>{f.portamentoFromKey=r,f.portamentoDuration=C,f.overridePan=d;let m=f.exclusiveClass;m!==0&&B.forEach(E=>{E.exclusiveClass===m&&E.exclusiveRelease()}),he(f,this.midiControllers);let S=f.modulatedGenerators[a.startAddrsOffset]+f.modulatedGenerators[a.startAddrsCoarseOffset]*32768,M=f.modulatedGenerators[a.endAddrOffset]+f.modulatedGenerators[a.endAddrsCoarseOffset]*32768,D=f.modulatedGenerators[a.startloopAddrsOffset]+f.modulatedGenerators[a.startloopAddrsCoarseOffset]*32768,x=f.modulatedGenerators[a.endloopAddrsOffset]+f.modulatedGenerators[a.endloopAddrsCoarseOffset]*32768,G=f.sample,F=E=>Math.max(0,Math.min(G.sampleData.length-1,E));if(G.cursor=F(G.cursor+S),G.end=F(G.end+M),G.loopStart=F(G.loopStart+D),G.loopEnd=F(G.loopEnd+x),G.loopEndthis.synth.voiceCap&&this.synth.voiceKilling(h.length),B.push(...h),this.sendChannelProperty(),this.synth.callEvent("noteon",{midiNote:e,channel:this.channelNumber,velocity:A})}function Zr(e){if(e>127||e<0){Y("Received a noteOn for note",e,"Ignoring.");return}let A=e+this.channelTransposeKeyShift;if(this.synth.highPerformanceMode&&!this.drumChannel){this.killNote(A,-6950),this.synth.callEvent("noteoff",{midiNote:e,channel:this.channelNumber});return}this.voices.forEach(n=>{n.realKey!==A||n.isInRelease===!0||(this.holdPedal?this.sustainedVoices.push(n):n.release())}),this.synth.callEvent("noteoff",{midiNote:e,channel:this.channelNumber})}function Xr(e,A){this.voices.forEach(t=>{t.midiNote===e&&(t.pressure=A,he(t,this.midiControllers,0,j.polyPressure))}),this.synth.callEvent("polypressure",{channel:this.channelNumber,midiNote:e,pressure:A})}function Wr(e){this.midiControllers[xA+j.channelPressure]=e<<7,this.voices.forEach(A=>he(A,this.midiControllers,0,j.channelPressure)),this.synth.callEvent("channelpressure",{channel:this.channelNumber,pressure:e})}function _r(e,A){if(this.lockedControllers[xA+j.pitchWheel])return;let t=A|e<<7;this.synth.callEvent("pitchwheel",{channel:this.channelNumber,MSB:e,LSB:A}),this.midiControllers[xA+j.pitchWheel]=t,this.voices.forEach(n=>he(n,this.midiControllers,0,j.pitchWheel)),this.sendChannelProperty()}function zr(e){if(e.length!==12)throw new Error("Tuning is not the length of 12.");this.channelOctaveTuning=new Int8Array(128);for(let A=0;A<128;A++)this.channelOctaveTuning[A]=e[A%12]}function jr(e){if(this.lockPreset)return;let A=this.getBankSelect(),t,n,s=this.isXGChannel;if(this.synth.overrideSoundfont){let o=A===128?128:A-this.synth.soundfontBankOffset,r=this.synth.overrideSoundfont.getPresetNoFallback(o,e,s);if(r)t=r.bank===128?128:r.bank+this.synth.soundfontBankOffset,n=r,this.presetUsesOverride=!0;else{n=this.synth.soundfontManager.getPreset(A,e,s);let C=this.synth.soundfontManager.soundfontList.find(i=>i.soundfont===n.parentSoundBank).bankOffset;t=n.bank-C,this.presetUsesOverride=!1}}else{n=this.synth.soundfontManager.getPreset(A,e,s);let o=this.synth.soundfontManager.soundfontList.find(r=>r.soundfont===n.parentSoundBank).bankOffset;t=n.bank-o,this.presetUsesOverride=!1}this.setPreset(n),this.sentBank=t,this.synth.callEvent("programchange",{channel:this.channelNumber,program:n.program,bank:t}),this.sendChannelProperty()}var SA=class{midiControllers=new Int16Array(zt);lockedControllers=Array(zt).fill(!1);customControllers=new Float32Array(Kn);channelTransposeKeyShift=0;channelOctaveTuning=new Int8Array(128);channelTuningCents=0;holdPedal=!1;drumChannel=!1;velocityOverride=0;randomPan=!1;dataEntryState=YA.Idle;bank=0;sentBank=0;bankLSB=0;preset=void 0;lockPreset=!1;lockedSystem="gs";presetUsesOverride=!1;lockGSNRPNParams=!1;channelVibrato={delay:0,depth:0,rate:0};isMuted=!1;voices=[];sustainedVoices=[];channelNumber;synth;constructor(A,t,n){this.synth=A,this.preset=t,this.channelNumber=n}get isXGChannel(){return FA(this.synth.system)||this.lockPreset&&FA(this.lockedSystem)}setCustomController(A,t){this.customControllers[A]=t,this.updateChannelTuning()}updateChannelTuning(){this.channelTuningCents=this.customControllers[cA.channelTuning]+this.customControllers[cA.channelTransposeFine]+this.customControllers[cA.masterTuning]+this.customControllers[cA.channelTuningSemitones]*100}renderAudio(A,t,n,s,o,r){this.voices=this.voices.filter(C=>!this.renderVoice(C,A,t,n,s,o,r))}setPresetLock(A){this.lockPreset=A,A&&(this.lockedSystem=this.synth.system)}setBankSelect(A,t=!1){if(!this.lockPreset)if(t)this.bankLSB=A;else switch(this.bank=A,Et(this.getBankSelect(),A,this.synth.system,!1,this.drumChannel,this.channelNumber).drumsStatus){default:case 0:break;case 1:this.channelNumber%16===9&&(this.bank=127);break;case 2:this.setDrums(!0);break}}getBankSelect(){return Bt(this.bank,this.bankLSB,this.drumChannel,this.isXGChannel)}setPreset(A){this.lockPreset||(delete this.preset,this.preset=A)}setDrums(A){this.lockPreset||this.drumChannel!==A&&(A?(this.channelTransposeKeyShift=0,this.drumChannel=!0):this.drumChannel=!1,this.presetUsesOverride=!1,this.synth.callEvent("drumchange",{channel:this.channelNumber,isDrumChannel:this.drumChannel}),this.programChange(this.preset.program),this.sendChannelProperty())}setVibrato(A,t,n){this.lockGSNRPNParams||(this.channelVibrato.rate=t,this.channelVibrato.delay=n,this.channelVibrato.depth=A)}disableAndLockGSNRPN(){this.lockGSNRPNParams=!0,this.channelVibrato.rate=0,this.channelVibrato.delay=0,this.channelVibrato.depth=0}sendChannelProperty(){if(!this.synth.enableEventSystem)return;let A={voicesAmount:this.voices.length,pitchBend:this.midiControllers[xA+j.pitchWheel],pitchBendRangeSemitones:this.midiControllers[xA+j.pitchWheelRange]/128,isMuted:this.isMuted,isDrum:this.drumChannel,transposition:this.channelTransposeKeyShift+this.customControllers[cA.channelTransposeFine]/100,bank:this.sentBank,program:this.preset.program};this.synth.post({messageType:JA.channelPropertyChange,messageData:[this.channelNumber,A]})}};SA.prototype.renderVoice=br;SA.prototype.panVoice=yr;SA.prototype.killNote=Lr;SA.prototype.stopAllNotes=Jr;SA.prototype.muteChannel=Kr;SA.prototype.noteOn=Vr;SA.prototype.noteOff=Zr;SA.prototype.polyPressure=Xr;SA.prototype.channelPressure=Wr;SA.prototype.pitchWheel=_r;SA.prototype.programChange=jr;SA.prototype.setTuning=Ur;SA.prototype.setOctaveTuning=zr;SA.prototype.setModulationDepth=Tr;SA.prototype.transposeChannel=Or;SA.prototype.controllerChange=Yr;SA.prototype.resetControllers=oo;SA.prototype.resetControllersRP15Compliant=ro;SA.prototype.resetParameters=io;SA.prototype.dataEntryFine=vr;SA.prototype.dataEntryCoarse=qr;function $r(e=!1){let A=new SA(this,this.defaultPreset,this.workletProcessorChannels.length);this.workletProcessorChannels.push(A),A.resetControllers(),A.sendChannelProperty(),e&&this.callEvent("newchannel",void 0),A.channelNumber%16===9&&this.workletProcessorChannels[this.workletProcessorChannels.length-1].setDrums(!0)}function Ai(e,A){e===void 0&&(e={});for(let t in A)A.hasOwnProperty(t)&&!(t in e)&&(e[t]=A[t]);return e}var ei={skipToFirstNoteOn:!0,autoPlay:!0,preservePlaybackState:!1};var lr=.03,Qr=.07,ns=1,kA=class extends AudioWorkletProcessor{cachedVoices=[];alive=!0;deviceID=ie;eventQueue=[];interpolationType=Qt.fourthOrder;sequencer=new DA(this);transposition=0;tunings=[];soundfontBankOffset=0;masterGain=ns;midiVolume=1;reverbGain=1;chorusGain=1;voiceCap=350;pan=0;panLeft=.5;panRight=.5;highPerformanceMode=!1;keyModifierManager=new pn;overrideSoundfont=void 0;workletProcessorChannels=[];system=$t;totalVoicesAmount=0;defaultPreset;defaultPresetUsesOverride=!1;drumPreset;defaultDrumsUsesOverride=!1;processorInitialized=me.isInitialized;constructor(A){super(),this.midiOutputsCount=A.processorOptions.midiChannels;let t=this.midiOutputsCount;this.oneOutputMode=this.midiOutputsCount===1,this.oneOutputMode&&(t=16),this.enableEventSystem=A.processorOptions.enableEventSystem;for(let n=0;n<127;n++)this.tunings.push([]);try{this.soundfontManager=new mn(A.processorOptions.soundfont,this.postReady.bind(this))}catch(n){throw this.post({messageType:JA.soundfontError,messageData:n}),n}this.sendPresetList(),this.getDefaultPresets();for(let n=0;nthis.handleMessage(n.data),A.processorOptions.startRenderingData&&(this._snapshot!==void 0&&(this.applySynthesizerSnapshot(this._snapshot),this.resetAllControllers()),p("%cRendering enabled! Starting render.",I.info),A.processorOptions.startRenderingData.parsedMIDI&&(A.processorOptions.startRenderingData?.loopCount!==void 0?(this.sequencer.loopCount=A.processorOptions.startRenderingData?.loopCount,this.sequencer.loop=!0):this.sequencer.loop=!1,this.voiceCap=1/0,this.processorInitialized.then(()=>{let n=Ai(A.processorOptions.startRenderingData.sequencerOptions,ei);this.sequencer.skipToFirstNoteOn=n.skipToFirstNoteOn,this.sequencer.preservePlaybackState=n.preservePlaybackState,this.sequencer.loadNewSongList([A.processorOptions.startRenderingData.parsedMIDI])}))),this.postReady()}get currentGain(){return this.masterGain*this.midiVolume}getDefaultPresets(){let A=this.system;this.system="xg",this.defaultPreset=this.getPreset(0,0),this.defaultPresetUsesOverride=this.overrideSoundfont?.presets?.indexOf(this.defaultPreset)>=0,this.system=A,this.drumPreset=this.getPreset(128,0),this.defaultDrumsUsesOverride=this.overrideSoundfont?.presets?.indexOf(this.drumPreset)>=0}setSystem(A){this.system=A,this.post({messageType:JA.masterParameterChange,messageData:[Ue.midiSystem,this.system]})}getCachedVoice(A,t,n,s){return this.cachedVoices?.[A]?.[t]?.[n]?.[s]}setCachedVoice(A,t,n,s,o){this.cachedVoices||(this.cachedVoices=[]),this.cachedVoices[A]||(this.cachedVoices[A]=[]),this.cachedVoices[A][t]||(this.cachedVoices[A][t]=[]),this.cachedVoices[A][t][n]||(this.cachedVoices[A][t][n]=[]),this.cachedVoices[A][t][n][s]=o}post(A){this.enableEventSystem&&this.port.postMessage(A)}postReady(){this.processorInitialized.then(()=>{this.port.postMessage({messageType:JA.isFullyInitialized,messageData:void 0}),p("%cSpessaSynth is ready!",I.recognized)})}debugMessage(){p({channels:this.workletProcessorChannels,voicesAmount:this.totalVoicesAmount,dumpedSamples:this.workletDumpedSamplesList})}process(A,t){if(!this.alive)return!1;this.sequencer.processTick();let n=currentTime;for(;this.eventQueue[0]?.time<=n;)this.eventQueue.shift().callback();return this.totalVoicesAmount=0,this.workletProcessorChannels.forEach((s,o)=>{if(s.voices.length<1||s.isMuted)return;let r=s.voices.length,C,i,g,c,h,d,B;if(this.oneOutputMode){let f=t[0];C=o%16*2,i=f[C],g=f[C+1]}else C=o%this.midiOutputsCount+2,i=t[C][0],g=t[C][1],c=t[0][0],h=t[0][1],d=t[1][0],B=t[1][1];s.renderAudio(i,g,c,h,d,B),this.totalVoicesAmount+=s.voices.length,s.voices.length!==r&&s.sendChannelProperty()}),!0}destroyWorkletProcessor(){this.alive=!1,this.workletProcessorChannels.forEach(A=>{delete A.midiControllers,delete A.voices,delete A.sustainedVoices,delete A.lockedControllers,delete A.preset,delete A.customControllers}),delete this.cachedVoices,delete this.workletProcessorChannels,delete this.sequencer.midiData,delete this.sequencer,this.soundfontManager.destroyManager(),delete this.soundfontManager}controllerChange(A,t,n,s=!1){this.workletProcessorChannels[A].controllerChange(t,n,s)}noteOn(A,t,n){this.workletProcessorChannels[A].noteOn(t,n)}noteOff(A,t){this.workletProcessorChannels[A].noteOff(t)}polyPressure(A,t,n){this.workletProcessorChannels[A].polyPressure(t,n)}channelPressure(A,t){this.workletProcessorChannels[A].channelPressure(t)}pitchWheel(A,t,n){this.workletProcessorChannels[A].pitchWheel(t,n)}programChange(A,t){this.workletProcessorChannels[A].programChange(t)}processMessage(A,t,n,s){let o=()=>{let C=at(A[0]),i=C.channel+t;switch(C.status){case w.noteOn:let g=A[2];g>0?this.noteOn(i,A[1],g):this.noteOff(i,A[1]);break;case w.noteOff:n?this.workletProcessorChannels[i].killNote(A[1]):this.noteOff(i,A[1]);break;case w.pitchBend:this.pitchWheel(i,A[2],A[1]);break;case w.controllerChange:this.controllerChange(i,A[1],A[2],n);break;case w.programChange:this.programChange(i,A[1]);break;case w.polyPressure:this.polyPressure(i,A[0],A[1]);break;case w.channelPressure:this.channelPressure(i,A[1]);break;case w.systemExclusive:this.systemExclusive(new b(A.slice(1)),t);break;case w.reset:this.stopAllChannels(!0),this.resetAllControllers();break;default:break}},r=s.time;r>currentTime?(this.eventQueue.push({callback:o.bind(this),time:r}),this.eventQueue.sort((C,i)=>C.time-i.time)):o()}};kA.prototype.voiceKilling=fo;kA.prototype.getWorkletVoices=cr;kA.prototype.handleMessage=Do;kA.prototype.callEvent=ko;kA.prototype.systemExclusive=wo;kA.prototype.stopAllChannels=Sr;kA.prototype.createWorkletChannel=$r;kA.prototype.resetAllControllers=so;kA.prototype.setMasterGain=Ro;kA.prototype.setMasterPan=Go;kA.prototype.setMIDIVolume=Fo;kA.prototype.transposeAllChannels=Gr;kA.prototype.setMasterTuning=xr;kA.prototype.getPreset=Rr;kA.prototype.reloadSoundFont=kr;kA.prototype.clearSoundFont=wr;kA.prototype.setEmbeddedSoundFont=Dr;kA.prototype.sendPresetList=Fr;kA.prototype.sendSynthesizerSnapshot=Mr;kA.prototype.applySynthesizerSnapshot=Nr;registerProcessor(qs,kA);p("%cProcessor succesfully registered!",I.recognized); diff --git a/spessasynth_lib/synthetizer/worklet_system/README.md b/spessasynth_lib/synthetizer/worklet_system/README.md new file mode 100644 index 0000000000000000000000000000000000000000..af792ffd4b5468b97df5307df89126b1224fcf51 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/README.md @@ -0,0 +1,12 @@ +## This is 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. + +For those interested, `render_voice.js` file contains the actual DSP synthesis code. + +`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/spessasynth_lib/synthetizer/worklet_system/main_processor.js b/spessasynth_lib/synthetizer/worklet_system/main_processor.js new file mode 100644 index 0000000000000000000000000000000000000000..6032c4d5b1765a42f1d1d16f771111d58a177fcd --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/main_processor.js @@ -0,0 +1,748 @@ +import { WorkletSequencer } from "../../sequencer/worklet_sequencer/worklet_sequencer.js"; +import { SpessaSynthInfo } from "../../utils/loggin.js"; +import { consoleColors } from "../../utils/other.js"; +import { voiceKilling } from "./worklet_methods/stopping_notes/voice_killing.js"; +import { + ALL_CHANNELS_OR_DIFFERENT_ACTION, + masterParameterType, + 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 } from "./message_protocol/message_sending.js"; +import { systemExclusive } from "./worklet_methods/system_exclusive.js"; +import { setMasterGain, setMasterPan, setMIDIVolume } from "./worklet_methods/controller_control/master_parameters.js"; +import { resetAllControllers } from "./worklet_methods/controller_control/reset_controllers.js"; +import { WorkletSoundfontManager } from "./worklet_methods/worklet_soundfont_manager/worklet_soundfont_manager.js"; +import { interpolationTypes } from "./worklet_utilities/wavetable_oscillator.js"; +import { WorkletKeyModifierManager } from "./worklet_methods/worklet_key_modifier.js"; +import { getWorkletVoices } from "./worklet_utilities/worklet_voice.js"; +import { PAN_SMOOTHING_FACTOR } from "./worklet_utilities/stereo_panner.js"; +import { stopAllChannels } from "./worklet_methods/stopping_notes/stop_all_channels.js"; +import { setEmbeddedSoundFont } from "./worklet_methods/soundfont_management/set_embedded_sound_font.js"; +import { reloadSoundFont } from "./worklet_methods/soundfont_management/reload_sound_font.js"; +import { clearSoundFont } from "./worklet_methods/soundfont_management/clear_sound_font.js"; +import { sendPresetList } from "./worklet_methods/soundfont_management/send_preset_list.js"; +import { getPreset } from "./worklet_methods/soundfont_management/get_preset.js"; +import { transposeAllChannels } from "./worklet_methods/tuning_control/transpose_all_channels.js"; +import { setMasterTuning } from "./worklet_methods/tuning_control/set_master_tuning.js"; +import { sendSynthesizerSnapshot } from "./snapshot/send_synthesizer_snapshot.js"; +import { applySynthesizerSnapshot } from "./snapshot/apply_synthesizer_snapshot.js"; +import { createWorkletChannel } from "./worklet_methods/create_worklet_channel.js"; +import { FILTER_SMOOTHING_FACTOR } from "./worklet_utilities/lowpass_filter.js"; +import { DEFAULT_PERCUSSION, DEFAULT_SYNTH_MODE, VOICE_CAP } from "../synth_constants.js"; +import { fillWithDefaults } from "../../utils/fill_with_defaults.js"; +import { DEFAULT_SEQUENCER_OPTIONS } from "../../sequencer/default_sequencer_options.js"; +import { getEvent, messageTypes } from "../../midi_parser/midi_message.js"; +import { IndexedByteArray } from "../../utils/indexed_array.js"; + + +/** + * @typedef {"gm"|"gm2"|"gs"|"xg"} SynthSystem + */ + +/** + * worklet_processor.js + * purpose: manages the synthesizer (and worklet sequencer) from the AudioWorkletGlobalScope and renders the audio data + */ + +// if the note is released faster than that, it forced to last that long +// this is used mostly for drum channels, where a lot of midis like to send instant note off after a note on +export const MIN_NOTE_LENGTH = 0.03; +// this sounds way nicer for an instant hi-hat cutoff +export const MIN_EXCLUSIVE_LENGTH = 0.07; + +export const SYNTHESIZER_GAIN = 1.0; + + +// noinspection JSUnresolvedReference +class SpessaSynthProcessor extends AudioWorkletProcessor +{ + + /** + * Cached voices for all presets for this synthesizer. + * Nesting goes like this: + * this.cachedVoices[bankNumber][programNumber][midiNote][velocity] = a list of workletvoices for that. + * @type {WorkletVoice[][][][][]} + */ + cachedVoices = []; + + + /** + * If the worklet is alive + * @type {boolean} + */ + alive = true; + + /** + * Synth's device id: -1 means all + * @type {number} + */ + deviceID = ALL_CHANNELS_OR_DIFFERENT_ACTION; + + /** + * Synth's event queue from the main thread + * @type {{callback: function(), time: number}[]} + */ + eventQueue = []; + + /** + * Interpolation type used + * @type {interpolationTypes} + */ + interpolationType = interpolationTypes.fourthOrder; + + /** + * The sequencer attached to this processor + * @type {WorkletSequencer} + */ + sequencer = new WorkletSequencer(this); + + /** + * Global transposition in semitones + * @type {number} + */ + transposition = 0; + + /** + * this.tunings[program][key] = tuning + * @type {MTSProgramTuning[]} + */ + tunings = []; + + + /** + * Bank offset for things like embedded RMIDIS. Added for every program change + * @type {number} + */ + soundfontBankOffset = 0; + + + /** + * The volume gain, set by user + * @type {number} + */ + masterGain = SYNTHESIZER_GAIN; + + /** + * The volume gain, set by MIDI sysEx + * @type {number} + */ + midiVolume = 1; + + /** + * Reverb linear gain + * @type {number} + */ + reverbGain = 1; + /** + * Chorus linear gain + * @type {number} + */ + chorusGain = 1; + + /** + * Maximum number of voices allowed at once + * @type {number} + */ + voiceCap = VOICE_CAP; + + /** + * (-1 to 1) + * @type {number} + */ + pan = 0.0; + /** + * the pan of the left channel + * @type {number} + */ + panLeft = 0.5; + + /** + * the pan of the right channel + * @type {number} + */ + panRight = 0.5; + + /** + * forces note killing instead of releasing + * @type {boolean} + */ + highPerformanceMode = false; + + /** + * Handlese custom key overrides: velocity and preset + * @type {WorkletKeyModifierManager} + */ + keyModifierManager = new WorkletKeyModifierManager(); + + /** + * Overrides the main soundfont (embedded, for example) + * @type {BasicSoundBank} + */ + overrideSoundfont = undefined; + + /** + * contains all the channels with their voices on the processor size + * @type {WorkletProcessorChannel[]} + */ + workletProcessorChannels = []; + + /** + * Controls the bank selection & SysEx + * @type {SynthSystem} + */ + system = DEFAULT_SYNTH_MODE; + /** + * Current total voices amount + * @type {number} + */ + totalVoicesAmount = 0; + + /** + * Synth's default (reset) preset + * @type {BasicPreset} + */ + defaultPreset; + + defaultPresetUsesOverride = false; + + /** + * Synth's default (reset) drum preset + * @type {BasicPreset} + */ + drumPreset; + + defaultDrumsUsesOverride = false; + + /** + * Controls if the worklet processor is fully initialized + * @type {Promise} + */ + processorInitialized = stbvorbis.isInitialized; + + /** + * Creates a new worklet synthesis system. contains all channels + * @param options {{ + * processorOptions: { + * midiChannels: number, + * soundfont: ArrayBuffer, + * enableEventSystem: boolean, + * startRenderingData: StartRenderingDataConfig + * }}} + */ + constructor(options) + { + super(); + this.midiOutputsCount = options.processorOptions.midiChannels; + let initialChannelCount = this.midiOutputsCount; + this.oneOutputMode = this.midiOutputsCount === 1; + if (this.oneOutputMode) + { + initialChannelCount = 16; + } + + this.enableEventSystem = options.processorOptions.enableEventSystem; + + + for (let i = 0; i < 127; i++) + { + this.tunings.push([]); + } + + try + { + /** + * @type {WorkletSoundfontManager} + */ + this.soundfontManager = new WorkletSoundfontManager( + options.processorOptions.soundfont, + this.postReady.bind(this) + ); + } + catch (e) + { + this.post({ + messageType: returnMessageType.soundfontError, + messageData: e + }); + throw e; + } + this.sendPresetList(); + + this.getDefaultPresets(); + + + for (let i = 0; i < initialChannelCount; i++) + { + this.createWorkletChannel(false); + } + + this.workletProcessorChannels[DEFAULT_PERCUSSION].preset = this.drumPreset; + this.workletProcessorChannels[DEFAULT_PERCUSSION].drumChannel = true; + + // these smoothing factors were tested on 44,100 Hz, adjust them to target sample rate here + this.volumeEnvelopeSmoothingFactor = VOLUME_ENVELOPE_SMOOTHING_FACTOR * (44100 / sampleRate); + this.panSmoothingFactor = PAN_SMOOTHING_FACTOR * (44100 / sampleRate); + this.filterSmoothingFactor = FILTER_SMOOTHING_FACTOR * (44100 / sampleRate); + + /** + * 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 (this._snapshot !== undefined) + { + this.applySynthesizerSnapshot(this._snapshot); + this.resetAllControllers(); + } + + SpessaSynthInfo("%cRendering enabled! Starting render.", consoleColors.info); + if (options.processorOptions.startRenderingData.parsedMIDI) + { + if (options.processorOptions.startRenderingData?.loopCount !== undefined) + { + this.sequencer.loopCount = options.processorOptions.startRenderingData?.loopCount; + this.sequencer.loop = true; + } + else + { + this.sequencer.loop = false; + } + // set voice cap to unlimited + this.voiceCap = Infinity; + this.processorInitialized.then(() => + { + /** + * set options + * @type {SequencerOptions} + */ + const seqOptions = fillWithDefaults( + options.processorOptions.startRenderingData.sequencerOptions, + DEFAULT_SEQUENCER_OPTIONS + ); + this.sequencer.skipToFirstNoteOn = seqOptions.skipToFirstNoteOn; + this.sequencer.preservePlaybackState = seqOptions.preservePlaybackState; + // autoplay is ignored + this.sequencer.loadNewSongList([options.processorOptions.startRenderingData.parsedMIDI]); + }); + } + } + + this.postReady(); + } + + /** + * @returns {number} + */ + get currentGain() + { + return this.masterGain * this.midiVolume; + } + + getDefaultPresets() + { + // override this to XG, to set the default preset to NOT be XG drums! + const sys = this.system; + this.system = "xg"; + this.defaultPreset = this.getPreset(0, 0); + this.defaultPresetUsesOverride = this.overrideSoundfont?.presets?.indexOf(this.defaultPreset) >= 0; + this.system = sys; + this.drumPreset = this.getPreset(128, 0); + this.defaultDrumsUsesOverride = this.overrideSoundfont?.presets?.indexOf(this.drumPreset) >= 0; + } + + /** + * @param value {SynthSystem} + */ + setSystem(value) + { + this.system = value; + this.post({ + messageType: returnMessageType.masterParameterChange, + messageData: [masterParameterType.midiSystem, this.system] + }); + } + + /** + * @param bank {number} + * @param program {number} + * @param midiNote {number} + * @param velocity {number} + * @returns {WorkletVoice[]|undefined} + */ + getCachedVoice(bank, program, midiNote, velocity) + { + return this.cachedVoices?.[bank]?.[program]?.[midiNote]?.[velocity]; + } + + /** + * @param bank {number} + * @param program {number} + * @param midiNote {number} + * @param velocity {number} + * @param voices {WorkletVoice[]} + */ + setCachedVoice(bank, program, midiNote, velocity, voices) + { + // make sure that it exists + if (!this.cachedVoices) + { + this.cachedVoices = []; + } + if (!this.cachedVoices[bank]) + { + this.cachedVoices[bank] = []; + } + if (!this.cachedVoices[bank][program]) + { + this.cachedVoices[bank][program] = []; + } + if (!this.cachedVoices[bank][program][midiNote]) + { + this.cachedVoices[bank][program][midiNote] = []; + } + + // cache + this.cachedVoices[bank][program][midiNote][velocity] = voices; + } + + + /** + * @param data {WorkletReturnMessage} + */ + post(data) + { + if (!this.enableEventSystem) + { + return; + } + this.port.postMessage(data); + } + + postReady() + { + // ensure stbvorbis is fully initialized + this.processorInitialized.then(() => + { + // post-ready cannot be constrained by the event system + this.port.postMessage({ + messageType: returnMessageType.isFullyInitialized, + messageData: undefined + }); + SpessaSynthInfo("%cSpessaSynth is ready!", consoleColors.recognized); + }); + } + + debugMessage() + { + SpessaSynthInfo({ + channels: this.workletProcessorChannels, + voicesAmount: this.totalVoicesAmount, + dumpedSamples: this.workletDumpedSamplesList + }); + } + + // noinspection JSUnusedGlobalSymbols + /** + * Syntesizes the voice to buffers + * @param inputs {Float32Array[][]} required by WebAudioAPI + * @param outputs {Float32Array[][]} the outputs to write to, only the first two channels are populated + * @returns {boolean} true + */ + process(inputs, outputs) + { + if (!this.alive) + { + return false; + } + // process the sequencer playback + this.sequencer.processTick(); + + // process event queue + const time = currentTime; + while (this.eventQueue[0]?.time <= time) + { + this.eventQueue.shift().callback(); + } + + // for every channel + this.totalVoicesAmount = 0; + this.workletProcessorChannels.forEach((channel, index) => + { + if (channel.voices.length < 1 || channel.isMuted) + { + // skip the channels + return; + } + let voiceCount = channel.voices.length; + let outputIndex; + let outputL; + let outputR; + let reverbL; + let reverbR; + let chorusL; + let chorusR; + // one output mode + if (this.oneOutputMode) + { + // first output only + const output = outputs[0]; + // reverb and chorus are disabled. 32 output channels: two for each midi channel + outputIndex = (index % 16) * 2; + outputL = output[outputIndex]; + outputR = output[outputIndex + 1]; + } + else + { + // 2 first outputs are reverb and chorus, others are for channels + outputIndex = (index % this.midiOutputsCount) + 2; + outputL = outputs[outputIndex][0]; + outputR = outputs[outputIndex][1]; + reverbL = outputs[0][0]; + reverbR = outputs[0][1]; + chorusL = outputs[1][0]; + chorusR = outputs[1][1]; + } + + // for every voice, render it + channel.renderAudio( + outputL, outputR, + reverbL, reverbR, + chorusL, chorusR + ); + + this.totalVoicesAmount += channel.voices.length; + // if voice count changed, update voice amount + if (channel.voices.length !== voiceCount) + { + channel.sendChannelProperty(); + } + }); + // keep the processor alive + return true; + } + + destroyWorkletProcessor() + { + this.alive = false; + this.workletProcessorChannels.forEach(c => + { + delete c.midiControllers; + delete c.voices; + delete c.sustainedVoices; + delete c.lockedControllers; + delete c.preset; + delete c.customControllers; + }); + delete this.cachedVoices; + delete this.workletProcessorChannels; + delete this.sequencer.midiData; + delete this.sequencer; + this.soundfontManager.destroyManager(); + delete this.soundfontManager; + } + + /** + * @param channel {number} + * @param controllerNumber {number} + * @param controllerValue {number} + * @param force {boolean} + */ + controllerChange(channel, controllerNumber, controllerValue, force = false) + { + this.workletProcessorChannels[channel].controllerChange(controllerNumber, controllerValue, force); + } + + /** + * @param channel {number} + * @param midiNote {number} + * @param velocity {number} + */ + noteOn(channel, midiNote, velocity) + { + this.workletProcessorChannels[channel].noteOn(midiNote, velocity); + } + + /** + * @param channel {number} + * @param midiNote {number} + */ + noteOff(channel, midiNote) + { + this.workletProcessorChannels[channel].noteOff(midiNote); + } + + /** + * @param channel {number} + * @param midiNote {number} + * @param pressure {number} + */ + polyPressure(channel, midiNote, pressure) + { + this.workletProcessorChannels[channel].polyPressure(midiNote, pressure); + } + + /** + * @param channel {number} + * @param pressure {number} + */ + channelPressure(channel, pressure) + { + this.workletProcessorChannels[channel].channelPressure(pressure); + } + + /** + * @param channel {number} + * @param MSB {number} + * @param LSB {number} + */ + pitchWheel(channel, MSB, LSB) + { + this.workletProcessorChannels[channel].pitchWheel(MSB, LSB); + } + + /** + * @param channel {number} + * @param programNumber {number} + */ + programChange(channel, programNumber) + { + this.workletProcessorChannels[channel].programChange(programNumber); + } + + /** + * @param message {Uint8Array} + * @param channelOffset {number} + * @param force {boolean} cool stuff + * @param options {SynthMethodOptions} + */ + processMessage(message, channelOffset, force, options) + { + const call = () => + { + const statusByteData = getEvent(message[0]); + + const channel = statusByteData.channel + channelOffset; + // process the event + switch (statusByteData.status) + { + case messageTypes.noteOn: + const velocity = message[2]; + if (velocity > 0) + { + this.noteOn(channel, message[1], velocity); + } + else + { + this.noteOff(channel, message[1]); + } + break; + + case messageTypes.noteOff: + if (force) + { + this.workletProcessorChannels[channel].killNote(message[1]); + } + else + { + this.noteOff(channel, message[1]); + } + break; + + case messageTypes.pitchBend: + this.pitchWheel(channel, message[2], message[1]); + break; + + case messageTypes.controllerChange: + this.controllerChange(channel, message[1], message[2], force); + break; + + case messageTypes.programChange: + this.programChange(channel, message[1]); + break; + + case messageTypes.polyPressure: + this.polyPressure(channel, message[0], message[1]); + break; + + case messageTypes.channelPressure: + this.channelPressure(channel, message[1]); + break; + + case messageTypes.systemExclusive: + this.systemExclusive(new IndexedByteArray(message.slice(1)), channelOffset); + break; + + case messageTypes.reset: + this.stopAllChannels(true); + this.resetAllControllers(); + break; + + default: + break; + } + }; + + const time = options.time; + if (time > currentTime) + { + this.eventQueue.push({ + callback: call.bind(this), + time: time + }); + this.eventQueue.sort((e1, e2) => e1.time - e2.time); + } + else + { + call(); + } + } +} + +// include other methods +// voice related +SpessaSynthProcessor.prototype.voiceKilling = voiceKilling; +SpessaSynthProcessor.prototype.getWorkletVoices = getWorkletVoices; + +// message port related +SpessaSynthProcessor.prototype.handleMessage = handleMessage; +SpessaSynthProcessor.prototype.callEvent = callEvent; + +// system-exclusive related +SpessaSynthProcessor.prototype.systemExclusive = systemExclusive; + +// channel related +SpessaSynthProcessor.prototype.stopAllChannels = stopAllChannels; +SpessaSynthProcessor.prototype.createWorkletChannel = createWorkletChannel; +SpessaSynthProcessor.prototype.resetAllControllers = resetAllControllers; + +// master parameter related +SpessaSynthProcessor.prototype.setMasterGain = setMasterGain; +SpessaSynthProcessor.prototype.setMasterPan = setMasterPan; +SpessaSynthProcessor.prototype.setMIDIVolume = setMIDIVolume; + +// tuning related +SpessaSynthProcessor.prototype.transposeAllChannels = transposeAllChannels; +SpessaSynthProcessor.prototype.setMasterTuning = setMasterTuning; + +// program related +SpessaSynthProcessor.prototype.getPreset = getPreset; +SpessaSynthProcessor.prototype.reloadSoundFont = reloadSoundFont; +SpessaSynthProcessor.prototype.clearSoundFont = clearSoundFont; +SpessaSynthProcessor.prototype.setEmbeddedSoundFont = setEmbeddedSoundFont; +SpessaSynthProcessor.prototype.sendPresetList = sendPresetList; + +// snapshot related +SpessaSynthProcessor.prototype.sendSynthesizerSnapshot = sendSynthesizerSnapshot; +SpessaSynthProcessor.prototype.applySynthesizerSnapshot = applySynthesizerSnapshot; + +export { SpessaSynthProcessor }; \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/message_protocol/README.md b/spessasynth_lib/synthetizer/worklet_system/message_protocol/README.md new file mode 100644 index 0000000000000000000000000000000000000000..05e5b0e30be6c6b0930f52697c0d53c57f721b26 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/message_protocol/README.md @@ -0,0 +1,13 @@ +# About the message protocol +Since spessasynth runs in the audioWorklet thread, here is an explanation of how it works: + +There's one processor per synthesizer, with a `MessagePort` for communication. +Each processor has a single `WorkletSequencer` instance that is idle by default. + +The `Synthetizer`, +`Sequencer` and `SoundFontManager` classes are all interfaces +that do not do anything except sending the commands to te processor. + +The synthesizer sends the commands (note on, off, etc.) directly to the processor where they are processed and executed. + +The sequencer sends the commands through the connected synthesizer's messagePort, which then get processed as sequencer messages and routed \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/message_protocol/handle_message.js b/spessasynth_lib/synthetizer/worklet_system/message_protocol/handle_message.js new file mode 100644 index 0000000000000000000000000000000000000000..025df3d405b3ed33bfac4ddc7951d7a65308bcc7 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/message_protocol/handle_message.js @@ -0,0 +1,210 @@ +import { + ALL_CHANNELS_OR_DIFFERENT_ACTION, + masterParameterType, + returnMessageType, + workletMessageType +} from "./worklet_message.js"; +import { SpessaSynthLogging, SpessaSynthWarn } from "../../../utils/loggin.js"; + +/** + * @this {SpessaSynthProcessor} + * @param message {WorkletMessage} + */ +export function handleMessage(message) +{ + const data = message.messageData; + const channel = message.channelNumber; + /** + * @type {WorkletProcessorChannel} + */ + let channelObject; + if (channel >= 0) + { + channelObject = this.workletProcessorChannels[channel]; + if (channelObject === undefined) + { + SpessaSynthWarn(`Trying to access channel ${channel} which does not exist... ignoring!`); + return; + } + } + switch (message.messageType) + { + case workletMessageType.midiMessage: + this.processMessage(...data); + break; + + case workletMessageType.customcCcChange: + // custom controller change + channelObject.setCustomController(data[0], data[1]); + channelObject.updateChannelTuning(); + break; + + case workletMessageType.ccReset: + if (channel === ALL_CHANNELS_OR_DIFFERENT_ACTION) + { + this.resetAllControllers(); + } + else + { + channelObject.resetControllers(); + } + break; + + case workletMessageType.setChannelVibrato: + if (channel === ALL_CHANNELS_OR_DIFFERENT_ACTION) + { + for (let i = 0; i < this.workletProcessorChannels.length; i++) + { + const chan = this.workletProcessorChannels[i]; + if (data.rate === -1) + { + chan.disableAndLockGSNRPN(); + } + else + { + chan.setVibrato(data.depth, data.rate, data.delay); + } + } + } + else if (data.rate === -1) + { + channelObject.disableAndLockGSNRPN(); + } + else + { + channelObject.setVibrato(data.depth, data.rate, data.delay); + } + break; + + case workletMessageType.stopAll: + if (channel === ALL_CHANNELS_OR_DIFFERENT_ACTION) + { + this.stopAllChannels(data === 1); + } + else + { + channelObject.stopAllNotes(data === 1); + } + break; + + case workletMessageType.killNotes: + this.voiceKilling(data); + break; + + case workletMessageType.muteChannel: + channelObject.muteChannel(data); + break; + + case workletMessageType.addNewChannel: + this.createWorkletChannel(true); + break; + + case workletMessageType.debugMessage: + this.debugMessage(); + break; + + case workletMessageType.setMasterParameter: + /** + * @type {masterParameterType} + */ + const type = data[0]; + const value = data[1]; + switch (type) + { + 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; + + case masterParameterType.midiSystem: + this.setSystem(value); + } + break; + + case workletMessageType.setDrums: + channelObject.setDrums(data); + break; + + case workletMessageType.transpose: + if (channel === ALL_CHANNELS_OR_DIFFERENT_ACTION) + { + this.transposeAllChannels(data[0], data[1]); + } + else + { + channelObject.transposeChannel(data[0], data[1]); + } + break; + + case workletMessageType.highPerformanceMode: + this.highPerformanceMode = data; + break; + + case workletMessageType.lockController: + if (data[0] === ALL_CHANNELS_OR_DIFFERENT_ACTION) + { + channelObject.setPresetLock(data[1]); + } + else + { + channelObject.lockedControllers[data[0]] = data[1]; + } + break; + + case workletMessageType.sequencerSpecific: + this.sequencer.processMessage(data.messageType, data.messageData); + break; + + case workletMessageType.soundFontManager: + try + { + this.soundfontManager.handleMessage(data[0], data[1]); + } + catch (e) + { + this.post({ + messageType: returnMessageType.soundfontError, + messageData: e + }); + } + this.clearSoundFont(true, false); + break; + + case workletMessageType.keyModifierManager: + this.keyModifierManager.handleMessage(data[0], data[1]); + break; + + case workletMessageType.requestSynthesizerSnapshot: + this.sendSynthesizerSnapshot(); + break; + + case workletMessageType.setLogLevel: + SpessaSynthLogging(data[0], data[1], data[2], data[3]); + break; + + case workletMessageType.setEffectsGain: + this.reverbGain = data[0]; + this.chorusGain = data[1]; + break; + + case workletMessageType.destroyWorklet: + this.alive = false; + this.destroyWorkletProcessor(); + break; + + default: + SpessaSynthWarn("Unrecognized event:", data); + break; + } +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/message_protocol/message_sending.js b/spessasynth_lib/synthetizer/worklet_system/message_protocol/message_sending.js new file mode 100644 index 0000000000000000000000000000000000000000..9f3b785927dcfa4e542efadefff0c90c68a902c4 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/message_protocol/message_sending.js @@ -0,0 +1,22 @@ +import { returnMessageType } from "./worklet_message.js"; + +/** + * Calls synth event from the worklet side + * @param eventName {EventTypes} the event name + * @param eventData {EventCallbackData} + * @this {SpessaSynthProcessor} + */ +export function callEvent(eventName, eventData) +{ + if (!this.enableEventSystem) + { + return; + } + this.post({ + messageType: returnMessageType.eventCall, + messageData: { + eventName: eventName, + eventData: eventData + } + }); +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/message_protocol/worklet_message.js b/spessasynth_lib/synthetizer/worklet_system/message_protocol/worklet_message.js new file mode 100644 index 0000000000000000000000000000000000000000..b7ad056ff6255ddc27872ff893402dce4c030fe4 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/message_protocol/worklet_message.js @@ -0,0 +1,118 @@ +/** + * @enum {number} + * // NOTE: Every message needs a channel number (if not relevant or all, set to -1) + * @property {number} midiMessage - 0 -> [messageData, channelOffset, force, options] + * @property {number} ccReset - 7 -> (no data) note: if channel is -1 then reset all channels + * @property {number} setChannelVibrato - 8 -> {frequencyHz: number, depthCents: number, delaySeconds: number} note: if channel is -1 then stop all channels note 2: if rate is -1, it means locking + * @property {number} soundFontManager - 9 -> [messageType messageData] note: refer to sfman_message.js + * @property {number} stopAll - 10 -> force (0 false, 1 true) note: if channel is -1 then stop all channels + * @property {number} killNotes - 11 -> amount + * @property {number} muteChannel - 12 -> isMuted + * @property {number} addNewChannel - 13 -> (no data) + * @property {number} customCcChange - 14 -> [ccNumber, ccValue] + * @property {number} debugMessage - 15 -> (no data) + * @property {number} setMasterParameter - 17 -> [parameter, value] + * @property {number} setDrums - 18 -> isDrums + * @property {number} transpose - 19 -> [semitones, force] note: if channel is -1 then transpose all channels + * @property {number} highPerformanceMode - 20 -> isOn + * @property {number} lockController - 21 -> [controllerNumber, isLocked] + * @property {number} sequencerSpecific - 22 -> [messageType messageData] note: refer to sequencer_message.js + * @property {number} requestSynthesizerSnapshot - 23 -> (no data) + * @property {number} setLogLevel - 24 -> [enableInfo, enableWarning, enableGroup, enableTable] + * @property {number} keyModifier - 25 -> [messageType messageData] + * @property {number} setEffectsGain - 26 -> [reverbGain, chorusGain] + * @property {number} destroyWorklet - 27 -> (no data) + */ +export const workletMessageType = { + midiMessage: 0, + // free 6 slots here, use when needed instead of adding new ones + ccReset: 7, + setChannelVibrato: 8, + soundFontManager: 9, + stopAll: 10, + killNotes: 11, + muteChannel: 12, + addNewChannel: 13, + customcCcChange: 14, + debugMessage: 15, + // free slot here + setMasterParameter: 17, + setDrums: 18, + transpose: 19, + highPerformanceMode: 20, + lockController: 21, + sequencerSpecific: 22, + requestSynthesizerSnapshot: 23, + setLogLevel: 24, + keyModifierManager: 25, + setEffectsGain: 26, + destroyWorklet: 27 +}; + +/** + * @enum {number} + */ +export const masterParameterType = { + mainVolume: 0, + masterPan: 1, + voicesCap: 2, + interpolationType: 3, + midiSystem: 4 +}; + + +export const ALL_CHANNELS_OR_DIFFERENT_ACTION = -1; +/** + * @typedef {{ + * channelNumber: number + * messageType: (workletMessageType|number), + * messageData: ( + * boolean| + * (number|Uint8Array|object)[] + * |undefined + * |boolean[] + * |boolean + * |WorkletVoice[] + * |number + * |{rate: number, depth: number, delay: number} + * |ArrayBuffer + * |{messageType: WorkletSequencerMessageType, messageData: any} + * |{messageType: workletKeyModifierMessageType, messageData: any} + * ) + * }} WorkletMessage + */ + +/** + * @typedef {Object} WorkletReturnMessage + * @property {returnMessageType} messageType - the message's type + * @property {{ + * eventName: string, + * eventData: any + * }|ChannelProperty + * |{presetName: string, bank: number, program: number}[] + * |string + * |{messageType: WorkletSequencerReturnMessageType, messageData: any} + * |SynthesizerSnapshot + * |[WorkletSoundfontManagerMessageType, any]} messageData - the message's data + * + * 0 - channel property change -> [channel, property] see message_sending.js line 29 + * 1 - event call -> {eventName, eventData:} + * 2 - master parameter change -> [parameter, value] + * 3 - sequencer specific -> [messageType messageData] note: refer to sequencer_message.js + * 4 - synthesizer snapshot -> snapshot note: refer to synthesizer_snapshot.js + * 5 - isFullyInitialized -> (no data) + * 6 - soundfontError -> errorMessage + */ + +/** + * @enum {number} + */ +export const returnMessageType = { + channelPropertyChange: 0, + eventCall: 1, + masterParameterChange: 2, + sequencerSpecific: 3, + synthesizerSnapshot: 4, + isFullyInitialized: 5, + soundfontError: 6 +}; \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/snapshot/apply_synthesizer_snapshot.js b/spessasynth_lib/synthetizer/worklet_system/snapshot/apply_synthesizer_snapshot.js new file mode 100644 index 0000000000000000000000000000000000000000..e355e32432c6ebc49996694c7d17672874405a76 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/snapshot/apply_synthesizer_snapshot.js @@ -0,0 +1,14 @@ +import { SpessaSynthInfo } from "../../../utils/loggin.js"; +import { consoleColors } from "../../../utils/other.js"; +import { SynthesizerSnapshot } from "./synthesizer_snapshot.js"; + +/** + * Applies the snapshot to the synth + * @param snapshot {SynthesizerSnapshot} + * @this {SpessaSynthProcessor} + */ +export function applySynthesizerSnapshot(snapshot) +{ + SynthesizerSnapshot.applySnapshot(this, snapshot); + SpessaSynthInfo("%cFinished applying snapshot!", consoleColors.info); +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/snapshot/channel_snapshot.js b/spessasynth_lib/synthetizer/worklet_system/snapshot/channel_snapshot.js new file mode 100644 index 0000000000000000000000000000000000000000..110db79386c4fec214082042d2e4785f349c6290 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/snapshot/channel_snapshot.js @@ -0,0 +1,175 @@ +/** + * Represents a snapshot of a single channel's state in the synthesizer. + */ +export class ChannelSnapshot +{ + /** + * The channel's MIDI program number. + * @type {number} + */ + program; + + /** + * The channel's bank number. + * @type {number} + */ + bank; + + /** + * If the bank is LSB. For restoring. + * @type {boolean} + */ + isBankLSB; + + /** + * The name of the patch currently loaded in the channel. + * @type {string} + */ + patchName; + + /** + * Indicates whether the channel's program change is disabled. + * @type {boolean} + */ + lockPreset; + + /** + * Indicates the MIDI system when the preset was locked + * @type {SynthSystem} + */ + lockedSystem; + + /** + * The array of all MIDI controllers (in 14-bit values) with the modulator sources at the end. + * @type {Int16Array} + */ + midiControllers; + + /** + * An array of booleans, indicating if the controller with a current index is locked. + * @type {boolean[]} + */ + lockedControllers; + + /** + * Array of custom (not SF2) control values such as RPN pitch tuning, transpose, modulation depth, etc. + * @type {Float32Array} + */ + customControllers; + + /** + * Indicates whether the channel vibrato is locked. + * @type {boolean} + */ + lockVibrato; + + /** + * The channel's vibrato settings. + * @type {Object} + * @property {number} depth - Vibrato depth, in gain. + * @property {number} delay - Vibrato delay from note on in seconds. + * @property {number} rate - Vibrato rate in Hz. + */ + channelVibrato; + + /** + * Key shift for the channel. + * @type {number} + */ + channelTransposeKeyShift; + + /** + * The channel's octave tuning in cents. + * @type {Int8Array} + */ + channelOctaveTuning; + + /** + * Indicates whether the channel is muted. + * @type {boolean} + */ + isMuted; + + /** + * Overrides velocity if greater than 0, otherwise disabled. + * @type {number} + */ + velocityOverride; + + /** + * Indicates whether the channel is a drum channel. + * @type {boolean} + */ + drumChannel; + + /** + * Creates a snapshot of a single channel's state in the synthesizer. + * @param workletProcessor {SpessaSynthProcessor} + * @param channelNumber {number} + * @returns {ChannelSnapshot} + */ + static getChannelSnapshot(workletProcessor, channelNumber) + { + const channelObject = workletProcessor.workletProcessorChannels[channelNumber]; + const channelSnapshot = new ChannelSnapshot(); + // program data + channelSnapshot.program = channelObject.preset.program; + channelSnapshot.bank = channelObject.getBankSelect(); + channelSnapshot.isBankLSB = channelSnapshot.bank !== channelObject.bank; + channelSnapshot.lockPreset = channelObject.lockPreset; + channelSnapshot.lockedSystem = channelObject.lockedSystem; + channelSnapshot.patchName = channelObject.preset.presetName; + + // controller data + channelSnapshot.midiControllers = channelObject.midiControllers; + channelSnapshot.lockedControllers = channelObject.lockedControllers; + channelSnapshot.customControllers = channelObject.customControllers; + + // vibrato data + channelSnapshot.channelVibrato = channelObject.channelVibrato; + channelSnapshot.lockVibrato = channelObject.lockGSNRPNParams; + + // tuning and transpose data + channelSnapshot.channelTransposeKeyShift = channelObject.channelTransposeKeyShift; + channelSnapshot.channelOctaveTuning = channelObject.channelOctaveTuning; + + // other data + channelSnapshot.isMuted = channelObject.isMuted; + channelSnapshot.velocityOverride = channelObject.velocityOverride; + channelSnapshot.drumChannel = channelObject.drumChannel; + return channelSnapshot; + } + + /** + * Applies the snapshot to the specified channel. + * @param workletProcessor {SpessaSynthProcessor} + * @param channelNumber {number} + * @param channelSnapshot {ChannelSnapshot} + */ + static applyChannelSnapshot(workletProcessor, channelNumber, channelSnapshot) + { + const channelObject = workletProcessor.workletProcessorChannels[channelNumber]; + channelObject.muteChannel(channelSnapshot.isMuted); + channelObject.setDrums(channelSnapshot.drumChannel); + + // restore controllers + channelObject.midiControllers = channelSnapshot.midiControllers; + channelObject.lockedControllers = channelSnapshot.lockedControllers; + channelObject.customControllers = channelSnapshot.customControllers; + channelObject.updateChannelTuning(); + + // 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.setPresetLock(false); + channelObject.setBankSelect(channelSnapshot.bank, channelSnapshot.isBankLSB); + channelObject.programChange(channelSnapshot.program); + channelObject.setPresetLock(channelSnapshot.lockPreset); + channelObject.lockedSystem = channelSnapshot.lockedSystem; + } +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/snapshot/send_synthesizer_snapshot.js b/spessasynth_lib/synthetizer/worklet_system/snapshot/send_synthesizer_snapshot.js new file mode 100644 index 0000000000000000000000000000000000000000..cc05eb8c08063e1cef5e9e2eab2045e3839513aa --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/snapshot/send_synthesizer_snapshot.js @@ -0,0 +1,14 @@ +import { returnMessageType } from "../message_protocol/worklet_message.js"; +import { SynthesizerSnapshot } from "./synthesizer_snapshot.js"; + +/** + * sends a snapshot of the current controller values of the synth (used to copy that data to OfflineAudioContext when rendering) + * @this {SpessaSynthProcessor} + */ +export function sendSynthesizerSnapshot() +{ + this.post({ + messageType: returnMessageType.synthesizerSnapshot, + messageData: SynthesizerSnapshot.createSynthesizerSnapshot(this) + }); +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/snapshot/synthesizer_snapshot.js b/spessasynth_lib/synthetizer/worklet_system/snapshot/synthesizer_snapshot.js new file mode 100644 index 0000000000000000000000000000000000000000..81b14fce5de7e8d3ec4c3dd4994ae48d917f5647 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/snapshot/synthesizer_snapshot.js @@ -0,0 +1,121 @@ +import { SpessaSynthInfo } from "../../../utils/loggin.js"; +import { consoleColors } from "../../../utils/other.js"; +import { ChannelSnapshot } from "./channel_snapshot.js"; + +/** + * Represents a snapshot of the synthesizer's state. + */ +export class SynthesizerSnapshot +{ + /** + * The individual channel snapshots. + * @type {ChannelSnapshot[]} + */ + channelSnapshots; + + /** + * Key modifiers. + * @type {KeyModifier[][]} + */ + keyMappings; + + /** + * Main synth volume (set by MIDI), from 0 to 1. + * @type {number} + */ + mainVolume; + + /** + * Master stereo panning, from -1 to 1. + * @type {number} + */ + pan; + + /** + * The synth's interpolation type. + * @type {interpolationTypes} + */ + interpolation; + + /** + * The synth's system. Values can be "gs", "gm", "gm2" or "xg". + * @type {SynthSystem} + */ + system; + + /** + * The current synth transposition in semitones. Can be a float. + * @type {number} + */ + transposition; + + /** + * The effect configuration object. + * @type {SynthConfig} + */ + effectsConfig; + + + /** + * Creates a snapshot of the synthesizer's state. + * @param workletProcessor {SpessaSynthProcessor} + * @returns {SynthesizerSnapshot} + */ + static createSynthesizerSnapshot(workletProcessor) + { + const snapshot = new SynthesizerSnapshot(); + // channel snapshots + snapshot.channelSnapshots = + workletProcessor.workletProcessorChannels.map((_, i) => + ChannelSnapshot.getChannelSnapshot(workletProcessor, i)); + + // key mappings + snapshot.keyMappings = workletProcessor.keyModifierManager.getMappings(); + // pan and volume + snapshot.mainVolume = workletProcessor.midiVolume; + snapshot.pan = workletProcessor.pan; + + // others + snapshot.system = workletProcessor.system; + snapshot.interpolation = workletProcessor.interpolationType; + snapshot.transposition = workletProcessor.transposition; + + // effect config is stored on the main thread, leave it empty + snapshot.effectsConfig = {}; + return snapshot; + + } + + /** + * Applies the snapshot to the synthesizer. + * @param workletProcessor {SpessaSynthProcessor} + * @param snapshot {SynthesizerSnapshot} + */ + static applySnapshot(workletProcessor, snapshot) + { + // restore system + workletProcessor.setSystem(snapshot.system); + + // restore pan and volume + workletProcessor.setMasterGain(snapshot.mainVolume); + workletProcessor.setMasterPan(snapshot.pan); + workletProcessor.transposeAllChannels(snapshot.transposition); + workletProcessor.interpolationType = snapshot.interpolation; + workletProcessor.keyModifierManager.setMappings(snapshot.keyMappings); + + // add channels if more needed + while (workletProcessor.workletProcessorChannels.length < snapshot.channelSnapshots.length) + { + workletProcessor.createWorkletChannel(); + } + + // restore channels + snapshot.channelSnapshots.forEach((channelSnapshot, index) => + { + ChannelSnapshot.applyChannelSnapshot(workletProcessor, index, channelSnapshot); + }); + + SpessaSynthInfo("%cFinished restoring controllers!", consoleColors.info); + } +} + diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/controller_control/controller_change.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/controller_control/controller_change.js new file mode 100644 index 0000000000000000000000000000000000000000..b3b93c589c344bbb41a77d202bc85242b1b9c285 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/controller_control/controller_change.js @@ -0,0 +1,132 @@ +import { midiControllers } from "../../../../midi_parser/midi_message.js"; +import { computeModulators } from "../../worklet_utilities/worklet_modulator.js"; +import { channelConfiguration, dataEntryStates } from "../../worklet_utilities/controller_tables.js"; + +/** + * @param controllerNumber {number} + * @param controllerValue {number} + * @param force {boolean} + * @this {WorkletProcessorChannel} + */ +export function controllerChange(controllerNumber, controllerValue, force = false) +{ + if (controllerNumber > 127) + { + // channel configuration. force must be set to true + if (!force) + { + return; + } + switch (controllerNumber) + { + default: + return; + + case channelConfiguration.velocityOverride: + this.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 ( + controllerNumber >= midiControllers.lsbForControl1ModulationWheel + && controllerNumber <= midiControllers.lsbForControl13EffectControl2 + && controllerNumber !== midiControllers.lsbForControl6DataEntry + ) + { + const actualCCNum = controllerNumber - 32; + if (this.lockedControllers[actualCCNum]) + { + return; + } + // append the lower nibble to the main controller + this.midiControllers[actualCCNum] = (this.midiControllers[actualCCNum] & 0x3F80) | (controllerValue & 0x7F); + this.voices.forEach(v => computeModulators(v, this.midiControllers, 1, actualCCNum)); + } + if (this.lockedControllers[controllerNumber]) + { + return; + } + + // apply the cc to the table + this.midiControllers[controllerNumber] = controllerValue << 7; + + // interpret special CCs + { + switch (controllerNumber) + { + case midiControllers.allNotesOff: + this.stopAllNotes(); + break; + + case midiControllers.allSoundOff: + this.stopAllNotes(true); + break; + + // special case: bank select + case midiControllers.bankSelect: + this.setBankSelect(controllerValue); + break; + + case midiControllers.lsbForControl0BankSelect: + this.setBankSelect(controllerValue, true); + break; + + // check for RPN and NPRN and data entry + case midiControllers.RPNLsb: + this.dataEntryState = dataEntryStates.RPFine; + break; + + case midiControllers.RPNMsb: + this.dataEntryState = dataEntryStates.RPCoarse; + break; + + case midiControllers.NRPNMsb: + this.dataEntryState = dataEntryStates.NRPCoarse; + break; + + case midiControllers.NRPNLsb: + this.dataEntryState = dataEntryStates.NRPFine; + break; + + case midiControllers.dataEntryMsb: + this.dataEntryCoarse(controllerValue); + break; + + case midiControllers.lsbForControl6DataEntry: + this.dataEntryFine(controllerValue); + break; + + case midiControllers.resetAllControllers: + this.resetControllersRP15Compliant(); + break; + + case midiControllers.sustainPedal: + if (controllerValue >= 64) + { + this.holdPedal = true; + } + else + { + this.holdPedal = false; + this.sustainedVoices.forEach(v => + { + v.release(); + }); + this.sustainedVoices = []; + } + break; + + // default: just compute modulators + default: + this.voices.forEach(v => computeModulators(v, this.midiControllers, 1, controllerNumber)); + break; + } + } + this.synth.callEvent("controllerchange", { + channel: this.channelNumber, + controllerNumber: controllerNumber, + controllerValue: controllerValue + }); +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/controller_control/master_parameters.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/controller_control/master_parameters.js new file mode 100644 index 0000000000000000000000000000000000000000..32dd0d8d97a9b21142a74249fc6bde2bb9f3eb22 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/controller_control/master_parameters.js @@ -0,0 +1,36 @@ +import { SYNTHESIZER_GAIN } from "../../main_processor.js"; + +/** + * @param volume {number} 0 to 1 + * @this {SpessaSynthProcessor} + */ +export function setMIDIVolume(volume) +{ + // GM2 specification, section 4.1: master-volume is squared though, + // according to my own testing, e seems like a better choice + this.midiVolume = Math.pow(volume, Math.E); + this.setMasterPan(this.pan); +} + +/** + * @param volume {number} 0-1 + * @this {SpessaSynthProcessor} + */ +export function setMasterGain(volume) +{ + this.masterGain = volume * SYNTHESIZER_GAIN; + this.setMasterPan(this.pan); +} + +/** + * @param pan {number} -1 to one + * @this {SpessaSynthProcessor} + */ +export function setMasterPan(pan) +{ + this.pan = pan; + // clamp to 0-1 (0 is left) + pan = (pan / 2) + 0.5; + this.panLeft = (1 - pan); + this.panRight = (pan); +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/controller_control/reset_controllers.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/controller_control/reset_controllers.js new file mode 100644 index 0000000000000000000000000000000000000000..d500258ff9de1d6cbc5163ae0d0e64b1275f4dd0 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/controller_control/reset_controllers.js @@ -0,0 +1,241 @@ +import { consoleColors } from "../../../../utils/other.js"; +import { SpessaSynthInfo } from "../../../../utils/loggin.js"; +import { modulatorSources } from "../../../../soundfont/basic_soundfont/modulator.js"; +import { + customControllers, + customResetArray, + dataEntryStates, + NON_CC_INDEX_OFFSET, + PORTAMENTO_CONTROL_UNSET, + resetArray +} from "../../worklet_utilities/controller_tables.js"; +import { midiControllers } from "../../../../midi_parser/midi_message.js"; +import { DEFAULT_PERCUSSION, DEFAULT_SYNTH_MODE } from "../../../synth_constants.js"; +import { getDefaultBank } from "../../../../utils/xg_hacks.js"; + + +/** + * Full system reset + * @this {SpessaSynthProcessor} + * @param log {boolean} + */ +export function resetAllControllers(log = true) +{ + if (log) + { + SpessaSynthInfo("%cResetting all controllers!", consoleColors.info); + } + this.callEvent("allcontrollerreset", undefined); + this.setSystem(DEFAULT_SYNTH_MODE); + for (let channelNumber = 0; channelNumber < this.workletProcessorChannels.length; channelNumber++) + { + this.workletProcessorChannels[channelNumber].resetControllers(); + + /** + * @type {WorkletProcessorChannel} + **/ + const ch = this.workletProcessorChannels[channelNumber]; + + // if preset is unlocked, switch to non-drums and call event + if (!ch.lockPreset) + { + ch.setBankSelect(getDefaultBank(this.system)); + if (channelNumber % 16 === DEFAULT_PERCUSSION) + { + ch.setPreset(this.drumPreset); + ch.presetUsesOverride = this.defaultDrumsUsesOverride; + ch.drumChannel = true; + this.callEvent("drumchange", { + channel: channelNumber, + isDrumChannel: true + }); + } + else + { + ch.drumChannel = false; + ch.presetUsesOverride = this.defaultDrumsUsesOverride; + ch.setPreset(this.defaultPreset); + this.callEvent("drumchange", { + channel: channelNumber, + isDrumChannel: false + }); + } + } + else + { + this.callEvent("drumchange", { + channel: channelNumber, + isDrumChannel: ch.drumChannel + }); + } + + const presetBank = ch.preset.bank; + const sentBank = presetBank === 128 ? 128 : (ch.presetUsesOverride ? presetBank + this.soundfontBankOffset : presetBank); + + // call program change + this.callEvent("programchange", { + channel: channelNumber, + program: ch.preset.program, + bank: sentBank + }); + + for (let ccNum = 0; ccNum < 128; ccNum++) + { + if (this.workletProcessorChannels[channelNumber].lockedControllers[ccNum]) + { + // was not reset so restore the value + this.callEvent("controllerchange", { + channel: channelNumber, + controllerNumber: ccNum, + controllerValue: this.workletProcessorChannels[channelNumber].midiControllers[ccNum] >> 7 + }); + } + + } + + + // restore pitch wheel + if (this.workletProcessorChannels[channelNumber].lockedControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel] === false) + { + const val = this.workletProcessorChannels[channelNumber].midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel]; + const msb = val >> 7; + const lsb = val & 0x7F; + this.callEvent("pitchwheel", { + channel: channelNumber, + MSB: msb, + LSB: lsb + }); + } + } + this.tunings = []; + this.tunings = []; + for (let i = 0; 127 > i; i++) + { + this.tunings.push([]); + } + + this.setMIDIVolume(1); +} + +/** + * Resets all controllers for channel + * @this {WorkletProcessorChannel} + */ +export function resetControllers() +{ + this.channelOctaveTuning.fill(0); + + // reset the array + for (let i = 0; i < resetArray.length; i++) + { + if (this.lockedControllers[i]) + { + continue; + } + const resetValue = resetArray[i]; + if (this.midiControllers[i] !== resetValue && i < 127) + { + if (i === midiControllers.portamentoControl) + { + this.midiControllers[i] = PORTAMENTO_CONTROL_UNSET; + } + else + { + this.controllerChange(i, resetValue >> 7); + } + } + else + { + // out of range, do a regular reset + this.midiControllers[i] = resetValue; + } + } + this.channelVibrato = { rate: 0, depth: 0, delay: 0 }; + this.holdPedal = false; + this.randomPan = false; + + // reset custom controllers + // special case: transpose does not get affected + const transpose = this.customControllers[customControllers.channelTransposeFine]; + this.customControllers.set(customResetArray); + this.setCustomController(customControllers.channelTransposeFine, transpose); + + this.resetParameters(); + +} + + +/** + * @type {Set} + */ +export const nonResetableCCs = new Set([ + midiControllers.bankSelect, + midiControllers.lsbForControl0BankSelect, + midiControllers.mainVolume, + midiControllers.lsbForControl7MainVolume, + midiControllers.pan, + midiControllers.lsbForControl10Pan, + midiControllers.reverbDepth, + midiControllers.tremoloDepth, + midiControllers.chorusDepth, + midiControllers.detuneDepth, + midiControllers.phaserDepth, + midiControllers.soundVariation, + midiControllers.filterResonance, + midiControllers.releaseTime, + midiControllers.attackTime, + midiControllers.brightness, + midiControllers.decayTime, + midiControllers.vibratoRate, + midiControllers.vibratoDepth, + midiControllers.vibratoDelay, + midiControllers.soundController10 +]); + +/** + * Reset all controllers for channel, but RP-15 compliant + * https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf + * @this {WorkletProcessorChannel} + */ +export function resetControllersRP15Compliant() +{ + // reset tunings + this.channelOctaveTuning.fill(0); + + // reset pitch bend + this.pitchWheel(64, 0); + + this.channelVibrato = { rate: 0, depth: 0, delay: 0 }; + + for (let i = 0; i < 128; i++) + { + const resetValue = resetArray[i]; + if (!nonResetableCCs.has(i) && resetValue !== this.midiControllers[i]) + { + if (i === midiControllers.portamentoControl) + { + this.midiControllers[i] = PORTAMENTO_CONTROL_UNSET; + } + else + { + this.controllerChange(i, resetValue >> 7); + } + } + } +} + +/** + * @this {WorkletProcessorChannel} + */ +export function resetParameters() +{ + /** + * reset the state machine to idle + * @type {string} + */ + this.dataEntryState = dataEntryStates.Idle; + SpessaSynthInfo( + "%cResetting Registered and Non-Registered Parameters!", + consoleColors.info + ); +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/create_worklet_channel.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/create_worklet_channel.js new file mode 100644 index 0000000000000000000000000000000000000000..f52aafaf1c972c7e33b8633b4ad3d6385a79ed45 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/create_worklet_channel.js @@ -0,0 +1,27 @@ +import { WorkletProcessorChannel } from "../worklet_utilities/worklet_processor_channel.js"; + +import { DEFAULT_PERCUSSION } from "../../synth_constants.js"; + +/** + * @param sendEvent {boolean} + * @this {SpessaSynthProcessor} + */ +export function createWorkletChannel(sendEvent = false) +{ + /** + * @type {WorkletProcessorChannel} + */ + const channel = new WorkletProcessorChannel(this, this.defaultPreset, this.workletProcessorChannels.length); + this.workletProcessorChannels.push(channel); + channel.resetControllers(); + channel.sendChannelProperty(); + if (sendEvent) + { + this.callEvent("newchannel", undefined); + } + + if (channel.channelNumber % 16 === DEFAULT_PERCUSSION) + { + this.workletProcessorChannels[this.workletProcessorChannels.length - 1].setDrums(true); + } +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/data_entry/data_entry_coarse.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/data_entry/data_entry_coarse.js new file mode 100644 index 0000000000000000000000000000000000000000..df87d31805685455a0f6d0418d597d57a3502495 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/data_entry/data_entry_coarse.js @@ -0,0 +1,253 @@ +import { customControllers, dataEntryStates, NON_CC_INDEX_OFFSET } from "../../worklet_utilities/controller_tables.js"; +import { SpessaSynthInfo, SpessaSynthWarn } from "../../../../utils/loggin.js"; +import { consoleColors } from "../../../../utils/other.js"; +import { midiControllers } from "../../../../midi_parser/midi_message.js"; +import { modulatorSources } from "../../../../soundfont/basic_soundfont/modulator.js"; + + +/** + * @enum {number} + */ +const registeredParameterTypes = { + pitchBendRange: 0x0000, + fineTuning: 0x0001, + coarseTuning: 0x0002, + modulationDepth: 0x0005, + resetParameters: 0x3FFF +}; + +/** + * https://cdn.roland.com/assets/media/pdf/SC-88PRO_OM.pdf + * http://hummer.stanford.edu/sig/doc/classes/MidiOutput/rpn.html + * @enum {number} + */ +const nonRegisteredParameterNumbers = { + partParameter: 0x01, + + vibratoRate: 0x08, + vibratoDepth: 0x09, + vibratoDelay: 0x0A, + + EGAttackTime: 0x64, + EGReleaseTime: 0x66, + + TVFFilterCutoff: 0x20, + drumReverb: 0x1D +}; + + +/** + * Executes a data entry for an NRP for a sc88pro NRP (because touhou yes) and RPN tuning + * @param dataValue {number} dataEntryCoarse MSB + * @this {WorkletProcessorChannel} + * @private + */ +export function dataEntryCoarse(dataValue) +{ + const addDefaultVibrato = () => + { + if (this.channelVibrato.delay === 0 && this.channelVibrato.rate === 0 && this.channelVibrato.depth === 0) + { + this.channelVibrato.depth = 50; + this.channelVibrato.rate = 8; + this.channelVibrato.delay = 0.6; + } + }; + + const coolInfo = (what, value, type) => + { + if (type.length > 0) + { + type = " " + type; + } + SpessaSynthInfo( + `%c${what} for %c${this.channelNumber}%c is now set to %c${value}%c${type}.`, + consoleColors.info, + consoleColors.recognized, + consoleColors.info, + consoleColors.value, + consoleColors.info + ); + }; + switch (this.dataEntryState) + { + default: + case dataEntryStates.Idle: + break; + + // process GS NRPNs + case dataEntryStates.NRPFine: + if (this.lockGSNRPNParams) + { + return; + } + /** + * @type {number} + */ + const NRPNCoarse = this.midiControllers[midiControllers.NRPNMsb] >> 7; + /** + * @type {number} + */ + const NRPNFine = this.midiControllers[midiControllers.NRPNLsb] >> 7; + switch (NRPNCoarse) + { + default: + if (dataValue === 64) + { + // default value + return; + } + SpessaSynthWarn( + `%cUnrecognized NRPN for %c${this.channelNumber}%c: %c(0x${NRPNFine.toString(16) + .toUpperCase()} 0x${NRPNFine.toString( + 16).toUpperCase()})%c data value: %c${dataValue}`, + consoleColors.warn, + consoleColors.recognized, + consoleColors.warn, + consoleColors.unrecognized, + consoleColors.warn, + consoleColors.value + ); + break; + + // part parameters: vibrato, cutoff + case nonRegisteredParameterNumbers.partParameter: + switch (NRPNFine) + { + default: + if (dataValue === 64) + { + // default value + return; + } + SpessaSynthWarn( + `%cUnrecognized NRPN for %c${this.channelNumber}%c: %c(0x${NRPNCoarse.toString(16)} 0x${NRPNFine.toString( + 16)})%c data value: %c${dataValue}`, + consoleColors.warn, + consoleColors.recognized, + consoleColors.warn, + consoleColors.unrecognized, + consoleColors.warn, + consoleColors.value + ); + break; + + // vibrato rate + case nonRegisteredParameterNumbers.vibratoRate: + if (dataValue === 64) + { + return; + } + addDefaultVibrato(); + this.channelVibrato.rate = (dataValue / 64) * 8; + coolInfo("Vibrato rate", `${dataValue} = ${this.channelVibrato.rate}`, "Hz"); + break; + + // vibrato depth + case nonRegisteredParameterNumbers.vibratoDepth: + if (dataValue === 64) + { + return; + } + addDefaultVibrato(); + this.channelVibrato.depth = dataValue / 2; + coolInfo("Vibrato depth", `${dataValue} = ${this.channelVibrato.depth}`, "cents of detune"); + break; + + // vibrato delay + case nonRegisteredParameterNumbers.vibratoDelay: + if (dataValue === 64) + { + return; + } + addDefaultVibrato(); + this.channelVibrato.delay = (dataValue / 64) / 3; + coolInfo("Vibrato delay", `${dataValue} = ${this.channelVibrato.delay}`, "seconds"); + break; + + // filter cutoff + case nonRegisteredParameterNumbers.TVFFilterCutoff: + // affect the "brightness" controller as we have a default modulator that controls it + this.controllerChange(midiControllers.brightness, dataValue); + coolInfo("Filter cutoff", dataValue.toString(), ""); + break; + + // attack time + case nonRegisteredParameterNumbers.EGAttackTime: + // affect the "attack time" controller as we have a default modulator that controls it + this.controllerChange(midiControllers.attackTime, dataValue); + coolInfo("EG attack time", dataValue.toString(), ""); + break; + + // release time + case nonRegisteredParameterNumbers.EGReleaseTime: + // affect the "release time" controller as we have a default modulator that controls it + this.controllerChange(midiControllers.releaseTime, dataValue); + coolInfo("EG release time", dataValue.toString(), ""); + break; + } + break; + + // drum reverb + case nonRegisteredParameterNumbers.drumReverb: + const reverb = dataValue; + this.controllerChange(midiControllers.reverbDepth, reverb); + coolInfo("GS Drum reverb", reverb.toString(), "percent"); + break; + } + break; + + case dataEntryStates.RPCoarse: + case dataEntryStates.RPFine: + /** + * @type {number} + */ + const rpnValue = this.midiControllers[midiControllers.RPNMsb] | (this.midiControllers[midiControllers.RPNLsb] >> 7); + switch (rpnValue) + { + default: + SpessaSynthWarn( + `%cUnrecognized RPN for %c${this.channelNumber}%c: %c(0x${rpnValue.toString(16)})%c data value: %c${dataValue}`, + consoleColors.warn, + consoleColors.recognized, + consoleColors.warn, + consoleColors.unrecognized, + consoleColors.warn, + consoleColors.value + ); + break; + + // pitch bend range + case registeredParameterTypes.pitchBendRange: + this.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheelRange] = dataValue << 7; + coolInfo("Pitch bend range", dataValue.toString(), "semitones"); + break; + + // coarse tuning + case registeredParameterTypes.coarseTuning: + // semitones + const semitones = dataValue - 64; + this.setCustomController(customControllers.channelTuningSemitones, semitones); + coolInfo("Coarse tuning", semitones.toString(), "semitones"); + break; + + // fine-tuning + case registeredParameterTypes.fineTuning: + // note: this will not work properly unless the lsb is sent! + // here we store the raw value to then adjust in fine + this.setTuning(dataValue - 64, false); + break; + + // modulation depth + case registeredParameterTypes.modulationDepth: + this.setModulationDepth(dataValue * 100); + break; + + case registeredParameterTypes.resetParameters: + this.resetParameters(); + break; + + } + + } +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/data_entry/data_entry_fine.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/data_entry/data_entry_fine.js new file mode 100644 index 0000000000000000000000000000000000000000..a08bddadf924c58c17eb23b949ae07a580ef8934 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/data_entry/data_entry_fine.js @@ -0,0 +1,66 @@ +import { consoleColors } from "../../../../utils/other.js"; +import { SpessaSynthInfo } from "../../../../utils/loggin.js"; +import { modulatorSources } from "../../../../soundfont/basic_soundfont/modulator.js"; +import { customControllers, dataEntryStates, NON_CC_INDEX_OFFSET } from "../../worklet_utilities/controller_tables.js"; +import { midiControllers } from "../../../../midi_parser/midi_message.js"; + +/** + * Executes a data entry for an RPN tuning + * @param dataValue {number} dataEntry LSB + * @this {WorkletProcessorChannel} + * @private + */ +export function dataEntryFine(dataValue) +{ + switch (this.dataEntryState) + { + default: + break; + + case dataEntryStates.RPCoarse: + case dataEntryStates.RPFine: + const rpnValue = this.midiControllers[midiControllers.RPNMsb] | (this.midiControllers[midiControllers.RPNLsb] >> 7); + switch (rpnValue) + { + default: + break; + + // pitch bend range fine tune + case 0x0000: + if (dataValue === 0) + { + break; + } + // 14-bit value, so upper 7 are coarse and lower 7 are fine! + this.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheelRange] |= dataValue; + const actualTune = (this.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheelRange] >> 7) + dataValue / 128; + SpessaSynthInfo( + `%cChannel ${this.channelNumber} bend range. Semitones: %c${actualTune}`, + consoleColors.info, + consoleColors.value + ); + break; + + // fine-tuning + case 0x0001: + // grab the data and shift + const coarse = this.customControllers[customControllers.channelTuning]; + const finalTuning = (coarse << 7) | dataValue; + this.setTuning(finalTuning * 0.01220703125); // multiply by 8192 / 100 (cent increments) + break; + + // modulation depth + case 0x0005: + const currentModulationDepthCents = this.customControllers[customControllers.modulationMultiplier] * 50; + let cents = currentModulationDepthCents + (dataValue / 128) * 100; + this.setModulationDepth(cents); + break; + + case 0x3FFF: + this.resetParameters(); + break; + + } + + } +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/mute_channel.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/mute_channel.js new file mode 100644 index 0000000000000000000000000000000000000000..7131c5e7d196598861b84cb12f70b9c76267af73 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/mute_channel.js @@ -0,0 +1,17 @@ +/** + * @param isMuted {boolean} + * @this {WorkletProcessorChannel} + */ +export function muteChannel(isMuted) +{ + if (isMuted) + { + this.stopAllNotes(true); + } + this.isMuted = isMuted; + this.sendChannelProperty(); + this.synth.callEvent("mutechannel", { + channel: this.channelNumber, + isMuted: isMuted + }); +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/note_on.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/note_on.js new file mode 100644 index 0000000000000000000000000000000000000000..0c73b49057812d823f9c2567210b3ccb6f313516 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/note_on.js @@ -0,0 +1,169 @@ +import { computeModulators } from "../worklet_utilities/worklet_modulator.js"; +import { generatorTypes } from "../../../soundfont/basic_soundfont/generator.js"; +import { midiControllers } from "../../../midi_parser/midi_message.js"; +import { portamentoTimeToSeconds } from "./portamento_time.js"; + +/** + * sends a "MIDI Note on message" + * @param midiNote {number} + * @param velocity {number} + * @this {WorkletProcessorChannel} + */ +export function noteOn(midiNote, velocity) +{ + if (velocity < 1) + { + this.noteOff(midiNote); + return; + } + velocity = Math.min(127, velocity); + + if ( + (this.synth.highPerformanceMode && this.synth.totalVoicesAmount > 200 && velocity < 40) || + (this.synth.highPerformanceMode && velocity < 10) || + (this.isMuted) + ) + { + return; + } + + const realKey = midiNote + this.channelTransposeKeyShift; + let sentMidiNote = realKey; + + if (realKey > 127 || realKey < 0) + { + return; + } + const program = this.preset.program; + if (this.synth.tunings[program]?.[realKey]?.midiNote >= 0) + { + sentMidiNote = this.synth.tunings[program]?.[realKey].midiNote; + } + + // velocity override + if (this.velocityOverride > 0) + { + velocity = this.velocityOverride; + } + + // key velocity override + const keyVel = this.synth.keyModifierManager.getVelocity(this.channelNumber, realKey); + if (keyVel > -1) + { + velocity = keyVel; + } + + // portamento + let portamentoFromKey = -1; + let portamentoDuration = 0; + // note: the 14-bit value needs to go down to 7-bit + const portamentoTime = this.midiControllers[midiControllers.portamentoTime] >> 7; + const control = this.midiControllers[midiControllers.portamentoControl]; + const currentFromKey = control >> 7; + if ( + !this.drumChannel && // no portamento on drum channel + currentFromKey !== sentMidiNote && // if the same note, there's no portamento + this.midiControllers[midiControllers.portamentoOnOff] >= 8192 && // (64 << 7) + portamentoTime > 0 // 0 duration is no portamento + ) + { + // a value of one means the initial portamento + if (control !== 1) + { + const diff = Math.abs(sentMidiNote - currentFromKey); + portamentoDuration = portamentoTimeToSeconds(portamentoTime, diff); + portamentoFromKey = currentFromKey; + } + // set portamento control to previous value + this.controllerChange(midiControllers.portamentoControl, sentMidiNote); + } + + // get voices + const voices = this.synth.getWorkletVoices( + this.channelNumber, + sentMidiNote, + velocity, + currentTime, + realKey + ); + + // zero means disabled + let panOverride = 0; + if (this.randomPan) + { + // the range is -500 to 500 + panOverride = Math.round(Math.random() * 1000 - 500); + } + + // add voices + const channelVoices = this.voices; + voices.forEach(voice => + { + // apply portamento + voice.portamentoFromKey = portamentoFromKey; + voice.portamentoDuration = portamentoDuration; + + // apply pan override + voice.overridePan = panOverride; + + // apply exclusive class + const exclusive = voice.exclusiveClass; + if (exclusive !== 0) + { + // kill all voices with the same exclusive class + channelVoices.forEach(v => + { + if (v.exclusiveClass === exclusive) + { + v.exclusiveRelease(); + } + }); + } + // compute all modulators + computeModulators(voice, this.midiControllers); + // modulate sample offsets (these are not real time) + const cursorStartOffset = voice.modulatedGenerators[generatorTypes.startAddrsOffset] + voice.modulatedGenerators[generatorTypes.startAddrsCoarseOffset] * 32768; + const endOffset = voice.modulatedGenerators[generatorTypes.endAddrOffset] + voice.modulatedGenerators[generatorTypes.endAddrsCoarseOffset] * 32768; + const loopStartOffset = voice.modulatedGenerators[generatorTypes.startloopAddrsOffset] + voice.modulatedGenerators[generatorTypes.startloopAddrsCoarseOffset] * 32768; + const loopEndOffset = voice.modulatedGenerators[generatorTypes.endloopAddrsOffset] + voice.modulatedGenerators[generatorTypes.endloopAddrsCoarseOffset] * 32768; + 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.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) + { + const temp = sm.loopStart; + sm.loopStart = sm.loopEnd; + sm.loopEnd = temp; + } + if (sm.loopEnd - sm.loopStart < 1) + { + sm.loopingMode = 0; + sm.isLooping = false; + } + // set the current attenuation to target, + // as it's interpolated (we don't want 0 attenuation for even a split second) + voice.volumeEnvelope.attenuation = voice.volumeEnvelope.attenuationTargetGain; + // 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 to 500 + }); + + this.synth.totalVoicesAmount += voices.length; + // cap the voices + if (this.synth.totalVoicesAmount > this.synth.voiceCap) + { + this.synth.voiceKilling(voices.length); + } + channelVoices.push(...voices); + this.sendChannelProperty(); + this.synth.callEvent("noteon", { + midiNote: midiNote, + channel: this.channelNumber, + velocity: velocity + }); + +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/portamento_time.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/portamento_time.js new file mode 100644 index 0000000000000000000000000000000000000000..2b02abb42f06f6365b1dbf426ff3f9943d0335ad --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/portamento_time.js @@ -0,0 +1,92 @@ +// Tests were performed by John Novak +// https://github.com/dosbox-staging/dosbox-staging/pull/2705 + +/* +CC 5 value Portamento time +---------- --------------- + 0 0.000 s + 1 0.006 s + 2 0.023 s + 4 0.050 s + 8 0.110 s + 16 0.250 s + 32 0.500 s + 64 2.060 s + 80 4.200 s + 96 8.400 s + 112 19.500 s + 116 26.700 s + 120 40.000 s + 124 80.000 s + 127 480.000 s +*/ + +const portamentoLookup = { + 0: 0.000, + 1: 0.006, + 2: 0.023, + 4: 0.050, + 8: 0.110, + 16: 0.250, + 32: 0.500, + 64: 2.060, + 80: 4.200, + 96: 8.400, + 112: 19.500, + 116: 26.700, + 120: 40.000, + 124: 80.000, + 127: 480.000 +}; + +/** + * @param value {number} + * @returns {number} + */ +function getLookup(value) +{ + if (portamentoLookup[value] !== undefined) + { + return portamentoLookup[value]; + } + // get the nearest lower and upper points from the lookup table + let lower = null; + let upper = null; + + for (let key of Object.keys(portamentoLookup)) + { + key = parseInt(key); + if (key < value && (lower === null || key > lower)) + { + lower = key; + } + if (key > value && (upper === null || key < upper)) + { + upper = key; + } + } + + // if we have found both lower and upper points, perform linear interpolation + if (lower !== null && upper !== null) + { + let lowerTime = portamentoLookup[lower]; + let upperTime = portamentoLookup[upper]; + + // linear interpolation + return lowerTime + ((value - lower) * (upperTime - lowerTime)) / (upper - lower); + } + return 0; +} + + +/** + * Converts portamento time to seconds + * @param time {number} 0 - 127 + * @param distance {number} distance in keys + * @returns {number} seconds + */ +export function portamentoTimeToSeconds(time, distance) +{ + // this seems to work fine for the MIDIs I have + return getLookup(time) * (distance / 30); +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/program_change.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/program_change.js new file mode 100644 index 0000000000000000000000000000000000000000..22e8cfd4b50e6391e88821d2e9eaedf3b7e1da95 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/program_change.js @@ -0,0 +1,61 @@ +/** + * executes a program change + * @param programNumber {number} + * @this {WorkletProcessorChannel} + */ +export function programChange(programNumber) +{ + if (this.lockPreset) + { + return; + } + // always 128 for percussion + let bank = this.getBankSelect(); + let sentBank; + /** + * @type {BasicPreset} + */ + let preset; + + const isXG = this.isXGChannel; + // check if override + if (this.synth.overrideSoundfont) + { + const bankWithOffset = bank === 128 ? 128 : bank - this.synth.soundfontBankOffset; + const p = this.synth.overrideSoundfont.getPresetNoFallback( + bankWithOffset, + programNumber, + isXG + ); + if (p) + { + sentBank = p.bank === 128 ? 128 : p.bank + this.synth.soundfontBankOffset; + preset = p; + this.presetUsesOverride = true; + } + else + { + preset = this.synth.soundfontManager.getPreset(bank, programNumber, isXG); + const offset = this.synth.soundfontManager.soundfontList + .find(s => s.soundfont === preset.parentSoundBank).bankOffset; + sentBank = preset.bank - offset; + this.presetUsesOverride = false; + } + } + else + { + preset = this.synth.soundfontManager.getPreset(bank, programNumber, isXG); + const offset = this.synth.soundfontManager.soundfontList + .find(s => s.soundfont === preset.parentSoundBank).bankOffset; + sentBank = preset.bank - offset; + this.presetUsesOverride = false; + } + this.setPreset(preset); + this.sentBank = sentBank; + this.synth.callEvent("programchange", { + channel: this.channelNumber, + program: preset.program, + bank: sentBank + }); + this.sendChannelProperty(); +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/render_voice.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/render_voice.js new file mode 100644 index 0000000000000000000000000000000000000000..399bf73060ad535a4456bf9dce7c73a3b7ef6dc4 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/render_voice.js @@ -0,0 +1,197 @@ +import { WorkletVolumeEnvelope } from "../worklet_utilities/volume_envelope.js"; +import { WorkletModulationEnvelope } from "../worklet_utilities/modulation_envelope.js"; +import { generatorTypes } from "../../../soundfont/basic_soundfont/generator.js"; +import { customControllers } from "../worklet_utilities/controller_tables.js"; +import { absCentsToHz, timecentsToSeconds } from "../worklet_utilities/unit_converter.js"; +import { getLFOValue } from "../worklet_utilities/lfo.js"; +import { interpolationTypes, WavetableOscillator } from "../worklet_utilities/wavetable_oscillator.js"; +import { WorkletLowpassFilter } from "../worklet_utilities/lowpass_filter.js"; + +/** + * Renders a voice to the stereo output buffer + * @param voice {WorkletVoice} the voice to render + * @param outputLeft {Float32Array} the left output buffer + * @param outputRight {Float32Array} the right output buffer + * @param reverbOutputLeft {Float32Array} left output for reverb + * @param reverbOutputRight {Float32Array} right output for reverb + * @param chorusOutputLeft {Float32Array} left output for chorus + * @param chorusOutputRight {Float32Array} right output for chorus + * @this {WorkletProcessorChannel} + * @returns {boolean} true if the voice is finished + */ +export function renderVoice( + voice, + outputLeft, outputRight, + reverbOutputLeft, reverbOutputRight, + chorusOutputLeft, chorusOutputRight +) +{ + // avoid jetbrains errors + const timeNow = currentTime; + // check if release + if (!voice.isInRelease) + { + // if not in release, check if the release time is + if (timeNow >= voice.releaseStartTime) + { + // release the voice here + voice.isInRelease = true; + WorkletVolumeEnvelope.startRelease(voice); + WorkletModulationEnvelope.startRelease(voice); + if (voice.sample.loopingMode === 3) + { + voice.sample.isLooping = false; + } + } + } + + + // if the initial attenuation is more than 100dB, skip the voice (it's silent anyway) + if (voice.modulatedGenerators[generatorTypes.initialAttenuation] > 2500) + { + if (voice.isInRelease) + { + voice.finished = true; + } + return voice.finished; + } + + // TUNING + let targetKey = voice.targetKey; + + // calculate tuning + let cents = voice.modulatedGenerators[generatorTypes.fineTune] // soundfont fine tune + + this.channelOctaveTuning[voice.midiNote] // MTS octave tuning + + this.channelTuningCents; // channel tuning + let semitones = voice.modulatedGenerators[generatorTypes.coarseTune]; // soundfont coarse tuning + + // midi tuning standard + const tuning = this.synth.tunings[this.preset.program]?.[voice.realKey]; + if (tuning !== undefined && tuning?.midiNote >= 0) + { + // override key + targetKey = tuning.midiNote; + // add micro-tonal tuning + cents += tuning.centTuning; + } + + // portamento + if (voice.portamentoFromKey > -1) + { + // 0 to 1 + const elapsed = Math.min((timeNow - voice.startTime) / voice.portamentoDuration, 1); + const diff = targetKey - voice.portamentoFromKey; + // zero progress means the pitch being in fromKey, full progress means the normal pitch + semitones -= diff * (1 - elapsed); + } + + // 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) + { + // calculate start time and lfo value + const vibStart = voice.startTime + timecentsToSeconds(voice.modulatedGenerators[generatorTypes.delayVibLFO]); + const vibFreqHz = absCentsToHz(voice.modulatedGenerators[generatorTypes.freqVibLFO]); + const lfoVal = getLFOValue(vibStart, vibFreqHz, timeNow); + // use modulation multiplier (RPN modulation depth) + cents += lfoVal * (vibratoDepth * this.customControllers[customControllers.modulationMultiplier]); + } + + // low pass excursion with LFO and mod envelope + let lowpassExcursion = 0; + + // mod LFO + const modPitchDepth = voice.modulatedGenerators[generatorTypes.modLfoToPitch]; + const modVolDepth = voice.modulatedGenerators[generatorTypes.modLfoToVolume]; + const modFilterDepth = voice.modulatedGenerators[generatorTypes.modLfoToFilterFc]; + let modLfoCentibels = 0; + // don't compute mod lfo unless necessary + if (modPitchDepth !== 0 || modFilterDepth !== 0 || modVolDepth !== 0) + { + // calculate start time and lfo value + const modStart = voice.startTime + timecentsToSeconds(voice.modulatedGenerators[generatorTypes.delayModLFO]); + const modFreqHz = absCentsToHz(voice.modulatedGenerators[generatorTypes.freqModLFO]); + const modLfoValue = getLFOValue(modStart, modFreqHz, timeNow); + // use modulation multiplier (RPN modulation depth) + cents += modLfoValue * (modPitchDepth * this.customControllers[customControllers.modulationMultiplier]); + // vole nv volume offset + // negate the lfo value because audigy starts with increase rather than decrease + modLfoCentibels = -modLfoValue * modVolDepth; + // low pass frequency + lowpassExcursion += modLfoValue * modFilterDepth; + } + + // channel vibrato (GS NRPN) + if (this.channelVibrato.depth > 0) + { + // same as others + const channelVibrato = getLFOValue( + voice.startTime + this.channelVibrato.delay, + this.channelVibrato.rate, + timeNow + ); + if (channelVibrato) + { + cents += channelVibrato * this.channelVibrato.depth; + } + } + + // mod env + const modEnvPitchDepth = voice.modulatedGenerators[generatorTypes.modEnvToPitch]; + const modEnvFilterDepth = voice.modulatedGenerators[generatorTypes.modEnvToFilterFc]; + // don't compute mod env unless necessary + if (modEnvFilterDepth !== 0 || modEnvPitchDepth !== 0) + { + const modEnv = WorkletModulationEnvelope.getValue(voice, timeNow); + // apply values + lowpassExcursion += modEnv * modEnvFilterDepth; + cents += modEnv * modEnvPitchDepth; + } + + // finally, calculate the playback rate + const centsTotal = ~~(cents + semitones * 100); + if (centsTotal !== voice.currentTuningCents) + { + voice.currentTuningCents = centsTotal; + voice.currentTuningCalculated = Math.pow(2, centsTotal / 1200); + } + + + // SYNTHESIS + const bufferOut = new Float32Array(outputLeft.length); + + // wave table oscillator + switch (this.synth.interpolationType) + { + case interpolationTypes.fourthOrder: + WavetableOscillator.getSampleCubic(voice, bufferOut); + break; + + case interpolationTypes.linear: + default: + WavetableOscillator.getSampleLinear(voice, bufferOut); + break; + + case interpolationTypes.nearestNeighbor: + WavetableOscillator.getSampleNearest(voice, bufferOut); + break; + } + + // low pass filter + WorkletLowpassFilter.apply(voice, bufferOut, lowpassExcursion, this.synth.filterSmoothingFactor); + + // vol env + WorkletVolumeEnvelope.apply(voice, bufferOut, modLfoCentibels, this.synth.volumeEnvelopeSmoothingFactor); + + this.panVoice( + voice, + bufferOut, + outputLeft, outputRight, + reverbOutputLeft, reverbOutputRight, + chorusOutputLeft, chorusOutputRight + ); + return voice.finished; +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/soundfont_management/clear_sound_font.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/soundfont_management/clear_sound_font.js new file mode 100644 index 0000000000000000000000000000000000000000..fcc8db17e34d402d62539e601833a10eb64320d6 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/soundfont_management/clear_sound_font.js @@ -0,0 +1,30 @@ +/** + * @this {SpessaSynthProcessor} + * @param sendPresets {boolean} + * @param clearOverride {boolean} + */ +export function clearSoundFont(sendPresets = true, clearOverride = true) +{ + this.stopAllChannels(true); + if (clearOverride) + { + delete this.overrideSoundfont; + this.overrideSoundfont = undefined; + } + this.getDefaultPresets(); + this.cachedVoices = []; + + for (let i = 0; i < this.workletProcessorChannels.length; i++) + { + const channelObject = this.workletProcessorChannels[i]; + if (!clearOverride || (clearOverride && channelObject.presetUsesOverride)) + { + channelObject.setPresetLock(false); + } + channelObject.programChange(channelObject.preset.program); + } + if (sendPresets) + { + this.sendPresetList(); + } +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/soundfont_management/get_preset.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/soundfont_management/get_preset.js new file mode 100644 index 0000000000000000000000000000000000000000..04243f8ca87817e0aa00e100a825936bf570402a --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/soundfont_management/get_preset.js @@ -0,0 +1,22 @@ +import { isSystemXG } from "../../../../utils/xg_hacks.js"; + +/** + * @this {SpessaSynthProcessor} + * @param program {number} + * @param bank {number} + * @returns {BasicPreset} + */ +export function getPreset(bank, program) +{ + if (this.overrideSoundfont) + { + // if override soundfont + const bankWithOffset = bank === 128 ? 128 : bank - this.soundfontBankOffset; + const preset = this.overrideSoundfont.getPresetNoFallback(bankWithOffset, program, isSystemXG(this.system)); + if (preset) + { + return preset; + } + } + return this.soundfontManager.getPreset(bank, program, isSystemXG(this.system)); +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/soundfont_management/reload_sound_font.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/soundfont_management/reload_sound_font.js new file mode 100644 index 0000000000000000000000000000000000000000..ce6ffd2fc1d3c09e879e991483c351a086f35ed1 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/soundfont_management/reload_sound_font.js @@ -0,0 +1,40 @@ +import { loadSoundFont } from "../../../../soundfont/load_soundfont.js"; +import { returnMessageType } from "../../message_protocol/worklet_message.js"; +import { SpessaSynthInfo } from "../../../../utils/loggin.js"; +import { consoleColors } from "../../../../utils/other.js"; + +/** + * @param buffer {ArrayBuffer} + * @param isOverride {Boolean} + * @this {SpessaSynthProcessor} + */ +export function reloadSoundFont(buffer, isOverride = false) +{ + this.clearSoundFont(false, isOverride); + try + { + if (isOverride) + { + this.overrideSoundfont = loadSoundFont(buffer); + } + else + { + this.soundfontManager.reloadManager(buffer); + } + } + catch (e) + { + this.post({ + messageType: returnMessageType.soundfontError, + messageData: e + }); + return; + } + this.getDefaultPresets(); + this.workletProcessorChannels.forEach(c => + c.programChange(c.preset.program) + ); + this.postReady(); + this.sendPresetList(); + SpessaSynthInfo("%cSpessaSynth is ready!", consoleColors.recognized); +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/soundfont_management/send_preset_list.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/soundfont_management/send_preset_list.js new file mode 100644 index 0000000000000000000000000000000000000000..9afd53d7cfab62795eaed7444646c7de94dbdffc --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/soundfont_management/send_preset_list.js @@ -0,0 +1,34 @@ +/** + * @this {SpessaSynthProcessor} + */ +export function sendPresetList() +{ + /** + * @type {{bank: number, presetName: string, program: number}[]} + */ + const mainFont = this.soundfontManager.getPresetList(); + if (this.overrideSoundfont !== undefined) + { + 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) + { + exists.presetName = p.presetName; + } + else + { + mainFont.push({ + presetName: p.presetName, + bank: bankCheck, + program: p.program + }); + } + }); + } + this.processorInitialized.then(() => + { + this.callEvent("presetlistchange", mainFont); + }); +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/soundfont_management/set_embedded_sound_font.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/soundfont_management/set_embedded_sound_font.js new file mode 100644 index 0000000000000000000000000000000000000000..8e5d1fc0c9a4453f71d67ee4a519f1fad66d6b58 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/soundfont_management/set_embedded_sound_font.js @@ -0,0 +1,21 @@ +/** + * Sets the embedded (RMI soundfont) + * @param font {ArrayBuffer} + * @param offset {number} + * @this {SpessaSynthProcessor} + */ +export function setEmbeddedSoundFont(font, offset) +{ + // set offset + this.soundfontBankOffset = offset; + this.reloadSoundFont(font, true); + // preload all samples + this.overrideSoundfont.samples.forEach(s => s.getAudioData()); + + // apply snapshot again if applicable + if (this._snapshot !== undefined) + { + this.applySynthesizerSnapshot(this._snapshot); + this.resetAllControllers(); + } +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/stopping_notes/kill_note.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/stopping_notes/kill_note.js new file mode 100644 index 0000000000000000000000000000000000000000..021328f55e3089a2ed917b845052095dfa55ec4b --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/stopping_notes/kill_note.js @@ -0,0 +1,20 @@ +import { generatorTypes } from "../../../../soundfont/basic_soundfont/generator.js"; + +/** + * Stops a note nearly instantly + * @param midiNote {number} + * @param releaseTime {number} ticks + * @this {WorkletProcessorChannel} + */ +export function killNote(midiNote, releaseTime = -12000) +{ + this.voices.forEach(v => + { + if (v.realKey !== midiNote) + { + return; + } + v.modulatedGenerators[generatorTypes.releaseVolEnv] = releaseTime; // set release to be very short + v.release(); + }); +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/stopping_notes/note_off.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/stopping_notes/note_off.js new file mode 100644 index 0000000000000000000000000000000000000000..9d6e5425359f88119097f3ba96090b14721c2100 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/stopping_notes/note_off.js @@ -0,0 +1,55 @@ +import { SpessaSynthWarn } from "../../../../utils/loggin.js"; + +/** + * Release a note + * @param midiNote {number} + * @this {WorkletProcessorChannel} + */ +export function noteOff(midiNote) +{ + if (midiNote > 127 || midiNote < 0) + { + SpessaSynthWarn(`Received a noteOn for note`, midiNote, "Ignoring."); + return; + } + + let realKey = midiNote + this.channelTransposeKeyShift; + + // if high performance mode, kill notes instead of stopping them + if (this.synth.highPerformanceMode) + { + // if the channel is percussion channel, do not kill the notes + if (!this.drumChannel) + { + this.killNote(realKey, -6950); + this.synth.callEvent("noteoff", { + midiNote: midiNote, + channel: this.channelNumber + }); + return; + } + } + + const channelVoices = this.voices; + channelVoices.forEach(v => + { + if (v.realKey !== realKey || v.isInRelease === true) + { + return; + } + // if hold pedal, move to sustain + if (this.holdPedal) + { + this.sustainedVoices.push(v); + } + else + { + v.release(); + } + }); + this.synth.callEvent("noteoff", { + midiNote: midiNote, + channel: this.channelNumber + }); +} + diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/stopping_notes/stop_all_channels.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/stopping_notes/stop_all_channels.js new file mode 100644 index 0000000000000000000000000000000000000000..bdb14b256859d5ef2ba792be3b415233f3e9b814 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/stopping_notes/stop_all_channels.js @@ -0,0 +1,16 @@ +import { SpessaSynthInfo } from "../../../../utils/loggin.js"; +import { consoleColors } from "../../../../utils/other.js"; + +/** + * @this {SpessaSynthProcessor} + * @param force {boolean} + */ +export function stopAllChannels(force = false) +{ + SpessaSynthInfo("%cStop all received!", consoleColors.info); + for (let i = 0; i < this.workletProcessorChannels.length; i++) + { + this.workletProcessorChannels[i].stopAllNotes(force); + } + this.callEvent("stopall", undefined); +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/stopping_notes/stop_all_notes.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/stopping_notes/stop_all_notes.js new file mode 100644 index 0000000000000000000000000000000000000000..5cbb81e3654c424b46210fb3a789a04f82fb376e --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/stopping_notes/stop_all_notes.js @@ -0,0 +1,30 @@ +/** + * stops all notes on a given channel + * @param force {boolean} + * @this {WorkletProcessorChannel} + */ +export function stopAllNotes(force = false) +{ + if (force) + { + // force stop all + this.voices.length = 0; + this.sustainedVoices.length = 0; + this.sendChannelProperty(); + } + else + { + this.voices.forEach(v => + { + if (v.isInRelease) + { + return; + } + v.release(); + }); + this.sustainedVoices.forEach(v => + { + v.release(); + }); + } +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/stopping_notes/voice_killing.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/stopping_notes/voice_killing.js new file mode 100644 index 0000000000000000000000000000000000000000..6372d34210af2717ed0100179ad8cf3303e3a656 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/stopping_notes/voice_killing.js @@ -0,0 +1,63 @@ +/** + * @param channel {WorkletProcessorChannel} + * @param voice {WorkletVoice} + * @return {number} + */ +function getPriority(channel, voice) +{ + let priority = 0; + if (channel.drumChannel) + { + // important + priority += 5; + } + if (voice.isInRelease) + { + // not important + priority -= 5; + } + // less velocity = less important + priority += voice.velocity / 25; // map to 0-5 + // the newer, more important + priority -= voice.volumeEnvelope.state; + if (voice.isInRelease) + { + priority -= 5; + } + priority -= voice.volumeEnvelope.currentAttenuationDb / 50; + return priority; +} + +/** + * @this {SpessaSynthProcessor} + * @param amount {number} + */ +export function voiceKilling(amount) +{ + let allVoices = []; + for (const channel of this.workletProcessorChannels) + { + for (const voice of channel.voices) + { + if (!voice.finished) + { + const priority = getPriority(channel, voice); + allVoices.push({ channel, voice, priority }); + } + } + } + + // 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); + if (index > -1) + { + channel.voices.splice(index, 1); + } + } +} + diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/system_exclusive.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/system_exclusive.js new file mode 100644 index 0000000000000000000000000000000000000000..bb72f11e63fe4e8898038bddddcbf73523602f4e --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/system_exclusive.js @@ -0,0 +1,743 @@ +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 { isSystemXG } from "../../../utils/xg_hacks.js"; + +/** + * KeyNum: tuning + * @typedef {MTSNoteTuning[]} MTSProgramTuning + */ + +/** + * @typedef {Object} MTSNoteTuning + * @property {number} midiNote - the base midi note to use, -1 means no change + * @property {number} centTuning - additional tuning + */ + +/** + * Calculates freqency for MIDI Tuning Standard + * @param byte1 {number} + * @param byte2 {number} + * @param byte3 {number} + * @return {{midiNote: number, centTuning: number|null}} + */ +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 }; +} + +/** + * The text types for the synth display + * @enum {number} + */ +export const SynthDisplayType = { + SoundCanvasText: 0, + XGText: 1, + SoundCanvasDotDisplay: 2 +}; + + +/** + * Executes a system exclusive + * @param messageData {number[]|IndexedByteArray} - the message data without f0 + * @param channelOffset {number} + * @this {SpessaSynthProcessor} + */ +export function systemExclusive(messageData, channelOffset = 0) +{ + const type = messageData[0]; + if (this.deviceID !== ALL_CHANNELS_OR_DIFFERENT_ACTION && messageData[1] !== 0x7F) + { + if (this.deviceID !== messageData[1]) + { + // not our device ID + return; + } + } + switch (type) + { + default: + SpessaSynthWarn( + `%cUnrecognized SysEx: %c${arrayToHexString(messageData)}`, + consoleColors.warn, + consoleColors.unrecognized + ); + break; + + // non realtime + case 0x7E: + case 0x7F: + switch (messageData[2]) + { + case 0x04: + let cents; + // device control + switch (messageData[3]) + { + case 0x01: + // main volume + const vol = messageData[5] << 7 | messageData[4]; + this.setMIDIVolume(vol / 16384); + SpessaSynthInfo( + `%cMaster Volume. Volume: %c${vol}`, + consoleColors.info, + 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}`, + consoleColors.info, + 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}`, + consoleColors.info, + 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}`, + consoleColors.info, + consoleColors.value + ); + break; + + default: + SpessaSynthWarn( + `%cUnrecognized MIDI Device Control Real-time message: %c${arrayToHexString(messageData)}`, + consoleColors.warn, + consoleColors.unrecognized + ); + } + break; + + case 0x09: + // gm system related + if (messageData[3] === 0x01) + { + SpessaSynthInfo("%cGM1 system on", consoleColors.info); + this.setSystem("gm"); + } + else if (messageData[3] === 0x03) + { + SpessaSynthInfo("%cGM2 system on", consoleColors.info); + this.setSystem("gm2"); + } + else + { + SpessaSynthInfo("%cGM system off, defaulting to GS", consoleColors.info); + this.setSystem("gs"); + } + break; + + // MIDI Tuning standard + // https://midi.org/midi-tuning-updated-specification + case 0x08: + switch (messageData[3]) + { + // single note change + // single note change bank + case 0x02: + case 0x07: + let currentMessageIndex = 4; + if (messageData[3] === 0x07) + { + // skip the bank + currentMessageIndex++; + } + // get program and number of changes + const tuningProgram = messageData[currentMessageIndex++]; + const numberOfChanges = messageData[currentMessageIndex++]; + for (let i = 0; i < numberOfChanges; i++) + { + // set the given tuning to the program + this.tunings[tuningProgram][messageData[currentMessageIndex++]] = getTuning( + messageData[currentMessageIndex++], + messageData[currentMessageIndex++], + messageData[currentMessageIndex++] + ); + } + SpessaSynthInfo( + `%cSingle Note Tuning. Program: %c${tuningProgram}%c Keys affected: %c${numberOfChanges}`, + consoleColors.info, + consoleColors.recognized, + consoleColors.info, + consoleColors.recognized + ); + break; + + // octave tuning (1 byte) + // and octave tuning (2 bytes) + case 0x09: + case 0x08: + // get tuning: + const newOctaveTuning = new Int8Array(12); + // start from bit 7 + if (messageData[3] === 0x08) + { + // 1 byte tuning: 0 is -64 cents, 64 is 0, 127 is +63 + for (let i = 0; i < 12; i++) + { + newOctaveTuning[i] = messageData[7 + i] - 64; + } + } + else + { + // 2 byte tuning. Like fine tune: 0 is -100 cents, 8192 is 0 cents, 16,383 is +100 cents + for (let i = 0; i < 24; i += 2) + { + const tuning = ((messageData[7 + i] << 7) | messageData[8 + i]) - 8192; + newOctaveTuning[i / 2] = Math.floor(tuning / 81.92); // map to [-100;+99] cents + } + } + // apply to channels (ordered from 0) + // bit 1: 14 and 15 + if ((messageData[4] & 1) === 1) + { + this.workletProcessorChannels[14 + channelOffset].setOctaveTuning(newOctaveTuning); + } + if (((messageData[4] >> 1) & 1) === 1) + { + this.workletProcessorChannels[15 + channelOffset].setOctaveTuning(newOctaveTuning); + } + + // bit 2: channels 7 to 13 + for (let i = 0; i < 7; i++) + { + const bit = (messageData[5] >> i) & 1; + if (bit === 1) + { + this.workletProcessorChannels[7 + i + channelOffset].setOctaveTuning(newOctaveTuning); + } + } + + // bit 3: channels 0 to 16 + for (let i = 0; i < 7; i++) + { + const bit = (messageData[6] >> i) & 1; + if (bit === 1) + { + this.workletProcessorChannels[i + channelOffset].setOctaveTuning(newOctaveTuning); + } + } + + SpessaSynthInfo( + `%cMIDI Octave Scale ${ + messageData[3] === 0x08 ? "(1 byte)" : "(2 bytes)" + } tuning via Tuning: %c${newOctaveTuning.join(" ")}`, + consoleColors.info, + consoleColors.value + ); + break; + + default: + SpessaSynthWarn( + `%cUnrecognized MIDI Tuning standard message: %c${arrayToHexString(messageData)}`, + consoleColors.warn, + consoleColors.unrecognized + ); + break; + } + break; + + default: + SpessaSynthWarn( + `%cUnrecognized MIDI Realtime/non realtime message: %c${arrayToHexString(messageData)}`, + consoleColors.warn, + 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) + { + // 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) + { + // GS mode set + if (messageValue === 0x00) + { + // this is a GS reset + SpessaSynthInfo("%cGS Reset received!", consoleColors.info); + this.resetAllControllers(false); + this.setSystem("gs"); + } + else if (messageValue === 0x7F) + { + // GS mode off + SpessaSynthInfo("%cGS system off, switching to GM2", consoleColors.info); + this.resetAllControllers(false); + this.setSystem("gm2"); + } + return; + } + else if (messageData[4] === 0x40) + { + // this is a system parameter + if ((messageData[5] & 0x10) > 0) + { + // this is an individual part (channel) parameter + // determine the channel 0 means channel 10 (default), 1 means 1 etc. + const channel = [9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15][messageData[5] & 0x0F] + channelOffset; + // for example, 0x1A means A = 11, which corresponds to channel 12 (counting from 1) + const channelObject = this.workletProcessorChannels[channel]; + switch (messageData[6]) + { + default: + // 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 + channelObject.setDrums(isDrums); + SpessaSynthInfo( + `%cChannel %c${channel}%c ${isDrums ? + "is now a drum channel" + : + "now isn't a drum channel" + }%c via: %c${arrayToHexString(messageData)}`, + consoleColors.info, + consoleColors.value, + consoleColors.recognized, + consoleColors.info, + consoleColors.value + ); + return; + + case 0x16: + // this is the pitch key shift sysex + const keyShift = messageValue - 64; + channelObject.transposeChannel(keyShift); + 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 + ); + return; + + // pan position + case 0x1C: + // 0 is random + let panpot = messageValue; + if (panpot === 0) + { + channelObject.randomPan = true; + SpessaSynthInfo( + `%cRandom pan is set to %cON%c for %c${channel}`, + consoleColors.info, + consoleColors.recognized, + consoleColors.info, + consoleColors.value + ); + } + else + { + channelObject.randomPan = false; + channelObject.controllerChange(midiControllers.pan, panpot); + } + break; + + // chorus send + case 0x21: + channelObject.controllerChange(midiControllers.chorusDepth, messageValue); + break; + + // reverb send + case 0x22: + channelObject.controllerChange(midiControllers.reverbDepth, messageValue); + break; + + case 0x40: + case 0x41: + case 0x42: + case 0x43: + case 0x44: + case 0x45: + case 0x46: + case 0x47: + case 0x48: + case 0x49: + case 0x4A: + case 0x4B: + // scale tuning: up to 12 bytes + const tuningBytes = messageData.length - 9; // data starts at 7, minus checksum and f7 + // read em bytes + const newTuning = new Int8Array(12); + for (let i = 0; i < tuningBytes; i++) + { + newTuning[i] = messageData[i + 7] - 64; + } + channelObject.setOctaveTuning(newTuning); + const cents = messageValue - 64; + 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 + ); + channelObject.setTuning(cents); + break; + } + return; + } + else + // 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)}`, + consoleColors.info, + consoleColors.value, + consoleColors.info, + consoleColors.value + ); + this.setMasterPan((messageValue - 64) / 64); + return; + } + 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)}`, + consoleColors.info, + consoleColors.value, + consoleColors.info, + consoleColors.value + ); + this.setMasterTuning(transpose * 100); + return; + } + 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)}`, + consoleColors.info, + consoleColors.value, + consoleColors.info, + consoleColors.value + ); + this.setMIDIVolume(messageValue / 127); + return; + } + } + // this is some other GS sysex... + notRecognized(); + return; + } + else if (messageData[2] === 0x45 && messageData[3] === 0x12) + { + // 0x45: GS Display Data, 0x12: DT1 (Device Transmit) + // check for embedded copyright + // (roland SC display sysex) http://www.bandtrax.com.au/sysex.htm + + if ( + messageData[4] === 0x10 && // Sound Canvas Display + messageData[6] === 0x00 // Data follows + ) + { + if (messageData[5] === 0x00) // Display letters + { + // get the text + // and header ends with (checksum) F7 + const text = new Uint8Array(messageData.slice(7, messageData.length - 2)); + this.callEvent( + "synthdisplay", + { + displayData: text, + displayType: SynthDisplayType.SoundCanvasText + } + ); + } + else if (messageData[5] === 0x01) // Matrix display + { + // get the data + // and header ends with (checksum) F7 + const dotMatrixData = new Uint8Array(messageData.slice(7, messageData.length - 3)); + this.callEvent( + "synthdisplay", + { + displayData: dotMatrixData, + displayType: SynthDisplayType.SoundCanvasDotDisplay + } + ); + SpessaSynthInfo( + `%cRoland SC Display Dot Matrix via: %c${arrayToHexString( + messageData)}`, + consoleColors.info, + consoleColors.value + ); + } + else + { + // this is some other GS sysex... + notRecognized(); + } + } + } + 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)}`, + consoleColors.info, + consoleColors.value, + consoleColors.info, + consoleColors.value + ); + return; + } + else + { + // this is something else... + SpessaSynthWarn( + `%cUnrecognized Roland SysEx: %c${arrayToHexString(messageData)}`, + consoleColors.warn, + consoleColors.unrecognized + ); + return; + } + break; + + // yamaha + // http://www.studio4all.de/htmle/main91.html + case 0x43: + // XG sysex + if (messageData[2] === 0x4C) + { + // XG system parameter + if (messageData[3] === 0x00 && messageData[4] === 0x00) + { + switch (messageData[5]) + { + // master volume + case 0x04: + const vol = messageData[6]; + this.setMIDIVolume(vol / 127); + SpessaSynthInfo( + `%cXG master volume. Volume: %c${vol}`, + consoleColors.info, + consoleColors.recognized + ); + break; + + // master transpose + case 0x06: + const transpose = messageData[6] - 64; + this.transposeAllChannels(transpose); + SpessaSynthInfo( + `%cXG master transpose. Volume: %c${transpose}`, + consoleColors.info, + consoleColors.recognized + ); + break; + + // XG on + case 0x7E: + SpessaSynthInfo("%cXG system on", consoleColors.info); + this.resetAllControllers(false); + this.setSystem("xg"); + break; + } + } + else + // XG part parameter + if (messageData[3] === 0x08) + { + if (!isSystemXG(this.system)) + { + return; + } + const channel = messageData[4] + channelOffset; + if (channel >= this.workletProcessorChannels.length) + { + // invalid channel + return; + } + const channelObject = this.workletProcessorChannels[channel]; + const value = messageData[6]; + switch (messageData[5]) + { + // bank-select MSB + case 0x01: + channelObject.controllerChange(midiControllers.bankSelect, value); + break; + + // bank-select LSB + case 0x02: + channelObject.controllerChange(midiControllers.lsbForControl0BankSelect, value); + break; + + // program change + case 0x03: + channelObject.programChange(value); + break; + + // note shift + case 0x08: + if (channelObject.drumChannel) + { + return; + } + const semitones = value - 64; + channelObject.channelTransposeKeyShift = semitones; + break; + + // volume + case 0x0B: + channelObject.controllerChange(midiControllers.mainVolume, value); + break; + + // pan position + case 0x0E: + let pan = value; + if (pan === 0) + { + // 0 means random + channelObject.randomPan = true; + SpessaSynthInfo( + `%cRandom pan is set to %cON%c for %c${channel}`, + consoleColors.info, + consoleColors.recognized, + consoleColors.info, + consoleColors.value + ); + } + else + { + channelObject.controllerChange(midiControllers.pan, pan); + } + break; + + // reverb + case 0x13: + channelObject.controllerChange(midiControllers.reverbDepth, value); + break; + + // chorus + case 0x12: + channelObject.controllerChange(midiControllers.chorusDepth, value); + break; + + default: + SpessaSynthWarn( + `%cUnrecognized Yamaha XG Part Setup: %c${messageData[5].toString(16) + .toUpperCase()}`, + consoleColors.warn, + consoleColors.unrecognized + ); + } + } + else if ( + messageData[3] === 0x06 && // XG System parameter + messageData[4] === 0x00 // System Byte + ) + { + // displayed letters (remove F7 at the end) + // include byte 5 as it seems to be line information (useful) + const textData = new Uint8Array(messageData.slice(5, messageData.length - 1)); + this.callEvent( + "synthdisplay", + { + displayData: textData, + displayType: SynthDisplayType.XGText + } + ); + } + else if (isSystemXG(this.system)) + { + SpessaSynthWarn( + `%cUnrecognized Yamaha XG SysEx: %c${arrayToHexString(messageData)}`, + consoleColors.warn, + consoleColors.unrecognized + ); + } + + } + else + { + if (isSystemXG(this.system)) + { + SpessaSynthWarn( + `%cUnrecognized Yamaha SysEx: %c${arrayToHexString(messageData)}`, + consoleColors.warn, + consoleColors.unrecognized + ); + } + } + break; + + + } +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/channel_pressure.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/channel_pressure.js new file mode 100644 index 0000000000000000000000000000000000000000..b97790e6eaeb1cd5701cb18e378db9342210d1a7 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/channel_pressure.js @@ -0,0 +1,24 @@ +import { NON_CC_INDEX_OFFSET } from "../../worklet_utilities/controller_tables.js"; +import { modulatorSources } from "../../../../soundfont/basic_soundfont/modulator.js"; +import { computeModulators } from "../../worklet_utilities/worklet_modulator.js"; + +/** + * Sets the pressure of the given channel + * @this {WorkletProcessorChannel} + * @param pressure {number} the pressure of the channel + */ +export function channelPressure(pressure) +{ + this.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.channelPressure] = pressure << 7; + this.voices.forEach(v => + computeModulators( + v, + this.midiControllers, + 0, + modulatorSources.channelPressure + )); + this.synth.callEvent("channelpressure", { + channel: this.channelNumber, + pressure: pressure + }); +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/pitch_wheel.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/pitch_wheel.js new file mode 100644 index 0000000000000000000000000000000000000000..1a645fb3adf5d7ab534ebaa6449f52cc20af6815 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/pitch_wheel.js @@ -0,0 +1,33 @@ +import { NON_CC_INDEX_OFFSET } from "../../worklet_utilities/controller_tables.js"; +import { modulatorSources } from "../../../../soundfont/basic_soundfont/modulator.js"; +import { computeModulators } from "../../worklet_utilities/worklet_modulator.js"; + +/** + * Sets the pitch of the given channel + * @this {WorkletProcessorChannel} + * @param MSB {number} SECOND byte of the MIDI pitchWheel message + * @param LSB {number} FIRST byte of the MIDI pitchWheel message + */ +export function pitchWheel(MSB, LSB) +{ + if (this.lockedControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel]) + { + return; + } + const bend = (LSB | (MSB << 7)); + this.synth.callEvent("pitchwheel", { + channel: this.channelNumber, + MSB: MSB, + LSB: LSB + }); + this.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel] = bend; + this.voices.forEach(v => + // compute pitch modulators + computeModulators( + v, + this.midiControllers, + 0, + modulatorSources.pitchWheel + )); + this.sendChannelProperty(); +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/poly_pressure.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/poly_pressure.js new file mode 100644 index 0000000000000000000000000000000000000000..3e97a287494cfb9ed038e8fddce057300dcdd1d8 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/poly_pressure.js @@ -0,0 +1,31 @@ +import { computeModulators } from "../../worklet_utilities/worklet_modulator.js"; +import { modulatorSources } from "../../../../soundfont/basic_soundfont/modulator.js"; + +/** + * Sets the pressure of the given note on a specific channel + * @this {WorkletProcessorChannel} + * @param midiNote {number} 0-127 + * @param pressure {number} the pressure of the note + */ +export function polyPressure(midiNote, pressure) +{ + this.voices.forEach(v => + { + if (v.midiNote !== midiNote) + { + return; + } + v.pressure = pressure; + computeModulators( + v, + this.midiControllers, + 0, + modulatorSources.polyPressure + ); + }); + this.synth.callEvent("polypressure", { + channel: this.channelNumber, + midiNote: midiNote, + pressure: pressure + }); +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/set_master_tuning.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/set_master_tuning.js new file mode 100644 index 0000000000000000000000000000000000000000..bccd08babd38793a65bdb9d2b37d91b5365d9f2a --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/set_master_tuning.js @@ -0,0 +1,15 @@ +import { customControllers } from "../../worklet_utilities/controller_tables.js"; + +/** + * Sets the worklet's primary tuning + * @this {SpessaSynthProcessor} + * @param cents {number} + */ +export function setMasterTuning(cents) +{ + cents = Math.round(cents); + for (let i = 0; i < this.workletProcessorChannels.length; i++) + { + this.workletProcessorChannels[i].setCustomController(customControllers.masterTuning, cents); + } +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/set_modulation_depth.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/set_modulation_depth.js new file mode 100644 index 0000000000000000000000000000000000000000..60d3f8481df5d6bbe3966a7ee28a4e65a49664e2 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/set_modulation_depth.js @@ -0,0 +1,27 @@ +import { SpessaSynthInfo } from "../../../../utils/loggin.js"; +import { consoleColors } from "../../../../utils/other.js"; +import { customControllers } from "../../worklet_utilities/controller_tables.js"; + +/** + * @this {WorkletProcessorChannel} + * @param cents {number} + */ +export function setModulationDepth(cents) +{ + cents = Math.round(cents); + SpessaSynthInfo( + `%cChannel ${this.channelNumber} modulation depth. Cents: %c${cents}`, + consoleColors.info, + consoleColors.value + ); + /* ============== + IMPORTANT + here we convert cents into a multiplier. + midi spec assumes the default is 50 cents, + but it might be different for the soundfont, + so we create a multiplier by dividing cents by 50. + for example, if we want 100 cents, then multiplier will be 2, + which for a preset with depth of 50 will create 100. + ================ */ + this.setCustomController(customControllers.modulationMultiplier, cents / 50); +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/set_octave_tuning.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/set_octave_tuning.js new file mode 100644 index 0000000000000000000000000000000000000000..610e40e7a7a0267146176b3478d525a73723a9b7 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/set_octave_tuning.js @@ -0,0 +1,19 @@ +/** + * Sets the octave tuning for a given channel + * @this {WorkletProcessorChannel} + * @param tuning {Int8Array} LENGTH of 12! + * relative cent tuning. + * min -128 max 127. + */ +export function setOctaveTuning(tuning) +{ + if (tuning.length !== 12) + { + throw new Error("Tuning is not the length of 12."); + } + this.channelOctaveTuning = new Int8Array(128); + for (let i = 0; i < 128; i++) + { + this.channelOctaveTuning[i] = tuning[i % 12]; + } +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/set_tuning.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/set_tuning.js new file mode 100644 index 0000000000000000000000000000000000000000..ee3deb8574c2836703bbc658d214b173af7e4eb3 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/set_tuning.js @@ -0,0 +1,27 @@ +import { customControllers } from "../../worklet_utilities/controller_tables.js"; +import { SpessaSynthInfo } from "../../../../utils/loggin.js"; +import { consoleColors } from "../../../../utils/other.js"; + +/** + * Sets the channel's tuning + * @this {WorkletProcessorChannel} + * @param cents {number} + * @param log {boolean} + */ +export function setTuning(cents, log = true) +{ + cents = Math.round(cents); + this.setCustomController(customControllers.channelTuning, cents); + if (!log) + { + return; + } + SpessaSynthInfo( + `%cFine tuning for %c${this.channelNumber}%c is now set to %c${cents}%c cents.`, + consoleColors.info, + consoleColors.recognized, + consoleColors.info, + consoleColors.value, + consoleColors.info + ); +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/transpose_all_channels.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/transpose_all_channels.js new file mode 100644 index 0000000000000000000000000000000000000000..161a57bc605dbe26cc9f20cc1977507b66ddc906 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/transpose_all_channels.js @@ -0,0 +1,15 @@ +/** + * Transposes all channels by given amount of semitones + * @this {SpessaSynthProcessor} + * @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 transposeAllChannels(semitones, force = false) +{ + this.transposition = 0; + for (let i = 0; i < this.workletProcessorChannels.length; i++) + { + this.workletProcessorChannels[i].transposeChannel(semitones, force); + } + this.transposition = semitones; +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/transpose_channel.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/transpose_channel.js new file mode 100644 index 0000000000000000000000000000000000000000..6cf698b700d72c72e63e17ea4de592a02f96da1d --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/tuning_control/transpose_channel.js @@ -0,0 +1,34 @@ +import { customControllers } from "../../worklet_utilities/controller_tables.js"; +import { midiControllers } from "../../../../midi_parser/midi_message.js"; + +/** + * Transposes the channel by given amount of semitones + * @this {WorkletProcessorChannel} + * @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(semitones, force = false) +{ + if (!this.drumChannel) + { + semitones += this.synth.transposition; + } + const keyShift = Math.trunc(semitones); + const currentTranspose = this.channelTransposeKeyShift + this.customControllers[customControllers.channelTransposeFine] / 100; + if ( + (this.drumChannel && !force) + || semitones === currentTranspose + ) + { + return; + } + if (keyShift !== this.channelTransposeKeyShift) + { + // stop all (and emit cc change) + this.controllerChange(midiControllers.allNotesOff, 127); + } + // apply transpose + this.channelTransposeKeyShift = keyShift; + this.setCustomController(customControllers.channelTransposeFine, (semitones - keyShift) * 100); + this.sendChannelProperty(); +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/worklet_key_modifier.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/worklet_key_modifier.js new file mode 100644 index 0000000000000000000000000000000000000000..6023c50bb545829a6bef772ed6c367a1701486c1 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/worklet_key_modifier.js @@ -0,0 +1,157 @@ +export class KeyModifier +{ + + /** + * The new override velocity. -1 means unchanged + * @type {number} + */ + velocity = -1; + /** + * The patch this key uses. -1 on either means default + * @type {{bank: number, program: number}} + */ + patch = { bank: -1, program: -1 }; + + /** + * @param velocity {number} + * @param bank {number} + * @param program {number} + */ + constructor(velocity = -1, bank = -1, program = -1) + { + this.velocity = velocity; + this.patch = { + bank: bank, + program: program + }; + } +} + +/** + * @enum {number} + */ +export const workletKeyModifierMessageType = { + addMapping: 0, // [channel, midiNote, mapping] + deleteMapping: 1, // [channel, midiNote] + clearMappings: 2 // +}; + +export class WorkletKeyModifierManager +{ + /** + * The velocity override mappings for MIDI keys + * @type {KeyModifier[][]} + * @private + */ + _keyMappings = []; + + /** + * @param type {workletKeyModifierMessageType} + * @param data {any} + */ + handleMessage(type, data) + { + switch (type) + { + default: + return; + + case workletKeyModifierMessageType.addMapping: + this.addMapping(...data); + break; + + case workletKeyModifierMessageType.clearMappings: + this.clearMappings(); + break; + + case workletKeyModifierMessageType.deleteMapping: + this.deleteMapping(...data); + } + } + + /** + * @param channel {number} + * @param midiNote {number} + * @param mapping {KeyModifier} + */ + addMapping(channel, midiNote, mapping) + { + if (this._keyMappings[channel] === undefined) + { + this._keyMappings[channel] = []; + } + this._keyMappings[channel][midiNote] = mapping; + } + + deleteMapping(channel, midiNote) + { + if (this._keyMappings[channel]?.[midiNote] === undefined) + { + return; + } + this._keyMappings[channel][midiNote] = undefined; + } + + clearMappings() + { + this._keyMappings = []; + } + + /** + * @param mappings {KeyModifier[][]} + */ + setMappings(mappings) + { + this._keyMappings = mappings; + } + + /** + * @returns {KeyModifier[][]} + */ + getMappings() + { + return this._keyMappings; + } + + /** + * @param channel {number} + * @param midiNote {number} + * @returns {number} velocity, -1 if unchanged + */ + getVelocity(channel, midiNote) + { + const modifier = this._keyMappings[channel]?.[midiNote]; + if (modifier) + { + return modifier.velocity; + } + return -1; + } + + /** + * @param channel {number} + * @param midiNote {number} + * @returns {boolean} + */ + hasOverridePatch(channel, midiNote) + { + const bank = this._keyMappings[channel]?.[midiNote]?.patch?.bank; + return bank !== undefined && bank >= 0; + } + + /** + * @param channel {number} + * @param midiNote {number} + * @returns {{bank: number, program: number}} -1 if unchanged + */ + getPatch(channel, midiNote) + { + const modifier = this._keyMappings[channel]?.[midiNote]; + if (modifier) + { + return modifier.patch; + } + throw new Error("No modifier."); + } + +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/worklet_soundfont_manager/sfman_message.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/worklet_soundfont_manager/sfman_message.js new file mode 100644 index 0000000000000000000000000000000000000000..182190580a19387cdc094604d7eada41a9dafd54 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/worklet_soundfont_manager/sfman_message.js @@ -0,0 +1,9 @@ +/** + * @enum {number} + */ +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 diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_methods/worklet_soundfont_manager/worklet_soundfont_manager.js b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/worklet_soundfont_manager/worklet_soundfont_manager.js new file mode 100644 index 0000000000000000000000000000000000000000..e7ea93b9ca4abf61e09b9f2256bb0cc368eaf89f --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_methods/worklet_soundfont_manager/worklet_soundfont_manager.js @@ -0,0 +1,254 @@ +import { SpessaSynthWarn } from "../../../../utils/loggin.js"; +import { WorkletSoundfontManagerMessageType } from "./sfman_message.js"; +import { loadSoundFont } from "../../../../soundfont/load_soundfont.js"; +import { isXGDrums } from "../../../../utils/xg_hacks.js"; + +/** + * @typedef {Object} SoundFontType + * @property {string} id - unique id for the soundfont + * @property {BasicSoundBank} soundfont - the soundfont itself + * @property {number} bankOffset - the soundfont's bank offset + */ + +export class WorkletSoundfontManager +{ + /** + * Creates a new instance of worklet soundfont manager (worklet scope) + * @param initialSoundFontBuffer {ArrayBuffer} Array buffer of the soundfont. This soudfont always has the id "main" + * @param readyCallback {function} postReady() method from synth + */ + constructor(initialSoundFontBuffer, readyCallback) + { + /** + * @type {Function} + */ + this.ready = readyCallback; + this.reloadManager(initialSoundFontBuffer); + } + + generatePresetList() + { + /** + * <"bank-program", "presetName"> + * @type {Object} + */ + const presetList = {}; + // gather the presets in reverse and replace if necessary + for (let i = this.soundfontList.length - 1; i >= 0; i--) + { + const font = this.soundfontList[i]; + /** + * prevent preset names from the same soudfont from being overriden + * if the soundfont has two presets with matching bank and program + * @type {Set} + */ + const presets = new Set(); + for (const p of font.soundfont.presets) + { + const presetString = `${p.bank + font.bankOffset}-${p.program}`; + if (presets.has(presetString)) + { + continue; + } + presets.add(presetString); + presetList[presetString] = p.presetName; + } + } + + /** + * @type {{bank: number, presetName: string, program: number}[]} + */ + this.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} + */ + handleMessage(type, data) + { + switch (type) + { + 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}[]} + */ + getPresetList() + { + return this.presetList.slice(); + } + + /** + * Clears all soundfonts and adds a new one + * @param soundFontArrayBuffer {ArrayBuffer} + */ + reloadManager(soundFontArrayBuffer) + { + const font = loadSoundFont(soundFontArrayBuffer); + /** + * All the soundfonts, ordered from the most important to the least. + * @type {SoundFontType[]} + */ + this.soundfontList = []; + this.soundfontList.push({ + id: "main", + bankOffset: 0, + soundfont: font + }); + this.generatePresetList(); + this.ready(); + } + + deleteSoundFont(id) + { + if (this.soundfontList.length === 0) + { + SpessaSynthWarn("1 soundfont left. Aborting!"); + return; + } + const index = this.soundfontList.findIndex(s => s.id === id); + if (index === -1) + { + SpessaSynthWarn(`No soundfont with id of "${id}" found. Aborting!`); + return; + } + delete this.soundfontList[index].soundfont.presets; + delete this.soundfontList[index].soundfont.instruments; + delete this.soundfontList[index].soundfont.samples; + this.soundfontList.splice(index, 1); + this.generatePresetList(); + } + + /** + * Adds a new soundfont buffer with a given ID + * @param buffer {ArrayBuffer} + * @param id {string} + * @param bankOffset {number} + */ + addNewSoundFont(buffer, id, bankOffset) + { + if (this.soundfontList.find(s => s.id === id) !== undefined) + { + throw new Error("Cannot overwrite the existing soundfont. Use soundfontManager.delete(id) instead."); + } + this.soundfontList.push({ + id: id, + soundfont: loadSoundFont(buffer), + bankOffset: bankOffset + }); + this.generatePresetList(); + this.ready(); + } + + /** + * Rearranges the soundfonts + * @param newList {string[]} the order of soundfonts, a list of strings, first overwrites second + */ + rearrangeSoundFonts(newList) + { + this.soundfontList.sort((a, b) => + newList.indexOf(a.id) - newList.indexOf(b.id) + ); + this.generatePresetList(); + } + + /** + * Gets a given preset from the soundfont stack + * @param bankNumber {number} + * @param programNumber {number} + * @param allowXGDrums {boolean} if true, allows XG drum banks (120, 126 and 127) as drum preset + * @returns {BasicPreset} the preset + */ + getPreset(bankNumber, programNumber, allowXGDrums = false) + { + if (this.soundfontList.length < 1) + { + throw new Error("No soundfonts! This should never happen."); + } + for (const sf of this.soundfontList) + { + // check for the preset (with given offset) + const preset = sf.soundfont.getPresetNoFallback( + bankNumber - sf.bankOffset, + programNumber, + allowXGDrums + ); + if (preset !== undefined) + { + return preset; + } + // if not found, advance to the next soundfont + } + const isDrum = bankNumber === 128 || (allowXGDrums && isXGDrums(bankNumber)); + // if none found, return the first correct preset found + if (!isDrum) + { + for (const sf of this.soundfontList) + { + const preset = sf.soundfont.presets.find(p => p.program === programNumber && !p.isDrumPreset( + allowXGDrums)); + if (preset) + { + return preset; + } + } + // if nothing at all, use the first preset + return this.soundfontList[0].soundfont.presets[0]; + } + else + { + for (const sf of this.soundfontList) + { + // check for any drum type (127/128) and matching program + const p = sf.soundfont.presets.find(p => p.isDrumPreset(allowXGDrums) && p.program === programNumber); + if (p) + { + return p; + } + // check for any drum preset + const preset = sf.soundfont.presets.find(p => p.isDrumPreset(allowXGDrums)); + if (preset) + { + return preset; + } + } + // if nothing at all, use the first preset + return this.soundfontList[0].soundfont.presets[0]; + } + } + + destroyManager() + { + this.soundfontList.forEach(s => + { + s.soundfont.destroySoundBank(); + }); + delete this.soundfontList; + } +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_processor.js b/spessasynth_lib/synthetizer/worklet_system/worklet_processor.js new file mode 100644 index 0000000000000000000000000000000000000000..dd2533ad2dc43981b8f5230d545f2858ab5c7e0e --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_processor.js @@ -0,0 +1,9 @@ +import { consoleColors } from "../../utils/other.js"; +import { SpessaSynthProcessor } from "./main_processor.js"; +import { SpessaSynthInfo } from "../../utils/loggin.js"; +import { WORKLET_PROCESSOR_NAME } from "../synth_constants.js"; + + +// noinspection JSUnresolvedReference +registerProcessor(WORKLET_PROCESSOR_NAME, SpessaSynthProcessor); +SpessaSynthInfo("%cProcessor succesfully registered!", consoleColors.recognized); \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/controller_tables.js b/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/controller_tables.js new file mode 100644 index 0000000000000000000000000000000000000000..6c7378eaa45ebe9ea2d49c702283f1110f029e34 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/controller_tables.js @@ -0,0 +1,88 @@ +import { midiControllers } from "../../../midi_parser/midi_message.js"; +import { modulatorSources } from "../../../soundfont/basic_soundfont/modulator.js"; + +/* + * A bit of explanation: + * The controller table is stored as an int16 array, it stores 14-bit values. + * This controller table is then extended with the modulatorSources section, + * for example, pitch range and pitch range depth. + * This allows us for precise control range and supports full pitch-wheel resolution. + */ +export const NON_CC_INDEX_OFFSET = 128; +export const CONTROLLER_TABLE_SIZE = 147; + + +// an array with preset default values, so we can quickly use set() to reset the controllers +export const resetArray = new Int16Array(CONTROLLER_TABLE_SIZE).fill(0); +export const setResetValue = (i, v) => resetArray[i] = v << 7; + +// values come from Falcosoft MidiPlayer 6 +setResetValue(midiControllers.mainVolume, 100); +setResetValue(midiControllers.balance, 64); +setResetValue(midiControllers.expressionController, 127); +setResetValue(midiControllers.pan, 64); + +setResetValue(midiControllers.portamentoOnOff, 127); + +setResetValue(midiControllers.filterResonance, 64); +setResetValue(midiControllers.releaseTime, 64); +setResetValue(midiControllers.attackTime, 64); +setResetValue(midiControllers.brightness, 64); + +setResetValue(midiControllers.decayTime, 64); +setResetValue(midiControllers.vibratoRate, 64); +setResetValue(midiControllers.vibratoDepth, 64); +setResetValue(midiControllers.vibratoDelay, 64); +setResetValue(midiControllers.generalPurposeController6, 64); +setResetValue(midiControllers.generalPurposeController8, 64); + +setResetValue(midiControllers.RPNLsb, 127); +setResetValue(midiControllers.RPNMsb, 127); +setResetValue(midiControllers.NRPNLsb, 127); +setResetValue(midiControllers.NRPNMsb, 127); + + +export const PORTAMENTO_CONTROL_UNSET = 1; +// special case: portamento control +// since it is only 7-bit, only the values at multiple of 128 are allowed. +// a value of just 1 indicates no key set, hence no portamento. +// this is the "initial unset portamento key" flag. +resetArray[midiControllers.portamentoControl] = PORTAMENTO_CONTROL_UNSET; + +// pitch wheel +setResetValue(NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel, 64); +setResetValue(NON_CC_INDEX_OFFSET + modulatorSources.pitchWheelRange, 2); + +/** + * @enum {number} + */ +export const customControllers = { + channelTuning: 0, // cents, RPN for fine tuning + 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 modulation depth RPN + masterTuning: 3, // cents, set by system exclusive + 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; +/** + * @enum {number} + */ +export const dataEntryStates = { + Idle: 0, + RPCoarse: 1, + RPFine: 2, + NRPCoarse: 3, + NRPFine: 4, + DataCoarse: 5, + DataFine: 6 +}; +/** + * This is a channel configuration enum, it is internally sent from Synthetizer via controller change + * @enum {number} + */ +export const channelConfiguration = { + velocityOverride: 128 // overrides velocity for the given channel +}; \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/lfo.js b/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/lfo.js new file mode 100644 index 0000000000000000000000000000000000000000..546c6169e3801ff82c91cee7bc2bf8a891f00c79 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/lfo.js @@ -0,0 +1,26 @@ +/** + * lfo.js + * purpose: low frequency triangel oscillator + */ + +/** + * Calculates a triangular wave value for the given time + * @param startTime {number} seconds + * @param frequency {number} Hz + * @param currentTime {number} seconds + * @return {number} the value from -1 to 1 + */ +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 low-pass + // (happened with Synth Strings 2) + + // triangle, not sine + return Math.abs(xVal - (~~(xVal + 0.5))) * 4 - 1; +} diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js b/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js new file mode 100644 index 0000000000000000000000000000000000000000..a53a7aaeb563fe18988492de8d59225e668c619b --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js @@ -0,0 +1,265 @@ +import { absCentsToHz, decibelAttenuationToGain } from "./unit_converter.js"; +import { generatorTypes } from "../../../soundfont/basic_soundfont/generator.js"; + +/** + * lowpass_filter.js + * purpose: applies a low pass filter to a voice + * note to self: a lot of tricks and come from fluidsynth. + * They are the real smart guys. + * Shoutout to them! + * Give their repo a star over at: + * https://github.com/FluidSynth/fluidsynth + */ + +export const FILTER_SMOOTHING_FACTOR = 0.1; + +/** + * @typedef {Object} CachedCoefficient + * @property {number} a0 - Filter coefficient 1 + * @property {number} a1 - Filter coefficient 2 + * @property {number} a2 - Filter coefficient 3 + * @property {number} a3 - Filter coefficient 4 + * @property {number} a4 - Filter coefficient 5 + */ + +export class WorkletLowpassFilter +{ + /** + * Cached coefficient calculations + * stored as cachedCoefficients[resonanceCb][currentInitialFc] + * @type {CachedCoefficient[][]} + * @private + */ + static cachedCoefficients = []; + /** + * Filter coefficient 1 + * @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} + */ + resonanceCb = 0; + + /** + * Cutoff frequency in absolute cents + * @type {number} + */ + currentInitialFc = 13500; + + /** + * For tracking the last cutoff frequency in the apply method, absolute cents + * Set to infinity to force recalculation + * @type {number} + */ + lastTargetCutoff = Infinity; + + /** + * used for tracking if the filter has been initialized + * @type {boolean} + */ + initialized = false; + + /** + * Applies a low-pass filter to the given buffer + * @param voice {WorkletVoice} the voice we're working on + * @param outputBuffer {Float32Array} the buffer to apply the filter to + * @param fcExcursion {number} the addition of modenv and mod lfo in cents to the filter + * @param smoothingFactor {number} filter's cutoff frequency smoothing factor + */ + static apply(voice, outputBuffer, fcExcursion, smoothingFactor) + { + const initialFc = voice.modulatedGenerators[generatorTypes.initialFilterFc]; + const filter = voice.filter; + + + if (!filter.initialized) + { + // filter initialization, set the current fc to target + filter.initialized = true; + filter.currentInitialFc = initialFc; + } + else + { + /* Note: + * We only smooth out the initialFc part, + * the modulation envelope and LFO excursions are not smoothed. + */ + filter.currentInitialFc += (initialFc - filter.currentInitialFc) * smoothingFactor; + } + + // the final cutoff for this calculation + const targetCutoff = filter.currentInitialFc + fcExcursion; + const modulatedResonance = voice.modulatedGenerators[generatorTypes.initialFilterQ]; + /* note: + * the check for initialFC is because of the filter optimization + * (if cents are the maximum then the filter is open) + * filter cannot use this optimization if it's dynamic (see #53), and + * the filter can only be dynamic if the initial filter is not open + */ + if (filter.currentInitialFc > 13499 && targetCutoff > 13499 && modulatedResonance === 0) + { + filter.currentInitialFc = 13500; + return; // filter is open + } + + // check if the frequency has changed. if so, calculate new coefficients + if (Math.abs(filter.lastTargetCutoff - targetCutoff) > 1 || filter.resonanceCb !== modulatedResonance) + { + filter.lastTargetCutoff = targetCutoff; + filter.resonanceCb = modulatedResonance; + WorkletLowpassFilter.calculateCoefficients(filter, targetCutoff); + } + + // filter the input + // initial filtering code was ported from meltysynth created by sinshu. + for (let i = 0; i < outputBuffer.length; i++) + { + let input = outputBuffer[i]; + let filtered = filter.a0 * input + + filter.a1 * filter.x1 + + 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 filter {WorkletLowpassFilter} + * @param cutoffCents {number} + */ + static calculateCoefficients(filter, cutoffCents) + { + cutoffCents = ~~cutoffCents; // Math.floor + const qCb = filter.resonanceCb; + // check if these coefficients were already cached + const cached = WorkletLowpassFilter.cachedCoefficients?.[qCb]?.[cutoffCents]; + if (cached !== undefined) + { + filter.a0 = cached.a0; + filter.a1 = cached.a1; + filter.a2 = cached.a2; + filter.a3 = cached.a3; + filter.a4 = cached.a4; + return; + } + let cutoffHz = absCentsToHz(cutoffCents); + + // fix cutoff on low sample rates + cutoffHz = Math.min(cutoffHz, 0.45 * sampleRate); + + // the coefficient calculation code was originally ported from meltysynth by sinshu. + // turn resonance to gain, -3.01 so it gives a non-resonant peak + const qDb = qCb / 10; + // -1 because it's attenuation, and we don't want attenuation + const resonanceGain = decibelAttenuationToGain(-(qDb - 3.01)); + + // the sfspec asks for a reduction in gain based on the Q value. + // note that we calculate it again, + // without the 3.01-peak offset as it only applies to the coefficients, not the gain. + const qGain = 1 / Math.sqrt(decibelAttenuationToGain(-qDb)); + + + // note: no sin or cos tables here as the coefficients are cached + let w = 2 * Math.PI * cutoffHz / sampleRate; + let cosw = Math.cos(w); + let alpha = Math.sin(w) / (2 * resonanceGain); + + 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 + * @type {CachedCoefficient} + */ + const toCache = {}; + toCache.a0 = b0 / a0; + toCache.a1 = b1 / a0; + toCache.a2 = b2 / a0; + toCache.a3 = a1 / a0; + toCache.a4 = a2 / a0; + filter.a0 = toCache.a0; + filter.a1 = toCache.a1; + filter.a2 = toCache.a2; + filter.a3 = toCache.a3; + filter.a4 = toCache.a4; + + if (WorkletLowpassFilter.cachedCoefficients[qCb] === undefined) + { + WorkletLowpassFilter.cachedCoefficients[qCb] = []; + } + WorkletLowpassFilter.cachedCoefficients[qCb][cutoffCents] = toCache; + } +} + +// precompute all the cutoffs for 0q (most common) +const dummy = new WorkletLowpassFilter(); +dummy.resonanceCb = 0; +// sfspec section 8.1.3: initialFilterFc ranges from 1500 to 13,500 cents +for (let i = 1500; i < 13500; i++) +{ + dummy.currentInitialFc = i; + WorkletLowpassFilter.calculateCoefficients(dummy, i); +} diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js b/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js new file mode 100644 index 0000000000000000000000000000000000000000..f386d09e0d8c9e4800a9054a687ee4a10ec53761 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js @@ -0,0 +1,181 @@ +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"; + +/** + * modulation_envelope.js + * purpose: calculates the modulation envelope for the given voice + */ +const MODENV_PEAK = 1; + +// 1000 should be precise enough +const CONVEX_ATTACK = new Float32Array(1000); +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); +} + +export class WorkletModulationEnvelope +{ + /** + * The attack duration, in seconds + * @type {number} + */ + attackDuration = 0; + /** + * The decay duration, in seconds + * @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} + */ + delayEnd = 0; + /** + * Attack phase end time in seconds, absolute (audio context time) + * @type {number} + */ + attackEnd = 0; + /** + * Hold phase end time in seconds, absolute (audio context time) + * @type {number} + */ + holdEnd = 0; + /** + * Decay phase end time in seconds, absolute (audio context time) + * @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 + */ + static startRelease(voice) + { + WorkletModulationEnvelope.recalculate(voice); + } + + /** + * @param voice {WorkletVoice} the voice to recalculate + */ + static recalculate(voice) + { + const env = voice.modulationEnvelope; + + // in release? Might need to recalculate the value as it can be modulated + if (voice.isInRelease) + { + env.releaseStartLevel = WorkletModulationEnvelope.getValue(voice, voice.releaseStartTime, true); + } + + 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 are working on + * @param currentTime {number} in seconds + * @param ignoreRelease {boolean} if true, it will compute the value as if the voice was not released + * @returns {number} modenv value, from 0 to 1 + */ + static getValue(voice, currentTime, ignoreRelease = false) + { + const env = voice.modulationEnvelope; + if (voice.isInRelease && !ignoreRelease) + { + // if the voice is still in the delay phase, + // start level will be 0 that will result in divide by zero + if (env.releaseStartLevel === 0) + { + return 0; + } + return Math.max( + 0, + (1 - (currentTime - voice.releaseStartTime) / env.releaseDuration) * env.releaseStartLevel + ); + } + + if (currentTime < env.delayEnd) + { + env.currentValue = 0; // delay + } + 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) + { + // hold: stay at 1 + env.currentValue = MODENV_PEAK; + } + 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; + } + else + { + // sustain: stay at sustain level + env.currentValue = env.sustainLevel; + } + return env.currentValue; + } +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/modulator_curves.js b/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/modulator_curves.js new file mode 100644 index 0000000000000000000000000000000000000000..6734a79d803ff778de953099b26374da14a969ee --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/modulator_curves.js @@ -0,0 +1,89 @@ +import { modulatorCurveTypes } from "../../../soundfont/basic_soundfont/modulator.js"; + +/** + * modulator_curves.js + * precomputes modulator concave and conves curves and calculates a curve value for a given polarity, direction and type + */ + +// the length of the precomputed curve tables +export const MOD_PRECOMPUTED_LENGTH = 16384; + +// Precalculate lookup tables for concave and convex curves +const concave = new Float32Array(MOD_PRECOMPUTED_LENGTH + 1); +const convex = new Float32Array(MOD_PRECOMPUTED_LENGTH + 1); +// the equation is taken from FluidSynth as it's the standard for soundFonts +// more precisely, the gen_conv.c file +concave[0] = 0; +concave[concave.length - 1] = 1; + +convex[0] = 0; +convex[convex.length - 1] = 1; +for (let i = 1; i < MOD_PRECOMPUTED_LENGTH - 1; i++) +{ + let x = (-200 * 2 / 960) * Math.log(i / (concave.length - 1)) / Math.LN10; + convex[i] = 1 - x; + concave[concave.length - 1 - i] = x; +} + +/** + * Transforms a value with a given curve type + * @param polarity {number} 0 or 1 + * @param direction {number} 0 or 1 + * @param curveType {number} see modulatorCurveTypes in modulators.js + * @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) +{ + // inverse the value if needed + if (direction) + { + value = 1 - value; + } + switch (curveType) + { + case modulatorCurveTypes.linear: + if (polarity) + { + // bipolar curve + return value * 2 - 1; + } + return value; + + case modulatorCurveTypes.switch: + // switch + value = value > 0.5 ? 1 : 0; + if (polarity) + { + // multiply + return value * 2 - 1; + } + return value; + + case modulatorCurveTypes.concave: + // look up the value + if (polarity) + { + value = value * 2 - 1; + if (value < 0) + { + 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) + { + value = value * 2 - 1; + if (value < 0) + { + return -convex[~~(value * -MOD_PRECOMPUTED_LENGTH)]; + } + return convex[~~(value * MOD_PRECOMPUTED_LENGTH)]; + } + return convex[~~(value * MOD_PRECOMPUTED_LENGTH)]; + } +} diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/stereo_panner.js b/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/stereo_panner.js new file mode 100644 index 0000000000000000000000000000000000000000..2e653198dfb1b938f5ea7b1cbfd47a0abd401501 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/stereo_panner.js @@ -0,0 +1,120 @@ +import { generatorTypes } from "../../../soundfont/basic_soundfont/generator.js"; + +/** + * stereo_panner.js + * purpose: pans a given voice out to the stereo output and to the effects' outputs + */ + +export const PAN_SMOOTHING_FACTOR = 0.05; + +export const WORKLET_SYSTEM_REVERB_DIVIDER = 4600; +export const WORKLET_SYSTEM_CHORUS_DIVIDER = 2000; +const HALF_PI = Math.PI / 2; + +const MIN_PAN = -500; +const MAX_PAN = 500; +const PAN_RESOLUTION = MAX_PAN - MIN_PAN; + +// initialize pan lookup tables +const panTableLeft = new Float32Array(PAN_RESOLUTION + 1); +const panTableRight = new Float32Array(PAN_RESOLUTION + 1); +for (let pan = MIN_PAN; pan <= MAX_PAN; pan++) +{ + // clamp to 0-1 + const realPan = (pan - MIN_PAN) / PAN_RESOLUTION; + const tableIndex = pan - MIN_PAN; + panTableLeft[tableIndex] = Math.cos(HALF_PI * realPan); + panTableRight[tableIndex] = Math.sin(HALF_PI * realPan); +} + +/** + * Pans the voice to the given output buffers + * @param voice {WorkletVoice} the voice to pan + * @param inputBuffer {Float32Array} the input buffer in mono + * @param outputLeft {Float32Array} left output buffer + * @param outputRight {Float32Array} right output buffer + * @param reverbLeft {Float32Array} left reverb input + * @param reverbRight {Float32Array} right reverb input + * @param chorusLeft {Float32Array} left chorus buffer + * @param chorusRight {Float32Array} right chorus buffer + * @this {WorkletProcessorChannel} + */ +export function panVoice(voice, + inputBuffer, + outputLeft, outputRight, + reverbLeft, reverbRight, + chorusLeft, chorusRight) +{ + if (isNaN(inputBuffer[0])) + { + return; + } + /** + * clamp -500 to 500 + * @type {number} + */ + let pan; + if (voice.overridePan) + { + pan = voice.overridePan; + } + else + { + // smooth out pan to prevent clicking + voice.currentPan += (voice.modulatedGenerators[generatorTypes.pan] - voice.currentPan) * this.synth.panSmoothingFactor; + pan = voice.currentPan; + } + + const gain = this.synth.currentGain; + const index = ~~(pan + 500); + // get voice's gain levels for each channel + const gainLeft = panTableLeft[index] * gain * this.synth.panLeft; + const gainRight = panTableRight[index] * gain * this.synth.panRight; + + // disable reverb and chorus in one output mode + if (!this.synth.oneOutputMode) + { + const reverbSend = voice.modulatedGenerators[generatorTypes.reverbEffectsSend]; + if (reverbSend > 0) + { + // reverb is mono so we need to multiply by gain + const reverbGain = this.synth.reverbGain * gain * (reverbSend / WORKLET_SYSTEM_REVERB_DIVIDER); + for (let i = 0; i < inputBuffer.length; i++) + { + reverbLeft[i] += reverbGain * inputBuffer[i]; + } + // copy as its mono + reverbRight.set(reverbLeft); + } + + const chorusSend = voice.modulatedGenerators[generatorTypes.chorusEffectsSend]; + if (chorusSend > 0) + { + // chorus is stereo so we do not need to + const chorusGain = this.synth.chorusGain * chorusSend / WORKLET_SYSTEM_CHORUS_DIVIDER; + const chorusLeftGain = gainLeft * chorusGain; + const chorusRightGain = gainRight * chorusGain; + for (let i = 0; i < inputBuffer.length; i++) + { + chorusLeft[i] += chorusLeftGain * inputBuffer[i]; + chorusRight[i] += chorusRightGain * inputBuffer[i]; + } + } + } + + // mix down the audio data + if (gainLeft > 0) + { + for (let i = 0; i < inputBuffer.length; i++) + { + outputLeft[i] += gainLeft * inputBuffer[i]; + } + } + if (gainRight > 0) + { + for (let i = 0; i < inputBuffer.length; i++) + { + outputRight[i] += gainRight * inputBuffer[i]; + } + } +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/unit_converter.js b/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/unit_converter.js new file mode 100644 index 0000000000000000000000000000000000000000..23b4deed8c3266a62a9964081b894290e043bbda --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/unit_converter.js @@ -0,0 +1,73 @@ +/** + * unit_converter.js + * purpose: converts soundfont units into more useable values with the use of lookup tables to improve performance + */ + + +// timecent lookup table +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++) +{ + const timecents = MIN_TIMECENT + i; + timecentLookupTable[i] = Math.pow(2, timecents / 1200); +} + +/** + * Converts timecents to seconds + * @param timecents {number} timecents + * @returns {number} seconds + */ +export function timecentsToSeconds(timecents) +{ + if (timecents <= -32767) + { + return 0; + } + return timecentLookupTable[timecents - MIN_TIMECENT]; +} + +// abs cent lookup table +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++) +{ + const absoluteCents = MIN_ABS_CENT + i; + absoluteCentLookupTable[i] = 440 * Math.pow(2, (absoluteCents - 6900) / 1200); +} + +/** + * Converts absolute cents to hertz + * @param cents {number} absolute cents + * @returns {number} hertz + */ +export function absCentsToHz(cents) +{ + if (cents < MIN_ABS_CENT || cents > MAX_ABS_CENT) + { + return 440 * Math.pow(2, (cents - 6900) / 1200); + } + return absoluteCentLookupTable[~~(cents) - MIN_ABS_CENT]; +} + +// decibel lookup table (2 points of precision) +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++) +{ + const decibels = (MIN_DECIBELS * 100 + i) / 100; + decibelLookUpTable[i] = Math.pow(10, -decibels / 20); +} + +/** + * convers decibel attenuation to gain + * @param decibels {number} the decibel attenuation + * @returns {number} gain + */ +export function decibelAttenuationToGain(decibels) +{ + return decibelLookUpTable[Math.floor((decibels - MIN_DECIBELS) * 100)]; +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/volume_envelope.js b/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/volume_envelope.js new file mode 100644 index 0000000000000000000000000000000000000000..b3ab532d9d02e63f9aafaca78fd4b2520d73234b --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/volume_envelope.js @@ -0,0 +1,401 @@ +import { decibelAttenuationToGain, timecentsToSeconds } from "./unit_converter.js"; + +import { generatorTypes } from "../../../soundfont/basic_soundfont/generator.js"; + +/** + * volume_envelope.js + * purpose: applies a volume envelope for a given voice + */ + +export const VOLUME_ENVELOPE_SMOOTHING_FACTOR = 0.01; + +const DB_SILENCE = 100; +const PERCEIVED_DB_SILENCE = 90; +// around 96 dB of attenuation +const PERCEIVED_GAIN_SILENCE = 0.000015; // can't go lower than that (see #50) + +/** + * VOL ENV STATES: + * 0 - delay + * 1 - attack + * 2 - hold/peak + * 3 - decay + * 4 - sustain + * release is indicated by isInRelease property + */ + +export class WorkletVolumeEnvelope +{ + /** + * The envelope's current time in samples + * @type {number} + */ + currentSampleTime = 0; + /** + * The sample rate in Hz + * @type {number} + */ + sampleRate; + /** + * The current attenuation of the envelope in dB + * @type {number} + */ + currentAttenuationDb = DB_SILENCE; + /** + * The current stage of the volume envelope + * @type {0|1|2|3|4} + */ + state = 0; + /** + * The dB attenuation of the envelope when it entered the release stage + * @type {number} + */ + releaseStartDb = DB_SILENCE; + /** + * The time in samples relative to the start of the envelope + * @type {number} + */ + releaseStartTimeSamples = 0; + /** + * The current gain applied to the voice in the release stage + * @type {number} + */ + currentReleaseGain = 1; + /** + * The attack duration in samples + * @type {number} + */ + attackDuration = 0; + /** + * The decay duration in samples + * @type {number} + */ + decayDuration = 0; + /** + * The release duration in samples + * @type {number} + */ + releaseDuration = 0; + /** + * The voice's absolute attenuation as linear gain + * @type {number} + */ + attenuation = 0; + /** + * The attenuation target, which the "attenuation" property is linearly interpolated towards (gain) + * @type {number} + */ + attenuationTargetGain = 0; + /** + * The attenuation target, which the "attenuation" property is linearly interpolated towards (dB) + * @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 + */ + static startRelease(voice) + { + voice.volumeEnvelope.releaseStartTimeSamples = voice.volumeEnvelope.currentSampleTime; + voice.volumeEnvelope.currentReleaseGain = decibelAttenuationToGain(voice.volumeEnvelope.currentAttenuationDb); + WorkletVolumeEnvelope.recalculate(voice); + } + + /** + * Recalculates the envelope + * @param voice {WorkletVoice} the voice this envelope belongs to + */ + static recalculate(voice) + { + const env = voice.volumeEnvelope; + 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.attenuationTargetGain = decibelAttenuationToGain(env.attenuationTarget); + 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) + const fullChange = voice.modulatedGenerators[generatorTypes.decayVolEnv]; + 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.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) + { + // env.currentAttenuationDb = env.attenuationTarget; + env.state = 2; + } + + // check if voice is in release + 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 + // (to make volume go down exponentially) + // attack is linear (in gain) so we need to do get db from that + let elapsed = 1 - ((env.attackEnd - env.releaseStartTimeSamples) / env.attackDuration); + // calculate the gain that the attack would have + // 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) + { + voice.finished = true; + } + env.currentReleaseGain = decibelAttenuationToGain(env.releaseStartDb); + + // release: sfspec page 35: the time is for change from attenuation to -100dB + // therefore we need to calculate the real time + // (changing from release start to -100dB instead of from peak to -100dB) + const releaseFraction = (DB_SILENCE - env.releaseStartDb) / DB_SILENCE; + env.releaseDuration *= releaseFraction; + + } + } + + /** + * Applies volume envelope gain to the given output buffer + * @param voice {WorkletVoice} the voice we're working on + * @param audioBuffer {Float32Array} the audio buffer to modify + * @param centibelOffset {number} the centibel offset of volume, for modLFOtoVolume + * @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) + { + const env = voice.volumeEnvelope; + let decibelOffset = centibelOffset / 10; + + const attenuationSmoothing = smoothingFactor; + + // RELEASE PHASE + if (voice.isInRelease) + { + let elapsedRelease = env.currentSampleTime - env.releaseStartTimeSamples; + if (elapsedRelease >= env.releaseDuration) + { + for (let i = 0; i < audioBuffer.length; i++) + { + audioBuffer[i] = 0; + } + voice.finished = true; + return; + } + let dbDifference = DB_SILENCE - env.releaseStartDb; + for (let i = 0; i < audioBuffer.length; i++) + { + // attenuation interpolation + env.attenuation += (env.attenuationTargetGain - env.attenuation) * attenuationSmoothing; + let db = (elapsedRelease / env.releaseDuration) * dbDifference + env.releaseStartDb; + env.currentReleaseGain = env.attenuation * decibelAttenuationToGain(db + decibelOffset); + audioBuffer[i] *= env.currentReleaseGain; + env.currentSampleTime++; + elapsedRelease++; + } + + if (env.currentReleaseGain <= PERCEIVED_GAIN_SILENCE) + { + voice.finished = true; + } + return; + } + + let filledBuffer = 0; + switch (env.state) + { + case 0: + // delay phase, no sound is produced + while (env.currentSampleTime < env.delayEnd) + { + env.currentAttenuationDb = DB_SILENCE; + audioBuffer[filledBuffer] = 0; + + env.currentSampleTime++; + if (++filledBuffer >= audioBuffer.length) + { + return; + } + } + env.state++; + // fallthrough + + case 1: + // attack phase: ramp from 0 to attenuation + while (env.currentSampleTime < env.attackEnd) + { + // attenuation interpolation + env.attenuation += (env.attenuationTargetGain - 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 * env.attenuation * decibelAttenuationToGain( + decibelOffset); + // set current attenuation to peak as its invalid during this phase + env.currentAttenuationDb = 0; + + env.currentSampleTime++; + if (++filledBuffer >= audioBuffer.length) + { + return; + } + } + env.state++; + // fallthrough + + case 2: + // hold/peak phase: stay at attenuation + while (env.currentSampleTime < env.holdEnd) + { + // attenuation interpolation + env.attenuation += (env.attenuationTargetGain - env.attenuation) * attenuationSmoothing; + + audioBuffer[filledBuffer] *= env.attenuation * decibelAttenuationToGain(decibelOffset); + env.currentAttenuationDb = 0; + + env.currentSampleTime++; + if (++filledBuffer >= audioBuffer.length) + { + return; + } + } + env.state++; + // fallthrough + + case 3: + // decay phase: linear ramp from attenuation to sustain + while (env.currentSampleTime < env.decayEnd) + { + // attenuation interpolation + env.attenuation += (env.attenuationTargetGain - env.attenuation) * attenuationSmoothing; + + env.currentAttenuationDb = (1 - (env.decayEnd - env.currentSampleTime) / env.decayDuration) * env.sustainDbRelative; + audioBuffer[filledBuffer] *= env.attenuation * decibelAttenuationToGain(env.currentAttenuationDb + decibelOffset); + + env.currentSampleTime++; + if (++filledBuffer >= audioBuffer.length) + { + return; + } + } + env.state++; + // fallthrough + + case 4: + if (env.canEndOnSilentSustain && env.sustainDbRelative >= PERCEIVED_DB_SILENCE) + { + voice.finished = true; + } + // sustain phase: stay at sustain + while (true) + { + // attenuation interpolation + env.attenuation += (env.attenuationTargetGain - env.attenuation) * attenuationSmoothing; + + audioBuffer[filledBuffer] *= env.attenuation * decibelAttenuationToGain(env.sustainDbRelative + decibelOffset); + env.currentAttenuationDb = env.sustainDbRelative; + env.currentSampleTime++; + if (++filledBuffer >= audioBuffer.length) + { + return; + } + } + } + } +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js b/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js new file mode 100644 index 0000000000000000000000000000000000000000..c6cd6c837779cd6cfc0dd2b89fab5e701ababf48 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js @@ -0,0 +1,263 @@ +/** + * wavetable_oscillator.js + * purpose: plays back raw audio data at an arbitrary playback rate + */ + +/** + * + * @enum {number} + */ +export const interpolationTypes = { + linear: 0, + nearestNeighbor: 1, + fourthOrder: 2 +}; + + +export class WavetableOscillator +{ + + /** + * Fills the output buffer with raw sample data using linear interpolation + * @param voice {WorkletVoice} the voice we're working on + * @param outputBuffer {Float32Array} the output buffer to write to + */ + static getSampleLinear(voice, outputBuffer) + { + const sample = voice.sample; + let cur = sample.cursor; + const sampleData = sample.sampleData; + + if (sample.isLooping) + { + const loopLength = sample.loopEnd - sample.loopStart; + for (let i = 0; i < outputBuffer.length; i++) + { + // check for loop + while (cur >= sample.loopEnd) + { + cur -= loopLength; + } + + // grab the 2 nearest points + const floor = ~~cur; + let ceil = floor + 1; + + 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) + { + 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) + { + 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; + } + } + voice.sample.cursor = cur; + } + + /** + * Fills the output buffer with raw sample data using no interpolation (nearest neighbor) + * @param voice {WorkletVoice} the voice we're working on + * @param outputBuffer {Float32Array} the output buffer to write to + */ + static getSampleNearest(voice, outputBuffer) + { + const sample = voice.sample; + let cur = sample.cursor; + const loopLength = sample.loopEnd - sample.loopStart; + const sampleData = sample.sampleData; + if (voice.sample.isLooping) + { + for (let i = 0; i < outputBuffer.length; i++) + { + // check for loop + while (cur >= sample.loopEnd) + { + cur -= loopLength; + } + + // grab the nearest neighbor + let ceil = ~~cur + 1; + + while (ceil >= sample.loopEnd) + { + ceil -= loopLength; + } + + outputBuffer[i] = sampleData[ceil]; + cur += sample.playbackStep * voice.currentTuningCalculated; + } + } + else + { + 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) + { + voice.finished = true; + return; + } + + //nearest neighbor (uncomment to use) + outputBuffer[i] = sampleData[ceil]; + cur += sample.playbackStep * voice.currentTuningCalculated; + } + } + sample.cursor = cur; + } + + + /** + * Fills the output buffer with raw sample data using cubic interpolation + * @param voice {WorkletVoice} the voice we're working on + * @param outputBuffer {Float32Array} the output buffer to write to + */ + static getSampleCubic(voice, outputBuffer) + { + const sample = voice.sample; + let cur = sample.cursor; + const sampleData = sample.sampleData; + + if (sample.isLooping) + { + const loopLength = sample.loopEnd - sample.loopStart; + for (let i = 0; i < outputBuffer.length; i++) + { + // check for loop + 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 + 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 + // 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; + } + + // 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) + { + 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; + } + + // 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; + } + } + voice.sample.cursor = cur; + } +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js b/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js new file mode 100644 index 0000000000000000000000000000000000000000..61f629f69aa25553c9667a47c308ca00960b27ad --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js @@ -0,0 +1,266 @@ +import { getModulatorCurveValue, MOD_PRECOMPUTED_LENGTH } from "./modulator_curves.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"; +import { NON_CC_INDEX_OFFSET } from "./controller_tables.js"; + +/** + * worklet_modulator.js + * purpose: precomputes all curve types and computes modulators + */ + +const EFFECT_MODULATOR_TRANSFORM_MULTIPLIER = 1000 / 200; + +/** + * Computes a given modulator + * @param controllerTable {Int16Array} all midi controllers as 14bit values + the non-controller indexes, starting at 128 + * @param modulator {Modulator} the modulator to compute + * @param voice {WorkletVoice} the voice belonging to the modulator + * @returns {number} the computed value + */ +export function computeWorkletModulator(controllerTable, modulator, voice) +{ + if (modulator.transformAmount === 0) + { + modulator.currentValue = 0; + return 0; + } + // mapped to 0-16384 + let rawSourceValue; + if (modulator.sourceUsesCC) + { + rawSourceValue = controllerTable[modulator.sourceIndex]; + } + else + { + const index = modulator.sourceIndex + NON_CC_INDEX_OFFSET; + switch (modulator.sourceIndex) + { + 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) + { + rawSecondSrcValue = controllerTable[modulator.secSrcIndex]; + } + else + { + const index = modulator.secSrcIndex + NON_CC_INDEX_OFFSET; + switch (modulator.secSrcIndex) + { + 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]; + + // see the comment for isEffectModulator (modulator.js in basic_soundfont) for explanation + let transformAmount = modulator.transformAmount; + if (modulator.isEffectModulator && transformAmount <= 1000) + { + transformAmount *= EFFECT_MODULATOR_TRANSFORM_MULTIPLIER; + transformAmount = Math.min(transformAmount, 1000); + } + + // compute the modulator + let computedValue = sourceValue * secondSrcValue * transformAmount; + + if (modulator.transformType === 2) + { + // abs value + computedValue = Math.abs(computedValue); + } + + modulator.currentValue = computedValue; + return computedValue; +} + +/** + * Computes modulators of a given voice. Source and index indicate what modulators shall be computed + * @param voice {WorkletVoice} the voice to compute modulators for + * @param controllerTable {Int16Array} all midi controllers as 14bit values + the non-controller indexes, starting at 128 + * @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) +{ + const modulators = voice.modulators; + const generators = voice.generators; + const modulatedGenerators = voice.modulatedGenerators; + + if (sourceUsesCC === -1) + { + // All modulators mode: compute all modulators + modulatedGenerators.set(generators); + 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) + ); + }); + WorkletVolumeEnvelope.recalculate(voice); + WorkletModulationEnvelope.recalculate(voice); + return; + } + + // Optimized mode: calculate only modulators that use the given source + const volenvNeedsRecalculation = new Set([ + generatorTypes.initialAttenuation, + generatorTypes.delayVolEnv, + generatorTypes.attackVolEnv, + generatorTypes.holdVolEnv, + generatorTypes.decayVolEnv, + generatorTypes.sustainVolEnv, + generatorTypes.releaseVolEnv, + generatorTypes.keyNumToVolEnvHold, + generatorTypes.keyNumToVolEnvDecay + ]); + + const computedDestinations = new Set(); + + modulators.forEach(mod => + { + if ( + (mod.sourceUsesCC === sourceUsesCC && mod.sourceIndex === sourceIndex) || + (mod.secSrcUsesCC === sourceUsesCC && mod.secSrcIndex === sourceIndex) + ) + { + const destination = mod.modulatorDestination; + if (!computedDestinations.has(destination)) + { + // Reset this destination + modulatedGenerators[destination] = generators[destination]; + // compute our modulator + computeWorkletModulator(controllerTable, mod, voice); + // sum the values of all modulators for this destination + 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) + ); + } + }); + computedDestinations.add(destination); + } + } + }); + + // Recalculate volume envelope if necessary + if ([...computedDestinations].some(dest => volenvNeedsRecalculation.has(dest))) + { + WorkletVolumeEnvelope.recalculate(voice); + } + + WorkletModulationEnvelope.recalculate(voice); +} + + +/** + * as follows: transforms[curveType][polarity][direction] is an array + * @type {Float32Array[][][]} + */ +const transforms = []; + +for (let curve = 0; curve < 4; curve++) +{ + transforms[curve] = + [ + [ + new Float32Array(MOD_PRECOMPUTED_LENGTH), + new Float32Array(MOD_PRECOMPUTED_LENGTH) + ], + [ + new Float32Array(MOD_PRECOMPUTED_LENGTH), + new Float32Array(MOD_PRECOMPUTED_LENGTH) + ] + ]; + 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 + ); + + // polarity 1 dir 0 + transforms[curve][1][0][i] = getModulatorCurveValue( + 0, + curve, + i / MOD_PRECOMPUTED_LENGTH, + 1 + ); + + // polarity 0 dir 1 + transforms[curve][0][1][i] = getModulatorCurveValue( + 1, + curve, + i / MOD_PRECOMPUTED_LENGTH, + 0 + ); + + // polarity 1 dir 1 + transforms[curve][1][1][i] = getModulatorCurveValue( + 1, + curve, + i / MOD_PRECOMPUTED_LENGTH, + 1 + ); + } +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js b/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js new file mode 100644 index 0000000000000000000000000000000000000000..32ad46e1458ea2316cbd88237ceebd979ee8c5e3 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js @@ -0,0 +1,471 @@ +import { + CONTROLLER_TABLE_SIZE, + CUSTOM_CONTROLLER_TABLE_SIZE, + customControllers, + dataEntryStates, + NON_CC_INDEX_OFFSET +} from "./controller_tables.js"; +import { + resetControllers, + resetControllersRP15Compliant, + resetParameters +} from "../worklet_methods/controller_control/reset_controllers.js"; +import { renderVoice } from "../worklet_methods/render_voice.js"; +import { panVoice } from "./stereo_panner.js"; +import { killNote } from "../worklet_methods/stopping_notes/kill_note.js"; +import { setTuning } from "../worklet_methods/tuning_control/set_tuning.js"; +import { setModulationDepth } from "../worklet_methods/tuning_control/set_modulation_depth.js"; +import { dataEntryFine } from "../worklet_methods/data_entry/data_entry_fine.js"; +import { controllerChange } from "../worklet_methods/controller_control/controller_change.js"; +import { stopAllNotes } from "../worklet_methods/stopping_notes/stop_all_notes.js"; +import { muteChannel } from "../worklet_methods/mute_channel.js"; +import { transposeChannel } from "../worklet_methods/tuning_control/transpose_channel.js"; +import { dataEntryCoarse } from "../worklet_methods/data_entry/data_entry_coarse.js"; +import { noteOn } from "../worklet_methods/note_on.js"; +import { noteOff } from "../worklet_methods/stopping_notes/note_off.js"; +import { polyPressure } from "../worklet_methods/tuning_control/poly_pressure.js"; +import { channelPressure } from "../worklet_methods/tuning_control/channel_pressure.js"; +import { pitchWheel } from "../worklet_methods/tuning_control/pitch_wheel.js"; +import { setOctaveTuning } from "../worklet_methods/tuning_control/set_octave_tuning.js"; +import { programChange } from "../worklet_methods/program_change.js"; +import { chooseBank, isSystemXG, parseBankSelect } from "../../../utils/xg_hacks.js"; +import { DEFAULT_PERCUSSION } from "../../synth_constants.js"; +import { modulatorSources } from "../../../soundfont/basic_soundfont/modulator.js"; +import { returnMessageType } from "../message_protocol/worklet_message.js"; + +/** + * This class represents a single MIDI Channel within the synthesizer. + */ +class WorkletProcessorChannel +{ + /** + * An array of MIDI controller values and values used by modulators as the source (e.g., pitch bend, bend range, etc.). + * These are stored as 14-bit values. + * Refer to controller_tables.js for the index definitions. + * @type {Int16Array} + */ + midiControllers = new Int16Array(CONTROLLER_TABLE_SIZE); + + /** + * An array indicating if a controller, at the equivalent index in the midiControllers array, is locked + * (i.e., not allowed changing). + * A locked controller cannot be modified. + * @type {boolean[]} + */ + lockedControllers = Array(CONTROLLER_TABLE_SIZE).fill(false); + + /** + * An array of custom (non-SF2) control values such as RPN pitch tuning, transpose, modulation depth, etc. + * Refer to controller_tables.js for the index definitions. + * @type {Float32Array} + */ + customControllers = new Float32Array(CUSTOM_CONTROLLER_TABLE_SIZE); + + /** + * The key shift of the channel (in semitones). + * @type {number} + */ + channelTransposeKeyShift = 0; + + /** + * An array of octave tuning values for each note on the channel. + * Each index corresponds to a note (0 = C, 1 = C#, ..., 11 = B). + * Note: Repeaded every 12 notes + * @type {Int8Array} + */ + channelOctaveTuning = new Int8Array(128); + + /** + * Will be updated every time something tuning-related gets changed. + * This is used to avoid a big addition for every voice rendering call. + * @type {number} + */ + channelTuningCents = 0; + + /** + * Indicates whether the sustain (hold) pedal is active. + * @type {boolean} + */ + holdPedal = false; + + /** + * Indicates whether this channel is a drum channel. + * @type {boolean} + */ + drumChannel = false; + + /** + * If greater than 0, overrides the velocity value for the channel, otherwise it's disabled. + * @type {number} + */ + velocityOverride = 0; + + /** + * Enables random panning for every note played on this channel. + * @type {boolean} + */ + randomPan = false; + + /** + * The current state of the data entry for the channel. + * @type {dataEntryStates} + */ + dataEntryState = dataEntryStates.Idle; + + /** + * The bank number of the channel (used for patch changes). + * @type {number} + */ + bank = 0; + + /** + * The bank number sent as channel properties. + * @type {number} + */ + sentBank = 0; + + /** + * The bank LSB number of the channel (used for patch changes in XG mode). + * @type {number} + */ + bankLSB = 0; + + /** + * The preset currently assigned to the channel. + * @type {BasicPreset} + */ + preset = undefined; + + /** + * Indicates whether the program on this channel is locked. + * @type {boolean} + */ + lockPreset = false; + + /** + * Indicates the MIDI system when the preset was locked. + * @type {SynthSystem} + */ + lockedSystem = "gs"; + + /** + * Indicates whether the channel uses a preset from the override soundfont. + * @type {boolean} + */ + presetUsesOverride = false; + + /** + * Indicates whether the GS NRPN parameters are enabled for this channel. + * @type {boolean} + */ + lockGSNRPNParams = false; + + /** + * The vibrato settings for the channel. + * @type {Object} + * @property {number} depth - Depth of the vibrato effect in cents. + * @property {number} delay - Delay before the vibrato effect starts (in seconds). + * @property {number} rate - Rate of the vibrato oscillation (in Hz). + */ + channelVibrato = { delay: 0, depth: 0, rate: 0 }; + + /** + * Indicates whether the channel is muted. + * @type {boolean} + */ + isMuted = false; + + /** + * An array of voices currently active on the channel. + * @type {WorkletVoice[]} + */ + voices = []; + + /** + * An array of voices that are sustained on the channel. + * @type {WorkletVoice[]} + */ + sustainedVoices = []; + + /** + * The channel's number (0-based index) + * @type {number} + */ + channelNumber; + + /** + * Parent processor instance. + * @type {SpessaSynthProcessor} + */ + synth; + + /** + * Constructs a new MIDI channel + * @param synth {SpessaSynthProcessor} + * @param preset {BasicPreset} + * @param channelNumber {number} + */ + constructor(synth, preset, channelNumber) + { + this.synth = synth; + this.preset = preset; + this.channelNumber = channelNumber; + } + + get isXGChannel() + { + return isSystemXG(this.synth.system) || (this.lockPreset && isSystemXG(this.lockedSystem)); + } + + /** + * @param type {customControllers|number} + * @param value {number} + */ + setCustomController(type, value) + { + this.customControllers[type] = value; + this.updateChannelTuning(); + } + + updateChannelTuning() + { + this.channelTuningCents = + this.customControllers[customControllers.channelTuning] // RPN channel fine tuning + + this.customControllers[customControllers.channelTransposeFine] // user tuning (transpose) + + this.customControllers[customControllers.masterTuning] // master tuning, set by sysEx + + (this.customControllers[customControllers.channelTuningSemitones] * 100); // RPN channel coarse tuning + } + + /** + * @param outputLeft {Float32Array} the left output buffer + * @param outputRight {Float32Array} the right output buffer + * @param reverbOutputLeft {Float32Array} left output for reverb + * @param reverbOutputRight {Float32Array} right output for reverb + * @param chorusOutputLeft {Float32Array} left output for chorus + * @param chorusOutputRight {Float32Array} right output for chorus + */ + renderAudio( + outputLeft, outputRight, + reverbOutputLeft, reverbOutputRight, + chorusOutputLeft, chorusOutputRight + ) + { + this.voices = this.voices.filter(v => !this.renderVoice( + v, + outputLeft, outputRight, + reverbOutputLeft, reverbOutputRight, + chorusOutputLeft, chorusOutputRight + )); + } + + /** + * @param locked {boolean} + */ + setPresetLock(locked) + { + this.lockPreset = locked; + if (locked) + { + this.lockedSystem = this.synth.system; + } + } + + /** + * @param bank {number} + * @param isLSB {boolean} + */ + setBankSelect(bank, isLSB = false) + { + if (this.lockPreset) + { + return; + } + if (isLSB) + { + this.bankLSB = bank; + } + else + { + this.bank = bank; + const bankLogic = parseBankSelect( + this.getBankSelect(), + bank, + this.synth.system, + false, + this.drumChannel, + this.channelNumber + ); + switch (bankLogic.drumsStatus) + { + default: + case 0: + break; + + case 1: + if (this.channelNumber % 16 === DEFAULT_PERCUSSION) + { + // cannot disable drums on channel 9 + this.bank = 127; + } + break; + + case 2: + this.setDrums(true); + break; + } + } + } + + /** + * @returns {number} + */ + getBankSelect() + { + return chooseBank(this.bank, this.bankLSB, this.drumChannel, this.isXGChannel); + } + + /** + * Changes a preset of this channel + * @param preset {BasicPreset} + */ + setPreset(preset) + { + if (this.lockPreset) + { + return; + } + delete this.preset; + this.preset = preset; + } + + /** + * Sets drums on channel. + * @param isDrum {boolean} + */ + setDrums(isDrum) + { + if (this.lockPreset) + { + return; + } + if (this.drumChannel === isDrum) + { + return; + } + if (isDrum) + { + // clear transpose + this.channelTransposeKeyShift = 0; + this.drumChannel = true; + } + else + { + this.drumChannel = false; + } + this.presetUsesOverride = false; + this.synth.callEvent("drumchange", { + channel: this.channelNumber, + isDrumChannel: this.drumChannel + }); + this.programChange(this.preset.program); + this.sendChannelProperty(); + } + + /** + * Sets a custom vibrato + * @param depth {number} cents + * @param rate {number} Hz + * @param delay {number} seconds + */ + setVibrato(depth, rate, delay) + { + if (this.lockGSNRPNParams) + { + return; + } + this.channelVibrato.rate = rate; + this.channelVibrato.delay = delay; + this.channelVibrato.depth = depth; + } + + disableAndLockGSNRPN() + { + this.lockGSNRPNParams = true; + this.channelVibrato.rate = 0; + this.channelVibrato.delay = 0; + this.channelVibrato.depth = 0; + } + + + /** + * @typedef {Object} ChannelProperty + * @property {number} voicesAmount - the channel's current voice amount + * @property {number} pitchBend - the channel's current pitch bend from -8192 do 8192 + * @property {number} pitchBendRangeSemitones - the pitch bend's range, in semitones + * @property {boolean} isMuted - indicates whether the channel is muted + * @property {boolean} isDrum - indicates whether the channel is a drum channel + * @property {number} transposition - the channel's transposition, in semitones + * @property {number} bank - the bank number of the current preset + * @property {number} program - the MIDI program number of the current preset + */ + + + /** + * Sends this channel's property + */ + sendChannelProperty() + { + if (!this.synth.enableEventSystem) + { + return; + } + /** + * @type {ChannelProperty} + */ + const data = { + voicesAmount: this.voices.length, + pitchBend: this.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel], + pitchBendRangeSemitones: this.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheelRange] / 128, + isMuted: this.isMuted, + isDrum: this.drumChannel, + transposition: this.channelTransposeKeyShift + this.customControllers[customControllers.channelTransposeFine] / 100, + bank: this.sentBank, + program: this.preset.program + }; + this.synth.post({ + messageType: returnMessageType.channelPropertyChange, + messageData: [this.channelNumber, data] + }); + } +} + +// voice +WorkletProcessorChannel.prototype.renderVoice = renderVoice; +WorkletProcessorChannel.prototype.panVoice = panVoice; +WorkletProcessorChannel.prototype.killNote = killNote; +WorkletProcessorChannel.prototype.stopAllNotes = stopAllNotes; +WorkletProcessorChannel.prototype.muteChannel = muteChannel; + +// MIDI messages +WorkletProcessorChannel.prototype.noteOn = noteOn; +WorkletProcessorChannel.prototype.noteOff = noteOff; +WorkletProcessorChannel.prototype.polyPressure = polyPressure; +WorkletProcessorChannel.prototype.channelPressure = channelPressure; +WorkletProcessorChannel.prototype.pitchWheel = pitchWheel; +WorkletProcessorChannel.prototype.programChange = programChange; + +// Tuning +WorkletProcessorChannel.prototype.setTuning = setTuning; +WorkletProcessorChannel.prototype.setOctaveTuning = setOctaveTuning; +WorkletProcessorChannel.prototype.setModulationDepth = setModulationDepth; +WorkletProcessorChannel.prototype.transposeChannel = transposeChannel; + +// CC +WorkletProcessorChannel.prototype.controllerChange = controllerChange; +WorkletProcessorChannel.prototype.resetControllers = resetControllers; +WorkletProcessorChannel.prototype.resetControllersRP15Compliant = resetControllersRP15Compliant; +WorkletProcessorChannel.prototype.resetParameters = resetParameters; +WorkletProcessorChannel.prototype.dataEntryFine = dataEntryFine; +WorkletProcessorChannel.prototype.dataEntryCoarse = dataEntryCoarse; + +export { WorkletProcessorChannel }; diff --git a/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_voice.js b/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_voice.js new file mode 100644 index 0000000000000000000000000000000000000000..4a527fbca335c68d61e52145fd19ab8e810b2553 --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_system/worklet_utilities/worklet_voice.js @@ -0,0 +1,512 @@ +/** + * worklet_voice.js + * purpose: prepares workletvoices from sample and generator data and manages sample dumping + * note: sample dumping means sending it over to the AudioWorkletGlobalScope + */ +import { MIN_EXCLUSIVE_LENGTH, MIN_NOTE_LENGTH } from "../main_processor.js"; +import { 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"; +import { isSystemXG } from "../../../utils/xg_hacks.js"; + +const EXCLUSIVE_CUTOFF_TIME = -2320; +const EXCLUSIVE_MOD_CUTOFF_TIME = -1130; // less because filter shenanigans + +class WorkletSample +{ + /** + * 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 + * 1 - loop + * 2 - UNOFFICIAL: polyphone 2.4 added start on release + * 3 - loop then play when released + * @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; + } +} + + +/** + * WorkletVoice represents a single instance of the + * SoundFont2 synthesis model. + * That is: + * A wavetable oscillator (sample) + * A volume envelope (volumeEnvelope) + * A modulation envelope (modulationEnvelope) + * Generators (generators and modulatedGenerators) + * Modulators (modulators) + * And MIDI params such as channel, MIDI note, velocity + */ +class WorkletVoice +{ + /** + * The sample of the voice. + * @type {WorkletSample} + */ + sample; + + /** + * Lowpass filter applied to the voice. + * @type {WorkletLowpassFilter} + */ + filter = new WorkletLowpassFilter(); + + /** + * The unmodulated (copied to) generators of the voice. + * @type {Int16Array} + */ + generators; + + /** + * The voice's modulators. + * @type {Modulator[]} + */ + modulators = []; + + /** + * The generators in real-time, affected by modulators. + * This is used during rendering. + * @type {Int16Array} + */ + modulatedGenerators; + + /** + * Indicates if the voice is finished. + * @type {boolean} + */ + finished = false; + + /** + * Indicates if the voice is in the release phase. + * @type {boolean} + */ + isInRelease = false; + + /** + * MIDI channel number. + * @type {number} + */ + channelNumber = 0; + + /** + * Velocity of the note. + * @type {number} + */ + velocity = 0; + + /** + * MIDI note number. + * @type {number} + */ + midiNote = 0; + + /** + * The pressure of the voice + * @type {number} + */ + pressure = 0; + + /** + * Target key for the note. + * @type {number} + */ + targetKey = 0; + + /** + * Modulation envelope. + * @type {WorkletModulationEnvelope} + */ + modulationEnvelope = new WorkletModulationEnvelope(); + + /** + * Volume envelope. + * @type {WorkletVolumeEnvelope} + */ + volumeEnvelope; + + /** + * Start time of the voice, absolute. + * @type {number} + */ + startTime = 0; + + /** + * Start time of the release phase, absolute. + * @type {number} + */ + releaseStartTime = Infinity; + + /** + * Current tuning in cents. + * @type {number} + */ + currentTuningCents = 0; + + /** + * Current calculated tuning. (as in ratio) + * @type {number} + */ + currentTuningCalculated = 1; + + /** + * From -500 to 500. + * @param {number} + */ + currentPan = 0; + + /** + * If MIDI Tuning Standard is already applied (at note-on time), + * this will be used to take the values at real-time tuning as "midiNote" + * property contains the tuned number. + * see #29 comment by @paulikaro + * @type {number} + */ + realKey; + + /** + * @type {number} Initial key to glide from, MIDI Note number. If -1, the portamento is OFF. + */ + portamentoFromKey = -1; + + /** + * Duration of the linear glide, in seconds. + * @type {number} + */ + portamentoDuration = 0; + + /** + * From -500 to 500, where zero means disabled (use the channel pan). Used for random pan. + * @type {number} + */ + overridePan = 0; + + /** + * Exclusive class number for hi-hats etc. + * @type {number} + */ + exclusiveClass = 0; + + /** + * Creates a workletVoice + * @param sampleRate {number} + * @param workletSample {WorkletSample} + * @param midiNote {number} + * @param velocity {number} + * @param channel {number} + * @param currentTime {number} + * @param targetKey {number} + * @param realKey {number} + * @param generators {Int16Array} + * @param modulators {Modulator[]} + */ + constructor( + sampleRate, + workletSample, + midiNote, + velocity, + channel, + currentTime, + targetKey, + realKey, + generators, + modulators + ) + { + this.sample = workletSample; + this.generators = generators; + this.exclusiveClass = this.generators[generatorTypes.exclusiveClass]; + this.modulatedGenerators = new Int16Array(generators); + this.modulators = modulators; + + this.velocity = velocity; + this.midiNote = midiNote; + this.channelNumber = channel; + this.startTime = currentTime; + this.targetKey = targetKey; + this.realKey = realKey; + this.volumeEnvelope = new WorkletVolumeEnvelope(sampleRate, generators[generatorTypes.sustainVolEnv]); + } + + /** + * copies the voice + * @param voice {WorkletVoice} + * @param currentTime {number} + * @returns WorkletVoice + */ + static copy(voice, currentTime) + { + const sampleToCopy = voice.sample; + const sample = new WorkletSample( + sampleToCopy.sampleData, + sampleToCopy.playbackStep, + sampleToCopy.cursor, + sampleToCopy.rootKey, + sampleToCopy.loopStart, + sampleToCopy.loopEnd, + sampleToCopy.end, + sampleToCopy.loopingMode + ); + return new WorkletVoice( + voice.volumeEnvelope.sampleRate, + sample, + voice.midiNote, + voice.velocity, + voice.channelNumber, + currentTime, + voice.targetKey, + voice.realKey, + voice.generators, + voice.modulators.map(m => Modulator.copy(m)) + ); + } + + /** + * Releases the voice as exclusiveClass + */ + exclusiveRelease() + { + this.release(MIN_EXCLUSIVE_LENGTH); + this.modulatedGenerators[generatorTypes.releaseVolEnv] = EXCLUSIVE_CUTOFF_TIME; // make the release nearly instant + this.modulatedGenerators[generatorTypes.releaseModEnv] = EXCLUSIVE_MOD_CUTOFF_TIME; + WorkletVolumeEnvelope.recalculate(this); + WorkletModulationEnvelope.recalculate(this); + } + + /** + * Stops the voice + * @param minNoteLength {number} minimum note length in seconds + */ + release(minNoteLength = MIN_NOTE_LENGTH) + { + this.releaseStartTime = currentTime; + // check if the note is shorter than the min note time, if so, extend it + if (this.releaseStartTime - this.startTime < minNoteLength) + { + this.releaseStartTime = this.startTime + minNoteLength; + } + } +} + +/** + * @param channel {number} a hint for the processor to recalculate sample cursors when sample dumping + * @param midiNote {number} the MIDI note to use + * @param velocity {number} the velocity to use + * @param currentTime {number} the current time in seconds + * @param realKey {number} the real MIDI note if the "midiNote" was changed by MIDI Tuning Standard + * @this {SpessaSynthProcessor} + * @returns {WorkletVoice[]} output is an array of WorkletVoices + */ +export function getWorkletVoices(channel, + midiNote, + velocity, + currentTime, + realKey) +{ + /** + * @type {WorkletVoice[]} + */ + let workletVoices; + const channelObject = this.workletProcessorChannels[channel]; + + // override patch + const overridePatch = this.keyModifierManager.hasOverridePatch(channel, midiNote); + + let bank = channelObject.getBankSelect(); + let program = channelObject.preset.program; + if (overridePatch) + { + const override = this.keyModifierManager.getPatch(channel, midiNote); + bank = override.bank; + program = override.program; + } + + const cached = this.getCachedVoice(bank, program, midiNote, velocity); + + // if cached, return it! + if (cached !== undefined) + { + return cached.map(v => WorkletVoice.copy(v, currentTime)); + } + + // not cached... + let preset = channelObject.preset; + if (overridePatch) + { + preset = this.soundfontManager.getPreset(bank, program, isSystemXG(this.system)); + } + /** + * @returns {WorkletVoice[]} + */ + workletVoices = preset.getSamplesAndGenerators(midiNote, velocity) + .reduce((voices, sampleAndGenerators) => + { + if (sampleAndGenerators.sample.getAudioData() === 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.sampleData, + (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]; + } + + // uncomment to print debug info + // SpessaSynthTable([{ + // Sample: sampleAndGenerators.sample.sampleName, + // Generators: generators, + // Modulators: sampleAndGenerators.modulators.map(m => Modulator.debugString(m)), + // Velocity: velocity, + // TargetKey: targetKey, + // MidiNote: midiNote, + // WorkletSample: workletSample + // }]); + + + voices.push( + new WorkletVoice( + sampleRate, + workletSample, + midiNote, + velocity, + channel, + currentTime, + targetKey, + realKey, + generators, + sampleAndGenerators.modulators.map(m => Modulator.copy(m)) + ) + ); + return voices; + }, []); + // cache the voice + this.setCachedVoice(bank, program, midiNote, velocity, workletVoices.map(v => + WorkletVoice.copy(v, currentTime))); + return workletVoices; +} \ No newline at end of file diff --git a/spessasynth_lib/synthetizer/worklet_url.js b/spessasynth_lib/synthetizer/worklet_url.js new file mode 100644 index 0000000000000000000000000000000000000000..ccbd7c932a39ec80b06cb5c2fda9bb742da8d66f --- /dev/null +++ b/spessasynth_lib/synthetizer/worklet_url.js @@ -0,0 +1,5 @@ +/** + * The absolute path (from the spessasynth_lib folder) to the worklet module + * @type {string} + */ +export const WORKLET_URL_ABSOLUTE = "synthetizer/worklet_processor.min.js"; \ No newline at end of file diff --git a/spessasynth_lib/utils/README.md b/spessasynth_lib/utils/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ee095c0872c3513a2f1e451f377e32cb4953cb4c --- /dev/null +++ b/spessasynth_lib/utils/README.md @@ -0,0 +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/spessasynth_lib/utils/buffer_to_wav.js b/spessasynth_lib/utils/buffer_to_wav.js new file mode 100644 index 0000000000000000000000000000000000000000..4dc55246d522f9b74b3cf36df26dd445f1b808b3 --- /dev/null +++ b/spessasynth_lib/utils/buffer_to_wav.js @@ -0,0 +1,186 @@ +/** + * @typedef {Object} WaveMetadata + * @property {string|undefined} title - the song's title + * @property {string|undefined} artist - the song's artist + * @property {string|undefined} album - the song's album + * @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"; + +/** + * + * @param audioBuffer {AudioBuffer} + * @param normalizeAudio {boolean} find the max sample point and set it to 1, and scale others with it + * @param channelOffset {number} channel offset and channel offset + 1 get saved + * @param metadata {WaveMetadata} + * @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) +{ + 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) + { + const encoder = new TextEncoder(); + const infoChunks = [ + getStringBytes("INFO"), + writeRIFFOddSize("ICMT", encoder.encode("Created with SpessaSynth"), true) + ]; + if (metadata.artist) + { + infoChunks.push( + writeRIFFOddSize("IART", encoder.encode(metadata.artist), true) + ); + } + if (metadata.album) + { + infoChunks.push( + writeRIFFOddSize("IPRD", encoder.encode(metadata.album), true) + ); + } + if (metadata.genre) + { + infoChunks.push( + writeRIFFOddSize("IGNR", encoder.encode(metadata.genre), true) + ); + } + if (metadata.title) + { + infoChunks.push( + writeRIFFOddSize("INAM", encoder.encode(metadata.title), true) + ); + } + infoChunk = writeRIFFOddSize("LIST", combineArrays(infoChunks)); + } + + // prepare CUE chunk + let cueChunk = new IndexedByteArray(0); + const cueOn = loop?.end !== undefined && loop?.start !== undefined; + 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 + writeStringAsBytes(cueStart, "data"); // cue point ID + 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 + writeStringAsBytes(cueEnd, "data"); // cue point ID + 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, + cueEnd + ]); + 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 + ); + // 'WAVE' + header.set([87, 65, 86, 69], 8); + // 'fmt ' + header.set([102, 109, 116, 32], 12); + // fmt chunk length + header.set([16, 0, 0, 0], 16); // 16 for PCM + // audio format (PCM) + header.set([1, 0], 20); + // number of channels (2) + 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 + ); + // 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 + ); + // 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); + let offset = headerSize; + wavData.set(header, 0); + + // Interleave audio data (combine channels) + let multiplier = 32767; + 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)); + multiplier = maxAbsValue > 0 ? (32767 / maxAbsValue) : 1; + } + for (let i = 0; i < length; i++) + { + // 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) + { + wavData.set(infoChunk, offset); + offset += infoChunk.length; + } + if (cueOn) + { + wavData.set(cueChunk, offset); + } + + return new Blob([wavData.buffer], { type: "audio/wav" }); +} diff --git a/spessasynth_lib/utils/byte_functions/big_endian.js b/spessasynth_lib/utils/byte_functions/big_endian.js new file mode 100644 index 0000000000000000000000000000000000000000..e1bf376236f6516d19978df9de256ccbeea18146 --- /dev/null +++ b/spessasynth_lib/utils/byte_functions/big_endian.js @@ -0,0 +1,32 @@ +/** + * Reads as Big endian + * @param dataArray {IndexedByteArray} + * @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); + } + return out >>> 0; +} + +/** + * @param number {number} + * @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; + } + + return bytes; +} \ No newline at end of file diff --git a/spessasynth_lib/utils/byte_functions/little_endian.js b/spessasynth_lib/utils/byte_functions/little_endian.js new file mode 100644 index 0000000000000000000000000000000000000000..be27a911b3acda8c89408b849f3a57156419a5d8 --- /dev/null +++ b/spessasynth_lib/utils/byte_functions/little_endian.js @@ -0,0 +1,77 @@ +/** + * Reads as little endian + * @param dataArray {IndexedByteArray} + * @param bytesAmount {number} + * @returns {number} + */ +export function readLittleEndian(dataArray, bytesAmount) +{ + let out = 0; + for (let i = 0; i < bytesAmount; i++) + { + out |= (dataArray[dataArray.currentIndex++] << i * 8); + } + // make sure it stays unsigned + return out >>> 0; +} + +/** + * Writes a number as little endian seems to also work for negative numbers so yay? + * @param dataArray {IndexedByteArray} + * @param number {number} + * @param byteTarget {number} + */ +export function writeLittleEndian(dataArray, number, byteTarget) +{ + for (let i = 0; i < byteTarget; i++) + { + dataArray[dataArray.currentIndex++] = (number >> (i * 8)) & 0xFF; + } +} + +/** + * @param dataArray {IndexedByteArray} + * @param word {number} + */ +export function writeWord(dataArray, word) +{ + dataArray[dataArray.currentIndex++] = word & 0xFF; + dataArray[dataArray.currentIndex++] = word >> 8; +} + +/** + * @param dataArray {IndexedByteArray} + * @param dword {number} + */ +export function writeDword(dataArray, dword) +{ + writeLittleEndian(dataArray, dword, 4); +} + +/** + * @param byte1 {number} + * @param byte2 {number} + * @returns {number} + */ +export function signedInt16(byte1, byte2) +{ + let val = (byte2 << 8) | byte1; + if (val > 32767) + { + return val - 65536; + } + return val; +} + +/** + * @param byte {number} + * @returns {number} + */ +export function signedInt8(byte) +{ + if (byte > 127) + { + return byte - 256; + } + return byte; +} \ No newline at end of file diff --git a/spessasynth_lib/utils/byte_functions/string.js b/spessasynth_lib/utils/byte_functions/string.js new file mode 100644 index 0000000000000000000000000000000000000000..73dbf3f44c58717b380254e2b4f4d6d64ae37296 --- /dev/null +++ b/spessasynth_lib/utils/byte_functions/string.js @@ -0,0 +1,107 @@ +import { IndexedByteArray } from "../indexed_array.js"; + +/** + * @param dataArray {IndexedByteArray} + * @param bytes {number} + * @param encoding {string} the textElement encoding + * @param trimEnd {boolean} if we should trim once we reach an invalid byte + * @returns {string} + */ +export function readBytesAsString(dataArray, bytes, encoding = undefined, trimEnd = true) +{ + if (!encoding) + { + let finished = false; + let string = ""; + for (let i = 0; i < bytes; i++) + { + let byte = dataArray[dataArray.currentIndex++]; + if (finished) + { + continue; + } + if ((byte < 32 || byte > 127) && byte !== 10) // 10 is "\n" + { + if (trimEnd) + { + finished = true; + continue; + } + else + { + if (byte === 0) + { + finished = true; + continue; + } + } + } + string += String.fromCharCode(byte); + } + 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); + } +} + +/** + * @param string {string} + * @param padLength {number} + * @returns {IndexedByteArray} + */ +export function getStringBytes(string, padLength = 0) +{ + let len = string.length; + if (padLength > 0) + { + len = padLength; + } + const arr = new IndexedByteArray(len); + writeStringAsBytes(arr, string, padLength); + return arr; +} + +/** + * @param string {string} + * @returns {IndexedByteArray} + */ +export function getStringBytesZero(string) +{ + return getStringBytes(string, string.length + 1); +} + +/** + * @param string {string} + * @param outArray {IndexedByteArray} + * @param padLength {number} + * @returns {IndexedByteArray} modified IN PLACE + */ +export function writeStringAsBytes(outArray, string, padLength = 0) +{ + if (padLength > 0) + { + if (string.length > padLength) + { + string = string.slice(0, padLength); + } + } + for (let i = 0; i < string.length; i++) + { + outArray[outArray.currentIndex++] = string.charCodeAt(i); + } + + // pad with zeros if needed + if (padLength > string.length) + { + for (let i = 0; i < padLength - string.length; i++) + { + outArray[outArray.currentIndex++] = 0; + } + } + return outArray; +} \ No newline at end of file diff --git a/spessasynth_lib/utils/byte_functions/variable_length_quantity.js b/spessasynth_lib/utils/byte_functions/variable_length_quantity.js new file mode 100644 index 0000000000000000000000000000000000000000..678ade24f5fb58d7653523db70168e3107a4fe8e --- /dev/null +++ b/spessasynth_lib/utils/byte_functions/variable_length_quantity.js @@ -0,0 +1,42 @@ +/** + * Reads VLQ From a MIDI byte array + * @param MIDIbyteArray {IndexedByteArray} + * @returns {number} + */ +export function readVariableLengthQuantity(MIDIbyteArray) +{ + let out = 0; + while (MIDIbyteArray) + { + const byte = MIDIbyteArray[MIDIbyteArray.currentIndex++]; + // extract the first 7 bytes + out = (out << 7) | (byte & 127); + + // if the last byte isn't 1, stop reading + if ((byte >> 7) !== 1) + { + break; + } + } + return out; +} + +/** + * Write a VLQ from a number to a byte array + * @param number {number} + * @returns {number[]} + */ +export function writeVariableLengthQuantity(number) +{ + // Add the first byte + let bytes = [number & 127]; + number >>= 7; + + // Continue processing the remaining bytes + while (number > 0) + { + bytes.unshift((number & 127) | 128); + number >>= 7; + } + return bytes; +} \ No newline at end of file diff --git a/spessasynth_lib/utils/fill_with_defaults.js b/spessasynth_lib/utils/fill_with_defaults.js new file mode 100644 index 0000000000000000000000000000000000000000..f3e65627b99bc787d70883251f172546ee5aff04 --- /dev/null +++ b/spessasynth_lib/utils/fill_with_defaults.js @@ -0,0 +1,21 @@ +/** + * Fills the object with default values + * @param obj {Object} + * @param defObj {Object} + * @returns {Object} + */ +export function fillWithDefaults(obj, defObj) +{ + if (obj === undefined) + { + obj = {}; + } + for (const key in defObj) + { + if (defObj.hasOwnProperty(key) && !(key in obj)) + { + obj[key] = defObj[key]; + } + } + return obj; +} \ No newline at end of file diff --git a/spessasynth_lib/utils/indexed_array.js b/spessasynth_lib/utils/indexed_array.js new file mode 100644 index 0000000000000000000000000000000000000000..2d54126861e80973f9e51cc24f0f3f70d8943088 --- /dev/null +++ b/spessasynth_lib/utils/indexed_array.js @@ -0,0 +1,52 @@ +/** + * indexed_array.js + * purpose: exteds Uint8Array with a currentIndex property + */ + +export class IndexedByteArray extends Uint8Array +{ + /** + * The current index of the array + * @type {number} + */ + currentIndex = 0; + + /** + * Creates a new instance of an Uint8Array with a currentIndex property + * @param args {any} same as for Uint8Array + */ + constructor(args) + { + super(args); + } + + /** + * @param start {number?} + * @param end {number?} + * @returns {IndexedByteArray} + */ + slice(start, end) + { + const a = super.slice(start, end); + a.currentIndex = 0; + return a; + } +} + + +/** + * @param arrs {(IndexedByteArray|Uint8Array)[]} + * @returns {IndexedByteArray|Uint8Array} + */ +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) + { + newArr.set(arr, offset); + offset += arr.length; + } + return newArr; +} \ No newline at end of file diff --git a/spessasynth_lib/utils/loggin.js b/spessasynth_lib/utils/loggin.js new file mode 100644 index 0000000000000000000000000000000000000000..e3e0dcc97308132dded1012d9093ee95a172164d --- /dev/null +++ b/spessasynth_lib/utils/loggin.js @@ -0,0 +1,79 @@ +let ENABLE_INFO = false; +let ENABLE_WARN = true; +let ENABLE_GROUP = false; +let ENABLE_TABLE = true; + +/** + * Enables or disables looging + * @param enableInfo {boolean} - enables info + * @param enableWarn {boolean} - enables warning + * @param enableGroup {boolean} - enables groups + * @param enableTable {boolean} - enables tables + */ +export function SpessaSynthLogging(enableInfo, enableWarn, enableGroup, enableTable) +{ + ENABLE_INFO = enableInfo; + ENABLE_WARN = enableWarn; + ENABLE_GROUP = enableGroup; + ENABLE_TABLE = enableTable; +} + +/** + * @param message {...any} + */ +export function SpessaSynthInfo(...message) +{ + if (ENABLE_INFO) + { + console.info(...message); + } +} + +/** + * @param message {...any} + */ +export function SpessaSynthWarn(...message) +{ + if (ENABLE_WARN) + { + console.warn(...message); + } +} + +export function SpessaSynthTable(...args) +{ + if (ENABLE_TABLE) + { + console.table(...args); + } +} + +/** + * @param message {...any} the message + */ +export function SpessaSynthGroup(...message) +{ + if (ENABLE_GROUP) + { + console.group(...message); + } +} + +/** + * @param message {...any} the message + */ +export function SpessaSynthGroupCollapsed(...message) +{ + if (ENABLE_GROUP) + { + console.groupCollapsed(...message); + } +} + +export function SpessaSynthGroupEnd() +{ + if (ENABLE_GROUP) + { + console.groupEnd(); + } +} \ No newline at end of file diff --git a/spessasynth_lib/utils/other.js b/spessasynth_lib/utils/other.js new file mode 100644 index 0000000000000000000000000000000000000000..17c6b9bc8cc5d7453010dcecb33c8c6eb22417eb --- /dev/null +++ b/spessasynth_lib/utils/other.js @@ -0,0 +1,92 @@ +/** + * other.js + * purpose: contains some useful functions that don't belong in any specific category + */ + +/** + * Formats the given seconds to nice readable time + * @param totalSeconds {number} time in seconds + * @return {{seconds: number, minutes: number, time: string}} + */ +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")}` + }; +} + +/** + * @param fileName {string} + * @returns {string} + */ +export function formatTitle(fileName) +{ + return fileName + .trim() + .replaceAll(".mid", "") + .replaceAll(".kar", "") + .replaceAll(".rmi", "") + .replaceAll(".xmf", "") + .replaceAll(".mxmf", "") + .replaceAll("_", " ") + .trim(); +} + +/** + * Does what it says + * @param arr {number[]|Uint8Array} + * @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(); + hexString += hex; + hexString += " "; + } + + return hexString; +} + +/** + * @param eventData {Uint8Array} + * @returns {Uint8Array} + */ +export function sanitizeKarLyrics(eventData) +{ + // for KAR files: + // https://www.mixagesoftware.com/en/midikit/help/HTML/karaoke_formats.html + // "/" is the newline character + // "\" is also the newline character + // "\" ASCII code is 92 + // "/" ASCII code is 47 + // newline ASCII code is 10 + const sanitized = []; + for (let byte of eventData) + { + if (byte === 47 || byte === 92) + { + byte = 10; + } + sanitized.push(byte); + } + return new Uint8Array(sanitized); +} + +export const consoleColors = { + warn: "color: orange;", + unrecognized: "color: red;", + info: "color: aqua;", + recognized: "color: lime", + value: "color: yellow; background-color: black;" +}; + + diff --git a/spessasynth_lib/utils/sysex_detector.js b/spessasynth_lib/utils/sysex_detector.js new file mode 100644 index 0000000000000000000000000000000000000000..aaac4be816ab5a5cb545cf3748c838ebeb5f603e --- /dev/null +++ b/spessasynth_lib/utils/sysex_detector.js @@ -0,0 +1,58 @@ +/** + * @param e {MIDIMessage} + * @returns boolean + */ +export function isXGOn(e) +{ + return e.messageData[0] === 0x43 && // Yamaha + e.messageData[2] === 0x4C && // XG ON + e.messageData[5] === 0x7E && + e.messageData[6] === 0x00; +} + +/** + * @param e {MIDIMessage} + * @returns boolean + */ +export function isGSDrumsOn(e) +{ + return e.messageData[0] === 0x41 && // roland + e.messageData[2] === 0x42 && // GS + e.messageData[3] === 0x12 && // GS + e.messageData[4] === 0x40 && // system parameter + (e.messageData[5] & 0x10) !== 0 && // part parameter + e.messageData[6] === 0x15; // drum pars +} + +/** + * @param e {MIDIMessage} + * @returns boolean + */ +export function isGSOn(e) +{ + return e.messageData[0] === 0x41 // roland + && e.messageData[2] === 0x42 // GS + && e.messageData[6] === 0x7F; // Mode set +} + +/** + * @param e {MIDIMessage} + * @returns boolean + */ +export function isGMOn(e) +{ + return e.messageData[0] === 0x7E // non realtime + && e.messageData[2] === 0x09 // gm system + && e.messageData[3] === 0x01; // gm1 +} + +/** + * @param e {MIDIMessage} + * @returns boolean + */ +export function isGM2On(e) +{ + return e.messageData[0] === 0x7E // non realtime + && e.messageData[2] === 0x09 // gm system + && e.messageData[3] === 0x03; // gm2 +} \ No newline at end of file diff --git a/spessasynth_lib/utils/xg_hacks.js b/spessasynth_lib/utils/xg_hacks.js new file mode 100644 index 0000000000000000000000000000000000000000..6cb90665ee2660ebfe1fd47b4202117c70ae1499 --- /dev/null +++ b/spessasynth_lib/utils/xg_hacks.js @@ -0,0 +1,193 @@ +import { SpessaSynthInfo } from "./loggin.js"; +import { consoleColors } from "./other.js"; +import { DEFAULT_PERCUSSION } from "../synthetizer/synth_constants.js"; + +export const XG_SFX_VOICE = 64; + +const GM2_DEFAULT_BANK = 121; + +/** + * @param sys {SynthSystem} + * @returns {number} + */ +export function getDefaultBank(sys) +{ + return sys === "gm2" ? GM2_DEFAULT_BANK : 0; +} + +/** + * @param bankNr {number} + * @returns {boolean} + */ +export function isXGDrums(bankNr) +{ + return bankNr === 120 || bankNr === 126 || bankNr === 127; +} + +/** + * @param bank {number} + * @returns {boolean} + */ +export function isValidXGMSB(bank) +{ + return isXGDrums(bank) || bank === XG_SFX_VOICE || bank === GM2_DEFAULT_BANK; +} + +/** + * Bank select hacks abstracted here + * @param bankBefore {number} the current bank number + * @param bank {number} the cc change bank number + * @param system {SynthSystem} MIDI system + * @param isLSB {boolean} is bank LSB? + * @param isDrums {boolean} is drum channel? + * @param channelNumber {number} channel number + * @returns {{ + * newBank: number, + * drumsStatus: 0|1|2 + * }} 0 - unchanged, 1 - OFF, 2 - ON + */ +export function parseBankSelect(bankBefore, bank, system, isLSB, isDrums, channelNumber) +{ + // 64 means SFX in MSB, so it is allowed + let out = bankBefore; + let drumsStatus = 0; + if (isLSB) + { + if (isSystemXG(system)) + { + if (!isValidXGMSB(bank)) + { + out = bank; + } + } + else if (system === "gm2") + { + out = bank; + } + } + else + { + let canSetBankSelect = true; + switch (system) + { + case "gm": + // gm ignores bank select + SpessaSynthInfo( + `%cIgnoring the Bank Select (${bank}), as the synth is in GM mode.`, + consoleColors.info + ); + canSetBankSelect = false; + break; + + case "xg": + canSetBankSelect = isValidXGMSB(bank); + // for xg, if msb is 120, 126 or 127, then it's drums + if (isXGDrums(bank)) + { + drumsStatus = 2; + } + else + { + // drums shall not be disabled on channel 9 + if (channelNumber % 16 !== DEFAULT_PERCUSSION) + { + drumsStatus = 1; + } + } + break; + + case "gm2": + if (bank === 120) + { + drumsStatus = 2; + } + else + { + if (channelNumber % 16 !== DEFAULT_PERCUSSION) + { + drumsStatus = 1; + } + } + } + + if (isDrums) + { + // 128 for percussion channel + bank = 128; + } + if (bank === 128 && !isDrums) + { + // if a channel is not for percussion, default to bank current + bank = bankBefore; + } + if (canSetBankSelect) + { + out = bank; + } + } + return { + newBank: out, + drumsStatus: drumsStatus + }; +} + + +/** + * Chooses a bank number according to spessasynth logic + * That is: + * for GS, bank MSB if not drum, otherwise 128 + * for XG: bank MSB if drum and MSB is valid, 128 othewise, bank MSB if it is SFX voice, LSB otherwise + * @param msb {number} + * @param lsb {number} + * @param isDrums {boolean} + * @param isXG {boolean} + * @returns {number} + */ +export function chooseBank(msb, lsb, isDrums, isXG) +{ + if (isXG) + { + if (isDrums) + { + if (isXGDrums(msb)) + { + return msb; + } + else + { + return 128; + } + } + else + { + // check for SFX + if (isValidXGMSB(msb)) + { + return msb; + } + // if lsb is 0 and msb is not, use that + if (lsb === 0 && msb !== 0) + { + return msb; + } + if (!isValidXGMSB(lsb)) + { + return lsb; + } + return 0; + } + } + else + { + return isDrums ? 128 : msb; + } +} + +/** + * @param system {SynthSystem} + * @returns boolean + */ +export function isSystemXG(system) +{ + return system === "gm2" || system === "xg"; +} \ No newline at end of file