Files
u-desktop/audio.go

428 lines
9.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"encoding/json"
"fmt"
"log"
"math"
"math/cmplx"
"runtime"
"sync"
"syscall"
"time"
"unsafe"
"golang.org/x/sys/windows"
)
// ============================================================
// FFT: 纯 Go radix-2 Cooley-Tukey (1024 点)
// ============================================================
const fftSize = 1024
const targetBins = 128
func fft(x []complex128) {
n := len(x)
if n <= 1 {
return
}
for i, j := 1, 0; i < n; i++ {
bit := n >> 1
for j&bit != 0 {
j ^= bit
bit >>= 1
}
j ^= bit
if i < j {
x[i], x[j] = x[j], x[i]
}
}
for length := 2; length <= n; length <<= 1 {
half := length >> 1
angle := -2 * math.Pi / float64(length)
wn := complex(math.Cos(angle), math.Sin(angle))
for i := 0; i < n; i += length {
w := complex(1, 0)
for j := 0; j < half; j++ {
u := x[i+j]
v := x[i+j+half] * w
x[i+j] = u + v
x[i+j+half] = u - v
w *= wn
}
}
}
}
func fftMagnitude(input []float32) []float64 {
n := len(input)
if n > fftSize {
n = fftSize
input = input[:fftSize]
}
x := make([]complex128, fftSize)
for i := 0; i < n; i++ {
w := 0.5 * (1 - math.Cos(2*math.Pi*float64(i)/float64(n-1)))
x[i] = complex(float64(input[i])*w, 0)
}
fft(x)
mag := make([]float64, fftSize/2)
for i := range mag {
mag[i] = cmplx.Abs(x[i]) / float64(fftSize)
}
return mag
}
// downsampleBins: 分段映射 — 低频线性保细节,中高频对数保覆盖
// 不再做额外 tilt 补偿,映射本身就是均衡的
func downsampleBins(magnitudes []float64, target int) []float64 {
srcLen := len(magnitudes)
if srcLen == 0 || target == 0 {
return make([]float64, target)
}
// 低频线性区间前20%的 display bins 覆盖 FFT 1-80 (约47Hz-3.7kHz)
// 中高频对数区间后80%覆盖 FFT 80-512 (3.7kHz-24kHz)
const lowEnd = 80 // 低频截止 FFT 索引
const lowFrac = 0.2
bins := make([]float64, target)
for i := 0; i < target; i++ {
frac := float64(i) / float64(target)
fracNext := float64(i+1) / float64(target)
var start, end int
if frac < lowFrac {
start = int(frac / lowFrac * float64(lowEnd))
end = int(fracNext / lowFrac * float64(lowEnd))
} else {
f := (frac - lowFrac) / (1 - lowFrac)
fNext := (fracNext - lowFrac) / (1 - lowFrac)
start = lowEnd + int(float64(srcLen-lowEnd)*math.Pow(f, 0.7))
end = lowEnd + int(float64(srcLen-lowEnd)*math.Pow(fNext, 0.7))
}
if end <= start {
end = start + 1
}
if end > srcLen {
end = srcLen
}
// 取最大值保留峰值冲击力
maxVal := 0.0
for j := start; j < end; j++ {
if magnitudes[j] > maxVal {
maxVal = magnitudes[j]
}
}
bins[i] = maxVal
}
return bins
}
func smoothBinsGo(prev, curr []float64, factor float64) []float64 {
n := len(curr)
if len(prev) != n {
return curr
}
out := make([]float64, n)
for i := 0; i < n; i++ {
out[i] = prev[i]*factor + curr[i]*(1-factor)
}
return out
}
// ============================================================
// COM vtable 调用辅助
// ============================================================
func comCall(obj uintptr, vtableIdx uintptr, args ...uintptr) uintptr {
if obj == 0 {
return 0x80004003 // E_POINTER
}
vtable := *(*uintptr)(unsafe.Pointer(obj))
method := *(*uintptr)(unsafe.Pointer(vtable + vtableIdx*unsafe.Sizeof(uintptr(0))))
a := make([]uintptr, 12)
a[0] = obj
for i, arg := range args {
a[i+1] = arg
}
n := uintptr(len(args) + 1)
switch {
case n <= 3:
r, _, _ := syscall.Syscall(method, n, a[0], a[1], a[2])
return r
case n <= 6:
r, _, _ := syscall.Syscall6(method, n, a[0], a[1], a[2], a[3], a[4], a[5])
return r
case n <= 9:
r, _, _ := syscall.Syscall9(method, n, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8])
return r
case n <= 12:
r, _, _ := syscall.Syscall12(method, n, a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11])
return r
default:
return 0x80070057 // E_INVALIDARG
}
}
// COM GUID 常量
var (
clsidMMDeviceEnumerator = windows.GUID{Data1: 0xBCDE0395, Data2: 0xE52F, Data3: 0x467C, Data4: [8]byte{0x8E, 0x3D, 0xC4, 0x57, 0x92, 0x91, 0x69, 0x2E}}
iidIMMDeviceEnumerator = windows.GUID{Data1: 0xA95664D2, Data2: 0x9614, Data3: 0x4F35, Data4: [8]byte{0xA7, 0x46, 0xDE, 0x8D, 0xB6, 0x36, 0x17, 0xE6}}
iidIAudioClient = windows.GUID{Data1: 0x1CB9AD4C, Data2: 0xDBFA, Data3: 0x4C32, Data4: [8]byte{0xB1, 0x78, 0xC2, 0xF5, 0x68, 0xA7, 0x03, 0xB2}}
iidIAudioCaptureClient = windows.GUID{Data1: 0xC8ADBD64, Data2: 0xE71E, Data3: 0x48A0, Data4: [8]byte{0xA4, 0xDE, 0x18, 0x5C, 0x39, 0x5C, 0xD3, 0x17}}
)
const (
enumeratorGetDefaultAudioEndpoint = 4
)
const (
deviceActivate = 3
)
const (
audioClientInitialize = 3
audioClientGetBufferSize = 4
audioClientGetMixFormat = 8
audioClientStart = 10
audioClientStop = 11
audioClientGetService = 14
)
const (
captureGetBuffer = 3
captureReleaseBuffer = 4
)
// ============================================================
// WASAPI 捕获生命周期
// ============================================================
var (
audioMu sync.Mutex
audioStop chan struct{}
audioDone chan struct{}
audioActive bool
smoothed []float64
runningMax float64
)
func startAudioCapture() {
audioMu.Lock()
defer audioMu.Unlock()
if audioActive {
return
}
audioStop = make(chan struct{})
audioDone = make(chan struct{})
audioActive = true
smoothed = nil
runningMax = 0
go audioCaptureLoop()
log.Println("音频捕获: 已启动")
}
func stopAudioCapture() {
audioMu.Lock()
stop := audioStop
audioMu.Unlock()
if stop == nil {
return
}
close(stop)
if audioDone != nil {
<-audioDone
}
audioMu.Lock()
audioActive = false
audioStop = nil
audioDone = nil
audioMu.Unlock()
log.Println("音频捕获: 已停止")
}
func audioActiveState() bool {
audioMu.Lock()
defer audioMu.Unlock()
return audioActive
}
func audioCaptureLoop() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ole32dll.NewProc("CoInitializeEx").Call(0, 0)
defer ole32dll.NewProc("CoUninitialize").Call()
var enumerator uintptr
hr, _, _ := ole32dll.NewProc("CoCreateInstance").Call(
uintptr(unsafe.Pointer(&clsidMMDeviceEnumerator)),
0, 1,
uintptr(unsafe.Pointer(&iidIMMDeviceEnumerator)),
uintptr(unsafe.Pointer(&enumerator)),
)
if hr != 0 || enumerator == 0 {
log.Printf("音频捕获: 创建枚举器失败 hr=0x%x", hr)
close(audioDone)
return
}
defer comRelease(enumerator)
var device uintptr
hr = comCall(enumerator, enumeratorGetDefaultAudioEndpoint, 0, 0, uintptr(unsafe.Pointer(&device)))
if hr != 0 || device == 0 {
log.Printf("音频捕获: 获取默认音频设备失败 hr=0x%x", hr)
close(audioDone)
return
}
defer comRelease(device)
var audioClient uintptr
hr = comCall(device, deviceActivate,
uintptr(unsafe.Pointer(&iidIAudioClient)),
1, 0,
uintptr(unsafe.Pointer(&audioClient)),
)
if hr != 0 || audioClient == 0 {
log.Printf("音频捕获: Activate(IAudioClient) 失败 hr=0x%x", hr)
close(audioDone)
return
}
defer comRelease(audioClient)
var pwfx uintptr
hr = comCall(audioClient, audioClientGetMixFormat, uintptr(unsafe.Pointer(&pwfx)))
if hr != 0 || pwfx == 0 {
log.Printf("音频捕获: GetMixFormat 失败 hr=0x%x", hr)
close(audioDone)
return
}
defer ole32dll.NewProc("CoTaskMemFree").Call(pwfx)
nChannels := *(*uint16)(unsafe.Pointer(pwfx + 2))
nSamplesPerSec := *(*uint32)(unsafe.Pointer(pwfx + 4))
log.Printf("音频捕获: 格式 ch=%d rate=%d", nChannels, nSamplesPerSec)
hr = comCall(audioClient, audioClientInitialize,
0,
0x00020000,
10000000,
0,
pwfx,
0,
)
if hr != 0 {
log.Printf("音频捕获: Initialize 失败 hr=0x%x", hr)
close(audioDone)
return
}
var captureClient uintptr
hr = comCall(audioClient, audioClientGetService,
uintptr(unsafe.Pointer(&iidIAudioCaptureClient)),
uintptr(unsafe.Pointer(&captureClient)),
)
if hr != 0 || captureClient == 0 {
log.Printf("音频捕获: GetService(IAudioCaptureClient) 失败 hr=0x%x", hr)
close(audioDone)
return
}
defer comRelease(captureClient)
comCall(audioClient, audioClientStart)
defer comCall(audioClient, audioClientStop)
ch := int(nChannels)
bufSize := int(nSamplesPerSec)/10*ch + fftSize*ch
ringBuf := make([]float32, bufSize)
writePos := 0
cfg := loadConfig()
smoothingFactor := cfg.AudioSmoothing
if smoothingFactor <= 0 {
smoothingFactor = 0.7
}
// 预分配 mono slice
mono := make([]float32, fftSize)
ticker := time.NewTicker(33 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-audioStop:
close(audioDone)
return
case <-ticker.C:
}
for {
var pData, nFrames, flags uintptr
hr = comCall(captureClient, captureGetBuffer,
uintptr(unsafe.Pointer(&pData)),
uintptr(unsafe.Pointer(&nFrames)),
uintptr(unsafe.Pointer(&flags)),
0, 0,
)
if hr != 0 || nFrames == 0 {
break
}
samples := unsafe.Slice((*float32)(unsafe.Pointer(pData)), nFrames*uintptr(ch))
for _, s := range samples {
ringBuf[writePos] = s
writePos = (writePos + 1) % bufSize
}
comCall(captureClient, captureReleaseBuffer, uintptr(nFrames))
}
for i := 0; i < fftSize; i++ {
pos := (writePos - fftSize*ch + i*ch + bufSize) % bufSize
var sum float32
for c := 0; c < ch; c++ {
sum += ringBuf[(pos+c)%bufSize]
}
mono[i] = sum / float32(ch)
}
mag := fftMagnitude(mono)
bins := downsampleBins(mag, targetBins)
if smoothed != nil {
bins = smoothBinsGo(smoothed, bins, smoothingFactor)
}
smoothed = bins
// 全局归一化(慢衰减 runningMax
curMax := 0.0
for _, v := range bins {
if v > curMax {
curMax = v
}
}
if curMax > runningMax {
runningMax = curMax
} else {
runningMax *= 0.985
}
if runningMax > 0.001 {
for i := range bins {
bins[i] /= runningMax
}
}
data, _ := json.Marshal(bins)
evalJS(fmt.Sprintf(`if(window.updateAudioDataFromGo) updateAudioDataFromGo(%s)`, string(data)))
}
}
func comRelease(obj uintptr) {
if obj != 0 {
comCall(obj, 2)
}
}