目前2.x引擎不支持播放变速音频的播放(目前不知道3.x可能支持不)
1.web端
let audioClip = await $app.load.loadAudio(path)//这是个封装的异步加载音频资源的函数,获取音频资源
this._misucAudio = cc.audioEngine.playMusic(audioClip, loop)
//@ts-ignore
this._audioEs = cc.audioEngine._id2audio[this._misucAudio]._element._currentSource as AudioBufferSourceNode
this._audioEs.playbackRate.setValueAtTime(speed, this._audioEs.context.currentTime)
类似这样就能搞定,如果声音变调可以使用另外一种播放方式,论坛其他地方有贴
2.微信小游戏
let audioClip = await $app.load.loadAudio(path)
let audio = this._audioEs as WechatMinigame.InnerAudioContext
if (!audio) {
//{useWebAudioImplement: true }需要加上这个,不然ios端可能播放不出来
audio = this._audioEs = wx.createInnerAudioContext({ useWebAudioImplement: true })
}
audio.src = audio.nativeUrl //音源
audio.volume = this.getMusicVolume()//播放的音量
audio.loop = loop//是否循环
audio.playbackRate = speed//播放速度 0.5-2之间
audio.onEnded(() => {
callback?.()//播放回调
audio.onEnded((err) => { })//只执行一次
})
audio.play()
this._misucAudio = id
3.android端
android使用的是media3这个库
implementation ‘androidx.media3:media3-common:1.1.1’
implementation ‘androidx.media3:media3-exoplayer:1.1.1’
以下是部分代码
class Audio {
long lastPosition = 0;
ExoPlayer player = null;
boolean isSetStop = false;//是否设置了stop 如果是调用resume将无效
String url = "";//当前播放的资源地址
float speed = 1.0f;
boolean loop = false;
float volume = 1.0f;
String funcName = "";//回调函数名字
int lv = 1; //播放等级
//直接获取文件名字
public String getFileName() {
if (!url.isEmpty()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Path p = Paths.get(url);
Log.i("AudioMgr", "文件名字 " + p.getFileName().toString());
return p.getFileName().toString();
}
}
return "";
}
}
public class AudioMgr {
static private final String Tag = "AudioMgr";
static private final Audio bgmAudio = new Audio();
static private final ArrayList<Audio> effectAudios = new ArrayList<>(); // 音效播放器
static private final int sampleEffectCount = 10;//effect List<audio>中最多存储个数
static private AppActivity getContext() {
return AppActivity.getInstance();
}
// static {
// System.loadLibrary("cocos2djs");
// }
public native byte[] getFileContentAsByteArray(String filePath);
static private AudioMgr _instance = null;
static public AudioMgr getInstance() {
if (_instance == null) {
_instance = new AudioMgr();
}
return _instance;
}
// 初始化背景音乐播放器
static private void initialize() {
if (bgmAudio.player == null) {
bgmAudio.player = new ExoPlayer.Builder(getContext()).build();
bgmAudio.player.setAudioAttributes(
new AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MUSIC).build(), false);
bgmAudio.player.addListener(new Player.Listener() {
@Override
public void onPlaybackStateChanged(int playbackState) {
if (playbackState == Player.STATE_ENDED) {
if (!bgmAudio.funcName.isEmpty()) {
getContext().runOnGLThread(new Runnable() {
@Override
public void run() {
Log.i(Tag, "开始执行回调函数" + bgmAudio.funcName);
Cocos2dxJavascriptJavaBridge.evalString("console." + bgmAudio.funcName + "()");
bgmAudio.funcName = "";
}
});
}
}
// bgmAudio.state = playbackState;
}
@Override
public void onPlayerError(PlaybackException err) {
Log.e(Tag, "背景bgm play 错误 :" + err.getMessage());
}
});
}
}
// 播放背景音乐
static public void playBGM(String filePath, String funcName, float volume, boolean loop, float playbackRate) {
getContext().runOnUiThread(new Runnable() {
@Override
public void run() {
initialize();
if (bgmAudio.player != null) {
bgmAudio.url = filePath;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
bgmAudio.url = Paths.get(filePath).toString();
}
bgmAudio.speed = playbackRate;
bgmAudio.loop = loop;
bgmAudio.funcName = funcName;
bgmAudio.volume = volume;
bgmAudio.isSetStop = false;
bgmAudio.lastPosition = 0;
// bgmAudio.state = Player.STATE_IDLE;
_preparePlayerBgm();
}
}
});
}
private static void _preparePlayerBgm() {
getContext().runOnUiThread(new Runnable() {
@Override
public void run() {
if (bgmAudio.player != null && !bgmAudio.url.isEmpty()) {
try {
File cacheFile = new File(getContext().getCacheDir(), "audio/" + bgmAudio.getFileName());
Log.i(Tag, "准备bgm播放");
if (!cacheFile.exists()) {
cacheFile.getParentFile().mkdirs();
AssetManager assetManager = getContext().getAssets();
InputStream inputStream = null;
if (bgmAudio.url.contains("/com.")) {//走热更目录拿资源
byte[] bytes = getInstance().getFileContentAsByteArray(bgmAudio.url);
inputStream = new ByteArrayInputStream(bytes);
} else {
inputStream = assetManager.open(bgmAudio.url);
}
_copyInputStreamToFile(inputStream, cacheFile);
}
MediaItem mediaItem = MediaItem.fromUri(Uri.fromFile(cacheFile));
bgmAudio.player.setMediaItem(mediaItem);
bgmAudio.player.setVolume(bgmAudio.volume);
PlaybackParameters params = new PlaybackParameters(bgmAudio.speed, 1.0f);
bgmAudio.player.setPlaybackParameters(params);
bgmAudio.player.setRepeatMode(bgmAudio.loop ? ExoPlayer.REPEAT_MODE_ALL : ExoPlayer.REPEAT_MODE_OFF);
bgmAudio.player.seekTo(bgmAudio.lastPosition);
bgmAudio.player.prepare();
bgmAudio.player.setPlayWhenReady(true);
} catch (IOException e) {
Log.e(Tag, e.getMessage());
}
Log.i(Tag, "Playback Parameters: " + bgmAudio.player.getPlaybackParameters().speed);
}
}
});
}
static private void _copyInputStreamToFile(InputStream in, File file) throws IOException {
try (InputStream is = in; java.io.FileOutputStream fos = new java.io.FileOutputStream(file)) {
byte[] buffer = new byte[1024];
int read;
while ((read = is.read(buffer)) != -1) {
fos.write(buffer, 0, read);
}
}
}
static public void resumeBGM() {
getContext().runOnUiThread(new Runnable() {
@Override
public void run() {
if (bgmAudio.player != null && !bgmAudio.isSetStop) {
int playbackState = bgmAudio.player.getPlaybackState();
if (playbackState == Player.STATE_READY && !bgmAudio.player.getPlayWhenReady()) {
bgmAudio.player.setPlayWhenReady(true);
} else {
// 如果播放器还没有准备好,等待状态变化
_preparePlayerBgm();
}
Log.i(Tag, "bgm恢复 Playback State: " + playbackState);
}
}
});
}
static public void pauseBGM() {
getContext().runOnUiThread(new Runnable() {
@Override
public void run() {
if (bgmAudio.player != null) {
bgmAudio.lastPosition = bgmAudio.player.getCurrentPosition();
bgmAudio.player.setPlayWhenReady(false);
Log.i(Tag, " bgm暂停 state = " + bgmAudio.player.getPlaybackState());
}
}
});
}
/*清理缓存目录中的所有音频文件 防止热更后 播放的是旧文件*/
static public void clearCacheAudio() {
File cacheDir = getContext().getCacheDir();
// 定义audio文件夹路径
File audioDir = new File(cacheDir, "audio");
// 检查audio文件夹是否存在且是目录
if (audioDir.exists() && audioDir.isDirectory()) {
// 调用删除文件夹的递归方法
_deleteRecursive(audioDir);
Log.i(Tag, "Deleted audio folder and its contents.");
} else {
Log.i(Tag, "audio folder does not exist.");
}
}
// 递归删除文件夹及其中的所有文件
private static void _deleteRecursive(File file) {
// 如果是文件夹,首先删除其中的所有文件
if (file.isDirectory()) {
File[] children = file.listFiles();
if (children != null) {
for (File child : children) {
_deleteRecursive(child); // 递归删除子文件
}
}
}
// 删除文件或空文件夹
boolean deleted = file.delete();
if (deleted) {
Log.i(Tag, "Deleted: " + file.getAbsolutePath());
} else {
Log.i(Tag, "Failed to delete: " + file.getAbsolutePath());
}
}
// 释放资源
static public void release() {
getContext().runOnUiThread(new Runnable() {
@Override
public void run() {
if (bgmAudio.player != null) {
bgmAudio.player.stop();
bgmAudio.player.release();
bgmAudio.player = null;
}
for (Audio audio : effectAudios) {
audio.player.stop();
audio.player.release();
}
effectAudios.clear();
}
});
}
其实其中最主要的核心就是,打包出来的assets目中的资源是不能直接播放的,需要拷贝资源进入cache目录下,然后创建MediaItem才能播放,这个卡了很久,AI也是乱回答.如果是热更目录的话,android这边是直接拿不到的需要使用c++代码拷贝进入cache目录,所以在安卓工程中proj.android-studio下的jni的CocosAndroid.mk中新增cpp文件
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := cocos2djs
LOCAL_MODULE_FILENAME := libcocos2djs
ifeq ($(USE_ARM_MODE),1)
LOCAL_ARM_MODE := arm
endif
LOCAL_SRC_FILES := hellojavascript/main.cpp \
../../Classes/AppDelegate.cpp \
../../Classes/jsb_module_register.cpp \
hellojavascript/java_to_cpp_tools.cpp
LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../Classes
LOCAL_STATIC_LIBRARIES := cocos2dx_static
include $(BUILD_SHARED_LIBRARY)
$(call import-module, cocos)
同目录下的hellojavascript中新建java_to_cpp_tools.cpp
#include <jni.h>
#include <string>
#include <fstream>
#include <sstream>
extern "C" {
JNIEXPORT jbyteArray JNICALL
Java_org_cocos2dx_javascript_AudioMgr_getFileContentAsByteArray(JNIEnv *env, jobject /* this */, jstring filePath) {
const char *path = env->GetStringUTFChars(filePath, nullptr);
std::ifstream file(path, std::ios::binary);
if (!file) {
env->ReleaseStringUTFChars(filePath, path);
return nullptr;
}
// 读取文件内容到 vector 中
std::vector<unsigned char> buffer((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
file.close();
// 创建 jbyteArray 并填充数据
jbyteArray result = env->NewByteArray(buffer.size());
env->SetByteArrayRegion(result, 0, buffer.size(), reinterpret_cast<const jbyte*>(buffer.data()));
env->ReleaseStringUTFChars(filePath, path);
return result;
}
}
就是这个拷贝代码,
然后还需要在AppActivity中的onResume,onPause函数中去监听,暂停和恢复播放
@Override
protected void onResume() {
super.onResume();
SDKWrapper.getInstance().onResume();
AudioMgr.resumeBGM();
AudioMgr.resumeEffect("");
}
@Override
protected void onPause() {
super.onPause();
SDKWrapper.getInstance().onPause();
AudioMgr.pauseBGM();
AudioMgr.pauseEffect("");
}
在AudioMgr中只贴了bgm的播放暂停等 ,音效等播放是同理的,
在ts工程这边
let audioClip = await $app.load.loadAudio(path)
this._misucAudio = cc.audioEngine.playMusic(audioClip, loop)
jsb.reflection.callStaticMethod(androidPackage, "playBGM",
"(Ljava/lang/String;Ljava/lang/String;FZF)V",
audioClip.nativeUrl, callback ? `bgm${id}playEnd` : "", this.getMusicVolume(), loop, speed
)
if (callback) {
console["bgm" + id + "playEnd"] = () => {
callback?.()
console["bgm" + id + "playEnd"] = null//只执行一次
}
}
this._misucAudio = id
就可以正常的播放了
4.特殊处理nativeUrl
现在wechat和android端都没有使用引擎的播放组件,但是又加载了一次资源进入内存,播放的时候wechat那边又是下载,android这边还copy了一份资源到cache,浪费了内存,所以在打包结束的时候将游戏内的所有audio资源写入进一份json中
a.android的优化处理.
由于用的是coco自带热更所以,将audio的json写入到version.manifest中,可以跟着一起热更,读取到version.manifest的资源并新增audioTable字段
"audioTable":{"audio.BGM_1":"autio/native/78/78d9ef4b-e8c5-4942-bbcb-8fccc792695a.mp3",xxx,xxx}
类似的信息注入,然后在管理代码中
/**
* 原生 android 平台
*/
private _audioTable: { [i in string]: string } = null//key:路径 bundle.xxx/xxx value:audio\\native\\29\\29fa2a89-2e89-4cf2-acbe-641e73364a26.mp3
getAudioTable(path: string) {
if (!this._audioTable) {
//读取
this._audioTable = (JSON.parse(getManifest()) as { audioTable: { [i in string]: string } }).audioTable
}
return "assets/" + this._audioTable[path]
}
并在播放处修改
/**
* 获取manifest 的内容
* @param name
* @returns
*/
getManifest(name: "project" | "version" = "version") {
const path = this._storagePath + "/%s.manifest".format(name)
if (jsb.fileUtils.isFileExist(path)) {
LogFunc.log("读取缓存manifest")
return jsb.fileUtils.getStringFromFile(path)
} else {
LogFunc.log("读取本地的manifest")
return jsb.fileUtils.getStringFromFile(`assets/${name}.manifest`)
}
}
if (PlatformFunc.isAndroid) {
let nativeUrl = this.getAudioTable(path)//这个path就是自定义的key,就是audioTable中的audio.BGM_1
const hotPath = $app.hot.getRootPath() + "/" + nativeUrl
if (jsb.fileUtils.isFileExist(hotPath)) {//有热更
nativeUrl = hotPath
}
jsb.reflection.callStaticMethod(androidPackage, "playBGM",
"(Ljava/lang/String;Ljava/lang/String;FZF)V",
nativeUrl, callback ? `bgm${id}playEnd` : "", this.getMusicVolume(), loop, speed
)
if (callback) {
console["bgm" + id + "playEnd"] = () => {
callback?.()
console["bgm" + id + "playEnd"] = null//只执行一次
}
}
this._misucAudio = id
}
在第一个启动场景中新增
if (PlatformFunc.isAndroid) {
LogFunc.warn("清理缓存的音源!")
jsb.reflection.callStaticMethod("org/cocos2dx/javascript/AudioMgr", "clearCacheAudio", "()V")
jsb.reflection.callStaticMethod("org/cocos2dx/javascript/AudioMgr", "release", "()V")
}
清理cache下的缓存,保证是最新的资源,这样热更就处理好了
b.微信小游戏的优化处理
也是在打包完后读取处理好的audioTable.json,如果路径中包含subpackages字段,这是分包路径.将所有的路径注入到另外一个json中,并将名字md5化
最后变成293c548cc04d01cafb86eeb22a9b3186.json这样的名字,其中包含一下路径
{"audio.BGM_1":{"file":"audio/native/29/29fa2a89-2e89-4cf2-acbe-641e73364a26.371e2.mp3","type":1},.....}
其中type=1是走remote远程下载,2是走分包路径播放,并将公共地址存入其中,在播放的时候进行组装
"rUrl":"https://xxx.com/remote/","nUrl":"subpackages/"
将这个json文件放入到remote下,打开src目录下的settings.js文件,在末尾新增window.wechatAudioUrl = “https://xxx.com/remote/293c548cc04d01cafb86eeb22a9b3186.json”,然后上传远端,启动游戏的时候下载这个json表
cc.assetManager.loadRemote(window.wechatAudioUrl, { type: cc.JsonAsset }, (err, assets: cc.JsonAsset) => {
const table = assets.json//将这个文件存储起来
})
然后播放处改为
if (PlatformFunc.isWechat_game) {
let info = this.wechatAudioTable[path] as wcType//wechatAudioTable就是存储的表
const nativeUrl = this.wechatAudioTable[info.type == 1 ? "rUrl" : "nUrl"] + info.file
let audio = this._audioEs as WechatMinigame.InnerAudioContext
if (!audio) {
audio = this._audioEs = wx.createInnerAudioContext({ useWebAudioImplement: true })
}
// LogFunc.warn("bgm播放", nativeUrl)
audio.src = nativeUrl
audio.volume = this.getMusicVolume()
audio.loop = loop
audio.playbackRate = speed
audio.onEnded(() => {
callback?.()
audio.onEnded((err) => { })//只执行一次
})
audio.play()
this._misucAudio = id
}
大功告成!