diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..eb5a316c --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +target diff --git a/Cargo.toml b/Cargo.toml index d4fda23d..bed85d8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] ] diff --git a/README.md b/README.md index 914f40e0..bd8155bf 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/contrib/Dockerfile b/contrib/Dockerfile new file mode 100644 index 00000000..175f1efe --- /dev/null +++ b/contrib/Dockerfile @@ -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"] diff --git a/contrib/docker-build.sh b/contrib/docker-build.sh new file mode 100755 index 00000000..984e1293 --- /dev/null +++ b/contrib/docker-build.sh @@ -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" diff --git a/assets/librespot.service b/contrib/librespot.service similarity index 100% rename from assets/librespot.service rename to contrib/librespot.service diff --git a/src/audio_backend/mod.rs b/src/audio_backend/mod.rs index a7034055..689379cf 100644 --- a/src/audio_backend/mod.rs +++ b/src/audio_backend/mod.rs @@ -73,20 +73,19 @@ use self::pipe::StdoutSink; declare_backends! { pub const BACKENDS : &'static [ - (&'static str, - &'static (Fn(Option) -> Box + Sync + Send + 'static)) + (&'static str, fn(Option) -> Box) ] = &[ #[cfg(feature = "alsa-backend")] - ("alsa", &mk_sink::), + ("alsa", mk_sink::), #[cfg(feature = "portaudio-backend")] - ("portaudio", &mk_sink::), + ("portaudio", mk_sink::), #[cfg(feature = "pulseaudio-backend")] - ("pulseaudio", &mk_sink::), - ("pipe", &mk_sink::), + ("pulseaudio", mk_sink::), + ("pipe", mk_sink::), ]; } -pub fn find>(name: Option) -> Option<&'static (Fn(Option) -> Box + Send + Sync)> { +pub fn find>(name: Option) -> Option) -> Box> { if let Some(name) = name.as_ref().map(AsRef::as_ref) { BACKENDS.iter().find(|backend| name == backend.0).map(|backend| backend.1) } else { diff --git a/src/lib.rs b/src/lib.rs index 0aaa4040..58e49322 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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")); diff --git a/src/main.rs b/src/main.rs index 8ec28a0f..17acd057 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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) -> Box + Send + Sync), + backend: fn(Option) -> Box, + mixer: Box, cache: Option, 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 = 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 || { diff --git a/src/mixer/mod.rs b/src/mixer/mod.rs new file mode 100644 index 00000000..3c7e6b75 --- /dev/null +++ b/src/mixer/mod.rs @@ -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> { + None + } +} + +pub trait AudioFilter { + fn modify_stream(&self, data: &mut [i16]); +} + +pub mod softmixer; +use self::softmixer::SoftMixer; + +pub fn find>(name: Option) -> Option> { + match name.as_ref().map(AsRef::as_ref) { + None | Some("softvol") => Some(Box::new(SoftMixer::new())), + _ => None, + } +} diff --git a/src/mixer/softmixer.rs b/src/mixer/softmixer.rs new file mode 100644 index 00000000..e54e728e --- /dev/null +++ b/src/mixer/softmixer.rs @@ -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 +} + +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> { + Some(Box::new(SoftVolumeApplier { volume: self.volume.clone() })) + } +} + +struct SoftVolumeApplier { + volume: Arc +} + +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; + } + } + } +} \ No newline at end of file diff --git a/src/player.rs b/src/player.rs index d0410967..ecadb3e4 100644 --- a/src/player.rs +++ b/src/player.rs @@ -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, state: PlayerState, - volume: u16, sink: Box, + audio_filter: Option>, } enum PlayerCommand { Load(SpotifyId, bool, u32, oneshot::Sender<()>), Play, Pause, - Volume(u16), Stop, Seek(u32), } impl Player { - pub fn new(session: Session, sink_builder: F) -> Player + pub fn new(session: Session, audio_filter: Option>, sink_builder: F) -> Player where F: FnOnce() -> Box + 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>>; @@ -203,11 +199,9 @@ impl PlayerInternal { fn handle_packet(&mut self, packet: Option>) { 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() } diff --git a/src/spirc.rs b/src/spirc.rs index 66706ab4..14fda2eb 100644 --- a/src/spirc.rs +++ b/src/spirc.rs @@ -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, sequence: SeqGenerator, @@ -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) + -> (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, + 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()); } }