// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: MIT package signal import ( "math" "testing" ) // TestWindowTaperPeriodicBits pins the exact bit patterns the legacy // windowTaper loops produced for WelchPSD, STFT and Spectrogram: the // catalogue's periodic forms must reproduce them sample for sample, // not merely to a tolerance. func TestWindowTaperPeriodicBits(t *testing.T) { hann := []uint64{ 0x0, 0x3fa37ca1866b95d0, 0x3fc2bec333018866, 0x3fd3c10eaca8ab4e, 0x3fdfffffffffffff, 0x3fe61f78a9abaa58, 0x3feb504f333f9de6, 0x3feec835e79946a3, 0x3ff0000000000000, 0x3feec835e79946a4, 0x3feb504f333f9de7, 0x3fe61f78a9abaa5b, 0x3fe0000000000001, 0x3fd3c10eaca8ab4c, 0x3fc2bec333018868, 0x3fa37ca1866b95e0, } hamming := []uint64{ 0x3fb47ae147ae147c, 0x3fbd71a676273fcc, 0x3fcb7c4d2eecee22, 0x3fd74b3675e2db0b, 0x3fe147ae147ae148, 0x3fe6e9c0ee04550a, 0x3febb048dd3a8707, 0x3feee1275a30da96, 0x3ff0000000000000, 0x3feee1275a30da97, 0x3febb048dd3a8708, 0x3fe6e9c0ee04550d, 0x3fe147ae147ae149, 0x3fd74b3675e2db0a, 0x3fcb7c4d2eecee24, 0x3fbd71a676273fd4, } for _, c := range []struct { name string want []uint64 }{ {"hann", hann}, {"hamming", hamming}, } { w, err := windowTaper(c.name, 16) if err != nil { t.Fatalf("windowTaper(%s): %v", c.name, err) } for i := range w { if got := math.Float64bits(w[i]); got != c.want[i] { t.Fatalf("%s[%d]: bits %x, want %x", c.name, i, got, c.want[i]) } } } w, err := windowTaper("box", 16) if err != nil { t.Fatalf("windowTaper(box): %v", err) } for i := range w { if got := math.Float64bits(w[i]); got != 0x3ff0000000000000 { t.Fatalf("box[%d]: bits %x, want 3ff0000000000000", i, got) } } } // TestWindowSymmetry pins the two length conventions: symmetric // windows read the same at both ends and vanish there where their // shape says so, periodic windows keep the raised tail the spectral // estimates treat as one period. func TestWindowSymmetry(t *testing.T) { const n = 16 for _, c := range []struct { build func(n int, periodic bool) ([]float64, error) edge float64 // the symmetric window's edge value }{ {WindowHann, 0}, {WindowHamming, 0.08}, {WindowBlackman, 0}, {WindowBartlett, 0}, {WindowCosine, 0}, } { sym, err := c.build(n, false) if err != nil { t.Fatalf("symmetric: %v", err) } per, err := c.build(n, true) if err != nil { t.Fatalf("periodic: %v", err) } if math.Abs(sym[0]-c.edge) > 1e-12 || math.Abs(sym[n-1]-c.edge) > 1e-12 { t.Fatalf("symmetric edges %v, %v, want %v", sym[0], sym[n-1], c.edge) } for i := range n { // Symmetry holds to the rounding of the per-sample // argument, not bitwise: i and n−1−i compute their // cosines from independently rounded arguments. if math.Abs(sym[i]-sym[n-1-i]) > 1e-14 { t.Fatalf("symmetric window off at %d: %v vs %v", i, sym[i], sym[n-1-i]) } if per[i] == per[n-1-i] && i != n-1-i { t.Fatalf("periodic window mirrors its symmetric twin at %d", i) } } // The conventions share only the first sample (argument 0); // the periodic window continues to the raised tail, the // symmetric one closes to the edge value. if per[0] != sym[0] { t.Fatalf("conventions disagree at the first sample: %v vs %v", per[0], sym[0]) } if c.edge == 0 && per[n-1] <= 0 { t.Fatalf("periodic tail %v not above the symmetric edge", per[n-1]) } } // Blackman-Harris and flat top share the symmetry, with the // flat top's slightly negative edge. bh, err := WindowBlackmanHarris(n, false) if err != nil { t.Fatal(err) } for i := range n { if math.Abs(bh[i]-bh[n-1-i]) > 1e-14 { t.Fatalf("Blackman-Harris off symmetry at %d: %v vs %v", i, bh[i], bh[n-1-i]) } } ft, err := WindowFlatTop(n, false) if err != nil { t.Fatal(err) } wantEdge := -0.008 / 19 if math.Abs(ft[0]-wantEdge) > 1e-14 { t.Fatalf("flat top edge %v, want %v", ft[0], wantEdge) } // The flat top's flatness is a frequency-domain property: a // spectral line reads the same amplitude wherever it falls // between bins. The DTFT of the periodic window, sampled at // bin offsets, must stay flat to the window's hundredth of a // decibel. ftPer, err := WindowFlatTop(16, true) if err != nil { t.Fatal(err) } dtft := func(bins float64) float64 { var re, im float64 for i, v := range ftPer { ang := 2 * math.Pi * bins * float64(i) / float64(len(ftPer)) re += v * math.Cos(ang) im -= v * math.Sin(ang) } return math.Hypot(re, im) } w0 := dtft(0) for _, bins := range []float64{0.25, 0.5} { dev := math.Abs(dtft(bins)-w0) / w0 if dev > 2e-3 { t.Fatalf("flat top scalloping at %.2f bins: %g relative, want under 2e-3", bins, dev) } } } // TestWindowKaiser pins the Kaiser taper: beta 0 is the box, the // window peaks at its centre, and the underlying Bessel I0 hits its // tabulated values. func TestWindowKaiser(t *testing.T) { box, err := WindowKaiser(9, 0, false) if err != nil { t.Fatal(err) } for i := range box { if box[i] != 1 { t.Fatalf("beta 0 sample %d = %v, want 1", i, box[i]) } } w, err := WindowKaiser(11, 8.6, false) if err != nil { t.Fatal(err) } if w[5] != 1 { t.Fatalf("Kaiser centre %v, want 1", w[5]) } for i := range w { if math.Abs(w[i]-w[10-i]) > 1e-14 { t.Fatalf("Kaiser off symmetry at %d: %v vs %v", i, w[i], w[10-i]) } } // I0 against tabulated values. for _, c := range []struct{ x, want float64 }{ {0, 1}, {1, 1.2660658777520084}, {5, 27.23987182360444}, } { if got := kaiserI0(c.x); math.Abs(got-c.want) > 1e-12 { t.Fatalf("I0(%g) = %v, want %v", c.x, got, c.want) } } // A bigger beta is a stricter taper: lower at the same offset. hard, err := WindowKaiser(11, 14, false) if err != nil { t.Fatal(err) } if hard[1] >= w[1] { t.Fatalf("beta 14 edge %v not below beta 8.6 edge %v", hard[1], w[1]) } } // TestWindowShapes pins a few hand-computed sample values per // builder. func TestWindowShapes(t *testing.T) { hann, err := WindowHann(5, false) if err != nil { t.Fatal(err) } for i, want := range []float64{0, 0.5, 1, 0.5, 0} { if math.Abs(hann[i]-want) > 1e-12 { t.Fatalf("Hann(5)[%d] = %v, want %v", i, hann[i], want) } } bart, err := WindowBartlett(5, false) if err != nil { t.Fatal(err) } for i, want := range []float64{0, 0.5, 1, 0.5, 0} { if math.Abs(bart[i]-want) > 1e-12 { t.Fatalf("Bartlett(5)[%d] = %v, want %v", i, bart[i], want) } } cos, err := WindowCosine(5, false) if err != nil { t.Fatal(err) } for i, want := range []float64{0, math.Sqrt2 / 2, 1, math.Sqrt2 / 2, 0} { if math.Abs(cos[i]-want) > 1e-12 { t.Fatalf("Cosine(5)[%d] = %v, want %v", i, cos[i], want) } } black, err := WindowBlackman(5, false) if err != nil { t.Fatal(err) } if math.Abs(black[2]-1) > 1e-12 { t.Fatalf("Blackman centre %v, want 1", black[2]) } // The periodic cosine window vanishes only at its first sample. per, err := WindowCosine(8, true) if err != nil { t.Fatal(err) } if per[0] != 0 { t.Fatalf("periodic cosine starts at %v, want 0", per[0]) } for i := 1; i < 8; i++ { if per[i] <= 0 { t.Fatalf("periodic cosine non-positive at %d: %v", i, per[i]) } } } // TestWindowOneSample pins the one-sample convention: the constant 1 // in both modes, for every builder. func TestWindowOneSample(t *testing.T) { for _, c := range []struct { name string build func(n int, periodic bool) ([]float64, error) }{ {"box", WindowBox}, {"hann", WindowHann}, {"hamming", WindowHamming}, {"blackman", WindowBlackman}, {"blackman-harris", WindowBlackmanHarris}, {"flat-top", WindowFlatTop}, {"bartlett", WindowBartlett}, {"kaiser", func(n int, periodic bool) ([]float64, error) { return WindowKaiser(n, 6, periodic) }}, {"cosine", WindowCosine}, } { for _, periodic := range []bool{false, true} { w, err := c.build(1, periodic) if err != nil { t.Fatalf("%s periodic=%v: %v", c.name, periodic, err) } if len(w) != 1 || w[0] != 1 { t.Fatalf("%s periodic=%v: one-sample window %v, want [1]", c.name, periodic, w) } } } } // TestWindowErrors pins the length and beta gates of the catalogue. func TestWindowErrors(t *testing.T) { for _, c := range []struct { name string build func() error }{ {"box n=0", func() error { _, err := WindowBox(0, true); return err }}, {"hann n=-3", func() error { _, err := WindowHann(-3, false); return err }}, {"hamming n=0", func() error { _, err := WindowHamming(0, true); return err }}, {"blackman n=0", func() error { _, err := WindowBlackman(0, false); return err }}, {"blackman-harris n=0", func() error { _, err := WindowBlackmanHarris(0, true); return err }}, {"flat-top n=0", func() error { _, err := WindowFlatTop(0, false); return err }}, {"bartlett n=0", func() error { _, err := WindowBartlett(0, true); return err }}, {"kaiser n=0", func() error { _, err := WindowKaiser(0, 5, false); return err }}, {"kaiser negative beta", func() error { _, err := WindowKaiser(8, -1, false); return err }}, {"cosine n=0", func() error { _, err := WindowCosine(0, true); return err }}, {"unknown taper name", func() error { _, err := windowTaper("hann2", 8); return err }}, } { if err := c.build(); err == nil { t.Errorf("%s: want an error", c.name) } } }