From bdfe2d37bffc6ba59f87bef84ff906ed8c57ca3c Mon Sep 17 00:00:00 2001 From: "5684185+vsariola@users.noreply.github.com" <5684185+vsariola@users.noreply.github.com> Date: Wed, 8 Oct 2025 08:53:46 +0300 Subject: [PATCH] feat(tracker): panic synth if Inf or NaN, and handle these in detectors Closes #210. --- CHANGELOG.md | 7 ++++++- tracker/detector.go | 16 ++++++++++++++-- tracker/player.go | 15 ++++++++++++++- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a32b9bf..a914565 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## [0.5.0] +## [Unreleased] +### Added +- Panic the synth if it outputs NaN or Inf, and handle these more gracefully in + the loudness and peak detector. ([#210][i210]) +## [0.5.0] ### BREAKING CHANGES - BREAKING CHANGE: always first modulate delay time, then apply notetracking. In a delay unit, modulation adds to the delay time, while note tracking @@ -368,3 +372,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). [i192]: https://github.com/vsariola/sointu/issues/192 [i196]: https://github.com/vsariola/sointu/issues/196 [i200]: https://github.com/vsariola/sointu/issues/200 +[i210]: https://github.com/vsariola/sointu/issues/210 diff --git a/tracker/detector.go b/tracker/detector.go index d21ef34..0cb07f5 100644 --- a/tracker/detector.go +++ b/tracker/detector.go @@ -74,6 +74,11 @@ const ( ) const MAX_INTEGRATED_DATA = 10 * 60 * 60 // 1 hour of samples at 10 Hz (100 ms per sample) +// In the detector, we clamp the signal levels to +-MAX_SIGNAL_AMPLITUDE to +// avoid Inf results. This is 240 dBFS. max float32 is about 3.4e38, so squaring +// the amplitude values gives 1e24, and adding 4410 of those together (when +// taking the mean) gives a value < 1e37, which is still < max float32. +const MAX_SIGNAL_AMPLITUDE = 1e12 const ( PeakMomentary PeakType = iota @@ -232,7 +237,7 @@ func (d *loudnessDetector) update(chunk sointu.AudioBuffer) LoudnessResult { for chn := range 2 { // deinterleave the channels for i := range chunk { - d.tmp[i] = chunk[i][chn] + d.tmp[i] = removeNaNsAndClamp(chunk[i][chn]) } // filter the signal with the weighting filter for k := range d.weighting { @@ -287,6 +292,13 @@ func (d *loudnessDetector) reset() { d.integratedPower = 0 } +func removeNaNsAndClamp(s float32) float32 { + if s != s { // NaN + return 0 + } + return min(max(s, -MAX_SIGNAL_AMPLITUDE), MAX_SIGNAL_AMPLITUDE) +} + func powerToDecibel(power float32) Decibel { return Decibel(float32(10 * math.Log10(float64(power)))) } @@ -382,7 +394,7 @@ func (d *peakDetector) update(buf sointu.AudioBuffer) (ret PeakResult) { for chn := range 2 { // deinterleave the channels for i := range buf { - d.tmp[i] = buf[i][chn] + d.tmp[i] = removeNaNsAndClamp(buf[i][chn]) } // 4x oversample the signal var o []float32 diff --git a/tracker/player.go b/tracker/player.go index 2adbcb8..247e62e 100644 --- a/tracker/player.go +++ b/tracker/player.go @@ -128,7 +128,12 @@ func (p *Player) Process(buffer sointu.AudioBuffer, context PlayerProcessContext rendered, timeAdvanced, err = p.synth.Render(buffer[:framesUntilEvent], timeUntilRowAdvance) if err != nil { p.synth = nil - p.send(Alert{Message: fmt.Sprintf("synth.Render: %s", err.Error()), Priority: Error, Name: "PlayerCrash"}) + p.send(Alert{Message: fmt.Sprintf("synth.Render: %s", err.Error()), Priority: Error, Name: "PlayerCrash", Duration: defaultAlertDuration}) + } + // for performance, we don't check for NaN of every sample, because typically NaNs propagate + if rendered > 0 && (isNaN(buffer[0][0]) || isNaN(buffer[0][1]) || isInf(buffer[0][0]) || isInf(buffer[0][1])) { + p.synth = nil + p.send(Alert{Message: "Inf or NaN detected in synth output", Priority: Error, Name: "PlayerCrash", Duration: defaultAlertDuration}) } } else { rendered = min(framesUntilEvent, timeUntilRowAdvance) @@ -206,6 +211,14 @@ func (p NullPlayerProcessContext) BPM() (bpm float64, ok bool) { return 0, false // no BPM available } +func isNaN(f float32) bool { + return f != f +} + +func isInf(f float32) bool { + return f > math.MaxFloat32 || f < -math.MaxFloat32 +} + func (p *Player) processMessages(context PlayerProcessContext) { loop: for { // process new message