11export interface AmplitudeEnvelopeParameter {
22 attackTime : number
3+ holdTime : number
34 decayTime : number
45 sustainLevel : number
56 releaseTime : number
67}
78
89enum EnvelopePhase {
910 attack ,
11+ hold ,
1012 decay ,
1113 sustain ,
1214 release ,
@@ -18,7 +20,10 @@ const forceStopReleaseTime = 0.1
1820
1921export class AmplitudeEnvelope {
2022 private readonly parameter : AmplitudeEnvelopeParameter
21- private phase = EnvelopePhase . attack
23+ private _phase = EnvelopePhase . stopped
24+ private isNoteOff = false
25+ private phaseTime = 0
26+ private decayLevel = 0 // amplitude level at the end of decay phase
2227 private lastAmplitude = 0
2328 private readonly sampleRate : number
2429
@@ -27,69 +32,99 @@ export class AmplitudeEnvelope {
2732 this . sampleRate = sampleRate
2833 }
2934
35+ private get phase ( ) {
36+ return this . _phase
37+ }
38+
39+ private set phase ( phase : EnvelopePhase ) {
40+ if ( this . _phase === phase ) {
41+ return
42+ }
43+ this . _phase = phase
44+ this . phaseTime = 0
45+ }
46+
3047 noteOn ( ) {
3148 this . phase = EnvelopePhase . attack
49+ this . isNoteOff = false
50+ this . phaseTime = 0
51+ this . decayLevel = this . parameter . sustainLevel
3252 }
3353
3454 noteOff ( ) {
35- if ( this . phase !== EnvelopePhase . forceStop ) {
36- this . phase = EnvelopePhase . release
37- }
55+ this . isNoteOff = true
3856 }
3957
4058 // Rapidly decrease the volume. This method ignores release time parameter
4159 forceStop ( ) {
4260 this . phase = EnvelopePhase . forceStop
4361 }
4462
45- getAmplitude ( bufferSize : number ) : number {
46- const { attackTime, decayTime, sustainLevel, releaseTime } = this . parameter
63+ calculateAmplitude ( bufferSize : number ) : number {
64+ const { attackTime, holdTime, decayTime, sustainLevel, releaseTime } =
65+ this . parameter
4766 const { sampleRate } = this
4867
68+ if (
69+ this . isNoteOff &&
70+ ( this . phase === EnvelopePhase . decay ||
71+ this . phase === EnvelopePhase . sustain )
72+ ) {
73+ this . phase = EnvelopePhase . release
74+ this . decayLevel = this . lastAmplitude
75+ }
76+
4977 // Attack
5078 switch ( this . phase ) {
5179 case EnvelopePhase . attack : {
5280 const amplificationPerFrame =
5381 ( 1 / ( attackTime * sampleRate ) ) * bufferSize
5482 const value = this . lastAmplitude + amplificationPerFrame
5583 if ( value >= 1 ) {
56- this . phase = EnvelopePhase . decay
57- this . lastAmplitude = 1
84+ this . phase = EnvelopePhase . hold
5885 return 1
5986 }
60- this . lastAmplitude = value
6187 return value
6288 }
89+ case EnvelopePhase . hold : {
90+ if ( this . phaseTime >= holdTime ) {
91+ this . phase = EnvelopePhase . decay
92+ }
93+ return this . lastAmplitude
94+ }
6395 case EnvelopePhase . decay : {
64- const attenuationPerFrame = ( 1 / ( decayTime * sampleRate ) ) * bufferSize
65- const value = this . lastAmplitude - attenuationPerFrame
66- if ( value <= sustainLevel ) {
96+ const attenuationDecibel = linearToDecibel ( sustainLevel / 1 )
97+ const value = logAttenuation (
98+ 1.0 ,
99+ attenuationDecibel ,
100+ decayTime ,
101+ this . phaseTime
102+ )
103+ if ( this . phaseTime > decayTime ) {
67104 if ( sustainLevel <= 0 ) {
68105 this . phase = EnvelopePhase . stopped
69- this . lastAmplitude = 0
70106 return 0
71107 } else {
72108 this . phase = EnvelopePhase . sustain
73- this . lastAmplitude = sustainLevel
74109 return sustainLevel
75110 }
76111 }
77- this . lastAmplitude = value
78112 return value
79113 }
80114 case EnvelopePhase . sustain : {
81115 return sustainLevel
82116 }
83117 case EnvelopePhase . release : {
84- const attenuationPerFrame =
85- ( 1 / ( releaseTime * sampleRate ) ) * bufferSize
86- const value = this . lastAmplitude - attenuationPerFrame
87- if ( value <= 0 ) {
118+ const value = logAttenuation (
119+ this . decayLevel ,
120+ - 100 , // -100dB means almost silence
121+ releaseTime ,
122+ this . phaseTime
123+ )
124+ if ( this . phaseTime > releaseTime || value <= 0 ) {
88125 this . phase = EnvelopePhase . stopped
89- this . lastAmplitude = 0
90126 return 0
91127 }
92- this . lastAmplitude = value
93128 return value
94129 }
95130 case EnvelopePhase . forceStop : {
@@ -98,10 +133,8 @@ export class AmplitudeEnvelope {
98133 const value = this . lastAmplitude - attenuationPerFrame
99134 if ( value <= 0 ) {
100135 this . phase = EnvelopePhase . stopped
101- this . lastAmplitude = 0
102136 return 0
103137 }
104- this . lastAmplitude = value
105138 return value
106139 }
107140 case EnvelopePhase . stopped : {
@@ -110,7 +143,32 @@ export class AmplitudeEnvelope {
110143 }
111144 }
112145
146+ getAmplitude ( bufferSize : number ) : number {
147+ const value = this . calculateAmplitude ( bufferSize )
148+ this . lastAmplitude = value
149+ this . phaseTime += bufferSize / sampleRate
150+ return value
151+ }
152+
113153 get isPlaying ( ) {
114154 return this . phase !== EnvelopePhase . stopped
115155 }
116156}
157+
158+ // An exponential decay function. It attenuates the value of decibel over the duration time.
159+ function logAttenuation (
160+ fromLevel : number ,
161+ attenuationDecibel : number ,
162+ duration : number ,
163+ time : number
164+ ) : number {
165+ return fromLevel * decibelToLinear ( ( attenuationDecibel / duration ) * time )
166+ }
167+
168+ function linearToDecibel ( value : number ) : number {
169+ return 20 * Math . log10 ( value )
170+ }
171+
172+ function decibelToLinear ( value : number ) : number {
173+ return Math . pow ( 10 , value / 20 )
174+ }
0 commit comments