feat: full WSL2 audio support and Windows audio stability fix
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
//go:build windows
|
||||
|
||||
package audio
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
@@ -32,19 +35,11 @@ type Player struct {
|
||||
bufferMu sync.Mutex
|
||||
}
|
||||
|
||||
type UserSettings struct {
|
||||
Volume float32 // 0.0 - 1.0 (or higher for boost)
|
||||
Muted bool
|
||||
}
|
||||
|
||||
const (
|
||||
frameSamples = 960 // 20ms at 48kHz
|
||||
)
|
||||
|
||||
// NewPlayer creates a new WASAPI audio player
|
||||
func NewPlayer() (*Player, error) {
|
||||
// Initialize COM
|
||||
ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED)
|
||||
log.Printf("[Audio] Windows/WASAPI initializing...")
|
||||
|
||||
var deviceEnumerator *wca.IMMDeviceEnumerator
|
||||
if err := wca.CoCreateInstance(
|
||||
@@ -255,7 +250,7 @@ func (p *Player) GetUserSettings(clientID uint16) (float32, bool) {
|
||||
}
|
||||
|
||||
func (p *Player) playbackLoop() {
|
||||
ticker := time.NewTicker(20 * time.Millisecond)
|
||||
ticker := time.NewTicker(10 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
@@ -269,79 +264,88 @@ func (p *Player) playbackLoop() {
|
||||
}
|
||||
|
||||
func (p *Player) writeFrame() {
|
||||
var padding uint32
|
||||
if err := p.client.GetCurrentPadding(&padding); err != nil {
|
||||
return
|
||||
}
|
||||
for {
|
||||
var padding uint32
|
||||
if err := p.client.GetCurrentPadding(&padding); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
available := p.bufferSize - padding
|
||||
if available < frameSamples {
|
||||
return
|
||||
}
|
||||
available := p.bufferSize - padding
|
||||
if available < frameSamples {
|
||||
return
|
||||
}
|
||||
|
||||
p.bufferMu.Lock()
|
||||
p.bufferMu.Lock()
|
||||
|
||||
// Mix audio from all active user buffers
|
||||
mixed := make([]int32, frameSamples)
|
||||
activeUsers := 0
|
||||
// Mix audio from all active user buffers
|
||||
mixed := make([]int32, frameSamples)
|
||||
activeUsers := 0
|
||||
hasAnyAudio := false
|
||||
|
||||
for id, buf := range p.userBuffers {
|
||||
if len(buf) > 0 {
|
||||
activeUsers++
|
||||
// Take up to frameSamples from this user
|
||||
toTake := frameSamples
|
||||
if len(buf) < frameSamples {
|
||||
toTake = len(buf)
|
||||
}
|
||||
|
||||
for i := 0; i < toTake; i++ {
|
||||
sample := int32(buf[i])
|
||||
|
||||
// Apply user volume if set
|
||||
if settings, ok := p.userSettings[id]; ok {
|
||||
sample = int32(float32(sample) * settings.Volume)
|
||||
for id, buf := range p.userBuffers {
|
||||
if len(buf) > 0 {
|
||||
hasAnyAudio = true
|
||||
activeUsers++
|
||||
// Take up to frameSamples from this user
|
||||
toTake := frameSamples
|
||||
if len(buf) < frameSamples {
|
||||
toTake = len(buf)
|
||||
}
|
||||
|
||||
mixed[i] += sample
|
||||
}
|
||||
for i := 0; i < toTake; i++ {
|
||||
sample := int32(buf[i])
|
||||
|
||||
// Advance buffer
|
||||
if len(buf) <= frameSamples {
|
||||
delete(p.userBuffers, id)
|
||||
} else {
|
||||
p.userBuffers[id] = buf[frameSamples:]
|
||||
// Apply user volume if set
|
||||
if settings, ok := p.userSettings[id]; ok {
|
||||
sample = int32(float32(sample) * settings.Volume)
|
||||
}
|
||||
|
||||
mixed[i] += sample
|
||||
}
|
||||
|
||||
// Advance buffer
|
||||
if len(buf) <= frameSamples {
|
||||
delete(p.userBuffers, id)
|
||||
} else {
|
||||
p.userBuffers[id] = buf[frameSamples:]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.bufferMu.Unlock()
|
||||
p.bufferMu.Unlock()
|
||||
|
||||
// Get WASAPI buffer
|
||||
var buffer *byte
|
||||
if err := p.renderClient.GetBuffer(uint32(frameSamples), &buffer); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
vol := p.volume
|
||||
p.mu.Unlock()
|
||||
|
||||
// Write mixed samples with clipping protection and volume application
|
||||
bufSlice := unsafe.Slice(buffer, int(frameSamples)*2)
|
||||
for i := 0; i < int(frameSamples); i++ {
|
||||
val := mixed[i]
|
||||
|
||||
// Apply volume
|
||||
val = int32(float32(val) * vol)
|
||||
|
||||
// Hard clipping
|
||||
if val > 32767 {
|
||||
val = 32767
|
||||
} else if val < -32768 {
|
||||
val = -32768
|
||||
// If no audio is playing, don't write anything (keep buffer empty for lower latency when audio starts)
|
||||
if !hasAnyAudio {
|
||||
return
|
||||
}
|
||||
binary.LittleEndian.PutUint16(bufSlice[i*2:], uint16(val))
|
||||
}
|
||||
|
||||
p.renderClient.ReleaseBuffer(uint32(frameSamples), 0)
|
||||
// Get WASAPI buffer
|
||||
var buffer *byte
|
||||
if err := p.renderClient.GetBuffer(uint32(frameSamples), &buffer); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
vol := p.volume
|
||||
p.mu.Unlock()
|
||||
|
||||
// Write mixed samples with clipping protection and volume application
|
||||
bufSlice := unsafe.Slice(buffer, int(frameSamples)*2)
|
||||
for i := 0; i < int(frameSamples); i++ {
|
||||
val := mixed[i]
|
||||
|
||||
// Apply master volume
|
||||
val = int32(float32(val) * vol)
|
||||
|
||||
// Hard clipping
|
||||
if val > 32767 {
|
||||
val = 32767
|
||||
} else if val < -32768 {
|
||||
val = -32768
|
||||
}
|
||||
binary.LittleEndian.PutUint16(bufSlice[i*2:], uint16(val))
|
||||
}
|
||||
|
||||
p.renderClient.ReleaseBuffer(uint32(frameSamples), 0)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user