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 +1 -1
- package/spessasynth_core/synthetizer/synthesizer.js +11 -1
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/note_on.js +3 -1
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/voice_control.js +17 -16
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js +38 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/volume_envelope.js +188 -110
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js +2 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +11 -39
package/package.json
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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 {
|
|
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) *
|
|
135
|
-
const 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.
|
|
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
|
-
*
|
|
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
|
-
|
|
34
|
-
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
54
|
-
switch (voice.volumeEnvelopeState)
|
|
55
|
-
{
|
|
94
|
+
switch (env.state) {
|
|
56
95
|
case 0:
|
|
57
|
-
|
|
58
|
-
|
|
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) /
|
|
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
|
|
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
|
-
|
|
76
|
-
releaseStartDb = attenuation;
|
|
111
|
+
env.releaseStartDb = env.attenuation;
|
|
77
112
|
break;
|
|
78
113
|
|
|
79
114
|
case 3:
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
+
env.currentReleaseGain += (gain - env.currentReleaseGain) * releaseSmoothingFactor;
|
|
157
|
+
audioBuffer[i] *= env.currentReleaseGain;
|
|
109
158
|
elapsedRelease += sampleTime;
|
|
110
159
|
}
|
|
111
160
|
|
|
112
|
-
if(
|
|
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
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
189
|
+
return;
|
|
128
190
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
audioBuffer[i] = 0;
|
|
191
|
+
}
|
|
192
|
+
env.state++;
|
|
193
|
+
// fallthrough
|
|
133
194
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
210
|
+
return;
|
|
145
211
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
166
|
-
break;
|
|
230
|
+
return;
|
|
167
231
|
}
|
|
168
|
-
|
|
232
|
+
}
|
|
233
|
+
env.state++;
|
|
234
|
+
// fallthrough
|
|
169
235
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
250
|
+
return;
|
|
175
251
|
}
|
|
176
|
-
|
|
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
|
-
|
|
179
|
-
break
|
|
267
|
+
return;
|
|
180
268
|
}
|
|
181
|
-
|
|
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 {
|
|
53
|
-
*
|
|
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
|
-
|
|
269
|
+
hasStarted: false,
|
|
299
270
|
currentModEnvValue: 0,
|
|
300
271
|
releaseStartModEnv: 1,
|
|
301
|
-
|
|
302
|
-
|
|
272
|
+
currentPan: 0.5,
|
|
273
|
+
|
|
274
|
+
volumeEnvelope: deepClone(DEFAULT_WORKLET_VOLUME_ENVELOPE)
|
|
303
275
|
};
|
|
304
276
|
|
|
305
277
|
});
|