From 09e506ed668c582f4a9c6af928465d222c967392 Mon Sep 17 00:00:00 2001 From: johannesd3 Date: Tue, 9 Feb 2021 19:42:56 +0100 Subject: [PATCH 01/46] Replace version functions by constants --- connect/src/spirc.rs | 2 +- core/src/config.rs | 2 +- core/src/connection/mod.rs | 6 ++--- core/src/version.rs | 50 ++++++++------------------------------ src/main.rs | 10 ++++---- 5 files changed, 20 insertions(+), 50 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 352a3fcf..33d74366 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -102,7 +102,7 @@ fn initial_state() -> State { fn initial_device_state(config: ConnectConfig) -> DeviceState { { let mut msg = DeviceState::new(); - msg.set_sw_version(version::version_string()); + msg.set_sw_version(version::VERSION_STRING.to_string()); msg.set_is_active(false); msg.set_can_play(true); msg.set_volume(0); diff --git a/core/src/config.rs b/core/src/config.rs index 12c1b2ed..def30f6d 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -17,7 +17,7 @@ impl Default for SessionConfig { fn default() -> SessionConfig { let device_id = Uuid::new_v4().to_hyphenated().to_string(); SessionConfig { - user_agent: version::version_string(), + user_agent: version::VERSION_STRING.to_string(), device_id: device_id, proxy: None, ap_port: None, diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index ffc6d0f4..eccd5355 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -88,11 +88,11 @@ pub fn authenticate( .mut_system_info() .set_system_information_string(format!( "librespot_{}_{}", - version::short_sha(), - version::build_id() + version::SHA_SHORT, + version::BUILD_ID )); packet.mut_system_info().set_device_id(device_id); - packet.set_version_string(version::version_string()); + packet.set_version_string(version::VERSION_STRING.to_string()); let cmd = 0xab; let data = packet.write_to_bytes().unwrap(); diff --git a/core/src/version.rs b/core/src/version.rs index cd7fa042..d15f296b 100644 --- a/core/src/version.rs +++ b/core/src/version.rs @@ -1,44 +1,14 @@ -pub fn version_string() -> String { - format!("librespot-{}", short_sha()) -} +/// Version string of the form "librespot-" +pub const VERSION_STRING: &str = concat!("librespot-", env!("VERGEN_SHA_SHORT")); -// Generate a timestamp representing now (UTC) in RFC3339 format. -pub fn now() -> &'static str { - env!("VERGEN_BUILD_TIMESTAMP") -} +/// Generate a timstamp string representing now (UTC). +pub const BUILD_DATE: &str = env!("VERGEN_BUILD_DATE"); -// Generate a timstamp string representing now (UTC). -pub fn short_now() -> &'static str { - env!("VERGEN_BUILD_DATE") -} +/// Short sha of the latest git commit. +pub const SHA_SHORT: &str = env!("VERGEN_SHA_SHORT"); -// Generate a SHA string -pub fn sha() -> &'static str { - env!("VERGEN_SHA") -} +/// Date of the latest git commit. +pub const COMMIT_DATE: &str = env!("VERGEN_COMMIT_DATE"); -// Generate a short SHA string -pub fn short_sha() -> &'static str { - env!("VERGEN_SHA_SHORT") -} - -// Generate the commit date string -pub fn commit_date() -> &'static str { - env!("VERGEN_COMMIT_DATE") -} - -// Generate the target triple string -pub fn target() -> &'static str { - env!("VERGEN_TARGET_TRIPLE") -} - -// Generate a semver string -pub fn semver() -> &'static str { - // env!("VERGEN_SEMVER") - env!("CARGO_PKG_VERSION") -} - -// Generate a random build id. -pub fn build_id() -> &'static str { - env!("VERGEN_BUILD_ID") -} +/// A random build id. +pub const BUILD_ID: &str = env!("VERGEN_BUILD_ID"); diff --git a/src/main.rs b/src/main.rs index 4c57808f..b1101098 100644 --- a/src/main.rs +++ b/src/main.rs @@ -225,10 +225,10 @@ fn setup(args: &[String]) -> Setup { info!( "librespot {} ({}). Built on {}. Build ID: {}", - version::short_sha(), - version::commit_date(), - version::short_now(), - version::build_id() + version::SHA_SHORT, + version::COMMIT_DATE, + version::BUILD_DATE, + version::BUILD_ID ); let backend_name = matches.opt_str("backend"); @@ -329,7 +329,7 @@ fn setup(args: &[String]) -> Setup { let device_id = device_id(&name); SessionConfig { - user_agent: version::version_string(), + user_agent: version::VERSION_STRING.to_string(), device_id: device_id, proxy: matches.opt_str("proxy").or(std::env::var("http_proxy").ok()).map( |s| { From 85be0d075a93d167476b355c4a93602c808ba689 Mon Sep 17 00:00:00 2001 From: johannesd3 Date: Fri, 12 Feb 2021 20:21:07 +0100 Subject: [PATCH 02/46] Adjust documentation --- core/src/version.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/version.rs b/core/src/version.rs index d15f296b..62778450 100644 --- a/core/src/version.rs +++ b/core/src/version.rs @@ -1,7 +1,7 @@ /// Version string of the form "librespot-" pub const VERSION_STRING: &str = concat!("librespot-", env!("VERGEN_SHA_SHORT")); -/// Generate a timstamp string representing now (UTC). +/// Generate a timestamp string representing the build date (UTC). pub const BUILD_DATE: &str = env!("VERGEN_BUILD_DATE"); /// Short sha of the latest git commit. From bce4858b9e0bd5308376dcba90c17f14c74a969c Mon Sep 17 00:00:00 2001 From: johannesd3 Date: Sat, 13 Feb 2021 18:43:24 +0100 Subject: [PATCH 03/46] Add semver constant, rename "build id" env var --- core/build.rs | 3 ++- core/src/version.rs | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/core/build.rs b/core/build.rs index e8c71e4a..4877ff46 100644 --- a/core/build.rs +++ b/core/build.rs @@ -15,5 +15,6 @@ fn main() { .map(|()| rng.sample(Alphanumeric)) .take(8) .collect(); - println!("cargo:rustc-env=VERGEN_BUILD_ID={}", build_id); + + println!("cargo:rustc-env=LIBRESPOT_BUILD_ID={}", build_id); } diff --git a/core/src/version.rs b/core/src/version.rs index 62778450..ef553463 100644 --- a/core/src/version.rs +++ b/core/src/version.rs @@ -10,5 +10,8 @@ pub const SHA_SHORT: &str = env!("VERGEN_SHA_SHORT"); /// Date of the latest git commit. pub const COMMIT_DATE: &str = env!("VERGEN_COMMIT_DATE"); +/// Librespot crate version. +pub const SEMVER: &str = env!("CARGO_PKG_VERSION"); + /// A random build id. -pub const BUILD_ID: &str = env!("VERGEN_BUILD_ID"); +pub const BUILD_ID: &str = env!("LIBRESPOT_BUILD_ID"); From c8e45ab6903d299d46acaee71d91ceec63a09457 Mon Sep 17 00:00:00 2001 From: Johannes Dertmann Date: Wed, 17 Feb 2021 15:13:57 +0100 Subject: [PATCH 04/46] Modified startup message --- src/main.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index b1101098..f1c02946 100644 --- a/src/main.rs +++ b/src/main.rs @@ -224,11 +224,11 @@ fn setup(args: &[String]) -> Setup { setup_logging(verbose); info!( - "librespot {} ({}). Built on {}. Build ID: {}", - version::SHA_SHORT, - version::COMMIT_DATE, - version::BUILD_DATE, - version::BUILD_ID + "librespot {semver} {sha} (Built on {build_date}, Build ID: {build_id})", + semver = version::SEMVER, + sha = version::SHA_SHORT, + build_date = version::BUILD_DATE, + build_id = version::BUILD_ID ); let backend_name = matches.opt_str("backend"); From 58bd339e90ddf6f8a53dd23f8deb00392b9868af Mon Sep 17 00:00:00 2001 From: Johannes Dertmann Date: Thu, 18 Feb 2021 09:53:46 +0100 Subject: [PATCH 05/46] Restore MSRV to 1.41 --- .github/workflows/test.yml | 2 +- COMPILING.md | 2 +- Cargo.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4ad4b406..adbd10c4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ jobs: matrix: os: [ubuntu-latest] toolchain: - - 1.42.0 # MSRV (Minimum supported rust version) + - 1.41.1 # MSRV (Minimum supported rust version) - stable - beta experimental: [false] diff --git a/COMPILING.md b/COMPILING.md index 40eefb39..4320cdbb 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -13,7 +13,7 @@ curl https://sh.rustup.rs -sSf | sh Follow any prompts it gives you to install Rust. Once that’s done, Rust's standard tools should be setup and ready to use. -*Note: The current minimum required Rust version at the time of writing is 1.40.0, you can find the current minimum version specified in the `.github/workflow/test.yml` file.* +*Note: The current minimum required Rust version at the time of writing is 1.41, you can find the current minimum version specified in the `.github/workflow/test.yml` file.* #### Additional Rust tools - `rustfmt` To ensure a consistent codebase, we utilise [`rustfmt`](https://github.com/rust-lang/rustfmt), which is installed by default with `rustup` these days, else it can be installed manually with: diff --git a/Cargo.lock b/Cargo.lock index efaae37a..9a2e42ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,9 +117,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "backtrace" -version = "0.3.56" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc" +checksum = "ef5140344c85b01f9bbb4d4b7288a8aa4b3287ccef913a14bcc78a1063623598" dependencies = [ "addr2line", "cfg-if 1.0.0", @@ -1943,9 +1943,9 @@ dependencies = [ [[package]] name = "object" -version = "0.23.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4" +checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397" [[package]] name = "oboe" From 4beb3d5e60dd4992691a6ac896aab4911a86cd2b Mon Sep 17 00:00:00 2001 From: Sasha Hilton Date: Tue, 23 Feb 2021 18:35:57 +0000 Subject: [PATCH 06/46] Add version string CLI parameter, set name to optional parameter, default to 'librespot' --- src/main.rs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index f1c02946..58391b1b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -71,6 +71,16 @@ fn list_backends() { } } +fn print_version() { + println!( + "librespot {semver} {sha} (Built on {build_date}, Build ID: {build_id})", + semver = version::SEMVER, + sha = version::SHA_SHORT, + build_date = version::BUILD_DATE, + build_id = version::BUILD_ID + ); +} + #[derive(Clone)] struct Setup { backend: fn(Option) -> Box, @@ -103,7 +113,7 @@ fn setup(args: &[String]) -> Setup { "Path to a directory where system files (credentials, volume) will be cached. Can be different from cache option value", "SYTEMCACHE", ).optflag("", "disable-audio-cache", "Disable caching of the audio data.") - .reqopt("n", "name", "Device name", "NAME") + .optopt("n", "name", "Device name", "NAME") .optopt("", "device-type", "Displayed device type", "DEVICE_TYPE") .optopt( "b", @@ -119,6 +129,7 @@ fn setup(args: &[String]) -> Setup { ) .optflag("", "emit-sink-events", "Run program set by --onevent before sink is opened and after it is closed.") .optflag("v", "verbose", "Enable verbose output") + .optflag("V", "version", "Display librespot version string") .optopt("u", "username", "Username to sign in with", "USERNAME") .optopt("p", "password", "Password", "PASSWORD") .optopt("", "proxy", "HTTP proxy to use when connecting", "PROXY") @@ -220,6 +231,11 @@ fn setup(args: &[String]) -> Setup { } }; + if matches.opt_present("version") { + print_version(); + exit(0); + } + let verbose = matches.opt_present("verbose"); setup_logging(verbose); @@ -306,7 +322,7 @@ fn setup(args: &[String]) -> Setup { .map(|port| port.parse::().unwrap()) .unwrap_or(0); - let name = matches.opt_str("name").unwrap(); + let name = matches.opt_str("name").unwrap_or("Librespot".to_string()); let credentials = { let cached_credentials = cache.as_ref().and_then(Cache::credentials); From 8dc1e80633cccebed2a28224d8e43b19047d747e Mon Sep 17 00:00:00 2001 From: Philippe G Date: Sat, 27 Feb 2021 14:59:53 -0800 Subject: [PATCH 07/46] separated stream for each seek --- audio/src/passthrough_decoder.rs | 171 +++++++++++++++++-------------- 1 file changed, 92 insertions(+), 79 deletions(-) diff --git a/audio/src/passthrough_decoder.rs b/audio/src/passthrough_decoder.rs index 3a011011..6fab78ec 100644 --- a/audio/src/passthrough_decoder.rs +++ b/audio/src/passthrough_decoder.rs @@ -5,75 +5,32 @@ use std::fmt; use std::io::{Read, Seek}; use std::time::{SystemTime, UNIX_EPOCH}; -fn write_headers( - rdr: &mut PacketReader, - wtr: &mut PacketWriter>, -) -> Result { - let mut stream_serial: u32 = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as u32; - - // search for ident, comment, setup - get_header(1, rdr, wtr, &mut stream_serial, PacketWriteEndInfo::EndPage)?; - get_header( - 3, - rdr, - wtr, - &mut stream_serial, - PacketWriteEndInfo::NormalPacket, - )?; - get_header(5, rdr, wtr, &mut stream_serial, PacketWriteEndInfo::EndPage)?; - - // remove un-needed packets - rdr.delete_unread_packets(); - return Ok(stream_serial); -} - -fn get_header( - code: u8, - rdr: &mut PacketReader, - wtr: &mut PacketWriter>, - stream_serial: &mut u32, - info: PacketWriteEndInfo, -) -> Result +fn get_header(code: u8, rdr: &mut PacketReader) -> Result, PassthroughError> where T: Read + Seek, { let pck: Packet = rdr.read_packet_expected()?; - // set a unique serial number - if pck.stream_serial() != 0 { - *stream_serial = pck.stream_serial(); - } - let pkt_type = pck.data[0]; debug!("Vorbis header type{}", &pkt_type); - // all headers are mandatory if pkt_type != code { return Err(PassthroughError(OggReadError::InvalidData)); } - // headers keep original granule number - let absgp_page = pck.absgp_page(); - wtr.write_packet( - pck.data.into_boxed_slice(), - *stream_serial, - info, - absgp_page, - ) - .unwrap(); - - return Ok(*stream_serial); + return Ok(pck.data.into_boxed_slice()); } pub struct PassthroughDecoder { rdr: PacketReader, wtr: PacketWriter>, - lastgp_page: Option, - absgp_page: u64, + eos: bool, + bos: bool, + ofsgp_page: u64, stream_serial: u32, + ident: Box<[u8]>, + comment: Box<[u8]>, + setup: Box<[u8]>, } pub struct PassthroughError(ogg::OggReadError); @@ -82,17 +39,31 @@ impl PassthroughDecoder { /// Constructs a new Decoder from a given implementation of `Read + Seek`. pub fn new(rdr: R) -> Result { let mut rdr = PacketReader::new(rdr); - let mut wtr = PacketWriter::new(Vec::new()); + let stream_serial = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as u32; - let stream_serial = write_headers(&mut rdr, &mut wtr)?; info!("Starting passthrough track with serial {}", stream_serial); + // search for ident, comment, setup + let ident = get_header(1, &mut rdr)?; + let comment = get_header(3, &mut rdr)?; + let setup = get_header(5, &mut rdr)?; + + // remove un-needed packets + rdr.delete_unread_packets(); + return Ok(PassthroughDecoder { rdr, - wtr, - lastgp_page: Some(0), - absgp_page: 0, + wtr: PacketWriter::new(Vec::new()), + ofsgp_page: 0, stream_serial, + ident, + comment, + setup, + eos: false, + bos: false, }); } } @@ -100,52 +71,94 @@ impl PassthroughDecoder { impl AudioDecoder for PassthroughDecoder { fn seek(&mut self, ms: i64) -> Result<(), AudioError> { info!("Seeking to {}", ms); - self.lastgp_page = match ms { - 0 => Some(0), - _ => None, - }; + + // add an eos to previous stream if missing + if self.bos == true && self.eos == false { + match self.rdr.read_packet() { + Ok(Some(pck)) => { + let absgp_page = pck.absgp_page() - self.ofsgp_page; + self.wtr + .write_packet( + pck.data.into_boxed_slice(), + self.stream_serial, + PacketWriteEndInfo::EndStream, + absgp_page, + ) + .unwrap(); + } + _ => warn! {"Cannot write EoS after seeking"}, + }; + } + + self.eos = false; + self.bos = false; + self.ofsgp_page = 0; + self.stream_serial += 1; // hard-coded to 44.1 kHz match self.rdr.seek_absgp(None, (ms * 44100 / 1000) as u64) { - Ok(_) => return Ok(()), + Ok(_) => { + // need to set some offset for next_page() + let pck = self.rdr.read_packet().unwrap().unwrap(); + self.ofsgp_page = pck.absgp_page(); + debug!("Seek to offset page {}", self.ofsgp_page); + return Ok(()); + } Err(err) => return Err(AudioError::PassthroughError(err.into())), } } fn next_packet(&mut self) -> Result, AudioError> { - let mut skip = self.lastgp_page.is_none(); + // write headers if we are (re)starting + if self.bos == false { + self.wtr + .write_packet( + self.ident.clone(), + self.stream_serial, + PacketWriteEndInfo::EndPage, + 0, + ) + .unwrap(); + self.wtr + .write_packet( + self.comment.clone(), + self.stream_serial, + PacketWriteEndInfo::NormalPacket, + 0, + ) + .unwrap(); + self.wtr + .write_packet( + self.setup.clone(), + self.stream_serial, + PacketWriteEndInfo::EndPage, + 0, + ) + .unwrap(); + self.bos = true; + debug!("Wrote Ogg headers"); + } + loop { let pck = match self.rdr.read_packet() { Ok(Some(pck)) => pck, - Ok(None) | Err(OggReadError::NoCapturePatternFound) => { info!("end of streaming"); return Ok(None); } - Err(err) => return Err(AudioError::PassthroughError(err.into())), }; let pckgp_page = pck.absgp_page(); - let lastgp_page = self.lastgp_page.get_or_insert(pckgp_page); - // consume packets till next page to get a granule reference - if skip { - if *lastgp_page == pckgp_page { - debug!("skipping packet"); - continue; - } - skip = false; - info!("skipped at {}", pckgp_page); + // skip till we have audio and a calculable granule position + if pckgp_page == 0 || pckgp_page == self.ofsgp_page { + continue; } - // now we can calculate absolute granule - self.absgp_page += pckgp_page - *lastgp_page; - self.lastgp_page = Some(pckgp_page); - // set packet type let inf = if pck.last_in_stream() { - self.lastgp_page = Some(0); + self.eos = true; PacketWriteEndInfo::EndStream } else if pck.last_in_page() { PacketWriteEndInfo::EndPage @@ -158,7 +171,7 @@ impl AudioDecoder for PassthroughDecoder { pck.data.into_boxed_slice(), self.stream_serial, inf, - self.absgp_page, + pckgp_page - self.ofsgp_page, ) .unwrap(); From f29e5212c402074c1a12eb493af7e5d4c966dcdf Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 24 Feb 2021 21:39:42 +0100 Subject: [PATCH 08/46] 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 --- audio/src/lewton_decoder.rs | 7 +- audio/src/lib.rs | 4 +- audio/src/libvorbis_decoder.rs | 11 +- playback/src/audio_backend/alsa.rs | 20 +-- playback/src/audio_backend/gstreamer.rs | 2 +- playback/src/audio_backend/jackaudio.rs | 14 +- playback/src/audio_backend/pipe.rs | 2 +- playback/src/audio_backend/portaudio.rs | 6 +- playback/src/audio_backend/pulseaudio.rs | 11 +- playback/src/audio_backend/rodio.rs | 2 +- playback/src/audio_backend/sdl.rs | 4 +- playback/src/audio_backend/subprocess.rs | 2 +- playback/src/config.rs | 33 ++++ playback/src/mixer/mod.rs | 2 +- playback/src/mixer/softmixer.rs | 5 +- playback/src/player.rs | 183 +++++++++++++++++++++-- src/main.rs | 72 ++++++++- 17 files changed, 327 insertions(+), 53 deletions(-) diff --git a/audio/src/lewton_decoder.rs b/audio/src/lewton_decoder.rs index 1addaa01..8e7d089e 100644 --- a/audio/src/lewton_decoder.rs +++ b/audio/src/lewton_decoder.rs @@ -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::>() + { + Ok(Some(packet)) => return Ok(Some(AudioPacket::Samples(packet.samples))), Ok(None) => return Ok(None), Err(BadAudio(AudioIsHeader)) => (), diff --git a/audio/src/lib.rs b/audio/src/lib.rs index fd764071..c4d862b3 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -33,12 +33,12 @@ pub use fetch::{ use std::fmt; pub enum AudioPacket { - Samples(Vec), + Samples(Vec), OggData(Vec), } 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"), diff --git a/audio/src/libvorbis_decoder.rs b/audio/src/libvorbis_decoder.rs index 8aced556..e7ccc984 100644 --- a/audio/src/libvorbis_decoder.rs +++ b/audio/src/libvorbis_decoder.rs @@ -39,7 +39,16 @@ where fn next_packet(&mut self) -> Result, 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) / (0x7FFF as f64 + 0.5)) as f32) + .collect(), + ))); + } None => return Ok(None), Some(Err(vorbis::VorbisError::Hole)) => (), diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index bf7b1376..9bc17fe6 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -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, device: String, - buffer: Vec, + buffer: Vec, } fn list_outputs() { @@ -36,19 +36,19 @@ fn list_outputs() { fn open_device(dev_name: &str) -> Result<(PCM, Frames), Box> { 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(), diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 6be6dd72..1ad3631e 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -15,7 +15,7 @@ pub struct GstreamerSink { impl Open for GstreamerSink { fn open(device: Option) -> 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), diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 4699c182..e95933fc 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -7,20 +7,18 @@ use std::io; use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; pub struct JackSink { - send: SyncSender, + send: SyncSender, + // 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, + rec: Receiver, port_l: Port, port_r: Port, } -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 } diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index 210c0ce9..5516ee94 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -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::(), + data.len() * mem::size_of::(), ) }, AudioPacket::OggData(data) => data, diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 0e25021e..0b8eac0b 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -8,8 +8,8 @@ use std::process::exit; use std::time::Duration; pub struct PortAudioSink<'a>( - Option>, - StreamParameters, + Option>, + StreamParameters, ); fn output_devices() -> Box> { @@ -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) diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 11ea026a..4dca2108 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -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::(), ) }; diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 3b920c30..6c996e85 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -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); diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 27d650f9..727615d1 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -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, @@ -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)); } diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index 0dd25638..123e0233 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -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::(), + packet.samples().len() * mem::size_of::(), ) }; if let Some(child) = &mut self.child { diff --git a/playback/src/config.rs b/playback/src/config.rs index 31f63626..be15b268 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -48,12 +48,40 @@ impl Default for NormalisationType { } } +#[derive(Clone, Debug, PartialEq)] +pub enum NormalisationMethod { + Basic, + Dynamic, +} + +impl FromStr for NormalisationMethod { + type Err = (); + fn from_str(s: &str) -> Result { + match s { + "basic" => Ok(NormalisationMethod::Basic), + "dynamic" => Ok(NormalisationMethod::Dynamic), + _ => Err(()), + } + } +} + +impl Default for NormalisationMethod { + fn default() -> NormalisationMethod { + NormalisationMethod::Dynamic + } +} + #[derive(Clone, Debug)] pub struct PlayerConfig { pub bitrate: Bitrate, pub normalisation: bool, pub normalisation_type: NormalisationType, + pub normalisation_method: NormalisationMethod, pub normalisation_pregain: f32, + pub normalisation_threshold: f32, + pub normalisation_attack: f32, + pub normalisation_release: f32, + pub normalisation_steepness: f32, pub gapless: bool, pub passthrough: bool, } @@ -64,7 +92,12 @@ impl Default for PlayerConfig { bitrate: Bitrate::default(), normalisation: false, normalisation_type: NormalisationType::default(), + normalisation_method: NormalisationMethod::default(), normalisation_pregain: 0.0, + normalisation_threshold: -1.0, + normalisation_attack: 0.005, + normalisation_release: 0.1, + normalisation_steepness: 1.0, gapless: true, passthrough: false, } diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs index 325c1e18..3424526a 100644 --- a/playback/src/mixer/mod.rs +++ b/playback/src/mixer/mod.rs @@ -12,7 +12,7 @@ pub trait Mixer: Send { } pub trait AudioFilter { - fn modify_stream(&self, data: &mut [i16]); + fn modify_stream(&self, data: &mut [f32]); } #[cfg(feature = "alsa-backend")] diff --git a/playback/src/mixer/softmixer.rs b/playback/src/mixer/softmixer.rs index 28e1cf57..ec8ed6b2 100644 --- a/playback/src/mixer/softmixer.rs +++ b/playback/src/mixer/softmixer.rs @@ -35,11 +35,12 @@ struct SoftVolumeApplier { } impl AudioFilter for SoftVolumeApplier { - fn modify_stream(&self, data: &mut [i16]) { + fn modify_stream(&self, data: &mut [f32]) { let volume = self.volume.load(Ordering::Relaxed) as u16; if volume != 0xFFFF { + let volume_factor = volume as f64 / 0xFFFF as f64; for x in data.iter_mut() { - *x = (*x as i32 * volume as i32 / 0xFFFF) as i16; + *x = (*x as f64 * volume_factor) as f32; } } } diff --git a/playback/src/player.rs b/playback/src/player.rs index 9b4eefb9..c7edf3a4 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -9,7 +9,7 @@ use std::mem; use std::thread; use std::time::{Duration, Instant}; -use crate::config::{Bitrate, NormalisationType, PlayerConfig}; +use crate::config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}; use librespot_core::session::Session; use librespot_core::spotify_id::SpotifyId; @@ -26,6 +26,7 @@ use crate::metadata::{AudioItem, FileFormat}; use crate::mixer::AudioFilter; const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000; +const SAMPLES_PER_SECOND: u32 = 44100 * 2; pub struct Player { commands: Option>, @@ -54,6 +55,13 @@ struct PlayerInternal { sink_event_callback: Option, audio_filter: Option>, event_senders: Vec>, + + limiter_active: bool, + limiter_attack_counter: u32, + limiter_release_counter: u32, + limiter_peak_sample: f32, + limiter_factor: f32, + limiter_strength: f32, } enum PlayerCommand { @@ -185,7 +193,7 @@ impl PlayerEvent { pub type PlayerEventChannel = futures::sync::mpsc::UnboundedReceiver; #[derive(Clone, Copy, Debug)] -struct NormalisationData { +pub struct NormalisationData { track_gain_db: f32, track_peak: f32, album_gain_db: f32, @@ -193,6 +201,14 @@ struct NormalisationData { } impl NormalisationData { + pub fn db_to_ratio(db: f32) -> f32 { + return f32::powf(10.0, db / 20.0); + } + + pub fn ratio_to_db(ratio: f32) -> f32 { + return ratio.log10() * 20.0; + } + fn parse_from_file(mut file: T) -> Result { const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144; file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET)) @@ -218,17 +234,44 @@ impl NormalisationData { NormalisationType::Album => [data.album_gain_db, data.album_peak], NormalisationType::Track => [data.track_gain_db, data.track_peak], }; - let mut normalisation_factor = - f32::powf(10.0, (gain_db + config.normalisation_pregain) / 20.0); - if normalisation_factor * gain_peak > 1.0 { - warn!("Reducing normalisation factor to prevent clipping. Please add negative pregain to avoid."); - normalisation_factor = 1.0 / gain_peak; + let normalisation_power = gain_db + config.normalisation_pregain; + let mut normalisation_factor = Self::db_to_ratio(normalisation_power); + + if normalisation_factor * gain_peak > config.normalisation_threshold { + let limited_normalisation_factor = config.normalisation_threshold / gain_peak; + let limited_normalisation_power = Self::ratio_to_db(limited_normalisation_factor); + + if config.normalisation_method == NormalisationMethod::Basic { + warn!("Limiting gain to {:.2} for the duration of this track to stay under normalisation threshold.", limited_normalisation_power); + normalisation_factor = limited_normalisation_factor; + } else { + warn!( + "This track will at its peak be subject to {:.2} dB of dynamic limiting.", + normalisation_power - limited_normalisation_power + ); + } + + warn!("Please lower pregain to avoid."); } debug!("Normalisation Data: {:?}", data); debug!("Normalisation Type: {:?}", config.normalisation_type); - debug!("Applied normalisation factor: {}", normalisation_factor); + debug!( + "Normalisation Threshold: {:.1}", + Self::ratio_to_db(config.normalisation_threshold) + ); + debug!("Normalisation Method: {:?}", config.normalisation_method); + debug!("Normalisation Factor: {}", normalisation_factor); + + if config.normalisation_method == NormalisationMethod::Dynamic { + debug!("Normalisation Attack: {:?}", config.normalisation_attack); + debug!("Normalisation Release: {:?}", config.normalisation_release); + debug!( + "Normalisation Steepness: {:?}", + config.normalisation_steepness + ); + } normalisation_factor } @@ -262,6 +305,13 @@ impl Player { sink_event_callback: None, audio_filter: audio_filter, event_senders: [event_sender].to_vec(), + + limiter_active: false, + limiter_attack_counter: 0, + limiter_release_counter: 0, + limiter_peak_sample: 0.0, + limiter_factor: 1.0, + limiter_strength: 0.0, }; // While PlayerInternal is written as a future, it still contains blocking code. @@ -1113,9 +1163,111 @@ impl PlayerInternal { editor.modify_stream(data) } - if self.config.normalisation && normalisation_factor != 1.0 { - for x in data.iter_mut() { - *x = (*x as f32 * normalisation_factor) as i16; + if self.config.normalisation + && (normalisation_factor != 1.0 + || self.config.normalisation_method != NormalisationMethod::Basic) + { + for sample in data.iter_mut() { + let mut actual_normalisation_factor = normalisation_factor; + if self.config.normalisation_method == NormalisationMethod::Dynamic + { + if self.limiter_active { + // "S"-shaped curve with a configurable steepness during attack and release: + // - > 1.0 yields soft knees at start and end, steeper in between + // - 1.0 yields a linear function from 0-100% + // - between 0.0 and 1.0 yields hard knees at start and end, flatter in between + // - 0.0 yields a step response to 50%, causing distortion + // - Rates < 0.0 invert the limiter and are invalid + let mut shaped_limiter_strength = self.limiter_strength; + if shaped_limiter_strength > 0.0 + && shaped_limiter_strength < 1.0 + { + shaped_limiter_strength = 1.0 + / (1.0 + + f32::powf( + shaped_limiter_strength + / (1.0 - shaped_limiter_strength), + -1.0 * self.config.normalisation_steepness, + )); + } + actual_normalisation_factor = + (1.0 - shaped_limiter_strength) * normalisation_factor + + shaped_limiter_strength * self.limiter_factor; + }; + + // Always check for peaks, even when the limiter is already active. + // There may be even higher peaks than we initially targeted. + // Check against the normalisation factor that would be applied normally. + let abs_sample = + ((*sample as f64 * normalisation_factor as f64) as f32) + .abs(); + if abs_sample > self.config.normalisation_threshold { + self.limiter_active = true; + if self.limiter_release_counter > 0 { + // A peak was encountered while releasing the limiter; + // synchronize with the current release limiter strength. + self.limiter_attack_counter = (((SAMPLES_PER_SECOND + as f32 + * self.config.normalisation_release) + - self.limiter_release_counter as f32) + / (self.config.normalisation_release + / self.config.normalisation_attack)) + as u32; + self.limiter_release_counter = 0; + } + + self.limiter_attack_counter = + self.limiter_attack_counter.saturating_add(1); + self.limiter_strength = self.limiter_attack_counter as f32 + / (SAMPLES_PER_SECOND as f32 + * self.config.normalisation_attack); + + if abs_sample > self.limiter_peak_sample { + self.limiter_peak_sample = abs_sample; + self.limiter_factor = + self.config.normalisation_threshold + / self.limiter_peak_sample; + } + } else if self.limiter_active { + if self.limiter_attack_counter > 0 { + // Release may start within the attack period, before + // the limiter reached full strength. For that reason + // start the release by synchronizing with the current + // attack limiter strength. + self.limiter_release_counter = (((SAMPLES_PER_SECOND + as f32 + * self.config.normalisation_attack) + - self.limiter_attack_counter as f32) + * (self.config.normalisation_release + / self.config.normalisation_attack)) + as u32; + self.limiter_attack_counter = 0; + } + + self.limiter_release_counter = + self.limiter_release_counter.saturating_add(1); + + if self.limiter_release_counter + > (SAMPLES_PER_SECOND as f32 + * self.config.normalisation_release) + as u32 + { + self.reset_limiter(); + } else { + self.limiter_strength = ((SAMPLES_PER_SECOND as f32 + * self.config.normalisation_release) + - self.limiter_release_counter as f32) + / (SAMPLES_PER_SECOND as f32 + * self.config.normalisation_release); + } + } + } + + // Extremely sharp attacks, however unlikely, *may* still clip and provide + // undefined results, so strictly enforce output within [-1.0, 1.0]. + *sample = (*sample as f64 * actual_normalisation_factor as f64) + .clamp(-1.0, 1.0) + as f32; } } } @@ -1146,6 +1298,15 @@ impl PlayerInternal { } } + fn reset_limiter(&mut self) { + self.limiter_active = false; + self.limiter_release_counter = 0; + self.limiter_attack_counter = 0; + self.limiter_peak_sample = 0.0; + self.limiter_factor = 1.0; + self.limiter_strength = 0.0; + } + fn start_playback( &mut self, track_id: SpotifyId, diff --git a/src/main.rs b/src/main.rs index 53603152..91a58659 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,13 +22,15 @@ use librespot::core::version; use librespot::connect::discovery::{discovery, DiscoveryStream}; use librespot::connect::spirc::{Spirc, SpircTask}; use librespot::playback::audio_backend::{self, Sink, BACKENDS}; -use librespot::playback::config::{Bitrate, NormalisationType, PlayerConfig}; +use librespot::playback::config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}; use librespot::playback::mixer::{self, Mixer, MixerConfig}; -use librespot::playback::player::{Player, PlayerEvent}; +use librespot::playback::player::{NormalisationData, Player, PlayerEvent}; mod player_event_handler; use crate::player_event_handler::{emit_sink_event, run_program_on_events}; +const MILLIS: f32 = 1000.0; + fn device_id(name: &str) -> String { hex::encode(Sha1::digest(name.as_bytes())) } @@ -188,6 +190,12 @@ fn setup(args: &[String]) -> Setup { "enable-volume-normalisation", "Play all tracks at the same volume", ) + .optopt( + "", + "normalisation-method", + "Specify the normalisation method to use - [basic, dynamic]. Default is dynamic.", + "NORMALISATION_METHOD", + ) .optopt( "", "normalisation-gain-type", @@ -200,6 +208,30 @@ fn setup(args: &[String]) -> Setup { "Pregain (dB) applied by volume normalisation", "PREGAIN", ) + .optopt( + "", + "normalisation-threshold", + "Threshold (dBFS) to prevent clipping. Default is -1.0.", + "THRESHOLD", + ) + .optopt( + "", + "normalisation-attack", + "Attack time (ms) in which the dynamic limiter is reducing gain. Default is 5.", + "ATTACK", + ) + .optopt( + "", + "normalisation-release", + "Release or decay time (ms) in which the dynamic limiter is restoring gain. Default is 100.", + "RELEASE", + ) + .optopt( + "", + "normalisation-steepness", + "Steepness of the dynamic limiting curve. Default is 1.0.", + "STEEPNESS", + ) .optopt( "", "volume-ctrl", @@ -390,15 +422,51 @@ fn setup(args: &[String]) -> Setup { NormalisationType::from_str(gain_type).expect("Invalid normalisation type") }) .unwrap_or(NormalisationType::default()); + let normalisation_method = matches + .opt_str("normalisation-method") + .as_ref() + .map(|gain_type| { + NormalisationMethod::from_str(gain_type).expect("Invalid normalisation method") + }) + .unwrap_or(NormalisationMethod::default()); PlayerConfig { bitrate: bitrate, gapless: !matches.opt_present("disable-gapless"), normalisation: matches.opt_present("enable-volume-normalisation"), + normalisation_method: normalisation_method, normalisation_type: gain_type, normalisation_pregain: matches .opt_str("normalisation-pregain") .map(|pregain| pregain.parse::().expect("Invalid pregain float value")) .unwrap_or(PlayerConfig::default().normalisation_pregain), + normalisation_threshold: NormalisationData::db_to_ratio( + matches + .opt_str("normalisation-threshold") + .map(|threshold| { + threshold + .parse::() + .expect("Invalid threshold float value") + }) + .unwrap_or(PlayerConfig::default().normalisation_threshold), + ), + normalisation_attack: matches + .opt_str("normalisation-attack") + .map(|attack| attack.parse::().expect("Invalid attack float value")) + .unwrap_or(PlayerConfig::default().normalisation_attack * MILLIS) + / MILLIS, + normalisation_release: matches + .opt_str("normalisation-release") + .map(|release| release.parse::().expect("Invalid release float value")) + .unwrap_or(PlayerConfig::default().normalisation_release * MILLIS) + / MILLIS, + normalisation_steepness: matches + .opt_str("normalisation-steepness") + .map(|steepness| { + steepness + .parse::() + .expect("Invalid steepness float value") + }) + .unwrap_or(PlayerConfig::default().normalisation_steepness), passthrough, } }; From 1672eb87abd22f9c45e3086b8a2eb93c8c8fe6ed Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 2 Mar 2021 20:21:05 +0100 Subject: [PATCH 09/46] Fix build on Rust < 1.50.0 --- playback/src/player.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index c7edf3a4..b6b8ad5f 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1263,11 +1263,16 @@ impl PlayerInternal { } } + *sample = + (*sample as f64 * actual_normalisation_factor as f64) as f32; + // Extremely sharp attacks, however unlikely, *may* still clip and provide // undefined results, so strictly enforce output within [-1.0, 1.0]. - *sample = (*sample as f64 * actual_normalisation_factor as f64) - .clamp(-1.0, 1.0) - as f32; + if *sample < -1.0 { + *sample = -1.0; + } else if *sample > 1.0 { + *sample = 1.0; + } } } } From 5257be7824e0fd2c76cf889f88f82047080fed90 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 12 Mar 2021 23:05:38 +0100 Subject: [PATCH 10/46] Add command-line option to set F32 or S16 bit output Usage: `--format {F32|S16}`. Default is F32. - Implemented for all backends, except for JACK audio which itself only supports 32-bit output at this time. Setting JACK audio to S16 will panic and instruct the user to set output to F32. - The F32 default works fine for Rodio on macOS, but not on Raspian 10 with Alsa as host. Therefore users on Linux systems are warned to set output to S16 in case of garbled sound with Rodio. This seems an issue with cpal incorrectly detecting the output stream format. - While at it, DRY up lots of code in the backends and by that virtue, also enable OggData passthrough on the subprocess backend. - I tested Rodio, ALSA, pipe and subprocess quite a bit, and call on others to join in and test the other backends. --- audio/src/lib.rs | 7 + playback/Cargo.toml | 4 +- playback/src/audio_backend/alsa.rs | 88 ++++++---- playback/src/audio_backend/gstreamer.rs | 55 +++--- playback/src/audio_backend/jackaudio.rs | 25 +-- playback/src/audio_backend/mod.rs | 48 +++++- playback/src/audio_backend/pipe.rs | 55 +++--- playback/src/audio_backend/portaudio.rs | 113 +++++++++---- playback/src/audio_backend/pulseaudio.rs | 42 ++--- playback/src/audio_backend/rodio.rs | 202 ++++++++++------------- playback/src/audio_backend/sdl.rs | 87 +++++++--- playback/src/audio_backend/subprocess.rs | 24 +-- playback/src/config.rs | 24 +++ playback/src/player.rs | 14 +- src/main.rs | 30 +++- 15 files changed, 504 insertions(+), 314 deletions(-) diff --git a/audio/src/lib.rs b/audio/src/lib.rs index c4d862b3..8a9f88f5 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -58,6 +58,13 @@ impl AudioPacket { AudioPacket::OggData(d) => d.is_empty(), } } + + pub fn f32_to_s16(samples: &[f32]) -> Vec { + samples + .iter() + .map(|sample| (*sample as f64 * (0x7FFF as f64 + 0.5) - 0.5) as i16) + .collect() + } } #[cfg(not(any(feature = "with-tremor", feature = "with-vorbis")))] diff --git a/playback/Cargo.toml b/playback/Cargo.toml index b8995a4b..67e06be7 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -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"] @@ -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" ] diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 9bc17fe6..92b71f40 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -1,18 +1,21 @@ use super::{Open, Sink}; 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}; use std::cmp::min; use std::ffi::CString; -use std::io; use std::process::exit; +use std::{io, mem}; -const PREFERRED_PERIOD_SIZE: Frames = 11025; // Period of roughly 125ms -const BUFFERED_PERIODS: Frames = 4; +const BUFFERED_LATENCY: f32 = 0.125; // seconds +const BUFFERED_PERIODS: u8 = 4; pub struct AlsaSink { pcm: Option, + format: AudioFormat, device: String, buffer: Vec, } @@ -34,25 +37,28 @@ fn list_outputs() { } } -fn open_device(dev_name: &str) -> Result<(PCM, Frames), Box> { +fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box> { let pcm = PCM::new(dev_name, Direction::Playback, false)?; - let mut period_size = PREFERRED_PERIOD_SIZE; + let (alsa_format, sample_size) = match format { + AudioFormat::F32 => (Format::float(), mem::size_of::()), + AudioFormat::S16 => (Format::s16(), mem::size_of::()), + }; + // http://www.linuxjournal.com/article/6735?page=0,1#N0x19ab2890.0x19ba78d8 // latency = period_size * periods / (rate * bytes_per_frame) - // 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 / 32-bit float - let hwp = HwParams::any(&pcm)?; + // For stereo samples encoded as 32-bit float, one frame has a length of eight bytes. + let mut period_size = ((SAMPLES_PER_SECOND * sample_size as u32) as f32 + * (BUFFERED_LATENCY / BUFFERED_PERIODS as f32)) as i32; + // Set hardware parameters: 44100 Hz / stereo / 32-bit float or 16-bit signed integer + { + let hwp = HwParams::any(&pcm)?; hwp.set_access(Access::RWInterleaved)?; - hwp.set_format(Format::float())?; - 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)?; + hwp.set_buffer_size_near(period_size * BUFFERED_PERIODS as i32)?; pcm.hw_params(&hwp)?; let swp = pcm.sw_params_current()?; @@ -64,12 +70,12 @@ fn open_device(dev_name: &str) -> Result<(PCM, Frames), Box> { } impl Open for AlsaSink { - fn open(device: Option) -> AlsaSink { - info!("Using alsa sink"); + fn open(device: Option, 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) } @@ -80,6 +86,7 @@ impl Open for AlsaSink { AlsaSink { pcm: None, + format: format, device: name, buffer: vec![], } @@ -89,12 +96,13 @@ 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 * BUFFERED_PERIODS as i32) as usize); } Err(e) => { error!("Alsa error PCM open {}", e); @@ -111,14 +119,10 @@ 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_f32().unwrap(); - match io.writei(&self.buffer[..]) { - Ok(_) => (), - Err(err) => pcm.try_recover(err, false).unwrap(), - } + self.write_buf().expect("could not flush buffer"); + let pcm = self.pcm.as_mut().unwrap(); pcm.drain().unwrap(); } self.pcm = None; @@ -137,12 +141,7 @@ 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_f32().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(); } } @@ -150,3 +149,26 @@ impl Sink for AlsaSink { Ok(()) } } + +impl AlsaSink { + fn write_buf(&mut self) -> io::Result<()> { + let pcm = self.pcm.as_mut().unwrap(); + let io_result = match self.format { + AudioFormat::F32 => { + let io = pcm.io_f32().unwrap(); + io.writei(&self.buffer) + } + AudioFormat::S16 => { + let io = pcm.io_i16().unwrap(); + let buf_s16: Vec = AudioPacket::f32_to_s16(&self.buffer); + io.writei(&buf_s16[..]) + } + }; + match io_result { + Ok(_) => (), + Err(err) => pcm.try_recover(err, false).unwrap(), + }; + + Ok(()) + } +} diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 1ad3631e..17ad86e6 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -1,21 +1,29 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkAsBytes}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use gst::prelude::*; use gst::*; use std::sync::mpsc::{sync_channel, SyncSender}; use std::{io, thread}; -use zerocopy::*; +use zerocopy::AsBytes; #[allow(dead_code)] pub struct GstreamerSink { tx: SyncSender>, pipeline: gst::Pipeline, + format: AudioFormat, } impl Open for GstreamerSink { - fn open(device: Option) -> GstreamerSink { - gst::init().expect("Failed to init gstreamer!"); - 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 "#; + fn open(device: Option, format: AudioFormat) -> GstreamerSink { + info!("Using GStreamer sink with format: {:?}", format); + + gst::init().expect("failed to init GStreamer!"); + let pipeline_str_preamble = format!( + r#"appsrc caps="audio/x-raw,format={:?},layout=interleaved,channels={},rate={}" block=true max-bytes=4096 name=appsrc0 "#, + format, NUM_CHANNELS, SAMPLE_RATE + ); let pipeline_str_rest = r#" ! audioconvert ! autoaudiosink"#; let pipeline_str: String = match device { Some(x) => format!("{}{}", pipeline_str_preamble, x), @@ -27,25 +35,25 @@ impl Open for GstreamerSink { let pipelinee = gst::parse_launch(&*pipeline_str).expect("Couldn't launch pipeline; likely a GStreamer issue or an error in the pipeline string you specified in the 'device' argument to librespot."); let pipeline = pipelinee .dynamic_cast::() - .expect("Couldn't cast pipeline element at runtime!"); - let bus = pipeline.get_bus().expect("Couldn't get bus from pipeline"); + .expect("couldn't cast pipeline element at runtime!"); + let bus = pipeline.get_bus().expect("couldn't get bus from pipeline"); let mainloop = glib::MainLoop::new(None, false); let appsrce: gst::Element = pipeline .get_by_name("appsrc0") - .expect("Couldn't get appsrc from pipeline"); + .expect("couldn't get appsrc from pipeline"); let appsrc: gst_app::AppSrc = appsrce .dynamic_cast::() - .expect("Couldn't cast AppSrc element at runtime!"); + .expect("couldn't cast AppSrc element at runtime!"); let bufferpool = gst::BufferPool::new(); - let appsrc_caps = appsrc.get_caps().expect("Couldn't get appsrc caps"); + let appsrc_caps = appsrc.get_caps().expect("couldn't get appsrc caps"); let mut conf = bufferpool.get_config(); conf.set_params(Some(&appsrc_caps), 8192, 0, 0); bufferpool .set_config(conf) - .expect("Couldn't configure the buffer pool"); + .expect("couldn't configure the buffer pool"); bufferpool .set_active(true) - .expect("Couldn't activate buffer pool"); + .expect("couldn't activate buffer pool"); let (tx, rx) = sync_channel::>(128); thread::spawn(move || { @@ -57,7 +65,7 @@ impl Open for GstreamerSink { mutbuf.set_size(data.len()); mutbuf .copy_from_slice(0, data.as_bytes()) - .expect("Failed to copy from slice"); + .expect("failed to copy from slice"); let _eat = appsrc.push_buffer(okbuffer); } } @@ -83,33 +91,32 @@ impl Open for GstreamerSink { glib::Continue(true) }) - .expect("Failed to add bus watch"); + .expect("failed to add bus watch"); thread_mainloop.run(); }); pipeline .set_state(gst::State::Playing) - .expect("Unable to set the pipeline to the `Playing` state"); + .expect("unable to set the pipeline to the `Playing` state"); GstreamerSink { tx: tx, pipeline: pipeline, + format: format, } } } impl Sink for GstreamerSink { - fn start(&mut self) -> io::Result<()> { - Ok(()) - } - fn stop(&mut self) -> io::Result<()> { - Ok(()) - } - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + start_stop_noop!(); + sink_as_bytes!(); +} + +impl SinkAsBytes for GstreamerSink { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { // Copy expensively (in to_vec()) to avoid thread synchronization - let deighta: &[u8] = packet.samples().as_bytes(); self.tx - .send(deighta.to_vec()) + .send(data.to_vec()) .expect("tx send failed in write function"); Ok(()) } diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index e95933fc..295941a4 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -1,10 +1,12 @@ use super::{Open, Sink}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; +use crate::player::NUM_CHANNELS; use jack::{ AsyncClient, AudioOut, Client, ClientOptions, Control, Port, ProcessHandler, ProcessScope, }; -use std::io; use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; +use std::{io, mem}; pub struct JackSink { send: SyncSender, @@ -39,8 +41,15 @@ impl ProcessHandler for JackData { } impl Open for JackSink { - fn open(client_name: Option) -> JackSink { - info!("Using jack sink!"); + fn open(client_name: Option, format: AudioFormat) -> JackSink { + info!("Using JACK sink with format {:?}", format); + + if format != AudioFormat::F32 { + panic!( + "JACK sink only supports 32-bit floating point output. Use `--format {:?}`", + AudioFormat::F32 + ); + } let client_name = client_name.unwrap_or("librespot".to_string()); let (client, _status) = @@ -48,7 +57,7 @@ impl Open for JackSink { let ch_r = client.register_port("out_0", AudioOut::default()).unwrap(); let ch_l = client.register_port("out_1", AudioOut::default()).unwrap(); // buffer for samples from librespot (~10ms) - let (tx, rx) = sync_channel(2 * 1024 * 4); + let (tx, rx) = sync_channel::(NUM_CHANNELS as usize * 1024 * mem::size_of::()); let jack_data = JackData { rec: rx, port_l: ch_l, @@ -64,13 +73,7 @@ impl Open for JackSink { } impl Sink for JackSink { - fn start(&mut self) -> io::Result<()> { - Ok(()) - } - - fn stop(&mut self) -> io::Result<()> { - Ok(()) - } + start_stop_noop!(); fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { for s in packet.samples().iter() { diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 3f5dae8d..550ebb84 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -1,8 +1,9 @@ use crate::audio::AudioPacket; +use crate::config::AudioFormat; use std::io; pub trait Open { - fn open(_: Option) -> Self; + fn open(_: Option, format: AudioFormat) -> Self; } pub trait Sink { @@ -11,8 +12,42 @@ pub trait Sink { fn write(&mut self, packet: &AudioPacket) -> io::Result<()>; } -fn mk_sink(device: Option) -> Box { - Box::new(S::open(device)) +pub trait SinkAsBytes { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()>; +} + +fn mk_sink(device: Option, format: AudioFormat) -> Box { + Box::new(S::open(device, format)) +} + +// reuse code for various backends +macro_rules! sink_as_bytes { + () => { + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + use zerocopy::AsBytes; + match packet { + AudioPacket::Samples(samples) => match self.format { + AudioFormat::F32 => self.write_bytes(samples.as_bytes()), + AudioFormat::S16 => { + let samples_s16 = AudioPacket::f32_to_s16(samples); + self.write_bytes(samples_s16.as_bytes()) + } + }, + AudioPacket::OggData(samples) => self.write_bytes(samples), + } + } + }; +} + +macro_rules! start_stop_noop { + () => { + fn start(&mut self) -> io::Result<()> { + Ok(()) + } + fn stop(&mut self) -> io::Result<()> { + Ok(()) + } + }; } #[cfg(feature = "alsa-backend")] @@ -68,7 +103,10 @@ use self::pipe::StdoutSink; mod subprocess; use self::subprocess::SubprocessSink; -pub const BACKENDS: &'static [(&'static str, fn(Option) -> Box)] = &[ +pub const BACKENDS: &'static [( + &'static str, + fn(Option, AudioFormat) -> Box, +)] = &[ #[cfg(feature = "alsa-backend")] ("alsa", mk_sink::), #[cfg(feature = "portaudio-backend")] @@ -92,7 +130,7 @@ pub const BACKENDS: &'static [(&'static str, fn(Option) -> Box ("subprocess", mk_sink::), ]; -pub fn find(name: Option) -> Option) -> Box> { +pub fn find(name: Option) -> Option, AudioFormat) -> Box> { if let Some(name) = name { BACKENDS .iter() diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index 5516ee94..3a90d06f 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -1,46 +1,39 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkAsBytes}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; use std::fs::OpenOptions; use std::io::{self, Write}; -use std::mem; -use std::slice; -pub struct StdoutSink(Box); +pub struct StdoutSink { + output: Box, + format: AudioFormat, +} impl Open for StdoutSink { - fn open(path: Option) -> StdoutSink { - if let Some(path) = path { - let file = OpenOptions::new().write(true).open(path).unwrap(); - StdoutSink(Box::new(file)) - } else { - StdoutSink(Box::new(io::stdout())) + fn open(path: Option, format: AudioFormat) -> StdoutSink { + info!("Using pipe sink with format: {:?}", format); + + let output: Box = match path { + Some(path) => Box::new(OpenOptions::new().write(true).open(path).unwrap()), + _ => Box::new(io::stdout()), + }; + + StdoutSink { + output: output, + format: format, } } } impl Sink for StdoutSink { - fn start(&mut self) -> io::Result<()> { - Ok(()) - } - - fn stop(&mut self) -> io::Result<()> { - Ok(()) - } - - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - let data: &[u8] = match packet { - AudioPacket::Samples(data) => unsafe { - slice::from_raw_parts( - data.as_ptr() as *const u8, - data.len() * mem::size_of::(), - ) - }, - AudioPacket::OggData(data) => data, - }; - - self.0.write_all(data)?; - self.0.flush()?; + start_stop_noop!(); + sink_as_bytes!(); +} +impl SinkAsBytes for StdoutSink { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { + self.output.write_all(data)?; + self.output.flush()?; Ok(()) } } diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 0b8eac0b..70caedd7 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -1,5 +1,7 @@ use super::{Open, Sink}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use portaudio_rs; use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo}; use portaudio_rs::stream::*; @@ -7,10 +9,16 @@ use std::io; use std::process::exit; use std::time::Duration; -pub struct PortAudioSink<'a>( - Option>, - StreamParameters, -); +pub enum PortAudioSink<'a> { + F32( + Option>, + StreamParameters, + ), + S16( + Option>, + StreamParameters, + ), +} fn output_devices() -> Box> { let count = portaudio_rs::device::get_count().unwrap(); @@ -40,8 +48,8 @@ fn find_output(device: &str) -> Option { } impl<'a> Open for PortAudioSink<'a> { - fn open(device: Option) -> PortAudioSink<'a> { - debug!("Using PortAudio sink"); + fn open(device: Option, format: AudioFormat) -> PortAudioSink<'a> { + info!("Using PortAudio sink with format: {:?}", format); portaudio_rs::initialize().unwrap(); @@ -53,7 +61,7 @@ impl<'a> Open for PortAudioSink<'a> { Some(device) => find_output(device), None => get_default_output_index(), } - .expect("Could not find device"); + .expect("could not find device"); let info = portaudio_rs::device::get_info(device_idx); let latency = match info { @@ -61,46 +69,87 @@ impl<'a> Open for PortAudioSink<'a> { None => Duration::new(0, 0), }; - let params = StreamParameters { - device: device_idx, - channel_count: 2, - suggested_latency: latency, - data: 0.0, - }; - - PortAudioSink(None, params) + macro_rules! open_sink { + ($sink: expr, $data: expr) => {{ + let params = StreamParameters { + device: device_idx, + channel_count: NUM_CHANNELS as u32, + suggested_latency: latency, + data: $data, + }; + $sink(None, params) + }}; + } + match format { + AudioFormat::F32 => open_sink!(PortAudioSink::F32, 0.0), + AudioFormat::S16 => open_sink!(PortAudioSink::S16, 0), + } } } impl<'a> Sink for PortAudioSink<'a> { fn start(&mut self) -> io::Result<()> { - if self.0.is_none() { - self.0 = Some( - Stream::open( - None, - Some(self.1), - 44100.0, - FRAMES_PER_BUFFER_UNSPECIFIED, - StreamFlags::empty(), - None, - ) - .unwrap(), - ); + macro_rules! start_sink { + ($stream: ident, $parameters: ident) => {{ + if $stream.is_none() { + *$stream = Some( + Stream::open( + None, + Some(*$parameters), + SAMPLE_RATE as f64, + FRAMES_PER_BUFFER_UNSPECIFIED, + StreamFlags::empty(), + None, + ) + .unwrap(), + ); + } + $stream.as_mut().unwrap().start().unwrap() + }}; } + match self { + PortAudioSink::F32(stream, parameters) => start_sink!(stream, parameters), + PortAudioSink::S16(stream, parameters) => start_sink!(stream, parameters), + }; - self.0.as_mut().unwrap().start().unwrap(); Ok(()) } + fn stop(&mut self) -> io::Result<()> { - self.0.as_mut().unwrap().stop().unwrap(); - self.0 = None; + macro_rules! stop_sink { + ($stream: expr) => {{ + $stream.as_mut().unwrap().stop().unwrap(); + *$stream = None; + }}; + } + match self { + PortAudioSink::F32(stream, _parameters) => stop_sink!(stream), + PortAudioSink::S16(stream, _parameters) => stop_sink!(stream), + }; + Ok(()) } + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - match self.0.as_mut().unwrap().write(packet.samples()) { + macro_rules! write_sink { + ($stream: expr, $samples: expr) => { + $stream.as_mut().unwrap().write($samples) + }; + } + let result = match self { + PortAudioSink::F32(stream, _parameters) => { + let samples = packet.samples(); + write_sink!(stream, &samples) + } + PortAudioSink::S16(stream, _parameters) => { + let samples_s16: Vec = AudioPacket::f32_to_s16(packet.samples()); + write_sink!(stream, &samples_s16) + } + }; + match result { Ok(_) => (), Err(portaudio_rs::PaError::OutputUnderflowed) => error!("PortAudio write underflow"), - Err(e) => panic!("PA Error {}", e), + Err(e) => panic!("PortAudio error {}", e), }; Ok(()) diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 4dca2108..8c1e8e83 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -1,9 +1,10 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkAsBytes}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; 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"; @@ -12,16 +13,22 @@ pub struct PulseAudioSink { s: Option, ss: pulse::sample::Spec, device: Option, + format: AudioFormat, } impl Open for PulseAudioSink { - fn open(device: Option) -> PulseAudioSink { - debug!("Using PulseAudio sink"); + fn open(device: Option, format: AudioFormat) -> PulseAudioSink { + info!("Using PulseAudio sink with format: {:?}", format); + + let pulse_format = match format { + AudioFormat::F32 => pulse::sample::Format::F32le, + AudioFormat::S16 => pulse::sample::Format::S16le, + }; let ss = pulse::sample::Spec { - format: pulse::sample::Format::F32le, - channels: 2, // stereo - rate: 44100, + format: pulse_format, + channels: NUM_CHANNELS, + rate: SAMPLE_RATE, }; debug_assert!(ss.is_valid()); @@ -29,6 +36,7 @@ impl Open for PulseAudioSink { s: None, ss: ss, device: device, + format: format, } } } @@ -67,19 +75,13 @@ impl Sink for PulseAudioSink { Ok(()) } - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - if let Some(s) = &self.s { - // 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() * mem::size_of::(), - ) - }; + sink_as_bytes!(); +} - match s.write(d) { +impl SinkAsBytes for PulseAudioSink { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { + if let Some(s) = &self.s { + match s.write(data) { Ok(_) => Ok(()), Err(e) => Err(io::Error::new( io::ErrorKind::BrokenPipe, @@ -89,7 +91,7 @@ impl Sink for PulseAudioSink { } else { Err(io::Error::new( io::ErrorKind::NotConnected, - "Not connected to pulseaudio", + "Not connected to PulseAudio", )) } } diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 6c996e85..7571aa20 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -2,33 +2,90 @@ use super::{Open, Sink}; extern crate cpal; extern crate rodio; use crate::audio::AudioPacket; +use crate::config::AudioFormat; use cpal::traits::{DeviceTrait, HostTrait}; use std::process::exit; use std::{io, thread, time}; -pub struct RodioSink { - rodio_sink: rodio::Sink, - // We have to keep hold of this object, or the Sink can't play... - #[allow(dead_code)] - stream: rodio::OutputStream, +// most code is shared between RodioSink and JackRodioSink +macro_rules! rodio_sink { + ($name: ident) => { + pub struct $name { + rodio_sink: rodio::Sink, + // We have to keep hold of this object, or the Sink can't play... + #[allow(dead_code)] + stream: rodio::OutputStream, + format: AudioFormat, + } + + impl Sink for $name { + start_stop_noop!(); + + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + let samples = packet.samples(); + match self.format { + AudioFormat::F32 => { + let source = rodio::buffer::SamplesBuffer::new(2, 44100, samples); + self.rodio_sink.append(source) + } + AudioFormat::S16 => { + let samples_s16: Vec = AudioPacket::f32_to_s16(samples); + let source = rodio::buffer::SamplesBuffer::new(2, 44100, samples_s16); + self.rodio_sink.append(source) + } + }; + + // Chunk sizes seem to be about 256 to 3000 ish items long. + // Assuming they're on average 1628 then a half second buffer is: + // 44100 elements --> about 27 chunks + while self.rodio_sink.len() > 26 { + // sleep and wait for rodio to drain a bit + thread::sleep(time::Duration::from_millis(10)); + } + Ok(()) + } + } + + impl $name { + fn open_sink(host: &cpal::Host, device: Option, format: AudioFormat) -> $name { + if format != AudioFormat::S16 { + #[cfg(target_os = "linux")] + { + warn!("Rodio output to Alsa is known to cause garbled sound on output formats other than 16-bit signed integer."); + warn!("Consider using `--backend alsa` OR `--format {:?}`", AudioFormat::S16); + } + } + + let rodio_device = match_device(&host, device); + debug!("Using cpal device"); + let stream = rodio::OutputStream::try_from_device(&rodio_device) + .expect("couldn't open output stream."); + debug!("Using Rodio stream"); + let sink = rodio::Sink::try_new(&stream.1).expect("couldn't create output sink."); + debug!("Using Rodio sink"); + + $name { + rodio_sink: sink, + stream: stream.0, + format: format, + } + } + } + }; } +rodio_sink!(RodioSink); #[cfg(all( feature = "rodiojack-backend", any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd") ))] -pub struct JackRodioSink { - jackrodio_sink: rodio::Sink, - // We have to keep hold of this object, or the Sink can't play... - #[allow(dead_code)] - stream: rodio::OutputStream, -} +rodio_sink!(JackRodioSink); fn list_formats(ref device: &rodio::Device) { let default_fmt = match device.default_output_config() { Ok(fmt) => cpal::SupportedStreamConfig::from(fmt), Err(e) => { - warn!("Error getting default rodio::Sink config: {}", e); + warn!("Error getting default Rodio output config: {}", e); return; } }; @@ -38,13 +95,13 @@ fn list_formats(ref device: &rodio::Device) { let mut output_configs = match device.supported_output_configs() { Ok(f) => f.peekable(), Err(e) => { - warn!("Error getting supported rodio::Sink configs: {}", e); + warn!("Error getting supported Rodio output configs: {}", e); return; } }; if output_configs.peek().is_some() { - debug!(" Available configs:"); + debug!(" Available output configs:"); for format in output_configs { debug!(" {:?}", format); } @@ -54,13 +111,13 @@ fn list_formats(ref device: &rodio::Device) { fn list_outputs(ref host: &cpal::Host) { let default_device = get_default_device(host); let default_device_name = default_device.name().expect("cannot get output name"); - println!("Default Audio Device:\n {}", default_device_name); + println!("Default audio device:\n {}", default_device_name); list_formats(&default_device); - println!("Other Available Audio Devices:"); + println!("Other available audio devices:"); let found_devices = host.output_devices().expect(&format!( - "Cannot get list of output devices of Host: {:?}", + "Cannot get list of output devices of host: {:?}", host.id() )); for device in found_devices { @@ -86,7 +143,7 @@ fn match_device(ref host: &cpal::Host, device: Option) -> rodio::Device } let found_devices = host.output_devices().expect(&format!( - "Cannot get list of output devices of Host: {:?}", + "cannot get list of output devices of host: {:?}", host.id() )); for d in found_devices { @@ -102,22 +159,14 @@ fn match_device(ref host: &cpal::Host, device: Option) -> rodio::Device } impl Open for RodioSink { - fn open(device: Option) -> RodioSink { + fn open(device: Option, format: AudioFormat) -> RodioSink { let host = cpal::default_host(); - debug!("Using rodio sink with cpal host: {:?}", host.id()); - - let rodio_device = match_device(&host, device); - debug!("Using cpal device"); - let stream = rodio::OutputStream::try_from_device(&rodio_device) - .expect("Couldn't open output stream."); - debug!("Using rodio stream"); - let sink = rodio::Sink::try_new(&stream.1).expect("Couldn't create output sink."); - debug!("Using rodio sink"); - - RodioSink { - rodio_sink: sink, - stream: stream.0, - } + info!( + "Using Rodio sink with format {:?} and cpal host: {:?}", + format, + host.id() + ); + Self::open_sink(&host, device, format) } } @@ -126,89 +175,18 @@ impl Open for RodioSink { any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd") ))] impl Open for JackRodioSink { - fn open(device: Option) -> JackRodioSink { + fn open(device: Option, format: AudioFormat) -> JackRodioSink { let host = cpal::host_from_id( cpal::available_hosts() .into_iter() .find(|id| *id == cpal::HostId::Jack) - .expect("Jack Host not found"), + .expect("JACK host not found"), ) - .expect("Jack Host not found"); - debug!("Using jack rodio sink with cpal Jack host"); - - let rodio_device = match_device(&host, device); - debug!("Using cpal device"); - let stream = rodio::OutputStream::try_from_device(&rodio_device) - .expect("Couldn't open output stream."); - debug!("Using jack rodio stream"); - let sink = rodio::Sink::try_new(&stream.1).expect("Couldn't create output sink."); - debug!("Using jack rodio sink"); - - JackRodioSink { - jackrodio_sink: sink, - stream: stream.0, - } - } -} - -impl Sink for RodioSink { - fn start(&mut self) -> io::Result<()> { - // More similar to an "unpause" than "play". Doesn't undo "stop". - // self.rodio_sink.play(); - Ok(()) - } - - fn stop(&mut self) -> io::Result<()> { - // This will immediately stop playback, but the sink is then unusable. - // We just have to let the current buffer play till the end. - // self.rodio_sink.stop(); - Ok(()) - } - - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - let source = rodio::buffer::SamplesBuffer::new(2, 44100, packet.samples()); - self.rodio_sink.append(source); - - // Chunk sizes seem to be about 256 to 3000 ish items long. - // Assuming they're on average 1628 then a half second buffer is: - // 44100 elements --> about 27 chunks - while self.rodio_sink.len() > 26 { - // sleep and wait for rodio to drain a bit - thread::sleep(time::Duration::from_millis(10)); - } - Ok(()) - } -} - -#[cfg(all( - feature = "rodiojack-backend", - any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd") -))] -impl Sink for JackRodioSink { - fn start(&mut self) -> io::Result<()> { - // More similar to an "unpause" than "play". Doesn't undo "stop". - // self.rodio_sink.play(); - Ok(()) - } - - fn stop(&mut self) -> io::Result<()> { - // This will immediately stop playback, but the sink is then unusable. - // We just have to let the current buffer play till the end. - // self.rodio_sink.stop(); - Ok(()) - } - - fn write(&mut self, data: &[f32]) -> io::Result<()> { - let source = rodio::buffer::SamplesBuffer::new(2, 44100, data); - self.jackrodio_sink.append(source); - - // Chunk sizes seem to be about 256 to 3000 ish items long. - // Assuming they're on average 1628 then a half second buffer is: - // 44100 elements --> about 27 chunks - while self.jackrodio_sink.len() > 26 { - // sleep and wait for rodio to drain a bit - thread::sleep(time::Duration::from_millis(10)); - } - Ok(()) + .expect("JACK host not found"); + info!( + "Using JACK Rodio sink with format {:?} and cpal JACK host", + format + ); + Self::open_sink(&host, device, format) } } diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 727615d1..6e52b322 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -1,57 +1,98 @@ use super::{Open, Sink}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use sdl2::audio::{AudioQueue, AudioSpecDesired}; -use std::{io, thread, time}; +use std::{io, mem, thread, time}; -type Channel = f32; - -pub struct SdlSink { - queue: AudioQueue, +pub enum SdlSink { + F32(AudioQueue), + S16(AudioQueue), } impl Open for SdlSink { - fn open(device: Option) -> SdlSink { - debug!("Using SDL sink"); + fn open(device: Option, format: AudioFormat) -> SdlSink { + info!("Using SDL sink with format: {:?}", format); if device.is_some() { panic!("SDL sink does not support specifying a device name"); } - let ctx = sdl2::init().expect("Could not init SDL"); - let audio = ctx.audio().expect("Could not init SDL audio subsystem"); + let ctx = sdl2::init().expect("could not initialize SDL"); + let audio = ctx + .audio() + .expect("could not initialize SDL audio subsystem"); let desired_spec = AudioSpecDesired { - freq: Some(44_100), - channels: Some(2), + freq: Some(SAMPLE_RATE as i32), + channels: Some(NUM_CHANNELS), samples: None, }; - let queue = audio - .open_queue(None, &desired_spec) - .expect("Could not open SDL audio device"); - SdlSink { queue: queue } + macro_rules! open_sink { + ($sink: expr, $type: ty) => {{ + let queue: AudioQueue<$type> = audio + .open_queue(None, &desired_spec) + .expect("could not open SDL audio device"); + $sink(queue) + }}; + } + match format { + AudioFormat::F32 => open_sink!(SdlSink::F32, f32), + AudioFormat::S16 => open_sink!(SdlSink::S16, i16), + } } } impl Sink for SdlSink { fn start(&mut self) -> io::Result<()> { - self.queue.clear(); - self.queue.resume(); + macro_rules! start_sink { + ($queue: expr) => {{ + $queue.clear(); + $queue.resume(); + }}; + } + match self { + SdlSink::F32(queue) => start_sink!(queue), + SdlSink::S16(queue) => start_sink!(queue), + }; Ok(()) } fn stop(&mut self) -> io::Result<()> { - self.queue.pause(); - self.queue.clear(); + macro_rules! stop_sink { + ($queue: expr) => {{ + $queue.pause(); + $queue.clear(); + }}; + } + match self { + SdlSink::F32(queue) => stop_sink!(queue), + SdlSink::S16(queue) => stop_sink!(queue), + }; Ok(()) } fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - 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)); + macro_rules! drain_sink { + ($queue: expr, $size: expr) => {{ + // sleep and wait for sdl thread to drain the queue a bit + while $queue.size() > (NUM_CHANNELS as u32 * $size as u32 * SAMPLE_RATE) { + thread::sleep(time::Duration::from_millis(10)); + } + }}; } - self.queue.queue(packet.samples()); + match self { + SdlSink::F32(queue) => { + drain_sink!(queue, mem::size_of::()); + queue.queue(packet.samples()) + } + SdlSink::S16(queue) => { + drain_sink!(queue, mem::size_of::()); + let samples_s16: Vec = AudioPacket::f32_to_s16(packet.samples()); + queue.queue(&samples_s16) + } + }; Ok(()) } } diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index 123e0233..586bb75b 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -1,22 +1,25 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkAsBytes}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; use shell_words::split; use std::io::{self, Write}; -use std::mem; use std::process::{Child, Command, Stdio}; -use std::slice; pub struct SubprocessSink { shell_command: String, child: Option, + format: AudioFormat, } impl Open for SubprocessSink { - fn open(shell_command: Option) -> SubprocessSink { + fn open(shell_command: Option, format: AudioFormat) -> SubprocessSink { + info!("Using subprocess sink with format: {:?}", format); + if let Some(shell_command) = shell_command { SubprocessSink { shell_command: shell_command, child: None, + format: format, } } else { panic!("subprocess sink requires specifying a shell command"); @@ -44,16 +47,15 @@ impl Sink for SubprocessSink { Ok(()) } - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - let data: &[u8] = unsafe { - slice::from_raw_parts( - packet.samples().as_ptr() as *const u8, - packet.samples().len() * mem::size_of::(), - ) - }; + sink_as_bytes!(); +} + +impl SinkAsBytes for SubprocessSink { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { if let Some(child) = &mut self.child { let child_stdin = child.stdin.as_mut().unwrap(); child_stdin.write_all(data)?; + child_stdin.flush()?; } Ok(()) } diff --git a/playback/src/config.rs b/playback/src/config.rs index be15b268..e1ed8dcf 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -1,3 +1,4 @@ +use std::convert::TryFrom; use std::str::FromStr; #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] @@ -25,6 +26,29 @@ impl Default for Bitrate { } } +#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] +pub enum AudioFormat { + F32, + S16, +} + +impl TryFrom<&String> for AudioFormat { + type Error = (); + fn try_from(s: &String) -> Result { + match s.to_uppercase().as_str() { + "F32" => Ok(AudioFormat::F32), + "S16" => Ok(AudioFormat::S16), + _ => unimplemented!(), + } + } +} + +impl Default for AudioFormat { + fn default() -> AudioFormat { + AudioFormat::F32 + } +} + #[derive(Clone, Debug)] pub enum NormalisationType { Album, diff --git a/playback/src/player.rs b/playback/src/player.rs index b6b8ad5f..dbc09695 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -25,8 +25,12 @@ use crate::audio_backend::Sink; use crate::metadata::{AudioItem, FileFormat}; use crate::mixer::AudioFilter; +pub const SAMPLE_RATE: u32 = 44100; +pub const NUM_CHANNELS: u8 = 2; +pub const SAMPLES_PER_SECOND: u32 = SAMPLE_RATE as u32 * NUM_CHANNELS as u32; + const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000; -const SAMPLES_PER_SECOND: u32 = 44100 * 2; +const DB_VOLTAGE_RATIO: f32 = 20.0; pub struct Player { commands: Option>, @@ -202,11 +206,11 @@ pub struct NormalisationData { impl NormalisationData { pub fn db_to_ratio(db: f32) -> f32 { - return f32::powf(10.0, db / 20.0); + return f32::powf(10.0, db / DB_VOLTAGE_RATIO); } pub fn ratio_to_db(ratio: f32) -> f32 { - return ratio.log10() * 20.0; + return ratio.log10() * DB_VOLTAGE_RATIO; } fn parse_from_file(mut file: T) -> Result { @@ -937,8 +941,8 @@ impl Future for PlayerInternal { if !self.config.passthrough { if let Some(ref packet) = packet { - *stream_position_pcm = - *stream_position_pcm + (packet.samples().len() / 2) as u64; + *stream_position_pcm = *stream_position_pcm + + (packet.samples().len() / NUM_CHANNELS as usize) as u64; let stream_position_millis = Self::position_pcm_to_ms(*stream_position_pcm); diff --git a/src/main.rs b/src/main.rs index 91a58659..a7cd8b30 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use futures::sync::mpsc::UnboundedReceiver; use futures::{Async, Future, Poll, Stream}; use log::{error, info, trace, warn}; use sha1::{Digest, Sha1}; +use std::convert::TryFrom; use std::env; use std::io::{stderr, Write}; use std::mem; @@ -22,7 +23,9 @@ use librespot::core::version; use librespot::connect::discovery::{discovery, DiscoveryStream}; use librespot::connect::spirc::{Spirc, SpircTask}; use librespot::playback::audio_backend::{self, Sink, BACKENDS}; -use librespot::playback::config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}; +use librespot::playback::config::{ + AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, +}; use librespot::playback::mixer::{self, Mixer, MixerConfig}; use librespot::playback::player::{NormalisationData, Player, PlayerEvent}; @@ -85,7 +88,8 @@ fn print_version() { #[derive(Clone)] struct Setup { - backend: fn(Option) -> Box, + format: AudioFormat, + backend: fn(Option, AudioFormat) -> Box, device: Option, mixer: fn(Option) -> Box, @@ -149,6 +153,12 @@ fn setup(args: &[String]) -> Setup { "Audio device to use. Use '?' to list options if using portaudio or alsa", "DEVICE", ) + .optopt( + "", + "format", + "Output format (F32 or S16). Defaults to F32", + "FORMAT", + ) .optopt("", "mixer", "Mixer to use (alsa or softvol)", "MIXER") .optopt( "m", @@ -292,9 +302,15 @@ fn setup(args: &[String]) -> Setup { let backend = audio_backend::find(backend_name).expect("Invalid backend"); + let format = matches + .opt_str("format") + .as_ref() + .map(|format| AudioFormat::try_from(format).expect("Invalid output format")) + .unwrap_or(AudioFormat::default()); + let device = matches.opt_str("device"); if device == Some("?".into()) { - backend(device); + backend(device, format); exit(0); } @@ -496,6 +512,7 @@ fn setup(args: &[String]) -> Setup { let enable_discovery = !matches.opt_present("disable-discovery"); Setup { + format: format, backend: backend, cache: cache, session_config: session_config, @@ -517,7 +534,8 @@ struct Main { player_config: PlayerConfig, session_config: SessionConfig, connect_config: ConnectConfig, - backend: fn(Option) -> Box, + format: AudioFormat, + backend: fn(Option, AudioFormat) -> Box, device: Option, mixer: fn(Option) -> Box, mixer_config: MixerConfig, @@ -547,6 +565,7 @@ impl Main { session_config: setup.session_config, player_config: setup.player_config, connect_config: setup.connect_config, + format: setup.format, backend: setup.backend, device: setup.device, mixer: setup.mixer, @@ -626,11 +645,12 @@ impl Future for Main { let connect_config = self.connect_config.clone(); let audio_filter = mixer.get_audio_filter(); + let format = self.format; let backend = self.backend; let device = self.device.clone(); let (player, event_channel) = Player::new(player_config, session.clone(), audio_filter, move || { - (backend)(device) + (backend)(device, format) }); if self.emit_sink_events { From 6379926eb4c2dd786ccde075c6f1779aae3238ca Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 12 Mar 2021 23:18:18 +0100 Subject: [PATCH 11/46] Fix example --- examples/play.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/play.rs b/examples/play.rs index 4ba4c5b5..2c239ef3 100644 --- a/examples/play.rs +++ b/examples/play.rs @@ -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; @@ -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 { @@ -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); From a4ef174fd00bac3d2be779653ccf35617aa0f379 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 12 Mar 2021 23:48:41 +0100 Subject: [PATCH 12/46] Fix Alsa backend for 64-bit systems --- playback/src/audio_backend/alsa.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 92b71f40..ce758dcb 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -11,7 +11,7 @@ use std::process::exit; use std::{io, mem}; const BUFFERED_LATENCY: f32 = 0.125; // seconds -const BUFFERED_PERIODS: u8 = 4; +const BUFFERED_PERIODS: Frames = 4; pub struct AlsaSink { pcm: Option, @@ -48,7 +48,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box // latency = period_size * periods / (rate * bytes_per_frame) // For stereo samples encoded as 32-bit float, one frame has a length of eight bytes. let mut period_size = ((SAMPLES_PER_SECOND * sample_size as u32) as f32 - * (BUFFERED_LATENCY / BUFFERED_PERIODS as f32)) as i32; + * (BUFFERED_LATENCY / BUFFERED_PERIODS as f32)) as Frames; // Set hardware parameters: 44100 Hz / stereo / 32-bit float or 16-bit signed integer { @@ -58,7 +58,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box 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 as i32)?; + hwp.set_buffer_size_near(period_size * BUFFERED_PERIODS)?; pcm.hw_params(&hwp)?; let swp = pcm.sw_params_current()?; @@ -101,8 +101,7 @@ impl Sink for AlsaSink { 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 * BUFFERED_PERIODS as i32) as usize); + self.buffer = Vec::with_capacity((period_size * BUFFERED_PERIODS) as usize); } Err(e) => { error!("Alsa error PCM open {}", e); From 5f26a745d7dbe085cb52f4e433ae3438f54c5f95 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 13 Mar 2021 23:43:24 +0100 Subject: [PATCH 13/46] Add support for S32 output format While at it, add a small tweak when converting "silent" samples from float to integer. This ensures 0.0 converts to 0 and vice versa. --- audio/src/lib.rs | 25 ++++++++++++++++++++---- audio/src/libvorbis_decoder.rs | 8 +++++++- playback/src/audio_backend/alsa.rs | 6 ++++++ playback/src/audio_backend/mod.rs | 4 ++++ playback/src/audio_backend/portaudio.rs | 19 ++++++++++++++---- playback/src/audio_backend/pulseaudio.rs | 1 + playback/src/audio_backend/rodio.rs | 23 ++++++++++++++-------- playback/src/audio_backend/sdl.rs | 9 +++++++++ playback/src/config.rs | 2 ++ src/main.rs | 2 +- 10 files changed, 81 insertions(+), 18 deletions(-) diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 8a9f88f5..cafadae9 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -37,6 +37,22 @@ pub enum AudioPacket { OggData(Vec), } +// Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX] while maintaining DC linearity. +macro_rules! convert_samples_to { + ($type: ident, $samples: expr) => { + $samples + .iter() + .map(|sample| { + if *sample == 0.0 { + 0 as $type + } else { + (*sample as f64 * (std::$type::MAX as f64 + 0.5) - 0.5) as $type + } + }) + .collect() + }; +} + impl AudioPacket { pub fn samples(&self) -> &[f32] { match self { @@ -59,11 +75,12 @@ impl AudioPacket { } } + pub fn f32_to_s32(samples: &[f32]) -> Vec { + convert_samples_to!(i32, samples) + } + pub fn f32_to_s16(samples: &[f32]) -> Vec { - samples - .iter() - .map(|sample| (*sample as f64 * (0x7FFF as f64 + 0.5) - 0.5) as i16) - .collect() + convert_samples_to!(i16, samples) } } diff --git a/audio/src/libvorbis_decoder.rs b/audio/src/libvorbis_decoder.rs index e7ccc984..449caaeb 100644 --- a/audio/src/libvorbis_decoder.rs +++ b/audio/src/libvorbis_decoder.rs @@ -45,7 +45,13 @@ where packet .data .iter() - .map(|sample| ((*sample as f64 + 0.5) / (0x7FFF as f64 + 0.5)) as f32) + .map(|sample| { + if *sample == 0 { + 0.0 + } else { + ((*sample as f64 + 0.5) / (0x7FFF as f64 + 0.5)) as f32 + } + }) .collect(), ))); } diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index ce758dcb..4d9f19ed 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -41,6 +41,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box let pcm = PCM::new(dev_name, Direction::Playback, false)?; let (alsa_format, sample_size) = match format { AudioFormat::F32 => (Format::float(), mem::size_of::()), + AudioFormat::S32 => (Format::s32(), mem::size_of::()), AudioFormat::S16 => (Format::s16(), mem::size_of::()), }; @@ -157,6 +158,11 @@ impl AlsaSink { let io = pcm.io_f32().unwrap(); io.writei(&self.buffer) } + AudioFormat::S32 => { + let io = pcm.io_i32().unwrap(); + let buf_s32: Vec = AudioPacket::f32_to_s32(&self.buffer); + io.writei(&buf_s32[..]) + } AudioFormat::S16 => { let io = pcm.io_i16().unwrap(); let buf_s16: Vec = AudioPacket::f32_to_s16(&self.buffer); diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 550ebb84..bc10e88a 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -28,6 +28,10 @@ macro_rules! sink_as_bytes { match packet { AudioPacket::Samples(samples) => match self.format { AudioFormat::F32 => self.write_bytes(samples.as_bytes()), + AudioFormat::S32 => { + let samples_s32 = AudioPacket::f32_to_s32(samples); + self.write_bytes(samples_s32.as_bytes()) + } AudioFormat::S16 => { let samples_s16 = AudioPacket::f32_to_s16(samples); self.write_bytes(samples_s16.as_bytes()) diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 70caedd7..a7aa38cc 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -14,6 +14,10 @@ pub enum PortAudioSink<'a> { Option>, StreamParameters, ), + S32( + Option>, + StreamParameters, + ), S16( Option>, StreamParameters, @@ -70,19 +74,20 @@ impl<'a> Open for PortAudioSink<'a> { }; macro_rules! open_sink { - ($sink: expr, $data: expr) => {{ + ($sink: expr, $type: ty) => {{ let params = StreamParameters { device: device_idx, channel_count: NUM_CHANNELS as u32, suggested_latency: latency, - data: $data, + data: 0.0 as $type, }; $sink(None, params) }}; } match format { - AudioFormat::F32 => open_sink!(PortAudioSink::F32, 0.0), - AudioFormat::S16 => open_sink!(PortAudioSink::S16, 0), + AudioFormat::F32 => open_sink!(PortAudioSink::F32, f32), + AudioFormat::S32 => open_sink!(PortAudioSink::S32, i32), + AudioFormat::S16 => open_sink!(PortAudioSink::S16, i16), } } } @@ -109,6 +114,7 @@ impl<'a> Sink for PortAudioSink<'a> { } match self { PortAudioSink::F32(stream, parameters) => start_sink!(stream, parameters), + PortAudioSink::S32(stream, parameters) => start_sink!(stream, parameters), PortAudioSink::S16(stream, parameters) => start_sink!(stream, parameters), }; @@ -124,6 +130,7 @@ impl<'a> Sink for PortAudioSink<'a> { } match self { PortAudioSink::F32(stream, _parameters) => stop_sink!(stream), + PortAudioSink::S32(stream, _parameters) => stop_sink!(stream), PortAudioSink::S16(stream, _parameters) => stop_sink!(stream), }; @@ -141,6 +148,10 @@ impl<'a> Sink for PortAudioSink<'a> { let samples = packet.samples(); write_sink!(stream, &samples) } + PortAudioSink::S32(stream, _parameters) => { + let samples_s32: Vec = AudioPacket::f32_to_s32(packet.samples()); + write_sink!(stream, &samples_s32) + } PortAudioSink::S16(stream, _parameters) => { let samples_s16: Vec = AudioPacket::f32_to_s16(packet.samples()); write_sink!(stream, &samples_s16) diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 8c1e8e83..a2d89f21 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -22,6 +22,7 @@ impl Open for PulseAudioSink { let pulse_format = match format { AudioFormat::F32 => pulse::sample::Format::F32le, + AudioFormat::S32 => pulse::sample::Format::S32le, AudioFormat::S16 => pulse::sample::Format::S16le, }; diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 7571aa20..97e03ec0 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -7,6 +7,8 @@ use cpal::traits::{DeviceTrait, HostTrait}; use std::process::exit; use std::{io, thread, time}; +const FORMAT_NOT_SUPPORTED: &'static str = "Rodio currently does not support that output format"; + // most code is shared between RodioSink and JackRodioSink macro_rules! rodio_sink { ($name: ident) => { @@ -27,12 +29,13 @@ macro_rules! rodio_sink { AudioFormat::F32 => { let source = rodio::buffer::SamplesBuffer::new(2, 44100, samples); self.rodio_sink.append(source) - } + }, AudioFormat::S16 => { let samples_s16: Vec = AudioPacket::f32_to_s16(samples); let source = rodio::buffer::SamplesBuffer::new(2, 44100, samples_s16); self.rodio_sink.append(source) - } + }, + _ => panic!(FORMAT_NOT_SUPPORTED), }; // Chunk sizes seem to be about 256 to 3000 ish items long. @@ -48,12 +51,16 @@ macro_rules! rodio_sink { impl $name { fn open_sink(host: &cpal::Host, device: Option, format: AudioFormat) -> $name { - if format != AudioFormat::S16 { - #[cfg(target_os = "linux")] - { - warn!("Rodio output to Alsa is known to cause garbled sound on output formats other than 16-bit signed integer."); - warn!("Consider using `--backend alsa` OR `--format {:?}`", AudioFormat::S16); - } + match format { + AudioFormat::F32 => { + #[cfg(target_os = "linux")] + { + warn!("Rodio output to Alsa is known to cause garbled sound on output formats other than 16-bit signed integer."); + warn!("Consider using `--backend alsa` OR `--format {:?}`", AudioFormat::S16); + } + }, + AudioFormat::S16 => {}, + _ => panic!(FORMAT_NOT_SUPPORTED), } let rodio_device = match_device(&host, device); diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 6e52b322..ef8c1836 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -7,6 +7,7 @@ use std::{io, mem, thread, time}; pub enum SdlSink { F32(AudioQueue), + S32(AudioQueue), S16(AudioQueue), } @@ -39,6 +40,7 @@ impl Open for SdlSink { } match format { AudioFormat::F32 => open_sink!(SdlSink::F32, f32), + AudioFormat::S32 => open_sink!(SdlSink::S32, i32), AudioFormat::S16 => open_sink!(SdlSink::S16, i16), } } @@ -54,6 +56,7 @@ impl Sink for SdlSink { } match self { SdlSink::F32(queue) => start_sink!(queue), + SdlSink::S32(queue) => start_sink!(queue), SdlSink::S16(queue) => start_sink!(queue), }; Ok(()) @@ -68,6 +71,7 @@ impl Sink for SdlSink { } match self { SdlSink::F32(queue) => stop_sink!(queue), + SdlSink::S32(queue) => stop_sink!(queue), SdlSink::S16(queue) => stop_sink!(queue), }; Ok(()) @@ -87,6 +91,11 @@ impl Sink for SdlSink { drain_sink!(queue, mem::size_of::()); queue.queue(packet.samples()) } + SdlSink::S32(queue) => { + drain_sink!(queue, mem::size_of::()); + let samples_s32: Vec = AudioPacket::f32_to_s32(packet.samples()); + queue.queue(&samples_s32) + } SdlSink::S16(queue) => { drain_sink!(queue, mem::size_of::()); let samples_s16: Vec = AudioPacket::f32_to_s16(packet.samples()); diff --git a/playback/src/config.rs b/playback/src/config.rs index e1ed8dcf..80771582 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -29,6 +29,7 @@ impl Default for Bitrate { #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] pub enum AudioFormat { F32, + S32, S16, } @@ -37,6 +38,7 @@ impl TryFrom<&String> for AudioFormat { fn try_from(s: &String) -> Result { match s.to_uppercase().as_str() { "F32" => Ok(AudioFormat::F32), + "S32" => Ok(AudioFormat::S32), "S16" => Ok(AudioFormat::S16), _ => unimplemented!(), } diff --git a/src/main.rs b/src/main.rs index a7cd8b30..b4cfc437 100644 --- a/src/main.rs +++ b/src/main.rs @@ -156,7 +156,7 @@ fn setup(args: &[String]) -> Setup { .optopt( "", "format", - "Output format (F32 or S16). Defaults to F32", + "Output format (F32, S32 or S16). Defaults to F32", "FORMAT", ) .optopt("", "mixer", "Mixer to use (alsa or softvol)", "MIXER") From 309e26456ef2d381cf0a1338ec45fac2f3b25665 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 14 Mar 2021 14:28:16 +0100 Subject: [PATCH 14/46] Rename steepness to knee --- playback/src/config.rs | 4 ++-- playback/src/player.rs | 9 +++------ src/main.rs | 18 +++++++----------- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/playback/src/config.rs b/playback/src/config.rs index 80771582..312f1709 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -107,7 +107,7 @@ pub struct PlayerConfig { pub normalisation_threshold: f32, pub normalisation_attack: f32, pub normalisation_release: f32, - pub normalisation_steepness: f32, + pub normalisation_knee: f32, pub gapless: bool, pub passthrough: bool, } @@ -123,7 +123,7 @@ impl Default for PlayerConfig { normalisation_threshold: -1.0, normalisation_attack: 0.005, normalisation_release: 0.1, - normalisation_steepness: 1.0, + normalisation_knee: 1.0, gapless: true, passthrough: false, } diff --git a/playback/src/player.rs b/playback/src/player.rs index dbc09695..0a573c93 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -271,10 +271,7 @@ impl NormalisationData { if config.normalisation_method == NormalisationMethod::Dynamic { debug!("Normalisation Attack: {:?}", config.normalisation_attack); debug!("Normalisation Release: {:?}", config.normalisation_release); - debug!( - "Normalisation Steepness: {:?}", - config.normalisation_steepness - ); + debug!("Normalisation Knee: {:?}", config.normalisation_knee); } normalisation_factor @@ -1176,7 +1173,7 @@ impl PlayerInternal { if self.config.normalisation_method == NormalisationMethod::Dynamic { if self.limiter_active { - // "S"-shaped curve with a configurable steepness during attack and release: + // "S"-shaped curve with a configurable knee during attack and release: // - > 1.0 yields soft knees at start and end, steeper in between // - 1.0 yields a linear function from 0-100% // - between 0.0 and 1.0 yields hard knees at start and end, flatter in between @@ -1191,7 +1188,7 @@ impl PlayerInternal { + f32::powf( shaped_limiter_strength / (1.0 - shaped_limiter_strength), - -1.0 * self.config.normalisation_steepness, + -1.0 * self.config.normalisation_knee, )); } actual_normalisation_factor = diff --git a/src/main.rs b/src/main.rs index b4cfc437..bf553a86 100644 --- a/src/main.rs +++ b/src/main.rs @@ -238,9 +238,9 @@ fn setup(args: &[String]) -> Setup { ) .optopt( "", - "normalisation-steepness", - "Steepness of the dynamic limiting curve. Default is 1.0.", - "STEEPNESS", + "normalisation-knee", + "Knee steepness of the dynamic limiter. Default is 1.0.", + "KNEE", ) .optopt( "", @@ -475,14 +475,10 @@ fn setup(args: &[String]) -> Setup { .map(|release| release.parse::().expect("Invalid release float value")) .unwrap_or(PlayerConfig::default().normalisation_release * MILLIS) / MILLIS, - normalisation_steepness: matches - .opt_str("normalisation-steepness") - .map(|steepness| { - steepness - .parse::() - .expect("Invalid steepness float value") - }) - .unwrap_or(PlayerConfig::default().normalisation_steepness), + normalisation_knee: matches + .opt_str("normalisation-knee") + .map(|knee| knee.parse::().expect("Invalid knee float value")) + .unwrap_or(PlayerConfig::default().normalisation_knee), passthrough, } }; From 9dcaeee6d445c0942eac6dd8abc74a2f53486c17 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 16 Mar 2021 20:22:00 +0100 Subject: [PATCH 15/46] Default to S16 output --- playback/src/audio_backend/jackaudio.rs | 8 ++------ playback/src/config.rs | 2 +- src/main.rs | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 295941a4..2412d07c 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -42,14 +42,10 @@ impl ProcessHandler for JackData { impl Open for JackSink { fn open(client_name: Option, format: AudioFormat) -> JackSink { - info!("Using JACK sink with format {:?}", format); - if format != AudioFormat::F32 { - panic!( - "JACK sink only supports 32-bit floating point output. Use `--format {:?}`", - AudioFormat::F32 - ); + warn!("JACK currently does not support {:?} output", format); } + info!("Using JACK sink with format {:?}", AudioFormat::F32); let client_name = client_name.unwrap_or("librespot".to_string()); let (client, _status) = diff --git a/playback/src/config.rs b/playback/src/config.rs index 312f1709..7348b7bf 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -47,7 +47,7 @@ impl TryFrom<&String> for AudioFormat { impl Default for AudioFormat { fn default() -> AudioFormat { - AudioFormat::F32 + AudioFormat::S16 } } diff --git a/src/main.rs b/src/main.rs index bf553a86..07b85b30 100644 --- a/src/main.rs +++ b/src/main.rs @@ -156,7 +156,7 @@ fn setup(args: &[String]) -> Setup { .optopt( "", "format", - "Output format (F32, S32 or S16). Defaults to F32", + "Output format (F32, S32 or S16). Defaults to S16", "FORMAT", ) .optopt("", "mixer", "Mixer to use (alsa or softvol)", "MIXER") From 770ea15498a0f1cfc7b9986f0954f4150258c29f Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 17 Mar 2021 00:00:27 +0100 Subject: [PATCH 16/46] Add support for S24 and S24_3 output formats --- Cargo.lock | 1 + audio/Cargo.toml | 1 + audio/src/lib.rs | 33 ++++++++++++--- playback/src/audio_backend/alsa.rs | 52 ++++++++++-------------- playback/src/audio_backend/gstreamer.rs | 19 ++++++--- playback/src/audio_backend/jackaudio.rs | 2 +- playback/src/audio_backend/mod.rs | 8 ++++ playback/src/audio_backend/pipe.rs | 2 +- playback/src/audio_backend/portaudio.rs | 38 +++++++++++------ playback/src/audio_backend/pulseaudio.rs | 5 ++- playback/src/audio_backend/rodio.rs | 8 ++-- playback/src/audio_backend/sdl.rs | 36 ++++++++++------ playback/src/config.rs | 28 ++++++++++--- src/main.rs | 2 +- 14 files changed, 155 insertions(+), 80 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9a2e42ea..2296cfed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1473,6 +1473,7 @@ dependencies = [ "ogg", "tempfile", "vorbis", + "zerocopy", ] [[package]] diff --git a/audio/Cargo.toml b/audio/Cargo.toml index b7e6e35f..06f1dda6 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -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 } diff --git a/audio/src/lib.rs b/audio/src/lib.rs index cafadae9..86c5b4ae 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -31,23 +31,35 @@ pub use fetch::{ READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS, }; use std::fmt; +use zerocopy::AsBytes; pub enum AudioPacket { Samples(Vec), OggData(Vec), } +#[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| { - if *sample == 0.0 { - 0 as $type - } else { - (*sample as f64 * (std::$type::MAX as f64 + 0.5) - 0.5) as $type - } + (*sample as f64 * (std::$type::MAX as f64 + 0.5) - 0.5) as $type >> $shift }) .collect() }; @@ -79,6 +91,17 @@ impl AudioPacket { convert_samples_to!(i32, samples) } + pub fn f32_to_s24(samples: &[f32]) -> Vec { + convert_samples_to!(i32, samples, 8) + } + + pub fn f32_to_s24_3(samples: &[f32]) -> Vec { + Self::f32_to_s32(samples) + .iter() + .map(|sample| i24::pcm_from_i32(*sample)) + .collect() + } + pub fn f32_to_s16(samples: &[f32]) -> Vec { convert_samples_to!(i16, samples) } diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 4d9f19ed..35d0ab11 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -1,4 +1,4 @@ -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}; @@ -7,8 +7,8 @@ use alsa::pcm::{Access, Format, Frames, HwParams, PCM}; use alsa::{Direction, Error, ValueOr}; use std::cmp::min; use std::ffi::CString; +use std::io; use std::process::exit; -use std::{io, mem}; const BUFFERED_LATENCY: f32 = 0.125; // seconds const BUFFERED_PERIODS: Frames = 4; @@ -17,7 +17,7 @@ pub struct AlsaSink { pcm: Option, format: AudioFormat, device: String, - buffer: Vec, + buffer: Vec, } fn list_outputs() { @@ -39,16 +39,18 @@ fn list_outputs() { fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box> { let pcm = PCM::new(dev_name, Direction::Playback, false)?; - let (alsa_format, sample_size) = match format { - AudioFormat::F32 => (Format::float(), mem::size_of::()), - AudioFormat::S32 => (Format::s32(), mem::size_of::()), - AudioFormat::S16 => (Format::s16(), mem::size_of::()), + 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 stereo samples encoded as 32-bit float, one frame has a length of eight bytes. - let mut period_size = ((SAMPLES_PER_SECOND * sample_size as u32) as f32 + 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 @@ -85,7 +87,7 @@ impl Open for AlsaSink { } .to_string(); - AlsaSink { + Self { pcm: None, format: format, device: name, @@ -102,7 +104,9 @@ impl Sink for AlsaSink { 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 * BUFFERED_PERIODS) 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); @@ -121,7 +125,7 @@ impl Sink for AlsaSink { { // Write any leftover data in the period buffer // before draining the actual buffer - self.write_buf().expect("could not flush buffer"); + self.write_bytes(&[]).expect("could not flush buffer"); let pcm = self.pcm.as_mut().unwrap(); pcm.drain().unwrap(); } @@ -129,9 +133,12 @@ impl Sink for AlsaSink { 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(), @@ -153,23 +160,8 @@ impl Sink for AlsaSink { impl AlsaSink { fn write_buf(&mut self) -> io::Result<()> { let pcm = self.pcm.as_mut().unwrap(); - let io_result = match self.format { - AudioFormat::F32 => { - let io = pcm.io_f32().unwrap(); - io.writei(&self.buffer) - } - AudioFormat::S32 => { - let io = pcm.io_i32().unwrap(); - let buf_s32: Vec = AudioPacket::f32_to_s32(&self.buffer); - io.writei(&buf_s32[..]) - } - AudioFormat::S16 => { - let io = pcm.io_i16().unwrap(); - let buf_s16: Vec = AudioPacket::f32_to_s16(&self.buffer); - io.writei(&buf_s16[..]) - } - }; - match io_result { + let io = pcm.io_bytes(); + match io.writei(&self.buffer) { Ok(_) => (), Err(err) => pcm.try_recover(err, false).unwrap(), }; diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 17ad86e6..3695857e 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -18,11 +18,18 @@ pub struct GstreamerSink { impl Open for GstreamerSink { fn open(device: Option, format: AudioFormat) -> GstreamerSink { info!("Using GStreamer sink with format: {:?}", format); - gst::init().expect("failed to init GStreamer!"); + + // GStreamer calls S24 and S24_3 different from the rest of the world + let gst_format = match format { + AudioFormat::S24 => "S24_32".to_string(), + AudioFormat::S24_3 => "S24".to_string(), + _ => format!("{:?}", format), + }; + let pipeline_str_preamble = format!( - r#"appsrc caps="audio/x-raw,format={:?},layout=interleaved,channels={},rate={}" block=true max-bytes=4096 name=appsrc0 "#, - format, NUM_CHANNELS, SAMPLE_RATE + "appsrc caps=\"audio/x-raw,format={}LE,layout=interleaved,channels={},rate={}\" block=true max-bytes=4096 name=appsrc0 ", + gst_format, NUM_CHANNELS, SAMPLE_RATE ); let pipeline_str_rest = r#" ! audioconvert ! autoaudiosink"#; let pipeline_str: String = match device { @@ -47,7 +54,7 @@ impl Open for GstreamerSink { let bufferpool = gst::BufferPool::new(); let appsrc_caps = appsrc.get_caps().expect("couldn't get appsrc caps"); let mut conf = bufferpool.get_config(); - conf.set_params(Some(&appsrc_caps), 8192, 0, 0); + conf.set_params(Some(&appsrc_caps), 2048 * format.size() as u32, 0, 0); bufferpool .set_config(conf) .expect("couldn't configure the buffer pool"); @@ -55,7 +62,7 @@ impl Open for GstreamerSink { .set_active(true) .expect("couldn't activate buffer pool"); - let (tx, rx) = sync_channel::>(128); + let (tx, rx) = sync_channel::>(64 * format.size()); thread::spawn(move || { for data in rx { let buffer = bufferpool.acquire_buffer(None); @@ -99,7 +106,7 @@ impl Open for GstreamerSink { .set_state(gst::State::Playing) .expect("unable to set the pipeline to the `Playing` state"); - GstreamerSink { + Self { tx: tx, pipeline: pipeline, format: format, diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 2412d07c..05c6c317 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -61,7 +61,7 @@ impl Open for JackSink { }; let active_client = AsyncClient::new(client, (), jack_data).unwrap(); - JackSink { + Self { send: tx, active_client: active_client, } diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index bc10e88a..9c46dbe4 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -32,6 +32,14 @@ macro_rules! sink_as_bytes { let samples_s32 = AudioPacket::f32_to_s32(samples); self.write_bytes(samples_s32.as_bytes()) } + AudioFormat::S24 => { + let samples_s24 = AudioPacket::f32_to_s24(samples); + self.write_bytes(samples_s24.as_bytes()) + } + AudioFormat::S24_3 => { + let samples_s24_3 = AudioPacket::f32_to_s24_3(samples); + self.write_bytes(samples_s24_3.as_bytes()) + } AudioFormat::S16 => { let samples_s16 = AudioPacket::f32_to_s16(samples); self.write_bytes(samples_s16.as_bytes()) diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index 3a90d06f..ae77e320 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -18,7 +18,7 @@ impl Open for StdoutSink { _ => Box::new(io::stdout()), }; - StdoutSink { + Self { output: output, format: format, } diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index a7aa38cc..213f2d02 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -18,6 +18,10 @@ pub enum PortAudioSink<'a> { Option>, StreamParameters, ), + S24( + Option>, + StreamParameters, + ), S16( Option>, StreamParameters, @@ -85,9 +89,13 @@ impl<'a> Open for PortAudioSink<'a> { }}; } match format { - AudioFormat::F32 => open_sink!(PortAudioSink::F32, f32), - AudioFormat::S32 => open_sink!(PortAudioSink::S32, i32), - AudioFormat::S16 => open_sink!(PortAudioSink::S16, i16), + AudioFormat::F32 => open_sink!(Self::F32, f32), + AudioFormat::S32 => open_sink!(Self::S32, i32), + AudioFormat::S24 => open_sink!(Self::S24, i32), + AudioFormat::S24_3 => { + unimplemented!("PortAudio currently does not support S24_3 output") + } + AudioFormat::S16 => open_sink!(Self::S16, i16), } } } @@ -113,9 +121,10 @@ impl<'a> Sink for PortAudioSink<'a> { }}; } match self { - PortAudioSink::F32(stream, parameters) => start_sink!(stream, parameters), - PortAudioSink::S32(stream, parameters) => start_sink!(stream, parameters), - PortAudioSink::S16(stream, parameters) => start_sink!(stream, parameters), + Self::F32(stream, parameters) => start_sink!(stream, parameters), + Self::S32(stream, parameters) => start_sink!(stream, parameters), + Self::S24(stream, parameters) => start_sink!(stream, parameters), + Self::S16(stream, parameters) => start_sink!(stream, parameters), }; Ok(()) @@ -129,9 +138,10 @@ impl<'a> Sink for PortAudioSink<'a> { }}; } match self { - PortAudioSink::F32(stream, _parameters) => stop_sink!(stream), - PortAudioSink::S32(stream, _parameters) => stop_sink!(stream), - PortAudioSink::S16(stream, _parameters) => stop_sink!(stream), + Self::F32(stream, _parameters) => stop_sink!(stream), + Self::S32(stream, _parameters) => stop_sink!(stream), + Self::S24(stream, _parameters) => stop_sink!(stream), + Self::S16(stream, _parameters) => stop_sink!(stream), }; Ok(()) @@ -144,15 +154,19 @@ impl<'a> Sink for PortAudioSink<'a> { }; } let result = match self { - PortAudioSink::F32(stream, _parameters) => { + Self::F32(stream, _parameters) => { let samples = packet.samples(); write_sink!(stream, &samples) } - PortAudioSink::S32(stream, _parameters) => { + Self::S32(stream, _parameters) => { let samples_s32: Vec = AudioPacket::f32_to_s32(packet.samples()); write_sink!(stream, &samples_s32) } - PortAudioSink::S16(stream, _parameters) => { + Self::S24(stream, _parameters) => { + let samples_s24: Vec = AudioPacket::f32_to_s24(packet.samples()); + write_sink!(stream, &samples_s24) + } + Self::S16(stream, _parameters) => { let samples_s16: Vec = AudioPacket::f32_to_s16(packet.samples()); write_sink!(stream, &samples_s16) } diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index a2d89f21..16800eb0 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -20,9 +20,12 @@ impl Open for PulseAudioSink { fn open(device: Option, format: AudioFormat) -> PulseAudioSink { info!("Using PulseAudio sink with format: {:?}", format); + // PulseAudio calls S24 and S24_3 different from the rest of the world let pulse_format = match format { AudioFormat::F32 => pulse::sample::Format::F32le, AudioFormat::S32 => pulse::sample::Format::S32le, + AudioFormat::S24 => pulse::sample::Format::S24_32le, + AudioFormat::S24_3 => pulse::sample::Format::S24le, AudioFormat::S16 => pulse::sample::Format::S16le, }; @@ -33,7 +36,7 @@ impl Open for PulseAudioSink { }; debug_assert!(ss.is_valid()); - PulseAudioSink { + Self { s: None, ss: ss, device: device, diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 97e03ec0..5262a9cc 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -7,8 +7,6 @@ use cpal::traits::{DeviceTrait, HostTrait}; use std::process::exit; use std::{io, thread, time}; -const FORMAT_NOT_SUPPORTED: &'static str = "Rodio currently does not support that output format"; - // most code is shared between RodioSink and JackRodioSink macro_rules! rodio_sink { ($name: ident) => { @@ -35,7 +33,7 @@ macro_rules! rodio_sink { let source = rodio::buffer::SamplesBuffer::new(2, 44100, samples_s16); self.rodio_sink.append(source) }, - _ => panic!(FORMAT_NOT_SUPPORTED), + _ => unimplemented!(), }; // Chunk sizes seem to be about 256 to 3000 ish items long. @@ -60,7 +58,7 @@ macro_rules! rodio_sink { } }, AudioFormat::S16 => {}, - _ => panic!(FORMAT_NOT_SUPPORTED), + _ => unimplemented!("Rodio currently only supports F32 and S16 formats"), } let rodio_device = match_device(&host, device); @@ -71,7 +69,7 @@ macro_rules! rodio_sink { let sink = rodio::Sink::try_new(&stream.1).expect("couldn't create output sink."); debug!("Using Rodio sink"); - $name { + Self { rodio_sink: sink, stream: stream.0, format: format, diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index ef8c1836..32d710f8 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -8,6 +8,7 @@ use std::{io, mem, thread, time}; pub enum SdlSink { F32(AudioQueue), S32(AudioQueue), + S24(AudioQueue), S16(AudioQueue), } @@ -16,7 +17,7 @@ impl Open for SdlSink { info!("Using SDL sink with format: {:?}", format); if device.is_some() { - panic!("SDL sink does not support specifying a device name"); + warn!("SDL sink does not support specifying a device name"); } let ctx = sdl2::init().expect("could not initialize SDL"); @@ -39,9 +40,11 @@ impl Open for SdlSink { }}; } match format { - AudioFormat::F32 => open_sink!(SdlSink::F32, f32), - AudioFormat::S32 => open_sink!(SdlSink::S32, i32), - AudioFormat::S16 => open_sink!(SdlSink::S16, i16), + AudioFormat::F32 => open_sink!(Self::F32, f32), + AudioFormat::S32 => open_sink!(Self::S32, i32), + AudioFormat::S24 => open_sink!(Self::S24, i32), + AudioFormat::S24_3 => unimplemented!("SDL currently does not support S24_3 output"), + AudioFormat::S16 => open_sink!(Self::S16, i16), } } } @@ -55,9 +58,10 @@ impl Sink for SdlSink { }}; } match self { - SdlSink::F32(queue) => start_sink!(queue), - SdlSink::S32(queue) => start_sink!(queue), - SdlSink::S16(queue) => start_sink!(queue), + Self::F32(queue) => start_sink!(queue), + Self::S32(queue) => start_sink!(queue), + Self::S24(queue) => start_sink!(queue), + Self::S16(queue) => start_sink!(queue), }; Ok(()) } @@ -70,9 +74,10 @@ impl Sink for SdlSink { }}; } match self { - SdlSink::F32(queue) => stop_sink!(queue), - SdlSink::S32(queue) => stop_sink!(queue), - SdlSink::S16(queue) => stop_sink!(queue), + Self::F32(queue) => stop_sink!(queue), + Self::S32(queue) => stop_sink!(queue), + Self::S24(queue) => stop_sink!(queue), + Self::S16(queue) => stop_sink!(queue), }; Ok(()) } @@ -87,16 +92,21 @@ impl Sink for SdlSink { }}; } match self { - SdlSink::F32(queue) => { + Self::F32(queue) => { drain_sink!(queue, mem::size_of::()); queue.queue(packet.samples()) } - SdlSink::S32(queue) => { + Self::S32(queue) => { drain_sink!(queue, mem::size_of::()); let samples_s32: Vec = AudioPacket::f32_to_s32(packet.samples()); queue.queue(&samples_s32) } - SdlSink::S16(queue) => { + Self::S24(queue) => { + drain_sink!(queue, mem::size_of::()); + let samples_s24: Vec = AudioPacket::f32_to_s24(packet.samples()); + queue.queue(&samples_s24) + } + Self::S16(queue) => { drain_sink!(queue, mem::size_of::()); let samples_s16: Vec = AudioPacket::f32_to_s16(packet.samples()); queue.queue(&samples_s16) diff --git a/playback/src/config.rs b/playback/src/config.rs index 7348b7bf..630c1406 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -1,4 +1,6 @@ +use crate::audio::i24; use std::convert::TryFrom; +use std::mem; use std::str::FromStr; #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] @@ -30,6 +32,8 @@ impl Default for Bitrate { pub enum AudioFormat { F32, S32, + S24, + S24_3, S16, } @@ -37,17 +41,31 @@ impl TryFrom<&String> for AudioFormat { type Error = (); fn try_from(s: &String) -> Result { match s.to_uppercase().as_str() { - "F32" => Ok(AudioFormat::F32), - "S32" => Ok(AudioFormat::S32), - "S16" => Ok(AudioFormat::S16), - _ => unimplemented!(), + "F32" => Ok(Self::F32), + "S32" => Ok(Self::S32), + "S24" => Ok(Self::S24), + "S24_3" => Ok(Self::S24_3), + "S16" => Ok(Self::S16), + _ => Err(()), } } } impl Default for AudioFormat { fn default() -> AudioFormat { - AudioFormat::S16 + Self::S16 + } +} + +impl AudioFormat { + // not used by all backends + #[allow(dead_code)] + pub fn size(&self) -> usize { + match self { + Self::S24_3 => mem::size_of::(), + Self::S16 => mem::size_of::(), + _ => mem::size_of::(), + } } } diff --git a/src/main.rs b/src/main.rs index 07b85b30..7426e2c4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -156,7 +156,7 @@ fn setup(args: &[String]) -> Setup { .optopt( "", "format", - "Output format (F32, S32 or S16). Defaults to S16", + "Output format (F32, S32, S24, S24_3 or S16). Defaults to S16", "FORMAT", ) .optopt("", "mixer", "Mixer to use (alsa or softvol)", "MIXER") From b94879de62f3fbf11a38aa2fa5df11b24e9998b8 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 18 Mar 2021 20:51:53 +0100 Subject: [PATCH 17/46] Fix GStreamer buffer pool size [ref #660 review] --- playback/src/audio_backend/gstreamer.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 3695857e..d3c736a4 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -26,10 +26,12 @@ impl Open for GstreamerSink { AudioFormat::S24_3 => "S24".to_string(), _ => format!("{:?}", format), }; + let sample_size = format.size(); + let gst_bytes = 2048 * sample_size; let pipeline_str_preamble = format!( - "appsrc caps=\"audio/x-raw,format={}LE,layout=interleaved,channels={},rate={}\" block=true max-bytes=4096 name=appsrc0 ", - gst_format, NUM_CHANNELS, SAMPLE_RATE + "appsrc caps=\"audio/x-raw,format={}LE,layout=interleaved,channels={},rate={}\" block=true max-bytes={} name=appsrc0 ", + gst_format, NUM_CHANNELS, SAMPLE_RATE, gst_bytes ); let pipeline_str_rest = r#" ! audioconvert ! autoaudiosink"#; let pipeline_str: String = match device { @@ -54,7 +56,7 @@ impl Open for GstreamerSink { let bufferpool = gst::BufferPool::new(); let appsrc_caps = appsrc.get_caps().expect("couldn't get appsrc caps"); let mut conf = bufferpool.get_config(); - conf.set_params(Some(&appsrc_caps), 2048 * format.size() as u32, 0, 0); + conf.set_params(Some(&appsrc_caps), 4096 * sample_size as u32, 0, 0); bufferpool .set_config(conf) .expect("couldn't configure the buffer pool"); @@ -62,7 +64,7 @@ impl Open for GstreamerSink { .set_active(true) .expect("couldn't activate buffer pool"); - let (tx, rx) = sync_channel::>(64 * format.size()); + let (tx, rx) = sync_channel::>(64 * sample_size); thread::spawn(move || { for data in rx { let buffer = bufferpool.acquire_buffer(None); From a1326ba9f45c5d6f61950f95a87a5642421ad2a9 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 18 Mar 2021 22:06:43 +0100 Subject: [PATCH 18/46] First round of refactoring - DRY-ups - Remove incorrect optimization attempt in the libvorbis decoder, that skewed 0.0 samples non-linear - PortAudio and SDL backends do not support S24 output. The PortAudio bindings could, but not through this API. --- audio/src/libvorbis_decoder.rs | 8 +------ playback/src/audio_backend/alsa.rs | 2 -- playback/src/audio_backend/jackaudio.rs | 6 ++--- playback/src/audio_backend/portaudio.rs | 17 +++----------- playback/src/audio_backend/sdl.rs | 13 +++-------- playback/src/config.rs | 30 ++++++++++++------------- 6 files changed, 25 insertions(+), 51 deletions(-) diff --git a/audio/src/libvorbis_decoder.rs b/audio/src/libvorbis_decoder.rs index 449caaeb..e7ccc984 100644 --- a/audio/src/libvorbis_decoder.rs +++ b/audio/src/libvorbis_decoder.rs @@ -45,13 +45,7 @@ where packet .data .iter() - .map(|sample| { - if *sample == 0 { - 0.0 - } else { - ((*sample as f64 + 0.5) / (0x7FFF as f64 + 0.5)) as f32 - } - }) + .map(|sample| ((*sample as f64 + 0.5) / (0x7FFF as f64 + 0.5)) as f32) .collect(), ))); } diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 35d0ab11..fc2a775c 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -52,8 +52,6 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box // 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 { let hwp = HwParams::any(&pcm)?; hwp.set_access(Access::RWInterleaved)?; diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 05c6c317..ed6ae1f7 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -5,8 +5,8 @@ use crate::player::NUM_CHANNELS; use jack::{ AsyncClient, AudioOut, Client, ClientOptions, Control, Port, ProcessHandler, ProcessScope, }; +use std::io; use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; -use std::{io, mem}; pub struct JackSink { send: SyncSender, @@ -53,7 +53,7 @@ impl Open for JackSink { let ch_r = client.register_port("out_0", AudioOut::default()).unwrap(); let ch_l = client.register_port("out_1", AudioOut::default()).unwrap(); // buffer for samples from librespot (~10ms) - let (tx, rx) = sync_channel::(NUM_CHANNELS as usize * 1024 * mem::size_of::()); + let (tx, rx) = sync_channel::(NUM_CHANNELS as usize * 1024 * format.size()); let jack_data = JackData { rec: rx, port_l: ch_l, @@ -75,7 +75,7 @@ impl Sink for JackSink { for s in packet.samples().iter() { let res = self.send.send(*s); if res.is_err() { - error!("jackaudio: cannot write to channel"); + error!("cannot write to channel"); } } Ok(()) diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 213f2d02..fca305e0 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -18,10 +18,6 @@ pub enum PortAudioSink<'a> { Option>, StreamParameters, ), - S24( - Option>, - StreamParameters, - ), S16( Option>, StreamParameters, @@ -91,11 +87,10 @@ impl<'a> Open for PortAudioSink<'a> { match format { AudioFormat::F32 => open_sink!(Self::F32, f32), AudioFormat::S32 => open_sink!(Self::S32, i32), - AudioFormat::S24 => open_sink!(Self::S24, i32), - AudioFormat::S24_3 => { - unimplemented!("PortAudio currently does not support S24_3 output") - } AudioFormat::S16 => open_sink!(Self::S16, i16), + _ => { + unimplemented!("PortAudio currently does not support {:?} output", format) + } } } } @@ -123,7 +118,6 @@ impl<'a> Sink for PortAudioSink<'a> { match self { Self::F32(stream, parameters) => start_sink!(stream, parameters), Self::S32(stream, parameters) => start_sink!(stream, parameters), - Self::S24(stream, parameters) => start_sink!(stream, parameters), Self::S16(stream, parameters) => start_sink!(stream, parameters), }; @@ -140,7 +134,6 @@ impl<'a> Sink for PortAudioSink<'a> { match self { Self::F32(stream, _parameters) => stop_sink!(stream), Self::S32(stream, _parameters) => stop_sink!(stream), - Self::S24(stream, _parameters) => stop_sink!(stream), Self::S16(stream, _parameters) => stop_sink!(stream), }; @@ -162,10 +155,6 @@ impl<'a> Sink for PortAudioSink<'a> { let samples_s32: Vec = AudioPacket::f32_to_s32(packet.samples()); write_sink!(stream, &samples_s32) } - Self::S24(stream, _parameters) => { - let samples_s24: Vec = AudioPacket::f32_to_s24(packet.samples()); - write_sink!(stream, &samples_s24) - } Self::S16(stream, _parameters) => { let samples_s16: Vec = AudioPacket::f32_to_s16(packet.samples()); write_sink!(stream, &samples_s16) diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 32d710f8..64523732 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -8,7 +8,6 @@ use std::{io, mem, thread, time}; pub enum SdlSink { F32(AudioQueue), S32(AudioQueue), - S24(AudioQueue), S16(AudioQueue), } @@ -42,9 +41,10 @@ impl Open for SdlSink { match format { AudioFormat::F32 => open_sink!(Self::F32, f32), AudioFormat::S32 => open_sink!(Self::S32, i32), - AudioFormat::S24 => open_sink!(Self::S24, i32), - AudioFormat::S24_3 => unimplemented!("SDL currently does not support S24_3 output"), AudioFormat::S16 => open_sink!(Self::S16, i16), + _ => { + unimplemented!("SDL currently does not support {:?} output", format) + } } } } @@ -60,7 +60,6 @@ impl Sink for SdlSink { match self { Self::F32(queue) => start_sink!(queue), Self::S32(queue) => start_sink!(queue), - Self::S24(queue) => start_sink!(queue), Self::S16(queue) => start_sink!(queue), }; Ok(()) @@ -76,7 +75,6 @@ impl Sink for SdlSink { match self { Self::F32(queue) => stop_sink!(queue), Self::S32(queue) => stop_sink!(queue), - Self::S24(queue) => stop_sink!(queue), Self::S16(queue) => stop_sink!(queue), }; Ok(()) @@ -101,11 +99,6 @@ impl Sink for SdlSink { let samples_s32: Vec = AudioPacket::f32_to_s32(packet.samples()); queue.queue(&samples_s32) } - Self::S24(queue) => { - drain_sink!(queue, mem::size_of::()); - let samples_s24: Vec = AudioPacket::f32_to_s24(packet.samples()); - queue.queue(&samples_s24) - } Self::S16(queue) => { drain_sink!(queue, mem::size_of::()); let samples_s16: Vec = AudioPacket::f32_to_s16(packet.samples()); diff --git a/playback/src/config.rs b/playback/src/config.rs index 630c1406..95c97092 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -14,17 +14,17 @@ impl FromStr for Bitrate { type Err = (); fn from_str(s: &str) -> Result { match s { - "96" => Ok(Bitrate::Bitrate96), - "160" => Ok(Bitrate::Bitrate160), - "320" => Ok(Bitrate::Bitrate320), + "96" => Ok(Self::Bitrate96), + "160" => Ok(Self::Bitrate160), + "320" => Ok(Self::Bitrate320), _ => Err(()), } } } impl Default for Bitrate { - fn default() -> Bitrate { - Bitrate::Bitrate160 + fn default() -> Self { + Self::Bitrate160 } } @@ -52,7 +52,7 @@ impl TryFrom<&String> for AudioFormat { } impl Default for AudioFormat { - fn default() -> AudioFormat { + fn default() -> Self { Self::S16 } } @@ -64,7 +64,7 @@ impl AudioFormat { match self { Self::S24_3 => mem::size_of::(), Self::S16 => mem::size_of::(), - _ => mem::size_of::(), + _ => mem::size_of::(), // S32 and S24 are both stored in i32 } } } @@ -79,16 +79,16 @@ impl FromStr for NormalisationType { type Err = (); fn from_str(s: &str) -> Result { match s { - "album" => Ok(NormalisationType::Album), - "track" => Ok(NormalisationType::Track), + "album" => Ok(Self::Album), + "track" => Ok(Self::Track), _ => Err(()), } } } impl Default for NormalisationType { - fn default() -> NormalisationType { - NormalisationType::Album + fn default() -> Self { + Self::Album } } @@ -102,16 +102,16 @@ impl FromStr for NormalisationMethod { type Err = (); fn from_str(s: &str) -> Result { match s { - "basic" => Ok(NormalisationMethod::Basic), - "dynamic" => Ok(NormalisationMethod::Dynamic), + "basic" => Ok(Self::Basic), + "dynamic" => Ok(Self::Dynamic), _ => Err(()), } } } impl Default for NormalisationMethod { - fn default() -> NormalisationMethod { - NormalisationMethod::Dynamic + fn default() -> Self { + Self::Dynamic } } From 001d3ca1cf70fdbfef9703aa9cc47f43b050aa76 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 19 Mar 2021 22:28:55 +0100 Subject: [PATCH 19/46] Bump Alsa, cpal and GStreamer crates --- Cargo.lock | 584 ++++---------------------------------------- playback/Cargo.toml | 2 +- 2 files changed, 52 insertions(+), 534 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2296cfed..2d711765 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,26 +1,5 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -[[package]] -name = "addr2line" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" - -[[package]] -name = "adler32" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" - [[package]] name = "aes" version = "0.6.0" @@ -66,9 +45,9 @@ dependencies = [ [[package]] name = "alsa" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb213f6b3e4b1480a60931ca2035794aa67b73103d254715b1db7b70dcb3c934" +checksum = "75c4da790adcb2ce5e758c064b4f3ec17a30349f9961d3e5e6c9688b052a9e18" dependencies = [ "alsa-sys", "bitflags", @@ -92,12 +71,6 @@ version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1" -[[package]] -name = "ascii" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" - [[package]] name = "atty" version = "0.2.14" @@ -115,26 +88,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" -[[package]] -name = "backtrace" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef5140344c85b01f9bbb4d4b7288a8aa4b3287ccef913a14bcc78a1063623598" -dependencies = [ - "addr2line", - "cfg-if 1.0.0", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "base-x" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" - [[package]] name = "base64" version = "0.9.3" @@ -276,6 +229,9 @@ name = "cc" version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" +dependencies = [ + "jobserver", +] [[package]] name = "cesu8" @@ -313,16 +269,10 @@ dependencies = [ "libc", "num-integer", "num-traits", - "time 0.1.43", + "time", "winapi 0.3.9", ] -[[package]] -name = "chunked_transfer" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" - [[package]] name = "cipher" version = "0.2.5" @@ -352,19 +302,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "combine" -version = "3.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" -dependencies = [ - "ascii", - "byteorder", - "either", - "memchr", - "unreachable", -] - [[package]] name = "combine" version = "4.5.2" @@ -375,39 +312,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "const_fn" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b9d6de7f49e22cf97ad17fc4036ece69300032f45f78f30b4a4482cdc3f4a6" - -[[package]] -name = "cookie" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784ad0fbab4f3e9cef09f20e0aea6000ae08d2cb98ac4c0abc53df18803d702f" -dependencies = [ - "percent-encoding 2.1.0", - "time 0.2.25", - "version_check", -] - -[[package]] -name = "cookie_store" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3818dfca4b0cb5211a659bbcbb94225b7127407b2b135e650d717bfb78ab10d3" -dependencies = [ - "cookie", - "idna 0.2.1", - "log 0.4.14", - "publicsuffix", - "serde", - "serde_json", - "time 0.2.25", - "url 2.2.0", -] - [[package]] name = "core-foundation-sys" version = "0.6.2" @@ -416,9 +320,9 @@ checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" [[package]] name = "coreaudio-rs" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f229761965dad3e9b11081668a6ea00f1def7aa46062321b5ec245b834f6e491" +checksum = "11894b20ebfe1ff903cbdc52259693389eea03b94918a2def2c30c3bf227ad88" dependencies = [ "bitflags", "coreaudio-sys", @@ -435,15 +339,15 @@ dependencies = [ [[package]] name = "cpal" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05631e2089dfa5d3b6ea1cfbbfd092e2ee5deeb69698911bc976b28b746d3657" +checksum = "840981d3f30230d9120328d64be72319dbbedabb61bcd4c370a54cdd051238ac" dependencies = [ "alsa", "core-foundation-sys", "coreaudio-rs", "jack", - "jni 0.17.0", + "jni", "js-sys", "lazy_static", "libc", @@ -453,7 +357,7 @@ dependencies = [ "nix", "oboe", "parking_lot 0.11.1", - "stdweb 0.1.3", + "stdweb", "thiserror", "web-sys", "winapi 0.3.9", @@ -465,15 +369,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" -[[package]] -name = "crc32fast" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" -dependencies = [ - "cfg-if 1.0.0", -] - [[package]] name = "crossbeam-deque" version = "0.7.3" @@ -624,12 +519,6 @@ dependencies = [ "generic-array 0.14.4", ] -[[package]] -name = "discard" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" - [[package]] name = "dns-sd" version = "0.1.3" @@ -664,7 +553,6 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" dependencies = [ - "backtrace", "version_check", ] @@ -674,45 +562,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" -[[package]] -name = "fetch_unroll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d44807d562d137f063cbfe209da1c3f9f2fa8375e11166ef495daab7b847f9" -dependencies = [ - "libflate", - "tar", - "ureq", -] - -[[package]] -name = "filetime" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "redox_syscall 0.2.4", - "winapi 0.3.9", -] - [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "form_urlencoded" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00" -dependencies = [ - "matches", - "percent-encoding 2.1.0", -] - [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -876,12 +731,6 @@ dependencies = [ "wasi 0.10.2+wasi-snapshot-preview1", ] -[[package]] -name = "gimli" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" - [[package]] name = "glib" version = "0.10.3" @@ -1115,9 +964,9 @@ dependencies = [ "log 0.4.14", "mime", "net2", - "percent-encoding 1.0.1", + "percent-encoding", "relay", - "time 0.1.43", + "time", "tokio-core", "tokio-io", "tokio-proto", @@ -1156,17 +1005,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "idna" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de910d521f7cc3135c4de8db1cb910e0b5ed1dc6f57c381cd07e8e661ce10094" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "if-addrs" version = "0.6.5" @@ -1246,29 +1084,15 @@ dependencies = [ [[package]] name = "jni" -version = "0.14.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1981310da491a4f0f815238097d0d43d8072732b5ae5f8bd0d8eadf5bf245402" +checksum = "24967112a1e4301ca5342ea339763613a37592b8a6ce6cf2e4494537c7a42faf" dependencies = [ "cesu8", - "combine 3.8.1", - "error-chain", - "jni-sys", - "log 0.4.14", - "walkdir", -] - -[[package]] -name = "jni" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36bcc950632e48b86da402c5c077590583da5ac0d480103611d5374e7c967a3c" -dependencies = [ - "cesu8", - "combine 4.5.2", - "error-chain", + "combine", "jni-sys", "log 0.4.14", + "thiserror", "walkdir", ] @@ -1278,6 +1102,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.47" @@ -1328,27 +1161,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.85" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ccac4b00700875e6a07c6cde370d44d32fa01c5a65cdd2fca6858c479d28bb3" - -[[package]] -name = "libflate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389de7875e06476365974da3e7ff85d55f1972188ccd9f6020dd7c8156e17914" -dependencies = [ - "adler32", - "crc32fast", - "libflate_lz77", - "rle-decode-fast", -] - -[[package]] -name = "libflate_lz77" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3286f09f7d4926fc486334f28d8d2e6ebe4f7f9994494b6dab27ddfad2c9b11b" +checksum = "ba4aede83fc3617411dc6993bc8c70919750c1c257c6ca6a502aed6e0e2394ae" [[package]] name = "libloading" @@ -1452,7 +1267,7 @@ dependencies = [ "tokio-io", "tokio-process", "tokio-signal", - "url 1.7.2", + "url", ] [[package]] @@ -1500,7 +1315,7 @@ dependencies = [ "serde_json", "sha-1 0.9.3", "tokio-core", - "url 1.7.2", + "url", ] [[package]] @@ -1534,7 +1349,7 @@ dependencies = [ "tokio-codec", "tokio-core", "tokio-io", - "url 1.7.2", + "url", "uuid", "vergen", ] @@ -1690,16 +1505,6 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" -[[package]] -name = "miniz_oxide" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d" -dependencies = [ - "adler", - "autocfg", -] - [[package]] name = "mio" version = "0.6.23" @@ -1781,9 +1586,9 @@ dependencies = [ [[package]] name = "ndk" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb167c1febed0a496639034d0c76b3b74263636045db5489eee52143c246e73" +checksum = "8794322172319b972f528bf90c6b467be0079f1fa82780ffb431088e741a73ab" dependencies = [ "jni-sys", "ndk-sys", @@ -1793,9 +1598,9 @@ dependencies = [ [[package]] name = "ndk-glue" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdf399b8b7a39c6fb153c4ec32c72fd5fe789df24a647f229c239aa7adb15241" +checksum = "c5caf0c24d51ac1c905c27d4eda4fa0635bbe0de596b8f79235e0b17a4d29385" dependencies = [ "lazy_static", "libc", @@ -1837,15 +1642,14 @@ dependencies = [ [[package]] name = "nix" -version = "0.15.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2e0b4f3320ed72aaedb9a5ac838690a8047c7b275da22711fddff4f8a14229" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" dependencies = [ "bitflags", "cc", - "cfg-if 0.1.10", + "cfg-if 1.0.0", "libc", - "void", ] [[package]] @@ -1922,9 +1726,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.4.3" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca565a7df06f3d4b485494f25ba05da1435950f4dc263440eda7a6fa9b8e36e4" +checksum = "226b45a5c2ac4dd696ed30fa6b94b057ad909c7b7fc2e0d0808192bced894066" dependencies = [ "derivative", "num_enum_derive", @@ -1932,9 +1736,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.4.3" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffa5a33ddddfee04c0283a7653987d634e880347e96b5b2ed64de07efb59db9d" +checksum = "1c0fd9eba1d5db0994a239e09c1be402d35622277e35468ba891aa5e3188ce7e" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1942,19 +1746,13 @@ dependencies = [ "syn", ] -[[package]] -name = "object" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397" - [[package]] name = "oboe" -version = "0.3.1" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aadc2b0867bdbb9a81c4d99b9b682958f49dbea1295a81d2f646cca2afdd9fc" +checksum = "4cfb2390bddb9546c0f7448fd1d2abdd39e6075206f960991eb28c7fa7f126c4" dependencies = [ - "jni 0.14.0", + "jni", "ndk", "ndk-glue", "num-derive", @@ -1964,11 +1762,11 @@ dependencies = [ [[package]] name = "oboe-sys" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ff7a51600eabe34e189eec5c995a62f151d8d97e5fbca39e87ca738bb99b82" +checksum = "fe069264d082fc820dfa172f79be3f2e088ecfece9b1c47b0c9fd838d2bef103" dependencies = [ - "fetch_unroll", + "cc", ] [[package]] @@ -2088,12 +1886,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" -[[package]] -name = "percent-encoding" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" - [[package]] name = "pin-project-lite" version = "0.2.4" @@ -2224,28 +2016,6 @@ dependencies = [ "protobuf-codegen", ] -[[package]] -name = "publicsuffix" -version = "1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bbaa49075179162b49acac1c6aa45fb4dafb5f13cf6794276d77bc7fd95757b" -dependencies = [ - "error-chain", - "idna 0.2.1", - "lazy_static", - "regex", - "url 2.2.0", -] - -[[package]] -name = "qstring" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" -dependencies = [ - "percent-encoding 2.1.0", -] - [[package]] name = "quick-error" version = "1.2.3" @@ -2437,27 +2207,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin", - "untrusted", - "web-sys", - "winapi 0.3.9", -] - -[[package]] -name = "rle-decode-fast" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cabe4fa914dec5870285fa7f71f602645da47c486e68486d2b4ceb4a343e90ac" - [[package]] name = "rodio" version = "0.13.0" @@ -2477,12 +2226,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "rustc-demangle" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" - [[package]] name = "rustc-hash" version = "1.1.0" @@ -2498,19 +2241,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustls" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "064fd21ff87c6e87ed4506e68beb42459caa4a0e2eb144932e6776768556980b" -dependencies = [ - "base64 0.13.0", - "log 0.4.14", - "ring", - "sct", - "webpki", -] - [[package]] name = "ryu" version = "1.0.5" @@ -2544,16 +2274,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" -[[package]] -name = "sct" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "sdl2" version = "0.34.3" @@ -2597,9 +2317,6 @@ name = "serde" version = "1.0.123" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" -dependencies = [ - "serde_derive", -] [[package]] name = "serde_derive" @@ -2648,12 +2365,6 @@ dependencies = [ "opaque-debug 0.3.0", ] -[[package]] -name = "sha1" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" - [[package]] name = "shannon" version = "0.2.0" @@ -2728,76 +2439,12 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "standback" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2beb4d1860a61f571530b3f855a1b538d0200f7871c63331ecd6f17b1f014f8" -dependencies = [ - "version_check", -] - [[package]] name = "stdweb" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" -[[package]] -name = "stdweb" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" -dependencies = [ - "discard", - "rustc_version", - "stdweb-derive", - "stdweb-internal-macros", - "stdweb-internal-runtime", - "wasm-bindgen", -] - -[[package]] -name = "stdweb-derive" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "serde_derive", - "syn", -] - -[[package]] -name = "stdweb-internal-macros" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" -dependencies = [ - "base-x", - "proc-macro2", - "quote", - "serde", - "serde_derive", - "serde_json", - "sha1", - "syn", -] - -[[package]] -name = "stdweb-internal-runtime" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" - [[package]] name = "strsim" version = "0.9.3" @@ -2872,17 +2519,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b157868d8ac1f56b64604539990685fa7611d8fa9e5476cf0c02cf34d32917c5" -[[package]] -name = "tar" -version = "0.4.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0313546c01d59e29be4f09687bcb4fb6690cec931cc3607b6aec7a0e417f4cc6" -dependencies = [ - "filetime", - "libc", - "xattr", -] - [[package]] name = "tempfile" version = "3.2.0" @@ -2936,44 +2572,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "time" -version = "0.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1195b046942c221454c2539395f85413b33383a067449d78aab2b7b052a142f7" -dependencies = [ - "const_fn", - "libc", - "standback", - "stdweb 0.4.20", - "time-macros", - "version_check", - "winapi 0.3.9", -] - -[[package]] -name = "time-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" -dependencies = [ - "proc-macro-hack", - "time-macros-impl", -] - -[[package]] -name = "time-macros-impl" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" -dependencies = [ - "proc-macro-hack", - "proc-macro2", - "quote", - "standback", - "syn", -] - [[package]] name = "tinyvec" version = "1.1.1" @@ -3319,61 +2917,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" -[[package]] -name = "unreachable" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" -dependencies = [ - "void", -] - -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - -[[package]] -name = "ureq" -version = "1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "294b85ef5dbc3670a72e82a89971608a1fcc4ed5c7c5a2895230d31a95f0569b" -dependencies = [ - "base64 0.13.0", - "chunked_transfer", - "cookie", - "cookie_store", - "log 0.4.14", - "once_cell", - "qstring", - "rustls", - "url 2.2.0", - "webpki", - "webpki-roots", -] - [[package]] name = "url" version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" dependencies = [ - "idna 0.1.5", + "idna", "matches", - "percent-encoding 1.0.1", -] - -[[package]] -name = "url" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e" -dependencies = [ - "form_urlencoded", - "idna 0.2.1", - "matches", - "percent-encoding 2.1.0", + "percent-encoding", ] [[package]] @@ -3407,12 +2959,6 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" -[[package]] -name = "void" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" - [[package]] name = "vorbis" version = "0.0.14" @@ -3548,25 +3094,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "webpki-roots" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82015b7e0b8bad8185994674a13a93306bea76cf5a16c5a181382fd3a5ec2376" -dependencies = [ - "webpki", -] - [[package]] name = "winapi" version = "0.2.8" @@ -3620,15 +3147,6 @@ dependencies = [ "winapi-build", ] -[[package]] -name = "xattr" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" -dependencies = [ - "libc", -] - [[package]] name = "zerocopy" version = "0.3.0" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 67e06be7..952ecdea 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -23,7 +23,7 @@ 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 } From 86dbaa8ed53c5b76414d1a577f680d2a65dd2c89 Mon Sep 17 00:00:00 2001 From: philippe44 Date: Sat, 20 Mar 2021 12:11:49 -0700 Subject: [PATCH 20/46] true/false don't need to be explicit Co-authored-by: Johannesd3 <51954457+Johannesd3@users.noreply.github.com> --- audio/src/passthrough_decoder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audio/src/passthrough_decoder.rs b/audio/src/passthrough_decoder.rs index 6fab78ec..082f6915 100644 --- a/audio/src/passthrough_decoder.rs +++ b/audio/src/passthrough_decoder.rs @@ -73,7 +73,7 @@ impl AudioDecoder for PassthroughDecoder { info!("Seeking to {}", ms); // add an eos to previous stream if missing - if self.bos == true && self.eos == false { + if self.bos && !self.eos { match self.rdr.read_packet() { Ok(Some(pck)) => { let absgp_page = pck.absgp_page() - self.ofsgp_page; From 74b2fea33814b8ea190343d8ab6a7cd1f5c6f9c2 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 21 Mar 2021 22:16:47 +0100 Subject: [PATCH 21/46] Refactor sample conversion into separate struct --- audio/src/lib.rs | 67 +++++++++++++------------ playback/src/audio_backend/mod.rs | 9 ++-- playback/src/audio_backend/portaudio.rs | 15 +++--- playback/src/audio_backend/rodio.rs | 9 ++-- playback/src/audio_backend/sdl.rs | 14 +++--- 5 files changed, 61 insertions(+), 53 deletions(-) diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 86c5b4ae..fe3b5c96 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -38,33 +38,6 @@ pub enum AudioPacket { OggData(Vec), } -#[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) -> &[f32] { match self { @@ -86,23 +59,53 @@ impl AudioPacket { AudioPacket::OggData(d) => d.is_empty(), } } +} - pub fn f32_to_s32(samples: &[f32]) -> Vec { +#[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| { + (*sample as f64 * (std::$type::MAX as f64 + 0.5) - 0.5) as $type >> $drop_bits + }) + .collect() + }; +} + +pub struct SamplesConverter {} +impl SamplesConverter { + pub fn to_s32(samples: &[f32]) -> Vec { convert_samples_to!(i32, samples) } - pub fn f32_to_s24(samples: &[f32]) -> Vec { + pub fn to_s24(samples: &[f32]) -> Vec { convert_samples_to!(i32, samples, 8) } - pub fn f32_to_s24_3(samples: &[f32]) -> Vec { - Self::f32_to_s32(samples) + pub fn to_s24_3(samples: &[f32]) -> Vec { + Self::to_s32(samples) .iter() .map(|sample| i24::pcm_from_i32(*sample)) .collect() } - pub fn f32_to_s16(samples: &[f32]) -> Vec { + pub fn to_s16(samples: &[f32]) -> Vec { convert_samples_to!(i16, samples) } } diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 9c46dbe4..94b6a529 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -24,24 +24,25 @@ fn mk_sink(device: Option, format: AudioFormat macro_rules! sink_as_bytes { () => { fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + use crate::audio::{i24, SamplesConverter}; use zerocopy::AsBytes; match packet { AudioPacket::Samples(samples) => match self.format { AudioFormat::F32 => self.write_bytes(samples.as_bytes()), AudioFormat::S32 => { - let samples_s32 = AudioPacket::f32_to_s32(samples); + let samples_s32: &[i32] = &SamplesConverter::to_s32(samples); self.write_bytes(samples_s32.as_bytes()) } AudioFormat::S24 => { - let samples_s24 = AudioPacket::f32_to_s24(samples); + let samples_s24: &[i32] = &SamplesConverter::to_s24(samples); self.write_bytes(samples_s24.as_bytes()) } AudioFormat::S24_3 => { - let samples_s24_3 = AudioPacket::f32_to_s24_3(samples); + let samples_s24_3: &[i24] = &SamplesConverter::to_s24_3(samples); self.write_bytes(samples_s24_3.as_bytes()) } AudioFormat::S16 => { - let samples_s16 = AudioPacket::f32_to_s16(samples); + let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); self.write_bytes(samples_s16.as_bytes()) } }, diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index fca305e0..f29bac2d 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -1,5 +1,5 @@ use super::{Open, Sink}; -use crate::audio::AudioPacket; +use crate::audio::{AudioPacket, SamplesConverter}; use crate::config::AudioFormat; use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use portaudio_rs; @@ -146,18 +146,19 @@ impl<'a> Sink for PortAudioSink<'a> { $stream.as_mut().unwrap().write($samples) }; } + + let samples = packet.samples(); let result = match self { Self::F32(stream, _parameters) => { - let samples = packet.samples(); - write_sink!(stream, &samples) + write_sink!(stream, samples) } Self::S32(stream, _parameters) => { - let samples_s32: Vec = AudioPacket::f32_to_s32(packet.samples()); - write_sink!(stream, &samples_s32) + let samples_s32: &[i32] = &SamplesConverter::to_s32(samples); + write_sink!(stream, samples_s32) } Self::S16(stream, _parameters) => { - let samples_s16: Vec = AudioPacket::f32_to_s16(packet.samples()); - write_sink!(stream, &samples_s16) + let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); + write_sink!(stream, samples_s16) } }; match result { diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 5262a9cc..2fc4fbde 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -1,8 +1,9 @@ use super::{Open, Sink}; extern crate cpal; extern crate rodio; -use crate::audio::AudioPacket; +use crate::audio::{AudioPacket, SamplesConverter}; use crate::config::AudioFormat; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use cpal::traits::{DeviceTrait, HostTrait}; use std::process::exit; use std::{io, thread, time}; @@ -25,12 +26,12 @@ macro_rules! rodio_sink { let samples = packet.samples(); match self.format { AudioFormat::F32 => { - let source = rodio::buffer::SamplesBuffer::new(2, 44100, samples); + let source = rodio::buffer::SamplesBuffer::new(NUM_CHANNELS as u16, SAMPLE_RATE, samples); self.rodio_sink.append(source) }, AudioFormat::S16 => { - let samples_s16: Vec = AudioPacket::f32_to_s16(samples); - let source = rodio::buffer::SamplesBuffer::new(2, 44100, samples_s16); + let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); + let source = rodio::buffer::SamplesBuffer::new(NUM_CHANNELS as u16, SAMPLE_RATE, samples_s16); self.rodio_sink.append(source) }, _ => unimplemented!(), diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 64523732..b1b4c2e1 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -1,5 +1,5 @@ use super::{Open, Sink}; -use crate::audio::AudioPacket; +use crate::audio::{AudioPacket, SamplesConverter}; use crate::config::AudioFormat; use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use sdl2::audio::{AudioQueue, AudioSpecDesired}; @@ -89,20 +89,22 @@ impl Sink for SdlSink { } }}; } + + let samples = packet.samples(); match self { Self::F32(queue) => { drain_sink!(queue, mem::size_of::()); - queue.queue(packet.samples()) + queue.queue(samples) } Self::S32(queue) => { + let samples_s32: &[i32] = &SamplesConverter::to_s32(samples); drain_sink!(queue, mem::size_of::()); - let samples_s32: Vec = AudioPacket::f32_to_s32(packet.samples()); - queue.queue(&samples_s32) + queue.queue(samples_s32) } Self::S16(queue) => { + let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); drain_sink!(queue, mem::size_of::()); - let samples_s16: Vec = AudioPacket::f32_to_s16(packet.samples()); - queue.queue(&samples_s16) + queue.queue(samples_s16) } }; Ok(()) From bfca1ec15eae36637b55dac9f7080d8289100546 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 27 Mar 2021 21:13:14 +0100 Subject: [PATCH 22/46] Minor code improvements and crates bump --- Cargo.lock | 16 ++++++++-------- audio/Cargo.toml | 4 ++-- playback/Cargo.toml | 4 ++-- playback/src/audio_backend/portaudio.rs | 25 +++++++++++++------------ playback/src/audio_backend/rodio.rs | 12 ++++++------ 5 files changed, 31 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2d711765..f40f4dca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -795,9 +795,9 @@ dependencies = [ [[package]] name = "gstreamer" -version = "0.16.5" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d50f822055923f1cbede233aa5dfd4ee957cf328fb3076e330886094e11d6cf" +checksum = "9ff5d0f7ff308ae37e6eb47b6ded17785bdea06e438a708cd09e0288c1862f33" dependencies = [ "bitflags", "cfg-if 1.0.0", @@ -1061,9 +1061,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" [[package]] name = "jack" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c1871c91fa65aa328f3bedbaa54a6e5d1de009264684c153eb708ba933aa6f5" +checksum = "2deb4974bd7e6b2fb7784f27fa13d819d11292b3b004dce0185ec08163cf686a" dependencies = [ "bitflags", "jack-sys", @@ -1073,9 +1073,9 @@ dependencies = [ [[package]] name = "jack-sys" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d6ab7ada402b6a27912a2b86504be62a48c58313c886fe72a059127acb4d7" +checksum = "57983f0d72dfecf2b719ed39bc9cacd85194e1a94cb3f9146009eff9856fef41" dependencies = [ "lazy_static", "libc", @@ -1161,9 +1161,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4aede83fc3617411dc6993bc8c70919750c1c257c6ca6a502aed6e0e2394ae" +checksum = "8916b1f6ca17130ec6568feccee27c156ad12037880833a3b842a823236502e7" [[package]] name = "libloading" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 06f1dda6..2d67f4ce 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -24,8 +24,8 @@ 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"] diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 952ecdea..07e31799 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -25,8 +25,8 @@ shell-words = "1.0.0" 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 } diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index f29bac2d..35f7852f 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -98,7 +98,7 @@ impl<'a> Open for PortAudioSink<'a> { impl<'a> Sink for PortAudioSink<'a> { fn start(&mut self) -> io::Result<()> { macro_rules! start_sink { - ($stream: ident, $parameters: ident) => {{ + (ref mut $stream: ident, ref $parameters: ident) => {{ if $stream.is_none() { *$stream = Some( Stream::open( @@ -115,10 +115,11 @@ impl<'a> Sink for PortAudioSink<'a> { $stream.as_mut().unwrap().start().unwrap() }}; } + match self { - Self::F32(stream, parameters) => start_sink!(stream, parameters), - Self::S32(stream, parameters) => start_sink!(stream, parameters), - Self::S16(stream, parameters) => start_sink!(stream, parameters), + Self::F32(stream, parameters) => start_sink!(ref mut stream, ref parameters), + Self::S32(stream, parameters) => start_sink!(ref mut stream, ref parameters), + Self::S16(stream, parameters) => start_sink!(ref mut stream, ref parameters), }; Ok(()) @@ -126,15 +127,15 @@ impl<'a> Sink for PortAudioSink<'a> { fn stop(&mut self) -> io::Result<()> { macro_rules! stop_sink { - ($stream: expr) => {{ + (ref mut $stream: ident) => {{ $stream.as_mut().unwrap().stop().unwrap(); *$stream = None; }}; } match self { - Self::F32(stream, _parameters) => stop_sink!(stream), - Self::S32(stream, _parameters) => stop_sink!(stream), - Self::S16(stream, _parameters) => stop_sink!(stream), + 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), }; Ok(()) @@ -142,7 +143,7 @@ impl<'a> Sink for PortAudioSink<'a> { fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { macro_rules! write_sink { - ($stream: expr, $samples: expr) => { + (ref mut $stream: expr, $samples: expr) => { $stream.as_mut().unwrap().write($samples) }; } @@ -150,15 +151,15 @@ impl<'a> Sink for PortAudioSink<'a> { let samples = packet.samples(); let result = match self { Self::F32(stream, _parameters) => { - write_sink!(stream, samples) + write_sink!(ref mut stream, samples) } Self::S32(stream, _parameters) => { let samples_s32: &[i32] = &SamplesConverter::to_s32(samples); - write_sink!(stream, samples_s32) + write_sink!(ref mut stream, samples_s32) } Self::S16(stream, _parameters) => { let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); - write_sink!(stream, samples_s16) + write_sink!(ref mut stream, samples_s16) } }; match result { diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 2fc4fbde..6e914ea0 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -27,14 +27,14 @@ macro_rules! rodio_sink { match self.format { AudioFormat::F32 => { let source = rodio::buffer::SamplesBuffer::new(NUM_CHANNELS as u16, SAMPLE_RATE, samples); - self.rodio_sink.append(source) + self.rodio_sink.append(source); }, AudioFormat::S16 => { let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); let source = rodio::buffer::SamplesBuffer::new(NUM_CHANNELS as u16, SAMPLE_RATE, samples_s16); - self.rodio_sink.append(source) + self.rodio_sink.append(source); }, - _ => unimplemented!(), + _ => unreachable!(), }; // Chunk sizes seem to be about 256 to 3000 ish items long. @@ -64,15 +64,15 @@ macro_rules! rodio_sink { let rodio_device = match_device(&host, device); debug!("Using cpal device"); - let stream = rodio::OutputStream::try_from_device(&rodio_device) + let (stream, stream_handle) = rodio::OutputStream::try_from_device(&rodio_device) .expect("couldn't open output stream."); debug!("Using Rodio stream"); - let sink = rodio::Sink::try_new(&stream.1).expect("couldn't create output sink."); + let sink = rodio::Sink::try_new(&stream_handle).expect("couldn't create output sink."); debug!("Using Rodio sink"); Self { rodio_sink: sink, - stream: stream.0, + stream: stream, format: format, } } From cdbce21e71398e05f042ed2c6bf759402b3f6ac8 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 27 Mar 2021 21:42:10 +0100 Subject: [PATCH 23/46] Make S16 to F32 sample conversion less magical --- audio/src/libvorbis_decoder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audio/src/libvorbis_decoder.rs b/audio/src/libvorbis_decoder.rs index e7ccc984..e86fc000 100644 --- a/audio/src/libvorbis_decoder.rs +++ b/audio/src/libvorbis_decoder.rs @@ -45,7 +45,7 @@ where packet .data .iter() - .map(|sample| ((*sample as f64 + 0.5) / (0x7FFF as f64 + 0.5)) as f32) + .map(|sample| ((*sample as f64 + 0.5) / (std::i16::MAX as f64 + 0.5)) as f32) .collect(), ))); } From a200b259164f534cdcb151fbd57d0eefbb9ad48e Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 27 Mar 2021 21:44:01 +0100 Subject: [PATCH 24/46] Fix formatting --- audio/src/libvorbis_decoder.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/audio/src/libvorbis_decoder.rs b/audio/src/libvorbis_decoder.rs index e86fc000..5ad48f0d 100644 --- a/audio/src/libvorbis_decoder.rs +++ b/audio/src/libvorbis_decoder.rs @@ -45,7 +45,9 @@ where packet .data .iter() - .map(|sample| ((*sample as f64 + 0.5) / (std::i16::MAX as f64 + 0.5)) as f32) + .map(|sample| { + ((*sample as f64 + 0.5) / (std::i16::MAX as f64 + 0.5)) as f32 + }) .collect(), ))); } From cc60dc11dc15ed8316aac83cf3108a6e79c50d23 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 27 Mar 2021 22:52:43 +0100 Subject: [PATCH 25/46] Fix buffer size in JACK Audio backend --- playback/src/audio_backend/jackaudio.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index ed6ae1f7..7449cc11 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -53,7 +53,7 @@ impl Open for JackSink { let ch_r = client.register_port("out_0", AudioOut::default()).unwrap(); let ch_l = client.register_port("out_1", AudioOut::default()).unwrap(); // buffer for samples from librespot (~10ms) - let (tx, rx) = sync_channel::(NUM_CHANNELS as usize * 1024 * format.size()); + let (tx, rx) = sync_channel::(NUM_CHANNELS as usize * 1024 * AudioFormat::F32.size()); let jack_data = JackData { rec: rx, port_l: ch_l, From d252eeedc542c7933cbf3a8c3db11b23f1a8f1dd Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 27 Mar 2021 22:53:05 +0100 Subject: [PATCH 26/46] Warn about broken backends --- playback/src/audio_backend/portaudio.rs | 3 +++ playback/src/audio_backend/rodio.rs | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 35f7852f..5faff6ca 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -55,6 +55,9 @@ impl<'a> Open for PortAudioSink<'a> { fn open(device: Option, format: AudioFormat) -> PortAudioSink<'a> { info!("Using PortAudio sink with format: {:?}", format); + warn!("This backend is known to panic on several platforms."); + warn!("Consider using some other backend, or better yet, contributing a fix."); + portaudio_rs::initialize().unwrap(); let device_idx = match device.as_ref().map(AsRef::as_ref) { diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 6e914ea0..908099f1 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -54,8 +54,7 @@ macro_rules! rodio_sink { AudioFormat::F32 => { #[cfg(target_os = "linux")] { - warn!("Rodio output to Alsa is known to cause garbled sound on output formats other than 16-bit signed integer."); - warn!("Consider using `--backend alsa` OR `--format {:?}`", AudioFormat::S16); + warn!("Rodio output to Alsa is known to cause garbled sound, consider using `--backend alsa`"); } }, AudioFormat::S16 => {}, From 07d710e14f9a1aca94db7d35d33f4f65327a741a Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 31 Mar 2021 20:41:09 +0200 Subject: [PATCH 27/46] Use AudioFormat size for SDL --- playback/src/audio_backend/sdl.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index b1b4c2e1..7e071dd1 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -3,7 +3,7 @@ use crate::audio::{AudioPacket, SamplesConverter}; use crate::config::AudioFormat; use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use sdl2::audio::{AudioQueue, AudioSpecDesired}; -use std::{io, mem, thread, time}; +use std::{io, thread, time}; pub enum SdlSink { F32(AudioQueue), @@ -93,17 +93,17 @@ impl Sink for SdlSink { let samples = packet.samples(); match self { Self::F32(queue) => { - drain_sink!(queue, mem::size_of::()); + drain_sink!(queue, AudioFormat::F32.size()); queue.queue(samples) } Self::S32(queue) => { let samples_s32: &[i32] = &SamplesConverter::to_s32(samples); - drain_sink!(queue, mem::size_of::()); + drain_sink!(queue, AudioFormat::S32.size()); queue.queue(samples_s32) } Self::S16(queue) => { let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); - drain_sink!(queue, mem::size_of::()); + drain_sink!(queue, AudioFormat::S16.size()); queue.queue(samples_s16) } }; From 78bc621ebba467208358519f58e8bab7d9eafec8 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 5 Apr 2021 21:30:40 +0200 Subject: [PATCH 28/46] Move SamplesConverter into convert.rs --- audio/src/convert.rs | 50 ++++++++++++++++++++++++++++++++++++++++++ audio/src/lib.rs | 52 ++------------------------------------------ 2 files changed, 52 insertions(+), 50 deletions(-) create mode 100644 audio/src/convert.rs diff --git a/audio/src/convert.rs b/audio/src/convert.rs new file mode 100644 index 00000000..74a4d8f4 --- /dev/null +++ b/audio/src/convert.rs @@ -0,0 +1,50 @@ +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| { + (*sample as f64 * (std::$type::MAX as f64 + 0.5) - 0.5) as $type >> $drop_bits + }) + .collect() + }; +} + +pub struct SamplesConverter {} +impl SamplesConverter { + pub fn to_s32(samples: &[f32]) -> Vec { + convert_samples_to!(i32, samples) + } + + pub fn to_s24(samples: &[f32]) -> Vec { + convert_samples_to!(i32, samples, 8) + } + + pub fn to_s24_3(samples: &[f32]) -> Vec { + Self::to_s32(samples) + .iter() + .map(|sample| i24::pcm_from_i32(*sample)) + .collect() + } + + pub fn to_s16(samples: &[f32]) -> Vec { + convert_samples_to!(i16, samples) + } +} diff --git a/audio/src/lib.rs b/audio/src/lib.rs index fe3b5c96..44f732b3 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -13,6 +13,7 @@ extern crate tempfile; extern crate librespot_core; +mod convert; mod decrypt; mod fetch; @@ -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::{ @@ -31,7 +33,6 @@ pub use fetch::{ READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS, }; use std::fmt; -use zerocopy::AsBytes; pub enum AudioPacket { Samples(Vec), @@ -61,55 +62,6 @@ impl AudioPacket { } } -#[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| { - (*sample as f64 * (std::$type::MAX as f64 + 0.5) - 0.5) as $type >> $drop_bits - }) - .collect() - }; -} - -pub struct SamplesConverter {} -impl SamplesConverter { - pub fn to_s32(samples: &[f32]) -> Vec { - convert_samples_to!(i32, samples) - } - - pub fn to_s24(samples: &[f32]) -> Vec { - convert_samples_to!(i32, samples, 8) - } - - pub fn to_s24_3(samples: &[f32]) -> Vec { - Self::to_s32(samples) - .iter() - .map(|sample| i24::pcm_from_i32(*sample)) - .collect() - } - - pub fn to_s16(samples: &[f32]) -> Vec { - convert_samples_to!(i16, samples) - } -} - #[cfg(not(any(feature = "with-tremor", feature = "with-vorbis")))] pub use crate::lewton_decoder::{VorbisDecoder, VorbisError}; #[cfg(any(feature = "with-tremor", feature = "with-vorbis"))] From 928a6736538da12e509ec55a626b29bb9e9d54da Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 5 Apr 2021 23:14:02 +0200 Subject: [PATCH 29/46] DRY up constructors --- playback/src/audio_backend/alsa.rs | 2 +- playback/src/audio_backend/gstreamer.rs | 2 +- playback/src/audio_backend/jackaudio.rs | 2 +- playback/src/audio_backend/pipe.rs | 2 +- playback/src/audio_backend/pulseaudio.rs | 2 +- playback/src/audio_backend/sdl.rs | 2 +- playback/src/audio_backend/subprocess.rs | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index fc2a775c..1d551878 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -71,7 +71,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box } impl Open for AlsaSink { - fn open(device: Option, format: AudioFormat) -> AlsaSink { + fn open(device: Option, format: AudioFormat) -> Self { info!("Using Alsa sink with format: {:?}", format); let name = match device.as_ref().map(AsRef::as_ref) { diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index d3c736a4..d59677ac 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -16,7 +16,7 @@ pub struct GstreamerSink { } impl Open for GstreamerSink { - fn open(device: Option, format: AudioFormat) -> GstreamerSink { + fn open(device: Option, format: AudioFormat) -> Self { info!("Using GStreamer sink with format: {:?}", format); gst::init().expect("failed to init GStreamer!"); diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 7449cc11..24a94a3e 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -41,7 +41,7 @@ impl ProcessHandler for JackData { } impl Open for JackSink { - fn open(client_name: Option, format: AudioFormat) -> JackSink { + fn open(client_name: Option, format: AudioFormat) -> Self { if format != AudioFormat::F32 { warn!("JACK currently does not support {:?} output", format); } diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index ae77e320..6948db94 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -10,7 +10,7 @@ pub struct StdoutSink { } impl Open for StdoutSink { - fn open(path: Option, format: AudioFormat) -> StdoutSink { + fn open(path: Option, format: AudioFormat) -> Self { info!("Using pipe sink with format: {:?}", format); let output: Box = match path { diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 16800eb0..507e453c 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -17,7 +17,7 @@ pub struct PulseAudioSink { } impl Open for PulseAudioSink { - fn open(device: Option, format: AudioFormat) -> PulseAudioSink { + fn open(device: Option, format: AudioFormat) -> Self { info!("Using PulseAudio sink with format: {:?}", format); // PulseAudio calls S24 and S24_3 different from the rest of the world diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 7e071dd1..0a3fd433 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -12,7 +12,7 @@ pub enum SdlSink { } impl Open for SdlSink { - fn open(device: Option, format: AudioFormat) -> SdlSink { + fn open(device: Option, format: AudioFormat) -> Self { info!("Using SDL sink with format: {:?}", format); if device.is_some() { diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index 586bb75b..bebb6ea0 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -12,7 +12,7 @@ pub struct SubprocessSink { } impl Open for SubprocessSink { - fn open(shell_command: Option, format: AudioFormat) -> SubprocessSink { + fn open(shell_command: Option, format: AudioFormat) -> Self { info!("Using subprocess sink with format: {:?}", format); if let Some(shell_command) = shell_command { From 7c3d89112dc0cdb4a8105592268bbb6fe9f035f8 Mon Sep 17 00:00:00 2001 From: johannesd3 Date: Wed, 31 Mar 2021 20:05:32 +0200 Subject: [PATCH 30/46] Fix clippy warnings --- audio/src/passthrough_decoder.rs | 2 +- core/src/apresolve.rs | 4 ++-- core/src/config.rs | 18 +++++++++--------- core/src/connection/codec.rs | 12 ++++++------ core/src/connection/handshake.rs | 10 +++++----- core/src/connection/mod.rs | 4 ++-- core/src/diffie_hellman.rs | 8 ++++---- core/src/mercury/mod.rs | 6 +++--- core/src/mercury/types.rs | 22 +++++++++++----------- core/src/spotify_id.rs | 6 +++--- src/main.rs | 4 +++- 11 files changed, 49 insertions(+), 47 deletions(-) diff --git a/audio/src/passthrough_decoder.rs b/audio/src/passthrough_decoder.rs index d519baf8..e064cba3 100644 --- a/audio/src/passthrough_decoder.rs +++ b/audio/src/passthrough_decoder.rs @@ -110,7 +110,7 @@ impl AudioDecoder for PassthroughDecoder { fn next_packet(&mut self) -> Result, AudioError> { // write headers if we are (re)starting - if self.bos == false { + if !self.bos { self.wtr .write_packet( self.ident.clone(), diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index c954dab5..85baba69 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -11,7 +11,7 @@ use super::AP_FALLBACK; const APRESOLVE_ENDPOINT: &str = "http://apresolve.spotify.com:80"; #[derive(Clone, Debug, Deserialize)] -struct APResolveData { +struct ApResolveData { ap_list: Vec, } @@ -41,7 +41,7 @@ async fn try_apresolve( }; let body = hyper::body::to_bytes(response.into_body()).await?; - let data: APResolveData = serde_json::from_slice(body.as_ref())?; + let data: ApResolveData = serde_json::from_slice(body.as_ref())?; let ap = if ap_port.is_some() || proxy.is_some() { data.ap_list.into_iter().find_map(|ap| { diff --git a/core/src/config.rs b/core/src/config.rs index a9e5ea2c..9c70c25b 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -29,9 +29,9 @@ pub enum DeviceType { Tablet = 2, Smartphone = 3, Speaker = 4, - TV = 5, - AVR = 6, - STB = 7, + Tv = 5, + Avr = 6, + Stb = 7, AudioDongle = 8, GameConsole = 9, CastAudio = 10, @@ -54,9 +54,9 @@ impl FromStr for DeviceType { "tablet" => Ok(Tablet), "smartphone" => Ok(Smartphone), "speaker" => Ok(Speaker), - "tv" => Ok(TV), - "avr" => Ok(AVR), - "stb" => Ok(STB), + "tv" => Ok(Tv), + "avr" => Ok(Avr), + "stb" => Ok(Stb), "audiodongle" => Ok(AudioDongle), "gameconsole" => Ok(GameConsole), "castaudio" => Ok(CastAudio), @@ -80,9 +80,9 @@ impl fmt::Display for DeviceType { Tablet => f.write_str("Tablet"), Smartphone => f.write_str("Smartphone"), Speaker => f.write_str("Speaker"), - TV => f.write_str("TV"), - AVR => f.write_str("AVR"), - STB => f.write_str("STB"), + Tv => f.write_str("TV"), + Avr => f.write_str("AVR"), + Stb => f.write_str("STB"), AudioDongle => f.write_str("AudioDongle"), GameConsole => f.write_str("GameConsole"), CastAudio => f.write_str("CastAudio"), diff --git a/core/src/connection/codec.rs b/core/src/connection/codec.rs index ead07b6e..299220f6 100644 --- a/core/src/connection/codec.rs +++ b/core/src/connection/codec.rs @@ -13,7 +13,7 @@ enum DecodeState { Payload(u8, usize), } -pub struct APCodec { +pub struct ApCodec { encode_nonce: u32, encode_cipher: Shannon, @@ -22,9 +22,9 @@ pub struct APCodec { decode_state: DecodeState, } -impl APCodec { - pub fn new(send_key: &[u8], recv_key: &[u8]) -> APCodec { - APCodec { +impl ApCodec { + pub fn new(send_key: &[u8], recv_key: &[u8]) -> ApCodec { + ApCodec { encode_nonce: 0, encode_cipher: Shannon::new(send_key), @@ -35,7 +35,7 @@ impl APCodec { } } -impl Encoder<(u8, Vec)> for APCodec { +impl Encoder<(u8, Vec)> for ApCodec { type Error = io::Error; fn encode(&mut self, item: (u8, Vec), buf: &mut BytesMut) -> io::Result<()> { @@ -60,7 +60,7 @@ impl Encoder<(u8, Vec)> for APCodec { } } -impl Decoder for APCodec { +impl Decoder for ApCodec { type Item = (u8, Bytes); type Error = io::Error; diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index 67a786ee..879df67c 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -7,16 +7,16 @@ use std::io; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio_util::codec::{Decoder, Framed}; -use super::codec::APCodec; -use crate::diffie_hellman::DHLocalKeys; +use super::codec::ApCodec; +use crate::diffie_hellman::DhLocalKeys; use crate::protocol; use crate::protocol::keyexchange::{APResponseMessage, ClientHello, ClientResponsePlaintext}; use crate::util; pub async fn handshake( mut connection: T, -) -> io::Result> { - let local_keys = DHLocalKeys::random(&mut thread_rng()); +) -> io::Result> { + let local_keys = DhLocalKeys::random(&mut thread_rng()); let gc = local_keys.public_key(); let mut accumulator = client_hello(&mut connection, gc).await?; let message: APResponseMessage = recv_packet(&mut connection, &mut accumulator).await?; @@ -29,7 +29,7 @@ pub async fn handshake( let shared_secret = local_keys.shared_secret(&remote_key); let (challenge, send_key, recv_key) = compute_keys(&shared_secret, &accumulator); - let codec = APCodec::new(&send_key, &recv_key); + let codec = ApCodec::new(&send_key, &recv_key); client_response(&mut connection, challenge).await?; diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index 56d579bd..d8a40129 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -1,7 +1,7 @@ mod codec; mod handshake; -pub use self::codec::APCodec; +pub use self::codec::ApCodec; pub use self::handshake::handshake; use std::io::{self, ErrorKind}; @@ -19,7 +19,7 @@ use crate::protocol::keyexchange::{APLoginFailed, ErrorCode}; use crate::proxytunnel; use crate::version; -pub type Transport = Framed; +pub type Transport = Framed; fn login_error_message(code: &ErrorCode) -> &'static str { pub use ErrorCode::*; diff --git a/core/src/diffie_hellman.rs b/core/src/diffie_hellman.rs index 85448098..42d1fd9c 100644 --- a/core/src/diffie_hellman.rs +++ b/core/src/diffie_hellman.rs @@ -17,19 +17,19 @@ pub static DH_PRIME: Lazy = Lazy::new(|| { ]) }); -pub struct DHLocalKeys { +pub struct DhLocalKeys { private_key: BigUint, public_key: BigUint, } -impl DHLocalKeys { - pub fn random(rng: &mut R) -> DHLocalKeys { +impl DhLocalKeys { + pub fn random(rng: &mut R) -> DhLocalKeys { let key_data = util::rand_vec(rng, 95); let private_key = BigUint::from_bytes_be(&key_data); let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME); - DHLocalKeys { + DhLocalKeys { private_key, public_key, } diff --git a/core/src/mercury/mod.rs b/core/src/mercury/mod.rs index 1a68e15e..b920e7e6 100644 --- a/core/src/mercury/mod.rs +++ b/core/src/mercury/mod.rs @@ -82,7 +82,7 @@ impl MercuryManager { pub fn get>(&self, uri: T) -> MercuryFuture { self.request(MercuryRequest { - method: MercuryMethod::GET, + method: MercuryMethod::Get, uri: uri.into(), content_type: None, payload: Vec::new(), @@ -91,7 +91,7 @@ impl MercuryManager { pub fn send>(&self, uri: T, data: Vec) -> MercuryFuture { self.request(MercuryRequest { - method: MercuryMethod::SEND, + method: MercuryMethod::Send, uri: uri.into(), content_type: None, payload: vec![data], @@ -109,7 +109,7 @@ impl MercuryManager { { let uri = uri.into(); let request = self.request(MercuryRequest { - method: MercuryMethod::SUB, + method: MercuryMethod::Sub, uri: uri.clone(), content_type: None, payload: Vec::new(), diff --git a/core/src/mercury/types.rs b/core/src/mercury/types.rs index 57cedce5..402a954c 100644 --- a/core/src/mercury/types.rs +++ b/core/src/mercury/types.rs @@ -6,10 +6,10 @@ use crate::protocol; #[derive(Debug, PartialEq, Eq)] pub enum MercuryMethod { - GET, - SUB, - UNSUB, - SEND, + Get, + Sub, + Unsub, + Send, } #[derive(Debug)] @@ -33,10 +33,10 @@ pub struct MercuryError; impl ToString for MercuryMethod { fn to_string(&self) -> String { match *self { - MercuryMethod::GET => "GET", - MercuryMethod::SUB => "SUB", - MercuryMethod::UNSUB => "UNSUB", - MercuryMethod::SEND => "SEND", + MercuryMethod::Get => "GET", + MercuryMethod::Sub => "SUB", + MercuryMethod::Unsub => "UNSUB", + MercuryMethod::Send => "SEND", } .to_owned() } @@ -45,9 +45,9 @@ impl ToString for MercuryMethod { impl MercuryMethod { pub fn command(&self) -> u8 { match *self { - MercuryMethod::GET | MercuryMethod::SEND => 0xb2, - MercuryMethod::SUB => 0xb3, - MercuryMethod::UNSUB => 0xb4, + MercuryMethod::Get | MercuryMethod::Send => 0xb2, + MercuryMethod::Sub => 0xb3, + MercuryMethod::Unsub => 0xb4, } } } diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index 09d10975..801c6ac9 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -18,9 +18,9 @@ impl From<&str> for SpotifyAudioType { } } -impl Into<&str> for SpotifyAudioType { - fn into(self) -> &'static str { - match self { +impl From for &str { + fn from(audio_type: SpotifyAudioType) -> &'static str { + match audio_type { SpotifyAudioType::Track => "track", SpotifyAudioType::Podcast => "episode", SpotifyAudioType::NonPlayable => "unknown", diff --git a/src/main.rs b/src/main.rs index 1f67793e..cd2a7861 100644 --- a/src/main.rs +++ b/src/main.rs @@ -345,7 +345,9 @@ fn setup(args: &[String]) -> Setup { .map(|port| port.parse::().unwrap()) .unwrap_or(0); - let name = matches.opt_str("name").unwrap_or("Librespot".to_string()); + let name = matches + .opt_str("name") + .unwrap_or_else(|| "Librespot".to_string()); let credentials = { let cached_credentials = cache.as_ref().and_then(Cache::credentials); From 11ce29077e661100fa71580dbcb9dda6a4950932 Mon Sep 17 00:00:00 2001 From: johannesd3 Date: Thu, 1 Apr 2021 18:10:42 +0200 Subject: [PATCH 31/46] Fix formatting --- core/src/authentication.rs | 7 ++----- examples/get_token.rs | 4 +++- examples/play.rs | 10 +++++----- examples/playlist_tracks.rs | 4 +++- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/core/src/authentication.rs b/core/src/authentication.rs index 2992abcd..db787bbe 100644 --- a/core/src/authentication.rs +++ b/core/src/authentication.rs @@ -31,13 +31,10 @@ impl Credentials { /// ### Example /// ```rust /// use librespot_core::authentication::Credentials; - /// + /// /// let creds = Credentials::with_password("my account", "my password"); /// ``` - pub fn with_password( - username: impl Into, - password: impl Into, - ) -> Credentials { + pub fn with_password(username: impl Into, password: impl Into) -> Credentials { Credentials { username: username.into(), auth_type: AuthenticationType::AUTHENTICATION_USER_PASS, diff --git a/examples/get_token.rs b/examples/get_token.rs index 15b97bcb..636155e0 100644 --- a/examples/get_token.rs +++ b/examples/get_token.rs @@ -20,7 +20,9 @@ async fn main() { println!("Connecting.."); let credentials = Credentials::with_password(&args[1], &args[2]); - let session = Session::connect(session_config, credentials, None).await.unwrap(); + let session = Session::connect(session_config, credentials, None) + .await + .unwrap(); println!( "Token: {:#?}", diff --git a/examples/play.rs b/examples/play.rs index 9b1988a6..b4f2b68e 100644 --- a/examples/play.rs +++ b/examples/play.rs @@ -4,8 +4,8 @@ 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::audio_backend; +use librespot::playback::config::PlayerConfig; use librespot::playback::player::Player; #[tokio::main] @@ -25,11 +25,11 @@ async fn main() { let backend = audio_backend::find(None).unwrap(); println!("Connecting .."); - let session = Session::connect(session_config, credentials, None).await.unwrap(); + let session = Session::connect(session_config, credentials, None) + .await + .unwrap(); - let (mut player, _) = Player::new(player_config, session, None, move || { - backend(None) - }); + let (mut player, _) = Player::new(player_config, session, None, move || backend(None)); player.load(track, true, 0); diff --git a/examples/playlist_tracks.rs b/examples/playlist_tracks.rs index 7bd95ae2..e96938cb 100644 --- a/examples/playlist_tracks.rs +++ b/examples/playlist_tracks.rs @@ -25,7 +25,9 @@ async fn main() { let plist_uri = SpotifyId::from_base62(uri_parts[2]).unwrap(); - let session = Session::connect(session_config, credentials, None).await.unwrap(); + let session = Session::connect(session_config, credentials, None) + .await + .unwrap(); let plist = Playlist::get(&session, plist_uri).await.unwrap(); println!("{:?}", plist); From d0ea9631d2c9e0b1be5198061280a3894bce9287 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 9 Apr 2021 19:31:26 +0200 Subject: [PATCH 32/46] Optimize requantizer to work in `f32`, then round --- audio/src/convert.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/audio/src/convert.rs b/audio/src/convert.rs index 74a4d8f4..e291c804 100644 --- a/audio/src/convert.rs +++ b/audio/src/convert.rs @@ -21,7 +21,16 @@ macro_rules! convert_samples_to { $samples .iter() .map(|sample| { - (*sample as f64 * (std::$type::MAX as f64 + 0.5) - 0.5) as $type >> $drop_bits + // 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() }; From 222f9bbd01994f55160c3d226641c9e258eee7fd Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 9 Apr 2021 20:01:21 +0200 Subject: [PATCH 33/46] Bump playback crates to the latest supporting Rust 1.41.1 For Rodio, this fixes garbled sound on some but not all Alsa hosts. --- Cargo.lock | 20 ++++++++++---------- playback/Cargo.toml | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f40f4dca..837df2fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -339,9 +339,9 @@ dependencies = [ [[package]] name = "cpal" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "840981d3f30230d9120328d64be72319dbbedabb61bcd4c370a54cdd051238ac" +checksum = "8351ddf2aaa3c583fa388029f8b3d26f3c7035a20911fdd5f2e2ed7ab57dad25" dependencies = [ "alsa", "core-foundation-sys", @@ -1161,9 +1161,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.91" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8916b1f6ca17130ec6568feccee27c156ad12037880833a3b842a823236502e7" +checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41" [[package]] name = "libloading" @@ -1195,9 +1195,9 @@ dependencies = [ [[package]] name = "libpulse-binding" -version = "2.23.0" +version = "2.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2405f806801527dfb3d2b6d48a282cdebe9a1b41b0652e0d7b5bad81dbc700e" +checksum = "db951f37898e19a6785208e3a290261e0f1a8e086716be596aaad684882ca8e3" dependencies = [ "bitflags", "libc", @@ -2209,9 +2209,9 @@ dependencies = [ [[package]] name = "rodio" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9683532495146e98878d4948fa1a1953f584cd923f2a5f5c26b7a8701b56943" +checksum = "b65c2eda643191f6d1bb12ea323a9db8d9ba95374e9be3780b5a9fb5cfb8520f" dependencies = [ "cpal", ] @@ -2288,9 +2288,9 @@ dependencies = [ [[package]] name = "sdl2-sys" -version = "0.34.3" +version = "0.34.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d81feded049b9c14eceb4a4f6d596a98cebbd59abdba949c5552a015466d33" +checksum = "4cb164f53dbcad111de976bbf1f3083d3fcdeda88da9cfa281c70822720ee3da" dependencies = [ "cfg-if 0.1.10", "libc", diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 07e31799..0666259d 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -31,7 +31,7 @@ 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 } @@ -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" ] +gstreamer-backend = ["gstreamer", "gstreamer-app", "glib"] From 7ddb1a20bb21ceae17821bba2f3714377d8b1cc3 Mon Sep 17 00:00:00 2001 From: johannesd3 Date: Wed, 10 Mar 2021 22:55:40 +0100 Subject: [PATCH 34/46] Reuse librespot-core's Diffie Hellman in discovery --- Cargo.lock | 1 - connect/Cargo.toml | 1 - connect/src/discovery.rs | 27 +++++++++------------------ 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a43939d9..023add28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1344,7 +1344,6 @@ dependencies = [ "librespot-playback", "librespot-protocol", "log", - "num-bigint", "protobuf", "rand", "serde", diff --git a/connect/Cargo.toml b/connect/Cargo.toml index b03de885..1331b627 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -16,7 +16,6 @@ futures-util = { version = "0.3", default_features = false } hmac = "0.10" hyper = { version = "0.14", features = ["server", "http1", "tcp"] } log = "0.4" -num-bigint = "0.3" protobuf = "~2.14.0" rand = "0.8" serde = { version = "1.0", features = ["derive"] } diff --git a/connect/src/discovery.rs b/connect/src/discovery.rs index 7bb36a20..383035cc 100644 --- a/connect/src/discovery.rs +++ b/connect/src/discovery.rs @@ -5,7 +5,6 @@ use futures_core::Stream; use hmac::{Hmac, Mac, NewMac}; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Method, Request, Response, StatusCode}; -use num_bigint::BigUint; use serde_json::json; use sha1::{Digest, Sha1}; use tokio::sync::{mpsc, oneshot}; @@ -15,8 +14,7 @@ use dns_sd::DNSService; use librespot_core::authentication::Credentials; use librespot_core::config::ConnectConfig; -use librespot_core::diffie_hellman::{DH_GENERATOR, DH_PRIME}; -use librespot_core::util; +use librespot_core::diffie_hellman::DhLocalKeys; use std::borrow::Cow; use std::collections::BTreeMap; @@ -34,8 +32,7 @@ struct Discovery(Arc); struct DiscoveryInner { config: ConnectConfig, device_id: String, - private_key: BigUint, - public_key: BigUint, + keys: DhLocalKeys, tx: mpsc::UnboundedSender, } @@ -46,15 +43,10 @@ impl Discovery { ) -> (Discovery, mpsc::UnboundedReceiver) { let (tx, rx) = mpsc::unbounded_channel(); - let key_data = util::rand_vec(&mut rand::thread_rng(), 95); - let private_key = BigUint::from_bytes_be(&key_data); - let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME); - let discovery = Discovery(Arc::new(DiscoveryInner { config, device_id, - private_key, - public_key, + keys: DhLocalKeys::random(&mut rand::thread_rng()), tx, })); @@ -62,8 +54,7 @@ impl Discovery { } fn handle_get_info(&self, _: BTreeMap, Cow<'_, str>>) -> Response { - let public_key = self.0.public_key.to_bytes_be(); - let public_key = base64::encode(&public_key); + let public_key = base64::encode(&self.0.keys.public_key()); let result = json!({ "status": 101, @@ -98,16 +89,16 @@ impl Discovery { let encrypted_blob = base64::decode(encrypted_blob.as_bytes()).unwrap(); - let client_key = base64::decode(client_key.as_bytes()).unwrap(); - let client_key = BigUint::from_bytes_be(&client_key); - - let shared_key = util::powm(&client_key, &self.0.private_key, &DH_PRIME); + let shared_key = self + .0 + .keys + .shared_secret(&base64::decode(client_key.as_bytes()).unwrap()); let iv = &encrypted_blob[0..16]; let encrypted = &encrypted_blob[16..encrypted_blob.len() - 20]; let cksum = &encrypted_blob[encrypted_blob.len() - 20..encrypted_blob.len()]; - let base_key = Sha1::digest(&shared_key.to_bytes_be()); + let base_key = Sha1::digest(&shared_key); let base_key = &base_key[..16]; let checksum_key = { From 9378ae5b6f5ff5a6700e3882058a69a18d547d70 Mon Sep 17 00:00:00 2001 From: johannesd3 Date: Thu, 18 Mar 2021 17:52:39 +0100 Subject: [PATCH 35/46] Bump num-bigint dependency --- Cargo.lock | 4 ++-- core/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 023add28..3991775f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1633,9 +1633,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e9a41747ae4633fce5adffb4d2e81ffc5e89593cb19917f8fb2cc5ff76507bf" +checksum = "4e0d047c1062aa51e256408c560894e5251f08925980e53cf1aa5bd00eec6512" dependencies = [ "autocfg", "num-integer", diff --git a/core/Cargo.toml b/core/Cargo.toml index 7aabb1b0..4e9b03c9 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -25,7 +25,7 @@ http = "0.2" hyper = { version = "0.14", optional = true, features = ["client", "tcp", "http1"] } hyper-proxy = { version = "0.9.1", optional = true, default-features = false } log = "0.4" -num-bigint = "0.3" +num-bigint = "0.4" num-integer = "0.1" num-traits = "0.2" once_cell = "1.5.2" From e688e7e886791b98e5af0271f5ec552c6a699cca Mon Sep 17 00:00:00 2001 From: johannesd3 Date: Thu, 18 Mar 2021 17:51:50 +0100 Subject: [PATCH 36/46] Almost eliminate util module --- Cargo.lock | 3 +++ connect/Cargo.toml | 1 + connect/src/spirc.rs | 7 +++-- core/Cargo.toml | 3 ++- core/src/connection/handshake.rs | 8 +++--- core/src/diffie_hellman.rs | 38 +++++++++++++++++--------- core/src/lib.rs | 2 ++ core/src/mercury/mod.rs | 18 ++++++++++--- core/src/util.rs | 46 -------------------------------- 9 files changed, 59 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3991775f..9b66a41e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1335,6 +1335,7 @@ dependencies = [ "base64", "block-modes", "dns-sd", + "form_urlencoded", "futures-core", "futures-util", "hmac", @@ -1363,6 +1364,7 @@ dependencies = [ "byteorder", "bytes", "env_logger", + "form_urlencoded", "futures-core", "futures-util", "hmac", @@ -1640,6 +1642,7 @@ dependencies = [ "autocfg", "num-integer", "num-traits", + "rand", ] [[package]] diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 1331b627..052cf6a5 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -11,6 +11,7 @@ edition = "2018" aes-ctr = "0.6" base64 = "0.13" block-modes = "0.7" +form_urlencoded = "1.0" futures-core = "0.3" futures-util = { version = "0.3", default_features = false } hmac = "0.10" diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 1a7f64ec..4fcb025a 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -7,7 +7,6 @@ use crate::core::config::{ConnectConfig, VolumeCtrl}; use crate::core::mercury::{MercuryError, MercurySender}; use crate::core::session::Session; use crate::core::spotify_id::{SpotifyAudioType, SpotifyId, SpotifyIdError}; -use crate::core::util::url_encode; use crate::core::util::SeqGenerator; use crate::core::version; use crate::playback::mixer::Mixer; @@ -244,6 +243,10 @@ fn volume_to_mixer(volume: u16, volume_ctrl: &VolumeCtrl) -> u16 { } } +fn url_encode(bytes: impl AsRef<[u8]>) -> String { + form_urlencoded::byte_serialize(bytes.as_ref()).collect() +} + impl Spirc { pub fn new( config: ConnectConfig, @@ -256,7 +259,7 @@ impl Spirc { let ident = session.device_id().to_owned(); // Uri updated in response to issue #288 - debug!("canonical_username: {}", url_encode(&session.username())); + debug!("canonical_username: {}", &session.username()); let uri = format!("hm://remote/user/{}/", url_encode(&session.username())); let subscription = Box::pin( diff --git a/core/Cargo.toml b/core/Cargo.toml index 4e9b03c9..29f4f332 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -17,6 +17,7 @@ aes = "0.6" base64 = "0.13" byteorder = "1.4" bytes = "1.0" +form_urlencoded = "1.0" futures-core = { version = "0.3", default-features = false } futures-util = { version = "0.3", default-features = false, features = ["alloc", "bilock", "unstable", "sink"] } hmac = "0.10" @@ -25,7 +26,7 @@ http = "0.2" hyper = { version = "0.14", optional = true, features = ["client", "tcp", "http1"] } hyper-proxy = { version = "0.9.1", optional = true, default-features = false } log = "0.4" -num-bigint = "0.4" +num-bigint = { version = "0.4", features = ["rand"] } num-integer = "0.1" num-traits = "0.2" once_cell = "1.5.2" diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index 879df67c..6f802ab5 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -1,7 +1,7 @@ use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; use hmac::{Hmac, Mac, NewMac}; use protobuf::{self, Message}; -use rand::thread_rng; +use rand::{thread_rng, RngCore}; use sha1::Sha1; use std::io; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; @@ -11,7 +11,6 @@ use super::codec::ApCodec; use crate::diffie_hellman::DhLocalKeys; use crate::protocol; use crate::protocol::keyexchange::{APResponseMessage, ClientHello, ClientResponsePlaintext}; -use crate::util; pub async fn handshake( mut connection: T, @@ -40,6 +39,9 @@ async fn client_hello(connection: &mut T, gc: Vec) -> io::Result> where T: AsyncWrite + Unpin, { + let mut client_nonce = vec![0; 0x10]; + thread_rng().fill_bytes(&mut client_nonce); + let mut packet = ClientHello::new(); packet .mut_build_info() @@ -59,7 +61,7 @@ where .mut_login_crypto_hello() .mut_diffie_hellman() .set_server_keys_known(1); - packet.set_client_nonce(util::rand_vec(&mut thread_rng(), 0x10)); + packet.set_client_nonce(client_nonce); packet.set_padding(vec![0x1e]); let mut buffer = vec![0, 4]; diff --git a/core/src/diffie_hellman.rs b/core/src/diffie_hellman.rs index 42d1fd9c..57caa029 100644 --- a/core/src/diffie_hellman.rs +++ b/core/src/diffie_hellman.rs @@ -1,11 +1,11 @@ -use num_bigint::BigUint; +use num_bigint::{BigUint, RandBigInt}; +use num_integer::Integer; +use num_traits::{One, Zero}; use once_cell::sync::Lazy; -use rand::Rng; +use rand::{CryptoRng, Rng}; -use crate::util; - -pub static DH_GENERATOR: Lazy = Lazy::new(|| BigUint::from_bytes_be(&[0x02])); -pub static DH_PRIME: Lazy = Lazy::new(|| { +static DH_GENERATOR: Lazy = Lazy::new(|| BigUint::from_bytes_be(&[0x02])); +static DH_PRIME: Lazy = Lazy::new(|| { BigUint::from_bytes_be(&[ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, @@ -17,17 +17,31 @@ pub static DH_PRIME: Lazy = Lazy::new(|| { ]) }); +fn powm(base: &BigUint, exp: &BigUint, modulus: &BigUint) -> BigUint { + let mut base = base.clone(); + let mut exp = exp.clone(); + let mut result: BigUint = One::one(); + + while !exp.is_zero() { + if exp.is_odd() { + result = (result * &base) % modulus; + } + exp >>= 1; + base = (&base * &base) % modulus; + } + + result +} + pub struct DhLocalKeys { private_key: BigUint, public_key: BigUint, } impl DhLocalKeys { - pub fn random(rng: &mut R) -> DhLocalKeys { - let key_data = util::rand_vec(rng, 95); - - let private_key = BigUint::from_bytes_be(&key_data); - let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME); + pub fn random(rng: &mut R) -> DhLocalKeys { + let private_key = rng.gen_biguint(95 * 8); + let public_key = powm(&DH_GENERATOR, &private_key, &DH_PRIME); DhLocalKeys { private_key, @@ -40,7 +54,7 @@ impl DhLocalKeys { } pub fn shared_secret(&self, remote_key: &[u8]) -> Vec { - let shared_key = util::powm( + let shared_key = powm( &BigUint::from_bytes_be(remote_key), &self.private_key, &DH_PRIME, diff --git a/core/src/lib.rs b/core/src/lib.rs index 320967f7..bb3e21d5 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -14,12 +14,14 @@ pub mod cache; pub mod channel; pub mod config; mod connection; +#[doc(hidden)] pub mod diffie_hellman; pub mod keymaster; pub mod mercury; mod proxytunnel; pub mod session; pub mod spotify_id; +#[doc(hidden)] pub mod util; pub mod version; diff --git a/core/src/mercury/mod.rs b/core/src/mercury/mod.rs index b920e7e6..ef04e985 100644 --- a/core/src/mercury/mod.rs +++ b/core/src/mercury/mod.rs @@ -10,7 +10,6 @@ use bytes::Bytes; use tokio::sync::{mpsc, oneshot}; use crate::protocol; -use crate::util::url_encode; use crate::util::SeqGenerator; mod types; @@ -199,7 +198,7 @@ impl MercuryManager { let header: protocol::mercury::Header = protobuf::parse_from_bytes(&header_data).unwrap(); let response = MercuryResponse { - uri: url_encode(header.get_uri()), + uri: header.get_uri().to_string(), status_code: header.get_status_code(), payload: pending.parts, }; @@ -214,8 +213,21 @@ impl MercuryManager { } else if cmd == 0xb5 { self.lock(|inner| { let mut found = false; + + // TODO: This is just a workaround to make utf-8 encoded usernames work. + // A better solution would be to use an uri struct and urlencode it directly + // before sending while saving the subscription under its unencoded form. + let mut uri_split = response.uri.split('/'); + + let encoded_uri = std::iter::once(uri_split.next().unwrap().to_string()) + .chain(uri_split.map(|component| { + form_urlencoded::byte_serialize(component.as_bytes()).collect::() + })) + .collect::>() + .join("/"); + inner.subscriptions.retain(|&(ref prefix, ref sub)| { - if response.uri.starts_with(prefix) { + if encoded_uri.starts_with(prefix) { found = true; // if send fails, remove from list of subs diff --git a/core/src/util.rs b/core/src/util.rs index c55d9601..df9ea714 100644 --- a/core/src/util.rs +++ b/core/src/util.rs @@ -1,50 +1,4 @@ -use num_bigint::BigUint; -use num_integer::Integer; -use num_traits::{One, Zero}; -use rand::Rng; use std::mem; -use std::ops::{Mul, Rem, Shr}; - -pub fn rand_vec(rng: &mut G, size: usize) -> Vec { - ::std::iter::repeat(()) - .map(|()| rng.gen()) - .take(size) - .collect() -} - -pub fn url_encode(inp: &str) -> String { - let mut encoded = String::new(); - - for c in inp.as_bytes().iter() { - match *c as char { - 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' | ':' | '/' => { - encoded.push(*c as char) - } - c => encoded.push_str(format!("%{:02X}", c as u32).as_str()), - }; - } - - encoded -} - -pub fn powm(base: &BigUint, exp: &BigUint, modulus: &BigUint) -> BigUint { - let mut base = base.clone(); - let mut exp = exp.clone(); - let mut result: BigUint = One::one(); - - while !exp.is_zero() { - if exp.is_odd() { - result = result.mul(&base).rem(modulus); - } - exp = exp.shr(1); - base = (&base).mul(&base).rem(modulus); - } - - result -} - -pub trait ReadSeek: ::std::io::Read + ::std::io::Seek {} -impl ReadSeek for T {} pub trait Seq { fn next(&self) -> Self; From cb8c9c2afa6880c95ee29f1e403a7f291aaf6013 Mon Sep 17 00:00:00 2001 From: johannesd3 Date: Thu, 1 Apr 2021 18:20:42 +0200 Subject: [PATCH 37/46] Enable apresolve always in binary Librespot-connect uses hyper anyway, so no one needs to disable it only to reduce the number of dependencies. Furthermore, when using another backend, people will use --no-default-features and will forget to enable the apresolve feature again. --- Cargo.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8f3e44c2..7939b76c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ version = "0.1.6" [dependencies.librespot-core] path = "core" version = "0.1.6" +features = ["apresolve"] [dependencies.librespot-metadata] path = "metadata" @@ -58,8 +59,6 @@ url = "2.1" sha-1 = "0.9" [features] -apresolve = ["librespot-core/apresolve"] - alsa-backend = ["librespot-playback/alsa-backend"] portaudio-backend = ["librespot-playback/portaudio-backend"] pulseaudio-backend = ["librespot-playback/pulseaudio-backend"] @@ -75,7 +74,7 @@ with-lewton = ["librespot-audio/with-lewton"] # with-dns-sd = ["librespot-connect/with-dns-sd"] -default = ["rodio-backend", "apresolve", "with-lewton"] +default = ["rodio-backend", "with-lewton"] [package.metadata.deb] maintainer = "librespot-org" From b7350b71da72251b57c9449a94b722b0a1420b01 Mon Sep 17 00:00:00 2001 From: johannesd3 Date: Fri, 9 Apr 2021 16:38:43 +0200 Subject: [PATCH 38/46] Restore previous feature flags Some of the feature flags librespot uses are not really additive but rather mutual exclusive. A previous attempt to improve the situation had other drawbacks, so it's better to postpone a decision and restore the old behaviour. --- Cargo.toml | 5 ++--- audio/Cargo.toml | 3 +-- audio/src/lib.rs | 15 +++------------ connect/Cargo.toml | 4 +--- 4 files changed, 7 insertions(+), 20 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7939b76c..14e33a83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,11 +70,10 @@ gstreamer-backend = ["librespot-playback/gstreamer-backend"] with-tremor = ["librespot-audio/with-tremor"] with-vorbis = ["librespot-audio/with-vorbis"] -with-lewton = ["librespot-audio/with-lewton"] -# with-dns-sd = ["librespot-connect/with-dns-sd"] +with-dns-sd = ["librespot-connect/with-dns-sd"] -default = ["rodio-backend", "with-lewton"] +default = ["rodio-backend"] [package.metadata.deb] maintainer = "librespot-org" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index d8c0eea2..5dec789f 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -15,17 +15,16 @@ aes-ctr = "0.6" byteorder = "1.4" bytes = "1.0" cfg-if = "1" +lewton = "0.10" log = "0.4" futures-util = { version = "0.3", default_features = false } ogg = "0.8" tempfile = "3.1" tokio = { version = "1", features = ["sync"] } -lewton = { version = "0.10", optional = true } librespot-tremor = { version = "0.2.0", optional = true } vorbis = { version ="0.0.14", optional = true } [features] -with-lewton = ["lewton"] with-tremor = ["librespot-tremor"] with-vorbis = ["vorbis"] diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 80f1097d..35ec4dd7 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -8,22 +8,13 @@ mod fetch; use cfg_if::cfg_if; -#[cfg(any( - all(feature = "with-lewton", feature = "with-tremor"), - all(feature = "with-vorbis", feature = "with-tremor"), - all(feature = "with-lewton", feature = "with-vorbis") -))] -compile_error!("Cannot use two decoders at the same time."); - cfg_if! { - if #[cfg(feature = "with-lewton")] { - mod lewton_decoder; - pub use lewton_decoder::{VorbisDecoder, VorbisError}; - } else if #[cfg(any(feature = "with-tremor", feature = "with-vorbis"))] { + if #[cfg(any(feature = "with-tremor", feature = "with-vorbis"))] { mod libvorbis_decoder; pub use crate::libvorbis_decoder::{VorbisDecoder, VorbisError}; } else { - compile_error!("Must choose a vorbis decoder."); + mod lewton_decoder; + pub use lewton_decoder::{VorbisDecoder, VorbisError}; } } diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 052cf6a5..689efd3c 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -16,6 +16,7 @@ futures-core = "0.3" futures-util = { version = "0.3", default_features = false } hmac = "0.10" hyper = { version = "0.14", features = ["server", "http1", "tcp"] } +libmdns = "0.6" log = "0.4" protobuf = "~2.14.0" rand = "0.8" @@ -27,7 +28,6 @@ tokio-stream = { version = "0.1" } url = "2.1" dns-sd = { version = "0.1.3", optional = true } -libmdns = { version = "0.6", optional = true } [dependencies.librespot-core] path = "../core" @@ -42,7 +42,5 @@ path = "../protocol" version = "0.1.6" [features] -with-libmdns = ["libmdns"] with-dns-sd = ["dns-sd"] -default = ["with-libmdns"] From 9a3a6668568bb1169a50f6cbc9e7cd1964d21916 Mon Sep 17 00:00:00 2001 From: johannesd3 Date: Fri, 9 Apr 2021 16:43:05 +0200 Subject: [PATCH 39/46] Bump MSRV to 1.45 --- .github/workflows/test.yml | 2 +- COMPILING.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9d1250f9..907a6eb7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,7 +41,7 @@ jobs: matrix: os: [ubuntu-latest] toolchain: - - 1.41.1 # MSRV (Minimum supported rust version) + - 1.45 # MSRV (Minimum supported rust version) - stable - beta experimental: [false] diff --git a/COMPILING.md b/COMPILING.md index 4320cdbb..539dc698 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -13,7 +13,7 @@ curl https://sh.rustup.rs -sSf | sh Follow any prompts it gives you to install Rust. Once that’s done, Rust's standard tools should be setup and ready to use. -*Note: The current minimum required Rust version at the time of writing is 1.41, you can find the current minimum version specified in the `.github/workflow/test.yml` file.* +*Note: The current minimum required Rust version at the time of writing is 1.45, you can find the current minimum version specified in the `.github/workflow/test.yml` file.* #### Additional Rust tools - `rustfmt` To ensure a consistent codebase, we utilise [`rustfmt`](https://github.com/rust-lang/rustfmt), which is installed by default with `rustup` these days, else it can be installed manually with: From 5435ab3270fc889e2c217803906f775f5d36f5d0 Mon Sep 17 00:00:00 2001 From: johannesd3 Date: Thu, 1 Apr 2021 18:41:01 +0200 Subject: [PATCH 40/46] Fix compile errors in backends fe37186 added the restriction that `Sink`s must be `Send`. It turned out later that this restrictions was unnecessary, and since some `Sink`s aren't `Send` yet, this restriction is lifted again. librespot-org/librespot#601 refactored the `RodioSink` in order to make it `Send`. These changes are partly reverted in favour of the initial simpler design. Furthermore, there were some compile errors in the gstreamer backend which are hereby fixed. --- playback/src/audio_backend/gstreamer.rs | 15 +++++++------ playback/src/audio_backend/mod.rs | 4 ++-- playback/src/audio_backend/rodio.rs | 29 +++++-------------------- playback/src/player.rs | 4 ++-- src/main.rs | 2 +- 5 files changed, 18 insertions(+), 36 deletions(-) diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 4678bfb0..e2b52d74 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -1,7 +1,9 @@ use super::{Open, Sink}; use crate::audio::AudioPacket; + use gst::prelude::*; -use gst::*; +use gstreamer as gst; +use gstreamer_app as gst_app; use zerocopy::*; use std::sync::mpsc::{sync_channel, SyncSender}; @@ -52,14 +54,13 @@ impl Open for GstreamerSink { thread::spawn(move || { for data in rx { let buffer = bufferpool.acquire_buffer(None); - if !buffer.is_err() { - let mut okbuffer = buffer.unwrap(); - let mutbuf = okbuffer.make_mut(); + if let Ok(mut buffer) = buffer { + let mutbuf = buffer.make_mut(); mutbuf.set_size(data.len()); mutbuf .copy_from_slice(0, data.as_bytes()) .expect("Failed to copy from slice"); - let _eat = appsrc.push_buffer(okbuffer); + let _eat = appsrc.push_buffer(buffer); } } }); @@ -69,8 +70,8 @@ impl Open for GstreamerSink { let watch_mainloop = thread_mainloop.clone(); bus.add_watch(move |_, msg| { match msg.view() { - MessageView::Eos(..) => watch_mainloop.quit(), - MessageView::Error(err) => { + gst::MessageView::Eos(..) => watch_mainloop.quit(), + gst::MessageView::Error(err) => { println!( "Error from {:?}: {} ({:?})", err.get_src().map(|s| s.get_path_string()), diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index c816a6d6..5fa518ee 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -11,9 +11,9 @@ pub trait Sink { fn write(&mut self, packet: &AudioPacket) -> io::Result<()>; } -pub type SinkBuilder = fn(Option) -> Box; +pub type SinkBuilder = fn(Option) -> Box; -fn mk_sink(device: Option) -> Box { +fn mk_sink(device: Option) -> Box { Box::new(S::open(device)) } diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 338dfbbf..8e448211 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -1,5 +1,4 @@ use std::process::exit; -use std::{convert::Infallible, sync::mpsc}; use std::{io, thread, time}; use cpal::traits::{DeviceTrait, HostTrait}; @@ -15,12 +14,12 @@ use crate::audio::AudioPacket; compile_error!("Rodio JACK backend is currently only supported on linux."); #[cfg(feature = "rodio-backend")] -pub fn mk_rodio(device: Option) -> Box { +pub fn mk_rodio(device: Option) -> Box { Box::new(open(cpal::default_host(), device)) } #[cfg(feature = "rodiojack-backend")] -pub fn mk_rodiojack(device: Option) -> Box { +pub fn mk_rodiojack(device: Option) -> Box { Box::new(open( cpal::host_from_id(cpal::HostId::Jack).unwrap(), device, @@ -43,8 +42,7 @@ pub enum RodioError { pub struct RodioSink { rodio_sink: rodio::Sink, - // will produce a TryRecvError on the receiver side when it is dropped. - _close_tx: mpsc::SyncSender, + _stream: rodio::OutputStream, } fn list_formats(device: &rodio::Device) { @@ -152,29 +150,12 @@ fn create_sink( pub fn open(host: cpal::Host, device: Option) -> RodioSink { debug!("Using rodio sink with cpal host: {}", host.id().name()); - let (sink_tx, sink_rx) = mpsc::sync_channel(1); - let (close_tx, close_rx) = mpsc::sync_channel(1); - - std::thread::spawn(move || match create_sink(&host, device) { - Ok((sink, stream)) => { - sink_tx.send(Ok(sink)).unwrap(); - - close_rx.recv().unwrap_err(); // This will fail as soon as the sender is dropped - debug!("drop rodio::OutputStream"); - drop(stream); - } - Err(e) => { - sink_tx.send(Err(e)).unwrap(); - } - }); - - // Instead of the second `unwrap`, better error handling could be introduced - let sink = sink_rx.recv().unwrap().unwrap(); + let (sink, stream) = create_sink(&host, device).unwrap(); debug!("Rodio sink was created"); RodioSink { rodio_sink: sink, - _close_tx: close_tx, + _stream: stream, } } diff --git a/playback/src/player.rs b/playback/src/player.rs index 0d2380e7..497a4a11 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -50,7 +50,7 @@ struct PlayerInternal { state: PlayerState, preload: PlayerPreload, - sink: Box, + sink: Box, sink_status: SinkStatus, sink_event_callback: Option, audio_filter: Option>, @@ -242,7 +242,7 @@ impl Player { sink_builder: F, ) -> (Player, PlayerEventChannel) where - F: FnOnce() -> Box + Send + 'static, + F: FnOnce() -> Box + Send + 'static, { let (cmd_tx, cmd_rx) = mpsc::unbounded_channel(); let (event_sender, event_receiver) = mpsc::unbounded_channel(); diff --git a/src/main.rs b/src/main.rs index cd2a7861..54057aed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -105,7 +105,7 @@ fn print_version() { #[derive(Clone)] struct Setup { - backend: fn(Option) -> Box, + backend: fn(Option) -> Box, device: Option, mixer: fn(Option) -> Box, From 690e0d2e10f942d1243ea2bae1ca902ffc322cb6 Mon Sep 17 00:00:00 2001 From: johannesd3 Date: Fri, 2 Apr 2021 14:44:42 +0200 Subject: [PATCH 41/46] Add simple tests to librespot-core The first test checks whether apresolve works. A second test tries to create a Spotify sessions with fake credentials and asserts that an error is returned. --- core/src/apresolve.rs | 23 +++++++++++++++++++++ core/tests/connect.rs | 48 +++++++++++++++---------------------------- 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index 85baba69..b11e275f 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -66,3 +66,26 @@ pub async fn apresolve(proxy: Option<&Url>, ap_port: Option) -> String { AP_FALLBACK.into() }) } + +#[cfg(test)] +mod test { + use std::net::ToSocketAddrs; + + use super::try_apresolve; + + #[tokio::test] + async fn test_apresolve() { + let ap = try_apresolve(None, None).await.unwrap(); + + // Assert that the result contains a valid host and port + ap.to_socket_addrs().unwrap().next().unwrap(); + } + + #[tokio::test] + async fn test_apresolve_port_443() { + let ap = try_apresolve(None, Some(443)).await.unwrap(); + + let port = ap.to_socket_addrs().unwrap().next().unwrap().port(); + assert_eq!(port, 443); + } +} diff --git a/core/tests/connect.rs b/core/tests/connect.rs index b7bc29f8..4f1dbe6b 100644 --- a/core/tests/connect.rs +++ b/core/tests/connect.rs @@ -1,34 +1,18 @@ -use librespot_core::*; +use librespot_core::authentication::Credentials; +use librespot_core::config::SessionConfig; +use librespot_core::session::Session; -// TODO: test is broken -// #[cfg(test)] -// mod tests { -// use super::*; -// // Test AP Resolve -// use apresolve::apresolve_or_fallback; -// #[tokio::test] -// async fn test_ap_resolve() { -// env_logger::init(); -// let ap = apresolve_or_fallback(&None, &None).await; -// println!("AP: {:?}", ap); -// } +#[tokio::test] +async fn test_connection() { + let result = Session::connect( + SessionConfig::default(), + Credentials::with_password("test", "test"), + None, + ) + .await; -// // Test connect -// use authentication::Credentials; -// use config::SessionConfig; -// #[tokio::test] -// async fn test_connection() -> Result<(), Box> { -// println!("Running connection test"); -// let ap = apresolve_or_fallback(&None, &None).await; -// let credentials = Credentials::with_password(String::from("test"), String::from("test")); -// let session_config = SessionConfig::default(); -// let proxy = None; - -// println!("Connecting to AP \"{}\"", ap); -// let mut connection = connection::connect(ap, &proxy).await?; -// let rc = connection::authenticate(&mut connection, credentials, &session_config.device_id) -// .await?; -// println!("Authenticated as \"{}\"", rc.username); -// Ok(()) -// } -// } + match result { + Ok(_) => panic!("Authentication succeeded despite of bad credentials."), + Err(e) => assert_eq!(e.to_string(), "Login failed with reason: Bad credentials"), + }; +} From ff499825e0ce509b5d49669ac328e4ba85073da4 Mon Sep 17 00:00:00 2001 From: johannesd3 Date: Fri, 9 Apr 2021 17:00:56 +0200 Subject: [PATCH 42/46] Add missing feature flag to tokio --- audio/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 5dec789f..2cd801a1 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -20,7 +20,7 @@ log = "0.4" futures-util = { version = "0.3", default_features = false } ogg = "0.8" tempfile = "3.1" -tokio = { version = "1", features = ["sync"] } +tokio = { version = "1", features = ["sync", "macros"] } librespot-tremor = { version = "0.2.0", optional = true } vorbis = { version ="0.0.14", optional = true } From 317e5864729e07f9d7b2150d928f95c2424d8ad2 Mon Sep 17 00:00:00 2001 From: johannesd3 Date: Fri, 9 Apr 2021 17:01:30 +0200 Subject: [PATCH 43/46] Improve CI Run the tests and add build for Windows. --- .github/workflows/test.yml | 137 ++++++++++++++++++++++++++++--------- 1 file changed, 104 insertions(+), 33 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 907a6eb7..f06445e6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,36 +4,49 @@ name: test on: push: branches: [master, dev] - paths: ['**.rs', '**.toml', '**.lock', '**.yml'] + paths: + [ + "**.rs", + "Cargo.toml", + "/Cargo.lock", + "/rustfmt.toml", + "/.github/workflows", + ] pull_request: - branches: [master, dev] - paths: ['**.rs', '**.toml', '**.lock', '**.yml'] + paths: + [ + "**.rs", + "Cargo.toml", + "/Cargo.lock", + "/rustfmt.toml", + "/.github/workflows", + ] + schedule: + # Run CI every week + - cron: "00 01 * * 0" + +env: + RUST_BACKTRACE: 1 jobs: fmt: - name: 'Rust: format check' - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - # Only run the formatting check for stable - include: - - os: ubuntu-latest - toolchain: stable + name: rustfmt + runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Install toolchain uses: actions-rs/toolchain@v1 with: - # Use default profile to get rustfmt - profile: default - toolchain: ${{ matrix.toolchain }} + profile: minimal + toolchain: stable override: true - - run: cargo fmt --verbose --all -- --check + components: rustfmt + - run: cargo fmt --all -- --check - test: + test-linux: needs: fmt + name: cargo +${{ matrix.toolchain }} build (${{ matrix.os }}) runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.experimental }} strategy: @@ -45,7 +58,7 @@ jobs: - stable - beta experimental: [false] - # Ignore failures in nightly, not ideal, but necessary + # Ignore failures in nightly include: - os: ubuntu-latest toolchain: nightly @@ -53,12 +66,19 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 + - name: Install toolchain uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: ${{ matrix.toolchain }} override: true + + - name: Get Rustc version + id: get-rustc-version + run: echo "::set-output name=version::$(rustc -V)" + shell: bash + - name: Cache Rust dependencies uses: actions/cache@v2 with: @@ -67,21 +87,65 @@ jobs: ~/.cargo/registry/cache ~/.cargo/git target - key: ${{ runner.os }}-build-${{ hashFiles('**/Cargo.lock') }} + key: ${{ runner.os }}-${{ steps.get-rustc-version.outputs.version }}-${{ hashFiles('**/Cargo.lock') }} + - name: Install developer package dependencies - run: sudo apt-get update && sudo apt-get install libpulse-dev portaudio19-dev libasound2-dev libsdl2-dev gstreamer1.0-dev libgstreamer-plugins-base1.0-dev - - run: cargo build --locked --no-default-features - - run: cargo build --locked --examples - - run: cargo build --locked --no-default-features --features "with-tremor" - - run: cargo build --locked --no-default-features --features "with-vorbis" - - run: cargo build --locked --no-default-features --features "alsa-backend" - - run: cargo build --locked --no-default-features --features "portaudio-backend" - - run: cargo build --locked --no-default-features --features "pulseaudio-backend" - - run: cargo build --locked --no-default-features --features "jackaudio-backend" - - run: cargo build --locked --no-default-features --features "rodiojack-backend" - - run: cargo build --locked --no-default-features --features "rodio-backend" - - run: cargo build --locked --no-default-features --features "sdl-backend" - - run: cargo build --locked --no-default-features --features "gstreamer-backend" + run: sudo apt-get update && sudo apt-get install libpulse-dev portaudio19-dev libasound2-dev libsdl2-dev gstreamer1.0-dev libgstreamer-plugins-base1.0-dev libavahi-compat-libdnssd-dev + + - run: cargo build --workspace --examples + - run: cargo test --workspace + + - run: cargo install cargo-hack + - run: cargo hack --workspace --remove-dev-deps + - run: cargo build -p librespot-core --no-default-features + - run: cargo build -p librespot-core + - run: cargo hack build --each-feature -p librespot-audio + - run: cargo build -p librespot-connect + - run: cargo build -p librespot-connect --no-default-features --features with-dns-sd + - run: cargo hack build --locked --each-feature + + test-windows: + needs: fmt + name: cargo build (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [windows-latest] + toolchain: [stable] + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.toolchain }} + profile: minimal + override: true + + - name: Get Rustc version + id: get-rustc-version + run: echo "::set-output name=version::$(rustc -V)" + shell: bash + + - name: Cache Rust dependencies + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git + target + key: ${{ runner.os }}-${{ steps.get-rustc-version.outputs.version }}-${{ hashFiles('**/Cargo.lock') }} + + - run: cargo build --workspace --examples + - run: cargo test --workspace + + - run: cargo install cargo-hack + - run: cargo hack --workspace --remove-dev-deps + - run: cargo build --no-default-features + - run: cargo build test-cross-arm: needs: fmt @@ -97,6 +161,7 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 + - name: Install toolchain uses: actions-rs/toolchain@v1 with: @@ -104,6 +169,12 @@ jobs: target: ${{ matrix.target }} toolchain: ${{ matrix.toolchain }} override: true + + - name: Get Rustc version + id: get-rustc-version + run: echo "::set-output name=version::$(rustc -V)" + shell: bash + - name: Cache Rust dependencies uses: actions/cache@v2 with: @@ -112,7 +183,7 @@ jobs: ~/.cargo/registry/cache ~/.cargo/git target - key: ${{ runner.os }}-build-${{ hashFiles('**/Cargo.lock') }} + key: ${{ runner.os }}-${{ matrix.target }}-${{ steps.get-rustc-version.outputs.version }}-${{ hashFiles('**/Cargo.lock') }} - name: Install cross run: cargo install cross || true - name: Build From a576194b0ecf7ec825e5a474d69fcde8bc9b0400 Mon Sep 17 00:00:00 2001 From: johannesd3 Date: Sat, 10 Apr 2021 13:31:42 +0200 Subject: [PATCH 44/46] Fix bug in rodio backend --- playback/src/audio_backend/rodio.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 98494dba..035cd328 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -199,8 +199,6 @@ impl Sink for RodioSink { } _ => unreachable!(), }; - let source = rodio::buffer::SamplesBuffer::new(2, 44100, packet.samples()); - self.rodio_sink.append(source); // Chunk sizes seem to be about 256 to 3000 ish items long. // Assuming they're on average 1628 then a half second buffer is: From b4f9ae31e2832ba6c0bd753eeb36902dd2cb07cc Mon Sep 17 00:00:00 2001 From: johannesd3 Date: Sat, 10 Apr 2021 14:06:41 +0200 Subject: [PATCH 45/46] Fix clippy warnings --- audio/src/convert.rs | 37 ++++++++++++------------- audio/src/lib.rs | 3 +- playback/src/audio_backend/alsa.rs | 8 ++---- playback/src/audio_backend/jackaudio.rs | 2 +- playback/src/audio_backend/mod.rs | 10 +++---- playback/src/audio_backend/portaudio.rs | 7 ++--- playback/src/audio_backend/rodio.rs | 4 +-- playback/src/audio_backend/sdl.rs | 6 ++-- playback/src/config.rs | 2 +- playback/src/mixer/alsamixer.rs | 7 ++--- playback/src/player.rs | 6 ++-- src/main.rs | 4 +-- 12 files changed, 44 insertions(+), 52 deletions(-) diff --git a/audio/src/convert.rs b/audio/src/convert.rs index e291c804..450910b0 100644 --- a/audio/src/convert.rs +++ b/audio/src/convert.rs @@ -36,24 +36,21 @@ macro_rules! convert_samples_to { }; } -pub struct SamplesConverter {} -impl SamplesConverter { - pub fn to_s32(samples: &[f32]) -> Vec { - convert_samples_to!(i32, samples) - } - - pub fn to_s24(samples: &[f32]) -> Vec { - convert_samples_to!(i32, samples, 8) - } - - pub fn to_s24_3(samples: &[f32]) -> Vec { - Self::to_s32(samples) - .iter() - .map(|sample| i24::pcm_from_i32(*sample)) - .collect() - } - - pub fn to_s16(samples: &[f32]) -> Vec { - convert_samples_to!(i16, samples) - } +pub fn to_s32(samples: &[f32]) -> Vec { + convert_samples_to!(i32, samples) +} + +pub fn to_s24(samples: &[f32]) -> Vec { + convert_samples_to!(i32, samples, 8) +} + +pub fn to_s24_3(samples: &[f32]) -> Vec { + to_s32(samples) + .iter() + .map(|sample| i24::pcm_from_i32(*sample)) + .collect() +} + +pub fn to_s16(samples: &[f32]) -> Vec { + convert_samples_to!(i16, samples) } diff --git a/audio/src/lib.rs b/audio/src/lib.rs index e0a4858c..b587f038 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -3,7 +3,7 @@ #[macro_use] extern crate log; -mod convert; +pub mod convert; mod decrypt; mod fetch; @@ -24,7 +24,6 @@ pub use passthrough_decoder::{PassthroughDecoder, PassthroughError}; mod range_set; -pub use convert::{i24, SamplesConverter}; pub use decrypt::AudioDecrypt; pub use fetch::{AudioFile, StreamLoaderController}; pub use fetch::{ diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 1d551878..54fed319 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -87,7 +87,7 @@ impl Open for AlsaSink { Self { pcm: None, - format: format, + format, device: name, buffer: vec![], } @@ -146,7 +146,7 @@ impl SinkAsBytes 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() { - self.write_buf().expect("could not append to buffer"); + self.write_buf(); self.buffer.clear(); } } @@ -156,14 +156,12 @@ impl SinkAsBytes for AlsaSink { } impl AlsaSink { - fn write_buf(&mut self) -> io::Result<()> { + fn write_buf(&mut self) { 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(()) } } diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index b38c9014..aca2edd2 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -47,7 +47,7 @@ impl Open for JackSink { } info!("Using JACK sink with format {:?}", AudioFormat::F32); - let client_name = client_name.unwrap_or("librespot".to_string()); + let client_name = client_name.unwrap_or_else(|| "librespot".to_string()); let (client, _status) = Client::new(&client_name[..], ClientOptions::NO_START_SERVER).unwrap(); let ch_r = client.register_port("out_0", AudioOut::default()).unwrap(); diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 24f5f855..72659f19 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -26,25 +26,25 @@ fn mk_sink(device: Option, format: AudioFormat macro_rules! sink_as_bytes { () => { fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - use crate::audio::{i24, SamplesConverter}; + use crate::audio::convert::{self, 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] = &SamplesConverter::to_s32(samples); + let samples_s32: &[i32] = &convert::to_s32(samples); self.write_bytes(samples_s32.as_bytes()) } AudioFormat::S24 => { - let samples_s24: &[i32] = &SamplesConverter::to_s24(samples); + let samples_s24: &[i32] = &convert::to_s24(samples); self.write_bytes(samples_s24.as_bytes()) } AudioFormat::S24_3 => { - let samples_s24_3: &[i24] = &SamplesConverter::to_s24_3(samples); + let samples_s24_3: &[i24] = &convert::to_s24_3(samples); self.write_bytes(samples_s24_3.as_bytes()) } AudioFormat::S16 => { - let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); + let samples_s16: &[i16] = &convert::to_s16(samples); self.write_bytes(samples_s16.as_bytes()) } }, diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 5faff6ca..234a9af6 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -1,8 +1,7 @@ use super::{Open, Sink}; -use crate::audio::{AudioPacket, SamplesConverter}; +use crate::audio::{convert, AudioPacket}; use crate::config::AudioFormat; use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; -use portaudio_rs; use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo}; use portaudio_rs::stream::*; use std::io; @@ -157,11 +156,11 @@ impl<'a> Sink for PortAudioSink<'a> { write_sink!(ref mut stream, samples) } Self::S32(stream, _parameters) => { - let samples_s32: &[i32] = &SamplesConverter::to_s32(samples); + let samples_s32: &[i32] = &convert::to_s32(samples); write_sink!(ref mut stream, samples_s32) } Self::S16(stream, _parameters) => { - let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); + let samples_s16: &[i16] = &convert::to_s16(samples); write_sink!(ref mut stream, samples_s16) } }; diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 035cd328..65436a32 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -5,7 +5,7 @@ use cpal::traits::{DeviceTrait, HostTrait}; use thiserror::Error; use super::Sink; -use crate::audio::{AudioPacket, SamplesConverter}; +use crate::audio::{convert, AudioPacket}; use crate::config::AudioFormat; use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; @@ -189,7 +189,7 @@ impl Sink for RodioSink { self.rodio_sink.append(source); } AudioFormat::S16 => { - let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); + let samples_s16: &[i16] = &convert::to_s16(samples); let source = rodio::buffer::SamplesBuffer::new( NUM_CHANNELS as u16, SAMPLE_RATE, diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 0a3fd433..29566533 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -1,5 +1,5 @@ use super::{Open, Sink}; -use crate::audio::{AudioPacket, SamplesConverter}; +use crate::audio::{convert, AudioPacket}; use crate::config::AudioFormat; use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use sdl2::audio::{AudioQueue, AudioSpecDesired}; @@ -97,12 +97,12 @@ impl Sink for SdlSink { queue.queue(samples) } Self::S32(queue) => { - let samples_s32: &[i32] = &SamplesConverter::to_s32(samples); + let samples_s32: &[i32] = &convert::to_s32(samples); drain_sink!(queue, AudioFormat::S32.size()); queue.queue(samples_s32) } Self::S16(queue) => { - let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); + let samples_s16: &[i16] = &convert::to_s16(samples); drain_sink!(queue, AudioFormat::S16.size()); queue.queue(samples_s16) } diff --git a/playback/src/config.rs b/playback/src/config.rs index 95c97092..f8f02893 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -1,4 +1,4 @@ -use crate::audio::i24; +use crate::audio::convert::i24; use std::convert::TryFrom; use std::mem; use std::str::FromStr; diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index d9dbe311..5e0a963f 100644 --- a/playback/src/mixer/alsamixer.rs +++ b/playback/src/mixer/alsamixer.rs @@ -33,13 +33,12 @@ impl AlsaMixer { let mixer = alsa::mixer::Mixer::new(&config.card, false)?; let sid = alsa::mixer::SelemId::new(&config.mixer, config.index); - let selem = mixer.find_selem(&sid).expect( - format!( + let selem = mixer.find_selem(&sid).unwrap_or_else(|| { + panic!( "Couldn't find simple mixer control for {},{}", &config.mixer, &config.index, ) - .as_str(), - ); + }); let (min, max) = selem.get_playback_volume_range(); let (min_db, max_db) = selem.get_playback_db_range(); let hw_mix = selem diff --git a/playback/src/player.rs b/playback/src/player.rs index f53abbfb..3f0778f9 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -206,11 +206,11 @@ pub struct NormalisationData { impl NormalisationData { pub fn db_to_ratio(db: f32) -> f32 { - return f32::powf(10.0, db / DB_VOLTAGE_RATIO); + f32::powf(10.0, db / DB_VOLTAGE_RATIO) } pub fn ratio_to_db(ratio: f32) -> f32 { - return ratio.log10() * DB_VOLTAGE_RATIO; + ratio.log10() * DB_VOLTAGE_RATIO } fn parse_from_file(mut file: T) -> io::Result { @@ -1161,7 +1161,7 @@ impl PlayerInternal { } if self.config.normalisation - && (normalisation_factor != 1.0 + && (f32::abs(normalisation_factor - 1.0) < f32::EPSILON || self.config.normalisation_method != NormalisationMethod::Basic) { for sample in data.iter_mut() { diff --git a/src/main.rs b/src/main.rs index 4abe4e5e..318e1757 100644 --- a/src/main.rs +++ b/src/main.rs @@ -322,7 +322,7 @@ fn setup(args: &[String]) -> Setup { .opt_str("format") .as_ref() .map(|format| AudioFormat::try_from(format).expect("Invalid output format")) - .unwrap_or(AudioFormat::default()); + .unwrap_or_default(); let device = matches.opt_str("device"); if device == Some("?".into()) { @@ -470,7 +470,7 @@ fn setup(args: &[String]) -> Setup { bitrate, gapless: !matches.opt_present("disable-gapless"), normalisation: matches.opt_present("enable-volume-normalisation"), - normalisation_method: normalisation_method, + normalisation_method, normalisation_type: gain_type, normalisation_pregain: matches .opt_str("normalisation-pregain") From 3e9aee1d46e9bc90d2b7c321a5f8d7c823c98788 Mon Sep 17 00:00:00 2001 From: johannesd3 Date: Sat, 10 Apr 2021 15:08:39 +0200 Subject: [PATCH 46/46] Renamed variable --- src/main.rs | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/main.rs b/src/main.rs index 318e1757..78e7e2f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -128,7 +128,7 @@ struct Setup { emit_sink_events: bool, } -fn setup(args: &[String]) -> Setup { +fn get_setup(args: &[String]) -> Setup { let mut opts = getopts::Options::new(); opts.optopt( "c", @@ -553,7 +553,7 @@ async fn main() { } let args: Vec = std::env::args().collect(); - let setupp = setup(&args); + let setup = get_setup(&args); let mut last_credentials = None; let mut spirc: Option = None; @@ -563,23 +563,23 @@ async fn main() { let mut discovery = None; let mut connecting: Pin>> = Box::pin(future::pending()); - if setupp.enable_discovery { - let config = setupp.connect_config.clone(); - let device_id = setupp.session_config.device_id.clone(); + if setup.enable_discovery { + let config = setup.connect_config.clone(); + let device_id = setup.session_config.device_id.clone(); discovery = Some( - librespot_connect::discovery::discovery(config, device_id, setupp.zeroconf_port) + librespot_connect::discovery::discovery(config, device_id, setup.zeroconf_port) .unwrap(), ); } - if let Some(credentials) = setupp.credentials { + if let Some(credentials) = setup.credentials { last_credentials = Some(credentials.clone()); connecting = Box::pin( Session::connect( - setupp.session_config.clone(), + setup.session_config.clone(), credentials, - setupp.cache.clone(), + setup.cache.clone(), ) .fuse(), ); @@ -602,9 +602,9 @@ async fn main() { } connecting = Box::pin(Session::connect( - setupp.session_config.clone(), + setup.session_config.clone(), credentials, - setupp.cache.clone(), + setup.cache.clone(), ).fuse()); }, None => { @@ -615,22 +615,22 @@ async fn main() { }, session = &mut connecting, if !connecting.is_terminated() => match session { Ok(session) => { - let mixer_config = setupp.mixer_config.clone(); - let mixer = (setupp.mixer)(Some(mixer_config)); - let player_config = setupp.player_config.clone(); - let connect_config = setupp.connect_config.clone(); + let mixer_config = setup.mixer_config.clone(); + let mixer = (setup.mixer)(Some(mixer_config)); + let player_config = setup.player_config.clone(); + let connect_config = setup.connect_config.clone(); let audio_filter = mixer.get_audio_filter(); - let format = setupp.format; - let backend = setupp.backend; - let device = setupp.device.clone(); + let format = setup.format; + let backend = setup.backend; + let device = setup.device.clone(); let (player, event_channel) = Player::new(player_config, session.clone(), audio_filter, move || { (backend)(device, format) }); - if setupp.emit_sink_events { - if let Some(player_event_program) = setupp.player_event_program.clone() { + if setup.emit_sink_events { + if let Some(player_event_program) = setup.player_event_program.clone() { player.set_sink_event_callback(Some(Box::new(move |sink_status| { match emit_sink_event(sink_status, &player_event_program) { Ok(e) if e.success() => (), @@ -676,16 +676,16 @@ async fn main() { auto_connect_times.push(Instant::now()); connecting = Box::pin(Session::connect( - setupp.session_config.clone(), + setup.session_config.clone(), credentials, - setupp.cache.clone(), + setup.cache.clone(), ).fuse()); } } }, event = async { player_event_channel.as_mut().unwrap().recv().await }, if player_event_channel.is_some() => match event { Some(event) => { - if let Some(program) = &setupp.player_event_program { + if let Some(program) = &setup.player_event_program { if let Some(child) = run_program_on_events(event, program) { let mut child = child.expect("program failed to start");