test(19-02): add WAV test fixture builder and read-back helper

- createTestWAV: builds minimal valid WAV with optional ID3v2 tag
- readWavID3Tags: extracts ID3v2 from RIFF via parseRIFF + dhowden/tag
- createTestWAVWithExtraChunks: adds LIST INFO and bext chunks for preservation tests
- makePCMFmtData: generates 16-byte PCM format data
This commit is contained in:
2026-03-19 08:50:20 -04:00
parent 8794584b97
commit 21fe17212d
+289
View File
@@ -0,0 +1,289 @@
package tagwriter
import (
"bytes"
"encoding/binary"
"os"
"path/filepath"
"testing"
id3v2 "github.com/bogem/id3v2/v2"
"github.com/dhowden/tag"
"yellowjacket/backend/metadata"
)
// createTestWAV builds a minimal valid WAV file with an optional
// ID3v2 tag populated from fields. It returns the path to the
// created file inside dir.
func createTestWAV(
t *testing.T,
dir string,
name string,
fields TagChanges,
) string {
t.Helper()
var buf bytes.Buffer
// --- fmt chunk (24 bytes total: 8 header + 16 data) ---
fmtData := makePCMFmtData()
// --- data chunk: 200 bytes of silence ---
const silenceLen = 200 //nolint:mnd
silence := make([]byte, silenceLen)
// --- optional id3 chunk ---
var id3Bytes []byte
if len(fields) > 0 {
id3Tag := id3v2.NewEmptyTag()
id3Tag.SetDefaultEncoding(id3v2.EncodingUTF8)
applyTextChanges(id3Tag, fields)
applyCoverArtChanges(id3Tag, fields)
var id3Buf bytes.Buffer
if _, err := id3Tag.WriteTo(&id3Buf); err != nil {
t.Fatalf("write id3v2 tag: %v", err)
}
id3Bytes = id3Buf.Bytes()
}
// Calculate RIFF payload size:
// 4 (WAVE)
// + 8 + 16 (fmt chunk)
// + 8 + silenceLen (data chunk)
// + optional id3 chunk: 8 + len(id3Bytes) + padding
riffPayload := uint32(4 + 8 + 16 + 8 + silenceLen) //nolint:mnd
if len(id3Bytes) > 0 {
riffPayload += 8 + uint32(len(id3Bytes)) //nolint:mnd
if len(id3Bytes)%2 != 0 {
riffPayload++
}
}
// RIFF header.
buf.WriteString("RIFF")
_ = binary.Write(&buf, binary.LittleEndian, riffPayload)
buf.WriteString("WAVE")
// fmt chunk.
buf.WriteString("fmt ")
_ = binary.Write(
&buf, binary.LittleEndian, uint32(len(fmtData)),
)
buf.Write(fmtData)
// data chunk.
buf.WriteString("data")
_ = binary.Write(
&buf, binary.LittleEndian, uint32(silenceLen),
)
buf.Write(silence)
// id3 chunk (if any).
if len(id3Bytes) > 0 {
buf.WriteString("id3 ")
_ = binary.Write(
&buf, binary.LittleEndian, uint32(len(id3Bytes)),
)
buf.Write(id3Bytes)
if len(id3Bytes)%2 != 0 {
buf.WriteByte(0)
}
}
path := filepath.Join(dir, name)
if err := os.WriteFile(path, buf.Bytes(), 0o644); err != nil {
t.Fatalf("write test WAV: %v", err)
}
return path
}
// makePCMFmtData returns 16 bytes of PCM format data:
// AudioFormat=1, Channels=1, SampleRate=44100,
// ByteRate=88200, BlockAlign=2, BitsPerSample=16.
func makePCMFmtData() []byte {
var buf bytes.Buffer
_ = binary.Write(&buf, binary.LittleEndian, uint16(1)) // PCM
_ = binary.Write(&buf, binary.LittleEndian, uint16(1)) // mono
_ = binary.Write(&buf, binary.LittleEndian, uint32(44100)) // sample rate
_ = binary.Write(&buf, binary.LittleEndian, uint32(88200)) // byte rate
_ = binary.Write(&buf, binary.LittleEndian, uint16(2)) // block align
_ = binary.Write(&buf, binary.LittleEndian, uint16(16)) // bits per sample
return buf.Bytes()
}
// readWavID3Tags extracts ID3v2 metadata from a WAV file by parsing
// the RIFF structure and reading the id3 chunk with dhowden/tag.
// This is necessary because dhowden/tag's ReadFrom does not support
// WAV files directly.
func readWavID3Tags(
t *testing.T,
path string,
) *metadata.TrackMetadata {
t.Helper()
f, err := os.Open(path)
if err != nil {
t.Fatalf("open WAV for read-back: %v", err)
}
defer func() { _ = f.Close() }()
chunks, err := parseRIFF(f)
if err != nil {
t.Fatalf("parseRIFF: %v", err)
}
// Find the id3 chunk.
var id3Data []byte
for _, c := range chunks {
if isID3ChunkID(c.id) {
id3Data = c.data
break
}
}
if id3Data == nil {
t.Fatal("no id3 chunk found in WAV file")
}
// dhowden/tag ReadID3v2Tags expects an io.ReadSeeker starting
// with the "ID3" magic bytes.
m, err := tag.ReadID3v2Tags(bytes.NewReader(id3Data))
if err != nil {
t.Fatalf("ReadID3v2Tags: %v", err)
}
trackNum, _ := m.Track()
discNum, _ := m.Disc()
meta := &metadata.TrackMetadata{
Title: m.Title(),
Artist: m.Artist(),
Album: m.Album(),
AlbumArtist: m.AlbumArtist(),
Composer: m.Composer(),
Genre: m.Genre(),
Year: m.Year(),
TrackNumber: trackNum,
DiscNumber: discNum,
}
if pic := m.Picture(); pic != nil {
meta.Picture = &metadata.PictureData{
Data: pic.Data,
MIMEType: pic.MIMEType,
Ext: pic.Ext,
}
}
return meta
}
// createTestWAVWithExtraChunks creates a WAV file containing
// additional non-standard chunks (LIST INFO, bext) to verify that
// writeWavTags preserves chunks it does not understand.
func createTestWAVWithExtraChunks(
t *testing.T,
dir string,
name string,
) string {
t.Helper()
var buf bytes.Buffer
fmtData := makePCMFmtData()
// LIST INFO chunk: LIST + size + "INFO" + INAM sub-chunk.
var listBuf bytes.Buffer
listBuf.WriteString("INFO")
// INAM sub-chunk.
inamValue := []byte("Test Track Name")
listBuf.WriteString("INAM")
_ = binary.Write(
&listBuf, binary.LittleEndian, uint32(len(inamValue)),
)
listBuf.Write(inamValue)
// Pad INAM if odd length.
if len(inamValue)%2 != 0 {
listBuf.WriteByte(0)
}
listData := listBuf.Bytes()
// Fake bext chunk: 8 bytes of test data.
bextData := []byte{0xBE, 0xEF, 0xCA, 0xFE, 0xDE, 0xAD, 0x01, 0x02}
// 200 bytes of silence for data chunk.
const silenceLen = 200 //nolint:mnd
silence := make([]byte, silenceLen)
// Calculate RIFF payload.
riffPayload := uint32(4) // WAVE
// fmt: 8 + 16.
riffPayload += 8 + uint32(len(fmtData)) //nolint:mnd
// LIST: 8 + len(listData).
riffPayload += 8 + uint32(len(listData)) //nolint:mnd
// bext: 8 + len(bextData).
riffPayload += 8 + uint32(len(bextData)) //nolint:mnd
// data: 8 + silenceLen.
riffPayload += 8 + silenceLen //nolint:mnd
// RIFF header.
buf.WriteString("RIFF")
_ = binary.Write(&buf, binary.LittleEndian, riffPayload)
buf.WriteString("WAVE")
// fmt chunk.
buf.WriteString("fmt ")
_ = binary.Write(
&buf, binary.LittleEndian, uint32(len(fmtData)),
)
buf.Write(fmtData)
// LIST chunk.
buf.WriteString("LIST")
_ = binary.Write(
&buf, binary.LittleEndian, uint32(len(listData)),
)
buf.Write(listData)
// bext chunk.
buf.WriteString("bext")
_ = binary.Write(
&buf, binary.LittleEndian, uint32(len(bextData)),
)
buf.Write(bextData)
// data chunk.
buf.WriteString("data")
_ = binary.Write(
&buf, binary.LittleEndian, uint32(silenceLen),
)
buf.Write(silence)
path := filepath.Join(dir, name)
if err := os.WriteFile(path, buf.Bytes(), 0o644); err != nil {
t.Fatalf("write test WAV with extra chunks: %v", err)
}
return path
}