wav

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 20, 2026 License: MIT Imports: 2 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. 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), read and written.
  • 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.
  • Sample rates and channels: unrestricted. 384 kHz and eight channels are ordinary, not special cases.
  • Validated bit-exactly in both directions against ffmpeg and sox, across every supported depth and format, 8 kHz to 384 kHz, and RF64.

Not implemented: A-law, mu-law, 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

By default the decoder is a pass-through: 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. 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.

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, including the 64-bit RF64 and BW64 extensions.

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 by writing a sentinel into the 32-bit fields and carrying the real 64-bit sizes 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.

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].

A-law, mu-law, 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.1.0"

Version is the module version.

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 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: a compressed or companded format tag such as A-law, mu-law or
	// ADPCM, a bit depth outside the supported set, or a float stream at a
	// width other than 32 or 64 bits.
	ErrUnsupported = errors.New("go-wav: unsupported sample 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.
	ContainerBW64
)

func (Container) Sized64

func (c Container) Sized64() bool

Sized 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.

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
)

func (SampleFormat) String

func (f SampleFormat) String() string

String returns a short name for the sample format.

type StreamInfo

type StreamInfo struct {
	// SampleRate is the number of samples per second per channel.
	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.
	//
	// 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.
	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;
	// 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. It is
	// 0 when the count is not known, which happens for a stream whose data
	// chunk size was absent or unreadable.
	TotalFrames uint64
}

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. It is 0 when TotalFrames or SampleRate is 0, so a stream of unknown length reports 0 rather than a wrong answer.

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