Skip to content

Commit 43a3990

Browse files
xaionaro@dx.centerxaionaro@dx.center
authored andcommitted
Add host-side gRPC audio injector for E2E tests
Generates a sine wave and streams it to the Android emulator's virtual microphone via the injectAudio gRPC API.
1 parent eaf1df9 commit 43a3990

1 file changed

Lines changed: 93 additions & 0 deletions

File tree

tests/e2e/audio-inject/main.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Audio injector: streams a sine wave to the Android emulator's virtual
2+
// microphone via the gRPC injectAudio API.
3+
//
4+
// Usage: audio-inject -port 8554 -freq 440 -duration 3s
5+
package main
6+
7+
import (
8+
"context"
9+
"encoding/binary"
10+
"flag"
11+
"fmt"
12+
"io"
13+
"log"
14+
"math"
15+
"time"
16+
17+
emupb "github.com/xaionaro-go/ndk/tests/e2e/audio-inject/proto/emupb"
18+
"google.golang.org/grpc"
19+
"google.golang.org/grpc/credentials/insecure"
20+
)
21+
22+
func main() {
23+
port := flag.Int("port", 8554, "emulator gRPC port")
24+
freq := flag.Float64("freq", 440, "sine wave frequency in Hz")
25+
dur := flag.Duration("duration", 3*time.Second, "injection duration")
26+
sampleRate := flag.Uint64("rate", 48000, "sample rate in Hz")
27+
flag.Parse()
28+
29+
addr := fmt.Sprintf("localhost:%d", *port)
30+
log.Printf("connecting to emulator gRPC at %s", addr)
31+
32+
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
33+
if err != nil {
34+
log.Fatalf("grpc dial: %v", err)
35+
}
36+
defer conn.Close()
37+
38+
client := emupb.NewEmulatorControllerClient(conn)
39+
stream, err := client.InjectAudio(context.Background())
40+
if err != nil {
41+
log.Fatalf("InjectAudio: %v", err)
42+
}
43+
44+
// Send audio in 10ms chunks.
45+
samplesPerChunk := int(*sampleRate / 100)
46+
totalSamples := int(float64(*sampleRate) * dur.Seconds())
47+
sent := 0
48+
phase := 0.0
49+
phaseInc := 2.0 * math.Pi * *freq / float64(*sampleRate)
50+
51+
format := &emupb.AudioFormat{
52+
SamplingRate: *sampleRate,
53+
Channels: emupb.AudioFormat_Mono,
54+
Format: emupb.AudioFormat_AUD_FMT_S16,
55+
}
56+
57+
for sent < totalSamples {
58+
n := samplesPerChunk
59+
if sent+n > totalSamples {
60+
n = totalSamples - sent
61+
}
62+
63+
buf := make([]byte, n*2) // 2 bytes per S16LE sample
64+
for i := 0; i < n; i++ {
65+
sample := int16(math.Sin(phase) * 0.8 * 32767)
66+
binary.LittleEndian.PutUint16(buf[i*2:], uint16(sample))
67+
phase += phaseInc
68+
}
69+
70+
pkt := &emupb.AudioPacket{
71+
Audio: buf,
72+
}
73+
if sent == 0 {
74+
pkt.Format = format
75+
}
76+
77+
if err := stream.Send(pkt); err != nil {
78+
if err == io.EOF {
79+
break
80+
}
81+
log.Fatalf("send: %v", err)
82+
}
83+
sent += n
84+
85+
// Pace at roughly real-time to avoid overflowing the 300ms buffer.
86+
time.Sleep(10 * time.Millisecond)
87+
}
88+
89+
if _, err := stream.CloseAndRecv(); err != nil && err != io.EOF {
90+
log.Printf("close stream: %v", err)
91+
}
92+
log.Printf("injected %d samples (%.2fs) at %d Hz", sent, float64(sent)/float64(*sampleRate), *sampleRate)
93+
}

0 commit comments

Comments
 (0)