Skip to content

Commit ae7a9f3

Browse files
torokati44Dinnerbone
authored andcommitted
web/audio: Make buffer size adaptive
1 parent b87fe2d commit ae7a9f3

File tree

1 file changed

+94
-14
lines changed

1 file changed

+94
-14
lines changed

web/src/audio.rs

Lines changed: 94 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,34 +16,51 @@ use web_sys::AudioContext;
1616
pub struct WebAudioBackend {
1717
mixer: AudioMixer,
1818
context: AudioContext,
19+
/// The current length of both buffers, in frames (pairs of left/right samples).
20+
buffer_size: Arc<RwLock<u32>>,
1921
buffers: Vec<Arc<RwLock<Buffer>>>,
22+
/// When the last submitted buffer is expected to play out completely, in seconds.
2023
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>>,
2226
log_subscriber: Arc<Layered<WASMLayer, Registry>>,
2327
}
2428

2529
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;
2745

2846
pub fn new(log_subscriber: Arc<Layered<WASMLayer, Registry>>) -> Result<Self, JsError> {
2947
let context = AudioContext::new().into_js_result()?;
3048
let sample_rate = context.sample_rate();
3149
let mut audio = Self {
3250
context,
3351
mixer: AudioMixer::new(2, sample_rate as u32),
52+
buffer_size: Arc::new(RwLock::new(Self::INITIAL_BUFFER_SIZE)),
3453
buffers: Vec::with_capacity(2),
3554
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)),
3956
log_subscriber,
4057
};
4158

4259
// Create and start the audio buffers.
4360
// These buffers ping-pong as the audio stream plays.
4461
for _ in 0..2 {
4562
let buffer = Buffer::new(&audio)?;
46-
let _ = buffer.write().expect("Cannot reenter locks").play();
63+
buffer.write().expect("Cannot reenter locks").play()?;
4764
audio.buffers.push(buffer);
4865
}
4966

@@ -68,7 +85,11 @@ impl AudioBackend for WebAudioBackend {
6885
}
6986

7087
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+
})
7293
}
7394
}
7495

@@ -81,12 +102,13 @@ impl Drop for WebAudioBackend {
81102
struct Buffer {
82103
context: AudioContext,
83104
mixer_proxy: AudioMixerProxy,
105+
buffer_size: Arc<RwLock<u32>>,
84106
audio_buffer: Vec<f32>,
85107
js_buffer: web_sys::AudioBuffer,
86108
audio_node: Option<web_sys::AudioBufferSourceNode>,
87109
on_ended_handler: Closure<dyn FnMut()>,
88110
time: Arc<RwLock<f64>>,
89-
buffer_timestep: f64,
111+
num_quick_fills: Arc<RwLock<u32>>,
90112
log_subscriber: Arc<Layered<WASMLayer, Registry>>,
91113
}
92114

@@ -96,15 +118,16 @@ impl Buffer {
96118
let buffer = Arc::new(RwLock::new(Self {
97119
context: audio.context.clone(),
98120
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],
101123
js_buffer: audio
102124
.context
103-
.create_buffer(2, WebAudioBackend::BUFFER_SIZE, sample_rate)
125+
.create_buffer(2, WebAudioBackend::INITIAL_BUFFER_SIZE, sample_rate)
104126
.into_js_result()?,
127+
audio_node: None,
105128
on_ended_handler: Closure::new(|| {}),
106129
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(),
108131
log_subscriber: audio.log_subscriber.clone(),
109132
}));
110133

@@ -124,6 +147,64 @@ impl Buffer {
124147
fn play(&mut self) -> Result<(), JsError> {
125148
let _subscriber = tracing::subscriber::set_default(self.log_subscriber.clone());
126149

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+
127208
// Mix new audio into the output buffer and copy to JS.
128209
self.mixer_proxy.mix(&mut self.audio_buffer);
129210
copy_to_audio_buffer_interleaved(&self.js_buffer, &self.audio_buffer);
@@ -137,12 +218,11 @@ impl Buffer {
137218
audio_node.set_onended(Some(self.on_ended_handler.as_ref().unchecked_ref()));
138219

139220
// 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");
141221
*time = f64::max(*time, self.context.current_time());
142222

143223
// Schedule this buffer for playback and advance the player time.
144224
audio_node.start_with_when(*time).into_js_result()?;
145-
*time += self.buffer_timestep;
225+
*time += buffer_timestep;
146226

147227
self.audio_node = Some(audio_node);
148228
Ok(())

0 commit comments

Comments
 (0)