Fix Audio Pipeline: EAX Decrypt, Decoder State Fix, Opus Optimization
This commit is contained in:
16
Dockerfile
Normal file
16
Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
FROM golang:1.24-bookworm
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
pkg-config \
|
||||||
|
libopus-dev \
|
||||||
|
libopusfile-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN go build -o ts-client ./cmd/client/main.go
|
||||||
|
|
||||||
|
ENTRYPOINT ["./ts-client"]
|
||||||
BIN
client.exe
Normal file
BIN
client.exe
Normal file
Binary file not shown.
@@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"go-ts/internal/client"
|
"go-ts/internal/client"
|
||||||
)
|
)
|
||||||
@@ -15,6 +16,10 @@ func main() {
|
|||||||
nickname := flag.String("nickname", "GoCient", "Nickname")
|
nickname := flag.String("nickname", "GoCient", "Nickname")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
// Wait for server to start (Docker fix)
|
||||||
|
log.Println("Waiting 5 seconds for server to start...")
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
log.Printf("Starting TS3 Client...")
|
log.Printf("Starting TS3 Client...")
|
||||||
log.Printf("Server: %s", *serverAddr)
|
log.Printf("Server: %s", *serverAddr)
|
||||||
log.Printf("Nickname: %s", *nickname)
|
log.Printf("Nickname: %s", *nickname)
|
||||||
|
|||||||
@@ -19,5 +19,12 @@ services:
|
|||||||
max-size: "10m"
|
max-size: "10m"
|
||||||
max-file: "3"
|
max-file: "3"
|
||||||
|
|
||||||
|
client:
|
||||||
|
build: .
|
||||||
|
depends_on:
|
||||||
|
- teamspeak
|
||||||
|
command: ["--server", "teamspeak:9987"]
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
ts3-data:
|
ts3-data:
|
||||||
|
|||||||
9
go.mod
9
go.mod
@@ -4,10 +4,9 @@ go 1.24.0
|
|||||||
|
|
||||||
toolchain go1.24.11
|
toolchain go1.24.11
|
||||||
|
|
||||||
require filippo.io/edwards25519 v1.1.0
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Hiroko103/go-quicklz v0.0.0-20190115215310-59904abc50d0 // indirect
|
filippo.io/edwards25519 v1.1.0
|
||||||
github.com/dgryski/go-quicklz v0.0.0-20151014073603-d7042a82d57e // indirect
|
github.com/dgryski/go-quicklz v0.0.0-20151014073603-d7042a82d57e
|
||||||
golang.org/x/crypto v0.47.0 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -1,8 +1,6 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/Hiroko103/go-quicklz v0.0.0-20190115215310-59904abc50d0 h1:6+dYUy8Dg9WdPkXnA3MtdAZq1ry1+pLrc+ZMCMi0MPU=
|
|
||||||
github.com/Hiroko103/go-quicklz v0.0.0-20190115215310-59904abc50d0/go.mod h1:qBfAulKipfG2mar83JHqv6ykKbgDXHbmTWPY5gvr2zw=
|
|
||||||
github.com/dgryski/go-quicklz v0.0.0-20151014073603-d7042a82d57e h1:MhBotBstN1h/GeA7lx7xstbFB8avummjt+nzOi2cY7Y=
|
github.com/dgryski/go-quicklz v0.0.0-20151014073603-d7042a82d57e h1:MhBotBstN1h/GeA7lx7xstbFB8avummjt+nzOi2cY7Y=
|
||||||
github.com/dgryski/go-quicklz v0.0.0-20151014073603-d7042a82d57e/go.mod h1:XLmYwGWgVzMPLlMmcNcWt3b5ixRabPLstWnPVEDRhzc=
|
github.com/dgryski/go-quicklz v0.0.0-20151014073603-d7042a82d57e/go.mod h1:XLmYwGWgVzMPLlMmcNcWt3b5ixRabPLstWnPVEDRhzc=
|
||||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 h1:xeVptzkP8BuJhoIjNizd2bRHfq9KB9HfOLZu90T04XM=
|
||||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302/go.mod h1:/L5E7a21VWl8DeuCPKxQBdVG5cy+L0MRZ08B1wnqt7g=
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
|
|
||||||
"go-ts/pkg/protocol"
|
"go-ts/pkg/protocol"
|
||||||
"go-ts/pkg/transport"
|
"go-ts/pkg/transport"
|
||||||
|
|
||||||
|
"gopkg.in/hraban/opus.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Channel struct {
|
type Channel struct {
|
||||||
@@ -24,6 +26,7 @@ type Client struct {
|
|||||||
|
|
||||||
// Counters
|
// Counters
|
||||||
PacketIDCounterC2S uint16
|
PacketIDCounterC2S uint16
|
||||||
|
VoicePacketID uint16
|
||||||
|
|
||||||
// State
|
// State
|
||||||
Connected bool
|
Connected bool
|
||||||
@@ -36,13 +39,19 @@ type Client struct {
|
|||||||
|
|
||||||
// Server Data
|
// Server Data
|
||||||
Channels map[uint64]*Channel
|
Channels map[uint64]*Channel
|
||||||
|
|
||||||
|
// Audio
|
||||||
|
VoiceDecoders map[uint16]*opus.Decoder // Map VID (sender ID) to decoder
|
||||||
|
VoiceEncoder *opus.Encoder // Encoder for outgoing audio
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(nickname string) *Client {
|
func NewClient(nickname string) *Client {
|
||||||
return &Client{
|
return &Client{
|
||||||
Nickname: nickname,
|
Nickname: nickname,
|
||||||
PacketIDCounterC2S: 1,
|
PacketIDCounterC2S: 1,
|
||||||
|
VoicePacketID: 1,
|
||||||
Channels: make(map[uint64]*Channel),
|
Channels: make(map[uint64]*Channel),
|
||||||
|
VoiceDecoders: make(map[uint16]*opus.Decoder),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,34 +5,154 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
|
|
||||||
"go-ts/pkg/protocol"
|
"go-ts/pkg/protocol"
|
||||||
|
|
||||||
|
"gopkg.in/hraban/opus.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TeamSpeak Codecs
|
||||||
|
CodecOpusVoice = 4
|
||||||
|
CodecOpusMusic = 5
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Client) handleVoice(pkt *protocol.Packet) {
|
func (c *Client) handleVoice(pkt *protocol.Packet) {
|
||||||
|
// Only process Opus packets
|
||||||
// Parse Voice Header (Server -> Client)
|
// Parse Voice Header (Server -> Client)
|
||||||
// VID(2) + CID(2) + Codec(1) + Data
|
// VId(2) + CId(2) + Codec(1) + Data
|
||||||
if len(pkt.Data) < 5 {
|
|
||||||
|
var data []byte = pkt.Data
|
||||||
|
|
||||||
|
// Decrypt if Encrypted
|
||||||
|
if !pkt.Header.FlagUnencrypted() {
|
||||||
|
if c.Handshake == nil || len(c.Handshake.SharedIV) == 0 {
|
||||||
|
log.Println("Received encrypted voice packet but no SharedIV available. Dropping.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
crypto := &protocol.CryptoState{
|
||||||
|
SharedIV: c.Handshake.SharedIV,
|
||||||
|
SharedMac: c.Handshake.SharedMac,
|
||||||
|
GenerationID: 0,
|
||||||
|
}
|
||||||
|
// Server->Client = false
|
||||||
|
key, nonce := crypto.GenerateKeyNonce(&pkt.Header, false)
|
||||||
|
|
||||||
|
// Meta for Server->Client: PID(2) + PT(1) = 3 bytes
|
||||||
|
meta := make([]byte, 3)
|
||||||
|
binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID)
|
||||||
|
meta[2] = pkt.Header.Type
|
||||||
|
|
||||||
|
var err error
|
||||||
|
data, err = protocol.DecryptEAX(key, nonce, meta, pkt.Data, pkt.Header.MAC[:])
|
||||||
|
if err != nil {
|
||||||
|
// Voice decryption failure is common if keys mismatch or packet loss affects counter?
|
||||||
|
// But EAX doesn't depend on counter state, only PID which is in header.
|
||||||
|
log.Printf("Voice decryption failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) < 6 {
|
||||||
|
// Ignore empty/too small packets (e.g. silence or end)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
vid := binary.BigEndian.Uint16(pkt.Data[0:2])
|
// The first 2 bytes are the sequence ID, not the client ID for the decoder key.
|
||||||
// cid := binary.BigEndian.Uint16(pkt.Data[2:4]) // Talking client ID (not needed for echo)
|
// The client ID is at data[2:4]
|
||||||
codec := pkt.Data[4]
|
// vid := binary.BigEndian.Uint16(data[0:2]) // Sequence ID
|
||||||
voiceData := pkt.Data[5:]
|
vid := binary.BigEndian.Uint16(data[2:4]) // Talking Client ID (CORRECT KEY)
|
||||||
|
codec := data[4]
|
||||||
|
voiceData := data[5:]
|
||||||
|
|
||||||
log.Printf("Voice Packet received. VID=%d, Codec=%d, Size=%d", vid, codec, len(voiceData))
|
// Only process Opus packets
|
||||||
|
if codec != CodecOpusVoice && codec != CodecOpusMusic {
|
||||||
|
log.Printf("Received non-Opus voice packet (Codec=%d). Ignoring echo.", codec)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Build echo packet (Client -> Server)
|
channels := 1
|
||||||
// Format: VID(2) + Codec(1) + Data
|
if codec == CodecOpusMusic {
|
||||||
echoData := make([]byte, 2+1+len(voiceData))
|
channels = 2
|
||||||
binary.BigEndian.PutUint16(echoData[0:2], vid)
|
}
|
||||||
|
|
||||||
|
// 1. Get or Create Decoder for this VID
|
||||||
|
decoder, ok := c.VoiceDecoders[vid]
|
||||||
|
if !ok {
|
||||||
|
var err error
|
||||||
|
decoder, err = opus.NewDecoder(48000, channels)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to create Opus decoder: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.VoiceDecoders[vid] = decoder
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Decode Opus to PCM
|
||||||
|
// Max frame size for 120ms at 48kHz is 5760 samples
|
||||||
|
pcm := make([]int16, 5760*channels)
|
||||||
|
n, err := decoder.Decode(voiceData, pcm)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Opus decode error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pcm = pcm[:n*channels]
|
||||||
|
|
||||||
|
if n != 960 {
|
||||||
|
log.Printf("WARNING: Unusual Opus frame size: %d samples (expected 960 for 20ms)", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Process PCM: Reduce Volume (divide by 4)
|
||||||
|
for i := range pcm {
|
||||||
|
pcm[i] = pcm[i] / 4
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Get or Create Encoder
|
||||||
|
if c.VoiceEncoder == nil {
|
||||||
|
var err error
|
||||||
|
app := opus.AppVoIP
|
||||||
|
if channels == 2 {
|
||||||
|
app = opus.AppAudio
|
||||||
|
}
|
||||||
|
encoder, err := opus.NewEncoder(48000, channels, app)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to create Opus encoder: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimize Quality
|
||||||
|
encoder.SetBitrate(64000) // 64 kbps (High Quality for Voice)
|
||||||
|
encoder.SetComplexity(10) // Max Complexity (Best Quality)
|
||||||
|
|
||||||
|
c.VoiceEncoder = encoder
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Encode PCM to Opus
|
||||||
|
encoded := make([]byte, 1024)
|
||||||
|
nEnc, err := c.VoiceEncoder.Encode(pcm, encoded)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Opus encode error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
encoded = encoded[:nEnc]
|
||||||
|
|
||||||
|
// log.Printf("Voice Processed (CGO): VID=%d, In=%d bytes, PCM=%d samples, Out=%d bytes", vid, len(voiceData), n, nEnc)
|
||||||
|
|
||||||
|
// 6. Build echo packet (Client -> Server)
|
||||||
|
// Payload format: [VId(2)] [Codec(1)] [Data...]
|
||||||
|
echoData := make([]byte, 2+1+len(encoded))
|
||||||
|
|
||||||
|
c.VoicePacketID++ // Increment counter before using it
|
||||||
|
|
||||||
|
// Correctly set VId in Payload to be the Sequence Number (not ClientID)
|
||||||
|
binary.BigEndian.PutUint16(echoData[0:2], c.VoicePacketID)
|
||||||
echoData[2] = codec
|
echoData[2] = codec
|
||||||
copy(echoData[3:], voiceData)
|
copy(echoData[3:], encoded)
|
||||||
|
|
||||||
echoPkt := protocol.NewPacket(protocol.PacketTypeVoice, echoData)
|
echoPkt := protocol.NewPacket(protocol.PacketTypeVoice, echoData)
|
||||||
echoPkt.Header.PacketID = pkt.Header.PacketID // Use same ID for voice
|
echoPkt.Header.PacketID = c.VoicePacketID
|
||||||
echoPkt.Header.ClientID = c.ClientID
|
echoPkt.Header.ClientID = c.ClientID
|
||||||
|
|
||||||
// Encrypt voice packet with SharedSecret
|
// Encrypt voice packet
|
||||||
if c.Handshake != nil && len(c.Handshake.SharedIV) > 0 {
|
if c.Handshake != nil && len(c.Handshake.SharedIV) > 0 {
|
||||||
crypto := &protocol.CryptoState{
|
crypto := &protocol.CryptoState{
|
||||||
SharedIV: c.Handshake.SharedIV,
|
SharedIV: c.Handshake.SharedIV,
|
||||||
@@ -41,7 +161,6 @@ func (c *Client) handleVoice(pkt *protocol.Packet) {
|
|||||||
}
|
}
|
||||||
key, nonce := crypto.GenerateKeyNonce(&echoPkt.Header, true)
|
key, nonce := crypto.GenerateKeyNonce(&echoPkt.Header, true)
|
||||||
|
|
||||||
// Meta for Client->Server: PID(2) + CID(2) + PT(1)
|
|
||||||
meta := make([]byte, 5)
|
meta := make([]byte, 5)
|
||||||
binary.BigEndian.PutUint16(meta[0:2], echoPkt.Header.PacketID)
|
binary.BigEndian.PutUint16(meta[0:2], echoPkt.Header.PacketID)
|
||||||
binary.BigEndian.PutUint16(meta[2:4], echoPkt.Header.ClientID)
|
binary.BigEndian.PutUint16(meta[2:4], echoPkt.Header.ClientID)
|
||||||
@@ -55,7 +174,6 @@ func (c *Client) handleVoice(pkt *protocol.Packet) {
|
|||||||
echoPkt.Data = encData
|
echoPkt.Data = encData
|
||||||
copy(echoPkt.Header.MAC[:], mac)
|
copy(echoPkt.Header.MAC[:], mac)
|
||||||
} else {
|
} else {
|
||||||
// If no encryption keys, use SharedMac if available, otherwise HandshakeMac
|
|
||||||
if c.Handshake != nil && len(c.Handshake.SharedMac) > 0 {
|
if c.Handshake != nil && len(c.Handshake.SharedMac) > 0 {
|
||||||
copy(echoPkt.Header.MAC[:], c.Handshake.SharedMac)
|
copy(echoPkt.Header.MAC[:], c.Handshake.SharedMac)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
BIN
libgcc_s_seh-1.dll
Normal file
BIN
libgcc_s_seh-1.dll
Normal file
Binary file not shown.
BIN
libiconv-2.dll
Normal file
BIN
libiconv-2.dll
Normal file
Binary file not shown.
BIN
libintl-8.dll
Normal file
BIN
libintl-8.dll
Normal file
Binary file not shown.
BIN
libogg-0.dll
Normal file
BIN
libogg-0.dll
Normal file
Binary file not shown.
BIN
libopus-0.dll
Normal file
BIN
libopus-0.dll
Normal file
Binary file not shown.
BIN
libopusfile-0.dll
Normal file
BIN
libopusfile-0.dll
Normal file
Binary file not shown.
BIN
libstdc++-6.dll
Normal file
BIN
libstdc++-6.dll
Normal file
Binary file not shown.
BIN
libwinpthread-1.dll
Normal file
BIN
libwinpthread-1.dll
Normal file
Binary file not shown.
5
run.ps1
Normal file
5
run.ps1
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
$env:PATH = "D:\esto_al_path\msys64\mingw64\bin;$env:PATH"
|
||||||
|
$env:PKG_CONFIG_PATH = "D:\esto_al_path\msys64\mingw64\lib\pkgconfig"
|
||||||
|
|
||||||
|
Write-Host "Starting TeamSpeak Client (Windows Native)..." -ForegroundColor Cyan
|
||||||
|
go run ./cmd/client/main.go --server localhost:9987
|
||||||
BIN
ts-client.exe
Normal file
BIN
ts-client.exe
Normal file
Binary file not shown.
Reference in New Issue
Block a user