mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
High-resolution volume control and normalisation
- Store and output samples as 32-bit floats instead of 16-bit integers. This provides 24-25 bits of transparency, allowing for 42-48 dB of headroom to do volume control and normalisation without throwing away bits or dropping dynamic range below 96 dB CD quality. - Perform volume control and normalisation in 64-bit arithmetic. - Add a dynamic limiter with configurable threshold, attack time, release or decay time, and steepness for the sigmoid transfer function. This mimics the native Spotify limiter, offering greater dynamic range than the old limiter, that just reduced overall gain to prevent clipping. - Make the configurable threshold also apply to the old limiter, which is still available. Resolves: librespot-org/librespot#608
This commit is contained in:
parent
56f1fb6dae
commit
f29e5212c4
17 changed files with 327 additions and 53 deletions
|
@ -37,8 +37,11 @@ where
|
|||
use self::lewton::VorbisError::BadAudio;
|
||||
use self::lewton::VorbisError::OggError;
|
||||
loop {
|
||||
match self.0.read_dec_packet_itl() {
|
||||
Ok(Some(packet)) => return Ok(Some(AudioPacket::Samples(packet))),
|
||||
match self
|
||||
.0
|
||||
.read_dec_packet_generic::<lewton::samples::InterleavedSamples<f32>>()
|
||||
{
|
||||
Ok(Some(packet)) => return Ok(Some(AudioPacket::Samples(packet.samples))),
|
||||
Ok(None) => return Ok(None),
|
||||
|
||||
Err(BadAudio(AudioIsHeader)) => (),
|
||||
|
|
|
@ -33,12 +33,12 @@ pub use fetch::{
|
|||
use std::fmt;
|
||||
|
||||
pub enum AudioPacket {
|
||||
Samples(Vec<i16>),
|
||||
Samples(Vec<f32>),
|
||||
OggData(Vec<u8>),
|
||||
}
|
||||
|
||||
impl AudioPacket {
|
||||
pub fn samples(&self) -> &[i16] {
|
||||
pub fn samples(&self) -> &[f32] {
|
||||
match self {
|
||||
AudioPacket::Samples(s) => s,
|
||||
AudioPacket::OggData(_) => panic!("can't return OggData on samples"),
|
||||
|
|
|
@ -39,7 +39,16 @@ where
|
|||
fn next_packet(&mut self) -> Result<Option<AudioPacket>, AudioError> {
|
||||
loop {
|
||||
match self.0.packets().next() {
|
||||
Some(Ok(packet)) => return Ok(Some(AudioPacket::Samples(packet.data))),
|
||||
Some(Ok(packet)) => {
|
||||
// Losslessly represent [-32768, 32767] to [-1.0, 1.0] while maintaining DC linearity.
|
||||
return Ok(Some(AudioPacket::Samples(
|
||||
packet
|
||||
.data
|
||||
.iter()
|
||||
.map(|sample| ((*sample as f64 + 0.5) / (0x7FFF as f64 + 0.5)) as f32)
|
||||
.collect(),
|
||||
)));
|
||||
}
|
||||
None => return Ok(None),
|
||||
|
||||
Some(Err(vorbis::VorbisError::Hole)) => (),
|
||||
|
|
|
@ -8,13 +8,13 @@ use std::ffi::CString;
|
|||
use std::io;
|
||||
use std::process::exit;
|
||||
|
||||
const PREFERED_PERIOD_SIZE: Frames = 5512; // Period of roughly 125ms
|
||||
const PREFERRED_PERIOD_SIZE: Frames = 11025; // Period of roughly 125ms
|
||||
const BUFFERED_PERIODS: Frames = 4;
|
||||
|
||||
pub struct AlsaSink {
|
||||
pcm: Option<PCM>,
|
||||
device: String,
|
||||
buffer: Vec<i16>,
|
||||
buffer: Vec<f32>,
|
||||
}
|
||||
|
||||
fn list_outputs() {
|
||||
|
@ -36,19 +36,19 @@ fn list_outputs() {
|
|||
|
||||
fn open_device(dev_name: &str) -> Result<(PCM, Frames), Box<Error>> {
|
||||
let pcm = PCM::new(dev_name, Direction::Playback, false)?;
|
||||
let mut period_size = PREFERED_PERIOD_SIZE;
|
||||
let mut period_size = PREFERRED_PERIOD_SIZE;
|
||||
// http://www.linuxjournal.com/article/6735?page=0,1#N0x19ab2890.0x19ba78d8
|
||||
// latency = period_size * periods / (rate * bytes_per_frame)
|
||||
// For 16 Bit stereo data, one frame has a length of four bytes.
|
||||
// 500ms = buffer_size / (44100 * 4)
|
||||
// buffer_size_bytes = 0.5 * 44100 / 4
|
||||
// For stereo samples encoded as 32-bit floats, one frame has a length of eight bytes.
|
||||
// 500ms = buffer_size / (44100 * 8)
|
||||
// buffer_size_bytes = 0.5 * 44100 / 8
|
||||
// buffer_size_frames = 0.5 * 44100 = 22050
|
||||
{
|
||||
// Set hardware parameters: 44100 Hz / Stereo / 16 bit
|
||||
// Set hardware parameters: 44100 Hz / Stereo / 32-bit float
|
||||
let hwp = HwParams::any(&pcm)?;
|
||||
|
||||
hwp.set_access(Access::RWInterleaved)?;
|
||||
hwp.set_format(Format::s16())?;
|
||||
hwp.set_format(Format::float())?;
|
||||
hwp.set_rate(44100, ValueOr::Nearest)?;
|
||||
hwp.set_channels(2)?;
|
||||
period_size = hwp.set_period_size_near(period_size, ValueOr::Greater)?;
|
||||
|
@ -114,7 +114,7 @@ impl Sink for AlsaSink {
|
|||
let pcm = self.pcm.as_mut().unwrap();
|
||||
// Write any leftover data in the period buffer
|
||||
// before draining the actual buffer
|
||||
let io = pcm.io_i16().unwrap();
|
||||
let io = pcm.io_f32().unwrap();
|
||||
match io.writei(&self.buffer[..]) {
|
||||
Ok(_) => (),
|
||||
Err(err) => pcm.try_recover(err, false).unwrap(),
|
||||
|
@ -138,7 +138,7 @@ impl Sink for AlsaSink {
|
|||
processed_data += data_to_buffer;
|
||||
if self.buffer.len() == self.buffer.capacity() {
|
||||
let pcm = self.pcm.as_mut().unwrap();
|
||||
let io = pcm.io_i16().unwrap();
|
||||
let io = pcm.io_f32().unwrap();
|
||||
match io.writei(&self.buffer) {
|
||||
Ok(_) => (),
|
||||
Err(err) => pcm.try_recover(err, false).unwrap(),
|
||||
|
|
|
@ -15,7 +15,7 @@ pub struct GstreamerSink {
|
|||
impl Open for GstreamerSink {
|
||||
fn open(device: Option<String>) -> GstreamerSink {
|
||||
gst::init().expect("Failed to init gstreamer!");
|
||||
let pipeline_str_preamble = r#"appsrc caps="audio/x-raw,format=S16LE,layout=interleaved,channels=2,rate=44100" block=true max-bytes=4096 name=appsrc0 "#;
|
||||
let pipeline_str_preamble = r#"appsrc caps="audio/x-raw,format=F32,layout=interleaved,channels=2,rate=44100" block=true max-bytes=4096 name=appsrc0 "#;
|
||||
let pipeline_str_rest = r#" ! audioconvert ! autoaudiosink"#;
|
||||
let pipeline_str: String = match device {
|
||||
Some(x) => format!("{}{}", pipeline_str_preamble, x),
|
||||
|
|
|
@ -7,20 +7,18 @@ use std::io;
|
|||
use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
|
||||
|
||||
pub struct JackSink {
|
||||
send: SyncSender<i16>,
|
||||
send: SyncSender<f32>,
|
||||
// We have to keep hold of this object, or the Sink can't play...
|
||||
#[allow(dead_code)]
|
||||
active_client: AsyncClient<(), JackData>,
|
||||
}
|
||||
|
||||
pub struct JackData {
|
||||
rec: Receiver<i16>,
|
||||
rec: Receiver<f32>,
|
||||
port_l: Port<AudioOut>,
|
||||
port_r: Port<AudioOut>,
|
||||
}
|
||||
|
||||
fn pcm_to_f32(sample: i16) -> f32 {
|
||||
sample as f32 / 32768.0
|
||||
}
|
||||
|
||||
impl ProcessHandler for JackData {
|
||||
fn process(&mut self, _: &Client, ps: &ProcessScope) -> Control {
|
||||
// get output port buffers
|
||||
|
@ -33,8 +31,8 @@ impl ProcessHandler for JackData {
|
|||
|
||||
let buf_size = buf_r.len();
|
||||
for i in 0..buf_size {
|
||||
buf_r[i] = pcm_to_f32(queue_iter.next().unwrap_or(0));
|
||||
buf_l[i] = pcm_to_f32(queue_iter.next().unwrap_or(0));
|
||||
buf_r[i] = queue_iter.next().unwrap_or(0.0);
|
||||
buf_l[i] = queue_iter.next().unwrap_or(0.0);
|
||||
}
|
||||
Control::Continue
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ impl Sink for StdoutSink {
|
|||
AudioPacket::Samples(data) => unsafe {
|
||||
slice::from_raw_parts(
|
||||
data.as_ptr() as *const u8,
|
||||
data.len() * mem::size_of::<i16>(),
|
||||
data.len() * mem::size_of::<f32>(),
|
||||
)
|
||||
},
|
||||
AudioPacket::OggData(data) => data,
|
||||
|
|
|
@ -8,8 +8,8 @@ use std::process::exit;
|
|||
use std::time::Duration;
|
||||
|
||||
pub struct PortAudioSink<'a>(
|
||||
Option<portaudio_rs::stream::Stream<'a, i16, i16>>,
|
||||
StreamParameters<i16>,
|
||||
Option<portaudio_rs::stream::Stream<'a, f32, f32>>,
|
||||
StreamParameters<f32>,
|
||||
);
|
||||
|
||||
fn output_devices() -> Box<dyn Iterator<Item = (DeviceIndex, DeviceInfo)>> {
|
||||
|
@ -65,7 +65,7 @@ impl<'a> Open for PortAudioSink<'a> {
|
|||
device: device_idx,
|
||||
channel_count: 2,
|
||||
suggested_latency: latency,
|
||||
data: 0i16,
|
||||
data: 0.0,
|
||||
};
|
||||
|
||||
PortAudioSink(None, params)
|
||||
|
|
|
@ -3,6 +3,7 @@ use crate::audio::AudioPacket;
|
|||
use libpulse_binding::{self as pulse, stream::Direction};
|
||||
use libpulse_simple_binding::Simple;
|
||||
use std::io;
|
||||
use std::mem;
|
||||
|
||||
const APP_NAME: &str = "librespot";
|
||||
const STREAM_NAME: &str = "Spotify endpoint";
|
||||
|
@ -18,7 +19,7 @@ impl Open for PulseAudioSink {
|
|||
debug!("Using PulseAudio sink");
|
||||
|
||||
let ss = pulse::sample::Spec {
|
||||
format: pulse::sample::Format::S16le,
|
||||
format: pulse::sample::Format::F32le,
|
||||
channels: 2, // stereo
|
||||
rate: 44100,
|
||||
};
|
||||
|
@ -68,13 +69,13 @@ impl Sink for PulseAudioSink {
|
|||
|
||||
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
|
||||
if let Some(s) = &self.s {
|
||||
// SAFETY: An i16 consists of two bytes, so that the given slice can be interpreted
|
||||
// as a byte array of double length. Each byte pointer is validly aligned, and so
|
||||
// is the newly created slice.
|
||||
// SAFETY: An f32 consists of four bytes, so that the given slice can be interpreted
|
||||
// as a byte array of four. Each byte pointer is validly aligned, and so is the newly
|
||||
// created slice.
|
||||
let d: &[u8] = unsafe {
|
||||
std::slice::from_raw_parts(
|
||||
packet.samples().as_ptr() as *const u8,
|
||||
packet.samples().len() * 2,
|
||||
packet.samples().len() * mem::size_of::<f32>(),
|
||||
)
|
||||
};
|
||||
|
||||
|
|
|
@ -198,7 +198,7 @@ impl Sink for JackRodioSink {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn write(&mut self, data: &[i16]) -> io::Result<()> {
|
||||
fn write(&mut self, data: &[f32]) -> io::Result<()> {
|
||||
let source = rodio::buffer::SamplesBuffer::new(2, 44100, data);
|
||||
self.jackrodio_sink.append(source);
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ use crate::audio::AudioPacket;
|
|||
use sdl2::audio::{AudioQueue, AudioSpecDesired};
|
||||
use std::{io, thread, time};
|
||||
|
||||
type Channel = i16;
|
||||
type Channel = f32;
|
||||
|
||||
pub struct SdlSink {
|
||||
queue: AudioQueue<Channel>,
|
||||
|
@ -47,7 +47,7 @@ impl Sink for SdlSink {
|
|||
}
|
||||
|
||||
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
|
||||
while self.queue.size() > (2 * 2 * 44_100) {
|
||||
while self.queue.size() > (2 * 4 * 44_100) {
|
||||
// sleep and wait for sdl thread to drain the queue a bit
|
||||
thread::sleep(time::Duration::from_millis(10));
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ impl Sink for SubprocessSink {
|
|||
let data: &[u8] = unsafe {
|
||||
slice::from_raw_parts(
|
||||
packet.samples().as_ptr() as *const u8,
|
||||
packet.samples().len() * mem::size_of::<i16>(),
|
||||
packet.samples().len() * mem::size_of::<f32>(),
|
||||
)
|
||||
};
|
||||
if let Some(child) = &mut self.child {
|
||||
|
|
|
@ -48,12 +48,40 @@ impl Default for NormalisationType {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum NormalisationMethod {
|
||||
Basic,
|
||||
Dynamic,
|
||||
}
|
||||
|
||||
impl FromStr for NormalisationMethod {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"basic" => Ok(NormalisationMethod::Basic),
|
||||
"dynamic" => Ok(NormalisationMethod::Dynamic),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NormalisationMethod {
|
||||
fn default() -> NormalisationMethod {
|
||||
NormalisationMethod::Dynamic
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PlayerConfig {
|
||||
pub bitrate: Bitrate,
|
||||
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_steepness: f32,
|
||||
pub gapless: bool,
|
||||
pub passthrough: bool,
|
||||
}
|
||||
|
@ -64,7 +92,12 @@ impl Default for PlayerConfig {
|
|||
bitrate: Bitrate::default(),
|
||||
normalisation: false,
|
||||
normalisation_type: NormalisationType::default(),
|
||||
normalisation_method: NormalisationMethod::default(),
|
||||
normalisation_pregain: 0.0,
|
||||
normalisation_threshold: -1.0,
|
||||
normalisation_attack: 0.005,
|
||||
normalisation_release: 0.1,
|
||||
normalisation_steepness: 1.0,
|
||||
gapless: true,
|
||||
passthrough: false,
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ pub trait Mixer: Send {
|
|||
}
|
||||
|
||||
pub trait AudioFilter {
|
||||
fn modify_stream(&self, data: &mut [i16]);
|
||||
fn modify_stream(&self, data: &mut [f32]);
|
||||
}
|
||||
|
||||
#[cfg(feature = "alsa-backend")]
|
||||
|
|
|
@ -35,11 +35,12 @@ struct SoftVolumeApplier {
|
|||
}
|
||||
|
||||
impl AudioFilter for SoftVolumeApplier {
|
||||
fn modify_stream(&self, data: &mut [i16]) {
|
||||
fn modify_stream(&self, data: &mut [f32]) {
|
||||
let volume = self.volume.load(Ordering::Relaxed) as u16;
|
||||
if volume != 0xFFFF {
|
||||
let volume_factor = volume as f64 / 0xFFFF as f64;
|
||||
for x in data.iter_mut() {
|
||||
*x = (*x as i32 * volume as i32 / 0xFFFF) as i16;
|
||||
*x = (*x as f64 * volume_factor) as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ use std::mem;
|
|||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::config::{Bitrate, NormalisationType, PlayerConfig};
|
||||
use crate::config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig};
|
||||
use librespot_core::session::Session;
|
||||
use librespot_core::spotify_id::SpotifyId;
|
||||
|
||||
|
@ -26,6 +26,7 @@ use crate::metadata::{AudioItem, FileFormat};
|
|||
use crate::mixer::AudioFilter;
|
||||
|
||||
const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000;
|
||||
const SAMPLES_PER_SECOND: u32 = 44100 * 2;
|
||||
|
||||
pub struct Player {
|
||||
commands: Option<futures::sync::mpsc::UnboundedSender<PlayerCommand>>,
|
||||
|
@ -54,6 +55,13 @@ struct PlayerInternal {
|
|||
sink_event_callback: Option<SinkEventCallback>,
|
||||
audio_filter: Option<Box<dyn AudioFilter + Send>>,
|
||||
event_senders: Vec<futures::sync::mpsc::UnboundedSender<PlayerEvent>>,
|
||||
|
||||
limiter_active: bool,
|
||||
limiter_attack_counter: u32,
|
||||
limiter_release_counter: u32,
|
||||
limiter_peak_sample: f32,
|
||||
limiter_factor: f32,
|
||||
limiter_strength: f32,
|
||||
}
|
||||
|
||||
enum PlayerCommand {
|
||||
|
@ -185,7 +193,7 @@ impl PlayerEvent {
|
|||
pub type PlayerEventChannel = futures::sync::mpsc::UnboundedReceiver<PlayerEvent>;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct NormalisationData {
|
||||
pub struct NormalisationData {
|
||||
track_gain_db: f32,
|
||||
track_peak: f32,
|
||||
album_gain_db: f32,
|
||||
|
@ -193,6 +201,14 @@ struct NormalisationData {
|
|||
}
|
||||
|
||||
impl NormalisationData {
|
||||
pub fn db_to_ratio(db: f32) -> f32 {
|
||||
return f32::powf(10.0, db / 20.0);
|
||||
}
|
||||
|
||||
pub fn ratio_to_db(ratio: f32) -> f32 {
|
||||
return ratio.log10() * 20.0;
|
||||
}
|
||||
|
||||
fn parse_from_file<T: Read + Seek>(mut file: T) -> Result<NormalisationData> {
|
||||
const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144;
|
||||
file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET))
|
||||
|
@ -218,17 +234,44 @@ impl NormalisationData {
|
|||
NormalisationType::Album => [data.album_gain_db, data.album_peak],
|
||||
NormalisationType::Track => [data.track_gain_db, data.track_peak],
|
||||
};
|
||||
let mut normalisation_factor =
|
||||
f32::powf(10.0, (gain_db + config.normalisation_pregain) / 20.0);
|
||||
|
||||
if normalisation_factor * gain_peak > 1.0 {
|
||||
warn!("Reducing normalisation factor to prevent clipping. Please add negative pregain to avoid.");
|
||||
normalisation_factor = 1.0 / gain_peak;
|
||||
let normalisation_power = gain_db + config.normalisation_pregain;
|
||||
let mut normalisation_factor = Self::db_to_ratio(normalisation_power);
|
||||
|
||||
if normalisation_factor * gain_peak > config.normalisation_threshold {
|
||||
let limited_normalisation_factor = config.normalisation_threshold / gain_peak;
|
||||
let limited_normalisation_power = Self::ratio_to_db(limited_normalisation_factor);
|
||||
|
||||
if config.normalisation_method == NormalisationMethod::Basic {
|
||||
warn!("Limiting gain to {:.2} for the duration of this track to stay under normalisation threshold.", limited_normalisation_power);
|
||||
normalisation_factor = limited_normalisation_factor;
|
||||
} else {
|
||||
warn!(
|
||||
"This track will at its peak be subject to {:.2} dB of dynamic limiting.",
|
||||
normalisation_power - limited_normalisation_power
|
||||
);
|
||||
}
|
||||
|
||||
warn!("Please lower pregain to avoid.");
|
||||
}
|
||||
|
||||
debug!("Normalisation Data: {:?}", data);
|
||||
debug!("Normalisation Type: {:?}", config.normalisation_type);
|
||||
debug!("Applied normalisation factor: {}", normalisation_factor);
|
||||
debug!(
|
||||
"Normalisation Threshold: {:.1}",
|
||||
Self::ratio_to_db(config.normalisation_threshold)
|
||||
);
|
||||
debug!("Normalisation Method: {:?}", config.normalisation_method);
|
||||
debug!("Normalisation Factor: {}", normalisation_factor);
|
||||
|
||||
if config.normalisation_method == NormalisationMethod::Dynamic {
|
||||
debug!("Normalisation Attack: {:?}", config.normalisation_attack);
|
||||
debug!("Normalisation Release: {:?}", config.normalisation_release);
|
||||
debug!(
|
||||
"Normalisation Steepness: {:?}",
|
||||
config.normalisation_steepness
|
||||
);
|
||||
}
|
||||
|
||||
normalisation_factor
|
||||
}
|
||||
|
@ -262,6 +305,13 @@ impl Player {
|
|||
sink_event_callback: None,
|
||||
audio_filter: audio_filter,
|
||||
event_senders: [event_sender].to_vec(),
|
||||
|
||||
limiter_active: false,
|
||||
limiter_attack_counter: 0,
|
||||
limiter_release_counter: 0,
|
||||
limiter_peak_sample: 0.0,
|
||||
limiter_factor: 1.0,
|
||||
limiter_strength: 0.0,
|
||||
};
|
||||
|
||||
// While PlayerInternal is written as a future, it still contains blocking code.
|
||||
|
@ -1113,9 +1163,111 @@ impl PlayerInternal {
|
|||
editor.modify_stream(data)
|
||||
}
|
||||
|
||||
if self.config.normalisation && normalisation_factor != 1.0 {
|
||||
for x in data.iter_mut() {
|
||||
*x = (*x as f32 * normalisation_factor) as i16;
|
||||
if self.config.normalisation
|
||||
&& (normalisation_factor != 1.0
|
||||
|| self.config.normalisation_method != NormalisationMethod::Basic)
|
||||
{
|
||||
for sample in data.iter_mut() {
|
||||
let mut actual_normalisation_factor = normalisation_factor;
|
||||
if self.config.normalisation_method == NormalisationMethod::Dynamic
|
||||
{
|
||||
if self.limiter_active {
|
||||
// "S"-shaped curve with a configurable steepness during attack and release:
|
||||
// - > 1.0 yields soft knees at start and end, steeper in between
|
||||
// - 1.0 yields a linear function from 0-100%
|
||||
// - between 0.0 and 1.0 yields hard knees at start and end, flatter in between
|
||||
// - 0.0 yields a step response to 50%, causing distortion
|
||||
// - Rates < 0.0 invert the limiter and are invalid
|
||||
let mut shaped_limiter_strength = self.limiter_strength;
|
||||
if shaped_limiter_strength > 0.0
|
||||
&& shaped_limiter_strength < 1.0
|
||||
{
|
||||
shaped_limiter_strength = 1.0
|
||||
/ (1.0
|
||||
+ f32::powf(
|
||||
shaped_limiter_strength
|
||||
/ (1.0 - shaped_limiter_strength),
|
||||
-1.0 * self.config.normalisation_steepness,
|
||||
));
|
||||
}
|
||||
actual_normalisation_factor =
|
||||
(1.0 - shaped_limiter_strength) * normalisation_factor
|
||||
+ shaped_limiter_strength * self.limiter_factor;
|
||||
};
|
||||
|
||||
// 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();
|
||||
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
|
||||
* self.config.normalisation_release)
|
||||
- self.limiter_release_counter as f32)
|
||||
/ (self.config.normalisation_release
|
||||
/ self.config.normalisation_attack))
|
||||
as u32;
|
||||
self.limiter_release_counter = 0;
|
||||
}
|
||||
|
||||
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.config.normalisation_attack);
|
||||
|
||||
if abs_sample > self.limiter_peak_sample {
|
||||
self.limiter_peak_sample = abs_sample;
|
||||
self.limiter_factor =
|
||||
self.config.normalisation_threshold
|
||||
/ self.limiter_peak_sample;
|
||||
}
|
||||
} else if self.limiter_active {
|
||||
if self.limiter_attack_counter > 0 {
|
||||
// Release may start within the attack period, before
|
||||
// the limiter reached full strength. For that reason
|
||||
// start the release by synchronizing with the current
|
||||
// attack limiter strength.
|
||||
self.limiter_release_counter = (((SAMPLES_PER_SECOND
|
||||
as f32
|
||||
* self.config.normalisation_attack)
|
||||
- self.limiter_attack_counter as f32)
|
||||
* (self.config.normalisation_release
|
||||
/ self.config.normalisation_attack))
|
||||
as u32;
|
||||
self.limiter_attack_counter = 0;
|
||||
}
|
||||
|
||||
self.limiter_release_counter =
|
||||
self.limiter_release_counter.saturating_add(1);
|
||||
|
||||
if self.limiter_release_counter
|
||||
> (SAMPLES_PER_SECOND as f32
|
||||
* self.config.normalisation_release)
|
||||
as u32
|
||||
{
|
||||
self.reset_limiter();
|
||||
} else {
|
||||
self.limiter_strength = ((SAMPLES_PER_SECOND as f32
|
||||
* self.config.normalisation_release)
|
||||
- self.limiter_release_counter as f32)
|
||||
/ (SAMPLES_PER_SECOND as f32
|
||||
* self.config.normalisation_release);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extremely sharp attacks, however unlikely, *may* still clip and provide
|
||||
// undefined results, so strictly enforce output within [-1.0, 1.0].
|
||||
*sample = (*sample as f64 * actual_normalisation_factor as f64)
|
||||
.clamp(-1.0, 1.0)
|
||||
as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1146,6 +1298,15 @@ impl PlayerInternal {
|
|||
}
|
||||
}
|
||||
|
||||
fn reset_limiter(&mut self) {
|
||||
self.limiter_active = false;
|
||||
self.limiter_release_counter = 0;
|
||||
self.limiter_attack_counter = 0;
|
||||
self.limiter_peak_sample = 0.0;
|
||||
self.limiter_factor = 1.0;
|
||||
self.limiter_strength = 0.0;
|
||||
}
|
||||
|
||||
fn start_playback(
|
||||
&mut self,
|
||||
track_id: SpotifyId,
|
||||
|
|
72
src/main.rs
72
src/main.rs
|
@ -22,13 +22,15 @@ use librespot::core::version;
|
|||
use librespot::connect::discovery::{discovery, DiscoveryStream};
|
||||
use librespot::connect::spirc::{Spirc, SpircTask};
|
||||
use librespot::playback::audio_backend::{self, Sink, BACKENDS};
|
||||
use librespot::playback::config::{Bitrate, NormalisationType, PlayerConfig};
|
||||
use librespot::playback::config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig};
|
||||
use librespot::playback::mixer::{self, Mixer, MixerConfig};
|
||||
use librespot::playback::player::{Player, PlayerEvent};
|
||||
use librespot::playback::player::{NormalisationData, Player, PlayerEvent};
|
||||
|
||||
mod player_event_handler;
|
||||
use crate::player_event_handler::{emit_sink_event, run_program_on_events};
|
||||
|
||||
const MILLIS: f32 = 1000.0;
|
||||
|
||||
fn device_id(name: &str) -> String {
|
||||
hex::encode(Sha1::digest(name.as_bytes()))
|
||||
}
|
||||
|
@ -188,6 +190,12 @@ fn setup(args: &[String]) -> Setup {
|
|||
"enable-volume-normalisation",
|
||||
"Play all tracks at the same volume",
|
||||
)
|
||||
.optopt(
|
||||
"",
|
||||
"normalisation-method",
|
||||
"Specify the normalisation method to use - [basic, dynamic]. Default is dynamic.",
|
||||
"NORMALISATION_METHOD",
|
||||
)
|
||||
.optopt(
|
||||
"",
|
||||
"normalisation-gain-type",
|
||||
|
@ -200,6 +208,30 @@ fn setup(args: &[String]) -> Setup {
|
|||
"Pregain (dB) applied by volume normalisation",
|
||||
"PREGAIN",
|
||||
)
|
||||
.optopt(
|
||||
"",
|
||||
"normalisation-threshold",
|
||||
"Threshold (dBFS) to prevent clipping. Default is -1.0.",
|
||||
"THRESHOLD",
|
||||
)
|
||||
.optopt(
|
||||
"",
|
||||
"normalisation-attack",
|
||||
"Attack time (ms) in which the dynamic limiter is reducing gain. Default is 5.",
|
||||
"ATTACK",
|
||||
)
|
||||
.optopt(
|
||||
"",
|
||||
"normalisation-release",
|
||||
"Release or decay time (ms) in which the dynamic limiter is restoring gain. Default is 100.",
|
||||
"RELEASE",
|
||||
)
|
||||
.optopt(
|
||||
"",
|
||||
"normalisation-steepness",
|
||||
"Steepness of the dynamic limiting curve. Default is 1.0.",
|
||||
"STEEPNESS",
|
||||
)
|
||||
.optopt(
|
||||
"",
|
||||
"volume-ctrl",
|
||||
|
@ -390,15 +422,51 @@ fn setup(args: &[String]) -> Setup {
|
|||
NormalisationType::from_str(gain_type).expect("Invalid normalisation type")
|
||||
})
|
||||
.unwrap_or(NormalisationType::default());
|
||||
let normalisation_method = matches
|
||||
.opt_str("normalisation-method")
|
||||
.as_ref()
|
||||
.map(|gain_type| {
|
||||
NormalisationMethod::from_str(gain_type).expect("Invalid normalisation method")
|
||||
})
|
||||
.unwrap_or(NormalisationMethod::default());
|
||||
PlayerConfig {
|
||||
bitrate: bitrate,
|
||||
gapless: !matches.opt_present("disable-gapless"),
|
||||
normalisation: matches.opt_present("enable-volume-normalisation"),
|
||||
normalisation_method: normalisation_method,
|
||||
normalisation_type: gain_type,
|
||||
normalisation_pregain: matches
|
||||
.opt_str("normalisation-pregain")
|
||||
.map(|pregain| pregain.parse::<f32>().expect("Invalid pregain float value"))
|
||||
.unwrap_or(PlayerConfig::default().normalisation_pregain),
|
||||
normalisation_threshold: NormalisationData::db_to_ratio(
|
||||
matches
|
||||
.opt_str("normalisation-threshold")
|
||||
.map(|threshold| {
|
||||
threshold
|
||||
.parse::<f32>()
|
||||
.expect("Invalid threshold float value")
|
||||
})
|
||||
.unwrap_or(PlayerConfig::default().normalisation_threshold),
|
||||
),
|
||||
normalisation_attack: matches
|
||||
.opt_str("normalisation-attack")
|
||||
.map(|attack| attack.parse::<f32>().expect("Invalid attack float value"))
|
||||
.unwrap_or(PlayerConfig::default().normalisation_attack * MILLIS)
|
||||
/ MILLIS,
|
||||
normalisation_release: matches
|
||||
.opt_str("normalisation-release")
|
||||
.map(|release| release.parse::<f32>().expect("Invalid release float value"))
|
||||
.unwrap_or(PlayerConfig::default().normalisation_release * MILLIS)
|
||||
/ MILLIS,
|
||||
normalisation_steepness: matches
|
||||
.opt_str("normalisation-steepness")
|
||||
.map(|steepness| {
|
||||
steepness
|
||||
.parse::<f32>()
|
||||
.expect("Invalid steepness float value")
|
||||
})
|
||||
.unwrap_or(PlayerConfig::default().normalisation_steepness),
|
||||
passthrough,
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue