Android audiotrack尾帧无声

本文探讨了Android音频播放过程中遇到的音频截断问题,重点在于MediaExtractor、MediaCodec和AudioTrack的使用,提出了解决方案,包括调整startThresholdInFrames和在必要时调用stop方法来确保音频完整播放。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

产品一直有用户反馈音频截断问题。在机遇巧合下现学现卖音频知识处理相关问题。

问题描述

我们查看以下简化播放器代码:


class AACPlayer(private val filePath: String) {

    private val TAG = "AACPlayer"
    private var extractor: MediaExtractor? = null
    private var codec: MediaCodec? = null
    private var audioTrack: AudioTrack? = null
    fun play() {
        try {
            extractor = MediaExtractor().apply {
                setDataSource(filePath)
            }
            var trackIndex = -1
            for (i in 0 until extractor!!.trackCount) {
                val format = extractor!!.getTrackFormat(i)
                val mime = format.getString(MediaFormat.KEY_MIME)
                if (mime!!.startsWith("audio/")) {
                    trackIndex = i
                    break
                }
            }

            if (trackIndex >= 0) {
                extractor!!.selectTrack(trackIndex)
                val format = extractor!!.getTrackFormat(trackIndex)
                val mime = format.getString(MediaFormat.KEY_MIME)
                codec = MediaCodec.createDecoderByType(mime!!)
                codec!!.configure(format, null, null, 0)
                codec!!.start()

                val sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE)
                val channelConfig =
                    if (format.getInteger(MediaFormat.KEY_CHANNEL_COUNT) == 1) AudioFormat.CHANNEL_OUT_MONO else AudioFormat.CHANNEL_OUT_STEREO

                val bufferSize = AudioTrack.getMinBufferSize(
                    sampleRate,
                    channelConfig,
                    AudioFormat.ENCODING_PCM_16BIT
                );
                val audioTrackAttributes =
                    AudioAttributes.Builder()
                        .setUsage(AudioAttributes.USAGE_MEDIA)
                        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                        .build()
                val audioFormat = AudioFormat
                    .Builder()
                    .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                    .setSampleRate(sampleRate)
                    .setChannelMask(channelConfig)
                    .build()
                audioTrack = AudioTrack.Builder()
                    .setAudioAttributes(audioTrackAttributes)
                    .setAudioFormat(audioFormat)
                    .setTransferMode(AudioTrack.MODE_STREAM)
                    .setBufferSizeInBytes(bufferSize)
                    .build()


                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                    logD("bufferSizeInFrames = [${audioTrack?.bufferSizeInFrames}] bufferCapacityInFrames = [${audioTrack?.bufferCapacityInFrames}] bufferSize = [${bufferSize}]  startThresholdInFrames = [${audioTrack!!.startThresholdInFrames}]")
                }
                audioTrack!!.play()


                val inputBuffers = codec!!.inputBuffers
                val outputBuffers = codec!!.outputBuffers
                val bufferInfo = MediaCodec.BufferInfo()

                var isEOS = false
                while (!isEOS) {
                    val inIndex = codec!!.dequeueInputBuffer(10000)
                    if (inIndex >= 0) {
                        val buffer = inputBuffers[inIndex]
                        val sampleSize = extractor!!.readSampleData(buffer, 0)
                        if (sampleSize < 0) {
                            codec!!.queueInputBuffer(
                                inIndex,
                                0,
                                0,
                                0,
                                MediaCodec.BUFFER_FLAG_END_OF_STREAM
                            )
                            isEOS = true
                        } else {
                            val presentationTimeUs = extractor!!.sampleTime
                            codec!!.queueInputBuffer(inIndex, 0, sampleSize, presentationTimeUs, 0)
                            extractor!!.advance()
                        }
                    }

                    var outIndex = codec!!.dequeueOutputBuffer(bufferInfo, 10000)
                    while (outIndex >= 0) {
                        val outBuffer = outputBuffers[outIndex]
                        val bufferBackup = outBuffer.slice()
                        if (outBuffer.remaining() <= 0) {
                            continue
                        }
                        
                        //仅仅为了打印无他用
                        val array = ByteArray(bufferBackup.remaining())
                        bufferBackup.get(array, 0, array.size)
                        logD(array.joinToString(transform = { String.format("%02x", it) }))
                        logD("写入数据大小${array.size} hashCode ${array.contentHashCode()}")


                        audioTrack!!.write(
                            outBuffer,
                            outBuffer.remaining(),
                            AudioTrack.WRITE_BLOCKING
                        )
                        codec!!.releaseOutputBuffer(outIndex, false)
                        outIndex = codec!!.dequeueOutputBuffer(bufferInfo, 0)
                    }
                }

            }
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            extractor?.release()
            codec?.stop()
            codec?.release()
            audioTrack?.flush()
            audioTrack?.stop()
            audioTrack?.release()
        }
    }

    fun logD(msg:String) {
        Log.d(TAG, msg)
    }
}

这也是网上充斥最多的示例代码,但是上面的代码丢失尾帧的音频的问题。

getStartThresholdInFrames文档

Androidaudiotrack有一个缓冲区,调用则可以阻塞或阻塞式使用audiotrack.write向里面写入数据。播放器为提高效率在缓冲大于startThresholdInFrames时取出进行播放。startThresholdInFrames一般大于等于bufferSizeInFrames

你播放音频时不敢保证所有音频数据都是对齐startThresholdInFrames,所以你会以为调用audiotrack.flush可以解决问题了。但是我们阅读相关文档flush文档发现这个API只是丢弃之前的数据,加速audiotrack.write

在这里插入图片描述
解决方案 在音频流写入结束调用audiotrack.stop 这个函数会将未播放的数据进行加载播放在结束。audiotrack.stop文档
在这里插入图片描述
在上述的代码我们如下编写:

class AACPlayer(...) {
	//...
    fun play() {
        try {
			 while (outIndex >= 0) {
			 		//....略
			 			//结束调用stop刷出残余音频
                        if (bufferInfo.size == 0
                            && (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0
                        ) {
                            audioTrack!!.stop()
                            break
                        }
                  //....略
			  }
		}catch(...){
		//...
        }finally{
        	} finally {
            extractor?.release()
            codec?.stop()
            codec?.release()
            audioTrack?.flush()
            //注释多余的stop
            //audioTrack?.stop()
            audioTrack?.release()
        }
        }
}

当然你如果比较骚可以进行补帧操作

class AACPlayer(...) {
	//...
    fun play() {
        try {
			 while (outIndex >= 0) {
			 		//....略
                        if (bufferInfo.size == 0
                            && (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0
                        ) {
                           var interpolationFrame =
                                (audioTrack!!.startThresholdInFrames / bufferSize) + 1
                            while (interpolationFrame > 0) {
                                interpolationFrame--;
                                audioTrack!!.write(
                                    ByteArray(bufferSize), 0, bufferSize
                                )
                            }
                            break
                        }
                  //....略
			  }
		}catch(...){
		//...
        }finally{
        	} finally {
            //略
        }
        }
}

实践

我们有一个极短音频且有效音在末尾,那么在部分手机上将无法听到这个音频
在这里插入图片描述
在某手机上相关输出参数如下:

bufferSizeInFrames = [11310] 
bufferCapacityInFrames = [11310] 
bufferSize = [45240]  
startThresholdInFrames = [11310]

这个文件对应的PCM数据40960(A000h)字节大小。
我们看下这个文件的末端可以看到很多有效数据。
在这里插入图片描述
我们在看看文件最前面PCM数据 全是空数据。
在这里插入图片描述
所以这个文件只有末尾才音频。
我们算一下Audiotrack刷新次数

文件PCM大小/Audiotrack刷新阈值 =40960/11310 = 3.6

假设如果我们有效音频在最后0.6将无法播放

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值