feat(音频可视化): 添加音频可视化功能并优化播放队列显示
引入音频可视化器模块,用于实时显示音频频谱。在播放队列中添加可视化效果,并优化播放队列的UI显示。同时,更新播放队列的标题为“播放队列”以提升用户体验。
This commit is contained in:
		
							parent
							
								
									519816c050
								
							
						
					
					
						commit
						73aaef1662
					
				| 
						 | 
				
			
			@ -1,17 +1,22 @@
 | 
			
		|||
<!-- Player.vue - 添加调试信息 -->
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
 | 
			
		||||
import { useTemplateRef, watch } from 'vue'
 | 
			
		||||
import { useTemplateRef, watch, nextTick } from 'vue'
 | 
			
		||||
import { useRoute } from 'vue-router'
 | 
			
		||||
 | 
			
		||||
import PlayIcon from '../assets/icons/play.vue'
 | 
			
		||||
import PauseIcon from '../assets/icons/pause.vue'
 | 
			
		||||
import LoadingIndicator from '../assets/icons/loadingindicator.vue'
 | 
			
		||||
import { audioVisualizer } from '../utils'
 | 
			
		||||
 | 
			
		||||
const playQueueStore = usePlayQueueStore()
 | 
			
		||||
const route = useRoute()
 | 
			
		||||
const player = useTemplateRef('playerRef')
 | 
			
		||||
 | 
			
		||||
console.log('[Player] 组件初始化')
 | 
			
		||||
 | 
			
		||||
watch(() => playQueueStore.isPlaying, (newValue) => {
 | 
			
		||||
	console.log('[Player] 播放状态变化:', newValue)
 | 
			
		||||
	if (newValue) {
 | 
			
		||||
		player.value?.play()
 | 
			
		||||
		setMetadata()
 | 
			
		||||
| 
						 | 
				
			
			@ -20,6 +25,7 @@ watch(() => playQueueStore.isPlaying, (newValue) => {
 | 
			
		|||
})
 | 
			
		||||
 | 
			
		||||
watch(() => playQueueStore.currentIndex, () => {
 | 
			
		||||
	console.log('[Player] 当前索引变化:', playQueueStore.currentIndex)
 | 
			
		||||
	setMetadata()
 | 
			
		||||
	playQueueStore.isBuffering = true
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -32,6 +38,7 @@ function artistsOrganize(list: string[]) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
function setMetadata() {
 | 
			
		||||
	console.log('[Player] 设置元数据')
 | 
			
		||||
	if ('mediaSession' in navigator) {
 | 
			
		||||
		navigator.mediaSession.metadata = new MediaMetadata({
 | 
			
		||||
			title: playQueueStore.list[playQueueStore.currentIndex].song.name,
 | 
			
		||||
| 
						 | 
				
			
			@ -80,6 +87,63 @@ function playPrevious() {
 | 
			
		|||
function updateCurrentTime() {
 | 
			
		||||
	playQueueStore.currentTime = player.value?.currentTime?? 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
console.log('[Player] 初始化 audioVisualizer')
 | 
			
		||||
const { barHeights, connectAudio, isAnalyzing, error } = audioVisualizer({
 | 
			
		||||
	sensitivity: 1.5,
 | 
			
		||||
	barCount: 6,
 | 
			
		||||
	debug: true
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
console.log('[Player] audioVisualizer 返回值:', { barHeights: barHeights.value, isAnalyzing: isAnalyzing.value })
 | 
			
		||||
 | 
			
		||||
// 监听播放列表变化
 | 
			
		||||
watch(() => playQueueStore.list.length, async (newLength) => {
 | 
			
		||||
	console.log('[Player] 播放列表长度变化:', newLength)
 | 
			
		||||
	if (newLength === 0) { 
 | 
			
		||||
		console.log('[Player] 播放列表为空,跳过连接')
 | 
			
		||||
		return 
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	// 等待下一帧,确保 audio 元素已经渲染
 | 
			
		||||
	await nextTick()
 | 
			
		||||
	
 | 
			
		||||
	if (player.value) { 
 | 
			
		||||
		console.log('[Player] 连接音频元素到可视化器')
 | 
			
		||||
		console.log('[Player] 音频元素状态:', {
 | 
			
		||||
			src: player.value.src?.substring(0, 50) + '...',
 | 
			
		||||
			readyState: player.value.readyState,
 | 
			
		||||
			paused: player.value.paused
 | 
			
		||||
		})
 | 
			
		||||
		connectAudio(player.value)
 | 
			
		||||
	} else {
 | 
			
		||||
		console.log('[Player] ❌ 音频元素不存在')
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	playQueueStore.visualizer = barHeights.value
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 监听音频元素变化
 | 
			
		||||
watch(() => player.value, (audioElement) => {
 | 
			
		||||
	console.log('[Player] 音频元素 ref 变化:', !!audioElement)
 | 
			
		||||
	if (audioElement && playQueueStore.list.length > 0) {
 | 
			
		||||
		console.log('[Player] 重新连接音频元素')
 | 
			
		||||
		connectAudio(audioElement)
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 监听可视化器数据变化
 | 
			
		||||
watch(() => barHeights.value, (newHeights) => {
 | 
			
		||||
	console.log('[Player] 可视化器数据更新:', newHeights.map(h => Math.round(h)))
 | 
			
		||||
	playQueueStore.visualizer = newHeights
 | 
			
		||||
}, { deep: true })
 | 
			
		||||
 | 
			
		||||
// 监听错误
 | 
			
		||||
watch(() => error.value, (newError) => {
 | 
			
		||||
	if (newError) {
 | 
			
		||||
		console.error('[Player] 可视化器错误:', newError)
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
| 
						 | 
				
			
			@ -93,37 +157,41 @@ function updateCurrentTime() {
 | 
			
		|||
			@pause="playQueueStore.isPlaying = false" 
 | 
			
		||||
			@play="playQueueStore.isPlaying = true" 
 | 
			
		||||
			@playing="() => {
 | 
			
		||||
				console.log('[Player] 音频开始播放事件')
 | 
			
		||||
				playQueueStore.isBuffering = false
 | 
			
		||||
				setMetadata()
 | 
			
		||||
			}" 
 | 
			
		||||
			@waiting="playQueueStore.isBuffering = true"
 | 
			
		||||
			@loadeddata="() => console.log('[Player] 音频数据加载完成')"
 | 
			
		||||
			@canplay="() => console.log('[Player] 音频可以播放')"
 | 
			
		||||
			@error="(e) => console.error('[Player] 音频错误:', e)"
 | 
			
		||||
			crossorigin="anonymous"
 | 
			
		||||
			@timeupdate="updateCurrentTime">
 | 
			
		||||
		</audio>
 | 
			
		||||
 | 
			
		||||
		
 | 
			
		||||
			<div
 | 
			
		||||
				class="text-white h-9 bg-neutral-800/80 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex gap-2 overflow-hidden select-none"
 | 
			
		||||
				v-if="playQueueStore.list.length !== 0 && route.path !== '/playroom'"
 | 
			
		||||
				>
 | 
			
		||||
				<RouterLink to="/playroom">
 | 
			
		||||
					<img :src="playQueueStore.list[playQueueStore.currentIndex].album?.coverUrl ?? ''" class="rounded-full h-9 w-9" />
 | 
			
		||||
				</RouterLink>
 | 
			
		||||
		<div
 | 
			
		||||
			class="text-white h-9 bg-neutral-800/80 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex gap-2 overflow-hidden select-none"
 | 
			
		||||
			v-if="playQueueStore.list.length !== 0 && route.path !== '/playroom'"
 | 
			
		||||
			>
 | 
			
		||||
			<RouterLink to="/playroom">
 | 
			
		||||
				<img :src="playQueueStore.list[playQueueStore.currentIndex].album?.coverUrl ?? ''" class="rounded-full h-9 w-9" />
 | 
			
		||||
			</RouterLink>
 | 
			
		||||
 | 
			
		||||
				<RouterLink to="/playroom">
 | 
			
		||||
					<div class="flex items-center w-32 h-9">
 | 
			
		||||
						<span class="truncate">{{ playQueueStore.list[playQueueStore.currentIndex].song.name }}</span>
 | 
			
		||||
					</div>
 | 
			
		||||
				</RouterLink>
 | 
			
		||||
			<RouterLink to="/playroom">
 | 
			
		||||
				<div class="flex items-center w-32 h-9">
 | 
			
		||||
					<span class="truncate">{{ playQueueStore.list[playQueueStore.currentIndex].song.name }}</span>
 | 
			
		||||
				</div>
 | 
			
		||||
			</RouterLink>
 | 
			
		||||
 | 
			
		||||
				<button class="h-9 w-9 flex justify-center items-center" @click.stop="() => {
 | 
			
		||||
					playQueueStore.isPlaying = !playQueueStore.isPlaying
 | 
			
		||||
				}">
 | 
			
		||||
					<div v-if="playQueueStore.isPlaying">
 | 
			
		||||
						<LoadingIndicator v-if="playQueueStore.isBuffering === true" :size="4" />
 | 
			
		||||
						<PauseIcon v-else :size="4" />
 | 
			
		||||
					</div>
 | 
			
		||||
					<PlayIcon v-else :size="4" />
 | 
			
		||||
				</button>
 | 
			
		||||
			</div>
 | 
			
		||||
			<button class="h-9 w-9 flex justify-center items-center" @click.stop="() => {
 | 
			
		||||
				playQueueStore.isPlaying = !playQueueStore.isPlaying
 | 
			
		||||
			}">
 | 
			
		||||
				<div v-if="playQueueStore.isPlaying">
 | 
			
		||||
					<LoadingIndicator v-if="playQueueStore.isBuffering === true" :size="4" />
 | 
			
		||||
					<PauseIcon v-else :size="4" />
 | 
			
		||||
				</div>
 | 
			
		||||
				<PlayIcon v-else :size="4" />
 | 
			
		||||
			</button>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -224,7 +224,7 @@ function makePlayQueueListDismiss() {
 | 
			
		|||
	<dialog :open="presentQueueListDialog" class="z-20 w-screen h-screen" @click="makePlayQueueListDismiss" ref="playQueueDialogContainer" style="background-color: #17171780;">
 | 
			
		||||
		<div class="w-96 h-screen bg-neutral-900/80 shadow-[0_0_16px_0_rgba(0,0,0,0.5)] backdrop-blur-2xl pt-8 flex flex-col" @click.stop ref="playQueueDialog">
 | 
			
		||||
			<div class="flex justify-between mx-8 mb-4">
 | 
			
		||||
				<div class="text-white font-medium text-2xl">待播清单</div>
 | 
			
		||||
				<div class="text-white font-medium text-2xl">播放队列</div>
 | 
			
		||||
				<button class="text-white w-9 h-9 bg-neutral-800/80 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex justify-center items-center" @click="makePlayQueueListDismiss">
 | 
			
		||||
					<XIcon :size="4" />
 | 
			
		||||
				</button>
 | 
			
		||||
| 
						 | 
				
			
			@ -241,8 +241,25 @@ function makePlayQueueListDismiss() {
 | 
			
		|||
 | 
			
		||||
			<hr class="border-[#ffffff39]" />
 | 
			
		||||
 | 
			
		||||
			<div class="flex-auto h-0 overflow-y-auto px-8">
 | 
			
		||||
				
 | 
			
		||||
			<div class="flex-auto h-0 overflow-y-auto px-4 flex flex-col gap-2">
 | 
			
		||||
				<button v-for="(track, index) in playQueueStore.list" class="p-4 w-full rounded-md hover:bg-white/5 first:mt-2" :key="track.song.cid">
 | 
			
		||||
					<div class="flex gap-2">
 | 
			
		||||
						<div class="relative w-12 h-12 rounded-md shadow-xl overflow-hidden">
 | 
			
		||||
							<img :src="track.album?.coverUrl" />
 | 
			
		||||
							<div class="w-full h-full absolute top-0 left-0 bg-neutral-900/75 flex justify-center items-center" v-if="index === playQueueStore.currentIndex">
 | 
			
		||||
								<div style="height: 1rem;" class="flex justify-center items-center gap-[.125rem]">
 | 
			
		||||
									<div class="bg-white w-[.125rem] rounded-full" v-for="(bar, index) in playQueueStore.visualizer" :key="index" :style="{
 | 
			
		||||
										height: `${Math.max(10, bar)}%`
 | 
			
		||||
									}" />
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div class="flex flex-col text-left">
 | 
			
		||||
							<div class="text-white text-base font-medium">{{ track.song.name }}</div>
 | 
			
		||||
							<div class="text-white/75 text-sm">{{ artistsOrganize(track.song.artists?? []) }} — {{ track.album?.name?? '未知专辑' }}</div>
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</button>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</dialog>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,6 +10,7 @@ export const usePlayQueueStore = defineStore('queue', () =>{
 | 
			
		|||
	const currentTime = ref<number>(0)
 | 
			
		||||
	const duration = ref<number>(0)
 | 
			
		||||
	const updatedCurrentTime = ref<number | null>(null)
 | 
			
		||||
	const visualizer = ref<number[]>([0, 0, 0, 0])
 | 
			
		||||
 | 
			
		||||
  return { list, currentIndex, isPlaying, queueReplaceLock, isBuffering, currentTime, duration, updatedCurrentTime }
 | 
			
		||||
  return { list, currentIndex, isPlaying, queueReplaceLock, isBuffering, currentTime, duration, updatedCurrentTime, visualizer }
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										331
									
								
								src/utils/audioVisualizer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										331
									
								
								src/utils/audioVisualizer.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,331 @@
 | 
			
		|||
// utils/audioVisualizer.ts - 平衡频谱版本
 | 
			
		||||
import { ref, onUnmounted, Ref } from 'vue'
 | 
			
		||||
 | 
			
		||||
interface AudioVisualizerOptions {
 | 
			
		||||
  sensitivity?: number
 | 
			
		||||
  smoothing?: number
 | 
			
		||||
  barCount?: number
 | 
			
		||||
  debug?: boolean
 | 
			
		||||
  bassBoost?: number     // 低音增强倍数 (默认 0.7,降低低音)
 | 
			
		||||
  midBoost?: number      // 中音增强倍数 (默认 1.2)
 | 
			
		||||
  trebleBoost?: number   // 高音增强倍数 (默认 1.5)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
			
		||||
  const {
 | 
			
		||||
    sensitivity = 1,
 | 
			
		||||
    smoothing = 0.7,
 | 
			
		||||
    barCount = 4,
 | 
			
		||||
    debug = false,
 | 
			
		||||
    bassBoost = 0.7,      // 降低低音权重
 | 
			
		||||
    midBoost = 1.2,       // 提升中音
 | 
			
		||||
    trebleBoost = 1.5     // 提升高音
 | 
			
		||||
  } = options
 | 
			
		||||
 | 
			
		||||
  console.log('[AudioVisualizer] 初始化平衡频谱,选项:', options)
 | 
			
		||||
 | 
			
		||||
  // 导出的竖杠高度值数组 (0-100)
 | 
			
		||||
  const barHeights: Ref<number[]> = ref(Array(barCount).fill(0))
 | 
			
		||||
  const isAnalyzing = ref(false)
 | 
			
		||||
  const error = ref<string | null>(null)
 | 
			
		||||
  const isInitialized = ref(false)
 | 
			
		||||
 | 
			
		||||
  // 内部变量
 | 
			
		||||
  let audioContext: AudioContext | null = null
 | 
			
		||||
  let analyser: AnalyserNode | null = null
 | 
			
		||||
  let source: MediaElementAudioSourceNode | null = null
 | 
			
		||||
  let dataArray: Uint8Array | null = null
 | 
			
		||||
  let animationId: number | null = null
 | 
			
		||||
  let currentAudioElement: HTMLAudioElement | null = null
 | 
			
		||||
 | 
			
		||||
  // 调试日志
 | 
			
		||||
  function log(...args: any[]) {
 | 
			
		||||
    if (debug) {
 | 
			
		||||
      console.log('[AudioVisualizer]', ...args)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 初始化音频分析
 | 
			
		||||
  function initAudioContext(audioElement: HTMLAudioElement) {
 | 
			
		||||
    if (!audioElement) {
 | 
			
		||||
      log('错误: 音频元素为空')
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    if (audioContext) {
 | 
			
		||||
      log('AudioContext 已存在,跳过初始化')
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      log('开始初始化音频上下文...')
 | 
			
		||||
 | 
			
		||||
      audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
 | 
			
		||||
      log('AudioContext 创建成功, 状态:', audioContext.state, '采样率:', audioContext.sampleRate)
 | 
			
		||||
 | 
			
		||||
      // 如果上下文被暂停,尝试恢复
 | 
			
		||||
      if (audioContext.state === 'suspended') {
 | 
			
		||||
        audioContext.resume().then(() => {
 | 
			
		||||
          log('AudioContext 已恢复')
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      analyser = audioContext.createAnalyser()
 | 
			
		||||
      
 | 
			
		||||
      // 尝试创建音频源
 | 
			
		||||
      try {
 | 
			
		||||
        source = audioContext.createMediaElementSource(audioElement)
 | 
			
		||||
        log('MediaElementSource 创建成功')
 | 
			
		||||
      } catch (sourceError) {
 | 
			
		||||
        log('创建 MediaElementSource 失败:', sourceError)
 | 
			
		||||
        error.value = 'CORS 错误: 无法访问跨域音频'
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // 优化分析器配置
 | 
			
		||||
      analyser.fftSize = 2048        // 增加分辨率
 | 
			
		||||
      analyser.smoothingTimeConstant = smoothing
 | 
			
		||||
      analyser.minDecibels = -100    // 更低的最小分贝
 | 
			
		||||
      analyser.maxDecibels = -30     // 调整最大分贝范围
 | 
			
		||||
      
 | 
			
		||||
      log('分析器配置:', {
 | 
			
		||||
        fftSize: analyser.fftSize,
 | 
			
		||||
        frequencyBinCount: analyser.frequencyBinCount,
 | 
			
		||||
        sampleRate: audioContext.sampleRate,
 | 
			
		||||
        frequencyResolution: audioContext.sampleRate / analyser.fftSize
 | 
			
		||||
      })
 | 
			
		||||
      
 | 
			
		||||
      // 连接音频节点
 | 
			
		||||
      source.connect(analyser)
 | 
			
		||||
      analyser.connect(audioContext.destination)
 | 
			
		||||
      
 | 
			
		||||
      // 创建数据数组
 | 
			
		||||
      dataArray = new Uint8Array(analyser.frequencyBinCount)
 | 
			
		||||
      
 | 
			
		||||
      isInitialized.value = true
 | 
			
		||||
      error.value = null
 | 
			
		||||
      log('✅ 音频可视化器初始化成功')
 | 
			
		||||
      
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      log('❌ 音频上下文初始化失败:', err)
 | 
			
		||||
      error.value = `初始化失败: ${err instanceof Error ? err.message : String(err)}`
 | 
			
		||||
      isInitialized.value = false
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 开始分析
 | 
			
		||||
  function startAnalysis() {
 | 
			
		||||
    if (!analyser || !dataArray || !isInitialized.value) {
 | 
			
		||||
      log('❌ 无法开始分析: 分析器未初始化')
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    log('✅ 开始频谱分析')
 | 
			
		||||
    isAnalyzing.value = true
 | 
			
		||||
    animate()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 停止分析
 | 
			
		||||
  function stopAnalysis() {
 | 
			
		||||
    log('停止频谱分析')
 | 
			
		||||
    isAnalyzing.value = false
 | 
			
		||||
    if (animationId) {
 | 
			
		||||
      cancelAnimationFrame(animationId)
 | 
			
		||||
      animationId = null
 | 
			
		||||
    }
 | 
			
		||||
    barHeights.value = Array(barCount).fill(0)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 动画循环
 | 
			
		||||
  function animate() {
 | 
			
		||||
    if (!isAnalyzing.value || !analyser || !dataArray || !audioContext) return
 | 
			
		||||
    
 | 
			
		||||
    // 获取频率数据
 | 
			
		||||
    analyser.getByteFrequencyData(dataArray)
 | 
			
		||||
    
 | 
			
		||||
    // 使用平衡的频段分割
 | 
			
		||||
    const frequencyBands = divideFrequencyBandsBalanced(dataArray, barCount, audioContext.sampleRate)
 | 
			
		||||
    
 | 
			
		||||
    // 应用频段特定的增强
 | 
			
		||||
    const enhancedBands = applyFrequencyEnhancement(frequencyBands)
 | 
			
		||||
    
 | 
			
		||||
    // 更新竖杠高度 (0-100)
 | 
			
		||||
    barHeights.value = enhancedBands.map(value => 
 | 
			
		||||
      Math.min(100, Math.max(0, (value / 255) * 100 * sensitivity))
 | 
			
		||||
    )
 | 
			
		||||
    
 | 
			
		||||
    animationId = requestAnimationFrame(animate)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 平衡的频段分割 - 使用对数分布和人耳感知特性
 | 
			
		||||
  function divideFrequencyBandsBalanced(data: Uint8Array, bands: number, sampleRate: number): number[] {
 | 
			
		||||
    const nyquist = sampleRate / 2
 | 
			
		||||
    const result: number[] = []
 | 
			
		||||
    
 | 
			
		||||
    // 定义人耳感知的频率范围 (Hz)
 | 
			
		||||
    const frequencyRanges = [
 | 
			
		||||
      { min: 20, max: 250, name: '低音' },      // 低音
 | 
			
		||||
      { min: 250, max: 2000, name: '中低音' },   // 中低音  
 | 
			
		||||
      { min: 2000, max: 8000, name: '中高音' },  // 中高音
 | 
			
		||||
      { min: 8000, max: 20000, name: '高音' }   // 高音
 | 
			
		||||
    ]
 | 
			
		||||
    
 | 
			
		||||
    for (let i = 0; i < bands; i++) {
 | 
			
		||||
      const range = frequencyRanges[i] || frequencyRanges[frequencyRanges.length - 1]
 | 
			
		||||
      
 | 
			
		||||
      // 将频率转换为 bin 索引
 | 
			
		||||
      const startBin = Math.floor((range.min / nyquist) * data.length)
 | 
			
		||||
      const endBin = Math.floor((range.max / nyquist) * data.length)
 | 
			
		||||
      
 | 
			
		||||
      // 确保范围有效
 | 
			
		||||
      const actualStart = Math.max(0, startBin)
 | 
			
		||||
      const actualEnd = Math.min(data.length - 1, endBin)
 | 
			
		||||
      
 | 
			
		||||
      if (debug && Math.random() < 0.01) {
 | 
			
		||||
        log(`频段 ${i} (${range.name}): ${range.min}-${range.max}Hz, bins ${actualStart}-${actualEnd}`)
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // 计算该频段的 RMS (均方根) 值,而不是简单平均
 | 
			
		||||
      let sumSquares = 0
 | 
			
		||||
      let count = 0
 | 
			
		||||
      
 | 
			
		||||
      for (let j = actualStart; j <= actualEnd; j++) {
 | 
			
		||||
        const value = data[j]
 | 
			
		||||
        sumSquares += value * value
 | 
			
		||||
        count++
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      const rms = count > 0 ? Math.sqrt(sumSquares / count) : 0
 | 
			
		||||
      result.push(rms)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    return result
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 应用频段特定的增强
 | 
			
		||||
  function applyFrequencyEnhancement(bands: number[]): number[] {
 | 
			
		||||
    const boosts = [bassBoost, midBoost, trebleBoost, trebleBoost]
 | 
			
		||||
    
 | 
			
		||||
    return bands.map((value, index) => {
 | 
			
		||||
      const boost = boosts[index] || 1
 | 
			
		||||
      let enhanced = value * boost
 | 
			
		||||
      
 | 
			
		||||
      // 应用压缩曲线,防止过度增强
 | 
			
		||||
      enhanced = 255 * Math.tanh(enhanced / 255)
 | 
			
		||||
      
 | 
			
		||||
      return Math.min(255, Math.max(0, enhanced))
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 连接音频元素
 | 
			
		||||
  function connectAudio(audioElement: HTMLAudioElement) {
 | 
			
		||||
    log('🔗 连接音频元素...')
 | 
			
		||||
    
 | 
			
		||||
    if (currentAudioElement === audioElement) {
 | 
			
		||||
      log('音频元素相同,跳过重复连接')
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // 清理旧的连接
 | 
			
		||||
    cleanup()
 | 
			
		||||
    
 | 
			
		||||
    currentAudioElement = audioElement
 | 
			
		||||
    
 | 
			
		||||
    // 等待音频加载完成后再初始化
 | 
			
		||||
    if (audioElement.readyState >= 2) {
 | 
			
		||||
      initAudioContext(audioElement)
 | 
			
		||||
    } else {
 | 
			
		||||
      audioElement.addEventListener('loadeddata', () => {
 | 
			
		||||
        initAudioContext(audioElement)
 | 
			
		||||
      }, { once: true })
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // 监听播放状态
 | 
			
		||||
    audioElement.addEventListener('play', startAnalysis)
 | 
			
		||||
    audioElement.addEventListener('pause', stopAnalysis)
 | 
			
		||||
    audioElement.addEventListener('ended', stopAnalysis)
 | 
			
		||||
    
 | 
			
		||||
    // 监听错误
 | 
			
		||||
    audioElement.addEventListener('error', (e) => {
 | 
			
		||||
      log('❌ 音频加载错误:', e)
 | 
			
		||||
      error.value = '音频加载失败'
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 断开音频元素
 | 
			
		||||
  function disconnectAudio() {
 | 
			
		||||
    if (currentAudioElement) {
 | 
			
		||||
      currentAudioElement.removeEventListener('play', startAnalysis)
 | 
			
		||||
      currentAudioElement.removeEventListener('pause', stopAnalysis)
 | 
			
		||||
      currentAudioElement.removeEventListener('ended', stopAnalysis)
 | 
			
		||||
      currentAudioElement = null
 | 
			
		||||
    }
 | 
			
		||||
    cleanup()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 清理资源
 | 
			
		||||
  function cleanup() {
 | 
			
		||||
    stopAnalysis()
 | 
			
		||||
    if (audioContext && audioContext.state !== 'closed') {
 | 
			
		||||
      audioContext.close()
 | 
			
		||||
    }
 | 
			
		||||
    audioContext = null
 | 
			
		||||
    analyser = null
 | 
			
		||||
    source = null
 | 
			
		||||
    dataArray = null
 | 
			
		||||
    isInitialized.value = false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 手动测试数据
 | 
			
		||||
  function testWithFakeData() {
 | 
			
		||||
    log('🧪 开始平衡频谱模拟测试')
 | 
			
		||||
    isAnalyzing.value = true
 | 
			
		||||
    
 | 
			
		||||
    let testCount = 0
 | 
			
		||||
    const maxTests = 50
 | 
			
		||||
    
 | 
			
		||||
    const fakeInterval = setInterval(() => {
 | 
			
		||||
      // 模拟更平衡的频谱数据
 | 
			
		||||
      barHeights.value = [
 | 
			
		||||
        Math.random() * 60 + 20,  // 低音:20-80
 | 
			
		||||
        Math.random() * 80 + 10,  // 中低音:10-90
 | 
			
		||||
        Math.random() * 90 + 5,   // 中高音:5-95
 | 
			
		||||
        Math.random() * 70 + 15   // 高音:15-85
 | 
			
		||||
      ]
 | 
			
		||||
      testCount++
 | 
			
		||||
      
 | 
			
		||||
      if (testCount >= maxTests) {
 | 
			
		||||
        clearInterval(fakeInterval)
 | 
			
		||||
        barHeights.value = Array(barCount).fill(0)
 | 
			
		||||
        isAnalyzing.value = false
 | 
			
		||||
        log('🧪 模拟测试结束')
 | 
			
		||||
      }
 | 
			
		||||
    }, 100)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 动态调整增强参数
 | 
			
		||||
  function updateEnhancement(bass: number, mid: number, treble: number) {
 | 
			
		||||
    options.bassBoost = bass
 | 
			
		||||
    options.midBoost = mid
 | 
			
		||||
    options.trebleBoost = treble
 | 
			
		||||
    log('更新频段增强:', { bass, mid, treble })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 组件卸载时清理
 | 
			
		||||
  onUnmounted(() => {
 | 
			
		||||
    disconnectAudio()
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    barHeights,
 | 
			
		||||
    isAnalyzing,
 | 
			
		||||
    isInitialized,
 | 
			
		||||
    error,
 | 
			
		||||
    connectAudio,
 | 
			
		||||
    disconnectAudio,
 | 
			
		||||
    startAnalysis,
 | 
			
		||||
    stopAnalysis,
 | 
			
		||||
    testWithFakeData,
 | 
			
		||||
    updateEnhancement
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +1,4 @@
 | 
			
		|||
import artistsOrganize from "./artistsOrganize"
 | 
			
		||||
import { audioVisualizer } from "./audioVisualizer"
 | 
			
		||||
 | 
			
		||||
export { artistsOrganize }
 | 
			
		||||
export { artistsOrganize, audioVisualizer }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue
	
	Block a user