diff --git a/pmoaudio-ext/src/sinks/streaming_flac_sink.rs b/pmoaudio-ext/src/sinks/streaming_flac_sink.rs index bb354b7f..9d132adb 100644 --- a/pmoaudio-ext/src/sinks/streaming_flac_sink.rs +++ b/pmoaudio-ext/src/sinks/streaming_flac_sink.rs @@ -477,6 +477,7 @@ impl StreamingFlacSink { timestamp_offset_sec: 0.0, current_timestamp: Arc::new(RwLock::new(0.0)), pending_track_duration: None, + pending_total_samples: None, }, }; diff --git a/pmoaudio-ext/src/sinks/streaming_ogg_flac_sink.rs b/pmoaudio-ext/src/sinks/streaming_ogg_flac_sink.rs index 533b4067..50cbc6dc 100644 --- a/pmoaudio-ext/src/sinks/streaming_ogg_flac_sink.rs +++ b/pmoaudio-ext/src/sinks/streaming_ogg_flac_sink.rs @@ -432,6 +432,7 @@ impl StreamingOggFlacSink { timestamp_offset_sec: 0.0, current_timestamp: Arc::new(RwLock::new(0.0)), pending_track_duration: None, + pending_total_samples: None, }, }; diff --git a/pmoaudio-ext/src/sinks/streaming_sink_common.rs b/pmoaudio-ext/src/sinks/streaming_sink_common.rs index dc142126..3a37f4f8 100644 --- a/pmoaudio-ext/src/sinks/streaming_sink_common.rs +++ b/pmoaudio-ext/src/sinks/streaming_sink_common.rs @@ -16,7 +16,7 @@ use tokio::io::{AsyncRead, ReadBuf}; use tokio::sync::{mpsc, RwLock}; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; -use tracing::{debug, error, trace, warn}; +use tracing::{debug, error, info, trace, warn}; use crate::byte_stream_reader::{ByteStreamReader, PcmChunk}; use crate::sinks::timed_broadcast::{self, TryRecvError}; @@ -214,6 +214,7 @@ pub struct SharedSinkContext { pub timestamp_offset_sec: f64, pub current_timestamp: Arc>, pub pending_track_duration: Option, + pub pending_total_samples: Option, } impl SharedSinkContext { @@ -316,31 +317,50 @@ impl SharedSinkContext { metadata.get_duration().await.ok().flatten() }; self.pending_track_duration = duration_opt; - + self.pending_total_samples = { + let metadata = metadata_lock.read().await; + metadata.get_total_samples().await.ok().flatten() + }; + // Compute total_samples only when we know the sample rate. - if let (Some(duration), Some(sr)) = (self.pending_track_duration, self.sample_rate) { - debug!("Encoder metadata: duration: {:3}s - rate: {}Hz", Duration::as_secs_f64(&duration),sr); - let samples = (duration.as_secs_f64() * sr as f64).round() as u64; - self.encoder_options.total_samples = Some(samples); - } else { - if self.pending_track_duration.is_none() { - debug!("Encoder metadata: duration: None"); - } - if self.sample_rate.is_none() { - debug!("Encoder metadata: sample rate: None"); - } - // Avoid leaking the previous track's length. - self.encoder_options.total_samples = None; - } + self.refresh_total_samples_with_sample_rate(); Ok(()) } /// Refresh total_samples when the sample rate is learned after metadata was already set. pub fn refresh_total_samples_with_sample_rate(&mut self) { + info!( + "Encoder metadata: refresh_total_samples (pending_total_samples={:?}, pending_duration={:?}, sample_rate={:?})", + self.pending_total_samples, + self.pending_track_duration + .as_ref() + .map(Duration::as_secs_f64), + self.sample_rate + ); + + if let Some(total) = self.pending_total_samples { + self.encoder_options.total_samples = Some(total); + info!( + "Encoder metadata: using provided total_samples={} from TrackBoundary", + total + ); + return; + } + if let (Some(duration), Some(sr)) = (self.pending_track_duration, self.sample_rate) { let samples = (duration.as_secs_f64() * sr as f64).round() as u64; self.encoder_options.total_samples = Some(samples); + info!( + "Encoder metadata: computed total_samples={} (duration {:.3}s @ {} Hz)", + samples, + duration.as_secs_f64(), + sr + ); + } else { + // Avoid leaking the previous track's length. + self.encoder_options.total_samples = None; + info!("Encoder metadata: no duration/total_samples available; clearing total_samples in encoder options"); } } diff --git a/pmoaudiocache/src/cache.rs b/pmoaudiocache/src/cache.rs index 8c117856..7a02ebdd 100644 --- a/pmoaudiocache/src/cache.rs +++ b/pmoaudiocache/src/cache.rs @@ -6,7 +6,7 @@ use anyhow::Result; use pmocache::CacheConfig; -use serde_json::Value; +use serde_json::{Number, Value}; use std::sync::Arc; /// Configuration pour le cache audio @@ -144,6 +144,57 @@ pub async fn add_with_metadata_extraction( // Extraire les métadonnées let mut metadata = crate::metadata::AudioMetadata::from_bytes(&flac_bytes)?; + // Propager les métadonnées techniques dans la DB (sans passer par le JSON) + if let Some(d) = metadata.duration_secs { + let _ = cache + .db + .set_a_metadata(&pk, "duration_secs", Value::Number(Number::from(d))); + } + if let Some((sr, bps, total_samples)) = parse_flac_streaminfo(&flac_bytes) { + let _ = cache + .db + .set_a_metadata(&pk, "sample_rate", Value::Number(Number::from(sr))); + let _ = cache + .db + .set_a_metadata(&pk, "bits_per_sample", Value::Number(Number::from(bps))); + let _ = cache.db.set_a_metadata( + &pk, + "total_samples", + Value::Number(Number::from(total_samples)), + ); + if metadata.duration_secs.is_none() && sr > 0 { + let secs = (total_samples as f64 / sr as f64).round() as u64; + let _ = cache + .db + .set_a_metadata(&pk, "duration_secs", Value::Number(Number::from(secs))); + metadata.duration_secs = Some(secs); + } + if metadata.sample_rate.is_none() { + metadata.sample_rate = Some(sr); + } + } + + // Extraire aussi les informations FLAC de base (STREAMINFO) pour peupler TrackMetadata + if let Some((sr, bps, total_samples)) = parse_flac_streaminfo(&flac_bytes) { + if let Err(e) = cache + .db + .set_a_metadata(&pk, "sample_rate", Value::Number(Number::from(sr))) + { + tracing::warn!("Failed to persist sample_rate for {}: {}", pk, e); + } + if let Err(e) = cache + .db + .set_a_metadata(&pk, "bits_per_sample", Value::Number(Number::from(bps))) + { + tracing::warn!("Failed to persist bits_per_sample for {}: {}", pk, e); + } + if let Err(e) = cache + .db + .set_a_metadata(&pk, "total_samples", Value::Number(Number::from(total_samples))) + { + tracing::warn!("Failed to persist total_samples for {}: {}", pk, e); + } + } if let Some(transform) = cache.transform_metadata(&pk).await { if let Some(mode) = transform.mode { @@ -210,3 +261,40 @@ pub fn get_metadata(cache: &Cache, pk: &str) -> Result Option<(u32, u8, u64)> { + // Expect "fLaC" + 4-byte metadata block header + 34-byte STREAMINFO + if data.len() < 4 + 4 + 34 { + return None; + } + if &data[0..4] != b"fLaC" { + return None; + } + + let block_type = data[4] & 0x7F; + let block_len = ((data[5] as usize) << 16) | ((data[6] as usize) << 8) | data[7] as usize; + if block_type != 0 || block_len < 34 || data.len() < 8 + block_len { + return None; + } + + let s = &data[8..8 + 34]; + + // sample_rate: 20 bits: bytes 10..12 + let sample_rate = + ((s[10] as u32) << 12) | ((s[11] as u32) << 4) | ((s[12] as u32 & 0xF0) >> 4); + + // bits_per_sample: 5 bits spanning byte 12 (lsb) and byte 13 (msb) + let bps_raw = (((s[12] & 0x01) as u8) << 4) | ((s[13] & 0xF0) >> 4); + let bits_per_sample = bps_raw.saturating_add(1); + + // total_samples: 36 bits: lower 4 bits of byte 13 + bytes 14..17 + let total_samples = (((s[13] & 0x0F) as u64) << 32) + | ((s[14] as u64) << 24) + | ((s[15] as u64) << 16) + | ((s[16] as u64) << 8) + | (s[17] as u64); + + Some((sample_rate, bits_per_sample, total_samples)) +} diff --git a/pmometadata/src/lib.rs b/pmometadata/src/lib.rs index eb42dd20..ec97b9e4 100755 --- a/pmometadata/src/lib.rs +++ b/pmometadata/src/lib.rs @@ -281,15 +281,6 @@ pub trait TrackMetadata: Send + Sync { async fn touch(&mut self) -> MetadataResult<()> { Err(MetadataError::NotImplemented) } - - async fn set_sample_rate(&mut self, _value: Option) -> MetadataResult<()> { - Err(MetadataError::NotImplemented) - } - - async fn get_sample_rate(self) -> MetadataResult { - Err(MetadataError::NotImplemented) - } - } /// Copies all available metadata from one implementation to another.