Browse Source

Make audio backend configurable at run time.

Paul Lietar 9 years ago
parent
commit
968a39a131
11 changed files with 179 additions and 81 deletions
  1. 4 3
      .travis.yml
  2. 11 9
      Cargo.toml
  3. 15 1
      README.md
  4. 65 0
      src/audio_backend/mod.rs
  5. 45 0
      src/audio_backend/portaudio.rs
  6. 0 57
      src/audio_sink.rs
  7. 1 1
      src/authentication/mod.rs
  8. 1 1
      src/lib.in.rs
  9. 3 1
      src/lib.rs
  10. 30 4
      src/main.rs
  11. 4 4
      src/player.rs

+ 4 - 3
.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;

+ 11 - 9
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"]

+ 15 - 1
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.

+ 65 - 0
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<S: Sink + Open + 'static>() -> Box<Sink> {
+    Box::new(S::open())
+}
+
+#[cfg(feature = "portaudio-backend")]
+mod portaudio;
+
+declare_backends! {
+    pub const BACKENDS : &'static [(&'static str, &'static (Fn() -> Box<Sink> + Sync + Send + 'static))] = &[
+        #[cfg(feature = "portaudio-backend")]
+        ("portaudio", &mk_sink::<self::portaudio::PortAudioSink>),
+    ];
+}

+ 45 - 0
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();
+    }
+}

+ 0 - 57
src/audio_sink.rs

@@ -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>;
-

+ 1 - 1
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<Credentials, ()> {
+pub fn discovery_login(_device_name: &str, _device_id: &str) -> Result<Credentials, ()> {
     Err(())
 }
 

+ 1 - 1
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;

+ 3 - 1
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

+ 30 - 4
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());
 

+ 4 - 4
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<S, F>(session: Session, sink_builder: F) -> Player
-        where S: Sink, F: FnOnce() -> S + Send + 'static {
+    pub fn new<F>(session: Session, sink_builder: F) -> Player
+        where F: FnOnce() -> Box<Sink> + 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<S: Sink>(self, sink: S) {
+    fn run(self, sink: Box<Sink>) {
         let mut decoder = None;
 
         loop {