spessasynth_lib 3.9.12 → 3.9.14
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/midi_parser/midi_data.d.ts +5 -0
- package/@types/midi_parser/midi_loader.d.ts +5 -0
- package/@types/sequencer/sequencer.d.ts +11 -0
- package/midi_parser/midi_data.js +7 -0
- package/midi_parser/midi_editor.js +149 -128
- package/midi_parser/midi_loader.js +19 -1
- package/midi_parser/used_keys_loaded.js +125 -90
- package/package.json +2 -5
- package/sequencer/sequencer.js +21 -0
- package/sequencer/worklet_sequencer/song_control.js +3 -3
- package/synthetizer/worklet_processor.min.js +7 -7
- package/synthetizer/worklet_system/worklet_utilities/volume_envelope.js +1 -0
|
@@ -59,6 +59,11 @@ export class MidiData {
|
|
|
59
59
|
* @type {number[]}
|
|
60
60
|
*/
|
|
61
61
|
midiPorts: number[];
|
|
62
|
+
/**
|
|
63
|
+
* Channel offsets for each port, using the SpessaSynth method
|
|
64
|
+
* @type {number[]}
|
|
65
|
+
*/
|
|
66
|
+
midiPortChannelOffsets: number[];
|
|
62
67
|
/**
|
|
63
68
|
* All channels that each track uses
|
|
64
69
|
* @type {Set<number>[]}
|
|
@@ -52,6 +52,11 @@ export class MIDI {
|
|
|
52
52
|
* @type {number[]}
|
|
53
53
|
*/
|
|
54
54
|
midiPorts: number[];
|
|
55
|
+
/**
|
|
56
|
+
* Channel offsets for each port, using the SpessaSynth method
|
|
57
|
+
* @type {number[]}
|
|
58
|
+
*/
|
|
59
|
+
midiPortChannelOffsets: number[];
|
|
55
60
|
/**
|
|
56
61
|
* All channels that each track uses. Note: these channels range from 0 to 15, excluding the port offsets!
|
|
57
62
|
* @type {Set<number>[]}
|
|
@@ -87,6 +87,12 @@ export class Sequencer {
|
|
|
87
87
|
* @param id {string} must be unique
|
|
88
88
|
*/
|
|
89
89
|
addOnSongChangeEvent(callback: (arg0: MidiData) => any, id: string): void;
|
|
90
|
+
/**
|
|
91
|
+
* Adds a new event that gets called when the song ends
|
|
92
|
+
* @param callback {function}
|
|
93
|
+
* @param id {string} must be unique
|
|
94
|
+
*/
|
|
95
|
+
addOnSongEndedEvent(callback: Function, id: string): void;
|
|
90
96
|
/**
|
|
91
97
|
* Adds a new event that gets called when the time changes
|
|
92
98
|
* @param callback {function(number)} the new time, in seconds
|
|
@@ -160,6 +166,11 @@ export class Sequencer {
|
|
|
160
166
|
* @private
|
|
161
167
|
*/
|
|
162
168
|
private onTimeChange;
|
|
169
|
+
/**
|
|
170
|
+
* @type {Object<string, function>}
|
|
171
|
+
* @private
|
|
172
|
+
*/
|
|
173
|
+
private onSongEnded;
|
|
163
174
|
}
|
|
164
175
|
/**
|
|
165
176
|
* {Object}
|
package/midi_parser/midi_data.js
CHANGED
|
@@ -62,6 +62,12 @@ export class MidiData
|
|
|
62
62
|
*/
|
|
63
63
|
this.midiPorts = midi.midiPorts;
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Channel offsets for each port, using the SpessaSynth method
|
|
67
|
+
* @type {number[]}
|
|
68
|
+
*/
|
|
69
|
+
this.midiPortChannelOffsets = midi.midiPortChannelOffsets;
|
|
70
|
+
|
|
65
71
|
/**
|
|
66
72
|
* All channels that each track uses
|
|
67
73
|
* @type {Set<number>[]}
|
|
@@ -110,6 +116,7 @@ export const DUMMY_MIDI_DATA = {
|
|
|
110
116
|
lyrics: [],
|
|
111
117
|
copyright: "",
|
|
112
118
|
midiPorts: [],
|
|
119
|
+
midiPortChannelOffsets: [],
|
|
113
120
|
tracksAmount: 0,
|
|
114
121
|
tempoChanges: [{ticks: 0, tempo: 120}],
|
|
115
122
|
fileName: "NOT_LOADED.mid",
|
|
@@ -125,8 +125,9 @@ export function modifyMIDI(
|
|
|
125
125
|
});
|
|
126
126
|
}
|
|
127
127
|
desiredChannelsToClear.forEach(c => {
|
|
128
|
-
const port = Math.floor(c / 16);
|
|
129
128
|
const channel = c % 16;
|
|
129
|
+
const offset = c - channel;
|
|
130
|
+
const port = midi.midiPortChannelOffsets.findIndex(o => o === offset);
|
|
130
131
|
clearChannelMessages(channel, port);
|
|
131
132
|
SpessaSynthInfo(`%cRemoving channel %c${c}%c!`,
|
|
132
133
|
consoleColors.info,
|
|
@@ -189,24 +190,42 @@ export function modifyMIDI(
|
|
|
189
190
|
})
|
|
190
191
|
});
|
|
191
192
|
|
|
192
|
-
|
|
193
|
+
/**
|
|
194
|
+
* @param chan {number}
|
|
195
|
+
* @param port {number}
|
|
196
|
+
* @param searchForNoteOn {boolean} search for note on if true, any voice otherwise.
|
|
197
|
+
* first note on is needed because multi port midis like to reference other ports before playing to the port we want.
|
|
198
|
+
* First voice otherwise, because MP6 doesn't like program changes after cc changes in embedded midis
|
|
199
|
+
* @return {{index: number, track: number}[]}
|
|
200
|
+
*/
|
|
201
|
+
const getFirstVoiceForChannel = (chan, port, searchForNoteOn) => {
|
|
193
202
|
return midi.tracks
|
|
194
203
|
.reduce((noteOns, track, trackNum) => {
|
|
195
204
|
if(midi.usedChannelsOnTrack[trackNum].has(chan) && midi.midiPorts[trackNum] === port)
|
|
196
205
|
{
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
event
|
|
208
|
-
|
|
209
|
-
|
|
206
|
+
let eventIndex;
|
|
207
|
+
if(searchForNoteOn)
|
|
208
|
+
{
|
|
209
|
+
eventIndex = track.findIndex(event =>
|
|
210
|
+
// event is a noteon
|
|
211
|
+
(event.messageStatusByte & 0xF0) === messageTypes.noteOn);
|
|
212
|
+
}
|
|
213
|
+
else
|
|
214
|
+
{
|
|
215
|
+
eventIndex = track.findIndex(event =>
|
|
216
|
+
// event is a voice event
|
|
217
|
+
(event.messageStatusByte > 0x80 && event.messageStatusByte < 0xF0) &&
|
|
218
|
+
// event has the channel we want
|
|
219
|
+
(event.messageStatusByte & 0xF) === chan &&
|
|
220
|
+
// event is not a controller change which resets all controllers or kills all sounds
|
|
221
|
+
(
|
|
222
|
+
(event.messageStatusByte & 0xF0) === messageTypes.controllerChange &&
|
|
223
|
+
event.messageData[0] !== midiControllers.resetAllControllers &&
|
|
224
|
+
event.messageData[0] !== midiControllers.allNotesOff &&
|
|
225
|
+
event.messageData[0] !== midiControllers.allSoundOff
|
|
226
|
+
)
|
|
227
|
+
);
|
|
228
|
+
}
|
|
210
229
|
if(eventIndex !== -1)
|
|
211
230
|
{
|
|
212
231
|
noteOns.push({
|
|
@@ -243,7 +262,8 @@ export function modifyMIDI(
|
|
|
243
262
|
desiredControllerChanges.forEach(desiredChange => {
|
|
244
263
|
const channel = desiredChange.channel;
|
|
245
264
|
const midiChannel = channel % 16;
|
|
246
|
-
const
|
|
265
|
+
const offset = channel - midiChannel;
|
|
266
|
+
const port = midi.midiPortChannelOffsets.findIndex(o => o === offset);
|
|
247
267
|
const targetValue = desiredChange.controllerValue;
|
|
248
268
|
const ccNumber = desiredChange.controllerNumber;
|
|
249
269
|
// the controller is locked. Clear all controllers
|
|
@@ -259,14 +279,14 @@ export function modifyMIDI(
|
|
|
259
279
|
/**
|
|
260
280
|
* @type {{index: number, track: number}[]}
|
|
261
281
|
*/
|
|
262
|
-
const firstNoteOnForTrack = getFirstVoiceForChannel(midiChannel, port);
|
|
282
|
+
const firstNoteOnForTrack = getFirstVoiceForChannel(midiChannel, port, offset > 0);
|
|
263
283
|
if(firstNoteOnForTrack.length === 0)
|
|
264
284
|
{
|
|
265
285
|
SpessaSynthWarn("Program change but no notes... ignoring!");
|
|
266
286
|
return;
|
|
267
287
|
}
|
|
268
288
|
const firstNoteOn = firstNoteOnForTrack.reduce((first, current) =>
|
|
269
|
-
midi.tracks[current.track][current.index].ticks < midi.tracks[first.track][first.index] ? current : first);
|
|
289
|
+
midi.tracks[current.track][current.index].ticks < midi.tracks[first.track][first.index].ticks ? current : first);
|
|
270
290
|
// prepend with controller change
|
|
271
291
|
const ccChange = getControllerChange(midiChannel, ccNumber, targetValue, midi.tracks[firstNoteOn.track][firstNoteOn.index].ticks);
|
|
272
292
|
midi.tracks[firstNoteOn.track].splice(firstNoteOn.index, 0, ccChange);
|
|
@@ -274,135 +294,136 @@ export function modifyMIDI(
|
|
|
274
294
|
|
|
275
295
|
desiredProgramChanges.forEach(change => {
|
|
276
296
|
const midiChannel = change.channel % 16;
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
297
|
+
const offset = change.channel - midiChannel;
|
|
298
|
+
const port = midi.midiPortChannelOffsets.findIndex(o => o === offset);
|
|
299
|
+
let desiredBank = change.isDrum ? 0 : change.bank;
|
|
300
|
+
const desiredProgram = change.program;
|
|
280
301
|
|
|
281
|
-
|
|
282
|
-
|
|
302
|
+
// get the program changes that are relevant for this channel (and port)
|
|
303
|
+
const thisProgramChanges = programChanges.filter(c => midi.midiPorts[c.track] === port && c.channel === midiChannel);
|
|
283
304
|
|
|
284
305
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
306
|
+
// clear bank selects
|
|
307
|
+
clearControllers(midiChannel, port, midiControllers.bankSelect);
|
|
308
|
+
clearControllers(midiChannel, port, midiControllers.lsbForControl0BankSelect);
|
|
288
309
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
310
|
+
// if drums or the program uses bank select, flag as gs
|
|
311
|
+
if((change.isDrum || desiredBank > 0) && !addedGs)
|
|
312
|
+
{
|
|
313
|
+
// make sure that GS is on
|
|
314
|
+
// GS on: F0 41 10 42 12 40 00 7F 00 41 F7
|
|
315
|
+
midi.tracks.forEach(track => {
|
|
316
|
+
for(let eventIndex = 0; eventIndex < track.length; eventIndex++)
|
|
317
|
+
{
|
|
318
|
+
const event = track[eventIndex];
|
|
319
|
+
if(event.messageStatusByte === messageTypes.systemExclusive)
|
|
296
320
|
{
|
|
297
|
-
|
|
298
|
-
|
|
321
|
+
if(
|
|
322
|
+
event.messageData[0] === 0x41 // roland
|
|
323
|
+
&& event.messageData[2] === 0x42 // GS
|
|
324
|
+
&& event.messageData[6] === 0x7F // Mode set
|
|
325
|
+
)
|
|
299
326
|
{
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
{
|
|
316
|
-
// thats a GM/2 system change, remove it!
|
|
317
|
-
SpessaSynthInfo("%cGM/2 on detected, removing!", consoleColors.info);
|
|
318
|
-
track.splice(eventIndex, 1);
|
|
319
|
-
// adjust program and bank changes
|
|
320
|
-
eventIndex--;
|
|
321
|
-
}
|
|
327
|
+
// thats a GS on, we're done here
|
|
328
|
+
addedGs = true;
|
|
329
|
+
SpessaSynthInfo("%cGS on detected!", consoleColors.recognized);
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
else if(
|
|
333
|
+
event.messageData[0] === 0x7E // non realtime
|
|
334
|
+
&& event.messageData[2] === 0x09 // gm system
|
|
335
|
+
)
|
|
336
|
+
{
|
|
337
|
+
// thats a GM/2 system change, remove it!
|
|
338
|
+
SpessaSynthInfo("%cGM/2 on detected, removing!", consoleColors.info);
|
|
339
|
+
track.splice(eventIndex, 1);
|
|
340
|
+
// adjust program and bank changes
|
|
341
|
+
eventIndex--;
|
|
322
342
|
}
|
|
323
343
|
}
|
|
324
|
-
|
|
325
|
-
});
|
|
326
|
-
if(!addedGs)
|
|
327
|
-
{
|
|
328
|
-
// gs is not on, add it on the first track at index 0 (or 1 if track name is first)
|
|
329
|
-
let index = 0;
|
|
330
|
-
if(midi.tracks[0][0].messageStatusByte === messageTypes.trackName)
|
|
331
|
-
index++;
|
|
332
|
-
midi.tracks[0].splice(index, 0, getGsOn(0));
|
|
333
|
-
SpessaSynthInfo("%cGS on not detected. Adding it.", consoleColors.info);
|
|
334
|
-
addedGs = true;
|
|
335
344
|
}
|
|
336
|
-
}
|
|
337
345
|
|
|
338
|
-
|
|
339
|
-
|
|
346
|
+
});
|
|
347
|
+
if(!addedGs)
|
|
340
348
|
{
|
|
341
|
-
|
|
349
|
+
// gs is not on, add it on the first track at index 0 (or 1 if track name is first)
|
|
350
|
+
let index = 0;
|
|
351
|
+
if(midi.tracks[0][0].messageStatusByte === messageTypes.trackName)
|
|
352
|
+
index++;
|
|
353
|
+
midi.tracks[0].splice(index, 0, getGsOn(0));
|
|
354
|
+
SpessaSynthInfo("%cGS on not detected. Adding it.", consoleColors.info);
|
|
355
|
+
addedGs = true;
|
|
342
356
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// remove all program changes
|
|
360
|
+
for(const change of thisProgramChanges)
|
|
361
|
+
{
|
|
362
|
+
midi.tracks[change.track].splice(midi.tracks[change.track].indexOf(change.message), 1);
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Find the first voice message
|
|
366
|
+
* @type {{index: number, track: number}[]}
|
|
367
|
+
*/
|
|
368
|
+
const firstVoiceForTrack = getFirstVoiceForChannel(midiChannel, port, offset > 0);
|
|
369
|
+
if(firstVoiceForTrack.length === 0)
|
|
370
|
+
{
|
|
371
|
+
SpessaSynthWarn("Program change but no notes... ignoring!");
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
// get the first voice overall
|
|
375
|
+
const firstVoice = firstVoiceForTrack.reduce((first, current) =>
|
|
376
|
+
midi.tracks[current.track][current.index].ticks < midi.tracks[first.track][first.index].ticks ? current : first);
|
|
377
|
+
// get the index and ticks
|
|
378
|
+
let firstIndex = firstVoice.index;
|
|
379
|
+
const ticks = midi.tracks[firstVoice.track][firstVoice.index].ticks;
|
|
380
|
+
|
|
381
|
+
// add drums if needed
|
|
382
|
+
if(change.isDrum)
|
|
383
|
+
{
|
|
384
|
+
// do not add gs drum change on drum channel
|
|
385
|
+
if(midiSystem === "gs" && midiChannel !== DEFAULT_PERCUSSION)
|
|
349
386
|
{
|
|
350
|
-
|
|
351
|
-
|
|
387
|
+
SpessaSynthInfo(`%cAdding GS Drum change on track %c${firstVoice.track}`,
|
|
388
|
+
consoleColors.recognized,
|
|
389
|
+
consoleColors.value
|
|
390
|
+
);
|
|
391
|
+
midi.tracks[firstVoice.track].splice(firstIndex, 0, getDrumChange(change.channel, ticks));
|
|
392
|
+
firstIndex++;
|
|
352
393
|
}
|
|
353
|
-
|
|
354
|
-
const firstVoice = firstVoiceForTrack.reduce((first, current) =>
|
|
355
|
-
midi.tracks[current.track][current.index].ticks < midi.tracks[first.track][first.index] ? current : first);
|
|
356
|
-
// get the index and ticks
|
|
357
|
-
let firstIndex = firstVoice.index;
|
|
358
|
-
const ticks = midi.tracks[firstVoice.track][firstVoice.index].ticks;
|
|
359
|
-
|
|
360
|
-
// add drums if needed
|
|
361
|
-
if(change.isDrum)
|
|
394
|
+
else if(midiSystem === "xg")
|
|
362
395
|
{
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
);
|
|
370
|
-
midi.tracks[firstVoice.track].splice(firstIndex, 0, getDrumChange(change.channel, ticks));
|
|
371
|
-
firstIndex++;
|
|
372
|
-
}
|
|
373
|
-
else if(midiSystem === "xg")
|
|
374
|
-
{
|
|
375
|
-
SpessaSynthInfo(`%cAdding XG Drum change on track %c${firstVoice.track}`,
|
|
376
|
-
consoleColors.recognized,
|
|
377
|
-
consoleColors.value
|
|
378
|
-
);
|
|
379
|
-
// system is xg. drums are on msb bank 127.
|
|
380
|
-
desiredBank = 127;
|
|
381
|
-
}
|
|
396
|
+
SpessaSynthInfo(`%cAdding XG Drum change on track %c${firstVoice.track}`,
|
|
397
|
+
consoleColors.recognized,
|
|
398
|
+
consoleColors.value
|
|
399
|
+
);
|
|
400
|
+
// system is xg. drums are on msb bank 127.
|
|
401
|
+
desiredBank = 127;
|
|
382
402
|
}
|
|
403
|
+
}
|
|
383
404
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const bankChange = getControllerChange(midiChannel, midiControllers.bankSelect, desiredBank, ticks);
|
|
392
|
-
midi.tracks[firstVoice.track].splice(firstIndex, 0, bankChange);
|
|
393
|
-
firstIndex++;
|
|
394
|
-
|
|
395
|
-
// add program change
|
|
396
|
-
const programChange = new MidiMessage(
|
|
397
|
-
ticks,
|
|
398
|
-
messageTypes.programChange | midiChannel,
|
|
399
|
-
new IndexedByteArray([
|
|
400
|
-
desiredProgram
|
|
401
|
-
])
|
|
402
|
-
);
|
|
403
|
-
midi.tracks[firstVoice.track].splice(firstIndex, 0, programChange);
|
|
405
|
+
SpessaSynthInfo(`%cSetting %c${change.channel}%c to %c${desiredBank}:${desiredProgram}%c. Track num: %c${firstVoice.track}`,
|
|
406
|
+
consoleColors.info,
|
|
407
|
+
consoleColors.recognized,
|
|
408
|
+
consoleColors.info,
|
|
409
|
+
consoleColors.recognized,
|
|
410
|
+
consoleColors.info,
|
|
411
|
+
consoleColors.recognized);
|
|
404
412
|
|
|
413
|
+
// add bank
|
|
414
|
+
const bankChange = getControllerChange(midiChannel, midiControllers.bankSelect, desiredBank, ticks);
|
|
415
|
+
midi.tracks[firstVoice.track].splice(firstIndex, 0, bankChange);
|
|
416
|
+
firstIndex++;
|
|
405
417
|
|
|
418
|
+
// add program change
|
|
419
|
+
const programChange = new MidiMessage(
|
|
420
|
+
ticks,
|
|
421
|
+
messageTypes.programChange | midiChannel,
|
|
422
|
+
new IndexedByteArray([
|
|
423
|
+
desiredProgram
|
|
424
|
+
])
|
|
425
|
+
);
|
|
426
|
+
midi.tracks[firstVoice.track].splice(firstIndex, 0, programChange);
|
|
406
427
|
});
|
|
407
428
|
|
|
408
429
|
// transpose channels
|
|
@@ -129,6 +129,13 @@ class MIDI{
|
|
|
129
129
|
*/
|
|
130
130
|
this.midiPorts = [];
|
|
131
131
|
|
|
132
|
+
let portOffset = 0
|
|
133
|
+
/**
|
|
134
|
+
* Channel offsets for each port, using the SpessaSynth method
|
|
135
|
+
* @type {number[]}
|
|
136
|
+
*/
|
|
137
|
+
this.midiPortChannelOffsets = [];
|
|
138
|
+
|
|
132
139
|
/**
|
|
133
140
|
* All channels that each track uses. Note: these channels range from 0 to 15, excluding the port offsets!
|
|
134
141
|
* @type {Set<number>[]}
|
|
@@ -280,7 +287,13 @@ class MIDI{
|
|
|
280
287
|
break;
|
|
281
288
|
|
|
282
289
|
case messageTypes.midiPort:
|
|
283
|
-
|
|
290
|
+
const port = eventData[0];
|
|
291
|
+
this.midiPorts[i] = port;
|
|
292
|
+
if(this.midiPortChannelOffsets[port] === undefined)
|
|
293
|
+
{
|
|
294
|
+
this.midiPortChannelOffsets[port] = portOffset;
|
|
295
|
+
portOffset += 16;
|
|
296
|
+
}
|
|
284
297
|
break;
|
|
285
298
|
|
|
286
299
|
case messageTypes.copyright:
|
|
@@ -395,6 +408,11 @@ class MIDI{
|
|
|
395
408
|
}
|
|
396
409
|
}
|
|
397
410
|
this.midiPorts = this.midiPorts.map(port => port === -1 ? defaultPort : port);
|
|
411
|
+
// add dummy port if empty
|
|
412
|
+
if(this.midiPortChannelOffsets.length === 0)
|
|
413
|
+
{
|
|
414
|
+
this.midiPortChannelOffsets = [0];
|
|
415
|
+
}
|
|
398
416
|
|
|
399
417
|
/**
|
|
400
418
|
*
|