From c947904d0fe056cc3205d321ebf61e3fa0123c07 Mon Sep 17 00:00:00 2001 From: Jamie Hardt Date: Thu, 10 Dec 2020 12:26:49 -0800 Subject: [PATCH] Cue point implementation --- src/cue.rs | 253 +++++++++++++++++++++++++++++++++++++++++++++ src/fourcc.rs | 7 ++ src/lib.rs | 3 + src/list_form.rs | 31 ++++++ src/wavereader.rs | 21 +++- tests/Untitled.txt | 5 + 6 files changed, 319 insertions(+), 1 deletion(-) create mode 100644 src/cue.rs create mode 100644 src/list_form.rs create mode 100644 tests/Untitled.txt diff --git a/src/cue.rs b/src/cue.rs new file mode 100644 index 0000000..bf43f18 --- /dev/null +++ b/src/cue.rs @@ -0,0 +1,253 @@ +use super::fourcc::{FourCC,ReadFourCC, LABL_SIG, NOTE_SIG, LTXT_SIG}; +use super::list_form::collect_list_form; +use byteorder::{ReadBytesExt, LittleEndian}; + +use std::io::{Cursor, Error, Read}; +use std::str; + +#[derive(Copy,Clone, Debug)] +struct RawCue { + cue_point_id : u32, + frame : u32, + chunk_id : FourCC, + chunk_start : u32, + block_start : u32, + frame_offset : u32 +} + +impl RawCue { + fn read_from(data : &[u8]) -> Result,Error> { + let mut rdr = Cursor::new(data); + let count = rdr.read_u32::()?; + let mut retval : Vec = vec![]; + + for _ in 0..count { + retval.push( Self { + cue_point_id : rdr.read_u32::()?, + frame : rdr.read_u32::()?, + chunk_id : rdr.read_fourcc()?, + chunk_start : rdr.read_u32::()?, + block_start : rdr.read_u32::()?, + frame_offset : rdr.read_u32::()? + }) + } + + Ok( retval ) + } +} + +#[derive(Clone, Debug)] +struct RawLabel { + cue_point_id : u32, + text : Vec +} + +impl RawLabel { + fn read_from(data : &[u8]) -> Result { + let mut rdr = Cursor::new(data); + let length = data.len(); + + Ok( Self { + cue_point_id : rdr.read_u32::()?, + text : { + let mut buf = vec![0u8; (length - 4) as usize ]; + rdr.read_exact(&mut buf)?; + if buf.len() % 2 == 1 { rdr.read_u8()?; }; + buf + // String::from_utf8(buf).unwrap_or(String::from("")) + } + }) + } +} + +#[derive(Clone, Debug)] +struct RawNote { + cue_point_id : u32, + text : Vec +} + +impl RawNote { + fn read_from(data : &[u8]) -> Result { + let mut rdr = Cursor::new(data); + let length = data.len(); + + Ok( Self { + cue_point_id : rdr.read_u32::()?, + text : { + let mut buf = vec![0u8; (length - 4) as usize ]; + rdr.read_exact(&mut buf)?; + if length % 2 == 1 { rdr.read_u8()?; }; + buf + //String::from_utf8(buf).unwrap_or(String::from("")) + } + }) + } +} + +#[derive(Clone, Debug)] +struct RawLtxt { + cue_point_id : u32, + frame_length : u32, + purpose : FourCC, + country : u16, + language : u16, + dialect : u16, + code_page : u16, + text: Option> +} + +impl RawLtxt { + fn read_from(data : &[u8]) -> Result { + let mut rdr = Cursor::new(data); + let length = data.len(); + + Ok( Self { + cue_point_id : rdr.read_u32::()?, + frame_length : rdr.read_u32::()?, + purpose : rdr.read_fourcc()?, + country : rdr.read_u16::()?, + language : rdr.read_u16::()?, + dialect : rdr.read_u16::()?, + code_page : rdr.read_u16::()?, + text : { + if length - 20 > 0 { + let mut buf = vec![0u8; (length - 20) as usize]; + rdr.read_exact(&mut buf)?; + if length % 2 == 1 { rdr.read_u8()?; }; + Some( buf ) + //Some( String::from_utf8(buf).unwrap_or(String::from("")) ) + } else { + None + } + } + }) + } +} + +#[derive(Clone, Debug)] +enum RawAdtlMember { + Label(RawLabel), + Note(RawNote), + LabeledText(RawLtxt), + Unrecognized(FourCC) +} + +impl RawAdtlMember { + fn collect_from(chunk : &[u8]) -> Result,Error> { + let chunks = collect_list_form(chunk)?; + let mut retval : Vec = vec![]; + + for chunk in chunks.iter() { + retval.push( + match chunk.signature { + LABL_SIG => RawAdtlMember::Label( RawLabel::read_from(&chunk.contents)? ), + NOTE_SIG => RawAdtlMember::Note( RawNote::read_from(&chunk.contents)? ), + LTXT_SIG => RawAdtlMember::LabeledText( RawLtxt::read_from(&chunk.contents)? ), + x => RawAdtlMember::Unrecognized(x) + } + ) + } + Ok( retval ) + } +} + +trait AdtlMemberSearch { + fn labels_for_cue_point(&self, id: u32) -> Vec<&RawLabel>; + fn notes_for_cue_point(&self, id : u32) -> Vec<&RawNote>; + fn ltxt_for_cue_point(&self, id: u32) -> Vec<&RawLtxt>; +} + +impl AdtlMemberSearch for Vec { + + fn labels_for_cue_point(&self, id: u32) -> Vec<&RawLabel> { + self.iter().filter_map(|item| { + match item { + RawAdtlMember::Label(x) if x.cue_point_id == id => Some(x), + _ => None + } + }) + .collect() + } + + fn notes_for_cue_point(&self, id: u32) -> Vec<&RawNote> { + self.iter().filter_map(|item| { + match item { + RawAdtlMember::Note(x) if x.cue_point_id == id => Some(x), + _ => None + } + }) + .collect() + } + + fn ltxt_for_cue_point(&self, id: u32) -> Vec<&RawLtxt> { + self.iter().filter_map(|item| { + match item { + RawAdtlMember::LabeledText(x) if x.cue_point_id == id => Some(x), + _ => None + } + }) + .collect() + } +} + +/// A cue point recorded in the `cue` and `adtl` metadata. +/// +/// +pub struct Cue { + + /// Unique numeric identifier for this cue + pub ident : u32, + + /// The time of this marker + pub frame : u32, + + /// The length of this marker, if it is a range + pub length : Option, + + /// The text "label"/name of this marker if provided + pub label : Option, + + /// The text "note"/comment of this marker if provided + pub note : Option +} + + +impl Cue { + + pub fn collect_from(cue_chunk : &[u8], adtl_chunk : Option<&[u8]>) -> Result, Error> { + let raw_cues = RawCue::read_from(cue_chunk)?; + let raw_adtl : Vec; + + if let Some(adtl) = adtl_chunk { + raw_adtl = RawAdtlMember::collect_from(adtl)?; + } else { + raw_adtl = vec![]; + } + + Ok( + raw_cues.iter() + .map(|i| { + Cue { + ident : i.cue_point_id, + frame : i.frame, + length: { + raw_adtl.ltxt_for_cue_point(i.cue_point_id).first().map(|x| x.frame_length) + }, + label: { + raw_adtl.labels_for_cue_point(i.cue_point_id).iter() + .filter_map(|x| str::from_utf8(&x.text).ok()) + .map(|s| String::from(s)) + .next() + }, + note : { + raw_adtl.notes_for_cue_point(i.cue_point_id).iter() + .filter_map(|x| str::from_utf8(&x.text).ok()) + .map(|s| String::from(s)) + .next() + } + } + }).collect() + ) + } + +} \ No newline at end of file diff --git a/src/fourcc.rs b/src/fourcc.rs index 7bdc3b8..dc852fc 100644 --- a/src/fourcc.rs +++ b/src/fourcc.rs @@ -106,6 +106,13 @@ pub const BEXT_SIG: FourCC = FourCC::make(b"bext"); pub const JUNK_SIG: FourCC = FourCC::make(b"JUNK"); pub const FLLR_SIG: FourCC = FourCC::make(b"FLLR"); +pub const CUE__SIG: FourCC = FourCC::make(b"cue "); +pub const ADTL_SIG: FourCC = FourCC::make(b"adtl"); +pub const LABL_SIG: FourCC = FourCC::make(b"labl"); +pub const NOTE_SIG: FourCC = FourCC::make(b"note"); +pub const LTXT_SIG: FourCC = FourCC::make(b"ltxt"); + + #[cfg(test)] mod tests { use super::*; diff --git a/src/lib.rs b/src/lib.rs index a595570..095a8b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,7 +113,10 @@ mod parser; mod raw_chunk_reader; mod audio_frame_reader; +mod list_form; + mod chunks; +mod cue; mod bext; mod fmt; diff --git a/src/list_form.rs b/src/list_form.rs new file mode 100644 index 0000000..61046d7 --- /dev/null +++ b/src/list_form.rs @@ -0,0 +1,31 @@ +use super::fourcc::{FourCC, ReadFourCC}; +use byteorder::{ReadBytesExt, LittleEndian}; +use std::io::{Cursor, Error, Read}; + +pub struct ListFormItem { + pub signature : FourCC, + pub contents : Vec +} + +/// A helper that will accept a LIST chunk as a [u8] +/// and give you back each segment +/// +pub fn collect_list_form(list_contents :& [u8]) -> Result, Error> { + let mut cursor = Cursor::new(list_contents); + let mut remain = list_contents.len(); + let _ = cursor.read_fourcc()?; // skip signature + + remain -= 4; + let mut retval : Vec = vec![]; + + while remain > 0 { + let this_sig = cursor.read_fourcc()?; + let this_size = cursor.read_u32::()? as usize; + let mut content_buf = vec![0u8; this_size]; + + cursor.read_exact(&mut content_buf)?; + retval.push( ListFormItem { signature : this_sig, contents : content_buf } ); + } + + Ok( retval ) +} \ No newline at end of file diff --git a/src/wavereader.rs b/src/wavereader.rs index 89a97f9..b3986e4 100644 --- a/src/wavereader.rs +++ b/src/wavereader.rs @@ -2,13 +2,14 @@ use std::fs::File; use super::parser::Parser; -use super::fourcc::{FourCC, FMT__SIG,DATA_SIG, BEXT_SIG, JUNK_SIG, FLLR_SIG}; +use super::fourcc::{FourCC, FMT__SIG,DATA_SIG, BEXT_SIG, JUNK_SIG, FLLR_SIG, CUE__SIG, ADTL_SIG}; use super::errors::Error as ParserError; use super::raw_chunk_reader::RawChunkReader; use super::fmt::{WaveFmt, ChannelDescriptor, ChannelMask}; use super::bext::Bext; use super::audio_frame_reader::AudioFrameReader; use super::chunks::ReadBWaveChunks; +use super::cue::Cue; use std::io::Cursor; use std::io::{Read, Seek}; @@ -171,6 +172,24 @@ impl WaveReader { .collect() ) } + /// Read cue points. + /// + /// + pub fn cue_points(&mut self) -> Result,ParserError> { + let mut cue_buffer : Vec = vec![]; + let mut adtl_buffer : Vec = vec![]; + + let cue_read = self.read_chunk(CUE__SIG, 0, &mut cue_buffer)?; + let adtl_read = self.read_chunk(ADTL_SIG, 0, &mut adtl_buffer)?; + + match (cue_read, adtl_read) { + (0,_) => Ok( vec![] ), + (_,0) => Ok( Cue::collect_from(&cue_buffer, None)? ), + (_,_) => Ok( Cue::collect_from(&cue_buffer, Some(&adtl_buffer) )? ) + } + + } + /// Read iXML data. /// /// The iXML data will be appended to `buffer`. diff --git a/tests/Untitled.txt b/tests/Untitled.txt new file mode 100644 index 0000000..a2ea4d2 --- /dev/null +++ b/tests/Untitled.txt @@ -0,0 +1,5 @@ +Marker file version: 1 +Time format: Time +Marker 1 00:00:00.28417234 Marker 1 Comment +Marker 2 00:00:00.47612245 Marker 2 Comment +Timed Region 00:00:00.60569161 00:00:00.75226757 Region Comment