Skip to main content

audio_samples_io/
lib.rs

1pub mod error;
2pub mod traits;
3pub mod types;
4
5#[cfg(feature = "wav")]
6pub mod wav;
7
8#[cfg(feature = "wav")]
9pub use crate::wav::{
10    StreamedWavFile, StreamedWavWriter, build_wav_header, wav_data_len, wav_file::WavFile, wav_file_len, wav_header_len,
11};
12
13#[cfg(feature = "flac")]
14pub mod flac;
15
16#[cfg(feature = "flac")]
17pub use crate::flac::{CompressionLevel, StreamedFlacFile, StreamedFlacWriter};
18
19#[cfg(any(feature = "wav", feature = "flac"))]
20pub mod streaming;
21#[cfg(any(feature = "wav", feature = "flac"))]
22pub use crate::streaming::StreamedAudioWriter;
23
24#[cfg(feature = "numpy")]
25pub mod python;
26
27#[cfg(feature = "resampling")]
28use std::num::NonZeroU32;
29use std::{
30    any::TypeId,
31    fs::File,
32    io::{BufReader, BufWriter, Read, Seek, Write},
33    path::Path,
34};
35
36#[cfg(feature = "resampling")]
37pub use audio_samples::operations::ResamplingQuality;
38#[cfg(feature = "resampling")]
39pub use audio_samples::operations::resample;
40use audio_samples::{AudioSamples, traits::StandardSample};
41
42#[cfg(feature = "numpy")]
43pub use crate::python::read_pyarray;
44#[cfg(all(feature = "numpy", target_endian = "little"))]
45pub use crate::python::{NativeAudioArray, read_pyarray_native};
46pub use crate::{
47    error::{AudioIOError, AudioIOResult},
48    traits::{AudioFile, AudioFileMetadata, AudioFileRead, AudioStreamReader},
49    types::{BaseAudioInfo, FileType, OpenOptions, ValidatedSampleType, WriteOptions},
50};
51
52pub(crate) const MAX_WAV_SIZE: u64 = 2 * 1024 * 1024 * 1024; // 2GB limit
53pub(crate) const MAX_MMAP_SIZE: u64 = 512 * 1024 * 1024; // 512MB for memory mapping
54
55/// Convenience trait for types that implement both Read and Seek
56pub trait ReadSeek: Read + Seek {}
57
58impl<RS> ReadSeek for RS where RS: Read + Seek {}
59
60pub trait WriteSeek: Write + Seek {}
61
62impl<WS> WriteSeek for WS where WS: Write + Seek {}
63
64// Public API
65
66/// Peek at the native sample type of an audio file with minimal I/O.
67///
68/// Uses a small buffered read (one syscall for the first 64 KB covers any WAV/FLAC header)
69/// instead of mmapping the entire file.  This is significantly cheaper than [`info`] when
70/// only the sample type is needed, such as when auto-detecting the read target type.
71pub fn peek_native_type<P: AsRef<Path>>(fp: P) -> AudioIOResult<ValidatedSampleType> {
72    let path = fp.as_ref();
73
74    match FileType::from_path(path) {
75        FileType::WAV => {
76            #[cfg(not(feature = "wav"))]
77            return Err(crate::error::AudioIOError::missing_feature(
78                "'wav' feature must be enabled to peek WAV files",
79            ));
80
81            #[cfg(feature = "wav")]
82            {
83                use crate::wav::wav_file::parse_wav_header_streaming;
84                let file = File::open(path)?;
85                let mut reader = BufReader::with_capacity(65536, file);
86                let (info, _) = parse_wav_header_streaming(&mut reader)?;
87                ValidatedSampleType::try_from(info.sample_type).map_err(|_| {
88                    AudioIOError::unsupported_format(format!("Unsupported native sample type: {:?}", info.sample_type))
89                })
90            }
91        },
92        FileType::FLAC => {
93            #[cfg(not(feature = "flac"))]
94            return Err(crate::error::AudioIOError::missing_feature(
95                "'flac' feature must be enabled to peek FLAC files",
96            ));
97
98            #[cfg(feature = "flac")]
99            {
100                use crate::flac::FlacFile;
101                use crate::traits::{AudioFile, AudioFileMetadata};
102                let flac_file = FlacFile::open_with_options(path, OpenOptions::default())?;
103                let info = flac_file.base_info()?;
104                ValidatedSampleType::try_from(info.sample_type).map_err(|_| {
105                    AudioIOError::unsupported_format(format!("Unsupported native sample type: {:?}", info.sample_type))
106                })
107            }
108        },
109        other => Err(crate::error::AudioIOError::unsupported_format(format!(
110            "peek_native_type does not support: {other:?}"
111        ))),
112    }
113}
114
115/// Get basic audio information from a file
116///
117/// Automatically detects the file format and extracts metadata.
118/// Currently supports WAV and FLAC formats (with appropriate features enabled).
119pub fn info<P: AsRef<Path>>(fp: P) -> AudioIOResult<BaseAudioInfo> {
120    let path = fp.as_ref();
121
122    match FileType::from_path(path) {
123        FileType::WAV => {
124            #[cfg(not(feature = "wav"))]
125            return Err(crate::error::AudioIOError::missing_feature(
126                "'wav' feature must be enabled to read WAV files",
127            ));
128
129            #[cfg(feature = "wav")]
130            {
131                let wav_file = WavFile::open_metadata(path)?;
132                wav_file.base_info()
133            }
134        },
135        FileType::FLAC => {
136            #[cfg(not(feature = "flac"))]
137            return Err(crate::error::AudioIOError::missing_feature(
138                "'flac' feature must be enabled to read FLAC files",
139            ));
140
141            #[cfg(feature = "flac")]
142            {
143                use crate::flac::FlacFile;
144                use crate::traits::AudioFileMetadata;
145                let flac_file = FlacFile::open_metadata(path)?;
146                flac_file.base_info()
147            }
148        },
149        other => Err(crate::error::AudioIOError::unsupported_format(format!(
150            "Unsupported file format: {other:?}"
151        ))),
152    }
153}
154
155/// Read audio samples from a file
156///
157/// Returns owned AudioSamples containing all audio data from the file.
158/// Automatically detects file format and handles sample type conversion.
159/// Currently supports WAV and FLAC formats (with appropriate features enabled).
160pub fn read<P, T>(fp: P) -> AudioIOResult<AudioSamples<'static, T>>
161where
162    P: AsRef<Path>,
163    T: StandardSample + 'static,
164{
165    let path = fp.as_ref();
166
167    match FileType::from_path(path) {
168        FileType::WAV => {
169            #[cfg(not(feature = "wav"))]
170            return Err(crate::error::AudioIOError::missing_feature(
171                "'wav' feature must be enabled to read WAV files",
172            ));
173
174            #[cfg(feature = "wav")]
175            {
176                let wav_file = WavFile::open_with_options(path, OpenOptions::default())?;
177                let samples = wav_file.read::<T>()?;
178                Ok(samples.into_owned())
179            }
180        },
181        FileType::FLAC => {
182            #[cfg(not(feature = "flac"))]
183            return Err(crate::error::AudioIOError::missing_feature(
184                "'flac' feature must be enabled to read FLAC files",
185            ));
186
187            #[cfg(feature = "flac")]
188            {
189                use crate::flac::FlacFile;
190                use crate::traits::{AudioFile, AudioFileRead};
191                let flac_file = FlacFile::open_with_options(path, OpenOptions::default())?;
192                let samples = flac_file.read::<T>()?;
193                Ok(samples.into_owned())
194            }
195        },
196        other => Err(crate::error::AudioIOError::unsupported_format(format!(
197            "Unsupported file format: {other:?}"
198        ))),
199    }
200}
201
202#[cfg(feature = "resampling")]
203pub fn read_and_resample<P, T>(
204    fp: P,
205    target_sr: NonZeroU32,
206    quality: Option<ResamplingQuality>,
207) -> AudioIOResult<AudioSamples<'static, T>>
208where
209    P: AsRef<Path>,
210    T: StandardSample,
211{
212    let signal = read(fp)?;
213    resample::<T>(&signal, target_sr, quality.unwrap_or(ResamplingQuality::Fast)).map_err(AudioIOError::AudioSamples)
214}
215
216/// Open a WAV file for streaming reads.
217///
218/// Unlike `read()` which loads the entire file, this opens the file for incremental reading,
219/// parsing only the header initially. Returns a concrete [`StreamedWavFile`] for full API access.
220///
221/// For format-agnostic streaming use [`open_streamed_dyn`], which returns a trait object.
222///
223/// # Example
224///
225/// ```no_run
226/// use audio_samples_io::open_streamed;
227/// use audio_samples_io::traits::AudioFileMetadata;
228/// use audio_samples::{AudioSamples, nzu};
229/// use std::num::NonZeroU32;
230///
231/// let mut streamed = open_streamed("large_file.wav")?;
232/// let channels = NonZeroU32::new(streamed.num_channels() as u32).ok_or_else(|| audio_samples_io::error::AudioIOError::UnsupportedFormat("channels must be non-zero".to_string()))?;
233/// let sample_rate = NonZeroU32::new(streamed.sample_rate()).ok_or_else(|| audio_samples_io::error::AudioIOError::UnsupportedFormat("sample_rate must be non-zero".to_string()))?;
234/// let mut buffer = AudioSamples::<f32>::zeros_multi(channels, nzu!(1024), sample_rate);
235///
236/// while streamed.remaining_frames() > 0 {
237///     let frames = streamed.read_frames_into(&mut buffer, nzu!(1024))?;
238///     // Process frames...
239/// }
240/// # Ok::<(), audio_samples_io::error::AudioIOError>(())
241/// ```
242#[cfg(feature = "wav")]
243pub fn open_streamed<P>(fp: P) -> AudioIOResult<StreamedWavFile<BufReader<File>>>
244where
245    P: AsRef<Path>,
246{
247    let path = fp.as_ref();
248
249    match FileType::from_path(path) {
250        FileType::WAV => {
251            let file = File::open(path)?;
252            let reader = BufReader::new(file);
253            StreamedWavFile::new_with_path(reader, path.to_path_buf())
254        },
255        other => Err(crate::error::AudioIOError::unsupported_format(format!(
256            "Unsupported file format for streaming: {other:?}"
257        ))),
258    }
259}
260
261/// Open any `Read + Seek` source for streaming WAV reads.
262///
263/// This allows streaming from any source implementing `Read + Seek`,
264/// such as network streams with range request support, in-memory cursors,
265/// or custom I/O implementations.
266///
267/// # Example
268///
269/// ```no_run
270/// use audio_samples_io::open_streamed_reader;
271/// use std::io::Cursor;
272///
273/// let wav_bytes: Vec<u8> = load_from_network();
274/// let cursor = Cursor::new(wav_bytes);
275/// let mut streamed = open_streamed_reader(cursor)?;
276/// # fn load_from_network() -> Vec<u8> { vec![] }
277/// # Ok::<(), audio_samples_io::error::AudioIOError>(())
278/// ```
279#[cfg(feature = "wav")]
280pub fn open_streamed_reader<R>(reader: R) -> AudioIOResult<wav::StreamedWavFile<R>>
281where
282    R: ReadSeek,
283{
284    wav::StreamedWavFile::new(reader)
285}
286
287/// Open a FLAC file for streaming reads.
288///
289/// Parses only the metadata header on construction and decodes frames on demand.
290/// Returns a concrete [`StreamedFlacFile`] for full API access.
291///
292/// For format-agnostic streaming use [`open_streamed_dyn`], which returns a trait object.
293///
294/// # Example
295///
296/// ```no_run
297/// use audio_samples_io::open_streamed_flac;
298/// use audio_samples_io::traits::AudioFileMetadata;
299/// use audio_samples::{AudioSamples, nzu};
300/// use std::num::NonZeroU32;
301///
302/// let mut streamed = open_streamed_flac("large_file.flac")?;
303/// let channels = NonZeroU32::new(streamed.num_channels() as u32).ok_or_else(|| audio_samples_io::error::AudioIOError::UnsupportedFormat("channels must be non-zero".to_string()))?;
304/// let sample_rate = NonZeroU32::new(streamed.sample_rate()).ok_or_else(|| audio_samples_io::error::AudioIOError::UnsupportedFormat("sample_rate must be non-zero".to_string()))?;
305/// let mut buffer = AudioSamples::<f32>::zeros_multi(channels, nzu!(1024), sample_rate);
306///
307/// while streamed.remaining_frames() > 0 {
308///     let frames = streamed.read_frames_into(&mut buffer, nzu!(1024))?;
309///     // Process frames...
310/// }
311/// # Ok::<(), audio_samples_io::error::AudioIOError>(())
312/// ```
313#[cfg(feature = "flac")]
314pub fn open_streamed_flac<P>(fp: P) -> AudioIOResult<StreamedFlacFile<BufReader<File>>>
315where
316    P: AsRef<Path>,
317{
318    let path = fp.as_ref();
319    match FileType::from_path(path) {
320        FileType::FLAC => {
321            let file = File::open(path)?;
322            let reader = BufReader::new(file);
323            StreamedFlacFile::new_with_path(reader, path.to_path_buf())
324        },
325        other => Err(crate::error::AudioIOError::unsupported_format(format!(
326            "Unsupported file format for FLAC streaming: {other:?}"
327        ))),
328    }
329}
330
331/// Open any `Read + Seek` source for streaming FLAC reads.
332///
333/// This allows streaming from any source implementing `Read + Seek`,
334/// such as in-memory cursors or custom I/O implementations.
335///
336/// # Example
337///
338/// ```no_run
339/// use audio_samples_io::open_streamed_flac_reader;
340/// use std::io::Cursor;
341///
342/// let flac_bytes: Vec<u8> = std::fs::read("audio.flac").unwrap();
343/// let cursor = Cursor::new(flac_bytes);
344/// let mut streamed = open_streamed_flac_reader(cursor)?;
345/// # Ok::<(), audio_samples_io::error::AudioIOError>(())
346/// ```
347#[cfg(feature = "flac")]
348pub fn open_streamed_flac_reader<R>(reader: R) -> AudioIOResult<flac::StreamedFlacFile<R>>
349where
350    R: ReadSeek,
351{
352    flac::StreamedFlacFile::new(reader)
353}
354
355/// Open an audio file for streaming reads, returning a trait object.
356///
357/// This function returns a `Box<dyn AudioStreamReader>` which provides
358/// format-agnostic streaming access. Use this when you need to work with
359/// multiple formats through a unified interface.
360///
361/// For format-specific access with full functionality (including generic
362/// `read_frames_into<T>()`), use [`open_streamed()`] or [`open_streamed_reader()`]
363/// which return concrete types.
364///
365/// # Example
366///
367/// ```no_run
368/// use audio_samples_io::open_streamed_dyn;
369/// use audio_samples_io::traits::AudioStreamReader;
370///
371/// fn process_any_stream(mut stream: Box<dyn AudioStreamReader>) -> Result<(), Box<dyn std::error::Error>> {
372///     println!("channels={} sample_rate={} total_frames={}",
373///         stream.num_channels(), stream.sample_rate(), stream.total_frames());
374///     stream.seek_to_frame(1000)?;
375///     Ok(())
376/// }
377///
378/// let stream = open_streamed_dyn("audio.wav")?;
379/// process_any_stream(stream);
380/// # Ok::<(), audio_samples_io::error::AudioIOError>(())
381/// ```
382pub fn open_streamed_dyn<P>(fp: P) -> AudioIOResult<Box<dyn AudioStreamReader>>
383where
384    P: AsRef<Path>,
385{
386    let path = fp.as_ref();
387
388    match FileType::from_path(path) {
389        FileType::WAV => {
390            #[cfg(not(feature = "wav"))]
391            return Err(crate::error::AudioIOError::missing_feature(
392                "'wav' feature must be enabled for WAV streaming",
393            ));
394
395            #[cfg(feature = "wav")]
396            {
397                let file = File::open(path)?;
398                let reader = BufReader::new(file);
399                let streamed = StreamedWavFile::new_with_path(reader, path.to_path_buf())?;
400                Ok(Box::new(streamed))
401            }
402        },
403        FileType::FLAC => {
404            #[cfg(not(feature = "flac"))]
405            return Err(crate::error::AudioIOError::missing_feature(
406                "'flac' feature must be enabled for FLAC streaming",
407            ));
408
409            #[cfg(feature = "flac")]
410            {
411                let file = File::open(path)?;
412                let reader = BufReader::new(file);
413                let streamed = StreamedFlacFile::new_with_path(reader, path.to_path_buf())?;
414                Ok(Box::new(streamed))
415            }
416        },
417        other => Err(crate::error::AudioIOError::unsupported_format(format!(
418            "Unsupported file format for streaming: {other:?}"
419        ))),
420    }
421}
422
423/// Create a streaming writer to a file path, choosing WAV or FLAC from the extension.
424///
425/// Returns a format-agnostic [`StreamedAudioWriter`]; for the concrete per-format writer
426/// use [`create_streamed_writer`] (WAV) or [`create_streamed_flac`] (FLAC).
427///
428/// The sample type is inferred from the generic parameter `T`. Use the turbofish
429/// syntax when the type cannot be inferred: `create_streamed::<f32>(...)`.
430///
431/// # Example
432///
433/// ```no_run
434/// use audio_samples_io::create_streamed;
435/// use audio_samples_io::traits::{AudioStreamWrite, AudioStreamWriter};
436/// use audio_samples::{AudioSamples, channels, nzu, sample_rate};
437///
438/// let mut writer = create_streamed::<_, f32>("output.wav", 2, 44100)?;
439///
440/// let sr = sample_rate!(44100);
441/// let chunk = AudioSamples::<f32>::zeros_multi(channels!(2), nzu!(1024), sr);
442/// writer.write_frames(&chunk)?;
443/// writer.finalize()?;
444/// # Ok::<(), audio_samples_io::error::AudioIOError>(())
445/// ```
446#[cfg(any(feature = "wav", feature = "flac"))]
447pub fn create_streamed<P, T>(
448    fp: P,
449    channels: u16,
450    sample_rate: u32,
451) -> AudioIOResult<StreamedAudioWriter<BufWriter<File>>>
452where
453    P: AsRef<Path>,
454    T: StandardSample + 'static,
455{
456    let path = fp.as_ref();
457    let format = FileType::from_path(path);
458    let file = File::create(path)?;
459    // 256 KiB: buffers ~8 typical streaming chunks (4096-frame stereo f32 = 32 KiB)
460    // before issuing a write syscall, reducing syscall count ~8× vs the 8 KiB default.
461    let writer = BufWriter::with_capacity(256 * 1024, file);
462    create_streamed_with::<_, T>(writer, channels, sample_rate, format)
463}
464
465/// Create a streaming writer to a file path with explicit [`WriteOptions`].
466///
467/// Identical to [`create_streamed`] (format chosen by extension) but lets you control the
468/// write-buffer size.
469///
470/// # Example
471///
472/// ```no_run
473/// use audio_samples_io::{create_streamed_with_options, WriteOptions};
474/// use audio_samples_io::traits::{AudioStreamWrite, AudioStreamWriter};
475/// use audio_samples::{AudioSamples, channels, nzu, sample_rate};
476///
477/// // 1 MiB buffer for large streaming chunks.
478/// let opts = WriteOptions { write_buf_capacity: 1024 * 1024 };
479/// let mut writer = create_streamed_with_options::<_, f32>("output.wav", 2, 44100, opts)?;
480///
481/// let sr = sample_rate!(44100);
482/// let chunk = AudioSamples::<f32>::zeros_multi(channels!(2), nzu!(8192), sr);
483/// writer.write_frames(&chunk)?;
484/// writer.finalize()?;
485/// # Ok::<(), audio_samples_io::error::AudioIOError>(())
486/// ```
487#[cfg(any(feature = "wav", feature = "flac"))]
488pub fn create_streamed_with_options<P, T>(
489    fp: P,
490    channels: u16,
491    sample_rate: u32,
492    opts: WriteOptions,
493) -> AudioIOResult<StreamedAudioWriter<BufWriter<File>>>
494where
495    P: AsRef<Path>,
496    T: StandardSample + 'static,
497{
498    let path = fp.as_ref();
499    let format = FileType::from_path(path);
500    let file = File::create(path)?;
501    let writer = BufWriter::with_capacity(opts.write_buf_capacity, file);
502    create_streamed_with::<_, T>(writer, channels, sample_rate, format)
503}
504
505/// Create a streaming writer to any [`WriteSeek`] destination with an explicit format.
506///
507/// The format-agnostic, bring-your-own-writer counterpart of [`write_with`]: returns a
508/// [`StreamedAudioWriter`] that encodes as WAV or FLAC according to `format`. The output
509/// sample type / bit depth is derived from `T`.
510///
511/// ```no_run
512/// use audio_samples_io::{create_streamed_with, types::FileType};
513/// use audio_samples_io::traits::{AudioStreamWrite, AudioStreamWriter};
514/// use audio_samples::{AudioSamples, nzu, sample_rate};
515/// use std::io::Cursor;
516///
517/// let mut buf = Vec::new();
518/// let mut writer = create_streamed_with::<_, i16>(Cursor::new(&mut buf), 1, 44100, FileType::FLAC)?;
519/// writer.write_frames(&AudioSamples::<i16>::zeros_mono(nzu!(1024), sample_rate!(44100)))?;
520/// writer.finalize()?;
521/// # Ok::<(), audio_samples_io::error::AudioIOError>(())
522/// ```
523#[cfg(any(feature = "wav", feature = "flac"))]
524pub fn create_streamed_with<W, T>(
525    writer: W,
526    channels: u16,
527    sample_rate: u32,
528    format: FileType,
529) -> AudioIOResult<StreamedAudioWriter<W>>
530where
531    W: WriteSeek,
532    T: StandardSample + 'static,
533{
534    match format {
535        FileType::WAV => {
536            #[cfg(not(feature = "wav"))]
537            {
538                let _ = (writer, channels, sample_rate);
539                Err(AudioIOError::missing_feature(
540                    "'wav' feature must be enabled for WAV streaming writes",
541                ))
542            }
543            #[cfg(feature = "wav")]
544            {
545                Ok(StreamedAudioWriter::Wav(wav_writer_for_type::<T, W>(
546                    writer,
547                    channels,
548                    sample_rate,
549                )?))
550            }
551        },
552        FileType::FLAC => {
553            #[cfg(not(feature = "flac"))]
554            {
555                let _ = (writer, channels, sample_rate);
556                Err(AudioIOError::missing_feature(
557                    "'flac' feature must be enabled for FLAC streaming writes",
558                ))
559            }
560            #[cfg(feature = "flac")]
561            {
562                Ok(StreamedAudioWriter::Flac(flac_writer_for_type::<T, W>(
563                    writer,
564                    channels,
565                    sample_rate,
566                )?))
567            }
568        },
569        other => Err(AudioIOError::unsupported_format(format!(
570            "Unsupported output format for streaming write: {other:?}"
571        ))),
572    }
573}
574
575/// Create a streaming FLAC writer to a file path.
576///
577/// FLAC is a block-based codec, so this writer buffers each block and encodes it
578/// incrementally as frames are written, using the same encoder as the bulk [`write`]
579/// path. The STREAMINFO header's total sample count is back-patched on
580/// [`finalize`](crate::traits::AudioStreamWriter::finalize), which is why a seekable
581/// destination is required. The output bit depth is derived from `T` (16-bit for `i16`,
582/// 24-bit otherwise), matching [`write`].
583///
584/// FLAC's concrete return type can't be folded into [`create_streamed`] (which is
585/// WAV-only); this mirrors the [`open_streamed`]/[`open_streamed_flac`] split.
586///
587/// # Example
588///
589/// ```no_run
590/// use audio_samples_io::create_streamed_flac;
591/// use audio_samples_io::traits::{AudioStreamWrite, AudioStreamWriter};
592/// use audio_samples::{AudioSamples, channels, nzu, sample_rate};
593///
594/// let mut writer = create_streamed_flac::<_, i16>("output.flac", 2, 44100)?;
595///
596/// let chunk = AudioSamples::<i16>::zeros_multi(channels!(2), nzu!(4096), sample_rate!(44100));
597/// writer.write_frames(&chunk)?;
598/// writer.finalize()?;
599/// # Ok::<(), audio_samples_io::error::AudioIOError>(())
600/// ```
601#[cfg(feature = "flac")]
602pub fn create_streamed_flac<P, T>(
603    fp: P,
604    channels: u16,
605    sample_rate: u32,
606) -> AudioIOResult<StreamedFlacWriter<BufWriter<File>>>
607where
608    P: AsRef<Path>,
609    T: StandardSample + 'static,
610{
611    let path = fp.as_ref();
612    match FileType::from_path(path) {
613        FileType::FLAC => {
614            let file = File::create(path)?;
615            let writer = BufWriter::with_capacity(256 * 1024, file);
616            flac_writer_for_type::<T, _>(writer, channels, sample_rate)
617        },
618        other => Err(crate::error::AudioIOError::unsupported_format(format!(
619            "Unsupported output format for streaming FLAC write: {other:?}"
620        ))),
621    }
622}
623
624/// Create a streaming FLAC writer to any [`WriteSeek`] destination (e.g. an in-memory
625/// cursor). See [`create_streamed_flac`] for behaviour and bit-depth rules.
626#[cfg(feature = "flac")]
627pub fn create_streamed_flac_writer<W, T>(
628    writer: W,
629    channels: u16,
630    sample_rate: u32,
631) -> AudioIOResult<StreamedFlacWriter<W>>
632where
633    W: WriteSeek,
634    T: StandardSample + 'static,
635{
636    flac_writer_for_type::<T, W>(writer, channels, sample_rate)
637}
638
639/// Construct a [`StreamedFlacWriter`] for sample type `T` at the default compression level.
640///
641/// Shared by [`create_streamed_flac`] and [`create_streamed_flac_writer`].
642#[cfg(feature = "flac")]
643fn flac_writer_for_type<T, W>(writer: W, channels: u16, sample_rate: u32) -> AudioIOResult<StreamedFlacWriter<W>>
644where
645    T: StandardSample + 'static,
646    W: WriteSeek,
647{
648    let sample_type = validated_sample_type_of::<T>()?;
649    StreamedFlacWriter::new(writer, channels, sample_rate, sample_type, CompressionLevel::default())
650}
651
652/// Create a streaming WAV writer to any `WriteSeek` destination.
653///
654/// The sample type is inferred from the generic parameter `T`. Allows streaming to
655/// in-memory buffers, network streams, or any custom `WriteSeek` implementation.
656#[cfg(feature = "wav")]
657pub fn create_streamed_writer<W, T>(writer: W, channels: u16, sample_rate: u32) -> AudioIOResult<StreamedWavWriter<W>>
658where
659    W: WriteSeek,
660    T: StandardSample + 'static,
661{
662    wav_writer_for_type::<T, W>(writer, channels, sample_rate)
663}
664
665/// Dispatch to the appropriate `StreamedWavWriter` constructor based on `T`.
666///
667/// Shared by `create_streamed` and `create_streamed_writer` to avoid duplication.
668#[cfg(feature = "wav")]
669fn wav_writer_for_type<T, W>(writer: W, channels: u16, sample_rate: u32) -> AudioIOResult<StreamedWavWriter<W>>
670where
671    T: StandardSample + 'static,
672    W: WriteSeek,
673{
674    use audio_samples::I24;
675    let type_id = TypeId::of::<T>();
676    match type_id {
677        id if id == TypeId::of::<u8>() || id == TypeId::of::<i16>() => {
678            StreamedWavWriter::new_i16(writer, channels, sample_rate)
679        },
680        id if id == TypeId::of::<I24>() => StreamedWavWriter::new_i24(writer, channels, sample_rate),
681        id if id == TypeId::of::<i32>() => StreamedWavWriter::new_i32(writer, channels, sample_rate),
682        id if id == TypeId::of::<f32>() => StreamedWavWriter::new_f32(writer, channels, sample_rate),
683        id if id == TypeId::of::<f64>() => StreamedWavWriter::new_f64(writer, channels, sample_rate),
684        _ => Err(AudioIOError::unsupported_format(format!(
685            "No WAV encoding for sample type (TypeId: {type_id:?})"
686        ))),
687    }
688}
689
690/// Map a Rust sample type to its [`ValidatedSampleType`].
691#[cfg(any(feature = "wav", feature = "flac"))]
692fn validated_sample_type_of<T>() -> AudioIOResult<ValidatedSampleType>
693where
694    T: StandardSample + 'static,
695{
696    use audio_samples::I24;
697    let id = TypeId::of::<T>();
698    if id == TypeId::of::<u8>() {
699        Ok(ValidatedSampleType::U8)
700    } else if id == TypeId::of::<i16>() {
701        Ok(ValidatedSampleType::I16)
702    } else if id == TypeId::of::<I24>() {
703        Ok(ValidatedSampleType::I24)
704    } else if id == TypeId::of::<i32>() {
705        Ok(ValidatedSampleType::I32)
706    } else if id == TypeId::of::<f32>() {
707        Ok(ValidatedSampleType::F32)
708    } else if id == TypeId::of::<f64>() {
709        Ok(ValidatedSampleType::F64)
710    } else {
711        Err(AudioIOError::unsupported_format(format!(
712            "No WAV encoding for sample type (TypeId: {id:?})"
713        )))
714    }
715}
716
717/// Create a non-seekable streaming WAV writer ([`WavSink`](crate::wav::WavSink)) over any
718/// `Write` destination — stdout, a pipe, a socket, etc.
719///
720/// Because a `!Seek` sink cannot backpatch size fields, the header is written with final sizes
721/// up front. Pass `total_frames = Some(n)` when the frame count is known (recommended; produces
722/// a fully standard file and verifies the count on `finalize`), or `None` for an open-ended
723/// stream (uses the `0xFFFFFFFF` streaming-size convention).
724///
725/// # Example
726///
727/// ```no_run
728/// use audio_samples_io::create_streamed_sink;
729/// use audio_samples_io::traits::{AudioStreamWrite, AudioStreamWriter};
730/// use audio_samples::{AudioSamples, nzu, sample_rate};
731///
732/// let stdout = std::io::stdout();
733/// let mut sink = create_streamed_sink::<_, i16>(stdout.lock(), 1, 44100, Some(1024))?;
734/// let audio = AudioSamples::<f32>::zeros_mono(nzu!(1024), sample_rate!(44100));
735/// sink.write_frames(&audio)?;
736/// sink.finalize()?;
737/// # Ok::<(), audio_samples_io::error::AudioIOError>(())
738/// ```
739#[cfg(feature = "wav")]
740pub fn create_streamed_sink<W, T>(
741    writer: W,
742    channels: u16,
743    sample_rate: u32,
744    total_frames: Option<usize>,
745) -> AudioIOResult<wav::WavSink<W>>
746where
747    W: Write,
748    T: StandardSample + 'static,
749{
750    let sample_type = validated_sample_type_of::<T>()?;
751    wav::WavSink::new(writer, channels, sample_rate, sample_type, total_frames)
752}
753
754/// Open an audio file for reading/writing operations
755///
756/// Returns a trait object that can be used for low-level file operations.
757/// For simple use cases, prefer the `read()` and `info()` convenience functions.
758/// Currently supports WAV and FLAC formats (with appropriate features enabled).
759pub fn open<P>(fp: P) -> AudioIOResult<Box<dyn AudioFile>>
760where
761    P: AsRef<Path>,
762{
763    let path = fp.as_ref();
764
765    match FileType::from_path(path) {
766        FileType::WAV => {
767            #[cfg(not(feature = "wav"))]
768            return Err(crate::error::AudioIOError::missing_feature(
769                "'wav' feature must be enabled to open WAV files",
770            ));
771
772            #[cfg(feature = "wav")]
773            {
774                let wav_file = WavFile::open_with_options(path, OpenOptions::default())?;
775                Ok(Box::new(wav_file))
776            }
777        },
778        FileType::FLAC => {
779            #[cfg(not(feature = "flac"))]
780            return Err(crate::error::AudioIOError::missing_feature(
781                "'flac' feature must be enabled to open FLAC files",
782            ));
783
784            #[cfg(feature = "flac")]
785            {
786                use crate::flac::FlacFile;
787                use crate::traits::AudioFile;
788                let flac_file = FlacFile::open_with_options(path, OpenOptions::default())?;
789                Ok(Box::new(flac_file))
790            }
791        },
792        other => Err(crate::error::AudioIOError::unsupported_format(format!(
793            "Unsupported file format: {other:?}"
794        ))),
795    }
796}
797
798pub fn write<P, T>(fp: P, audio: &AudioSamples<T>) -> AudioIOResult<()>
799where
800    P: AsRef<Path>,
801    T: StandardSample + 'static,
802{
803    write_with_options(fp, audio, WriteOptions::default())
804}
805
806/// Write audio samples to a file with explicit [`WriteOptions`].
807///
808/// Identical to [`write`] but lets you control the write-buffer size:
809///
810/// ```no_run
811/// use audio_samples_io::{write_with_options, WriteOptions};
812/// use audio_samples::{AudioSamples, sine_wave, sample_rate};
813/// use std::time::Duration;
814///
815/// let audio = sine_wave::<f32>(440.0, Duration::from_secs(60), sample_rate!(44100), 0.5);
816///
817/// // 16 MiB buffer for a 60-second file (~21 MiB stereo f32).
818/// write_with_options("long.wav", &audio, WriteOptions { write_buf_capacity: 16 * 1024 * 1024 })?;
819/// # Ok::<(), audio_samples_io::error::AudioIOError>(())
820/// ```
821pub fn write_with_options<P, T>(fp: P, audio: &AudioSamples<T>, opts: WriteOptions) -> AudioIOResult<()>
822where
823    P: AsRef<Path>,
824    T: StandardSample + 'static,
825{
826    let path = fp.as_ref();
827
828    match FileType::from_path(path) {
829        FileType::WAV => {
830            #[cfg(not(feature = "wav"))]
831            return Err(crate::error::AudioIOError::missing_feature(
832                "'wav' feature must be enabled to write WAV files",
833            ));
834
835            #[cfg(feature = "wav")]
836            {
837                let file = std::fs::File::create(path)?;
838                crate::wav::wav_file::write_wav(file, audio, opts)
839            }
840        },
841        FileType::FLAC => {
842            #[cfg(not(feature = "flac"))]
843            return Err(crate::error::AudioIOError::missing_feature(
844                "'flac' feature must be enabled to write FLAC files",
845            ));
846
847            #[cfg(feature = "flac")]
848            {
849                let file = std::fs::File::create(path)?;
850                let buf_writer = std::io::BufWriter::with_capacity(opts.write_buf_capacity, file);
851                crate::flac::write_flac(buf_writer, audio, CompressionLevel::default())
852            }
853        },
854        other => Err(crate::error::AudioIOError::unsupported_format(format!(
855            "Unsupported format: {other:?}"
856        ))),
857    }
858}
859
860/// Write a WAV file with trailing metadata chunks (LIST/INFO tags, cue points).
861///
862/// Like [`write`], but also serialises the given [`WavMetadata`](crate::wav::WavMetadata) after
863/// the audio data — letting you persist tags/markers that a plain read→write round-trip would
864/// drop. WAV only.
865///
866/// ```no_run
867/// use audio_samples_io::write_with_metadata;
868/// use audio_samples_io::wav::WavMetadata;
869/// use audio_samples_io::wav::list_info::InfoMetadata;
870/// use audio_samples::{AudioSamples, sine_wave, sample_rate};
871/// use std::time::Duration;
872///
873/// let audio = sine_wave::<f32>(440.0, Duration::from_secs(1), sample_rate!(44100), 0.5);
874/// let mut meta = WavMetadata::default();
875/// meta.info = Some(InfoMetadata { title: Some("My Track".into()), ..Default::default() });
876/// write_with_metadata("tagged.wav", &audio, &meta)?;
877/// # Ok::<(), audio_samples_io::error::AudioIOError>(())
878/// ```
879#[cfg(feature = "wav")]
880pub fn write_with_metadata<P, T>(
881    fp: P,
882    audio: &AudioSamples<T>,
883    metadata: &crate::wav::WavMetadata,
884) -> AudioIOResult<()>
885where
886    P: AsRef<Path>,
887    T: StandardSample + 'static,
888{
889    let file = std::fs::File::create(fp)?;
890    crate::wav::wav_file::write_wav_with_metadata(file, audio, WriteOptions::default(), metadata)
891}
892
893/// Write a WAV file with trailing metadata to any `Write` destination (e.g. an in-memory buffer).
894#[cfg(feature = "wav")]
895pub fn write_with_metadata_to<T, W>(
896    writer: W,
897    audio: &AudioSamples<T>,
898    metadata: &crate::wav::WavMetadata,
899) -> AudioIOResult<()>
900where
901    T: StandardSample + 'static,
902    W: Write,
903{
904    crate::wav::wav_file::write_wav_with_metadata(writer, audio, WriteOptions::default(), metadata)
905}
906
907/// Write audio data to any `Write` destination with explicit format specification.
908///
909/// This function allows writing audio data to any destination implementing `Write`,
910/// such as in-memory buffers, network streams, or custom I/O implementations.
911/// Unlike `write()` which determines format from file extension, this function
912/// requires an explicit format parameter.
913///
914/// # Arguments
915///
916/// * `writer` - The destination implementing `Write`
917/// * `audio` - The audio samples to write
918/// * `format` - The target audio format (WAV, FLAC, etc.)
919///
920/// # Example
921///
922/// ```no_run
923/// use audio_samples_io::{write_with, types::FileType};
924/// use audio_samples::{AudioSamples, sine_wave, sample_rate};
925/// use std::io::Cursor;
926/// use std::time::Duration;
927///
928/// let audio = sine_wave::<f32>(440.0, Duration::from_secs(1), sample_rate!(44100), 0.5);
929/// let mut buffer = Vec::new();
930/// let cursor = Cursor::new(&mut buffer);
931///
932/// write_with(cursor, &audio, FileType::WAV)?;
933/// # Ok::<(), audio_samples_io::error::AudioIOError>(())
934/// ```
935pub fn write_with<T, W>(writer: W, audio: &AudioSamples<T>, format: FileType) -> AudioIOResult<()>
936where
937    T: StandardSample + 'static,
938    W: Write,
939{
940    write_with_writer_options(writer, audio, format, WriteOptions::default())
941}
942
943/// Write audio data to any `Write` destination with explicit format and [`WriteOptions`].
944///
945/// Identical to [`write_with`] but lets you control the write-buffer size.
946pub fn write_with_writer_options<T, W>(
947    writer: W,
948    audio: &AudioSamples<T>,
949    format: FileType,
950    opts: WriteOptions,
951) -> AudioIOResult<()>
952where
953    T: StandardSample + 'static,
954    W: Write,
955{
956    match format {
957        FileType::WAV => {
958            #[cfg(not(feature = "wav"))]
959            return Err(crate::error::AudioIOError::missing_feature(
960                "'wav' feature must be enabled to write WAV files",
961            ));
962
963            #[cfg(feature = "wav")]
964            {
965                crate::wav::wav_file::write_wav(writer, audio, opts)
966            }
967        },
968        FileType::FLAC => {
969            #[cfg(not(feature = "flac"))]
970            return Err(crate::error::AudioIOError::missing_feature(
971                "'flac' feature must be enabled to write FLAC files",
972            ));
973
974            #[cfg(feature = "flac")]
975            {
976                crate::flac::write_flac(writer, audio, CompressionLevel::default())
977            }
978        },
979        other => Err(crate::error::AudioIOError::unsupported_format(format!(
980            "Unsupported format for write_with: {other:?}"
981        ))),
982    }
983}
984
985#[cfg(all(test, feature = "wav"))]
986mod lib_tests {
987    use std::time::Duration;
988
989    use audio_samples::sample_rate;
990
991    use super::*;
992
993    #[test]
994    fn test_info_function() {
995        let info_result = info("resources/test.wav");
996        assert!(info_result.is_ok(), "Failed to get info from test WAV file");
997
998        let audio_info = info_result.expect("Expected successful info retrieval");
999        assert_eq!(audio_info.file_type, FileType::WAV);
1000        assert!(audio_info.sample_rate.get() > 0, "Sample rate should be positive");
1001        assert!(audio_info.channels > 0, "Channel count should be positive");
1002        println!("Audio info: {audio_info:#}");
1003    }
1004
1005    #[test]
1006    fn test_read_function() {
1007        let audio_result = read::<_, f32>("resources/test.wav");
1008        assert!(audio_result.is_ok(), "Failed to read test WAV file");
1009
1010        let audio_samples = audio_result.expect("Expected successful audio read");
1011        println!(
1012            "Read {} samples at {} Hz",
1013            audio_samples.len(),
1014            audio_samples.sample_rate()
1015        );
1016    }
1017
1018    #[test]
1019    fn test_open_function() {
1020        let file_result = open("resources/test.wav");
1021        assert!(file_result.is_ok(), "Failed to open test WAV file");
1022    }
1023
1024    #[test]
1025    fn test_write_function() {
1026        use std::fs;
1027
1028        use audio_samples::sine_wave;
1029
1030        // Generate test audio
1031        let sample_rate = sample_rate!(44100);
1032        let sine_samples = sine_wave::<f32>(440.0, Duration::from_secs_f64(0.1), sample_rate, 0.5);
1033
1034        // Test writing WAV file
1035        let output_path = std::env::temp_dir().join("test_lib_write.wav");
1036        write(&output_path, &sine_samples).expect("Failed to write WAV file");
1037
1038        // Verify file exists and can be read back
1039        assert!(fs::metadata(&output_path).is_ok(), "Output file should exist");
1040
1041        let read_back = read::<_, f32>(&output_path).expect("Failed to read back WAV file");
1042        assert_eq!(read_back.sample_rate(), sample_rate);
1043        assert_eq!(read_back.total_samples(), sine_samples.total_samples());
1044        assert_eq!(read_back.num_channels(), sine_samples.num_channels());
1045
1046        // Verify the actual audio data matches (approximately for floating point)
1047        let original_bytes = sine_samples.bytes().expect("bytes should be available");
1048        let read_bytes = read_back.bytes().expect("bytes should be available");
1049        assert_eq!(
1050            original_bytes.as_slice().len(),
1051            read_bytes.as_slice().len(),
1052            "Audio data size should match"
1053        );
1054
1055        // For f32, check that values are very close (allowing for minor precision differences)
1056        let original_samples: &[f32] = bytemuck::cast_slice(original_bytes.as_slice());
1057        let read_samples: &[f32] = bytemuck::cast_slice(read_bytes.as_slice());
1058
1059        for (i, (orig, read)) in original_samples.iter().zip(read_samples.iter()).enumerate() {
1060            let diff = (orig - read).abs();
1061            assert!(
1062                diff < 1e-6,
1063                "Sample {i} differs too much: {orig} vs {read} (diff: {diff})"
1064            );
1065        }
1066
1067        // Clean up
1068        fs::remove_file(&output_path).ok();
1069    }
1070
1071    #[test]
1072    fn test_write_with_function() {
1073        use std::io::Cursor;
1074
1075        use audio_samples::sine_wave;
1076
1077        // Generate test audio
1078        let sample_rate = sample_rate!(22050);
1079        let sine_samples = sine_wave::<i16>(880.0, Duration::from_secs_f64(0.05), sample_rate, 0.8);
1080
1081        // Write to in-memory buffer
1082        let mut buffer = Vec::new();
1083        let cursor = Cursor::new(&mut buffer);
1084        write_with(cursor, &sine_samples, FileType::WAV).expect("Failed to write with cursor");
1085
1086        // Verify buffer has WAV data
1087        assert!(buffer.len() > 44, "Buffer should contain WAV header and data");
1088        assert_eq!(&buffer[0..4], b"RIFF", "Should start with RIFF header");
1089        assert_eq!(&buffer[8..12], b"WAVE", "Should contain WAVE identifier");
1090
1091        // Verify we can read the WAV data back from the buffer
1092        let temp_file = std::env::temp_dir().join("test_write_with_buffer.wav");
1093        std::fs::write(&temp_file, &buffer).expect("Failed to write buffer to temp file");
1094
1095        let read_back = read::<_, i16>(&temp_file).expect("Failed to read back WAV from buffer");
1096        assert_eq!(read_back.sample_rate(), sample_rate);
1097        assert_eq!(read_back.total_samples(), sine_samples.total_samples());
1098        assert_eq!(read_back.num_channels(), sine_samples.num_channels());
1099
1100        // Verify the actual audio data matches exactly for i16
1101        let original_bytes = sine_samples.bytes().expect("bytes should be available");
1102        let read_bytes = read_back.bytes().expect("bytes should be available");
1103        assert_eq!(
1104            original_bytes.as_slice(),
1105            read_bytes.as_slice(),
1106            "Audio data should match exactly for i16"
1107        );
1108
1109        // Clean up
1110        std::fs::remove_file(&temp_file).ok();
1111    }
1112
1113    #[test]
1114    fn test_write_with_format_parameter() {
1115        use std::io::Cursor;
1116
1117        use audio_samples::sine_wave;
1118
1119        // Generate test audio
1120        let sample_rate = sample_rate!(44100);
1121        let sine_samples = sine_wave::<f32>(440.0, Duration::from_secs_f64(0.01), sample_rate, 0.5);
1122
1123        // Test with explicit WAV format
1124        let mut wav_buffer = Vec::new();
1125        let wav_cursor = Cursor::new(&mut wav_buffer);
1126        write_with(wav_cursor, &sine_samples, FileType::WAV).expect("Failed to write WAV format");
1127
1128        // Verify WAV buffer has data and proper WAV header
1129        assert!(wav_buffer.len() > 44, "WAV buffer should contain header and data");
1130        assert_eq!(&wav_buffer[0..4], b"RIFF", "Should start with RIFF header");
1131        assert_eq!(&wav_buffer[8..12], b"WAVE", "Should contain WAVE identifier");
1132
1133        // Test error for unsupported format
1134        let mut buffer = Vec::new();
1135        let cursor = Cursor::new(&mut buffer);
1136        let result = write_with(cursor, &sine_samples, FileType::MP3);
1137        assert!(result.is_err(), "Should return error for unsupported format");
1138
1139        let error_msg = format!("{}", result.expect_err("Expected error"));
1140        assert!(
1141            error_msg.contains("Unsupported format"),
1142            "Error should mention unsupported format"
1143        );
1144    }
1145
1146    #[test]
1147    fn test_write_different_formats() {
1148        use std::fs;
1149
1150        use audio_samples::{AudioTypeConversion, sine_wave};
1151
1152        let sample_rate = sample_rate!(48000);
1153        let sine_base = sine_wave::<f32>(1000.0, Duration::from_secs_f64(0.02), sample_rate, 0.3);
1154
1155        // Test different sample types
1156        let test_cases = vec![
1157            ("i16", std::env::temp_dir().join("test_format_i16.wav")),
1158            ("f32", std::env::temp_dir().join("test_format_f32.wav")),
1159        ];
1160
1161        for (format_name, output_path) in test_cases {
1162            match format_name {
1163                "i16" => {
1164                    let samples_i16 = sine_base.to_format::<i16>();
1165                    write(&output_path, &samples_i16).expect("Failed to write i16 WAV");
1166
1167                    // Verify the written file can be read back as i16
1168                    let read_back = read::<_, i16>(&output_path).expect("Failed to read back i16 WAV");
1169                    assert_eq!(read_back.sample_rate(), sample_rate, "Sample rate mismatch for i16");
1170                    assert_eq!(
1171                        read_back.total_samples(),
1172                        samples_i16.total_samples(),
1173                        "Sample count mismatch for i16"
1174                    );
1175
1176                    // Verify WAV file properties using info function
1177                    let wav_info = info(&output_path).expect("Failed to get WAV info for i16");
1178                    assert_eq!(wav_info.bits_per_sample, 16, "Bits per sample should be 16 for i16");
1179                    assert_eq!(
1180                        wav_info.sample_type,
1181                        audio_samples::SampleType::I16,
1182                        "Sample type should be I16"
1183                    );
1184                },
1185                "f32" => {
1186                    write(&output_path, &sine_base).expect("Failed to write f32 WAV");
1187
1188                    // Verify the written file can be read back as f32
1189                    let read_back = read::<_, f32>(&output_path).expect("Failed to read back f32 WAV");
1190                    assert_eq!(read_back.sample_rate(), sample_rate, "Sample rate mismatch for f32");
1191                    assert_eq!(
1192                        read_back.total_samples(),
1193                        sine_base.total_samples(),
1194                        "Sample count mismatch for f32"
1195                    );
1196
1197                    // Verify WAV file properties using info function
1198                    let wav_info = info(&output_path).expect("Failed to get WAV info for f32");
1199                    assert_eq!(wav_info.bits_per_sample, 32, "Bits per sample should be 32 for f32");
1200                    assert_eq!(
1201                        wav_info.sample_type,
1202                        audio_samples::SampleType::F32,
1203                        "Sample type should be F32"
1204                    );
1205                },
1206                _ => unreachable!("Unknown format"),
1207            }
1208
1209            // Verify file was created and is a valid WAV
1210            assert!(
1211                fs::metadata(&output_path).is_ok(),
1212                "File should exist for {format_name}"
1213            );
1214
1215            // Clean up
1216            fs::remove_file(&output_path).ok();
1217        }
1218    }
1219
1220    #[test]
1221    fn test_unsupported_format_error() {
1222        use audio_samples::sine_wave;
1223
1224        let sample_rate = sample_rate!(44100);
1225        let sine_samples = sine_wave::<f32>(440.0, Duration::from_secs_f64(0.01), sample_rate, 0.1);
1226
1227        // Try to write to unsupported format
1228        let result = write(std::env::temp_dir().join("test.mp3"), &sine_samples);
1229        assert!(result.is_err(), "Should fail for unsupported format");
1230
1231        let error_msg = format!("{}", result.expect_err("Expected error"));
1232        assert!(
1233            error_msg.contains("Unsupported"),
1234            "Error should mention unsupported format"
1235        );
1236    }
1237}