import { ref, onUnmounted } from 'vue' import { useQuasar } from 'quasar' export interface AudioOptions { playbackRate?: number volume?: number loop?: boolean preload?: boolean } export function useAudio() { const $q = useQuasar() // 狀態管理 const isPlaying = ref(false) const isLoading = ref(false) const duration = ref(0) const currentTime = ref(0) const volume = ref(1) const playbackRate = ref(1) const error = ref(null) // Web Audio API 支援 let audioContext: AudioContext | null = null let currentAudioSource: AudioBufferSourceNode | null = null let gainNode: GainNode | null = null let audioBuffer: AudioBuffer | null = null // HTML5 Audio fallback let htmlAudio: HTMLAudioElement | null = null // 初始化音頻上下文 const initAudioContext = async (): Promise => { if (audioContext) return true try { audioContext = new (window.AudioContext || (window as any).webkitAudioContext)() gainNode = audioContext.createGain() gainNode.connect(audioContext.destination) return true } catch (err) { console.warn('Web Audio API 不支援,使用 HTML5 Audio fallback:', err) return false } } // 載入音頻文件 const loadAudio = async (url: string): Promise => { error.value = null isLoading.value = true try { const useWebAudio = await initAudioContext() if (useWebAudio && audioContext) { // 使用 Web Audio API const response = await fetch(url) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } const arrayBuffer = await response.arrayBuffer() audioBuffer = await audioContext.decodeAudioData(arrayBuffer) duration.value = audioBuffer.duration } else { // 使用 HTML5 Audio fallback htmlAudio = new Audio() htmlAudio.preload = 'auto' htmlAudio.src = url return new Promise((resolve, reject) => { if (!htmlAudio) { reject(new Error('無法創建 Audio 元素')) return } htmlAudio.onloadedmetadata = () => { duration.value = htmlAudio!.duration resolve(true) } htmlAudio.onerror = () => { reject(new Error('音頻載入失敗')) } }) } return true } catch (err) { error.value = err instanceof Error ? err.message : '載入音頻失敗' console.error('載入音頻失敗:', err) return false } finally { isLoading.value = false } } // 播放音頻 const play = async (options?: AudioOptions) => { if (isPlaying.value) { stop() } try { if (audioBuffer && audioContext && gainNode) { // 使用 Web Audio API 播放 currentAudioSource = audioContext.createBufferSource() currentAudioSource.buffer = audioBuffer currentAudioSource.playbackRate.value = options?.playbackRate || playbackRate.value gainNode.gain.value = options?.volume || volume.value currentAudioSource.connect(gainNode) currentAudioSource.start(0) currentAudioSource.onended = () => { isPlaying.value = false currentTime.value = 0 } } else if (htmlAudio) { // 使用 HTML5 Audio 播放 htmlAudio.volume = options?.volume || volume.value htmlAudio.playbackRate = options?.playbackRate || playbackRate.value htmlAudio.loop = options?.loop || false htmlAudio.ontimeupdate = () => { currentTime.value = htmlAudio!.currentTime } htmlAudio.onended = () => { isPlaying.value = false currentTime.value = 0 } await htmlAudio.play() } else { throw new Error('沒有可用的音頻資源') } isPlaying.value = true error.value = null } catch (err) { error.value = err instanceof Error ? err.message : '播放失敗' isPlaying.value = false $q.notify({ type: 'negative', message: error.value }) } } // 暫停音頻 const pause = () => { if (currentAudioSource) { currentAudioSource.stop() currentAudioSource = null } if (htmlAudio) { htmlAudio.pause() } isPlaying.value = false } // 停止音頻 const stop = () => { pause() currentTime.value = 0 if (htmlAudio) { htmlAudio.currentTime = 0 } } // 設置音量 const setVolume = (newVolume: number) => { volume.value = Math.max(0, Math.min(1, newVolume)) if (gainNode) { gainNode.gain.value = volume.value } if (htmlAudio) { htmlAudio.volume = volume.value } } // 設置播放速度 const setPlaybackRate = (rate: number) => { playbackRate.value = Math.max(0.25, Math.min(4, rate)) if (currentAudioSource) { currentAudioSource.playbackRate.value = playbackRate.value } if (htmlAudio) { htmlAudio.playbackRate = playbackRate.value } } // 跳轉到指定時間 const seekTo = (time: number) => { if (htmlAudio) { htmlAudio.currentTime = Math.max(0, Math.min(duration.value, time)) currentTime.value = htmlAudio.currentTime } } // 快速播放功能(用於詞彙學習) const quickPlay = async (url: string, options?: AudioOptions) => { const success = await loadAudio(url) if (success) { await play(options) } return success } // 銷毀資源 const cleanup = () => { stop() if (audioBuffer) { audioBuffer = null } if (htmlAudio) { htmlAudio.remove() htmlAudio = null } if (audioContext && audioContext.state !== 'closed') { audioContext.close() audioContext = null } gainNode = null currentAudioSource = null } // 組件卸載時清理資源 onUnmounted(() => { cleanup() }) return { // 狀態 isPlaying, isLoading, duration, currentTime, volume, playbackRate, error, // 方法 loadAudio, play, pause, stop, setVolume, setPlaybackRate, seekTo, quickPlay, cleanup } }