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 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)
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)),
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)) => (),
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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].
|
||||||
|
|
18
src/main.rs
18
src/main.rs
|
@ -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"))
|
||||||
|
|
Loading…
Reference in a new issue