//go:build linux package audio import ( "fmt" "sync" "github.com/gordonklaus/portaudio" ) // Capturer handles audio capture using PortAudio type Capturer struct { stream *portaudio.Stream running bool mu sync.Mutex onAudio func(samples []int16) currentLevel int levelMu sync.RWMutex } func NewCapturer() (*Capturer, error) { if err := initPortAudio(); err != nil { return nil, err } return &Capturer{}, nil } func (c *Capturer) SetCallback(fn func(samples []int16)) { c.mu.Lock() c.onAudio = fn c.mu.Unlock() } func (c *Capturer) Start() error { c.mu.Lock() defer c.mu.Unlock() if c.running { return nil } var err error c.stream, err = portaudio.OpenDefaultStream(1, 0, 48000, frameSamples, c.processCapture) if err != nil { return fmt.Errorf("failed to open portaudio capture stream: %w", err) } if err := c.stream.Start(); err != nil { c.stream.Close() return fmt.Errorf("failed to start portaudio capture stream: %w", err) } c.running = true return nil } func (c *Capturer) Stop() { c.mu.Lock() defer c.mu.Unlock() if !c.running { return } c.running = false if c.stream != nil { c.stream.Abort() } } func (c *Capturer) Close() { c.Stop() c.mu.Lock() if c.stream != nil { c.stream.Close() } c.mu.Unlock() terminatePortAudio() } func (c *Capturer) processCapture(in []int16) { c.mu.Lock() callback := c.onAudio running := c.running c.mu.Unlock() if !running || callback == nil { return } // Calculate level level := CalculateRMSLevel(in) c.levelMu.Lock() c.currentLevel = level c.levelMu.Unlock() // Clone buffer and send to callback samples := make([]int16, len(in)) copy(samples, in) callback(samples) } func (c *Capturer) GetLevel() int { c.levelMu.RLock() defer c.levelMu.RUnlock() return c.currentLevel } func (c *Capturer) IsRunning() bool { c.mu.Lock() defer c.mu.Unlock() return c.running }