Check local Mumble audio fidelity
This commit is contained in:
committed by
Brandon McGinty
parent
074bd67ad3
commit
0dbe178e7f
@@ -82,34 +82,18 @@ func TestLocalMumbleAudioRoundTrip(t *testing.T) {
|
|||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
case packet = <-listener.packets:
|
case packet = <-listener.packets:
|
||||||
peak := 0
|
frequency, purity, peak := audioQuality(packet.AudioBuffer)
|
||||||
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
|
|
||||||
}
|
|
||||||
if peak < 500 {
|
if peak < 500 {
|
||||||
t.Fatalf("received silent audio peak=%d", peak)
|
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 {
|
if math.Abs(frequency-440) > 120 {
|
||||||
t.Fatalf("received frequency %.1f Hz, want generated 440 Hz", frequency)
|
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):
|
case <-time.After(8 * time.Second):
|
||||||
t.Fatal("timed out waiting for relayed audio")
|
t.Fatal("timed out waiting for relayed audio")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user