mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-28 17:21:52 +00:00
5435ab3270
fe37186
added the restriction that `Sink`s must be `Send`. It turned
out later that this restrictions was unnecessary, and since some
`Sink`s aren't `Send` yet, this restriction is lifted again.
librespot-org/librespot#601 refactored the `RodioSink` in order to make
it `Send`. These changes are partly reverted in favour of the initial
simpler design.
Furthermore, there were some compile errors in the gstreamer backend
which are hereby fixed.
189 lines
5.5 KiB
Rust
189 lines
5.5 KiB
Rust
use std::process::exit;
|
|
use std::{io, thread, time};
|
|
|
|
use cpal::traits::{DeviceTrait, HostTrait};
|
|
use thiserror::Error;
|
|
|
|
use super::Sink;
|
|
use crate::audio::AudioPacket;
|
|
|
|
#[cfg(all(
|
|
feature = "rodiojack-backend",
|
|
not(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"))
|
|
))]
|
|
compile_error!("Rodio JACK backend is currently only supported on linux.");
|
|
|
|
#[cfg(feature = "rodio-backend")]
|
|
pub fn mk_rodio(device: Option<String>) -> Box<dyn Sink> {
|
|
Box::new(open(cpal::default_host(), device))
|
|
}
|
|
|
|
#[cfg(feature = "rodiojack-backend")]
|
|
pub fn mk_rodiojack(device: Option<String>) -> Box<dyn Sink> {
|
|
Box::new(open(
|
|
cpal::host_from_id(cpal::HostId::Jack).unwrap(),
|
|
device,
|
|
))
|
|
}
|
|
|
|
#[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 {
|
|
rodio_sink: rodio::Sink,
|
|
_stream: rodio::OutputStream,
|
|
}
|
|
|
|
fn list_formats(device: &rodio::Device) {
|
|
match device.default_output_config() {
|
|
Ok(cfg) => {
|
|
debug!(" Default config:");
|
|
debug!(" {:?}", cfg);
|
|
}
|
|
Err(e) => {
|
|
// Use loglevel debug, since even the output is only debug
|
|
debug!("Error getting default rodio::Sink config: {}", e);
|
|
}
|
|
};
|
|
|
|
match device.supported_output_configs() {
|
|
Ok(mut cfgs) => {
|
|
if let Some(first) = cfgs.next() {
|
|
debug!(" Available configs:");
|
|
debug!(" {:?}", first);
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
for cfg in cfgs {
|
|
debug!(" {:?}", cfg);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
debug!("Error getting supported rodio::Sink configs: {}", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn list_outputs(host: &cpal::Host) -> Result<(), cpal::DevicesError> {
|
|
let mut default_device_name = None;
|
|
|
|
if let Some(default_device) = host.default_output_device() {
|
|
default_device_name = default_device.name().ok();
|
|
println!(
|
|
"Default Audio Device:\n {}",
|
|
default_device_name.as_deref().unwrap_or("[unknown name]")
|
|
);
|
|
|
|
list_formats(&default_device);
|
|
|
|
println!("Other Available Audio Devices:");
|
|
} else {
|
|
warn!("No default device was found");
|
|
}
|
|
|
|
for device in host.output_devices()? {
|
|
match device.name() {
|
|
Ok(name) if Some(&name) == default_device_name.as_ref() => (),
|
|
Ok(name) => {
|
|
println!(" {}", name);
|
|
list_formats(&device);
|
|
}
|
|
Err(e) => {
|
|
warn!("Cannot get device name: {}", e);
|
|
println!(" [unknown name]");
|
|
list_formats(&device);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn create_sink(
|
|
host: &cpal::Host,
|
|
device: Option<String>,
|
|
) -> Result<(rodio::Sink, rodio::OutputStream), RodioError> {
|
|
let rodio_device = match device {
|
|
Some(ask) if &ask == "?" => {
|
|
let exit_code = match list_outputs(host) {
|
|
Ok(()) => 0,
|
|
Err(e) => {
|
|
error!("{}", e);
|
|
1
|
|
}
|
|
};
|
|
exit(exit_code)
|
|
}
|
|
Some(device_name) => {
|
|
host.output_devices()?
|
|
.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))?
|
|
}
|
|
None => host
|
|
.default_output_device()
|
|
.ok_or(RodioError::NoDeviceAvailable)?,
|
|
};
|
|
|
|
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))
|
|
}
|
|
|
|
pub fn open(host: cpal::Host, device: Option<String>) -> RodioSink {
|
|
debug!("Using rodio sink with cpal host: {}", host.id().name());
|
|
|
|
let (sink, stream) = create_sink(&host, device).unwrap();
|
|
|
|
debug!("Rodio sink was created");
|
|
RodioSink {
|
|
rodio_sink: sink,
|
|
_stream: stream,
|
|
}
|
|
}
|
|
|
|
impl Sink for RodioSink {
|
|
fn start(&mut self) -> io::Result<()> {
|
|
// More similar to an "unpause" than "play". Doesn't undo "stop".
|
|
// self.rodio_sink.play();
|
|
Ok(())
|
|
}
|
|
|
|
fn stop(&mut self) -> io::Result<()> {
|
|
// This will immediately stop playback, but the sink is then unusable.
|
|
// We just have to let the current buffer play till the end.
|
|
// self.rodio_sink.stop();
|
|
Ok(())
|
|
}
|
|
|
|
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
|
|
let source = rodio::buffer::SamplesBuffer::new(2, 44100, packet.samples());
|
|
self.rodio_sink.append(source);
|
|
|
|
// Chunk sizes seem to be about 256 to 3000 ish items long.
|
|
// Assuming they're on average 1628 then a half second buffer is:
|
|
// 44100 elements --> about 27 chunks
|
|
while self.rodio_sink.len() > 26 {
|
|
// sleep and wait for rodio to drain a bit
|
|
thread::sleep(time::Duration::from_millis(10));
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|