spessasynth_lib 3.23.3 → 3.23.8

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 (40) hide show
  1. package/@types/midi_parser/basic_midi.d.ts +41 -21
  2. package/@types/midi_parser/midi_builder.d.ts +0 -4
  3. package/@types/midi_parser/midi_data.d.ts +38 -20
  4. package/@types/sequencer/sequencer.d.ts +2 -1
  5. package/@types/soundfont/basic_soundfont/basic_sample.d.ts +5 -0
  6. package/@types/soundfont/basic_soundfont/basic_soundfont.d.ts +8 -0
  7. package/@types/soundfont/basic_soundfont/modulator.d.ts +2 -1
  8. package/@types/soundfont/dls/articulator_converter.d.ts +9 -0
  9. package/@types/soundfont/dls/dls_sample.d.ts +0 -5
  10. package/@types/soundfont/dls/dls_sources.d.ts +5 -0
  11. package/@types/soundfont/read_sf2/samples.d.ts +0 -1
  12. package/@types/synthetizer/synth_event_handler.d.ts +5 -0
  13. package/@types/utils/byte_functions/string.d.ts +5 -0
  14. package/midi_parser/basic_midi.js +269 -109
  15. package/midi_parser/midi_builder.js +1 -106
  16. package/midi_parser/midi_data.js +140 -90
  17. package/midi_parser/midi_loader.js +2 -0
  18. package/midi_parser/rmidi_writer.js +4 -4
  19. package/package.json +1 -1
  20. package/sequencer/sequencer.js +3 -2
  21. package/soundfont/basic_soundfont/basic_sample.js +15 -6
  22. package/soundfont/basic_soundfont/basic_soundfont.js +64 -0
  23. package/soundfont/basic_soundfont/modulator.js +4 -2
  24. package/soundfont/basic_soundfont/riff_chunk.js +1 -1
  25. package/soundfont/basic_soundfont/write_dls/art2.js +17 -0
  26. package/soundfont/basic_soundfont/write_dls/ins.js +2 -2
  27. package/soundfont/basic_soundfont/write_dls/modulator_converter.js +12 -6
  28. package/soundfont/basic_soundfont/write_dls/rgn2.js +30 -25
  29. package/soundfont/basic_soundfont/write_dls/wave.js +8 -3
  30. package/soundfont/basic_soundfont/write_dls/write_dls.js +3 -2
  31. package/soundfont/basic_soundfont/write_dls/wsmp.js +2 -2
  32. package/soundfont/dls/articulator_converter.js +12 -10
  33. package/soundfont/dls/dls_sample.js +9 -8
  34. package/soundfont/dls/dls_sources.js +36 -1
  35. package/soundfont/dls/read_articulation.js +3 -15
  36. package/soundfont/dls/read_instrument.js +3 -14
  37. package/synthetizer/synth_event_handler.js +38 -1
  38. package/synthetizer/synth_soundfont_manager.js +0 -0
  39. package/synthetizer/worklet_processor.min.js +10 -10
  40. package/utils/byte_functions/string.js +9 -0
@@ -1,128 +1,288 @@
1
+ import { messageTypes } from "./midi_message.js";
2
+ import { readBytesAsUintBigEndian } from "../utils/byte_functions/big_endian.js";
3
+
1
4
  export class BasicMIDI
2
5
  {
3
- constructor()
6
+ /**
7
+ * The time division of the sequence, representing the number of ticks per beat.
8
+ * @type {number}
9
+ */
10
+ timeDivision = 0;
11
+
12
+ /**
13
+ * The duration of the sequence, in seconds.
14
+ * @type {number}
15
+ */
16
+ duration = 0;
17
+
18
+ /**
19
+ * The tempo changes in the sequence, ordered from the last change to the first.
20
+ * Each change is represented by an object with a tick position and a tempo value in beats per minute.
21
+ * @type {{ticks: number, tempo: number}[]}
22
+ */
23
+ tempoChanges = [{ ticks: 0, tempo: 120 }];
24
+
25
+ /**
26
+ * A string containing the copyright information for the MIDI sequence if detected.
27
+ * @type {string}
28
+ */
29
+ copyright = "";
30
+
31
+ /**
32
+ * The number of tracks in the MIDI sequence.
33
+ * @type {number}
34
+ */
35
+ tracksAmount = 0;
36
+
37
+ /**
38
+ * An array containing the lyrics of the sequence, stored as binary chunks (Uint8Array).
39
+ * @type {Uint8Array[]}
40
+ */
41
+ lyrics = [];
42
+
43
+ /**
44
+ * The tick position of the first note-on event in the MIDI sequence.
45
+ * @type {number}
46
+ */
47
+ firstNoteOn = 0;
48
+
49
+ /**
50
+ * The MIDI key range used in the sequence, represented by a minimum and maximum note value.
51
+ * @type {{min: number, max: number}}
52
+ */
53
+ keyRange = { min: 0, max: 127 };
54
+
55
+ /**
56
+ * The tick position of the last voice event (such as note-on, note-off, or control change) in the sequence.
57
+ * @type {number}
58
+ */
59
+ lastVoiceEventTick = 0;
60
+
61
+ /**
62
+ * An array of MIDI port numbers used by each track in the sequence.
63
+ * @type {number[]}
64
+ */
65
+ midiPorts = [0];
66
+
67
+ /**
68
+ * An array of channel offsets for each MIDI port, using the SpessaSynth method.
69
+ * @type {number[]}
70
+ */
71
+ midiPortChannelOffsets = [0];
72
+
73
+ /**
74
+ * A list of sets, where each set contains the MIDI channels used by each track in the sequence.
75
+ * @type {Set<number>[]}
76
+ */
77
+ usedChannelsOnTrack = [];
78
+
79
+ /**
80
+ * The loop points (in ticks) of the sequence, including both start and end points.
81
+ * @type {{start: number, end: number}}
82
+ */
83
+ loop = { start: 0, end: 0 };
84
+
85
+ /**
86
+ * The name of the MIDI sequence.
87
+ * @type {string}
88
+ */
89
+ midiName = "";
90
+
91
+ /**
92
+ * A boolean indicating if the sequence's name is the same as the file name.
93
+ * @type {boolean}
94
+ */
95
+ midiNameUsesFileName = false;
96
+
97
+ /**
98
+ * The file name of the MIDI sequence, if provided during parsing.
99
+ * @type {string}
100
+ */
101
+ fileName = "";
102
+
103
+ /**
104
+ * The raw, encoded MIDI name, represented as a Uint8Array.
105
+ * Useful when the MIDI file uses a different code page.
106
+ * @type {Uint8Array}
107
+ */
108
+ rawMidiName = undefined;
109
+
110
+ /**
111
+ * The embedded soundfont in the MIDI file, represented as an ArrayBuffer, if available.
112
+ * @type {ArrayBuffer|undefined}
113
+ */
114
+ embeddedSoundFont = undefined;
115
+
116
+ /**
117
+ * The format of the MIDI file, which can be 0, 1, or 2, indicating the type of the MIDI file.
118
+ * @type {number}
119
+ */
120
+ format = 0;
121
+
122
+ /**
123
+ * The RMID (Resource Interchangeable MIDI) info data, if the file is RMID formatted.
124
+ * Otherwise, this field is undefined.
125
+ * Chunk type (e.g. "INAM"): Chunk data as binary array.
126
+ * @type {Object<string, IndexedByteArray>}
127
+ */
128
+ RMIDInfo = {};
129
+
130
+ /**
131
+ * The bank offset used for RMID files.
132
+ * @type {number}
133
+ */
134
+ bankOffset = 0;
135
+
136
+ /**
137
+ * The actual track data of the MIDI file, represented as an array of tracks.
138
+ * Tracks are arrays of MidiMessage objects.
139
+ * @type {MidiMessage[][]}
140
+ */
141
+ tracks = [];
142
+
143
+ /**
144
+ * Copies a MIDI
145
+ * @param mid {BasicMIDI}
146
+ * @returns {BasicMIDI}
147
+ */
148
+ static copyFrom(mid)
4
149
  {
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 = "";
150
+ const m = new BasicMIDI();
25
151
 
26
- /**
27
- * The amount of tracks in the sequence
28
- * @type {number}
29
- */
30
- this.tracksAmount = 0;
152
+ m.midiName = mid.midiName;
153
+ m.midiNameUsesFileName = mid.midiNameUsesFileName;
154
+ m.fileName = mid.fileName;
155
+ m.timeDivision = mid.timeDivision;
156
+ m.duration = mid.duration;
157
+ m.copyright = mid.copyright;
158
+ m.tracksAmount = mid.tracksAmount;
159
+ m.firstNoteOn = mid.firstNoteOn;
160
+ m.keyRange = { ...mid.keyRange }; // Deep copy of keyRange
161
+ m.lastVoiceEventTick = mid.lastVoiceEventTick;
162
+ m.loop = { ...mid.loop }; // Deep copy of loop
163
+ m.format = mid.format;
164
+ m.bankOffset = mid.bankOffset;
31
165
 
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;
166
+ // Copying arrays
167
+ m.tempoChanges = [...mid.tempoChanges]; // Shallow copy
168
+ m.lyrics = mid.lyrics.map(arr => new Uint8Array(arr)); // Deep copy of each binary chunk
169
+ m.midiPorts = [...mid.midiPorts]; // Shallow copy
170
+ m.midiPortChannelOffsets = [...mid.midiPortChannelOffsets]; // Shallow copy
171
+ m.usedChannelsOnTrack = mid.usedChannelsOnTrack.map(set => new Set(set)); // Deep copy
172
+ m.rawMidiName = mid.rawMidiName ? new Uint8Array(mid.rawMidiName) : undefined; // Deep copy
173
+ m.embeddedSoundFont = mid.embeddedSoundFont ? mid.embeddedSoundFont.slice() : undefined; // Deep copy
43
174
 
44
- /**
45
- * The MIDI's key range
46
- * @type {{min: number, max: number}}
47
- */
48
- this.keyRange = { min: 0, max: 127 };
175
+ // Copying RMID Info object (deep copy)
176
+ m.RMIDInfo = { ...mid.RMIDInfo };
49
177
 
50
- /**
51
- * The last voice (note on, off, cc change etc.) event tick
52
- * @type {number}
53
- */
54
- this.lastVoiceEventTick = 0;
178
+ // Copying track data (deep copy of each track)
179
+ m.tracks = mid.tracks.map(track => [...track]); // Shallow copy of each track array
55
180
 
56
- /**
57
- * Midi port numbers for each track
58
- * @type {number[]}
59
- */
60
- this.midiPorts = [0];
181
+ return m;
182
+ }
183
+
184
+ /**
185
+ * Updates all internal values
186
+ */
187
+ flush()
188
+ {
61
189
 
62
- /**
63
- * Channel offsets for each port, using the SpessaSynth method
64
- * @type {number[]}
65
- */
66
- this.midiPortChannelOffsets = [0];
190
+ // find first note on
191
+ const firstNoteOns = [];
192
+ for (const t of this.tracks)
193
+ {
194
+ // sost the track by ticks
195
+ t.sort((e1, e2) => e1.ticks - e2.ticks);
196
+ const firstNoteOn = t.find(e => (e.messageStatusByte & 0xF0) === messageTypes.noteOn);
197
+ if (firstNoteOn)
198
+ {
199
+ firstNoteOns.push(firstNoteOn.ticks);
200
+ }
201
+ }
202
+ this.firstNoteOn = Math.min(...firstNoteOns);
67
203
 
204
+ // find tempo changes
205
+ // and used channels on tracks
206
+ // and midi ports
207
+ // and last voice event tick
208
+ // and loop
209
+ this.lastVoiceEventTick = 0;
210
+ this.tempoChanges = [{ ticks: 0, tempo: 120 }];
211
+ this.midiPorts = [];
212
+ this.midiPortChannelOffsets = [];
213
+ let portOffset = 0;
68
214
  /**
69
- * All channels that each track uses
70
215
  * @type {Set<number>[]}
71
216
  */
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 = "";
217
+ this.usedChannelsOnTrack = this.tracks.map(() => new Set());
218
+ this.tracks.forEach((t, trackNum) =>
219
+ {
220
+ this.midiPorts.push(-1);
221
+ t.forEach(e =>
222
+ {
223
+ // last voice event tick
224
+ if (e.messageStatusByte >= 0x80 && e.messageStatusByte < 0xF0)
225
+ {
226
+ if (e.ticks > this.lastVoiceEventTick)
227
+ {
228
+ this.lastVoiceEventTick = e.ticks;
229
+ }
230
+ }
231
+
232
+ // tempo, used channels, port
233
+ if (e.messageStatusByte === messageTypes.setTempo)
234
+ {
235
+ this.tempoChanges.push({
236
+ ticks: e.ticks,
237
+ tempo: 60000000 / readBytesAsUintBigEndian(
238
+ e.messageData,
239
+ 3
240
+ )
241
+ });
242
+ }
243
+ else if ((e.messageStatusByte & 0xF0) === messageTypes.noteOn)
244
+ {
245
+ this.usedChannelsOnTrack[trackNum].add(e.messageData[0]);
246
+ }
247
+ else if (e.messageStatusByte === messageTypes.midiPort)
248
+ {
249
+ const port = e.messageData[0];
250
+ this.midiPorts[trackNum] = port;
251
+ if (this.midiPortChannelOffsets[port] === undefined)
252
+ {
253
+ this.midiPortChannelOffsets[port] = portOffset;
254
+ portOffset += 16;
255
+ }
256
+ }
257
+ });
258
+ });
91
259
 
92
- /**
93
- * The raw, encoded MIDI name.
94
- * @type {Uint8Array}
95
- */
96
- this.rawMidiName = undefined;
260
+ this.loop = { start: this.firstNoteOn, end: this.lastVoiceEventTick };
97
261
 
98
- /**
99
- * The MIDI's embedded soundfont
100
- * @type {ArrayBuffer|undefined}
101
- */
102
- this.embeddedSoundFont = undefined;
262
+ // reverse tempo and compute duration
263
+ this.tempoChanges.reverse();
264
+ this.duration = MIDIticksToSeconds(this.lastVoiceEventTick, this);
103
265
 
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 = [];
266
+ // fix midi ports:
267
+ // midi tracks without ports will have a value of -1
268
+ // if all ports have a value of -1, set it to 0, otherwise take the first midi port and replace all -1 with it
269
+ // 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.
270
+ // 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.
271
+ let defaultP = 0;
272
+ for (let port of this.midiPorts)
273
+ {
274
+ if (port !== -1)
275
+ {
276
+ defaultP = port;
277
+ break;
278
+ }
279
+ }
280
+ this.midiPorts = this.midiPorts.map(port => port === -1 ? defaultP : port);
281
+ // add dummy port if empty
282
+ if (this.midiPortChannelOffsets.length === 0)
283
+ {
284
+ this.midiPortChannelOffsets = [0];
285
+ }
126
286
  }
127
287
  }
128
288
 
@@ -1,7 +1,6 @@
1
- import { BasicMIDI, MIDIticksToSeconds } from "./basic_midi.js";
1
+ import { BasicMIDI } from "./basic_midi.js";
2
2
  import { messageTypes, MidiMessage } from "./midi_message.js";
3
3
  import { IndexedByteArray } from "../utils/indexed_array.js";
4
- import { readBytesAsUintBigEndian } from "../utils/byte_functions/big_endian.js";
5
4
  import { SpessaSynthWarn } from "../utils/loggin.js";
6
5
 
7
6
  export class MIDIBuilder extends BasicMIDI
@@ -24,110 +23,6 @@ export class MIDIBuilder extends BasicMIDI
24
23
  this.addSetTempo(0, initialTempo);
25
24
  }
26
25
 
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
- {
63
- this.midiPorts.push(-1);
64
- t.forEach(e =>
65
- {
66
- // last voice event tick
67
- if (e.messageStatusByte >= 0x80 && e.messageStatusByte < 0xF0)
68
- {
69
- if (e.ticks > this.lastVoiceEventTick)
70
- {
71
- this.lastVoiceEventTick = e.ticks;
72
- }
73
- }
74
-
75
- // tempo, used channels, port
76
- if (e.messageStatusByte === messageTypes.setTempo)
77
- {
78
- this.tempoChanges.push({
79
- ticks: e.ticks,
80
- tempo: 60000000 / readBytesAsUintBigEndian(
81
- e.messageData,
82
- 3
83
- )
84
- });
85
- }
86
- else if ((e.messageStatusByte & 0xF0) === messageTypes.noteOn)
87
- {
88
- this.usedChannelsOnTrack[trackNum].add(e.messageData[0]);
89
- }
90
- else if (e.messageStatusByte === messageTypes.midiPort)
91
- {
92
- const port = e.messageData[0];
93
- this.midiPorts[trackNum] = port;
94
- if (this.midiPortChannelOffsets[port] === undefined)
95
- {
96
- this.midiPortChannelOffsets[port] = portOffset;
97
- portOffset += 16;
98
- }
99
- }
100
- });
101
- });
102
-
103
- this.loop = { start: this.firstNoteOn, end: this.lastVoiceEventTick };
104
-
105
- // reverse tempo and compute duration
106
- this.tempoChanges.reverse();
107
- this.duration = MIDIticksToSeconds(this.lastVoiceEventTick, this);
108
-
109
- // fix midi ports:
110
- // midi tracks without ports will have a value of -1
111
- // if all ports have a value of -1, set it to 0, otherwise take the first midi port and replace all -1 with it
112
- // 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.
113
- // 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.
114
- let defaultP = 0;
115
- for (let port of this.midiPorts)
116
- {
117
- if (port !== -1)
118
- {
119
- defaultP = port;
120
- break;
121
- }
122
- }
123
- this.midiPorts = this.midiPorts.map(port => port === -1 ? defaultP : port);
124
- // add dummy port if empty
125
- if (this.midiPortChannelOffsets.length === 0)
126
- {
127
- this.midiPortChannelOffsets = [0];
128
- }
129
- }
130
-
131
26
  /**
132
27
  * Adds a new "set tempo" message
133
28
  * @param ticks {number} the tick number of the event