diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ee476558..2634915c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,7 @@ https://github.com/librespot-org/librespot - [playback] The passthrough decoder is now feature-gated (breaking) - [playback] `rodio`: call play and pause - [protocol] protobufs have been updated +- [playback] Moved audio processing with the exception of decoding out of `player` (breaking) ### Added @@ -102,6 +103,8 @@ https://github.com/librespot-org/librespot - [connect] Add `activate` and `load` functions to `Spirc`, allowing control over local connect sessions - [metadata] Add `Lyrics` - [discovery] Add discovery initialisation retries if within the 1st min of uptime +- [playback] Add `normaliser`, `resampler` and `sample_pipeline`. +- [playback] Add resampling support to 48kHz, 88.2kHz, and 96kHz. ### Fixed @@ -119,6 +122,7 @@ https://github.com/librespot-org/librespot - [playback] Handle seek, pause, and play commands while loading - [playback] Handle disabled normalisation correctly when using fixed volume - [metadata] Fix missing colon when converting named spotify IDs to URIs +- [playback] Better thread handling in `player`. ## [0.4.2] - 2022-07-29 diff --git a/Cargo.toml b/Cargo.toml index 1f380a1cb..a9666a1a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,12 +55,12 @@ futures-util = { version = "0.3", default_features = false } getopts = "0.2" hex = "0.4" log = "0.4" -rpassword = "7.0" +rpassword = "7.2" sha1 = "0.10" sysinfo = { version = "0.29", default-features = false } thiserror = "1.0" tokio = { version = "1", features = ["rt", "macros", "signal", "sync", "parking_lot", "process"] } -url = "2.2" +url = "2.4" [features] alsa-backend = ["librespot-playback/alsa-backend"] @@ -78,6 +78,12 @@ passthrough-decoder = ["librespot-playback/passthrough-decoder"] default = ["rodio-backend"] +[profile.release-dist-optimized] +inherits = "release" +panic = "abort" +codegen-units = 1 +lto = true + [package.metadata.deb] maintainer = "librespot-org" copyright = "2018 Paul Liétar" diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 1e02df84d..7665ffa4f 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -9,7 +9,7 @@ repository = "https://github.com/librespot-org/librespot" edition = "2021" [dependencies] -form_urlencoded = "1.0" +form_urlencoded = "1.2" futures-util = "0.3" log = "0.4" protobuf = "3" diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index be0a8b807..ed1e86131 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -848,6 +848,7 @@ impl SpircTask { // First see if this update was intended for us. let device_id = &self.ident; let ident = update.ident(); + if ident == device_id || (!update.recipient.is_empty() && !update.recipient.contains(device_id)) { @@ -880,7 +881,6 @@ impl SpircTask { match update.typ() { MessageType::kMessageTypeHello => self.notify(Some(ident)), - MessageType::kMessageTypeLoad => { self.handle_load(update.state.get_or_default())?; self.notify(None) diff --git a/core/Cargo.toml b/core/Cargo.toml index ff69eaae4..4583d91c7 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -19,13 +19,13 @@ base64 = "0.21" byteorder = "1.4" bytes = "1" dns-sd = { version = "0.1", optional = true } -form_urlencoded = "1.0" +form_urlencoded = "1.2" futures-core = "0.3" futures-util = { version = "0.3", features = ["alloc", "bilock", "sink", "unstable"] } governor = { version = "0.5", default-features = false, features = ["std", "jitter"] } hex = "0.4" hmac = "0.12" -httparse = "1.7" +httparse = "1.8" http = "0.2" hyper = { version = "0.14", features = ["client", "http1", "http2", "tcp"] } hyper-proxy = { version = "0.9", default-features = false, features = ["rustls"] } @@ -39,11 +39,11 @@ num-traits = "0.2" once_cell = "1" parking_lot = { version = "0.12", features = ["deadlock_detection"] } pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] } -priority-queue = "1.2" +priority-queue = "1.3" protobuf = "3" quick-xml = { version = "0.29", features = ["serialize"] } rand = "0.8" -rsa = "0.9.2" +rsa = "0.9" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha1 = { version = "0.10", features = ["oid"] } diff --git a/core/src/session.rs b/core/src/session.rs index 69125e17d..3151c6a3e 100755 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -258,8 +258,20 @@ impl Session { if session.is_invalid() { break; } - let last_ping = session.0.data.read().last_ping.unwrap_or_else(Instant::now); - if last_ping.elapsed() >= SESSION_TIMEOUT { + + // It would be so much easier to use elapsed but elapsed could + // potentially panic is rare cases. + // See: + // https://doc.rust-lang.org/std/time/struct.Instant.html#monotonicity + let now = Instant::now(); + + let last_ping = session.0.data.read().last_ping.unwrap_or(now); + + let since_last_ping = now + .checked_duration_since(last_ping) + .unwrap_or(SESSION_TIMEOUT); + + if since_last_ping >= SESSION_TIMEOUT { session.shutdown(); // TODO: Optionally reconnect (with cached/last credentials?) return Err(io::Error::new( diff --git a/core/src/spclient.rs b/core/src/spclient.rs index d6c5ffb1b..88130317c 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -114,8 +114,9 @@ impl SpClient { dst: &mut [u8], ) -> Result<(), Error> { // after a certain number of seconds, the challenge expires - const TIMEOUT: u64 = 5; // seconds - let now = Instant::now(); + const TIMEOUT: Duration = Duration::from_secs(5); + + let then = Instant::now(); let md = Sha1::digest(ctx); @@ -123,9 +124,18 @@ impl SpClient { let target: i64 = BigEndian::read_i64(&md[12..20]); let suffix = loop { - if now.elapsed().as_secs() >= TIMEOUT { + // It would be so much easier to use elapsed but elapsed could + // potentially panic is rare cases. + // See: + // https://doc.rust-lang.org/std/time/struct.Instant.html#monotonicity + if Instant::now() + .checked_duration_since(then) + .unwrap_or(TIMEOUT) + >= TIMEOUT + { return Err(Error::deadline_exceeded(format!( - "{TIMEOUT} seconds expired" + "{} seconds expired", + TIMEOUT.as_secs(), ))); } diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index b30ddd993..80377803e 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -14,7 +14,7 @@ base64 = "0.21" cfg-if = "1.0" ctr = "0.9" dns-sd = { version = "0.1.3", optional = true } -form_urlencoded = "1.0" +form_urlencoded = "1.2" futures-core = "0.3" futures-util = "0.3" hmac = "0.12" diff --git a/examples/play.rs b/examples/play.rs index 9e4e29afb..fb615ac2e 100644 --- a/examples/play.rs +++ b/examples/play.rs @@ -20,6 +20,7 @@ async fn main() { let session_config = SessionConfig::default(); let player_config = PlayerConfig::default(); let audio_format = AudioFormat::default(); + let sample_rate = player_config.sample_rate.as_u32(); let args: Vec<_> = env::args().collect(); if args.len() != 4 { @@ -41,7 +42,7 @@ async fn main() { } let player = Player::new(player_config, session, Box::new(NoOpVolume), move || { - backend(None, audio_format) + backend(None, audio_format, sample_rate) }); player.load(track, true, 0); diff --git a/examples/play_connect.rs b/examples/play_connect.rs index a61d3d674..dd9c6780c 100644 --- a/examples/play_connect.rs +++ b/examples/play_connect.rs @@ -26,6 +26,7 @@ async fn main() { let player_config = PlayerConfig::default(); let audio_format = AudioFormat::default(); let connect_config = ConnectConfig::default(); + let sample_rate = player_config.sample_rate.as_u32(); let mut args: Vec<_> = env::args().collect(); let context_uri = if args.len() == 4 { @@ -47,7 +48,7 @@ async fn main() { player_config, session.clone(), Box::new(NoOpVolume), - move || backend(None, audio_format), + move || backend(None, audio_format, sample_rate), ); let (spirc, spirc_task) = Spirc::new( diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 5ef5b4f7a..060e1686e 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -44,7 +44,7 @@ glib = { version = "0.17", optional = true } # Rodio dependencies rodio = { version = "0.17.1", optional = true, default-features = false } -cpal = { version = "0.15.1", optional = true } +cpal = { version = "0.15.2", optional = true } # Container and audio decoder symphonia = { version = "0.5", default-features = false, features = ["mp3", "ogg", "vorbis"] } diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index fada25807..3df47da25 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -1,42 +1,48 @@ use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult}; -use crate::config::AudioFormat; -use crate::convert::Converter; -use crate::decoder::AudioPacket; -use crate::{NUM_CHANNELS, SAMPLE_RATE}; -use alsa::device_name::HintIter; -use alsa::pcm::{Access, Format, Frames, HwParams, PCM}; -use alsa::{Direction, ValueOr}; + +use crate::{ + config::{AudioFormat, SampleRate}, + convert::Converter, + decoder::AudioPacket, + CommonSampleRates, NUM_CHANNELS, SAMPLE_RATE as DECODER_SAMPLE_RATE, +}; + +use alsa::{ + device_name::HintIter, + pcm::{Access, Format, Frames, HwParams, PCM}, + Direction, ValueOr, +}; + use std::process::exit; use thiserror::Error; -const MAX_BUFFER: Frames = (SAMPLE_RATE / 2) as Frames; -const MIN_BUFFER: Frames = (SAMPLE_RATE / 10) as Frames; -const ZERO_FRAMES: Frames = 0; - -const MAX_PERIOD_DIVISOR: Frames = 4; -const MIN_PERIOD_DIVISOR: Frames = 10; +const OPTIMAL_NUM_PERIODS: Frames = 5; +const MIN_NUM_PERIODS: Frames = 2; #[derive(Debug, Error)] enum AlsaError { - #[error(" Device {device} Unsupported Format {alsa_format:?} ({format:?}), {e}")] + #[error(" Device {device} Unsupported Format {alsa_format} ({format:?}), {e}, Supported Format(s): {supported_formats:?}")] UnsupportedFormat { device: String, alsa_format: Format, format: AudioFormat, + supported_formats: Vec, e: alsa::Error, }, - #[error(" Device {device} Unsupported Channel Count {channel_count}, {e}")] + #[error(" Device {device} Unsupported Channel Count {channel_count}, {e}, Supported Channel Count(s): {supported_channel_counts:?}")] UnsupportedChannelCount { device: String, channel_count: u8, + supported_channel_counts: Vec, e: alsa::Error, }, - #[error(" Device {device} Unsupported Sample Rate {samplerate}, {e}")] + #[error(" Device {device} Unsupported Sample Rate {samplerate}, {e}, Supported Sample Rate(s): {supported_rates:?}")] UnsupportedSampleRate { device: String, samplerate: u32, + supported_rates: Vec, e: alsa::Error, }, @@ -63,9 +69,6 @@ enum AlsaError { #[error(" Could Not Parse Output Name(s) and/or Description(s), {0}")] Parsing(alsa::Error), - - #[error("")] - NotConnected, } impl From for SinkError { @@ -73,9 +76,8 @@ impl From for SinkError { use AlsaError::*; let es = e.to_string(); match e { - DrainFailure(_) | OnWrite(_) => SinkError::OnWrite(es), + OnWrite(_) => SinkError::OnWrite(es), PcmSetUp { .. } => SinkError::ConnectionRefused(es), - NotConnected => SinkError::NotConnected(es), _ => SinkError::InvalidParams(es), } } @@ -98,6 +100,8 @@ impl From for Format { pub struct AlsaSink { pcm: Option, format: AudioFormat, + sample_rate: u32, + latency_scale_factor: f64, device: String, period_buffer: Vec, } @@ -106,302 +110,97 @@ fn list_compatible_devices() -> SinkResult<()> { let i = HintIter::new_str(None, "pcm").map_err(AlsaError::Parsing)?; println!("\n\n\tCompatible alsa device(s):\n"); - println!("\t------------------------------------------------------\n"); + println!("\t--------------------------------------------------------------------\n"); for a in i { if let Some(Direction::Playback) = a.direction { if let Some(name) = a.name { - if let Ok(pcm) = PCM::new(&name, Direction::Playback, false) { - if let Ok(hwp) = HwParams::any(&pcm) { - // Only show devices that support - // 2 ch 44.1 Interleaved. - - if hwp.set_access(Access::RWInterleaved).is_ok() - && hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest).is_ok() - && hwp.set_channels(NUM_CHANNELS as u32).is_ok() - { - let mut supported_formats = vec![]; - - for f in &[ - AudioFormat::S16, - AudioFormat::S24, - AudioFormat::S24_3, - AudioFormat::S32, - AudioFormat::F32, - AudioFormat::F64, - ] { - if hwp.test_format(Format::from(*f)).is_ok() { - supported_formats.push(format!("{f:?}")); + // surround* outputs throw: + // ALSA lib pcm_route.c:877:(find_matching_chmap) Found no matching channel map + if name.contains(':') && !name.starts_with("surround") { + if let Ok(pcm) = PCM::new(&name, Direction::Playback, false) { + if let Ok(hwp) = HwParams::any(&pcm) { + if hwp.set_access(Access::RWInterleaved).is_ok() + && hwp.set_channels(NUM_CHANNELS as u32).is_ok() + { + let mut supported_formats_and_samplerates = String::new(); + + for format in AudioFormat::default().into_iter() { + let hwp = hwp.clone(); + + if hwp.set_format(format.into()).is_ok() { + let sample_rates: Vec = SampleRate::default() + .into_iter() + .filter_map(|sample_rate| { + let hwp = hwp.clone(); + if hwp + .set_rate( + sample_rate.as_u32(), + ValueOr::Nearest, + ) + .is_ok() + { + Some(sample_rate.to_string()) + } else { + None + } + }) + .collect(); + + if !sample_rates.is_empty() { + let format_and_sample_rates = + if format == AudioFormat::S24_3 { + format!( + "\n\t\tFormat: {format:?} Sample Rate(s): {}", + sample_rates.join(", ") + ) + } else { + format!( + "\n\t\tFormat: {format:?} Sample Rate(s): {}", + sample_rates.join(", ") + ) + }; + + supported_formats_and_samplerates + .push_str(&format_and_sample_rates); + } + } } - } - - if !supported_formats.is_empty() { - println!("\tDevice:\n\n\t\t{name}\n"); - - println!( - "\tDescription:\n\n\t\t{}\n", - a.desc.unwrap_or_default().replace('\n', "\n\t\t") - ); - - println!( - "\tSupported Format(s):\n\n\t\t{}\n", - supported_formats.join(" ") - ); - - println!( - "\t------------------------------------------------------\n" - ); - } - } - }; - } - } - } - } - - Ok(()) -} - -fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> { - let pcm = PCM::new(dev_name, Direction::Playback, false).map_err(|e| AlsaError::PcmSetUp { - device: dev_name.to_string(), - e, - })?; - - let bytes_per_period = { - let hwp = HwParams::any(&pcm).map_err(AlsaError::HwParams)?; - - hwp.set_access(Access::RWInterleaved) - .map_err(|e| AlsaError::UnsupportedAccessType { - device: dev_name.to_string(), - e, - })?; - - let alsa_format = Format::from(format); - - hwp.set_format(alsa_format) - .map_err(|e| AlsaError::UnsupportedFormat { - device: dev_name.to_string(), - alsa_format, - format, - e, - })?; - - hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest).map_err(|e| { - AlsaError::UnsupportedSampleRate { - device: dev_name.to_string(), - samplerate: SAMPLE_RATE, - e, - } - })?; - - hwp.set_channels(NUM_CHANNELS as u32) - .map_err(|e| AlsaError::UnsupportedChannelCount { - device: dev_name.to_string(), - channel_count: NUM_CHANNELS, - e, - })?; - - // Clone the hwp while it's in - // a good working state so that - // in the event of an error setting - // the buffer and period sizes - // we can use the good working clone - // instead of the hwp that's in an - // error state. - let hwp_clone = hwp.clone(); - - // At a sampling rate of 44100: - // The largest buffer is 22050 Frames (500ms) with 5512 Frame periods (125ms). - // The smallest buffer is 4410 Frames (100ms) with 441 Frame periods (10ms). - // Actual values may vary. - // - // Larger buffer and period sizes are preferred as extremely small values - // will cause high CPU useage. - // - // If no buffer or period size is in those ranges or an error happens - // trying to set the buffer or period size use the device's defaults - // which may not be ideal but are *hopefully* serviceable. - - let buffer_size = { - let max = match hwp.get_buffer_size_max() { - Err(e) => { - trace!("Error getting the device's max Buffer size: {}", e); - ZERO_FRAMES - } - Ok(s) => s, - }; - let min = match hwp.get_buffer_size_min() { - Err(e) => { - trace!("Error getting the device's min Buffer size: {}", e); - ZERO_FRAMES - } - Ok(s) => s, - }; + if !supported_formats_and_samplerates.is_empty() { + println!("\tDevice:\n\n\t\t{name}\n"); - let buffer_size = if min < max { - match (MIN_BUFFER..=MAX_BUFFER) - .rev() - .find(|f| (min..=max).contains(f)) - { - Some(size) => { - trace!("Desired Frames per Buffer: {:?}", size); - - match hwp.set_buffer_size_near(size) { - Err(e) => { - trace!("Error setting the device's Buffer size: {}", e); - ZERO_FRAMES - } - Ok(s) => s, - } - } - None => { - trace!("No Desired Buffer size in range reported by the device."); - ZERO_FRAMES - } - } - } else { - trace!("The device's min reported Buffer size was greater than or equal to it's max reported Buffer size."); - ZERO_FRAMES - }; + println!( + "\tDescription:\n\n\t\t{}\n", + a.desc.unwrap_or_default().replace('\n', "\n\t\t") + ); - if buffer_size == ZERO_FRAMES { - trace!( - "Desired Buffer Frame range: {:?} - {:?}", - MIN_BUFFER, - MAX_BUFFER - ); - - trace!( - "Actual Buffer Frame range as reported by the device: {:?} - {:?}", - min, - max - ); - } + println!("\tSupported Format & Sample Rate Combinations:\n{supported_formats_and_samplerates}\n"); - buffer_size - }; - - let period_size = { - if buffer_size == ZERO_FRAMES { - ZERO_FRAMES - } else { - let max = match hwp.get_period_size_max() { - Err(e) => { - trace!("Error getting the device's max Period size: {}", e); - ZERO_FRAMES - } - Ok(s) => s, - }; - - let min = match hwp.get_period_size_min() { - Err(e) => { - trace!("Error getting the device's min Period size: {}", e); - ZERO_FRAMES - } - Ok(s) => s, - }; - - let max_period = buffer_size / MAX_PERIOD_DIVISOR; - let min_period = buffer_size / MIN_PERIOD_DIVISOR; - - let period_size = if min < max && min_period < max_period { - match (min_period..=max_period) - .rev() - .find(|f| (min..=max).contains(f)) - { - Some(size) => { - trace!("Desired Frames per Period: {:?}", size); - - match hwp.set_period_size_near(size, ValueOr::Nearest) { - Err(e) => { - trace!("Error setting the device's Period size: {}", e); - ZERO_FRAMES + println!( + "\t--------------------------------------------------------------------\n" + ); } - Ok(s) => s, } - } - None => { - trace!("No Desired Period size in range reported by the device."); - ZERO_FRAMES - } + }; } - } else { - trace!("The device's min reported Period size was greater than or equal to it's max reported Period size,"); - trace!("or the desired min Period size was greater than or equal to the desired max Period size."); - ZERO_FRAMES - }; - - if period_size == ZERO_FRAMES { - trace!("Buffer size: {:?}", buffer_size); - - trace!( - "Desired Period Frame range: {:?} (Buffer size / {:?}) - {:?} (Buffer size / {:?})", - min_period, - MIN_PERIOD_DIVISOR, - max_period, - MAX_PERIOD_DIVISOR, - ); - - trace!( - "Actual Period Frame range as reported by the device: {:?} - {:?}", - min, - max - ); } - - period_size } - }; - - if buffer_size == ZERO_FRAMES || period_size == ZERO_FRAMES { - trace!( - "Failed to set Buffer and/or Period size, falling back to the device's defaults." - ); - - trace!("You may experience higher than normal CPU usage and/or audio issues."); - - pcm.hw_params(&hwp_clone).map_err(AlsaError::Pcm)?; - } else { - pcm.hw_params(&hwp).map_err(AlsaError::Pcm)?; } + } - let hwp = pcm.hw_params_current().map_err(AlsaError::Pcm)?; - - // Don't assume we got what we wanted. Ask to make sure. - let frames_per_period = hwp.get_period_size().map_err(AlsaError::HwParams)?; - - let frames_per_buffer = hwp.get_buffer_size().map_err(AlsaError::HwParams)?; - - let swp = pcm.sw_params_current().map_err(AlsaError::Pcm)?; - - swp.set_start_threshold(frames_per_buffer - frames_per_period) - .map_err(AlsaError::SwParams)?; - - pcm.sw_params(&swp).map_err(AlsaError::Pcm)?; - - trace!("Actual Frames per Buffer: {:?}", frames_per_buffer); - trace!("Actual Frames per Period: {:?}", frames_per_period); - - // Let ALSA do the math for us. - pcm.frames_to_bytes(frames_per_period) as usize - }; - - trace!("Period Buffer size in bytes: {:?}", bytes_per_period); - - Ok((pcm, bytes_per_period)) + Ok(()) } impl Open for AlsaSink { - fn open(device: Option, format: AudioFormat) -> Self { + fn open(device: Option, format: AudioFormat, sample_rate: u32) -> Self { let name = match device.as_deref() { Some("?") => match list_compatible_devices() { Ok(_) => { exit(0); } Err(e) => { - error!("{}", e); + error!("{e}"); exit(1); } }, @@ -410,11 +209,20 @@ impl Open for AlsaSink { } .to_string(); - info!("Using AlsaSink with format: {:?}", format); + let latency_scale_factor = DECODER_SAMPLE_RATE as f64 / sample_rate as f64; + + info!( + "Using AlsaSink with format: {format:?}, sample rate: {}", + CommonSampleRates::try_from(sample_rate) + .unwrap_or_default() + .to_string() + ); Self { pcm: None, format, + sample_rate, + latency_scale_factor, device: name, period_buffer: vec![], } @@ -424,38 +232,45 @@ impl Open for AlsaSink { impl Sink for AlsaSink { fn start(&mut self) -> SinkResult<()> { if self.pcm.is_none() { - let (pcm, bytes_per_period) = open_device(&self.device, self.format)?; - self.pcm = Some(pcm); - - if self.period_buffer.capacity() != bytes_per_period { - self.period_buffer = Vec::with_capacity(bytes_per_period); - } - - // Should always match the "Period Buffer size in bytes: " trace! message. - trace!( - "Period Buffer capacity: {:?}", - self.period_buffer.capacity() - ); + self.open_device()?; } Ok(()) } fn stop(&mut self) -> SinkResult<()> { - if self.pcm.is_some() { - // Zero fill the remainder of the period buffer and - // write any leftover data before draining the actual PCM buffer. - self.period_buffer.resize(self.period_buffer.capacity(), 0); - self.write_buf()?; - - let pcm = self.pcm.take().ok_or(AlsaError::NotConnected)?; + // Zero fill the remainder of the period buffer and + // write any leftover data before draining the actual PCM buffer. + self.period_buffer.resize(self.period_buffer.capacity(), 0); + self.write_buf()?; + if let Some(pcm) = self.pcm.take() { pcm.drain().map_err(AlsaError::DrainFailure)?; } Ok(()) } + fn get_latency_pcm(&mut self) -> u64 { + let buffer_len = self.period_buffer.len(); + let latency_scale_factor = self.latency_scale_factor; + + self.pcm + .as_mut() + .and_then(|pcm| { + pcm.status().ok().map(|status| { + let delay_frames = status.get_delay(); + + let frames_in_buffer = pcm.bytes_to_frames(buffer_len as isize); + + let total_frames = (delay_frames + frames_in_buffer) as f64; + + (total_frames * latency_scale_factor) as u64 + }) + }) + .unwrap_or(0) + } + sink_as_bytes!(); } @@ -490,33 +305,215 @@ impl SinkAsBytes for AlsaSink { impl AlsaSink { pub const NAME: &'static str = "alsa"; - fn write_buf(&mut self) -> SinkResult<()> { - if self.pcm.is_some() { - let write_result = { - let pcm = self.pcm.as_mut().ok_or(AlsaError::NotConnected)?; - - match pcm.io_bytes().writei(&self.period_buffer) { - Ok(_) => Ok(()), - Err(e) => { - // Capture and log the original error as a warning, and then try to recover. - // If recovery fails then forward that error back to player. - warn!( - "Error writing from AlsaSink buffer to PCM, trying to recover, {}", - e - ); - - pcm.try_recover(e, false).map_err(AlsaError::OnWrite) + fn set_period_and_buffer_size( + hwp: &HwParams, + optimal_buffer_size: Frames, + optimal_period_size: Frames, + ) -> bool { + let period_size = match hwp.set_period_size_near(optimal_period_size, ValueOr::Nearest) { + Ok(period_size) => { + if period_size > 0 { + trace!("Closest Supported Period Size to Optimal ({optimal_period_size}): {period_size}"); + period_size + } else { + trace!("Error getting Period Size, Period Size must be greater than 0, falling back to the device's default Buffer parameters"); + 0 + } + } + Err(e) => { + trace!("Error getting Period Size: {e}, falling back to the device's default Buffer parameters"); + 0 + } + }; + + if period_size > 0 { + let buffer_size = match hwp + .set_buffer_size_near((period_size * OPTIMAL_NUM_PERIODS).max(optimal_buffer_size)) + { + Ok(buffer_size) => { + if buffer_size >= period_size * MIN_NUM_PERIODS { + trace!("Closest Supported Buffer Size to Optimal ({optimal_buffer_size}): {buffer_size}"); + buffer_size + } else { + trace!("Error getting Buffer Size, Buffer Size must be at least {period_size} * {MIN_NUM_PERIODS}, falling back to the device's default Buffer parameters"); + 0 } } + Err(e) => { + trace!("Error getting Buffer Size: {e}, falling back to the device's default Buffer parameters"); + 0 + } }; - if let Err(e) = write_result { - self.pcm = None; - return Err(e.into()); + return buffer_size > 0; + } + + false + } + + fn open_device(&mut self) -> SinkResult<()> { + let optimal_buffer_size: Frames = self.sample_rate as Frames / 2; + let optimal_period_size: Frames = self.sample_rate as Frames / 10; + + let pcm = PCM::new(&self.device, Direction::Playback, false).map_err(|e| { + AlsaError::PcmSetUp { + device: self.device.clone(), + e, + } + })?; + + { + let hwp = HwParams::any(&pcm).map_err(AlsaError::HwParams)?; + + hwp.set_access(Access::RWInterleaved).map_err(|e| { + AlsaError::UnsupportedAccessType { + device: self.device.clone(), + e, + } + })?; + + let alsa_format = self.format.into(); + + hwp.set_format(alsa_format).map_err(|e| { + let supported_formats = AudioFormat::default() + .into_iter() + .filter_map(|f| { + if hwp.test_format(f.into()).is_ok() { + Some(format!("{f:?}")) + } else { + None + } + }) + .collect(); + + AlsaError::UnsupportedFormat { + device: self.device.clone(), + alsa_format, + format: self.format, + supported_formats, + e, + } + })?; + + hwp.set_rate(self.sample_rate, ValueOr::Nearest) + .map_err(|e| { + let common_sample_rates = CommonSampleRates::default(); + + let supported_rates = (hwp.get_rate_min().unwrap_or_default() + ..=hwp.get_rate_max().unwrap_or_default()) + .filter_map(|r| { + if common_sample_rates.contains(r) && hwp.test_rate(r).is_ok() { + Some( + CommonSampleRates::try_from(r) + .unwrap_or_default() + .to_string(), + ) + } else { + None + } + }) + .collect(); + + AlsaError::UnsupportedSampleRate { + device: self.device.clone(), + samplerate: self.sample_rate, + supported_rates, + e, + } + })?; + + hwp.set_channels(NUM_CHANNELS as u32).map_err(|e| { + let supported_channel_counts = (hwp.get_channels_min().unwrap_or_default() + ..=hwp.get_channels_max().unwrap_or_default()) + .filter(|c| hwp.test_channels(*c).is_ok()) + .collect(); + + AlsaError::UnsupportedChannelCount { + device: self.device.clone(), + channel_count: NUM_CHANNELS, + supported_channel_counts, + e, + } + })?; + + // Calculate a buffer and period size as close + // to optimal as possible. + + // hwp continuity is very important. + let hwp_clone = hwp.clone(); + + if Self::set_period_and_buffer_size( + &hwp_clone, + optimal_buffer_size, + optimal_period_size, + ) { + pcm.hw_params(&hwp_clone).map_err(AlsaError::Pcm)?; + } else { + pcm.hw_params(&hwp).map_err(AlsaError::Pcm)?; + } + + let hwp = pcm.hw_params_current().map_err(AlsaError::Pcm)?; + + // Don't assume we got what we wanted. Ask to make sure. + let buffer_size = hwp.get_buffer_size().map_err(AlsaError::HwParams)?; + + let period_size = hwp.get_period_size().map_err(AlsaError::HwParams)?; + + let swp = pcm.sw_params_current().map_err(AlsaError::Pcm)?; + + swp.set_start_threshold(buffer_size - period_size) + .map_err(AlsaError::SwParams)?; + + pcm.sw_params(&swp).map_err(AlsaError::Pcm)?; + + if buffer_size != optimal_buffer_size { + trace!("A Buffer Size of {buffer_size} Frames is Suboptimal"); + + if buffer_size < optimal_buffer_size { + trace!("A smaller than necessary Buffer Size can lead to Buffer underruns (audio glitches) and high CPU usage."); + } else { + trace!("A larger than necessary Buffer Size can lead to perceivable latency (lag)."); + } + } + + let optimal_period_size = buffer_size / OPTIMAL_NUM_PERIODS; + + if period_size != optimal_period_size { + trace!("A Period Size of {period_size} Frames is Suboptimal"); + + if period_size < optimal_period_size { + trace!("A smaller than necessary Period Size relative to Buffer Size can lead to high CPU usage."); + } else { + trace!("A larger than necessary Period Size relative to Buffer Size can lessen Buffer underrun (audio glitch) protection."); + } + } + + // Let ALSA do the math for us. + let bytes_per_period = pcm.frames_to_bytes(period_size) as usize; + + trace!("Period Buffer size in bytes: {bytes_per_period}"); + + self.period_buffer = Vec::with_capacity(bytes_per_period); + } + + self.pcm = Some(pcm); + + Ok(()) + } + + fn write_buf(&mut self) -> SinkResult<()> { + if let Some(pcm) = self.pcm.as_mut() { + if let Err(e) = pcm.io_bytes().writei(&self.period_buffer) { + // Capture and log the original error as a warning, and then try to recover. + // If recovery fails then forward that error back to player. + warn!("Error writing from AlsaSink Buffer to PCM, trying to recover, {e}"); + + pcm.try_recover(e, false).map_err(AlsaError::OnWrite)?; } } self.period_buffer.clear(); + Ok(()) } } diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index e3cc78cf4..f70ee37f0 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -14,7 +14,7 @@ use std::sync::Arc; use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult}; use crate::{ - config::AudioFormat, convert::Converter, decoder::AudioPacket, NUM_CHANNELS, SAMPLE_RATE, + config::AudioFormat, convert::Converter, decoder::AudioPacket, CommonSampleRates, NUM_CHANNELS, }; pub struct GstreamerSink { @@ -26,8 +26,14 @@ pub struct GstreamerSink { } impl Open for GstreamerSink { - fn open(device: Option, format: AudioFormat) -> Self { - info!("Using GStreamer sink with format: {format:?}"); + fn open(device: Option, format: AudioFormat, sample_rate: u32) -> Self { + info!( + "Using GstreamerSink with format: {format:?}, sample rate: {}", + CommonSampleRates::try_from(sample_rate) + .unwrap_or_default() + .to_string() + ); + gst::init().expect("failed to init GStreamer!"); let gst_format = match format { @@ -39,7 +45,7 @@ impl Open for GstreamerSink { AudioFormat::S16 => gst_audio::AUDIO_FORMAT_S16, }; - let gst_info = gst_audio::AudioInfo::builder(gst_format, SAMPLE_RATE, NUM_CHANNELS as u32) + let gst_info = gst_audio::AudioInfo::builder(gst_format, sample_rate, NUM_CHANNELS as u32) .build() .expect("Failed to create GStreamer audio format"); let gst_caps = gst_info.to_caps().expect("Failed to create GStreamer caps"); diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 5c7a28fbc..b38f4612d 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -1,11 +1,13 @@ use super::{Open, Sink, SinkError, SinkResult}; -use crate::config::AudioFormat; -use crate::convert::Converter; -use crate::decoder::AudioPacket; -use crate::NUM_CHANNELS; + +use crate::{ + config::AudioFormat, convert::Converter, decoder::AudioPacket, CommonSampleRates, NUM_CHANNELS, +}; + use jack::{ AsyncClient, AudioOut, Client, ClientOptions, Control, Port, ProcessHandler, ProcessScope, }; + use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; pub struct JackSink { @@ -38,11 +40,17 @@ impl ProcessHandler for JackData { } impl Open for JackSink { - fn open(client_name: Option, format: AudioFormat) -> Self { + fn open(client_name: Option, format: AudioFormat, sample_rate: u32) -> Self { if format != AudioFormat::F32 { warn!("JACK currently does not support {format:?} output"); } - info!("Using JACK sink with format {:?}", AudioFormat::F32); + + info!( + "Using JackSink with format: {format:?}, sample rate: {}", + CommonSampleRates::try_from(sample_rate) + .unwrap_or_default() + .to_string() + ); let client_name = client_name.unwrap_or_else(|| "librespot".to_string()); let (client, _status) = diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 058223956..fc39162fa 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -20,7 +20,7 @@ pub enum SinkError { pub type SinkResult = Result; pub trait Open { - fn open(_: Option, format: AudioFormat) -> Self; + fn open(_: Option, format: AudioFormat, sample_rate: u32) -> Self; } pub trait Sink { @@ -30,17 +30,24 @@ pub trait Sink { fn stop(&mut self) -> SinkResult<()> { Ok(()) } + fn get_latency_pcm(&mut self) -> u64 { + 0 + } fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()>; } -pub type SinkBuilder = fn(Option, AudioFormat) -> Box; +pub type SinkBuilder = fn(Option, AudioFormat, u32) -> Box; pub trait SinkAsBytes { fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()>; } -fn mk_sink(device: Option, format: AudioFormat) -> Box { - Box::new(S::open(device, format)) +fn mk_sink( + device: Option, + format: AudioFormat, + sample_rate: u32, +) -> Box { + Box::new(S::open(device, format, sample_rate)) } // reuse code for various backends diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index e0e8a77c3..6c0751e73 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -1,11 +1,12 @@ use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult}; -use crate::config::AudioFormat; -use crate::convert::Converter; -use crate::decoder::AudioPacket; +use crate::{config::AudioFormat, convert::Converter, decoder::AudioPacket, CommonSampleRates}; + +use std::{ + fs::OpenOptions, + io::{self, Write}, + process::exit, +}; -use std::fs::OpenOptions; -use std::io::{self, Write}; -use std::process::exit; use thiserror::Error; #[derive(Debug, Error)] @@ -42,13 +43,18 @@ pub struct StdoutSink { } impl Open for StdoutSink { - fn open(file: Option, format: AudioFormat) -> Self { + fn open(file: Option, format: AudioFormat, sample_rate: u32) -> Self { if let Some("?") = file.as_deref() { println!("\nUsage:\n\nOutput to stdout:\n\n\t--backend pipe\n\nOutput to file:\n\n\t--backend pipe --device {{filename}}\n"); exit(0); } - info!("Using StdoutSink (pipe) with format: {:?}", format); + info!( + "Using StdoutSink with format: {format:?}, sample rate: {}", + CommonSampleRates::try_from(sample_rate) + .unwrap_or_default() + .to_string() + ); Self { output: None, diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index c44245cfe..2868926be 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -1,25 +1,31 @@ use super::{Open, Sink, SinkError, SinkResult}; -use crate::config::AudioFormat; -use crate::convert::Converter; -use crate::decoder::AudioPacket; -use crate::{NUM_CHANNELS, SAMPLE_RATE}; -use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo}; -use portaudio_rs::stream::*; -use std::process::exit; -use std::time::Duration; + +use crate::{ + config::AudioFormat, convert::Converter, decoder::AudioPacket, CommonSampleRates, NUM_CHANNELS, +}; + +use portaudio_rs::{ + device::{get_default_output_index, DeviceIndex, DeviceInfo}, + stream::*, +}; + +use std::{process::exit, time::Duration}; pub enum PortAudioSink<'a> { F32( Option>, StreamParameters, + f64, ), S32( Option>, StreamParameters, + f64, ), S16( Option>, StreamParameters, + f64, ), } @@ -51,8 +57,13 @@ fn find_output(device: &str) -> Option { } impl<'a> Open for PortAudioSink<'a> { - fn open(device: Option, format: AudioFormat) -> PortAudioSink<'a> { - info!("Using PortAudio sink with format: {format:?}"); + fn open(device: Option, format: AudioFormat, sample_rate: u32) -> PortAudioSink<'a> { + info!( + "Using PortAudioSink with format: {format:?}, sample rate: {}", + CommonSampleRates::try_from(sample_rate) + .unwrap_or_default() + .to_string() + ); portaudio_rs::initialize().unwrap(); @@ -73,20 +84,23 @@ impl<'a> Open for PortAudioSink<'a> { }; macro_rules! open_sink { - ($sink: expr, $type: ty) => {{ + ($sink: expr, $type: ty, $sample_rate: ident) => {{ let params = StreamParameters { device: device_idx, channel_count: NUM_CHANNELS as u32, suggested_latency: latency, data: 0.0 as $type, }; - $sink(None, params) + $sink(None, params, $sample_rate) }}; } + + let sample_rate = sample_rate as f64; + match format { - AudioFormat::F32 => open_sink!(Self::F32, f32), - AudioFormat::S32 => open_sink!(Self::S32, i32), - AudioFormat::S16 => open_sink!(Self::S16, i16), + AudioFormat::F32 => open_sink!(Self::F32, f32, sample_rate), + AudioFormat::S32 => open_sink!(Self::S32, i32, sample_rate), + AudioFormat::S16 => open_sink!(Self::S16, i16, sample_rate), _ => { unimplemented!("PortAudio currently does not support {format:?} output") } @@ -97,13 +111,13 @@ impl<'a> Open for PortAudioSink<'a> { impl<'a> Sink for PortAudioSink<'a> { fn start(&mut self) -> SinkResult<()> { macro_rules! start_sink { - (ref mut $stream: ident, ref $parameters: ident) => {{ + (ref mut $stream: ident, ref $parameters: ident, ref $sample_rate: ident ) => {{ if $stream.is_none() { *$stream = Some( Stream::open( None, Some(*$parameters), - SAMPLE_RATE as f64, + *$sample_rate, FRAMES_PER_BUFFER_UNSPECIFIED, StreamFlags::DITHER_OFF, // no need to dither twice; use librespot dithering instead None, @@ -116,9 +130,15 @@ impl<'a> Sink for PortAudioSink<'a> { } match self { - 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), + Self::F32(stream, parameters, sample_rate) => { + start_sink!(ref mut stream, ref parameters, ref sample_rate) + } + Self::S32(stream, parameters, sample_rate) => { + start_sink!(ref mut stream, ref parameters, ref sample_rate) + } + Self::S16(stream, parameters, sample_rate) => { + start_sink!(ref mut stream, ref parameters, ref sample_rate) + } }; Ok(()) @@ -132,9 +152,9 @@ impl<'a> Sink for PortAudioSink<'a> { }}; } match self { - Self::F32(stream, _) => stop_sink!(ref mut stream), - Self::S32(stream, _) => stop_sink!(ref mut stream), - Self::S16(stream, _) => stop_sink!(ref mut stream), + Self::F32(stream, _, _) => stop_sink!(ref mut stream), + Self::S32(stream, _, _) => stop_sink!(ref mut stream), + Self::S16(stream, _, _) => stop_sink!(ref mut stream), }; Ok(()) @@ -152,15 +172,15 @@ impl<'a> Sink for PortAudioSink<'a> { .map_err(|e| SinkError::OnWrite(e.to_string()))?; let result = match self { - Self::F32(stream, _parameters) => { + Self::F32(stream, _parameters, _sample_rate) => { let samples_f32: &[f32] = &converter.f64_to_f32(samples); write_sink!(ref mut stream, samples_f32) } - Self::S32(stream, _parameters) => { + Self::S32(stream, _parameters, _sample_rate) => { let samples_s32: &[i32] = &converter.f64_to_s32(samples); write_sink!(ref mut stream, samples_s32) } - Self::S16(stream, _parameters) => { + Self::S16(stream, _parameters, _sample_rate) => { let samples_s16: &[i16] = &converter.f64_to_s16(samples); write_sink!(ref mut stream, samples_s16) } diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 43d7ec079..cae97d1b3 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -1,8 +1,10 @@ use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult}; -use crate::config::AudioFormat; -use crate::convert::Converter; -use crate::decoder::AudioPacket; -use crate::{NUM_CHANNELS, SAMPLE_RATE}; + +use crate::{ + config::AudioFormat, convert::Converter, decoder::AudioPacket, CommonSampleRates, NUM_CHANNELS, + SAMPLE_RATE as DECODER_SAMPLE_RATE, +}; + use libpulse_binding::{self as pulse, error::PAErr, stream::Direction}; use libpulse_simple_binding::Simple; use std::env; @@ -24,9 +26,6 @@ enum PulseError { #[error(" Failed to Drain Pulseaudio Buffer, {0}")] DrainFailure(PAErr), - #[error("")] - NotConnected, - #[error(" {0}")] OnWrite(PAErr), } @@ -38,40 +37,68 @@ impl From for SinkError { match e { DrainFailure(_) | OnWrite(_) => SinkError::OnWrite(es), ConnectionRefused(_) => SinkError::ConnectionRefused(es), - NotConnected => SinkError::NotConnected(es), InvalidSampleSpec { .. } => SinkError::InvalidParams(es), } } } +impl From for pulse::sample::Format { + fn from(f: AudioFormat) -> pulse::sample::Format { + use AudioFormat::*; + match f { + F64 | F32 => pulse::sample::Format::FLOAT32NE, + S32 => pulse::sample::Format::S32NE, + S24 => pulse::sample::Format::S24_32NE, + S24_3 => pulse::sample::Format::S24NE, + S16 => pulse::sample::Format::S16NE, + } + } +} + pub struct PulseAudioSink { sink: Option, device: Option, app_name: String, stream_desc: String, format: AudioFormat, + sample_rate: u32, + + sample_spec: pulse::sample::Spec, } impl Open for PulseAudioSink { - fn open(device: Option, format: AudioFormat) -> Self { + fn open(device: Option, format: AudioFormat, sample_rate: u32) -> Self { let app_name = env::var("PULSE_PROP_application.name").unwrap_or_default(); let stream_desc = env::var("PULSE_PROP_stream.description").unwrap_or_default(); - let mut actual_format = format; - - if actual_format == AudioFormat::F64 { + let format = if format == AudioFormat::F64 { warn!("PulseAudio currently does not support F64 output"); - actual_format = AudioFormat::F32; - } - - info!("Using PulseAudioSink with format: {actual_format:?}"); + AudioFormat::F32 + } else { + format + }; + + info!( + "Using PulseAudioSink with format: {format:?}, sample rate: {}", + CommonSampleRates::try_from(sample_rate) + .unwrap_or_default() + .to_string() + ); + + let sample_spec = pulse::sample::Spec { + format: format.into(), + channels: NUM_CHANNELS, + rate: sample_rate, + }; Self { sink: None, device, app_name, stream_desc, - format: actual_format, + format, + sample_rate, + sample_spec, } } } @@ -79,31 +106,15 @@ impl Open for PulseAudioSink { impl Sink for PulseAudioSink { fn start(&mut self) -> SinkResult<()> { if self.sink.is_none() { - // PulseAudio calls S24 and S24_3 different from the rest of the world - let pulse_format = match self.format { - AudioFormat::F32 => pulse::sample::Format::FLOAT32NE, - AudioFormat::S32 => pulse::sample::Format::S32NE, - AudioFormat::S24 => pulse::sample::Format::S24_32NE, - AudioFormat::S24_3 => pulse::sample::Format::S24NE, - AudioFormat::S16 => pulse::sample::Format::S16NE, - _ => unreachable!(), - }; - - let sample_spec = pulse::sample::Spec { - format: pulse_format, - channels: NUM_CHANNELS, - rate: SAMPLE_RATE, - }; - - if !sample_spec.is_valid() { + if !self.sample_spec.is_valid() { let pulse_error = PulseError::InvalidSampleSpec { - pulse_format, + pulse_format: self.sample_spec.format, format: self.format, channels: NUM_CHANNELS, - rate: SAMPLE_RATE, + rate: self.sample_rate, }; - return Err(SinkError::from(pulse_error)); + return Err(pulse_error.into()); } let sink = Simple::new( @@ -112,7 +123,7 @@ impl Sink for PulseAudioSink { Direction::Playback, // Direction. self.device.as_deref(), // Our device (sink) name. &self.stream_desc, // Description of our stream. - &sample_spec, // Our sample format. + &self.sample_spec, // Our sample format. None, // Use default channel map. None, // Use default buffering attributes. ) @@ -125,20 +136,32 @@ impl Sink for PulseAudioSink { } fn stop(&mut self) -> SinkResult<()> { - let sink = self.sink.take().ok_or(PulseError::NotConnected)?; + if let Some(sink) = self.sink.take() { + sink.flush().map_err(PulseError::DrainFailure)?; + } - sink.drain().map_err(PulseError::DrainFailure)?; Ok(()) } + fn get_latency_pcm(&mut self) -> u64 { + self.sink + .as_mut() + .and_then(|sink| { + sink.get_latency() + .ok() + .map(|micro_sec| (micro_sec.as_secs_f64() * DECODER_SAMPLE_RATE as f64) as u64) + }) + .unwrap_or(0) + } + sink_as_bytes!(); } impl SinkAsBytes for PulseAudioSink { fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { - let sink = self.sink.as_mut().ok_or(PulseError::NotConnected)?; - - sink.write(data).map_err(PulseError::OnWrite)?; + if let Some(sink) = self.sink.as_mut() { + sink.write(data).map_err(PulseError::OnWrite)?; + } Ok(()) } diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 2632f54a5..956014a4a 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -1,15 +1,13 @@ -use std::process::exit; -use std::thread; -use std::time::Duration; +use std::{process::exit, thread, time::Duration}; use cpal::traits::{DeviceTrait, HostTrait}; use thiserror::Error; use super::{Sink, SinkError, SinkResult}; -use crate::config::AudioFormat; -use crate::convert::Converter; -use crate::decoder::AudioPacket; -use crate::{NUM_CHANNELS, SAMPLE_RATE}; + +use crate::{ + config::AudioFormat, convert::Converter, decoder::AudioPacket, CommonSampleRates, NUM_CHANNELS, +}; #[cfg(all( feature = "rodiojack-backend", @@ -18,16 +16,21 @@ use crate::{NUM_CHANNELS, SAMPLE_RATE}; compile_error!("Rodio JACK backend is currently only supported on linux."); #[cfg(feature = "rodio-backend")] -pub fn mk_rodio(device: Option, format: AudioFormat) -> Box { - Box::new(open(cpal::default_host(), device, format)) +pub fn mk_rodio(device: Option, format: AudioFormat, sample_rate: u32) -> Box { + Box::new(open(cpal::default_host(), device, format, sample_rate)) } #[cfg(feature = "rodiojack-backend")] -pub fn mk_rodiojack(device: Option, format: AudioFormat) -> Box { +pub fn mk_rodiojack( + device: Option, + format: AudioFormat, + sample_rate: u32, +) -> Box { Box::new(open( cpal::host_from_id(cpal::HostId::Jack).unwrap(), device, format, + sample_rate, )) } @@ -62,6 +65,7 @@ impl From for SinkError { pub struct RodioSink { rodio_sink: rodio::Sink, format: AudioFormat, + sample_rate: u32, _stream: rodio::OutputStream, } @@ -164,22 +168,31 @@ fn create_sink( Ok((sink, stream)) } -pub fn open(host: cpal::Host, device: Option, format: AudioFormat) -> RodioSink { - info!( - "Using Rodio sink with format {format:?} and cpal host: {}", - host.id().name() - ); - +pub fn open( + host: cpal::Host, + device: Option, + format: AudioFormat, + sample_rate: u32, +) -> RodioSink { if format != AudioFormat::S16 && format != AudioFormat::F32 { unimplemented!("Rodio currently only supports F32 and S16 formats"); } + info!( + "Using RodioSink with format: {format:?}, sample rate: {}, and cpal host: {}", + CommonSampleRates::try_from(sample_rate) + .unwrap_or_default() + .to_string(), + host.id().name(), + ); + let (sink, stream) = create_sink(&host, device).unwrap(); debug!("Rodio sink was created"); RodioSink { rodio_sink: sink, format, + sample_rate, _stream: stream, } } @@ -205,7 +218,7 @@ impl Sink for RodioSink { let samples_f32: &[f32] = &converter.f64_to_f32(samples); let source = rodio::buffer::SamplesBuffer::new( NUM_CHANNELS as u16, - SAMPLE_RATE, + self.sample_rate, samples_f32, ); self.rodio_sink.append(source); @@ -214,7 +227,7 @@ impl Sink for RodioSink { let samples_s16: &[i16] = &converter.f64_to_s16(samples); let source = rodio::buffer::SamplesBuffer::new( NUM_CHANNELS as u16, - SAMPLE_RATE, + self.sample_rate, samples_s16, ); self.rodio_sink.append(source); diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 0d2209282..a3856a256 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -1,11 +1,12 @@ use super::{Open, Sink, SinkError, SinkResult}; -use crate::config::AudioFormat; -use crate::convert::Converter; -use crate::decoder::AudioPacket; -use crate::{NUM_CHANNELS, SAMPLE_RATE}; + +use crate::{ + config::AudioFormat, convert::Converter, decoder::AudioPacket, CommonSampleRates, NUM_CHANNELS, + SAMPLE_RATE, +}; + use sdl2::audio::{AudioQueue, AudioSpecDesired}; -use std::thread; -use std::time::Duration; +use std::{thread, time::Duration}; pub enum SdlSink { F32(AudioQueue), @@ -14,8 +15,13 @@ pub enum SdlSink { } impl Open for SdlSink { - fn open(device: Option, format: AudioFormat) -> Self { - info!("Using SDL sink with format: {:?}", format); + fn open(device: Option, format: AudioFormat, sample_rate: u32) -> Self { + info!( + "Using SdlSink with format: {format:?}, sample rate: {}", + CommonSampleRates::try_from(sample_rate) + .unwrap_or_default() + .to_string() + ); if device.is_some() { warn!("SDL sink does not support specifying a device name"); @@ -27,7 +33,7 @@ impl Open for SdlSink { .expect("could not initialize SDL audio subsystem"); let desired_spec = AudioSpecDesired { - freq: Some(SAMPLE_RATE as i32), + freq: Some(sample_rate as i32), channels: Some(NUM_CHANNELS), samples: None, }; diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index 6ce545dac..21f9a291b 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -1,11 +1,13 @@ use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult}; -use crate::config::AudioFormat; -use crate::convert::Converter; -use crate::decoder::AudioPacket; -use shell_words::split; -use std::io::{ErrorKind, Write}; -use std::process::{exit, Child, Command, Stdio}; +use crate::{config::AudioFormat, convert::Converter, decoder::AudioPacket, CommonSampleRates}; + +use std::{ + io::{ErrorKind, Write}, + process::{exit, Child, Command, Stdio}, +}; + +use shell_words::split; use thiserror::Error; #[derive(Debug, Error)] @@ -66,13 +68,18 @@ pub struct SubprocessSink { } impl Open for SubprocessSink { - fn open(shell_command: Option, format: AudioFormat) -> Self { + fn open(shell_command: Option, format: AudioFormat, sample_rate: u32) -> Self { if let Some("?") = shell_command.as_deref() { println!("\nUsage:\n\nOutput to a Subprocess:\n\n\t--backend subprocess --device {{shell_command}}\n"); exit(0); } - info!("Using SubprocessSink with format: {:?}", format); + info!( + "Using SubprocessSink with format: {format:?}, sample rate: {}", + CommonSampleRates::try_from(sample_rate) + .unwrap_or_default() + .to_string() + ); Self { shell_command, diff --git a/playback/src/config.rs b/playback/src/config.rs index cdb455cee..20f5a1f5c 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -1,11 +1,232 @@ use std::{mem, str::FromStr, time::Duration}; pub use crate::dither::{mk_ditherer, DithererBuilder, TriangularDitherer}; -use crate::{convert::i24, player::duration_to_coefficient}; -#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] +use crate::{ + convert::i24, + filter_coefficients::{HZ48000_COEFFICIENTS, HZ88200_COEFFICIENTS, HZ96000_COEFFICIENTS}, + RESAMPLER_INPUT_SIZE, SAMPLE_RATE, +}; + +const HZ48000_RESAMPLE_FACTOR: f64 = 48_000.0 / (SAMPLE_RATE as f64); +const HZ88200_RESAMPLE_FACTOR: f64 = 88_200.0 / (SAMPLE_RATE as f64); +const HZ96000_RESAMPLE_FACTOR: f64 = 96_000.0 / (SAMPLE_RATE as f64); + +// Reciprocals allow us to multiply instead of divide during normal interpolation. +const HZ48000_RESAMPLE_FACTOR_RECIPROCAL: f64 = 1.0 / HZ48000_RESAMPLE_FACTOR; +const HZ88200_RESAMPLE_FACTOR_RECIPROCAL: f64 = 1.0 / HZ88200_RESAMPLE_FACTOR; +const HZ96000_RESAMPLE_FACTOR_RECIPROCAL: f64 = 1.0 / HZ96000_RESAMPLE_FACTOR; + +// sample rate * channels +const HZ44100_SAMPLES_PER_SECOND: f64 = 44_100.0 * 2.0; +const HZ48000_SAMPLES_PER_SECOND: f64 = 48_000.0 * 2.0; +const HZ88200_SAMPLES_PER_SECOND: f64 = 88_200.0 * 2.0; +const HZ96000_SAMPLES_PER_SECOND: f64 = 96_000.0 * 2.0; + +// Given a RESAMPLER_INPUT_SIZE of 147 all of our output sizes work out +// to be integers, which is a very good thing. That means no fractional samples +// which translates to much better interpolation. +const HZ48000_INTERPOLATION_OUTPUT_SIZE: usize = + (RESAMPLER_INPUT_SIZE as f64 * HZ48000_RESAMPLE_FACTOR) as usize; + +const HZ88200_INTERPOLATION_OUTPUT_SIZE: usize = + (RESAMPLER_INPUT_SIZE as f64 * HZ88200_RESAMPLE_FACTOR) as usize; + +const HZ96000_INTERPOLATION_OUTPUT_SIZE: usize = + (RESAMPLER_INPUT_SIZE as f64 * HZ96000_RESAMPLE_FACTOR) as usize; + +#[derive(Clone, Copy, Debug, Default)] +pub enum SampleRate { + #[default] + Hz44100, + Hz48000, + Hz88200, + Hz96000, +} + +impl IntoIterator for SampleRate { + type Item = SampleRate; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + use SampleRate::*; + + vec![Hz44100, Hz48000, Hz88200, Hz96000].into_iter() + } +} + +impl FromStr for SampleRate { + type Err = (); + + fn from_str(s: &str) -> Result { + use SampleRate::*; + + let lowercase_input = s.to_lowercase(); + + // Match against both the actual + // stringified value and how most + // humans would write a sample rate. + match lowercase_input.as_str() { + "hz44100" | "44100hz" | "44100" | "44.1khz" => Ok(Hz44100), + "hz48000" | "48000hz" | "48000" | "48khz" => Ok(Hz48000), + "hz88200" | "88200hz" | "88200" | "88.2khz" => Ok(Hz88200), + "hz96000" | "96000hz" | "96000" | "96khz" => Ok(Hz96000), + _ => Err(()), + } + } +} + +impl std::fmt::Display for SampleRate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use SampleRate::*; + + match self { + // Let's make these more human readable. + // "Hz44100" is just awkward. + Hz44100 => write!(f, "44.1kHz"), + Hz48000 => write!(f, "48kHz"), + Hz88200 => write!(f, "88.2kHz"), + Hz96000 => write!(f, "96kHz"), + } + } +} + +impl SampleRate { + pub fn as_u32(&self) -> u32 { + use SampleRate::*; + + match self { + Hz44100 => 44100, + Hz48000 => 48000, + Hz88200 => 88200, + Hz96000 => 96000, + } + } + + pub fn duration_to_normalisation_coefficient(&self, duration: Duration) -> f64 { + let secs = duration.as_secs_f64(); + let ms = secs * 1000.0; + + if ms < 1.0 { + warn!("Coefficient Duration: {:.0} ms, a Normalisation Attack/Release of < 1 ms will cause severe distortion", ms); + } + + (-1.0 / (secs * self.samples_per_second())).exp() + } + + pub fn normalisation_coefficient_to_duration(&self, coefficient: f64) -> Duration { + let duration = Duration::from_secs_f64(-1.0 / coefficient.ln() / self.samples_per_second()); + + let secs = duration.as_secs_f64(); + let ms = secs * 1000.0; + + if ms < 1.0 { + warn!("Coefficient Duration: {:.0} ms, a Normalisation Attack/Release of < 1 ms will cause severe distortion", ms); + } + + duration + } + + fn samples_per_second(&self) -> f64 { + use SampleRate::*; + + match self { + Hz44100 => HZ44100_SAMPLES_PER_SECOND, + Hz48000 => HZ48000_SAMPLES_PER_SECOND, + Hz88200 => HZ88200_SAMPLES_PER_SECOND, + Hz96000 => HZ96000_SAMPLES_PER_SECOND, + } + } + + pub fn get_resample_factor(&self) -> Option { + use SampleRate::*; + + match self { + Hz44100 => None, + Hz48000 => Some(HZ48000_RESAMPLE_FACTOR), + Hz88200 => Some(HZ88200_RESAMPLE_FACTOR), + Hz96000 => Some(HZ96000_RESAMPLE_FACTOR), + } + } + + pub fn get_resample_factor_reciprocal(&self) -> Option { + use SampleRate::*; + + match self { + Hz44100 => None, + Hz48000 => Some(HZ48000_RESAMPLE_FACTOR_RECIPROCAL), + Hz88200 => Some(HZ88200_RESAMPLE_FACTOR_RECIPROCAL), + Hz96000 => Some(HZ96000_RESAMPLE_FACTOR_RECIPROCAL), + } + } + + pub fn get_interpolation_output_size(&self) -> Option { + use SampleRate::*; + + match self { + Hz44100 => None, + Hz48000 => Some(HZ48000_INTERPOLATION_OUTPUT_SIZE), + Hz88200 => Some(HZ88200_INTERPOLATION_OUTPUT_SIZE), + Hz96000 => Some(HZ96000_INTERPOLATION_OUTPUT_SIZE), + } + } + + pub fn get_interpolation_coefficients(&self) -> Option> { + use SampleRate::*; + + match self { + Hz44100 => None, + Hz48000 => Some(Self::calculate_interpolation_coefficients( + HZ48000_COEFFICIENTS.to_vec(), + HZ48000_RESAMPLE_FACTOR_RECIPROCAL, + )), + Hz88200 => Some(Self::calculate_interpolation_coefficients( + HZ88200_COEFFICIENTS.to_vec(), + HZ88200_RESAMPLE_FACTOR_RECIPROCAL, + )), + Hz96000 => Some(Self::calculate_interpolation_coefficients( + HZ96000_COEFFICIENTS.to_vec(), + HZ96000_RESAMPLE_FACTOR_RECIPROCAL, + )), + } + } + + fn calculate_interpolation_coefficients( + mut coefficients: Vec, + resample_factor_reciprocal: f64, + ) -> Vec { + let mut coefficients_sum = 0.0; + + coefficients + .iter_mut() + .enumerate() + .for_each(|(index, coefficient)| { + *coefficient *= Self::sinc((index as f64 * resample_factor_reciprocal).fract()); + + coefficients_sum += *coefficient; + }); + + coefficients + .iter_mut() + .for_each(|coefficient| *coefficient /= coefficients_sum); + + coefficients + } + + fn sinc(x: f64) -> f64 { + if x.abs() < f64::EPSILON { + 1.0 + } else { + let pi_x = std::f64::consts::PI * x; + pi_x.sin() / pi_x + } + } +} + +#[derive(Clone, Copy, Debug, Default, Hash, PartialOrd, Ord, PartialEq, Eq)] pub enum Bitrate { Bitrate96, + #[default] Bitrate160, Bitrate320, } @@ -22,22 +243,28 @@ impl FromStr for Bitrate { } } -impl Default for Bitrate { - fn default() -> Self { - Self::Bitrate160 - } -} - -#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Default, Hash, PartialOrd, Ord, PartialEq, Eq)] pub enum AudioFormat { F64, F32, S32, S24, S24_3, + #[default] S16, } +impl IntoIterator for AudioFormat { + type Item = AudioFormat; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + use AudioFormat::*; + + vec![F64, F32, S32, S24, S24_3, S16].into_iter() + } +} + impl FromStr for AudioFormat { type Err = (); fn from_str(s: &str) -> Result { @@ -53,12 +280,6 @@ impl FromStr for AudioFormat { } } -impl Default for AudioFormat { - fn default() -> Self { - Self::S16 - } -} - impl AudioFormat { // not used by all backends #[allow(dead_code)] @@ -73,10 +294,11 @@ impl AudioFormat { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum NormalisationType { Album, Track, + #[default] Auto, } @@ -92,15 +314,10 @@ impl FromStr for NormalisationType { } } -impl Default for NormalisationType { - fn default() -> Self { - Self::Auto - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum NormalisationMethod { Basic, + #[default] Dynamic, } @@ -115,18 +332,14 @@ impl FromStr for NormalisationMethod { } } -impl Default for NormalisationMethod { - fn default() -> Self { - Self::Dynamic - } -} - #[derive(Clone)] pub struct PlayerConfig { pub bitrate: Bitrate, pub gapless: bool, pub passthrough: bool, + pub sample_rate: SampleRate, + pub normalisation: bool, pub normalisation_type: NormalisationType, pub normalisation_method: NormalisationMethod, @@ -147,12 +360,16 @@ impl Default for PlayerConfig { bitrate: Bitrate::default(), gapless: true, normalisation: false, + sample_rate: SampleRate::default(), normalisation_type: NormalisationType::default(), normalisation_method: NormalisationMethod::default(), normalisation_pregain_db: 0.0, normalisation_threshold_dbfs: -2.0, - normalisation_attack_cf: duration_to_coefficient(Duration::from_millis(5)), - normalisation_release_cf: duration_to_coefficient(Duration::from_millis(100)), + // Dummy value. We can't use the default because + // no matter what it's dependent on the sample rate. + normalisation_attack_cf: 0.0, + // Same with release. + normalisation_release_cf: 0.0, normalisation_knee_db: 5.0, passthrough: false, ditherer: Some(mk_ditherer::), diff --git a/playback/src/convert.rs b/playback/src/convert.rs index a7efe452b..f825490c2 100644 --- a/playback/src/convert.rs +++ b/playback/src/convert.rs @@ -80,36 +80,60 @@ impl Converter { } pub fn f64_to_f32(&mut self, samples: &[f64]) -> Vec { - samples.iter().map(|sample| *sample as f32).collect() + let mut output = Vec::with_capacity(samples.len()); + + output.extend(samples.iter().map(|sample| *sample as f32)); + + output } pub fn f64_to_s32(&mut self, samples: &[f64]) -> Vec { - samples - .iter() - .map(|sample| self.scale(*sample, Self::SCALE_S32) as i32) - .collect() + let mut output = Vec::with_capacity(samples.len()); + + output.extend( + samples + .iter() + .map(|sample| self.scale(*sample, Self::SCALE_S32) as i32), + ); + + output } // S24 is 24-bit PCM packed in an upper 32-bit word pub fn f64_to_s24(&mut self, samples: &[f64]) -> Vec { - samples - .iter() - .map(|sample| self.clamping_scale(*sample, Self::SCALE_S24) as i32) - .collect() + let mut output = Vec::with_capacity(samples.len()); + + output.extend( + samples + .iter() + .map(|sample| self.clamping_scale(*sample, Self::SCALE_S24) as i32), + ); + + output } // S24_3 is 24-bit PCM in a 3-byte array pub fn f64_to_s24_3(&mut self, samples: &[f64]) -> Vec { - samples - .iter() - .map(|sample| i24::from_s24(self.clamping_scale(*sample, Self::SCALE_S24) as i32)) - .collect() + let mut output = Vec::with_capacity(samples.len()); + + output.extend( + samples + .iter() + .map(|sample| i24::from_s24(self.clamping_scale(*sample, Self::SCALE_S24) as i32)), + ); + + output } pub fn f64_to_s16(&mut self, samples: &[f64]) -> Vec { - samples - .iter() - .map(|sample| self.scale(*sample, Self::SCALE_S16) as i16) - .collect() + let mut output = Vec::with_capacity(samples.len()); + + output.extend( + samples + .iter() + .map(|sample| self.scale(*sample, Self::SCALE_S16) as i16), + ); + + output } } diff --git a/playback/src/decoder/mod.rs b/playback/src/decoder/mod.rs index f980b6800..6d82cbc41 100644 --- a/playback/src/decoder/mod.rs +++ b/playback/src/decoder/mod.rs @@ -1,5 +1,3 @@ -use std::ops::Deref; - use thiserror::Error; #[cfg(feature = "passthrough-decoder")] @@ -58,22 +56,9 @@ impl AudioPacket { } } -#[derive(Debug, Clone)] -pub struct AudioPacketPosition { - pub position_ms: u32, - pub skipped: bool, -} - -impl Deref for AudioPacketPosition { - type Target = u32; - fn deref(&self) -> &Self::Target { - &self.position_ms - } -} - pub trait AudioDecoder { fn seek(&mut self, position_ms: u32) -> Result; - fn next_packet(&mut self) -> DecoderResult>; + fn next_packet(&mut self) -> DecoderResult>; } impl From for librespot_core::error::Error { diff --git a/playback/src/decoder/passthrough_decoder.rs b/playback/src/decoder/passthrough_decoder.rs index 59a721632..43cf7433b 100644 --- a/playback/src/decoder/passthrough_decoder.rs +++ b/playback/src/decoder/passthrough_decoder.rs @@ -7,7 +7,7 @@ use std::{ // TODO: move this to the Symphonia Ogg demuxer use ogg::{OggReadError, Packet, PacketReader, PacketWriteEndInfo, PacketWriter}; -use super::{AudioDecoder, AudioPacket, AudioPacketPosition, DecoderError, DecoderResult}; +use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult}; use crate::{ metadata::audio::{AudioFileFormat, AudioFiles}, @@ -136,7 +136,7 @@ impl AudioDecoder for PassthroughDecoder { } } - fn next_packet(&mut self) -> DecoderResult> { + fn next_packet(&mut self) -> DecoderResult> { // write headers if we are (re)starting if !self.bos { self.wtr @@ -207,14 +207,10 @@ impl AudioDecoder for PassthroughDecoder { if !data.is_empty() { let position_ms = Self::position_pcm_to_ms(pckgp_page); - let packet_position = AudioPacketPosition { - position_ms, - skipped: false, - }; let ogg_data = AudioPacket::Raw(std::mem::take(data)); - return Ok(Some((packet_position, ogg_data))); + return Ok(Some((position_ms, ogg_data))); } } } diff --git a/playback/src/decoder/symphonia_decoder.rs b/playback/src/decoder/symphonia_decoder.rs index 2bf2517d3..c716f7e64 100644 --- a/playback/src/decoder/symphonia_decoder.rs +++ b/playback/src/decoder/symphonia_decoder.rs @@ -8,7 +8,7 @@ use symphonia::{ formats::{FormatOptions, FormatReader, SeekMode, SeekTo}, io::{MediaSource, MediaSourceStream, MediaSourceStreamOptions}, meta::{StandardTagKey, Value}, - units::Time, + units::TimeBase, }, default::{ codecs::{MpaDecoder, VorbisDecoder}, @@ -16,7 +16,7 @@ use symphonia::{ }, }; -use super::{AudioDecoder, AudioPacket, AudioPacketPosition, DecoderError, DecoderResult}; +use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult}; use crate::{ metadata::audio::{AudioFileFormat, AudioFiles}, @@ -27,6 +27,7 @@ use crate::{ pub struct SymphoniaDecoder { format: Box, decoder: Box, + time_base: Option, sample_buffer: Option>, } @@ -88,9 +89,12 @@ impl SymphoniaDecoder { ))); } + let time_base = decoder.codec_params().time_base; + Ok(Self { format, decoder, + time_base, // We set the sample buffer when decoding the first full packet, // whose duration is also the ideal sample buffer size. @@ -133,30 +137,23 @@ impl SymphoniaDecoder { } fn ts_to_ms(&self, ts: u64) -> u32 { - let time_base = self.decoder.codec_params().time_base; - let seeked_to_ms = match time_base { - Some(time_base) => { + // Falls back in the unexpected case that the format has no base time set. + self.time_base + .map(|time_base| { let time = time_base.calc_time(ts); - (time.seconds as f64 + time.frac) * 1000. - } - // Fallback in the unexpected case that the format has no base time set. - None => ts as f64 * PAGES_PER_MS, - }; - seeked_to_ms as u32 + ((time.seconds as f64 + time.frac) * 1000.) as u32 + }) + .unwrap_or((ts as f64 * PAGES_PER_MS) as u32) } } impl AudioDecoder for SymphoniaDecoder { fn seek(&mut self, position_ms: u32) -> Result { - let seconds = position_ms as u64 / 1000; - let frac = (position_ms as f64 % 1000.) / 1000.; - let time = Time::new(seconds, frac); - // `track_id: None` implies the default track ID (of the container, not of Spotify). let seeked_to_ts = self.format.seek( SeekMode::Accurate, SeekTo::Time { - time, + time: (position_ms as f64 / 1000.).into(), track_id: None, }, )?; @@ -168,9 +165,7 @@ impl AudioDecoder for SymphoniaDecoder { Ok(self.ts_to_ms(seeked_to_ts.actual_ts)) } - fn next_packet(&mut self) -> DecoderResult> { - let mut skipped = false; - + fn next_packet(&mut self) -> DecoderResult> { loop { let packet = match self.format.next_packet() { Ok(packet) => packet, @@ -187,10 +182,6 @@ impl AudioDecoder for SymphoniaDecoder { }; let position_ms = self.ts_to_ms(packet.ts()); - let packet_position = AudioPacketPosition { - position_ms, - skipped, - }; match self.decoder.decode(&packet) { Ok(decoded) => { @@ -206,13 +197,12 @@ impl AudioDecoder for SymphoniaDecoder { sample_buffer.copy_interleaved_ref(decoded); let samples = AudioPacket::Samples(sample_buffer.samples().to_vec()); - return Ok(Some((packet_position, samples))); + return Ok(Some((position_ms, samples))); } Err(Error::DecodeError(_)) => { // The packet failed to decode due to corrupted or invalid data, get a new // packet and try again. warn!("Skipping malformed audio packet at {position_ms} ms"); - skipped = true; continue; } Err(err) => return Err(err.into()), diff --git a/playback/src/filter_coefficients.rs b/playback/src/filter_coefficients.rs new file mode 100644 index 000000000..1f89764b7 --- /dev/null +++ b/playback/src/filter_coefficients.rs @@ -0,0 +1,1188 @@ +// All coefficients calculated with pyfda (Python Filter Design Analysis Tool) +// https://github.com/chipmuenk/pyfda +// Window = Kaiser +// Ripple = 0.00017371779276643043 + +// β = 20.8 +// fc = 20.66 +// -195dB before 22kHz +#[allow(clippy::excessive_precision)] +pub const HZ48000_COEFFICIENTS: [f64; 241] = [ + -2.258118962333164e-11, + 1.0448101000984154e-10, + -2.579276986417335e-10, + 4.3213325781961056e-10, + -4.5843163381727067e-10, + 2.430239393368342e-11, + 1.2790201415889041e-09, + -3.792077258698514e-09, + 7.493499387692452e-09, + -1.1626845054841438e-08, + 1.4380896605407448e-08, + -1.2833471192711628e-08, + 3.3983200532147757e-09, + 1.7048569784995365e-08, + -4.9373422503539266e-08, + 9.003364640559896e-08, + -1.2924579989482033e-07, + 1.5044668040182147e-07, + -1.3210217109716104e-07, + 5.2624736306518187e-08, + 1.0159667858088225e-07, + -3.2622696353370664e-07, + 5.896182325418432e-07, + -8.277448145932576e-07, + 9.475531903328232e-07, + -8.420069483888789e-07, + 4.1772688158879863e-07, + 3.676938974884631e-07, + -1.4662320604642053e-06, + 2.70957271705655e-06, + -3.802817981364587e-06, + 4.354432548140361e-06, + -3.949196046553083e-06, + 2.260451641101642e-06, + 8.156542880169858e-07, + -5.034945638176245e-06, + 9.729638811991692e-06, + -1.3824155754916521e-05, + 1.5973180290174572e-05, + -1.4828839170824971e-05, + 9.409279173382038e-06, + 4.97915044540138e-07, + -1.3997250778589253e-05, + 2.8927821383294266e-05, + -4.199180492418264e-05, + 4.92301628886413e-05, + -4.683190451509506e-05, + 3.216863473381749e-05, + -4.851687701631952e-06, + -3.245913549120116e-05, + 7.378212902653717e-05, + -0.00011030659888783692, + 0.0001317501695922293, + -0.00012848221714262343, + 9.409014122219963e-05, + -2.7862808366416616e-05, + -6.345544705962209e-05, + 0.00016527387065770158, + -0.00025658320129024414, + 0.00031331992887743433, + -0.0003133321268094909, + 0.0002421329206487914, + -9.823590003163207e-05, + -0.0001033189088939493, + 0.0003305257920305843, + -0.000537884656631541, + 0.0006738503239522973, + -0.0006913872260666551, + 0.0005598234351094426, + -0.00027549921950280733, + -0.0001315408318317103, + 0.0005972048317626658, + -0.0010307540796842497, + 0.0013298141762067902, + -0.0014010675688317755, + 0.001183005538332482, + -0.0006659459018028209, + -9.575731141327346e-05, + 0.0009835467015551942, + -0.001828807023851628, + 0.0024407594224301493, + -0.002644336904102565, + 0.002321370370940948, + -0.0014465226885409, + 0.00010912299971391861, + 0.0014863005385165546, + -0.0030444757114376, + 0.004227621393143003, + -0.004720858410663518, + 0.00430340749485201, + -0.0029111568371450493, + 0.0006758266942872845, + 0.002071261954870286, + -0.004837052438450331, + 0.007044582477270947, + -0.008140448727362718, + 0.0077138932032073445, + -0.005604540990581585, + 0.0019749656455550968, + 0.0026718056262489383, + -0.007541245521216586, + 0.011662297443732754, + -0.014054752766974423, + 0.0139234284401599, + -0.010845235097865776, + 0.0049126327506936645, + 0.0031988445349823255, + -0.01226053329472526, + 0.020623711066876563, + -0.026443075826064425, + 0.027960344694893176, + -0.023804280659778104, + 0.01325702377313349, + 0.0035612135716636354, + -0.025626116624367066, + 0.05108081411605677, + -0.07744152221841791, + 0.10191100680419664, + -0.12175444572282804, + 0.1346803352883561, + 0.8608333333333897, + 0.1346803352883561, + -0.12175444572282804, + 0.10191100680419664, + -0.07744152221841791, + 0.05108081411605677, + -0.025626116624367066, + 0.0035612135716636354, + 0.01325702377313349, + -0.023804280659778104, + 0.027960344694893176, + -0.026443075826064425, + 0.020623711066876563, + -0.01226053329472526, + 0.0031988445349823255, + 0.0049126327506936645, + -0.010845235097865776, + 0.0139234284401599, + -0.014054752766974423, + 0.011662297443732754, + -0.007541245521216586, + 0.0026718056262489383, + 0.0019749656455550968, + -0.005604540990581585, + 0.0077138932032073445, + -0.008140448727362718, + 0.007044582477270947, + -0.004837052438450331, + 0.002071261954870286, + 0.0006758266942872845, + -0.0029111568371450493, + 0.00430340749485201, + -0.004720858410663518, + 0.004227621393143003, + -0.0030444757114376, + 0.0014863005385165546, + 0.00010912299971391861, + -0.0014465226885409, + 0.002321370370940948, + -0.002644336904102565, + 0.0024407594224301493, + -0.001828807023851628, + 0.0009835467015551942, + -9.575731141327346e-05, + -0.0006659459018028209, + 0.001183005538332482, + -0.0014010675688317755, + 0.0013298141762067902, + -0.0010307540796842497, + 0.0005972048317626658, + -0.0001315408318317103, + -0.00027549921950280733, + 0.0005598234351094426, + -0.0006913872260666551, + 0.0006738503239522973, + -0.000537884656631541, + 0.0003305257920305843, + -0.0001033189088939493, + -9.823590003163207e-05, + 0.0002421329206487914, + -0.0003133321268094909, + 0.00031331992887743433, + -0.00025658320129024414, + 0.00016527387065770158, + -6.345544705962209e-05, + -2.7862808366416616e-05, + 9.409014122219963e-05, + -0.00012848221714262343, + 0.0001317501695922293, + -0.00011030659888783692, + 7.378212902653717e-05, + -3.245913549120116e-05, + -4.851687701631952e-06, + 3.216863473381749e-05, + -4.683190451509506e-05, + 4.92301628886413e-05, + -4.199180492418264e-05, + 2.8927821383294266e-05, + -1.3997250778589253e-05, + 4.97915044540138e-07, + 9.409279173382038e-06, + -1.4828839170824971e-05, + 1.5973180290174572e-05, + -1.3824155754916521e-05, + 9.729638811991692e-06, + -5.034945638176245e-06, + 8.156542880169858e-07, + 2.260451641101642e-06, + -3.949196046553083e-06, + 4.354432548140361e-06, + -3.802817981364587e-06, + 2.70957271705655e-06, + -1.4662320604642053e-06, + 3.676938974884631e-07, + 4.1772688158879863e-07, + -8.420069483888789e-07, + 9.475531903328232e-07, + -8.277448145932576e-07, + 5.896182325418432e-07, + -3.2622696353370664e-07, + 1.0159667858088225e-07, + 5.2624736306518187e-08, + -1.3210217109716104e-07, + 1.5044668040182147e-07, + -1.2924579989482033e-07, + 9.003364640559896e-08, + -4.9373422503539266e-08, + 1.7048569784995365e-08, + 3.3983200532147757e-09, + -1.2833471192711628e-08, + 1.4380896605407448e-08, + -1.1626845054841438e-08, + 7.493499387692452e-09, + -3.792077258698514e-09, + 1.2790201415889041e-09, + 2.430239393368342e-11, + -4.5843163381727067e-10, + 4.3213325781961056e-10, + -2.579276986417335e-10, + 1.0448101000984154e-10, + -2.258118962333164e-11, +]; + +// β = 20.6 +// fc = 20.6 +// -195dB before 22kHz +#[allow(clippy::excessive_precision)] +pub const HZ88200_COEFFICIENTS: [f64; 441] = [ + 1.2391850466535611e-11, + 3.333531231852631e-11, + -3.818076213800401e-11, + -1.1509190451828784e-10, + 6.213231728454743e-11, + 2.87564799120199e-10, + -4.83714488602796e-11, + -5.98140269952324e-10, + -7.613942071856133e-11, + 1.091658461057221e-09, + 4.366508439461712e-10, + -1.7901478075133797e-09, + -1.2235803551276082e-09, + 2.6606550552703873e-09, + 2.6983221513529074e-09, + -3.5701268624480448e-09, + -5.1850938604085815e-09, + 4.22849061463688e-09, + 9.040594409322398e-09, + -4.124314021234486e-09, + -1.4592660607406648e-08, + 2.4616283588031036e-09, + 2.2039870818100458e-08, + 1.8885900693913606e-09, + -3.1306686778954656e-08, + -1.0403682851560303e-08, + 4.185370804820422e-08, + 2.4868193950736736e-08, + -5.2450215719700035e-08, + -4.725031763559409e-08, + 6.092638395586248e-08, + 7.946970063915712e-08, + -6.393490946981877e-08, + -1.2303428788774302e-07, + 5.676541482012926e-08, + 1.78533026093471e-07, + -3.326832582637974e-08, + -2.449864750294829e-07, + -1.4044042441359084e-08, + 3.1907908092442563e-07, + 9.36452494218834e-08, + -3.9432463332063344e-07, + -2.143188256204172e-07, + 4.6024886815334354e-07, + 3.8409805762714097e-07, + -5.017077715653631e-07, + -6.087855952658568e-07, + 4.984931642791297e-07, + 8.900563561995833e-07, + -4.2540377575602484e-07, + -1.22320149064785e-06, + 2.529745550676697e-07, + 1.5946390442210958e-06, + 5.0946720741292064e-08, + -1.9793949347642123e-06, + -5.18614345326223e-07, + 2.3388400822855868e-06, + 1.178686943365455e-06, + -2.6190477406863324e-06, + -2.051402354112695e-06, + 2.750198862065714e-06, + 3.1427325820583206e-06, + -2.6475006778025306e-06, + -4.437778022762077e-06, + 2.2140819263549915e-06, + 5.893834031499378e-06, + -1.346275173199573e-06, + -7.433746697465689e-06, + -5.84176093744815e-08, + 8.940351623169533e-06, + 2.0905619376415587e-06, + -1.0252938719737726e-05, + -4.815366354286811e-06, + 1.116678371934488e-05, + 8.257045673101373e-06, + -1.1436808304058493e-05, + -1.2381782138946336e-05, + 1.07863516162757e-05, + 1.7080481116289375e-05, + -8.921836494731473e-06, + -2.2152897980823273e-05, + 5.553781010971955e-06, + 2.7295047456388915e-05, + -4.241366082350153e-07, + -3.20920511104106e-05, + -6.660662180398171e-06, + 3.6018686158196044e-05, + 1.579225072211783e-05, + -3.8449823912780616e-05, + -2.6920566641892563e-05, + 3.86826504349648e-05, + 3.9814608385366115e-05, + -3.5972018152061004e-05, + -5.40272758493732e-05, + 2.957947522797144e-05, + 6.886841768724772e-05, + -1.883547055403238e-05, + -8.339055426463937e-05, + 3.2129726777802045e-06, + 9.639178287766639e-05, + 1.7590665981469e-05, + -0.00010644003002621123, + -4.356823270696307e-05, + 0.00011192203972004702, + 7.43196068089881e-05, + -0.00011111924915469956, + -0.00010897840035651321, + 0.0001023110102270036, + 0.0001461564332782211, + -8.39035143510593e-05, + -0.0001839145994539007, + 5.458036000311854e-05, + 0.00021976848452479796, + -1.3468102824193484e-05, + -0.0002507361886566276, + -3.969247678935454e-05, + 0.00027343412277189825, + 0.00010438105445408068, + -0.00028422407111677975, + -0.00017915202264934046, + 0.00027941159473480933, + 0.0002615232577530964, + -0.0002554920062562159, + -0.0003479147183809301, + 0.00020943586336383046, + 0.0004336515130111242, + -0.00013900145970572364, + -0.0005130441646730521, + 4.305744764222408e-05, + 0.0005795556035604514, + 7.810514286236892e-05, + -0.000626059960741494, + -0.00022249525192315596, + 0.0006451926445289425, + 0.0003862040895990157, + -0.0006297846881076496, + -0.000563273268175783, + 0.0005733672979368042, + 0.0007456731567251603, + -0.00047072533356158625, + -0.0009234124120507005, + 0.00031847160782443723, + 0.0010847942203041586, + -0.00011560795133020693, + -0.001216827481451714, + -0.00013596551747171158, + 0.0013057922370346962, + 0.000431034022287387, + -0.0013379484836784292, + -0.0007607859194021795, + 0.0013003666729946694, + 0.0011127033514844527, + -0.0011818473308014906, + -0.0014706590334826724, + 0.0009738870733597434, + 0.0018152441480513153, + -0.0006716396297363814, + -0.00212434140340482, + 0.00027481405517449135, + 0.0023739441826708192, + 0.00021155117442863655, + -0.002539208026359171, + -0.0007764995060906656, + 0.002595705252002013, + 0.0014028105594797902, + -0.002520838280105065, + -0.002066946890004278, + 0.002295353243766825, + 0.0027393311085735996, + -0.0019048837776677375, + -0.0033849759136069676, + 0.0013414465376334745, + 0.003964471427659919, + -0.0006048059155471451, + -0.0044353140878159725, + -0.0002963736582894608, + 0.004753540316566857, + 0.0013436629459418286, + -0.004875607545075603, + -0.002508353422240745, + 0.004760446192255864, + 0.0037513374964797752, + -0.004371590227561371, + -0.005023403561028252, + 0.003679282168147092, + 0.006265935796833347, + -0.0026624417988238744, + -0.0074119723035414265, + 0.001310387304196292, + 0.008387531541080585, + 0.0003757967194566891, + -0.009113063498733195, + -0.002382337716262893, + 0.009504812205198838, + 0.004682375201032287, + -0.0094757769641619, + -0.007236133980982176, + 0.008935803592365966, + 0.00999169454693342, + -0.00779006602885168, + -0.012886354290132159, + 0.005934686330738007, + 0.015848541869091636, + -0.0032471953026167945, + -0.018800216515377825, + -0.00043276438591991135, + 0.021659656151486406, + 0.00534258783499553, + -0.024344514621636986, + -0.011880324982220983, + 0.026775010617614767, + 0.02080098573194893, + -0.028877100210586794, + -0.033749565270108174, + 0.03058548206942095, + 0.05507167446179364, + -0.03184628978971992, + -0.10086083835694314, + 0.03261933911867418, + 0.3165475195411714, + 0.46712018140067035, + 0.3165475195411714, + 0.03261933911867418, + -0.10086083835694314, + -0.03184628978971992, + 0.05507167446179364, + 0.03058548206942095, + -0.033749565270108174, + -0.028877100210586794, + 0.02080098573194893, + 0.026775010617614767, + -0.011880324982220983, + -0.024344514621636986, + 0.00534258783499553, + 0.021659656151486406, + -0.00043276438591991135, + -0.018800216515377825, + -0.0032471953026167945, + 0.015848541869091636, + 0.005934686330738007, + -0.012886354290132159, + -0.00779006602885168, + 0.00999169454693342, + 0.008935803592365966, + -0.007236133980982176, + -0.0094757769641619, + 0.004682375201032287, + 0.009504812205198838, + -0.002382337716262893, + -0.009113063498733195, + 0.0003757967194566891, + 0.008387531541080585, + 0.001310387304196292, + -0.0074119723035414265, + -0.0026624417988238744, + 0.006265935796833347, + 0.003679282168147092, + -0.005023403561028252, + -0.004371590227561371, + 0.0037513374964797752, + 0.004760446192255864, + -0.002508353422240745, + -0.004875607545075603, + 0.0013436629459418286, + 0.004753540316566857, + -0.0002963736582894608, + -0.0044353140878159725, + -0.0006048059155471451, + 0.003964471427659919, + 0.0013414465376334745, + -0.0033849759136069676, + -0.0019048837776677375, + 0.0027393311085735996, + 0.002295353243766825, + -0.002066946890004278, + -0.002520838280105065, + 0.0014028105594797902, + 0.002595705252002013, + -0.0007764995060906656, + -0.002539208026359171, + 0.00021155117442863655, + 0.0023739441826708192, + 0.00027481405517449135, + -0.00212434140340482, + -0.0006716396297363814, + 0.0018152441480513153, + 0.0009738870733597434, + -0.0014706590334826724, + -0.0011818473308014906, + 0.0011127033514844527, + 0.0013003666729946694, + -0.0007607859194021795, + -0.0013379484836784292, + 0.000431034022287387, + 0.0013057922370346962, + -0.00013596551747171158, + -0.001216827481451714, + -0.00011560795133020693, + 0.0010847942203041586, + 0.00031847160782443723, + -0.0009234124120507005, + -0.00047072533356158625, + 0.0007456731567251603, + 0.0005733672979368042, + -0.000563273268175783, + -0.0006297846881076496, + 0.0003862040895990157, + 0.0006451926445289425, + -0.00022249525192315596, + -0.000626059960741494, + 7.810514286236892e-05, + 0.0005795556035604514, + 4.305744764222408e-05, + -0.0005130441646730521, + -0.00013900145970572364, + 0.0004336515130111242, + 0.00020943586336383046, + -0.0003479147183809301, + -0.0002554920062562159, + 0.0002615232577530964, + 0.00027941159473480933, + -0.00017915202264934046, + -0.00028422407111677975, + 0.00010438105445408068, + 0.00027343412277189825, + -3.969247678935454e-05, + -0.0002507361886566276, + -1.3468102824193484e-05, + 0.00021976848452479796, + 5.458036000311854e-05, + -0.0001839145994539007, + -8.39035143510593e-05, + 0.0001461564332782211, + 0.0001023110102270036, + -0.00010897840035651321, + -0.00011111924915469956, + 7.43196068089881e-05, + 0.00011192203972004702, + -4.356823270696307e-05, + -0.00010644003002621123, + 1.7590665981469e-05, + 9.639178287766639e-05, + 3.2129726777802045e-06, + -8.339055426463937e-05, + -1.883547055403238e-05, + 6.886841768724772e-05, + 2.957947522797144e-05, + -5.40272758493732e-05, + -3.5972018152061004e-05, + 3.9814608385366115e-05, + 3.86826504349648e-05, + -2.6920566641892563e-05, + -3.8449823912780616e-05, + 1.579225072211783e-05, + 3.6018686158196044e-05, + -6.660662180398171e-06, + -3.20920511104106e-05, + -4.241366082350153e-07, + 2.7295047456388915e-05, + 5.553781010971955e-06, + -2.2152897980823273e-05, + -8.921836494731473e-06, + 1.7080481116289375e-05, + 1.07863516162757e-05, + -1.2381782138946336e-05, + -1.1436808304058493e-05, + 8.257045673101373e-06, + 1.116678371934488e-05, + -4.815366354286811e-06, + -1.0252938719737726e-05, + 2.0905619376415587e-06, + 8.940351623169533e-06, + -5.84176093744815e-08, + -7.433746697465689e-06, + -1.346275173199573e-06, + 5.893834031499378e-06, + 2.2140819263549915e-06, + -4.437778022762077e-06, + -2.6475006778025306e-06, + 3.1427325820583206e-06, + 2.750198862065714e-06, + -2.051402354112695e-06, + -2.6190477406863324e-06, + 1.178686943365455e-06, + 2.3388400822855868e-06, + -5.18614345326223e-07, + -1.9793949347642123e-06, + 5.0946720741292064e-08, + 1.5946390442210958e-06, + 2.529745550676697e-07, + -1.22320149064785e-06, + -4.2540377575602484e-07, + 8.900563561995833e-07, + 4.984931642791297e-07, + -6.087855952658568e-07, + -5.017077715653631e-07, + 3.8409805762714097e-07, + 4.6024886815334354e-07, + -2.143188256204172e-07, + -3.9432463332063344e-07, + 9.36452494218834e-08, + 3.1907908092442563e-07, + -1.4044042441359084e-08, + -2.449864750294829e-07, + -3.326832582637974e-08, + 1.78533026093471e-07, + 5.676541482012926e-08, + -1.2303428788774302e-07, + -6.393490946981877e-08, + 7.946970063915712e-08, + 6.092638395586248e-08, + -4.725031763559409e-08, + -5.2450215719700035e-08, + 2.4868193950736736e-08, + 4.185370804820422e-08, + -1.0403682851560303e-08, + -3.1306686778954656e-08, + 1.8885900693913606e-09, + 2.2039870818100458e-08, + 2.4616283588031036e-09, + -1.4592660607406648e-08, + -4.124314021234486e-09, + 9.040594409322398e-09, + 4.22849061463688e-09, + -5.1850938604085815e-09, + -3.5701268624480448e-09, + 2.6983221513529074e-09, + 2.6606550552703873e-09, + -1.2235803551276082e-09, + -1.7901478075133797e-09, + 4.366508439461712e-10, + 1.091658461057221e-09, + -7.613942071856133e-11, + -5.98140269952324e-10, + -4.83714488602796e-11, + 2.87564799120199e-10, + 6.213231728454743e-11, + -1.1509190451828784e-10, + -3.818076213800401e-11, + 3.333531231852631e-11, + 1.2391850466535611e-11, +]; + +// β = 20.6 +// fc = 20.65 +// -195dB before 22kHz +#[allow(clippy::excessive_precision)] +pub const HZ96000_COEFFICIENTS: [f64; 481] = [ + -1.1994395146732941e-11, + 1.915913091651547e-11, + 5.960745176448492e-11, + -1.3061824926945334e-11, + -1.5601965248971414e-10, + -7.018946259546669e-11, + 2.809141662660533e-10, + 3.0701859865581745e-10, + -3.4623720597839356e-10, + -7.560570063834295e-10, + 1.7447498568802657e-10, + 1.3876779389493582e-09, + 4.841122743309274e-10, + -1.9950759509148982e-09, + -1.8677373375643354e-09, + 2.1241568247563375e-09, + 4.040333248197303e-09, + -1.0805250278628995e-09, + -6.6709623778697e-09, + -1.9223082282338022e-09, + 8.824396176308693e-09, + 7.432242319481839e-09, + -8.879743628810473e-09, + -1.526391589543894e-08, + 4.7187712854981555e-09, + 2.395097795739618e-08, + 5.699370544081556e-09, + -3.035967368747228e-08, + -2.3354970601865938e-08, + 2.9753058833130447e-08, + 4.6919535092840925e-08, + -1.6591644718097893e-08, + -7.159335536018259e-08, + -1.377054465796504e-08, + 8.856657748781657e-08, + 6.260407964576534e-08, + -8.57196191379699e-08, + -1.2502708392121218e-07, + 5.005664742272042e-08, + 1.8782810133829837e-07, + 2.8016936501450825e-08, + -2.2906722734906596e-07, + -1.4909601103428738e-07, + 2.206254163789919e-07, + 2.991551330257851e-07, + -1.3442648724143923e-07, + -4.4590734890745537e-07, + -4.7879991080474856e-08, + 5.394825425816262e-07, + 3.2331554237412893e-07, + -5.194446675053159e-07, + -6.57027401634456e-07, + 3.289904476631279e-07, + 9.768751569312674e-07, + 6.484665244473348e-08, + -1.1774482315206333e-06, + -6.487149967509684e-07, + 1.1367035368094803e-06, + 1.3443762821484977e-06, + -7.457595423462837e-07, + -2.0017613173009023e-06, + -5.1597883890098977e-08, + 2.411048558044751e-06, + 1.2177127242482222e-06, + -2.3383632432586653e-06, + -2.5901102398864627e-06, + 1.584294923048922e-06, + 3.874557757285263e-06, + -5.710332117813373e-08, + -4.67416428974752e-06, + -2.1552409835570375e-06, + 4.560474161361854e-06, + 4.735928837508751e-06, + -3.1823115172240006e-06, + -7.136437289370632e-06, + 3.9537838003464527e-07, + 8.638487741992554e-06, + 3.6168030625091555e-06, + -8.487457353378934e-06, + -8.268279687162828e-06, + 6.08570579554361e-06, + 1.2580315824988063e-05, + -1.2131040701919644e-06, + -1.5302661405843096e-05, + -5.777021855417445e-06, + 1.5151846270420658e-05, + 1.384911600179759e-05, + -1.1141109862235736e-05, + -2.1323121189248238e-05, + 2.942823993601164e-06, + 2.6097190687324947e-05, + 8.803876391769033e-06, + -2.605572654342193e-05, + -2.2340476276265842e-05, + 1.9612964695179654e-05, + 3.488121193630334e-05, + -6.290426820769221e-06, + -4.300164683848037e-05, + -1.2813427009008755e-05, + 4.3312083775684775e-05, + 3.48167368145463e-05, + -3.332549671812382e-05, + -5.5242609990036297e-05, + 1.2352369776983332e-05, + 6.866855793659725e-05, + 1.7799995928150835e-05, + -6.980212930301614e-05, + -5.2557793316981136e-05, + 5.482811512633073e-05, + 8.492850576208972e-05, + -2.2760842363500776e-05, + -0.00010654680234804103, + -2.3537583858735534e-05, + 0.00010934295961065633, + 7.701671223906466e-05, + -8.758109455240882e-05, + -0.00012703637978599055, + 3.985676013463422e-05, + 0.000160996958416934, + 2.9449694220786016e-05, + -0.0001668593951537981, + -0.00010975678946015862, + 0.00013615772393127433, + 0.00018525861849561634, + -6.68894489448072e-05, + -0.0002373927755966462, + -3.4446399696334056e-05, + 0.0002485555889307161, + 0.00015235556237131378, + -0.00020646049212939892, + -0.0002638742511127665, + 0.00010824229568279674, + 0.00034220774450222953, + 3.672879517702098e-05, + -0.00036208738952237034, + -0.00020627705332200324, + 0.0003059539182138257, + 0.00036771798854165044, + -0.000169686983664989, + -0.0004830949655477637, + -3.356087538554157e-05, + 0.000516747424579293, + 0.0002727180713288947, + -0.0004439276317579888, + -0.0005021408279491042, + 0.0002586774737622995, + 0.0006689840372751786, + 2.1005660832265722e-05, + -0.0007236944059030511, + -0.0003524392471042917, + 0.0006318239364446761, + 0.0006729912856795399, + -0.00038471232202596237, + -0.0009102437079813836, + 6.38666808859098e-06, + 0.0009962913814304114, + 0.0004455959415068065, + -0.000883700794702363, + -0.0008866682568559575, + 0.0005598267515351781, + 0.0012189997524931617, + -5.597052247416223e-05, + -0.0013506744814651686, + -0.0005515874903799232, + 0.0012169664404254664, + 0.0011503309637701427, + -0.0007993367464203114, + -0.0016097669927078522, + 0.00013754769531381425, + 0.0018067748745233803, + 0.0006689447112888635, + -0.0016536419135613023, + -0.0014724108324020095, + 0.0011230729782408402, + 0.002100681647505729, + -0.0002642101377135884, + -0.0023902085007997164, + -0.0007952746528221351, + 0.0022226419001671943, + 0.0018636837000276705, + -0.0015575734284970257, + -0.002715873624285176, + 0.00045379149820339325, + 0.003135840880826238, + 0.000927277958513202, + -0.0029640553852661746, + -0.002339401002118082, + 0.0021401998423146917, + 0.0034900692956516236, + -0.0007315871721489155, + -0.004094709676209372, + -0.0010608480645675196, + 0.003937529987755766, + 0.002923538240315551, + -0.0029273062430608095, + -0.004477830306334461, + 0.0011358453900638611, + 0.005348134434122349, + 0.0011912532792074859, + -0.005239696871558134, + -0.0036576629006305752, + 0.00401161436225257, + 0.005773326918770769, + -0.0017297944791563327, + -0.007038739381869608, + -0.0013133934970218752, + 0.007043643801584631, + 0.0046211349636550774, + -0.005562920332614075, + -0.007557142405019193, + 0.0026309665606692157, + 0.009446822141073024, + 0.0014221140453043691, + -0.009700340779953782, + -0.005983789053885521, + 0.007937816806243354, + 0.010225042584697239, + -0.004094712744425417, + -0.01321279387321101, + -0.0015125512082305491, + 0.014053552677719357, + 0.00817458594195583, + -0.012046130339034358, + -0.014834568635464904, + 0.006817826441535441, + 0.020182379994583177, + 0.0015804785111363736, + -0.02277241530222604, + -0.012639398046523115, + 0.021117172951004536, + 0.025426139972915516, + -0.013660357026856107, + -0.038688052580126614, + -0.001622620777002329, + 0.05101135603655969, + 0.02896082858633861, + -0.06101196868999017, + -0.0838338660597568, + 0.06752918327641895, + 0.3106351462916042, + 0.4302083333281181, + 0.3106351462916042, + 0.06752918327641895, + -0.0838338660597568, + -0.06101196868999017, + 0.02896082858633861, + 0.05101135603655969, + -0.001622620777002329, + -0.038688052580126614, + -0.013660357026856107, + 0.025426139972915516, + 0.021117172951004536, + -0.012639398046523115, + -0.02277241530222604, + 0.0015804785111363736, + 0.020182379994583177, + 0.006817826441535441, + -0.014834568635464904, + -0.012046130339034358, + 0.00817458594195583, + 0.014053552677719357, + -0.0015125512082305491, + -0.01321279387321101, + -0.004094712744425417, + 0.010225042584697239, + 0.007937816806243354, + -0.005983789053885521, + -0.009700340779953782, + 0.0014221140453043691, + 0.009446822141073024, + 0.0026309665606692157, + -0.007557142405019193, + -0.005562920332614075, + 0.0046211349636550774, + 0.007043643801584631, + -0.0013133934970218752, + -0.007038739381869608, + -0.0017297944791563327, + 0.005773326918770769, + 0.00401161436225257, + -0.0036576629006305752, + -0.005239696871558134, + 0.0011912532792074859, + 0.005348134434122349, + 0.0011358453900638611, + -0.004477830306334461, + -0.0029273062430608095, + 0.002923538240315551, + 0.003937529987755766, + -0.0010608480645675196, + -0.004094709676209372, + -0.0007315871721489155, + 0.0034900692956516236, + 0.0021401998423146917, + -0.002339401002118082, + -0.0029640553852661746, + 0.000927277958513202, + 0.003135840880826238, + 0.00045379149820339325, + -0.002715873624285176, + -0.0015575734284970257, + 0.0018636837000276705, + 0.0022226419001671943, + -0.0007952746528221351, + -0.0023902085007997164, + -0.0002642101377135884, + 0.002100681647505729, + 0.0011230729782408402, + -0.0014724108324020095, + -0.0016536419135613023, + 0.0006689447112888635, + 0.0018067748745233803, + 0.00013754769531381425, + -0.0016097669927078522, + -0.0007993367464203114, + 0.0011503309637701427, + 0.0012169664404254664, + -0.0005515874903799232, + -0.0013506744814651686, + -5.597052247416223e-05, + 0.0012189997524931617, + 0.0005598267515351781, + -0.0008866682568559575, + -0.000883700794702363, + 0.0004455959415068065, + 0.0009962913814304114, + 6.38666808859098e-06, + -0.0009102437079813836, + -0.00038471232202596237, + 0.0006729912856795399, + 0.0006318239364446761, + -0.0003524392471042917, + -0.0007236944059030511, + 2.1005660832265722e-05, + 0.0006689840372751786, + 0.0002586774737622995, + -0.0005021408279491042, + -0.0004439276317579888, + 0.0002727180713288947, + 0.000516747424579293, + -3.356087538554157e-05, + -0.0004830949655477637, + -0.000169686983664989, + 0.00036771798854165044, + 0.0003059539182138257, + -0.00020627705332200324, + -0.00036208738952237034, + 3.672879517702098e-05, + 0.00034220774450222953, + 0.00010824229568279674, + -0.0002638742511127665, + -0.00020646049212939892, + 0.00015235556237131378, + 0.0002485555889307161, + -3.4446399696334056e-05, + -0.0002373927755966462, + -6.68894489448072e-05, + 0.00018525861849561634, + 0.00013615772393127433, + -0.00010975678946015862, + -0.0001668593951537981, + 2.9449694220786016e-05, + 0.000160996958416934, + 3.985676013463422e-05, + -0.00012703637978599055, + -8.758109455240882e-05, + 7.701671223906466e-05, + 0.00010934295961065633, + -2.3537583858735534e-05, + -0.00010654680234804103, + -2.2760842363500776e-05, + 8.492850576208972e-05, + 5.482811512633073e-05, + -5.2557793316981136e-05, + -6.980212930301614e-05, + 1.7799995928150835e-05, + 6.866855793659725e-05, + 1.2352369776983332e-05, + -5.5242609990036297e-05, + -3.332549671812382e-05, + 3.48167368145463e-05, + 4.3312083775684775e-05, + -1.2813427009008755e-05, + -4.300164683848037e-05, + -6.290426820769221e-06, + 3.488121193630334e-05, + 1.9612964695179654e-05, + -2.2340476276265842e-05, + -2.605572654342193e-05, + 8.803876391769033e-06, + 2.6097190687324947e-05, + 2.942823993601164e-06, + -2.1323121189248238e-05, + -1.1141109862235736e-05, + 1.384911600179759e-05, + 1.5151846270420658e-05, + -5.777021855417445e-06, + -1.5302661405843096e-05, + -1.2131040701919644e-06, + 1.2580315824988063e-05, + 6.08570579554361e-06, + -8.268279687162828e-06, + -8.487457353378934e-06, + 3.6168030625091555e-06, + 8.638487741992554e-06, + 3.9537838003464527e-07, + -7.136437289370632e-06, + -3.1823115172240006e-06, + 4.735928837508751e-06, + 4.560474161361854e-06, + -2.1552409835570375e-06, + -4.67416428974752e-06, + -5.710332117813373e-08, + 3.874557757285263e-06, + 1.584294923048922e-06, + -2.5901102398864627e-06, + -2.3383632432586653e-06, + 1.2177127242482222e-06, + 2.411048558044751e-06, + -5.1597883890098977e-08, + -2.0017613173009023e-06, + -7.457595423462837e-07, + 1.3443762821484977e-06, + 1.1367035368094803e-06, + -6.487149967509684e-07, + -1.1774482315206333e-06, + 6.484665244473348e-08, + 9.768751569312674e-07, + 3.289904476631279e-07, + -6.57027401634456e-07, + -5.194446675053159e-07, + 3.2331554237412893e-07, + 5.394825425816262e-07, + -4.7879991080474856e-08, + -4.4590734890745537e-07, + -1.3442648724143923e-07, + 2.991551330257851e-07, + 2.206254163789919e-07, + -1.4909601103428738e-07, + -2.2906722734906596e-07, + 2.8016936501450825e-08, + 1.8782810133829837e-07, + 5.005664742272042e-08, + -1.2502708392121218e-07, + -8.57196191379699e-08, + 6.260407964576534e-08, + 8.856657748781657e-08, + -1.377054465796504e-08, + -7.159335536018259e-08, + -1.6591644718097893e-08, + 4.6919535092840925e-08, + 2.9753058833130447e-08, + -2.3354970601865938e-08, + -3.035967368747228e-08, + 5.699370544081556e-09, + 2.395097795739618e-08, + 4.7187712854981555e-09, + -1.526391589543894e-08, + -8.879743628810473e-09, + 7.432242319481839e-09, + 8.824396176308693e-09, + -1.9223082282338022e-09, + -6.6709623778697e-09, + -1.0805250278628995e-09, + 4.040333248197303e-09, + 2.1241568247563375e-09, + -1.8677373375643354e-09, + -1.9950759509148982e-09, + 4.841122743309274e-10, + 1.3876779389493582e-09, + 1.7447498568802657e-10, + -7.560570063834295e-10, + -3.4623720597839356e-10, + 3.0701859865581745e-10, + 2.809141662660533e-10, + -7.018946259546669e-11, + -1.5601965248971414e-10, + -1.3061824926945334e-11, + 5.960745176448492e-11, + 1.915913091651547e-11, + -1.1994395146732941e-11, +]; diff --git a/playback/src/lib.rs b/playback/src/lib.rs index 43a5b4f0c..5c60791cd 100644 --- a/playback/src/lib.rs +++ b/playback/src/lib.rs @@ -1,3 +1,6 @@ +use std::convert::TryFrom; +use std::fmt; + #[macro_use] extern crate log; @@ -10,11 +13,138 @@ pub mod config; pub mod convert; pub mod decoder; pub mod dither; +pub mod filter_coefficients; pub mod mixer; +pub mod normaliser; pub mod player; +pub mod resampler; +pub mod sample_pipeline; +pub const DB_VOLTAGE_RATIO: f64 = 20.0; +pub const PCM_AT_0DBFS: f64 = 1.0; +pub const RESAMPLER_INPUT_SIZE: usize = 147; pub const SAMPLE_RATE: u32 = 44100; pub const NUM_CHANNELS: u8 = 2; pub const SAMPLES_PER_SECOND: u32 = SAMPLE_RATE * NUM_CHANNELS as u32; pub const PAGES_PER_MS: f64 = SAMPLE_RATE as f64 / 1000.0; pub const MS_PER_PAGE: f64 = 1000.0 / SAMPLE_RATE as f64; + +pub fn db_to_ratio(db: f64) -> f64 { + f64::powf(10.0, db / DB_VOLTAGE_RATIO) +} + +pub fn ratio_to_db(ratio: f64) -> f64 { + ratio.log10() * DB_VOLTAGE_RATIO +} + +#[derive(Copy, Clone, Debug, Default)] +pub enum CommonSampleRates { + #[default] + Hz8000, + Hz11025, + Hz16000, + Hz22050, + Hz44100, + Hz48000, + Hz88200, + Hz96000, + Hz176400, + Hz192000, + Hz352800, + Hz384000, + Hz705600, + Hz768000, +} + +impl TryFrom for CommonSampleRates { + type Error = (); + + fn try_from(value: u32) -> Result { + use CommonSampleRates::*; + + match value { + 8000 => Ok(Hz8000), + 11025 => Ok(Hz11025), + 16000 => Ok(Hz16000), + 22050 => Ok(Hz22050), + 44100 => Ok(Hz44100), + 48000 => Ok(Hz48000), + 88200 => Ok(Hz88200), + 96000 => Ok(Hz96000), + 176400 => Ok(Hz176400), + 192000 => Ok(Hz192000), + 352800 => Ok(Hz352800), + 384000 => Ok(Hz384000), + 705600 => Ok(Hz705600), + 768000 => Ok(Hz768000), + _ => Err(()), + } + } +} + +impl fmt::Display for CommonSampleRates { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use CommonSampleRates::*; + + let rate_str = match self { + Hz8000 => "8kHz", + Hz11025 => "11.025kHz", + Hz16000 => "16kHz", + Hz22050 => "22.05kHz", + Hz44100 => "44.1kHz", + Hz48000 => "48kHz", + Hz88200 => "88.2kHz", + Hz96000 => "96kHz", + Hz176400 => "176.4kHz", + Hz192000 => "192kHz", + Hz352800 => "352.8kHz", + Hz384000 => "384kHz", + Hz705600 => "705.6kHz", + Hz768000 => "768kHz", + }; + + write!(f, "{}", rate_str) + } +} + +impl IntoIterator for CommonSampleRates { + type Item = CommonSampleRates; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + use CommonSampleRates::*; + + vec![ + Hz8000, Hz11025, Hz16000, Hz22050, Hz44100, Hz48000, Hz88200, Hz96000, Hz176400, + Hz192000, Hz352800, Hz384000, Hz705600, Hz768000, + ] + .into_iter() + } +} + +impl CommonSampleRates { + pub fn as_u32(&self) -> u32 { + use CommonSampleRates::*; + + match self { + Hz8000 => 8000, + Hz11025 => 11025, + Hz16000 => 16000, + Hz22050 => 22050, + Hz44100 => 44100, + Hz48000 => 48000, + Hz88200 => 88200, + Hz96000 => 96000, + Hz176400 => 176400, + Hz192000 => 192000, + Hz352800 => 352800, + Hz384000 => 384000, + Hz705600 => 705600, + Hz768000 => 768000, + } + } + + pub fn contains(&self, rate: u32) -> bool { + self.into_iter().any(|r| r.as_u32() == rate) + } +} diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index 52be10852..17f996d87 100644 --- a/playback/src/mixer/alsamixer.rs +++ b/playback/src/mixer/alsamixer.rs @@ -1,4 +1,4 @@ -use crate::player::{db_to_ratio, ratio_to_db}; +use crate::{db_to_ratio, ratio_to_db}; use super::mappings::{LogMapping, MappedCtrl, VolumeMapping}; use super::{Mixer, MixerConfig, VolumeCtrl}; diff --git a/playback/src/mixer/mappings.rs b/playback/src/mixer/mappings.rs index 736b3c3f7..38290d5ee 100644 --- a/playback/src/mixer/mappings.rs +++ b/playback/src/mixer/mappings.rs @@ -1,5 +1,5 @@ use super::VolumeCtrl; -use crate::player::db_to_ratio; +use crate::db_to_ratio; pub trait MappedCtrl { fn to_mapped(&self, volume: u16) -> f64; diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs index 83d008532..9571c2a87 100644 --- a/playback/src/mixer/mod.rs +++ b/playback/src/mixer/mod.rs @@ -15,12 +15,12 @@ pub trait Mixer: Send + Sync { fn set_volume(&self, volume: u16); fn volume(&self) -> u16; - fn get_soft_volume(&self) -> Box { + fn get_soft_volume(&self) -> Box { Box::new(NoOpVolume) } } -pub trait VolumeGetter { +pub trait VolumeGetter: Send { fn attenuation_factor(&self) -> f64; } diff --git a/playback/src/mixer/softmixer.rs b/playback/src/mixer/softmixer.rs index 061f39b94..2f7d21f76 100644 --- a/playback/src/mixer/softmixer.rs +++ b/playback/src/mixer/softmixer.rs @@ -35,7 +35,7 @@ impl Mixer for SoftMixer { .store(mapped_volume.to_bits(), Ordering::Relaxed) } - fn get_soft_volume(&self) -> Box { + fn get_soft_volume(&self) -> Box { Box::new(SoftVolume(self.volume.clone())) } } diff --git a/playback/src/normaliser.rs b/playback/src/normaliser.rs new file mode 100644 index 000000000..04b7ed32b --- /dev/null +++ b/playback/src/normaliser.rs @@ -0,0 +1,338 @@ +use crate::{ + config::{NormalisationMethod, NormalisationType, PlayerConfig}, + db_to_ratio, + decoder::AudioPacket, + mixer::VolumeGetter, + player::NormalisationData, + ratio_to_db, PCM_AT_0DBFS, +}; + +#[derive(PartialEq)] +struct DynamicNormalisation { + threshold_db: f64, + attack_cf: f64, + release_cf: f64, + knee_db: f64, + integrator: f64, + peak: f64, +} + +impl DynamicNormalisation { + fn new(config: &PlayerConfig) -> Self { + // as_millis() has rounding errors (truncates) + let attack = config + .sample_rate + .normalisation_coefficient_to_duration(config.normalisation_attack_cf) + .as_secs_f64() + * 1000.0; + + debug!("Normalisation Attack: {:.0} ms", attack); + + let release = config + .sample_rate + .normalisation_coefficient_to_duration(config.normalisation_release_cf) + .as_secs_f64() + * 1000.0; + + debug!("Normalisation Release: {:.0} ms", release); + + Self { + threshold_db: config.normalisation_threshold_dbfs, + attack_cf: config.normalisation_attack_cf, + release_cf: config.normalisation_release_cf, + knee_db: config.normalisation_knee_db, + integrator: 0.0, + peak: 0.0, + } + } + + fn stop(&mut self) { + self.integrator = 0.0; + self.peak = 0.0; + } + + fn normalise(&mut self, mut samples: Vec, volume: f64, factor: f64) -> Vec { + samples.iter_mut().for_each(|sample| { + *sample *= factor; + + // Feedforward limiter in the log domain + // After: Giannoulis, D., Massberg, M., & Reiss, J.D. (2012). Digital Dynamic + // Range Compressor Design—A Tutorial and Analysis. Journal of The Audio + // Engineering Society, 60, 399-408. + + // Some tracks have samples that are precisely 0.0. That's silence + // and we know we don't need to limit that, in which we can spare + // the CPU cycles. + // + // Also, calling `ratio_to_db(0.0)` returns `inf` and would get the + // peak detector stuck. Also catch the unlikely case where a sample + // is decoded as `NaN` or some other non-normal value. + let limiter_db = if sample.is_normal() { + // step 1-4: half-wave rectification and conversion into dB + // and gain computer with soft knee and subtractor + let bias_db = ratio_to_db(sample.abs()) - self.threshold_db; + let knee_boundary_db = bias_db * 2.0; + + if knee_boundary_db < -self.knee_db { + 0.0 + } else if knee_boundary_db.abs() <= self.knee_db { + // The textbook equation: + // ratio_to_db(sample.abs()) - (ratio_to_db(sample.abs()) - (bias_db + knee_db / 2.0).powi(2) / (2.0 * knee_db)) + // Simplifies to: + // ((2.0 * bias_db) + knee_db).powi(2) / (8.0 * knee_db) + // Which in our case further simplifies to: + // (knee_boundary_db + knee_db).powi(2) / (8.0 * knee_db) + // because knee_boundary_db is 2.0 * bias_db. + (knee_boundary_db + self.knee_db).powi(2) / (8.0 * self.knee_db) + } else { + // Textbook: + // ratio_to_db(sample.abs()) - threshold_db, which is already our bias_db. + bias_db + } + } else { + 0.0 + }; + + // Spare the CPU unless (1) the limiter is engaged, (2) we + // were in attack or (3) we were in release, and that attack/ + // release wasn't finished yet. + if limiter_db > 0.0 || self.integrator > 0.0 || self.peak > 0.0 { + // step 5: smooth, decoupled peak detector + // Textbook: + // release_cf * integrator + (1.0 - release_cf) * limiter_db + // Simplifies to: + // release_cf * integrator - release_cf * limiter_db + limiter_db + self.integrator = limiter_db.max( + self.release_cf * self.integrator - self.release_cf * limiter_db + limiter_db, + ); + // Textbook: + // attack_cf * peak + (1.0 - attack_cf) * integrator + // Simplifies to: + // attack_cf * peak - attack_cf * integrator + integrator + self.peak = + self.attack_cf * self.peak - self.attack_cf * self.integrator + self.integrator; + + // step 6: make-up gain applied later (volume attenuation) + // Applying the standard normalisation factor here won't work, + // because there are tracks with peaks as high as 6 dB above + // the default threshold, so that would clip. + + // steps 7-8: conversion into level and multiplication into gain stage + *sample *= db_to_ratio(-self.peak); + } + + *sample *= volume + }); + + samples + } +} + +#[derive(PartialEq)] +enum Normalisation { + None, + Basic, + Dynamic(DynamicNormalisation), +} + +impl Normalisation { + fn new(config: &PlayerConfig) -> Self { + if !config.normalisation { + Normalisation::None + } else { + debug!("Normalisation Type: {:?}", config.normalisation_type); + debug!( + "Normalisation Pregain: {:.1} dB", + config.normalisation_pregain_db + ); + + debug!( + "Normalisation Threshold: {:.1} dBFS", + config.normalisation_threshold_dbfs + ); + + debug!("Normalisation Method: {:?}", config.normalisation_method); + + match config.normalisation_method { + NormalisationMethod::Dynamic => { + Normalisation::Dynamic(DynamicNormalisation::new(config)) + } + NormalisationMethod::Basic => Normalisation::Basic, + } + } + } + + fn stop(&mut self) { + if let Normalisation::Dynamic(ref mut d) = self { + d.stop() + } + } + + fn normalise(&mut self, mut samples: Vec, volume: f64, factor: f64) -> Vec { + use Normalisation::*; + + match self { + None => { + // We only care about volume. + // We don't care about factor. + // volume: 0.0 - 1.0 + if volume < 1.0 { + // for each sample: sample = sample * volume + samples.iter_mut().for_each(|sample| *sample *= volume); + } + + samples + } + Basic => { + // We care about both volume and factor. + // volume: 0.0 - 1.0 + // factor: 0.0 - 1.0 + if volume < 1.0 || factor < 1.0 { + // for each sample: sample = sample * volume * factor + samples + .iter_mut() + .for_each(|sample| *sample *= volume * factor); + } + + samples + } + // We don't care about anything, DynamicNormalisation does that for us. + Dynamic(ref mut d) => d.normalise(samples, volume, factor), + } + } +} + +pub struct Normaliser { + normalisation: Normalisation, + volume_getter: Box, + normalisation_type: NormalisationType, + pregain_db: f64, + threshold_dbfs: f64, + factor: f64, +} + +impl Normaliser { + pub fn new(config: &PlayerConfig, volume_getter: Box) -> Self { + Self { + normalisation: Normalisation::new(config), + volume_getter, + normalisation_type: config.normalisation_type, + pregain_db: config.normalisation_pregain_db, + threshold_dbfs: config.normalisation_threshold_dbfs, + factor: 0.0, + } + } + + pub fn normalise(&mut self, samples: Vec) -> AudioPacket { + let volume = self.volume_getter.attenuation_factor(); + + AudioPacket::Samples(self.normalisation.normalise(samples, volume, self.factor)) + } + + pub fn stop(&mut self) { + self.factor = 0.0; + self.normalisation.stop(); + } + + pub fn update_normalisation_data( + &mut self, + auto_normalise_as_album: bool, + data: NormalisationData, + ) { + // Normalisation::None doesn't use the factor, + // so there is no need to waste the time calculating it. + if self.normalisation != Normalisation::None { + self.factor = self.get_factor(auto_normalise_as_album, data); + } + } + + fn get_factor(&self, auto_normalise_as_album: bool, data: NormalisationData) -> f64 { + let (gain_db, gain_peak, norm_type) = match self.normalisation_type { + NormalisationType::Album => ( + data.album_gain_db, + data.album_peak, + NormalisationType::Album, + ), + NormalisationType::Track => ( + data.track_gain_db, + data.track_peak, + NormalisationType::Track, + ), + NormalisationType::Auto => { + if auto_normalise_as_album { + ( + data.album_gain_db, + data.album_peak, + NormalisationType::Album, + ) + } else { + ( + data.track_gain_db, + data.track_peak, + NormalisationType::Track, + ) + } + } + }; + + // As per the ReplayGain 1.0 & 2.0 (proposed) spec: + // https://wiki.hydrogenaud.io/index.php?title=ReplayGain_1.0_specification#Clipping_prevention + // https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#Clipping_prevention + let normalisation_factor = if self.normalisation == Normalisation::Basic { + // For Basic Normalisation, factor = min(ratio of (ReplayGain + PreGain), 1.0 / peak level). + // https://wiki.hydrogenaud.io/index.php?title=ReplayGain_1.0_specification#Peak_amplitude + // https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#Peak_amplitude + // We then limit that to 1.0 as not to exceed dBFS (0.0 dB). + let factor = f64::min( + db_to_ratio(gain_db + self.pregain_db), + PCM_AT_0DBFS / gain_peak, + ); + + if factor > PCM_AT_0DBFS { + info!( + "Lowering gain by {:.2} dB for the duration of this track to avoid potentially exceeding dBFS.", + ratio_to_db(factor) + ); + + PCM_AT_0DBFS + } else { + factor + } + } else { + // For Dynamic Normalisation it's up to the player to decide, + // factor = ratio of (ReplayGain + PreGain). + // We then let the dynamic limiter handle gain reduction. + let factor = db_to_ratio(gain_db + self.pregain_db); + let threshold_ratio = db_to_ratio(self.threshold_dbfs); + + if factor > PCM_AT_0DBFS { + let factor_db = gain_db + self.pregain_db; + let limiting_db = factor_db + self.threshold_dbfs.abs(); + + warn!( + "This track may exceed dBFS by {:.2} dB and be subject to {:.2} dB of dynamic limiting at it's peak.", + factor_db, limiting_db + ); + } else if factor > threshold_ratio { + let limiting_db = gain_db + self.pregain_db + self.threshold_dbfs.abs(); + + info!( + "This track may be subject to {:.2} dB of dynamic limiting at it's peak.", + limiting_db + ); + } + + factor + }; + + debug!("Normalisation Data: {:?}", data); + debug!("Normalisation Type: {:?}", self.normalisation_type); + debug!( + "Calculated Normalisation Factor for {:?}: {:.2}%", + norm_type, + normalisation_factor * 100.0 + ); + + normalisation_factor + } +} diff --git a/playback/src/player.rs b/playback/src/player.rs index 162750531..d99b7756d 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -29,22 +29,18 @@ use crate::{ READ_AHEAD_DURING_PLAYBACK, }, audio_backend::Sink, - config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}, - convert::Converter, + config::{Bitrate, PlayerConfig}, core::{util::SeqGenerator, Error, Session, SpotifyId}, - decoder::{AudioDecoder, AudioPacket, AudioPacketPosition, SymphoniaDecoder}, + decoder::{AudioDecoder, AudioPacket, SymphoniaDecoder}, metadata::audio::{AudioFileFormat, AudioFiles, AudioItem}, mixer::VolumeGetter, + sample_pipeline::SamplePipeline, }; #[cfg(feature = "passthrough-decoder")] use crate::decoder::PassthroughDecoder; -use crate::SAMPLES_PER_SECOND; - const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000; -pub const DB_VOLTAGE_RATIO: f64 = 20.0; -pub const PCM_AT_0DBFS: f64 = 1.0; // Spotify inserts a custom Ogg packet at the start with custom metadata values, that you would // otherwise expect in Vorbis comments. This packet isn't well-formed and players may balk at it. @@ -74,15 +70,10 @@ struct PlayerInternal { state: PlayerState, preload: PlayerPreload, - sink: Box, sink_status: SinkStatus, sink_event_callback: Option, - volume_getter: Box, + sample_pipeline: SamplePipeline, event_senders: Vec>, - converter: Converter, - - normalisation_integrator: f64, - normalisation_peak: f64, auto_normalise_as_album: bool, @@ -90,7 +81,7 @@ struct PlayerInternal { play_request_id_generator: SeqGenerator, } -static PLAYER_COUNTER: AtomicUsize = AtomicUsize::new(0); +pub static PLAYER_COUNTER: AtomicUsize = AtomicUsize::new(0); enum PlayerCommand { Load { @@ -269,22 +260,6 @@ impl PlayerEvent { pub type PlayerEventChannel = mpsc::UnboundedReceiver; -pub fn db_to_ratio(db: f64) -> f64 { - f64::powf(10.0, db / DB_VOLTAGE_RATIO) -} - -pub fn ratio_to_db(ratio: f64) -> f64 { - ratio.log10() * DB_VOLTAGE_RATIO -} - -pub fn duration_to_coefficient(duration: Duration) -> f64 { - f64::exp(-1.0 / (duration.as_secs_f64() * SAMPLES_PER_SECOND as f64)) -} - -pub fn coefficient_to_duration(coefficient: f64) -> Duration { - Duration::from_secs_f64(-1.0 / f64::ln(coefficient) / SAMPLES_PER_SECOND as f64) -} - #[derive(Clone, Copy, Debug)] pub struct NormalisationData { // Spotify provides these as `f32`, but audio metadata can contain up to `f64`. @@ -339,86 +314,13 @@ impl NormalisationData { album_peak, }) } - - fn get_factor(config: &PlayerConfig, data: NormalisationData) -> f64 { - if !config.normalisation { - return 1.0; - } - - let (gain_db, gain_peak) = if config.normalisation_type == NormalisationType::Album { - (data.album_gain_db, data.album_peak) - } else { - (data.track_gain_db, data.track_peak) - }; - - // As per the ReplayGain 1.0 & 2.0 (proposed) spec: - // https://wiki.hydrogenaud.io/index.php?title=ReplayGain_1.0_specification#Clipping_prevention - // https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#Clipping_prevention - let normalisation_factor = if config.normalisation_method == NormalisationMethod::Basic { - // For Basic Normalisation, factor = min(ratio of (ReplayGain + PreGain), 1.0 / peak level). - // https://wiki.hydrogenaud.io/index.php?title=ReplayGain_1.0_specification#Peak_amplitude - // https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#Peak_amplitude - // We then limit that to 1.0 as not to exceed dBFS (0.0 dB). - let factor = f64::min( - db_to_ratio(gain_db + config.normalisation_pregain_db), - PCM_AT_0DBFS / gain_peak, - ); - - if factor > PCM_AT_0DBFS { - info!( - "Lowering gain by {:.2} dB for the duration of this track to avoid potentially exceeding dBFS.", - ratio_to_db(factor) - ); - - PCM_AT_0DBFS - } else { - factor - } - } else { - // For Dynamic Normalisation it's up to the player to decide, - // factor = ratio of (ReplayGain + PreGain). - // We then let the dynamic limiter handle gain reduction. - let factor = db_to_ratio(gain_db + config.normalisation_pregain_db); - let threshold_ratio = db_to_ratio(config.normalisation_threshold_dbfs); - - if factor > PCM_AT_0DBFS { - let factor_db = gain_db + config.normalisation_pregain_db; - let limiting_db = factor_db + config.normalisation_threshold_dbfs.abs(); - - warn!( - "This track may exceed dBFS by {:.2} dB and be subject to {:.2} dB of dynamic limiting at it's peak.", - factor_db, limiting_db - ); - } else if factor > threshold_ratio { - let limiting_db = gain_db - + config.normalisation_pregain_db - + config.normalisation_threshold_dbfs.abs(); - - info!( - "This track may be subject to {:.2} dB of dynamic limiting at it's peak.", - limiting_db - ); - } - - factor - }; - - debug!("Normalisation Data: {:?}", data); - debug!( - "Calculated Normalisation Factor for {:?}: {:.2}%", - config.normalisation_type, - normalisation_factor * 100.0 - ); - - normalisation_factor - } } impl Player { pub fn new( config: PlayerConfig, session: Session, - volume_getter: Box, + volume_getter: Box, sink_builder: F, ) -> Arc where @@ -426,37 +328,14 @@ impl Player { { let (cmd_tx, cmd_rx) = mpsc::unbounded_channel(); - if config.normalisation { - debug!("Normalisation Type: {:?}", config.normalisation_type); - debug!( - "Normalisation Pregain: {:.1} dB", - config.normalisation_pregain_db - ); - debug!( - "Normalisation Threshold: {:.1} dBFS", - config.normalisation_threshold_dbfs - ); - debug!("Normalisation Method: {:?}", config.normalisation_method); + let player_id = PLAYER_COUNTER.fetch_add(1, Ordering::SeqCst); - if config.normalisation_method == NormalisationMethod::Dynamic { - // as_millis() has rounding errors (truncates) - debug!( - "Normalisation Attack: {:.0} ms", - coefficient_to_duration(config.normalisation_attack_cf).as_secs_f64() * 1000. - ); - debug!( - "Normalisation Release: {:.0} ms", - coefficient_to_duration(config.normalisation_release_cf).as_secs_f64() * 1000. - ); - debug!("Normalisation Knee: {} dB", config.normalisation_knee_db); - } - } + let thread_name = format!("player:{}", player_id); - let handle = thread::spawn(move || { - let player_id = PLAYER_COUNTER.fetch_add(1, Ordering::AcqRel); - debug!("new Player [{}]", player_id); + let builder = thread::Builder::new().name(thread_name.clone()); - let converter = Converter::new(config.ditherer); + let handle = match builder.spawn(move || { + let sample_pipeline = SamplePipeline::new(&config, sink_builder(), volume_getter); let internal = PlayerInternal { session, @@ -466,15 +345,10 @@ impl Player { state: PlayerState::Stopped, preload: PlayerPreload::None, - sink: sink_builder(), sink_status: SinkStatus::Closed, sink_event_callback: None, - volume_getter, + sample_pipeline, event_senders: vec![], - converter, - - normalisation_peak: 0.0, - normalisation_integrator: 0.0, auto_normalise_as_album: false, @@ -484,11 +358,34 @@ impl Player { // While PlayerInternal is written as a future, it still contains blocking code. // It must be run by using block_on() in a dedicated thread. - let runtime = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime"); + let runtime = match tokio::runtime::Runtime::new() { + Ok(runtime) => runtime, + Err(e) => { + match thread::current().name() { + Some(name) => debug!("Error creating [{name}] thread, Failed to create Tokio runtime: {e}"), + None => debug!("Error creating thread, Failed to create Tokio runtime: {e}"), + } + + exit(1); + } + }; + runtime.block_on(internal); - debug!("PlayerInternal thread finished."); - }); + match thread::current().name() { + Some(name) => debug!(" [{name}] thread finished"), + None => debug!(" thread finished"), + } + }) { + Ok(handle) => { + debug!("Created [{thread_name}] thread"); + handle + } + Err(e) => { + error!("Error creating [{thread_name}] thread: {e}"); + exit(1); + } + }; Arc::new(Self { commands: Some(cmd_tx), @@ -621,11 +518,11 @@ impl Player { impl Drop for Player { fn drop(&mut self) { - debug!("Shutting down player thread ..."); + debug!("Shutting down thread ..."); self.commands = None; if let Some(handle) = self.thread_handle.take() { if let Err(e) = handle.join() { - error!("Player thread Error: {:?}", e); + error!(" thread Error: {:?}", e); } } } @@ -670,7 +567,6 @@ enum PlayerState { decoder: Decoder, audio_item: AudioItem, normalisation_data: NormalisationData, - normalisation_factor: f64, stream_loader_controller: StreamLoaderController, bytes_per_second: usize, duration_ms: u32, @@ -684,7 +580,6 @@ enum PlayerState { decoder: Decoder, normalisation_data: NormalisationData, audio_item: AudioItem, - normalisation_factor: f64, stream_loader_controller: StreamLoaderController, bytes_per_second: usize, duration_ms: u32, @@ -792,10 +687,9 @@ impl PlayerState { Paused { track_id, play_request_id, - decoder, + mut decoder, audio_item, normalisation_data, - normalisation_factor, stream_loader_controller, duration_ms, bytes_per_second, @@ -803,13 +697,14 @@ impl PlayerState { suggested_to_preload_next_track, is_explicit, } => { + let stream_position_ms = decoder.seek(stream_position_ms).unwrap_or_default(); + *self = Playing { track_id, play_request_id, decoder, audio_item, normalisation_data, - normalisation_factor, stream_loader_controller, duration_ms, bytes_per_second, @@ -840,7 +735,6 @@ impl PlayerState { decoder, audio_item, normalisation_data, - normalisation_factor, stream_loader_controller, duration_ms, bytes_per_second, @@ -855,7 +749,6 @@ impl PlayerState { decoder, audio_item, normalisation_data, - normalisation_factor, stream_loader_controller, duration_ms, bytes_per_second, @@ -1256,11 +1149,12 @@ impl Future for PlayerInternal { if self.state.is_playing() { self.ensure_sink_running(); + let sample_pipeline_latency_ms = self.sample_pipeline.get_latency_ms(); + if let PlayerState::Playing { track_id, play_request_id, ref mut decoder, - normalisation_factor, ref mut stream_position_ms, ref mut reported_nominal_start_time, .. @@ -1268,66 +1162,57 @@ impl Future for PlayerInternal { { match decoder.next_packet() { Ok(result) => { - if let Some((ref packet_position, ref packet)) = result { - let new_stream_position_ms = packet_position.position_ms; - let expected_position_ms = std::mem::replace( - &mut *stream_position_ms, - new_stream_position_ms, - ); + if let Some((decoder_position_ms, ref packet)) = result { + *stream_position_ms = + decoder_position_ms.saturating_sub(sample_pipeline_latency_ms); if !passthrough { match packet.samples() { Ok(_) => { - let new_stream_position = Duration::from_millis( - new_stream_position_ms as u64, - ); - let now = Instant::now(); - // Only notify if we're skipped some packets *or* we are behind. - // If we're ahead it's probably due to a buffer of the backend - // and we're actually in time. - let notify_about_position = - match *reported_nominal_start_time { - None => true, - Some(reported_nominal_start_time) => { - let mut notify = false; - - if packet_position.skipped { - if let Some(ahead) = new_stream_position - .checked_sub(Duration::from_millis( - expected_position_ms as u64, - )) - { - notify |= - ahead >= Duration::from_secs(1) - } - } - - if let Some(lag) = now - .checked_duration_since( - reported_nominal_start_time, - ) - { - if let Some(lag) = - lag.checked_sub(new_stream_position) - { - notify |= - lag >= Duration::from_secs(1) - } - } - - notify - } - }; + let notify_about_position = { + // It would be so much easier to use elapsed but elapsed could + // potentially panic is rare cases. + // See: + // https://doc.rust-lang.org/std/time/struct.Instant.html#monotonicity + // + // Otherwise this is pretty straight forward. If anything fails getting + // expected_position_ms it will return 0 which will trigger a notify if + // either stream_position_ms or decoder_position_ms is > 1000. If all goes + // well it's simply a matter of calculating the max delta of expected_position_ms + // and stream_position_ms and expected_position_ms and decoder_position_ms. + // So if the decoder or the sample pipeline are off by more than 1 sec we notify. + let expected_position_ms = now + .checked_duration_since( + reported_nominal_start_time.unwrap_or(now), + ) + .unwrap_or(Duration::ZERO) + .as_millis(); + + let max_expected_position_delta_ms = + expected_position_ms + .abs_diff(*stream_position_ms as u128) + .max( + expected_position_ms.abs_diff( + decoder_position_ms as u128, + ), + ); + + max_expected_position_delta_ms > 1000 + }; if notify_about_position { - *reported_nominal_start_time = - now.checked_sub(new_stream_position); + let position_ms = *stream_position_ms; + + *reported_nominal_start_time = now.checked_sub( + Duration::from_millis(position_ms as u64), + ); + self.send_event(PlayerEvent::PositionCorrection { play_request_id, track_id, - position_ms: new_stream_position_ms, + position_ms, }); } } @@ -1342,7 +1227,7 @@ impl Future for PlayerInternal { } } - self.handle_packet(result, normalisation_factor); + self.handle_packet(result); } Err(e) => { error!("Skipping to next track, unable to get next packet for track <{:?}>: {:?}", track_id, e); @@ -1404,7 +1289,7 @@ impl PlayerInternal { if let Some(callback) = &mut self.sink_event_callback { callback(SinkStatus::Running); } - match self.sink.start() { + match self.sample_pipeline.start() { Ok(()) => self.sink_status = SinkStatus::Running, Err(e) => { error!("{}", e); @@ -1418,7 +1303,7 @@ impl PlayerInternal { match self.sink_status { SinkStatus::Running => { trace!("== Stopping sink =="); - match self.sink.stop() { + match self.sample_pipeline.stop() { Ok(()) => { self.sink_status = if temporarily { SinkStatus::TemporarilyClosed @@ -1538,132 +1423,16 @@ impl PlayerInternal { } } - fn handle_packet( - &mut self, - packet: Option<(AudioPacketPosition, AudioPacket)>, - normalisation_factor: f64, - ) { + fn handle_packet(&mut self, packet: Option<(u32, AudioPacket)>) { match packet { - Some((_, mut packet)) => { + Some((_, packet)) => { if !packet.is_empty() { - if let AudioPacket::Samples(ref mut data) = packet { - // Get the volume for the packet. - // In the case of hardware volume control this will - // always be 1.0 (no change). - let volume = self.volume_getter.attenuation_factor(); - - // For the basic normalisation method, a normalisation factor of 1.0 indicates that - // there is nothing to normalise (all samples should pass unaltered). For the - // dynamic method, there may still be peaks that we want to shave off. - - // No matter the case we apply volume attenuation last if there is any. - if !self.config.normalisation { - if volume < 1.0 { - for sample in data.iter_mut() { - *sample *= volume; - } - } - } else if self.config.normalisation_method == NormalisationMethod::Basic - && (normalisation_factor < 1.0 || volume < 1.0) - { - for sample in data.iter_mut() { - *sample *= normalisation_factor * volume; - } - } else if self.config.normalisation_method == NormalisationMethod::Dynamic { - // zero-cost shorthands - let threshold_db = self.config.normalisation_threshold_dbfs; - let knee_db = self.config.normalisation_knee_db; - let attack_cf = self.config.normalisation_attack_cf; - let release_cf = self.config.normalisation_release_cf; - - for sample in data.iter_mut() { - *sample *= normalisation_factor; - - // Feedforward limiter in the log domain - // After: Giannoulis, D., Massberg, M., & Reiss, J.D. (2012). Digital Dynamic - // Range Compressor Design—A Tutorial and Analysis. Journal of The Audio - // Engineering Society, 60, 399-408. - - // Some tracks have samples that are precisely 0.0. That's silence - // and we know we don't need to limit that, in which we can spare - // the CPU cycles. - // - // Also, calling `ratio_to_db(0.0)` returns `inf` and would get the - // peak detector stuck. Also catch the unlikely case where a sample - // is decoded as `NaN` or some other non-normal value. - let limiter_db = if sample.is_normal() { - // step 1-4: half-wave rectification and conversion into dB - // and gain computer with soft knee and subtractor - let bias_db = ratio_to_db(sample.abs()) - threshold_db; - let knee_boundary_db = bias_db * 2.0; - - if knee_boundary_db < -knee_db { - 0.0 - } else if knee_boundary_db.abs() <= knee_db { - // The textbook equation: - // ratio_to_db(sample.abs()) - (ratio_to_db(sample.abs()) - (bias_db + knee_db / 2.0).powi(2) / (2.0 * knee_db)) - // Simplifies to: - // ((2.0 * bias_db) + knee_db).powi(2) / (8.0 * knee_db) - // Which in our case further simplifies to: - // (knee_boundary_db + knee_db).powi(2) / (8.0 * knee_db) - // because knee_boundary_db is 2.0 * bias_db. - (knee_boundary_db + knee_db).powi(2) / (8.0 * knee_db) - } else { - // Textbook: - // ratio_to_db(sample.abs()) - threshold_db, which is already our bias_db. - bias_db - } - } else { - 0.0 - }; - - // Spare the CPU unless (1) the limiter is engaged, (2) we - // were in attack or (3) we were in release, and that attack/ - // release wasn't finished yet. - if limiter_db > 0.0 - || self.normalisation_integrator > 0.0 - || self.normalisation_peak > 0.0 - { - // step 5: smooth, decoupled peak detector - // Textbook: - // release_cf * self.normalisation_integrator + (1.0 - release_cf) * limiter_db - // Simplifies to: - // release_cf * self.normalisation_integrator - release_cf * limiter_db + limiter_db - self.normalisation_integrator = f64::max( - limiter_db, - release_cf * self.normalisation_integrator - - release_cf * limiter_db - + limiter_db, - ); - // Textbook: - // attack_cf * self.normalisation_peak + (1.0 - attack_cf) * self.normalisation_integrator - // Simplifies to: - // attack_cf * self.normalisation_peak - attack_cf * self.normalisation_integrator + self.normalisation_integrator - self.normalisation_peak = attack_cf * self.normalisation_peak - - attack_cf * self.normalisation_integrator - + self.normalisation_integrator; - - // step 6: make-up gain applied later (volume attenuation) - // Applying the standard normalisation factor here won't work, - // because there are tracks with peaks as high as 6 dB above - // the default threshold, so that would clip. - - // steps 7-8: conversion into level and multiplication into gain stage - *sample *= db_to_ratio(-self.normalisation_peak); - } - - *sample *= volume; - } - } - } - - if let Err(e) = self.sink.write(packet, &mut self.converter) { + if let Err(e) = self.sample_pipeline.write(packet) { error!("{}", e); self.handle_pause(); } } } - None => { self.state.playing_to_end_of_track(); if let PlayerState::EndOfTrack { @@ -1697,16 +1466,10 @@ impl PlayerInternal { let position_ms = loaded_track.stream_position_ms; - let mut config = self.config.clone(); - if config.normalisation_type == NormalisationType::Auto { - if self.auto_normalise_as_album { - config.normalisation_type = NormalisationType::Album; - } else { - config.normalisation_type = NormalisationType::Track; - } - }; - let normalisation_factor = - NormalisationData::get_factor(&config, loaded_track.normalisation_data); + self.sample_pipeline.update_normalisation_data( + self.auto_normalise_as_album, + loaded_track.normalisation_data, + ); if start_playback { self.ensure_sink_running(); @@ -1722,7 +1485,6 @@ impl PlayerInternal { decoder: loaded_track.decoder, audio_item: loaded_track.audio_item, normalisation_data: loaded_track.normalisation_data, - normalisation_factor, stream_loader_controller: loaded_track.stream_loader_controller, duration_ms: loaded_track.duration_ms, bytes_per_second: loaded_track.bytes_per_second, @@ -1741,7 +1503,6 @@ impl PlayerInternal { decoder: loaded_track.decoder, audio_item: loaded_track.audio_item, normalisation_data: loaded_track.normalisation_data, - normalisation_factor, stream_loader_controller: loaded_track.stream_loader_controller, duration_ms: loaded_track.duration_ms, bytes_per_second: loaded_track.bytes_per_second, @@ -2204,7 +1965,17 @@ impl PlayerInternal { let load_handles_clone = self.load_handles.clone(); let handle = tokio::runtime::Handle::current(); - let load_handle = thread::spawn(move || { + + // The player increments the player id when it gets it... + let thread_name = format!( + "loader:{}:{}", + PLAYER_COUNTER.load(Ordering::SeqCst).saturating_sub(1), + spotify_id.to_uri().unwrap_or_default() + ); + + let builder = thread::Builder::new().name(thread_name.clone()); + + let load_handle = match builder.spawn(move || { let data = handle.block_on(loader.load_track(spotify_id, position_ms)); if let Some(data) = data { let _ = result_tx.send(data); @@ -2212,7 +1983,21 @@ impl PlayerInternal { let mut load_handles = load_handles_clone.lock(); load_handles.remove(&thread::current().id()); - }); + + match thread::current().name() { + Some(name) => debug!(" [{name}] thread finished"), + None => debug!(" [loader] thread finished"), + } + }) { + Ok(handle) => { + debug!("Created [{thread_name}] thread"); + handle + } + Err(e) => { + error!("Error creating [{thread_name}] thread: {e}"); + exit(1); + } + }; let mut load_handles = self.load_handles.lock(); load_handles.insert(load_handle.thread().id(), load_handle); diff --git a/playback/src/resampler.rs b/playback/src/resampler.rs new file mode 100644 index 000000000..17f141c35 --- /dev/null +++ b/playback/src/resampler.rs @@ -0,0 +1,457 @@ +use std::{ + collections::VecDeque, process::exit, sync::atomic::Ordering::SeqCst, sync::mpsc, thread, +}; + +use crate::{config::SampleRate, player::PLAYER_COUNTER, RESAMPLER_INPUT_SIZE}; + +struct ConvolutionFilter { + coefficients: Vec, + coefficients_length: usize, + delay_line: VecDeque, +} + +impl ConvolutionFilter { + fn new(coefficients: Vec) -> Self { + let coefficients_length = coefficients.len(); + let delay_line = VecDeque::with_capacity(coefficients_length); + + Self { + coefficients, + coefficients_length, + delay_line, + } + } + + fn get_convoluted_sample(&mut self) -> f64 { + let output_sample = self + .coefficients + .iter() + .zip(&self.delay_line) + .fold(0.0, |acc, (coefficient, delay_line_sample)| { + acc + coefficient * delay_line_sample + }); + + self.delay_line.pop_front(); + + output_sample + } + + fn convolute(&mut self, sample: f64) -> f64 { + self.delay_line.push_back(sample); + + if self.delay_line.len() == self.coefficients_length { + self.get_convoluted_sample() + } else { + 0.0 + } + } + + fn drain(&mut self) -> Vec { + let delay_line_len = self.delay_line.len(); + let mut output = Vec::with_capacity(delay_line_len); + + for _ in 0..delay_line_len { + output.push(self.get_convoluted_sample()); + } + + output + } + + fn clear(&mut self) { + self.delay_line.clear(); + } +} + +struct MonoSincResampler { + interpolator: ConvolutionFilter, + input_buffer: Vec, + resample_factor: f64, + resample_factor_reciprocal: f64, + delay_line_latency: u64, + interpolation_output_size: usize, +} + +impl MonoSincResampler { + fn new(sample_rate: SampleRate) -> Self { + let coefficients = sample_rate + .get_interpolation_coefficients() + .unwrap_or_default(); + + let resample_factor = sample_rate.get_resample_factor().unwrap_or_default(); + + let resample_factor_reciprocal = sample_rate + .get_resample_factor_reciprocal() + .unwrap_or_default(); + + let interpolation_output_size = sample_rate + .get_interpolation_output_size() + .unwrap_or_default(); + + let delay_line_latency = (coefficients.len() as f64 * resample_factor_reciprocal) as u64; + + Self { + interpolator: ConvolutionFilter::new(coefficients), + input_buffer: Vec::with_capacity(RESAMPLER_INPUT_SIZE), + resample_factor, + resample_factor_reciprocal, + delay_line_latency, + interpolation_output_size, + } + } + + fn get_latency_pcm(&mut self) -> u64 { + self.input_buffer.len() as u64 + self.delay_line_latency + } + + fn stop(&mut self) { + self.interpolator.clear(); + self.input_buffer.clear(); + } + + fn drain(&mut self) -> (Option>, u64) { + // On drain the interpolation isn't perfect for a couple reasons: + // 1. buffer len * resample_factor more than likely isn't an integer. + // 2. As you drain the delay line there are less and less samples to use for interpolation. + let output_len = (self.input_buffer.len() as f64 * self.resample_factor) as usize; + let mut output = Vec::with_capacity(output_len); + + output.extend((0..output_len).map(|ouput_index| { + self.interpolator.convolute( + *self + .input_buffer + .get((ouput_index as f64 * self.resample_factor_reciprocal) as usize) + .unwrap_or(&0.0), + ) + })); + + let interpolator_drainage = self.interpolator.drain(); + + output.reserve_exact(interpolator_drainage.len()); + + output.extend(interpolator_drainage.iter()); + + let output_len = output.len() as f64; + + // Do a simple linear fade out of the drainage (about 5ms) to hide/prevent audible artifacts. + for (index, sample) in output.iter_mut().enumerate() { + let fade_factor = 1.0 - (index as f64) / output_len; + *sample *= fade_factor; + } + + (Some(output), 0) + } + + fn resample(&mut self, samples: &[f64]) -> (Option>, u64) { + self.input_buffer.extend_from_slice(samples); + + let num_buffer_chunks = self.input_buffer.len().saturating_div(RESAMPLER_INPUT_SIZE); + + if num_buffer_chunks == 0 { + return (None, self.get_latency_pcm()); + } + + let input_size = num_buffer_chunks * RESAMPLER_INPUT_SIZE; + + let output_size = num_buffer_chunks * self.interpolation_output_size; + + let mut output = Vec::with_capacity(output_size); + + output.extend((0..output_size).map(|ouput_index| { + // Since the interpolation coefficients are pre-calculated we can pretend like + // we're doing nearest neighbor interpolation and then push the samples though + // the interpolator as if it were a simple FIR filter (which it actually also is). + // What comes out the other side is anti-aliased windowed sinc interpolated samples. + self.interpolator.convolute( + *self + .input_buffer + .get((ouput_index as f64 * self.resample_factor_reciprocal) as usize) + .unwrap_or(&0.0), + ) + })); + + self.input_buffer.drain(..input_size); + + (Some(output), self.get_latency_pcm()) + } +} + +enum ResampleTask { + Stop, + Drain, + Terminate, + Resample(Vec), +} + +struct ResampleWorker { + task_sender: Option>, + result_receiver: Option>, u64)>>, + handle: Option>, +} + +impl ResampleWorker { + fn new(sample_rate: SampleRate, name: String) -> Self { + let (task_sender, task_receiver) = mpsc::channel(); + let (result_sender, result_receiver) = mpsc::channel(); + + let builder = thread::Builder::new().name(name.clone()); + + let mut resampler = MonoSincResampler::new(sample_rate); + + let handle = match builder.spawn(move || loop { + match task_receiver.recv() { + Err(e) => { + match thread::current().name() { + Some(name) => error!("Error in [{name}] thread: {e}"), + None => error!("Error in thread: {e}"), + } + + exit(1); + } + Ok(task) => match task { + ResampleTask::Stop => resampler.stop(), + ResampleTask::Drain => { + result_sender.send(resampler.drain()).ok(); + } + ResampleTask::Resample(samples) => { + result_sender.send(resampler.resample(&samples)).ok(); + } + ResampleTask::Terminate => { + loop { + let drained = task_receiver.recv().ok(); + + if drained.is_none() { + break; + } + } + + match thread::current().name() { + Some(name) => debug!(" [{name}] thread finished"), + None => debug!(" thread finished"), + } + + break; + } + }, + } + }) { + Ok(handle) => { + debug!("Created [{name}] thread"); + handle + } + Err(e) => { + error!("Error creating [{name}] thread: {e}"); + exit(1); + } + }; + + Self { + task_sender: Some(task_sender), + result_receiver: Some(result_receiver), + handle: Some(handle), + } + } + + fn stop(&mut self) { + self.task_sender + .as_mut() + .and_then(|sender| sender.send(ResampleTask::Stop).ok()); + } + + fn drain(&mut self) { + self.task_sender + .as_mut() + .and_then(|sender| sender.send(ResampleTask::Drain).ok()); + } + + fn resample(&mut self, samples: Vec) { + self.task_sender + .as_mut() + .and_then(|sender| sender.send(ResampleTask::Resample(samples)).ok()); + } + + fn get_resampled(&mut self) -> (Option>, u64) { + self.result_receiver + .as_mut() + .and_then(|result_receiver| result_receiver.recv().ok()) + .unwrap_or((None, 0)) + } +} + +impl Drop for ResampleWorker { + fn drop(&mut self) { + debug!("Shutting down thread ..."); + self.task_sender + .take() + .and_then(|sender| sender.send(ResampleTask::Terminate).ok()); + + self.result_receiver + .take() + .and_then(|result_receiver| loop { + let drained = result_receiver.recv().ok(); + + if drained.is_none() { + break drained; + } + }); + + self.handle.take().and_then(|handle| handle.join().ok()); + } +} + +enum Resampler { + Bypass, + Worker { + left_resampler: ResampleWorker, + right_resampler: ResampleWorker, + }, +} + +pub struct StereoInterleavedResampler { + resampler: Resampler, + latency_pcm: u64, +} + +impl StereoInterleavedResampler { + pub fn new(sample_rate: SampleRate) -> Self { + debug!("Sample Rate: {sample_rate}"); + + let resampler = match sample_rate { + SampleRate::Hz44100 => { + debug!("Interpolation Type: Bypass"); + debug!("No threads required"); + + Resampler::Bypass + } + _ => { + debug!("Interpolation Type: Windowed Sinc"); + + // The player increments the player id when it gets it... + let player_id = PLAYER_COUNTER.load(SeqCst).saturating_sub(1); + + Resampler::Worker { + left_resampler: ResampleWorker::new( + sample_rate, + format!("resampler:L:{player_id}"), + ), + right_resampler: ResampleWorker::new( + sample_rate, + format!("resampler:R:{player_id}"), + ), + } + } + }; + + Self { + resampler, + latency_pcm: 0, + } + } + + pub fn get_latency_pcm(&mut self) -> u64 { + self.latency_pcm + } + + pub fn drain(&mut self) -> Option> { + match &mut self.resampler { + // Bypass is basically a no-op. + Resampler::Bypass => None, + Resampler::Worker { + left_resampler, + right_resampler, + } => { + left_resampler.drain(); + right_resampler.drain(); + + let (resampled, latency_pcm) = Self::get_resampled(left_resampler, right_resampler); + + self.latency_pcm = latency_pcm; + + resampled + } + } + } + + pub fn resample(&mut self, input_samples: Vec) -> Option> { + match &mut self.resampler { + // Bypass is basically a no-op. + Resampler::Bypass => Some(input_samples), + Resampler::Worker { + left_resampler, + right_resampler, + } => { + let (left_samples, right_samples) = Self::deinterleave_samples(&input_samples); + + left_resampler.resample(left_samples); + right_resampler.resample(right_samples); + + let (resampled, latency_pcm) = Self::get_resampled(left_resampler, right_resampler); + + self.latency_pcm = latency_pcm; + + resampled + } + } + } + + pub fn stop(&mut self) { + self.latency_pcm = 0; + + match &mut self.resampler { + // Stop does nothing + // if we're bypassed. + Resampler::Bypass => (), + Resampler::Worker { + left_resampler, + right_resampler, + } => { + left_resampler.stop(); + right_resampler.stop(); + } + } + } + + fn get_resampled( + left_resampler: &mut ResampleWorker, + right_resampler: &mut ResampleWorker, + ) -> (Option>, u64) { + let (left_resampled, left_latency_pcm) = left_resampler.get_resampled(); + let (right_resampled, right_latency_pcm) = right_resampler.get_resampled(); + + let resampled = left_resampled.and_then(|left_samples| { + right_resampled + .map(|right_samples| Self::interleave_samples(&left_samples, &right_samples)) + }); + + // They should always be equal + let latency_pcm = left_latency_pcm.max(right_latency_pcm); + + (resampled, latency_pcm) + } + + fn interleave_samples(left_samples: &[f64], right_samples: &[f64]) -> Vec { + // Re-interleave the resampled channels. + let mut output = Vec::with_capacity(left_samples.len() + right_samples.len()); + + output.extend( + left_samples + .iter() + .zip(right_samples.iter()) + .flat_map(|(&left, &right)| std::iter::once(left).chain(std::iter::once(right))), + ); + + output + } + + fn deinterleave_samples(samples: &[f64]) -> (Vec, Vec) { + // Split the stereo interleaved samples into left and right channels. + let samples_len = samples.len() / 2; + + let mut left_samples = Vec::with_capacity(samples_len); + let mut right_samples = Vec::with_capacity(samples_len); + + left_samples.extend(samples.iter().step_by(2)); + right_samples.extend(samples.iter().skip(1).step_by(2)); + + (left_samples, right_samples) + } +} diff --git a/playback/src/sample_pipeline.rs b/playback/src/sample_pipeline.rs new file mode 100644 index 000000000..b8e438ef4 --- /dev/null +++ b/playback/src/sample_pipeline.rs @@ -0,0 +1,191 @@ +use crate::{ + audio_backend::{Sink, SinkResult}, + config::PlayerConfig, + convert::Converter, + decoder::AudioPacket, + mixer::VolumeGetter, + normaliser::Normaliser, + player::NormalisationData, + resampler::StereoInterleavedResampler, + MS_PER_PAGE, +}; + +pub enum SamplePipeline { + PassThrough(Bypass), + Process(Pipeline), +} + +impl SamplePipeline { + pub fn new( + config: &PlayerConfig, + sink: Box, + volume_getter: Box, + ) -> Self { + if config.passthrough { + SamplePipeline::PassThrough(Bypass::new(config, sink)) + } else { + SamplePipeline::Process(Pipeline::new(config, sink, volume_getter)) + } + } + + pub fn get_latency_ms(&mut self) -> u32 { + use SamplePipeline::*; + + match self { + PassThrough(_) => 0, + Process(ref mut p) => p.get_latency_ms(), + } + } + + pub fn start(&mut self) -> SinkResult<()> { + use SamplePipeline::*; + + match self { + PassThrough(ref mut p) => p.start()?, + Process(ref mut p) => p.start()?, + } + + Ok(()) + } + + pub fn stop(&mut self) -> SinkResult<()> { + use SamplePipeline::*; + + match self { + PassThrough(ref mut p) => p.stop()?, + Process(ref mut p) => p.stop()?, + } + + Ok(()) + } + + pub fn update_normalisation_data( + &mut self, + auto_normalise_as_album: bool, + data: NormalisationData, + ) { + use SamplePipeline::*; + + match self { + PassThrough(_) => (), + Process(ref mut p) => p.update_normalisation_data(auto_normalise_as_album, data), + } + } + + pub fn write(&mut self, packet: AudioPacket) -> SinkResult<()> { + use SamplePipeline::*; + + match self { + PassThrough(ref mut p) => p.write(packet)?, + Process(ref mut p) => p.write(packet)?, + } + + Ok(()) + } +} + +pub struct Bypass { + converter: Converter, + sink: Box, +} + +impl Bypass { + fn new(config: &PlayerConfig, sink: Box) -> Self { + let converter = Converter::new(config.ditherer); + + Self { converter, sink } + } + + fn start(&mut self) -> SinkResult<()> { + self.sink.start()?; + + Ok(()) + } + + fn stop(&mut self) -> SinkResult<()> { + self.sink.stop()?; + + Ok(()) + } + + fn write(&mut self, packet: AudioPacket) -> SinkResult<()> { + self.sink.write(packet, &mut self.converter)?; + + Ok(()) + } +} + +pub struct Pipeline { + resampler: StereoInterleavedResampler, + normaliser: Normaliser, + converter: Converter, + sink: Box, +} + +impl Pipeline { + fn new( + config: &PlayerConfig, + sink: Box, + volume_getter: Box, + ) -> Self { + let resampler = StereoInterleavedResampler::new(config.sample_rate); + + let normaliser = Normaliser::new(config, volume_getter); + let converter = Converter::new(config.ditherer); + + Self { + resampler, + normaliser, + converter, + sink, + } + } + + fn get_latency_ms(&mut self) -> u32 { + let total_latency_pcm = self.sink.get_latency_pcm() + self.resampler.get_latency_pcm(); + + (total_latency_pcm as f64 * MS_PER_PAGE) as u32 + } + + fn start(&mut self) -> SinkResult<()> { + self.sink.start()?; + + Ok(()) + } + + fn stop(&mut self) -> SinkResult<()> { + self.resampler + .drain() + .map(|processed_samples| self.normaliser.normalise(processed_samples)) + .map(|new_packet| self.sink.write(new_packet, &mut self.converter)) + .transpose()?; + + self.resampler.stop(); + self.normaliser.stop(); + + self.sink.stop()?; + + Ok(()) + } + + fn update_normalisation_data( + &mut self, + auto_normalise_as_album: bool, + data: NormalisationData, + ) { + self.normaliser + .update_normalisation_data(auto_normalise_as_album, data); + } + + fn write(&mut self, packet: AudioPacket) -> SinkResult<()> { + if let AudioPacket::Samples(samples) = packet { + self.resampler + .resample(samples) + .map(|processed_samples| self.normaliser.normalise(processed_samples)) + .map(|new_packet| self.sink.write(new_packet, &mut self.converter)) + .transpose()?; + } + + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 11574d925..06bdcdbb9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,11 +24,12 @@ use librespot::{ playback::{ audio_backend::{self, SinkBuilder, BACKENDS}, config::{ - AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, VolumeCtrl, + AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, SampleRate, + VolumeCtrl, }, dither, mixer::{self, MixerConfig, MixerFn}, - player::{coefficient_to_duration, duration_to_coefficient, Player}, + player::Player, }, }; @@ -239,6 +240,7 @@ fn get_setup() -> Setup { const VOLUME_RANGE: &str = "volume-range"; const ZEROCONF_PORT: &str = "zeroconf-port"; const ZEROCONF_INTERFACE: &str = "zeroconf-interface"; + const SAMPLE_RATE: &str = "sample-rate"; // Mostly arbitrary. const AP_PORT_SHORT: &str = "a"; @@ -576,6 +578,11 @@ fn get_setup() -> Setup { ZEROCONF_INTERFACE, "Comma-separated interface IP addresses on which zeroconf will bind. Defaults to all interfaces. Ignored by DNS-SD.", "IP" + ).optopt( + "", + SAMPLE_RATE, + "Sample Rate to Resample to {44.1kHz|48kHz|88.2kHz|96kHz}. Defaults to 44.1kHz meaning no resampling.", + "SAMPLERATE" ); #[cfg(feature = "passthrough-decoder")] @@ -732,10 +739,18 @@ fn get_setup() -> Setup { let invalid_error_msg = |long: &str, short: &str, invalid: &str, valid_values: &str, default_value: &str| { - error!("Invalid `--{long}` / `-{short}`: \"{invalid}\""); + if short.is_empty() { + error!("Invalid `--{long}`: \"{invalid}\""); + } else { + error!("Invalid `--{long}` / `-{short}`: \"{invalid}\""); + } if !valid_values.is_empty() { - println!("Valid `--{long}` / `-{short}` values: {valid_values}"); + if short.is_empty() { + println!("Valid `--{long}` values: {valid_values}"); + } else { + println!("Valid `--{long}` / `-{short}` values: {valid_values}"); + } } if !default_value.is_empty() { @@ -761,6 +776,24 @@ fn get_setup() -> Setup { exit(1); }); + let sample_rate = opt_str(SAMPLE_RATE) + .as_deref() + .map(|sample_rate| { + SampleRate::from_str(sample_rate).unwrap_or_else(|_| { + let default_value = &format!("{}", SampleRate::default()); + invalid_error_msg( + SAMPLE_RATE, + "", + sample_rate, + "44.1kHz, 48kHz, 88.2kHz, 96kHz", + default_value, + ); + + exit(1); + }) + }) + .unwrap_or_default(); + let format = opt_str(FORMAT) .as_deref() .map(|format| { @@ -782,7 +815,7 @@ fn get_setup() -> Setup { let device = opt_str(DEVICE); if let Some(ref value) = device { if value == "?" { - backend(device, format); + backend(device, format, sample_rate.as_u32()); exit(0); } else if value.is_empty() { empty_string_error_msg(DEVICE, DEVICE_SHORT); @@ -1491,9 +1524,8 @@ fn get_setup() -> Setup { normalisation_attack_cf = opt_str(NORMALISATION_ATTACK) .map(|attack| match attack.parse::() { - Ok(value) if (VALID_NORMALISATION_ATTACK_RANGE).contains(&value) => { - duration_to_coefficient(Duration::from_millis(value)) - } + Ok(value) if (VALID_NORMALISATION_ATTACK_RANGE).contains(&value) => sample_rate + .duration_to_normalisation_coefficient(Duration::from_millis(value)), _ => { let valid_values = &format!( "{} - {}", @@ -1506,20 +1538,21 @@ fn get_setup() -> Setup { NORMALISATION_ATTACK_SHORT, &attack, valid_values, - &coefficient_to_duration(player_default_config.normalisation_attack_cf) - .as_millis() - .to_string(), + "5", ); exit(1); } }) - .unwrap_or(player_default_config.normalisation_attack_cf); + .unwrap_or( + sample_rate.duration_to_normalisation_coefficient(Duration::from_millis(5)), + ); normalisation_release_cf = opt_str(NORMALISATION_RELEASE) .map(|release| match release.parse::() { Ok(value) if (VALID_NORMALISATION_RELEASE_RANGE).contains(&value) => { - duration_to_coefficient(Duration::from_millis(value)) + sample_rate + .duration_to_normalisation_coefficient(Duration::from_millis(value)) } _ => { let valid_values = &format!( @@ -1533,17 +1566,15 @@ fn get_setup() -> Setup { NORMALISATION_RELEASE_SHORT, &release, valid_values, - &coefficient_to_duration( - player_default_config.normalisation_release_cf, - ) - .as_millis() - .to_string(), + "100", ); exit(1); } }) - .unwrap_or(player_default_config.normalisation_release_cf); + .unwrap_or( + sample_rate.duration_to_normalisation_coefficient(Duration::from_millis(100)), + ); normalisation_knee_db = opt_str(NORMALISATION_KNEE) .map(|knee| match knee.parse::() { @@ -1608,6 +1639,7 @@ fn get_setup() -> Setup { bitrate, gapless, passthrough, + sample_rate, normalisation, normalisation_type, normalisation_method, @@ -1692,7 +1724,7 @@ async fn main() { Err(e) => { sys.refresh_processes(); - if sys.uptime() <= 1 { + if sys.uptime() <= 60 { debug!("Retrying to initialise discovery: {e}"); tokio::time::sleep(DISCOVERY_RETRY_TIMEOUT).await; } else { @@ -1723,8 +1755,9 @@ async fn main() { let format = setup.format; let backend = setup.backend; let device = setup.device.clone(); + let sample_rate = player_config.sample_rate.as_u32(); let player = Player::new(player_config, session.clone(), soft_volume, move || { - (backend)(device, format) + (backend)(device, format, sample_rate) }); if let Some(player_event_program) = setup.player_event_program.clone() { @@ -1782,6 +1815,7 @@ async fn main() { let connect_config = setup.connect_config.clone(); + let (spirc_, spirc_task_) = match Spirc::new(connect_config, session.clone(), last_credentials.clone().unwrap_or_default(), @@ -1808,7 +1842,16 @@ async fn main() { warn!("Spirc shut down unexpectedly"); let mut reconnect_exceeds_rate_limit = || { - auto_connect_times.retain(|&t| t.elapsed() < RECONNECT_RATE_LIMIT_WINDOW); + // It would be so much easier to use elapsed but elapsed could + // potentially panic is rare cases. + // See: + // https://doc.rust-lang.org/std/time/struct.Instant.html#monotonicity + let now = Instant::now(); + + auto_connect_times.retain(|&t| { + now.checked_duration_since(t).unwrap_or(RECONNECT_RATE_LIMIT_WINDOW) < RECONNECT_RATE_LIMIT_WINDOW + }); + auto_connect_times.len() > RECONNECT_RATE_LIMIT }; diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs index 3d0a47df5..8af195fc8 100644 --- a/src/player_event_handler.rs +++ b/src/player_event_handler.rs @@ -1,10 +1,15 @@ use log::{debug, error, warn}; -use std::{collections::HashMap, process::Command, thread}; +use std::{ + collections::HashMap, + process::{exit, Command}, + sync::atomic::Ordering, + thread, +}; use librespot::{ metadata::audio::UniqueFields, - playback::player::{PlayerEvent, PlayerEventChannel, SinkStatus}, + playback::player::{PlayerEvent, PlayerEventChannel, SinkStatus, PLAYER_COUNTER}, }; pub struct EventHandler { @@ -14,9 +19,25 @@ pub struct EventHandler { impl EventHandler { pub fn new(mut player_events: PlayerEventChannel, onevent: &str) -> Self { let on_event = onevent.to_string(); - let thread_handle = Some(thread::spawn(move || loop { + + // The player increments the player id when it gets it... + let thread_name = format!( + "event-handler:{}", + PLAYER_COUNTER.load(Ordering::SeqCst).saturating_sub(1) + ); + + let builder = thread::Builder::new().name(thread_name.clone()); + + let thread_handle = match builder.spawn(move || loop { match player_events.blocking_recv() { - None => break, + None => { + match thread::current().name() { + Some(name) => debug!(" [{name}] thread finished"), + None => debug!(" thread finished"), + } + + break; + } Some(event) => { let mut env_vars = HashMap::new(); @@ -249,18 +270,29 @@ impl EventHandler { } } } - })); + }) { + Ok(handle) => { + debug!("Created [{thread_name}] thread"); + handle + } + Err(e) => { + error!("Error creating [{thread_name}] thread: {e}"); + exit(1); + } + }; - Self { thread_handle } + Self { + thread_handle: Some(thread_handle), + } } } impl Drop for EventHandler { fn drop(&mut self) { - debug!("Shutting down EventHandler thread ..."); + debug!("Shutting down thread ..."); if let Some(handle) = self.thread_handle.take() { if let Err(e) = handle.join() { - error!("EventHandler thread Error: {:?}", e); + error!(" thread Error: {:?}", e); } } }