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.
This commit is contained in:
Claude
2025-11-13 09:45:17 +00:00
parent 1e8eedc253
commit 3219a8beda

View File

@@ -700,44 +700,73 @@ fn chunk_to_pcm_bytes(chunk: &AudioChunk, bits_per_sample: u8) -> Result<Vec<u8>
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<u32> {
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<u8> {
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<u8> {
use std::io::Write;