From 0dbe178e7fba2ca4dbd430653fb7b019b9453171 Mon Sep 17 00:00:00 2001 From: "Brandon McGinty (chatgpt)" Date: Sun, 9 Aug 2026 20:29:02 -0400 Subject: [PATCH] Check local Mumble audio fidelity --- gumble/gumble/integration_audio_test.go | 28 +++----------- gumble/gumble/integration_quality_test.go | 45 +++++++++++++++++++++++ 2 files changed, 51 insertions(+), 22 deletions(-) create mode 100644 gumble/gumble/integration_quality_test.go diff --git a/gumble/gumble/integration_audio_test.go b/gumble/gumble/integration_audio_test.go index 10da40a..8355309 100644 --- a/gumble/gumble/integration_audio_test.go +++ b/gumble/gumble/integration_audio_test.go @@ -82,34 +82,18 @@ func TestLocalMumbleAudioRoundTrip(t *testing.T) { } select { case packet = <-listener.packets: - peak := 0 - crossings := 0 - previous := 0 - // The decoder returns interleaved stereo; inspect one channel. A rising - // zero-crossing estimate verifies the 440 Hz source survived Opus rather - // than merely proving that arbitrary non-silent audio was relayed. - for i := 0; i < len(packet.AudioBuffer); i += gumble.AudioChannels { - v := int(packet.AudioBuffer[i]) - if v < 0 { - if -v > peak { - peak = -v - } - } else if v > peak { - peak = v - } - if previous <= 0 && v > 0 { - crossings++ - } - previous = v - } + frequency, purity, peak := audioQuality(packet.AudioBuffer) if peak < 500 { t.Fatalf("received silent audio peak=%d", peak) } - samples := len(packet.AudioBuffer) / gumble.AudioChannels - frequency := float64(crossings*gumble.AudioSampleRate) / float64(samples) if math.Abs(frequency-440) > 120 { t.Fatalf("received frequency %.1f Hz, want generated 440 Hz", frequency) } + // A clean sine projects strongly onto its fundamental. This detects + // severe codec distortion beyond simple packet arrival and pitch checks. + if purity < 0.65 { + t.Fatalf("received audio is distorted: 440 Hz purity=%.2f", purity) + } case <-time.After(8 * time.Second): t.Fatal("timed out waiting for relayed audio") } diff --git a/gumble/gumble/integration_quality_test.go b/gumble/gumble/integration_quality_test.go new file mode 100644 index 0000000..aa7e383 --- /dev/null +++ b/gumble/gumble/integration_quality_test.go @@ -0,0 +1,45 @@ +//go:build integration + +package gumble_test + +import ( + "math" + + "git.stormux.org/storm/barnard/gumble/gumble" +) + +// audioQuality returns the rising-crossing pitch estimate, the fraction of +// RMS energy explained by the 440 Hz fundamental, and the sample peak. +func audioQuality(audio gumble.AudioBuffer) (frequency, purity float64, peak int) { + if len(audio) < gumble.AudioChannels { + return 0, 0, 0 + } + samples := len(audio) / gumble.AudioChannels + crossings, previous := 0, 0 + var energy, sine, cosine float64 + for i := 0; i < samples; i++ { + value := int(audio[i*gumble.AudioChannels]) + if value < 0 { + if -value > peak { + peak = -value + } + } else if value > peak { + peak = value + } + if previous <= 0 && value > 0 { + crossings++ + } + previous = value + x := float64(value) + phase := 2 * math.Pi * 440 * float64(i) / gumble.AudioSampleRate + energy += x * x + sine += x * math.Sin(phase) + cosine += x * math.Cos(phase) + } + frequency = float64(crossings*gumble.AudioSampleRate) / float64(samples) + if energy != 0 { + // Projection amplitude divided by RMS, normalized for sine RMS. + purity = math.Sqrt(2) * math.Hypot(sine, cosine) / math.Sqrt(energy*float64(samples)) + } + return +}