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:
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user