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#[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#[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 pub total_frames: Option<u64>,
67}
68
69#[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
224pub 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
271pub 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
445pub 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
501pub 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
602pub 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 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 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
694pub 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
837pub 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 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
1054pub 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
1076pub 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 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 #[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 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 pub fn finalize(self) -> Result<W, String> {
1319 self.finalize_with_fingerprint().map(|(sink, _)| sink)
1320 }
1321
1322 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 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}