在阅读这篇文章之前,最好可以看一下这篇文章WoMic虚拟麦克风技术剖析。
这篇文章介绍Womic的技术原理,因此这篇文章按照这个技术路线实现简单的WoMic。
WoMic由三个部分进行介绍:
1.虚拟声卡
2.PC端
3.Android 端
Womic采用自己开发的虚拟声卡,但是这里我们采用开源的虚拟声卡 Virtual Audio Cables,它是一款免费个人使用的虚拟声卡,它在虚拟麦克风中主要作用就是输入数据并从虚拟麦克中输出声音,应用就可以自由从虚拟声卡中读取数据。
自行从网站上下载并安装,我相信这一点不会难到你,若是安装有问题你回复评论。
Android端是用于录制声音的,也就是作为麦克风。Android 采用的AudioTrack获取声音的pcm流,采用的是tcp方式进行传输pcm流,不了解网络传输的可以参考这篇文章Android中socket(tcp|udp),websocket基本使用。
设置AudioTrack 频率,声道,码率等等,这些必须也pc端保持一致,不然出来的声音会是噪音。
public class AudioTrackUtil {
AudioRecord mAudioRecord;
private Integer mRecordBufferSize;
private boolean mWhetherRecord;
private void initMinBufferSize() {
//获取每一帧的字节流大小
mRecordBufferSize = AudioRecord.getMinBufferSize(8000
, AudioFormat.CHANNEL_IN_MONO
, AudioFormat.ENCODING_PCM_8BIT);
}
public AudioTrackUtil() {
initMinBufferSize();
if (mAudioRecord == null) {
mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC
, 8000
, AudioFormat.CHANNEL_IN_MONO
, AudioFormat.ENCODING_PCM_8BIT
, mRecordBufferSize);
}
}
public void startRecord() {
mWhetherRecord = true;
new Thread(new Runnable() {
@Override
public void run() {
mAudioRecord.startRecording();//开始录制
try {
byte[] bytes = new byte[mRecordBufferSize];
while (mWhetherRecord) {
mAudioRecord.read(bytes, 0, bytes.length);//读取流
mIAudioCallBack.callBack(bytes);
}
Log.e("test", "run: 暂停录制");
mAudioRecord.stop();//停止录制
} catch (Exception e) {
e.printStackTrace();
mWhetherRecord = false;
mAudioRecord.stop();
}
}
}).start();
}
void stopRecord() {
if (mAudioRecord != null) {
mAudioRecord.stop();
mAudioRecord.release();
}
}
public IAudioCallBack mIAudioCallBack;
public interface IAudioCallBack {
void callBack(byte[] o);
}
public void setIAudioCallBackListener(IAudioCallBack audioCallBack) {
mIAudioCallBack = audioCallBack;
}
}
public class MainActivity extends AppCompatActivity {
private Button mBtnSendMessage;
public Socket socket;
private AudioTrackUtil mAudioTrackUtil;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBtnSendMessage = findViewById(R.id.btn_start_send_message);
mAudioTrackUtil = new AudioTrackUtil();
final String ip = "192.168.40.57";//10.5.169.16服务端的IP地址;10.5.206.92
final int port = 5678;//自己定义个端口号要与服务端匹配。
startClient(ip, port);
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
}
mBtnSendMessage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (socket == null) {
startClient(ip, port);
}
if (socket.isConnected()) {
mAudioTrackUtil.startRecord();
mAudioTrackUtil.setIAudioCallBackListener(new AudioTrackUtil.IAudioCallBack() {
@Override
public void callBack(byte[] o) {
sendTcpMessage(o);
}
});
}
}
});
findViewById(R.id.btn_stop_send_message).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (socket != null) {
try {
sendTcpMessage("我要结束了".getBytes());
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
mAudioTrackUtil.startRecord();
}
public void startClient(final String address, final int port) {
if (address == null) {
return;
}
if (socket == null) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Log.i("tcp", "启动客户端");
socket = new Socket(address, port);
Log.i("tcp", "客户端连接成功");
InputStream inputStream = socket.getInputStream();
byte[] buffer = new byte[1024];
int len = -1;
while ((len = inputStream.read(buffer)) != -1) {
String data = new String(buffer, 0, len);
Log.i("tcp", "收到服务器的数据-------------------:" + data);
}
Log.i("tcp", "客户端断开连接");
} catch (Exception EE) {
EE.printStackTrace();
Log.i("tcp", "客户端无法连接服务器" + EE.getMessage());
} finally {
try {
if (socket != null) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
socket = null;
}
}
}).start();
}
}
public void sendTcpMessage(final byte[] msg) {
if (socket != null && socket.isConnected()) {
new Thread(new Runnable() {
@Override
public void run() {
try {
socket.getOutputStream().write(msg);
socket.getOutputStream().flush();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
try {
if (socket != null) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
mAudioTrackUtil.stopRecord();
}
}
第一步,获取虚拟声卡,需要设置采样频率,样本位数,声道等,需要和Android的参数一致,不然是噪音。
public class AudioVirtualCableUtile {
SourceDataLine auline = null;
public AudioVirtualCableUtile() {
AudioFormat.Encoding encoding = new AudioFormat.Encoding("PCM_SIGNED");
AudioFormat format = new AudioFormat(encoding,
8000,
16,
2,
2,
8000,
true);//编码格式,采样率,每个样本的位数,声道,帧长(字节),帧数,是否按big-endian字节顺序存储
DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
try {
Mixer.Info[] mixerInfos = AudioSystem.getMixerInfo();
Mixer mixer = null;
for (int i = 0; i < mixerInfos.length; i++) {
System.out.println(mixerInfos[i]);
if (mixerInfos[i].getName().equals("CABLE Input (VB-Audio Virtual Cable)")) {
mixer = AudioSystem.getMixer(mixerInfos[i]);
System.out.println("get successful");
}
}
auline = (SourceDataLine) mixer.getLine(info);
auline.open(format);
} catch (LineUnavailableException e) {
e.printStackTrace();
return;
} catch (Exception e) {
e.printStackTrace();
return;
}
}
SourceDataLine getAudioLine() {
return auline;
}
第二步,获取从Android过来的pcm流,并写入到虚拟声卡中。
private static ServerSocket mServerSocket;
private static Socket mSocket;
private static AudioVirtualCableUtile pcmUtile = new AudioVirtualCableUtile();
public static void main(String args[]) {
startServer();
}
static void startServer() {
if (mServerSocket == null) {
new Thread(new Runnable() {
@Override
public void run() {
try {
mServerSocket = new ServerSocket(5678);
System.out.println("tcp connecting");
mSocket = mServerSocket.accept();
System.out.println("tcp connected");
InputStream inputStream = mSocket.getInputStream();
byte[] buffer = new byte[1024];
int len = -1;
//
pcmUtile.getAudioLine().start();
while ((len = inputStream.read(buffer)) != -1) {
pcmUtile.getAudioLine().write(buffer, 0, len);//写入到虚拟声卡中
System.out.println("----" + System.currentTimeMillis());
}
} catch (IOException e) {
e.printStackTrace();
try {
mSocket.close();
mServerSocket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
}).start();
}
}
}
到此代码基本已经完成,还需要持续优化,本篇文章中的代码纯demo,没有过多考虑,只要功能满足即可。
备注:
需要手动切换虚拟声卡。需要先启动pc端,在启动Android,因为pc端为服务端。