diff --git a/audio.go b/audio.go index 2432177..1fe6b44 100644 --- a/audio.go +++ b/audio.go @@ -74,33 +74,55 @@ func fftMagnitude(input []float32) []float64 { 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) - start := int(math.Pow(float64(srcLen), frac)) fracNext := float64(i+1) / float64(target) - end := int(math.Pow(float64(srcLen), fracNext)) + + 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 } - sum := 0.0 + + // 取最大值保留峰值冲击力 + maxVal := 0.0 for j := start; j < end; j++ { - sum += magnitudes[j] + if magnitudes[j] > maxVal { + maxVal = magnitudes[j] + } } - bins[i] = sum / float64(end-start) + bins[i] = maxVal } return bins } -func smoothBins(prev, curr []float64, factor float64) []float64 { +func smoothBinsGo(prev, curr []float64, factor float64) []float64 { n := len(curr) if len(prev) != n { return curr @@ -122,7 +144,6 @@ func comCall(obj uintptr, vtableIdx uintptr, args ...uintptr) uintptr { } vtable := *(*uintptr)(unsafe.Pointer(obj)) method := *(*uintptr)(unsafe.Pointer(vtable + vtableIdx*unsafe.Sizeof(uintptr(0)))) - // 构建参数数组 (this + args),最多支持 12 参数 a := make([]uintptr, 12) a[0] = obj for i, arg := range args { @@ -155,27 +176,23 @@ var ( iidIAudioCaptureClient = windows.GUID{Data1: 0xC8ADBD64, Data2: 0xE71E, Data3: 0x48A0, Data4: [8]byte{0xA4, 0xDE, 0x18, 0x5C, 0x39, 0x5C, 0xD3, 0x17}} ) -// IMMDeviceEnumerator vtable (IUnknown 3 + 自身方法) const ( - enumeratorGetDefaultAudioEndpoint = 4 // 第 4 个方法 (0-based from vtable start) + enumeratorGetDefaultAudioEndpoint = 4 ) -// IMMDevice vtable const ( - deviceActivate = 3 // 第 3 个方法 + deviceActivate = 3 ) -// IAudioClient vtable const ( - audioClientInitialize = 3 + audioClientInitialize = 3 audioClientGetBufferSize = 4 - audioClientGetMixFormat = 8 - audioClientStart = 10 - audioClientStop = 11 - audioClientGetService = 14 + audioClientGetMixFormat = 8 + audioClientStart = 10 + audioClientStop = 11 + audioClientGetService = 14 ) -// IAudioCaptureClient vtable const ( captureGetBuffer = 3 captureReleaseBuffer = 4 @@ -191,6 +208,7 @@ var ( audioDone chan struct{} audioActive bool smoothed []float64 + runningMax float64 ) func startAudioCapture() { @@ -203,6 +221,7 @@ func startAudioCapture() { audioDone = make(chan struct{}) audioActive = true smoothed = nil + runningMax = 0 go audioCaptureLoop() log.Println("音频捕获: 已启动") } @@ -236,15 +255,13 @@ func audioCaptureLoop() { runtime.LockOSThread() defer runtime.UnlockOSThread() - // COM 初始化 ole32dll.NewProc("CoInitializeEx").Call(0, 0) defer ole32dll.NewProc("CoUninitialize").Call() - // CoCreateInstance -> IMMDeviceEnumerator var enumerator uintptr hr, _, _ := ole32dll.NewProc("CoCreateInstance").Call( uintptr(unsafe.Pointer(&clsidMMDeviceEnumerator)), - 0, 1, // CLSCTX_INPROC_SERVER + 0, 1, uintptr(unsafe.Pointer(&iidIMMDeviceEnumerator)), uintptr(unsafe.Pointer(&enumerator)), ) @@ -255,7 +272,6 @@ func audioCaptureLoop() { } defer comRelease(enumerator) - // enumerator->GetDefaultAudioEndpoint(eRender=0, eConsole=0) var device uintptr hr = comCall(enumerator, enumeratorGetDefaultAudioEndpoint, 0, 0, uintptr(unsafe.Pointer(&device))) if hr != 0 || device == 0 { @@ -265,7 +281,6 @@ func audioCaptureLoop() { } defer comRelease(device) - // device->Activate(IID_IAudioClient, CLSCTX_INPROC=1, NULL, &audioClient) var audioClient uintptr hr = comCall(device, deviceActivate, uintptr(unsafe.Pointer(&iidIAudioClient)), @@ -279,7 +294,6 @@ func audioCaptureLoop() { } defer comRelease(audioClient) - // audioClient->GetMixFormat(&pwfx) var pwfx uintptr hr = comCall(audioClient, audioClientGetMixFormat, uintptr(unsafe.Pointer(&pwfx))) if hr != 0 || pwfx == 0 { @@ -293,11 +307,10 @@ func audioCaptureLoop() { nSamplesPerSec := *(*uint32)(unsafe.Pointer(pwfx + 4)) log.Printf("音频捕获: 格式 ch=%d rate=%d", nChannels, nSamplesPerSec) - // audioClient->Initialize(SHARED, LOOPBACK, 1s buffer, 0, pwfx, NULL) hr = comCall(audioClient, audioClientInitialize, - 0, // AUDCLNT_SHAREMODE_SHARED - 0x00020000, // AUDCLNT_STREAMFLAGS_LOOPBACK - 10000000, // 1 second (100ns units) + 0, + 0x00020000, + 10000000, 0, pwfx, 0, @@ -308,7 +321,6 @@ func audioCaptureLoop() { return } - // audioClient->GetService(IID_IAudioCaptureClient, &captureClient) var captureClient uintptr hr = comCall(audioClient, audioClientGetService, uintptr(unsafe.Pointer(&iidIAudioCaptureClient)), @@ -321,11 +333,9 @@ func audioCaptureLoop() { } defer comRelease(captureClient) - // audioClient->Start() comCall(audioClient, audioClientStart) defer comCall(audioClient, audioClientStop) - // 环形缓冲区 (~100ms) ch := int(nChannels) bufSize := int(nSamplesPerSec)/10*ch + fftSize*ch ringBuf := make([]float32, bufSize) @@ -337,6 +347,9 @@ func audioCaptureLoop() { smoothingFactor = 0.7 } + // 预分配 mono slice + mono := make([]float32, fftSize) + ticker := time.NewTicker(33 * time.Millisecond) defer ticker.Stop() @@ -348,7 +361,6 @@ func audioCaptureLoop() { case <-ticker.C: } - // 读取 WASAPI 缓冲区 for { var pData, nFrames, flags uintptr hr = comCall(captureClient, captureGetBuffer, @@ -368,8 +380,6 @@ func audioCaptureLoop() { comCall(captureClient, captureReleaseBuffer, uintptr(nFrames)) } - // 取 fftSize 个单声道样本 - mono := make([]float32, fftSize) for i := 0; i < fftSize; i++ { pos := (writePos - fftSize*ch + i*ch + bufSize) % bufSize var sum float32 @@ -383,20 +393,25 @@ func audioCaptureLoop() { bins := downsampleBins(mag, targetBins) if smoothed != nil { - bins = smoothBins(smoothed, bins, smoothingFactor) + bins = smoothBinsGo(smoothed, bins, smoothingFactor) } smoothed = bins - // 归一化 - maxVal := 0.0 + // 全局归一化(慢衰减 runningMax) + curMax := 0.0 for _, v := range bins { - if v > maxVal { - maxVal = v + if v > curMax { + curMax = v } } - if maxVal > 0.001 { + if curMax > runningMax { + runningMax = curMax + } else { + runningMax *= 0.985 + } + if runningMax > 0.001 { for i := range bins { - bins[i] /= maxVal + bins[i] /= runningMax } } @@ -407,6 +422,6 @@ func audioCaptureLoop() { func comRelease(obj uintptr) { if obj != 0 { - comCall(obj, 2) // IUnknown::Release = vtable index 2 + comCall(obj, 2) } } diff --git a/web-ui/src/settings/components/WallpaperSection.vue b/web-ui/src/settings/components/WallpaperSection.vue index 15a286c..0cffe1e 100644 --- a/web-ui/src/settings/components/WallpaperSection.vue +++ b/web-ui/src/settings/components/WallpaperSection.vue @@ -34,7 +34,7 @@