关于播放变速(加速)音频(微信小游戏和android)

目前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;
    }
}

image 就是这个拷贝代码,
然后还需要在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
}

大功告成!

4赞

楼主方便指个路吗

我可太喜欢2.x的技术贴了 用一句话来形容就是很有代码活力

直接搜索 音频加速

真的是超级实用

超级实用,赞