feat: full WSL2 audio support and Windows audio stability fix

This commit is contained in:
Jose Luis Montañes Ojados
2026-01-17 01:38:13 +01:00
parent c1bb24473e
commit c81f64d9ca
12 changed files with 492 additions and 75 deletions

View File

@@ -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)
}
}