介绍
fmod是一款非常优秀的c++开源框架,这次就通过这个框架来实现仿QQ的变声效果,变声效果有6种,正常,萝莉,大叔,惊悚,搞怪,空灵。关于更多的效果自己感兴趣再去探索,套路大致相同。
项目效果图及其原理
项目采用Fmod开源库,一个非常简单通用的音频引擎,对原始声音进行音效的处理即可做出变声的效果,下面是变声音频的处理
- 原声:直接播放音频文件
- 萝莉:对音频提高八度
- 大叔:对音频减低八度
- 惊悚:增加音频的颤音
- 搞笑:增加音频的播放速度
- 空灵:增加音频的回音
运行
如果能将上次的fmod的示例程序在Android studio中跑起来了,这次运行也没有多大的问题。
本次使用的工具是 android studio 2.3.3,
步骤
1.下载fmod
到fmod的官网注册下载相应的fmod的Android版本。解压。得到如下
这次要用到的是api/lowlevel包下的inc下的文件和lib下的库
2.新建Android项目
通过as新建Android项目,比如这次我的项目名是QQVoice_change
然后将api/lowlevel下包的lib包下的 armeabi,x86文件及其文件下的.so库,fmod.jar 拷贝到项目的libs目录下。
将api/lowlevel包下的inc文件及其文件拷贝到项目的cpp目录下拷贝完成后如图所示
不用忘了拷过去之后要将fmod.jar编译到项目中。
3.修改build.gradle和CMakeLists.txt
很多人在android studio编译c/c++文件动态库,都死在了这一步,这一步需要有gradle和Cmake编程的基础才能真正的搞明白。这一步如果弄错会发现一编译会发生很多错误。
修改Builde.gradle
1、假如build.gradle
中没有配置ndk{ }
。那么当编译器在编译的时候将会编译所有平台的so库。而这里只导入了armeabi
和x86
的so文件,那么在编译其他的平台的so时,将找不到其他平台so文件而报错。因此需要在build.gradle
中配置ndk{ }
。
如果不在build.gradle
中配置ndk{ }
,那么就需要导入所有平台的so库。
在这个例子中只导入了armeabi
和x86
的so文件,如果直接编译将会报错。所以要在build.gradle
添加如下代码:
ndk {
abiFilters "armeabi","x86"
}
2.这里将so文件配置在了libs文件夹下,因此要在build.gradle
中配置:
sourceSets.main {
jniLibs.srcDirs = ['libs']
jni.srcDirs = []
}
修改后的build.gradle内容如下
android {
compileSdkVersion 26
buildToolsVersion "27.0.3"
defaultConfig {
applicationId "com.gxl.change"
minSdkVersion 15
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags ""
}
}
ndk {
abiFilters "armeabi","x86"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
sourceSets.main {
jniLibs.srcDirs = ['libs']
jni.srcDirs = []
}
}
修改CMakeLists.txt
由于这里编译需要fmod.so和fmodL.so,我们实现变声的cpp代码就放在native-lib.cpp中,生成的变声.so名称为changeVoice,所以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.4.1)
# 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.
#-----------------------------------------
find_library( log-lib
log )
set(my_lib_path ${CMAKE_SOURCE_DIR}/libs)
# 添加三方的so库
add_library(libfmod
SHARED
IMPORTED )
# 指名第三方库的绝对路径
set_target_properties( libfmod
PROPERTIES IMPORTED_LOCATION
${my_lib_path}/${ANDROID_ABI}/libfmod.so )
add_library(libfmodL
SHARED
IMPORTED )
set_target_properties( libfmodL
PROPERTIES IMPORTED_LOCATION
${my_lib_path}/${ANDROID_ABI}/libfmodL.so )
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")
#--------------------------------
add_library( # Sets the name of the library.
changeVoice
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp )
# 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.
# 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.
#---------------------
# 导入路径,为了让编译时能够寻找到这个文件夹
include_directories(src/main/cpp/inc)
# 链接好三个路径
target_link_libraries( changeVoice
libfmod
libfmodL
${log-lib} )
可以多看看这个文件,这个文件看多了后就会发现很多c/c++的库在as下运行套路大致相同.
4.实现变声的功能
新建EffectUtils.java文件,通过这个文件来处理变声,代码如下
package com.gxl.change;
/**
* Created by Administrator on 2018/7/10/010.
*/
public class EffectUtils {
//音效的类型
public static final int MODE_NORMAL = 0;//正常
public static final int MODE_LUOLI = 1;//萝莉
public static final int MODE_DASHU = 2;//大叔
public static final int MODE_JINGSONG = 3;//惊悚
public static final int MODE_GAOGUAI = 4;//搞怪
public static final int MODE_KONGLING = 5;//空灵
/**
* 变声
* @param path 声音路径
* @param type 变声类型
*/
public static native void fix(String path,int type);
static
{
System.loadLibrary("fmodL");
System.loadLibrary("fmod");
System.loadLibrary("changeVoice");
}
}
在android studio 2.2版本以上都可以自动生成头文件到native-lib.cpp中
5.通过c++实现功能
在native-lib中实现逻辑
#include <jni.h>
#include "inc/fmod.hpp"
#include <string>
#include <unistd.h>
using namespace FMOD;
#define MODE_NORMAL 0
#define MODE_LUOLI 1
#define MODE_DASHU 2
#define MODE_JINGSONG 3
#define MODE_GAOGUAI 4
#define MODE_KONGLING 5
#include <android/log.h>
#define LOGI(FORMAT, ...) __android_log_print(ANDROID_LOG_INFO,"jason",FORMAT,##__VA_ARGS__);
#define LOGE(FORMAT, ...) __android_log_print(ANDROID_LOG_ERROR,"jason",FORMAT,##__VA_ARGS__);
extern "C"
JNIEXPORT void JNICALL
Java_com_gxl_change_EffectUtils_fix(JNIEnv *env, jclass cls, jstring path_str, jint type) {
//这里很多都是二级指针,但是这里可以通过定义一级指针,下面通过引用来达到同样的效果
System * system;
Sound * sound;
Channel *channel;
DSP *dsp;
bool playing= true;
float frequency=1;
//初始化
System_Create(&system);
system->init(32, FMOD_INIT_NORMAL, NULL);
//将string转成char*
const char* path=env->GetStringUTFChars(path_str,NULL);
//创建声音
system->createSound(path,FMOD_DEFAULT, 0, &sound);
try {
//根据类型改变声音
switch (type) {
case MODE_NORMAL:
//正常声音
system->playSound(sound, 0, false, &channel);
break;
case MODE_LUOLI:
//萝莉
//DSP digital signal process
//dsp -> 音效 创建fmod中预定义好的音效
//FMOD_DSP_TYPE_PITCHSHIFT dsp,提升或者降低音调用的一种音效
system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
//提高音效
dsp->setParameterFloat(FMOD_DSP_TYPE_PITCHSHIFT, 2.5);
//播放声音
system->playSound(sound, 0, false, &channel);
//将channel添加到dsp
channel->addDSP(0, dsp);
break;
case MODE_DASHU:
//大叔
system->createDSPByType(FMOD_DSP_TYPE_PITCHSHIFT, &dsp);
//降低声音
dsp->setParameterFloat(FMOD_DSP_PITCHSHIFT_PITCH, 0.8);
//播放声音
system->playSound(sound, 0, false, &channel);
//将channel添加到dsp
channel->addDSP(0, dsp);
break;
case MODE_JINGSONG:
//惊悚
system->createDSPByType(FMOD_DSP_TYPE_TREMOLO, &dsp);
dsp->setParameterFloat(FMOD_DSP_TREMOLO_SKEW, 0.5);
system->playSound(sound, 0, false, &channel);
channel->addDSP(0, dsp);
break;
case MODE_GAOGUAI:
//搞怪
//提高说话的速度
system->playSound(sound, 0, false, &channel);
//frequency 原来的声音速度
channel->getFrequency(&frequency);
frequency = frequency * 1.6;
channel->setFrequency(frequency);
LOGI("%s", "fix gaoguai");
break;
case MODE_KONGLING:
//空灵
system->createDSPByType(FMOD_DSP_TYPE_ECHO, &dsp);
dsp->setParameterFloat(FMOD_DSP_ECHO_DELAY, 300);
dsp->setParameterFloat(FMOD_DSP_ECHO_FEEDBACK, 20);
system->playSound(sound, 0, false, &channel);
channel->addDSP(0, dsp);
LOGI("%s", "fix kongling");
break;
default:
break;
}
}
catch (...){
//捕捉异常
LOGE("%s","发生异常");
}
system->update();
//释放资源
//单位是微秒
//每秒钟判断下是否在播放
while(playing){
channel->isPlaying(&playing);
usleep(1000 * 1000);
}
//释放
sound->release();
system->close();
system->release();
env->ReleaseStringUTFChars(path_str,path);
}
上面就是关于声音的处理,可以看到套路大致相同,最难的就是改变的音效的调节,如果需要更多音效,自行百度。我也只是将老师讲的例子敲一遍,其中的如何改变成什么样的音效还是不太清楚。可能可以参考源码中的例子吧,可能会有些麻烦.
6.编写界面处理
在MainActivity.java中实现界面的点击事件
public class QQActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
FMOD.init(this);
setContentView(R.layout.activity_main);
}
public void mFix1(View btn) {
Toast.makeText(this, "变声", Toast.LENGTH_SHORT).show();
String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separatorChar + "gxl.wav";
EffectUtils.fix(path, EffectUtils.MODE_NORMAL);
Log.d("jason", "mFix");
}
public void mFix2(View btn) {
Toast.makeText(this, "变声", Toast.LENGTH_SHORT).show();
String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separatorChar + "gxl.wav";
EffectUtils.fix(path, EffectUtils.MODE_LUOLI);
Log.d("jason", "mFix");
}
public void mFix3(View btn) {
Toast.makeText(this, "变声", Toast.LENGTH_SHORT).show();
String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separatorChar + "gxl.wav";
EffectUtils.fix(path, EffectUtils.MODE_DASHU);
Log.d("jason", "mFix");
}
public void mFix4(View btn) {
Toast.makeText(this, "变声", Toast.LENGTH_SHORT).show();
String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separatorChar + "gxl.wav";
EffectUtils.fix(path, EffectUtils.MODE_JINGSONG);
Log.d("jason", "mFix");
}
public void mFix5(View btn) {
Toast.makeText(this, "变声", Toast.LENGTH_SHORT).show();
String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separatorChar + "gxl.wav";
EffectUtils.fix(path, EffectUtils.MODE_GAOGUAI);
Log.d("jason", "mFix");
}
public void mFix6(View btn) {
Toast.makeText(this, "变声", Toast.LENGTH_SHORT).show();
String path = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separatorChar + "gxl.wav";
EffectUtils.fix(path, EffectUtils.MODE_KONGLING);
Log.d("jason", "mFix");
}
@Override
protected void onDestroy() {
super.onDestroy();
FMOD.close();
}
}
界面如下
activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:orientation="vertical"
android:background="#FFF"
android:paddingBottom="20dp" >
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="20dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/btn_record"
style="@style/AudioImgStyle"
android:src="@drawable/record"
android:onClick="mFix1"/>
<TextView
style="@style/AudioTextStyle"
android:text="原声"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/btn_luoli"
style="@style/AudioImgStyle"
android:src="@drawable/luoli"
android:onClick="mFix2"/>
<TextView
style="@style/AudioTextStyle"
android:text="萝莉"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/btn_dashu"
style="@style/AudioImgStyle"
android:src="@drawable/dashu"
android:onClick="mFix3"/>
<TextView
style="@style/AudioTextStyle"
android:text="大叔"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="20dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/btn_jingsong"
style="@style/AudioImgStyle"
android:src="@drawable/jingsong"
android:onClick="mFix4"/>
<TextView
style="@style/AudioTextStyle"
android:text="惊悚"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/btn_gaoguai"
style="@style/AudioImgStyle"
android:src="@drawable/gaoguai"
android:onClick="mFix5"/>
<TextView
style="@style/AudioTextStyle"
android:text="搞怪"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:id="@+id/btn_kongling"
style="@style/AudioImgStyle"
android:src="@drawable/kongling"
android:onClick="mFix6"/>
<TextView
style="@style/AudioTextStyle"
android:text="空灵"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</RelativeLayout>