Make RodioSink Send and improve error handling

This commit is contained in:
johannesd3 2021-02-12 18:25:13 +01:00
parent 2f05ddfbc2
commit b2f1be4374
3 changed files with 78 additions and 43 deletions

1
Cargo.lock generated
View file

@ -1375,6 +1375,7 @@ dependencies = [
"rodio", "rodio",
"sdl2", "sdl2",
"shell-words", "shell-words",
"thiserror",
"zerocopy", "zerocopy",
] ]

View file

@ -29,19 +29,22 @@ libpulse-binding = { version = "2.13", optional = true, default-features
libpulse-simple-binding = { version = "2.13", optional = true, default-features = false } libpulse-simple-binding = { version = "2.13", optional = true, default-features = false }
jack = { version = "0.6", optional = true } jack = { version = "0.6", optional = true }
libc = { version = "0.2", optional = true } libc = { version = "0.2", optional = true }
rodio = { version = "0.13", optional = true, default-features = false }
cpal = { version = "0.13", optional = true }
sdl2 = { version = "0.34", optional = true } sdl2 = { version = "0.34", optional = true }
gstreamer = { version = "0.16", optional = true } gstreamer = { version = "0.16", optional = true }
gstreamer-app = { version = "0.16", optional = true } gstreamer-app = { version = "0.16", optional = true }
glib = { version = "0.10", optional = true } glib = { version = "0.10", optional = true }
zerocopy = { version = "0.3", optional = true } zerocopy = { version = "0.3", optional = true }
# Rodio dependencies
rodio = { version = "0.13", optional = true, default-features = false }
cpal = { version = "0.13", optional = true }
thiserror = { version = "1", optional = true }
[features] [features]
alsa-backend = ["alsa"] alsa-backend = ["alsa"]
portaudio-backend = ["portaudio-rs"] portaudio-backend = ["portaudio-rs"]
pulseaudio-backend = ["libpulse-binding", "libpulse-simple-binding"] pulseaudio-backend = ["libpulse-binding", "libpulse-simple-binding"]
jackaudio-backend = ["jack"] jackaudio-backend = ["jack"]
rodio-backend = ["rodio", "cpal"] rodio-backend = ["rodio", "cpal", "thiserror"]
sdl-backend = ["sdl2"] sdl-backend = ["sdl2"]
gstreamer-backend = ["gstreamer", "gstreamer-app", "glib", "zerocopy"] gstreamer-backend = ["gstreamer", "gstreamer-app", "glib", "zerocopy"]

View file

@ -1,20 +1,36 @@
use super::{Open, Sink};
extern crate cpal;
extern crate rodio;
use cpal::traits::{DeviceTrait, HostTrait};
use std::process::exit; use std::process::exit;
use std::{convert::Infallible, sync::mpsc};
use std::{io, thread, time}; use std::{io, thread, time};
use cpal::traits::{DeviceTrait, HostTrait};
use thiserror::Error;
use super::{Open, Sink};
#[derive(Debug, Error)]
pub enum RodioError {
#[error("Rodio: no device available")]
NoDeviceAvailable,
#[error("Rodio: device \"{0}\" is not available")]
DeviceNotAvailable(String),
#[error("Rodio play error: {0}")]
PlayError(#[from] rodio::PlayError),
#[error("Rodio stream error: {0}")]
StreamError(#[from] rodio::StreamError),
#[error("Cannot get audio devices: {0}")]
DevicesError(#[from] cpal::DevicesError),
}
pub struct RodioSink { pub struct RodioSink {
rodio_sink: rodio::Sink, rodio_sink: rodio::Sink,
// We have to keep hold of this object, or the Sink can't play...
#[allow(dead_code)] // will produce a TryRecvError on the receiver side when it is dropped.
stream: rodio::OutputStream, _close_tx: mpsc::SyncSender<Infallible>,
} }
fn list_formats(ref device: &rodio::Device) { fn list_formats(device: &rodio::Device) {
let default_fmt = match device.default_output_config() { let default_fmt = match device.default_output_config() {
Ok(fmt) => cpal::SupportedStreamConfig::from(fmt), Ok(fmt) => fmt,
Err(e) => { Err(e) => {
warn!("Error getting default rodio::Sink config: {}", e); warn!("Error getting default rodio::Sink config: {}", e);
return; return;
@ -39,8 +55,8 @@ fn list_formats(ref device: &rodio::Device) {
} }
} }
fn list_outputs() { fn list_outputs_and_exit() -> ! {
let default_device = get_default_device(); let default_device = get_default_device().unwrap();
let default_device_name = default_device.name().expect("cannot get output name"); let default_device_name = default_device.name().expect("cannot get output name");
println!("Default Audio Device:\n {}", default_device_name); println!("Default Audio Device:\n {}", default_device_name);
list_formats(&default_device); list_formats(&default_device);
@ -56,54 +72,69 @@ fn list_outputs() {
list_formats(&device); list_formats(&device);
} }
} }
exit(0)
} }
fn get_default_device() -> rodio::Device { fn get_default_device() -> Result<rodio::Device, RodioError> {
cpal::default_host() cpal::default_host()
.default_output_device() .default_output_device()
.expect("no default output device available") .ok_or(RodioError::NoDeviceAvailable)
} }
fn match_device(device: Option<String>) -> rodio::Device { fn create_sink(device: Option<String>) -> Result<(rodio::Sink, rodio::OutputStream), RodioError> {
match device { let rodio_device = match device {
Some(ask) if &ask == "?" => list_outputs_and_exit(),
Some(device_name) => { Some(device_name) => {
if device_name == "?".to_string() { cpal::default_host()
list_outputs(); .output_devices()?
exit(0) .find(|d| d.name().ok().map_or(false, |name| name == device_name)) // Ignore devices for which getting name fails
} .ok_or(RodioError::DeviceNotAvailable(device_name))?
for d in cpal::default_host()
.output_devices()
.expect("cannot get list of output devices")
{
if d.name().expect("cannot get output name") == device_name {
return d;
}
}
println!("No output sink matching '{}' found.", device_name);
exit(0)
} }
None => return get_default_device(), None => get_default_device()?,
} };
let name = rodio_device.name().ok();
info!(
"Using audio device: {}",
name.as_deref().unwrap_or("(unknown name)")
);
let (stream, handle) = rodio::OutputStream::try_from_device(&rodio_device)?;
let sink = rodio::Sink::try_new(&handle)?;
Ok((sink, stream))
} }
impl Open for RodioSink { impl Open for RodioSink {
fn open(device: Option<String>) -> RodioSink { fn open(device: Option<String>) -> RodioSink {
debug!( debug!(
"Using rodio sink with cpal host: {:?}", "Using rodio sink with cpal host: {:?}",
cpal::default_host().id() cpal::default_host().id().name()
); );
let rodio_device = match_device(device); let (sink_tx, sink_rx) = mpsc::sync_channel(1);
debug!("Using cpal device"); let (close_tx, close_rx) = mpsc::sync_channel(1);
let stream = rodio::OutputStream::try_from_device(&rodio_device)
.expect("Couldn't open output stream.");
debug!("Using rodio stream");
let sink = rodio::Sink::try_new(&stream.1).expect("Couldn't create output sink.");
debug!("Using rodio sink");
std::thread::spawn(move || match create_sink(device) {
Ok((sink, stream)) => {
sink_tx.send(Ok(sink)).unwrap();
close_rx.recv().unwrap_err(); // This will fail as soon as the sender is dropped
debug!("drop rodio::OutputStream");
drop(stream);
}
Err(e) => {
sink_tx.send(Err(e)).unwrap();
}
});
// Instead of the second `unwrap`, better error handling could be introduced
let sink = sink_rx.recv().unwrap().unwrap();
debug!("Rodio sink was created");
RodioSink { RodioSink {
rodio_sink: sink, rodio_sink: sink,
stream: stream.0, _close_tx: close_tx,
} }
} }
} }