kvm/internal/audio/unified_ipc.go

511 lines
12 KiB
Go

package audio
import (
"encoding/binary"
"fmt"
"io"
"net"
"os"
"path/filepath"
"sync"
"sync/atomic"
"time"
"github.com/jetkvm/kvm/internal/logging"
"github.com/rs/zerolog"
)
// Unified IPC constants
var (
outputMagicNumber uint32 = GetConfig().OutputMagicNumber // "JKOU" (JetKVM Output)
inputMagicNumber uint32 = GetConfig().InputMagicNumber // "JKMI" (JetKVM Microphone Input)
outputSocketName = "audio_output.sock"
inputSocketName = "audio_input.sock"
headerSize = 17 // Fixed header size: 4+1+4+8 bytes
)
// UnifiedMessageType represents the type of IPC message for both input and output
type UnifiedMessageType uint8
const (
MessageTypeOpusFrame UnifiedMessageType = iota
MessageTypeConfig
MessageTypeOpusConfig
MessageTypeStop
MessageTypeHeartbeat
MessageTypeAck
)
// UnifiedIPCMessage represents a message sent over IPC for both input and output
type UnifiedIPCMessage struct {
Magic uint32
Type UnifiedMessageType
Length uint32
Timestamp int64
Data []byte
}
// Implement IPCMessage interface
func (msg *UnifiedIPCMessage) GetMagic() uint32 {
return msg.Magic
}
func (msg *UnifiedIPCMessage) GetType() uint8 {
return uint8(msg.Type)
}
func (msg *UnifiedIPCMessage) GetLength() uint32 {
return msg.Length
}
func (msg *UnifiedIPCMessage) GetTimestamp() int64 {
return msg.Timestamp
}
func (msg *UnifiedIPCMessage) GetData() []byte {
return msg.Data
}
// UnifiedIPCConfig represents configuration for audio
type UnifiedIPCConfig struct {
SampleRate int
Channels int
FrameSize int
}
// UnifiedIPCOpusConfig represents Opus-specific configuration
type UnifiedIPCOpusConfig struct {
SampleRate int
Channels int
FrameSize int
Bitrate int
Complexity int
VBR int
SignalType int
Bandwidth int
DTX int
}
// UnifiedAudioServer provides common functionality for both input and output servers
type UnifiedAudioServer struct {
// Atomic counters for performance monitoring
bufferSize int64 // Current buffer size (atomic)
droppedFrames int64 // Dropped frames counter (atomic)
totalFrames int64 // Total frames counter (atomic)
listener net.Listener
conn net.Conn
mtx sync.Mutex
running bool
logger zerolog.Logger
// Message channels
messageChan chan *UnifiedIPCMessage // Buffered channel for incoming messages
processChan chan *UnifiedIPCMessage // Buffered channel for processing queue
wg sync.WaitGroup // Wait group for goroutine coordination
// Configuration
socketPath string
magicNumber uint32
socketBufferConfig SocketBufferConfig
// Performance monitoring
latencyMonitor *LatencyMonitor
adaptiveOptimizer *AdaptiveOptimizer
}
// NewUnifiedAudioServer creates a new unified audio server
func NewUnifiedAudioServer(isInput bool) (*UnifiedAudioServer, error) {
var socketPath string
var magicNumber uint32
var componentName string
if isInput {
socketPath = getInputSocketPath()
magicNumber = inputMagicNumber
componentName = "audio-input-server"
} else {
socketPath = getOutputSocketPath()
magicNumber = outputMagicNumber
componentName = "audio-output-server"
}
logger := logging.GetDefaultLogger().With().Str("component", componentName).Logger()
server := &UnifiedAudioServer{
logger: logger,
socketPath: socketPath,
magicNumber: magicNumber,
messageChan: make(chan *UnifiedIPCMessage, GetConfig().ChannelBufferSize),
processChan: make(chan *UnifiedIPCMessage, GetConfig().ChannelBufferSize),
socketBufferConfig: DefaultSocketBufferConfig(),
latencyMonitor: nil,
adaptiveOptimizer: nil,
}
return server, nil
}
// Start starts the unified audio server
func (s *UnifiedAudioServer) Start() error {
s.mtx.Lock()
defer s.mtx.Unlock()
if s.running {
return fmt.Errorf("server already running")
}
// Remove existing socket file
if err := os.Remove(s.socketPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove existing socket: %w", err)
}
// Create listener
listener, err := net.Listen("unix", s.socketPath)
if err != nil {
return fmt.Errorf("failed to create listener: %w", err)
}
s.listener = listener
s.running = true
// Start goroutines
s.wg.Add(3)
go s.acceptConnections()
go s.startReaderGoroutine()
go s.startProcessorGoroutine()
s.logger.Info().Str("socket_path", s.socketPath).Msg("Unified audio server started")
return nil
}
// Stop stops the unified audio server
func (s *UnifiedAudioServer) Stop() {
s.mtx.Lock()
defer s.mtx.Unlock()
if !s.running {
return
}
s.running = false
if s.listener != nil {
s.listener.Close()
}
if s.conn != nil {
s.conn.Close()
}
// Close channels
close(s.messageChan)
close(s.processChan)
// Wait for goroutines to finish
s.wg.Wait()
// Remove socket file
os.Remove(s.socketPath)
s.logger.Info().Msg("Unified audio server stopped")
}
// acceptConnections handles incoming connections
func (s *UnifiedAudioServer) acceptConnections() {
defer s.wg.Done()
for s.running {
conn, err := AcceptConnectionWithRetry(s.listener, 3, 100*time.Millisecond)
if err != nil {
if s.running {
s.logger.Error().Err(err).Msg("Failed to accept connection")
}
continue
}
s.mtx.Lock()
if s.conn != nil {
s.conn.Close()
}
s.conn = conn
s.mtx.Unlock()
s.logger.Info().Msg("Client connected")
}
}
// startReaderGoroutine handles reading messages from the connection
func (s *UnifiedAudioServer) startReaderGoroutine() {
defer s.wg.Done()
for s.running {
s.mtx.Lock()
conn := s.conn
s.mtx.Unlock()
if conn == nil {
time.Sleep(10 * time.Millisecond)
continue
}
msg, err := s.readMessage(conn)
if err != nil {
if s.running {
s.logger.Error().Err(err).Msg("Failed to read message")
}
continue
}
select {
case s.messageChan <- msg:
default:
atomic.AddInt64(&s.droppedFrames, 1)
s.logger.Warn().Msg("Message channel full, dropping message")
}
}
}
// startProcessorGoroutine handles processing messages
func (s *UnifiedAudioServer) startProcessorGoroutine() {
defer s.wg.Done()
for msg := range s.messageChan {
select {
case s.processChan <- msg:
atomic.AddInt64(&s.totalFrames, 1)
default:
atomic.AddInt64(&s.droppedFrames, 1)
s.logger.Warn().Msg("Process channel full, dropping message")
}
}
}
// readMessage reads a message from the connection
func (s *UnifiedAudioServer) readMessage(conn net.Conn) (*UnifiedIPCMessage, error) {
// Read header
header := make([]byte, headerSize)
if _, err := io.ReadFull(conn, header); err != nil {
return nil, fmt.Errorf("failed to read header: %w", err)
}
// Parse header
magic := binary.LittleEndian.Uint32(header[0:4])
if magic != s.magicNumber {
return nil, fmt.Errorf("invalid magic number: expected %d, got %d", s.magicNumber, magic)
}
msgType := UnifiedMessageType(header[4])
length := binary.LittleEndian.Uint32(header[5:9])
timestamp := int64(binary.LittleEndian.Uint64(header[9:17]))
// Validate length
if length > uint32(GetConfig().MaxFrameSize) {
return nil, fmt.Errorf("message too large: %d bytes", length)
}
// Read data
var data []byte
if length > 0 {
data = make([]byte, length)
if _, err := io.ReadFull(conn, data); err != nil {
return nil, fmt.Errorf("failed to read data: %w", err)
}
}
return &UnifiedIPCMessage{
Magic: magic,
Type: msgType,
Length: length,
Timestamp: timestamp,
Data: data,
}, nil
}
// SendFrame sends a frame to the connected client
func (s *UnifiedAudioServer) SendFrame(frame []byte) error {
s.mtx.Lock()
defer s.mtx.Unlock()
if !s.running || s.conn == nil {
return fmt.Errorf("no client connected")
}
start := time.Now()
// Create message
msg := &UnifiedIPCMessage{
Magic: s.magicNumber,
Type: MessageTypeOpusFrame,
Length: uint32(len(frame)),
Timestamp: start.UnixNano(),
Data: frame,
}
// Write message to connection
err := s.writeMessage(s.conn, msg)
if err != nil {
atomic.AddInt64(&s.droppedFrames, 1)
return err
}
// Record latency for monitoring
if s.latencyMonitor != nil {
writeLatency := time.Since(start)
s.latencyMonitor.RecordLatency(writeLatency, "ipc_write")
}
atomic.AddInt64(&s.totalFrames, 1)
return nil
}
// writeMessage writes a message to the connection
func (s *UnifiedAudioServer) writeMessage(conn net.Conn, msg *UnifiedIPCMessage) error {
// Write header
header := make([]byte, headerSize)
binary.LittleEndian.PutUint32(header[0:4], msg.Magic)
header[4] = uint8(msg.Type)
binary.LittleEndian.PutUint32(header[5:9], msg.Length)
binary.LittleEndian.PutUint64(header[9:17], uint64(msg.Timestamp))
if _, err := conn.Write(header); err != nil {
return fmt.Errorf("failed to write header: %w", err)
}
// Write data if present
if msg.Length > 0 && msg.Data != nil {
if _, err := conn.Write(msg.Data); err != nil {
return fmt.Errorf("failed to write data: %w", err)
}
}
return nil
}
// UnifiedAudioClient provides common functionality for both input and output clients
type UnifiedAudioClient struct {
// Atomic fields first for ARM32 alignment
droppedFrames int64 // Atomic counter for dropped frames
totalFrames int64 // Atomic counter for total frames
conn net.Conn
mtx sync.Mutex
running bool
logger zerolog.Logger
socketPath string
magicNumber uint32
bufferPool *AudioBufferPool // Buffer pool for memory optimization
}
// NewUnifiedAudioClient creates a new unified audio client
func NewUnifiedAudioClient(isInput bool) *UnifiedAudioClient {
var socketPath string
var magicNumber uint32
var componentName string
if isInput {
socketPath = getInputSocketPath()
magicNumber = inputMagicNumber
componentName = "audio-input-client"
} else {
socketPath = getOutputSocketPath()
magicNumber = outputMagicNumber
componentName = "audio-output-client"
}
logger := logging.GetDefaultLogger().With().Str("component", componentName).Logger()
return &UnifiedAudioClient{
logger: logger,
socketPath: socketPath,
magicNumber: magicNumber,
bufferPool: NewAudioBufferPool(GetConfig().MaxFrameSize),
}
}
// Connect connects the client to the server
func (c *UnifiedAudioClient) Connect() error {
c.mtx.Lock()
defer c.mtx.Unlock()
if c.running {
return nil // Already connected
}
// Ensure clean state before connecting
if c.conn != nil {
c.conn.Close()
c.conn = nil
}
// Try connecting multiple times as the server might not be ready
// Reduced retry count and delay for faster startup
for i := 0; i < 10; i++ {
conn, err := net.Dial("unix", c.socketPath)
if err == nil {
c.conn = conn
c.running = true
// Reset frame counters on successful connection
atomic.StoreInt64(&c.totalFrames, 0)
atomic.StoreInt64(&c.droppedFrames, 0)
c.logger.Info().Str("socket_path", c.socketPath).Msg("Connected to server")
return nil
}
// Exponential backoff starting from config
backoffStart := GetConfig().BackoffStart
delay := time.Duration(backoffStart.Nanoseconds()*(1<<uint(i/3))) * time.Nanosecond
maxDelay := GetConfig().MaxRetryDelay
if delay > maxDelay {
delay = maxDelay
}
time.Sleep(delay)
}
// Ensure clean state on connection failure
c.conn = nil
c.running = false
return fmt.Errorf("failed to connect to audio server after 10 attempts")
}
// Disconnect disconnects the client from the server
func (c *UnifiedAudioClient) Disconnect() {
c.mtx.Lock()
defer c.mtx.Unlock()
if !c.running {
return
}
c.running = false
if c.conn != nil {
c.conn.Close()
c.conn = nil
}
c.logger.Info().Msg("Disconnected from server")
}
// IsConnected returns whether the client is connected
func (c *UnifiedAudioClient) IsConnected() bool {
c.mtx.Lock()
defer c.mtx.Unlock()
return c.running && c.conn != nil
}
// GetFrameStats returns frame statistics
func (c *UnifiedAudioClient) GetFrameStats() (total, dropped int64) {
total = atomic.LoadInt64(&c.totalFrames)
dropped = atomic.LoadInt64(&c.droppedFrames)
return total, dropped
}
// Helper functions for socket paths
func getInputSocketPath() string {
return filepath.Join(os.TempDir(), inputSocketName)
}
func getOutputSocketPath() string {
return filepath.Join(os.TempDir(), outputSocketName)
}