1
0
Fork 0
mirror of https://github.com/librespot-org/librespot.git synced 2025-10-04 18:29:45 +02: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:
Roderick van Domburg 2021-02-24 21:39:42 +01:00
parent 56f1fb6dae
commit f29e5212c4
17 changed files with 327 additions and 53 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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