mirror of https://github.com/jetkvm/kvm.git
191 lines
5.5 KiB
Go
191 lines
5.5 KiB
Go
package audio
|
|
|
|
import (
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/jetkvm/kvm/internal/logging"
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
// AudioInputIPCManager manages microphone input using IPC when enabled
|
|
type AudioInputIPCManager struct {
|
|
// metrics MUST be first for ARM32 alignment (contains int64 fields)
|
|
metrics AudioInputMetrics
|
|
|
|
supervisor *AudioInputSupervisor
|
|
logger zerolog.Logger
|
|
running int32
|
|
}
|
|
|
|
// NewAudioInputIPCManager creates a new IPC-based audio input manager
|
|
func NewAudioInputIPCManager() *AudioInputIPCManager {
|
|
return &AudioInputIPCManager{
|
|
supervisor: NewAudioInputSupervisor(),
|
|
logger: logging.GetDefaultLogger().With().Str("component", "audio-input-ipc").Logger(),
|
|
}
|
|
}
|
|
|
|
// Start starts the IPC-based audio input system
|
|
func (aim *AudioInputIPCManager) Start() error {
|
|
if !atomic.CompareAndSwapInt32(&aim.running, 0, 1) {
|
|
return nil // Already running
|
|
}
|
|
|
|
aim.logger.Info().Msg("Starting IPC-based audio input system")
|
|
|
|
// Start the supervisor which will launch the subprocess
|
|
err := aim.supervisor.Start()
|
|
if err != nil {
|
|
atomic.StoreInt32(&aim.running, 0)
|
|
return err
|
|
}
|
|
|
|
// Send initial configuration
|
|
config := InputIPCConfig{
|
|
SampleRate: 48000,
|
|
Channels: 2,
|
|
FrameSize: 960, // 20ms at 48kHz
|
|
}
|
|
|
|
// Wait a bit for the subprocess to be ready
|
|
time.Sleep(time.Second)
|
|
|
|
err = aim.supervisor.SendConfig(config)
|
|
if err != nil {
|
|
aim.logger.Warn().Err(err).Msg("Failed to send initial config to audio input server")
|
|
// Don't fail startup for config errors
|
|
}
|
|
|
|
aim.logger.Info().Msg("IPC-based audio input system started")
|
|
return nil
|
|
}
|
|
|
|
// Stop stops the IPC-based audio input system
|
|
func (aim *AudioInputIPCManager) Stop() {
|
|
if !atomic.CompareAndSwapInt32(&aim.running, 1, 0) {
|
|
return // Already stopped
|
|
}
|
|
|
|
aim.logger.Info().Msg("Stopping IPC-based audio input system")
|
|
|
|
// Stop the supervisor
|
|
aim.supervisor.Stop()
|
|
|
|
aim.logger.Info().Msg("IPC-based audio input system stopped")
|
|
}
|
|
|
|
// WriteOpusFrame sends an Opus frame to the audio input server via IPC
|
|
func (aim *AudioInputIPCManager) WriteOpusFrame(frame []byte) error {
|
|
if atomic.LoadInt32(&aim.running) == 0 {
|
|
return nil // Not running, silently ignore
|
|
}
|
|
|
|
if len(frame) == 0 {
|
|
return nil // Empty frame, ignore
|
|
}
|
|
|
|
// Start latency measurement
|
|
startTime := time.Now()
|
|
|
|
// Update metrics
|
|
atomic.AddInt64(&aim.metrics.FramesSent, 1)
|
|
atomic.AddInt64(&aim.metrics.BytesProcessed, int64(len(frame)))
|
|
aim.metrics.LastFrameTime = startTime
|
|
|
|
// Send frame via IPC
|
|
err := aim.supervisor.SendFrame(frame)
|
|
if err != nil {
|
|
// Count as dropped frame
|
|
atomic.AddInt64(&aim.metrics.FramesDropped, 1)
|
|
aim.logger.Debug().Err(err).Msg("Failed to send frame via IPC")
|
|
return err
|
|
}
|
|
|
|
// Calculate and update latency
|
|
latency := time.Since(startTime)
|
|
aim.updateLatencyMetrics(latency)
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsRunning returns whether the IPC audio input system is running
|
|
func (aim *AudioInputIPCManager) IsRunning() bool {
|
|
return atomic.LoadInt32(&aim.running) == 1
|
|
}
|
|
|
|
// GetMetrics returns current metrics
|
|
func (aim *AudioInputIPCManager) GetMetrics() AudioInputMetrics {
|
|
return AudioInputMetrics{
|
|
FramesSent: atomic.LoadInt64(&aim.metrics.FramesSent),
|
|
FramesDropped: atomic.LoadInt64(&aim.metrics.FramesDropped),
|
|
BytesProcessed: atomic.LoadInt64(&aim.metrics.BytesProcessed),
|
|
ConnectionDrops: atomic.LoadInt64(&aim.metrics.ConnectionDrops),
|
|
AverageLatency: aim.metrics.AverageLatency, // TODO: Calculate actual latency
|
|
LastFrameTime: aim.metrics.LastFrameTime,
|
|
}
|
|
}
|
|
|
|
// updateLatencyMetrics updates the latency metrics with exponential moving average
|
|
func (aim *AudioInputIPCManager) updateLatencyMetrics(latency time.Duration) {
|
|
// Use exponential moving average for smooth latency calculation
|
|
currentAvg := aim.metrics.AverageLatency
|
|
if currentAvg == 0 {
|
|
aim.metrics.AverageLatency = latency
|
|
} else {
|
|
// EMA with alpha = 0.1 for smooth averaging
|
|
aim.metrics.AverageLatency = time.Duration(float64(currentAvg)*0.9 + float64(latency)*0.1)
|
|
}
|
|
}
|
|
|
|
// GetDetailedMetrics returns comprehensive performance metrics
|
|
func (aim *AudioInputIPCManager) GetDetailedMetrics() (AudioInputMetrics, map[string]interface{}) {
|
|
metrics := aim.GetMetrics()
|
|
|
|
// Get client frame statistics
|
|
client := aim.supervisor.GetClient()
|
|
totalFrames, droppedFrames := int64(0), int64(0)
|
|
dropRate := 0.0
|
|
if client != nil {
|
|
totalFrames, droppedFrames = client.GetFrameStats()
|
|
dropRate = client.GetDropRate()
|
|
}
|
|
|
|
// Get server statistics if available
|
|
serverStats := make(map[string]interface{})
|
|
if aim.supervisor.IsRunning() {
|
|
// Note: Server stats would need to be exposed through IPC
|
|
serverStats["status"] = "running"
|
|
} else {
|
|
serverStats["status"] = "stopped"
|
|
}
|
|
|
|
detailedStats := map[string]interface{}{
|
|
"client_total_frames": totalFrames,
|
|
"client_dropped_frames": droppedFrames,
|
|
"client_drop_rate": dropRate,
|
|
"server_stats": serverStats,
|
|
"ipc_latency_ms": float64(metrics.AverageLatency.Nanoseconds()) / 1e6,
|
|
"frames_per_second": aim.calculateFrameRate(),
|
|
}
|
|
|
|
return metrics, detailedStats
|
|
}
|
|
|
|
// calculateFrameRate calculates the current frame rate
|
|
func (aim *AudioInputIPCManager) calculateFrameRate() float64 {
|
|
framesSent := atomic.LoadInt64(&aim.metrics.FramesSent)
|
|
if framesSent == 0 {
|
|
return 0.0
|
|
}
|
|
|
|
// Estimate based on recent activity (simplified)
|
|
// In a real implementation, you'd track frames over time windows
|
|
return 50.0 // Typical Opus frame rate
|
|
}
|
|
|
|
// GetSupervisor returns the supervisor for advanced operations
|
|
func (aim *AudioInputIPCManager) GetSupervisor() *AudioInputSupervisor {
|
|
return aim.supervisor
|
|
}
|