feat: initial release
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package signal
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/big"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestChirpConstantToneIsAPureSine pins the degenerate sweep: with f0
|
||||
// equal to f1 the slope is exactly zero and every sample must be bit
|
||||
// for bit the plain sine at that frequency.
|
||||
func TestChirpConstantToneIsAPureSine(t *testing.T) {
|
||||
const f, rate = 17.3, 800.0
|
||||
got, err := Chirp(400, f, f, rate)
|
||||
if err != nil {
|
||||
t.Fatalf("Chirp: %v", err)
|
||||
}
|
||||
for i := range got {
|
||||
tt := float64(i) / rate
|
||||
if want := math.Sin(2 * math.Pi * (f*tt + 0.5*0*tt*tt)); got[i] != want {
|
||||
t.Fatalf("sample %d = %.17g, want the plain sine %.17g", i, got[i], want)
|
||||
}
|
||||
}
|
||||
if got[0] != 0 {
|
||||
t.Fatalf("the first sample = %.17g, want the phase-zero zero", got[0])
|
||||
}
|
||||
}
|
||||
|
||||
// chirpPhaseBig evaluates the sweep phase 2π(f0·t + (f1−f0)t²/(2T)) in
|
||||
// 256-bit arithmetic from the exact float64 edges, the referent the
|
||||
// samples are held against.
|
||||
func chirpPhaseBig(i int, f0, f1, span float64, rate int64) *big.Float {
|
||||
const prec = 256
|
||||
t := new(big.Float).SetPrec(prec).SetInt64(int64(i))
|
||||
t.Quo(t, new(big.Float).SetPrec(prec).SetInt64(rate))
|
||||
f0b := new(big.Float).SetPrec(prec).SetFloat64(f0)
|
||||
slope := new(big.Float).SetPrec(prec).SetFloat64(f1 - f0)
|
||||
slope.Quo(slope, new(big.Float).SetPrec(prec).SetFloat64(span))
|
||||
phase := new(big.Float).SetPrec(prec).Mul(slope, t)
|
||||
phase.Mul(phase, t)
|
||||
phase.Quo(phase, new(big.Float).SetPrec(prec).SetInt64(2))
|
||||
phase.Add(phase, new(big.Float).SetPrec(prec).Mul(f0b, t))
|
||||
return phase.Mul(phase, new(big.Float).SetPrec(prec).SetFloat64(2*math.Pi))
|
||||
}
|
||||
|
||||
// TestChirpPhaseAgainstBigFloat holds selected samples against the
|
||||
// phase evaluated in 256-bit arithmetic: the float64 route may lose
|
||||
// only the rounding its own phase arithmetic costs, a few parts in
|
||||
// 1e16 of a phase that stays near 2π·40.
|
||||
func TestChirpPhaseAgainstBigFloat(t *testing.T) {
|
||||
const n, rate = 200, 1000
|
||||
const f0, f1 = 3, 40
|
||||
span := float64(n-1) / rate
|
||||
got, err := Chirp(n, f0, f1, float64(rate))
|
||||
if err != nil {
|
||||
t.Fatalf("Chirp: %v", err)
|
||||
}
|
||||
for _, i := range []int{0, 1, 37, 100, 150, 198, 199} {
|
||||
phase, _ := chirpPhaseBig(i, f0, f1, span, rate).Float64()
|
||||
want := math.Sin(phase)
|
||||
if math.Abs(got[i]-want) > 5e-13 {
|
||||
t.Fatalf("sample %d = %.17g, want sin(256-bit phase) %.17g", i, got[i], want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestChirpSweepDirection counts zero crossings per eighth of a rising
|
||||
// sweep: the count must climb from roughly f0's rate to roughly f1's,
|
||||
// which pins both the direction of the sweep and its approximate
|
||||
// linearity.
|
||||
func TestChirpSweepDirection(t *testing.T) {
|
||||
const n, rate = 8000, 8000.0
|
||||
const f0, f1 = 100, 900
|
||||
got, err := Chirp(n, f0, f1, rate)
|
||||
if err != nil {
|
||||
t.Fatalf("Chirp: %v", err)
|
||||
}
|
||||
crossings := func(from, to int) int {
|
||||
c := 0
|
||||
for i := from; i < to; i++ {
|
||||
if got[i] >= 0 && got[i+1] < 0 {
|
||||
c++
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
first := crossings(0, n/8)
|
||||
last := crossings(7*n/8, n-1)
|
||||
// The eighth spans 1000 samples; the instantaneous frequency runs
|
||||
// from about 200 to about 887 Hz, so the crossing counts must
|
||||
// climb and sit near those rates.
|
||||
if first < 15 || first > 40 || last < 85 || last > 110 {
|
||||
t.Fatalf("crossings per eighth: first %d, last %d, want a climb from near 200 Hz to near 890 Hz", first, last)
|
||||
}
|
||||
if last <= first {
|
||||
t.Fatalf("the sweep does not rise: %d crossings at the end against %d at the start", last, first)
|
||||
}
|
||||
}
|
||||
|
||||
// TestChirpErrors pins the guard rails: the length, the rate, the
|
||||
// finite edges and the Nyquist boundary the sweep must stay under,
|
||||
// counting the magnitude for negative edges too.
|
||||
func TestChirpErrors(t *testing.T) {
|
||||
if _, err := Chirp(0, 1, 2, 100); err == nil {
|
||||
t.Fatal("n = 0: want an error")
|
||||
}
|
||||
if _, err := Chirp(1, 1, 2, 0); err == nil {
|
||||
t.Fatal("a zero rate: want an error")
|
||||
}
|
||||
if _, err := Chirp(1, 1, 2, math.Inf(1)); err == nil {
|
||||
t.Fatal("an infinite rate: want an error")
|
||||
}
|
||||
if _, err := Chirp(1, math.NaN(), 2, 100); err == nil {
|
||||
t.Fatal("a NaN edge: want an error")
|
||||
}
|
||||
if _, err := Chirp(10, 1, math.Inf(-1), 100); err == nil {
|
||||
t.Fatal("an infinite edge: want an error")
|
||||
}
|
||||
if _, err := Chirp(10, 1, 50, 100); err == nil {
|
||||
t.Fatal("an edge on the Nyquist frequency: want an error")
|
||||
}
|
||||
if _, err := Chirp(10, 60, 1, 100); err == nil {
|
||||
t.Fatal("a negative edge beyond Nyquist in magnitude: want an error")
|
||||
}
|
||||
// The one-sample chirp is legal and carries the phase-zero zero.
|
||||
one, err := Chirp(1, 7, 9, 100)
|
||||
if err != nil {
|
||||
t.Fatalf("Chirp(1, ...): %v", err)
|
||||
}
|
||||
if len(one) != 1 || one[0] != 0 {
|
||||
t.Fatalf("the one-sample chirp = %v, want [0]", one)
|
||||
}
|
||||
// A downward sweep inside the guard is legal.
|
||||
if _, err := Chirp(16, 900, 100, 8000); err != nil {
|
||||
t.Fatalf("Chirp downward: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestChirpDeterministic redraws one sweep and requires the same bits,
|
||||
// the contract every signal builder here carries.
|
||||
func TestChirpDeterministic(t *testing.T) {
|
||||
a, err := Chirp(256, 4, 90, 512)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err := Chirp(256, 4, 90, 512)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i := range a {
|
||||
if math.Float64bits(a[i]) != math.Float64bits(b[i]) {
|
||||
t.Fatalf("sample %d moved between identical calls", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user