OFL26ZLGKGJERNHPURO6CBN4FT3VN5MLASOAPXO6GJVCMSMCLPUAC F6A736FQ4WKBW4G6IYCZTTGN43HKZ2JHIOHX2BW3TPHATCTERAGQC 3JA7HYRMHV57SIMGMGPDOMKQ3NBQS2SKOX3EKDHRBQRP7ZPZGFTQC 2IURSWW3ZXRBH3DPJO437YE2FGMG54MPFL6W6UCEF5HODPVWVVTQC U6JEEU5O477ZOJ5UMRMOJSGPEJEU6Q7KMPKUSDF56CYVUJWL7QBQC EW7VBNMGWFBC73ZUDLB4LIK2HWFKA74ZUTUDG4J575ZQHEFHW4UQC DBOROCRFD6A5SJBMFYFEJI5S5M77X4EFEK6KDQWA5QDMQJKIHRWQC 5KIKDA72HM6JFIPKOWGLM2EO7D5PTSK7WEVYV3YZWGMG3M34PJXQC W3A2EECCD23SVHJZN6MXPH2PAVFHH5CNFD2XHPQRRW6M4GUTG3FAC func TestResampleRate(t *testing.T) {t.Run("should return same samples for same rate", func(t *testing.T) {samples := []float64{0.1, 0.2, 0.3, 0.4, 0.5}result := ResampleRate(samples, 16000, 16000)if len(result) != len(samples) {t.Errorf("length mismatch: got %d, want %d", len(result), len(samples))}for i := range samples {if result[i] != samples[i] {t.Errorf("sample %d mismatch: got %f, want %f", i, result[i], samples[i])}}})t.Run("should downsample from 250000 to 16000", func(t *testing.T) {// 250000 / 16000 = 15.625 ratiosamples := make([]float64, 2500) // 0.01 seconds at 250kHzfor i := range samples {samples[i] = float64(i) / float64(len(samples))}result := ResampleRate(samples, 250000, 16000)expectedLen := 160 // 0.01 seconds at 16kHzif len(result) != expectedLen {t.Errorf("length mismatch: got %d, want %d", len(result), expectedLen)}})t.Run("should downsample from 44100 to 16000", func(t *testing.T) {// 44100 / 16000 = 2.75625 ratiosamples := make([]float64, 441) // 0.01 seconds at 44.1kHzfor i := range samples {samples[i] = float64(i) / float64(len(samples))}result := ResampleRate(samples, 44100, 16000)expectedLen := 160 // 0.01 seconds at 16kHzif len(result) != expectedLen {t.Errorf("length mismatch: got %d, want %d", len(result), expectedLen)}})t.Run("should preserve signal shape", func(t *testing.T) {// Create a simple ramp signalsamples := []float64{0.0, 0.25, 0.5, 0.75, 1.0}result := ResampleRate(samples, 50000, 16000)// Should still be a roughly increasing signalfor i := 1; i < len(result); i++ {if result[i] < result[i-1]-0.1 {t.Errorf("signal not preserved: result[%d]=%f < result[%d]=%f", i, result[i], i-1, result[i-1])}}})
// ResampleRate converts samples from one sample rate to another using linear interpolation.// This is used to downsample high sample rate audio for spectrogram visualization.// fromRate: original sample rate (e.g., 250000)// toRate: target sample rate (e.g., 16000)func ResampleRate(samples []float64, fromRate, toRate int) []float64 {if fromRate == toRate || len(samples) == 0 {return samples}// Calculate ratio: toRate/fromRate (e.g., 16000/250000 = 0.064)ratio := float64(toRate) / float64(fromRate)newLen := int(float64(len(samples)) * ratio)if newLen <= 0 {return samples}result := make([]float64, newLen)for i := 0; i < newLen; i++ {// Source index in original samples (floating point)srcIdx := float64(i) / ratioidx0 := int(srcIdx)idx1 := idx0 + 1// Clamp to valid rangeif idx0 >= len(samples) {idx0 = len(samples) - 1}if idx1 >= len(samples) {idx1 = len(samples) - 1}// Linear interpolation between adjacent samplesfrac := srcIdx - float64(idx0)result[i] = samples[idx0]*(1-frac) + samples[idx1]*frac}
config := utils.DefaultSpectrogramConfig(sampleRate)
// For spectrograms, downsample if sample rate exceeds 16kHzspectSampleRate := sampleRateif sampleRate > utils.DefaultMaxSampleRate {segSamples = utils.ResampleRate(segSamples, sampleRate, utils.DefaultMaxSampleRate)spectSampleRate = utils.DefaultMaxSampleRate}config := utils.DefaultSpectrogramConfig(spectSampleRate)
## [2026-03-10] Spectrogram Sample Rate Limiting**Feature:** Spectrograms now automatically downsample high sample rate audio to 16kHz.**Changes:**- `utils/spectrogram.go` — Added `DefaultMaxSampleRate = 16000` constant- `utils/resample.go` — Added `ResampleRate()` function for sample rate conversion- `tools/calls_show_images.go` — Downsample segments before spectrogram generation- `tui/classify.go` — Downsample segments before spectrogram generation
**Rationale:**- High sample rates (e.g., 250kHz bat detectors) produce very tall spectrograms- Birds are typically in 0-8kHz range; 16kHz sample rate (Nyquist = 8kHz) is sufficient- Audio playback unchanged — plays at original sample rate**Behavior:**| Original Rate | Spectrogram Rate | Playback Rate ||---------------|------------------|---------------|| 8000 Hz | 8000 Hz | 8000 Hz || 16000 Hz | 16000 Hz | 16000 Hz || 44100 Hz | 16000 Hz | 44100 Hz || 250000 Hz | 16000 Hz | 250000 Hz |