目录
一、前言
本部分将AudioRecorder(封装AudioRecord)采集的pcm数据通过局域网传输,并通过局域网接收pcm数据,之后送给AudioPlayer(封装AudioTrack)进行播放。
二、创建udp socket发送接收线程
启动两个线程,一个线程将AudioRecorder采集的pcm数据通过udp socket发送,另一个线程监听udp socket,达到数据后将数据送给AudioPlayer进行播放。
发送线程:
new Thread(new Runnable() {
@Override
public void run() {
final int bufferSize = AudioRecorder.BUFFER_SIZE;
byte[] buffer = new byte[bufferSize];
try {
// 创建一个数据报套接字对象
DatagramSocket socket = new DatagramSocket();
// 创建一个InetAddress对象,用于指定目标IP地址
InetAddress IPAddress = InetAddress.getByName(remoteIp);
while (calling) {
int bytesRead = audioRecorder.readRecord(buffer, bufferSize);
if(bytesRead > 0){
//将数据写到缓存
//writeToRecorderBuffer(buffer, bytesRead);
// 发送数据
// 将要发送的数据打包
//String data = "Hello, UDP Server!";
//byte[] sendData = data.getBytes();
// 创建一个DatagramPacket对象,包含了要发送的信息
int numPacket = bytesRead / 1000;
for(int i = 0; i < numPacket; i++){
byte[] sendData = new byte[1000];
System.arraycopy(buffer, i*1000, sendData, 0, 1000);
DatagramPacket sendPacket = new DatagramPacket(sendData, 1000, IPAddress, 9999);
socket.send(sendPacket);
}
int remainLength = bytesRead % 1000;
if(remainLength > 0){
byte[] sendData = new byte[remainLength];
System.arraycopy(buffer, numPacket*1000, sendData, 0, remainLength);
DatagramPacket sendPacket = new DatagramPacket(sendData, remainLength, IPAddress, 9999);
socket.send(sendPacket);
}
}
}
// 关闭套接字
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
接收线程:
new Thread(new Runnable() {
@Override
public void run() {
try {
DatagramSocket socket = new DatagramSocket(9999);
byte[] receiveData = new byte[1024];
// 创建一个数据包对象,用于接收数据
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
while (calling) {
// 接收数据
socket.receive(receivePacket);
byte[] pcmData = receivePacket.getData();
if (pcmData != null) {
int pcmLength = receivePacket.getLength();
audioPlayer.play(pcmData, pcmLength);
}
//从缓存中获取数据
//byte[] pcmData = getFromRecorderBuffer();
//if (pcmData != null) {
// audioPlayer.play(pcmData, pcmData.length);
//}
}
socket.close();
}catch (Exception e){
e.printStackTrace();
}
}
}).start();
三、效果展示
四、优化后的版本
将udp socket封装成一个类(UdpIo),并在类内开启发送、接收线程,同时在AudioRecorder和AudioPlayer开启线程进行采集和发送。每个线程的数据出入都有缓存,且通过锁保证线程安全。该方式不是最好的方式,目的只是暂时为了架构清晰一些,实际使用可设计更好的架构。
UdpIo:
package com.example.ffmpegtestprj;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.sql.PreparedStatement;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.ReentrantLock;
public class UdpIo {
private DatagramSocket socketRecv;
private DatagramSocket socketSend;
private String remoteIp = "127.0.0.1";
private int remotePort = 9999;
private int localPort = 9999;
private InetAddress remoteIpAddress;
private boolean recving = false;
private boolean sending = false;
private ReentrantLock queueSendLock = new ReentrantLock();
private Queue<byte[]> sendQueue = new LinkedList<>();
private ReentrantLock queueRecvLock = new ReentrantLock();
private Queue<byte[]> recvQueue = new LinkedList<>();
private void initReceiver(int port){
localPort = port;
try{
// 创建一个数据报套接字对象
socketRecv = new DatagramSocket(localPort);
}catch (Exception e) {
e.printStackTrace();
}
}
private void initSender(String ip, int port){
remoteIp = ip;
remotePort = port;
try{
// 创建一个数据报套接字对象
socketSend = new DatagramSocket();
// 创建一个InetAddress对象,用于指定目标IP地址
remoteIpAddress = InetAddress.getByName(remoteIp);
}catch (Exception e) {
e.printStackTrace();
}
}
public void startRecving(int port){
if(recving){
return;
}else {
recving = true;
initReceiver(port);
}
new Thread(new Runnable() {
@Override
public void run() {
try {
byte[] receiveData = new byte[1024];
// 创建一个数据包对象,用于接收数据
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
while (recving) {
// 接收数据
socketRecv.receive(receivePacket);
byte[] data = receivePacket.getData();
if (data != null) {
int pcmLength = receivePacket.getLength();
writeToRecvQueue(data, pcmLength);
}
}
socketRecv.close();
}catch (Exception e){
e.printStackTrace();
}
}
}).start();
}
public void stopRecving(){
recving = false;
}
public void startSending(String ip, int port){
if(sending){
return;
}else {
sending = true;
initSender(ip, port);
}
new Thread(new Runnable() {
@Override
public void run() {
try {
while (sending) {
//1、从缓存中获取rtp包
byte[] dataToSend = getFromSendQueue();
if(dataToSend != null){
if(dataToSend.length > 1000){
int pktNum = dataToSend.length / 1000;
int remainDataLength = dataToSend.length % 1000;
for(int i = 0; i < pktNum; ++i){
byte[] tmpData = new byte[1000];
System.arraycopy(dataToSend, i * 1000, tmpData, 0, 1000);
DatagramPacket sendPacket = new DatagramPacket(tmpData, tmpData.length, remoteIpAddress, remotePort);
socketSend.send(sendPacket);
}
if(remainDataLength > 0){
byte[] tmpData = new byte[1000];
System.arraycopy(dataToSend, pktNum * 1000, tmpData, 0, remainDataLength);
DatagramPacket sendPacket = new DatagramPacket(tmpData, tmpData.length, remoteIpAddress, remotePort);
socketSend.send(sendPacket);
}
}
else{
// 创建一个DatagramPacket对象,包含了要发送的信息
DatagramPacket sendPacket = new DatagramPacket(dataToSend, dataToSend.length, remoteIpAddress, remotePort);
socketSend.send(sendPacket);
}
}
}
// 关闭套接字
socketSend.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
public void stopSending(){
sending = false;
}
private void writeToRecvQueue(byte[] data, int length){
//锁
byte[] dataToSave = new byte[length];
System.arraycopy(data, 0, dataToSave, 0, length);
queueRecvLock.lock();
recvQueue.offer(dataToSave);
queueRecvLock.unlock();
}
public byte[] getFromRecvQueue(){
//锁
queueRecvLock.lock();
if(!recvQueue.isEmpty()){
byte[] data = recvQueue.poll();
queueRecvLock.unlock();
return data;
}
queueRecvLock.unlock();
return null;
}
public void writeToSendQueue(byte[] data){
//锁
queueSendLock.lock();
sendQueue.offer(data);
queueSendLock.unlock();
}
private byte[] getFromSendQueue(){
//锁
queueSendLock.lock();
if(!sendQueue.isEmpty()){
byte[] data = sendQueue.poll();
queueSendLock.unlock();
return data;
}
queueSendLock.unlock();
return null;
}
}
AudioRecorder:
package com.example.ffmpegtestprj;
import android.content.pm.PackageManager;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.util.Log;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.ReentrantLock;
public class AudioRecorder {
private ReentrantLock pcmQueueLock = new ReentrantLock();
private Queue<byte[]> pcmQueue = new LinkedList<>();
private boolean recording = false;
private AudioRecord audioRecord;
private static final String TAG = "AudioCapture";
private static final int PERMISSION_REQUEST_CODE = 200;
private static final int SAMPLE_RATE = 44100;
private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
public static final int BUFFER_SIZE = AudioRecord.getMinBufferSize(
SAMPLE_RATE,
CHANNEL_CONFIG,
AUDIO_FORMAT);
private void initRecording() {
audioRecord = new AudioRecord(
MediaRecorder.AudioSource.MIC,
SAMPLE_RATE,
CHANNEL_CONFIG,
AUDIO_FORMAT,
BUFFER_SIZE);
if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
Log.e(TAG, "音频录制器初始化失败");
return;
}
}
private void startRecording() {
audioRecord.startRecording();
}
public void start(){
if(!recording){
recording = true;
}else{
return;
}
initRecording();
startRecording();
new Thread(new Runnable() {
@Override
public void run() {
final int bufferSize = AudioRecorder.BUFFER_SIZE;
try {
while (recording) {
byte[] buffer = new byte[bufferSize];
int bytesRead = audioRecord.read(buffer, 0, bufferSize);
if(bytesRead > 0){
//将数据写到缓存
writeToRecorderBuffer(buffer, bytesRead);
}
//睡眠10ms
//Thread.sleep(10);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
public void stop() {
if(recording){
recording = false;
audioRecord.stop();
audioRecord.release();
}
}
private void writeToRecorderBuffer(byte[] buffer, int size){
byte[] pcmData = new byte[size];
System.arraycopy(buffer, 0, pcmData, 0, size);
//锁
pcmQueueLock.lock();
pcmQueue.offer(pcmData);
pcmQueueLock.unlock();
}
public byte[] getFromRecorderBuffer() {
//锁
pcmQueueLock.lock();
if(!pcmQueue.isEmpty()){
byte[] pcmData = pcmQueue.poll();
pcmQueueLock.unlock();
return pcmData;
}
pcmQueueLock.unlock();
return null;
}
}
AudioPlayer:
package com.example.ffmpegtestprj;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.ReentrantLock;
public class AudioPlayer {
private boolean inited = false;
private boolean playing = false;
private ReentrantLock pcmQueueLock = new ReentrantLock();
private Queue<byte[]> pcmQueue = new LinkedList<>();
private int sampleRateInHz = 44100;
private int channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
private int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
private int bufferSizeInBytes = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
private AudioTrack audioTrack;
private void init() {
audioTrack = new AudioTrack(
AudioManager.STREAM_MUSIC, // 指定音频流类型
sampleRateInHz, // 采样率,例如44100Hz
channelConfig, // 声道配置,例如AudioFormat.CHANNEL_OUT_MONO
audioFormat, // 数据格式,例如AudioFormat.ENCODING_PCM_16BIT
bufferSizeInBytes, // 缓冲区大小,例如1024字节
AudioTrack.MODE_STREAM // 播放模式,使用流模式**
);
}
public void start() {
if(!inited){
inited = true;
init();
}
if (audioTrack.getState() != AudioTrack.STATE_UNINITIALIZED) {
audioTrack.play();
}
if(!playing){
playing = true;
}else{
return;
}
new Thread(new Runnable() {
@Override
public void run() {
while (playing) {
try {
byte[] pcmData = getFromRecorderBuffer();
if (pcmData != null) {
play(pcmData, pcmData.length);
}
//Thread.sleep(10);//睡眠10ms
} catch (Exception e) {
e.printStackTrace();
}
}
}
}).start();
}
public void stop() {
playing = false;
try {
if (audioTrack.getState() != AudioTrack.STATE_UNINITIALIZED) {
audioTrack.stop();
audioTrack.release();
inited = false;
}
} catch (IllegalStateException e) {
e.printStackTrace();
}
}
public void writeToRecorderBuffer(byte[] buffer, int size){
byte[] pcmData = new byte[size];
System.arraycopy(buffer, 0, pcmData, 0, size);
//锁
pcmQueueLock.lock();
pcmQueue.offer(pcmData);
pcmQueueLock.unlock();
}
private byte[] getFromRecorderBuffer() {
//锁
pcmQueueLock.lock();
if(!pcmQueue.isEmpty()){
byte[] pcmData = pcmQueue.poll();
pcmQueueLock.unlock();
return pcmData;
}
pcmQueueLock.unlock();
return null;
}
private void play(byte[] buffer, int size){
if(size > 0 ) {
audioTrack.write(buffer, 0, size);
}
}
}
MainActivity:
package com.example.ffmpegtestprj;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.view.View;
import com.example.ffmpegtestprj.databinding.ActivityMainBinding;
import java.io.File;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.sql.Struct;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.ReentrantLock;
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
// Used to load the 'ffmpegtestprj' library on application startup.
static {
System.loadLibrary("ffmpegtestprj");
}
/**
* A native method that is implemented by the 'ffmpegtestprj' native library,
* which is packaged with this application.
*/
public native String fetchPcmFromMp4();
public native int getPcm(byte[] data, int tmpPcmLen);
public native void mp4ToMp3(String url);
private ActivityMainBinding binding;
private AudioRecorder audioRecorder;
private AudioPlayer audioPlayer;
private UdpIo udpIo;
private boolean calling = false;
EditText ipText;
private String remoteIp = "127.0.0.1";
private ReentrantLock queueLock = new ReentrantLock();
private Queue<byte[]> pcmQueue = new LinkedList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
initDevice();
initView();
}
private void initDevice(){
audioRecorder = new AudioRecorder();
audioPlayer = new AudioPlayer();
udpIo = new UdpIo();
}
private void initView(){
//初始化控件
Button btnDecode = (Button) findViewById(R.id.btnGetPcm);
Button btnPlay = (Button) findViewById(R.id.btnPlayPcm);
Button btnCall = (Button) findViewById(R.id.btnCall);
Button btnHangUp = (Button) findViewById(R.id.btnHangUp);
//注册按钮事件监听器
btnDecode.setOnClickListener(this);
btnPlay.setOnClickListener(this);
btnCall.setOnClickListener(this);
btnHangUp.setOnClickListener(this);
}
@Override
public void onClick(View view) {
if(view.getId() == R.id.btnGetPcm){
decodeAudio();
}
if(view.getId() == R.id.btnPlayPcm){
startPlayPcm();
}
if(view.getId() == R.id.btnCall){
ipText = findViewById(R.id.editText);
remoteIp = ipText.getText().toString();
startCall(remoteIp, 9999);
}
if(view.getId() == R.id.btnHangUp){
stopCall();
}
}
private void decodeAudio(){
AlertDialog.Builder dialog = new AlertDialog.Builder(MainActivity.this);
dialog.setTitle("HINT!!!");
String str = fetchPcmFromMp4();
dialog.setMessage("find mp4 file OK, get audio:"+str);
dialog.show();
}
private void startPlayPcm(){
int sampleRateInHz = 44100;
int channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
int bufferSizeInBytes = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
AudioTrack audioTrack = new AudioTrack(
AudioManager.STREAM_MUSIC, // 指定音频流类型
sampleRateInHz, // 采样率,例如44100Hz
channelConfig, // 声道配置,例如AudioFormat.CHANNEL_OUT_MONO
audioFormat, // 数据格式,例如AudioFormat.ENCODING_PCM_16BIT
bufferSizeInBytes, // 缓冲区大小,例如1024字节
AudioTrack.MODE_STREAM // 播放模式,使用流模式**
);
if ( audioTrack.getState() != AudioTrack.STATE_UNINITIALIZED) {
audioTrack.play();
}
byte[] data = new byte[bufferSizeInBytes];
while(true){
int pcmLen = getPcm(data,bufferSizeInBytes);
if(pcmLen > 0){
audioTrack.write(data, 0, pcmLen);
}else{
break;
}
}
if (audioTrack.getState() != AudioTrack.STATE_UNINITIALIZED) {
audioTrack.stop();
audioTrack.release();
}
}
private void startCall(String ip, int port){
if(calling == true) {
return;
}
calling = true;
audioPlayer.start();
udpIo.startSending(ip, port);
udpIo.startRecving(port);
audioRecorder.start();
new Thread(new Runnable() {
@Override
public void run() {
final int bufferSize = AudioRecorder.BUFFER_SIZE;
try {
while (calling) {
byte[] pcmRecord = audioRecorder.getFromRecorderBuffer();
if(pcmRecord != null){
udpIo.writeToSendQueue(pcmRecord);
}
byte[] pcmRecv = udpIo.getFromRecvQueue();
if(pcmRecv != null){
audioPlayer.writeToRecorderBuffer(pcmRecv, pcmRecv.length);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
private void stopCall(){
if(calling == true) {
calling = false;
audioRecorder.stop();
udpIo.stopRecving();
udpIo.stopSending();
audioPlayer.stop();
}
}
}