wav

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 21, 2026 License: MIT Imports: 3 Imported by: 0

README

go-wav

CI Go Reference Go Version Sponsor

A pure-Go library for reading and writing WAV audio, including the 64-bit RF64 and BW64 extensions for files past 4 GiB (both are read, RF64 is written). No cgo, no runtime dependencies.

It is the WAV member of a family that also covers FLAC, Opus, AAC and M4A, and it presents the same API shape as its siblings, so a program that already speaks one of them speaks this one too.

Install

go get github.com/tphakala/go-wav

Requires Go 1.26 or newer.

Status

  • Containers: RIFF, RF64 (EBU Tech 3306) and BW64 (ITU-R BS.2088) are read; RIFF and RF64 are written. BW64 is read-only because the ADM metadata in its axml and chna chunks is what makes a file BW64 rather than RF64, and this library writes neither chunk, so the magic on its own would misrepresent the file.

  • Sample formats: integer PCM at 8, 16, 24 and 32 bits, and IEEE float at 32 and 64 bits. WAVE_FORMAT_EXTENSIBLE is read, and written automatically wherever the format requires it.

  • G.711 companding: WAVE_FORMAT_ALAW and WAVE_FORMAT_MULAW are decoded, from the bare format tag and from the extensible SubFormat GUID alike. A companded byte is not a sample on any linear scale, so it is always expanded to linear 16-bit PCM rather than handed back as stored; SourceFormat and SourceBitDepth report what the file holds. Neither law is written, because nothing here compands linear samples.

  • Sample rates and channels: 384 kHz and eight channels are ordinary, not special cases, and nothing in between is. The outer limits are the fmt chunk's: 65535 channels, from its 16-bit field, and a sample rate of 2147483647. That second number is not a field width, since the field is a 32-bit unsigned integer reaching 4294967295; it is this library's policy, so the rate stays a positive int on a 32-bit platform instead of wrapping negative. It applies on read and on write alike, so a file this library writes is one it can read back.

    The two outer limits are not reachable at the same time, because the fmt chunk also derives a 16-bit block alignment and a 32-bit byte rate from them: 65535 channels needs 8-bit samples and a rate no higher than 65535. The encoder reports which derived field a configuration overflows rather than writing a header it knows is wrong.

  • Validated bit-exactly in both directions against ffmpeg and sox, across every supported depth and format, 8 kHz to 384 kHz, and RF64. Both companding laws are checked against both tools over all 256 codes.

Not implemented: ADPCM and the other compressed format tags; and the metadata chunks (bext, LIST/INFO, cue , iXML, axml, chna). Unknown chunks are skipped cleanly on read, so files carrying them decode normally, their metadata simply is not exposed.

Usage

import wavpcm "github.com/tphakala/go-wav/pcm"
Encoding
cfg := wavpcm.Config{SampleRate: 48000, BitDepth: 16, Channels: 1}

// One shot, drawn from a pool and safe for concurrent use:
err := wavpcm.EncodeInterleaved(w, cfg, samples)

// Or streaming, accepting any chunk size:
e, err := wavpcm.NewEncoder(w, cfg)
_, err = io.Copy(e, src)
err = e.Close()

Config is a flat struct whose zero values are all documented, so the three required fields make a complete configuration. Float output is a field, not a different constructor:

cfg := wavpcm.Config{SampleRate: 96000, BitDepth: 32, Channels: 2,
    Format: wav.SampleFormatFloat}

A plain io.Writer is always accepted. When the sink also implements io.WriteSeeker the header is patched at Close, which is what allows a stream to become RF64 once it outgrows plain RIFF.

Decoding
d, err := wavpcm.NewDecoder(r)
info := d.Info()        // valid immediately
_, err = io.Copy(w, d)  // WriteTo drains the whole stream

A whole file already in memory needs no reader and no copy:

info, samples, err := wavpcm.DecodeInterleaved(b)

When the bytes handed back are the bytes as stored, samples aliases the audio inside b instead of being copied out of it, so writing through either one is visible through the other. When they are not, the returned buffer is freshly allocated and aliases nothing: that is the case under WithConvertTo, and also over an A-law or mu-law file with no option at all, since one of those is expanded whether or not a conversion was asked for. So the options alone do not tell you which you got; check SourceFormat on the returned StreamInfo before relying on the aliasing.

By default the decoder is a pass-through for everything but the two companding laws: Read yields the bytes as stored, so 24-bit audio stays packed in three bytes and nothing is widened behind your back. That also means the encoding varies with the file, and in particular that 8-bit data is unsigned while every wider integer depth is signed, because that is how WAV stores them. To normalise:

d, err := wavpcm.NewDecoder(r, wavpcm.WithConvertTo(16))

which converts every source, float and 8-bit included, to signed 16-bit.

Info() always describes what Read yields rather than what is on disk, so the two can never disagree; the stored encoding stays available as SourceBitDepth and SourceFormat.

Files past 4 GiB

A plain RIFF header stores sizes in 32-bit fields, so it cannot describe 4 GiB or more. RF64 and BW64 lift that limit with a ds64 chunk, and RF64 is the one the encoder writes. The policy is one config field, and the vocabulary matches ffmpeg's -rf64 flag:

cfg.RF64 = wavpcm.RF64Auto   // the default

RF64Auto writes an ordinary RIFF header with a small JUNK chunk reserved after it, and if the stream outgrows 4 GiB that header is rewritten in place as RF64. Small files stay plain RIFF and readable by anything; large ones become valid RF64, with no second pass over the audio.

The rescue needs a seekable sink, since the ds64 goes at the front. When the sink cannot seek, declare the length up front instead:

cfg.TotalFrames = frames // the header is then correct from the first byte

Declaring it is a promise, and Close reports an error if you write a different number of frames.

Where no correct size can be produced, the encoder returns wav.ErrTooLarge. It never writes a size field it knows to be wrong, which is the failure mode this library was written to eliminate.

Sniffing
if wav.Sniff(header) { /* RIFF, RF64 or BW64 */ }

Twelve bytes are enough, and unlike a hand-rolled RIFF check it recognises RF64 and BW64.

Errors

Sentinels only, testable with errors.Is: ErrNotRIFF, ErrCorruptStream, ErrUnsupported, ErrEncoderClosed, ErrTooLarge and ErrSeekUnsupported. ErrNotRIFF means the input is not a WAV file; ErrCorruptStream means it is one and it is broken.

The reader tolerates what real files get wrong: a missing pad byte after an odd chunk, a data size of zero or 0xFFFFFFFF, a declared size running past the end of the file, trailing bytes after the audio, and chunks in any order. It does not tolerate ambiguity about the sample format: a stream it cannot decode is reported rather than guessed at.

Because it tolerates those, StreamInfo.TotalFrames is a claim the header makes and never a count of bytes present: a truncated or interrupted file reports the length it was given. StreamInfo.DataSizeKnown says whether that claim came with a boundary the decoder will bound reads and seeks by, which is what decides whether SeekToFrame clamps; it does not say the audio is all there. Reading until io.EOF is the only way to learn that.

License

MIT. See LICENSE.

Sponsor

If this is useful to you, sponsorship is welcome.

Documentation

Overview

Package wav provides the shared types and error sentinels for reading and writing WAV audio. It reads the 64-bit RF64 and BW64 extensions and writes RF64.

The streaming API lives in the pcm subpackage, so that this package can hold the types both layers need without an import cycle. Callers who want to encode or decode audio want github.com/tphakala/go-wav/pcm; this package is what that one returns and the errors it reports.

Containers

Three container flavours share one chunk layout. Plain RIFF stores sizes in 32-bit fields and therefore cannot describe a file of 4 GiB or more. RF64 (EBU Tech 3306) and BW64 (ITU-R BS.2088) lift that limit with a sentinel in the 32-bit fields and the real 64-bit sizes carried in a ds64 chunk. BW64 is structurally identical to RF64 and differs only in its magic and in the metadata chunks it is expected to carry, so this package reads both and reports which one it saw in StreamInfo.Container.

Writing is RIFF or RF64. BW64 is read only, because the ADM metadata in its axml and chna chunks is what makes a file BW64 rather than RF64 and this library writes no such chunk; see ContainerBW64.

Sample formats

Samples are always little-endian. Integer PCM is unsigned at 8 bits with a midpoint of 128, and signed two's complement at 16, 24 and 32 bits; 24-bit samples are packed into three bytes with no padding. Float samples are IEEE 754 at 32 or 64 bits with a nominal full scale of [-1, +1].

The two G.711 companding laws, A-law and mu-law, are decoded. A companded byte is not a sample on any linear scale, so it is always expanded to linear 16-bit PCM on the way out rather than handed back as stored: StreamInfo reports the expansion in Format and BitDepth and the stored encoding in SourceFormat and SourceBitDepth. Neither law is written, because nothing here compands linear samples; see SampleFormatALaw.

ADPCM and the other compressed WAVE format tags are not supported. A file using one is reported as ErrUnsupported rather than decoded incorrectly.

Index

Constants

View Source
const Version = "0.3.0"

Version is the module version. The release workflow checks it against the tag it is building and refuses to publish a release where the two disagree.

Variables

View Source
var (
	// ErrNotRIFF reports a stream whose first twelve bytes are not a RIFF,
	// RF64 or BW64 WAVE header. It means "this is not a WAV file at all",
	// as distinct from ErrCorruptStream, which means "this is a WAV file and
	// it is broken".
	ErrNotRIFF = errors.New("go-wav: not a RIFF, RF64 or BW64 stream")

	// ErrCorruptStream reports a malformed stream: a chunk header that runs
	// past the end of its parent, a fmt chunk shorter than 16 bytes, a
	// missing fmt or data chunk, or an RF64 stream with no ds64 chunk. It
	// also covers a fmt chunk that parses but describes no stream that could
	// exist, which is zero channels, a zero sample rate, or a sample rate too
	// large to hold as a positive int on every platform. It is reserved for
	// damage the reader cannot work around; the tolerated real-world
	// deviations documented on the decoder do not produce it.
	ErrCorruptStream = errors.New("go-wav: corrupt stream")

	// ErrUnsupported reports a well-formed stream this package will not
	// decode, or an output this package will not write: a compressed format
	// tag such as ADPCM, a bit depth outside the supported set, a float
	// stream at a width other than 32 or 64 bits, an A-law or mu-law stream
	// at a width other than the 8 bits G.711 defines, or an encoding that is
	// read but never written, which is the BW64 container and the two
	// companding laws. The text is deliberately container-neutral, because
	// the sentinel covers more than the sample format.
	ErrUnsupported = errors.New("go-wav: unsupported format")

	// ErrEncoderClosed is returned by Encoder.Write and Encoder.Close after
	// Close has been called, and by methods on an encoder left uninitialised
	// by a zero value or a failed Reset.
	ErrEncoderClosed = errors.New("go-wav: encoder is closed")

	// ErrTooLarge reports a stream that has outgrown the 4 GiB limit of the
	// 32-bit RIFF size fields at a point where no RF64 upgrade is possible:
	// the policy forbids it, or the sink cannot seek and the frame count was
	// not declared up front. The encoder returns this instead of writing a
	// size field it knows to be wrong.
	ErrTooLarge = errors.New("go-wav: stream exceeds the 4 GiB RIFF limit and RF64 is unavailable")

	// ErrSeekUnsupported reports a seek requested on a source that does not
	// implement io.Seeker.
	ErrSeekUnsupported = errors.New("go-wav: seek unsupported (source is not an io.Seeker)")
)

Sentinel errors returned by the wav and pcm packages, testable with errors.Is. They live in the root package so that both layers can report the same values, mirroring go-flac's layout.

Functions

func Sniff

func Sniff(b []byte) bool

Sniff reports whether b begins with a RIFF, RF64 or BW64 WAVE header. It reads at most the first twelve bytes, needs no allocation, and returns false rather than panicking on a short slice.

It exists so that callers dispatching on file type do not have to hand-roll a magic check that forgets RF64 and BW64.

Types

type Container

type Container int

Container identifies the RIFF flavour of a stream.

const (
	// ContainerRIFF is a plain RIFF WAVE stream with 32-bit size fields. It
	// cannot describe 4 GiB or more of audio.
	ContainerRIFF Container = iota
	// ContainerRF64 is an RF64 stream as defined by EBU Tech 3306: the magic
	// is "RF64", the 32-bit sizes hold 0xFFFFFFFF, and a ds64 chunk carries
	// the real 64-bit values.
	ContainerRF64
	// ContainerBW64 is a BW64 stream as defined by ITU-R BS.2088. It is
	// structurally identical to RF64 and differs only in its magic.
	//
	// It is read but never written, so it is reported in StreamInfo of a
	// decoder and never of an encoder. A BW64 file exists to carry ADM
	// metadata in its axml and chna chunks, and this library carries
	// neither, so writing the magic alone would hand back an RF64 file under
	// a name that promises metadata it does not hold: strict tooling may
	// reject it, and a caller who asked for BW64 would reasonably expect the
	// metadata that is the point of the format. An encoder therefore emits
	// RIFF or RF64, chosen by pcm.RF64Mode.
	ContainerBW64
)

func (Container) Sized64

func (c Container) Sized64() bool

Sized64 reports whether the container carries 64-bit sizes in a ds64 chunk.

func (Container) String

func (c Container) String() string

String returns the four-character magic of the container, or the string "unknown" for a value outside the declared set.

type SampleFormat

type SampleFormat int

SampleFormat identifies how samples in the data chunk are encoded.

const (
	// SampleFormatPCM is integer PCM: unsigned with a midpoint of 128 at 8
	// bits, signed two's complement at 16, 24 and 32 bits.
	SampleFormatPCM SampleFormat = iota
	// SampleFormatFloat is IEEE 754 floating point at 32 or 64 bits, with a
	// nominal full scale of [-1, +1].
	SampleFormatFloat
	// SampleFormatALaw is 8-bit G.711 A-law, the companding law of European
	// telephony and of WAVE_FORMAT_ALAW. Each byte carries a sign, a segment
	// and a mantissa that expand to 13 bits of resolution.
	SampleFormatALaw
	// SampleFormatMuLaw is 8-bit G.711 mu-law, the companding law of North
	// American and Japanese telephony and of WAVE_FORMAT_MULAW. Each byte
	// expands to 14 bits of resolution.
	SampleFormatMuLaw
)

func (SampleFormat) Companded added in v0.2.0

func (f SampleFormat) Companded() bool

Companded reports whether the format is one of the G.711 companding laws.

A companded byte is not a sample on any linear scale, so it can only ever describe what a file stores, never what a decoder hands back: a companded stream is always expanded to linear 16-bit PCM on the way out. It therefore appears in StreamInfo.SourceFormat and never in StreamInfo.Format, and this predicate exists so that callers branching on that distinction do not have to enumerate the laws themselves.

func (SampleFormat) String

func (f SampleFormat) String() string

String returns a short lower-case name for the sample format: "pcm" for integer PCM, "float" for IEEE 754 floating point, "a-law" and "mu-law" for the two G.711 companding laws, and "unknown" for a value outside the declared set.

type StreamInfo

type StreamInfo struct {
	// SampleRate is the number of samples per second per channel.
	//
	// Read back from a decoder that opened successfully it is always positive,
	// and an encoder fills it from a Config that must already be positive.
	//
	// The fmt chunk stores the rate in 32 bits, so a file may declare one up
	// to 4294967295. An int holds every such value on a 64-bit platform and
	// only those up to math.MaxInt32 on a 32-bit one, where the rest wrap
	// negative. The reader therefore refuses a declaration above math.MaxInt32
	// on every platform, rather than accepting a value whose sign would depend
	// on the machine, and the encoder refuses the same range so this package
	// never writes a rate it will not read. The ceiling is that representation
	// limit rather than a judgement about what a plausible rate is: it sits
	// far above anything recording or radio-capture hardware produces.
	//
	// The promise is only as good as its source. A Decoder whose Reset failed
	// is invalidated, so it reports the zero value here rather than what it
	// held before, and a StreamInfo a caller builds by hand can hold anything
	// at all, which is why Duration guards the field rather than trusting it.
	//
	// It remains a number the file declared. Nothing checks it against the
	// audio, so a rate that passes is still a claim, and code sizing a buffer
	// from it is sizing a buffer from something a file said.
	SampleRate int

	// Channels is the interleaved channel count.
	Channels int

	// BitDepth is the storage width in bits of one sample as the caller sees
	// it: 8, 16, 24 or 32 for integer PCM, 32 or 64 for float. It describes
	// the bytes Decoder.Read yields, so under a conversion option it is the
	// converted width, not the width stored in the file. SourceBitDepth
	// reports the latter.
	//
	// A companded source is always converted, so an A-law or mu-law stream
	// reports the 16 bits its codes expand to here and the 8 bits it stores
	// in SourceBitDepth.
	//
	// It is the container width, which under WAVE_FORMAT_EXTENSIBLE may
	// exceed the meaningful width reported by ValidBits.
	BitDepth int

	// SourceBitDepth is the storage width in bits of one sample as it is
	// encoded in the file. It differs from BitDepth only when the decoder
	// was asked to convert; otherwise the two are equal.
	SourceBitDepth int

	// ValidBits is the number of meaningful bits per sample declared by an
	// extensible fmt chunk, for sources such as 20-bit audio stored in a
	// 24-bit container. It is 0 when the stream did not declare one, in
	// which case every bit of BitDepth is meaningful.
	ValidBits int

	// Format is the sample encoding as the caller sees it. Like BitDepth it
	// describes the bytes Decoder.Read yields, so a float file being
	// converted to integer reports SampleFormatPCM here and
	// SampleFormatFloat in SourceFormat.
	//
	// It is never a companded format, because a companded stream is always
	// expanded; see [SampleFormat.Companded].
	Format SampleFormat

	// SourceFormat is the sample encoding as it appears in the file. It
	// differs from Format only when the decoder was asked to convert, or
	// when the source is companded and was therefore converted without being
	// asked; otherwise the two are equal.
	SourceFormat SampleFormat

	// Container is the RIFF flavour the stream was read from or written as.
	Container Container

	// Extensible reports whether the fmt chunk used WAVE_FORMAT_EXTENSIBLE
	// rather than a bare format tag.
	Extensible bool

	// ChannelMask is the dwChannelMask speaker assignment from an extensible
	// fmt chunk. It is 0 when the stream did not declare one.
	ChannelMask uint32

	// TotalFrames is the number of inter-channel frames in the stream.
	//
	// Read back from a decoder it is always a claim, never a measurement.
	// When the data chunk size is known the count is derived from that size;
	// when the size is absent or unreadable it comes instead from a ds64
	// sampleCount or a fact chunk. Both are numbers a writer stamped, and
	// neither is checked against the bytes that are actually there, because
	// checking would mean reading the stream to its end before answering. A
	// recording cut short by a crash routinely declares more than it holds,
	// and so does a file truncated after it was written: both report the
	// count their header still claims.
	//
	// So a caller must not treat this as the end of the audio. Reading until
	// io.EOF is the only way to learn how much a stream really carries, and
	// [StreamInfo.DataSizeKnown] reports whether the count came with a
	// boundary the decoder will bound reads and seeks by, which is a
	// different question from whether the count is right.
	//
	// The reader will not repeat a declaration it can see is impossible. A
	// declared count is kept only if the audio it claims stays under the
	// ceiling the reader is willing to believe a declaration up to, which is
	// far above any real recording and leaves the count small enough that
	// int64(TotalFrames) stays non-negative. That ceiling is a policy of this
	// reader, not a limit of the format: RF64 can describe more than the
	// reader will accept on a header's word alone. A count that fails is
	// reported as unknown instead.
	//
	// Read back from a decoder it is 0 whenever a frame count is unavailable
	// or genuinely zero: no source offered one, the only count on offer was
	// not credible, the decoder was told to ignore the declared length, or the
	// data chunk holds less than one whole frame. Only the last of those says
	// anything about how much audio is present, so a caller that needs the
	// real end of the audio reads until io.EOF rather than inferring it from
	// this field. An encoder fills the same struct from what it has written,
	// where 0 does mean no frames yet.
	TotalFrames uint64

	// DataSizeKnown reports whether the stream declared a data chunk size the
	// decoder will bound reads and seeks by.
	//
	// It is the one fact that predicts how a decoder behaves at the end of a
	// stream, and it is not observable from TotalFrames, because a count can
	// be present either way: with a size, it is derived from that size;
	// without one, it comes from a ds64 sampleCount or a fact chunk, which
	// the reader consults precisely because the size is missing.
	//
	// When it is true, Decoder.Read stops at the declared boundary and
	// Decoder.SeekToFrame clamps to it, so a seek returning a lower frame
	// than the one requested means the stream ran out. When it is false there
	// is no boundary: reads run to the end of the source, and a seek past the
	// audio is performed as asked and reports the frame requested. It is
	// It is false whenever the reader found no size it could use: a data
	// chunk size of zero or the RF64 sentinel, a ds64 that was never stamped
	// or that declares an implausible length, and, whatever the header said,
	// whenever pcm.WithIgnoreLength is in force.
	//
	// It says nothing about whether TotalFrames is correct. A declared size
	// is a claim like any other, so a true here bounds the decoder by a
	// number that may still overstate the audio; it means the decoder has a
	// boundary to honour, not that the boundary is honest. An encoder fills
	// the same struct and always reports true, since it is writing the data
	// chunk whose size it is stating.
	DataSizeKnown bool
}

StreamInfo describes a WAVE stream. A Decoder reports the properties of the stream it is reading; an Encoder reports the properties of the stream it is writing. It mirrors flac.StreamInfo in the sibling go-flac library.

func (StreamInfo) BytesPerFrame

func (si StreamInfo) BytesPerFrame() int

BytesPerFrame is the storage width of one inter-channel frame in bytes. It is the value a WAVE fmt chunk records as nBlockAlign.

func (StreamInfo) BytesPerSample

func (si StreamInfo) BytesPerSample() int

BytesPerSample is the storage width of a single-channel sample in bytes.

func (StreamInfo) Duration

func (si StreamInfo) Duration() time.Duration

Duration is the length of the stream, or 0 when that length cannot be stated as a time.Duration. A caller that sees 0 knows the length is unavailable; an unchecked computation would instead hand back a wrapped, meaningless number. Such a number is as readily positive as negative, and the positive ones are the more dangerous, because nothing about a plausible-looking length invites a second look.

A TotalFrames of 0 means the frame count is unknown, and a SampleRate of 0 or less cannot divide anything. The remaining zero cases are arithmetic ceilings. A StreamInfo is an ordinary exported struct, so the values that reach them need not have come from a file: the reader bounds a declared count before publishing it, which puts the conversion guard below out of reach of any parsed stream, but a caller can build a StreamInfo by hand holding anything a uint64 does. The two later ceilings stay reachable either way, since the reader's bound is far above the point where a length stops fitting in a time.Duration. Each step therefore rejects what it cannot carry. The conversion to int64 rejects a count above math.MaxInt64. The whole-seconds term rejects a count whose whole seconds alone would pass math.MaxInt64 nanoseconds, which is the ceiling time.Duration itself has, about 292 years. The final addition rejects the few counts that clear both and overflow only once the sub-second remainder is added. At 48 kHz the largest frame count that survives all three is 442721857769029.

The remainder term carries a bound of its own: rem * nsPerSecond overflows for a sample rate above math.MaxInt64/nsPerSecond, roughly 9.22 GHz, so such a rate is rejected outright. This is the one bound that gives up a length it could in principle have represented, and it is deliberate. No file can reach it, because a fmt chunk stores the sample rate in 32 bits, so the only way in is a hand-built StreamInfo, and rejecting those costs less than the wider arithmetic serving them would take.

The whole-seconds-plus-remainder split is what buys the range in between. A naive frames * time.Second wraps once the frame count passes math.MaxInt64/nsPerSecond, about 53 hours of audio at 48 kHz; splitting the multiplication off the whole seconds pushes that out to the full 292 years.

Directories

Path Synopsis
internal
riff
Package riff implements the container half of a WAV file: the RIFF chunk structure, the fmt chunk, and the ds64 mechanics that lift RF64 and BW64 past the 4 GiB ceiling of a plain RIFF stream.
Package riff implements the container half of a WAV file: the RIFF chunk structure, the fmt chunk, and the ds64 mechanics that lift RF64 and BW64 past the 4 GiB ceiling of a plain RIFF stream.
sample
Package sample converts WAVE sample data between the encodings this library supports.
Package sample converts WAVE sample data between the encodings this library supports.
Package pcm reads and writes WAV audio as interleaved little-endian PCM.
Package pcm reads and writes WAV audio as interleaved little-endian PCM.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL