spessasynth_core 3.26.9 → 3.26.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.
package/README.md CHANGED
@@ -63,6 +63,7 @@ npm install --save spessasynth_core
63
63
  - **Sound Controllers:** Real-time filter and envelope control!
64
64
  - **MIDI Tuning Standard Support:** [more info here](https://github.com/spessasus/spessasynth_core/wiki/MIDI-Implementation#midi-tuning-standard)
65
65
  - [Full **RPN** and limited **NRPN** support](https://github.com/spessasus/spessasynth_core/wiki/MIDI-Implementation#supported-registered-parameters)
66
+ - [**AWE32** NRPN Compatibility Layer](https://github.com/spessasus/spessasynth_core/wiki/MIDI-Implementation#awe32-nrpn-compatibility-layer)
66
67
  - Supports some [**Roland GS** and **Yamaha XG** system exclusives](https://github.com/spessasus/spessasynth_core/wiki/MIDI-Implementation#supported-system-exclusives)
67
68
 
68
69
  ### Powerful and Fast MIDI Sequencer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spessasynth_core",
3
- "version": "3.26.9",
3
+ "version": "3.26.11",
4
4
  "description": "MIDI and SoundFont2/DLS library with no compromises",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -18,14 +18,6 @@ export class DynamicModulatorSystem
18
18
  this.modulatorList = [];
19
19
  }
20
20
 
21
- /**
22
- * @returns {Modulator[]}
23
- */
24
- getModulators()
25
- {
26
- return this.modulatorList.map(m => m.mod);
27
- }
28
-
29
21
  /**
30
22
  * @param source {number}
31
23
  * @param destination {generatorTypes}
@@ -29,9 +29,11 @@ import { pitchWheel } from "../engine_methods/tuning_control/pitch_wheel.js";
29
29
  import { setOctaveTuning } from "../engine_methods/tuning_control/set_octave_tuning.js";
30
30
  import { programChange } from "../engine_methods/program_change.js";
31
31
  import { chooseBank, isSystemXG, parseBankSelect } from "../../../utils/xg_hacks.js";
32
- import { DEFAULT_PERCUSSION } from "../../synth_constants.js";
32
+ import { DEFAULT_PERCUSSION, GENERATOR_OVERRIDE_NO_CHANGE_VALUE } from "../../synth_constants.js";
33
33
  import { modulatorSources } from "../../../soundfont/basic_soundfont/modulator.js";
34
34
  import { DynamicModulatorSystem } from "./dynamic_modulator_system.js";
35
+ import { GENERATORS_AMOUNT } from "../../../soundfont/basic_soundfont/generator.js";
36
+ import { computeModulators } from "./compute_modulator.js";
35
37
 
36
38
  /**
37
39
  * This class represents a single MIDI Channel within the synthesizer.
@@ -83,11 +85,24 @@ class MidiAudioChannel
83
85
  channelTuningCents = 0;
84
86
 
85
87
  /**
86
- * A system for dynamic modulator assignment for system exclusives
88
+ * A system for dynamic modulator assignment for advanced system exclusives.
87
89
  * @type {DynamicModulatorSystem}
88
90
  */
89
91
  sysExModulators = new DynamicModulatorSystem();
90
92
 
93
+ /**
94
+ * An array of override generators for AWE32 support.
95
+ * A value of 32,767 means unchanged, as it is not allowed anywhere.
96
+ * @type {Int16Array}
97
+ */
98
+ generatorOverrides = new Int16Array(GENERATORS_AMOUNT);
99
+
100
+ /**
101
+ * A small optimization that disables applying overrides until at least one is set.
102
+ * @type {boolean}
103
+ */
104
+ generatorOverridesEnabled = false;
105
+
91
106
  /**
92
107
  * Indicates whether the sustain (hold) pedal is active.
93
108
  * @type {boolean}
@@ -210,6 +225,7 @@ class MidiAudioChannel
210
225
  this.synth = synth;
211
226
  this.preset = preset;
212
227
  this.channelNumber = channelNumber;
228
+ this.resetGeneratorOverrides();
213
229
  }
214
230
 
215
231
  get isXGChannel()
@@ -438,6 +454,31 @@ class MidiAudioChannel
438
454
  };
439
455
  this.synth?.onChannelPropertyChange?.(data, this.channelNumber);
440
456
  }
457
+
458
+ resetGeneratorOverrides()
459
+ {
460
+ this.generatorOverrides.fill(GENERATOR_OVERRIDE_NO_CHANGE_VALUE);
461
+ this.generatorOverridesEnabled = false;
462
+ }
463
+
464
+ /**
465
+ * @param gen {generatorTypes}
466
+ * @param value {number}
467
+ * @param realtime {boolean}
468
+ */
469
+ setGeneratorOverride(gen, value, realtime = false)
470
+ {
471
+ this.generatorOverrides[gen] = value;
472
+ this.generatorOverridesEnabled = true;
473
+ if (realtime)
474
+ {
475
+ this.voices.forEach(v =>
476
+ {
477
+ v.generators[gen] = value;
478
+ computeModulators(v, this.midiControllers);
479
+ });
480
+ }
481
+ }
441
482
  }
442
483
 
443
484
  // voice
@@ -134,13 +134,19 @@ export class SoundFontManager
134
134
  {
135
135
  if (this.soundfontList.find(s => s.id === id) !== undefined)
136
136
  {
137
- throw new Error("Cannot overwrite the existing soundfont. Use soundfontManager.delete(id) instead.");
137
+ // replace
138
+ const soundfont = this.soundfontList.find(s => s.id === id);
139
+ soundfont.soundfont = font;
140
+ soundfont.bankOffset = bankOffset;
141
+ }
142
+ else
143
+ {
144
+ this.soundfontList.push({
145
+ id: id,
146
+ soundfont: font,
147
+ bankOffset: bankOffset
148
+ });
138
149
  }
139
- this.soundfontList.push({
140
- id: id,
141
- soundfont: font,
142
- bankOffset: bankOffset
143
- });
144
150
  this.generatePresetList();
145
151
  }
146
152
 
@@ -219,6 +219,7 @@ export function resetControllersRP15Compliant()
219
219
  }
220
220
  }
221
221
  }
222
+ this.resetGeneratorOverrides();
222
223
  }
223
224
 
224
225
  /**
@@ -228,9 +229,13 @@ export function resetParameters()
228
229
  {
229
230
  /**
230
231
  * reset the state machine to idle
231
- * @type {string}
232
232
  */
233
233
  this.dataEntryState = dataEntryStates.Idle;
234
+ this.midiControllers[midiControllers.NRPNLsb] = 127 << 7;
235
+ this.midiControllers[midiControllers.NRPNMsb] = 127 << 7;
236
+ this.midiControllers[midiControllers.RPNLsb] = 127 << 7;
237
+ this.midiControllers[midiControllers.RPNMsb] = 127 << 7;
238
+ this.resetGeneratorOverrides();
234
239
  SpessaSynthInfo(
235
240
  "%cResetting Registered and Non-Registered Parameters!",
236
241
  consoleColors.info
@@ -0,0 +1,198 @@
1
+ import { generatorTypes } from "../../../../soundfont/basic_soundfont/generator.js";
2
+ import { SpessaSynthWarn } from "../../../../utils/loggin.js";
3
+ import { consoleColors } from "../../../../utils/other.js";
4
+
5
+ /**
6
+ * http://archive.gamedev.net/archive/reference/articles/article445.html
7
+ * https://github.com/user-attachments/files/15757220/adip301.pdf
8
+ * @type {generatorTypes[]}
9
+ */
10
+ const AWE_NRPN_GENERATOR_MAPPINGS = [
11
+ generatorTypes.delayModLFO,
12
+ generatorTypes.freqModLFO,
13
+
14
+ generatorTypes.delayVibLFO,
15
+ generatorTypes.freqVibLFO,
16
+
17
+ generatorTypes.delayModEnv,
18
+ generatorTypes.attackModEnv,
19
+ generatorTypes.holdModEnv,
20
+ generatorTypes.decayModEnv,
21
+ generatorTypes.sustainModEnv,
22
+ generatorTypes.releaseModEnv,
23
+
24
+ generatorTypes.delayVolEnv,
25
+ generatorTypes.attackVolEnv,
26
+ generatorTypes.holdVolEnv,
27
+ generatorTypes.decayVolEnv,
28
+ generatorTypes.sustainVolEnv,
29
+ generatorTypes.releaseVolEnv,
30
+
31
+ generatorTypes.fineTune,
32
+
33
+ generatorTypes.modLfoToPitch,
34
+ generatorTypes.vibLfoToPitch,
35
+ generatorTypes.modEnvToPitch,
36
+ generatorTypes.modLfoToVolume,
37
+
38
+ generatorTypes.initialFilterFc,
39
+ generatorTypes.initialFilterQ,
40
+
41
+ generatorTypes.modLfoToFilterFc,
42
+ generatorTypes.modEnvToFilterFc,
43
+
44
+ generatorTypes.chorusEffectsSend,
45
+ generatorTypes.reverbEffectsSend
46
+ ];
47
+
48
+ /**
49
+ * Function that emulates AWE32 similarly to fluidsynth
50
+ * https://github.com/FluidSynth/fluidsynth/wiki/FluidFeatures
51
+ *
52
+ * Note: This makes use of findings by mrbumpy409:
53
+ * https://github.com/fluidSynth/fluidsynth/issues/1473
54
+ *
55
+ * The excellent test files are available here, also collected and converted by mrbumpy409:
56
+ * https://github.com/mrbumpy409/AWE32-midi-conversions
57
+ * @this {MidiAudioChannel}
58
+ * @param aweGen {number}
59
+ * @param dataLSB {number}
60
+ * @param dataMSB {number}
61
+ */
62
+ export function handleAWE32NRPN(aweGen, dataLSB, dataMSB)
63
+ {
64
+ const clip = (v, min, max) => Math.max(min, Math.min(max, v));
65
+ const msecToTimecents = ms => Math.max(-32768, 1200 * Math.log2(ms / 1000));
66
+ const hzToCents = hz => 6900 + 1200 * Math.log2(hz / 440);
67
+
68
+
69
+ let dataValue = (dataMSB << 7) | dataLSB;
70
+ // center the value
71
+ // though ranges reported as 0 to 127 only use LSB
72
+ dataValue -= 8192;
73
+ const generator = AWE_NRPN_GENERATOR_MAPPINGS[aweGen];
74
+ if (!generator)
75
+ {
76
+ SpessaSynthWarn(
77
+ `Invalid AWE32 LSB: %c${aweGen}`,
78
+ consoleColors.unrecognized
79
+ );
80
+ }
81
+ let milliseconds, hertz, centibels, cents;
82
+ switch (generator)
83
+ {
84
+ default:
85
+ // this should not happen
86
+ break;
87
+
88
+ // delays
89
+ case generatorTypes.delayModLFO:
90
+ case generatorTypes.delayVibLFO:
91
+ case generatorTypes.delayVolEnv:
92
+ case generatorTypes.delayModEnv:
93
+ milliseconds = 4 * clip(dataValue, 0, 5900);
94
+ // convert to timecents
95
+ this.setGeneratorOverride(generator, msecToTimecents(milliseconds));
96
+ break;
97
+
98
+ // attacks
99
+ case generatorTypes.attackVolEnv:
100
+ case generatorTypes.attackModEnv:
101
+ milliseconds = clip(dataValue, 0, 5940);
102
+ // convert to timecents
103
+ this.setGeneratorOverride(generator, msecToTimecents(milliseconds));
104
+ break;
105
+
106
+ // holds
107
+ case generatorTypes.holdVolEnv:
108
+ case generatorTypes.holdModEnv:
109
+ milliseconds = clip(dataValue, 0, 8191);
110
+ // convert to timecents
111
+ this.setGeneratorOverride(generator, msecToTimecents(milliseconds));
112
+ break;
113
+
114
+ // decays and releases (share clips and units)
115
+ case generatorTypes.decayModEnv:
116
+ case generatorTypes.decayVolEnv:
117
+ case generatorTypes.releaseVolEnv:
118
+ case generatorTypes.releaseModEnv:
119
+ milliseconds = 4 * clip(dataValue, 0, 5940);
120
+ // convert to timecents
121
+ this.setGeneratorOverride(generator, msecToTimecents(milliseconds));
122
+ break;
123
+
124
+ // lfo frequencies
125
+ case generatorTypes.freqVibLFO:
126
+ case generatorTypes.freqModLFO:
127
+ hertz = 0.084 * dataLSB;
128
+ // convert to abs cents
129
+ this.setGeneratorOverride(generator, hzToCents(hertz), true);
130
+ break;
131
+
132
+ // sustains
133
+ case generatorTypes.sustainVolEnv:
134
+ case generatorTypes.sustainModEnv:
135
+ // 0.75 dB is 7.5 cB
136
+ centibels = dataLSB * 7.5;
137
+ this.setGeneratorOverride(generator, centibels);
138
+ break;
139
+
140
+ // pitch
141
+ case generatorTypes.fineTune:
142
+ // data is already centered
143
+ this.setGeneratorOverride(generator, dataValue, true);
144
+ break;
145
+
146
+ // lfo to pitch
147
+ case generatorTypes.modLfoToPitch:
148
+ case generatorTypes.vibLfoToPitch:
149
+ cents = clip(dataValue, -127, 127) * 9.375;
150
+ this.setGeneratorOverride(generator, cents, true);
151
+ break;
152
+
153
+ // env to pitch
154
+ case generatorTypes.modEnvToPitch:
155
+ cents = clip(dataValue, -127, 127) * 9.375;
156
+ this.setGeneratorOverride(generator, cents);
157
+ break;
158
+
159
+ // mod lfo to vol
160
+ case generatorTypes.modLfoToVolume:
161
+ // 0.1875 dB is 1.875 cB
162
+ centibels = 1.875 * dataLSB;
163
+ this.setGeneratorOverride(generator, centibels, true);
164
+ break;
165
+
166
+ // filter fc
167
+ case generatorTypes.initialFilterFc:
168
+ // minimum: 100 Hz -> 4335 cents
169
+ const fcCents = 4335 + 59 * dataLSB;
170
+ this.setGeneratorOverride(generator, fcCents, true);
171
+ break;
172
+
173
+ // filter Q
174
+ case generatorTypes.initialFilterQ:
175
+ // note: this uses the "modulator-ish" approach proposed by mrbumpy409
176
+ // here https://github.com/FluidSynth/fluidsynth/issues/1473
177
+ centibels = 215 * (dataLSB / 127);
178
+ this.setGeneratorOverride(generator, centibels, true);
179
+ break;
180
+
181
+ // to filterFc
182
+ case generatorTypes.modLfoToFilterFc:
183
+ cents = clip(dataValue, -64, 63) * 56.25;
184
+ this.setGeneratorOverride(generator, cents, true);
185
+ break;
186
+
187
+ case generatorTypes.modEnvToFilterFc:
188
+ cents = clip(dataValue, -64, 63) * 56.25;
189
+ this.setGeneratorOverride(generator, cents);
190
+ break;
191
+
192
+ // effects
193
+ case generatorTypes.chorusEffectsSend:
194
+ case generatorTypes.reverbEffectsSend:
195
+ this.setGeneratorOverride(generator, clip(dataValue, 0, 255) * (1000 / 255));
196
+ break;
197
+ }
198
+ }
@@ -8,7 +8,7 @@ import { modulatorSources } from "../../../../soundfont/basic_soundfont/modulato
8
8
  /**
9
9
  * @enum {number}
10
10
  */
11
- const registeredParameterTypes = {
11
+ export const registeredParameterTypes = {
12
12
  pitchBendRange: 0x0000,
13
13
  fineTuning: 0x0001,
14
14
  coarseTuning: 0x0002,
@@ -19,8 +19,9 @@ const registeredParameterTypes = {
19
19
  /**
20
20
  * @enum {number}
21
21
  */
22
- const nonRegisteredGSMSB = {
23
- partParameter: 0x01
22
+ export const nonRegisteredMSB = {
23
+ partParameter: 0x01,
24
+ awe32: 0x7F
24
25
  };
25
26
 
26
27
  /**
@@ -50,6 +51,8 @@ const nonRegisteredGSLSB = {
50
51
  */
51
52
  export function dataEntryCoarse(dataValue)
52
53
  {
54
+ // store in cc table
55
+ this.midiControllers[midiControllers.dataEntryMsb] = dataValue << 7;
53
56
  const addDefaultVibrato = () =>
54
57
  {
55
58
  if (this.channelVibrato.delay === 0 && this.channelVibrato.rate === 0 && this.channelVibrato.depth === 0)
@@ -117,7 +120,7 @@ export function dataEntryCoarse(dataValue)
117
120
  break;
118
121
 
119
122
  // part parameters: vibrato, cutoff
120
- case nonRegisteredGSMSB.partParameter:
123
+ case nonRegisteredMSB.partParameter:
121
124
  switch (NRPNFine)
122
125
  {
123
126
  default:
@@ -193,6 +196,9 @@ export function dataEntryCoarse(dataValue)
193
196
  break;
194
197
  }
195
198
  break;
199
+
200
+ case nonRegisteredMSB.awe32:
201
+ break;
196
202
  }
197
203
  break;
198
204
 
@@ -1,8 +1,10 @@
1
1
  import { consoleColors } from "../../../../utils/other.js";
2
- import { SpessaSynthInfo } from "../../../../utils/loggin.js";
2
+ import { SpessaSynthInfo, SpessaSynthWarn } from "../../../../utils/loggin.js";
3
3
  import { modulatorSources } from "../../../../soundfont/basic_soundfont/modulator.js";
4
4
  import { customControllers, dataEntryStates, NON_CC_INDEX_OFFSET } from "../../engine_components/controller_tables.js";
5
5
  import { midiControllers } from "../../../../midi/midi_message.js";
6
+ import { nonRegisteredMSB, registeredParameterTypes } from "./data_entry_coarse.js";
7
+ import { handleAWE32NRPN } from "./awe32.js";
6
8
 
7
9
  /**
8
10
  * Executes a data entry for an RPN tuning
@@ -12,6 +14,8 @@ import { midiControllers } from "../../../../midi/midi_message.js";
12
14
  */
13
15
  export function dataEntryFine(dataValue)
14
16
  {
17
+ // store in cc table
18
+ this.midiControllers[midiControllers.lsbForControl6DataEntry] = dataValue << 7;
15
19
  switch (this.dataEntryState)
16
20
  {
17
21
  default:
@@ -26,7 +30,7 @@ export function dataEntryFine(dataValue)
26
30
  break;
27
31
 
28
32
  // pitch bend range fine tune
29
- case 0x0000:
33
+ case registeredParameterTypes.pitchBendRange:
30
34
  if (dataValue === 0)
31
35
  {
32
36
  break;
@@ -42,7 +46,7 @@ export function dataEntryFine(dataValue)
42
46
  break;
43
47
 
44
48
  // fine-tuning
45
- case 0x0001:
49
+ case registeredParameterTypes.fineTuning:
46
50
  // grab the data and shift
47
51
  const coarse = this.customControllers[customControllers.channelTuning];
48
52
  const finalTuning = (coarse << 7) | dataValue;
@@ -50,7 +54,7 @@ export function dataEntryFine(dataValue)
50
54
  break;
51
55
 
52
56
  // modulation depth
53
- case 0x0005:
57
+ case registeredParameterTypes.modulationDepth:
54
58
  const currentModulationDepthCents = this.customControllers[customControllers.modulationMultiplier] * 50;
55
59
  let cents = currentModulationDepthCents + (dataValue / 128) * 100;
56
60
  this.setModulationDepth(cents);
@@ -61,6 +65,41 @@ export function dataEntryFine(dataValue)
61
65
  break;
62
66
 
63
67
  }
68
+ break;
64
69
 
70
+ case dataEntryStates.NRPFine:
71
+ /**
72
+ * @type {number}
73
+ */
74
+ const NRPNCoarse = this.midiControllers[midiControllers.NRPNMsb] >> 7;
75
+ /**
76
+ * @type {number}
77
+ */
78
+ const NRPNFine = this.midiControllers[midiControllers.NRPNLsb] >> 7;
79
+ switch (NRPNCoarse)
80
+ {
81
+ default:
82
+ SpessaSynthWarn(
83
+ `%cUnrecognized NRPN LSB for %c${this.channelNumber}%c: %c(0x${NRPNFine.toString(16)
84
+ .toUpperCase()} 0x${NRPNFine.toString(
85
+ 16).toUpperCase()})%c data value: %c${dataValue}`,
86
+ consoleColors.warn,
87
+ consoleColors.recognized,
88
+ consoleColors.warn,
89
+ consoleColors.unrecognized,
90
+ consoleColors.warn,
91
+ consoleColors.value
92
+ );
93
+ break;
94
+
95
+ case nonRegisteredMSB.awe32:
96
+ handleAWE32NRPN.call(
97
+ this,
98
+ NRPNFine,
99
+ dataValue,
100
+ this.midiControllers[midiControllers.dataEntryMsb] >> 7
101
+ );
102
+ break;
103
+ }
65
104
  }
66
105
  }
@@ -4,6 +4,7 @@ import { midiControllers } from "../../../midi/midi_message.js";
4
4
  import { portamentoTimeToSeconds } from "./portamento_time.js";
5
5
  import { customControllers } from "../engine_components/controller_tables.js";
6
6
  import { Modulator } from "../../../soundfont/basic_soundfont/modulator.js";
7
+ import { GENERATOR_OVERRIDE_NO_CHANGE_VALUE } from "../../synth_constants.js";
7
8
 
8
9
  /**
9
10
  * sends a "MIDI Note on message"
@@ -99,9 +100,6 @@ export function noteOn(midiNote, velocity)
99
100
  panOverride = Math.round(Math.random() * 1000 - 500);
100
101
  }
101
102
 
102
- // dynamic modulators (sysEx)
103
- const dynamicModulators = this.sysExModulators.getModulators();
104
-
105
103
  // add voices
106
104
  const channelVoices = this.voices;
107
105
  voices.forEach(voice =>
@@ -116,8 +114,10 @@ export function noteOn(midiNote, velocity)
116
114
  // apply gain override
117
115
  voice.gain = voiceGain;
118
116
 
119
- dynamicModulators.forEach(mod =>
117
+ // dynamic modulators (if none, this won't iterate over anything)
118
+ this.sysExModulators.modulatorList.forEach(m =>
120
119
  {
120
+ const mod = m.mod;
121
121
  const existingModIndex = voice.modulators.findIndex(voiceMod => Modulator.isIdentical(voiceMod, mod));
122
122
 
123
123
  // replace or add
@@ -131,6 +131,19 @@ export function noteOn(midiNote, velocity)
131
131
  }
132
132
  });
133
133
 
134
+ // apply generator override
135
+ if (this.generatorOverridesEnabled)
136
+ {
137
+ this.generatorOverrides.forEach((overrideValue, generatorType) =>
138
+ {
139
+ if (overrideValue === GENERATOR_OVERRIDE_NO_CHANGE_VALUE)
140
+ {
141
+ return;
142
+ }
143
+ voice.generators[generatorType] = overrideValue;
144
+ });
145
+ }
146
+
134
147
 
135
148
  // apply exclusive class
136
149
  const exclusive = voice.exclusiveClass;
@@ -21,4 +21,6 @@ export const DEFAULT_SYNTH_MODE = "gs";
21
21
 
22
22
  export const ALL_CHANNELS_OR_DIFFERENT_ACTION = -1;
23
23
 
24
- export const EMBEDDED_SOUND_BANK_ID = `SPESSASYNTH_EMBEDDED_BANK_${Math.random()}`;
24
+ export const EMBEDDED_SOUND_BANK_ID = `SPESSASYNTH_EMBEDDED_BANK_${Math.random()}`;
25
+
26
+ export const GENERATOR_OVERRIDE_NO_CHANGE_VALUE = 32767;