WaxLabel is a pure-Go library and command-line tool for reading and writing audio-file metadata: tags, embedded pictures, and chapters where the format supports them. It is preservation-first: edits are planned against the parsed native structure, metadata is rewritten only where needed, and audio bytes are copied rather than transcoded.
The public API lives in github.com/colespringer/waxlabel and
github.com/colespringer/waxlabel/tag. Codec packages are internal implementation
details.
WaxLabel reads and writes FLAC, Ogg Vorbis, Ogg Opus, MP3, WAV, MP4/M4A, raw AAC/ADTS, Matroska/WebM, and AIFF/AIFF-C.
Library:
go get github.com/colespringer/waxlabelCLI:
go install github.com/colespringer/waxlabel/cmd/waxlabel@latestWaxLabel requires Go 1.26 or newer. The library packages use only the standard library; the CLI uses Cobra.
package main
import (
"context"
"fmt"
"log"
waxlabel "github.com/colespringer/waxlabel"
"github.com/colespringer/waxlabel/tag"
)
func main() {
ctx := context.Background()
doc, err := waxlabel.ParseFile(ctx, "track.flac")
if err != nil {
log.Fatal(err)
}
fmt.Println(doc.Fields().Title)
plan, err := doc.Edit().
Set(tag.Title, "New Title").
Set(tag.Artist, "Lead", "Featured").
Clear(tag.Encoder).
Prepare()
if err != nil {
log.Fatal(err)
}
fmt.Println(plan)
_, result, err := plan.Execute(ctx, waxlabel.SaveBack())
if err != nil {
log.Fatal(err)
}
fmt.Println("committed:", result.Committed)
}Parse, ParseFile, and OpenSource return an immutable Document. A document
does not hold an open file descriptor, and accessors return detached data. Editing
starts with Document.Edit(), resolves through Editor.Prepare(), and writes only
when the resulting Plan is executed.
Write destinations:
SaveBack()atomically rewrites the parsed file in place. A no-op writes nothing.SaveAsFile(path)writes a complete new file.WriteTo(w, source)streams a complete output to anio.Writer.
waxlabel dump track.flac
waxlabel plan track.flac --set TITLE="New Title"
waxlabel set track.flac --set TITLE="New Title" --add ARTIST=Featured
waxlabel lint track.flac --fix
waxlabel verify track.flac
waxlabel caps --format flac
waxlabel keys
waxlabel copy source.flac dest.m4a
waxlabel diff before.flac after.flacCommands:
| Command | Purpose |
|---|---|
dump <file>... |
Show tags, audio properties, pictures, chapters, and parse warnings. --native also shows native blocks and source families. |
plan <file>... |
Preview an edit without writing. |
set <file>... |
Apply edits and save. Use -o for a new output file. |
lint <file>... |
Report metadata issues. --fix applies only safe fixes, such as clearing encoder noise or stripping legacy containers. |
verify <file>... |
Print tag-independent audio-essence digests. --whole-file also hashes every byte. |
caps <file>... or caps --format <name> |
Show what a file or format can store and edit. |
keys |
List the canonical tag vocabulary and cardinality. |
copy <source> <dest> |
Overlay source metadata onto the destination, reporting values that carry, downgrade, or drop. |
diff <a> <b> |
Compare canonical tags, pictures, and chapters. |
Common edit flags:
--set KEY=VALUEreplaces a value.--add KEY=VALUEappends a value.--clear KEYremoves a key.--strip-encoderremoves inherited encoder stamps where the format allows it.--add-cover FILEand--add-picture ROLE=FILEembed pictures.--remove-picture SELECTORand--remove-picturesremove pictures.--add-chapter TIMESTAMP=TITLEand--clear-chaptersedit chapters.--preset preserve|compatible|minimal,--legacy preserve|strip,--padding N, and--no-paddingshape the write.
The read commands accept a single - for standard input; set - also works when
paired with -o. dump, verify, lint, plan, and set can walk directories
with --recursive; walked files are selected by extension, while direct file
arguments are content-sniffed.
All data commands accept --json. Commands that process many inputs return an
array, one element per input. Single-result commands such as diff, copy,
caps --format, keys, and version return one object.
Exit code summary:
0: success, clean lint, or identical diff.1: generic error, lint warnings found, or files differ.2: usage error.3: unsupported format or unsupported metadata operation.4: invalid or contradictory data.5: source changed since parse.6: I/O or not-found error.130: canceled or timed out.
For multi-file commands, a more severe file error determines the process exit code.
Canonical tags. WaxLabel projects native metadata into a format-neutral
tag.TagSet. Known keys live in tag.KnownKeys(), but unknown uppercase keys are
preserved as custom fields when a format can carry them.
Presence-aware edits. WaxLabel distinguishes absent keys, present empty-string values, and keys present with no values. Formats cannot store every distinction, so the plan reports what will actually be written after codec projection.
Planning before writing. Prepare() builds a Plan and a WriteReport from
the same state used by Execute(). The preview is not a separate guess.
Preservation. Legacy or secondary metadata containers are preserved and warned
by default. Use WithLegacyPolicy(LegacyStrip) or the CLI's --legacy strip when
you want them removed.
Terminal-safe text. Human text renderers sanitize untrusted tag values, paths, and warning strings so control bytes cannot inject terminal escapes. JSON output uses the exact machine-readable values.
Audio identity. Document.HashAudioEssence hashes encoded audio plus
decoder-critical configuration, independent of tags. Document.HashFile hashes the
entire file.
| Format | Metadata | Notes |
|---|---|---|
| FLAC | read/write | Vorbis comments and FLAC pictures; padding is fully controllable. |
| Ogg Vorbis / Opus | read/write | Vorbis comments and METADATA_BLOCK_PICTURE; audio packet payloads are preserved. |
| MP3 | read/write | ID3v2 is writable; ID3v1 and APEv2 are surfaced as legacy families. |
| WAV | read/write | RIFF LIST/INFO plus embedded id3 ; chunks are preserved. |
| MP4 / M4A / M4B | read/write | iTunes ilst, cover art, Nero chapters, and QuickTime chapter text tracks. Fragmented MP4 is rejected. |
| Matroska / WebM | read/write | Scoped SimpleTags, segment title, attachments, and default-edition chapters. WebM cannot write cover attachments. |
| AAC (ADTS) | read/write | Front ID3v2 tag plus ADTS frames. |
| AIFF / AIFF-C | read/write | Native text chunks plus embedded ID3 ; chunks are preserved. |
The capability table below is generated from the same codec capability model used
by waxlabel caps.
| Format | Pictures | Chapters |
|---|---|---|
| AAC (ADTS) | read full, write full · APIC frame | read none, write none |
| AIFF | read full, write full · APIC (ID3 chunk) | read none, write none |
| FLAC | read full, write full · FLAC PICTURE block | read none, write none · CUESHEET preserved |
| MP3 | read full, write full · APIC frame | read none, write none · CHAP preserved |
| MP4 | read full, write full · covr atom (JPEG/PNG/BMP) | read full, write full · Nero chpl and a QuickTime chapter text track |
| Matroska | read full, write full · AttachedFile (image attachment) | read full, write full · Chapters > EditionEntry > ChapterAtom (default edition) |
| Ogg Opus | read full, write full · METADATA_BLOCK_PICTURE | read none, write none |
| Ogg Vorbis | read full, write full · METADATA_BLOCK_PICTURE | read none, write none |
| WAV | read full, write full · APIC (id3 chunk) | read none, write none · cue/adtl preserved |
Input is treated as untrusted. Parsers use bounded allocation and recursion limits, fuzz tests cover arbitrary input, and human output sanitizes terminal-control bytes.
Save-back writes use a temp file in the target directory, fsync it, rename it into
place, and fsync the directory. If the source file changed since parse,
SaveBack() refuses with waxerr.ErrSourceChanged instead of overwriting newer
bytes.
Atomic rename saves have normal filesystem consequences:
- Editing through a symlink rewrites the symlink target and leaves the link in place.
- Other hard links keep pointing at the old inode.
- A read-only file can still be replaced when its directory is writable; the original mode is preserved on the replacement.
MIT.
Mutagen, TagLib, bogem/id3v2, sentriz/go-taglib, and libogg were direct influences on WaxLabel's design and test cross-checks. WaxLabel's implementation follows public specifications and does not copy their code.