背景
先看两个场景:
怎么做到的?我也想装一波
今天带小伙伴们了解一下如何做到上面的效果。
fmod介绍
古人有云:视频界有ffmpeg,音频界有fmod。
下面的游戏大家伙可以看看,有没有眼熟的:
我随便截了几张图,有兴趣的可以到Fmod官网去具体了解。
怎么样有没有兴趣搞点有意思的玩玩,音频引擎已经有了。
我这边在调音编辑器里玩了一会,但是调音水平有限,不过多介绍了,有兴趣的自己down下来玩儿。
废话不多说,准备干活
第一步 先下载Android平台的引擎
第二步 下载后需要的文件:
.hpp是什么
.hpp,本质就是将.cpp的实现代码混入.h头文件当中,定义与实现都包含在同一文件,则该类的调用者只需要include该.hpp文件即可,无需再将cpp加入到project中进行编译。而实现代码将直接编译到调用者的obj文件中,不再生成单独的obj,采用hpp将大幅度减少调用project中的cpp文件数与编译次数,也不用再发布lib与dll文件,因此非常适合用来编写公用的开源库。
hpp的优点不少,但是编写中有以下几点要注意:
1、是Header Plus Plus的简写。(.h和.hpp就如同.c和.cpp似的)
2、与.h类似,.hpp是C++程序头文件格式。
3、是VCL专用的头文件,已预编译。
4、是一般模板类的头文件。
5、一般来说,.h里面只有声明,没有实现,而.hpp里声明实现都有,后者可以减少.cpp的数量。
6、.h里面可以有using namespace std,而.hpp里则无。
7、不可包含全局对象和全局函数。
新建项目并部署C/C++库步骤
第一步 新建Native Project
然后一路next 完事儿。
但是在运行的时候遇到了一个插曲(环境问题,可以忽略),仅做记录,各位看官大佬可以忽略。
- AS环境为最新版白狐
- AGP为最新版7.0.2
要求JDK为java 11,但是我主项目必须配置为jdk1.8,AS本身自带的就是java11所以我没有另外下载,而是直接在app的build.gradle里面
//app的build.gradle中android闭包下修改为
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
结果无效,不想折腾了所以降低agp版本,解决首次编译问题。
如果有大佬操作过不同项目配置不同jdk版本和环境变量的请不吝赐教。
最终可运行的项目如下
Cmake导入库流程
前置知识
CMake是什么?
一个跨平台构建系统
add_library 指令
语法:add_library(libname [SHARED | STATIC | MODULE] [EXCLUDE_FROM_ALL] [source])
将一组源文件 source 编译出一个库文件,并保存为 libname.so (lib 前缀是生成文件时 CMake自动添加上去的)。其中有三种库文件类型,不写的话,默认为 STATIC:
- SHARED: 表示动态库,可以在(Java)代码中使用 System.loadLibrary(name) 动态调用;
- STATIC: 表示静态库,集成到代码中会在编译时调用;
- MODULE: 只有在使用 dyId 的系统有效,如果不支持 dyId,则被当作 SHARED 对待;
- EXCLUDE_FROM_ALL: 表示这个库不被默认构建,除非其他组件依赖或手工构建
target_link_libraries 指令
语法:target_link_libraries(target library <debug | optimized> library2…)
这个指令可以用来为 target 添加需要的链接的共享库,同样也可以用于为自己编写的共享库添加共享库链接。
#指定 compress 工程需要用到 libjpeg 库和 log 库
target_link_libraries(compress libjpeg ${log-lib})
第一步 导入头文件并声明
将上面提到的Fmod引擎Android平台的inc文件夹拷贝到项目的cpp文件下。并声明之:
//CMakeLists.txt
# 声明导入的头文件(以cpp文件夹为~)
include_directories("inc")
第二步 为了以后拓展,修改c文件引入形式(可忽略)
//CMakeLists.txt
# 批量导入所有源文件
file(GLOB allCPP *.c *.h *.cpp)
add_library(
voicechangeapp
SHARED
${allCPP}
)
第三步 导入fmod库文件并配置
将上面提到的Fmod引擎Android平台的lib文件夹下面的arm64-v8a(armeabi、armeabi-v7a、x86等)放到src/main/jniLibs文件夹下。并在CMakeList.txt
中新增配置
//CMakeLists.txt
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}")
target_link_libraries( # Specifies the target library.
voicechangeapp
log
fmod
fmodL
)
最终CMakeLists.txt文件为:
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.10.2)
# 声明导入的头文件(以cpp文件夹为~)
include_directories("inc")
# 批量导入所有源文件
file(GLOB allCPP *.c *.h *.cpp)
# Declares and names the project.
project("voicechangeapp")
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
voicechangeapp
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
${allCPP}
)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
#find_library( # Sets the name of the path variable.
# log-lib
#
# # Specifies the name of the NDK library that
# # you want CMake to locate.
# log )
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}")
target_link_libraries( # Specifies the target library.
voicechangeapp
log
fmod
fmodL
)
第四步 导入jar并配置ndk架构过滤
文件还是上面提到的Android平台提供的
这一步随意点,理论上来说其实不需要配置,直接在jniLibs里面只保留一个主流的即可,或者动态下发更贴近实际开发,这里只是为了演示。
QQ语音变声实现
activity里的代码很简单,随便给了几个按钮,提供一个java去调用native的方法,以及一个native播放完成后的方法回调。
public class MainActivity extends AppCompatActivity {
private static final int MODE_NORMAL = 0;
private static final int MODE_LOLITA = 1;
private static final int MODE_UNCLE = 2;
private static final int MODE_HORROR = 3;
private static final int MODE_FUNNY = 4;
private static final int MODE_INTANGIBLE = 5;
static {
System.loadLibrary("native-lib");
}
private String path;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
path = "file:///android_asset/hello.m4a";
FMOD.init(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
FMOD.close();
}
// 六个 点击事件
public void onFix(View view) {
switch (view.getId()) {
case R.id.btn_normal:
voiceChangeNative(MODE_NORMAL, path); // 真实开发中,必须子线程 JNI线程(很多坑)
break;
case R.id.btn_lolita:
voiceChangeNative(MODE_LOLITA, path);
break;
case R.id.btn_uncle:
voiceChangeNative(MODE_UNCLE, path);
break;
case R.id.btn_horror:
voiceChangeNative(MODE_HORROR, path);
break;
case R.id.btn_funny:
voiceChangeNative(MODE_FUNNY, path);
break;
case R.id.btn_intangible:
voiceChangeNative(MODE_INTANGIBLE, path);
break;
}
}
// 给C++调用的函数
private void playerEnd(String msg) {
Toast.makeText(this, "" + msg, Toast.LENGTH_SHORT).show();
}
private native void voiceChangeNative(int modeNormal, String path);
}
生成.h的头文件
cd到src/main/java文件夹下,运行命令即可生成,然后拷贝到cpp文件夹下,备用。
javah com.alex.voicechangeapp.MainActivity
拷贝c代码去实现方法
extern "C"
JNIEXPORT void JNICALL
Java_com_alex_voicechangeapp_MainActivity_voiceChangeNative(JNIEnv *env, jobject thiz, jint mode, jstring path) {
char *content_ = "默认 播放完毕";
const char *path_ = env->GetStringUTFChars(path, NULL);
System *system = 0;
Sound *sound = 0;
Channel *channel = 0;
// DSP:digital signal process == 数字信号处理 指针
DSP *dsp = 0;
// TODO 第一步 创建系统
System_Create(&system);
// TODO 第二步 系统的初始化 参数1:最大音轨数, 参数2:系统初始化标记, 参数3:额外数据
system->init(32, FMOD_INIT_NORMAL, 0);
// TODO 第三步 创建声音 参数1:路径, 参数2:声音初始化标记, 参数3:额外数据, 参数4:声音指针
system->createSound(path_, FMOD_DEFAULT, 0, &sound);
// TODO 第四步:播放声音 音轨 声音
// 参数1:声音, 参数2:分组音轨, 参数3:控制, 参数4:通道
system->playSound(sound, 0, false, &channel);
// TODO 第五步:增加特效
switch (mode) {
case com_alex_voicechangeapp_MainActivity_MODE_NORMAL: // 原生
content_ = "原生 播放完毕";
break;
case com_alex_voicechangeapp_MainActivity_MODE_LOLITA: // 萝莉
content_ = "萝莉 播放完毕";
// 音调高 -- 萝莉 2.0
// 1.创建DSP类型的Pitch 音调条件
system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
// 2.设置Pitch音调调节2.0
dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 2.0f);
// 3.添加音效进去 音轨
channel->addDSP(0, dsp);
break;
case com_alex_voicechangeapp_MainActivity_MODE_UNCLE: // 大叔
content_ = "大叔 播放完毕";
// 音调低 -- 大叔 0.7
// 1.创建DSP类型的Pitch 音调条件
system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
// 2.设置Pitch音调调节2.0
dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 0.7f);
// 3.添加音效进去 音轨
channel->addDSP(0, dsp);
break;
case com_alex_voicechangeapp_MainActivity_MODE_FUNNY: // 搞怪
content_ = "搞怪 小黄人 播放完毕";
// 小黄人声音 频率快
// 从音轨拿 当前 频率
float mFrequency;
channel->getFrequency(&mFrequency);
// 修改频率
channel->setFrequency(mFrequency * 1.5f); // 频率加快 小黄人的声音
break;
case com_alex_voicechangeapp_MainActivity_MODE_HORROR: // 惊悚
content_ = "惊悚 播放完毕";
// 惊悚音效:特点: 很多声音的拼接
// TODO 音调低
// 音调低 -- 大叔 0.7
// 1.创建DSP类型的Pitch 音调条件
system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
// 2.设置Pitch音调调节2.0
dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 0.7f);
// 3.添加音效进去 音轨
channel->addDSP(0, dsp); // 第一个音轨
// TODO 搞点回声
// 回音 ECHO
system->createDSPByType(FMOD_DSP_TYPE_ECHO, &dsp);
dsp->setParameterFloat(FMOD_DSP_ECHO_DELAY, 200); // 回音 延时 to 5000. Default = 500.
dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK, 10); // 回音 衰减度 Default = 50 0 完全衰减了
channel->addDSP(1, dsp); // 第二个音轨
// TODO 颤抖
// Tremolo 颤抖音 正常5 非常颤抖 20
system->createDSPByType(FMOD_DSP_TYPE_TREMOLO, &dsp);
dsp->setParameterFloat(FMOD_DSP_TREMOLO_FREQUENCY, 20); // 非常颤抖
dsp->setParameterFloat(FMOD_DSP_TREMOLO_SKEW, 0.8f); // ???
channel->addDSP(2, dsp); // 第三个音轨
break;
case com_alex_voicechangeapp_MainActivity_MODE_INTANGIBLE: // 空灵 学校广播
content_ = "空灵 播放完毕";
// 回音 ECHO
system->createDSPByType(FMOD_DSP_TYPE_ECHO, &dsp);
dsp->setParameterFloat(FMOD_DSP_ECHO_DELAY, 200); // 回音 延时 to 5000. Default = 500.
dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK, 10); // 回音 衰减度 Default = 50 0 完全衰减了
channel->addDSP(0, dsp);
break;
}
// 等待播放完毕 再回收
bool isPlayer = true;
while (isPlayer) {
channel->isPlaying(&isPlayer);
usleep(500 * 1000);
}
// 时时刻刻记得回收
sound->release();
system->close();
system->release();
env->ReleaseStringUTFChars(path, path_);
// 告知Java播放完毕
jclass mainCls = env->GetObjectClass(thiz);
jmethodID endMethod = env->GetMethodID(mainCls, "playerEnd", "(Ljava/lang/String;)V");
jstring value = env->NewStringUTF(content_);
env->CallVoidMethod(thiz, endMethod, value);
}
总结
这就是一整个java调C代码,C调java代码的比较初级的例子。通过Fmod提供的引擎可以比较轻松的实现变音效果。总体来说在技术上没有什么难度,唯一需要注意的就是C层的配置和声明都要小心一点,否则就直接崩溃。
参考
《享学课堂》Derry老师的NDK开发系列