Files
dino/assets/seripts/libs/SpeedAudioPlayer.ts
2026-04-30 00:13:37 +08:00

507 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { _decorator, AudioClip, sys } from "cc";
const { ccclass } = _decorator;
// 声明微信小游戏全局变量(避免 TypeScript 报错)
declare const wx: any;
@ccclass("SpeedAudioPlayer")
export class SpeedAudioPlayer {
// H5 平台核心对象
private audioElement: HTMLAudioElement | null = null;
// 微信小游戏平台核心对象
private innerAudioContext: any = null;
// 跨平台统一播放状态
private currentPlayRate: number = 1.0;
private volume: number = 1.0;
private isPlaying: boolean = false;
private isPaused: boolean = false;
// 跨平台统一状态缓存(切换音频/恢复播放复用)
private lastPlayState: { rate: number; position: number } = {
rate: 1.0,
position: 0,
};
// 缓存当前音频剪辑
private currentAudioClip: AudioClip | null = null;
/**
* 初始化音频(跨平台自动适配)
* @param audioClip Cocos 音频剪辑
*/
async init(audioClip: AudioClip) {
if (!audioClip) {
console.error("SpeedAudioPlayer: 音频剪辑不能为空");
return;
}
// 保存当前音频剪辑
this.currentAudioClip = audioClip;
// 1. H5 平台(浏览器/支持 DOM 的环境)
if (this.isH5Platform()) {
await this.initH5(audioClip);
}
// 2. 微信小游戏平台(原生 API 环境)
else if (this.isWxGamePlatform()) {
await this.initWxGame(audioClip);
}
// 3. 不支持的平台
else {
console.error("SpeedAudioPlayer: 暂不支持当前运行平台");
}
}
/**
* 跨平台核心:切换音频文件(保留/重置状态)
* @param newAudioClip 新的音频剪辑
* @param keepState 是否保留原播放状态(速率/暂停位置默认true
*/
async changeAudioClip(newAudioClip: AudioClip, keepState: boolean = true) {
if (!newAudioClip) {
console.error("SpeedAudioPlayer: 切换音频失败,音频剪辑不能为空");
return;
}
// 1. 记录当前状态(跨平台统一)
if (keepState) {
this.lastPlayState.rate = this.currentPlayRate;
this.lastPlayState.position = this.getCurrentPlayTime();
} else {
this.lastPlayState = { rate: 1.0, position: 0 };
}
// 2. 停止当前音频(跨平台统一)
this.stop();
// 3. 初始化新音频(自动适配平台)
await this.init(newAudioClip);
// 4. 恢复播放状态(跨平台统一)
if (keepState) {
this.currentPlayRate = this.lastPlayState.rate;
this.setPlayRate(this.currentPlayRate); // 同步速率到当前平台
// 定位到缓存位置,根据原状态恢复播放/暂停
if (this.isPaused) {
this.seek(this.lastPlayState.position);
console.log(
`SpeedAudioPlayer: 切换音频后保留暂停状态,位置:${this.lastPlayState.position.toFixed(
3,
)}`,
);
} else if (this.isPlaying) {
this.play(this.currentPlayRate, this.lastPlayState.position);
}
}
console.log("SpeedAudioPlayer: 音频文件切换成功");
}
/**
* 播放音频(跨平台统一接口,支持指定速率/起始位置)
* @param playRate 播放速率0.5~2.0
* @param startAt 起始位置默认0
*/
play(playRate: number = 1.0, startAt: number = 0) {
// 限制参数范围
this.currentPlayRate = Math.max(0.5, Math.min(playRate, 2.0));
startAt = Math.max(0, Math.min(startAt, this.getTotalDuration()));
// H5 平台
if (this.isH5Platform() && this.audioElement) {
this.audioElement.playbackRate = this.currentPlayRate;
this.audioElement.currentTime = startAt;
this.audioElement
.play()
.then(() => {
this.updatePlayState(true, false);
console.log(
`SpeedAudioPlayer: H5 播放开始,速率:${
this.currentPlayRate
},起始位置:${startAt.toFixed(3)}`,
);
})
.catch((e) => {
console.error("SpeedAudioPlayer: H5 播放失败(需用户交互触发):", e);
});
}
// 微信小游戏平台
else if (this.isWxGamePlatform() && this.innerAudioContext) {
this.innerAudioContext.playbackRate = this.currentPlayRate;
this.innerAudioContext.seek(startAt);
this.innerAudioContext.play();
this.updatePlayState(true, false);
this.lastPlayState.position = startAt;
console.log(
`SpeedAudioPlayer: 微信小游戏播放开始,速率:${
this.currentPlayRate
},起始位置:${startAt.toFixed(3)}`,
);
}
// 未初始化
else {
console.warn("SpeedAudioPlayer: 音频未初始化完成,无法播放");
}
}
/**
* 暂停播放(跨平台统一接口,精准保留当前位置)
*/
pause() {
if (!this.isPlaying || this.isPaused) {
console.warn("SpeedAudioPlayer: 音频未播放或已暂停,无法暂停");
return;
}
// H5 平台
if (this.isH5Platform() && this.audioElement) {
this.audioElement.pause();
const pausePos = this.audioElement.currentTime;
this.cachePausePosition(pausePos);
this.updatePlayState(false, true);
}
// 微信小游戏平台
else if (this.isWxGamePlatform() && this.innerAudioContext) {
this.innerAudioContext.pause();
const pausePos = this.innerAudioContext.currentTime;
this.cachePausePosition(pausePos);
this.updatePlayState(false, true);
}
}
/**
* 恢复播放(跨平台统一接口,从暂停位置继续)
*/
resume() {
if (!this.isPaused) {
console.warn("SpeedAudioPlayer: 音频未暂停,无法恢复播放");
return;
}
const resumePos = this.lastPlayState.position;
// H5 平台
if (this.isH5Platform() && this.audioElement) {
this.audioElement.currentTime = resumePos;
this.audioElement
.play()
.then(() => {
this.updatePlayState(true, false);
console.log(
`SpeedAudioPlayer: H5 恢复播放,从${resumePos.toFixed(3)}秒开始`,
);
})
.catch((e) => {
console.error("SpeedAudioPlayer: H5 恢复播放失败:", e);
});
}
// 微信小游戏平台
else if (this.isWxGamePlatform() && this.innerAudioContext) {
// 恢复播放前确保应用当前设置的速率
this.innerAudioContext.playbackRate = this.currentPlayRate;
this.innerAudioContext.seek(resumePos);
this.innerAudioContext.play();
this.updatePlayState(true, false);
console.log(
`SpeedAudioPlayer: 微信小游戏恢复播放,速率:${
this.currentPlayRate
},从${resumePos.toFixed(3)}秒开始`,
);
}
}
/**
* 停止播放(跨平台统一接口,重置位置到开头)
*/
stop() {
// H5 平台
if (this.isH5Platform() && this.audioElement) {
this.audioElement.pause();
this.audioElement.currentTime = 0;
}
// 微信小游戏平台
else if (this.isWxGamePlatform() && this.innerAudioContext) {
this.innerAudioContext.pause();
this.innerAudioContext.seek(0);
}
// 跨平台统一重置状态
this.updatePlayState(false, false);
this.lastPlayState.position = 0;
console.log("SpeedAudioPlayer: 停止播放,状态已重置");
}
/**
* 动态调节播放速率(跨平台统一接口,播放中实时生效)
* @param playRate 新速率
*/
setPlayRate(playRate: number) {
this.currentPlayRate = Math.max(0.5, Math.min(playRate, 2.0));
this.lastPlayState.rate = this.currentPlayRate;
// H5 平台
if (this.isH5Platform() && this.audioElement) {
this.audioElement.playbackRate = this.currentPlayRate;
}
// 微信小游戏平台
else if (this.isWxGamePlatform() && this.innerAudioContext) {
this.innerAudioContext.playbackRate = this.currentPlayRate;
// 微信小游戏真机环境下设置playbackRate后需要重启播放才能生效
// 保存当前播放位置,暂停后重新播放
if (this.isPlaying) {
const currentPosition = this.getCurrentPlayTime();
this.innerAudioContext.pause();
this.innerAudioContext.seek(currentPosition);
this.innerAudioContext.play();
console.log(
`SpeedAudioPlayer: 微信小游戏重启播放以应用新速率,当前位置:${currentPosition.toFixed(
3,
)}`,
);
}
}
console.log(`SpeedAudioPlayer: 速率已调整为:${this.currentPlayRate}`);
}
/**
* 设置音量跨平台统一接口0~1
* @param volume 音量值
*/
setVolume(volume: number) {
this.volume = Math.max(0, Math.min(volume, 1));
// H5 平台
if (this.isH5Platform() && this.audioElement) {
this.audioElement.volume = this.volume;
}
// 微信小游戏平台
else if (this.isWxGamePlatform() && this.innerAudioContext) {
this.innerAudioContext.volume = this.volume;
}
console.log(`SpeedAudioPlayer: 音量已设置为:${this.volume}`);
}
/**
* 获取音频总时长(跨平台统一接口)
*/
getTotalDuration(): number {
// H5 平台
if (this.isH5Platform() && this.audioElement) {
return this.audioElement.duration || 0;
}
// 微信小游戏平台
else if (this.isWxGamePlatform() && this.innerAudioContext) {
return this.innerAudioContext.duration || 0;
}
return 0;
}
/**
* 获取当前播放位置(跨平台统一接口,秒)
*/
getCurrentPlayTime(): number {
// H5 平台
if (this.isH5Platform() && this.audioElement) {
return this.audioElement.currentTime || 0;
}
// 微信小游戏平台
else if (this.isWxGamePlatform() && this.innerAudioContext) {
return this.innerAudioContext.currentTime || 0;
}
return 0;
}
/**
* 获取当前播放状态(跨平台统一接口)
*/
getPlayState() {
return {
isPlaying: this.isPlaying,
isPaused: this.isPaused,
currentPlayRate: this.currentPlayRate,
currentPlayTime: this.getCurrentPlayTime(),
totalDuration: this.getTotalDuration(),
volume: this.volume,
platform: this.getCurrentPlatform(),
};
}
/**
* 销毁资源(跨平台统一接口,释放内存)
*/
destroy() {
// H5 平台
if (this.isH5Platform() && this.audioElement) {
this.stop();
this.audioElement.remove();
this.audioElement = null;
}
// 微信小游戏平台
else if (this.isWxGamePlatform() && this.innerAudioContext) {
this.stop();
this.innerAudioContext.offCanplay();
this.innerAudioContext.offEnded();
this.innerAudioContext.offError();
this.innerAudioContext.destroy();
this.innerAudioContext = null;
}
this.currentAudioClip = null;
console.log(`SpeedAudioPlayer: ${this.getCurrentPlatform()} 资源已销毁`);
}
// ---------------------- 内部辅助方法(跨平台适配核心)----------------------
/**
* 初始化 H5 平台音频HTML5 Audio
*/
private async initH5(audioClip: AudioClip) {
// 首次创建 Audio 元素
if (!this.audioElement) {
this.audioElement = document.createElement("audio");
this.audioElement.preload = "auto";
// 监听播放结束
this.audioElement.onended = () => {
this.updatePlayState(false, false);
this.lastPlayState.position = 0;
console.log("SpeedAudioPlayer: H5 音频播放结束");
};
}
// 设置音频源并等待加载
this.audioElement.src = audioClip.nativeUrl;
await new Promise((resolve, reject) => {
const onCanplayCallback = () => {
this.audioElement!.removeEventListener(
"canplaythrough",
onCanplayCallback,
);
resolve(true);
};
this.audioElement!.addEventListener("canplaythrough", onCanplayCallback);
this.audioElement!.addEventListener("error", (e) => {
reject(new Error(`H5 音频加载失败:${e}`));
});
});
// 恢复缓存状态
this.audioElement.volume = this.volume;
this.audioElement.playbackRate = this.currentPlayRate;
console.log(
"SpeedAudioPlayer: H5 音频初始化成功,总时长:",
this.getTotalDuration().toFixed(2),
"秒",
);
}
/**
* 初始化 微信小游戏 平台音频wx.createInnerAudioContext
*/
private async initWxGame(audioClip: AudioClip) {
// 首次创建微信原生音频上下文
if (!this.innerAudioContext) {
this.innerAudioContext = wx.createInnerAudioContext();
this.innerAudioContext.volume = this.volume;
this.innerAudioContext.playbackRate = this.currentPlayRate;
this.innerAudioContext.loop = false;
// 监听原生事件
this.innerAudioContext.onCanplay(() => {
console.log(
"SpeedAudioPlayer: 微信小游戏音频加载完成,总时长:",
this.getTotalDuration().toFixed(2),
"秒",
);
});
this.innerAudioContext.onEnded(() => {
this.updatePlayState(false, false);
this.lastPlayState.position = 0;
console.log("SpeedAudioPlayer: 微信小游戏音频播放结束");
});
this.innerAudioContext.onError((err: any) => {
console.error("SpeedAudioPlayer: 微信小游戏音频发生错误:", err);
this.updatePlayState(false, false);
});
}
// 设置音频源并等待加载(核心修复:移除 onCanplayOnce改用 onCanplay + 手动解除监听)
this.innerAudioContext.src = audioClip.nativeUrl;
await new Promise((resolve) => {
// 定义一次性监听函数
const onCanplayCallback = () => {
// 手动移除监听,实现「一次性触发」的效果,避免重复回调
this.innerAudioContext.offCanplay(onCanplayCallback);
resolve(true);
};
// 先判断音频是否已加载完成
if (this.innerAudioContext.readyState === "canplay") {
resolve(true);
} else {
// 绑定普通 onCanplay 监听,触发后手动移除,替代不存在的 onCanplayOnce
this.innerAudioContext.onCanplay(onCanplayCallback);
}
});
// 恢复缓存状态
this.innerAudioContext.volume = this.volume;
this.innerAudioContext.playbackRate = this.currentPlayRate;
}
/**
* 判断是否为 H5 平台(浏览器/支持 DOM 的环境)
*/
private isH5Platform(): boolean {
return sys.isBrowser && sys.platform !== sys.Platform.WECHAT_GAME;
}
/**
* 判断是否为 微信小游戏 平台
*/
private isWxGamePlatform(): boolean {
return sys.platform === sys.Platform.WECHAT_GAME;
}
/**
* 获取当前平台名称(用于日志/调试)
*/
private getCurrentPlatform(): string {
if (this.isH5Platform()) return "H5";
if (this.isWxGamePlatform()) return "微信小游戏";
return "未知平台";
}
/**
* 跨平台统一更新播放状态
*/
private updatePlayState(isPlaying: boolean, isPaused: boolean) {
this.isPlaying = isPlaying;
this.isPaused = isPaused;
}
/**
* 跨平台统一缓存暂停位置
*/
private cachePausePosition(position: number) {
const clampPos = Math.max(0, Math.min(position, this.getTotalDuration()));
this.lastPlayState.position = clampPos;
console.log(
`SpeedAudioPlayer: 暂停成功,当前位置:${clampPos.toFixed(3)}`,
);
}
/**
* 跨平台统一定位音频位置
*/
private seek(position: number) {
const clampPos = Math.max(0, Math.min(position, this.getTotalDuration()));
if (this.isH5Platform() && this.audioElement) {
this.audioElement.currentTime = clampPos;
} else if (this.isWxGamePlatform() && this.innerAudioContext) {
this.innerAudioContext.seek(clampPos);
}
}
}