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.
@@ -191,6 +191,22 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
191
191
  */
192
192
  totalVoicesAmount = 0;
193
193
 
194
+ /**
195
+ * Synth's default (reset) preset
196
+ * @type {BasicPreset}
197
+ */
198
+ defaultPreset;
199
+
200
+ defaultPresetUsesOverride = false;
201
+
202
+ /**
203
+ * Synth's default (reset) drum preset
204
+ * @type {BasicPreset}
205
+ */
206
+ drumPreset;
207
+
208
+ defaultDrumsUsesOverride = false;
209
+
194
210
  /**
195
211
  * Creates a new worklet synthesis system. contains all channels
196
212
  * @param options {{
@@ -235,8 +251,7 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
235
251
  }
236
252
  this.sendPresetList();
237
253
 
238
- this.defaultPreset = this.getPreset(0, 0);
239
- this.drumPreset = this.getPreset(128, 0);
254
+ this.getDefaultPresets();
240
255
 
241
256
 
242
257
  for (let i = 0; i < options.processorOptions.midiChannels; i++)
@@ -303,6 +318,18 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
303
318
  return this.masterGain * this.midiVolume;
304
319
  }
305
320
 
321
+ getDefaultPresets()
322
+ {
323
+ // override this to XG, to set the default preset to NOT be XG drums!
324
+ const sys = this.system;
325
+ this.system = "xg";
326
+ this.defaultPreset = this.getPreset(0, 0);
327
+ this.defaultPresetUsesOverride = this.overrideSoundfont?.presets?.indexOf(this.defaultPreset) >= 0;
328
+ this.system = sys;
329
+ this.drumPreset = this.getPreset(128, 0);
330
+ this.defaultDrumsUsesOverride = this.overrideSoundfont?.presets?.indexOf(this.drumPreset) >= 0;
331
+ }
332
+
306
333
  /**
307
334
  * @param value {SynthSystem}
308
335
  */
@@ -186,7 +186,7 @@ export function handleMessage(message)
186
186
  case workletMessageType.lockController:
187
187
  if (data[0] === ALL_CHANNELS_OR_DIFFERENT_ACTION)
188
188
  {
189
- channelObject.lockPreset = data[1];
189
+ channelObject.setPresetLock(data[1]);
190
190
  }
191
191
  else
192
192
  {
@@ -15,6 +15,12 @@ export class ChannelSnapshot
15
15
  */
16
16
  bank;
17
17
 
18
+ /**
19
+ * If the bank is LSB. For restoring.
20
+ * @type {boolean}
21
+ */
22
+ isBankLSB;
23
+
18
24
  /**
19
25
  * The name of the patch currently loaded in the channel.
20
26
  * @type {string}
@@ -27,6 +33,12 @@ export class ChannelSnapshot
27
33
  */
28
34
  lockPreset;
29
35
 
36
+ /**
37
+ * Indicates the MIDI system when the preset was locked
38
+ * @type {SynthSystem}
39
+ */
40
+ lockedSystem;
41
+
30
42
  /**
31
43
  * The array of all MIDI controllers (in 14-bit values) with the modulator sources at the end.
32
44
  * @type {Int16Array}
@@ -103,7 +115,9 @@ export class ChannelSnapshot
103
115
  // program data
104
116
  channelSnapshot.program = channelObject.preset.program;
105
117
  channelSnapshot.bank = channelObject.getBankSelect();
118
+ channelSnapshot.isBankLSB = channelSnapshot.bank !== channelObject.bank;
106
119
  channelSnapshot.lockPreset = channelObject.lockPreset;
120
+ channelSnapshot.lockedSystem = channelObject.lockedSystem;
107
121
  channelSnapshot.patchName = channelObject.preset.presetName;
108
122
 
109
123
  // controller data
@@ -152,9 +166,10 @@ export class ChannelSnapshot
152
166
  channelObject.velocityOverride = channelSnapshot.velocityOverride;
153
167
 
154
168
  // restore preset and lock
155
- channelObject.lockPreset = false;
156
- channelObject.setBankSelect(channelSnapshot.bank, true);
169
+ channelObject.setPresetLock(false);
170
+ channelObject.setBankSelect(channelSnapshot.bank, channelSnapshot.isBankLSB);
157
171
  channelObject.programChange(channelSnapshot.program);
158
- channelObject.lockPreset = channelSnapshot.lockPreset;
172
+ channelObject.setPresetLock(channelSnapshot.lockPreset);
173
+ channelObject.lockedSystem = channelSnapshot.lockedSystem;
159
174
  }
160
175
  }
@@ -66,18 +66,11 @@ export function controllerChange(controllerNumber, controllerValue, force = fals
66
66
 
67
67
  // special case: bank select
68
68
  case midiControllers.bankSelect:
69
- if (!force)
70
- {
71
- this.setBankSelect(controllerValue);
72
- }
73
- else
74
- {
75
- this.bank = controllerValue;
76
- }
69
+ this.setBankSelect(controllerValue);
77
70
  break;
78
71
 
79
72
  case midiControllers.lsbForControl0BankSelect:
80
- this.setBankSelect(controllerValue, false, true);
73
+ this.setBankSelect(controllerValue, true);
81
74
  break;
82
75
 
83
76
  // check for RPN and NPRN and data entry
@@ -38,11 +38,11 @@ export function resetAllControllers(log = true)
38
38
  // if preset is unlocked, switch to non-drums and call event
39
39
  if (!ch.lockPreset)
40
40
  {
41
- ch.presetUsesOverride = true;
42
- ch.setBankSelect(0, true);
41
+ ch.setBankSelect(0);
43
42
  if (channelNumber % 16 === DEFAULT_PERCUSSION)
44
43
  {
45
- this.workletProcessorChannels[channelNumber].setPreset(this.drumPreset);
44
+ ch.setPreset(this.drumPreset);
45
+ ch.presetUsesOverride = this.defaultDrumsUsesOverride;
46
46
  ch.drumChannel = true;
47
47
  this.callEvent("drumchange", {
48
48
  channel: channelNumber,
@@ -52,6 +52,7 @@ export function resetAllControllers(log = true)
52
52
  else
53
53
  {
54
54
  ch.drumChannel = false;
55
+ ch.presetUsesOverride = this.defaultDrumsUsesOverride;
55
56
  ch.setPreset(this.defaultPreset);
56
57
  this.callEvent("drumchange", {
57
58
  channel: channelNumber,
@@ -67,11 +68,14 @@ export function resetAllControllers(log = true)
67
68
  });
68
69
  }
69
70
 
71
+ const presetBank = ch.preset.bank;
72
+ const sentBank = presetBank === 128 ? 128 : (ch.presetUsesOverride ? presetBank + this.soundfontBankOffset : presetBank);
73
+
70
74
  // call program change
71
75
  this.callEvent("programchange", {
72
76
  channel: channelNumber,
73
77
  program: ch.preset.program,
74
- bank: ch.getBankSelect(),
78
+ bank: sentBank,
75
79
  userCalled: false
76
80
  });
77
81
 
@@ -11,31 +11,36 @@ export function programChange(programNumber, userChange = false)
11
11
  return;
12
12
  }
13
13
  // always 128 for percussion
14
- const bank = this.getBankSelect();
14
+ let bank = this.getBankSelect();
15
15
  let sentBank;
16
16
  let preset;
17
17
 
18
+ const isXG = this.isXGChannel;
18
19
  // check if override
19
20
  if (this.synth.overrideSoundfont)
20
21
  {
21
22
  const bankWithOffset = bank === 128 ? 128 : bank - this.synth.soundfontBankOffset;
22
- const p = this.synth.overrideSoundfont.getPresetNoFallback(bankWithOffset, programNumber);
23
+ const p = this.synth.overrideSoundfont.getPresetNoFallback(
24
+ bankWithOffset,
25
+ programNumber,
26
+ isXG
27
+ );
23
28
  if (p)
24
29
  {
25
- sentBank = bank;
30
+ sentBank = p.bank === 128 ? 128 : p.bank + this.synth.soundfontBankOffset;
26
31
  preset = p;
27
32
  this.presetUsesOverride = true;
28
33
  }
29
34
  else
30
35
  {
31
- preset = this.synth.soundfontManager.getPreset(bank, programNumber);
36
+ preset = this.synth.soundfontManager.getPreset(bank, programNumber, isXG);
32
37
  sentBank = preset.bank;
33
38
  this.presetUsesOverride = false;
34
39
  }
35
40
  }
36
41
  else
37
42
  {
38
- preset = this.synth.soundfontManager.getPreset(bank, programNumber);
43
+ preset = this.synth.soundfontManager.getPreset(bank, programNumber, isXG);
39
44
  sentBank = preset.bank;
40
45
  this.presetUsesOverride = false;
41
46
  }
@@ -11,8 +11,7 @@ export function clearSoundFont(sendPresets = true, clearOverride = true)
11
11
  delete this.overrideSoundfont;
12
12
  this.overrideSoundfont = undefined;
13
13
  }
14
- this.defaultPreset = this.getPreset(0, 0);
15
- this.drumPreset = this.getPreset(128, 0);
14
+ this.getDefaultPresets();
16
15
  this.cachedVoices = [];
17
16
 
18
17
  for (let i = 0; i < this.workletProcessorChannels.length; i++)
@@ -20,7 +19,7 @@ export function clearSoundFont(sendPresets = true, clearOverride = true)
20
19
  const channelObject = this.workletProcessorChannels[i];
21
20
  if (!clearOverride || (clearOverride && channelObject.presetUsesOverride))
22
21
  {
23
- channelObject.lockPreset = false;
22
+ channelObject.setPresetLock(false);
24
23
  }
25
24
  channelObject.programChange(channelObject.preset.program);
26
25
  }
@@ -10,11 +10,11 @@ export function getPreset(bank, program)
10
10
  {
11
11
  // if override soundfont
12
12
  const bankWithOffset = bank === 128 ? 128 : bank - this.soundfontBankOffset;
13
- const preset = this.overrideSoundfont.getPresetNoFallback(bankWithOffset, program);
13
+ const preset = this.overrideSoundfont.getPresetNoFallback(bankWithOffset, program, this.system === "xg");
14
14
  if (preset)
15
15
  {
16
16
  return preset;
17
17
  }
18
18
  }
19
- return this.soundfontManager.getPreset(bank, program);
19
+ return this.soundfontManager.getPreset(bank, program, this.system === "xg");
20
20
  }
@@ -32,8 +32,7 @@ export function reloadSoundFont(buffer, isOverride = false)
32
32
  });
33
33
  return;
34
34
  }
35
- this.defaultPreset = this.getPreset(0, 0);
36
- this.drumPreset = this.getPreset(128, 0);
35
+ this.getDefaultPresets();
37
36
  this.workletProcessorChannels.forEach(c =>
38
37
  c.programChange(c.preset.program)
39
38
  );
@@ -1,6 +1,7 @@
1
1
  import { SpessaSynthWarn } from "../../../../utils/loggin.js";
2
2
  import { WorkletSoundfontManagerMessageType } from "./sfman_message.js";
3
3
  import { loadSoundFont } from "../../../../soundfont/load_soundfont.js";
4
+ import { isXGDrums } from "../../../../utils/xg_hacks.js";
4
5
 
5
6
  /**
6
7
  * @typedef {Object} SoundFontType
@@ -194,9 +195,10 @@ export class WorkletSoundfontManager
194
195
  * Gets a given preset from the soundfont stack
195
196
  * @param bankNumber {number}
196
197
  * @param programNumber {number}
198
+ * @param allowXGDrums {boolean} if true, allows XG drum banks (120, 126 and 127) as drum preset
197
199
  * @returns {BasicPreset} the preset
198
200
  */
199
- getPreset(bankNumber, programNumber)
201
+ getPreset(bankNumber, programNumber, allowXGDrums = false)
200
202
  {
201
203
  if (this.soundfontList.length < 1)
202
204
  {
@@ -205,19 +207,25 @@ export class WorkletSoundfontManager
205
207
  for (const sf of this.soundfontList)
206
208
  {
207
209
  // check for the preset (with given offset)
208
- const preset = sf.soundfont.getPresetNoFallback(bankNumber - sf.bankOffset, programNumber);
210
+ const preset = sf.soundfont.getPresetNoFallback(
211
+ bankNumber - sf.bankOffset,
212
+ programNumber,
213
+ allowXGDrums
214
+ );
209
215
  if (preset !== undefined)
210
216
  {
211
217
  return preset;
212
218
  }
213
219
  // if not found, advance to the next soundfont
214
220
  }
221
+ const isDrum = bankNumber === 128 || (allowXGDrums && isXGDrums(bankNumber));
215
222
  // if none found, return the first correct preset found
216
- if (bankNumber !== 128)
223
+ if (!isDrum)
217
224
  {
218
225
  for (const sf of this.soundfontList)
219
226
  {
220
- const preset = sf.soundfont.presets.find(p => p.program === programNumber);
227
+ const preset = sf.soundfont.presets.find(p => p.program === programNumber && !p.isDrumPreset(
228
+ allowXGDrums));
221
229
  if (preset)
222
230
  {
223
231
  return preset;
@@ -230,7 +238,14 @@ export class WorkletSoundfontManager
230
238
  {
231
239
  for (const sf of this.soundfontList)
232
240
  {
233
- const preset = sf.soundfont.presets.find(p => p.bank === 128);
241
+ // check for any drum type (127/128) and matching program
242
+ const p = sf.soundfont.presets.find(p => p.isDrumPreset(allowXGDrums) && p.program === programNumber);
243
+ if (p)
244
+ {
245
+ return p;
246
+ }
247
+ // check for any drum preset
248
+ const preset = sf.soundfont.presets.find(p => p.isDrumPreset(allowXGDrums));
234
249
  if (preset)
235
250
  {
236
251
  return preset;
@@ -27,7 +27,8 @@ import { channelPressure } from "../worklet_methods/tuning_control/channel_press
27
27
  import { pitchWheel } from "../worklet_methods/tuning_control/pitch_wheel.js";
28
28
  import { setOctaveTuning } from "../worklet_methods/tuning_control/set_octave_tuning.js";
29
29
  import { programChange } from "../worklet_methods/program_change.js";
30
- import { parseBankSelect } from "../../../utils/xg_hacks.js";
30
+ import { chooseBank, parseBankSelect } from "../../../utils/xg_hacks.js";
31
+ import { DEFAULT_PERCUSSION } from "../../synth_constants.js";
31
32
 
32
33
  /**
33
34
  * This class represents a single MIDI Channel within the synthesizer.
@@ -114,6 +115,12 @@ class WorkletProcessorChannel
114
115
  */
115
116
  bank = 0;
116
117
 
118
+ /**
119
+ * The bank LSB number of the channel (used for patch changes in XG mode).
120
+ * @type {number}
121
+ */
122
+ bankLSB = 0;
123
+
117
124
  /**
118
125
  * The preset currently assigned to the channel.
119
126
  * @type {BasicPreset}
@@ -126,6 +133,12 @@ class WorkletProcessorChannel
126
133
  */
127
134
  lockPreset = false;
128
135
 
136
+ /**
137
+ * Indicates the MIDI system when the preset was locked.
138
+ * @type {SynthSystem}
139
+ */
140
+ lockedSystem = "gs";
141
+
129
142
  /**
130
143
  * Indicates whether the channel uses a preset from the override soundfont.
131
144
  * @type {boolean}
@@ -190,6 +203,11 @@ class WorkletProcessorChannel
190
203
  this.channelNumber = channelNumber;
191
204
  }
192
205
 
206
+ get isXGChannel()
207
+ {
208
+ return this.synth.system === "xg" || (this.lockPreset && this.lockedSystem === "xg");
209
+ }
210
+
193
211
  /**
194
212
  * @param type {customControllers|number}
195
213
  * @param value {number}
@@ -231,32 +249,43 @@ class WorkletProcessorChannel
231
249
  ));
232
250
  }
233
251
 
252
+ /**
253
+ * @param locked {boolean}
254
+ */
255
+ setPresetLock(locked)
256
+ {
257
+ this.lockPreset = locked;
258
+ if (locked)
259
+ {
260
+ this.lockedSystem = this.synth.system;
261
+ }
262
+ }
263
+
234
264
  /**
235
265
  * @param bank {number}
236
- * @param force {boolean}
237
266
  * @param isLSB {boolean}
238
267
  */
239
- setBankSelect(bank, force = false, isLSB = false)
268
+ setBankSelect(bank, isLSB = false)
240
269
  {
241
270
  if (this.lockPreset)
242
271
  {
243
272
  return;
244
273
  }
245
- if (force)
274
+ if (isLSB)
246
275
  {
247
- this.bank = bank;
276
+ this.bankLSB = bank;
248
277
  }
249
278
  else
250
279
  {
280
+ this.bank = bank;
251
281
  const bankLogic = parseBankSelect(
252
282
  this.getBankSelect(),
253
283
  bank,
254
284
  this.synth.system,
255
- isLSB,
285
+ false,
256
286
  this.drumChannel,
257
287
  this.channelNumber
258
288
  );
259
- this.bank = bankLogic.newBank;
260
289
  switch (bankLogic.drumsStatus)
261
290
  {
262
291
  default:
@@ -264,7 +293,11 @@ class WorkletProcessorChannel
264
293
  break;
265
294
 
266
295
  case 1:
267
- this.setDrums(false);
296
+ if (this.channelNumber % 16 === DEFAULT_PERCUSSION)
297
+ {
298
+ // cannot disable drums on channel 9
299
+ this.bank = 127;
300
+ }
268
301
  break;
269
302
 
270
303
  case 2:
@@ -279,11 +312,7 @@ class WorkletProcessorChannel
279
312
  */
280
313
  getBankSelect()
281
314
  {
282
- if (this.drumChannel)
283
- {
284
- return 128;
285
- }
286
- return this.bank;
315
+ return chooseBank(this.bank, this.bankLSB, this.drumChannel, this.isXGChannel);
287
316
  }
288
317
 
289
318
  /**
@@ -319,23 +348,17 @@ class WorkletProcessorChannel
319
348
  // clear transpose
320
349
  this.channelTransposeKeyShift = 0;
321
350
  this.drumChannel = true;
322
- this.setPreset(this.synth.getPreset(this.getBankSelect(), this.preset.program));
323
351
  }
324
352
  else
325
353
  {
326
354
  this.drumChannel = false;
327
- this.setPreset(
328
- this.synth.getPreset(
329
- this.getBankSelect(),
330
- this.preset.program
331
- )
332
- );
333
355
  }
334
356
  this.presetUsesOverride = false;
335
357
  this.synth.callEvent("drumchange", {
336
358
  channel: this.channelNumber,
337
359
  isDrumChannel: this.drumChannel
338
360
  });
361
+ this.programChange(this.preset.program);
339
362
  this.synth.sendChannelProperties();
340
363
  }
341
364
 
@@ -408,7 +408,7 @@ export function getWorkletVoices(channel,
408
408
  let preset = channelObject.preset;
409
409
  if (overridePatch)
410
410
  {
411
- preset = this.soundfontManager.getPreset(bank, program);
411
+ preset = this.soundfontManager.getPreset(bank, program, this.system === "xg");
412
412
  }
413
413
  /**
414
414
  * @returns {WorkletVoice[]}
@@ -503,8 +503,7 @@ export function getWorkletVoices(channel,
503
503
  targetKey,
504
504
  realKey,
505
505
  generators,
506
- sampleAndGenerators.modulators.map(m => Modulator.copy(m)),
507
- this.filterSmoothingFactor
506
+ sampleAndGenerators.modulators.map(m => Modulator.copy(m))
508
507
  )
509
508
  );
510
509
  return voices;
package/utils/xg_hacks.js CHANGED
@@ -2,6 +2,8 @@ import { SpessaSynthInfo } from "./loggin.js";
2
2
  import { consoleColors } from "./other.js";
3
3
  import { DEFAULT_PERCUSSION } from "../synthetizer/synth_constants.js";
4
4
 
5
+ export const XG_SFX_VOICE = 64;
6
+
5
7
  /**
6
8
  * @param bankNr {number}
7
9
  * @returns {boolean}
@@ -11,6 +13,15 @@ export function isXGDrums(bankNr)
11
13
  return bankNr === 120 || bankNr === 126 || bankNr === 127;
12
14
  }
13
15
 
16
+ /**
17
+ * @param bank {number}
18
+ * @returns {boolean}
19
+ */
20
+ export function isValidXGMSB(bank)
21
+ {
22
+ return isXGDrums(bank) || bank === XG_SFX_VOICE;
23
+ }
24
+
14
25
  /**
15
26
  * Bank select hacks abstracted here
16
27
  * @param bankBefore {number} the current bank number
@@ -29,12 +40,11 @@ export function parseBankSelect(bankBefore, bank, system, isLSB, isDrums, channe
29
40
  // 64 means SFX in MSB, so it is allowed
30
41
  let out = bankBefore;
31
42
  let drumsStatus = 0;
32
- const isValidMSB = b => isXGDrums(b) || b === 64;
33
43
  if (isLSB)
34
44
  {
35
45
  if (system === "xg")
36
46
  {
37
- if (!isValidMSB(bank))
47
+ if (!isValidXGMSB(bank))
38
48
  {
39
49
  out = bank;
40
50
  }
@@ -59,7 +69,7 @@ export function parseBankSelect(bankBefore, bank, system, isLSB, isDrums, channe
59
69
  break;
60
70
 
61
71
  case "xg":
62
- canSetBankSelect = isValidMSB(bank);
72
+ canSetBankSelect = isValidXGMSB(bank);
63
73
  // for xg, if msb is 120, 126 or 127, then it's drums
64
74
  if (isXGDrums(bank))
65
75
  {
@@ -112,17 +122,55 @@ export function parseBankSelect(bankBefore, bank, system, isLSB, isDrums, channe
112
122
 
113
123
 
114
124
  /**
125
+ * Chooses a bank number according to spessasynth logic
126
+ * That is:
127
+ * for GS, bank MSB if not drum, otherwise 128
128
+ * for XG: bank MSB if drum and MSB is valid, 128 othewise, bank MSB if it is SFX voice, LSB otherwise
115
129
  * @param msb {number}
116
130
  * @param lsb {number}
131
+ * @param isDrums {boolean}
132
+ * @param isXG {boolean}
133
+ * @returns {number}
117
134
  */
118
- export function chooseBank(msb, lsb)
135
+ export function chooseBank(msb, lsb, isDrums, isXG)
119
136
  {
120
- if (lsb > 0)
137
+ if (isXG)
121
138
  {
122
- if (!isXGDrums(msb) && msb !== 64)
139
+ if (isDrums)
123
140
  {
124
- return lsb;
141
+ if (isXGDrums(msb))
142
+ {
143
+ return msb;
144
+ }
145
+ else
146
+ {
147
+ return 128;
148
+ }
125
149
  }
150
+ else
151
+ {
152
+ // check for SFX
153
+ if (msb !== XG_SFX_VOICE)
154
+ {
155
+ // if lsb is 0 and msb is not, use that
156
+ if (lsb === 0 && msb !== 0)
157
+ {
158
+ return msb;
159
+ }
160
+ if (!isValidXGMSB(lsb))
161
+ {
162
+ return lsb;
163
+ }
164
+ return 0;
165
+ }
166
+ else
167
+ {
168
+ return XG_SFX_VOICE;
169
+ }
170
+ }
171
+ }
172
+ else
173
+ {
174
+ return isDrums ? 128 : msb;
126
175
  }
127
- return msb;
128
176
  }