Documentation
¶
Overview ¶
Package spectrum produces frame-rate-limited windowed FFT magnitude frames from a stream of IQ chunks. It is the producer side of the live spectrum / waterfall feature — a Frame goes onto an SSE/WS wire at a configurable refresh rate (10 Hz by default), low enough that the producer's CPU cost is negligible relative to control-channel decode and the wire bandwidth stays reasonable for LAN clients.
Design notes:
One FFT per Frame, not per IQ chunk. The producer consumes chunks continuously but only invokes an FFT once per 1/FrameRate interval. Intermediate chunks are dropped — for a 2.048 MS/s control SDR running 10 fps, ~204k samples elapse between frames; processing them all would burn CPU for no visual benefit.
Power normalized to dBFS so the wire representation is human- readable and small (float32 per bin). Subscribers receive freshly-allocated slices to avoid coupling consumers.
The producer doesn't own the IQ source — it consumes from a caller-provided <-chan []complex64 (typically an iqtap.Subscriber.C). Lifecycle is "run until ctx cancels OR the input channel closes."
Index ¶
- func Percentile(bins []float32, p float64) float32
- func PowerDB(plan fft.Plan, win []float64, winSum float64, in []complex64, ...)
- func Smooth(bins []float32, w int) []float32
- func Spectrogram(iq []complex64, fftSize, hop int) [][]float32
- func WeightedCentroidHz(binsDb []float32, peakBin, halfWidth int, floorDb float32, ...) float64
- func Welch(plan fft.Plan, win []float64, winSum float64, in []complex64, noverlap int) []float32
- type Frame
- type Occupancy
- type Options
- type Producer
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Percentile ¶ added in v0.4.3
Percentile returns the p-quantile (p in 0..1) of bins via copy+sort, the robust noise-floor estimate (np.percentile with nearest-rank interpolation). An empty slice returns 0; p is clamped into range.
func PowerDB ¶ added in v0.4.0
func PowerDB(plan fft.Plan, win []float64, winSum float64, in []complex64, bufIn, bufOut []complex128, dst []float32)
PowerDB computes the FFT-shifted dBFS power spectrum of in into dst. It is the single source of truth for the spectrum math, shared by the streaming Producer and one-shot callers (the Mixer plot's per-window FFTs).
All of in, dst, win, bufIn and bufOut must have the same length n — a power of two — and plan must be fft.New(n). bufIn/bufOut are caller- owned FFT scratch (reused across calls to stay allocation-free); their contents are overwritten. winSum is Σ(win²), precomputed once.
Bins are FFT-shifted so dst[0] is the lowest frequency (-Fs/2) and dst[n/2] is DC, matching Frame's documented layout. Normalization is power = |X|² / (n · Σ(w²)), so a unit-amplitude tone reads ~0 dBFS at its bin.
func Smooth ¶ added in v0.4.3
Smooth returns bins convolved with a centered boxcar of half-width w (total window 2w+1), the discrete uniform filter used to tame per-bin noise before peak picking (np.convolve with a normalized rectangular kernel). w<=0 returns a copy. Windows are clipped at the array edges so the output keeps the input length without wrap-around.
func Spectrogram ¶ added in v0.4.3
Spectrogram computes the short-time Fourier transform of iq as a sequence of FFT-shifted dBFS frames — the time-frequency plane behind a waterfall plot (the scipy.signal.stft / matplotlib specgram analog). It slides a Hann window of length fftSize by hop samples, computing each frame's power spectrum via PowerDB, so each returned row uses the same dBFS normalization and FFT-shift (DC in the middle) as AverageDB and the streaming Producer.
fftSize must be a power of two; hop <= 0 defaults to fftSize/2 (50% overlap). The result is frames×fftSize: rows are time steps, columns are frequency bins (column 0 = centerFreq - sampleRate/2). Returns nil when iq holds fewer than fftSize samples.
func WeightedCentroidHz ¶ added in v0.4.3
func WeightedCentroidHz(binsDb []float32, peakBin, halfWidth int, floorDb float32, baseHz, binHz float64) float64
WeightedCentroidHz returns the power-weighted mean frequency of the carrier hump around peakBin: the contiguous band of bins within ±halfWidth, each weighted by its linear power (dB → power via DBToLinearPower). This finds the true center of a wide carrier whose single strongest bin is noisy (np.average of the bin frequencies weighted by power). baseHz is the absolute frequency of bin 0 and binHz the bin spacing. Bins at or below floorDb are excluded; when no bin clears the floor the peak bin's frequency is returned.
func Welch ¶ added in v0.4.3
Welch returns the averaged power spectral density (dBFS) of in — the native Go equivalent of scipy.signal.welch. It splits in into overlapping segments of length len(win), windows and FFTs each, and averages their periodograms in linear power before a single conversion to dB. Averaging trades frequency resolution for a far lower-variance noise floor than the single-shot PowerDB periodogram, which is what makes a weak carrier legible against noise.
win is the analysis window (length n, a power of two) and winSum is Σ(win²), precomputed once by the caller; plan must be fft.New(n). noverlap is the number of samples shared between consecutive segments (0 ≤ noverlap < n; a common choice is n/2). Bins are FFT-shifted so dst[0] is -Fs/2 and dst[n/2] is DC, identical to PowerDB / Frame. Normalization matches PowerDB (power = |X|²/(n·Σw²)), so a unit-amplitude tone reads ~0 dBFS.
The returned slice is freshly allocated (length n). When in is shorter than one segment, no periodogram can be formed and every bin reads the clamp floor.
Types ¶
type Frame ¶
Frame is one spectrum snapshot. Bins are dBFS magnitudes, length equal to the configured FFT size. Bin order is FFT-shifted so Bins[0] corresponds to (CenterHz - SampleRate/2) and Bins[len-1] corresponds to (CenterHz + SampleRate/2 - SampleRate/N).
func AverageDB ¶ added in v0.4.3
AverageDB builds a Welch-averaged dBFS spectrum over the whole of iq, returning a Frame ready for peak detection or display. It is the offline, one-shot analog of the streaming Producer: rather than emitting frames at a fixed rate it walks iq in non-overlapping n-sample windows, computes the dBFS power spectrum of each via PowerDB, and averages the per-bin dB values across all windows (scipy.signal.welch with a Hann window and zero overlap).
n must be a power of two. When iq holds fewer than n samples a single zero-padded window is taken so a short capture still yields a frame. The returned Frame's CenterHz/SampleRate carry centerHz and the rounded sampleRateHz so callers can map bins back to absolute frequency.
type Occupancy ¶ added in v0.5.8
type Occupancy struct {
// OccupiedBandwidthHz is the ITU 99% occupied bandwidth: the span between
// the points where the cumulative power crosses 0.5% and 99.5% of the
// total, i.e. the band holding 99% of the power with 0.5% in each tail.
OccupiedBandwidthHz float64 `json:"occupied_bandwidth_hz"`
// ChannelPowerDBFS is the integrated power within ±channelBW/2 of DC,
// in dBFS (10·log10 of the summed linear bin power).
ChannelPowerDBFS float64 `json:"channel_power_dbfs"`
// ACPRUpperDB / ACPRLowerDB are the adjacent-channel power ratios for the
// upper/lower adjacent channels relative to the main channel, in dB
// (negative for a clean signal that does not splatter into its neighbours).
ACPRUpperDB float64 `json:"acpr_upper_db"`
ACPRLowerDB float64 `json:"acpr_lower_db"`
// SpectralFlatness is the Wiener entropy (geometric-mean / arithmetic-mean
// of the in-channel linear power), 0..1: ~1 for white noise, ≪1 for a tone
// or a narrow carrier.
SpectralFlatness float64 `json:"spectral_flatness"`
}
Occupancy holds the spectral-occupancy measurements derived from a single FFT-shifted dBFS spectrum (a Frame's Bins). These are the lab-grade channel figures a vector-signal analyzer reports alongside the constellation: how wide the signal really is, how much power sits in the channel, how much leaks into the neighbours, and how tone-like vs noise-like the channel is.
func ComputeOccupancy ¶ added in v0.5.8
func ComputeOccupancy(binsDB []float32, sampleRateHz, channelBWHz, adjOffsetHz, adjBWHz float64) Occupancy
ComputeOccupancy derives the spectral-occupancy metrics from FFT-shifted dBFS bins (bin 0 = -sampleRate/2, DC at len/2, the order spectrum.AverageDB produces with centerHz=0). channelBWHz is the main channel width; adjOffsetHz is the centre spacing to each adjacent channel and adjBWHz its width — pass the protocol channel spacing for both to measure ACPR into the neighbouring channels. Non-positive widths fall back to sensible spans so a caller can ask for occupied bandwidth alone.
type Options ¶
type Options struct {
// FFTSize is the number of complex samples per FFT. Must be a
// power of two; 4096 is a good default for a 2.048 MS/s control
// stream (~500 Hz/bin resolution).
FFTSize int
// FrameRate caps the output rate in frames per second. 0 picks
// 10 fps. Higher rates burn CPU and wire bandwidth without
// visible benefit beyond ~15 fps.
FrameRate float64
// CenterFreqHz and SampleRateHz are stamped on every Frame so
// downstream clients can map bins to absolute frequencies
// without consulting a side channel. Callers should update them
// when the device retunes (Producer.SetCenter / SetSampleRate).
CenterFreqHz uint32
SampleRateHz uint32
}
Options configure a Producer.
type Producer ¶
type Producer struct {
// contains filtered or unexported fields
}
Producer consumes IQ chunks and emits Frames at a capped rate. One Producer per (device, subscriber) pairing — the bulk of the CPU cost is the FFT, so two Producers off the same source pay double. Callers that want multiple subscribers should keep a single Producer and fan its output channel.
func New ¶
New builds a Producer with the given options. Returns an error if the FFT size isn't a positive power of two.
func (*Producer) Dropped ¶
Dropped returns the cumulative number of IQ chunks consumed but not processed (i.e. arrived during a rate-limited interval). Non-zero is expected and healthy at typical SDR rates.
func (*Producer) Run ¶
Run consumes from in and emits Frames onto out until ctx cancels or in closes. Frames are sent non-blocking; a slow consumer silently drops them (the rate-cap means losses are at most FrameRate per second).
Run owns the goroutine; callers spawn it and read frames on out.
func (*Producer) SetCenter ¶
SetCenter updates the centre frequency stamped on subsequent Frames. Call after the underlying device retunes.
func (*Producer) SetSampleRate ¶
SetSampleRate updates the sample rate stamped on subsequent Frames. Call after the underlying device changes rates.