Skip to main content

denoize/encode/
stream.rs

1use std::fs::File;
2use std::io::{BufWriter, Read, Seek, SeekFrom, Write};
3use std::path::Path;
4
5use hound::{SampleFormat, WavSpec};
6
7use crate::audio::WavStreamWriter;
8use crate::channel_layout::ChannelMask;
9
10use super::pcm::StreamPcmLayout;
11use super::{EncodeOptions, OutputFormat};
12
13const IO_BUFFER_BYTES: u64 = 64 * 1024;
14const MP3_PRIVATE_ALLOWANCE_BYTES: u64 = 8 * 1024 * 1024;
15const OPUS_PRIVATE_ALLOWANCE_BYTES: u64 = 8 * 1024 * 1024;
16const FLAC_PRIVATE_ALLOWANCE_BYTES: u64 = 32 * 1024 * 1024;
17const AAC_PRIVATE_ALLOWANCE_BYTES: u64 = 128 * 1024 * 1024;
18const DEFAULT_MAX_AUXILIARY_TEMPORARY_BYTES: u64 = 1024 * 1024 * 1024;
19const AAC_LC_FRAME_FRAMES: u64 = 1024;
20const M4A_TABLE_RECORD_BYTES: u64 = 12;
21const STREAM_CONTAINER_ALLOWANCE_BYTES: u64 = 1024 * 1024;
22const FLAC_MAX_BYTES_PER_SAMPLE: u64 = 8;
23const MP3_MAX_FRAME_BYTES: u64 = 2 * 1024;
24const MP3_MIN_FRAMES_PER_PACKET: u64 = 576;
25const OPUS_FRAME_FRAMES: u64 = 960;
26const OGG_OPUS_MAX_PACKET_WITH_CONTAINER_BYTES: u64 = 8 * 1024;
27const DEFAULT_SPOOL_REPLAY_FRAMES: usize = 8_192;
28
29/// Duration-independent temporary-file bounds used by stream encoders.
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31#[non_exhaustive]
32pub struct StreamEncodeLimits {
33    max_auxiliary_temporary_bytes: u64,
34}
35
36impl StreamEncodeLimits {
37    #[must_use]
38    pub const fn new(max_auxiliary_temporary_bytes: u64) -> Self {
39        Self {
40            max_auxiliary_temporary_bytes,
41        }
42    }
43
44    #[must_use]
45    pub const fn max_auxiliary_temporary_bytes(self) -> u64 {
46        self.max_auxiliary_temporary_bytes
47    }
48}
49
50impl Default for StreamEncodeLimits {
51    fn default() -> Self {
52        Self::new(DEFAULT_MAX_AUXILIARY_TEMPORARY_BYTES)
53    }
54}
55
56/// PCM geometry used to configure a block-oriented output encoder.
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58#[non_exhaustive]
59pub struct StreamEncodeSpec {
60    pub sample_rate: u32,
61    pub channels: u16,
62    pub bits_per_sample: u16,
63    pub sample_format: SampleFormat,
64    pub channel_mask: Option<ChannelMask>,
65    /// Exact presentation frames when known before encoding.
66    pub total_frames: Option<u64>,
67}
68
69/// Geometry observed while validating a completed private stream output.
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71#[non_exhaustive]
72pub struct StreamOutputVerification {
73    pub format: crate::AudioFormat,
74    pub codec: crate::AudioCodec,
75    pub sample_rate: u32,
76    pub channels: u16,
77    pub presentation_frames: u64,
78}
79
80impl StreamEncodeSpec {
81    #[must_use]
82    pub const fn new(
83        wav: WavSpec,
84        channel_mask: Option<ChannelMask>,
85        total_frames: Option<u64>,
86    ) -> Self {
87        Self {
88            sample_rate: wav.sample_rate,
89            channels: wav.channels,
90            bits_per_sample: wav.bits_per_sample,
91            sample_format: wav.sample_format,
92            channel_mask,
93            total_frames,
94        }
95    }
96
97    #[must_use]
98    pub const fn wav_spec(self) -> WavSpec {
99        WavSpec {
100            channels: self.channels,
101            sample_rate: self.sample_rate,
102            bits_per_sample: self.bits_per_sample,
103            sample_format: self.sample_format,
104        }
105    }
106
107    pub(crate) fn validate_structure(self) -> Result<(), String> {
108        if self.channels == 0 {
109            return Err("stream output requires at least one channel".into());
110        }
111        if self.sample_rate == 0 {
112            return Err("stream output sample rate must be greater than zero".into());
113        }
114        if let Some(mask) = self.channel_mask {
115            if mask.bits() != 0 && mask.channels() != self.channels as usize {
116                return Err(format!(
117                    "stream output channel mask describes {} channels, but PCM has {}",
118                    mask.channels(),
119                    self.channels
120                ));
121            }
122        }
123        Ok(())
124    }
125}
126
127pub(super) fn validate_stream_config(
128    format: OutputFormat,
129    spec: StreamEncodeSpec,
130    options: EncodeOptions,
131) -> Result<(), String> {
132    options.validate_options(format)?;
133    spec.validate_structure()?;
134    match format {
135        OutputFormat::Wav => {
136            crate::audio::validate_wav_stream_spec(spec.wav_spec())?;
137            if let Some(frames) = spec.total_frames {
138                let bytes_per_sample = u64::from(spec.bits_per_sample / 8);
139                let data_bytes = frames
140                    .checked_mul(u64::from(spec.channels))
141                    .and_then(|samples| samples.checked_mul(bytes_per_sample))
142                    .ok_or_else(|| "WAV stream data length overflows".to_string())?;
143                super::validate_wav_container_size(
144                    data_bytes,
145                    u64::from(spec.channels),
146                    spec.bits_per_sample,
147                )?;
148            }
149            Ok(())
150        }
151        OutputFormat::Flac => {
152            super::flac::validate_geometry(
153                spec.sample_rate,
154                spec.channels as usize,
155                spec.bits_per_sample,
156            )?;
157            reject_known_empty(spec, "FLAC")
158        }
159        OutputFormat::OggOpus => {
160            reject_known_empty(spec, "Opus")?;
161            if spec.sample_rate > crate::config::MAX_SAMPLE_RATE {
162                return Err(format!(
163                    "Opus encode: unsupported source sample rate {} Hz (supported: 1..={})",
164                    spec.sample_rate,
165                    crate::config::MAX_SAMPLE_RATE
166                ));
167            }
168            let layout =
169                StreamPcmLayout::new(spec.channels as usize, spec.channel_mask, options.downmix)?;
170            crate::resample::validate_resampler_plan(
171                layout.output().count as usize,
172                spec.sample_rate,
173                48_000,
174            )
175        }
176        OutputFormat::Mp3 => {
177            reject_known_empty(spec, "MP3")?;
178            let layout =
179                StreamPcmLayout::new(spec.channels as usize, spec.channel_mask, options.downmix)?;
180            super::mp3::effective_mp3_stream_config(
181                spec.sample_rate,
182                layout.output(),
183                options.mp3_bitrate_kbps,
184            )
185            .map(|_| ())
186        }
187        OutputFormat::M4a | OutputFormat::AacAdts => {
188            let codec = if format == OutputFormat::M4a {
189                "M4A"
190            } else {
191                "AAC"
192            };
193            reject_known_empty(spec, codec)?;
194            let layout =
195                StreamPcmLayout::new(spec.channels as usize, spec.channel_mask, options.downmix)?;
196            if !super::AAC_ENCODER_SAMPLE_RATES.contains(&spec.sample_rate) {
197                return Err(format!(
198                    "{codec} encode: unsupported sample rate {} Hz (AAC standard rates only)",
199                    spec.sample_rate
200                ));
201            }
202            #[cfg(feature = "fdk-aac-encoder")]
203            if options.aac_encoder == super::AacEncoder::Fdk {
204                super::validate_fdk_aac_config(
205                    layout.output().count as usize,
206                    spec.sample_rate,
207                    options.m4a_bitrate_bps,
208                )?;
209            }
210            let _ = layout;
211            Ok(())
212        }
213    }
214}
215
216fn reject_known_empty(spec: StreamEncodeSpec, codec: &str) -> Result<(), String> {
217    if spec.total_frames == Some(0) {
218        Err(format!("{codec} output requires at least one frame"))
219    } else {
220        Ok(())
221    }
222}
223
224/// Conservative denoize-owned and explicitly-accounted codec bytes retained
225/// by a stream encoder in addition to the caller's input/enhanced blocks.
226pub fn estimate_stream_encode_additional_bytes(
227    format: OutputFormat,
228    spec: StreamEncodeSpec,
229    block_frames: usize,
230    options: EncodeOptions,
231) -> Result<u64, String> {
232    validate_stream_config(format, spec, options)?;
233    let layout = if matches!(
234        format,
235        OutputFormat::OggOpus | OutputFormat::Mp3 | OutputFormat::M4a | OutputFormat::AacAdts
236    ) {
237        Some(StreamPcmLayout::new(
238            spec.channels as usize,
239            spec.channel_mask,
240            options.downmix,
241        )?)
242    } else {
243        None
244    };
245    let output_channels = layout.as_ref().map_or(spec.channels as usize, |layout| {
246        layout.output().count as usize
247    });
248    let block_samples = checked_samples(block_frames, output_channels)?;
249    let conversion_bytes = block_samples
250        .checked_mul(std::mem::size_of::<f64>() as u64)
251        .ok_or_else(|| "stream encoder conversion byte count overflows".to_string())?;
252    let fixed = match format {
253        OutputFormat::Wav => IO_BUFFER_BYTES,
254        OutputFormat::Flac => FLAC_PRIVATE_ALLOWANCE_BYTES,
255        OutputFormat::Mp3 => MP3_PRIVATE_ALLOWANCE_BYTES,
256        OutputFormat::OggOpus => OPUS_PRIVATE_ALLOWANCE_BYTES
257            .checked_add(crate::resample::resampler_plan_bytes(
258                output_channels,
259                spec.sample_rate,
260                48_000,
261            )?)
262            .ok_or_else(|| "Opus stream encoder byte count overflows".to_string())?,
263        OutputFormat::M4a | OutputFormat::AacAdts => AAC_PRIVATE_ALLOWANCE_BYTES,
264    };
265    fixed
266        .checked_add(IO_BUFFER_BYTES)
267        .and_then(|bytes| bytes.checked_add(conversion_bytes))
268        .ok_or_else(|| "stream encoder byte count overflows".to_string())
269}
270
271/// Conservative peak bytes used while decoding a completed private output for
272/// pre-publication verification.
273///
274/// Encoding and verification are sequential phases. Callers should reserve
275/// the greater of this value and their live input/backend/encoder phase rather
276/// than summing both phases.
277pub fn estimate_stream_output_verification_bytes(
278    format: OutputFormat,
279    spec: StreamEncodeSpec,
280    block_frames: usize,
281    options: EncodeOptions,
282    encode_limits: StreamEncodeLimits,
283    decode_limits: crate::DecodeLimits,
284) -> Result<u64, String> {
285    validate_stream_config(format, spec, options)?;
286    if block_frames == 0 || block_frames > crate::config::MAX_STREAM_BLOCK_FRAMES {
287        return Err(format!(
288            "stream output verification block size must be between 1 and {} frames",
289            crate::config::MAX_STREAM_BLOCK_FRAMES
290        ));
291    }
292    let channels = if matches!(format, OutputFormat::Wav | OutputFormat::Flac) {
293        usize::from(spec.channels)
294    } else {
295        StreamPcmLayout::new(spec.channels as usize, spec.channel_mask, options.downmix)?
296            .output()
297            .count as usize
298    };
299    let block_bytes = checked_samples(block_frames, channels)?
300        .checked_mul(std::mem::size_of::<f64>() as u64)
301        .ok_or_else(|| "stream verification block byte count overflows".to_string())?;
302    let descriptors = u64::try_from(channels)
303        .ok()
304        .and_then(|channels| channels.checked_mul(std::mem::size_of::<Vec<f64>>() as u64))
305        .and_then(|bytes| bytes.checked_add(std::mem::size_of::<Vec<Vec<f64>>>() as u64))
306        .ok_or_else(|| "stream verification descriptor byte count overflows".to_string())?;
307    let add = |left: u64, right: u64, context: &str| {
308        left.checked_add(right)
309            .ok_or_else(|| format!("{context} byte count overflows"))
310    };
311    let verification = match format {
312        OutputFormat::Wav => add(
313            block_bytes
314                .checked_mul(2)
315                .ok_or_else(|| "WAV verification byte count overflows".to_string())?,
316            descriptors,
317            "WAV verification",
318        )?,
319        OutputFormat::Flac => {
320            let decoded = checked_samples(65_535, channels)?
321                .checked_mul(std::mem::size_of::<i32>() as u64)
322                .ok_or_else(|| "FLAC verification byte count overflows".to_string())?;
323            add(
324                add(decoded, block_bytes, "FLAC verification")?,
325                descriptors,
326                "FLAC verification",
327            )?
328        }
329        OutputFormat::Mp3 => {
330            let packet_pcm = checked_samples(1_152, channels)?
331                .checked_mul(std::mem::size_of::<f64>() as u64)
332                .and_then(|bytes| bytes.checked_mul(2))
333                .ok_or_else(|| "MP3 verification byte count overflows".to_string())?;
334            let id3 = u64::try_from(decode_limits.metadata.max_total_bytes)
335                .map_err(|_| "MP3 metadata limit does not fit in u64".to_string())?
336                .checked_mul(4)
337                .ok_or_else(|| "MP3 metadata verification byte count overflows".to_string())?;
338            [
339                packet_pcm,
340                block_bytes,
341                descriptors,
342                id3,
343                2 * 1024,
344                128 * 1024,
345            ]
346            .into_iter()
347            .try_fold(0_u64, |total, bytes| add(total, bytes, "MP3 verification"))?
348        }
349        OutputFormat::OggOpus => {
350            let scratch = checked_samples(5_760, channels)?
351                .checked_mul(std::mem::size_of::<f32>() as u64)
352                .ok_or_else(|| "Opus verification scratch byte count overflows".to_string())?;
353            let page_pcm = checked_samples(5_760 * 255, channels)?
354                .checked_mul(std::mem::size_of::<f32>() as u64)
355                .ok_or_else(|| "Opus verification page byte count overflows".to_string())?;
356            let packet = u64::try_from(decode_limits.metadata.max_ogg_packet_bytes)
357                .map_err(|_| "Opus packet limit does not fit in u64".to_string())?;
358            let packet_buffers = packet
359                .checked_mul(2)
360                .ok_or_else(|| "Opus packet verification byte count overflows".to_string())?;
361            [
362                scratch,
363                page_pcm,
364                block_bytes,
365                descriptors,
366                packet_buffers,
367                255 * 255,
368                256 * 1024,
369            ]
370            .into_iter()
371            .try_fold(0_u64, |total, bytes| add(total, bytes, "Opus verification"))?
372        }
373        OutputFormat::AacAdts | OutputFormat::M4a => {
374            let access_unit_bytes = if format == OutputFormat::M4a
375                && options.aac_encoder == super::AacEncoder::Fdk
376            {
377                #[cfg(feature = "fdk-aac-encoder")]
378                {
379                    let layout = StreamPcmLayout::new(
380                        spec.channels as usize,
381                        spec.channel_mask,
382                        options.downmix,
383                    )?;
384                    let (frame_length, _) = super::m4a_fdk::stream_timing(
385                        layout.output().count as usize,
386                        spec.sample_rate,
387                        options.m4a_bitrate_bps,
388                    )?;
389                    fdk_access_unit_ceiling(
390                        layout.output().count as usize,
391                        spec.sample_rate,
392                        frame_length,
393                        options.m4a_bitrate_bps,
394                    )?
395                }
396                #[cfg(not(feature = "fdk-aac-encoder"))]
397                {
398                    return Err(
399                        "FDK-AAC is unavailable in this build; rebuild with --features fdk-aac-encoder"
400                            .into(),
401                    );
402                }
403            } else {
404                oxide_adts_frame_ceiling(spec.sample_rate, options.m4a_bitrate_bps)?
405                    .saturating_sub(7)
406            };
407            let decoder = access_unit_bytes
408                .checked_mul(64 * 1024)
409                .and_then(|bytes| bytes.checked_add(AAC_PRIVATE_ALLOWANCE_BYTES))
410                .ok_or_else(|| "AAC verification decoder byte count overflows".to_string())?;
411            let decoded_packet = checked_samples(2_048, channels)?
412                .checked_mul((std::mem::size_of::<i16>() + std::mem::size_of::<f64>()) as u64)
413                .ok_or_else(|| "AAC verification frame byte count overflows".to_string())?;
414            let parser = if format == OutputFormat::M4a {
415                let table =
416                    estimate_stream_encode_temporary_bytes(format, spec, options, encode_limits)?;
417                let metadata = u64::try_from(decode_limits.metadata.max_total_bytes)
418                    .map_err(|_| "M4A metadata limit does not fit in u64".to_string())?;
419                let metadata = metadata
420                    .checked_mul(4)
421                    .ok_or_else(|| "M4A metadata verification byte count overflows".to_string())?;
422                table
423                    .checked_mul(8)
424                    .and_then(|bytes| bytes.checked_add(metadata))
425                    .and_then(|bytes| bytes.checked_add(STREAM_CONTAINER_ALLOWANCE_BYTES))
426                    .ok_or_else(|| "M4A verification parser byte count overflows".to_string())?
427            } else {
428                0
429            };
430            [
431                decoder,
432                access_unit_bytes,
433                decoded_packet,
434                block_bytes,
435                descriptors,
436                parser,
437            ]
438            .into_iter()
439            .try_fold(0_u64, |total, bytes| add(total, bytes, "AAC verification"))?
440        }
441    };
442    Ok(verification)
443}
444
445/// Temporary bytes retained outside the destination staging file.
446///
447/// M4A stores fixed-width sample-size/offset records in an anonymous bounded
448/// spool. A known presentation length yields the exact record bytes; an
449/// unknown length reserves the configured spool ceiling. Other encoders do
450/// not retain duration-sized auxiliary files and return zero.
451pub fn estimate_stream_encode_temporary_bytes(
452    format: OutputFormat,
453    spec: StreamEncodeSpec,
454    options: EncodeOptions,
455    limits: StreamEncodeLimits,
456) -> Result<u64, String> {
457    validate_stream_config(format, spec, options)?;
458    if format != OutputFormat::M4a {
459        return Ok(0);
460    }
461    let Some(frames) = spec.total_frames else {
462        return Ok(limits.max_auxiliary_temporary_bytes);
463    };
464    let (sample_duration, encoder_delay) = if options.aac_encoder == super::AacEncoder::Fdk {
465        #[cfg(feature = "fdk-aac-encoder")]
466        {
467            let layout =
468                StreamPcmLayout::new(spec.channels as usize, spec.channel_mask, options.downmix)?;
469            super::m4a_fdk::stream_timing(
470                layout.output().count as usize,
471                spec.sample_rate,
472                options.m4a_bitrate_bps,
473            )?
474        }
475        #[cfg(not(feature = "fdk-aac-encoder"))]
476        {
477            return Err(
478                "FDK-AAC is unavailable in this build; rebuild with --features fdk-aac-encoder"
479                    .into(),
480            );
481        }
482    } else {
483        (AAC_LC_FRAME_FRAMES as u32, AAC_LC_FRAME_FRAMES)
484    };
485    let media_frames = frames
486        .checked_add(encoder_delay)
487        .ok_or_else(|| "M4A sample-table duration overflows".to_string())?;
488    let access_units = media_frames.div_ceil(u64::from(sample_duration));
489    let required = access_units
490        .checked_mul(M4A_TABLE_RECORD_BYTES)
491        .ok_or_else(|| "M4A sample-table byte count overflows".to_string())?;
492    if required > limits.max_auxiliary_temporary_bytes {
493        return Err(format!(
494            "M4A sample table requires {required} bytes, exceeding its {}-byte limit",
495            limits.max_auxiliary_temporary_bytes
496        ));
497    }
498    Ok(required)
499}
500
501/// Conservative staged-file bytes for a stream whose presentation length is
502/// known, or `None` when the input container does not expose that length.
503///
504/// Bounds use each configured encoder's actual packet ceiling rather than a
505/// bitrate-average guess. Callers should add metadata and M4A auxiliary-table
506/// bytes, then still inspect the staged file before atomic publication.
507pub fn estimate_stream_encode_output_bytes(
508    format: OutputFormat,
509    spec: StreamEncodeSpec,
510    options: EncodeOptions,
511    limits: StreamEncodeLimits,
512) -> Result<Option<u64>, String> {
513    validate_stream_config(format, spec, options)?;
514    let Some(frames) = spec.total_frames else {
515        return Ok(None);
516    };
517    let channels = u64::from(spec.channels);
518    let bytes = match format {
519        OutputFormat::Wav => frames
520            .checked_mul(channels)
521            .and_then(|samples| samples.checked_mul(u64::from(spec.bits_per_sample / 8)))
522            .and_then(|data| data.checked_add(68))
523            .ok_or_else(|| "WAV stream output byte count overflows".to_string())?,
524        OutputFormat::Flac => frames
525            .checked_mul(channels)
526            .and_then(|samples| samples.checked_mul(FLAC_MAX_BYTES_PER_SAMPLE))
527            .and_then(|data| data.checked_add(STREAM_CONTAINER_ALLOWANCE_BYTES))
528            .ok_or_else(|| "FLAC stream output byte count overflows".to_string())?,
529        OutputFormat::Mp3 => frames
530            .div_ceil(MP3_MIN_FRAMES_PER_PACKET)
531            .checked_add(4)
532            .and_then(|packets| packets.checked_mul(MP3_MAX_FRAME_BYTES))
533            .and_then(|data| data.checked_add(STREAM_CONTAINER_ALLOWANCE_BYTES))
534            .ok_or_else(|| "MP3 stream output byte count overflows".to_string())?,
535        OutputFormat::OggOpus => {
536            let resampled_frames = rounded_resampled_frames(frames, spec.sample_rate, 48_000)?;
537            resampled_frames
538                .div_ceil(OPUS_FRAME_FRAMES)
539                .checked_add(2)
540                .and_then(|packets| packets.checked_mul(OGG_OPUS_MAX_PACKET_WITH_CONTAINER_BYTES))
541                .and_then(|data| data.checked_add(STREAM_CONTAINER_ALLOWANCE_BYTES))
542                .ok_or_else(|| "Ogg Opus stream output byte count overflows".to_string())?
543        }
544        OutputFormat::AacAdts => {
545            let access_units = frames
546                .div_ceil(AAC_LC_FRAME_FRAMES)
547                .checked_add(1)
548                .ok_or_else(|| "ADTS AAC stream access-unit count overflows".to_string())?;
549            access_units
550                .checked_mul(oxide_adts_frame_ceiling(
551                    spec.sample_rate,
552                    options.m4a_bitrate_bps,
553                )?)
554                .and_then(|data| data.checked_add(STREAM_CONTAINER_ALLOWANCE_BYTES))
555                .ok_or_else(|| "ADTS AAC stream output byte count overflows".to_string())?
556        }
557        OutputFormat::M4a => {
558            let table_bytes =
559                estimate_stream_encode_temporary_bytes(format, spec, options, limits)?;
560            let access_units = table_bytes / M4A_TABLE_RECORD_BYTES;
561            let access_unit_bytes = if options.aac_encoder == super::AacEncoder::Fdk {
562                #[cfg(feature = "fdk-aac-encoder")]
563                {
564                    let layout = StreamPcmLayout::new(
565                        spec.channels as usize,
566                        spec.channel_mask,
567                        options.downmix,
568                    )?;
569                    let (frame_length, _) = super::m4a_fdk::stream_timing(
570                        layout.output().count as usize,
571                        spec.sample_rate,
572                        options.m4a_bitrate_bps,
573                    )?;
574                    fdk_access_unit_ceiling(
575                        layout.output().count as usize,
576                        spec.sample_rate,
577                        frame_length,
578                        options.m4a_bitrate_bps,
579                    )?
580                }
581                #[cfg(not(feature = "fdk-aac-encoder"))]
582                {
583                    return Err(
584                        "FDK-AAC is unavailable in this build; rebuild with --features fdk-aac-encoder"
585                            .into(),
586                    );
587                }
588            } else {
589                oxide_adts_frame_ceiling(spec.sample_rate, options.m4a_bitrate_bps)?
590                    .saturating_sub(7)
591            };
592            access_units
593                .checked_mul(access_unit_bytes)
594                .and_then(|data| data.checked_add(table_bytes))
595                .and_then(|data| data.checked_add(STREAM_CONTAINER_ALLOWANCE_BYTES))
596                .ok_or_else(|| "M4A stream output byte count overflows".to_string())?
597        }
598    };
599    Ok(Some(bytes))
600}
601
602/// Conservative anonymous-spool bytes for a non-seekable output sink.
603///
604/// The spooled writer retains interleaved `f64` PCM until final geometry is
605/// known, then encodes into a second anonymous file and validates that file
606/// before copying any bytes to the caller's `Write` sink. Both live spools and
607/// codec auxiliary data are included here.
608pub fn estimate_spooled_stream_output_bytes(
609    format: OutputFormat,
610    spec: StreamEncodeSpec,
611    options: EncodeOptions,
612    limits: StreamEncodeLimits,
613) -> Result<Option<u64>, String> {
614    validate_stream_config(format, spec, options)?;
615    spec.total_frames
616        .map(|frames| {
617            estimate_spooled_stream_output_for_frames(format, spec, frames, options, limits)
618        })
619        .transpose()
620}
621
622fn estimate_spooled_stream_output_for_frames(
623    format: OutputFormat,
624    mut spec: StreamEncodeSpec,
625    frames: u64,
626    options: EncodeOptions,
627    limits: StreamEncodeLimits,
628) -> Result<u64, String> {
629    spec.total_frames = Some(frames);
630    let pcm = frames
631        .checked_mul(u64::from(spec.channels))
632        .and_then(|samples| samples.checked_mul(std::mem::size_of::<f64>() as u64))
633        .ok_or_else(|| "non-seekable PCM spool byte count overflows".to_string())?;
634    let encoded = estimate_stream_encode_output_bytes(format, spec, options, limits)?
635        .ok_or_else(|| "known non-seekable output length produced no encoded bound".to_string())?;
636    let auxiliary = estimate_stream_encode_temporary_bytes(format, spec, options, limits)?;
637    pcm.checked_add(encoded)
638        .and_then(|bytes| bytes.checked_add(auxiliary))
639        .ok_or_else(|| "non-seekable output spool byte count overflows".to_string())
640}
641
642fn rounded_resampled_frames(frames: u64, from_rate: u32, to_rate: u32) -> Result<u64, String> {
643    let numerator = u128::from(frames)
644        .checked_mul(u128::from(to_rate))
645        .and_then(|value| value.checked_add(u128::from(from_rate) / 2))
646        .ok_or_else(|| "stream resampled frame count overflows".to_string())?;
647    u64::try_from(numerator / u128::from(from_rate))
648        .map_err(|_| "stream resampled frame count exceeds u64".to_string())
649}
650
651fn expected_stream_output_frames(
652    format: OutputFormat,
653    source_rate: u32,
654    encoded_input_frames: u64,
655) -> Result<u64, String> {
656    match format {
657        OutputFormat::Wav | OutputFormat::Flac | OutputFormat::M4a => Ok(encoded_input_frames),
658        OutputFormat::OggOpus => {
659            rounded_resampled_frames(encoded_input_frames, source_rate, 48_000)
660        }
661        OutputFormat::Mp3 => {
662            // MPEG-1 Layer III carries 1,152 PCM frames per packet; MPEG-2 and
663            // 2.5 carry 576. The Shine writer emits at least two complete
664            // packets so even a tiny file is independently demuxable.
665            let packet_frames = if source_rate >= 32_000 { 1_152 } else { 576 };
666            let minimum = packet_frames * 2;
667            Ok(encoded_input_frames.max(minimum).div_ceil(packet_frames) * packet_frames)
668        }
669        OutputFormat::AacAdts => {
670            // Raw ADTS cannot signal codec delay or final padding. Oxide emits
671            // one delayed access unit after the padded source access units.
672            encoded_input_frames
673                .div_ceil(AAC_LC_FRAME_FRAMES)
674                .checked_add(1)
675                .and_then(|units| units.checked_mul(AAC_LC_FRAME_FRAMES))
676                .ok_or_else(|| "ADTS AAC presentation frame count overflows".to_string())
677        }
678    }
679}
680
681fn expected_stream_output_identity(
682    format: OutputFormat,
683) -> (crate::AudioFormat, crate::AudioCodec) {
684    match format {
685        OutputFormat::Wav => (crate::AudioFormat::Wav, crate::AudioCodec::Pcm),
686        OutputFormat::Flac => (crate::AudioFormat::Flac, crate::AudioCodec::Flac),
687        OutputFormat::OggOpus => (crate::AudioFormat::OggOpus, crate::AudioCodec::Opus),
688        OutputFormat::Mp3 => (crate::AudioFormat::Mp3, crate::AudioCodec::Mp3),
689        OutputFormat::M4a => (crate::AudioFormat::M4a, crate::AudioCodec::Aac),
690        OutputFormat::AacAdts => (crate::AudioFormat::AacAdts, crate::AudioCodec::Aac),
691    }
692}
693
694/// Decode and validate a completed private stream output before publication.
695///
696/// The supplied handle is authoritative; `display_path` is used only in error
697/// messages, so a concurrent pathname replacement cannot redirect validation.
698/// Validation walks the complete encoded stream with the bounded block decoder
699/// and rejects container/codec, channel, sample-rate, or presentation-duration
700/// drift. The caller may then fingerprint and atomically publish the same open
701/// file. The file cursor is unspecified on return.
702pub fn verify_stream_output_file(
703    file: &mut File,
704    display_path: &Path,
705    format: OutputFormat,
706    spec: StreamEncodeSpec,
707    encoded_input_frames: u64,
708    options: EncodeOptions,
709    limits: crate::DecodeLimits,
710    block_frames: usize,
711) -> Result<StreamOutputVerification, String> {
712    validate_stream_config(format, spec, options)?;
713    if block_frames == 0 || block_frames > crate::config::MAX_STREAM_BLOCK_FRAMES {
714        return Err(format!(
715            "stream output verification block size must be between 1 and {} frames",
716            crate::config::MAX_STREAM_BLOCK_FRAMES
717        ));
718    }
719    if spec
720        .total_frames
721        .is_some_and(|declared| declared != encoded_input_frames)
722    {
723        return Err(format!(
724            "stream output verification received {encoded_input_frames} frames, but the encoder declared {}",
725            spec.total_frames.unwrap_or(0)
726        ));
727    }
728
729    file.flush()
730        .map_err(|error| format!("flush staged stream output: {error}"))?;
731    file.seek(SeekFrom::Start(0))
732        .map_err(|error| format!("rewind staged stream output: {error}"))?;
733    let mut source = file
734        .try_clone()
735        .map_err(|error| format!("clone staged stream output: {error}"))?;
736    source
737        .seek(SeekFrom::Start(0))
738        .map_err(|error| format!("rewind cloned staged stream output: {error}"))?;
739    let session =
740        crate::AudioInputSession::from_open_file(display_path, source, "staged stream output")?;
741    let mut reader = crate::AudioStreamReader::from_session(session, limits)?;
742    let info = reader.info();
743    let (expected_format, expected_codec) = expected_stream_output_identity(format);
744    if info.format != expected_format || info.codec != expected_codec {
745        return Err(format!(
746            "staged stream output identifies as {:?}/{:?}, expected {:?}/{:?}",
747            info.format, info.codec, expected_format, expected_codec
748        ));
749    }
750
751    let expected_channels = if matches!(format, OutputFormat::Wav | OutputFormat::Flac) {
752        spec.channels
753    } else {
754        u16::from(
755            StreamPcmLayout::new(spec.channels as usize, spec.channel_mask, options.downmix)?
756                .output()
757                .count,
758        )
759    };
760    let expected_sample_rate = if format == OutputFormat::OggOpus {
761        48_000
762    } else {
763        spec.sample_rate
764    };
765    if info.output_spec.channels != expected_channels
766        || info.output_spec.sample_rate != expected_sample_rate
767    {
768        return Err(format!(
769            "staged stream output geometry is {}ch at {} Hz, expected {expected_channels}ch at {expected_sample_rate} Hz",
770            info.output_spec.channels, info.output_spec.sample_rate
771        ));
772    }
773
774    let mut presentation_frames = 0_u64;
775    while let Some(block) = reader.next_block(block_frames)? {
776        if block.len() != usize::from(expected_channels) {
777            return Err("staged stream output channel count changed while decoding".into());
778        }
779        let frames = block.first().map_or(0, Vec::len);
780        if frames == 0 || block.iter().any(|channel| channel.len() != frames) {
781            return Err("staged stream output decoder returned an invalid block".into());
782        }
783        presentation_frames = presentation_frames
784            .checked_add(frames as u64)
785            .ok_or_else(|| "staged stream output frame count overflows".to_string())?;
786    }
787    let expected_frames =
788        expected_stream_output_frames(format, spec.sample_rate, encoded_input_frames)?;
789    if presentation_frames != expected_frames {
790        return Err(format!(
791            "staged stream output decodes to {presentation_frames} presentation frames, expected {expected_frames}"
792        ));
793    }
794    Ok(StreamOutputVerification {
795        format: info.format,
796        codec: info.codec,
797        sample_rate: info.output_spec.sample_rate,
798        channels: info.output_spec.channels,
799        presentation_frames,
800    })
801}
802
803fn oxide_adts_frame_ceiling(sample_rate: u32, bitrate_bps: u32) -> Result<u64, String> {
804    let bits = u64::from(bitrate_bps)
805        .checked_mul(AAC_LC_FRAME_FRAMES)
806        .ok_or_else(|| "AAC per-frame bitrate budget overflows".to_string())?
807        / u64::from(sample_rate);
808    Ok((bits / 8).max(23))
809}
810
811#[cfg(feature = "fdk-aac-encoder")]
812fn fdk_access_unit_ceiling(
813    channels: usize,
814    sample_rate: u32,
815    frame_length: u32,
816    bitrate_bps: u32,
817) -> Result<u64, String> {
818    let nominal_bits = u64::from(bitrate_bps)
819        .checked_mul(u64::from(frame_length))
820        .and_then(|bits| bits.checked_add(u64::from(sample_rate) - 1))
821        .ok_or_else(|| "FDK-AAC per-frame bitrate budget overflows".to_string())?
822        / u64::from(sample_rate);
823    let reservoir_bits = u64::try_from(channels)
824        .ok()
825        .and_then(|channels| channels.checked_mul(6_144))
826        .ok_or_else(|| "FDK-AAC reservoir budget overflows".to_string())?;
827    Ok(nominal_bits.max(reservoir_bits).div_ceil(8))
828}
829
830fn checked_samples(frames: usize, channels: usize) -> Result<u64, String> {
831    u64::try_from(frames)
832        .ok()
833        .and_then(|frames| frames.checked_mul(u64::try_from(channels).ok()?))
834        .ok_or_else(|| "stream encoder sample count overflows".to_string())
835}
836
837/// One seekable block-oriented output transaction.
838///
839/// The writer borrows the caller's private staging sink. [`finalize`](Self::finalize)
840/// completes container headers and releases that borrow before metadata or an
841/// atomic publication step is applied by the caller.
842pub struct AudioStreamWriter<'a, W: Write + Seek> {
843    inner: StreamWriterInner<'a, W>,
844    spec: StreamEncodeSpec,
845    frames_written: u64,
846}
847
848enum StreamWriterInner<'a, W: Write + Seek> {
849    Wav(WavStreamWriter<BufWriter<&'a mut W>>),
850    Flac(super::flac::FlacStreamWriter<&'a mut W>),
851    OggOpus(super::opus::OggOpusStreamWriter<&'a mut W>),
852    Mp3(super::mp3::Mp3StreamWriter<&'a mut W>),
853    #[cfg(feature = "m4a-encode")]
854    M4a(super::m4a::M4aStreamWriter<&'a mut W>),
855    #[cfg(feature = "fdk-aac-encoder")]
856    M4aFdk(super::m4a_fdk::FdkM4aStreamWriter<&'a mut W>),
857    #[cfg(feature = "m4a-encode")]
858    AacAdts(super::aac::AdtsAacStreamWriter<&'a mut W>),
859}
860
861impl<'a, W: Write + Seek> AudioStreamWriter<'a, W> {
862    pub fn new(
863        sink: &'a mut W,
864        format: OutputFormat,
865        spec: StreamEncodeSpec,
866        options: EncodeOptions,
867    ) -> Result<Self, String> {
868        Self::new_with_limits(sink, format, spec, options, StreamEncodeLimits::default())
869    }
870
871    pub fn new_with_limits(
872        sink: &'a mut W,
873        format: OutputFormat,
874        spec: StreamEncodeSpec,
875        options: EncodeOptions,
876        limits: StreamEncodeLimits,
877    ) -> Result<Self, String> {
878        validate_stream_config(format, spec, options)?;
879        estimate_stream_encode_temporary_bytes(format, spec, options, limits)?;
880        let inner = match format {
881            OutputFormat::Wav => StreamWriterInner::Wav(WavStreamWriter::from_sink(
882                BufWriter::new(sink),
883                spec.wav_spec(),
884            )?),
885            OutputFormat::Flac => StreamWriterInner::Flac(super::flac::FlacStreamWriter::new(
886                sink,
887                spec.sample_rate,
888                spec.channels as usize,
889                spec.bits_per_sample,
890            )?),
891            OutputFormat::OggOpus => {
892                StreamWriterInner::OggOpus(super::opus::OggOpusStreamWriter::new(
893                    sink,
894                    spec.sample_rate,
895                    spec.channels as usize,
896                    spec.channel_mask,
897                    128_000,
898                    options.downmix,
899                )?)
900            }
901            OutputFormat::Mp3 => StreamWriterInner::Mp3(super::mp3::Mp3StreamWriter::new(
902                sink,
903                spec.sample_rate,
904                spec.channels as usize,
905                spec.channel_mask,
906                options.mp3_bitrate_kbps,
907                options.downmix,
908            )?),
909            OutputFormat::M4a => {
910                #[cfg(feature = "m4a-encode")]
911                {
912                    if options.aac_encoder == super::AacEncoder::Fdk {
913                        #[cfg(feature = "fdk-aac-encoder")]
914                        {
915                            StreamWriterInner::M4aFdk(super::m4a_fdk::FdkM4aStreamWriter::new(
916                                sink,
917                                spec.sample_rate,
918                                spec.channels as usize,
919                                spec.channel_mask,
920                                options.m4a_bitrate_bps,
921                                options.downmix,
922                                Some(limits.max_auxiliary_temporary_bytes),
923                            )?)
924                        }
925                        #[cfg(not(feature = "fdk-aac-encoder"))]
926                        {
927                            return Err(
928                                "FDK-AAC is unavailable in this build; rebuild with --features fdk-aac-encoder"
929                                    .into(),
930                            );
931                        }
932                    } else {
933                        StreamWriterInner::M4a(super::m4a::M4aStreamWriter::new(
934                            sink,
935                            spec.sample_rate,
936                            spec.channels as usize,
937                            spec.channel_mask,
938                            options.m4a_bitrate_bps,
939                            options.downmix,
940                            Some(limits.max_auxiliary_temporary_bytes),
941                        )?)
942                    }
943                }
944                #[cfg(not(feature = "m4a-encode"))]
945                {
946                    return Err(
947                        "M4A output is unavailable in this build; rebuild with --features m4a-encode"
948                            .into(),
949                    );
950                }
951            }
952            OutputFormat::AacAdts => {
953                #[cfg(feature = "m4a-encode")]
954                {
955                    if options.aac_encoder == super::AacEncoder::Fdk {
956                        return Err(
957                            "FDK-AAC ADTS output is not available; use M4A or --aac-encoder oxide"
958                                .into(),
959                        );
960                    }
961                    StreamWriterInner::AacAdts(super::aac::AdtsAacStreamWriter::new(
962                        sink,
963                        spec.sample_rate,
964                        spec.channels as usize,
965                        spec.channel_mask,
966                        options.m4a_bitrate_bps,
967                        options.downmix,
968                    )?)
969                }
970                #[cfg(not(feature = "m4a-encode"))]
971                {
972                    return Err(
973                        "AAC output is unavailable in this build; rebuild with --features m4a-encode"
974                            .into(),
975                    );
976                }
977            }
978        };
979        Ok(Self {
980            inner,
981            spec,
982            frames_written: 0,
983        })
984    }
985
986    pub fn write_block(&mut self, channels: &[Vec<f64>]) -> Result<(), String> {
987        if channels.len() != self.spec.channels as usize {
988            return Err(format!(
989                "stream output expected {} channels, received {}",
990                self.spec.channels,
991                channels.len()
992            ));
993        }
994        let frames = channels.first().map_or(0, Vec::len);
995        if channels.iter().any(|channel| channel.len() != frames) {
996            return Err("stream output blocks must have equal channel lengths".into());
997        }
998        // Stateful processors and resamplers may legitimately emit an empty
999        // block while retaining overlap. Treat it as a no-op after validating
1000        // the channel geometry; several codec APIs reject a zero-length call.
1001        if frames == 0 {
1002            return Ok(());
1003        }
1004        let next = self
1005            .frames_written
1006            .checked_add(frames as u64)
1007            .ok_or_else(|| "stream output frame count overflows".to_string())?;
1008        if self.spec.total_frames.is_some_and(|total| next > total) {
1009            return Err("stream output exceeds its declared presentation length".into());
1010        }
1011        match &mut self.inner {
1012            StreamWriterInner::Wav(writer) => writer.write_block(channels),
1013            StreamWriterInner::Flac(writer) => writer.write_block(channels),
1014            StreamWriterInner::OggOpus(writer) => writer.write_block(channels),
1015            StreamWriterInner::Mp3(writer) => writer.write_block(channels),
1016            #[cfg(feature = "m4a-encode")]
1017            StreamWriterInner::M4a(writer) => writer.write_block(channels),
1018            #[cfg(feature = "fdk-aac-encoder")]
1019            StreamWriterInner::M4aFdk(writer) => writer.write_block(channels),
1020            #[cfg(feature = "m4a-encode")]
1021            StreamWriterInner::AacAdts(writer) => writer.write_block(channels),
1022        }?;
1023        self.frames_written = next;
1024        Ok(())
1025    }
1026
1027    pub fn finalize(self) -> Result<(), String> {
1028        if self
1029            .spec
1030            .total_frames
1031            .is_some_and(|total| total != self.frames_written)
1032        {
1033            return Err(format!(
1034                "stream output wrote {} frames, expected {}",
1035                self.frames_written,
1036                self.spec.total_frames.unwrap_or(0)
1037            ));
1038        }
1039        match self.inner {
1040            StreamWriterInner::Wav(writer) => writer.finalize(),
1041            StreamWriterInner::Flac(writer) => writer.finalize(),
1042            StreamWriterInner::OggOpus(writer) => writer.finalize(),
1043            StreamWriterInner::Mp3(writer) => writer.finalize(),
1044            #[cfg(feature = "m4a-encode")]
1045            StreamWriterInner::M4a(writer) => writer.finalize(),
1046            #[cfg(feature = "fdk-aac-encoder")]
1047            StreamWriterInner::M4aFdk(writer) => writer.finalize(),
1048            #[cfg(feature = "m4a-encode")]
1049            StreamWriterInner::AacAdts(writer) => writer.finalize(),
1050        }
1051    }
1052}
1053
1054/// Block-oriented encoder for a plain non-seekable [`Write`] sink.
1055///
1056/// Input PCM is retained in a finite anonymous file. On [`finalize`](Self::finalize),
1057/// it is replayed through [`AudioStreamWriter`], fully decoded and validated,
1058/// and only then copied to the supplied sink. Consequently an encode or
1059/// verification error writes no caller-visible bytes. A sink I/O error during
1060/// the final copy can still leave a partial external stream; non-seekable
1061/// destinations cannot provide filesystem atomicity.
1062pub struct SpooledAudioStreamWriter<W: Write> {
1063    sink: W,
1064    pcm: File,
1065    format: OutputFormat,
1066    spec: StreamEncodeSpec,
1067    options: EncodeOptions,
1068    encode_limits: StreamEncodeLimits,
1069    decode_limits: crate::DecodeLimits,
1070    spool_limits: crate::StreamSpoolLimits,
1071    replay_frames: usize,
1072    frames_written: u64,
1073    pcm_bytes: u64,
1074}
1075
1076/// Finite anonymous interleaved-f64 spool for multi-pass stream processing.
1077///
1078/// This is used when a first pass must finish before encoding can begin, such
1079/// as integrated-loudness normalization. It never names or publishes a file;
1080/// dropping it removes the operating-system-managed anonymous storage.
1081pub struct StreamPcmSpool {
1082    pcm: File,
1083    channels: usize,
1084    frames: u64,
1085    bytes: u64,
1086    max_bytes: u64,
1087    read_frames: u64,
1088    reading: bool,
1089}
1090
1091impl StreamPcmSpool {
1092    pub fn new(channels: usize, max_bytes: u64) -> Result<Self, String> {
1093        if channels == 0 || channels > crate::config::MAX_STREAM_CHANNELS {
1094            return Err(format!(
1095                "PCM stream spool channels must be between 1 and {}",
1096                crate::config::MAX_STREAM_CHANNELS
1097            ));
1098        }
1099        let pcm = tempfile::tempfile()
1100            .map_err(|error| format!("create anonymous PCM stream spool: {error}"))?;
1101        Ok(Self {
1102            pcm,
1103            channels,
1104            frames: 0,
1105            bytes: 0,
1106            max_bytes,
1107            read_frames: 0,
1108            reading: false,
1109        })
1110    }
1111
1112    pub fn write_block(&mut self, channels: &[Vec<f64>]) -> Result<(), String> {
1113        if self.reading {
1114            return Err("PCM stream spool cannot append after replay has begun".into());
1115        }
1116        if channels.len() != self.channels {
1117            return Err(format!(
1118                "PCM stream spool expected {} channels, received {}",
1119                self.channels,
1120                channels.len()
1121            ));
1122        }
1123        let frames = channels.first().map_or(0, Vec::len);
1124        if channels.iter().any(|channel| channel.len() != frames) {
1125            return Err("PCM stream spool blocks must have equal channel lengths".into());
1126        }
1127        let block_bytes = (frames as u64)
1128            .checked_mul(self.channels as u64)
1129            .and_then(|samples| samples.checked_mul(std::mem::size_of::<f64>() as u64))
1130            .ok_or_else(|| "PCM stream spool block size overflows".to_string())?;
1131        let next_bytes = self
1132            .bytes
1133            .checked_add(block_bytes)
1134            .ok_or_else(|| "PCM stream spool size overflows".to_string())?;
1135        if next_bytes > self.max_bytes {
1136            return Err(format!(
1137                "PCM stream spool requires {next_bytes} bytes, exceeding its {}-byte limit",
1138                self.max_bytes
1139            ));
1140        }
1141        for frame in 0..frames {
1142            for channel in channels {
1143                self.pcm
1144                    .write_all(&crate::sanitize_sample(channel[frame]).to_le_bytes())
1145                    .map_err(|error| format!("write anonymous PCM stream spool: {error}"))?;
1146            }
1147        }
1148        self.frames = self
1149            .frames
1150            .checked_add(frames as u64)
1151            .ok_or_else(|| "PCM stream spool frame count overflows".to_string())?;
1152        self.bytes = next_bytes;
1153        Ok(())
1154    }
1155
1156    pub fn prepare_read(&mut self) -> Result<(), String> {
1157        self.pcm
1158            .flush()
1159            .and_then(|_| self.pcm.seek(SeekFrom::Start(0)).map(|_| ()))
1160            .map_err(|error| format!("rewind anonymous PCM stream spool: {error}"))?;
1161        self.read_frames = 0;
1162        self.reading = true;
1163        Ok(())
1164    }
1165
1166    pub fn next_block(&mut self, max_frames: usize) -> Result<Option<Vec<Vec<f64>>>, String> {
1167        if max_frames == 0 || max_frames > crate::config::MAX_STREAM_BLOCK_FRAMES {
1168            return Err(format!(
1169                "PCM stream spool replay size must be between 1 and {} frames",
1170                crate::config::MAX_STREAM_BLOCK_FRAMES
1171            ));
1172        }
1173        if !self.reading {
1174            return Err("PCM stream spool must be prepared before replay".into());
1175        }
1176        let remaining = self.frames.saturating_sub(self.read_frames);
1177        if remaining == 0 {
1178            return Ok(None);
1179        }
1180        let frames = remaining.min(max_frames as u64) as usize;
1181        let block = read_interleaved_pcm_block(&mut self.pcm, self.channels, frames)?;
1182        self.read_frames += frames as u64;
1183        Ok(Some(block))
1184    }
1185
1186    #[must_use]
1187    pub const fn frames(&self) -> u64 {
1188        self.frames
1189    }
1190
1191    #[must_use]
1192    pub const fn len(&self) -> u64 {
1193        self.bytes
1194    }
1195
1196    #[must_use]
1197    pub const fn is_empty(&self) -> bool {
1198        self.frames == 0
1199    }
1200}
1201
1202impl<W: Write> SpooledAudioStreamWriter<W> {
1203    /// Construct a non-seekable writer with finite default spool/codec limits.
1204    pub fn new(
1205        sink: W,
1206        format: OutputFormat,
1207        spec: StreamEncodeSpec,
1208        options: EncodeOptions,
1209    ) -> Result<Self, String> {
1210        Self::new_with_limits(
1211            sink,
1212            format,
1213            spec,
1214            options,
1215            StreamEncodeLimits::default(),
1216            crate::DecodeLimits::default(),
1217            crate::StreamSpoolLimits::default(),
1218            DEFAULT_SPOOL_REPLAY_FRAMES,
1219        )
1220    }
1221
1222    /// Construct a non-seekable writer with explicit finite limits.
1223    #[allow(clippy::too_many_arguments)]
1224    pub fn new_with_limits(
1225        sink: W,
1226        format: OutputFormat,
1227        spec: StreamEncodeSpec,
1228        options: EncodeOptions,
1229        encode_limits: StreamEncodeLimits,
1230        decode_limits: crate::DecodeLimits,
1231        spool_limits: crate::StreamSpoolLimits,
1232        replay_frames: usize,
1233    ) -> Result<Self, String> {
1234        validate_stream_config(format, spec, options)?;
1235        if !(1..=crate::config::MAX_STREAM_BLOCK_FRAMES).contains(&replay_frames) {
1236            return Err(format!(
1237                "non-seekable output replay size must be between 1 and {} frames",
1238                crate::config::MAX_STREAM_BLOCK_FRAMES
1239            ));
1240        }
1241        if let Some(required) =
1242            estimate_spooled_stream_output_bytes(format, spec, options, encode_limits)?
1243        {
1244            ensure_spool_limit(required, spool_limits, "declared non-seekable output")?;
1245        }
1246        let pcm = tempfile::tempfile()
1247            .map_err(|error| format!("create anonymous PCM output spool: {error}"))?;
1248        Ok(Self {
1249            sink,
1250            pcm,
1251            format,
1252            spec,
1253            options,
1254            encode_limits,
1255            decode_limits,
1256            spool_limits,
1257            replay_frames,
1258            frames_written: 0,
1259            pcm_bytes: 0,
1260        })
1261    }
1262
1263    /// Append one planar block without retaining it in memory after return.
1264    pub fn write_block(&mut self, channels: &[Vec<f64>]) -> Result<(), String> {
1265        if channels.len() != usize::from(self.spec.channels) {
1266            return Err(format!(
1267                "non-seekable stream output expected {} channels, received {}",
1268                self.spec.channels,
1269                channels.len()
1270            ));
1271        }
1272        let frames = channels.first().map_or(0, Vec::len);
1273        if channels.iter().any(|channel| channel.len() != frames) {
1274            return Err("non-seekable stream blocks must have equal channel lengths".into());
1275        }
1276        if frames == 0 {
1277            return Ok(());
1278        }
1279        let next_frames = self
1280            .frames_written
1281            .checked_add(frames as u64)
1282            .ok_or_else(|| "non-seekable output frame count overflows".to_string())?;
1283        if self
1284            .spec
1285            .total_frames
1286            .is_some_and(|declared| next_frames > declared)
1287        {
1288            return Err("non-seekable output exceeds its declared presentation length".into());
1289        }
1290        let required = estimate_spooled_stream_output_for_frames(
1291            self.format,
1292            self.spec,
1293            next_frames,
1294            self.options,
1295            self.encode_limits,
1296        )?;
1297        ensure_spool_limit(required, self.spool_limits, "non-seekable output")?;
1298        for frame in 0..frames {
1299            for channel in channels {
1300                self.pcm
1301                    .write_all(&channel[frame].to_le_bytes())
1302                    .map_err(|error| format!("write anonymous PCM output spool: {error}"))?;
1303            }
1304        }
1305        let block_bytes = (frames as u64)
1306            .checked_mul(u64::from(self.spec.channels))
1307            .and_then(|samples| samples.checked_mul(std::mem::size_of::<f64>() as u64))
1308            .ok_or_else(|| "non-seekable PCM block byte count overflows".to_string())?;
1309        self.pcm_bytes = self
1310            .pcm_bytes
1311            .checked_add(block_bytes)
1312            .ok_or_else(|| "non-seekable PCM spool byte count overflows".to_string())?;
1313        self.frames_written = next_frames;
1314        Ok(())
1315    }
1316
1317    /// Complete, validate, and copy the encoded stream to the caller's sink.
1318    pub fn finalize(self) -> Result<W, String> {
1319        self.finalize_with_fingerprint().map(|(sink, _)| sink)
1320    }
1321
1322    /// Complete, validate, fingerprint, and copy the encoded stream.
1323    ///
1324    /// The fingerprint describes the exact verified encoded spool. It is
1325    /// returned only after the sink accepted and flushed every byte, allowing a
1326    /// caller to sign a stdout receipt without authenticating a partial copy.
1327    pub fn finalize_with_fingerprint(
1328        self,
1329    ) -> Result<(W, crate::batch_resume::FileFingerprint), String> {
1330        self.finalize_with_metadata_and_loudness(
1331            None,
1332            crate::metadata::MetadataLimits::default(),
1333            None,
1334        )
1335        .map(|(sink, fingerprint, _)| (sink, fingerprint))
1336    }
1337
1338    /// Complete optional two-pass normalization and metadata preservation,
1339    /// verify the encoded spool, and only then copy it to the sink.
1340    ///
1341    /// `loudness` is `(target_lufs, true_peak_dbtp)`. The returned report is
1342    /// present exactly when normalization was requested.
1343    pub fn finalize_with_metadata_and_loudness(
1344        mut self,
1345        metadata: Option<crate::metadata::Metadata>,
1346        metadata_limits: crate::metadata::MetadataLimits,
1347        loudness: Option<(f64, f64)>,
1348    ) -> Result<
1349        (
1350            W,
1351            crate::batch_resume::FileFingerprint,
1352            Option<crate::loudness::LoudnessReport>,
1353        ),
1354        String,
1355    > {
1356        if self
1357            .spec
1358            .total_frames
1359            .is_some_and(|declared| declared != self.frames_written)
1360        {
1361            return Err(format!(
1362                "non-seekable output wrote {} frames, expected {}",
1363                self.frames_written,
1364                self.spec.total_frames.unwrap_or(0)
1365            ));
1366        }
1367        let mut final_spec = self.spec;
1368        final_spec.total_frames = Some(self.frames_written);
1369        let reserved = estimate_spooled_stream_output_for_frames(
1370            self.format,
1371            final_spec,
1372            self.frames_written,
1373            self.options,
1374            self.encode_limits,
1375        )?;
1376        ensure_spool_limit(reserved, self.spool_limits, "non-seekable output")?;
1377
1378        self.pcm
1379            .flush()
1380            .and_then(|_| self.pcm.seek(SeekFrom::Start(0)).map(|_| ()))
1381            .map_err(|error| format!("rewind anonymous PCM output spool: {error}"))?;
1382        let loudness_gain = if let Some((target_lufs, peak_limit_dbtp)) = loudness {
1383            let mut analyzer = crate::loudness::StreamingLoudnessAnalyzer::new(
1384                usize::from(self.spec.channels),
1385                self.spec.sample_rate,
1386                self.spec.channel_mask,
1387            )?;
1388            let mut remaining = self.frames_written;
1389            while remaining != 0 {
1390                let frames = remaining.min(self.replay_frames as u64) as usize;
1391                let block = read_interleaved_pcm_block(
1392                    &mut self.pcm,
1393                    usize::from(self.spec.channels),
1394                    frames,
1395                )?;
1396                analyzer.add_block(&block)?;
1397                remaining -= frames as u64;
1398            }
1399            self.pcm
1400                .seek(SeekFrom::Start(0))
1401                .map_err(|error| format!("rewind analyzed PCM output spool: {error}"))?;
1402            Some(analyzer.finish(target_lufs, peak_limit_dbtp)?)
1403        } else {
1404            None
1405        };
1406        let mut encoded = tempfile::tempfile()
1407            .map_err(|error| format!("create anonymous encoded output spool: {error}"))?;
1408        {
1409            let mut writer = AudioStreamWriter::new_with_limits(
1410                &mut encoded,
1411                self.format,
1412                final_spec,
1413                self.options,
1414                self.encode_limits,
1415            )?;
1416            let mut remaining = self.frames_written;
1417            while remaining != 0 {
1418                let frames = remaining.min(self.replay_frames as u64) as usize;
1419                let mut block = read_interleaved_pcm_block(
1420                    &mut self.pcm,
1421                    usize::from(self.spec.channels),
1422                    frames,
1423                )?;
1424                if let Some(gain) = loudness_gain {
1425                    gain.apply(&mut block);
1426                }
1427                writer.write_block(&block)?;
1428                remaining -= frames as u64;
1429            }
1430            writer.finalize()?;
1431        }
1432        if self.format == OutputFormat::Wav {
1433            crate::audio::write_wav_channel_mask_to_file(
1434                &mut encoded,
1435                usize::from(self.spec.channels),
1436                self.spec.channel_mask,
1437            )?;
1438        }
1439        if let Some(metadata) = metadata {
1440            crate::metadata::write_extended_to_file_with_limits(
1441                metadata,
1442                &mut encoded,
1443                metadata_limits,
1444            )?;
1445        }
1446        let encoded_bytes = encoded
1447            .metadata()
1448            .map_err(|error| format!("inspect anonymous encoded output spool: {error}"))?
1449            .len();
1450        let auxiliary = estimate_stream_encode_temporary_bytes(
1451            self.format,
1452            final_spec,
1453            self.options,
1454            self.encode_limits,
1455        )?;
1456        let actual = self
1457            .pcm_bytes
1458            .checked_add(encoded_bytes)
1459            .and_then(|bytes| bytes.checked_add(auxiliary))
1460            .ok_or_else(|| "non-seekable output actual spool byte count overflows".to_string())?;
1461        ensure_spool_limit(actual, self.spool_limits, "non-seekable output")?;
1462        verify_stream_output_file(
1463            &mut encoded,
1464            spooled_output_display_path(self.format),
1465            self.format,
1466            final_spec,
1467            self.frames_written,
1468            self.options,
1469            self.decode_limits,
1470            self.replay_frames,
1471        )?;
1472        let fingerprint = crate::batch_resume::fingerprint_open_file_at(
1473            &encoded,
1474            spooled_output_display_path(self.format),
1475        )?;
1476        encoded
1477            .seek(SeekFrom::Start(0))
1478            .map_err(|error| format!("rewind verified encoded output spool: {error}"))?;
1479        std::io::copy(&mut encoded, &mut self.sink)
1480            .map_err(|error| format!("copy verified audio to non-seekable output: {error}"))?;
1481        self.sink
1482            .flush()
1483            .map_err(|error| format!("flush non-seekable audio output: {error}"))?;
1484        Ok((
1485            self.sink,
1486            fingerprint,
1487            loudness_gain.map(crate::loudness::StreamingLoudnessGain::report),
1488        ))
1489    }
1490}
1491
1492fn ensure_spool_limit(
1493    required: u64,
1494    limits: crate::StreamSpoolLimits,
1495    context: &str,
1496) -> Result<(), String> {
1497    if required > limits.max_bytes() {
1498        return Err(format!(
1499            "{context} requires {required} bytes across its PCM, encoded, and auxiliary spools, exceeding the {}-byte spool limit",
1500            limits.max_bytes()
1501        ));
1502    }
1503    Ok(())
1504}
1505
1506fn read_interleaved_pcm_block(
1507    source: &mut File,
1508    channels: usize,
1509    frames: usize,
1510) -> Result<Vec<Vec<f64>>, String> {
1511    let mut block = Vec::new();
1512    block
1513        .try_reserve_exact(channels)
1514        .map_err(|error| format!("reserve spooled output channel list: {error}"))?;
1515    for _ in 0..channels {
1516        let mut channel = Vec::new();
1517        channel
1518            .try_reserve_exact(frames)
1519            .map_err(|error| format!("reserve spooled output channel: {error}"))?;
1520        block.push(channel);
1521    }
1522    let mut bytes = [0_u8; 8];
1523    for _ in 0..frames {
1524        for channel in &mut block {
1525            source
1526                .read_exact(&mut bytes)
1527                .map_err(|error| format!("read anonymous PCM output spool: {error}"))?;
1528            channel.push(f64::from_le_bytes(bytes));
1529        }
1530    }
1531    Ok(block)
1532}
1533
1534fn spooled_output_display_path(format: OutputFormat) -> &'static Path {
1535    Path::new(match format {
1536        OutputFormat::Wav => "<writer>.wav",
1537        OutputFormat::Flac => "<writer>.flac",
1538        OutputFormat::OggOpus => "<writer>.opus",
1539        OutputFormat::Mp3 => "<writer>.mp3",
1540        OutputFormat::M4a => "<writer>.m4a",
1541        OutputFormat::AacAdts => "<writer>.aac",
1542    })
1543}
1544
1545#[cfg(test)]
1546mod tests {
1547    use super::*;
1548    use std::io::Cursor;
1549
1550    fn spec(sample_rate: u32, frames: u64) -> StreamEncodeSpec {
1551        StreamEncodeSpec::new(
1552            WavSpec {
1553                channels: 2,
1554                sample_rate,
1555                bits_per_sample: 32,
1556                sample_format: SampleFormat::Float,
1557            },
1558            crate::ChannelLayout::Stereo.mask(),
1559            Some(frames),
1560        )
1561    }
1562
1563    #[test]
1564    fn bounded_writers_accept_multiple_blocks() {
1565        let formats = vec![
1566            OutputFormat::Wav,
1567            OutputFormat::Flac,
1568            OutputFormat::OggOpus,
1569            OutputFormat::Mp3,
1570            #[cfg(feature = "m4a-encode")]
1571            OutputFormat::M4a,
1572            #[cfg(feature = "m4a-encode")]
1573            OutputFormat::AacAdts,
1574        ];
1575        for format in formats {
1576            let mut output = Cursor::new(Vec::new());
1577            let frames = 4_321usize;
1578            let output_bound = estimate_stream_encode_output_bytes(
1579                format,
1580                spec(48_000, frames as u64),
1581                EncodeOptions::default(),
1582                StreamEncodeLimits::default(),
1583            )
1584            .unwrap()
1585            .unwrap();
1586            {
1587                let mut writer = AudioStreamWriter::new(
1588                    &mut output,
1589                    format,
1590                    spec(48_000, frames as u64),
1591                    EncodeOptions::default(),
1592                )
1593                .unwrap();
1594                for start in (0..frames).step_by(317) {
1595                    let len = (frames - start).min(317);
1596                    let left = (start..start + len)
1597                        .map(|index| (index as f64 / 31.0).sin() * 0.2)
1598                        .collect::<Vec<_>>();
1599                    writer.write_block(&[left.clone(), left]).unwrap();
1600                }
1601                writer.finalize().unwrap();
1602            }
1603            assert!(output.get_ref().len() > 64, "{format:?} output is empty");
1604            assert!(
1605                output.get_ref().len() as u64 <= output_bound,
1606                "{format:?} output exceeded its {output_bound}-byte bound"
1607            );
1608        }
1609    }
1610
1611    #[test]
1612    fn finite_pcm_spool_replays_exact_blocks_and_enforces_limit() {
1613        let bytes = 3_u64 * 2 * std::mem::size_of::<f64>() as u64;
1614        let mut spool = StreamPcmSpool::new(2, bytes).unwrap();
1615        spool
1616            .write_block(&[vec![0.1, 0.2], vec![-0.1, -0.2]])
1617            .unwrap();
1618        spool.write_block(&[vec![0.3], vec![-0.3]]).unwrap();
1619        assert_eq!(spool.frames(), 3);
1620        assert_eq!(spool.len(), bytes);
1621        assert!(spool.write_block(&[vec![0.4], vec![-0.4]]).is_err());
1622        assert!(spool.next_block(2).is_err());
1623        spool.prepare_read().unwrap();
1624        assert!(spool.write_block(&[vec![0.4], vec![-0.4]]).is_err());
1625        assert_eq!(
1626            spool.next_block(2).unwrap().unwrap(),
1627            vec![vec![0.1, 0.2], vec![-0.1, -0.2]]
1628        );
1629        assert_eq!(
1630            spool.next_block(2).unwrap().unwrap(),
1631            vec![vec![0.3], vec![-0.3]]
1632        );
1633        assert!(spool.next_block(2).unwrap().is_none());
1634    }
1635
1636    #[test]
1637    fn spooled_writer_publishes_to_a_plain_write_sink_after_verification() {
1638        let frames = 4_321usize;
1639        let input = (0..frames)
1640            .map(|index| (index as f64 / 31.0).sin() * 0.2)
1641            .collect::<Vec<_>>();
1642        let mut writer = SpooledAudioStreamWriter::new(
1643            Vec::new(),
1644            OutputFormat::Flac,
1645            spec(48_000, frames as u64),
1646            EncodeOptions::default(),
1647        )
1648        .unwrap();
1649        for start in (0..frames).step_by(317) {
1650            let end = (start + 317).min(frames);
1651            writer
1652                .write_block(&[input[start..end].to_vec(), input[start..end].to_vec()])
1653                .unwrap();
1654        }
1655        let (encoded, fingerprint) = writer.finalize_with_fingerprint().unwrap();
1656        assert!(encoded.starts_with(b"fLaC"));
1657        let root = tempfile::tempdir().unwrap();
1658        let captured = root.path().join("captured.flac");
1659        std::fs::write(&captured, &encoded).unwrap();
1660        assert_eq!(
1661            fingerprint,
1662            crate::batch_resume::fingerprint_file(&captured).unwrap()
1663        );
1664
1665        let mut reader = crate::AudioStreamReader::from_reader(std::io::Cursor::new(encoded))
1666            .expect("decode published plain Write sink");
1667        let mut decoded_frames = 0usize;
1668        while let Some(block) = reader.next_block(113).unwrap() {
1669            assert_eq!(block.len(), 2);
1670            decoded_frames += block[0].len();
1671        }
1672        assert_eq!(decoded_frames, frames);
1673    }
1674
1675    #[test]
1676    fn spooled_writer_declared_bound_has_an_exact_prewrite_boundary() {
1677        let stream_spec = spec(48_000, 321);
1678        let required = estimate_spooled_stream_output_bytes(
1679            OutputFormat::Flac,
1680            stream_spec,
1681            EncodeOptions::default(),
1682            StreamEncodeLimits::default(),
1683        )
1684        .unwrap()
1685        .unwrap();
1686        SpooledAudioStreamWriter::new_with_limits(
1687            Vec::new(),
1688            OutputFormat::Flac,
1689            stream_spec,
1690            EncodeOptions::default(),
1691            StreamEncodeLimits::default(),
1692            crate::DecodeLimits::default(),
1693            crate::StreamSpoolLimits::new(required),
1694            73,
1695        )
1696        .unwrap();
1697        let error = match SpooledAudioStreamWriter::new_with_limits(
1698            Vec::new(),
1699            OutputFormat::Flac,
1700            stream_spec,
1701            EncodeOptions::default(),
1702            StreamEncodeLimits::default(),
1703            crate::DecodeLimits::default(),
1704            crate::StreamSpoolLimits::new(required - 1),
1705            73,
1706        ) {
1707            Ok(_) => panic!("one byte below the declared spool bound must fail"),
1708            Err(error) => error,
1709        };
1710        assert!(error.contains("spool limit"), "{error}");
1711    }
1712
1713    #[test]
1714    fn exact_declared_length_is_enforced_before_finalize() {
1715        let mut output = Cursor::new(Vec::new());
1716        let mut writer = AudioStreamWriter::new(
1717            &mut output,
1718            OutputFormat::Wav,
1719            spec(48_000, 2),
1720            EncodeOptions::default(),
1721        )
1722        .unwrap();
1723        let error = writer
1724            .write_block(&[vec![0.0; 3], vec![0.0; 3]])
1725            .unwrap_err();
1726        assert!(error.contains("declared presentation length"));
1727    }
1728
1729    #[cfg(feature = "m4a-encode")]
1730    #[test]
1731    fn m4a_auxiliary_table_limit_is_checked_before_touching_the_sink() {
1732        let stream_spec = spec(48_000, 1_024);
1733        let options = EncodeOptions::default();
1734        let exact = 2 * M4A_TABLE_RECORD_BYTES;
1735        assert_eq!(
1736            estimate_stream_encode_temporary_bytes(
1737                OutputFormat::M4a,
1738                stream_spec,
1739                options,
1740                StreamEncodeLimits::new(exact),
1741            )
1742            .unwrap(),
1743            exact
1744        );
1745
1746        let mut output = Cursor::new(Vec::new());
1747        let error = match AudioStreamWriter::new_with_limits(
1748            &mut output,
1749            OutputFormat::M4a,
1750            stream_spec,
1751            options,
1752            StreamEncodeLimits::new(exact - 1),
1753        ) {
1754            Ok(_) => panic!("undersized M4A sample-table limit was accepted"),
1755            Err(error) => error,
1756        };
1757        assert!(error.contains("requires 24 bytes"), "{error}");
1758        assert!(output.get_ref().is_empty());
1759    }
1760
1761    #[cfg(feature = "m4a-encode")]
1762    #[test]
1763    fn unknown_m4a_duration_reserves_the_configured_auxiliary_ceiling() {
1764        let mut stream_spec = spec(48_000, 1);
1765        stream_spec.total_frames = None;
1766        let limit = 7_777;
1767        assert_eq!(
1768            estimate_stream_encode_temporary_bytes(
1769                OutputFormat::M4a,
1770                stream_spec,
1771                EncodeOptions::default(),
1772                StreamEncodeLimits::new(limit),
1773            )
1774            .unwrap(),
1775            limit
1776        );
1777    }
1778
1779    #[cfg(feature = "fdk-aac-encoder")]
1780    #[test]
1781    fn bounded_fdk_m4a_writer_accepts_multiple_blocks() {
1782        let mut output = Cursor::new(Vec::new());
1783        let frames = 3_217usize;
1784        let mut options = EncodeOptions::default();
1785        options.aac_encoder = crate::encode::AacEncoder::Fdk;
1786        let stream_spec = spec(48_000, frames as u64);
1787        let exact_table_bytes = estimate_stream_encode_temporary_bytes(
1788            OutputFormat::M4a,
1789            stream_spec,
1790            options,
1791            StreamEncodeLimits::default(),
1792        )
1793        .unwrap();
1794        let mut writer = AudioStreamWriter::new_with_limits(
1795            &mut output,
1796            OutputFormat::M4a,
1797            stream_spec,
1798            options,
1799            StreamEncodeLimits::new(exact_table_bytes),
1800        )
1801        .unwrap();
1802        for start in (0..frames).step_by(211) {
1803            let len = (frames - start).min(211);
1804            let left = (start..start + len)
1805                .map(|index| (index as f64 / 17.0).sin() * 0.2)
1806                .collect::<Vec<_>>();
1807            writer.write_block(&[left.clone(), left]).unwrap();
1808        }
1809        writer.finalize().unwrap();
1810        assert!(output.get_ref().len() > 64);
1811        let output_bound = estimate_stream_encode_output_bytes(
1812            OutputFormat::M4a,
1813            stream_spec,
1814            options,
1815            StreamEncodeLimits::new(exact_table_bytes),
1816        )
1817        .unwrap()
1818        .unwrap();
1819        assert!(output.get_ref().len() as u64 <= output_bound);
1820    }
1821}