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]]
name = "librespot"
path = "src/main.rs"
doc = false
[dependencies.librespot-protocol]
path = "protocol"
@ -86,5 +87,5 @@ section = "sound"
priority = "optional"
assets = [
["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
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
Rust 1.7.0 or later is required to build librespot.
@ -60,6 +56,36 @@ The following backends are currently available :
- PortAudio
- 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
Using this code to connect to Spotify's API is probably forbidden by them.
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! {
pub const BACKENDS : &'static [
(&'static str,
&'static (Fn(Option<String>) -> Box<Sink> + Sync + Send + 'static))
(&'static str, fn(Option<String>) -> Box<Sink>)
] = &[
#[cfg(feature = "alsa-backend")]
("alsa", &mk_sink::<AlsaSink>),
("alsa", mk_sink::<AlsaSink>),
#[cfg(feature = "portaudio-backend")]
("portaudio", &mk_sink::<PortAudioSink>),
("portaudio", mk_sink::<PortAudioSink>),
#[cfg(feature = "pulseaudio-backend")]
("pulseaudio", &mk_sink::<PulseAudioSink>),
("pipe", &mk_sink::<StdoutSink>),
("pulseaudio", mk_sink::<PulseAudioSink>),
("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) {
BACKENDS.iter().find(|backend| name == backend.0).map(|backend| backend.1)
} else {

View file

@ -64,6 +64,7 @@ pub mod player;
pub mod session;
pub mod util;
pub mod version;
pub mod mixer;
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::player::Player;
use librespot::session::{Bitrate, Config, Session};
use librespot::mixer::{self, Mixer};
use librespot::version;
fn usage(program: &str, opts: &getopts::Options) -> String {
@ -62,9 +64,9 @@ fn list_backends() {
}
}
#[derive(Clone)]
struct Setup {
backend: &'static (Fn(Option<String>) -> Box<Sink> + Send + Sync),
backend: fn(Option<String>) -> Box<Sink>,
mixer: Box<Mixer + Send>,
cache: Option<Cache>,
config: Config,
credentials: Credentials,
@ -82,7 +84,8 @@ fn setup(args: &[String]) -> Setup {
.optopt("u", "username", "Username to sign in with", "USERNAME")
.optopt("p", "password", "Password", "PASSWORD")
.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..]) {
Ok(m) => m,
@ -109,6 +112,10 @@ fn setup(args: &[String]) -> Setup {
let backend = audio_backend::find(backend_name.as_ref())
.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()
.map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate"))
.unwrap_or(Bitrate::Bitrate160);
@ -140,6 +147,7 @@ fn setup(args: &[String]) -> Setup {
Setup {
backend: backend,
mixer: mixer,
cache: cache,
config: config,
credentials: credentials,
@ -153,16 +161,18 @@ fn main() {
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 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)
});
let (spirc, task) = Spirc::new(session.clone(), player);
let (spirc, task) = Spirc::new(session.clone(), player, mixer);
let spirc = ::std::cell::RefCell::new(spirc);
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 metadata::{FileFormat, Track};
use session::{Bitrate, Session};
use mixer::AudioFilter;
use util::{self, SpotifyId, Subfile};
#[derive(Clone)]
@ -25,21 +26,20 @@ struct PlayerInternal {
commands: std::sync::mpsc::Receiver<PlayerCommand>,
state: PlayerState,
volume: u16,
sink: Box<Sink>,
audio_filter: Option<Box<AudioFilter + Send>>,
}
enum PlayerCommand {
Load(SpotifyId, bool, u32, oneshot::Sender<()>),
Play,
Pause,
Volume(u16),
Stop,
Seek(u32),
}
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 {
let (cmd_tx, cmd_rx) = std::sync::mpsc::channel();
@ -49,8 +49,8 @@ impl Player {
commands: cmd_rx,
state: PlayerState::Stopped,
volume: 0xFFFF,
sink: sink_builder(),
audio_filter: audio_filter,
};
internal.run();
@ -89,10 +89,6 @@ impl Player {
pub fn seek(&self, position_ms: u32) {
self.command(PlayerCommand::Seek(position_ms));
}
pub fn volume(&self, vol: u16) {
self.command(PlayerCommand::Volume(vol));
}
}
type Decoder = vorbis::Decoder<Subfile<AudioDecrypt<AudioFile>>>;
@ -203,11 +199,9 @@ impl PlayerInternal {
fn handle_packet(&mut self, packet: Option<Result<vorbis::Packet, VorbisError>>) {
match packet {
Some(Ok(mut packet)) => {
if self.volume < 0xFFFF {
for x in &mut packet.data {
*x = (*x as i32 * self.volume as i32 / 0xFFFF) as i16;
}
}
if let Some(ref editor) = self.audio_filter {
editor.modify_stream(&mut packet.data)
};
self.sink.write(&packet.data).unwrap();
}
@ -313,10 +307,6 @@ impl PlayerInternal {
PlayerState::Invalid => panic!("invalid state"),
}
}
PlayerCommand::Volume(vol) => {
self.volume = vol;
}
}
}
@ -419,11 +409,6 @@ impl ::std::fmt::Debug for PlayerCommand {
PlayerCommand::Pause => {
f.debug_tuple("Pause").finish()
}
PlayerCommand::Volume(volume) => {
f.debug_tuple("Volume")
.field(&volume)
.finish()
}
PlayerCommand::Stop => {
f.debug_tuple("Stop").finish()
}

View file

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