spessasynth_lib 0.0.1
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/.idea/modules.xml +8 -0
- package/.idea/spessasynth_lib.iml +12 -0
- package/.idea/vcs.xml +6 -0
- package/copy_version.sh +38 -0
- package/index.js +73 -0
- package/package/@types/externals/stbvorbis_sync/stbvorbis_sync.min.d.ts +1 -0
- package/package/@types/index.d.ts +34 -0
- package/package/@types/midi_handler/midi_handler.d.ts +39 -0
- package/package/@types/midi_handler/web_midi_link.d.ts +12 -0
- package/package/@types/midi_parser/midi_data.d.ts +95 -0
- package/package/@types/midi_parser/midi_editor.d.ts +45 -0
- package/package/@types/midi_parser/midi_loader.d.ts +100 -0
- package/package/@types/midi_parser/midi_message.d.ts +154 -0
- package/package/@types/midi_parser/midi_writer.d.ts +6 -0
- package/package/@types/midi_parser/rmidi_writer.d.ts +9 -0
- package/package/@types/midi_parser/used_keys_loaded.d.ts +7 -0
- package/package/@types/sequencer/sequencer.d.ts +180 -0
- package/package/@types/sequencer/worklet_sequencer/sequencer_message.d.ts +28 -0
- package/package/@types/soundfont/read/generators.d.ts +98 -0
- package/package/@types/soundfont/read/instruments.d.ts +50 -0
- package/package/@types/soundfont/read/modulators.d.ts +73 -0
- package/package/@types/soundfont/read/presets.d.ts +87 -0
- package/package/@types/soundfont/read/riff_chunk.d.ts +31 -0
- package/package/@types/soundfont/read/samples.d.ts +134 -0
- package/package/@types/soundfont/read/zones.d.ts +141 -0
- package/package/@types/soundfont/soundfont.d.ts +76 -0
- package/package/@types/soundfont/write/ibag.d.ts +6 -0
- package/package/@types/soundfont/write/igen.d.ts +6 -0
- package/package/@types/soundfont/write/imod.d.ts +6 -0
- package/package/@types/soundfont/write/inst.d.ts +6 -0
- package/package/@types/soundfont/write/pbag.d.ts +6 -0
- package/package/@types/soundfont/write/pgen.d.ts +6 -0
- package/package/@types/soundfont/write/phdr.d.ts +6 -0
- package/package/@types/soundfont/write/pmod.d.ts +6 -0
- package/package/@types/soundfont/write/sdta.d.ts +11 -0
- package/package/@types/soundfont/write/shdr.d.ts +8 -0
- package/package/@types/soundfont/write/soundfont_trimmer.d.ts +6 -0
- package/package/@types/soundfont/write/write.d.ts +21 -0
- package/package/@types/synthetizer/audio_effects/effects_config.d.ts +29 -0
- package/package/@types/synthetizer/audio_effects/fancy_chorus.d.ts +93 -0
- package/package/@types/synthetizer/audio_effects/reverb.d.ts +7 -0
- package/package/@types/synthetizer/synth_event_handler.d.ts +161 -0
- package/package/@types/synthetizer/synthetizer.d.ts +294 -0
- package/package/@types/synthetizer/worklet_system/message_protocol/worklet_message.d.ts +89 -0
- package/package/@types/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.d.ts +134 -0
- package/package/@types/synthetizer/worklet_url.d.ts +5 -0
- package/package/@types/utils/buffer_to_wav.d.ts +8 -0
- package/package/@types/utils/byte_functions/big_endian.d.ts +13 -0
- package/package/@types/utils/byte_functions/little_endian.d.ts +35 -0
- package/package/@types/utils/byte_functions/string.d.ts +22 -0
- package/package/@types/utils/byte_functions/variable_length_quantity.d.ts +12 -0
- package/package/@types/utils/indexed_array.d.ts +21 -0
- package/package/@types/utils/loggin.d.ts +26 -0
- package/package/@types/utils/other.d.ts +32 -0
- package/package/LICENSE +26 -0
- package/package/README.md +84 -0
- package/package/externals/NOTICE +9 -0
- package/package/externals/libvorbis/@types/OggVorbisEncoder.d.ts +34 -0
- package/package/externals/libvorbis/OggVorbisEncoder.min.js +1 -0
- package/package/externals/stbvorbis_sync/@types/stbvorbis_sync.d.ts +12 -0
- package/package/externals/stbvorbis_sync/LICENSE +202 -0
- package/package/externals/stbvorbis_sync/stbvorbis_sync.min.js +1 -0
- package/package/index.js +73 -0
- package/package/midi_handler/README.md +3 -0
- package/package/midi_handler/midi_handler.js +118 -0
- package/package/midi_handler/web_midi_link.js +41 -0
- package/package/midi_parser/README.md +3 -0
- package/package/midi_parser/midi_data.js +121 -0
- package/package/midi_parser/midi_editor.js +557 -0
- package/package/midi_parser/midi_loader.js +502 -0
- package/package/midi_parser/midi_message.js +234 -0
- package/package/midi_parser/midi_writer.js +95 -0
- package/package/midi_parser/rmidi_writer.js +271 -0
- package/package/midi_parser/used_keys_loaded.js +172 -0
- package/package/package.json +43 -0
- package/package/sequencer/README.md +23 -0
- package/package/sequencer/sequencer.js +439 -0
- package/package/sequencer/worklet_sequencer/events.js +92 -0
- package/package/sequencer/worklet_sequencer/play.js +309 -0
- package/package/sequencer/worklet_sequencer/process_event.js +167 -0
- package/package/sequencer/worklet_sequencer/process_tick.js +85 -0
- package/package/sequencer/worklet_sequencer/sequencer_message.js +39 -0
- package/package/sequencer/worklet_sequencer/song_control.js +193 -0
- package/package/sequencer/worklet_sequencer/worklet_sequencer.js +218 -0
- package/package/soundfont/README.md +8 -0
- package/package/soundfont/read/generators.js +212 -0
- package/package/soundfont/read/instruments.js +125 -0
- package/package/soundfont/read/modulators.js +249 -0
- package/package/soundfont/read/presets.js +300 -0
- package/package/soundfont/read/riff_chunk.js +81 -0
- package/package/soundfont/read/samples.js +398 -0
- package/package/soundfont/read/zones.js +310 -0
- package/package/soundfont/soundfont.js +357 -0
- package/package/soundfont/write/ibag.js +39 -0
- package/package/soundfont/write/igen.js +75 -0
- package/package/soundfont/write/imod.js +46 -0
- package/package/soundfont/write/inst.js +34 -0
- package/package/soundfont/write/pbag.js +39 -0
- package/package/soundfont/write/pgen.js +77 -0
- package/package/soundfont/write/phdr.js +42 -0
- package/package/soundfont/write/pmod.js +46 -0
- package/package/soundfont/write/sdta.js +72 -0
- package/package/soundfont/write/shdr.js +54 -0
- package/package/soundfont/write/soundfont_trimmer.js +169 -0
- package/package/soundfont/write/write.js +180 -0
- package/package/synthetizer/README.md +6 -0
- package/package/synthetizer/audio_effects/effects_config.js +21 -0
- package/package/synthetizer/audio_effects/fancy_chorus.js +120 -0
- package/package/synthetizer/audio_effects/impulse_response_2.flac +0 -0
- package/package/synthetizer/audio_effects/reverb.js +24 -0
- package/package/synthetizer/synth_event_handler.js +156 -0
- package/package/synthetizer/synthetizer.js +766 -0
- package/package/synthetizer/worklet_processor.min.js +13 -0
- package/package/synthetizer/worklet_system/README.md +6 -0
- package/package/synthetizer/worklet_system/main_processor.js +363 -0
- package/package/synthetizer/worklet_system/message_protocol/handle_message.js +197 -0
- package/package/synthetizer/worklet_system/message_protocol/message_sending.js +74 -0
- package/package/synthetizer/worklet_system/message_protocol/worklet_message.js +121 -0
- package/package/synthetizer/worklet_system/minify_processor.sh +4 -0
- package/package/synthetizer/worklet_system/worklet_methods/controller_control.js +230 -0
- package/package/synthetizer/worklet_system/worklet_methods/data_entry.js +277 -0
- package/package/synthetizer/worklet_system/worklet_methods/note_off.js +109 -0
- package/package/synthetizer/worklet_system/worklet_methods/note_on.js +91 -0
- package/package/synthetizer/worklet_system/worklet_methods/program_control.js +183 -0
- package/package/synthetizer/worklet_system/worklet_methods/reset_controllers.js +177 -0
- package/package/synthetizer/worklet_system/worklet_methods/snapshot.js +129 -0
- package/package/synthetizer/worklet_system/worklet_methods/system_exclusive.js +272 -0
- package/package/synthetizer/worklet_system/worklet_methods/tuning_control.js +195 -0
- package/package/synthetizer/worklet_system/worklet_methods/vibrato_control.js +29 -0
- package/package/synthetizer/worklet_system/worklet_methods/voice_control.js +233 -0
- package/package/synthetizer/worklet_system/worklet_processor.js +9 -0
- package/package/synthetizer/worklet_system/worklet_utilities/lfo.js +23 -0
- package/package/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js +130 -0
- package/package/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js +73 -0
- package/package/synthetizer/worklet_system/worklet_utilities/modulator_curves.js +86 -0
- package/package/synthetizer/worklet_system/worklet_utilities/stereo_panner.js +81 -0
- package/package/synthetizer/worklet_system/worklet_utilities/unit_converter.js +66 -0
- package/package/synthetizer/worklet_system/worklet_utilities/volume_envelope.js +265 -0
- package/package/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js +83 -0
- package/package/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js +234 -0
- package/package/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js +116 -0
- package/package/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +272 -0
- package/package/synthetizer/worklet_url.js +5 -0
- package/package/utils/README.md +4 -0
- package/package/utils/buffer_to_wav.js +101 -0
- package/package/utils/byte_functions/big_endian.js +28 -0
- package/package/utils/byte_functions/little_endian.js +74 -0
- package/package/utils/byte_functions/string.js +97 -0
- package/package/utils/byte_functions/variable_length_quantity.js +37 -0
- package/package/utils/encode_vorbis.js +30 -0
- package/package/utils/indexed_array.js +41 -0
- package/package/utils/loggin.js +79 -0
- package/package/utils/other.js +54 -0
- package/package.json +43 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import { dataBytesAmount, getChannel, messageTypes, MidiMessage } from './midi_message.js'
|
|
2
|
+
import { IndexedByteArray } from '../utils/indexed_array.js'
|
|
3
|
+
import { arrayToHexString, consoleColors, formatTitle } from '../utils/other.js'
|
|
4
|
+
import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from '../utils/loggin.js'
|
|
5
|
+
import { readRIFFChunk } from '../soundfont/read/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
|
+
|
|
10
|
+
/**
|
|
11
|
+
* midi_loader.js
|
|
12
|
+
* purpose: parses a midi file for the seqyencer, including things like marker or CC 2/4 loop detection, copyright detection etc.
|
|
13
|
+
*/
|
|
14
|
+
class MIDI{
|
|
15
|
+
/**
|
|
16
|
+
* Parses a given midi file
|
|
17
|
+
* @param arrayBuffer {ArrayBuffer}
|
|
18
|
+
* @param fileName {string} optional, replaces the decoded title if empty
|
|
19
|
+
*/
|
|
20
|
+
constructor(arrayBuffer, fileName="") {
|
|
21
|
+
SpessaSynthGroupCollapsed(`%cParsing MIDI File...`, consoleColors.info);
|
|
22
|
+
const binaryData = new IndexedByteArray(arrayBuffer);
|
|
23
|
+
let fileByteArray;
|
|
24
|
+
|
|
25
|
+
// check for rmid
|
|
26
|
+
/**
|
|
27
|
+
* If the RMI file has an embedded sf2 in it, it will appeear here, otherwise undefined
|
|
28
|
+
* @type {ArrayBuffer}
|
|
29
|
+
*/
|
|
30
|
+
this.embeddedSoundFont = undefined;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Contains the copyright strings
|
|
34
|
+
* @type {string}
|
|
35
|
+
*/
|
|
36
|
+
this.copyright = "";
|
|
37
|
+
|
|
38
|
+
const initialString = readBytesAsString(binaryData, 4);
|
|
39
|
+
binaryData.currentIndex -= 4;
|
|
40
|
+
if(initialString === "RIFF")
|
|
41
|
+
{
|
|
42
|
+
// possibly an RMID file (https://web.archive.org/web/20110610135604/http://www.midi.org/about-midi/rp29spec(rmid).pdf)
|
|
43
|
+
// skip size
|
|
44
|
+
binaryData.currentIndex += 8;
|
|
45
|
+
const rmid = readBytesAsString(binaryData, 4, undefined, false);
|
|
46
|
+
if(rmid !== "RMID")
|
|
47
|
+
{
|
|
48
|
+
SpessaSynthGroupEnd();
|
|
49
|
+
throw new SyntaxError(`Invalid RMIDI Header! Expected "RMID", got "${rmid}"`);
|
|
50
|
+
}
|
|
51
|
+
const riff = readRIFFChunk(binaryData);
|
|
52
|
+
if(riff.header !== 'data')
|
|
53
|
+
{
|
|
54
|
+
SpessaSynthGroupEnd();
|
|
55
|
+
throw new SyntaxError(`Invalid RMIDI Chunk header! Expected "data", got "${rmid}"`);
|
|
56
|
+
}
|
|
57
|
+
// this is an rmid, load the midi into array for parsing
|
|
58
|
+
fileByteArray = riff.chunkData;
|
|
59
|
+
|
|
60
|
+
// keep loading chunks until we get sfbk
|
|
61
|
+
while(binaryData.currentIndex <= binaryData.length)
|
|
62
|
+
{
|
|
63
|
+
const startIndex = binaryData.currentIndex;
|
|
64
|
+
const currentChunk = readRIFFChunk(binaryData, true);
|
|
65
|
+
if(currentChunk.header === "RIFF")
|
|
66
|
+
{
|
|
67
|
+
const type = readBytesAsString(currentChunk.chunkData, 4);
|
|
68
|
+
if(type === "sfbk")
|
|
69
|
+
{
|
|
70
|
+
SpessaSynthInfo("%cFound embedded soundfont!", consoleColors.recognized);
|
|
71
|
+
this.embeddedSoundFont = binaryData.slice(startIndex, startIndex + currentChunk.size).buffer;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else
|
|
77
|
+
{
|
|
78
|
+
fileByteArray = binaryData;
|
|
79
|
+
}
|
|
80
|
+
const headerChunk = this.readMIDIChunk(fileByteArray);
|
|
81
|
+
if(headerChunk.type !== "MThd")
|
|
82
|
+
{
|
|
83
|
+
SpessaSynthGroupEnd();
|
|
84
|
+
throw new SyntaxError(`Invalid MIDI Header! Expected "MThd", got "${headerChunk.type}"`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if(headerChunk.size !== 6)
|
|
88
|
+
{
|
|
89
|
+
SpessaSynthGroupEnd();
|
|
90
|
+
throw new RangeError(`Invalid MIDI header chunk size! Expected 6, got ${headerChunk.size}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// format
|
|
94
|
+
this.format = readBytesAsUintBigEndian(headerChunk.data, 2);
|
|
95
|
+
// tracks count
|
|
96
|
+
this.tracksAmount = readBytesAsUintBigEndian(headerChunk.data, 2);
|
|
97
|
+
// time division
|
|
98
|
+
this.timeDivision = readBytesAsUintBigEndian(headerChunk.data, 2);
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The MIDI's key range
|
|
102
|
+
* @type {{min: number, max: number}}
|
|
103
|
+
*/
|
|
104
|
+
this.keyRange = {min: 127, max: 0};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Contains the lyrics as binary chunks
|
|
108
|
+
* @type {Uint8Array[]}
|
|
109
|
+
*/
|
|
110
|
+
this.lyrics = [];
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Contains all the tempo changes in the file. (Ordered from last to first)
|
|
114
|
+
* @type {{
|
|
115
|
+
* ticks: number,
|
|
116
|
+
* tempo: number
|
|
117
|
+
* }[]}
|
|
118
|
+
*/
|
|
119
|
+
this.tempoChanges = [{ticks: 0, tempo: 120}];
|
|
120
|
+
|
|
121
|
+
let loopStart = null;
|
|
122
|
+
let loopEnd = null;
|
|
123
|
+
|
|
124
|
+
this.lastVoiceEventTick = 0;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Midi port numbers for each tracks
|
|
128
|
+
* @type {number[]}
|
|
129
|
+
*/
|
|
130
|
+
this.midiPorts = [];
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* All channels that each track uses. Note: these channels range from 0 to 15, excluding the port offsets!
|
|
134
|
+
* @type {Set<number>[]}
|
|
135
|
+
*/
|
|
136
|
+
this.usedChannelsOnTrack = [];
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Read all the tracks
|
|
140
|
+
* @type {MidiMessage[][]}
|
|
141
|
+
*/
|
|
142
|
+
this.tracks = [];
|
|
143
|
+
for(let i = 0; i < this.tracksAmount; i++)
|
|
144
|
+
{
|
|
145
|
+
/**
|
|
146
|
+
* @type {MidiMessage[]}
|
|
147
|
+
*/
|
|
148
|
+
const track = [];
|
|
149
|
+
const trackChunk = this.readMIDIChunk(fileByteArray);
|
|
150
|
+
const usedChannels = new Set();
|
|
151
|
+
this.midiPorts.push(-1);
|
|
152
|
+
|
|
153
|
+
if(trackChunk.type !== "MTrk")
|
|
154
|
+
{
|
|
155
|
+
SpessaSynthGroupEnd();
|
|
156
|
+
throw new SyntaxError(`Invalid track header! Expected "MTrk" got "${trackChunk.type}"`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* MIDI running byte
|
|
161
|
+
* @type {number}
|
|
162
|
+
*/
|
|
163
|
+
let runningByte = undefined;
|
|
164
|
+
|
|
165
|
+
let totalTicks = 0;
|
|
166
|
+
// format 2 plays sequentially
|
|
167
|
+
if(this.format === 2 && i > 0)
|
|
168
|
+
{
|
|
169
|
+
totalTicks += this.tracks[i - 1][this.tracks[i - 1].length - 1].ticks;
|
|
170
|
+
}
|
|
171
|
+
// loop until we reach the end of track
|
|
172
|
+
while(trackChunk.data.currentIndex < trackChunk.size)
|
|
173
|
+
{
|
|
174
|
+
totalTicks += readVariableLengthQuantity(trackChunk.data);
|
|
175
|
+
|
|
176
|
+
// check if the status byte is valid (IE. larger than 127)
|
|
177
|
+
const statusByteCheck = trackChunk.data[trackChunk.data.currentIndex];
|
|
178
|
+
|
|
179
|
+
let statusByte;
|
|
180
|
+
// if we have a running byte and the status byte isn't valid
|
|
181
|
+
if(runningByte !== undefined && statusByteCheck < 0x80)
|
|
182
|
+
{
|
|
183
|
+
statusByte = runningByte;
|
|
184
|
+
}
|
|
185
|
+
else if(!runningByte && statusByteCheck < 0x80)
|
|
186
|
+
{
|
|
187
|
+
// if we don't have a running byte and the status byte isn't valid, it's an error.
|
|
188
|
+
SpessaSynthGroupEnd();
|
|
189
|
+
throw new SyntaxError(`Unexpected byte with no running byte. (${statusByteCheck})`);
|
|
190
|
+
}
|
|
191
|
+
else
|
|
192
|
+
{
|
|
193
|
+
// if the status byte is valid, just use that
|
|
194
|
+
statusByte = trackChunk.data[trackChunk.data.currentIndex++];
|
|
195
|
+
}
|
|
196
|
+
const statusByteChannel = getChannel(statusByte);
|
|
197
|
+
|
|
198
|
+
let eventDataLength;
|
|
199
|
+
|
|
200
|
+
// determine the message's length;
|
|
201
|
+
switch(statusByteChannel)
|
|
202
|
+
{
|
|
203
|
+
case -1:
|
|
204
|
+
// system common/realtime (no length)
|
|
205
|
+
eventDataLength = 0;
|
|
206
|
+
break;
|
|
207
|
+
|
|
208
|
+
case -2:
|
|
209
|
+
// meta (the next is the actual status byte)
|
|
210
|
+
statusByte = trackChunk.data[trackChunk.data.currentIndex++];
|
|
211
|
+
eventDataLength = readVariableLengthQuantity(trackChunk.data);
|
|
212
|
+
break;
|
|
213
|
+
|
|
214
|
+
case -3:
|
|
215
|
+
// sysex
|
|
216
|
+
eventDataLength = readVariableLengthQuantity(trackChunk.data);
|
|
217
|
+
break;
|
|
218
|
+
|
|
219
|
+
default:
|
|
220
|
+
// voice message
|
|
221
|
+
// get the midi message length
|
|
222
|
+
if(totalTicks > this.lastVoiceEventTick)
|
|
223
|
+
{
|
|
224
|
+
this.lastVoiceEventTick = totalTicks;
|
|
225
|
+
}
|
|
226
|
+
eventDataLength = dataBytesAmount[statusByte >> 4];
|
|
227
|
+
if((statusByte & 0xF0) === messageTypes.noteOn)
|
|
228
|
+
{
|
|
229
|
+
usedChannels.add(statusByteChannel);
|
|
230
|
+
const note = trackChunk.data[trackChunk.data.currentIndex]
|
|
231
|
+
this.keyRange.min = Math.min(this.keyRange.min, note);
|
|
232
|
+
this.keyRange.max = Math.max(this.keyRange.max, note);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// save the status byte
|
|
236
|
+
runningByte = statusByte;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// put the event data into the array
|
|
241
|
+
const eventData = new IndexedByteArray(eventDataLength);
|
|
242
|
+
const messageData = trackChunk.data.slice(trackChunk.data.currentIndex, trackChunk.data.currentIndex + eventDataLength);
|
|
243
|
+
trackChunk.data.currentIndex += eventDataLength;
|
|
244
|
+
eventData.set(messageData, 0);
|
|
245
|
+
|
|
246
|
+
const message = new MidiMessage(totalTicks, statusByte, eventData);
|
|
247
|
+
track.push(message);
|
|
248
|
+
|
|
249
|
+
switch(statusByteChannel)
|
|
250
|
+
{
|
|
251
|
+
case -2:
|
|
252
|
+
// since this is a meta message
|
|
253
|
+
switch(statusByte)
|
|
254
|
+
{
|
|
255
|
+
case messageTypes.setTempo:
|
|
256
|
+
// add the tempo change
|
|
257
|
+
this.tempoChanges.push({
|
|
258
|
+
ticks: totalTicks,
|
|
259
|
+
tempo: 60000000 / readBytesAsUintBigEndian(messageData, 3)
|
|
260
|
+
});
|
|
261
|
+
break;
|
|
262
|
+
|
|
263
|
+
case messageTypes.marker:
|
|
264
|
+
// check for loop markers
|
|
265
|
+
const text = readBytesAsString(eventData, eventData.length).trim().toLowerCase();
|
|
266
|
+
switch (text)
|
|
267
|
+
{
|
|
268
|
+
default:
|
|
269
|
+
break;
|
|
270
|
+
|
|
271
|
+
case "start":
|
|
272
|
+
case "loopstart":
|
|
273
|
+
loopStart = totalTicks;
|
|
274
|
+
break;
|
|
275
|
+
|
|
276
|
+
case "loopend":
|
|
277
|
+
loopEnd = totalTicks;
|
|
278
|
+
}
|
|
279
|
+
eventData.currentIndex = 0;
|
|
280
|
+
break;
|
|
281
|
+
|
|
282
|
+
case messageTypes.midiPort:
|
|
283
|
+
this.midiPorts[i] = eventData[0];
|
|
284
|
+
break;
|
|
285
|
+
|
|
286
|
+
case messageTypes.copyright:
|
|
287
|
+
this.copyright += readBytesAsString(eventData, eventData.length) + "\n";
|
|
288
|
+
break;
|
|
289
|
+
|
|
290
|
+
case messageTypes.lyric:
|
|
291
|
+
this.lyrics.push(eventData);
|
|
292
|
+
}
|
|
293
|
+
break;
|
|
294
|
+
|
|
295
|
+
case -3:
|
|
296
|
+
// since this is a sysex message
|
|
297
|
+
// check for embedded copyright (roland SC display sysex) http://www.bandtrax.com.au/sysex.htm
|
|
298
|
+
// header goes like this: 41 10 45 12 10 00 00
|
|
299
|
+
if(arrayToHexString(eventData.slice(0, 7)).trim() === "41 10 45 12 10 00 00")
|
|
300
|
+
{
|
|
301
|
+
/**
|
|
302
|
+
* @type {IndexedByteArray}
|
|
303
|
+
*/
|
|
304
|
+
const cutText = eventData.slice(7, messageData.length - 3);
|
|
305
|
+
const decoded = readBytesAsString(cutText, cutText.length) + "\n";
|
|
306
|
+
this.copyright += decoded;
|
|
307
|
+
SpessaSynthInfo(`%cDecoded Roland SC message! %c${decoded}`,
|
|
308
|
+
consoleColors.recognized,
|
|
309
|
+
consoleColors.value)
|
|
310
|
+
}
|
|
311
|
+
break;
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
default:
|
|
315
|
+
// since this is a voice message
|
|
316
|
+
// check for loop (CC 2/4)
|
|
317
|
+
if((statusByte & 0xF0) === messageTypes.controllerChange)
|
|
318
|
+
{
|
|
319
|
+
switch(eventData[0])
|
|
320
|
+
{
|
|
321
|
+
case 2:
|
|
322
|
+
case 116:
|
|
323
|
+
loopStart = totalTicks;
|
|
324
|
+
break;
|
|
325
|
+
|
|
326
|
+
case 4:
|
|
327
|
+
case 117:
|
|
328
|
+
if(loopEnd === null)
|
|
329
|
+
{
|
|
330
|
+
loopEnd = totalTicks;
|
|
331
|
+
}
|
|
332
|
+
else
|
|
333
|
+
{
|
|
334
|
+
// this controller has occured more than once, this means that it doesnt indicate the loop
|
|
335
|
+
loopEnd = 0;
|
|
336
|
+
}
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
this.tracks.push(track);
|
|
343
|
+
this.usedChannelsOnTrack.push(usedChannels);
|
|
344
|
+
SpessaSynthInfo(`%cParsed %c${this.tracks.length}%c / %c${this.tracksAmount}`,
|
|
345
|
+
consoleColors.info,
|
|
346
|
+
consoleColors.value,
|
|
347
|
+
consoleColors.info,
|
|
348
|
+
consoleColors.value);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const firstNoteOns = [];
|
|
352
|
+
for(const t of this.tracks)
|
|
353
|
+
{
|
|
354
|
+
const firstNoteOn = t.find(e => (e.messageStatusByte & 0xF0) === messageTypes.noteOn);
|
|
355
|
+
if(firstNoteOn)
|
|
356
|
+
{
|
|
357
|
+
firstNoteOns.push(firstNoteOn.ticks);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
this.firstNoteOn = Math.min(...firstNoteOns);
|
|
361
|
+
|
|
362
|
+
SpessaSynthInfo(`%cMIDI file parsed. Total tick time: %c${this.lastVoiceEventTick}`,
|
|
363
|
+
consoleColors.info,
|
|
364
|
+
consoleColors.recognized);
|
|
365
|
+
SpessaSynthGroupEnd();
|
|
366
|
+
|
|
367
|
+
if(loopStart !== null && loopEnd === null)
|
|
368
|
+
{
|
|
369
|
+
// not a loop
|
|
370
|
+
loopStart = this.firstNoteOn;
|
|
371
|
+
loopEnd = this.lastVoiceEventTick;
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
if (loopStart === null) {
|
|
375
|
+
loopStart = this.firstNoteOn;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (loopEnd === null || loopEnd === 0) {
|
|
379
|
+
loopEnd = this.lastVoiceEventTick;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// fix midi ports:
|
|
384
|
+
// midi tracks without ports will have a value of -1
|
|
385
|
+
// if all ports have a value of -1, set it to 0, otherwise take the first midi port and replace all -1 with it
|
|
386
|
+
// 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.
|
|
387
|
+
// 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.
|
|
388
|
+
let defaultPort = 0;
|
|
389
|
+
for(let port of this.midiPorts)
|
|
390
|
+
{
|
|
391
|
+
if(port !== -1)
|
|
392
|
+
{
|
|
393
|
+
defaultPort = port;
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
this.midiPorts = this.midiPorts.map(port => port === -1 ? defaultPort : port);
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
*
|
|
401
|
+
* @type {{start: number, end: number}}
|
|
402
|
+
*/
|
|
403
|
+
this.loop = {start: loopStart, end: loopEnd};
|
|
404
|
+
|
|
405
|
+
// get track name
|
|
406
|
+
this.midiName = "";
|
|
407
|
+
|
|
408
|
+
this.rawMidiName = new Uint8Array(0);
|
|
409
|
+
|
|
410
|
+
// midi name
|
|
411
|
+
if(this.tracks.length > 1)
|
|
412
|
+
{
|
|
413
|
+
// if more than 1 track and the first track has no notes, just find the first trackName in the first track
|
|
414
|
+
if(this.tracks[0].find(
|
|
415
|
+
message => message.messageStatusByte >= messageTypes.noteOn
|
|
416
|
+
&&
|
|
417
|
+
message.messageStatusByte < messageTypes.systemExclusive
|
|
418
|
+
) === undefined)
|
|
419
|
+
{
|
|
420
|
+
let name = this.tracks[0].find(message => message.messageStatusByte === messageTypes.trackName);
|
|
421
|
+
if(name)
|
|
422
|
+
{
|
|
423
|
+
this.rawMidiName = name.messageData;
|
|
424
|
+
this.midiName = readBytesAsString(name.messageData, name.messageData.length, undefined, false);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
else
|
|
429
|
+
{
|
|
430
|
+
// if only 1 track, find the first "track name" event
|
|
431
|
+
let name = this.tracks[0].find(message => message.messageStatusByte === messageTypes.trackName);
|
|
432
|
+
if(name)
|
|
433
|
+
{
|
|
434
|
+
this.rawMidiName = name.messageData;
|
|
435
|
+
this.midiName = readBytesAsString(name.messageData, name.messageData.length, undefined, false);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
this.fileName = fileName;
|
|
440
|
+
this.midiName = this.midiName.trim();
|
|
441
|
+
// if midiName is "", use the file name
|
|
442
|
+
if(this.midiName.length === 0)
|
|
443
|
+
{
|
|
444
|
+
this.midiName = formatTitle(fileName);
|
|
445
|
+
// encode it too
|
|
446
|
+
this.rawMidiName = new Uint8Array(this.midiName.length);
|
|
447
|
+
for(let i = 0; i < this.midiName.length; i++)
|
|
448
|
+
{
|
|
449
|
+
this.rawMidiName[i] = this.midiName.charCodeAt(i);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// reverse the tempo changes
|
|
454
|
+
this.tempoChanges.reverse();
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* The total playback time, in seconds
|
|
458
|
+
* @type {number}
|
|
459
|
+
*/
|
|
460
|
+
this.duration = this._ticksToSeconds(this.lastVoiceEventTick);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* @param fileByteArray {IndexedByteArray}
|
|
465
|
+
* @returns {{type: string, size: number, data: IndexedByteArray}}
|
|
466
|
+
*/
|
|
467
|
+
readMIDIChunk(fileByteArray)
|
|
468
|
+
{
|
|
469
|
+
const chunk = {};
|
|
470
|
+
// type
|
|
471
|
+
chunk.type = readBytesAsString(fileByteArray, 4);
|
|
472
|
+
// size
|
|
473
|
+
chunk.size = readBytesAsUintBigEndian(fileByteArray, 4);
|
|
474
|
+
// data
|
|
475
|
+
chunk.data = new IndexedByteArray(chunk.size);
|
|
476
|
+
const dataSlice = fileByteArray.slice(fileByteArray.currentIndex, fileByteArray.currentIndex + chunk.size);
|
|
477
|
+
chunk.data.set(dataSlice, 0);
|
|
478
|
+
fileByteArray.currentIndex += chunk.size;
|
|
479
|
+
return chunk;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Coverts ticks to time in seconds
|
|
485
|
+
* @param ticks {number}
|
|
486
|
+
* @returns {number}
|
|
487
|
+
* @private
|
|
488
|
+
*/
|
|
489
|
+
_ticksToSeconds(ticks)
|
|
490
|
+
{
|
|
491
|
+
if (ticks <= 0) {
|
|
492
|
+
return 0;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// find the last tempo change that has occured
|
|
496
|
+
let tempo = this.tempoChanges.find(v => v.ticks < ticks);
|
|
497
|
+
|
|
498
|
+
let timeSinceLastTempo = ticks - tempo.ticks;
|
|
499
|
+
return this._ticksToSeconds(ticks - timeSinceLastTempo) + (timeSinceLastTempo * 60) / (tempo.tempo * this.timeDivision);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
export { MIDI }
|