spessasynth_lib 3.24.9 → 3.24.11

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.
@@ -1,6 +1,6 @@
1
1
  import { messageTypes, midiControllers, MidiMessage } from "./midi_message.js";
2
2
  import { IndexedByteArray } from "../utils/indexed_array.js";
3
- import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn } from "../utils/loggin.js";
3
+ import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from "../utils/loggin.js";
4
4
  import { consoleColors } from "../utils/other.js";
5
5
  import { DEFAULT_PERCUSSION } from "../synthetizer/synthetizer.js";
6
6
 
@@ -20,16 +20,23 @@ export function getGsOn(ticks)
20
20
  0x10, // Device ID (defaults to 16 on roland)
21
21
  0x42, // GS
22
22
  0x12, // Command ID (DT1) (whatever that means...)
23
- 0x40, // System parameter }
24
- 0x00, // Global parameter } Address
25
- 0x7F, // GS Change }
26
- 0x00, // turn on } Data
23
+ 0x40, // System parameter - Address
24
+ 0x00, // Global parameter - Address
25
+ 0x7F, // GS Change - Address
26
+ 0x00, // turn on - Data
27
27
  0x41, // checksum
28
28
  0xF7 // end of exclusive
29
29
  ])
30
30
  );
31
31
  }
32
32
 
33
+ /**
34
+ * @param channel {number}
35
+ * @param cc {number}
36
+ * @param value {number}
37
+ * @param ticks {number}
38
+ * @returns {MidiMessage}
39
+ */
33
40
  function getControllerChange(channel, cc, value, ticks)
34
41
  {
35
42
  return new MidiMessage(
@@ -56,7 +63,7 @@ function getDrumChange(channel, ticks)
56
63
  0x40, // System parameter }
57
64
  chanAddress, // Channel parameter } Address
58
65
  0x15, // Drum change }
59
- 0x01 // Is Drums } Data
66
+ 0x01 // Is Drums } Data
60
67
  ];
61
68
  // calculate checksum
62
69
  // https://cdn.roland.com/assets/media/pdf/F-20_MIDI_Imple_e01_W.pdf section 4
@@ -98,6 +105,7 @@ function getDrumChange(channel, ticks)
98
105
  * which will be used to fine-tune the pitch in cents using RPN.
99
106
  */
100
107
 
108
+
101
109
  /**
102
110
  * Allows easy editing of the file by removing channels, changing programs,
103
111
  * changing controllers and transposing channels. Note that this modifies the MIDI in-place.
@@ -117,494 +125,397 @@ export function modifyMIDI(
117
125
  )
118
126
  {
119
127
  SpessaSynthGroupCollapsed("%cApplying changes to the MIDI file...", consoleColors.info);
128
+
120
129
  /**
121
- * @param channel {number}
122
- * @param port {number}
130
+ * @type {Set<number>}
123
131
  */
124
- const clearChannelMessages = (channel, port) =>
125
- {
126
- midi.tracks.forEach((track, trackNum) =>
127
- {
128
- if (midi.midiPorts[trackNum] !== port)
129
- {
130
- return;
131
- }
132
- for (let i = track.length - 1; i >= 0; i--) // iterate in reverse to not mess up indexes
133
- {
134
- if (track[i].messageStatusByte >= 0x80 && track[i].messageStatusByte < 0xF0) // do not clear sysexes
135
- {
136
- if ((track[i].messageStatusByte & 0xF) === channel)
137
- {
138
- track.splice(i, 1);
139
- }
140
- }
141
- }
142
- });
143
- };
144
- desiredChannelsToClear.forEach(c =>
132
+ const channelsToChangeProgram = new Set();
133
+ desiredProgramChanges.forEach(c =>
145
134
  {
146
- const channel = c % 16;
147
- const offset = c - channel;
148
- const port = midi.midiPortChannelOffsets.findIndex(o => o === offset);
149
- clearChannelMessages(channel, port);
150
- SpessaSynthInfo(
151
- `%cRemoving channel %c${c}%c!`,
152
- consoleColors.info,
153
- consoleColors.recognized,
154
- consoleColors.info
155
- );
135
+ channelsToChangeProgram.add(c.channel);
156
136
  });
137
+
138
+
139
+ // go through all events one by one
140
+ let system = "gs";
157
141
  let addedGs = false;
158
- let midiSystem = "gs";
159
- /**
160
- * find all controller changes in the file
161
- * @type {{
162
- * track: number,
163
- * message: MidiMessage,
164
- * channel: number
165
- * }[]}
166
- */
167
- const ccChanges = [];
168
142
  /**
169
- * @type {{
170
- * track: number,
171
- * message: MidiMessage,
172
- * channel: number
173
- * }[]}
143
+ * indexes for tracks
144
+ * @type {number[]}
174
145
  */
175
- const programChanges = [];
176
- midi.tracks.forEach((track, trackNum) =>
146
+ const eventIndexes = Array(midi.tracks.length).fill(0);
147
+ let remainingTracks = midi.tracks.length;
148
+
149
+ function findFirstEventIndex()
177
150
  {
178
- track.forEach(message =>
151
+ let index = 0;
152
+ let ticks = Infinity;
153
+ midi.tracks.forEach((track, i) =>
179
154
  {
180
- const status = message.messageStatusByte & 0xF0;
181
- if (status === messageTypes.controllerChange)
155
+ if (eventIndexes[i] >= track.length)
182
156
  {
183
- ccChanges.push({
184
- track: trackNum,
185
- message: message,
186
- channel: message.messageStatusByte & 0xF
187
- });
188
- }
189
- else if (status === messageTypes.programChange)
190
- {
191
- programChanges.push({
192
- track: trackNum,
193
- message: message,
194
- channel: message.messageStatusByte & 0xF
195
- });
157
+ return;
196
158
  }
197
- else if (message.messageStatusByte === messageTypes.systemExclusive)
159
+ if (track[eventIndexes[i]].ticks < ticks)
198
160
  {
199
- // check for xg
200
- if (
201
- message.messageData[0] === 0x43 && // Yamaha
202
- message.messageData[2] === 0x4C && // XG ON
203
- message.messageData[5] === 0x7E &&
204
- message.messageData[6] === 0x00
205
- )
206
- {
207
- SpessaSynthInfo("%cXG system on detected", consoleColors.info);
208
- midiSystem = "xg";
209
- addedGs = true; // flag as true so gs won't get added
210
- }
211
- else
212
- // check for xg program change
213
- if (
214
- message.messageData[0] === 0x43 // yamaha
215
- && message.messageData[2] === 0x4C // XG
216
- && message.messageData[3] === 0x08 // part parameter
217
- && message.messageData[5] === 0x03 // program change
218
- )
219
- {
220
- programChanges.push({
221
- track: trackNum,
222
- message: message,
223
- channel: message.messageData[4]
224
- });
225
- }
161
+ index = i;
162
+ ticks = track[eventIndexes[i]].ticks;
226
163
  }
227
164
  });
228
- });
165
+ return index;
166
+ }
229
167
 
168
+ // it copies midiPorts everywhere else, but here 0 works so DO NOT CHANGE!
230
169
  /**
231
- * @param chan {number}
232
- * @param port {number}
233
- * @param searchForNoteOn {boolean} search for note on if true, any voice otherwise.
234
- * first note on is needed because multi port midis like to reference other ports before playing to the port we want.
235
- * First voice otherwise, because MP6 doesn't like program changes after cc changes in embedded midis
236
- * @return {{index: number, track: number}[]}
170
+ * midi port number for the corresponding track
171
+ * @type {number[]}
237
172
  */
238
- const getFirstVoiceForChannel = (chan, port, searchForNoteOn) =>
239
- {
240
- return midi.tracks
241
- .reduce((noteOns, track, trackNum) =>
242
- {
243
- if (midi.usedChannelsOnTrack[trackNum].has(chan) && midi.midiPorts[trackNum] === port)
244
- {
245
- let eventIndex;
246
- if (searchForNoteOn)
247
- {
248
- eventIndex = track.findIndex(event =>
249
- // event is a noteon
250
- (event.messageStatusByte & 0xF0) === messageTypes.noteOn);
251
- }
252
- else
253
- {
254
- eventIndex = track.findIndex(event =>
255
- // event is a voice event
256
- (event.messageStatusByte > 0x80 && event.messageStatusByte < 0xF0) &&
257
- // event has the channel we want
258
- (event.messageStatusByte & 0xF) === chan &&
259
- // event is not one of the controller changes that reset things
260
- !(
261
- (event.messageStatusByte & 0xF0 === messageTypes.controllerChange) &&
262
- (
263
- event.messageData[0] === midiControllers.resetAllControllers ||
264
- event.messageData[0] === midiControllers.allNotesOff ||
265
- event.messageData[0] === midiControllers.allSoundOff
266
- )
267
- )
268
- );
269
- }
270
- if (eventIndex !== -1)
271
- {
272
- noteOns.push({
273
- index: eventIndex,
274
- track: trackNum
275
- });
276
- }
277
- }
278
- return noteOns;
279
- }, []);
280
- };
281
-
282
-
173
+ const midiPorts = midi.midiPorts.slice();
283
174
  /**
284
- * @param channel {number}
285
- * @param port {number}
286
- * @param cc {number}
175
+ * midi port: channel offset
176
+ * @type {Object<number, number>}
287
177
  */
288
- const clearControllers = (channel, port, cc) =>
178
+ const midiPortChannelOffsets = {};
179
+ let midiPortChannelOffset = 0;
180
+
181
+ function assignMIDIPort(trackNum, port)
289
182
  {
290
- const thisCcChanges = ccChanges.filter(m =>
291
- m.channel === channel
292
- && m.message.messageData[0] === cc
293
- && midi.midiPorts[m.track] === port);
294
- // delete
295
- for (let i = 0; i < thisCcChanges.length; i++)
183
+ // do not assign ports to empty tracks
184
+ if (midi.usedChannelsOnTrack[trackNum].size === 0)
296
185
  {
297
- // remove
298
- const e = thisCcChanges[i];
299
- midi.tracks[e.track].splice(midi.tracks[e.track].indexOf(e.message), 1);
300
- ccChanges.splice(ccChanges.indexOf(e), 1);
186
+ return;
301
187
  }
302
188
 
303
- };
304
- desiredControllerChanges.forEach(desiredChange =>
305
- {
306
- const channel = desiredChange.channel;
307
- const midiChannel = channel % 16;
308
- const offset = channel - midiChannel;
309
- const port = midi.midiPortChannelOffsets.findIndex(o => o === offset);
310
- const targetValue = desiredChange.controllerValue;
311
- const ccNumber = desiredChange.controllerNumber;
312
- // the controller is locked. Clear all controllers
313
- clearControllers(midiChannel, port, ccNumber);
314
- // since we've removed all ccs, we need to add the first one.
315
- SpessaSynthInfo(
316
- `%cNo controller %c${ccNumber}%c on channel %c${channel}%c found. Adding it!`,
317
- consoleColors.info,
318
- consoleColors.unrecognized,
319
- consoleColors.info,
320
- consoleColors.value,
321
- consoleColors.info
322
- );
323
- /**
324
- * @type {{index: number, track: number}[]}
325
- */
326
- const firstNoteOnForTrack = getFirstVoiceForChannel(midiChannel, port, true);
327
- if (firstNoteOnForTrack.length === 0)
189
+ // assign new 16 channels if the port is not occupied yet
190
+ if (midiPortChannelOffset === 0)
328
191
  {
329
- SpessaSynthWarn("Program change but no notes... ignoring!");
330
- return;
192
+ midiPortChannelOffset += 16;
193
+ midiPortChannelOffsets[port] = 0;
194
+ }
195
+
196
+ if (midiPortChannelOffsets[port] === undefined)
197
+ {
198
+ midiPortChannelOffsets[port] = midiPortChannelOffset;
199
+ midiPortChannelOffset += 16;
331
200
  }
332
- const firstNoteOn = firstNoteOnForTrack.reduce((first, current) =>
333
- midi.tracks[current.track][current.index].ticks < midi.tracks[first.track][first.index].ticks ? current : first);
334
- // prepend with controller change
335
- const ccChange = getControllerChange(
336
- midiChannel,
337
- ccNumber,
338
- targetValue,
339
- midi.tracks[firstNoteOn.track][firstNoteOn.index].ticks
340
- );
341
- midi.tracks[firstNoteOn.track].splice(firstNoteOn.index, 0, ccChange);
201
+
202
+ midiPorts[trackNum] = port;
203
+ }
204
+
205
+ // assign port offsets
206
+ midi.midiPorts.forEach((port, trackIndex) =>
207
+ {
208
+ assignMIDIPort(trackIndex, port);
209
+ });
210
+
211
+ const channelsAmount = midiPortChannelOffset;
212
+ /**
213
+ * Tracks if the channel already had its first note on
214
+ * @type {boolean[]}
215
+ */
216
+ const isFirstNoteOn = Array(channelsAmount).fill(true);
217
+
218
+ /**
219
+ * MIDI key transpose
220
+ * @type {number[]}
221
+ */
222
+ const coarseTranspose = Array(channelsAmount).fill(0);
223
+ /**
224
+ * RPN fine transpose
225
+ * @type {number[]}
226
+ */
227
+ const fineTranspose = Array(channelsAmount).fill(0);
228
+ desiredChannelsToTranspose.forEach(transpose =>
229
+ {
230
+ const coarse = Math.trunc(transpose.keyShift);
231
+ const fine = transpose.keyShift - coarse;
232
+ coarseTranspose[transpose.channel] = coarse;
233
+ fineTranspose[transpose.channel] = fine;
342
234
  });
343
235
 
344
- desiredProgramChanges.forEach(change =>
236
+ while (remainingTracks > 0)
345
237
  {
346
- const midiChannel = change.channel % 16;
347
- const offset = change.channel - midiChannel;
348
- const port = midi.midiPortChannelOffsets.findIndex(o => o === offset);
349
- let desiredBank = change.isDrum ? 0 : change.bank;
350
- const desiredProgram = change.program;
238
+ let trackNum = findFirstEventIndex();
239
+ const track = midi.tracks[trackNum];
240
+ if (eventIndexes[trackNum] >= track.length)
241
+ {
242
+ remainingTracks--;
243
+ continue;
244
+ }
245
+ const index = eventIndexes[trackNum]++;
246
+ const e = track[index];
351
247
 
352
- // get the program changes that are relevant for this channel (and port)
353
- const thisProgramChanges = programChanges.filter(c => midi.midiPorts[c.track] === port && c.channel === midiChannel);
248
+ const deleteThisEvent = () =>
249
+ {
250
+ track.splice(index, 1);
251
+ eventIndexes[trackNum]--;
252
+ };
354
253
 
254
+ /**
255
+ * @param e {MidiMessage}
256
+ * @param offset{number}
257
+ */
258
+ const addEventBefore = (e, offset = 0) =>
259
+ {
260
+ track.splice(index + offset, 0, e);
261
+ eventIndexes[trackNum]++;
262
+ };
355
263
 
356
- // clear bank selects
357
- clearControllers(midiChannel, port, midiControllers.bankSelect);
358
- clearControllers(midiChannel, port, midiControllers.lsbForControl0BankSelect);
359
264
 
360
- // if drums or the program uses bank select, flag as gs
361
- if ((change.isDrum || desiredBank > 0) && !addedGs)
265
+ let portOffset = midiPortChannelOffsets[midiPorts[trackNum]] || 0;
266
+ if (e.messageStatusByte === messageTypes.midiPort)
362
267
  {
363
- // make sure that GS is on
364
- // GS on: F0 41 10 42 12 40 00 7F 00 41 F7
365
- midi.tracks.forEach(track =>
366
- {
367
- for (let eventIndex = 0; eventIndex < track.length; eventIndex++)
268
+ assignMIDIPort(trackNum, e.messageData[0]);
269
+ continue;
270
+ }
271
+ const status = e.messageStatusByte & 0xF0;
272
+ const midiChannel = e.messageStatusByte & 0xF;
273
+ const channel = midiChannel + portOffset;
274
+ // clear channel?
275
+ if (desiredChannelsToClear.indexOf(channel) !== -1)
276
+ {
277
+ deleteThisEvent();
278
+ continue;
279
+ }
280
+ switch (status)
281
+ {
282
+ case messageTypes.noteOn:
283
+ // is it first?
284
+ if (isFirstNoteOn[channel])
368
285
  {
369
- const event = track[eventIndex];
370
- if (event.messageStatusByte === messageTypes.systemExclusive)
286
+ isFirstNoteOn[channel] = false;
287
+ // all right, so this is the first note on
288
+ // first: controllers
289
+ // because FSMP does not like program changes after cc changes in embedded midis
290
+ // and since we use splice,
291
+ // controllers get added first, then programs before them
292
+ // now add controllers
293
+ desiredControllerChanges.filter(c => c.channel === channel).forEach(change =>
294
+ {
295
+ const ccChange = getControllerChange(
296
+ midiChannel,
297
+ change.controllerNumber,
298
+ change.controllerValue,
299
+ e.ticks
300
+ );
301
+ addEventBefore(ccChange);
302
+ });
303
+ const fineTune = fineTranspose[channel];
304
+
305
+ if (fineTune !== 0)
306
+ {
307
+ // add rpn
308
+ // 64 is the center, 96 = 50 cents up
309
+ const centsCoarse = (fineTune * 64) + 64;
310
+ const rpnCoarse = getControllerChange(midiChannel, midiControllers.RPNMsb, 0, e.ticks);
311
+ const rpnFine = getControllerChange(midiChannel, midiControllers.RPNLsb, 1, e.ticks);
312
+ const dataEntryCoarse = getControllerChange(
313
+ channel,
314
+ midiControllers.dataEntryMsb,
315
+ centsCoarse,
316
+ e.ticks
317
+ );
318
+ const dataEntryFine = getControllerChange(
319
+ midiChannel,
320
+ midiControllers.lsbForControl6DataEntry,
321
+ 0,
322
+ e.ticks
323
+ );
324
+ addEventBefore(dataEntryFine);
325
+ addEventBefore(dataEntryCoarse);
326
+ addEventBefore(rpnFine);
327
+ addEventBefore(rpnCoarse);
328
+
329
+ }
330
+
331
+ if (channelsToChangeProgram.has(channel))
371
332
  {
372
- if (
373
- event.messageData[0] === 0x41 // roland
374
- && event.messageData[2] === 0x42 // GS
375
- && event.messageData[6] === 0x7F // Mode set
376
- )
333
+ const change = desiredProgramChanges.find(c => c.channel === channel);
334
+ let desiredBank = change.bank;
335
+ const desiredProgram = change.program;
336
+ SpessaSynthInfo(
337
+ `%cSetting %c${change.channel}%c to %c${desiredBank}:${desiredProgram}%c. Track num: %c${trackNum}`,
338
+ consoleColors.info,
339
+ consoleColors.recognized,
340
+ consoleColors.info,
341
+ consoleColors.recognized,
342
+ consoleColors.info,
343
+ consoleColors.recognized
344
+ );
345
+
346
+ // note: this is in reverse.
347
+ // the output event order is: drums -> lsb -> msb -> program change
348
+
349
+ // add program change
350
+ const programChange = new MidiMessage(
351
+ e.ticks,
352
+ messageTypes.programChange | midiChannel,
353
+ new IndexedByteArray([
354
+ desiredProgram
355
+ ])
356
+ );
357
+ addEventBefore(programChange);
358
+
359
+ // on xg, add lsb
360
+ if (!change.isDrum && system === "xg")
377
361
  {
378
- // thats a GS on, we're done here
379
- addedGs = true;
380
- SpessaSynthInfo(
381
- "%cGS on detected!",
382
- consoleColors.recognized
362
+ const bankChangeLSB = getControllerChange(
363
+ midiChannel,
364
+ midiControllers.lsbForControl0BankSelect,
365
+ desiredBank,
366
+ e.ticks
383
367
  );
384
- break;
368
+ addEventBefore(bankChangeLSB);
385
369
  }
386
- else if (
387
- event.messageData[0] === 0x7E // non realtime
388
- && event.messageData[2] === 0x09 // gm system
389
- )
370
+
371
+ // add the bank MSB
372
+ const bankChange = getControllerChange(
373
+ midiChannel,
374
+ midiControllers.bankSelect,
375
+ desiredBank,
376
+ e.ticks
377
+ );
378
+ addEventBefore(bankChange);
379
+
380
+ // is drums?
381
+ // if so, adjust
382
+ // do not add gs drum change on the drum channel
383
+ if (change.isDrum)
390
384
  {
391
- // thats a GM/2 system change, remove it!
392
- SpessaSynthInfo(
393
- "%cGM/2 on detected, removing!",
394
- consoleColors.info
395
- );
396
- track.splice(eventIndex, 1);
397
- // adjust program and bank changes
398
- eventIndex--;
385
+ if (system === "gs" && midiChannel !== DEFAULT_PERCUSSION)
386
+ {
387
+ SpessaSynthInfo(
388
+ `%cAdding GS Drum change on track %c${trackNum}`,
389
+ consoleColors.recognized,
390
+ consoleColors.value
391
+ );
392
+ addEventBefore(getDrumChange(midiChannel, e.ticks));
393
+ }
394
+ else if (system === "xg")
395
+ {
396
+ SpessaSynthInfo(
397
+ `%cAdding XG Drum change on track %c${trackNum}`,
398
+ consoleColors.recognized,
399
+ consoleColors.value
400
+ );
401
+ // the system is xg. drums are on msb bank 127.
402
+ desiredBank = 127;
403
+ }
399
404
  }
400
405
  }
401
406
  }
402
-
403
- });
404
- if (!addedGs)
405
- {
406
- // gs is not on, add it on the first track at index 0 (or 1 if track name is first)
407
- let index = 0;
408
- if (midi.tracks[0][0].messageStatusByte === messageTypes.trackName)
407
+ // transpose key (for zero it won't change anyway)
408
+ e.messageData[0] += coarseTranspose[channel];
409
+ break;
410
+
411
+ case messageTypes.noteOff:
412
+ e.messageData[0] += coarseTranspose[channel];
413
+ break;
414
+
415
+ case messageTypes.programChange:
416
+ // do we delete it?
417
+ if (channelsToChangeProgram.has(channel))
409
418
  {
410
- index++;
419
+ // this channel has program change. BEGONE!
420
+ deleteThisEvent();
421
+ continue;
411
422
  }
412
- midi.tracks[0].splice(index, 0, getGsOn(0));
413
- SpessaSynthInfo("%cGS on not detected. Adding it.", consoleColors.info);
414
- addedGs = true;
415
- }
416
- }
417
-
418
- // remove all program changes
419
- for (const change of thisProgramChanges)
420
- {
421
- midi.tracks[change.track].splice(
422
- midi.tracks[change.track].indexOf(change.message),
423
- 1
424
- );
425
- }
426
- /**
427
- * Find the first voice message
428
- * @type {{index: number, track: number}[]}
429
- */
430
- const firstVoiceForTrack = getFirstVoiceForChannel(midiChannel, port, offset > 0);
431
- if (firstVoiceForTrack.length === 0)
432
- {
433
- SpessaSynthWarn("Program change but no notes... ignoring!");
434
- return;
435
- }
436
- // get the first voice overall
437
- const firstVoice = firstVoiceForTrack.reduce((first, current) =>
438
- midi.tracks[current.track][current.index].ticks < midi.tracks[first.track][first.index].ticks ? current : first);
439
- // get the index and ticks
440
- let firstIndex = firstVoice.index;
441
- const ticks = midi.tracks[firstVoice.track][firstVoice.index].ticks;
442
-
443
- // add drums if needed
444
- if (change.isDrum)
445
- {
446
- // do not add gs drum change on drum channel
447
- if (midiSystem === "gs" && midiChannel !== DEFAULT_PERCUSSION)
448
- {
449
- SpessaSynthInfo(
450
- `%cAdding GS Drum change on track %c${firstVoice.track}`,
451
- consoleColors.recognized,
452
- consoleColors.value
453
- );
454
- midi.tracks[firstVoice.track].splice(
455
- firstIndex,
456
- 0,
457
- getDrumChange(change.channel, ticks)
458
- );
459
- firstIndex++;
460
- }
461
- else if (midiSystem === "xg")
462
- {
463
- SpessaSynthInfo(
464
- `%cAdding XG Drum change on track %c${firstVoice.track}`,
465
- consoleColors.recognized,
466
- consoleColors.value
467
- );
468
- // system is xg. drums are on msb bank 127.
469
- desiredBank = 127;
470
- }
471
- }
472
-
473
- SpessaSynthInfo(
474
- `%cSetting %c${change.channel}%c to %c${desiredBank}:${desiredProgram}%c. Track num: %c${firstVoice.track}`,
475
- consoleColors.info,
476
- consoleColors.recognized,
477
- consoleColors.info,
478
- consoleColors.recognized,
479
- consoleColors.info,
480
- consoleColors.recognized
481
- );
482
-
483
- // add bank
484
- const bankChange = getControllerChange(
485
- midiChannel,
486
- midiControllers.bankSelect,
487
- desiredBank,
488
- ticks
489
- );
490
- midi.tracks[firstVoice.track].splice(firstIndex, 0, bankChange);
491
- firstIndex++;
492
-
493
- // on xg, add lsb
494
- if (!change.isDrum && midiSystem === "xg")
495
- {
496
- const bankChangeLSB = getControllerChange(
497
- midiChannel,
498
- midiControllers.lsbForControl0BankSelect,
499
- desiredBank,
500
- ticks
501
- );
502
- midi.tracks[firstVoice.track].splice(firstIndex, 0, bankChangeLSB);
503
- firstIndex++;
504
- }
505
-
506
- // add program change
507
- const programChange = new MidiMessage(
508
- ticks,
509
- messageTypes.programChange | midiChannel,
510
- new IndexedByteArray([
511
- desiredProgram
512
- ])
513
- );
514
- midi.tracks[firstVoice.track].splice(firstIndex, 0, programChange);
515
- });
516
-
517
- // transpose channels
518
- for (const transpose of desiredChannelsToTranspose)
519
- {
520
- const midiChannel = transpose.channel % 16;
521
- const port = Math.floor(transpose.channel / 16);
522
- const keyShift = Math.trunc(transpose.keyShift);
523
- const fineTune = transpose.keyShift - keyShift;
524
- SpessaSynthInfo(
525
- `%cTransposing channel %c${transpose.channel}%c by %c${transpose.keyShift}%c semitones`,
526
- consoleColors.info,
527
- consoleColors.recognized,
528
- consoleColors.info,
529
- consoleColors.value,
530
- consoleColors.info
531
- );
532
- if (keyShift !== 0)
533
- {
534
- midi.tracks.forEach((track, trackNum) =>
535
- {
423
+ break;
424
+
425
+ case messageTypes.controllerChange:
426
+ const ccNum = e.messageData[0];
427
+ const changes = desiredControllerChanges.find(c => c.channel === channel && ccNum === c.controllerNumber);
428
+ if (changes !== undefined)
429
+ {
430
+ // this controller is locked, BEGONE CHANGE!
431
+ deleteThisEvent();
432
+ continue;
433
+ }
434
+ // bank maybe?
435
+ if (ccNum === midiControllers.bankSelect || ccNum === midiControllers.lsbForControl0BankSelect)
436
+ {
437
+ if (channelsToChangeProgram.has(channel))
438
+ {
439
+ // BEGONE!
440
+ deleteThisEvent();
441
+ continue;
442
+ }
443
+ }
444
+ break;
445
+
446
+ case messageTypes.systemExclusive:
447
+ // check for xg on
536
448
  if (
537
- midi.midiPorts[trackNum] !== port ||
538
- !midi.usedChannelsOnTrack[trackNum].has(midiChannel)
449
+ e.messageData[0] === 0x43 && // Yamaha
450
+ e.messageData[2] === 0x4C && // XG ON
451
+ e.messageData[5] === 0x7E &&
452
+ e.messageData[6] === 0x00
539
453
  )
540
454
  {
541
- return;
455
+ SpessaSynthInfo("%cXG system on detected", consoleColors.info);
456
+ system = "xg";
457
+ addedGs = true; // flag as true so gs won't get added
542
458
  }
543
- const onStatus = messageTypes.noteOn | midiChannel;
544
- const offStatus = messageTypes.noteOff | midiChannel;
545
- const polyStatus = messageTypes.polyPressure | midiChannel;
546
- track.forEach(event =>
459
+ else
460
+ // check for xg program change
461
+ if (
462
+ e.messageData[0] === 0x43 // yamaha
463
+ && e.messageData[2] === 0x4C // XG
464
+ && e.messageData[3] === 0x08 // part parameter
465
+ && e.messageData[5] === 0x03 // program change
466
+ )
547
467
  {
548
- if (
549
- event.messageStatusByte !== onStatus &&
550
- event.messageStatusByte !== offStatus &&
551
- event.messageStatusByte !== polyStatus
552
- )
468
+ // do we delete it?
469
+ if (channelsToChangeProgram.has(e.messageData[4] + portOffset))
553
470
  {
554
- return;
471
+ // this channel has program change. BEGONE!
472
+ deleteThisEvent();
555
473
  }
556
- event.messageData[0] = Math.max(
557
- 0,
558
- Math.min(
559
- 127,
560
- event.messageData[0] + keyShift
561
- )
474
+ }
475
+ else
476
+ // check for GS on
477
+ if (
478
+ e.messageData[0] === 0x41 // roland
479
+ && e.messageData[2] === 0x42 // GS
480
+ && e.messageData[6] === 0x7F // Mode set
481
+ )
482
+ {
483
+ // that's a GS on, we're done here
484
+ addedGs = true;
485
+ SpessaSynthInfo(
486
+ "%cGS on detected!",
487
+ consoleColors.recognized
562
488
  );
563
- });
564
- });
489
+ break;
490
+ }
491
+ else
492
+ // check for GM/2 on
493
+ if (
494
+ e.messageData[0] === 0x7E // non realtime
495
+ && e.messageData[2] === 0x09 // gm system
496
+ )
497
+ {
498
+ // that's a GM/2 system change, remove it!
499
+ SpessaSynthInfo(
500
+ "%cGM/2 on detected, removing!",
501
+ consoleColors.info
502
+ );
503
+ deleteThisEvent();
504
+ addedGs = false;
505
+ }
565
506
  }
566
-
567
- if (fineTune !== 0)
507
+ }
508
+ // check for gs
509
+ if (!addedGs)
510
+ {
511
+ // gs is not on, add it on the first track at index 0 (or 1 if track name is first)
512
+ let index = 0;
513
+ if (midi.tracks[0][0].messageStatusByte === messageTypes.trackName)
568
514
  {
569
- // find the first track that uses this channel
570
- const track = midi.tracks.find((t, tNum) => midi.usedChannelsOnTrack[tNum].has(transpose.channel));
571
- if (track === undefined)
572
- {
573
- SpessaSynthWarn(`Channel ${transpose.channel} unused but transpose requested???`);
574
- continue;
575
- }
576
- // find first noteon for this channel
577
- const noteOn = messageTypes.noteOn | (transpose.channel % 16);
578
- const noteIndex = track.findIndex(n => n.messageStatusByte === noteOn);
579
- if (noteIndex === -1)
580
- {
581
- SpessaSynthWarn(`No notes on channel ${transpose.channel} but transpose requested???`);
582
- continue;
583
- }
584
- const ticks = track[noteIndex].ticks;
585
- // add rpn
586
- // 64 is the center, 96 = 50 cents up
587
- const centsCoarse = (fineTune * 64) + 64;
588
- const ccChange = messageTypes.controllerChange | (transpose.channel % 16);
589
- const rpnCoarse = new MidiMessage(ticks, ccChange, new IndexedByteArray([midiControllers.RPNMsb, 0]));
590
- const rpnFine = new MidiMessage(ticks, ccChange, new IndexedByteArray([midiControllers.RPNLsb, 1]));
591
- const deCoarse = new MidiMessage(
592
- ticks,
593
- ccChange,
594
- new IndexedByteArray([midiControllers.dataEntryMsb, centsCoarse])
595
- );
596
- const deFine = new MidiMessage(
597
- ticks,
598
- ccChange,
599
- new IndexedByteArray([midiControllers.lsbForControl6DataEntry, 0])
600
- );
601
- // add in reverse
602
- track.splice(noteIndex, 0, deFine);
603
- track.splice(noteIndex, 0, deCoarse);
604
- track.splice(noteIndex, 0, rpnFine);
605
- track.splice(noteIndex, 0, rpnCoarse);
606
-
515
+ index++;
607
516
  }
517
+ midi.tracks[0].splice(index, 0, getGsOn(0));
518
+ SpessaSynthInfo("%cGS on not detected. Adding it.", consoleColors.info);
608
519
  }
609
520
  SpessaSynthGroupEnd();
610
521
  }