android MediaCodec 音频解码+讯飞语音 实现本地语音转文字功能
因项目需要,需求是:把语音消息转换成文本,功能类似微信的语音转文字。于是下载了科大讯飞的语音sdk,研究使用。怎样把本地的音频转成文字,讯飞的开发文档上很清楚,这里就不说明了,直接调用讯飞的方法就好。
但是问题来了:
因项目录制的语音消息,保存的格式AMR格式,因为发送消息,如果保存的格式是wav的则文件太大,所以才保存成AMR格式。但是讯飞语音,进行音频识别,只支持wav格式,或者pcm格式的,于是就牵扯到音频格式的转换。由于个人技术还不到位,所以在网上搜索资料,虽然有一些ffmpeg这样类似的格式转换工具,也曾尝试过,但是结果都不理想,其他相关的内容,能够帮到我的很少,不过还好,最终找到一篇利用MediaCodec 进行解码的博客(搜索关键字很重要,一开始徒劳了太久),仔细研究一下,参照着,修改了一下,原博客地址 http://blog.csdn.net/TinsanMr/article/details/51049179
博主写的是mp3->PCM->aac,而我不需要进行二次编码成aac,所以只需要拿到pcm数据就够了。
改好后,运行也成功了,但是使用的测试机是5.0以下的,当时也没注意这个,当公司测试说魅族无法成功转写文字,我以为是和机型有关,后来自己的手机也不行,这就不对了,然后恍然大悟,和手机系统有关?因为我的手机和出现问题的魅族测试机都是Android 5.0以上的系统,于是又测试了几次,果然是和系统版本有关。
只能再从网上查查相关资料了。看到了一篇博客,博客地址 http://blog.csdn.net/zgcqflqinhao/article/details/52525697?locationNum=6&fps=1,这篇博客的博主,用的也是上文提到的工具类,遇到了和我一样的问题,看完后才明白,原来,MediaCodec 的两个方法getInputBuffers()和getOutputBuffers(),适用于android 4.0以上5.0以下,而对于android 5.0以上的系统,从API 21开始就弃用了这两个方法,因为列中移除位置和输出缓冲区的限制将被设置为有效数据范围,所以不要使用这种方法,而选择用getInputBuffer(int
index)和getOutputBuffer(int index)。
修改后5.0系统还是没有结果,问题仍然出在解码上,然后参考了博客 http://blog.csdn.net/qq_24554061/article/details/52318622,最终成功了,真是一波三折。
好了,直接上代码。修改后,代码见:下面的封装工具类,可以直接使用—–AudioDecode
/**
* 本地AMR录音解码成PCM数据流
*/
public class AudioDecode {
private static final String TAG = "AudioDecode";
private String srcPath;//语音本地路径
private MediaCodec mediaDecode;
private MediaExtractor mediaExtractor;
private ByteBuffer[] decodeInputBuffers;
private ByteBuffer[] decodeOutputBuffers;
private MediaCodec.BufferInfo decodeBufferInfo;
private ArrayList<byte[]> chunkPCMDataContainer;//PCM数据
private OnCompleteListener onCompleteListener;
private boolean codeOver = false;//解码结束
private Thread decoderThread;
public static AudioDecode newInstance() {
return new AudioDecode();
}
/**
* 设置要读取的文件位置
*
* @param srcPath
*/
public void setFilePath(String srcPath) {
this.srcPath = srcPath;
}
/**
* 准备工作
*/
public void prepare() {
if (srcPath == null || "".equas(srcPath)) {//其实这个路径在使用转码前就已经判断本地文件是否存在了,这个判断可有可无
throw new IllegalArgumentException("srcPath can't be null");
}
chunkPCMDataContainer = new ArrayList<>();
initMediaDecode();//解码器
}
/**
* 初始化解码器
*/
private void initMediaDecode() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
mediaExtractor = new MediaExtractor();//此类可分离视频文件的音轨和视频轨道
mediaExtractor.setDataSource(srcPath);//媒体文件的位置
for (int i = 0; i < mediaExtractor.getTrackCount(); i++) {//遍历媒体轨道 此处我们传入的是音频文件,所以也就只有一条轨道
MediaFormat format = mediaExtractor.getTrackFormat(i);;
format.setInteger(MediaFormat.KEY_BIT_RATE, AudioFormat.ENCODING_PCM_16BIT);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("audio")) {//获取音频轨道
mediaExtractor.selectTrack(i);//选择此音频轨道
mediaDecode = MediaCodec.createDecoderByType(mime);//创建Decode解码器
mediaDecode.configure(format, null, null, 0);
break;
}
}
if (mediaDecode == null) {
Log.e(TAG, "create mediaDecode failed");
return;
}
mediaDecode.start();//启动MediaCodec ,等待传入数据
decodeInputBuffers = mediaDecode.getInputBuffers();//MediaCodec在此ByteBuffer[]中获取输入数据
decodeOutputBuffers = mediaDecode.getOutputBuffers();//MediaCodec将解码后的数据放到此ByteBuffer[]中 我们可以直接在这里面得到PCM数据
decodeBufferInfo = new MediaCodec.BufferInfo();//用于描述解码得到的byte[]数据的相关信息
showLog("buffers:" + decodeInputBuffers.length);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 开始转码
* 音频数据 解码成PCM,获取到PCM数组
*/
public void startAsync() {
decoderThread = new Thread(new DecodeRunnable());
decoderThread.start();
}
/**
* 将PCM数据存入{@link #chunkPCMDataContainer}
*
* @param pcmChunk PCM数据块
*/
private void putPCMData(byte[] pcmChunk) {
synchronized (AudioDecode.class) {//记得加锁
chunkPCMDataContainer.add(pcmChunk);
}
}
/**
* 在Container中{@link #chunkPCMDataContainer}取出PCM数据
*
* @return PCM数据块
*/
private byte[] getPCMData() {
synchronized (AudioDecode.class) {//记得加锁
if (chunkPCMDataContainer.isEmpty()) {
return null;
}
byte[] pcmChunk = chunkPCMDataContainer.get(0);//每次取出index 0 的数据
chunkPCMDataContainer.remove(pcmChunk);//取出后将此数据remove掉 既能保证PCM数据块的取出顺序 又能及时释放内存
return pcmChunk;
}
}
/**
* 解码{@link #srcPath}音频文件 得到PCM数据块
*
* @return 是否解码完所有数据
*/
private void srcAudioFormatToPCM() {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
if(decodeInputBuffers!=null){
try {
for (int i = 0; i < decodeInputBuffers.length - 1; i++) {
int inputIndex = 0;//获取可用的inputBuffer -1代表一直等待,0表示不等待 建议-1,避免丢帧
inputIndex = mediaDecode.dequeueInputBuffer(-1);
if (inputIndex < 0) {
codeOver = true;
return;
}
ByteBuffer inputBuffer = decodeInputBuffers[inputIndex];//拿到inputBuffer
inputBuffer.clear();//清空之前传入inputBuffer内的数据
int sampleSize = mediaExtractor.readSampleData(inputBuffer, 0);//MediaExtractor读取数据到inputBuffer中
if (sampleSize < 0) {//小于0 代表所有数据已读取完成
codeOver = true;
} else {
mediaDecode.queueInputBuffer(inputIndex, 0, sampleSize, 0, 0);//通知MediaDecode解码刚刚传入的数据
mediaExtractor.advance();//MediaExtractor移动到下一取样处
}
}
//获取解码得到的byte[]数据 参数BufferInfo上面已介绍 10000同样为等待时间 同上-1代表一直等待,0代表不等待。此处单位为微秒
//此处建议不要填-1 有些时候并没有数据输出,那么他就会一直卡在这 等待
int outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, 10000);
ByteBuffer outputBuffer;
byte[] chunkPCM;
while (outputIndex >= 0) {//每次解码完成的数据不一定能一次吐出 所以用while循环,保证解码器吐出所有数据
outputBuffer = decodeOutputBuffers[outputIndex];//拿到用于存放PCM数据的Buffer
chunkPCM = new byte[decodeBufferInfo.size];//BufferInfo内定义了此数据块的大小
outputBuffer.get(chunkPCM);//将Buffer内的数据取出到字节数组中
outputBuffer.clear();//数据取出后一定记得清空此Buffer MediaCodec是循环使用这些Buffer的,不清空下次会得到同样的数据
putPCMData(chunkPCM);//自己定义的方法,供编码器所在的线程获取数据,下面会贴出代码
mediaDecode.releaseOutputBuffer(outputIndex, false);//此操作一定要做,不然MediaCodec用完所有的Buffer后 将不能向外输出数据
outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, 10000);//再次获取数据,如果没有数据输出则outputIndex=-1 循环结束
}
if(codeOver){
if (onCompleteListener != null) {
onCompleteListener.completed(chunkPCMDataContainer);
}
}
}catch (Exception e){
e.printStackTrace();
codeOver = true;
if (onCompleteListener != null) {
onCompleteListener.completed(chunkPCMDataContainer);
}
}
}
}
}
/**
* android 5.0以上
*/
private void srcAudioFormatToPCMHigherApi() {
if (android.os.Build.VERSION.SDK_INT >= 21){
boolean sawOutputEOS = false;
final long kTimeOutUs = 10000;
long presentationTimeUs = 0;
while (!sawOutputEOS){
try{
int inputIndex = mediaDecode.dequeueInputBuffer(-1);
if (inputIndex >= 0){
ByteBuffer inputBuffer = mediaDecode.getInputBuffer(inputIndex);
if(inputBuffer!=null){
int sampleSize = mediaExtractor.readSampleData(inputBuffer, 0);
if (sampleSize < 0) {// 小于0 代表所有数据已读取完成
sawOutputEOS = true;
codeOver = true;
break;
}else{
presentationTimeUs = mediaExtractor.getSampleTime();
mediaDecode.queueInputBuffer(inputIndex, 0, sampleSize, presentationTimeUs, 0);// 通知MediaDecode解码刚刚传入的数据
mediaExtractor.advance();
}
}
}else{
sawOutputEOS = true;
codeOver = true;
}
int outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, kTimeOutUs);
ByteBuffer outputBuffer ;//= mediaDecode.getOutputBuffer(outputIndex);// 拿到用于存放PCM数据的Buffer
while (outputIndex >= 0){
outputBuffer = mediaDecode.getOutputBuffer(outputIndex);
boolean doRender = (decodeBufferInfo.size != 0);
if(doRender && outputBuffer!=null){
outputBuffer.position(decodeBufferInfo.offset);
outputBuffer.limit(decodeBufferInfo.offset + decodeBufferInfo.size);
byte[] chunkPCM = new byte[decodeBufferInfo.size];// BufferInfo内定义了此数据块的大小
outputBuffer.get(chunkPCM);
outputBuffer.clear();// 数据取出后一定记得清空此Buffer MediaCodec是循环使用这些Buffer的,不清空下次会得到同样的数据
putPCMData(chunkPCM);// 自己定义的方法,供编码器所在的线程获取数据,下面会贴出代码
mediaDecode.releaseOutputBuffer(outputIndex, false);// 此操作一定要做,不然MediaCodec用完所有的Buffer后将不能向外输出数据
outputIndex = mediaDecode.dequeueOutputBuffer(decodeBufferInfo, kTimeOutUs);
}
}
}catch (Exception e){
e.printStackTrace();
sawOutputEOS = true;
codeOver = true;
}
}
if(codeOver){
if (onCompleteListener != null) {
onCompleteListener.completed(chunkPCMDataContainer);
}
}
}
}
/**
* 释放资源
*/
public void release() {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN){
try{
if(decoderThread!=null && decoderThread.isAlive()){
decoderThread.interrupt();
codeOver = true;
}
if (mediaDecode != null) {
mediaDecode.stop();
mediaDecode.release();
mediaDecode = null;
}
if (mediaExtractor != null) {
mediaExtractor.release();
mediaExtractor = null;
}
if (onCompleteListener != null) {
onCompleteListener = null;
}
}catch (Exception e){
e.printStackTrace();
}
}
}
/**
* 解码线程
*/
private class DecodeRunnable implements Runnable {
@Override
public void run() {
while (!codeOver) {
if(Build.VERSION.SDK_INT>=21){
srcAudioFormatToPCMHigherApi();
}else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN){
srcAudioFormatToPCM();
}
}
}
}
/**
* 解码完成回调接口
*/
public interface OnCompleteListener {
void completed(ArrayList<byte[]> chunkPCMDataContainer);
}
/**
* 设置转码完成监听器
* @param onCompleteListener 监听器
*/
public void setOnCompleteListener(OnCompleteListener onCompleteListener) {
this.onCompleteListener = onCompleteListener;
}
private void showLog(String msg) {
Log.e("AudioCodec", msg);
}
}
调用方法
这里只列出在Activity中主要的使用部分,其他的都是基本的控件使用还有讯飞的相关方法调用就不列了。
public class AudioToTextActivity extends BaseActivity{
private AudioDecode audioDecode;
//语音转换
private void startTranslate(){
int ret = mIat.startListening(mRecognizerListener);
if (ret != ErrorCode.SUCCESS) {
LogUtil.d(TAG+"--->识别失败,错误码:" + ret);
}else{
try {
audioDecode = AudioDecode.newInstance();
audioDecode.setFilePath(audioPath);
audioDecode.prepare();
audioDecode.setOnCompleteListener(new AudioDecode.OnCompleteListener() {
@Override
public void completed(final ArrayList<byte[]> pcmData) {
if(pcmData!=null){
//写入音频文件数据,数据格式必须是采样率为8KHz或16KHz(本地识别只支持16K采样率,云端都支持),位长16bit,单声道的wav或者pcm
//必须要先保存到本地,才能被讯飞识别
for (byte[] data : pcmData){
mIat.writeAudio(data, 0, data.length);
}
mIat.stopListening();
}else{
mIat.cancel();
LogUtil.d(TAG+"--->读取音频流失败");
}
audioDecode.release();
}
});
audioDecode.startAsync();
}catch (Exception e){
e.printStackTrace();
}
}
}
}
好了,一个类似微信,语音转文字的功能就实现了。不过还有点小缺陷,因为讯飞本地语音转文字,最大支持60s的,而且技术人员的建议是最长40s左右,这样的成功率较高,而且,这个工具类,在Android 5.0以上的系统,40s左右的语音在转写时经常出现网络异常错误,30s左右以及以下基本没问题。不过蛮期待讯飞的新产品:语音转文字,因为这个产品支持长语音了!!
第一次写博客,不喜勿喷,哈哈~
另附上Demo,可以参考,链接如下
https://github.com/Alvin9234/SpeechDemo