diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 2efd6ca6..1d9390c3 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -15,7 +15,7 @@ futures = "0.1.8" log = "0.3.5" byteorder = "1.2.1" -alsa = { git = "https://github.com/plietar/rust-alsa", optional = true } +alsa = { version = "0.1.5", optional = true } portaudio-rs = { version = "0.3.0", optional = true } libpulse-sys = { version = "0.0.0", optional = true } jack = { version = "0.5.3", optional = true } diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 982a2625..7a067697 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -1,11 +1,14 @@ use super::{Open, Sink}; -use alsa::{Access, Format, Mode, Stream, PCM}; use std::io; +use std::ffi::CString; +use alsa::{Direction, ValueOr}; +use alsa::pcm::{PCM, HwParams, Format, Access}; + pub struct AlsaSink(Option, String); impl Open for AlsaSink { - fn open(device: Option) -> AlsaSink { + fn open(device: Option) -> AlsaSink { info!("Using alsa sink"); let name = device.unwrap_or("default".to_string()); @@ -16,26 +19,24 @@ impl Open for AlsaSink { impl Sink for AlsaSink { fn start(&mut self) -> io::Result<()> { - if self.0.is_none() { - match PCM::open( - &*self.1, - Stream::Playback, - Mode::Blocking, - Format::Signed16, - Access::Interleaved, - 2, - 44100, - ) { - Ok(f) => self.0 = Some(f), - Err(e) => { - error!("Alsa error PCM open {}", e); - return Err(io::Error::new( - io::ErrorKind::Other, - "Alsa error: PCM open failed", - )); - } + if self.0.is_some() { + } else { + let pcm = PCM::open(&*CString::new(self.1.to_owned().into_bytes()).unwrap(), + Direction::Playback, + false).unwrap(); + { + // Set hardware parameters: 44100 Hz / Stereo / 16 bit + let hwp = HwParams::any(&pcm).unwrap(); + hwp.set_channels(2).unwrap(); + hwp.set_rate(44100, ValueOr::Nearest).unwrap(); + hwp.set_format(Format::s16()).unwrap(); + hwp.set_access(Access::RWInterleaved).unwrap(); + pcm.hw_params(&hwp).unwrap(); } + + self.0 = Some(pcm); } + Ok(()) } @@ -45,7 +46,14 @@ impl Sink for AlsaSink { } fn write(&mut self, data: &[i16]) -> io::Result<()> { - self.0.as_mut().unwrap().write_interleaved(&data).unwrap(); + let pcm = self.0.as_mut().unwrap(); + let io = pcm.io_i16().unwrap(); + + match io.writei(&data) { + Ok(_) => (), + Err(err) => pcm.recover(err.code(), false).unwrap(), + } + Ok(()) } } diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs new file mode 100644 index 00000000..1f7e1092 --- /dev/null +++ b/playback/src/mixer/alsamixer.rs @@ -0,0 +1,62 @@ +use super::Mixer; +use super::AudioFilter; + +use alsa; + +#[derive(Clone)] +pub struct AlsaMixer { + name: String +} + +impl Mixer for AlsaMixer { + fn open(device: Option) -> AlsaMixer { + let name = device.unwrap_or("default".to_string()); + AlsaMixer { + name: name + } + } + + fn start(&self) { + } + + fn stop(&self) { + } + + fn volume(&self) -> u16 { + let mixer = alsa::mixer::Mixer::new(&self.name, false).unwrap(); + let selem_id = alsa::mixer::SelemId::new("Master", 0); + let selem = mixer.find_selem(&selem_id).unwrap(); + let (min, max) = selem.get_playback_volume_range(); + let volume: i64 = selem.get_playback_volume(alsa::mixer::SelemChannelId::FrontLeft).unwrap(); + + // Spotify uses a volume range from 0 to 65535, but the ALSA mixers resolution might + // differ, e.g. most ALSA mixers uses a resolution of 256. Therefore, we have to calculate + // the multiplier to use, to get the corresponding Spotify volume value from the ALSA + // mixers volume. + let resolution = max - min + 1; + let multiplier: u16 = (((0xFFFF + 1) / resolution) - 1) as u16; + + volume as u16 * multiplier + } + + fn set_volume(&self, volume: u16) { + let mixer = alsa::mixer::Mixer::new(&self.name, false).unwrap(); + let selem_id = alsa::mixer::SelemId::new("Master", 0); + let selem = mixer.find_selem(&selem_id).unwrap(); + let (min, max) = selem.get_playback_volume_range(); + + // Spotify uses a volume range from 0 to 65535, but the ALSA mixers resolution might + // differ, e.g. most ALSA mixers uses a resolution of 256. Therefore, we have to calculate + // the factor to use, to get the corresponding ALSA mixers volume value from the Spotify + // volume. + let resolution = max - min + 1; + let factor: u16 = (((0xFFFF + 1) / resolution) - 1) as u16; + let volume: i64 = (volume / factor) as i64; + + selem.set_playback_volume_all(volume).unwrap(); + } + + fn get_audio_filter(&self) -> Option> { + None + } +} diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs index a6ba34aa..34e4dd3e 100644 --- a/playback/src/mixer/mod.rs +++ b/playback/src/mixer/mod.rs @@ -1,5 +1,5 @@ pub trait Mixer: Send { - fn open() -> Self + fn open(Option) -> Self where Self: Sized; fn start(&self); @@ -15,16 +15,23 @@ pub trait AudioFilter { fn modify_stream(&self, data: &mut [i16]); } +#[cfg(feature = "alsa-backend")] +pub mod alsamixer; +#[cfg(feature = "alsa-backend")] +use self::alsamixer::AlsaMixer; + pub mod softmixer; use self::softmixer::SoftMixer; -fn mk_sink() -> Box { - Box::new(M::open()) +fn mk_sink(device: Option) -> Box { + Box::new(M::open(device)) } -pub fn find>(name: Option) -> Option Box> { +pub fn find>(name: Option) -> Option) -> Box> { match name.as_ref().map(AsRef::as_ref) { None | Some("softvol") => Some(mk_sink::), + #[cfg(feature = "alsa-backend")] + Some("alsa") => Some(mk_sink::), _ => None, } } diff --git a/playback/src/mixer/softmixer.rs b/playback/src/mixer/softmixer.rs index b197f09c..36e4c6f8 100644 --- a/playback/src/mixer/softmixer.rs +++ b/playback/src/mixer/softmixer.rs @@ -10,7 +10,7 @@ pub struct SoftMixer { } impl Mixer for SoftMixer { - fn open() -> SoftMixer { + fn open(_: Option) -> SoftMixer { SoftMixer { volume: Arc::new(AtomicUsize::new(0xFFFF)), } diff --git a/src/main.rs b/src/main.rs index 61290fb1..333095d0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -90,7 +90,7 @@ struct Setup { backend: fn(Option) -> Box, device: Option, - mixer: fn() -> Box, + mixer: fn(Option) -> Box, cache: Option, player_config: PlayerConfig, @@ -335,7 +335,7 @@ struct Main { connect_config: ConnectConfig, backend: fn(Option) -> Box, device: Option, - mixer: fn() -> Box, + mixer: fn(Option) -> Box, handle: Handle, discovery: Option, @@ -423,12 +423,13 @@ impl Future for Main { if let Async::Ready(session) = self.connect.poll().unwrap() { self.connect = Box::new(futures::future::empty()); let device = self.device.clone(); - let mixer = (self.mixer)(); + let mixer = (self.mixer)(device); let player_config = self.player_config.clone(); let connect_config = self.connect_config.clone(); let audio_filter = mixer.get_audio_filter(); let backend = self.backend; + let device = self.device.clone(); let (player, event_channel) = Player::new(player_config, session.clone(), audio_filter, move || { (backend)(device)