Browse Source

Merge remote-tracking branch 'origin/master'

Paul Lietar 7 years ago
parent
commit
2708aa4fef
13 changed files with 216 additions and 73 deletions
  1. 1 0
      .dockerignore
  2. 2 1
      Cargo.toml
  3. 30 4
      README.md
  4. 40 0
      contrib/Dockerfile
  5. 6 0
      contrib/docker-build.sh
  6. 0 0
      contrib/librespot.service
  7. 6 7
      src/audio_backend/mod.rs
  8. 1 0
      src/lib.rs
  9. 16 6
      src/main.rs
  10. 23 0
      src/mixer/mod.rs
  11. 48 0
      src/mixer/softmixer.rs
  12. 7 22
      src/player.rs
  13. 36 33
      src/spirc.rs

+ 1 - 0
.dockerignore

@@ -0,0 +1 @@
+target

+ 2 - 1
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"]
 ]

+ 30 - 4
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.

+ 40 - 0
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"]

+ 6 - 0
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"

+ 0 - 0
assets/librespot.service → contrib/librespot.service


+ 6 - 7
src/audio_backend/mod.rs

@@ -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 {

+ 1 - 0
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"));

+ 16 - 6
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<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 - 0
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<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 - 0
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<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;
+            }
+        }
+    }
+}

+ 7 - 22
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<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()
             }

+ 36 - 33
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<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 {
+        let frame = protobuf_init!(protocol::spirc::Frame::new(), {
+            version: 1,
+            protocol_version: "2.0.0",
+            ident: spirc.ident.clone(),
+            seq_nr: spirc.sequence.get(),
+            typ: cmd,
+
+            device_state: spirc.device.clone(),
+            state_update_id: now_ms(),
+        });
+
         CommandSender {
             spirc: spirc,
-            cmd: cmd,
-            recipient: None,
+            frame: frame,
         }
     }
 
-    fn recipient(mut self, r: &str) -> CommandSender<'a> {
-        self.recipient = Some(r.to_owned());
+    fn recipient(mut self, recipient: &'a str) -> CommandSender {
+        self.frame.mut_recipient().push(recipient.to_owned());
         self
     }
 
-    fn send(self) {
-        let mut frame = protobuf_init!(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(),
-            state_update_id: now_ms(),
-        });
-
-        if let Some(recipient) = self.recipient {
-            frame.mut_recipient().push(recipient.to_owned());
-        }
+    #[allow(dead_code)]
+    fn state(mut self, state: protocol::spirc::State) -> CommandSender<'a> {
+        self.frame.set_state(state);
+        self
+    }
 
-        if self.spirc.device.get_is_active() {
-            frame.set_state(self.spirc.spirc_state());
+    fn send(mut self) {
+        if !self.frame.has_state() && self.spirc.device.get_is_active() {
+            self.frame.set_state(self.spirc.state.clone());
         }
 
-        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());
     }
 }