package audio import ( "math" "testing" ) // goertzel returns the magnitude of one frequency bin, so a test can ask "how // much energy is at 10 kHz" without pulling in an FFT. func goertzel(x []int16, fs, freq float64) float64 { w := 2 * math.Pi * freq / fs c := 2 * math.Cos(w) var s1, s2 float64 for _, v := range x { s := float64(v) + c*s1 - s2 s2, s1 = s1, s } return math.Hypot(s1-s2*math.Cos(w), s2*math.Sin(w)) } // Upsampling 16 kHz → 32 kHz must not create an audible mirror image. // // MP3 recordings came back with a hiss that is not on the air and is not in the // WAV of the same audio — "louder than the voice", from a station that hears the // voice well clear of the noise. The MP3 path is the only one that resamples, // and it did so by linear interpolation: every component at f is mirrored to // 16 kHz − f, which for broadband receiver noise means a second noise floor // spread across the top of the band. func TestUpsampleSuppressesTheImage(t *testing.T) { const fs = 16000.0 const tone = 6000.0 // its image lands at 16000 − 6000 = 10 kHz in := make([]int16, 4096) for i := range in { in[i] = int16(12000 * math.Sin(2*math.Pi*tone*float64(i)/fs)) } out := upsample2(in) if len(out) != 2*len(in) { t.Fatalf("upsample2 returned %d samples for %d", len(out), len(in)) } wanted := goertzel(out, 2*fs, tone) image := goertzel(out, 2*fs, 2*fs-tone-16000) // = 10 kHz if wanted == 0 { t.Fatal("the tone itself did not survive upsampling") } db := 20 * math.Log10(image/wanted) t.Logf("image at 10 kHz is %.1f dB below the 6 kHz tone", db) if db > -35 { t.Errorf("image only %.1f dB down — it is audible as added hiss; want at least 35 dB", db) } } // The filter must not change the level, or every existing recording would come // back quieter (or clipped) after the fix — a second surprise on top of the one // being repaired. func TestUpsampleKeepsTheLevel(t *testing.T) { const fs = 16000.0 in := make([]int16, 4096) for i := range in { in[i] = int16(10000 * math.Sin(2*math.Pi*1000*float64(i)/fs)) } rms := func(x []int16) float64 { var acc float64 for _, v := range x { acc += float64(v) * float64(v) } return math.Sqrt(acc / float64(len(x))) } // Skip the filter's warm-up and tail, where the window is only partly fed. out := upsample2(in) got, want := rms(out[200:len(out)-200]), rms(in[100:len(in)-100]) ratio := got / want t.Logf("level after upsampling: ×%.3f", ratio) if ratio < 0.9 || ratio > 1.1 { t.Errorf("level changed by ×%.3f — want within 10%%", ratio) } }