hound: reading and writing WAVE files in Rust without pulling in a codec
A wav encoding and decoding library in Rust
At a glance
- What is it?
- hound is a small Rust library for the WAVE format that reads and writes uncompressed audio, supports integer PCM and IEEE float samples, and accepts three different chunk format headers on read. It suits a Rust program that needs to move audio bytes around without adopting a general purpose audio stack, and it is a format library rather than a decoder, so compressed formats are out of scope.
- Who is it for?
- hound fits a Rust project that needs to read or produce raw PCM audio files, particularly one that is already handling sample data itself and wants a small dependency with no codec machinery attached. It does not fit a project that needs to decode compressed audio, since the library covers only the uncompressed WAVE format, and it does not fit a program that wants a high level audio API, since there is no mixing, resampling or device handling.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 75 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A format library, deliberately not an audio stack
Hound is a wav encoding and decoding library in Rust, and the README describes it as being able to read and write the WAVE audio format, an ubiquitous format for raw, uncompressed audio.
The word ubiquitous in that sentence is the design brief. WAVE is the container that PCM audio arrives in, it has been the interchange format for decades, and it carries no codec, which means a library for it does not need a decoder. A file is a header, a data chunk, and samples.
That is the entire scope, and it is worth being clear about because the name suggests more. There is no decoding of compressed formats, no resampling, no mixing, no effect processing and no device access. What you get is a reader that walks the chunks of a WAVE file and gives you samples, and a writer that produces one.
The stated motivation is unusual and worth quoting, because it explains the project's priorities. The main motivation to write it was to test Claxon, a FLAC decoding library written in Rust, also by the same author. So hound exists partly as a testbed, and that explains a detail in the dependency list you would otherwise find puzzling.
The crate is Apache-2.0 licensed, the version is 3.5.1, the primary language is Rust, and the default branch is named release rather than main. The last push was on 2026-08-09, and the repository has no GitHub releases, so the crates.io version and the commit history are the version record.
Writing a file, and the sample format that decides everything
The first example in the README renders a 440 Hz sine wave and stores it as a mono wav file with a sample rate of 44.1 kHz and 16 bits per sample.
use std::f32::consts::PI;
use std::i16;
use hound;
let spec = hound::WavSpec {
channels: 1,
sample_rate: 44100,
bits_per_sample: 16,
sample_format: hound::SampleFormat::Int,
};
let mut writer = hound::WavWriter::create("sine.wav", spec).unwrap();
for t in (0 .. 44100).map(|x| x as f32 / 44100.0) {
let sample = (t * 440.0 * 2.0 * PI).sin();
let amplitude = i16::MAX as f32;
writer.write_sample((sample * amplitude) as i16).unwrap();
}The WavSpec struct is the whole configuration surface, with four fields: channels, sample_rate, bits_per_sample and sample_format, where the sample format is either Int or Float. That last field is the one to get right, because it decides how the bytes in the file are to be interpreted, and a mismatch there produces noise rather than an error.
The second half of the example shows the conversion that trips people up. The sine is computed as an f32, and then it is scaled by i16::MAX as f32 and cast down to i16, because the file format is 16 bit integer PCM. The amplitude constant is the ceiling of the integer range, so the signal uses the full scale without wrapping. A reader that asks for i16 samples gets exactly the values that were written, which is the property that makes the format library useful in a round trip.
The reader is the mirror image, and the second example computes the root mean square of an audio file with at most 16 bits per sample.
use hound;
let mut reader = hound::WavReader::open("testsamples/pop.wav").unwrap();
let sqr_sum = reader.samples::<i16>()
.fold(0.0, |sqr_sum, s| {
let sample = s.unwrap() as f64;
sqr_sum + sample * sample
});
println!("RMS is {}", (sqr_sum / reader.len() as f64).sqrt());The samples call is generic over the integer type, so the reader is asked for i16 and each item comes back as a Result that the closure unwraps. The fold accumulates the sum of squares and the length comes from the reader, which is why the reader is borrowed mutably and then asked for its length after the fold has consumed the iterator.
Three header formats on read, two on write
The features table in the README is the most useful single piece of documentation in the project, because it states exactly where the reader and the writer differ.
On read, the supported formats are PCMWAVEFORMAT, WAVEFORMATEX and WAVEFORMATEXTENSIBLE. On write, the supported formats are PCMWAVEFORMAT and WAVEFORMATEXTENSIBLE. So the reader accepts a superset of what the writer produces.
That gap is not an oversight. WAVEFORMATEX is the older Microsoft extension carrying extra fields such as the channel mask and the sub-format GUID, and files produced by many tools carry it. WAVEFORMATEXTENSIBLE is the further extension that adds a valid bits per sample field, which matters for 24 bit audio stored in a 32 bit container. The plain PCMWAVEFORMAT is the original, and it is enough for a file the writer produces itself, which is why the writer does not need it to be more capable than that.
The encoding row is simpler. Both directions support integer PCM and IEEE float. The bits per sample row gives the detail: 8, 16, 24 and 32 bits for integer, and 32 bits for float.
Two consequences follow for anyone using this in anger. First, a 24 bit integer file and a 32 bit float file are both supported, but a 32 bit float file is not the same thing as a 64 bit double, and there is no 64 bit path in the table. Second, since the reader tolerates headers the writer does not produce, a file written by hound is not necessarily byte identical to a file written by a tool that emits WAVEFORMATEX, and a diff of two files that are supposed to hold the same audio can still show differences in the header.
Finalisation on drop, and the error that surfaces late
There is a short note in the README between the two examples that is easy to skim past and that changes how you should use the writer.
The file is finalized implicitly when the writer is dropped, and to observe errors you should call writer.finalize().
That is the whole story of the design in one sentence. The writer writes a header with placeholder sizes, streams samples into the data chunk, and then has to seek back and patch the real sizes. The patch happens on finalize, and finalize is called by the drop implementation. So in the common case, where you write a file and let the writer go out of scope, everything works and you never see an error.
But the error handling is opt in, and that is the point. A disk that fills up halfway through writing a two hour recording will fail on the write call, not on the finalize, so the failure you will see is an I/O error on a sample write. What finalize reports is the set of errors that only become known at the end, in particular the seek back to the header. A caller that ignores the finalize result, whether by dropping the writer or by calling finalize and discarding its return, will not learn that the file was never properly completed.
The consequence for a library user is a small but real discipline. If you are writing a file whose validity matters, call finalize explicitly and check the result, and do not rely on the drop path to report anything. And if you want to know the length of the data that was written, the reader exposes len, which is what the RMS example uses.
A dependency list that tells you about the Rust version policy
The Cargo.toml in the repository has a single dev-dependency, and the comment attached to it is more informative than the dependency itself.
The dev-dependency is cpal, an audio output library, pinned to 0.2.12, and the comment says it is an older version of cpal but that newer versions depend on the futures library, which is incompatible with the version of Rust that Hound guarantees to support.
That is a small entry that reveals a real constraint. Hound promises a specific Rust version, and the project will not take on a dependency that would raise the minimum, which means the examples that play audio are pinned to an old release of the playback library. The examples directory shows this: append.rs, convert_to_mono.rs, cpal.rs, mean.rs, rms.rs and wavstdout.rs. The cpal example is the one that plays sound, and it is the one built on the old version.
It is also worth noting what is not in the dependency list. There are no runtime dependencies at all. Hound is a self-contained format library, which is what makes it usable inside a project that has strong opinions about its dependency tree, and it is why the license question in the next section comes up at all.
The repository layout supports the same picture. There is a src directory, a testsamples directory with the sample audio the examples read, a fuzz directory, an examples directory, a build.rcl file for the release build, a rust-toolchain.toml pinning the toolchain, a contributing.md and a changelog.md. The fuzz directory is worth a second look, because a library that parses a binary format from untrusted files is exactly the kind of code where fuzzing is not optional, and the presence of a fuzz target is a reasonable signal about how the author treats parsing bugs.
Apache 2.0, and the GPL question the README pre-empts
The licence section of the README is longer than a paragraph, and it is worth reading because it resolves a question that many Rust users have not thought about.
Hound is licensed under the Apache 2.0 license. It may be used in free software as well as closed-source applications, both for commercial and non-commercial use under the conditions given in the license.
Then comes the part that matters for the Rust ecosystem specifically. If you want to use Hound in your GPLv2-licensed software, you can add an exception to your copyright notice, and the README links to the GNU licence FAQ page on incompatible libraries for the exact wording. It then asks you not to open an issue if you disagree with the choice of licence.
That last sentence is unusual to see, and it tells you the author has already had that argument. Apache 2.0 is incompatible with GPLv2, and the usual resolution in the Rust world is exactly what is described here, an exception in the downstream project's copyright notice. The request not to reopen it is a request for a settled question rather than a refusal to discuss the licence at all, and the alternative would have been a licence that is friendlier to copyleft and less convenient for the closed-source applications the README explicitly wants to support.
The final piece of the picture is the upstream project. The homepage points at codeberg.org/ruuda/hound while the Cargo.toml repository field points at github.com/ruuda/hound, which is the usual arrangement of a project that has moved its canonical home and kept the old address working. The Claxon link in the acknowledgement is to a github.com/ruuda/claxon repository. The same author writes both, and the reason hound exists at all, to test Claxon, explains why the two are referenced together.
Editorial conclusion
hound fits a Rust project that needs to read or produce raw PCM audio files, particularly one that is already handling sample data itself and wants a small dependency with no codec machinery attached. It does not fit a project that needs to decode compressed audio, since the library covers only the uncompressed WAVE format, and it does not fit a program that wants a high level audio API, since there is no mixing, resampling or device handling. Before adopting it, check the sample format you actually have, because the reader and the writer accept slightly different sets of chunk headers, and read the changelog since the crate is at version 3.5.1 and the default branch is named release rather than main.
Frequently asked questions
What audio formats can hound read and write?
Hound reads and writes the WAVE format for raw, uncompressed audio. On read it supports PCMWAVEFORMAT, WAVEFORMATEX and WAVEFORMATEXTENSIBLE headers, and on write PCMWAVEFORMAT and WAVEFORMATEXTENSIBLE, with integer PCM and IEEE float encodings at 8, 16, 24 and 32 bits for integer and 32 bits for float.
Why do I need to call finalize() on a hound WavWriter?
The file is finalized implicitly when the writer is dropped, and the README says to call writer.finalize() to observe errors. The finalisation step is where the header sizes are patched, so an error there is only reported if you ask for it explicitly.
Can I use hound in a GPLv2 licensed project?
Yes, by adding an exception to your copyright notice, and the README links to the GNU licence FAQ entry on incompatible libraries for the wording. Hound itself is Apache 2.0, usable in both free software and closed-source applications for commercial and non-commercial use.
Does hound depend on any other Rust crates?
It has no runtime dependencies. The only dev-dependency is cpal pinned to 0.2.12, because newer versions depend on the futures library, which the README notes is incompatible with the Rust version Hound guarantees to support.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/ruuda-hound)Was this useful?