spessasynth_lib 3.16.4 → 3.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/@types/index.d.ts +5 -4
  2. package/@types/midi_parser/basic_midi.d.ts +125 -0
  3. package/@types/midi_parser/midi_builder.d.ts +69 -0
  4. package/@types/midi_parser/midi_data.d.ts +2 -2
  5. package/@types/midi_parser/midi_editor.d.ts +4 -4
  6. package/@types/midi_parser/midi_loader.d.ts +3 -100
  7. package/@types/midi_parser/midi_writer.d.ts +2 -2
  8. package/@types/midi_parser/rmidi_writer.d.ts +4 -3
  9. package/@types/midi_parser/used_keys_loaded.d.ts +2 -2
  10. package/@types/sequencer/sequencer.d.ts +1 -1
  11. package/@types/soundfont/basic_soundfont/basic_soundfont.d.ts +5 -4
  12. package/@types/soundfont/basic_soundfont/write_sf2/soundfont_trimmer.d.ts +2 -2
  13. package/@types/soundfont/dls/dls_preset.d.ts +11 -0
  14. package/@types/soundfont/dls/dls_soundfont.d.ts +24 -0
  15. package/@types/soundfont/dls/read_instrument.d.ts +5 -0
  16. package/@types/soundfont/dls/read_instrument_list.d.ts +5 -0
  17. package/@types/soundfont/load_soundfont.d.ts +6 -0
  18. package/@types/soundfont/soundfont.d.ts +2 -1
  19. package/@types/synthetizer/synthetizer.d.ts +2 -2
  20. package/@types/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.d.ts +2 -2
  21. package/@types/utils/byte_functions/little_endian.d.ts +1 -1
  22. package/README.md +17 -15
  23. package/index.js +6 -4
  24. package/midi_parser/basic_midi.js +146 -0
  25. package/midi_parser/midi_builder.js +281 -0
  26. package/midi_parser/midi_data.js +1 -1
  27. package/midi_parser/midi_editor.js +2 -2
  28. package/midi_parser/midi_loader.js +17 -53
  29. package/midi_parser/midi_writer.js +1 -1
  30. package/midi_parser/rmidi_writer.js +227 -246
  31. package/midi_parser/used_keys_loaded.js +1 -1
  32. package/package.json +1 -1
  33. package/sequencer/sequencer.js +1 -1
  34. package/sequencer/worklet_sequencer/song_control.js +3 -3
  35. package/sequencer/worklet_sequencer/worklet_sequencer.js +1 -1
  36. package/soundfont/basic_soundfont/basic_soundfont.js +25 -10
  37. package/soundfont/basic_soundfont/riff_chunk.js +2 -2
  38. package/soundfont/basic_soundfont/write_sf2/soundfont_trimmer.js +1 -1
  39. package/soundfont/dls/dls_preset.js +25 -0
  40. package/soundfont/dls/dls_soundfont.js +93 -0
  41. package/soundfont/dls/read_instrument.js +22 -0
  42. package/soundfont/dls/read_instrument_list.js +17 -0
  43. package/soundfont/load_soundfont.js +21 -0
  44. package/soundfont/read_sf2/instruments.js +2 -2
  45. package/soundfont/read_sf2/modulators.js +5 -5
  46. package/soundfont/read_sf2/presets.js +7 -7
  47. package/soundfont/read_sf2/samples.js +8 -8
  48. package/soundfont/read_sf2/zones.js +5 -5
  49. package/soundfont/soundfont.js +8 -3
  50. package/synthetizer/synthetizer.js +1 -1
  51. package/synthetizer/worklet_processor.min.js +7 -6
  52. package/synthetizer/worklet_system/main_processor.js +1 -2
  53. package/synthetizer/worklet_system/worklet_methods/program_control.js +12 -20
  54. package/synthetizer/worklet_system/worklet_methods/worklet_soundfont_manager/worklet_soundfont_manager.js +5 -5
  55. package/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js +1 -1
  56. package/utils/byte_functions/little_endian.js +1 -1
  57. /package/@types/{midi_handler → external_midi}/midi_handler.d.ts +0 -0
  58. /package/@types/{midi_handler → external_midi}/web_midi_link.d.ts +0 -0
  59. /package/{midi_handler → external_midi}/README.md +0 -0
  60. /package/{midi_handler → external_midi}/midi_handler.js +0 -0
  61. /package/{midi_handler → external_midi}/web_midi_link.js +0 -0
package/README.md CHANGED
@@ -36,24 +36,24 @@ document.getElementById("button").onclick = async () => {
36
36
  ### Powerful SoundFont Synthesizer
37
37
  - Suitable for both **real-time** and **offline** synthesis
38
38
  - **Excellent SoundFont support:**
39
- - **Generator Support**
40
- - **Modulator Support:** _First (to my knowledge) JavaScript SoundFont synth with that feature!_
41
- - **SoundFont3 Support:** Play compressed SoundFonts!
42
- - **Can load very large SoundFonts:** up to 4GB! _Note: Only Firefox handles this well; Chromium has a hard-coded memory limit_
43
- - **Soundfont manager:** Stack multiple soundfonts!
39
+ - **Generator Support**
40
+ - **Modulator Support:** *First (to my knowledge) JavaScript SoundFont synth with that feature!*
41
+ - **SoundFont3 Support:** Play compressed SoundFonts!
42
+ - **Experimental SF2Pack Support:** Play soundfonts compressed with BASSMIDI! (*Note: only works with vorbis compression*)
43
+ - **Can load very large SoundFonts:** up to 4GB! *Note: Only Firefox handles this well; Chromium has a hard-coded memory limit*
44
+ - **Soundfont manager:** Stack multiple soundfonts!
44
45
  - **Reverb and chorus support:** [customizable!](https://github.com/spessasus/SpessaSynth/wiki/Synthetizer-Class#effects-configuration-object)
45
46
  - **Export audio files** using [OfflineAudioContext](https://developer.mozilla.org/en-US/docs/Web/API/OfflineAudioContext)
46
47
  - **[Custom modulators for additional controllers](https://github.com/spessasus/SpessaSynth/wiki/Modulator-Class#default-modulators):** Why not?
47
48
  - **Written using AudioWorklets:**
48
- - Runs in a **separate thread** for maximum performance
49
- - Supported by all modern browsers
49
+ - Runs in a **separate thread** for maximum performance
50
+ - Supported by all modern browsers
50
51
  - **Unlimited channel count:** Your CPU is the limit!
51
52
  - **Excellent MIDI Standards Support:**
52
- - **MIDI Controller Support:** Default supported controllers [here](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#supported-controllers)
53
- - **MIDI Tuning Standard Support:** [more info here](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#midi-tuning-standard)
54
- - [Full **RPN** and limited **NRPN** support](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#supported-registered-parameters)
55
- - **MIDI Tuning Standard Support:** [more info here](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#midi-tuning-standard)
56
- - Supports some [**Roland GS** and **Yamaha XG** system exclusives](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#supported-system-exclusives)
53
+ - **MIDI Controller Support:** Default supported controllers [here](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#supported-controllers)
54
+ - **MIDI Tuning Standard Support:** [more info here](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#midi-tuning-standard)
55
+ - [Full **RPN** and limited **NRPN** support](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#supported-registered-parameters)
56
+ - Supports some [**Roland GS** and **Yamaha XG** system exclusives](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#supported-system-exclusives)
57
57
 
58
58
  - **High-performance mode:** Play Rush E! _note: may kill your browser ;)_
59
59
 
@@ -76,8 +76,9 @@ document.getElementById("button").onclick = async () => {
76
76
  - **Easy MIDI editing:** Use [helper functions](https://github.com/spessasus/SpessaSynth/wiki/Writing-MIDI-Files#modifymidi) to modify the song to your needs!
77
77
  - **Loop detection:** Automatically detects loops in MIDIs (e.g., from _Touhou Project_)
78
78
  - **First note detection:** Skip unnecessary silence at the start by jumping to the first note!
79
+ - **[Write MIDI files from scratch](https://github.com/spessasus/SpessaSynth/wiki/Creating-MIDI-Files.md)**
79
80
  - **Easy saving:** Save with just [one function!](https://github.com/spessasus/SpessaSynth/wiki/Writing-MIDI-Files#writemidifile)
80
- -
81
+
81
82
  #### Read and write [RMID files with embedded SF2 soundfonts](https://github.com/spessasus/sf2-rmidi-specification#readme)
82
83
  - **[Level 4](https://github.com/spessasus/sf2-rmidi-specification#level-4) compliance:** Reads and writes *everything!*
83
84
  - **Compression and trimming support:** Reduce a MIDI file with a 1GB soundfont to **as small as 5MB**!
@@ -85,16 +86,17 @@ document.getElementById("button").onclick = async () => {
85
86
  - **Metadata support:** Add title, artist, album name and cover and more! And of course read them too! *(In any encoding!)*
86
87
  - **Compatible with [Falcosoft Midi Player 6!](https://falcosoft.hu/softwares.html#midiplayer)**
87
88
  - **Easy saving:** [As simple as saving a MIDI file!](https://github.com/spessasus/SpessaSynth/wiki/Writing-MIDI-Files#writermidi)
88
- -
89
+
89
90
  #### Read and write SoundFont2 files
90
91
  - **Easy info access:** Just an [object of strings!](https://github.com/spessasus/SpessaSynth/wiki/SoundFont2-Class#soundfontinfo)
91
92
  - **Smart trimming:** Trim the SoundFont to only include samples used in the MIDI *(down to key and velocity!)*
92
93
  - **sf3 conversion:** Compress SoundFont2 files to SoundFont3 with variable quality!
93
94
  - **Easy saving:** Also just [one function!](https://github.com/spessasus/SpessaSynth/wiki/SoundFont2-Class#write)
94
- -
95
+
95
96
  #### Read and write SoundFont3 files
96
97
  - Same features as SoundFont2 but with now with **Ogg Vorbis compression!**
97
98
  - **Variable compression quality:** You choose between file size and quality!
98
99
  - **Compression preserving:** Avoid decompressing and recompressing uncompressed samples for minimal quality loss!
100
+
99
101
  ## License
100
102
  MIT License, except for the stbvorbis_sync.js in the `externals` folder which is licensed under the Apache-2.0 license.
package/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Import modules
2
- import { SoundFont2 } from './soundfont/soundfont.js';
2
+ import { loadSoundFont } from "./soundfont/load_soundfont.js";
3
3
  import { MIDI } from './midi_parser/midi_loader.js';
4
+ import { MIDIBuilder } from "./midi_parser/midi_builder.js";
4
5
  import { Synthetizer, VOICE_CAP, DEFAULT_PERCUSSION } from './synthetizer/synthetizer.js';
5
6
  import { Sequencer } from './sequencer/sequencer.js';
6
7
  import { IndexedByteArray } from './utils/indexed_array.js';
@@ -18,8 +19,8 @@ import {
18
19
  SpessaSynthGroup
19
20
  } from './utils/loggin.js';
20
21
  import { midiControllers, messageTypes } from './midi_parser/midi_message.js';
21
- import { MIDIDeviceHandler } from './midi_handler/midi_handler.js';
22
- import { WebMidiLinkHandler } from './midi_handler/web_midi_link.js';
22
+ import { MIDIDeviceHandler} from "./external_midi/midi_handler.js";
23
+ import { WebMidiLinkHandler} from "./external_midi/web_midi_link.js";
23
24
  import { formatTime, formatTitle, consoleColors, arrayToHexString } from './utils/other.js';
24
25
  import { readBytesAsUintBigEndian } from './utils/byte_functions/big_endian.js';
25
26
  import { NON_CC_INDEX_OFFSET } from './synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js';
@@ -38,13 +39,14 @@ export {
38
39
  VOICE_CAP,
39
40
 
40
41
  // SoundFont
41
- SoundFont2,
42
+ loadSoundFont,
42
43
  trimSoundfont,
43
44
  modulatorSources,
44
45
  encodeVorbis,
45
46
 
46
47
  // MIDI
47
48
  MIDI,
49
+ MIDIBuilder,
48
50
  IndexedByteArray,
49
51
  writeMIDIFile,
50
52
  writeRMIDI,
@@ -0,0 +1,146 @@
1
+ export class BasicMIDI
2
+ {
3
+ constructor()
4
+ {
5
+ /**
6
+ * The time division of the sequence
7
+ * @type {number}
8
+ */
9
+ this.timeDivision = 0;
10
+ /**
11
+ * The duration of the sequence, in seconds
12
+ * @type {number}
13
+ */
14
+ this.duration = 0;
15
+ /**
16
+ * The tempo changes in the sequence, ordered from last to first
17
+ * @type {{ticks: number, tempo: number}[]}
18
+ */
19
+ this.tempoChanges = [{ticks: 0, tempo: 120}];
20
+ /**
21
+ * Contains the copyright strings
22
+ * @type {string}
23
+ */
24
+ this.copyright = "";
25
+
26
+ /**
27
+ * The amount of tracks in the sequence
28
+ * @type {number}
29
+ */
30
+ this.tracksAmount = 0;
31
+
32
+ /**
33
+ * The lyrics of the sequence as binary chunks
34
+ * @type {Uint8Array[]}
35
+ */
36
+ this.lyrics = [];
37
+
38
+ /**
39
+ * First note on of the MIDI file
40
+ * @type {number}
41
+ */
42
+ this.firstNoteOn = 0;
43
+
44
+ /**
45
+ * The MIDI's key range
46
+ * @type {{min: number, max: number}}
47
+ */
48
+ this.keyRange = { min: 0, max: 127 };
49
+
50
+ /**
51
+ * The last voice (note on, off, cc change etc.) event tick
52
+ * @type {number}
53
+ */
54
+ this.lastVoiceEventTick = 0;
55
+
56
+ /**
57
+ * Midi port numbers for each track
58
+ * @type {number[]}
59
+ */
60
+ this.midiPorts = [0];
61
+
62
+ /**
63
+ * Channel offsets for each port, using the SpessaSynth method
64
+ * @type {number[]}
65
+ */
66
+ this.midiPortChannelOffsets = [0];
67
+
68
+ /**
69
+ * All channels that each track uses
70
+ * @type {Set<number>[]}
71
+ */
72
+ this.usedChannelsOnTrack = [];
73
+
74
+ /**
75
+ * The loop points (in ticks) of the sequence
76
+ * @type {{start: number, end: number}}
77
+ */
78
+ this.loop = { start: 0, end: 0 };
79
+
80
+ /**
81
+ * The sequence's name
82
+ * @type {string}
83
+ */
84
+ this.midiName = "";
85
+
86
+ /**
87
+ * The file name of the sequence, if provided in the MIDI class
88
+ * @type {string}
89
+ */
90
+ this.fileName = "";
91
+
92
+ /**
93
+ * The raw, encoded MIDI name.
94
+ * @type {Uint8Array}
95
+ */
96
+ this.rawMidiName = undefined;
97
+
98
+ /**
99
+ * The MIDI's embedded soundfont
100
+ * @type {ArrayBuffer|undefined}
101
+ */
102
+ this.embeddedSoundFont = undefined;
103
+
104
+ /**
105
+ * The MIDI file's format
106
+ * @type {number}
107
+ */
108
+ this.format = 0;
109
+
110
+ /**
111
+ * The RMID Info data if RMID, otherwise undefined
112
+ * @type {Object<string, IndexedByteArray>}
113
+ */
114
+ this.RMIDInfo = {};
115
+ /**
116
+ * The bank offset for RMIDI
117
+ * @type {number}
118
+ */
119
+ this.bankOffset = 0;
120
+
121
+ /**
122
+ * The actual track data of the MIDI file
123
+ * @type {MidiMessage[][]}
124
+ */
125
+ this.tracks = [];
126
+ }
127
+
128
+ /**
129
+ * Coverts ticks to time in seconds
130
+ * @param ticks {number}
131
+ * @returns {number}
132
+ * @protected
133
+ */
134
+ _ticksToSeconds(ticks)
135
+ {
136
+ if (ticks <= 0) {
137
+ return 0;
138
+ }
139
+
140
+ // find the last tempo change that has occured
141
+ let tempo = this.tempoChanges.find(v => v.ticks < ticks);
142
+
143
+ let timeSinceLastTempo = ticks - tempo.ticks;
144
+ return this._ticksToSeconds(ticks - timeSinceLastTempo) + (timeSinceLastTempo * 60) / (tempo.tempo * this.timeDivision);
145
+ }
146
+ }
@@ -0,0 +1,281 @@
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 { readBytesAsUintBigEndian } from '../utils/byte_functions/big_endian.js'
5
+ import { SpessaSynthWarn } from '../utils/loggin.js'
6
+
7
+ export class MIDIBuilder extends BasicMIDI
8
+ {
9
+ /**
10
+ * @param name {string} The MIDI's name
11
+ * @param timeDivision {number} the file's time division
12
+ * @param initialTempo {number} the file's initial tempo
13
+ */
14
+ constructor(name, timeDivision = 480, initialTempo = 120)
15
+ {
16
+ super();
17
+ this.timeDivision = timeDivision;
18
+ this.midiName = name;
19
+ this.encoder = new TextEncoder();
20
+ this.rawMidiName = this.encoder.encode(name);
21
+
22
+ // create the first track with the file name
23
+ this.addNewTrack(name);
24
+ this.addSetTempo(0, initialTempo);
25
+ }
26
+
27
+ /**
28
+ * Updates all internal values
29
+ */
30
+ flush()
31
+ {
32
+
33
+ // find first note on
34
+ const firstNoteOns = [];
35
+ for(const t of this.tracks)
36
+ {
37
+ // sost the track by ticks
38
+ t.sort((e1, e2) => e1.ticks - e2.ticks);
39
+ const firstNoteOn = t.find(e => (e.messageStatusByte & 0xF0) === messageTypes.noteOn);
40
+ if(firstNoteOn)
41
+ {
42
+ firstNoteOns.push(firstNoteOn.ticks);
43
+ }
44
+ }
45
+ this.firstNoteOn = Math.min(...firstNoteOns);
46
+
47
+ // find tempo changes
48
+ // and used channels on tracks
49
+ // and midi ports
50
+ // and last voice event tick
51
+ // and loop
52
+ this.lastVoiceEventTick = 0
53
+ this.tempoChanges = [{ticks: 0, tempo: 120}];
54
+ this.midiPorts = [];
55
+ this.midiPortChannelOffsets = [];
56
+ let portOffset = 0;
57
+ /**
58
+ * @type {Set<number>[]}
59
+ */
60
+ this.usedChannelsOnTrack = this.tracks.map(() => new Set());
61
+ this.tracks.forEach((t, trackNum) => {
62
+ this.midiPorts.push(-1);
63
+ t.forEach(e => {
64
+ // last voice event tick
65
+ if(e.messageStatusByte >= 0x80 && e.messageStatusByte < 0xF0)
66
+ {
67
+ if(e.ticks > this.lastVoiceEventTick)
68
+ {
69
+ this.lastVoiceEventTick = e.ticks;
70
+ }
71
+ }
72
+
73
+ // tempo, used channels, port
74
+ if(e.messageStatusByte === messageTypes.setTempo)
75
+ {
76
+ this.tempoChanges.push({
77
+ ticks: e.ticks,
78
+ tempo: 60000000 / readBytesAsUintBigEndian(e.messageData, 3)
79
+ });
80
+ }
81
+ else
82
+ if((e.messageStatusByte & 0xF0) === messageTypes.noteOn)
83
+ {
84
+ this.usedChannelsOnTrack[trackNum].add(e.messageData[0]);
85
+ }
86
+ else
87
+ if(e.messageStatusByte === messageTypes.midiPort)
88
+ {
89
+ const port = e.messageData[0];
90
+ this.midiPorts[trackNum] = port;
91
+ if(this.midiPortChannelOffsets[port] === undefined)
92
+ {
93
+ this.midiPortChannelOffsets[port] = portOffset;
94
+ portOffset += 16;
95
+ }
96
+ }
97
+ })
98
+ });
99
+
100
+ this.loop = {start: this.firstNoteOn, end: this.lastVoiceEventTick};
101
+
102
+ // reverse tempo and compute duration
103
+ this.tempoChanges.reverse();
104
+ this.duration = this._ticksToSeconds(this.lastVoiceEventTick);
105
+
106
+ // fix midi ports:
107
+ // midi tracks without ports will have a value of -1
108
+ // if all ports have a value of -1, set it to 0, otherwise take the first midi port and replace all -1 with it
109
+ // why do this? some midis (for some reason) specify all channels to port 1 or else, but leave the conductor track with no port pref.
110
+ // this spessasynth to reserve the first 16 channels for the conductor track (which doesn't play anything) and use additional 16 for the actual ports.
111
+ let defaultP = 0;
112
+ for(let port of this.midiPorts)
113
+ {
114
+ if(port !== -1)
115
+ {
116
+ defaultP = port;
117
+ break;
118
+ }
119
+ }
120
+ this.midiPorts = this.midiPorts.map(port => port === -1 ? defaultP : port);
121
+ // add dummy port if empty
122
+ if(this.midiPortChannelOffsets.length === 0)
123
+ {
124
+ this.midiPortChannelOffsets = [0];
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Adds a new "set tempo" message
130
+ * @param ticks {number} the tick number of the event
131
+ * @param tempo {number} the tempo in beats per minute (BPM)
132
+ */
133
+ addSetTempo(ticks, tempo)
134
+ {
135
+ const array = new IndexedByteArray(3);
136
+
137
+ tempo = 60000000 / tempo;
138
+
139
+ // Extract each byte in big-endian order
140
+ array[0] = (tempo >> 16) & 0xFF;
141
+ array[1] = (tempo >> 8) & 0xFF;
142
+ array[2] = tempo & 0xFF;
143
+
144
+ this.addEvent(ticks, 0, messageTypes.setTempo, array);
145
+ }
146
+
147
+ /**
148
+ * Adds a new MIDI track
149
+ * @param name {string} the new track's name
150
+ * @param port {number} the new track's port
151
+ */
152
+ addNewTrack(name, port = 0)
153
+ {
154
+ this.tracksAmount++;
155
+ if(this.tracksAmount > 1)
156
+ {
157
+ this.format = 1;
158
+ }
159
+ this.tracks.push([]);
160
+ this.tracks[this.tracksAmount - 1].push(
161
+ new MidiMessage(0, messageTypes.endOfTrack, new IndexedByteArray(0))
162
+ );
163
+ this.addEvent(0, this.tracksAmount - 1, messageTypes.trackName, this.encoder.encode(name));
164
+ this.addEvent(0, this.tracksAmount - 1, messageTypes.midiPort, [port]);
165
+ }
166
+
167
+ /**
168
+ * Adds a new MIDI Event
169
+ * @param ticks {number} the tick time of the event
170
+ * @param track {number} the track number to use
171
+ * @param event {number} the MIDI event number
172
+ * @param eventData {Uint8Array|Iterable<number>} the raw event data
173
+ */
174
+ addEvent(ticks, track, event, eventData)
175
+ {
176
+ if(!this.tracks[track])
177
+ {
178
+ throw new Error(`Track ${track} does not exist. Add it via addTrack method.`);
179
+ }
180
+ if(event === messageTypes.endOfTrack)
181
+ {
182
+ SpessaSynthWarn("The EndOfTrack is added automatically. Ignoring!");
183
+ return;
184
+ }
185
+ // remove end of track
186
+ this.tracks[track].pop();
187
+ this.tracks[track].push(new MidiMessage(
188
+ ticks,
189
+ event,
190
+ new IndexedByteArray(eventData)
191
+ ));
192
+ // add end of track
193
+ this.tracks[track].push(new MidiMessage(
194
+ ticks,
195
+ messageTypes.endOfTrack,
196
+ new IndexedByteArray(0)
197
+ ));
198
+ }
199
+
200
+ /**
201
+ * Adds a new Note On event
202
+ * @param ticks {number} the tick time of the event
203
+ * @param track {number} the track number to use
204
+ * @param channel {number} the channel to use
205
+ * @param midiNote {number} the midi note of the keypress
206
+ * @param velocity {number} the velocity of the keypress
207
+ */
208
+ addNoteOn(ticks, track, channel, midiNote, velocity)
209
+ {
210
+ channel %= 16;
211
+ midiNote %= 128;
212
+ velocity %= 128;
213
+ this.addEvent(
214
+ ticks,
215
+ track,
216
+ messageTypes.noteOn | channel,
217
+ [midiNote, velocity]
218
+ );
219
+ }
220
+
221
+ /**
222
+ * Adds a new Note Off event
223
+ * @param ticks {number} the tick time of the event
224
+ * @param track {number} the track number to use
225
+ * @param channel {number} the channel to use
226
+ * @param midiNote {number} the midi note of the key release
227
+ */
228
+ addNoteOff(ticks, track, channel, midiNote)
229
+ {
230
+ channel %= 16;
231
+ midiNote %= 128;
232
+ this.addEvent(
233
+ ticks,
234
+ track,
235
+ messageTypes.noteOff | channel,
236
+ [midiNote, 64]
237
+ );
238
+ }
239
+
240
+ /**
241
+ * Adds a new Controller Change event
242
+ * @param ticks {number} the tick time of the event
243
+ * @param track {number} the track number to use
244
+ * @param channel {number} the channel to use
245
+ * @param controllerNumber {number} the MIDI CC to use
246
+ * @param controllerValue {number} the new CC value
247
+ */
248
+ addControllerChange(ticks, track, channel, controllerNumber, controllerValue)
249
+ {
250
+ channel %= 16;
251
+ controllerNumber %= 128;
252
+ controllerValue %= 128;
253
+ this.addEvent(
254
+ ticks,
255
+ track,
256
+ messageTypes.controllerChange | channel,
257
+ [controllerNumber, controllerValue]
258
+ );
259
+ }
260
+
261
+ /**
262
+ * Adds a new Pitch Wheel event
263
+ * @param ticks {number} the tick time of the event
264
+ * @param track {number} the track to use
265
+ * @param channel {number} the channel to use
266
+ * @param MSB {number} SECOND byte of the MIDI pitchWheel message
267
+ * @param LSB {number} FIRST byte of the MIDI pitchWheel message
268
+ */
269
+ addPitchWheel(ticks, track, channel, MSB, LSB)
270
+ {
271
+ channel %= 16;
272
+ MSB %= 128;
273
+ LSB %= 128;
274
+ this.addEvent(
275
+ ticks,
276
+ track,
277
+ messageTypes.pitchBend | channel,
278
+ [LSB, MSB]
279
+ );
280
+ }
281
+ }
@@ -5,7 +5,7 @@
5
5
  export class MidiData
6
6
  {
7
7
  /**
8
- * @param midi {MIDI}
8
+ * @param midi {BasicMIDI}
9
9
  */
10
10
  constructor(midi)
11
11
  {
@@ -75,7 +75,7 @@ function getDrumChange(channel, ticks)
75
75
 
76
76
  /**
77
77
  * Allows easy editing of the file
78
- * @param midi {MIDI}
78
+ * @param midi {BasicMIDI}
79
79
  * @param desiredProgramChanges {{
80
80
  * channel: number,
81
81
  * program: number,
@@ -520,7 +520,7 @@ export function modifyMIDI(
520
520
 
521
521
  /**
522
522
  * Modifies the sequence according to the locked presets and controllers in the given snapshot
523
- * @param midi {MIDI}
523
+ * @param midi {BasicMIDI}
524
524
  * @param snapshot {SynthesizerSnapshot}
525
525
  */
526
526
  export function applySnapshotToMIDI(midi, snapshot)
@@ -6,56 +6,31 @@ import { readRIFFChunk } from '../soundfont/basic_soundfont/riff_chunk.js'
6
6
  import { readVariableLengthQuantity } from '../utils/byte_functions/variable_length_quantity.js'
7
7
  import { readBytesAsUintBigEndian } from '../utils/byte_functions/big_endian.js'
8
8
  import { readBytesAsString } from '../utils/byte_functions/string.js'
9
- import { readBytesAsUintLittleEndian } from '../utils/byte_functions/little_endian.js'
9
+ import { readLittleEndian } from '../utils/byte_functions/little_endian.js'
10
10
  import { RMIDINFOChunks } from './rmidi_writer.js'
11
+ import { BasicMIDI } from './basic_midi.js'
11
12
 
12
13
  /**
13
14
  * midi_loader.js
14
15
  * purpose: parses a midi file for the seqyencer, including things like marker or CC 2/4 loop detection, copyright detection etc.
15
16
  */
16
- class MIDI{
17
+ class MIDI extends BasicMIDI
18
+ {
17
19
  /**
18
20
  * Parses a given midi file
19
21
  * @param arrayBuffer {ArrayBuffer}
20
22
  * @param fileName {string} optional, replaces the decoded title if empty
21
23
  */
22
- constructor(arrayBuffer, fileName="") {
24
+ constructor(arrayBuffer, fileName="")
25
+ {
26
+ super();
23
27
  SpessaSynthGroupCollapsed(`%cParsing MIDI File...`, consoleColors.info);
24
28
  const binaryData = new IndexedByteArray(arrayBuffer);
25
29
  let fileByteArray;
26
30
 
27
31
  // check for rmid
28
- /**
29
- * If the RMI file has an embedded sf2 in it, it will appeear here, otherwise undefined
30
- * @type {ArrayBuffer}
31
- */
32
- this.embeddedSoundFont = undefined;
33
-
34
- /**
35
- * The RMID Info data if RMID, otherwise undefined
36
- * @type {Object<string, IndexedByteArray>}
37
- */
38
- this.RMIDInfo = undefined;
39
- /**
40
- * The bank offset for RMIDI
41
- * @type {number}
42
- */
43
- this.bankOffset = 0;
44
-
45
- /**
46
- * Contains the copyright strings
47
- * @type {string}
48
- */
49
- this.copyright = "";
50
32
  let copyrightDetected = false;
51
33
 
52
- /**
53
- * The MIDI name
54
- * @type {string}
55
- */
56
- this.midiName = "";
57
-
58
- this.rawMidiName = new Uint8Array(0);
59
34
  let nameDetected = false;
60
35
 
61
36
  const initialString = readBytesAsString(binaryData, 4);
@@ -117,10 +92,19 @@ class MIDI{
117
92
  this.midiName = readBytesAsString(this.rawMidiName, this.rawMidiName.length, undefined, false);
118
93
  nameDetected = true;
119
94
  }
95
+ // these can be used interchangeably
96
+ if(this.RMIDInfo['IALB'] && !this.RMIDInfo['IPRD'])
97
+ {
98
+ this.RMIDInfo['IPRD'] = this.RMIDInfo['IALB'];
99
+ }
100
+ if(this.RMIDInfo['PRD'] && !this.RMIDInfo['IALB'])
101
+ {
102
+ this.RMIDInfo['IALB'] = this.RMIDInfo['IPRD'];
103
+ }
120
104
  this.bankOffset = 1; // defaults to 1
121
105
  if(this.RMIDInfo[RMIDINFOChunks.bankOffset])
122
106
  {
123
- this.bankOffset = readBytesAsUintLittleEndian(this.RMIDInfo[RMIDINFOChunks.bankOffset], 2);
107
+ this.bankOffset = readLittleEndian(this.RMIDInfo[RMIDINFOChunks.bankOffset], 2);
124
108
  }
125
109
  }
126
110
  }
@@ -555,25 +539,5 @@ class MIDI{
555
539
  fileByteArray.currentIndex += chunk.size;
556
540
  return chunk;
557
541
  }
558
-
559
-
560
- /**
561
- * Coverts ticks to time in seconds
562
- * @param ticks {number}
563
- * @returns {number}
564
- * @private
565
- */
566
- _ticksToSeconds(ticks)
567
- {
568
- if (ticks <= 0) {
569
- return 0;
570
- }
571
-
572
- // find the last tempo change that has occured
573
- let tempo = this.tempoChanges.find(v => v.ticks < ticks);
574
-
575
- let timeSinceLastTempo = ticks - tempo.ticks;
576
- return this._ticksToSeconds(ticks - timeSinceLastTempo) + (timeSinceLastTempo * 60) / (tempo.tempo * this.timeDivision);
577
- }
578
542
  }
579
543
  export { MIDI }
@@ -4,7 +4,7 @@ import { writeBytesAsUintBigEndian } from '../utils/byte_functions/big_endian.js
4
4
 
5
5
  /**
6
6
  * Exports the midi as a .mid file
7
- * @param midi {MIDI}
7
+ * @param midi {BasicMIDI}
8
8
  * @returns {Uint8Array} the binary .mid file data
9
9
  */
10
10
  export function writeMIDIFile(midi)