diff --git a/main.go b/main.go index c9ab172..ccba606 100644 --- a/main.go +++ b/main.go @@ -33,6 +33,11 @@ type goertzel struct { coeff float64 } +// newGoertzel initializes and returns a new Goertzel filter configured to detect a specific target frequency. +// targetHz specifies the frequency to detect in Hertz. +// fs is the sampling rate in Hertz. +// N is the number of samples to process. +// The function calculates the filter coefficients based on the provided parameters. func newGoertzel(targetHz float64, fs float64, N int) *goertzel { g := &goertzel{N: N, fs: fs} g.k = int(0.5 + (float64(g.N)*targetHz)/fs) @@ -41,6 +46,10 @@ func newGoertzel(targetHz float64, fs float64, N int) *goertzel { return g } +// Power computes the power of the target frequency in the input signal x using the Goertzel algorithm. +// It processes the input slice x of length g.N, applying the Goertzel recurrence to accumulate state. +// The function returns the squared magnitude (power) of the frequency bin specified by g.k. +// x should be a slice of float64 samples, typically representing a windowed segment of a signal. func (g *goertzel) Power(x []float64) float64 { var s0, s1, s2 float64 for i := 0; i < g.N; i++ { @@ -54,6 +63,10 @@ func (g *goertzel) Power(x []float64) float64 { return real*real + imag*imag } +// windowHann applies a Hann window to the input slice x in place. +// The Hann window is commonly used in signal processing to reduce spectral leakage +// by tapering the beginning and end of the signal to zero. +// The function modifies the input slice directly. func windowHann(x []float64) { n := float64(len(x)) for i := range x { @@ -61,6 +74,9 @@ func windowHann(x []float64) { } } +// pcmToFloat converts a slice of 16-bit PCM audio samples (buf) to a slice of float64 values. +// The output slice has length N, and each element is the float64 representation of the corresponding PCM sample. +// If N is greater than the length of buf, the output slice will contain zero values for the remaining elements. func pcmToFloat(buf []int16, N int) []float64 { out := make([]float64, N) for i := 0; i < N && i < len(buf); i++ { @@ -69,6 +85,9 @@ func pcmToFloat(buf []int16, N int) []float64 { return out } +// rmsPCM calculates the root mean square (RMS) value of a slice of 16-bit PCM audio samples. +// It returns the RMS as a float64, which represents the signal's effective amplitude. +// If the input slice is empty, it returns 0. func rmsPCM(buf []int16) float64 { var s float64 for _, v := range buf { @@ -107,6 +126,23 @@ type twoToneDetector struct { logger *slog.Logger } +// newTwoToneDetector creates and initializes a new twoToneDetector instance. +// It sets up a bank of Goertzel filters for frequencies ranging from 300 Hz to 3000 Hz in 10 Hz steps. +// Parameters: +// +// fs - Sample rate in Hz. +// winN - Window size for analysis. +// hopN - Hop size between windows. +// ratioThresh- Threshold for tone ratio detection. +// rmsThresh - RMS threshold for signal detection. +// minAms - Minimum duration of tone A in milliseconds. +// minBms - Minimum duration of tone B in milliseconds. +// gapMaxMs - Maximum allowed gap between tones in milliseconds. +// logger - Logger for diagnostic output. +// +// Returns: +// +// Pointer to a twoToneDetector configured with the specified parameters. func newTwoToneDetector(fs, winN, hopN int, ratioThresh, rmsThresh float64, minAms, minBms, gapMaxMs int, logger *slog.Logger) *twoToneDetector { freqs := make([]float64, 0) for f := 300.0; f <= 3000.0; f += 10.0 { @@ -131,6 +167,26 @@ func newTwoToneDetector(fs, winN, hopN int, ratioThresh, rmsThresh float64, minA } } +// stepWindow processes a window of PCM audio samples to detect a two-tone sequence. +// It applies a Hann window to the samples, computes RMS and power ratios, and tracks +// the presence and duration of two distinct tones (A and B) according to configured thresholds. +// The function returns an event string (e.g., "TWO_TONE_DETECTED") when a valid two-tone +// sequence is detected, along with the frequencies and durations (in milliseconds) of both tones, +// and the timestamp of detection. +// +// Parameters: +// +// pcms - Slice of int16 PCM audio samples for the current window. +// t0 - Start time of the current window. +// +// Returns: +// +// event - Event string indicating detection status (e.g., "TWO_TONE_DETECTED" or ""). +// aFreq - Frequency of detected Tone A (Hz). +// aDur - Duration of Tone A (milliseconds). +// bFreq - Frequency of detected Tone B (Hz). +// bDur - Duration of Tone B (milliseconds). +// timestamp - Timestamp of detection (time.Time). Zero value if no event detected. func (d *twoToneDetector) stepWindow(pcms []int16, t0 time.Time) (event string, aFreq, aDur, bFreq, bDur float64, timestamp time.Time) { xi := pcmToFloat(pcms, d.winN) windowHann(xi) @@ -236,6 +292,10 @@ func (d *twoToneDetector) stepWindow(pcms []int16, t0 time.Time) (event string, return "", 0, 0, 0, 0, time.Time{} } +// reset reinitializes all internal state fields of the twoToneDetector, +// clearing any ongoing detection data and preparing the detector for a new +// detection sequence. This includes resetting flags, frequency values, +// accumulated durations, start/end timestamps, and gap tracking. func (d *twoToneDetector) reset() { d.inA = false d.aFreq = 0 @@ -337,6 +397,7 @@ func main() { } } +// min returns the smaller of two integer values a and b. func min(a, b int) int { if a < b { return a