mirror of
https://github.com/librespot-org/librespot.git
synced 2025-10-04 18:29:45 +02:00
Lay groundwork for new Spotify API client (#805)
Lay groundwork for new Spotify API before removing `spirc` * Add token provider * Introduce HTTP client * Introduce caching `ApResolver` component * Remove `keymaster` and update example * Use `PacketType` instead of hex identifiers * Document new unknown packet 0xb6
This commit is contained in:
parent
113ac94c07
commit
39bf40bcc7
62 changed files with 3101 additions and 1837 deletions
|
@ -1,95 +1,189 @@
|
|||
use super::{Open, Sink, SinkAsBytes};
|
||||
use crate::config::AudioFormat;
|
||||
use crate::convert::Converter;
|
||||
use crate::decoder::AudioPacket;
|
||||
use crate::player::{NUM_CHANNELS, SAMPLES_PER_SECOND, SAMPLE_RATE};
|
||||
use crate::{NUM_CHANNELS, SAMPLE_RATE};
|
||||
use alsa::device_name::HintIter;
|
||||
use alsa::pcm::{Access, Format, Frames, HwParams, PCM};
|
||||
use alsa::{Direction, Error, ValueOr};
|
||||
use alsa::pcm::{Access, Format, HwParams, PCM};
|
||||
use alsa::{Direction, ValueOr};
|
||||
use std::cmp::min;
|
||||
use std::ffi::CString;
|
||||
use std::io;
|
||||
use std::process::exit;
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
|
||||
const BUFFERED_LATENCY: f32 = 0.125; // seconds
|
||||
const BUFFERED_PERIODS: Frames = 4;
|
||||
// 125 ms Period time * 4 periods = 0.5 sec buffer.
|
||||
const PERIOD_TIME: Duration = Duration::from_millis(125);
|
||||
const NUM_PERIODS: u32 = 4;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
enum AlsaError {
|
||||
#[error("AlsaSink, device {device} may be invalid or busy, {err}")]
|
||||
PcmSetUp { device: String, err: alsa::Error },
|
||||
#[error("AlsaSink, device {device} unsupported access type RWInterleaved, {err}")]
|
||||
UnsupportedAccessType { device: String, err: alsa::Error },
|
||||
#[error("AlsaSink, device {device} unsupported format {format:?}, {err}")]
|
||||
UnsupportedFormat {
|
||||
device: String,
|
||||
format: AudioFormat,
|
||||
err: alsa::Error,
|
||||
},
|
||||
#[error("AlsaSink, device {device} unsupported sample rate {samplerate}, {err}")]
|
||||
UnsupportedSampleRate {
|
||||
device: String,
|
||||
samplerate: u32,
|
||||
err: alsa::Error,
|
||||
},
|
||||
#[error("AlsaSink, device {device} unsupported channel count {channel_count}, {err}")]
|
||||
UnsupportedChannelCount {
|
||||
device: String,
|
||||
channel_count: u8,
|
||||
err: alsa::Error,
|
||||
},
|
||||
#[error("AlsaSink Hardware Parameters Error, {0}")]
|
||||
HwParams(alsa::Error),
|
||||
#[error("AlsaSink Software Parameters Error, {0}")]
|
||||
SwParams(alsa::Error),
|
||||
#[error("AlsaSink PCM Error, {0}")]
|
||||
Pcm(alsa::Error),
|
||||
}
|
||||
|
||||
pub struct AlsaSink {
|
||||
pcm: Option<PCM>,
|
||||
format: AudioFormat,
|
||||
device: String,
|
||||
buffer: Vec<u8>,
|
||||
period_buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
fn list_outputs() {
|
||||
fn list_outputs() -> io::Result<()> {
|
||||
println!("Listing available Alsa outputs:");
|
||||
for t in &["pcm", "ctl", "hwdep"] {
|
||||
println!("{} devices:", t);
|
||||
let i = HintIter::new(None, &*CString::new(*t).unwrap()).unwrap();
|
||||
let i = match HintIter::new_str(None, &t) {
|
||||
Ok(i) => i,
|
||||
Err(e) => {
|
||||
return Err(io::Error::new(io::ErrorKind::Other, e));
|
||||
}
|
||||
};
|
||||
for a in i {
|
||||
if let Some(Direction::Playback) = a.direction {
|
||||
// mimic aplay -L
|
||||
println!(
|
||||
"{}\n\t{}\n",
|
||||
a.name.unwrap(),
|
||||
a.desc.unwrap().replace("\n", "\n\t")
|
||||
);
|
||||
let name = a
|
||||
.name
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Could not parse name"))?;
|
||||
let desc = a
|
||||
.desc
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Could not parse desc"))?;
|
||||
println!("{}\n\t{}\n", name, desc.replace("\n", "\n\t"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box<Error>> {
|
||||
let pcm = PCM::new(dev_name, Direction::Playback, false)?;
|
||||
fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, usize), AlsaError> {
|
||||
let pcm = PCM::new(dev_name, Direction::Playback, false).map_err(|e| AlsaError::PcmSetUp {
|
||||
device: dev_name.to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
|
||||
let alsa_format = match format {
|
||||
AudioFormat::F64 => Format::float64(),
|
||||
AudioFormat::F32 => Format::float(),
|
||||
AudioFormat::S32 => Format::s32(),
|
||||
AudioFormat::S24 => Format::s24(),
|
||||
AudioFormat::S24_3 => Format::S243LE,
|
||||
AudioFormat::S16 => Format::s16(),
|
||||
|
||||
#[cfg(target_endian = "little")]
|
||||
AudioFormat::S24_3 => Format::S243LE,
|
||||
#[cfg(target_endian = "big")]
|
||||
AudioFormat::S24_3 => Format::S243BE,
|
||||
};
|
||||
|
||||
// http://www.linuxjournal.com/article/6735?page=0,1#N0x19ab2890.0x19ba78d8
|
||||
// latency = period_size * periods / (rate * bytes_per_frame)
|
||||
// For stereo samples encoded as 32-bit float, one frame has a length of eight bytes.
|
||||
let mut period_size = ((SAMPLES_PER_SECOND * format.size() as u32) as f32
|
||||
* (BUFFERED_LATENCY / BUFFERED_PERIODS as f32)) as Frames;
|
||||
{
|
||||
let hwp = HwParams::any(&pcm)?;
|
||||
hwp.set_access(Access::RWInterleaved)?;
|
||||
hwp.set_format(alsa_format)?;
|
||||
hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest)?;
|
||||
hwp.set_channels(NUM_CHANNELS as u32)?;
|
||||
period_size = hwp.set_period_size_near(period_size, ValueOr::Greater)?;
|
||||
hwp.set_buffer_size_near(period_size * BUFFERED_PERIODS)?;
|
||||
pcm.hw_params(&hwp)?;
|
||||
let bytes_per_period = {
|
||||
let hwp = HwParams::any(&pcm).map_err(AlsaError::HwParams)?;
|
||||
hwp.set_access(Access::RWInterleaved)
|
||||
.map_err(|e| AlsaError::UnsupportedAccessType {
|
||||
device: dev_name.to_string(),
|
||||
err: e,
|
||||
})?;
|
||||
|
||||
let swp = pcm.sw_params_current()?;
|
||||
swp.set_start_threshold(hwp.get_buffer_size()? - hwp.get_period_size()?)?;
|
||||
pcm.sw_params(&swp)?;
|
||||
}
|
||||
hwp.set_format(alsa_format)
|
||||
.map_err(|e| AlsaError::UnsupportedFormat {
|
||||
device: dev_name.to_string(),
|
||||
format,
|
||||
err: e,
|
||||
})?;
|
||||
|
||||
Ok((pcm, period_size))
|
||||
hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest).map_err(|e| {
|
||||
AlsaError::UnsupportedSampleRate {
|
||||
device: dev_name.to_string(),
|
||||
samplerate: SAMPLE_RATE,
|
||||
err: e,
|
||||
}
|
||||
})?;
|
||||
|
||||
hwp.set_channels(NUM_CHANNELS as u32)
|
||||
.map_err(|e| AlsaError::UnsupportedChannelCount {
|
||||
device: dev_name.to_string(),
|
||||
channel_count: NUM_CHANNELS,
|
||||
err: e,
|
||||
})?;
|
||||
|
||||
// Deal strictly in time and periods.
|
||||
hwp.set_periods(NUM_PERIODS, ValueOr::Nearest)
|
||||
.map_err(AlsaError::HwParams)?;
|
||||
|
||||
hwp.set_period_time_near(PERIOD_TIME.as_micros() as u32, ValueOr::Nearest)
|
||||
.map_err(AlsaError::HwParams)?;
|
||||
|
||||
pcm.hw_params(&hwp).map_err(AlsaError::Pcm)?;
|
||||
|
||||
let swp = pcm.sw_params_current().map_err(AlsaError::Pcm)?;
|
||||
|
||||
// Don't assume we got what we wanted.
|
||||
// Ask to make sure.
|
||||
let frames_per_period = hwp.get_period_size().map_err(AlsaError::HwParams)?;
|
||||
|
||||
let frames_per_buffer = hwp.get_buffer_size().map_err(AlsaError::HwParams)?;
|
||||
|
||||
swp.set_start_threshold(frames_per_buffer - frames_per_period)
|
||||
.map_err(AlsaError::SwParams)?;
|
||||
|
||||
pcm.sw_params(&swp).map_err(AlsaError::Pcm)?;
|
||||
|
||||
// Let ALSA do the math for us.
|
||||
pcm.frames_to_bytes(frames_per_period) as usize
|
||||
};
|
||||
|
||||
Ok((pcm, bytes_per_period))
|
||||
}
|
||||
|
||||
impl Open for AlsaSink {
|
||||
fn open(device: Option<String>, format: AudioFormat) -> Self {
|
||||
info!("Using Alsa sink with format: {:?}", format);
|
||||
|
||||
let name = match device.as_ref().map(AsRef::as_ref) {
|
||||
Some("?") => {
|
||||
println!("Listing available Alsa outputs:");
|
||||
list_outputs();
|
||||
exit(0)
|
||||
}
|
||||
let name = match device.as_deref() {
|
||||
Some("?") => match list_outputs() {
|
||||
Ok(_) => {
|
||||
exit(0);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Error listing Alsa outputs, {}", err);
|
||||
exit(1);
|
||||
}
|
||||
},
|
||||
Some(device) => device,
|
||||
None => "default",
|
||||
}
|
||||
.to_string();
|
||||
|
||||
info!("Using AlsaSink with format: {:?}", format);
|
||||
|
||||
Self {
|
||||
pcm: None,
|
||||
format,
|
||||
device: name,
|
||||
buffer: vec![],
|
||||
period_buffer: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -97,21 +191,13 @@ impl Open for AlsaSink {
|
|||
impl Sink for AlsaSink {
|
||||
fn start(&mut self) -> io::Result<()> {
|
||||
if self.pcm.is_none() {
|
||||
let pcm = open_device(&self.device, self.format);
|
||||
match pcm {
|
||||
Ok((p, period_size)) => {
|
||||
self.pcm = Some(p);
|
||||
// Create a buffer for all samples for a full period
|
||||
self.buffer = Vec::with_capacity(
|
||||
period_size as usize * BUFFERED_PERIODS as usize * self.format.size(),
|
||||
);
|
||||
match open_device(&self.device, self.format) {
|
||||
Ok((pcm, bytes_per_period)) => {
|
||||
self.pcm = Some(pcm);
|
||||
self.period_buffer = Vec::with_capacity(bytes_per_period);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Alsa error PCM open {}", e);
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Alsa error: PCM open failed",
|
||||
));
|
||||
return Err(io::Error::new(io::ErrorKind::Other, e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -123,9 +209,16 @@ impl Sink for AlsaSink {
|
|||
{
|
||||
// Write any leftover data in the period buffer
|
||||
// before draining the actual buffer
|
||||
self.write_bytes(&[]).expect("could not flush buffer");
|
||||
let pcm = self.pcm.as_mut().unwrap();
|
||||
pcm.drain().unwrap();
|
||||
self.write_bytes(&[])?;
|
||||
let pcm = self.pcm.as_mut().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::Other, "Error stopping AlsaSink, PCM is None")
|
||||
})?;
|
||||
pcm.drain().map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("Error stopping AlsaSink {}", e),
|
||||
)
|
||||
})?
|
||||
}
|
||||
self.pcm = None;
|
||||
Ok(())
|
||||
|
@ -139,15 +232,15 @@ impl SinkAsBytes for AlsaSink {
|
|||
let mut processed_data = 0;
|
||||
while processed_data < data.len() {
|
||||
let data_to_buffer = min(
|
||||
self.buffer.capacity() - self.buffer.len(),
|
||||
self.period_buffer.capacity() - self.period_buffer.len(),
|
||||
data.len() - processed_data,
|
||||
);
|
||||
self.buffer
|
||||
self.period_buffer
|
||||
.extend_from_slice(&data[processed_data..processed_data + data_to_buffer]);
|
||||
processed_data += data_to_buffer;
|
||||
if self.buffer.len() == self.buffer.capacity() {
|
||||
self.write_buf();
|
||||
self.buffer.clear();
|
||||
if self.period_buffer.len() == self.period_buffer.capacity() {
|
||||
self.write_buf()?;
|
||||
self.period_buffer.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -156,12 +249,34 @@ impl SinkAsBytes for AlsaSink {
|
|||
}
|
||||
|
||||
impl AlsaSink {
|
||||
fn write_buf(&mut self) {
|
||||
let pcm = self.pcm.as_mut().unwrap();
|
||||
pub const NAME: &'static str = "alsa";
|
||||
|
||||
fn write_buf(&mut self) -> io::Result<()> {
|
||||
let pcm = self.pcm.as_mut().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Error writing from AlsaSink buffer to PCM, PCM is None",
|
||||
)
|
||||
})?;
|
||||
let io = pcm.io_bytes();
|
||||
match io.writei(&self.buffer) {
|
||||
Ok(_) => (),
|
||||
Err(err) => pcm.try_recover(err, false).unwrap(),
|
||||
};
|
||||
if let Err(err) = io.writei(&self.period_buffer) {
|
||||
// Capture and log the original error as a warning, and then try to recover.
|
||||
// If recovery fails then forward that error back to player.
|
||||
warn!(
|
||||
"Error writing from AlsaSink buffer to PCM, trying to recover {}",
|
||||
err
|
||||
);
|
||||
pcm.try_recover(err, false).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!(
|
||||
"Error writing from AlsaSink buffer to PCM, recovery failed {}",
|
||||
e
|
||||
),
|
||||
)
|
||||
})?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use super::{Open, Sink, SinkAsBytes};
|
||||
use crate::config::AudioFormat;
|
||||
use crate::convert::Converter;
|
||||
use crate::decoder::AudioPacket;
|
||||
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
|
||||
use crate::{NUM_CHANNELS, SAMPLE_RATE};
|
||||
|
||||
use gstreamer as gst;
|
||||
use gstreamer_app as gst_app;
|
||||
|
@ -33,11 +34,17 @@ impl Open for GstreamerSink {
|
|||
let sample_size = format.size();
|
||||
let gst_bytes = 2048 * sample_size;
|
||||
|
||||
#[cfg(target_endian = "little")]
|
||||
const ENDIANNESS: &str = "LE";
|
||||
#[cfg(target_endian = "big")]
|
||||
const ENDIANNESS: &str = "BE";
|
||||
|
||||
let pipeline_str_preamble = format!(
|
||||
"appsrc caps=\"audio/x-raw,format={}LE,layout=interleaved,channels={},rate={}\" block=true max-bytes={} name=appsrc0 ",
|
||||
gst_format, NUM_CHANNELS, SAMPLE_RATE, gst_bytes
|
||||
"appsrc caps=\"audio/x-raw,format={}{},layout=interleaved,channels={},rate={}\" block=true max-bytes={} name=appsrc0 ",
|
||||
gst_format, ENDIANNESS, NUM_CHANNELS, SAMPLE_RATE, gst_bytes
|
||||
);
|
||||
let pipeline_str_rest = r#" ! audioconvert ! autoaudiosink"#;
|
||||
// no need to dither twice; use librespot dithering instead
|
||||
let pipeline_str_rest = r#" ! audioconvert dithering=none ! autoaudiosink"#;
|
||||
let pipeline_str: String = match device {
|
||||
Some(x) => format!("{}{}", pipeline_str_preamble, x),
|
||||
None => format!("{}{}", pipeline_str_preamble, pipeline_str_rest),
|
||||
|
@ -120,7 +127,6 @@ impl Open for GstreamerSink {
|
|||
}
|
||||
|
||||
impl Sink for GstreamerSink {
|
||||
start_stop_noop!();
|
||||
sink_as_bytes!();
|
||||
}
|
||||
|
||||
|
@ -133,3 +139,7 @@ impl SinkAsBytes for GstreamerSink {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl GstreamerSink {
|
||||
pub const NAME: &'static str = "gstreamer";
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use super::{Open, Sink};
|
||||
use crate::config::AudioFormat;
|
||||
use crate::convert::Converter;
|
||||
use crate::decoder::AudioPacket;
|
||||
use crate::player::NUM_CHANNELS;
|
||||
use crate::NUM_CHANNELS;
|
||||
use jack::{
|
||||
AsyncClient, AudioOut, Client, ClientOptions, Control, Port, ProcessHandler, ProcessScope,
|
||||
};
|
||||
|
@ -69,11 +70,10 @@ impl Open for JackSink {
|
|||
}
|
||||
|
||||
impl Sink for JackSink {
|
||||
start_stop_noop!();
|
||||
|
||||
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
|
||||
for s in packet.samples().iter() {
|
||||
let res = self.send.send(*s);
|
||||
fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> {
|
||||
let samples_f32: &[f32] = &converter.f64_to_f32(packet.samples());
|
||||
for sample in samples_f32.iter() {
|
||||
let res = self.send.send(*sample);
|
||||
if res.is_err() {
|
||||
error!("cannot write to channel");
|
||||
}
|
||||
|
@ -81,3 +81,7 @@ impl Sink for JackSink {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl JackSink {
|
||||
pub const NAME: &'static str = "jackaudio";
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use crate::config::AudioFormat;
|
||||
use crate::convert::Converter;
|
||||
use crate::decoder::AudioPacket;
|
||||
use std::io;
|
||||
|
||||
|
@ -7,9 +8,13 @@ pub trait Open {
|
|||
}
|
||||
|
||||
pub trait Sink {
|
||||
fn start(&mut self) -> io::Result<()>;
|
||||
fn stop(&mut self) -> io::Result<()>;
|
||||
fn write(&mut self, packet: &AudioPacket) -> io::Result<()>;
|
||||
fn start(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn stop(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()>;
|
||||
}
|
||||
|
||||
pub type SinkBuilder = fn(Option<String>, AudioFormat) -> Box<dyn Sink>;
|
||||
|
@ -25,26 +30,30 @@ fn mk_sink<S: Sink + Open + 'static>(device: Option<String>, format: AudioFormat
|
|||
// reuse code for various backends
|
||||
macro_rules! sink_as_bytes {
|
||||
() => {
|
||||
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
|
||||
use crate::convert::{self, i24};
|
||||
fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> {
|
||||
use crate::convert::i24;
|
||||
use zerocopy::AsBytes;
|
||||
match packet {
|
||||
AudioPacket::Samples(samples) => match self.format {
|
||||
AudioFormat::F32 => self.write_bytes(samples.as_bytes()),
|
||||
AudioFormat::F64 => self.write_bytes(samples.as_bytes()),
|
||||
AudioFormat::F32 => {
|
||||
let samples_f32: &[f32] = &converter.f64_to_f32(samples);
|
||||
self.write_bytes(samples_f32.as_bytes())
|
||||
}
|
||||
AudioFormat::S32 => {
|
||||
let samples_s32: &[i32] = &convert::to_s32(samples);
|
||||
let samples_s32: &[i32] = &converter.f64_to_s32(samples);
|
||||
self.write_bytes(samples_s32.as_bytes())
|
||||
}
|
||||
AudioFormat::S24 => {
|
||||
let samples_s24: &[i32] = &convert::to_s24(samples);
|
||||
let samples_s24: &[i32] = &converter.f64_to_s24(samples);
|
||||
self.write_bytes(samples_s24.as_bytes())
|
||||
}
|
||||
AudioFormat::S24_3 => {
|
||||
let samples_s24_3: &[i24] = &convert::to_s24_3(samples);
|
||||
let samples_s24_3: &[i24] = &converter.f64_to_s24_3(samples);
|
||||
self.write_bytes(samples_s24_3.as_bytes())
|
||||
}
|
||||
AudioFormat::S16 => {
|
||||
let samples_s16: &[i16] = &convert::to_s16(samples);
|
||||
let samples_s16: &[i16] = &converter.f64_to_s16(samples);
|
||||
self.write_bytes(samples_s16.as_bytes())
|
||||
}
|
||||
},
|
||||
|
@ -54,17 +63,6 @@ macro_rules! sink_as_bytes {
|
|||
};
|
||||
}
|
||||
|
||||
macro_rules! start_stop_noop {
|
||||
() => {
|
||||
fn start(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn stop(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "alsa-backend")]
|
||||
mod alsa;
|
||||
#[cfg(feature = "alsa-backend")]
|
||||
|
@ -92,6 +90,8 @@ use self::gstreamer::GstreamerSink;
|
|||
|
||||
#[cfg(any(feature = "rodio-backend", feature = "rodiojack-backend"))]
|
||||
mod rodio;
|
||||
#[cfg(any(feature = "rodio-backend", feature = "rodiojack-backend"))]
|
||||
use self::rodio::RodioSink;
|
||||
|
||||
#[cfg(feature = "sdl-backend")]
|
||||
mod sdl;
|
||||
|
@ -105,24 +105,24 @@ mod subprocess;
|
|||
use self::subprocess::SubprocessSink;
|
||||
|
||||
pub const BACKENDS: &[(&str, SinkBuilder)] = &[
|
||||
#[cfg(feature = "alsa-backend")]
|
||||
("alsa", mk_sink::<AlsaSink>),
|
||||
#[cfg(feature = "portaudio-backend")]
|
||||
("portaudio", mk_sink::<PortAudioSink>),
|
||||
#[cfg(feature = "pulseaudio-backend")]
|
||||
("pulseaudio", mk_sink::<PulseAudioSink>),
|
||||
#[cfg(feature = "jackaudio-backend")]
|
||||
("jackaudio", mk_sink::<JackSink>),
|
||||
#[cfg(feature = "gstreamer-backend")]
|
||||
("gstreamer", mk_sink::<GstreamerSink>),
|
||||
#[cfg(feature = "rodio-backend")]
|
||||
("rodio", rodio::mk_rodio),
|
||||
(RodioSink::NAME, rodio::mk_rodio), // default goes first
|
||||
#[cfg(feature = "alsa-backend")]
|
||||
(AlsaSink::NAME, mk_sink::<AlsaSink>),
|
||||
#[cfg(feature = "portaudio-backend")]
|
||||
(PortAudioSink::NAME, mk_sink::<PortAudioSink>),
|
||||
#[cfg(feature = "pulseaudio-backend")]
|
||||
(PulseAudioSink::NAME, mk_sink::<PulseAudioSink>),
|
||||
#[cfg(feature = "jackaudio-backend")]
|
||||
(JackSink::NAME, mk_sink::<JackSink>),
|
||||
#[cfg(feature = "gstreamer-backend")]
|
||||
(GstreamerSink::NAME, mk_sink::<GstreamerSink>),
|
||||
#[cfg(feature = "rodiojack-backend")]
|
||||
("rodiojack", rodio::mk_rodiojack),
|
||||
#[cfg(feature = "sdl-backend")]
|
||||
("sdl", mk_sink::<SdlSink>),
|
||||
("pipe", mk_sink::<StdoutSink>),
|
||||
("subprocess", mk_sink::<SubprocessSink>),
|
||||
(SdlSink::NAME, mk_sink::<SdlSink>),
|
||||
(StdoutSink::NAME, mk_sink::<StdoutSink>),
|
||||
(SubprocessSink::NAME, mk_sink::<SubprocessSink>),
|
||||
];
|
||||
|
||||
pub fn find(name: Option<String>) -> Option<SinkBuilder> {
|
||||
|
|
|
@ -1,36 +1,66 @@
|
|||
use super::{Open, Sink, SinkAsBytes};
|
||||
use crate::config::AudioFormat;
|
||||
use crate::convert::Converter;
|
||||
use crate::decoder::AudioPacket;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::{self, Write};
|
||||
|
||||
pub struct StdoutSink {
|
||||
output: Box<dyn Write>,
|
||||
output: Option<Box<dyn Write>>,
|
||||
path: Option<String>,
|
||||
format: AudioFormat,
|
||||
}
|
||||
|
||||
impl Open for StdoutSink {
|
||||
fn open(path: Option<String>, format: AudioFormat) -> Self {
|
||||
info!("Using pipe sink with format: {:?}", format);
|
||||
|
||||
let output: Box<dyn Write> = match path {
|
||||
Some(path) => Box::new(OpenOptions::new().write(true).open(path).unwrap()),
|
||||
_ => Box::new(io::stdout()),
|
||||
};
|
||||
|
||||
Self { output, format }
|
||||
Self {
|
||||
output: None,
|
||||
path,
|
||||
format,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sink for StdoutSink {
|
||||
start_stop_noop!();
|
||||
fn start(&mut self) -> io::Result<()> {
|
||||
if self.output.is_none() {
|
||||
let output: Box<dyn Write> = match self.path.as_deref() {
|
||||
Some(path) => {
|
||||
let open_op = OpenOptions::new()
|
||||
.write(true)
|
||||
.open(path)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
Box::new(open_op)
|
||||
}
|
||||
None => Box::new(io::stdout()),
|
||||
};
|
||||
|
||||
self.output = Some(output);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
sink_as_bytes!();
|
||||
}
|
||||
|
||||
impl SinkAsBytes for StdoutSink {
|
||||
fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> {
|
||||
self.output.write_all(data)?;
|
||||
self.output.flush()?;
|
||||
match self.output.as_deref_mut() {
|
||||
Some(output) => {
|
||||
output.write_all(data)?;
|
||||
output.flush()?;
|
||||
}
|
||||
None => {
|
||||
return Err(io::Error::new(io::ErrorKind::Other, "Output is None"));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl StdoutSink {
|
||||
pub const NAME: &'static str = "pipe";
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use super::{Open, Sink};
|
||||
use crate::config::AudioFormat;
|
||||
use crate::convert;
|
||||
use crate::convert::Converter;
|
||||
use crate::decoder::AudioPacket;
|
||||
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
|
||||
use crate::{NUM_CHANNELS, SAMPLE_RATE};
|
||||
use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo};
|
||||
use portaudio_rs::stream::*;
|
||||
use std::io;
|
||||
|
@ -55,12 +55,9 @@ impl<'a> Open for PortAudioSink<'a> {
|
|||
fn open(device: Option<String>, format: AudioFormat) -> PortAudioSink<'a> {
|
||||
info!("Using PortAudio sink with format: {:?}", format);
|
||||
|
||||
warn!("This backend is known to panic on several platforms.");
|
||||
warn!("Consider using some other backend, or better yet, contributing a fix.");
|
||||
|
||||
portaudio_rs::initialize().unwrap();
|
||||
|
||||
let device_idx = match device.as_ref().map(AsRef::as_ref) {
|
||||
let device_idx = match device.as_deref() {
|
||||
Some("?") => {
|
||||
list_outputs();
|
||||
exit(0)
|
||||
|
@ -109,7 +106,7 @@ impl<'a> Sink for PortAudioSink<'a> {
|
|||
Some(*$parameters),
|
||||
SAMPLE_RATE as f64,
|
||||
FRAMES_PER_BUFFER_UNSPECIFIED,
|
||||
StreamFlags::empty(),
|
||||
StreamFlags::DITHER_OFF, // no need to dither twice; use librespot dithering instead
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
|
@ -136,15 +133,15 @@ impl<'a> Sink for PortAudioSink<'a> {
|
|||
}};
|
||||
}
|
||||
match self {
|
||||
Self::F32(stream, _parameters) => stop_sink!(ref mut stream),
|
||||
Self::S32(stream, _parameters) => stop_sink!(ref mut stream),
|
||||
Self::S16(stream, _parameters) => stop_sink!(ref mut stream),
|
||||
Self::F32(stream, _) => stop_sink!(ref mut stream),
|
||||
Self::S32(stream, _) => stop_sink!(ref mut stream),
|
||||
Self::S16(stream, _) => stop_sink!(ref mut stream),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
|
||||
fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> {
|
||||
macro_rules! write_sink {
|
||||
(ref mut $stream: expr, $samples: expr) => {
|
||||
$stream.as_mut().unwrap().write($samples)
|
||||
|
@ -154,14 +151,15 @@ impl<'a> Sink for PortAudioSink<'a> {
|
|||
let samples = packet.samples();
|
||||
let result = match self {
|
||||
Self::F32(stream, _parameters) => {
|
||||
write_sink!(ref mut stream, samples)
|
||||
let samples_f32: &[f32] = &converter.f64_to_f32(samples);
|
||||
write_sink!(ref mut stream, samples_f32)
|
||||
}
|
||||
Self::S32(stream, _parameters) => {
|
||||
let samples_s32: &[i32] = &convert::to_s32(samples);
|
||||
let samples_s32: &[i32] = &converter.f64_to_s32(samples);
|
||||
write_sink!(ref mut stream, samples_s32)
|
||||
}
|
||||
Self::S16(stream, _parameters) => {
|
||||
let samples_s16: &[i16] = &convert::to_s16(samples);
|
||||
let samples_s16: &[i16] = &converter.f64_to_s16(samples);
|
||||
write_sink!(ref mut stream, samples_s16)
|
||||
}
|
||||
};
|
||||
|
@ -180,3 +178,7 @@ impl<'a> Drop for PortAudioSink<'a> {
|
|||
portaudio_rs::terminate().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PortAudioSink<'a> {
|
||||
pub const NAME: &'static str = "portaudio";
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use super::{Open, Sink, SinkAsBytes};
|
||||
use crate::config::AudioFormat;
|
||||
use crate::convert::Converter;
|
||||
use crate::decoder::AudioPacket;
|
||||
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
|
||||
use crate::{NUM_CHANNELS, SAMPLE_RATE};
|
||||
use libpulse_binding::{self as pulse, stream::Direction};
|
||||
use libpulse_simple_binding::Simple;
|
||||
use std::io;
|
||||
|
@ -22,11 +23,14 @@ impl Open for PulseAudioSink {
|
|||
|
||||
// PulseAudio calls S24 and S24_3 different from the rest of the world
|
||||
let pulse_format = match format {
|
||||
AudioFormat::F32 => pulse::sample::Format::F32le,
|
||||
AudioFormat::S32 => pulse::sample::Format::S32le,
|
||||
AudioFormat::S24 => pulse::sample::Format::S24_32le,
|
||||
AudioFormat::S24_3 => pulse::sample::Format::S24le,
|
||||
AudioFormat::S16 => pulse::sample::Format::S16le,
|
||||
AudioFormat::F32 => pulse::sample::Format::FLOAT32NE,
|
||||
AudioFormat::S32 => pulse::sample::Format::S32NE,
|
||||
AudioFormat::S24 => pulse::sample::Format::S24_32NE,
|
||||
AudioFormat::S24_3 => pulse::sample::Format::S24NE,
|
||||
AudioFormat::S16 => pulse::sample::Format::S16NE,
|
||||
_ => {
|
||||
unimplemented!("PulseAudio currently does not support {:?} output", format)
|
||||
}
|
||||
};
|
||||
|
||||
let ss = pulse::sample::Spec {
|
||||
|
@ -51,7 +55,7 @@ impl Sink for PulseAudioSink {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
let device = self.device.as_ref().map(|s| (*s).as_str());
|
||||
let device = self.device.as_deref();
|
||||
let result = Simple::new(
|
||||
None, // Use the default server.
|
||||
APP_NAME, // Our application's name.
|
||||
|
@ -100,3 +104,7 @@ impl SinkAsBytes for PulseAudioSink {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PulseAudioSink {
|
||||
pub const NAME: &'static str = "pulseaudio";
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
use std::process::exit;
|
||||
use std::{io, thread, time};
|
||||
use std::time::Duration;
|
||||
use std::{io, thread};
|
||||
|
||||
use cpal::traits::{DeviceTrait, HostTrait};
|
||||
use thiserror::Error;
|
||||
|
||||
use super::Sink;
|
||||
use crate::config::AudioFormat;
|
||||
use crate::convert;
|
||||
use crate::convert::Converter;
|
||||
use crate::decoder::AudioPacket;
|
||||
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
|
||||
use crate::{NUM_CHANNELS, SAMPLE_RATE};
|
||||
|
||||
#[cfg(all(
|
||||
feature = "rodiojack-backend",
|
||||
|
@ -174,18 +175,20 @@ pub fn open(host: cpal::Host, device: Option<String>, format: AudioFormat) -> Ro
|
|||
}
|
||||
|
||||
impl Sink for RodioSink {
|
||||
start_stop_noop!();
|
||||
|
||||
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
|
||||
fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> {
|
||||
let samples = packet.samples();
|
||||
match self.format {
|
||||
AudioFormat::F32 => {
|
||||
let source =
|
||||
rodio::buffer::SamplesBuffer::new(NUM_CHANNELS as u16, SAMPLE_RATE, samples);
|
||||
let samples_f32: &[f32] = &converter.f64_to_f32(samples);
|
||||
let source = rodio::buffer::SamplesBuffer::new(
|
||||
NUM_CHANNELS as u16,
|
||||
SAMPLE_RATE,
|
||||
samples_f32,
|
||||
);
|
||||
self.rodio_sink.append(source);
|
||||
}
|
||||
AudioFormat::S16 => {
|
||||
let samples_s16: &[i16] = &convert::to_s16(samples);
|
||||
let samples_s16: &[i16] = &converter.f64_to_s16(samples);
|
||||
let source = rodio::buffer::SamplesBuffer::new(
|
||||
NUM_CHANNELS as u16,
|
||||
SAMPLE_RATE,
|
||||
|
@ -201,8 +204,12 @@ impl Sink for RodioSink {
|
|||
// 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));
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl RodioSink {
|
||||
pub const NAME: &'static str = "rodio";
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use super::{Open, Sink};
|
||||
use crate::config::AudioFormat;
|
||||
use crate::convert;
|
||||
use crate::convert::Converter;
|
||||
use crate::decoder::AudioPacket;
|
||||
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
|
||||
use crate::{NUM_CHANNELS, SAMPLE_RATE};
|
||||
use sdl2::audio::{AudioQueue, AudioSpecDesired};
|
||||
use std::{io, thread, time};
|
||||
use std::time::Duration;
|
||||
use std::{io, thread};
|
||||
|
||||
pub enum SdlSink {
|
||||
F32(AudioQueue<f32>),
|
||||
|
@ -81,12 +82,12 @@ impl Sink for SdlSink {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
|
||||
fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> {
|
||||
macro_rules! drain_sink {
|
||||
($queue: expr, $size: expr) => {{
|
||||
// sleep and wait for sdl thread to drain the queue a bit
|
||||
while $queue.size() > (NUM_CHANNELS as u32 * $size as u32 * SAMPLE_RATE) {
|
||||
thread::sleep(time::Duration::from_millis(10));
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
@ -94,16 +95,17 @@ impl Sink for SdlSink {
|
|||
let samples = packet.samples();
|
||||
match self {
|
||||
Self::F32(queue) => {
|
||||
let samples_f32: &[f32] = &converter.f64_to_f32(samples);
|
||||
drain_sink!(queue, AudioFormat::F32.size());
|
||||
queue.queue(samples)
|
||||
queue.queue(samples_f32)
|
||||
}
|
||||
Self::S32(queue) => {
|
||||
let samples_s32: &[i32] = &convert::to_s32(samples);
|
||||
let samples_s32: &[i32] = &converter.f64_to_s32(samples);
|
||||
drain_sink!(queue, AudioFormat::S32.size());
|
||||
queue.queue(samples_s32)
|
||||
}
|
||||
Self::S16(queue) => {
|
||||
let samples_s16: &[i16] = &convert::to_s16(samples);
|
||||
let samples_s16: &[i16] = &converter.f64_to_s16(samples);
|
||||
drain_sink!(queue, AudioFormat::S16.size());
|
||||
queue.queue(samples_s16)
|
||||
}
|
||||
|
@ -111,3 +113,7 @@ impl Sink for SdlSink {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SdlSink {
|
||||
pub const NAME: &'static str = "sdl";
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use super::{Open, Sink, SinkAsBytes};
|
||||
use crate::config::AudioFormat;
|
||||
use crate::convert::Converter;
|
||||
use crate::decoder::AudioPacket;
|
||||
use shell_words::split;
|
||||
|
||||
|
@ -61,3 +62,7 @@ impl SinkAsBytes for SubprocessSink {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SubprocessSink {
|
||||
pub const NAME: &'static str = "subprocess";
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue