Browse Source

Make audio backend configurable at run time.

Paul Lietar 10 năm trước cách đây
mục cha
commit
968a39a131
11 tập tin đã thay đổi với 179 bổ sung81 xóa
  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 {