export const audioFils = { // 激光已校准: // "https://static.shelingxingqiu.com/attachment/2025-10-29/ddupaur1vdkyhzaqdc.mp3", 胜利: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcuo9yjp0kt5msvmvd.mp3", 失败: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcuo9yht2sdwhuqygy.mp3", 请射箭测试距离: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcuvj8avzqyw4hpq7t.mp3", 距离合格: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcutwrda0amn5kqr4j.mp3", 距离不足: "https://static.shelingxingqiu.com/attachment/2025-11-12/de6hr2faw28t0ianh0.mp3", 轮到你了: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcutzdrn4lxcpv8aqr.mp3", 第一轮: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcutyyr9a7m1vz2w13.mp3", 第二轮: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcutyyr9ldnfexjxtw.mp3", 第三轮: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcutyyr97m4ipxaze4.mp3", 第四轮: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcutyyr9x5addohlzf.mp3", 第五轮: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcutyyr9d7lw2gebpv.mp3", 决金箭轮: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcutwrd9zs4oi2kujv.mp3", 请蓝方射箭: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcutzdrxcbe5ll46as.mp3", 请红方射箭: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcutzdrl3re3dhlfjd.mp3", 中场休息: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcutwrd9zdk1xyolst.mp3", 比赛结束: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcutya59b6pu0ur4um.mp3", 比赛开始: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcuu5z3a3lumkutske.mp3", 请开始射击: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcutzdrl5u0iromqhf.mp3", 射击无效: "https://static.shelingxingqiu.com/attachment/2025-09-17/dcutya55ufiiw8oo55.mp3", 未上靶: "https://static.shelingxingqiu.com/attachment/2025-11-12/de6n45o3tsm1v4unam.mp3", "1环": "https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin1aq7gxjih5l.mp3", "2环": "https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin64tdgx2s4at.mp3", "3环": "https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxinlmf87vt8z65.mp3", "4环": "https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxinniv97sx0q9u.mp3", "5环": "https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin7j01kknpb7k.mp3", "6环": "https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin4syy1015rtq.mp3", "7环": "https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin3iz3dvmjdai.mp3", "8环": "https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxinnjd42lhpfiw.mp3", "9环": "https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxin69nj1xh7yfz.mp3", "10环": "https://static.shelingxingqiu.com/attachment/2025-09-17/dcutxinnvsx0tt7ksa.mp3", X环: "https://static.shelingxingqiu.com/attachment/2026-02-09/dga8puwekpe2gmtbu4.mp3", 向上调整: "https://static.shelingxingqiu.com/attachment/2025-11-12/de6ellf5pfvu3l8dhr.mp3", 向右上调整: "https://static.shelingxingqiu.com/attachment/2025-11-12/de6ellf45v88pirarr.mp3", 向右调整: "https://static.shelingxingqiu.com/attachment/2025-11-12/de6elleqnhrenggxsb.mp3", 向右下调整: "https://static.shelingxingqiu.com/attachment/2025-11-12/de6elleo6q16qctf6a.mp3", 向下调整: "https://static.shelingxingqiu.com/attachment/2025-11-12/de6ellek2mu2cri2n9.mp3", 向左下调整: "https://static.shelingxingqiu.com/attachment/2025-11-12/de6ellf25yu1pt2k5r.mp3", 向左调整: "https://static.shelingxingqiu.com/attachment/2025-11-12/de6ellen3zoalxcb06.mp3", 向左上调整: "https://static.shelingxingqiu.com/attachment/2025-11-12/de6ellf37a2iw6w4pu.mp3", 最后30秒: "https://static.shelingxingqiu.com/attachment/2025-11-13/de7kzzllq0futwynso.mp3", 练习开始: "https://static.shelingxingqiu.com/attachment/2025-11-14/de88w0lmmt43nnfmoi.mp3", }; // 版本控制日志函数 function debugLog(...args) { // 获取当前环境信息 const accountInfo = uni.getAccountInfoSync(); const envVersion = accountInfo.miniProgram.envVersion; // 只在体验版打印日志,正式版(release)和开发版(develop)不打印 if (envVersion === "trial") { console.log(...args); } } class AudioManager { constructor() { this.audioMap = new Map(); this.currentPlayingKey = null; this.maxRetries = 3; // 多轮统一重试:最多重试的轮次与每轮间隔 this.maxRetryRounds = 10; this.retryRoundIntervalMs = 1500; // 显式授权播放标记,防止 iOS 在设置 src 后误播 this.allowPlayMap = new Map(); // 串行加载相关属性 this.audioKeys = []; this.currentLoadingIndex = 0; this.isLoading = false; this.loadingPromise = null; // 连续播放队列相关属性 this.sequenceQueue = []; this.sequenceIndex = 0; this.isSequenceRunning = false; // 防重复播放保护 this.lastPlayKey = null; this.lastPlayAt = 0; // 静音开关 this.isMuted = false; this.pendingPlayKey = null; // 新增:就绪状态映射 this.readyMap = new Map(); // 新增:首轮失败的音频集合与重试阶段标识 this.failedLoadKeys = new Set(); // 加载代数,用于 reloadAll 时作废旧的加载循环 this.loadGeneration = 0; // 本地路径缓存 Map: { url: localPath } this.localFileCache = uni.getStorageSync("audio_local_files") || {}; // 启动时自动清理过期的缓存文件(URL 已不在 audioFils 中的文件) this.cleanObsoleteCache(); this.initAudios(); } // 清理不再使用的缓存文件 cleanObsoleteCache() { const activeUrls = new Set(Object.values(audioFils)); const cachedUrls = Object.keys(this.localFileCache); let hasChanges = false; for (const url of cachedUrls) { if (!activeUrls.has(url)) { debugLog(`发现废弃音频缓存,正在清理: ${url}`); const path = this.localFileCache[url]; // 移除物理文件 uni.removeSavedFile({ filePath: path, complete: () => { // 忽略移除结果,直接移除记录 }, }); // 移除记录 delete this.localFileCache[url]; hasChanges = true; } } if (hasChanges) { uni.setStorageSync("audio_local_files", this.localFileCache); debugLog("废弃缓存清理完成"); } } // 初始化音频(两阶段:首轮串行加载全部,次轮仅串行加载失败项一次) initAudios() { if (this.isLoading) { debugLog("音频正在加载中,跳过重复初始化"); return this.loadingPromise; } debugLog("开始串行加载音频..."); this.isLoading = true; this.audioKeys = Object.keys(audioFils); this.currentLoadingIndex = 0; this.failedLoadKeys.clear(); // 增加代数,使得旧的加载循环失效 this.loadGeneration = (this.loadGeneration || 0) + 1; const currentGen = this.loadGeneration; this.loadingPromise = new Promise((resolve) => { const finalize = () => { if (currentGen !== this.loadGeneration) return; const runRounds = (round) => { if (currentGen !== this.loadGeneration) return; // 达到最大轮次或没有失败项,收尾 if (this.failedLoadKeys.size === 0 || round > this.maxRetryRounds) { this.isLoading = false; resolve(); return; } const retryKeys = Array.from(this.failedLoadKeys); this.failedLoadKeys.clear(); debugLog(`开始第 ${round} 轮串行加载,共 ${retryKeys.length} 个`); this.loadKeysSequentially( retryKeys, () => { if (currentGen !== this.loadGeneration) return; // 如仍有失败项,继续下一轮;否则结束 if (this.failedLoadKeys.size > 0 && round < this.maxRetryRounds) { setTimeout( () => runRounds(round + 1), this.retryRoundIntervalMs ); } else { this.isLoading = false; resolve(); } }, currentGen ); }; // 启动第 1 轮重试(如有失败项) runRounds(1); }; this.loadNextAudio(finalize, currentGen); }); return this.loadingPromise; } // 按自定义列表串行加载音频(避免并发过多) loadKeysSequentially(keys, onComplete, gen) { if (gen !== undefined && gen !== this.loadGeneration) return; let idx = 0; const list = Array.from(keys); const next = () => { if (gen !== undefined && gen !== this.loadGeneration) return; if (idx >= list.length) { if (onComplete) onComplete(); return; } const k = list[idx++]; // 已就绪的音频不再重载,避免把 ready 状态重置为 false if (this.readyMap.get(k)) { setTimeout(next, 50); return; } // 未就绪:已存在则重载;不存在则创建 if (this.audioMap.has(k)) { this.retryLoadAudio(k); setTimeout(next, 100); } else { this.createAudio(k, () => { setTimeout(next, 100); }); return; // createAudio 内部会触发 next } }; next(); } // 串行加载下一个音频(首轮) loadNextAudio(onComplete, gen) { if (gen !== undefined && gen !== this.loadGeneration) return; if (this.currentLoadingIndex >= this.audioKeys.length) { debugLog("首轮加载遍历完成", this.currentLoadingIndex); if (onComplete) onComplete(); return; } const key = this.audioKeys[this.currentLoadingIndex]; debugLog( `开始加载音频 ${this.currentLoadingIndex + 1}/${ this.audioKeys.length }: ${key}` ); this.createAudio(key, () => { setTimeout(() => { this.loadNextAudio(onComplete, gen); }, 100); }); } // 创建单个音频实例(支持本地缓存) createAudio(key, callback) { this.currentLoadingIndex++; const src = audioFils[key]; const setupAudio = (realSrc) => { const audio = uni.createInnerAudioContext(); audio.autoplay = false; audio.src = realSrc; try { if (typeof audio.volume === "number") { audio.volume = this.isMuted ? 0 : 1; } else if (typeof audio.muted !== "undefined") { audio.muted = this.isMuted; } } catch (_) {} this.allowPlayMap.set(key, false); audio.onPlay(() => { if (!this.allowPlayMap.get(key)) { try { audio.stop(); } catch (_) {} } }); const loadTimeout = setTimeout(() => { debugLog(`音频 ${key} 加载超时`); this.recordLoadFailure(key); try { audio.destroy(); } catch (_) {} if (callback) callback(); }, 10000); audio.onCanplay(() => { if (!this.allowPlayMap.get(key)) { try { audio.pause(); } catch (_) {} } clearTimeout(loadTimeout); this.readyMap.set(key, true); this.failedLoadKeys.delete(key); // debugLog(`音频 ${key} 已加载完成`, this.getLoadProgress()); uni.$emit("audioLoaded", key); const loadedAudioKeys = uni.getStorageSync("loadedAudioKeys") || {}; loadedAudioKeys[key] = true; uni.setStorageSync("loadedAudioKeys", loadedAudioKeys); if (callback) callback(); }); audio.onError((res) => { clearTimeout(loadTimeout); debugLog(`音频 ${key} 加载失败:`, res.errMsg); // 如果是本地文件加载失败,可能是文件损坏,清除缓存以便下次重新下载 if (realSrc !== src && this.localFileCache[src] === realSrc) { debugLog(`本地缓存失效,移除记录: ${key}`); delete this.localFileCache[src]; uni.setStorageSync("audio_local_files", this.localFileCache); // 移除文件 uni.removeSavedFile({ filePath: realSrc }); } this.recordLoadFailure(key); this.audioMap.delete(key); audio.destroy(); if (this.readyMap.get(key)) { // 这里不要去除,不然检查进度的时候由于没有重新加载而进度卡住,等播放失败的时候会重新加载 // this.readyMap.set(key, false); } else { if (callback) callback(); } }); audio.onEnded(() => { if (this.currentPlayingKey === key) { this.currentPlayingKey = null; } this.allowPlayMap.set(key, false); this.onAudioEnded(key); }); audio.onStop(() => { if (this.currentPlayingKey === key) { this.currentPlayingKey = null; } this.allowPlayMap.set(key, false); }); this.audioMap.set(key, audio); }; // 检查是否有可用的本地缓存 this.checkLocalFile(src).then((localPath) => { if (localPath) { debugLog(`命中本地缓存: ${key}`); setupAudio(localPath); } else { // 下载并尝试保存 uni.downloadFile({ url: src, timeout: 20000, success: (res) => { if (res.tempFilePath) { // 尝试保存文件到本地存储(持久化) uni.getFileSystemManager().saveFile({ tempFilePath: res.tempFilePath, success: (saveRes) => { const savedPath = saveRes.savedFilePath; this.localFileCache[src] = savedPath; uni.setStorageSync("audio_local_files", this.localFileCache); debugLog(`音频已缓存到本地: ${key}`); setupAudio(savedPath); }, fail: (err) => { debugLog( `保存音频失败(可能空间不足),使用临时文件: ${key}`, err ); setupAudio(res.tempFilePath); }, }); } else { this.recordLoadFailure(key); if (callback) callback(); } }, fail: () => { this.recordLoadFailure(key); if (callback) callback(); }, }); } }); } // 检查本地文件是否有效 checkLocalFile(url) { return new Promise((resolve) => { const path = this.localFileCache[url]; if (!path) { resolve(null); return; } // 检查文件是否存在 uni.getFileSystemManager().getFileInfo({ filePath: path, success: () => { resolve(path); }, fail: () => { // 文件不存在,清理记录 delete this.localFileCache[url]; uni.setStorageSync("audio_local_files", this.localFileCache); resolve(null); }, }); }); } // 新增:记录失败(首轮与次轮都会用到) recordLoadFailure(key) { this.failedLoadKeys.add(key); } // 重新加载音频 retryLoadAudio(key) { const oldAudio = this.audioMap.get(key); if (oldAudio) oldAudio.destroy(); this.createAudio(key); } // 播放指定音频或音频数组(数组则按顺序连续播放) play(input, interrupt = true) { // 统一规范化为队列 let queue = []; if (Array.isArray(input)) { queue = input.filter((k) => !!audioFils[k]); } else if (typeof input === "string") { queue = !!audioFils[input] ? [input] : []; } else { debugLog("play 参数类型无效,仅支持字符串或字符串数组"); return; } if (queue.length === 0) { debugLog("连续播放队列为空或无效"); return; } if (interrupt) { // 立即打断并启动新的播放序列 this.stopAll(); this.isSequenceRunning = false; this.sequenceQueue = []; this.sequenceIndex = 0; this.sequenceQueue = queue; this.sequenceIndex = 0; this.isSequenceRunning = true; this._playSingle(queue[0], false); return; } // 不打断当前播放:把新的队列加入到序列中,等待当前播放结束后衔接 if (this.currentPlayingKey) { if (this.isSequenceRunning) { // 已有序列在跑:直接追加 this.sequenceQueue = this.sequenceQueue.concat(queue); } else { // 没有序列但当前有正在播放的:以当前为序列的起点 this.isSequenceRunning = true; this.sequenceQueue = [this.currentPlayingKey].concat(queue); this.sequenceIndex = 0; // 不触发 _playSingle,等待当前音频自然结束后由 onAudioEnded 接管 } } else { // 当前没有播放:直接启动新的序列 this.sequenceQueue = queue; this.sequenceIndex = 0; this.isSequenceRunning = true; this._playSingle(queue[0], false); } } // 内部方法:播放单个 key _playSingle(key, forceStopAll = false) { // 200ms 内的同 key 重复播放直接忽略,避免“比比赛开始”这类重复首音 const now = Date.now(); if (this.lastPlayKey === key && now - this.lastPlayAt < 250) { debugLog(`忽略快速重复播放: ${key}`); return; } if (forceStopAll) { this.stopAll(); } else if (this.currentPlayingKey && this.currentPlayingKey !== key) { this.stop(this.currentPlayingKey); } else if (this.currentPlayingKey === key) { // 同一音频正在播放:不重启,避免听到重复开头 return; } const audio = this.audioMap.get(key); if (audio) { // 播放前确保遵循当前静音状态 try { if (typeof audio.volume === "number") { audio.volume = this.isMuted ? 0 : 1; } else if (typeof audio.muted !== "undefined") { audio.muted = this.isMuted; } } catch (_) {} // 同一音频:避免 stop() 触发 onStop 清除授权,使用 pause()+seek(0) try { audio.pause(); } catch (_) {} try { if (typeof audio.seek === "function") { audio.seek(0); } else { audio.startTime = 0; } } catch (_) { audio.startTime = 0; } // 显式授权播放并立即播放 this.allowPlayMap.set(key, true); audio.play(); this.currentPlayingKey = key; this.lastPlayKey = key; this.lastPlayAt = Date.now(); } else { debugLog(`音频 ${key} 不存在,尝试重新加载...`); this.retryLoadAudio(key); const handler = (loadedKey) => { if (loadedKey === key) { try { uni.$off("audioLoaded", handler); } catch (_) {} // 再次校验是否存在且就绪 const a = this.audioMap.get(key); if (a && this.readyMap.get(key)) { this._playSingle(key, false); } } }; try { uni.$on("audioLoaded", handler); } catch (_) {} } } // 连续播放:在某个音频结束后,若处于队列播放状态则继续下一个 onAudioEnded(key) { if (!this.isSequenceRunning) return; const currentKey = this.sequenceQueue[this.sequenceIndex]; if (currentKey !== key) return; const nextIndex = this.sequenceIndex + 1; if (nextIndex < this.sequenceQueue.length) { this.sequenceIndex = nextIndex; const nextKey = this.sequenceQueue[nextIndex]; this._playSingle(nextKey, false); } else { // 队列播放完成 this.isSequenceRunning = false; this.sequenceQueue = []; this.sequenceIndex = 0; } } // 停止指定音频 stop(key) { const audio = this.audioMap.get(key); if (audio) { audio.stop(); this.allowPlayMap.set(key, false); if (this.currentPlayingKey === key) { this.currentPlayingKey = null; } } } // 停止所有音频 stopAll() { for (const [k, audio] of this.audioMap.entries()) { try { audio.stop(); } catch (_) {} this.allowPlayMap.set(k, false); } this.currentPlayingKey = null; } // 设置静音开关:true 静音,false 取消静音 setMuted(muted) { this.isMuted = !!muted; for (const audio of this.audioMap.values()) { try { if (typeof audio.volume === "number") { audio.volume = this.isMuted ? 0 : 1; } else if (typeof audio.muted !== "undefined") { audio.muted = this.isMuted; } } catch (_) {} } debugLog(`静音状态已设置为: ${this.isMuted}`); } // 新增:返回音频加载进度(0~1) getLoadProgress() { const keys = Object.keys(audioFils); const total = keys.length; if (total === 0) return 0; let loaded = 0; for (const k of keys) { if (this.readyMap.get(k)) loaded++; } return Number((loaded / total).toFixed(2)); } // 清理本地音频缓存文件 clearCache() { debugLog("开始清理本地音频缓存..."); const cache = uni.getStorageSync("audio_local_files") || {}; const paths = Object.values(cache); for (const path of paths) { uni.removeSavedFile({ filePath: path, complete: (res) => { // 无论成功失败都继续 }, }); } uni.removeStorageSync("audio_local_files"); this.localFileCache = {}; debugLog("本地音频缓存清理完成"); } // 手动重置并重新加载所有音频(用于卡住时恢复) reloadAll() { debugLog("执行 reloadAll: 重置所有状态并重新加载"); // 1. 停止所有播放 this.stopAll(); // 2. 销毁现有音频实例 for (const audio of this.audioMap.values()) { try { audio.destroy(); } catch (_) {} } this.audioMap.clear(); // 3. 重置状态 this.readyMap.clear(); this.failedLoadKeys.clear(); this.allowPlayMap.clear(); this.currentPlayingKey = null; this.sequenceQueue = []; this.sequenceIndex = 0; this.isSequenceRunning = false; // 清理一下可能损坏的缓存(可选,如果用户因为缓存坏了卡住,这一步很有用) // 这里选择不自动全清,而是依赖 onError 里的单点清除。如果需要彻底重置,可取消注释: // this.clearCache(); // 4. 强制重置加载锁 this.isLoading = false; this.loadingPromise = null; this.currentLoadingIndex = 0; // 5. 重新初始化 (initAudios 会自增 loadGeneration,从而终止之前的任何异步循环) return this.initAudios(); } } // 导出单例 export default new AudioManager();