您的位置:首页 > 移动开发 > Android开发

Android音频实时传输与播放(三):AMR硬编码与硬解码

2016-07-07 02:13 603 查看
2012-07-16 22:17
原文链接:http://blog.csdn.net/zgyulongfei/article/details/7753163

Android中我所知道的音频编解码有两种方式:

(一)使用AudioRecord采集音频,用这种方式采集的是未经压缩的音频流;用AudioTrack播放实时音频流。用这两个类的话,如果需要对音频进行编解码,就需要自己移植编解码库了,比如可以移植ilbc,speex等开源编解码库。

ilbc的编解码实现可以查看这个专栏:http://blog.csdn.net/column/details/media.html

(二)使用MediaRecorder获取编码后的AMR音频,但由于MediaRecorder的特点,只能将流保存到文件中,但通过其他方式是可以获取到实时音频流的,这篇文章将介绍用LocalSocket的方法来实现;使用MediaPlayer来播放AMR音频流,但同样MediaPlayer也只能播放文件流,因此我用缓存的方式来播放音频。

以上两种方式各有利弊,使用方法(一)需移植编解码库,但可以播放实时音频流;使用方法(二)直接硬编硬解码效率高,但是需要对文件进行操作。

PS:这篇文章只是给大家一个参考,仅供学习之用,如果真正用到项目中还有很多地方需要优化。

我强烈推荐播放音频时候用方法(一),方法(二)虽然能够实现功能,但是实现方式不太好。

接下来看代码:

编码器:

[java] view
plain copy

package cn.edu.xmu.zgy.audio.encoder;

import java.io.DataInputStream;

import java.io.IOException;

import java.net.DatagramPacket;

import java.net.DatagramSocket;

import java.net.InetAddress;

import cn.edu.xmu.zgy.config.CommonConfig;

import android.app.Activity;

import android.media.MediaRecorder;

import android.net.LocalServerSocket;

import android.net.LocalSocket;

import android.net.LocalSocketAddress;

import android.util.Log;

import android.widget.Toast;

//blog.csdn.net/zgyulongfei

//Email: zgyulongfei@gmail.com

public class AmrAudioEncoder {

private static final String TAG = "ArmAudioEncoder";

private static AmrAudioEncoder amrAudioEncoder = null;

private Activity activity;

private MediaRecorder audioRecorder;

private boolean isAudioRecording;

private LocalServerSocket lss;

private LocalSocket sender, receiver;

private AmrAudioEncoder() {

}

public static AmrAudioEncoder getArmAudioEncoderInstance() {

if (amrAudioEncoder == null) {

synchronized (AmrAudioEncoder.class) {

if (amrAudioEncoder == null) {

amrAudioEncoder = new AmrAudioEncoder();

}

}

}

return amrAudioEncoder;

}

public void initArmAudioEncoder(Activity activity) {

this.activity = activity;

isAudioRecording = false;

}

public void start() {

if (activity == null) {

showToastText("音频编码器未初始化,请先执行init方法");

return;

}

if (isAudioRecording) {

showToastText("音频已经开始编码,无需再次编码");

return;

}

if (!initLocalSocket()) {

showToastText("本地服务开启失败");

releaseAll();

return;

}

if (!initAudioRecorder()) {

showToastText("音频编码器初始化失败");

releaseAll();

return;

}

this.isAudioRecording = true;

startAudioRecording();

}

private boolean initLocalSocket() {

boolean ret = true;

try {

releaseLocalSocket();

String serverName = "armAudioServer";

final int bufSize = 1024;

lss = new LocalServerSocket(serverName);

receiver = new LocalSocket();

receiver.connect(new LocalSocketAddress(serverName));

receiver.setReceiveBufferSize(bufSize);

receiver.setSendBufferSize(bufSize);

sender = lss.accept();

sender.setReceiveBufferSize(bufSize);

sender.setSendBufferSize(bufSize);

} catch (IOException e) {

ret = false;

}

return ret;

}

private boolean initAudioRecorder() {

if (audioRecorder != null) {

audioRecorder.reset();

audioRecorder.release();

}

audioRecorder = new MediaRecorder();

audioRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);

audioRecorder.setOutputFormat(MediaRecorder.OutputFormat.RAW_AMR);

final int mono = 1;

audioRecorder.setAudioChannels(mono);

audioRecorder.setAudioSamplingRate(8000);

audioRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);

audioRecorder.setOutputFile(sender.getFileDescriptor());

boolean ret = true;

try {

audioRecorder.prepare();

audioRecorder.start();

} catch (Exception e) {

releaseMediaRecorder();

showToastText("手机不支持录音此功能");

ret = false;

}

return ret;

}

private void startAudioRecording() {

new Thread(new AudioCaptureAndSendThread()).start();

}

public void stop() {

if (isAudioRecording) {

isAudioRecording = false;

}

releaseAll();

}

private void releaseAll() {

releaseMediaRecorder();

releaseLocalSocket();

amrAudioEncoder = null;

}

private void releaseMediaRecorder() {

try {

if (audioRecorder == null) {

return;

}

if (isAudioRecording) {

audioRecorder.stop();

isAudioRecording = false;

}

audioRecorder.reset();

audioRecorder.release();

audioRecorder = null;

} catch (Exception err) {

Log.d(TAG, err.toString());

}

}

private void releaseLocalSocket() {

try {

if (sender != null) {

sender.close();

}

if (receiver != null) {

receiver.close();

}

if (lss != null) {

lss.close();

}

} catch (IOException e) {

e.printStackTrace();

}

sender = null;

receiver = null;

lss = null;

}

private boolean isAudioRecording() {

return isAudioRecording;

}

private void showToastText(String msg) {

Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show();

}

private class AudioCaptureAndSendThread implements Runnable {

public void run() {

try {

sendAmrAudio();

} catch (Exception e) {

Log.e(TAG, "sendAmrAudio() 出错");

}

}

private void sendAmrAudio() throws Exception {

DatagramSocket udpSocket = new DatagramSocket();

DataInputStream dataInput = new DataInputStream(receiver.getInputStream());

skipAmrHead(dataInput);

final int SEND_FRAME_COUNT_ONE_TIME = 10;// 每次发送10帧的数据,1帧大约32B

// AMR格式见博客:http://blog.csdn.net/dinggo/article/details/1966444

final int BLOCK_SIZE[] = { 12, 13, 15, 17, 19, 20, 26, 31, 5, 0, 0, 0, 0, 0, 0, 0 };

byte[] sendBuffer = new byte[1024];

while (isAudioRecording()) {

int offset = 0;

for (int index = 0; index < SEND_FRAME_COUNT_ONE_TIME; ++index) {

if (!isAudioRecording()) {

break;

}

dataInput.read(sendBuffer, offset, 1);

int blockIndex = (int) (sendBuffer[offset] >> 3) & 0x0F;

int frameLength = BLOCK_SIZE[blockIndex];

readSomeData(sendBuffer, offset + 1, frameLength, dataInput);

offset += frameLength + 1;

}

udpSend(udpSocket, sendBuffer, offset);

}

udpSocket.close();

dataInput.close();

releaseAll();

}

private void skipAmrHead(DataInputStream dataInput) {

final byte[] AMR_HEAD = new byte[] { 0x23, 0x21, 0x41, 0x4D, 0x52, 0x0A };

int result = -1;

int state = 0;

try {

while (-1 != (result = dataInput.readByte())) {

if (AMR_HEAD[0] == result) {

state = (0 == state) ? 1 : 0;

} else if (AMR_HEAD[1] == result) {

state = (1 == state) ? 2 : 0;

} else if (AMR_HEAD[2] == result) {

state = (2 == state) ? 3 : 0;

} else if (AMR_HEAD[3] == result) {

state = (3 == state) ? 4 : 0;

} else if (AMR_HEAD[4] == result) {

state = (4 == state) ? 5 : 0;

} else if (AMR_HEAD[5] == result) {

state = (5 == state) ? 6 : 0;

}

if (6 == state) {

break;

}

}

} catch (Exception e) {

Log.e(TAG, "read mdat error...");

}

}

private void readSomeData(byte[] buffer, int offset, int length, DataInputStream dataInput) {

int numOfRead = -1;

while (true) {

try {

numOfRead = dataInput.read(buffer, offset, length);

if (numOfRead == -1) {

Log.d(TAG, "amr...no data get wait for data coming.....");

Thread.sleep(100);

} else {

offset += numOfRead;

length -= numOfRead;

if (length <= 0) {

break;

}

}

} catch (Exception e) {

Log.e(TAG, "amr..error readSomeData");

break;

}

}

}

private void udpSend(DatagramSocket udpSocket, byte[] buffer, int sendLength) {

try {

InetAddress ip = InetAddress.getByName(CommonConfig.SERVER_IP_ADDRESS.trim());

int port = CommonConfig.AUDIO_SERVER_UP_PORT;

byte[] sendBuffer = new byte[sendLength];

System.arraycopy(buffer, 0, sendBuffer, 0, sendLength);

DatagramPacket packet = new DatagramPacket(sendBuffer, sendLength);

packet.setAddress(ip);

packet.setPort(port);

udpSocket.send(packet);

} catch (IOException e) {

e.printStackTrace();

}

}

}

}

关于编码器:前面提到了,MediaRecorder的硬编码的方式只能将码流保存到文件中,这里用了LocalSocket的方式将流保存到内存中,然后从缓冲中读取码流。由于保存的格式RAW_AMR格式的,因此需要对读取到的数据进行解析,从而获得真正的音频流。想了解AMR音频码流格式的,可以查看代码中附上的网页链接。由于压缩过的码流很小,因此我在实现的时候,组合了int SEND_FRAME_COUNT_ONE_TIME
= 10帧的码流后才往外发送,这样的方式造成的延迟会加重,大家可以根据自己的需要进行修改。造成延迟的另一因素是LocalSocket缓冲的大小,在这里我设置的大小是final int bufSize = 1024;代码写的很清楚详细,有疑问的可以提出。

播放器:

[java] view
plain copy

package cn.edu.xmu.zgy.audio.player;

import java.io.BufferedInputStream;

import java.io.BufferedOutputStream;

import java.io.File;

import java.io.FileInputStream;

import java.io.FileOutputStream;

import java.io.IOException;

import java.io.InputStream;

import java.net.InetAddress;

import java.net.Socket;

import cn.edu.xmu.zgy.config.CommonConfig;

import android.app.Activity;

import android.media.MediaPlayer;

import android.os.Handler;

import android.util.Log;

//blog.csdn.net/zgyulongfei

//Email: zgyulongfei@gmail.com

public class AmrAudioPlayer {

private static final String TAG = "AmrAudioPlayer";

private static AmrAudioPlayer playerInstance = null;

private long alreadyReadByteCount = 0;

private MediaPlayer audioPlayer;

private Handler handler = new Handler();

private final String cacheFileName = "audioCacheFile";

private File cacheFile;

private int cacheFileCount = 0;

// 用来记录是否已经从cacheFile中复制数据到另一个cache中

private boolean hasMovedTheCacheFlag;

private boolean isPlaying;

private Activity activity;

private boolean isChaingCacheToAnother;

private AmrAudioPlayer() {

}

public static AmrAudioPlayer getAmrAudioPlayerInstance() {

if (playerInstance == null) {

synchronized (AmrAudioPlayer.class) {

if (playerInstance == null) {

playerInstance = new AmrAudioPlayer();

}

}

}

return playerInstance;

}

public void initAmrAudioPlayer(Activity activity) {

this.activity = activity;

deleteExistCacheFile();

initCacheFile();

}

private void deleteExistCacheFile() {

File cacheDir = activity.getCacheDir();

File[] needDeleteCacheFiles = cacheDir.listFiles();

for (int index = 0; index < needDeleteCacheFiles.length; ++index) {

File cache = needDeleteCacheFiles[index];

if (cache.isFile()) {

if (cache.getName().contains(cacheFileName.trim())) {

Log.e(TAG, "delete cache file: " + cache.getName());

cache.delete();

}

}

}

needDeleteCacheFiles = null;

}

private void initCacheFile() {

cacheFile = null;

cacheFile = new File(activity.getCacheDir(), cacheFileName);

}

public void start() {

isPlaying = true;

isChaingCacheToAnother = false;

setHasMovedTheCacheToAnotherCache(false);

new Thread(new NetAudioPlayerThread()).start();

}

public void stop() {

isPlaying = false;

isChaingCacheToAnother = false;

setHasMovedTheCacheToAnotherCache(false);

releaseAudioPlayer();

deleteExistCacheFile();

cacheFile = null;

handler = null;

}

private void releaseAudioPlayer() {

playerInstance = null;

if (audioPlayer != null) {

try {

if (audioPlayer.isPlaying()) {

audioPlayer.pause();

}

audioPlayer.release();

audioPlayer = null;

} catch (Exception e) {

}

}

}

private boolean hasMovedTheCacheToAnotherCache() {

return hasMovedTheCacheFlag;

}

private void setHasMovedTheCacheToAnotherCache(boolean result) {

hasMovedTheCacheFlag = result;

}

private class NetAudioPlayerThread implements Runnable {

// 从接受数据开始计算,当缓存大于INIT_BUFFER_SIZE时候开始播放

private final int INIT_AUDIO_BUFFER = 2 * 1024;

// 剩1秒的时候播放新的缓存的音乐

private final int CHANGE_CACHE_TIME = 1000;

public void run() {

try {

Socket socket = createSocketConnectToServer();

receiveNetAudioThenPlay(socket);

} catch (Exception e) {

Log.e(TAG, e.getMessage() + "从服务端接受音频失败。。。");

}

}

private Socket createSocketConnectToServer() throws Exception {

String hostName = CommonConfig.SERVER_IP_ADDRESS;

InetAddress ipAddress = InetAddress.getByName(hostName);

int port = CommonConfig.AUDIO_SERVER_DOWN_PORT;

Socket socket = new Socket(ipAddress, port);

return socket;

}

private void receiveNetAudioThenPlay(Socket socket) throws Exception {

InputStream inputStream = socket.getInputStream();

FileOutputStream outputStream = new FileOutputStream(cacheFile);

final int BUFFER_SIZE = 100 * 1024;// 100kb buffer size

byte[] buffer = new byte[BUFFER_SIZE];

// 收集了10*350b了之后才开始更换缓存

int testTime = 10;

try {

alreadyReadByteCount = 0;

while (isPlaying) {

int numOfRead = inputStream.read(buffer);

if (numOfRead <= 0) {

break;

}

alreadyReadByteCount += numOfRead;

outputStream.write(buffer, 0, numOfRead);

outputStream.flush();

try {

if (testTime++ >= 10) {

Log.e(TAG, "cacheFile=" + cacheFile.length());

testWhetherToChangeCache();

testTime = 0;

}

} catch (Exception e) {

// TODO: handle exception

}

// 如果复制了接收网络流的cache,则执行此操作

if (hasMovedTheCacheToAnotherCache() && !isChaingCacheToAnother) {

if (outputStream != null) {

outputStream.close();

outputStream = null;

}

// 将接收网络流的cache删除,然后重0开始存储

// initCacheFile();

outputStream = new FileOutputStream(cacheFile);

setHasMovedTheCacheToAnotherCache(false);

alreadyReadByteCount = 0;

}

}

} catch (Exception e) {

errorOperator();

e.printStackTrace();

Log.e(TAG, "socket disconnect...:" + e.getMessage());

throw new Exception("socket disconnect....");

} finally {

buffer = null;

if (socket != null) {

socket.close();

}

if (inputStream != null) {

inputStream.close();

inputStream = null;

}

if (outputStream != null) {

outputStream.close();

outputStream = null;

}

stop();

}

}

private void testWhetherToChangeCache() throws Exception {

if (audioPlayer == null) {

firstTimeStartPlayer();

} else {

changeAnotherCacheWhenEndOfCurrentCache();

}

}

private void firstTimeStartPlayer() throws Exception {

// 当缓存已经大于INIT_AUDIO_BUFFER则开始播放

if (alreadyReadByteCount >= INIT_AUDIO_BUFFER) {

Runnable r = new Runnable() {

public void run() {

try {

File firstCacheFile = createFirstCacheFile();

// 设置已经从cache中复制数据,然后会删除这个cache

setHasMovedTheCacheToAnotherCache(true);

audioPlayer = createAudioPlayer(firstCacheFile);

audioPlayer.start();

} catch (Exception e) {

Log.e(TAG, e.getMessage() + " :in firstTimeStartPlayer() fun");

} finally {

}

}

};

handler.post(r);

}

}

private File createFirstCacheFile() throws Exception {

String firstCacheFileName = cacheFileName + (cacheFileCount++);

File firstCacheFile = new File(activity.getCacheDir(), firstCacheFileName);

// 为什么不直接播放cacheFile,而要复制cacheFile到一个新的cache,然后播放此新的cache?

// 是为了防止潜在的读/写错误,可能在写入cacheFile的时候,

// MediaPlayer正试图读数据, 这样可以防止死锁的发生。

moveFile(cacheFile, firstCacheFile);

return firstCacheFile;

}

private void moveFile(File oldFile, File newFile) throws IOException {

if (!oldFile.exists()) {

throw new IOException("oldFile is not exists. in moveFile() fun");

}

if (oldFile.length() <= 0) {

throw new IOException("oldFile size = 0. in moveFile() fun");

}

BufferedInputStream reader = new BufferedInputStream(new FileInputStream(oldFile));

BufferedOutputStream writer = new BufferedOutputStream(new FileOutputStream(newFile,

false));

final byte[] AMR_HEAD = new byte[] { 0x23, 0x21, 0x41, 0x4D, 0x52, 0x0A };

writer.write(AMR_HEAD, 0, AMR_HEAD.length);

writer.flush();

try {

byte[] buffer = new byte[1024];

int numOfRead = 0;

Log.d(TAG, "POS...newFile.length=" + newFile.length() + " old=" + oldFile.length());

while ((numOfRead = reader.read(buffer, 0, buffer.length)) != -1) {

writer.write(buffer, 0, numOfRead);

writer.flush();

}

Log.d(TAG, "POS..AFTER...newFile.length=" + newFile.length());

} catch (IOException e) {

Log.e(TAG, "moveFile error.. in moveFile() fun." + e.getMessage());

throw new IOException("moveFile error.. in moveFile() fun.");

} finally {

if (reader != null) {

reader.close();

reader = null;

}

if (writer != null) {

writer.close();

writer = null;

}

}

}

private MediaPlayer createAudioPlayer(File audioFile) throws IOException {

MediaPlayer mPlayer = new MediaPlayer();

// It appears that for security/permission reasons, it is better to

// pass

// a FileDescriptor rather than a direct path to the File.

// Also I have seen errors such as "PVMFErrNotSupported" and

// "Prepare failed.: status=0x1" if a file path String is passed to

// setDataSource(). So unless otherwise noted, we use a

// FileDescriptor here.

FileInputStream fis = new FileInputStream(audioFile);

mPlayer.reset();

mPlayer.setDataSource(fis.getFD());

mPlayer.prepare();

return mPlayer;

}

private void changeAnotherCacheWhenEndOfCurrentCache() throws IOException {

// 检查当前cache剩余时间

long theRestTime = audioPlayer.getDuration() - audioPlayer.getCurrentPosition();

Log.e(TAG, "theRestTime=" + theRestTime + " isChaingCacheToAnother="

+ isChaingCacheToAnother);

if (!isChaingCacheToAnother && theRestTime <= CHANGE_CACHE_TIME) {

isChaingCacheToAnother = true;

Runnable r = new Runnable() {

public void run() {

try {

File newCacheFile = createNewCache();

// 设置已经从cache中复制数据,然后会删除这个cache

setHasMovedTheCacheToAnotherCache(true);

transferNewCacheToAudioPlayer(newCacheFile);

} catch (Exception e) {

Log.e(TAG, e.getMessage()

+ ":changeAnotherCacheWhenEndOfCurrentCache() fun");

} finally {

deleteOldCache();

isChaingCacheToAnother = false;

}

}

};

handler.post(r);

}

}

private File createNewCache() throws Exception {

// 将保存网络数据的cache复制到newCache中进行播放

String newCacheFileName = cacheFileName + (cacheFileCount++);

File newCacheFile = new File(activity.getCacheDir(), newCacheFileName);

Log.e(TAG, "before moveFile............the size=" + cacheFile.length());

moveFile(cacheFile, newCacheFile);

return newCacheFile;

}

private void transferNewCacheToAudioPlayer(File newCacheFile) throws Exception {

MediaPlayer oldPlayer = audioPlayer;

try {

audioPlayer = createAudioPlayer(newCacheFile);

audioPlayer.start();

} catch (Exception e) {

Log.e(TAG, "filename=" + newCacheFile.getName() + " size=" + newCacheFile.length());

Log.e(TAG, e.getMessage() + " " + e.getCause() + " error start..in transfanNer..");

}

try {

oldPlayer.pause();

oldPlayer.reset();

oldPlayer.release();

} catch (Exception e) {

Log.e(TAG, "ERROR release oldPlayer.");

} finally {

oldPlayer = null;

}

}

private void deleteOldCache() {

int oldCacheFileCount = cacheFileCount - 1;

String oldCacheFileName = cacheFileName + oldCacheFileCount;

File oldCacheFile = new File(activity.getCacheDir(), oldCacheFileName);

if (oldCacheFile.exists()) {

oldCacheFile.delete();

}

}

private void errorOperator() {

}

}

}

关于播放器:由于MediaPlayer的限制,我用了cache的方式来实现音频的实时播放。即把获取到的音频流首先保存到文件中,然后当保存到一定大小的时候就播放之,类似于QQ播放器那样有缓存的,只不过我这里的处理得挺粗糙。代码写的也挺详细了,如果有疑问也可以提出来。

注:编码器和播放器的编写,我都是站在巨人的肩膀上完成的,参考了一些其他资料。

在后面一篇文章中,我将附上服务器和客户端的所有代码。

希望朋友们看完能提出意见和建议,也希望看完能有所收获 ^_^
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: