1
This commit is contained in:
@@ -1,213 +0,0 @@
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 重要:微信 BackgroundAudioManager.src 只接受 http/https 网络流,不能是包内资源
|
||||
* 所以必须把音频上传到 CDN(腾讯云 COS),并在小程序后台加 downloadFile 合法域名
|
||||
*
|
||||
* 当前线上文件(2026-05-26 上传到 cos.ap-guangzhou):
|
||||
* - loop_80bpm_2.mp3 循环音轨 慢走档
|
||||
* - loop_110bpm_2.mp3 循环音轨 健走档
|
||||
* - loop_130bpm_2.mp3 循环音轨 快走档
|
||||
*
|
||||
* 历史源文件(本地 training/static/audio/*.mp3) 已删除以减小包体积,
|
||||
* 如果以后要重新合成,先从 COS 下回来再用 ffmpeg 处理
|
||||
*/
|
||||
export const LOOP_PRESETS: readonly LoopPreset[] = [
|
||||
{
|
||||
id: 'slow',
|
||||
bpm: 80,
|
||||
accent: 2,
|
||||
src: 'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/file/20260526/20260526105129dcbc50112.mp3',
|
||||
label: '慢走 80 BPM',
|
||||
},
|
||||
{
|
||||
id: 'normal',
|
||||
bpm: 110,
|
||||
accent: 2,
|
||||
src: 'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/file/20260526/202605261051284f5b04928.mp3',
|
||||
label: '健走 110 BPM',
|
||||
},
|
||||
{
|
||||
id: 'brisk',
|
||||
bpm: 130,
|
||||
accent: 2,
|
||||
src: 'https://gz-1349751149.cos.ap-guangzhou.myqcloud.com/uploads/file/20260526/20260526105128164d77281.mp3',
|
||||
label: '快走 130 BPM',
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* 锁屏控制条封面
|
||||
* 暂时留空,微信会显示默认音乐图标,等以后有正式品牌图再上传到 COS 后填入这里
|
||||
* 注意:URL 必须是 https,且域名要加进小程序后台 downloadFile 合法域名
|
||||
*/
|
||||
const COVER_URL = ''
|
||||
|
||||
export function useMetronomeBg() {
|
||||
const isPlaying = ref<boolean>(false)
|
||||
const currentLoop = ref<LoopId | null>(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 = '健走配速'
|
||||
if (COVER_URL) 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user