Skip to main content

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;