使用ExoPlayer向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

