spessasynth_core 3.26.5 → 3.26.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -8
- package/package.json +1 -1
- package/src/midi/basic_midi.js +4 -1
- 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 -10
- package/src/synthetizer/audio_engine/engine_components/stereo_panner.js +2 -2
- package/src/synthetizer/audio_engine/engine_components/voice.js +6 -2
- package/src/synthetizer/audio_engine/engine_methods/controller_control/reset_controllers.js +2 -1
- 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/render_voice.js +18 -11
- 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 +11 -0
package/README.md
CHANGED
|
@@ -21,6 +21,12 @@ npm install --save spessasynth_core
|
|
|
21
21
|
|
|
22
22
|
> Note: This is the new heart of the SpessaSynth library, after the repository has been split.
|
|
23
23
|
|
|
24
|
+
**Project index**
|
|
25
|
+
|
|
26
|
+
- spessasynth_core (you are here) - SF2/DLS/MIDI library
|
|
27
|
+
- [spessasynth_lib](https://github.com/spessasus/spessasynth_lib) - spessasynth_core wrapper optimized for browsers and WebAudioAPI
|
|
28
|
+
- [SpessaSynth](https://github.com/spessasus/SpessaSynth) - online/local web player/editor application
|
|
29
|
+
|
|
24
30
|
## Current Features
|
|
25
31
|
|
|
26
32
|
### Easy Integration
|
|
@@ -87,7 +93,7 @@ npm install --save spessasynth_core
|
|
|
87
93
|
- **Compression and trimming support:** Reduce a MIDI file with a 1GB soundfont to **as small as 5MB**!
|
|
88
94
|
- **DLS Version support:** The original legacy format with bank offset detection!
|
|
89
95
|
- **Automatic bank shifting and validation:** Every soundfont *just works!*
|
|
90
|
-
- **Metadata support:** Add title, artist, album name and cover and more! And of course read them too! *(In any encoding!)*
|
|
96
|
+
- **Metadata support:** Add title, artist, album name and cover and more! And of course, read them too! *(In any encoding!)*
|
|
91
97
|
- **Compatible with [Falcosoft Midi Player 6!](https://falcosoft.hu/softwares.html#midiplayer)**
|
|
92
98
|
- **Easy saving:** [As simple as saving a MIDI file!](https://github.com/spessasus/spessasynth_core/wiki/Writing-MIDI-Files#writermidi)
|
|
93
99
|
|
|
@@ -120,13 +126,9 @@ npm install --save spessasynth_core
|
|
|
120
126
|
- **Loop multiple times:** *Render two (or more) loops into the file for seamless transitions!*
|
|
121
127
|
- *That's right, saving as WAV is also [just one function!](https://github.com/spessasus/spessasynth_core/wiki/Writing-Wave-Files#audiobuffertowav)*
|
|
122
128
|
|
|
123
|
-
|
|
124
|
-
- Synth's performance may be
|
|
125
|
-
- [SF2 to DLS Conversion](https://github.com/spessasus/SpessaSynth/wiki/DLS-Conversion-Problem)
|
|
126
|
-
- Audio may sometimes sound distorted in Chrome, Edge, Brave,
|
|
127
|
-
etc. due to a **[Chromium Bug](https://issues.chromium.org/issues/367304685).**
|
|
128
|
-
I can't do anything about it, only hope that it gets fixed.
|
|
129
|
-
Please consider voting for it on the bug tracker to get it fixed!
|
|
129
|
+
### Limitations
|
|
130
|
+
- Synth's performance may be questionable sometimes
|
|
131
|
+
- [SF2 to DLS Conversion limits](https://github.com/spessasus/SpessaSynth/wiki/DLS-Conversion-Problem)
|
|
130
132
|
|
|
131
133
|
#### TODO
|
|
132
134
|
- Improve the performance of the engine
|
package/package.json
CHANGED
package/src/midi/basic_midi.js
CHANGED
|
@@ -402,6 +402,9 @@ 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)
|
|
406
|
+
this.midiPortChannelOffsets = [...this.midiPortChannelOffsets].map(o => o ?? 0);
|
|
407
|
+
|
|
405
408
|
// fix midi ports:
|
|
406
409
|
// midi tracks without ports will have a value of -1
|
|
407
410
|
// if all ports have a value of -1, set it to 0,
|
|
@@ -426,7 +429,7 @@ class BasicMIDI extends MIDISequenceData
|
|
|
426
429
|
{
|
|
427
430
|
defaultPort = 0;
|
|
428
431
|
}
|
|
429
|
-
this.midiPorts = this.midiPorts.map(port => port === -1 ? defaultPort : port);
|
|
432
|
+
this.midiPorts = this.midiPorts.map(port => port === -1 || port === undefined ? defaultPort : port);
|
|
430
433
|
// add fake port if empty
|
|
431
434
|
if (this.midiPortChannelOffsets.length === 0)
|
|
432
435
|
{
|
|
@@ -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
|
/**
|
|
@@ -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++)
|
|
@@ -7,7 +7,11 @@ 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
16
|
|
|
13
17
|
const EXCLUSIVE_CUTOFF_TIME = -2320;
|
|
@@ -385,7 +389,7 @@ export function getVoicesForPreset(preset, bank, program, midiNote, velocity, re
|
|
|
385
389
|
}
|
|
386
390
|
|
|
387
391
|
// create the generator list
|
|
388
|
-
const generators = new Int16Array(
|
|
392
|
+
const generators = new Int16Array(GENERATORS_AMOUNT);
|
|
389
393
|
// apply and sum the gens
|
|
390
394
|
for (let i = 0; i < 60; i++)
|
|
391
395
|
{
|
|
@@ -150,12 +150,13 @@ export function resetControllers()
|
|
|
150
150
|
this.holdPedal = false;
|
|
151
151
|
this.randomPan = false;
|
|
152
152
|
|
|
153
|
+
this.sysExModulators.resetModulators();
|
|
154
|
+
|
|
153
155
|
// reset custom controllers
|
|
154
156
|
// special case: transpose does not get affected
|
|
155
157
|
const transpose = this.customControllers[customControllers.channelTransposeFine];
|
|
156
158
|
this.customControllers.set(customResetArray);
|
|
157
159
|
this.setCustomController(customControllers.channelTransposeFine, transpose);
|
|
158
|
-
|
|
159
160
|
this.resetParameters();
|
|
160
161
|
|
|
161
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)
|
|
@@ -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,
|
|
@@ -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)
|