Files
aego/audio/pump/mixclock_windows.go
sanspie 91090d27c4
GitHub / windows amd64 native GitHub-hosted native CI: success
GitHub / darwin arm64 native GitHub-hosted native CI: success
GitHub / darwin amd64 native GitHub-hosted native CI: success
ci-platforms / darwin amd64 smoke (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
github-native / gitea-pending (push) Skipped
github-native / darwin amd64 native (push) Skipped
github-native / linux amd64 native (push) Skipped
github-native / windows amd64 native (push) Skipped
ci / checks (push) Failing after 13m5s
github-native / darwin arm64 native (push) Skipped
github-native / gitea-result (push) Skipped
GitHub / linux amd64 native GitHub-hosted native CI: success
ci / release (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m6s
fix: use precise Windows audio mix timing
2026-11-10 01:03:49 +03:00

85 lines
2.3 KiB
Go

package pump
import (
"math/bits"
"syscall"
"time"
"unsafe"
)
var perfClock = newPerformanceClock()
type performanceClock struct {
counter uintptr
frequency int64
}
// Go's Windows monotonic clock uses interrupt time, whose resolution can be
// coarser than a mix block. QPC measures those blocks without inventing a
// minimum duration. Keep a Go clock reading as a fallback if QPC fails.
type mixTimer struct {
start time.Time
ticks int64
qpc bool
}
func newPerformanceClock() performanceClock {
kernel32 := syscall.NewLazyDLL("kernel32.dll")
counter := kernel32.NewProc("QueryPerformanceCounter")
frequencyProc := kernel32.NewProc("QueryPerformanceFrequency")
if counter.Find() != nil || frequencyProc.Find() != nil {
return performanceClock{}
}
var frequency int64
ok, _, _ := syscall.SyscallN(frequencyProc.Addr(), uintptr(unsafe.Pointer(&frequency)))
if ok == 0 || frequency <= 0 {
return performanceClock{}
}
return performanceClock{counter: counter.Addr(), frequency: frequency}
}
func performanceTicks() (int64, bool) {
var ticks int64
// SyscallN keeps the pointer alive for the call without forcing a heap
// allocation for each block, unlike LazyProc.Call.
ok, _, _ := syscall.SyscallN(perfClock.counter, uintptr(unsafe.Pointer(&ticks)))
return ticks, ok != 0 && ticks >= 0
}
func startMixTimer() mixTimer {
t := mixTimer{start: time.Now()}
if perfClock.frequency > 0 {
t.ticks, t.qpc = performanceTicks()
}
return t
}
func (t mixTimer) elapsed() time.Duration {
if t.qpc {
if end, ok := performanceTicks(); ok && end >= t.ticks {
return counterDuration(end-t.ticks, perfClock.frequency)
}
}
return time.Since(t.start)
}
// counterDuration converts a difference of counter readings, not an absolute
// uptime, to avoid overflow. Saturate if the elapsed time exceeds Duration.
func counterDuration(ticks, frequency int64) time.Duration {
if ticks <= 0 || frequency <= 0 {
return 0
}
const maxDuration = time.Duration(1<<63 - 1)
seconds := ticks / frequency
if seconds > int64(maxDuration/time.Second) {
return maxDuration
}
hi, lo := bits.Mul64(uint64(ticks%frequency), uint64(time.Second))
nanoseconds, _ := bits.Div64(hi, lo, uint64(frequency))
d := time.Duration(seconds) * time.Second
if time.Duration(nanoseconds) > maxDuration-d {
return maxDuration
}
return d + time.Duration(nanoseconds)
}