Merge pull request #825 from vangel76/gap-aac-audio
#814: AAC Audio export (audio-only .m4a, or ADTS .aac)
This commit is contained in:
commit
2a9f39db63
|
|
@ -79,7 +79,7 @@ SUBCOMMANDS
|
||||||
[--scale f] [--quality 0-100] [--no-audio] [--queue]
|
[--scale f] [--quality 0-100] [--no-audio] [--queue]
|
||||||
export the active sequence and wait for it to finish: with an
|
export the active sequence and wait for it to finish: with an
|
||||||
export preset (`export --list-presets`; built-in or the user's), or
|
export preset (`export --list-presets`; built-in or the user's), or
|
||||||
a format (h264|hevc|prores|dnxhr|apv|mjpeg|mxf-op1a|mxf-opatom|png|tiff|bmp|gif|wav|aiff, guessed
|
a format (h264|hevc|prores|dnxhr|apv|mjpeg|mxf-op1a|mxf-opatom|png|tiff|bmp|gif|wav|aiff|aac, guessed
|
||||||
from the extension); --range entire|inOut|workArea, or a custom
|
from the extension); --range entire|inOut|workArea, or a custom
|
||||||
range in seconds; --settings is ExportSettings JSON merged over the
|
range in seconds; --settings is ExportSettings JSON merged over the
|
||||||
preset; --scale renders at a fraction of the frame size (0.5 =
|
preset; --scale renders at a fraction of the frame size (0.5 =
|
||||||
|
|
@ -209,6 +209,7 @@ fn format_for(path: &str) -> Option<&'static str> {
|
||||||
"tif" | "tiff" => "tiff",
|
"tif" | "tiff" => "tiff",
|
||||||
"bmp" => "bmp",
|
"bmp" => "bmp",
|
||||||
"aif" | "aiff" => "aiff",
|
"aif" | "aiff" => "aiff",
|
||||||
|
"m4a" | "aac" => "aac",
|
||||||
_ => return None,
|
_ => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1285,7 +1285,7 @@ fn build() -> Vec<CommandSpec> {
|
||||||
"Media…",
|
"Media…",
|
||||||
["File", "Export"],
|
["File", "Export"],
|
||||||
None,
|
None,
|
||||||
r#"{"path":str,"preset":str?,"settings":ExportSettings?,"format":"h264|hevc|prores|dnxhr|apv|mjpeg|mxf-op1a|mxf-opatom|png|tiff|bmp|jpg|tga|dpx|exr|gif|wav|aiff"?,"width":u32?,"height":u32?,"fps":f64?,"bitrateKbps":u32?,"bitrateMode":"cbr|vbr1Pass|vbr2Pass|crf"?,"crf":0..51=23,"hardwareEncoding":"off|auto"?,"scale":f32=1,"audio":bool=true,"quality":0..100,"burnCaptions":bool=false,"alpha":bool=false,"captionSidecar":"srt|vtt"?,"loudnessLufs":f64?,"proresProfile":"proxy|lt|standard|hq|4444|4444xq"?,"dnxProfile":"lb|sq|hq|hqx"?,"apvProfile":"422-10|422-12|444-10|444-12"?,"mxfVideoCodec":"dnxhr|proRes|h264"?,"sequence":id?,"range":"entire|inOut|workArea|custom"?,"startSeconds":f64?,"endSeconds":f64?,"wait":bool=false}"#,
|
r#"{"path":str,"preset":str?,"settings":ExportSettings?,"format":"h264|hevc|prores|dnxhr|apv|mjpeg|mxf-op1a|mxf-opatom|png|tiff|bmp|jpg|tga|dpx|exr|gif|wav|aiff|aac"?,"width":u32?,"height":u32?,"fps":f64?,"bitrateKbps":u32?,"bitrateMode":"cbr|vbr1Pass|vbr2Pass|crf"?,"crf":0..51=23,"hardwareEncoding":"off|auto"?,"scale":f32=1,"audio":bool=true,"quality":0..100,"burnCaptions":bool=false,"alpha":bool=false,"captionSidecar":"srt|vtt"?,"loudnessLufs":f64?,"proresProfile":"proxy|lt|standard|hq|4444|4444xq"?,"dnxProfile":"lb|sq|hq|hqx"?,"apvProfile":"422-10|422-12|444-10|444-12"?,"mxfVideoCodec":"dnxhr|proRes|h264"?,"sequence":id?,"range":"entire|inOut|workArea|custom"?,"startSeconds":f64?,"endSeconds":f64?,"wait":bool=false}"#,
|
||||||
has_seq,
|
has_seq,
|
||||||
crate::export_tools::export_media
|
crate::export_tools::export_media
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,16 @@ fn every_builtin_preset_exports_and_ffprobe_confirms() {
|
||||||
assert!((num(&j["format"]["duration"]) - 0.25).abs() < 0.01, "{name}: {}", j["format"]["duration"]);
|
assert!((num(&j["format"]["duration"]) - 0.25).abs() < 0.01, "{name}: {}", j["format"]["duration"]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
"aac" => {
|
||||||
|
// AAC Audio: an MPEG-4 audio file, sound only
|
||||||
|
assert!(out.ends_with(".m4a"), "{name}: {out}");
|
||||||
|
assert!(fmt_name.contains("m4a"), "{name}: {fmt_name}");
|
||||||
|
assert_eq!(j["streams"].as_array().unwrap().len(), 1, "{name}");
|
||||||
|
let a = stream(&j, "audio").unwrap();
|
||||||
|
assert_eq!((a["codec_name"].as_str(), a["sample_rate"].as_str(), a["channels"].as_u64()), (Some("aac"), Some("48000"), Some(2)), "{name}");
|
||||||
|
assert!((num(&j["format"]["duration"]) - 0.25).abs() < 0.03, "{name}: {}", j["format"]["duration"]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
"gif" => {
|
"gif" => {
|
||||||
let v = stream(&j, "video").unwrap();
|
let v = stream(&j, "video").unwrap();
|
||||||
assert_eq!(v["codec_name"], "gif");
|
assert_eq!(v["codec_name"], "gif");
|
||||||
|
|
@ -282,6 +292,25 @@ fn every_builtin_preset_exports_and_ffprobe_confirms() {
|
||||||
/// H.264 bitrate control over three seconds of the demo: the adaptive Match Source preset
|
/// H.264 bitrate control over three seconds of the demo: the adaptive Match Source preset
|
||||||
/// targets width × height × fps × bpp, the 1080p delivery presets their fixed targets; ffprobe's
|
/// targets width × height × fps × bpp, the 1080p delivery presets their fixed targets; ffprobe's
|
||||||
/// measured video bitrate stays within the VBR maximum and close to the target.
|
/// measured video bitrate stays within the VBR maximum and close to the target.
|
||||||
|
#[test]
|
||||||
|
fn aac_audio_keeps_a_dot_aac_path_as_adts_and_adds_m4a_otherwise() {
|
||||||
|
let mut s = demo();
|
||||||
|
let dir = Scratch::new("aac-audio");
|
||||||
|
let range = json!({"range": "custom", "startSeconds": 1.0, "endSeconds": 1.25, "wait": true, "format": "aac"});
|
||||||
|
let run = |s: &mut Session, path: String| {
|
||||||
|
let mut p = range.clone();
|
||||||
|
p["path"] = json!(path);
|
||||||
|
s.execute("file.exportMedia", p).unwrap()["path"].as_str().unwrap().to_string()
|
||||||
|
};
|
||||||
|
let adts = run(&mut s, dir.path("Mix.aac"));
|
||||||
|
assert!(adts.ends_with("Mix.aac"), "{adts}");
|
||||||
|
let b = std::fs::read(&adts).unwrap();
|
||||||
|
assert_eq!((b[0], b[1]), (0xFF, 0xF1), "an ADTS header");
|
||||||
|
let m4a = run(&mut s, dir.path("Mix"));
|
||||||
|
assert!(m4a.ends_with("Mix.m4a"), "{m4a}");
|
||||||
|
assert_eq!(&std::fs::read(&m4a).unwrap()[4..8], b"ftyp");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn h264_presets_hit_their_bitrates() {
|
fn h264_presets_hit_their_bitrates() {
|
||||||
let mut s = demo();
|
let mut s = demo();
|
||||||
|
|
|
||||||
|
|
@ -363,7 +363,8 @@ fn with_extension(path: &str, settings: &ExportSettings) -> String {
|
||||||
Some(e)
|
Some(e)
|
||||||
if e.eq_ignore_ascii_case(ext)
|
if e.eq_ignore_ascii_case(ext)
|
||||||
|| (settings.format == Format::TiffSequence && e.eq_ignore_ascii_case("tiff"))
|
|| (settings.format == Format::TiffSequence && e.eq_ignore_ascii_case("tiff"))
|
||||||
|| (settings.format == Format::JpegSequence && e.eq_ignore_ascii_case("jpeg")) =>
|
|| (settings.format == Format::JpegSequence && e.eq_ignore_ascii_case("jpeg"))
|
||||||
|
|| (settings.format == Format::Aac && e.eq_ignore_ascii_case("aac")) =>
|
||||||
{
|
{
|
||||||
path.to_string()
|
path.to_string()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,201 @@
|
||||||
|
//! AAC Audio: the sequence mix alone, AAC-LC from the same encoder as the MPEG-4 movies, in an
|
||||||
|
//! MPEG-4 audio file (`.m4a`, the encoder delay signalled by an edit list) or, when the path ends
|
||||||
|
//! in `.aac`, a raw ADTS stream (ISO/IEC 14496-3 §1.A.2; one header per access unit, no CRC). ADTS
|
||||||
|
//! cannot signal the encoder delay, so a player starts it with the priming samples.
|
||||||
|
|
||||||
|
use std::io::Write;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
|
use filmcraft_isobmff::{Brand, CodecConfig, Mp4Writer, TrackConfig, WriteSample, WriterOptions};
|
||||||
|
use filmcraft_project::{ItemId, Project};
|
||||||
|
use filmcraft_render::SourceProvider;
|
||||||
|
use filmcraft_time::TimeRange;
|
||||||
|
|
||||||
|
use crate::audio_out::AudioOut;
|
||||||
|
use crate::{AudioEncoder, ExportError, ExportSettings, Format, Out, Progress, Result, audio_factories};
|
||||||
|
|
||||||
|
/// Whether `path` asks for a raw ADTS stream rather than an MPEG-4 file.
|
||||||
|
pub(crate) fn is_adts_path(path: &str) -> bool {
|
||||||
|
std::path::Path::new(path).extension().and_then(|e| e.to_str()).is_some_and(|e| e.eq_ignore_ascii_case("aac"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where the encoded access units go.
|
||||||
|
enum Sink {
|
||||||
|
Mp4 { mux: Mp4Writer<Out>, track: usize, frame: u32 },
|
||||||
|
Adts { out: Out, header: AdtsHeader },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sink {
|
||||||
|
fn write(&mut self, units: Vec<Vec<u8>>) -> Result<()> {
|
||||||
|
let io = |e: std::io::Error| ExportError::Io(e.to_string());
|
||||||
|
match self {
|
||||||
|
Sink::Mp4 { mux, track, frame } => {
|
||||||
|
for au in units {
|
||||||
|
mux.write_sample(*track, WriteSample { data: &au, duration: *frame, composition_offset: 0, is_sync: true })
|
||||||
|
.map_err(|e| ExportError::Io(e.to_string()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Sink::Adts { out, header } => {
|
||||||
|
for au in units {
|
||||||
|
out.write_all(&header.bytes(au.len())?).map_err(io)?;
|
||||||
|
out.write_all(&au).map_err(io)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(self, settings: &ExportSettings) -> Result<u64> {
|
||||||
|
match self {
|
||||||
|
Sink::Mp4 { mux, .. } => mux.finish().map_err(|e| ExportError::Io(e.to_string()))?.finish(settings),
|
||||||
|
Sink::Adts { out, .. } => out.finish(settings),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The fixed fields of an ADTS header, from the AudioSpecificConfig.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) struct AdtsHeader {
|
||||||
|
/// `profile_ObjectType`: the audio object type minus one (1 = AAC-LC).
|
||||||
|
profile: u8,
|
||||||
|
sf_index: u8,
|
||||||
|
channel_config: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AdtsHeader {
|
||||||
|
/// Read the object type, sampling frequency index and channel configuration from an
|
||||||
|
/// AudioSpecificConfig; ADTS can carry only object types 1–4 with an indexed rate and a
|
||||||
|
/// channel configuration of 1–7.
|
||||||
|
pub(crate) fn from_asc(asc: &[u8]) -> Result<AdtsHeader> {
|
||||||
|
let (Some(&a), Some(&b)) = (asc.first(), asc.get(1)) else {
|
||||||
|
return Err(ExportError::Encode("AAC: the encoder gave no AudioSpecificConfig".into()));
|
||||||
|
};
|
||||||
|
let object_type = a >> 3;
|
||||||
|
let sf_index = ((a & 0x07) << 1) | (b >> 7);
|
||||||
|
let channel_config = (b >> 3) & 0x0F;
|
||||||
|
if !(1..=4).contains(&object_type) || sf_index > 12 || !(1..=7).contains(&channel_config) {
|
||||||
|
return Err(ExportError::Unsupported(format!(
|
||||||
|
"an ADTS .aac stream cannot carry this AAC configuration (object type {object_type}, rate index {sf_index}, channels {channel_config}): export .m4a instead"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(AdtsHeader { profile: object_type - 1, sf_index, channel_config })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The 7-byte header (`protection_absent`) of an access unit of `payload` bytes.
|
||||||
|
pub(crate) fn bytes(&self, payload: usize) -> Result<[u8; 7]> {
|
||||||
|
// aac_frame_length is 13 bits and counts the header
|
||||||
|
let too_big = || ExportError::Encode(format!("AAC: a {payload}-byte frame does not fit ADTS"));
|
||||||
|
let len = u32::try_from(payload).ok().and_then(|n| n.checked_add(7)).filter(|n| *n < 1 << 13).ok_or_else(too_big)?;
|
||||||
|
Ok([
|
||||||
|
0xFF,
|
||||||
|
0xF1, // syncword, MPEG-4, layer 0, protection absent
|
||||||
|
(self.profile << 6) | (self.sf_index << 2) | (self.channel_config >> 2),
|
||||||
|
((self.channel_config & 0x03) << 6) | ((len >> 11) & 0x03) as u8,
|
||||||
|
((len >> 3) & 0xFF) as u8,
|
||||||
|
(((len & 0x07) << 5) as u8) | 0x1F, // buffer fullness 0x7FF: variable rate
|
||||||
|
0xFC, // … and one raw data block
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode the mix of `range` to `settings.path`; returns (bytes written, samples per channel).
|
||||||
|
pub(crate) fn export(
|
||||||
|
project: &Arc<Project>,
|
||||||
|
seq: ItemId,
|
||||||
|
settings: &ExportSettings,
|
||||||
|
range: TimeRange,
|
||||||
|
sources: &dyn SourceProvider,
|
||||||
|
progress: &Progress,
|
||||||
|
) -> Result<(u64, u64)> {
|
||||||
|
let cancelled = || progress.cancel.load(Ordering::Relaxed);
|
||||||
|
let mut a = AudioOut::new(project.clone(), seq, settings, range)?;
|
||||||
|
let mut enc: Box<dyn AudioEncoder> = audio_factories()
|
||||||
|
.read()
|
||||||
|
.unwrap_or_else(|e| e.into_inner())
|
||||||
|
.iter()
|
||||||
|
.find_map(|f| f(Format::Aac, a.sr, a.channels as u32, settings))
|
||||||
|
.ok_or_else(|| ExportError::Unsupported("AAC encoder not available".into()))??;
|
||||||
|
let entry = enc.sample_entry();
|
||||||
|
let frames = a.remaining();
|
||||||
|
// one second per chunk: memory stays flat however long the range is
|
||||||
|
let chunk = i64::from(a.sr.max(1));
|
||||||
|
if !settings.part_of_batch {
|
||||||
|
progress.total.store(frames.div_ceil(chunk as u64).max(1), Ordering::Relaxed);
|
||||||
|
progress.set_status(format!("Exporting audio ({})", settings.format.label()));
|
||||||
|
}
|
||||||
|
a.measure(settings, sources, &cancelled)?;
|
||||||
|
*progress.loudness.lock().unwrap_or_else(|e| e.into_inner()) = a.loudness;
|
||||||
|
let mut sink = if is_adts_path(&settings.path) {
|
||||||
|
let CodecConfig::Aac(cfg) = &entry.codec else {
|
||||||
|
return Err(ExportError::Encode("AAC: the encoder's sample entry is not AAC".into()));
|
||||||
|
};
|
||||||
|
Sink::Adts { header: AdtsHeader::from_asc(&cfg.asc)?, out: Out::create(settings)? }
|
||||||
|
} else {
|
||||||
|
let mut opts = WriterOptions::new(Brand::Mp4);
|
||||||
|
opts.metadata = settings.metadata.udta();
|
||||||
|
let mut mux = Mp4Writer::new(Out::create(settings)?, opts).map_err(|e| ExportError::Io(e.to_string()))?;
|
||||||
|
let mut track = TrackConfig::new(entry, a.sr);
|
||||||
|
track.media_start = Some(i64::from(enc.priming()));
|
||||||
|
let track = mux.add_track(track).map_err(|e| ExportError::Io(e.to_string()))?;
|
||||||
|
Sink::Mp4 { mux, track, frame: enc.frame_size() }
|
||||||
|
};
|
||||||
|
while let Some(planar) = a.pull(a.out.saturating_add(chunk), sources) {
|
||||||
|
if cancelled() {
|
||||||
|
return Err(ExportError::Cancelled);
|
||||||
|
}
|
||||||
|
sink.write(enc.encode(&planar)?)?;
|
||||||
|
if !settings.part_of_batch {
|
||||||
|
progress.done.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sink.write(enc.flush()?)?;
|
||||||
|
let n = sink.finish(settings)?;
|
||||||
|
if settings.part_of_batch {
|
||||||
|
progress.done.store(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
Ok((n, frames))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn adts_header_fields_follow_the_asc() {
|
||||||
|
// AAC-LC (2), 48 kHz (index 3), stereo: 00010 0011 0010 000
|
||||||
|
let h = AdtsHeader::from_asc(&[0x11, 0x90]).unwrap();
|
||||||
|
assert_eq!(h, AdtsHeader { profile: 1, sf_index: 3, channel_config: 2 });
|
||||||
|
let b = h.bytes(371).unwrap();
|
||||||
|
assert_eq!(&b[..2], &[0xFF, 0xF1]);
|
||||||
|
assert_eq!(b[2] >> 6, 1);
|
||||||
|
assert_eq!((b[2] >> 2) & 0x0F, 3);
|
||||||
|
assert_eq!(((b[2] & 1) << 2) | (b[3] >> 6), 2);
|
||||||
|
let len = (u32::from(b[3] & 0x03) << 11) | (u32::from(b[4]) << 3) | u32::from(b[5] >> 5);
|
||||||
|
assert_eq!(len, 378);
|
||||||
|
assert_eq!(((u32::from(b[5] & 0x1F)) << 6) | u32::from(b[6] >> 2), 0x7FF);
|
||||||
|
assert_eq!(b[6] & 0x03, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn adts_refuses_what_it_cannot_signal() {
|
||||||
|
assert!(AdtsHeader::from_asc(&[]).is_err());
|
||||||
|
assert!(AdtsHeader::from_asc(&[0x11]).is_err());
|
||||||
|
// explicit sampling frequency (index 15)
|
||||||
|
assert!(AdtsHeader::from_asc(&[0x17, 0x90, 0, 0, 0]).is_err());
|
||||||
|
// object type 5 (SBR) has no ADTS profile
|
||||||
|
assert!(AdtsHeader::from_asc(&[0x29, 0x90]).is_err());
|
||||||
|
let h = AdtsHeader::from_asc(&[0x11, 0x90]).unwrap();
|
||||||
|
assert!(h.bytes(8184).is_ok());
|
||||||
|
assert!(h.bytes(8185).is_err());
|
||||||
|
assert!(h.bytes(usize::MAX).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_a_dot_aac_path_is_adts() {
|
||||||
|
assert!(is_adts_path("/x/mix.aac"));
|
||||||
|
assert!(is_adts_path("mix.AAC"));
|
||||||
|
assert!(!is_adts_path("mix.m4a"));
|
||||||
|
assert!(!is_adts_path("aac"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
|
mod aac_out;
|
||||||
mod audio_out;
|
mod audio_out;
|
||||||
mod job;
|
mod job;
|
||||||
mod mxf_out;
|
mod mxf_out;
|
||||||
|
|
@ -115,6 +116,10 @@ pub enum Format {
|
||||||
Wav,
|
Wav,
|
||||||
#[serde(rename = "aiff", alias = "Aiff")]
|
#[serde(rename = "aiff", alias = "Aiff")]
|
||||||
Aiff,
|
Aiff,
|
||||||
|
/// AAC Audio: the mix alone, AAC-LC in an MPEG-4 audio file (`.m4a`), or a raw ADTS stream when
|
||||||
|
/// the path ends in `.aac`.
|
||||||
|
#[serde(rename = "aac", alias = "Aac")]
|
||||||
|
Aac,
|
||||||
/// MXF OP1a (SMPTE ST 378): frame-wrapped DNxHR, ProRes or H.264 ([`MxfVideoCodec`]) + PCM.
|
/// MXF OP1a (SMPTE ST 378): frame-wrapped DNxHR, ProRes or H.264 ([`MxfVideoCodec`]) + PCM.
|
||||||
#[serde(rename = "mxf-op1a", alias = "MxfOp1a")]
|
#[serde(rename = "mxf-op1a", alias = "MxfOp1a")]
|
||||||
MxfOp1a,
|
MxfOp1a,
|
||||||
|
|
@ -147,6 +152,7 @@ impl Format {
|
||||||
"gif" | "animatedgif" => Format::Gif,
|
"gif" | "animatedgif" => Format::Gif,
|
||||||
"wav" | "waveform" | "waveformaudio" => Format::Wav,
|
"wav" | "waveform" | "waveformaudio" => Format::Wav,
|
||||||
"aif" | "aiff" | "aifc" => Format::Aiff,
|
"aif" | "aiff" | "aifc" => Format::Aiff,
|
||||||
|
"aac" | "aacaudio" | "m4a" | "adts" => Format::Aac,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -170,6 +176,7 @@ impl Format {
|
||||||
Format::Gif => "gif",
|
Format::Gif => "gif",
|
||||||
Format::Wav => "wav",
|
Format::Wav => "wav",
|
||||||
Format::Aiff => "aiff",
|
Format::Aiff => "aiff",
|
||||||
|
Format::Aac => "aac",
|
||||||
Format::MxfOp1a => "mxf-op1a",
|
Format::MxfOp1a => "mxf-op1a",
|
||||||
Format::MxfOpAtom => "mxf-opatom",
|
Format::MxfOpAtom => "mxf-opatom",
|
||||||
}
|
}
|
||||||
|
|
@ -188,6 +195,7 @@ impl Format {
|
||||||
Format::Gif => "gif",
|
Format::Gif => "gif",
|
||||||
Format::Wav => "wav",
|
Format::Wav => "wav",
|
||||||
Format::Aiff => "aif",
|
Format::Aiff => "aif",
|
||||||
|
Format::Aac => "m4a",
|
||||||
Format::MxfOp1a | Format::MxfOpAtom => "mxf",
|
Format::MxfOp1a | Format::MxfOpAtom => "mxf",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -210,6 +218,7 @@ impl Format {
|
||||||
Format::Gif => "Animated GIF",
|
Format::Gif => "Animated GIF",
|
||||||
Format::Wav => "Waveform Audio",
|
Format::Wav => "Waveform Audio",
|
||||||
Format::Aiff => "AIFF",
|
Format::Aiff => "AIFF",
|
||||||
|
Format::Aac => "AAC Audio",
|
||||||
Format::MxfOp1a => "MXF OP1a",
|
Format::MxfOp1a => "MXF OP1a",
|
||||||
Format::MxfOpAtom => "MXF OP-Atom",
|
Format::MxfOpAtom => "MXF OP-Atom",
|
||||||
}
|
}
|
||||||
|
|
@ -236,6 +245,10 @@ impl Format {
|
||||||
pub fn has_builtin_encoder(self) -> bool {
|
pub fn has_builtin_encoder(self) -> bool {
|
||||||
!matches!(self, Format::Hevc | Format::Av1)
|
!matches!(self, Format::Hevc | Format::Av1)
|
||||||
}
|
}
|
||||||
|
/// Sound without picture: Waveform Audio, AIFF and AAC Audio.
|
||||||
|
pub fn is_audio_only(self) -> bool {
|
||||||
|
matches!(self, Format::Wav | Format::Aiff | Format::Aac)
|
||||||
|
}
|
||||||
/// H.264 or H.265: MPEG-4 (or QuickTime) with AAC audio, set up with the same bitrate controls.
|
/// H.264 or H.265: MPEG-4 (or QuickTime) with AAC audio, set up with the same bitrate controls.
|
||||||
pub fn is_h26x(self) -> bool {
|
pub fn is_h26x(self) -> bool {
|
||||||
matches!(self, Format::H264 | Format::Hevc)
|
matches!(self, Format::H264 | Format::Hevc)
|
||||||
|
|
@ -248,7 +261,7 @@ impl Format {
|
||||||
pub fn is_mp4_with(self, mux: Multiplexer) -> bool {
|
pub fn is_mp4_with(self, mux: Multiplexer) -> bool {
|
||||||
self == Format::Av1 || (self.is_h26x() && mux == Multiplexer::Mp4)
|
self == Format::Av1 || (self.is_h26x() && mux == Multiplexer::Mp4)
|
||||||
}
|
}
|
||||||
pub const ALL: [Format; 19] = [
|
pub const ALL: [Format; 20] = [
|
||||||
Format::H264,
|
Format::H264,
|
||||||
Format::Hevc,
|
Format::Hevc,
|
||||||
Format::Av1,
|
Format::Av1,
|
||||||
|
|
@ -266,6 +279,7 @@ impl Format {
|
||||||
Format::Gif,
|
Format::Gif,
|
||||||
Format::Wav,
|
Format::Wav,
|
||||||
Format::Aiff,
|
Format::Aiff,
|
||||||
|
Format::Aac,
|
||||||
Format::MxfOp1a,
|
Format::MxfOp1a,
|
||||||
Format::MxfOpAtom,
|
Format::MxfOpAtom,
|
||||||
];
|
];
|
||||||
|
|
@ -1739,6 +1753,7 @@ pub fn export(project: &Arc<Project>, seq: ItemId, settings: &ExportSettings, so
|
||||||
let cancelled = || progress.cancel.load(Ordering::Relaxed);
|
let cancelled = || progress.cancel.load(Ordering::Relaxed);
|
||||||
let batch = rayon::current_num_threads().clamp(2, 16) as i64;
|
let batch = rayon::current_num_threads().clamp(2, 16) as i64;
|
||||||
let (bytes, nframes) = match settings.format {
|
let (bytes, nframes) = match settings.format {
|
||||||
|
Format::Aac => aac_out::export(project, seq, settings, range, sources, progress)?,
|
||||||
Format::Wav | Format::Aiff => {
|
Format::Wav | Format::Aiff => {
|
||||||
let mut a = audio_out::AudioOut::new(project.clone(), seq, settings, range)?;
|
let mut a = audio_out::AudioOut::new(project.clone(), seq, settings, range)?;
|
||||||
let (ch, sr, bits) = (a.channels as u16, a.sr, settings.audio.bits);
|
let (ch, sr, bits) = (a.channels as u16, a.sr, settings.audio.bits);
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,16 @@ pub fn builtin_presets() -> Vec<ExportPreset> {
|
||||||
p("Waveform Audio 48 kHz 16-bit", "Audio Only", "WAV, stereo 48 kHz 16-bit PCM", audio_only(Format::Wav, 16)),
|
p("Waveform Audio 48 kHz 16-bit", "Audio Only", "WAV, stereo 48 kHz 16-bit PCM", audio_only(Format::Wav, 16)),
|
||||||
p("Waveform Audio 48 kHz 24-bit", "Audio Only", "WAV, stereo 48 kHz 24-bit PCM", audio_only(Format::Wav, 24)),
|
p("Waveform Audio 48 kHz 24-bit", "Audio Only", "WAV, stereo 48 kHz 24-bit PCM", audio_only(Format::Wav, 24)),
|
||||||
p("AIFF 48 kHz 16-bit", "Audio Only", "AIFF, stereo 48 kHz 16-bit PCM", audio_only(Format::Aiff, 16)),
|
p("AIFF 48 kHz 16-bit", "Audio Only", "AIFF, stereo 48 kHz 16-bit PCM", audio_only(Format::Aiff, 16)),
|
||||||
|
p(
|
||||||
|
"AAC Audio 48 kHz 320 kbps",
|
||||||
|
"Audio Only",
|
||||||
|
"MPEG-4 audio (.m4a), stereo 48 kHz AAC-LC at 320 kbps",
|
||||||
|
ExportSettings {
|
||||||
|
format: Format::Aac,
|
||||||
|
audio: AudioSettings { codec: AudioCodec::Aac, sample_rate: Some(48_000), channels: 2, bitrate_kbps: 320, bits: 16 },
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ impl MxfVideoCodec {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Audio codec. `Auto` = AAC in MP4, PCM in QuickTime / WAV / AIFF.
|
/// Audio codec. `Auto` = AAC in MP4 and AAC Audio, PCM in QuickTime / WAV / AIFF.
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub enum AudioCodec {
|
pub enum AudioCodec {
|
||||||
|
|
@ -432,6 +432,7 @@ impl ExportSettings {
|
||||||
pub fn audio_codec(&self) -> AudioCodec {
|
pub fn audio_codec(&self) -> AudioCodec {
|
||||||
match (self.format, self.audio.codec) {
|
match (self.format, self.audio.codec) {
|
||||||
(Format::Wav | Format::Aiff | Format::MxfOp1a | Format::MxfOpAtom, _) => AudioCodec::Pcm,
|
(Format::Wav | Format::Aiff | Format::MxfOp1a | Format::MxfOpAtom, _) => AudioCodec::Pcm,
|
||||||
|
(Format::Aac, _) => AudioCodec::Aac,
|
||||||
(f, _) if f.is_mp4_with(self.multiplexer) => AudioCodec::Aac,
|
(f, _) if f.is_mp4_with(self.multiplexer) => AudioCodec::Aac,
|
||||||
(_, AudioCodec::Auto) if self.format.is_mp4_video() => AudioCodec::Aac,
|
(_, AudioCodec::Auto) if self.format.is_mp4_video() => AudioCodec::Aac,
|
||||||
(_, AudioCodec::Auto) => AudioCodec::Pcm,
|
(_, AudioCodec::Auto) => AudioCodec::Pcm,
|
||||||
|
|
@ -446,7 +447,7 @@ impl ExportSettings {
|
||||||
|
|
||||||
/// Whether the format carries video.
|
/// Whether the format carries video.
|
||||||
pub fn has_video(&self) -> bool {
|
pub fn has_video(&self) -> bool {
|
||||||
!matches!(self.format, Format::Wav | Format::Aiff)
|
!self.format.is_audio_only()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// File extension of the output.
|
/// File extension of the output.
|
||||||
|
|
@ -461,6 +462,7 @@ impl ExportSettings {
|
||||||
let fps = r.rate.num as f64 / r.rate.den as f64;
|
let fps = r.rate.num as f64 / r.rate.den as f64;
|
||||||
let px = r.width as f64 * r.height as f64;
|
let px = r.width as f64 * r.height as f64;
|
||||||
let video_bps = match self.video_format() {
|
let video_bps = match self.video_format() {
|
||||||
|
Format::Aac => 0.0,
|
||||||
Format::H264 | Format::Hevc | Format::Av1 => r.target_kbps as f64 * 1000.0,
|
Format::H264 | Format::Hevc | Format::Av1 => r.target_kbps as f64 * 1000.0,
|
||||||
Format::ProRes => crate::prores_profile(&self.prores_profile).nominal_mbps_1080p30() * 1e6 * px / (1920.0 * 1080.0) * fps / 29.97,
|
Format::ProRes => crate::prores_profile(&self.prores_profile).nominal_mbps_1080p30() * 1e6 * px / (1920.0 * 1080.0) * fps / 29.97,
|
||||||
Format::DnxHr => {
|
Format::DnxHr => {
|
||||||
|
|
@ -493,7 +495,7 @@ impl ExportSettings {
|
||||||
Format::Gif => px * 0.6 * 8.0 * fps,
|
Format::Gif => px * 0.6 * 8.0 * fps,
|
||||||
Format::Wav | Format::Aiff | Format::MxfOp1a | Format::MxfOpAtom => 0.0,
|
Format::Wav | Format::Aiff | Format::MxfOp1a | Format::MxfOpAtom => 0.0,
|
||||||
};
|
};
|
||||||
let audio_bps = if self.has_audio() || matches!(self.format, Format::Wav | Format::Aiff) {
|
let audio_bps = if self.has_audio() || self.format.is_audio_only() {
|
||||||
match self.audio_codec() {
|
match self.audio_codec() {
|
||||||
AudioCodec::Aac => self.audio.bitrate_kbps as f64 * 1000.0,
|
AudioCodec::Aac => self.audio.bitrate_kbps as f64 * 1000.0,
|
||||||
_ => r.sample_rate as f64 * r.channels as f64 * if self.audio.bits >= 24 { 24.0 } else { 16.0 },
|
_ => r.sample_rate as f64 * r.channels as f64 * if self.audio.bits >= 24 { 24.0 } else { 16.0 },
|
||||||
|
|
@ -578,7 +580,7 @@ impl ExportSettings {
|
||||||
}
|
}
|
||||||
v
|
v
|
||||||
};
|
};
|
||||||
let audio = if self.has_audio() || matches!(self.format, Format::Wav | Format::Aiff) {
|
let audio = if self.has_audio() || self.format.is_audio_only() {
|
||||||
let ch = match r.channels {
|
let ch = match r.channels {
|
||||||
1 => "Mono",
|
1 => "Mono",
|
||||||
6 => "5.1",
|
6 => "5.1",
|
||||||
|
|
|
||||||
|
|
@ -261,6 +261,103 @@ fn wav_and_aiff_audio_only() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Root-mean-square level of decoded planar audio.
|
||||||
|
fn rms(planes: &[Vec<f32>]) -> f32 {
|
||||||
|
let n = planes.iter().map(Vec::len).sum::<usize>().max(1);
|
||||||
|
(planes.iter().flatten().map(|s| s * s).sum::<f32>() / n as f32).sqrt()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aac_audio_writes_m4a_and_adts() {
|
||||||
|
let (p, seq, m) = matte([0.0, 0.0, 0.0, 1.0], 64, 36, Some(-6.0));
|
||||||
|
let dir = Scratch::new("aac-audio");
|
||||||
|
let range = Some(TimeRange::new(Tick::ZERO, Tick(TICKS_PER_SECOND)));
|
||||||
|
// .m4a: one AAC track, no video, the encoder delay in an edit list
|
||||||
|
let m4a = dir.path("a.m4a");
|
||||||
|
let s = ExportSettings { format: Format::Aac, path: m4a.clone(), range, ..Default::default() };
|
||||||
|
assert!(!s.has_video());
|
||||||
|
assert_eq!(s.audio_codec(), AudioCodec::Aac);
|
||||||
|
assert_eq!(s.extension(), "m4a");
|
||||||
|
let r = export(&p, seq, &s, &m, &Progress::default()).unwrap();
|
||||||
|
assert_eq!(r.frames, 48_000, "one second of samples");
|
||||||
|
let bytes = std::fs::read(&m4a).unwrap();
|
||||||
|
let file = filmcraft_isobmff::open(bytes.clone()).unwrap();
|
||||||
|
assert_eq!(file.tracks.len(), 1, "audio only");
|
||||||
|
let t = &file.tracks[0];
|
||||||
|
let filmcraft_isobmff::CodecConfig::Aac(cfg) = &t.entries[0].codec else { panic!("not AAC: {:?}", t.entries[0].codec) };
|
||||||
|
assert_eq!((cfg.object_type, cfg.sample_rate, cfg.channel_config), (2, 48_000, 2));
|
||||||
|
assert!(t.samples.len() >= 48_000 / 1024, "{} access units", t.samples.len());
|
||||||
|
let mut dec = filmcraft_aac::Decoder::new(&cfg.asc).unwrap();
|
||||||
|
let mut pcm: Vec<Vec<f32>> = Vec::new();
|
||||||
|
for i in 0..t.samples.len() {
|
||||||
|
let out = dec.decode(&file.read_sample(&bytes, 0, i).unwrap()).unwrap();
|
||||||
|
pcm.resize(out.len(), Vec::new());
|
||||||
|
pcm.iter_mut().zip(out).for_each(|(a, b)| a.extend(b));
|
||||||
|
}
|
||||||
|
// the -6 dBFS tone (RMS about 0.35) survives the round trip
|
||||||
|
assert!((0.2..0.5).contains(&rms(&pcm)), "rms {}", rms(&pcm));
|
||||||
|
if let Some(j) = ffprobe_json(&["-show_streams", "-show_format"], &m4a) {
|
||||||
|
assert_eq!(j["streams"].as_array().unwrap().len(), 1);
|
||||||
|
let st = &j["streams"][0];
|
||||||
|
assert_eq!(st["codec_name"], "aac");
|
||||||
|
assert_eq!(st["sample_rate"], "48000");
|
||||||
|
assert_eq!(st["channels"], 2);
|
||||||
|
let d: f64 = j["format"]["duration"].as_str().unwrap().parse().unwrap();
|
||||||
|
assert!((d - 1.0).abs() < 0.03, "duration {d}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// .aac: raw ADTS, mono 44.1 kHz at 128 kbps
|
||||||
|
let adts = dir.path("a.aac");
|
||||||
|
let mut s = ExportSettings { format: Format::Aac, path: adts.clone(), range, ..Default::default() };
|
||||||
|
s.audio = AudioSettings { sample_rate: Some(44_100), channels: 1, bitrate_kbps: 128, ..Default::default() };
|
||||||
|
export(&p, seq, &s, &m, &Progress::default()).unwrap();
|
||||||
|
let b = std::fs::read(&adts).unwrap();
|
||||||
|
// AAC-LC 44.1 kHz (index 4) mono: 00010 0100 0001 000
|
||||||
|
let mut dec = filmcraft_aac::Decoder::new(&[0x12, 0x08]).unwrap();
|
||||||
|
let (mut pos, mut units, mut pcm) = (0usize, 0usize, vec![Vec::new()]);
|
||||||
|
while pos < b.len() {
|
||||||
|
let h = &b[pos..pos + 7];
|
||||||
|
assert_eq!((h[0], h[1] & 0xF6), (0xFF, 0xF0), "ADTS sync at {pos}");
|
||||||
|
assert_eq!(h[1] & 0x01, 1, "no CRC");
|
||||||
|
assert_eq!(((h[2] >> 2) & 0x0F, ((h[2] & 1) << 2) | (h[3] >> 6)), (4, 1), "44.1 kHz mono");
|
||||||
|
let len = ((h[3] as usize & 0x03) << 11) | ((h[4] as usize) << 3) | (h[5] as usize >> 5);
|
||||||
|
assert!(len > 7 && pos + len <= b.len(), "frame length {len} at {pos}");
|
||||||
|
pcm[0].extend(dec.decode(&b[pos + 7..pos + len]).unwrap().swap_remove(0));
|
||||||
|
pos += len;
|
||||||
|
units += 1;
|
||||||
|
}
|
||||||
|
assert!(units >= 44_100 / 1024, "{units} frames");
|
||||||
|
assert!((0.2..0.5).contains(&rms(&pcm)), "rms {}", rms(&pcm));
|
||||||
|
if let Some(j) = ffprobe_json(&["-show_streams", "-show_format"], &adts) {
|
||||||
|
assert_eq!(j["format"]["format_name"], "aac");
|
||||||
|
let st = &j["streams"][0];
|
||||||
|
assert_eq!(st["codec_name"], "aac");
|
||||||
|
assert_eq!(st["sample_rate"], "44100");
|
||||||
|
assert_eq!(st["channels"], 1);
|
||||||
|
}
|
||||||
|
if let Some(ffmpeg) = filmcraft_testkit::ffmpeg_or_skip("AAC Audio decode") {
|
||||||
|
for f in [&m4a, &adts] {
|
||||||
|
let out = std::process::Command::new(&ffmpeg).args(["-v", "error", "-xerror", "-i", f, "-f", "null", "-"]).output().unwrap();
|
||||||
|
assert!(out.status.success() && out.stderr.is_empty(), "{f}: {}", String::from_utf8_lossy(&out.stderr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aac_audio_settings_and_names() {
|
||||||
|
assert_eq!(Format::from_name("m4a"), Some(Format::Aac));
|
||||||
|
assert_eq!(Format::from_name("AAC Audio"), Some(Format::Aac));
|
||||||
|
assert_eq!(Format::from_name(Format::Aac.id()), Some(Format::Aac));
|
||||||
|
assert!(Format::ALL.contains(&Format::Aac) && Format::Aac.is_audio_only() && !Format::H264.is_audio_only());
|
||||||
|
let s = ExportSettings { format: Format::Aac, include_audio: false, ..Default::default() };
|
||||||
|
let sum = s.summary(1920, 1080, FrameRate::FPS_24, 48_000, Tick(10 * TICKS_PER_SECOND));
|
||||||
|
assert_eq!(sum.video, "No video");
|
||||||
|
assert_eq!(sum.audio, "AAC, 320 kbps, 48000 Hz, Stereo");
|
||||||
|
assert_eq!(sum.estimated_bytes, 400_000, "320 kbps for 10 s, no picture");
|
||||||
|
let preset = presets::find_builtin("AAC Audio 48 kHz 320 kbps").unwrap();
|
||||||
|
assert_eq!(preset.settings.format, Format::Aac);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wav_export_honours_explicit_sample_rates() {
|
fn wav_export_honours_explicit_sample_rates() {
|
||||||
let (project, sequence, sources) = matte([0.0, 0.0, 0.0, 1.0], 64, 36, Some(-6.0));
|
let (project, sequence, sources) = matte([0.0, 0.0, 0.0, 1.0], 64, 36, Some(-6.0));
|
||||||
|
|
@ -701,7 +798,7 @@ fn h265_is_a_format_that_needs_a_registered_encoder() {
|
||||||
}
|
}
|
||||||
assert_eq!(Format::from_name(Format::Hevc.id()), Some(Format::Hevc));
|
assert_eq!(Format::from_name(Format::Hevc.id()), Some(Format::Hevc));
|
||||||
assert_eq!((Format::Hevc.extension(), Format::Hevc.label()), ("mp4", "H.265 (HEVC)"));
|
assert_eq!((Format::Hevc.extension(), Format::Hevc.label()), ("mp4", "H.265 (HEVC)"));
|
||||||
assert!(Format::ALL.contains(&Format::Hevc) && Format::ALL.len() == 19);
|
assert!(Format::ALL.contains(&Format::Hevc) && Format::ALL.len() == 20);
|
||||||
let s: ExportSettings = serde_json::from_value(serde_json::json!({"format": "hevc"})).unwrap();
|
let s: ExportSettings = serde_json::from_value(serde_json::json!({"format": "hevc"})).unwrap();
|
||||||
assert_eq!(s.format, Format::Hevc);
|
assert_eq!(s.format, Format::Hevc);
|
||||||
assert_eq!(serde_json::to_value(&s).unwrap()["format"], "hevc");
|
assert_eq!(serde_json::to_value(&s).unwrap()["format"], "hevc");
|
||||||
|
|
|
||||||
|
|
@ -2974,6 +2974,9 @@
|
||||||
Step Forward 1 Frame Avanzar 1 fotograma
|
Step Forward 1 Frame Avanzar 1 fotograma
|
||||||
Match Playhead Igualar cabezal
|
Match Playhead Igualar cabezal
|
||||||
Gang to Program Vincular al programa
|
Gang to Program Vincular al programa
|
||||||
|
AAC Audio AAC Audio
|
||||||
|
AAC Audio 48 kHz 320 kbps AAC Audio 48 kHz 320 kbps
|
||||||
|
MPEG-4 audio (.m4a), stereo 48 kHz AAC-LC at 320 kbps Audio MPEG-4 (.m4a), estéreo, AAC-LC de 48 kHz a 320 kbps
|
||||||
AIFF AIFF
|
AIFF AIFF
|
||||||
AIFF 48 kHz 16-bit AIFF 48 kHz 16 bits
|
AIFF 48 kHz 16-bit AIFF 48 kHz 16 bits
|
||||||
AIFF, stereo 48 kHz 16-bit PCM AIFF, estéreo, PCM de 48 kHz y 16 bits
|
AIFF, stereo 48 kHz 16-bit PCM AIFF, estéreo, PCM de 48 kHz y 16 bits
|
||||||
|
|
|
||||||
|
Can't render this file because it contains an unexpected character in line 3 and column 95.
|
|
|
@ -2976,6 +2976,9 @@
|
||||||
Step Forward 1 Frame Avancer d'une image
|
Step Forward 1 Frame Avancer d'une image
|
||||||
Match Playhead Faire correspondre la tête de lecture
|
Match Playhead Faire correspondre la tête de lecture
|
||||||
Gang to Program Coupler au moniteur programme
|
Gang to Program Coupler au moniteur programme
|
||||||
|
AAC Audio AAC Audio
|
||||||
|
AAC Audio 48 kHz 320 kbps AAC Audio 48 kHz 320 kbps
|
||||||
|
MPEG-4 audio (.m4a), stereo 48 kHz AAC-LC at 320 kbps Audio MPEG-4 (.m4a), AAC-LC stéréo 48 kHz à 320 kbps
|
||||||
AIFF AIFF
|
AIFF AIFF
|
||||||
AIFF 48 kHz 16-bit AIFF 48 kHz 16 bits
|
AIFF 48 kHz 16-bit AIFF 48 kHz 16 bits
|
||||||
AIFF, stereo 48 kHz 16-bit PCM AIFF, PCM stéréo 48 kHz 16 bits
|
AIFF, stereo 48 kHz 16-bit PCM AIFF, PCM stéréo 48 kHz 16 bits
|
||||||
|
|
|
||||||
|
Can't render this file because it contains an unexpected character in line 5 and column 83.
|
|
|
@ -2963,6 +2963,9 @@
|
||||||
Step Forward 1 Frame 1 フレーム進む
|
Step Forward 1 Frame 1 フレーム進む
|
||||||
Match Playhead 再生ヘッドに合わせる
|
Match Playhead 再生ヘッドに合わせる
|
||||||
Gang to Program プログラムと連動
|
Gang to Program プログラムと連動
|
||||||
|
AAC Audio AAC Audio
|
||||||
|
AAC Audio 48 kHz 320 kbps AAC Audio 48 kHz 320 kbps
|
||||||
|
MPEG-4 audio (.m4a), stereo 48 kHz AAC-LC at 320 kbps MPEG-4 オーディオ (.m4a)、ステレオ 48 kHz AAC-LC 320 kbps
|
||||||
AIFF AIFF
|
AIFF AIFF
|
||||||
AIFF 48 kHz 16-bit AIFF 48 kHz 16-bit
|
AIFF 48 kHz 16-bit AIFF 48 kHz 16-bit
|
||||||
AIFF, stereo 48 kHz 16-bit PCM AIFF、ステレオ 48 kHz 16-bit PCM
|
AIFF, stereo 48 kHz 16-bit PCM AIFF、ステレオ 48 kHz 16-bit PCM
|
||||||
|
|
|
||||||
|
Can't render this file because it contains an unexpected character in line 11 and column 59.
|
|
|
@ -2957,6 +2957,9 @@
|
||||||
Step Forward 1 Frame 前进 1 帧
|
Step Forward 1 Frame 前进 1 帧
|
||||||
Match Playhead 匹配播放头
|
Match Playhead 匹配播放头
|
||||||
Gang to Program 联动到节目
|
Gang to Program 联动到节目
|
||||||
|
AAC Audio AAC 音频
|
||||||
|
AAC Audio 48 kHz 320 kbps AAC 音频 48 kHz 320 kbps
|
||||||
|
MPEG-4 audio (.m4a), stereo 48 kHz AAC-LC at 320 kbps MPEG-4 音频 (.m4a),立体声 48 kHz AAC-LC,320 kbps
|
||||||
AIFF AIFF
|
AIFF AIFF
|
||||||
AIFF 48 kHz 16-bit AIFF 48 kHz 16 位
|
AIFF 48 kHz 16-bit AIFF 48 kHz 16 位
|
||||||
AIFF, stereo 48 kHz 16-bit PCM AIFF,立体声 48 kHz 16 位 PCM
|
AIFF, stereo 48 kHz 16-bit PCM AIFF,立体声 48 kHz 16 位 PCM
|
||||||
|
|
|
||||||
|
Can't render this file because it contains an unexpected character in line 3 and column 95.
|
|
|
@ -665,6 +665,7 @@ file.exportMedia {path, preset?, settings?, format?, range?, …} export.qui
|
||||||
| `prores` | `filmcraft-prores` (Proxy / LT / 422 / HQ) | MOV |
|
| `prores` | `filmcraft-prores` (Proxy / LT / 422 / HQ) | MOV |
|
||||||
| `dnxhr` | `filmcraft-dnx` (LB / SQ / HQ / HQX) | MOV (`AVdh`) |
|
| `dnxhr` | `filmcraft-dnx` (LB / SQ / HQ / HQX) | MOV (`AVdh`) |
|
||||||
| `mjpeg` | built in | MOV |
|
| `mjpeg` | built in | MOV |
|
||||||
|
| `aac` | `filmcraft-aac` (AAC-LC, the audio settings' bitrate) | audio only: MPEG-4 audio `.m4a` (`isobmff`, priming in an edit list), or raw ADTS when the path ends in `.aac` |
|
||||||
| `mxf-op1a` | DNxHR (default), ProRes or H.264 (Annex B, long GOP), `mxfVideoCodec`; PCM 16/24-bit | MXF OP1a (`filmcraft-mxf` writer: frame-wrapped, index with temporal offsets, start timecode) |
|
| `mxf-op1a` | DNxHR (default), ProRes or H.264 (Annex B, long GOP), `mxfVideoCodec`; PCM 16/24-bit | MXF OP1a (`filmcraft-mxf` writer: frame-wrapped, index with temporal offsets, start timecode) |
|
||||||
| `mxf-opatom` | as `mxf-op1a` | Avid-style MXF OP-Atom: clip-wrapped picture at `path`, one mono PCM file per channel (`<stem>_A1.mxf` …, `Report::extra_files`) |
|
| `mxf-opatom` | as `mxf-op1a` | Avid-style MXF OP-Atom: clip-wrapped picture at `path`, one mono PCM file per channel (`<stem>_A1.mxf` …, `Report::extra_files`) |
|
||||||
| `png`, `tiff`, `bmp`, `jpg` | `image` | numbered stills `<name>000.<ext>`, `<name>001.<ext>` … |
|
| `png`, `tiff`, `bmp`, `jpg` | `image` | numbered stills `<name>000.<ext>`, `<name>001.<ext>` … |
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ only). "Tested" names the evidence.
|
||||||
| WAV / BWF (timecode, iXML partial) | read + write | yes | yes | 90% |
|
| WAV / BWF (timecode, iXML partial) | read + write | yes | yes | 90% |
|
||||||
| AIFF / AIFC | read + write | yes | yes | 95% |
|
| AIFF / AIFC | read + write | yes | yes | 95% |
|
||||||
| MP3 | read + write | read | **no** | 60% |
|
| MP3 | read + write | read | **no** | 60% |
|
||||||
| AAC / M4A | read + write | yes | inside MP4 / MOV only, **no AAC-only file** | 70% |
|
| AAC / M4A | read + write | yes | yes (in MP4 / MOV, and AAC Audio: `.m4a` or ADTS `.aac`, #814) | 95% |
|
||||||
| FLAC, Ogg, Opus | partial | yes | no | ahead on read |
|
| FLAC, Ogg, Opus | partial | yes | no | ahead on read |
|
||||||
| WMA | read (Windows) | no | no | 0% |
|
| WMA | read (Windows) | no | no | 0% |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -192,11 +192,11 @@ platforms** G4, G9, G17; **performance** G16; **ecosystem** G11; **measurement**
|
||||||
## G13. Delivery formats
|
## G13. Delivery formats
|
||||||
|
|
||||||
- **Missing:** AV1 export (software or hardware), MPEG-2 / DVD / Blu-ray, DCP, JPEG 2000 MXF,
|
- **Missing:** AV1 export (software or hardware), MPEG-2 / DVD / Blu-ray, DCP, JPEG 2000 MXF,
|
||||||
AS-10 / AS-11, XDCAM HD MXF, P2, compressed (ZIP / PIZ / DWA) OpenEXR, MP3 and AAC-only audio,
|
AS-10 / AS-11, XDCAM HD MXF, P2, compressed (ZIP / PIZ / DWA) OpenEXR, MP3 audio, interlaced
|
||||||
ProRes 4444 / XQ in the export UI (#342; the encoder has the profiles), interlaced encoding,
|
encoding, smart render, HEVC software encoder (HEVC exists only where a hardware encoder does).
|
||||||
smart render, HEVC software encoder (HEVC exists only where a hardware encoder does).
|
|
||||||
- **Have:** PNG, TIFF, BMP, JPEG, Targa and DPX (10-bit) image sequences (#779); OpenEXR
|
- **Have:** PNG, TIFF, BMP, JPEG, Targa and DPX (10-bit) image sequences (#779); OpenEXR
|
||||||
sequences (half float, linear, uncompressed; #806).
|
sequences (half float, linear, uncompressed; #806); AAC Audio (audio-only `.m4a`, or ADTS
|
||||||
|
`.aac`; #814); ProRes 4444 / 4444 XQ in the export UI (#342).
|
||||||
- **Estimate:** 60–110 h. Doc: [file-format-parity.md](file-format-parity.md).
|
- **Estimate:** 60–110 h. Doc: [file-format-parity.md](file-format-parity.md).
|
||||||
|
|
||||||
## G14. Localization: 8 of the 12 key languages missing
|
## G14. Localization: 8 of the 12 key languages missing
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue