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
|
@@ -6,49 +6,10 @@ import { generatorTypes } from '../../../soundfont/read_sf2/generators.js'
|
|
|
6
6
|
* purpose: applies a volume envelope for a given voice
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* @typedef {Object} WorkletVolumeEnvelope
|
|
11
|
-
* @property {number} currentAttenuationDb - current voice attenuation in dB (current sample)
|
|
12
|
-
* @property {0|1|2|3|4} state - state of the volume envelope. 0 is delay, 1 is attack, 2 is hold, 3 is decay, 4 is sustain
|
|
13
|
-
* @property {number} releaseStartDb - the dB attenuation of the voice when it was released
|
|
14
|
-
* @property {number} currentReleaseGain - the current linear gain of the release phase
|
|
15
|
-
*
|
|
16
|
-
* @property {number} attackDuration - the duration of the attack phase, in seconds
|
|
17
|
-
* @property {number} decayDuration - the duration of the decay phase, in seconds
|
|
18
|
-
*
|
|
19
|
-
* @property {number} attenuation - the absolute attenuation in dB
|
|
20
|
-
* @property {number} releaseDuration - the duration of the release phase in seconds
|
|
21
|
-
* @property {number} sustainDb - the sustain amount in dB
|
|
22
|
-
*
|
|
23
|
-
* @property {number} delayEnd - the time when delay ends, in absolute seconds
|
|
24
|
-
* @property {number} attackEnd - the time when the attack phase ends, in absolute seconds
|
|
25
|
-
* @property {number} holdEnd - the time when the hold phase ends, in absolute seconds
|
|
26
|
-
* @property {number} decayEnd - the time when the decay phase ends, in absolute seconds
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* @type {WorkletVolumeEnvelope}
|
|
31
|
-
*/
|
|
32
|
-
export const DEFAULT_WORKLET_VOLUME_ENVELOPE = {
|
|
33
|
-
attenuation: 100,
|
|
34
|
-
currentAttenuationDb: 100,
|
|
35
|
-
state: 0,
|
|
36
|
-
releaseStartDb: 100,
|
|
37
|
-
attackDuration: 0,
|
|
38
|
-
decayDuration: 0,
|
|
39
|
-
releaseDuration: 0,
|
|
40
|
-
sustainDb: 0,
|
|
41
|
-
delayEnd: 0,
|
|
42
|
-
attackEnd: 0,
|
|
43
|
-
holdEnd: 0,
|
|
44
|
-
decayEnd: 0,
|
|
45
|
-
currentReleaseGain: 1,
|
|
46
|
-
}
|
|
47
|
-
|
|
48
9
|
export const VOLUME_ENVELOPE_SMOOTHING_FACTOR = 0.001;
|
|
49
10
|
|
|
50
11
|
const DB_SILENCE = 100;
|
|
51
|
-
const
|
|
12
|
+
const PERCEIVED_DB_SILENCE = 96;
|
|
52
13
|
|
|
53
14
|
/**
|
|
54
15
|
* VOL ENV STATES:
|
|
@@ -60,207 +21,331 @@ const GAIN_SILENCE = 0.005;
|
|
|
60
21
|
* release is indicated by isInRelease property
|
|
61
22
|
*/
|
|
62
23
|
|
|
63
|
-
|
|
64
|
-
* Recalculates the times of the volume envelope
|
|
65
|
-
* @param voice {WorkletVoice} the voice we're working on
|
|
66
|
-
*/
|
|
67
|
-
export function recalculateVolumeEnvelope(voice)
|
|
24
|
+
export class WorkletVolumeEnvelope
|
|
68
25
|
{
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
+ ((60 - voice.midiNote) * voice.modulatedGenerators[generatorTypes.keyNumToVolEnvDecay]));
|
|
74
|
-
env.releaseDuration = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.releaseVolEnv]);
|
|
75
|
-
|
|
76
|
-
// calculate absolute times (they can change so we have to recalculate every time
|
|
77
|
-
env.attenuation = voice.modulatedGenerators[generatorTypes.initialAttenuation] / 10; // divide by ten to get decibelts
|
|
78
|
-
env.sustainDb = voice.volumeEnvelope.attenuation + voice.modulatedGenerators[generatorTypes.sustainVolEnv] / 10;
|
|
79
|
-
|
|
80
|
-
// calculate absolute end time
|
|
81
|
-
env.delayEnd = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.delayVolEnv]) + voice.startTime;
|
|
82
|
-
env.attackEnd = env.attackDuration + env.delayEnd;
|
|
83
|
-
|
|
84
|
-
// make sure to take keyNumToVolEnvHold into account!!!
|
|
85
|
-
env.holdEnd = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.holdVolEnv]
|
|
86
|
-
+ ((60 - voice.midiNote) * voice.modulatedGenerators[generatorTypes.keyNumToVolEnvHold]))
|
|
87
|
-
+ env.attackEnd;
|
|
88
|
-
|
|
89
|
-
env.decayEnd = env.decayDuration + env.holdEnd;
|
|
90
|
-
// check if voice is in release
|
|
91
|
-
if(voice.isInRelease)
|
|
26
|
+
/**
|
|
27
|
+
* @param sampleRate {number} Hz
|
|
28
|
+
*/
|
|
29
|
+
constructor(sampleRate)
|
|
92
30
|
{
|
|
93
|
-
|
|
94
|
-
switch (env.state) {
|
|
95
|
-
case 0:
|
|
96
|
-
env.releaseStartDb = DB_SILENCE;
|
|
97
|
-
break;
|
|
98
|
-
|
|
99
|
-
case 1:
|
|
100
|
-
// attack phase: get linear gain of the attack phase when release started
|
|
101
|
-
// and turn it into db as we're ramping the db up linearly (to make volume go down exponentially)
|
|
102
|
-
// attack is linear (in gain) so we need to do get db from that
|
|
103
|
-
let elapsed = 1 - ((env.attackEnd - voice.releaseStartTime) / env.attackDuration);
|
|
104
|
-
// calculate the gain that the attack would have
|
|
105
|
-
let attackGain = elapsed * decibelAttenuationToGain(env.attenuation);
|
|
106
|
-
|
|
107
|
-
// turn that into db
|
|
108
|
-
env.releaseStartDb = 20 * Math.log10(attackGain) * -1;
|
|
109
|
-
break;
|
|
110
|
-
|
|
111
|
-
case 2:
|
|
112
|
-
env.releaseStartDb = env.attenuation;
|
|
113
|
-
break;
|
|
114
|
-
|
|
115
|
-
case 3:
|
|
116
|
-
env.releaseStartDb = (1 - (env.decayEnd - voice.releaseStartTime) / env.decayDuration) * (env.sustainDb - env.attenuation) + env.attenuation;
|
|
117
|
-
break;
|
|
118
|
-
|
|
119
|
-
case 4:
|
|
120
|
-
env.releaseStartDb = env.sustainDb;
|
|
121
|
-
break;
|
|
122
|
-
|
|
123
|
-
default:
|
|
124
|
-
env.releaseStartDb = env.currentAttenuationDb;
|
|
125
|
-
}
|
|
31
|
+
this.sampleRate = sampleRate;
|
|
126
32
|
}
|
|
127
|
-
}
|
|
128
33
|
|
|
129
|
-
/**
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
34
|
+
/**
|
|
35
|
+
* The envelope's current time in samples
|
|
36
|
+
* @type {number}
|
|
37
|
+
*/
|
|
38
|
+
currentSampleTime = 0;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The sample rate in Hz
|
|
42
|
+
* @type {number}
|
|
43
|
+
*/
|
|
44
|
+
sampleRate;
|
|
45
|
+
/**
|
|
46
|
+
* The current attenuation of the envelope in dB
|
|
47
|
+
* @type {number}
|
|
48
|
+
*/
|
|
49
|
+
currentAttenuationDb = DB_SILENCE;
|
|
50
|
+
/**
|
|
51
|
+
* The current stage of the volume envelope
|
|
52
|
+
* @type {0|1|2|3|4}
|
|
53
|
+
*/
|
|
54
|
+
state = 0;
|
|
55
|
+
/**
|
|
56
|
+
* The dB attenuation of the envelope when it entered the release stage
|
|
57
|
+
* @type {number}
|
|
58
|
+
*/
|
|
59
|
+
releaseStartDb = 100;
|
|
60
|
+
/**
|
|
61
|
+
* The time in samples relative to the start of the envelope
|
|
62
|
+
* @type {number}
|
|
63
|
+
*/
|
|
64
|
+
releaseStartTimeSamples = 0;
|
|
65
|
+
/**
|
|
66
|
+
* The current gain applied to the voice in the release stage
|
|
67
|
+
* @type {number}
|
|
68
|
+
*/
|
|
69
|
+
currentReleaseGain = 1;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The attack duration in samples
|
|
73
|
+
* @type {number}
|
|
74
|
+
*/
|
|
75
|
+
attackDuration = 0;
|
|
76
|
+
/**
|
|
77
|
+
* The decay duration in samples
|
|
78
|
+
* @type {number}
|
|
79
|
+
*/
|
|
80
|
+
decayDuration = 0;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The release duration in samples
|
|
84
|
+
* @type {number}
|
|
85
|
+
*/
|
|
86
|
+
releaseDuration = 0;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The voice's absolute attenuation in dB
|
|
90
|
+
* @type {number}
|
|
91
|
+
*/
|
|
92
|
+
attenuation = 0;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The voice's sustain amount in dB, absolute
|
|
96
|
+
* @type {number}
|
|
97
|
+
*/
|
|
98
|
+
sustainDb = 0;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The time in samples to the end of delay stage, relative to start of the envelope
|
|
102
|
+
* @type {number}
|
|
103
|
+
*/
|
|
104
|
+
delayEnd = 0;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The time in samples to the end of attack stage, relative to start of the envelope
|
|
108
|
+
* @type {number}
|
|
109
|
+
*/
|
|
110
|
+
attackEnd = 0;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The time in samples to the end of hold stage, relative to start of the envelope
|
|
114
|
+
* @type {number}
|
|
115
|
+
*/
|
|
116
|
+
holdEnd = 0;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The time in samples to the end of decay stage, relative to start of the envelope
|
|
120
|
+
* @type {number}
|
|
121
|
+
*/
|
|
122
|
+
decayEnd = 0;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Starts the release phase in the envelope
|
|
126
|
+
* @param voice {WorkletVoice} the voice this envelope belongs to
|
|
127
|
+
*/
|
|
128
|
+
static startRelease(voice)
|
|
129
|
+
{
|
|
130
|
+
voice.volumeEnvelope.releaseStartTimeSamples = voice.volumeEnvelope.currentSampleTime;
|
|
131
|
+
voice.volumeEnvelope.currentReleaseGain = decibelAttenuationToGain(voice.volumeEnvelope.currentAttenuationDb);
|
|
132
|
+
WorkletVolumeEnvelope.recalculate(voice);
|
|
133
|
+
}
|
|
143
134
|
|
|
144
|
-
|
|
145
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Recalculates the envelope
|
|
137
|
+
* @param voice {WorkletVoice} the voice this envelope belongs to
|
|
138
|
+
*/
|
|
139
|
+
static recalculate(voice)
|
|
146
140
|
{
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
const releaseStartDb = env.releaseStartDb + decibelOffset;
|
|
150
|
-
let elapsedRelease = currentTime - voice.releaseStartTime;
|
|
151
|
-
let dbDifference = DB_SILENCE - releaseStartDb;
|
|
152
|
-
let gain = env.currentReleaseGain;
|
|
153
|
-
for (let i = 0; i < audioBuffer.length; i++)
|
|
141
|
+
const env = voice.volumeEnvelope;
|
|
142
|
+
const timecentsToSamples = tc =>
|
|
154
143
|
{
|
|
155
|
-
|
|
156
|
-
gain = decibelAttenuationToGain(db + decibelOffset);
|
|
157
|
-
env.currentReleaseGain += (gain - env.currentReleaseGain) * releaseSmoothingFactor;
|
|
158
|
-
audioBuffer[i] *= env.currentReleaseGain;
|
|
159
|
-
elapsedRelease += sampleTime;
|
|
144
|
+
return Math.floor(timecentsToSeconds(tc) * env.sampleRate);
|
|
160
145
|
}
|
|
161
|
-
|
|
162
|
-
|
|
146
|
+
// calculate absolute times (they can change so we have to recalculate every time
|
|
147
|
+
env.attenuation = voice.modulatedGenerators[generatorTypes.initialAttenuation] / 10; // divide by ten to get decibelts
|
|
148
|
+
env.sustainDb = voice.volumeEnvelope.attenuation + voice.modulatedGenerators[generatorTypes.sustainVolEnv] / 10;
|
|
149
|
+
|
|
150
|
+
// calculate durations
|
|
151
|
+
env.attackDuration = timecentsToSamples(voice.modulatedGenerators[generatorTypes.attackVolEnv]);
|
|
152
|
+
|
|
153
|
+
// decay: sfspec page 35: the time is for change from attenuation to -100dB
|
|
154
|
+
// therefore we need to calculate the real time
|
|
155
|
+
// (changing from attenuation to sustain instead of -100dB)
|
|
156
|
+
const fullChange = voice.modulatedGenerators[generatorTypes.decayVolEnv];
|
|
157
|
+
const keyNumAddition = ((60 - voice.targetKey) * voice.modulatedGenerators[generatorTypes.keyNumToVolEnvDecay]);
|
|
158
|
+
const fraction = (env.sustainDb - env.attenuation) / (100 - env.attenuation);
|
|
159
|
+
env.decayDuration = timecentsToSamples(fullChange + keyNumAddition) * fraction;
|
|
160
|
+
|
|
161
|
+
env.releaseDuration = timecentsToSamples(voice.modulatedGenerators[generatorTypes.releaseVolEnv]);
|
|
162
|
+
|
|
163
|
+
// calculate absolute end times for the values
|
|
164
|
+
env.delayEnd = timecentsToSamples(voice.modulatedGenerators[generatorTypes.delayVolEnv]);
|
|
165
|
+
env.attackEnd = env.attackDuration + env.delayEnd;
|
|
166
|
+
|
|
167
|
+
// make sure to take keyNumToVolEnvHold into account!!!
|
|
168
|
+
const holdExcursion = (60 - voice.targetKey) * voice.modulatedGenerators[generatorTypes.keyNumToVolEnvHold];
|
|
169
|
+
env.holdEnd = timecentsToSamples(voice.modulatedGenerators[generatorTypes.holdVolEnv]
|
|
170
|
+
+ holdExcursion)
|
|
171
|
+
+ env.attackEnd;
|
|
172
|
+
|
|
173
|
+
env.decayEnd = env.decayDuration + env.holdEnd;
|
|
174
|
+
// check if voice is in release
|
|
175
|
+
if(voice.isInRelease)
|
|
163
176
|
{
|
|
164
|
-
|
|
177
|
+
switch (env.state)
|
|
178
|
+
{
|
|
179
|
+
case 0:
|
|
180
|
+
env.releaseStartDb = DB_SILENCE;
|
|
181
|
+
break;
|
|
182
|
+
|
|
183
|
+
case 1:
|
|
184
|
+
// attack phase: get linear gain of the attack phase when release started
|
|
185
|
+
// and turn it into db as we're ramping the db up linearly
|
|
186
|
+
// (to make volume go down exponentially)
|
|
187
|
+
// attack is linear (in gain) so we need to do get db from that
|
|
188
|
+
let elapsed = 1 - ((env.attackEnd - env.releaseStartTimeSamples) / env.attackDuration);
|
|
189
|
+
// calculate the gain that the attack would have
|
|
190
|
+
let attackGain = elapsed * decibelAttenuationToGain(env.attenuation);
|
|
191
|
+
|
|
192
|
+
// turn that into db
|
|
193
|
+
env.releaseStartDb = 20 * Math.log10(attackGain) * -1;
|
|
194
|
+
break;
|
|
195
|
+
|
|
196
|
+
case 2:
|
|
197
|
+
env.releaseStartDb = env.attenuation;
|
|
198
|
+
break;
|
|
199
|
+
|
|
200
|
+
case 3:
|
|
201
|
+
env.releaseStartDb = (1 - (env.decayEnd - env.releaseStartTimeSamples) / env.decayDuration) * (env.sustainDb - env.attenuation) + env.attenuation;
|
|
202
|
+
break;
|
|
203
|
+
|
|
204
|
+
case 4:
|
|
205
|
+
env.releaseStartDb = env.sustainDb;
|
|
206
|
+
break;
|
|
207
|
+
|
|
208
|
+
default:
|
|
209
|
+
env.releaseStartDb = env.currentAttenuationDb;
|
|
210
|
+
}
|
|
165
211
|
}
|
|
166
|
-
return;
|
|
167
212
|
}
|
|
168
213
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
214
|
+
/**
|
|
215
|
+
* Gets interpolated gain
|
|
216
|
+
* @param env {WorkletVolumeEnvelope}
|
|
217
|
+
* @param attenuationDb {number} in decibels
|
|
218
|
+
* @param smoothingFactor {number}
|
|
219
|
+
* @returns {number} the gain value
|
|
220
|
+
*/
|
|
221
|
+
static getInterpolatedGain(env, attenuationDb, smoothingFactor)
|
|
172
222
|
{
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
env.currentAttenuationDb = DB_SILENCE;
|
|
178
|
-
audioBuffer[filledBuffer] = 0;
|
|
223
|
+
// interpolate attenuation to prevent clicking
|
|
224
|
+
env.currentAttenuationDb += (attenuationDb - env.currentAttenuationDb) * smoothingFactor;
|
|
225
|
+
return decibelAttenuationToGain(env.currentAttenuationDb);
|
|
226
|
+
}
|
|
179
227
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
228
|
+
/**
|
|
229
|
+
* Applies volume envelope gain to the given output buffer
|
|
230
|
+
* @param voice {WorkletVoice} the voice we're working on
|
|
231
|
+
* @param audioBuffer {Float32Array} the audio buffer to modify
|
|
232
|
+
* @param centibelOffset {number} the centibel offset of volume, for modLFOtoVolume
|
|
233
|
+
* @param smoothingFactor {number} the adjusted smoothing factor for the envelope
|
|
234
|
+
*/
|
|
235
|
+
static apply(voice, audioBuffer, centibelOffset, smoothingFactor)
|
|
236
|
+
{
|
|
237
|
+
const env = voice.volumeEnvelope;
|
|
238
|
+
let decibelOffset = centibelOffset / 10;
|
|
239
|
+
|
|
240
|
+
// RELEASE PHASE
|
|
241
|
+
if(voice.isInRelease)
|
|
242
|
+
{
|
|
243
|
+
// release needs a more aggressive smoothing factor
|
|
244
|
+
// as the instant notes don't end instantly when they should
|
|
245
|
+
const releaseSmoothingFactor = smoothingFactor * 10;
|
|
246
|
+
let elapsedRelease = env.currentSampleTime - env.releaseStartTimeSamples;
|
|
247
|
+
let dbDifference = DB_SILENCE - env.releaseStartDb;
|
|
248
|
+
let db = 0;
|
|
249
|
+
for (let i = 0; i < audioBuffer.length; i++)
|
|
250
|
+
{
|
|
251
|
+
db = (elapsedRelease / env.releaseDuration) * dbDifference + env.releaseStartDb;
|
|
252
|
+
let gain = decibelAttenuationToGain(db + decibelOffset);
|
|
253
|
+
env.currentReleaseGain += (gain - env.currentReleaseGain) * releaseSmoothingFactor;
|
|
254
|
+
audioBuffer[i] *= env.currentReleaseGain;
|
|
255
|
+
env.currentSampleTime++;
|
|
256
|
+
elapsedRelease++;
|
|
185
257
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
case 1:
|
|
190
|
-
// attack phase: ramp from 0 to attenuation
|
|
191
|
-
while(currentFrameTime < env.attackEnd)
|
|
258
|
+
|
|
259
|
+
if(db >= PERCEIVED_DB_SILENCE)
|
|
192
260
|
{
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
// set current attenuation to peak as its invalid during this phase
|
|
199
|
-
env.currentAttenuationDb = env.attenuation;
|
|
261
|
+
voice.finished = true;
|
|
262
|
+
}
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
200
265
|
|
|
201
|
-
|
|
202
|
-
|
|
266
|
+
let filledBuffer = 0;
|
|
267
|
+
switch(env.state)
|
|
268
|
+
{
|
|
269
|
+
case 0:
|
|
270
|
+
// delay phase, no sound is produced
|
|
271
|
+
while(env.currentSampleTime < env.delayEnd)
|
|
203
272
|
{
|
|
204
|
-
|
|
273
|
+
env.currentAttenuationDb = DB_SILENCE;
|
|
274
|
+
audioBuffer[filledBuffer] = 0;
|
|
275
|
+
|
|
276
|
+
env.currentSampleTime++
|
|
277
|
+
if(++filledBuffer >= audioBuffer.length)
|
|
278
|
+
{
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
205
281
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
while(currentFrameTime < env.holdEnd)
|
|
213
|
-
{
|
|
214
|
-
const newAttenuation = env.attenuation
|
|
215
|
-
+ decibelOffset;
|
|
216
|
-
|
|
217
|
-
// interpolate attenuation to prevent clicking
|
|
218
|
-
env.currentAttenuationDb += (newAttenuation - env.currentAttenuationDb) * smoothingFactor;
|
|
219
|
-
audioBuffer[filledBuffer] *= decibelAttenuationToGain(env.currentAttenuationDb);
|
|
220
|
-
|
|
221
|
-
currentFrameTime += sampleTime;
|
|
222
|
-
if(++filledBuffer >= audioBuffer.length)
|
|
282
|
+
env.state++;
|
|
283
|
+
// fallthrough
|
|
284
|
+
|
|
285
|
+
case 1:
|
|
286
|
+
// attack phase: ramp from 0 to attenuation
|
|
287
|
+
while(env.currentSampleTime < env.attackEnd)
|
|
223
288
|
{
|
|
224
|
-
|
|
289
|
+
// Special case: linear gain ramp instead of linear db ramp
|
|
290
|
+
let linearAttenuation = 1 - (env.attackEnd - env.currentSampleTime) / env.attackDuration; // 0 to 1
|
|
291
|
+
audioBuffer[filledBuffer] *= linearAttenuation * decibelAttenuationToGain(env.attenuation + decibelOffset)
|
|
292
|
+
|
|
293
|
+
// set current attenuation to peak as its invalid during this phase
|
|
294
|
+
env.currentAttenuationDb = env.attenuation;
|
|
295
|
+
|
|
296
|
+
env.currentSampleTime++;
|
|
297
|
+
if(++filledBuffer >= audioBuffer.length)
|
|
298
|
+
{
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
225
301
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
while(currentFrameTime < env.decayEnd)
|
|
233
|
-
{
|
|
234
|
-
const newAttenuation = (1 - (env.decayEnd - currentFrameTime) / env.decayDuration) * (env.sustainDb - env.attenuation) + env.attenuation
|
|
235
|
-
+ decibelOffset;
|
|
236
|
-
|
|
237
|
-
// interpolate attenuation to prevent clicking
|
|
238
|
-
env.currentAttenuationDb += (newAttenuation - env.currentAttenuationDb) * smoothingFactor;
|
|
239
|
-
audioBuffer[filledBuffer] *= decibelAttenuationToGain(env.currentAttenuationDb);
|
|
240
|
-
|
|
241
|
-
currentFrameTime += sampleTime;
|
|
242
|
-
if(++filledBuffer >= audioBuffer.length)
|
|
302
|
+
env.state++;
|
|
303
|
+
// fallthrough
|
|
304
|
+
|
|
305
|
+
case 2:
|
|
306
|
+
// hold/peak phase: stay at attenuation
|
|
307
|
+
while(env.currentSampleTime < env.holdEnd)
|
|
243
308
|
{
|
|
244
|
-
|
|
309
|
+
audioBuffer[filledBuffer] *= WorkletVolumeEnvelope.getInterpolatedGain(env, env.attenuation + decibelOffset, smoothingFactor);
|
|
310
|
+
|
|
311
|
+
env.currentSampleTime++;
|
|
312
|
+
if(++filledBuffer >= audioBuffer.length)
|
|
313
|
+
{
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
245
316
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
{
|
|
254
|
-
// interpolate attenuation to prevent clicking
|
|
255
|
-
const newAttenuation = env.sustainDb
|
|
256
|
-
+ decibelOffset;
|
|
257
|
-
env.currentAttenuationDb += (newAttenuation - env.currentAttenuationDb) * smoothingFactor;
|
|
258
|
-
audioBuffer[filledBuffer] *= decibelAttenuationToGain(env.currentAttenuationDb);
|
|
259
|
-
if(++filledBuffer >= audioBuffer.length)
|
|
317
|
+
env.state++;
|
|
318
|
+
// fallthrough
|
|
319
|
+
|
|
320
|
+
case 3:
|
|
321
|
+
// decay phase: linear ramp from attenuation to sustain
|
|
322
|
+
const dbDifference = env.sustainDb - env.attenuation;
|
|
323
|
+
while(env.currentSampleTime++ < env.decayEnd)
|
|
260
324
|
{
|
|
261
|
-
|
|
325
|
+
const newAttenuation = (1 - (env.decayEnd - env.currentSampleTime) / env.decayDuration) * dbDifference + env.attenuation;
|
|
326
|
+
audioBuffer[filledBuffer] *= WorkletVolumeEnvelope.getInterpolatedGain(env, newAttenuation + decibelOffset, smoothingFactor);
|
|
327
|
+
|
|
328
|
+
env.currentSampleTime++;
|
|
329
|
+
if(++filledBuffer >= audioBuffer.length)
|
|
330
|
+
{
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
262
333
|
}
|
|
263
|
-
|
|
264
|
-
|
|
334
|
+
env.state++;
|
|
335
|
+
// fallthrough
|
|
336
|
+
|
|
337
|
+
case 4:
|
|
338
|
+
// sustain phase: stay at sustain
|
|
339
|
+
while(true)
|
|
340
|
+
{
|
|
341
|
+
audioBuffer[filledBuffer] *= WorkletVolumeEnvelope.getInterpolatedGain(env, env.sustainDb + decibelOffset, smoothingFactor);
|
|
342
|
+
env.currentSampleTime++;
|
|
343
|
+
if(++filledBuffer >= audioBuffer.length)
|
|
344
|
+
{
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
}
|
|
265
350
|
}
|
|
266
351
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { modulatorSources } from '../../../soundfont/read_sf2/modulators.js'
|
|
2
2
|
import { getModulatorCurveValue, MOD_PRECOMPUTED_LENGTH } from './modulator_curves.js'
|
|
3
3
|
import { NON_CC_INDEX_OFFSET } from './worklet_processor_channel.js'
|
|
4
|
-
import { recalculateVolumeEnvelope } from './volume_envelope.js'
|
|
5
4
|
import { generatorTypes } from '../../../soundfont/read_sf2/generators.js'
|
|
5
|
+
import { WorkletVolumeEnvelope } from './volume_envelope.js'
|
|
6
|
+
import { WorkletModulationEnvelope } from './modulation_envelope.js'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* worklet_modulator.js
|
|
@@ -114,6 +115,9 @@ export function computeWorkletModulator(controllerTable, modulator, voice)
|
|
|
114
115
|
export function computeModulators(voice, controllerTable, sourceUsesCC = -1, sourceIndex = 0) {
|
|
115
116
|
const { modulators, generators, modulatedGenerators } = voice;
|
|
116
117
|
|
|
118
|
+
// Modulation envelope is cheap to recalculate
|
|
119
|
+
WorkletModulationEnvelope.recalculate(voice);
|
|
120
|
+
|
|
117
121
|
if (sourceUsesCC === -1)
|
|
118
122
|
{
|
|
119
123
|
// All modulators mode: compute all modulators
|
|
@@ -121,7 +125,7 @@ export function computeModulators(voice, controllerTable, sourceUsesCC = -1, sou
|
|
|
121
125
|
modulators.forEach(mod => {
|
|
122
126
|
modulatedGenerators[mod.modulatorDestination] += computeWorkletModulator(controllerTable, mod, voice);
|
|
123
127
|
});
|
|
124
|
-
|
|
128
|
+
WorkletVolumeEnvelope.recalculate(voice);
|
|
125
129
|
return;
|
|
126
130
|
}
|
|
127
131
|
|
|
@@ -165,7 +169,7 @@ export function computeModulators(voice, controllerTable, sourceUsesCC = -1, sou
|
|
|
165
169
|
// Recalculate volume envelope if necessary
|
|
166
170
|
if ([...computedDestinations].some(dest => volenvNeedsRecalculation.has(dest)))
|
|
167
171
|
{
|
|
168
|
-
|
|
172
|
+
WorkletVolumeEnvelope.recalculate(voice);
|
|
169
173
|
}
|
|
170
174
|
}
|
|
171
175
|
|
|
@@ -33,13 +33,11 @@
|
|
|
33
33
|
* @property {number} pressure - the pressure of the note
|
|
34
34
|
* @property {number} targetKey - target key for the note
|
|
35
35
|
*
|
|
36
|
+
* @property {WorkletModulationEnvelope} modulationEnvelope
|
|
36
37
|
* @property {WorkletVolumeEnvelope} volumeEnvelope
|
|
37
38
|
*
|
|
38
|
-
* @property {number}
|
|
39
|
-
* @property {number}
|
|
40
|
-
*
|
|
41
|
-
* @property {number} startTime - start time of the voice
|
|
42
|
-
* @property {number} releaseStartTime - start time of the release phase
|
|
39
|
+
* @property {number} startTime - start time of the voice absolute
|
|
40
|
+
* @property {number} releaseStartTime - start time of the release phase absolute
|
|
43
41
|
*
|
|
44
42
|
* @property {number} currentTuningCents - current tuning adjustment in cents
|
|
45
43
|
* @property {number} currentTuningCalculated - calculated tuning adjustment
|
|
@@ -48,8 +46,9 @@
|
|
|
48
46
|
|
|
49
47
|
import { addAndClampGenerator, generatorTypes } from '../../../soundfont/read_sf2/generators.js'
|
|
50
48
|
import { SpessaSynthTable, SpessaSynthWarn } from '../../../utils/loggin.js'
|
|
51
|
-
import { DEFAULT_WORKLET_VOLUME_ENVELOPE } from './volume_envelope.js'
|
|
52
49
|
import { DEFAULT_WORKLET_LOWPASS_FILTER } from './lowpass_filter.js'
|
|
50
|
+
import { WorkletVolumeEnvelope } from './volume_envelope.js'
|
|
51
|
+
import { WorkletModulationEnvelope } from './modulation_envelope.js'
|
|
53
52
|
|
|
54
53
|
|
|
55
54
|
/**
|
|
@@ -87,8 +86,9 @@ dumpSample(channel, sample, id, sampleDumpCallback)
|
|
|
87
86
|
* Deep clone function for the WorkletVoice object and its nested structures.
|
|
88
87
|
* This function handles Int16Array, objects, arrays, and primitives.
|
|
89
88
|
* It does not handle circular references.
|
|
90
|
-
* @
|
|
91
|
-
* @
|
|
89
|
+
* @template T
|
|
90
|
+
* @param {T} obj - The object to clone.
|
|
91
|
+
* @returns {T} - Cloned object.
|
|
92
92
|
*/
|
|
93
93
|
function deepClone(obj) {
|
|
94
94
|
if (obj === null || typeof obj !== 'object') {
|
|
@@ -116,12 +116,11 @@ function deepClone(obj) {
|
|
|
116
116
|
return clonedObj;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
|
|
120
119
|
/**
|
|
121
120
|
* @param channel {number} a hint for the processor to recalculate sample cursors when sample dumping
|
|
122
121
|
* @param midiNote {number}
|
|
123
122
|
* @param velocity {number}
|
|
124
|
-
* @param preset {
|
|
123
|
+
* @param preset {BasicPreset}
|
|
125
124
|
* @param currentTime {number}
|
|
126
125
|
* @param sampleRate {number}
|
|
127
126
|
* @param sampleDumpCallback {function({channel: number, sampleID: number, sampleData: Float32Array})}
|
|
@@ -246,7 +245,7 @@ export function getWorkletVoices(channel,
|
|
|
246
245
|
// generators and modulators
|
|
247
246
|
generators: generators,
|
|
248
247
|
modulators: sampleAndGenerators.modulators,
|
|
249
|
-
modulatedGenerators: new Int16Array(
|
|
248
|
+
modulatedGenerators: new Int16Array(generators),
|
|
250
249
|
|
|
251
250
|
// sample and playback data
|
|
252
251
|
sample: workletSample,
|
|
@@ -263,11 +262,10 @@ export function getWorkletVoices(channel,
|
|
|
263
262
|
// envelope data
|
|
264
263
|
finished: false,
|
|
265
264
|
isInRelease: false,
|
|
266
|
-
currentModEnvValue: 0,
|
|
267
|
-
releaseStartModEnv: 1,
|
|
268
265
|
currentPan: 0.5,
|
|
269
266
|
|
|
270
|
-
volumeEnvelope:
|
|
267
|
+
volumeEnvelope: new WorkletVolumeEnvelope(sampleRate),
|
|
268
|
+
modulationEnvelope: new WorkletModulationEnvelope()
|
|
271
269
|
});
|
|
272
270
|
return voices;
|
|
273
271
|
}, []);
|