From f29e5212c402074c1a12eb493af7e5d4c966dcdf Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 24 Feb 2021 21:39:42 +0100 Subject: [PATCH 01/23] High-resolution volume control and normalisation - Store and output samples as 32-bit floats instead of 16-bit integers. This provides 24-25 bits of transparency, allowing for 42-48 dB of headroom to do volume control and normalisation without throwing away bits or dropping dynamic range below 96 dB CD quality. - Perform volume control and normalisation in 64-bit arithmetic. - Add a dynamic limiter with configurable threshold, attack time, release or decay time, and steepness for the sigmoid transfer function. This mimics the native Spotify limiter, offering greater dynamic range than the old limiter, that just reduced overall gain to prevent clipping. - Make the configurable threshold also apply to the old limiter, which is still available. Resolves: librespot-org/librespot#608 --- audio/src/lewton_decoder.rs | 7 +- audio/src/lib.rs | 4 +- audio/src/libvorbis_decoder.rs | 11 +- playback/src/audio_backend/alsa.rs | 20 +-- playback/src/audio_backend/gstreamer.rs | 2 +- playback/src/audio_backend/jackaudio.rs | 14 +- playback/src/audio_backend/pipe.rs | 2 +- playback/src/audio_backend/portaudio.rs | 6 +- playback/src/audio_backend/pulseaudio.rs | 11 +- playback/src/audio_backend/rodio.rs | 2 +- playback/src/audio_backend/sdl.rs | 4 +- playback/src/audio_backend/subprocess.rs | 2 +- playback/src/config.rs | 33 ++++ playback/src/mixer/mod.rs | 2 +- playback/src/mixer/softmixer.rs | 5 +- playback/src/player.rs | 183 +++++++++++++++++++++-- src/main.rs | 72 ++++++++- 17 files changed, 327 insertions(+), 53 deletions(-) diff --git a/audio/src/lewton_decoder.rs b/audio/src/lewton_decoder.rs index 1addaa01..8e7d089e 100644 --- a/audio/src/lewton_decoder.rs +++ b/audio/src/lewton_decoder.rs @@ -37,8 +37,11 @@ where use self::lewton::VorbisError::BadAudio; use self::lewton::VorbisError::OggError; loop { - match self.0.read_dec_packet_itl() { - Ok(Some(packet)) => return Ok(Some(AudioPacket::Samples(packet))), + match self + .0 + .read_dec_packet_generic::>() + { + Ok(Some(packet)) => return Ok(Some(AudioPacket::Samples(packet.samples))), Ok(None) => return Ok(None), Err(BadAudio(AudioIsHeader)) => (), diff --git a/audio/src/lib.rs b/audio/src/lib.rs index fd764071..c4d862b3 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -33,12 +33,12 @@ pub use fetch::{ use std::fmt; pub enum AudioPacket { - Samples(Vec), + Samples(Vec), OggData(Vec), } impl AudioPacket { - pub fn samples(&self) -> &[i16] { + pub fn samples(&self) -> &[f32] { match self { AudioPacket::Samples(s) => s, AudioPacket::OggData(_) => panic!("can't return OggData on samples"), diff --git a/audio/src/libvorbis_decoder.rs b/audio/src/libvorbis_decoder.rs index 8aced556..e7ccc984 100644 --- a/audio/src/libvorbis_decoder.rs +++ b/audio/src/libvorbis_decoder.rs @@ -39,7 +39,16 @@ where fn next_packet(&mut self) -> Result, AudioError> { loop { match self.0.packets().next() { - Some(Ok(packet)) => return Ok(Some(AudioPacket::Samples(packet.data))), + Some(Ok(packet)) => { + // Losslessly represent [-32768, 32767] to [-1.0, 1.0] while maintaining DC linearity. + return Ok(Some(AudioPacket::Samples( + packet + .data + .iter() + .map(|sample| ((*sample as f64 + 0.5) / (0x7FFF as f64 + 0.5)) as f32) + .collect(), + ))); + } None => return Ok(None), Some(Err(vorbis::VorbisError::Hole)) => (), diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index bf7b1376..9bc17fe6 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -8,13 +8,13 @@ use std::ffi::CString; use std::io; use std::process::exit; -const PREFERED_PERIOD_SIZE: Frames = 5512; // Period of roughly 125ms +const PREFERRED_PERIOD_SIZE: Frames = 11025; // Period of roughly 125ms const BUFFERED_PERIODS: Frames = 4; pub struct AlsaSink { pcm: Option, device: String, - buffer: Vec, + buffer: Vec, } fn list_outputs() { @@ -36,19 +36,19 @@ fn list_outputs() { fn open_device(dev_name: &str) -> Result<(PCM, Frames), Box> { let pcm = PCM::new(dev_name, Direction::Playback, false)?; - let mut period_size = PREFERED_PERIOD_SIZE; + let mut period_size = PREFERRED_PERIOD_SIZE; // http://www.linuxjournal.com/article/6735?page=0,1#N0x19ab2890.0x19ba78d8 // latency = period_size * periods / (rate * bytes_per_frame) - // For 16 Bit stereo data, one frame has a length of four bytes. - // 500ms = buffer_size / (44100 * 4) - // buffer_size_bytes = 0.5 * 44100 / 4 + // For stereo samples encoded as 32-bit floats, one frame has a length of eight bytes. + // 500ms = buffer_size / (44100 * 8) + // buffer_size_bytes = 0.5 * 44100 / 8 // buffer_size_frames = 0.5 * 44100 = 22050 { - // Set hardware parameters: 44100 Hz / Stereo / 16 bit + // Set hardware parameters: 44100 Hz / Stereo / 32-bit float let hwp = HwParams::any(&pcm)?; hwp.set_access(Access::RWInterleaved)?; - hwp.set_format(Format::s16())?; + hwp.set_format(Format::float())?; hwp.set_rate(44100, ValueOr::Nearest)?; hwp.set_channels(2)?; period_size = hwp.set_period_size_near(period_size, ValueOr::Greater)?; @@ -114,7 +114,7 @@ impl Sink for AlsaSink { let pcm = self.pcm.as_mut().unwrap(); // Write any leftover data in the period buffer // before draining the actual buffer - let io = pcm.io_i16().unwrap(); + let io = pcm.io_f32().unwrap(); match io.writei(&self.buffer[..]) { Ok(_) => (), Err(err) => pcm.try_recover(err, false).unwrap(), @@ -138,7 +138,7 @@ impl Sink for AlsaSink { processed_data += data_to_buffer; if self.buffer.len() == self.buffer.capacity() { let pcm = self.pcm.as_mut().unwrap(); - let io = pcm.io_i16().unwrap(); + let io = pcm.io_f32().unwrap(); match io.writei(&self.buffer) { Ok(_) => (), Err(err) => pcm.try_recover(err, false).unwrap(), diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 6be6dd72..1ad3631e 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -15,7 +15,7 @@ pub struct GstreamerSink { impl Open for GstreamerSink { fn open(device: Option) -> GstreamerSink { gst::init().expect("Failed to init gstreamer!"); - let pipeline_str_preamble = r#"appsrc caps="audio/x-raw,format=S16LE,layout=interleaved,channels=2,rate=44100" block=true max-bytes=4096 name=appsrc0 "#; + let pipeline_str_preamble = r#"appsrc caps="audio/x-raw,format=F32,layout=interleaved,channels=2,rate=44100" block=true max-bytes=4096 name=appsrc0 "#; let pipeline_str_rest = r#" ! audioconvert ! autoaudiosink"#; let pipeline_str: String = match device { Some(x) => format!("{}{}", pipeline_str_preamble, x), diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 4699c182..e95933fc 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -7,20 +7,18 @@ use std::io; use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; pub struct JackSink { - send: SyncSender, + send: SyncSender, + // We have to keep hold of this object, or the Sink can't play... + #[allow(dead_code)] active_client: AsyncClient<(), JackData>, } pub struct JackData { - rec: Receiver, + rec: Receiver, port_l: Port, port_r: Port, } -fn pcm_to_f32(sample: i16) -> f32 { - sample as f32 / 32768.0 -} - impl ProcessHandler for JackData { fn process(&mut self, _: &Client, ps: &ProcessScope) -> Control { // get output port buffers @@ -33,8 +31,8 @@ impl ProcessHandler for JackData { let buf_size = buf_r.len(); for i in 0..buf_size { - buf_r[i] = pcm_to_f32(queue_iter.next().unwrap_or(0)); - buf_l[i] = pcm_to_f32(queue_iter.next().unwrap_or(0)); + buf_r[i] = queue_iter.next().unwrap_or(0.0); + buf_l[i] = queue_iter.next().unwrap_or(0.0); } Control::Continue } diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index 210c0ce9..5516ee94 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -32,7 +32,7 @@ impl Sink for StdoutSink { AudioPacket::Samples(data) => unsafe { slice::from_raw_parts( data.as_ptr() as *const u8, - data.len() * mem::size_of::(), + data.len() * mem::size_of::(), ) }, AudioPacket::OggData(data) => data, diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 0e25021e..0b8eac0b 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -8,8 +8,8 @@ use std::process::exit; use std::time::Duration; pub struct PortAudioSink<'a>( - Option>, - StreamParameters, + Option>, + StreamParameters, ); fn output_devices() -> Box> { @@ -65,7 +65,7 @@ impl<'a> Open for PortAudioSink<'a> { device: device_idx, channel_count: 2, suggested_latency: latency, - data: 0i16, + data: 0.0, }; PortAudioSink(None, params) diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 11ea026a..4dca2108 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -3,6 +3,7 @@ use crate::audio::AudioPacket; use libpulse_binding::{self as pulse, stream::Direction}; use libpulse_simple_binding::Simple; use std::io; +use std::mem; const APP_NAME: &str = "librespot"; const STREAM_NAME: &str = "Spotify endpoint"; @@ -18,7 +19,7 @@ impl Open for PulseAudioSink { debug!("Using PulseAudio sink"); let ss = pulse::sample::Spec { - format: pulse::sample::Format::S16le, + format: pulse::sample::Format::F32le, channels: 2, // stereo rate: 44100, }; @@ -68,13 +69,13 @@ impl Sink for PulseAudioSink { fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { if let Some(s) = &self.s { - // SAFETY: An i16 consists of two bytes, so that the given slice can be interpreted - // as a byte array of double length. Each byte pointer is validly aligned, and so - // is the newly created slice. + // SAFETY: An f32 consists of four bytes, so that the given slice can be interpreted + // as a byte array of four. Each byte pointer is validly aligned, and so is the newly + // created slice. let d: &[u8] = unsafe { std::slice::from_raw_parts( packet.samples().as_ptr() as *const u8, - packet.samples().len() * 2, + packet.samples().len() * mem::size_of::(), ) }; diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 3b920c30..6c996e85 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -198,7 +198,7 @@ impl Sink for JackRodioSink { Ok(()) } - fn write(&mut self, data: &[i16]) -> io::Result<()> { + fn write(&mut self, data: &[f32]) -> io::Result<()> { let source = rodio::buffer::SamplesBuffer::new(2, 44100, data); self.jackrodio_sink.append(source); diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 27d650f9..727615d1 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -3,7 +3,7 @@ use crate::audio::AudioPacket; use sdl2::audio::{AudioQueue, AudioSpecDesired}; use std::{io, thread, time}; -type Channel = i16; +type Channel = f32; pub struct SdlSink { queue: AudioQueue, @@ -47,7 +47,7 @@ impl Sink for SdlSink { } fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - while self.queue.size() > (2 * 2 * 44_100) { + while self.queue.size() > (2 * 4 * 44_100) { // sleep and wait for sdl thread to drain the queue a bit thread::sleep(time::Duration::from_millis(10)); } diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index 0dd25638..123e0233 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -48,7 +48,7 @@ impl Sink for SubprocessSink { let data: &[u8] = unsafe { slice::from_raw_parts( packet.samples().as_ptr() as *const u8, - packet.samples().len() * mem::size_of::(), + packet.samples().len() * mem::size_of::(), ) }; if let Some(child) = &mut self.child { diff --git a/playback/src/config.rs b/playback/src/config.rs index 31f63626..be15b268 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -48,12 +48,40 @@ impl Default for NormalisationType { } } +#[derive(Clone, Debug, PartialEq)] +pub enum NormalisationMethod { + Basic, + Dynamic, +} + +impl FromStr for NormalisationMethod { + type Err = (); + fn from_str(s: &str) -> Result { + match s { + "basic" => Ok(NormalisationMethod::Basic), + "dynamic" => Ok(NormalisationMethod::Dynamic), + _ => Err(()), + } + } +} + +impl Default for NormalisationMethod { + fn default() -> NormalisationMethod { + NormalisationMethod::Dynamic + } +} + #[derive(Clone, Debug)] pub struct PlayerConfig { pub bitrate: Bitrate, pub normalisation: bool, pub normalisation_type: NormalisationType, + pub normalisation_method: NormalisationMethod, pub normalisation_pregain: f32, + pub normalisation_threshold: f32, + pub normalisation_attack: f32, + pub normalisation_release: f32, + pub normalisation_steepness: f32, pub gapless: bool, pub passthrough: bool, } @@ -64,7 +92,12 @@ impl Default for PlayerConfig { bitrate: Bitrate::default(), normalisation: false, normalisation_type: NormalisationType::default(), + normalisation_method: NormalisationMethod::default(), normalisation_pregain: 0.0, + normalisation_threshold: -1.0, + normalisation_attack: 0.005, + normalisation_release: 0.1, + normalisation_steepness: 1.0, gapless: true, passthrough: false, } diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs index 325c1e18..3424526a 100644 --- a/playback/src/mixer/mod.rs +++ b/playback/src/mixer/mod.rs @@ -12,7 +12,7 @@ pub trait Mixer: Send { } pub trait AudioFilter { - fn modify_stream(&self, data: &mut [i16]); + fn modify_stream(&self, data: &mut [f32]); } #[cfg(feature = "alsa-backend")] diff --git a/playback/src/mixer/softmixer.rs b/playback/src/mixer/softmixer.rs index 28e1cf57..ec8ed6b2 100644 --- a/playback/src/mixer/softmixer.rs +++ b/playback/src/mixer/softmixer.rs @@ -35,11 +35,12 @@ struct SoftVolumeApplier { } impl AudioFilter for SoftVolumeApplier { - fn modify_stream(&self, data: &mut [i16]) { + fn modify_stream(&self, data: &mut [f32]) { let volume = self.volume.load(Ordering::Relaxed) as u16; if volume != 0xFFFF { + let volume_factor = volume as f64 / 0xFFFF as f64; for x in data.iter_mut() { - *x = (*x as i32 * volume as i32 / 0xFFFF) as i16; + *x = (*x as f64 * volume_factor) as f32; } } } diff --git a/playback/src/player.rs b/playback/src/player.rs index 9b4eefb9..c7edf3a4 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -9,7 +9,7 @@ use std::mem; use std::thread; use std::time::{Duration, Instant}; -use crate::config::{Bitrate, NormalisationType, PlayerConfig}; +use crate::config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}; use librespot_core::session::Session; use librespot_core::spotify_id::SpotifyId; @@ -26,6 +26,7 @@ use crate::metadata::{AudioItem, FileFormat}; use crate::mixer::AudioFilter; const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000; +const SAMPLES_PER_SECOND: u32 = 44100 * 2; pub struct Player { commands: Option>, @@ -54,6 +55,13 @@ struct PlayerInternal { sink_event_callback: Option, audio_filter: Option>, event_senders: Vec>, + + limiter_active: bool, + limiter_attack_counter: u32, + limiter_release_counter: u32, + limiter_peak_sample: f32, + limiter_factor: f32, + limiter_strength: f32, } enum PlayerCommand { @@ -185,7 +193,7 @@ impl PlayerEvent { pub type PlayerEventChannel = futures::sync::mpsc::UnboundedReceiver; #[derive(Clone, Copy, Debug)] -struct NormalisationData { +pub struct NormalisationData { track_gain_db: f32, track_peak: f32, album_gain_db: f32, @@ -193,6 +201,14 @@ struct NormalisationData { } impl NormalisationData { + pub fn db_to_ratio(db: f32) -> f32 { + return f32::powf(10.0, db / 20.0); + } + + pub fn ratio_to_db(ratio: f32) -> f32 { + return ratio.log10() * 20.0; + } + fn parse_from_file(mut file: T) -> Result { const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144; file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET)) @@ -218,17 +234,44 @@ impl NormalisationData { NormalisationType::Album => [data.album_gain_db, data.album_peak], NormalisationType::Track => [data.track_gain_db, data.track_peak], }; - let mut normalisation_factor = - f32::powf(10.0, (gain_db + config.normalisation_pregain) / 20.0); - if normalisation_factor * gain_peak > 1.0 { - warn!("Reducing normalisation factor to prevent clipping. Please add negative pregain to avoid."); - normalisation_factor = 1.0 / gain_peak; + let normalisation_power = gain_db + config.normalisation_pregain; + let mut normalisation_factor = Self::db_to_ratio(normalisation_power); + + if normalisation_factor * gain_peak > config.normalisation_threshold { + let limited_normalisation_factor = config.normalisation_threshold / gain_peak; + let limited_normalisation_power = Self::ratio_to_db(limited_normalisation_factor); + + if config.normalisation_method == NormalisationMethod::Basic { + warn!("Limiting gain to {:.2} for the duration of this track to stay under normalisation threshold.", limited_normalisation_power); + normalisation_factor = limited_normalisation_factor; + } else { + warn!( + "This track will at its peak be subject to {:.2} dB of dynamic limiting.", + normalisation_power - limited_normalisation_power + ); + } + + warn!("Please lower pregain to avoid."); } debug!("Normalisation Data: {:?}", data); debug!("Normalisation Type: {:?}", config.normalisation_type); - debug!("Applied normalisation factor: {}", normalisation_factor); + debug!( + "Normalisation Threshold: {:.1}", + Self::ratio_to_db(config.normalisation_threshold) + ); + debug!("Normalisation Method: {:?}", config.normalisation_method); + debug!("Normalisation Factor: {}", normalisation_factor); + + if config.normalisation_method == NormalisationMethod::Dynamic { + debug!("Normalisation Attack: {:?}", config.normalisation_attack); + debug!("Normalisation Release: {:?}", config.normalisation_release); + debug!( + "Normalisation Steepness: {:?}", + config.normalisation_steepness + ); + } normalisation_factor } @@ -262,6 +305,13 @@ impl Player { sink_event_callback: None, audio_filter: audio_filter, event_senders: [event_sender].to_vec(), + + limiter_active: false, + limiter_attack_counter: 0, + limiter_release_counter: 0, + limiter_peak_sample: 0.0, + limiter_factor: 1.0, + limiter_strength: 0.0, }; // While PlayerInternal is written as a future, it still contains blocking code. @@ -1113,9 +1163,111 @@ impl PlayerInternal { editor.modify_stream(data) } - if self.config.normalisation && normalisation_factor != 1.0 { - for x in data.iter_mut() { - *x = (*x as f32 * normalisation_factor) as i16; + if self.config.normalisation + && (normalisation_factor != 1.0 + || self.config.normalisation_method != NormalisationMethod::Basic) + { + for sample in data.iter_mut() { + let mut actual_normalisation_factor = normalisation_factor; + if self.config.normalisation_method == NormalisationMethod::Dynamic + { + if self.limiter_active { + // "S"-shaped curve with a configurable steepness during attack and release: + // - > 1.0 yields soft knees at start and end, steeper in between + // - 1.0 yields a linear function from 0-100% + // - between 0.0 and 1.0 yields hard knees at start and end, flatter in between + // - 0.0 yields a step response to 50%, causing distortion + // - Rates < 0.0 invert the limiter and are invalid + let mut shaped_limiter_strength = self.limiter_strength; + if shaped_limiter_strength > 0.0 + && shaped_limiter_strength < 1.0 + { + shaped_limiter_strength = 1.0 + / (1.0 + + f32::powf( + shaped_limiter_strength + / (1.0 - shaped_limiter_strength), + -1.0 * self.config.normalisation_steepness, + )); + } + actual_normalisation_factor = + (1.0 - shaped_limiter_strength) * normalisation_factor + + shaped_limiter_strength * self.limiter_factor; + }; + + // Always check for peaks, even when the limiter is already active. + // There may be even higher peaks than we initially targeted. + // Check against the normalisation factor that would be applied normally. + let abs_sample = + ((*sample as f64 * normalisation_factor as f64) as f32) + .abs(); + if abs_sample > self.config.normalisation_threshold { + self.limiter_active = true; + if self.limiter_release_counter > 0 { + // A peak was encountered while releasing the limiter; + // synchronize with the current release limiter strength. + self.limiter_attack_counter = (((SAMPLES_PER_SECOND + as f32 + * self.config.normalisation_release) + - self.limiter_release_counter as f32) + / (self.config.normalisation_release + / self.config.normalisation_attack)) + as u32; + self.limiter_release_counter = 0; + } + + self.limiter_attack_counter = + self.limiter_attack_counter.saturating_add(1); + self.limiter_strength = self.limiter_attack_counter as f32 + / (SAMPLES_PER_SECOND as f32 + * self.config.normalisation_attack); + + if abs_sample > self.limiter_peak_sample { + self.limiter_peak_sample = abs_sample; + self.limiter_factor = + self.config.normalisation_threshold + / self.limiter_peak_sample; + } + } else if self.limiter_active { + if self.limiter_attack_counter > 0 { + // Release may start within the attack period, before + // the limiter reached full strength. For that reason + // start the release by synchronizing with the current + // attack limiter strength. + self.limiter_release_counter = (((SAMPLES_PER_SECOND + as f32 + * self.config.normalisation_attack) + - self.limiter_attack_counter as f32) + * (self.config.normalisation_release + / self.config.normalisation_attack)) + as u32; + self.limiter_attack_counter = 0; + } + + self.limiter_release_counter = + self.limiter_release_counter.saturating_add(1); + + if self.limiter_release_counter + > (SAMPLES_PER_SECOND as f32 + * self.config.normalisation_release) + as u32 + { + self.reset_limiter(); + } else { + self.limiter_strength = ((SAMPLES_PER_SECOND as f32 + * self.config.normalisation_release) + - self.limiter_release_counter as f32) + / (SAMPLES_PER_SECOND as f32 + * self.config.normalisation_release); + } + } + } + + // Extremely sharp attacks, however unlikely, *may* still clip and provide + // undefined results, so strictly enforce output within [-1.0, 1.0]. + *sample = (*sample as f64 * actual_normalisation_factor as f64) + .clamp(-1.0, 1.0) + as f32; } } } @@ -1146,6 +1298,15 @@ impl PlayerInternal { } } + fn reset_limiter(&mut self) { + self.limiter_active = false; + self.limiter_release_counter = 0; + self.limiter_attack_counter = 0; + self.limiter_peak_sample = 0.0; + self.limiter_factor = 1.0; + self.limiter_strength = 0.0; + } + fn start_playback( &mut self, track_id: SpotifyId, diff --git a/src/main.rs b/src/main.rs index 53603152..91a58659 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,13 +22,15 @@ use librespot::core::version; use librespot::connect::discovery::{discovery, DiscoveryStream}; use librespot::connect::spirc::{Spirc, SpircTask}; use librespot::playback::audio_backend::{self, Sink, BACKENDS}; -use librespot::playback::config::{Bitrate, NormalisationType, PlayerConfig}; +use librespot::playback::config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}; use librespot::playback::mixer::{self, Mixer, MixerConfig}; -use librespot::playback::player::{Player, PlayerEvent}; +use librespot::playback::player::{NormalisationData, Player, PlayerEvent}; mod player_event_handler; use crate::player_event_handler::{emit_sink_event, run_program_on_events}; +const MILLIS: f32 = 1000.0; + fn device_id(name: &str) -> String { hex::encode(Sha1::digest(name.as_bytes())) } @@ -188,6 +190,12 @@ fn setup(args: &[String]) -> Setup { "enable-volume-normalisation", "Play all tracks at the same volume", ) + .optopt( + "", + "normalisation-method", + "Specify the normalisation method to use - [basic, dynamic]. Default is dynamic.", + "NORMALISATION_METHOD", + ) .optopt( "", "normalisation-gain-type", @@ -200,6 +208,30 @@ fn setup(args: &[String]) -> Setup { "Pregain (dB) applied by volume normalisation", "PREGAIN", ) + .optopt( + "", + "normalisation-threshold", + "Threshold (dBFS) to prevent clipping. Default is -1.0.", + "THRESHOLD", + ) + .optopt( + "", + "normalisation-attack", + "Attack time (ms) in which the dynamic limiter is reducing gain. Default is 5.", + "ATTACK", + ) + .optopt( + "", + "normalisation-release", + "Release or decay time (ms) in which the dynamic limiter is restoring gain. Default is 100.", + "RELEASE", + ) + .optopt( + "", + "normalisation-steepness", + "Steepness of the dynamic limiting curve. Default is 1.0.", + "STEEPNESS", + ) .optopt( "", "volume-ctrl", @@ -390,15 +422,51 @@ fn setup(args: &[String]) -> Setup { NormalisationType::from_str(gain_type).expect("Invalid normalisation type") }) .unwrap_or(NormalisationType::default()); + let normalisation_method = matches + .opt_str("normalisation-method") + .as_ref() + .map(|gain_type| { + NormalisationMethod::from_str(gain_type).expect("Invalid normalisation method") + }) + .unwrap_or(NormalisationMethod::default()); PlayerConfig { bitrate: bitrate, gapless: !matches.opt_present("disable-gapless"), normalisation: matches.opt_present("enable-volume-normalisation"), + normalisation_method: normalisation_method, normalisation_type: gain_type, normalisation_pregain: matches .opt_str("normalisation-pregain") .map(|pregain| pregain.parse::().expect("Invalid pregain float value")) .unwrap_or(PlayerConfig::default().normalisation_pregain), + normalisation_threshold: NormalisationData::db_to_ratio( + matches + .opt_str("normalisation-threshold") + .map(|threshold| { + threshold + .parse::() + .expect("Invalid threshold float value") + }) + .unwrap_or(PlayerConfig::default().normalisation_threshold), + ), + normalisation_attack: matches + .opt_str("normalisation-attack") + .map(|attack| attack.parse::().expect("Invalid attack float value")) + .unwrap_or(PlayerConfig::default().normalisation_attack * MILLIS) + / MILLIS, + normalisation_release: matches + .opt_str("normalisation-release") + .map(|release| release.parse::().expect("Invalid release float value")) + .unwrap_or(PlayerConfig::default().normalisation_release * MILLIS) + / MILLIS, + normalisation_steepness: matches + .opt_str("normalisation-steepness") + .map(|steepness| { + steepness + .parse::() + .expect("Invalid steepness float value") + }) + .unwrap_or(PlayerConfig::default().normalisation_steepness), passthrough, } }; From 1672eb87abd22f9c45e3086b8a2eb93c8c8fe6ed Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 2 Mar 2021 20:21:05 +0100 Subject: [PATCH 02/23] Fix build on Rust < 1.50.0 --- playback/src/player.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index c7edf3a4..b6b8ad5f 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1263,11 +1263,16 @@ impl PlayerInternal { } } + *sample = + (*sample as f64 * actual_normalisation_factor as f64) as f32; + // Extremely sharp attacks, however unlikely, *may* still clip and provide // undefined results, so strictly enforce output within [-1.0, 1.0]. - *sample = (*sample as f64 * actual_normalisation_factor as f64) - .clamp(-1.0, 1.0) - as f32; + if *sample < -1.0 { + *sample = -1.0; + } else if *sample > 1.0 { + *sample = 1.0; + } } } } From 5257be7824e0fd2c76cf889f88f82047080fed90 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 12 Mar 2021 23:05:38 +0100 Subject: [PATCH 03/23] Add command-line option to set F32 or S16 bit output Usage: `--format {F32|S16}`. Default is F32. - Implemented for all backends, except for JACK audio which itself only supports 32-bit output at this time. Setting JACK audio to S16 will panic and instruct the user to set output to F32. - The F32 default works fine for Rodio on macOS, but not on Raspian 10 with Alsa as host. Therefore users on Linux systems are warned to set output to S16 in case of garbled sound with Rodio. This seems an issue with cpal incorrectly detecting the output stream format. - While at it, DRY up lots of code in the backends and by that virtue, also enable OggData passthrough on the subprocess backend. - I tested Rodio, ALSA, pipe and subprocess quite a bit, and call on others to join in and test the other backends. --- audio/src/lib.rs | 7 + playback/Cargo.toml | 4 +- playback/src/audio_backend/alsa.rs | 88 ++++++---- playback/src/audio_backend/gstreamer.rs | 55 +++--- playback/src/audio_backend/jackaudio.rs | 25 +-- playback/src/audio_backend/mod.rs | 48 +++++- playback/src/audio_backend/pipe.rs | 55 +++--- playback/src/audio_backend/portaudio.rs | 113 +++++++++---- playback/src/audio_backend/pulseaudio.rs | 42 ++--- playback/src/audio_backend/rodio.rs | 202 ++++++++++------------- playback/src/audio_backend/sdl.rs | 87 +++++++--- playback/src/audio_backend/subprocess.rs | 24 +-- playback/src/config.rs | 24 +++ playback/src/player.rs | 14 +- src/main.rs | 30 +++- 15 files changed, 504 insertions(+), 314 deletions(-) diff --git a/audio/src/lib.rs b/audio/src/lib.rs index c4d862b3..8a9f88f5 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -58,6 +58,13 @@ impl AudioPacket { AudioPacket::OggData(d) => d.is_empty(), } } + + pub fn f32_to_s16(samples: &[f32]) -> Vec { + samples + .iter() + .map(|sample| (*sample as f64 * (0x7FFF as f64 + 0.5) - 0.5) as i16) + .collect() + } } #[cfg(not(any(feature = "with-tremor", feature = "with-vorbis")))] diff --git a/playback/Cargo.toml b/playback/Cargo.toml index b8995a4b..67e06be7 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -35,7 +35,7 @@ sdl2 = { version = "0.34", optional = true } gstreamer = { version = "0.16", optional = true } gstreamer-app = { version = "0.16", optional = true } glib = { version = "0.10", optional = true } -zerocopy = { version = "0.3", optional = true } +zerocopy = { version = "0.3" } [features] alsa-backend = ["alsa"] @@ -45,4 +45,4 @@ jackaudio-backend = ["jack"] rodiojack-backend = ["rodio", "cpal/jack"] rodio-backend = ["rodio", "cpal"] sdl-backend = ["sdl2"] -gstreamer-backend = ["gstreamer", "gstreamer-app", "glib", "zerocopy"] +gstreamer-backend = ["gstreamer", "gstreamer-app", "glib" ] diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 9bc17fe6..92b71f40 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -1,18 +1,21 @@ use super::{Open, Sink}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; +use crate::player::{NUM_CHANNELS, SAMPLES_PER_SECOND, SAMPLE_RATE}; use alsa::device_name::HintIter; use alsa::pcm::{Access, Format, Frames, HwParams, PCM}; use alsa::{Direction, Error, ValueOr}; use std::cmp::min; use std::ffi::CString; -use std::io; use std::process::exit; +use std::{io, mem}; -const PREFERRED_PERIOD_SIZE: Frames = 11025; // Period of roughly 125ms -const BUFFERED_PERIODS: Frames = 4; +const BUFFERED_LATENCY: f32 = 0.125; // seconds +const BUFFERED_PERIODS: u8 = 4; pub struct AlsaSink { pcm: Option, + format: AudioFormat, device: String, buffer: Vec, } @@ -34,25 +37,28 @@ fn list_outputs() { } } -fn open_device(dev_name: &str) -> Result<(PCM, Frames), Box> { +fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box> { let pcm = PCM::new(dev_name, Direction::Playback, false)?; - let mut period_size = PREFERRED_PERIOD_SIZE; + let (alsa_format, sample_size) = match format { + AudioFormat::F32 => (Format::float(), mem::size_of::()), + AudioFormat::S16 => (Format::s16(), mem::size_of::()), + }; + // http://www.linuxjournal.com/article/6735?page=0,1#N0x19ab2890.0x19ba78d8 // latency = period_size * periods / (rate * bytes_per_frame) - // For stereo samples encoded as 32-bit floats, one frame has a length of eight bytes. - // 500ms = buffer_size / (44100 * 8) - // buffer_size_bytes = 0.5 * 44100 / 8 - // buffer_size_frames = 0.5 * 44100 = 22050 - { - // Set hardware parameters: 44100 Hz / Stereo / 32-bit float - let hwp = HwParams::any(&pcm)?; + // For stereo samples encoded as 32-bit float, one frame has a length of eight bytes. + let mut period_size = ((SAMPLES_PER_SECOND * sample_size as u32) as f32 + * (BUFFERED_LATENCY / BUFFERED_PERIODS as f32)) as i32; + // Set hardware parameters: 44100 Hz / stereo / 32-bit float or 16-bit signed integer + { + let hwp = HwParams::any(&pcm)?; hwp.set_access(Access::RWInterleaved)?; - hwp.set_format(Format::float())?; - hwp.set_rate(44100, ValueOr::Nearest)?; - hwp.set_channels(2)?; + hwp.set_format(alsa_format)?; + hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest)?; + hwp.set_channels(NUM_CHANNELS as u32)?; period_size = hwp.set_period_size_near(period_size, ValueOr::Greater)?; - hwp.set_buffer_size_near(period_size * BUFFERED_PERIODS)?; + hwp.set_buffer_size_near(period_size * BUFFERED_PERIODS as i32)?; pcm.hw_params(&hwp)?; let swp = pcm.sw_params_current()?; @@ -64,12 +70,12 @@ fn open_device(dev_name: &str) -> Result<(PCM, Frames), Box> { } impl Open for AlsaSink { - fn open(device: Option) -> AlsaSink { - info!("Using alsa sink"); + fn open(device: Option, format: AudioFormat) -> AlsaSink { + info!("Using Alsa sink with format: {:?}", format); let name = match device.as_ref().map(AsRef::as_ref) { Some("?") => { - println!("Listing available alsa outputs"); + println!("Listing available Alsa outputs:"); list_outputs(); exit(0) } @@ -80,6 +86,7 @@ impl Open for AlsaSink { AlsaSink { pcm: None, + format: format, device: name, buffer: vec![], } @@ -89,12 +96,13 @@ impl Open for AlsaSink { impl Sink for AlsaSink { fn start(&mut self) -> io::Result<()> { if self.pcm.is_none() { - let pcm = open_device(&self.device); + let pcm = open_device(&self.device, self.format); match pcm { Ok((p, period_size)) => { self.pcm = Some(p); // Create a buffer for all samples for a full period - self.buffer = Vec::with_capacity((period_size * 2) as usize); + self.buffer = + Vec::with_capacity((period_size * BUFFERED_PERIODS as i32) as usize); } Err(e) => { error!("Alsa error PCM open {}", e); @@ -111,14 +119,10 @@ impl Sink for AlsaSink { fn stop(&mut self) -> io::Result<()> { { - let pcm = self.pcm.as_mut().unwrap(); // Write any leftover data in the period buffer // before draining the actual buffer - let io = pcm.io_f32().unwrap(); - match io.writei(&self.buffer[..]) { - Ok(_) => (), - Err(err) => pcm.try_recover(err, false).unwrap(), - } + self.write_buf().expect("could not flush buffer"); + let pcm = self.pcm.as_mut().unwrap(); pcm.drain().unwrap(); } self.pcm = None; @@ -137,12 +141,7 @@ impl Sink for AlsaSink { .extend_from_slice(&data[processed_data..processed_data + data_to_buffer]); processed_data += data_to_buffer; if self.buffer.len() == self.buffer.capacity() { - let pcm = self.pcm.as_mut().unwrap(); - let io = pcm.io_f32().unwrap(); - match io.writei(&self.buffer) { - Ok(_) => (), - Err(err) => pcm.try_recover(err, false).unwrap(), - } + self.write_buf().expect("could not append to buffer"); self.buffer.clear(); } } @@ -150,3 +149,26 @@ impl Sink for AlsaSink { Ok(()) } } + +impl AlsaSink { + fn write_buf(&mut self) -> io::Result<()> { + let pcm = self.pcm.as_mut().unwrap(); + let io_result = match self.format { + AudioFormat::F32 => { + let io = pcm.io_f32().unwrap(); + io.writei(&self.buffer) + } + AudioFormat::S16 => { + let io = pcm.io_i16().unwrap(); + let buf_s16: Vec = AudioPacket::f32_to_s16(&self.buffer); + io.writei(&buf_s16[..]) + } + }; + match io_result { + Ok(_) => (), + Err(err) => pcm.try_recover(err, false).unwrap(), + }; + + Ok(()) + } +} diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 1ad3631e..17ad86e6 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -1,21 +1,29 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkAsBytes}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use gst::prelude::*; use gst::*; use std::sync::mpsc::{sync_channel, SyncSender}; use std::{io, thread}; -use zerocopy::*; +use zerocopy::AsBytes; #[allow(dead_code)] pub struct GstreamerSink { tx: SyncSender>, pipeline: gst::Pipeline, + format: AudioFormat, } impl Open for GstreamerSink { - fn open(device: Option) -> GstreamerSink { - gst::init().expect("Failed to init gstreamer!"); - let pipeline_str_preamble = r#"appsrc caps="audio/x-raw,format=F32,layout=interleaved,channels=2,rate=44100" block=true max-bytes=4096 name=appsrc0 "#; + fn open(device: Option, format: AudioFormat) -> GstreamerSink { + info!("Using GStreamer sink with format: {:?}", format); + + gst::init().expect("failed to init GStreamer!"); + let pipeline_str_preamble = format!( + r#"appsrc caps="audio/x-raw,format={:?},layout=interleaved,channels={},rate={}" block=true max-bytes=4096 name=appsrc0 "#, + format, NUM_CHANNELS, SAMPLE_RATE + ); let pipeline_str_rest = r#" ! audioconvert ! autoaudiosink"#; let pipeline_str: String = match device { Some(x) => format!("{}{}", pipeline_str_preamble, x), @@ -27,25 +35,25 @@ impl Open for GstreamerSink { let pipelinee = gst::parse_launch(&*pipeline_str).expect("Couldn't launch pipeline; likely a GStreamer issue or an error in the pipeline string you specified in the 'device' argument to librespot."); let pipeline = pipelinee .dynamic_cast::() - .expect("Couldn't cast pipeline element at runtime!"); - let bus = pipeline.get_bus().expect("Couldn't get bus from pipeline"); + .expect("couldn't cast pipeline element at runtime!"); + let bus = pipeline.get_bus().expect("couldn't get bus from pipeline"); let mainloop = glib::MainLoop::new(None, false); let appsrce: gst::Element = pipeline .get_by_name("appsrc0") - .expect("Couldn't get appsrc from pipeline"); + .expect("couldn't get appsrc from pipeline"); let appsrc: gst_app::AppSrc = appsrce .dynamic_cast::() - .expect("Couldn't cast AppSrc element at runtime!"); + .expect("couldn't cast AppSrc element at runtime!"); let bufferpool = gst::BufferPool::new(); - let appsrc_caps = appsrc.get_caps().expect("Couldn't get appsrc caps"); + let appsrc_caps = appsrc.get_caps().expect("couldn't get appsrc caps"); let mut conf = bufferpool.get_config(); conf.set_params(Some(&appsrc_caps), 8192, 0, 0); bufferpool .set_config(conf) - .expect("Couldn't configure the buffer pool"); + .expect("couldn't configure the buffer pool"); bufferpool .set_active(true) - .expect("Couldn't activate buffer pool"); + .expect("couldn't activate buffer pool"); let (tx, rx) = sync_channel::>(128); thread::spawn(move || { @@ -57,7 +65,7 @@ impl Open for GstreamerSink { mutbuf.set_size(data.len()); mutbuf .copy_from_slice(0, data.as_bytes()) - .expect("Failed to copy from slice"); + .expect("failed to copy from slice"); let _eat = appsrc.push_buffer(okbuffer); } } @@ -83,33 +91,32 @@ impl Open for GstreamerSink { glib::Continue(true) }) - .expect("Failed to add bus watch"); + .expect("failed to add bus watch"); thread_mainloop.run(); }); pipeline .set_state(gst::State::Playing) - .expect("Unable to set the pipeline to the `Playing` state"); + .expect("unable to set the pipeline to the `Playing` state"); GstreamerSink { tx: tx, pipeline: pipeline, + format: format, } } } impl Sink for GstreamerSink { - fn start(&mut self) -> io::Result<()> { - Ok(()) - } - fn stop(&mut self) -> io::Result<()> { - Ok(()) - } - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + start_stop_noop!(); + sink_as_bytes!(); +} + +impl SinkAsBytes for GstreamerSink { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { // Copy expensively (in to_vec()) to avoid thread synchronization - let deighta: &[u8] = packet.samples().as_bytes(); self.tx - .send(deighta.to_vec()) + .send(data.to_vec()) .expect("tx send failed in write function"); Ok(()) } diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index e95933fc..295941a4 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -1,10 +1,12 @@ use super::{Open, Sink}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; +use crate::player::NUM_CHANNELS; use jack::{ AsyncClient, AudioOut, Client, ClientOptions, Control, Port, ProcessHandler, ProcessScope, }; -use std::io; use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; +use std::{io, mem}; pub struct JackSink { send: SyncSender, @@ -39,8 +41,15 @@ impl ProcessHandler for JackData { } impl Open for JackSink { - fn open(client_name: Option) -> JackSink { - info!("Using jack sink!"); + fn open(client_name: Option, format: AudioFormat) -> JackSink { + info!("Using JACK sink with format {:?}", format); + + if format != AudioFormat::F32 { + panic!( + "JACK sink only supports 32-bit floating point output. Use `--format {:?}`", + AudioFormat::F32 + ); + } let client_name = client_name.unwrap_or("librespot".to_string()); let (client, _status) = @@ -48,7 +57,7 @@ impl Open for JackSink { let ch_r = client.register_port("out_0", AudioOut::default()).unwrap(); let ch_l = client.register_port("out_1", AudioOut::default()).unwrap(); // buffer for samples from librespot (~10ms) - let (tx, rx) = sync_channel(2 * 1024 * 4); + let (tx, rx) = sync_channel::(NUM_CHANNELS as usize * 1024 * mem::size_of::()); let jack_data = JackData { rec: rx, port_l: ch_l, @@ -64,13 +73,7 @@ impl Open for JackSink { } impl Sink for JackSink { - fn start(&mut self) -> io::Result<()> { - Ok(()) - } - - fn stop(&mut self) -> io::Result<()> { - Ok(()) - } + start_stop_noop!(); fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { for s in packet.samples().iter() { diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 3f5dae8d..550ebb84 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -1,8 +1,9 @@ use crate::audio::AudioPacket; +use crate::config::AudioFormat; use std::io; pub trait Open { - fn open(_: Option) -> Self; + fn open(_: Option, format: AudioFormat) -> Self; } pub trait Sink { @@ -11,8 +12,42 @@ pub trait Sink { fn write(&mut self, packet: &AudioPacket) -> io::Result<()>; } -fn mk_sink(device: Option) -> Box { - Box::new(S::open(device)) +pub trait SinkAsBytes { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()>; +} + +fn mk_sink(device: Option, format: AudioFormat) -> Box { + Box::new(S::open(device, format)) +} + +// reuse code for various backends +macro_rules! sink_as_bytes { + () => { + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + use zerocopy::AsBytes; + match packet { + AudioPacket::Samples(samples) => match self.format { + AudioFormat::F32 => self.write_bytes(samples.as_bytes()), + AudioFormat::S16 => { + let samples_s16 = AudioPacket::f32_to_s16(samples); + self.write_bytes(samples_s16.as_bytes()) + } + }, + AudioPacket::OggData(samples) => self.write_bytes(samples), + } + } + }; +} + +macro_rules! start_stop_noop { + () => { + fn start(&mut self) -> io::Result<()> { + Ok(()) + } + fn stop(&mut self) -> io::Result<()> { + Ok(()) + } + }; } #[cfg(feature = "alsa-backend")] @@ -68,7 +103,10 @@ use self::pipe::StdoutSink; mod subprocess; use self::subprocess::SubprocessSink; -pub const BACKENDS: &'static [(&'static str, fn(Option) -> Box)] = &[ +pub const BACKENDS: &'static [( + &'static str, + fn(Option, AudioFormat) -> Box, +)] = &[ #[cfg(feature = "alsa-backend")] ("alsa", mk_sink::), #[cfg(feature = "portaudio-backend")] @@ -92,7 +130,7 @@ pub const BACKENDS: &'static [(&'static str, fn(Option) -> Box ("subprocess", mk_sink::), ]; -pub fn find(name: Option) -> Option) -> Box> { +pub fn find(name: Option) -> Option, AudioFormat) -> Box> { if let Some(name) = name { BACKENDS .iter() diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index 5516ee94..3a90d06f 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -1,46 +1,39 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkAsBytes}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; use std::fs::OpenOptions; use std::io::{self, Write}; -use std::mem; -use std::slice; -pub struct StdoutSink(Box); +pub struct StdoutSink { + output: Box, + format: AudioFormat, +} impl Open for StdoutSink { - fn open(path: Option) -> StdoutSink { - if let Some(path) = path { - let file = OpenOptions::new().write(true).open(path).unwrap(); - StdoutSink(Box::new(file)) - } else { - StdoutSink(Box::new(io::stdout())) + fn open(path: Option, format: AudioFormat) -> StdoutSink { + info!("Using pipe sink with format: {:?}", format); + + let output: Box = match path { + Some(path) => Box::new(OpenOptions::new().write(true).open(path).unwrap()), + _ => Box::new(io::stdout()), + }; + + StdoutSink { + output: output, + format: format, } } } impl Sink for StdoutSink { - fn start(&mut self) -> io::Result<()> { - Ok(()) - } - - fn stop(&mut self) -> io::Result<()> { - Ok(()) - } - - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - let data: &[u8] = match packet { - AudioPacket::Samples(data) => unsafe { - slice::from_raw_parts( - data.as_ptr() as *const u8, - data.len() * mem::size_of::(), - ) - }, - AudioPacket::OggData(data) => data, - }; - - self.0.write_all(data)?; - self.0.flush()?; + start_stop_noop!(); + sink_as_bytes!(); +} +impl SinkAsBytes for StdoutSink { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { + self.output.write_all(data)?; + self.output.flush()?; Ok(()) } } diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 0b8eac0b..70caedd7 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -1,5 +1,7 @@ use super::{Open, Sink}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use portaudio_rs; use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo}; use portaudio_rs::stream::*; @@ -7,10 +9,16 @@ use std::io; use std::process::exit; use std::time::Duration; -pub struct PortAudioSink<'a>( - Option>, - StreamParameters, -); +pub enum PortAudioSink<'a> { + F32( + Option>, + StreamParameters, + ), + S16( + Option>, + StreamParameters, + ), +} fn output_devices() -> Box> { let count = portaudio_rs::device::get_count().unwrap(); @@ -40,8 +48,8 @@ fn find_output(device: &str) -> Option { } impl<'a> Open for PortAudioSink<'a> { - fn open(device: Option) -> PortAudioSink<'a> { - debug!("Using PortAudio sink"); + fn open(device: Option, format: AudioFormat) -> PortAudioSink<'a> { + info!("Using PortAudio sink with format: {:?}", format); portaudio_rs::initialize().unwrap(); @@ -53,7 +61,7 @@ impl<'a> Open for PortAudioSink<'a> { Some(device) => find_output(device), None => get_default_output_index(), } - .expect("Could not find device"); + .expect("could not find device"); let info = portaudio_rs::device::get_info(device_idx); let latency = match info { @@ -61,46 +69,87 @@ impl<'a> Open for PortAudioSink<'a> { None => Duration::new(0, 0), }; - let params = StreamParameters { - device: device_idx, - channel_count: 2, - suggested_latency: latency, - data: 0.0, - }; - - PortAudioSink(None, params) + macro_rules! open_sink { + ($sink: expr, $data: expr) => {{ + let params = StreamParameters { + device: device_idx, + channel_count: NUM_CHANNELS as u32, + suggested_latency: latency, + data: $data, + }; + $sink(None, params) + }}; + } + match format { + AudioFormat::F32 => open_sink!(PortAudioSink::F32, 0.0), + AudioFormat::S16 => open_sink!(PortAudioSink::S16, 0), + } } } impl<'a> Sink for PortAudioSink<'a> { fn start(&mut self) -> io::Result<()> { - if self.0.is_none() { - self.0 = Some( - Stream::open( - None, - Some(self.1), - 44100.0, - FRAMES_PER_BUFFER_UNSPECIFIED, - StreamFlags::empty(), - None, - ) - .unwrap(), - ); + macro_rules! start_sink { + ($stream: ident, $parameters: ident) => {{ + if $stream.is_none() { + *$stream = Some( + Stream::open( + None, + Some(*$parameters), + SAMPLE_RATE as f64, + FRAMES_PER_BUFFER_UNSPECIFIED, + StreamFlags::empty(), + None, + ) + .unwrap(), + ); + } + $stream.as_mut().unwrap().start().unwrap() + }}; } + match self { + PortAudioSink::F32(stream, parameters) => start_sink!(stream, parameters), + PortAudioSink::S16(stream, parameters) => start_sink!(stream, parameters), + }; - self.0.as_mut().unwrap().start().unwrap(); Ok(()) } + fn stop(&mut self) -> io::Result<()> { - self.0.as_mut().unwrap().stop().unwrap(); - self.0 = None; + macro_rules! stop_sink { + ($stream: expr) => {{ + $stream.as_mut().unwrap().stop().unwrap(); + *$stream = None; + }}; + } + match self { + PortAudioSink::F32(stream, _parameters) => stop_sink!(stream), + PortAudioSink::S16(stream, _parameters) => stop_sink!(stream), + }; + Ok(()) } + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - match self.0.as_mut().unwrap().write(packet.samples()) { + macro_rules! write_sink { + ($stream: expr, $samples: expr) => { + $stream.as_mut().unwrap().write($samples) + }; + } + let result = match self { + PortAudioSink::F32(stream, _parameters) => { + let samples = packet.samples(); + write_sink!(stream, &samples) + } + PortAudioSink::S16(stream, _parameters) => { + let samples_s16: Vec = AudioPacket::f32_to_s16(packet.samples()); + write_sink!(stream, &samples_s16) + } + }; + match result { Ok(_) => (), Err(portaudio_rs::PaError::OutputUnderflowed) => error!("PortAudio write underflow"), - Err(e) => panic!("PA Error {}", e), + Err(e) => panic!("PortAudio error {}", e), }; Ok(()) diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 4dca2108..8c1e8e83 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -1,9 +1,10 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkAsBytes}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use libpulse_binding::{self as pulse, stream::Direction}; use libpulse_simple_binding::Simple; use std::io; -use std::mem; const APP_NAME: &str = "librespot"; const STREAM_NAME: &str = "Spotify endpoint"; @@ -12,16 +13,22 @@ pub struct PulseAudioSink { s: Option, ss: pulse::sample::Spec, device: Option, + format: AudioFormat, } impl Open for PulseAudioSink { - fn open(device: Option) -> PulseAudioSink { - debug!("Using PulseAudio sink"); + fn open(device: Option, format: AudioFormat) -> PulseAudioSink { + info!("Using PulseAudio sink with format: {:?}", format); + + let pulse_format = match format { + AudioFormat::F32 => pulse::sample::Format::F32le, + AudioFormat::S16 => pulse::sample::Format::S16le, + }; let ss = pulse::sample::Spec { - format: pulse::sample::Format::F32le, - channels: 2, // stereo - rate: 44100, + format: pulse_format, + channels: NUM_CHANNELS, + rate: SAMPLE_RATE, }; debug_assert!(ss.is_valid()); @@ -29,6 +36,7 @@ impl Open for PulseAudioSink { s: None, ss: ss, device: device, + format: format, } } } @@ -67,19 +75,13 @@ impl Sink for PulseAudioSink { Ok(()) } - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - if let Some(s) = &self.s { - // SAFETY: An f32 consists of four bytes, so that the given slice can be interpreted - // as a byte array of four. Each byte pointer is validly aligned, and so is the newly - // created slice. - let d: &[u8] = unsafe { - std::slice::from_raw_parts( - packet.samples().as_ptr() as *const u8, - packet.samples().len() * mem::size_of::(), - ) - }; + sink_as_bytes!(); +} - match s.write(d) { +impl SinkAsBytes for PulseAudioSink { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { + if let Some(s) = &self.s { + match s.write(data) { Ok(_) => Ok(()), Err(e) => Err(io::Error::new( io::ErrorKind::BrokenPipe, @@ -89,7 +91,7 @@ impl Sink for PulseAudioSink { } else { Err(io::Error::new( io::ErrorKind::NotConnected, - "Not connected to pulseaudio", + "Not connected to PulseAudio", )) } } diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 6c996e85..7571aa20 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -2,33 +2,90 @@ use super::{Open, Sink}; extern crate cpal; extern crate rodio; use crate::audio::AudioPacket; +use crate::config::AudioFormat; use cpal::traits::{DeviceTrait, HostTrait}; use std::process::exit; use std::{io, thread, time}; -pub struct RodioSink { - rodio_sink: rodio::Sink, - // We have to keep hold of this object, or the Sink can't play... - #[allow(dead_code)] - stream: rodio::OutputStream, +// most code is shared between RodioSink and JackRodioSink +macro_rules! rodio_sink { + ($name: ident) => { + pub struct $name { + rodio_sink: rodio::Sink, + // We have to keep hold of this object, or the Sink can't play... + #[allow(dead_code)] + stream: rodio::OutputStream, + format: AudioFormat, + } + + impl Sink for $name { + start_stop_noop!(); + + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + let samples = packet.samples(); + match self.format { + AudioFormat::F32 => { + let source = rodio::buffer::SamplesBuffer::new(2, 44100, samples); + self.rodio_sink.append(source) + } + AudioFormat::S16 => { + let samples_s16: Vec = AudioPacket::f32_to_s16(samples); + let source = rodio::buffer::SamplesBuffer::new(2, 44100, samples_s16); + self.rodio_sink.append(source) + } + }; + + // Chunk sizes seem to be about 256 to 3000 ish items long. + // Assuming they're on average 1628 then a half second buffer is: + // 44100 elements --> about 27 chunks + while self.rodio_sink.len() > 26 { + // sleep and wait for rodio to drain a bit + thread::sleep(time::Duration::from_millis(10)); + } + Ok(()) + } + } + + impl $name { + fn open_sink(host: &cpal::Host, device: Option, format: AudioFormat) -> $name { + if format != AudioFormat::S16 { + #[cfg(target_os = "linux")] + { + warn!("Rodio output to Alsa is known to cause garbled sound on output formats other than 16-bit signed integer."); + warn!("Consider using `--backend alsa` OR `--format {:?}`", AudioFormat::S16); + } + } + + let rodio_device = match_device(&host, device); + debug!("Using cpal device"); + let stream = rodio::OutputStream::try_from_device(&rodio_device) + .expect("couldn't open output stream."); + debug!("Using Rodio stream"); + let sink = rodio::Sink::try_new(&stream.1).expect("couldn't create output sink."); + debug!("Using Rodio sink"); + + $name { + rodio_sink: sink, + stream: stream.0, + format: format, + } + } + } + }; } +rodio_sink!(RodioSink); #[cfg(all( feature = "rodiojack-backend", any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd") ))] -pub struct JackRodioSink { - jackrodio_sink: rodio::Sink, - // We have to keep hold of this object, or the Sink can't play... - #[allow(dead_code)] - stream: rodio::OutputStream, -} +rodio_sink!(JackRodioSink); fn list_formats(ref device: &rodio::Device) { let default_fmt = match device.default_output_config() { Ok(fmt) => cpal::SupportedStreamConfig::from(fmt), Err(e) => { - warn!("Error getting default rodio::Sink config: {}", e); + warn!("Error getting default Rodio output config: {}", e); return; } }; @@ -38,13 +95,13 @@ fn list_formats(ref device: &rodio::Device) { let mut output_configs = match device.supported_output_configs() { Ok(f) => f.peekable(), Err(e) => { - warn!("Error getting supported rodio::Sink configs: {}", e); + warn!("Error getting supported Rodio output configs: {}", e); return; } }; if output_configs.peek().is_some() { - debug!(" Available configs:"); + debug!(" Available output configs:"); for format in output_configs { debug!(" {:?}", format); } @@ -54,13 +111,13 @@ fn list_formats(ref device: &rodio::Device) { fn list_outputs(ref host: &cpal::Host) { let default_device = get_default_device(host); let default_device_name = default_device.name().expect("cannot get output name"); - println!("Default Audio Device:\n {}", default_device_name); + println!("Default audio device:\n {}", default_device_name); list_formats(&default_device); - println!("Other Available Audio Devices:"); + println!("Other available audio devices:"); let found_devices = host.output_devices().expect(&format!( - "Cannot get list of output devices of Host: {:?}", + "Cannot get list of output devices of host: {:?}", host.id() )); for device in found_devices { @@ -86,7 +143,7 @@ fn match_device(ref host: &cpal::Host, device: Option) -> rodio::Device } let found_devices = host.output_devices().expect(&format!( - "Cannot get list of output devices of Host: {:?}", + "cannot get list of output devices of host: {:?}", host.id() )); for d in found_devices { @@ -102,22 +159,14 @@ fn match_device(ref host: &cpal::Host, device: Option) -> rodio::Device } impl Open for RodioSink { - fn open(device: Option) -> RodioSink { + fn open(device: Option, format: AudioFormat) -> RodioSink { let host = cpal::default_host(); - debug!("Using rodio sink with cpal host: {:?}", host.id()); - - let rodio_device = match_device(&host, device); - debug!("Using cpal device"); - let stream = rodio::OutputStream::try_from_device(&rodio_device) - .expect("Couldn't open output stream."); - debug!("Using rodio stream"); - let sink = rodio::Sink::try_new(&stream.1).expect("Couldn't create output sink."); - debug!("Using rodio sink"); - - RodioSink { - rodio_sink: sink, - stream: stream.0, - } + info!( + "Using Rodio sink with format {:?} and cpal host: {:?}", + format, + host.id() + ); + Self::open_sink(&host, device, format) } } @@ -126,89 +175,18 @@ impl Open for RodioSink { any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd") ))] impl Open for JackRodioSink { - fn open(device: Option) -> JackRodioSink { + fn open(device: Option, format: AudioFormat) -> JackRodioSink { let host = cpal::host_from_id( cpal::available_hosts() .into_iter() .find(|id| *id == cpal::HostId::Jack) - .expect("Jack Host not found"), + .expect("JACK host not found"), ) - .expect("Jack Host not found"); - debug!("Using jack rodio sink with cpal Jack host"); - - let rodio_device = match_device(&host, device); - debug!("Using cpal device"); - let stream = rodio::OutputStream::try_from_device(&rodio_device) - .expect("Couldn't open output stream."); - debug!("Using jack rodio stream"); - let sink = rodio::Sink::try_new(&stream.1).expect("Couldn't create output sink."); - debug!("Using jack rodio sink"); - - JackRodioSink { - jackrodio_sink: sink, - stream: stream.0, - } - } -} - -impl Sink for RodioSink { - fn start(&mut self) -> io::Result<()> { - // More similar to an "unpause" than "play". Doesn't undo "stop". - // self.rodio_sink.play(); - Ok(()) - } - - fn stop(&mut self) -> io::Result<()> { - // This will immediately stop playback, but the sink is then unusable. - // We just have to let the current buffer play till the end. - // self.rodio_sink.stop(); - Ok(()) - } - - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - let source = rodio::buffer::SamplesBuffer::new(2, 44100, packet.samples()); - self.rodio_sink.append(source); - - // Chunk sizes seem to be about 256 to 3000 ish items long. - // Assuming they're on average 1628 then a half second buffer is: - // 44100 elements --> about 27 chunks - while self.rodio_sink.len() > 26 { - // sleep and wait for rodio to drain a bit - thread::sleep(time::Duration::from_millis(10)); - } - Ok(()) - } -} - -#[cfg(all( - feature = "rodiojack-backend", - any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd") -))] -impl Sink for JackRodioSink { - fn start(&mut self) -> io::Result<()> { - // More similar to an "unpause" than "play". Doesn't undo "stop". - // self.rodio_sink.play(); - Ok(()) - } - - fn stop(&mut self) -> io::Result<()> { - // This will immediately stop playback, but the sink is then unusable. - // We just have to let the current buffer play till the end. - // self.rodio_sink.stop(); - Ok(()) - } - - fn write(&mut self, data: &[f32]) -> io::Result<()> { - let source = rodio::buffer::SamplesBuffer::new(2, 44100, data); - self.jackrodio_sink.append(source); - - // Chunk sizes seem to be about 256 to 3000 ish items long. - // Assuming they're on average 1628 then a half second buffer is: - // 44100 elements --> about 27 chunks - while self.jackrodio_sink.len() > 26 { - // sleep and wait for rodio to drain a bit - thread::sleep(time::Duration::from_millis(10)); - } - Ok(()) + .expect("JACK host not found"); + info!( + "Using JACK Rodio sink with format {:?} and cpal JACK host", + format + ); + Self::open_sink(&host, device, format) } } diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 727615d1..6e52b322 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -1,57 +1,98 @@ use super::{Open, Sink}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use sdl2::audio::{AudioQueue, AudioSpecDesired}; -use std::{io, thread, time}; +use std::{io, mem, thread, time}; -type Channel = f32; - -pub struct SdlSink { - queue: AudioQueue, +pub enum SdlSink { + F32(AudioQueue), + S16(AudioQueue), } impl Open for SdlSink { - fn open(device: Option) -> SdlSink { - debug!("Using SDL sink"); + fn open(device: Option, format: AudioFormat) -> SdlSink { + info!("Using SDL sink with format: {:?}", format); if device.is_some() { panic!("SDL sink does not support specifying a device name"); } - let ctx = sdl2::init().expect("Could not init SDL"); - let audio = ctx.audio().expect("Could not init SDL audio subsystem"); + let ctx = sdl2::init().expect("could not initialize SDL"); + let audio = ctx + .audio() + .expect("could not initialize SDL audio subsystem"); let desired_spec = AudioSpecDesired { - freq: Some(44_100), - channels: Some(2), + freq: Some(SAMPLE_RATE as i32), + channels: Some(NUM_CHANNELS), samples: None, }; - let queue = audio - .open_queue(None, &desired_spec) - .expect("Could not open SDL audio device"); - SdlSink { queue: queue } + macro_rules! open_sink { + ($sink: expr, $type: ty) => {{ + let queue: AudioQueue<$type> = audio + .open_queue(None, &desired_spec) + .expect("could not open SDL audio device"); + $sink(queue) + }}; + } + match format { + AudioFormat::F32 => open_sink!(SdlSink::F32, f32), + AudioFormat::S16 => open_sink!(SdlSink::S16, i16), + } } } impl Sink for SdlSink { fn start(&mut self) -> io::Result<()> { - self.queue.clear(); - self.queue.resume(); + macro_rules! start_sink { + ($queue: expr) => {{ + $queue.clear(); + $queue.resume(); + }}; + } + match self { + SdlSink::F32(queue) => start_sink!(queue), + SdlSink::S16(queue) => start_sink!(queue), + }; Ok(()) } fn stop(&mut self) -> io::Result<()> { - self.queue.pause(); - self.queue.clear(); + macro_rules! stop_sink { + ($queue: expr) => {{ + $queue.pause(); + $queue.clear(); + }}; + } + match self { + SdlSink::F32(queue) => stop_sink!(queue), + SdlSink::S16(queue) => stop_sink!(queue), + }; Ok(()) } fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - while self.queue.size() > (2 * 4 * 44_100) { - // sleep and wait for sdl thread to drain the queue a bit - thread::sleep(time::Duration::from_millis(10)); + macro_rules! drain_sink { + ($queue: expr, $size: expr) => {{ + // sleep and wait for sdl thread to drain the queue a bit + while $queue.size() > (NUM_CHANNELS as u32 * $size as u32 * SAMPLE_RATE) { + thread::sleep(time::Duration::from_millis(10)); + } + }}; } - self.queue.queue(packet.samples()); + match self { + SdlSink::F32(queue) => { + drain_sink!(queue, mem::size_of::()); + queue.queue(packet.samples()) + } + SdlSink::S16(queue) => { + drain_sink!(queue, mem::size_of::()); + let samples_s16: Vec = AudioPacket::f32_to_s16(packet.samples()); + queue.queue(&samples_s16) + } + }; Ok(()) } } diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index 123e0233..586bb75b 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -1,22 +1,25 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkAsBytes}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; use shell_words::split; use std::io::{self, Write}; -use std::mem; use std::process::{Child, Command, Stdio}; -use std::slice; pub struct SubprocessSink { shell_command: String, child: Option, + format: AudioFormat, } impl Open for SubprocessSink { - fn open(shell_command: Option) -> SubprocessSink { + fn open(shell_command: Option, format: AudioFormat) -> SubprocessSink { + info!("Using subprocess sink with format: {:?}", format); + if let Some(shell_command) = shell_command { SubprocessSink { shell_command: shell_command, child: None, + format: format, } } else { panic!("subprocess sink requires specifying a shell command"); @@ -44,16 +47,15 @@ impl Sink for SubprocessSink { Ok(()) } - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - let data: &[u8] = unsafe { - slice::from_raw_parts( - packet.samples().as_ptr() as *const u8, - packet.samples().len() * mem::size_of::(), - ) - }; + sink_as_bytes!(); +} + +impl SinkAsBytes for SubprocessSink { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { if let Some(child) = &mut self.child { let child_stdin = child.stdin.as_mut().unwrap(); child_stdin.write_all(data)?; + child_stdin.flush()?; } Ok(()) } diff --git a/playback/src/config.rs b/playback/src/config.rs index be15b268..e1ed8dcf 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -1,3 +1,4 @@ +use std::convert::TryFrom; use std::str::FromStr; #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] @@ -25,6 +26,29 @@ impl Default for Bitrate { } } +#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] +pub enum AudioFormat { + F32, + S16, +} + +impl TryFrom<&String> for AudioFormat { + type Error = (); + fn try_from(s: &String) -> Result { + match s.to_uppercase().as_str() { + "F32" => Ok(AudioFormat::F32), + "S16" => Ok(AudioFormat::S16), + _ => unimplemented!(), + } + } +} + +impl Default for AudioFormat { + fn default() -> AudioFormat { + AudioFormat::F32 + } +} + #[derive(Clone, Debug)] pub enum NormalisationType { Album, diff --git a/playback/src/player.rs b/playback/src/player.rs index b6b8ad5f..dbc09695 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -25,8 +25,12 @@ use crate::audio_backend::Sink; use crate::metadata::{AudioItem, FileFormat}; use crate::mixer::AudioFilter; +pub const SAMPLE_RATE: u32 = 44100; +pub const NUM_CHANNELS: u8 = 2; +pub const SAMPLES_PER_SECOND: u32 = SAMPLE_RATE as u32 * NUM_CHANNELS as u32; + const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000; -const SAMPLES_PER_SECOND: u32 = 44100 * 2; +const DB_VOLTAGE_RATIO: f32 = 20.0; pub struct Player { commands: Option>, @@ -202,11 +206,11 @@ pub struct NormalisationData { impl NormalisationData { pub fn db_to_ratio(db: f32) -> f32 { - return f32::powf(10.0, db / 20.0); + return f32::powf(10.0, db / DB_VOLTAGE_RATIO); } pub fn ratio_to_db(ratio: f32) -> f32 { - return ratio.log10() * 20.0; + return ratio.log10() * DB_VOLTAGE_RATIO; } fn parse_from_file(mut file: T) -> Result { @@ -937,8 +941,8 @@ impl Future for PlayerInternal { if !self.config.passthrough { if let Some(ref packet) = packet { - *stream_position_pcm = - *stream_position_pcm + (packet.samples().len() / 2) as u64; + *stream_position_pcm = *stream_position_pcm + + (packet.samples().len() / NUM_CHANNELS as usize) as u64; let stream_position_millis = Self::position_pcm_to_ms(*stream_position_pcm); diff --git a/src/main.rs b/src/main.rs index 91a58659..a7cd8b30 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use futures::sync::mpsc::UnboundedReceiver; use futures::{Async, Future, Poll, Stream}; use log::{error, info, trace, warn}; use sha1::{Digest, Sha1}; +use std::convert::TryFrom; use std::env; use std::io::{stderr, Write}; use std::mem; @@ -22,7 +23,9 @@ use librespot::core::version; use librespot::connect::discovery::{discovery, DiscoveryStream}; use librespot::connect::spirc::{Spirc, SpircTask}; use librespot::playback::audio_backend::{self, Sink, BACKENDS}; -use librespot::playback::config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}; +use librespot::playback::config::{ + AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, +}; use librespot::playback::mixer::{self, Mixer, MixerConfig}; use librespot::playback::player::{NormalisationData, Player, PlayerEvent}; @@ -85,7 +88,8 @@ fn print_version() { #[derive(Clone)] struct Setup { - backend: fn(Option) -> Box, + format: AudioFormat, + backend: fn(Option, AudioFormat) -> Box, device: Option, mixer: fn(Option) -> Box, @@ -149,6 +153,12 @@ fn setup(args: &[String]) -> Setup { "Audio device to use. Use '?' to list options if using portaudio or alsa", "DEVICE", ) + .optopt( + "", + "format", + "Output format (F32 or S16). Defaults to F32", + "FORMAT", + ) .optopt("", "mixer", "Mixer to use (alsa or softvol)", "MIXER") .optopt( "m", @@ -292,9 +302,15 @@ fn setup(args: &[String]) -> Setup { let backend = audio_backend::find(backend_name).expect("Invalid backend"); + let format = matches + .opt_str("format") + .as_ref() + .map(|format| AudioFormat::try_from(format).expect("Invalid output format")) + .unwrap_or(AudioFormat::default()); + let device = matches.opt_str("device"); if device == Some("?".into()) { - backend(device); + backend(device, format); exit(0); } @@ -496,6 +512,7 @@ fn setup(args: &[String]) -> Setup { let enable_discovery = !matches.opt_present("disable-discovery"); Setup { + format: format, backend: backend, cache: cache, session_config: session_config, @@ -517,7 +534,8 @@ struct Main { player_config: PlayerConfig, session_config: SessionConfig, connect_config: ConnectConfig, - backend: fn(Option) -> Box, + format: AudioFormat, + backend: fn(Option, AudioFormat) -> Box, device: Option, mixer: fn(Option) -> Box, mixer_config: MixerConfig, @@ -547,6 +565,7 @@ impl Main { session_config: setup.session_config, player_config: setup.player_config, connect_config: setup.connect_config, + format: setup.format, backend: setup.backend, device: setup.device, mixer: setup.mixer, @@ -626,11 +645,12 @@ impl Future for Main { let connect_config = self.connect_config.clone(); let audio_filter = mixer.get_audio_filter(); + let format = self.format; let backend = self.backend; let device = self.device.clone(); let (player, event_channel) = Player::new(player_config, session.clone(), audio_filter, move || { - (backend)(device) + (backend)(device, format) }); if self.emit_sink_events { From 6379926eb4c2dd786ccde075c6f1779aae3238ca Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 12 Mar 2021 23:18:18 +0100 Subject: [PATCH 04/23] Fix example --- examples/play.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/play.rs b/examples/play.rs index 4ba4c5b5..2c239ef3 100644 --- a/examples/play.rs +++ b/examples/play.rs @@ -5,7 +5,7 @@ use librespot::core::authentication::Credentials; use librespot::core::config::SessionConfig; use librespot::core::session::Session; use librespot::core::spotify_id::SpotifyId; -use librespot::playback::config::PlayerConfig; +use librespot::playback::config::{AudioFormat, PlayerConfig}; use librespot::playback::audio_backend; use librespot::playback::player::Player; @@ -16,6 +16,7 @@ fn main() { let session_config = SessionConfig::default(); let player_config = PlayerConfig::default(); + let audio_format = AudioFormat::default(); let args: Vec<_> = env::args().collect(); if args.len() != 4 { @@ -35,7 +36,7 @@ fn main() { .unwrap(); let (mut player, _) = Player::new(player_config, session.clone(), None, move || { - (backend)(None) + (backend)(None, audio_format) }); player.load(track, true, 0); From a4ef174fd00bac3d2be779653ccf35617aa0f379 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 12 Mar 2021 23:48:41 +0100 Subject: [PATCH 05/23] Fix Alsa backend for 64-bit systems --- playback/src/audio_backend/alsa.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 92b71f40..ce758dcb 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -11,7 +11,7 @@ use std::process::exit; use std::{io, mem}; const BUFFERED_LATENCY: f32 = 0.125; // seconds -const BUFFERED_PERIODS: u8 = 4; +const BUFFERED_PERIODS: Frames = 4; pub struct AlsaSink { pcm: Option, @@ -48,7 +48,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box // latency = period_size * periods / (rate * bytes_per_frame) // For stereo samples encoded as 32-bit float, one frame has a length of eight bytes. let mut period_size = ((SAMPLES_PER_SECOND * sample_size as u32) as f32 - * (BUFFERED_LATENCY / BUFFERED_PERIODS as f32)) as i32; + * (BUFFERED_LATENCY / BUFFERED_PERIODS as f32)) as Frames; // Set hardware parameters: 44100 Hz / stereo / 32-bit float or 16-bit signed integer { @@ -58,7 +58,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest)?; hwp.set_channels(NUM_CHANNELS as u32)?; period_size = hwp.set_period_size_near(period_size, ValueOr::Greater)?; - hwp.set_buffer_size_near(period_size * BUFFERED_PERIODS as i32)?; + hwp.set_buffer_size_near(period_size * BUFFERED_PERIODS)?; pcm.hw_params(&hwp)?; let swp = pcm.sw_params_current()?; @@ -101,8 +101,7 @@ impl Sink for AlsaSink { Ok((p, period_size)) => { self.pcm = Some(p); // Create a buffer for all samples for a full period - self.buffer = - Vec::with_capacity((period_size * BUFFERED_PERIODS as i32) as usize); + self.buffer = Vec::with_capacity((period_size * BUFFERED_PERIODS) as usize); } Err(e) => { error!("Alsa error PCM open {}", e); From 5f26a745d7dbe085cb52f4e433ae3438f54c5f95 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 13 Mar 2021 23:43:24 +0100 Subject: [PATCH 06/23] Add support for S32 output format While at it, add a small tweak when converting "silent" samples from float to integer. This ensures 0.0 converts to 0 and vice versa. --- audio/src/lib.rs | 25 ++++++++++++++++++++---- audio/src/libvorbis_decoder.rs | 8 +++++++- playback/src/audio_backend/alsa.rs | 6 ++++++ playback/src/audio_backend/mod.rs | 4 ++++ playback/src/audio_backend/portaudio.rs | 19 ++++++++++++++---- playback/src/audio_backend/pulseaudio.rs | 1 + playback/src/audio_backend/rodio.rs | 23 ++++++++++++++-------- playback/src/audio_backend/sdl.rs | 9 +++++++++ playback/src/config.rs | 2 ++ src/main.rs | 2 +- 10 files changed, 81 insertions(+), 18 deletions(-) diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 8a9f88f5..cafadae9 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -37,6 +37,22 @@ pub enum AudioPacket { OggData(Vec), } +// Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX] while maintaining DC linearity. +macro_rules! convert_samples_to { + ($type: ident, $samples: expr) => { + $samples + .iter() + .map(|sample| { + if *sample == 0.0 { + 0 as $type + } else { + (*sample as f64 * (std::$type::MAX as f64 + 0.5) - 0.5) as $type + } + }) + .collect() + }; +} + impl AudioPacket { pub fn samples(&self) -> &[f32] { match self { @@ -59,11 +75,12 @@ impl AudioPacket { } } + pub fn f32_to_s32(samples: &[f32]) -> Vec { + convert_samples_to!(i32, samples) + } + pub fn f32_to_s16(samples: &[f32]) -> Vec { - samples - .iter() - .map(|sample| (*sample as f64 * (0x7FFF as f64 + 0.5) - 0.5) as i16) - .collect() + convert_samples_to!(i16, samples) } } diff --git a/audio/src/libvorbis_decoder.rs b/audio/src/libvorbis_decoder.rs index e7ccc984..449caaeb 100644 --- a/audio/src/libvorbis_decoder.rs +++ b/audio/src/libvorbis_decoder.rs @@ -45,7 +45,13 @@ where packet .data .iter() - .map(|sample| ((*sample as f64 + 0.5) / (0x7FFF as f64 + 0.5)) as f32) + .map(|sample| { + if *sample == 0 { + 0.0 + } else { + ((*sample as f64 + 0.5) / (0x7FFF as f64 + 0.5)) as f32 + } + }) .collect(), ))); } diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index ce758dcb..4d9f19ed 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -41,6 +41,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box let pcm = PCM::new(dev_name, Direction::Playback, false)?; let (alsa_format, sample_size) = match format { AudioFormat::F32 => (Format::float(), mem::size_of::()), + AudioFormat::S32 => (Format::s32(), mem::size_of::()), AudioFormat::S16 => (Format::s16(), mem::size_of::()), }; @@ -157,6 +158,11 @@ impl AlsaSink { let io = pcm.io_f32().unwrap(); io.writei(&self.buffer) } + AudioFormat::S32 => { + let io = pcm.io_i32().unwrap(); + let buf_s32: Vec = AudioPacket::f32_to_s32(&self.buffer); + io.writei(&buf_s32[..]) + } AudioFormat::S16 => { let io = pcm.io_i16().unwrap(); let buf_s16: Vec = AudioPacket::f32_to_s16(&self.buffer); diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 550ebb84..bc10e88a 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -28,6 +28,10 @@ macro_rules! sink_as_bytes { match packet { AudioPacket::Samples(samples) => match self.format { AudioFormat::F32 => self.write_bytes(samples.as_bytes()), + AudioFormat::S32 => { + let samples_s32 = AudioPacket::f32_to_s32(samples); + self.write_bytes(samples_s32.as_bytes()) + } AudioFormat::S16 => { let samples_s16 = AudioPacket::f32_to_s16(samples); self.write_bytes(samples_s16.as_bytes()) diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 70caedd7..a7aa38cc 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -14,6 +14,10 @@ pub enum PortAudioSink<'a> { Option>, StreamParameters, ), + S32( + Option>, + StreamParameters, + ), S16( Option>, StreamParameters, @@ -70,19 +74,20 @@ impl<'a> Open for PortAudioSink<'a> { }; macro_rules! open_sink { - ($sink: expr, $data: expr) => {{ + ($sink: expr, $type: ty) => {{ let params = StreamParameters { device: device_idx, channel_count: NUM_CHANNELS as u32, suggested_latency: latency, - data: $data, + data: 0.0 as $type, }; $sink(None, params) }}; } match format { - AudioFormat::F32 => open_sink!(PortAudioSink::F32, 0.0), - AudioFormat::S16 => open_sink!(PortAudioSink::S16, 0), + AudioFormat::F32 => open_sink!(PortAudioSink::F32, f32), + AudioFormat::S32 => open_sink!(PortAudioSink::S32, i32), + AudioFormat::S16 => open_sink!(PortAudioSink::S16, i16), } } } @@ -109,6 +114,7 @@ impl<'a> Sink for PortAudioSink<'a> { } match self { PortAudioSink::F32(stream, parameters) => start_sink!(stream, parameters), + PortAudioSink::S32(stream, parameters) => start_sink!(stream, parameters), PortAudioSink::S16(stream, parameters) => start_sink!(stream, parameters), }; @@ -124,6 +130,7 @@ impl<'a> Sink for PortAudioSink<'a> { } match self { PortAudioSink::F32(stream, _parameters) => stop_sink!(stream), + PortAudioSink::S32(stream, _parameters) => stop_sink!(stream), PortAudioSink::S16(stream, _parameters) => stop_sink!(stream), }; @@ -141,6 +148,10 @@ impl<'a> Sink for PortAudioSink<'a> { let samples = packet.samples(); write_sink!(stream, &samples) } + PortAudioSink::S32(stream, _parameters) => { + let samples_s32: Vec = AudioPacket::f32_to_s32(packet.samples()); + write_sink!(stream, &samples_s32) + } PortAudioSink::S16(stream, _parameters) => { let samples_s16: Vec = AudioPacket::f32_to_s16(packet.samples()); write_sink!(stream, &samples_s16) diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 8c1e8e83..a2d89f21 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -22,6 +22,7 @@ impl Open for PulseAudioSink { let pulse_format = match format { AudioFormat::F32 => pulse::sample::Format::F32le, + AudioFormat::S32 => pulse::sample::Format::S32le, AudioFormat::S16 => pulse::sample::Format::S16le, }; diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 7571aa20..97e03ec0 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -7,6 +7,8 @@ use cpal::traits::{DeviceTrait, HostTrait}; use std::process::exit; use std::{io, thread, time}; +const FORMAT_NOT_SUPPORTED: &'static str = "Rodio currently does not support that output format"; + // most code is shared between RodioSink and JackRodioSink macro_rules! rodio_sink { ($name: ident) => { @@ -27,12 +29,13 @@ macro_rules! rodio_sink { AudioFormat::F32 => { let source = rodio::buffer::SamplesBuffer::new(2, 44100, samples); self.rodio_sink.append(source) - } + }, AudioFormat::S16 => { let samples_s16: Vec = AudioPacket::f32_to_s16(samples); let source = rodio::buffer::SamplesBuffer::new(2, 44100, samples_s16); self.rodio_sink.append(source) - } + }, + _ => panic!(FORMAT_NOT_SUPPORTED), }; // Chunk sizes seem to be about 256 to 3000 ish items long. @@ -48,12 +51,16 @@ macro_rules! rodio_sink { impl $name { fn open_sink(host: &cpal::Host, device: Option, format: AudioFormat) -> $name { - if format != AudioFormat::S16 { - #[cfg(target_os = "linux")] - { - warn!("Rodio output to Alsa is known to cause garbled sound on output formats other than 16-bit signed integer."); - warn!("Consider using `--backend alsa` OR `--format {:?}`", AudioFormat::S16); - } + match format { + AudioFormat::F32 => { + #[cfg(target_os = "linux")] + { + warn!("Rodio output to Alsa is known to cause garbled sound on output formats other than 16-bit signed integer."); + warn!("Consider using `--backend alsa` OR `--format {:?}`", AudioFormat::S16); + } + }, + AudioFormat::S16 => {}, + _ => panic!(FORMAT_NOT_SUPPORTED), } let rodio_device = match_device(&host, device); diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 6e52b322..ef8c1836 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -7,6 +7,7 @@ use std::{io, mem, thread, time}; pub enum SdlSink { F32(AudioQueue), + S32(AudioQueue), S16(AudioQueue), } @@ -39,6 +40,7 @@ impl Open for SdlSink { } match format { AudioFormat::F32 => open_sink!(SdlSink::F32, f32), + AudioFormat::S32 => open_sink!(SdlSink::S32, i32), AudioFormat::S16 => open_sink!(SdlSink::S16, i16), } } @@ -54,6 +56,7 @@ impl Sink for SdlSink { } match self { SdlSink::F32(queue) => start_sink!(queue), + SdlSink::S32(queue) => start_sink!(queue), SdlSink::S16(queue) => start_sink!(queue), }; Ok(()) @@ -68,6 +71,7 @@ impl Sink for SdlSink { } match self { SdlSink::F32(queue) => stop_sink!(queue), + SdlSink::S32(queue) => stop_sink!(queue), SdlSink::S16(queue) => stop_sink!(queue), }; Ok(()) @@ -87,6 +91,11 @@ impl Sink for SdlSink { drain_sink!(queue, mem::size_of::()); queue.queue(packet.samples()) } + SdlSink::S32(queue) => { + drain_sink!(queue, mem::size_of::()); + let samples_s32: Vec = AudioPacket::f32_to_s32(packet.samples()); + queue.queue(&samples_s32) + } SdlSink::S16(queue) => { drain_sink!(queue, mem::size_of::()); let samples_s16: Vec = AudioPacket::f32_to_s16(packet.samples()); diff --git a/playback/src/config.rs b/playback/src/config.rs index e1ed8dcf..80771582 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -29,6 +29,7 @@ impl Default for Bitrate { #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] pub enum AudioFormat { F32, + S32, S16, } @@ -37,6 +38,7 @@ impl TryFrom<&String> for AudioFormat { fn try_from(s: &String) -> Result { match s.to_uppercase().as_str() { "F32" => Ok(AudioFormat::F32), + "S32" => Ok(AudioFormat::S32), "S16" => Ok(AudioFormat::S16), _ => unimplemented!(), } diff --git a/src/main.rs b/src/main.rs index a7cd8b30..b4cfc437 100644 --- a/src/main.rs +++ b/src/main.rs @@ -156,7 +156,7 @@ fn setup(args: &[String]) -> Setup { .optopt( "", "format", - "Output format (F32 or S16). Defaults to F32", + "Output format (F32, S32 or S16). Defaults to F32", "FORMAT", ) .optopt("", "mixer", "Mixer to use (alsa or softvol)", "MIXER") From 309e26456ef2d381cf0a1338ec45fac2f3b25665 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 14 Mar 2021 14:28:16 +0100 Subject: [PATCH 07/23] Rename steepness to knee --- playback/src/config.rs | 4 ++-- playback/src/player.rs | 9 +++------ src/main.rs | 18 +++++++----------- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/playback/src/config.rs b/playback/src/config.rs index 80771582..312f1709 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -107,7 +107,7 @@ pub struct PlayerConfig { pub normalisation_threshold: f32, pub normalisation_attack: f32, pub normalisation_release: f32, - pub normalisation_steepness: f32, + pub normalisation_knee: f32, pub gapless: bool, pub passthrough: bool, } @@ -123,7 +123,7 @@ impl Default for PlayerConfig { normalisation_threshold: -1.0, normalisation_attack: 0.005, normalisation_release: 0.1, - normalisation_steepness: 1.0, + normalisation_knee: 1.0, gapless: true, passthrough: false, } diff --git a/playback/src/player.rs b/playback/src/player.rs index dbc09695..0a573c93 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -271,10 +271,7 @@ impl NormalisationData { if config.normalisation_method == NormalisationMethod::Dynamic { debug!("Normalisation Attack: {:?}", config.normalisation_attack); debug!("Normalisation Release: {:?}", config.normalisation_release); - debug!( - "Normalisation Steepness: {:?}", - config.normalisation_steepness - ); + debug!("Normalisation Knee: {:?}", config.normalisation_knee); } normalisation_factor @@ -1176,7 +1173,7 @@ impl PlayerInternal { if self.config.normalisation_method == NormalisationMethod::Dynamic { if self.limiter_active { - // "S"-shaped curve with a configurable steepness during attack and release: + // "S"-shaped curve with a configurable knee during attack and release: // - > 1.0 yields soft knees at start and end, steeper in between // - 1.0 yields a linear function from 0-100% // - between 0.0 and 1.0 yields hard knees at start and end, flatter in between @@ -1191,7 +1188,7 @@ impl PlayerInternal { + f32::powf( shaped_limiter_strength / (1.0 - shaped_limiter_strength), - -1.0 * self.config.normalisation_steepness, + -1.0 * self.config.normalisation_knee, )); } actual_normalisation_factor = diff --git a/src/main.rs b/src/main.rs index b4cfc437..bf553a86 100644 --- a/src/main.rs +++ b/src/main.rs @@ -238,9 +238,9 @@ fn setup(args: &[String]) -> Setup { ) .optopt( "", - "normalisation-steepness", - "Steepness of the dynamic limiting curve. Default is 1.0.", - "STEEPNESS", + "normalisation-knee", + "Knee steepness of the dynamic limiter. Default is 1.0.", + "KNEE", ) .optopt( "", @@ -475,14 +475,10 @@ fn setup(args: &[String]) -> Setup { .map(|release| release.parse::().expect("Invalid release float value")) .unwrap_or(PlayerConfig::default().normalisation_release * MILLIS) / MILLIS, - normalisation_steepness: matches - .opt_str("normalisation-steepness") - .map(|steepness| { - steepness - .parse::() - .expect("Invalid steepness float value") - }) - .unwrap_or(PlayerConfig::default().normalisation_steepness), + normalisation_knee: matches + .opt_str("normalisation-knee") + .map(|knee| knee.parse::().expect("Invalid knee float value")) + .unwrap_or(PlayerConfig::default().normalisation_knee), passthrough, } }; From 9dcaeee6d445c0942eac6dd8abc74a2f53486c17 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 16 Mar 2021 20:22:00 +0100 Subject: [PATCH 08/23] Default to S16 output --- playback/src/audio_backend/jackaudio.rs | 8 ++------ playback/src/config.rs | 2 +- src/main.rs | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 295941a4..2412d07c 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -42,14 +42,10 @@ impl ProcessHandler for JackData { impl Open for JackSink { fn open(client_name: Option, format: AudioFormat) -> JackSink { - info!("Using JACK sink with format {:?}", format); - if format != AudioFormat::F32 { - panic!( - "JACK sink only supports 32-bit floating point output. Use `--format {:?}`", - AudioFormat::F32 - ); + warn!("JACK currently does not support {:?} output", format); } + info!("Using JACK sink with format {:?}", AudioFormat::F32); let client_name = client_name.unwrap_or("librespot".to_string()); let (client, _status) = diff --git a/playback/src/config.rs b/playback/src/config.rs index 312f1709..7348b7bf 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -47,7 +47,7 @@ impl TryFrom<&String> for AudioFormat { impl Default for AudioFormat { fn default() -> AudioFormat { - AudioFormat::F32 + AudioFormat::S16 } } diff --git a/src/main.rs b/src/main.rs index bf553a86..07b85b30 100644 --- a/src/main.rs +++ b/src/main.rs @@ -156,7 +156,7 @@ fn setup(args: &[String]) -> Setup { .optopt( "", "format", - "Output format (F32, S32 or S16). Defaults to F32", + "Output format (F32, S32 or S16). Defaults to S16", "FORMAT", ) .optopt("", "mixer", "Mixer to use (alsa or softvol)", "MIXER") From 770ea15498a0f1cfc7b9986f0954f4150258c29f Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 17 Mar 2021 00:00:27 +0100 Subject: [PATCH 09/23] Add support for S24 and S24_3 output formats --- Cargo.lock | 1 + audio/Cargo.toml | 1 + audio/src/lib.rs | 33 ++++++++++++--- playback/src/audio_backend/alsa.rs | 52 ++++++++++-------------- playback/src/audio_backend/gstreamer.rs | 19 ++++++--- playback/src/audio_backend/jackaudio.rs | 2 +- playback/src/audio_backend/mod.rs | 8 ++++ playback/src/audio_backend/pipe.rs | 2 +- playback/src/audio_backend/portaudio.rs | 38 +++++++++++------ playback/src/audio_backend/pulseaudio.rs | 5 ++- playback/src/audio_backend/rodio.rs | 8 ++-- playback/src/audio_backend/sdl.rs | 36 ++++++++++------ playback/src/config.rs | 28 ++++++++++--- src/main.rs | 2 +- 14 files changed, 155 insertions(+), 80 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9a2e42ea..2296cfed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1473,6 +1473,7 @@ dependencies = [ "ogg", "tempfile", "vorbis", + "zerocopy", ] [[package]] diff --git a/audio/Cargo.toml b/audio/Cargo.toml index b7e6e35f..06f1dda6 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -22,6 +22,7 @@ log = "0.4" num-bigint = "0.3" num-traits = "0.2" tempfile = "3.1" +zerocopy = "0.3" librespot-tremor = { version = "0.2.0", optional = true } vorbis = { version ="0.0.14", optional = true } diff --git a/audio/src/lib.rs b/audio/src/lib.rs index cafadae9..86c5b4ae 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -31,23 +31,35 @@ pub use fetch::{ READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS, }; use std::fmt; +use zerocopy::AsBytes; pub enum AudioPacket { Samples(Vec), OggData(Vec), } +#[derive(AsBytes, Copy, Clone, Debug)] +#[allow(non_camel_case_types)] +#[repr(transparent)] +pub struct i24([u8; 3]); +impl i24 { + fn pcm_from_i32(sample: i32) -> Self { + // drop the least significant byte + let [a, b, c, _d] = (sample >> 8).to_le_bytes(); + i24([a, b, c]) + } +} + // Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX] while maintaining DC linearity. macro_rules! convert_samples_to { ($type: ident, $samples: expr) => { + convert_samples_to!($type, $samples, 0) + }; + ($type: ident, $samples: expr, $shift: expr) => { $samples .iter() .map(|sample| { - if *sample == 0.0 { - 0 as $type - } else { - (*sample as f64 * (std::$type::MAX as f64 + 0.5) - 0.5) as $type - } + (*sample as f64 * (std::$type::MAX as f64 + 0.5) - 0.5) as $type >> $shift }) .collect() }; @@ -79,6 +91,17 @@ impl AudioPacket { convert_samples_to!(i32, samples) } + pub fn f32_to_s24(samples: &[f32]) -> Vec { + convert_samples_to!(i32, samples, 8) + } + + pub fn f32_to_s24_3(samples: &[f32]) -> Vec { + Self::f32_to_s32(samples) + .iter() + .map(|sample| i24::pcm_from_i32(*sample)) + .collect() + } + pub fn f32_to_s16(samples: &[f32]) -> Vec { convert_samples_to!(i16, samples) } diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 4d9f19ed..35d0ab11 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -1,4 +1,4 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkAsBytes}; use crate::audio::AudioPacket; use crate::config::AudioFormat; use crate::player::{NUM_CHANNELS, SAMPLES_PER_SECOND, SAMPLE_RATE}; @@ -7,8 +7,8 @@ use alsa::pcm::{Access, Format, Frames, HwParams, PCM}; use alsa::{Direction, Error, ValueOr}; use std::cmp::min; use std::ffi::CString; +use std::io; use std::process::exit; -use std::{io, mem}; const BUFFERED_LATENCY: f32 = 0.125; // seconds const BUFFERED_PERIODS: Frames = 4; @@ -17,7 +17,7 @@ pub struct AlsaSink { pcm: Option, format: AudioFormat, device: String, - buffer: Vec, + buffer: Vec, } fn list_outputs() { @@ -39,16 +39,18 @@ fn list_outputs() { fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box> { let pcm = PCM::new(dev_name, Direction::Playback, false)?; - let (alsa_format, sample_size) = match format { - AudioFormat::F32 => (Format::float(), mem::size_of::()), - AudioFormat::S32 => (Format::s32(), mem::size_of::()), - AudioFormat::S16 => (Format::s16(), mem::size_of::()), + let alsa_format = match format { + AudioFormat::F32 => Format::float(), + AudioFormat::S32 => Format::s32(), + AudioFormat::S24 => Format::s24(), + AudioFormat::S24_3 => Format::S243LE, + AudioFormat::S16 => Format::s16(), }; // http://www.linuxjournal.com/article/6735?page=0,1#N0x19ab2890.0x19ba78d8 // latency = period_size * periods / (rate * bytes_per_frame) // For stereo samples encoded as 32-bit float, one frame has a length of eight bytes. - let mut period_size = ((SAMPLES_PER_SECOND * sample_size as u32) as f32 + let mut period_size = ((SAMPLES_PER_SECOND * format.size() as u32) as f32 * (BUFFERED_LATENCY / BUFFERED_PERIODS as f32)) as Frames; // Set hardware parameters: 44100 Hz / stereo / 32-bit float or 16-bit signed integer @@ -85,7 +87,7 @@ impl Open for AlsaSink { } .to_string(); - AlsaSink { + Self { pcm: None, format: format, device: name, @@ -102,7 +104,9 @@ impl Sink for AlsaSink { Ok((p, period_size)) => { self.pcm = Some(p); // Create a buffer for all samples for a full period - self.buffer = Vec::with_capacity((period_size * BUFFERED_PERIODS) as usize); + self.buffer = Vec::with_capacity( + period_size as usize * BUFFERED_PERIODS as usize * self.format.size(), + ); } Err(e) => { error!("Alsa error PCM open {}", e); @@ -121,7 +125,7 @@ impl Sink for AlsaSink { { // Write any leftover data in the period buffer // before draining the actual buffer - self.write_buf().expect("could not flush buffer"); + self.write_bytes(&[]).expect("could not flush buffer"); let pcm = self.pcm.as_mut().unwrap(); pcm.drain().unwrap(); } @@ -129,9 +133,12 @@ impl Sink for AlsaSink { Ok(()) } - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + sink_as_bytes!(); +} + +impl SinkAsBytes for AlsaSink { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { let mut processed_data = 0; - let data = packet.samples(); while processed_data < data.len() { let data_to_buffer = min( self.buffer.capacity() - self.buffer.len(), @@ -153,23 +160,8 @@ impl Sink for AlsaSink { impl AlsaSink { fn write_buf(&mut self) -> io::Result<()> { let pcm = self.pcm.as_mut().unwrap(); - let io_result = match self.format { - AudioFormat::F32 => { - let io = pcm.io_f32().unwrap(); - io.writei(&self.buffer) - } - AudioFormat::S32 => { - let io = pcm.io_i32().unwrap(); - let buf_s32: Vec = AudioPacket::f32_to_s32(&self.buffer); - io.writei(&buf_s32[..]) - } - AudioFormat::S16 => { - let io = pcm.io_i16().unwrap(); - let buf_s16: Vec = AudioPacket::f32_to_s16(&self.buffer); - io.writei(&buf_s16[..]) - } - }; - match io_result { + let io = pcm.io_bytes(); + match io.writei(&self.buffer) { Ok(_) => (), Err(err) => pcm.try_recover(err, false).unwrap(), }; diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 17ad86e6..3695857e 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -18,11 +18,18 @@ pub struct GstreamerSink { impl Open for GstreamerSink { fn open(device: Option, format: AudioFormat) -> GstreamerSink { info!("Using GStreamer sink with format: {:?}", format); - gst::init().expect("failed to init GStreamer!"); + + // GStreamer calls S24 and S24_3 different from the rest of the world + let gst_format = match format { + AudioFormat::S24 => "S24_32".to_string(), + AudioFormat::S24_3 => "S24".to_string(), + _ => format!("{:?}", format), + }; + let pipeline_str_preamble = format!( - r#"appsrc caps="audio/x-raw,format={:?},layout=interleaved,channels={},rate={}" block=true max-bytes=4096 name=appsrc0 "#, - format, NUM_CHANNELS, SAMPLE_RATE + "appsrc caps=\"audio/x-raw,format={}LE,layout=interleaved,channels={},rate={}\" block=true max-bytes=4096 name=appsrc0 ", + gst_format, NUM_CHANNELS, SAMPLE_RATE ); let pipeline_str_rest = r#" ! audioconvert ! autoaudiosink"#; let pipeline_str: String = match device { @@ -47,7 +54,7 @@ impl Open for GstreamerSink { let bufferpool = gst::BufferPool::new(); let appsrc_caps = appsrc.get_caps().expect("couldn't get appsrc caps"); let mut conf = bufferpool.get_config(); - conf.set_params(Some(&appsrc_caps), 8192, 0, 0); + conf.set_params(Some(&appsrc_caps), 2048 * format.size() as u32, 0, 0); bufferpool .set_config(conf) .expect("couldn't configure the buffer pool"); @@ -55,7 +62,7 @@ impl Open for GstreamerSink { .set_active(true) .expect("couldn't activate buffer pool"); - let (tx, rx) = sync_channel::>(128); + let (tx, rx) = sync_channel::>(64 * format.size()); thread::spawn(move || { for data in rx { let buffer = bufferpool.acquire_buffer(None); @@ -99,7 +106,7 @@ impl Open for GstreamerSink { .set_state(gst::State::Playing) .expect("unable to set the pipeline to the `Playing` state"); - GstreamerSink { + Self { tx: tx, pipeline: pipeline, format: format, diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 2412d07c..05c6c317 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -61,7 +61,7 @@ impl Open for JackSink { }; let active_client = AsyncClient::new(client, (), jack_data).unwrap(); - JackSink { + Self { send: tx, active_client: active_client, } diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index bc10e88a..9c46dbe4 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -32,6 +32,14 @@ macro_rules! sink_as_bytes { let samples_s32 = AudioPacket::f32_to_s32(samples); self.write_bytes(samples_s32.as_bytes()) } + AudioFormat::S24 => { + let samples_s24 = AudioPacket::f32_to_s24(samples); + self.write_bytes(samples_s24.as_bytes()) + } + AudioFormat::S24_3 => { + let samples_s24_3 = AudioPacket::f32_to_s24_3(samples); + self.write_bytes(samples_s24_3.as_bytes()) + } AudioFormat::S16 => { let samples_s16 = AudioPacket::f32_to_s16(samples); self.write_bytes(samples_s16.as_bytes()) diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index 3a90d06f..ae77e320 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -18,7 +18,7 @@ impl Open for StdoutSink { _ => Box::new(io::stdout()), }; - StdoutSink { + Self { output: output, format: format, } diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index a7aa38cc..213f2d02 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -18,6 +18,10 @@ pub enum PortAudioSink<'a> { Option>, StreamParameters, ), + S24( + Option>, + StreamParameters, + ), S16( Option>, StreamParameters, @@ -85,9 +89,13 @@ impl<'a> Open for PortAudioSink<'a> { }}; } match format { - AudioFormat::F32 => open_sink!(PortAudioSink::F32, f32), - AudioFormat::S32 => open_sink!(PortAudioSink::S32, i32), - AudioFormat::S16 => open_sink!(PortAudioSink::S16, i16), + AudioFormat::F32 => open_sink!(Self::F32, f32), + AudioFormat::S32 => open_sink!(Self::S32, i32), + AudioFormat::S24 => open_sink!(Self::S24, i32), + AudioFormat::S24_3 => { + unimplemented!("PortAudio currently does not support S24_3 output") + } + AudioFormat::S16 => open_sink!(Self::S16, i16), } } } @@ -113,9 +121,10 @@ impl<'a> Sink for PortAudioSink<'a> { }}; } match self { - PortAudioSink::F32(stream, parameters) => start_sink!(stream, parameters), - PortAudioSink::S32(stream, parameters) => start_sink!(stream, parameters), - PortAudioSink::S16(stream, parameters) => start_sink!(stream, parameters), + Self::F32(stream, parameters) => start_sink!(stream, parameters), + Self::S32(stream, parameters) => start_sink!(stream, parameters), + Self::S24(stream, parameters) => start_sink!(stream, parameters), + Self::S16(stream, parameters) => start_sink!(stream, parameters), }; Ok(()) @@ -129,9 +138,10 @@ impl<'a> Sink for PortAudioSink<'a> { }}; } match self { - PortAudioSink::F32(stream, _parameters) => stop_sink!(stream), - PortAudioSink::S32(stream, _parameters) => stop_sink!(stream), - PortAudioSink::S16(stream, _parameters) => stop_sink!(stream), + Self::F32(stream, _parameters) => stop_sink!(stream), + Self::S32(stream, _parameters) => stop_sink!(stream), + Self::S24(stream, _parameters) => stop_sink!(stream), + Self::S16(stream, _parameters) => stop_sink!(stream), }; Ok(()) @@ -144,15 +154,19 @@ impl<'a> Sink for PortAudioSink<'a> { }; } let result = match self { - PortAudioSink::F32(stream, _parameters) => { + Self::F32(stream, _parameters) => { let samples = packet.samples(); write_sink!(stream, &samples) } - PortAudioSink::S32(stream, _parameters) => { + Self::S32(stream, _parameters) => { let samples_s32: Vec = AudioPacket::f32_to_s32(packet.samples()); write_sink!(stream, &samples_s32) } - PortAudioSink::S16(stream, _parameters) => { + Self::S24(stream, _parameters) => { + let samples_s24: Vec = AudioPacket::f32_to_s24(packet.samples()); + write_sink!(stream, &samples_s24) + } + Self::S16(stream, _parameters) => { let samples_s16: Vec = AudioPacket::f32_to_s16(packet.samples()); write_sink!(stream, &samples_s16) } diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index a2d89f21..16800eb0 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -20,9 +20,12 @@ impl Open for PulseAudioSink { fn open(device: Option, format: AudioFormat) -> PulseAudioSink { info!("Using PulseAudio sink with format: {:?}", format); + // PulseAudio calls S24 and S24_3 different from the rest of the world let pulse_format = match format { AudioFormat::F32 => pulse::sample::Format::F32le, AudioFormat::S32 => pulse::sample::Format::S32le, + AudioFormat::S24 => pulse::sample::Format::S24_32le, + AudioFormat::S24_3 => pulse::sample::Format::S24le, AudioFormat::S16 => pulse::sample::Format::S16le, }; @@ -33,7 +36,7 @@ impl Open for PulseAudioSink { }; debug_assert!(ss.is_valid()); - PulseAudioSink { + Self { s: None, ss: ss, device: device, diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 97e03ec0..5262a9cc 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -7,8 +7,6 @@ use cpal::traits::{DeviceTrait, HostTrait}; use std::process::exit; use std::{io, thread, time}; -const FORMAT_NOT_SUPPORTED: &'static str = "Rodio currently does not support that output format"; - // most code is shared between RodioSink and JackRodioSink macro_rules! rodio_sink { ($name: ident) => { @@ -35,7 +33,7 @@ macro_rules! rodio_sink { let source = rodio::buffer::SamplesBuffer::new(2, 44100, samples_s16); self.rodio_sink.append(source) }, - _ => panic!(FORMAT_NOT_SUPPORTED), + _ => unimplemented!(), }; // Chunk sizes seem to be about 256 to 3000 ish items long. @@ -60,7 +58,7 @@ macro_rules! rodio_sink { } }, AudioFormat::S16 => {}, - _ => panic!(FORMAT_NOT_SUPPORTED), + _ => unimplemented!("Rodio currently only supports F32 and S16 formats"), } let rodio_device = match_device(&host, device); @@ -71,7 +69,7 @@ macro_rules! rodio_sink { let sink = rodio::Sink::try_new(&stream.1).expect("couldn't create output sink."); debug!("Using Rodio sink"); - $name { + Self { rodio_sink: sink, stream: stream.0, format: format, diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index ef8c1836..32d710f8 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -8,6 +8,7 @@ use std::{io, mem, thread, time}; pub enum SdlSink { F32(AudioQueue), S32(AudioQueue), + S24(AudioQueue), S16(AudioQueue), } @@ -16,7 +17,7 @@ impl Open for SdlSink { info!("Using SDL sink with format: {:?}", format); if device.is_some() { - panic!("SDL sink does not support specifying a device name"); + warn!("SDL sink does not support specifying a device name"); } let ctx = sdl2::init().expect("could not initialize SDL"); @@ -39,9 +40,11 @@ impl Open for SdlSink { }}; } match format { - AudioFormat::F32 => open_sink!(SdlSink::F32, f32), - AudioFormat::S32 => open_sink!(SdlSink::S32, i32), - AudioFormat::S16 => open_sink!(SdlSink::S16, i16), + AudioFormat::F32 => open_sink!(Self::F32, f32), + AudioFormat::S32 => open_sink!(Self::S32, i32), + AudioFormat::S24 => open_sink!(Self::S24, i32), + AudioFormat::S24_3 => unimplemented!("SDL currently does not support S24_3 output"), + AudioFormat::S16 => open_sink!(Self::S16, i16), } } } @@ -55,9 +58,10 @@ impl Sink for SdlSink { }}; } match self { - SdlSink::F32(queue) => start_sink!(queue), - SdlSink::S32(queue) => start_sink!(queue), - SdlSink::S16(queue) => start_sink!(queue), + Self::F32(queue) => start_sink!(queue), + Self::S32(queue) => start_sink!(queue), + Self::S24(queue) => start_sink!(queue), + Self::S16(queue) => start_sink!(queue), }; Ok(()) } @@ -70,9 +74,10 @@ impl Sink for SdlSink { }}; } match self { - SdlSink::F32(queue) => stop_sink!(queue), - SdlSink::S32(queue) => stop_sink!(queue), - SdlSink::S16(queue) => stop_sink!(queue), + Self::F32(queue) => stop_sink!(queue), + Self::S32(queue) => stop_sink!(queue), + Self::S24(queue) => stop_sink!(queue), + Self::S16(queue) => stop_sink!(queue), }; Ok(()) } @@ -87,16 +92,21 @@ impl Sink for SdlSink { }}; } match self { - SdlSink::F32(queue) => { + Self::F32(queue) => { drain_sink!(queue, mem::size_of::()); queue.queue(packet.samples()) } - SdlSink::S32(queue) => { + Self::S32(queue) => { drain_sink!(queue, mem::size_of::()); let samples_s32: Vec = AudioPacket::f32_to_s32(packet.samples()); queue.queue(&samples_s32) } - SdlSink::S16(queue) => { + Self::S24(queue) => { + drain_sink!(queue, mem::size_of::()); + let samples_s24: Vec = AudioPacket::f32_to_s24(packet.samples()); + queue.queue(&samples_s24) + } + Self::S16(queue) => { drain_sink!(queue, mem::size_of::()); let samples_s16: Vec = AudioPacket::f32_to_s16(packet.samples()); queue.queue(&samples_s16) diff --git a/playback/src/config.rs b/playback/src/config.rs index 7348b7bf..630c1406 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -1,4 +1,6 @@ +use crate::audio::i24; use std::convert::TryFrom; +use std::mem; use std::str::FromStr; #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] @@ -30,6 +32,8 @@ impl Default for Bitrate { pub enum AudioFormat { F32, S32, + S24, + S24_3, S16, } @@ -37,17 +41,31 @@ impl TryFrom<&String> for AudioFormat { type Error = (); fn try_from(s: &String) -> Result { match s.to_uppercase().as_str() { - "F32" => Ok(AudioFormat::F32), - "S32" => Ok(AudioFormat::S32), - "S16" => Ok(AudioFormat::S16), - _ => unimplemented!(), + "F32" => Ok(Self::F32), + "S32" => Ok(Self::S32), + "S24" => Ok(Self::S24), + "S24_3" => Ok(Self::S24_3), + "S16" => Ok(Self::S16), + _ => Err(()), } } } impl Default for AudioFormat { fn default() -> AudioFormat { - AudioFormat::S16 + Self::S16 + } +} + +impl AudioFormat { + // not used by all backends + #[allow(dead_code)] + pub fn size(&self) -> usize { + match self { + Self::S24_3 => mem::size_of::(), + Self::S16 => mem::size_of::(), + _ => mem::size_of::(), + } } } diff --git a/src/main.rs b/src/main.rs index 07b85b30..7426e2c4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -156,7 +156,7 @@ fn setup(args: &[String]) -> Setup { .optopt( "", "format", - "Output format (F32, S32 or S16). Defaults to S16", + "Output format (F32, S32, S24, S24_3 or S16). Defaults to S16", "FORMAT", ) .optopt("", "mixer", "Mixer to use (alsa or softvol)", "MIXER") From b94879de62f3fbf11a38aa2fa5df11b24e9998b8 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 18 Mar 2021 20:51:53 +0100 Subject: [PATCH 10/23] Fix GStreamer buffer pool size [ref #660 review] --- playback/src/audio_backend/gstreamer.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 3695857e..d3c736a4 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -26,10 +26,12 @@ impl Open for GstreamerSink { AudioFormat::S24_3 => "S24".to_string(), _ => format!("{:?}", format), }; + let sample_size = format.size(); + let gst_bytes = 2048 * sample_size; let pipeline_str_preamble = format!( - "appsrc caps=\"audio/x-raw,format={}LE,layout=interleaved,channels={},rate={}\" block=true max-bytes=4096 name=appsrc0 ", - gst_format, NUM_CHANNELS, SAMPLE_RATE + "appsrc caps=\"audio/x-raw,format={}LE,layout=interleaved,channels={},rate={}\" block=true max-bytes={} name=appsrc0 ", + gst_format, NUM_CHANNELS, SAMPLE_RATE, gst_bytes ); let pipeline_str_rest = r#" ! audioconvert ! autoaudiosink"#; let pipeline_str: String = match device { @@ -54,7 +56,7 @@ impl Open for GstreamerSink { let bufferpool = gst::BufferPool::new(); let appsrc_caps = appsrc.get_caps().expect("couldn't get appsrc caps"); let mut conf = bufferpool.get_config(); - conf.set_params(Some(&appsrc_caps), 2048 * format.size() as u32, 0, 0); + conf.set_params(Some(&appsrc_caps), 4096 * sample_size as u32, 0, 0); bufferpool .set_config(conf) .expect("couldn't configure the buffer pool"); @@ -62,7 +64,7 @@ impl Open for GstreamerSink { .set_active(true) .expect("couldn't activate buffer pool"); - let (tx, rx) = sync_channel::>(64 * format.size()); + let (tx, rx) = sync_channel::>(64 * sample_size); thread::spawn(move || { for data in rx { let buffer = bufferpool.acquire_buffer(None); From a1326ba9f45c5d6f61950f95a87a5642421ad2a9 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 18 Mar 2021 22:06:43 +0100 Subject: [PATCH 11/23] First round of refactoring - DRY-ups - Remove incorrect optimization attempt in the libvorbis decoder, that skewed 0.0 samples non-linear - PortAudio and SDL backends do not support S24 output. The PortAudio bindings could, but not through this API. --- audio/src/libvorbis_decoder.rs | 8 +------ playback/src/audio_backend/alsa.rs | 2 -- playback/src/audio_backend/jackaudio.rs | 6 ++--- playback/src/audio_backend/portaudio.rs | 17 +++----------- playback/src/audio_backend/sdl.rs | 13 +++-------- playback/src/config.rs | 30 ++++++++++++------------- 6 files changed, 25 insertions(+), 51 deletions(-) diff --git a/audio/src/libvorbis_decoder.rs b/audio/src/libvorbis_decoder.rs index 449caaeb..e7ccc984 100644 --- a/audio/src/libvorbis_decoder.rs +++ b/audio/src/libvorbis_decoder.rs @@ -45,13 +45,7 @@ where packet .data .iter() - .map(|sample| { - if *sample == 0 { - 0.0 - } else { - ((*sample as f64 + 0.5) / (0x7FFF as f64 + 0.5)) as f32 - } - }) + .map(|sample| ((*sample as f64 + 0.5) / (0x7FFF as f64 + 0.5)) as f32) .collect(), ))); } diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 35d0ab11..fc2a775c 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -52,8 +52,6 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box // For stereo samples encoded as 32-bit float, one frame has a length of eight bytes. let mut period_size = ((SAMPLES_PER_SECOND * format.size() as u32) as f32 * (BUFFERED_LATENCY / BUFFERED_PERIODS as f32)) as Frames; - - // Set hardware parameters: 44100 Hz / stereo / 32-bit float or 16-bit signed integer { let hwp = HwParams::any(&pcm)?; hwp.set_access(Access::RWInterleaved)?; diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 05c6c317..ed6ae1f7 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -5,8 +5,8 @@ use crate::player::NUM_CHANNELS; use jack::{ AsyncClient, AudioOut, Client, ClientOptions, Control, Port, ProcessHandler, ProcessScope, }; +use std::io; use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; -use std::{io, mem}; pub struct JackSink { send: SyncSender, @@ -53,7 +53,7 @@ impl Open for JackSink { let ch_r = client.register_port("out_0", AudioOut::default()).unwrap(); let ch_l = client.register_port("out_1", AudioOut::default()).unwrap(); // buffer for samples from librespot (~10ms) - let (tx, rx) = sync_channel::(NUM_CHANNELS as usize * 1024 * mem::size_of::()); + let (tx, rx) = sync_channel::(NUM_CHANNELS as usize * 1024 * format.size()); let jack_data = JackData { rec: rx, port_l: ch_l, @@ -75,7 +75,7 @@ impl Sink for JackSink { for s in packet.samples().iter() { let res = self.send.send(*s); if res.is_err() { - error!("jackaudio: cannot write to channel"); + error!("cannot write to channel"); } } Ok(()) diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 213f2d02..fca305e0 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -18,10 +18,6 @@ pub enum PortAudioSink<'a> { Option>, StreamParameters, ), - S24( - Option>, - StreamParameters, - ), S16( Option>, StreamParameters, @@ -91,11 +87,10 @@ impl<'a> Open for PortAudioSink<'a> { match format { AudioFormat::F32 => open_sink!(Self::F32, f32), AudioFormat::S32 => open_sink!(Self::S32, i32), - AudioFormat::S24 => open_sink!(Self::S24, i32), - AudioFormat::S24_3 => { - unimplemented!("PortAudio currently does not support S24_3 output") - } AudioFormat::S16 => open_sink!(Self::S16, i16), + _ => { + unimplemented!("PortAudio currently does not support {:?} output", format) + } } } } @@ -123,7 +118,6 @@ impl<'a> Sink for PortAudioSink<'a> { match self { Self::F32(stream, parameters) => start_sink!(stream, parameters), Self::S32(stream, parameters) => start_sink!(stream, parameters), - Self::S24(stream, parameters) => start_sink!(stream, parameters), Self::S16(stream, parameters) => start_sink!(stream, parameters), }; @@ -140,7 +134,6 @@ impl<'a> Sink for PortAudioSink<'a> { match self { Self::F32(stream, _parameters) => stop_sink!(stream), Self::S32(stream, _parameters) => stop_sink!(stream), - Self::S24(stream, _parameters) => stop_sink!(stream), Self::S16(stream, _parameters) => stop_sink!(stream), }; @@ -162,10 +155,6 @@ impl<'a> Sink for PortAudioSink<'a> { let samples_s32: Vec = AudioPacket::f32_to_s32(packet.samples()); write_sink!(stream, &samples_s32) } - Self::S24(stream, _parameters) => { - let samples_s24: Vec = AudioPacket::f32_to_s24(packet.samples()); - write_sink!(stream, &samples_s24) - } Self::S16(stream, _parameters) => { let samples_s16: Vec = AudioPacket::f32_to_s16(packet.samples()); write_sink!(stream, &samples_s16) diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 32d710f8..64523732 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -8,7 +8,6 @@ use std::{io, mem, thread, time}; pub enum SdlSink { F32(AudioQueue), S32(AudioQueue), - S24(AudioQueue), S16(AudioQueue), } @@ -42,9 +41,10 @@ impl Open for SdlSink { match format { AudioFormat::F32 => open_sink!(Self::F32, f32), AudioFormat::S32 => open_sink!(Self::S32, i32), - AudioFormat::S24 => open_sink!(Self::S24, i32), - AudioFormat::S24_3 => unimplemented!("SDL currently does not support S24_3 output"), AudioFormat::S16 => open_sink!(Self::S16, i16), + _ => { + unimplemented!("SDL currently does not support {:?} output", format) + } } } } @@ -60,7 +60,6 @@ impl Sink for SdlSink { match self { Self::F32(queue) => start_sink!(queue), Self::S32(queue) => start_sink!(queue), - Self::S24(queue) => start_sink!(queue), Self::S16(queue) => start_sink!(queue), }; Ok(()) @@ -76,7 +75,6 @@ impl Sink for SdlSink { match self { Self::F32(queue) => stop_sink!(queue), Self::S32(queue) => stop_sink!(queue), - Self::S24(queue) => stop_sink!(queue), Self::S16(queue) => stop_sink!(queue), }; Ok(()) @@ -101,11 +99,6 @@ impl Sink for SdlSink { let samples_s32: Vec = AudioPacket::f32_to_s32(packet.samples()); queue.queue(&samples_s32) } - Self::S24(queue) => { - drain_sink!(queue, mem::size_of::()); - let samples_s24: Vec = AudioPacket::f32_to_s24(packet.samples()); - queue.queue(&samples_s24) - } Self::S16(queue) => { drain_sink!(queue, mem::size_of::()); let samples_s16: Vec = AudioPacket::f32_to_s16(packet.samples()); diff --git a/playback/src/config.rs b/playback/src/config.rs index 630c1406..95c97092 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -14,17 +14,17 @@ impl FromStr for Bitrate { type Err = (); fn from_str(s: &str) -> Result { match s { - "96" => Ok(Bitrate::Bitrate96), - "160" => Ok(Bitrate::Bitrate160), - "320" => Ok(Bitrate::Bitrate320), + "96" => Ok(Self::Bitrate96), + "160" => Ok(Self::Bitrate160), + "320" => Ok(Self::Bitrate320), _ => Err(()), } } } impl Default for Bitrate { - fn default() -> Bitrate { - Bitrate::Bitrate160 + fn default() -> Self { + Self::Bitrate160 } } @@ -52,7 +52,7 @@ impl TryFrom<&String> for AudioFormat { } impl Default for AudioFormat { - fn default() -> AudioFormat { + fn default() -> Self { Self::S16 } } @@ -64,7 +64,7 @@ impl AudioFormat { match self { Self::S24_3 => mem::size_of::(), Self::S16 => mem::size_of::(), - _ => mem::size_of::(), + _ => mem::size_of::(), // S32 and S24 are both stored in i32 } } } @@ -79,16 +79,16 @@ impl FromStr for NormalisationType { type Err = (); fn from_str(s: &str) -> Result { match s { - "album" => Ok(NormalisationType::Album), - "track" => Ok(NormalisationType::Track), + "album" => Ok(Self::Album), + "track" => Ok(Self::Track), _ => Err(()), } } } impl Default for NormalisationType { - fn default() -> NormalisationType { - NormalisationType::Album + fn default() -> Self { + Self::Album } } @@ -102,16 +102,16 @@ impl FromStr for NormalisationMethod { type Err = (); fn from_str(s: &str) -> Result { match s { - "basic" => Ok(NormalisationMethod::Basic), - "dynamic" => Ok(NormalisationMethod::Dynamic), + "basic" => Ok(Self::Basic), + "dynamic" => Ok(Self::Dynamic), _ => Err(()), } } } impl Default for NormalisationMethod { - fn default() -> NormalisationMethod { - NormalisationMethod::Dynamic + fn default() -> Self { + Self::Dynamic } } From 001d3ca1cf70fdbfef9703aa9cc47f43b050aa76 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 19 Mar 2021 22:28:55 +0100 Subject: [PATCH 12/23] Bump Alsa, cpal and GStreamer crates --- Cargo.lock | 584 ++++---------------------------------------- playback/Cargo.toml | 2 +- 2 files changed, 52 insertions(+), 534 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2296cfed..2d711765 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,26 +1,5 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -[[package]] -name = "addr2line" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" - -[[package]] -name = "adler32" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" - [[package]] name = "aes" version = "0.6.0" @@ -66,9 +45,9 @@ dependencies = [ [[package]] name = "alsa" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb213f6b3e4b1480a60931ca2035794aa67b73103d254715b1db7b70dcb3c934" +checksum = "75c4da790adcb2ce5e758c064b4f3ec17a30349f9961d3e5e6c9688b052a9e18" dependencies = [ "alsa-sys", "bitflags", @@ -92,12 +71,6 @@ version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1" -[[package]] -name = "ascii" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" - [[package]] name = "atty" version = "0.2.14" @@ -115,26 +88,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" -[[package]] -name = "backtrace" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef5140344c85b01f9bbb4d4b7288a8aa4b3287ccef913a14bcc78a1063623598" -dependencies = [ - "addr2line", - "cfg-if 1.0.0", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "base-x" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" - [[package]] name = "base64" version = "0.9.3" @@ -276,6 +229,9 @@ name = "cc" version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" +dependencies = [ + "jobserver", +] [[package]] name = "cesu8" @@ -313,16 +269,10 @@ dependencies = [ "libc", "num-integer", "num-traits", - "time 0.1.43", + "time", "winapi 0.3.9", ] -[[package]] -name = "chunked_transfer" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" - [[package]] name = "cipher" version = "0.2.5" @@ -352,19 +302,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "combine" -version = "3.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" -dependencies = [ - "ascii", - "byteorder", - "either", - "memchr", - "unreachable", -] - [[package]] name = "combine" version = "4.5.2" @@ -375,39 +312,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "const_fn" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b9d6de7f49e22cf97ad17fc4036ece69300032f45f78f30b4a4482cdc3f4a6" - -[[package]] -name = "cookie" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784ad0fbab4f3e9cef09f20e0aea6000ae08d2cb98ac4c0abc53df18803d702f" -dependencies = [ - "percent-encoding 2.1.0", - "time 0.2.25", - "version_check", -] - -[[package]] -name = "cookie_store" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3818dfca4b0cb5211a659bbcbb94225b7127407b2b135e650d717bfb78ab10d3" -dependencies = [ - "cookie", - "idna 0.2.1", - "log 0.4.14", - "publicsuffix", - "serde", - "serde_json", - "time 0.2.25", - "url 2.2.0", -] - [[package]] name = "core-foundation-sys" version = "0.6.2" @@ -416,9 +320,9 @@ checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" [[package]] name = "coreaudio-rs" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f229761965dad3e9b11081668a6ea00f1def7aa46062321b5ec245b834f6e491" +checksum = "11894b20ebfe1ff903cbdc52259693389eea03b94918a2def2c30c3bf227ad88" dependencies = [ "bitflags", "coreaudio-sys", @@ -435,15 +339,15 @@ dependencies = [ [[package]] name = "cpal" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05631e2089dfa5d3b6ea1cfbbfd092e2ee5deeb69698911bc976b28b746d3657" +checksum = "840981d3f30230d9120328d64be72319dbbedabb61bcd4c370a54cdd051238ac" dependencies = [ "alsa", "core-foundation-sys", "coreaudio-rs", "jack", - "jni 0.17.0", + "jni", "js-sys", "lazy_static", "libc", @@ -453,7 +357,7 @@ dependencies = [ "nix", "oboe", "parking_lot 0.11.1", - "stdweb 0.1.3", + "stdweb", "thiserror", "web-sys", "winapi 0.3.9", @@ -465,15 +369,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" -[[package]] -name = "crc32fast" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" -dependencies = [ - "cfg-if 1.0.0", -] - [[package]] name = "crossbeam-deque" version = "0.7.3" @@ -624,12 +519,6 @@ dependencies = [ "generic-array 0.14.4", ] -[[package]] -name = "discard" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" - [[package]] name = "dns-sd" version = "0.1.3" @@ -664,7 +553,6 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" dependencies = [ - "backtrace", "version_check", ] @@ -674,45 +562,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" -[[package]] -name = "fetch_unroll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d44807d562d137f063cbfe209da1c3f9f2fa8375e11166ef495daab7b847f9" -dependencies = [ - "libflate", - "tar", - "ureq", -] - -[[package]] -name = "filetime" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "redox_syscall 0.2.4", - "winapi 0.3.9", -] - [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "form_urlencoded" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00" -dependencies = [ - "matches", - "percent-encoding 2.1.0", -] - [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -876,12 +731,6 @@ dependencies = [ "wasi 0.10.2+wasi-snapshot-preview1", ] -[[package]] -name = "gimli" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" - [[package]] name = "glib" version = "0.10.3" @@ -1115,9 +964,9 @@ dependencies = [ "log 0.4.14", "mime", "net2", - "percent-encoding 1.0.1", + "percent-encoding", "relay", - "time 0.1.43", + "time", "tokio-core", "tokio-io", "tokio-proto", @@ -1156,17 +1005,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "idna" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de910d521f7cc3135c4de8db1cb910e0b5ed1dc6f57c381cd07e8e661ce10094" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "if-addrs" version = "0.6.5" @@ -1246,29 +1084,15 @@ dependencies = [ [[package]] name = "jni" -version = "0.14.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1981310da491a4f0f815238097d0d43d8072732b5ae5f8bd0d8eadf5bf245402" +checksum = "24967112a1e4301ca5342ea339763613a37592b8a6ce6cf2e4494537c7a42faf" dependencies = [ "cesu8", - "combine 3.8.1", - "error-chain", - "jni-sys", - "log 0.4.14", - "walkdir", -] - -[[package]] -name = "jni" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36bcc950632e48b86da402c5c077590583da5ac0d480103611d5374e7c967a3c" -dependencies = [ - "cesu8", - "combine 4.5.2", - "error-chain", + "combine", "jni-sys", "log 0.4.14", + "thiserror", "walkdir", ] @@ -1278,6 +1102,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.47" @@ -1328,27 +1161,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.85" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ccac4b00700875e6a07c6cde370d44d32fa01c5a65cdd2fca6858c479d28bb3" - -[[package]] -name = "libflate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389de7875e06476365974da3e7ff85d55f1972188ccd9f6020dd7c8156e17914" -dependencies = [ - "adler32", - "crc32fast", - "libflate_lz77", - "rle-decode-fast", -] - -[[package]] -name = "libflate_lz77" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3286f09f7d4926fc486334f28d8d2e6ebe4f7f9994494b6dab27ddfad2c9b11b" +checksum = "ba4aede83fc3617411dc6993bc8c70919750c1c257c6ca6a502aed6e0e2394ae" [[package]] name = "libloading" @@ -1452,7 +1267,7 @@ dependencies = [ "tokio-io", "tokio-process", "tokio-signal", - "url 1.7.2", + "url", ] [[package]] @@ -1500,7 +1315,7 @@ dependencies = [ "serde_json", "sha-1 0.9.3", "tokio-core", - "url 1.7.2", + "url", ] [[package]] @@ -1534,7 +1349,7 @@ dependencies = [ "tokio-codec", "tokio-core", "tokio-io", - "url 1.7.2", + "url", "uuid", "vergen", ] @@ -1690,16 +1505,6 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" -[[package]] -name = "miniz_oxide" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d" -dependencies = [ - "adler", - "autocfg", -] - [[package]] name = "mio" version = "0.6.23" @@ -1781,9 +1586,9 @@ dependencies = [ [[package]] name = "ndk" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb167c1febed0a496639034d0c76b3b74263636045db5489eee52143c246e73" +checksum = "8794322172319b972f528bf90c6b467be0079f1fa82780ffb431088e741a73ab" dependencies = [ "jni-sys", "ndk-sys", @@ -1793,9 +1598,9 @@ dependencies = [ [[package]] name = "ndk-glue" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdf399b8b7a39c6fb153c4ec32c72fd5fe789df24a647f229c239aa7adb15241" +checksum = "c5caf0c24d51ac1c905c27d4eda4fa0635bbe0de596b8f79235e0b17a4d29385" dependencies = [ "lazy_static", "libc", @@ -1837,15 +1642,14 @@ dependencies = [ [[package]] name = "nix" -version = "0.15.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2e0b4f3320ed72aaedb9a5ac838690a8047c7b275da22711fddff4f8a14229" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" dependencies = [ "bitflags", "cc", - "cfg-if 0.1.10", + "cfg-if 1.0.0", "libc", - "void", ] [[package]] @@ -1922,9 +1726,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.4.3" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca565a7df06f3d4b485494f25ba05da1435950f4dc263440eda7a6fa9b8e36e4" +checksum = "226b45a5c2ac4dd696ed30fa6b94b057ad909c7b7fc2e0d0808192bced894066" dependencies = [ "derivative", "num_enum_derive", @@ -1932,9 +1736,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.4.3" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffa5a33ddddfee04c0283a7653987d634e880347e96b5b2ed64de07efb59db9d" +checksum = "1c0fd9eba1d5db0994a239e09c1be402d35622277e35468ba891aa5e3188ce7e" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1942,19 +1746,13 @@ dependencies = [ "syn", ] -[[package]] -name = "object" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397" - [[package]] name = "oboe" -version = "0.3.1" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aadc2b0867bdbb9a81c4d99b9b682958f49dbea1295a81d2f646cca2afdd9fc" +checksum = "4cfb2390bddb9546c0f7448fd1d2abdd39e6075206f960991eb28c7fa7f126c4" dependencies = [ - "jni 0.14.0", + "jni", "ndk", "ndk-glue", "num-derive", @@ -1964,11 +1762,11 @@ dependencies = [ [[package]] name = "oboe-sys" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ff7a51600eabe34e189eec5c995a62f151d8d97e5fbca39e87ca738bb99b82" +checksum = "fe069264d082fc820dfa172f79be3f2e088ecfece9b1c47b0c9fd838d2bef103" dependencies = [ - "fetch_unroll", + "cc", ] [[package]] @@ -2088,12 +1886,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" -[[package]] -name = "percent-encoding" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" - [[package]] name = "pin-project-lite" version = "0.2.4" @@ -2224,28 +2016,6 @@ dependencies = [ "protobuf-codegen", ] -[[package]] -name = "publicsuffix" -version = "1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bbaa49075179162b49acac1c6aa45fb4dafb5f13cf6794276d77bc7fd95757b" -dependencies = [ - "error-chain", - "idna 0.2.1", - "lazy_static", - "regex", - "url 2.2.0", -] - -[[package]] -name = "qstring" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" -dependencies = [ - "percent-encoding 2.1.0", -] - [[package]] name = "quick-error" version = "1.2.3" @@ -2437,27 +2207,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin", - "untrusted", - "web-sys", - "winapi 0.3.9", -] - -[[package]] -name = "rle-decode-fast" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cabe4fa914dec5870285fa7f71f602645da47c486e68486d2b4ceb4a343e90ac" - [[package]] name = "rodio" version = "0.13.0" @@ -2477,12 +2226,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "rustc-demangle" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" - [[package]] name = "rustc-hash" version = "1.1.0" @@ -2498,19 +2241,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustls" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "064fd21ff87c6e87ed4506e68beb42459caa4a0e2eb144932e6776768556980b" -dependencies = [ - "base64 0.13.0", - "log 0.4.14", - "ring", - "sct", - "webpki", -] - [[package]] name = "ryu" version = "1.0.5" @@ -2544,16 +2274,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" -[[package]] -name = "sct" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "sdl2" version = "0.34.3" @@ -2597,9 +2317,6 @@ name = "serde" version = "1.0.123" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" -dependencies = [ - "serde_derive", -] [[package]] name = "serde_derive" @@ -2648,12 +2365,6 @@ dependencies = [ "opaque-debug 0.3.0", ] -[[package]] -name = "sha1" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" - [[package]] name = "shannon" version = "0.2.0" @@ -2728,76 +2439,12 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "standback" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2beb4d1860a61f571530b3f855a1b538d0200f7871c63331ecd6f17b1f014f8" -dependencies = [ - "version_check", -] - [[package]] name = "stdweb" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" -[[package]] -name = "stdweb" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" -dependencies = [ - "discard", - "rustc_version", - "stdweb-derive", - "stdweb-internal-macros", - "stdweb-internal-runtime", - "wasm-bindgen", -] - -[[package]] -name = "stdweb-derive" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "serde_derive", - "syn", -] - -[[package]] -name = "stdweb-internal-macros" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" -dependencies = [ - "base-x", - "proc-macro2", - "quote", - "serde", - "serde_derive", - "serde_json", - "sha1", - "syn", -] - -[[package]] -name = "stdweb-internal-runtime" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" - [[package]] name = "strsim" version = "0.9.3" @@ -2872,17 +2519,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b157868d8ac1f56b64604539990685fa7611d8fa9e5476cf0c02cf34d32917c5" -[[package]] -name = "tar" -version = "0.4.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0313546c01d59e29be4f09687bcb4fb6690cec931cc3607b6aec7a0e417f4cc6" -dependencies = [ - "filetime", - "libc", - "xattr", -] - [[package]] name = "tempfile" version = "3.2.0" @@ -2936,44 +2572,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "time" -version = "0.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1195b046942c221454c2539395f85413b33383a067449d78aab2b7b052a142f7" -dependencies = [ - "const_fn", - "libc", - "standback", - "stdweb 0.4.20", - "time-macros", - "version_check", - "winapi 0.3.9", -] - -[[package]] -name = "time-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" -dependencies = [ - "proc-macro-hack", - "time-macros-impl", -] - -[[package]] -name = "time-macros-impl" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" -dependencies = [ - "proc-macro-hack", - "proc-macro2", - "quote", - "standback", - "syn", -] - [[package]] name = "tinyvec" version = "1.1.1" @@ -3319,61 +2917,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" -[[package]] -name = "unreachable" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" -dependencies = [ - "void", -] - -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - -[[package]] -name = "ureq" -version = "1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "294b85ef5dbc3670a72e82a89971608a1fcc4ed5c7c5a2895230d31a95f0569b" -dependencies = [ - "base64 0.13.0", - "chunked_transfer", - "cookie", - "cookie_store", - "log 0.4.14", - "once_cell", - "qstring", - "rustls", - "url 2.2.0", - "webpki", - "webpki-roots", -] - [[package]] name = "url" version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" dependencies = [ - "idna 0.1.5", + "idna", "matches", - "percent-encoding 1.0.1", -] - -[[package]] -name = "url" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e" -dependencies = [ - "form_urlencoded", - "idna 0.2.1", - "matches", - "percent-encoding 2.1.0", + "percent-encoding", ] [[package]] @@ -3407,12 +2959,6 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" -[[package]] -name = "void" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" - [[package]] name = "vorbis" version = "0.0.14" @@ -3548,25 +3094,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "webpki-roots" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82015b7e0b8bad8185994674a13a93306bea76cf5a16c5a181382fd3a5ec2376" -dependencies = [ - "webpki", -] - [[package]] name = "winapi" version = "0.2.8" @@ -3620,15 +3147,6 @@ dependencies = [ "winapi-build", ] -[[package]] -name = "xattr" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" -dependencies = [ - "libc", -] - [[package]] name = "zerocopy" version = "0.3.0" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 67e06be7..952ecdea 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -23,7 +23,7 @@ log = "0.4" byteorder = "1.3" shell-words = "1.0.0" -alsa = { version = "0.4", optional = true } +alsa = { version = "0.5", optional = true } portaudio-rs = { version = "0.3", optional = true } libpulse-binding = { version = "2.13", optional = true, default-features = false } libpulse-simple-binding = { version = "2.13", optional = true, default-features = false } From 74b2fea33814b8ea190343d8ab6a7cd1f5c6f9c2 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 21 Mar 2021 22:16:47 +0100 Subject: [PATCH 13/23] Refactor sample conversion into separate struct --- audio/src/lib.rs | 67 +++++++++++++------------ playback/src/audio_backend/mod.rs | 9 ++-- playback/src/audio_backend/portaudio.rs | 15 +++--- playback/src/audio_backend/rodio.rs | 9 ++-- playback/src/audio_backend/sdl.rs | 14 +++--- 5 files changed, 61 insertions(+), 53 deletions(-) diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 86c5b4ae..fe3b5c96 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -38,33 +38,6 @@ pub enum AudioPacket { OggData(Vec), } -#[derive(AsBytes, Copy, Clone, Debug)] -#[allow(non_camel_case_types)] -#[repr(transparent)] -pub struct i24([u8; 3]); -impl i24 { - fn pcm_from_i32(sample: i32) -> Self { - // drop the least significant byte - let [a, b, c, _d] = (sample >> 8).to_le_bytes(); - i24([a, b, c]) - } -} - -// Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX] while maintaining DC linearity. -macro_rules! convert_samples_to { - ($type: ident, $samples: expr) => { - convert_samples_to!($type, $samples, 0) - }; - ($type: ident, $samples: expr, $shift: expr) => { - $samples - .iter() - .map(|sample| { - (*sample as f64 * (std::$type::MAX as f64 + 0.5) - 0.5) as $type >> $shift - }) - .collect() - }; -} - impl AudioPacket { pub fn samples(&self) -> &[f32] { match self { @@ -86,23 +59,53 @@ impl AudioPacket { AudioPacket::OggData(d) => d.is_empty(), } } +} - pub fn f32_to_s32(samples: &[f32]) -> Vec { +#[derive(AsBytes, Copy, Clone, Debug)] +#[allow(non_camel_case_types)] +#[repr(transparent)] +pub struct i24([u8; 3]); +impl i24 { + fn pcm_from_i32(sample: i32) -> Self { + // drop the least significant byte + let [a, b, c, _d] = (sample >> 8).to_le_bytes(); + i24([a, b, c]) + } +} + +// Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX] while maintaining DC linearity. +macro_rules! convert_samples_to { + ($type: ident, $samples: expr) => { + convert_samples_to!($type, $samples, 0) + }; + ($type: ident, $samples: expr, $drop_bits: expr) => { + $samples + .iter() + .map(|sample| { + (*sample as f64 * (std::$type::MAX as f64 + 0.5) - 0.5) as $type >> $drop_bits + }) + .collect() + }; +} + +pub struct SamplesConverter {} +impl SamplesConverter { + pub fn to_s32(samples: &[f32]) -> Vec { convert_samples_to!(i32, samples) } - pub fn f32_to_s24(samples: &[f32]) -> Vec { + pub fn to_s24(samples: &[f32]) -> Vec { convert_samples_to!(i32, samples, 8) } - pub fn f32_to_s24_3(samples: &[f32]) -> Vec { - Self::f32_to_s32(samples) + pub fn to_s24_3(samples: &[f32]) -> Vec { + Self::to_s32(samples) .iter() .map(|sample| i24::pcm_from_i32(*sample)) .collect() } - pub fn f32_to_s16(samples: &[f32]) -> Vec { + pub fn to_s16(samples: &[f32]) -> Vec { convert_samples_to!(i16, samples) } } diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 9c46dbe4..94b6a529 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -24,24 +24,25 @@ fn mk_sink(device: Option, format: AudioFormat macro_rules! sink_as_bytes { () => { fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + use crate::audio::{i24, SamplesConverter}; use zerocopy::AsBytes; match packet { AudioPacket::Samples(samples) => match self.format { AudioFormat::F32 => self.write_bytes(samples.as_bytes()), AudioFormat::S32 => { - let samples_s32 = AudioPacket::f32_to_s32(samples); + let samples_s32: &[i32] = &SamplesConverter::to_s32(samples); self.write_bytes(samples_s32.as_bytes()) } AudioFormat::S24 => { - let samples_s24 = AudioPacket::f32_to_s24(samples); + let samples_s24: &[i32] = &SamplesConverter::to_s24(samples); self.write_bytes(samples_s24.as_bytes()) } AudioFormat::S24_3 => { - let samples_s24_3 = AudioPacket::f32_to_s24_3(samples); + let samples_s24_3: &[i24] = &SamplesConverter::to_s24_3(samples); self.write_bytes(samples_s24_3.as_bytes()) } AudioFormat::S16 => { - let samples_s16 = AudioPacket::f32_to_s16(samples); + let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); self.write_bytes(samples_s16.as_bytes()) } }, diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index fca305e0..f29bac2d 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -1,5 +1,5 @@ use super::{Open, Sink}; -use crate::audio::AudioPacket; +use crate::audio::{AudioPacket, SamplesConverter}; use crate::config::AudioFormat; use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use portaudio_rs; @@ -146,18 +146,19 @@ impl<'a> Sink for PortAudioSink<'a> { $stream.as_mut().unwrap().write($samples) }; } + + let samples = packet.samples(); let result = match self { Self::F32(stream, _parameters) => { - let samples = packet.samples(); - write_sink!(stream, &samples) + write_sink!(stream, samples) } Self::S32(stream, _parameters) => { - let samples_s32: Vec = AudioPacket::f32_to_s32(packet.samples()); - write_sink!(stream, &samples_s32) + let samples_s32: &[i32] = &SamplesConverter::to_s32(samples); + write_sink!(stream, samples_s32) } Self::S16(stream, _parameters) => { - let samples_s16: Vec = AudioPacket::f32_to_s16(packet.samples()); - write_sink!(stream, &samples_s16) + let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); + write_sink!(stream, samples_s16) } }; match result { diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 5262a9cc..2fc4fbde 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -1,8 +1,9 @@ use super::{Open, Sink}; extern crate cpal; extern crate rodio; -use crate::audio::AudioPacket; +use crate::audio::{AudioPacket, SamplesConverter}; use crate::config::AudioFormat; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use cpal::traits::{DeviceTrait, HostTrait}; use std::process::exit; use std::{io, thread, time}; @@ -25,12 +26,12 @@ macro_rules! rodio_sink { let samples = packet.samples(); match self.format { AudioFormat::F32 => { - let source = rodio::buffer::SamplesBuffer::new(2, 44100, samples); + let source = rodio::buffer::SamplesBuffer::new(NUM_CHANNELS as u16, SAMPLE_RATE, samples); self.rodio_sink.append(source) }, AudioFormat::S16 => { - let samples_s16: Vec = AudioPacket::f32_to_s16(samples); - let source = rodio::buffer::SamplesBuffer::new(2, 44100, samples_s16); + let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); + let source = rodio::buffer::SamplesBuffer::new(NUM_CHANNELS as u16, SAMPLE_RATE, samples_s16); self.rodio_sink.append(source) }, _ => unimplemented!(), diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 64523732..b1b4c2e1 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -1,5 +1,5 @@ use super::{Open, Sink}; -use crate::audio::AudioPacket; +use crate::audio::{AudioPacket, SamplesConverter}; use crate::config::AudioFormat; use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use sdl2::audio::{AudioQueue, AudioSpecDesired}; @@ -89,20 +89,22 @@ impl Sink for SdlSink { } }}; } + + let samples = packet.samples(); match self { Self::F32(queue) => { drain_sink!(queue, mem::size_of::()); - queue.queue(packet.samples()) + queue.queue(samples) } Self::S32(queue) => { + let samples_s32: &[i32] = &SamplesConverter::to_s32(samples); drain_sink!(queue, mem::size_of::()); - let samples_s32: Vec = AudioPacket::f32_to_s32(packet.samples()); - queue.queue(&samples_s32) + queue.queue(samples_s32) } Self::S16(queue) => { + let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); drain_sink!(queue, mem::size_of::()); - let samples_s16: Vec = AudioPacket::f32_to_s16(packet.samples()); - queue.queue(&samples_s16) + queue.queue(samples_s16) } }; Ok(()) From bfca1ec15eae36637b55dac9f7080d8289100546 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 27 Mar 2021 21:13:14 +0100 Subject: [PATCH 14/23] Minor code improvements and crates bump --- Cargo.lock | 16 ++++++++-------- audio/Cargo.toml | 4 ++-- playback/Cargo.toml | 4 ++-- playback/src/audio_backend/portaudio.rs | 25 +++++++++++++------------ playback/src/audio_backend/rodio.rs | 12 ++++++------ 5 files changed, 31 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2d711765..f40f4dca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -795,9 +795,9 @@ dependencies = [ [[package]] name = "gstreamer" -version = "0.16.5" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d50f822055923f1cbede233aa5dfd4ee957cf328fb3076e330886094e11d6cf" +checksum = "9ff5d0f7ff308ae37e6eb47b6ded17785bdea06e438a708cd09e0288c1862f33" dependencies = [ "bitflags", "cfg-if 1.0.0", @@ -1061,9 +1061,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" [[package]] name = "jack" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c1871c91fa65aa328f3bedbaa54a6e5d1de009264684c153eb708ba933aa6f5" +checksum = "2deb4974bd7e6b2fb7784f27fa13d819d11292b3b004dce0185ec08163cf686a" dependencies = [ "bitflags", "jack-sys", @@ -1073,9 +1073,9 @@ dependencies = [ [[package]] name = "jack-sys" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d6ab7ada402b6a27912a2b86504be62a48c58313c886fe72a059127acb4d7" +checksum = "57983f0d72dfecf2b719ed39bc9cacd85194e1a94cb3f9146009eff9856fef41" dependencies = [ "lazy_static", "libc", @@ -1161,9 +1161,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4aede83fc3617411dc6993bc8c70919750c1c257c6ca6a502aed6e0e2394ae" +checksum = "8916b1f6ca17130ec6568feccee27c156ad12037880833a3b842a823236502e7" [[package]] name = "libloading" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 06f1dda6..2d67f4ce 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -24,8 +24,8 @@ num-traits = "0.2" tempfile = "3.1" zerocopy = "0.3" -librespot-tremor = { version = "0.2.0", optional = true } -vorbis = { version ="0.0.14", optional = true } +librespot-tremor = { version = "0.2", optional = true } +vorbis = { version ="0.0", optional = true } [features] with-tremor = ["librespot-tremor"] diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 952ecdea..07e31799 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -25,8 +25,8 @@ shell-words = "1.0.0" alsa = { version = "0.5", optional = true } portaudio-rs = { version = "0.3", optional = true } -libpulse-binding = { version = "2.13", optional = true, default-features = false } -libpulse-simple-binding = { version = "2.13", optional = true, default-features = false } +libpulse-binding = { version = "2", optional = true, default-features = false } +libpulse-simple-binding = { version = "2", optional = true, default-features = false } jack = { version = "0.6", optional = true } libc = { version = "0.2", optional = true } rodio = { version = "0.13", optional = true, default-features = false } diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index f29bac2d..35f7852f 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -98,7 +98,7 @@ impl<'a> Open for PortAudioSink<'a> { impl<'a> Sink for PortAudioSink<'a> { fn start(&mut self) -> io::Result<()> { macro_rules! start_sink { - ($stream: ident, $parameters: ident) => {{ + (ref mut $stream: ident, ref $parameters: ident) => {{ if $stream.is_none() { *$stream = Some( Stream::open( @@ -115,10 +115,11 @@ impl<'a> Sink for PortAudioSink<'a> { $stream.as_mut().unwrap().start().unwrap() }}; } + match self { - Self::F32(stream, parameters) => start_sink!(stream, parameters), - Self::S32(stream, parameters) => start_sink!(stream, parameters), - Self::S16(stream, parameters) => start_sink!(stream, parameters), + Self::F32(stream, parameters) => start_sink!(ref mut stream, ref parameters), + Self::S32(stream, parameters) => start_sink!(ref mut stream, ref parameters), + Self::S16(stream, parameters) => start_sink!(ref mut stream, ref parameters), }; Ok(()) @@ -126,15 +127,15 @@ impl<'a> Sink for PortAudioSink<'a> { fn stop(&mut self) -> io::Result<()> { macro_rules! stop_sink { - ($stream: expr) => {{ + (ref mut $stream: ident) => {{ $stream.as_mut().unwrap().stop().unwrap(); *$stream = None; }}; } match self { - Self::F32(stream, _parameters) => stop_sink!(stream), - Self::S32(stream, _parameters) => stop_sink!(stream), - Self::S16(stream, _parameters) => stop_sink!(stream), + Self::F32(stream, _parameters) => stop_sink!(ref mut stream), + Self::S32(stream, _parameters) => stop_sink!(ref mut stream), + Self::S16(stream, _parameters) => stop_sink!(ref mut stream), }; Ok(()) @@ -142,7 +143,7 @@ impl<'a> Sink for PortAudioSink<'a> { fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { macro_rules! write_sink { - ($stream: expr, $samples: expr) => { + (ref mut $stream: expr, $samples: expr) => { $stream.as_mut().unwrap().write($samples) }; } @@ -150,15 +151,15 @@ impl<'a> Sink for PortAudioSink<'a> { let samples = packet.samples(); let result = match self { Self::F32(stream, _parameters) => { - write_sink!(stream, samples) + write_sink!(ref mut stream, samples) } Self::S32(stream, _parameters) => { let samples_s32: &[i32] = &SamplesConverter::to_s32(samples); - write_sink!(stream, samples_s32) + write_sink!(ref mut stream, samples_s32) } Self::S16(stream, _parameters) => { let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); - write_sink!(stream, samples_s16) + write_sink!(ref mut stream, samples_s16) } }; match result { diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 2fc4fbde..6e914ea0 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -27,14 +27,14 @@ macro_rules! rodio_sink { match self.format { AudioFormat::F32 => { let source = rodio::buffer::SamplesBuffer::new(NUM_CHANNELS as u16, SAMPLE_RATE, samples); - self.rodio_sink.append(source) + self.rodio_sink.append(source); }, AudioFormat::S16 => { let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); let source = rodio::buffer::SamplesBuffer::new(NUM_CHANNELS as u16, SAMPLE_RATE, samples_s16); - self.rodio_sink.append(source) + self.rodio_sink.append(source); }, - _ => unimplemented!(), + _ => unreachable!(), }; // Chunk sizes seem to be about 256 to 3000 ish items long. @@ -64,15 +64,15 @@ macro_rules! rodio_sink { let rodio_device = match_device(&host, device); debug!("Using cpal device"); - let stream = rodio::OutputStream::try_from_device(&rodio_device) + let (stream, stream_handle) = rodio::OutputStream::try_from_device(&rodio_device) .expect("couldn't open output stream."); debug!("Using Rodio stream"); - let sink = rodio::Sink::try_new(&stream.1).expect("couldn't create output sink."); + let sink = rodio::Sink::try_new(&stream_handle).expect("couldn't create output sink."); debug!("Using Rodio sink"); Self { rodio_sink: sink, - stream: stream.0, + stream: stream, format: format, } } From cdbce21e71398e05f042ed2c6bf759402b3f6ac8 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 27 Mar 2021 21:42:10 +0100 Subject: [PATCH 15/23] Make S16 to F32 sample conversion less magical --- audio/src/libvorbis_decoder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audio/src/libvorbis_decoder.rs b/audio/src/libvorbis_decoder.rs index e7ccc984..e86fc000 100644 --- a/audio/src/libvorbis_decoder.rs +++ b/audio/src/libvorbis_decoder.rs @@ -45,7 +45,7 @@ where packet .data .iter() - .map(|sample| ((*sample as f64 + 0.5) / (0x7FFF as f64 + 0.5)) as f32) + .map(|sample| ((*sample as f64 + 0.5) / (std::i16::MAX as f64 + 0.5)) as f32) .collect(), ))); } From a200b259164f534cdcb151fbd57d0eefbb9ad48e Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 27 Mar 2021 21:44:01 +0100 Subject: [PATCH 16/23] Fix formatting --- audio/src/libvorbis_decoder.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/audio/src/libvorbis_decoder.rs b/audio/src/libvorbis_decoder.rs index e86fc000..5ad48f0d 100644 --- a/audio/src/libvorbis_decoder.rs +++ b/audio/src/libvorbis_decoder.rs @@ -45,7 +45,9 @@ where packet .data .iter() - .map(|sample| ((*sample as f64 + 0.5) / (std::i16::MAX as f64 + 0.5)) as f32) + .map(|sample| { + ((*sample as f64 + 0.5) / (std::i16::MAX as f64 + 0.5)) as f32 + }) .collect(), ))); } From cc60dc11dc15ed8316aac83cf3108a6e79c50d23 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 27 Mar 2021 22:52:43 +0100 Subject: [PATCH 17/23] Fix buffer size in JACK Audio backend --- playback/src/audio_backend/jackaudio.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index ed6ae1f7..7449cc11 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -53,7 +53,7 @@ impl Open for JackSink { let ch_r = client.register_port("out_0", AudioOut::default()).unwrap(); let ch_l = client.register_port("out_1", AudioOut::default()).unwrap(); // buffer for samples from librespot (~10ms) - let (tx, rx) = sync_channel::(NUM_CHANNELS as usize * 1024 * format.size()); + let (tx, rx) = sync_channel::(NUM_CHANNELS as usize * 1024 * AudioFormat::F32.size()); let jack_data = JackData { rec: rx, port_l: ch_l, From d252eeedc542c7933cbf3a8c3db11b23f1a8f1dd Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 27 Mar 2021 22:53:05 +0100 Subject: [PATCH 18/23] Warn about broken backends --- playback/src/audio_backend/portaudio.rs | 3 +++ playback/src/audio_backend/rodio.rs | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 35f7852f..5faff6ca 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -55,6 +55,9 @@ impl<'a> Open for PortAudioSink<'a> { fn open(device: Option, format: AudioFormat) -> PortAudioSink<'a> { info!("Using PortAudio sink with format: {:?}", format); + warn!("This backend is known to panic on several platforms."); + warn!("Consider using some other backend, or better yet, contributing a fix."); + portaudio_rs::initialize().unwrap(); let device_idx = match device.as_ref().map(AsRef::as_ref) { diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 6e914ea0..908099f1 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -54,8 +54,7 @@ macro_rules! rodio_sink { AudioFormat::F32 => { #[cfg(target_os = "linux")] { - warn!("Rodio output to Alsa is known to cause garbled sound on output formats other than 16-bit signed integer."); - warn!("Consider using `--backend alsa` OR `--format {:?}`", AudioFormat::S16); + warn!("Rodio output to Alsa is known to cause garbled sound, consider using `--backend alsa`"); } }, AudioFormat::S16 => {}, From 07d710e14f9a1aca94db7d35d33f4f65327a741a Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 31 Mar 2021 20:41:09 +0200 Subject: [PATCH 19/23] Use AudioFormat size for SDL --- playback/src/audio_backend/sdl.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index b1b4c2e1..7e071dd1 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -3,7 +3,7 @@ use crate::audio::{AudioPacket, SamplesConverter}; use crate::config::AudioFormat; use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use sdl2::audio::{AudioQueue, AudioSpecDesired}; -use std::{io, mem, thread, time}; +use std::{io, thread, time}; pub enum SdlSink { F32(AudioQueue), @@ -93,17 +93,17 @@ impl Sink for SdlSink { let samples = packet.samples(); match self { Self::F32(queue) => { - drain_sink!(queue, mem::size_of::()); + drain_sink!(queue, AudioFormat::F32.size()); queue.queue(samples) } Self::S32(queue) => { let samples_s32: &[i32] = &SamplesConverter::to_s32(samples); - drain_sink!(queue, mem::size_of::()); + drain_sink!(queue, AudioFormat::S32.size()); queue.queue(samples_s32) } Self::S16(queue) => { let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); - drain_sink!(queue, mem::size_of::()); + drain_sink!(queue, AudioFormat::S16.size()); queue.queue(samples_s16) } }; From 78bc621ebba467208358519f58e8bab7d9eafec8 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 5 Apr 2021 21:30:40 +0200 Subject: [PATCH 20/23] Move SamplesConverter into convert.rs --- audio/src/convert.rs | 50 ++++++++++++++++++++++++++++++++++++++++++ audio/src/lib.rs | 52 ++------------------------------------------ 2 files changed, 52 insertions(+), 50 deletions(-) create mode 100644 audio/src/convert.rs diff --git a/audio/src/convert.rs b/audio/src/convert.rs new file mode 100644 index 00000000..74a4d8f4 --- /dev/null +++ b/audio/src/convert.rs @@ -0,0 +1,50 @@ +use zerocopy::AsBytes; + +#[derive(AsBytes, Copy, Clone, Debug)] +#[allow(non_camel_case_types)] +#[repr(transparent)] +pub struct i24([u8; 3]); +impl i24 { + fn pcm_from_i32(sample: i32) -> Self { + // drop the least significant byte + let [a, b, c, _d] = (sample >> 8).to_le_bytes(); + i24([a, b, c]) + } +} + +// Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX] while maintaining DC linearity. +macro_rules! convert_samples_to { + ($type: ident, $samples: expr) => { + convert_samples_to!($type, $samples, 0) + }; + ($type: ident, $samples: expr, $drop_bits: expr) => { + $samples + .iter() + .map(|sample| { + (*sample as f64 * (std::$type::MAX as f64 + 0.5) - 0.5) as $type >> $drop_bits + }) + .collect() + }; +} + +pub struct SamplesConverter {} +impl SamplesConverter { + pub fn to_s32(samples: &[f32]) -> Vec { + convert_samples_to!(i32, samples) + } + + pub fn to_s24(samples: &[f32]) -> Vec { + convert_samples_to!(i32, samples, 8) + } + + pub fn to_s24_3(samples: &[f32]) -> Vec { + Self::to_s32(samples) + .iter() + .map(|sample| i24::pcm_from_i32(*sample)) + .collect() + } + + pub fn to_s16(samples: &[f32]) -> Vec { + convert_samples_to!(i16, samples) + } +} diff --git a/audio/src/lib.rs b/audio/src/lib.rs index fe3b5c96..44f732b3 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -13,6 +13,7 @@ extern crate tempfile; extern crate librespot_core; +mod convert; mod decrypt; mod fetch; @@ -24,6 +25,7 @@ mod passthrough_decoder; mod range_set; +pub use convert::{i24, SamplesConverter}; pub use decrypt::AudioDecrypt; pub use fetch::{AudioFile, AudioFileOpen, StreamLoaderController}; pub use fetch::{ @@ -31,7 +33,6 @@ pub use fetch::{ READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS, }; use std::fmt; -use zerocopy::AsBytes; pub enum AudioPacket { Samples(Vec), @@ -61,55 +62,6 @@ impl AudioPacket { } } -#[derive(AsBytes, Copy, Clone, Debug)] -#[allow(non_camel_case_types)] -#[repr(transparent)] -pub struct i24([u8; 3]); -impl i24 { - fn pcm_from_i32(sample: i32) -> Self { - // drop the least significant byte - let [a, b, c, _d] = (sample >> 8).to_le_bytes(); - i24([a, b, c]) - } -} - -// Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX] while maintaining DC linearity. -macro_rules! convert_samples_to { - ($type: ident, $samples: expr) => { - convert_samples_to!($type, $samples, 0) - }; - ($type: ident, $samples: expr, $drop_bits: expr) => { - $samples - .iter() - .map(|sample| { - (*sample as f64 * (std::$type::MAX as f64 + 0.5) - 0.5) as $type >> $drop_bits - }) - .collect() - }; -} - -pub struct SamplesConverter {} -impl SamplesConverter { - pub fn to_s32(samples: &[f32]) -> Vec { - convert_samples_to!(i32, samples) - } - - pub fn to_s24(samples: &[f32]) -> Vec { - convert_samples_to!(i32, samples, 8) - } - - pub fn to_s24_3(samples: &[f32]) -> Vec { - Self::to_s32(samples) - .iter() - .map(|sample| i24::pcm_from_i32(*sample)) - .collect() - } - - pub fn to_s16(samples: &[f32]) -> Vec { - convert_samples_to!(i16, samples) - } -} - #[cfg(not(any(feature = "with-tremor", feature = "with-vorbis")))] pub use crate::lewton_decoder::{VorbisDecoder, VorbisError}; #[cfg(any(feature = "with-tremor", feature = "with-vorbis"))] From 928a6736538da12e509ec55a626b29bb9e9d54da Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 5 Apr 2021 23:14:02 +0200 Subject: [PATCH 21/23] DRY up constructors --- playback/src/audio_backend/alsa.rs | 2 +- playback/src/audio_backend/gstreamer.rs | 2 +- playback/src/audio_backend/jackaudio.rs | 2 +- playback/src/audio_backend/pipe.rs | 2 +- playback/src/audio_backend/pulseaudio.rs | 2 +- playback/src/audio_backend/sdl.rs | 2 +- playback/src/audio_backend/subprocess.rs | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index fc2a775c..1d551878 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -71,7 +71,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box } impl Open for AlsaSink { - fn open(device: Option, format: AudioFormat) -> AlsaSink { + fn open(device: Option, format: AudioFormat) -> Self { info!("Using Alsa sink with format: {:?}", format); let name = match device.as_ref().map(AsRef::as_ref) { diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index d3c736a4..d59677ac 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -16,7 +16,7 @@ pub struct GstreamerSink { } impl Open for GstreamerSink { - fn open(device: Option, format: AudioFormat) -> GstreamerSink { + fn open(device: Option, format: AudioFormat) -> Self { info!("Using GStreamer sink with format: {:?}", format); gst::init().expect("failed to init GStreamer!"); diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 7449cc11..24a94a3e 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -41,7 +41,7 @@ impl ProcessHandler for JackData { } impl Open for JackSink { - fn open(client_name: Option, format: AudioFormat) -> JackSink { + fn open(client_name: Option, format: AudioFormat) -> Self { if format != AudioFormat::F32 { warn!("JACK currently does not support {:?} output", format); } diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index ae77e320..6948db94 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -10,7 +10,7 @@ pub struct StdoutSink { } impl Open for StdoutSink { - fn open(path: Option, format: AudioFormat) -> StdoutSink { + fn open(path: Option, format: AudioFormat) -> Self { info!("Using pipe sink with format: {:?}", format); let output: Box = match path { diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 16800eb0..507e453c 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -17,7 +17,7 @@ pub struct PulseAudioSink { } impl Open for PulseAudioSink { - fn open(device: Option, format: AudioFormat) -> PulseAudioSink { + fn open(device: Option, format: AudioFormat) -> Self { info!("Using PulseAudio sink with format: {:?}", format); // PulseAudio calls S24 and S24_3 different from the rest of the world diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 7e071dd1..0a3fd433 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -12,7 +12,7 @@ pub enum SdlSink { } impl Open for SdlSink { - fn open(device: Option, format: AudioFormat) -> SdlSink { + fn open(device: Option, format: AudioFormat) -> Self { info!("Using SDL sink with format: {:?}", format); if device.is_some() { diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index 586bb75b..bebb6ea0 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -12,7 +12,7 @@ pub struct SubprocessSink { } impl Open for SubprocessSink { - fn open(shell_command: Option, format: AudioFormat) -> SubprocessSink { + fn open(shell_command: Option, format: AudioFormat) -> Self { info!("Using subprocess sink with format: {:?}", format); if let Some(shell_command) = shell_command { From d0ea9631d2c9e0b1be5198061280a3894bce9287 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 9 Apr 2021 19:31:26 +0200 Subject: [PATCH 22/23] Optimize requantizer to work in `f32`, then round --- audio/src/convert.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/audio/src/convert.rs b/audio/src/convert.rs index 74a4d8f4..e291c804 100644 --- a/audio/src/convert.rs +++ b/audio/src/convert.rs @@ -21,7 +21,16 @@ macro_rules! convert_samples_to { $samples .iter() .map(|sample| { - (*sample as f64 * (std::$type::MAX as f64 + 0.5) - 0.5) as $type >> $drop_bits + // Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX] + // while maintaining DC linearity. There is nothing to be gained + // by doing this in f64, as the significand of a f32 is 24 bits, + // just like the maximum bit depth we are converting to. + let int_value = *sample * (std::$type::MAX as f32 + 0.5) - 0.5; + + // Casting floats to ints truncates by default, which results + // in larger quantization error than rounding arithmetically. + // Flooring is faster, but again with larger error. + int_value.round() as $type >> $drop_bits }) .collect() }; From 222f9bbd01994f55160c3d226641c9e258eee7fd Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 9 Apr 2021 20:01:21 +0200 Subject: [PATCH 23/23] Bump playback crates to the latest supporting Rust 1.41.1 For Rodio, this fixes garbled sound on some but not all Alsa hosts. --- Cargo.lock | 20 ++++++++++---------- playback/Cargo.toml | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f40f4dca..837df2fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -339,9 +339,9 @@ dependencies = [ [[package]] name = "cpal" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "840981d3f30230d9120328d64be72319dbbedabb61bcd4c370a54cdd051238ac" +checksum = "8351ddf2aaa3c583fa388029f8b3d26f3c7035a20911fdd5f2e2ed7ab57dad25" dependencies = [ "alsa", "core-foundation-sys", @@ -1161,9 +1161,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.91" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8916b1f6ca17130ec6568feccee27c156ad12037880833a3b842a823236502e7" +checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41" [[package]] name = "libloading" @@ -1195,9 +1195,9 @@ dependencies = [ [[package]] name = "libpulse-binding" -version = "2.23.0" +version = "2.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2405f806801527dfb3d2b6d48a282cdebe9a1b41b0652e0d7b5bad81dbc700e" +checksum = "db951f37898e19a6785208e3a290261e0f1a8e086716be596aaad684882ca8e3" dependencies = [ "bitflags", "libc", @@ -2209,9 +2209,9 @@ dependencies = [ [[package]] name = "rodio" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9683532495146e98878d4948fa1a1953f584cd923f2a5f5c26b7a8701b56943" +checksum = "b65c2eda643191f6d1bb12ea323a9db8d9ba95374e9be3780b5a9fb5cfb8520f" dependencies = [ "cpal", ] @@ -2288,9 +2288,9 @@ dependencies = [ [[package]] name = "sdl2-sys" -version = "0.34.3" +version = "0.34.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d81feded049b9c14eceb4a4f6d596a98cebbd59abdba949c5552a015466d33" +checksum = "4cb164f53dbcad111de976bbf1f3083d3fcdeda88da9cfa281c70822720ee3da" dependencies = [ "cfg-if 0.1.10", "libc", diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 07e31799..0666259d 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -31,7 +31,7 @@ jack = { version = "0.6", optional = true } libc = { version = "0.2", optional = true } rodio = { version = "0.13", optional = true, default-features = false } cpal = { version = "0.13", optional = true } -sdl2 = { version = "0.34", optional = true } +sdl2 = { version = "0.34.3", optional = true } gstreamer = { version = "0.16", optional = true } gstreamer-app = { version = "0.16", optional = true } glib = { version = "0.10", optional = true } @@ -45,4 +45,4 @@ jackaudio-backend = ["jack"] rodiojack-backend = ["rodio", "cpal/jack"] rodio-backend = ["rodio", "cpal"] sdl-backend = ["sdl2"] -gstreamer-backend = ["gstreamer", "gstreamer-app", "glib" ] +gstreamer-backend = ["gstreamer", "gstreamer-app", "glib"]