FMOD
FMOD是一个强大的声音引擎框架,QQ、魔兽世界及其他很多游戏都是使用的这套框架,框架内包含几十种声音类型,还可以修改声音的频率、速度等等。
1.FMOD download
进入FMOD download,下载Android端引擎库。
2.复制代码
将下载下来的FOMD引擎库中的Jar包,so库和jni c++文件,复制到项目中。
3.编辑配置
编辑配置CMakeLists.txt
-----------------------------------------
find_library( log-lib
log )
set(lib_path ${CMAKE_SOURCE_DIR}/libs)
# 添加三方的so库
add_library(libfmod
SHARED
IMPORTED )
# 指名第三方库的绝对路径
set_target_properties( libfmod
PROPERTIES IMPORTED_LOCATION
${lib_path}/${ANDROID_ABI}/libfmod.so )
add_library(libfmodL
SHARED
IMPORTED )
set_target_properties( libfmodL
PROPERTIES IMPORTED_LOCATION
${lib_path}/${ANDROID_ABI}/libfmodL.so )
#--------------------------------
add_library( # Sets the name of the library.
FmodSound
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp)
#---------------------
# 导入路径,为了让编译时能够寻找到这个文件夹
include_directories(src/main/cpp/inc)
# 链接好三个路径
target_link_libraries( FmodSound
libfmod
libfmodL
${log-lib} )
4.修改代码
修改native-lib.cpp
,已完成项目需求,如保存变声文件的代码:
extern "C"
JNIEXPORT jint JNICALL
Java_com_demon_fmodsound_FmodSound_saveSound(JNIEnv *env, jobject cls, jstring path_jstr, jint type, jstring save_jstr) {
Sound *sound;
DSP *dsp;
bool playing = true;
float frequency = 0;
System *mSystem;
JNIEnv *mEnv = env;
int code = 0;
System_Create(&mSystem);
const char *path_cstr = mEnv->GetStringUTFChars(path_jstr, NULL);
LOGI("saveAiSound-%s", path_cstr)
const char *save_cstr;
if (save_jstr != NULL) {
save_cstr = mEnv->GetStringUTFChars(save_jstr, NULL);
LOGI("saveAiSound-save_path=%s", save_cstr)
}
try {
if (save_jstr != NULL) {
char cDest[200];
strcpy(cDest, save_cstr);
mSystem->setSoftwareFormat(8000, FMOD_SPEAKERMODE_MONO, 0); //设置采样率为8000,channel为1
mSystem->setOutput(FMOD_OUTPUTTYPE_WAVWRITER); //保存文件格式为WAV
mSystem->init(32, FMOD_INIT_NORMAL, cDest);
mSystem->recordStart(0, sound, true);
}
//创建声音
mSystem->createSound(path_cstr, FMOD_DEFAULT, NULL, &sound);
mSystem->playSound(sound, 0, false, &channel);
LOGI("saveAiSound-%s", "save_start")
switch (type) {
case MODE_NORMAL:
LOGI("saveAiSound-%s", "save MODE_NORMAL")
break;
case MODE_FUNNY:
LOGI("saveAiSound-%s", "save MODE_FUNNY")
mSystem->createDSPByType(FMOD_DSP_TYPE_NORMALIZE, &dsp);
channel->getFrequency(&frequency);
frequency = frequency * 1.6;
channel->setFrequency(frequency);
break;
case MODE_UNCLE:
LOGI("saveAiSound-%s", "save MODE_UNCLE")
mSystem->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 0.8);
channel->addDSP(0, dsp);
break;
case MODE_LOLITA:
LOGI("saveAiSound-%s", "save MODE_LOLITA")
mSystem->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 1.8);
channel->addDSP(0, dsp);
break;
case MODE_ROBOT:
LOGI("saveAiSound-%s", "save MODE_ROBOT")
mSystem->createDSPByType(FMOD_DSP_TYPE_ECHO, &dsp);
dsp->setParameterFloat(FMOD_DSP_ECHO_DELAY, 50);
dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK, 60);
channel->addDSP(0, dsp);
break;
case MODE_ETHEREAL:
LOGI("saveAiSound-%s", "save MODE_ETHEREAL")
mSystem->createDSPByType(FMOD_DSP_TYPE_ECHO, &dsp);
dsp->setParameterFloat(FMOD_DSP_ECHO_DELAY, 300);
dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK, 20);
channel->addDSP(0, dsp);
break;
case MODE_CHORUS:
LOGI("saveAiSound-%s", "save MODE_CHORUS")
mSystem->createDSPByType(FMOD_DSP_TYPE_ECHO, &dsp);
dsp->setParameterFloat(FMOD_DSP_ECHO_DELAY, 100);
dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK, 50);
channel->addDSP(0, dsp);
break;
case MODE_HORROR:
LOGI("saveAiSound-%s", "save MODE_HORROR")
mSystem->createDSPByType(FMOD_DSP_TYPE_TREMOLO, &dsp);
dsp->setParameterFloat(FMOD_DSP_TREMOLO_SKEW, 0.8);
channel->addDSP(0, dsp);
break;
default:
break;
}
mSystem->update();
} catch (...) {
LOGE("saveAiSound-%s", "save error!")
code = 1;
goto end;
}
while (playing) {
usleep(1000);
channel->isPlaying(&playing);
}
LOGI("saveAiSound-%s", "save over!")
goto end;
end:
if (path_jstr != NULL) {
mEnv->ReleaseStringUTFChars(path_jstr, path_cstr);
}
if (save_jstr != NULL) {
mEnv->ReleaseStringUTFChars(save_jstr, save_cstr);
}
sound->release();
mSystem->close();
mSystem->release();
return code;
}
5.原生加载SO
加载FMOD的方法,提供给Android调用:
object FmodSound {
//音效的类型
const val MODE_NORMAL = 0 //正常
const val MODE_FUNNY = 1 //搞笑
const val MODE_UNCLE = 2 //大叔
const val MODE_LOLITA = 3 //萝莉
const val MODE_ROBOT = 4 //机器人
const val MODE_ETHEREAL = 5 //空灵
const val MODE_CHORUS = 6 //混合
const val MODE_HORROR = 7 //恐怖
init {
System.loadLibrary("fmodL")
System.loadLibrary("fmod")
System.loadLibrary("FmodSound")
}
external fun saveSound(path: String, type: Int, savePath: String): Int
external fun playSound(path: String, type: Int = MODE_NORMAL): Int
external fun stopPlay()
external fun resumePlay()
external fun pausePlay()
external fun isPlaying(): Boolean
fun saveSoundAsync(path: String, type: Int, savePath: String, listener: ISaveSoundListener? = null) {
try {
if (isPlaying()) {
stopPlay()
}
val result = saveSound(path, type, savePath)
if (result == 0) {
listener?.onFinish(path, savePath, type)
} else {
listener?.onError("error")
}
} catch (e: Exception) {
listener?.onError(e.message)
}
}
interface ISaveSoundListener {
//成功
fun onFinish(path: String, savePath: String, type: Int)
//出错
fun onError(msg: String?)
}
}
6.原生调用
在项目中调用FMOD方法,先保存变声文件,保存成功后播放。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//初始化
FMOD.init(this)
binding = ActivitySoundFmodBinding.inflate(layoutInflater)
setContentView(binding.root)
var path = intent.getStringExtra("path") ?: ""
val file = File(path)
if (!file.exists()) {
showToast("录音文件不存在,请重新录制!")
finish()
} else {
if (path.endsWith(".amr")) {
path = AmrToWav.makeAmrToWav(path, false)
}
binding.tvPath.text = "音频文件:$path"
}
binding.fnRG.setOnCheckedChangeListener { group, checkedId ->
val pos = group.indexOfChild(group.findViewById(checkedId))
Log.i(TAG, "onCreate: $pos")
type = pos
}
binding.btnPlay.setOnClickListener {
GlobalScope.launchIO {
FmodSound.playSound(path, type)
}
}
binding.btnSave.setOnClickListener {
binding.tvSave.text = "开始变声..."
//耗时任务,需要在子线程中执行
GlobalScope.launchIO {
FmodSound.saveSoundAsync(path, type, getRecordFilePath(1), object : FmodSound.ISaveSoundListener {
override fun onFinish(path: String, savePath: String, type: Int) {
runOnUiThread {
binding.tvSave.text = "变声输出文件路径:$savePath"
}
FmodSound.playSound(savePath)
}
override fun onError(msg: String?) {
Log.e(TAG, "onError: $msg")
runOnUiThread {
binding.tvSave.text = "变声失败:$msg"
}
}
})
}
}
}
override fun onDestroy() {
super.onDestroy()
//释放
FMOD.close()
}
7.FMOD优缺分析
- 优点:变声类型多,自定义功能强大,百度文档比较多。
- 缺点:变声引擎库体积大,调用方法多,使用麻烦,保存变声文件的速度较慢。
SoundTouch
SoundTouch是一个开源音频处理库,用于更改音频流或音频文件的速度,音调和播放速率。该库还支持估算音轨的稳定每分钟节拍速率。
1.下载源码
进入SoundTouch的Gitlab仓库下载最新的源码。
2.解压编译
解压下载的源码,进入到目录soundtouch/source/Android-lib/jni
,打开CMD执行ndk-build
,将jni编译成so库。
注意安装NDK环境。
如果编译中遇到下图的错误:
可以在Android.mk
中添加APP_ALLOW_MISSING_DEPS=true
即可。
3.复制SO库
将soundtouch/source/Android-lib/libs
生成的so库,选择需要的平台复制到你的项目中。
由于SoundTouch已经不再支持armeabi
,需要armeabi
平台的,可以直接使用armeabi-v7a
平台的,两个平台的so库是完全兼容的。
如果so不是放在默认的'src/main/jniLibs'
目录下,需要在在build.gradle
中配置。
例如放在libs目录下:
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
4.原生加载SO
复制source/Android-lib/src/net/surina/soundtouch/SoundTouch.java
到你项目中,注意该文件需要放在包名net.surina.soundtouch
下(如果你修改了soundtouch-jni.cpp
中的包名,则放到自己指定的包名下即可)。
5.原生调用
在项目中调用,设置变声音调/速率,生成变声文件,并播放。
/**
* 执行变声,需要在子线程中执行
*
* @param path 音频文件陆宇
* @param savePath 变声后文件保存路径
*/
private fun process(path: String, savePath: String) {
try {
val st = SoundTouch()
st.setTempo(tempo) //速度
st.setSpeed(speed) //速度&音调
st.setPitchSemiTones(pitch) //音调
val res = st.processFile(path, savePath)
//res==0 变声成功
if (res == 0) {
//播放savePath
} else {
showToast(SoundTouch.getErrorString())
}
} catch (e: Exception) {
e.printStackTrace()
}
}
6.SoundTouch优缺分析
- 优点:so库体积小,使用方便,方法简单,生成变声文件速度快。
- 缺点:变声选择少,只能控制音调和速率;没有现成的so库需要自己配环境编译。
参考文档
FMOD
http://blackchy.com/2018/12/10/2018-12-10-Fmod-Voice-Change/
https://www.jianshu.com/p/2e1fd3035ae1
SoundTouch
http://www.surina.net/soundtouch/README-SoundTouch-Android.html
https://gitlab.com/soundtouch/soundtouch