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
|
- 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;
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
||||||
|
@ -59,5 +60,6 @@ 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"]
|
||||||
|
portaudio-backend = ["portaudio"]
|
||||||
static-appkey = []
|
static-appkey = []
|
||||||
default = ["with-syntex"]
|
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.
|
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
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")]
|
#[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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
34
src/main.rs
34
src/main.rs
|
@ -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());
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue