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 ¶
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 ¶
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 ¶
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 )
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. |