Accept Windows surround audio on Mini 2

This commit is contained in:
2026-11-14 15:15:19 +03:00
parent 00e2c3cd99
commit 44d191831f
2 changed files with 54 additions and 7 deletions
+1 -1
View File
@@ -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
+53 -6
View File
@@ -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<<bit) != 0 {
positions = append(positions, position)
}
}
if len(positions) == int(channels) {
return strings.Join(positions, ",")
}
// Some endpoints omit the speaker mask. Use the conventional surround order.
if channels == 6 {
return "front-left,front-right,front-center,lfe,rear-left,rear-right"
}
if channels == 8 {
return "front-left,front-right,front-center,lfe,rear-left,rear-right,side-left,side-right"
}
return ""
}
func packetFormat(packet []byte) (format, bool) {
if len(packet) != packetBytes || packet[2] < 1 || packet[2] > 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()