spessasynth_core 1.0.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/.idea/inspectionProfiles/Project_Default.xml +10 -0
- package/.idea/jsLibraryMappings.xml +6 -0
- package/.idea/modules.xml +8 -0
- package/.idea/spessasynth_core.iml +12 -0
- package/.idea/vcs.xml +6 -0
- package/README.md +376 -0
- package/index.js +7 -0
- package/package.json +34 -0
- package/spessasynth_core/midi_parser/README.md +3 -0
- package/spessasynth_core/midi_parser/midi_loader.js +381 -0
- package/spessasynth_core/midi_parser/midi_message.js +231 -0
- package/spessasynth_core/sequencer/sequencer.js +192 -0
- package/spessasynth_core/sequencer/worklet_sequencer/play.js +221 -0
- package/spessasynth_core/sequencer/worklet_sequencer/process_event.js +138 -0
- package/spessasynth_core/sequencer/worklet_sequencer/process_tick.js +85 -0
- package/spessasynth_core/sequencer/worklet_sequencer/song_control.js +90 -0
- package/spessasynth_core/soundfont/README.md +4 -0
- package/spessasynth_core/soundfont/chunk/generators.js +205 -0
- package/spessasynth_core/soundfont/chunk/instruments.js +60 -0
- package/spessasynth_core/soundfont/chunk/modulators.js +232 -0
- package/spessasynth_core/soundfont/chunk/presets.js +264 -0
- package/spessasynth_core/soundfont/chunk/riff_chunk.js +46 -0
- package/spessasynth_core/soundfont/chunk/samples.js +250 -0
- package/spessasynth_core/soundfont/chunk/zones.js +264 -0
- package/spessasynth_core/soundfont/soundfont_parser.js +301 -0
- package/spessasynth_core/synthetizer/README.md +6 -0
- package/spessasynth_core/synthetizer/synthesizer.js +303 -0
- package/spessasynth_core/synthetizer/worklet_system/README.md +3 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/controller_control.js +285 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/data_entry.js +280 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/note_off.js +102 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/note_on.js +75 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/program_control.js +140 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/system_exclusive.js +265 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/tuning_control.js +105 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/vibrato_control.js +29 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/voice_control.js +186 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/lfo.js +23 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js +95 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js +73 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/modulator_curves.js +86 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/stereo_panner.js +76 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/unit_converter.js +66 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/volume_envelope.js +194 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js +83 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js +173 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js +105 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +313 -0
- package/spessasynth_core/utils/README.md +4 -0
- package/spessasynth_core/utils/buffer_to_wav.js +70 -0
- package/spessasynth_core/utils/byte_functions.js +141 -0
- package/spessasynth_core/utils/loggin.js +79 -0
- package/spessasynth_core/utils/other.js +49 -0
- package/spessasynth_core/utils/shiftable_array.js +26 -0
- package/spessasynth_core/utils/stbvorbis_sync.js +1877 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { dataBytesAmount, getChannel, messageTypes, MidiMessage } from './midi_message.js'
|
|
2
|
+
import {ShiftableByteArray} from "../utils/shiftable_array.js";
|
|
3
|
+
import {
|
|
4
|
+
readByte,
|
|
5
|
+
readBytesAsString,
|
|
6
|
+
readBytesAsUintBigEndian,
|
|
7
|
+
readVariableLengthQuantity
|
|
8
|
+
} from "../utils/byte_functions.js";
|
|
9
|
+
import { arrayToHexString, consoleColors, formatTitle } from '../utils/other.js'
|
|
10
|
+
import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from '../utils/loggin.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* midi_loader.js
|
|
14
|
+
* purpose: parses a midi file for the seqyencer, including things like marker or CC 2/4 loop detection, copyright detection etc.
|
|
15
|
+
*/
|
|
16
|
+
export class MIDI{
|
|
17
|
+
/**
|
|
18
|
+
* Parses a given midi file
|
|
19
|
+
* @param arrayBuffer {Buffer}
|
|
20
|
+
* @param fileName {string} optional, replaces the decoded title if empty
|
|
21
|
+
*/
|
|
22
|
+
constructor(arrayBuffer, fileName="") {
|
|
23
|
+
SpessaSynthGroupCollapsed(`%cParsing MIDI File...`, consoleColors.info);
|
|
24
|
+
|
|
25
|
+
const fileByteArray = new ShiftableByteArray(arrayBuffer);
|
|
26
|
+
const headerChunk = this.readMIDIChunk(fileByteArray);
|
|
27
|
+
if(headerChunk.type !== "MThd")
|
|
28
|
+
{
|
|
29
|
+
SpessaSynthGroupEnd();
|
|
30
|
+
throw new SyntaxError(`Invalid MIDI Header! Expected "MThd", got "${headerChunk.type}"`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if(headerChunk.size !== 6)
|
|
34
|
+
{
|
|
35
|
+
SpessaSynthGroupEnd();
|
|
36
|
+
throw new RangeError(`Invalid MIDI header chunk size! Expected 6, got ${headerChunk.size}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// format (ignore)
|
|
40
|
+
readBytesAsUintBigEndian(headerChunk.data, 2);
|
|
41
|
+
// tracks count
|
|
42
|
+
this.tracksAmount = readBytesAsUintBigEndian(headerChunk.data, 2);
|
|
43
|
+
// time division
|
|
44
|
+
this.timeDivision = readBytesAsUintBigEndian(headerChunk.data, 2);
|
|
45
|
+
|
|
46
|
+
const decoder = new TextDecoder('shift-jis');
|
|
47
|
+
|
|
48
|
+
// read the copyright
|
|
49
|
+
this.copyright = "";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Contains all the tempo changes in the file. (Ordered from last to first)
|
|
53
|
+
* @type {{
|
|
54
|
+
* ticks: number,
|
|
55
|
+
* tempo: number
|
|
56
|
+
* }[]}
|
|
57
|
+
*/
|
|
58
|
+
this.tempoChanges = [{ticks: 0, tempo: 120}];
|
|
59
|
+
|
|
60
|
+
let loopStart = null;
|
|
61
|
+
let loopEnd = null;
|
|
62
|
+
|
|
63
|
+
this.lastVoiceEventTick = 0;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Midi port numbers for each tracks
|
|
67
|
+
* @type {number[]}
|
|
68
|
+
*/
|
|
69
|
+
this.midiPorts = [];
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Read all the tracks
|
|
73
|
+
* @type {MidiMessage[][]}
|
|
74
|
+
*/
|
|
75
|
+
this.tracks = [];
|
|
76
|
+
for(let i = 0; i < this.tracksAmount; i++)
|
|
77
|
+
{
|
|
78
|
+
/**
|
|
79
|
+
* @type {MidiMessage[]}
|
|
80
|
+
*/
|
|
81
|
+
const track = [];
|
|
82
|
+
const trackChunk = this.readMIDIChunk(fileByteArray);
|
|
83
|
+
this.midiPorts.push(0)
|
|
84
|
+
|
|
85
|
+
if(trackChunk.type !== "MTrk")
|
|
86
|
+
{
|
|
87
|
+
SpessaSynthGroupEnd();
|
|
88
|
+
throw new SyntaxError(`Invalid track header! Expected "MTrk" got "${trackChunk.type}"`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* MIDI running byte
|
|
93
|
+
* @type {number}
|
|
94
|
+
*/
|
|
95
|
+
let runningByte = undefined;
|
|
96
|
+
|
|
97
|
+
let totalTicks = 0;
|
|
98
|
+
// loop until we reach the end of track
|
|
99
|
+
while(trackChunk.data.currentIndex < trackChunk.size)
|
|
100
|
+
{
|
|
101
|
+
totalTicks += readVariableLengthQuantity(trackChunk.data);
|
|
102
|
+
|
|
103
|
+
// check if the status byte is valid (IE. larger than 127)
|
|
104
|
+
const statusByteCheck = trackChunk.data[trackChunk.data.currentIndex];
|
|
105
|
+
|
|
106
|
+
let statusByte;
|
|
107
|
+
// if we have a running byte and the status byte isn't valid
|
|
108
|
+
if(runningByte !== undefined && statusByteCheck < 0x80)
|
|
109
|
+
{
|
|
110
|
+
statusByte = runningByte;
|
|
111
|
+
}
|
|
112
|
+
else if(!runningByte && statusByteCheck < 0x80)
|
|
113
|
+
{
|
|
114
|
+
// if we don't have a running byte and the status byte isn't valid, it's an error.
|
|
115
|
+
SpessaSynthGroupEnd();
|
|
116
|
+
throw new SyntaxError(`Unexpected byte with no running byte. (${statusByteCheck})`);
|
|
117
|
+
}
|
|
118
|
+
else
|
|
119
|
+
{
|
|
120
|
+
// if the status byte is valid, just use that
|
|
121
|
+
statusByte = readByte(trackChunk.data);
|
|
122
|
+
}
|
|
123
|
+
const statusByteChannel = getChannel(statusByte);
|
|
124
|
+
|
|
125
|
+
let eventDataLength;
|
|
126
|
+
|
|
127
|
+
// determine the message's length;
|
|
128
|
+
switch(statusByteChannel)
|
|
129
|
+
{
|
|
130
|
+
case -1:
|
|
131
|
+
// system common/realtime (no length)
|
|
132
|
+
eventDataLength = 0;
|
|
133
|
+
break;
|
|
134
|
+
case -2:
|
|
135
|
+
// meta (the next is the actual status byte)
|
|
136
|
+
statusByte = readByte(trackChunk.data);
|
|
137
|
+
eventDataLength = readVariableLengthQuantity(trackChunk.data);
|
|
138
|
+
break;
|
|
139
|
+
case -3:
|
|
140
|
+
// sysex
|
|
141
|
+
eventDataLength = readVariableLengthQuantity(trackChunk.data);
|
|
142
|
+
break;
|
|
143
|
+
default:
|
|
144
|
+
// voice message
|
|
145
|
+
// get the midi message length
|
|
146
|
+
if(totalTicks > this.lastVoiceEventTick)
|
|
147
|
+
{
|
|
148
|
+
this.lastVoiceEventTick = totalTicks;
|
|
149
|
+
}
|
|
150
|
+
eventDataLength = dataBytesAmount[statusByte >> 4];
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// put the event data into the array
|
|
155
|
+
const eventData = new ShiftableByteArray(eventDataLength);
|
|
156
|
+
const messageData = trackChunk.data.slice(trackChunk.data.currentIndex, trackChunk.data.currentIndex + eventDataLength);
|
|
157
|
+
trackChunk.data.currentIndex += eventDataLength;
|
|
158
|
+
eventData.set(messageData, 0);
|
|
159
|
+
|
|
160
|
+
runningByte = statusByte;
|
|
161
|
+
|
|
162
|
+
const message = new MidiMessage(totalTicks, statusByte, eventData);
|
|
163
|
+
track.push(message);
|
|
164
|
+
|
|
165
|
+
// check for tempo change
|
|
166
|
+
if(statusByte === messageTypes.setTempo)
|
|
167
|
+
{
|
|
168
|
+
this.tempoChanges.push({
|
|
169
|
+
ticks: totalTicks,
|
|
170
|
+
tempo: 60000000 / readBytesAsUintBigEndian(messageData, 3)
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
else
|
|
174
|
+
// check for loop start (Marker "start")
|
|
175
|
+
|
|
176
|
+
if(statusByte === messageTypes.marker)
|
|
177
|
+
{
|
|
178
|
+
const text = readBytesAsString(eventData, eventData.length).trim().toLowerCase();
|
|
179
|
+
switch (text)
|
|
180
|
+
{
|
|
181
|
+
default:
|
|
182
|
+
break;
|
|
183
|
+
|
|
184
|
+
case "start":
|
|
185
|
+
case "loopstart":
|
|
186
|
+
loopStart = totalTicks;
|
|
187
|
+
break;
|
|
188
|
+
|
|
189
|
+
case "loopend":
|
|
190
|
+
loopEnd = totalTicks;
|
|
191
|
+
}
|
|
192
|
+
eventData.currentIndex = 0;
|
|
193
|
+
|
|
194
|
+
}
|
|
195
|
+
else
|
|
196
|
+
// check for loop (CC 2/4)
|
|
197
|
+
if((statusByte & 0xF0) === messageTypes.controllerChange)
|
|
198
|
+
{
|
|
199
|
+
switch(eventData[0])
|
|
200
|
+
{
|
|
201
|
+
case 2:
|
|
202
|
+
case 116:
|
|
203
|
+
loopStart = totalTicks;
|
|
204
|
+
break;
|
|
205
|
+
|
|
206
|
+
case 4:
|
|
207
|
+
case 117:
|
|
208
|
+
if(loopEnd === null)
|
|
209
|
+
{
|
|
210
|
+
loopEnd = totalTicks;
|
|
211
|
+
}
|
|
212
|
+
else
|
|
213
|
+
{
|
|
214
|
+
// this controller has occured more than once, this means that it doesn't indicate the loop
|
|
215
|
+
loopEnd = 0;
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else
|
|
221
|
+
// check for midi port
|
|
222
|
+
if(statusByte === messageTypes.midiPort)
|
|
223
|
+
{
|
|
224
|
+
this.midiPorts[i] = eventData[0];
|
|
225
|
+
}
|
|
226
|
+
else
|
|
227
|
+
// check for copyright
|
|
228
|
+
if(statusByte === messageTypes.copyright)
|
|
229
|
+
{
|
|
230
|
+
this.copyright += decoder.decode(eventData) + "\n";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// check for embedded copyright (roland SC display sysex) http://www.bandtrax.com.au/sysex.htm
|
|
234
|
+
if(statusByte === messageTypes.systemExclusive)
|
|
235
|
+
{
|
|
236
|
+
// header goes like this: 41 10 45 12 10 00 00
|
|
237
|
+
if(arrayToHexString(messageData.slice(0, 7)).trim() === "41 10 45 12 10 00 00")
|
|
238
|
+
{
|
|
239
|
+
const decoded = decoder.decode(messageData.slice(7, messageData.length - 3)) + "\n";
|
|
240
|
+
this.copyright += decoded;
|
|
241
|
+
SpessaSynthInfo(`%cDecoded Roland SC message! %c${decoded}`,
|
|
242
|
+
consoleColors.recognized,
|
|
243
|
+
consoleColors.value)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
this.tracks.push(track);
|
|
248
|
+
SpessaSynthInfo(`%cParsed %c${this.tracks.length}%c / %c${this.tracksAmount}`,
|
|
249
|
+
consoleColors.info,
|
|
250
|
+
consoleColors.value,
|
|
251
|
+
consoleColors.info,
|
|
252
|
+
consoleColors.value);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
//this.lastVoiceEventTick = Math.max(...this.tracks.map(track =>
|
|
256
|
+
//track[track.length - 1].ticks));
|
|
257
|
+
const firstNoteOns = [];
|
|
258
|
+
for(const t of this.tracks)
|
|
259
|
+
{
|
|
260
|
+
const firstNoteOn = t.find(e => (e.messageStatusByte & 0xF0) === messageTypes.noteOn);
|
|
261
|
+
if(firstNoteOn)
|
|
262
|
+
{
|
|
263
|
+
firstNoteOns.push(firstNoteOn.ticks);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
this.firstNoteOn = Math.min(...firstNoteOns);
|
|
267
|
+
|
|
268
|
+
SpessaSynthInfo(`%cMIDI file parsed. Total tick time: %c${this.lastVoiceEventTick}`,
|
|
269
|
+
consoleColors.info,
|
|
270
|
+
consoleColors.recognized);
|
|
271
|
+
SpessaSynthGroupEnd();
|
|
272
|
+
|
|
273
|
+
if(loopStart !== null && loopEnd === null)
|
|
274
|
+
{
|
|
275
|
+
// not a loop
|
|
276
|
+
loopStart = this.firstNoteOn;
|
|
277
|
+
loopEnd = this.lastVoiceEventTick;
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
if (loopStart === null) {
|
|
281
|
+
loopStart = this.firstNoteOn;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (loopEnd === null || loopEnd === 0) {
|
|
285
|
+
loopEnd = this.lastVoiceEventTick;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
*
|
|
291
|
+
* @type {{start: number, end: number}}
|
|
292
|
+
*/
|
|
293
|
+
this.loop = {start: loopStart, end: loopEnd};
|
|
294
|
+
|
|
295
|
+
// get track name
|
|
296
|
+
this.midiName = "";
|
|
297
|
+
|
|
298
|
+
// midi name
|
|
299
|
+
if(this.tracks.length > 1)
|
|
300
|
+
{
|
|
301
|
+
// if more than 1 track and the first track has no notes, just find the first trackName in the first track
|
|
302
|
+
if(this.tracks[0].find(
|
|
303
|
+
message => message.messageStatusByte >= messageTypes.noteOn
|
|
304
|
+
&&
|
|
305
|
+
message.messageStatusByte < messageTypes.systemExclusive
|
|
306
|
+
) === undefined)
|
|
307
|
+
{
|
|
308
|
+
let name = this.tracks[0].find(message => message.messageStatusByte === messageTypes.trackName);
|
|
309
|
+
if(name)
|
|
310
|
+
{
|
|
311
|
+
this.midiName = decoder.decode(name.messageData);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else
|
|
316
|
+
{
|
|
317
|
+
// if only 1 track, find the first "track name" event
|
|
318
|
+
let name = this.tracks[0].find(message => message.messageStatusByte === messageTypes.trackName);
|
|
319
|
+
if(name)
|
|
320
|
+
{
|
|
321
|
+
this.midiName = decoder.decode(name.messageData);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
this.fileName = fileName;
|
|
326
|
+
|
|
327
|
+
// if midiName is "", use the file name
|
|
328
|
+
if(this.midiName.trim().length === 0 && fileName.length > 0)
|
|
329
|
+
{
|
|
330
|
+
this.midiName = formatTitle(fileName);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// reverse the tempo changes
|
|
334
|
+
this.tempoChanges.reverse();
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* The total playback time, in seconds
|
|
338
|
+
* @type {number}
|
|
339
|
+
*/
|
|
340
|
+
this.duration = this._ticksToSeconds(this.lastVoiceEventTick);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* @param fileByteArray {ShiftableByteArray}
|
|
345
|
+
* @returns {{type: string, size: number, data: ShiftableByteArray}}
|
|
346
|
+
*/
|
|
347
|
+
readMIDIChunk(fileByteArray)
|
|
348
|
+
{
|
|
349
|
+
const chunk = {};
|
|
350
|
+
// type
|
|
351
|
+
chunk.type = readBytesAsString(fileByteArray, 4);
|
|
352
|
+
// size
|
|
353
|
+
chunk.size = readBytesAsUintBigEndian(fileByteArray, 4);
|
|
354
|
+
// data
|
|
355
|
+
chunk.data = new ShiftableByteArray(chunk.size);
|
|
356
|
+
const dataSlice = fileByteArray.slice(fileByteArray.currentIndex, fileByteArray.currentIndex + chunk.size);
|
|
357
|
+
chunk.data.set(dataSlice, 0);
|
|
358
|
+
fileByteArray.currentIndex += chunk.size;
|
|
359
|
+
return chunk;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Coverts ticks to time in seconds
|
|
365
|
+
* @param ticks {number}
|
|
366
|
+
* @returns {number}
|
|
367
|
+
* @private
|
|
368
|
+
*/
|
|
369
|
+
_ticksToSeconds(ticks)
|
|
370
|
+
{
|
|
371
|
+
if (ticks <= 0) {
|
|
372
|
+
return 0;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// find the last tempo change that has occured
|
|
376
|
+
let tempo = this.tempoChanges.find(v => v.ticks < ticks);
|
|
377
|
+
|
|
378
|
+
let timeSinceLastTempo = ticks - tempo.ticks;
|
|
379
|
+
return this._ticksToSeconds(ticks - timeSinceLastTempo) + (timeSinceLastTempo * 60) / (tempo.tempo * this.timeDivision);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import {ShiftableByteArray} from "../utils/shiftable_array.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* midi_message.js
|
|
5
|
+
* purpose: contains enums for midi events and controllers and functions to parse them
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export class MidiMessage
|
|
9
|
+
{
|
|
10
|
+
/**
|
|
11
|
+
* @param ticks {number}
|
|
12
|
+
* @param byte {number} the message status byte
|
|
13
|
+
* @param data {ShiftableByteArray}
|
|
14
|
+
*/
|
|
15
|
+
constructor(ticks, byte, data) {
|
|
16
|
+
// absolute ticks from the start
|
|
17
|
+
this.ticks = ticks;
|
|
18
|
+
// message status byte (for meta it's the second byte)
|
|
19
|
+
this.messageStatusByte = byte;
|
|
20
|
+
this.messageData = data;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Gets the status byte's channel
|
|
26
|
+
* @param statusByte
|
|
27
|
+
* @returns {number} channel is -1 for system messages -2 for meta and -3 for sysex
|
|
28
|
+
*/
|
|
29
|
+
export function getChannel(statusByte) {
|
|
30
|
+
const eventType = statusByte & 0xF0;
|
|
31
|
+
const channel = statusByte & 0x0F;
|
|
32
|
+
|
|
33
|
+
let resultChannel = channel;
|
|
34
|
+
|
|
35
|
+
switch (eventType) {
|
|
36
|
+
// midi (and meta and sysex headers)
|
|
37
|
+
case 0x80:
|
|
38
|
+
case 0x90:
|
|
39
|
+
case 0xA0:
|
|
40
|
+
case 0xB0:
|
|
41
|
+
case 0xC0:
|
|
42
|
+
case 0xD0:
|
|
43
|
+
case 0xE0:
|
|
44
|
+
break;
|
|
45
|
+
|
|
46
|
+
case 0xF0:
|
|
47
|
+
switch (channel) {
|
|
48
|
+
case 0x0:
|
|
49
|
+
resultChannel = -3;
|
|
50
|
+
break;
|
|
51
|
+
|
|
52
|
+
case 0x1:
|
|
53
|
+
case 0x2:
|
|
54
|
+
case 0x3:
|
|
55
|
+
case 0x4:
|
|
56
|
+
case 0x5:
|
|
57
|
+
case 0x6:
|
|
58
|
+
case 0x7:
|
|
59
|
+
case 0x8:
|
|
60
|
+
case 0x9:
|
|
61
|
+
case 0xA:
|
|
62
|
+
case 0xB:
|
|
63
|
+
case 0xC:
|
|
64
|
+
case 0xD:
|
|
65
|
+
case 0xE:
|
|
66
|
+
resultChannel = -1;
|
|
67
|
+
break;
|
|
68
|
+
|
|
69
|
+
case 0xF:
|
|
70
|
+
resultChannel = -2;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
|
|
75
|
+
default:
|
|
76
|
+
resultChannel = -1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return resultChannel;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// all the midi statuses dictionary
|
|
83
|
+
export const messageTypes = {
|
|
84
|
+
noteOff: 0x80,
|
|
85
|
+
noteOn: 0x90,
|
|
86
|
+
noteAftertouch: 0xA0,
|
|
87
|
+
controllerChange: 0xB0,
|
|
88
|
+
programChange: 0xC0,
|
|
89
|
+
channelAftertouch: 0xD0,
|
|
90
|
+
pitchBend: 0xE0,
|
|
91
|
+
systemExclusive: 0xF0,
|
|
92
|
+
timecode: 0xF1,
|
|
93
|
+
songPosition: 0xF2,
|
|
94
|
+
songSelect: 0xF3,
|
|
95
|
+
tuneRequest: 0xF6,
|
|
96
|
+
clock: 0xF8,
|
|
97
|
+
start: 0xFA,
|
|
98
|
+
continue: 0xFB,
|
|
99
|
+
stop: 0xFC,
|
|
100
|
+
activeSensing: 0xFE,
|
|
101
|
+
reset: 0xFF,
|
|
102
|
+
sequenceNumber: 0x00,
|
|
103
|
+
text: 0x01,
|
|
104
|
+
copyright: 0x02,
|
|
105
|
+
trackName: 0x03,
|
|
106
|
+
instrumentName: 0x04,
|
|
107
|
+
lyric: 0x05,
|
|
108
|
+
marker: 0x06,
|
|
109
|
+
cuePoint: 0x07,
|
|
110
|
+
midiChannelPrefix: 0x20,
|
|
111
|
+
midiPort: 0x21,
|
|
112
|
+
endOfTrack: 0x2F,
|
|
113
|
+
setTempo: 0x51,
|
|
114
|
+
smpteOffset: 0x54,
|
|
115
|
+
timeSignature: 0x58,
|
|
116
|
+
keySignature: 0x59,
|
|
117
|
+
sequenceSpecific: 0x7F
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Gets the event's status and channel from the status byte
|
|
123
|
+
* @param statusByte {number} the status byte
|
|
124
|
+
* @returns {{channel: number, status: number}} channel will be -1 for sysex and meta
|
|
125
|
+
*/
|
|
126
|
+
export function getEvent(statusByte) {
|
|
127
|
+
const status = statusByte & 0xF0;
|
|
128
|
+
const channel = statusByte & 0x0F;
|
|
129
|
+
|
|
130
|
+
let eventChannel = -1;
|
|
131
|
+
let eventStatus = statusByte;
|
|
132
|
+
|
|
133
|
+
if (status >= 0x80 && status <= 0xE0) {
|
|
134
|
+
eventChannel = channel;
|
|
135
|
+
eventStatus = status;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
status: eventStatus,
|
|
140
|
+
channel: eventChannel
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @type {{timbreHarmonicContent: number, omniModeOn: number, polyModeOn: number, localControlOnOff: number, NRPNLsb: number, allNotesOff: number, footController: number, effects2Depth: number, lsbForControl7MainVolume: number, expressionController: number, monoModeOn: number, balance: number, effectControl1: number, effectControl2: number, modulationWheel: number, lsbForControl1ModulationWheel: number, allSoundOff: number, pan: number, effects1Depth: number, effects3Depth: number, attackTime: number, dataEntryMsb: number, portamentoControl: number, sostenutoPedal: number, lsbForControl5PortamentoTime: number, RPNLsb: number, bankSelect: number, portamentoTime: number, mainVolume: number, hold2Pedal: number, releaseTime: number, dataDecrement: number, NRPNMsb: number, legatoFootswitch: number, sustainPedal: number, portamentoOnOff: number, lsbForControl0BankSelect: number, lsbForControl13EffectControl2: number, effects5Depth: number, generalPurposeController2: number, lsbForControl6DataEntry: number, resetAllControllers: number, generalPurposeController3: number, generalPurposeController4: number, generalPurposeController5: number, softPedal: number, generalPurposeController6: number, lsbForControl4FootController: number, lsbForControl12EffectControl1: number, generalPurposeController7: number, generalPurposeController8: number, effects4Depth: number, lsbForControl8Balance: number, soundController9: number, soundVariation: number, soundController8: number, soundController7: number, soundController6: number, soundController10: number, dataIncrement: number, generalPurposeController1: number, lsbForControl2BreathController: number, lsbForControl11ExpressionController: number, brightness: number, lsbForControl10Pan: number, RPNMsb: number, breathController: number, omniModeOff: number}}
|
|
147
|
+
*/
|
|
148
|
+
export const midiControllers = {
|
|
149
|
+
bankSelect: 0,
|
|
150
|
+
modulationWheel: 1,
|
|
151
|
+
breathController: 2,
|
|
152
|
+
footController: 4,
|
|
153
|
+
portamentoTime: 5,
|
|
154
|
+
dataEntryMsb: 6,
|
|
155
|
+
mainVolume: 7,
|
|
156
|
+
balance: 8,
|
|
157
|
+
pan: 10,
|
|
158
|
+
expressionController: 11,
|
|
159
|
+
effectControl1: 12,
|
|
160
|
+
effectControl2: 13,
|
|
161
|
+
generalPurposeController1: 16,
|
|
162
|
+
generalPurposeController2: 17,
|
|
163
|
+
generalPurposeController3: 18,
|
|
164
|
+
generalPurposeController4: 19,
|
|
165
|
+
lsbForControl0BankSelect: 32,
|
|
166
|
+
lsbForControl1ModulationWheel: 33,
|
|
167
|
+
lsbForControl2BreathController: 34,
|
|
168
|
+
lsbForControl4FootController: 36,
|
|
169
|
+
lsbForControl5PortamentoTime: 37,
|
|
170
|
+
lsbForControl6DataEntry: 38,
|
|
171
|
+
lsbForControl7MainVolume: 39,
|
|
172
|
+
lsbForControl8Balance: 40,
|
|
173
|
+
lsbForControl10Pan: 42,
|
|
174
|
+
lsbForControl11ExpressionController: 43,
|
|
175
|
+
lsbForControl12EffectControl1: 44,
|
|
176
|
+
lsbForControl13EffectControl2: 45,
|
|
177
|
+
sustainPedal: 64,
|
|
178
|
+
portamentoOnOff: 65,
|
|
179
|
+
sostenutoPedal: 66,
|
|
180
|
+
softPedal: 67,
|
|
181
|
+
legatoFootswitch: 68,
|
|
182
|
+
hold2Pedal: 69,
|
|
183
|
+
soundVariation: 70,
|
|
184
|
+
timbreHarmonicContent: 71,
|
|
185
|
+
releaseTime: 72,
|
|
186
|
+
attackTime: 73,
|
|
187
|
+
brightness: 74,
|
|
188
|
+
soundController6: 75,
|
|
189
|
+
soundController7: 76,
|
|
190
|
+
soundController8: 77,
|
|
191
|
+
soundController9: 78,
|
|
192
|
+
soundController10: 79,
|
|
193
|
+
generalPurposeController5: 80,
|
|
194
|
+
generalPurposeController6: 81,
|
|
195
|
+
generalPurposeController7: 82,
|
|
196
|
+
generalPurposeController8: 83,
|
|
197
|
+
portamentoControl: 84,
|
|
198
|
+
effects1Depth: 91,
|
|
199
|
+
effects2Depth: 92,
|
|
200
|
+
effects3Depth: 93,
|
|
201
|
+
effects4Depth: 94,
|
|
202
|
+
effects5Depth: 95,
|
|
203
|
+
dataIncrement: 96,
|
|
204
|
+
dataDecrement: 97,
|
|
205
|
+
NRPNLsb: 98,
|
|
206
|
+
NRPNMsb: 99,
|
|
207
|
+
RPNLsb: 100,
|
|
208
|
+
RPNMsb: 101,
|
|
209
|
+
allSoundOff: 120,
|
|
210
|
+
resetAllControllers: 121,
|
|
211
|
+
localControlOnOff: 122,
|
|
212
|
+
allNotesOff: 123,
|
|
213
|
+
omniModeOff: 124,
|
|
214
|
+
omniModeOn: 125,
|
|
215
|
+
monoModeOn: 126,
|
|
216
|
+
polyModeOn: 127
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* @type {{"11": number, "12": number, "13": number, "14": number, "8": number, "9": number, "10": number}}
|
|
222
|
+
*/
|
|
223
|
+
export const dataBytesAmount = {
|
|
224
|
+
0x8: 2, // note off
|
|
225
|
+
0x9: 2, // note on
|
|
226
|
+
0xA: 2, // note at
|
|
227
|
+
0xB: 2, // cc change
|
|
228
|
+
0xC: 1, // pg change
|
|
229
|
+
0xD: 1, // channel aftertouch
|
|
230
|
+
0xE: 2 // pitch wheel
|
|
231
|
+
};
|