Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions audio/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ 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 }
Expand Down
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
51 changes: 49 additions & 2 deletions audio/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,42 @@ pub use fetch::{
READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS,
};
use std::fmt;
use zerocopy::AsBytes;

pub enum AudioPacket {
Samples(Vec<i16>),
Samples(Vec<f32>),
OggData(Vec<u8>),
}

#[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, $shift: expr) => {
$samples
.iter()
.map(|sample| {
(*sample as f64 * (std::$type::MAX as f64 + 0.5) - 0.5) as $type >> $shift
})
.collect()
};
}

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 All @@ -58,6 +86,25 @@ impl AudioPacket {
AudioPacket::OggData(d) => d.is_empty(),
}
}

pub fn f32_to_s32(samples: &[f32]) -> Vec<i32> {
Comment thread
roderickvd marked this conversation as resolved.
Outdated
convert_samples_to!(i32, samples)
}

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

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

pub fn f32_to_s16(samples: &[f32]) -> Vec<i16> {
convert_samples_to!(i16, samples)
}
}

#[cfg(not(any(feature = "with-tremor", feature = "with-vorbis")))]
Expand Down
17 changes: 16 additions & 1 deletion audio/src/libvorbis_decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,22 @@ 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| {
if *sample == 0 {
0.0
} else {
((*sample as f64 + 0.5) / (0x7FFF 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
4 changes: 2 additions & 2 deletions playback/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ sdl2 = { version = "0.34", 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" ]
87 changes: 53 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,29 @@ 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 / 32-bit float or 16-bit signed integer
{
// 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 +73,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) -> AlsaSink {
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 +87,9 @@ impl Open for AlsaSink {
}
.to_string();

AlsaSink {
Self {
pcm: None,
format: format,
device: name,
buffer: vec![],
}
Expand All @@ -89,12 +99,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 +123,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 +148,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