前一段时间要做一个自动匹配与跑步节奏相适应的音乐播放器的应用。找了很久发现了十分良心的soundtouch开源库。
这个应用的步频判断部分是使用pedometer开源应用进行提取改造的。具体可以参考我相关的博客。
一、关于SoundTouch开源库
下面为官网,可以进行参考。
http://www.surina.net/soundtouch/index.html
该库主要提供了wav格式的音频处理和节奏检测,包含以下几个功能。
1.重新设定歌曲的bpm改变歌曲速度。
2.改变歌曲的音调二不改变速度。
3.直接加速或者减速使得音调和速度同时改变。
4.检测歌曲的bpm(beat per minute)
网站提供了两种资源的下载,一种是可执行文件soundstrech演示项目,可以使用命令行来运行子,一种是源代码,源码中有一个android的demo,需要先按照其中的readme进行cygwin-ndk编译以后才能生成so库在android上面运行,这是下文讲的重点。
先用可执行文件在命令行下面做了一个测试,发现其准确率和windows下的很准确切快速的mixMeister相当。而且是c/c++库,速度可观。
(之前找到了scala库,使用最高模式准确率达到mixMeister,但是速度感人,一首歌在pc上要1分钟左右能完成检测。感兴趣可以参见https://github.com/mziccard/scala-audio-file)
二、关于JNI编程
该库的文档十分亲民,一个是根目录下的readme,另一个是android-lib下面的readme。后者几乎是一个jni的快速入门。
这个库的主要思想是讲c代码编译成so共享库,然后通过jni的方式在java代码里面调用。工程里面有现成的make文件(配置要编译的文件,生成的参数和目标等等配置),我们只需要直接编译就可以了。
这里简述一下:
1.安装cygwin(一个windows下的linux模拟环境),确保下载了gnu的make.exe。如果忘记了后面用到的时候回提示。
2.配置home路径下的.bash_profile文件。(注意这里要使用vi或者vim进行编辑而不要在window环境中进行编辑,否则换行符无法识别。)把NDK路径加入进“linux”环境变量。
3.进入android-lib下的jni文件夹,发现有两个mk文件和一个cpp。mk文件里面就含有编译的配置信息。cpp是我们的编译目标,里面引用了一些这个文件夹之外的其他目录的库,不只是我们看到的这个。
4.在cygwin下进入这个目录。输入命令行 $NDK/ndk-build 。这里面$NDK的意思就是直接引用之前在.bash_profile里面配置的环境变量。ndk-build是ndk目录下的一个可执行文件。他会自动根据mk文件中的 设置来编译。
5.接着命令行中开始出现编译的过程。会生成arm armv7 等4个平台的so,在jni同级的libs文件夹里面的平台子文件里面。
6.直接在eclipse(或者as,需要进一步设置,因为原工程是按照eclipse配置的)import这个工程。包含so的libs文件夹会被包含进来。这时候连上手机直接运行就可以了。
我们会发现demo只使用了设定pitch和tempo生成新音乐的接口。而没有使用bpm检测的接口。这时候我们可以手动输入路径或者修改代码调用系统文件浏览器选择歌曲进行修改。
系统文件浏览器代码请参考我的文章:
http://blog.csdn.net/wallezhe/article/details/51614985
三、动手创建新的接口
这里面因为我对于jni的掌握还没有足够熟练,所以我注意模仿原来的函数。这里挑几点说一下:
1.参数的个数和函数的形式,使用javah命令的主要目的是获得接口的形式,.h文件在时机编译的时候并没有作用,只要有实现了接口的cpp文件就可以被java调用。如果足够熟练可以直接按照Java_包名_类名_方法名 的形式直接进行编写。而如果使用这种方式要注意参数的个数。无论有无java的实际参数传入,都是要传入c++代码JNIEnv *env, jobject thiz这两种类型的参数。前者是一个作为java类型和c桥梁的一个对象,可以调用其中的一些函数。后者至今没发现什么用处。但是一定要留出其位置。然后才是时机传入的参数。最好使用javah生成以防错误。
2.传入参数的形式。涉及到java类型和c++类型的转换。java传入的String对应的C++应该是jstring类型,而函数里面可能还会使用env->的方法将其转化为constant char*的类型。而float会编程c++里面的jfloat类型。具体请参照规范和手册。
3.返回值的形式要响应地转换回来,尤其是字符串需要调用env的方法进行转换。
4.重新编译一下,以为可能没有ide的提示,所以一些拼写错误或者忘记include的文件,按照cygwin里面的提示补充好就可以了。
5.将生成的新的so文件导入,修改一下android的ui显示,使得返回的float可以显示出来。
下面是我添加过的jni目录下的c++代码,以及java代码。这里面native 的java方法都是private声明然后使用public的驱壳进行调用是个不错的注意,如果出现问题方便进行修改以及一些native方法的组合。
四、效果
每首歌在android上也大概只需要不到8秒就可以识别完毕,同样非常准确。
下面的开发就是写一个数据库保存路径,文件名以及bpm的值。以及一个文件浏览器的添加入口。以及修改根据步频的播放机制就可以了。
五、展望
其实利用这个库可以开发一个一首歌实时根据步频调整播放速度的东西。
当然步频超过某一个范围应该直接滤掉。
1.变生成边播放。
2.播放的过程中以某一项为参照继续生成,其实以原来的文件就可以了。并直接切换。
3.可以实现,怎样调优使之节省资源就是很重要的了。
///
/// Example Interface class for SoundTouch native compilation
///
/// Author : Copyright (c) Olli Parviainen
/// Author e-mail : oparviai 'at' iki.fi
/// WWW : http://www.surina.net
///
//
// $Id: soundtouch-jni.cpp 212 2015-05-15 10:22:36Z oparviai $
//
#include <jni.h>
#include <android/log.h>
#include <stdexcept>
#include <string>
using namespace std;
#include "../../../include/SoundTouch.h"
#include "../source/SoundStretch/WavFile.h"
#include "../../../include/BPMDetect.h"
#define LOGV(...) __android_log_print((int)ANDROID_LOG_INFO, "SOUNDTOUCH", __VA_ARGS__)
//#define LOGV(...)
// String for keeping possible c++ exception error messages. Notice that this isn't
// thread-safe but it's expected that exceptions are special situations that won't
// occur in several threads in parallel.
static string _errMsg = "";
#define DLL_PUBLIC __attribute__ ((visibility ("default")))
#define BUFF_SIZE 4096
using namespace soundtouch;
// Set error message to return
static void _setErrmsg(const char *msg)
{
_errMsg = msg;
}
#ifdef _OPENMP
#include <pthread.h>
extern pthread_key_t gomp_tls_key;
static void * _p_gomp_tls = NULL;
/// Function to initialize threading for OpenMP.
///
/// This is a workaround for bug in Android NDK v10 regarding OpenMP: OpenMP works only if
/// called from the Android App main thread because in the main thread the gomp_tls storage is
/// properly set, however, Android does not properly initialize gomp_tls storage for other threads.
/// Thus if OpenMP routines are invoked from some other thread than the main thread,
/// the OpenMP routine will crash the application due to NULL pointer access on uninitialized storage.
///
/// This workaround stores the gomp_tls storage from main thread, and copies to other threads.
/// In order this to work, the Application main thread needws to call at least "getVersionString"
/// routine.
static int _init_threading(bool warn)
{
void *ptr = pthread_getspecific(gomp_tls_key);
LOGV("JNI thread-specific TLS storage %ld", (long)ptr);
if (ptr == NULL)
{
LOGV("JNI set missing TLS storage to %ld", (long)_p_gomp_tls);
pthread_setspecific(gomp_tls_key, _p_gomp_tls);
}
else
{
LOGV("JNI store this TLS storage");
_p_gomp_tls = ptr;
}
// Where critical, show warning if storage still not properly initialized
if ((warn) && (_p_gomp_tls == NULL))
{
_setErrmsg("Error - OpenMP threading not properly initialized: Call SoundTouch.getVersionString() from the App main thread!");
return -1;
}
return 0;
}
#else
static int _init_threading(bool warn)
{
// do nothing if not OpenMP build
return 0;
}
#endif
// Processes the sound file
static void _processFile(SoundTouch *pSoundTouch, const char *inFileName, const char *outFileName)
{
int nSamples;
int nChannels;
int buffSizeSamples;
SAMPLETYPE sampleBuffer[BUFF_SIZE];
// open input file
WavInFile inFile(inFileName);
int sampleRate = inFile.getSampleRate();
int bits = inFile.getNumBits();
nChannels = inFile.getNumChannels();
// create output file
WavOutFile outFile(outFileName, sampleRate, bits, nChannels);
pSoundTouch->setSampleRate(sampleRate);
pSoundTouch->setChannels(nChannels);
assert(nChannels > 0);
buffSizeSamples = BUFF_SIZE / nChannels;
// Process samples read from the input file
while (inFile.eof() == 0)
{
int num;
// Read a chunk of samples from the input file
num = inFile.read(sampleBuffer, BUFF_SIZE);
nSamples = num / nChannels;
// Feed the samples into SoundTouch processor
pSoundTouch->putSamples(sampleBuffer, nSamples);
// Read ready samples from SoundTouch processor & write them output file.
// NOTES:
// - 'receiveSamples' doesn't necessarily return any samples at all
// during some rounds!
// - On the other hand, during some round 'receiveSamples' may have more
// ready samples than would fit into 'sampleBuffer', and for this reason
// the 'receiveSamples' call is iterated for as many times as it
// outputs samples.
do
{
nSamples = pSoundTouch->receiveSamples(sampleBuffer, buffSizeSamples);
outFile.write(sampleBuffer, nSamples * nChannels);
} while (nSamples != 0);
}
// Now the input file is processed, yet 'flush' few last samples that are
// hiding in the SoundTouch's internal processing pipeline.
pSoundTouch->flush();
do
{
nSamples = pSoundTouch->receiveSamples(sampleBuffer, buffSizeSamples);
outFile.write(sampleBuffer, nSamples * nChannels);
} while (nSamples != 0);
}
extern "C" DLL_PUBLIC jstring Java_net_surina_soundtouch_SoundTouch_getVersionString(JNIEnv *env, jobject thiz)
{
const char *verStr;
LOGV("JNI call SoundTouch.getVersionString");
// Call example SoundTouch routine
verStr = SoundTouch::getVersionString();
/// gomp_tls storage bug workaround - see comments in _init_threading() function!
_init_threading(false);
int threads = 0;
#pragma omp parallel
{
#pragma omp atomic
threads ++;
}
LOGV("JNI thread count %d", threads);
// return version as string
return env->NewStringUTF(verStr);
}
extern "C" DLL_PUBLIC jlong Java_net_surina_soundtouch_SoundTouch_newInstance(JNIEnv *env, jobject thiz)
{
return (jlong)(new SoundTouch());
}
extern "C" DLL_PUBLIC void Java_net_surina_soundtouch_SoundTouch_deleteInstance(JNIEnv *env, jobject thiz, jlong handle)
{
SoundTouch *ptr = (SoundTouch*)handle;
delete ptr;
}
extern "C" DLL_PUBLIC void Java_net_surina_soundtouch_SoundTouch_setTempo(JNIEnv *env, jobject thiz, jlong handle, jfloat tempo)
{
SoundTouch *ptr = (SoundTouch*)handle;
ptr->setTempo(tempo);
}
extern "C" DLL_PUBLIC void Java_net_surina_soundtouch_SoundTouch_setPitchSemiTones(JNIEnv *env, jobject thiz, jlong handle, jfloat pitch)
{
SoundTouch *ptr = (SoundTouch*)handle;
ptr->setPitchSemiTones(pitch);
}
extern "C" DLL_PUBLIC void Java_net_surina_soundtouch_SoundTouch_setSpeed(JNIEnv *env, jobject thiz, jlong handle, jfloat speed)
{
SoundTouch *ptr = (SoundTouch*)handle;
ptr->setRate(speed);
}
extern "C" DLL_PUBLIC jstring Java_net_surina_soundtouch_SoundTouch_getErrorString(JNIEnv *env, jobject thiz)
{
jstring result = env->NewStringUTF(_errMsg.c_str());
_errMsg.clear();
return result;
}
extern "C" DLL_PUBLIC int Java_net_surina_soundtouch_SoundTouch_processFile(JNIEnv *env, jobject thiz, jlong handle, jstring jinputFile, jstring joutputFile)
{
SoundTouch *ptr = (SoundTouch*)handle;
const char *inputFile = env->GetStringUTFChars(jinputFile, 0);
const char *outputFile = env->GetStringUTFChars(joutputFile, 0);
LOGV("JNI process file %s", inputFile);
/// gomp_tls storage bug workaround - see comments in _init_threading() function!
if (_init_threading(true)) return -1;
try
{
_processFile(ptr, inputFile, outputFile);
}
catch (const runtime_error &e)
{
const char *err = e.what();
// An exception occurred during processing, return the error message
LOGV("JNI exception in SoundTouch::processFile: %s", err);
_setErrmsg(err);
return -1;
}
env->ReleaseStringUTFChars(jinputFile, inputFile);
env->ReleaseStringUTFChars(joutputFile, outputFile);
return 0;
}
extern "C" DLL_PUBLIC jfloat Java_net_surina_soundtouch_SoundTouch_getMyBPM(JNIEnv* env,jobject thiz, jstring inFileName )
{
const char *inFilechar = env->GetStringUTFChars(inFileName, 0);
WavInFile inFileobj(inFilechar);
WavInFile * inFile=&inFileobj;
float bpmValue;
int nChannels;
BPMDetect bpm(inFile->getNumChannels(), inFile->getSampleRate());
SAMPLETYPE sampleBuffer[BUFF_SIZE];
// detect bpm rate
fprintf(stderr, "Detecting BPM rate...");
fflush(stderr);
nChannels = (int)inFile->getNumChannels();
assert(BUFF_SIZE % nChannels == 0);
// Process the 'inFile' in small blocks, repeat until whole file has
// been processed
while (inFile->eof() == 0)
{
int num, samples;
// Read sample data from input file
num = inFile->read(sampleBuffer, BUFF_SIZE);
// Enter the new samples to the bpm analyzer class
samples = num / nChannels;
bpm.inputSamples(sampleBuffer, samples);
}
// Now the whole song data has been analyzed. Read the resulting bpm.
bpmValue = bpm.getBpm();
return jfloat(bpmValue);
}
SoundTouch.java
//function added by Marc
private native final float getMyBPM(String inputFile);
public float getBPM(String inputFile){
return getMyBPM(inputFile);
}