package metadata import ( "os" "path/filepath" "testing" "yellowjacket/internal/testfixtures" ) // testFlacFiles returns every .flac in the generated fixture library // (`make testdata`). // // Sourced from the manifest rather than by walking test_data/, which // used to sweep up the deliberately malformed fixtures — a zero-byte // .flac is there to prove the scanner survives it, not to be handed to // a duration parser. func testFlacFiles(t *testing.T) []string { t.Helper() m := testfixtures.Load(t) var files []string for _, track := range m.Tracks { if track.Format == "flac" { files = append(files, m.Abs(track.Path)) } } if len(files) == 0 { t.Skip("no .flac fixtures in the manifest") } 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") return } 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 }