61 lines
2.3 KiB
Go
61 lines
2.3 KiB
Go
// 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
|
|||
|
|
}
|