import { defineStore } from 'pinia'; import { ref, computed } from 'vue'; import { get, post, getFullUrl } from '../utils/request'; import type { AudioItem, Voice, VoiceParams } from '../types'; export const useAudioStore = defineStore('audio', () => { // 状态 const voices = ref([]); const currentAudio = ref(null); const playlist = ref([]); const currentIndex = ref(-1); const isPlaying = ref(false); const currentTime = ref(0); const duration = ref(0); const playRate = ref(1); // 播放模式: 'sequence' | 'loop' | 'single' | 'random' const playMode = ref<'sequence' | 'loop' | 'single' | 'random'>('sequence'); // 音频上下文 let audioContext: UniApp.InnerAudioContext | null = null; // 计算属性 const hasPlaylist = computed(() => playlist.value.length > 0); const hasNext = computed(() => currentIndex.value < playlist.value.length - 1); const hasPrev = computed(() => currentIndex.value > 0); // 初始化音频上下文 function initAudioContext() { if (audioContext) return; audioContext = uni.createInnerAudioContext(); audioContext.onPlay(() => { isPlaying.value = true; }); audioContext.onPause(() => { isPlaying.value = false; }); audioContext.onEnded(() => { isPlaying.value = false; // 自动播放下一首 if (hasNext.value) { playNext(); } else { // 列表播放完毕,停止播放 handlePlayMode(); } }); audioContext.onTimeUpdate(() => { currentTime.value = audioContext?.currentTime || 0; const d = audioContext?.duration || 0; // 只有当音频元数据时长合理(小于1小时)且当前显示时长异常(为0或异常大)时才更新 if (d > 0 && d < 3600) { if (duration.value === 0 || duration.value > 3600 || Math.abs(duration.value - d) < 1) { duration.value = d; } } }); audioContext.onError((err: any) => { console.error('音频播放错误:', err); console.error('错误码:', err.errCode); console.error('错误信息:', err.errMsg); isPlaying.value = false; // 尝试获取更详细的错误信息 if (audioContext) { console.error('audioContext.src:', audioContext.src); console.error('audioContext.readyState:', audioContext.readyState); } }); audioContext.onCanplay(() => { console.log('音频已准备好播放'); }); } // 获取音色列表 async function fetchVoices() { const result = await get<{ voices: Voice[] }>('/tts/voices'); voices.value = result.voices; return result.voices; } // 生成音频 async function generateAudio( text: string, voiceId: string, voiceParams: VoiceParams ) { uni.showLoading({ title: '生成中...', mask: true }); try { const result = await post<{ audioId: string; audioUrl: string; duration: number; size: number; }>('/tts/generate', { text, voiceId, voiceParams }); uni.hideLoading(); return result; } catch (error) { uni.hideLoading(); throw error; } } // 播放音频 function play(audio: AudioItem) { initAudioContext(); // 重置时长,避免切换书籍后显示旧时长 currentTime.value = 0; duration.value = 0; currentAudio.value = audio; // 获取完整的音频 URL const fullUrl = getFullUrl(audio.audioUrl); console.log('播放音频:', audio.title, 'URL:', fullUrl, '时长:', audio.audioDuration); // 如果音频源相同,只切换播放状态 if (audioContext && audioContext.src === fullUrl) { console.log('音频源相同,切换播放状态'); audioContext.play(); } else if (audioContext) { console.log('设置新音频源并播放:', fullUrl); audioContext.src = fullUrl; audioContext.play(); } else { console.error('audioContext 未初始化!'); } // 使用 API 返回的 audioDuration 作为初始时长(避免音频文件 metadata 错误的问题) if (audio.audioDuration && audio.audioDuration > 0 && audio.audioDuration < 3600) { duration.value = audio.audioDuration; } } // 暂停 function pause() { if (audioContext) { audioContext.pause(); } } // 继续播放 function resume() { if (audioContext) { audioContext.play(); } } // 切换播放/暂停 function togglePlay() { if (isPlaying.value) { pause(); } else { resume(); } } // 播放上一首 function playPrev() { if (hasPrev.value) { currentIndex.value--; play(playlist.value[currentIndex.value]); } } // 播放下一首 function playNext() { if (hasNext.value) { currentIndex.value++; play(playlist.value[currentIndex.value]); } } // 处理播放模式 function handlePlayMode() { if (playlist.value.length === 0) return; switch (playMode.value) { case 'single': // 单曲循环 play(playlist.value[currentIndex.value]); break; case 'loop': // 列表循环 if (currentIndex.value < playlist.value.length - 1) { playNext(); } else { // 回到第一首 currentIndex.value = 0; play(playlist.value[0]); } break; case 'random': // 随机播放 const randomIndex = Math.floor(Math.random() * playlist.value.length); currentIndex.value = randomIndex; play(playlist.value[randomIndex]); break; case 'sequence': default: // 顺序播放(不自动播放) break; } } // 切换播放模式 function togglePlayMode() { const modes: Array<'sequence' | 'loop' | 'single' | 'random'> = ['sequence', 'loop', 'single', 'random']; const currentIndex = modes.indexOf(playMode.value); playMode.value = modes[(currentIndex + 1) % modes.length]; const modeNames = { sequence: '顺序播放', loop: '列表循环', single: '单曲循环', random: '随机播放', }; uni.showToast({ title: modeNames[playMode.value], icon: 'none', }); } // 跳转到指定位置 function seek(time: number) { if (audioContext) { audioContext.seek(time); currentTime.value = time; } } // 设置播放速度 function setPlayRate(rate: number) { playRate.value = rate; if (audioContext) { audioContext.playbackRate = rate; } } // 设置播放列表 function setPlaylist(list: AudioItem[], index: number = 0, autoPlay: boolean = true) { playlist.value = list; currentIndex.value = index; if (list.length > 0 && autoPlay) { play(list[index]); } } // 销毁音频上下文 function destroy() { if (audioContext) { audioContext.destroy(); audioContext = null; } } return { // 状态 voices, currentAudio, playlist, currentIndex, isPlaying, currentTime, duration, playRate, playMode, // 计算属性 hasPlaylist, hasNext, hasPrev, // 方法 fetchVoices, generateAudio, play, pause, resume, togglePlay, playPrev, playNext, handlePlayMode, togglePlayMode, seek, setPlayRate, setPlaylist, destroy, }; });