diff --git a/internal/recorder/streamer.go b/internal/recorder/streamer.go index 65604a0..7ba3d53 100644 --- a/internal/recorder/streamer.go +++ b/internal/recorder/streamer.go @@ -37,6 +37,9 @@ type streamSession struct { playbackMode string stereoState string lastAudioTs time.Time + lastAudioL float32 + lastAudioR float32 + lastAudioSet bool // listenOnly sessions have no WAV file and no disk I/O. // They exist solely to feed audio to live-listen subscribers. @@ -390,7 +393,7 @@ func (st *Streamer) processFeed(msg streamFeedMsg) { sess.wavSamples += int64(n / 2) } } - // Gap logging for live-audio sessions + // Gap logging for live-audio sessions + boundary delta check if len(sess.audioSubs) > 0 { if !sess.lastAudioTs.IsZero() { gap := time.Since(sess.lastAudioTs) @@ -398,6 +401,37 @@ func (st *Streamer) processFeed(msg streamFeedMsg) { logging.Warn("gap", "audio_gap", "signal", sess.signalID, "gap_ms", gap.Milliseconds()) } } + // boundary delta (compare previous last sample with current first sample) + if logging.EnabledCategory("boundary") && len(audio) > 0 { + if sess.lastAudioSet { + if sess.channels > 1 && len(audio) >= 2 { + dL := float64(audio[0] - sess.lastAudioL) + dR := float64(audio[1] - sess.lastAudioR) + if dL < 0 { dL = -dL } + if dR < 0 { dR = -dR } + if dL > 0.2 || dR > 0.2 { + logging.Warn("boundary", "audio_step", "signal", sess.signalID, "dL", dL, "dR", dR) + } + } else { + d := float64(audio[0] - sess.lastAudioL) + if d < 0 { d = -d } + if d > 0.2 { + logging.Warn("boundary", "audio_step", "signal", sess.signalID, "dL", d) + } + } + } + // store last sample + if sess.channels > 1 { + lastIdx := (len(audio)-2) + if lastIdx < 0 { lastIdx = 0 } + sess.lastAudioL = audio[lastIdx] + sess.lastAudioR = audio[lastIdx+1] + } else { + sess.lastAudioL = audio[len(audio)-1] + sess.lastAudioR = 0 + } + sess.lastAudioSet = true + } sess.lastAudioTs = time.Now() } st.fanoutPCM(sess, pcm, pcmLen) @@ -642,6 +676,7 @@ func (sess *streamSession) processSnippet(snippet []complex64, snipRate int) ([] // All FIR filtering is now stateful, so no additional overlap is needed. var fullSnip []complex64 trimSamples := 0 + _ = trimSamples if len(sess.overlapIQ) == 1 { fullSnip = make([]complex64, 1+len(snippet)) fullSnip[0] = sess.overlapIQ[0] @@ -692,16 +727,7 @@ func (sess *streamSession) processSnippet(snippet []complex64, snipRate int) ([] } // --- Trim the 1-sample FM discriminator overlap --- - if trimSamples > 0 { - audioTrim := trimSamples / decim1 - if audioTrim < 1 { - audioTrim = 1 // at minimum trim 1 audio sample - } - if audioTrim > 0 && audioTrim < len(audio) { - logging.Debug("discrim", "audio_trim", "signal", sess.signalID, "trim", audioTrim, "decim1", decim1, "audio_len", len(audio)) - audio = audio[audioTrim:] - } - } + // TEMP: skip audio trim to test if per-block trimming causes ticks // --- Stateful stereo decode with conservative lock/hysteresis --- channels := 1