1 Commits

Author SHA1 Message Date
Jose Luis Montañes Ojados
74043db851 fix(linux): port audio engine 2.0 (stereo+eq) to linux
All checks were successful
Build and Release / build-linux (push) Successful in 33s
Build and Release / build-windows (push) Successful in 3m34s
Build and Release / release (push) Successful in 10s
2026-01-17 20:55:14 +01:00

View File

@@ -18,8 +18,12 @@ type Player struct {
running bool running bool
stopChan chan struct{} stopChan chan struct{}
// User buffers for mixing // User buffers for mixing (Stereo Interleaved)
userBuffers map[uint16][]int16 userBuffers map[uint16][]int16
// User EQs (DSP Filters)
userEQs map[uint16]*EQChain
// User settings // User settings
userSettings map[uint16]*UserSettings userSettings map[uint16]*UserSettings
bufferMu sync.Mutex bufferMu sync.Mutex
@@ -35,6 +39,7 @@ func NewPlayer() (*Player, error) {
muted: false, muted: false,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
userBuffers: make(map[uint16][]int16), userBuffers: make(map[uint16][]int16),
userEQs: make(map[uint16]*EQChain),
userSettings: make(map[uint16]*UserSettings), userSettings: make(map[uint16]*UserSettings),
} }
@@ -48,10 +53,10 @@ func (p *Player) Start() error {
return nil return nil
} }
// Create stream (Mono, 48kHz, 16-bit) // Create stream (Stereo, 48kHz, 16-bit)
// We'll use a callback-based stream for lower latency // We'll use a callback-based stream for lower latency
var err error 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 { if err != nil {
p.mu.Unlock() p.mu.Unlock()
return fmt.Errorf("failed to open portaudio stream: %w", err) return fmt.Errorf("failed to open portaudio stream: %w", err)
@@ -108,6 +113,8 @@ func (p *Player) processAudio(out []int16) {
vol := p.volume vol := p.volume
p.mu.Unlock() p.mu.Unlock()
// Output `out` is Stereo Interleaved (L, R, L, R...)
// Its length is frameSamples * 2.
mixed := make([]int32, len(out)) mixed := make([]int32, len(out))
for id, buf := range p.userBuffers { for id, buf := range p.userBuffers {
@@ -117,6 +124,9 @@ func (p *Player) processAudio(out []int16) {
toTake = len(buf) toTake = len(buf)
} }
// Ensure pair alignment
toTake = toTake &^ 1
for i := 0; i < toTake; i++ { for i := 0; i < toTake; i++ {
sample := int32(buf[i]) sample := int32(buf[i])
if settings, ok := p.userSettings[id]; ok { if settings, ok := p.userSettings[id]; ok {
@@ -126,10 +136,10 @@ func (p *Player) processAudio(out []int16) {
} }
// Advance buffer // Advance buffer
if len(buf) <= len(out) { if len(buf) <= toTake {
delete(p.userBuffers, id) delete(p.userBuffers, id)
} else { } 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) { func (p *Player) PlayPCM(senderID uint16, samples []int16) {
p.bufferMu.Lock() if p.muted {
defer p.bufferMu.Unlock()
if settings, ok := p.userSettings[senderID]; ok && settings.Muted {
return return
} }
p.userBuffers[senderID] = append(p.userBuffers[senderID], samples...) // ---------------------------------------------------------
if len(p.userBuffers[senderID]) > 48000*2 { // PHASE 1: Read Configuration (Safe Copy)
drop := len(p.userBuffers[senderID]) - 48000 // ---------------------------------------------------------
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:] 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) { func (p *Player) SetVolume(vol float32) {
p.mu.Lock() p.mu.Lock()
defer p.mu.Unlock() defer p.mu.Unlock()
@@ -176,18 +310,14 @@ func (p *Player) SetMuted(muted bool) {
func (p *Player) SetUserVolume(clientID uint16, vol float32) { func (p *Player) SetUserVolume(clientID uint16, vol float32) {
p.bufferMu.Lock() p.bufferMu.Lock()
defer p.bufferMu.Unlock() defer p.bufferMu.Unlock()
if _, ok := p.userSettings[clientID]; !ok { p.ensureUserSettings(clientID)
p.userSettings[clientID] = &UserSettings{Volume: 1.0, Muted: false}
}
p.userSettings[clientID].Volume = vol p.userSettings[clientID].Volume = vol
} }
func (p *Player) SetUserMuted(clientID uint16, muted bool) { func (p *Player) SetUserMuted(clientID uint16, muted bool) {
p.bufferMu.Lock() p.bufferMu.Lock()
defer p.bufferMu.Unlock() defer p.bufferMu.Unlock()
if _, ok := p.userSettings[clientID]; !ok { p.ensureUserSettings(clientID)
p.userSettings[clientID] = &UserSettings{Volume: 1.0, Muted: false}
}
p.userSettings[clientID].Muted = muted p.userSettings[clientID].Muted = muted
} }