mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Make audio backend configurable at run time.
This commit is contained in:
parent
e6dd77fc02
commit
968a39a131
11 changed files with 179 additions and 81 deletions
|
@ -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;
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
@ -59,5 +60,6 @@ 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"]
|
||||
|
|
16
README.md
16
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
src/audio_backend/mod.rs
Normal file
65
src/audio_backend/mod.rs
Normal 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>),
|
||||
];
|
||||
}
|
45
src/audio_backend/portaudio.rs
Normal file
45
src/audio_backend/portaudio.rs
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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>;
|
||||
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
34
src/main.rs
34
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());
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue