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 `--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)

View file

@ -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(),

View file

@ -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");
}

View file

@ -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())
}
},

View file

@ -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)
}
};

View file

@ -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 {

View file

@ -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,

View file

@ -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)
}

View file

@ -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)),

View file

@ -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()
}
}

View file

@ -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)) => (),

View file

@ -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"),

View file

@ -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;

View file

@ -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");
}
}

View file

@ -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)
}
}

View file

@ -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;

View file

@ -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;
}
}
}

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;
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].

View file

@ -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"))