feat(tracker): panic synth if Inf or NaN, and handle these in detectors

Closes #210.
This commit is contained in:
5684185+vsariola@users.noreply.github.com
2025-10-08 08:53:46 +03:00
parent 167f541a52
commit bdfe2d37bf
3 changed files with 34 additions and 4 deletions

View File

@ -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

View File

@ -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