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
相关产品推荐
相关产品推荐

