spessasynth_core 3.26.14 → 3.26.15

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.
Files changed (28) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/src/midi/README.md +1 -1
  4. package/src/midi/basic_midi.js +1 -1
  5. package/src/midi/midi_builder.js +3 -2
  6. package/src/midi/midi_tools/rmidi_writer.js +4 -4
  7. package/src/sequencer/sequencer_engine.js +1 -0
  8. package/src/soundfont/basic_soundfont/generator.js +52 -51
  9. package/src/soundfont/basic_soundfont/riff_chunk.js +1 -1
  10. package/src/soundfont/basic_soundfont/write_dls/ins.js +2 -2
  11. package/src/soundfont/basic_soundfont/write_dls/wave.js +2 -2
  12. package/src/soundfont/basic_soundfont/write_dls/write_dls.js +2 -2
  13. package/src/soundfont/basic_soundfont/write_sf2/write.js +5 -8
  14. package/src/synthetizer/audio_engine/engine_components/compute_modulator.js +15 -6
  15. package/src/synthetizer/audio_engine/engine_components/controller_tables.js +2 -1
  16. package/src/synthetizer/audio_engine/engine_components/lowpass_filter.js +1 -1
  17. package/src/synthetizer/audio_engine/engine_components/midi_audio_channel.js +37 -2
  18. package/src/synthetizer/audio_engine/engine_components/voice.js +2 -1
  19. package/src/synthetizer/audio_engine/engine_components/wavetable_oscillator.js +1 -1
  20. package/src/synthetizer/audio_engine/engine_methods/controller_control/controller_change.js +31 -4
  21. package/src/synthetizer/audio_engine/engine_methods/controller_control/reset_controllers.js +2 -0
  22. package/src/synthetizer/audio_engine/engine_methods/data_entry/data_entry_coarse.js +17 -1
  23. package/src/synthetizer/audio_engine/engine_methods/data_entry/data_entry_fine.js +4 -0
  24. package/src/synthetizer/audio_engine/engine_methods/note_on.js +1 -2
  25. package/src/synthetizer/audio_engine/engine_methods/tuning_control/channel_pressure.js +1 -3
  26. package/src/synthetizer/audio_engine/engine_methods/tuning_control/pitch_wheel.js +1 -3
  27. package/src/synthetizer/audio_engine/engine_methods/tuning_control/poly_pressure.js +1 -3
  28. package/src/utils/byte_functions/string.js +5 -14
package/README.md CHANGED
@@ -69,6 +69,7 @@ npm install --save spessasynth_core
69
69
  - **Sound Controllers:** Real-time filter and envelope control!
70
70
  - **MIDI Tuning Standard Support:** [more info here](https://github.com/spessasus/spessasynth_core/wiki/MIDI-Implementation#midi-tuning-standard)
71
71
  - [Full **RPN** and limited **NRPN** support](https://github.com/spessasus/spessasynth_core/wiki/MIDI-Implementation#supported-registered-parameters)
72
+ - **SoundFont2 NRPN Support**
72
73
  - [**AWE32** NRPN Compatibility Layer](https://github.com/spessasus/spessasynth_core/wiki/MIDI-Implementation#awe32-nrpn-compatibility-layer)
73
74
  - Supports some [**Roland GS** and **Yamaha XG** system exclusives](https://github.com/spessasus/spessasynth_core/wiki/MIDI-Implementation#supported-system-exclusives)
74
75
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spessasynth_core",
3
- "version": "3.26.14",
3
+ "version": "3.26.15",
4
4
  "description": "MIDI and SoundFont2/DLS library with no compromises",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  ## This is the MIDI file parsing folder.
2
2
 
3
- The code here is responsible for parsing the MIDI files and interpreting the messsages.
3
+ The code here is responsible for parsing the MIDI files and interpreting the messages.
4
4
  All the events are defined in the `midi_message.js` file.
5
5
 
6
6
  ### MIDI Classes hierarchy
@@ -402,7 +402,7 @@ class BasicMIDI extends MIDISequenceData
402
402
  }
403
403
  }
404
404
 
405
- // fix empty port channel offsets (do a copy to turn empty slots into undefined so map goes over them)
405
+ // fix empty port channel offsets (do a copy to turn empty slots into undefined so the map goes over them)
406
406
  this.midiPortChannelOffsets = [...this.midiPortChannelOffsets].map(o => o ?? 0);
407
407
 
408
408
  // fix midi ports:
@@ -126,8 +126,9 @@ export class MIDIBuilder extends BasicMIDI
126
126
  * @param track {number} the track number to use
127
127
  * @param channel {number} the channel to use
128
128
  * @param midiNote {number} the midi note of the key release
129
+ * @param velocity {number} optional and unsupported by spessasynth
129
130
  */
130
- addNoteOff(ticks, track, channel, midiNote)
131
+ addNoteOff(ticks, track, channel, midiNote, velocity = 64)
131
132
  {
132
133
  channel %= 16;
133
134
  midiNote %= 128;
@@ -135,7 +136,7 @@ export class MIDIBuilder extends BasicMIDI
135
136
  ticks,
136
137
  track,
137
138
  messageTypes.noteOff | channel,
138
- [midiNote, 64]
139
+ [midiNote, velocity]
139
140
  );
140
141
  }
141
142
 
@@ -1,6 +1,6 @@
1
1
  import { combineArrays, IndexedByteArray } from "../../utils/indexed_array.js";
2
2
  import { writeRIFFOddSize } from "../../soundfont/basic_soundfont/riff_chunk.js";
3
- import { getStringBytes, getStringBytesZero } from "../../utils/byte_functions/string.js";
3
+ import { getStringBytes } from "../../utils/byte_functions/string.js";
4
4
  import { messageTypes, midiControllers, MIDIMessage } from "../midi_message.js";
5
5
  import { getGsOn } from "./midi_editor.js";
6
6
  import { SpessaSynthGroup, SpessaSynthGroupEnd, SpessaSynthInfo } from "../../utils/loggin.js";
@@ -459,7 +459,7 @@ export function writeRMIDI(
459
459
  minute: "numeric"
460
460
  });
461
461
  infoContent.push(
462
- writeRIFFOddSize(RMIDINFOChunks.creationDate, getStringBytesZero(today), true)
462
+ writeRIFFOddSize(RMIDINFOChunks.creationDate, getStringBytes(today, true), true)
463
463
  );
464
464
  }
465
465
  // comment
@@ -525,7 +525,7 @@ export function writeRMIDI(
525
525
  // use midi copyright if possible
526
526
  const copyright = mid.copyright.length > 0 ? mid.copyright : DEFAULT_COPYRIGHT;
527
527
  infoContent.push(
528
- writeRIFFOddSize(RMIDINFOChunks.copyright, getStringBytesZero(copyright))
528
+ writeRIFFOddSize(RMIDINFOChunks.copyright, getStringBytes(copyright, true))
529
529
  );
530
530
  }
531
531
 
@@ -542,7 +542,7 @@ export function writeRMIDI(
542
542
  encoding = FORCED_ENCODING;
543
543
  }
544
544
  // encoding
545
- infoContent.push(writeRIFFOddSize(RMIDINFOChunks.encoding, getStringBytesZero(encoding)));
545
+ infoContent.push(writeRIFFOddSize(RMIDINFOChunks.encoding, getStringBytes(encoding, true)));
546
546
 
547
547
  // combine and write out
548
548
  const infodata = combineArrays(infoContent);
@@ -185,6 +185,7 @@ class SpessaSynthSequencer
185
185
  */
186
186
  _playbackRate = 1;
187
187
 
188
+ // noinspection JSUnusedGlobalSymbols
188
189
  /**
189
190
  * @param value {number}
190
191
  */
@@ -73,81 +73,82 @@ export const GENERATORS_AMOUNT = Object.keys(generatorTypes).length;
73
73
  export const MAX_GENERATOR = Math.max(...Object.values(generatorTypes));
74
74
 
75
75
  /**
76
- * @type {{min: number, max: number, def: number}[]}
76
+ * @type {{min: number, max: number, def: number, nrpn: number}[]}
77
+ * min: minimum value, max: maximum value, def: default value, nrpn: nprn scale...
77
78
  */
78
79
  const generatorLimits = [];
79
80
  // offsets
80
- generatorLimits[generatorTypes.startAddrsOffset] = { min: 0, max: 32768, def: 0 };
81
- generatorLimits[generatorTypes.endAddrOffset] = { min: -32768, max: 32768, def: 0 };
82
- generatorLimits[generatorTypes.startloopAddrsOffset] = { min: -32768, max: 32768, def: 0 };
83
- generatorLimits[generatorTypes.endloopAddrsOffset] = { min: -32768, max: 32768, def: 0 };
84
- generatorLimits[generatorTypes.startAddrsCoarseOffset] = { min: 0, max: 32768, def: 0 };
81
+ generatorLimits[generatorTypes.startAddrsOffset] = { min: 0, max: 32768, def: 0, nrpn: 1 };
82
+ generatorLimits[generatorTypes.endAddrOffset] = { min: -32768, max: 32768, def: 0, nrpn: 1 };
83
+ generatorLimits[generatorTypes.startloopAddrsOffset] = { min: -32768, max: 32768, def: 0, nrpn: 1 };
84
+ generatorLimits[generatorTypes.endloopAddrsOffset] = { min: -32768, max: 32768, def: 0, nrpn: 1 };
85
+ generatorLimits[generatorTypes.startAddrsCoarseOffset] = { min: 0, max: 32768, def: 0, nrpn: 1 };
85
86
 
86
87
  // pitch influence
87
- generatorLimits[generatorTypes.modLfoToPitch] = { min: -12000, max: 12000, def: 0 };
88
- generatorLimits[generatorTypes.vibLfoToPitch] = { min: -12000, max: 12000, def: 0 };
89
- generatorLimits[generatorTypes.modEnvToPitch] = { min: -12000, max: 12000, def: 0 };
88
+ generatorLimits[generatorTypes.modLfoToPitch] = { min: -12000, max: 12000, def: 0, nrpn: 2 };
89
+ generatorLimits[generatorTypes.vibLfoToPitch] = { min: -12000, max: 12000, def: 0, nrpn: 2 };
90
+ generatorLimits[generatorTypes.modEnvToPitch] = { min: -12000, max: 12000, def: 0, nrpn: 2 };
90
91
 
91
92
  // lowpass
92
- generatorLimits[generatorTypes.initialFilterFc] = { min: 1500, max: 13500, def: 13500 };
93
- generatorLimits[generatorTypes.initialFilterQ] = { min: 0, max: 960, def: 0 };
94
- generatorLimits[generatorTypes.modLfoToFilterFc] = { min: -12000, max: 12000, def: 0 };
95
- generatorLimits[generatorTypes.vibLfoToFilterFc] = { min: -12000, max: 12000, def: 0 }; // NON-STANDARD
96
- generatorLimits[generatorTypes.modEnvToFilterFc] = { min: -12000, max: 12000, def: 0 };
93
+ generatorLimits[generatorTypes.initialFilterFc] = { min: 1500, max: 13500, def: 13500, nrpn: 2 };
94
+ generatorLimits[generatorTypes.initialFilterQ] = { min: 0, max: 960, def: 0, nrpn: 1 };
95
+ generatorLimits[generatorTypes.modLfoToFilterFc] = { min: -12000, max: 12000, def: 0, nrpn: 2 };
96
+ generatorLimits[generatorTypes.vibLfoToFilterFc] = { min: -12000, max: 12000, def: 0, nrpn: 2 }; // NON-STANDARD
97
+ generatorLimits[generatorTypes.modEnvToFilterFc] = { min: -12000, max: 12000, def: 0, nrpn: 2 };
97
98
 
98
- generatorLimits[generatorTypes.endAddrsCoarseOffset] = { min: -32768, max: 32768, def: 0 };
99
+ generatorLimits[generatorTypes.endAddrsCoarseOffset] = { min: -32768, max: 32768, def: 0, nrpn: 1 };
99
100
 
100
- generatorLimits[generatorTypes.modLfoToVolume] = { min: -960, max: 960, def: 0 };
101
- generatorLimits[generatorTypes.vibLfoToVolume] = { min: -960, max: 960, def: 0 }; // NON-STANDARD
101
+ generatorLimits[generatorTypes.modLfoToVolume] = { min: -960, max: 960, def: 0, nrpn: 1 };
102
+ generatorLimits[generatorTypes.vibLfoToVolume] = { min: -960, max: 960, def: 0, nrpn: 1 }; // NON-STANDARD
102
103
 
103
104
  // effects, pan
104
- generatorLimits[generatorTypes.chorusEffectsSend] = { min: 0, max: 1000, def: 0 };
105
- generatorLimits[generatorTypes.reverbEffectsSend] = { min: 0, max: 1000, def: 0 };
106
- generatorLimits[generatorTypes.pan] = { min: -500, max: 500, def: 0 };
105
+ generatorLimits[generatorTypes.chorusEffectsSend] = { min: 0, max: 1000, def: 0, nrpn: 1 };
106
+ generatorLimits[generatorTypes.reverbEffectsSend] = { min: 0, max: 1000, def: 0, nrpn: 1 };
107
+ generatorLimits[generatorTypes.pan] = { min: -500, max: 500, def: 0, nrpn: 1 };
107
108
 
108
109
  // lfo
109
- generatorLimits[generatorTypes.delayModLFO] = { min: -12000, max: 5000, def: -12000 };
110
- generatorLimits[generatorTypes.freqModLFO] = { min: -16000, max: 4500, def: 0 };
111
- generatorLimits[generatorTypes.delayVibLFO] = { min: -12000, max: 5000, def: -12000 };
112
- generatorLimits[generatorTypes.freqVibLFO] = { min: -16000, max: 4500, def: 0 };
110
+ generatorLimits[generatorTypes.delayModLFO] = { min: -12000, max: 5000, def: -12000, nrpn: 2 };
111
+ generatorLimits[generatorTypes.freqModLFO] = { min: -16000, max: 4500, def: 0, nrpn: 4 };
112
+ generatorLimits[generatorTypes.delayVibLFO] = { min: -12000, max: 5000, def: -12000, nrpn: 2 };
113
+ generatorLimits[generatorTypes.freqVibLFO] = { min: -16000, max: 4500, def: 0, nrpn: 4 };
113
114
 
114
115
  // mod env
115
- generatorLimits[generatorTypes.delayModEnv] = { min: -32768, max: 5000, def: -32768 }; // -32,768 indicates instant phase,
116
+ generatorLimits[generatorTypes.delayModEnv] = { min: -32768, max: 5000, def: -32768, nrpn: 2 }; // -32,768 indicates instant phase,
116
117
  // this is done to prevent click at the start of filter modenv
117
- generatorLimits[generatorTypes.attackModEnv] = { min: -32768, max: 8000, def: -32768 };
118
- generatorLimits[generatorTypes.holdModEnv] = { min: -12000, max: 5000, def: -12000 };
119
- generatorLimits[generatorTypes.decayModEnv] = { min: -12000, max: 8000, def: -12000 };
120
- generatorLimits[generatorTypes.sustainModEnv] = { min: 0, max: 1000, def: 0 };
121
- generatorLimits[generatorTypes.releaseModEnv] = { min: -7200, max: 8000, def: -12000 }; // min is set to -7200 to prevent lowpass clicks
118
+ generatorLimits[generatorTypes.attackModEnv] = { min: -32768, max: 8000, def: -32768, nrpn: 2 };
119
+ generatorLimits[generatorTypes.holdModEnv] = { min: -12000, max: 5000, def: -12000, nrpn: 2 };
120
+ generatorLimits[generatorTypes.decayModEnv] = { min: -12000, max: 8000, def: -12000, nrpn: 2 };
121
+ generatorLimits[generatorTypes.sustainModEnv] = { min: 0, max: 1000, def: 0, nrpn: 1 };
122
+ generatorLimits[generatorTypes.releaseModEnv] = { min: -7200, max: 8000, def: -12000, nrpn: 2 }; // min is set to -7200 to prevent lowpass clicks
122
123
  // key num to mod env
123
- generatorLimits[generatorTypes.keyNumToModEnvHold] = { min: -1200, max: 1200, def: 0 };
124
- generatorLimits[generatorTypes.keyNumToModEnvDecay] = { min: -1200, max: 1200, def: 0 };
124
+ generatorLimits[generatorTypes.keyNumToModEnvHold] = { min: -1200, max: 1200, def: 0, nrpn: 1 };
125
+ generatorLimits[generatorTypes.keyNumToModEnvDecay] = { min: -1200, max: 1200, def: 0, nrpn: 1 };
125
126
 
126
127
  // vol env
127
- generatorLimits[generatorTypes.delayVolEnv] = { min: -12000, max: 5000, def: -12000 };
128
- generatorLimits[generatorTypes.attackVolEnv] = { min: -12000, max: 8000, def: -12000 };
129
- generatorLimits[generatorTypes.holdVolEnv] = { min: -12000, max: 5000, def: -12000 };
130
- generatorLimits[generatorTypes.decayVolEnv] = { min: -12000, max: 8000, def: -12000 };
131
- generatorLimits[generatorTypes.sustainVolEnv] = { min: 0, max: 1440, def: 0 };
132
- generatorLimits[generatorTypes.releaseVolEnv] = { min: -7200, max: 8000, def: -12000 }; // min is set to -7200 prevent clicks
128
+ generatorLimits[generatorTypes.delayVolEnv] = { min: -12000, max: 5000, def: -12000, nrpn: 2 };
129
+ generatorLimits[generatorTypes.attackVolEnv] = { min: -12000, max: 8000, def: -12000, nrpn: 2 };
130
+ generatorLimits[generatorTypes.holdVolEnv] = { min: -12000, max: 5000, def: -12000, nrpn: 2 };
131
+ generatorLimits[generatorTypes.decayVolEnv] = { min: -12000, max: 8000, def: -12000, nrpn: 2 };
132
+ generatorLimits[generatorTypes.sustainVolEnv] = { min: 0, max: 1440, def: 0, nrpn: 1 };
133
+ generatorLimits[generatorTypes.releaseVolEnv] = { min: -7200, max: 8000, def: -12000, nrpn: 2 }; // min is set to -7200 prevent clicks
133
134
  // key num to vol env
134
- generatorLimits[generatorTypes.keyNumToVolEnvHold] = { min: -1200, max: 1200, def: 0 };
135
- generatorLimits[generatorTypes.keyNumToVolEnvDecay] = { min: -1200, max: 1200, def: 0 };
135
+ generatorLimits[generatorTypes.keyNumToVolEnvHold] = { min: -1200, max: 1200, def: 0, nrpn: 1 };
136
+ generatorLimits[generatorTypes.keyNumToVolEnvDecay] = { min: -1200, max: 1200, def: 0, nrpn: 1 };
136
137
 
137
- generatorLimits[generatorTypes.startloopAddrsCoarseOffset] = { min: -32768, max: 32768, def: 0 };
138
- generatorLimits[generatorTypes.keyNum] = { min: -1, max: 127, def: -1 };
139
- generatorLimits[generatorTypes.velocity] = { min: -1, max: 127, def: -1 };
138
+ generatorLimits[generatorTypes.startloopAddrsCoarseOffset] = { min: -32768, max: 32768, def: 0, nrpn: 1 };
139
+ generatorLimits[generatorTypes.keyNum] = { min: -1, max: 127, def: -1, nrpn: 1 };
140
+ generatorLimits[generatorTypes.velocity] = { min: -1, max: 127, def: -1, nrpn: 1 };
140
141
 
141
- generatorLimits[generatorTypes.initialAttenuation] = { min: 0, max: 1440, def: 0 };
142
+ generatorLimits[generatorTypes.initialAttenuation] = { min: 0, max: 1440, def: 0, nrpn: 1 };
142
143
 
143
- generatorLimits[generatorTypes.endloopAddrsCoarseOffset] = { min: -32768, max: 32768, def: 0 };
144
+ generatorLimits[generatorTypes.endloopAddrsCoarseOffset] = { min: -32768, max: 32768, def: 0, nrpn: 1 };
144
145
 
145
- generatorLimits[generatorTypes.coarseTune] = { min: -120, max: 120, def: 0 };
146
- generatorLimits[generatorTypes.fineTune] = { min: -12700, max: 12700, def: 0 }; // this generator is used as initial pitch, hence this range
147
- generatorLimits[generatorTypes.scaleTuning] = { min: 0, max: 1200, def: 100 };
148
- generatorLimits[generatorTypes.exclusiveClass] = { min: 0, max: 99999, def: 0 };
149
- generatorLimits[generatorTypes.overridingRootKey] = { min: 0 - 1, max: 127, def: -1 };
150
- generatorLimits[generatorTypes.sampleModes] = { min: 0, max: 3, def: 0 };
146
+ generatorLimits[generatorTypes.coarseTune] = { min: -120, max: 120, def: 0, nrpn: 1 };
147
+ generatorLimits[generatorTypes.fineTune] = { min: -12700, max: 12700, def: 0, nrpn: 1 }; // this generator is used as initial pitch, hence this range
148
+ generatorLimits[generatorTypes.scaleTuning] = { min: 0, max: 1200, def: 100, nrpn: 1 };
149
+ generatorLimits[generatorTypes.exclusiveClass] = { min: 0, max: 99999, def: 0, nrpn: 0 };
150
+ generatorLimits[generatorTypes.overridingRootKey] = { min: 0 - 1, max: 127, def: -1, nrpn: 0 };
151
+ generatorLimits[generatorTypes.sampleModes] = { min: 0, max: 3, def: 0, nrpn: 0 };
151
152
 
152
153
  export { generatorLimits };
153
154
 
@@ -93,7 +93,7 @@ export function writeRIFFChunk(chunk, prepend = undefined)
93
93
  * @param header {string}
94
94
  * @param data {Uint8Array}
95
95
  * @param addZeroByte {Boolean}
96
- * @param isList {boolean}
96
+ * @param isList {boolean} adds "LIST" as the chunk type and writes the actual type at the start of the data
97
97
  * @returns {IndexedByteArray}
98
98
  */
99
99
  export function writeRIFFOddSize(header, data, addZeroByte = false, isList = false)
@@ -3,10 +3,10 @@ import { combineZones } from "./combine_zones.js";
3
3
  import { writeRIFFOddSize } from "../riff_chunk.js";
4
4
  import { writeDword } from "../../../utils/byte_functions/little_endian.js";
5
5
  import { writeDLSRegion } from "./rgn2.js";
6
- import { getStringBytesZero } from "../../../utils/byte_functions/string.js";
7
6
  import { writeArticulator } from "./art2.js";
8
7
  import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd } from "../../../utils/loggin.js";
9
8
  import { consoleColors } from "../../../utils/other.js";
9
+ import { getStringBytes } from "../../../utils/byte_functions/string.js";
10
10
 
11
11
  /**
12
12
  * @this {BasicSoundBank}
@@ -84,7 +84,7 @@ export function writeIns(preset)
84
84
  // writeINFO
85
85
  const inam = writeRIFFOddSize(
86
86
  "INAM",
87
- getStringBytesZero(preset.presetName)
87
+ getStringBytes(preset.presetName, true)
88
88
  );
89
89
  const info = writeRIFFOddSize(
90
90
  "INFO",
@@ -2,9 +2,9 @@ import { combineArrays, IndexedByteArray } from "../../../utils/indexed_array.js
2
2
  import { writeDword, writeWord } from "../../../utils/byte_functions/little_endian.js";
3
3
  import { writeRIFFOddSize } from "../riff_chunk.js";
4
4
  import { writeWavesample } from "./wsmp.js";
5
- import { getStringBytesZero } from "../../../utils/byte_functions/string.js";
6
5
  import { SpessaSynthInfo } from "../../../utils/loggin.js";
7
6
  import { consoleColors } from "../../../utils/other.js";
7
+ import { getStringBytes } from "../../../utils/byte_functions/string.js";
8
8
 
9
9
  /**
10
10
  * @param sample {BasicSample}
@@ -66,7 +66,7 @@ export function writeDLSSample(sample)
66
66
 
67
67
  const inam = writeRIFFOddSize(
68
68
  "INAM",
69
- getStringBytesZero(sample.sampleName)
69
+ getStringBytes(sample.sampleName, true)
70
70
  );
71
71
  const info = writeRIFFOddSize(
72
72
  "INFO",
@@ -2,7 +2,7 @@ import { writeRIFFOddSize } from "../riff_chunk.js";
2
2
  import { writeDword } from "../../../utils/byte_functions/little_endian.js";
3
3
  import { combineArrays, IndexedByteArray } from "../../../utils/indexed_array.js";
4
4
  import { writeLins } from "./lins.js";
5
- import { getStringBytesZero, writeStringAsBytes } from "../../../utils/byte_functions/string.js";
5
+ import { getStringBytes, writeStringAsBytes } from "../../../utils/byte_functions/string.js";
6
6
  import { writeWavePool } from "./wvpl.js";
7
7
  import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from "../../../utils/loggin.js";
8
8
  import { consoleColors } from "../../../utils/other.js";
@@ -80,7 +80,7 @@ export function writeDLS()
80
80
  infos.push(
81
81
  writeRIFFOddSize(
82
82
  info,
83
- getStringBytesZero(data),
83
+ getStringBytes(data, true),
84
84
  true
85
85
  )
86
86
  );
@@ -1,5 +1,5 @@
1
1
  import { combineArrays, IndexedByteArray } from "../../../utils/indexed_array.js";
2
- import { RiffChunk, writeRIFFChunk } from "../riff_chunk.js";
2
+ import { RiffChunk, writeRIFFChunk, writeRIFFOddSize } from "../riff_chunk.js";
3
3
  import { writeStringAsBytes } from "../../../utils/byte_functions/string.js";
4
4
  import { consoleColors } from "../../../utils/other.js";
5
5
  import { getIGEN } from "./igen.js";
@@ -97,20 +97,17 @@ export function write(options = DEFAULT_WRITE_OPTIONS)
97
97
  }
98
98
  else
99
99
  {
100
- const arr = new IndexedByteArray(data.length);
100
+ // pad with zero
101
+ const arr = new IndexedByteArray(data.length + 1);
101
102
  writeStringAsBytes(arr, data);
102
103
  infoArrays.push(writeRIFFChunk(new RiffChunk(
103
104
  type,
104
- data.length,
105
+ arr.length,
105
106
  arr
106
107
  )));
107
108
  }
108
109
  }
109
- const combined = combineArrays([
110
- new IndexedByteArray([73, 78, 70, 79]), // INFO
111
- ...infoArrays
112
- ]);
113
- const infoChunk = writeRIFFChunk(new RiffChunk("LIST", combined.length, combined));
110
+ const infoChunk = writeRIFFOddSize("INFO", combineArrays(infoArrays), false, true);
114
111
 
115
112
  SpessaSynthInfo(
116
113
  "%cWriting SDTA...",
@@ -27,7 +27,7 @@ export function computeModulator(controllerTable, modulator, voice)
27
27
  modulator.currentValue = 0;
28
28
  return 0;
29
29
  }
30
- // mapped to 0-16384
30
+ // mapped to 0-16,384
31
31
  let rawSourceValue;
32
32
  if (modulator.sourceUsesCC)
33
33
  {
@@ -121,14 +121,23 @@ export function computeModulator(controllerTable, modulator, voice)
121
121
  /**
122
122
  * Computes modulators of a given voice. Source and index indicate what modulators shall be computed
123
123
  * @param voice {Voice} the voice to compute modulators for
124
- * @param controllerTable {Int16Array} all midi controllers as 14bit values + the non-controller indexes, starting at 128
125
124
  * @param sourceUsesCC {number} what modulators should be computed, -1 means all, 0 means modulator source enum 1 means midi controller
126
125
  * @param sourceIndex {number} enum for the source
126
+ * @this {MidiAudioChannel}
127
127
  */
128
- export function computeModulators(voice, controllerTable, sourceUsesCC = -1, sourceIndex = 0)
128
+ export function computeModulators(voice, sourceUsesCC = -1, sourceIndex = 0)
129
129
  {
130
130
  const modulators = voice.modulators;
131
- const generators = voice.generators;
131
+ let generators = voice.generators;
132
+ // apply offsets if enabled
133
+ if (this.generatorOffsetsEnabled)
134
+ {
135
+ generators = new Int16Array(generators);
136
+ for (let i = 0; i < generators.length; i++)
137
+ {
138
+ generators[i] += this.generatorOffsets[i];
139
+ }
140
+ }
132
141
  const modulatedGenerators = voice.modulatedGenerators;
133
142
 
134
143
  if (sourceUsesCC === -1)
@@ -144,7 +153,7 @@ export function computeModulators(voice, controllerTable, sourceUsesCC = -1, sou
144
153
  return;
145
154
  }
146
155
  const newValue = modulatedGenerators[mod.modulatorDestination] + computeModulator(
147
- controllerTable,
156
+ this.midiControllers,
148
157
  mod,
149
158
  voice
150
159
  );
@@ -186,7 +195,7 @@ export function computeModulators(voice, controllerTable, sourceUsesCC = -1, sou
186
195
  // Reset this destination
187
196
  modulatedGenerators[destination] = generators[destination];
188
197
  // compute our modulator
189
- computeModulator(controllerTable, mod, voice);
198
+ computeModulator(this.midiControllers, mod, voice);
190
199
  // sum the values of all modulators for this destination
191
200
  modulators.forEach(m =>
192
201
  {
@@ -63,7 +63,8 @@ export const customControllers = {
63
63
  modulationMultiplier: 2, // cents, set by modulation depth RPN
64
64
  masterTuning: 3, // cents, set by system exclusive
65
65
  channelTuningSemitones: 4, // semitones, for RPN coarse tuning
66
- channelKeyShift: 5 // key shift: for system exclusive
66
+ channelKeyShift: 5, // key shift: for system exclusive
67
+ sf2NPRNGeneratorLSB: 6 // sf2 NPRN LSB for selecting a generator value
67
68
  };
68
69
  export const CUSTOM_CONTROLLER_TABLE_SIZE = Object.keys(customControllers).length;
69
70
  export const customResetArray = new Float32Array(CUSTOM_CONTROLLER_TABLE_SIZE);
@@ -129,7 +129,7 @@ export class LowpassFilter
129
129
 
130
130
  /**
131
131
  * Applies a low-pass filter to the given buffer
132
- * @param voice {Voice} the voice we're working on
132
+ * @param voice {Voice} the voice we are working on
133
133
  * @param outputBuffer {Float32Array} the buffer to apply the filter to
134
134
  * @param fcExcursion {number} the addition of modenv and mod lfo in cents to the filter
135
135
  * @param smoothingFactor {number} filter's cutoff frequency smoothing factor
@@ -32,7 +32,7 @@ import { chooseBank, isSystemXG, parseBankSelect } from "../../../utils/xg_hacks
32
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";
35
+ import { generatorLimits, GENERATORS_AMOUNT } from "../../../soundfont/basic_soundfont/generator.js";
36
36
  import { computeModulators } from "./compute_modulator.js";
37
37
 
38
38
  /**
@@ -90,6 +90,19 @@ class MidiAudioChannel
90
90
  */
91
91
  sysExModulators = new DynamicModulatorSystem();
92
92
 
93
+ /**
94
+ * An array of offsets generators for SF2 nrpn support.
95
+ * A value of 0 means no change; -10 means 10 lower, etc.
96
+ * @type {Int16Array}
97
+ */
98
+ generatorOffsets = new Int16Array(GENERATORS_AMOUNT);
99
+
100
+ /**
101
+ * A small optimization that disables applying offsets until at least one is set.
102
+ * @type {boolean}
103
+ */
104
+ generatorOffsetsEnabled = false;
105
+
93
106
  /**
94
107
  * An array of override generators for AWE32 support.
95
108
  * A value of 32,767 means unchanged, as it is not allowed anywhere.
@@ -226,6 +239,7 @@ class MidiAudioChannel
226
239
  this.preset = preset;
227
240
  this.channelNumber = channelNumber;
228
241
  this.resetGeneratorOverrides();
242
+ this.resetGeneratorOffsets();
229
243
  }
230
244
 
231
245
  get isXGChannel()
@@ -475,10 +489,30 @@ class MidiAudioChannel
475
489
  this.voices.forEach(v =>
476
490
  {
477
491
  v.generators[gen] = value;
478
- computeModulators(v, this.midiControllers);
492
+ this.computeModulators(v);
479
493
  });
480
494
  }
481
495
  }
496
+
497
+ resetGeneratorOffsets()
498
+ {
499
+ this.generatorOffsets.fill(0);
500
+ this.generatorOffsetsEnabled = false;
501
+ }
502
+
503
+ /**
504
+ * @param gen {generatorTypes}
505
+ * @param value {number}
506
+ */
507
+ setGeneratorOffset(gen, value)
508
+ {
509
+ this.generatorOffsets[gen] = value * generatorLimits[gen].nrpn;
510
+ this.generatorOffsetsEnabled = true;
511
+ this.voices.forEach(v =>
512
+ {
513
+ this.computeModulators(v);
514
+ });
515
+ }
482
516
  }
483
517
 
484
518
  // voice
@@ -487,6 +521,7 @@ MidiAudioChannel.prototype.panVoice = panVoice;
487
521
  MidiAudioChannel.prototype.killNote = killNote;
488
522
  MidiAudioChannel.prototype.stopAllNotes = stopAllNotes;
489
523
  MidiAudioChannel.prototype.muteChannel = muteChannel;
524
+ MidiAudioChannel.prototype.computeModulators = computeModulators;
490
525
 
491
526
  // MIDI messages
492
527
  MidiAudioChannel.prototype.noteOn = noteOn;
@@ -400,7 +400,8 @@ export function getVoicesForPreset(preset, bank, program, midiNote, velocity, re
400
400
  );
401
401
  }
402
402
 
403
- // !! EMU initial attenuation correction, multiply initial attenuation by 0.4
403
+ // EMU initial attenuation correction, multiply initial attenuation by 0.4!
404
+ // all EMU sound cards have this quirk and all sf2 editors and players emulate it too
404
405
  generators[generatorTypes.initialAttenuation] = Math.floor(generators[generatorTypes.initialAttenuation] * 0.4);
405
406
 
406
407
  // key override
@@ -172,7 +172,7 @@ export class WavetableOscillator
172
172
  let y1 = y0 + 1; // point after the cursor
173
173
  let y2 = y1 + 1; // point 1 after the cursor
174
174
  let y3 = y2 + 1; // point 2 after the cursor
175
- const t = cur - y0; // distance from y0 to cursor
175
+ const t = cur - y0; // the distance from y0 to cursor
176
176
  // y0 is not handled here
177
177
  // as it's math.floor of cur which is handled above
178
178
  if (y1 >= sample.loopEnd)
@@ -1,6 +1,6 @@
1
1
  import { midiControllers } from "../../../../midi/midi_message.js";
2
- import { computeModulators } from "../../engine_components/compute_modulator.js";
3
- import { channelConfiguration, dataEntryStates } from "../../engine_components/controller_tables.js";
2
+ import { channelConfiguration, customControllers, dataEntryStates } from "../../engine_components/controller_tables.js";
3
+ import { nonRegisteredMSB } from "../data_entry/data_entry_coarse.js";
4
4
 
5
5
  /**
6
6
  * @param controllerNumber {number}
@@ -42,7 +42,7 @@ export function controllerChange(controllerNumber, controllerValue, force = fals
42
42
  }
43
43
  // append the lower nibble to the main controller
44
44
  this.midiControllers[actualCCNum] = (this.midiControllers[actualCCNum] & 0x3F80) | (controllerValue & 0x7F);
45
- this.voices.forEach(v => computeModulators(v, this.midiControllers, 1, actualCCNum));
45
+ this.voices.forEach(v => this.computeModulators(v, 1, actualCCNum));
46
46
  }
47
47
  if (this.lockedControllers[controllerNumber])
48
48
  {
@@ -83,10 +83,37 @@ export function controllerChange(controllerNumber, controllerValue, force = fals
83
83
  break;
84
84
 
85
85
  case midiControllers.NRPNMsb:
86
+ // sfspec section 9.6.2
87
+ this.customControllers[customControllers.sf2NPRNGeneratorLSB] = 0;
86
88
  this.dataEntryState = dataEntryStates.NRPCoarse;
87
89
  break;
88
90
 
89
91
  case midiControllers.NRPNLsb:
92
+ if (this.midiControllers[midiControllers.NRPNMsb] >> 7 === nonRegisteredMSB.SF2)
93
+ {
94
+ // if a <100 value has already been sent, reset!
95
+ if (this.customControllers[customControllers.sf2NPRNGeneratorLSB] % 100 !== 0)
96
+ {
97
+ this.customControllers[customControllers.sf2NPRNGeneratorLSB] = 0;
98
+ }
99
+
100
+ if (controllerValue === 100)
101
+ {
102
+ this.customControllers[customControllers.sf2NPRNGeneratorLSB] += 100;
103
+ }
104
+ else if (controllerValue === 101)
105
+ {
106
+ this.customControllers[customControllers.sf2NPRNGeneratorLSB] += 1000;
107
+ }
108
+ else if (controllerValue === 102)
109
+ {
110
+ this.customControllers[customControllers.sf2NPRNGeneratorLSB] += 10000;
111
+ }
112
+ else if (controllerValue < 100)
113
+ {
114
+ this.customControllers[customControllers.sf2NPRNGeneratorLSB] += controllerValue;
115
+ }
116
+ }
90
117
  this.dataEntryState = dataEntryStates.NRPFine;
91
118
  break;
92
119
 
@@ -120,7 +147,7 @@ export function controllerChange(controllerNumber, controllerValue, force = fals
120
147
 
121
148
  // default: just compute modulators
122
149
  default:
123
- this.voices.forEach(v => computeModulators(v, this.midiControllers, 1, controllerNumber));
150
+ this.voices.forEach(v => this.computeModulators(v, 1, controllerNumber));
124
151
  break;
125
152
  }
126
153
  }
@@ -220,6 +220,7 @@ export function resetControllersRP15Compliant()
220
220
  }
221
221
  }
222
222
  this.resetGeneratorOverrides();
223
+ this.resetGeneratorOffsets();
223
224
  }
224
225
 
225
226
  /**
@@ -236,6 +237,7 @@ export function resetParameters()
236
237
  this.midiControllers[midiControllers.RPNLsb] = 127 << 7;
237
238
  this.midiControllers[midiControllers.RPNMsb] = 127 << 7;
238
239
  this.resetGeneratorOverrides();
240
+ this.resetGeneratorOffsets();
239
241
  SpessaSynthInfo(
240
242
  "%cResetting Registered and Non-Registered Parameters!",
241
243
  consoleColors.info
@@ -21,7 +21,8 @@ export const registeredParameterTypes = {
21
21
  */
22
22
  export const nonRegisteredMSB = {
23
23
  partParameter: 0x01,
24
- awe32: 0x7F
24
+ awe32: 0x7F,
25
+ SF2: 120
25
26
  };
26
27
 
27
28
  /**
@@ -98,6 +99,7 @@ export function dataEntryCoarse(dataValue)
98
99
  * @type {number}
99
100
  */
100
101
  const NRPNFine = this.midiControllers[midiControllers.NRPNLsb] >> 7;
102
+ const dataEntryFine = this.midiControllers[midiControllers.lsbForControl6DataEntry] >> 7;
101
103
  switch (NRPNCoarse)
102
104
  {
103
105
  default:
@@ -199,6 +201,20 @@ export function dataEntryCoarse(dataValue)
199
201
 
200
202
  case nonRegisteredMSB.awe32:
201
203
  break;
204
+
205
+ // SF2 NRPN
206
+ case nonRegisteredMSB.SF2:
207
+ if (NRPNFine > 100)
208
+ {
209
+ // sfspec:
210
+ // Note that NRPN Select LSB greater than 100 are for setup only, and should not be used on their own to select a
211
+ // generator parameter.
212
+ break;
213
+ }
214
+ const gen = this.customControllers[customControllers.sf2NPRNGeneratorLSB];
215
+ const offset = (dataValue << 7 | dataEntryFine) - 8192;
216
+ this.setGeneratorOffset(gen, offset);
217
+ break;
202
218
  }
203
219
  break;
204
220
 
@@ -76,6 +76,10 @@ export function dataEntryFine(dataValue)
76
76
  * @type {number}
77
77
  */
78
78
  const NRPNFine = this.midiControllers[midiControllers.NRPNLsb] >> 7;
79
+ if (NRPNCoarse === nonRegisteredMSB.SF2)
80
+ {
81
+ return;
82
+ }
79
83
  switch (NRPNCoarse)
80
84
  {
81
85
  default:
@@ -1,4 +1,3 @@
1
- import { computeModulators } from "../engine_components/compute_modulator.js";
2
1
  import { generatorTypes } from "../../../soundfont/basic_soundfont/generator.js";
3
2
  import { midiControllers } from "../../../midi/midi_message.js";
4
3
  import { portamentoTimeToSeconds } from "./portamento_time.js";
@@ -159,7 +158,7 @@ export function noteOn(midiNote, velocity)
159
158
  });
160
159
  }
161
160
  // compute all modulators
162
- computeModulators(voice, this.midiControllers);
161
+ this.computeModulators(voice);
163
162
  // modulate sample offsets (these are not real time)
164
163
  const cursorStartOffset = voice.modulatedGenerators[generatorTypes.startAddrsOffset] + voice.modulatedGenerators[generatorTypes.startAddrsCoarseOffset] * 32768;
165
164
  const endOffset = voice.modulatedGenerators[generatorTypes.endAddrOffset] + voice.modulatedGenerators[generatorTypes.endAddrsCoarseOffset] * 32768;
@@ -1,6 +1,5 @@
1
1
  import { NON_CC_INDEX_OFFSET } from "../../engine_components/controller_tables.js";
2
2
  import { modulatorSources } from "../../../../soundfont/basic_soundfont/modulator.js";
3
- import { computeModulators } from "../../engine_components/compute_modulator.js";
4
3
 
5
4
  /**
6
5
  * Sets the pressure of the given channel
@@ -12,9 +11,8 @@ export function channelPressure(pressure)
12
11
  this.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.channelPressure] = pressure << 7;
13
12
  this.updateChannelTuning();
14
13
  this.voices.forEach(v =>
15
- computeModulators(
14
+ this.computeModulators(
16
15
  v,
17
- this.midiControllers,
18
16
  0,
19
17
  modulatorSources.channelPressure
20
18
  ));
@@ -1,6 +1,5 @@
1
1
  import { NON_CC_INDEX_OFFSET } from "../../engine_components/controller_tables.js";
2
2
  import { modulatorSources } from "../../../../soundfont/basic_soundfont/modulator.js";
3
- import { computeModulators } from "../../engine_components/compute_modulator.js";
4
3
 
5
4
  /**
6
5
  * Sets the pitch of the given channel
@@ -23,9 +22,8 @@ export function pitchWheel(MSB, LSB)
23
22
  this.midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel] = bend;
24
23
  this.voices.forEach(v =>
25
24
  // compute pitch modulators
26
- computeModulators(
25
+ this.computeModulators(
27
26
  v,
28
- this.midiControllers,
29
27
  0,
30
28
  modulatorSources.pitchWheel
31
29
  ));
@@ -1,4 +1,3 @@
1
- import { computeModulators } from "../../engine_components/compute_modulator.js";
2
1
  import { modulatorSources } from "../../../../soundfont/basic_soundfont/modulator.js";
3
2
 
4
3
  /**
@@ -16,9 +15,8 @@ export function polyPressure(midiNote, pressure)
16
15
  return;
17
16
  }
18
17
  v.pressure = pressure;
19
- computeModulators(
18
+ this.computeModulators(
20
19
  v,
21
- this.midiControllers,
22
20
  0,
23
21
  modulatorSources.polyPressure
24
22
  );
@@ -51,30 +51,21 @@ export function readBytesAsString(dataArray, bytes, encoding = undefined, trimEn
51
51
 
52
52
  /**
53
53
  * @param string {string}
54
- * @param padLength {number}
54
+ * @param addZero {boolean} adds a zero terminator at the end
55
55
  * @returns {IndexedByteArray}
56
56
  */
57
- export function getStringBytes(string, padLength = 0)
57
+ export function getStringBytes(string, addZero = false)
58
58
  {
59
59
  let len = string.length;
60
- if (padLength > 0)
60
+ if (addZero)
61
61
  {
62
- len = padLength;
62
+ len = len + 1;
63
63
  }
64
64
  const arr = new IndexedByteArray(len);
65
- writeStringAsBytes(arr, string, padLength);
65
+ writeStringAsBytes(arr, string);
66
66
  return arr;
67
67
  }
68
68
 
69
- /**
70
- * @param string {string}
71
- * @returns {IndexedByteArray}
72
- */
73
- export function getStringBytesZero(string)
74
- {
75
- return getStringBytes(string, string.length + 1);
76
- }
77
-
78
69
  /**
79
70
  * @param string {string}
80
71
  * @param outArray {IndexedByteArray}