mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Store and process samples in 64 bit (#773)
This commit is contained in:
parent
8062bd2518
commit
fe2d5ca7c6
19 changed files with 177 additions and 149 deletions
|
@ -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)
|
||||
|
|
|
@ -41,6 +41,7 @@ fn list_outputs() {
|
|||
fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box<Error>> {
|
||||
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(),
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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<Self, Self::Error> {
|
||||
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::<f64>(),
|
||||
Self::F32 => mem::size_of::<f32>(),
|
||||
Self::S24_3 => mem::size_of::<i24>(),
|
||||
Self::S16 => mem::size_of::<i16>(),
|
||||
_ => mem::size_of::<i32>(), // 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<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::*;
|
||||
match s.to_lowercase().as_ref() {
|
||||
"cubic" => Ok(Cubic(db_range)),
|
||||
|
|
|
@ -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<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
|
||||
.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<i32> {
|
||||
pub fn f64_to_s24(&mut self, samples: &[f64]) -> Vec<i32> {
|
||||
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<i24> {
|
||||
pub fn f64_to_s24_3(&mut self, samples: &[f64]) -> Vec<i24> {
|
||||
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<i16> {
|
||||
pub fn f64_to_s16(&mut self, samples: &[f64]) -> Vec<i16> {
|
||||
samples
|
||||
.iter()
|
||||
.map(|sample| self.scale(*sample, 0x8000) as i16)
|
||||
.map(|sample| self.scale(*sample, Self::SCALE_S16) as i16)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::<lewton::samples::InterleavedSamples<f32>>()
|
||||
{
|
||||
Ok(Some(packet)) => return Ok(Some(AudioPacket::Samples(packet.samples))),
|
||||
match self.0.read_dec_packet_generic::<InterleavedSamples<f32>>() {
|
||||
Ok(Some(packet)) => return Ok(Some(AudioPacket::samples_from_f32(packet.samples))),
|
||||
Ok(None) => return Ok(None),
|
||||
|
||||
Err(BadAudio(AudioIsHeader)) => (),
|
||||
|
|
|
@ -7,12 +7,17 @@ mod passthrough_decoder;
|
|||
pub use passthrough_decoder::{PassthroughDecoder, PassthroughError};
|
||||
|
||||
pub enum AudioPacket {
|
||||
Samples(Vec<f32>),
|
||||
Samples(Vec<f64>),
|
||||
OggData(Vec<u8>),
|
||||
}
|
||||
|
||||
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 {
|
||||
AudioPacket::Samples(s) => s,
|
||||
AudioPacket::OggData(_) => panic!("can't return OggData on samples"),
|
||||
|
|
|
@ -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<f32>,
|
||||
distribution: Triangular<f64>,
|
||||
}
|
||||
|
||||
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<f32>,
|
||||
distribution: Normal<f64>,
|
||||
}
|
||||
|
||||
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<f32>,
|
||||
distribution: Uniform<f64>,
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<f32>.
|
||||
volume: Arc<AtomicU32>,
|
||||
// There is no AtomicF64, so we store the f64 as bits in a u64 field.
|
||||
// It's much faster than a Mutex<f64>.
|
||||
volume: Arc<AtomicU64>,
|
||||
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<AtomicU32>,
|
||||
volume: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<mpsc::UnboundedSender<PlayerCommand>>,
|
||||
|
@ -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<PlayerEvent>;
|
||||
|
||||
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<AudioPacket>, normalisation_factor: f32) {
|
||||
fn handle_packet(&mut self, packet: Option<AudioPacket>, 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].
|
||||
|
|
18
src/main.rs
18
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::<f32>().unwrap())
|
||||
.map(|range| range.parse::<f64>().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::<f32>().expect("Invalid pregain float value"))
|
||||
.map(|pregain| pregain.parse::<f64>().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::<f32>()
|
||||
.parse::<f64>()
|
||||
.expect("Invalid threshold float value"),
|
||||
)
|
||||
})
|
||||
.unwrap_or(PlayerConfig::default().normalisation_threshold);
|
||||
let normalisation_attack = matches
|
||||
.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);
|
||||
let normalisation_release = matches
|
||||
.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);
|
||||
let normalisation_knee = matches
|
||||
.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);
|
||||
|
||||
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"))
|
||||
|
|
Loading…
Reference in a new issue