| @@ -314,7 +314,9 @@ func main() { | |||||
| det := detector.New(cfg.Detector.ThresholdDb, cfg.SampleRate, cfg.FFTSize, | det := detector.New(cfg.Detector.ThresholdDb, cfg.SampleRate, cfg.FFTSize, | ||||
| time.Duration(cfg.Detector.MinDurationMs)*time.Millisecond, | time.Duration(cfg.Detector.MinDurationMs)*time.Millisecond, | ||||
| time.Duration(cfg.Detector.HoldMs)*time.Millisecond) | |||||
| time.Duration(cfg.Detector.HoldMs)*time.Millisecond, | |||||
| cfg.Detector.EmaAlpha, | |||||
| cfg.Detector.HysteresisDb) | |||||
| window := fftutil.Hann(cfg.FFTSize) | window := fftutil.Hann(cfg.FFTSize) | ||||
| h := newHub() | h := newHub() | ||||
| @@ -435,7 +437,9 @@ func main() { | |||||
| if detChanged { | if detChanged { | ||||
| newDet = detector.New(next.Detector.ThresholdDb, next.SampleRate, next.FFTSize, | newDet = detector.New(next.Detector.ThresholdDb, next.SampleRate, next.FFTSize, | ||||
| time.Duration(next.Detector.MinDurationMs)*time.Millisecond, | time.Duration(next.Detector.MinDurationMs)*time.Millisecond, | ||||
| time.Duration(next.Detector.HoldMs)*time.Millisecond) | |||||
| time.Duration(next.Detector.HoldMs)*time.Millisecond, | |||||
| next.Detector.EmaAlpha, | |||||
| next.Detector.HysteresisDb) | |||||
| } | } | ||||
| if windowChanged { | if windowChanged { | ||||
| newWindow = fftutil.Hann(next.FFTSize) | newWindow = fftutil.Hann(next.FFTSize) | ||||
| @@ -15,6 +15,8 @@ detector: | |||||
| threshold_db: -20 | threshold_db: -20 | ||||
| min_duration_ms: 250 | min_duration_ms: 250 | ||||
| hold_ms: 500 | hold_ms: 500 | ||||
| ema_alpha: 0.2 | |||||
| hysteresis_db: 3 | |||||
| recorder: | recorder: | ||||
| enabled: true | enabled: true | ||||
| min_snr_db: 10 | min_snr_db: 10 | ||||
| @@ -17,6 +17,8 @@ type DetectorConfig struct { | |||||
| ThresholdDb float64 `yaml:"threshold_db" json:"threshold_db"` | ThresholdDb float64 `yaml:"threshold_db" json:"threshold_db"` | ||||
| MinDurationMs int `yaml:"min_duration_ms" json:"min_duration_ms"` | MinDurationMs int `yaml:"min_duration_ms" json:"min_duration_ms"` | ||||
| HoldMs int `yaml:"hold_ms" json:"hold_ms"` | HoldMs int `yaml:"hold_ms" json:"hold_ms"` | ||||
| EmaAlpha float64 `yaml:"ema_alpha" json:"ema_alpha"` | |||||
| HysteresisDb float64 `yaml:"hysteresis_db" json:"hysteresis_db"` | |||||
| } | } | ||||
| type RecorderConfig struct { | type RecorderConfig struct { | ||||
| @@ -79,7 +81,7 @@ func Default() Config { | |||||
| AGC: false, | AGC: false, | ||||
| DCBlock: false, | DCBlock: false, | ||||
| IQBalance: false, | IQBalance: false, | ||||
| Detector: DetectorConfig{ThresholdDb: -20, MinDurationMs: 250, HoldMs: 500}, | |||||
| Detector: DetectorConfig{ThresholdDb: -20, MinDurationMs: 250, HoldMs: 500, EmaAlpha: 0.2, HysteresisDb: 3}, | |||||
| Recorder: RecorderConfig{ | Recorder: RecorderConfig{ | ||||
| Enabled: false, | Enabled: false, | ||||
| MinSNRDb: 10, | MinSNRDb: 10, | ||||
| @@ -22,14 +22,17 @@ type Event struct { | |||||
| } | } | ||||
| type Detector struct { | type Detector struct { | ||||
| ThresholdDb float64 | |||||
| MinDuration time.Duration | |||||
| Hold time.Duration | |||||
| ThresholdDb float64 | |||||
| MinDuration time.Duration | |||||
| Hold time.Duration | |||||
| EmaAlpha float64 | |||||
| HysteresisDb float64 | |||||
| binWidth float64 | binWidth float64 | ||||
| nbins int | nbins int | ||||
| sampleRate int | sampleRate int | ||||
| ema []float64 | |||||
| active map[int64]*activeEvent | active map[int64]*activeEvent | ||||
| nextID int64 | nextID int64 | ||||
| } | } | ||||
| @@ -57,22 +60,31 @@ type Signal struct { | |||||
| Class *classifier.Classification `json:"class,omitempty"` | Class *classifier.Classification `json:"class,omitempty"` | ||||
| } | } | ||||
| func New(thresholdDb float64, sampleRate int, fftSize int, minDur, hold time.Duration) *Detector { | |||||
| func New(thresholdDb float64, sampleRate int, fftSize int, minDur, hold time.Duration, emaAlpha, hysteresis float64) *Detector { | |||||
| if minDur <= 0 { | if minDur <= 0 { | ||||
| minDur = 250 * time.Millisecond | minDur = 250 * time.Millisecond | ||||
| } | } | ||||
| if hold <= 0 { | if hold <= 0 { | ||||
| hold = 500 * time.Millisecond | hold = 500 * time.Millisecond | ||||
| } | } | ||||
| if emaAlpha <= 0 || emaAlpha > 1 { | |||||
| emaAlpha = 0.2 | |||||
| } | |||||
| if hysteresis <= 0 { | |||||
| hysteresis = 3 | |||||
| } | |||||
| return &Detector{ | return &Detector{ | ||||
| ThresholdDb: thresholdDb, | |||||
| MinDuration: minDur, | |||||
| Hold: hold, | |||||
| binWidth: float64(sampleRate) / float64(fftSize), | |||||
| nbins: fftSize, | |||||
| sampleRate: sampleRate, | |||||
| active: map[int64]*activeEvent{}, | |||||
| nextID: 1, | |||||
| ThresholdDb: thresholdDb, | |||||
| MinDuration: minDur, | |||||
| Hold: hold, | |||||
| EmaAlpha: emaAlpha, | |||||
| HysteresisDb: hysteresis, | |||||
| binWidth: float64(sampleRate) / float64(fftSize), | |||||
| nbins: fftSize, | |||||
| sampleRate: sampleRate, | |||||
| ema: make([]float64, fftSize), | |||||
| active: map[int64]*activeEvent{}, | |||||
| nextID: 1, | |||||
| } | } | ||||
| } | } | ||||
| @@ -102,16 +114,18 @@ func (d *Detector) detectSignals(spectrum []float64, centerHz float64) []Signal | |||||
| if n == 0 { | if n == 0 { | ||||
| return nil | return nil | ||||
| } | } | ||||
| threshold := d.ThresholdDb | |||||
| noise := median(spectrum) | |||||
| smooth := d.smoothSpectrum(spectrum) | |||||
| thresholdOn := d.ThresholdDb | |||||
| thresholdOff := d.ThresholdDb - d.HysteresisDb | |||||
| noise := median(smooth) | |||||
| var signals []Signal | var signals []Signal | ||||
| in := false | in := false | ||||
| start := 0 | start := 0 | ||||
| peak := -1e9 | peak := -1e9 | ||||
| peakBin := 0 | peakBin := 0 | ||||
| for i := 0; i < n; i++ { | for i := 0; i < n; i++ { | ||||
| v := spectrum[i] | |||||
| if v >= threshold { | |||||
| v := smooth[i] | |||||
| if v >= thresholdOn { | |||||
| if !in { | if !in { | ||||
| in = true | in = true | ||||
| start = i | start = i | ||||
| @@ -121,13 +135,13 @@ func (d *Detector) detectSignals(spectrum []float64, centerHz float64) []Signal | |||||
| peak = v | peak = v | ||||
| peakBin = i | peakBin = i | ||||
| } | } | ||||
| } else if in { | |||||
| signals = append(signals, d.makeSignal(start, i-1, peak, peakBin, noise, centerHz, spectrum)) | |||||
| } else if in && v < thresholdOff { | |||||
| signals = append(signals, d.makeSignal(start, i-1, peak, peakBin, noise, centerHz, smooth)) | |||||
| in = false | in = false | ||||
| } | } | ||||
| } | } | ||||
| if in { | if in { | ||||
| signals = append(signals, d.makeSignal(start, n-1, peak, peakBin, noise, centerHz, spectrum)) | |||||
| signals = append(signals, d.makeSignal(start, n-1, peak, peakBin, noise, centerHz, smooth)) | |||||
| } | } | ||||
| return signals | return signals | ||||
| } | } | ||||
| @@ -147,6 +161,20 @@ func (d *Detector) makeSignal(first, last int, peak float64, peakBin int, noise | |||||
| } | } | ||||
| } | } | ||||
| func (d *Detector) smoothSpectrum(spectrum []float64) []float64 { | |||||
| if d.ema == nil || len(d.ema) != len(spectrum) { | |||||
| d.ema = make([]float64, len(spectrum)) | |||||
| copy(d.ema, spectrum) | |||||
| return d.ema | |||||
| } | |||||
| alpha := d.EmaAlpha | |||||
| for i := range spectrum { | |||||
| v := spectrum[i] | |||||
| d.ema[i] = alpha*v + (1-alpha)*d.ema[i] | |||||
| } | |||||
| return d.ema | |||||
| } | |||||
| func (d *Detector) matchSignals(now time.Time, signals []Signal) []Event { | func (d *Detector) matchSignals(now time.Time, signals []Signal) []Event { | ||||
| used := make(map[int64]bool, len(d.active)) | used := make(map[int64]bool, len(d.active)) | ||||
| for _, s := range signals { | for _, s := range signals { | ||||
| @@ -6,7 +6,7 @@ import ( | |||||
| ) | ) | ||||
| func TestDetectorCreatesEvent(t *testing.T) { | func TestDetectorCreatesEvent(t *testing.T) { | ||||
| d := New(-10, 1000, 10, 1*time.Millisecond, 10*time.Millisecond) | |||||
| d := New(-10, 1000, 10, 1*time.Millisecond, 10*time.Millisecond, 0.2, 3) | |||||
| center := 0.0 | center := 0.0 | ||||
| spectrum := []float64{-30, -30, -30, -5, -5, -30, -30, -30, -30, -30} | spectrum := []float64{-30, -30, -30, -5, -5, -30, -30, -30, -30, -30} | ||||
| now := time.Now() | now := time.Now() | ||||
| @@ -19,9 +19,11 @@ type ConfigUpdate struct { | |||||
| } | } | ||||
| type DetectorUpdate struct { | type DetectorUpdate struct { | ||||
| ThresholdDb *float64 `json:"threshold_db"` | |||||
| MinDuration *int `json:"min_duration_ms"` | |||||
| HoldMs *int `json:"hold_ms"` | |||||
| ThresholdDb *float64 `json:"threshold_db"` | |||||
| MinDuration *int `json:"min_duration_ms"` | |||||
| HoldMs *int `json:"hold_ms"` | |||||
| EmaAlpha *float64 `json:"ema_alpha"` | |||||
| HysteresisDb *float64 `json:"hysteresis_db"` | |||||
| } | } | ||||
| type SettingsUpdate struct { | type SettingsUpdate struct { | ||||
| @@ -121,6 +123,12 @@ func (m *Manager) ApplyConfig(update ConfigUpdate) (config.Config, error) { | |||||
| } | } | ||||
| next.Detector.HoldMs = *update.Detector.HoldMs | next.Detector.HoldMs = *update.Detector.HoldMs | ||||
| } | } | ||||
| if update.Detector.EmaAlpha != nil { | |||||
| next.Detector.EmaAlpha = *update.Detector.EmaAlpha | |||||
| } | |||||
| if update.Detector.HysteresisDb != nil { | |||||
| next.Detector.HysteresisDb = *update.Detector.HysteresisDb | |||||
| } | |||||
| } | } | ||||
| if update.Recorder != nil { | if update.Recorder != nil { | ||||
| if update.Recorder.Enabled != nil { | if update.Recorder.Enabled != nil { | ||||