diff --git a/station/README.md b/station/README.md index 7875428..401c199 100644 --- a/station/README.md +++ b/station/README.md @@ -88,7 +88,7 @@ mount -o remount,ro / CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -trimpath -ldflags='-s -w' -o scream-receiver ./cmd/scream-receiver ``` -Скопируй бинарник на станцию в `/data/opt/alice-backend` и запусти из этого каталога. Укажи адрес станции и адрес Windows. Приёмник работает с запущенным PulseAudio, принимает моно или стерео PCM в пакетах Scream и воспроизводит их через вход `music`. На этот вход действуют Room EQ и приглушение на время разговора. +Скопируй бинарник на станцию в `/data/opt/alice-backend` и запусти из этого каталога. Укажи адрес станции и адрес Windows. Приёмник работает с запущенным PulseAudio, принимает до восьми каналов PCM в пакетах Scream и сводит их в PulseAudio и воспроизводит их через вход `music`. На этот вход действуют Room EQ и приглушение на время разговора. ```sh ./scream-receiver -listen 192.168.1.135:4010 -source 192.168.1.170 diff --git a/station/cmd/scream-receiver/main.go b/station/cmd/scream-receiver/main.go index 07eed49..97ed991 100644 --- a/station/cmd/scream-receiver/main.go +++ b/station/cmd/scream-receiver/main.go @@ -1,6 +1,7 @@ package main import ( + "encoding/binary" "errors" "flag" "log" @@ -10,6 +11,7 @@ import ( "os/exec" "os/signal" "strconv" + "strings" "syscall" "time" ) @@ -21,13 +23,48 @@ const ( ) type format struct { - rate int - channels byte - sample string + rate int + channels byte + sample string + channelMap string +} + +// Windows sends channels in ascending order of the speaker mask bits. +var speakerPositions = [...]string{ + "front-left", "front-right", "front-center", "lfe", + "rear-left", "rear-right", "front-left-of-center", "front-right-of-center", + "rear-center", "side-left", "side-right", "top-center", + "top-front-left", "top-front-center", "top-front-right", "top-rear-left", +} + +func channelMap(channels byte, mask uint16) string { + if channels == 1 { + return "mono" + } + if channels == 2 { + return "front-left,front-right" + } + positions := make([]string, 0, channels) + for bit, position := range speakerPositions { + if mask&(1< 2 { + if len(packet) != packetBytes || packet[2] < 1 || packet[2] > 8 { return format{}, false } rate := 48000 @@ -49,7 +86,11 @@ func packetFormat(packet []byte) (format, bool) { default: return format{}, false } - return format{rate * multiple, packet[2], sample}, true + if (packetBytes-headerBytes)%(int(packet[2])*int(packet[1]/8)) != 0 { + return format{}, false + } + layout := channelMap(packet[2], binary.BigEndian.Uint16(packet[3:5])) + return format{rate * multiple, packet[2], sample, layout}, layout != "" } const pulse = "/data/opt/alice-backend/spotify" @@ -63,7 +104,8 @@ type playback struct { func startPlayback(f format, latency int) (*playback, error) { cmd := exec.Command(pulse+"/run", pulse+"/bin/pacat", "--playback", "--raw", "--device=music", "--format="+f.sample, "--rate="+strconv.Itoa(f.rate), - "--channels="+strconv.Itoa(int(f.channels)), "--latency-msec="+strconv.Itoa(latency), + "--channels="+strconv.Itoa(int(f.channels)), "--channel-map="+f.channelMap, + "--latency-msec="+strconv.Itoa(latency), "--client-name=PC Audio", "--stream-name=Scream") cmd.Env = append(os.Environ(), "PULSE_SERVER=unix:/tmp/alice-pulse/native", "PULSE_RUNTIME_PATH=/tmp/alice-pulse", "PULSE_COOKIE=/tmp/alice-pulse/cookie") @@ -103,6 +145,7 @@ func receive(conn *net.UDPConn, peer netip.Addr, latency int) error { packet := make([]byte, packetBytes+1) var lastSeen time.Time var retryAfter time.Time + var lastRejected time.Time for { if current != nil { if err := conn.SetReadDeadline(lastSeen.Add(idleTimeout)); err != nil { @@ -126,6 +169,10 @@ func receive(conn *net.UDPConn, peer netip.Addr, latency int) error { } f, valid := packetFormat(packet[:n]) if !valid { + if time.Since(lastRejected) >= 30*time.Second { + log.Printf("unsupported Scream packet from %s: %d bytes, header %x", peer, n, packet[:min(n, headerBytes)]) + lastRejected = time.Now() + } continue } lastSeen = time.Now()