diff --git a/.travis.yml b/.travis.yml index dc0293a5..bdbfd22f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,9 +11,10 @@ addons: - portaudio19-dev script: - - cargo build - - cargo build --features with-tremor - - cargo build --features facebook + - cargo build --no-default-features --features "with-syntex" + - cargo build --no-default-features --features "with-syntex with-tremor" + - cargo build --no-default-features --features "with-syntex facebook" + - cargo build --no-default-features --features "with-syntex portaudio-backend" # Building without syntex only works on nightly - if [[ $(rustc --version) == *"nightly"* ]]; then cargo build --no-default-features; diff --git a/Cargo.toml b/Cargo.toml index 26b61527..9dda1a25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,16 +33,17 @@ rustc-serialize = "~0.3.16" tempfile = "~2.0.0" time = "~0.1.34" url = "~0.5.2" +shannon = { git = "https://github.com/plietar/rust-shannon" } + vorbis = "~0.0.14" +tremor = { git = "https://github.com/plietar/rust-tremor", optional = true } dns-sd = { version = "~0.1.1", optional = true } -portaudio = { git = "https://github.com/mvdnes/portaudio-rs" } +portaudio = { git = "https://github.com/mvdnes/portaudio-rs", optional = true } json_macros = { git = "https://github.com/plietar/json_macros" } protobuf_macros = { git = "https://github.com/plietar/rust-protobuf-macros" } -shannon = { git = "https://github.com/plietar/rust-shannon" } -tremor = { git = "https://github.com/plietar/rust-tremor", optional = true } clippy = { version = "*", optional = true } @@ -55,9 +56,10 @@ protobuf_macros = { git = "https://github.com/plietar/rust-protobuf-macros" } json_macros = { git = "https://github.com/plietar/json_macros" } [features] -discovery = ["dns-sd"] -with-syntex = ["syntex", "protobuf_macros/with-syntex", "json_macros/with-syntex"] -with-tremor = ["tremor"] -facebook = ["hyper/ssl", "openssl"] -static-appkey = [] -default = ["with-syntex"] +discovery = ["dns-sd"] +with-syntex = ["syntex", "protobuf_macros/with-syntex", "json_macros/with-syntex"] +with-tremor = ["tremor"] +facebook = ["hyper/ssl", "openssl"] +portaudio-backend = ["portaudio"] +static-appkey = [] +default = ["with-syntex"] diff --git a/README.md b/README.md index cd2b4e3c..5b93de64 100644 --- a/README.md +++ b/README.md @@ -63,10 +63,24 @@ target/release/librespot --appkey APPKEY --cache CACHEDIR --name DEVICENAME --fa This will print a link to the console, which must be visited on the same computer *librespot* is running on. +## Audio Backends +*librespot* supports various audio backends. Multiple backends can be enabled at compile time by enabling the +corresponding cargo feature. By default, only PortAudio is enabled. + +A specific backend can selected at runtime using the `--backend` switch. + +```shell +cargo build --features portaudio-backend +target/release/librespot [...] --backend portaudio +``` + +The following backends are currently available : +- PortAudio + ## Development When developing *librespot*, it is preferable to use Rust nightly, and build it using the following : ```shell -cargo build --no-default-features +cargo build --no-default-features --features portaudio-backend ``` This produces better compilation error messages than with the default configuration. diff --git a/src/audio_backend/mod.rs b/src/audio_backend/mod.rs new file mode 100644 index 00000000..93150d1c --- /dev/null +++ b/src/audio_backend/mod.rs @@ -0,0 +1,65 @@ +use std::io; + +pub trait Open { + fn open() -> Self; +} + +pub trait Sink { + fn start(&self) -> io::Result<()>; + fn stop(&self) -> io::Result<()>; + fn write(&self, data: &[i16]) -> io::Result<()>; +} + +/* + * Allow #[cfg] rules around elements of a list. + * Workaround until stmt_expr_attributes is stable. + * + * This generates 2^n declarations of the list, with every combination possible + */ +macro_rules! declare_backends { + (pub const $name:ident : $ty:ty = & [ $($tt:tt)* ];) => ( + _declare_backends!($name ; $ty ; []; []; []; $($tt)*); + ); +} + +macro_rules! _declare_backends { + ($name:ident ; $ty:ty ; [ $($yes:meta,)* ] ; [ $($no:meta,)* ] ; [ $($exprs:expr,)* ] ; #[cfg($m:meta)] $e:expr, $($rest:tt)* ) => ( + _declare_backends!($name ; $ty ; [ $m, $($yes,)* ] ; [ $($no,)* ] ; [ $($exprs,)* $e, ] ; $($rest)*); + _declare_backends!($name ; $ty ; [ $($yes,)* ] ; [ $m, $($no,)* ] ; [ $($exprs,)* ] ; $($rest)*); + ); + + ($name:ident ; $ty:ty ; [ $($yes:meta,)* ] ; [ $($no:meta,)* ] ; [ $($exprs:expr,)* ] ; $e:expr, $($rest:tt)*) => ( + _declare_backends!($name ; $ty ; [ $($yes,)* ] ; [ $($no,)* ] ; [ $($exprs,)* $e, ] ; $($rest)*); + ); + + ($name:ident ; $ty:ty ; [ $($yes:meta,)* ] ; [ $($no:meta,)* ] ; [ $($exprs:expr,)* ] ; #[cfg($m:meta)] $e:expr) => ( + _declare_backends!($name ; $ty ; [ $m, $($yes,)* ] ; [ $($no,)* ] ; [ $($exprs,)* $e, ] ; ); + _declare_backends!($name ; $ty ; [ $($yes,)* ] ; [ $m, $($no,)* ] ; [ $($exprs,)* ] ; ); + ); + + ($name:ident ; $ty:ty ; [ $($yes:meta,)* ] ; [ $($no:meta,)* ] ; [ $($exprs:expr,)* ] ; $e:expr ) => ( + _declare_backends!($name ; $ty ; [ $($yes,)* ] ; [ $($no,)* ] ; [ $($exprs,)* $e, ] ; ); + ); + + ($name:ident ; $ty:ty ; [ $($yes:meta,)* ] ; [ $($no:meta,)* ] ; [ $($exprs:expr,)* ] ; ) => ( + #[cfg(all($($yes,)* not(any($($no),*))))] + pub const $name : $ty = &[ + $($exprs,)* + ]; + ) +} + +#[allow(dead_code)] +fn mk_sink() -> Box { + Box::new(S::open()) +} + +#[cfg(feature = "portaudio-backend")] +mod portaudio; + +declare_backends! { + pub const BACKENDS : &'static [(&'static str, &'static (Fn() -> Box + Sync + Send + 'static))] = &[ + #[cfg(feature = "portaudio-backend")] + ("portaudio", &mk_sink::), + ]; +} diff --git a/src/audio_backend/portaudio.rs b/src/audio_backend/portaudio.rs new file mode 100644 index 00000000..a3c6c1bd --- /dev/null +++ b/src/audio_backend/portaudio.rs @@ -0,0 +1,45 @@ +use super::{Open, Sink}; +use std::io; +use portaudio; + +pub struct PortAudioSink<'a>(portaudio::stream::Stream<'a, i16, i16>); + +impl <'a> Open for PortAudioSink<'a> { + fn open() -> PortAudioSink<'a> { + portaudio::initialize().unwrap(); + + let stream = portaudio::stream::Stream::open_default( + 0, 2, 44100.0, + portaudio::stream::FRAMES_PER_BUFFER_UNSPECIFIED, + None + ).unwrap(); + + PortAudioSink(stream) + } +} + +impl <'a> Sink for PortAudioSink<'a> { + fn start(&self) -> io::Result<()> { + self.0.start().unwrap(); + Ok(()) + } + fn stop(&self) -> io::Result<()> { + self.0.stop().unwrap(); + Ok(()) + } + fn write(&self, data: &[i16]) -> io::Result<()> { + match self.0.write(&data) { + Ok(_) => (), + Err(portaudio::PaError::OutputUnderflowed) => eprintln!("Underflow"), + Err(e) => panic!("PA Error {}", e), + }; + + Ok(()) + } +} + +impl <'a> Drop for PortAudioSink<'a> { + fn drop(&mut self) { + portaudio::terminate().unwrap(); + } +} diff --git a/src/audio_sink.rs b/src/audio_sink.rs deleted file mode 100644 index 14d4492f..00000000 --- a/src/audio_sink.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::io; - -pub trait Sink { - fn start(&self) -> io::Result<()>; - fn stop(&self) -> io::Result<()>; - fn write(&self, data: &[i16]) -> io::Result<()>; -} - -mod portaudio_sink { - use audio_sink::Sink; - use std::io; - use portaudio; - pub struct PortAudioSink<'a>(portaudio::stream::Stream<'a, i16, i16>); - - impl <'a> PortAudioSink<'a> { - pub fn open() -> PortAudioSink<'a> { - portaudio::initialize().unwrap(); - - let stream = portaudio::stream::Stream::open_default( - 0, 2, 44100.0, - portaudio::stream::FRAMES_PER_BUFFER_UNSPECIFIED, - None - ).unwrap(); - - PortAudioSink(stream) - } - } - - impl <'a> Sink for PortAudioSink<'a> { - fn start(&self) -> io::Result<()> { - self.0.start().unwrap(); - Ok(()) - } - fn stop(&self) -> io::Result<()> { - self.0.stop().unwrap(); - Ok(()) - } - fn write(&self, data: &[i16]) -> io::Result<()> { - match self.0.write(&data) { - Ok(_) => (), - Err(portaudio::PaError::OutputUnderflowed) => eprintln!("Underflow"), - Err(e) => panic!("PA Error {}", e), - }; - - Ok(()) - } - } - - impl <'a> Drop for PortAudioSink<'a> { - fn drop(&mut self) { - portaudio::terminate().unwrap(); - } - } -} - -pub type DefaultSink = portaudio_sink::PortAudioSink<'static>; - diff --git a/src/authentication/mod.rs b/src/authentication/mod.rs index 575fd869..f31d073e 100644 --- a/src/authentication/mod.rs +++ b/src/authentication/mod.rs @@ -168,7 +168,7 @@ mod discovery; #[cfg(feature = "discovery")] pub use self::discovery::discovery_login; #[cfg(not(feature = "discovery"))] -pub fn discovery_login(device_name: &str, device_id: &str) -> Result { +pub fn discovery_login(_device_name: &str, _device_id: &str) -> Result { Err(()) } diff --git a/src/lib.in.rs b/src/lib.in.rs index e98487aa..c419b7f8 100644 --- a/src/lib.in.rs +++ b/src/lib.in.rs @@ -4,7 +4,7 @@ pub mod apresolve; mod audio_decrypt; mod audio_file; mod audio_key; -pub mod audio_sink; +pub mod audio_backend; pub mod authentication; pub mod cache; mod connection; diff --git a/src/lib.rs b/src/lib.rs index b397dcd8..6a4185f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,6 @@ extern crate eventual; extern crate hyper; extern crate lmdb_rs; extern crate num; -extern crate portaudio; extern crate protobuf; extern crate shannon; extern crate rand; @@ -37,6 +36,9 @@ extern crate dns_sd; #[cfg(feature = "openssl")] extern crate openssl; +#[cfg(feature = "portaudio")] +extern crate portaudio; + extern crate librespot_protocol as protocol; // This doesn't play nice with syntex, so place it here diff --git a/src/main.rs b/src/main.rs index d5916b37..82875ba6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,7 @@ use std::io::{stdout, Read, Write}; use std::path::PathBuf; use std::thread; -use librespot::audio_sink::DefaultSink; +use librespot::audio_backend::BACKENDS; use librespot::authentication::{Credentials, facebook_login, discovery_login}; use librespot::cache::{Cache, DefaultCache, NoCache}; use librespot::player::Player; @@ -43,7 +43,8 @@ fn main() { .optopt("p", "password", "Password", "PASSWORD") .optopt("c", "cache", "Path to a directory where files will be cached.", "CACHE") .reqopt("n", "name", "Device name", "NAME") - .optopt("b", "bitrate", "Bitrate (96, 160 or 320). Defaults to 160", "BITRATE"); + .optopt("b", "bitrate", "Bitrate (96, 160 or 320). Defaults to 160", "BITRATE") + .optopt("", "backend", "Audio backend to use. Use '?' to list options", "BACKEND"); if APPKEY.is_none() { opts.reqopt("a", "appkey", "Path to a spotify appkey", "APPKEY"); @@ -63,6 +64,27 @@ fn main() { } }; + let make_backend = match matches.opt_str("backend").as_ref().map(AsRef::as_ref) { + Some("?") => { + println!("Available Backends : "); + for (&(name, _), idx) in BACKENDS.iter().zip(0..) { + if idx == 0 { + println!("- {} (default)", name); + } else { + println!("- {}", name); + } + } + + return; + }, + Some(name) => { + BACKENDS.iter().find(|backend| name == backend.0).expect("Unknown backend").1 + }, + None => { + BACKENDS.first().expect("No backends were enabled at build time").1 + } + }; + let appkey = matches.opt_str("a").map(|appkey_path| { let mut file = File::open(appkey_path) .expect("Could not open app key."); @@ -96,6 +118,8 @@ fn main() { bitrate: bitrate, }; + let stored_credentials = cache.get_credentials(); + let session = Session::new(config, cache); let credentials = username.map(|username| { @@ -114,7 +138,8 @@ fn main() { } else { None } - }).or_else(|| { + }).or(stored_credentials) + .or_else(|| { if cfg!(feature = "discovery") { println!("No username provided and no stored credentials, starting discovery ..."); Some(discovery_login(&session.config().device_name, @@ -129,7 +154,8 @@ fn main() { let reusable_credentials = session.login(credentials).unwrap(); session.cache().put_credentials(&reusable_credentials); - let player = Player::new(session.clone(), || DefaultSink::open()); + let player = Player::new(session.clone(), move || make_backend()); + let spirc = SpircManager::new(session.clone(), player); thread::spawn(move || spirc.run()); diff --git a/src/player.rs b/src/player.rs index 8cc326cc..781c3b4c 100644 --- a/src/player.rs +++ b/src/player.rs @@ -6,7 +6,7 @@ use std::io::{Read, Seek}; use vorbis; use audio_decrypt::AudioDecrypt; -use audio_sink::Sink; +use audio_backend::Sink; use metadata::{FileFormat, Track, TrackRef}; use session::{Bitrate, Session}; use util::{self, SpotifyId, Subfile}; @@ -71,8 +71,8 @@ enum PlayerCommand { } impl Player { - pub fn new(session: Session, sink_builder: F) -> Player - where S: Sink, F: FnOnce() -> S + Send + 'static { + pub fn new(session: Session, sink_builder: F) -> Player + where F: FnOnce() -> Box + Send + 'static { let (cmd_tx, cmd_rx) = mpsc::channel(); let state = Arc::new(Mutex::new(PlayerState { @@ -155,7 +155,7 @@ fn apply_volume(volume: u16, data: &[i16]) -> Cow<[i16]> { } impl PlayerInternal { - fn run(self, sink: S) { + fn run(self, sink: Box) { let mut decoder = None; loop {