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:
parent
56f1fb6dae
commit
f29e5212c4
17 changed files with 327 additions and 53 deletions
|
@ -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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue