Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement dithering #694

Merged
merged 29 commits into from
May 26, 2021
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c8ec268
Implement dithering and noise shaping
roderickvd Apr 13, 2021
f3553e1
cargo fmt
roderickvd Apr 13, 2021
fde697b
Fix example
roderickvd Apr 13, 2021
977dbed
Correct dithering noise powers
roderickvd Apr 14, 2021
6ea089c
Disable noise shaping by default
roderickvd Apr 14, 2021
00b36be
Document default ditherer and noise shaper
roderickvd Apr 14, 2021
f7ac001
Fix panic when no noise shaper is specified
roderickvd Apr 14, 2021
636d181
Implement fmt::Display for Ditherer and NoiseShaper
roderickvd Apr 14, 2021
2f11bbc
Refactor name() into &'static str
roderickvd Apr 15, 2021
34abd0d
Merge remote-tracking branch 'upstream/dev' into dithering-and-noise-…
roderickvd Apr 15, 2021
5dec737
Simplify name macro
roderickvd Apr 15, 2021
c995088
Move dithering and noise shaping to PlayerConfig
roderickvd Apr 19, 2021
5ee2edd
fix examples
roderickvd Apr 19, 2021
2ab4136
Fix high pass ditherer on interleaved samples
roderickvd Apr 28, 2021
b5ea6cc
Fix dithering and noise shaping on 24-bit formats
roderickvd Apr 30, 2021
3c527e4
Refactor sample conversion into `Requantizer` struct
roderickvd Apr 30, 2021
2f2c2ca
Merge remote-tracking branch 'upstream/dev' into dithering-and-noise-…
roderickvd Apr 30, 2021
cf8e8e3
Merge remote-tracking branch 'upstream/dev' into dithering-and-noise-…
roderickvd May 11, 2021
6f0f9bc
Update changelog
roderickvd May 11, 2021
418e44b
Fix some clippy lints
roderickvd May 11, 2021
de77487
Merge remote-tracking branch 'upstream/dev' into dithering-and-noise-…
roderickvd May 16, 2021
703b667
Merge remote-tracking branch 'upstream/dev' into dithering-and-noise-…
roderickvd May 18, 2021
f7bcb49
Clean up API
roderickvd May 19, 2021
68f409c
Match reference Vorbis sample conversion technique
roderickvd May 21, 2021
35296bf
Simplify and cut down on features
roderickvd May 23, 2021
3b5519d
Update changelog
roderickvd May 23, 2021
8454a2b
Refactor getting default ditherer
roderickvd May 24, 2021
deb1715
Don't dither twice on PortAudio and GStreamer
roderickvd May 25, 2021
1879499
Merge branch 'dev' into dithering-and-noise-shaping
roderickvd May 26, 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
10 changes: 5 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since v0.2.0.

## [Unreleased]
### Added
- [playback] Add support for dithering with `--dither` for lower requantization error (breaking)

### Removed

* [librespot-audio] Removed `VorbisDecoder`, `VorbisError`, `AudioPacket`, `PassthroughDecoder`, `PassthroughError`, `AudioError`, `AudioDecoder` and the `convert` module from `librespot_audio`. The underlying crates `vorbis`, `librespot-tremor`, `lewton` and `ogg` should be used directly.
### Changed
* [audio, playback] Moved `VorbisDecoder`, `VorbisError`, `AudioPacket`, `PassthroughDecoder`, `PassthroughError`, `AudioError`, `AudioDecoder` and the `convert` module from `librespot-audio` to `librespot-playback`. The underlying crates `vorbis`, `librespot-tremor`, `lewton` and `ogg` should be used directly. (breaking)

### Fixed

* [librespot-playback] Incorrect `PlayerConfig::default().normalisation_threshold` caused distortion when using dynamic volume normalisation downstream
* [playback] Incorrect `PlayerConfig::default().normalisation_threshold` caused distortion when using dynamic volume normalisation downstream

## [0.2.0] - 2021-05-04

Expand Down
21 changes: 19 additions & 2 deletions Cargo.lock

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

4 changes: 4 additions & 0 deletions playback/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ librespot-tremor = { version = "0.2", optional = true }
ogg = "0.8"
vorbis = { version ="0.0", optional = true }

# Dithering
rand = "0.8"
rand_distr = "0.4"

[features]
alsa-backend = ["alsa"]
portaudio-backend = ["portaudio-rs"]
Expand Down
1 change: 1 addition & 0 deletions playback/src/audio_backend/alsa.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::{Open, Sink, SinkAsBytes};
use crate::config::AudioFormat;
use crate::convert::Converter;
use crate::decoder::AudioPacket;
use crate::player::{NUM_CHANNELS, SAMPLES_PER_SECOND, SAMPLE_RATE};
use alsa::device_name::HintIter;
Expand Down
2 changes: 1 addition & 1 deletion playback/src/audio_backend/gstreamer.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::{Open, Sink, SinkAsBytes};
use crate::config::AudioFormat;
use crate::convert::Converter;
use crate::decoder::AudioPacket;
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};

Expand Down Expand Up @@ -120,7 +121,6 @@ impl Open for GstreamerSink {
}

impl Sink for GstreamerSink {
start_stop_noop!();
sink_as_bytes!();
}

Expand Down
5 changes: 2 additions & 3 deletions playback/src/audio_backend/jackaudio.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::{Open, Sink};
use crate::config::AudioFormat;
use crate::convert::Converter;
use crate::decoder::AudioPacket;
use crate::player::NUM_CHANNELS;
use jack::{
Expand Down Expand Up @@ -69,9 +70,7 @@ impl Open for JackSink {
}

impl Sink for JackSink {
start_stop_noop!();

fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
fn write(&mut self, packet: &AudioPacket, _: &mut Converter) -> io::Result<()> {
for s in packet.samples().iter() {
let res = self.send.send(*s);
if res.is_err() {
Expand Down
38 changes: 16 additions & 22 deletions playback/src/audio_backend/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::config::AudioFormat;
use crate::convert::Converter;
use crate::decoder::AudioPacket;
use std::io;

Expand All @@ -7,9 +8,13 @@ pub trait Open {
}

pub trait Sink {
fn start(&mut self) -> io::Result<()>;
fn stop(&mut self) -> io::Result<()>;
fn write(&mut self, packet: &AudioPacket) -> io::Result<()>;
fn start(&mut self) -> io::Result<()> {
Ok(())
}
fn stop(&mut self) -> io::Result<()> {
Ok(())
}
fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()>;
}

pub type SinkBuilder = fn(Option<String>, AudioFormat) -> Box<dyn Sink>;
Expand All @@ -25,26 +30,26 @@ fn mk_sink<S: Sink + Open + 'static>(device: Option<String>, format: AudioFormat
// reuse code for various backends
macro_rules! sink_as_bytes {
() => {
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
use crate::convert::{self, i24};
fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> {
use crate::convert::i24;
use zerocopy::AsBytes;
match packet {
AudioPacket::Samples(samples) => match self.format {
AudioFormat::F32 => self.write_bytes(samples.as_bytes()),
AudioFormat::S32 => {
let samples_s32: &[i32] = &convert::to_s32(samples);
let samples_s32: &[i32] = &converter.f32_to_s32(samples);
self.write_bytes(samples_s32.as_bytes())
}
AudioFormat::S24 => {
let samples_s24: &[i32] = &convert::to_s24(samples);
let samples_s24: &[i32] = &converter.f32_to_s24(samples);
self.write_bytes(samples_s24.as_bytes())
}
AudioFormat::S24_3 => {
let samples_s24_3: &[i24] = &convert::to_s24_3(samples);
let samples_s24_3: &[i24] = &converter.f32_to_s24_3(samples);
self.write_bytes(samples_s24_3.as_bytes())
}
AudioFormat::S16 => {
let samples_s16: &[i16] = &convert::to_s16(samples);
let samples_s16: &[i16] = &converter.f32_to_s16(samples);
self.write_bytes(samples_s16.as_bytes())
}
},
Expand All @@ -54,17 +59,6 @@ macro_rules! sink_as_bytes {
};
}

macro_rules! start_stop_noop {
() => {
fn start(&mut self) -> io::Result<()> {
Ok(())
}
fn stop(&mut self) -> io::Result<()> {
Ok(())
}
};
}

#[cfg(feature = "alsa-backend")]
mod alsa;
#[cfg(feature = "alsa-backend")]
Expand Down Expand Up @@ -105,6 +99,8 @@ mod subprocess;
use self::subprocess::SubprocessSink;

pub const BACKENDS: &[(&str, SinkBuilder)] = &[
#[cfg(feature = "rodio-backend")]
("rodio", rodio::mk_rodio), // default goes first
#[cfg(feature = "alsa-backend")]
("alsa", mk_sink::<AlsaSink>),
#[cfg(feature = "portaudio-backend")]
Expand All @@ -115,8 +111,6 @@ pub const BACKENDS: &[(&str, SinkBuilder)] = &[
("jackaudio", mk_sink::<JackSink>),
#[cfg(feature = "gstreamer-backend")]
("gstreamer", mk_sink::<GstreamerSink>),
#[cfg(feature = "rodio-backend")]
("rodio", rodio::mk_rodio),
#[cfg(feature = "rodiojack-backend")]
("rodiojack", rodio::mk_rodiojack),
#[cfg(feature = "sdl-backend")]
Expand Down
2 changes: 1 addition & 1 deletion playback/src/audio_backend/pipe.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::{Open, Sink, SinkAsBytes};
use crate::config::AudioFormat;
use crate::convert::Converter;
use crate::decoder::AudioPacket;
use std::fs::OpenOptions;
use std::io::{self, Write};
Expand All @@ -23,7 +24,6 @@ impl Open for StdoutSink {
}

impl Sink for StdoutSink {
start_stop_noop!();
sink_as_bytes!();
}

Expand Down
14 changes: 7 additions & 7 deletions playback/src/audio_backend/portaudio.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::{Open, Sink};
use crate::config::AudioFormat;
use crate::convert;
use crate::convert::Converter;
use crate::decoder::AudioPacket;
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo};
Expand Down Expand Up @@ -136,15 +136,15 @@ impl<'a> Sink for PortAudioSink<'a> {
}};
}
match self {
Self::F32(stream, _parameters) => stop_sink!(ref mut stream),
Self::S32(stream, _parameters) => stop_sink!(ref mut stream),
Self::S16(stream, _parameters) => stop_sink!(ref mut stream),
Self::F32(stream, _) => stop_sink!(ref mut stream),
Self::S32(stream, _) => stop_sink!(ref mut stream),
Self::S16(stream, _) => stop_sink!(ref mut stream),
};

Ok(())
}

fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> {
macro_rules! write_sink {
(ref mut $stream: expr, $samples: expr) => {
$stream.as_mut().unwrap().write($samples)
Expand All @@ -157,11 +157,11 @@ impl<'a> Sink for PortAudioSink<'a> {
write_sink!(ref mut stream, samples)
}
Self::S32(stream, _parameters) => {
let samples_s32: &[i32] = &convert::to_s32(samples);
let samples_s32: &[i32] = &converter.f32_to_s32(samples);
write_sink!(ref mut stream, samples_s32)
}
Self::S16(stream, _parameters) => {
let samples_s16: &[i16] = &convert::to_s16(samples);
let samples_s16: &[i16] = &converter.f32_to_s16(samples);
write_sink!(ref mut stream, samples_s16)
}
};
Expand Down
1 change: 1 addition & 0 deletions playback/src/audio_backend/pulseaudio.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::{Open, Sink, SinkAsBytes};
use crate::config::AudioFormat;
use crate::convert::Converter;
use crate::decoder::AudioPacket;
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
use libpulse_binding::{self as pulse, stream::Direction};
Expand Down
8 changes: 3 additions & 5 deletions playback/src/audio_backend/rodio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use thiserror::Error;

use super::Sink;
use crate::config::AudioFormat;
use crate::convert;
use crate::convert::Converter;
use crate::decoder::AudioPacket;
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};

Expand Down Expand Up @@ -174,9 +174,7 @@ pub fn open(host: cpal::Host, device: Option<String>, format: AudioFormat) -> Ro
}

impl Sink for RodioSink {
start_stop_noop!();

fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> {
let samples = packet.samples();
match self.format {
AudioFormat::F32 => {
Expand All @@ -185,7 +183,7 @@ impl Sink for RodioSink {
self.rodio_sink.append(source);
}
AudioFormat::S16 => {
let samples_s16: &[i16] = &convert::to_s16(samples);
let samples_s16: &[i16] = &converter.f32_to_s16(samples);
let source = rodio::buffer::SamplesBuffer::new(
NUM_CHANNELS as u16,
SAMPLE_RATE,
Expand Down
8 changes: 4 additions & 4 deletions playback/src/audio_backend/sdl.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::{Open, Sink};
use crate::config::AudioFormat;
use crate::convert;
use crate::convert::Converter;
use crate::decoder::AudioPacket;
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
use sdl2::audio::{AudioQueue, AudioSpecDesired};
Expand Down Expand Up @@ -81,7 +81,7 @@ impl Sink for SdlSink {
Ok(())
}

fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> {
macro_rules! drain_sink {
($queue: expr, $size: expr) => {{
// sleep and wait for sdl thread to drain the queue a bit
Expand All @@ -98,12 +98,12 @@ impl Sink for SdlSink {
queue.queue(samples)
}
Self::S32(queue) => {
let samples_s32: &[i32] = &convert::to_s32(samples);
let samples_s32: &[i32] = &converter.f32_to_s32(samples);
drain_sink!(queue, AudioFormat::S32.size());
queue.queue(samples_s32)
}
Self::S16(queue) => {
let samples_s16: &[i16] = &convert::to_s16(samples);
let samples_s16: &[i16] = &converter.f32_to_s16(samples);
drain_sink!(queue, AudioFormat::S16.size());
queue.queue(samples_s16)
}
Expand Down
1 change: 1 addition & 0 deletions playback/src/audio_backend/subprocess.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::{Open, Sink, SinkAsBytes};
use crate::config::AudioFormat;
use crate::convert::Converter;
use crate::decoder::AudioPacket;
use shell_words::split;

Expand Down
13 changes: 10 additions & 3 deletions playback/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::player::NormalisationData;
use crate::convert::i24;
pub use crate::dither::{Ditherer, DithererBuilder};

use std::convert::TryFrom;
use std::mem;
Expand Down Expand Up @@ -117,9 +118,12 @@ impl Default for NormalisationMethod {
}
}

#[derive(Clone, Debug)]
#[derive(Clone)]
pub struct PlayerConfig {
pub bitrate: Bitrate,
pub gapless: bool,
pub passthrough: bool,

pub normalisation: bool,
pub normalisation_type: NormalisationType,
pub normalisation_method: NormalisationMethod,
Expand All @@ -128,8 +132,10 @@ pub struct PlayerConfig {
pub normalisation_attack: f32,
pub normalisation_release: f32,
pub normalisation_knee: f32,
pub gapless: bool,
pub passthrough: bool,

// pass function pointers so they can be lazily instantiated *after* spawning a thread
// (thereby circumventing Send bounds that they might not satisfy)
pub ditherer: Option<DithererBuilder>,
}

impl Default for PlayerConfig {
Expand All @@ -146,6 +152,7 @@ impl Default for PlayerConfig {
normalisation_knee: 1.0,
gapless: true,
passthrough: false,
ditherer: Some(<dyn Ditherer>::default()),
}
}
}
Loading