//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 (Stereo Interleaved) userBuffers map[uint16][]int16 // User EQs (DSP Filters) userEQs map[uint16]*EQChain // 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), userEQs: make(map[uint16]*EQChain), 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 (Stereo, 48kHz, 16-bit) // We'll use a callback-based stream for lower latency var err error p.stream, err = portaudio.OpenDefaultStream(0, 2, 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() // Output `out` is Stereo Interleaved (L, R, L, R...) // Its length is frameSamples * 2. 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) } // Ensure pair alignment toTake = toTake &^ 1 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) <= toTake { delete(p.userBuffers, id) } else { p.userBuffers[id] = buf[toTake:] } } } // 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) { if p.muted { return } // --------------------------------------------------------- // PHASE 1: Read Configuration (Safe Copy) // --------------------------------------------------------- p.bufferMu.Lock() // Check per-user mute settings, hasSettings := p.userSettings[senderID] if hasSettings && settings.Muted { p.bufferMu.Unlock() return } // Get EQ Instance (Create if needed) if _, ok := p.userEQs[senderID]; !ok { p.userEQs[senderID] = NewEQChain(48000) } userEQ := p.userEQs[senderID] // Check/Copy Gains var gains []float64 hasActiveEQ := false if hasSettings && len(settings.Gains) == 5 { gains = make([]float64, 5) copy(gains, settings.Gains) for _, g := range gains { if g != 0 { hasActiveEQ = true break } } } p.bufferMu.Unlock() // --------------------------------------------------------- // PHASE 2: Heavy Processing (Concurrent) // --------------------------------------------------------- // Normalize to Stereo var stereoSamples []int16 if len(samples) < 1500 { // Mono stereoSamples = make([]int16, len(samples)*2) for i, s := range samples { stereoSamples[i*2] = s stereoSamples[i*2+1] = s } } else { // Already stereo stereoSamples = make([]int16, len(samples)) copy(stereoSamples, samples) } if hasActiveEQ { for i, g := range gains { userEQ.SetGain(i, g) } stereoSamples = userEQ.Process(stereoSamples) } // Calculate EQ bands (Downmix for visualization) vizSamples := make([]int16, len(stereoSamples)/2) for i := 0; i < len(vizSamples); i++ { val := (int32(stereoSamples[i*2]) + int32(stereoSamples[i*2+1])) / 2 vizSamples[i] = int16(val) } bands := CalculateEQBands(vizSamples, 48000) // --------------------------------------------------------- // PHASE 3: Write Output (Lock Acquired) // --------------------------------------------------------- p.bufferMu.Lock() defer p.bufferMu.Unlock() if _, ok := p.userSettings[senderID]; !ok { p.userSettings[senderID] = &UserSettings{Volume: 1.0, Muted: false} } p.userSettings[senderID].EQBands = bands p.userBuffers[senderID] = append(p.userBuffers[senderID], stereoSamples...) const maxBufferSize = 48000 * 2 * 2 // 2 seconds stereo if len(p.userBuffers[senderID]) > maxBufferSize { drop := len(p.userBuffers[senderID]) - maxBufferSize if drop%2 != 0 { drop++ } p.userBuffers[senderID] = p.userBuffers[senderID][drop:] } } // GetEQBands returns the current 5-band EQ values for a user (0.0-1.0) func (p *Player) GetEQBands(clientID uint16) []float64 { p.bufferMu.Lock() defer p.bufferMu.Unlock() if settings, ok := p.userSettings[clientID]; ok { return settings.EQBands } return nil } // SetUserGain sets the EQ gain for a specific band (0-4) and user. func (p *Player) SetUserGain(clientID uint16, bandIdx int, gainDb float64) { p.bufferMu.Lock() defer p.bufferMu.Unlock() p.ensureUserSettings(clientID) if len(p.userSettings[clientID].Gains) != 5 { p.userSettings[clientID].Gains = make([]float64, 5) } if bandIdx >= 0 && bandIdx < 5 { p.userSettings[clientID].Gains[bandIdx] = gainDb } } // GetUserGain returns the gain for a band func (p *Player) GetUserGain(clientID uint16, bandIdx int) float64 { p.bufferMu.Lock() defer p.bufferMu.Unlock() if settings, ok := p.userSettings[clientID]; ok { if len(settings.Gains) > bandIdx { return settings.Gains[bandIdx] } } return 0.0 } func (p *Player) ensureUserSettings(clientID uint16) { if _, ok := p.userSettings[clientID]; !ok { p.userSettings[clientID] = &UserSettings{ Volume: 1.0, Muted: false, Gains: make([]float64, 5), } } } 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() p.ensureUserSettings(clientID) p.userSettings[clientID].Volume = vol } func (p *Player) SetUserMuted(clientID uint16, muted bool) { p.bufferMu.Lock() defer p.bufferMu.Unlock() p.ensureUserSettings(clientID) 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 }