spessasynth_lib 3.20.1 → 3.20.3
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/@types/soundfont/{soundfont.d.ts → read_sf2/soundfont.d.ts} +4 -4
- package/midi_parser/midi_loader.js +0 -1
- package/package.json +5 -2
- package/sequencer/worklet_sequencer/play.js +1 -3
- package/sequencer/worklet_sequencer/process_event.js +1 -2
- package/sequencer/worklet_sequencer/worklet_sequencer.js +1 -2
- package/soundfont/dls/read_articulation.js +43 -6
- package/soundfont/load_soundfont.js +1 -1
- package/soundfont/{soundfont.js → read_sf2/soundfont.js} +14 -14
- package/synthetizer/worklet_processor.min.js +9 -9
- package/synthetizer/worklet_system/worklet_methods/note_on.js +4 -2
- package/synthetizer/worklet_system/worklet_methods/voice_control.js +7 -7
- package/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js +143 -45
- package/synthetizer/worklet_system/worklet_utilities/stereo_panner.js +2 -2
- package/synthetizer/worklet_system/worklet_utilities/volume_envelope.js +302 -217
- package/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js +7 -3
- package/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +12 -14
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { getWorkletVoices } from '../worklet_utilities/worklet_voice.js'
|
|
2
2
|
import { generatorTypes } from '../../../soundfont/read_sf2/generators.js'
|
|
3
3
|
import { computeModulators } from '../worklet_utilities/worklet_modulator.js'
|
|
4
|
-
import {
|
|
4
|
+
import { WorkletVolumeEnvelope } from '../worklet_utilities/volume_envelope.js'
|
|
5
|
+
import { WorkletModulationEnvelope } from '../worklet_utilities/modulation_envelope.js'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Append the voices
|
|
@@ -72,7 +73,8 @@ export function noteOn(channel, midiNote, velocity, enableDebugging = false, sen
|
|
|
72
73
|
this.releaseVoice(v);
|
|
73
74
|
v.modulatedGenerators[generatorTypes.releaseVolEnv] = -7000; // make the release nearly instant
|
|
74
75
|
v.modulatedGenerators[generatorTypes.releaseModEnv] = -7000;
|
|
75
|
-
|
|
76
|
+
WorkletVolumeEnvelope.recalculate(v);
|
|
77
|
+
WorkletModulationEnvelope.recalculate(v);
|
|
76
78
|
}
|
|
77
79
|
})
|
|
78
80
|
}
|
|
@@ -2,12 +2,12 @@ import { generatorTypes } from '../../../soundfont/read_sf2/generators.js'
|
|
|
2
2
|
import { absCentsToHz, decibelAttenuationToGain, timecentsToSeconds } from '../worklet_utilities/unit_converter.js'
|
|
3
3
|
import { getLFOValue } from '../worklet_utilities/lfo.js'
|
|
4
4
|
import { customControllers } from '../worklet_utilities/worklet_processor_channel.js'
|
|
5
|
-
import {
|
|
5
|
+
import { WorkletModulationEnvelope } from '../worklet_utilities/modulation_envelope.js'
|
|
6
6
|
import { getOscillatorData } from '../worklet_utilities/wavetable_oscillator.js'
|
|
7
7
|
import { panVoice } from '../worklet_utilities/stereo_panner.js'
|
|
8
|
-
import { applyVolumeEnvelope, recalculateVolumeEnvelope } from '../worklet_utilities/volume_envelope.js'
|
|
9
8
|
import { applyLowpassFilter } from '../worklet_utilities/lowpass_filter.js'
|
|
10
9
|
import { MIN_NOTE_LENGTH } from '../main_processor.js'
|
|
10
|
+
import { WorkletVolumeEnvelope } from '../worklet_utilities/volume_envelope.js'
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
const HALF_PI = Math.PI / 2;
|
|
@@ -36,10 +36,10 @@ export function renderVoice(
|
|
|
36
36
|
// if not in release, check if the release time is
|
|
37
37
|
if (currentTime >= voice.releaseStartTime)
|
|
38
38
|
{
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
voice.isInRelease = true;
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
WorkletVolumeEnvelope.startRelease(voice);
|
|
42
|
+
WorkletModulationEnvelope.startRelease(voice);
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -127,7 +127,7 @@ export function renderVoice(
|
|
|
127
127
|
// mod env
|
|
128
128
|
const modEnvPitchDepth = voice.modulatedGenerators[generatorTypes.modEnvToPitch];
|
|
129
129
|
const modEnvFilterDepth = voice.modulatedGenerators[generatorTypes.modEnvToFilterFc];
|
|
130
|
-
const modEnv =
|
|
130
|
+
const modEnv = WorkletModulationEnvelope.getValue(voice, currentTime);
|
|
131
131
|
// apply values
|
|
132
132
|
lowpassCents += modEnv * modEnvFilterDepth;
|
|
133
133
|
cents += modEnv * modEnvPitchDepth;
|
|
@@ -153,7 +153,7 @@ export function renderVoice(
|
|
|
153
153
|
applyLowpassFilter(voice, bufferOut, lowpassCents);
|
|
154
154
|
|
|
155
155
|
// volenv
|
|
156
|
-
|
|
156
|
+
WorkletVolumeEnvelope.apply(voice, bufferOut, modLfoCentibels, this.volumeEnvelopeSmoothingFactor);
|
|
157
157
|
|
|
158
158
|
// pan the voice and write out
|
|
159
159
|
voice.currentPan += (pan - voice.currentPan) * this.panSmoothingFactor; // smooth out pan to prevent clicking
|
|
@@ -7,7 +7,7 @@ import { modulatorCurveTypes } from '../../../soundfont/read_sf2/modulators.js'
|
|
|
7
7
|
* modulation_envelope.js
|
|
8
8
|
* purpose: calculates the modulation envelope for the given voice
|
|
9
9
|
*/
|
|
10
|
-
const
|
|
10
|
+
const MODENV_PEAK = 1;
|
|
11
11
|
|
|
12
12
|
// 1000 should be precise enough
|
|
13
13
|
const CONVEX_ATTACK = new Float32Array(1000);
|
|
@@ -16,58 +16,156 @@ for (let i = 0; i < CONVEX_ATTACK.length; i++) {
|
|
|
16
16
|
CONVEX_ATTACK[i] = getModulatorCurveValue(0, modulatorCurveTypes.convex, i / 1000, 0);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
* Calculates the current modulation envelope value for the given time and voice
|
|
21
|
-
* @param voice {WorkletVoice} the voice we're working on
|
|
22
|
-
* @param currentTime {number} in seconds
|
|
23
|
-
* @returns {number} modenv value, from 0 to 1
|
|
24
|
-
*/
|
|
25
|
-
export function getModEnvValue(voice, currentTime)
|
|
19
|
+
export class WorkletModulationEnvelope
|
|
26
20
|
{
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The attack duration, in seconds
|
|
23
|
+
* @type {number}
|
|
24
|
+
*/
|
|
25
|
+
attackDuration = 0;
|
|
26
|
+
/**
|
|
27
|
+
* The decay duration, in seconds
|
|
28
|
+
* @type {number}
|
|
29
|
+
*/
|
|
30
|
+
decayDuration = 0;
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
{
|
|
38
|
-
// prevent lowpass bugs if release is instant
|
|
39
|
-
return voice.releaseStartModEnv;
|
|
40
|
-
}
|
|
41
|
-
return (1 - (currentTime - voice.releaseStartTime) / release) * voice.releaseStartModEnv;
|
|
42
|
-
}
|
|
32
|
+
/**
|
|
33
|
+
* The hold duration, in seconds
|
|
34
|
+
* @type {number}
|
|
35
|
+
*/
|
|
36
|
+
holdDuration = 0;
|
|
43
37
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Release duration, in seconds
|
|
40
|
+
* @type {number}
|
|
41
|
+
*/
|
|
42
|
+
releaseDuration = 0;
|
|
49
43
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
44
|
+
/**
|
|
45
|
+
* The sustain level 0-1
|
|
46
|
+
* @type {number}
|
|
47
|
+
*/
|
|
48
|
+
sustainLevel = 0;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Delay phase end time in seconds, absolute (audio context time)
|
|
52
|
+
* @type {number}
|
|
53
|
+
*/
|
|
54
|
+
delayEnd = 0;
|
|
55
|
+
/**
|
|
56
|
+
* Attack phase end time in seconds, absolute (audio context time)
|
|
57
|
+
* @type {number}
|
|
58
|
+
*/
|
|
59
|
+
attackEnd = 0;
|
|
60
|
+
/**
|
|
61
|
+
* Hold phase end time in seconds, absolute (audio context time)
|
|
62
|
+
* @type {number}
|
|
63
|
+
*/
|
|
64
|
+
holdEnd = 0;
|
|
65
|
+
/**
|
|
66
|
+
* Decay phase end time in seconds, absolute (audio context time)
|
|
67
|
+
* @type {number}
|
|
68
|
+
*/
|
|
69
|
+
decayEnd = 0;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The level of the envelope when the release phase starts
|
|
73
|
+
* @type {number}
|
|
74
|
+
*/
|
|
75
|
+
releaseStartLevel = 0;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The current modulation envelope value
|
|
79
|
+
* @type {number}
|
|
80
|
+
*/
|
|
81
|
+
currentValue = 0;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Starts the release phase in the envelope
|
|
85
|
+
* @param voice {WorkletVoice} the voice this envelope belongs to
|
|
86
|
+
*/
|
|
87
|
+
static startRelease(voice)
|
|
60
88
|
{
|
|
61
|
-
|
|
89
|
+
voice.modulationEnvelope.releaseStartLevel = voice.modulationEnvelope.currentValue;
|
|
90
|
+
WorkletModulationEnvelope.recalculate(voice);
|
|
62
91
|
}
|
|
63
|
-
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param voice {WorkletVoice} the voice to recalculate
|
|
95
|
+
*/
|
|
96
|
+
static recalculate(voice)
|
|
64
97
|
{
|
|
65
|
-
|
|
98
|
+
const env = voice.modulationEnvelope;
|
|
99
|
+
|
|
100
|
+
env.sustainLevel = 1 - (voice.modulatedGenerators[generatorTypes.sustainModEnv] / 1000);
|
|
101
|
+
|
|
102
|
+
env.attackDuration = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.attackModEnv]);
|
|
103
|
+
|
|
104
|
+
const decayKeyExcursionCents = ((60 - voice.midiNote) * voice.modulatedGenerators[generatorTypes.keyNumToModEnvDecay]);
|
|
105
|
+
const decayTime = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.decayModEnv] + decayKeyExcursionCents);
|
|
106
|
+
// according to the specification, the decay time is the time it takes to reach 0% from 100%.
|
|
107
|
+
// calculate the time to reach actual sustain level
|
|
108
|
+
// for example, sustain 0.6 will be 0.4 of the decay time
|
|
109
|
+
env.decayDuration = decayTime * (1 - env.sustainLevel);
|
|
110
|
+
|
|
111
|
+
const holdKeyExcursionCents = ((60 - voice.midiNote) * voice.modulatedGenerators[generatorTypes.keyNumToModEnvHold]);
|
|
112
|
+
env.holdDuration = timecentsToSeconds(holdKeyExcursionCents + voice.modulatedGenerators[generatorTypes.holdModEnv]);
|
|
113
|
+
|
|
114
|
+
const releaseTime = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.releaseVolEnv]);
|
|
115
|
+
// release time is from the full level to 0%
|
|
116
|
+
// to get the actual time, multiply by the release start level
|
|
117
|
+
env.releaseDuration = releaseTime * env.releaseStartLevel;
|
|
118
|
+
|
|
119
|
+
env.delayEnd = voice.startTime + timecentsToSeconds(voice.modulatedGenerators[generatorTypes.delayModEnv]);
|
|
120
|
+
env.attackEnd = env.delayEnd + env.attackDuration;
|
|
121
|
+
env.holdEnd = env.attackEnd + env.holdDuration;
|
|
122
|
+
env.decayEnd = env.holdEnd + env.decayDuration;
|
|
66
123
|
}
|
|
67
|
-
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* FIXME: GeneralUserGS 808 toms sound incorrect
|
|
127
|
+
* Calculates the current modulation envelope value for the given time and voice
|
|
128
|
+
* @param voice {WorkletVoice} the voice we're working on
|
|
129
|
+
* @param currentTime {number} in seconds
|
|
130
|
+
* @returns {number} modenv value, from 0 to 1
|
|
131
|
+
*/
|
|
132
|
+
static getValue(voice, currentTime)
|
|
68
133
|
{
|
|
69
|
-
|
|
134
|
+
const env = voice.modulationEnvelope;
|
|
135
|
+
if(voice.isInRelease)
|
|
136
|
+
{
|
|
137
|
+
if(voice.modulatedGenerators[generatorTypes.releaseModEnv] < -7199)
|
|
138
|
+
{
|
|
139
|
+
// prevent lowpass bugs if release is instant
|
|
140
|
+
return env.releaseStartLevel;
|
|
141
|
+
}
|
|
142
|
+
return Math.max(0, (1 - (currentTime - voice.releaseStartTime) / env.releaseDuration) * env.releaseStartLevel);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if(currentTime < env.delayEnd)
|
|
146
|
+
{
|
|
147
|
+
env.currentValue = 0; // delay
|
|
148
|
+
}
|
|
149
|
+
else if(currentTime < env.attackEnd)
|
|
150
|
+
{
|
|
151
|
+
// modulation envelope uses convex curve for attack
|
|
152
|
+
env.currentValue = CONVEX_ATTACK[~~((1 - (env.attackEnd - currentTime) / env.attackDuration) * 1000)];
|
|
153
|
+
}
|
|
154
|
+
else if(currentTime < env.holdEnd)
|
|
155
|
+
{
|
|
156
|
+
// hold: stay at 1
|
|
157
|
+
env.currentValue = MODENV_PEAK;
|
|
158
|
+
}
|
|
159
|
+
else if(currentTime < env.decayEnd)
|
|
160
|
+
{
|
|
161
|
+
// decay: linear ramp from 1 to sustain level
|
|
162
|
+
env.currentValue = (1 - (env.decayEnd - currentTime) / env.decayDuration) * (env.sustainLevel - MODENV_PEAK) + MODENV_PEAK;
|
|
163
|
+
}
|
|
164
|
+
else
|
|
165
|
+
{
|
|
166
|
+
// sustain: stay at sustain level
|
|
167
|
+
env.currentValue = env.sustainLevel;
|
|
168
|
+
}
|
|
169
|
+
return env.currentValue;
|
|
70
170
|
}
|
|
71
|
-
voice.currentModEnvValue = modEnvVal;
|
|
72
|
-
return modEnvVal;
|
|
73
171
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export const WORKLET_SYSTEM_REVERB_DIVIDER =
|
|
2
|
-
export const WORKLET_SYSTEM_CHORUS_DIVIDER =
|
|
1
|
+
export const WORKLET_SYSTEM_REVERB_DIVIDER = 800;
|
|
2
|
+
export const WORKLET_SYSTEM_CHORUS_DIVIDER = 800;
|
|
3
3
|
/**
|
|
4
4
|
* stereo_panner.js
|
|
5
5
|
* purpose: pans a given voice out to the stereo output and to the effects' outputs
|