spessasynth_core 3.26.4 → 3.26.6
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/package.json +1 -1
- package/src/midi/midi_tools/used_keys_loaded.js +26 -8
- package/src/sequencer/song_control.js +20 -25
- package/src/soundfont/basic_soundfont/basic_preset.js +1 -40
- package/src/soundfont/basic_soundfont/generator.js +9 -1
- package/src/synthetizer/audio_engine/engine_components/controller_tables.js +6 -5
- package/src/synthetizer/audio_engine/engine_components/dynamic_modulator_system.js +95 -0
- package/src/synthetizer/audio_engine/engine_components/midi_audio_channel.js +11 -11
- package/src/synthetizer/audio_engine/engine_components/soundfont_manager.js +45 -17
- package/src/synthetizer/audio_engine/engine_components/stereo_panner.js +2 -2
- package/src/synthetizer/audio_engine/engine_components/voice.js +51 -51
- package/src/synthetizer/audio_engine/engine_methods/controller_control/reset_controllers.js +3 -6
- package/src/synthetizer/audio_engine/engine_methods/data_entry/data_entry_coarse.js +20 -21
- package/src/synthetizer/audio_engine/engine_methods/note_on.js +28 -8
- package/src/synthetizer/audio_engine/engine_methods/program_change.js +4 -39
- package/src/synthetizer/audio_engine/engine_methods/render_voice.js +18 -11
- package/src/synthetizer/audio_engine/engine_methods/soundfont_management/embedded_sound_bank.js +43 -0
- package/src/synthetizer/audio_engine/engine_methods/soundfont_management/get_preset.js +0 -22
- package/src/synthetizer/audio_engine/engine_methods/soundfont_management/update_preset_list.js +5 -20
- package/src/synthetizer/audio_engine/engine_methods/stopping_notes/kill_note.js +3 -0
- package/src/synthetizer/audio_engine/engine_methods/stopping_notes/note_off.js +2 -1
- package/src/synthetizer/audio_engine/engine_methods/system_exclusive.js +442 -173
- package/src/synthetizer/audio_engine/engine_methods/tuning_control/channel_pressure.js +1 -0
- package/src/synthetizer/audio_engine/main_processor.js +27 -20
- package/src/synthetizer/synth_constants.js +3 -1
- package/src/synthetizer/audio_engine/engine_methods/soundfont_management/clear_sound_font.js +0 -32
- package/src/synthetizer/audio_engine/engine_methods/soundfont_management/set_embedded_sound_font.js +0 -33
package/package.json
CHANGED
|
@@ -4,11 +4,12 @@ import { messageTypes, midiControllers } from "../midi_message.js";
|
|
|
4
4
|
import { DEFAULT_PERCUSSION } from "../../synthetizer/synth_constants.js";
|
|
5
5
|
import { chooseBank, isSystemXG, parseBankSelect } from "../../utils/xg_hacks.js";
|
|
6
6
|
import { isGSDrumsOn, isXGOn } from "../../utils/sysex_detector.js";
|
|
7
|
+
import { SoundFontManager } from "../../synthetizer/audio_engine/engine_components/soundfont_manager.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Gets the used programs and keys for this MIDI file with a given sound bank
|
|
10
11
|
* @this {BasicMIDI}
|
|
11
|
-
* @param soundfont {BasicSoundBank
|
|
12
|
+
* @param soundfont {SoundFontManager|BasicSoundBank} - the sound bank
|
|
12
13
|
* @returns {Object<string, Set<string>>} Object<bank:program, Set<key-velocity>>
|
|
13
14
|
*/
|
|
14
15
|
export function getUsedProgramsAndKeys(soundfont)
|
|
@@ -44,9 +45,27 @@ export function getUsedProgramsAndKeys(soundfont)
|
|
|
44
45
|
{
|
|
45
46
|
const bank = chooseBank(ch.bank, ch.bankLSB, ch.drums, isSystemXG(system));
|
|
46
47
|
// check if this exists in the soundfont
|
|
47
|
-
let
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
let existsBank, existsProgram;
|
|
49
|
+
if (soundfont instanceof SoundFontManager)
|
|
50
|
+
{
|
|
51
|
+
/**
|
|
52
|
+
* @type {{preset: BasicPreset, bankOffset: number}}
|
|
53
|
+
*/
|
|
54
|
+
let exists = soundfont.getPreset(bank, ch.program, isSystemXG(system));
|
|
55
|
+
existsBank = exists.preset.bank + exists.bankOffset;
|
|
56
|
+
existsProgram = exists.preset.program;
|
|
57
|
+
}
|
|
58
|
+
else
|
|
59
|
+
{
|
|
60
|
+
/**
|
|
61
|
+
* @type {BasicPreset}
|
|
62
|
+
*/
|
|
63
|
+
let exists = soundfont.getPreset(bank, ch.program, isSystemXG(system));
|
|
64
|
+
existsBank = exists.bank;
|
|
65
|
+
existsProgram = exists.program;
|
|
66
|
+
}
|
|
67
|
+
ch.actualBank = existsBank;
|
|
68
|
+
ch.program = existsProgram;
|
|
50
69
|
ch.string = ch.actualBank + ":" + ch.program;
|
|
51
70
|
if (!usedProgramsAndKeys[ch.string])
|
|
52
71
|
{
|
|
@@ -147,19 +166,18 @@ export function getUsedProgramsAndKeys(soundfont)
|
|
|
147
166
|
continue;
|
|
148
167
|
}
|
|
149
168
|
const bank = event.messageData[1];
|
|
150
|
-
const realBank = Math.max(0, bank - mid.bankOffset);
|
|
151
169
|
if (isLSB)
|
|
152
170
|
{
|
|
153
|
-
ch.bankLSB =
|
|
171
|
+
ch.bankLSB = bank;
|
|
154
172
|
}
|
|
155
173
|
else
|
|
156
174
|
{
|
|
157
|
-
ch.bank =
|
|
175
|
+
ch.bank = bank;
|
|
158
176
|
}
|
|
159
177
|
// interpret the bank
|
|
160
178
|
const intepretation = parseBankSelect(
|
|
161
179
|
ch.bank,
|
|
162
|
-
|
|
180
|
+
bank,
|
|
163
181
|
system,
|
|
164
182
|
isLSB,
|
|
165
183
|
ch.drums,
|
|
@@ -57,41 +57,36 @@ export function loadNewSequence(parsedMidi, autoPlay = true)
|
|
|
57
57
|
*/
|
|
58
58
|
this.midiData = parsedMidi;
|
|
59
59
|
|
|
60
|
+
// clear old embedded bank if exists
|
|
61
|
+
this.synth.clearEmbeddedBank();
|
|
62
|
+
|
|
60
63
|
// check for embedded soundfont
|
|
61
64
|
if (this.midiData.embeddedSoundFont !== undefined)
|
|
62
65
|
{
|
|
63
66
|
SpessaSynthInfo("%cEmbedded soundfont detected! Using it.", consoleColors.recognized);
|
|
64
67
|
this.synth.setEmbeddedSoundFont(this.midiData.embeddedSoundFont, this.midiData.bankOffset);
|
|
65
68
|
}
|
|
66
|
-
|
|
69
|
+
|
|
70
|
+
SpessaSynthGroupCollapsed("%cPreloading samples...", consoleColors.info);
|
|
71
|
+
// smart preloading: load only samples used in the midi!
|
|
72
|
+
const used = this.midiData.getUsedProgramsAndKeys(this.synth.soundfontManager);
|
|
73
|
+
for (const [programBank, combos] of Object.entries(used))
|
|
67
74
|
{
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
for (const
|
|
75
|
+
const [bank, program] = programBank.split(":").map(Number);
|
|
76
|
+
const preset = this.synth.getPreset(bank, program);
|
|
77
|
+
SpessaSynthInfo(
|
|
78
|
+
`%cPreloading used samples on %c${preset.presetName}%c...`,
|
|
79
|
+
consoleColors.info,
|
|
80
|
+
consoleColors.recognized,
|
|
81
|
+
consoleColors.info
|
|
82
|
+
);
|
|
83
|
+
for (const combo of combos)
|
|
77
84
|
{
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
const preset = this.synth.getPreset(bank, program);
|
|
81
|
-
SpessaSynthInfo(
|
|
82
|
-
`%cPreloading used samples on %c${preset.presetName}%c...`,
|
|
83
|
-
consoleColors.info,
|
|
84
|
-
consoleColors.recognized,
|
|
85
|
-
consoleColors.info
|
|
86
|
-
);
|
|
87
|
-
for (const combo of combos)
|
|
88
|
-
{
|
|
89
|
-
const split = combo.split("-");
|
|
90
|
-
preset.preloadSpecific(parseInt(split[0]), parseInt(split[1]));
|
|
91
|
-
}
|
|
85
|
+
const [midiNote, velocity] = combo.split("-").map(Number);
|
|
86
|
+
this.synth.getVoicesForPreset(preset, bank, program, midiNote, velocity, midiNote);
|
|
92
87
|
}
|
|
93
|
-
SpessaSynthGroupEnd();
|
|
94
88
|
}
|
|
89
|
+
SpessaSynthGroupEnd();
|
|
95
90
|
|
|
96
91
|
/**
|
|
97
92
|
* the midi track data
|
|
@@ -44,12 +44,6 @@ export class BasicPreset
|
|
|
44
44
|
*/
|
|
45
45
|
presetZones = [];
|
|
46
46
|
|
|
47
|
-
/**
|
|
48
|
-
* Stores already found getSamplesAndGenerators for reuse
|
|
49
|
-
* @type {SampleAndGenerators[][][]}
|
|
50
|
-
*/
|
|
51
|
-
foundSamplesAndGenerators = [];
|
|
52
|
-
|
|
53
47
|
/**
|
|
54
48
|
* unused metadata
|
|
55
49
|
* @type {number}
|
|
@@ -73,15 +67,6 @@ export class BasicPreset
|
|
|
73
67
|
constructor(parentSoundBank)
|
|
74
68
|
{
|
|
75
69
|
this.parentSoundBank = parentSoundBank;
|
|
76
|
-
for (let i = 0; i < 128; i++)
|
|
77
|
-
{
|
|
78
|
-
this.foundSamplesAndGenerators[i] = [];
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
clearCache()
|
|
83
|
-
{
|
|
84
|
-
this.foundSamplesAndGenerators = [];
|
|
85
70
|
}
|
|
86
71
|
|
|
87
72
|
/**
|
|
@@ -136,34 +121,13 @@ export class BasicPreset
|
|
|
136
121
|
}
|
|
137
122
|
|
|
138
123
|
/**
|
|
139
|
-
*
|
|
140
|
-
* @param key {number}
|
|
141
|
-
* @param velocity {number}
|
|
142
|
-
*/
|
|
143
|
-
preloadSpecific(key, velocity)
|
|
144
|
-
{
|
|
145
|
-
this.getSamplesAndGenerators(key, velocity).forEach(samandgen =>
|
|
146
|
-
{
|
|
147
|
-
if (!samandgen.sample.isSampleLoaded)
|
|
148
|
-
{
|
|
149
|
-
samandgen.sample.getAudioData();
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Returns generatorTranslator and generators for given note
|
|
124
|
+
* Returns samples and generators for given note
|
|
156
125
|
* @param midiNote {number}
|
|
157
126
|
* @param velocity {number}
|
|
158
127
|
* @returns {SampleAndGenerators[]}
|
|
159
128
|
*/
|
|
160
129
|
getSamplesAndGenerators(midiNote, velocity)
|
|
161
130
|
{
|
|
162
|
-
const memorized = this.foundSamplesAndGenerators[midiNote][velocity];
|
|
163
|
-
if (memorized)
|
|
164
|
-
{
|
|
165
|
-
return memorized;
|
|
166
|
-
}
|
|
167
131
|
|
|
168
132
|
if (this.presetZones.length < 1)
|
|
169
133
|
{
|
|
@@ -333,9 +297,6 @@ export class BasicPreset
|
|
|
333
297
|
});
|
|
334
298
|
});
|
|
335
299
|
});
|
|
336
|
-
|
|
337
|
-
// save and return
|
|
338
|
-
this.foundSamplesAndGenerators[midiNote][velocity] = parsedGeneratorsAndSamples;
|
|
339
300
|
return parsedGeneratorsAndSamples;
|
|
340
301
|
}
|
|
341
302
|
}
|
|
@@ -63,8 +63,14 @@ export const generatorTypes = {
|
|
|
63
63
|
exclusiveClass: 57, // sample - = cut = choke group
|
|
64
64
|
overridingRootKey: 58, // sample - can override the sample's original pitch
|
|
65
65
|
unused5: 59, // unused
|
|
66
|
-
endOper: 60
|
|
66
|
+
endOper: 60, // end marker
|
|
67
|
+
|
|
68
|
+
// additional generators that are used in system exclusives and will not be saved
|
|
69
|
+
vibLfoToVolume: 61,
|
|
70
|
+
vibLfoToFilterFc: 62
|
|
67
71
|
};
|
|
72
|
+
export const GENERATORS_AMOUNT = Object.keys(generatorTypes).length;
|
|
73
|
+
|
|
68
74
|
/**
|
|
69
75
|
* @type {{min: number, max: number, def: number}[]}
|
|
70
76
|
*/
|
|
@@ -85,11 +91,13 @@ generatorLimits[generatorTypes.modEnvToPitch] = { min: -12000, max: 12000, def:
|
|
|
85
91
|
generatorLimits[generatorTypes.initialFilterFc] = { min: 1500, max: 13500, def: 13500 };
|
|
86
92
|
generatorLimits[generatorTypes.initialFilterQ] = { min: 0, max: 960, def: 0 };
|
|
87
93
|
generatorLimits[generatorTypes.modLfoToFilterFc] = { min: -12000, max: 12000, def: 0 };
|
|
94
|
+
generatorLimits[generatorTypes.vibLfoToFilterFc] = { min: -12000, max: 12000, def: 0 }; // NON-STANDARD
|
|
88
95
|
generatorLimits[generatorTypes.modEnvToFilterFc] = { min: -12000, max: 12000, def: 0 };
|
|
89
96
|
|
|
90
97
|
generatorLimits[generatorTypes.endAddrsCoarseOffset] = { min: -32768, max: 32768, def: 0 };
|
|
91
98
|
|
|
92
99
|
generatorLimits[generatorTypes.modLfoToVolume] = { min: -960, max: 960, def: 0 };
|
|
100
|
+
generatorLimits[generatorTypes.vibLfoToVolume] = { min: -960, max: 960, def: 0 }; // NON-STANDARD
|
|
93
101
|
|
|
94
102
|
// effects, pan
|
|
95
103
|
generatorLimits[generatorTypes.chorusEffectsSend] = { min: 0, max: 1000, def: 0 };
|
|
@@ -57,12 +57,13 @@ setResetValue(NON_CC_INDEX_OFFSET + modulatorSources.pitchWheelRange, 2);
|
|
|
57
57
|
* @enum {number}
|
|
58
58
|
*/
|
|
59
59
|
export const customControllers = {
|
|
60
|
-
channelTuning: 0,
|
|
61
|
-
channelTransposeFine: 1,
|
|
60
|
+
channelTuning: 0, // cents, RPN for fine tuning
|
|
61
|
+
channelTransposeFine: 1, // cents, only the decimal tuning, (e.g., transpose is 4.5,
|
|
62
62
|
// then shift by 4 keys + tune by 50 cents)
|
|
63
|
-
modulationMultiplier: 2,
|
|
64
|
-
masterTuning: 3,
|
|
65
|
-
channelTuningSemitones: 4
|
|
63
|
+
modulationMultiplier: 2, // cents, set by modulation depth RPN
|
|
64
|
+
masterTuning: 3, // cents, set by system exclusive
|
|
65
|
+
channelTuningSemitones: 4, // semitones, for RPN coarse tuning
|
|
66
|
+
channelKeyShift: 5 // key shift: for system exclusive
|
|
66
67
|
};
|
|
67
68
|
export const CUSTOM_CONTROLLER_TABLE_SIZE = Object.keys(customControllers).length;
|
|
68
69
|
export const customResetArray = new Float32Array(CUSTOM_CONTROLLER_TABLE_SIZE);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { getModSourceEnum, Modulator, modulatorCurveTypes } from "../../../soundfont/basic_soundfont/modulator.js";
|
|
2
|
+
import { NON_CC_INDEX_OFFSET } from "./controller_tables.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A class for dynamic modulators
|
|
6
|
+
* that are assigned for more complex system exclusive messages
|
|
7
|
+
*/
|
|
8
|
+
export class DynamicModulatorSystem
|
|
9
|
+
{
|
|
10
|
+
/**
|
|
11
|
+
* the current dynamic modulator list
|
|
12
|
+
* @type {{mod: Modulator, id: string}[]}
|
|
13
|
+
*/
|
|
14
|
+
modulatorList = [];
|
|
15
|
+
|
|
16
|
+
resetModulators()
|
|
17
|
+
{
|
|
18
|
+
this.modulatorList = [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @returns {Modulator[]}
|
|
23
|
+
*/
|
|
24
|
+
getModulators()
|
|
25
|
+
{
|
|
26
|
+
return this.modulatorList.map(m => m.mod);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param source {number}
|
|
31
|
+
* @param destination {generatorTypes}
|
|
32
|
+
* @param isBipolar {boolean}
|
|
33
|
+
* @param isNegative {boolean}
|
|
34
|
+
*/
|
|
35
|
+
_getModulatorId(source, destination, isBipolar, isNegative)
|
|
36
|
+
{
|
|
37
|
+
return `${source}-${destination}-${isBipolar}-${isNegative}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param id {string}
|
|
42
|
+
* @private
|
|
43
|
+
*/
|
|
44
|
+
_deleteModulator(id)
|
|
45
|
+
{
|
|
46
|
+
this.modulatorList = this.modulatorList.filter(m => m.id !== id);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param source {number} like in midiControllers: values below NON_CC_INDEX_OFFSET are CCs,
|
|
51
|
+
* above are regular modulator sources
|
|
52
|
+
* @param destination {generatorTypes}
|
|
53
|
+
* @param amount {number}
|
|
54
|
+
* @param isBipolar {boolean}
|
|
55
|
+
* @param isNegative {boolean}
|
|
56
|
+
*/
|
|
57
|
+
setModulator(source, destination, amount, isBipolar = false, isNegative = false)
|
|
58
|
+
{
|
|
59
|
+
const id = this._getModulatorId(source, destination, isBipolar, isNegative);
|
|
60
|
+
if (amount === 0)
|
|
61
|
+
{
|
|
62
|
+
this._deleteModulator(id);
|
|
63
|
+
}
|
|
64
|
+
const mod = this.modulatorList.find(m => m.id === id);
|
|
65
|
+
if (mod)
|
|
66
|
+
{
|
|
67
|
+
mod.mod.transformAmount = amount;
|
|
68
|
+
}
|
|
69
|
+
else
|
|
70
|
+
{
|
|
71
|
+
let srcNum, isCC;
|
|
72
|
+
if (source >= NON_CC_INDEX_OFFSET)
|
|
73
|
+
{
|
|
74
|
+
srcNum = source - NON_CC_INDEX_OFFSET;
|
|
75
|
+
isCC = false;
|
|
76
|
+
}
|
|
77
|
+
else
|
|
78
|
+
{
|
|
79
|
+
srcNum = source;
|
|
80
|
+
isCC = true;
|
|
81
|
+
}
|
|
82
|
+
const modulator = new Modulator(
|
|
83
|
+
getModSourceEnum(modulatorCurveTypes.linear, isBipolar, 0, isCC, srcNum),
|
|
84
|
+
0x0, // linear no controller
|
|
85
|
+
destination,
|
|
86
|
+
amount,
|
|
87
|
+
0
|
|
88
|
+
);
|
|
89
|
+
this.modulatorList.push({
|
|
90
|
+
mod: modulator,
|
|
91
|
+
id: id
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -31,6 +31,7 @@ import { programChange } from "../engine_methods/program_change.js";
|
|
|
31
31
|
import { chooseBank, isSystemXG, parseBankSelect } from "../../../utils/xg_hacks.js";
|
|
32
32
|
import { DEFAULT_PERCUSSION } from "../../synth_constants.js";
|
|
33
33
|
import { modulatorSources } from "../../../soundfont/basic_soundfont/modulator.js";
|
|
34
|
+
import { DynamicModulatorSystem } from "./dynamic_modulator_system.js";
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* This class represents a single MIDI Channel within the synthesizer.
|
|
@@ -81,6 +82,12 @@ class MidiAudioChannel
|
|
|
81
82
|
*/
|
|
82
83
|
channelTuningCents = 0;
|
|
83
84
|
|
|
85
|
+
/**
|
|
86
|
+
* A system for dynamic modulator assignment for system exclusives
|
|
87
|
+
* @type {DynamicModulatorSystem}
|
|
88
|
+
*/
|
|
89
|
+
sysExModulators = new DynamicModulatorSystem();
|
|
90
|
+
|
|
84
91
|
/**
|
|
85
92
|
* Indicates whether the sustain (hold) pedal is active.
|
|
86
93
|
* @type {boolean}
|
|
@@ -147,12 +154,6 @@ class MidiAudioChannel
|
|
|
147
154
|
*/
|
|
148
155
|
lockedSystem = "gs";
|
|
149
156
|
|
|
150
|
-
/**
|
|
151
|
-
* Indicates whether the channel uses a preset from the override soundfont.
|
|
152
|
-
* @type {boolean}
|
|
153
|
-
*/
|
|
154
|
-
presetUsesOverride = false;
|
|
155
|
-
|
|
156
157
|
/**
|
|
157
158
|
* Indicates whether the GS NRPN parameters are enabled for this channel.
|
|
158
159
|
* @type {boolean}
|
|
@@ -229,10 +230,10 @@ class MidiAudioChannel
|
|
|
229
230
|
updateChannelTuning()
|
|
230
231
|
{
|
|
231
232
|
this.channelTuningCents =
|
|
232
|
-
this.customControllers[customControllers.channelTuning]
|
|
233
|
-
+ this.customControllers[customControllers.channelTransposeFine]
|
|
234
|
-
+ this.customControllers[customControllers.masterTuning]
|
|
235
|
-
+ (this.customControllers[customControllers.channelTuningSemitones] * 100);
|
|
233
|
+
this.customControllers[customControllers.channelTuning] // RPN channel fine tuning
|
|
234
|
+
+ this.customControllers[customControllers.channelTransposeFine] // user tuning (transpose)
|
|
235
|
+
+ this.customControllers[customControllers.masterTuning] // master tuning, set by sysEx
|
|
236
|
+
+ (this.customControllers[customControllers.channelTuningSemitones] * 100); // RPN channel coarse tuning
|
|
236
237
|
}
|
|
237
238
|
|
|
238
239
|
/**
|
|
@@ -361,7 +362,6 @@ class MidiAudioChannel
|
|
|
361
362
|
{
|
|
362
363
|
this.drumChannel = false;
|
|
363
364
|
}
|
|
364
|
-
this.presetUsesOverride = false;
|
|
365
365
|
this.synth.callEvent("drumchange", {
|
|
366
366
|
channel: this.channelNumber,
|
|
367
367
|
isDrumChannel: this.drumChannel
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { SpessaSynthWarn } from "../../../utils/loggin.js";
|
|
1
|
+
import { SpessaSynthInfo, SpessaSynthWarn } from "../../../utils/loggin.js";
|
|
2
2
|
import { isXGDrums } from "../../../utils/xg_hacks.js";
|
|
3
|
+
import { EMBEDDED_SOUND_BANK_ID } from "../../synth_constants.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* @typedef {Object} SoundFontType
|
|
@@ -47,7 +48,8 @@ export class SoundFontManager
|
|
|
47
48
|
const presets = new Set();
|
|
48
49
|
for (const p of font.soundfont.presets)
|
|
49
50
|
{
|
|
50
|
-
const
|
|
51
|
+
const bank = Math.min(128, p.bank + font.bankOffset);
|
|
52
|
+
const presetString = `${bank}-${p.program}`;
|
|
51
53
|
if (presets.has(presetString))
|
|
52
54
|
{
|
|
53
55
|
continue;
|
|
@@ -86,11 +88,8 @@ export class SoundFontManager
|
|
|
86
88
|
*/
|
|
87
89
|
reloadManager(soundFont)
|
|
88
90
|
{
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
* @type {SoundFontType[]}
|
|
92
|
-
*/
|
|
93
|
-
this.soundfontList = [];
|
|
91
|
+
// do not clear the embedded bank
|
|
92
|
+
this.soundfontList = this.soundfontList.filter(sf => sf.id === EMBEDDED_SOUND_BANK_ID);
|
|
94
93
|
this.soundfontList.push({
|
|
95
94
|
id: "main",
|
|
96
95
|
bankOffset: 0,
|
|
@@ -114,7 +113,7 @@ export class SoundFontManager
|
|
|
114
113
|
const index = this.soundfontList.findIndex(s => s.id === id);
|
|
115
114
|
if (index === -1)
|
|
116
115
|
{
|
|
117
|
-
|
|
116
|
+
SpessaSynthInfo(`No soundfont with id of "${id}" found. Aborting!`);
|
|
118
117
|
return;
|
|
119
118
|
}
|
|
120
119
|
delete this.soundfontList[index].soundfont.presets;
|
|
@@ -145,6 +144,15 @@ export class SoundFontManager
|
|
|
145
144
|
this.generatePresetList();
|
|
146
145
|
}
|
|
147
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Gets the current soundfont order
|
|
149
|
+
* @returns {string[]}
|
|
150
|
+
*/
|
|
151
|
+
getCurrentSoundFontOrder()
|
|
152
|
+
{
|
|
153
|
+
return this.soundfontList.map(s => s.id);
|
|
154
|
+
}
|
|
155
|
+
|
|
148
156
|
// noinspection JSUnusedGlobalSymbols
|
|
149
157
|
/**
|
|
150
158
|
* Rearranges the soundfonts
|
|
@@ -163,7 +171,7 @@ export class SoundFontManager
|
|
|
163
171
|
* @param bankNumber {number}
|
|
164
172
|
* @param programNumber {number}
|
|
165
173
|
* @param allowXGDrums {boolean} if true, allows XG drum banks (120, 126 and 127) as drum preset
|
|
166
|
-
* @returns {BasicPreset} the preset
|
|
174
|
+
* @returns {{preset: BasicPreset, bankOffset: number}} the preset and its bank offset
|
|
167
175
|
*/
|
|
168
176
|
getPreset(bankNumber, programNumber, allowXGDrums = false)
|
|
169
177
|
{
|
|
@@ -171,21 +179,24 @@ export class SoundFontManager
|
|
|
171
179
|
{
|
|
172
180
|
throw new Error("No soundfonts! Did you forget to add one?");
|
|
173
181
|
}
|
|
182
|
+
const isDrum = bankNumber === 128 || (allowXGDrums && isXGDrums(bankNumber));
|
|
174
183
|
for (const sf of this.soundfontList)
|
|
175
184
|
{
|
|
176
185
|
// check for the preset (with given offset)
|
|
177
186
|
const preset = sf.soundfont.getPresetNoFallback(
|
|
178
|
-
bankNumber - sf.bankOffset,
|
|
187
|
+
bankNumber === 128 ? 128 : bankNumber - sf.bankOffset,
|
|
179
188
|
programNumber,
|
|
180
189
|
allowXGDrums
|
|
181
190
|
);
|
|
182
191
|
if (preset !== undefined)
|
|
183
192
|
{
|
|
184
|
-
return
|
|
193
|
+
return {
|
|
194
|
+
preset: preset,
|
|
195
|
+
bankOffset: sf.bankOffset
|
|
196
|
+
};
|
|
185
197
|
}
|
|
186
198
|
// if not found, advance to the next soundfont
|
|
187
199
|
}
|
|
188
|
-
const isDrum = bankNumber === 128 || (allowXGDrums && isXGDrums(bankNumber));
|
|
189
200
|
// if none found, return the first correct preset found
|
|
190
201
|
if (!isDrum)
|
|
191
202
|
{
|
|
@@ -195,11 +206,18 @@ export class SoundFontManager
|
|
|
195
206
|
allowXGDrums));
|
|
196
207
|
if (preset)
|
|
197
208
|
{
|
|
198
|
-
return
|
|
209
|
+
return {
|
|
210
|
+
preset: preset,
|
|
211
|
+
bankOffset: sf.bankOffset
|
|
212
|
+
};
|
|
199
213
|
}
|
|
200
214
|
}
|
|
201
215
|
// if nothing at all, use the first preset
|
|
202
|
-
|
|
216
|
+
const sf = this.soundfontList[0];
|
|
217
|
+
return {
|
|
218
|
+
preset: sf.soundfont.presets[0],
|
|
219
|
+
bankOffset: sf.bankOffset
|
|
220
|
+
};
|
|
203
221
|
}
|
|
204
222
|
else
|
|
205
223
|
{
|
|
@@ -209,17 +227,27 @@ export class SoundFontManager
|
|
|
209
227
|
const p = sf.soundfont.presets.find(p => p.isDrumPreset(allowXGDrums) && p.program === programNumber);
|
|
210
228
|
if (p)
|
|
211
229
|
{
|
|
212
|
-
return
|
|
230
|
+
return {
|
|
231
|
+
preset: p,
|
|
232
|
+
bankOffset: sf.bankOffset
|
|
233
|
+
};
|
|
213
234
|
}
|
|
214
235
|
// check for any drum preset
|
|
215
236
|
const preset = sf.soundfont.presets.find(p => p.isDrumPreset(allowXGDrums));
|
|
216
237
|
if (preset)
|
|
217
238
|
{
|
|
218
|
-
return
|
|
239
|
+
return {
|
|
240
|
+
preset: preset,
|
|
241
|
+
bankOffset: sf.bankOffset
|
|
242
|
+
};
|
|
219
243
|
}
|
|
220
244
|
}
|
|
221
245
|
// if nothing at all, use the first preset
|
|
222
|
-
|
|
246
|
+
const sf = this.soundfontList[0];
|
|
247
|
+
return {
|
|
248
|
+
preset: sf.soundfont.presets[0],
|
|
249
|
+
bankOffset: sf.bankOffset
|
|
250
|
+
};
|
|
223
251
|
}
|
|
224
252
|
}
|
|
225
253
|
|
|
@@ -78,7 +78,7 @@ export function panVoice(voice,
|
|
|
78
78
|
if (reverbSend > 0)
|
|
79
79
|
{
|
|
80
80
|
// reverb is mono so we need to multiply by gain
|
|
81
|
-
const reverbGain = this.synth.reverbGain * gain * (reverbSend / REVERB_DIVIDER);
|
|
81
|
+
const reverbGain = this.synth.reverbGain * this.synth.reverbSend * gain * (reverbSend / REVERB_DIVIDER);
|
|
82
82
|
for (let i = 0; i < inputBuffer.length; i++)
|
|
83
83
|
{
|
|
84
84
|
reverbLeft[i] += reverbGain * inputBuffer[i];
|
|
@@ -91,7 +91,7 @@ export function panVoice(voice,
|
|
|
91
91
|
if (chorusSend > 0)
|
|
92
92
|
{
|
|
93
93
|
// chorus is stereo so we do not need to
|
|
94
|
-
const chorusGain = this.synth.chorusGain * chorusSend / CHORUS_DIVIDER;
|
|
94
|
+
const chorusGain = this.synth.chorusGain * this.synth.chorusSend * (chorusSend / CHORUS_DIVIDER);
|
|
95
95
|
const chorusLeftGain = gainLeft * chorusGain;
|
|
96
96
|
const chorusRightGain = gainRight * chorusGain;
|
|
97
97
|
for (let i = 0; i < inputBuffer.length; i++)
|