spessasynth_lib 3.25.1 → 3.25.2

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.
@@ -6,6 +6,7 @@ import { consoleColors } from "../utils/other.js";
6
6
  import { customControllers } from "../synthetizer/worklet_system/worklet_utilities/controller_tables.js";
7
7
  import { DEFAULT_PERCUSSION } from "../synthetizer/synth_constants.js";
8
8
  import { isGMOn, isGSOn, isXGOn } from "../utils/sysex_detector.js";
9
+ import { isXGDrums, XG_SFX_VOICE } from "../utils/xg_hacks.js";
9
10
 
10
11
  /**
11
12
  * @param ticks {number}
@@ -127,6 +128,11 @@ export function modifyMIDI(
127
128
  const midi = this;
128
129
  SpessaSynthGroupCollapsed("%cApplying changes to the MIDI file...", consoleColors.info);
129
130
 
131
+ SpessaSynthInfo("Desired program changes:", desiredProgramChanges);
132
+ SpessaSynthInfo("Desired CC changes:", desiredControllerChanges);
133
+ SpessaSynthInfo("Desired channels to clear:", desiredChannelsToClear);
134
+ SpessaSynthInfo("Desired channels to transpose:", desiredChannelsToTranspose);
135
+
130
136
  /**
131
137
  * @type {Set<number>}
132
138
  */
@@ -337,7 +343,7 @@ export function modifyMIDI(
337
343
  if (channelsToChangeProgram.has(channel))
338
344
  {
339
345
  const change = desiredProgramChanges.find(c => c.channel === channel);
340
- let desiredBank = change.bank;
346
+ let desiredBank = Math.max(0, Math.min(change.bank, 127));
341
347
  const desiredProgram = change.program;
342
348
  SpessaSynthInfo(
343
349
  `%cSetting %c${change.channel}%c to %c${desiredBank}:${desiredProgram}%c. Track num: %c${trackNum}`,
@@ -362,50 +368,61 @@ export function modifyMIDI(
362
368
  );
363
369
  addEventBefore(programChange);
364
370
 
365
- // on xg, add lsb
366
- if (!change.isDrum && system === "xg")
371
+ const addBank = (isLSB, v) =>
367
372
  {
368
- const bankChangeLSB = getControllerChange(
373
+ const bankChange = getControllerChange(
369
374
  midiChannel,
370
- midiControllers.lsbForControl0BankSelect,
371
- desiredBank,
375
+ isLSB ? midiControllers.lsbForControl0BankSelect : midiControllers.bankSelect,
376
+ v,
372
377
  e.ticks
373
378
  );
374
- addEventBefore(bankChangeLSB);
375
- }
379
+ addEventBefore(bankChange);
380
+ };
376
381
 
377
- // add the bank MSB
378
- const bankChange = getControllerChange(
379
- midiChannel,
380
- midiControllers.bankSelect,
381
- desiredBank,
382
- e.ticks
383
- );
384
- addEventBefore(bankChange);
385
-
386
- // is drums?
387
- // if so, adjust
388
- // do not add gs drum change on the drum channel
389
- if (change.isDrum)
382
+ // on xg, add lsb
383
+ if (system === "xg")
390
384
  {
391
- if (system === "gs" && midiChannel !== DEFAULT_PERCUSSION)
385
+ // xg drums: msb can be 120, 126 or 127
386
+ if (change.isDrum)
392
387
  {
393
388
  SpessaSynthInfo(
394
- `%cAdding GS Drum change on track %c${trackNum}`,
389
+ `%cAdding XG Drum change on track %c${trackNum}`,
395
390
  consoleColors.recognized,
396
391
  consoleColors.value
397
392
  );
398
- addEventBefore(getDrumChange(midiChannel, e.ticks));
393
+ addBank(false, isXGDrums(desiredBank) ? desiredBank : 127);
394
+ addBank(true, 0);
399
395
  }
400
- else if (system === "xg")
396
+ else
401
397
  {
398
+ // sfx voice is set via MSB
399
+ if (desiredBank === XG_SFX_VOICE)
400
+ {
401
+ addBank(false, XG_SFX_VOICE);
402
+ addBank(true, 0);
403
+ }
404
+ else
405
+ {
406
+ // add variation as LSB
407
+ addBank(false, 0);
408
+ addBank(true, desiredBank);
409
+ }
410
+ }
411
+ }
412
+ else
413
+ {
414
+ // add just msb
415
+ addBank(false, desiredBank);
416
+
417
+ if (change.isDrum && midiChannel !== DEFAULT_PERCUSSION)
418
+ {
419
+ // add gs drum change
402
420
  SpessaSynthInfo(
403
- `%cAdding XG Drum change on track %c${trackNum}`,
421
+ `%cAdding GS Drum change on track %c${trackNum}`,
404
422
  consoleColors.recognized,
405
423
  consoleColors.value
406
424
  );
407
- // the system is xg. drums are on msb bank 127.
408
- desiredBank = 127;
425
+ addEventBefore(getDrumChange(midiChannel, e.ticks));
409
426
  }
410
427
  }
411
428
  }
@@ -204,46 +204,54 @@ export function writeRMIDI(
204
204
  const channel = channelsInfo[chNum];
205
205
  if (status === messageTypes.programChange)
206
206
  {
207
+ const isXG = system === "xg";
207
208
  // check if the preset for this program exists
209
+ const initialProgram = e.messageData[0];
208
210
  if (channel.drums)
209
211
  {
210
- if (soundfont.presets.findIndex(p => p.program === e.messageData[0] && p.bank === 128) === -1)
212
+ if (soundfont.presets.findIndex(p => p.program === initialProgram && p.isDrumPreset(
213
+ isXG,
214
+ true
215
+ )) === -1)
211
216
  {
212
217
  // doesn't exist. pick any preset that has bank 128.
213
- e.messageData[0] = soundfont.presets.find(p => p.bank === 128)?.program || 0;
218
+ e.messageData[0] = soundfont.presets.find(p => p.isDrumPreset(isXG))?.program || 0;
219
+ SpessaSynthInfo(
220
+ `%cNo drum preset %c${initialProgram}%c. Channel %c${chNum}%c. Changing program to ${e.messageData[0]}.`,
221
+ consoleColors.info,
222
+ consoleColors.unrecognized,
223
+ consoleColors.info,
224
+ consoleColors.recognized,
225
+ consoleColors.info
226
+ );
214
227
  }
215
228
  }
216
229
  else
217
230
  {
218
- if (soundfont.presets.findIndex(p => p.program === e.messageData[0] && p.bank !== 128) === -1)
231
+ if (soundfont.presets.findIndex(p => p.program === initialProgram && !p.isDrumPreset(isXG)) === -1)
219
232
  {
220
233
  // doesn't exist. pick any preset that does not have bank 128.
221
- e.messageData[0] = soundfont.presets.find(p => p.bank !== 128)?.program || 0;
234
+ e.messageData[0] = soundfont.presets.find(p => !p.isDrumPreset(isXG))?.program || 0;
235
+ SpessaSynthInfo(
236
+ `%cNo preset %c${initialProgram}%c. Channel %c${chNum}%c. Changing program to ${e.messageData[0]}.`,
237
+ consoleColors.info,
238
+ consoleColors.unrecognized,
239
+ consoleColors.info,
240
+ consoleColors.recognized,
241
+ consoleColors.info
242
+ );
222
243
  }
223
244
  }
224
245
  channel.program = e.messageData[0];
225
246
  // check if this preset exists for program and bank
226
247
  const realBank = Math.max(0, channel.lastBank?.messageData[1] - mid.bankOffset); // make sure to take the previous bank offset into account
227
- let bank = channel.drums ? 128 : realBank;
248
+ const bankLSB = (channel?.lastBankLSB?.messageData[1] - mid.bankOffset) || 0;
228
249
  if (channel.lastBank === undefined)
229
250
  {
230
251
  continue;
231
252
  }
232
- if (system === "xg")
233
- {
234
- if (channel.drums)
235
- {
236
- // drums override: set the bank to 127
237
- channelsInfo[chNum].lastBank.messageData[1] = 127;
238
- }
239
- else
240
- {
241
- // potential bank lsb override
242
- const bankLSB = (channel?.lastBankLSB?.messageData[1] - mid.bankOffset) || 0;
243
- bank = chooseBank(bank, bankLSB);
244
- }
245
- }
246
-
253
+ // adjust bank for XG
254
+ let bank = chooseBank(realBank, bankLSB, channel.drums, isXG);
247
255
  if (soundfont.presets.findIndex(p => p.bank === bank && p.program === e.messageData[0]) === -1)
248
256
  {
249
257
  // no preset with this bank. find this program with any bank
@@ -254,7 +262,9 @@ export function writeRMIDI(
254
262
  channel.lastBankLSB.messageData[1] = targetBank;
255
263
  }
256
264
  SpessaSynthInfo(
257
- `%cNo preset %c${bank}:${e.messageData[0]}%c. Changing bank to ${targetBank}.`,
265
+ `%cNo preset %c${bank}:${e.messageData[0]}%c. Channel %c${chNum}%c. Changing bank to ${targetBank}.`,
266
+ consoleColors.info,
267
+ consoleColors.unrecognized,
258
268
  consoleColors.info,
259
269
  consoleColors.recognized,
260
270
  consoleColors.info
@@ -263,15 +273,21 @@ export function writeRMIDI(
263
273
  else
264
274
  {
265
275
  // There is a preset with this bank. Add offset. For drums add the normal offset.
266
- let drumBank = system === "xg" ? 127 : 0;
267
- const newBank = (bank === 128 ? drumBank : realBank) + bankOffset;
276
+ let drumBank = bank;
277
+ if (system === "xg" && bank === 128)
278
+ {
279
+ bank = 127;
280
+ }
281
+ const newBank = (bank === 128 ? 128 : drumBank) + bankOffset;
268
282
  channel.lastBank.messageData[1] = newBank;
269
- if (channel?.lastBankLSB?.messageData)
283
+ if (channel?.lastBankLSB?.messageData && !channel.drums)
270
284
  {
271
285
  channel.lastBankLSB.messageData[1] = channel.lastBankLSB.messageData[1] - mid.bankOffset + bankOffset;
272
286
  }
273
287
  SpessaSynthInfo(
274
- `%cPreset %c${bank}:${e.messageData[0]}%c exists. Changing bank to ${newBank}.`,
288
+ `%cPreset %c${bank}:${e.messageData[0]}%c exists. Channel %c${chNum}%c. Changing bank to ${newBank}.`,
289
+ consoleColors.info,
290
+ consoleColors.recognized,
275
291
  consoleColors.info,
276
292
  consoleColors.recognized,
277
293
  consoleColors.info
@@ -307,16 +323,13 @@ export function writeRMIDI(
307
323
  {
308
324
  channel.drums = false;
309
325
  }
310
- if (bankNumber === intepretation.newBank || channel.drums)
326
+ if (isLSB)
311
327
  {
312
- if (isLSB)
313
- {
314
- channel.lastBankLSB = e;
315
- }
316
- else
317
- {
318
- channel.lastBank = e;
319
- }
328
+ channel.lastBankLSB = e;
329
+ }
330
+ else
331
+ {
332
+ channel.lastBank = e;
320
333
  }
321
334
  }
322
335
 
@@ -370,7 +383,8 @@ export function writeRMIDI(
370
383
  const ticks = track[indexToAdd].ticks;
371
384
  const targetBank = (soundfont.getPreset(
372
385
  0,
373
- has.program
386
+ has.program,
387
+ system === "xg"
374
388
  )?.bank + bankOffset) || bankOffset;
375
389
  track.splice(indexToAdd, 0, new MIDIMessage(
376
390
  ticks,
@@ -8,7 +8,7 @@ import { isGSDrumsOn, isXGOn } from "../utils/sysex_detector.js";
8
8
  /**
9
9
  * Gets the used programs and keys for this MIDI file with a given sound bank
10
10
  * @this {BasicMIDI}
11
- * @param soundfont {{getPreset: function(number, number): BasicPreset}} - the sound bank
11
+ * @param soundfont {BasicSoundBank|WorkletSoundfontManager} - the sound bank
12
12
  * @returns {Object<string, Set<string>>}
13
13
  */
14
14
  export function getUsedProgramsAndKeys(soundfont)
@@ -21,7 +21,7 @@ export function getUsedProgramsAndKeys(soundfont)
21
21
  // Find every bank:program combo and every key:velocity for each. Make sure to care about ports and drums
22
22
  const channelsAmount = 16 + mid.midiPortChannelOffsets.reduce((max, cur) => cur > max ? cur : max);
23
23
  /**
24
- * @type {{program: number, bank: number, bankLSB: number, drums: boolean, string: string}[]}
24
+ * @type {{program: number, bank: number, bankLSB: number, drums: boolean, string: string, actualBank: number}[]}
25
25
  */
26
26
  const channelPresets = [];
27
27
  for (let i = 0; i < channelsAmount; i++)
@@ -31,20 +31,23 @@ export function getUsedProgramsAndKeys(soundfont)
31
31
  program: 0,
32
32
  bank: bank,
33
33
  bankLSB: 0,
34
+ actualBank: bank,
34
35
  drums: i % 16 === DEFAULT_PERCUSSION, // drums appear on 9 every 16 channels,
35
36
  string: `${bank}:0`
36
37
  });
37
38
  }
38
39
 
40
+ // check for xg
41
+ let system = "gs";
42
+
39
43
  function updateString(ch)
40
44
  {
41
- const bank = chooseBank(ch.bank, ch.bankLSB);
45
+ const bank = chooseBank(ch.bank, ch.bankLSB, ch.drums, system === "xg");
42
46
  // check if this exists in the soundfont
43
- let exists = soundfont.getPreset(bank, ch.program);
44
- ch.bank = exists.bank;
45
- ch.bankLSB = 0;
47
+ let exists = soundfont.getPreset(bank, ch.program, system === "xg");
48
+ ch.actualBank = exists.bank;
46
49
  ch.program = exists.program;
47
- ch.string = ch.bank + ":" + ch.program;
50
+ ch.string = ch.actualBank + ":" + ch.program;
48
51
  if (!usedProgramsAndKeys[ch.string])
49
52
  {
50
53
  SpessaSynthInfo(
@@ -90,8 +93,11 @@ export function getUsedProgramsAndKeys(soundfont)
90
93
  }
91
94
 
92
95
  const ports = mid.midiPorts.slice();
93
- // check for xg
94
- let system = "gs";
96
+ // initialize
97
+ channelPresets.forEach(c =>
98
+ {
99
+ updateString(c);
100
+ });
95
101
  while (remainingTracks > 0)
96
102
  {
97
103
  let trackNum = findFirstEventIndex();
@@ -142,6 +148,14 @@ export function getUsedProgramsAndKeys(soundfont)
142
148
  }
143
149
  const bank = event.messageData[1];
144
150
  const realBank = Math.max(0, bank - mid.bankOffset);
151
+ if (isLSB)
152
+ {
153
+ ch.bankLSB = realBank;
154
+ }
155
+ else
156
+ {
157
+ ch.bank = realBank;
158
+ }
145
159
  // interpret the bank
146
160
  const intepretation = parseBankSelect(
147
161
  ch.bank,
@@ -151,7 +165,6 @@ export function getUsedProgramsAndKeys(soundfont)
151
165
  ch.drums,
152
166
  channel
153
167
  );
154
-
155
168
  switch (intepretation.drumsStatus)
156
169
  {
157
170
  case 0:
@@ -162,7 +175,6 @@ export function getUsedProgramsAndKeys(soundfont)
162
175
  // drums changed to off
163
176
  // drum change is a program change
164
177
  ch.drums = false;
165
- ch.bank = realBank;
166
178
  updateString(ch);
167
179
  break;
168
180
 
@@ -170,18 +182,9 @@ export function getUsedProgramsAndKeys(soundfont)
170
182
  // drums changed to on
171
183
  // drum change is a program change
172
184
  ch.drums = true;
173
- ch.bank = 128;
174
185
  updateString(ch);
175
186
  break;
176
187
  }
177
- if (isLSB)
178
- {
179
- ch.bankLSB = intepretation.newBank;
180
- }
181
- else
182
- {
183
- ch.bank = ch.drums ? 128 : intepretation.newBank;
184
- }
185
188
  // do not update the data, bank change doesn't change the preset
186
189
  break;
187
190
 
@@ -191,7 +194,6 @@ export function getUsedProgramsAndKeys(soundfont)
191
194
  // that's a note off
192
195
  continue;
193
196
  }
194
- updateString(ch);
195
197
  usedProgramsAndKeys[ch.string].add(`${event.messageData[0]}-${event.messageData[1]}`);
196
198
  break;
197
199
 
@@ -203,6 +205,10 @@ export function getUsedProgramsAndKeys(soundfont)
203
205
  if (isXGOn(event))
204
206
  {
205
207
  system = "xg";
208
+ SpessaSynthInfo(
209
+ "%cXG on detected!",
210
+ consoleColors.recognized
211
+ );
206
212
  }
207
213
  continue;
208
214
  }
@@ -210,7 +216,6 @@ export function getUsedProgramsAndKeys(soundfont)
210
216
  const isDrum = !!(event.messageData[7] > 0 && event.messageData[5] >> 4);
211
217
  ch = channelPresets[sysexChannel];
212
218
  ch.drums = isDrum;
213
- ch.bank = isDrum ? 128 : 0;
214
219
  updateString(ch);
215
220
  break;
216
221
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spessasynth_lib",
3
- "version": "3.25.1",
3
+ "version": "3.25.2",
4
4
  "description": "MIDI and SoundFont2/DLS library with no compromises",
5
5
  "browser": "index.js",
6
6
  "type": "module",
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { generatorTypes } from "./generator.js";
11
11
  import { Modulator } from "./modulator.js";
12
+ import { isXGDrums } from "../../utils/xg_hacks.js";
12
13
 
13
14
  export class BasicPreset
14
15
  {
@@ -78,6 +79,17 @@ export class BasicPreset
78
79
  this.defaultModulators = modulators;
79
80
  }
80
81
 
82
+ /**
83
+ * @param allowXG {boolean}
84
+ * @param allowSFX {boolean}
85
+ * @returns {boolean}
86
+ */
87
+ isDrumPreset(allowXG, allowSFX = false)
88
+ {
89
+ // sfx is not cool
90
+ return this.bank === 128 || (allowXG && isXGDrums(this.bank) && (this.bank !== 126 || allowSFX));
91
+ }
92
+
81
93
  deletePreset()
82
94
  {
83
95
  this.presetZones.forEach(z => z.deleteZone());
@@ -14,6 +14,7 @@ import { BasicInstrumentZone, BasicPresetZone } from "./basic_zones.js";
14
14
  import { Generator, generatorTypes } from "./generator.js";
15
15
  import { BasicInstrument } from "./basic_instrument.js";
16
16
  import { BasicPreset } from "./basic_preset.js";
17
+ import { isXGDrums } from "../../utils/xg_hacks.js";
17
18
 
18
19
  class BasicSoundBank
19
20
  {
@@ -392,54 +393,63 @@ class BasicSoundBank
392
393
  * Get the appropriate preset, undefined if not foun d
393
394
  * @param bankNr {number}
394
395
  * @param programNr {number}
395
- * @param fallbackToProgram {boolean} if true, if no exact match is found, will use any bank with the given preset
396
+ * @param allowXGDrums {boolean} if true, allows XG drum banks (120, 126 and 127) as drum preset
396
397
  * @return {BasicPreset}
397
398
  */
398
- getPresetNoFallback(bankNr, programNr, fallbackToProgram = false)
399
+ getPresetNoFallback(bankNr, programNr, allowXGDrums = false)
399
400
  {
401
+ // check for exact match
400
402
  const p = this.presets.find(p => p.bank === bankNr && p.program === programNr);
401
403
  if (p)
402
404
  {
403
405
  return p;
404
406
  }
405
- if (fallbackToProgram === false)
406
- {
407
- return undefined;
408
- }
409
- if (bankNr === 128)
407
+ // no match...
408
+ const isDrum = bankNr === 128 || (allowXGDrums && isXGDrums(bankNr));
409
+ if (isDrum)
410
410
  {
411
- // any drum preset
412
- return this.presets.find(p => p.bank === 128);
411
+ if (allowXGDrums)
412
+ {
413
+ // try any drum preset with matching program?
414
+ const p = this.presets.find(p => p.isDrumPreset(allowXGDrums) && p.program === programNr);
415
+ if (p)
416
+ {
417
+ return p;
418
+ }
419
+ }
413
420
  }
414
- return this.presets.find(p => p.program === programNr);
421
+ return undefined;
415
422
  }
416
423
 
417
424
  /**
418
425
  * Get the appropriate preset
419
426
  * @param bankNr {number}
420
427
  * @param programNr {number}
428
+ * @param allowXGDrums {boolean} if true, allows XG drum banks (120, 126 and 127) as drum preset
421
429
  * @returns {BasicPreset}
422
430
  */
423
- getPreset(bankNr, programNr)
431
+ getPreset(bankNr, programNr, allowXGDrums = false)
424
432
  {
425
433
  // check for exact match
426
434
  let preset = this.presets.find(p => p.bank === bankNr && p.program === programNr);
435
+ const isDrums = bankNr === 128 || (allowXGDrums && isXGDrums(bankNr));
427
436
  if (!preset)
428
437
  {
429
438
  // no match...
430
- if (bankNr === 128)
439
+ if (isDrums)
431
440
  {
432
441
  // drum preset: find any preset with bank 128
433
- preset = this.presets.find(p => p.bank === 128 && p.program === programNr);
442
+ preset = this.presets.find(p => p.isDrumPreset(allowXGDrums) && p.program === programNr);
434
443
  if (!preset)
435
444
  {
436
- preset = this.presets.find(p => p.bank === 128);
445
+ // only allow 128, otherwise it would default to XG SFX
446
+ preset = this.presets.find(p => p.isDrumPreset(allowXGDrums));
437
447
  }
438
448
  }
439
449
  else
440
450
  {
441
451
  // non-drum preset: find any preset with the given program that is not a drum preset
442
- preset = this.presets.find(p => p.program === programNr && p.bank !== 128);
452
+ preset = this.presets.find(p => p.program === programNr && !p.isDrumPreset(allowXGDrums));
443
453
  }
444
454
  if (preset)
445
455
  {
@@ -15,7 +15,17 @@ export class DLSPreset extends BasicPreset
15
15
  // use stock default modulators, dls won't ever have DMOD chunk
16
16
  super(defaultModulators);
17
17
  this.program = ulInstrument & 127;
18
- this.bank = (ulBank >> 8) & 127;
18
+ const bankMSB = (ulBank >> 8) & 127;
19
+ const bankLSB = ulBank & 127;
20
+ // switch accordingly
21
+ if (bankMSB > 0)
22
+ {
23
+ this.bank = bankMSB;
24
+ }
25
+ else
26
+ {
27
+ this.bank = bankLSB;
28
+ }
19
29
  const isDrums = ulBank >> 31;
20
30
  if (isDrums)
21
31
  {