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.
@@ -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 GAIN_SILENCE = 0.005;
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
- const env = voice.volumeEnvelope;
70
- // calculate durations
71
- env.attackDuration = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.attackVolEnv]);
72
- env.decayDuration = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.decayVolEnv]
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
- // calculate the db attenuation at the time of release (not a constant because it can change (ex, volume set to 0, the sound should cut off)
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
- * Applies volume envelope gain to the given output buffer
131
- * @param voice {WorkletVoice} the voice we're working on
132
- * @param audioBuffer {Float32Array} the audio buffer to modify
133
- * @param currentTime {number} the current audio time
134
- * @param centibelOffset {number} the centibel offset of volume, for modLFOtoVolume
135
- * @param sampleTime {number} single sample time in seconds, usually 1 / 44100 of a second
136
- * @param smoothingFactor {number} the adjusted smoothing factor for the envelope
137
- */
138
-
139
- export function applyVolumeEnvelope(voice, audioBuffer, currentTime, centibelOffset, sampleTime, smoothingFactor)
140
- {
141
- let decibelOffset = centibelOffset / 10;
142
- const env = voice.volumeEnvelope;
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
- // RELEASE PHASE
145
- if(voice.isInRelease)
135
+ /**
136
+ * Recalculates the envelope
137
+ * @param voice {WorkletVoice} the voice this envelope belongs to
138
+ */
139
+ static recalculate(voice)
146
140
  {
147
- // release needs a more aggressive smoothing factor as the instant notes don't end instantly when they should
148
- const releaseSmoothingFactor = smoothingFactor * 10;
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
- let db = (elapsedRelease / env.releaseDuration) * dbDifference + releaseStartDb;
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
- if(env.currentReleaseGain <= GAIN_SILENCE)
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
- voice.finished = true;
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
- let currentFrameTime = currentTime;
170
- let filledBuffer = 0;
171
- switch(env.state)
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
- case 0:
174
- // delay phase, no sound is produced
175
- while(currentFrameTime < env.delayEnd)
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
- currentFrameTime += sampleTime;
181
- if(++filledBuffer >= audioBuffer.length)
182
- {
183
- return;
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
- env.state++;
187
- // fallthrough
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
- // Special case: linear gain ramp instead of linear db ramp
194
- let linearAttenuation = 1 - (env.attackEnd - currentFrameTime) / env.attackDuration; // 0 to 1
195
- const gain = linearAttenuation * decibelAttenuationToGain(env.attenuation + decibelOffset)
196
- audioBuffer[filledBuffer] *= gain;
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
- currentFrameTime += sampleTime;
202
- if(++filledBuffer >= audioBuffer.length)
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
- return;
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
- env.state++;
208
- // fallthrough
209
-
210
- case 2:
211
- // hold/peak phase: stay at attenuation
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
- return;
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
- env.state++;
228
- // fallthrough
229
-
230
- case 3:
231
- // decay phase: linear ramp from attenuation to sustain
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
- return;
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
- env.state++;
248
- // fallthrough
249
-
250
- case 4:
251
- // sustain phase: stay at sustain
252
- while(true)
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
- return;
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
- recalculateVolumeEnvelope(voice);
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
- recalculateVolumeEnvelope(voice);
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} currentModEnvValue - current value of the modulation envelope
39
- * @property {number} releaseStartModEnv - modenv value at the start of the release phase
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
- * @param {WorkletVoice} obj - The object to clone.
91
- * @returns {WorkletVoice} - Cloned object.
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 {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(60),
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: deepClone(DEFAULT_WORKLET_VOLUME_ENVELOPE)
267
+ volumeEnvelope: new WorkletVolumeEnvelope(sampleRate),
268
+ modulationEnvelope: new WorkletModulationEnvelope()
271
269
  });
272
270
  return voices;
273
271
  }, []);