Files
yellowjacket/backend/metadata/flacduration_test.go
T

307 lines
6.8 KiB
Go

package metadata
import (
"os"
"path/filepath"
"testing"
)
// testFlacFiles returns the paths to all .flac files in the
// test_data directory. It skips the test if none are found.
func testFlacFiles(t *testing.T) []string {
t.Helper()
root := filepath.Join("..", "..", "test_data")
var files []string
err := filepath.Walk(root, func(
path string, info os.FileInfo, err error,
) error {
if err != nil {
return err
}
if !info.IsDir() && filepath.Ext(path) == ".flac" {
files = append(files, path)
}
return nil
})
if err != nil {
t.Fatalf("walking test_data: %v", err)
}
if len(files) == 0 {
t.Skip("no .flac test fixtures found in test_data/")
}
return files
}
// TestGetFlacDuration_BasicParsing verifies that getFlacDuration
// returns a positive duration for every FLAC test fixture.
func TestGetFlacDuration_BasicParsing(t *testing.T) {
for _, path := range testFlacFiles(t) {
t.Run(filepath.Base(path), func(t *testing.T) {
f, err := os.Open(path)
if err != nil {
t.Fatalf("open: %v", err)
}
defer func() { _ = f.Close() }()
ms, props, err := getFlacDuration(f)
if err != nil {
t.Fatalf("getFlacDuration: %v", err)
}
if ms <= 0 {
t.Errorf(
"expected positive duration, got %d",
ms,
)
}
if props == nil {
t.Fatal("expected non-nil AudioProperties")
}
if props.SampleRate <= 0 {
t.Errorf(
"expected positive sample rate, got %d",
props.SampleRate,
)
}
if props.BitDepth <= 0 {
t.Errorf(
"expected positive bit depth, got %d",
props.BitDepth,
)
}
if props.Channels <= 0 {
t.Errorf(
"expected positive channels, got %d",
props.Channels,
)
}
t.Logf(
"duration: %dms rate: %dHz depth: %d ch: %d",
ms, props.SampleRate, props.BitDepth,
props.Channels,
)
})
}
}
// TestGetFlacDuration_MatchesBeepDecode verifies that the fast
// header-only parser produces a duration within 1 second of the full
// decode via beep, for every FLAC test fixture.
func TestGetFlacDuration_MatchesBeepDecode(t *testing.T) {
for _, path := range testFlacFiles(t) {
t.Run(filepath.Base(path), func(t *testing.T) {
refMS, err := GetTrackLengthMillis(path)
if err != nil {
t.Fatalf("beep decode failed: %v", err)
}
f, err := os.Open(path)
if err != nil {
t.Fatalf("open: %v", err)
}
defer func() { _ = f.Close() }()
fastMS, _, err := getFlacDuration(f)
if err != nil {
t.Fatalf("getFlacDuration: %v", err)
}
diffMS := refMS - fastMS
if diffMS < 0 {
diffMS = -diffMS
}
const toleranceMS = 1000
t.Logf(
"beep=%dms fast=%dms diff=%dms",
refMS, fastMS, diffMS,
)
if diffMS > toleranceMS {
t.Errorf(
"duration mismatch: beep=%dms "+
"fast=%dms (diff %dms "+
"exceeds %dms tolerance)",
refMS, fastMS, diffMS, toleranceMS,
)
}
})
}
}
// TestGetFlacDuration_WithPrependedID3v2 creates a temporary FLAC
// file with a synthetic ID3v2 tag prepended and verifies that
// getFlacDuration correctly skips it and parses the duration.
func TestGetFlacDuration_WithPrependedID3v2(t *testing.T) {
files := testFlacFiles(t)
// Use the first test fixture as our source.
src := files[0]
srcData, err := os.ReadFile(src)
if err != nil {
t.Fatalf("reading source: %v", err)
}
// Build a minimal ID3v2.3 header with 256 bytes of padding.
//nolint:mnd // synthetic tag construction.
paddingSize := 256
id3Header := buildID3v2Header(paddingSize)
// Write: ID3v2 header + padding + original FLAC data.
tmpDir := t.TempDir()
tmpPath := filepath.Join(tmpDir, "test_id3v2.flac")
out := make([]byte, 0, len(id3Header)+paddingSize+len(srcData))
out = append(out, id3Header...)
out = append(out, make([]byte, paddingSize)...)
out = append(out, srcData...)
if err := os.WriteFile(tmpPath, out, 0o644); err != nil {
t.Fatalf("writing temp file: %v", err)
}
// Get reference duration from original file.
origF, err := os.Open(src)
if err != nil {
t.Fatalf("open original: %v", err)
}
defer func() { _ = origF.Close() }()
origMS, _, err := getFlacDuration(origF)
if err != nil {
t.Fatalf("getFlacDuration on original: %v", err)
}
// Parse the ID3v2-wrapped file.
tmpF, err := os.Open(tmpPath)
if err != nil {
t.Fatalf("open temp: %v", err)
}
defer func() { _ = tmpF.Close() }()
wrappedMS, _, err := getFlacDuration(tmpF)
if err != nil {
t.Fatalf(
"getFlacDuration on ID3v2-wrapped file: %v", err,
)
}
if origMS != wrappedMS {
t.Errorf(
"duration mismatch: original=%dms wrapped=%dms",
origMS, wrappedMS,
)
}
t.Logf(
"original=%dms wrapped=%dms", origMS, wrappedMS,
)
}
// TestParseFlacStreamInfo verifies the bit-level parsing of sample
// rate and total samples from a known StreamInfo block.
func TestParseFlacStreamInfo(t *testing.T) {
// Construct a 34-byte StreamInfo with known values.
// Layout of bytes 10-17 (64 bits, big-endian):
// bits 0-19: sample rate (20 bits)
// bits 20-22: channels - 1 (3 bits)
// bits 23-27: bps - 1 (5 bits)
// bits 28-63: total samples (36 bits)
//
// Test values:
// sample rate = 44100 (0x0AC44)
// channels = 2 (stored as 1, 0b001)
// bps = 16 (stored as 15, 0b01111)
// total samples = 11614366 (0x00B1389E)
//
// Packed: 0x0AC442F000B1389E
// byte 10 = 0x0A byte 14 = 0x00
// byte 11 = 0xC4 byte 15 = 0xB1
// byte 12 = 0x42 byte 16 = 0x38
// byte 13 = 0xF0 byte 17 = 0x9E
//
//nolint:mnd // byte values from manual FLAC spec packing.
var si [streamInfoLength]byte
si[10] = 0x0A
si[11] = 0xC4
si[12] = 0x42
si[13] = 0xF0
si[14] = 0x00
si[15] = 0xB1
si[16] = 0x38
si[17] = 0x9E
sr, total, ch, bps := parseFlacStreamInfo(si)
//nolint:mnd // expected test values.
const (
wantSR = 44100
wantTotal = 11614366
wantChannels = 2
wantBPS = 16
)
if sr != wantSR {
t.Errorf("sample rate: got %d, want %d", sr, wantSR)
}
if total != wantTotal {
t.Errorf(
"total samples: got %d, want %d",
total, wantTotal,
)
}
if ch != wantChannels {
t.Errorf(
"channels: got %d, want %d", ch, wantChannels,
)
}
if bps != wantBPS {
t.Errorf(
"bits per sample: got %d, want %d", bps, wantBPS,
)
}
}
// buildID3v2Header creates a minimal 10-byte ID3v2.3 header with
// the given payload size encoded as a syncsafe integer.
//
//nolint:mnd // byte offsets from the ID3v2 spec.
func buildID3v2Header(payloadSize int) []byte {
header := []byte{
'I', 'D', '3', // signature
3, 0, // version 2.3.0
0, // flags
0, 0, 0, 0, // size (syncsafe, filled below)
}
header[6] = byte((payloadSize >> 21) & 0x7F)
header[7] = byte((payloadSize >> 14) & 0x7F)
header[8] = byte((payloadSize >> 7) & 0x7F)
header[9] = byte(payloadSize & 0x7F)
return header
}