mirror of https://github.com/jetkvm/kvm.git
511 lines
12 KiB
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)
|
|
}
|