背景
最近RN的项目中用的音频文件是fmod的.bank 格式的,关于fmod是什么,可以参考fmod的官网(https://www.fmod.com/),或者自己去网上搜索。本项目的核心诉求是,在播放音频的同时,音频会不定时的产生事件回调,通知到RN,然后RN根据事件来绘制不同的页面(展示不同的内容)。
在网上根本找不到任何RN使用fmod的资料,退一步,iOS和Android 使用fmod的资料也是少之又少,都看了一遍之后,毫无帮助,没办法,只能靠自己去尝试了。
从fmod官网下载了iOS、Android、H5的相关库,里面的例子全是C++的,iOS和Android的项目工程文件,根本打不开,官方指导也没有说明第一步、第二步等等分别调用什么API。fmod 以前是做PC上的游戏的,他对移动端的支持非常有限,从文档不全就能略知一二。
本文就梳理一下自己摸索的流程和踩过的坑。
iOS
首先还是必须去官网下载fmod的API文档(https://www.fmod.com/download),虽然看了文档也是一脸懵逼,但是聊胜于无,也必须看,因为所有的尝试还是需要依据官方文档来进行。
这个要分成两部分来讲,因为iOS和Android 两个平台,要分别写两套代码,虽然核心使用fmod api的部分是一样的。
对于,iOS的平台,需要提前知道的知识点:
- 会react-native(废话,不会这个就不会有这一堆问题了);
- 稍微会一点点Object-c;
- 会OC和C++的混编;
基本思路是在RN界面点击播放按钮,然后将音频的相关URL传递给iOS的native函数,下载音频文件,调用FMOD STUDIO API 播放、暂停、停止等操作,同时给播放时注册回调函数,在回调函数中回去数据再回调给RN,然后更新界面。
下面给出关键步骤的部分代码。
React-Native 调用iOS Native 接口
在RN端是通过NativeModules 来实现的
import {NativeModules} from 'react-native';
// OpenNativeModule 这个是iOS端和Android端自己定义和实现的module
var nativeModule = NativeModules.OpenNativeModule;
...
// 调用的时候
nativeModule.testNativeDownloadFile(params);
// testNativeDownloadFile 是OpenNativeModule中实现的 method
RN端的核心代码就这几行,剩下的是iOS端的实现。
// 所有能被RN调用的方法,需要用RCT_EXPORT_METHOD声明
RCT_EXPORT_METHOD(testNativeDownloadFile:(NSDictionary *)dict) {
NSArray *url_list = [dict objectForKey:@"url_list"];
NSMutableArray __block *file_list = [NSMutableArray arrayWithCapacity:url_list.count];
NSInteger __block task_count = 0;
for (int i = 0; i < url_list.count; ++i) {
// 替换url中的空格,否则ios不支持,会有错误码-1002
NSString *tmpUrl = [url_list[i] stringByReplacingOccurrencesOfString:@" " withString:@"%20"];
NSURL* url = [NSURL URLWithString:tmpUrl];
NSLog(@"xxx log: download url: %@", tmpUrl);
// 得到session对象
NSURLSession* session = [NSURLSession sharedSession];
// 创建任务
NSURLSessionDownloadTask* downloadTask = [session downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
//NSLog(@"xxx log: file path: %@", location.path);
// NSLog(@"xxx log: file name: %@", response.suggestedFilename);
//NSLog(@"xxx log: error code: %@", error); // 如果有异常,输出错误信息
// [file_list addObject:response.suggestedFilename];
NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
// response.suggestedFilename :建议使用的文件名,一般跟服务器端的文件名一致
NSString *file = [caches stringByAppendingPathComponent:response.suggestedFilename];
// 将临时文件剪切或者复制Caches文件夹
NSFileManager *mgr = [NSFileManager defaultManager];
// AtPath : 剪切前的文件路径
// ToPath : 剪切后的文件路径
[mgr moveItemAtPath:location.path toPath:file error:nil];
NSLog(@"xxx log: file path:%@", file);
[file_list addObject:file];
NSLog(@"xxx log: LINE:%d, file list: %lu", __LINE__, file_list.count);
--task_count; // 任务完成
}];
// 开始任务
++task_count;
[downloadTask resume];
NSLog(@"xxx log: finish");
//break;
}
while (true) {
if (task_count <= 0) {
NSLog(@"xxx log: LINE:%d, file list: %lu", __LINE__, file_list.count);
for (int i = 0; i < file_list.count; ++i) {
NSLog(@"xxx log: LINE:%d, file_name: %@", __LINE__, file_list[i]);
}
break;
}
}
播放FMOD Studio 的 bank 音频
由于FMOD官方给的API都是C/C++的,所以这里需要OC和C++混编来实现音频播放,上面的调用Native接口的代码已经把所有的音频文件下载好了,剩下的就是加载相关的库来播放。
FMOD::Studio::System* gsystem = NULL;
FMOD::Studio::EventDescription * geventDesc = NULL;
FMOD::Studio::EventInstance * gengine = NULL;
const int BANK_COUNT = (int)file_list.count;
FMOD::Studio::Bank* banks[BANK_COUNT];
if (gsystem == NULL) {
NSLog(@"xxx log: LINE:%d, init fmod studio system", __LINE__);
//FMOD::Studio::System::create(&gsystem);
} else {
NSLog(@"xxx log: LINE:%d, unloadAll fmod studio system", __LINE__);
//gengine->stop(FMOD_STUDIO_STOP_IMMEDIATE);
//gsystem->unloadAll();
gsystem->release();
}
FMOD::Studio::System::create(&gsystem);
gsystem->initialize(1024, FMOD_STUDIO_INIT_NORMAL, FMOD_INIT_NORMAL, 0);
gsystem->setCallback(studioCallback, FMOD_STUDIO_SYSTEM_CALLBACK_BANK_UNLOAD);
for (int i = 0; i < file_list.count; ++i) {
gsystem->loadBankFile([file_list[i] cStringUsingEncoding:NSUTF8StringEncoding], FMOD_STUDIO_LOAD_BANK_NORMAL, &banks[i]);
}
gsystem->update();
NSString *eventStr = [@"event:/" stringByAppendingString: fmod_name];
NSLog(@"xxx log: LINE:%d, event_file: %@", __LINE__, eventStr);
gsystem->getEvent([eventStr cStringUsingEncoding:NSUTF8StringEncoding], &geventDesc);
// 控制延迟的,暂时不用
//FMOD::System *lowLevelSystem;
//gsystem->getLowLevelSystem(&lowLevelSystem);
//lowLevelSystem->setDSPBufferSize(4096, 2);
//gsystem->getEvent("event:/xxxxx", &geventDesc);
geventDesc->createInstance(&gengine);
gengine->start();
gsystem->update();
}
// Callback to free memory-point allocation when it is safe to do so
//
FMOD_RESULT F_CALLBACK studioCallback(FMOD_STUDIO_SYSTEM *system, FMOD_STUDIO_SYSTEM_CALLBACK_TYPE type, void *commanddata, void *userdata)
{
if (type == FMOD_STUDIO_SYSTEM_CALLBACK_BANK_UNLOAD)
{
// For memory-point, it is now safe to free our memory
FMOD::Studio::Bank* bank = (FMOD::Studio::Bank*)commanddata;
void* memory;
ERRCHECK(bank->getUserData(&memory));
if (memory)
{
free(memory);
}
}
return FMOD_OK;
}
现在,已经可以正常播放FMOD的音频了,接下来我们要获取音频播放过程中的事件,这个相对就很简单了,设置一下回调就ok。
gengine->setCallback(markerCallback,
FMOD_STUDIO_EVENT_CALLBACK_TIMELINE_MARKER
| FMOD_STUDIO_EVENT_CALLBACK_TIMELINE_BEAT
| FMOD_STUDIO_EVENT_CALLBACK_SOUND_PLAYED
| FMOD_STUDIO_EVENT_CALLBACK_SOUND_STOPPED);
// marker的回调
FMOD_RESULT F_CALLBACK markerCallback(FMOD_STUDIO_EVENT_CALLBACK_TYPE type, FMOD_STUDIO_EVENTINSTANCE *event, void *parameters) {
cout << "LINE:" << __LINE__ << ",xxx log: type:" << type << ",obj:" << FMOD_STUDIO_EVENT_CALLBACK_TIMELINE_MARKER << endl;
if (type == FMOD_STUDIO_EVENT_CALLBACK_TIMELINE_MARKER) {
FMOD_STUDIO_TIMELINE_MARKER_PROPERTIES* props = (FMOD_STUDIO_TIMELINE_MARKER_PROPERTIES*)parameters;
///TODO
}
...
return FMOD_OK;
}
接下来需要把音频的回调传递给RN侧,iOS侧通过一个事件通知来做。
// native 主动通知 rn端
- (instancetype)init {
self = [super init];
if (self) {
NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
[defaultCenter removeObserver:self];
[defaultCenter addObserver:self
selector:@selector(sendCustomEvent:)
name:@"sendCustomEventNotification"
object:nil];
}
return self;
}
- (void)sendCustomEvent:(NSNotification *)notification {
NSString *name = notification.object;
NSLog(@"LINE: %d,xxx log: notification:%@", __LINE__,name);
[self sendEventWithName:@"customEvent" body:name];
}
RN侧接收事件,然后进行相应渲染。
componentDidMount() {
let eventEmitter = new NativeEventEmitter(nativeModule);
this.listener = eventEmitter.addListener("customEvent", this.listenCallback);
}
listenCallback(item) {
console.log("native notition:"+item);
}
以上,就是在iOS上播放fmod studio 的 bank 音频的整个流程。
Android
同样由于FMOD的API是c/c++的,所以Android端只能用JNI来实现。这里和iOS不同的地方在于,在iOS上,OC和C/C++可以混编,所以可以同时实现,但是Android端的java和C/C++不能混编,只能分开弄,也就是对音频文件的下周再Android做,调用音频播放接口在C/C++这里做。
下载音频文件的代码如下:
// 能够被rn调用的方法使用@ReactMethod声明
@ReactMethod
public void testNativeDownloadFile(ReadableMap map) {
// 测试文件下载
//System.out.print(map);
Log.d("xxx", map.getString("event"));
ReadableArray urlList = map.getArray("url_list");
for (int i = 0; i < urlList.size(); ++i) {
Log.d("xxx", "url:"+urlList.getString(i));
}
// /storage/emulated/0
String path = Environment.getExternalStorageDirectory().getAbsolutePath();
Log.d("xxx","path: " + path);
String path1 = Environment.getDataDirectory().getPath();
Log.d("xxx","path1: " + path1); // /data
String path2 = Environment.getDownloadCacheDirectory().getPath();
Log.d("xxx","path2: " + path2); // /data/cache
String path3 = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
Log.d("xxx","path3: " + path3); // /storage/emulated/0/Download
String path4 = Environment.getExternalStorageDirectory().toString();
Log.d("xxx","path4: " + path4);
String path5 = mReactContext.getExternalFilesDir(null).toString();
Log.d("xxx","path5:"+path5); // /storage/emulated/0/Android/data/com.test_rn_native/files
String namePre = map.getString("name_pre");
String fmodName = map.getString("event");
String[] fileList = new String[urlList.size()];
for (int i = 0; i < urlList.size(); ++i) {
String fileName = downloadFile(urlList.getString(i), namePre);
fileList[i] = fileName;
}
for (int i = 0; i < fileList.length; ++i) {
File f = new File(fileList[i]);
if (f.exists() && f.isFile()) {
Log.d("xxx", fileList[i] + " size:" + f.length());
} else {
Log.d("xxx", "file not exists:" + fileList[i]);
}
}
}
public String downloadFile(String url, String namePre) {
Log.i("xxx","downloadFile step into,url=" + url);
String filePath = "";
try {
String downPath = mReactContext.getExternalFilesDir(null).toString() + "/";
long startTime = System.currentTimeMillis();
String filename = namePre + url.substring(url.lastIndexOf("/") + 1);
Log.i("xxx","start download");
URL myURL = new URL(url);
URLConnection conn = myURL.openConnection();
conn.connect();
InputStream is = conn.getInputStream();
int fileSize = conn.getContentLength();//根据响应获取文件大小
if (fileSize <= 0) throw new RuntimeException("无法获知文件大小 ");
if (is == null) throw new RuntimeException("stream is null");
File file1 = new File(downPath);
if (!file1.exists()) {
file1.mkdirs();
}
filePath = downPath + filename;
FileOutputStream fos = new FileOutputStream(filePath);
byte buf[] = new byte[4096];
int downLoadFileSize = 0;
do{
//循环读取
int numread = is.read(buf);
if (numread == -1)
{
break;
}
fos.write(buf, 0, numread);
downLoadFileSize += numread;
//更新进度条
} while (true);
Log.i("xxx","download success:" + filePath);
Log.i("xxx","totalTime="+ (System.currentTimeMillis() - startTime));
} catch (Exception ex) {
Log.e("xxx", "error: " + ex.getMessage(), ex);
}
return filePath;
}
然后把下载的文件传递给JNI层,这里有一点要特别注意, JNI 的编译,只能用CMAKE,老的方式是用NDK编译,但是会有各种各样的问题,用cmake编译,一切都会变得简单。
JNI的代码如下:
static FMOD::Studio::System* gsystem = NULL;
static FMOD::Studio::EventDescription * geventDesc = NULL;
static FMOD::Studio::EventInstance * gengine = NULL;
static bool gStop = false;
// 回调使用
JavaVM *gVM = NULL;
jobject gObj;
//
// Callback to free memory-point allocation when it is safe to do so
//
FMOD_RESULT F_CALLBACK studioCallback(FMOD_STUDIO_SYSTEM *system, FMOD_STUDIO_SYSTEM_CALLBACK_TYPE type, void *commanddata, void *userdata)
{
if (type == FMOD_STUDIO_SYSTEM_CALLBACK_BANK_UNLOAD)
{
// For memory-point, it is now safe to free our memory
FMOD::Studio::Bank* bank = (FMOD::Studio::Bank*)commanddata;
void* memory;
// ERRCHECK(bank->getUserData(&memory));
bank->getUserData(&memory);
if (memory)
{
free(memory);
}
}
return FMOD_OK;
}
// marker的回调
FMOD_RESULT F_CALLBACK markerCallback(FMOD_STUDIO_EVENT_CALLBACK_TYPE type, FMOD_STUDIO_EVENTINSTANCE *event, void *parameters) {
//cout << "LINE:" << __LINE__ << ",xxx log: type:" << type << ",obj:" << FMOD_STUDIO_EVENT_CALLBACK_TIMELINE_MARKER << endl;
if (type == FMOD_STUDIO_EVENT_CALLBACK_TIMELINE_MARKER) {
FMOD_STUDIO_TIMELINE_MARKER_PROPERTIES* props = (FMOD_STUDIO_TIMELINE_MARKER_PROPERTIES*)parameters;
LOGD("jni params marker: %s",props->name);
// [[NSNotificationCenter defaultCenter] postNotificationName:@"sendCustomEventNotification" object:[NSString stringWithUTF8String:props->name]];
// 回调java代码
// 获取方法ID, 通过方法名和签名, 调用静态方法
// 非静态方法需要创建实例
JNIEnv *env;
int getEnvStat = gVM->GetEnv((void **) &env,JNI_VERSION_1_6);
if (getEnvStat == JNI_EDETACHED) {
//如果没有,主动附加到jvm环境中,获取到env
if (gVM->AttachCurrentThread(&env, NULL) != 0) {
return FMOD_OK;
}
}
//通过全局变量g_obj 获取到要回调的类
jclass javaClass = env->GetObjectClass(gObj);
if (javaClass == 0) {
LOGD("Unable to find class");
gVM->DetachCurrentThread();
return FMOD_OK;
}
jmethodID mid = env->GetMethodID(javaClass, "callMethod", "(Ljava/lang/String;)V");
if (mid == NULL) {
LOGD("Unable to find method:callMethod");
return FMOD_OK;
}
env->CallVoidMethod(gObj, mid, env->NewStringUTF(props->name));
}
return FMOD_OK;
}
JNIEXPORT jstring JNICALL Java_com_test_1rn_1native_OpenNativeModule_testParams
(JNIEnv *env, jobject obj, jobjectArray urlList, jstring fmodName) {
LOGD("STEP into native method");
jboolean isCopy = false;
const char *str = env->GetStringUTFChars(fmodName, &isCopy);
if (str == NULL) {
LOGD("this error");
return NULL;
}
LOGD("fmodName:%s", str);
string fmodNameStr = str;
int len = env->GetArrayLength(urlList);
vector<string> fileList;
for (int i = 0; i < len; ++i) {
jstring jstr = (jstring)env->GetObjectArrayElement(urlList, i);
string url = env->GetStringUTFChars(jstr, NULL);
fileList.push_back(url);
}
for (int i = 0; i < fileList.size(); ++i) {
LOGD("file %d: %s",i, fileList[i].c_str());
}
//FMOD::Studio::System* gsystem;
//FMOD::Studio::EventDescription * geventDesc;
//FMOD::Studio::EventInstance * gengine;
const int BANK_COUNT = fileList.size();
FMOD::Studio::Bank* banks[BANK_COUNT];
if (gsystem == NULL) {
LOGD("init fmod studio");
} else {
LOGD("release fmod studio");
gsystem->release();
}
FMOD::Studio::System::create(&gsystem);
gsystem->initialize(1024, FMOD_STUDIO_INIT_NORMAL, FMOD_INIT_NORMAL, 0);
gsystem->setCallback(studioCallback, FMOD_STUDIO_SYSTEM_CALLBACK_BANK_UNLOAD);
for (int i = 0; i < fileList.size(); ++i) {
gsystem->loadBankFile(fileList[i].c_str(), FMOD_STUDIO_LOAD_BANK_NORMAL, &banks[i]);
}
gsystem->update();
string eventStr = "event:/" + fmodNameStr;
LOGD("event:%s",eventStr.c_str());
gsystem->getEvent(eventStr.c_str(), &geventDesc);
geventDesc->createInstance(&gengine);
gengine->setCallback(markerCallback,
FMOD_STUDIO_EVENT_CALLBACK_TIMELINE_MARKER
| FMOD_STUDIO_EVENT_CALLBACK_TIMELINE_BEAT
| FMOD_STUDIO_EVENT_CALLBACK_SOUND_PLAYED
| FMOD_STUDIO_EVENT_CALLBACK_SOUND_STOPPED);
env->GetJavaVM(&gVM);
gObj = env->NewGlobalRef(obj);
gengine->start();
gsystem->update();
return env->NewStringUTF("success testParams");
}
JNIEXPORT void JNICALL Java_com_test_1rn_1native_OpenNativeModule_testFmodPause
(JNIEnv *env, jobject obj) {
if (gsystem == NULL) {
return;
}
bool paused = false;
int ret = gengine->getPaused(&paused);
LOGD("pause: %d, ret:%d", paused, ret);
ret = gengine->setPaused(!paused);
gsystem->update();
}
JNIEXPORT void JNICALL Java_com_test_1rn_1native_OpenNativeModule_testFmodStop
(JNIEnv *env, jobject obj) {
if (gsystem == NULL) {
return;
}
if (gStop == false) {
gengine->stop(FMOD_STUDIO_STOP_IMMEDIATE);
gStop = true;
} else {
gengine->start();
gStop = false;
}
gsystem->update();
LOGD("stop status: %d", gStop);
}
需要修改app/build.gradle 文件,才能把fmod相关的.so 打包进apk中。
sourceSets.main {
jniLibs.srcDirs = ['src/main/jniLibs']
}
要解决react-native 和 FMOD之后引起的32-bit和64-bit 的arm结构不兼容问题,也需要修改app/build.gradle.
defaultConfig {
applicationId "com.test_rn_native"
minSdkVersion 16
targetSdkVersion 22
versionCode 1
versionName "1.0"
ndk {
abiFilters "armeabi-v7a", "x86"
}
externalNativeBuild {
cmake {
cppFlags ""
abiFilters "armeabi-v7a", "x86"
}
}
packagingOptions {
exclude "lib/arm64-v8a" // 排除这个文件夹
}
}
现在,Android端应该也能正常播放fmod studio bank的音频文件了。最后一步,在rn端接受Android的回调。
componentDidMount() {
let eventEmitter = new NativeEventEmitter(nativeModule);
this.listener = eventEmitter.addListener("customEvent", this.listenCallback);
DeviceEventEmitter.addListener('customEvent', this.listenCallback);
}
listenCallback(item) {
console.log("native notition:"+item);
}
尾
整个项目的demo工程已经放到GitHub,在几乎没有资料的情况下,自己摸索和尝试,记录一下,给后面有需要的朋友一些参考,当然上面并没有列全我这个过程碰到的各种问题。