From fe2d5ca7c654cd5981969ae880ccd69e6d1066d3 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 30 May 2021 20:09:39 +0200 Subject: [PATCH] Store and process samples in 64 bit (#773) --- CHANGELOG.md | 2 + playback/src/audio_backend/alsa.rs | 1 + playback/src/audio_backend/jackaudio.rs | 7 +-- playback/src/audio_backend/mod.rs | 14 +++-- playback/src/audio_backend/portaudio.rs | 7 +-- playback/src/audio_backend/pulseaudio.rs | 3 ++ playback/src/audio_backend/rodio.rs | 10 ++-- playback/src/audio_backend/sdl.rs | 7 +-- playback/src/config.rs | 22 ++++---- playback/src/convert.rs | 37 +++++++------ playback/src/decoder/lewton_decoder.rs | 8 ++- playback/src/decoder/mod.rs | 9 +++- playback/src/dither.rs | 16 +++--- playback/src/mixer/alsamixer.rs | 26 ++++----- playback/src/mixer/mappings.rs | 50 +++++++++--------- playback/src/mixer/mod.rs | 2 +- playback/src/mixer/softmixer.rs | 20 +++---- playback/src/player.rs | 67 +++++++++++------------- src/main.rs | 18 +++---- 19 files changed, 177 insertions(+), 149 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 499bebce..83fd100f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [playback] Add support for dithering with `--dither` for lower requantization error (breaking) - [playback] Add `--volume-range` option to set dB range and control `log` and `cubic` volume control curves - [playback] `alsamixer`: support for querying dB range from Alsa softvol +- [playback] Add `--format F64` (supported by Alsa and GStreamer only) ### Changed - [audio, playback] Moved `VorbisDecoder`, `VorbisError`, `AudioPacket`, `PassthroughDecoder`, `PassthroughError`, `AudioError`, `AudioDecoder` and the `convert` module from `librespot-audio` to `librespot-playback`. The underlying crates `vorbis`, `librespot-tremor`, `lewton` and `ogg` should be used directly. (breaking) - [connect, playback] Moved volume controls from `librespot-connect` to `librespot-playback` crate - [connect] Synchronize player volume with mixer volume on playback +- [playback] Store and pass samples in 64-bit floating point - [playback] Make cubic volume control available to all mixers with `--volume-ctrl cubic` - [playback] Normalize volumes to `[0.0..1.0]` instead of `[0..65535]` for greater precision and performance (breaking) - [playback] `alsamixer`: complete rewrite (breaking) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 98939668..b5deb2eb 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -41,6 +41,7 @@ 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 = match format { + AudioFormat::F64 => Format::float64(), AudioFormat::F32 => Format::float(), AudioFormat::S32 => Format::s32(), AudioFormat::S24 => Format::s24(), diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index ab75fff2..75c4eb9f 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -70,9 +70,10 @@ impl Open for JackSink { } impl Sink for JackSink { - fn write(&mut self, packet: &AudioPacket, _: &mut Converter) -> io::Result<()> { - for s in packet.samples().iter() { - let res = self.send.send(*s); + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { + let samples_f32: &[f32] = &converter.f64_to_f32(packet.samples()); + for sample in samples_f32.iter() { + let res = self.send.send(*sample); if res.is_err() { error!("cannot write to channel"); } diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index e4653f17..0996bfb6 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -35,21 +35,25 @@ macro_rules! sink_as_bytes { use zerocopy::AsBytes; match packet { AudioPacket::Samples(samples) => match self.format { - AudioFormat::F32 => self.write_bytes(samples.as_bytes()), + AudioFormat::F64 => self.write_bytes(samples.as_bytes()), + AudioFormat::F32 => { + let samples_f32: &[f32] = &converter.f64_to_f32(samples); + self.write_bytes(samples_f32.as_bytes()) + } AudioFormat::S32 => { - let samples_s32: &[i32] = &converter.f32_to_s32(samples); + let samples_s32: &[i32] = &converter.f64_to_s32(samples); self.write_bytes(samples_s32.as_bytes()) } AudioFormat::S24 => { - let samples_s24: &[i32] = &converter.f32_to_s24(samples); + let samples_s24: &[i32] = &converter.f64_to_s24(samples); self.write_bytes(samples_s24.as_bytes()) } AudioFormat::S24_3 => { - let samples_s24_3: &[i24] = &converter.f32_to_s24_3(samples); + let samples_s24_3: &[i24] = &converter.f64_to_s24_3(samples); self.write_bytes(samples_s24_3.as_bytes()) } AudioFormat::S16 => { - let samples_s16: &[i16] = &converter.f32_to_s16(samples); + let samples_s16: &[i16] = &converter.f64_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 0bcd1aa5..ccebcfdf 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -151,14 +151,15 @@ impl<'a> Sink for PortAudioSink<'a> { let samples = packet.samples(); let result = match self { Self::F32(stream, _parameters) => { - write_sink!(ref mut stream, samples) + let samples_f32: &[f32] = &converter.f64_to_f32(samples); + write_sink!(ref mut stream, samples_f32) } Self::S32(stream, _parameters) => { - let samples_s32: &[i32] = &converter.f32_to_s32(samples); + let samples_s32: &[i32] = &converter.f64_to_s32(samples); write_sink!(ref mut stream, samples_s32) } Self::S16(stream, _parameters) => { - let samples_s16: &[i16] = &converter.f32_to_s16(samples); + let samples_s16: &[i16] = &converter.f64_to_s16(samples); write_sink!(ref mut stream, samples_s16) } }; diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 57c9b8bc..1298cd7c 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -28,6 +28,9 @@ impl Open for PulseAudioSink { AudioFormat::S24 => pulse::sample::Format::S24_32le, AudioFormat::S24_3 => pulse::sample::Format::S24le, AudioFormat::S16 => pulse::sample::Format::S16le, + _ => { + unimplemented!("PulseAudio currently does not support {:?} output", format) + } }; let ss = pulse::sample::Spec { diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 52e9bc91..2951560a 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -178,12 +178,16 @@ impl Sink for RodioSink { let samples = packet.samples(); match self.format { AudioFormat::F32 => { - let source = - rodio::buffer::SamplesBuffer::new(NUM_CHANNELS as u16, SAMPLE_RATE, samples); + let samples_f32: &[f32] = &converter.f64_to_f32(samples); + let source = rodio::buffer::SamplesBuffer::new( + NUM_CHANNELS as u16, + SAMPLE_RATE, + samples_f32, + ); self.rodio_sink.append(source); } AudioFormat::S16 => { - let samples_s16: &[i16] = &converter.f32_to_s16(samples); + let samples_s16: &[i16] = &converter.f64_to_s16(samples); let source = rodio::buffer::SamplesBuffer::new( NUM_CHANNELS as u16, SAMPLE_RATE, diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index ab7c7ecc..d07e562f 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -94,16 +94,17 @@ impl Sink for SdlSink { let samples = packet.samples(); match self { Self::F32(queue) => { + let samples_f32: &[f32] = &converter.f64_to_f32(samples); drain_sink!(queue, AudioFormat::F32.size()); - queue.queue(samples) + queue.queue(samples_f32) } Self::S32(queue) => { - let samples_s32: &[i32] = &converter.f32_to_s32(samples); + let samples_s32: &[i32] = &converter.f64_to_s32(samples); drain_sink!(queue, AudioFormat::S32.size()); queue.queue(samples_s32) } Self::S16(queue) => { - let samples_s16: &[i16] = &converter.f32_to_s16(samples); + let samples_s16: &[i16] = &converter.f64_to_s16(samples); drain_sink!(queue, AudioFormat::S16.size()); queue.queue(samples_s16) } diff --git a/playback/src/config.rs b/playback/src/config.rs index 4b9a74f0..a2e1c6c7 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -33,6 +33,7 @@ impl Default for Bitrate { #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] pub enum AudioFormat { + F64, F32, S32, S24, @@ -44,6 +45,7 @@ impl TryFrom<&String> for AudioFormat { type Error = (); fn try_from(s: &String) -> Result { match s.to_uppercase().as_str() { + "F64" => Ok(Self::F64), "F32" => Ok(Self::F32), "S32" => Ok(Self::S32), "S24" => Ok(Self::S24), @@ -65,6 +67,8 @@ impl AudioFormat { #[allow(dead_code)] pub fn size(&self) -> usize { match self { + Self::F64 => mem::size_of::(), + Self::F32 => mem::size_of::(), Self::S24_3 => mem::size_of::(), Self::S16 => mem::size_of::(), _ => mem::size_of::(), // S32 and S24 are both stored in i32 @@ -127,11 +131,11 @@ pub struct PlayerConfig { 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_knee: f32, + pub normalisation_pregain: f64, + pub normalisation_threshold: f64, + pub normalisation_attack: f64, + pub normalisation_release: f64, + pub normalisation_knee: f64, // pass function pointers so they can be lazily instantiated *after* spawning a thread // (thereby circumventing Send bounds that they might not satisfy) @@ -160,10 +164,10 @@ impl Default for PlayerConfig { // fields are intended for volume control range in dB #[derive(Clone, Copy, Debug)] pub enum VolumeCtrl { - Cubic(f32), + Cubic(f64), Fixed, Linear, - Log(f32), + Log(f64), } impl FromStr for VolumeCtrl { @@ -183,9 +187,9 @@ impl VolumeCtrl { pub const MAX_VOLUME: u16 = std::u16::MAX; // Taken from: https://www.dr-lex.be/info-stuff/volumecontrols.html - pub const DEFAULT_DB_RANGE: f32 = 60.0; + pub const DEFAULT_DB_RANGE: f64 = 60.0; - pub fn from_str_with_range(s: &str, db_range: f32) -> Result::Err> { + pub fn from_str_with_range(s: &str, db_range: f64) -> Result::Err> { use self::VolumeCtrl::*; match s.to_lowercase().as_ref() { "cubic" => Ok(Cubic(db_range)), diff --git a/playback/src/convert.rs b/playback/src/convert.rs index 91fa0e96..ddeff2fc 100644 --- a/playback/src/convert.rs +++ b/playback/src/convert.rs @@ -30,8 +30,11 @@ impl Converter { } } - // Denormalize and dither - pub fn scale(&mut self, sample: f32, factor: i64) -> f32 { + const SCALE_S32: f64 = 2147483648.; + const SCALE_S24: f64 = 8388608.; + const SCALE_S16: f64 = 32768.; + + pub fn scale(&mut self, sample: f64, factor: f64) -> f64 { let dither = match self.ditherer { Some(ref mut d) => d.noise(), None => 0.0, @@ -39,12 +42,12 @@ impl Converter { // From the many float to int conversion methods available, match what // the reference Vorbis implementation uses: sample * 32768 (for 16 bit) - let int_value = sample * factor as f32 + dither; + let int_value = sample * factor + dither; // Casting float to integer rounds towards zero by default, i.e. it // truncates, and that generates larger error than rounding to nearest. // Absolute lowest error is gained from rounding ties to even. - math::round::half_to_even(int_value.into(), 0) as f32 + math::round::half_to_even(int_value, 0) } // Special case for samples packed in a word of greater bit depth (e.g. @@ -52,12 +55,12 @@ impl Converter { // byte is zero. Otherwise, dithering may cause an overflow. This is not // necessary for other formats, because casting to integer will saturate // to the bounds of the primitive. - pub fn clamping_scale(&mut self, sample: f32, factor: i64) -> f32 { + pub fn clamping_scale(&mut self, sample: f64, factor: f64) -> f64 { let int_value = self.scale(sample, factor); // In two's complement, there are more negative than positive values. - let min = -factor as f32; - let max = (factor - 1) as f32; + let min = -factor; + let max = factor - 1.0; if int_value < min { return min; @@ -67,38 +70,42 @@ impl Converter { int_value } - pub fn f32_to_s32(&mut self, samples: &[f32]) -> Vec { + pub fn f64_to_f32(&mut self, samples: &[f64]) -> Vec { + samples.iter().map(|sample| *sample as f32).collect() + } + + pub fn f64_to_s32(&mut self, samples: &[f64]) -> Vec { samples .iter() - .map(|sample| self.scale(*sample, 0x80000000) as i32) + .map(|sample| self.scale(*sample, Self::SCALE_S32) as i32) .collect() } // S24 is 24-bit PCM packed in an upper 32-bit word - pub fn f32_to_s24(&mut self, samples: &[f32]) -> Vec { + pub fn f64_to_s24(&mut self, samples: &[f64]) -> Vec { samples .iter() - .map(|sample| self.clamping_scale(*sample, 0x800000) as i32) + .map(|sample| self.clamping_scale(*sample, Self::SCALE_S24) as i32) .collect() } // S24_3 is 24-bit PCM in a 3-byte array - pub fn f32_to_s24_3(&mut self, samples: &[f32]) -> Vec { + pub fn f64_to_s24_3(&mut self, samples: &[f64]) -> Vec { samples .iter() .map(|sample| { // Not as DRY as calling f32_to_s24 first, but this saves iterating // over all samples twice. - let int_value = self.clamping_scale(*sample, 0x800000) as i32; + let int_value = self.clamping_scale(*sample, Self::SCALE_S24) as i32; i24::from_s24(int_value) }) .collect() } - pub fn f32_to_s16(&mut self, samples: &[f32]) -> Vec { + pub fn f64_to_s16(&mut self, samples: &[f64]) -> Vec { samples .iter() - .map(|sample| self.scale(*sample, 0x8000) as i16) + .map(|sample| self.scale(*sample, Self::SCALE_S16) as i16) .collect() } } diff --git a/playback/src/decoder/lewton_decoder.rs b/playback/src/decoder/lewton_decoder.rs index 528d9344..64a49e57 100644 --- a/playback/src/decoder/lewton_decoder.rs +++ b/playback/src/decoder/lewton_decoder.rs @@ -1,6 +1,7 @@ use super::{AudioDecoder, AudioError, AudioPacket}; use lewton::inside_ogg::OggStreamReader; +use lewton::samples::InterleavedSamples; use std::error; use std::fmt; @@ -35,11 +36,8 @@ where use lewton::OggReadError::NoCapturePatternFound; use lewton::VorbisError::{BadAudio, OggError}; loop { - match self - .0 - .read_dec_packet_generic::>() - { - Ok(Some(packet)) => return Ok(Some(AudioPacket::Samples(packet.samples))), + match self.0.read_dec_packet_generic::>() { + Ok(Some(packet)) => return Ok(Some(AudioPacket::samples_from_f32(packet.samples))), Ok(None) => return Ok(None), Err(BadAudio(AudioIsHeader)) => (), diff --git a/playback/src/decoder/mod.rs b/playback/src/decoder/mod.rs index 46c39212..9641e8b3 100644 --- a/playback/src/decoder/mod.rs +++ b/playback/src/decoder/mod.rs @@ -7,12 +7,17 @@ mod passthrough_decoder; pub use passthrough_decoder::{PassthroughDecoder, PassthroughError}; pub enum AudioPacket { - Samples(Vec), + Samples(Vec), OggData(Vec), } impl AudioPacket { - pub fn samples(&self) -> &[f32] { + pub fn samples_from_f32(f32_samples: Vec) -> Self { + let f64_samples = f32_samples.iter().map(|sample| *sample as f64).collect(); + AudioPacket::Samples(f64_samples) + } + + pub fn samples(&self) -> &[f64] { match self { AudioPacket::Samples(s) => s, AudioPacket::OggData(_) => panic!("can't return OggData on samples"), diff --git a/playback/src/dither.rs b/playback/src/dither.rs index 972cca2d..63447ce8 100644 --- a/playback/src/dither.rs +++ b/playback/src/dither.rs @@ -32,7 +32,7 @@ pub trait Ditherer { where Self: Sized; fn name(&self) -> &'static str; - fn noise(&mut self) -> f32; + fn noise(&mut self) -> f64; } impl fmt::Display for dyn Ditherer { @@ -48,7 +48,7 @@ impl fmt::Display for dyn Ditherer { pub struct TriangularDitherer { cached_rng: ThreadRng, - distribution: Triangular, + distribution: Triangular, } impl Ditherer for TriangularDitherer { @@ -64,14 +64,14 @@ impl Ditherer for TriangularDitherer { "Triangular" } - fn noise(&mut self) -> f32 { + fn noise(&mut self) -> f64 { self.distribution.sample(&mut self.cached_rng) } } pub struct GaussianDitherer { cached_rng: ThreadRng, - distribution: Normal, + distribution: Normal, } impl Ditherer for GaussianDitherer { @@ -87,16 +87,16 @@ impl Ditherer for GaussianDitherer { "Gaussian" } - fn noise(&mut self) -> f32 { + fn noise(&mut self) -> f64 { self.distribution.sample(&mut self.cached_rng) } } pub struct HighPassDitherer { active_channel: usize, - previous_noises: [f32; NUM_CHANNELS], + previous_noises: [f64; NUM_CHANNELS], cached_rng: ThreadRng, - distribution: Uniform, + distribution: Uniform, } impl Ditherer for HighPassDitherer { @@ -113,7 +113,7 @@ impl Ditherer for HighPassDitherer { "Triangular, High Passed" } - fn noise(&mut self) -> f32 { + fn noise(&mut self) -> f64 { let new_noise = self.distribution.sample(&mut self.cached_rng); let high_passed_noise = new_noise - self.previous_noises[self.active_channel]; self.previous_noises[self.active_channel] = new_noise; diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index 62009184..fb6853bb 100644 --- a/playback/src/mixer/alsamixer.rs +++ b/playback/src/mixer/alsamixer.rs @@ -15,9 +15,9 @@ pub struct AlsaMixer { min: i64, max: i64, range: i64, - min_db: f32, - max_db: f32, - db_range: f32, + min_db: f64, + max_db: f64, + db_range: f64, has_switch: bool, is_softvol: bool, use_linear_in_db: bool, @@ -101,9 +101,9 @@ impl Mixer for AlsaMixer { (min_millibel, max_millibel) }; - let min_db = min_millibel.to_db(); - let max_db = max_millibel.to_db(); - let db_range = f32::abs(max_db - min_db); + let min_db = min_millibel.to_db() as f64; + let max_db = max_millibel.to_db() as f64; + let db_range = f64::abs(max_db - min_db); // Synchronize the volume control dB range with the mixer control, // unless it was already set with a command line option. @@ -157,17 +157,17 @@ impl Mixer for AlsaMixer { let raw_volume = simple_element .get_playback_volume(SelemChannelId::mono()) .expect("Could not get raw Alsa volume"); - - raw_volume as f32 / self.range as f32 - self.min as f32 + raw_volume as f64 / self.range as f64 - self.min as f64 } else { let db_volume = simple_element .get_playback_vol_db(SelemChannelId::mono()) .expect("Could not get Alsa dB volume") - .to_db(); + .to_db() as f64; if self.use_linear_in_db { (db_volume - self.min_db) / self.db_range - } else if f32::abs(db_volume - SND_CTL_TLV_DB_GAIN_MUTE.to_db()) <= f32::EPSILON { + } else if f64::abs(db_volume - SND_CTL_TLV_DB_GAIN_MUTE.to_db() as f64) <= f64::EPSILON + { 0.0 } else { db_to_ratio(db_volume - self.max_db) @@ -216,7 +216,7 @@ impl Mixer for AlsaMixer { } if self.is_softvol { - let scaled_volume = (self.min as f32 + mapped_volume * self.range as f32) as i64; + let scaled_volume = (self.min as f64 + mapped_volume * self.range as f64) as i64; debug!("Setting Alsa raw volume to {}", scaled_volume); simple_element .set_playback_volume_all(scaled_volume) @@ -228,14 +228,14 @@ impl Mixer for AlsaMixer { self.min_db + mapped_volume * self.db_range } else if volume == 0 { // prevent ratio_to_db(0.0) from returning -inf - SND_CTL_TLV_DB_GAIN_MUTE.to_db() + SND_CTL_TLV_DB_GAIN_MUTE.to_db() as f64 } else { ratio_to_db(mapped_volume) + self.max_db }; debug!("Setting Alsa volume to {:.2} dB", db_volume); simple_element - .set_playback_db_all(MilliBel::from_db(db_volume), Round::Floor) + .set_playback_db_all(MilliBel::from_db(db_volume as f32), Round::Floor) .expect("Could not set Alsa dB volume"); } } diff --git a/playback/src/mixer/mappings.rs b/playback/src/mixer/mappings.rs index d47744d3..04cef439 100644 --- a/playback/src/mixer/mappings.rs +++ b/playback/src/mixer/mappings.rs @@ -2,16 +2,16 @@ use super::VolumeCtrl; use crate::player::db_to_ratio; pub trait MappedCtrl { - fn to_mapped(&self, volume: u16) -> f32; - fn from_mapped(&self, mapped_volume: f32) -> u16; + fn to_mapped(&self, volume: u16) -> f64; + fn from_mapped(&self, mapped_volume: f64) -> u16; - fn db_range(&self) -> f32; - fn set_db_range(&mut self, new_db_range: f32); + fn db_range(&self) -> f64; + fn set_db_range(&mut self, new_db_range: f64); fn range_ok(&self) -> bool; } impl MappedCtrl for VolumeCtrl { - fn to_mapped(&self, volume: u16) -> f32 { + fn to_mapped(&self, volume: u16) -> f64 { // More than just an optimization, this ensures that zero volume is // really mute (both the log and cubic equations would otherwise not // reach zero). @@ -22,7 +22,7 @@ impl MappedCtrl for VolumeCtrl { return 1.0; } - let normalized_volume = volume as f32 / Self::MAX_VOLUME as f32; + let normalized_volume = volume as f64 / Self::MAX_VOLUME as f64; let mapped_volume = if self.range_ok() { match *self { Self::Cubic(db_range) => { @@ -49,13 +49,13 @@ impl MappedCtrl for VolumeCtrl { mapped_volume } - fn from_mapped(&self, mapped_volume: f32) -> u16 { + fn from_mapped(&self, mapped_volume: f64) -> u16 { // More than just an optimization, this ensures that zero mapped volume // is unmapped to non-negative real numbers (otherwise the log and cubic // equations would respectively return -inf and -1/9.) - if f32::abs(mapped_volume - 0.0) <= f32::EPSILON { + if f64::abs(mapped_volume - 0.0) <= f64::EPSILON { return 0; - } else if f32::abs(mapped_volume - 1.0) <= f32::EPSILON { + } else if f64::abs(mapped_volume - 1.0) <= f64::EPSILON { return Self::MAX_VOLUME; } @@ -74,10 +74,10 @@ impl MappedCtrl for VolumeCtrl { mapped_volume }; - (unmapped_volume * Self::MAX_VOLUME as f32) as u16 + (unmapped_volume * Self::MAX_VOLUME as f64) as u16 } - fn db_range(&self) -> f32 { + fn db_range(&self) -> f64 { match *self { Self::Fixed => 0.0, Self::Linear => Self::DEFAULT_DB_RANGE, // arbitrary, could be anything > 0 @@ -85,7 +85,7 @@ impl MappedCtrl for VolumeCtrl { } } - fn set_db_range(&mut self, new_db_range: f32) { + fn set_db_range(&mut self, new_db_range: f64) { match self { Self::Cubic(ref mut db_range) | Self::Log(ref mut db_range) => *db_range = new_db_range, _ => error!("Invalid to set dB range for volume control type {:?}", self), @@ -100,8 +100,8 @@ impl MappedCtrl for VolumeCtrl { } pub trait VolumeMapping { - fn linear_to_mapped(unmapped_volume: f32, db_range: f32) -> f32; - fn mapped_to_linear(mapped_volume: f32, db_range: f32) -> f32; + fn linear_to_mapped(unmapped_volume: f64, db_range: f64) -> f64; + fn mapped_to_linear(mapped_volume: f64, db_range: f64) -> f64; } // Volume conversion taken from: https://www.dr-lex.be/info-stuff/volumecontrols.html#ideal2 @@ -110,21 +110,21 @@ pub trait VolumeMapping { // mapping results in a near linear loudness experience with the listener. pub struct LogMapping {} impl VolumeMapping for LogMapping { - fn linear_to_mapped(normalized_volume: f32, db_range: f32) -> f32 { + fn linear_to_mapped(normalized_volume: f64, db_range: f64) -> f64 { let (db_ratio, ideal_factor) = Self::coefficients(db_range); - f32::exp(ideal_factor * normalized_volume) / db_ratio + f64::exp(ideal_factor * normalized_volume) / db_ratio } - fn mapped_to_linear(mapped_volume: f32, db_range: f32) -> f32 { + fn mapped_to_linear(mapped_volume: f64, db_range: f64) -> f64 { let (db_ratio, ideal_factor) = Self::coefficients(db_range); - f32::ln(db_ratio * mapped_volume) / ideal_factor + f64::ln(db_ratio * mapped_volume) / ideal_factor } } impl LogMapping { - fn coefficients(db_range: f32) -> (f32, f32) { + fn coefficients(db_range: f64) -> (f64, f64) { let db_ratio = db_to_ratio(db_range); - let ideal_factor = f32::ln(db_ratio); + let ideal_factor = f64::ln(db_ratio); (db_ratio, ideal_factor) } } @@ -143,21 +143,21 @@ impl LogMapping { // logarithmic mapping, then use that volume control. pub struct CubicMapping {} impl VolumeMapping for CubicMapping { - fn linear_to_mapped(normalized_volume: f32, db_range: f32) -> f32 { + fn linear_to_mapped(normalized_volume: f64, db_range: f64) -> f64 { let min_norm = Self::min_norm(db_range); - f32::powi(normalized_volume * (1.0 - min_norm) + min_norm, 3) + f64::powi(normalized_volume * (1.0 - min_norm) + min_norm, 3) } - fn mapped_to_linear(mapped_volume: f32, db_range: f32) -> f32 { + fn mapped_to_linear(mapped_volume: f64, db_range: f64) -> f64 { let min_norm = Self::min_norm(db_range); (mapped_volume.powf(1.0 / 3.0) - min_norm) / (1.0 - min_norm) } } impl CubicMapping { - fn min_norm(db_range: f32) -> f32 { + fn min_norm(db_range: f64) -> f64 { // Note that this 60.0 is unrelated to DEFAULT_DB_RANGE. // Instead, it's the cubic voltage to dB ratio. - f32::powf(10.0, -1.0 * db_range / 60.0) + f64::powf(10.0, -1.0 * db_range / 60.0) } } diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs index 3c3bed2e..aaecd779 100644 --- a/playback/src/mixer/mod.rs +++ b/playback/src/mixer/mod.rs @@ -17,7 +17,7 @@ pub trait Mixer: Send { } pub trait AudioFilter { - fn modify_stream(&self, data: &mut [f32]); + fn modify_stream(&self, data: &mut [f64]); } pub mod softmixer; diff --git a/playback/src/mixer/softmixer.rs b/playback/src/mixer/softmixer.rs index 0be161ad..d1c6eb20 100644 --- a/playback/src/mixer/softmixer.rs +++ b/playback/src/mixer/softmixer.rs @@ -1,4 +1,4 @@ -use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use super::AudioFilter; @@ -7,9 +7,9 @@ use super::{Mixer, MixerConfig}; #[derive(Clone)] pub struct SoftMixer { - // There is no AtomicF32, so we store the f32 as bits in a u32 field. - // It's much faster than a Mutex. - volume: Arc, + // There is no AtomicF64, so we store the f64 as bits in a u64 field. + // It's much faster than a Mutex. + volume: Arc, volume_ctrl: VolumeCtrl, } @@ -19,13 +19,13 @@ impl Mixer for SoftMixer { info!("Mixing with softvol and volume control: {:?}", volume_ctrl); Self { - volume: Arc::new(AtomicU32::new(f32::to_bits(0.5))), + volume: Arc::new(AtomicU64::new(f64::to_bits(0.5))), volume_ctrl, } } fn volume(&self) -> u16 { - let mapped_volume = f32::from_bits(self.volume.load(Ordering::Relaxed)); + let mapped_volume = f64::from_bits(self.volume.load(Ordering::Relaxed)); self.volume_ctrl.from_mapped(mapped_volume) } @@ -43,15 +43,15 @@ impl Mixer for SoftMixer { } struct SoftVolumeApplier { - volume: Arc, + volume: Arc, } impl AudioFilter for SoftVolumeApplier { - fn modify_stream(&self, data: &mut [f32]) { - let volume = f32::from_bits(self.volume.load(Ordering::Relaxed)); + fn modify_stream(&self, data: &mut [f64]) { + let volume = f64::from_bits(self.volume.load(Ordering::Relaxed)); if volume < 1.0 { for x in data.iter_mut() { - *x = (*x as f64 * volume as f64) as f32; + *x *= volume; } } } diff --git a/playback/src/player.rs b/playback/src/player.rs index 254908eb..f5af69f8 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -31,7 +31,7 @@ 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; -pub const DB_VOLTAGE_RATIO: f32 = 20.0; +pub const DB_VOLTAGE_RATIO: f64 = 20.0; pub struct Player { commands: Option>, @@ -65,9 +65,9 @@ struct PlayerInternal { limiter_active: bool, limiter_attack_counter: u32, limiter_release_counter: u32, - limiter_peak_sample: f32, - limiter_factor: f32, - limiter_strength: f32, + limiter_peak_sample: f64, + limiter_factor: f64, + limiter_strength: f64, } enum PlayerCommand { @@ -198,11 +198,11 @@ impl PlayerEvent { pub type PlayerEventChannel = mpsc::UnboundedReceiver; -pub fn db_to_ratio(db: f32) -> f32 { - f32::powf(10.0, db / DB_VOLTAGE_RATIO) +pub fn db_to_ratio(db: f64) -> f64 { + f64::powf(10.0, db / DB_VOLTAGE_RATIO) } -pub fn ratio_to_db(ratio: f32) -> f32 { +pub fn ratio_to_db(ratio: f64) -> f64 { ratio.log10() * DB_VOLTAGE_RATIO } @@ -234,7 +234,7 @@ impl NormalisationData { Ok(r) } - fn get_factor(config: &PlayerConfig, data: NormalisationData) -> f32 { + fn get_factor(config: &PlayerConfig, data: NormalisationData) -> f64 { if !config.normalisation { return 1.0; } @@ -244,11 +244,11 @@ impl NormalisationData { NormalisationType::Track => [data.track_gain_db, data.track_peak], }; - let normalisation_power = gain_db + config.normalisation_pregain; + let normalisation_power = gain_db as f64 + config.normalisation_pregain; let mut normalisation_factor = db_to_ratio(normalisation_power); - if normalisation_factor * gain_peak > config.normalisation_threshold { - let limited_normalisation_factor = config.normalisation_threshold / gain_peak; + if normalisation_factor * gain_peak as f64 > config.normalisation_threshold { + let limited_normalisation_factor = config.normalisation_threshold / gain_peak as f64; let limited_normalisation_power = ratio_to_db(limited_normalisation_factor); if config.normalisation_method == NormalisationMethod::Basic { @@ -267,7 +267,7 @@ impl NormalisationData { debug!("Normalisation Data: {:?}", data); debug!("Normalisation Factor: {:.2}%", normalisation_factor * 100.0); - normalisation_factor + normalisation_factor as f64 } } @@ -430,7 +430,7 @@ impl Drop for Player { struct PlayerLoadedTrackData { decoder: Decoder, - normalisation_factor: f32, + normalisation_factor: f64, stream_loader_controller: StreamLoaderController, bytes_per_second: usize, duration_ms: u32, @@ -463,7 +463,7 @@ enum PlayerState { track_id: SpotifyId, play_request_id: u64, decoder: Decoder, - normalisation_factor: f32, + normalisation_factor: f64, stream_loader_controller: StreamLoaderController, bytes_per_second: usize, duration_ms: u32, @@ -474,7 +474,7 @@ enum PlayerState { track_id: SpotifyId, play_request_id: u64, decoder: Decoder, - normalisation_factor: f32, + normalisation_factor: f64, stream_loader_controller: StreamLoaderController, bytes_per_second: usize, duration_ms: u32, @@ -789,7 +789,7 @@ impl PlayerTrackLoader { } Err(_) => { warn!("Unable to extract normalisation data, using default value."); - 1.0_f32 + 1.0 } }; @@ -1178,7 +1178,7 @@ impl PlayerInternal { } } - fn handle_packet(&mut self, packet: Option, normalisation_factor: f32) { + fn handle_packet(&mut self, packet: Option, normalisation_factor: f64) { match packet { Some(mut packet) => { if !packet.is_empty() { @@ -1188,7 +1188,7 @@ impl PlayerInternal { } if self.config.normalisation - && !(f32::abs(normalisation_factor - 1.0) <= f32::EPSILON + && !(f64::abs(normalisation_factor - 1.0) <= f64::EPSILON && self.config.normalisation_method == NormalisationMethod::Basic) { for sample in data.iter_mut() { @@ -1208,10 +1208,10 @@ impl PlayerInternal { { shaped_limiter_strength = 1.0 / (1.0 - + f32::powf( + + f64::powf( shaped_limiter_strength / (1.0 - shaped_limiter_strength), - -1.0 * self.config.normalisation_knee, + -self.config.normalisation_knee, )); } actual_normalisation_factor = @@ -1222,18 +1222,16 @@ impl PlayerInternal { // 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(); + let abs_sample = f64::abs(*sample * normalisation_factor); 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 + as f64 * self.config.normalisation_release) - - self.limiter_release_counter as f32) + - self.limiter_release_counter as f64) / (self.config.normalisation_release / self.config.normalisation_attack)) as u32; @@ -1242,8 +1240,8 @@ impl PlayerInternal { 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.limiter_strength = self.limiter_attack_counter as f64 + / (SAMPLES_PER_SECOND as f64 * self.config.normalisation_attack); if abs_sample > self.limiter_peak_sample { @@ -1259,9 +1257,9 @@ impl PlayerInternal { // start the release by synchronizing with the current // attack limiter strength. self.limiter_release_counter = (((SAMPLES_PER_SECOND - as f32 + as f64 * self.config.normalisation_attack) - - self.limiter_attack_counter as f32) + - self.limiter_attack_counter as f64) * (self.config.normalisation_release / self.config.normalisation_attack)) as u32; @@ -1272,23 +1270,22 @@ impl PlayerInternal { self.limiter_release_counter.saturating_add(1); if self.limiter_release_counter - > (SAMPLES_PER_SECOND as f32 + > (SAMPLES_PER_SECOND as f64 * self.config.normalisation_release) as u32 { self.reset_limiter(); } else { - self.limiter_strength = ((SAMPLES_PER_SECOND as f32 + self.limiter_strength = ((SAMPLES_PER_SECOND as f64 * self.config.normalisation_release) - - self.limiter_release_counter as f32) - / (SAMPLES_PER_SECOND as f32 + - self.limiter_release_counter as f64) + / (SAMPLES_PER_SECOND as f64 * self.config.normalisation_release); } } } - *sample = - (*sample as f64 * actual_normalisation_factor as f64) as f32; + *sample *= actual_normalisation_factor; // Extremely sharp attacks, however unlikely, *may* still clip and provide // undefined results, so strictly enforce output within [-1.0, 1.0]. diff --git a/src/main.rs b/src/main.rs index 81988136..4f8b8f1b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,7 +34,7 @@ use std::{ pin::Pin, }; -const MILLIS: f32 = 1000.0; +const MILLIS: f64 = 1000.0; fn device_id(name: &str) -> String { hex::encode(Sha1::digest(name.as_bytes())) @@ -247,7 +247,7 @@ fn get_setup(args: &[String]) -> Setup { .optopt( "", "format", - "Output format {F32|S32|S24|S24_3|S16}. Defaults to S16.", + "Output format {F64|F32|S32|S24|S24_3|S16}. Defaults to S16.", "FORMAT", ) .optopt( @@ -435,7 +435,7 @@ fn get_setup(args: &[String]) -> Setup { .unwrap_or_else(|| String::from("PCM")); let mut volume_range = matches .opt_str("volume-range") - .map(|range| range.parse::().unwrap()) + .map(|range| range.parse::().unwrap()) .unwrap_or_else(|| match mixer_name.as_ref().map(AsRef::as_ref) { Some("alsa") => 0.0, // let Alsa query the control _ => VolumeCtrl::DEFAULT_DB_RANGE, @@ -609,29 +609,29 @@ fn get_setup(args: &[String]) -> Setup { .unwrap_or_default(); let normalisation_pregain = matches .opt_str("normalisation-pregain") - .map(|pregain| pregain.parse::().expect("Invalid pregain float value")) + .map(|pregain| pregain.parse::().expect("Invalid pregain float value")) .unwrap_or(PlayerConfig::default().normalisation_pregain); let normalisation_threshold = matches .opt_str("normalisation-threshold") .map(|threshold| { db_to_ratio( threshold - .parse::() + .parse::() .expect("Invalid threshold float value"), ) }) .unwrap_or(PlayerConfig::default().normalisation_threshold); let normalisation_attack = matches .opt_str("normalisation-attack") - .map(|attack| attack.parse::().expect("Invalid attack float value") / MILLIS) + .map(|attack| attack.parse::().expect("Invalid attack float value") / MILLIS) .unwrap_or(PlayerConfig::default().normalisation_attack); let normalisation_release = matches .opt_str("normalisation-release") - .map(|release| release.parse::().expect("Invalid release float value") / MILLIS) + .map(|release| release.parse::().expect("Invalid release float value") / MILLIS) .unwrap_or(PlayerConfig::default().normalisation_release); let normalisation_knee = matches .opt_str("normalisation-knee") - .map(|knee| knee.parse::().expect("Invalid knee float value")) + .map(|knee| knee.parse::().expect("Invalid knee float value")) .unwrap_or(PlayerConfig::default().normalisation_knee); let ditherer_name = matches.opt_str("dither"); @@ -640,7 +640,7 @@ fn get_setup(args: &[String]) -> Setup { Some("none") => None, // explicitly set on command line Some(_) => { - if format == AudioFormat::F32 { + if format == AudioFormat::F64 || format == AudioFormat::F32 { unimplemented!("Dithering is not available on format {:?}", format); } Some(dither::find_ditherer(ditherer_name).expect("Invalid ditherer"))