syom/options.rs
1//! Decode options: channel mode and duration / rate caps.
2//!
3//! Memory budgets (compressed bytes, collected PCM, channels, M4A index,
4//! resident workspace) live in [`crate::MemoryBudgets`] and are independent of
5//! `max_duration_secs`. `speech()` duration and `ChannelMode` are unchanged.
6
7use crate::budgets::MemoryBudgets;
8
9/// Hard upper bound on a finite compressed buffer or file (1 GiB).
10///
11/// Applies to one-shot `decode` / `decode_with` / `decode_streaming` /
12/// `read`. Independent of `max_duration_secs`. Streaming `Decoder::feed`
13/// must not inherit this as a lifetime cap ([`crate::InputScope`]; TASK-25).
14pub const DEFAULT_MAX_INPUT_BYTES: u64 = 1 << 30;
15
16/// Duration / rate caps for a lecture-length MP4.
17pub const DEFAULT_MAX_DURATION_SECS: f64 = 7200.0;
18pub const DEFAULT_MAX_SAMPLE_RATE: u32 = 192_000;
19/// Ceiling when converting duration → a per-channel sample budget.
20pub const DEFAULT_MAX_DECODE_SAMPLE_RATE: u32 = 48_000;
21
22/// Whether decoded channels are mixed to mono or kept separate.
23///
24/// [`Default`] is [`Self::Mono`]: it follows [`DecodeOptions::speech`],
25/// the one-call [`crate::decode`] / [`crate::read`] contract. General
26/// audio uses [`DecodeOptions::audio`] or [`DecodeOptions::unbounded`]
27/// ([`Self::Split`]), not a silent default flip.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub enum ChannelMode {
30 #[default]
31 Mono,
32 Split,
33}
34
35/// Options for `decode_with`.
36///
37/// `#[non_exhaustive]`: new fields (memory, labels, timing) may appear
38/// before 1.0. Construct via [`Self::speech`], [`Self::audio`],
39/// [`Self::unbounded`], or `..` update syntax.
40#[derive(Debug, Clone, PartialEq)]
41#[non_exhaustive]
42pub struct DecodeOptions {
43 pub channel_mode: ChannelMode,
44 /// Hard upper bound on decoded audio length (seconds).
45 pub max_duration_secs: f64,
46 /// Reject headers whose sample rate is above this value (or zero).
47 pub max_sample_rate: u32,
48 /// Cap used when deriving the per-channel sample budget from `max_duration_secs`.
49 pub max_decode_sample_rate: u32,
50 /// Independent input/output/workspace budgets (TASK-23/24).
51 pub memory: MemoryBudgets,
52}
53
54impl Default for DecodeOptions {
55 fn default() -> Self {
56 Self {
57 channel_mode: ChannelMode::Mono,
58 max_duration_secs: DEFAULT_MAX_DURATION_SECS,
59 max_sample_rate: DEFAULT_MAX_SAMPLE_RATE,
60 max_decode_sample_rate: DEFAULT_MAX_DECODE_SAMPLE_RATE,
61 memory: MemoryBudgets::default(),
62 }
63 }
64}
65
66impl DecodeOptions {
67 /// Mono + lecture caps (same as [`Default`]). One-call [`crate::decode`]
68 /// / [`crate::read`] use this. Any layout is mixed to one plane (non-LFE mean).
69 #[inline]
70 pub fn speech() -> Self {
71 Self::default()
72 }
73
74 /// Split channels and the same caps as [`speech`]: headers up to 192 kHz,
75 /// duration counted at most 48 kHz.
76 ///
77 /// Collection still uses the default 4 GiB / 8-channel fences, not
78 /// [`Self::unbounded`]. `decode` / `read` stay speech-mono; pass this
79 /// to [`crate::decode_with`] / [`crate::read_with`] when coded layout
80 /// must be kept.
81 ///
82 /// ```
83 /// use syom::{decode, decode_with, DecodeOptions};
84 /// let bytes = include_bytes!("goldens/lecture.m4a");
85 /// assert_eq!(decode(bytes)?.channels.len(), 1);
86 /// assert_eq!(decode_with(bytes, &DecodeOptions::audio())?.channels.len(), 2);
87 /// # Ok::<(), syom::AacError>(())
88 /// ```
89 #[inline]
90 pub fn audio() -> Self {
91 Self {
92 channel_mode: ChannelMode::Split,
93 ..Self::speech()
94 }
95 }
96
97 /// Split channels, no practical duration / rate ceiling.
98 #[inline]
99 pub fn unbounded() -> Self {
100 Self {
101 channel_mode: ChannelMode::Split,
102 max_duration_secs: f64::INFINITY,
103 max_sample_rate: u32::MAX,
104 max_decode_sample_rate: u32::MAX,
105 memory: MemoryBudgets::unbounded_collection(),
106 }
107 }
108
109 #[inline]
110 pub fn with_channel_mode(mut self, mode: ChannelMode) -> Self {
111 self.channel_mode = mode;
112 self
113 }
114
115 #[inline]
116 pub fn with_max_duration_secs(mut self, secs: f64) -> Self {
117 self.max_duration_secs = secs;
118 self
119 }
120
121 #[inline]
122 pub fn with_max_sample_rate(mut self, rate: u32) -> Self {
123 self.max_sample_rate = rate;
124 self
125 }
126
127 #[inline]
128 pub fn with_memory(mut self, memory: MemoryBudgets) -> Self {
129 self.memory = memory;
130 self
131 }
132}
133
134/// Output container for the encoder.
135///
136#[path = "options_container.rs"]
137mod container;
138pub use container::EncodeContainer;
139
140/// Options for `encode_with` / `write_with`.
141///
142/// `#[non_exhaustive]`: rate-control / preset fields may appear (TASK-65).
143#[derive(Debug, Clone, PartialEq)]
144#[non_exhaustive]
145pub struct EncodeOptions {
146 pub container: EncodeContainer,
147 /// ABR target in bits per second of the whole stream (TASK-65/66).
148 /// Per-frame ceiling plus EXT_FILL before `ID_END` keeps payload/valid
149 /// within ±3% on ≥10 s non-silent tracks. Silence and short inputs may
150 /// undershoot. Default is not CBR (ADTS fullness `0x7FF`). Cap:
151 /// `6144 · channels · sample_rate / 1024` (HE at the core rate; LD / 512).
152 pub bitrate_bps: u32,
153 /// One-frame attack lookahead (default off). When on, the attack
154 /// detector runs one frame ahead of the encode: an attack anywhere in
155 /// frame N+1 makes frame N a LongStart (its start-window slope covers
156 /// the pre-attack tail) and frame N+1 an EightShort, so even a click in
157 /// the first samples of a frame is coded on short windows — the causal
158 /// default codes such early attacks with the LongStart's flat-region
159 /// long transform, which leaves reduced-but-audible pre-echo. Costs one
160 /// extra frame of latency (1024 samples) in both one-shot and push
161 /// encode; the push `Encoder` flushes the held frame at `finish`.
162 pub lookahead: bool,
163 /// Terhardt absolute-threshold floor (TASK-68). Default off: production
164 /// goldens stay byte-identical. When on, bands below ATH at 0 dBFS =
165 /// 96 dB SPL are dropped in addition to the −60 dB relative floor.
166 pub ath: bool,
167 /// Johnston SFM tonality (TASK-69). Default off. When on, noise-like
168 /// bands get a lower `target_q` (0.25×); the coded mask is unchanged.
169 pub tonality: bool,
170 /// Short-window TNS (TASK-72). Default off — long TNS unchanged.
171 pub short_tns: bool,
172 /// Short-window grouping (TASK-71). Default off — 8 groups of 1.
173 pub short_group: bool,
174 /// Bandwise leftover-bit sf refine (TASK-74). Default off.
175 pub band_refine: bool,
176 /// Perceptual noise substitution (TASK-75). Default off.
177 pub pns: bool,
178 /// Intensity stereo (TASK-76). Default off.
179 pub intensity: bool,
180 /// HE-AAC v1 (SBR) instead of AAC-LC (TASK-90). Default off — `encode`
181 /// stays LC. The LC core runs at half the input rate (16 / 22.05 /
182 /// 24 / 32 / 44.1 / 48 kHz input only, else
183 /// [`crate::UnsupportedFeature::EncodeHeRate`]), mono / stereo,
184 /// `bitrate_bps` is the whole-stream budget (core + SBR; ≤ 6144 bits
185 /// per channel per core frame). ADTS signals SBR implicitly (header
186 /// carries the core rate); M4A and [`crate::Encoder::asc`] carry the
187 /// explicit two-rate AOT 5 config. Priming is 3018 output samples.
188 pub he: bool,
189 /// HE-AAC v2, see `with_he_v2`.
190 pub ps: bool,
191 /// Measurement only (TASK-134): 20-band fine IID (`iid_mode` 4).
192 /// [`Self::with_he_v2`] leaves this off, so the product path stays
193 /// coarse `iid_mode` 1.
194 #[doc(hidden)]
195 pub ps_iid_fine: bool,
196 /// AAC-LD (AOT 23, 512-sample frames). Default off — [`crate::encode`]
197 /// stays LC. LOAS or M4A only; ADTS cannot signal AOT 23. Mono or
198 /// stereo, priming one frame. Not combined with HE, lookahead, quality
199 /// VBR, or the LC-only tools.
200 pub ld: bool,
201 /// Quality VBR level `0..=10` (TASK-67). `None` (default) = ABR on
202 /// `bitrate_bps`. A level fixes one allowed-noise offset (level 5 is
203 /// the psy target; each step is 6 dB) with no rate target, padding,
204 /// or bit credit. The 6144 bits/channel cap coarsens uniformly.
205 /// `bitrate_bps` is ignored. LC only (`he` must be off).
206 pub quality: Option<u8>,
207 /// Opt-in CBR bit reservoir. Default off. See [`Self::with_cbr`].
208 pub cbr: bool,
209}
210
211impl Default for EncodeOptions {
212 fn default() -> Self {
213 Self {
214 container: EncodeContainer::Adts,
215 bitrate_bps: 128_000,
216 lookahead: false,
217 ath: false,
218 tonality: false,
219 short_tns: false,
220 short_group: false,
221 band_refine: false,
222 pns: false,
223 intensity: false,
224 he: false,
225 ps: false,
226 ps_iid_fine: false,
227 ld: false,
228 quality: None,
229 cbr: false,
230 }
231 }
232}
233
234impl EncodeOptions {
235 /// ADTS at 128 kbps (same as [`Default`]).
236 #[inline]
237 pub fn adts() -> Self {
238 Self::default()
239 }
240
241 /// M4A at 128 kbps.
242 #[inline]
243 pub fn m4a() -> Self {
244 Self {
245 container: EncodeContainer::M4a,
246 ..Self::default()
247 }
248 }
249
250 /// Development-curve preset (TASK-108, `lab/quality/CURVES.md`): AAC-LC
251 /// ADTS at 192 kbps, causal (1024-sample priming, no lookahead). The
252 /// top of that LC curve (noise 19.1 dB, click 18.8 dB, tremolo 56.9 dB,
253 /// voice-like 8.7 dB); about 1.5× the default's bytes. Not a
254 /// transparency claim.
255 #[inline]
256 pub fn high_quality() -> Self {
257 Self::default().with_bitrate_bps(192_000)
258 }
259
260 /// Development-curve preset (TASK-108): HE-AAC v1 ADTS at 48 kbps —
261 /// full-band content under 64 kbps (the LC core keeps the low band,
262 /// SBR carries the top octave; priming 3018 output samples). Below
263 /// 48 kbps on the speech lecture LC measured a higher SNR than HE;
264 /// tonal content above 64 kbps belongs to LC.
265 #[inline]
266 pub fn low_rate() -> Self {
267 Self::default().with_bitrate_bps(48_000).with_he(true)
268 }
269
270 /// Quality VBR level `0..=10` instead of an ABR target (LC only). One-shot
271 /// and push encode honor it identically; bytes are monotonic in the
272 /// level on aggregate (`lab/quality/CURVES.md`).
273 ///
274 /// ```
275 /// use syom::{EncodeOptions, encode_with};
276 /// let tone: Vec<f32> = (0..48_000).map(|i| 0.3 * (i as f32 * 0.0576).sin()).collect();
277 /// let lo = encode_with(&[tone.clone()], 48_000, &EncodeOptions::adts().with_quality(2))?;
278 /// let hi = encode_with(&[tone], 48_000, &EncodeOptions::adts().with_quality(8))?;
279 /// assert!(lo.len() < hi.len());
280 /// # Ok::<(), syom::AacError>(())
281 /// ```
282 #[inline]
283 pub fn with_quality(mut self, level: u8) -> Self {
284 self.quality = Some(level);
285 self
286 }
287
288 /// Raw access units at 128 kbps (push [`crate::Encoder`] only).
289 #[inline]
290 pub fn raw() -> Self {
291 Self {
292 container: EncodeContainer::Raw,
293 ..Self::default()
294 }
295 }
296
297 #[inline]
298 pub fn with_container(mut self, container: EncodeContainer) -> Self {
299 self.container = container;
300 self
301 }
302
303 #[inline]
304 pub fn with_bitrate_bps(mut self, bps: u32) -> Self {
305 self.bitrate_bps = bps;
306 self
307 }
308
309 /// One-frame attack lookahead: better pre-echo suppression on
310 /// early-in-frame onsets, at one extra frame (1024 samples) of latency.
311 /// Off by default; honored identically by one-shot encode and the push
312 /// `Encoder` (which stays byte-exact with one-shot for the same
313 /// options).
314 ///
315 /// ```
316 /// use syom::{DecodeOptions, EncodeOptions, encode_with, decode_with};
317 /// let pcm = vec![vec![0.0f32; 4096]];
318 /// let opts = EncodeOptions::adts().with_lookahead(true);
319 /// let adts = encode_with(&pcm, 48_000, &opts)?;
320 /// let dec = decode_with(&adts, &DecodeOptions::unbounded())?;
321 /// assert_eq!(dec.channels[0].len(), 4096); // source length; the tag trims the drain
322 /// # Ok::<(), syom::AacError>(())
323 /// ```
324 #[inline]
325 pub fn with_lookahead(mut self, on: bool) -> Self {
326 self.lookahead = on;
327 self
328 }
329
330 /// Terhardt ATH floor. Off by default; one-shot and push encode honor
331 /// it identically (byte-exact for the same options).
332 #[inline]
333 pub fn with_ath(mut self, on: bool) -> Self {
334 self.ath = on;
335 self
336 }
337
338 /// Johnston SFM tonality. Off by default; one-shot and push encode
339 /// honor it identically (byte-exact for the same options).
340 #[inline]
341 pub fn with_tonality(mut self, on: bool) -> Self {
342 self.tonality = on;
343 self
344 }
345
346 /// Short-window TNS. Off by default; long-window TNS is unchanged.
347 #[inline]
348 pub fn with_short_tns(mut self, on: bool) -> Self {
349 self.short_tns = on;
350 self
351 }
352
353 /// Short-window grouping. Off by default (8×1). One-shot and push
354 /// encode honor it identically (byte-exact for the same options).
355 #[inline]
356 pub fn with_short_group(mut self, on: bool) -> Self {
357 self.short_group = on;
358 self
359 }
360
361 /// Spend leftover frame bits on `sf[b] -= 1` for underfunded long-window
362 /// bands (at most 16 keeps / 48 tries). Off by default.
363 #[inline]
364 pub fn with_band_refine(mut self, on: bool) -> Self {
365 self.band_refine = on;
366 self
367 }
368
369 /// Perceptual noise substitution on long-window noise-like HF bands.
370 /// Off by default; one-shot and push encode honor it identically.
371 #[inline]
372 pub fn with_pns(mut self, on: bool) -> Self {
373 self.pns = on;
374 self
375 }
376
377 /// Intensity stereo on long-window HF bands. Off by default; one-shot
378 /// and push encode honor it identically.
379 #[inline]
380 pub fn with_intensity(mut self, on: bool) -> Self {
381 self.intensity = on;
382 self
383 }
384}
385
386#[path = "options_cbr.rs"]
387mod cbr_opt;
388#[path = "options_he.rs"]
389mod he_v2;
390#[path = "options_ld.rs"]
391mod ld;
392#[path = "options_validate.rs"]
393mod validate;
394
395#[cfg(test)]
396#[path = "options_tests.rs"]
397mod options_tests;