use chdlady_av::avhuff::raw_frame_size;
use chdlady_av::avi::{AviChunk, AviInfo, AviReader, AviWriter};
use chdlady_core::{
ChdFile, ChdWriteConfig, ChdWriter, CreateResult, MetadataItem, OperationPhase, ProgressStatus,
};
use std::collections::VecDeque;
use std::fs::File;
use std::io::{self, BufReader, BufWriter, Read, Seek};
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
pub const AV_METADATA_TAG: u32 = 0x41564156;
pub const AV_LD_METADATA_TAG: u32 = 0x41564c44;
pub const DEFAULT_LD_CODECS: [u32; 4] = [u32::from_be_bytes(*b"avhu"), 0, 0, 0];
#[derive(Debug)]
pub enum LdError {
Io(std::io::Error),
Avi(chdlady_av::avi::AviError),
Chd(chdlady_core::ChdError),
Invalid(String),
}
impl std::fmt::Display for LdError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LdError::Io(e) => write!(f, "I/O error: {}", e),
LdError::Avi(e) => write!(f, "AVI error: {}", e),
LdError::Chd(e) => write!(f, "CHD error: {}", e),
LdError::Invalid(s) => write!(f, "Invalid parameter: {}", s),
}
}
}
impl std::error::Error for LdError {}
impl From<std::io::Error> for LdError {
fn from(e: std::io::Error) -> Self {
LdError::Io(e)
}
}
impl From<chdlady_av::avi::AviError> for LdError {
fn from(e: chdlady_av::avi::AviError) -> Self {
LdError::Avi(e)
}
}
impl From<chdlady_core::ChdError> for LdError {
fn from(e: chdlady_core::ChdError) -> Self {
LdError::Chd(e)
}
}
#[derive(Debug, Clone, Default)]
pub struct CreateLdOptions {
pub start_frame: Option<u64>,
pub end_frame: Option<u64>,
pub hunk_size: Option<u32>,
pub codecs: Option<[u32; 4]>,
pub parent_path: Option<PathBuf>,
pub num_processors: Option<usize>,
pub cancel_token: Option<Arc<AtomicBool>>,
}
#[derive(Debug, Clone, Default)]
pub struct ExtractLdOptions {
pub start_frame: Option<u64>,
pub end_frame: Option<u64>,
pub parent_path: Option<PathBuf>,
}
#[derive(Debug, Clone)]
pub struct LdCreateInfo {
pub output_path: PathBuf,
pub parent_path: Option<PathBuf>,
pub input_path: PathBuf,
pub input_start: u64,
pub input_length: u64,
pub total_samples: u64,
pub fps_times_1million: u64,
pub width: u32,
pub height: u32,
pub interlaced: bool,
pub channels: u32,
pub sample_rate: u32,
pub hunk_size: u32,
pub logical_bytes: u64,
pub codecs: [u32; 4],
}
pub fn inspect_create_ld(
input_path: &Path,
output_path: &Path,
options: &CreateLdOptions,
) -> Result<LdCreateInfo, LdError> {
let file = File::open(input_path)?;
let reader = AviReader::open(BufReader::with_capacity(2 * 1024 * 1024, file))?;
let info = reader.info.clone();
let mut fps_times_1million = info.fps_times_1million;
let width = info.width;
let mut height = info.height;
let interlaced =
((fps_times_1million / 1_000_000) <= 30) && height.is_multiple_of(2) && (height > 288);
let channels = info.channels;
let rate = info.sample_rate;
if interlaced {
fps_times_1million *= 2;
height /= 2;
}
let max_samples_per_frame = (rate as u64 * 1_000_000).div_ceil(fps_times_1million as u64);
let bytes_per_frame = raw_frame_size(width, height, channels, max_samples_per_frame as u32);
let hunk_size = options.hunk_size.unwrap_or(bytes_per_frame);
let interlace_factor: u64 = if interlaced { 2 } else { 1 };
let total_fields = if reader.info.total_frames > 0 {
reader.info.total_frames * interlace_factor
} else {
return Err(LdError::Invalid(
"AVI file does not specify total frames in header".into(),
));
};
let input_start = options.start_frame.unwrap_or(0) * interlace_factor;
let input_end = options
.end_frame
.map(|f| (f * interlace_factor).min(total_fields))
.unwrap_or(total_fields);
if input_start >= input_end {
return Err(LdError::Invalid(format!(
"start frame {} is >= end frame {}",
input_start / interlace_factor,
input_end / interlace_factor
)));
}
let field_count = input_end - input_start;
let logical_bytes = field_count * hunk_size as u64;
let codecs = options.codecs.unwrap_or(DEFAULT_LD_CODECS);
Ok(LdCreateInfo {
output_path: output_path.to_path_buf(),
parent_path: options.parent_path.clone(),
input_path: input_path.to_path_buf(),
input_start,
input_length: field_count,
total_samples: total_fields,
fps_times_1million: fps_times_1million as u64,
width,
height,
interlaced,
channels,
sample_rate: rate,
hunk_size,
logical_bytes,
codecs,
})
}
pub fn create_ld<F>(
input_path: &Path,
output_path: &Path,
options: &CreateLdOptions,
mut progress: F,
) -> Result<CreateResult, LdError>
where
F: FnMut(ProgressStatus) + Send,
{
let file = File::open(input_path)?;
let reader = AviReader::open(BufReader::with_capacity(2 * 1024 * 1024, file))?;
let info = reader.info.clone();
let mut fps_times_1million = info.fps_times_1million;
let width = info.width;
let mut height = info.height;
let interlaced =
((fps_times_1million / 1_000_000) <= 30) && height.is_multiple_of(2) && (height > 288);
let channels = info.channels;
let rate = info.sample_rate;
if interlaced {
fps_times_1million *= 2;
height /= 2;
}
let max_samples_per_frame = (rate as u64 * 1_000_000).div_ceil(fps_times_1million as u64);
let bytes_per_frame = raw_frame_size(width, height, channels, max_samples_per_frame as u32);
let hunk_size = options.hunk_size.unwrap_or(bytes_per_frame);
let interlace_factor: u64 = if interlaced { 2 } else { 1 };
let total_fields = if reader.info.total_frames > 0 {
reader.info.total_frames * interlace_factor
} else {
return Err(LdError::Invalid(
"AVI file does not specify total frames in header".into(),
));
};
let input_start = options.start_frame.unwrap_or(0) * interlace_factor;
let input_end = options
.end_frame
.map(|f| (f * interlace_factor).min(total_fields))
.unwrap_or(total_fields);
if input_start >= input_end {
return Err(LdError::Invalid(format!(
"start frame {} is >= end frame {}",
input_start / interlace_factor,
input_end / interlace_factor
)));
}
let field_count = input_end - input_start;
let logical_bytes = field_count * hunk_size as u64;
let compressors = options.codecs.unwrap_or(DEFAULT_LD_CODECS);
let mut parent_sha1 = [0u8; 20];
let parent_map = if let Some(parent_p) = &options.parent_path {
let parent_f = File::open(parent_p)?;
let mut parent_file = ChdFile::open(BufReader::new(parent_f))?;
parent_sha1 = parent_file.header().sha1;
Some(ChdWriter::build_parent_map(
&mut parent_file,
hunk_size,
bytes_per_frame,
)?)
} else {
None
};
let config = ChdWriteConfig {
logical_bytes,
hunk_bytes: hunk_size,
unit_bytes: bytes_per_frame,
compressors,
parent_sha1,
num_processors: options.num_processors,
cancel_token: options.cancel_token.clone(),
};
let metadata_str = format!(
"FPS:{}.{:06} WIDTH:{} HEIGHT:{} INTERLACED:{} CHANNELS:{} SAMPLERATE:{}",
fps_times_1million / 1_000_000,
fps_times_1million % 1_000_000,
width,
height,
if interlaced { 1 } else { 0 },
channels,
rate
);
let metadata = vec![MetadataItem {
tag: AV_METADATA_TAG,
flags: 0,
value: metadata_str.into_bytes(),
}];
let mut stream_reader = LdStreamReader::new(
reader,
input_start,
input_end,
interlace_factor,
interlaced,
width as usize,
height as usize,
channels as usize,
rate,
fps_times_1million,
hunk_size as usize,
);
let output_file = File::create(output_path)?;
let mut writer = BufWriter::with_capacity(2 * 1024 * 1024, output_file);
let result = ChdWriter::write_chd_with_parent(
&mut stream_reader,
&mut writer,
&config,
&metadata,
parent_map.as_ref(),
&mut progress,
);
match result {
Ok(res) => Ok(res),
Err(e) => {
let _ = std::fs::remove_file(output_path);
Err(LdError::Chd(e))
}
}
}
pub fn extract_ld<F>(
input_path: &Path,
output_path: &Path,
options: &ExtractLdOptions,
mut progress: F,
) -> Result<(), LdError>
where
F: FnMut(ProgressStatus) + Send,
{
let chd_f = File::open(input_path)?;
let mut chd = ChdFile::open(BufReader::with_capacity(1024 * 1024, chd_f))?;
let metadata_entries = chd.read_all_metadata()?;
let av_entry = metadata_entries
.into_iter()
.find(|e| e.metatag == AV_METADATA_TAG)
.ok_or_else(|| LdError::Invalid("Unable to find A/V metadata in the input CHD".into()))?;
let metadata_str = std::str::from_utf8(&av_entry.value)
.map_err(|_| LdError::Invalid("Improperly formatted A/V metadata".into()))?;
let (fps_times_1million, width, height, interlaced, channels, rate) =
parse_av_metadata(metadata_str)?;
let interlace_factor = if interlaced { 2 } else { 1 };
let avi_info = AviInfo {
fps_times_1million: fps_times_1million / (interlace_factor as u32),
width,
height: height * (interlace_factor as u32),
channels,
sample_rate: rate,
audio_samples: 0,
total_frames: chd.header().hunk_count() / interlace_factor,
};
let out_file = File::create(output_path)?;
let mut writer = AviWriter::new(
BufWriter::with_capacity(2 * 1024 * 1024, out_file),
avi_info,
)?;
let input_start = options.start_frame.unwrap_or(0) * interlace_factor;
let total_hunks = chd.header().hunk_count();
let input_end = options
.end_frame
.map(|f| (f * interlace_factor).min(total_hunks))
.unwrap_or(total_hunks);
let full_frame_height = height * (interlace_factor as u32);
let mut full_frame = vec![0u8; (width * full_frame_height * 2) as usize];
let row_bytes = (width * 2) as usize;
let mut samples: Vec<i16> = Vec::new();
let count = (input_end - input_start) as usize;
let mut last_framenum: Option<u64> = None;
progress(ProgressStatus {
phase: OperationPhase::Extracting,
current: 0,
total: count as u64,
ratio: None,
});
chd.read_hunks_parallel(input_start, count as u64, |framenum, hunk_buffer| {
if let Some(prev) = last_framenum {
debug_assert_eq!(
framenum,
prev + 1,
"hunks must be processed in sequential order"
);
}
last_framenum = Some(framenum);
let step = (framenum - input_start) as usize;
progress(ProgressStatus {
phase: OperationPhase::Extracting,
current: step as u64,
total: count as u64,
ratio: None,
});
if hunk_buffer.len() < 12 || &hunk_buffer[0..4] != b"chav" {
return Err(chdlady_core::ChdError::InvalidData(
"Hunk missing chav header".into(),
));
}
let metasize = hunk_buffer[4] as usize;
let num_samples = u16::from_be_bytes([hunk_buffer[6], hunk_buffer[7]]) as usize;
let field_idx = (framenum % interlace_factor) as usize;
let mut audio_offset = 12 + metasize;
for ch in 0..channels as usize {
samples.clear();
samples.reserve(num_samples);
for _ in 0..num_samples {
let s =
i16::from_be_bytes([hunk_buffer[audio_offset], hunk_buffer[audio_offset + 1]]);
samples.push(s);
audio_offset += 2;
}
writer
.append_audio_samples(ch, &samples)
.map_err(|e| chdlady_core::ChdError::InvalidData(e.to_string()))?;
}
let video_offset = audio_offset;
let field_video = &hunk_buffer[video_offset..video_offset + (width * height * 2) as usize];
if interlaced {
for y in 0..height as usize {
let dst_row = (y * 2 + field_idx) * row_bytes;
let src_row = y * row_bytes;
full_frame[dst_row..dst_row + row_bytes]
.copy_from_slice(&field_video[src_row..src_row + row_bytes]);
}
if (framenum + 1) % 2 == 0 {
writer
.append_video_frame(&full_frame)
.map_err(|e| chdlady_core::ChdError::InvalidData(e.to_string()))?;
}
} else {
writer
.append_video_frame(field_video)
.map_err(|e| chdlady_core::ChdError::InvalidData(e.to_string()))?;
}
Ok(())
})?;
progress(ProgressStatus {
phase: OperationPhase::Extracting,
current: count as u64,
total: count as u64,
ratio: None,
});
writer.finish()?;
Ok(())
}
fn parse_av_metadata(s: &str) -> Result<(u32, u32, u32, bool, u32, u32), LdError> {
let mut fps_val = 0u32;
let mut width = 0u32;
let mut height = 0u32;
let mut interlaced = false;
let mut channels = 0u32;
let mut rate = 0u32;
for part in s.split_whitespace() {
let part = part.trim_matches(|c: char| c.is_whitespace() || c == '\0');
if let Some(val) = part.strip_prefix("FPS:") {
let val = val.trim_matches(|c: char| c.is_whitespace() || c == '\0');
if let Some((int_part, frac_part)) = val.split_once('.') {
let int_num: u32 = int_part
.parse()
.map_err(|_| LdError::Invalid(format!("invalid FPS int: {}", int_part)))?;
let mut frac_str = frac_part.to_string();
while frac_str.len() < 6 {
frac_str.push('0');
}
frac_str.truncate(6);
let frac_num: u32 = frac_str
.parse()
.map_err(|_| LdError::Invalid(format!("invalid FPS frac: {}", frac_str)))?;
fps_val = int_num * 1_000_000 + frac_num;
} else {
let int_num: u32 = val
.parse()
.map_err(|_| LdError::Invalid(format!("invalid FPS: {}", val)))?;
fps_val = int_num * 1_000_000;
}
} else if let Some(val) = part.strip_prefix("WIDTH:") {
let val = val.trim_matches(|c: char| c.is_whitespace() || c == '\0');
width = val
.parse()
.map_err(|_| LdError::Invalid(format!("invalid WIDTH: {}", val)))?;
} else if let Some(val) = part.strip_prefix("HEIGHT:") {
let val = val.trim_matches(|c: char| c.is_whitespace() || c == '\0');
height = val
.parse()
.map_err(|_| LdError::Invalid(format!("invalid HEIGHT: {}", val)))?;
} else if let Some(val) = part.strip_prefix("INTERLACED:") {
let val = val.trim_matches(|c: char| c.is_whitespace() || c == '\0');
let i: u32 = val
.parse()
.map_err(|_| LdError::Invalid(format!("invalid INTERLACED: {}", val)))?;
interlaced = i != 0;
} else if let Some(val) = part.strip_prefix("CHANNELS:") {
let val = val.trim_matches(|c: char| c.is_whitespace() || c == '\0');
channels = val
.parse()
.map_err(|_| LdError::Invalid(format!("invalid CHANNELS: {}", val)))?;
} else if let Some(val) = part
.strip_prefix("SAMPLERATE:")
.or_else(|| part.strip_prefix("RATE:"))
{
let val = val.trim_matches(|c: char| c.is_whitespace() || c == '\0');
rate = val
.parse()
.map_err(|_| LdError::Invalid(format!("invalid SAMPLERATE: {}", val)))?;
}
}
if width == 0 || height == 0 || fps_val == 0 {
return Err(LdError::Invalid("Incomplete A/V metadata".into()));
}
Ok((fps_val, width, height, interlaced, channels, rate))
}
struct LdStreamReader<R: Read + Seek> {
reader: AviReader<R>,
framenum: u64,
input_end: u64,
interlace_factor: u64,
interlaced: bool,
width: usize,
height: usize,
channels: usize,
rate: u32,
fps_times_1million: u32,
hunk_size: usize,
current_frame: Option<Vec<u8>>,
current_frame_idx: i64,
next_frames: VecDeque<Vec<u8>>,
audio_queues: Vec<VecDeque<i16>>,
audio_samples_popped: u64,
current_hunk: Vec<u8>,
hunk_offset: usize,
}
impl<R: Read + Seek> LdStreamReader<R> {
#[allow(clippy::too_many_arguments)]
fn new(
reader: AviReader<R>,
input_start: u64,
input_end: u64,
interlace_factor: u64,
interlaced: bool,
width: usize,
height: usize,
channels: usize,
rate: u32,
fps_times_1million: u32,
hunk_size: usize,
) -> Self {
Self {
reader,
framenum: input_start,
input_end,
interlace_factor,
interlaced,
width,
height,
channels,
rate,
fps_times_1million,
hunk_size,
current_frame: None,
current_frame_idx: -1,
next_frames: VecDeque::new(),
audio_queues: (0..channels).map(|_| VecDeque::new()).collect(),
audio_samples_popped: 0,
current_hunk: Vec::new(),
hunk_offset: 0,
}
}
fn fill_next_hunk(&mut self) -> io::Result<()> {
let target_frame_idx = (self.framenum / self.interlace_factor) as i64;
let field_idx = (self.framenum % self.interlace_factor) as usize;
while self.current_frame.is_none() || self.current_frame_idx < target_frame_idx {
if let Some(next) = self.next_frames.pop_front() {
self.current_frame = Some(next);
self.current_frame_idx += 1;
continue;
}
match self
.reader
.next_chunk()
.map_err(|e| io::Error::other(e.to_string()))?
{
Some(AviChunk::Video(vf)) => {
self.current_frame = Some(vf.data);
self.current_frame_idx += 1;
}
Some(AviChunk::Audio(ab)) => {
if ab.channel < self.channels {
self.audio_queues[ab.channel].extend(ab.samples);
}
}
None => break,
}
}
let first_sample =
(self.rate as u64 * self.framenum * 1_000_000).div_ceil(self.fps_times_1million as u64);
let next_sample = (self.rate as u64 * (self.framenum + 1) * 1_000_000)
.div_ceil(self.fps_times_1million as u64);
let num_samples = (next_sample - first_sample) as usize;
let old_samples = first_sample.saturating_sub(self.audio_samples_popped);
if old_samples > 0 {
for q in &mut self.audio_queues {
let drop_len = (old_samples as usize).min(q.len());
q.drain(0..drop_len);
}
self.audio_samples_popped += old_samples;
}
while self
.audio_queues
.iter()
.any(|q| self.audio_samples_popped + (q.len() as u64) < next_sample)
{
match self
.reader
.next_chunk()
.map_err(|e| io::Error::other(e.to_string()))?
{
Some(AviChunk::Audio(ab)) => {
if ab.channel < self.channels {
self.audio_queues[ab.channel].extend(ab.samples);
}
}
Some(AviChunk::Video(vf)) => {
self.next_frames.push_back(vf.data);
}
None => break,
}
}
if self.current_hunk.len() != self.hunk_size {
self.current_hunk.resize(self.hunk_size, 0);
} else {
self.current_hunk.fill(0);
}
self.current_hunk[0..4].copy_from_slice(b"chav");
self.current_hunk[4] = 0; self.current_hunk[5] = self.channels as u8;
self.current_hunk[6..8].copy_from_slice(&(num_samples as u16).to_be_bytes());
self.current_hunk[8..10].copy_from_slice(&(self.width as u16).to_be_bytes());
self.current_hunk[10..12].copy_from_slice(&(self.height as u16).to_be_bytes());
let mut audio_offset = 12usize;
for ch in 0..self.channels {
for s in 0..num_samples {
let sample_abs = first_sample + s as u64;
let sample_val = if sample_abs >= self.audio_samples_popped {
let rel_idx = (sample_abs - self.audio_samples_popped) as usize;
self.audio_queues[ch].get(rel_idx).copied().unwrap_or(0)
} else {
0
};
let be_bytes = sample_val.to_be_bytes();
self.current_hunk[audio_offset] = be_bytes[0];
self.current_hunk[audio_offset + 1] = be_bytes[1];
audio_offset += 2;
}
}
let video_offset = audio_offset;
let row_bytes = self.width * 2;
if self.interlaced {
if let Some(ref full_frame) = self.current_frame {
for y in 0..self.height {
let src_row = (y * 2 + field_idx) * row_bytes;
let dst_row = video_offset + y * row_bytes;
if src_row + row_bytes <= full_frame.len() {
self.current_hunk[dst_row..dst_row + row_bytes]
.copy_from_slice(&full_frame[src_row..src_row + row_bytes]);
}
}
}
} else if let Some(ref full_frame) = self.current_frame {
let field_len = self.width * self.height * 2;
let copy_len = field_len.min(full_frame.len());
self.current_hunk[video_offset..video_offset + copy_len]
.copy_from_slice(&full_frame[..copy_len]);
}
let to_pop = next_sample.saturating_sub(self.audio_samples_popped);
for q in &mut self.audio_queues {
let pop_len = (to_pop as usize).min(q.len());
q.drain(0..pop_len);
}
self.audio_samples_popped += to_pop;
self.framenum += 1;
self.hunk_offset = 0;
Ok(())
}
}
impl<R: Read + Seek> Read for LdStreamReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if buf.is_empty() {
return Ok(0);
}
while self.hunk_offset >= self.current_hunk.len() {
if self.framenum >= self.input_end {
return Ok(0);
}
self.fill_next_hunk()?;
}
let available = &self.current_hunk[self.hunk_offset..];
let to_copy = buf.len().min(available.len());
buf[..to_copy].copy_from_slice(&available[..to_copy]);
self.hunk_offset += to_copy;
Ok(to_copy)
}
}