新增: 音频可视化8种主题+节拍检测+交互效果
This commit is contained in:
99
audio.go
99
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user