//go:build linux package audio import ( "fmt" "sync" "github.com/gordonklaus/portaudio" ) // Player handles audio playback using PortAudio type Player struct { stream *portaudio.Stream volume float32 muted bool mu sync.Mutex running bool stopChan chan struct{} // User buffers for mixing userBuffers map[uint16][]int16 // User settings userSettings map[uint16]*UserSettings bufferMu sync.Mutex } func NewPlayer() (*Player, error) { if err := initPortAudio(); err != nil { return nil, err } p := &Player{ volume: 1.0, muted: false, stopChan: make(chan struct{}), userBuffers: make(map[uint16][]int16), userSettings: make(map[uint16]*UserSettings), } return p, nil } func (p *Player) Start() error { p.mu.Lock() if p.running { p.mu.Unlock() return nil } // Create stream (Mono, 48kHz, 16-bit) // We'll use a callback-based stream for lower latency var err error p.stream, err = portaudio.OpenDefaultStream(0, 1, 48000, frameSamples, p.processAudio) if err != nil { p.mu.Unlock() return fmt.Errorf("failed to open portaudio stream: %w", err) } if err := p.stream.Start(); err != nil { p.stream.Close() p.mu.Unlock() return fmt.Errorf("failed to start portaudio stream: %w", err) } p.running = true p.mu.Unlock() return nil } func (p *Player) Stop() { p.mu.Lock() defer p.mu.Unlock() if !p.running { return } p.running = false if p.stream != nil { p.stream.Abort() } } func (p *Player) Close() { p.Stop() p.mu.Lock() if p.stream != nil { p.stream.Close() } p.mu.Unlock() terminatePortAudio() } // processAudio is the PortAudio callback func (p *Player) processAudio(out []int16) { p.bufferMu.Lock() defer p.bufferMu.Unlock() // Initial silence for i := range out { out[i] = 0 } if p.muted { return } p.mu.Lock() vol := p.volume p.mu.Unlock() mixed := make([]int32, len(out)) for id, buf := range p.userBuffers { if len(buf) > 0 { toTake := len(out) if len(buf) < toTake { toTake = len(buf) } for i := 0; i < toTake; i++ { sample := int32(buf[i]) if settings, ok := p.userSettings[id]; ok { sample = int32(float32(sample) * settings.Volume) } mixed[i] += sample } // Advance buffer if len(buf) <= len(out) { delete(p.userBuffers, id) } else { p.userBuffers[id] = buf[len(out):] } } } // Apply master volume and clip for i := 0; i < len(out); i++ { val := int32(float32(mixed[i]) * vol) if val > 32767 { val = 32767 } else if val < -32768 { val = -32768 } out[i] = int16(val) } } func (p *Player) PlayPCM(senderID uint16, samples []int16) { p.bufferMu.Lock() defer p.bufferMu.Unlock() if settings, ok := p.userSettings[senderID]; ok && settings.Muted { return } p.userBuffers[senderID] = append(p.userBuffers[senderID], samples...) if len(p.userBuffers[senderID]) > 48000*2 { drop := len(p.userBuffers[senderID]) - 48000 p.userBuffers[senderID] = p.userBuffers[senderID][drop:] } } func (p *Player) SetVolume(vol float32) { p.mu.Lock() defer p.mu.Unlock() p.volume = vol } func (p *Player) SetMuted(muted bool) { p.mu.Lock() defer p.mu.Unlock() p.muted = muted } func (p *Player) SetUserVolume(clientID uint16, vol float32) { p.bufferMu.Lock() defer p.bufferMu.Unlock() if _, ok := p.userSettings[clientID]; !ok { p.userSettings[clientID] = &UserSettings{Volume: 1.0, Muted: false} } p.userSettings[clientID].Volume = vol } func (p *Player) SetUserMuted(clientID uint16, muted bool) { p.bufferMu.Lock() defer p.bufferMu.Unlock() if _, ok := p.userSettings[clientID]; !ok { p.userSettings[clientID] = &UserSettings{Volume: 1.0, Muted: false} } p.userSettings[clientID].Muted = muted } func (p *Player) GetUserSettings(clientID uint16) (float32, bool) { p.bufferMu.Lock() defer p.bufferMu.Unlock() if settings, ok := p.userSettings[clientID]; ok { return settings.Volume, settings.Muted } return 1.0, false }