|
| 1 | +// E2E test: records audio from AAudio input stream at 48 kHz and verifies |
| 2 | +// the captured signal contains a 440 Hz tone using the Goertzel algorithm. |
| 3 | +// |
| 4 | +// Exit 0 = PASS (440 Hz detected), exit 1 = FAIL. |
| 5 | +package main |
| 6 | + |
| 7 | +import ( |
| 8 | + "fmt" |
| 9 | + "math" |
| 10 | + "os" |
| 11 | + "unsafe" |
| 12 | + |
| 13 | + "github.com/xaionaro-go/ndk/audio" |
| 14 | +) |
| 15 | + |
| 16 | +// goertzelMagnitude computes the magnitude of a specific frequency in |
| 17 | +// a buffer of int16 PCM samples using the Goertzel algorithm. |
| 18 | +func goertzelMagnitude(samples []int16, targetFreq float64, sampleRate float64) float64 { |
| 19 | + n := len(samples) |
| 20 | + k := int(0.5 + float64(n)*targetFreq/sampleRate) |
| 21 | + w := 2.0 * math.Pi * float64(k) / float64(n) |
| 22 | + coeff := 2.0 * math.Cos(w) |
| 23 | + |
| 24 | + var s0, s1, s2 float64 |
| 25 | + for _, sample := range samples { |
| 26 | + s0 = coeff*s1 - s2 + float64(sample) |
| 27 | + s2 = s1 |
| 28 | + s1 = s0 |
| 29 | + } |
| 30 | + |
| 31 | + return s1*s1 + s2*s2 - coeff*s1*s2 |
| 32 | +} |
| 33 | + |
| 34 | +func main() { |
| 35 | + builder, err := audio.NewStreamBuilder() |
| 36 | + if err != nil { |
| 37 | + fmt.Fprintf(os.Stderr, "FAIL: create builder: %v\n", err) |
| 38 | + os.Exit(1) |
| 39 | + } |
| 40 | + defer builder.Close() |
| 41 | + |
| 42 | + builder. |
| 43 | + SetDirection(audio.Input). |
| 44 | + SetSampleRate(48000). |
| 45 | + SetChannelCount(1). |
| 46 | + SetFormat(audio.PcmI16). |
| 47 | + SetPerformanceMode(audio.LowLatency). |
| 48 | + SetSharingMode(audio.Shared) |
| 49 | + |
| 50 | + stream, err := builder.Open() |
| 51 | + if err != nil { |
| 52 | + fmt.Fprintf(os.Stderr, "FAIL: open stream: %v\n", err) |
| 53 | + os.Exit(1) |
| 54 | + } |
| 55 | + defer stream.Close() |
| 56 | + |
| 57 | + actualRate := float64(stream.SampleRate()) |
| 58 | + fmt.Printf("stream opened: rate=%.0f Hz, channels=%d\n", actualRate, stream.ChannelCount()) |
| 59 | + |
| 60 | + if err := stream.Start(); err != nil { |
| 61 | + fmt.Fprintf(os.Stderr, "FAIL: start stream: %v\n", err) |
| 62 | + os.Exit(1) |
| 63 | + } |
| 64 | + |
| 65 | + // Read ~1 second of audio. |
| 66 | + totalFrames := int32(actualRate) |
| 67 | + buf := make([]int16, 1024) |
| 68 | + var captured []int16 |
| 69 | + |
| 70 | + for int32(len(captured)) < totalFrames { |
| 71 | + framesToRead := int32(len(buf)) |
| 72 | + if remaining := totalFrames - int32(len(captured)); remaining < framesToRead { |
| 73 | + framesToRead = remaining |
| 74 | + } |
| 75 | + n, err := stream.Read(unsafe.Pointer(&buf[0]), framesToRead, 1_000_000_000) |
| 76 | + if err != nil { |
| 77 | + fmt.Fprintf(os.Stderr, "FAIL: read: %v\n", err) |
| 78 | + os.Exit(1) |
| 79 | + } |
| 80 | + if n == 0 { |
| 81 | + fmt.Fprintf(os.Stderr, "FAIL: read returned 0 frames\n") |
| 82 | + os.Exit(1) |
| 83 | + } |
| 84 | + captured = append(captured, buf[:n]...) |
| 85 | + } |
| 86 | + |
| 87 | + stream.Stop() |
| 88 | + |
| 89 | + fmt.Printf("captured %d frames\n", len(captured)) |
| 90 | + |
| 91 | + // Goertzel: check 440 Hz vs a few other frequencies. |
| 92 | + targetFreq := 440.0 |
| 93 | + targetMag := goertzelMagnitude(captured, targetFreq, actualRate) |
| 94 | + |
| 95 | + // Check energy at non-target frequencies for comparison. |
| 96 | + otherFreqs := []float64{200, 600, 1000, 2000, 5000} |
| 97 | + var maxOtherMag float64 |
| 98 | + for _, f := range otherFreqs { |
| 99 | + mag := goertzelMagnitude(captured, f, actualRate) |
| 100 | + fmt.Printf(" %.0f Hz magnitude: %.2e\n", f, mag) |
| 101 | + if mag > maxOtherMag { |
| 102 | + maxOtherMag = mag |
| 103 | + } |
| 104 | + } |
| 105 | + fmt.Printf(" 440 Hz magnitude: %.2e\n", targetMag) |
| 106 | + fmt.Printf(" max other magnitude: %.2e\n", maxOtherMag) |
| 107 | + |
| 108 | + // Pass criteria: |
| 109 | + // 1. 440 Hz energy must be significantly above noise floor. |
| 110 | + // 2. 440 Hz must be at least 10x stronger than any other checked frequency. |
| 111 | + if targetMag < 1e10 { |
| 112 | + fmt.Fprintf(os.Stderr, "FAIL: 440 Hz energy too low (%.2e < 1e10)\n", targetMag) |
| 113 | + os.Exit(1) |
| 114 | + } |
| 115 | + if maxOtherMag > 0 && targetMag/maxOtherMag < 10 { |
| 116 | + fmt.Fprintf(os.Stderr, "FAIL: 440 Hz not dominant (ratio=%.1f, need >=10)\n", targetMag/maxOtherMag) |
| 117 | + os.Exit(1) |
| 118 | + } |
| 119 | + |
| 120 | + fmt.Println("PASS: 440 Hz tone detected") |
| 121 | +} |
0 commit comments