From 3219a8beda6ee7a32012f45cd3d9e6c28207f601 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Nov 2025 09:45:17 +0000 Subject: [PATCH] WIP: Attempt granule position tracking for OGG-FLAC (incomplete) Added FLAC frame parsing to calculate granule positions, but stream still fails ffmpeg decode with errors like "invalid sync code", "invalid frame header". Changes attempted: - parse_flac_block_size(): Parse block size from FLAC frame headers - find_complete_frames_with_samples(): Track samples for granule position - OggPageWriter::add_samples(): Update granule position incrementally - Extract sample rate from STREAMINFO for calculations Issues remaining: - Block size parsing incomplete (codes 0x06/0x07 not handled) - Granule position calculation may be incorrect - Stream still produces decode errors in ffmpeg/ffplay - Need deeper analysis of OGG page structure vs FLAC frame alignment This commit preserves the work in progress. Further debugging needed to identify root cause of decode failures. --- .../src/sinks/streaming_ogg_flac_sink.rs | 97 +++++++++++++------ 1 file changed, 70 insertions(+), 27 deletions(-) diff --git a/pmoaudio-ext/src/sinks/streaming_ogg_flac_sink.rs b/pmoaudio-ext/src/sinks/streaming_ogg_flac_sink.rs index 754b7924..d8073e4d 100644 --- a/pmoaudio-ext/src/sinks/streaming_ogg_flac_sink.rs +++ b/pmoaudio-ext/src/sinks/streaming_ogg_flac_sink.rs @@ -700,44 +700,73 @@ fn chunk_to_pcm_bytes(chunk: &AudioChunk, bits_per_sample: u8) -> Result Ok(bytes) } -/// Find the position where we should split the buffer to send complete FLAC frames. -/// Returns the byte position just before the last FLAC frame starts. -/// -/// FLAC frames start with a sync code: 14 bits set to 1, followed by a 0 bit. -/// This corresponds to byte patterns: 0xFF 0xF8 through 0xFF 0xFF. -/// -/// The sync code marks the START of a frame. To send complete frames, we find the -/// last sync code and send everything BEFORE it (which contains complete frames), -/// keeping the data from the last sync code onward for the next iteration. -fn find_complete_frames_boundary(data: &[u8]) -> usize { +/// Parse FLAC block size from frame header +/// Returns number of samples in the frame, or None if parsing fails +fn parse_flac_block_size(data: &[u8], offset: usize) -> Option { + if offset + 4 > data.len() { + return None; + } + + // FLAC frame header starts with sync code 0xFF 0xF8-0xFF + if data[offset] != 0xFF || data[offset + 1] < 0xF8 { + return None; + } + + // Byte 2 contains block size code in bits 4-7 + let block_size_code = (data[offset + 2] >> 4) & 0x0F; + + // Decode block size according to FLAC spec + let block_size = match block_size_code { + 0x00 => return None, // Reserved + 0x01 => 192, + 0x02..=0x05 => 576 * (1 << (block_size_code - 2)), + 0x06 => return None, // Get 8-bit value from end of header (not implemented) + 0x07 => return None, // Get 16-bit value from end of header (not implemented) + 0x08..=0x0F => 256 * (1 << (block_size_code - 8)), + _ => return None, + }; + + Some(block_size) +} + +/// Find complete FLAC frames and calculate total samples +/// Returns (byte_position, total_samples) or (0, 0) if no complete frames +fn find_complete_frames_with_samples(data: &[u8]) -> (usize, u64) { if data.len() < 4 { - return 0; + return (0, 0); } let mut sync_positions = Vec::new(); + let mut frame_samples = Vec::new(); - // Search for FLAC sync codes - // FLAC sync is 14 bits of 1: first byte is always 0xFF - // Second byte: 0xF8-0xFF (most common: 0xF8 for fixed blocksize, 0xF9 for variable) - // We search more conservatively for 0xF8-0xFE to avoid false positives + // Search for FLAC sync codes and parse block sizes for i in 0..data.len() - 1 { let byte1 = data[i]; let byte2 = data[i + 1]; - // Check for FLAC sync pattern: 0xFF followed by 0xF8-0xFE - // We exclude 0xFF 0xFF as it's less common and more likely to be a false positive if byte1 == 0xFF && byte2 >= 0xF8 && byte2 <= 0xFE { sync_positions.push(i); + + // Try to parse block size for this frame + if let Some(samples) = parse_flac_block_size(data, i) { + frame_samples.push(samples); + } else { + // If we can't parse, assume typical 4096 samples + frame_samples.push(4096); + } } } // We need at least 2 sync codes to identify one complete frame - // The last sync code marks the start of a potentially incomplete frame - // Return the position of the last sync code - everything before it is complete if sync_positions.len() >= 2 { - *sync_positions.last().unwrap() + let boundary = *sync_positions.last().unwrap(); + // Sum samples for all complete frames (all except the last incomplete one) + let total_samples: u64 = frame_samples.iter().take(sync_positions.len() - 1) + .map(|&s| s as u64) + .sum(); + (boundary, total_samples) } else { - 0 + (0, 0) } } @@ -758,11 +787,16 @@ async fn broadcast_ogg_flac_stream( let mut total_ogg_bytes = 0u64; let mut header_captured = false; let start_time = std::time::Instant::now(); + let mut last_granule_update_time = 0.0f64; // Step 1: Read FLAC header (fLaC + metadata blocks) let flac_header = read_flac_header(&mut flac_stream).await?; info!("Read FLAC header: {} bytes", flac_header.len()); + // Extract sample rate from STREAMINFO for granule position calculation + let sample_rate = extract_sample_rate_from_streaminfo(&flac_header)?; + info!("Extracted sample rate from STREAMINFO: {} Hz", sample_rate); + // Step 2: Create OGG-FLAC identification packet (BOS) // Format according to https://xiph.org/flac/ogg_mapping.html let ogg_flac_id = create_ogg_flac_identification(&flac_header)?; @@ -822,19 +856,20 @@ async fn broadcast_ogg_flac_stream( flac_accumulator.extend_from_slice(&read_buffer[..n]); // Find where to split: position of last sync code (start of last incomplete frame) - // Everything before this position contains only complete frames - let boundary = find_complete_frames_boundary(&flac_accumulator); + // Also calculate total samples for granule position + let (boundary, samples_in_frames) = find_complete_frames_with_samples(&flac_accumulator); trace!( - "Buffer state: accumulator={} bytes, boundary={} bytes, will_send={}", + "Buffer state: accumulator={} bytes, boundary={} bytes, samples={}, will_send={}", flac_accumulator.len(), boundary, + samples_in_frames, boundary >= 4096 ); // Only broadcast if we have at least one complete frame (4KB minimum for efficiency) // OGG pages can be larger than pure FLAC broadcasts since they include page overhead - if boundary >= 4096 { + if boundary >= 4096 && samples_in_frames > 0 { // Precise pacing based on audio timestamp let audio_timestamp = *current_timestamp.read().await; let elapsed = start_time.elapsed().as_secs_f64(); @@ -853,6 +888,9 @@ async fn broadcast_ogg_flac_stream( let remaining = flac_accumulator.split_off(boundary); let complete_frames = std::mem::replace(&mut flac_accumulator, remaining); + // Update granule position (cumulative sample count) + ogg_writer.add_samples(samples_in_frames); + // Wrap complete FLAC frames in OGG page let ogg_page = ogg_writer.create_page(&complete_frames, false, false, false); let ogg_bytes = Bytes::from(ogg_page); @@ -861,7 +899,7 @@ async fn broadcast_ogg_flac_stream( if let Err(e) = broadcast_tx.send(ogg_bytes.clone()) { trace!("No active receivers for OGG-FLAC broadcast: {}", e); } else { - trace!("Broadcasted OGG page with {} bytes of FLAC data ({} bytes total with OGG overhead)", complete_frames.len(), ogg_bytes.len()); + trace!("Broadcasted OGG page with {} bytes of FLAC data, {} samples ({} bytes total with OGG overhead)", complete_frames.len(), samples_in_frames, ogg_bytes.len()); } } } @@ -1019,7 +1057,7 @@ fn create_empty_vorbis_comment() -> Vec { block } -/// OGG page writer (same as in pmoflac::ogg_flac_encoder) +/// OGG page writer with granule position tracking for FLAC struct OggPageWriter { stream_serial: u32, page_sequence: u32, @@ -1035,6 +1073,11 @@ impl OggPageWriter { } } + /// Add samples to the granule position (for FLAC: cumulative PCM sample count) + fn add_samples(&mut self, samples: u64) { + self.granule_position += samples; + } + fn create_page(&mut self, packet_data: &[u8], is_bos: bool, is_eos: bool, is_continuation: bool) -> Vec { use std::io::Write;