新增: 音频可视化8种主题+节拍检测+交互效果
This commit is contained in:
@@ -74,33 +74,55 @@ func fftMagnitude(input []float32) []float64 {
|
|||||||
return mag
|
return mag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// downsampleBins: 分段映射 — 低频线性保细节,中高频对数保覆盖
|
||||||
|
// 不再做额外 tilt 补偿,映射本身就是均衡的
|
||||||
func downsampleBins(magnitudes []float64, target int) []float64 {
|
func downsampleBins(magnitudes []float64, target int) []float64 {
|
||||||
srcLen := len(magnitudes)
|
srcLen := len(magnitudes)
|
||||||
if srcLen == 0 || target == 0 {
|
if srcLen == 0 || target == 0 {
|
||||||
return make([]float64, target)
|
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)
|
bins := make([]float64, target)
|
||||||
for i := 0; i < target; i++ {
|
for i := 0; i < target; i++ {
|
||||||
frac := float64(i) / float64(target)
|
frac := float64(i) / float64(target)
|
||||||
start := int(math.Pow(float64(srcLen), frac))
|
|
||||||
fracNext := float64(i+1) / float64(target)
|
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 {
|
if end <= start {
|
||||||
end = start + 1
|
end = start + 1
|
||||||
}
|
}
|
||||||
if end > srcLen {
|
if end > srcLen {
|
||||||
end = srcLen
|
end = srcLen
|
||||||
}
|
}
|
||||||
sum := 0.0
|
|
||||||
|
// 取最大值保留峰值冲击力
|
||||||
|
maxVal := 0.0
|
||||||
for j := start; j < end; j++ {
|
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
|
return bins
|
||||||
}
|
}
|
||||||
|
|
||||||
func smoothBins(prev, curr []float64, factor float64) []float64 {
|
func smoothBinsGo(prev, curr []float64, factor float64) []float64 {
|
||||||
n := len(curr)
|
n := len(curr)
|
||||||
if len(prev) != n {
|
if len(prev) != n {
|
||||||
return curr
|
return curr
|
||||||
@@ -122,7 +144,6 @@ func comCall(obj uintptr, vtableIdx uintptr, args ...uintptr) uintptr {
|
|||||||
}
|
}
|
||||||
vtable := *(*uintptr)(unsafe.Pointer(obj))
|
vtable := *(*uintptr)(unsafe.Pointer(obj))
|
||||||
method := *(*uintptr)(unsafe.Pointer(vtable + vtableIdx*unsafe.Sizeof(uintptr(0))))
|
method := *(*uintptr)(unsafe.Pointer(vtable + vtableIdx*unsafe.Sizeof(uintptr(0))))
|
||||||
// 构建参数数组 (this + args),最多支持 12 参数
|
|
||||||
a := make([]uintptr, 12)
|
a := make([]uintptr, 12)
|
||||||
a[0] = obj
|
a[0] = obj
|
||||||
for i, arg := range args {
|
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}}
|
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 (
|
const (
|
||||||
enumeratorGetDefaultAudioEndpoint = 4 // 第 4 个方法 (0-based from vtable start)
|
enumeratorGetDefaultAudioEndpoint = 4
|
||||||
)
|
)
|
||||||
|
|
||||||
// IMMDevice vtable
|
|
||||||
const (
|
const (
|
||||||
deviceActivate = 3 // 第 3 个方法
|
deviceActivate = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
// IAudioClient vtable
|
|
||||||
const (
|
const (
|
||||||
audioClientInitialize = 3
|
audioClientInitialize = 3
|
||||||
audioClientGetBufferSize = 4
|
audioClientGetBufferSize = 4
|
||||||
audioClientGetMixFormat = 8
|
audioClientGetMixFormat = 8
|
||||||
audioClientStart = 10
|
audioClientStart = 10
|
||||||
audioClientStop = 11
|
audioClientStop = 11
|
||||||
audioClientGetService = 14
|
audioClientGetService = 14
|
||||||
)
|
)
|
||||||
|
|
||||||
// IAudioCaptureClient vtable
|
|
||||||
const (
|
const (
|
||||||
captureGetBuffer = 3
|
captureGetBuffer = 3
|
||||||
captureReleaseBuffer = 4
|
captureReleaseBuffer = 4
|
||||||
@@ -191,6 +208,7 @@ var (
|
|||||||
audioDone chan struct{}
|
audioDone chan struct{}
|
||||||
audioActive bool
|
audioActive bool
|
||||||
smoothed []float64
|
smoothed []float64
|
||||||
|
runningMax float64
|
||||||
)
|
)
|
||||||
|
|
||||||
func startAudioCapture() {
|
func startAudioCapture() {
|
||||||
@@ -203,6 +221,7 @@ func startAudioCapture() {
|
|||||||
audioDone = make(chan struct{})
|
audioDone = make(chan struct{})
|
||||||
audioActive = true
|
audioActive = true
|
||||||
smoothed = nil
|
smoothed = nil
|
||||||
|
runningMax = 0
|
||||||
go audioCaptureLoop()
|
go audioCaptureLoop()
|
||||||
log.Println("音频捕获: 已启动")
|
log.Println("音频捕获: 已启动")
|
||||||
}
|
}
|
||||||
@@ -236,15 +255,13 @@ func audioCaptureLoop() {
|
|||||||
runtime.LockOSThread()
|
runtime.LockOSThread()
|
||||||
defer runtime.UnlockOSThread()
|
defer runtime.UnlockOSThread()
|
||||||
|
|
||||||
// COM 初始化
|
|
||||||
ole32dll.NewProc("CoInitializeEx").Call(0, 0)
|
ole32dll.NewProc("CoInitializeEx").Call(0, 0)
|
||||||
defer ole32dll.NewProc("CoUninitialize").Call()
|
defer ole32dll.NewProc("CoUninitialize").Call()
|
||||||
|
|
||||||
// CoCreateInstance -> IMMDeviceEnumerator
|
|
||||||
var enumerator uintptr
|
var enumerator uintptr
|
||||||
hr, _, _ := ole32dll.NewProc("CoCreateInstance").Call(
|
hr, _, _ := ole32dll.NewProc("CoCreateInstance").Call(
|
||||||
uintptr(unsafe.Pointer(&clsidMMDeviceEnumerator)),
|
uintptr(unsafe.Pointer(&clsidMMDeviceEnumerator)),
|
||||||
0, 1, // CLSCTX_INPROC_SERVER
|
0, 1,
|
||||||
uintptr(unsafe.Pointer(&iidIMMDeviceEnumerator)),
|
uintptr(unsafe.Pointer(&iidIMMDeviceEnumerator)),
|
||||||
uintptr(unsafe.Pointer(&enumerator)),
|
uintptr(unsafe.Pointer(&enumerator)),
|
||||||
)
|
)
|
||||||
@@ -255,7 +272,6 @@ func audioCaptureLoop() {
|
|||||||
}
|
}
|
||||||
defer comRelease(enumerator)
|
defer comRelease(enumerator)
|
||||||
|
|
||||||
// enumerator->GetDefaultAudioEndpoint(eRender=0, eConsole=0)
|
|
||||||
var device uintptr
|
var device uintptr
|
||||||
hr = comCall(enumerator, enumeratorGetDefaultAudioEndpoint, 0, 0, uintptr(unsafe.Pointer(&device)))
|
hr = comCall(enumerator, enumeratorGetDefaultAudioEndpoint, 0, 0, uintptr(unsafe.Pointer(&device)))
|
||||||
if hr != 0 || device == 0 {
|
if hr != 0 || device == 0 {
|
||||||
@@ -265,7 +281,6 @@ func audioCaptureLoop() {
|
|||||||
}
|
}
|
||||||
defer comRelease(device)
|
defer comRelease(device)
|
||||||
|
|
||||||
// device->Activate(IID_IAudioClient, CLSCTX_INPROC=1, NULL, &audioClient)
|
|
||||||
var audioClient uintptr
|
var audioClient uintptr
|
||||||
hr = comCall(device, deviceActivate,
|
hr = comCall(device, deviceActivate,
|
||||||
uintptr(unsafe.Pointer(&iidIAudioClient)),
|
uintptr(unsafe.Pointer(&iidIAudioClient)),
|
||||||
@@ -279,7 +294,6 @@ func audioCaptureLoop() {
|
|||||||
}
|
}
|
||||||
defer comRelease(audioClient)
|
defer comRelease(audioClient)
|
||||||
|
|
||||||
// audioClient->GetMixFormat(&pwfx)
|
|
||||||
var pwfx uintptr
|
var pwfx uintptr
|
||||||
hr = comCall(audioClient, audioClientGetMixFormat, uintptr(unsafe.Pointer(&pwfx)))
|
hr = comCall(audioClient, audioClientGetMixFormat, uintptr(unsafe.Pointer(&pwfx)))
|
||||||
if hr != 0 || pwfx == 0 {
|
if hr != 0 || pwfx == 0 {
|
||||||
@@ -293,11 +307,10 @@ func audioCaptureLoop() {
|
|||||||
nSamplesPerSec := *(*uint32)(unsafe.Pointer(pwfx + 4))
|
nSamplesPerSec := *(*uint32)(unsafe.Pointer(pwfx + 4))
|
||||||
log.Printf("音频捕获: 格式 ch=%d rate=%d", nChannels, nSamplesPerSec)
|
log.Printf("音频捕获: 格式 ch=%d rate=%d", nChannels, nSamplesPerSec)
|
||||||
|
|
||||||
// audioClient->Initialize(SHARED, LOOPBACK, 1s buffer, 0, pwfx, NULL)
|
|
||||||
hr = comCall(audioClient, audioClientInitialize,
|
hr = comCall(audioClient, audioClientInitialize,
|
||||||
0, // AUDCLNT_SHAREMODE_SHARED
|
0,
|
||||||
0x00020000, // AUDCLNT_STREAMFLAGS_LOOPBACK
|
0x00020000,
|
||||||
10000000, // 1 second (100ns units)
|
10000000,
|
||||||
0,
|
0,
|
||||||
pwfx,
|
pwfx,
|
||||||
0,
|
0,
|
||||||
@@ -308,7 +321,6 @@ func audioCaptureLoop() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// audioClient->GetService(IID_IAudioCaptureClient, &captureClient)
|
|
||||||
var captureClient uintptr
|
var captureClient uintptr
|
||||||
hr = comCall(audioClient, audioClientGetService,
|
hr = comCall(audioClient, audioClientGetService,
|
||||||
uintptr(unsafe.Pointer(&iidIAudioCaptureClient)),
|
uintptr(unsafe.Pointer(&iidIAudioCaptureClient)),
|
||||||
@@ -321,11 +333,9 @@ func audioCaptureLoop() {
|
|||||||
}
|
}
|
||||||
defer comRelease(captureClient)
|
defer comRelease(captureClient)
|
||||||
|
|
||||||
// audioClient->Start()
|
|
||||||
comCall(audioClient, audioClientStart)
|
comCall(audioClient, audioClientStart)
|
||||||
defer comCall(audioClient, audioClientStop)
|
defer comCall(audioClient, audioClientStop)
|
||||||
|
|
||||||
// 环形缓冲区 (~100ms)
|
|
||||||
ch := int(nChannels)
|
ch := int(nChannels)
|
||||||
bufSize := int(nSamplesPerSec)/10*ch + fftSize*ch
|
bufSize := int(nSamplesPerSec)/10*ch + fftSize*ch
|
||||||
ringBuf := make([]float32, bufSize)
|
ringBuf := make([]float32, bufSize)
|
||||||
@@ -337,6 +347,9 @@ func audioCaptureLoop() {
|
|||||||
smoothingFactor = 0.7
|
smoothingFactor = 0.7
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 预分配 mono slice
|
||||||
|
mono := make([]float32, fftSize)
|
||||||
|
|
||||||
ticker := time.NewTicker(33 * time.Millisecond)
|
ticker := time.NewTicker(33 * time.Millisecond)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
@@ -348,7 +361,6 @@ func audioCaptureLoop() {
|
|||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取 WASAPI 缓冲区
|
|
||||||
for {
|
for {
|
||||||
var pData, nFrames, flags uintptr
|
var pData, nFrames, flags uintptr
|
||||||
hr = comCall(captureClient, captureGetBuffer,
|
hr = comCall(captureClient, captureGetBuffer,
|
||||||
@@ -368,8 +380,6 @@ func audioCaptureLoop() {
|
|||||||
comCall(captureClient, captureReleaseBuffer, uintptr(nFrames))
|
comCall(captureClient, captureReleaseBuffer, uintptr(nFrames))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取 fftSize 个单声道样本
|
|
||||||
mono := make([]float32, fftSize)
|
|
||||||
for i := 0; i < fftSize; i++ {
|
for i := 0; i < fftSize; i++ {
|
||||||
pos := (writePos - fftSize*ch + i*ch + bufSize) % bufSize
|
pos := (writePos - fftSize*ch + i*ch + bufSize) % bufSize
|
||||||
var sum float32
|
var sum float32
|
||||||
@@ -383,20 +393,25 @@ func audioCaptureLoop() {
|
|||||||
bins := downsampleBins(mag, targetBins)
|
bins := downsampleBins(mag, targetBins)
|
||||||
|
|
||||||
if smoothed != nil {
|
if smoothed != nil {
|
||||||
bins = smoothBins(smoothed, bins, smoothingFactor)
|
bins = smoothBinsGo(smoothed, bins, smoothingFactor)
|
||||||
}
|
}
|
||||||
smoothed = bins
|
smoothed = bins
|
||||||
|
|
||||||
// 归一化
|
// 全局归一化(慢衰减 runningMax)
|
||||||
maxVal := 0.0
|
curMax := 0.0
|
||||||
for _, v := range bins {
|
for _, v := range bins {
|
||||||
if v > maxVal {
|
if v > curMax {
|
||||||
maxVal = v
|
curMax = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if maxVal > 0.001 {
|
if curMax > runningMax {
|
||||||
|
runningMax = curMax
|
||||||
|
} else {
|
||||||
|
runningMax *= 0.985
|
||||||
|
}
|
||||||
|
if runningMax > 0.001 {
|
||||||
for i := range bins {
|
for i := range bins {
|
||||||
bins[i] /= maxVal
|
bins[i] /= runningMax
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,6 +422,6 @@ func audioCaptureLoop() {
|
|||||||
|
|
||||||
func comRelease(obj uintptr) {
|
func comRelease(obj uintptr) {
|
||||||
if obj != 0 {
|
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'">
|
<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="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="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"
|
<button v-for="st in audioVizStyles" :key="st.value"
|
||||||
@click="s.audioVizStyle = st.value; go.saveAudioVizStyle(st.value)"
|
@click="s.audioVizStyle = st.value; go.saveAudioVizStyle(st.value)"
|
||||||
:class="s.audioVizStyle === st.value
|
:class="s.audioVizStyle === st.value
|
||||||
@@ -167,6 +167,14 @@ const audioVizStyles = [
|
|||||||
{ value: 'bars', label: '柱状' },
|
{ value: 'bars', label: '柱状' },
|
||||||
{ value: 'waveform', label: '波形' },
|
{ value: 'waveform', label: '波形' },
|
||||||
{ value: 'circular', 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 = [
|
const audioColors = [
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+4
-4
File diff suppressed because one or more lines are too long
+1153
-140
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user