mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 22:42:46 +00:00
craft-audio: review round 3 for the coded formats
- craft-media: Mp4Writer::new refuses a writer positioned where the header would end past u64::MAX instead of overflowing the sum (correction to round 2, which let the M4A writer reach that addition). - AAC is written at 6 kbit/s or a quarter of the sample rate per channel at least: the encoder accepts lower rates but codes only silence at them, so with_bitrate(128) gave a silent file. - README: what to do with a layout a coded format refuses, and the names encode_format_for takes for M4A. Refs #1230
This commit is contained in:
5 files changed
+47
-7
No files matched your search
+10
-2
@@ -239,7 +239,7 @@ let alac = encode(&buf, &EncodeOptions::new(EncodeFormat::Alac).with_bit_depth(B
|
||||
|
||||
`EncodeOptions::default()` is 24-bit WAV. `with_bwf` adds a BWF `bext` chunk and
|
||||
`with_force_rf64` writes RF64 below 4 GiB; both apply to WAV only. `encode_format_for("aiff")`
|
||||
maps an extension to an `EncodeFormat`, and `ENCODE_EXTENSIONS` lists the extensions the enabled
|
||||
maps an extension to an `EncodeFormat` (`"m4a"` to AAC; `"alac"` names ALAC), and `ENCODE_EXTENSIONS` lists the extensions the enabled
|
||||
features can write. `EncodeFormat::ALL` lists the formats and `is_built()` says whether this build
|
||||
writes one; `extension()` is the extension a new file gets and `mime_type()` its media type.
|
||||
`write` refuses a block without one slice per channel, or with a slice shorter than `frames`, and
|
||||
@@ -256,7 +256,8 @@ writes nothing. After a write fails for another reason, every later
|
||||
|
||||
`with_bitrate(bits_per_second)` sets a constant bit rate for all channels together. A rate the
|
||||
stream cannot carry is moved to the nearest one it can: MP3 and AC-3 use the highest rate of their
|
||||
tables not above it, AAC and E-AC-3 keep it inside their range. `with_quality(0.0..=1.0)` is AAC's
|
||||
tables not above it, AAC and E-AC-3 keep it inside their range (AAC from 6 kbit/s or a quarter of
|
||||
the sample rate per channel, whichever is more: below that the encoder has bits for silence only). `with_quality(0.0..=1.0)` is AAC's
|
||||
constant quality (VBR); the other lossy formats return `Unsupported` for it. Without either, AAC
|
||||
is written at 64 kbit/s per channel, MP3 at 96 kbit/s per channel, and AC-3 and E-AC-3 at
|
||||
96 kbit/s per full-bandwidth channel (the LFE not counted).
|
||||
@@ -275,6 +276,13 @@ rather than written to the wrong ones:
|
||||
- A surround pair is a surround pair: side and back surrounds are interchangeable where the format
|
||||
has one pair.
|
||||
|
||||
The layout `decode` reports (`AudioInfo::layout`) can be passed to `with_layout` as it is. When a
|
||||
coded format refuses it (2.1 or quad into AAC, a single channel tagged as left, channels with no
|
||||
speaker assignment), the choices are another format, or audio rearranged into one of the layouts
|
||||
above with that layout set; leaving `layout` unset declares the usual layout for the count. This
|
||||
crate's decoder returns 7- and 8-channel AAC without a speaker assignment, in AAC order, so such a
|
||||
file needs that step before it is written again.
|
||||
|
||||
An M4A's edit list skips the AAC encoder's priming (1024 frames) and ends at the last frame
|
||||
written, so the file decodes to the length that went in. An MP3, AC-3 or E-AC-3 stream has no
|
||||
place for that (see [Limits](#limits)). For a container of your own, use the codec modules below.
|
||||
|
||||
@@ -366,7 +366,8 @@ impl EncodeFormat {
|
||||
pub enum RateControl {
|
||||
/// A constant bit rate in bits per second, for all channels together. A format with a fixed
|
||||
/// set of rates (MP3, AC-3) uses the highest one not above it; a rate outside what the
|
||||
/// stream can carry is moved to the nearest one inside.
|
||||
/// stream can carry is moved to the nearest one inside. AAC is written at 6 kbit/s or a
|
||||
/// quarter of the sample rate per channel at least, where its encoder starts to code audio.
|
||||
Bitrate(u32),
|
||||
/// A constant quality from 0.0 (smallest) to 1.0 (best), at whatever bit rate each frame
|
||||
/// needs. AAC only; values outside the range are clamped, and NaN is an error.
|
||||
|
||||
@@ -104,12 +104,21 @@ fn aac_order(channels: usize) -> Option<&'static [usize]> {
|
||||
if channels == 8 { Some(&[2, 6, 7, 0, 1, 4, 5, 3]) } else { crate::aac::aac_order(channels) }
|
||||
}
|
||||
|
||||
/// The lowest AAC bit rate the writer uses, per channel: 6 kbit/s, and a quarter of the sample
|
||||
/// rate (256 bits a frame). The encoder accepts less, but then has too few bits for anything but
|
||||
/// silence, and a file of silence is no answer to a low bit rate (or to kbit/s given as bit/s).
|
||||
#[cfg(feature = "aac-encode")]
|
||||
fn aac_floor_per_channel(sample_rate: u32) -> u32 {
|
||||
(sample_rate / 4).max(6000)
|
||||
}
|
||||
|
||||
/// The AAC rate control for `rate`: a bit rate is kept within what the stream can carry, a
|
||||
/// quality of 0–1 spans the encoder's VBR range.
|
||||
#[cfg(feature = "aac-encode")]
|
||||
fn aac_mode(rate: Option<RateControl>, sample_rate: u32, channels: usize) -> crate::aac::BitrateMode {
|
||||
use crate::aac::BitrateMode;
|
||||
let (lo, hi) = crate::aac::cbr_range(sample_rate, channels);
|
||||
let lo = lo.max(aac_floor_per_channel(sample_rate).saturating_mul(channels as u32)).min(hi.max(lo));
|
||||
// An empty range (a rate or channel count the encoder refuses anyway) leaves the rate alone.
|
||||
let fit = |bps: u32| if lo <= hi { bps.clamp(lo, hi) } else { bps };
|
||||
match rate {
|
||||
|
||||
@@ -268,8 +268,16 @@ fn the_bit_rate_is_followed() {
|
||||
);
|
||||
assert!((7000..10_000).contains(&low) && (15_000..18_500).contains(&default) && (30_000..35_000).contains(&high), "{low} {default} {high}");
|
||||
// A rate no stream can carry is moved into range, not refused.
|
||||
encode(&buf, &EncodeOptions::new(EncodeFormat::Aac).with_bitrate(1)).unwrap();
|
||||
encode(&buf, &EncodeOptions::new(EncodeFormat::Aac).with_bitrate(u32::MAX)).unwrap();
|
||||
// The lowest rate still codes the audio: the encoder accepts rates that leave only silence
|
||||
// (128 for 128 kbit/s is one), and the writer does not go that low.
|
||||
for (sample_rate, channels) in [(8000, 1), (16_000, 2), (44_100, 2), (48_000, 6), (96_000, 2)] {
|
||||
let buf = tones(sample_rate, channels, sample_rate as usize);
|
||||
let (_, out) = decode(&encode(&buf, &EncodeOptions::new(EncodeFormat::Aac).with_bitrate(128)).unwrap(), Some("m4a")).unwrap();
|
||||
let rms = |x: &[f32]| (x.iter().map(|&s| f64::from(s).powi(2)).sum::<f64>() / x.len() as f64).sqrt();
|
||||
let level = rms(&out.channels[0]) / rms(&buf.channels[0]);
|
||||
assert!(level > 0.5, "{sample_rate} Hz, {channels} channels: {level} of the level");
|
||||
}
|
||||
// Quality is AAC's; ALAC has no rate to control.
|
||||
let (small, large) =
|
||||
(size(&EncodeOptions::new(EncodeFormat::Aac).with_quality(0.0)), size(&EncodeOptions::new(EncodeFormat::Aac).with_quality(1.0)));
|
||||
|
||||
@@ -640,12 +640,15 @@ impl<W: Write + Seek> Mp4Writer<W> {
|
||||
let start = w.stream_position()?;
|
||||
let mut b = BoxBuf::new();
|
||||
write_ftyp(&mut b, opts.brand);
|
||||
let pad_pos = start + b.len() as u64;
|
||||
let pad = b.len() as u64;
|
||||
b.leaf(if opts.brand == Brand::Mov { b"wide" } else { b"free" }, &[]);
|
||||
let mdat_pos = start + b.len() as u64;
|
||||
let mdat = b.len() as u64;
|
||||
b.leaf(b"mdat", &[]);
|
||||
// A writer positioned at the very end of what a position can count has no room for the
|
||||
// header; the positions below are sums that would overflow.
|
||||
let pos = start.checked_add(b.len() as u64).ok_or_else(|| Error::Mux("the writer's position leaves no room for a file".into()))?;
|
||||
let (pad_pos, mdat_pos) = (start + pad, start + mdat);
|
||||
w.write_all(&b.buf)?;
|
||||
let pos = start + b.len() as u64;
|
||||
Ok(Mp4Writer { w, opts, tracks: Vec::new(), start, pad_pos, mdat_pos, pos, last_track: None, chapters: Vec::new() })
|
||||
}
|
||||
|
||||
@@ -1401,6 +1404,17 @@ mod tests {
|
||||
assert!(out.windows(both.len()).any(|w| w == both));
|
||||
}
|
||||
|
||||
/// A writer positioned where the header's end would pass `u64::MAX` is refused, not overflowed.
|
||||
#[test]
|
||||
fn a_writer_at_the_end_of_the_position_range_is_refused() {
|
||||
for back in [0, 1, 10, 27] {
|
||||
let mut bytes = [0u8; 8];
|
||||
let mut sink = Cursor::new(&mut bytes[..]);
|
||||
sink.set_position(u64::MAX - back);
|
||||
assert!(matches!(Mp4Writer::new(sink, WriterOptions::new(Brand::Mp4)), Err(Error::Mux(_))), "{back} before the end");
|
||||
}
|
||||
}
|
||||
|
||||
/// A file begun in the middle of a writer is a file from there: its chunk offsets count from
|
||||
/// its own first byte, with `moov` at the end or in front, a timecode track and chapters.
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user