Skip to main content

ogg_table/
lib.rs

1use anyhow::Result;
2
3pub mod ogg;
4pub mod ogg_vorbis;
5pub mod vorbis;
6
7pub use ogg_vorbis::OggVorbisReader;
8
9#[derive(Debug, Clone)]
10pub struct Entry {
11    pub file_pos: u64,
12    pub granule_position: u64,
13}
14
15#[derive(Debug, Clone)]
16pub struct TableOfContent {
17    pub entries: Vec<Entry>,
18}
19
20impl TableOfContent {
21    /// Build a table of content from the headers of a ogg file.
22    pub fn from_ogg_reader<R: std::io::Read + std::io::Seek>(rdr: &mut R) -> Result<Self> {
23        let all_headers = ogg::all_headers(rdr)?;
24        let entries: Vec<_> = all_headers
25            .into_iter()
26            .map(|(file_pos, hdr)| Entry { file_pos, granule_position: hdr.granule_position })
27            .collect();
28        Ok(Self { entries })
29    }
30
31    /// Read a table of content file.
32    ///
33    /// This uses a very simple serialization format.
34    pub fn from_reader<R: std::io::Read>(rdr: &mut R) -> Result<Self> {
35        use byteorder::{LittleEndian, ReadBytesExt};
36
37        let mut entries = Vec::new();
38        while let Ok(file_pos) = rdr.read_u64::<LittleEndian>() {
39            let granule_position = rdr.read_u64::<LittleEndian>().unwrap();
40            entries.push(Entry { file_pos, granule_position });
41        }
42        Ok(Self { entries })
43    }
44
45    pub fn write<W: std::io::Write>(&self, w: &mut W) -> Result<()> {
46        use byteorder::{LittleEndian, WriteBytesExt};
47        for entry in self.entries.iter() {
48            w.write_u64::<LittleEndian>(entry.file_pos)?;
49            w.write_u64::<LittleEndian>(entry.granule_position)?;
50        }
51        Ok(())
52    }
53
54    pub fn last_entry_before(&self, start_pos: u64) -> Option<&Entry> {
55        let packet_idx = self.entries.partition_point(|entry| entry.granule_position < start_pos);
56        let mut packet_idx = packet_idx.saturating_sub(1);
57        while packet_idx < self.entries.len() && self.entries[packet_idx].granule_position == 0 {
58            packet_idx += 1;
59        }
60        self.entries.get(packet_idx)
61    }
62}
63
64/// Read a sample of data at the given start time and for the target duration from a file. This
65/// uses a table file if available and otherwise fallsback to a linear scan.
66pub fn read_ogg_vorbis_sample<P: AsRef<std::path::Path>>(
67    p: P,
68    start_time_sec: f64,
69    duration_sec: f64,
70) -> Result<(Vec<Vec<f32>>, u32)> {
71    let mut ovr = {
72        let file = std::fs::File::open(p.as_ref())?;
73        let reader = std::io::BufReader::new(file);
74        OggVorbisReader::new(reader)?
75    };
76    let sample_rate = ovr.sample_rate();
77    let start_pos = (start_time_sec * sample_rate as f64) as u64;
78    let duration = (duration_sec * sample_rate as f64) as u64;
79
80    let table_path = p.as_ref().with_extension("ogg_table");
81    let granule_pos = if table_path.is_file() {
82        let table_file = std::fs::File::open(&table_path)?;
83        let mut table_reader = std::io::BufReader::new(table_file);
84        let toc = TableOfContent::from_reader(&mut table_reader)?;
85        let entry = match toc.last_entry_before(start_pos) {
86            None => anyhow::bail!("not enough audio packets in file"),
87            Some(entry) => entry,
88        };
89        ovr.seek(entry.file_pos, true)?
90    } else {
91        ovr.seek_granule_position(start_pos, true)?
92    };
93    let to_skip = start_pos.saturating_sub(granule_pos);
94    let data = ovr.decode(to_skip as usize, duration as usize)?;
95    Ok((data, sample_rate))
96}