新增: 音频可视化8种主题+节拍检测+交互效果

This commit is contained in:
2026-06-05 09:50:32 +08:00
parent 1f892fc390
commit 004dba7db6
5 changed files with 1224 additions and 188 deletions

View File

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

View File

@@ -34,7 +34,7 @@
<template v-if="s.theme === 'audio-viz'">
<div class="flex justify-between items-center px-3.5 py-2 border-t border-[var(--card-divider)]">
<div class="text-xs font-medium text-[var(--text-muted)]">可视化样式</div>
<div class="flex gap-1">
<div class="flex flex-wrap gap-1 justify-end">
<button v-for="st in audioVizStyles" :key="st.value"
@click="s.audioVizStyle = st.value; go.saveAudioVizStyle(st.value)"
:class="s.audioVizStyle === st.value
@@ -167,6 +167,14 @@ const audioVizStyles = [
{ value: 'bars', label: '柱状' },
{ value: 'waveform', label: '波形' },
{ value: 'circular', label: '环形' },
{ value: 'particles', label: '粒子' },
{ value: 'ripples', label: '涟漪' },
{ value: 'text', label: '文字' },
{ value: 'stars', label: '星空' },
{ value: 'dna', label: 'DNA' },
{ value: 'matrix', label: '矩阵' },
{ value: 'jellyfish', label: '水母' },
{ value: 'aurora', label: '极光' },
]
const audioColors = [

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff