Skip to content

Commit 8fe2e01

Browse files
Merge pull request #660 from roderickvd/hi-res-volume-control
High-resolution volume control and normalisation
2 parents 6df9779 + e20b96c commit 8fe2e01

23 files changed

+1023
-909
lines changed

Cargo.lock

Lines changed: 64 additions & 545 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

audio/Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ log = "0.4"
2222
num-bigint = "0.3"
2323
num-traits = "0.2"
2424
tempfile = "3.1"
25+
zerocopy = "0.3"
2526

26-
librespot-tremor = { version = "0.2.0", optional = true }
27-
vorbis = { version ="0.0.14", optional = true }
27+
librespot-tremor = { version = "0.2", optional = true }
28+
vorbis = { version ="0.0", optional = true }
2829

2930
[features]
3031
with-tremor = ["librespot-tremor"]

audio/src/convert.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use zerocopy::AsBytes;
2+
3+
#[derive(AsBytes, Copy, Clone, Debug)]
4+
#[allow(non_camel_case_types)]
5+
#[repr(transparent)]
6+
pub struct i24([u8; 3]);
7+
impl i24 {
8+
fn pcm_from_i32(sample: i32) -> Self {
9+
// drop the least significant byte
10+
let [a, b, c, _d] = (sample >> 8).to_le_bytes();
11+
i24([a, b, c])
12+
}
13+
}
14+
15+
// Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX] while maintaining DC linearity.
16+
macro_rules! convert_samples_to {
17+
($type: ident, $samples: expr) => {
18+
convert_samples_to!($type, $samples, 0)
19+
};
20+
($type: ident, $samples: expr, $drop_bits: expr) => {
21+
$samples
22+
.iter()
23+
.map(|sample| {
24+
// Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX]
25+
// while maintaining DC linearity. There is nothing to be gained
26+
// by doing this in f64, as the significand of a f32 is 24 bits,
27+
// just like the maximum bit depth we are converting to.
28+
let int_value = *sample * (std::$type::MAX as f32 + 0.5) - 0.5;
29+
30+
// Casting floats to ints truncates by default, which results
31+
// in larger quantization error than rounding arithmetically.
32+
// Flooring is faster, but again with larger error.
33+
int_value.round() as $type >> $drop_bits
34+
})
35+
.collect()
36+
};
37+
}
38+
39+
pub struct SamplesConverter {}
40+
impl SamplesConverter {
41+
pub fn to_s32(samples: &[f32]) -> Vec<i32> {
42+
convert_samples_to!(i32, samples)
43+
}
44+
45+
pub fn to_s24(samples: &[f32]) -> Vec<i32> {
46+
convert_samples_to!(i32, samples, 8)
47+
}
48+
49+
pub fn to_s24_3(samples: &[f32]) -> Vec<i24> {
50+
Self::to_s32(samples)
51+
.iter()
52+
.map(|sample| i24::pcm_from_i32(*sample))
53+
.collect()
54+
}
55+
56+
pub fn to_s16(samples: &[f32]) -> Vec<i16> {
57+
convert_samples_to!(i16, samples)
58+
}
59+
}

audio/src/lewton_decoder.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,11 @@ where
3737
use self::lewton::VorbisError::BadAudio;
3838
use self::lewton::VorbisError::OggError;
3939
loop {
40-
match self.0.read_dec_packet_itl() {
41-
Ok(Some(packet)) => return Ok(Some(AudioPacket::Samples(packet))),
40+
match self
41+
.0
42+
.read_dec_packet_generic::<lewton::samples::InterleavedSamples<f32>>()
43+
{
44+
Ok(Some(packet)) => return Ok(Some(AudioPacket::Samples(packet.samples))),
4245
Ok(None) => return Ok(None),
4346

4447
Err(BadAudio(AudioIsHeader)) => (),

audio/src/lib.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ extern crate tempfile;
1313

1414
extern crate librespot_core;
1515

16+
mod convert;
1617
mod decrypt;
1718
mod fetch;
1819

@@ -24,6 +25,7 @@ mod passthrough_decoder;
2425

2526
mod range_set;
2627

28+
pub use convert::{i24, SamplesConverter};
2729
pub use decrypt::AudioDecrypt;
2830
pub use fetch::{AudioFile, AudioFileOpen, StreamLoaderController};
2931
pub use fetch::{
@@ -33,12 +35,12 @@ pub use fetch::{
3335
use std::fmt;
3436

3537
pub enum AudioPacket {
36-
Samples(Vec<i16>),
38+
Samples(Vec<f32>),
3739
OggData(Vec<u8>),
3840
}
3941

4042
impl AudioPacket {
41-
pub fn samples(&self) -> &[i16] {
43+
pub fn samples(&self) -> &[f32] {
4244
match self {
4345
AudioPacket::Samples(s) => s,
4446
AudioPacket::OggData(_) => panic!("can't return OggData on samples"),

audio/src/libvorbis_decoder.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,18 @@ where
3939
fn next_packet(&mut self) -> Result<Option<AudioPacket>, AudioError> {
4040
loop {
4141
match self.0.packets().next() {
42-
Some(Ok(packet)) => return Ok(Some(AudioPacket::Samples(packet.data))),
42+
Some(Ok(packet)) => {
43+
// Losslessly represent [-32768, 32767] to [-1.0, 1.0] while maintaining DC linearity.
44+
return Ok(Some(AudioPacket::Samples(
45+
packet
46+
.data
47+
.iter()
48+
.map(|sample| {
49+
((*sample as f64 + 0.5) / (std::i16::MAX as f64 + 0.5)) as f32
50+
})
51+
.collect(),
52+
)));
53+
}
4354
None => return Ok(None),
4455

4556
Some(Err(vorbis::VorbisError::Hole)) => (),

examples/play.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use librespot::core::authentication::Credentials;
55
use librespot::core::config::SessionConfig;
66
use librespot::core::session::Session;
77
use librespot::core::spotify_id::SpotifyId;
8-
use librespot::playback::config::PlayerConfig;
8+
use librespot::playback::config::{AudioFormat, PlayerConfig};
99

1010
use librespot::playback::audio_backend;
1111
use librespot::playback::player::Player;
@@ -16,6 +16,7 @@ fn main() {
1616

1717
let session_config = SessionConfig::default();
1818
let player_config = PlayerConfig::default();
19+
let audio_format = AudioFormat::default();
1920

2021
let args: Vec<_> = env::args().collect();
2122
if args.len() != 4 {
@@ -35,7 +36,7 @@ fn main() {
3536
.unwrap();
3637

3738
let (mut player, _) = Player::new(player_config, session.clone(), None, move || {
38-
(backend)(None)
39+
(backend)(None, audio_format)
3940
});
4041

4142
player.load(track, true, 0);

playback/Cargo.toml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,19 @@ log = "0.4"
2323
byteorder = "1.3"
2424
shell-words = "1.0.0"
2525

26-
alsa = { version = "0.4", optional = true }
26+
alsa = { version = "0.5", optional = true }
2727
portaudio-rs = { version = "0.3", optional = true }
28-
libpulse-binding = { version = "2.13", optional = true, default-features = false }
29-
libpulse-simple-binding = { version = "2.13", optional = true, default-features = false }
28+
libpulse-binding = { version = "2", optional = true, default-features = false }
29+
libpulse-simple-binding = { version = "2", optional = true, default-features = false }
3030
jack = { version = "0.6", optional = true }
3131
libc = { version = "0.2", optional = true }
3232
rodio = { version = "0.13", optional = true, default-features = false }
3333
cpal = { version = "0.13", optional = true }
34-
sdl2 = { version = "0.34", optional = true }
34+
sdl2 = { version = "0.34.3", optional = true }
3535
gstreamer = { version = "0.16", optional = true }
3636
gstreamer-app = { version = "0.16", optional = true }
3737
glib = { version = "0.10", optional = true }
38-
zerocopy = { version = "0.3", optional = true }
38+
zerocopy = { version = "0.3" }
3939

4040
[features]
4141
alsa-backend = ["alsa"]
@@ -45,4 +45,4 @@ jackaudio-backend = ["jack"]
4545
rodiojack-backend = ["rodio", "cpal/jack"]
4646
rodio-backend = ["rodio", "cpal"]
4747
sdl-backend = ["sdl2"]
48-
gstreamer-backend = ["gstreamer", "gstreamer-app", "glib", "zerocopy"]
48+
gstreamer-backend = ["gstreamer", "gstreamer-app", "glib"]

playback/src/audio_backend/alsa.rs

Lines changed: 51 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
use super::{Open, Sink};
1+
use super::{Open, Sink, SinkAsBytes};
22
use crate::audio::AudioPacket;
3+
use crate::config::AudioFormat;
4+
use crate::player::{NUM_CHANNELS, SAMPLES_PER_SECOND, SAMPLE_RATE};
35
use alsa::device_name::HintIter;
46
use alsa::pcm::{Access, Format, Frames, HwParams, PCM};
57
use alsa::{Direction, Error, ValueOr};
@@ -8,13 +10,14 @@ use std::ffi::CString;
810
use std::io;
911
use std::process::exit;
1012

11-
const PREFERED_PERIOD_SIZE: Frames = 5512; // Period of roughly 125ms
13+
const BUFFERED_LATENCY: f32 = 0.125; // seconds
1214
const BUFFERED_PERIODS: Frames = 4;
1315

1416
pub struct AlsaSink {
1517
pcm: Option<PCM>,
18+
format: AudioFormat,
1619
device: String,
17-
buffer: Vec<i16>,
20+
buffer: Vec<u8>,
1821
}
1922

2023
fn list_outputs() {
@@ -34,23 +37,27 @@ fn list_outputs() {
3437
}
3538
}
3639

37-
fn open_device(dev_name: &str) -> Result<(PCM, Frames), Box<Error>> {
40+
fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box<Error>> {
3841
let pcm = PCM::new(dev_name, Direction::Playback, false)?;
39-
let mut period_size = PREFERED_PERIOD_SIZE;
42+
let alsa_format = match format {
43+
AudioFormat::F32 => Format::float(),
44+
AudioFormat::S32 => Format::s32(),
45+
AudioFormat::S24 => Format::s24(),
46+
AudioFormat::S24_3 => Format::S243LE,
47+
AudioFormat::S16 => Format::s16(),
48+
};
49+
4050
// http://www.linuxjournal.com/article/6735?page=0,1#N0x19ab2890.0x19ba78d8
4151
// latency = period_size * periods / (rate * bytes_per_frame)
42-
// For 16 Bit stereo data, one frame has a length of four bytes.
43-
// 500ms = buffer_size / (44100 * 4)
44-
// buffer_size_bytes = 0.5 * 44100 / 4
45-
// buffer_size_frames = 0.5 * 44100 = 22050
52+
// For stereo samples encoded as 32-bit float, one frame has a length of eight bytes.
53+
let mut period_size = ((SAMPLES_PER_SECOND * format.size() as u32) as f32
54+
* (BUFFERED_LATENCY / BUFFERED_PERIODS as f32)) as Frames;
4655
{
47-
// Set hardware parameters: 44100 Hz / Stereo / 16 bit
4856
let hwp = HwParams::any(&pcm)?;
49-
5057
hwp.set_access(Access::RWInterleaved)?;
51-
hwp.set_format(Format::s16())?;
52-
hwp.set_rate(44100, ValueOr::Nearest)?;
53-
hwp.set_channels(2)?;
58+
hwp.set_format(alsa_format)?;
59+
hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest)?;
60+
hwp.set_channels(NUM_CHANNELS as u32)?;
5461
period_size = hwp.set_period_size_near(period_size, ValueOr::Greater)?;
5562
hwp.set_buffer_size_near(period_size * BUFFERED_PERIODS)?;
5663
pcm.hw_params(&hwp)?;
@@ -64,12 +71,12 @@ fn open_device(dev_name: &str) -> Result<(PCM, Frames), Box<Error>> {
6471
}
6572

6673
impl Open for AlsaSink {
67-
fn open(device: Option<String>) -> AlsaSink {
68-
info!("Using alsa sink");
74+
fn open(device: Option<String>, format: AudioFormat) -> Self {
75+
info!("Using Alsa sink with format: {:?}", format);
6976

7077
let name = match device.as_ref().map(AsRef::as_ref) {
7178
Some("?") => {
72-
println!("Listing available alsa outputs");
79+
println!("Listing available Alsa outputs:");
7380
list_outputs();
7481
exit(0)
7582
}
@@ -78,8 +85,9 @@ impl Open for AlsaSink {
7885
}
7986
.to_string();
8087

81-
AlsaSink {
88+
Self {
8289
pcm: None,
90+
format: format,
8391
device: name,
8492
buffer: vec![],
8593
}
@@ -89,12 +97,14 @@ impl Open for AlsaSink {
8997
impl Sink for AlsaSink {
9098
fn start(&mut self) -> io::Result<()> {
9199
if self.pcm.is_none() {
92-
let pcm = open_device(&self.device);
100+
let pcm = open_device(&self.device, self.format);
93101
match pcm {
94102
Ok((p, period_size)) => {
95103
self.pcm = Some(p);
96104
// Create a buffer for all samples for a full period
97-
self.buffer = Vec::with_capacity((period_size * 2) as usize);
105+
self.buffer = Vec::with_capacity(
106+
period_size as usize * BUFFERED_PERIODS as usize * self.format.size(),
107+
);
98108
}
99109
Err(e) => {
100110
error!("Alsa error PCM open {}", e);
@@ -111,23 +121,22 @@ impl Sink for AlsaSink {
111121

112122
fn stop(&mut self) -> io::Result<()> {
113123
{
114-
let pcm = self.pcm.as_mut().unwrap();
115124
// Write any leftover data in the period buffer
116125
// before draining the actual buffer
117-
let io = pcm.io_i16().unwrap();
118-
match io.writei(&self.buffer[..]) {
119-
Ok(_) => (),
120-
Err(err) => pcm.try_recover(err, false).unwrap(),
121-
}
126+
self.write_bytes(&[]).expect("could not flush buffer");
127+
let pcm = self.pcm.as_mut().unwrap();
122128
pcm.drain().unwrap();
123129
}
124130
self.pcm = None;
125131
Ok(())
126132
}
127133

128-
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
134+
sink_as_bytes!();
135+
}
136+
137+
impl SinkAsBytes for AlsaSink {
138+
fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> {
129139
let mut processed_data = 0;
130-
let data = packet.samples();
131140
while processed_data < data.len() {
132141
let data_to_buffer = min(
133142
self.buffer.capacity() - self.buffer.len(),
@@ -137,16 +146,24 @@ impl Sink for AlsaSink {
137146
.extend_from_slice(&data[processed_data..processed_data + data_to_buffer]);
138147
processed_data += data_to_buffer;
139148
if self.buffer.len() == self.buffer.capacity() {
140-
let pcm = self.pcm.as_mut().unwrap();
141-
let io = pcm.io_i16().unwrap();
142-
match io.writei(&self.buffer) {
143-
Ok(_) => (),
144-
Err(err) => pcm.try_recover(err, false).unwrap(),
145-
}
149+
self.write_buf().expect("could not append to buffer");
146150
self.buffer.clear();
147151
}
148152
}
149153

150154
Ok(())
151155
}
152156
}
157+
158+
impl AlsaSink {
159+
fn write_buf(&mut self) -> io::Result<()> {
160+
let pcm = self.pcm.as_mut().unwrap();
161+
let io = pcm.io_bytes();
162+
match io.writei(&self.buffer) {
163+
Ok(_) => (),
164+
Err(err) => pcm.try_recover(err, false).unwrap(),
165+
};
166+
167+
Ok(())
168+
}
169+
}

0 commit comments

Comments
 (0)