Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions examples/into_file.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -12,7 +12,7 @@ fn main() -> Result<(), Box<dyn Error>> {

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(())
}
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
115 changes: 98 additions & 17 deletions src/wav_output.rs
Original file line number Diff line number Diff line change
@@ -1,67 +1,148 @@
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<std::io::Error>),
#[error("Could not create wav writer")]
Creating(#[source] Arc<hound::Error>),
#[error("Failed to write samples to wav file")]
#[error("Failed to write samples writer")]
Writing(#[source] Arc<hound::Error>),
#[error("Failed to update the wav header")]
Finishing(#[source] Arc<hound::Error>),
#[error("Failed to flush all bytes to writer")]
Flushing(#[source] Arc<std::io::Error>),
}
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<path::Path>,
) -> 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<u8> = writer.into_inner();
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
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(),
sample_rate: source.sample_rate().get(),
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<I: Iterator<Item = Sample>> {
buffer: Vec<Sample>,
pos: usize,
source: I,
}

impl<S: Source> WholeFrames<S> {
fn new(source: S) -> Self {
Self {
buffer: vec![0.0; source.channels().get().into()],
pos: source.channels().get().into(),
source,
}
}
}

impl<I: Iterator<Item = Sample>> Iterator for WholeFrames<I> {
type Item = Sample;

fn next(&mut self) -> Option<Sample> {
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.
Expand Down
Loading