新增: 音频可视化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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user