@@ -16,34 +16,51 @@ use web_sys::AudioContext;
16
16
pub struct WebAudioBackend {
17
17
mixer : AudioMixer ,
18
18
context : AudioContext ,
19
+ /// The current length of both buffers, in frames (pairs of left/right samples).
20
+ buffer_size : Arc < RwLock < u32 > > ,
19
21
buffers : Vec < Arc < RwLock < Buffer > > > ,
22
+ /// When the last submitted buffer is expected to play out completely, in seconds.
20
23
time : Arc < RwLock < f64 > > ,
21
- position_resolution : Duration ,
24
+ /// How many consecutive times we have filled the next buffer "at a sufficiently early time".
25
+ num_quick_fills : Arc < RwLock < u32 > > ,
22
26
log_subscriber : Arc < Layered < WASMLayer , Registry > > ,
23
27
}
24
28
25
29
impl WebAudioBackend {
26
- const BUFFER_SIZE : u32 = 4096 ;
30
+ /// These govern the adaptive buffer size algorithm, all are in number of frames (pairs of samples).
31
+ /// They must all be integer powers of 2 (due to how the algorithm works).
32
+ const INITIAL_BUFFER_SIZE : u32 = 2048 ; // 46.44 ms at 44.1 kHz
33
+ const MIN_BUFFER_SIZE : u32 = 1024 ; // 23.22 ms at 44.1 kHz
34
+ const MAX_BUFFER_SIZE : u32 = 16384 ; // 371.52 ms at 44.1 kHz
35
+
36
+ /// How many consecutive "quick fills" to wait before decreasing the buffer size.
37
+ /// A higher value is more conservative.
38
+ const NUM_QUICK_FILLS_THRESHOLD : u32 = 100 ;
39
+ /// The limit of playout ratio (progress) when filling the next buffer, under which it is
40
+ /// considered "quick". Must be in 0..1, and less than `0.5 * NORMAL_PROGRESS_RANGE_MAX`.
41
+ const NORMAL_PROGRESS_RANGE_MIN : f64 = 0.25 ;
42
+ /// The limit of playout ratio (progress) when filling the next buffer, over which buffer size
43
+ /// is increased immediately. Must be in 0..1, and greater than `2 * NORMAL_PROGRESS_RANGE_MIN`.
44
+ const NORMAL_PROGRESS_RANGE_MAX : f64 = 0.75 ;
27
45
28
46
pub fn new ( log_subscriber : Arc < Layered < WASMLayer , Registry > > ) -> Result < Self , JsError > {
29
47
let context = AudioContext :: new ( ) . into_js_result ( ) ?;
30
48
let sample_rate = context. sample_rate ( ) ;
31
49
let mut audio = Self {
32
50
context,
33
51
mixer : AudioMixer :: new ( 2 , sample_rate as u32 ) ,
52
+ buffer_size : Arc :: new ( RwLock :: new ( Self :: INITIAL_BUFFER_SIZE ) ) ,
34
53
buffers : Vec :: with_capacity ( 2 ) ,
35
54
time : Arc :: new ( RwLock :: new ( 0.0 ) ) ,
36
- position_resolution : Duration :: from_secs_f64 (
37
- f64:: from ( Self :: BUFFER_SIZE ) / f64:: from ( sample_rate) ,
38
- ) ,
55
+ num_quick_fills : Arc :: new ( RwLock :: new ( 0u32 ) ) ,
39
56
log_subscriber,
40
57
} ;
41
58
42
59
// Create and start the audio buffers.
43
60
// These buffers ping-pong as the audio stream plays.
44
61
for _ in 0 ..2 {
45
62
let buffer = Buffer :: new ( & audio) ?;
46
- let _ = buffer. write ( ) . expect ( "Cannot reenter locks" ) . play ( ) ;
63
+ buffer. write ( ) . expect ( "Cannot reenter locks" ) . play ( ) ? ;
47
64
audio. buffers . push ( buffer) ;
48
65
}
49
66
@@ -68,7 +85,11 @@ impl AudioBackend for WebAudioBackend {
68
85
}
69
86
70
87
fn position_resolution ( & self ) -> Option < Duration > {
71
- Some ( self . position_resolution )
88
+ self . buffer_size . read ( ) . map_or ( None , |bs| {
89
+ Some ( Duration :: from_secs_f64 (
90
+ f64:: from ( * bs) / f64:: from ( self . context . sample_rate ( ) ) ,
91
+ ) )
92
+ } )
72
93
}
73
94
}
74
95
@@ -81,12 +102,13 @@ impl Drop for WebAudioBackend {
81
102
struct Buffer {
82
103
context : AudioContext ,
83
104
mixer_proxy : AudioMixerProxy ,
105
+ buffer_size : Arc < RwLock < u32 > > ,
84
106
audio_buffer : Vec < f32 > ,
85
107
js_buffer : web_sys:: AudioBuffer ,
86
108
audio_node : Option < web_sys:: AudioBufferSourceNode > ,
87
109
on_ended_handler : Closure < dyn FnMut ( ) > ,
88
110
time : Arc < RwLock < f64 > > ,
89
- buffer_timestep : f64 ,
111
+ num_quick_fills : Arc < RwLock < u32 > > ,
90
112
log_subscriber : Arc < Layered < WASMLayer , Registry > > ,
91
113
}
92
114
@@ -96,15 +118,16 @@ impl Buffer {
96
118
let buffer = Arc :: new ( RwLock :: new ( Self {
97
119
context : audio. context . clone ( ) ,
98
120
mixer_proxy : audio. mixer . proxy ( ) ,
99
- audio_node : None ,
100
- audio_buffer : vec ! [ 0.0 ; 2 * WebAudioBackend :: BUFFER_SIZE as usize ] ,
121
+ buffer_size : audio . buffer_size . clone ( ) ,
122
+ audio_buffer : vec ! [ 0.0 ; 2 * WebAudioBackend :: INITIAL_BUFFER_SIZE as usize ] ,
101
123
js_buffer : audio
102
124
. context
103
- . create_buffer ( 2 , WebAudioBackend :: BUFFER_SIZE , sample_rate)
125
+ . create_buffer ( 2 , WebAudioBackend :: INITIAL_BUFFER_SIZE , sample_rate)
104
126
. into_js_result ( ) ?,
127
+ audio_node : None ,
105
128
on_ended_handler : Closure :: new ( || { } ) ,
106
129
time : audio. time . clone ( ) ,
107
- buffer_timestep : f64 :: from ( WebAudioBackend :: BUFFER_SIZE ) / f64 :: from ( sample_rate ) ,
130
+ num_quick_fills : audio . num_quick_fills . clone ( ) ,
108
131
log_subscriber : audio. log_subscriber . clone ( ) ,
109
132
} ) ) ;
110
133
@@ -124,6 +147,64 @@ impl Buffer {
124
147
fn play ( & mut self ) -> Result < ( ) , JsError > {
125
148
let _subscriber = tracing:: subscriber:: set_default ( self . log_subscriber . clone ( ) ) ;
126
149
150
+ let mut time = self . time . write ( ) . expect ( "Cannot reenter locks" ) ;
151
+ let mut buffer_size = self . buffer_size . write ( ) . expect ( "Cannot reenter locks" ) ;
152
+ let mut num_quick_fills = self . num_quick_fills . write ( ) . expect ( "Cannot reenter locks" ) ;
153
+
154
+ let time_left = * time - self . context . current_time ( ) ;
155
+ let mut buffer_timestep = f64:: from ( * buffer_size) / f64:: from ( self . context . sample_rate ( ) ) ;
156
+
157
+ // How far along the other buffer is in playing out right now:
158
+ // ~0: it has just started playing, we are well within time
159
+ // 0.25 .. 0.75: "optimal range"
160
+ // ~1: we are just barely keeping up with feeding the output
161
+ // >1: we are falling behind, audio stutters
162
+ let progress = ( buffer_timestep - time_left) / buffer_timestep;
163
+ tracing:: trace!(
164
+ "Audio buffer progress when filling the next one: {}%" ,
165
+ progress * 100.0
166
+ ) ;
167
+
168
+ if progress < WebAudioBackend :: NORMAL_PROGRESS_RANGE_MIN {
169
+ // This fill is considered quick, let's count it.
170
+ * num_quick_fills += 1 ;
171
+ } else if progress < WebAudioBackend :: NORMAL_PROGRESS_RANGE_MAX {
172
+ // This fill is in the "normal" range, only resetting the "quick fill" counter.
173
+ * num_quick_fills = 0 ;
174
+ } else {
175
+ // This fill is considered slow (maybe even too slow), increasing the buffer size.
176
+ if progress >= 1.0 {
177
+ tracing:: debug!( "Audio underrun detected!" ) ;
178
+ }
179
+ * num_quick_fills = 0 ;
180
+ if * buffer_size < WebAudioBackend :: MAX_BUFFER_SIZE {
181
+ * buffer_size *= 2 ;
182
+ tracing:: debug!( "Increased audio buffer size to {} frames" , buffer_size) ;
183
+ * num_quick_fills = 0 ;
184
+ }
185
+ }
186
+
187
+ // If enough quick fills happened, we decrease the buffer size.
188
+ if * num_quick_fills > WebAudioBackend :: NUM_QUICK_FILLS_THRESHOLD
189
+ && * buffer_size > WebAudioBackend :: MIN_BUFFER_SIZE
190
+ {
191
+ * buffer_size /= 2 ;
192
+ tracing:: debug!( "Decreased audio buffer size to {} frames" , buffer_size) ;
193
+ * num_quick_fills = 0 ;
194
+ }
195
+
196
+ // In case buffer_size changed above (or in the latest call in the other instance),
197
+ // we need to recaulculate/recreate/resize a couple of things that depend on it.
198
+ if self . js_buffer . length ( ) != * buffer_size {
199
+ tracing:: trace!( "Recreating JS side buffer with new length" ) ;
200
+ buffer_timestep = f64:: from ( * buffer_size) / f64:: from ( self . context . sample_rate ( ) ) ;
201
+ self . js_buffer = self
202
+ . context
203
+ . create_buffer ( 2 , * buffer_size, self . context . sample_rate ( ) )
204
+ . into_js_result ( ) ?;
205
+ self . audio_buffer . resize ( 2 * * buffer_size as usize , 0.0 ) ;
206
+ }
207
+
127
208
// Mix new audio into the output buffer and copy to JS.
128
209
self . mixer_proxy . mix ( & mut self . audio_buffer ) ;
129
210
copy_to_audio_buffer_interleaved ( & self . js_buffer , & self . audio_buffer ) ;
@@ -137,12 +218,11 @@ impl Buffer {
137
218
audio_node. set_onended ( Some ( self . on_ended_handler . as_ref ( ) . unchecked_ref ( ) ) ) ;
138
219
139
220
// Sanity: ensure our player time is not in the past. This can happen due to underruns.
140
- let mut time = self . time . write ( ) . expect ( "Cannot reenter locks" ) ;
141
221
* time = f64:: max ( * time, self . context . current_time ( ) ) ;
142
222
143
223
// Schedule this buffer for playback and advance the player time.
144
224
audio_node. start_with_when ( * time) . into_js_result ( ) ?;
145
- * time += self . buffer_timestep ;
225
+ * time += buffer_timestep;
146
226
147
227
self . audio_node = Some ( audio_node) ;
148
228
Ok ( ( ) )
0 commit comments