fix(linux): port audio engine 2.0 (stereo+eq) to linux
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user