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:
Claude
2025-11-07 07:24:19 +00:00
parent 64586721b9
commit 78004b0327
4 changed files with 129 additions and 9 deletions

View File

@@ -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 {