Store and process samples in 64 bit (#773)

This commit is contained in:
Roderick van Domburg 2021-05-30 20:09:39 +02:00 committed by GitHub
parent 8062bd2518
commit fe2d5ca7c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 177 additions and 149 deletions

View file

@ -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 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] 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] `alsamixer`: support for querying dB range from Alsa softvol
- [playback] Add `--format F64` (supported by Alsa and GStreamer only)
### Changed ### 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) - [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, playback] Moved volume controls from `librespot-connect` to `librespot-playback` crate
- [connect] Synchronize player volume with mixer volume on playback - [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] 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] Normalize volumes to `[0.0..1.0]` instead of `[0..65535]` for greater precision and performance (breaking)
- [playback] `alsamixer`: complete rewrite (breaking) - [playback] `alsamixer`: complete rewrite (breaking)

View file

@ -41,6 +41,7 @@ fn list_outputs() {
fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box<Error>> { fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box<Error>> {
let pcm = PCM::new(dev_name, Direction::Playback, false)?; let pcm = PCM::new(dev_name, Direction::Playback, false)?;
let alsa_format = match format { let alsa_format = match format {
AudioFormat::F64 => Format::float64(),
AudioFormat::F32 => Format::float(), AudioFormat::F32 => Format::float(),
AudioFormat::S32 => Format::s32(), AudioFormat::S32 => Format::s32(),
AudioFormat::S24 => Format::s24(), AudioFormat::S24 => Format::s24(),

View file

@ -70,9 +70,10 @@ impl Open for JackSink {
} }
impl Sink for JackSink { impl Sink for JackSink {
fn write(&mut self, packet: &AudioPacket, _: &mut Converter) -> io::Result<()> { fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> {
for s in packet.samples().iter() { let samples_f32: &[f32] = &converter.f64_to_f32(packet.samples());
let res = self.send.send(*s); for sample in samples_f32.iter() {
let res = self.send.send(*sample);
if res.is_err() { if res.is_err() {
error!("cannot write to channel"); error!("cannot write to channel");
} }

View file

@ -35,21 +35,25 @@ macro_rules! sink_as_bytes {
use zerocopy::AsBytes; use zerocopy::AsBytes;
match packet { match packet {
AudioPacket::Samples(samples) => match self.format { 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 => { 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()) self.write_bytes(samples_s32.as_bytes())
} }
AudioFormat::S24 => { 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()) self.write_bytes(samples_s24.as_bytes())
} }
AudioFormat::S24_3 => { 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()) self.write_bytes(samples_s24_3.as_bytes())
} }
AudioFormat::S16 => { 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()) self.write_bytes(samples_s16.as_bytes())
} }
}, },

View file

@ -151,14 +151,15 @@ impl<'a> Sink for PortAudioSink<'a> {
let samples = packet.samples(); let samples = packet.samples();
let result = match self { let result = match self {
Self::F32(stream, _parameters) => { 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) => { 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) write_sink!(ref mut stream, samples_s32)
} }
Self::S16(stream, _parameters) => { 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) write_sink!(ref mut stream, samples_s16)
} }
}; };

View file

@ -28,6 +28,9 @@ impl Open for PulseAudioSink {
AudioFormat::S24 => pulse::sample::Format::S24_32le, AudioFormat::S24 => pulse::sample::Format::S24_32le,
AudioFormat::S24_3 => pulse::sample::Format::S24le, AudioFormat::S24_3 => pulse::sample::Format::S24le,
AudioFormat::S16 => pulse::sample::Format::S16le, AudioFormat::S16 => pulse::sample::Format::S16le,
_ => {
unimplemented!("PulseAudio currently does not support {:?} output", format)
}
}; };
let ss = pulse::sample::Spec { let ss = pulse::sample::Spec {

View file

@ -178,12 +178,16 @@ impl Sink for RodioSink {
let samples = packet.samples(); let samples = packet.samples();
match self.format { match self.format {
AudioFormat::F32 => { AudioFormat::F32 => {
let source = let samples_f32: &[f32] = &converter.f64_to_f32(samples);
rodio::buffer::SamplesBuffer::new(NUM_CHANNELS as u16, SAMPLE_RATE, samples); let source = rodio::buffer::SamplesBuffer::new(
NUM_CHANNELS as u16,
SAMPLE_RATE,
samples_f32,
);
self.rodio_sink.append(source); self.rodio_sink.append(source);
} }
AudioFormat::S16 => { 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( let source = rodio::buffer::SamplesBuffer::new(
NUM_CHANNELS as u16, NUM_CHANNELS as u16,
SAMPLE_RATE, SAMPLE_RATE,

View file

@ -94,16 +94,17 @@ impl Sink for SdlSink {
let samples = packet.samples(); let samples = packet.samples();
match self { match self {
Self::F32(queue) => { Self::F32(queue) => {
let samples_f32: &[f32] = &converter.f64_to_f32(samples);
drain_sink!(queue, AudioFormat::F32.size()); drain_sink!(queue, AudioFormat::F32.size());
queue.queue(samples) queue.queue(samples_f32)
} }
Self::S32(queue) => { 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()); drain_sink!(queue, AudioFormat::S32.size());
queue.queue(samples_s32) queue.queue(samples_s32)
} }
Self::S16(queue) => { 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()); drain_sink!(queue, AudioFormat::S16.size());
queue.queue(samples_s16) queue.queue(samples_s16)
} }

View file

@ -33,6 +33,7 @@ impl Default for Bitrate {
#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)]
pub enum AudioFormat { pub enum AudioFormat {
F64,
F32, F32,
S32, S32,
S24, S24,
@ -44,6 +45,7 @@ impl TryFrom<&String> for AudioFormat {
type Error = (); type Error = ();
fn try_from(s: &String) -> Result<Self, Self::Error> { fn try_from(s: &String) -> Result<Self, Self::Error> {
match s.to_uppercase().as_str() { match s.to_uppercase().as_str() {
"F64" => Ok(Self::F64),
"F32" => Ok(Self::F32), "F32" => Ok(Self::F32),
"S32" => Ok(Self::S32), "S32" => Ok(Self::S32),
"S24" => Ok(Self::S24), "S24" => Ok(Self::S24),
@ -65,6 +67,8 @@ impl AudioFormat {
#[allow(dead_code)] #[allow(dead_code)]
pub fn size(&self) -> usize { pub fn size(&self) -> usize {
match self { match self {
Self::F64 => mem::size_of::<f64>(),
Self::F32 => mem::size_of::<f32>(),
Self::S24_3 => mem::size_of::<i24>(), Self::S24_3 => mem::size_of::<i24>(),
Self::S16 => mem::size_of::<i16>(), Self::S16 => mem::size_of::<i16>(),
_ => mem::size_of::<i32>(), // S32 and S24 are both stored in i32 _ => mem::size_of::<i32>(), // S32 and S24 are both stored in i32
@ -127,11 +131,11 @@ pub struct PlayerConfig {
pub normalisation: bool, pub normalisation: bool,
pub normalisation_type: NormalisationType, pub normalisation_type: NormalisationType,
pub normalisation_method: NormalisationMethod, pub normalisation_method: NormalisationMethod,
pub normalisation_pregain: f32, pub normalisation_pregain: f64,
pub normalisation_threshold: f32, pub normalisation_threshold: f64,
pub normalisation_attack: f32, pub normalisation_attack: f64,
pub normalisation_release: f32, pub normalisation_release: f64,
pub normalisation_knee: f32, pub normalisation_knee: f64,
// pass function pointers so they can be lazily instantiated *after* spawning a thread // pass function pointers so they can be lazily instantiated *after* spawning a thread
// (thereby circumventing Send bounds that they might not satisfy) // (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 // fields are intended for volume control range in dB
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum VolumeCtrl { pub enum VolumeCtrl {
Cubic(f32), Cubic(f64),
Fixed, Fixed,
Linear, Linear,
Log(f32), Log(f64),
} }
impl FromStr for VolumeCtrl { impl FromStr for VolumeCtrl {
@ -183,9 +187,9 @@ impl VolumeCtrl {
pub const MAX_VOLUME: u16 = std::u16::MAX; pub const MAX_VOLUME: u16 = std::u16::MAX;
// Taken from: https://www.dr-lex.be/info-stuff/volumecontrols.html // 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<Self, <Self as FromStr>::Err> { pub fn from_str_with_range(s: &str, db_range: f64) -> Result<Self, <Self as FromStr>::Err> {
use self::VolumeCtrl::*; use self::VolumeCtrl::*;
match s.to_lowercase().as_ref() { match s.to_lowercase().as_ref() {
"cubic" => Ok(Cubic(db_range)), "cubic" => Ok(Cubic(db_range)),

View file

@ -30,8 +30,11 @@ impl Converter {
} }
} }
// Denormalize and dither const SCALE_S32: f64 = 2147483648.;
pub fn scale(&mut self, sample: f32, factor: i64) -> f32 { 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 { let dither = match self.ditherer {
Some(ref mut d) => d.noise(), Some(ref mut d) => d.noise(),
None => 0.0, None => 0.0,
@ -39,12 +42,12 @@ impl Converter {
// From the many float to int conversion methods available, match what // From the many float to int conversion methods available, match what
// the reference Vorbis implementation uses: sample * 32768 (for 16 bit) // 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 // Casting float to integer rounds towards zero by default, i.e. it
// truncates, and that generates larger error than rounding to nearest. // truncates, and that generates larger error than rounding to nearest.
// Absolute lowest error is gained from rounding ties to even. // 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. // 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 // byte is zero. Otherwise, dithering may cause an overflow. This is not
// necessary for other formats, because casting to integer will saturate // necessary for other formats, because casting to integer will saturate
// to the bounds of the primitive. // 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); let int_value = self.scale(sample, factor);
// In two's complement, there are more negative than positive values. // In two's complement, there are more negative than positive values.
let min = -factor as f32; let min = -factor;
let max = (factor - 1) as f32; let max = factor - 1.0;
if int_value < min { if int_value < min {
return min; return min;
@ -67,38 +70,42 @@ impl Converter {
int_value int_value
} }
pub fn f32_to_s32(&mut self, samples: &[f32]) -> Vec<i32> { pub fn f64_to_f32(&mut self, samples: &[f64]) -> Vec<f32> {
samples.iter().map(|sample| *sample as f32).collect()
}
pub fn f64_to_s32(&mut self, samples: &[f64]) -> Vec<i32> {
samples samples
.iter() .iter()
.map(|sample| self.scale(*sample, 0x80000000) as i32) .map(|sample| self.scale(*sample, Self::SCALE_S32) as i32)
.collect() .collect()
} }
// S24 is 24-bit PCM packed in an upper 32-bit word // S24 is 24-bit PCM packed in an upper 32-bit word
pub fn f32_to_s24(&mut self, samples: &[f32]) -> Vec<i32> { pub fn f64_to_s24(&mut self, samples: &[f64]) -> Vec<i32> {
samples samples
.iter() .iter()
.map(|sample| self.clamping_scale(*sample, 0x800000) as i32) .map(|sample| self.clamping_scale(*sample, Self::SCALE_S24) as i32)
.collect() .collect()
} }
// S24_3 is 24-bit PCM in a 3-byte array // S24_3 is 24-bit PCM in a 3-byte array
pub fn f32_to_s24_3(&mut self, samples: &[f32]) -> Vec<i24> { pub fn f64_to_s24_3(&mut self, samples: &[f64]) -> Vec<i24> {
samples samples
.iter() .iter()
.map(|sample| { .map(|sample| {
// Not as DRY as calling f32_to_s24 first, but this saves iterating // Not as DRY as calling f32_to_s24 first, but this saves iterating
// over all samples twice. // 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) i24::from_s24(int_value)
}) })
.collect() .collect()
} }
pub fn f32_to_s16(&mut self, samples: &[f32]) -> Vec<i16> { pub fn f64_to_s16(&mut self, samples: &[f64]) -> Vec<i16> {
samples samples
.iter() .iter()
.map(|sample| self.scale(*sample, 0x8000) as i16) .map(|sample| self.scale(*sample, Self::SCALE_S16) as i16)
.collect() .collect()
} }
} }

View file

@ -1,6 +1,7 @@
use super::{AudioDecoder, AudioError, AudioPacket}; use super::{AudioDecoder, AudioError, AudioPacket};
use lewton::inside_ogg::OggStreamReader; use lewton::inside_ogg::OggStreamReader;
use lewton::samples::InterleavedSamples;
use std::error; use std::error;
use std::fmt; use std::fmt;
@ -35,11 +36,8 @@ where
use lewton::OggReadError::NoCapturePatternFound; use lewton::OggReadError::NoCapturePatternFound;
use lewton::VorbisError::{BadAudio, OggError}; use lewton::VorbisError::{BadAudio, OggError};
loop { loop {
match self match self.0.read_dec_packet_generic::<InterleavedSamples<f32>>() {
.0 Ok(Some(packet)) => return Ok(Some(AudioPacket::samples_from_f32(packet.samples))),
.read_dec_packet_generic::<lewton::samples::InterleavedSamples<f32>>()
{
Ok(Some(packet)) => return Ok(Some(AudioPacket::Samples(packet.samples))),
Ok(None) => return Ok(None), Ok(None) => return Ok(None),
Err(BadAudio(AudioIsHeader)) => (), Err(BadAudio(AudioIsHeader)) => (),

View file

@ -7,12 +7,17 @@ mod passthrough_decoder;
pub use passthrough_decoder::{PassthroughDecoder, PassthroughError}; pub use passthrough_decoder::{PassthroughDecoder, PassthroughError};
pub enum AudioPacket { pub enum AudioPacket {
Samples(Vec<f32>), Samples(Vec<f64>),
OggData(Vec<u8>), OggData(Vec<u8>),
} }
impl AudioPacket { impl AudioPacket {
pub fn samples(&self) -> &[f32] { pub fn samples_from_f32(f32_samples: Vec<f32>) -> Self {
let f64_samples = f32_samples.iter().map(|sample| *sample as f64).collect();
AudioPacket::Samples(f64_samples)
}
pub fn samples(&self) -> &[f64] {
match self { match self {
AudioPacket::Samples(s) => s, AudioPacket::Samples(s) => s,
AudioPacket::OggData(_) => panic!("can't return OggData on samples"), AudioPacket::OggData(_) => panic!("can't return OggData on samples"),

View file

@ -32,7 +32,7 @@ pub trait Ditherer {
where where
Self: Sized; Self: Sized;
fn name(&self) -> &'static str; fn name(&self) -> &'static str;
fn noise(&mut self) -> f32; fn noise(&mut self) -> f64;
} }
impl fmt::Display for dyn Ditherer { impl fmt::Display for dyn Ditherer {
@ -48,7 +48,7 @@ impl fmt::Display for dyn Ditherer {
pub struct TriangularDitherer { pub struct TriangularDitherer {
cached_rng: ThreadRng, cached_rng: ThreadRng,
distribution: Triangular<f32>, distribution: Triangular<f64>,
} }
impl Ditherer for TriangularDitherer { impl Ditherer for TriangularDitherer {
@ -64,14 +64,14 @@ impl Ditherer for TriangularDitherer {
"Triangular" "Triangular"
} }
fn noise(&mut self) -> f32 { fn noise(&mut self) -> f64 {
self.distribution.sample(&mut self.cached_rng) self.distribution.sample(&mut self.cached_rng)
} }
} }
pub struct GaussianDitherer { pub struct GaussianDitherer {
cached_rng: ThreadRng, cached_rng: ThreadRng,
distribution: Normal<f32>, distribution: Normal<f64>,
} }
impl Ditherer for GaussianDitherer { impl Ditherer for GaussianDitherer {
@ -87,16 +87,16 @@ impl Ditherer for GaussianDitherer {
"Gaussian" "Gaussian"
} }
fn noise(&mut self) -> f32 { fn noise(&mut self) -> f64 {
self.distribution.sample(&mut self.cached_rng) self.distribution.sample(&mut self.cached_rng)
} }
} }
pub struct HighPassDitherer { pub struct HighPassDitherer {
active_channel: usize, active_channel: usize,
previous_noises: [f32; NUM_CHANNELS], previous_noises: [f64; NUM_CHANNELS],
cached_rng: ThreadRng, cached_rng: ThreadRng,
distribution: Uniform<f32>, distribution: Uniform<f64>,
} }
impl Ditherer for HighPassDitherer { impl Ditherer for HighPassDitherer {
@ -113,7 +113,7 @@ impl Ditherer for HighPassDitherer {
"Triangular, High Passed" "Triangular, High Passed"
} }
fn noise(&mut self) -> f32 { fn noise(&mut self) -> f64 {
let new_noise = self.distribution.sample(&mut self.cached_rng); let new_noise = self.distribution.sample(&mut self.cached_rng);
let high_passed_noise = new_noise - self.previous_noises[self.active_channel]; let high_passed_noise = new_noise - self.previous_noises[self.active_channel];
self.previous_noises[self.active_channel] = new_noise; self.previous_noises[self.active_channel] = new_noise;

View file

@ -15,9 +15,9 @@ pub struct AlsaMixer {
min: i64, min: i64,
max: i64, max: i64,
range: i64, range: i64,
min_db: f32, min_db: f64,
max_db: f32, max_db: f64,
db_range: f32, db_range: f64,
has_switch: bool, has_switch: bool,
is_softvol: bool, is_softvol: bool,
use_linear_in_db: bool, use_linear_in_db: bool,
@ -101,9 +101,9 @@ impl Mixer for AlsaMixer {
(min_millibel, max_millibel) (min_millibel, max_millibel)
}; };
let min_db = min_millibel.to_db(); let min_db = min_millibel.to_db() as f64;
let max_db = max_millibel.to_db(); let max_db = max_millibel.to_db() as f64;
let db_range = f32::abs(max_db - min_db); let db_range = f64::abs(max_db - min_db);
// Synchronize the volume control dB range with the mixer control, // Synchronize the volume control dB range with the mixer control,
// unless it was already set with a command line option. // unless it was already set with a command line option.
@ -157,17 +157,17 @@ impl Mixer for AlsaMixer {
let raw_volume = simple_element let raw_volume = simple_element
.get_playback_volume(SelemChannelId::mono()) .get_playback_volume(SelemChannelId::mono())
.expect("Could not get raw Alsa volume"); .expect("Could not get raw Alsa volume");
raw_volume as f64 / self.range as f64 - self.min as f64
raw_volume as f32 / self.range as f32 - self.min as f32
} else { } else {
let db_volume = simple_element let db_volume = simple_element
.get_playback_vol_db(SelemChannelId::mono()) .get_playback_vol_db(SelemChannelId::mono())
.expect("Could not get Alsa dB volume") .expect("Could not get Alsa dB volume")
.to_db(); .to_db() as f64;
if self.use_linear_in_db { if self.use_linear_in_db {
(db_volume - self.min_db) / self.db_range (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 0.0
} else { } else {
db_to_ratio(db_volume - self.max_db) db_to_ratio(db_volume - self.max_db)
@ -216,7 +216,7 @@ impl Mixer for AlsaMixer {
} }
if self.is_softvol { 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); debug!("Setting Alsa raw volume to {}", scaled_volume);
simple_element simple_element
.set_playback_volume_all(scaled_volume) .set_playback_volume_all(scaled_volume)
@ -228,14 +228,14 @@ impl Mixer for AlsaMixer {
self.min_db + mapped_volume * self.db_range self.min_db + mapped_volume * self.db_range
} else if volume == 0 { } else if volume == 0 {
// prevent ratio_to_db(0.0) from returning -inf // 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 { } else {
ratio_to_db(mapped_volume) + self.max_db ratio_to_db(mapped_volume) + self.max_db
}; };
debug!("Setting Alsa volume to {:.2} dB", db_volume); debug!("Setting Alsa volume to {:.2} dB", db_volume);
simple_element 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"); .expect("Could not set Alsa dB volume");
} }
} }

View file

@ -2,16 +2,16 @@ use super::VolumeCtrl;
use crate::player::db_to_ratio; use crate::player::db_to_ratio;
pub trait MappedCtrl { pub trait MappedCtrl {
fn to_mapped(&self, volume: u16) -> f32; fn to_mapped(&self, volume: u16) -> f64;
fn from_mapped(&self, mapped_volume: f32) -> u16; fn from_mapped(&self, mapped_volume: f64) -> u16;
fn db_range(&self) -> f32; fn db_range(&self) -> f64;
fn set_db_range(&mut self, new_db_range: f32); fn set_db_range(&mut self, new_db_range: f64);
fn range_ok(&self) -> bool; fn range_ok(&self) -> bool;
} }
impl MappedCtrl for VolumeCtrl { 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 // More than just an optimization, this ensures that zero volume is
// really mute (both the log and cubic equations would otherwise not // really mute (both the log and cubic equations would otherwise not
// reach zero). // reach zero).
@ -22,7 +22,7 @@ impl MappedCtrl for VolumeCtrl {
return 1.0; 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() { let mapped_volume = if self.range_ok() {
match *self { match *self {
Self::Cubic(db_range) => { Self::Cubic(db_range) => {
@ -49,13 +49,13 @@ impl MappedCtrl for VolumeCtrl {
mapped_volume 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 // More than just an optimization, this ensures that zero mapped volume
// is unmapped to non-negative real numbers (otherwise the log and cubic // is unmapped to non-negative real numbers (otherwise the log and cubic
// equations would respectively return -inf and -1/9.) // 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; 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; return Self::MAX_VOLUME;
} }
@ -74,10 +74,10 @@ impl MappedCtrl for VolumeCtrl {
mapped_volume 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 { match *self {
Self::Fixed => 0.0, Self::Fixed => 0.0,
Self::Linear => Self::DEFAULT_DB_RANGE, // arbitrary, could be anything > 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 { match self {
Self::Cubic(ref mut db_range) | Self::Log(ref mut db_range) => *db_range = new_db_range, 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), _ => error!("Invalid to set dB range for volume control type {:?}", self),
@ -100,8 +100,8 @@ impl MappedCtrl for VolumeCtrl {
} }
pub trait VolumeMapping { pub trait VolumeMapping {
fn linear_to_mapped(unmapped_volume: f32, db_range: f32) -> f32; fn linear_to_mapped(unmapped_volume: f64, db_range: f64) -> f64;
fn mapped_to_linear(mapped_volume: f32, db_range: f32) -> f32; 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 // 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. // mapping results in a near linear loudness experience with the listener.
pub struct LogMapping {} pub struct LogMapping {}
impl VolumeMapping for 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); 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); 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 { 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 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) (db_ratio, ideal_factor)
} }
} }
@ -143,21 +143,21 @@ impl LogMapping {
// logarithmic mapping, then use that volume control. // logarithmic mapping, then use that volume control.
pub struct CubicMapping {} pub struct CubicMapping {}
impl VolumeMapping for 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); 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); let min_norm = Self::min_norm(db_range);
(mapped_volume.powf(1.0 / 3.0) - min_norm) / (1.0 - min_norm) (mapped_volume.powf(1.0 / 3.0) - min_norm) / (1.0 - min_norm)
} }
} }
impl CubicMapping { 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. // Note that this 60.0 is unrelated to DEFAULT_DB_RANGE.
// Instead, it's the cubic voltage to dB ratio. // 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)
} }
} }

View file

@ -17,7 +17,7 @@ pub trait Mixer: Send {
} }
pub trait AudioFilter { pub trait AudioFilter {
fn modify_stream(&self, data: &mut [f32]); fn modify_stream(&self, data: &mut [f64]);
} }
pub mod softmixer; pub mod softmixer;

View file

@ -1,4 +1,4 @@
use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc; use std::sync::Arc;
use super::AudioFilter; use super::AudioFilter;
@ -7,9 +7,9 @@ use super::{Mixer, MixerConfig};
#[derive(Clone)] #[derive(Clone)]
pub struct SoftMixer { pub struct SoftMixer {
// There is no AtomicF32, so we store the f32 as bits in a u32 field. // There is no AtomicF64, so we store the f64 as bits in a u64 field.
// It's much faster than a Mutex<f32>. // It's much faster than a Mutex<f64>.
volume: Arc<AtomicU32>, volume: Arc<AtomicU64>,
volume_ctrl: VolumeCtrl, volume_ctrl: VolumeCtrl,
} }
@ -19,13 +19,13 @@ impl Mixer for SoftMixer {
info!("Mixing with softvol and volume control: {:?}", volume_ctrl); info!("Mixing with softvol and volume control: {:?}", volume_ctrl);
Self { Self {
volume: Arc::new(AtomicU32::new(f32::to_bits(0.5))), volume: Arc::new(AtomicU64::new(f64::to_bits(0.5))),
volume_ctrl, volume_ctrl,
} }
} }
fn volume(&self) -> u16 { 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) self.volume_ctrl.from_mapped(mapped_volume)
} }
@ -43,15 +43,15 @@ impl Mixer for SoftMixer {
} }
struct SoftVolumeApplier { struct SoftVolumeApplier {
volume: Arc<AtomicU32>, volume: Arc<AtomicU64>,
} }
impl AudioFilter for SoftVolumeApplier { impl AudioFilter for SoftVolumeApplier {
fn modify_stream(&self, data: &mut [f32]) { fn modify_stream(&self, data: &mut [f64]) {
let volume = f32::from_bits(self.volume.load(Ordering::Relaxed)); let volume = f64::from_bits(self.volume.load(Ordering::Relaxed));
if volume < 1.0 { if volume < 1.0 {
for x in data.iter_mut() { for x in data.iter_mut() {
*x = (*x as f64 * volume as f64) as f32; *x *= volume;
} }
} }
} }

View file

@ -31,7 +31,7 @@ pub const NUM_CHANNELS: u8 = 2;
pub const SAMPLES_PER_SECOND: u32 = SAMPLE_RATE as u32 * NUM_CHANNELS as u32; 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 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 { pub struct Player {
commands: Option<mpsc::UnboundedSender<PlayerCommand>>, commands: Option<mpsc::UnboundedSender<PlayerCommand>>,
@ -65,9 +65,9 @@ struct PlayerInternal {
limiter_active: bool, limiter_active: bool,
limiter_attack_counter: u32, limiter_attack_counter: u32,
limiter_release_counter: u32, limiter_release_counter: u32,
limiter_peak_sample: f32, limiter_peak_sample: f64,
limiter_factor: f32, limiter_factor: f64,
limiter_strength: f32, limiter_strength: f64,
} }
enum PlayerCommand { enum PlayerCommand {
@ -198,11 +198,11 @@ impl PlayerEvent {
pub type PlayerEventChannel = mpsc::UnboundedReceiver<PlayerEvent>; pub type PlayerEventChannel = mpsc::UnboundedReceiver<PlayerEvent>;
pub fn db_to_ratio(db: f32) -> f32 { pub fn db_to_ratio(db: f64) -> f64 {
f32::powf(10.0, db / DB_VOLTAGE_RATIO) 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 ratio.log10() * DB_VOLTAGE_RATIO
} }
@ -234,7 +234,7 @@ impl NormalisationData {
Ok(r) Ok(r)
} }
fn get_factor(config: &PlayerConfig, data: NormalisationData) -> f32 { fn get_factor(config: &PlayerConfig, data: NormalisationData) -> f64 {
if !config.normalisation { if !config.normalisation {
return 1.0; return 1.0;
} }
@ -244,11 +244,11 @@ impl NormalisationData {
NormalisationType::Track => [data.track_gain_db, data.track_peak], 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); let mut normalisation_factor = db_to_ratio(normalisation_power);
if normalisation_factor * gain_peak > config.normalisation_threshold { if normalisation_factor * gain_peak as f64 > config.normalisation_threshold {
let limited_normalisation_factor = config.normalisation_threshold / gain_peak; let limited_normalisation_factor = config.normalisation_threshold / gain_peak as f64;
let limited_normalisation_power = ratio_to_db(limited_normalisation_factor); let limited_normalisation_power = ratio_to_db(limited_normalisation_factor);
if config.normalisation_method == NormalisationMethod::Basic { if config.normalisation_method == NormalisationMethod::Basic {
@ -267,7 +267,7 @@ impl NormalisationData {
debug!("Normalisation Data: {:?}", data); debug!("Normalisation Data: {:?}", data);
debug!("Normalisation Factor: {:.2}%", normalisation_factor * 100.0); debug!("Normalisation Factor: {:.2}%", normalisation_factor * 100.0);
normalisation_factor normalisation_factor as f64
} }
} }
@ -430,7 +430,7 @@ impl Drop for Player {
struct PlayerLoadedTrackData { struct PlayerLoadedTrackData {
decoder: Decoder, decoder: Decoder,
normalisation_factor: f32, normalisation_factor: f64,
stream_loader_controller: StreamLoaderController, stream_loader_controller: StreamLoaderController,
bytes_per_second: usize, bytes_per_second: usize,
duration_ms: u32, duration_ms: u32,
@ -463,7 +463,7 @@ enum PlayerState {
track_id: SpotifyId, track_id: SpotifyId,
play_request_id: u64, play_request_id: u64,
decoder: Decoder, decoder: Decoder,
normalisation_factor: f32, normalisation_factor: f64,
stream_loader_controller: StreamLoaderController, stream_loader_controller: StreamLoaderController,
bytes_per_second: usize, bytes_per_second: usize,
duration_ms: u32, duration_ms: u32,
@ -474,7 +474,7 @@ enum PlayerState {
track_id: SpotifyId, track_id: SpotifyId,
play_request_id: u64, play_request_id: u64,
decoder: Decoder, decoder: Decoder,
normalisation_factor: f32, normalisation_factor: f64,
stream_loader_controller: StreamLoaderController, stream_loader_controller: StreamLoaderController,
bytes_per_second: usize, bytes_per_second: usize,
duration_ms: u32, duration_ms: u32,
@ -789,7 +789,7 @@ impl PlayerTrackLoader {
} }
Err(_) => { Err(_) => {
warn!("Unable to extract normalisation data, using default value."); 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<AudioPacket>, normalisation_factor: f32) { fn handle_packet(&mut self, packet: Option<AudioPacket>, normalisation_factor: f64) {
match packet { match packet {
Some(mut packet) => { Some(mut packet) => {
if !packet.is_empty() { if !packet.is_empty() {
@ -1188,7 +1188,7 @@ impl PlayerInternal {
} }
if self.config.normalisation 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) && self.config.normalisation_method == NormalisationMethod::Basic)
{ {
for sample in data.iter_mut() { for sample in data.iter_mut() {
@ -1208,10 +1208,10 @@ impl PlayerInternal {
{ {
shaped_limiter_strength = 1.0 shaped_limiter_strength = 1.0
/ (1.0 / (1.0
+ f32::powf( + f64::powf(
shaped_limiter_strength shaped_limiter_strength
/ (1.0 - shaped_limiter_strength), / (1.0 - shaped_limiter_strength),
-1.0 * self.config.normalisation_knee, -self.config.normalisation_knee,
)); ));
} }
actual_normalisation_factor = actual_normalisation_factor =
@ -1222,18 +1222,16 @@ impl PlayerInternal {
// Always check for peaks, even when the limiter is already active. // Always check for peaks, even when the limiter is already active.
// There may be even higher peaks than we initially targeted. // There may be even higher peaks than we initially targeted.
// Check against the normalisation factor that would be applied normally. // Check against the normalisation factor that would be applied normally.
let abs_sample = let abs_sample = f64::abs(*sample * normalisation_factor);
((*sample as f64 * normalisation_factor as f64) as f32)
.abs();
if abs_sample > self.config.normalisation_threshold { if abs_sample > self.config.normalisation_threshold {
self.limiter_active = true; self.limiter_active = true;
if self.limiter_release_counter > 0 { if self.limiter_release_counter > 0 {
// A peak was encountered while releasing the limiter; // A peak was encountered while releasing the limiter;
// synchronize with the current release limiter strength. // synchronize with the current release limiter strength.
self.limiter_attack_counter = (((SAMPLES_PER_SECOND self.limiter_attack_counter = (((SAMPLES_PER_SECOND
as f32 as f64
* self.config.normalisation_release) * self.config.normalisation_release)
- self.limiter_release_counter as f32) - self.limiter_release_counter as f64)
/ (self.config.normalisation_release / (self.config.normalisation_release
/ self.config.normalisation_attack)) / self.config.normalisation_attack))
as u32; as u32;
@ -1242,8 +1240,8 @@ impl PlayerInternal {
self.limiter_attack_counter = self.limiter_attack_counter =
self.limiter_attack_counter.saturating_add(1); self.limiter_attack_counter.saturating_add(1);
self.limiter_strength = self.limiter_attack_counter as f32 self.limiter_strength = self.limiter_attack_counter as f64
/ (SAMPLES_PER_SECOND as f32 / (SAMPLES_PER_SECOND as f64
* self.config.normalisation_attack); * self.config.normalisation_attack);
if abs_sample > self.limiter_peak_sample { if abs_sample > self.limiter_peak_sample {
@ -1259,9 +1257,9 @@ impl PlayerInternal {
// start the release by synchronizing with the current // start the release by synchronizing with the current
// attack limiter strength. // attack limiter strength.
self.limiter_release_counter = (((SAMPLES_PER_SECOND self.limiter_release_counter = (((SAMPLES_PER_SECOND
as f32 as f64
* self.config.normalisation_attack) * self.config.normalisation_attack)
- self.limiter_attack_counter as f32) - self.limiter_attack_counter as f64)
* (self.config.normalisation_release * (self.config.normalisation_release
/ self.config.normalisation_attack)) / self.config.normalisation_attack))
as u32; as u32;
@ -1272,23 +1270,22 @@ impl PlayerInternal {
self.limiter_release_counter.saturating_add(1); self.limiter_release_counter.saturating_add(1);
if self.limiter_release_counter if self.limiter_release_counter
> (SAMPLES_PER_SECOND as f32 > (SAMPLES_PER_SECOND as f64
* self.config.normalisation_release) * self.config.normalisation_release)
as u32 as u32
{ {
self.reset_limiter(); self.reset_limiter();
} else { } else {
self.limiter_strength = ((SAMPLES_PER_SECOND as f32 self.limiter_strength = ((SAMPLES_PER_SECOND as f64
* self.config.normalisation_release) * self.config.normalisation_release)
- self.limiter_release_counter as f32) - self.limiter_release_counter as f64)
/ (SAMPLES_PER_SECOND as f32 / (SAMPLES_PER_SECOND as f64
* self.config.normalisation_release); * self.config.normalisation_release);
} }
} }
} }
*sample = *sample *= actual_normalisation_factor;
(*sample as f64 * actual_normalisation_factor as f64) as f32;
// Extremely sharp attacks, however unlikely, *may* still clip and provide // Extremely sharp attacks, however unlikely, *may* still clip and provide
// undefined results, so strictly enforce output within [-1.0, 1.0]. // undefined results, so strictly enforce output within [-1.0, 1.0].

View file

@ -34,7 +34,7 @@ use std::{
pin::Pin, pin::Pin,
}; };
const MILLIS: f32 = 1000.0; const MILLIS: f64 = 1000.0;
fn device_id(name: &str) -> String { fn device_id(name: &str) -> String {
hex::encode(Sha1::digest(name.as_bytes())) hex::encode(Sha1::digest(name.as_bytes()))
@ -247,7 +247,7 @@ fn get_setup(args: &[String]) -> Setup {
.optopt( .optopt(
"", "",
"format", "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", "FORMAT",
) )
.optopt( .optopt(
@ -435,7 +435,7 @@ fn get_setup(args: &[String]) -> Setup {
.unwrap_or_else(|| String::from("PCM")); .unwrap_or_else(|| String::from("PCM"));
let mut volume_range = matches let mut volume_range = matches
.opt_str("volume-range") .opt_str("volume-range")
.map(|range| range.parse::<f32>().unwrap()) .map(|range| range.parse::<f64>().unwrap())
.unwrap_or_else(|| match mixer_name.as_ref().map(AsRef::as_ref) { .unwrap_or_else(|| match mixer_name.as_ref().map(AsRef::as_ref) {
Some("alsa") => 0.0, // let Alsa query the control Some("alsa") => 0.0, // let Alsa query the control
_ => VolumeCtrl::DEFAULT_DB_RANGE, _ => VolumeCtrl::DEFAULT_DB_RANGE,
@ -609,29 +609,29 @@ fn get_setup(args: &[String]) -> Setup {
.unwrap_or_default(); .unwrap_or_default();
let normalisation_pregain = matches let normalisation_pregain = matches
.opt_str("normalisation-pregain") .opt_str("normalisation-pregain")
.map(|pregain| pregain.parse::<f32>().expect("Invalid pregain float value")) .map(|pregain| pregain.parse::<f64>().expect("Invalid pregain float value"))
.unwrap_or(PlayerConfig::default().normalisation_pregain); .unwrap_or(PlayerConfig::default().normalisation_pregain);
let normalisation_threshold = matches let normalisation_threshold = matches
.opt_str("normalisation-threshold") .opt_str("normalisation-threshold")
.map(|threshold| { .map(|threshold| {
db_to_ratio( db_to_ratio(
threshold threshold
.parse::<f32>() .parse::<f64>()
.expect("Invalid threshold float value"), .expect("Invalid threshold float value"),
) )
}) })
.unwrap_or(PlayerConfig::default().normalisation_threshold); .unwrap_or(PlayerConfig::default().normalisation_threshold);
let normalisation_attack = matches let normalisation_attack = matches
.opt_str("normalisation-attack") .opt_str("normalisation-attack")
.map(|attack| attack.parse::<f32>().expect("Invalid attack float value") / MILLIS) .map(|attack| attack.parse::<f64>().expect("Invalid attack float value") / MILLIS)
.unwrap_or(PlayerConfig::default().normalisation_attack); .unwrap_or(PlayerConfig::default().normalisation_attack);
let normalisation_release = matches let normalisation_release = matches
.opt_str("normalisation-release") .opt_str("normalisation-release")
.map(|release| release.parse::<f32>().expect("Invalid release float value") / MILLIS) .map(|release| release.parse::<f64>().expect("Invalid release float value") / MILLIS)
.unwrap_or(PlayerConfig::default().normalisation_release); .unwrap_or(PlayerConfig::default().normalisation_release);
let normalisation_knee = matches let normalisation_knee = matches
.opt_str("normalisation-knee") .opt_str("normalisation-knee")
.map(|knee| knee.parse::<f32>().expect("Invalid knee float value")) .map(|knee| knee.parse::<f64>().expect("Invalid knee float value"))
.unwrap_or(PlayerConfig::default().normalisation_knee); .unwrap_or(PlayerConfig::default().normalisation_knee);
let ditherer_name = matches.opt_str("dither"); let ditherer_name = matches.opt_str("dither");
@ -640,7 +640,7 @@ fn get_setup(args: &[String]) -> Setup {
Some("none") => None, Some("none") => None,
// explicitly set on command line // explicitly set on command line
Some(_) => { Some(_) => {
if format == AudioFormat::F32 { if format == AudioFormat::F64 || format == AudioFormat::F32 {
unimplemented!("Dithering is not available on format {:?}", format); unimplemented!("Dithering is not available on format {:?}", format);
} }
Some(dither::find_ditherer(ditherer_name).expect("Invalid ditherer")) Some(dither::find_ditherer(ditherer_name).expect("Invalid ditherer"))