package tools
import (
"encoding/binary"
"image"
"math"
"os"
"testing"
"skraak/utils"
"github.com/madelynnblue/go-dsp/fft"
"github.com/madelynnblue/go-dsp/window"
)
const testWAV = "../audio/20211028_211500.WAV"
// ========== Individual pipeline stage benchmarks ==========
func BenchmarkReadWAV(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _, err := utils.ReadWAVSamples(testWAV)
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkExtractSegment(b *testing.B) {
samples, sr, _ := utils.ReadWAVSamples(testWAV)
b.Logf("full file: %d samples, sr=%d", len(samples), sr)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
seg := utils.ExtractSegmentSamples(samples, sr, 872, 895)
if len(seg) == 0 {
b.Fatal("empty segment")
}
}
}
func BenchmarkResampleRate(b *testing.B) {
samples, _, _ := utils.ReadWAVSamples(testWAV)
// Simulate 48kHz source by treating the 16kHz data as 48kHz
b.Logf("resampling %d samples from 48000->16000", len(samples))
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
utils.ResampleRate(samples, 48000, 16000)
}
}
func BenchmarkResampleRate250k(b *testing.B) {
samples, _, _ := utils.ReadWAVSamples(testWAV)
// Simulate 250kHz source (AudioMoth high-gain)
b.Logf("resampling %d samples from 250000->16000", len(samples))
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
utils.ResampleRate(samples, 250000, 16000)
}
}
func BenchmarkSpectrogram(b *testing.B) {
samples, sr, _ := utils.ReadWAVSamples(testWAV)
segSamples := utils.ExtractSegmentSamples(samples, sr, 872, 895)
cfg := utils.DefaultSpectrogramConfig(16000)
b.Logf("segment samples=%d, windowSize=%d, hopSize=%d", len(segSamples), cfg.WindowSize, cfg.HopSize)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
spect := utils.GenerateSpectrogram(segSamples, cfg)
if spect == nil {
b.Fatal("nil spectrogram")
}
}
}
func BenchmarkSpectrogramLong(b *testing.B) {
// 60-second segment — typical long clip
samples, sr, _ := utils.ReadWAVSamples(testWAV)
segSamples := utils.ExtractSegmentSamples(samples, sr, 0, 60)
cfg := utils.DefaultSpectrogramConfig(16000)
b.Logf("long segment samples=%d", len(segSamples))
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
spect := utils.GenerateSpectrogram(segSamples, cfg)
if spect == nil {
b.Fatal("nil spectrogram")
}
}
}
func BenchmarkCreateGrayscaleImage(b *testing.B) {
samples, sr, _ := utils.ReadWAVSamples(testWAV)
segSamples := utils.ExtractSegmentSamples(samples, sr, 872, 895)
cfg := utils.DefaultSpectrogramConfig(16000)
spect := utils.GenerateSpectrogram(segSamples, cfg)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
img := utils.CreateGrayscaleImage(spect)
if img == nil {
b.Fatal("nil image")
}
}
}
func BenchmarkCreateRGBImage(b *testing.B) {
samples, sr, _ := utils.ReadWAVSamples(testWAV)
segSamples := utils.ExtractSegmentSamples(samples, sr, 872, 895)
cfg := utils.DefaultSpectrogramConfig(16000)
spect := utils.GenerateSpectrogram(segSamples, cfg)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
colorData := utils.ApplyL4Colormap(spect)
img := utils.CreateRGBImage(colorData)
if img == nil {
b.Fatal("nil image")
}
}
}
func BenchmarkResizeGray224(b *testing.B) {
samples, sr, _ := utils.ReadWAVSamples(testWAV)
segSamples := utils.ExtractSegmentSamples(samples, sr, 872, 895)
cfg := utils.DefaultSpectrogramConfig(16000)
spect := utils.GenerateSpectrogram(segSamples, cfg)
img := utils.CreateGrayscaleImage(spect)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
resized := utils.ResizeImage(img, 224, 224)
if resized == nil {
b.Fatal("nil resize")
}
}
}
func BenchmarkResizeGray448(b *testing.B) {
samples, sr, _ := utils.ReadWAVSamples(testWAV)
segSamples := utils.ExtractSegmentSamples(samples, sr, 872, 895)
cfg := utils.DefaultSpectrogramConfig(16000)
spect := utils.GenerateSpectrogram(segSamples, cfg)
img := utils.CreateGrayscaleImage(spect)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
resized := utils.ResizeImage(img, 448, 448)
if resized == nil {
b.Fatal("nil resize")
}
}
}
func BenchmarkWritePNG(b *testing.B) {
samples, sr, _ := utils.ReadWAVSamples(testWAV)
segSamples := utils.ExtractSegmentSamples(samples, sr, 872, 895)
cfg := utils.DefaultSpectrogramConfig(16000)
spect := utils.GenerateSpectrogram(segSamples, cfg)
img := utils.CreateGrayscaleImage(spect)
resized := utils.ResizeImage(img, 224, 224)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
f, _ := os.CreateTemp("", "bench_*.png")
utils.WritePNG(resized, f)
f.Close()
os.Remove(f.Name())
}
}
func BenchmarkWriteWAV(b *testing.B) {
samples, sr, _ := utils.ReadWAVSamples(testWAV)
segSamples := utils.ExtractSegmentSamples(samples, sr, 872, 895)
b.Logf("segment samples=%d", len(segSamples))
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
f, _ := os.CreateTemp("", "bench_*.wav")
utils.WriteWAVFile(f.Name(), segSamples, sr)
f.Close()
os.Remove(f.Name())
}
}
// ========== Raw FFT benchmark (to isolate FFT cost from spectrogram overhead) ==========
func BenchmarkFFTRaw(b *testing.B) {
const windowSize = 512
hann := window.Hann(windowSize)
samples, sr, _ := utils.ReadWAVSamples(testWAV)
segSamples := utils.ExtractSegmentSamples(samples, sr, 872, 895)
numFrames := (len(segSamples)-windowSize)/256 + 1
b.Logf("frames=%d", numFrames)
frameData := make([]float64, windowSize)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for frame := 0; frame < numFrames; frame++ {
start := frame * 256
for j := 0; j < windowSize; j++ {
frameData[j] = segSamples[start+j] * hann[j]
}
fft.FFTReal(frameData)
}
}
}
// ========== convertToFloat64 isolated benchmark ==========
func BenchmarkConvertToFloat64_16bit(b *testing.B) {
// Simulate 16-bit mono WAV data (same size as test file)
numSamples := 14320000
data := make([]byte, numSamples*2)
// Fill with dummy data
for i := range numSamples {
binary.LittleEndian.PutUint16(data[i*2:], uint16(i%65536))
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = convertToFloat64Bench(data, 16, 1)
}
}
// Duplicate of convertToFloat64 for benchmarking (unexported in utils)
func convertToFloat64Bench(data []byte, bitsPerSample, channels int) []float64 {
bytesPerSample := bitsPerSample / 8
blockAlign := bytesPerSample * channels
numSamples := len(data) / blockAlign
samples := make([]float64, numSamples)
for i := range numSamples {
offset := i * blockAlign
sample := int16(binary.LittleEndian.Uint16(data[offset : offset+2]))
samples[i] = float64(sample) / 32768.0
}
return samples
}
// ========== Full pipeline benchmarks ==========
func BenchmarkFullPipelineGray224(b *testing.B) {
samples, sr, _ := utils.ReadWAVSamples(testWAV)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
segSamples := utils.ExtractSegmentSamples(samples, sr, 872, 895)
outputSR := sr
if sr > 16000 {
segSamples = utils.ResampleRate(segSamples, sr, 16000)
outputSR = 16000
}
cfg := utils.DefaultSpectrogramConfig(outputSR)
spect := utils.GenerateSpectrogram(segSamples, cfg)
img := utils.CreateGrayscaleImage(spect)
resized := utils.ResizeImage(img, 224, 224)
f, _ := os.CreateTemp("", "bench_*.png")
utils.WritePNG(resized, f)
f.Close()
os.Remove(f.Name())
utils.WriteWAVFile(f.Name(), segSamples, outputSR)
os.Remove(f.Name())
_ = resized
}
}
func BenchmarkFullPipelineColor448(b *testing.B) {
samples, sr, _ := utils.ReadWAVSamples(testWAV)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
segSamples := utils.ExtractSegmentSamples(samples, sr, 872, 895)
outputSR := sr
if sr > 16000 {
segSamples = utils.ResampleRate(segSamples, sr, 16000)
outputSR = 16000
}
cfg := utils.DefaultSpectrogramConfig(outputSR)
spect := utils.GenerateSpectrogram(segSamples, cfg)
colorData := utils.ApplyL4Colormap(spect)
img := utils.CreateRGBImage(colorData)
resized := utils.ResizeImage(img, 448, 448)
f, _ := os.CreateTemp("", "bench_*.png")
utils.WritePNG(resized, f)
f.Close()
os.Remove(f.Name())
utils.WriteWAVFile(f.Name(), segSamples, outputSR)
os.Remove(f.Name())
_ = resized
}
}
// ========== Allocation profiling: measure allocs per stage ==========
func TestPipelineAllocationSizes(t *testing.T) {
samples, sr, _ := utils.ReadWAVSamples(testWAV)
segSamples := utils.ExtractSegmentSamples(samples, sr, 872, 895)
t.Logf("Input: %d samples, sr=%d, segment=%d samples", len(samples), sr, len(segSamples))
cfg := utils.DefaultSpectrogramConfig(16000)
spect := utils.GenerateSpectrogram(segSamples, cfg)
t.Logf("Spectrogram: %d x %d (freq x time)", len(spect), len(spect[0]))
colorData := utils.ApplyL4Colormap(spect)
t.Logf("ColorData: %d x %d pixels", len(colorData), len(colorData[0]))
img := utils.CreateGrayscaleImage(spect)
bounds := img.Bounds()
t.Logf("Grayscale image: %dx%d", bounds.Dx(), bounds.Dy())
resized := utils.ResizeImage(img, 224, 224)
rBounds := resized.Bounds()
t.Logf("Resized: %dx%d", rBounds.Dx(), rBounds.Dy())
}
// ========== Math benchmarks: isolate costly operations ==========
func BenchmarkMathLog10(b *testing.B) {
vals := make([]float64, 10000)
for i := range vals {
vals[i] = float64(i+1) * 0.001
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, v := range vals {
math.Log10(v)
}
}
}
// Dummy to satisfy image.Image interface check
var _ image.Image = (*image.Gray)(nil)