diff --git a/docs/src/config/File.md b/docs/src/config/File.md index d4b05b41..89de7efa 100644 --- a/docs/src/config/File.md +++ b/docs/src/config/File.md @@ -50,6 +50,12 @@ backend = "alsa" # use portaudio for macOS [homebrew] # list of valid devices, run `aplay -L`, device = "alsa_audio_device" # omit for macOS +# The PCM sample format to use. Possible values +# are F32, S32, S24, S24_3, S16. +# Change this value if you encounter errors like +# "Alsa error PCM open ALSA function 'snd_pcm_hw_params_set_format' failed with error 'EINVAL: Invalid argument'" +audio_format = "S16" + # The alsa control device. By default this is the same # name as the `device` field. control = "alsa_audio_device" # omit for macOS diff --git a/src/config.rs b/src/config.rs index 0e8af349..f397b90a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,7 +8,9 @@ use gethostname::gethostname; use librespot_core::{ cache::Cache, config::DeviceType as LSDeviceType, config::SessionConfig, version, }; -use librespot_playback::config::{Bitrate as LSBitrate, PlayerConfig}; +use librespot_playback::config::{ + AudioFormat as LSAudioFormat, Bitrate as LSBitrate, PlayerConfig, +}; use log::{error, info, warn}; use serde::{de::Error, de::Unexpected, Deserialize, Deserializer}; use sha1::{Digest, Sha1}; @@ -266,6 +268,57 @@ impl ToString for DBusType { } } +/// LibreSpot supported audio formats +static AUDIO_FORMAT_VALUES: &[&str] = &["F32", "S32", "S24", "S24_3", "S16"]; + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, StructOpt)] +pub enum AudioFormat { + F32, + S32, + S24, + S24_3, + S16, +} + +impl FromStr for AudioFormat { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + match s { + "F32" => Ok(AudioFormat::F32), + "S32" => Ok(AudioFormat::S32), + "S24" => Ok(AudioFormat::S24), + "S24_3" => Ok(AudioFormat::S24_3), + "S16" => Ok(AudioFormat::S16), + _ => unreachable!(), + } + } +} + +impl ToString for AudioFormat { + fn to_string(&self) -> String { + match self { + AudioFormat::F32 => "F32".to_string(), + AudioFormat::S32 => "S32".to_string(), + AudioFormat::S24 => "S24".to_string(), + AudioFormat::S24_3 => "S24_3".to_string(), + AudioFormat::S16 => "S16".to_string(), + } + } +} + +impl From for LSAudioFormat { + fn from(audio_format: AudioFormat) -> Self { + match audio_format { + AudioFormat::F32 => LSAudioFormat::F32, + AudioFormat::S32 => LSAudioFormat::S32, + AudioFormat::S24 => LSAudioFormat::S24, + AudioFormat::S24_3 => LSAudioFormat::S24_3, + AudioFormat::S16 => LSAudioFormat::S16, + } + } +} + #[derive(Debug, Default, StructOpt)] #[structopt( about = "A Spotify daemon", @@ -404,6 +457,10 @@ pub struct SharedConfigValues { #[structopt(long, short = "B", possible_values = &BITRATE_VALUES, value_name = "number")] bitrate: Option, + /// The audio format of the streamed audio data + #[structopt(long, possible_values = &AUDIO_FORMAT_VALUES, value_name = "string")] + audio_format: Option, + /// Initial volume between 0 and 100 #[structopt(long, value_name = "initial_volume")] initial_volume: Option, @@ -511,6 +568,7 @@ impl fmt::Debug for SharedConfigValues { .field("mixer", &self.mixer) .field("device_name", &self.device_name) .field("bitrate", &self.bitrate) + .field("audio_format", &self.audio_format) .field("initial_volume", &self.initial_volume) .field("volume_normalisation", &self.volume_normalisation) .field("normalisation_pregain", &self.normalisation_pregain) @@ -583,7 +641,8 @@ impl SharedConfigValues { device_type, use_mpris, max_cache_size, - dbus_type + dbus_type, + audio_format ); // Handles boolean merging. @@ -622,6 +681,7 @@ pub(crate) struct SpotifydConfig { pub(crate) cache: Option, pub(crate) backend: Option, pub(crate) audio_device: Option, + pub(crate) audio_format: LSAudioFormat, #[allow(unused)] pub(crate) control_device: Option, #[allow(unused)] @@ -667,6 +727,12 @@ pub(crate) fn get_internal_config(config: CliConfig) -> SpotifydConfig { .unwrap_or(Bitrate::Bitrate160) .into(); + let audio_format: LSAudioFormat = config + .shared_config + .audio_format + .unwrap_or(AudioFormat::S16) + .into(); + let backend = config .shared_config .backend @@ -780,6 +846,7 @@ pub(crate) fn get_internal_config(config: CliConfig) -> SpotifydConfig { cache, backend: Some(backend), audio_device: config.shared_config.device, + audio_format, control_device: config.shared_config.control, mixer: config.shared_config.mixer, volume_controller, diff --git a/src/main_loop.rs b/src/main_loop.rs index dd4ea578..7c48b80b 100644 --- a/src/main_loop.rs +++ b/src/main_loop.rs @@ -30,6 +30,7 @@ pub struct AudioSetup { pub mixer: Box Box>, pub backend: fn(Option, AudioFormat) -> Box, pub audio_device: Option, + pub audio_format: AudioFormat, } pub struct SpotifydState { @@ -124,13 +125,12 @@ impl MainLoop { let mixer = (self.audio_setup.mixer)(); let backend = self.audio_setup.backend; let audio_device = self.audio_setup.audio_device.clone(); + let audio_format = self.audio_setup.audio_format; let (player, mut event_channel) = Player::new( self.player_config.clone(), session.clone(), mixer.get_soft_volume(), - // TODO: dunno how to work with AudioFormat yet, maybe dig further if this - // doesn't work for all configurations - move || (backend)(audio_device, AudioFormat::default()), + move || (backend)(audio_device, audio_format), ); let (spirc, spirc_task) = Spirc::new( diff --git a/src/setup.rs b/src/setup.rs index eca5619b..1af9bc68 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -111,6 +111,7 @@ pub(crate) fn initial_state(config: config::SpotifydConfig) -> main_loop::MainLo mixer, backend, audio_device: config.audio_device, + audio_format: config.audio_format, }, spotifyd_state: main_loop::SpotifydState { cache,