26 KiB
Architecture Patterns: OGG Vorbis + WAV Tag Writer Integration
Domain: Adding OGG Vorbis and WAV tag writing to existing music player tag editing pipeline Researched: 2026-03-18 Confidence: HIGH (based on full codebase analysis of existing architecture + container format research)
Recommended Architecture
OGG Vorbis and WAV tag writing integrate into the existing tagwriter package by implementing two new format-specific writer functions (writeOggTags and writeWavTags) that follow the exact pattern established by writeMp3Tags and writeFlacTags. The existing pipeline (WriteTrackTags → file write → DB sync → event emission) requires only a switch-case extension — no new interfaces, no refactoring.
High-Level Data Flow (Unchanged)
UI: track-details "Save" click
→ Wails binding: TagWriter.WriteTrackTagsByPath(filePath, changes)
→ 1. Resolve audio_file by path
→ 2. DetectFormat(filePath) ← EXTEND: add .ogg, .wav cases
→ 3. AcquirePipelineLock
→ 4. PlayerStopper check
→ 5. Format switch:
case FormatMP3: writeMp3Tags(...) ← existing
case FormatFLAC: writeFlacTags(...) ← existing
case FormatOGG: writeOggTags(...) ← NEW
case FormatWAV: writeWavTags(...) ← NEW
→ 6. syncDatabase(...) ← NO CHANGES
→ 7. EventsEmit(TrackMetadataChanged) ← NO CHANGES
Key insight: The existing architecture was designed for format extension. The pipeline.go switch statement is the single point of modification. Everything downstream (DB sync, FTS5, orphan cleanup, event emission, batch processing, cover art pipeline) is format-agnostic and works unchanged.
Component Boundaries
| Component | Status | Changes |
|---|---|---|
backend/tagwriter/tagwriter.go |
MODIFY | Add FormatOGG, FormatWAV constants; extend DetectFormat() switch |
backend/tagwriter/pipeline.go |
MODIFY | Add two cases to format switch in WriteTrackTags() |
backend/tagwriter/ogg.go |
NEW | writeOggTags() — OGG container rewrite with Vorbis Comment manipulation |
backend/tagwriter/wav.go |
NEW | writeWavTags() — RIFF/WAV chunk manipulation with ID3v2 or LIST-INFO |
backend/tagwriter/ogg_test.go |
NEW | 7 round-trip tests following FLAC pattern |
backend/tagwriter/wav_test.go |
NEW | 7 round-trip tests following FLAC pattern |
backend/tagwriter/dbsync.go |
UNCHANGED | Format-agnostic entity sync |
backend/tagwriter/helpers_test.go |
UNCHANGED | Shared test helpers (tinyJPEG, assertEqual, etc.) |
backend/metadata/tags.go |
UNCHANGED | dhowden/tag already reads OGG and WAV tags |
backend/fileutil/atomicwrite.go |
UNCHANGED | Used by both new writers |
frontend/src/components/track-details/ |
UNCHANGED | Format-agnostic edit UI |
OGG Vorbis Writer: writeOggTags()
Container Structure
An OGG Vorbis file consists of OGG pages containing three types of Vorbis packets:
- Identification header (page 0, BOS flag) — audio parameters, must not be modified
- Comment header (page 1) — Vorbis Comments (tags) + optional METADATA_BLOCK_PICTURE
- Setup header (page 1 or 2) — codebooks, must not be modified
- Audio data (remaining pages) — compressed audio, must not be modified
Writing tags means replacing only the comment header packet while preserving everything else exactly.
Approach: Full-File Rewrite via AtomicWrite
Why full rewrite, not in-place: Changing the comment header changes its size. OGG pages have fixed-size segment tables (max 255 segments × 255 bytes = ~64KB per page). A larger comment header may require different page segmentation. Every subsequent page has a page sequence number and CRC-32 checksum that must be recalculated. In-place editing is impossible — the file must be rewritten.
This is fine: AtomicWrite already does full-file rewrite for MP3 and FLAC. OGG Vorbis files are typically 3-10MB (compressed audio). Memory usage is bounded because we stream page-by-page, not load the entire file.
Implementation Strategy
func writeOggTags(logger *slog.Logger, filePath string, changes TagChanges) error {
// 1. Open and parse OGG file page-by-page
// 2. Read the three header packets (identification, comment, setup)
// 3. Parse existing Vorbis Comment from comment header packet
// 4. Apply changes to Vorbis Comment fields (same field mapping as FLAC)
// 5. If cover_art changed: encode METADATA_BLOCK_PICTURE and add/remove
// from Vorbis Comment as base64 field
// 6. Re-serialize comment header packet
// 7. AtomicWrite: stream all pages to temp file
// - Page 0 (BOS): rewrite with correct CRC (identification header unchanged)
// - Page 1+: rewrite with new comment header + setup header, recalculate
// segmentation and CRC
// - Remaining pages: copy byte-for-byte (page sequence numbers and CRCs
// only need recalculation if page boundaries shifted)
// 8. Rename over original
}
OGG Page Structure (For Implementation)
OGG Page Header (27 bytes + segment table):
- Capture pattern: "OggS" (4 bytes)
- Stream structure version: 0 (1 byte)
- Header type flag: BOS/EOS/continued (1 byte)
- Absolute granule position: int64 (8 bytes)
- Stream serial number: uint32 (4 bytes)
- Page sequence number: uint32 (4 bytes)
- CRC-32 checksum: uint32 (4 bytes)
- Number of page segments: uint8 (1 byte)
- Segment table: [num_segments]uint8
OGG Page Body:
- Raw data (sum of segment table values bytes)
Vorbis Comment Format (Shared with FLAC)
The comment header packet uses the exact same Vorbis Comment format as FLAC's Vorbis Comment metadata block, with one difference:
- In FLAC: Vorbis Comments are a metadata block (type 4), binary data
- In OGG: Vorbis Comments are preceded by a 7-byte Vorbis packet header (
\x03vorbis)
The field mapping is identical to applyFlacTextChanges():
| Field | Vorbis Comment Key |
|---|---|
| title | TITLE |
| artist | ARTIST |
| album | ALBUM |
| album_artist | ALBUMARTIST |
| genre | GENRE |
| year | DATE |
| track_number | TRACKNUMBER |
| disc_number | DISCNUMBER |
| composer | COMPOSER |
Code Reuse Opportunity
The replaceVorbisComment() function in flac.go operates on *flacvorbis.MetaDataBlockVorbisComment which is specific to the go-flac/flacvorbis library. However, the Vorbis Comment binary format is identical across FLAC and OGG. Two approaches:
-
Build a minimal Vorbis Comment parser/serializer (~80 lines) directly in
ogg.gothat reads/writes thevendor_string + comments[]binary format. This avoids pulling in FLAC dependencies for OGG files. The existingparseVorbisComment()helper inflac_test.goalready demonstrates the parsing logic — promote it to a shared implementation. -
Reuse
go-flac/flacvorbisby constructing a fakeMetaDataBlockfrom the OGG comment packet (strip the 7-byte\x03vorbisprefix). This is hacky and creates a false dependency.
Recommendation: Approach 1. Write a self-contained vorbisComment type in ogg.go with parse(data []byte) and marshal() []byte methods. The format is simple (little-endian length-prefixed strings) and the test already has the parser. This is ~80 lines and avoids coupling.
Cover Art in OGG: METADATA_BLOCK_PICTURE
OGG Vorbis stores cover art as a Vorbis Comment field named METADATA_BLOCK_PICTURE. The value is a base64-encoded binary blob using the same FLAC PICTURE block format:
METADATA_BLOCK_PICTURE binary format:
- Picture type: uint32 BE (3 = front cover)
- MIME string length: uint32 BE
- MIME string: UTF-8
- Description length: uint32 BE
- Description: UTF-8
- Width: uint32 BE
- Height: uint32 BE
- Color depth: uint32 BE
- Colors used: uint32 BE
- Data length: uint32 BE
- Data: raw image bytes
This is then base64-encoded and stored as: METADATA_BLOCK_PICTURE=<base64 data>
Implementation: Encode using encoding/base64 and the same detectMIME() helper. Width/height/depth can be set to 0 (players derive them from the image data). This adds ~30 lines to the cover art handling.
Read-back verification: dhowden/tag already reads METADATA_BLOCK_PICTURE from OGG files and returns it via the Picture() method. Round-trip tests will work with metadata.ExtractTags() unchanged.
No Existing Go Library
Confirmed: There is no pure-Go library for writing OGG Vorbis tags. The existing jfreymuth/oggvorbis library (already an indirect dependency via beep) is read-only — it provides NewReader(), Read(), CommentHeader(), and GetCommentHeader() but no write functionality. The ogg.go source reveals the page-level reading infrastructure (page.read(), page.readHeader(), page.readContent()) but no page writing.
Impact: We must implement OGG page writing ourselves. This is ~200 lines of code for the page serializer + CRC calculation. The jfreymuth/oggvorbis package's crc.go provides the CRC-32 lookup table (crcUpdate()) we can reference for the polynomial (0x04C11DB7, same as in the OGG spec). However, since it's unexported, we must either vendor the CRC table or compute it at init time.
Risk Assessment
| Risk | Severity | Mitigation |
|---|---|---|
| OGG page re-segmentation correctness | HIGH | Comprehensive round-trip tests; verify with dhowden/tag read-back |
| CRC-32 calculation error | HIGH | Use the same polynomial as the OGG spec; test against known-good files |
| METADATA_BLOCK_PICTURE encoding | LOW | Well-documented format; base64 encoding is trivial |
| Large OGG files (>100MB) | LOW | OGG Vorbis files are typically <15MB; stream page-by-page |
| Multi-stream OGG files | LOW | Music files are single-stream; reject multi-stream as unsupported |
WAV Writer: writeWavTags()
Container Structure
WAV files use the RIFF (Resource Interchange File Format) container:
RIFF header:
"RIFF" (4 bytes)
File size - 8 (uint32 LE)
"WAVE" (4 bytes)
Chunks (in any order):
"fmt " chunk — audio format parameters (required)
"data" chunk — raw PCM audio samples (required)
"LIST" chunk (subtype "INFO") — metadata as sub-chunks
"id3 " or "ID3 " chunk — embedded ID3v2 tag
other chunks (fact, cue, etc.)
Metadata Approach: ID3v2 in RIFF Chunk
WAV metadata can be stored in two ways:
- LIST-INFO chunks: Simple key-value pairs (
INAM=title,IART=artist,IPRD=album, etc.). No cover art support. Limited charset (originally ASCII, some tools use UTF-8). - id3 chunk: A full ID3v2 tag embedded in a RIFF chunk named
id3orID3. Supports all fields including cover art. Most modern tools (foobar2000, MusicBee, TagLib) use this approach.
Recommendation: Use the id3 chunk approach because:
- Reuses the existing
bogem/id3v2library already used bywriteMp3Tags() - Supports cover art embedding (LIST-INFO does not)
dhowden/tagreads ID3v2 from WAV files, so round-trip tests work- Consistent field semantics with MP3 tag writing
Implementation Strategy
func writeWavTags(logger *slog.Logger, filePath string, changes TagChanges) error {
// 1. Open WAV file, parse RIFF header
// 2. Enumerate chunks: find existing "id3 " chunk (if any),
// locate "fmt " and "data" chunks
// 3. If existing id3 chunk found:
// a. Parse existing ID3v2 tag
// b. Apply changes using same applyTextChanges()/applyCoverArtChanges()
// as MP3 writer
// c. Serialize updated tag
// 4. If no existing id3 chunk:
// a. Create new ID3v2 tag
// b. Apply changes
// c. Serialize
// 5. AtomicWrite: rebuild RIFF file
// a. Write RIFF header with new total size
// b. Copy all original chunks EXCEPT old id3 chunk
// c. Append new id3 chunk (with proper RIFF chunk header)
// d. Update RIFF file size in header
}
Code Reuse with MP3 Writer
The MP3 writer's applyTextChanges(tag, changes) and applyCoverArtChanges(tag, changes) functions operate on *id3v2.Tag objects. These exact functions can be reused by the WAV writer since the ID3v2 tag format is identical:
// In wav.go — reuse existing functions from mp3.go:
tag, err := id3v2.Open(...) // or id3v2.ParseReader(...)
applyTextChanges(tag, changes) // ← same function from mp3.go
applyCoverArtChanges(tag, changes) // ← same function from mp3.go
RIFF Chunk Parser (~100 lines)
A minimal RIFF parser needs to:
- Read 12-byte RIFF header (
"RIFF" + size + "WAVE") - Iterate chunks: 8-byte chunk header (
id[4] + size[4]), skip body - Track positions and sizes of each chunk
- Handle padding bytes (RIFF chunks are word-aligned — if data size is odd, a pad byte follows)
This is straightforward binary parsing. No external library needed.
AtomicWrite for Large WAV Files
Concern: WAV files can be very large (uncompressed audio: a 60-minute CD-quality WAV is ~630MB). AtomicWrite creates a full copy in a temp file before renaming.
Assessment: This is acceptable because:
- AtomicWrite already streams data via
io.Copy— it doesn't load the file into memory - The WAV writer copies chunks sequentially: read chunk from source → write to temp file
- Disk space for the temp file is the only cost (~2× file size temporarily)
- The alternative (in-place chunk modification) risks corruption if the process is interrupted mid-write
- The existing FLAC writer already handles large files this way (with a warning for >500MB)
Practical consideration: WAV files >500MB are rare in music libraries (they're typically ripped CDs at ~30-50MB per track, or high-resolution at ~150MB). Add the same size warning as the FLAC writer.
if info.Size() > largeSizeThreshold {
logger.Warn("large WAV file may take extra time/space for atomic write",
slog.String("path", filePath),
slog.Int64("size", info.Size()),
)
}
Cover Art in WAV
Since we're using the id3 chunk approach, cover art embedding uses the exact same APIC frame mechanism as MP3. The applyCoverArtChanges() function handles this already.
Risk Assessment
| Risk | Severity | Mitigation |
|---|---|---|
| Large file temp space | MEDIUM | Log warning for >500MB; streaming copy avoids memory issues |
| RIFF chunk alignment (pad bytes) | MEDIUM | Follow spec: odd-sized chunks get 1 pad byte |
| Existing LIST-INFO metadata | LOW | Preserve LIST-INFO chunks as-is; only modify/add id3 chunk |
bogem/id3v2 reading from WAV |
LOW | Library may need ParseReader instead of Open (investigate) |
| WAV files without existing tags | LOW | Create new id3 chunk; all other chunks preserved |
Format Detection Extension
Current Implementation (tagwriter.go)
func DetectFormat(filePath string) (AudioFormat, error) {
ext := strings.ToLower(filepath.Ext(filePath))
switch ext {
case ".mp3":
return FormatMP3, nil
case ".flac":
return FormatFLAC, nil
default:
return "", fmt.Errorf("%w: %s", errUnsupportedFormat, ext)
}
}
Required Changes
const (
FormatMP3 AudioFormat = "mp3"
FormatFLAC AudioFormat = "flac"
FormatOGG AudioFormat = "ogg" // NEW
FormatWAV AudioFormat = "wav" // NEW
)
func DetectFormat(filePath string) (AudioFormat, error) {
ext := strings.ToLower(filepath.Ext(filePath))
switch ext {
case ".mp3":
return FormatMP3, nil
case ".flac":
return FormatFLAC, nil
case ".ogg": // NEW
return FormatOGG, nil // NEW
case ".wav": // NEW
return FormatWAV, nil // NEW
default:
return "", fmt.Errorf("%w: %s", errUnsupportedFormat, ext)
}
}
Extension-based detection is sufficient. The existing approach works because:
- The metadata package already uses extension-based routing for decoding (
metadata/decoder.go) - YellowJacket only indexes files with known extensions (
.mp3,.flac,.ogg,.wav) - File magic-byte detection is unnecessary — if a file is in the DB, it was already validated during scan
Pipeline Switch Extension
In pipeline.go, the format switch becomes:
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)
case FormatWAV:
err = writeWavTags(tw.logger, audioFile.FilePath, changes)
default:
err = fmt.Errorf("%w: %s", errUnsupportedFormat, format)
}
Frontend Impact: None
The track-details dialog component is completely format-agnostic. It:
- Shows the same 8 editable fields regardless of format
- Shows the same cover art pick/replace/remove UI
- Sends the same
TagChangesdiff map toWriteTrackTagsByPath() - Receives the same
TrackMetadataChangedevent - Uses the same three-state field model for batch editing
The backend handles all format-specific logic. The frontend never sees or cares about the audio format.
Verified: No frontend changes needed for OGG or WAV tag writing.
Test Strategy
Follow FLAC Round-Trip Pattern (7 Tests per Format)
The FLAC writer has 7 tests that verify the complete write→read cycle using metadata.ExtractTags() for read-back. The same test structure applies to OGG and WAV:
| Test | What It Verifies |
|---|---|
TestWriteOggTags_TextFields |
All 9 text fields round-trip correctly |
TestWriteOggTags_CoverArt |
METADATA_BLOCK_PICTURE embedded and readable |
TestWriteOggTags_ClearCoverArt |
Cover art removal works |
TestWriteOggTags_PartialUpdate |
Unchanged fields preserved |
TestWriteOggTags_PreservesAudioData |
Audio stream intact after tag write |
TestWriteOggTags_ReplaceComment |
No duplicate Vorbis Comment entries |
TestWriteOggTags_AtomicSafety |
Failed write leaves file untouched |
Same 7 tests for WAV (TestWriteWavTags_*).
Test Fixture Helpers
Each format needs a makeMinimal*() helper:
-
makeMinimalOGG(t, path)— Creates a minimal valid OGG Vorbis file containing: BOS page with identification header, comment header page (empty Vorbis Comment), EOS page with setup header + minimal audio frame. This is complex (~60 lines) but required for round-trip testing. -
makeMinimalWAV(t, path)— Creates a minimal valid WAV file containing: RIFF header,fmtchunk (PCM, 44100Hz, 16-bit, mono),datachunk (brief silence). This is simple (~30 lines) — just binary header construction.
Read-Back Verification
Both formats use metadata.ExtractTags() (which uses dhowden/tag) for read-back verification:
- OGG:
dhowden/tagreads Vorbis Comments from OGG files includingMETADATA_BLOCK_PICTURE. Verified: the library'sogg.goandvorbis.gohandle this. - WAV:
dhowden/tagreads ID3v2 tags from WAV files (it detects theid3chunk in the RIFF container). Verified: the library handles this per its README (MP3/MP4/OGG/FLAC metadata parsing).
Confidence: HIGH — dhowden/tag is already the read-back library for MP3 and FLAC tests. It supports OGG and WAV reading.
Suggested Build Order
Based on dependency analysis and risk levels:
Phase 1: WAV Writer (Lower Risk, Faster)
Rationale: WAV writing reuses the existing bogem/id3v2 library and the existing applyTextChanges()/applyCoverArtChanges() functions. The RIFF container is much simpler than OGG (no checksums, no page segmentation). This can be built and tested quickly, giving confidence in the pipeline extension pattern before tackling OGG.
- Add
FormatWAVconstant and extendDetectFormat() - Write RIFF chunk parser (~100 lines in
wav.go) - Implement
writeWavTags()using id3v2 tag in RIFF chunk - Add pipeline switch case
- Write
makeMinimalWAV()test fixture - Write 7 round-trip tests
- Verify with
make lint
Phase 2: OGG Vorbis Writer (Higher Risk, More Code)
Rationale: OGG requires implementing the page-level write infrastructure (CRC-32, segmentation, page serialization) from scratch. This is the riskiest part of the milestone and benefits from having the WAV writer already proving the pipeline extension pattern works.
- Implement OGG CRC-32 calculation (~30 lines)
- Implement OGG page serializer (~80 lines)
- Implement Vorbis Comment parser/serializer (~80 lines)
- Implement METADATA_BLOCK_PICTURE encoding (~40 lines)
- Implement
writeOggTags()orchestrator (~100 lines) - Add
FormatOGGconstant and pipeline switch case - Write
makeMinimalOGG()test fixture (~60 lines) - Write 7 round-trip tests
- Verify with
make lint
Phase 3: Cleanup
- Remove OGG/WAV from "Out of Scope" in PROJECT.md
- Update milestone status
- Fix any lint warnings from v1.2
Total New Code Estimate
| Component | Lines (approx) |
|---|---|
ogg.go (writer + helpers) |
~350 |
wav.go (writer + RIFF parser) |
~200 |
ogg_test.go |
~350 |
wav_test.go |
~250 |
tagwriter.go changes |
~10 |
pipeline.go changes |
~5 |
| Total | ~1,165 |
Patterns to Follow
Pattern 1: Format Writer Function Signature
What: All format writers follow the same signature: func write*Tags(logger *slog.Logger, filePath string, changes TagChanges) error
Why: Keeps the pipeline switch clean and uniform. No interface needed — package-internal functions with consistent signatures are simpler.
Example:
func writeOggTags(logger *slog.Logger, filePath string, changes TagChanges) error { ... }
func writeWavTags(logger *slog.Logger, filePath string, changes TagChanges) error { ... }
Pattern 2: AtomicWrite Integration
What: Every writer creates the complete output file inside the AtomicWrite callback, writing to tmp *os.File.
Example (from existing FLAC writer):
return fileutil.AtomicWrite(logger, filePath, func(tmp *os.File) error {
_, writeErr := f.WriteTo(tmp)
return writeErr
})
Pattern 3: Test Fixture + Round-Trip Verification
What: Each format has a makeMinimal*() helper that creates a valid file, and tests verify by writing tags then reading back with metadata.ExtractTags().
Why: Tests validate the complete pipeline without external tools. The same metadata.ExtractTags() function used in production reads back the tags, ensuring what we write is what gets read.
Anti-Patterns to Avoid
Anti-Pattern 1: FormatWriter Interface
What: Creating a FormatWriter interface with Write(path string, changes TagChanges) error
Why bad: The package has only 4 format writers, all internal. An interface adds abstraction without value. The switch statement is clearer and the functions share a signature by convention.
Instead: Keep format writers as package-internal functions with matching signatures.
Anti-Pattern 2: In-Place OGG Page Modification
What: Attempting to modify OGG pages in-place to avoid full file rewrite.
Why bad: OGG pages have checksums and sequence numbers. Changing the comment packet size shifts all subsequent page offsets. In-place modification requires recalculating every downstream page's CRC, which is effectively a full rewrite anyway — but without crash safety.
Instead: Full-file rewrite via AtomicWrite.
Anti-Pattern 3: In-Place RIFF Chunk Insertion
What: Attempting to insert or resize RIFF chunks in-place.
Why bad: Inserting a new id3 chunk or resizing an existing one shifts all subsequent chunks. The RIFF header's total size must be updated. In-place modification risks corruption.
Instead: Full-file rewrite via AtomicWrite.
Anti-Pattern 4: Shared Vorbis Comment Code via go-flac/flacvorbis
What: Importing go-flac/flacvorbis in the OGG writer to reuse Vorbis Comment parsing.
Why bad: Creates a dependency on a FLAC-specific library for OGG files. The flacvorbis types are coupled to FLAC MetaDataBlock structures. The binary format is simple enough to parse directly.
Instead: Self-contained Vorbis Comment parser in ogg.go (~80 lines).
Scalability Considerations
| Concern | Typical Case | Edge Case | Approach |
|---|---|---|---|
| OGG file size | 3-10 MB | 50 MB live recording | Stream page-by-page, no full-file memory load |
| WAV file size | 30-50 MB (CD track) | 2 GB (24-bit/96kHz long recording) | Streaming copy via io.Copy; warn for >500MB |
| Temp disk space | 2× file size briefly | 2× 2GB = 4GB temp | Same concern as FLAC; document as known limitation |
| Batch edit 100 WAV files | Sequential, 30-50 MB each | 100 × 50 MB = 5 GB total I/O | Existing batch pipeline with progress events |
Sources
- OGG container format specification: https://xiph.org/ogg/doc/rfc3533.txt (HIGH confidence)
- Vorbis Comment specification: https://xiph.org/vorbis/doc/v-comment.html (HIGH confidence)
- Vorbis I specification: https://xiph.org/vorbis/doc/Vorbis_I_spec.html (HIGH confidence)
- METADATA_BLOCK_PICTURE in Vorbis Comments: https://xiph.org/flac/format.html#metadata_block_picture (HIGH confidence)
- RIFF/WAV format: https://www.mmsp.ece.mcgill.ca/documents/AudioFormats/WAVE/WAVE.html (HIGH confidence)
jfreymuth/oggvorbispackage API: https://pkg.go.dev/github.com/jfreymuth/oggvorbis (HIGH confidence — verified read-only)jfreymuth/vorbisCommentHeader type: https://pkg.go.dev/github.com/jfreymuth/vorbis (HIGH confidence)dhowden/tagOGG/WAV reading: https://github.com/dhowden/tag (HIGH confidence — used in existing tests)bogem/id3v2for WAV id3 chunk: https://github.com/bogem/id3v2 (HIGH confidence — used in existing MP3 writer)- Existing codebase:
backend/tagwriter/package (analyzed in full)