feat(tui): add interactive 5-band per-user equalizer

This commit is contained in:
Jose Luis Montañes Ojados
2026-01-17 20:25:58 +01:00
parent 0010bc6cf7
commit 711eb148df
5 changed files with 485 additions and 3 deletions

View File

@@ -30,6 +30,9 @@ type Player struct {
// map[SenderID] -> AudioQueue
userBuffers map[uint16][]int16
// User EQs (DSP Filters)
userEQs map[uint16]*EQChain
// User settings
userSettings map[uint16]*UserSettings
bufferMu sync.Mutex
@@ -108,6 +111,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),
}, nil
}
@@ -171,6 +175,53 @@ func (p *Player) PlayPCM(senderID uint16, samples []int16) {
return
}
// Apply EQ Filters if gains are non-zero
p.ensureEQ(senderID)
// Check if any band has gain != 0
hasActiveEQ := false
if settings, ok := p.userSettings[senderID]; ok && len(settings.Gains) == 5 {
for _, g := range settings.Gains {
if g != 0 {
hasActiveEQ = true
break
}
}
}
// Apply filters if needed
// Note: We should probably process always if we want smooth transitions,
// but for optimization we skip if all 0.
// However, skipping might cause clicks if we jump from filtered to non-filtered state abruptly.
// For "Pro" audio, always process. For TUI app, let's process if active.
if hasActiveEQ {
if eq, ok := p.userEQs[senderID]; ok {
// Update gains from settings
// (Ideally we only do this on change, but doing it here ensures sync)
gains := p.userSettings[senderID].Gains
for i, g := range gains {
eq.SetGain(i, g)
}
// Process in-place (conceptually) - actually implementation creates new slice
samples = eq.Process(samples)
}
} else {
// Even if not active, we might want to reset filters if they were active before?
// Or just leave them alone.
}
// Calculate EQ bands for visualization
// We do this BEFORE appending to buffer to ensure we have visual feedback even if buffer is full/lagging
// This is a "fire and forget" calculation for UI
bands := CalculateEQBands(samples, 48000)
// Update user settings with new bands
if _, ok := p.userSettings[senderID]; !ok {
p.userSettings[senderID] = &UserSettings{Volume: 1.0, Muted: false}
}
p.userSettings[senderID].EQBands = bands
// Append to user's specific buffer
// This ensures sequential playback for the same user
p.userBuffers[senderID] = append(p.userBuffers[senderID], samples...)
@@ -249,6 +300,66 @@ func (p *Player) GetUserSettings(clientID uint16) (float32, bool) {
return 1.0, false
}
// 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.
// Gain is in dB (e.g. -12.0 to +12.0)
func (p *Player) SetUserGain(clientID uint16, bandIdx int, gainDb float64) {
p.bufferMu.Lock()
defer p.bufferMu.Unlock()
p.ensureUserSettings(clientID)
// Ensure Gains slice exists
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) ensureEQ(clientID uint16) {
if _, ok := p.userEQs[clientID]; !ok {
// New EQ chain
// Assume 48000 Hz, would be better to pass actual stream rate
p.userEQs[clientID] = NewEQChain(48000)
}
}
func (p *Player) playbackLoop() {
ticker := time.NewTicker(10 * time.Millisecond)
defer ticker.Stop()