use std::collections::VecDeque;
use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU32, Ordering};
#[cfg(feature = "aec")]
use std::sync::mpsc::TrySendError;
use std::sync::mpsc::{Receiver, SyncSender, sync_channel};
use std::sync::{Arc, Mutex, OnceLock};
use std::thread::Thread;
use std::time::{Duration, Instant};
use cpal::traits::{DeviceTrait, StreamTrait};
use rand::RngExt;
use super::mixer::{self, Mixer};
use super::sink::{Registration, Sink};
use crate::Error;
const RETRY_MIN: Duration = Duration::from_millis(500);
const RETRY_MAX: Duration = Duration::from_secs(4);
const UNDERRUN_LIMIT: u32 = 20;
const ERROR_LIMIT: u32 = 3;
const ERROR_WINDOW: Duration = Duration::from_secs(5);
const DRIVER_QUEUE: usize = 16;
const COMMAND_QUEUE: usize = 2 * mixer::MAX_SINKS;
const SYNC_ATTEMPTS: u32 = 8;
const SYNC_DELAY: Duration = Duration::from_millis(4);
const SCRATCH_FRAMES: usize = 2048;
#[derive(Default)]
pub(crate) struct Shared {
state: Mutex<State>,
}
#[derive(Default)]
struct State {
rate: u32,
mixer: Option<SyncSender<mixer::Command>>,
sinks: Vec<Registration>,
detaching: Vec<u64>,
next_id: u64,
#[cfg(feature = "aec")]
reference: Option<crate::aec::Reference>,
#[cfg(feature = "aec")]
detaching_reference: bool,
#[cfg(feature = "aec")]
waker: Option<Commands>,
}
impl Shared {
pub(super) fn add<F>(&self, build: F) -> Result<Sink, Error>
where
F: FnOnce(u64, u32) -> Result<(Sink, Registration), Error>,
{
let mut state = self.state.lock().unwrap();
if state.sinks.len() >= mixer::MAX_SINKS {
return Err(Error::Unsupported(format!(
"at most {} playback sinks per device",
mixer::MAX_SINKS
)));
}
let rate = if state.rate == 0 { 48_000 } else { state.rate };
let (sink, mut registration) = build(state.next_id, rate)?;
state.next_id += 1;
if let Some(mixer) = &state.mixer {
registration.attach(mixer);
}
state.sinks.push(registration);
Ok(sink)
}
pub(super) fn remove(&self, id: u64) {
let mut state = self.state.lock().unwrap();
state.sinks.retain(|s| s.id != id);
let Some(mixer) = &state.mixer else { return };
if mixer.try_send(mixer::Command::Remove { id }).is_err() {
state.detaching.push(id);
}
}
#[cfg(feature = "aec")]
pub(super) fn wake_with(&self, waker: Commands) {
self.state.lock().unwrap().waker = Some(waker);
}
#[cfg(feature = "aec")]
fn wake(state: &State) {
if let Some(waker) = &state.waker {
waker.sync();
}
}
#[cfg(feature = "aec")]
pub(crate) fn set_reference(&self, mut reference: crate::aec::Reference) {
let mut state = self.state.lock().unwrap();
if let Some(mixer) = &state.mixer
&& state.rate != 0
&& reference.rebuild(state.rate)
{
attach_reference(&mut reference, mixer);
}
state.detaching_reference = false;
state.reference = Some(reference);
Self::wake(&state);
}
#[cfg(feature = "aec")]
pub(crate) fn clear_reference(&self, id: u64) {
let mut state = self.state.lock().unwrap();
if !state.reference.as_ref().is_some_and(|r| r.owned_by(id)) {
return;
}
state.reference = None;
let Some(mixer) = &state.mixer else { return };
if mixer.try_send(mixer::Command::Reference(None)).is_err() {
state.detaching_reference = true;
}
Self::wake(&state);
}
pub(super) fn sync(&self) -> bool {
let mut state = self.state.lock().unwrap();
let Some(mixer) = state.mixer.clone() else {
state.detaching.clear();
#[cfg(feature = "aec")]
{
state.detaching_reference = false;
}
return true;
};
state
.detaching
.retain(|id| mixer.try_send(mixer::Command::Remove { id: *id }).is_err());
for sink in &mut state.sinks {
sink.attach(&mixer);
}
#[cfg(feature = "aec")]
{
if state.detaching_reference {
state.detaching_reference = mixer.try_send(mixer::Command::Reference(None)).is_err();
}
if let Some(reference) = &mut state.reference {
attach_reference(reference, &mixer);
}
}
let done = state.detaching.is_empty() && state.sinks.iter().all(|s| s.attached());
#[cfg(feature = "aec")]
let done = done && !state.detaching_reference && state.reference.as_ref().is_none_or(|r| r.attached());
done
}
fn rebind(&self, rate: u32, mixer: SyncSender<mixer::Command>) {
let mut state = self.state.lock().unwrap();
for sink in &mut state.sinks {
sink.rebuild(rate);
sink.attach(&mixer);
}
#[cfg(feature = "aec")]
if let Some(reference) = &mut state.reference {
if reference.rebuild(rate) {
attach_reference(reference, &mixer);
} else {
state.reference = None;
}
}
state.rate = rate;
state.mixer = Some(mixer);
state.detaching.clear();
#[cfg(feature = "aec")]
{
state.detaching_reference = false;
}
}
fn unbind(&self) {
self.state.lock().unwrap().mixer = None;
}
#[cfg(all(test, feature = "aec"))]
pub(crate) fn has_reference(&self) -> bool {
self.state.lock().unwrap().reference.is_some()
}
}
#[cfg(feature = "aec")]
fn attach_reference(reference: &mut crate::aec::Reference, mixer: &SyncSender<mixer::Command>) {
let Some(prod) = reference.take() else { return };
if let Err(err) = mixer.try_send(mixer::Command::Reference(Some(prod))) {
let (TrySendError::Full(rejected) | TrySendError::Disconnected(rejected)) = err;
if let mixer::Command::Reference(Some(prod)) = rejected {
reference.restore(prod);
}
}
}
enum Work {
Switch {
device: Option<String>,
reply: tokio::sync::oneshot::Sender<Result<(), Error>>,
},
Failed,
Sync,
Retry,
Shutdown,
}
struct Switch {
device: Option<String>,
reply: tokio::sync::oneshot::Sender<Result<(), Error>>,
}
#[derive(Default)]
struct Mailbox {
switches: Mutex<Switches>,
signals: Signals,
driver: OnceLock<Thread>,
}
#[derive(Default)]
struct Switches {
waiting: VecDeque<Switch>,
closed: bool,
}
#[derive(Default)]
struct Signals {
failed: AtomicBool,
sync: AtomicBool,
shutdown: AtomicBool,
}
#[derive(Clone, Default)]
pub(super) struct Commands {
mailbox: Arc<Mailbox>,
}
impl Commands {
pub(super) fn switch(
&self,
device: Option<String>,
reply: tokio::sync::oneshot::Sender<Result<(), Error>>,
) -> Result<(), Error> {
let mut switches = self.mailbox.switches.lock().unwrap();
if switches.closed {
return Err(Error::Playback("the playback thread stopped".into()));
} else if switches.waiting.len() >= DRIVER_QUEUE {
return Err(Error::Playback("the playback thread is busy".into()));
}
switches.waiting.push_back(Switch { device, reply });
drop(switches);
self.mailbox.wake();
Ok(())
}
pub(super) fn sync(&self) {
self.mailbox.signals.sync.store(true, Ordering::Release);
self.mailbox.wake();
}
pub(super) fn shutdown(&self) {
self.mailbox.signals.shutdown.store(true, Ordering::Release);
let mut switches = self.mailbox.switches.lock().unwrap();
switches.closed = true;
switches.waiting.clear();
drop(switches);
self.mailbox.wake();
}
fn failed(&self) {
self.mailbox.signals.failed.store(true, Ordering::Release);
self.mailbox.wake();
}
}
impl Mailbox {
fn wake(&self) {
if let Some(driver) = self.driver.get() {
driver.unpark();
}
}
}
pub(super) struct Requests {
mailbox: Arc<Mailbox>,
}
impl Requests {
fn attach(&self) {
let _ = self.mailbox.driver.set(std::thread::current());
}
fn poll(&self, deadline: Option<Instant>) -> Option<Work> {
let signals = &self.mailbox.signals;
if signals.shutdown.load(Ordering::Acquire) {
return Some(Work::Shutdown);
}
if let Some(Switch { device, reply }) = self.mailbox.switches.lock().unwrap().waiting.pop_front() {
return Some(Work::Switch { device, reply });
}
if deadline.is_some_and(|at| Instant::now() >= at) {
return Some(Work::Retry);
}
if signals.failed.swap(false, Ordering::AcqRel) {
return Some(Work::Failed);
}
if signals.sync.swap(false, Ordering::AcqRel) {
return Some(Work::Sync);
}
None
}
fn wait(&self, deadline: Option<Instant>) -> Work {
loop {
if let Some(work) = self.poll(deadline) {
return work;
}
match deadline {
Some(at) => std::thread::park_timeout(at.saturating_duration_since(Instant::now())),
None => std::thread::park(),
}
}
}
}
impl Drop for Requests {
fn drop(&mut self) {
let mut switches = self.mailbox.switches.lock().unwrap_or_else(|err| err.into_inner());
switches.closed = true;
switches.waiting.clear();
}
}
pub(super) fn channel() -> (Commands, Requests) {
let mailbox = Arc::new(Mailbox::default());
(
Commands {
mailbox: mailbox.clone(),
},
Requests { mailbox },
)
}
const UNCLASSIFIED: [cpal::ErrorKind; 9] = [
cpal::ErrorKind::DeviceBusy,
cpal::ErrorKind::HostUnavailable,
cpal::ErrorKind::InvalidInput,
cpal::ErrorKind::PermissionDenied,
cpal::ErrorKind::ResourceExhausted,
cpal::ErrorKind::UnsupportedConfig,
cpal::ErrorKind::UnsupportedOperation,
cpal::ErrorKind::BackendError,
cpal::ErrorKind::Other,
];
fn code(kind: cpal::ErrorKind) -> u8 {
UNCLASSIFIED.iter().position(|k| *k == kind).map_or(0, |i| i as u8 + 1)
}
fn named(code: u8) -> Option<cpal::ErrorKind> {
UNCLASSIFIED.get(usize::from(code.checked_sub(1)?)).copied()
}
#[derive(Default)]
struct Failures {
unavailable: AtomicBool,
invalidated: AtomicBool,
changed: AtomicBool,
realtime_denied: AtomicBool,
xruns: AtomicU32,
unclassified: AtomicU32,
last: AtomicU8,
}
impl Failures {
fn record(&self, kind: cpal::ErrorKind) {
match kind {
cpal::ErrorKind::DeviceNotAvailable => self.unavailable.store(true, Ordering::Release),
cpal::ErrorKind::StreamInvalidated => self.invalidated.store(true, Ordering::Release),
cpal::ErrorKind::DeviceChanged => self.changed.store(true, Ordering::Release),
cpal::ErrorKind::RealtimeDenied => self.realtime_denied.store(true, Ordering::Release),
cpal::ErrorKind::Xrun => {
self.xruns.fetch_add(1, Ordering::AcqRel);
}
_ => {
self.unclassified.fetch_add(1, Ordering::AcqRel);
self.last.store(code(kind), Ordering::Release);
}
}
}
fn take(&self) -> FailureBatch {
FailureBatch {
unavailable: self.unavailable.swap(false, Ordering::AcqRel),
invalidated: self.invalidated.swap(false, Ordering::AcqRel),
changed: self.changed.swap(false, Ordering::AcqRel),
realtime_denied: self.realtime_denied.swap(false, Ordering::AcqRel),
xruns: self.xruns.swap(0, Ordering::AcqRel).min(UNDERRUN_LIMIT + 1),
unclassified: self.unclassified.swap(0, Ordering::AcqRel).min(ERROR_LIMIT),
last: named(self.last.swap(0, Ordering::AcqRel)),
}
}
}
struct FailureBatch {
unavailable: bool,
invalidated: bool,
changed: bool,
realtime_denied: bool,
xruns: u32,
unclassified: u32,
last: Option<cpal::ErrorKind>,
}
struct FailureReporter {
failures: Arc<Failures>,
commands: Commands,
}
impl FailureReporter {
fn report(&self, error: &cpal::Error) {
self.failures.record(error.kind());
self.commands.failed();
}
}
pub(super) fn run(
requests: Requests,
commands: Commands,
shared: Arc<Shared>,
device: Option<String>,
opened: tokio::sync::oneshot::Sender<Result<(), Error>>,
) {
let mut driver = Driver {
shared,
commands,
device,
stream: None,
retired: None,
failures: None,
retry: RETRY_MIN,
retry_at: None,
underruns: 0,
unclassified: 0,
window: Instant::now(),
};
requests.attach();
let first = driver.start();
let started = first.is_ok();
if opened.send(first).is_err() || !started {
return;
}
loop {
match requests.wait(driver.retry_at) {
Work::Switch { device, reply } => {
driver.device = device;
let _ = reply.send(driver.restart());
}
Work::Failed => {
if driver.should_restart() {
let _ = driver.restart();
}
}
Work::Sync => driver.sync(),
Work::Retry => {
if driver.restart().is_ok() {
tracing::info!("audio output recovered");
}
}
Work::Shutdown => break,
}
}
}
struct Driver {
shared: Arc<Shared>,
commands: Commands,
device: Option<String>,
stream: Option<cpal::Stream>,
retired: Option<Receiver<mixer::Retired>>,
failures: Option<Arc<Failures>>,
retry: Duration,
retry_at: Option<Instant>,
underruns: u32,
unclassified: u32,
window: Instant,
}
impl Driver {
fn start(&mut self) -> Result<(), Error> {
let device = super::device::open(self.device.as_deref())?;
let supported = super::device::negotiate(&device)?;
let format = supported.sample_format();
let config: cpal::StreamConfig = supported.into();
let rate = config.sample_rate;
let channels = config.channels as usize;
if rate == 0 || channels == 0 {
return Err(Error::Playback(format!(
"output device negotiated an empty format ({rate} Hz, {channels} channels)"
)));
}
let (tx, rx) = sync_channel(COMMAND_QUEUE);
let (retired_tx, retired_rx) = sync_channel(COMMAND_QUEUE);
let mixer = Mixer::new(rx, retired_tx, rate, channels);
let failures = Arc::new(Failures::default());
let reporter = FailureReporter {
failures: failures.clone(),
commands: self.commands.clone(),
};
let stream = self.build(&device, config, format, mixer, reporter)?;
stream
.play()
.map_err(|err| Error::Playback(format!("cannot start output stream: {err}")))?;
self.shared.rebind(rate, tx);
self.stream = Some(stream);
self.failures = Some(failures);
self.retired = Some(retired_rx);
self.retry = RETRY_MIN;
tracing::info!(rate, channels, ?format, "opened audio output");
Ok(())
}
fn build(
&self,
device: &cpal::Device,
config: cpal::StreamConfig,
format: cpal::SampleFormat,
mixer: Mixer,
failures: FailureReporter,
) -> Result<cpal::Stream, Error> {
match format {
cpal::SampleFormat::F32 => self.build_as::<f32>(device, config, mixer, failures),
cpal::SampleFormat::I16 => self.build_as::<i16>(device, config, mixer, failures),
cpal::SampleFormat::U16 => self.build_as::<u16>(device, config, mixer, failures),
cpal::SampleFormat::I32 => self.build_as::<i32>(device, config, mixer, failures),
other => Err(Error::Unsupported(format!("output sample format {other:?}"))),
}
}
fn build_as<T>(
&self,
device: &cpal::Device,
config: cpal::StreamConfig,
mut mixer: Mixer,
failures: FailureReporter,
) -> Result<cpal::Stream, Error>
where
T: cpal::SizedSample + cpal::FromSample<f32>,
{
let mut scratch = vec![0.0f32; SCRATCH_FRAMES * config.channels as usize];
device
.build_output_stream::<T, _, _>(
config,
move |data, _| {
for chunk in data.chunks_mut(scratch.len()) {
let scratch = &mut scratch[..chunk.len()];
mixer.fill(scratch);
for (out, sample) in chunk.iter_mut().zip(scratch.iter()) {
*out = T::from_sample(*sample);
}
}
},
move |error| {
failures.report(&error);
},
None,
)
.map_err(|err| Error::Playback(format!("cannot open output stream: {err}")))
}
fn restart(&mut self) -> Result<(), Error> {
self.stop();
let result = self.start();
self.retry_at = match &result {
Ok(()) => None,
Err(err) => {
tracing::debug!(%err, "audio output unavailable");
Some(self.schedule())
}
};
result
}
fn stop(&mut self) {
self.shared.unbind();
self.failures = None;
self.stream = None;
self.retired = None;
}
fn sync(&mut self) {
if let Some(retired) = &self.retired {
while retired.try_recv().is_ok() {}
}
for _ in 0..SYNC_ATTEMPTS {
if self.shared.sync() {
return;
}
std::thread::sleep(SYNC_DELAY);
}
tracing::warn!("audio output is not keeping up with sink changes");
}
fn schedule(&mut self) -> Instant {
let wait = self.retry.mul_f64(0.5 + rand::rng().random::<f64>() / 2.0);
self.retry = (self.retry * 2).min(RETRY_MAX);
Instant::now() + wait
}
fn should_restart(&mut self) -> bool {
let Some(failures) = &self.failures else { return false };
let failures = failures.take();
self.roll_window();
self.underruns = self.underruns.saturating_add(failures.xruns);
self.unclassified = self.unclassified.saturating_add(failures.unclassified);
if failures.changed || failures.realtime_denied {
tracing::debug!("audio output changed underneath us");
}
if let Some(kind) = failures.last {
tracing::warn!(%kind, count = failures.unclassified, "audio output error");
}
if failures.unavailable || failures.invalidated {
tracing::warn!("audio output lost");
return true;
}
if self.underruns > UNDERRUN_LIMIT {
self.underruns = 0;
tracing::warn!("restarting audio output after repeated underruns");
return true;
}
if self.unclassified >= ERROR_LIMIT {
self.unclassified = 0;
tracing::warn!("restarting audio output after repeated unclassified errors");
return true;
}
false
}
fn roll_window(&mut self) {
if self.window.elapsed() > ERROR_WINDOW {
self.underruns = 0;
self.unclassified = 0;
self.window = Instant::now();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::playback::sink::{self, Input};
struct Wired {
shared: Arc<Shared>,
handle: Arc<super::super::Handle>,
mixer: Receiver<mixer::Command>,
driver: Requests,
}
fn wired(depth: usize) -> Wired {
let shared = Arc::new(Shared::default());
let (commands, driver) = channel();
let handle = Arc::new(super::super::Handle { commands });
let (tx, mixer) = sync_channel(depth);
shared.rebind(48_000, tx);
Wired {
shared,
handle,
mixer,
driver,
}
}
fn add(shared: &Arc<Shared>, handle: &Arc<super::super::Handle>) -> Result<Sink, Error> {
shared.add(|id, rate| sink::new(id, rate, Input::default(), shared.clone(), handle.clone()))
}
const PATIENCE: Duration = Duration::from_secs(5);
fn queue_switch(commands: &Commands) -> tokio::sync::oneshot::Receiver<Result<(), Error>> {
let (reply, response) = tokio::sync::oneshot::channel();
commands.switch(None, reply).unwrap();
response
}
#[test]
fn registrations_survive_a_full_mixer_queue() {
let depth = 4;
let w = wired(depth);
let sinks: Vec<_> = (0..depth + 1).map(|_| add(&w.shared, &w.handle).unwrap()).collect();
assert_eq!(sinks.len(), depth + 1);
assert!(!w.shared.sync(), "expected a sink to be waiting on the queue");
while w.mixer.try_recv().is_ok() {}
assert!(w.shared.sync(), "the waiting sink was never re-sent");
let state = w.shared.state.lock().unwrap();
assert!(state.sinks.iter().all(|s| s.attached()), "a sink is still unattached");
}
#[test]
fn adding_and_dropping_a_sink_wakes_the_driver() {
let w = wired(8);
let engine = super::super::Engine {
shared: w.shared.clone(),
handle: w.handle.clone(),
};
let sink = engine.sink(Input::default()).unwrap();
assert!(
matches!(w.driver.poll(None), Some(Work::Sync)),
"adding a sink did not wake the driver"
);
drop(sink);
assert!(
matches!(w.driver.poll(None), Some(Work::Sync)),
"dropping a sink did not wake the driver"
);
}
#[test]
fn removals_survive_a_full_mixer_queue() {
let w = wired(1);
let sink = add(&w.shared, &w.handle).unwrap();
let id = w.shared.state.lock().unwrap().sinks[0].id;
drop(sink);
assert_eq!(w.shared.state.lock().unwrap().detaching, vec![id]);
while w.mixer.try_recv().is_ok() {}
assert!(w.shared.sync());
assert!(
w.shared.state.lock().unwrap().detaching.is_empty(),
"the removal was lost"
);
}
#[test]
fn refuses_more_sinks_than_the_mixer_can_hold() {
let w = wired(4 * mixer::MAX_SINKS);
let sinks: Vec<_> = (0..mixer::MAX_SINKS)
.map(|_| add(&w.shared, &w.handle).unwrap())
.collect();
assert!(matches!(add(&w.shared, &w.handle), Err(Error::Unsupported(_))));
drop(sinks.into_iter().next_back());
add(&w.shared, &w.handle).expect("a slot freed by the dropped sink");
}
#[test]
fn notification_floods_are_coalesced() {
let (commands, requests) = channel();
let failures = Arc::new(Failures::default());
let reporter = FailureReporter {
failures: failures.clone(),
commands: commands.clone(),
};
for _ in 0..1_000 {
commands.sync();
for kind in [
cpal::ErrorKind::DeviceNotAvailable,
cpal::ErrorKind::StreamInvalidated,
cpal::ErrorKind::DeviceChanged,
cpal::ErrorKind::RealtimeDenied,
cpal::ErrorKind::Xrun,
cpal::ErrorKind::BackendError,
] {
reporter.report(&cpal::Error::new(kind));
}
}
assert!(requests.mailbox.switches.lock().unwrap().waiting.is_empty());
assert!(matches!(requests.poll(None), Some(Work::Failed)));
assert!(matches!(requests.poll(None), Some(Work::Sync)));
assert!(requests.poll(None).is_none(), "a flood outlived its coalesced wakes");
let failures = failures.take();
assert!(failures.unavailable);
assert!(failures.invalidated);
assert!(failures.changed);
assert!(failures.realtime_denied);
assert_eq!(failures.xruns, UNDERRUN_LIMIT + 1);
assert_eq!(failures.unclassified, ERROR_LIMIT);
assert_eq!(failures.last, Some(cpal::ErrorKind::BackendError));
}
#[test]
fn reporting_a_failure_never_waits_on_the_mailbox() {
let (commands, requests) = channel();
let failures = Arc::new(Failures::default());
let reporter = FailureReporter {
failures: failures.clone(),
commands: commands.clone(),
};
let held = requests.mailbox.switches.lock().unwrap();
reporter.report(&cpal::Error::new(cpal::ErrorKind::DeviceNotAvailable));
commands.sync();
drop(held);
assert!(failures.take().unavailable, "the failure never landed");
assert!(requests.mailbox.signals.sync.load(Ordering::Acquire));
}
#[test]
fn switches_complete_or_reject_overload() {
let (commands, requests) = channel();
let responses: Vec<_> = (0..DRIVER_QUEUE).map(|_| queue_switch(&commands)).collect();
let (reply, _response) = tokio::sync::oneshot::channel();
let error = commands.switch(None, reply).unwrap_err();
assert!(matches!(error, Error::Playback(message) if message.contains("busy")));
for response in responses {
let Some(Work::Switch { reply, .. }) = requests.poll(None) else {
panic!("a switch went missing");
};
reply.send(Ok(())).unwrap();
assert!(matches!(response.blocking_recv(), Ok(Ok(()))));
}
}
#[test]
fn sync_survives_a_saturated_driver() {
let (commands, requests) = channel();
let _responses: Vec<_> = (0..DRIVER_QUEUE).map(|_| queue_switch(&commands)).collect();
commands.sync();
for _ in 0..DRIVER_QUEUE {
assert!(matches!(requests.poll(None), Some(Work::Switch { .. })));
}
assert!(
matches!(requests.poll(None), Some(Work::Sync)),
"saturation lost a sync"
);
}
#[test]
fn final_handle_shuts_down_a_saturated_driver() {
let (commands, requests) = channel();
let handle = Arc::new(super::super::Handle {
commands: commands.clone(),
});
let responses: Vec<_> = (0..DRIVER_QUEUE).map(|_| queue_switch(&commands)).collect();
drop(handle);
assert!(
matches!(requests.poll(None), Some(Work::Shutdown)),
"saturation lost shutdown"
);
for response in responses {
assert!(response.blocking_recv().is_err());
}
let (reply, _response) = tokio::sync::oneshot::channel();
let error = commands.switch(None, reply).unwrap_err();
assert!(matches!(error, Error::Playback(message) if message.contains("stopped")));
}
#[test]
fn a_parked_driver_is_woken() {
let (commands, requests) = channel();
let (sent, arrived) = std::sync::mpsc::channel();
let waker = commands.clone();
let driver = std::thread::spawn(move || {
requests.attach();
let woken = matches!(requests.wait(Some(Instant::now() + PATIENCE)), Work::Sync);
sent.send(()).unwrap();
let deadline = Instant::now() + PATIENCE;
let stopped = loop {
match requests.wait(Some(deadline)) {
Work::Shutdown => break true,
Work::Retry => break false,
_ => continue,
}
};
(woken, stopped)
});
std::thread::sleep(Duration::from_millis(10));
std::thread::scope(|s| {
for _ in 0..8 {
s.spawn(|| waker.sync());
}
});
arrived.recv_timeout(PATIENCE).expect("a sync never woke the driver");
commands.shutdown();
let (woken, stopped) = driver.join().unwrap();
assert!(woken, "a sync never woke the driver");
assert!(stopped, "a shutdown never woke the driver");
}
#[test]
fn a_signal_racing_startup_is_not_slept_through() {
let (commands, requests) = channel();
commands.sync();
let driver = std::thread::spawn(move || {
requests.attach();
requests.wait(Some(Instant::now() + PATIENCE))
});
assert!(
matches!(driver.join().unwrap(), Work::Sync),
"a signal raised before attach was slept through"
);
}
#[test]
fn a_due_retry_outranks_reasserted_signals() {
let (commands, requests) = channel();
let due = Instant::now();
for _ in 0..8 {
commands.sync();
assert!(
matches!(requests.wait(Some(due)), Work::Retry),
"steady sink churn starved the device retry"
);
}
}
#[test]
fn an_early_wake_does_not_fake_a_retry() {
let (commands, requests) = channel();
let waker = commands.clone();
let driver = std::thread::spawn(move || {
requests.attach();
requests.wait(Some(Instant::now() + PATIENCE))
});
std::thread::sleep(Duration::from_millis(10));
waker.sync();
assert!(
matches!(driver.join().unwrap(), Work::Sync),
"an early wake was reported as a retry"
);
}
#[test]
fn losing_the_driver_releases_switch_callers() {
let (commands, requests) = channel();
let queued = queue_switch(&commands);
drop(requests);
assert!(queued.blocking_recv().is_err(), "a queued switch outlived its driver");
let (reply, _response) = tokio::sync::oneshot::channel();
let error = commands.switch(None, reply).unwrap_err();
assert!(matches!(error, Error::Playback(message) if message.contains("stopped")));
}
#[test]
fn a_fatal_batch_keeps_the_counts_that_preceded_it() {
let (commands, _requests) = channel();
let failures = Arc::new(Failures::default());
let mut driver = Driver {
shared: Arc::new(Shared::default()),
commands,
device: None,
stream: None,
retired: None,
failures: Some(failures.clone()),
retry: RETRY_MIN,
retry_at: None,
underruns: 0,
unclassified: 0,
window: Instant::now(),
};
failures.record(cpal::ErrorKind::BackendError);
failures.record(cpal::ErrorKind::BackendError);
failures.record(cpal::ErrorKind::DeviceNotAvailable);
assert!(driver.should_restart(), "a lost device did not restart the stream");
let replacement = Arc::new(Failures::default());
driver.failures = Some(replacement.clone());
replacement.record(cpal::ErrorKind::BackendError);
assert!(
driver.should_restart(),
"the fatal restart threw away the errors before it"
);
}
#[test]
fn ignores_errors_from_a_replaced_stream() {
let w = wired(8);
let (commands, _requests) = channel();
let stale = Arc::new(Failures::default());
let current = Arc::new(Failures::default());
let stale_reporter = FailureReporter {
failures: stale,
commands: commands.clone(),
};
let mut driver = Driver {
shared: w.shared,
commands: commands.clone(),
device: None,
stream: None,
retired: None,
failures: Some(current.clone()),
retry: RETRY_MIN,
retry_at: None,
underruns: 0,
unclassified: 0,
window: Instant::now(),
};
let lost = cpal::Error::new(cpal::ErrorKind::DeviceNotAvailable);
stale_reporter.report(&lost);
assert!(!driver.should_restart(), "acted on a retired stream's error");
current.record(lost.kind());
assert!(driver.should_restart(), "ignored the live stream's error");
}
}