package utils
import (
"bytes"
"encoding/binary"
"os"
"path/filepath"
"testing"
"time"
)
func createTestWAVFile(t *testing.T, dir string, filename string, options struct {
duration float64
sampleRate int
channels int
bitsPerSample int
comment string
artist string
}) string {
t.Helper()
path := filepath.Join(dir, filename)
file, err := os.Create(path)
if err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
defer file.Close()
bytesPerSample := options.bitsPerSample / 8
samplesPerSecond := options.sampleRate * options.channels
dataSize := int(options.duration * float64(samplesPerSecond*bytesPerSample))
fileSize := 4 + 8 + 16 + 8 + dataSize
var infoChunk []byte
if options.comment != "" || options.artist != "" {
infoChunk = buildINFOChunk(options.comment, options.artist)
fileSize += 8 + len(infoChunk) }
buf := &bytes.Buffer{}
buf.WriteString("RIFF")
binary.Write(buf, binary.LittleEndian, uint32(fileSize))
buf.WriteString("WAVE")
buf.WriteString("fmt ")
binary.Write(buf, binary.LittleEndian, uint32(16)) binary.Write(buf, binary.LittleEndian, uint16(1)) binary.Write(buf, binary.LittleEndian, uint16(options.channels))
binary.Write(buf, binary.LittleEndian, uint32(options.sampleRate))
byteRate := options.sampleRate * options.channels * bytesPerSample
binary.Write(buf, binary.LittleEndian, uint32(byteRate))
blockAlign := options.channels * bytesPerSample
binary.Write(buf, binary.LittleEndian, uint16(blockAlign))
binary.Write(buf, binary.LittleEndian, uint16(options.bitsPerSample))
if len(infoChunk) > 0 {
buf.WriteString("LIST")
binary.Write(buf, binary.LittleEndian, uint32(len(infoChunk)))
buf.Write(infoChunk)
}
buf.WriteString("data")
binary.Write(buf, binary.LittleEndian, uint32(dataSize))
buf.Write(make([]byte, dataSize))
if _, err := file.Write(buf.Bytes()); err != nil {
t.Fatalf("Failed to write test file: %v", err)
}
return path
}
func buildINFOChunk(comment, artist string) []byte {
buf := &bytes.Buffer{}
buf.WriteString("INFO")
if comment != "" {
buf.WriteString("ICMT")
size := len(comment) + 1
binary.Write(buf, binary.LittleEndian, uint32(size))
buf.WriteString(comment)
buf.WriteByte(0) if size%2 != 0 {
buf.WriteByte(0)
}
}
if artist != "" {
buf.WriteString("IART")
size := len(artist) + 1
binary.Write(buf, binary.LittleEndian, uint32(size))
buf.WriteString(artist)
buf.WriteByte(0) if size%2 != 0 {
buf.WriteByte(0)
}
}
return buf.Bytes()
}
func TestParseWAVHeader(t *testing.T) {
tmpDir := t.TempDir()
t.Run("should parse basic WAV metadata", func(t *testing.T) {
path := createTestWAVFile(t, tmpDir, "test_basic.wav", struct {
duration float64
sampleRate int
channels int
bitsPerSample int
comment string
artist string
}{
duration: 60.0,
sampleRate: 44100,
channels: 2,
bitsPerSample: 16,
comment: "",
artist: "",
})
metadata, err := ParseWAVHeader(path)
if err != nil {
t.Fatalf("Failed to parse WAV header: %v", err)
}
if metadata.SampleRate != 44100 {
t.Errorf("SampleRate incorrect: got %d, want 44100", metadata.SampleRate)
}
if metadata.Channels != 2 {
t.Errorf("Channels incorrect: got %d, want 2", metadata.Channels)
}
if metadata.BitsPerSample != 16 {
t.Errorf("BitsPerSample incorrect: got %d, want 16", metadata.BitsPerSample)
}
if metadata.Duration < 59.9 || metadata.Duration > 60.1 {
t.Errorf("Duration incorrect: got %f, want ~60.0", metadata.Duration)
}
})
t.Run("should extract comment metadata", func(t *testing.T) {
expectedComment := "Recorded at 21:00:00 24/02/2025 (UTC+13) by AudioMoth 248AB50153AB0549"
path := createTestWAVFile(t, tmpDir, "test_comment.wav", struct {
duration float64
sampleRate int
channels int
bitsPerSample int
comment string
artist string
}{
duration: 10.0,
sampleRate: 48000,
channels: 1,
bitsPerSample: 16,
comment: expectedComment,
artist: "",
})
metadata, err := ParseWAVHeader(path)
if err != nil {
t.Fatalf("Failed to parse WAV header: %v", err)
}
if metadata.Comment != expectedComment {
t.Errorf("Comment incorrect: got %q, want %q", metadata.Comment, expectedComment)
}
})
t.Run("should extract artist metadata", func(t *testing.T) {
expectedArtist := "AudioMoth"
path := createTestWAVFile(t, tmpDir, "test_artist.wav", struct {
duration float64
sampleRate int
channels int
bitsPerSample int
comment string
artist string
}{
duration: 5.0,
sampleRate: 48000,
channels: 1,
bitsPerSample: 16,
comment: "",
artist: expectedArtist,
})
metadata, err := ParseWAVHeader(path)
if err != nil {
t.Fatalf("Failed to parse WAV header: %v", err)
}
if metadata.Artist != expectedArtist {
t.Errorf("Artist incorrect: got %q, want %q", metadata.Artist, expectedArtist)
}
})
t.Run("should extract both comment and artist", func(t *testing.T) {
expectedComment := "Test recording comment"
expectedArtist := "Test Artist"
path := createTestWAVFile(t, tmpDir, "test_both.wav", struct {
duration float64
sampleRate int
channels int
bitsPerSample int
comment string
artist string
}{
duration: 15.0,
sampleRate: 44100,
channels: 2,
bitsPerSample: 16,
comment: expectedComment,
artist: expectedArtist,
})
metadata, err := ParseWAVHeader(path)
if err != nil {
t.Fatalf("Failed to parse WAV header: %v", err)
}
if metadata.Comment != expectedComment {
t.Errorf("Comment incorrect: got %q, want %q", metadata.Comment, expectedComment)
}
if metadata.Artist != expectedArtist {
t.Errorf("Artist incorrect: got %q, want %q", metadata.Artist, expectedArtist)
}
})
t.Run("should handle different sample rates", func(t *testing.T) {
testCases := []struct {
sampleRate int
}{
{8000},
{16000},
{22050},
{44100},
{48000},
{96000},
}
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
path := createTestWAVFile(t, tmpDir, "test_sr.wav", struct {
duration float64
sampleRate int
channels int
bitsPerSample int
comment string
artist string
}{
duration: 1.0,
sampleRate: tc.sampleRate,
channels: 1,
bitsPerSample: 16,
comment: "",
artist: "",
})
metadata, err := ParseWAVHeader(path)
if err != nil {
t.Fatalf("Failed to parse WAV header: %v", err)
}
if metadata.SampleRate != tc.sampleRate {
t.Errorf("SampleRate incorrect: got %d, want %d", metadata.SampleRate, tc.sampleRate)
}
})
}
})
t.Run("should handle different channel counts", func(t *testing.T) {
testCases := []struct {
channels int
}{
{1}, {2}, }
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
path := createTestWAVFile(t, tmpDir, "test_ch.wav", struct {
duration float64
sampleRate int
channels int
bitsPerSample int
comment string
artist string
}{
duration: 1.0,
sampleRate: 44100,
channels: tc.channels,
bitsPerSample: 16,
comment: "",
artist: "",
})
metadata, err := ParseWAVHeader(path)
if err != nil {
t.Fatalf("Failed to parse WAV header: %v", err)
}
if metadata.Channels != tc.channels {
t.Errorf("Channels incorrect: got %d, want %d", metadata.Channels, tc.channels)
}
})
}
})
t.Run("should handle different bit depths", func(t *testing.T) {
testCases := []struct {
bitsPerSample int
}{
{8},
{16},
{24},
{32},
}
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
path := createTestWAVFile(t, tmpDir, "test_bits.wav", struct {
duration float64
sampleRate int
channels int
bitsPerSample int
comment string
artist string
}{
duration: 1.0,
sampleRate: 44100,
channels: 1,
bitsPerSample: tc.bitsPerSample,
comment: "",
artist: "",
})
metadata, err := ParseWAVHeader(path)
if err != nil {
t.Fatalf("Failed to parse WAV header: %v", err)
}
if metadata.BitsPerSample != tc.bitsPerSample {
t.Errorf("BitsPerSample incorrect: got %d, want %d", metadata.BitsPerSample, tc.bitsPerSample)
}
})
}
})
t.Run("should handle very short durations", func(t *testing.T) {
path := createTestWAVFile(t, tmpDir, "test_short.wav", struct {
duration float64
sampleRate int
channels int
bitsPerSample int
comment string
artist string
}{
duration: 0.1, sampleRate: 44100,
channels: 1,
bitsPerSample: 16,
comment: "",
artist: "",
})
metadata, err := ParseWAVHeader(path)
if err != nil {
t.Fatalf("Failed to parse WAV header: %v", err)
}
if metadata.Duration < 0.09 || metadata.Duration > 0.11 {
t.Errorf("Duration incorrect: got %f, want ~0.1", metadata.Duration)
}
})
t.Run("should handle long durations", func(t *testing.T) {
path := createTestWAVFile(t, tmpDir, "test_long.wav", struct {
duration float64
sampleRate int
channels int
bitsPerSample int
comment string
artist string
}{
duration: 600.0, sampleRate: 44100,
channels: 1,
bitsPerSample: 16,
comment: "",
artist: "",
})
metadata, err := ParseWAVHeader(path)
if err != nil {
t.Fatalf("Failed to parse WAV header: %v", err)
}
if metadata.Duration < 599.0 || metadata.Duration > 601.0 {
t.Errorf("Duration incorrect: got %f, want ~600.0", metadata.Duration)
}
})
t.Run("should return error for non-existent file", func(t *testing.T) {
_, err := ParseWAVHeader("/nonexistent/file.wav")
if err == nil {
t.Error("Expected error for non-existent file")
}
})
t.Run("should return error for non-WAV file", func(t *testing.T) {
path := filepath.Join(tmpDir, "not_a_wav.txt")
if err := os.WriteFile(path, []byte("This is not a WAV file"), 0644); err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
_, err := ParseWAVHeader(path)
if err == nil {
t.Error("Expected error for non-WAV file")
}
})
t.Run("should return error for truncated file", func(t *testing.T) {
path := filepath.Join(tmpDir, "truncated.wav")
if err := os.WriteFile(path, []byte("RIFF"), 0644); err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
_, err := ParseWAVHeader(path)
if err == nil {
t.Error("Expected error for truncated file")
}
})
t.Run("should handle empty metadata strings", func(t *testing.T) {
path := createTestWAVFile(t, tmpDir, "test_empty.wav", struct {
duration float64
sampleRate int
channels int
bitsPerSample int
comment string
artist string
}{
duration: 10.0,
sampleRate: 44100,
channels: 1,
bitsPerSample: 16,
comment: "",
artist: "",
})
metadata, err := ParseWAVHeader(path)
if err != nil {
t.Fatalf("Failed to parse WAV header: %v", err)
}
if metadata.Comment != "" {
t.Errorf("Comment should be empty, got %q", metadata.Comment)
}
if metadata.Artist != "" {
t.Errorf("Artist should be empty, got %q", metadata.Artist)
}
})
t.Run("should handle long comment strings", func(t *testing.T) {
longComment := "Recorded at 21:00:00 24/02/2025 (UTC+13) by AudioMoth 248AB50153AB0549 at medium gain while battery was 4.3V and temperature was 15.8C. This is a very long comment with additional information about the recording session."
path := createTestWAVFile(t, tmpDir, "test_long_comment.wav", struct {
duration float64
sampleRate int
channels int
bitsPerSample int
comment string
artist string
}{
duration: 10.0,
sampleRate: 44100,
channels: 1,
bitsPerSample: 16,
comment: longComment,
artist: "",
})
metadata, err := ParseWAVHeader(path)
if err != nil {
t.Fatalf("Failed to parse WAV header: %v", err)
}
if metadata.Comment != longComment {
t.Errorf("Comment incorrect: got %q, want %q", metadata.Comment, longComment)
}
})
t.Run("should extract file modification time", func(t *testing.T) {
path := createTestWAVFile(t, tmpDir, "test_modtime.wav", struct {
duration float64
sampleRate int
channels int
bitsPerSample int
comment string
artist string
}{
duration: 5.0,
sampleRate: 44100,
channels: 1,
bitsPerSample: 16,
comment: "",
artist: "",
})
info, err := os.Stat(path)
if err != nil {
t.Fatalf("Failed to stat file: %v", err)
}
expectedModTime := info.ModTime()
metadata, err := ParseWAVHeader(path)
if err != nil {
t.Fatalf("Failed to parse WAV header: %v", err)
}
diff := metadata.FileModTime.Sub(expectedModTime)
if diff < -1*time.Second || diff > 1*time.Second {
t.Errorf("FileModTime incorrect: got %v, want %v (diff: %v)",
metadata.FileModTime, expectedModTime, diff)
}
if metadata.FileModTime.IsZero() {
t.Error("FileModTime should not be zero")
}
})
}
func TestExtractNullTerminatedString(t *testing.T) {
testCases := []struct {
name string
input []byte
expected string
}{
{
name: "string with null terminator",
input: []byte{'h', 'e', 'l', 'l', 'o', 0, 'w', 'o', 'r', 'l', 'd'},
expected: "hello",
},
{
name: "string without null terminator",
input: []byte{'h', 'e', 'l', 'l', 'o'},
expected: "hello",
},
{
name: "empty string",
input: []byte{},
expected: "",
},
{
name: "only null terminator",
input: []byte{0},
expected: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := extractNullTerminatedString(tc.input)
if result != tc.expected {
t.Errorf("Result incorrect: got %q, want %q", result, tc.expected)
}
})
}
}