README
¶
riff
RIFF file decoder and encoder for Go.
The riff package reads and writes files in the Resource Interchange File
Format (RIFF), the container behind WAVE, AVI, and RMID files. It decodes a
file into a list of chunks you can inspect, edit, and encode back, and lets
you register your own decoders for chunks it does not know.
Features
- Built-in chunk decoders:
fmt— audio formatdata— audio samplesLISTwithINFO(tags) oradtl(lablandltxtlabels)smpl— sampler loops
- Unknown chunks kept: chunks without a decoder are read as raw chunks
(
ChunkRAWC); withWithLoadDataa decode, edit, encode round trip preserves them. - Metadata-only mode: by default chunk bodies such as audio samples are skipped, not loaded.
- Custom decoders: implement the
Chunkinterface and register it in aRegistry. - Reusable instances: one
RIFFvalue decodes many files, reusing its chunk decoders.
Prerequisites
Go 1.26 or later.
Installation
go get github.com/ctx42/riff
Usage
Import the package from github.com/ctx42/riff/pkg/riff. More runnable
examples are in examples_test.go.
Print the audio format
package main
import (
"fmt"
"log"
"os"
"github.com/ctx42/riff/pkg/riff"
)
func main() {
src, err := os.Open("sample.wav")
if err != nil {
log.Fatal(err)
}
defer src.Close()
// Decode chunks, reading only the metadata (the default).
rif := riff.New()
if _, err = rif.ReadFrom(src); err != nil {
log.Fatal(err)
}
chi := rif.Chunks().First(riff.IDfmt)
if chi == nil {
log.Fatal("no fmt chunk")
}
ch := chi.(*riff.ChunkFMT)
fmt.Printf("Compression Code: %#04x\n", ch.CompCode)
fmt.Printf("Channel Count: %d\n", ch.ChannelCnt)
fmt.Printf("Sample Rate: %d\n", ch.SampleRate)
fmt.Printf("Average Byte Rate: %d\n", ch.AvgByteRate)
fmt.Printf("Block Align: %d\n", ch.BlockAlign)
fmt.Printf("Bits Per Sample: %d\n", ch.BitsPerSample)
}
Edit and save a file
Decode with WithLoadData so the chunk bodies are loaded and can be written
back.
package main
import (
"log"
"os"
"github.com/ctx42/riff/pkg/riff"
)
func main() {
src, err := os.Open("sample.wav")
if err != nil {
log.Fatal(err)
}
defer src.Close()
rif := riff.New(riff.WithLoadData())
if _, err = rif.ReadFrom(src); err != nil {
log.Fatal(err)
}
chi := rif.Chunks().First(riff.IDfmt)
if chi == nil {
log.Fatal("no fmt chunk")
}
fch := chi.(*riff.ChunkFMT)
fch.SampleRate = 44100
fch.AvgByteRate = fch.SampleRate * uint32(fch.BlockAlign)
dst, err := os.Create("edited.wav")
if err != nil {
log.Fatal(err)
}
if _, err = rif.WriteTo(dst); err != nil {
log.Fatal(err)
}
if err = dst.Close(); err != nil {
log.Fatal(err)
}
}
Set a tag
Tags live in INFO sub-chunks of a LIST chunk. This sets the title,
replacing an existing one, and adds an INFO list when the file has none.
package main
import (
"log"
"os"
"github.com/ctx42/riff/pkg/riff"
)
func main() {
src, err := os.Open("sample.wav")
if err != nil {
log.Fatal(err)
}
defer src.Close()
rif := riff.New(riff.WithLoadData())
if _, err = rif.ReadFrom(src); err != nil {
log.Fatal(err)
}
title := riff.INFO(riff.LabINAM)
if err = title.SetText([]byte("Kick")); err != nil {
log.Fatal(err)
}
// Find the INFO list, or add one.
chs := rif.Chunks()
var info *riff.ChunkLIST
for _, ch := range chs {
if lst, ok := ch.(*riff.ChunkLIST); ok && lst.Type() == riff.IDINFO {
info = lst
break
}
}
if info == nil {
info = riff.LIST(nil)
info.ListType = riff.IDINFO
chs = append(chs, info)
}
info.Modify(append(info.Chunks().Remove(riff.LabINAM), title))
rif.Modify(chs)
dst, err := os.Create("tagged.wav")
if err != nil {
log.Fatal(err)
}
if _, err = rif.WriteTo(dst); err != nil {
log.Fatal(err)
}
if err = dst.Close(); err != nil {
log.Fatal(err)
}
}
Other chunks have setters too: ChunkLABL.SetLabel, ChunkLTXT.SetText,
ChunkSMPL.SetSamplerData, ChunkRAWC.SetBody, ChunkDATA.SetData, and
ChunkFMT.SetExtra.
Decode only selected chunks
Build a Registry with the decoders you need and pass it to Bare. Chunks
without a registered decoder are read as ChunkRAWC.
package main
import (
"fmt"
"log"
"os"
"github.com/ctx42/riff/pkg/riff"
)
func main() {
src, err := os.Open("sample.wav")
if err != nil {
log.Fatal(err)
}
defer src.Close()
reg := riff.NewRegistry(riff.RAWCMake())
reg.Register(riff.IDfmt, riff.FMTMake())
rif := riff.Bare(reg)
if _, err = rif.ReadFrom(src); err != nil {
log.Fatal(err)
}
for _, ch := range rif.Chunks() {
fmt.Printf("%s %T\n", riff.Uint32(ch.ID()), ch)
}
}
Options
Pass options to New, Bare, or a chunk maker such as RAWCMake. With a
registry, Bare applies them to the RIFF chunk only; the registry's decoders
take their options from their makers. Bare(nil, ...) also passes them to its
raw chunk decoder.
| Option | Effect |
|---|---|
WithLoadData() |
Load chunk bodies; required to write a file back. |
WithSizeLimit(n) |
ErrTooLarge for any chunk over n bytes, RIFF too. |
WithIgnoreSize() |
Read past declared sizes; skip the RIFF size check. |
FAQ
Can I reuse a RIFF instance for multiple files?
Yes. ReadFrom resets the instance before decoding, so the chunks of the
previous file are no longer valid after the next call.
Is the library safe for concurrent use?
No. Use one RIFF instance per goroutine or synchronize access yourself.
How do I decode a chunk the package does not support?
Implement the Chunk interface and register a Maker for
its ID, as in Decode only selected chunks.
What happens to unsupported chunks?
They are decoded and encoded by ChunkRAWC. Decode with WithLoadData to
keep their bodies so a decode, edit, encode round trip preserves them.
How do I find where a file is broken?
When a chunk fails to decode, RIFF.ReadFrom returns a *DecodeError. Its
ChunkOffset is where the chunk starts and Offset is where decoding
stopped, both counted from the start of the input. Use errors.As to read
them, or errors.Is to test for the cause, such as io.ErrUnexpectedEOF or
riff.ErrPadByte.
How strict is the decoder?
It follows the RIFF specification (doc/riffmci.pdf) and rejects input that breaks it, such as a pad byte that is not zero. It tolerates these deviations, which some encoders write:
- Missing final pad byte: odd-sized chunk data is followed by a zero pad byte. When the input ends right after the data of the last chunk, also inside a LIST, the missing pad byte is accepted. A pad byte missing anywhere else is an error.
- Odd RIFF size: the RIFF chunk holds whole, padded chunks, so its size is even. Encoders that leave the final pad byte out of it write an odd size, which is rounded up.
WriteTo always writes a conforming file, with every pad byte and an even
RIFF size.
License
riff is released under the MIT License.
The RIFF specification in doc/riffmci.pdf, Multimedia Programming Interface and Data Specifications 1.0 (August 1991), is a joint publication of IBM Corporation and Microsoft Corporation, who hold its copyright. It is included for reference and is not covered by the MIT License.
Some test files in pkg/riff/testdata come from the go-audio projects and are distributed under the Apache License 2.0; their README lists their sources and changes.