spessasynth_lib 3.16.5 → 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.
- package/@types/index.d.ts +5 -4
- package/@types/midi_parser/basic_midi.d.ts +125 -0
- package/@types/midi_parser/midi_builder.d.ts +69 -0
- package/@types/midi_parser/midi_data.d.ts +2 -2
- package/@types/midi_parser/midi_editor.d.ts +4 -4
- package/@types/midi_parser/midi_loader.d.ts +3 -100
- package/@types/midi_parser/midi_writer.d.ts +2 -2
- package/@types/midi_parser/rmidi_writer.d.ts +3 -3
- package/@types/midi_parser/used_keys_loaded.d.ts +2 -2
- package/@types/sequencer/sequencer.d.ts +1 -1
- package/@types/soundfont/basic_soundfont/write_sf2/soundfont_trimmer.d.ts +2 -2
- package/@types/soundfont/dls/dls_preset.d.ts +11 -0
- package/@types/soundfont/dls/dls_soundfont.d.ts +24 -0
- package/@types/soundfont/dls/read_instrument.d.ts +5 -0
- package/@types/soundfont/dls/read_instrument_list.d.ts +5 -0
- package/@types/soundfont/load_soundfont.d.ts +6 -0
- package/@types/soundfont/soundfont.d.ts +2 -1
- package/@types/synthetizer/synthetizer.d.ts +2 -2
- package/@types/utils/byte_functions/little_endian.d.ts +1 -1
- package/README.md +17 -15
- package/index.js +6 -4
- package/midi_parser/basic_midi.js +146 -0
- package/midi_parser/midi_builder.js +281 -0
- package/midi_parser/midi_data.js +1 -1
- package/midi_parser/midi_editor.js +2 -2
- package/midi_parser/midi_loader.js +8 -53
- package/midi_parser/midi_writer.js +1 -1
- package/midi_parser/rmidi_writer.js +2 -2
- package/midi_parser/used_keys_loaded.js +1 -1
- package/package.json +1 -1
- package/sequencer/sequencer.js +1 -1
- package/sequencer/worklet_sequencer/song_control.js +3 -3
- package/sequencer/worklet_sequencer/worklet_sequencer.js +1 -1
- package/soundfont/basic_soundfont/riff_chunk.js +2 -2
- package/soundfont/basic_soundfont/write_sf2/soundfont_trimmer.js +1 -1
- package/soundfont/dls/dls_preset.js +25 -0
- package/soundfont/dls/dls_soundfont.js +93 -0
- package/soundfont/dls/read_instrument.js +22 -0
- package/soundfont/dls/read_instrument_list.js +17 -0
- package/soundfont/load_soundfont.js +21 -0
- package/soundfont/read_sf2/instruments.js +2 -2
- package/soundfont/read_sf2/modulators.js +5 -5
- package/soundfont/read_sf2/presets.js +7 -7
- package/soundfont/read_sf2/samples.js +8 -8
- package/soundfont/read_sf2/zones.js +5 -5
- package/soundfont/soundfont.js +8 -3
- package/synthetizer/synthetizer.js +1 -1
- package/synthetizer/worklet_processor.min.js +7 -6
- package/synthetizer/worklet_system/main_processor.js +1 -2
- package/synthetizer/worklet_system/worklet_methods/program_control.js +6 -3
- package/synthetizer/worklet_system/worklet_methods/worklet_soundfont_manager/worklet_soundfont_manager.js +5 -5
- package/utils/byte_functions/little_endian.js +1 -1
- /package/@types/{midi_handler → external_midi}/midi_handler.d.ts +0 -0
- /package/@types/{midi_handler → external_midi}/web_midi_link.d.ts +0 -0
- /package/{midi_handler → external_midi}/README.md +0 -0
- /package/{midi_handler → external_midi}/midi_handler.js +0 -0
- /package/{midi_handler → external_midi}/web_midi_link.js +0 -0
|
@@ -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
|
+
}
|
package/midi_parser/midi_data.js
CHANGED
|
@@ -75,7 +75,7 @@ function getDrumChange(channel, ticks)
|
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
77
|
* Allows easy editing of the file
|
|
78
|
-
* @param 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 {
|
|
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 {
|
|
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);
|
|
@@ -129,7 +104,7 @@ class MIDI{
|
|
|
129
104
|
this.bankOffset = 1; // defaults to 1
|
|
130
105
|
if(this.RMIDInfo[RMIDINFOChunks.bankOffset])
|
|
131
106
|
{
|
|
132
|
-
this.bankOffset =
|
|
107
|
+
this.bankOffset = readLittleEndian(this.RMIDInfo[RMIDINFOChunks.bankOffset], 2);
|
|
133
108
|
}
|
|
134
109
|
}
|
|
135
110
|
}
|
|
@@ -564,25 +539,5 @@ class MIDI{
|
|
|
564
539
|
fileByteArray.currentIndex += chunk.size;
|
|
565
540
|
return chunk;
|
|
566
541
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
/**
|
|
570
|
-
* Coverts ticks to time in seconds
|
|
571
|
-
* @param ticks {number}
|
|
572
|
-
* @returns {number}
|
|
573
|
-
* @private
|
|
574
|
-
*/
|
|
575
|
-
_ticksToSeconds(ticks)
|
|
576
|
-
{
|
|
577
|
-
if (ticks <= 0) {
|
|
578
|
-
return 0;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// find the last tempo change that has occured
|
|
582
|
-
let tempo = this.tempoChanges.find(v => v.ticks < ticks);
|
|
583
|
-
|
|
584
|
-
let timeSinceLastTempo = ticks - tempo.ticks;
|
|
585
|
-
return this._ticksToSeconds(ticks - timeSinceLastTempo) + (timeSinceLastTempo * 60) / (tempo.tempo * this.timeDivision);
|
|
586
|
-
}
|
|
587
542
|
}
|
|
588
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 {
|
|
7
|
+
* @param midi {BasicMIDI}
|
|
8
8
|
* @returns {Uint8Array} the binary .mid file data
|
|
9
9
|
*/
|
|
10
10
|
export function writeMIDIFile(midi)
|
|
@@ -47,8 +47,8 @@ const DEFAULT_COPYRIGHT = "Created using SpessaSynth";
|
|
|
47
47
|
/**
|
|
48
48
|
* Writes an RMIDI file
|
|
49
49
|
* @param soundfontBinary {Uint8Array}
|
|
50
|
-
* @param mid {
|
|
51
|
-
* @param soundfont {
|
|
50
|
+
* @param mid {BasicMIDI}
|
|
51
|
+
* @param soundfont {BasicSoundFont}
|
|
52
52
|
* @param bankOffset {number} the bank offset for RMIDI
|
|
53
53
|
* @param encoding {string} the encoding of the RMIDI info chunk
|
|
54
54
|
* @param metadata {RMIDMetadata} the metadata of the file. Optional. If provided, the encoding is forced to utf-8/
|
|
@@ -4,7 +4,7 @@ import { DEFAULT_PERCUSSION } from '../synthetizer/synthetizer.js'
|
|
|
4
4
|
import { messageTypes, midiControllers } from './midi_message.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* @param mid {
|
|
7
|
+
* @param mid {BasicMIDI}
|
|
8
8
|
* @param soundfont {{getPreset: function(number, number): BasicPreset}}
|
|
9
9
|
* @returns {Object<string, Set<string>>}
|
|
10
10
|
*/
|
package/package.json
CHANGED
package/sequencer/sequencer.js
CHANGED
|
@@ -35,7 +35,7 @@ export function assignMIDIPort(trackNum, port)
|
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Loads a new sequence
|
|
38
|
-
* @param parsedMidi {
|
|
38
|
+
* @param parsedMidi {BasicMIDI}
|
|
39
39
|
* @this {WorkletSequencer}
|
|
40
40
|
*/
|
|
41
41
|
export function loadNewSequence(parsedMidi)
|
|
@@ -49,7 +49,7 @@ export function loadNewSequence(parsedMidi)
|
|
|
49
49
|
this.oneTickToSeconds = 60 / (120 * parsedMidi.timeDivision)
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
* @type {
|
|
52
|
+
* @type {BasicMIDI}
|
|
53
53
|
*/
|
|
54
54
|
this.midiData = parsedMidi;
|
|
55
55
|
|
|
@@ -135,7 +135,7 @@ export function loadNewSongList(midiBuffers)
|
|
|
135
135
|
{
|
|
136
136
|
/**
|
|
137
137
|
* parse the MIDIs (only the array buffers, MIDI is unchanged)
|
|
138
|
-
* @type {
|
|
138
|
+
* @type {BasicMIDI[]}
|
|
139
139
|
*/
|
|
140
140
|
this.songs = midiBuffers.reduce((mids, b) => {
|
|
141
141
|
if(b.duration)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { IndexedByteArray } from '../../utils/indexed_array.js'
|
|
2
|
-
import {
|
|
2
|
+
import { readLittleEndian, writeDword } from '../../utils/byte_functions/little_endian.js'
|
|
3
3
|
import { readBytesAsString, writeStringAsBytes } from '../../utils/byte_functions/string.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -33,7 +33,7 @@ export class RiffChunk
|
|
|
33
33
|
export function readRIFFChunk(dataArray, readData = true, forceShift = false) {
|
|
34
34
|
let header = readBytesAsString(dataArray, 4)
|
|
35
35
|
|
|
36
|
-
let size =
|
|
36
|
+
let size = readLittleEndian(dataArray, 4)
|
|
37
37
|
let chunkData = undefined
|
|
38
38
|
if (readData)
|
|
39
39
|
{
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { BasicPreset } from '../basic_soundfont/basic_preset.js'
|
|
2
|
+
|
|
3
|
+
export class DLSPreset extends BasicPreset
|
|
4
|
+
{
|
|
5
|
+
/**
|
|
6
|
+
* Creates a new DLS preset
|
|
7
|
+
* @param ulBank {number} the ULONG value
|
|
8
|
+
* @param ulInstrument {number} the ULONG value
|
|
9
|
+
* @param regionsAmount {number}
|
|
10
|
+
*/
|
|
11
|
+
constructor(ulBank, ulInstrument, regionsAmount)
|
|
12
|
+
{
|
|
13
|
+
super();
|
|
14
|
+
this.regionsAmount = regionsAmount;
|
|
15
|
+
this.program = ulInstrument & 127;
|
|
16
|
+
this.bank = (ulBank >> 8) & 127;
|
|
17
|
+
const isDrums = ulInstrument & (1 << 31);
|
|
18
|
+
if(isDrums)
|
|
19
|
+
{
|
|
20
|
+
console.log("DEUMS")
|
|
21
|
+
}
|
|
22
|
+
console.log(this.bank, this.program)
|
|
23
|
+
|
|
24
|
+
}
|
|
25
|
+
}
|