web/audio: Require quick fills for a fixed amount of time before shrinking buffers

Instead of a fixed number of buffer fills, which changes wildly in duration.
This commit is contained in:
TÖRÖK Attila 2023-03-15 21:32:22 +01:00 committed by Nathan Adams
parent 6f9532e322
commit 63a1689c07
1 changed files with 25 additions and 20 deletions

View File

@ -21,8 +21,8 @@ pub struct WebAudioBackend {
buffers: Vec<Arc<RwLock<Buffer>>>, buffers: Vec<Arc<RwLock<Buffer>>>,
/// When the last submitted buffer is expected to play out completely, in seconds. /// When the last submitted buffer is expected to play out completely, in seconds.
time: Arc<RwLock<f64>>, time: Arc<RwLock<f64>>,
/// How many consecutive times we have filled the next buffer "at a sufficiently early time". /// For how many seconds were we able to continuously fill the next buffer "at a sufficiently early time".
num_quick_fills: Arc<RwLock<u32>>, probation_time: Arc<RwLock<f32>>,
log_subscriber: Arc<Layered<WASMLayer, Registry>>, log_subscriber: Arc<Layered<WASMLayer, Registry>>,
} }
@ -36,9 +36,9 @@ impl WebAudioBackend {
/// to account for any initialization (shape tessellation, WASM JIT, etc.) hitches. /// to account for any initialization (shape tessellation, WASM JIT, etc.) hitches.
const WARMUP_PERIOD: f32 = 2.0; const WARMUP_PERIOD: f32 = 2.0;
/// How many consecutive "quick fills" to wait before decreasing the buffer size. /// For how long we need to fill every single buffer "quickly enough" in order to decrease buffer size.
/// A higher value is more conservative. /// Measured in seconds. A higher value is more conservative.
const NUM_QUICK_FILLS_THRESHOLD: u32 = 100; const PROBATION_LENGTH: f32 = 10.0;
/// The limit of playout ratio (progress) when filling the next buffer, under which it is /// The limit of playout ratio (progress) when filling the next buffer, under which it is
/// considered "quick". Must be in 0..1, and less than `0.5 * NORMAL_PROGRESS_RANGE_MAX`. /// considered "quick". Must be in 0..1, and less than `0.5 * NORMAL_PROGRESS_RANGE_MAX`.
const NORMAL_PROGRESS_RANGE_MIN: f64 = 0.25; const NORMAL_PROGRESS_RANGE_MIN: f64 = 0.25;
@ -55,7 +55,7 @@ impl WebAudioBackend {
buffer_size: Arc::new(RwLock::new(Self::INITIAL_BUFFER_SIZE)), buffer_size: Arc::new(RwLock::new(Self::INITIAL_BUFFER_SIZE)),
buffers: Vec::with_capacity(2), buffers: Vec::with_capacity(2),
time: Arc::new(RwLock::new(0.0)), time: Arc::new(RwLock::new(0.0)),
num_quick_fills: Arc::new(RwLock::new(0u32)), probation_time: Arc::new(RwLock::new(0.0)),
log_subscriber, log_subscriber,
}; };
@ -111,7 +111,7 @@ struct Buffer {
audio_node: Option<web_sys::AudioBufferSourceNode>, audio_node: Option<web_sys::AudioBufferSourceNode>,
on_ended_handler: Closure<dyn FnMut()>, on_ended_handler: Closure<dyn FnMut()>,
time: Arc<RwLock<f64>>, time: Arc<RwLock<f64>>,
num_quick_fills: Arc<RwLock<u32>>, probation_elapsed: Arc<RwLock<f32>>,
log_subscriber: Arc<Layered<WASMLayer, Registry>>, log_subscriber: Arc<Layered<WASMLayer, Registry>>,
} }
@ -130,7 +130,7 @@ impl Buffer {
audio_node: None, audio_node: None,
on_ended_handler: Closure::new(|| {}), on_ended_handler: Closure::new(|| {}),
time: audio.time.clone(), time: audio.time.clone(),
num_quick_fills: audio.num_quick_fills.clone(), probation_elapsed: audio.probation_time.clone(),
log_subscriber: audio.log_subscriber.clone(), log_subscriber: audio.log_subscriber.clone(),
})); }));
@ -152,7 +152,10 @@ impl Buffer {
let mut time = self.time.write().expect("Cannot reenter locks"); let mut time = self.time.write().expect("Cannot reenter locks");
let mut buffer_size = self.buffer_size.write().expect("Cannot reenter locks"); let mut buffer_size = self.buffer_size.write().expect("Cannot reenter locks");
let mut num_quick_fills = self.num_quick_fills.write().expect("Cannot reenter locks"); let mut probation_elapsed = self
.probation_elapsed
.write()
.expect("Cannot reenter locks");
let time_left = *time - self.context.current_time(); let time_left = *time - self.context.current_time();
let mut buffer_timestep = f64::from(*buffer_size) / f64::from(self.context.sample_rate()); let mut buffer_timestep = f64::from(*buffer_size) / f64::from(self.context.sample_rate());
@ -170,13 +173,13 @@ impl Buffer {
if progress < WebAudioBackend::NORMAL_PROGRESS_RANGE_MIN { if progress < WebAudioBackend::NORMAL_PROGRESS_RANGE_MIN {
// This fill is considered quick, let's count it. // This fill is considered quick, let's count it.
*num_quick_fills += 1; *probation_elapsed += buffer_timestep as f32;
} else if progress < WebAudioBackend::NORMAL_PROGRESS_RANGE_MAX { } else if progress < WebAudioBackend::NORMAL_PROGRESS_RANGE_MAX {
// This fill is in the "normal" range, only resetting the "quick fill" counter. // This fill is in the "normal" range, only resetting the probation time.
*num_quick_fills = 0; *probation_elapsed = 0.0;
} else { } else {
// This fill is considered slow (maybe even too slow), increasing the buffer size. // This fill is considered slow (maybe even too slow), increasing the buffer size.
*num_quick_fills = 0; *probation_elapsed = 0.0;
if progress >= 1.0 { if progress >= 1.0 {
tracing::debug!("Audio underrun detected!"); tracing::debug!("Audio underrun detected!");
} }
@ -184,23 +187,25 @@ impl Buffer {
if *buffer_size < WebAudioBackend::MAX_BUFFER_SIZE { if *buffer_size < WebAudioBackend::MAX_BUFFER_SIZE {
*buffer_size *= 2; *buffer_size *= 2;
tracing::debug!("Increased audio buffer size to {} frames", buffer_size); tracing::debug!("Increased audio buffer size to {} frames", buffer_size);
} } else {
else {
tracing::debug!("Not increasing audio buffer size, already at max size"); tracing::debug!("Not increasing audio buffer size, already at max size");
} }
} } else {
else { tracing::debug!(
tracing::debug!("Not increasing audio buffer size, still in warmup period (at {} of {} sec)", *time, WebAudioBackend::WARMUP_PERIOD); "Not increasing audio buffer size, still in warmup period (at {} of {} sec)",
*time,
WebAudioBackend::WARMUP_PERIOD
);
} }
} }
// If enough quick fills happened, we decrease the buffer size. // If enough quick fills happened, we decrease the buffer size.
if *num_quick_fills > WebAudioBackend::NUM_QUICK_FILLS_THRESHOLD if *probation_elapsed > WebAudioBackend::PROBATION_LENGTH
&& *buffer_size > WebAudioBackend::MIN_BUFFER_SIZE && *buffer_size > WebAudioBackend::MIN_BUFFER_SIZE
{ {
*buffer_size /= 2; *buffer_size /= 2;
tracing::debug!("Decreased audio buffer size to {} frames", buffer_size); tracing::debug!("Decreased audio buffer size to {} frames", buffer_size);
*num_quick_fills = 0; *probation_elapsed = 0.0;
} }
// In case buffer_size changed above (or in the latest call in the other instance), // In case buffer_size changed above (or in the latest call in the other instance),