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]]
 [[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"]
 ]
 ]

+ 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
 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 - 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! {
 declare_backends! {
     pub const BACKENDS : &'static [
     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")]
         #[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>),
-        ("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) {
     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 {

+ 1 - 0
src/lib.rs

@@ -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"));

+ 16 - 6
src/main.rs

@@ -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 - 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 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 {
-                    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();
                 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()
             }
             }

+ 36 - 33
src/spirc.rs

@@ -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,
-    recipient: Option<String>,
+    frame: protocol::spirc::Frame,
 }
 }
 
 
 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 {
+        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 {
         CommandSender {
             spirc: spirc,
             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
         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());
     }
     }
 }
 }