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
|
@@ -7,9 +7,12 @@ import { SpessaSynthWarn } from "../../../utils/loggin.js";
|
|
|
7
7
|
import { LowpassFilter } from "./lowpass_filter.js";
|
|
8
8
|
import { VolumeEnvelope } from "./volume_envelope.js";
|
|
9
9
|
import { ModulationEnvelope } from "./modulation_envelope.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
addAndClampGenerator,
|
|
12
|
+
GENERATORS_AMOUNT,
|
|
13
|
+
generatorTypes
|
|
14
|
+
} from "../../../soundfont/basic_soundfont/generator.js";
|
|
11
15
|
import { Modulator } from "../../../soundfont/basic_soundfont/modulator.js";
|
|
12
|
-
import { isSystemXG } from "../../../utils/xg_hacks.js";
|
|
13
16
|
|
|
14
17
|
const EXCLUSIVE_CUTOFF_TIME = -2320;
|
|
15
18
|
const EXCLUSIVE_MOD_CUTOFF_TIME = -1130; // less because filter shenanigans
|
|
@@ -162,12 +165,6 @@ class Voice
|
|
|
162
165
|
*/
|
|
163
166
|
isInRelease = false;
|
|
164
167
|
|
|
165
|
-
/**
|
|
166
|
-
* MIDI channel number.
|
|
167
|
-
* @type {number}
|
|
168
|
-
*/
|
|
169
|
-
channelNumber = 0;
|
|
170
|
-
|
|
171
168
|
/**
|
|
172
169
|
* Velocity of the note.
|
|
173
170
|
* @type {number}
|
|
@@ -272,7 +269,6 @@ class Voice
|
|
|
272
269
|
* @param audioSample {AudioSample}
|
|
273
270
|
* @param midiNote {number}
|
|
274
271
|
* @param velocity {number}
|
|
275
|
-
* @param channel {number}
|
|
276
272
|
* @param currentTime {number}
|
|
277
273
|
* @param targetKey {number}
|
|
278
274
|
* @param realKey {number}
|
|
@@ -284,7 +280,6 @@ class Voice
|
|
|
284
280
|
audioSample,
|
|
285
281
|
midiNote,
|
|
286
282
|
velocity,
|
|
287
|
-
channel,
|
|
288
283
|
currentTime,
|
|
289
284
|
targetKey,
|
|
290
285
|
realKey,
|
|
@@ -301,7 +296,6 @@ class Voice
|
|
|
301
296
|
|
|
302
297
|
this.velocity = velocity;
|
|
303
298
|
this.midiNote = midiNote;
|
|
304
|
-
this.channelNumber = channel;
|
|
305
299
|
this.startTime = currentTime;
|
|
306
300
|
this.targetKey = targetKey;
|
|
307
301
|
this.realKey = realKey;
|
|
@@ -333,7 +327,6 @@ class Voice
|
|
|
333
327
|
sample,
|
|
334
328
|
voice.midiNote,
|
|
335
329
|
voice.velocity,
|
|
336
|
-
voice.channelNumber,
|
|
337
330
|
currentTime,
|
|
338
331
|
voice.targetKey,
|
|
339
332
|
realKey,
|
|
@@ -372,53 +365,21 @@ class Voice
|
|
|
372
365
|
}
|
|
373
366
|
|
|
374
367
|
/**
|
|
375
|
-
* @param
|
|
368
|
+
* @param preset {BasicPreset} the preset to get voices for
|
|
369
|
+
* @param bank {number} the bank to cache the voices in
|
|
370
|
+
* @param program {number} program to cache the voices in
|
|
376
371
|
* @param midiNote {number} the MIDI note to use
|
|
377
372
|
* @param velocity {number} the velocity to use
|
|
378
373
|
* @param realKey {number} the real MIDI note if the "midiNote" was changed by MIDI Tuning Standard
|
|
379
374
|
* @this {SpessaSynthProcessor}
|
|
380
375
|
* @returns {Voice[]} output is an array of Voices
|
|
381
376
|
*/
|
|
382
|
-
export function
|
|
383
|
-
midiNote,
|
|
384
|
-
velocity,
|
|
385
|
-
realKey)
|
|
377
|
+
export function getVoicesForPreset(preset, bank, program, midiNote, velocity, realKey)
|
|
386
378
|
{
|
|
387
379
|
/**
|
|
388
380
|
* @type {Voice[]}
|
|
389
381
|
*/
|
|
390
|
-
|
|
391
|
-
const channelObject = this.midiAudioChannels[channel];
|
|
392
|
-
|
|
393
|
-
// override patch
|
|
394
|
-
const overridePatch = this.keyModifierManager.hasOverridePatch(channel, midiNote);
|
|
395
|
-
|
|
396
|
-
let bank = channelObject.getBankSelect();
|
|
397
|
-
let program = channelObject.preset.program;
|
|
398
|
-
if (overridePatch)
|
|
399
|
-
{
|
|
400
|
-
const override = this.keyModifierManager.getPatch(channel, midiNote);
|
|
401
|
-
bank = override.bank;
|
|
402
|
-
program = override.program;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const cached = this.getCachedVoice(bank, program, midiNote, velocity);
|
|
406
|
-
// if cached, return it!
|
|
407
|
-
if (cached !== undefined)
|
|
408
|
-
{
|
|
409
|
-
return cached.map(v => Voice.copy(v, this.currentSynthTime, realKey));
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// not cached...
|
|
413
|
-
let preset = channelObject.preset;
|
|
414
|
-
if (overridePatch)
|
|
415
|
-
{
|
|
416
|
-
preset = this.soundfontManager.getPreset(bank, program, isSystemXG(this.system));
|
|
417
|
-
}
|
|
418
|
-
/**
|
|
419
|
-
* @returns {Voice[]}
|
|
420
|
-
*/
|
|
421
|
-
voices = preset.getSamplesAndGenerators(midiNote, velocity)
|
|
382
|
+
const voices = preset.getSamplesAndGenerators(midiNote, velocity)
|
|
422
383
|
.reduce((voices, sampleAndGenerators) =>
|
|
423
384
|
{
|
|
424
385
|
if (sampleAndGenerators.sample.getAudioData() === undefined)
|
|
@@ -428,7 +389,7 @@ export function getVoices(channel,
|
|
|
428
389
|
}
|
|
429
390
|
|
|
430
391
|
// create the generator list
|
|
431
|
-
const generators = new Int16Array(
|
|
392
|
+
const generators = new Int16Array(GENERATORS_AMOUNT);
|
|
432
393
|
// apply and sum the gens
|
|
433
394
|
for (let i = 0; i < 60; i++)
|
|
434
395
|
{
|
|
@@ -501,7 +462,6 @@ export function getVoices(channel,
|
|
|
501
462
|
audioSample,
|
|
502
463
|
midiNote,
|
|
503
464
|
velocity,
|
|
504
|
-
channel,
|
|
505
465
|
this.currentSynthTime,
|
|
506
466
|
targetKey,
|
|
507
467
|
realKey,
|
|
@@ -515,4 +475,44 @@ export function getVoices(channel,
|
|
|
515
475
|
this.setCachedVoice(bank, program, midiNote, velocity, voices.map(v =>
|
|
516
476
|
Voice.copy(v, this.currentSynthTime, realKey)));
|
|
517
477
|
return voices;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* @param channel {number} a hint for the processor to recalculate sample cursors when sample dumping
|
|
482
|
+
* @param midiNote {number} the MIDI note to use
|
|
483
|
+
* @param velocity {number} the velocity to use
|
|
484
|
+
* @param realKey {number} the real MIDI note if the "midiNote" was changed by MIDI Tuning Standard
|
|
485
|
+
* @this {SpessaSynthProcessor}
|
|
486
|
+
* @returns {Voice[]} output is an array of Voices
|
|
487
|
+
*/
|
|
488
|
+
export function getVoices(channel, midiNote, velocity, realKey)
|
|
489
|
+
{
|
|
490
|
+
const channelObject = this.midiAudioChannels[channel];
|
|
491
|
+
|
|
492
|
+
// override patch
|
|
493
|
+
const overridePatch = this.keyModifierManager.hasOverridePatch(channel, midiNote);
|
|
494
|
+
|
|
495
|
+
let bank = channelObject.getBankSelect();
|
|
496
|
+
let program = channelObject.preset.program;
|
|
497
|
+
if (overridePatch)
|
|
498
|
+
{
|
|
499
|
+
const override = this.keyModifierManager.getPatch(channel, midiNote);
|
|
500
|
+
bank = override.bank;
|
|
501
|
+
program = override.program;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const cached = this.getCachedVoice(bank, program, midiNote, velocity);
|
|
505
|
+
// if cached, return it!
|
|
506
|
+
if (cached !== undefined)
|
|
507
|
+
{
|
|
508
|
+
return cached.map(v => Voice.copy(v, this.currentSynthTime, realKey));
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// not cached...
|
|
512
|
+
let preset = channelObject.preset;
|
|
513
|
+
if (overridePatch)
|
|
514
|
+
{
|
|
515
|
+
preset = this.getPreset(bank, program);
|
|
516
|
+
}
|
|
517
|
+
return this.getVoicesForPreset(preset, bank, program, midiNote, velocity, realKey);
|
|
518
518
|
}
|
|
@@ -43,7 +43,6 @@ export function resetAllControllers(log = true)
|
|
|
43
43
|
if (channelNumber % 16 === DEFAULT_PERCUSSION)
|
|
44
44
|
{
|
|
45
45
|
ch.setPreset(this.drumPreset);
|
|
46
|
-
ch.presetUsesOverride = this.defaultDrumsUsesOverride;
|
|
47
46
|
ch.drumChannel = true;
|
|
48
47
|
this.callEvent("drumchange", {
|
|
49
48
|
channel: channelNumber,
|
|
@@ -53,7 +52,6 @@ export function resetAllControllers(log = true)
|
|
|
53
52
|
else
|
|
54
53
|
{
|
|
55
54
|
ch.drumChannel = false;
|
|
56
|
-
ch.presetUsesOverride = this.defaultDrumsUsesOverride;
|
|
57
55
|
ch.setPreset(this.defaultPreset);
|
|
58
56
|
this.callEvent("drumchange", {
|
|
59
57
|
channel: channelNumber,
|
|
@@ -70,13 +68,11 @@ export function resetAllControllers(log = true)
|
|
|
70
68
|
}
|
|
71
69
|
|
|
72
70
|
const presetBank = ch.preset.bank;
|
|
73
|
-
const sentBank = presetBank === 128 ? 128 : (ch.presetUsesOverride ? presetBank + this.soundfontBankOffset : presetBank);
|
|
74
|
-
|
|
75
71
|
// call program change
|
|
76
72
|
this.callEvent("programchange", {
|
|
77
73
|
channel: channelNumber,
|
|
78
74
|
program: ch.preset.program,
|
|
79
|
-
bank:
|
|
75
|
+
bank: presetBank
|
|
80
76
|
});
|
|
81
77
|
|
|
82
78
|
for (let ccNum = 0; ccNum < 128; ccNum++)
|
|
@@ -154,12 +150,13 @@ export function resetControllers()
|
|
|
154
150
|
this.holdPedal = false;
|
|
155
151
|
this.randomPan = false;
|
|
156
152
|
|
|
153
|
+
this.sysExModulators.resetModulators();
|
|
154
|
+
|
|
157
155
|
// reset custom controllers
|
|
158
156
|
// special case: transpose does not get affected
|
|
159
157
|
const transpose = this.customControllers[customControllers.channelTransposeFine];
|
|
160
158
|
this.customControllers.set(customResetArray);
|
|
161
159
|
this.setCustomController(customControllers.channelTransposeFine, transpose);
|
|
162
|
-
|
|
163
160
|
this.resetParameters();
|
|
164
161
|
|
|
165
162
|
}
|
|
@@ -16,23 +16,29 @@ const registeredParameterTypes = {
|
|
|
16
16
|
resetParameters: 0x3FFF
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* @enum {number}
|
|
21
|
+
*/
|
|
22
|
+
const nonRegisteredGSMSB = {
|
|
23
|
+
partParameter: 0x01
|
|
24
|
+
};
|
|
25
|
+
|
|
19
26
|
/**
|
|
20
27
|
* https://cdn.roland.com/assets/media/pdf/SC-88PRO_OM.pdf
|
|
21
28
|
* http://hummer.stanford.edu/sig/doc/classes/MidiOutput/rpn.html
|
|
22
29
|
* @enum {number}
|
|
23
30
|
*/
|
|
24
|
-
const
|
|
25
|
-
partParameter: 0x01,
|
|
26
|
-
|
|
31
|
+
const nonRegisteredGSLSB = {
|
|
27
32
|
vibratoRate: 0x08,
|
|
28
33
|
vibratoDepth: 0x09,
|
|
29
34
|
vibratoDelay: 0x0A,
|
|
30
35
|
|
|
31
|
-
EGAttackTime: 0x64,
|
|
32
|
-
EGReleaseTime: 0x66,
|
|
33
|
-
|
|
34
36
|
TVFFilterCutoff: 0x20,
|
|
35
|
-
|
|
37
|
+
TVFFilterResonance: 0x21,
|
|
38
|
+
|
|
39
|
+
EGAttackTime: 0x63,
|
|
40
|
+
EGReleaseTime: 0x66
|
|
41
|
+
|
|
36
42
|
};
|
|
37
43
|
|
|
38
44
|
|
|
@@ -111,7 +117,7 @@ export function dataEntryCoarse(dataValue)
|
|
|
111
117
|
break;
|
|
112
118
|
|
|
113
119
|
// part parameters: vibrato, cutoff
|
|
114
|
-
case
|
|
120
|
+
case nonRegisteredGSMSB.partParameter:
|
|
115
121
|
switch (NRPNFine)
|
|
116
122
|
{
|
|
117
123
|
default:
|
|
@@ -133,7 +139,7 @@ export function dataEntryCoarse(dataValue)
|
|
|
133
139
|
break;
|
|
134
140
|
|
|
135
141
|
// vibrato rate
|
|
136
|
-
case
|
|
142
|
+
case nonRegisteredGSLSB.vibratoRate:
|
|
137
143
|
if (dataValue === 64)
|
|
138
144
|
{
|
|
139
145
|
return;
|
|
@@ -144,7 +150,7 @@ export function dataEntryCoarse(dataValue)
|
|
|
144
150
|
break;
|
|
145
151
|
|
|
146
152
|
// vibrato depth
|
|
147
|
-
case
|
|
153
|
+
case nonRegisteredGSLSB.vibratoDepth:
|
|
148
154
|
if (dataValue === 64)
|
|
149
155
|
{
|
|
150
156
|
return;
|
|
@@ -155,7 +161,7 @@ export function dataEntryCoarse(dataValue)
|
|
|
155
161
|
break;
|
|
156
162
|
|
|
157
163
|
// vibrato delay
|
|
158
|
-
case
|
|
164
|
+
case nonRegisteredGSLSB.vibratoDelay:
|
|
159
165
|
if (dataValue === 64)
|
|
160
166
|
{
|
|
161
167
|
return;
|
|
@@ -166,34 +172,27 @@ export function dataEntryCoarse(dataValue)
|
|
|
166
172
|
break;
|
|
167
173
|
|
|
168
174
|
// filter cutoff
|
|
169
|
-
case
|
|
175
|
+
case nonRegisteredGSLSB.TVFFilterCutoff:
|
|
170
176
|
// affect the "brightness" controller as we have a default modulator that controls it
|
|
171
177
|
this.controllerChange(midiControllers.brightness, dataValue);
|
|
172
178
|
coolInfo("Filter cutoff", dataValue.toString(), "");
|
|
173
179
|
break;
|
|
174
180
|
|
|
175
181
|
// attack time
|
|
176
|
-
case
|
|
182
|
+
case nonRegisteredGSLSB.EGAttackTime:
|
|
177
183
|
// affect the "attack time" controller as we have a default modulator that controls it
|
|
178
184
|
this.controllerChange(midiControllers.attackTime, dataValue);
|
|
179
185
|
coolInfo("EG attack time", dataValue.toString(), "");
|
|
180
186
|
break;
|
|
181
187
|
|
|
182
188
|
// release time
|
|
183
|
-
case
|
|
189
|
+
case nonRegisteredGSLSB.EGReleaseTime:
|
|
184
190
|
// affect the "release time" controller as we have a default modulator that controls it
|
|
185
191
|
this.controllerChange(midiControllers.releaseTime, dataValue);
|
|
186
192
|
coolInfo("EG release time", dataValue.toString(), "");
|
|
187
193
|
break;
|
|
188
194
|
}
|
|
189
195
|
break;
|
|
190
|
-
|
|
191
|
-
// drum reverb
|
|
192
|
-
case nonRegisteredParameterNumbers.drumReverb:
|
|
193
|
-
const reverb = dataValue;
|
|
194
|
-
this.controllerChange(midiControllers.reverbDepth, reverb);
|
|
195
|
-
coolInfo("GS Drum reverb", reverb.toString(), "percent");
|
|
196
|
-
break;
|
|
197
196
|
}
|
|
198
197
|
break;
|
|
199
198
|
|
|
@@ -2,6 +2,8 @@ import { computeModulators } from "../engine_components/compute_modulator.js";
|
|
|
2
2
|
import { generatorTypes } from "../../../soundfont/basic_soundfont/generator.js";
|
|
3
3
|
import { midiControllers } from "../../../midi/midi_message.js";
|
|
4
4
|
import { portamentoTimeToSeconds } from "./portamento_time.js";
|
|
5
|
+
import { customControllers } from "../engine_components/controller_tables.js";
|
|
6
|
+
import { Modulator } from "../../../soundfont/basic_soundfont/modulator.js";
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* sends a "MIDI Note on message"
|
|
@@ -27,8 +29,8 @@ export function noteOn(midiNote, velocity)
|
|
|
27
29
|
return;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
const realKey = midiNote + this.channelTransposeKeyShift;
|
|
31
|
-
let
|
|
32
|
+
const realKey = midiNote + this.channelTransposeKeyShift + this.customControllers[customControllers.channelKeyShift];
|
|
33
|
+
let internalMidiNote = realKey;
|
|
32
34
|
|
|
33
35
|
if (realKey > 127 || realKey < 0)
|
|
34
36
|
{
|
|
@@ -38,7 +40,7 @@ export function noteOn(midiNote, velocity)
|
|
|
38
40
|
const tune = this.synth.tunings[program]?.[realKey]?.midiNote;
|
|
39
41
|
if (tune >= 0)
|
|
40
42
|
{
|
|
41
|
-
|
|
43
|
+
internalMidiNote = tune;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
// velocity override
|
|
@@ -66,7 +68,7 @@ export function noteOn(midiNote, velocity)
|
|
|
66
68
|
const currentFromKey = control >> 7;
|
|
67
69
|
if (
|
|
68
70
|
!this.drumChannel && // no portamento on drum channel
|
|
69
|
-
currentFromKey !==
|
|
71
|
+
currentFromKey !== internalMidiNote && // if the same note, there's no portamento
|
|
70
72
|
this.midiControllers[midiControllers.portamentoOnOff] >= 8192 && // (64 << 7)
|
|
71
73
|
portamentoTime > 0 // 0 duration is no portamento
|
|
72
74
|
)
|
|
@@ -74,18 +76,17 @@ export function noteOn(midiNote, velocity)
|
|
|
74
76
|
// a value of one means the initial portamento
|
|
75
77
|
if (control !== 1)
|
|
76
78
|
{
|
|
77
|
-
const diff = Math.abs(
|
|
79
|
+
const diff = Math.abs(internalMidiNote - currentFromKey);
|
|
78
80
|
portamentoDuration = portamentoTimeToSeconds(portamentoTime, diff);
|
|
79
81
|
portamentoFromKey = currentFromKey;
|
|
80
82
|
}
|
|
81
83
|
// set portamento control to previous value
|
|
82
|
-
this.controllerChange(midiControllers.portamentoControl,
|
|
84
|
+
this.controllerChange(midiControllers.portamentoControl, internalMidiNote);
|
|
83
85
|
}
|
|
84
|
-
|
|
85
86
|
// get voices
|
|
86
87
|
const voices = this.synth.getVoices(
|
|
87
88
|
this.channelNumber,
|
|
88
|
-
|
|
89
|
+
internalMidiNote,
|
|
89
90
|
velocity,
|
|
90
91
|
realKey
|
|
91
92
|
);
|
|
@@ -98,6 +99,9 @@ export function noteOn(midiNote, velocity)
|
|
|
98
99
|
panOverride = Math.round(Math.random() * 1000 - 500);
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
// dynamic modulators (sysEx)
|
|
103
|
+
const dynamicModulators = this.sysExModulators.getModulators();
|
|
104
|
+
|
|
101
105
|
// add voices
|
|
102
106
|
const channelVoices = this.voices;
|
|
103
107
|
voices.forEach(voice =>
|
|
@@ -112,6 +116,22 @@ export function noteOn(midiNote, velocity)
|
|
|
112
116
|
// apply gain override
|
|
113
117
|
voice.gain = voiceGain;
|
|
114
118
|
|
|
119
|
+
dynamicModulators.forEach(mod =>
|
|
120
|
+
{
|
|
121
|
+
const existingModIndex = voice.modulators.findIndex(voiceMod => Modulator.isIdentical(voiceMod, mod));
|
|
122
|
+
|
|
123
|
+
// replace or add
|
|
124
|
+
if (existingModIndex !== -1)
|
|
125
|
+
{
|
|
126
|
+
voice.modulators[existingModIndex] = Modulator.copy(mod);
|
|
127
|
+
}
|
|
128
|
+
else
|
|
129
|
+
{
|
|
130
|
+
voice.modulators.push(Modulator.copy(mod));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
|
|
115
135
|
// apply exclusive class
|
|
116
136
|
const exclusive = voice.exclusiveClass;
|
|
117
137
|
if (exclusive !== 0)
|
|
@@ -11,51 +11,16 @@ export function programChange(programNumber)
|
|
|
11
11
|
}
|
|
12
12
|
// always 128 for percussion
|
|
13
13
|
let bank = this.getBankSelect();
|
|
14
|
-
let sentBank;
|
|
15
|
-
/**
|
|
16
|
-
* @type {BasicPreset}
|
|
17
|
-
*/
|
|
18
|
-
let preset;
|
|
19
14
|
|
|
20
15
|
const isXG = this.isXGChannel;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
{
|
|
24
|
-
const bankWithOffset = bank === 128 ? 128 : bank - this.synth.soundfontBankOffset;
|
|
25
|
-
const p = this.synth.overrideSoundfont.getPresetNoFallback(
|
|
26
|
-
bankWithOffset,
|
|
27
|
-
programNumber,
|
|
28
|
-
isXG
|
|
29
|
-
);
|
|
30
|
-
if (p)
|
|
31
|
-
{
|
|
32
|
-
sentBank = p.bank === 128 ? 128 : p.bank + this.synth.soundfontBankOffset;
|
|
33
|
-
preset = p;
|
|
34
|
-
this.presetUsesOverride = true;
|
|
35
|
-
}
|
|
36
|
-
else
|
|
37
|
-
{
|
|
38
|
-
preset = this.synth.soundfontManager.getPreset(bank, programNumber, isXG);
|
|
39
|
-
const offset = this.synth.soundfontManager.soundfontList
|
|
40
|
-
.find(s => s.soundfont === preset.parentSoundBank).bankOffset;
|
|
41
|
-
sentBank = preset.bank - offset;
|
|
42
|
-
this.presetUsesOverride = false;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
else
|
|
46
|
-
{
|
|
47
|
-
preset = this.synth.soundfontManager.getPreset(bank, programNumber, isXG);
|
|
48
|
-
const offset = this.synth.soundfontManager.soundfontList
|
|
49
|
-
.find(s => s.soundfont === preset.parentSoundBank).bankOffset;
|
|
50
|
-
sentBank = preset.bank - offset;
|
|
51
|
-
this.presetUsesOverride = false;
|
|
52
|
-
}
|
|
16
|
+
const p = this.synth.soundfontManager.getPreset(bank, programNumber, isXG);
|
|
17
|
+
const preset = p.preset;
|
|
53
18
|
this.setPreset(preset);
|
|
54
|
-
this.sentBank =
|
|
19
|
+
this.sentBank = Math.min(128, preset.bank + p.bankOffset);
|
|
55
20
|
this.synth.callEvent("programchange", {
|
|
56
21
|
channel: this.channelNumber,
|
|
57
22
|
program: preset.program,
|
|
58
|
-
bank: sentBank
|
|
23
|
+
bank: this.sentBank
|
|
59
24
|
});
|
|
60
25
|
this.sendChannelProperty();
|
|
61
26
|
}
|
|
@@ -88,26 +88,33 @@ export function renderVoice(
|
|
|
88
88
|
// calculate tuning by key using soundfont's scale tuning
|
|
89
89
|
cents += (targetKey - voice.sample.rootKey) * voice.modulatedGenerators[generatorTypes.scaleTuning];
|
|
90
90
|
|
|
91
|
+
// low pass excursion with LFO and mod envelope
|
|
92
|
+
let lowpassExcursion = 0;
|
|
93
|
+
let volumeExcursionCentibels = 0;
|
|
94
|
+
|
|
91
95
|
// vibrato LFO
|
|
92
|
-
const
|
|
93
|
-
|
|
96
|
+
const vibPitchDepth = voice.modulatedGenerators[generatorTypes.vibLfoToPitch];
|
|
97
|
+
const vibVolDepth = voice.modulatedGenerators[generatorTypes.vibLfoToVolume];
|
|
98
|
+
const vibFilterDepth = voice.modulatedGenerators[generatorTypes.vibLfoToFilterFc];
|
|
99
|
+
if (vibPitchDepth !== 0 || vibVolDepth !== 0 || vibFilterDepth !== 0)
|
|
94
100
|
{
|
|
95
101
|
// calculate start time and lfo value
|
|
96
102
|
const vibStart = voice.startTime + timecentsToSeconds(voice.modulatedGenerators[generatorTypes.delayVibLFO]);
|
|
97
103
|
const vibFreqHz = absCentsToHz(voice.modulatedGenerators[generatorTypes.freqVibLFO]);
|
|
98
|
-
const
|
|
104
|
+
const vibLfoValue = getLFOValue(vibStart, vibFreqHz, timeNow);
|
|
99
105
|
// use modulation multiplier (RPN modulation depth)
|
|
100
|
-
cents +=
|
|
106
|
+
cents += vibLfoValue * (vibPitchDepth * this.customControllers[customControllers.modulationMultiplier]);
|
|
107
|
+
// vol env volume offset
|
|
108
|
+
// negate the lfo value because audigy starts with increase rather than decrease
|
|
109
|
+
volumeExcursionCentibels += -vibLfoValue * vibVolDepth;
|
|
110
|
+
// low pass frequency
|
|
111
|
+
lowpassExcursion += vibLfoValue * vibFilterDepth;
|
|
101
112
|
}
|
|
102
113
|
|
|
103
|
-
// low pass excursion with LFO and mod envelope
|
|
104
|
-
let lowpassExcursion = 0;
|
|
105
|
-
|
|
106
114
|
// mod LFO
|
|
107
115
|
const modPitchDepth = voice.modulatedGenerators[generatorTypes.modLfoToPitch];
|
|
108
116
|
const modVolDepth = voice.modulatedGenerators[generatorTypes.modLfoToVolume];
|
|
109
117
|
const modFilterDepth = voice.modulatedGenerators[generatorTypes.modLfoToFilterFc];
|
|
110
|
-
let modLfoCentibels = 0;
|
|
111
118
|
// don't compute mod lfo unless necessary
|
|
112
119
|
if (modPitchDepth !== 0 || modFilterDepth !== 0 || modVolDepth !== 0)
|
|
113
120
|
{
|
|
@@ -117,9 +124,9 @@ export function renderVoice(
|
|
|
117
124
|
const modLfoValue = getLFOValue(modStart, modFreqHz, timeNow);
|
|
118
125
|
// use modulation multiplier (RPN modulation depth)
|
|
119
126
|
cents += modLfoValue * (modPitchDepth * this.customControllers[customControllers.modulationMultiplier]);
|
|
120
|
-
//
|
|
127
|
+
// vol env volume offset
|
|
121
128
|
// negate the lfo value because audigy starts with increase rather than decrease
|
|
122
|
-
|
|
129
|
+
volumeExcursionCentibels += -modLfoValue * modVolDepth;
|
|
123
130
|
// low pass frequency
|
|
124
131
|
lowpassExcursion += modLfoValue * modFilterDepth;
|
|
125
132
|
}
|
|
@@ -184,7 +191,7 @@ export function renderVoice(
|
|
|
184
191
|
LowpassFilter.apply(voice, bufferOut, lowpassExcursion, this.synth.filterSmoothingFactor);
|
|
185
192
|
|
|
186
193
|
// vol env
|
|
187
|
-
VolumeEnvelope.apply(voice, bufferOut,
|
|
194
|
+
VolumeEnvelope.apply(voice, bufferOut, volumeExcursionCentibels, this.synth.volumeEnvelopeSmoothingFactor);
|
|
188
195
|
|
|
189
196
|
this.panVoice(
|
|
190
197
|
voice,
|
package/src/synthetizer/audio_engine/engine_methods/soundfont_management/embedded_sound_bank.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { loadSoundFont } from "../../../../soundfont/load_soundfont.js";
|
|
2
|
+
import { SpessaSynthInfo } from "../../../../utils/loggin.js";
|
|
3
|
+
import { consoleColors } from "../../../../utils/other.js";
|
|
4
|
+
import { EMBEDDED_SOUND_BANK_ID } from "../../../synth_constants.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @this {SpessaSynthProcessor}
|
|
8
|
+
*/
|
|
9
|
+
export function clearEmbeddedBank()
|
|
10
|
+
{
|
|
11
|
+
if (this.soundfontManager.soundfontList.some(s => s.id === EMBEDDED_SOUND_BANK_ID))
|
|
12
|
+
{
|
|
13
|
+
this.soundfontManager.deleteSoundFont(EMBEDDED_SOUND_BANK_ID);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Sets the embedded (RMI soundfont)
|
|
19
|
+
* @param font {ArrayBuffer}
|
|
20
|
+
* @param offset {number}
|
|
21
|
+
* @this {SpessaSynthProcessor}
|
|
22
|
+
*/
|
|
23
|
+
export function setEmbeddedSoundFont(font, offset)
|
|
24
|
+
{
|
|
25
|
+
// the embedded bank is set as the first bank in the manager,
|
|
26
|
+
// with a special ID that does not clear when reloadManager is performed.
|
|
27
|
+
this.soundfontBankOffset = offset;
|
|
28
|
+
const loadedFont = loadSoundFont(font);
|
|
29
|
+
this.soundfontManager.addNewSoundFont(loadedFont, EMBEDDED_SOUND_BANK_ID, offset);
|
|
30
|
+
// rearrange so the embedded is first (most important as it overrides all others)
|
|
31
|
+
const order = this.soundfontManager.getCurrentSoundFontOrder();
|
|
32
|
+
order.pop();
|
|
33
|
+
order.unshift(EMBEDDED_SOUND_BANK_ID);
|
|
34
|
+
this.soundfontManager.rearrangeSoundFonts(order);
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
// apply snapshot again if applicable
|
|
38
|
+
if (this._snapshot !== undefined)
|
|
39
|
+
{
|
|
40
|
+
this.applySynthesizerSnapshot(this._snapshot);
|
|
41
|
+
}
|
|
42
|
+
SpessaSynthInfo(`%cEmbedded sound bank set at offset %c${offset}`, consoleColors.recognized, consoleColors.value);
|
|
43
|
+
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { isSystemXG } from "../../../../utils/xg_hacks.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @this {SpessaSynthProcessor}
|
|
5
|
-
* @param program {number}
|
|
6
|
-
* @param bank {number}
|
|
7
|
-
* @returns {BasicPreset}
|
|
8
|
-
*/
|
|
9
|
-
export function getPreset(bank, program)
|
|
10
|
-
{
|
|
11
|
-
if (this.overrideSoundfont)
|
|
12
|
-
{
|
|
13
|
-
// if override soundfont
|
|
14
|
-
const bankWithOffset = bank === 128 ? 128 : bank - this.soundfontBankOffset;
|
|
15
|
-
const preset = this.overrideSoundfont.getPresetNoFallback(bankWithOffset, program, isSystemXG(this.system));
|
|
16
|
-
if (preset)
|
|
17
|
-
{
|
|
18
|
-
return preset;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return this.soundfontManager.getPreset(bank, program, isSystemXG(this.system));
|
|
22
|
-
}
|
package/src/synthetizer/audio_engine/engine_methods/soundfont_management/update_preset_list.js
CHANGED
|
@@ -7,28 +7,13 @@ export function updatePresetList()
|
|
|
7
7
|
* @type {{bank: number, presetName: string, program: number}[]}
|
|
8
8
|
*/
|
|
9
9
|
const mainFont = this.soundfontManager.getPresetList();
|
|
10
|
-
if (this.overrideSoundfont !== undefined)
|
|
11
|
-
{
|
|
12
|
-
this.overrideSoundfont.presets.forEach(p =>
|
|
13
|
-
{
|
|
14
|
-
const bankCheck = p.bank === 128 ? 128 : p.bank + this.soundfontBankOffset;
|
|
15
|
-
const exists = mainFont.find(pr => pr.bank === bankCheck && pr.program === p.program);
|
|
16
|
-
if (exists !== undefined)
|
|
17
|
-
{
|
|
18
|
-
exists.presetName = p.presetName;
|
|
19
|
-
}
|
|
20
|
-
else
|
|
21
|
-
{
|
|
22
|
-
mainFont.push({
|
|
23
|
-
presetName: p.presetName,
|
|
24
|
-
bank: bankCheck,
|
|
25
|
-
program: p.program
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
10
|
this.clearCache();
|
|
31
11
|
this.callEvent("presetlistchange", mainFont);
|
|
32
12
|
this.getDefaultPresets();
|
|
13
|
+
// unlock presets
|
|
14
|
+
this.midiAudioChannels.forEach(c =>
|
|
15
|
+
{
|
|
16
|
+
c.setPresetLock(false);
|
|
17
|
+
});
|
|
33
18
|
this.resetAllControllers(false);
|
|
34
19
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { generatorTypes } from "../../../../soundfont/basic_soundfont/generator.js";
|
|
2
|
+
import { customControllers } from "../../engine_components/controller_tables.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Stops a note nearly instantly
|
|
@@ -8,6 +9,8 @@ import { generatorTypes } from "../../../../soundfont/basic_soundfont/generator.
|
|
|
8
9
|
*/
|
|
9
10
|
export function killNote(midiNote, releaseTime = -12000)
|
|
10
11
|
{
|
|
12
|
+
midiNote += this.customControllers[customControllers.channelKeyShift];
|
|
13
|
+
|
|
11
14
|
this.voices.forEach(v =>
|
|
12
15
|
{
|
|
13
16
|
if (v.realKey !== midiNote)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SpessaSynthWarn } from "../../../../utils/loggin.js";
|
|
2
|
+
import { customControllers } from "../../engine_components/controller_tables.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Release a note
|
|
@@ -13,7 +14,7 @@ export function noteOff(midiNote)
|
|
|
13
14
|
return;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
let realKey = midiNote + this.channelTransposeKeyShift;
|
|
17
|
+
let realKey = midiNote + this.channelTransposeKeyShift + this.customControllers[customControllers.channelKeyShift];
|
|
17
18
|
|
|
18
19
|
// if high performance mode, kill notes instead of stopping them
|
|
19
20
|
if (this.synth.highPerformanceMode)
|