AUX转USB音频传输爆音问题技术求助
AUX转USB音频传输爆音问题排查与解决
项目背景
我正在开发一个AUX音频转USB传输的项目,目标是捕获手机、电视等设备的音频输出并传输至电脑播放。
所需设备与软件
- 带3.5mm母头AUX接口的设备(如手机)
- 3.5mm公头转三线(地、左声道、右声道)的定制线
- Arduino Mega 2560
- USB Type-A线
- 电脑端工具:Arduino IDE、Java JDK≥8、VSCode
硬件连接
- 定制线接设备AUX接口,三线分别连接Arduino的GND、A0(左声道)、A1(右声道)
- USB线连接Arduino与电脑,同时提供供电
现有代码
Arduino采样发送代码
const int SAMPLE_RATE = 44100; // 采样率(Hz) const unsigned long SAMPLE_INTERVAL = 1000000.0 / SAMPLE_RATE; // 采样间隔(微秒) void setup() { Serial.begin(115200); // 初始化串口,高波特率避免丢包 } void loop() { static unsigned long lastSampleTime = 0; if (micros() - lastSampleTime >= SAMPLE_INTERVAL) { lastSampleTime += SAMPLE_INTERVAL; // 读取模拟引脚值 int leftSample = analogRead(A0); int rightSample = analogRead(A1); // 转换为16位音频范围(-32768 至 32767) int16_t leftAudio = map(leftSample, 0, 1023, -32768, 32767); int16_t rightAudio = map(rightSample, 0, 1023, -32768, 32767); // 通过串口发送原始音频数据 Serial.write(0xFF); Serial.write(lowByte(leftSample)); Serial.write(highByte(leftSample)); Serial.write(lowByte(rightSample)); Serial.write(highByte(rightSample)); } }
Java端接收播放代码
import java.io.IOException; import java.util.ArrayList; import java.util.List; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.SourceDataLine; import com.fazecast.jSerialComm.SerialPort; import com.fazecast.jSerialComm.SerialPortDataListener; import com.fazecast.jSerialComm.SerialPortEvent; import io.nirahtech.soundpack.box.driver.AudioFrame; import io.nirahtech.soundpack.box.driver.Sample; public class Jack2Usb2Audio { private static final int SAMPLE_RATE = 44100; // 采样率(Hz) private static final int BITS_PER_SAMPLE = 16; // 采样位深 private static final int SAMPLE_SIZE = BITS_PER_SAMPLE / 8; // 单采样字节数 private static final int CHANNELS = 2; // 立体声 private static final int FRAME_SIZE = SAMPLE_SIZE * CHANNELS; // 单帧字节数(4字节) private static final int BUFFER_SIZE = FRAME_SIZE * 1024; // 缓冲大小 private static final int SERIAL_BAUD_RATE = 115200; // 串口波特率 private static final String SERIAL_PORT = "/dev/ttyACM0"; // USB端口 private static final byte START_OF_FRAME = (byte) 0xFF; // 帧起始标记 public static void main(String[] args) { SerialPort serialPort = SerialPort.getCommPort(SERIAL_PORT); serialPort.setBaudRate(SERIAL_BAUD_RATE); serialPort.setNumDataBits(8); serialPort.setNumStopBits(SerialPort.ONE_STOP_BIT); serialPort.setParity(SerialPort.NO_PARITY); if (!serialPort.openPort()) { System.out.println("Échec de l'ouverture du port."); return; } System.out.println("Port ouvert avec succès."); AudioFormat format = new AudioFormat(SAMPLE_RATE, BITS_PER_SAMPLE, CHANNELS, true, false); try { SourceDataLine line = AudioSystem.getSourceDataLine(format); line.open(format); line.start(); serialPort.addDataListener(new SerialPortDataListener() { private boolean isNewSampleCycleStarted = false; private List<Byte> cache = new ArrayList<>(); private AudioFrame audioFrame = new AudioFrame(FRAME_SIZE * 8); @Override public int getListeningEvents() { return SerialPort.LISTENING_EVENT_DATA_AVAILABLE; } @Override public void serialEvent(SerialPortEvent event) { if (event.getEventType() != SerialPort.LISTENING_EVENT_DATA_AVAILABLE) { return; } byte[] newSamples = new byte[serialPort.bytesAvailable()]; int totalSamplesRetrieved = serialPort.readBytes(newSamples, newSamples.length); for (int i = 0; i < totalSamplesRetrieved; i++) { final byte receivedSample = newSamples[i]; if (receivedSample == START_OF_FRAME) { this.audioFrame = new AudioFrame(FRAME_SIZE * 8); } else { if (!this.isNewSampleCycleStarted) { this.cache.add(receivedSample); this.isNewSampleCycleStarted = true; } else { this.isNewSampleCycleStarted = false; if (!this.cache.isEmpty()) { final byte previousSample = this.cache.get(0); this.cache.clear(); final Sample sample = Sample.of(previousSample, receivedSample); if (!this.audioFrame.hasSpaceLeft()) { this.audioFrame = new AudioFrame(FRAME_SIZE * 8); } try { this.audioFrame.addSample(sample); } catch (Exception e) { e.printStackTrace(); } } } if (!this.audioFrame.hasSpaceLeft()) { byte[] data = this.audioFrame.toBytes(); line.write(data, 0, this.audioFrame.getUsedSizeInBits()); } } } } }); System.out.println("Lecture audio en cours. Appuyez sur Enter pour arrêter."); System.in.read(); line.stop(); line.close(); } catch (LineUnavailableException | IOException e) { e.printStackTrace(); } finally { serialPort.closePort(); System.out.println("Port fermé."); } } } package io.nirahtech.soundpack.box.driver; import java.util.ArrayList; import java.util.List; import java.util.Objects; public class AudioFrame { private final List<Sample> samples; private final int maxSizeInBits; public AudioFrame(final int sizeInBits) { this.maxSizeInBits = sizeInBits; this.samples = new ArrayList<>(); } public int getMaxSizeInBits() { return this.maxSizeInBits; } public int getUsedSizeInBits() { return (int) this.samples.stream().mapToInt(Sample::getSizeInBits).sum(); } public int getEmptySizeInBits() { return this.getMaxSizeInBits() - this.getUsedSizeInBits(); } public boolean hasSpaceLeft() { return this.getUsedSizeInBits() < this.getMaxSizeInBits(); } public void addSample(final Sample sample) throws Exception { Objects.requireNonNull(sample, "Sample is required for AudioFrame."); if (!this.hasSpaceLeft()) { throw new Exception("No space left"); } this.samples.add(sample); } public byte[] toBytes() { final byte[] frame = new byte[this.maxSizeInBits]; int frameIndex = 0; for (final Sample sample : this.samples) { final byte[] sampleAsBytes = sample.toBytes(); for (byte sampleBit : sampleAsBytes) { frame[frameIndex] = sampleBit; frameIndex++; } } return frame; } } package io.nirahtech.soundpack.box.driver; public class Sample { private final short sample; private final byte lowByte; private final byte highByte; private Sample(final short sample) { this.sample = sample; this.lowByte = (byte) (this.sample & 0xFF); this.highByte = (byte) ((this.sample >> 8) & 0xFF); } public int getSizeInBits() { return 8 * 2; } public byte getLowByte() { return this.lowByte; } public byte getHighByte() { return this.highByte; } public static final Sample of(final short sample) { return new Sample(sample); } public static final Sample of(final byte lowByte, final byte highByte) { final short sample = (short) ((highByte << 8) | (lowByte & 0xFF)); return new Sample(sample); } public byte[] toBytes() { return new byte[] { this.lowByte, this.highByte }; } }
问题描述
当前播放音频时可识别曲调,但全程伴随严重爆音,怀疑问题出在Java代码逻辑中。
爆音问题根源与修复方案
核心问题分析
- 帧同步逻辑错误:Java代码中通过
isNewSampleCycleStarted拼接字节的逻辑存在漏洞,一旦串口数据出现丢包或错位,会导致采样字节配对错误,直接产生爆音。 - 音频帧字节转换错误:
AudioFrame的toBytes()方法创建了大小为maxSizeInBits的字节数组(单位是比特),但实际需要的是字节数,导致数组中存在大量无效数据,播放时产生杂音。 - 缓冲机制缺失:没有设置合理的音频缓冲,导致数据断断续续,引发爆音。
修复后的Java代码
简化数据处理逻辑,直接按帧接收并播放,移除冗余的AudioFrame和Sample类:
import java.io.IOException; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.LineUnavailableException; import javax.sound.sampled.SourceDataLine; import com.fazecast.jSerialComm.SerialPort; import com.fazecast.jSerialComm.SerialPortDataListener; import com.fazecast.jSerialComm.SerialPortEvent; public class Jack2Usb2Audio { private static final int SAMPLE_RATE = 44100; private static final int BITS_PER_SAMPLE = 16; private static final int SAMPLE_SIZE = BITS_PER_SAMPLE / 8; private static final int CHANNELS = 2; private static final int FRAME_SIZE = SAMPLE_SIZE * CHANNELS; // 单帧4字节(左右声道各2字节) private static final int BUFFER_SIZE = FRAME_SIZE * 1024; // 4KB音频缓冲 private static final int SERIAL_BAUD_RATE = 230400; // 提高波特率避免丢包 private static final String SERIAL_PORT = "/dev/ttyACM0"; private static final byte START_OF_FRAME = (byte) 0xFF; private static final int FULL_FRAME_LENGTH = 1 + FRAME_SIZE; // 起始符+4字节音频数据 public static void main(String[] args) { SerialPort serialPort = SerialPort.getCommPort(SERIAL_PORT); serialPort.setBaudRate(SERIAL_BAUD_RATE); serialPort.setNumDataBits(8); serialPort.setNumStopBits(SerialPort.ONE_STOP_BIT); serialPort.setParity(SerialPort.NO_PARITY); if (!serialPort.openPort()) { System.out.println("无法打开串口"); return; } System.out.println("串口已打开"); AudioFormat format = new AudioFormat(SAMPLE_RATE, BITS_PER_SAMPLE, CHANNELS, true, false); try { SourceDataLine line = AudioSystem.getSourceDataLine(format); line.open(format, BUFFER_SIZE); line.start(); byte[] frameBuffer = new byte[FULL_FRAME_LENGTH]; int bufferCursor = 0; serialPort.addDataListener(new SerialPortDataListener() { @Override public int getListeningEvents() { return SerialPort.LISTENING_EVENT_DATA_AVAILABLE; } @Override public void serialEvent(SerialPortEvent event) { if (event.getEventType() != SerialPort.LISTENING_EVENT_DATA_AVAILABLE) { return; } byte[] incomingData = new byte[serialPort.bytesAvailable()]; int readCount = serialPort.readBytes(incomingData, incomingData.length); for (int i = 0; i < readCount; i++) { byte currentByte = incomingData[i]; if (currentByte == START_OF_FRAME) { bufferCursor = 0; frameBuffer[bufferCursor++] = currentByte; } else if (bufferCursor > 0 && bufferCursor < FULL_FRAME_LENGTH) { frameBuffer[bufferCursor++] = currentByte; // 一帧数据接收完成 if (bufferCursor == FULL_FRAME_LENGTH) { // 跳过起始符,写入音频数据 line.write(frameBuffer, 1, FRAME_SIZE); bufferCursor = 0; } } } } }); System.out.println("正在播放音频,按Enter停止"); System.in.read(); line.drain(); // 播放完缓冲中剩余数据 line.stop(); line.close(); } catch (LineUnavailableException | IOException e) { e.printStackTrace(); } finally { serialPort.closePort(); System.out.println("串口已关闭"); } } }
配套Arduino代码优化
原代码中未使用转换后的16位音频数据,直接发送了原始采样值,导致音频动态范围不足,优化后:
const int SAMPLE_RATE = 44100; const unsigned long SAMPLE_INTERVAL = 1000000.0 / SAMPLE_RATE; void setup() { Serial.begin(230400); // 与Java端波特率一致 } void loop() { static unsigned long lastSampleTime = 0; if (micros() - lastSampleTime >= SAMPLE_INTERVAL) { lastSampleTime += SAMPLE_INTERVAL; int leftSample = analogRead(A0); int rightSample = analogRead(A1); // 转换为标准16位音频数据 int16_t leftAudio = map(leftSample, 0, 1023, -32768, 32767); int16_t rightAudio = map(rightSample, 0, 1023, -32768, 32767); // 发送帧起始符+左右声道数据(小端模式) Serial.write(0xFF); Serial.write((uint8_t)(leftAudio & 0xFF)); Serial.write((uint8_t)((leftAudio >> 8) & 0xFF)); Serial.write((uint8_t)(rightAudio & 0xFF)); Serial.write((uint8_t)((rightAudio >> 8) & 0xFF)); } }
额外优化建议
- 硬件滤波:在Arduino的A0、A1引脚与GND之间添加100nF电容,减少模拟输入噪声。
- 接地优化:确保所有设备(手机/电视、Arduino、电脑)的GND可靠连接,避免接地回路噪声。
- 串口波特率:使用230400或更高波特率,确保能承载44100采样率的立体声数据传输。
内容的提问来源于stack exchange,提问作者Nicolas Metivier
相关产品推荐
相关产品推荐

