From 6de2ac5ce3a5cfb296155c3c43ac12e2544b756a Mon Sep 17 00:00:00 2001 From: billsq Date: Mon, 7 May 2018 05:51:05 +0000 Subject: [PATCH] Add metadata pipe support which is compatible with forked-daapd. --- Cargo.lock | 1 + playback/Cargo.toml | 1 + playback/src/lib.rs | 2 +- playback/src/mixer/mod.rs | 5 +++ playback/src/mixer/pipemixer.rs | 56 +++++++++++++++++++++++++++++++++ playback/src/player.rs | 42 ++++++++++++++++++++++++- src/main.rs | 23 +++++++++++--- 7 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 playback/src/mixer/pipemixer.rs diff --git a/Cargo.lock b/Cargo.lock index c1aa626a..03e5f496 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,6 +487,7 @@ name = "librespot-playback" version = "0.1.0" dependencies = [ "alsa 0.0.1 (git+https://github.com/plietar/rust-alsa)", + "base64 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", "jack 0.5.7 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 653cbe6c..526cacb2 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -11,6 +11,7 @@ path = "../core" path = "../metadata" [dependencies] +base64 = "0.5.0" futures = "0.1.8" log = "0.3.5" byteorder = "1.2.1" diff --git a/playback/src/lib.rs b/playback/src/lib.rs index afce662c..94530565 100644 --- a/playback/src/lib.rs +++ b/playback/src/lib.rs @@ -1,6 +1,6 @@ #[macro_use] extern crate log; - +extern crate base64; extern crate byteorder; extern crate futures; diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs index a6ba34aa..d9966707 100644 --- a/playback/src/mixer/mod.rs +++ b/playback/src/mixer/mod.rs @@ -9,6 +9,7 @@ pub trait Mixer: Send { fn get_audio_filter(&self) -> Option> { None } + fn set_metadata_pipe(&mut self, _metadata_pipe: Option) {} } pub trait AudioFilter { @@ -18,6 +19,9 @@ pub trait AudioFilter { pub mod softmixer; use self::softmixer::SoftMixer; +pub mod pipemixer; +use self::pipemixer::PipeMixer; + fn mk_sink() -> Box { Box::new(M::open()) } @@ -25,6 +29,7 @@ fn mk_sink() -> Box { pub fn find>(name: Option) -> Option Box> { match name.as_ref().map(AsRef::as_ref) { None | Some("softvol") => Some(mk_sink::), + Some("pipe") => Some(mk_sink::), _ => None, } } diff --git a/playback/src/mixer/pipemixer.rs b/playback/src/mixer/pipemixer.rs new file mode 100644 index 00000000..3aeaf0df --- /dev/null +++ b/playback/src/mixer/pipemixer.rs @@ -0,0 +1,56 @@ +use base64; +use std::f32; +use std::fs::File; +use std::io::Write; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; + +use super::Mixer; + +#[derive(Clone)] +pub struct PipeMixer { + volume: Arc, + pipe: Option, +} + +impl Mixer for PipeMixer { + fn open() -> PipeMixer { + PipeMixer { + volume: Arc::new(AtomicUsize::new(0xFFFF)), + pipe: None, + } + } + 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); + + if let Some(path) = self.pipe.as_ref() { + let vol = volume; + let metadata_vol = if vol == 0 { + -144.0f32 + } else if vol == 1 { + -30.0f32 + } else if vol == 0xFFFF { + 0.0f32 + } else { + ((vol as f32) - (0xFFFF as f32)) * 30.0f32 / (0xFFFE as f32) + }; + + let vol_string = format!("{:.*},0.00,0.00,0.00", 2, metadata_vol); + let vol_string_len = vol_string.chars().count(); + let metadata_vol_string = base64::encode(&vol_string); + let metadata_xml = format!("73736e6370766f6c{}\n\n{}", vol_string_len, metadata_vol_string); + + let mut f = File::create(path).expect("Unable to open pipe"); + f.write_all(metadata_xml.as_bytes()) + .expect("Unable to write data"); + } + } + fn set_metadata_pipe(&mut self, metadata_pipe: Option) { + self.pipe = metadata_pipe; + } +} diff --git a/playback/src/player.rs b/playback/src/player.rs index dd994235..9e2621dc 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1,5 +1,6 @@ use byteorder::{LittleEndian, ReadBytesExt}; use futures; +use base64; use futures::sync::oneshot; use futures::{future, Future}; use std; @@ -9,6 +10,8 @@ use std::mem; use std::sync::mpsc::{RecvError, RecvTimeoutError, TryRecvError}; use std::thread; use std::time::Duration; +use std::fs::File; +use std::io::Write; use config::{Bitrate, PlayerConfig}; use core::session::Session; @@ -17,7 +20,7 @@ use core::spotify_id::SpotifyId; use audio::{AudioDecrypt, AudioFile}; use audio::{VorbisDecoder, VorbisPacket}; use audio_backend::Sink; -use metadata::{FileFormat, Metadata, Track}; +use metadata::{FileFormat, Metadata, Track, Album, Artist}; use mixer::AudioFilter; pub struct Player { @@ -35,6 +38,8 @@ struct PlayerInternal { sink_running: bool, audio_filter: Option>, event_sender: futures::sync::mpsc::UnboundedSender, + + metadata_pipe: Option, } enum PlayerCommand { @@ -113,6 +118,7 @@ impl Player { config: PlayerConfig, session: Session, audio_filter: Option>, + metadata_pipe: Option, sink_builder: F, ) -> (Player, PlayerEventChannel) where @@ -133,6 +139,7 @@ impl Player { sink: sink_builder(), sink_running: false, audio_filter: audio_filter, + metadata_pipe: metadata_pipe, event_sender: event_sender, }; @@ -535,6 +542,39 @@ impl PlayerInternal { track_id.to_base62() ); + if let Some(path) = self.metadata_pipe.as_ref() { + let mut f = File::create(path).expect("Unable to open pipe"); + + // title + let title = track.name.clone(); + let title_len = title.chars().count(); + let title_string = base64::encode(&title); + let title_xml = format!("636f72656d696e6d{}\n\n{}", title_len, title_string); + f.write_all(title_xml.as_bytes()).expect("Unable to write title"); + + // album + let album = Album::get(&self.session, track.album).wait().unwrap(); + let album_name = album.name.clone(); + let album_name_len = album_name.chars().count(); + let album_name_string = base64::encode(&album_name); + let album_name_xml = format!("636f72656173616c{}\n\n{}", album_name_len, album_name_string); + f.write_all(album_name_xml.as_bytes()).expect("Unable to write album"); + + // artist + let mut artists = String::new(); + for id in &track.artists { + if artists != "" { + artists.push_str(" & "); + } + let artist = Artist::get(&self.session, *id).wait().unwrap(); + artists.push_str(&artist.name); + } + let artists_len = artists.chars().count(); + let artists_string = base64::encode(&artists); + let artists_xml = format!("636f726561736172{}\n\n{}", artists_len, artists_string); + f.write_all(artists_xml.as_bytes()).expect("Unable to write artists"); + } + let track = match self.find_available_alternative(&track) { Some(track) => track, None => { diff --git a/src/main.rs b/src/main.rs index b949c93a..4fcdce1a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -90,6 +90,7 @@ fn list_backends() { struct Setup { backend: fn(Option) -> Box, device: Option, + metadata_pipe: Option, mixer: fn() -> Box, @@ -143,6 +144,7 @@ fn setup(args: &[String]) -> Setup { "DEVICE", ) .optopt("", "mixer", "Mixer to use", "MIXER") + .optopt("", "metadata-pipe", "Pipe to write metadata", "METADATA_PIPE") .optopt( "", "initial-volume", @@ -201,6 +203,8 @@ fn setup(args: &[String]) -> Setup { let device = matches.opt_str("device"); + let metadata_pipe = matches.opt_str("metadata-pipe"); + let mixer_name = matches.opt_str("mixer"); let mixer = mixer::find(mixer_name.as_ref()).expect("Invalid mixer"); @@ -312,6 +316,7 @@ fn setup(args: &[String]) -> Setup { connect_config: connect_config, credentials: credentials, device: device, + metadata_pipe: metadata_pipe, enable_discovery: enable_discovery, zeroconf_port: zeroconf_port, mixer: mixer, @@ -326,6 +331,7 @@ struct Main { connect_config: ConnectConfig, backend: fn(Option) -> Box, device: Option, + metadata_pipe: Option, mixer: fn() -> Box, handle: Handle, @@ -352,6 +358,7 @@ impl Main { connect_config: setup.connect_config, backend: setup.backend, device: setup.device, + metadata_pipe: setup.metadata_pipe, mixer: setup.mixer, connect: Box::new(futures::future::empty()), @@ -414,16 +421,22 @@ impl Future for Main { if let Async::Ready(session) = self.connect.poll().unwrap() { self.connect = Box::new(futures::future::empty()); let device = self.device.clone(); - let mixer = (self.mixer)(); + let mut mixer = (self.mixer)(); let player_config = self.player_config.clone(); let connect_config = self.connect_config.clone(); + let metadata_pipe = self.metadata_pipe.clone(); + + mixer.set_metadata_pipe(metadata_pipe.clone()); let audio_filter = mixer.get_audio_filter(); let backend = self.backend; - let (player, event_channel) = - Player::new(player_config, session.clone(), audio_filter, move || { - (backend)(device) - }); + let (player, event_channel) = Player::new( + player_config, + session.clone(), + audio_filter, + metadata_pipe, + move || (backend)(device), + ); let (spirc, spirc_task) = Spirc::new(connect_config, session, player, mixer); self.spirc = Some(spirc);