Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f29e521
High-resolution volume control and normalisation
roderickvd Feb 24, 2021
1672eb8
Fix build on Rust < 1.50.0
roderickvd Mar 2, 2021
5257be7
Add command-line option to set F32 or S16 bit output
roderickvd Mar 12, 2021
6379926
Fix example
roderickvd Mar 12, 2021
a4ef174
Fix Alsa backend for 64-bit systems
roderickvd Mar 12, 2021
5f26a74
Add support for S32 output format
roderickvd Mar 13, 2021
309e264
Rename steepness to knee
roderickvd Mar 14, 2021
9dcaeee
Default to S16 output
roderickvd Mar 16, 2021
770ea15
Add support for S24 and S24_3 output formats
roderickvd Mar 16, 2021
b94879d
Fix GStreamer buffer pool size [ref #660 review]
roderickvd Mar 18, 2021
a1326ba
First round of refactoring
roderickvd Mar 18, 2021
001d3ca
Bump Alsa, cpal and GStreamer crates
roderickvd Mar 19, 2021
74b2fea
Refactor sample conversion into separate struct
roderickvd Mar 21, 2021
bfca1ec
Minor code improvements and crates bump
roderickvd Mar 27, 2021
cdbce21
Make S16 to F32 sample conversion less magical
roderickvd Mar 27, 2021
a200b25
Fix formatting
roderickvd Mar 27, 2021
cc60dc1
Fix buffer size in JACK Audio backend
roderickvd Mar 27, 2021
d252eee
Warn about broken backends
roderickvd Mar 27, 2021
07d710e
Use AudioFormat size for SDL
roderickvd Mar 31, 2021
78bc621
Move SamplesConverter into convert.rs
roderickvd Apr 5, 2021
928a673
DRY up constructors
roderickvd Apr 5, 2021
d0ea963
Optimize requantizer to work in `f32`, then round
roderickvd Apr 9, 2021
222f9bb
Bump playback crates to the latest supporting Rust 1.41.1
roderickvd Apr 9, 2021
e20b96c
Merge remote-tracking branch 'upstream/dev' into hi-res-volume-control
roderickvd Apr 9, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
609 changes: 64 additions & 545 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions audio/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ log = "0.4"
num-bigint = "0.3"
num-traits = "0.2"
tempfile = "3.1"
zerocopy = "0.3"

librespot-tremor = { version = "0.2.0", optional = true }
vorbis = { version ="0.0.14", optional = true }
librespot-tremor = { version = "0.2", optional = true }
vorbis = { version ="0.0", optional = true }

[features]
with-tremor = ["librespot-tremor"]
Expand Down
59 changes: 59 additions & 0 deletions audio/src/convert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use zerocopy::AsBytes;

#[derive(AsBytes, Copy, Clone, Debug)]
#[allow(non_camel_case_types)]
#[repr(transparent)]
pub struct i24([u8; 3]);
impl i24 {
fn pcm_from_i32(sample: i32) -> Self {
// drop the least significant byte
let [a, b, c, _d] = (sample >> 8).to_le_bytes();
i24([a, b, c])
}
}

// Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX] while maintaining DC linearity.
macro_rules! convert_samples_to {
($type: ident, $samples: expr) => {
convert_samples_to!($type, $samples, 0)
};
($type: ident, $samples: expr, $drop_bits: expr) => {
$samples
.iter()
.map(|sample| {
// Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX]
// while maintaining DC linearity. There is nothing to be gained
// by doing this in f64, as the significand of a f32 is 24 bits,
// just like the maximum bit depth we are converting to.
let int_value = *sample * (std::$type::MAX as f32 + 0.5) - 0.5;

// Casting floats to ints truncates by default, which results
// in larger quantization error than rounding arithmetically.
// Flooring is faster, but again with larger error.
int_value.round() as $type >> $drop_bits
})
.collect()
};
}

pub struct SamplesConverter {}
impl SamplesConverter {
pub fn to_s32(samples: &[f32]) -> Vec<i32> {
convert_samples_to!(i32, samples)
}

pub fn to_s24(samples: &[f32]) -> Vec<i32> {
convert_samples_to!(i32, samples, 8)
}

pub fn to_s24_3(samples: &[f32]) -> Vec<i24> {
Self::to_s32(samples)
.iter()
.map(|sample| i24::pcm_from_i32(*sample))
.collect()
}

pub fn to_s16(samples: &[f32]) -> Vec<i16> {
convert_samples_to!(i16, samples)
}
}
7 changes: 5 additions & 2 deletions audio/src/lewton_decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)) => (),
Expand Down
6 changes: 4 additions & 2 deletions audio/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ extern crate tempfile;

extern crate librespot_core;

mod convert;
mod decrypt;
mod fetch;

Expand All @@ -24,6 +25,7 @@ mod passthrough_decoder;

mod range_set;

pub use convert::{i24, SamplesConverter};
pub use decrypt::AudioDecrypt;
pub use fetch::{AudioFile, AudioFileOpen, StreamLoaderController};
pub use fetch::{
Expand All @@ -33,12 +35,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"),
Expand Down
13 changes: 12 additions & 1 deletion audio/src/libvorbis_decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,18 @@ 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) / (std::i16::MAX as f64 + 0.5)) as f32
})
.collect(),
)));
}
None => return Ok(None),

Some(Err(vorbis::VorbisError::Hole)) => (),
Expand Down
5 changes: 3 additions & 2 deletions examples/play.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use librespot::core::authentication::Credentials;
use librespot::core::config::SessionConfig;
use librespot::core::session::Session;
use librespot::core::spotify_id::SpotifyId;
use librespot::playback::config::PlayerConfig;
use librespot::playback::config::{AudioFormat, PlayerConfig};

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

let session_config = SessionConfig::default();
let player_config = PlayerConfig::default();
let audio_format = AudioFormat::default();

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

let (mut player, _) = Player::new(player_config, session.clone(), None, move || {
(backend)(None)
(backend)(None, audio_format)
});

player.load(track, true, 0);
Expand Down
12 changes: 6 additions & 6 deletions playback/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,19 @@ log = "0.4"
byteorder = "1.3"
shell-words = "1.0.0"

alsa = { version = "0.4", optional = true }
alsa = { version = "0.5", optional = true }
portaudio-rs = { version = "0.3", optional = true }
libpulse-binding = { version = "2.13", optional = true, default-features = false }
libpulse-simple-binding = { version = "2.13", optional = true, default-features = false }
libpulse-binding = { version = "2", optional = true, default-features = false }
libpulse-simple-binding = { version = "2", optional = true, default-features = false }
jack = { version = "0.6", optional = true }
libc = { version = "0.2", optional = true }
rodio = { version = "0.13", optional = true, default-features = false }
cpal = { version = "0.13", optional = true }
sdl2 = { version = "0.34", optional = true }
sdl2 = { version = "0.34.3", optional = true }
gstreamer = { version = "0.16", optional = true }
gstreamer-app = { version = "0.16", optional = true }
glib = { version = "0.10", optional = true }
zerocopy = { version = "0.3", optional = true }
zerocopy = { version = "0.3" }

[features]
alsa-backend = ["alsa"]
Expand All @@ -45,4 +45,4 @@ jackaudio-backend = ["jack"]
rodiojack-backend = ["rodio", "cpal/jack"]
rodio-backend = ["rodio", "cpal"]
sdl-backend = ["sdl2"]
gstreamer-backend = ["gstreamer", "gstreamer-app", "glib", "zerocopy"]
gstreamer-backend = ["gstreamer", "gstreamer-app", "glib"]
85 changes: 51 additions & 34 deletions playback/src/audio_backend/alsa.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use super::{Open, Sink};
use super::{Open, Sink, SinkAsBytes};
use crate::audio::AudioPacket;
use crate::config::AudioFormat;
use crate::player::{NUM_CHANNELS, SAMPLES_PER_SECOND, SAMPLE_RATE};
use alsa::device_name::HintIter;
use alsa::pcm::{Access, Format, Frames, HwParams, PCM};
use alsa::{Direction, Error, ValueOr};
Expand All @@ -8,13 +10,14 @@ use std::ffi::CString;
use std::io;
use std::process::exit;

const PREFERED_PERIOD_SIZE: Frames = 5512; // Period of roughly 125ms
const BUFFERED_LATENCY: f32 = 0.125; // seconds
const BUFFERED_PERIODS: Frames = 4;

pub struct AlsaSink {
pcm: Option<PCM>,
format: AudioFormat,
device: String,
buffer: Vec<i16>,
buffer: Vec<u8>,
}

fn list_outputs() {
Expand All @@ -34,23 +37,27 @@ fn list_outputs() {
}
}

fn open_device(dev_name: &str) -> 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 mut period_size = PREFERED_PERIOD_SIZE;
let alsa_format = match format {
AudioFormat::F32 => Format::float(),
AudioFormat::S32 => Format::s32(),
AudioFormat::S24 => Format::s24(),
AudioFormat::S24_3 => Format::S243LE,
AudioFormat::S16 => Format::s16(),
};

// 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
// buffer_size_frames = 0.5 * 44100 = 22050
// For stereo samples encoded as 32-bit float, one frame has a length of eight bytes.
let mut period_size = ((SAMPLES_PER_SECOND * format.size() as u32) as f32
* (BUFFERED_LATENCY / BUFFERED_PERIODS as f32)) as Frames;
{
// Set hardware parameters: 44100 Hz / Stereo / 16 bit
let hwp = HwParams::any(&pcm)?;

hwp.set_access(Access::RWInterleaved)?;
hwp.set_format(Format::s16())?;
hwp.set_rate(44100, ValueOr::Nearest)?;
hwp.set_channels(2)?;
hwp.set_format(alsa_format)?;
hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest)?;
hwp.set_channels(NUM_CHANNELS as u32)?;
period_size = hwp.set_period_size_near(period_size, ValueOr::Greater)?;
hwp.set_buffer_size_near(period_size * BUFFERED_PERIODS)?;
pcm.hw_params(&hwp)?;
Expand All @@ -64,12 +71,12 @@ fn open_device(dev_name: &str) -> Result<(PCM, Frames), Box<Error>> {
}

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

let name = match device.as_ref().map(AsRef::as_ref) {
Some("?") => {
println!("Listing available alsa outputs");
println!("Listing available Alsa outputs:");
list_outputs();
exit(0)
}
Expand All @@ -78,8 +85,9 @@ impl Open for AlsaSink {
}
.to_string();

AlsaSink {
Self {
pcm: None,
format: format,
device: name,
buffer: vec![],
}
Expand All @@ -89,12 +97,14 @@ impl Open for AlsaSink {
impl Sink for AlsaSink {
fn start(&mut self) -> io::Result<()> {
if self.pcm.is_none() {
let pcm = open_device(&self.device);
let pcm = open_device(&self.device, self.format);
match pcm {
Ok((p, period_size)) => {
self.pcm = Some(p);
// Create a buffer for all samples for a full period
self.buffer = Vec::with_capacity((period_size * 2) as usize);
self.buffer = Vec::with_capacity(
period_size as usize * BUFFERED_PERIODS as usize * self.format.size(),
);
}
Err(e) => {
error!("Alsa error PCM open {}", e);
Expand All @@ -111,23 +121,22 @@ impl Sink for AlsaSink {

fn stop(&mut self) -> io::Result<()> {
{
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();
match io.writei(&self.buffer[..]) {
Ok(_) => (),
Err(err) => pcm.try_recover(err, false).unwrap(),
}
self.write_bytes(&[]).expect("could not flush buffer");
let pcm = self.pcm.as_mut().unwrap();
pcm.drain().unwrap();
}
self.pcm = None;
Ok(())
}

fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
sink_as_bytes!();
}

impl SinkAsBytes for AlsaSink {
fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> {
let mut processed_data = 0;
let data = packet.samples();
while processed_data < data.len() {
let data_to_buffer = min(
self.buffer.capacity() - self.buffer.len(),
Expand All @@ -137,16 +146,24 @@ impl Sink for AlsaSink {
.extend_from_slice(&data[processed_data..processed_data + data_to_buffer]);
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();
match io.writei(&self.buffer) {
Ok(_) => (),
Err(err) => pcm.try_recover(err, false).unwrap(),
}
self.write_buf().expect("could not append to buffer");
self.buffer.clear();
}
}

Ok(())
}
}

impl AlsaSink {
fn write_buf(&mut self) -> io::Result<()> {
let pcm = self.pcm.as_mut().unwrap();
let io = pcm.io_bytes();
match io.writei(&self.buffer) {
Ok(_) => (),
Err(err) => pcm.try_recover(err, false).unwrap(),
};

Ok(())
}
}
Loading