Merge remote-tracking branch 'origin/master'

This commit is contained in:
Paul Lietar 2017-02-21 22:46:19 +00:00
commit 2708aa4fef
13 changed files with 220 additions and 77 deletions

1
.dockerignore Normal file
View file

@ -0,0 +1 @@
target

View file

@ -16,6 +16,7 @@ path = "src/lib.rs"
[[bin]] [[bin]]
name = "librespot" name = "librespot"
path = "src/main.rs" path = "src/main.rs"
doc = false
[dependencies.librespot-protocol] [dependencies.librespot-protocol]
path = "protocol" path = "protocol"
@ -86,5 +87,5 @@ section = "sound"
priority = "optional" priority = "optional"
assets = [ assets = [
["target/release/librespot", "usr/bin/", "755"], ["target/release/librespot", "usr/bin/", "755"],
["assets/librespot.service", "lib/systemd/system/", "644"] ["contrib/librespot.service", "lib/systemd/system/", "644"]
] ]

View file

@ -4,10 +4,6 @@ applications to use Spotify's service, without using the official but
closed-source libspotify. Additionally, it will provide extra features closed-source libspotify. Additionally, it will provide extra features
which are not available in the official library. which are not available in the official library.
## Status
*librespot* is currently under development and is not fully functional yet. You
are however welcome to experiment with it.
## Building ## Building
Rust 1.7.0 or later is required to build librespot. Rust 1.7.0 or later is required to build librespot.
@ -60,6 +56,36 @@ The following backends are currently available :
- PortAudio - PortAudio
- PulseAudio - PulseAudio
## Cross-compiling
A cross compilation environment is provided as a docker image.
Build the image from the root of the project with the following command :
```
$ docker build -t librespot-cross -f contrib/Dockerfile .
```
The resulting image can be used to build librespot for linux x86_64, armhf and armel.
The compiled binaries will be located in /tmp/librespot-build
```
docker run -v /tmp/librespot-build:/build librespot-cross
```
If only one architecture is desired, cargo can be invoked directly with the appropriate options :
```shell
docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --no-default-features --features "with-syntex alsa-backend"
docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "with-syntex alsa-backend"
docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "with-syntex alsa-backend"
```
## Development
When developing *librespot*, it is preferable to use Rust nightly, and build it using the following :
```shell
cargo build --no-default-features --features "nightly portaudio-backend"
```
This produces better compilation error messages than with the default configuration.
## Disclaimer ## Disclaimer
Using this code to connect to Spotify's API is probably forbidden by them. Using this code to connect to Spotify's API is probably forbidden by them.
Use at your own risk. Use at your own risk.

40
contrib/Dockerfile Normal file
View file

@ -0,0 +1,40 @@
# Cross compilation environment for librespot
# Build the docker image from the root of the project with the following command :
# $ docker build -t librespot-cross -f contrib/Dockerfile .
#
# The resulting image can be used to build librespot for linux x86_64, armhf and armel.
# $ docker run -v /tmp/librespot-build:/build librespot-cross
#
# The compiled binaries will be located in /tmp/librespot-build
#
# If only one architecture is desired, cargo can be invoked directly with the appropriate options :
# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --no-default-features --features "with-syntex alsa-backend"
# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "with-syntex alsa-backend"
# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "with-syntex alsa-backend"
#
FROM debian:stretch
RUN dpkg --add-architecture armhf
RUN dpkg --add-architecture armel
RUN apt-get update
RUN apt-get install -y curl build-essential crossbuild-essential-armhf crossbuild-essential-armel
RUN apt-get install -y libasound2-dev libasound2-dev:armhf libasound2-dev:armel
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
ENV PATH="/root/.cargo/bin/:${PATH}"
RUN rustup target add arm-unknown-linux-gnueabi
RUN rustup target add arm-unknown-linux-gnueabihf
RUN mkdir /.cargo && \
echo '[target.arm-unknown-linux-gnueabihf]\nlinker = "arm-linux-gnueabihf-gcc"' > /.cargo/config && \
echo '[target.arm-unknown-linux-gnueabi]\nlinker = "arm-linux-gnueabi-gcc"' >> /.cargo/config
RUN mkdir /build
ENV CARGO_TARGET_DIR /build
ENV CARGO_HOME /build/cache
ADD . /src
WORKDIR /src
CMD ["/src/contrib/docker-build.sh"]

6
contrib/docker-build.sh Executable file
View file

@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -eux
cargo build --release --no-default-features --features "with-syntex alsa-backend"
cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "with-syntex alsa-backend"
cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "with-syntex alsa-backend"

View file

@ -73,20 +73,19 @@ use self::pipe::StdoutSink;
declare_backends! { declare_backends! {
pub const BACKENDS : &'static [ pub const BACKENDS : &'static [
(&'static str, (&'static str, fn(Option<String>) -> Box<Sink>)
&'static (Fn(Option<String>) -> Box<Sink> + Sync + Send + 'static))
] = &[ ] = &[
#[cfg(feature = "alsa-backend")] #[cfg(feature = "alsa-backend")]
("alsa", &mk_sink::<AlsaSink>), ("alsa", mk_sink::<AlsaSink>),
#[cfg(feature = "portaudio-backend")] #[cfg(feature = "portaudio-backend")]
("portaudio", &mk_sink::<PortAudioSink>), ("portaudio", mk_sink::<PortAudioSink>),
#[cfg(feature = "pulseaudio-backend")] #[cfg(feature = "pulseaudio-backend")]
("pulseaudio", &mk_sink::<PulseAudioSink>), ("pulseaudio", mk_sink::<PulseAudioSink>),
("pipe", &mk_sink::<StdoutSink>), ("pipe", mk_sink::<StdoutSink>),
]; ];
} }
pub fn find<T: AsRef<str>>(name: Option<T>) -> Option<&'static (Fn(Option<String>) -> Box<Sink> + Send + Sync)> { pub fn find<T: AsRef<str>>(name: Option<T>) -> Option<fn(Option<String>) -> Box<Sink>> {
if let Some(name) = name.as_ref().map(AsRef::as_ref) { if let Some(name) = name.as_ref().map(AsRef::as_ref) {
BACKENDS.iter().find(|backend| name == backend.0).map(|backend| backend.1) BACKENDS.iter().find(|backend| name == backend.0).map(|backend| backend.1)
} else { } else {

View file

@ -64,6 +64,7 @@ pub mod player;
pub mod session; pub mod session;
pub mod util; pub mod util;
pub mod version; pub mod version;
pub mod mixer;
include!(concat!(env!("OUT_DIR"), "/lib.rs")); include!(concat!(env!("OUT_DIR"), "/lib.rs"));

View file

@ -21,6 +21,8 @@ use librespot::audio_backend::{self, Sink, BACKENDS};
use librespot::cache::Cache; use librespot::cache::Cache;
use librespot::player::Player; use librespot::player::Player;
use librespot::session::{Bitrate, Config, Session}; use librespot::session::{Bitrate, Config, Session};
use librespot::mixer::{self, Mixer};
use librespot::version; use librespot::version;
fn usage(program: &str, opts: &getopts::Options) -> String { fn usage(program: &str, opts: &getopts::Options) -> String {
@ -62,9 +64,9 @@ fn list_backends() {
} }
} }
#[derive(Clone)]
struct Setup { struct Setup {
backend: &'static (Fn(Option<String>) -> Box<Sink> + Send + Sync), backend: fn(Option<String>) -> Box<Sink>,
mixer: Box<Mixer + Send>,
cache: Option<Cache>, cache: Option<Cache>,
config: Config, config: Config,
credentials: Credentials, credentials: Credentials,
@ -82,7 +84,8 @@ fn setup(args: &[String]) -> Setup {
.optopt("u", "username", "Username to sign in with", "USERNAME") .optopt("u", "username", "Username to sign in with", "USERNAME")
.optopt("p", "password", "Password", "PASSWORD") .optopt("p", "password", "Password", "PASSWORD")
.optopt("", "backend", "Audio backend to use. Use '?' to list options", "BACKEND") .optopt("", "backend", "Audio backend to use. Use '?' to list options", "BACKEND")
.optopt("", "device", "Audio device to use. Use '?' to list options", "DEVICE"); .optopt("", "device", "Audio device to use. Use '?' to list options", "DEVICE")
.optopt("", "mixer", "Mixer to use", "MIXER");
let matches = match opts.parse(&args[1..]) { let matches = match opts.parse(&args[1..]) {
Ok(m) => m, Ok(m) => m,
@ -109,6 +112,10 @@ fn setup(args: &[String]) -> Setup {
let backend = audio_backend::find(backend_name.as_ref()) let backend = audio_backend::find(backend_name.as_ref())
.expect("Invalid backend"); .expect("Invalid backend");
let mixer_name = matches.opt_str("mixer");
let mixer = mixer::find(mixer_name.as_ref())
.expect("Invalid mixer");
let bitrate = matches.opt_str("b").as_ref() let bitrate = matches.opt_str("b").as_ref()
.map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate")) .map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate"))
.unwrap_or(Bitrate::Bitrate160); .unwrap_or(Bitrate::Bitrate160);
@ -140,6 +147,7 @@ fn setup(args: &[String]) -> Setup {
Setup { Setup {
backend: backend, backend: backend,
mixer: mixer,
cache: cache, cache: cache,
config: config, config: config,
credentials: credentials, credentials: credentials,
@ -153,16 +161,18 @@ fn main() {
let args: Vec<String> = std::env::args().collect(); let args: Vec<String> = std::env::args().collect();
let Setup { backend, cache, config, credentials, device } = setup(&args); let Setup { backend, mixer, cache, config, credentials, device }
= setup(&args);
let connection = Session::connect(config, credentials, cache, handle); let connection = Session::connect(config, credentials, cache, handle);
let task = connection.and_then(move |session| { let task = connection.and_then(move |session| {
let player = Player::new(session.clone(), move || { let audio_filter = mixer.get_audio_filter();
let player = Player::new(session.clone(), audio_filter, move || {
(backend)(device) (backend)(device)
}); });
let (spirc, task) = Spirc::new(session.clone(), player); let (spirc, task) = Spirc::new(session.clone(), player, mixer);
let spirc = ::std::cell::RefCell::new(spirc); let spirc = ::std::cell::RefCell::new(spirc);
ctrlc::set_handler(move || { ctrlc::set_handler(move || {

23
src/mixer/mod.rs Normal file
View file

@ -0,0 +1,23 @@
pub trait Mixer {
fn start(&self);
fn stop(&self);
fn set_volume(&self, volume: u16);
fn volume(&self) -> u16;
fn get_audio_filter(&self) -> Option<Box<AudioFilter + Send>> {
None
}
}
pub trait AudioFilter {
fn modify_stream(&self, data: &mut [i16]);
}
pub mod softmixer;
use self::softmixer::SoftMixer;
pub fn find<T: AsRef<str>>(name: Option<T>) -> Option<Box<Mixer + Send>> {
match name.as_ref().map(AsRef::as_ref) {
None | Some("softvol") => Some(Box::new(SoftMixer::new())),
_ => None,
}
}

48
src/mixer/softmixer.rs Normal file
View file

@ -0,0 +1,48 @@
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use super::Mixer;
use super::AudioFilter;
pub struct SoftMixer {
volume: Arc<AtomicUsize>
}
impl SoftMixer {
pub fn new() -> SoftMixer {
SoftMixer {
volume: Arc::new(AtomicUsize::new(0xFFFF))
}
}
}
impl Mixer for SoftMixer {
fn start(&self) {
}
fn stop(&self) {
}
fn volume(&self) -> u16 {
self.volume.load(Ordering::Relaxed) as u16
}
fn set_volume(&self, volume: u16) {
self.volume.store(volume as usize, Ordering::Relaxed);
}
fn get_audio_filter(&self) -> Option<Box<AudioFilter + Send>> {
Some(Box::new(SoftVolumeApplier { volume: self.volume.clone() }))
}
}
struct SoftVolumeApplier {
volume: Arc<AtomicUsize>
}
impl AudioFilter for SoftVolumeApplier {
fn modify_stream(&self, data: &mut [i16]) {
let volume = self.volume.load(Ordering::Relaxed) as u16;
if volume != 0xFFFF {
for x in data.iter_mut() {
*x = (*x as i32 * volume as i32 / 0xFFFF) as i16;
}
}
}
}

View file

@ -13,6 +13,7 @@ use audio_decrypt::AudioDecrypt;
use audio_file::AudioFile; use audio_file::AudioFile;
use metadata::{FileFormat, Track}; use metadata::{FileFormat, Track};
use session::{Bitrate, Session}; use session::{Bitrate, Session};
use mixer::AudioFilter;
use util::{self, SpotifyId, Subfile}; use util::{self, SpotifyId, Subfile};
#[derive(Clone)] #[derive(Clone)]
@ -25,21 +26,20 @@ struct PlayerInternal {
commands: std::sync::mpsc::Receiver<PlayerCommand>, commands: std::sync::mpsc::Receiver<PlayerCommand>,
state: PlayerState, state: PlayerState,
volume: u16,
sink: Box<Sink>, sink: Box<Sink>,
audio_filter: Option<Box<AudioFilter + Send>>,
} }
enum PlayerCommand { enum PlayerCommand {
Load(SpotifyId, bool, u32, oneshot::Sender<()>), Load(SpotifyId, bool, u32, oneshot::Sender<()>),
Play, Play,
Pause, Pause,
Volume(u16),
Stop, Stop,
Seek(u32), Seek(u32),
} }
impl Player { impl Player {
pub fn new<F>(session: Session, sink_builder: F) -> Player pub fn new<F>(session: Session, audio_filter: Option<Box<AudioFilter + Send>>, sink_builder: F) -> Player
where F: FnOnce() -> Box<Sink> + Send + 'static { where F: FnOnce() -> Box<Sink> + Send + 'static {
let (cmd_tx, cmd_rx) = std::sync::mpsc::channel(); let (cmd_tx, cmd_rx) = std::sync::mpsc::channel();
@ -49,8 +49,8 @@ impl Player {
commands: cmd_rx, commands: cmd_rx,
state: PlayerState::Stopped, state: PlayerState::Stopped,
volume: 0xFFFF,
sink: sink_builder(), sink: sink_builder(),
audio_filter: audio_filter,
}; };
internal.run(); internal.run();
@ -89,10 +89,6 @@ impl Player {
pub fn seek(&self, position_ms: u32) { pub fn seek(&self, position_ms: u32) {
self.command(PlayerCommand::Seek(position_ms)); self.command(PlayerCommand::Seek(position_ms));
} }
pub fn volume(&self, vol: u16) {
self.command(PlayerCommand::Volume(vol));
}
} }
type Decoder = vorbis::Decoder<Subfile<AudioDecrypt<AudioFile>>>; type Decoder = vorbis::Decoder<Subfile<AudioDecrypt<AudioFile>>>;
@ -203,11 +199,9 @@ impl PlayerInternal {
fn handle_packet(&mut self, packet: Option<Result<vorbis::Packet, VorbisError>>) { fn handle_packet(&mut self, packet: Option<Result<vorbis::Packet, VorbisError>>) {
match packet { match packet {
Some(Ok(mut packet)) => { Some(Ok(mut packet)) => {
if self.volume < 0xFFFF { if let Some(ref editor) = self.audio_filter {
for x in &mut packet.data { editor.modify_stream(&mut packet.data)
*x = (*x as i32 * self.volume as i32 / 0xFFFF) as i16; };
}
}
self.sink.write(&packet.data).unwrap(); self.sink.write(&packet.data).unwrap();
} }
@ -313,10 +307,6 @@ impl PlayerInternal {
PlayerState::Invalid => panic!("invalid state"), PlayerState::Invalid => panic!("invalid state"),
} }
} }
PlayerCommand::Volume(vol) => {
self.volume = vol;
}
} }
} }
@ -419,11 +409,6 @@ impl ::std::fmt::Debug for PlayerCommand {
PlayerCommand::Pause => { PlayerCommand::Pause => {
f.debug_tuple("Pause").finish() f.debug_tuple("Pause").finish()
} }
PlayerCommand::Volume(volume) => {
f.debug_tuple("Volume")
.field(&volume)
.finish()
}
PlayerCommand::Stop => { PlayerCommand::Stop => {
f.debug_tuple("Stop").finish() f.debug_tuple("Stop").finish()
} }

View file

@ -7,6 +7,7 @@ use protobuf::{self, Message};
use mercury::MercuryError; use mercury::MercuryError;
use player::Player; use player::Player;
use mixer::Mixer;
use session::Session; use session::Session;
use util::{now_ms, SpotifyId, SeqGenerator}; use util::{now_ms, SpotifyId, SeqGenerator};
use version; use version;
@ -16,6 +17,7 @@ use protocol::spirc::{PlayStatus, State, MessageType, Frame, DeviceState};
pub struct SpircTask { pub struct SpircTask {
player: Player, player: Player,
mixer: Box<Mixer + Send>,
sequence: SeqGenerator<u32>, sequence: SeqGenerator<u32>,
@ -43,7 +45,6 @@ fn initial_state() -> State {
protobuf_init!(protocol::spirc::State::new(), { protobuf_init!(protocol::spirc::State::new(), {
repeat: false, repeat: false,
shuffle: false, shuffle: false,
status: PlayStatus::kPlayStatusStop, status: PlayStatus::kPlayStatusStop,
position_ms: 0, position_ms: 0,
position_measured_at: 0, position_measured_at: 0,
@ -109,7 +110,9 @@ fn initial_device_state(name: String, volume: u16) -> DeviceState {
} }
impl Spirc { impl Spirc {
pub fn new(session: Session, player: Player) -> (Spirc, SpircTask) { pub fn new(session: Session, player: Player, mixer: Box<Mixer + Send>)
-> (Spirc, SpircTask)
{
let ident = session.device_id().to_owned(); let ident = session.device_id().to_owned();
let name = session.config().name.clone(); let name = session.config().name.clone();
@ -130,10 +133,11 @@ impl Spirc {
let volume = 0xFFFF; let volume = 0xFFFF;
let device = initial_device_state(name, volume); let device = initial_device_state(name, volume);
player.volume(volume); mixer.set_volume(volume);
let mut task = SpircTask { let mut task = SpircTask {
player: player, player: player,
mixer: mixer,
sequence: SeqGenerator::new(1), sequence: SeqGenerator::new(1),
@ -269,6 +273,7 @@ impl SpircTask {
MessageType::kMessageTypePlay => { MessageType::kMessageTypePlay => {
if self.state.get_status() == PlayStatus::kPlayStatusPause { if self.state.get_status() == PlayStatus::kPlayStatusPause {
self.mixer.start();
self.player.play(); self.player.play();
self.state.set_status(PlayStatus::kPlayStatusPlay); self.state.set_status(PlayStatus::kPlayStatusPlay);
self.state.set_position_measured_at(now_ms() as u64); self.state.set_position_measured_at(now_ms() as u64);
@ -280,6 +285,7 @@ impl SpircTask {
MessageType::kMessageTypePause => { MessageType::kMessageTypePause => {
if self.state.get_status() == PlayStatus::kPlayStatusPlay { if self.state.get_status() == PlayStatus::kPlayStatusPlay {
self.player.pause(); self.player.pause();
self.mixer.stop();
self.state.set_status(PlayStatus::kPlayStatusPause); self.state.set_status(PlayStatus::kPlayStatusPause);
let now = now_ms() as u64; let now = now_ms() as u64;
@ -349,7 +355,7 @@ impl SpircTask {
MessageType::kMessageTypeVolume => { MessageType::kMessageTypeVolume => {
let volume = frame.get_volume(); let volume = frame.get_volume();
self.device.set_volume(volume); self.device.set_volume(volume);
self.player.volume(volume as u16); self.mixer.set_volume(frame.get_volume() as u16);
self.notify(None); self.notify(None);
} }
@ -360,6 +366,7 @@ impl SpircTask {
self.device.set_is_active(false); self.device.set_is_active(false);
self.state.set_status(PlayStatus::kPlayStatusStop); self.state.set_status(PlayStatus::kPlayStatusStop);
self.player.stop(); self.player.stop();
self.mixer.stop();
} }
} }
@ -398,7 +405,6 @@ impl SpircTask {
let gid = self.state.get_track()[index as usize].get_gid(); let gid = self.state.get_track()[index as usize].get_gid();
SpotifyId::from_raw(gid) SpotifyId::from_raw(gid)
}; };
let position = self.state.get_position_ms(); let position = self.state.get_position_ms();
let end_of_track = self.player.load(track, play, position); let end_of_track = self.player.load(track, play, position);
@ -423,52 +429,49 @@ impl SpircTask {
} }
cs.send(); cs.send();
} }
fn spirc_state(&self) -> protocol::spirc::State {
self.state.clone()
}
} }
struct CommandSender<'a> { struct CommandSender<'a> {
spirc: &'a mut SpircTask, spirc: &'a mut SpircTask,
cmd: MessageType, frame: protocol::spirc::Frame,
recipient: Option<String>,
} }
impl<'a> CommandSender<'a> { impl<'a> CommandSender<'a> {
fn new(spirc: &'a mut SpircTask, cmd: MessageType) -> CommandSender { fn new(spirc: &'a mut SpircTask, cmd: MessageType) -> CommandSender {
CommandSender { let frame = protobuf_init!(protocol::spirc::Frame::new(), {
spirc: spirc,
cmd: cmd,
recipient: None,
}
}
fn recipient(mut self, r: &str) -> CommandSender<'a> {
self.recipient = Some(r.to_owned());
self
}
fn send(self) {
let mut frame = protobuf_init!(Frame::new(), {
version: 1, version: 1,
ident: self.spirc.ident.clone(),
protocol_version: "2.0.0", protocol_version: "2.0.0",
seq_nr: self.spirc.sequence.get(), ident: spirc.ident.clone(),
typ: self.cmd, seq_nr: spirc.sequence.get(),
device_state: self.spirc.device.clone(), typ: cmd,
device_state: spirc.device.clone(),
state_update_id: now_ms(), state_update_id: now_ms(),
}); });
if let Some(recipient) = self.recipient { CommandSender {
frame.mut_recipient().push(recipient.to_owned()); spirc: spirc,
frame: frame,
}
}
fn recipient(mut self, recipient: &'a str) -> CommandSender {
self.frame.mut_recipient().push(recipient.to_owned());
self
}
#[allow(dead_code)]
fn state(mut self, state: protocol::spirc::State) -> CommandSender<'a> {
self.frame.set_state(state);
self
}
fn send(mut self) {
if !self.frame.has_state() && self.spirc.device.get_is_active() {
self.frame.set_state(self.spirc.state.clone());
} }
if self.spirc.device.get_is_active() { let send = self.spirc.sender.start_send(self.frame).unwrap();
frame.set_state(self.spirc.spirc_state()); assert!(send.is_ready());
}
let ready = self.spirc.sender.start_send(frame).unwrap().is_ready();
assert!(ready);
} }
} }