// Copyright (c) 2026 Petr Balvín (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 }