Files
tensor/signal/chirp.go
T

61 lines
2.3 KiB
Go
Raw Normal View History

2026-09-03 10:00:00 +02:00
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package signal
import (
"math"
"sourcedock.dev/petrbalvin/tensor/internal/base"
)
// Chirp returns n samples of a linear frequency sweep from f0 through
// f1, sampled at rate samples per unit of time. The sweep runs over
// the whole sample span and reaches f1 exactly at the last sample; the
// first sample sits at phase zero. Every phase comes from the closed
// form 2π·(f0·t + (f1−f0)·t²/(2T)) at that sample's own time, never
// from a recursive oscillator, so the samples carry no accumulated
// drift: sample k is as accurate as the formula at t_k and nothing
// that happened at earlier samples can bend it. That property is what
// aliasing studies of a swept tone need, which is where the transform
// side of this package keeps meeting chirps in the wild.
//
// The sweep edges live strictly below the Nyquist frequency rate/2: an
// edge on or beyond it folds onto lower frequencies, and the error
// names both. The edges may be negative, which reverses the direction
// of rotation without weakening the guard, which watches their
// magnitudes.
//
// Errors: n below 1, a non-positive or non-finite rate, a non-finite
// sweep edge, an edge at or beyond Nyquist.
func Chirp(n int, f0, f1, rate float64) ([]float64, error) {
if n < 1 {
return nil, base.Errf("Chirp: n must be at least 1, got %d", n)
}
if math.IsNaN(rate) || math.IsInf(rate, 0) || rate <= 0 {
return nil, base.Errf("Chirp: the sample rate must be finite and positive, got %g", rate)
}
if math.IsNaN(f0) || math.IsInf(f0, 0) || math.IsNaN(f1) || math.IsInf(f1, 0) {
return nil, base.Errf("Chirp: the sweep edges must be finite, got %g and %g", f0, f1)
}
nyquist := rate / 2
edge := math.Max(math.Abs(f0), math.Abs(f1))
if edge >= nyquist {
return nil, base.Errf("Chirp: the sweep edge %g reaches the Nyquist frequency %g at rate %g", edge, nyquist, rate)
}
out := make([]float64, n)
// The span up to the last sample; a one-sample chirp has no span
// and no slope, and its single sample is the phase-zero zero.
span := float64(n-1) / rate
slope := 0.0
if n > 1 {
slope = (f1 - f0) / span
}
const twoPi = 2 * math.Pi
for i := range out {
t := float64(i) / rate
out[i] = math.Sin(twoPi * (f0*t + 0.5*slope*t*t))
}
return out, nil
}