You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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代码逻辑中。


爆音问题根源与修复方案

核心问题分析

  1. 帧同步逻辑错误:Java代码中通过isNewSampleCycleStarted拼接字节的逻辑存在漏洞,一旦串口数据出现丢包或错位,会导致采样字节配对错误,直接产生爆音。
  2. 音频帧字节转换错误:AudioFrame的toBytes()方法创建了大小为maxSizeInBits的字节数组(单位是比特),但实际需要的是字节数,导致数组中存在大量无效数据,播放时产生杂音。
  3. 缓冲机制缺失:没有设置合理的音频缓冲,导致数据断断续续,引发爆音。

修复后的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 10:22:03