简易版WoMic(二):从零构建Android到PC的音频传输链路
1. 项目背景与核心原理几年前我在做一个远程会议系统时第一次接触到音频跨设备传输的需求。当时尝试了市面上各种方案要么延迟太高要么配置太复杂直到发现了WoMic这个思路。今天要分享的简易版实现可以说是WoMic的青春版保留了核心功能但大幅降低了实现门槛。这个系统的本质是建立一条数字音频流水线Android手机相当于一个智能麦克风把采集到的声音数据通过TCP协议实时传输到PCPC端再把这些数据喂给虚拟声卡让系统以为接入了真实麦克风。整个过程涉及四个关键技术点音频采集Android的AudioRecord API负责获取原始PCM数据网络传输通过TCP Socket建立稳定传输通道虚拟声卡VB-Audio这类工具模拟物理音频设备数据对齐两端参数必须完全匹配才能避免杂音提示PCM脉冲编码调制是音频最原始的数字化形式就像快递运输中的未打包商品保持原始状态有利于后续处理。2. 开发环境准备2.1 工具清单工欲善其事必先利其器这是我验证过能完美配合的工具组合Android Studio2023.2.1版本太新的版本可能有兼容性问题Java环境JDK 11注意PC端和Android的JDK版本尽量一致虚拟声卡VB-Audio Virtual Cable 4.59新版可能有驱动签名问题网络调试工具Wireshark用于排查传输问题安装VB-Audio时有个小坑要注意在Windows安全中心会提示驱动未签名需要手动点击更多信息→仍要运行。安装完成后在声音设置里会出现CABLE Input设备这就是我们的虚拟麦克风。2.2 参数规划参数对齐是避免噪音的关键推荐使用这套经过验证的配置参数项Android端值PC端值采样率8000 Hz8000 Hz声道数MONO2 (立体声)采样深度ENCODING_PCM_8BITPCM_SIGNED 16-bit缓冲区大小getMinBufferSize()值匹配Android缓冲区这里有个反直觉的点虽然Android用单声道采集但VB-Audio默认只支持立体声输入。实测发现这种组合反而最稳定不会出现声音断续。3. Android端实现详解3.1 音频采集模块AudioRecord是Android音频采集的核心类但直接使用容易踩坑。下面是我优化后的采集工具类public class AudioRecorder { private static final String TAG AudioRecorder; private AudioRecord audioRecord; private int bufferSize; private volatile boolean isRecording; // 推荐的参数组合 private static final int SAMPLE_RATE 8000; private static final int CHANNEL_CONFIG AudioFormat.CHANNEL_IN_MONO; private static final int AUDIO_FORMAT AudioFormat.ENCODING_PCM_8BIT; public AudioRecorder() { this.bufferSize AudioRecord.getMinBufferSize( SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT ); if(bufferSize AudioRecord.ERROR_BAD_VALUE) { throw new RuntimeException(无效的音频参数); } audioRecord new AudioRecord( MediaRecorder.AudioSource.MIC, SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT, bufferSize * 2 // 双缓冲避免溢出 ); } public void startRecording(DataCallback callback) { if(audioRecord.getState() ! AudioRecord.STATE_INITIALIZED) { Log.e(TAG, 录音器未初始化); return; } isRecording true; new Thread(() - { audioRecord.startRecording(); byte[] buffer new byte[bufferSize]; while(isRecording) { int read audioRecord.read(buffer, 0, buffer.length); if(read 0) { callback.onDataAvailable(buffer); } } audioRecord.stop(); }).start(); } public void stopRecording() { isRecording false; } public interface DataCallback { void onDataAvailable(byte[] data); } }这段代码有三个关键改进点双缓冲设计避免数据丢失增加设备状态检查使用volatile保证线程安全3.2 网络传输模块TCP传输看似简单但处理不好会导致声音卡顿。这是我总结的传输最佳实践public class AudioSender { private Socket socket; private OutputStream outputStream; private final Object lock new Object(); public void connect(String ip, int port) throws IOException { socket new Socket(); socket.connect(new InetSocketAddress(ip, port), 3000); socket.setTcpNoDelay(true); // 关闭Nagle算法 outputStream socket.getOutputStream(); } public void sendAudioData(byte[] data) { synchronized (lock) { try { if(outputStream ! null) { outputStream.write(data); outputStream.flush(); } } catch (IOException e) { Log.e(AudioSender, 发送失败, e); } } } public void disconnect() { try { if(socket ! null) socket.close(); } catch (IOException e) { Log.e(AudioSender, 关闭连接异常, e); } } }重点注意设置setTcpNoDelay(true)降低延迟同步块防止多线程竞争连接超时设为3秒避免卡死4. PC端实现关键点4.1 虚拟声卡配置VB-Audio的配置有门道这段代码可以自动检测可用设备public class VirtualAudioDevice { private SourceDataLine audioLine; public VirtualAudioDevice() throws LineUnavailableException { AudioFormat format new AudioFormat( 8000, 16, 2, // 必须设为2声道 true, false ); Mixer.Info[] mixers AudioSystem.getMixerInfo(); for(Mixer.Info info : mixers) { if(info.getName().contains(CABLE Input)) { Mixer mixer AudioSystem.getMixer(info); DataLine.Info lineInfo new DataLine.Info( SourceDataLine.class, format ); audioLine (SourceDataLine)mixer.getLine(lineInfo); break; } } if(audioLine null) { throw new IllegalStateException(未找到VB-Audio设备); } audioLine.open(format); audioLine.start(); } public void writeAudio(byte[] data) { audioLine.write(data, 0, data.length); } }4.2 网络服务端PC端作为TCP服务端需要处理可能的断连情况public class AudioServer { private static final int PORT 5678; private volatile boolean isRunning; public void start() { isRunning true; new Thread(() - { try(ServerSocket server new ServerSocket(PORT)) { while(isRunning) { handleClient(server.accept()); } } catch (IOException e) { e.printStackTrace(); } }).start(); } private void handleClient(Socket client) { new Thread(() - { try(InputStream input client.getInputStream()) { byte[] buffer new byte[4096]; int bytesRead; while((bytesRead input.read(buffer)) ! -1) { // 这里调用VirtualAudioDevice写入 audioDevice.writeAudio(buffer); } } catch (IOException e) { System.out.println(客户端断开连接); } }).start(); } }5. 常见问题排查指南5.1 声音出现爆音可能原因两端采样率不一致用Audacity检查原始数据缓冲区大小不匹配调整PC端缓冲区大小网络丢包用Wireshark抓包分析5.2 连接频繁断开解决方案增加心跳包机制设置SO_KEEPALIVE参数检查路由器MTU设置5.3 延迟过高优化方向改用UDP协议牺牲稳定性降低采样率到8000Hz以下使用OPUS编码压缩数据最后提醒一点测试时建议先用内网环境公网传输需要考虑NAT穿透问题。这套系统虽然简单但已经能满足远程会议、游戏语音等常见场景后续可以在此基础上增加降噪、回声消除等高级功能。