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

JDA+Lavaplayer多AudioPlayer混音异常:仅闻嗡嗡声求助

多AudioPlayer混音导致Discord播放嗡嗡声的排查与修复

问题描述

使用Java开发Discord机器人时,尝试将多个AudioPlayer的音频帧混音以实现多源播放,但播放时仅听到刺耳的嗡嗡声,无法正常播放混合音频。当前混音器实现代码如下:

public class MultiAudioPlayerSendHandler implements AudioSendHandler {

    private final AudioPlayer[] audioPlayers;
    private final MutableAudioFrame[] frames;

    private final byte[][] frameBuffers;
    private final boolean[] frameReady;

    private final ByteBuffer outputBuffer;
    private final byte[] mixBuffer;

    public MultiAudioPlayerSendHandler(AudioPlayer... audioPlayers) {
        this.audioPlayers = audioPlayers;
        this.frames = new MutableAudioFrame[audioPlayers.length];
        this.frameBuffers = new byte[audioPlayers.length][];
        this.frameReady = new boolean[audioPlayers.length];

        this.outputBuffer = ByteBuffer.allocate(3840);
        this.mixBuffer = new byte[3840];

        for (int i = 0; i < audioPlayers.length; i++) {
            frames[i] = new MutableAudioFrame();
            frameBuffers[i] = new byte[3840];
            frames[i].setBuffer(ByteBuffer.wrap(frameBuffers[i]));
        }
    }

    @Override
    public boolean canProvide() {
        boolean hasData = false;
        for (int i = 0; i < audioPlayers.length; i++) {
            frameReady[i] = audioPlayers[i].provide(frames[i]);
            if (frameReady[i]) hasData = true;
        }
        return hasData;
    }

    @Override
    public ByteBuffer provide20MsAudio() {
        Arrays.fill(mixBuffer, (byte) 0);

        boolean hasData = false;
        int activeStreams = 0;

        for (int i = 0; i < audioPlayers.length; i++) {
            if (!frameReady[i]) continue;
            hasData = true;
            activeStreams++;

            byte[] data = frameBuffers[i];
            int length = frames[i].getDataLength();

            for (int j = 0; j < length; j += 2) {
                int existing = ((mixBuffer[j] & 0xFF)) | ((mixBuffer[j + 1] & 0xFF) << 8);
                if (existing > 0x7FFF) existing -= 0x10000;

                int incoming = ((data[j] & 0xFF)) | ((data[j + 1] & 0xFF) << 8);
                if (incoming > 0x7FFF) incoming -= 0x10000;

                int mixed = existing + incoming;

                if (mixed > Short.MAX_VALUE) mixed = Short.MAX_VALUE;
                if (mixed < Short.MIN_VALUE) mixed = Short.MIN_VALUE;

                mixBuffer[j] = (byte) (mixed & 0xFF);
                mixBuffer[j + 1] = (byte) ((mixed >> 8) & 0xFF);
            }
        }

        if (!hasData) {
            return null;
        }

        if (activeStreams > 1) {
            for (int j = 0; j < mixBuffer.length; j += 2) {
                int sample = ((mixBuffer[j] & 0xFF) | ((mixBuffer[j + 1] & 0xFF) << 8));
                if (sample > 0x7FFF) sample -= 0x10000;

                sample /= activeStreams;

                mixBuffer[j] = (byte) (sample & 0xFF);
                mixBuffer[j + 1] = (byte) ((sample >> 8) & 0xFF);
            }
        }

        outputBuffer.clear();
        outputBuffer.put(mixBuffer);
        outputBuffer.flip();

        return outputBuffer;
    }

    @Override
    public boolean isOpus() {
        return false;
    }
    
}

怀疑点:PCM采样解析/混音逻辑错误、活跃流除法导致失真、isOpus()返回值是否正确。


核心问题与修复方案

1. 音频帧缓冲区大小错误(嗡嗡声主因)

Discord/JDA要求的20ms音频帧为48kHz采样率、16位单声道PCM,计算得单帧字节数为:
48000Hz × 0.02s × 2字节/样本 = 1920字节
代码中错误使用3840字节(两倍大小),导致音频数据被错误填充、播放速度减半,直接产生嗡嗡声。

修复:将所有缓冲区大小改为1920:

public MultiAudioPlayerSendHandler(AudioPlayer... audioPlayers) {
    this.audioPlayers = audioPlayers;
    this.frames = new MutableAudioFrame[audioPlayers.length];
    this.frameBuffers = new byte[audioPlayers.length][];
    this.frameReady = new boolean[audioPlayers.length];

    // 修正缓冲区大小
    this.outputBuffer = ByteBuffer.allocate(1920);
    this.mixBuffer = new byte[1920];

    for (int i = 0; i < audioPlayers.length; i++) {
        frames[i] = new MutableAudioFrame();
        frameBuffers[i] = new byte[1920]; // 修正此处
        frames[i].setBuffer(ByteBuffer.wrap(frameBuffers[i]));
    }
}

2. PCM采样处理优化

手动位运算易出错,改用ByteBuffer简化有符号short解析与写入:

@Override
public ByteBuffer provide20MsAudio() {
    Arrays.fill(mixBuffer, (byte) 0);

    boolean hasData = false;
    int activeStreams = 0;
    ByteOrder byteOrder = ByteOrder.LITTLE_ENDIAN; // JDA使用小端字节序

    for (int i = 0; i < audioPlayers.length; i++) {
        if (!frameReady[i]) continue;
        hasData = true;
        activeStreams++;

        byte[] data = frameBuffers[i];
        int length = frames[i].getDataLength();

        ByteBuffer dataBuffer = ByteBuffer.wrap(data).order(byteOrder);
        ByteBuffer mixBuffer = ByteBuffer.wrap(this.mixBuffer).order(byteOrder);

        // 按样本(2字节)遍历混音
        for (int j = 0; j < length / 2; j++) {
            short existing = mixBuffer.getShort(j * 2);
            short incoming = dataBuffer.getShort(j * 2);
            
            int mixed = existing + incoming;
            // 限幅避免溢出
            mixed = Math.max(Short.MIN_VALUE, Math.min(Short.MAX_VALUE, mixed));
            
            mixBuffer.putShort(j * 2, (short) mixed);
        }
    }

    if (!hasData) {
        // 返回静音帧而非null,避免音频中断
        Arrays.fill(mixBuffer, (byte) 0);
        outputBuffer.clear();
        outputBuffer.put(mixBuffer);
        outputBuffer.flip();
        return outputBuffer;
    }

    // 音量归一化
    if (activeStreams > 1) {
        ByteBuffer mixBuffer = ByteBuffer.wrap(this.mixBuffer).order(byteOrder);
        for (int j = 0; j < this.mixBuffer.length / 2; j++) {
            short sample = mixBuffer.getShort(j * 2);
            // 浮点除法保留精度,避免整数除法失真
            float normalized = sample / (float) activeStreams;
            mixBuffer.putShort(j * 2, (short) normalized);
        }
    }

    outputBuffer.clear();
    outputBuffer.put(mixBuffer);
    outputBuffer.flip();

    return outputBuffer;
}

3. isOpus()方法正确性确认

当前返回false是正确的:你提供的是原始PCM数据,JDA会自动将其编码为Discord要求的Opus格式。若返回true,则需自行完成Opus编码,会导致格式不匹配错误。

4. 额外优化

  • 在canProvide()中,可检查每个AudioPlayer提供的帧长度是否为1920,避免不完整帧参与混音
  • 无音频数据时返回静音帧(全0的1920字节缓冲区),而非null,防止音频流中断

内容的提问来源于stack exchange,提问作者toplay 4free

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 11:45:54