filmcraft/crates/export/src/mxf_out.rs

209 lines
8.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! MXF output of the stepped exporter: OP1a (one file, frame-wrapped picture + one PCM track) or
//! OP-Atom (Avid style: the picture in `path`, one mono PCM file per audio channel next to it,
//! `<stem>_A1.mxf`, `<stem>_A2.mxf`, …), written with [`filmcraft_mxf::MxfWriter`].
use filmcraft_mxf::{
CodedKind, ColorSpace, FrameInfo, MxfWriter, PackageIds, Pattern, PictureCoding, PictureDesc, Rational, SoundDesc, StartTimecode, Timestamp, WriterConfig,
};
use filmcraft_time::FrameRate;
use crate::{ColorSignal, EncodedPacket, ExportError, ExportSettings, Format, H264Profile, Out, Result};
/// Paths of the OP-Atom audio files for an export to `path` with `channels` channels.
pub fn opatom_audio_paths(path: &str, channels: usize) -> Vec<String> {
let p = std::path::Path::new(path);
let stem = p.file_stem().map(|s| s.to_string_lossy().to_string()).unwrap_or_else(|| "export".into());
(1..=channels)
.map(|k| {
let name = format!("{stem}_A{k}.mxf");
match p.parent().filter(|d| !d.as_os_str().is_empty()) {
Some(d) => d.join(name).to_string_lossy().to_string(),
None => name,
}
})
.collect()
}
fn io(e: std::io::Error) -> ExportError {
ExportError::Io(e.to_string())
}
/// The picture essence description for an export (codec from the MXF video codec setting).
fn picture(settings: &ExportSettings, w: u32, h: u32) -> PictureDesc {
let coding = match settings.video_format() {
Format::ProRes => {
use filmcraft_prores::Profile;
PictureCoding::ProRes {
profile: match crate::prores_profile(&settings.prores_profile) {
Profile::Proxy => 1,
Profile::Lt => 2,
Profile::Standard => 3,
Profile::Hq => 4,
Profile::P4444 => 5,
Profile::P4444Xq => 6,
},
}
}
Format::H264 => PictureCoding::Avc {
profile_idc: match settings.h264_profile {
H264Profile::Baseline => 66,
H264Profile::Main => 77,
H264Profile::High => 100,
},
intra: false,
},
_ => PictureCoding::Vc3 { cid: crate::dnx_profile(&settings.dnx_profile).cid() },
};
let mut d = PictureDesc::new(coding, w, h);
d.color = match settings.signal {
s if s == ColorSignal::PQ => ColorSpace::Rec2020Pq,
s if s == ColorSignal::HLG => ColorSpace::Rec2020Hlg,
_ => ColorSpace::Rec709,
};
if let Some((n, dn)) = settings.pixel_aspect.filter(|(n, d)| *n > 0 && *d > 0 && n != d) {
let (a, b) = (w as u64 * n as u64, h as u64 * dn as u64);
let g = gcd(a, b);
d.aspect = Rational::new((a / g) as i32, (b / g) as i32);
}
d
}
fn gcd(a: u64, b: u64) -> u64 {
if b == 0 { a.max(1) } else { gcd(b, a % b) }
}
/// Everything the MXF muxer needs from the export.
pub(crate) struct MxfSetup<'a> {
pub settings: &'a ExportSettings,
pub name: String,
pub width: u32,
pub height: u32,
pub rate: FrameRate,
/// (sample rate, channels) of the audio, if any.
pub audio: Option<(u32, usize)>,
/// Start timecode in frames of `rate`, and drop-frame.
pub timecode: (i64, bool),
}
/// An MXF export in progress.
pub(crate) struct MxfMux {
video: MxfWriter<Out>,
/// OP-Atom: one writer per audio channel.
audio: Vec<(String, MxfWriter<Out>)>,
atom: bool,
bits: u32,
channels: usize,
/// Stored pictures so far, and the composition offset of the first (H.264 reorder delay).
stored: i64,
first_offset: Option<i64>,
max_display: i64,
frame_den: i64,
}
impl MxfMux {
pub fn new(s: MxfSetup) -> Result<Self> {
let settings = s.settings;
let atom = settings.format == Format::MxfOpAtom;
let rate = Rational::new(s.rate.num as i32, s.rate.den as i32);
let now = web_time::SystemTime::now().duration_since(web_time::UNIX_EPOCH).map(|d| d.as_nanos()).unwrap_or(0);
let seed = format!("{}|{now}", settings.path);
let ids = PackageIds::from_seed(&seed, &s.name);
// integers only: `ExportSettings::validate` refuses float PCM in MXF
let bits = u32::from(settings.audio.sample_format().bits());
let mut cfg = WriterConfig::new(if atom { Pattern::OpAtom } else { Pattern::Op1a }, rate, ids.clone());
cfg.picture = Some(picture(settings, s.width, s.height));
cfg.modified = Timestamp::from_unix((now / 1_000_000_000) as i64);
cfg.timecode = Some(StartTimecode { frames: s.timecode.0, rate, drop_frame: s.timecode.1 && s.rate.supports_drop_frame() });
if !atom && let Some((sr, ch)) = s.audio {
cfg.sound = vec![SoundDesc { sample_rate: sr, channels: ch as u32, bits }];
}
let mut audio = Vec::new();
if atom && let Some((sr, ch)) = s.audio {
for (k, path) in opatom_audio_paths(&settings.path, ch).into_iter().enumerate() {
// Avid style: the files share the material package; each has its own file package.
let mut a = WriterConfig::new(
Pattern::OpAtom,
rate,
PackageIds {
material: ids.material,
file: filmcraft_mxf::Umid::from_seed(format!("file:{seed}:A{}", k + 1).as_bytes()),
material_name: ids.material_name.clone(),
file_name: format!("{} A{}", s.name, k + 1),
},
);
a.first_track_id = 3 + k as u32;
a.sound = vec![SoundDesc { sample_rate: sr, channels: 1, bits }];
a.timecode = cfg.timecode;
a.modified = cfg.modified;
let out = Out::create_path(settings, &path)?;
audio.push((path, MxfWriter::new(out, a).map_err(io)?));
}
}
let video = MxfWriter::new(Out::create_path(settings, &settings.path)?, cfg).map_err(io)?;
Ok(MxfMux {
video,
audio,
atom,
bits,
channels: s.audio.map_or(0, |a| a.1),
stored: 0,
first_offset: None,
max_display: -1,
frame_den: s.rate.den.max(1),
})
}
pub fn write_video(&mut self, packets: Vec<EncodedPacket>) -> Result<()> {
for p in packets {
let intra = !matches!(self.video.config().picture.as_ref().map(|p| p.coding), Some(PictureCoding::Avc { .. }));
let info = if intra {
FrameInfo::intra()
} else {
// display position from the composition offset (pts − dts) relative to the first
let off = p.composition_offset as i64;
let first = *self.first_offset.get_or_insert(off);
let display = self.stored + (off - first) / self.frame_den;
let kind = if p.key {
CodedKind::Intra
} else if display < self.max_display {
CodedKind::Bidirectional
} else {
CodedKind::Predicted
};
self.max_display = self.max_display.max(display);
FrameInfo { key: p.key, kind, display: Some(display) }
};
self.video.push_picture(&p.data, info).map_err(io)?;
self.stored += 1;
}
Ok(())
}
pub fn write_audio(&mut self, planar: &[Vec<f32>]) -> Result<()> {
if self.channels == 0 || planar.first().is_none_or(Vec::is_empty) {
return Ok(());
}
if self.atom {
for (k, (_, w)) in self.audio.iter_mut().enumerate() {
if let Some(c) = planar.get(k) {
w.push_sound(0, &filmcraft_mxf::encode_pcm(std::slice::from_ref(c), self.bits)).map_err(io)?;
}
}
Ok(())
} else {
self.video.push_sound(0, &filmcraft_mxf::encode_pcm(planar, self.bits)).map_err(io)
}
}
/// Finish every file; returns (total bytes, extra files written besides `settings.path`).
pub fn finish(self, settings: &ExportSettings) -> Result<(u64, Vec<String>)> {
let mut total = self.video.finish().map_err(io)?.finish_path(settings, &settings.path)?;
let mut extra = Vec::new();
for (path, w) in self.audio {
total += w.finish().map_err(io)?.finish_path(settings, &path)?;
extra.push(path);
}
Ok((total, extra))
}
}