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

使用ExoPlayer向Discord Bot流传输音频出现白噪音问题

音频推流至Discord Bot出现噪音问题排查

问题描述

我尝试将音频字节流传输至Discord Bot,但输出的是大量噪音而非正常音乐。Bot已成功连接语音频道,Codec本身可正常工作,推测问题出在数据获取或传输环节。Bot能正常识别声音,但无法还原正确音频;用ExoPlayer播放本地MP3文件时手机端正常,推流到Discord就异常。

Discord Bot要求

基于Kord开发的Discord Bot要求:一帧20ms的Opus编码48k立体声音频数据。

编解码器设置

基于上述要求配置的Codec参数:

@OptIn(UnstableApi::class)
class BotAudioProcessor(
    private val audioDataListener: AudioDataListener
): BaseAudioProcessor(){
    val codec = Opus()
    val SAMPLE_RATE = Constants.SampleRate._48000()
    val CHANNELS = Constants.Channels.stereo()
    val APPLICATION = Constants.Application.audio()
    val FRAME_SIZE = Constants.FrameSize._960()
    val COMPLEXITY = Constants.Complexity.instance(10)
    val BITRATE = Constants.Bitrate.max()

    init {
        codec.encoderInit(SAMPLE_RATE, CHANNELS, APPLICATION)
        codec.encoderSetComplexity(COMPLEXITY)
        codec.encoderSetBitrate(BITRATE)
    }

    override fun onConfigure(inputFormat: AudioProcessor.AudioFormat): AudioProcessor.AudioFormat {
        val outputFormat = AudioProcessor.AudioFormat(
            SAMPLE_RATE.v,
            CHANNELS.v,
            C.ENCODING_PCM_16BIT
        )
        return outputFormat
    }
    //... queueInput()
}

帧大小计算逻辑:frame size = SAMPLE_RATE * 0.02 (20ms) = 960

获取音频流

通过继承BaseAudioProcessor重写queueInput方法获取PCM流,编码为Opus后传递给监听器:

override fun queueInput(inputBuffer: ByteBuffer) {
    if (inputBuffer.remaining() < FRAME_SIZE.v / 2) {
        Log.e("BotAudioProcessor", "Input buffer too small")
        return
    }
    val frame = ShortArray( FRAME_SIZE.v / 2)
    inputBuffer.asShortBuffer().get(frame)
    val encoded = codec.encode(frame, FRAME_SIZE)

    audioDataListener.onAudioData(encoded!!.toByteArray())
}

向Bot传输流数据

将编码后的音频流保存至变量,由Bot广播:

@UnstableApi
class MusicBot(...) : AudioDataListener {
    private var audioBuffer: ByteArray? = null
    //...

    @OptIn(KordVoice::class)
    suspend fun start() {
        kord!!.on<ReadyEvent> {
            //...
            voiceChannel.connect {
                audioProvider {
                    println(audioBuffer?.joinToString(" "))
                    println(audioBuffer?.size)
                    audioBuffer?.let { AudioFrame(it) }
                }
            }
        }
    }

    override fun onAudioData(data: ByteArray) {
        audioBuffer = data
    }
}

处理器连接播放器代码

@OptIn(UnstableApi::class)
class DiscordRendersFactory
    (
    context: Context,
    private val audioDataListener: AudioDataListener
) : DefaultRenderersFactory(context) {

    override fun buildAudioSink(
        context: Context,
        enableFloatOutput: Boolean,
        enableAudioTrackPlaybackParams: Boolean
    ): AudioSink {

        val defaultAudioSink = DefaultAudioSink.Builder()
            .setAudioProcessors(arrayOf(BotAudioProcessor(audioDataListener)))
            .setEnableFloatOutput(true)
            .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams)
            .build()
        return defaultAudioSink
    }
}

排查方向

  • PCM帧大小与通道数不匹配
    计算的960是单声道20ms采样数,立体声需要960*2=1920个采样点(每个采样16bit)。当前代码只取了480个采样,编码数据不完整导致噪音。修正:将帧采样数改为1920,缓冲区剩余判断改为inputBuffer.remaining() < 1920*2,ShortArray大小设为1920。

  • 音频格式转换不彻底
    检查onConfigure中是否完成了正确的格式转换:打印输入输出格式,确认ExoPlayer的原始音频是否被正确转成48k立体声16bit PCM。

  • 编码数据缓存方式错误
    用单个变量存储音频帧会导致帧覆盖,无法连续传输。改用队列(如ConcurrentLinkedQueue)缓存所有Opus帧,在audioProvider中依次取出发送,保证流的连续性。

  • Opus编码参数验证
    确认Opus库是否正确支持立体声编码,编码后打印每帧字节数(正常范围几十到几百字节),若数值异常则说明编码过程有问题。

  • 字节序不匹配
    Discord要求小端字节序的PCM数据,检查ExoPlayer输出的字节序是否符合,必要时进行转换。

内容的提问来源于stack exchange,提问作者Ikrom

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 21:57:34