Skip to main content

libflo_audio/
seeking.rs

1/// TOC-based seeking and frame access functionality
2/// gives access to frames without fully decoding the entire file.
3use crate::core::{FloFile, FloResult, TocEntry};
4use crate::reader::Reader;
5use alloc::format;
6use alloc::vec;
7use alloc::vec::Vec;
8
9/// Represents the result of a seek operation
10#[derive(Debug, Clone)]
11pub struct SeekResult {
12    /// Frame index where seeking occurred
13    pub frame_index: u32,
14    /// Byte offset in the file's data chunk
15    pub byte_offset: u64,
16    /// Timestamp of this frame in milliseconds
17    pub timestamp_ms: u32,
18    /// Number of samples to skip at the start of the frame (for sub-frame seeking)
19    pub sample_offset: u32,
20    /// The next frame's timestamp for duration calculation
21    pub next_timestamp_ms: u32,
22}
23
24/// Extract TOC entries from a flo file
25///
26/// # Arguments
27/// * `flo_data` - Complete flo file bytes
28///
29/// # Returns
30/// Vector of TOC entries with frame indices, byte offsets, and timestamps
31pub fn get_toc(flo_data: &[u8]) -> FloResult<Vec<TocEntry>> {
32    let reader = Reader::new();
33    let file = reader.read(flo_data)?;
34    Ok(file.toc)
35}
36
37/// Decode a single frame at a specific index without decoding the entire file
38///
39/// # Arguments
40/// * `flo_data` - Complete flo file bytes
41/// * `frame_index` - Zero-based frame index
42///
43/// # Returns
44/// Raw interleaved audio samples for that frame (f32, -1.0 to 1.0)
45///
46/// # Note
47/// This function decodes only the requested frame
48pub fn decode_frame_at(flo_data: &[u8], frame_index: u32) -> FloResult<Vec<f32>> {
49    let reader = Reader::new();
50    let file = reader.read(flo_data)?;
51
52    if (frame_index as usize) >= file.frames.len() {
53        return Err(crate::core::FloError::from(format!(
54            "Frame index {} out of bounds (total frames: {})",
55            frame_index,
56            file.frames.len()
57        )));
58    }
59
60    let frame = &file.frames[frame_index as usize];
61
62    // Detect if this is a lossy (transform) frame
63    let is_transform = frame.frame_type == (crate::FrameType::Transform as u8);
64
65    if is_transform {
66        // Decode lossy frame
67        decode_frame_lossy(&file, frame_index as usize)
68    } else {
69        // Decode lossless frame
70        decode_frame_lossless(&file, frame_index as usize)
71    }
72}
73
74/// Seek to a specific time in milliseconds and get frame information
75///
76/// # Arguments
77/// * `flo_data` - Complete flo file bytes
78/// * `time_ms` - Target time in milliseconds
79///
80/// # Returns
81/// SeekResult with frame information and sample offset for sub-frame seeking
82pub fn seek_to_time(flo_data: &[u8], target_ms: u32) -> FloResult<SeekResult> {
83    let reader = Reader::new();
84    let file = reader.read(flo_data)?;
85
86    if file.toc.is_empty() {
87        return Err("No TOC available for seeking".into());
88    }
89    if file.frames.is_empty() {
90        return Err("TOC contains no decodable frames".into());
91    }
92
93    // Binary search for the frame containing target_ms
94    let mut frame_index = binary_search_frame(&file.toc, target_ms);
95
96    // Clamp to valid range
97    if (frame_index as usize) >= file.frames.len() {
98        frame_index = (file.frames.len() - 1) as u32;
99    }
100
101    let toc_entry = &file.toc[frame_index as usize];
102
103    // Calculate sample offset within this frame for sub-frame accuracy
104    let frame_duration_ms = if frame_index + 1 < (file.toc.len() as u32) {
105        file.toc[(frame_index + 1) as usize].timestamp_ms - toc_entry.timestamp_ms
106    } else {
107        // Last frame: estimate from duration
108        let last_frame_samples = file.frames[frame_index as usize].frame_samples;
109        (((last_frame_samples as u64) * 1000) / (file.header.sample_rate as u64)) as u32
110    };
111
112    // How far into this frame should we start?
113    let ms_into_frame = target_ms.saturating_sub(toc_entry.timestamp_ms);
114
115    // Convert to sample offset
116    let sample_offset = (((ms_into_frame as u64) * (file.header.sample_rate as u64)) / 1000) as u32;
117
118    // Clamp to actual frame size
119    let frame = &file.frames[frame_index as usize];
120    let sample_offset = sample_offset.min(frame.frame_samples);
121
122    let next_timestamp_ms = if frame_index + 1 < (file.toc.len() as u32) {
123        file.toc[(frame_index + 1) as usize].timestamp_ms
124    } else {
125        // Estimate next frame time based on last frame samples
126        toc_entry.timestamp_ms + frame_duration_ms
127    };
128
129    Ok(SeekResult {
130        frame_index,
131        byte_offset: toc_entry.byte_offset,
132        timestamp_ms: toc_entry.timestamp_ms,
133        sample_offset,
134        next_timestamp_ms,
135    })
136}
137
138/// Find the best frame for a given timestamp using binary search
139/// Returns the frame index that should be played for the given time
140fn binary_search_frame(toc: &[TocEntry], target_ms: u32) -> u32 {
141    if toc.is_empty() {
142        return 0;
143    }
144
145    let mut left = 0;
146    let mut right = toc.len() - 1;
147
148    // Binary search for the rightmost frame where timestamp_ms <= target_ms
149    while left < right {
150        let mid = left + (right - left).div_ceil(2);
151
152        if toc[mid].timestamp_ms <= target_ms {
153            left = mid;
154        } else {
155            right = mid - 1;
156        }
157    }
158
159    left as u32
160}
161
162/// Internal: Decode a lossless frame
163fn decode_frame_lossless(file: &FloFile, frame_index: usize) -> FloResult<Vec<f32>> {
164    let frame = &file.frames[frame_index];
165    let decoder = crate::Decoder::new();
166
167    // Create a temporary FloFile with just this frame
168    let frames = vec![frame.clone()];
169    let temp_file = FloFile {
170        header: file.header.clone(),
171        toc: file.toc.clone(),
172        frames,
173        extra: vec![],
174        metadata: file.metadata.clone(),
175    };
176
177    decoder.decode_file(&temp_file)
178}
179
180/// Internal: Decode a lossy frame
181fn decode_frame_lossy(file: &FloFile, frame_index: usize) -> FloResult<Vec<f32>> {
182    let frame = &file.frames[frame_index];
183
184    if frame.channels.is_empty() {
185        return Err("Transform frame has no channel data".into());
186    }
187
188    // Transform data is in first channel's residuals
189    let frame_data = &frame.channels[0].residuals;
190
191    if let Some(transform_frame) = crate::lossy::deserialize_frame(frame_data) {
192        let mut decoder =
193            crate::lossy::TransformDecoder::new(file.header.sample_rate, file.header.channels);
194
195        // The MDCT decoder's only state is the overlap buffer, which is the
196        // immediately preceding frame's IMDCT second half
197        if frame_index > 0 {
198            let prev = &file.frames[frame_index - 1];
199            if !prev.channels.is_empty() {
200                let ch_data = &prev.channels[0].residuals;
201                if let Some(tf) = crate::lossy::deserialize_frame(ch_data) {
202                    let _ = decoder.decode_frame(&tf);
203                }
204            }
205        }
206
207        // Now decode the target frame
208        Ok(decoder.decode_frame(&transform_frame))
209    } else {
210        Err("Failed to deserialize transform frame".into())
211    }
212}