From 1dd92669f4b6ba2ff62bb87bdb3b6be42114aa3f Mon Sep 17 00:00:00 2001 From: David Kleingeld Date: Fri, 29 Aug 2025 21:54:20 +0200 Subject: [PATCH 1/6] add wav output method which takes writer This will enable sending the wav somewhere else then the local filesystem. Examples are: send over tcp/http, collect multiple in a tar (my usecase). --- src/wav_output.rs | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/wav_output.rs b/src/wav_output.rs index 18f30c65..bae6a575 100644 --- a/src/wav_output.rs +++ b/src/wav_output.rs @@ -2,27 +2,43 @@ use crate::common::assert_error_traits; use crate::Source; use hound::{SampleFormat, WavSpec}; use std::path; +use std::io; use std::sync::Arc; #[derive(Debug, thiserror::Error, Clone)] pub enum ToWavError { - #[error("Could not create wav file")] + #[error("Opening file for writing")] + OpenFile(#[source] Arc), + #[error("Could not create wav writer")] Creating(#[source] Arc), - #[error("Failed to write samples to wav file")] + #[error("Failed to write samples writer")] Writing(#[source] Arc), #[error("Failed to update the wav header")] Finishing(#[source] Arc), } assert_error_traits!(ToWavError); -/// This procedure saves Source's output into a wav file. The output samples format is 32-bit float. -/// This function is intended primarily for testing and diagnostics. It can be used to see +/// Saves Source's output into a wav file. The output samples format is 32-bit +/// float. This function is intended primarily for testing and diagnostics. It can be used to see /// the output without opening output stream to a real audio device. /// /// If the file already exists it will be overwritten. pub fn output_to_wav( source: &mut impl Source, wav_file: impl AsRef, +) -> Result<(), ToWavError> { + let file = std::fs::File::create(wav_file) + .map_err(Arc::new) + .map_err(ToWavError::OpenFile)?; + collect_to_wav(source, file) +} + +/// Saves Source's output into a writer. The output samples format is 32-bit float. This function +/// is intended primarily for testing and diagnostics. It can be used to see the output without +/// opening output stream to a real audio device. +pub fn collect_to_wav( + source: &mut impl Source, + writer: impl io::Write + io::Seek, ) -> Result<(), ToWavError> { let format = WavSpec { channels: source.channels().get(), @@ -30,7 +46,8 @@ pub fn output_to_wav( bits_per_sample: 32, sample_format: SampleFormat::Float, }; - let mut writer = hound::WavWriter::create(wav_file, format) + let writer = io::BufWriter::new(writer); + let mut writer = hound::WavWriter::new(writer, format) .map_err(Arc::new) .map_err(ToWavError::Creating)?; for sample in source { From 43503b4a8c5773fe2ee1c1fd43b6d2aa85f9ace2 Mon Sep 17 00:00:00 2001 From: David Kleingeld Date: Fri, 29 Aug 2025 22:24:09 +0200 Subject: [PATCH 2/6] Adds example and makes both wav_output functions own source It does not make sense to get them by ref. Since we will not return till they are fully exhausted (or never if the source is infinite). Therefore there is no use to borrowing --- src/lib.rs | 3 +++ src/wav_output.rs | 26 +++++++++++++++++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5e7890eb..5283d407 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -199,3 +199,6 @@ pub use crate::stream::{play, OutputStream, OutputStreamBuilder, PlayError, Stre #[cfg(feature = "wav_output")] #[cfg_attr(docsrs, doc(cfg(feature = "wav_output")))] pub use crate::wav_output::output_to_wav; +#[cfg(feature = "wav_output")] +#[cfg_attr(docsrs, doc(cfg(feature = "wav_output")))] +pub use crate::wav_output::collect_to_wav; diff --git a/src/wav_output.rs b/src/wav_output.rs index bae6a575..3ae489f8 100644 --- a/src/wav_output.rs +++ b/src/wav_output.rs @@ -24,21 +24,37 @@ assert_error_traits!(ToWavError); /// /// If the file already exists it will be overwritten. pub fn output_to_wav( - source: &mut impl Source, + source: impl Source, wav_file: impl AsRef, ) -> Result<(), ToWavError> { - let file = std::fs::File::create(wav_file) + let mut file = std::fs::File::create(wav_file) .map_err(Arc::new) .map_err(ToWavError::OpenFile)?; - collect_to_wav(source, file) + collect_to_wav(source, &mut file) } /// Saves Source's output into a writer. The output samples format is 32-bit float. This function /// is intended primarily for testing and diagnostics. It can be used to see the output without /// opening output stream to a real audio device. +/// +/// # Example +/// ```rust +/// # use rodio::static_buffer::StaticSamplesBuffer; +/// # use rodio::collect_to_wav; +/// # const SAMPLES: [rodio::Sample; 5] = [0.0, 1.0, 2.0, 3.0, 4.0]; +/// # let source = StaticSamplesBuffer::new( +/// # 1.try_into().unwrap(), +/// # 1.try_into().unwrap(), +/// # &SAMPLES +/// # ); +/// let mut writer = std::io::Cursor::new(Vec::new()); +/// collect_to_wav(source, &mut writer)?; +/// let wav_bytes: Vec = writer.into_inner(); +/// # Ok::<(), Box>(()) +/// ``` pub fn collect_to_wav( - source: &mut impl Source, - writer: impl io::Write + io::Seek, + source: impl Source, + writer: &mut (impl io::Write + io::Seek), ) -> Result<(), ToWavError> { let format = WavSpec { channels: source.channels().get(), From 12c3aba88e32f16df1dfb4c6124e5c6e2119b750 Mon Sep 17 00:00:00 2001 From: dvdsk Date: Sat, 30 Aug 2025 15:41:28 +0200 Subject: [PATCH 3/6] flush buffer, changelog entry, rename wav output fns --- CHANGELOG.md | 2 ++ src/lib.rs | 4 ++-- src/wav_output.rs | 43 +++++++++++++++++++++++++++---------------- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfee3ae1..f44f6d5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Adds a new input source: Microphone. - Adds a new method on source: record which collects all samples into a SamplesBuffer. +- Adds `wav_to_writer` which writes a `Source` to a writer. ### Fixed - docs.rs will now document all features, including those that are optional. @@ -27,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `PeriodicAccess` is slightly more accurate for 44.1 kHz sample rate families. ### Changed +- `output_to_wav` renamed to `wav_to_file` and now takes ownership of the `Source`. - `Blue` noise generator uses uniform instead of Gaussian noise for better performance. - `Gaussian` noise generator has standard deviation of 0.6 for perceptual equivalence. diff --git a/src/lib.rs b/src/lib.rs index 5283d407..e4b8ba43 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -198,7 +198,7 @@ pub use crate::spatial_sink::SpatialSink; pub use crate::stream::{play, OutputStream, OutputStreamBuilder, PlayError, StreamError}; #[cfg(feature = "wav_output")] #[cfg_attr(docsrs, doc(cfg(feature = "wav_output")))] -pub use crate::wav_output::output_to_wav; +pub use crate::wav_output::wav_to_file; #[cfg(feature = "wav_output")] #[cfg_attr(docsrs, doc(cfg(feature = "wav_output")))] -pub use crate::wav_output::collect_to_wav; +pub use crate::wav_output::wav_to_writer; diff --git a/src/wav_output.rs b/src/wav_output.rs index 3ae489f8..54de223d 100644 --- a/src/wav_output.rs +++ b/src/wav_output.rs @@ -1,8 +1,8 @@ use crate::common::assert_error_traits; use crate::Source; use hound::{SampleFormat, WavSpec}; +use std::io::{self, Write}; use std::path; -use std::io; use std::sync::Arc; #[derive(Debug, thiserror::Error, Clone)] @@ -15,6 +15,8 @@ pub enum ToWavError { Writing(#[source] Arc), #[error("Failed to update the wav header")] Finishing(#[source] Arc), + #[error("Failed to flush all bytes to writer")] + Flushing(#[source] Arc), } assert_error_traits!(ToWavError); @@ -23,14 +25,17 @@ assert_error_traits!(ToWavError); /// the output without opening output stream to a real audio device. /// /// If the file already exists it will be overwritten. -pub fn output_to_wav( +/// +/// # Note +/// This is a convenience wrapper around `wav_to_writer` +pub fn wav_to_file( source: impl Source, wav_file: impl AsRef, ) -> Result<(), ToWavError> { let mut file = std::fs::File::create(wav_file) .map_err(Arc::new) .map_err(ToWavError::OpenFile)?; - collect_to_wav(source, &mut file) + wav_to_writer(source, &mut file) } /// Saves Source's output into a writer. The output samples format is 32-bit float. This function @@ -43,16 +48,16 @@ pub fn output_to_wav( /// # use rodio::collect_to_wav; /// # const SAMPLES: [rodio::Sample; 5] = [0.0, 1.0, 2.0, 3.0, 4.0]; /// # let source = StaticSamplesBuffer::new( -/// # 1.try_into().unwrap(), -/// # 1.try_into().unwrap(), +/// # 1.try_into().unwrap(), +/// # 1.try_into().unwrap(), /// # &SAMPLES /// # ); /// let mut writer = std::io::Cursor::new(Vec::new()); -/// collect_to_wav(source, &mut writer)?; +/// wav_to_writer(source, &mut writer)?; /// let wav_bytes: Vec = writer.into_inner(); /// # Ok::<(), Box>(()) /// ``` -pub fn collect_to_wav( +pub fn wav_to_writer( source: impl Source, writer: &mut (impl io::Write + io::Seek), ) -> Result<(), ToWavError> { @@ -62,20 +67,26 @@ pub fn collect_to_wav( bits_per_sample: 32, sample_format: SampleFormat::Float, }; - let writer = io::BufWriter::new(writer); - let mut writer = hound::WavWriter::new(writer, format) - .map_err(Arc::new) - .map_err(ToWavError::Creating)?; - for sample in source { + let mut writer = io::BufWriter::new(writer); + { + let mut writer = hound::WavWriter::new(&mut writer, format) + .map_err(Arc::new) + .map_err(ToWavError::Creating)?; + for sample in source { + writer + .write_sample(sample) + .map_err(Arc::new) + .map_err(ToWavError::Writing)?; + } writer - .write_sample(sample) + .finalize() .map_err(Arc::new) - .map_err(ToWavError::Writing)?; + .map_err(ToWavError::Finishing)?; } writer - .finalize() + .flush() .map_err(Arc::new) - .map_err(ToWavError::Finishing)?; + .map_err(ToWavError::Flushing)?; Ok(()) } From 082f2128b67e1915638986ef460e8238b36b57c5 Mon Sep 17 00:00:00 2001 From: dvdsk Date: Sat, 30 Aug 2025 21:28:26 +0200 Subject: [PATCH 4/6] fix test --- src/wav_output.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wav_output.rs b/src/wav_output.rs index 54de223d..fb987d2a 100644 --- a/src/wav_output.rs +++ b/src/wav_output.rs @@ -92,20 +92,20 @@ pub fn wav_to_writer( #[cfg(test)] mod test { - use super::output_to_wav; + use super::wav_to_file; use crate::Source; use std::io::BufReader; use std::time::Duration; #[test] - fn test_output_to_wav() { + fn test_wav_to_file() { let make_source = || { crate::source::SineWave::new(745.0) .amplify(0.1) .take_duration(Duration::from_secs(1)) }; let wav_file_path = "target/tmp/save-to-wav-test.wav"; - output_to_wav(&mut make_source(), wav_file_path).expect("output file can be written"); + wav_to_file(&mut make_source(), wav_file_path).expect("output file can be written"); let file = std::fs::File::open(wav_file_path).expect("output file can be opened"); // Not using crate::Decoder bcause it is limited to i16 samples. From 82514bd1f2c6cfd9a1a885019b26a8ffea75bc5c Mon Sep 17 00:00:00 2001 From: David Kleingeld Date: Mon, 1 Sep 2025 21:57:26 +0200 Subject: [PATCH 5/6] Only write whole frames to wav The wav writer returns an error if we do not write a whole frame. This prevents that error by only writing whole frames. --- src/wav_output.rs | 44 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/src/wav_output.rs b/src/wav_output.rs index fb987d2a..32c8aa18 100644 --- a/src/wav_output.rs +++ b/src/wav_output.rs @@ -1,5 +1,6 @@ use crate::common::assert_error_traits; use crate::Source; +use crate::Sample; use hound::{SampleFormat, WavSpec}; use std::io::{self, Write}; use std::path; @@ -29,7 +30,7 @@ assert_error_traits!(ToWavError); /// # Note /// This is a convenience wrapper around `wav_to_writer` pub fn wav_to_file( - source: impl Source, + source: impl Source, // TODO make this take a spanless source wav_file: impl AsRef, ) -> Result<(), ToWavError> { let mut file = std::fs::File::create(wav_file) @@ -58,7 +59,7 @@ pub fn wav_to_file( /// # Ok::<(), Box>(()) /// ``` pub fn wav_to_writer( - source: impl Source, + source: impl Source, // TODO make this take a spanless source writer: &mut (impl io::Write + io::Seek), ) -> Result<(), ToWavError> { let format = WavSpec { @@ -72,12 +73,15 @@ pub fn wav_to_writer( let mut writer = hound::WavWriter::new(&mut writer, format) .map_err(Arc::new) .map_err(ToWavError::Creating)?; - for sample in source { + + let whole_frames = WholeFrames::new(source); + for sample in whole_frames { writer .write_sample(sample) .map_err(Arc::new) .map_err(ToWavError::Writing)?; } + writer .finalize() .map_err(Arc::new) @@ -90,6 +94,40 @@ pub fn wav_to_writer( Ok(()) } +struct WholeFrames> { + buffer: Vec, + pos: usize, + source: I +} + +impl WholeFrames { + fn new(source: S) -> Self { + Self { + buffer: vec![0.0; source.channels().get().into()], + pos: source.channels().get().into(), + source, + } + } +} + +impl> Iterator for WholeFrames { + type Item = Sample; + + fn next(&mut self) -> Option { + if self.pos >= self.buffer.len() { + for sample in &mut self.buffer { + *sample = self.source.next()?; + } + self.pos = 0; + } + + let to_yield = self.buffer[self.pos]; + self.pos += 1; + Some(to_yield) + } +} + + #[cfg(test)] mod test { use super::wav_to_file; From ac6e22863971ae65d43ebc7ec557703dc2a85878 Mon Sep 17 00:00:00 2001 From: David Kleingeld Date: Mon, 15 Sep 2025 17:35:30 +0200 Subject: [PATCH 6/6] fixes example --- examples/into_file.rs | 4 ++-- src/wav_output.rs | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/examples/into_file.rs b/examples/into_file.rs index bedd8f8a..1e3c9d4b 100644 --- a/examples/into_file.rs +++ b/examples/into_file.rs @@ -1,4 +1,4 @@ -use rodio::{output_to_wav, Source}; +use rodio::{wav_to_file, Source}; use std::error::Error; /// Converts mp3 file to a wav file. @@ -12,7 +12,7 @@ fn main() -> Result<(), Box> { let wav_path = "music_mp3_converted.wav"; println!("Storing converted audio into {}", wav_path); - output_to_wav(&mut audio, wav_path)?; + wav_to_file(&mut audio, wav_path)?; Ok(()) } diff --git a/src/wav_output.rs b/src/wav_output.rs index 32c8aa18..ddf40f7b 100644 --- a/src/wav_output.rs +++ b/src/wav_output.rs @@ -1,6 +1,6 @@ use crate::common::assert_error_traits; -use crate::Source; use crate::Sample; +use crate::Source; use hound::{SampleFormat, WavSpec}; use std::io::{self, Write}; use std::path; @@ -94,10 +94,10 @@ pub fn wav_to_writer( Ok(()) } -struct WholeFrames> { +struct WholeFrames> { buffer: Vec, pos: usize, - source: I + source: I, } impl WholeFrames { @@ -120,14 +120,13 @@ impl> Iterator for WholeFrames { } self.pos = 0; } - + let to_yield = self.buffer[self.pos]; self.pos += 1; Some(to_yield) } } - #[cfg(test)] mod test { use super::wav_to_file;