@@ -15,15 +15,16 @@ type ReplayGain = {
15
15
}
16
16
17
17
export class AudioController {
18
- private audio = new Audio ( )
19
- private handle = - 1
20
- private volume = 1.0
21
- private fadeDuration = 200
18
+ private fadeDuration = 0.4
19
+
22
20
private buffer = new Audio ( )
23
21
private statsListener : any = null
24
22
private replayGainMode = ReplayGainMode . None
25
23
private replayGain : ReplayGain | null = null
26
24
25
+ private context = new AudioContext ( )
26
+ private pipeline = creatPipeline ( this . context )
27
+
27
28
ontimeupdate : ( value : number ) => void = ( ) => { /* do nothing */ }
28
29
ondurationchange : ( value : number ) => void = ( ) => { /* do nothing */ }
29
30
onpause : ( ) => void = ( ) => { /* do nothing */ }
@@ -32,78 +33,76 @@ export class AudioController {
32
33
onerror : ( err : MediaError | null ) => void = ( ) => { /* do nothing */ }
33
34
34
35
currentTime ( ) {
35
- return this . audio . currentTime
36
+ return this . pipeline . audio . currentTime
36
37
}
37
38
38
39
duration ( ) {
39
- return this . audio . duration
40
+ return this . pipeline . audio . duration
40
41
}
41
42
42
43
setBuffer ( url : string ) {
43
44
this . buffer . src = url
44
45
}
45
46
46
47
setVolume ( value : number ) {
47
- this . cancelFade ( )
48
- this . volume = value
49
- this . audio . volume = value * this . replayGainFactor ( )
48
+ this . pipeline . volumeNode . gain . value = value
50
49
}
51
50
52
51
setReplayGainMode ( value : ReplayGainMode ) {
53
52
this . replayGainMode = value
54
- this . setVolume ( this . volume )
53
+ this . pipeline . replayGainNode . gain . value = this . replayGainFactor ( )
54
+ console . log ( 'Set replay gain: ' + this . replayGainFactor ( ) )
55
55
}
56
56
57
57
setPlaybackRate ( value : number ) {
58
- this . audio . playbackRate = value
58
+ this . pipeline . audio . playbackRate = value
59
59
}
60
60
61
61
async pause ( ) {
62
- await this . fadeOut ( )
63
- this . audio . pause ( )
62
+ await this . fadeOut ( 0.1 )
63
+ this . pipeline . audio . pause ( )
64
64
}
65
65
66
66
async resume ( ) {
67
- this . audio . volume = 0.0
68
- await this . audio . play ( )
69
- this . fadeIn ( )
67
+ await this . pipeline . audio . play ( )
68
+ await this . fadeIn ( )
70
69
}
71
70
72
71
async seek ( value : number ) {
73
72
await this . fadeOut ( this . fadeDuration / 2.0 )
74
- this . audio . volume = 0.0
75
- this . audio . currentTime = value
73
+ this . pipeline . audio . currentTime = value
76
74
await this . fadeIn ( this . fadeDuration / 2.0 )
77
75
}
78
76
79
77
async changeTrack ( options : { url ?: string , paused ?: boolean , replayGain ?: ReplayGain , isStream ?: boolean , playbackRate ?: number } ) {
80
78
this . replayGain = options . replayGain || null
81
79
82
- if ( this . audio ) {
83
- this . cancelFade ( )
84
- endPlayback ( this . audio , this . fadeDuration )
80
+ if ( this . pipeline . audio ) {
81
+ endPlayback ( this . context , this . pipeline , this . fadeDuration )
85
82
}
86
- this . audio = new Audio ( options . url )
87
- this . audio . onerror = ( ) => {
88
- this . onerror ( this . audio . error )
83
+
84
+ this . pipeline = creatPipeline ( this . context , options . url )
85
+ this . pipeline . replayGainNode . gain . value = this . replayGainFactor ( )
86
+
87
+ this . pipeline . audio . onerror = ( ) => {
88
+ this . onerror ( this . pipeline . audio . error )
89
89
}
90
- this . audio . onended = ( ) => {
90
+ this . pipeline . audio . onended = ( ) => {
91
91
this . onended ( )
92
92
}
93
- this . audio . ontimeupdate = ( ) => {
94
- this . ontimeupdate ( this . audio . currentTime )
93
+ this . pipeline . audio . ontimeupdate = ( ) => {
94
+ this . ontimeupdate ( this . pipeline . audio . currentTime )
95
95
}
96
- this . audio . ondurationchange = ( ) => {
97
- this . ondurationchange ( this . audio . duration )
96
+ this . pipeline . audio . ondurationchange = ( ) => {
97
+ this . ondurationchange ( this . pipeline . audio . duration )
98
98
}
99
- this . audio . onpause = ( ) => {
99
+ this . pipeline . audio . onpause = ( ) => {
100
100
this . onpause ( )
101
101
}
102
- this . ondurationchange ( this . audio . duration )
103
- this . ontimeupdate ( this . audio . currentTime )
102
+ this . ondurationchange ( this . pipeline . audio . duration )
103
+ this . ontimeupdate ( this . pipeline . audio . currentTime )
104
104
this . onstreamtitlechange ( null )
105
- this . audio . volume = 0.0
106
- this . audio . playbackRate = options . playbackRate ?? 1.0
105
+ this . pipeline . audio . playbackRate = options . playbackRate ?? 1.0
107
106
108
107
this . statsListener ?. stop ( )
109
108
if ( options . isStream ) {
@@ -124,59 +123,30 @@ export class AudioController {
124
123
125
124
if ( options . paused !== true ) {
126
125
try {
127
- await this . audio . play ( )
126
+ await this . pipeline . audio . play ( )
128
127
} catch ( error ) {
129
128
if ( error instanceof Error && error . name === 'AbortError' ) {
130
129
console . warn ( error )
131
130
return
132
131
}
133
132
throw error
134
133
}
135
- this . fadeIn ( )
134
+ await this . fadeIn ( )
136
135
}
137
136
}
138
137
139
- private cancelFade ( ) {
140
- clearTimeout ( this . handle )
138
+ private async fadeIn ( duration : number = this . fadeDuration ) {
139
+ this . pipeline . fadeNode . gain . cancelScheduledValues ( 0 )
140
+ this . pipeline . fadeNode . gain . linearRampToValueAtTime ( 0 , this . context . currentTime )
141
+ this . pipeline . fadeNode . gain . linearRampToValueAtTime ( 1 , this . context . currentTime + duration )
142
+ await sleep ( duration * 1000 )
141
143
}
142
144
143
- private fadeIn ( duration : number = this . fadeDuration ) {
144
- this . fadeFromTo ( 0.0 , this . volume , duration ) . then ( )
145
- }
146
-
147
- private fadeOut ( duration : number = this . fadeDuration ) {
148
- return this . fadeFromTo ( this . volume , 0.0 , duration )
149
- }
150
-
151
- private fadeFromTo ( from : number , to : number , duration : number ) {
152
- const replayGainFactor = this . replayGainFactor ( )
153
- from *= replayGainFactor
154
- to *= replayGainFactor
155
-
156
- console . info ( `AudioController: start fade (${ from } , ${ to } , ${ duration } )` )
157
- const startTime = Date . now ( )
158
-
159
- const step = ( to - from ) / duration
160
- if ( duration <= 0.0 ) {
161
- this . audio . volume = to
162
- }
163
- clearTimeout ( this . handle )
164
- return new Promise < void > ( ( resolve ) => {
165
- const run = ( ) => {
166
- if ( this . audio . volume === to ) {
167
- console . info (
168
- 'AudioController: fade result. ' +
169
- `duration: ${ duration } ms, actual: ${ Date . now ( ) - startTime } ms, ` +
170
- `volume: ${ this . audio . volume } ` )
171
- resolve ( )
172
- return
173
- }
174
- const elapsed = Date . now ( ) - startTime
175
- this . audio . volume = clamp ( 0.0 , Math . max ( from , to ) , from + ( elapsed * step ) )
176
- this . handle = setTimeout ( run , 10 )
177
- }
178
- run ( )
179
- } )
145
+ private async fadeOut ( duration : number = this . fadeDuration ) {
146
+ this . pipeline . fadeNode . gain . cancelScheduledValues ( 0 )
147
+ this . pipeline . fadeNode . gain . linearRampToValueAtTime ( 1 , this . context . currentTime )
148
+ this . pipeline . fadeNode . gain . linearRampToValueAtTime ( 0 , this . context . currentTime + duration )
149
+ await sleep ( duration * 1000 )
180
150
}
181
151
182
152
private replayGainFactor ( ) : number {
@@ -202,48 +172,59 @@ export class AudioController {
202
172
const gainFactor = Math . pow ( 10 , ( gain + preAmp ) / 20 )
203
173
const peakFactor = 1 / peak
204
174
const factor = Math . min ( gainFactor , peakFactor )
205
-
206
175
console . info ( 'AudioController: calculated ReplayGain factor' , factor )
207
-
208
176
return factor
209
177
}
210
178
}
211
179
212
- function endPlayback ( audio : HTMLAudioElement , duration : number ) {
213
- async function fade ( audio : HTMLAudioElement , from : number , to : number , duration : number ) {
214
- if ( duration <= 0.0 ) {
215
- audio . volume = to
216
- return audio
217
- }
218
- const startTime = Date . now ( )
219
- const step = ( to - from ) / duration
220
- while ( audio . volume !== to ) {
221
- const elapsed = Date . now ( ) - startTime
222
- audio . volume = clamp ( 0.0 , 1.0 , from + ( elapsed * step ) )
223
- await sleep ( 10 )
224
- }
225
- return audio
180
+ function creatPipeline ( context : AudioContext , url ?: string ) {
181
+ const audio = new Audio ( url )
182
+ audio . crossOrigin = 'anonymous'
183
+ const sourceNode = context . createMediaElementSource ( audio )
184
+
185
+ const volumeNode = context . createGain ( )
186
+ const replayGainNode = context . createGain ( )
187
+
188
+ const fadeNode = context . createGain ( )
189
+ fadeNode . gain . value = 0
190
+
191
+ sourceNode
192
+ . connect ( volumeNode )
193
+ . connect ( replayGainNode )
194
+ . connect ( fadeNode )
195
+ . connect ( context . destination )
196
+
197
+ function disconnect ( ) {
198
+ audio . pause ( )
199
+ sourceNode . disconnect ( )
200
+ volumeNode . disconnect ( )
201
+ replayGainNode . disconnect ( )
202
+ fadeNode . disconnect ( )
226
203
}
227
- console . info ( `AudioController: ending payback for ${ audio } ` )
228
- audio . ontimeupdate = null
229
- audio . ondurationchange = null
230
- audio . onpause = null
231
- audio . onerror = null
232
- audio . onended = null
233
- audio . onloadedmetadata = null
204
+
205
+ return { audio, volumeNode, replayGainNode, fadeNode, disconnect }
206
+ }
207
+
208
+ function endPlayback ( context : AudioContext , pipeline : ReturnType < typeof creatPipeline > , duration : number ) {
209
+ console . info ( `AudioController: ending payback for ${ pipeline . audio } ` )
210
+ pipeline . audio . ontimeupdate = null
211
+ pipeline . audio . ondurationchange = null
212
+ pipeline . audio . onpause = null
213
+ pipeline . audio . onerror = null
214
+ pipeline . audio . onended = null
215
+ pipeline . audio . onloadedmetadata = null
216
+
217
+ duration = 3.0 // Testing
218
+ // pipeline.fadeNode.gain.cancelScheduledValues(0)
219
+ pipeline . fadeNode . gain . linearRampToValueAtTime ( 0 , context . currentTime + duration )
220
+
234
221
const startTime = Date . now ( )
235
- fade ( audio , audio . volume , 0.0 , duration )
236
- . catch ( ( err ) => console . warn ( 'Error during fade out: ' + err . stack ) )
237
- . finally ( ( ) => {
238
- audio . pause ( )
239
- console . info ( `AudioController: ending payback done. actual ${ Date . now ( ) - startTime } ms` )
240
- } )
222
+ setTimeout ( ( ) => {
223
+ console . info ( `AudioController: ending payback done. actual ${ Date . now ( ) - startTime } ms` )
224
+ pipeline . disconnect ( )
225
+ } , duration * 1000 )
241
226
}
242
227
243
228
function sleep ( ms : number ) {
244
229
return new Promise ( resolve => setTimeout ( resolve , ms ) )
245
230
}
246
-
247
- function clamp ( min : number , max : number , value : number ) {
248
- return Math . max ( min , Math . min ( value , max ) )
249
- }
0 commit comments