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.
@@ -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}
@@ -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
- const getFirstVoiceForChannel = (chan, port) => {
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
- const eventIndex = track.findIndex(event =>
198
- // event is a voice event
199
- (event.messageStatusByte > 0x80 && event.messageStatusByte < 0xF0) &&
200
- // event has the channel we want
201
- (event.messageStatusByte & 0xF) === chan &&
202
- // event is not a controller change which resets all controllers or kills all sounds
203
- (
204
- (event.messageStatusByte & 0xF0) === messageTypes.controllerChange &&
205
- event.messageData[0] !== midiControllers.resetAllControllers &&
206
- event.messageData[0] !== midiControllers.allNotesOff &&
207
- event.messageData[0] !== midiControllers.allSoundOff
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 port = Math.floor(channel / 16);
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 port = Math.floor(change.channel / 16);
278
- let desiredBank = change.isDrum ? 0 : change.bank;
279
- const desiredProgram = change.program;
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
- // get the program changes that are relevant for this channel (and port)
282
- const thisProgramChanges = programChanges.filter(c => midi.midiPorts[c.track] === port && c.channel === midiChannel);
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
- // clear bank selects
286
- clearControllers(midiChannel, port, midiControllers.bankSelect);
287
- clearControllers(midiChannel, port, midiControllers.lsbForControl0BankSelect);
306
+ // clear bank selects
307
+ clearControllers(midiChannel, port, midiControllers.bankSelect);
308
+ clearControllers(midiChannel, port, midiControllers.lsbForControl0BankSelect);
288
309
 
289
- // if drums or the program uses bank select, flag as gs
290
- if((change.isDrum || desiredBank > 0) && !addedGs)
291
- {
292
- // make sure that GS is on
293
- // GS on: F0 41 10 42 12 40 00 7F 00 41 F7
294
- midi.tracks.forEach(track => {
295
- for(let eventIndex = 0; eventIndex < track.length; eventIndex++)
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
- const event = track[eventIndex];
298
- if(event.messageStatusByte === messageTypes.systemExclusive)
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
- if(
301
- event.messageData[0] === 0x41 // roland
302
- && event.messageData[2] === 0x42 // GS
303
- && event.messageData[6] === 0x7F // Mode set
304
- )
305
- {
306
- // thats a GS on, we're done here
307
- addedGs = true;
308
- SpessaSynthInfo("%cGS on detected!", consoleColors.recognized);
309
- break;
310
- }
311
- else if(
312
- event.messageData[0] === 0x7E // non realtime
313
- && event.messageData[2] === 0x09 // gm system
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
- // remove all program changes
339
- for(const change of thisProgramChanges)
346
+ });
347
+ if(!addedGs)
340
348
  {
341
- midi.tracks[change.track].splice(midi.tracks[change.track].indexOf(change.message), 1);
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
- * Find the first voice message
345
- * @type {{index: number, track: number}[]}
346
- */
347
- const firstVoiceForTrack = getFirstVoiceForChannel(midiChannel, port);
348
- if(firstVoiceForTrack.length === 0)
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
- SpessaSynthWarn("Program change but no notes... ignoring!");
351
- return;
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
- // get the first voice overall
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
- // do not add gs drum change on drum channel
364
- if(midiSystem === "gs" && midiChannel !== DEFAULT_PERCUSSION)
365
- {
366
- SpessaSynthInfo(`%cAdding GS Drum change on track %c${firstVoice.track}`,
367
- consoleColors.recognized,
368
- consoleColors.value
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
- SpessaSynthInfo(`%cSetting %c${change.channel}%c to %c${desiredBank}:${desiredProgram}`,
385
- consoleColors.info,
386
- consoleColors.recognized,
387
- consoleColors.info,
388
- consoleColors.recognized);
389
-
390
- // add bank
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
- this.midiPorts[i] = eventData[0];
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
  *