From 74043db85156380b768348722d1c3340af7631b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Luis=20Monta=C3=B1es=20Ojados?= Date: Sat, 17 Jan 2026 20:55:14 +0100 Subject: [PATCH] fix(linux): port audio engine 2.0 (stereo+eq) to linux --- pkg/audio/playback_linux.go | 166 ++++++++++++++++++++++++++++++++---- 1 file changed, 148 insertions(+), 18 deletions(-) diff --git a/pkg/audio/playback_linux.go b/pkg/audio/playback_linux.go index fcf3a6c..53329f7 100644 --- a/pkg/audio/playback_linux.go +++ b/pkg/audio/playback_linux.go @@ -18,8 +18,12 @@ type Player struct { running bool stopChan chan struct{} - // User buffers for mixing + // 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 @@ -35,6 +39,7 @@ func NewPlayer() (*Player, error) { muted: false, stopChan: make(chan struct{}), userBuffers: make(map[uint16][]int16), + userEQs: make(map[uint16]*EQChain), userSettings: make(map[uint16]*UserSettings), } @@ -48,10 +53,10 @@ func (p *Player) Start() error { return nil } - // Create stream (Mono, 48kHz, 16-bit) + // 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, 1, 48000, frameSamples, p.processAudio) + 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) @@ -108,6 +113,8 @@ func (p *Player) processAudio(out []int16) { 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 { @@ -117,6 +124,9 @@ func (p *Player) processAudio(out []int16) { 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 { @@ -126,10 +136,10 @@ func (p *Player) processAudio(out []int16) { } // Advance buffer - if len(buf) <= len(out) { + if len(buf) <= toTake { delete(p.userBuffers, id) } else { - p.userBuffers[id] = buf[len(out):] + p.userBuffers[id] = buf[toTake:] } } } @@ -147,20 +157,144 @@ func (p *Player) processAudio(out []int16) { } func (p *Player) PlayPCM(senderID uint16, samples []int16) { - p.bufferMu.Lock() - defer p.bufferMu.Unlock() - - if settings, ok := p.userSettings[senderID]; ok && settings.Muted { + if p.muted { return } - p.userBuffers[senderID] = append(p.userBuffers[senderID], samples...) - if len(p.userBuffers[senderID]) > 48000*2 { - drop := len(p.userBuffers[senderID]) - 48000 + // --------------------------------------------------------- + // 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() @@ -176,18 +310,14 @@ func (p *Player) SetMuted(muted bool) { 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.ensureUserSettings(clientID) 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.ensureUserSettings(clientID) p.userSettings[clientID].Muted = muted }