first commit

This commit is contained in:
zzc
2026-04-30 00:13:37 +08:00
commit 493b4709bc
189 changed files with 24791 additions and 0 deletions

View File

@@ -0,0 +1,506 @@
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);
}
}
}