Fix FLAC pk collision by ensuring full 1024 bytes are read
Problem Analysis: - All FLAC files had the same pk (071c5713d5cf485ca688832207bef0f9) - Root cause: read() can return < 1024 bytes on first call - If read returned only 400 bytes: * header.len() = 400 * 400 > 512 = false * Used header[..] (first 400 bytes = FLAC header) * All FLAC files have identical headers → same pk! Solution: - Added read_exact_or_eof() that loops until 1024 bytes read (or EOF) - Guarantees we skip FLAC header and use actual audio content - Works for small files (< 512 bytes) and large files (>= 1024 bytes) Additional Feature: - Added AudioSink::with_null_output() for testing without audio device - Added --null-audio flag to play_and_cache example - Allows testing in containerized environments Changes: 1. pmocache/src/download.rs: Added read_exact_or_eof() 2. pmocache/src/cache.rs: Use read_exact_or_eof() for pk calculation 3. pmoaudio/src/nodes/audio_sink.rs: Added null output mode 4. pmoparadise/examples/play_and_cache.rs: Added --null-audio flag Test Results: - New pk: 83702c1cbca72074ebf7c123336786ea (was 071c...) - Null audio output works correctly - Ready for full testing
This commit is contained in:
@@ -172,11 +172,70 @@ fn chunk_to_f32_interleaved(chunk: &AudioChunk) -> Vec<f32> {
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Logique pure de lecture audio via cpal
|
||||
pub struct AudioSinkLogic {}
|
||||
pub struct AudioSinkLogic {
|
||||
use_null_output: bool,
|
||||
}
|
||||
|
||||
impl AudioSinkLogic {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
Self {
|
||||
use_null_output: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_null_output() -> Self {
|
||||
Self {
|
||||
use_null_output: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Version null output - consomme les segments sans les jouer
|
||||
async fn process_null_output(
|
||||
mut rx: mpsc::Receiver<Arc<AudioSegment>>,
|
||||
stop_token: CancellationToken,
|
||||
) -> Result<(), AudioError> {
|
||||
loop {
|
||||
let segment = tokio::select! {
|
||||
result = rx.recv() => {
|
||||
match result {
|
||||
Some(seg) => seg,
|
||||
None => {
|
||||
tracing::debug!("AudioSinkLogic (null): input channel closed");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = stop_token.cancelled() => {
|
||||
tracing::debug!("AudioSinkLogic (null): cancelled");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// Juste logger les segments sans les jouer
|
||||
match &segment.segment {
|
||||
crate::_AudioSegment::Chunk(chunk) => {
|
||||
tracing::trace!(
|
||||
"AudioSink (null): consumed chunk with {} frames at {}Hz",
|
||||
chunk.len(),
|
||||
chunk.sample_rate()
|
||||
);
|
||||
}
|
||||
crate::_AudioSegment::Sync(marker) => {
|
||||
match **marker {
|
||||
SyncMarker::TrackBoundary { .. } => {
|
||||
tracing::debug!("AudioSink (null): TrackBoundary received");
|
||||
}
|
||||
SyncMarker::EndOfStream => {
|
||||
tracing::debug!("AudioSink (null): EndOfStream received");
|
||||
return Ok(());
|
||||
}
|
||||
_ => {
|
||||
tracing::trace!("AudioSink (null): sync marker");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,6 +257,12 @@ impl NodeLogic for AudioSinkLogic {
|
||||
|
||||
tracing::debug!("AudioSinkLogic::process started");
|
||||
|
||||
// Si null output, juste consommer les segments sans jouer
|
||||
if self.use_null_output {
|
||||
tracing::debug!("Using null audio output (no playback)");
|
||||
return Self::process_null_output(rx, stop_token).await;
|
||||
}
|
||||
|
||||
// Créer le buffer partagé
|
||||
let buffer = Arc::new(Mutex::new(SharedBuffer::new()));
|
||||
let buffer_clone = buffer.clone();
|
||||
@@ -485,6 +550,14 @@ impl AudioSink {
|
||||
inner: Node::new_with_input(AudioSinkLogic::new(), channel_size),
|
||||
}
|
||||
}
|
||||
|
||||
/// Crée un AudioSink avec null output (pour tests sans carte audio)
|
||||
/// Consomme les segments audio sans les jouer
|
||||
pub fn with_null_output() -> Self {
|
||||
Self {
|
||||
inner: Node::new_with_input(AudioSinkLogic::with_null_output(), DEFAULT_CHANNEL_SIZE),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AudioSink {
|
||||
|
||||
@@ -387,10 +387,11 @@ impl<C: CacheConfig> Cache<C> {
|
||||
where
|
||||
R: AsyncRead + Send + Unpin + 'static,
|
||||
{
|
||||
// 1. Lire jusqu'à 1024 octets (ou ce qui est disponible)
|
||||
let header = crate::download::peek_reader_header(&mut reader, 1024)
|
||||
// 1. Lire EXACTEMENT 1024 octets (ou EOF si fichier plus petit)
|
||||
// Utilise read_exact_or_eof qui boucle jusqu'à avoir tous les octets demandés
|
||||
let header = crate::download::read_exact_or_eof(&mut reader, 1024)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to peek reader header: {}", e))?;
|
||||
.map_err(|e| anyhow!("Failed to read header bytes: {}", e))?;
|
||||
|
||||
// 2. Calculer le pk en utilisant au plus les 512 derniers octets
|
||||
// Ceci évite les collisions pour les fichiers avec headers identiques (ex: FLAC)
|
||||
@@ -399,7 +400,7 @@ impl<C: CacheConfig> Cache<C> {
|
||||
explicit
|
||||
} else {
|
||||
let pk_bytes = if header.len() > 512 {
|
||||
// Fichier >= 512 octets: utiliser les octets 512+ (au plus 512 octets)
|
||||
// Fichier >= 512 octets: utiliser les octets 512-1024 (contenu audio pour FLAC)
|
||||
&header[512..]
|
||||
} else {
|
||||
// Petit fichier < 512 octets: utiliser tout le contenu
|
||||
|
||||
@@ -600,3 +600,37 @@ where
|
||||
buffer.truncate(n);
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
/// Lit exactement `size` octets du reader, ou jusqu'à EOF.
|
||||
///
|
||||
/// Contrairement à `peek_reader_header`, cette fonction boucle jusqu'à avoir lu
|
||||
/// exactement `size` octets (ou atteindre EOF). Ceci est crucial pour calculer
|
||||
/// un pk fiable sur un nombre d'octets précis.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Le buffer contenant exactement `size` octets, ou moins si EOF est atteint
|
||||
pub async fn read_exact_or_eof<R>(reader: &mut R, size: usize) -> Result<Vec<u8>, String>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
{
|
||||
let mut buffer = vec![0u8; size];
|
||||
let mut total_read = 0;
|
||||
|
||||
while total_read < size {
|
||||
let n = reader
|
||||
.read(&mut buffer[total_read..])
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read from stream: {}", e))?;
|
||||
|
||||
if n == 0 {
|
||||
// EOF atteint
|
||||
buffer.truncate(total_read);
|
||||
return Ok(buffer);
|
||||
}
|
||||
|
||||
total_read += n;
|
||||
}
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// Récupérer les arguments
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() != 2 {
|
||||
eprintln!("Usage: {} <channel_id>", args[0]);
|
||||
if args.len() < 2 {
|
||||
eprintln!("Usage: {} <channel_id> [--null-audio]", args[0]);
|
||||
eprintln!();
|
||||
eprintln!("Downloads a Radio Paradise block, caches it, and plays it simultaneously.");
|
||||
eprintln!();
|
||||
@@ -60,6 +60,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
eprintln!(" 1 - Mellow Mix (smooth, chilled music)");
|
||||
eprintln!(" 2 - Rock Mix (classic & modern rock)");
|
||||
eprintln!(" 3 - World/Etc Mix (global sounds)");
|
||||
eprintln!();
|
||||
eprintln!("Options:");
|
||||
eprintln!(" --null-audio Don't play audio (for testing without audio device)");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
@@ -71,7 +74,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
};
|
||||
|
||||
let use_null_audio = args.len() > 2 && args[2] == "--null-audio";
|
||||
|
||||
tracing::info!("Channel ID: {}", channel_id);
|
||||
if use_null_audio {
|
||||
tracing::info!("Using null audio output (no playback)");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Initialiser les caches et le gestionnaire de playlist
|
||||
@@ -188,7 +196,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing::debug!("PlaylistSource created");
|
||||
|
||||
// Créer le sink audio
|
||||
let audio_sink = AudioSink::new();
|
||||
let audio_sink = if use_null_audio {
|
||||
AudioSink::with_null_output()
|
||||
} else {
|
||||
AudioSink::new()
|
||||
};
|
||||
tracing::debug!("AudioSink created");
|
||||
|
||||
// Connecter playlist → audio
|
||||
|
||||
Reference in New Issue
Block a user