23 KiB
Technology Stack: OGG Vorbis + WAV Tag Writing
Project: YellowJacket v1.2.1 Format Parity Researched: 2026-03-18 Scope: Stack additions/changes needed ONLY for OGG Vorbis and WAV tag writing
Context: MP3 writing (bogem/id3v2) and FLAC writing (go-flac ecosystem) are already implemented and validated in v1.2. This document focuses exclusively on what's needed for OGG Vorbis and WAV tag writing.
OGG Vorbis Tag Writing
The Problem
OGG Vorbis stores metadata as Vorbis Comments in the second header packet of the OGG bitstream. Modifying these comments requires:
- Parsing OGG pages to extract the three Vorbis header packets (identification, comment, setup)
- Decoding and modifying the Vorbis Comment packet
- Re-encoding the modified comment packet into new OGG pages (with correct segment tables, sequence numbers, and CRC32 checksums)
- Copying all audio data pages unchanged
- Writing the result atomically
No pure-Go library exists that provides this end-to-end. The recommendation is a custom OGG page rewriter using existing building blocks.
Option Analysis
Option 1: Custom OGG Page Rewriter (RECOMMENDED)
| Component | Source | Purpose |
|---|---|---|
| OGG page read/write | Custom (~300-400 LOC) | Parse OGG pages, rewrite with new comment packet |
| Vorbis Comment encode/decode | Reuse go-flac/flacvorbis patterns |
Same binary format as FLAC Vorbis Comments |
| CRC32 computation | Custom (~30 LOC, lookup table) | OGG uses CRC32 with polynomial 0x04c11db7 |
| METADATA_BLOCK_PICTURE | Reuse go-flac/flacpicture |
Same binary format, base64-wrapped for OGG |
Why this is the right approach:
- The OGG container format is simple and well-documented (27-byte header + segment table + page data)
jfreymuth/oggvorbisalready has a working OGG page reader inogg.go(~255 LOC) that can serve as reference- The Vorbis Comment binary format is identical to what's used in FLAC — the existing
flacvorbislibrary's comment manipulation code can be reused or adapted - Audio data pages are copied byte-for-byte — no audio re-encoding
- Full control over the implementation with no external dependency risk
Implementation complexity: MEDIUM. The OGG page format has exactly one tricky aspect: packets can span multiple pages (via the continuation-of-packet flag and lacing values). The comment packet is typically small enough to fit in one page, but the implementation must handle the general case (especially when large cover art is embedded, inflating the comment packet well beyond one page's ~65KB limit).
Estimated LOC: ~500-600 for a complete ogg.go writer in the tagwriter package.
Option 2: mccoyst/ogg for OGG Page Encoding
| Technology | Version | Stars | Purpose |
|---|---|---|---|
github.com/mccoyst/ogg |
latest (no semver tags) | 37 | OGG page encode/decode |
What it provides:
ogg.Decoder— reads OGG pages, extracts packetsogg.Encoder— writes packets into OGG pages with correct framing, CRC, segment tablesEncodeBOS(),Encode(),EncodeEOS()— page-type-aware encoding- Handles packet splitting across pages automatically
- 100% test coverage claimed, 52 commits, MIT license
Assessment: This is the strongest external option. The Encoder handles the hardest parts (lacing values, page splitting, CRC). However:
- No semver releases — importing is
github.com/mccoyst/oggwith no version guarantee - 37 stars, 1 open issue — small community, low bus factor
- Still requires Vorbis-specific logic — mccoyst/ogg handles OGG pages but knows nothing about Vorbis header packets. We'd still need to:
- Identify and extract the three Vorbis header packets
- Parse/modify the Vorbis Comment packet
- Handle the
\x03vorbispacket type prefix - Re-encode everything in the correct order
Verdict: POSSIBLE but the marginal benefit over custom code is small. The OGG page format is well-specified; the Encoder's value is mainly in lacing value calculation and CRC, which are ~80 LOC total. Adding a dependency for ~80 LOC of saved work introduces a maintenance and stability risk for a library with no tagged releases.
Recommendation: Custom implementation. If OGG page writing proves harder than expected
during implementation, mccoyst/ogg can be pulled in as a fallback.
Option 3: jfreymuth/oggvorbis (Already a Dependency)
| Technology | Version | Stars | Purpose |
|---|---|---|---|
github.com/jfreymuth/oggvorbis |
v1.0.5 | 75 | OGG Vorbis decoder |
Assessment: Decode-only. No write API. The ogg.go internal reader is useful as
reference code for understanding OGG page parsing, but the types are unexported
and the package provides no page-level write capability.
Verdict: NOT SUITABLE for writing. Useful as reference only.
Option 4: dhowden/tag Write Support
Assessment: dhowden/tag is read-only (642 stars, no write API). Its OGG parsing is minimal — it extracts Vorbis Comments for reading but does not provide any mechanism to modify or write back. Forking would require building the entire OGG page rewriter anyway, plus inheriting maintenance burden.
Verdict: NOT SUITABLE.
Option 5: External CLI Tools (vorbiscomment, ffmpeg)
| Tool | What It Does | Issue |
|---|---|---|
vorbiscomment |
CLI for reading/writing Vorbis Comments in OGG | Requires packaging/distributing a C binary |
ffmpeg |
Swiss-army multimedia tool | Massive binary (~100MB), CGo-equivalent burden |
opustags |
Opus metadata editor | Wrong codec (Opus, not Vorbis) |
Assessment: Shelling out to external CLI tools would work but violates the pure-Go spirit of the project. It introduces:
- Distribution complexity (packaging native binaries per platform)
- Runtime dependency management (checking tool availability, version compat)
- Error handling complexity (parsing CLI output)
- No Windows/macOS availability guarantee without extra bundling
Verdict: NOT RECOMMENDED. Only consider as absolute last resort if custom Go implementation proves infeasible (it won't — the format is well-understood).
OGG Vorbis Writing: Recommended Stack
| Component | Approach | New Dependency? |
|---|---|---|
| OGG page parsing | Custom oggRewriter in tagwriter package |
No |
| OGG page writing | Custom (header + segment table + CRC32) | No |
| Vorbis Comment manipulation | Reuse flacvorbis patterns + shared helpers |
No (already a dep) |
| Cover art (METADATA_BLOCK_PICTURE) | go-flac/flacpicture for binary encoding + base64 wrapper |
No (already a dep) |
| Atomic file write | Existing fileutil.AtomicWrite |
No |
Total new dependencies: ZERO. Everything needed is either already in the dep tree or implementable from well-documented specifications.
OGG Vorbis Cover Art
YES — OGG Vorbis can embed cover art.
Cover art in OGG Vorbis uses the METADATA_BLOCK_PICTURE Vorbis Comment field:
- Create a FLAC Picture binary block (same as used in FLAC files):
- Picture type (3 = front cover)
- MIME type string ("image/jpeg" or "image/png")
- Description string ("Front cover")
- Width, height, color depth, palette size (can be set to 0)
- Raw image data
- Base64-encode the entire binary block
- Add as Vorbis Comment:
METADATA_BLOCK_PICTURE=<base64 data>
Integration with existing code: The go-flac/flacpicture library already creates
the binary FLAC Picture block. For FLAC files, this block goes into a metadata block.
For OGG files, the same binary block gets base64-encoded and inserted as a Vorbis
Comment string. We can reuse flacpicture.NewFromImageData() and add a base64 wrapper.
Confidence: HIGH — This is the official Xiph.org recommendation per https://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE
OGG Vorbis Format Details (Implementation Reference)
OGG Page Structure (27 bytes header):
Bytes 0-3: "OggS" capture pattern
Byte 4: Stream structure version (always 0)
Byte 5: Header type flag (0x01=continued, 0x02=BOS, 0x04=EOS)
Bytes 6-13: Absolute granule position (int64, little-endian)
Bytes 14-17: Stream serial number (uint32, little-endian)
Bytes 18-21: Page sequence number (uint32, little-endian)
Bytes 22-25: CRC32 checksum (computed with this field zeroed)
Byte 26: Number of segments (0-255)
Bytes 27+: Segment table (one byte per segment, lacing values)
Page data follows immediately
Vorbis Header Packets in OGG:
- Packet 1: Identification header (starts with
\x01vorbis) - Packet 2: Comment header (starts with
\x03vorbis) ← THIS IS WHAT WE MODIFY - Packet 3: Setup header (starts with
\x05vorbis) - Packets 4+: Audio data
Vorbis Comment Binary Format (within packet 2, after \x03vorbis prefix):
[vendor_length: uint32 LE] [vendor_string: bytes]
[comment_count: uint32 LE]
for each comment:
[length: uint32 LE] [comment: bytes] // e.g. "ARTIST=Bob Dylan"
[framing_bit: 1 bit, must be 1]
Algorithm for tag writing:
- Read all OGG pages from source file
- Extract packets 1, 2, 3 from the first few pages (header pages)
- Parse Vorbis Comment from packet 2 (skip
\x03vorbisprefix) - Modify comments (add/replace/remove fields)
- Re-serialize Vorbis Comment packet (with
\x03vorbisprefix) - Write to temp file via AtomicWrite: a. Write packet 1 (identification) as BOS page b. Write modified packet 2 (comment) + packet 3 (setup) as continuation pages c. Copy all remaining audio pages, re-sequencing page numbers
- Atomic rename over original
WAV Tag Writing
The Problem
WAV files use the RIFF container format. Metadata in WAV files can be stored in:
- RIFF INFO chunks (
LIST/INFOsub-chunks likeIART,INAM,IPRD) — the oldest and most widely supported mechanism - ID3v2 chunks (
id3RIFF chunk containing a full ID3v2 tag) — newer, more expressive, used by some modern tools - BEXT chunks (Broadcast Wave Extension) — professional/broadcast use only
The challenge is that no dominant pure-Go library exists for WAV metadata writing, and the ecosystem recently lost its most popular option.
Option Analysis
Option 1: Custom RIFF Chunk Writer for LIST/INFO (RECOMMENDED)
| Component | Source | Purpose |
|---|---|---|
| RIFF chunk reader | Custom (~200 LOC) | Parse WAV RIFF structure, find/modify LIST INFO chunk |
| INFO field writing | Custom (~150 LOC) | Write INFO sub-chunks with metadata |
| Atomic file write | Existing fileutil.AtomicWrite |
Crash-safe file operations |
RIFF INFO Chunk Field Mapping:
| YellowJacket Field | INFO Chunk ID | Description |
|---|---|---|
| Title | INAM |
Name/title of the work |
| Artist | IART |
Artist name |
| Album | IPRD |
Product/album name |
| Genre | IGNR |
Genre |
| Year | ICRD |
Creation date |
| Track Number | ITRK |
Track number |
| Composer | IMUS |
Composer/music by |
| Comment | ICMT |
Comment |
Why RIFF INFO over ID3v2-in-WAV:
- RIFF INFO is the native WAV metadata format — it's part of the RIFF specification
- Universal player support (Windows Media Player, VLC, foobar2000, etc.)
- Simple format: 4-byte chunk ID + 4-byte size + null-terminated string
- dhowden/tag already reads RIFF INFO chunks, so round-trip works
- No additional dependencies needed
Why NOT ID3v2-in-WAV:
- The
id3chunk is a de facto standard, not an official RIFF spec feature - Not all players/tools support it
- dhowden/tag reads ID3v2 in WAV (it detects it), but our existing
bogem/id3v2expects MP3 file structure (it callstag.Open()which reads from position 0, expecting an ID3v2 header). Using bogem/id3v2 for WAV would require significant adaptation to handle the RIFF wrapper. - More complex: we'd need to create an ID3v2 tag, serialize it, then embed it as a RIFF chunk
Implementation approach:
- Read entire WAV file (parse RIFF chunks:
RIFF,fmt,data,LIST, etc.) - Find or create
LIST/INFOchunk - Write/replace INFO sub-chunks with new metadata values
- Reassemble file: RIFF header → fmt chunk → data chunk → LIST/INFO chunk → other chunks
- Update RIFF header size
- Write via AtomicWrite
Estimated LOC: ~300-400 for RIFF INFO reading/writing.
Option 2: go-audio/wav + go-audio/riff
| Technology | Status | Stars |
|---|---|---|
github.com/go-audio/wav |
ARCHIVED Feb 21, 2026 | 383 |
github.com/go-audio/riff |
ARCHIVED Feb 21, 2026 | 11 |
Assessment: Both libraries were archived less than one month ago. They provided WAV encoding/decoding and RIFF chunk parsing, but:
go-audio/wavis focused on audio encoding/decoding, not metadata manipulationgo-audio/riffhas only a parser (3 commits total), no writer- Neither supports writing RIFF INFO metadata
- Both are now unmaintained/archived — using them would be a dead-end dependency
Verdict: NOT SUITABLE. Archived, no metadata write support.
Option 3: bogem/id3v2 for ID3v2-in-WAV
Assessment: bogem/id3v2's Tag.WriteTo(w io.Writer) writes raw ID3v2 tag bytes to
any writer. In theory, we could:
- Create an ID3v2 tag with bogem/id3v2
- Serialize it to bytes via
WriteTo() - Wrap it in a RIFF
id3chunk - Insert the chunk into the WAV file
This is technically feasible but:
- Still requires custom RIFF chunk manipulation code
- ID3v2-in-WAV has worse player compatibility than RIFF INFO
- More complex than just writing RIFF INFO chunks directly
tag.Open("file.wav")andtag.Save()won't work — those expect MP3 file structure
Verdict: NOT RECOMMENDED as primary approach. Could be added later as an enhancement for richer metadata (cover art via APIC), but RIFF INFO should be the primary mechanism.
Option 4: External CLI Tools (ffmpeg, exiftool)
Same issues as OGG: distribution complexity, runtime dependencies, error handling overhead.
Verdict: NOT RECOMMENDED.
WAV Writing: Recommended Stack
| Component | Approach | New Dependency? |
|---|---|---|
| RIFF container parsing | Custom RIFF reader in tagwriter package | No |
| RIFF INFO chunk writing | Custom (4-byte IDs + null-terminated strings) | No |
| Atomic file write | Existing fileutil.AtomicWrite |
No |
Total new dependencies: ZERO.
WAV Cover Art
RIFF INFO: NO — cannot embed cover art.
RIFF INFO chunks are limited to simple text key-value pairs. There is no standard INFO sub-chunk for binary image data.
ID3v2-in-WAV: YES — via APIC frame.
If an id3 RIFF chunk is present (or added), it can contain a full ID3v2 tag with
APIC (Attached Picture) frames, just like MP3 files.
Recommendation for v1.2.1: Do NOT implement WAV cover art. The RIFF INFO approach gives us text tag support with zero dependencies, and WAV cover art support is rare in the wild. Cover art for WAV can be deferred to a future enhancement using the ID3v2-in-WAV approach if there's user demand.
WAV RIFF Format Details (Implementation Reference)
RIFF File Structure:
"RIFF" [file_size: uint32 LE] "WAVE"
"fmt " [chunk_size: uint32 LE] [format data...]
"data" [chunk_size: uint32 LE] [audio samples...]
"LIST" [chunk_size: uint32 LE] "INFO"
"INAM" [size: uint32 LE] "Track Title\0"
"IART" [size: uint32 LE] "Artist Name\0"
...
Key implementation details:
- All integers are little-endian (unlike OGG which is a mix)
- Chunk sizes must be even (pad with 0x00 byte if odd)
- Strings in INFO chunks are null-terminated
- The RIFF header size field = total file size - 8
- LIST/INFO chunk can appear anywhere after
fmtanddata - Multiple LIST chunks may exist; only
LIST/INFOcontains metadata
Algorithm for tag writing:
- Parse RIFF chunks by reading 4-byte ID + 4-byte size pairs
- Collect all chunks, preserving order
- Find or create LIST/INFO chunk
- Replace/add INFO sub-chunks for changed fields
- Reassemble file via AtomicWrite: a. Write RIFF header with updated total size b. Write fmt chunk (unchanged) c. Write data chunk (unchanged — just copy bytes) d. Write LIST/INFO chunk with metadata e. Write any other chunks (unchanged)
- Atomic rename
Integration with Existing Pipeline
Format Detection Changes
Current tagwriter.DetectFormat() supports MP3 and FLAC. Add OGG and WAV:
const (
FormatMP3 AudioFormat = "mp3"
FormatFLAC AudioFormat = "flac"
FormatOGG AudioFormat = "ogg" // NEW
FormatWAV AudioFormat = "wav" // NEW
)
func DetectFormat(filePath string) (AudioFormat, error) {
switch strings.ToLower(filepath.Ext(filePath)) {
case ".mp3": return FormatMP3, nil
case ".flac": return FormatFLAC, nil
case ".ogg": return FormatOGG, nil // NEW
case ".wav": return FormatWAV, nil // NEW
default: return "", errUnsupportedFormat
}
}
Pipeline Dispatch Changes
Current WriteTrackTags() switch in pipeline.go:
switch format {
case FormatMP3: err = writeMp3Tags(tw.logger, audioFile.FilePath, changes)
case FormatFLAC: err = writeFlacTags(tw.logger, audioFile.FilePath, changes)
case FormatOGG: err = writeOggTags(tw.logger, audioFile.FilePath, changes) // NEW
case FormatWAV: err = writeWavTags(tw.logger, audioFile.FilePath, changes) // NEW
}
Shared Code Reuse
Vorbis Comment helpers (already exist in flac.go):
replaceVorbisComment()— remove existing field, add new valueapplyFlacTextChanges()— map TagChanges to Vorbis Comment fields- Field mapping constants (
FIELD_TITLE,FIELD_ARTIST, etc.)
These should be extracted to a shared file (e.g., vorbis_comments.go) and reused
by both flac.go and the new ogg.go. The logic is identical — both formats use Vorbis
Comments with the same field names.
Cover art helpers (already exist in tagwriter.go):
detectMIME()— determine image MIME type from magic bytesasBytes()— extract byte slice from TagChanges value
AtomicWrite (already exists in fileutil/atomicwrite.go):
- Used identically for all formats: write to temp → sync → rename
Dependency Summary
New Dependencies Required
NONE. Both OGG Vorbis and WAV tag writing are implemented as custom code in the tagwriter package, using only:
- Go standard library (
encoding/binary,encoding/base64,bytes,io,os) - Existing dependencies (
go-flac/flacpicturefor METADATA_BLOCK_PICTURE binary format) - Existing utilities (
fileutil.AtomicWrite)
Existing Dependencies Unchanged
| Library | Current Use | Use in v1.2.1 |
|---|---|---|
dhowden/tag v0.0.0-20240417 |
Tag reading (all formats) | Unchanged — validates OGG/WAV round-trip |
go-flac/flacvorbis/v2 v2.0.2 |
FLAC Vorbis Comment manipulation | Shared patterns for OGG comment encoding |
go-flac/flacpicture/v2 v2.0.2 |
FLAC PICTURE block creation | Reused for OGG METADATA_BLOCK_PICTURE |
go-flac/go-flac/v2 v2.0.4 |
FLAC metadata block manipulation | Unchanged |
bogem/id3v2/v2 v2.1.4 |
MP3 ID3v2 tag writing | Unchanged |
jfreymuth/oggvorbis v1.0.5 |
OGG Vorbis decoding (via beep) | Reference for OGG page structure |
What NOT To Add
| Library / Approach | Why Avoid |
|---|---|
mccoyst/ogg |
No semver releases, 37 stars. The OGG page format is simple enough to implement directly (~80 LOC for the encoding part). Avoids dependency risk for minimal gain. |
go-audio/wav or go-audio/riff |
Archived Feb 21, 2026. Do not depend on abandoned libraries. |
| Any CGo-based library (taglib-go, etc.) | Violates pure-Go constraint |
bogem/id3v2 for WAV files |
Its Open()/Save() API expects MP3 file structure. Would need significant wrapping for RIFF container. RIFF INFO is simpler and more compatible. |
| ID3v2-in-WAV for v1.2.1 | Adds complexity for marginal benefit. RIFF INFO covers the primary use case. Defer ID3v2-in-WAV to a future milestone if cover art in WAV is needed. |
| External CLI tools (vorbiscomment, ffmpeg) | Distribution complexity, runtime deps, violates pure-Go spirit |
Format Coverage Matrix (v1.2.1 Target)
| Format | Text Tags | Cover Art | Approach | Complexity | Confidence |
|---|---|---|---|---|---|
| MP3 (ID3v2) | ✓ All fields | ✓ APIC frame | bogem/id3v2 (existing) | Done | HIGH |
| FLAC | ✓ All fields | ✓ PICTURE block | go-flac ecosystem (existing) | Done | HIGH |
| OGG Vorbis | ✓ All fields | ✓ METADATA_BLOCK_PICTURE | Custom OGG page rewriter | Medium | MEDIUM-HIGH |
| WAV | ✓ Core fields | ✗ Not in v1.2.1 | Custom RIFF INFO writer | Low-Medium | MEDIUM-HIGH |
"Core fields" for WAV: Title, Artist, Album, Genre, Year, Track Number, Composer. Album Artist and Disc Number have no standard RIFF INFO chunk IDs. They can be omitted or stored in non-standard INFO chunks if needed.
Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| OGG page splitting with large cover art | Medium | Medium | Test with cover art >64KB (forces multi-page comment packet) |
| WAV files with unusual RIFF chunk ordering | Low | Low | Parse all chunks generically, preserve unknown chunks |
| dhowden/tag can't read what we write (OGG) | Low | High | Round-trip tests: write with custom writer, read with dhowden/tag |
| dhowden/tag can't read what we write (WAV) | Low | High | Round-trip tests for RIFF INFO chunks |
| OGG CRC32 computation mismatch | Low | High | Use the exact polynomial from OGG spec (0x04c11db7), test against known files |
| WAV INFO chunk padding errors | Medium | Low | RIFF spec requires even-aligned chunks; easy to forget padding byte |
Sources
Official Specifications (HIGH confidence)
- OGG framing: https://xiph.org/ogg/doc/framing.html
- OGG RFC: https://xiph.org/ogg/doc/rfc3533.txt
- Vorbis I Spec (Section 5 — comment field): https://xiph.org/vorbis/doc/Vorbis_I_spec.html
- Vorbis Comment spec: https://xiph.org/vorbis/doc/v-comment.html
- METADATA_BLOCK_PICTURE: https://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE
- FLAC Picture block format: https://xiph.org/flac/format.html#metadata_block_picture
Libraries Evaluated (HIGH confidence — GitHub repos)
- jfreymuth/oggvorbis: https://github.com/jfreymuth/oggvorbis (75 stars, decode-only)
- mccoyst/ogg: https://github.com/mccoyst/ogg (37 stars, encode+decode, no semver)
- dhowden/tag: https://github.com/dhowden/tag (642 stars, read-only)
- go-audio/wav: https://github.com/go-audio/wav (383 stars, ARCHIVED 2026-02-21)
- go-audio/riff: https://github.com/go-audio/riff (11 stars, ARCHIVED 2026-02-21)
- bogem/id3v2: https://github.com/n10v/id3v2 (359 stars, MP3-focused API)
Existing Codebase (HIGH confidence — already validated in v1.2)
backend/tagwriter/flac.go— Vorbis Comment manipulation patternsbackend/tagwriter/mp3.go— AtomicWrite integration patternbackend/tagwriter/tagwriter.go— TagChanges, format detection, helper functionsbackend/fileutil/atomicwrite.go— Crash-safe file write utility