Help us shape the future of Go! Take the 2026 Go Developer Survey

riff

module
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Oct 18, 2026 License: MIT

README

Go Go Reference

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 format
    • data — audio samples
    • LIST with INFO (tags) or adtl (labl and ltxt labels)
    • smpl — sampler loops
  • Unknown chunks kept: chunks without a decoder are read as raw chunks (ChunkRAWC); with WithLoadData a 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 Chunk interface and register it in a Registry.
  • Reusable instances: one RIFF value 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.

Directories

Path Synopsis
pkg
riff
Package riff provides low level tools for working with files in Resource Interchange File Format (RIFF).
Package riff provides low level tools for working with files in Resource Interchange File Format (RIFF).

Jump to

Keyboard shortcuts

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