Make audio backend configurable at run time.

This commit is contained in:
Paul Lietar 2016-03-20 16:16:11 +00:00
parent e6dd77fc02
commit 968a39a131
11 changed files with 179 additions and 81 deletions

View file

@ -11,9 +11,10 @@ addons:
- portaudio19-dev - portaudio19-dev
script: script:
- cargo build - cargo build --no-default-features --features "with-syntex"
- cargo build --features with-tremor - cargo build --no-default-features --features "with-syntex with-tremor"
- cargo build --features facebook - 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 # Building without syntex only works on nightly
- if [[ $(rustc --version) == *"nightly"* ]]; then - if [[ $(rustc --version) == *"nightly"* ]]; then
cargo build --no-default-features; cargo build --no-default-features;

View file

@ -33,16 +33,17 @@ rustc-serialize = "~0.3.16"
tempfile = "~2.0.0" tempfile = "~2.0.0"
time = "~0.1.34" time = "~0.1.34"
url = "~0.5.2" url = "~0.5.2"
shannon = { git = "https://github.com/plietar/rust-shannon" }
vorbis = "~0.0.14" vorbis = "~0.0.14"
tremor = { git = "https://github.com/plietar/rust-tremor", optional = true }
dns-sd = { version = "~0.1.1", 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" } json_macros = { git = "https://github.com/plietar/json_macros" }
protobuf_macros = { git = "https://github.com/plietar/rust-protobuf-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 } 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" } json_macros = { git = "https://github.com/plietar/json_macros" }
[features] [features]
discovery = ["dns-sd"] discovery = ["dns-sd"]
with-syntex = ["syntex", "protobuf_macros/with-syntex", "json_macros/with-syntex"] with-syntex = ["syntex", "protobuf_macros/with-syntex", "json_macros/with-syntex"]
with-tremor = ["tremor"] with-tremor = ["tremor"]
facebook = ["hyper/ssl", "openssl"] facebook = ["hyper/ssl", "openssl"]
static-appkey = [] portaudio-backend = ["portaudio"]
default = ["with-syntex"] static-appkey = []
default = ["with-syntex"]

View file

@ -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. 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 ## Development
When developing *librespot*, it is preferable to use Rust nightly, and build it using the following : When developing *librespot*, it is preferable to use Rust nightly, and build it using the following :
```shell ```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. This produces better compilation error messages than with the default configuration.

65
src/audio_backend/mod.rs Normal file
View file

@ -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>),
];
}

View file

@ -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();
}
}

View file

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

View file

@ -168,7 +168,7 @@ mod discovery;
#[cfg(feature = "discovery")] #[cfg(feature = "discovery")]
pub use self::discovery::discovery_login; pub use self::discovery::discovery_login;
#[cfg(not(feature = "discovery"))] #[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(()) Err(())
} }

View file

@ -4,7 +4,7 @@ pub mod apresolve;
mod audio_decrypt; mod audio_decrypt;
mod audio_file; mod audio_file;
mod audio_key; mod audio_key;
pub mod audio_sink; pub mod audio_backend;
pub mod authentication; pub mod authentication;
pub mod cache; pub mod cache;
mod connection; mod connection;

View file

@ -17,7 +17,6 @@ extern crate eventual;
extern crate hyper; extern crate hyper;
extern crate lmdb_rs; extern crate lmdb_rs;
extern crate num; extern crate num;
extern crate portaudio;
extern crate protobuf; extern crate protobuf;
extern crate shannon; extern crate shannon;
extern crate rand; extern crate rand;
@ -37,6 +36,9 @@ extern crate dns_sd;
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
extern crate openssl; extern crate openssl;
#[cfg(feature = "portaudio")]
extern crate portaudio;
extern crate librespot_protocol as protocol; extern crate librespot_protocol as protocol;
// This doesn't play nice with syntex, so place it here // This doesn't play nice with syntex, so place it here

View file

@ -9,7 +9,7 @@ use std::io::{stdout, Read, Write};
use std::path::PathBuf; use std::path::PathBuf;
use std::thread; use std::thread;
use librespot::audio_sink::DefaultSink; use librespot::audio_backend::BACKENDS;
use librespot::authentication::{Credentials, facebook_login, discovery_login}; use librespot::authentication::{Credentials, facebook_login, discovery_login};
use librespot::cache::{Cache, DefaultCache, NoCache}; use librespot::cache::{Cache, DefaultCache, NoCache};
use librespot::player::Player; use librespot::player::Player;
@ -43,7 +43,8 @@ fn main() {
.optopt("p", "password", "Password", "PASSWORD") .optopt("p", "password", "Password", "PASSWORD")
.optopt("c", "cache", "Path to a directory where files will be cached.", "CACHE") .optopt("c", "cache", "Path to a directory where files will be cached.", "CACHE")
.reqopt("n", "name", "Device name", "NAME") .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() { if APPKEY.is_none() {
opts.reqopt("a", "appkey", "Path to a spotify appkey", "APPKEY"); 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 appkey = matches.opt_str("a").map(|appkey_path| {
let mut file = File::open(appkey_path) let mut file = File::open(appkey_path)
.expect("Could not open app key."); .expect("Could not open app key.");
@ -96,6 +118,8 @@ fn main() {
bitrate: bitrate, bitrate: bitrate,
}; };
let stored_credentials = cache.get_credentials();
let session = Session::new(config, cache); let session = Session::new(config, cache);
let credentials = username.map(|username| { let credentials = username.map(|username| {
@ -114,7 +138,8 @@ fn main() {
} else { } else {
None None
} }
}).or_else(|| { }).or(stored_credentials)
.or_else(|| {
if cfg!(feature = "discovery") { if cfg!(feature = "discovery") {
println!("No username provided and no stored credentials, starting discovery ..."); println!("No username provided and no stored credentials, starting discovery ...");
Some(discovery_login(&session.config().device_name, Some(discovery_login(&session.config().device_name,
@ -129,7 +154,8 @@ fn main() {
let reusable_credentials = session.login(credentials).unwrap(); let reusable_credentials = session.login(credentials).unwrap();
session.cache().put_credentials(&reusable_credentials); 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); let spirc = SpircManager::new(session.clone(), player);
thread::spawn(move || spirc.run()); thread::spawn(move || spirc.run());

View file

@ -6,7 +6,7 @@ use std::io::{Read, Seek};
use vorbis; use vorbis;
use audio_decrypt::AudioDecrypt; use audio_decrypt::AudioDecrypt;
use audio_sink::Sink; use audio_backend::Sink;
use metadata::{FileFormat, Track, TrackRef}; use metadata::{FileFormat, Track, TrackRef};
use session::{Bitrate, Session}; use session::{Bitrate, Session};
use util::{self, SpotifyId, Subfile}; use util::{self, SpotifyId, Subfile};
@ -71,8 +71,8 @@ enum PlayerCommand {
} }
impl Player { impl Player {
pub fn new<S, F>(session: Session, sink_builder: F) -> Player pub fn new<F>(session: Session, sink_builder: F) -> Player
where S: Sink, F: FnOnce() -> S + Send + 'static { where F: FnOnce() -> Box<Sink> + Send + 'static {
let (cmd_tx, cmd_rx) = mpsc::channel(); let (cmd_tx, cmd_rx) = mpsc::channel();
let state = Arc::new(Mutex::new(PlayerState { let state = Arc::new(Mutex::new(PlayerState {
@ -155,7 +155,7 @@ fn apply_volume(volume: u16, data: &[i16]) -> Cow<[i16]> {
} }
impl PlayerInternal { impl PlayerInternal {
fn run<S: Sink>(self, sink: S) { fn run(self, sink: Box<Sink>) {
let mut decoder = None; let mut decoder = None;
loop { loop {