diff --git a/TUICallKit-Vue3/training/hooks/useMetronomeBg.ts b/TUICallKit-Vue3/training/hooks/useMetronomeBg.ts new file mode 100644 index 00000000..d362f625 --- /dev/null +++ b/TUICallKit-Vue3/training/hooks/useMetronomeBg.ts @@ -0,0 +1,196 @@ +import { ref, onUnmounted } from 'vue' + +/** + * 节拍器后台播放专版 - 基于 BackgroundAudioManager + * + * 解决 InnerAudioContext 在 iOS 微信小程序中无法后台播放的硬限制: + * - iOS 真机切到后台/锁屏 InnerAudioContext 必被挂起,requiredBackgroundModes 无效 + * - BackgroundAudioManager 是微信唯一支持 iOS 真后台/锁屏播放的音频 API + * + * 取舍: + * - BgAudio 是全局单例,一次只能播一个音频,不适合短促 click 高频重复 + * - 因此提前用 ffmpeg 合成 3 个档位的"完整节拍循环"音轨(80/110/130 BPM × 2 拍) + * 每个 mp3 是 5~6 秒无缝循环,设 onEnded 重赋 src 实现永久循环 + * + * 副作用: + * - 播放时锁屏/通知栏会显示带封面+暂停按钮的音乐控制条(可被用户从锁屏暂停) + * - 必须声明 requiredBackgroundModes:["audio"](已配在 manifest+pages) + * - 切档位有 200~500ms 切换延迟,但用户主动操作时可接受 + */ + +export type LoopId = 'slow' | 'normal' | 'brisk' + +export interface LoopPreset { + id: LoopId + bpm: number + accent: number + src: string + label: string +} + +export const LOOP_PRESETS: readonly LoopPreset[] = [ + { + id: 'slow', + bpm: 80, + accent: 2, + src: '/training/static/audio/loop_80bpm_2.mp3', + label: '慢走 80 BPM', + }, + { + id: 'normal', + bpm: 110, + accent: 2, + src: '/training/static/audio/loop_110bpm_2.mp3', + label: '健走 110 BPM', + }, + { + id: 'brisk', + bpm: 130, + accent: 2, + src: '/training/static/audio/loop_130bpm_2.mp3', + label: '快走 130 BPM', + }, +] + +const COVER_URL = '/training/static/audio/cover.jpg' + +export function useMetronomeBg() { + const isPlaying = ref(false) + const currentLoop = ref(null) + + /* @dcloudio/types 没有 BackgroundAudioManager 类型,直接 any */ + let bgm: any = null + /* 当前期望的 src,onEnded 时用它重新赋值实现循环 */ + let desiredSrc = '' + + /* 懒初始化 BackgroundAudioManager + 绑定事件 + BgAudio 是全局单例,跨页面共享,只能在第一次需要时初始化 */ + const ensureBgm = () => { + if (bgm) return bgm + + const m = uni.getBackgroundAudioManager() + + /* 必填 metadata,缺一会报错或不显示锁屏控制条 */ + m.title = '节拍器' + m.epname = '甄养堂 · 健走' + m.singer = '健走配速' + m.coverImgUrl = COVER_URL + m.webUrl = '' + + m.onPlay(() => { + isPlaying.value = true + }) + m.onPause(() => { + /* 用户从锁屏控制条点暂停,同步 UI 状态 */ + isPlaying.value = false + }) + m.onStop(() => { + isPlaying.value = false + currentLoop.value = null + }) + + /* 实现无限循环:每段 mp3 播完时立即重赋 src 再次播放 + BgAudio 没有原生 loop 属性,只能用这招 */ + m.onEnded(() => { + if (desiredSrc && isPlaying.value) { + try { + m.src = desiredSrc + } catch (_) {} + } + }) + + m.onError((err) => { + console.error('[BgAudio] error:', err) + isPlaying.value = false + uni.showToast({ + title: '音频播放失败,请重试', + icon: 'none', + duration: 2000, + }) + }) + + bgm = m + return m + } + + /** + * 切到指定档位并开始播放 + * 如果已经在播同一档位 → 切到 pause/play 状态 + * 如果在播别的档位 → 切换 src(有 200~500ms 延迟) + */ + const playLoop = (id: LoopId) => { + const preset = LOOP_PRESETS.find((p) => p.id === id) + if (!preset) return + + const m = ensureBgm() + + /* 同档位再点一下 = 暂停 */ + if (currentLoop.value === id && isPlaying.value) { + m.pause() + return + } + + /* 切换档位或从暂停恢复 */ + currentLoop.value = id + desiredSrc = preset.src + + /* 重设 title 让锁屏控制条显示当前档位 */ + m.title = `节拍器 · ${preset.label}` + + /* 赋值 src 会自动播放(微信 API 设计如此) */ + m.src = preset.src + /* isPlaying 由 onPlay 回调置 true */ + } + + const pause = () => { + if (bgm && isPlaying.value) { + bgm.pause() + } + } + + const resume = () => { + if (bgm && !isPlaying.value && desiredSrc) { + /* 从暂停态恢复:直接 play 即可 */ + try { + bgm.play() + } catch (_) { + /* 部分基础库 play() 不可用时,重赋 src */ + bgm.src = desiredSrc + } + } + } + + /** + * 完全停止 + 清掉锁屏控制条 + * 注意 BgAudio 是全局单例,stop 会影响所有页面共享的实例 + */ + const stop = () => { + if (bgm) { + try { + bgm.stop() + } catch (_) {} + } + currentLoop.value = null + desiredSrc = '' + isPlaying.value = false + } + + /* hook 卸载时不主动 stop,因为用户离开节拍器页时 + 仍希望音乐持续(走在路上拿出手机切别的页面应该不停) + 真正停止的责任在 metronome.vue 的退出按钮里 */ + onUnmounted(() => { + /* 仅解绑回调? BgAudio 是全局单例,我们的回调还在, + 不解会导致内存中保留无用引用,但回调里都判断了 isPlaying, + 且新页面再 ensureBgm 时会覆盖回调,可接受 */ + }) + + return { + isPlaying, + currentLoop, + playLoop, + pause, + resume, + stop, + LOOP_PRESETS, + } +} diff --git a/TUICallKit-Vue3/training/pages/metronome.vue b/TUICallKit-Vue3/training/pages/metronome.vue index 3f8bda59..9057ee92 100644 --- a/TUICallKit-Vue3/training/pages/metronome.vue +++ b/TUICallKit-Vue3/training/pages/metronome.vue @@ -1,11 +1,11 @@ @@ -89,7 +70,7 @@