KEXEL commited on
Commit
b0bfea8
·
verified ·
1 Parent(s): 0ce40c8
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. 394.mid +0 -0
  3. advanced_demo.js +109 -0
  4. examples.css +66 -0
  5. index - Copia.html +32 -0
  6. index.html +32 -0
  7. spessasynth_lib/external_midi/README.md +4 -0
  8. spessasynth_lib/external_midi/midi_handler.js +130 -0
  9. spessasynth_lib/external_midi/web_midi_link.js +43 -0
  10. spessasynth_lib/externals/fflate/LICENSE +21 -0
  11. spessasynth_lib/externals/fflate/fflate.min.js +1 -0
  12. spessasynth_lib/externals/stbvorbis_sync/@types/stbvorbis_sync.d.ts +12 -0
  13. spessasynth_lib/externals/stbvorbis_sync/LICENSE +202 -0
  14. spessasynth_lib/externals/stbvorbis_sync/NOTICE +6 -0
  15. spessasynth_lib/externals/stbvorbis_sync/stbvorbis_sync.min.js +0 -0
  16. spessasynth_lib/midi_parser/README.md +32 -0
  17. spessasynth_lib/midi_parser/basic_midi.js +563 -0
  18. spessasynth_lib/midi_parser/midi_builder.js +202 -0
  19. spessasynth_lib/midi_parser/midi_data.js +63 -0
  20. spessasynth_lib/midi_parser/midi_editor.js +611 -0
  21. spessasynth_lib/midi_parser/midi_loader.js +324 -0
  22. spessasynth_lib/midi_parser/midi_message.js +254 -0
  23. spessasynth_lib/midi_parser/midi_sequence.js +225 -0
  24. spessasynth_lib/midi_parser/midi_writer.js +99 -0
  25. spessasynth_lib/midi_parser/rmidi_writer.js +567 -0
  26. spessasynth_lib/midi_parser/used_keys_loaded.js +238 -0
  27. spessasynth_lib/midi_parser/xmf_loader.js +454 -0
  28. spessasynth_lib/sequencer/README.md +30 -0
  29. spessasynth_lib/sequencer/default_sequencer_options.js +8 -0
  30. spessasynth_lib/sequencer/sequencer.js +804 -0
  31. spessasynth_lib/sequencer/worklet_sequencer/events.js +199 -0
  32. spessasynth_lib/sequencer/worklet_sequencer/play.js +355 -0
  33. spessasynth_lib/sequencer/worklet_sequencer/process_event.js +169 -0
  34. spessasynth_lib/sequencer/worklet_sequencer/process_tick.js +106 -0
  35. spessasynth_lib/sequencer/worklet_sequencer/sequencer_message.js +53 -0
  36. spessasynth_lib/sequencer/worklet_sequencer/song_control.js +229 -0
  37. spessasynth_lib/sequencer/worklet_sequencer/worklet_sequencer.js +336 -0
  38. spessasynth_lib/soundfont/README.md +13 -0
  39. spessasynth_lib/soundfont/basic_soundfont/basic_instrument.js +77 -0
  40. spessasynth_lib/soundfont/basic_soundfont/basic_preset.js +336 -0
  41. spessasynth_lib/soundfont/basic_soundfont/basic_sample.js +197 -0
  42. spessasynth_lib/soundfont/basic_soundfont/basic_soundfont.js +565 -0
  43. spessasynth_lib/soundfont/basic_soundfont/basic_zone.js +64 -0
  44. spessasynth_lib/soundfont/basic_soundfont/basic_zones.js +43 -0
  45. spessasynth_lib/soundfont/basic_soundfont/generator.js +220 -0
  46. spessasynth_lib/soundfont/basic_soundfont/modulator.js +378 -0
  47. spessasynth_lib/soundfont/basic_soundfont/riff_chunk.js +149 -0
  48. spessasynth_lib/soundfont/basic_soundfont/write_dls/art2.js +173 -0
  49. spessasynth_lib/soundfont/basic_soundfont/write_dls/articulator.js +49 -0
  50. spessasynth_lib/soundfont/basic_soundfont/write_dls/combine_zones.js +400 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ spessasynth_lib/soundfonts/GeneralUserGS.sf3 filter=lfs diff=lfs merge=lfs -text
394.mid ADDED
Binary file (10.8 kB). View file
 
advanced_demo.js ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+ // import the modules
14
+ import { WORKLET_URL_ABSOLUTE } from "./spessasynth_lib/synthetizer/worklet_url.js";
15
+ import { Sequencer } from "./spessasynth_lib/sequencer/sequencer.js";
16
+ import { Synthetizer } from "./spessasynth_lib/synthetizer/synthetizer.js";
17
+
18
+ // load the soundfont
19
+ fetch("./spessasynth_lib/soundfonts/GeneralUserGS.sf3").then(async response =>
20
+ {
21
+ // load the soundfont into an array buffer
22
+ let soundFontBuffer = await response.arrayBuffer();
23
+ document.getElementById("message").innerText = "SoundFont has been loaded!";
24
+
25
+ // create the context and add audio worklet
26
+ const context = new AudioContext();
27
+ await context.audioWorklet.addModule(new URL("./spessasynth_lib/" + WORKLET_URL_ABSOLUTE, import.meta.url));
28
+ const synth = new Synthetizer(context.destination, soundFontBuffer); // create the synthetizer
29
+ let seq;
30
+
31
+ // add an event listener for the file inout
32
+ document.getElementById("midi_input").addEventListener("change", async event =>
33
+ {
34
+ // check if any files are added
35
+ if (!event.target.files[0])
36
+ {
37
+ return;
38
+ }
39
+ // resume the context if paused
40
+ await context.resume();
41
+ // parse all the files
42
+ const parsedSongs = [];
43
+ for (let file of event.target.files)
44
+ {
45
+ const buffer = await file.arrayBuffer();
46
+ parsedSongs.push({
47
+ binary: buffer, // binary: the binary data of the file
48
+ altName: file.name // altName: the fallback name if the MIDI doesn't have one. Here we set it to the file name
49
+ });
50
+ }
51
+ if (seq === undefined)
52
+ {
53
+ seq = new Sequencer(parsedSongs, synth); // create the sequencer with the parsed midis
54
+ seq.play(); // play the midi
55
+ }
56
+ else
57
+ {
58
+ seq.loadNewSongList(parsedSongs); // the sequencer is already created, no need to create a new one.
59
+ }
60
+ seq.loop = false; // the sequencer loops a single song by default
61
+
62
+ // make the slider move with the song
63
+ let slider = document.getElementById("progress");
64
+ setInterval(() =>
65
+ {
66
+ // slider ranges from 0 to 1000
67
+ slider.value = (seq.currentTime / seq.duration) * 1000;
68
+ }, 100);
69
+
70
+ // on song change, show the name
71
+ seq.addOnSongChangeEvent(e =>
72
+ {
73
+ document.getElementById("message").innerText = "Now playing: " + e.midiName;
74
+ }, "example-time-change"); // make sure to add a unique id!
75
+
76
+ // add time adjustment
77
+ slider.onchange = () =>
78
+ {
79
+ // calculate the time
80
+ seq.currentTime = (slider.value / 1000) * seq.duration; // switch the time (the sequencer adjusts automatically)
81
+ };
82
+
83
+ // add button controls
84
+ document.getElementById("previous").onclick = () =>
85
+ {
86
+ seq.previousSong(); // go back by one song
87
+ };
88
+
89
+ // on pause click
90
+ document.getElementById("pause").onclick = () =>
91
+ {
92
+ if (seq.paused)
93
+ {
94
+ document.getElementById("pause").innerText = "Pause";
95
+ seq.play(); // resume
96
+ }
97
+ else
98
+ {
99
+ document.getElementById("pause").innerText = "Resume";
100
+ seq.pause(); // pause
101
+
102
+ }
103
+ };
104
+ document.getElementById("next").onclick = () =>
105
+ {
106
+ seq.nextSong(); // go to the next song
107
+ };
108
+ });
109
+ });
examples.css ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .txtlogee{
2
+ text-align: center;
3
+ text-transform: uppercase !important;
4
+ }
5
+
6
+ .playcax{
7
+ text-align: center;
8
+ margin: 15px 0px;
9
+ }
10
+
11
+ * {
12
+ font-family: "Noto Sans Light", "Open Sans Light", sans-serif;
13
+ color: #ccc;
14
+ font-size: 1.1rem;
15
+ }
16
+
17
+ button {
18
+ background: #444;
19
+ border: solid #555;
20
+ padding: 10px;
21
+ border-radius: 1rem;
22
+ cursor: pointer;
23
+ margin: 0.3rem;
24
+ display: inline-block;
25
+ text-transform: uppercase !important;
26
+ }
27
+
28
+ button:hover {
29
+ filter: brightness(1.1);
30
+ }
31
+
32
+ button:active {
33
+ transform: scale(0.9);
34
+ }
35
+
36
+ canvas {
37
+ width: 100%;
38
+ min-height: 0;
39
+ max-height: 40vh;
40
+ }
41
+
42
+ input[type="range"] {
43
+ width: 100%;
44
+ }
45
+
46
+ body {
47
+ background: #111;
48
+ display: flex;
49
+ flex-direction: column;
50
+ align-items: center;
51
+ max-height: 100vh;
52
+ margin: 50px;
53
+ }
54
+
55
+ .example_content {
56
+ width: 80%;
57
+ background: #333;
58
+ padding: 2em;
59
+ margin: 1rem;
60
+ /*border-radius: 1rem;*/
61
+ box-shadow: black 0 0 15px;
62
+ /*display: flex;*/
63
+ flex-direction: column;
64
+ align-items: center;
65
+ }
66
+
index - Copia.html ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang='en'>
3
+
4
+ <head>
5
+ <meta charset='UTF-8'>
6
+ <meta content='width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0'
7
+ name='viewport'>
8
+ <meta content='ie=edge' http-equiv='X-UA-Compatible'>
9
+ <title>SpessaSynth advanced example</title>
10
+ <link href='./examples.css' rel='stylesheet'>
11
+ </head>
12
+
13
+ <body>
14
+
15
+
16
+ <div class='example_content'>
17
+ <p id='message'>Please wait for the soundFont to load.</p>
18
+ <input accept='.mid, .rmi, .xmf, .mxmf' id='midi_input' multiple type='file'>
19
+ <br><br>
20
+ <input id='progress' max='1000' min='0' type='range' value='0'>
21
+ <br>
22
+
23
+ <button id='previous'>Previous song</button>
24
+ <button id='pause'>Pause</button>
25
+ <button id='next'>Next song</button>
26
+
27
+ <!-- note the type="module" -->
28
+ <script src='./advanced_demo.js' type='module'></script>
29
+ </div>
30
+ </body>
31
+
32
+ </html>
index.html ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang='en'>
3
+
4
+ <head>
5
+ <meta charset='UTF-8'>
6
+ <meta content='width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0'
7
+ name='viewport'>
8
+ <meta content='ie=edge' http-equiv='X-UA-Compatible'>
9
+ <title>SpessaSynth advanced example</title>
10
+ <link href='./examples.css' rel='stylesheet'>
11
+ </head>
12
+
13
+ <body>
14
+
15
+
16
+ <div class='example_content'>
17
+ <p id='message'>Please wait for the soundFont to load.</p>
18
+ <input accept='.mid, .rmi, .xmf, .mxmf' id='midi_input' multiple type='file'>
19
+ <br><br>
20
+ <input id='progress' max='1000' min='0' type='range' value='0'>
21
+ <br>
22
+
23
+ <button id='previous'>Previous song</button>
24
+ <button id='pause'>Pause</button>
25
+ <button id='next'>Next song</button>
26
+
27
+ <!-- note the type="module" -->
28
+ <script src='./advanced_demo.js' type='module'></script>
29
+ </div>
30
+ </body>
31
+
32
+ </html>
spessasynth_lib/external_midi/README.md ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ ## This is the MIDI handling folder.
2
+
3
+ The code here is respnsible for dealing with MIDI Inputs and outputs
4
+ and also for the WebMidiLink functionality.
spessasynth_lib/external_midi/midi_handler.js ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Synthetizer } from "../synthetizer/synthetizer.js";
2
+ import { consoleColors } from "../utils/other.js";
3
+ import { SpessaSynthInfo, SpessaSynthWarn } from "../utils/loggin.js";
4
+
5
+ /**
6
+ * midi_handler.js
7
+ * purpose: handles the connection between MIDI devices and synthesizer/sequencer via Web MIDI API
8
+ */
9
+
10
+ const NO_INPUT = null;
11
+
12
+ export class MIDIDeviceHandler
13
+ {
14
+ constructor()
15
+ {
16
+ }
17
+
18
+ /**
19
+ * @returns {Promise<boolean>} if succeded
20
+ */
21
+ async createMIDIDeviceHandler()
22
+ {
23
+ /**
24
+ * @type {MIDIInput}
25
+ */
26
+ this.selectedInput = NO_INPUT;
27
+ /**
28
+ * @type {MIDIOutput}
29
+ */
30
+ this.selectedOutput = NO_INPUT;
31
+ if (navigator.requestMIDIAccess)
32
+ {
33
+ // prepare the midi access
34
+ try
35
+ {
36
+ const response = await navigator.requestMIDIAccess({ sysex: true, software: true });
37
+ this.inputs = response.inputs;
38
+ this.outputs = response.outputs;
39
+ SpessaSynthInfo("%cMIDI handler created!", consoleColors.recognized);
40
+ return true;
41
+ }
42
+ catch (e)
43
+ {
44
+ SpessaSynthWarn(`Could not get MIDI Devices:`, e);
45
+ this.inputs = [];
46
+ this.outputs = [];
47
+ return false;
48
+ }
49
+ }
50
+ else
51
+ {
52
+ SpessaSynthWarn("Web MIDI Api not supported!", consoleColors.unrecognized);
53
+ this.inputs = [];
54
+ this.outputs = [];
55
+ return false;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Connects the sequencer to a given MIDI output port
61
+ * @param output {MIDIOutput}
62
+ * @param seq {Sequencer}
63
+ */
64
+ connectMIDIOutputToSeq(output, seq)
65
+ {
66
+ this.selectedOutput = output;
67
+ seq.connectMidiOutput(output);
68
+ SpessaSynthInfo(
69
+ `%cPlaying MIDI to %c${output.name}`,
70
+ consoleColors.info,
71
+ consoleColors.recognized
72
+ );
73
+ }
74
+
75
+ /**
76
+ * Disconnects a midi output port from the sequencer
77
+ * @param seq {Sequencer}
78
+ */
79
+ disconnectSeqFromMIDI(seq)
80
+ {
81
+ this.selectedOutput = NO_INPUT;
82
+ seq.connectMidiOutput(undefined);
83
+ SpessaSynthInfo(
84
+ "%cDisconnected from MIDI out.",
85
+ consoleColors.info
86
+ );
87
+ }
88
+
89
+ /**
90
+ * Connects a MIDI input to the synthesizer
91
+ * @param input {MIDIInput}
92
+ * @param synth {Synthetizer}
93
+ */
94
+ connectDeviceToSynth(input, synth)
95
+ {
96
+ this.selectedInput = input;
97
+ input.onmidimessage = event =>
98
+ {
99
+ synth.sendMessage(event.data);
100
+ };
101
+ SpessaSynthInfo(
102
+ `%cListening for messages on %c${input.name}`,
103
+ consoleColors.info,
104
+ consoleColors.recognized
105
+ );
106
+ }
107
+
108
+ /**
109
+ * @param input {MIDIInput}
110
+ */
111
+ disconnectDeviceFromSynth(input)
112
+ {
113
+ this.selectedInput = NO_INPUT;
114
+ input.onmidimessage = undefined;
115
+ SpessaSynthInfo(
116
+ `%cDisconnected from %c${input.name}`,
117
+ consoleColors.info,
118
+ consoleColors.recognized
119
+ );
120
+ }
121
+
122
+ disconnectAllDevicesFromSynth()
123
+ {
124
+ this.selectedInput = NO_INPUT;
125
+ for (const i of this.inputs)
126
+ {
127
+ i[1].onmidimessage = undefined;
128
+ }
129
+ }
130
+ }
spessasynth_lib/external_midi/web_midi_link.js ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Synthetizer } from "../synthetizer/synthetizer.js";
2
+ import { consoleColors } from "../utils/other.js";
3
+ import { SpessaSynthInfo } from "../utils/loggin.js";
4
+
5
+ /**
6
+ * web_midi_link.js
7
+ * purpose: handles the web midi link connection to the synthesizer
8
+ * https://www.g200kg.com/en/docs/webmidilink/
9
+ */
10
+
11
+ export class WebMIDILinkHandler
12
+ {
13
+ /**
14
+ * @param synth {Synthetizer} the synth to play to
15
+ */
16
+ constructor(synth)
17
+ {
18
+
19
+ window.addEventListener("message", msg =>
20
+ {
21
+ if (typeof msg.data !== "string")
22
+ {
23
+ return;
24
+ }
25
+ /**
26
+ * @type {string[]}
27
+ */
28
+ const data = msg.data.split(",");
29
+ if (data[0] !== "midi")
30
+ {
31
+ return;
32
+ }
33
+
34
+ data.shift(); // remove MIDI
35
+
36
+ const midiData = data.map(byte => parseInt(byte, 16));
37
+
38
+ synth.sendMessage(midiData);
39
+ });
40
+
41
+ SpessaSynthInfo("%cWeb MIDI Link handler created!", consoleColors.recognized);
42
+ }
43
+ }
spessasynth_lib/externals/fflate/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Arjun Barrett
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
spessasynth_lib/externals/fflate/fflate.min.js ADDED
@@ -0,0 +1 @@
 
 
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<<r[f-1];for(var v=new ur(e[30]),f=1;f<30;++f)for(var g=e[f];g<e[f+1];++g)v[g]=g-e[f]<<5|f;return{b:e,r:v}},Z=Y(W,2),$=Z.b,cr=Z.r;$[28]=258,cr[258]=28;var j=Y(X,0),hr=j.b,Fr=j.r,_=new T(32768);for(i=0;i<32768;++i)c=(i&43690)>>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<f;++v)r[v]&&++g[r[v]-1];var k=new T(a);for(v=1;v<a;++v)k[v]=k[v-1]+g[v-1]<<1;var b;if(e){b=new T(1<<a);var m=15-a;for(v=0;v<f;++v)if(r[v])for(var U=v<<4|r[v],x=a-r[v],n=k[r[v]-1]++<<x,o=n|(1<<x)-1;n<=o;++n)b[_[n]>>m]=U}else for(b=new T(f),v=0;v<f;++v)r[v]&&(b[v]=_[k[r[v]-1]++]>>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;e<r.length;++e)r[e]>a&&(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<s;++t)P[wr[t]]=u(r,n+t*3,7);n+=s*3;for(var rr=q(P),Ar=(1<<rr)-1,Mr=A(P,rr,1),t=0;t<d;){var ar=Mr[u(r,n,Ar)];n+=ar&15;var w=ar>>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<<z)-1,zr=(1<<D)-1,Q=n;;Q=n){var E=S[C(r,n)&Ur],p=E>>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<<B)-1)+$[t],n+=B}var R=I[C(r,n)&zr],V=R>>4;R||h(3),n+=R&15;var y=hr[V];if(V>3){var B=X[V];y+=C(r,n)&(1<<B)-1,n+=B}if(n>G){m&&h(0);break}b&&U(o+131072);var vr=o+nr;if(o<y){var ir=g-y,Dr=Math.min(y,vr);for(ir+o<0&&h(3);o<Dr;++o)e[o]=f[ir+o]}for(;o<vr;++o)e[o]=e[o-y]}}a.l=S,a.p=Q,a.b=o,a.f=x,S&&(x=1,a.m=z,a.d=I,a.n=D)}while(!x);return o!=e.length&&k?xr(e,0,o):e.subarray(0,o)},Tr=new l(0);function mr(r,a){return Sr(r,{i:2},a&&a.out,a&&a.dictionary)}var Er=typeof TextDecoder<"u"&&new TextDecoder,pr=0;try{Er.decode(Tr,{stream:!0}),pr=1}catch{}tr=mr})();export{tr as inflateSync};
spessasynth_lib/externals/stbvorbis_sync/@types/stbvorbis_sync.d.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ declare type DecodedData =
2
+ {
3
+ data: Float32Array[],
4
+ error: string | null,
5
+ sampleRate: number,
6
+ eof: boolean
7
+ }
8
+
9
+ declare const stbvorbis: {
10
+ decode(buffer: ArrayBuffer): DecodedData
11
+ isInitialized: Promise<boolean>
12
+ }
spessasynth_lib/externals/stbvorbis_sync/LICENSE ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "{}"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright {yyyy} {name of copyright owner}
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
202
+
spessasynth_lib/externals/stbvorbis_sync/NOTICE ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ LICENSE is for stbvorbis_sync.js which is licensed under Apache-2.0
2
+
3
+ Modifications made to stbvorbis_sync.js
4
+ 1. minified the code
5
+ 2. added types declaration
6
+ 3. changed the not initialized error message
spessasynth_lib/externals/stbvorbis_sync/stbvorbis_sync.min.js ADDED
The diff for this file is too large to render. See raw diff
 
spessasynth_lib/midi_parser/README.md ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## This is the MIDI file parsing folder.
2
+
3
+ The code here is responsible for parsing the MIDI files and interpreting the messsages.
4
+ All the events are defined in the `midi_message.js` file.
5
+
6
+ ### MIDI Classes hierarchy
7
+
8
+ #### MIDI Sequence
9
+ - The most basic class, containing all the metadata that inheritors have.
10
+ - It does not contain track data or embedded sound bank.
11
+ - Contains the function for calculating time from ticks.
12
+ - Contains the copying code.
13
+
14
+ #### MIDI Data
15
+ - Inherits from MIDI Sequence.
16
+ - Has an `isEmbedded` property to mark the existence of embedded bank. This is the class that is available in `sequencer.midiData`.
17
+
18
+ #### Basic MIDI
19
+ - Inherits from MIDI Sequence.
20
+ - The actual MIDI representation, containing the track data and embedded sound banks.
21
+ - Contains the code for parsing the MIDI and filling in the metadata automatically.
22
+ - Contains the SMF/RMI writing functions.
23
+ - Contains the code for determining used channels on tracks.
24
+
25
+ #### MIDI Builder
26
+ - Inherits from Basic MIDI.
27
+ - Used for building MIDIs from scratch.
28
+
29
+ ### MIDI
30
+ - Inherits from Basic MIDI.
31
+ - The SMF/RMI/XMF file parser.
32
+ - Called by the sequencer if an `ArrayBuffer` is provided.
spessasynth_lib/midi_parser/basic_midi.js ADDED
@@ -0,0 +1,563 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { MIDISequenceData } from "./midi_sequence.js";
2
+ import { getStringBytes, readBytesAsString } from "../utils/byte_functions/string.js";
3
+ import { messageTypes, MIDIMessage } from "./midi_message.js";
4
+ import { readBytesAsUintBigEndian } from "../utils/byte_functions/big_endian.js";
5
+ import { SpessaSynthGroup, SpessaSynthGroupEnd, SpessaSynthInfo } from "../utils/loggin.js";
6
+ import { consoleColors, formatTitle, sanitizeKarLyrics } from "../utils/other.js";
7
+ import { writeMIDI } from "./midi_writer.js";
8
+ import { applySnapshotToMIDI, modifyMIDI } from "./midi_editor.js";
9
+ import { writeRMIDI } from "./rmidi_writer.js";
10
+ import { getUsedProgramsAndKeys } from "./used_keys_loaded.js";
11
+ import { IndexedByteArray } from "../utils/indexed_array.js";
12
+
13
+ /**
14
+ * BasicMIDI is the base of a complete MIDI file, used by the sequencer internally.
15
+ * BasicMIDI is not available on the main thread, as it contains the actual track data which can be large.
16
+ * It can be accessed by calling getMIDI() on the Sequencer.
17
+ */
18
+ class BasicMIDI extends MIDISequenceData
19
+ {
20
+
21
+ /**
22
+ * The embedded soundfont in the MIDI file, represented as an ArrayBuffer, if available.
23
+ * @type {ArrayBuffer|undefined}
24
+ */
25
+ embeddedSoundFont = undefined;
26
+
27
+ /**
28
+ * The actual track data of the MIDI file, represented as an array of tracks.
29
+ * Tracks are arrays of MIDIMessage objects.
30
+ * @type {MIDIMessage[][]}
31
+ */
32
+ tracks = [];
33
+
34
+ /**
35
+ * If the MIDI file is a DLS RMIDI file.
36
+ * @type {boolean}
37
+ */
38
+ isDLSRMIDI = false;
39
+
40
+ /**
41
+ * Copies a MIDI
42
+ * @param mid {BasicMIDI}
43
+ * @returns {BasicMIDI}
44
+ */
45
+ static copyFrom(mid)
46
+ {
47
+ const m = new BasicMIDI();
48
+ m._copyFromSequence(mid);
49
+
50
+ m.isDLSRMIDI = mid.isDLSRMIDI;
51
+ m.embeddedSoundFont = mid.embeddedSoundFont ? mid.embeddedSoundFont.slice(0) : undefined; // Deep copy
52
+ m.tracks = mid.tracks.map(track => [...track]); // Shallow copy of each track array
53
+
54
+ return m;
55
+ }
56
+
57
+ /**
58
+ * Parses internal MIDI values
59
+ * @protected
60
+ */
61
+ _parseInternal()
62
+ {
63
+ SpessaSynthGroup(
64
+ "%cInterpreting MIDI events...",
65
+ consoleColors.info
66
+ );
67
+ /**
68
+ * For karaoke files, text events starting with @T are considered titles,
69
+ * usually the first one is the title, and the latter is things such as "sequenced by" etc.
70
+ * @type {boolean}
71
+ */
72
+ let karaokeHasTitle = false;
73
+
74
+ this.keyRange = { max: 0, min: 127 };
75
+
76
+ /**
77
+ * Will be joined with "\n" to form the final string
78
+ * @type {string[]}
79
+ */
80
+ let copyrightComponents = [];
81
+ let copyrightDetected = false;
82
+ if (typeof this.RMIDInfo["ICOP"] !== "undefined")
83
+ {
84
+ // if RMIDI has copyright info, don't try to detect one.
85
+ copyrightDetected = true;
86
+ }
87
+
88
+
89
+ let nameDetected = false;
90
+ if (typeof this.RMIDInfo["INAM"] !== "undefined")
91
+ {
92
+ // same as with copyright
93
+ nameDetected = true;
94
+ }
95
+
96
+ // loop tracking
97
+ let loopStart = null;
98
+ let loopEnd = null;
99
+
100
+ for (let i = 0; i < this.tracks.length; i++)
101
+ {
102
+ /**
103
+ * @type {MIDIMessage[]}
104
+ */
105
+ const track = this.tracks[i];
106
+ const usedChannels = new Set();
107
+ let trackHasVoiceMessages = false;
108
+
109
+ for (const e of track)
110
+ {
111
+ // check if it's a voice message
112
+ if (e.messageStatusByte >= 0x80 && e.messageStatusByte < 0xF0)
113
+ {
114
+ trackHasVoiceMessages = true;
115
+ // voice messages are 7-bit always
116
+ for (let j = 0; j < e.messageData.length; j++)
117
+ {
118
+ e.messageData[j] = Math.min(127, e.messageData[j]);
119
+ }
120
+ // last voice event tick
121
+ if (e.ticks > this.lastVoiceEventTick)
122
+ {
123
+ this.lastVoiceEventTick = e.ticks;
124
+ }
125
+
126
+ // interpret the voice message
127
+ switch (e.messageStatusByte & 0xF0)
128
+ {
129
+ // cc change: loop points
130
+ case messageTypes.controllerChange:
131
+ switch (e.messageData[0])
132
+ {
133
+ case 2:
134
+ case 116:
135
+ loopStart = e.ticks;
136
+ break;
137
+
138
+ case 4:
139
+ case 117:
140
+ if (loopEnd === null)
141
+ {
142
+ loopEnd = e.ticks;
143
+ }
144
+ else
145
+ {
146
+ // this controller has occurred more than once;
147
+ // this means
148
+ // that it doesn't indicate the loop
149
+ loopEnd = 0;
150
+ }
151
+ break;
152
+
153
+ case 0:
154
+ // check RMID
155
+ if (this.isDLSRMIDI && e.messageData[1] !== 0 && e.messageData[1] !== 127)
156
+ {
157
+ SpessaSynthInfo(
158
+ "%cDLS RMIDI with offset 1 detected!",
159
+ consoleColors.recognized
160
+ );
161
+ this.bankOffset = 1;
162
+ }
163
+ }
164
+ break;
165
+
166
+ // note on: used notes tracking and key range
167
+ case messageTypes.noteOn:
168
+ usedChannels.add(e.messageStatusByte & 0x0F);
169
+ const note = e.messageData[0];
170
+ this.keyRange.min = Math.min(this.keyRange.min, note);
171
+ this.keyRange.max = Math.max(this.keyRange.max, note);
172
+ break;
173
+ }
174
+ }
175
+ e.messageData.currentIndex = 0;
176
+ const eventText = readBytesAsString(e.messageData, e.messageData.length);
177
+ e.messageData.currentIndex = 0;
178
+ // interpret the message
179
+ switch (e.messageStatusByte)
180
+ {
181
+ case messageTypes.setTempo:
182
+ // add the tempo change
183
+ e.messageData.currentIndex = 0;
184
+ this.tempoChanges.push({
185
+ ticks: e.ticks,
186
+ tempo: 60000000 / readBytesAsUintBigEndian(e.messageData, 3)
187
+ });
188
+ e.messageData.currentIndex = 0;
189
+ break;
190
+
191
+ case messageTypes.marker:
192
+ // check for loop markers
193
+ const text = eventText.trim().toLowerCase();
194
+ switch (text)
195
+ {
196
+ default:
197
+ break;
198
+
199
+ case "start":
200
+ case "loopstart":
201
+ loopStart = e.ticks;
202
+ break;
203
+
204
+ case "loopend":
205
+ loopEnd = e.ticks;
206
+ }
207
+ e.messageData.currentIndex = 0;
208
+ break;
209
+
210
+ case messageTypes.copyright:
211
+ if (!copyrightDetected)
212
+ {
213
+ e.messageData.currentIndex = 0;
214
+ copyrightComponents.push(readBytesAsString(
215
+ e.messageData,
216
+ e.messageData.length,
217
+ undefined,
218
+ false
219
+ ));
220
+ e.messageData.currentIndex = 0;
221
+ }
222
+ break;
223
+
224
+ case messageTypes.lyric:
225
+ // note here: .kar files sometimes just use...
226
+ // lyrics instead of text because why not (of course)
227
+ // perform the same check for @KMIDI KARAOKE FILE
228
+ if (eventText.trim().startsWith("@KMIDI KARAOKE FILE"))
229
+ {
230
+ this.isKaraokeFile = true;
231
+ SpessaSynthInfo("%cKaraoke MIDI detected!", consoleColors.recognized);
232
+ }
233
+
234
+ if (this.isKaraokeFile)
235
+ {
236
+ // replace the type of the message with text
237
+ e.messageStatusByte = messageTypes.text;
238
+ }
239
+ else
240
+ {
241
+ // add lyrics like a regular midi file
242
+ this.lyrics.push(e.messageData);
243
+ this.lyricsTicks.push(e.ticks);
244
+ break;
245
+ }
246
+
247
+ // kar: treat the same as text
248
+ // fallthrough
249
+ case messageTypes.text:
250
+ // possibly Soft Karaoke MIDI file
251
+ // it has a text event at the start of the file
252
+ // "@KMIDI KARAOKE FILE"
253
+ const checkedText = eventText.trim();
254
+ if (checkedText.startsWith("@KMIDI KARAOKE FILE"))
255
+ {
256
+ this.isKaraokeFile = true;
257
+
258
+ SpessaSynthInfo("%cKaraoke MIDI detected!", consoleColors.recognized);
259
+ }
260
+ else if (this.isKaraokeFile)
261
+ {
262
+ // check for @T (title)
263
+ // or @A because it is a title too sometimes?
264
+ // IDK it's strange
265
+ if (checkedText.startsWith("@T") || checkedText.startsWith("@A"))
266
+ {
267
+ if (!karaokeHasTitle)
268
+ {
269
+ this.midiName = checkedText.substring(2).trim();
270
+ karaokeHasTitle = true;
271
+ nameDetected = true;
272
+ // encode to rawMidiName
273
+ this.rawMidiName = getStringBytes(this.midiName);
274
+ }
275
+ else
276
+ {
277
+ // append to copyright
278
+ copyrightComponents.push(checkedText.substring(2).trim());
279
+ }
280
+ }
281
+ else if (checkedText[0] !== "@")
282
+ {
283
+ // non @: the lyrics
284
+ this.lyrics.push(sanitizeKarLyrics(e.messageData));
285
+ this.lyricsTicks.push(e.ticks);
286
+ }
287
+ }
288
+ break;
289
+
290
+ case messageTypes.trackName:
291
+ break;
292
+ }
293
+ }
294
+ // add used channels
295
+ this.usedChannelsOnTrack.push(usedChannels);
296
+
297
+ // track name
298
+ this.trackNames[i] = "";
299
+ const trackName = track.find(e => e.messageStatusByte === messageTypes.trackName);
300
+ if (trackName)
301
+ {
302
+ trackName.messageData.currentIndex = 0;
303
+ const name = readBytesAsString(trackName.messageData, trackName.messageData.length);
304
+ this.trackNames[i] = name;
305
+ // If the track has no voice messages, its "track name" event (if it has any)
306
+ // is some metadata.
307
+ // Add it to copyright
308
+ if (!trackHasVoiceMessages)
309
+ {
310
+ copyrightComponents.push(name);
311
+ }
312
+ }
313
+ }
314
+
315
+ // reverse the tempo changes
316
+ this.tempoChanges.reverse();
317
+
318
+ SpessaSynthInfo(
319
+ `%cCorrecting loops, ports and detecting notes...`,
320
+ consoleColors.info
321
+ );
322
+
323
+ const firstNoteOns = [];
324
+ for (const t of this.tracks)
325
+ {
326
+ const firstNoteOn = t.find(e => (e.messageStatusByte & 0xF0) === messageTypes.noteOn);
327
+ if (firstNoteOn)
328
+ {
329
+ firstNoteOns.push(firstNoteOn.ticks);
330
+ }
331
+ }
332
+ this.firstNoteOn = Math.min(...firstNoteOns);
333
+
334
+ SpessaSynthInfo(
335
+ `%cFirst note-on detected at: %c${this.firstNoteOn}%c ticks!`,
336
+ consoleColors.info,
337
+ consoleColors.recognized,
338
+ consoleColors.info
339
+ );
340
+
341
+
342
+ if (loopStart !== null && loopEnd === null)
343
+ {
344
+ // not a loop
345
+ loopStart = this.firstNoteOn;
346
+ loopEnd = this.lastVoiceEventTick;
347
+ }
348
+ else
349
+ {
350
+ if (loopStart === null)
351
+ {
352
+ loopStart = this.firstNoteOn;
353
+ }
354
+
355
+ if (loopEnd === null || loopEnd === 0)
356
+ {
357
+ loopEnd = this.lastVoiceEventTick;
358
+ }
359
+ }
360
+
361
+ /**
362
+ *
363
+ * @type {{start: number, end: number}}
364
+ */
365
+ this.loop = { start: loopStart, end: loopEnd };
366
+
367
+ SpessaSynthInfo(
368
+ `%cLoop points: start: %c${this.loop.start}%c end: %c${this.loop.end}`,
369
+ consoleColors.info,
370
+ consoleColors.recognized,
371
+ consoleColors.info,
372
+ consoleColors.recognized
373
+ );
374
+
375
+ // determine ports
376
+ let portOffset = 0;
377
+ this.midiPorts = [];
378
+ this.midiPortChannelOffsets = [];
379
+ for (let trackNum = 0; trackNum < this.tracks.length; trackNum++)
380
+ {
381
+ this.midiPorts.push(-1);
382
+ if (this.usedChannelsOnTrack[trackNum].size === 0)
383
+ {
384
+ continue;
385
+ }
386
+ for (const e of this.tracks[trackNum])
387
+ {
388
+ if (e.messageStatusByte !== messageTypes.midiPort)
389
+ {
390
+ continue;
391
+ }
392
+ const port = e.messageData[0];
393
+ this.midiPorts[trackNum] = port;
394
+ if (this.midiPortChannelOffsets[port] === undefined)
395
+ {
396
+ this.midiPortChannelOffsets[port] = portOffset;
397
+ portOffset += 16;
398
+ }
399
+ }
400
+ }
401
+
402
+ // fix midi ports:
403
+ // midi tracks without ports will have a value of -1
404
+ // if all ports have a value of -1, set it to 0,
405
+ // otherwise take the first midi port and replace all -1 with it,
406
+ // why would we do this?
407
+ // some midis (for some reason) specify all channels to port 1 or else,
408
+ // but leave the conductor track with no port pref.
409
+ // this spessasynth to reserve the first 16 channels for the conductor track
410
+ // (which doesn't play anything) and use the additional 16 for the actual ports.
411
+ let defaultPort = Infinity;
412
+ for (let port of this.midiPorts)
413
+ {
414
+ if (port !== -1)
415
+ {
416
+ if (defaultPort > port)
417
+ {
418
+ defaultPort = port;
419
+ }
420
+ }
421
+ }
422
+ if (defaultPort === Infinity)
423
+ {
424
+ defaultPort = 0;
425
+ }
426
+ this.midiPorts = this.midiPorts.map(port => port === -1 ? defaultPort : port);
427
+ // add fake port if empty
428
+ if (this.midiPortChannelOffsets.length === 0)
429
+ {
430
+ this.midiPortChannelOffsets = [0];
431
+ }
432
+ if (this.midiPortChannelOffsets.length < 2)
433
+ {
434
+ SpessaSynthInfo(`%cNo additional MIDI Ports detected.`, consoleColors.info);
435
+ }
436
+ else
437
+ {
438
+ this.isMultiPort = true;
439
+ SpessaSynthInfo(`%cMIDI Ports detected!`, consoleColors.recognized);
440
+ }
441
+
442
+ // midi name
443
+ if (!nameDetected)
444
+ {
445
+ if (this.tracks.length > 1)
446
+ {
447
+ // if more than 1 track and the first track has no notes,
448
+ // just find the first trackName in the first track.
449
+ if (
450
+ this.tracks[0].find(
451
+ message => message.messageStatusByte >= messageTypes.noteOn
452
+ &&
453
+ message.messageStatusByte < messageTypes.polyPressure
454
+ ) === undefined
455
+ )
456
+ {
457
+
458
+ let name = this.tracks[0].find(message => message.messageStatusByte === messageTypes.trackName);
459
+ if (name)
460
+ {
461
+ this.rawMidiName = name.messageData;
462
+ name.messageData.currentIndex = 0;
463
+ this.midiName = readBytesAsString(name.messageData, name.messageData.length, undefined, false);
464
+ }
465
+ }
466
+ }
467
+ else
468
+ {
469
+ // if only 1 track, find the first "track name" event
470
+ let name = this.tracks[0].find(message => message.messageStatusByte === messageTypes.trackName);
471
+ if (name)
472
+ {
473
+ this.rawMidiName = name.messageData;
474
+ name.messageData.currentIndex = 0;
475
+ this.midiName = readBytesAsString(name.messageData, name.messageData.length, undefined, false);
476
+ }
477
+ }
478
+ }
479
+
480
+ if (!copyrightDetected)
481
+ {
482
+ this.copyright = copyrightComponents
483
+ // trim and group newlines into one
484
+ .map(c => c.trim().replace(/(\r?\n)+/g, "\n"))
485
+ // remove empty strings
486
+ .filter(c => c.length > 0)
487
+ // join with newlines
488
+ .join("\n") || "";
489
+ }
490
+
491
+ this.midiName = this.midiName.trim();
492
+ this.midiNameUsesFileName = false;
493
+ // if midiName is "", use the file name
494
+ if (this.midiName.length === 0)
495
+ {
496
+ SpessaSynthInfo(
497
+ `%cNo name detected. Using the alt name!`,
498
+ consoleColors.info
499
+ );
500
+ this.midiName = formatTitle(this.fileName);
501
+ this.midiNameUsesFileName = true;
502
+ // encode it too
503
+ this.rawMidiName = new Uint8Array(this.midiName.length);
504
+ for (let i = 0; i < this.midiName.length; i++)
505
+ {
506
+ this.rawMidiName[i] = this.midiName.charCodeAt(i);
507
+ }
508
+ }
509
+ else
510
+ {
511
+ SpessaSynthInfo(
512
+ `%cMIDI Name detected! %c"${this.midiName}"`,
513
+ consoleColors.info,
514
+ consoleColors.recognized
515
+ );
516
+ }
517
+
518
+ // if the first event is not at 0 ticks, add a track name
519
+ // https://github.com/spessasus/SpessaSynth/issues/145
520
+ if (!this.tracks.some(t => t[0].ticks === 0))
521
+ {
522
+ const track = this.tracks[0];
523
+ // can copy
524
+ track.unshift(new MIDIMessage(
525
+ 0,
526
+ messageTypes.trackName,
527
+ new IndexedByteArray(this.rawMidiName.buffer)
528
+ ));
529
+ }
530
+
531
+
532
+ /**
533
+ * The total playback time, in seconds
534
+ * @type {number}
535
+ */
536
+ this.duration = this.MIDIticksToSeconds(this.lastVoiceEventTick);
537
+
538
+ SpessaSynthInfo("%cSuccess!", consoleColors.recognized);
539
+ SpessaSynthGroupEnd();
540
+ }
541
+
542
+ /**
543
+ * Updates all internal values
544
+ */
545
+ flush()
546
+ {
547
+
548
+ for (const t of this.tracks)
549
+ {
550
+ // sort the track by ticks
551
+ t.sort((e1, e2) => e1.ticks - e2.ticks);
552
+ }
553
+ this._parseInternal();
554
+ }
555
+ }
556
+
557
+ BasicMIDI.prototype.writeMIDI = writeMIDI;
558
+ BasicMIDI.prototype.modifyMIDI = modifyMIDI;
559
+ BasicMIDI.prototype.applySnapshotToMIDI = applySnapshotToMIDI;
560
+ BasicMIDI.prototype.writeRMIDI = writeRMIDI;
561
+ BasicMIDI.prototype.getUsedProgramsAndKeys = getUsedProgramsAndKeys;
562
+
563
+ export { BasicMIDI };
spessasynth_lib/midi_parser/midi_builder.js ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BasicMIDI } from "./basic_midi.js";
2
+ import { messageTypes, MIDIMessage } from "./midi_message.js";
3
+ import { IndexedByteArray } from "../utils/indexed_array.js";
4
+ import { SpessaSynthWarn } from "../utils/loggin.js";
5
+
6
+ /**
7
+ * A class that helps to build a MIDI file from scratch.
8
+ */
9
+ export class MIDIBuilder extends BasicMIDI
10
+ {
11
+ /**
12
+ * @param name {string} The MIDI's name
13
+ * @param timeDivision {number} the file's time division
14
+ * @param initialTempo {number} the file's initial tempo
15
+ */
16
+ constructor(name, timeDivision = 480, initialTempo = 120)
17
+ {
18
+ super();
19
+ this.timeDivision = timeDivision;
20
+ this.midiName = name;
21
+ this.encoder = new TextEncoder();
22
+ this.rawMidiName = this.encoder.encode(name);
23
+
24
+ // create the first track with the file name
25
+ this.addNewTrack(name);
26
+ this.addSetTempo(0, initialTempo);
27
+ }
28
+
29
+ /**
30
+ * Adds a new Set Tempo event
31
+ * @param ticks {number} the tick number of the event
32
+ * @param tempo {number} the tempo in beats per minute (BPM)
33
+ */
34
+ addSetTempo(ticks, tempo)
35
+ {
36
+ const array = new IndexedByteArray(3);
37
+
38
+ tempo = 60000000 / tempo;
39
+
40
+ // Extract each byte in big-endian order
41
+ array[0] = (tempo >> 16) & 0xFF;
42
+ array[1] = (tempo >> 8) & 0xFF;
43
+ array[2] = tempo & 0xFF;
44
+
45
+ this.addEvent(ticks, 0, messageTypes.setTempo, array);
46
+ }
47
+
48
+ /**
49
+ * Adds a new MIDI track
50
+ * @param name {string} the new track's name
51
+ * @param port {number} the new track's port
52
+ */
53
+ addNewTrack(name, port = 0)
54
+ {
55
+ this.tracksAmount++;
56
+ if (this.tracksAmount > 1)
57
+ {
58
+ this.format = 1;
59
+ }
60
+ this.tracks.push([]);
61
+ this.tracks[this.tracksAmount - 1].push(
62
+ new MIDIMessage(0, messageTypes.endOfTrack, new IndexedByteArray(0))
63
+ );
64
+ this.addEvent(0, this.tracksAmount - 1, messageTypes.trackName, this.encoder.encode(name));
65
+ this.addEvent(0, this.tracksAmount - 1, messageTypes.midiPort, [port]);
66
+ }
67
+
68
+ /**
69
+ * Adds a new MIDI Event
70
+ * @param ticks {number} the tick time of the event
71
+ * @param track {number} the track number to use
72
+ * @param event {number} the MIDI event number
73
+ * @param eventData {Uint8Array|Iterable<number>} the raw event data
74
+ */
75
+ addEvent(ticks, track, event, eventData)
76
+ {
77
+ if (!this.tracks[track])
78
+ {
79
+ throw new Error(`Track ${track} does not exist. Add it via addTrack method.`);
80
+ }
81
+ if (event === messageTypes.endOfTrack)
82
+ {
83
+ SpessaSynthWarn(
84
+ "The EndOfTrack is added automatically and does not influence the duration. Consider adding a voice event instead.");
85
+ return;
86
+ }
87
+ // remove the end of track
88
+ this.tracks[track].pop();
89
+ this.tracks[track].push(new MIDIMessage(
90
+ ticks,
91
+ event,
92
+ new IndexedByteArray(eventData)
93
+ ));
94
+ // add the end of track
95
+ this.tracks[track].push(new MIDIMessage(
96
+ ticks,
97
+ messageTypes.endOfTrack,
98
+ new IndexedByteArray(0)
99
+ ));
100
+ }
101
+
102
+ /**
103
+ * Adds a new Note On event
104
+ * @param ticks {number} the tick time of the event
105
+ * @param track {number} the track number to use
106
+ * @param channel {number} the channel to use
107
+ * @param midiNote {number} the midi note of the keypress
108
+ * @param velocity {number} the velocity of the keypress
109
+ */
110
+ addNoteOn(ticks, track, channel, midiNote, velocity)
111
+ {
112
+ channel %= 16;
113
+ midiNote %= 128;
114
+ velocity %= 128;
115
+ this.addEvent(
116
+ ticks,
117
+ track,
118
+ messageTypes.noteOn | channel,
119
+ [midiNote, velocity]
120
+ );
121
+ }
122
+
123
+ /**
124
+ * Adds a new Note Off event
125
+ * @param ticks {number} the tick time of the event
126
+ * @param track {number} the track number to use
127
+ * @param channel {number} the channel to use
128
+ * @param midiNote {number} the midi note of the key release
129
+ */
130
+ addNoteOff(ticks, track, channel, midiNote)
131
+ {
132
+ channel %= 16;
133
+ midiNote %= 128;
134
+ this.addEvent(
135
+ ticks,
136
+ track,
137
+ messageTypes.noteOff | channel,
138
+ [midiNote, 64]
139
+ );
140
+ }
141
+
142
+ /**
143
+ * Adds a new Program Change event
144
+ * @param ticks {number} the tick time of the event
145
+ * @param track {number} the track number to use
146
+ * @param channel {number} the channel to use
147
+ * @param programNumber {number} the MIDI program to use
148
+ */
149
+ addProgramChange(ticks, track, channel, programNumber)
150
+ {
151
+ channel %= 16;
152
+ programNumber %= 128;
153
+ this.addEvent(
154
+ ticks,
155
+ track,
156
+ messageTypes.programChange | channel,
157
+ [programNumber]
158
+ );
159
+ }
160
+
161
+ /**
162
+ * Adds a new Controller Change event
163
+ * @param ticks {number} the tick time of the event
164
+ * @param track {number} the track number to use
165
+ * @param channel {number} the channel to use
166
+ * @param controllerNumber {number} the MIDI CC to use
167
+ * @param controllerValue {number} the new CC value
168
+ */
169
+ addControllerChange(ticks, track, channel, controllerNumber, controllerValue)
170
+ {
171
+ channel %= 16;
172
+ controllerNumber %= 128;
173
+ controllerValue %= 128;
174
+ this.addEvent(
175
+ ticks,
176
+ track,
177
+ messageTypes.controllerChange | channel,
178
+ [controllerNumber, controllerValue]
179
+ );
180
+ }
181
+
182
+ /**
183
+ * Adds a new Pitch Wheel event
184
+ * @param ticks {number} the tick time of the event
185
+ * @param track {number} the track to use
186
+ * @param channel {number} the channel to use
187
+ * @param MSB {number} SECOND byte of the MIDI pitchWheel message
188
+ * @param LSB {number} FIRST byte of the MIDI pitchWheel message
189
+ */
190
+ addPitchWheel(ticks, track, channel, MSB, LSB)
191
+ {
192
+ channel %= 16;
193
+ MSB %= 128;
194
+ LSB %= 128;
195
+ this.addEvent(
196
+ ticks,
197
+ track,
198
+ messageTypes.pitchBend | channel,
199
+ [LSB, MSB]
200
+ );
201
+ }
202
+ }
spessasynth_lib/midi_parser/midi_data.js ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { MIDISequenceData } from "./midi_sequence.js";
2
+
3
+ /**
4
+ * A simplified version of the MIDI, accessible at all times from the Sequencer.
5
+ * Use getMIDI() to get the actual sequence.
6
+ * This class contains all properties that MIDI does, except for tracks and the embedded soundfont.
7
+ */
8
+ export class MIDIData extends MIDISequenceData
9
+ {
10
+
11
+ /**
12
+ * A boolean indicating if the MIDI file contains an embedded soundfont.
13
+ * If the embedded soundfont is undefined, this will be false.
14
+ * @type {boolean}
15
+ */
16
+ isEmbedded = false;
17
+
18
+ /**
19
+ * Constructor that copies data from a BasicMIDI instance.
20
+ * @param {BasicMIDI} midi - The BasicMIDI instance to copy data from.
21
+ */
22
+ constructor(midi)
23
+ {
24
+ super();
25
+ this._copyFromSequence(midi);
26
+
27
+ // Set isEmbedded based on the presence of an embeddedSoundFont
28
+ this.isEmbedded = midi.embeddedSoundFont !== undefined;
29
+ }
30
+ }
31
+
32
+
33
+ /**
34
+ * Temporary MIDI data used when the MIDI is not loaded.
35
+ * @type {MIDIData}
36
+ */
37
+ export const DUMMY_MIDI_DATA = {
38
+ duration: 99999,
39
+ firstNoteOn: 0,
40
+ loop: {
41
+ start: 0,
42
+ end: 123456
43
+ },
44
+
45
+ lastVoiceEventTick: 123456,
46
+ lyrics: [],
47
+ copyright: "",
48
+ midiPorts: [],
49
+ midiPortChannelOffsets: [],
50
+ tracksAmount: 0,
51
+ tempoChanges: [{ ticks: 0, tempo: 120 }],
52
+ fileName: "NOT_LOADED.mid",
53
+ midiName: "Loading...",
54
+ rawMidiName: new Uint8Array([76, 111, 97, 100, 105, 110, 103, 46, 46, 46]), // "Loading..."
55
+ usedChannelsOnTrack: [],
56
+ timeDivision: 0,
57
+ keyRange: { min: 0, max: 127 },
58
+ isEmbedded: false,
59
+ RMIDInfo: {},
60
+ bankOffset: 0,
61
+ midiNameUsesFileName: false,
62
+ format: 0
63
+ };
spessasynth_lib/midi_parser/midi_editor.js ADDED
@@ -0,0 +1,611 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { messageTypes, midiControllers, MIDIMessage } from "./midi_message.js";
2
+ import { IndexedByteArray } from "../utils/indexed_array.js";
3
+ import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from "../utils/loggin.js";
4
+ import { consoleColors } from "../utils/other.js";
5
+
6
+ import { customControllers } from "../synthetizer/worklet_system/worklet_utilities/controller_tables.js";
7
+ import { DEFAULT_PERCUSSION } from "../synthetizer/synth_constants.js";
8
+ import { isGM2On, isGMOn, isGSOn, isXGOn } from "../utils/sysex_detector.js";
9
+ import { isSystemXG, isXGDrums, XG_SFX_VOICE } from "../utils/xg_hacks.js";
10
+
11
+ /**
12
+ * @param ticks {number}
13
+ * @returns {MIDIMessage}
14
+ */
15
+ export function getGsOn(ticks)
16
+ {
17
+ return new MIDIMessage(
18
+ ticks,
19
+ messageTypes.systemExclusive,
20
+ new IndexedByteArray([
21
+ 0x41, // Roland
22
+ 0x10, // Device ID (defaults to 16 on roland)
23
+ 0x42, // GS
24
+ 0x12, // Command ID (DT1) (whatever that means...)
25
+ 0x40, // System parameter - Address
26
+ 0x00, // Global parameter - Address
27
+ 0x7F, // GS Change - Address
28
+ 0x00, // turn on - Data
29
+ 0x41, // checksum
30
+ 0xF7 // end of exclusive
31
+ ])
32
+ );
33
+ }
34
+
35
+ /**
36
+ * @param channel {number}
37
+ * @param cc {number}
38
+ * @param value {number}
39
+ * @param ticks {number}
40
+ * @returns {MIDIMessage}
41
+ */
42
+ function getControllerChange(channel, cc, value, ticks)
43
+ {
44
+ return new MIDIMessage(
45
+ ticks,
46
+ messageTypes.controllerChange | (channel % 16),
47
+ new IndexedByteArray([cc, value])
48
+ );
49
+ }
50
+
51
+ /**
52
+ * @param channel {number}
53
+ * @param ticks {number}
54
+ * @returns {MIDIMessage}
55
+ */
56
+ function getDrumChange(channel, ticks)
57
+ {
58
+ const chanAddress = 0x10 | [1, 2, 3, 4, 5, 6, 7, 8, 0, 9, 10, 11, 12, 13, 14, 15][channel % 16];
59
+ // excluding manufacturerID DeviceID and ModelID (and F7)
60
+ const sysexData = [
61
+ 0x41, // Roland
62
+ 0x10, // Device ID (defaults to 16 on roland)
63
+ 0x42, // GS
64
+ 0x12, // Command ID (DT1) (whatever that means...)
65
+ 0x40, // System parameter }
66
+ chanAddress, // Channel parameter } Address
67
+ 0x15, // Drum change }
68
+ 0x01 // Is Drums } Data
69
+ ];
70
+ // calculate checksum
71
+ // https://cdn.roland.com/assets/media/pdf/F-20_MIDI_Imple_e01_W.pdf section 4
72
+ const sum = 0x40 + chanAddress + 0x15 + 0x01;
73
+ const checksum = 128 - (sum % 128);
74
+ // add system exclusive to enable drums
75
+ return new MIDIMessage(
76
+ ticks,
77
+ messageTypes.systemExclusive,
78
+ new IndexedByteArray([
79
+ ...sysexData,
80
+ checksum,
81
+ 0xF7
82
+ ])
83
+ );
84
+ }
85
+
86
+ /**
87
+ * @typedef {Object} DesiredProgramChange
88
+ * @property {number} channel - The channel number.
89
+ * @property {number} program - The program number.
90
+ * @property {number} bank - The bank number.
91
+ * @property {boolean} isDrum - Indicates if the channel is a drum channel.
92
+ * If it is, then the bank number is ignored.
93
+ */
94
+
95
+ /**
96
+ * @typedef {Object} DesiredControllerChange
97
+ * @property {number} channel - The channel number.
98
+ * @property {number} controllerNumber - The MIDI controller number.
99
+ * @property {number} controllerValue - The new controller value.
100
+ */
101
+
102
+ /**
103
+ * @typedef {Object} DesiredChanneltranspose
104
+ * @property {number} channel - The channel number.
105
+ * @property {number} keyShift - The number of semitones to transpose.
106
+ * Note that this can use floating point numbers,
107
+ * which will be used to fine-tune the pitch in cents using RPN.
108
+ */
109
+
110
+
111
+ /**
112
+ * Allows easy editing of the file by removing channels, changing programs,
113
+ * changing controllers and transposing channels. Note that this modifies the MIDI in-place.
114
+ *
115
+ * @this {BasicMIDI}
116
+ * @param {DesiredProgramChange[]} desiredProgramChanges - The programs to set on given channels.
117
+ * @param {DesiredControllerChange[]} desiredControllerChanges - The controllers to set on given channels.
118
+ * @param {number[]} desiredChannelsToClear - The channels to remove from the sequence.
119
+ * @param {DesiredChanneltranspose[]} desiredChannelsToTranspose - The channels to transpose.
120
+ */
121
+ export function modifyMIDI(
122
+ desiredProgramChanges = [],
123
+ desiredControllerChanges = [],
124
+ desiredChannelsToClear = [],
125
+ desiredChannelsToTranspose = []
126
+ )
127
+ {
128
+ const midi = this;
129
+ SpessaSynthGroupCollapsed("%cApplying changes to the MIDI file...", consoleColors.info);
130
+
131
+ SpessaSynthInfo("Desired program changes:", desiredProgramChanges);
132
+ SpessaSynthInfo("Desired CC changes:", desiredControllerChanges);
133
+ SpessaSynthInfo("Desired channels to clear:", desiredChannelsToClear);
134
+ SpessaSynthInfo("Desired channels to transpose:", desiredChannelsToTranspose);
135
+
136
+ /**
137
+ * @type {Set<number>}
138
+ */
139
+ const channelsToChangeProgram = new Set();
140
+ desiredProgramChanges.forEach(c =>
141
+ {
142
+ channelsToChangeProgram.add(c.channel);
143
+ });
144
+
145
+
146
+ // go through all events one by one
147
+ let system = "gs";
148
+ let addedGs = false;
149
+ /**
150
+ * indexes for tracks
151
+ * @type {number[]}
152
+ */
153
+ const eventIndexes = Array(midi.tracks.length).fill(0);
154
+ let remainingTracks = midi.tracks.length;
155
+
156
+ function findFirstEventIndex()
157
+ {
158
+ let index = 0;
159
+ let ticks = Infinity;
160
+ midi.tracks.forEach((track, i) =>
161
+ {
162
+ if (eventIndexes[i] >= track.length)
163
+ {
164
+ return;
165
+ }
166
+ if (track[eventIndexes[i]].ticks < ticks)
167
+ {
168
+ index = i;
169
+ ticks = track[eventIndexes[i]].ticks;
170
+ }
171
+ });
172
+ return index;
173
+ }
174
+
175
+ // it copies midiPorts everywhere else, but here 0 works so DO NOT CHANGE!
176
+ /**
177
+ * midi port number for the corresponding track
178
+ * @type {number[]}
179
+ */
180
+ const midiPorts = midi.midiPorts.slice();
181
+ /**
182
+ * midi port: channel offset
183
+ * @type {Object<number, number>}
184
+ */
185
+ const midiPortChannelOffsets = {};
186
+ let midiPortChannelOffset = 0;
187
+
188
+ function assignMIDIPort(trackNum, port)
189
+ {
190
+ // do not assign ports to empty tracks
191
+ if (midi.usedChannelsOnTrack[trackNum].size === 0)
192
+ {
193
+ return;
194
+ }
195
+
196
+ // assign new 16 channels if the port is not occupied yet
197
+ if (midiPortChannelOffset === 0)
198
+ {
199
+ midiPortChannelOffset += 16;
200
+ midiPortChannelOffsets[port] = 0;
201
+ }
202
+
203
+ if (midiPortChannelOffsets[port] === undefined)
204
+ {
205
+ midiPortChannelOffsets[port] = midiPortChannelOffset;
206
+ midiPortChannelOffset += 16;
207
+ }
208
+
209
+ midiPorts[trackNum] = port;
210
+ }
211
+
212
+ // assign port offsets
213
+ midi.midiPorts.forEach((port, trackIndex) =>
214
+ {
215
+ assignMIDIPort(trackIndex, port);
216
+ });
217
+
218
+ const channelsAmount = midiPortChannelOffset;
219
+ /**
220
+ * Tracks if the channel already had its first note on
221
+ * @type {boolean[]}
222
+ */
223
+ const isFirstNoteOn = Array(channelsAmount).fill(true);
224
+
225
+ /**
226
+ * MIDI key transpose
227
+ * @type {number[]}
228
+ */
229
+ const coarseTranspose = Array(channelsAmount).fill(0);
230
+ /**
231
+ * RPN fine transpose
232
+ * @type {number[]}
233
+ */
234
+ const fineTranspose = Array(channelsAmount).fill(0);
235
+ desiredChannelsToTranspose.forEach(transpose =>
236
+ {
237
+ const coarse = Math.trunc(transpose.keyShift);
238
+ const fine = transpose.keyShift - coarse;
239
+ coarseTranspose[transpose.channel] = coarse;
240
+ fineTranspose[transpose.channel] = fine;
241
+ });
242
+
243
+ while (remainingTracks > 0)
244
+ {
245
+ let trackNum = findFirstEventIndex();
246
+ const track = midi.tracks[trackNum];
247
+ if (eventIndexes[trackNum] >= track.length)
248
+ {
249
+ remainingTracks--;
250
+ continue;
251
+ }
252
+ const index = eventIndexes[trackNum]++;
253
+ const e = track[index];
254
+
255
+ const deleteThisEvent = () =>
256
+ {
257
+ track.splice(index, 1);
258
+ eventIndexes[trackNum]--;
259
+ };
260
+
261
+ /**
262
+ * @param e {MIDIMessage}
263
+ * @param offset{number}
264
+ */
265
+ const addEventBefore = (e, offset = 0) =>
266
+ {
267
+ track.splice(index + offset, 0, e);
268
+ eventIndexes[trackNum]++;
269
+ };
270
+
271
+
272
+ let portOffset = midiPortChannelOffsets[midiPorts[trackNum]] || 0;
273
+ if (e.messageStatusByte === messageTypes.midiPort)
274
+ {
275
+ assignMIDIPort(trackNum, e.messageData[0]);
276
+ continue;
277
+ }
278
+ // don't clear meta
279
+ if (e.messageStatusByte <= messageTypes.sequenceSpecific && e.messageStatusByte >= messageTypes.sequenceNumber)
280
+ {
281
+ continue;
282
+ }
283
+ const status = e.messageStatusByte & 0xF0;
284
+ const midiChannel = e.messageStatusByte & 0xF;
285
+ const channel = midiChannel + portOffset;
286
+ // clear channel?
287
+ if (desiredChannelsToClear.indexOf(channel) !== -1)
288
+ {
289
+ deleteThisEvent();
290
+ continue;
291
+ }
292
+ switch (status)
293
+ {
294
+ case messageTypes.noteOn:
295
+ // is it first?
296
+ if (isFirstNoteOn[channel])
297
+ {
298
+ isFirstNoteOn[channel] = false;
299
+ // all right, so this is the first note on
300
+ // first: controllers
301
+ // because FSMP does not like program changes after cc changes in embedded midis
302
+ // and since we use splice,
303
+ // controllers get added first, then programs before them
304
+ // now add controllers
305
+ desiredControllerChanges.filter(c => c.channel === channel).forEach(change =>
306
+ {
307
+ const ccChange = getControllerChange(
308
+ midiChannel,
309
+ change.controllerNumber,
310
+ change.controllerValue,
311
+ e.ticks
312
+ );
313
+ addEventBefore(ccChange);
314
+ });
315
+ const fineTune = fineTranspose[channel];
316
+
317
+ if (fineTune !== 0)
318
+ {
319
+ // add rpn
320
+ // 64 is the center, 96 = 50 cents up
321
+ const centsCoarse = (fineTune * 64) + 64;
322
+ const rpnCoarse = getControllerChange(midiChannel, midiControllers.RPNMsb, 0, e.ticks);
323
+ const rpnFine = getControllerChange(midiChannel, midiControllers.RPNLsb, 1, e.ticks);
324
+ const dataEntryCoarse = getControllerChange(
325
+ channel,
326
+ midiControllers.dataEntryMsb,
327
+ centsCoarse,
328
+ e.ticks
329
+ );
330
+ const dataEntryFine = getControllerChange(
331
+ midiChannel,
332
+ midiControllers.lsbForControl6DataEntry,
333
+ 0,
334
+ e.ticks
335
+ );
336
+ addEventBefore(dataEntryFine);
337
+ addEventBefore(dataEntryCoarse);
338
+ addEventBefore(rpnFine);
339
+ addEventBefore(rpnCoarse);
340
+
341
+ }
342
+
343
+ if (channelsToChangeProgram.has(channel))
344
+ {
345
+ const change = desiredProgramChanges.find(c => c.channel === channel);
346
+ let desiredBank = Math.max(0, Math.min(change.bank, 127));
347
+ const desiredProgram = change.program;
348
+ SpessaSynthInfo(
349
+ `%cSetting %c${change.channel}%c to %c${desiredBank}:${desiredProgram}%c. Track num: %c${trackNum}`,
350
+ consoleColors.info,
351
+ consoleColors.recognized,
352
+ consoleColors.info,
353
+ consoleColors.recognized,
354
+ consoleColors.info,
355
+ consoleColors.recognized
356
+ );
357
+
358
+ // note: this is in reverse.
359
+ // the output event order is: drums -> lsb -> msb -> program change
360
+
361
+ // add program change
362
+ const programChange = new MIDIMessage(
363
+ e.ticks,
364
+ messageTypes.programChange | midiChannel,
365
+ new IndexedByteArray([
366
+ desiredProgram
367
+ ])
368
+ );
369
+ addEventBefore(programChange);
370
+
371
+ const addBank = (isLSB, v) =>
372
+ {
373
+ const bankChange = getControllerChange(
374
+ midiChannel,
375
+ isLSB ? midiControllers.lsbForControl0BankSelect : midiControllers.bankSelect,
376
+ v,
377
+ e.ticks
378
+ );
379
+ addEventBefore(bankChange);
380
+ };
381
+
382
+ // on xg, add lsb
383
+ if (isSystemXG(system))
384
+ {
385
+ // xg drums: msb can be 120, 126 or 127
386
+ if (change.isDrum)
387
+ {
388
+ SpessaSynthInfo(
389
+ `%cAdding XG Drum change on track %c${trackNum}`,
390
+ consoleColors.recognized,
391
+ consoleColors.value
392
+ );
393
+ addBank(false, isXGDrums(desiredBank) ? desiredBank : 127);
394
+ addBank(true, 0);
395
+ }
396
+ else
397
+ {
398
+ // sfx voice is set via MSB
399
+ if (desiredBank === XG_SFX_VOICE)
400
+ {
401
+ addBank(false, XG_SFX_VOICE);
402
+ addBank(true, 0);
403
+ }
404
+ else
405
+ {
406
+ // add variation as LSB
407
+ addBank(false, 0);
408
+ addBank(true, desiredBank);
409
+ }
410
+ }
411
+ }
412
+ else
413
+ {
414
+ // add just msb
415
+ addBank(false, desiredBank);
416
+
417
+ if (change.isDrum && midiChannel !== DEFAULT_PERCUSSION)
418
+ {
419
+ // add gs drum change
420
+ SpessaSynthInfo(
421
+ `%cAdding GS Drum change on track %c${trackNum}`,
422
+ consoleColors.recognized,
423
+ consoleColors.value
424
+ );
425
+ addEventBefore(getDrumChange(midiChannel, e.ticks));
426
+ }
427
+ }
428
+ }
429
+ }
430
+ // transpose key (for zero it won't change anyway)
431
+ e.messageData[0] += coarseTranspose[channel];
432
+ break;
433
+
434
+ case messageTypes.noteOff:
435
+ e.messageData[0] += coarseTranspose[channel];
436
+ break;
437
+
438
+ case messageTypes.programChange:
439
+ // do we delete it?
440
+ if (channelsToChangeProgram.has(channel))
441
+ {
442
+ // this channel has program change. BEGONE!
443
+ deleteThisEvent();
444
+ continue;
445
+ }
446
+ break;
447
+
448
+ case messageTypes.controllerChange:
449
+ const ccNum = e.messageData[0];
450
+ const changes = desiredControllerChanges.find(c => c.channel === channel && ccNum === c.controllerNumber);
451
+ if (changes !== undefined)
452
+ {
453
+ // this controller is locked, BEGONE CHANGE!
454
+ deleteThisEvent();
455
+ continue;
456
+ }
457
+ // bank maybe?
458
+ if (ccNum === midiControllers.bankSelect || ccNum === midiControllers.lsbForControl0BankSelect)
459
+ {
460
+ if (channelsToChangeProgram.has(channel))
461
+ {
462
+ // BEGONE!
463
+ deleteThisEvent();
464
+ continue;
465
+ }
466
+ }
467
+ break;
468
+
469
+ case messageTypes.systemExclusive:
470
+ // check for xg on
471
+ if (isXGOn(e))
472
+ {
473
+ SpessaSynthInfo("%cXG system on detected", consoleColors.info);
474
+ system = "xg";
475
+ addedGs = true; // flag as true so gs won't get added
476
+ }
477
+ else
478
+ // check for xg program change
479
+ if (
480
+ e.messageData[0] === 0x43 // yamaha
481
+ && e.messageData[2] === 0x4C // XG
482
+ && e.messageData[3] === 0x08 // part parameter
483
+ && e.messageData[5] === 0x03 // program change
484
+ )
485
+ {
486
+ // do we delete it?
487
+ if (channelsToChangeProgram.has(e.messageData[4] + portOffset))
488
+ {
489
+ // this channel has program change. BEGONE!
490
+ deleteThisEvent();
491
+ }
492
+ }
493
+ else
494
+ // check for GS on
495
+ if (isGSOn(e))
496
+ {
497
+ // that's a GS on, we're done here
498
+ addedGs = true;
499
+ SpessaSynthInfo(
500
+ "%cGS on detected!",
501
+ consoleColors.recognized
502
+ );
503
+ break;
504
+ }
505
+ else
506
+ // check for GM/2 on
507
+ if (isGMOn(e) || isGM2On(e))
508
+ {
509
+ // that's a GM1 system change, remove it!
510
+ SpessaSynthInfo(
511
+ "%cGM/2 on detected, removing!",
512
+ consoleColors.info
513
+ );
514
+ deleteThisEvent();
515
+ addedGs = false;
516
+ }
517
+ }
518
+ }
519
+ // check for gs
520
+ if (!addedGs && desiredProgramChanges.length > 0)
521
+ {
522
+ // gs is not on, add it on the first track at index 0 (or 1 if track name is first)
523
+ let index = 0;
524
+ if (midi.tracks[0][0].messageStatusByte === messageTypes.trackName)
525
+ {
526
+ index++;
527
+ }
528
+ midi.tracks[0].splice(index, 0, getGsOn(0));
529
+ SpessaSynthInfo("%cGS on not detected. Adding it.", consoleColors.info);
530
+ }
531
+ this.flush();
532
+ SpessaSynthGroupEnd();
533
+ }
534
+
535
+ /**
536
+ * Modifies the sequence according to the locked presets and controllers in the given snapshot
537
+ * @this {BasicMIDI}
538
+ * @param snapshot {SynthesizerSnapshot}
539
+ */
540
+ export function applySnapshotToMIDI(snapshot)
541
+ {
542
+ /**
543
+ * @type {{
544
+ * channel: number,
545
+ * keyShift: number
546
+ * }[]}
547
+ */
548
+ const channelsToTranspose = [];
549
+ /**
550
+ * @type {number[]}
551
+ */
552
+ const channelsToClear = [];
553
+ /**
554
+ * @type {{
555
+ * channel: number,
556
+ * program: number,
557
+ * bank: number,
558
+ * isDrum: boolean
559
+ * }[]}
560
+ */
561
+ const programChanges = [];
562
+ /**
563
+ *
564
+ * @type {{
565
+ * channel: number,
566
+ * controllerNumber: number,
567
+ * controllerValue: number
568
+ * }[]}
569
+ */
570
+ const controllerChanges = [];
571
+ snapshot.channelSnapshots.forEach((channel, channelNumber) =>
572
+ {
573
+ if (channel.isMuted)
574
+ {
575
+ channelsToClear.push(channelNumber);
576
+ return;
577
+ }
578
+ const transposeFloat = channel.channelTransposeKeyShift + channel.customControllers[customControllers.channelTransposeFine] / 100;
579
+ if (transposeFloat !== 0)
580
+ {
581
+ channelsToTranspose.push({
582
+ channel: channelNumber,
583
+ keyShift: transposeFloat
584
+ });
585
+ }
586
+ if (channel.lockPreset)
587
+ {
588
+ programChanges.push({
589
+ channel: channelNumber,
590
+ program: channel.program,
591
+ bank: channel.bank,
592
+ isDrum: channel.drumChannel
593
+ });
594
+ }
595
+ // check for locked controllers and change them appropriately
596
+ channel.lockedControllers.forEach((l, ccNumber) =>
597
+ {
598
+ if (!l || ccNumber > 127 || ccNumber === midiControllers.bankSelect)
599
+ {
600
+ return;
601
+ }
602
+ const targetValue = channel.midiControllers[ccNumber] >> 7; // channel controllers are stored as 14 bit values
603
+ controllerChanges.push({
604
+ channel: channelNumber,
605
+ controllerNumber: ccNumber,
606
+ controllerValue: targetValue
607
+ });
608
+ });
609
+ });
610
+ this.modifyMIDI(programChanges, controllerChanges, channelsToClear, channelsToTranspose);
611
+ }
spessasynth_lib/midi_parser/midi_loader.js ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { dataBytesAmount, getChannel, MIDIMessage } from "./midi_message.js";
2
+ import { IndexedByteArray } from "../utils/indexed_array.js";
3
+ import { consoleColors } from "../utils/other.js";
4
+ import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn } from "../utils/loggin.js";
5
+ import { readRIFFChunk } from "../soundfont/basic_soundfont/riff_chunk.js";
6
+ import { readVariableLengthQuantity } from "../utils/byte_functions/variable_length_quantity.js";
7
+ import { readBytesAsUintBigEndian } from "../utils/byte_functions/big_endian.js";
8
+ import { readBytesAsString } from "../utils/byte_functions/string.js";
9
+ import { readLittleEndian } from "../utils/byte_functions/little_endian.js";
10
+ import { RMIDINFOChunks } from "./rmidi_writer.js";
11
+ import { BasicMIDI } from "./basic_midi.js";
12
+ import { loadXMF } from "./xmf_loader.js";
13
+
14
+ /**
15
+ * midi_loader.js
16
+ * purpose:
17
+ * parses a midi file for the seqyencer,
18
+ * including things like marker or CC 2/4 loop detection, copyright detection, etc.
19
+ */
20
+
21
+ /**
22
+ * The MIDI class is a MIDI file parser that reads a MIDI file and extracts all the necessary information from it.
23
+ * Supported formats are .mid and .rmi files.
24
+ */
25
+ class MIDI extends BasicMIDI
26
+ {
27
+ /**
28
+ * Parses a given midi file
29
+ * @param arrayBuffer {ArrayBuffer}
30
+ * @param fileName {string} optional, replaces the decoded title if empty
31
+ */
32
+ constructor(arrayBuffer, fileName = "")
33
+ {
34
+ super();
35
+ SpessaSynthGroupCollapsed(`%cParsing MIDI File...`, consoleColors.info);
36
+ this.fileName = fileName;
37
+ const binaryData = new IndexedByteArray(arrayBuffer);
38
+ let fileByteArray;
39
+
40
+ // check for rmid
41
+ const initialString = readBytesAsString(binaryData, 4);
42
+ binaryData.currentIndex -= 4;
43
+ if (initialString === "RIFF")
44
+ {
45
+ // possibly an RMID file (https://github.com/spessasus/sf2-rmidi-specification#readme)
46
+ // skip size
47
+ binaryData.currentIndex += 8;
48
+ const rmid = readBytesAsString(binaryData, 4, undefined, false);
49
+ if (rmid !== "RMID")
50
+ {
51
+ SpessaSynthGroupEnd();
52
+ throw new SyntaxError(`Invalid RMIDI Header! Expected "RMID", got "${rmid}"`);
53
+ }
54
+ const riff = readRIFFChunk(binaryData);
55
+ if (riff.header !== "data")
56
+ {
57
+ SpessaSynthGroupEnd();
58
+ throw new SyntaxError(`Invalid RMIDI Chunk header! Expected "data", got "${rmid}"`);
59
+ }
60
+ // this is a rmid, load the midi into an array for parsing
61
+ fileByteArray = riff.chunkData;
62
+
63
+ // keep loading chunks until we get the "SFBK" header
64
+ while (binaryData.currentIndex <= binaryData.length)
65
+ {
66
+ const startIndex = binaryData.currentIndex;
67
+ const currentChunk = readRIFFChunk(binaryData, true);
68
+ if (currentChunk.header === "RIFF")
69
+ {
70
+ const type = readBytesAsString(currentChunk.chunkData, 4).toLowerCase();
71
+ if (type === "sfbk" || type === "sfpk" || type === "dls ")
72
+ {
73
+ SpessaSynthInfo("%cFound embedded soundfont!", consoleColors.recognized);
74
+ this.embeddedSoundFont = binaryData.slice(startIndex, startIndex + currentChunk.size).buffer;
75
+ }
76
+ else
77
+ {
78
+ SpessaSynthWarn(`Unknown RIFF chunk: "${type}"`);
79
+ }
80
+ if (type === "dls ")
81
+ {
82
+ // Assume bank offset of 0 by default. If we find any bank selects, then the offset is 1.
83
+ this.isDLSRMIDI = true;
84
+ }
85
+ }
86
+ else if (currentChunk.header === "LIST")
87
+ {
88
+ const type = readBytesAsString(currentChunk.chunkData, 4);
89
+ if (type === "INFO")
90
+ {
91
+ SpessaSynthInfo("%cFound RMIDI INFO chunk!", consoleColors.recognized);
92
+ this.RMIDInfo = {};
93
+ while (currentChunk.chunkData.currentIndex <= currentChunk.size)
94
+ {
95
+ const infoChunk = readRIFFChunk(currentChunk.chunkData, true);
96
+ this.RMIDInfo[infoChunk.header] = infoChunk.chunkData;
97
+ }
98
+ if (this.RMIDInfo["ICOP"])
99
+ {
100
+ // special case, overwrites the copyright components array
101
+ this.copyright = readBytesAsString(
102
+ this.RMIDInfo["ICOP"],
103
+ this.RMIDInfo["ICOP"].length,
104
+ undefined,
105
+ false
106
+ ).replaceAll("\n", " ");
107
+ }
108
+ if (this.RMIDInfo["INAM"])
109
+ {
110
+ this.rawMidiName = this.RMIDInfo[RMIDINFOChunks.name];
111
+ // noinspection JSCheckFunctionSignatures
112
+ this.midiName = readBytesAsString(
113
+ this.rawMidiName,
114
+ this.rawMidiName.length,
115
+ undefined,
116
+ false
117
+ ).replaceAll("\n", " ");
118
+ }
119
+ // these can be used interchangeably
120
+ if (this.RMIDInfo["IALB"] && !this.RMIDInfo["IPRD"])
121
+ {
122
+ this.RMIDInfo["IPRD"] = this.RMIDInfo["IALB"];
123
+ }
124
+ if (this.RMIDInfo["IPRD"] && !this.RMIDInfo["IALB"])
125
+ {
126
+ this.RMIDInfo["IALB"] = this.RMIDInfo["IPRD"];
127
+ }
128
+ this.bankOffset = 1; // defaults to 1
129
+ if (this.RMIDInfo[RMIDINFOChunks.bankOffset])
130
+ {
131
+ this.bankOffset = readLittleEndian(this.RMIDInfo[RMIDINFOChunks.bankOffset], 2);
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ if (this.isDLSRMIDI)
138
+ {
139
+ // Assume bank offset of 0 by default. If we find any bank selects, then the offset is 1.
140
+ this.bankOffset = 0;
141
+ }
142
+
143
+ // if no embedded bank, assume 0
144
+ if (this.embeddedSoundFont === undefined)
145
+ {
146
+ this.bankOffset = 0;
147
+ }
148
+ }
149
+ else if (initialString === "XMF_")
150
+ {
151
+ // XMF file
152
+ fileByteArray = loadXMF(this, binaryData);
153
+ }
154
+ else
155
+ {
156
+ fileByteArray = binaryData;
157
+ }
158
+ const headerChunk = this._readMIDIChunk(fileByteArray);
159
+ if (headerChunk.type !== "MThd")
160
+ {
161
+ SpessaSynthGroupEnd();
162
+ throw new SyntaxError(`Invalid MIDI Header! Expected "MThd", got "${headerChunk.type}"`);
163
+ }
164
+
165
+ if (headerChunk.size !== 6)
166
+ {
167
+ SpessaSynthGroupEnd();
168
+ throw new RangeError(`Invalid MIDI header chunk size! Expected 6, got ${headerChunk.size}`);
169
+ }
170
+
171
+ // format
172
+ this.format = readBytesAsUintBigEndian(headerChunk.data, 2);
173
+ // tracks count
174
+ this.tracksAmount = readBytesAsUintBigEndian(headerChunk.data, 2);
175
+ // time division
176
+ this.timeDivision = readBytesAsUintBigEndian(headerChunk.data, 2);
177
+ // read all the tracks
178
+ for (let i = 0; i < this.tracksAmount; i++)
179
+ {
180
+ /**
181
+ * @type {MIDIMessage[]}
182
+ */
183
+ const track = [];
184
+ const trackChunk = this._readMIDIChunk(fileByteArray);
185
+
186
+ if (trackChunk.type !== "MTrk")
187
+ {
188
+ SpessaSynthGroupEnd();
189
+ throw new SyntaxError(`Invalid track header! Expected "MTrk" got "${trackChunk.type}"`);
190
+ }
191
+
192
+
193
+ /**
194
+ * MIDI running byte
195
+ * @type {number}
196
+ */
197
+ let runningByte = undefined;
198
+
199
+ let totalTicks = 0;
200
+ // format 2 plays sequentially
201
+ if (this.format === 2 && i > 0)
202
+ {
203
+ totalTicks += this.tracks[i - 1][this.tracks[i - 1].length - 1].ticks;
204
+ }
205
+ // loop until we reach the end of track
206
+ while (trackChunk.data.currentIndex < trackChunk.size)
207
+ {
208
+ totalTicks += readVariableLengthQuantity(trackChunk.data);
209
+
210
+ // check if the status byte is valid (IE. larger than 127)
211
+ const statusByteCheck = trackChunk.data[trackChunk.data.currentIndex];
212
+
213
+ let statusByte;
214
+ // if we have a running byte and the status byte isn't valid
215
+ if (runningByte !== undefined && statusByteCheck < 0x80)
216
+ {
217
+ statusByte = runningByte;
218
+ }
219
+ else
220
+ { // noinspection PointlessBooleanExpressionJS
221
+ if (runningByte === undefined && statusByteCheck < 0x80)
222
+ {
223
+ // if we don't have a running byte and the status byte isn't valid, it's an error.
224
+ SpessaSynthGroupEnd();
225
+ throw new SyntaxError(`Unexpected byte with no running byte. (${statusByteCheck})`);
226
+ }
227
+ else
228
+ {
229
+ // if the status byte is valid, use that
230
+ statusByte = trackChunk.data[trackChunk.data.currentIndex++];
231
+ }
232
+ }
233
+ const statusByteChannel = getChannel(statusByte);
234
+
235
+ let eventDataLength;
236
+
237
+ // determine the message's length;
238
+ switch (statusByteChannel)
239
+ {
240
+ case -1:
241
+ // system common/realtime (no length)
242
+ eventDataLength = 0;
243
+ break;
244
+
245
+ case -2:
246
+ // meta (the next is the actual status byte)
247
+ statusByte = trackChunk.data[trackChunk.data.currentIndex++];
248
+ eventDataLength = readVariableLengthQuantity(trackChunk.data);
249
+ break;
250
+
251
+ case -3:
252
+ // sysex
253
+ eventDataLength = readVariableLengthQuantity(trackChunk.data);
254
+ break;
255
+
256
+ default:
257
+ // voice message
258
+ // gets the midi message length
259
+ eventDataLength = dataBytesAmount[statusByte >> 4];
260
+ // save the status byte
261
+ runningByte = statusByte;
262
+ break;
263
+ }
264
+
265
+ // put the event data into the array
266
+ const eventData = new IndexedByteArray(eventDataLength);
267
+ eventData.set(trackChunk.data.slice(
268
+ trackChunk.data.currentIndex,
269
+ trackChunk.data.currentIndex + eventDataLength
270
+ ), 0);
271
+ const event = new MIDIMessage(totalTicks, statusByte, eventData);
272
+ track.push(event);
273
+ // advance the track chunk
274
+ trackChunk.data.currentIndex += eventDataLength;
275
+ }
276
+ this.tracks.push(track);
277
+
278
+ SpessaSynthInfo(
279
+ `%cParsed %c${this.tracks.length}%c / %c${this.tracksAmount}`,
280
+ consoleColors.info,
281
+ consoleColors.value,
282
+ consoleColors.info,
283
+ consoleColors.value
284
+ );
285
+ }
286
+
287
+ SpessaSynthInfo(
288
+ `%cAll tracks parsed correctly!`,
289
+ consoleColors.recognized
290
+ );
291
+ // parse the events
292
+ this._parseInternal();
293
+ SpessaSynthGroupEnd();
294
+ SpessaSynthInfo(
295
+ `%cMIDI file parsed. Total tick time: %c${this.lastVoiceEventTick}%c, total seconds time: %c${this.duration}`,
296
+ consoleColors.info,
297
+ consoleColors.recognized,
298
+ consoleColors.info,
299
+ consoleColors.recognized
300
+ );
301
+ }
302
+
303
+ /**
304
+ * @param fileByteArray {IndexedByteArray}
305
+ * @returns {{type: string, size: number, data: IndexedByteArray}}
306
+ * @private
307
+ */
308
+ _readMIDIChunk(fileByteArray)
309
+ {
310
+ const chunk = {};
311
+ // type
312
+ chunk.type = readBytesAsString(fileByteArray, 4);
313
+ // size
314
+ chunk.size = readBytesAsUintBigEndian(fileByteArray, 4);
315
+ // data
316
+ chunk.data = new IndexedByteArray(chunk.size);
317
+ const dataSlice = fileByteArray.slice(fileByteArray.currentIndex, fileByteArray.currentIndex + chunk.size);
318
+ chunk.data.set(dataSlice, 0);
319
+ fileByteArray.currentIndex += chunk.size;
320
+ return chunk;
321
+ }
322
+ }
323
+
324
+ export { MIDI };
spessasynth_lib/midi_parser/midi_message.js ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IndexedByteArray } from "../utils/indexed_array.js";
2
+
3
+ /**
4
+ * midi_message.js
5
+ * purpose: contains enums for midi events and controllers and functions to parse them
6
+ */
7
+
8
+ export class MIDIMessage
9
+ {
10
+ /**
11
+ * Absolute number of MIDI ticks from the start of the track.
12
+ * @type {number}
13
+ */
14
+ ticks;
15
+
16
+ /**
17
+ * The MIDI message status byte. Note that for meta events, it is the second byte. (not 0xFF)
18
+ * @type {number}
19
+ */
20
+ messageStatusByte;
21
+
22
+ /**
23
+ * Message's binary data
24
+ * @type {IndexedByteArray}
25
+ */
26
+ messageData;
27
+
28
+ /**
29
+ * @param ticks {number}
30
+ * @param byte {number} the message status byte
31
+ * @param data {IndexedByteArray}
32
+ */
33
+ constructor(ticks, byte, data)
34
+ {
35
+ this.ticks = ticks;
36
+ this.messageStatusByte = byte;
37
+ this.messageData = data;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Gets the status byte's channel
43
+ * @param statusByte
44
+ * @returns {number} channel is -1 for system messages -2 for meta and -3 for sysex
45
+ */
46
+ export function getChannel(statusByte)
47
+ {
48
+ const eventType = statusByte & 0xF0;
49
+ const channel = statusByte & 0x0F;
50
+
51
+ let resultChannel = channel;
52
+
53
+ switch (eventType)
54
+ {
55
+ // midi (and meta and sysex headers)
56
+ case 0x80:
57
+ case 0x90:
58
+ case 0xA0:
59
+ case 0xB0:
60
+ case 0xC0:
61
+ case 0xD0:
62
+ case 0xE0:
63
+ break;
64
+
65
+ case 0xF0:
66
+ switch (channel)
67
+ {
68
+ case 0x0:
69
+ resultChannel = -3;
70
+ break;
71
+
72
+ case 0x1:
73
+ case 0x2:
74
+ case 0x3:
75
+ case 0x4:
76
+ case 0x5:
77
+ case 0x6:
78
+ case 0x7:
79
+ case 0x8:
80
+ case 0x9:
81
+ case 0xA:
82
+ case 0xB:
83
+ case 0xC:
84
+ case 0xD:
85
+ case 0xE:
86
+ resultChannel = -1;
87
+ break;
88
+
89
+ case 0xF:
90
+ resultChannel = -2;
91
+ break;
92
+ }
93
+ break;
94
+
95
+ default:
96
+ resultChannel = -1;
97
+ }
98
+
99
+ return resultChannel;
100
+ }
101
+
102
+ // all the midi statuses dictionary
103
+ export const messageTypes = {
104
+ noteOff: 0x80,
105
+ noteOn: 0x90,
106
+ polyPressure: 0xA0,
107
+ controllerChange: 0xB0,
108
+ programChange: 0xC0,
109
+ channelPressure: 0xD0,
110
+ pitchBend: 0xE0,
111
+ systemExclusive: 0xF0,
112
+ timecode: 0xF1,
113
+ songPosition: 0xF2,
114
+ songSelect: 0xF3,
115
+ tuneRequest: 0xF6,
116
+ clock: 0xF8,
117
+ start: 0xFA,
118
+ continue: 0xFB,
119
+ stop: 0xFC,
120
+ activeSensing: 0xFE,
121
+ reset: 0xFF,
122
+ sequenceNumber: 0x00,
123
+ text: 0x01,
124
+ copyright: 0x02,
125
+ trackName: 0x03,
126
+ instrumentName: 0x04,
127
+ lyric: 0x05,
128
+ marker: 0x06,
129
+ cuePoint: 0x07,
130
+ programName: 0x08,
131
+ midiChannelPrefix: 0x20,
132
+ midiPort: 0x21,
133
+ endOfTrack: 0x2F,
134
+ setTempo: 0x51,
135
+ smpteOffset: 0x54,
136
+ timeSignature: 0x58,
137
+ keySignature: 0x59,
138
+ sequenceSpecific: 0x7F
139
+ };
140
+
141
+
142
+ /**
143
+ * Gets the event's status and channel from the status byte
144
+ * @param statusByte {number} the status byte
145
+ * @returns {{channel: number, status: number}} channel will be -1 for sysex and meta
146
+ */
147
+ export function getEvent(statusByte)
148
+ {
149
+ const status = statusByte & 0xF0;
150
+ const channel = statusByte & 0x0F;
151
+
152
+ let eventChannel = -1;
153
+ let eventStatus = statusByte;
154
+
155
+ if (status >= 0x80 && status <= 0xE0)
156
+ {
157
+ eventChannel = channel;
158
+ eventStatus = status;
159
+ }
160
+
161
+ return {
162
+ status: eventStatus,
163
+ channel: eventChannel
164
+ };
165
+ }
166
+
167
+
168
+ /**
169
+ * @enum {number}
170
+ */
171
+ export const midiControllers = {
172
+ bankSelect: 0,
173
+ modulationWheel: 1,
174
+ breathController: 2,
175
+ footController: 4,
176
+ portamentoTime: 5,
177
+ dataEntryMsb: 6,
178
+ mainVolume: 7,
179
+ balance: 8,
180
+ pan: 10,
181
+ expressionController: 11,
182
+ effectControl1: 12,
183
+ effectControl2: 13,
184
+ generalPurposeController1: 16,
185
+ generalPurposeController2: 17,
186
+ generalPurposeController3: 18,
187
+ generalPurposeController4: 19,
188
+ lsbForControl0BankSelect: 32,
189
+ lsbForControl1ModulationWheel: 33,
190
+ lsbForControl2BreathController: 34,
191
+ lsbForControl4FootController: 36,
192
+ lsbForControl5PortamentoTime: 37,
193
+ lsbForControl6DataEntry: 38,
194
+ lsbForControl7MainVolume: 39,
195
+ lsbForControl8Balance: 40,
196
+ lsbForControl10Pan: 42,
197
+ lsbForControl11ExpressionController: 43,
198
+ lsbForControl12EffectControl1: 44,
199
+ lsbForControl13EffectControl2: 45,
200
+ sustainPedal: 64,
201
+ portamentoOnOff: 65,
202
+ sostenutoPedal: 66,
203
+ softPedal: 67,
204
+ legatoFootswitch: 68,
205
+ hold2Pedal: 69,
206
+ soundVariation: 70,
207
+ filterResonance: 71,
208
+ releaseTime: 72,
209
+ attackTime: 73,
210
+ brightness: 74,
211
+ decayTime: 75,
212
+ vibratoRate: 76,
213
+ vibratoDepth: 77,
214
+ vibratoDelay: 78,
215
+ soundController10: 79,
216
+ generalPurposeController5: 80,
217
+ generalPurposeController6: 81,
218
+ generalPurposeController7: 82,
219
+ generalPurposeController8: 83,
220
+ portamentoControl: 84,
221
+ reverbDepth: 91,
222
+ tremoloDepth: 92,
223
+ chorusDepth: 93,
224
+ detuneDepth: 94,
225
+ phaserDepth: 95,
226
+ dataIncrement: 96,
227
+ dataDecrement: 97,
228
+ NRPNLsb: 98,
229
+ NRPNMsb: 99,
230
+ RPNLsb: 100,
231
+ RPNMsb: 101,
232
+ allSoundOff: 120,
233
+ resetAllControllers: 121,
234
+ localControlOnOff: 122,
235
+ allNotesOff: 123,
236
+ omniModeOff: 124,
237
+ omniModeOn: 125,
238
+ monoModeOn: 126,
239
+ polyModeOn: 127
240
+ };
241
+
242
+
243
+ /**
244
+ * @type {{"11": number, "12": number, "13": number, "14": number, "8": number, "9": number, "10": number}}
245
+ */
246
+ export const dataBytesAmount = {
247
+ 0x8: 2, // note off
248
+ 0x9: 2, // note on
249
+ 0xA: 2, // note at
250
+ 0xB: 2, // cc change
251
+ 0xC: 1, // pg change
252
+ 0xD: 1, // channel after touch
253
+ 0xE: 2 // pitch wheel
254
+ };
spessasynth_lib/midi_parser/midi_sequence.js ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * This is the base type for MIDI files. It contains all the "metadata" and information.
3
+ * It extends to:
4
+ * - BasicMIDI, which contains the actual track data of the MIDI file. Essentially the MIDI file itself.
5
+ * - MIDIData, which contains all properties that MIDI does, except for tracks and the embedded soundfont.
6
+ * MIDIData is the "shell" of the file which is available on the main thread at all times, containing the metadata.
7
+ */
8
+ class MIDISequenceData
9
+ {
10
+ /**
11
+ * The time division of the sequence, representing the number of ticks per beat.
12
+ * @type {number}
13
+ */
14
+ timeDivision = 0;
15
+
16
+ /**
17
+ * The duration of the sequence, in seconds.
18
+ * @type {number}
19
+ */
20
+ duration = 0;
21
+
22
+ /**
23
+ * The tempo changes in the sequence, ordered from the last change to the first.
24
+ * Each change is represented by an object with a tick position and a tempo value in beats per minute.
25
+ * @type {{ticks: number, tempo: number}[]}
26
+ */
27
+ tempoChanges = [{ ticks: 0, tempo: 120 }];
28
+
29
+ /**
30
+ * A string containing the copyright information for the MIDI sequence if detected.
31
+ * @type {string}
32
+ */
33
+ copyright = "";
34
+
35
+ /**
36
+ * The number of tracks in the MIDI sequence.
37
+ * @type {number}
38
+ */
39
+ tracksAmount = 0;
40
+
41
+ /**
42
+ * The track names in the MIDI file, an empty string if not set.
43
+ * @type {string[]}
44
+ */
45
+ trackNames = [];
46
+
47
+ /**
48
+ * An array containing the lyrics of the sequence, stored as binary chunks (Uint8Array).
49
+ * @type {Uint8Array[]}
50
+ */
51
+ lyrics = [];
52
+
53
+ /**
54
+ * An array of tick positions where lyrics events occur in the sequence.
55
+ * @type {number[]}
56
+ */
57
+ lyricsTicks = [];
58
+
59
+ /**
60
+ * The tick position of the first note-on event in the MIDI sequence.
61
+ * @type {number}
62
+ */
63
+ firstNoteOn = 0;
64
+
65
+ /**
66
+ * The MIDI key range used in the sequence, represented by a minimum and maximum note value.
67
+ * @type {{min: number, max: number}}
68
+ */
69
+ keyRange = { min: 0, max: 127 };
70
+
71
+ /**
72
+ * The tick position of the last voice event (such as note-on, note-off, or control change) in the sequence.
73
+ * @type {number}
74
+ */
75
+ lastVoiceEventTick = 0;
76
+
77
+ /**
78
+ * An array of MIDI port numbers used by each track in the sequence.
79
+ * @type {number[]}
80
+ */
81
+ midiPorts = [0];
82
+
83
+ /**
84
+ * An array of channel offsets for each MIDI port, using the SpessaSynth method.
85
+ * @type {number[]}
86
+ */
87
+ midiPortChannelOffsets = [0];
88
+
89
+ /**
90
+ * A list of sets, where each set contains the MIDI channels used by each track in the sequence.
91
+ * @type {Set<number>[]}
92
+ */
93
+ usedChannelsOnTrack = [];
94
+
95
+ /**
96
+ * The loop points (in ticks) of the sequence, including both start and end points.
97
+ * @type {{start: number, end: number}}
98
+ */
99
+ loop = { start: 0, end: 0 };
100
+
101
+ /**
102
+ * The name of the MIDI sequence.
103
+ * @type {string}
104
+ */
105
+ midiName = "";
106
+
107
+ /**
108
+ * A boolean indicating if the sequence's name is the same as the file name.
109
+ * @type {boolean}
110
+ */
111
+ midiNameUsesFileName = false;
112
+
113
+ /**
114
+ * The file name of the MIDI sequence, if provided during parsing.
115
+ * @type {string}
116
+ */
117
+ fileName = "";
118
+
119
+ /**
120
+ * The raw, encoded MIDI name, represented as a Uint8Array.
121
+ * Useful when the MIDI file uses a different code page.
122
+ * @type {Uint8Array}
123
+ */
124
+ rawMidiName;
125
+
126
+ /**
127
+ * The format of the MIDI file, which can be 0, 1, or 2, indicating the type of the MIDI file.
128
+ * @type {number}
129
+ */
130
+ format = 0;
131
+
132
+ /**
133
+ * The RMID (Resource-Interchangeable MIDI) info data, if the file is RMID formatted.
134
+ * Otherwise, this field is undefined.
135
+ * Chunk type (e.g. "INAM"): Chunk data as a binary array.
136
+ * @type {Object<string, IndexedByteArray>}
137
+ */
138
+ RMIDInfo = {};
139
+
140
+ /**
141
+ * The bank offset used for RMID files.
142
+ * @type {number}
143
+ */
144
+ bankOffset = 0;
145
+
146
+ /**
147
+ * If the MIDI file is a Soft Karaoke file (.kar), this flag is set to true.
148
+ * https://www.mixagesoftware.com/en/midikit/help/HTML/karaoke_formats.html
149
+ * @type {boolean}
150
+ */
151
+ isKaraokeFile = false;
152
+
153
+ /**
154
+ * Indicates if this file is a Multi-Port MIDI file.
155
+ * @type {boolean}
156
+ */
157
+ isMultiPort = false;
158
+
159
+ /**
160
+ * Converts ticks to time in seconds
161
+ * @param ticks {number} time in MIDI ticks
162
+ * @returns {number} time in seconds
163
+ */
164
+ MIDIticksToSeconds(ticks)
165
+ {
166
+ let totalSeconds = 0;
167
+
168
+ while (ticks > 0)
169
+ {
170
+ // tempo changes are reversed, so the first element is the last tempo change
171
+ // and the last element is the first tempo change
172
+ // (always at tick 0 and tempo 120)
173
+ // find the last tempo change that has occurred
174
+ let tempo = this.tempoChanges.find(v => v.ticks < ticks);
175
+
176
+ // calculate the difference and tempo time
177
+ let timeSinceLastTempo = ticks - tempo.ticks;
178
+ totalSeconds += (timeSinceLastTempo * 60) / (tempo.tempo * this.timeDivision);
179
+ ticks -= timeSinceLastTempo;
180
+ }
181
+
182
+ return totalSeconds;
183
+ }
184
+
185
+ /**
186
+ * INTERNAL USE ONLY!
187
+ * DO NOT USE IN SPESSASYNTH_LIB
188
+ * @param sequence {MIDISequenceData}
189
+ * @protected
190
+ */
191
+ _copyFromSequence(sequence)
192
+ {
193
+ // properties can be assigned
194
+ this.midiName = sequence.midiName;
195
+ this.midiNameUsesFileName = sequence.midiNameUsesFileName;
196
+ this.fileName = sequence.fileName;
197
+ this.timeDivision = sequence.timeDivision;
198
+ this.duration = sequence.duration;
199
+ this.copyright = sequence.copyright;
200
+ this.tracksAmount = sequence.tracksAmount;
201
+ this.firstNoteOn = sequence.firstNoteOn;
202
+ this.lastVoiceEventTick = sequence.lastVoiceEventTick;
203
+ this.format = sequence.format;
204
+ this.bankOffset = sequence.bankOffset;
205
+ this.isKaraokeFile = sequence.isKaraokeFile;
206
+ this.isMultiPort = sequence.isMultiPort;
207
+
208
+ // copying arrays
209
+ this.tempoChanges = [...sequence.tempoChanges];
210
+ this.lyrics = sequence.lyrics.map(arr => new Uint8Array(arr));
211
+ this.lyricsTicks = [...sequence.lyricsTicks];
212
+ this.midiPorts = [...sequence.midiPorts];
213
+ this.trackNames = [...sequence.trackNames];
214
+ this.midiPortChannelOffsets = [...sequence.midiPortChannelOffsets];
215
+ this.usedChannelsOnTrack = sequence.usedChannelsOnTrack.map(set => new Set(set));
216
+ this.rawMidiName = sequence.rawMidiName ? new Uint8Array(sequence.rawMidiName) : undefined;
217
+
218
+ // copying objects
219
+ this.loop = { ...sequence.loop };
220
+ this.keyRange = { ...sequence.keyRange };
221
+ this.RMIDInfo = { ...sequence.RMIDInfo };
222
+ }
223
+ }
224
+
225
+ export { MIDISequenceData };
spessasynth_lib/midi_parser/midi_writer.js ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { messageTypes } from "./midi_message.js";
2
+ import { writeVariableLengthQuantity } from "../utils/byte_functions/variable_length_quantity.js";
3
+ import { writeBytesAsUintBigEndian } from "../utils/byte_functions/big_endian.js";
4
+
5
+ /**
6
+ * Exports the midi as a standard MIDI file
7
+ * @this {BasicMIDI}
8
+ */
9
+ export function writeMIDI()
10
+ {
11
+ const midi = this;
12
+ if (!midi.tracks)
13
+ {
14
+ throw new Error("MIDI has no tracks!");
15
+ }
16
+ /**
17
+ * @type {Uint8Array[]}
18
+ */
19
+ const binaryTrackData = [];
20
+ for (const track of midi.tracks)
21
+ {
22
+ const binaryTrack = [];
23
+ let currentTick = 0;
24
+ let runningByte = undefined;
25
+ for (const event of track)
26
+ {
27
+ // Ticks stored in MIDI are absolute, but SMF wants relative. Convert them here.
28
+ const deltaTicks = event.ticks - currentTick;
29
+ /**
30
+ * @type {number[]}
31
+ */
32
+ let messageData;
33
+ // determine the message
34
+ if (event.messageStatusByte <= messageTypes.sequenceSpecific)
35
+ {
36
+ // this is a meta-message
37
+ // syntax is FF<type><length><data>
38
+ messageData = [0xff, event.messageStatusByte, ...writeVariableLengthQuantity(event.messageData.length), ...event.messageData];
39
+ }
40
+ else if (event.messageStatusByte === messageTypes.systemExclusive)
41
+ {
42
+ // this is a system exclusive message
43
+ // syntax is F0<length><data>
44
+ messageData = [0xf0, ...writeVariableLengthQuantity(event.messageData.length), ...event.messageData];
45
+ }
46
+ else
47
+ {
48
+ // this is a midi message
49
+ messageData = [];
50
+ if (runningByte !== event.messageStatusByte)
51
+ {
52
+ // Running byte was not the byte we want. Add the byte here.
53
+ runningByte = event.messageStatusByte;
54
+ // add the status byte to the midi
55
+ messageData.push(event.messageStatusByte);
56
+ }
57
+ // add the data
58
+ messageData.push(...event.messageData);
59
+ }
60
+ // write VLQ
61
+ binaryTrack.push(...writeVariableLengthQuantity(deltaTicks));
62
+ // write the message
63
+ binaryTrack.push(...messageData);
64
+ currentTick += deltaTicks;
65
+ }
66
+ binaryTrackData.push(new Uint8Array(binaryTrack));
67
+ }
68
+
69
+ /**
70
+ * @param text {string}
71
+ * @param arr {number[]}
72
+ */
73
+ function writeText(text, arr)
74
+ {
75
+ for (let i = 0; i < text.length; i++)
76
+ {
77
+ arr.push(text.charCodeAt(i));
78
+ }
79
+ }
80
+
81
+ // write the file
82
+ const binaryData = [];
83
+ // write header
84
+ writeText("MThd", binaryData); // MThd
85
+ binaryData.push(...writeBytesAsUintBigEndian(6, 4)); // length
86
+ binaryData.push(0, midi.format); // format
87
+ binaryData.push(...writeBytesAsUintBigEndian(midi.tracksAmount, 2)); // num tracks
88
+ binaryData.push(...writeBytesAsUintBigEndian(midi.timeDivision, 2)); // time division
89
+
90
+ // write tracks
91
+ for (const track of binaryTrackData)
92
+ {
93
+ // write track header
94
+ writeText("MTrk", binaryData); // MTrk
95
+ binaryData.push(...writeBytesAsUintBigEndian(track.length, 4)); // length
96
+ binaryData.push(...track); // write data
97
+ }
98
+ return new Uint8Array(binaryData);
99
+ }
spessasynth_lib/midi_parser/rmidi_writer.js ADDED
@@ -0,0 +1,567 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { combineArrays, IndexedByteArray } from "../utils/indexed_array.js";
2
+ import { writeRIFFOddSize } from "../soundfont/basic_soundfont/riff_chunk.js";
3
+ import { getStringBytes, getStringBytesZero } from "../utils/byte_functions/string.js";
4
+ import { messageTypes, midiControllers, MIDIMessage } from "./midi_message.js";
5
+ import { getGsOn } from "./midi_editor.js";
6
+ import { SpessaSynthGroup, SpessaSynthGroupEnd, SpessaSynthInfo } from "../utils/loggin.js";
7
+ import { consoleColors } from "../utils/other.js";
8
+ import { writeLittleEndian } from "../utils/byte_functions/little_endian.js";
9
+ import { DEFAULT_PERCUSSION } from "../synthetizer/synth_constants.js";
10
+ import { chooseBank, isSystemXG, parseBankSelect } from "../utils/xg_hacks.js";
11
+ import { isGM2On, isGMOn, isGSDrumsOn, isGSOn, isXGOn } from "../utils/sysex_detector.js";
12
+
13
+ /**
14
+ * @enum {string}
15
+ */
16
+ export const RMIDINFOChunks = {
17
+ name: "INAM",
18
+ album: "IPRD",
19
+ album2: "IALB",
20
+ artist: "IART",
21
+ genre: "IGNR",
22
+ picture: "IPIC",
23
+ copyright: "ICOP",
24
+ creationDate: "ICRD",
25
+ comment: "ICMT",
26
+ engineer: "IENG",
27
+ software: "ISFT",
28
+ encoding: "IENC",
29
+ midiEncoding: "MENC",
30
+ bankOffset: "DBNK"
31
+ };
32
+
33
+ const FORCED_ENCODING = "utf-8";
34
+ const DEFAULT_COPYRIGHT = "Created using SpessaSynth";
35
+
36
+ /**
37
+ * @typedef {Object} RMIDMetadata
38
+ * @property {string|undefined} name - the name of the file
39
+ * @property {string|undefined} engineer - the engineer who worked on the file
40
+ * @property {string|undefined} artist - the artist
41
+ * @property {string|undefined} album - the album
42
+ * @property {string|undefined} genre - the genre of the song
43
+ * @property {ArrayBuffer|undefined} picture - the image for the file (album cover)
44
+ * @property {string|undefined} comment - the coment of the file
45
+ * @property {string|undefined} creationDate - the creation date of the file
46
+ * @property {string|undefined} copyright - the copyright of the file
47
+ * @property {string|unescape} midiEncoding - the encoding of the inner MIDI file
48
+ */
49
+
50
+ /**
51
+ * Writes an RMIDI file
52
+ * @this {BasicMIDI}
53
+ * @param soundfontBinary {Uint8Array}
54
+ * @param soundfont {BasicSoundBank}
55
+ * @param bankOffset {number} the bank offset for RMIDI
56
+ * @param encoding {string} the encoding of the RMIDI info chunk
57
+ * @param metadata {RMIDMetadata} the metadata of the file. Optional. If provided, the encoding is forced to utf-8/
58
+ * @param correctBankOffset {boolean}
59
+ * @returns {IndexedByteArray}
60
+ */
61
+ export function writeRMIDI(
62
+ soundfontBinary,
63
+ soundfont,
64
+ bankOffset = 0,
65
+ encoding = "Shift_JIS",
66
+ metadata = {},
67
+ correctBankOffset = true
68
+ )
69
+ {
70
+ const mid = this;
71
+ SpessaSynthGroup("%cWriting the RMIDI File...", consoleColors.info);
72
+ SpessaSynthInfo(
73
+ `%cConfiguration: Bank offset: %c${bankOffset}%c, encoding: %c${encoding}`,
74
+ consoleColors.info,
75
+ consoleColors.value,
76
+ consoleColors.info,
77
+ consoleColors.value
78
+ );
79
+ SpessaSynthInfo("metadata", metadata);
80
+ SpessaSynthInfo("Initial bank offset", mid.bankOffset);
81
+ if (correctBankOffset)
82
+ {
83
+ // Add the offset to the bank.
84
+ // See https://github.com/spessasus/sf2-rmidi-specification#readme
85
+ // also fix presets that don't exist
86
+ // since midi player6 doesn't seem to default to 0 when non-existent...
87
+ let system = "gm";
88
+ /**
89
+ * The unwanted system messages such as gm/gm2 on
90
+ * @type {{tNum: number, e: MIDIMessage}[]}
91
+ */
92
+ let unwantedSystems = [];
93
+ /**
94
+ * indexes for tracks
95
+ * @type {number[]}
96
+ */
97
+ const eventIndexes = Array(mid.tracks.length).fill(0);
98
+ let remainingTracks = mid.tracks.length;
99
+
100
+ function findFirstEventIndex()
101
+ {
102
+ let index = 0;
103
+ let ticks = Infinity;
104
+ mid.tracks.forEach((track, i) =>
105
+ {
106
+ if (eventIndexes[i] >= track.length)
107
+ {
108
+ return;
109
+ }
110
+ if (track[eventIndexes[i]].ticks < ticks)
111
+ {
112
+ index = i;
113
+ ticks = track[eventIndexes[i]].ticks;
114
+ }
115
+ });
116
+ return index;
117
+ }
118
+
119
+ // it copies midiPorts everywhere else, but here 0 works so DO NOT CHANGE!
120
+ const ports = Array(mid.tracks.length).fill(0);
121
+ const channelsAmount = 16 + mid.midiPortChannelOffsets.reduce((max, cur) => cur > max ? cur : max);
122
+ /**
123
+ * @type {{
124
+ * program: number,
125
+ * drums: boolean,
126
+ * lastBank: MIDIMessage,
127
+ * lastBankLSB: MIDIMessage,
128
+ * hasBankSelect: boolean
129
+ * }[]}
130
+ */
131
+ const channelsInfo = [];
132
+ for (let i = 0; i < channelsAmount; i++)
133
+ {
134
+ channelsInfo.push({
135
+ program: 0,
136
+ drums: i % 16 === DEFAULT_PERCUSSION, // drums appear on 9 every 16 channels,
137
+ lastBank: undefined,
138
+ lastBankLSB: undefined,
139
+ hasBankSelect: false
140
+ });
141
+ }
142
+ while (remainingTracks > 0)
143
+ {
144
+ let trackNum = findFirstEventIndex();
145
+ const track = mid.tracks[trackNum];
146
+ if (eventIndexes[trackNum] >= track.length)
147
+ {
148
+ remainingTracks--;
149
+ continue;
150
+ }
151
+ const e = track[eventIndexes[trackNum]];
152
+ eventIndexes[trackNum]++;
153
+
154
+ let portOffset = mid.midiPortChannelOffsets[ports[trackNum]];
155
+ if (e.messageStatusByte === messageTypes.midiPort)
156
+ {
157
+ ports[trackNum] = e.messageData[0];
158
+ continue;
159
+ }
160
+ const status = e.messageStatusByte & 0xF0;
161
+ if (
162
+ status !== messageTypes.controllerChange &&
163
+ status !== messageTypes.programChange &&
164
+ status !== messageTypes.systemExclusive
165
+ )
166
+ {
167
+ continue;
168
+ }
169
+
170
+ if (status === messageTypes.systemExclusive)
171
+ {
172
+ // check for drum sysex
173
+ if (!isGSDrumsOn(e))
174
+ {
175
+ // check for XG
176
+ if (isXGOn(e))
177
+ {
178
+ system = "xg";
179
+ }
180
+ else if (isGSOn(e))
181
+ {
182
+ system = "gs";
183
+ }
184
+ else if (isGMOn(e))
185
+ {
186
+ // we do not want gm1
187
+ system = "gm";
188
+ unwantedSystems.push({
189
+ tNum: trackNum,
190
+ e: e
191
+ });
192
+ }
193
+ else if (isGM2On(e))
194
+ {
195
+ system = "gm2";
196
+ }
197
+ continue;
198
+ }
199
+ const sysexChannel = [9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15][e.messageData[5] & 0x0F] + portOffset;
200
+ channelsInfo[sysexChannel].drums = !!(e.messageData[7] > 0 && e.messageData[5] >> 4);
201
+ continue;
202
+ }
203
+
204
+ // program change
205
+ const chNum = (e.messageStatusByte & 0xF) + portOffset;
206
+ /**
207
+ * @type {{program: number, drums: boolean, lastBank: MIDIMessage, lastBankLSB: MIDIMessage, hasBankSelect: boolean}}
208
+ */
209
+ const channel = channelsInfo[chNum];
210
+ if (status === messageTypes.programChange)
211
+ {
212
+ const isXG = isSystemXG(system);
213
+ // check if the preset for this program exists
214
+ const initialProgram = e.messageData[0];
215
+ if (channel.drums)
216
+ {
217
+ if (soundfont.presets.findIndex(p => p.program === initialProgram && p.isDrumPreset(
218
+ isXG,
219
+ true
220
+ )) === -1)
221
+ {
222
+ // doesn't exist. pick any preset that has bank 128.
223
+ e.messageData[0] = soundfont.presets.find(p => p.isDrumPreset(isXG))?.program || 0;
224
+ SpessaSynthInfo(
225
+ `%cNo drum preset %c${initialProgram}%c. Channel %c${chNum}%c. Changing program to ${e.messageData[0]}.`,
226
+ consoleColors.info,
227
+ consoleColors.unrecognized,
228
+ consoleColors.info,
229
+ consoleColors.recognized,
230
+ consoleColors.info
231
+ );
232
+ }
233
+ }
234
+ else
235
+ {
236
+ if (soundfont.presets.findIndex(p => p.program === initialProgram && !p.isDrumPreset(isXG)) === -1)
237
+ {
238
+ // doesn't exist. pick any preset that does not have bank 128.
239
+ e.messageData[0] = soundfont.presets.find(p => !p.isDrumPreset(isXG))?.program || 0;
240
+ SpessaSynthInfo(
241
+ `%cNo preset %c${initialProgram}%c. Channel %c${chNum}%c. Changing program to ${e.messageData[0]}.`,
242
+ consoleColors.info,
243
+ consoleColors.unrecognized,
244
+ consoleColors.info,
245
+ consoleColors.recognized,
246
+ consoleColors.info
247
+ );
248
+ }
249
+ }
250
+ channel.program = e.messageData[0];
251
+ // check if this preset exists for program and bank
252
+ const realBank = Math.max(0, channel.lastBank?.messageData[1] - mid.bankOffset); // make sure to take the previous bank offset into account
253
+ const bankLSB = (channel?.lastBankLSB?.messageData[1] - mid.bankOffset) || 0;
254
+ if (channel.lastBank === undefined)
255
+ {
256
+ continue;
257
+ }
258
+ // adjust bank for XG
259
+ let bank = chooseBank(realBank, bankLSB, channel.drums, isXG);
260
+ if (soundfont.presets.findIndex(p => p.bank === bank && p.program === e.messageData[0]) === -1)
261
+ {
262
+ // no preset with this bank. find this program with any bank
263
+ const targetBank = (soundfont.presets.find(p => p.program === e.messageData[0])?.bank + bankOffset) || bankOffset;
264
+ channel.lastBank.messageData[1] = targetBank;
265
+ if (channel?.lastBankLSB?.messageData)
266
+ {
267
+ channel.lastBankLSB.messageData[1] = targetBank;
268
+ }
269
+ SpessaSynthInfo(
270
+ `%cNo preset %c${bank}:${e.messageData[0]}%c. Channel %c${chNum}%c. Changing bank to ${targetBank}.`,
271
+ consoleColors.info,
272
+ consoleColors.unrecognized,
273
+ consoleColors.info,
274
+ consoleColors.recognized,
275
+ consoleColors.info
276
+ );
277
+ }
278
+ else
279
+ {
280
+ // There is a preset with this bank. Add offset. For drums add the normal offset.
281
+ let drumBank = bank;
282
+ if (isSystemXG(system) && bank === 128)
283
+ {
284
+ bank = 127;
285
+ }
286
+ const newBank = (bank === 128 ? 128 : drumBank) + bankOffset;
287
+ channel.lastBank.messageData[1] = newBank;
288
+ if (channel?.lastBankLSB?.messageData && !channel.drums)
289
+ {
290
+ channel.lastBankLSB.messageData[1] = channel.lastBankLSB.messageData[1] - mid.bankOffset + bankOffset;
291
+ }
292
+ SpessaSynthInfo(
293
+ `%cPreset %c${bank}:${e.messageData[0]}%c exists. Channel %c${chNum}%c. Changing bank to ${newBank}.`,
294
+ consoleColors.info,
295
+ consoleColors.recognized,
296
+ consoleColors.info,
297
+ consoleColors.recognized,
298
+ consoleColors.info
299
+ );
300
+ }
301
+ continue;
302
+ }
303
+
304
+ // controller change
305
+ // we only care about bank-selects
306
+ const isLSB = e.messageData[0] === midiControllers.lsbForControl0BankSelect;
307
+ if (e.messageData[0] !== midiControllers.bankSelect && !isLSB)
308
+ {
309
+ continue;
310
+ }
311
+ // bank select
312
+ channel.hasBankSelect = true;
313
+ const bankNumber = e.messageData[1];
314
+ // interpret
315
+ const intepretation = parseBankSelect(
316
+ channel?.lastBank?.messageData[1] || 0,
317
+ bankNumber,
318
+ system,
319
+ isLSB,
320
+ channel.drums,
321
+ chNum
322
+ );
323
+ if (intepretation.drumsStatus === 2)
324
+ {
325
+ channel.drums = true;
326
+ }
327
+ else if (intepretation.drumsStatus === 1)
328
+ {
329
+ channel.drums = false;
330
+ }
331
+ if (isLSB)
332
+ {
333
+ channel.lastBankLSB = e;
334
+ }
335
+ else
336
+ {
337
+ channel.lastBank = e;
338
+ }
339
+ }
340
+
341
+ // add missing bank selects
342
+ // add all bank selects that are missing for this track
343
+ channelsInfo.forEach((has, ch) =>
344
+ {
345
+ if (has.hasBankSelect === true)
346
+ {
347
+ return;
348
+ }
349
+ // find the first program change (for the given channel)
350
+ const midiChannel = ch % 16;
351
+ const status = messageTypes.programChange | midiChannel;
352
+ // find track with this channel being used
353
+ const portOffset = Math.floor(ch / 16) * 16;
354
+ const port = mid.midiPortChannelOffsets.indexOf(portOffset);
355
+ const track = mid.tracks.find((t, tNum) => mid.midiPorts[tNum] === port && mid.usedChannelsOnTrack[tNum].has(
356
+ midiChannel));
357
+ if (track === undefined)
358
+ {
359
+ // this channel is not used at all
360
+ return;
361
+ }
362
+ let indexToAdd = track.findIndex(e => e.messageStatusByte === status);
363
+ if (indexToAdd === -1)
364
+ {
365
+ // no program change...
366
+ // add programs if they are missing from the track
367
+ // (need them to activate bank 1 for the embedded sfont)
368
+ const programIndex = track.findIndex(e => (e.messageStatusByte > 0x80 && e.messageStatusByte < 0xF0) && (e.messageStatusByte & 0xF) === midiChannel);
369
+ if (programIndex === -1)
370
+ {
371
+ // no voices??? skip
372
+ return;
373
+ }
374
+ const programTicks = track[programIndex].ticks;
375
+ const targetProgram = soundfont.getPreset(0, 0).program;
376
+ track.splice(programIndex, 0, new MIDIMessage(
377
+ programTicks,
378
+ messageTypes.programChange | midiChannel,
379
+ new IndexedByteArray([targetProgram])
380
+ ));
381
+ indexToAdd = programIndex;
382
+ }
383
+ SpessaSynthInfo(
384
+ `%cAdding bank select for %c${ch}`,
385
+ consoleColors.info,
386
+ consoleColors.recognized
387
+ );
388
+ const ticks = track[indexToAdd].ticks;
389
+ const targetBank = (soundfont.getPreset(
390
+ 0,
391
+ has.program,
392
+ isSystemXG(system)
393
+ )?.bank + bankOffset) || bankOffset;
394
+ track.splice(indexToAdd, 0, new MIDIMessage(
395
+ ticks,
396
+ messageTypes.controllerChange | midiChannel,
397
+ new IndexedByteArray([midiControllers.bankSelect, targetBank])
398
+ ));
399
+ });
400
+
401
+ // make sure to put xg if gm
402
+ if (system !== "gs" && !isSystemXG(system))
403
+ {
404
+ for (const m of unwantedSystems)
405
+ {
406
+ mid.tracks[m.tNum].splice(mid.tracks[m.tNum].indexOf(m.e), 1);
407
+ }
408
+ let index = 0;
409
+ if (mid.tracks[0][0].messageStatusByte === messageTypes.trackName)
410
+ {
411
+ index++;
412
+ }
413
+ mid.tracks[0].splice(index, 0, getGsOn(0));
414
+ }
415
+ }
416
+ const newMid = new IndexedByteArray(mid.writeMIDI().buffer);
417
+
418
+ // info data for RMID
419
+ /**
420
+ * @type {Uint8Array[]}
421
+ */
422
+ const infoContent = [getStringBytes("INFO")];
423
+ const encoder = new TextEncoder();
424
+ // software (SpessaSynth)
425
+ infoContent.push(
426
+ writeRIFFOddSize(RMIDINFOChunks.software, encoder.encode("SpessaSynth"), true)
427
+ );
428
+ // name
429
+ if (metadata.name !== undefined)
430
+ {
431
+
432
+ infoContent.push(
433
+ writeRIFFOddSize(RMIDINFOChunks.name, encoder.encode(metadata.name), true)
434
+ );
435
+ encoding = FORCED_ENCODING;
436
+ }
437
+ else
438
+ {
439
+ infoContent.push(
440
+ writeRIFFOddSize(RMIDINFOChunks.name, mid.rawMidiName, true)
441
+ );
442
+ }
443
+ // creation date
444
+ if (metadata.creationDate !== undefined)
445
+ {
446
+ encoding = FORCED_ENCODING;
447
+ infoContent.push(
448
+ writeRIFFOddSize(RMIDINFOChunks.creationDate, encoder.encode(metadata.creationDate), true)
449
+ );
450
+ }
451
+ else
452
+ {
453
+ const today = new Date().toLocaleString(undefined, {
454
+ weekday: "long",
455
+ year: "numeric",
456
+ month: "long",
457
+ day: "numeric",
458
+ hour: "numeric",
459
+ minute: "numeric"
460
+ });
461
+ infoContent.push(
462
+ writeRIFFOddSize(RMIDINFOChunks.creationDate, getStringBytesZero(today), true)
463
+ );
464
+ }
465
+ // comment
466
+ if (metadata.comment !== undefined)
467
+ {
468
+ encoding = FORCED_ENCODING;
469
+ infoContent.push(
470
+ writeRIFFOddSize(RMIDINFOChunks.comment, encoder.encode(metadata.comment))
471
+ );
472
+ }
473
+ // engineer
474
+ if (metadata.engineer !== undefined)
475
+ {
476
+ infoContent.push(
477
+ writeRIFFOddSize(RMIDINFOChunks.engineer, encoder.encode(metadata.engineer), true)
478
+ );
479
+ }
480
+ // album
481
+ if (metadata.album !== undefined)
482
+ {
483
+ // note that there are two album chunks: IPRD and IALB
484
+ encoding = FORCED_ENCODING;
485
+ infoContent.push(
486
+ writeRIFFOddSize(RMIDINFOChunks.album, encoder.encode(metadata.album), true)
487
+ );
488
+ infoContent.push(
489
+ writeRIFFOddSize(RMIDINFOChunks.album2, encoder.encode(metadata.album), true)
490
+ );
491
+ }
492
+ // artist
493
+ if (metadata.artist !== undefined)
494
+ {
495
+ encoding = FORCED_ENCODING;
496
+ infoContent.push(
497
+ writeRIFFOddSize(RMIDINFOChunks.artist, encoder.encode(metadata.artist), true)
498
+ );
499
+ }
500
+ // genre
501
+ if (metadata.genre !== undefined)
502
+ {
503
+ encoding = FORCED_ENCODING;
504
+ infoContent.push(
505
+ writeRIFFOddSize(RMIDINFOChunks.genre, encoder.encode(metadata.genre), true)
506
+ );
507
+ }
508
+ // picture
509
+ if (metadata.picture !== undefined)
510
+ {
511
+ infoContent.push(
512
+ writeRIFFOddSize(RMIDINFOChunks.picture, new Uint8Array(metadata.picture))
513
+ );
514
+ }
515
+ // copyright
516
+ if (metadata.copyright !== undefined)
517
+ {
518
+ encoding = FORCED_ENCODING;
519
+ infoContent.push(
520
+ writeRIFFOddSize(RMIDINFOChunks.copyright, encoder.encode(metadata.copyright), true)
521
+ );
522
+ }
523
+ else
524
+ {
525
+ // use midi copyright if possible
526
+ const copyright = mid.copyright.length > 0 ? mid.copyright : DEFAULT_COPYRIGHT;
527
+ infoContent.push(
528
+ writeRIFFOddSize(RMIDINFOChunks.copyright, getStringBytesZero(copyright))
529
+ );
530
+ }
531
+
532
+ // bank offset
533
+ const DBNK = new IndexedByteArray(2);
534
+ writeLittleEndian(DBNK, bankOffset, 2);
535
+ infoContent.push(writeRIFFOddSize(RMIDINFOChunks.bankOffset, DBNK));
536
+ // midi encoding
537
+ if (metadata.midiEncoding !== undefined)
538
+ {
539
+ infoContent.push(
540
+ writeRIFFOddSize(RMIDINFOChunks.midiEncoding, encoder.encode(metadata.midiEncoding))
541
+ );
542
+ encoding = FORCED_ENCODING;
543
+ }
544
+ // encoding
545
+ infoContent.push(writeRIFFOddSize(RMIDINFOChunks.encoding, getStringBytesZero(encoding)));
546
+
547
+ // combine and write out
548
+ const infodata = combineArrays(infoContent);
549
+ const rmiddata = combineArrays([
550
+ getStringBytes("RMID"),
551
+ writeRIFFOddSize(
552
+ "data",
553
+ newMid
554
+ ),
555
+ writeRIFFOddSize(
556
+ "LIST",
557
+ infodata
558
+ ),
559
+ soundfontBinary
560
+ ]);
561
+ SpessaSynthInfo("%cFinished!", consoleColors.info);
562
+ SpessaSynthGroupEnd();
563
+ return writeRIFFOddSize(
564
+ "RIFF",
565
+ rmiddata
566
+ );
567
+ }
spessasynth_lib/midi_parser/used_keys_loaded.js ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from "../utils/loggin.js";
2
+ import { consoleColors } from "../utils/other.js";
3
+ import { messageTypes, midiControllers } from "./midi_message.js";
4
+ import { DEFAULT_PERCUSSION } from "../synthetizer/synth_constants.js";
5
+ import { chooseBank, isSystemXG, parseBankSelect } from "../utils/xg_hacks.js";
6
+ import { isGSDrumsOn, isXGOn } from "../utils/sysex_detector.js";
7
+
8
+ /**
9
+ * Gets the used programs and keys for this MIDI file with a given sound bank
10
+ * @this {BasicMIDI}
11
+ * @param soundfont {BasicSoundBank|WorkletSoundfontManager} - the sound bank
12
+ * @returns {Object<string, Set<string>>}
13
+ */
14
+ export function getUsedProgramsAndKeys(soundfont)
15
+ {
16
+ const mid = this;
17
+ SpessaSynthGroupCollapsed(
18
+ "%cSearching for all used programs and keys...",
19
+ consoleColors.info
20
+ );
21
+ // Find every bank:program combo and every key:velocity for each. Make sure to care about ports and drums
22
+ const channelsAmount = 16 + mid.midiPortChannelOffsets.reduce((max, cur) => cur > max ? cur : max);
23
+ /**
24
+ * @type {{program: number, bank: number, bankLSB: number, drums: boolean, string: string, actualBank: number}[]}
25
+ */
26
+ const channelPresets = [];
27
+ for (let i = 0; i < channelsAmount; i++)
28
+ {
29
+ const bank = i % 16 === DEFAULT_PERCUSSION ? 128 : 0;
30
+ channelPresets.push({
31
+ program: 0,
32
+ bank: bank,
33
+ bankLSB: 0,
34
+ actualBank: bank,
35
+ drums: i % 16 === DEFAULT_PERCUSSION, // drums appear on 9 every 16 channels,
36
+ string: `${bank}:0`
37
+ });
38
+ }
39
+
40
+ // check for xg
41
+ let system = "gs";
42
+
43
+ function updateString(ch)
44
+ {
45
+ const bank = chooseBank(ch.bank, ch.bankLSB, ch.drums, isSystemXG(system));
46
+ // check if this exists in the soundfont
47
+ let exists = soundfont.getPreset(bank, ch.program, isSystemXG(system));
48
+ ch.actualBank = exists.bank;
49
+ ch.program = exists.program;
50
+ ch.string = ch.actualBank + ":" + ch.program;
51
+ if (!usedProgramsAndKeys[ch.string])
52
+ {
53
+ SpessaSynthInfo(
54
+ `%cDetected a new preset: %c${ch.string}`,
55
+ consoleColors.info,
56
+ consoleColors.recognized
57
+ );
58
+ usedProgramsAndKeys[ch.string] = new Set();
59
+ }
60
+ }
61
+
62
+ /**
63
+ * find all programs used and key-velocity combos in them
64
+ * bank:program each has a set of midiNote-velocity
65
+ * @type {Object<string, Set<string>>}
66
+ */
67
+ const usedProgramsAndKeys = {};
68
+
69
+ /**
70
+ * indexes for tracks
71
+ * @type {number[]}
72
+ */
73
+ const eventIndexes = Array(mid.tracks.length).fill(0);
74
+ let remainingTracks = mid.tracks.length;
75
+
76
+ function findFirstEventIndex()
77
+ {
78
+ let index = 0;
79
+ let ticks = Infinity;
80
+ mid.tracks.forEach((track, i) =>
81
+ {
82
+ if (eventIndexes[i] >= track.length)
83
+ {
84
+ return;
85
+ }
86
+ if (track[eventIndexes[i]].ticks < ticks)
87
+ {
88
+ index = i;
89
+ ticks = track[eventIndexes[i]].ticks;
90
+ }
91
+ });
92
+ return index;
93
+ }
94
+
95
+ const ports = mid.midiPorts.slice();
96
+ // initialize
97
+ channelPresets.forEach(c =>
98
+ {
99
+ updateString(c);
100
+ });
101
+ while (remainingTracks > 0)
102
+ {
103
+ let trackNum = findFirstEventIndex();
104
+ const track = mid.tracks[trackNum];
105
+ if (eventIndexes[trackNum] >= track.length)
106
+ {
107
+ remainingTracks--;
108
+ continue;
109
+ }
110
+ const event = track[eventIndexes[trackNum]];
111
+ eventIndexes[trackNum]++;
112
+
113
+ if (event.messageStatusByte === messageTypes.midiPort)
114
+ {
115
+ ports[trackNum] = event.messageData[0];
116
+ continue;
117
+ }
118
+ const status = event.messageStatusByte & 0xF0;
119
+ if (
120
+ status !== messageTypes.noteOn &&
121
+ status !== messageTypes.controllerChange &&
122
+ status !== messageTypes.programChange &&
123
+ status !== messageTypes.systemExclusive
124
+ )
125
+ {
126
+ continue;
127
+ }
128
+ const channel = (event.messageStatusByte & 0xF) + mid.midiPortChannelOffsets[ports[trackNum]] || 0;
129
+ let ch = channelPresets[channel];
130
+ switch (status)
131
+ {
132
+ case messageTypes.programChange:
133
+ ch.program = event.messageData[0];
134
+ updateString(ch);
135
+ break;
136
+
137
+ case messageTypes.controllerChange:
138
+ const isLSB = event.messageData[0] === midiControllers.lsbForControl0BankSelect;
139
+ if (event.messageData[0] !== midiControllers.bankSelect && !isLSB)
140
+ {
141
+ // we only care about bank select
142
+ continue;
143
+ }
144
+ if (system === "gs" && ch.drums)
145
+ {
146
+ // gs drums get changed via sysex, ignore here
147
+ continue;
148
+ }
149
+ const bank = event.messageData[1];
150
+ const realBank = Math.max(0, bank - mid.bankOffset);
151
+ if (isLSB)
152
+ {
153
+ ch.bankLSB = realBank;
154
+ }
155
+ else
156
+ {
157
+ ch.bank = realBank;
158
+ }
159
+ // interpret the bank
160
+ const intepretation = parseBankSelect(
161
+ ch.bank,
162
+ realBank,
163
+ system,
164
+ isLSB,
165
+ ch.drums,
166
+ channel
167
+ );
168
+ switch (intepretation.drumsStatus)
169
+ {
170
+ case 0:
171
+ // no change
172
+ break;
173
+
174
+ case 1:
175
+ // drums changed to off
176
+ // drum change is a program change
177
+ ch.drums = false;
178
+ updateString(ch);
179
+ break;
180
+
181
+ case 2:
182
+ // drums changed to on
183
+ // drum change is a program change
184
+ ch.drums = true;
185
+ updateString(ch);
186
+ break;
187
+ }
188
+ // do not update the data, bank change doesn't change the preset
189
+ break;
190
+
191
+ case messageTypes.noteOn:
192
+ if (event.messageData[1] === 0)
193
+ {
194
+ // that's a note off
195
+ continue;
196
+ }
197
+ usedProgramsAndKeys[ch.string].add(`${event.messageData[0]}-${event.messageData[1]}`);
198
+ break;
199
+
200
+ case messageTypes.systemExclusive:
201
+ // check for drum sysex
202
+ if (!isGSDrumsOn(event))
203
+ {
204
+ // check for XG
205
+ if (isXGOn(event))
206
+ {
207
+ system = "xg";
208
+ SpessaSynthInfo(
209
+ "%cXG on detected!",
210
+ consoleColors.recognized
211
+ );
212
+ }
213
+ continue;
214
+ }
215
+ 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]];
216
+ const isDrum = !!(event.messageData[7] > 0 && event.messageData[5] >> 4);
217
+ ch = channelPresets[sysexChannel];
218
+ ch.drums = isDrum;
219
+ updateString(ch);
220
+ break;
221
+
222
+ }
223
+ }
224
+ for (const key of Object.keys(usedProgramsAndKeys))
225
+ {
226
+ if (usedProgramsAndKeys[key].size === 0)
227
+ {
228
+ SpessaSynthInfo(
229
+ `%cDetected change but no keys for %c${key}`,
230
+ consoleColors.info,
231
+ consoleColors.value
232
+ );
233
+ delete usedProgramsAndKeys[key];
234
+ }
235
+ }
236
+ SpessaSynthGroupEnd();
237
+ return usedProgramsAndKeys;
238
+ }
spessasynth_lib/midi_parser/xmf_loader.js ADDED
@@ -0,0 +1,454 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { readBytesAsString } from "../utils/byte_functions/string.js";
2
+ import { SpessaSynthGroup, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn } from "../utils/loggin.js";
3
+ import { consoleColors } from "../utils/other.js";
4
+ import { readBytesAsUintBigEndian } from "../utils/byte_functions/big_endian.js";
5
+ import { readVariableLengthQuantity } from "../utils/byte_functions/variable_length_quantity.js";
6
+ import { RMIDINFOChunks } from "./rmidi_writer.js";
7
+ import { inflateSync } from "../externals/fflate/fflate.min.js";
8
+ import { IndexedByteArray } from "../utils/indexed_array.js";
9
+
10
+ /**
11
+ * @enum {number}
12
+ */
13
+ const metadataTypes = {
14
+ XMFFileType: 0,
15
+ nodeName: 1,
16
+ nodeIDNumber: 2,
17
+ resourceFormat: 3,
18
+ filenameOnDisk: 4,
19
+ filenameExtensionOnDisk: 5,
20
+ macOSFileTypeAndCreator: 6,
21
+ mimeType: 7,
22
+ title: 8,
23
+ copyrightNotice: 9,
24
+ comment: 10,
25
+ autoStart: 11, // Node Name of the FileNode containing the SMF image to autostart when the XMF file loads
26
+ preload: 12, // Used to preload specific SMF and DLS file images.
27
+ contentDescription: 13, // RP-42a (https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp42.pdf)
28
+ ID3Metadata: 14 // RP-47 (https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp47.pdf)
29
+ };
30
+
31
+ /**
32
+ * @enum {number}
33
+ */
34
+ const referenceTypeIds = {
35
+ inLineResource: 1,
36
+ inFileResource: 2,
37
+ inFileNode: 3,
38
+ externalFile: 4,
39
+ externalXMF: 5,
40
+ XMFFileURIandNodeID: 6
41
+ };
42
+
43
+ /**
44
+ * @enum {number}
45
+ */
46
+ const resourceFormatIDs = {
47
+ StandardMIDIFile: 0,
48
+ StandardMIDIFileType1: 1,
49
+ DLS1: 2,
50
+ DLS2: 3,
51
+ DLS22: 4,
52
+ mobileDLS: 5
53
+ };
54
+
55
+ /**
56
+ * @enum {number}
57
+ */
58
+ const formatTypeIDs = {
59
+ standard: 0,
60
+ MMA: 1,
61
+ registered: 2,
62
+ nonRegistered: 3
63
+ };
64
+
65
+
66
+ /**
67
+ * @enum {number}
68
+ */
69
+ const unpackerIDs = {
70
+ none: 0,
71
+ MMAUnpacker: 1,
72
+ registered: 2,
73
+ nonRegistered: 3
74
+ };
75
+
76
+ class XMFNode
77
+ {
78
+ /**
79
+ * @type {number}
80
+ */
81
+ length;
82
+ /**
83
+ * 0 means it's a file node
84
+ * @type {number}
85
+ */
86
+ itemCount;
87
+ /**
88
+ * @type {number}
89
+ */
90
+ metadataLength;
91
+
92
+ /**
93
+ * @type {Object<string, any>}
94
+ */
95
+ metadata = {};
96
+
97
+ /**
98
+ * @type {IndexedByteArray}
99
+ */
100
+ nodeData;
101
+
102
+ /**
103
+ * @type {XMFNode[]}
104
+ */
105
+ innerNodes = [];
106
+
107
+ packedContent = false;
108
+
109
+ nodeUnpackers = [];
110
+
111
+
112
+ /**
113
+ * @type {"StandardMIDIFile"|
114
+ * "StandardMIDIFileType1"|
115
+ * "DLS1"|
116
+ * "DLS2"|
117
+ * "DLS22"|
118
+ * "mobileDLS"|
119
+ * "unknown"|"folder"}
120
+ */
121
+ resourceFormat = "unknown";
122
+
123
+ /**
124
+ * @param binaryData {IndexedByteArray}
125
+ */
126
+ constructor(binaryData)
127
+ {
128
+ let nodeStartIndex = binaryData.currentIndex;
129
+ this.length = readVariableLengthQuantity(binaryData);
130
+ this.itemCount = readVariableLengthQuantity(binaryData);
131
+ // header length
132
+ const headerLength = readVariableLengthQuantity(binaryData);
133
+ const readBytes = binaryData.currentIndex - nodeStartIndex;
134
+
135
+ const remainingHeader = headerLength - readBytes;
136
+ const headerData = binaryData.slice(
137
+ binaryData.currentIndex,
138
+ binaryData.currentIndex + remainingHeader
139
+ );
140
+ binaryData.currentIndex += remainingHeader;
141
+
142
+ this.metadataLength = readVariableLengthQuantity(headerData);
143
+
144
+ const metadataChunk = headerData.slice(
145
+ headerData.currentIndex,
146
+ headerData.currentIndex + this.metadataLength
147
+ );
148
+ headerData.currentIndex += this.metadataLength;
149
+
150
+ /**
151
+ * @type {metadataTypes|string|number}
152
+ */
153
+ let fieldSpecifier;
154
+ let key;
155
+ while (metadataChunk.currentIndex < metadataChunk.length)
156
+ {
157
+ const firstSpecifierByte = metadataChunk[metadataChunk.currentIndex];
158
+ if (firstSpecifierByte === 0)
159
+ {
160
+ metadataChunk.currentIndex++;
161
+ fieldSpecifier = readVariableLengthQuantity(metadataChunk);
162
+ if (Object.values(metadataTypes).indexOf(fieldSpecifier) === -1)
163
+ {
164
+ SpessaSynthWarn(`Unknown field specifier: ${fieldSpecifier}`);
165
+ key = `unknown_${fieldSpecifier}`;
166
+ }
167
+ else
168
+ {
169
+ key = Object.keys(metadataTypes).find(k => metadataTypes[k] === fieldSpecifier);
170
+ }
171
+ }
172
+ else
173
+ {
174
+ // this is the length of string
175
+ const stringLength = readVariableLengthQuantity(metadataChunk);
176
+ fieldSpecifier = readBytesAsString(metadataChunk, stringLength);
177
+ key = fieldSpecifier;
178
+ }
179
+
180
+ const numberOfVersions = readVariableLengthQuantity(metadataChunk);
181
+ if (numberOfVersions === 0)
182
+ {
183
+ const dataLength = readVariableLengthQuantity(metadataChunk);
184
+ const contentsChunk = metadataChunk.slice(
185
+ metadataChunk.currentIndex,
186
+ metadataChunk.currentIndex + dataLength
187
+ );
188
+ metadataChunk.currentIndex += dataLength;
189
+ const formatID = readVariableLengthQuantity(contentsChunk);
190
+ // text only
191
+ if (formatID < 4)
192
+ {
193
+ this.metadata[key] = readBytesAsString(contentsChunk, dataLength - 1);
194
+ }
195
+ else
196
+ {
197
+ this.metadata[key] = contentsChunk.slice(contentsChunk.currentIndex);
198
+ }
199
+ }
200
+ else
201
+ {
202
+ // throw new Error ("International content is not supported.");
203
+ // Skip the number of versions
204
+ SpessaSynthWarn(`International content: ${numberOfVersions}`);
205
+ // Length in bytes
206
+ // Skip the whole thing!
207
+ metadataChunk.currentIndex += readVariableLengthQuantity(metadataChunk);
208
+ }
209
+ }
210
+
211
+ const unpackersStart = headerData.currentIndex;
212
+ const unpackersLength = readVariableLengthQuantity(headerData);
213
+ const unpackersData = headerData.slice(headerData.currentIndex, unpackersStart + unpackersLength);
214
+ headerData.currentIndex = unpackersStart + unpackersLength;
215
+ if (unpackersLength > 0)
216
+ {
217
+ this.packedContent = true;
218
+ while (unpackersData.currentIndex < unpackersLength)
219
+ {
220
+ const unpacker = {};
221
+ unpacker.id = readVariableLengthQuantity(unpackersData);
222
+ switch (unpacker.id)
223
+ {
224
+ case unpackerIDs.nonRegistered:
225
+ case unpackerIDs.registered:
226
+ SpessaSynthGroupEnd();
227
+ throw new Error(`Unsupported unpacker ID: ${unpacker.id}`);
228
+
229
+ default:
230
+ SpessaSynthGroupEnd();
231
+ throw new Error(`Unknown unpacker ID: ${unpacker.id}`);
232
+
233
+ case unpackerIDs.none:
234
+ unpacker.standardID = readVariableLengthQuantity(unpackersData);
235
+ break;
236
+
237
+ case unpackerIDs.MMAUnpacker:
238
+ let manufacturerID = unpackersData[unpackersData.currentIndex++];
239
+ // one or three byte form, depending on if the first byte is zero
240
+ if (manufacturerID === 0)
241
+ {
242
+ manufacturerID <<= 8;
243
+ manufacturerID |= unpackersData[unpackersData.currentIndex++];
244
+ manufacturerID <<= 8;
245
+ manufacturerID |= unpackersData[unpackersData.currentIndex++];
246
+ }
247
+ const manufacturerInternalID = readVariableLengthQuantity(unpackersData);
248
+ unpacker.manufacturerID = manufacturerID;
249
+ unpacker.manufacturerInternalID = manufacturerInternalID;
250
+ break;
251
+ }
252
+ unpacker.decodedSize = readVariableLengthQuantity(unpackersData);
253
+ this.nodeUnpackers.push(unpacker);
254
+ }
255
+ }
256
+ binaryData.currentIndex = nodeStartIndex + headerLength;
257
+ /**
258
+ * @type {referenceTypeIds|number}
259
+ */
260
+ this.referenceTypeID = readVariableLengthQuantity(binaryData);
261
+ this.nodeData = binaryData.slice(binaryData.currentIndex, nodeStartIndex + this.length);
262
+ binaryData.currentIndex = nodeStartIndex + this.length;
263
+ switch (this.referenceTypeID)
264
+ {
265
+ case referenceTypeIds.inLineResource:
266
+ break;
267
+
268
+ case referenceTypeIds.externalXMF:
269
+ case referenceTypeIds.inFileNode:
270
+ case referenceTypeIds.XMFFileURIandNodeID:
271
+ case referenceTypeIds.externalFile:
272
+ case referenceTypeIds.inFileResource:
273
+ SpessaSynthGroupEnd();
274
+ throw new Error(`Unsupported reference type: ${this.referenceTypeID}`);
275
+
276
+ default:
277
+ SpessaSynthGroupEnd();
278
+ throw new Error(`Unknown reference type: ${this.referenceTypeID}`);
279
+ }
280
+
281
+ // read the data
282
+ if (this.isFile)
283
+ {
284
+ if (this.packedContent)
285
+ {
286
+ const compressed = this.nodeData.slice(2, this.nodeData.length);
287
+ SpessaSynthInfo(
288
+ `%cPacked content. Attemting to deflate. Target size: %c${this.nodeUnpackers[0].decodedSize}`,
289
+ consoleColors.warn,
290
+ consoleColors.value
291
+ );
292
+ try
293
+ {
294
+ this.nodeData = new IndexedByteArray(inflateSync(compressed).buffer);
295
+ }
296
+ catch (e)
297
+ {
298
+ SpessaSynthGroupEnd();
299
+ throw new Error(`Error unpacking XMF file contents: ${e.message}.`);
300
+ }
301
+ }
302
+ /**
303
+ * interpret the content
304
+ * @type {number[]}
305
+ */
306
+ const resourceFormat = this.metadata["resourceFormat"];
307
+ if (resourceFormat === undefined)
308
+ {
309
+ SpessaSynthWarn("No resource format for this file node!");
310
+ }
311
+ else
312
+ {
313
+ const formatTypeID = resourceFormat[0];
314
+ if (formatTypeID !== formatTypeIDs.standard)
315
+ {
316
+ SpessaSynthWarn(`Non-standard formatTypeID: ${resourceFormat}`);
317
+ this.resourceFormat = resourceFormat.toString();
318
+ }
319
+ const resourceFormatID = resourceFormat[1];
320
+ if (Object.values(resourceFormatIDs).indexOf(resourceFormatID) === -1)
321
+ {
322
+ SpessaSynthWarn(`Unrecognized resource format: ${resourceFormatID}`);
323
+ }
324
+ else
325
+ {
326
+ this.resourceFormat = Object.keys(resourceFormatIDs)
327
+ .find(k => resourceFormatIDs[k] === resourceFormatID);
328
+ }
329
+ }
330
+ }
331
+ else
332
+ {
333
+ // folder node
334
+ this.resourceFormat = "folder";
335
+ while (this.nodeData.currentIndex < this.nodeData.length)
336
+ {
337
+ const nodeStartIndex = this.nodeData.currentIndex;
338
+ const nodeLength = readVariableLengthQuantity(this.nodeData);
339
+ const nodeData = this.nodeData.slice(nodeStartIndex, nodeStartIndex + nodeLength);
340
+ this.nodeData.currentIndex = nodeStartIndex + nodeLength;
341
+ this.innerNodes.push(new XMFNode(nodeData));
342
+ }
343
+ }
344
+ }
345
+
346
+ get isFile()
347
+ {
348
+ return this.itemCount === 0;
349
+ }
350
+ }
351
+
352
+ /**
353
+ * @param midi {MIDI}
354
+ * @param binaryData {IndexedByteArray}
355
+ * @returns {IndexedByteArray} the file byte array
356
+ */
357
+ export function loadXMF(midi, binaryData)
358
+ {
359
+ midi.bankOffset = 0;
360
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/xmf-v1a.pdf
361
+ // https://wiki.multimedia.cx/index.php?title=Extensible_Music_Format_(XMF)
362
+ const sanityCheck = readBytesAsString(binaryData, 4);
363
+ if (sanityCheck !== "XMF_")
364
+ {
365
+ SpessaSynthGroupEnd();
366
+ throw new SyntaxError(`Invalid XMF Header! Expected "_XMF", got "${sanityCheck}"`);
367
+ }
368
+
369
+ SpessaSynthGroup("%cParsing XMF file...", consoleColors.info);
370
+ const version = readBytesAsString(binaryData, 4);
371
+ SpessaSynthInfo(
372
+ `%cXMF version: %c${version}`,
373
+ consoleColors.info, consoleColors.recognized
374
+ );
375
+ // https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp43.pdf
376
+ // version 2.00 has additional bytes
377
+ if (version === "2.00")
378
+ {
379
+ const fileTypeId = readBytesAsUintBigEndian(binaryData, 4);
380
+ const fileTypeRevisionId = readBytesAsUintBigEndian(binaryData, 4);
381
+ SpessaSynthInfo(
382
+ `%cFile Type ID: %c${fileTypeId}%c, File Type Revision ID: %c${fileTypeRevisionId}`,
383
+ consoleColors.info,
384
+ consoleColors.recognized,
385
+ consoleColors.info,
386
+ consoleColors.recognized
387
+ );
388
+ }
389
+
390
+ // file length
391
+ readVariableLengthQuantity(binaryData);
392
+
393
+ const metadataTableLength = readVariableLengthQuantity(binaryData);
394
+ // skip metadata
395
+ binaryData.currentIndex += metadataTableLength;
396
+
397
+ // skip to tree root
398
+ binaryData.currentIndex = readVariableLengthQuantity(binaryData);
399
+ const rootNode = new XMFNode(binaryData);
400
+ /**
401
+ * @type {IndexedByteArray}
402
+ */
403
+ let midiArray;
404
+ /**
405
+ * find the stuff we care about
406
+ * @param node {XMFNode}
407
+ */
408
+ const searchNode = node =>
409
+ {
410
+ const checkMeta = (xmf, rmid) =>
411
+ {
412
+ if (node.metadata[xmf] !== undefined && typeof node.metadata[xmf] === "string")
413
+ {
414
+ midi.RMIDInfo[rmid] = node.metadata[xmf];
415
+ }
416
+ };
417
+ // meta
418
+ checkMeta("nodeName", RMIDINFOChunks.name);
419
+ checkMeta("title", RMIDINFOChunks.name);
420
+ checkMeta("copyrightNotice", RMIDINFOChunks.copyright);
421
+ checkMeta("comment", RMIDINFOChunks.comment);
422
+ if (node.isFile)
423
+ {
424
+ switch (node.resourceFormat)
425
+ {
426
+ default:
427
+ return;
428
+ case "DLS1":
429
+ case "DLS2":
430
+ case "DLS22":
431
+ case "mobileDLS":
432
+ SpessaSynthInfo("%cFound embedded DLS!", consoleColors.recognized);
433
+ midi.embeddedSoundFont = node.nodeData.buffer;
434
+ break;
435
+
436
+ case "StandardMIDIFile":
437
+ case "StandardMIDIFileType1":
438
+ SpessaSynthInfo("%cFound embedded MIDI!", consoleColors.recognized);
439
+ midiArray = node.nodeData;
440
+ break;
441
+ }
442
+ }
443
+ else
444
+ {
445
+ for (const n of node.innerNodes)
446
+ {
447
+ searchNode(n);
448
+ }
449
+ }
450
+ };
451
+ searchNode(rootNode);
452
+ SpessaSynthGroupEnd();
453
+ return midiArray;
454
+ }
spessasynth_lib/sequencer/README.md ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## This is the sequencer's folder.
2
+
3
+ The code here is responsible for playing back the parsed MIDI sequence with the synthesizer.
4
+
5
+ ### Message protocol:
6
+
7
+ #### Message structure
8
+
9
+ ```js
10
+ const message = {
11
+ messageType: number, // WorkletSequencerMessageType
12
+ messageData: any // any
13
+ }
14
+ ```
15
+
16
+ #### To worklet
17
+
18
+ Sequencer uses `Synthetizer`'s `post` method to post a message with `messageData` set to
19
+ `workletMessageType.sequencerSpecific`.
20
+ The `messageData` is set to the sequencer's message.
21
+
22
+ #### From worklet
23
+
24
+ `WorkletSequencer` uses `SpessaSynthProcessor`'s post to send a message with `messageData` set to
25
+ `returnMessageType.sequencerSpecific`.
26
+ The `messageData` is set to the sequencer's return message.
27
+
28
+ ### Process tick
29
+
30
+ `processTick` is called every time the `process` method is called via `SpessaSynthProcessor.processTickCallback`.
spessasynth_lib/sequencer/default_sequencer_options.js ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @type {SequencerOptions}
3
+ */
4
+ export const DEFAULT_SEQUENCER_OPTIONS = {
5
+ skipToFirstNoteOn: true,
6
+ autoPlay: true,
7
+ preservePlaybackState: false
8
+ };
spessasynth_lib/sequencer/sequencer.js ADDED
@@ -0,0 +1,804 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Synthetizer } from "../synthetizer/synthetizer.js";
2
+ import { messageTypes } from "../midi_parser/midi_message.js";
3
+ import { workletMessageType } from "../synthetizer/worklet_system/message_protocol/worklet_message.js";
4
+ import {
5
+ SongChangeType,
6
+ WorkletSequencerMessageType,
7
+ WorkletSequencerReturnMessageType
8
+ } from "./worklet_sequencer/sequencer_message.js";
9
+ import { SpessaSynthWarn } from "../utils/loggin.js";
10
+ import { DUMMY_MIDI_DATA, MIDIData } from "../midi_parser/midi_data.js";
11
+ import { BasicMIDI } from "../midi_parser/basic_midi.js";
12
+ import { readBytesAsUintBigEndian } from "../utils/byte_functions/big_endian.js";
13
+ import { DEFAULT_SEQUENCER_OPTIONS } from "./default_sequencer_options.js";
14
+
15
+ /**
16
+ * sequencer.js
17
+ * purpose: plays back the midi file decoded by midi_loader.js, including support for multichannel midis
18
+ * (adding channels when more than one midi port is detected)
19
+ * note: this is the sequencer class that runs on the main thread
20
+ * and only communicates with the worklet sequencer which does the actual playback
21
+ */
22
+
23
+ /**
24
+ * @typedef MidFile {Object}
25
+ * @property {ArrayBuffer} binary - the binary data of the file.
26
+ * @property {string|undefined} altName - the alternative name for the file
27
+ */
28
+
29
+ /**
30
+ * @typedef {BasicMIDI|MidFile} MIDIFile
31
+ */
32
+
33
+ // noinspection JSUnusedGlobalSymbols
34
+ /**
35
+ * @typedef {Object} SequencerOptions
36
+ * @property {boolean|undefined} skipToFirstNoteOn - if true, the sequencer will skip to the first note
37
+ * @property {boolean|undefined} autoPlay - if true, the sequencer will automatically start playing the MIDI
38
+ * @property {boolean|unescape} preservePlaybackState - if true,
39
+ * the sequencer will stay paused when seeking or changing the playback rate
40
+ */
41
+
42
+ // noinspection JSUnusedGlobalSymbols
43
+ export class Sequencer
44
+ {
45
+ /**
46
+ * Executes when MIDI parsing has an error.
47
+ * @type {function(Error)}
48
+ */
49
+ onError;
50
+
51
+ /**
52
+ * Fires on text event
53
+ * @type {Function}
54
+ * @param data {Uint8Array} the data text
55
+ * @param type {number} the status byte of the message (the meta-status byte)
56
+ * @param lyricsIndex {number} if the text is a lyric, the index of the lyric in midiData.lyrics, otherwise -1
57
+ */
58
+ onTextEvent;
59
+
60
+ /**
61
+ * The current MIDI data, with the exclusion of the embedded sound bank and event data.
62
+ * @type {MIDIData}
63
+ */
64
+ midiData;
65
+
66
+ /**
67
+ * The current MIDI data for all songs, like the midiData property.
68
+ * @type {MIDIData[]}
69
+ */
70
+ songListData = [];
71
+
72
+ /**
73
+ * @type {Object<string, function(MIDIData)>}
74
+ * @private
75
+ */
76
+ onSongChange = {};
77
+
78
+ /**
79
+ * Fires when CurrentTime changes
80
+ * @type {Object<string, function(number)>} the time that was changed to
81
+ * @private
82
+ */
83
+ onTimeChange = {};
84
+
85
+ /**
86
+ * @type {Object<string, function>}
87
+ * @private
88
+ */
89
+ onSongEnded = {};
90
+
91
+ /**
92
+ * Fires on tempo change
93
+ * @type {Object<string, function(number)>}
94
+ */
95
+ onTempoChange = {};
96
+
97
+ /**
98
+ * Fires on meta-event
99
+ * @type {Object<string, function([number, Uint8Array, number, number])>}
100
+ */
101
+ onMetaEvent = {};
102
+
103
+ /**
104
+ * Current song's tempo in BPM
105
+ * @type {number}
106
+ */
107
+ currentTempo = 120;
108
+ /**
109
+ * Current song index
110
+ * @type {number}
111
+ */
112
+ songIndex = 0;
113
+ /**
114
+ * @type {function(BasicMIDI)}
115
+ * @private
116
+ */
117
+ _getMIDIResolve = undefined;
118
+ /**
119
+ * Indicates if the current midiData property has fake data in it (not yet loaded)
120
+ * @type {boolean}
121
+ */
122
+ hasDummyData = true;
123
+ /**
124
+ * Indicates whether the sequencer has finished playing a sequence
125
+ * @type {boolean}
126
+ */
127
+ isFinished = false;
128
+ /**
129
+ * The current sequence's length, in seconds
130
+ * @type {number}
131
+ */
132
+ duration = 0;
133
+
134
+ /**
135
+ * Indicates if the sequencer is paused.
136
+ * Paused if a number, undefined if playing
137
+ * @type {undefined|number}
138
+ * @private
139
+ */
140
+ pausedTime = undefined;
141
+
142
+ /**
143
+ * Creates a new Midi sequencer for playing back MIDI files
144
+ * @param midiBinaries {MIDIFile[]} List of the buffers of the MIDI files
145
+ * @param synth {Synthetizer} synth to send events to
146
+ * @param options {SequencerOptions} the sequencer's options
147
+ */
148
+ constructor(midiBinaries, synth, options = DEFAULT_SEQUENCER_OPTIONS)
149
+ {
150
+ this.ignoreEvents = false;
151
+ this.synth = synth;
152
+ this.highResTimeOffset = 0;
153
+
154
+ /**
155
+ * Absolute playback startTime, bases on the synth's time
156
+ * @type {number}
157
+ */
158
+ this.absoluteStartTime = this.synth.currentTime;
159
+
160
+ this.synth.sequencerCallbackFunction = this._handleMessage.bind(this);
161
+
162
+ /**
163
+ * @type {boolean}
164
+ * @private
165
+ */
166
+ this._skipToFirstNoteOn = options?.skipToFirstNoteOn ?? true;
167
+ /**
168
+ * @type {boolean}
169
+ * @private
170
+ */
171
+ this._preservePlaybackState = options?.preservePlaybackState ?? false;
172
+
173
+ if (this._skipToFirstNoteOn === false)
174
+ {
175
+ // setter sends message
176
+ this._sendMessage(WorkletSequencerMessageType.setSkipToFirstNote, false);
177
+ }
178
+
179
+ if (this._preservePlaybackState === true)
180
+ {
181
+ this._sendMessage(WorkletSequencerMessageType.setPreservePlaybackState, true);
182
+ }
183
+
184
+ this.loadNewSongList(midiBinaries, options?.autoPlay ?? true);
185
+
186
+ window.addEventListener("beforeunload", this.resetMIDIOut.bind(this));
187
+ }
188
+
189
+ /**
190
+ * Internal loop marker
191
+ * @type {boolean}
192
+ * @private
193
+ */
194
+ _loop = true;
195
+
196
+ /**
197
+ * Indicates if the sequencer is currently looping
198
+ * @returns {boolean}
199
+ */
200
+ get loop()
201
+ {
202
+ return this._loop;
203
+ }
204
+
205
+ set loop(value)
206
+ {
207
+ this._sendMessage(WorkletSequencerMessageType.setLoop, [value, this._loopsRemaining]);
208
+ this._loop = value;
209
+ }
210
+
211
+ /**
212
+ * Internal loop count marker (-1 is infinite)
213
+ * @type {number}
214
+ * @private
215
+ */
216
+ _loopsRemaining = -1;
217
+
218
+ /**
219
+ * The current remaining number of loops. -1 means infinite looping
220
+ * @returns {number}
221
+ */
222
+ get loopsRemaining()
223
+ {
224
+ return this._loopsRemaining;
225
+ }
226
+
227
+ /**
228
+ * The current remaining number of loops. -1 means infinite looping
229
+ * @param val {number}
230
+ */
231
+ set loopsRemaining(val)
232
+ {
233
+ this._loopsRemaining = val;
234
+ this._sendMessage(WorkletSequencerMessageType.setLoop, [this._loop, val]);
235
+ }
236
+
237
+ /**
238
+ * Controls the playback's rate
239
+ * @type {number}
240
+ * @private
241
+ */
242
+ _playbackRate = 1;
243
+
244
+ /**
245
+ * @returns {number}
246
+ */
247
+ get playbackRate()
248
+ {
249
+ return this._playbackRate;
250
+ }
251
+
252
+ /**
253
+ * @param value {number}
254
+ */
255
+ set playbackRate(value)
256
+ {
257
+ this._sendMessage(WorkletSequencerMessageType.setPlaybackRate, value);
258
+ this.highResTimeOffset *= (value / this._playbackRate);
259
+ this._playbackRate = value;
260
+ }
261
+
262
+ /**
263
+ * @type {boolean}
264
+ * @private
265
+ */
266
+ _shuffleSongs = false;
267
+
268
+ /**
269
+ * Indicates if the song order is random
270
+ * @returns {boolean}
271
+ */
272
+ get shuffleSongs()
273
+ {
274
+ return this._shuffleSongs;
275
+ }
276
+
277
+ /**
278
+ * Indicates if the song order is random
279
+ * @param value {boolean}
280
+ */
281
+ set shuffleSongs(value)
282
+ {
283
+ this._shuffleSongs = value;
284
+ if (value)
285
+ {
286
+ this._sendMessage(WorkletSequencerMessageType.changeSong, [SongChangeType.shuffleOn]);
287
+ }
288
+ else
289
+ {
290
+ this._sendMessage(WorkletSequencerMessageType.changeSong, [SongChangeType.shuffleOff]);
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Indicates if the sequencer should skip to first note on
296
+ * @return {boolean}
297
+ */
298
+ get skipToFirstNoteOn()
299
+ {
300
+ return this._skipToFirstNoteOn;
301
+ }
302
+
303
+ /**
304
+ * Indicates if the sequencer should skip to first note on
305
+ * @param val {boolean}
306
+ */
307
+ set skipToFirstNoteOn(val)
308
+ {
309
+ this._skipToFirstNoteOn = val;
310
+ this._sendMessage(WorkletSequencerMessageType.setSkipToFirstNote, this._skipToFirstNoteOn);
311
+ }
312
+
313
+ /**
314
+ * if true,
315
+ * the sequencer will stay paused when seeking or changing the playback rate
316
+ * @returns {boolean}
317
+ */
318
+ get preservePlaybackState()
319
+ {
320
+ return this._preservePlaybackState;
321
+ }
322
+
323
+ /**
324
+ * if true,
325
+ * the sequencer will stay paused when seeking or changing the playback rate
326
+ * @param val {boolean}
327
+ */
328
+ set preservePlaybackState(val)
329
+ {
330
+ this._preservePlaybackState = val;
331
+ this._sendMessage(WorkletSequencerMessageType.setPreservePlaybackState, val);
332
+ }
333
+
334
+ /**
335
+ * @returns {number} Current playback time, in seconds
336
+ */
337
+ get currentTime()
338
+ {
339
+ // return the paused time if it's set to something other than undefined
340
+ if (this.pausedTime !== undefined)
341
+ {
342
+ return this.pausedTime;
343
+ }
344
+
345
+ return (this.synth.currentTime - this.absoluteStartTime) * this._playbackRate;
346
+ }
347
+
348
+ set currentTime(time)
349
+ {
350
+ if (!this._preservePlaybackState)
351
+ {
352
+ this.unpause();
353
+ }
354
+ this._sendMessage(WorkletSequencerMessageType.setTime, time);
355
+ }
356
+
357
+ /**
358
+ * Use for visualization as it's not affected by the audioContext stutter
359
+ * @returns {number}
360
+ */
361
+ get currentHighResolutionTime()
362
+ {
363
+ if (this.pausedTime !== undefined)
364
+ {
365
+ return this.pausedTime;
366
+ }
367
+ const highResTimeOffset = this.highResTimeOffset;
368
+ const absoluteStartTime = this.absoluteStartTime;
369
+
370
+ // sync performance.now to current time
371
+ const performanceElapsedTime = ((performance.now() / 1000) - absoluteStartTime) * this._playbackRate;
372
+
373
+ let currentPerformanceTime = highResTimeOffset + performanceElapsedTime;
374
+ const currentAudioTime = this.currentTime;
375
+
376
+ const smoothingFactor = 0.01 * this._playbackRate;
377
+
378
+ // diff times smoothing factor
379
+ const timeDifference = currentAudioTime - currentPerformanceTime;
380
+ this.highResTimeOffset += timeDifference * smoothingFactor;
381
+
382
+ // return a smoothed performance time
383
+ currentPerformanceTime = this.highResTimeOffset + performanceElapsedTime;
384
+ return currentPerformanceTime;
385
+ }
386
+
387
+ /**
388
+ * true if paused, false if playing or stopped
389
+ * @returns {boolean}
390
+ */
391
+ get paused()
392
+ {
393
+ return this.pausedTime !== undefined;
394
+ }
395
+
396
+ /**
397
+ * Adds a new event that gets called when the song changes
398
+ * @param callback {function(MIDIData)}
399
+ * @param id {string} must be unique
400
+ */
401
+ addOnSongChangeEvent(callback, id)
402
+ {
403
+ this.onSongChange[id] = callback;
404
+ }
405
+
406
+ /**
407
+ * Adds a new event that gets called when the song ends
408
+ * @param callback {function}
409
+ * @param id {string} must be unique
410
+ */
411
+ addOnSongEndedEvent(callback, id)
412
+ {
413
+ this.onSongEnded[id] = callback;
414
+ }
415
+
416
+ /**
417
+ * Adds a new event that gets called when the time changes
418
+ * @param callback {function(number)} the new time, in seconds
419
+ * @param id {string} must be unique
420
+ */
421
+ addOnTimeChangeEvent(callback, id)
422
+ {
423
+ this.onTimeChange[id] = callback;
424
+ }
425
+
426
+ /**
427
+ * Adds a new event that gets called when the tempo changes
428
+ * @param callback {function(number)} the new tempo, in BPM
429
+ * @param id {string} must be unique
430
+ */
431
+ addOnTempoChangeEvent(callback, id)
432
+ {
433
+ this.onTempoChange[id] = callback;
434
+ }
435
+
436
+ /**
437
+ * Adds a new event that gets called when a meta-event occurs
438
+ * @param callback {function([number, Uint8Array, number, number])} the meta-event type,
439
+ * its data, the track number and MIDI ticks
440
+ * @param id {string} must be unique
441
+ */
442
+ addOnMetaEvent(callback, id)
443
+ {
444
+ this.onMetaEvent[id] = callback;
445
+ }
446
+
447
+ resetMIDIOut()
448
+ {
449
+ if (!this.MIDIout)
450
+ {
451
+ return;
452
+ }
453
+ for (let i = 0; i < 16; i++)
454
+ {
455
+ this.MIDIout.send([messageTypes.controllerChange | i, 120, 0]); // all notes off
456
+ this.MIDIout.send([messageTypes.controllerChange | i, 123, 0]); // all sound off
457
+ }
458
+ this.MIDIout.send([messageTypes.reset]); // reset
459
+ }
460
+
461
+ /**
462
+ * @param messageType {WorkletSequencerMessageType}
463
+ * @param messageData {any}
464
+ * @private
465
+ */
466
+ _sendMessage(messageType, messageData = undefined)
467
+ {
468
+ this.synth.post({
469
+ channelNumber: -1,
470
+ messageType: workletMessageType.sequencerSpecific,
471
+ messageData: {
472
+ messageType: messageType,
473
+ messageData: messageData
474
+ }
475
+ });
476
+ }
477
+
478
+ /**
479
+ * Switch to the next song in the playlist
480
+ */
481
+ nextSong()
482
+ {
483
+ this._sendMessage(WorkletSequencerMessageType.changeSong, [SongChangeType.forwards]);
484
+ }
485
+
486
+ /**
487
+ * Switch to the previous song in the playlist
488
+ */
489
+ previousSong()
490
+ {
491
+ this._sendMessage(WorkletSequencerMessageType.changeSong, [SongChangeType.backwards]);
492
+ }
493
+
494
+ /**
495
+ * Sets the song index in the playlist
496
+ * @param index
497
+ */
498
+ setSongIndex(index)
499
+ {
500
+ const clamped = Math.max(Math.min(this.songsAmount - 1, index), 0);
501
+ this._sendMessage(WorkletSequencerMessageType.changeSong, [SongChangeType.index, clamped]);
502
+ }
503
+
504
+ /**
505
+ * @param type {Object<string, function>}
506
+ * @param params {any}
507
+ * @private
508
+ */
509
+ _callEvents(type, params)
510
+ {
511
+ for (const key in type)
512
+ {
513
+ const callback = type[key];
514
+ try
515
+ {
516
+ callback(params);
517
+ }
518
+ catch (e)
519
+ {
520
+ SpessaSynthWarn(`Failed to execute callback for ${callback[0]}:`, e);
521
+ }
522
+ }
523
+ }
524
+
525
+ /**
526
+ * @param {WorkletSequencerReturnMessageType} messageType
527
+ * @param {any} messageData
528
+ * @private
529
+ */
530
+ _handleMessage(messageType, messageData)
531
+ {
532
+ if (this.ignoreEvents)
533
+ {
534
+ return;
535
+ }
536
+ switch (messageType)
537
+ {
538
+ case WorkletSequencerReturnMessageType.midiEvent:
539
+ /**
540
+ * @type {number[]}
541
+ */
542
+ let midiEventData = messageData;
543
+ if (this.MIDIout)
544
+ {
545
+ if (midiEventData[0] >= 0x80)
546
+ {
547
+ this.MIDIout.send(midiEventData);
548
+ return;
549
+ }
550
+ }
551
+ break;
552
+
553
+ case WorkletSequencerReturnMessageType.songChange:
554
+ this.songIndex = messageData[0];
555
+ const songChangeData = this.songListData[this.songIndex];
556
+ this.midiData = songChangeData;
557
+ this.hasDummyData = false;
558
+ this.absoluteStartTime = 0;
559
+ this.duration = this.midiData.duration;
560
+ this._callEvents(this.onSongChange, songChangeData);
561
+ // if is auto played, unpause
562
+ if (messageData[1] === true)
563
+ {
564
+ this.unpause();
565
+ }
566
+ break;
567
+
568
+ case WorkletSequencerReturnMessageType.timeChange:
569
+ // message data is absolute time
570
+ const time = this.synth.currentTime - messageData;
571
+ this._callEvents(this.onTimeChange, time);
572
+ this._recalculateStartTime(time);
573
+ if (this.paused && this._preservePlaybackState)
574
+ {
575
+ this.pausedTime = time;
576
+ }
577
+ else
578
+ {
579
+ this.unpause();
580
+ }
581
+ break;
582
+
583
+ case WorkletSequencerReturnMessageType.pause:
584
+ this.pausedTime = this.currentTime;
585
+ this.isFinished = messageData;
586
+ if (this.isFinished)
587
+ {
588
+ this._callEvents(this.onSongEnded, undefined);
589
+ }
590
+ break;
591
+
592
+ case WorkletSequencerReturnMessageType.midiError:
593
+ if (this.onError)
594
+ {
595
+ this.onError(messageData);
596
+ }
597
+ else
598
+ {
599
+ throw new Error("Sequencer error: " + messageData);
600
+ }
601
+ return;
602
+
603
+ case WorkletSequencerReturnMessageType.getMIDI:
604
+ if (this._getMIDIResolve)
605
+ {
606
+ this._getMIDIResolve(BasicMIDI.copyFrom(messageData));
607
+ }
608
+ break;
609
+
610
+ case WorkletSequencerReturnMessageType.metaEvent:
611
+ /**
612
+ * @type {MIDIMessage}
613
+ */
614
+ const event = messageData[0];
615
+ switch (event.messageStatusByte)
616
+ {
617
+ case messageTypes.setTempo:
618
+ event.messageData.currentIndex = 0;
619
+ const bpm = 60000000 / readBytesAsUintBigEndian(event.messageData, 3);
620
+ event.messageData.currentIndex = 0;
621
+ this.currentTempo = Math.round(bpm * 100) / 100;
622
+ if (this.onTempoChange)
623
+ {
624
+ this._callEvents(this.onTempoChange, this.currentTempo);
625
+ }
626
+ break;
627
+
628
+ case messageTypes.text:
629
+ case messageTypes.lyric:
630
+ case messageTypes.copyright:
631
+ case messageTypes.trackName:
632
+ case messageTypes.marker:
633
+ case messageTypes.cuePoint:
634
+ case messageTypes.instrumentName:
635
+ case messageTypes.programName:
636
+ let lyricsIndex = -1;
637
+ if (event.messageStatusByte === messageTypes.lyric)
638
+ {
639
+ lyricsIndex = Math.min(
640
+ this.midiData.lyricsTicks.indexOf(event.ticks),
641
+ this.midiData.lyrics.length - 1
642
+ );
643
+ }
644
+ let sentStatus = event.messageStatusByte;
645
+ // if MIDI is a karaoke file, it uses the "text" event type or "lyrics" for lyrics (duh)
646
+ // why?
647
+ // because the MIDI standard is a messy pile of garbage,
648
+ // and it's not my fault that it's like this :(
649
+ // I'm just trying to make the best out of a bad situation.
650
+ // I'm sorry
651
+ // okay I should get back to work
652
+ // anyway,
653
+ // check for a karaoke file and change the status byte to "lyric"
654
+ // if it's a karaoke file
655
+ if (this.midiData.isKaraokeFile && (
656
+ event.messageStatusByte === messageTypes.text ||
657
+ event.messageStatusByte === messageTypes.lyric
658
+ ))
659
+ {
660
+ lyricsIndex = Math.min(
661
+ this.midiData.lyricsTicks.indexOf(event.ticks),
662
+ this.midiData.lyricsTicks.length
663
+ );
664
+ sentStatus = messageTypes.lyric;
665
+ }
666
+ if (this.onTextEvent)
667
+ {
668
+ this.onTextEvent(event.messageData, sentStatus, lyricsIndex, event.ticks);
669
+ }
670
+ break;
671
+ }
672
+ this._callEvents(this.onMetaEvent, messageData);
673
+ break;
674
+
675
+ case WorkletSequencerReturnMessageType.loopCountChange:
676
+ this._loopsRemaining = messageData;
677
+ if (this._loopsRemaining === 0)
678
+ {
679
+ this._loop = false;
680
+ }
681
+ break;
682
+
683
+ case WorkletSequencerReturnMessageType.songListChange:
684
+ this.songListData = messageData;
685
+ break;
686
+
687
+ default:
688
+ break;
689
+ }
690
+ }
691
+
692
+ /**
693
+ * @param time
694
+ * @private
695
+ */
696
+ _recalculateStartTime(time)
697
+ {
698
+ this.absoluteStartTime = this.synth.currentTime - time / this._playbackRate;
699
+ this.highResTimeOffset = (this.synth.currentTime - (performance.now() / 1000)) * this._playbackRate;
700
+ }
701
+
702
+ /**
703
+ * @returns {Promise<MIDI>}
704
+ */
705
+ async getMIDI()
706
+ {
707
+ return new Promise(resolve =>
708
+ {
709
+ this._getMIDIResolve = resolve;
710
+ this._sendMessage(WorkletSequencerMessageType.getMIDI, undefined);
711
+ });
712
+ }
713
+
714
+ /**
715
+ * Loads a new song list
716
+ * @param midiBuffers {MIDIFile[]} - the MIDI files to play
717
+ * @param autoPlay {boolean} - if true, the first sequence will automatically start playing
718
+ */
719
+ loadNewSongList(midiBuffers, autoPlay = true)
720
+ {
721
+ this.pause();
722
+ // add some fake data
723
+ this.midiData = DUMMY_MIDI_DATA;
724
+ this.hasDummyData = true;
725
+ this.duration = 99999;
726
+ /**
727
+ * sanitize MIDIs
728
+ * @type {({binary: ArrayBuffer, altName: string}|BasicMIDI)[]}
729
+ */
730
+ const sanitizedMidis = midiBuffers.map(m =>
731
+ {
732
+ if (m.binary !== undefined)
733
+ {
734
+ return m;
735
+ }
736
+ return BasicMIDI.copyFrom(m);
737
+ });
738
+ this._sendMessage(WorkletSequencerMessageType.loadNewSongList, [sanitizedMidis, autoPlay]);
739
+ this.songIndex = 0;
740
+ this.songsAmount = midiBuffers.length;
741
+ if (this.songsAmount > 1)
742
+ {
743
+ this.loop = false;
744
+ }
745
+ if (autoPlay === false)
746
+ {
747
+ this.pausedTime = this.currentTime;
748
+ }
749
+ }
750
+
751
+ /**
752
+ * @param output {MIDIOutput}
753
+ */
754
+ connectMidiOutput(output)
755
+ {
756
+ this.resetMIDIOut();
757
+ this.MIDIout = output;
758
+ this._sendMessage(WorkletSequencerMessageType.changeMIDIMessageSending, output !== undefined);
759
+ this.currentTime -= 0.1;
760
+ }
761
+
762
+ /**
763
+ * Pauses the playback
764
+ */
765
+ pause()
766
+ {
767
+ if (this.paused)
768
+ {
769
+ SpessaSynthWarn("Already paused");
770
+ return;
771
+ }
772
+ this.pausedTime = this.currentTime;
773
+ this._sendMessage(WorkletSequencerMessageType.pause);
774
+ }
775
+
776
+ unpause()
777
+ {
778
+ this.pausedTime = undefined;
779
+ this.isFinished = false;
780
+ }
781
+
782
+ /**
783
+ * Starts the playback
784
+ * @param resetTime {boolean} If true, time is set to 0 s
785
+ */
786
+ play(resetTime = false)
787
+ {
788
+ if (this.isFinished)
789
+ {
790
+ resetTime = true;
791
+ }
792
+ this._recalculateStartTime(this.pausedTime || 0);
793
+ this.unpause();
794
+ this._sendMessage(WorkletSequencerMessageType.play, resetTime);
795
+ }
796
+
797
+ /**
798
+ * Stops the playback
799
+ */
800
+ stop()
801
+ {
802
+ this._sendMessage(WorkletSequencerMessageType.stop);
803
+ }
804
+ }
spessasynth_lib/sequencer/worklet_sequencer/events.js ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ ALL_CHANNELS_OR_DIFFERENT_ACTION,
3
+ returnMessageType
4
+ } from "../../synthetizer/worklet_system/message_protocol/worklet_message.js";
5
+ import { SongChangeType, WorkletSequencerMessageType, WorkletSequencerReturnMessageType } from "./sequencer_message.js";
6
+ import { messageTypes, midiControllers } from "../../midi_parser/midi_message.js";
7
+
8
+ import { MIDI_CHANNEL_COUNT } from "../../synthetizer/synth_constants.js";
9
+
10
+ /**
11
+ * @param messageType {WorkletSequencerMessageType}
12
+ * @param messageData {any}
13
+ * @this {WorkletSequencer}
14
+ */
15
+ export function processMessage(messageType, messageData)
16
+ {
17
+ switch (messageType)
18
+ {
19
+ default:
20
+ break;
21
+
22
+ case WorkletSequencerMessageType.loadNewSongList:
23
+ this.loadNewSongList(messageData[0], messageData[1]);
24
+ break;
25
+
26
+ case WorkletSequencerMessageType.pause:
27
+ this.pause();
28
+ break;
29
+
30
+ case WorkletSequencerMessageType.play:
31
+ this.play(messageData);
32
+ break;
33
+
34
+ case WorkletSequencerMessageType.stop:
35
+ this.stop();
36
+ break;
37
+
38
+ case WorkletSequencerMessageType.setTime:
39
+ this.currentTime = messageData;
40
+ break;
41
+
42
+ case WorkletSequencerMessageType.changeMIDIMessageSending:
43
+ this.sendMIDIMessages = messageData;
44
+ break;
45
+
46
+ case WorkletSequencerMessageType.setPlaybackRate:
47
+ this.playbackRate = messageData;
48
+ break;
49
+
50
+ case WorkletSequencerMessageType.setLoop:
51
+ const [loop, count] = messageData;
52
+ this.loop = loop;
53
+ if (count === ALL_CHANNELS_OR_DIFFERENT_ACTION)
54
+ {
55
+ this.loopCount = Infinity;
56
+ }
57
+ else
58
+ {
59
+ this.loopCount = count;
60
+ }
61
+ break;
62
+
63
+ case WorkletSequencerMessageType.changeSong:
64
+ switch (messageData[0])
65
+ {
66
+ case SongChangeType.forwards:
67
+ this.nextSong();
68
+ break;
69
+
70
+ case SongChangeType.backwards:
71
+ this.previousSong();
72
+ break;
73
+
74
+ case SongChangeType.shuffleOff:
75
+ this.shuffleMode = false;
76
+ this.songIndex = this.shuffledSongIndexes[this.songIndex];
77
+ break;
78
+
79
+ case SongChangeType.shuffleOn:
80
+ this.shuffleMode = true;
81
+ this.shuffleSongIndexes();
82
+ this.songIndex = 0;
83
+ this.loadCurrentSong();
84
+ break;
85
+
86
+ case SongChangeType.index:
87
+ this.songIndex = messageData[1];
88
+ this.loadCurrentSong();
89
+ break;
90
+ }
91
+ break;
92
+
93
+ case WorkletSequencerMessageType.getMIDI:
94
+ this.post(WorkletSequencerReturnMessageType.getMIDI, this.midiData);
95
+ break;
96
+
97
+ case WorkletSequencerMessageType.setSkipToFirstNote:
98
+ this.skipToFirstNoteOn = messageData;
99
+ break;
100
+
101
+ case WorkletSequencerMessageType.setPreservePlaybackState:
102
+ this.preservePlaybackState = messageData;
103
+ }
104
+ }
105
+
106
+ /**
107
+ *
108
+ * @param messageType {WorkletSequencerReturnMessageType}
109
+ * @param messageData {any}
110
+ * @this {WorkletSequencer}
111
+ */
112
+ export function post(messageType, messageData = undefined)
113
+ {
114
+ if (!this.synth.enableEventSystem)
115
+ {
116
+ return;
117
+ }
118
+ this.synth.post({
119
+ messageType: returnMessageType.sequencerSpecific,
120
+ messageData: {
121
+ messageType: messageType,
122
+ messageData: messageData
123
+ }
124
+ });
125
+ }
126
+
127
+ /**
128
+ * @param message {number[]}
129
+ * @this {WorkletSequencer}
130
+ */
131
+ export function sendMIDIMessage(message)
132
+ {
133
+ this.post(WorkletSequencerReturnMessageType.midiEvent, message);
134
+ }
135
+
136
+ /**
137
+ * @this {WorkletSequencer}
138
+ * @param channel {number}
139
+ * @param type {number}
140
+ * @param value {number}
141
+ */
142
+ export function sendMIDICC(channel, type, value)
143
+ {
144
+ channel %= 16;
145
+ if (!this.sendMIDIMessages)
146
+ {
147
+ return;
148
+ }
149
+ this.sendMIDIMessage([messageTypes.controllerChange | channel, type, value]);
150
+ }
151
+
152
+ /**
153
+ * @this {WorkletSequencer}
154
+ * @param channel {number}
155
+ * @param program {number}
156
+ */
157
+ export function sendMIDIProgramChange(channel, program)
158
+ {
159
+ channel %= 16;
160
+ if (!this.sendMIDIMessages)
161
+ {
162
+ return;
163
+ }
164
+ this.sendMIDIMessage([messageTypes.programChange | channel, program]);
165
+ }
166
+
167
+ /**
168
+ * Sets the pitch of the given channel
169
+ * @this {WorkletSequencer}
170
+ * @param channel {number} usually 0-15: the channel to change pitch
171
+ * @param MSB {number} SECOND byte of the MIDI pitchWheel message
172
+ * @param LSB {number} FIRST byte of the MIDI pitchWheel message
173
+ */
174
+ export function sendMIDIPitchWheel(channel, MSB, LSB)
175
+ {
176
+ channel %= 16;
177
+ if (!this.sendMIDIMessages)
178
+ {
179
+ return;
180
+ }
181
+ this.sendMIDIMessage([messageTypes.pitchBend | channel, LSB, MSB]);
182
+ }
183
+
184
+ /**
185
+ * @this {WorkletSequencer}
186
+ */
187
+ export function sendMIDIReset()
188
+ {
189
+ if (!this.sendMIDIMessages)
190
+ {
191
+ return;
192
+ }
193
+ this.sendMIDIMessage([messageTypes.reset]);
194
+ for (let ch = 0; ch < MIDI_CHANNEL_COUNT; ch++)
195
+ {
196
+ this.sendMIDIMessage([messageTypes.controllerChange | ch, midiControllers.allSoundOff, 0]);
197
+ this.sendMIDIMessage([messageTypes.controllerChange | ch, midiControllers.resetAllControllers, 0]);
198
+ }
199
+ }
spessasynth_lib/sequencer/worklet_sequencer/play.js ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getEvent, messageTypes, midiControllers } from "../../midi_parser/midi_message.js";
2
+ import { WorkletSequencerReturnMessageType } from "./sequencer_message.js";
3
+ import { resetArray } from "../../synthetizer/worklet_system/worklet_utilities/controller_tables.js";
4
+ import {
5
+ nonResetableCCs
6
+ } from "../../synthetizer/worklet_system/worklet_methods/controller_control/reset_controllers.js";
7
+
8
+
9
+ // an array with preset default values
10
+ const defaultControllerArray = resetArray.slice(0, 128);
11
+
12
+ /**
13
+ * plays from start to the target time, excluding note messages (to get the synth to the correct state)
14
+ * @private
15
+ * @param time {number} in seconds
16
+ * @param ticks {number} optional MIDI ticks, when given is used instead of time
17
+ * @returns {boolean} true if the midi file is not finished
18
+ * @this {WorkletSequencer}
19
+ */
20
+ export function _playTo(time, ticks = undefined)
21
+ {
22
+ this.oneTickToSeconds = 60 / (120 * this.midiData.timeDivision);
23
+ // reset
24
+ this.synth.resetAllControllers();
25
+ this.sendMIDIReset();
26
+ this._resetTimers();
27
+
28
+ const channelsToSave = this.synth.workletProcessorChannels.length;
29
+ /**
30
+ * save pitch bends here and send them only after
31
+ * @type {number[]}
32
+ */
33
+ const pitchBends = Array(channelsToSave).fill(8192);
34
+
35
+ /**
36
+ * Save programs here and send them only after
37
+ * @type {{program: number, bank: number, actualBank: number}[]}
38
+ */
39
+ const programs = [];
40
+ for (let i = 0; i < channelsToSave; i++)
41
+ {
42
+ programs.push({
43
+ program: -1,
44
+ bank: 0,
45
+ actualBank: 0
46
+ });
47
+ }
48
+
49
+ const isCCNonSkippable = controllerNumber => (
50
+ controllerNumber === midiControllers.dataDecrement ||
51
+ controllerNumber === midiControllers.dataIncrement ||
52
+ controllerNumber === midiControllers.dataEntryMsb ||
53
+ controllerNumber === midiControllers.dataDecrement ||
54
+ controllerNumber === midiControllers.lsbForControl6DataEntry ||
55
+ controllerNumber === midiControllers.RPNLsb ||
56
+ controllerNumber === midiControllers.RPNMsb ||
57
+ controllerNumber === midiControllers.NRPNLsb ||
58
+ controllerNumber === midiControllers.NRPNMsb ||
59
+ controllerNumber === midiControllers.bankSelect ||
60
+ controllerNumber === midiControllers.lsbForControl0BankSelect ||
61
+ controllerNumber === midiControllers.resetAllControllers
62
+ );
63
+
64
+ /**
65
+ * Save controllers here and send them only after
66
+ * @type {number[][]}
67
+ */
68
+ const savedControllers = [];
69
+ for (let i = 0; i < channelsToSave; i++)
70
+ {
71
+ savedControllers.push(Array.from(defaultControllerArray));
72
+ }
73
+
74
+ /**
75
+ * RP-15 compliant reset
76
+ * https://amei.or.jp/midistandardcommittee/Recommended_Practice/e/rp15.pdf
77
+ * @param chan {number}
78
+ */
79
+ function resetAllControlllers(chan)
80
+ {
81
+ // reset pitch bend
82
+ pitchBends[chan] = 8192;
83
+ if (savedControllers?.[chan] === undefined)
84
+ {
85
+ return;
86
+ }
87
+ for (let i = 0; i < defaultControllerArray.length; i++)
88
+ {
89
+ if (!nonResetableCCs.has(i))
90
+ {
91
+ savedControllers[chan][i] = defaultControllerArray[i];
92
+ }
93
+ }
94
+ }
95
+
96
+ while (true)
97
+ {
98
+ // find the next event
99
+ let trackIndex = this._findFirstEventIndex();
100
+ let event = this.tracks[trackIndex][this.eventIndex[trackIndex]];
101
+ if (ticks !== undefined)
102
+ {
103
+ if (event.ticks >= ticks)
104
+ {
105
+ break;
106
+ }
107
+ }
108
+ else
109
+ {
110
+ if (this.playedTime >= time)
111
+ {
112
+ break;
113
+ }
114
+ }
115
+
116
+ // skip note ons
117
+ const info = getEvent(event.messageStatusByte);
118
+ // Keep in mind midi ports to determine the channel!
119
+ const channel = info.channel + (this.midiPortChannelOffsets[this.midiPorts[trackIndex]] || 0);
120
+ switch (info.status)
121
+ {
122
+ // skip note messages
123
+ case messageTypes.noteOn:
124
+ // track portamento control as last note
125
+ if (savedControllers[channel] === undefined)
126
+ {
127
+ savedControllers[channel] = Array.from(defaultControllerArray);
128
+ }
129
+ savedControllers[channel][midiControllers.portamentoControl] = event.messageData[0];
130
+ break;
131
+
132
+ case messageTypes.noteOff:
133
+ break;
134
+
135
+ // skip pitch bend
136
+ case messageTypes.pitchBend:
137
+ pitchBends[channel] = event.messageData[1] << 7 | event.messageData[0];
138
+ break;
139
+
140
+ case messageTypes.programChange:
141
+ // empty tracks cannot program change
142
+ if (this.midiData.isMultiPort && this.midiData.usedChannelsOnTrack[trackIndex].size === 0)
143
+ {
144
+ break;
145
+ }
146
+ const p = programs[channel];
147
+ p.program = event.messageData[0];
148
+ p.actualBank = p.bank;
149
+ break;
150
+
151
+ case messageTypes.controllerChange:
152
+ // empty tracks cannot controller change
153
+ if (this.midiData.isMultiPort && this.midiData.usedChannelsOnTrack[trackIndex].size === 0)
154
+ {
155
+ break;
156
+ }
157
+ // do not skip data entries
158
+ const controllerNumber = event.messageData[0];
159
+ if (isCCNonSkippable(controllerNumber))
160
+ {
161
+ let ccV = event.messageData[1];
162
+ if (controllerNumber === midiControllers.bankSelect)
163
+ {
164
+ // add the bank to be saved
165
+ programs[channel].bank = ccV;
166
+ break;
167
+ }
168
+ else if (controllerNumber === midiControllers.resetAllControllers)
169
+ {
170
+ resetAllControlllers(channel);
171
+ }
172
+ if (this.sendMIDIMessages)
173
+ {
174
+ this.sendMIDICC(channel, controllerNumber, ccV);
175
+ }
176
+ else
177
+ {
178
+ this.synth.controllerChange(channel, controllerNumber, ccV);
179
+ }
180
+ }
181
+ else
182
+ {
183
+ if (savedControllers[channel] === undefined)
184
+ {
185
+ savedControllers[channel] = Array.from(defaultControllerArray);
186
+ }
187
+ savedControllers[channel][controllerNumber] = event.messageData[1];
188
+ }
189
+ break;
190
+
191
+ default:
192
+ this._processEvent(event, trackIndex);
193
+ break;
194
+ }
195
+
196
+ this.eventIndex[trackIndex]++;
197
+ // find the next event
198
+ trackIndex = this._findFirstEventIndex();
199
+ let nextEvent = this.tracks[trackIndex][this.eventIndex[trackIndex]];
200
+ if (nextEvent === undefined)
201
+ {
202
+ this.stop();
203
+ return false;
204
+ }
205
+ this.playedTime += this.oneTickToSeconds * (nextEvent.ticks - event.ticks);
206
+ }
207
+
208
+ // restoring saved controllers
209
+ if (this.sendMIDIMessages)
210
+ {
211
+ for (let channelNumber = 0; channelNumber < channelsToSave; channelNumber++)
212
+ {
213
+ // restore pitch bends
214
+ if (pitchBends[channelNumber] !== undefined)
215
+ {
216
+ this.sendMIDIPitchWheel(
217
+ channelNumber,
218
+ pitchBends[channelNumber] >> 7,
219
+ pitchBends[channelNumber] & 0x7F
220
+ );
221
+ }
222
+ if (savedControllers[channelNumber] !== undefined)
223
+ {
224
+ // every controller that has changed
225
+ savedControllers[channelNumber].forEach((value, index) =>
226
+ {
227
+ if (value !== defaultControllerArray[index] && !isCCNonSkippable(
228
+ index))
229
+ {
230
+ this.sendMIDICC(channelNumber, index, value);
231
+ }
232
+ });
233
+ }
234
+ // restore programs
235
+ if (programs[channelNumber].program >= 0 && programs[channelNumber].actualBank >= 0)
236
+ {
237
+ const bank = programs[channelNumber].actualBank;
238
+ this.sendMIDICC(channelNumber, midiControllers.bankSelect, bank);
239
+ this.sendMIDIProgramChange(channelNumber, programs[channelNumber].program);
240
+ }
241
+ }
242
+ }
243
+ else
244
+ {
245
+ // for all synth channels
246
+ for (let channelNumber = 0; channelNumber < channelsToSave; channelNumber++)
247
+ {
248
+ // restore pitch bends
249
+ if (pitchBends[channelNumber] !== undefined)
250
+ {
251
+ this.synth.pitchWheel(channelNumber, pitchBends[channelNumber] >> 7, pitchBends[channelNumber] & 0x7F);
252
+ }
253
+ if (savedControllers[channelNumber] !== undefined)
254
+ {
255
+ // every controller that has changed
256
+ savedControllers[channelNumber].forEach((value, index) =>
257
+ {
258
+ if (value !== defaultControllerArray[index] && !isCCNonSkippable(
259
+ index))
260
+ {
261
+ this.synth.controllerChange(
262
+ channelNumber,
263
+ index,
264
+ value
265
+ );
266
+ }
267
+ });
268
+ }
269
+ // restore programs
270
+ if (programs[channelNumber].program >= 0 && programs[channelNumber].actualBank >= 0)
271
+ {
272
+ const bank = programs[channelNumber].actualBank;
273
+ this.synth.controllerChange(channelNumber, midiControllers.bankSelect, bank);
274
+ this.synth.programChange(channelNumber, programs[channelNumber].program);
275
+ }
276
+ }
277
+ }
278
+ return true;
279
+ }
280
+
281
+ /**
282
+ * Starts the playback
283
+ * @param resetTime {boolean} If true, time is set to 0 s
284
+ * @this {WorkletSequencer}
285
+ */
286
+ export function play(resetTime = false)
287
+ {
288
+ if (this.midiData === undefined)
289
+ {
290
+ return;
291
+ }
292
+
293
+ // reset the time if necessary
294
+ if (resetTime)
295
+ {
296
+ this.pausedTime = undefined;
297
+ this.currentTime = 0;
298
+ return;
299
+ }
300
+
301
+ if (this.currentTime >= this.duration)
302
+ {
303
+ this.pausedTime = undefined;
304
+ this.currentTime = 0;
305
+ return;
306
+ }
307
+
308
+ // unpause if paused
309
+ if (this.paused)
310
+ {
311
+ // adjust the start time
312
+ this._recalculateStartTime(this.pausedTime);
313
+ this.pausedTime = undefined;
314
+ }
315
+ if (!this.sendMIDIMessages)
316
+ {
317
+ this.playingNotes.forEach(n =>
318
+ {
319
+ this.synth.noteOn(n.channel, n.midiNote, n.velocity);
320
+ });
321
+ }
322
+ this.setProcessHandler();
323
+ }
324
+
325
+ /**
326
+ * @this {WorkletSequencer}
327
+ * @param ticks {number}
328
+ */
329
+ export function setTimeTicks(ticks)
330
+ {
331
+ this.stop();
332
+ this.playingNotes = [];
333
+ this.pausedTime = undefined;
334
+ this.post(
335
+ WorkletSequencerReturnMessageType.timeChange,
336
+ currentTime - this.midiData.MIDIticksToSeconds(ticks)
337
+ );
338
+ const isNotFinished = this._playTo(0, ticks);
339
+ this._recalculateStartTime(this.playedTime);
340
+ if (!isNotFinished)
341
+ {
342
+ return;
343
+ }
344
+ this.play();
345
+ }
346
+
347
+ /**
348
+ * @param time
349
+ * @private
350
+ * @this {WorkletSequencer}
351
+ */
352
+ export function _recalculateStartTime(time)
353
+ {
354
+ this.absoluteStartTime = currentTime - time / this._playbackRate;
355
+ }
spessasynth_lib/sequencer/worklet_sequencer/process_event.js ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getEvent, messageTypes } from "../../midi_parser/midi_message.js";
2
+ import { WorkletSequencerReturnMessageType } from "./sequencer_message.js";
3
+ import { consoleColors } from "../../utils/other.js";
4
+ import { SpessaSynthWarn } from "../../utils/loggin.js";
5
+ import { readBytesAsUintBigEndian } from "../../utils/byte_functions/big_endian.js";
6
+
7
+ /**
8
+ * Processes a single event
9
+ * @param event {MIDIMessage}
10
+ * @param trackIndex {number}
11
+ * @this {WorkletSequencer}
12
+ * @private
13
+ */
14
+ export function _processEvent(event, trackIndex)
15
+ {
16
+ if (this.sendMIDIMessages)
17
+ {
18
+ if (event.messageStatusByte >= 0x80)
19
+ {
20
+ this.sendMIDIMessage([event.messageStatusByte, ...event.messageData]);
21
+ return;
22
+ }
23
+ }
24
+ const statusByteData = getEvent(event.messageStatusByte);
25
+ const offset = this.midiPortChannelOffsets[this.midiPorts[trackIndex]] || 0;
26
+ statusByteData.channel += offset;
27
+ // process the event
28
+ switch (statusByteData.status)
29
+ {
30
+ case messageTypes.noteOn:
31
+ const velocity = event.messageData[1];
32
+ if (velocity > 0)
33
+ {
34
+ this.synth.noteOn(statusByteData.channel, event.messageData[0], velocity);
35
+ this.playingNotes.push({
36
+ midiNote: event.messageData[0],
37
+ channel: statusByteData.channel,
38
+ velocity: velocity
39
+ });
40
+ }
41
+ else
42
+ {
43
+ this.synth.noteOff(statusByteData.channel, event.messageData[0]);
44
+ const toDelete = this.playingNotes.findIndex(n =>
45
+ n.midiNote === event.messageData[0] && n.channel === statusByteData.channel);
46
+ if (toDelete !== -1)
47
+ {
48
+ this.playingNotes.splice(toDelete, 1);
49
+ }
50
+ }
51
+ break;
52
+
53
+ case messageTypes.noteOff:
54
+ this.synth.noteOff(statusByteData.channel, event.messageData[0]);
55
+ const toDelete = this.playingNotes.findIndex(n =>
56
+ n.midiNote === event.messageData[0] && n.channel === statusByteData.channel);
57
+ if (toDelete !== -1)
58
+ {
59
+ this.playingNotes.splice(toDelete, 1);
60
+ }
61
+ break;
62
+
63
+ case messageTypes.pitchBend:
64
+ this.synth.pitchWheel(statusByteData.channel, event.messageData[1], event.messageData[0]);
65
+ break;
66
+
67
+ case messageTypes.controllerChange:
68
+ // empty tracks cannot cc change
69
+ if (this.midiData.isMultiPort && this.midiData.usedChannelsOnTrack[trackIndex].size === 0)
70
+ {
71
+ return;
72
+ }
73
+ this.synth.controllerChange(statusByteData.channel, event.messageData[0], event.messageData[1]);
74
+ break;
75
+
76
+ case messageTypes.programChange:
77
+ // empty tracks cannot program change
78
+ if (this.midiData.isMultiPort && this.midiData.usedChannelsOnTrack[trackIndex].size === 0)
79
+ {
80
+ return;
81
+ }
82
+ this.synth.programChange(statusByteData.channel, event.messageData[0]);
83
+ break;
84
+
85
+ case messageTypes.polyPressure:
86
+ this.synth.polyPressure(statusByteData.channel, event.messageData[0], event.messageData[1]);
87
+ break;
88
+
89
+ case messageTypes.channelPressure:
90
+ this.synth.channelPressure(statusByteData.channel, event.messageData[0]);
91
+ break;
92
+
93
+ case messageTypes.systemExclusive:
94
+ this.synth.systemExclusive(event.messageData, offset);
95
+ break;
96
+
97
+ case messageTypes.setTempo:
98
+ event.messageData.currentIndex = 0;
99
+ let tempoBPM = 60000000 / readBytesAsUintBigEndian(event.messageData, 3);
100
+ this.oneTickToSeconds = 60 / (tempoBPM * this.midiData.timeDivision);
101
+ if (this.oneTickToSeconds === 0)
102
+ {
103
+ this.oneTickToSeconds = 60 / (120 * this.midiData.timeDivision);
104
+ SpessaSynthWarn("invalid tempo! falling back to 120 BPM");
105
+ tempoBPM = 120;
106
+ }
107
+ break;
108
+
109
+ // recognized but ignored
110
+ case messageTypes.timeSignature:
111
+ case messageTypes.endOfTrack:
112
+ case messageTypes.midiChannelPrefix:
113
+ case messageTypes.songPosition:
114
+ case messageTypes.activeSensing:
115
+ case messageTypes.keySignature:
116
+ case messageTypes.sequenceNumber:
117
+ case messageTypes.sequenceSpecific:
118
+ case messageTypes.text:
119
+ case messageTypes.lyric:
120
+ case messageTypes.copyright:
121
+ case messageTypes.trackName:
122
+ case messageTypes.marker:
123
+ case messageTypes.cuePoint:
124
+ case messageTypes.instrumentName:
125
+ case messageTypes.programName:
126
+ break;
127
+
128
+
129
+ case messageTypes.midiPort:
130
+ this.assignMIDIPort(trackIndex, event.messageData[0]);
131
+ break;
132
+
133
+ case messageTypes.reset:
134
+ this.synth.stopAllChannels();
135
+ this.synth.resetAllControllers();
136
+ break;
137
+
138
+ default:
139
+ SpessaSynthWarn(
140
+ `%cUnrecognized Event: %c${event.messageStatusByte}%c status byte: %c${Object.keys(
141
+ messageTypes).find(k => messageTypes[k] === statusByteData.status)}`,
142
+ consoleColors.warn,
143
+ consoleColors.unrecognized,
144
+ consoleColors.warn,
145
+ consoleColors.value
146
+ );
147
+ break;
148
+ }
149
+ if (statusByteData.status >= 0 && statusByteData.status < 0x80)
150
+ {
151
+ this.post(
152
+ WorkletSequencerReturnMessageType.metaEvent,
153
+ [event, trackIndex]
154
+ );
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Adds 16 channels to the synth
160
+ * @this {WorkletSequencer}
161
+ * @private
162
+ */
163
+ export function _addNewMidiPort()
164
+ {
165
+ for (let i = 0; i < 16; i++)
166
+ {
167
+ this.synth.createWorkletChannel(true);
168
+ }
169
+ }
spessasynth_lib/sequencer/worklet_sequencer/process_tick.js ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { WorkletSequencerReturnMessageType } from "./sequencer_message.js";
2
+
3
+ /**
4
+ * Processes a single tick
5
+ * @this {WorkletSequencer}
6
+ */
7
+ export function processTick()
8
+ {
9
+ if (!this.isActive)
10
+ {
11
+ return;
12
+ }
13
+ let current = this.currentTime;
14
+ while (this.playedTime < current)
15
+ {
16
+ // find the next event
17
+ let trackIndex = this._findFirstEventIndex();
18
+ let event = this.tracks[trackIndex][this.eventIndex[trackIndex]];
19
+ this._processEvent(event, trackIndex);
20
+
21
+ this.eventIndex[trackIndex]++;
22
+
23
+ // find the next event
24
+ trackIndex = this._findFirstEventIndex();
25
+ if (this.tracks[trackIndex].length <= this.eventIndex[trackIndex])
26
+ {
27
+ // the song has ended
28
+ if (this.loop)
29
+ {
30
+ this.setTimeTicks(this.midiData.loop.start);
31
+ return;
32
+ }
33
+ this.eventIndex[trackIndex]--;
34
+ this.pause(true);
35
+ if (this.songs.length > 1)
36
+ {
37
+ this.nextSong();
38
+ }
39
+ return;
40
+ }
41
+ let eventNext = this.tracks[trackIndex][this.eventIndex[trackIndex]];
42
+ this.playedTime += this.oneTickToSeconds * (eventNext.ticks - event.ticks);
43
+
44
+ const canLoop = this.loop && (this.loopCount > 0 || this.loopCount === -1);
45
+
46
+ // if we reached loop.end
47
+ if ((this.midiData.loop.end <= event.ticks) && canLoop)
48
+ {
49
+ // loop
50
+ if (this.loopCount !== Infinity)
51
+ {
52
+ this.loopCount--;
53
+ this.post(WorkletSequencerReturnMessageType.loopCountChange, this.loopCount);
54
+ }
55
+ this.setTimeTicks(this.midiData.loop.start);
56
+ return;
57
+ }
58
+ // if the song has ended
59
+ else if (current >= this.duration)
60
+ {
61
+ if (canLoop)
62
+ {
63
+ // loop
64
+ if (this.loopCount !== Infinity)
65
+ {
66
+ this.loopCount--;
67
+ this.post(WorkletSequencerReturnMessageType.loopCountChange, this.loopCount);
68
+ }
69
+ this.setTimeTicks(this.midiData.loop.start);
70
+ return;
71
+ }
72
+ // stop the playback
73
+ this.eventIndex[trackIndex]--;
74
+ this.pause(true);
75
+ if (this.songs.length > 1)
76
+ {
77
+ this.nextSong();
78
+ }
79
+ return;
80
+ }
81
+ }
82
+ }
83
+
84
+
85
+ /**
86
+ * @returns {number} the index of the first to the current played time
87
+ * @this {WorkletSequencer}
88
+ */
89
+ export function _findFirstEventIndex()
90
+ {
91
+ let index = 0;
92
+ let ticks = Infinity;
93
+ this.tracks.forEach((track, i) =>
94
+ {
95
+ if (this.eventIndex[i] >= track.length)
96
+ {
97
+ return;
98
+ }
99
+ if (track[this.eventIndex[i]].ticks < ticks)
100
+ {
101
+ index = i;
102
+ ticks = track[this.eventIndex[i]].ticks;
103
+ }
104
+ });
105
+ return index;
106
+ }
spessasynth_lib/sequencer/worklet_sequencer/sequencer_message.js ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const SongChangeType = {
2
+ backwards: 0, // no additional data
3
+ forwards: 1, // no additional data
4
+ shuffleOn: 2, // no additional data
5
+ shuffleOff: 3, // no additional data
6
+ index: 4 // songIndex<number>
7
+ };
8
+
9
+ /**
10
+ * @enum {number}
11
+ * @property {number} loadNewSongList - 0 -> [...song<MIDI>]
12
+ * @property {number} pause - 1 -> isFinished<boolean>
13
+ * @property {number} stop - 2 -> (no data)
14
+ * @property {number} play - 3 -> resetTime<boolean>
15
+ * @property {number} setTime - 4 -> time<number>
16
+ * @property {number} changeMIDIMessageSending - 5 -> sendMIDIMessages<boolean>
17
+ * @property {number} setPlaybackRate - 6 -> playbackRate<number>
18
+ * @property {number} setLoop - 7 -> [loop<boolean>, count<number>]
19
+ * @property {number} changeSong - 8 -> [changeType<SongChangeType>, data<number>]
20
+ * @property {number} getMIDI - 9 -> (no data)
21
+ * @property {number} setSkipToFirstNote -10 -> skipToFirstNoteOn<boolean>
22
+ * @property {number} setPreservePlaybackState -11 -> preservePlaybackState<boolean>
23
+ */
24
+ export const WorkletSequencerMessageType = {
25
+ loadNewSongList: 0,
26
+ pause: 1,
27
+ stop: 2,
28
+ play: 3,
29
+ setTime: 4,
30
+ changeMIDIMessageSending: 5,
31
+ setPlaybackRate: 6,
32
+ setLoop: 7,
33
+ changeSong: 8,
34
+ getMIDI: 9,
35
+ setSkipToFirstNote: 10,
36
+ setPreservePlaybackState: 11
37
+ };
38
+
39
+ /**
40
+ *
41
+ * @enum {number}
42
+ */
43
+ export const WorkletSequencerReturnMessageType = {
44
+ midiEvent: 0, // [...midiEventBytes<number>]
45
+ songChange: 1, // [songIndex<number>, isAutoPlayed<boolean>]
46
+ timeChange: 2, // newAbsoluteTime<number>
47
+ pause: 3, // no data
48
+ getMIDI: 4, // midiData<MIDI>
49
+ midiError: 5, // errorMSG<string>
50
+ metaEvent: 6, // [event<MIDIMessage>, trackNum<number>]
51
+ loopCountChange: 7, // newLoopCount<number>
52
+ songListChange: 8 // songListData<MIDIData[]>
53
+ };
spessasynth_lib/sequencer/worklet_sequencer/song_control.js ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { WorkletSequencerReturnMessageType } from "./sequencer_message.js";
2
+ import { consoleColors, formatTime } from "../../utils/other.js";
3
+ import {
4
+ SpessaSynthGroupCollapsed,
5
+ SpessaSynthGroupEnd,
6
+ SpessaSynthInfo,
7
+ SpessaSynthWarn
8
+ } from "../../utils/loggin.js";
9
+ import { MIDIData } from "../../midi_parser/midi_data.js";
10
+ import { MIDI } from "../../midi_parser/midi_loader.js";
11
+ import { BasicMIDI } from "../../midi_parser/basic_midi.js";
12
+
13
+
14
+ /**
15
+ * @param trackNum {number}
16
+ * @param port {number}
17
+ * @this {WorkletSequencer}
18
+ */
19
+ export function assignMIDIPort(trackNum, port)
20
+ {
21
+ // do not assign ports to empty tracks
22
+ if (this.midiData.usedChannelsOnTrack[trackNum].size === 0)
23
+ {
24
+ return;
25
+ }
26
+
27
+ // assign new 16 channels if the port is not occupied yet
28
+ if (this.midiPortChannelOffset === 0)
29
+ {
30
+ this.midiPortChannelOffset += 16;
31
+ this.midiPortChannelOffsets[port] = 0;
32
+ }
33
+
34
+ if (this.midiPortChannelOffsets[port] === undefined)
35
+ {
36
+ if (this.synth.workletProcessorChannels.length < this.midiPortChannelOffset + 15)
37
+ {
38
+ this._addNewMidiPort();
39
+ }
40
+ this.midiPortChannelOffsets[port] = this.midiPortChannelOffset;
41
+ this.midiPortChannelOffset += 16;
42
+ }
43
+
44
+ this.midiPorts[trackNum] = port;
45
+ }
46
+
47
+ /**
48
+ * Loads a new sequence
49
+ * @param parsedMidi {BasicMIDI}
50
+ * @param autoPlay {boolean}
51
+ * @this {WorkletSequencer}
52
+ */
53
+ export function loadNewSequence(parsedMidi, autoPlay = true)
54
+ {
55
+ this.stop();
56
+ if (!parsedMidi.tracks)
57
+ {
58
+ throw new Error("This MIDI has no tracks!");
59
+ }
60
+
61
+ this.oneTickToSeconds = 60 / (120 * parsedMidi.timeDivision);
62
+
63
+ /**
64
+ * @type {BasicMIDI}
65
+ */
66
+ this.midiData = parsedMidi;
67
+
68
+ // check for embedded soundfont
69
+ if (this.midiData.embeddedSoundFont !== undefined)
70
+ {
71
+ SpessaSynthInfo("%cEmbedded soundfont detected! Using it.", consoleColors.recognized);
72
+ this.synth.setEmbeddedSoundFont(this.midiData.embeddedSoundFont, this.midiData.bankOffset);
73
+ }
74
+ else
75
+ {
76
+ if (this.synth.overrideSoundfont)
77
+ {
78
+ // clean up the embedded soundfont
79
+ this.synth.clearSoundFont(true, true);
80
+ }
81
+ SpessaSynthGroupCollapsed("%cPreloading samples...", consoleColors.info);
82
+ // smart preloading: load only samples used in the midi!
83
+ const used = this.midiData.getUsedProgramsAndKeys(this.synth.soundfontManager);
84
+ for (const [programBank, combos] of Object.entries(used))
85
+ {
86
+ const bank = parseInt(programBank.split(":")[0]);
87
+ const program = parseInt(programBank.split(":")[1]);
88
+ const preset = this.synth.getPreset(bank, program);
89
+ SpessaSynthInfo(
90
+ `%cPreloading used samples on %c${preset.presetName}%c...`,
91
+ consoleColors.info,
92
+ consoleColors.recognized,
93
+ consoleColors.info
94
+ );
95
+ for (const combo of combos)
96
+ {
97
+ const split = combo.split("-");
98
+ preset.preloadSpecific(parseInt(split[0]), parseInt(split[1]));
99
+ }
100
+ }
101
+ SpessaSynthGroupEnd();
102
+ }
103
+
104
+ /**
105
+ * the midi track data
106
+ * @type {MIDIMessage[][]}
107
+ */
108
+ this.tracks = this.midiData.tracks;
109
+
110
+ // copy over the port data
111
+ this.midiPorts = this.midiData.midiPorts.slice();
112
+
113
+ // clear last port data
114
+ this.midiPortChannelOffset = 0;
115
+ this.midiPortChannelOffsets = {};
116
+ // assign port offsets
117
+ this.midiData.midiPorts.forEach((port, trackIndex) =>
118
+ {
119
+ this.assignMIDIPort(trackIndex, port);
120
+ });
121
+
122
+ /**
123
+ * Same as "audio.duration" property (seconds)
124
+ * @type {number}
125
+ */
126
+ this.duration = this.midiData.duration;
127
+ this.firstNoteTime = this.midiData.MIDIticksToSeconds(this.midiData.firstNoteOn);
128
+ SpessaSynthInfo(`%cTotal song time: ${formatTime(Math.ceil(this.duration)).time}`, consoleColors.recognized);
129
+
130
+ this.post(WorkletSequencerReturnMessageType.songChange, [this.songIndex, autoPlay]);
131
+
132
+ if (this.duration <= 1)
133
+ {
134
+ SpessaSynthWarn(
135
+ `%cVery short song: (${formatTime(Math.round(this.duration)).time}). Disabling loop!`,
136
+ consoleColors.warn
137
+ );
138
+ this.loop = false;
139
+ }
140
+ if (autoPlay)
141
+ {
142
+ this.play(true);
143
+ }
144
+ else
145
+ {
146
+ // this shall not play: play to the first note and then wait
147
+ const targetTime = this.skipToFirstNoteOn ? this.midiData.firstNoteOn - 1 : 0;
148
+ this.setTimeTicks(targetTime);
149
+ this.pause();
150
+ }
151
+ }
152
+
153
+ /**
154
+ * @param midiBuffers {MIDIFile[]}
155
+ * @param autoPlay {boolean}
156
+ * @this {WorkletSequencer}
157
+ */
158
+ export function loadNewSongList(midiBuffers, autoPlay = true)
159
+ {
160
+ /**
161
+ * parse the MIDIs (only the array buffers, MIDI is unchanged)
162
+ * @type {BasicMIDI[]}
163
+ */
164
+ this.songs = midiBuffers.reduce((mids, b) =>
165
+ {
166
+ if (b.duration)
167
+ {
168
+ mids.push(BasicMIDI.copyFrom(b));
169
+ return mids;
170
+ }
171
+ try
172
+ {
173
+ mids.push(new MIDI(b.binary, b.altName || ""));
174
+ }
175
+ catch (e)
176
+ {
177
+ console.error(e);
178
+ this.post(WorkletSequencerReturnMessageType.midiError, e);
179
+ return mids;
180
+ }
181
+ return mids;
182
+ }, []);
183
+ if (this.songs.length < 1)
184
+ {
185
+ return;
186
+ }
187
+ this.songIndex = 0;
188
+ if (this.songs.length > 1)
189
+ {
190
+ this.loop = false;
191
+ }
192
+ this.shuffleSongIndexes();
193
+ const midiDatas = this.songs.map(s => new MIDIData(s));
194
+ this.post(WorkletSequencerReturnMessageType.songListChange, midiDatas);
195
+ this.loadCurrentSong(autoPlay);
196
+ }
197
+
198
+ /**
199
+ * @this {WorkletSequencer}
200
+ */
201
+ export function nextSong()
202
+ {
203
+ if (this.songs.length === 1)
204
+ {
205
+ this.currentTime = 0;
206
+ return;
207
+ }
208
+ this.songIndex++;
209
+ this.songIndex %= this.songs.length;
210
+ this.loadCurrentSong();
211
+ }
212
+
213
+ /**
214
+ * @this {WorkletSequencer}
215
+ */
216
+ export function previousSong()
217
+ {
218
+ if (this.songs.length === 1)
219
+ {
220
+ this.currentTime = 0;
221
+ return;
222
+ }
223
+ this.songIndex--;
224
+ if (this.songIndex < 0)
225
+ {
226
+ this.songIndex = this.songs.length - 1;
227
+ }
228
+ this.loadCurrentSong();
229
+ }
spessasynth_lib/sequencer/worklet_sequencer/worklet_sequencer.js ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { WorkletSequencerReturnMessageType } from "./sequencer_message.js";
2
+ import { _addNewMidiPort, _processEvent } from "./process_event.js";
3
+ import { _findFirstEventIndex, processTick } from "./process_tick.js";
4
+ import { assignMIDIPort, loadNewSequence, loadNewSongList, nextSong, previousSong } from "./song_control.js";
5
+ import { _playTo, _recalculateStartTime, play, setTimeTicks } from "./play.js";
6
+ import { messageTypes, midiControllers } from "../../midi_parser/midi_message.js";
7
+ import {
8
+ post,
9
+ processMessage,
10
+ sendMIDICC,
11
+ sendMIDIMessage,
12
+ sendMIDIPitchWheel,
13
+ sendMIDIProgramChange,
14
+ sendMIDIReset
15
+ } from "./events.js";
16
+ import { SpessaSynthWarn } from "../../utils/loggin.js";
17
+
18
+ import { MIDI_CHANNEL_COUNT } from "../../synthetizer/synth_constants.js";
19
+
20
+ class WorkletSequencer
21
+ {
22
+ /**
23
+ * All the sequencer's songs
24
+ * @type {BasicMIDI[]}
25
+ */
26
+ songs = [];
27
+
28
+ /**
29
+ * Current song index
30
+ * @type {number}
31
+ */
32
+ songIndex = 0;
33
+
34
+ /**
35
+ * shuffled song indexes
36
+ * @type {number[]}
37
+ */
38
+ shuffledSongIndexes = [];
39
+
40
+ /**
41
+ * the synth to use
42
+ * @type {SpessaSynthProcessor}
43
+ */
44
+ synth;
45
+
46
+ /**
47
+ * if the sequencer is active
48
+ * @type {boolean}
49
+ */
50
+ isActive = false;
51
+
52
+ /**
53
+ * If the event should instead be sent back to the main thread instead of synth
54
+ * @type {boolean}
55
+ */
56
+ sendMIDIMessages = false;
57
+
58
+ /**
59
+ * sequencer's loop count
60
+ * @type {number}
61
+ */
62
+ loopCount = Infinity;
63
+
64
+ /**
65
+ * event's number in this.events
66
+ * @type {number[]}
67
+ */
68
+ eventIndex = [];
69
+
70
+ /**
71
+ * tracks the time that has already been played
72
+ * @type {number}
73
+ */
74
+ playedTime = 0;
75
+
76
+ /**
77
+ * The (relative) time when the sequencer was paused. If it's not paused, then it's undefined.
78
+ * @type {number}
79
+ */
80
+ pausedTime = undefined;
81
+
82
+ /**
83
+ * Absolute playback startTime, bases on the synth's time
84
+ * @type {number}
85
+ */
86
+ absoluteStartTime = currentTime;
87
+ /**
88
+ * Currently playing notes (for pausing and resuming)
89
+ * @type {{
90
+ * midiNote: number,
91
+ * channel: number,
92
+ * velocity: number
93
+ * }[]}
94
+ */
95
+ playingNotes = [];
96
+
97
+ /**
98
+ * controls if the sequencer loops (defaults to true)
99
+ * @type {boolean}
100
+ */
101
+ loop = true;
102
+
103
+ /**
104
+ * controls if the songs are ordered randomly
105
+ * @type {boolean}
106
+ */
107
+ shuffleMode = false;
108
+
109
+ /**
110
+ * the current track data
111
+ * @type {BasicMIDI}
112
+ */
113
+ midiData = undefined;
114
+
115
+ /**
116
+ * midi port number for the corresponding track
117
+ * @type {number[]}
118
+ */
119
+ midiPorts = [];
120
+ midiPortChannelOffset = 0;
121
+ /**
122
+ * stored as:
123
+ * Object<midi port, channel offset>
124
+ * @type {Object<number, number>}
125
+ */
126
+ midiPortChannelOffsets = {};
127
+
128
+ /**
129
+ * @type {boolean}
130
+ */
131
+ skipToFirstNoteOn = true;
132
+
133
+ /**
134
+ * If true, seq will stay paused when seeking or changing the playback rate
135
+ * @type {boolean}
136
+ */
137
+ preservePlaybackState = false;
138
+
139
+ /**
140
+ * @param spessasynthProcessor {SpessaSynthProcessor}
141
+ */
142
+ constructor(spessasynthProcessor)
143
+ {
144
+ this.synth = spessasynthProcessor;
145
+ }
146
+
147
+ /**
148
+ * Controls the playback's rate
149
+ * @type {number}
150
+ * @private
151
+ */
152
+ _playbackRate = 1;
153
+
154
+ /**
155
+ * @param value {number}
156
+ */
157
+ set playbackRate(value)
158
+ {
159
+ const time = this.currentTime;
160
+ this._playbackRate = value;
161
+ this.currentTime = time;
162
+ }
163
+
164
+ get currentTime()
165
+ {
166
+ // return the paused time if it's set to something other than undefined
167
+ if (this.pausedTime !== undefined)
168
+ {
169
+ return this.pausedTime;
170
+ }
171
+
172
+ return (currentTime - this.absoluteStartTime) * this._playbackRate;
173
+ }
174
+
175
+ set currentTime(time)
176
+ {
177
+ if (time > this.duration || time < 0)
178
+ {
179
+ // time is 0
180
+ if (this.skipToFirstNoteOn)
181
+ {
182
+ this.setTimeTicks(this.midiData.firstNoteOn - 1);
183
+ }
184
+ else
185
+ {
186
+ this.setTimeTicks(0);
187
+ }
188
+ return;
189
+ }
190
+ if (this.skipToFirstNoteOn)
191
+ {
192
+ if (time < this.firstNoteTime)
193
+ {
194
+ this.setTimeTicks(this.midiData.firstNoteOn - 1);
195
+ return;
196
+ }
197
+ }
198
+ this.stop();
199
+ this.playingNotes = [];
200
+ const wasPaused = this.paused && this.preservePlaybackState;
201
+ this.pausedTime = undefined;
202
+ this.post(WorkletSequencerReturnMessageType.timeChange, currentTime - time);
203
+ if (this.midiData.duration === 0)
204
+ {
205
+ SpessaSynthWarn("No duration!");
206
+ this.post(WorkletSequencerReturnMessageType.pause, true);
207
+ return;
208
+ }
209
+ this._playTo(time);
210
+ this._recalculateStartTime(time);
211
+ if (wasPaused)
212
+ {
213
+ this.pause();
214
+ }
215
+ else
216
+ {
217
+ this.play();
218
+ }
219
+ }
220
+
221
+ /**
222
+ * true if paused, false if playing or stopped
223
+ * @returns {boolean}
224
+ */
225
+ get paused()
226
+ {
227
+ return this.pausedTime !== undefined;
228
+ }
229
+
230
+ /**
231
+ * Pauses the playback
232
+ * @param isFinished {boolean}
233
+ */
234
+ pause(isFinished = false)
235
+ {
236
+ if (this.paused)
237
+ {
238
+ SpessaSynthWarn("Already paused");
239
+ return;
240
+ }
241
+ this.pausedTime = this.currentTime;
242
+ this.stop();
243
+ this.post(WorkletSequencerReturnMessageType.pause, isFinished);
244
+ }
245
+
246
+ /**
247
+ * Stops the playback
248
+ */
249
+ stop()
250
+ {
251
+ this.clearProcessHandler();
252
+ // disable sustain
253
+ for (let i = 0; i < 16; i++)
254
+ {
255
+ this.synth.controllerChange(i, midiControllers.sustainPedal, 0);
256
+ }
257
+ this.synth.stopAllChannels();
258
+ if (this.sendMIDIMessages)
259
+ {
260
+ for (let note of this.playingNotes)
261
+ {
262
+ this.sendMIDIMessage([messageTypes.noteOff | (note.channel % 16), note.midiNote]);
263
+ }
264
+ for (let c = 0; c < MIDI_CHANNEL_COUNT; c++)
265
+ {
266
+ this.sendMIDICC(c, midiControllers.allNotesOff, 0);
267
+ }
268
+ }
269
+ }
270
+
271
+ loadCurrentSong(autoPlay = true)
272
+ {
273
+ let index = this.songIndex;
274
+ if (this.shuffleMode)
275
+ {
276
+ index = this.shuffledSongIndexes[this.songIndex];
277
+ }
278
+ this.loadNewSequence(this.songs[index], autoPlay);
279
+ }
280
+
281
+ _resetTimers()
282
+ {
283
+ this.playedTime = 0;
284
+ this.eventIndex = Array(this.tracks.length).fill(0);
285
+ }
286
+
287
+ setProcessHandler()
288
+ {
289
+ this.isActive = true;
290
+ }
291
+
292
+ clearProcessHandler()
293
+ {
294
+ this.isActive = false;
295
+ }
296
+
297
+ shuffleSongIndexes()
298
+ {
299
+ const indexes = this.songs.map((_, i) => i);
300
+ this.shuffledSongIndexes = [];
301
+ while (indexes.length > 0)
302
+ {
303
+ const index = indexes[Math.floor(Math.random() * indexes.length)];
304
+ this.shuffledSongIndexes.push(index);
305
+ indexes.splice(indexes.indexOf(index), 1);
306
+ }
307
+ }
308
+ }
309
+
310
+ // Web MIDI sending
311
+ WorkletSequencer.prototype.sendMIDIMessage = sendMIDIMessage;
312
+ WorkletSequencer.prototype.sendMIDIReset = sendMIDIReset;
313
+ WorkletSequencer.prototype.sendMIDICC = sendMIDICC;
314
+ WorkletSequencer.prototype.sendMIDIProgramChange = sendMIDIProgramChange;
315
+ WorkletSequencer.prototype.sendMIDIPitchWheel = sendMIDIPitchWheel;
316
+ WorkletSequencer.prototype.assignMIDIPort = assignMIDIPort;
317
+
318
+ WorkletSequencer.prototype.post = post;
319
+ WorkletSequencer.prototype.processMessage = processMessage;
320
+
321
+ WorkletSequencer.prototype._processEvent = _processEvent;
322
+ WorkletSequencer.prototype._addNewMidiPort = _addNewMidiPort;
323
+ WorkletSequencer.prototype.processTick = processTick;
324
+ WorkletSequencer.prototype._findFirstEventIndex = _findFirstEventIndex;
325
+
326
+ WorkletSequencer.prototype.loadNewSequence = loadNewSequence;
327
+ WorkletSequencer.prototype.loadNewSongList = loadNewSongList;
328
+ WorkletSequencer.prototype.nextSong = nextSong;
329
+ WorkletSequencer.prototype.previousSong = previousSong;
330
+
331
+ WorkletSequencer.prototype.play = play;
332
+ WorkletSequencer.prototype._playTo = _playTo;
333
+ WorkletSequencer.prototype.setTimeTicks = setTimeTicks;
334
+ WorkletSequencer.prototype._recalculateStartTime = _recalculateStartTime;
335
+
336
+ export { WorkletSequencer };
spessasynth_lib/soundfont/README.md ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## This is the SoundFont2 parsing library.
2
+
3
+ The code here is responsible for parsing the SoundFont2 file and
4
+ providing an easy way to get the data out.
5
+ Default modulators are also stored here (in `modulators.js`)
6
+
7
+ `basic_soundfont` folder contains the classes that represent the soundfont file.
8
+
9
+ `read_sf2` folder contains the code for reading an `.sf2` file.
10
+
11
+ `write` folder contains the code for writing out an `.sf2` file.
12
+
13
+ `dls` folder contains the code for reading a `.dls` file (and converting in into a soundfont representation).
spessasynth_lib/soundfont/basic_soundfont/basic_instrument.js ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export class BasicInstrument
2
+ {
3
+ /**
4
+ * The instrument's name
5
+ * @type {string}
6
+ */
7
+ instrumentName = "";
8
+
9
+ /**
10
+ * The instrument's zones
11
+ * @type {BasicInstrumentZone[]}
12
+ */
13
+ instrumentZones = [];
14
+
15
+ /**
16
+ * Instrument's use count, used for trimming
17
+ * @type {number}
18
+ * @private
19
+ */
20
+ _useCount = 0;
21
+
22
+ /**
23
+ * @returns {number}
24
+ */
25
+ get useCount()
26
+ {
27
+ return this._useCount;
28
+ }
29
+
30
+ addUseCount()
31
+ {
32
+ this._useCount++;
33
+ this.instrumentZones.forEach(z => z.useCount++);
34
+ }
35
+
36
+ removeUseCount()
37
+ {
38
+ this._useCount--;
39
+ for (let i = 0; i < this.instrumentZones.length; i++)
40
+ {
41
+ if (this.safeDeleteZone(i))
42
+ {
43
+ i--;
44
+ }
45
+ }
46
+ }
47
+
48
+ deleteInstrument()
49
+ {
50
+ this.instrumentZones.forEach(z => z.deleteZone());
51
+ this.instrumentZones.length = 0;
52
+ }
53
+
54
+ /**
55
+ * @param index {number}
56
+ * @returns {boolean} is the zone has been deleted
57
+ */
58
+ safeDeleteZone(index)
59
+ {
60
+ this.instrumentZones[index].useCount--;
61
+ if (this.instrumentZones[index].useCount < 1)
62
+ {
63
+ this.deleteZone(index);
64
+ return true;
65
+ }
66
+ return false;
67
+ }
68
+
69
+ /**
70
+ * @param index {number}
71
+ */
72
+ deleteZone(index)
73
+ {
74
+ this.instrumentZones[index].deleteZone();
75
+ this.instrumentZones.splice(index, 1);
76
+ }
77
+ }
spessasynth_lib/soundfont/basic_soundfont/basic_preset.js ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @typedef {{
3
+ * instrumentGenerators: Generator[],
4
+ * presetGenerators: Generator[],
5
+ * modulators: Modulator[],
6
+ * sample: BasicSample,
7
+ * sampleID: number,
8
+ * }} SampleAndGenerators
9
+ */
10
+ import { generatorTypes } from "./generator.js";
11
+ import { Modulator } from "./modulator.js";
12
+ import { isXGDrums } from "../../utils/xg_hacks.js";
13
+
14
+ export class BasicPreset
15
+ {
16
+ /**
17
+ * The parent soundbank instance
18
+ * Currently used for determining default modulators and XG status
19
+ * @type {BasicSoundBank}
20
+ */
21
+ parentSoundBank;
22
+
23
+ /**
24
+ * The preset's name
25
+ * @type {string}
26
+ */
27
+ presetName = "";
28
+
29
+ /**
30
+ * The preset's MIDI program number
31
+ * @type {number}
32
+ */
33
+ program = 0;
34
+
35
+ /**
36
+ * The preset's MIDI bank number
37
+ * @type {number}
38
+ */
39
+ bank = 0;
40
+
41
+ /**
42
+ * The preset's zones
43
+ * @type {BasicPresetZone[]}
44
+ */
45
+ presetZones = [];
46
+
47
+ /**
48
+ * Stores already found getSamplesAndGenerators for reuse
49
+ * @type {SampleAndGenerators[][][]}
50
+ */
51
+ foundSamplesAndGenerators = [];
52
+
53
+ /**
54
+ * unused metadata
55
+ * @type {number}
56
+ */
57
+ library = 0;
58
+ /**
59
+ * unused metadata
60
+ * @type {number}
61
+ */
62
+ genre = 0;
63
+ /**
64
+ * unused metadata
65
+ * @type {number}
66
+ */
67
+ morphology = 0;
68
+
69
+ /**
70
+ * Creates a new preset representation
71
+ * @param parentSoundBank {BasicSoundBank}
72
+ */
73
+ constructor(parentSoundBank)
74
+ {
75
+ this.parentSoundBank = parentSoundBank;
76
+ for (let i = 0; i < 128; i++)
77
+ {
78
+ this.foundSamplesAndGenerators[i] = [];
79
+ }
80
+ }
81
+
82
+ /**
83
+ * @param allowXG {boolean}
84
+ * @param allowSFX {boolean}
85
+ * @returns {boolean}
86
+ */
87
+ isDrumPreset(allowXG, allowSFX = false)
88
+ {
89
+ const xg = allowXG && this.parentSoundBank.isXGBank;
90
+ // sfx is not cool
91
+ return this.bank === 128 || (
92
+ xg &&
93
+ (isXGDrums(this.bank) && (this.bank !== 126 || allowSFX))
94
+ );
95
+ }
96
+
97
+ deletePreset()
98
+ {
99
+ this.presetZones.forEach(z => z.deleteZone());
100
+ this.presetZones.length = 0;
101
+ }
102
+
103
+ /**
104
+ * @param index {number}
105
+ */
106
+ deleteZone(index)
107
+ {
108
+ this.presetZones[index].deleteZone();
109
+ this.presetZones.splice(index, 1);
110
+ }
111
+
112
+ // noinspection JSUnusedGlobalSymbols
113
+ /**
114
+ * Preloads all samples (async)
115
+ */
116
+ preload(keyMin, keyMax)
117
+ {
118
+ for (let key = keyMin; key < keyMax + 1; key++)
119
+ {
120
+ for (let velocity = 0; velocity < 128; velocity++)
121
+ {
122
+ this.getSamplesAndGenerators(key, velocity).forEach(samandgen =>
123
+ {
124
+ if (!samandgen.sample.isSampleLoaded)
125
+ {
126
+ samandgen.sample.getAudioData();
127
+ }
128
+ });
129
+ }
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Preloads a specific key/velocity combo
135
+ * @param key {number}
136
+ * @param velocity {number}
137
+ */
138
+ preloadSpecific(key, velocity)
139
+ {
140
+ this.getSamplesAndGenerators(key, velocity).forEach(samandgen =>
141
+ {
142
+ if (!samandgen.sample.isSampleLoaded)
143
+ {
144
+ samandgen.sample.getAudioData();
145
+ }
146
+ });
147
+ }
148
+
149
+ /**
150
+ * Returns generatorTranslator and generators for given note
151
+ * @param midiNote {number}
152
+ * @param velocity {number}
153
+ * @returns {SampleAndGenerators[]}
154
+ */
155
+ getSamplesAndGenerators(midiNote, velocity)
156
+ {
157
+ const memorized = this.foundSamplesAndGenerators[midiNote][velocity];
158
+ if (memorized)
159
+ {
160
+ return memorized;
161
+ }
162
+
163
+ if (this.presetZones.length < 1)
164
+ {
165
+ return [];
166
+ }
167
+
168
+ /**
169
+ * @param range {SoundFontRange}
170
+ * @param number {number}
171
+ * @returns {boolean}
172
+ */
173
+ function isInRange(range, number)
174
+ {
175
+ return number >= range.min && number <= range.max;
176
+ }
177
+
178
+ /**
179
+ * @param main {Generator[]}
180
+ * @param adder {Generator[]}
181
+ */
182
+ function addUnique(main, adder)
183
+ {
184
+ main.push(...adder.filter(g => !main.find(mg => mg.generatorType === g.generatorType)));
185
+ }
186
+
187
+ /**
188
+ * @param main {Modulator[]}
189
+ * @param adder {Modulator[]}
190
+ */
191
+ function addUniqueMods(main, adder)
192
+ {
193
+ main.push(...adder.filter(m => !main.find(mm => Modulator.isIdentical(m, mm))));
194
+ }
195
+
196
+ /**
197
+ * @type {SampleAndGenerators[]}
198
+ */
199
+ let parsedGeneratorsAndSamples = [];
200
+
201
+ /**
202
+ * global zone is always first, so it or nothing
203
+ * @type {Generator[]}
204
+ */
205
+ let globalPresetGenerators = this.presetZones[0].isGlobal ? [...this.presetZones[0].generators] : [];
206
+
207
+ /**
208
+ * @type {Modulator[]}
209
+ */
210
+ let globalPresetModulators = this.presetZones[0].isGlobal ? [...this.presetZones[0].modulators] : [];
211
+ const globalKeyRange = this.presetZones[0].isGlobal ? this.presetZones[0].keyRange : { min: 0, max: 127 };
212
+ const globalVelRange = this.presetZones[0].isGlobal ? this.presetZones[0].velRange : { min: 0, max: 127 };
213
+
214
+ // find the preset zones in range
215
+ let presetZonesInRange = this.presetZones.filter(currentZone =>
216
+ (
217
+ isInRange(
218
+ currentZone.hasKeyRange ? currentZone.keyRange : globalKeyRange,
219
+ midiNote
220
+ )
221
+ &&
222
+ isInRange(
223
+ currentZone.hasVelRange ? currentZone.velRange : globalVelRange,
224
+ velocity
225
+ )
226
+ ) && !currentZone.isGlobal);
227
+
228
+ presetZonesInRange.forEach(zone =>
229
+ {
230
+ // the global zone is already taken into account earlier
231
+ if (zone.instrument.instrumentZones.length < 1)
232
+ {
233
+ return;
234
+ }
235
+ let presetGenerators = zone.generators;
236
+ let presetModulators = zone.modulators;
237
+ const firstZone = zone.instrument.instrumentZones[0];
238
+ /**
239
+ * global zone is always first, so it or nothing
240
+ * @type {Generator[]}
241
+ */
242
+ let globalInstrumentGenerators = firstZone.isGlobal ? [...firstZone.generators] : [];
243
+ let globalInstrumentModulators = firstZone.isGlobal ? [...firstZone.modulators] : [];
244
+ const globalKeyRange = firstZone.isGlobal ? firstZone.keyRange : { min: 0, max: 127 };
245
+ const globalVelRange = firstZone.isGlobal ? firstZone.velRange : { min: 0, max: 127 };
246
+
247
+
248
+ let instrumentZonesInRange = zone.instrument.instrumentZones
249
+ .filter(currentZone =>
250
+ (
251
+ isInRange(
252
+ currentZone.hasKeyRange ? currentZone.keyRange : globalKeyRange,
253
+ midiNote
254
+ )
255
+ &&
256
+ isInRange(
257
+ currentZone.hasVelRange ? currentZone.velRange : globalVelRange,
258
+ velocity
259
+ )
260
+ ) && !currentZone.isGlobal
261
+ );
262
+
263
+ instrumentZonesInRange.forEach(instrumentZone =>
264
+ {
265
+ let instrumentGenerators = [...instrumentZone.generators];
266
+ let instrumentModulators = [...instrumentZone.modulators];
267
+
268
+ addUnique(
269
+ presetGenerators,
270
+ globalPresetGenerators
271
+ );
272
+ // add the unique global preset generators (local replace global(
273
+
274
+
275
+ // add the unique global instrument generators (local replace global)
276
+ addUnique(
277
+ instrumentGenerators,
278
+ globalInstrumentGenerators
279
+ );
280
+
281
+ addUniqueMods(
282
+ presetModulators,
283
+ globalPresetModulators
284
+ );
285
+ addUniqueMods(
286
+ instrumentModulators,
287
+ globalInstrumentModulators
288
+ );
289
+
290
+ // default mods
291
+ addUniqueMods(
292
+ instrumentModulators,
293
+ this.parentSoundBank.defaultModulators
294
+ );
295
+
296
+ /**
297
+ * sum preset modulators to instruments (amount) sf spec page 54
298
+ * @type {Modulator[]}
299
+ */
300
+ const finalModulatorList = [...instrumentModulators];
301
+ for (let i = 0; i < presetModulators.length; i++)
302
+ {
303
+ let mod = presetModulators[i];
304
+ const identicalInstrumentModulator = finalModulatorList.findIndex(
305
+ m => Modulator.isIdentical(mod, m));
306
+ if (identicalInstrumentModulator !== -1)
307
+ {
308
+ // sum the amounts
309
+ // (this makes a new modulator because otherwise it would overwrite the one in the soundfont!
310
+ finalModulatorList[identicalInstrumentModulator] = finalModulatorList[identicalInstrumentModulator].sumTransform(
311
+ mod);
312
+ }
313
+ else
314
+ {
315
+ finalModulatorList.push(mod);
316
+ }
317
+ }
318
+
319
+
320
+ // combine both generators and add to the final result
321
+ parsedGeneratorsAndSamples.push({
322
+ instrumentGenerators: instrumentGenerators,
323
+ presetGenerators: presetGenerators,
324
+ modulators: finalModulatorList,
325
+ sample: instrumentZone.sample,
326
+ sampleID: instrumentZone.generators.find(
327
+ g => g.generatorType === generatorTypes.sampleID).generatorValue
328
+ });
329
+ });
330
+ });
331
+
332
+ // save and return
333
+ this.foundSamplesAndGenerators[midiNote][velocity] = parsedGeneratorsAndSamples;
334
+ return parsedGeneratorsAndSamples;
335
+ }
336
+ }
spessasynth_lib/soundfont/basic_soundfont/basic_sample.js ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * samples.js
3
+ * purpose: parses soundfont samples, resamples if needed.
4
+ * loads sample data, handles async loading of sf3 compressed samples
5
+ */
6
+ import { SpessaSynthWarn } from "../../utils/loggin.js";
7
+
8
+ // should be reasonable for most cases
9
+ const RESAMPLE_RATE = 48000;
10
+
11
+ export class BasicSample
12
+ {
13
+
14
+ /**
15
+ * The sample's name
16
+ * @type {string}
17
+ */
18
+ sampleName;
19
+
20
+ /**
21
+ * Sample rate in Hz
22
+ * @type {number}
23
+ */
24
+ sampleRate;
25
+
26
+ /**
27
+ * Original pitch of the sample as a MIDI note number
28
+ * @type {number}
29
+ */
30
+ samplePitch;
31
+
32
+ /**
33
+ * Pitch correction, in cents. Can be negative
34
+ * @type {number}
35
+ */
36
+ samplePitchCorrection;
37
+
38
+ /**
39
+ * Sample link, currently unused here
40
+ * @type {number}
41
+ */
42
+ sampleLink;
43
+
44
+ /**
45
+ * Type of the sample, currently only used for SF3
46
+ * @type {number}
47
+ */
48
+ sampleType;
49
+
50
+ /**
51
+ * Relative to the start of the sample in sample points
52
+ * @type {number}
53
+ */
54
+ sampleLoopStartIndex;
55
+
56
+ /**
57
+ * Relative to the start of the sample in sample points
58
+ * @type {number}
59
+ */
60
+ sampleLoopEndIndex;
61
+
62
+ /**
63
+ * Indicates if the sample is compressed
64
+ * @type {boolean}
65
+ */
66
+ isCompressed;
67
+
68
+ /**
69
+ * The compressed sample data if it was compressed by spessasynth
70
+ * @type {Uint8Array}
71
+ */
72
+ compressedData = undefined;
73
+
74
+ /**
75
+ * The sample's use count
76
+ * @type {number}
77
+ */
78
+ useCount = 0;
79
+
80
+ /**
81
+ * The sample's audio data
82
+ * @type {Float32Array}
83
+ */
84
+ sampleData = undefined;
85
+
86
+ /**
87
+ * The basic representation of a soundfont sample
88
+ * @param sampleName {string} The sample's name
89
+ * @param sampleRate {number} The sample's rate in Hz
90
+ * @param samplePitch {number} The sample's pitch as a MIDI note number
91
+ * @param samplePitchCorrection {number} The sample's pitch correction in cents
92
+ * @param sampleLink {number} The sample's link, currently unused
93
+ * @param sampleType {number} The sample's type, an enum
94
+ * @param loopStart {number} The sample's loop start relative to the sample start in sample points
95
+ * @param loopEnd {number} The sample's loop end relative to the sample start in sample points
96
+ */
97
+ constructor(
98
+ sampleName,
99
+ sampleRate,
100
+ samplePitch,
101
+ samplePitchCorrection,
102
+ sampleLink,
103
+ sampleType,
104
+ loopStart,
105
+ loopEnd
106
+ )
107
+ {
108
+ this.sampleName = sampleName;
109
+ this.sampleRate = sampleRate;
110
+ this.samplePitch = samplePitch;
111
+ this.samplePitchCorrection = samplePitchCorrection;
112
+ this.sampleLink = sampleLink;
113
+ this.sampleType = sampleType;
114
+ this.sampleLoopStartIndex = loopStart;
115
+ this.sampleLoopEndIndex = loopEnd;
116
+ // https://github.com/FluidSynth/fluidsynth/wiki/SoundFont3Format
117
+ this.isCompressed = (sampleType & 0x10) > 0;
118
+ }
119
+
120
+
121
+ /**
122
+ * @returns {Uint8Array|IndexedByteArray}
123
+ */
124
+ getRawData()
125
+ {
126
+ const uint8 = new Uint8Array(this.sampleData.length * 2);
127
+ for (let i = 0; i < this.sampleData.length; i++)
128
+ {
129
+ const sample = Math.floor(this.sampleData[i] * 32768);
130
+ uint8[i * 2] = sample & 0xFF; // lower byte
131
+ uint8[i * 2 + 1] = (sample >> 8) & 0xFF; // upper byte
132
+ }
133
+ return uint8;
134
+ }
135
+
136
+ resampleData(newSampleRate)
137
+ {
138
+ let audioData = this.getAudioData();
139
+ const ratio = newSampleRate / this.sampleRate;
140
+ const resampled = new Float32Array(Math.floor(audioData.length * ratio));
141
+ for (let i = 0; i < resampled.length; i++)
142
+ {
143
+ resampled[i] = audioData[Math.floor(i * (1 / ratio))];
144
+ }
145
+ audioData = resampled;
146
+ this.sampleRate = newSampleRate;
147
+ // adjust loop points
148
+ this.sampleLoopStartIndex = Math.floor(this.sampleLoopStartIndex * ratio);
149
+ this.sampleLoopEndIndex = Math.floor(this.sampleLoopEndIndex * ratio);
150
+ this.sampleData = audioData;
151
+ }
152
+
153
+ /**
154
+ * @param quality {number}
155
+ * @param encodeVorbis {EncodeVorbisFunction}
156
+ */
157
+ compressSample(quality, encodeVorbis)
158
+ {
159
+ // no need to compress
160
+ if (this.isCompressed)
161
+ {
162
+ return;
163
+ }
164
+ // compress, always mono!
165
+ try
166
+ {
167
+ // if the sample rate is too low or too high, resample
168
+ let audioData = this.getAudioData();
169
+ if (this.sampleRate < 8000 || this.sampleRate > 96000)
170
+ {
171
+ this.resampleData(RESAMPLE_RATE);
172
+ audioData = this.getAudioData();
173
+ }
174
+ this.compressedData = encodeVorbis([audioData], 1, this.sampleRate, quality);
175
+ // flag as compressed
176
+ this.sampleType |= 0x10;
177
+ this.isCompressed = true;
178
+ }
179
+ catch (e)
180
+ {
181
+ SpessaSynthWarn(`Failed to compress ${this.sampleName}. Leaving as uncompressed!`);
182
+ this.isCompressed = false;
183
+ this.compressedData = undefined;
184
+ // flag as uncompressed
185
+ this.sampleType &= 0xEF;
186
+ }
187
+
188
+ }
189
+
190
+ /**
191
+ * @returns {Float32Array}
192
+ */
193
+ getAudioData()
194
+ {
195
+ return this.sampleData;
196
+ }
197
+ }
spessasynth_lib/soundfont/basic_soundfont/basic_soundfont.js ADDED
@@ -0,0 +1,565 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ SpessaSynthGroup,
3
+ SpessaSynthGroupCollapsed,
4
+ SpessaSynthGroupEnd,
5
+ SpessaSynthInfo,
6
+ SpessaSynthWarn
7
+ } from "../../utils/loggin.js";
8
+ import { consoleColors } from "../../utils/other.js";
9
+ import { write } from "./write_sf2/write.js";
10
+ import { defaultModulators, Modulator } from "./modulator.js";
11
+ import { writeDLS } from "./write_dls/write_dls.js";
12
+ import { BasicSample } from "./basic_sample.js";
13
+ import { BasicInstrumentZone, BasicPresetZone } from "./basic_zones.js";
14
+ import { Generator, generatorTypes } from "./generator.js";
15
+ import { BasicInstrument } from "./basic_instrument.js";
16
+ import { BasicPreset } from "./basic_preset.js";
17
+ import { isXGDrums } from "../../utils/xg_hacks.js";
18
+
19
+ class BasicSoundBank
20
+ {
21
+
22
+ /**
23
+ * Soundfont's info stored as name: value. ifil and iver are stored as string representation of float (e.g., 2.1)
24
+ * @type {Object<string, string|IndexedByteArray>}
25
+ */
26
+ soundFontInfo = {};
27
+
28
+ /**
29
+ * The soundfont's presets
30
+ * @type {BasicPreset[]}
31
+ */
32
+ presets = [];
33
+
34
+ /**
35
+ * The soundfont's samples
36
+ * @type {BasicSample[]}
37
+ */
38
+ samples = [];
39
+
40
+ /**
41
+ * The soundfont's instruments
42
+ * @type {BasicInstrument[]}
43
+ */
44
+ instruments = [];
45
+
46
+ /**
47
+ * Soundfont's default modulatorss
48
+ * @type {Modulator[]}
49
+ */
50
+ defaultModulators = defaultModulators.map(m => Modulator.copy(m));
51
+
52
+ /**
53
+ * Checks for XG drumsets and considers if this soundfont is XG.
54
+ * @type {boolean}
55
+ */
56
+ isXGBank = false;
57
+
58
+ /**
59
+ * Creates a new basic soundfont template
60
+ * @param data {undefined|{presets: BasicPreset[], info: Object<string, string>}}
61
+ */
62
+ constructor(data = undefined)
63
+ {
64
+ if (data?.presets)
65
+ {
66
+ this.presets.push(...data.presets);
67
+ this.soundFontInfo = data.info;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Merges soundfonts with the given order. Keep in mind that the info read is copied from the first one
73
+ * @param soundfonts {...BasicSoundBank} the soundfonts to merge, the first overwrites the last
74
+ * @returns {BasicSoundBank}
75
+ */
76
+ static mergeSoundBanks(...soundfonts)
77
+ {
78
+ const mainSf = soundfonts.shift();
79
+ const presets = mainSf.presets;
80
+ while (soundfonts.length)
81
+ {
82
+ const newPresets = soundfonts.shift().presets;
83
+ newPresets.forEach(newPreset =>
84
+ {
85
+ if (
86
+ presets.find(existingPreset => existingPreset.bank === newPreset.bank && existingPreset.program === newPreset.program) === undefined
87
+ )
88
+ {
89
+ presets.push(newPreset);
90
+ }
91
+ });
92
+ }
93
+
94
+ return new BasicSoundBank({ presets: presets, info: mainSf.soundFontInfo });
95
+ }
96
+
97
+ /**
98
+ * Creates a simple soundfont with one saw wave preset.
99
+ * @returns {ArrayBufferLike}
100
+ */
101
+ static getDummySoundfontFile()
102
+ {
103
+ const font = new BasicSoundBank();
104
+ const sample = new BasicSample(
105
+ "Saw",
106
+ 44100,
107
+ 65,
108
+ 20,
109
+ 0,
110
+ 0,
111
+ 0,
112
+ 127
113
+ );
114
+ sample.sampleData = new Float32Array(128);
115
+ for (let i = 0; i < 128; i++)
116
+ {
117
+ sample.sampleData[i] = (i / 128) * 2 - 1;
118
+ }
119
+ font.samples.push(sample);
120
+
121
+ const gZone = new BasicInstrumentZone();
122
+ gZone.isGlobal = true;
123
+ gZone.generators.push(new Generator(generatorTypes.initialAttenuation, 375));
124
+ gZone.generators.push(new Generator(generatorTypes.releaseVolEnv, -1000));
125
+ gZone.generators.push(new Generator(generatorTypes.sampleModes, 1));
126
+
127
+ const zone1 = new BasicInstrumentZone();
128
+ zone1.sample = sample;
129
+
130
+ const zone2 = new BasicInstrumentZone();
131
+ zone2.sample = sample;
132
+ zone2.generators.push(new Generator(generatorTypes.fineTune, -9));
133
+
134
+
135
+ const inst = new BasicInstrument();
136
+ inst.instrumentName = "Saw Wave";
137
+ inst.instrumentZones.push(gZone);
138
+ inst.instrumentZones.push(zone1);
139
+ inst.instrumentZones.push(zone2);
140
+ font.instruments.push(inst);
141
+
142
+ const pZone = new BasicPresetZone();
143
+ pZone.instrument = inst;
144
+
145
+ const preset = new BasicPreset(font);
146
+ preset.presetName = "Saw Wave";
147
+ preset.presetZones.push(pZone);
148
+ font.presets.push(preset);
149
+
150
+ font.soundFontInfo["ifil"] = "2.1";
151
+ font.soundFontInfo["isng"] = "EMU8000";
152
+ font.soundFontInfo["INAM"] = "Dummy";
153
+ font._parseInternal();
154
+ return font.write().buffer;
155
+ }
156
+
157
+ /**
158
+ * parses the bank after loading is done
159
+ * @protected
160
+ */
161
+ _parseInternal()
162
+ {
163
+ this.isXGBank = false;
164
+ // definitions for XG:
165
+ // at least one preset with bank 127, 126 or 120
166
+ // MUST be a valid XG bank.
167
+ // allowed banks: (see XG specification)
168
+ // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 24,
169
+ // 25, 27, 28, 29, 30, 31, 32, 33, 40, 41, 48, 56, 57, 58,
170
+ // 64, 65, 66, 126, 127
171
+ const allowedPrograms = new Set([
172
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 16, 17, 24,
173
+ 25, 27, 28, 29, 30, 31, 32, 33, 40, 41, 48, 56, 57, 58,
174
+ 64, 65, 66, 126, 127
175
+ ]);
176
+ for (const preset of this.presets)
177
+ {
178
+ if (isXGDrums(preset.bank))
179
+ {
180
+ this.isXGBank = true;
181
+ if (!allowedPrograms.has(preset.program))
182
+ {
183
+ // not valid!
184
+ this.isXGBank = false;
185
+ SpessaSynthInfo(
186
+ `%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.`,
187
+ consoleColors.info,
188
+ consoleColors.value,
189
+ consoleColors.info
190
+ );
191
+ break;
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Trims a sound bank to only contain samples in a given MIDI file
199
+ * @param mid {BasicMIDI} - the MIDI file
200
+ */
201
+ trimSoundBank(mid)
202
+ {
203
+ const soundfont = this;
204
+
205
+ /**
206
+ * @param instrument {Instrument}
207
+ * @param combos {{key: number, velocity: number}[]}
208
+ * @returns {number}
209
+ */
210
+ function trimInstrumentZones(instrument, combos)
211
+ {
212
+ let trimmedIZones = 0;
213
+ for (let iZoneIndex = 0; iZoneIndex < instrument.instrumentZones.length; iZoneIndex++)
214
+ {
215
+ const iZone = instrument.instrumentZones[iZoneIndex];
216
+ if (iZone.isGlobal)
217
+ {
218
+ continue;
219
+ }
220
+ const iKeyRange = iZone.keyRange;
221
+ const iVelRange = iZone.velRange;
222
+ let isIZoneUsed = false;
223
+ for (const iCombo of combos)
224
+ {
225
+ if (
226
+ (iCombo.key >= iKeyRange.min && iCombo.key <= iKeyRange.max) &&
227
+ (iCombo.velocity >= iVelRange.min && iCombo.velocity <= iVelRange.max)
228
+ )
229
+ {
230
+ isIZoneUsed = true;
231
+ break;
232
+ }
233
+ }
234
+ if (!isIZoneUsed)
235
+ {
236
+ SpessaSynthInfo(
237
+ `%c${iZone.sample.sampleName} %cremoved from %c${instrument.instrumentName}%c. Use count: %c${iZone.useCount - 1}`,
238
+ consoleColors.recognized,
239
+ consoleColors.info,
240
+ consoleColors.recognized,
241
+ consoleColors.info,
242
+ consoleColors.recognized
243
+ );
244
+ if (instrument.safeDeleteZone(iZoneIndex))
245
+ {
246
+ trimmedIZones++;
247
+ iZoneIndex--;
248
+ SpessaSynthInfo(
249
+ `%c${iZone.sample.sampleName} %cdeleted`,
250
+ consoleColors.recognized,
251
+ consoleColors.info
252
+ );
253
+ }
254
+ if (iZone.sample.useCount < 1)
255
+ {
256
+ soundfont.deleteSample(iZone.sample);
257
+ }
258
+ }
259
+
260
+ }
261
+ return trimmedIZones;
262
+ }
263
+
264
+ SpessaSynthGroup(
265
+ "%cTrimming soundfont...",
266
+ consoleColors.info
267
+ );
268
+ const usedProgramsAndKeys = mid.getUsedProgramsAndKeys(soundfont);
269
+
270
+ SpessaSynthGroupCollapsed(
271
+ "%cModifying soundfont...",
272
+ consoleColors.info
273
+ );
274
+ SpessaSynthInfo("Detected keys for midi:", usedProgramsAndKeys);
275
+ // modify the soundfont to only include programs and samples that are used
276
+ for (let presetIndex = 0; presetIndex < soundfont.presets.length; presetIndex++)
277
+ {
278
+ const p = soundfont.presets[presetIndex];
279
+ const string = p.bank + ":" + p.program;
280
+ const used = usedProgramsAndKeys[string];
281
+ if (used === undefined)
282
+ {
283
+ SpessaSynthInfo(
284
+ `%cDeleting preset %c${p.presetName}%c and its zones`,
285
+ consoleColors.info,
286
+ consoleColors.recognized,
287
+ consoleColors.info
288
+ );
289
+ soundfont.deletePreset(p);
290
+ presetIndex--;
291
+ }
292
+ else
293
+ {
294
+ const combos = [...used].map(s =>
295
+ {
296
+ const split = s.split("-");
297
+ return {
298
+ key: parseInt(split[0]),
299
+ velocity: parseInt(split[1])
300
+ };
301
+ });
302
+ SpessaSynthGroupCollapsed(
303
+ `%cTrimming %c${p.presetName}`,
304
+ consoleColors.info,
305
+ consoleColors.recognized
306
+ );
307
+ SpessaSynthInfo(`Keys for ${p.presetName}:`, combos);
308
+ let trimmedZones = 0;
309
+ // clean the preset to only use zones that are used
310
+ for (let zoneIndex = 0; zoneIndex < p.presetZones.length; zoneIndex++)
311
+ {
312
+ const zone = p.presetZones[zoneIndex];
313
+ if (zone.isGlobal)
314
+ {
315
+ continue;
316
+ }
317
+ const keyRange = zone.keyRange;
318
+ const velRange = zone.velRange;
319
+ // check if any of the combos matches the zone
320
+ let isZoneUsed = false;
321
+ for (const combo of combos)
322
+ {
323
+ if (
324
+ (combo.key >= keyRange.min && combo.key <= keyRange.max) &&
325
+ (combo.velocity >= velRange.min && combo.velocity <= velRange.max)
326
+ )
327
+ {
328
+ // zone is used, trim the instrument zones
329
+ isZoneUsed = true;
330
+ const trimmedIZones = trimInstrumentZones(zone.instrument, combos);
331
+ SpessaSynthInfo(
332
+ `%cTrimmed off %c${trimmedIZones}%c zones from %c${zone.instrument.instrumentName}`,
333
+ consoleColors.info,
334
+ consoleColors.recognized,
335
+ consoleColors.info,
336
+ consoleColors.recognized
337
+ );
338
+ break;
339
+ }
340
+ }
341
+ if (!isZoneUsed)
342
+ {
343
+ trimmedZones++;
344
+ p.deleteZone(zoneIndex);
345
+ if (zone.instrument.useCount < 1)
346
+ {
347
+ soundfont.deleteInstrument(zone.instrument);
348
+ }
349
+ zoneIndex--;
350
+ }
351
+ }
352
+ SpessaSynthInfo(
353
+ `%cTrimmed off %c${trimmedZones}%c zones from %c${p.presetName}`,
354
+ consoleColors.info,
355
+ consoleColors.recognized,
356
+ consoleColors.info,
357
+ consoleColors.recognized
358
+ );
359
+ SpessaSynthGroupEnd();
360
+ }
361
+ }
362
+ soundfont.removeUnusedElements();
363
+
364
+ soundfont.soundFontInfo["ICMT"] = `NOTE: This soundfont was trimmed by SpessaSynth to only contain presets used in "${mid.midiName}"\n\n`
365
+ + soundfont.soundFontInfo["ICMT"];
366
+
367
+ SpessaSynthInfo(
368
+ "%cSoundfont modified!",
369
+ consoleColors.recognized
370
+ );
371
+ SpessaSynthGroupEnd();
372
+ SpessaSynthGroupEnd();
373
+ }
374
+
375
+ removeUnusedElements()
376
+ {
377
+ this.instruments.forEach(i =>
378
+ {
379
+ if (i.useCount < 1)
380
+ {
381
+ i.instrumentZones.forEach(z =>
382
+ {
383
+ if (!z.isGlobal)
384
+ {
385
+ z.sample.useCount--;
386
+ }
387
+ });
388
+ }
389
+ });
390
+ this.instruments = this.instruments.filter(i => i.useCount > 0);
391
+ this.samples = this.samples.filter(s => s.useCount > 0);
392
+ }
393
+
394
+ /**
395
+ * @param instrument {BasicInstrument}
396
+ */
397
+ deleteInstrument(instrument)
398
+ {
399
+ if (instrument.useCount > 0)
400
+ {
401
+ throw new Error(`Cannot delete an instrument that has ${instrument.useCount} usages.`);
402
+ }
403
+ this.instruments.splice(this.instruments.indexOf(instrument), 1);
404
+ instrument.deleteInstrument();
405
+ this.removeUnusedElements();
406
+ }
407
+
408
+ /**
409
+ * @param preset {BasicPreset}
410
+ */
411
+ deletePreset(preset)
412
+ {
413
+ preset.deletePreset();
414
+ this.presets.splice(this.presets.indexOf(preset), 1);
415
+ this.removeUnusedElements();
416
+ }
417
+
418
+ /**
419
+ * @param sample {BasicSample}
420
+ */
421
+ deleteSample(sample)
422
+ {
423
+ if (sample.useCount > 0)
424
+ {
425
+ throw new Error(`Cannot delete sample that has ${sample.useCount} usages.`);
426
+ }
427
+ this.samples.splice(this.samples.indexOf(sample), 1);
428
+ this.removeUnusedElements();
429
+ }
430
+
431
+ /**
432
+ * Get the appropriate preset, undefined if not found
433
+ * @param bankNr {number}
434
+ * @param programNr {number}
435
+ * @param allowXGDrums {boolean} if true, allows XG drum banks (120, 126 and 127) as drum preset
436
+ * @return {BasicPreset}
437
+ */
438
+ getPresetNoFallback(bankNr, programNr, allowXGDrums = false)
439
+ {
440
+ const isDrum = bankNr === 128 || (allowXGDrums && isXGDrums(bankNr));
441
+ // check for exact match
442
+ let p;
443
+ if (isDrum)
444
+ {
445
+ p = this.presets.find(p => p.bank === bankNr && p.isDrumPreset(allowXGDrums) && p.program === programNr);
446
+ }
447
+ else
448
+ {
449
+ p = this.presets.find(p => p.bank === bankNr && p.program === programNr);
450
+ }
451
+ if (p)
452
+ {
453
+ return p;
454
+ }
455
+ // no match...
456
+ if (isDrum)
457
+ {
458
+ if (allowXGDrums)
459
+ {
460
+ // try any drum preset with matching program?
461
+ const p = this.presets.find(p => p.isDrumPreset(allowXGDrums) && p.program === programNr);
462
+ if (p)
463
+ {
464
+ return p;
465
+ }
466
+ }
467
+ }
468
+ return undefined;
469
+ }
470
+
471
+ /**
472
+ * Get the appropriate preset
473
+ * @param bankNr {number}
474
+ * @param programNr {number}
475
+ * @param allowXGDrums {boolean} if true, allows XG drum banks (120, 126 and 127) as drum preset
476
+ * @returns {BasicPreset}
477
+ */
478
+ getPreset(bankNr, programNr, allowXGDrums = false)
479
+ {
480
+ const isDrums = bankNr === 128 || (allowXGDrums && isXGDrums(bankNr));
481
+ // check for exact match
482
+ let preset;
483
+ // only allow drums if the preset is considered to be a drum preset
484
+ if (isDrums)
485
+ {
486
+ preset = this.presets.find(p => p.bank === bankNr && p.isDrumPreset(allowXGDrums) && p.program === programNr);
487
+ }
488
+ else
489
+ {
490
+ preset = this.presets.find(p => p.bank === bankNr && p.program === programNr);
491
+ }
492
+ if (preset)
493
+ {
494
+ return preset;
495
+ }
496
+ // no match...
497
+ if (isDrums)
498
+ {
499
+ // drum preset: find any preset with bank 128
500
+ preset = this.presets.find(p => p.isDrumPreset(allowXGDrums) && p.program === programNr);
501
+ if (!preset)
502
+ {
503
+ // only allow 128, otherwise it would default to XG SFX
504
+ preset = this.presets.find(p => p.isDrumPreset(allowXGDrums));
505
+ }
506
+ }
507
+ else
508
+ {
509
+ // non-drum preset: find any preset with the given program that is not a drum preset
510
+ preset = this.presets.find(p => p.program === programNr && !p.isDrumPreset(allowXGDrums));
511
+ }
512
+ if (preset)
513
+ {
514
+ SpessaSynthWarn(
515
+ `%cPreset ${bankNr}.${programNr} not found. Replaced with %c${preset.presetName} (${preset.bank}.${preset.program})`,
516
+ consoleColors.warn,
517
+ consoleColors.recognized
518
+ );
519
+ }
520
+
521
+ // no preset, use the first one available
522
+ if (!preset)
523
+ {
524
+ SpessaSynthWarn(`Preset ${programNr} not found. Defaulting to`, this.presets[0].presetName);
525
+ preset = this.presets[0];
526
+ }
527
+ return preset;
528
+ }
529
+
530
+ /**
531
+ * gets preset by name
532
+ * @param presetName {string}
533
+ * @returns {BasicPreset}
534
+ */
535
+ getPresetByName(presetName)
536
+ {
537
+ let preset = this.presets.find(p => p.presetName === presetName);
538
+ if (!preset)
539
+ {
540
+ SpessaSynthWarn("Preset not found. Defaulting to:", this.presets[0].presetName);
541
+ preset = this.presets[0];
542
+ }
543
+ return preset;
544
+ }
545
+
546
+ /**
547
+ * @param error {string}
548
+ */
549
+ parsingError(error)
550
+ {
551
+ throw new Error(`SF parsing error: ${error} The file may be corrupted.`);
552
+ }
553
+
554
+ destroySoundBank()
555
+ {
556
+ delete this.presets;
557
+ delete this.instruments;
558
+ delete this.samples;
559
+ }
560
+ }
561
+
562
+ BasicSoundBank.prototype.write = write;
563
+ BasicSoundBank.prototype.writeDLS = writeDLS;
564
+
565
+ export { BasicSoundBank };
spessasynth_lib/soundfont/basic_soundfont/basic_zone.js ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @typedef {Object} SoundFontRange
3
+ * @property {number} min - the minimum midi note
4
+ * @property {number} max - the maximum midi note
5
+ */
6
+
7
+ export class BasicZone
8
+ {
9
+ /**
10
+ * The zone's velocity range
11
+ * min -1 means that it is a default value
12
+ * @type {SoundFontRange}
13
+ */
14
+ velRange = { min: -1, max: 127 };
15
+
16
+ /**
17
+ * The zone's key range
18
+ * min -1 means that it is a default value
19
+ * @type {SoundFontRange}
20
+ */
21
+ keyRange = { min: -1, max: 127 };
22
+ /**
23
+ * Indicates if the zone is global
24
+ * @type {boolean}
25
+ */
26
+ isGlobal = false;
27
+ /**
28
+ * The zone's generators
29
+ * @type {Generator[]}
30
+ */
31
+ generators = [];
32
+ /**
33
+ * The zone's modulators
34
+ * @type {Modulator[]}
35
+ */
36
+ modulators = [];
37
+
38
+ /**
39
+ * @returns {boolean}
40
+ */
41
+ get hasKeyRange()
42
+ {
43
+ return this.keyRange.min !== -1;
44
+ }
45
+
46
+ /**
47
+ * @returns {boolean}
48
+ */
49
+ get hasVelRange()
50
+ {
51
+ return this.velRange.min !== -1;
52
+ }
53
+
54
+ /**
55
+ * @param generatorType {generatorTypes}
56
+ * @param notFoundValue {number}
57
+ * @returns {number}
58
+ */
59
+ getGeneratorValue(generatorType, notFoundValue)
60
+ {
61
+ return this.generators.find(g => g.generatorType === generatorType)?.generatorValue ?? notFoundValue;
62
+ }
63
+ }
64
+
spessasynth_lib/soundfont/basic_soundfont/basic_zones.js ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BasicZone } from "./basic_zone.js";
2
+
3
+ export class BasicInstrumentZone extends BasicZone
4
+ {
5
+ /**
6
+ * Zone's sample. Undefined if global
7
+ * @type {BasicSample|undefined}
8
+ */
9
+ sample = undefined;
10
+ /**
11
+ * The zone's use count
12
+ * @type {number}
13
+ */
14
+ useCount = 0;
15
+
16
+ deleteZone()
17
+ {
18
+ this.useCount--;
19
+ if (this.isGlobal)
20
+ {
21
+ return;
22
+ }
23
+ this.sample.useCount--;
24
+ }
25
+ }
26
+
27
+ export class BasicPresetZone extends BasicZone
28
+ {
29
+ /**
30
+ * Zone's instrument. Undefined if global
31
+ * @type {BasicInstrument|undefined}
32
+ */
33
+ instrument = undefined;
34
+
35
+ deleteZone()
36
+ {
37
+ if (this.isGlobal)
38
+ {
39
+ return;
40
+ }
41
+ this.instrument.removeUseCount();
42
+ }
43
+ }
spessasynth_lib/soundfont/basic_soundfont/generator.js ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @enum {number}
3
+ */
4
+ export const generatorTypes = {
5
+ INVALID: -1, // invalid generator
6
+ startAddrsOffset: 0, // sample control - moves sample start point
7
+ endAddrOffset: 1, // sample control - moves sample end point
8
+ startloopAddrsOffset: 2, // loop control - moves loop start point
9
+ endloopAddrsOffset: 3, // loop control - moves loop end point
10
+ startAddrsCoarseOffset: 4, // sample control - moves sample start point in 32,768 increments
11
+ modLfoToPitch: 5, // pitch modulation - modulation lfo pitch modulation in cents
12
+ vibLfoToPitch: 6, // pitch modulation - vibrato lfo pitch modulation in cents
13
+ modEnvToPitch: 7, // pitch modulation - modulation envelope pitch modulation in cents
14
+ initialFilterFc: 8, // filter - lowpass filter cutoff in cents
15
+ initialFilterQ: 9, // filter - lowpass filter resonance
16
+ modLfoToFilterFc: 10, // filter modulation - modulation lfo lowpass filter cutoff in cents
17
+ modEnvToFilterFc: 11, // filter modulation - modulation envelope lowpass filter cutoff in cents
18
+ endAddrsCoarseOffset: 12, // ample control - move sample end point in 32,768 increments
19
+ modLfoToVolume: 13, // modulation lfo - volume (tremolo), where 100 = 10dB
20
+ unused1: 14, // unused
21
+ chorusEffectsSend: 15, // effect send - how much is sent to chorus 0 - 1000
22
+ reverbEffectsSend: 16, // effect send - how much is sent to reverb 0 - 1000
23
+ pan: 17, // panning - where -500 = left, 0 = center, 500 = right
24
+ unused2: 18, // unused
25
+ unused3: 19, // unused
26
+ unused4: 20, // unused
27
+ delayModLFO: 21, // mod lfo - delay for mod lfo to start from zero
28
+ freqModLFO: 22, // mod lfo - frequency of mod lfo, 0 = 8.176 Hz, units: f => 1200log2(f/8.176)
29
+ delayVibLFO: 23, // vib lfo - delay for vibrato lfo to start from zero
30
+ freqVibLFO: 24, // vib lfo - frequency of vibrato lfo, 0 = 8.176Hz, unit: f => 1200log2(f/8.176)
31
+ delayModEnv: 25, // mod env - 0 = 1 s decay till mod env starts
32
+ attackModEnv: 26, // mod env - attack of mod env
33
+ holdModEnv: 27, // mod env - hold of mod env
34
+ decayModEnv: 28, // mod env - decay of mod env
35
+ sustainModEnv: 29, // mod env - sustain of mod env
36
+ releaseModEnv: 30, // mod env - release of mod env
37
+ keyNumToModEnvHold: 31, // mod env - also modulating mod envelope hold with key number
38
+ keyNumToModEnvDecay: 32, // mod env - also modulating mod envelope decay with key number
39
+ delayVolEnv: 33, // vol env - delay of envelope from zero (weird scale)
40
+ attackVolEnv: 34, // vol env - attack of envelope
41
+ holdVolEnv: 35, // vol env - hold of envelope
42
+ decayVolEnv: 36, // vol env - decay of envelope
43
+ sustainVolEnv: 37, // vol env - sustain of envelope
44
+ releaseVolEnv: 38, // vol env - release of envelope
45
+ keyNumToVolEnvHold: 39, // vol env - key number to volume envelope hold
46
+ keyNumToVolEnvDecay: 40, // vol env - key number to volume envelope decay
47
+ instrument: 41, // zone - instrument index to use for preset zone
48
+ reserved1: 42, // reserved
49
+ keyRange: 43, // zone - key range for which preset / instrument zone is active
50
+ velRange: 44, // zone - velocity range for which preset / instrument zone is active
51
+ startloopAddrsCoarseOffset: 45, // sample control - moves sample loop start point in 32,768 increments
52
+ keyNum: 46, // zone - instrument only: always use this midi number (ignore what's pressed)
53
+ velocity: 47, // zone - instrument only: always use this velocity (ignore what's pressed)
54
+ initialAttenuation: 48, // zone - allows turning down the volume, 10 = -1dB
55
+ reserved2: 49, // reserved
56
+ endloopAddrsCoarseOffset: 50, // sample control - moves sample loop end point in 32,768 increments
57
+ coarseTune: 51, // tune - pitch offset in semitones
58
+ fineTune: 52, // tune - pitch offset in cents
59
+ sampleID: 53, // sample - instrument zone only: which sample to use
60
+ sampleModes: 54, // sample - 0 = no loop, 1 = loop, 2 = reserved, 3 = loop and play till the end in release phase
61
+ reserved3: 55, // reserved
62
+ scaleTuning: 56, // sample - the degree to which MIDI key number influences pitch, 100 = default
63
+ exclusiveClass: 57, // sample - = cut = choke group
64
+ overridingRootKey: 58, // sample - can override the sample's original pitch
65
+ unused5: 59, // unused
66
+ endOper: 60 // end marker
67
+ };
68
+ /**
69
+ * @type {{min: number, max: number, def: number}[]}
70
+ */
71
+ export const generatorLimits = [];
72
+ // offsets
73
+ generatorLimits[generatorTypes.startAddrsOffset] = { min: 0, max: 32768, def: 0 };
74
+ generatorLimits[generatorTypes.endAddrOffset] = { min: -32768, max: 32768, def: 0 };
75
+ generatorLimits[generatorTypes.startloopAddrsOffset] = { min: -32768, max: 32768, def: 0 };
76
+ generatorLimits[generatorTypes.endloopAddrsOffset] = { min: -32768, max: 32768, def: 0 };
77
+ generatorLimits[generatorTypes.startAddrsCoarseOffset] = { min: 0, max: 32768, def: 0 };
78
+
79
+ // pitch influence
80
+ generatorLimits[generatorTypes.modLfoToPitch] = { min: -12000, max: 12000, def: 0 };
81
+ generatorLimits[generatorTypes.vibLfoToPitch] = { min: -12000, max: 12000, def: 0 };
82
+ generatorLimits[generatorTypes.modEnvToPitch] = { min: -12000, max: 12000, def: 0 };
83
+
84
+ // lowpass
85
+ generatorLimits[generatorTypes.initialFilterFc] = { min: 1500, max: 13500, def: 13500 };
86
+ generatorLimits[generatorTypes.initialFilterQ] = { min: 0, max: 960, def: 0 };
87
+ generatorLimits[generatorTypes.modLfoToFilterFc] = { min: -12000, max: 12000, def: 0 };
88
+ generatorLimits[generatorTypes.modEnvToFilterFc] = { min: -12000, max: 12000, def: 0 };
89
+
90
+ generatorLimits[generatorTypes.endAddrsCoarseOffset] = { min: -32768, max: 32768, def: 0 };
91
+
92
+ generatorLimits[generatorTypes.modLfoToVolume] = { min: -960, max: 960, def: 0 };
93
+
94
+ // effects, pan
95
+ generatorLimits[generatorTypes.chorusEffectsSend] = { min: 0, max: 1000, def: 0 };
96
+ generatorLimits[generatorTypes.reverbEffectsSend] = { min: 0, max: 1000, def: 0 };
97
+ generatorLimits[generatorTypes.pan] = { min: -500, max: 500, def: 0 };
98
+
99
+ // lfo
100
+ generatorLimits[generatorTypes.delayModLFO] = { min: -12000, max: 5000, def: -12000 };
101
+ generatorLimits[generatorTypes.freqModLFO] = { min: -16000, max: 4500, def: 0 };
102
+ generatorLimits[generatorTypes.delayVibLFO] = { min: -12000, max: 5000, def: -12000 };
103
+ generatorLimits[generatorTypes.freqVibLFO] = { min: -16000, max: 4500, def: 0 };
104
+
105
+ // mod env
106
+ generatorLimits[generatorTypes.delayModEnv] = { min: -32768, max: 5000, def: -32768 }; // -32,768 indicates instant phase,
107
+ // this is done to prevent click at the start of filter modenv
108
+ generatorLimits[generatorTypes.attackModEnv] = { min: -32768, max: 8000, def: -32768 };
109
+ generatorLimits[generatorTypes.holdModEnv] = { min: -12000, max: 5000, def: -12000 };
110
+ generatorLimits[generatorTypes.decayModEnv] = { min: -12000, max: 8000, def: -12000 };
111
+ generatorLimits[generatorTypes.sustainModEnv] = { min: 0, max: 1000, def: 0 };
112
+ generatorLimits[generatorTypes.releaseModEnv] = { min: -7200, max: 8000, def: -12000 }; // min is set to -7200 to prevent lowpass clicks
113
+ // key num to mod env
114
+ generatorLimits[generatorTypes.keyNumToModEnvHold] = { min: -1200, max: 1200, def: 0 };
115
+ generatorLimits[generatorTypes.keyNumToModEnvDecay] = { min: -1200, max: 1200, def: 0 };
116
+
117
+ // vol env
118
+ generatorLimits[generatorTypes.delayVolEnv] = { min: -12000, max: 5000, def: -12000 };
119
+ generatorLimits[generatorTypes.attackVolEnv] = { min: -12000, max: 8000, def: -12000 };
120
+ generatorLimits[generatorTypes.holdVolEnv] = { min: -12000, max: 5000, def: -12000 };
121
+ generatorLimits[generatorTypes.decayVolEnv] = { min: -12000, max: 8000, def: -12000 };
122
+ generatorLimits[generatorTypes.sustainVolEnv] = { min: 0, max: 1440, def: 0 };
123
+ generatorLimits[generatorTypes.releaseVolEnv] = { min: -7200, max: 8000, def: -12000 }; // min is set to -7200 prevent clicks
124
+ // key num to vol env
125
+ generatorLimits[generatorTypes.keyNumToVolEnvHold] = { min: -1200, max: 1200, def: 0 };
126
+ generatorLimits[generatorTypes.keyNumToVolEnvDecay] = { min: -1200, max: 1200, def: 0 };
127
+
128
+ generatorLimits[generatorTypes.startloopAddrsCoarseOffset] = { min: -32768, max: 32768, def: 0 };
129
+ generatorLimits[generatorTypes.keyNum] = { min: -1, max: 127, def: -1 };
130
+ generatorLimits[generatorTypes.velocity] = { min: -1, max: 127, def: -1 };
131
+
132
+ generatorLimits[generatorTypes.initialAttenuation] = { min: 0, max: 1440, def: 0 };
133
+
134
+ generatorLimits[generatorTypes.endloopAddrsCoarseOffset] = { min: -32768, max: 32768, def: 0 };
135
+
136
+ generatorLimits[generatorTypes.coarseTune] = { min: -120, max: 120, def: 0 };
137
+ generatorLimits[generatorTypes.fineTune] = { min: -12700, max: 12700, def: 0 }; // this generator is used as initial pitch, hence this range
138
+ generatorLimits[generatorTypes.scaleTuning] = { min: 0, max: 1200, def: 100 };
139
+ generatorLimits[generatorTypes.exclusiveClass] = { min: 0, max: 99999, def: 0 };
140
+ generatorLimits[generatorTypes.overridingRootKey] = { min: 0 - 1, max: 127, def: -1 };
141
+ generatorLimits[generatorTypes.sampleModes] = { min: 0, max: 3, def: 0 };
142
+
143
+ export class Generator
144
+ {
145
+ /**
146
+ * The generator's enum number
147
+ * @type {generatorTypes|number}
148
+ */
149
+ generatorType = generatorTypes.INVALID;
150
+ /**
151
+ * The generator's 16-bit value
152
+ * @type {number}
153
+ */
154
+ generatorValue = 0;
155
+
156
+ /**
157
+ * Constructs a new generator
158
+ * @param type {generatorTypes|number}
159
+ * @param value {number}
160
+ * @param validate {boolean}
161
+ */
162
+ constructor(type = generatorTypes.INVALID, value = 0, validate = true)
163
+ {
164
+ this.generatorType = type;
165
+ if (value === undefined)
166
+ {
167
+ throw new Error("No value provided.");
168
+ }
169
+ this.generatorValue = Math.round(value);
170
+ if (validate)
171
+ {
172
+ const lim = generatorLimits[type];
173
+
174
+ if (lim !== undefined)
175
+ {
176
+ this.generatorValue = Math.max(lim.min, Math.min(lim.max, this.generatorValue));
177
+ }
178
+ }
179
+ }
180
+ }
181
+
182
+ /**
183
+ * generator.js
184
+ * purpose: contains enums for generators,
185
+ * and their limis parses reads soundfont generators, sums them and applies limits
186
+ */
187
+ /**
188
+ * @param generatorType {number}
189
+ * @param presetGens {Generator[]}
190
+ * @param instrumentGens {Generator[]}
191
+ */
192
+ export function addAndClampGenerator(generatorType, presetGens, instrumentGens)
193
+ {
194
+ const limits = generatorLimits[generatorType] || { min: 0, max: 32768, def: 0 };
195
+ let presetGen = presetGens.find(g => g.generatorType === generatorType);
196
+ let presetValue = 0;
197
+ if (presetGen)
198
+ {
199
+ presetValue = presetGen.generatorValue;
200
+ }
201
+
202
+ let instruGen = instrumentGens.find(g => g.generatorType === generatorType);
203
+ let instruValue = limits.def;
204
+ if (instruGen)
205
+ {
206
+ instruValue = instruGen.generatorValue;
207
+ }
208
+
209
+ let value = instruValue + presetValue;
210
+
211
+ // Special case, initial attenuation.
212
+ // Shall get clamped in the volume envelope,
213
+ // so the modulators can be affected by negative generators (the "Brass" patch was problematic...)
214
+ if (generatorType === generatorTypes.initialAttenuation)
215
+ {
216
+ return value;
217
+ }
218
+
219
+ return Math.max(limits.min, Math.min(limits.max, value));
220
+ }
spessasynth_lib/soundfont/basic_soundfont/modulator.js ADDED
@@ -0,0 +1,378 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { generatorTypes } from "./generator.js";
2
+ import { midiControllers } from "../../midi_parser/midi_message.js";
3
+
4
+ /**
5
+ * modulators.js
6
+ * purpose: parses soundfont modulators and the source enums, also includes the default modulators list
7
+ **/
8
+
9
+ export const modulatorSources = {
10
+ noController: 0,
11
+ noteOnVelocity: 2,
12
+ noteOnKeyNum: 3,
13
+ polyPressure: 10,
14
+ channelPressure: 13,
15
+ pitchWheel: 14,
16
+ pitchWheelRange: 16,
17
+ link: 127
18
+
19
+ };
20
+ export const modulatorCurveTypes = {
21
+ linear: 0,
22
+ concave: 1,
23
+ convex: 2,
24
+ switch: 3
25
+ };
26
+
27
+ export class Modulator
28
+ {
29
+ /**
30
+ * The current computed value of this modulator
31
+ * @type {number}
32
+ */
33
+ currentValue = 0;
34
+
35
+ /**
36
+ * The source enumeration for this modulator
37
+ * @type {number}
38
+ */
39
+ sourceEnum;
40
+
41
+ /**
42
+ * The secondary source enumeration for this modulator
43
+ * @type {number}
44
+ */
45
+ secondarySourceEnum;
46
+
47
+ /**
48
+ * The generator destination of this modulator
49
+ * @type {generatorTypes}
50
+ */
51
+ modulatorDestination;
52
+
53
+ /**
54
+ * The transform amount for this modulator
55
+ * @type {number}
56
+ */
57
+ transformAmount;
58
+
59
+ /**
60
+ * The transform type for this modulator
61
+ * @type {0|2}
62
+ */
63
+ transformType;
64
+
65
+ /**
66
+ * creates a modulator
67
+ * @param srcEnum {number}
68
+ * @param secSrcEnum {number}
69
+ * @param destination {generatorTypes|number}
70
+ * @param amount {number}
71
+ * @param transformType {number}
72
+ */
73
+ constructor(srcEnum, secSrcEnum, destination, amount, transformType)
74
+ {
75
+ this.sourceEnum = srcEnum;
76
+ this.modulatorDestination = destination;
77
+ this.secondarySourceEnum = secSrcEnum;
78
+ this.transformAmount = amount;
79
+ this.transformType = transformType;
80
+
81
+
82
+ if (this.modulatorDestination > 58)
83
+ {
84
+ this.modulatorDestination = generatorTypes.INVALID; // flag as invalid (for linked ones)
85
+ }
86
+
87
+ // decode the source
88
+ this.sourcePolarity = this.sourceEnum >> 9 & 1;
89
+ this.sourceDirection = this.sourceEnum >> 8 & 1;
90
+ this.sourceUsesCC = this.sourceEnum >> 7 & 1;
91
+ this.sourceIndex = this.sourceEnum & 127;
92
+ this.sourceCurveType = this.sourceEnum >> 10 & 3;
93
+
94
+ // decode the secondary source
95
+ this.secSrcPolarity = this.secondarySourceEnum >> 9 & 1;
96
+ this.secSrcDirection = this.secondarySourceEnum >> 8 & 1;
97
+ this.secSrcUsesCC = this.secondarySourceEnum >> 7 & 1;
98
+ this.secSrcIndex = this.secondarySourceEnum & 127;
99
+ this.secSrcCurveType = this.secondarySourceEnum >> 10 & 3;
100
+
101
+ /**
102
+ * Indicates if the given modulator is chorus or reverb effects modulator.
103
+ * This is done to simulate BASSMIDI effects behavior:
104
+ * - defaults to 1000 transform amount rather than 200
105
+ * - values can be changed, but anything above 200 is 1000
106
+ * (except for values above 1000, they are copied directly)
107
+ * - all values below are multiplied by 5 (200 * 5 = 1000)
108
+ * - still can be disabled if the soundfont has its own modulator curve
109
+ * - this fixes the very low amount of reverb by default and doesn't break soundfonts
110
+ * @type {boolean}
111
+ */
112
+ this.isEffectModulator =
113
+ (
114
+ this.sourceEnum === 0x00DB
115
+ || this.sourceEnum === 0x00DD
116
+ )
117
+ && this.secondarySourceEnum === 0x0
118
+ && (
119
+ this.modulatorDestination === generatorTypes.reverbEffectsSend
120
+ || this.modulatorDestination === generatorTypes.chorusEffectsSend
121
+ );
122
+ }
123
+
124
+ /**
125
+ * @param modulator {Modulator}
126
+ * @returns {Modulator}
127
+ */
128
+ static copy(modulator)
129
+ {
130
+ return new Modulator(
131
+ modulator.sourceEnum,
132
+ modulator.secondarySourceEnum,
133
+ modulator.modulatorDestination,
134
+ modulator.transformAmount,
135
+ modulator.transformType
136
+ );
137
+ }
138
+
139
+ /**
140
+ * @param mod1 {Modulator}
141
+ * @param mod2 {Modulator}
142
+ * @param checkAmount {boolean}
143
+ * @returns {boolean}
144
+ */
145
+ static isIdentical(mod1, mod2, checkAmount = false)
146
+ {
147
+ return (mod1.sourceEnum === mod2.sourceEnum)
148
+ && (mod1.modulatorDestination === mod2.modulatorDestination)
149
+ && (mod1.secondarySourceEnum === mod2.secondarySourceEnum)
150
+ && (mod1.transformType === mod2.transformType)
151
+ && (!checkAmount || (mod1.transformAmount === mod2.transformAmount));
152
+ }
153
+
154
+ /**
155
+ * @param mod {Modulator}
156
+ * @returns {string}
157
+ */
158
+ static debugString(mod)
159
+ {
160
+ function getKeyByValue(object, value)
161
+ {
162
+ return Object.keys(object).find(key => object[key] === value);
163
+ }
164
+
165
+ let sourceString = getKeyByValue(modulatorCurveTypes, mod.sourceCurveType);
166
+ sourceString += mod.sourcePolarity === 0 ? " unipolar " : " bipolar ";
167
+ sourceString += mod.sourceDirection === 0 ? "forwards " : "backwards ";
168
+ if (mod.sourceUsesCC)
169
+ {
170
+ sourceString += getKeyByValue(midiControllers, mod.sourceIndex);
171
+ }
172
+ else
173
+ {
174
+ sourceString += getKeyByValue(modulatorSources, mod.sourceIndex);
175
+ }
176
+
177
+ let secSrcString = getKeyByValue(modulatorCurveTypes, mod.secSrcCurveType);
178
+ secSrcString += mod.secSrcPolarity === 0 ? " unipolar " : " bipolar ";
179
+ secSrcString += mod.secSrcCurveType === 0 ? "forwards " : "backwards ";
180
+ if (mod.secSrcUsesCC)
181
+ {
182
+ secSrcString += getKeyByValue(midiControllers, mod.secSrcIndex);
183
+ }
184
+ else
185
+ {
186
+ secSrcString += getKeyByValue(modulatorSources, mod.secSrcIndex);
187
+ }
188
+ return `Modulator:
189
+ Source: ${sourceString}
190
+ Secondary source: ${secSrcString}
191
+ Destination: ${getKeyByValue(generatorTypes, mod.modulatorDestination)}
192
+ Trasform amount: ${mod.transformAmount}
193
+ Transform type: ${mod.transformType}
194
+ \n\n`;
195
+ }
196
+
197
+ /**
198
+ * Sum transform and create a NEW modulator
199
+ * @param modulator {Modulator}
200
+ * @returns {Modulator}
201
+ */
202
+ sumTransform(modulator)
203
+ {
204
+ return new Modulator(
205
+ this.sourceEnum,
206
+ this.secondarySourceEnum,
207
+ this.modulatorDestination,
208
+ this.transformAmount + modulator.transformAmount,
209
+ this.transformType
210
+ );
211
+ }
212
+ }
213
+
214
+ export const DEFAULT_ATTENUATION_MOD_AMOUNT = 960;
215
+ export const DEFAULT_ATTENUATION_MOD_CURVE_TYPE = modulatorCurveTypes.concave;
216
+
217
+ export function getModSourceEnum(curveType, polarity, direction, isCC, index)
218
+ {
219
+ return (curveType << 10) | (polarity << 9) | (direction << 8) | (isCC << 7) | index;
220
+ }
221
+
222
+ const soundFontModulators = [
223
+ // vel to attenuation
224
+ new Modulator(
225
+ getModSourceEnum(
226
+ DEFAULT_ATTENUATION_MOD_CURVE_TYPE,
227
+ 0,
228
+ 1,
229
+ 0,
230
+ modulatorSources.noteOnVelocity
231
+ ),
232
+ 0x0,
233
+ generatorTypes.initialAttenuation,
234
+ DEFAULT_ATTENUATION_MOD_AMOUNT,
235
+ 0
236
+ ),
237
+
238
+ // mod wheel to vibrato
239
+ new Modulator(0x0081, 0x0, generatorTypes.vibLfoToPitch, 50, 0),
240
+
241
+ // vol to attenuation
242
+ new Modulator(
243
+ getModSourceEnum(
244
+ DEFAULT_ATTENUATION_MOD_CURVE_TYPE,
245
+ 0,
246
+ 1,
247
+ 1,
248
+ midiControllers.mainVolume
249
+ ),
250
+ 0x0,
251
+ generatorTypes.initialAttenuation,
252
+ DEFAULT_ATTENUATION_MOD_AMOUNT,
253
+ 0
254
+ ),
255
+
256
+ // channel pressure to vibrato
257
+ new Modulator(0x000D, 0x0, generatorTypes.vibLfoToPitch, 50, 0),
258
+
259
+ // pitch wheel to tuning
260
+ new Modulator(0x020E, 0x0010, generatorTypes.fineTune, 12700, 0),
261
+
262
+ // pan to uhh, pan
263
+ // amount is 500 instead of 1000, see #59
264
+ new Modulator(0x028A, 0x0, generatorTypes.pan, 500, 0),
265
+
266
+ // expression to attenuation
267
+ new Modulator(
268
+ getModSourceEnum(
269
+ DEFAULT_ATTENUATION_MOD_CURVE_TYPE,
270
+ 0,
271
+ 1,
272
+ 1,
273
+ midiControllers.expressionController
274
+ ),
275
+ 0x0,
276
+ generatorTypes.initialAttenuation,
277
+ DEFAULT_ATTENUATION_MOD_AMOUNT,
278
+ 0
279
+ ),
280
+
281
+ // reverb effects to send
282
+ new Modulator(0x00DB, 0x0, generatorTypes.reverbEffectsSend, 200, 0),
283
+
284
+ // chorus effects to send
285
+ new Modulator(0x00DD, 0x0, generatorTypes.chorusEffectsSend, 200, 0)
286
+ ];
287
+
288
+ const customModulators = [
289
+ // custom modulators heck yeah
290
+ // poly pressure to vibrato
291
+ new Modulator(
292
+ getModSourceEnum(modulatorCurveTypes.linear, 0, 0, 0, modulatorSources.polyPressure),
293
+ 0x0,
294
+ generatorTypes.vibLfoToPitch,
295
+ 50,
296
+ 0
297
+ ),
298
+
299
+ // cc 92 (tremolo) to modLFO volume
300
+ new Modulator(
301
+ getModSourceEnum(
302
+ modulatorCurveTypes.linear,
303
+ 0,
304
+ 0,
305
+ 1,
306
+ midiControllers.tremoloDepth
307
+ ), /*linear forward unipolar cc 92 */
308
+ 0x0, // no controller
309
+ generatorTypes.modLfoToVolume,
310
+ 24,
311
+ 0
312
+ ),
313
+
314
+ // cc 73 (attack time) to volEnv attack
315
+ new Modulator(
316
+ getModSourceEnum(
317
+ modulatorCurveTypes.convex,
318
+ 1,
319
+ 0,
320
+ 1,
321
+ midiControllers.attackTime
322
+ ), // linear forward bipolar cc 72
323
+ 0x0, // no controller
324
+ generatorTypes.attackVolEnv,
325
+ 6000,
326
+ 0
327
+ ),
328
+
329
+ // cc 72 (release time) to volEnv release
330
+ new Modulator(
331
+ getModSourceEnum(
332
+ modulatorCurveTypes.linear,
333
+ 1,
334
+ 0,
335
+ 1,
336
+ midiControllers.releaseTime
337
+ ), // linear forward bipolar cc 72
338
+ 0x0, // no controller
339
+ generatorTypes.releaseVolEnv,
340
+ 3600,
341
+ 0
342
+ ),
343
+
344
+ // cc 74 (brightness) to filterFc
345
+ new Modulator(
346
+ getModSourceEnum(
347
+ modulatorCurveTypes.linear,
348
+ 1,
349
+ 0,
350
+ 1,
351
+ midiControllers.brightness
352
+ ), // linear forwards bipolar cc 74
353
+ 0x0, // no controller
354
+ generatorTypes.initialFilterFc,
355
+ 6000,
356
+ 0
357
+ ),
358
+
359
+ // cc 71 (filter Q) to filter Q
360
+ new Modulator(
361
+ getModSourceEnum(
362
+ modulatorCurveTypes.linear,
363
+ 1,
364
+ 0,
365
+ 1,
366
+ midiControllers.filterResonance
367
+ ), // linear forwards bipolar cc 74
368
+ 0x0, // no controller
369
+ generatorTypes.initialFilterQ,
370
+ 250,
371
+ 0
372
+ )
373
+ ];
374
+
375
+ /**
376
+ * @type {Modulator[]}
377
+ */
378
+ export const defaultModulators = soundFontModulators.concat(customModulators);
spessasynth_lib/soundfont/basic_soundfont/riff_chunk.js ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IndexedByteArray } from "../../utils/indexed_array.js";
2
+ import { readLittleEndian, writeDword } from "../../utils/byte_functions/little_endian.js";
3
+ import { readBytesAsString, writeStringAsBytes } from "../../utils/byte_functions/string.js";
4
+
5
+ /**
6
+ * riff_chunk.js
7
+ * reads a riff read and stores it as a class
8
+ */
9
+
10
+ export class RiffChunk
11
+ {
12
+ /**
13
+ * Creates a new riff read
14
+ * @constructor
15
+ * @param header {string}
16
+ * @param size {number}
17
+ * @param data {IndexedByteArray}
18
+ */
19
+ constructor(header, size, data)
20
+ {
21
+ this.header = header;
22
+ this.size = size;
23
+ this.chunkData = data;
24
+ }
25
+
26
+ }
27
+
28
+ /**
29
+ * @param dataArray {IndexedByteArray}
30
+ * @param readData {boolean}
31
+ * @param forceShift {boolean}
32
+ * @returns {RiffChunk}
33
+ */
34
+ export function readRIFFChunk(dataArray, readData = true, forceShift = false)
35
+ {
36
+ let header = readBytesAsString(dataArray, 4);
37
+
38
+ let size = readLittleEndian(dataArray, 4);
39
+ let chunkData = undefined;
40
+ if (readData)
41
+ {
42
+ chunkData = new IndexedByteArray(dataArray.buffer.slice(dataArray.currentIndex, dataArray.currentIndex + size));
43
+ }
44
+ if (readData || forceShift)
45
+ {
46
+ dataArray.currentIndex += size;
47
+ }
48
+
49
+ if (size % 2 !== 0)
50
+ {
51
+ if (dataArray[dataArray.currentIndex] === 0)
52
+ {
53
+ dataArray.currentIndex++;
54
+ }
55
+ }
56
+
57
+ return new RiffChunk(header, size, chunkData);
58
+ }
59
+
60
+ /**
61
+ * @param chunk {RiffChunk}
62
+ * @param prepend {IndexedByteArray}
63
+ * @returns {IndexedByteArray}
64
+ */
65
+ export function writeRIFFChunk(chunk, prepend = undefined)
66
+ {
67
+ let size = 8 + chunk.size;
68
+ if (chunk.size % 2 !== 0)
69
+ {
70
+ size++;
71
+ }
72
+ if (prepend)
73
+ {
74
+ size += prepend.length;
75
+ }
76
+ const array = new IndexedByteArray(size);
77
+ // prepend data (for example, type before the read)
78
+ if (prepend)
79
+ {
80
+ array.set(prepend, array.currentIndex);
81
+ array.currentIndex += prepend.length;
82
+ }
83
+ // write header
84
+ writeStringAsBytes(array, chunk.header);
85
+ // write size (excluding header and the size itself) and then prepend if specified
86
+ writeDword(array, size - 8 - (prepend?.length || 0));
87
+ // write data
88
+ array.set(chunk.chunkData, array.currentIndex);
89
+ return array;
90
+ }
91
+
92
+ /**
93
+ * @param header {string}
94
+ * @param data {Uint8Array}
95
+ * @param addZeroByte {Boolean}
96
+ * @param isList {boolean}
97
+ * @returns {IndexedByteArray}
98
+ */
99
+ export function writeRIFFOddSize(header, data, addZeroByte = false, isList = false)
100
+ {
101
+ if (addZeroByte)
102
+ {
103
+ const tempData = new Uint8Array(data.length + 1);
104
+ tempData.set(data);
105
+ data = tempData;
106
+ }
107
+ let offset = 8;
108
+ let finalSize = offset + data.length;
109
+ let writtenSize = data.length;
110
+ if (finalSize % 2 !== 0)
111
+ {
112
+ finalSize++;
113
+ }
114
+ let headerWritten = header;
115
+ if (isList)
116
+ {
117
+ finalSize += 4;
118
+ writtenSize += 4;
119
+ offset += 4;
120
+ headerWritten = "LIST";
121
+ }
122
+ const outArray = new IndexedByteArray(finalSize);
123
+ writeStringAsBytes(outArray, headerWritten);
124
+ writeDword(outArray, writtenSize);
125
+ if (isList)
126
+ {
127
+ writeStringAsBytes(outArray, header);
128
+ }
129
+ outArray.set(data, offset);
130
+ return outArray;
131
+ }
132
+
133
+ /**
134
+ * @param collection {RiffChunk[]}
135
+ * @param type {string}
136
+ * @returns {RiffChunk|undefined}
137
+ */
138
+ export function findRIFFListType(collection, type)
139
+ {
140
+ return collection.find(c =>
141
+ {
142
+ if (c.header !== "LIST")
143
+ {
144
+ return false;
145
+ }
146
+ c.chunkData.currentIndex = 0;
147
+ return readBytesAsString(c.chunkData, 4) === type;
148
+ });
149
+ }
spessasynth_lib/soundfont/basic_soundfont/write_dls/art2.js ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getDLSArticulatorFromSf2Generator, getDLSArticulatorFromSf2Modulator } from "./modulator_converter.js";
2
+ import { writeRIFFOddSize } from "../riff_chunk.js";
3
+ import { combineArrays, IndexedByteArray } from "../../../utils/indexed_array.js";
4
+ import { Generator, generatorTypes } from "../generator.js";
5
+ import { writeDword } from "../../../utils/byte_functions/little_endian.js";
6
+ import { consoleColors } from "../../../utils/other.js";
7
+ import { SpessaSynthInfo, SpessaSynthWarn } from "../../../utils/loggin.js";
8
+ import { Modulator } from "../modulator.js";
9
+ import {
10
+ DEFAULT_DLS_CHORUS,
11
+ DEFAULT_DLS_REVERB,
12
+ DLS_1_NO_VIBRATO_MOD,
13
+ DLS_1_NO_VIBRATO_PRESSURE
14
+ } from "../../dls/dls_sources.js";
15
+
16
+ const invalidGeneratorTypes = new Set([
17
+ generatorTypes.sampleModes,
18
+ generatorTypes.initialAttenuation,
19
+ generatorTypes.keyRange,
20
+ generatorTypes.velRange,
21
+ generatorTypes.sampleID,
22
+ generatorTypes.fineTune,
23
+ generatorTypes.coarseTune,
24
+ generatorTypes.startAddrsOffset,
25
+ generatorTypes.startAddrsCoarseOffset,
26
+ generatorTypes.endAddrOffset,
27
+ generatorTypes.endAddrsCoarseOffset,
28
+ generatorTypes.startloopAddrsOffset,
29
+ generatorTypes.startloopAddrsCoarseOffset,
30
+ generatorTypes.endloopAddrsOffset,
31
+ generatorTypes.endloopAddrsCoarseOffset,
32
+ generatorTypes.overridingRootKey,
33
+ generatorTypes.exclusiveClass
34
+ ]);
35
+
36
+ /**
37
+ * @param zone {BasicInstrumentZone}
38
+ * @returns {IndexedByteArray}
39
+ */
40
+ export function writeArticulator(zone)
41
+ {
42
+
43
+
44
+ // envelope generators are limited to 40 seconds
45
+ // in timecents, this is 1200 * log2(10) = 6386
46
+
47
+ for (let i = 0; i < zone.generators.length; i++)
48
+ {
49
+ const g = zone.generators[i];
50
+ if (
51
+ g.generatorType === generatorTypes.delayVolEnv ||
52
+ g.generatorType === generatorTypes.attackVolEnv ||
53
+ g.generatorType === generatorTypes.holdVolEnv ||
54
+ g.generatorType === generatorTypes.decayVolEnv ||
55
+ g.generatorType === generatorTypes.releaseVolEnv ||
56
+ g.generatorType === generatorTypes.delayModEnv ||
57
+ g.generatorType === generatorTypes.attackModEnv ||
58
+ g.generatorType === generatorTypes.holdModEnv ||
59
+ g.generatorType === generatorTypes.decayModEnv
60
+ )
61
+ {
62
+ zone.generators[i] = new Generator(g.generatorType, Math.min(g.generatorValue, 6386), false);
63
+ }
64
+ }
65
+
66
+
67
+ // read_articulation.js:
68
+ // according to viena and another strange (with modulators) rendition of gm.dls in sf2,
69
+ // it shall be divided by -128,
70
+ // and a strange correction needs to be applied to the real value:
71
+ // real + (60 / 128) * scale
72
+ // we invert this here
73
+ for (let i = 0; i < zone.generators.length; i++)
74
+ {
75
+ const relativeGenerator = zone.generators[i];
76
+ let absoluteCounterpart = undefined;
77
+ switch (relativeGenerator.generatorType)
78
+ {
79
+ default:
80
+ continue;
81
+
82
+ case generatorTypes.keyNumToVolEnvDecay:
83
+ absoluteCounterpart = generatorTypes.decayVolEnv;
84
+ break;
85
+ case generatorTypes.keyNumToVolEnvHold:
86
+ absoluteCounterpart = generatorTypes.holdVolEnv;
87
+ break;
88
+ case generatorTypes.keyNumToModEnvDecay:
89
+ absoluteCounterpart = generatorTypes.decayModEnv;
90
+ break;
91
+ case generatorTypes.keyNumToModEnvHold:
92
+ absoluteCounterpart = generatorTypes.holdModEnv;
93
+ }
94
+ let absoluteGenerator = zone.generators.find(g => g.generatorType === absoluteCounterpart);
95
+ if (absoluteGenerator === undefined)
96
+ {
97
+ // there's no absolute generator here.
98
+ continue;
99
+ }
100
+ const dlsRelative = relativeGenerator.generatorValue * -128;
101
+ const subtraction = (60 / 128) * dlsRelative;
102
+ const newAbsolute = absoluteGenerator.generatorValue - subtraction;
103
+
104
+ const iR = zone.generators.indexOf(relativeGenerator);
105
+ const iA = zone.generators.indexOf(absoluteGenerator);
106
+ zone.generators[iA] =
107
+ new Generator(absoluteCounterpart, newAbsolute, false);
108
+ zone.generators[iR] =
109
+ new Generator(relativeGenerator.generatorType, dlsRelative, false);
110
+ }
111
+ /**
112
+ * @type {Articulator[]}
113
+ */
114
+ const generators = zone.generators.reduce((arrs, g) =>
115
+ {
116
+ if (invalidGeneratorTypes.has(g.generatorType))
117
+ {
118
+ return arrs;
119
+ }
120
+ const art = getDLSArticulatorFromSf2Generator(g);
121
+ if (art !== undefined)
122
+ {
123
+ arrs.push(art);
124
+ SpessaSynthInfo("%cSucceeded converting to DLS Articulator!", consoleColors.recognized);
125
+
126
+ }
127
+ else
128
+ {
129
+ SpessaSynthWarn("Failed converting to DLS Articulator!");
130
+ }
131
+ return arrs;
132
+ }, []);
133
+ /**
134
+ * @type {Articulator[]}
135
+ */
136
+ const modulators = zone.modulators.reduce((arrs, m) =>
137
+ {
138
+ // do not write the default DLS modulators
139
+ if (
140
+ Modulator.isIdentical(m, DEFAULT_DLS_CHORUS, true) ||
141
+ Modulator.isIdentical(m, DEFAULT_DLS_REVERB, true) ||
142
+ Modulator.isIdentical(m, DLS_1_NO_VIBRATO_MOD, true) ||
143
+ Modulator.isIdentical(m, DLS_1_NO_VIBRATO_PRESSURE, true)
144
+ )
145
+ {
146
+ return arrs;
147
+ }
148
+ const art = getDLSArticulatorFromSf2Modulator(m);
149
+ if (art !== undefined)
150
+ {
151
+ arrs.push(art);
152
+ SpessaSynthInfo("%cSucceeded converting to DLS Articulator!", consoleColors.recognized);
153
+
154
+ }
155
+ else
156
+ {
157
+ SpessaSynthWarn("Failed converting to DLS Articulator!");
158
+ }
159
+ return arrs;
160
+ }, []);
161
+ generators.push(...modulators);
162
+
163
+ const art2Data = new IndexedByteArray(8);
164
+ writeDword(art2Data, 8); // cbSize
165
+ writeDword(art2Data, generators.length); // cbConnectionBlocks
166
+
167
+
168
+ const out = generators.map(a => a.writeArticulator());
169
+ return writeRIFFOddSize(
170
+ "art2",
171
+ combineArrays([art2Data, ...out])
172
+ );
173
+ }
spessasynth_lib/soundfont/basic_soundfont/write_dls/articulator.js ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IndexedByteArray } from "../../../utils/indexed_array.js";
2
+ import { writeDword, writeWord } from "../../../utils/byte_functions/little_endian.js";
3
+
4
+ export class Articulator
5
+ {
6
+ /**
7
+ * @type {DLSSources}
8
+ */
9
+ source;
10
+ /**
11
+ * @type {DLSSources}
12
+ */
13
+ control;
14
+ /**
15
+ * @type {DLSDestinations}
16
+ */
17
+ destination;
18
+ /**
19
+ * @type {number}
20
+ */
21
+ scale;
22
+ /**
23
+ * @type {number}
24
+ */
25
+ transform;
26
+
27
+ constructor(source, control, destination, scale, transform)
28
+ {
29
+ this.source = source;
30
+ this.control = control;
31
+ this.destination = destination;
32
+ this.scale = scale;
33
+ this.transform = transform;
34
+ }
35
+
36
+ /**
37
+ * @returns {IndexedByteArray}
38
+ */
39
+ writeArticulator()
40
+ {
41
+ const out = new IndexedByteArray(12);
42
+ writeWord(out, this.source);
43
+ writeWord(out, this.control);
44
+ writeWord(out, this.destination);
45
+ writeWord(out, this.transform);
46
+ writeDword(out, this.scale << 16);
47
+ return out;
48
+ }
49
+ }
spessasynth_lib/soundfont/basic_soundfont/write_dls/combine_zones.js ADDED
@@ -0,0 +1,400 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Modulator } from "../modulator.js";
2
+ import { BasicInstrumentZone } from "../basic_zones.js";
3
+ import { Generator, generatorLimits, generatorTypes } from "../generator.js";
4
+
5
+ const notGlobalizedTypes = new Set([
6
+ generatorTypes.velRange,
7
+ generatorTypes.keyRange,
8
+ generatorTypes.instrument,
9
+ generatorTypes.exclusiveClass,
10
+ generatorTypes.endOper,
11
+ generatorTypes.sampleModes,
12
+ generatorTypes.startloopAddrsOffset,
13
+ generatorTypes.startloopAddrsCoarseOffset,
14
+ generatorTypes.endloopAddrsOffset,
15
+ generatorTypes.endloopAddrsCoarseOffset,
16
+ generatorTypes.startAddrsOffset,
17
+ generatorTypes.startAddrsCoarseOffset,
18
+ generatorTypes.endAddrOffset,
19
+ generatorTypes.endAddrsCoarseOffset,
20
+ generatorTypes.initialAttenuation, // written into wsmp, there's no global wsmp
21
+ generatorTypes.fineTune, // written into wsmp, there's no global wsmp
22
+ generatorTypes.coarseTune, // written into wsmp, there's no global wsmp
23
+ generatorTypes.keyNumToVolEnvHold, // KEY TO SOMETHING:
24
+ generatorTypes.keyNumToVolEnvDecay,// cannot be globalized as they modify their respective generators
25
+ generatorTypes.keyNumToModEnvHold, // (for example, keyNumToVolEnvDecay modifies VolEnvDecay)
26
+ generatorTypes.keyNumToModEnvDecay
27
+ ]);
28
+
29
+ /**
30
+ * Combines preset zones
31
+ * @param preset {BasicPreset}
32
+ * @param globalize {boolean}
33
+ * @returns {BasicInstrumentZone[]}
34
+ */
35
+ export function combineZones(preset, globalize = true)
36
+ {
37
+ /**
38
+ * @param main {Generator[]}
39
+ * @param adder {Generator[]}
40
+ */
41
+ function addUnique(main, adder)
42
+ {
43
+ main.push(...adder.filter(g => !main.find(mg => mg.generatorType === g.generatorType)));
44
+ }
45
+
46
+ /**
47
+ * @param r1 {SoundFontRange}
48
+ * @param r2 {SoundFontRange}
49
+ * @returns {SoundFontRange}
50
+ */
51
+ function subtractRanges(r1, r2)
52
+ {
53
+ return { min: Math.max(r1.min, r2.min), max: Math.min(r1.max, r2.max) };
54
+ }
55
+
56
+ /**
57
+ * @param main {Modulator[]}
58
+ * @param adder {Modulator[]}
59
+ */
60
+ function addUniqueMods(main, adder)
61
+ {
62
+ main.push(...adder.filter(m => !main.find(mm => Modulator.isIdentical(m, mm))));
63
+ }
64
+
65
+ /**
66
+ * @type {BasicInstrumentZone[]}
67
+ */
68
+ const finalZones = [];
69
+
70
+ /**
71
+ * @type {Generator[]}
72
+ */
73
+ const globalPresetGenerators = [];
74
+ /**
75
+ * @type {Modulator[]}
76
+ */
77
+ const globalPresetModulators = [];
78
+ let globalPresetKeyRange = { min: 0, max: 127 };
79
+ let globalPresetVelRange = { min: 0, max: 127 };
80
+
81
+ // find the global zone and apply ranges, generators, and modulators
82
+ const globalPresetZone = preset.presetZones.find(z => z.isGlobal);
83
+ if (globalPresetZone)
84
+ {
85
+ globalPresetGenerators.push(...globalPresetZone.generators);
86
+ globalPresetModulators.push(...globalPresetZone.modulators);
87
+ globalPresetKeyRange = globalPresetZone.keyRange;
88
+ globalPresetVelRange = globalPresetZone.velRange;
89
+ }
90
+ // for each non-global preset zone
91
+ for (const presetZone of preset.presetZones)
92
+ {
93
+ if (presetZone.isGlobal)
94
+ {
95
+ continue;
96
+ }
97
+ // use global ranges if not provided
98
+ let presetZoneKeyRange = presetZone.keyRange;
99
+ if (!presetZone.hasKeyRange)
100
+ {
101
+ presetZoneKeyRange = globalPresetKeyRange;
102
+ }
103
+ let presetZoneVelRange = presetZone.velRange;
104
+ if (!presetZone.hasVelRange)
105
+ {
106
+ presetZoneVelRange = globalPresetVelRange;
107
+ }
108
+ // add unique generators and modulators from the global zone
109
+ const presetGenerators = presetZone.generators.map(g => new Generator(g.generatorType, g.generatorValue));
110
+ addUnique(presetGenerators, globalPresetGenerators);
111
+ const presetModulators = [...presetZone.modulators];
112
+ addUniqueMods(presetModulators, globalPresetModulators);
113
+
114
+ const iZones = presetZone.instrument.instrumentZones;
115
+ /**
116
+ * @type {Generator[]}
117
+ */
118
+ const globalInstGenerators = [];
119
+ /**
120
+ * @type {Modulator[]}
121
+ */
122
+ const globalInstModulators = [];
123
+ let globalInstKeyRange = { min: 0, max: 127 };
124
+ let globalInstVelRange = { min: 0, max: 127 };
125
+ const globalInstZone = iZones.find(z => z.isGlobal);
126
+ if (globalInstZone)
127
+ {
128
+ globalInstGenerators.push(...globalInstZone.generators);
129
+ globalInstModulators.push(...globalInstZone.modulators);
130
+ globalInstKeyRange = globalInstZone.keyRange;
131
+ globalInstVelRange = globalInstZone.velRange;
132
+ }
133
+ // for each non-global instrument zone
134
+ for (const instZone of iZones)
135
+ {
136
+ if (instZone.isGlobal)
137
+ {
138
+ continue;
139
+ }
140
+ // use global ranges if not provided
141
+ let instZoneKeyRange = instZone.keyRange;
142
+ if (!instZone.hasKeyRange)
143
+ {
144
+ instZoneKeyRange = globalInstKeyRange;
145
+ }
146
+ let instZoneVelRange = instZone.velRange;
147
+ if (!instZone.hasVelRange)
148
+ {
149
+ instZoneVelRange = globalInstVelRange;
150
+ }
151
+ instZoneKeyRange = subtractRanges(instZoneKeyRange, presetZoneKeyRange);
152
+ instZoneVelRange = subtractRanges(instZoneVelRange, presetZoneVelRange);
153
+
154
+ // if either of the zones is out of range (i.e.m min larger than the max),
155
+ // then we discard that zone
156
+ if (instZoneKeyRange.max < instZoneKeyRange.min || instZoneVelRange.max < instZoneVelRange.min)
157
+ {
158
+ continue;
159
+ }
160
+
161
+ // add unique generators and modulators from the global zone
162
+ const instGenerators = instZone.generators.map(g => new Generator(g.generatorType, g.generatorValue));
163
+ addUnique(instGenerators, globalInstGenerators);
164
+ const instModulators = [...instZone.modulators];
165
+ addUniqueMods(instModulators, globalInstModulators);
166
+
167
+ /**
168
+ * sum preset modulators to instruments (amount) sf spec page 54
169
+ * @type {Modulator[]}
170
+ */
171
+ const finalModList = [...instModulators];
172
+ for (const mod of presetModulators)
173
+ {
174
+ const identicalInstMod = finalModList.findIndex(
175
+ m => Modulator.isIdentical(mod, m));
176
+ if (identicalInstMod !== -1)
177
+ {
178
+ // sum the amounts
179
+ // (this makes a new modulator
180
+ // because otherwise it would overwrite the one in the soundfont!
181
+ finalModList[identicalInstMod] = finalModList[identicalInstMod].sumTransform(
182
+ mod);
183
+ }
184
+ else
185
+ {
186
+ finalModList.push(mod);
187
+ }
188
+ }
189
+
190
+ // clone the generators as the values are modified during DLS conversion (keyNumToSomething)
191
+ let finalGenList = instGenerators.map(g => new Generator(g.generatorType, g.generatorValue));
192
+ for (const gen of presetGenerators)
193
+ {
194
+ if (gen.generatorType === generatorTypes.velRange ||
195
+ gen.generatorType === generatorTypes.keyRange ||
196
+ gen.generatorType === generatorTypes.instrument ||
197
+ gen.generatorType === generatorTypes.endOper ||
198
+ gen.generatorType === generatorTypes.sampleModes)
199
+ {
200
+ continue;
201
+ }
202
+ const identicalInstGen = instGenerators.findIndex(g => g.generatorType === gen.generatorType);
203
+ if (identicalInstGen !== -1)
204
+ {
205
+ // if exists, sum to that generator
206
+ const newAmount = finalGenList[identicalInstGen].generatorValue + gen.generatorValue;
207
+ finalGenList[identicalInstGen] = new Generator(gen.generatorType, newAmount);
208
+ }
209
+ else
210
+ {
211
+ // if not, sum to the default generator
212
+ const newAmount = generatorLimits[gen.generatorType].def + gen.generatorValue;
213
+ finalGenList.push(new Generator(gen.generatorType, newAmount));
214
+ }
215
+ }
216
+
217
+ // remove unwanted
218
+ finalGenList = finalGenList.filter(g =>
219
+ g.generatorType !== generatorTypes.sampleID &&
220
+ g.generatorType !== generatorTypes.keyRange &&
221
+ g.generatorType !== generatorTypes.velRange &&
222
+ g.generatorType !== generatorTypes.endOper &&
223
+ g.generatorType !== generatorTypes.instrument &&
224
+ g.generatorValue !== generatorLimits[g.generatorType].def
225
+ );
226
+
227
+ // create the zone and copy over values
228
+ const zone = new BasicInstrumentZone();
229
+ zone.keyRange = instZoneKeyRange;
230
+ zone.velRange = instZoneVelRange;
231
+ if (zone.keyRange.min === 0 && zone.keyRange.max === 127)
232
+ {
233
+ zone.keyRange.min = -1;
234
+ }
235
+ if (zone.velRange.min === 0 && zone.velRange.max === 127)
236
+ {
237
+ zone.velRange.min = -1;
238
+ }
239
+ zone.isGlobal = false;
240
+ zone.sample = instZone.sample;
241
+ zone.generators = finalGenList;
242
+ zone.modulators = finalModList;
243
+ finalZones.push(zone);
244
+ }
245
+ }
246
+
247
+ if (globalize)
248
+ {
249
+ // create a global zone and add repeating generators to it
250
+ // also modulators
251
+ const globalZone = new BasicInstrumentZone();
252
+ globalZone.isGlobal = true;
253
+ // iterate over every type of generator
254
+ for (let checkedType = 0; checkedType < 58; checkedType++)
255
+ {
256
+ // not these though
257
+ if (notGlobalizedTypes.has(checkedType))
258
+ {
259
+ continue;
260
+ }
261
+ /**
262
+ * @type {Object<string, number>}
263
+ */
264
+ let occurencesForValues = {};
265
+ const defaultForChecked = generatorLimits[checkedType]?.def || 0;
266
+ occurencesForValues[defaultForChecked] = 0;
267
+ for (const z of finalZones)
268
+ {
269
+ const gen = z.generators.find(g => g.generatorType === checkedType);
270
+ if (gen)
271
+ {
272
+ const value = gen.generatorValue;
273
+ if (occurencesForValues[value] === undefined)
274
+ {
275
+ occurencesForValues[value] = 1;
276
+ }
277
+ else
278
+ {
279
+ occurencesForValues[value]++;
280
+ }
281
+ }
282
+ else
283
+ {
284
+ occurencesForValues[defaultForChecked]++;
285
+ }
286
+
287
+ // if the checked type has the keyNumTo something generator set, it cannot be globalized.
288
+ let relativeCounterpart;
289
+ switch (checkedType)
290
+ {
291
+ default:
292
+ continue;
293
+
294
+ case generatorTypes.decayVolEnv:
295
+ relativeCounterpart = generatorTypes.keyNumToVolEnvDecay;
296
+ break;
297
+ case generatorTypes.holdVolEnv:
298
+ relativeCounterpart = generatorTypes.keyNumToVolEnvHold;
299
+ break;
300
+ case generatorTypes.decayModEnv:
301
+ relativeCounterpart = generatorTypes.keyNumToModEnvDecay;
302
+ break;
303
+ case generatorTypes.holdModEnv:
304
+ relativeCounterpart = generatorTypes.keyNumToModEnvHold;
305
+ }
306
+ const relative = z.generators.find(g => g.generatorType === relativeCounterpart);
307
+ if (relative !== undefined)
308
+ {
309
+ occurencesForValues = {};
310
+ break;
311
+ }
312
+ }
313
+ // if at least one occurrence, find the most used one and add it to global
314
+ if (Object.keys(occurencesForValues).length > 0)
315
+ {
316
+ // [value, occurrences]
317
+ const valueToGlobalize = Object.entries(occurencesForValues).reduce((max, curr) =>
318
+ {
319
+ if (max[1] < curr[1])
320
+ {
321
+ return curr;
322
+ }
323
+ return max;
324
+ }, [0, 0]);
325
+ const targetValue = parseInt(valueToGlobalize[0]);
326
+
327
+ // if the global value is the default value just remove it, no need to add it
328
+ if (targetValue !== defaultForChecked)
329
+ {
330
+ globalZone.generators.push(new Generator(checkedType, targetValue));
331
+ }
332
+ // remove from the zones
333
+ finalZones.forEach(z =>
334
+ {
335
+ const gen = z.generators.findIndex(g =>
336
+ g.generatorType === checkedType);
337
+ if (gen !== -1)
338
+ {
339
+ if (z.generators[gen].generatorValue === targetValue)
340
+ {
341
+ // That exact value exists. Since it's global now, remove it
342
+ z.generators.splice(gen, 1);
343
+ }
344
+ }
345
+ else
346
+ {
347
+ // That type does not exist at all here.
348
+ // Since we're globalizing, we need to add the default here.
349
+ if (targetValue !== defaultForChecked)
350
+ {
351
+ z.generators.push(new Generator(checkedType, defaultForChecked));
352
+ }
353
+ }
354
+ });
355
+ }
356
+ }
357
+
358
+ // globalize only modulators that exist in all zones
359
+ const firstZone = finalZones.find(z => !z.isGlobal);
360
+ const modulators = firstZone.modulators.map(m => Modulator.copy(m));
361
+ for (const checkedModulator of modulators)
362
+ {
363
+ let existsForAllZones = true;
364
+ for (const zone of finalZones)
365
+ {
366
+ if (zone.isGlobal || !existsForAllZones)
367
+ {
368
+ continue;
369
+ }
370
+ // check if that zone has an existing modulator
371
+ const mod = zone.modulators.find(m => Modulator.isIdentical(m, checkedModulator));
372
+ if (!mod)
373
+ {
374
+ // does not exist for this zone, so it's not global.
375
+ existsForAllZones = false;
376
+ }
377
+ // exists.
378
+
379
+ }
380
+ if (existsForAllZones === true)
381
+ {
382
+ globalZone.modulators.push(Modulator.copy(checkedModulator));
383
+ // delete it from local zones.
384
+ for (const zone of finalZones)
385
+ {
386
+ const modulator = zone.modulators.find(m => Modulator.isIdentical(m, checkedModulator));
387
+ // Check if the amount is correct.
388
+ // If so, delete it since it's global.
389
+ // If not, then it will simply override global as it's identical.
390
+ if (modulator.transformAmount === checkedModulator.transformAmount)
391
+ {
392
+ zone.modulators.splice(zone.modulators.indexOf(modulator), 1);
393
+ }
394
+ }
395
+ }
396
+ }
397
+ finalZones.splice(0, 0, globalZone);
398
+ }
399
+ return finalZones;
400
+ }