package utils
import (
"bytes"
"encoding/binary"
"fmt"
"math"
"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 TestConvertToFloat64_16Bit(t *testing.T) {
data := []byte{0x01, 0x00, 0xFF, 0x7F}
samples := convertToFloat64(data, 16, 1)
if len(samples) != 2 {
t.Fatalf("expected 2 samples, got %d", len(samples))
}
if math.Abs(samples[0]-1.0/32768.0) > 1e-10 {
t.Errorf("sample[0] = %v, want %v", samples[0], 1.0/32768.0)
}
if math.Abs(samples[1]-32767.0/32768.0) > 1e-10 {
t.Errorf("sample[1] = %v, want %v", samples[1], 32767.0/32768.0)
}
}
func TestConvertToFloat64_16BitNegative(t *testing.T) {
data := []byte{0x00, 0x80}
samples := convertToFloat64(data, 16, 1)
if len(samples) != 1 {
t.Fatalf("expected 1 sample, got %d", len(samples))
}
if math.Abs(samples[0]-(-1.0)) > 1e-10 {
t.Errorf("sample = %v, want -1.0", samples[0])
}
}
func TestConvertToFloat64_16BitStereo(t *testing.T) {
data := []byte{0x01, 0x00, 0x02, 0x00}
samples := convertToFloat64(data, 16, 2)
if len(samples) != 1 {
t.Fatalf("expected 1 sample (left only), got %d", len(samples))
}
if math.Abs(samples[0]-1.0/32768.0) > 1e-10 {
t.Errorf("sample = %v, want %v", samples[0], 1.0/32768.0)
}
}
func TestConvertToFloat64_24Bit(t *testing.T) {
data := []byte{0x01, 0x00, 0x00}
samples := convertToFloat64(data, 24, 1)
if len(samples) != 1 {
t.Fatalf("expected 1 sample, got %d", len(samples))
}
if math.Abs(samples[0]-1.0/8388608.0) > 1e-12 {
t.Errorf("sample = %v, want %v", samples[0], 1.0/8388608.0)
}
}
func TestConvertToFloat64_24BitNegative(t *testing.T) {
data := []byte{0xFF, 0xFF, 0xFF}
samples := convertToFloat64(data, 24, 1)
if len(samples) != 1 {
t.Fatalf("expected 1 sample, got %d", len(samples))
}
if math.Abs(samples[0]-(-1.0/8388608.0)) > 1e-12 {
t.Errorf("sample = %v, want %v", samples[0], -1.0/8388608.0)
}
}
func TestConvertToFloat64_24BitMaxPositive(t *testing.T) {
data := []byte{0xFF, 0xFF, 0x7F}
samples := convertToFloat64(data, 24, 1)
if len(samples) != 1 {
t.Fatalf("expected 1 sample, got %d", len(samples))
}
if math.Abs(samples[0]-8388607.0/8388608.0) > 1e-12 {
t.Errorf("sample = %v, want %v", samples[0], 8388607.0/8388608.0)
}
}
func TestConvertToFloat64_24BitMinNegative(t *testing.T) {
data := []byte{0x00, 0x00, 0x80}
samples := convertToFloat64(data, 24, 1)
if len(samples) != 1 {
t.Fatalf("expected 1 sample, got %d", len(samples))
}
if math.Abs(samples[0]-(-1.0)) > 1e-12 {
t.Errorf("sample = %v, want -1.0", samples[0])
}
}
func TestConvertToFloat64_24BitStereo(t *testing.T) {
data := []byte{0x01, 0x00, 0x00, 0x02, 0x00, 0x00}
samples := convertToFloat64(data, 24, 2)
if len(samples) != 1 {
t.Fatalf("expected 1 sample (left only), got %d", len(samples))
}
if math.Abs(samples[0]-1.0/8388608.0) > 1e-12 {
t.Errorf("sample = %v, want %v", samples[0], 1.0/8388608.0)
}
}
func TestConvertToFloat64_32Bit(t *testing.T) {
data := []byte{0x01, 0x00, 0x00, 0x00}
samples := convertToFloat64(data, 32, 1)
if len(samples) != 1 {
t.Fatalf("expected 1 sample, got %d", len(samples))
}
if math.Abs(samples[0]-1.0/2147483648.0) > 1e-15 {
t.Errorf("sample = %v, want %v", samples[0], 1.0/2147483648.0)
}
}
func TestConvertToFloat64_32BitNegative(t *testing.T) {
data := []byte{0x00, 0x00, 0x00, 0x80}
samples := convertToFloat64(data, 32, 1)
if len(samples) != 1 {
t.Fatalf("expected 1 sample, got %d", len(samples))
}
if math.Abs(samples[0]-(-1.0)) > 1e-15 {
t.Errorf("sample = %v, want -1.0", samples[0])
}
}
func TestConvertToFloat64_32BitMaxPositive(t *testing.T) {
data := []byte{0xFF, 0xFF, 0xFF, 0x7F}
samples := convertToFloat64(data, 32, 1)
if len(samples) != 1 {
t.Fatalf("expected 1 sample, got %d", len(samples))
}
if math.Abs(samples[0]-2147483647.0/2147483648.0) > 1e-15 {
t.Errorf("sample = %v, want %v", samples[0], 2147483647.0/2147483648.0)
}
}
func TestConvertToFloat64_32BitStereo(t *testing.T) {
data := []byte{0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00}
samples := convertToFloat64(data, 32, 2)
if len(samples) != 1 {
t.Fatalf("expected 1 sample (left only), got %d", len(samples))
}
if math.Abs(samples[0]-1.0/2147483648.0) > 1e-15 {
t.Errorf("sample = %v, want %v", samples[0], 1.0/2147483648.0)
}
}
func TestConvertToFloat64_UnsupportedBitDepth(t *testing.T) {
data := []byte{0x01, 0x00, 0x02, 0x00} samples := convertToFloat64(data, 8, 2) if len(samples) != 2 {
t.Fatalf("expected 2 samples (fallback 16-bit), got %d", len(samples))
}
}
func TestConvertToFloat64_EmptyData(t *testing.T) {
samples := convertToFloat64([]byte{}, 16, 1)
if len(samples) != 0 {
t.Errorf("expected 0 samples, got %d", len(samples))
}
}
func TestParseWAVHeader_BasicMetadata(t *testing.T) {
tmpDir := t.TempDir()
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)
}
}
func TestParseWAVHeader_CommentMetadata(t *testing.T) {
tmpDir := t.TempDir()
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)
}
}
func TestParseWAVHeader_ArtistMetadata(t *testing.T) {
tmpDir := t.TempDir()
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)
}
}
func TestParseWAVHeader_CommentAndArtist(t *testing.T) {
tmpDir := t.TempDir()
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)
}
}
func TestParseWAVHeader_SampleRates(t *testing.T) {
tmpDir := t.TempDir()
testCases := []int{8000, 16000, 22050, 44100, 48000, 96000}
for _, sr := range testCases {
t.Run(fmt.Sprintf("%dHz", sr), func(t *testing.T) {
path := createTestWAVFile(t, tmpDir, fmt.Sprintf("test_sr_%d.wav", sr), struct {
duration float64
sampleRate int
channels int
bitsPerSample int
comment string
artist string
}{
duration: 1.0,
sampleRate: sr,
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 != sr {
t.Errorf("SampleRate incorrect: got %d, want %d", metadata.SampleRate, sr)
}
})
}
}
func TestParseWAVHeader_ChannelCounts(t *testing.T) {
tmpDir := t.TempDir()
testCases := []int{1, 2}
for _, ch := range testCases {
t.Run(fmt.Sprintf("%dch", ch), func(t *testing.T) {
path := createTestWAVFile(t, tmpDir, fmt.Sprintf("test_ch_%d.wav", ch), struct {
duration float64
sampleRate int
channels int
bitsPerSample int
comment string
artist string
}{
duration: 1.0,
sampleRate: 44100,
channels: ch,
bitsPerSample: 16,
comment: "",
artist: "",
})
metadata, err := ParseWAVHeader(path)
if err != nil {
t.Fatalf("Failed to parse WAV header: %v", err)
}
if metadata.Channels != ch {
t.Errorf("Channels incorrect: got %d, want %d", metadata.Channels, ch)
}
})
}
}
func TestParseWAVHeader_BitDepths(t *testing.T) {
tmpDir := t.TempDir()
testCases := []int{8, 16, 24, 32}
for _, bits := range testCases {
t.Run(fmt.Sprintf("%dbit", bits), func(t *testing.T) {
path := createTestWAVFile(t, tmpDir, fmt.Sprintf("test_bits_%d.wav", bits), struct {
duration float64
sampleRate int
channels int
bitsPerSample int
comment string
artist string
}{
duration: 1.0,
sampleRate: 44100,
channels: 1,
bitsPerSample: bits,
comment: "",
artist: "",
})
metadata, err := ParseWAVHeader(path)
if err != nil {
t.Fatalf("Failed to parse WAV header: %v", err)
}
if metadata.BitsPerSample != bits {
t.Errorf("BitsPerSample incorrect: got %d, want %d", metadata.BitsPerSample, bits)
}
})
}
}
func TestParseWAVHeader_ShortDuration(t *testing.T) {
tmpDir := t.TempDir()
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)
}
}
func TestParseWAVHeader_LongDuration(t *testing.T) {
tmpDir := t.TempDir()
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)
}
}
func TestParseWAVHeader_NonExistentFile(t *testing.T) {
_, err := ParseWAVHeader("/nonexistent/file.wav")
if err == nil {
t.Error("Expected error for non-existent file")
}
}
func TestParseWAVHeader_NonWAVFile(t *testing.T) {
tmpDir := t.TempDir()
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")
}
}
func TestParseWAVHeader_TruncatedFile(t *testing.T) {
tmpDir := t.TempDir()
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")
}
}
func TestParseWAVHeader_EmptyMetadataStrings(t *testing.T) {
tmpDir := t.TempDir()
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)
}
}
func TestParseWAVHeader_LongCommentString(t *testing.T) {
tmpDir := t.TempDir()
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)
}
}
func TestParseWAVHeader_FileModTime(t *testing.T) {
tmpDir := t.TempDir()
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)
}
})
}
}
func assertWAVHeader(t *testing.T, tmpDir, filename string, wantSR int, wantDur float64, opts struct {
duration float64
sampleRate int
channels int
bitsPerSample int
comment string
artist string
}) {
t.Helper()
path := createTestWAVFile(t, tmpDir, filename, opts)
sr, dur, err := ParseWAVHeaderMinimal(path)
if err != nil {
t.Fatalf("Failed to parse WAV header: %v", err)
}
if sr != wantSR {
t.Errorf("SampleRate: got %d, want %d", sr, wantSR)
}
if dur < wantDur-0.1 || dur > wantDur+0.1 {
t.Errorf("Duration: got %f, want ~%f", dur, wantDur)
}
}
func TestParseWAVHeaderMinimal(t *testing.T) {
tmpDir := t.TempDir()
t.Run("basic", func(t *testing.T) {
assertWAVHeader(t, tmpDir, "test_minimal.wav", 44100, 10.0, struct {
duration float64
sampleRate int
channels int
bitsPerSample int
comment string
artist string
}{10.0, 44100, 1, 16, "", ""})
})
t.Run("sample_rates", func(t *testing.T) {
for _, sr := range []int{8000, 22050, 44100, 48000, 96000} {
t.Run(fmt.Sprintf("%dHz", sr), func(t *testing.T) {
assertWAVHeader(t, tmpDir, fmt.Sprintf("test_sr_%d.wav", sr), sr, 5.0, struct {
duration float64
sampleRate int
channels int
bitsPerSample int
comment string
artist string
}{5.0, sr, 1, 16, "", ""})
})
}
})
t.Run("stereo", func(t *testing.T) {
assertWAVHeader(t, tmpDir, "test_stereo.wav", 44100, 3.0, struct {
duration float64
sampleRate int
channels int
bitsPerSample int
comment string
artist string
}{3.0, 44100, 2, 16, "", ""})
})
t.Run("nonexistent", func(t *testing.T) {
_, _, err := ParseWAVHeaderMinimal("/nonexistent/file.wav")
if err == nil {
t.Error("Expected error for non-existent file")
}
})
t.Run("non_wav", func(t *testing.T) {
path := filepath.Join(tmpDir, "notawav.wav")
if err := os.WriteFile(path, []byte("Not a WAV file"), 0644); err != nil {
t.Fatalf("Failed to create test file: %v", err)
}
_, _, err := ParseWAVHeaderMinimal(path)
if err == nil {
t.Error("Expected error for non-WAV file")
}
})
}