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/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/lib.rs b/src/lib.rs index 5e7890eb..e4b8ba43 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -198,4 +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::wav_to_writer; diff --git a/src/wav_output.rs b/src/wav_output.rs index 18f30c65..ddf40f7b 100644 --- a/src/wav_output.rs +++ b/src/wav_output.rs @@ -1,28 +1,66 @@ use crate::common::assert_error_traits; +use crate::Sample; use crate::Source; use hound::{SampleFormat, WavSpec}; +use std::io::{self, Write}; use std::path; 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), + #[error("Failed to flush all bytes to writer")] + Flushing(#[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, +/// +/// # Note +/// This is a convenience wrapper around `wav_to_writer` +pub fn wav_to_file( + 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) + .map_err(Arc::new) + .map_err(ToWavError::OpenFile)?; + wav_to_writer(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()); +/// wav_to_writer(source, &mut writer)?; +/// let wav_bytes: Vec = writer.into_inner(); +/// # Ok::<(), Box>(()) +/// ``` +pub fn wav_to_writer( + source: impl Source, // TODO make this take a spanless source + writer: &mut (impl io::Write + io::Seek), ) -> Result<(), ToWavError> { let format = WavSpec { channels: source.channels().get(), @@ -30,38 +68,81 @@ pub fn output_to_wav( bits_per_sample: 32, sample_format: SampleFormat::Float, }; - let mut writer = hound::WavWriter::create(wav_file, 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)?; + + let whole_frames = WholeFrames::new(source); + for sample in whole_frames { + 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(()) } +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::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.