spessasynth_lib 3.20.26 → 3.20.28
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 +1 -1
- package/synthetizer/worklet_processor.min.js +7 -7
- package/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js +28 -26
- package/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js +1 -1
- package/synthetizer/worklet_system/worklet_utilities/volume_envelope.js +14 -2
- package/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +1 -1
|
@@ -97,16 +97,17 @@ export class WorkletLowpassFilter
|
|
|
97
97
|
*/
|
|
98
98
|
static apply(voice, outputBuffer, cutoffCents)
|
|
99
99
|
{
|
|
100
|
-
if(cutoffCents > 13499)
|
|
100
|
+
if(cutoffCents > 13499 && voice.filter.reasonanceCb === 0)
|
|
101
101
|
{
|
|
102
102
|
return; // filter is open
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
const filter = voice.filter
|
|
105
106
|
// check if the frequency has changed. if so, calculate new coefficients
|
|
106
|
-
if(
|
|
107
|
+
if(filter.cutoffCents !== cutoffCents || filter.reasonanceCb !== voice.modulatedGenerators[generatorTypes.initialFilterQ])
|
|
107
108
|
{
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
filter.cutoffCents = cutoffCents;
|
|
110
|
+
filter.reasonanceCb = voice.modulatedGenerators[generatorTypes.initialFilterQ];
|
|
110
111
|
WorkletLowpassFilter.calculateCoefficients(voice);
|
|
111
112
|
}
|
|
112
113
|
|
|
@@ -114,17 +115,17 @@ export class WorkletLowpassFilter
|
|
|
114
115
|
for (let i = 0; i < outputBuffer.length; i++)
|
|
115
116
|
{
|
|
116
117
|
let input = outputBuffer[i];
|
|
117
|
-
let filtered =
|
|
118
|
-
+
|
|
119
|
-
+
|
|
120
|
-
-
|
|
121
|
-
-
|
|
118
|
+
let filtered = filter.a0 * input
|
|
119
|
+
+ filter.a1 * filter.x1
|
|
120
|
+
+ filter.a2 * filter.x2
|
|
121
|
+
- filter.a3 * filter.y1
|
|
122
|
+
- filter.a4 * filter.y2;
|
|
122
123
|
|
|
123
124
|
// set buffer
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
filter.x2 = filter.x1;
|
|
126
|
+
filter.x1 = input;
|
|
127
|
+
filter.y2 = filter.y1;
|
|
128
|
+
filter.y1 = filtered;
|
|
128
129
|
|
|
129
130
|
outputBuffer[i] = filtered;
|
|
130
131
|
}
|
|
@@ -135,27 +136,28 @@ export class WorkletLowpassFilter
|
|
|
135
136
|
*/
|
|
136
137
|
static calculateCoefficients(voice)
|
|
137
138
|
{
|
|
138
|
-
|
|
139
|
+
const filter = voice.filter;
|
|
140
|
+
filter.cutoffHz = absCentsToHz(filter.cutoffCents);
|
|
139
141
|
|
|
140
142
|
// fix cutoff on low frequencies (fluid_iir_filter.c line 392)
|
|
141
|
-
if(
|
|
143
|
+
if(filter.cutoffHz > 0.45 * sampleRate)
|
|
142
144
|
{
|
|
143
|
-
|
|
145
|
+
filter.cutoffHz = 0.45 * sampleRate;
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
// adjust the filterQ (fluid_iir_filter.c line 204)
|
|
147
|
-
const qDb = (
|
|
148
|
-
|
|
149
|
+
const qDb = (filter.reasonanceCb / 10) - 3.01;
|
|
150
|
+
filter.reasonanceGain = decibelAttenuationToGain(-1 * qDb); // -1 because it's attenuation and we don't want attenuation
|
|
149
151
|
|
|
150
152
|
// reduce the gain by the Q factor (fluid_iir_filter.c line 250)
|
|
151
|
-
const qGain = 1 / Math.sqrt(
|
|
153
|
+
const qGain = 1 / Math.sqrt(filter.reasonanceGain);
|
|
152
154
|
|
|
153
155
|
|
|
154
156
|
// code is ported from https://github.com/sinshu/meltysynth/ to work with js.
|
|
155
157
|
// I'm too dumb to understand the math behind this...
|
|
156
|
-
let w = 2 * Math.PI *
|
|
158
|
+
let w = 2 * Math.PI * filter.cutoffHz / sampleRate; // we're in the audioworkletglobalscope so we can use sampleRate
|
|
157
159
|
let cosw = Math.cos(w);
|
|
158
|
-
let alpha = Math.sin(w) / (2 *
|
|
160
|
+
let alpha = Math.sin(w) / (2 * filter.reasonanceGain);
|
|
159
161
|
|
|
160
162
|
let b1 = (1 - cosw) * qGain;
|
|
161
163
|
let b0 = b1 / 2;
|
|
@@ -165,10 +167,10 @@ export class WorkletLowpassFilter
|
|
|
165
167
|
let a2 = 1 - alpha;
|
|
166
168
|
|
|
167
169
|
// set coefficients
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
filter.a0 = b0 / a0;
|
|
171
|
+
filter.a1 = b1 / a0;
|
|
172
|
+
filter.a2 = b2 / a0;
|
|
173
|
+
filter.a3 = a1 / a0;
|
|
174
|
+
filter.a4 = a2 / a0;
|
|
173
175
|
}
|
|
174
176
|
}
|
|
@@ -111,7 +111,7 @@ export class WorkletModulationEnvelope
|
|
|
111
111
|
const holdKeyExcursionCents = ((60 - voice.midiNote) * voice.modulatedGenerators[generatorTypes.keyNumToModEnvHold]);
|
|
112
112
|
env.holdDuration = timecentsToSeconds(holdKeyExcursionCents + voice.modulatedGenerators[generatorTypes.holdModEnv]);
|
|
113
113
|
|
|
114
|
-
const releaseTime = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.
|
|
114
|
+
const releaseTime = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.releaseModEnv]);
|
|
115
115
|
// release time is from the full level to 0%
|
|
116
116
|
// to get the actual time, multiply by the release start level
|
|
117
117
|
env.releaseDuration = releaseTime * env.releaseStartLevel;
|
|
@@ -9,7 +9,7 @@ import { generatorTypes } from '../../../soundfont/read_sf2/generators.js'
|
|
|
9
9
|
export const VOLUME_ENVELOPE_SMOOTHING_FACTOR = 0.001;
|
|
10
10
|
|
|
11
11
|
const DB_SILENCE = 100;
|
|
12
|
-
const PERCEIVED_DB_SILENCE =
|
|
12
|
+
const PERCEIVED_DB_SILENCE = 90;
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* VOL ENV STATES:
|
|
@@ -25,10 +25,18 @@ export class WorkletVolumeEnvelope
|
|
|
25
25
|
{
|
|
26
26
|
/**
|
|
27
27
|
* @param sampleRate {number} Hz
|
|
28
|
+
* @param initialDecay {number} cb
|
|
28
29
|
*/
|
|
29
|
-
constructor(sampleRate)
|
|
30
|
+
constructor(sampleRate, initialDecay)
|
|
30
31
|
{
|
|
31
32
|
this.sampleRate = sampleRate;
|
|
33
|
+
/**
|
|
34
|
+
* if sustain stge is silent,
|
|
35
|
+
* then we can turn off the voice when it is silent.
|
|
36
|
+
* We can't do that with modulated as it can silence the volume and then raise it again and the voice must keep playing
|
|
37
|
+
* @type {boolean}
|
|
38
|
+
*/
|
|
39
|
+
this.canEndOnSilentSustain = initialDecay / 10 >= PERCEIVED_DB_SILENCE;
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
/**
|
|
@@ -383,6 +391,10 @@ export class WorkletVolumeEnvelope
|
|
|
383
391
|
// fallthrough
|
|
384
392
|
|
|
385
393
|
case 4:
|
|
394
|
+
if(env.canEndOnSilentSustain && env.sustainDbRelative >= PERCEIVED_DB_SILENCE)
|
|
395
|
+
{
|
|
396
|
+
voice.finished = true;
|
|
397
|
+
}
|
|
386
398
|
// sustain phase: stay at sustain
|
|
387
399
|
while(true)
|
|
388
400
|
{
|
|
@@ -142,7 +142,7 @@ class WorkletVoice
|
|
|
142
142
|
this.channelNumber = channel;
|
|
143
143
|
this.startTime = currentTime;
|
|
144
144
|
this.targetKey = targetKey;
|
|
145
|
-
this.volumeEnvelope = new WorkletVolumeEnvelope(sampleRate);
|
|
145
|
+
this.volumeEnvelope = new WorkletVolumeEnvelope(sampleRate, generators[generatorTypes.sustainVolEnv]);
|
|
146
146
|
}
|
|
147
147
|
/**
|
|
148
148
|
* Sample ID for voice.
|