spessasynth_core 1.1.1 → 1.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spessasynth_core",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "SoundFont2 synthesizer library for node.js",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -30,8 +30,14 @@ import {
30
30
  import { disableAndLockVibrato, setVibrato } from './worklet_system/worklet_methods/vibrato_control.js'
31
31
  import { SpessaSynthInfo } from '../utils/loggin.js'
32
32
  import { consoleColors } from '../utils/other.js'
33
- import { releaseVoice, renderVoice, voiceKilling } from './worklet_system/worklet_methods/voice_control.js'
33
+ import {
34
+ PAN_SMOOTHING_FACTOR,
35
+ releaseVoice,
36
+ renderVoice,
37
+ voiceKilling
38
+ } from './worklet_system/worklet_methods/voice_control.js'
34
39
  import {stbvorbis} from "../utils/stbvorbis_sync.js";
40
+ import {VOLUME_ENVELOPE_SMOOTHING_FACTOR} from "./worklet_system/worklet_utilities/volume_envelope.js";
35
41
 
36
42
 
37
43
  export const VOICE_CAP = 450;
@@ -125,6 +131,10 @@ class Synthesizer {
125
131
  // in seconds, time between two samples (very, very short)
126
132
  this.sampleTime = 1 / this.sampleRate;
127
133
 
134
+ // these smoothing factors were tested on 44100Hz, adjust them here
135
+ this.volumeEnvelopeSmoothingFactor = VOLUME_ENVELOPE_SMOOTHING_FACTOR * (sampleRate / 44100);
136
+ this.panSmoothingFactor = PAN_SMOOTHING_FACTOR * (sampleRate / 44100);
137
+
128
138
  /**
129
139
  * Controls the system
130
140
  * @typedef {"gm"|"gm2"|"gs"|"xg"} SynthSystem
@@ -63,7 +63,9 @@ export function noteOn(channel, midiNote, velocity, enableDebugging = false)
63
63
  }
64
64
  computeModulators(voice, this.workletProcessorChannels[channel].midiControllers);
65
65
  voice.currentAttenuationDb = 100;
66
- })
66
+ // set initial pan to avoid split second changing from middle to the correct value
67
+ voice.currentPan = ( (Math.max(-500, Math.min(500, voice.modulatedGenerators[generatorTypes.pan] )) + 500) / 1000) // 0 to 1
68
+ });
67
69
 
68
70
  this.totalVoicesAmount += voices.length;
69
71
  // cap the voices
@@ -1,14 +1,17 @@
1
1
  import { generatorTypes } from '../../../soundfont/chunk/generators.js'
2
- import { absCentsToHz, timecentsToSeconds } from '../worklet_utilities/unit_converter.js'
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
5
  import { getModEnvValue } 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 } from '../worklet_utilities/volume_envelope.js'
8
+ import {applyVolumeEnvelope, recalculateVolumeEnvelope} from '../worklet_utilities/volume_envelope.js'
9
9
  import { applyLowpassFilter } from '../worklet_utilities/lowpass_filter.js'
10
10
  import { MIN_NOTE_LENGTH } from '../../synthesizer.js'
11
11
 
12
+ const HALF_PI = Math.PI / 2;
13
+ export const PAN_SMOOTHING_FACTOR = 0.01;
14
+
12
15
  /**
13
16
  * Renders a voice to the stereo output buffer
14
17
  * @param channel {WorkletProcessorChannel} the voice's channel
@@ -20,18 +23,16 @@ import { MIN_NOTE_LENGTH } from '../../synthesizer.js'
20
23
  */
21
24
  export function renderVoice(channel, voice, output, reverbOutput, chorusOutput)
22
25
  {
23
- // if no matching sample, perhaps it's still being loaded...?
24
- if(this.workletDumpedSamplesList[voice.sample.sampleID] === undefined)
25
- {
26
- return;
27
- }
28
-
29
26
  // check if release
30
- if(!voice.isInRelease) {
27
+ if(!voice.isInRelease)
28
+ {
31
29
  // if not in release, check if the release time is
32
- if (this.currentTime >= voice.releaseStartTime) {
30
+ if (this.currentTime >= voice.releaseStartTime)
31
+ {
33
32
  voice.releaseStartModEnv = voice.currentModEnvValue;
34
33
  voice.isInRelease = true;
34
+ recalculateVolumeEnvelope(voice);
35
+ voice.volumeEnvelope.currentReleaseGain = decibelAttenuationToGain(voice.volumeEnvelope.releaseStartDb);
35
36
  }
36
37
  }
37
38
 
@@ -128,12 +129,12 @@ export function renderVoice(channel, voice, output, reverbOutput, chorusOutput)
128
129
  applyLowpassFilter(voice, bufferOut, lowpassCents, this.sampleRate);
129
130
 
130
131
  // volenv
131
- applyVolumeEnvelope(voice, bufferOut, this.currentTime, modLfoCentibels, this.sampleTime);
132
+ applyVolumeEnvelope(voice, bufferOut, this.currentTime, modLfoCentibels, this.sampleTime, this.volumeEnvelopeSmoothingFactor);
132
133
 
133
134
  // pan the voice and write out
134
- voice.currentPan += (pan - voice.currentPan) * 0.1; // smooth out pan to prevent clicking
135
- const panLeft = (1 - voice.currentPan) * this.panLeft;
136
- const panRight = voice.currentPan * this.panRight;
135
+ voice.currentPan += (pan - voice.currentPan) * this.panSmoothingFactor; // smooth out pan to prevent clicking
136
+ const panLeft = Math.cos(HALF_PI * voice.currentPan) * this.panLeft;
137
+ const panRight = Math.sin(HALF_PI * voice.currentPan) * this.panRight;
137
138
  panVoice(
138
139
  panLeft,
139
140
  panRight,
@@ -164,12 +165,12 @@ function getPriority(channel, voice)
164
165
  // less velocity = less important
165
166
  priority += voice.velocity / 25; // map to 0-5
166
167
  // the newer, more important
167
- priority -= voice.volumeEnvelopeState;
168
+ priority -= voice.volumeEnvelope.state;
168
169
  if(voice.isInRelease)
169
170
  {
170
171
  priority -= 5;
171
172
  }
172
- priority -= voice.currentAttenuationDb / 50;
173
+ priority -= voice.volumeEnvelope.currentAttenuationDb / 50;
173
174
  return priority;
174
175
  }
175
176
 
@@ -9,6 +9,44 @@ import { absCentsToHz, decibelAttenuationToGain } from './unit_converter.js'
9
9
  * Shoutout to them!
10
10
  */
11
11
 
12
+ /**
13
+ * @typedef {Object} WorkletLowpassFilter
14
+ * @property {number} a0 - filter coefficient 1
15
+ * @property {number} a1 - filter coefficient 2
16
+ * @property {number} a2 - filter coefficient 3
17
+ * @property {number} a3 - filter coefficient 4
18
+ * @property {number} a4 - filter coefficient 5
19
+ * @property {number} x1 - input history 1
20
+ * @property {number} x2 - input history 2
21
+ * @property {number} y1 - output history 1
22
+ * @property {number} y2 - output history 2
23
+ * @property {number} reasonanceCb - reasonance in centibels
24
+ * @property {number} reasonanceGain - resonance gain
25
+ * @property {number} cutoffCents - cutoff frequency in cents
26
+ * @property {number} cutoffHz - cutoff frequency in Hz
27
+ */
28
+
29
+ /**
30
+ * @type {WorkletLowpassFilter}
31
+ */
32
+ export const DEFAULT_WORKLET_LOWPASS_FILTER = {
33
+ a0: 0,
34
+ a1: 0,
35
+ a2: 0,
36
+ a3: 0,
37
+ a4: 0,
38
+
39
+ x1: 0,
40
+ x2: 0,
41
+ y1: 0,
42
+ y2: 0,
43
+
44
+ reasonanceCb: 0,
45
+ reasonanceGain: 1,
46
+ cutoffCents: 13500,
47
+ cutoffHz: 20000
48
+ }
49
+
12
50
 
13
51
  /**
14
52
  * Applies a low-pass filter to the given buffer
@@ -6,6 +6,47 @@ import { generatorTypes } from '../../../soundfont/chunk/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
+ export const VOLUME_ENVELOPE_SMOOTHING_FACTOR = 0.001;
49
+
9
50
  const DB_SILENCE = 100;
10
51
  const GAIN_SILENCE = 0.005;
11
52
 
@@ -20,175 +61,212 @@ const GAIN_SILENCE = 0.005;
20
61
  */
21
62
 
22
63
  /**
23
- * Applies volume envelope gain to the given output buffer
64
+ * Recalculates the times of the volume envelope
24
65
  * @param voice {WorkletVoice} the voice we're working on
25
- * @param audioBuffer {Float32Array} the audio buffer to modify
26
- * @param currentTime {number} the current audio time
27
- * @param centibelOffset {number} the centibel offset of volume, for modLFOtoVolume
28
- * @param sampleTime {number} single sample time in seconds, usually 1 / 44100 of a second
29
66
  */
30
-
31
- export function applyVolumeEnvelope(voice, audioBuffer, currentTime, centibelOffset, sampleTime)
67
+ export function recalculateVolumeEnvelope(voice)
32
68
  {
33
- // calculate values
34
- let decibelOffset = centibelOffset / 10;
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;
35
79
 
36
- // calculate env times
37
- let attack = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.attackVolEnv]);
38
- let decay = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.decayVolEnv] + ((60 - voice.midiNote) * voice.modulatedGenerators[generatorTypes.keyNumToVolEnvDecay]));
80
+ // calculate absolute end time
81
+ env.delayEnd = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.delayVolEnv]) + voice.startTime;
82
+ env.attackEnd = env.attackDuration + env.delayEnd;
39
83
 
40
- // calculate absolute times
41
- let attenuation = voice.modulatedGenerators[generatorTypes.initialAttenuation] / 10; // divide by ten to get decibelts
42
- let release = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.releaseVolEnv]);
43
- let sustain = attenuation + voice.modulatedGenerators[generatorTypes.sustainVolEnv] / 10;
44
- let delayEnd = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.delayVolEnv]) + voice.startTime;
45
- let attackEnd = attack + delayEnd;
46
- let holdEnd = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.holdVolEnv] + ((60 - voice.midiNote) * voice.modulatedGenerators[generatorTypes.keyNumToVolEnvHold])) + attackEnd;
47
- let decayEnd = decay + holdEnd;
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;
48
88
 
89
+ env.decayEnd = env.decayDuration + env.holdEnd;
49
90
  // check if voice is in release
50
91
  if(voice.isInRelease)
51
92
  {
52
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)
53
- let releaseStartDb;
54
- switch (voice.volumeEnvelopeState)
55
- {
94
+ switch (env.state) {
56
95
  case 0:
57
- // no sound: fill with zero and skip!
58
- for (let i = 0; i < audioBuffer.length; i++) {
59
- audioBuffer[i] = 0;
60
- }
61
- return;
96
+ env.releaseStartDb = 0;
97
+ break;
62
98
 
63
99
  case 1:
64
100
  // attack phase
65
101
  // attack is linear (in gain) so we need to do get db from that
66
- let elapsed = 1 - ((attackEnd - voice.releaseStartTime) / attack);
102
+ let elapsed = 1 - ((env.attackEnd - voice.releaseStartTime) / env.attackDuration);
67
103
  // calculate the gain that the attack would have
68
- let attackGain = elapsed * decibelAttenuationToGain(attenuation + decibelOffset);
104
+ let attackGain = elapsed * decibelAttenuationToGain(env.attenuation);
69
105
 
70
106
  // turn that into db
71
- releaseStartDb = 20 * Math.log10(attackGain) * -1;
107
+ env.releaseStartDb = 20 * Math.log10(attackGain) * -1;
72
108
  break;
73
109
 
74
110
  case 2:
75
- // hold
76
- releaseStartDb = attenuation;
111
+ env.releaseStartDb = env.attenuation;
77
112
  break;
78
113
 
79
114
  case 3:
80
- // decay
81
- releaseStartDb = (1 - (decayEnd - voice.releaseStartTime) / decay) * (sustain - attenuation) + attenuation;
115
+ env.releaseStartDb = (1 - (env.decayEnd - voice.releaseStartTime) / env.decayDuration) * (env.sustainDb - env.attenuation) + env.attenuation;
82
116
  break;
83
117
 
84
118
  case 4:
85
- // sustain
86
- releaseStartDb = sustain;
119
+ env.releaseStartDb = env.sustainDb;
87
120
  break;
88
121
 
122
+ default:
123
+ env.releaseStartDb = env.currentAttenuationDb;
89
124
  }
125
+ }
126
+ }
90
127
 
91
- // uncommented because doesn't seem to be needed but just in case
92
- // // if the voice is not released, but state set to true (due to min note length, simply use the release db)
93
- // if(voice.releaseStartTime > currentTime)
94
- // {
95
- // const gain = decibelAttenuationToGain(releaseStartDb + decibelOffset);
96
- // for (let i = 0; i < audioBuffer.length; i++) {
97
- // audioBuffer[i] = gain * audioBuffer[i];
98
- // }
99
- // return;
100
- // }
128
+ /**
129
+ * Applies volume envelope gain to the given output buffer
130
+ * @param voice {WorkletVoice} the voice we're working on
131
+ * @param audioBuffer {Float32Array} the audio buffer to modify
132
+ * @param currentTime {number} the current audio time
133
+ * @param centibelOffset {number} the centibel offset of volume, for modLFOtoVolume
134
+ * @param sampleTime {number} single sample time in seconds, usually 1 / 44100 of a second
135
+ * @param smoothingFactor {number} the adjusted smoothing factor for the envelope
136
+ */
101
137
 
138
+ export function applyVolumeEnvelope(voice, audioBuffer, currentTime, centibelOffset, sampleTime, smoothingFactor)
139
+ {
140
+ let decibelOffset = centibelOffset / 10;
141
+ const env = voice.volumeEnvelope;
142
+
143
+ // RELEASE PHASE
144
+ if(voice.isInRelease)
145
+ {
146
+ // release needs a more aggressive smoothing factor as the instant notes don't end instantly when they should
147
+ const releaseSmoothingFactor = smoothingFactor * 10;
148
+ const releaseStartDb = env.releaseStartDb + decibelOffset;
102
149
  let elapsedRelease = currentTime - voice.releaseStartTime;
103
150
  let dbDifference = DB_SILENCE - releaseStartDb;
104
- let gain;
105
- for (let i = 0; i < audioBuffer.length; i++) {
106
- let db = (elapsedRelease / release) * dbDifference + releaseStartDb;
151
+ let gain = env.currentReleaseGain;
152
+ for (let i = 0; i < audioBuffer.length; i++)
153
+ {
154
+ let db = (elapsedRelease / env.releaseDuration) * dbDifference + releaseStartDb;
107
155
  gain = decibelAttenuationToGain(db + decibelOffset);
108
- audioBuffer[i] = gain * audioBuffer[i];
156
+ env.currentReleaseGain += (gain - env.currentReleaseGain) * releaseSmoothingFactor;
157
+ audioBuffer[i] *= env.currentReleaseGain;
109
158
  elapsedRelease += sampleTime;
110
159
  }
111
160
 
112
- if(gain <= GAIN_SILENCE)
161
+ if(env.currentReleaseGain <= GAIN_SILENCE)
113
162
  {
114
163
  voice.finished = true;
115
164
  }
116
165
  return;
117
166
  }
167
+
168
+ if(!voice.hasStarted)
169
+ {
170
+ voice.startTime = currentTime;
171
+ recalculateVolumeEnvelope(voice);
172
+ voice.hasStarted = true;
173
+ }
174
+
118
175
  let currentFrameTime = currentTime;
119
- let dbAttenuation;
120
- for (let i = 0; i < audioBuffer.length; i++) {
121
- switch(voice.volumeEnvelopeState)
122
- {
123
- case 0:
124
- // delay phase, no sound is produced
125
- if(currentFrameTime >= delayEnd)
176
+ let filledBuffer = 0;
177
+ switch(env.state)
178
+ {
179
+ case 0:
180
+ // delay phase, no sound is produced
181
+ while(currentFrameTime < env.delayEnd)
182
+ {
183
+ env.currentAttenuationDb = DB_SILENCE;
184
+ audioBuffer[filledBuffer] = 0;
185
+
186
+ currentFrameTime += sampleTime;
187
+ if(++filledBuffer >= audioBuffer.length)
126
188
  {
127
- voice.volumeEnvelopeState++;
189
+ return;
128
190
  }
129
- else
130
- {
131
- dbAttenuation = DB_SILENCE;
132
- audioBuffer[i] = 0;
191
+ }
192
+ env.state++;
193
+ // fallthrough
133
194
 
134
- // no need to go through the hassle of converting. Skip
135
- currentFrameTime += sampleTime;
136
- continue;
137
- }
138
- // fallthrough
195
+ case 1:
196
+ // attack phase: ramp from 0 to attenuation
197
+ while(currentFrameTime < env.attackEnd)
198
+ {
199
+ // Special case: linear gain ramp instead of linear db ramp
200
+ let linearAttenuation = 1 - (env.attackEnd - currentFrameTime) / env.attackDuration; // 0 to 1
201
+ const gain = linearAttenuation * decibelAttenuationToGain(env.attenuation + decibelOffset)
202
+ audioBuffer[filledBuffer] *= gain;
139
203
 
140
- case 1:
141
- // attack phase: ramp from 0 to attenuation
142
- if(currentFrameTime >= attackEnd)
204
+ // set current attenuation to peak as its invalid during this phase
205
+ env.currentAttenuationDb = env.attenuation;
206
+
207
+ currentFrameTime += sampleTime;
208
+ if(++filledBuffer >= audioBuffer.length)
143
209
  {
144
- voice.volumeEnvelopeState++;
210
+ return;
145
211
  }
146
- else {
147
- // Special case: linear gain ramp instead of linear db ramp
148
- const elapsed = (attackEnd - currentFrameTime) / attack;
149
- dbAttenuation = 10 * Math.log10((elapsed * (attenuation - DB_SILENCE) + DB_SILENCE) * -1);
150
- audioBuffer[i] = audioBuffer[i] * (1 - elapsed) * decibelAttenuationToGain(attenuation + decibelOffset);
151
- currentFrameTime += sampleTime;
152
- continue
212
+ }
213
+ env.state++;
214
+ // fallthrough
153
215
 
154
- }
155
- // fallthrough
216
+ case 2:
217
+ // hold/peak phase: stay at attenuation
218
+ while(currentFrameTime < env.holdEnd)
219
+ {
220
+ const newAttenuation = env.attenuation
221
+ + decibelOffset;
156
222
 
157
- case 2:
158
- // hold/peak phase: stay at attenuation
159
- if(currentFrameTime >= holdEnd)
160
- {
161
- voice.volumeEnvelopeState++;
162
- }
163
- else
223
+ // interpolate attenuation to prevent clicking
224
+ env.currentAttenuationDb += (newAttenuation - env.currentAttenuationDb) * smoothingFactor;
225
+ audioBuffer[filledBuffer] *= decibelAttenuationToGain(env.currentAttenuationDb);
226
+
227
+ currentFrameTime += sampleTime;
228
+ if(++filledBuffer >= audioBuffer.length)
164
229
  {
165
- dbAttenuation = attenuation;
166
- break;
230
+ return;
167
231
  }
168
- // fallthrough
232
+ }
233
+ env.state++;
234
+ // fallthrough
169
235
 
170
- case 3:
171
- // decay phase: linear ramp from attenuation to sustain
172
- if(currentFrameTime >= decayEnd)
236
+ case 3:
237
+ // decay phase: linear ramp from attenuation to sustain
238
+ while(currentFrameTime < env.decayEnd)
239
+ {
240
+ const newAttenuation = (1 - (env.decayEnd - currentFrameTime) / env.decayDuration) * (env.sustainDb - env.attenuation) + env.attenuation
241
+ + decibelOffset;
242
+
243
+ // interpolate attenuation to prevent clicking
244
+ env.currentAttenuationDb += (newAttenuation - env.currentAttenuationDb) * smoothingFactor;
245
+ audioBuffer[filledBuffer] *= decibelAttenuationToGain(env.currentAttenuationDb);
246
+
247
+ currentFrameTime += sampleTime;
248
+ if(++filledBuffer >= audioBuffer.length)
173
249
  {
174
- voice.volumeEnvelopeState++;
250
+ return;
175
251
  }
176
- else
252
+ }
253
+ env.state++;
254
+ // fallthrough
255
+
256
+ case 4:
257
+ // sustain phase: stay at sustain
258
+ while(true)
259
+ {
260
+ // interpolate attenuation to prevent clicking
261
+ const newAttenuation = env.sustainDb
262
+ + decibelOffset;
263
+ env.currentAttenuationDb += (newAttenuation - env.currentAttenuationDb) * smoothingFactor;
264
+ audioBuffer[filledBuffer] *= decibelAttenuationToGain(env.currentAttenuationDb);
265
+ if(++filledBuffer >= audioBuffer.length)
177
266
  {
178
- dbAttenuation = (1 - (decayEnd - currentFrameTime) / decay) * (sustain - attenuation) + attenuation;
179
- break
267
+ return;
180
268
  }
181
- // fallthrough
269
+ }
182
270
 
183
- case 4:
184
- // sustain phase: stay at sustain
185
- dbAttenuation = sustain;
186
-
187
- }
188
- // apply gain and advance the time
189
- const gain = decibelAttenuationToGain(dbAttenuation + decibelOffset);
190
- audioBuffer[i] = audioBuffer[i] * gain;
191
- currentFrameTime += sampleTime;
192
271
  }
193
- voice.currentAttenuationDb = dbAttenuation;
194
272
  }
@@ -1,6 +1,7 @@
1
1
  import { modulatorSources } from '../../../soundfont/chunk/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";
4
5
 
5
6
  /**
6
7
  * worklet_modulator.js
@@ -107,6 +108,7 @@ export function computeModulators(voice, controllerTable)
107
108
  voice.modulators.forEach(mod => {
108
109
  voice.modulatedGenerators[mod.modulatorDestination] += computeWorkletModulator(controllerTable, mod, voice.midiNote, voice.velocity);
109
110
  });
111
+ recalculateVolumeEnvelope(voice);
110
112
  }
111
113
 
112
114
  /**
@@ -16,23 +16,6 @@
16
16
  * @property {0|1|2} loopingMode - looping mode of the sample
17
17
  */
18
18
 
19
- /**
20
- * @typedef {Object} WorkletLowpassFilter
21
- * @property {number} a0 - filter coefficient 1
22
- * @property {number} a1 - filter coefficient 2
23
- * @property {number} a2 - filter coefficient 3
24
- * @property {number} a3 - filter coefficient 4
25
- * @property {number} a4 - filter coefficient 5
26
- * @property {number} x1 - input history 1
27
- * @property {number} x2 - input history 2
28
- * @property {number} y1 - output history 1
29
- * @property {number} y2 - output history 2
30
- * @property {number} reasonanceCb - reasonance in centibels
31
- * @property {number} reasonanceGain - resonance gain
32
- * @property {number} cutoffCents - cutoff frequency in cents
33
- * @property {number} cutoffHz - cutoff frequency in Hz
34
- */
35
-
36
19
  /**
37
20
  * @typedef {Object} WorkletVoice
38
21
  * @property {WorkletSample} sample - sample ID for voice.
@@ -43,19 +26,20 @@
43
26
  *
44
27
  * @property {boolean} finished - indicates if the voice has finished
45
28
  * @property {boolean} isInRelease - indicates if the voice is in the release phase
29
+ * @property {boolean} hasStarted - indicates if the voice has started rendering
46
30
  *
47
31
  * @property {number} channelNumber - MIDI channel number
48
32
  * @property {number} velocity - velocity of the note
49
33
  * @property {number} midiNote - MIDI note number
50
34
  * @property {number} targetKey - target key for the note
51
35
  *
52
- * @property {number} currentAttenuationDb - current attenuation in dB (used for calculating start of the release phase)
53
- * @property {0|1|2|3|4} volumeEnvelopeState - state of the volume envelope.
36
+ * @property {WorkletVolumeEnvelope} volumeEnvelope
37
+ *
54
38
  * @property {number} currentModEnvValue - current value of the modulation envelope
39
+ * @property {number} releaseStartModEnv - modenv value at the start of the release phase
55
40
  *
56
41
  * @property {number} startTime - start time of the voice
57
42
  * @property {number} releaseStartTime - start time of the release phase
58
- * @property {number} releaseStartModEnv - modenv value at the start of the release phase
59
43
  *
60
44
  * @property {number} currentTuningCents - current tuning adjustment in cents
61
45
  * @property {number} currentTuningCalculated - calculated tuning adjustment
@@ -64,6 +48,8 @@
64
48
 
65
49
  import { addAndClampGenerator, generatorTypes } from '../../../soundfont/chunk/generators.js'
66
50
  import { SpessaSynthTable } from '../../../utils/loggin.js'
51
+ import { DEFAULT_WORKLET_VOLUME_ENVELOPE } from './volume_envelope.js'
52
+ import { DEFAULT_WORKLET_LOWPASS_FILTER } from './lowpass_filter.js'
67
53
 
68
54
 
69
55
  /**
@@ -260,22 +246,7 @@ export function getWorkletVoices(channel,
260
246
  }
261
247
 
262
248
  return {
263
- filter: {
264
- a0: 0,
265
- a1: 0,
266
- a2: 0,
267
- a3: 0,
268
- a4: 0,
269
-
270
- x1: 0,
271
- x2: 0,
272
- y1: 0,
273
- y2: 0,
274
- reasonanceCb: 0,
275
- reasonanceGain: 1,
276
- cutoffCents: 13500,
277
- cutoffHz: 20000
278
- },
249
+ filter: deepClone(DEFAULT_WORKLET_LOWPASS_FILTER),
279
250
  // generators and modulators
280
251
  generators: generators,
281
252
  modulators: sampleAndGenerators.modulators,
@@ -295,11 +266,12 @@ export function getWorkletVoices(channel,
295
266
  // envelope data
296
267
  finished: false,
297
268
  isInRelease: false,
298
- currentAttenuationDb: 100,
269
+ hasStarted: false,
299
270
  currentModEnvValue: 0,
300
271
  releaseStartModEnv: 1,
301
- volumeEnvelopeState: 0,
302
- currentPan: 0.5
272
+ currentPan: 0.5,
273
+
274
+ volumeEnvelope: deepClone(DEFAULT_WORKLET_VOLUME_ENVELOPE)
303
275
  };
304
276
 
305
277
  });