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

Android平台Codec2编解码实现遇音频失真问题求助

Android Codec2编码解码音频失真问题修复

问题根源分析

你的代码存在三个核心问题,直接导致音频严重失真:

  1. 编码时重复处理同一帧数据:录音缓冲区包含4帧有效数据(640样本/160样本每帧),但你每次编码都传入整个缓冲区,相当于重复编码第一帧4次,导致数据冗余错乱。
  2. 用CharArray存储编码字节流:Codec2输出的是原始二进制字节数据,用CharArray存储会引入字符编码转换问题,导致解码时字节值丢失或异常。
  3. 解码时字节转换逻辑错误:将CharArray转ByteArray的方式会破坏原始字节结构,比如高位字节被截断,导致解码出的PCM数据完全错误。

修复后的代码实现

companion object {
    public const val codec2mode = Codec2.CODEC2_MODE_3200

    private const val sampleRate = 8000
    private const val audioFormat = AudioFormat.ENCODING_PCM_16BIT
    private const val monoStereoIn = AudioFormat.CHANNEL_IN_MONO
    private const val monoStereoOut = AudioFormat.CHANNEL_OUT_MONO
}

private val minBufSize = AudioRecord.getMinBufferSize(sampleRate, monoStereoIn, audioFormat)

private val c2instance = Codec2.create(codec2mode)
private val frameBytesSize = Codec2.getBitsSize(c2instance) // 注意:返回值是每帧编码后的字节数
private val samplesPerFrame = Codec2.getSamplesPerFrame(c2instance)

fun recordData(): ByteArray {
    Timber.i("recordData frameBytes:$frameBytesSize, samplesPerFrame:$samplesPerFrame, minBufSize:$minBufSize")
    val recorder = getRecorder()

    val recorderBuffer = ShortArray(minBufSize)
    val framesPerBuffer = recorderBuffer.size / samplesPerFrame

    Timber.i("startRecording recorderBuffer size:${recorderBuffer.size} framesPerBuffer:$framesPerBuffer")
    recorder.startRecording()

    val start = System.currentTimeMillis()
    val encodedData = mutableListOf<Byte>()

    var it = 50
    while (it > 0) {
        // 必须处理read返回值,确保读取到有效数据
        val readCount = recorder.read(recorderBuffer, 0, recorderBuffer.size)
        if (readCount <= 0) {
            Timber.w("Failed to read audio data")
            it--
            continue
        }

        // 分帧处理每一段样本
        for (i in 0 until framesPerBuffer) {
            val frameStart = i * samplesPerFrame
            // 避免越界:如果读取的样本数不足一整帧则跳过
            if (frameStart + samplesPerFrame > readCount) break
            
            val frameSamples = recorderBuffer.copyOfRange(frameStart, frameStart + samplesPerFrame)
            val encodedFrame = ByteArray(frameBytesSize)
            Codec2.encode(c2instance, frameSamples, encodedFrame)
            encodedData.addAll(encodedFrame.toList())
        }
        it--
    }

    recorder.stop()
    recorder.release()

    Timber.i(
        "encode finished in ${System.currentTimeMillis() - start}ms, data length:${encodedData.size}"
    )

    return encodedData.toByteArray()
}

fun playData(encodedBuffer: ByteArray) {
    Timber.i("playData frameBytes:$frameBytesSize, samplesPerFrame:$samplesPerFrame, minBufSize:$minBufSize")
    val start = System.currentTimeMillis()

    val audioTrack = getAudioTrack().apply { play() }

    var offset = 0
    var it = 0
    while (offset + frameBytesSize <= encodedBuffer.size) {
        // 截取当前帧的编码字节
        val encodedFrame = encodedBuffer.copyOfRange(offset, offset + frameBytesSize)
        val playbackAudioBuffer = ShortArray(samplesPerFrame)
        
        Codec2.decode(c2instance, playbackAudioBuffer, encodedFrame)
        audioTrack.write(playbackAudioBuffer, 0, playbackAudioBuffer.size)

        offset += frameBytesSize
        it++
    }

    audioTrack.stop()
    audioTrack.release()

    Timber.i(
        "decode and play finished in ${System.currentTimeMillis() - start}ms, iterations:$it"
    )
}

// 在合适的生命周期方法中释放Codec2实例,避免内存泄漏
fun releaseCodec2() {
    Codec2.destroy(c2instance)
}

额外注意事项

  • 资源释放:AudioRecord、AudioTrack使用完毕后必须调用release(),避免占用系统音频资源。
  • 数据有效性检查:AudioRecord的read()返回值可能为负(表示读取错误),必须处理这种情况,防止无效数据进入编码流程。
  • Native资源管理:Codec2实例基于Native实现,不用时必须调用destroy()释放,防止内存泄漏。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 08:06:01