需求:实现多人即时语音聊天室
实现方式:android端实现AudioRecord实时记录原生pcm流的原始录音数据,并通过websocket实时传输到后端,再由后端实时将pcm流数据分发给每个用户,用户接收到pcm流数据后通过AudioTrack进行实时播放。
注意事项:audioRecord的各种基础配置需要和audioTrack的基础配置项相对应。
扩展:需要做声音降噪,或者变声处理的,可在拿到pcm录音原始数据后进行相关的处理
1.录音端代码
private Boolean doStart() {
try {
startRecordTime = System.currentTimeMillis();
//创建MediaRecorder
//计算AudioRecord内部buffer最小缓冲区大小
bufferSize = AudioRecord.getMinBufferSize(44100,
AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
44100, AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT, Math.max(bufferSize,BUFFER_SIZE));
//记录开始录音时间 用于统计时长 时间太短就是无效录音
stopRecordTime = System.currentTimeMillis();
//开始录音
audioRecord.startRecording();
//获取录音的二进制数据 并使用websocket进行数据传输;
while(isStartRecord){
if (null != audioRecord) {
int read = audioRecord.read(mBuffer,0,BUFFER_SIZE);
if (read == AudioRecord.ERROR_INVALID_OPERATION || read == AudioRecord.ERROR_BAD_VALUE) {
Log.d("NLPService", "Could not read audio data.");
break;
}
if (read != 0 && read != -1) {
//在此可以对录制音频的数据进行二次处理 比如变声,压缩,降噪,增益等操作
//我们这里直接将pcm音频原数据写入文件 这里可以直接发送至服务器 对方采用AudioTrack进行播放原数据
// dos.write(mBuffer, 0, read);
// dos.flush();
broadcastRoomService.send(ByteString.of(mBuffer));
} else {
break;
}
}
}
}catch (RuntimeException e){
e.printStackTrace();
//捕获异常避免闪退 返回false提醒用户失败
return false;
}
return true;
}
2.webSocketService端
public class WebSocketService extends Service {
private static final String TAG = "websocket";
private WebSocket webSocket;
private WebSocketCallback webSocketCallback;
private int reconnectTimeout = 2000;
private boolean connected = false;
private int limitConnect = 20;
private int timeConnect = 0;
private Handler handler = new Handler();
public class LocalBinder extends Binder {
public WebSocketService getService() {
return WebSocketService.this;
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return new LocalBinder();
}
@Override
public void onCreate() {
super.onCreate();
webSocket = connect();
initConnectCount();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
// if (webSocket != null) {
// close();
// }
reconnect();
}
private WebSocket connect() {
SharedPreferences userInfo = getSharedPreferences("userInfo", MODE_PRIVATE);
String currentUserId = userInfo.getString("userId",null);
OkHttpClient client = new OkHttpClient.Builder().build();
Request request = new Request.Builder().url(GlobalVariable.getWsUrl()+"imserver/"+currentUserId).
addHeader("Content-Type","application/json").build();
return client.newWebSocket(request, new WebSocketHandler());
}
public void send(String text) {
Log.d(TAG, "send " + text);
if (webSocket != null) {
webSocket.send(text);
}
}
public void close() {
if (webSocket != null) {
boolean shutDownFlag = webSocket.close(1000, "manual close");
Log.d(TAG, "shutDownFlag " + shutDownFlag);
webSocket = null;
}
}
public void initConnectCount(){
limitConnect = 20;
timeConnect = 0;
}
private void reconnect() {
if(limitConnect>0) {
limitConnect--;
timeConnect++;
handler.postDelayed(new Runnable() {
@Override
public void run() {
Log.d(TAG, "reconnect...第"+timeConnect+"次");
if (!connected) {
connect();
}
}
}, reconnectTimeout);
}else {
close();
}
}
private class WebSocketHandler extends WebSocketListener {
@Override
public void onOpen(WebSocket webSocket, Response response) {
Log.d(TAG, "onOpen");
if (webSocketCallback != null) {
webSocketCallback.onOpen();
}
connected = true;
}
@Override
public void onMessage(WebSocket webSocket, String text) {
Log.d(TAG, "onMessage " + text);
if (webSocketCallback != null) {
webSocketCallback.onMessage(text);
}
}
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
Log.d(TAG, "onClosed");
if (webSocketCallback != null) {
webSocketCallback.onClosed();
}
connected = false;
reconnect();
}
/**
* Invoked when a web socket has been closed due to an error reading from or writing to the
* network. Both outgoing and incoming messages may have been lost. No further calls to this
* listener will be made.
*/
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
Log.d(TAG, "onFailure " + t.getMessage());
connected = false;
reconnect();
}
}
/**
* 只暴露需要的回调给页面,onFailure 你给了页面
*/
public interface WebSocketCallback {
void onMessage(String text);
void onOpen();
void onClosed();
}
public void setWebSocketCallback(WebSocketCallback webSocketCallback) {
this.webSocketCallback = webSocketCallback;
}
}
3.audioTrack播放接收到的二进制流数据
private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
broadcastRoomService = ((BroadcastRoomService.LocalBinder) service).getService();
broadcastRoomService.setBroadcastCallback(broadcastCallback);
}
@Override
public void onServiceDisconnected(ComponentName name) {
broadcastRoomService = null;
}
};
private BroadcastRoomService.BroadcastCallback broadcastCallback = new BroadcastRoomService.BroadcastCallback() {
@Override
public void onMessage(final ByteString message) {
//播放录音
executorService.submit(new Runnable() {
@Override
public void run() {
Log.e("message",message.toString());
if(!message.toString().contains("OK"))
voicePlayer.setDataSource(message.toByteArray());
}
});
}
@Override
public void onOpen() {
runOnUiThread(new Runnable() {
@Override
public void run() {
//可以做在线方面的
// tvMessage.setText("onOpen");
broadcastRoomService.initConnectCount();
}
});
}
@Override
public void onClosed() {
runOnUiThread(new Runnable() {
@Override
public void run() {
broadcastRoomService.close();
// tvMessage.setText("onClosed");
}
});
}
};
public class AudioParam {
private int mFrequency; //采样率
private int mChannel; //声道
private int mSampleBit; //采样精度
public int getmFrequency() {
return mFrequency;
}
public void setmFrequency(int mFrequency) {
this.mFrequency = mFrequency;
}
public int getmChannel() {
return mChannel;
}
public void setmChannel(int mChannel) {
this.mChannel = mChannel;
}
public int getmSampleBit() {
return mSampleBit;
}
public void setmSampleBit(int mSampleBit) {
this.mSampleBit = mSampleBit;
}
}
public class AudioUtil
{
private static AudioUtil mInstance;
private AudioRecord recorder;
//声音源
private static int audioSource = MediaRecorder.AudioSource.MIC;
//录音的采样频率
private static int audioRate = 44100;
//录音的声道,单声道
private static int audioChannel = AudioFormat.CHANNEL_IN_STEREO;
//量化的精度
private static int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
//缓存的大小
private static int bufferSize = AudioRecord.getMinBufferSize(audioRate , audioChannel , audioFormat);
//记录播放状态
private boolean isRecording = false;
//数字信号数组
private byte[] noteArray;
//PCM文件
private File pcmFile;
//wav文件
private File wavFile;
//文件输出流
private OutputStream os;
//文件根目录
private String basePath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/record/";
//wav文件目录
private String outFileName = basePath + "/encode.wav";
//pcm文件目录
private String inFileName = basePath + "/encode.pcm";
private AudioUtil()
{
//创建文件
createFile();
recorder = new AudioRecord(audioSource , audioRate ,
audioChannel , audioFormat , bufferSize);
}
//创建文件夹,首先创建目录,然后创建对应的文件
private void createFile()
{
File baseFile = new File(basePath);
if (!baseFile.exists())
baseFile.mkdirs();
pcmFile = new File(basePath + "/encode.pcm");
wavFile = new File(basePath + "/encode.wav");
if (pcmFile.exists())
pcmFile.delete();
if (wavFile.exists())
wavFile.delete();
try
{
pcmFile.createNewFile();
wavFile.createNewFile();
}
catch (IOException e)
{
e.printStackTrace();
}
}
public synchronized static AudioUtil getInstance()
{
if (mInstance == null)
{
mInstance = new AudioUtil();
}
return mInstance;
}
//读取录音数字数据线程
class WriteThread implements Runnable
{
@Override
public void run()
{
writeData();
}
}
//录音线程执行体
private void writeData()
{
noteArray = new byte[bufferSize];
//建立文件输出流
try
{
os = new BufferedOutputStream(new FileOutputStream(pcmFile));
}
catch (FileNotFoundException e)
{
e.printStackTrace();
}
while (isRecording)
{
int recordSize = recorder.read(noteArray , 0 , bufferSize);
if (recordSize > 0)
{
try
{
os.write(noteArray);
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
if (os != null)
{
try
{
os.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
//开始录音
public void startRecord()
{
isRecording = true;
recorder.startRecording();
}
//记录数据
public void recordData()
{
new Thread(new WriteThread()).start();
}
//停止录音
public void stopRecord()
{
if (recorder != null)
{
isRecording = false;
recorder.stop();
recorder.release();
}
}
//将pcm文件转换为wav文件
public void convertWavFile()
{
FileInputStream in = null;
FileOutputStream out = null;
long totalAudioLen;
long totalDataLen;
long longSampleRate = AudioUtil.audioRate;
int channels = 2;
long byteRate = 16 * AudioUtil.audioRate * channels / 8;
byte[] data = new byte[bufferSize];
try
{
in = new FileInputStream(inFileName);
out = new FileOutputStream(outFileName);
totalAudioLen = in.getChannel().size();
//由于不包括RIFF和WAV
totalDataLen = totalAudioLen + 36;
writeWaveFileHeader(out , totalAudioLen , totalDataLen , longSampleRate , channels , byteRate);
while (in.read(data) != -1)
{
out.write(data);
}
in.close();
out.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
public class VoicePlayer
{
private AudioTrack mAudioTrack; //AudioTrack对象
private AudioPlayThread mAudioPlayThread = null;
private AudioParam mAudioParam; //音频参数
private int mPrimePlaySize = 0; //较优播放块大小
private static final int BUFFER_SIZE = 2048;
//设置音频参数
public void setAudioParam(AudioParam audioParam)
{
mAudioParam = audioParam;
}
private void createAudioTrack()
{
if (mAudioTrack == null)
{
// 获得构建对象的最小缓冲区大小
int minBufSize = AudioTrack.getMinBufferSize(mAudioParam.getmFrequency() ,
mAudioParam.getmChannel() , mAudioParam.getmSampleBit());
mPrimePlaySize = minBufSize;
mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC , mAudioParam.getmFrequency() ,
mAudioParam.getmChannel() , mAudioParam.getmSampleBit() , Math.max(mPrimePlaySize,BUFFER_SIZE), AudioTrack.MODE_STREAM);
}
}
private void releaseAudioTrack()
{
if (mAudioTrack != null)
{
mAudioTrack.stop();
mAudioTrack.release();
mAudioTrack = null;
}
}
//就绪播放源
public void prepare()
{
if (mAudioParam != null)
{
createAudioTrack();
}
}
public void play()
{
if (mAudioPlayThread == null)
{
mAudioPlayThread = new AudioPlayThread();
mAudioPlayThread.start();
if (mAudioPlayThread.mPlayHandler == null)
{
try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}
private void stop()
{
if (mAudioPlayThread != null)
{
mAudioPlayThread = null;
}
}
public void release()
{
stop();
releaseAudioTrack();
}
//设置音频源
public void setDataSource(byte[] data)
{
if (mAudioPlayThread.mPlayHandler != null)
{
Message message = mAudioPlayThread.mPlayHandler.obtainMessage();
message.what = 0x123;
message.obj = data;
mAudioPlayThread.mPlayHandler.sendMessage(message);
}
}
class AudioPlayThread extends Thread
{
private Handler mPlayHandler;
@Override
public void run()
{
mAudioTrack.play();
Looper.prepare();
mPlayHandler = new Handler()
{
@Override
public void handleMessage(Message msg)
{
if (msg.what == 0x123)
{
mAudioTrack.write((byte[]) msg.obj, 0, ((byte[]) msg.obj).length);
}
}
};
Looper.loop();
}
}
}