Android audiotrack尾帧无声

news2024/11/25 18:31:26

前言

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

问题描述

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


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将无法播放

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1511957.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

nRF52832——串口 UART 和 UARTE 外设应用

nRF52832——串口 UART 和 UARTE 外设应用 UART 和 UARTE 原理UART 功能描述UARTE 功能介绍 应用实例串口打印实例串口输入与回环UART 模式串口中断 UART 和 UARTE 原理 UART 功能描述 串口 UART 也称为通用异步收发器。是各种处理器中常用的通信接口&#xff0c;在 nRF52 芯…

微信小程序实现上下手势滑动切换

效果图 思路 实现一个微信小程序的复合滚动页面&#xff0c;主要通过Swiper组件实现垂直方向的轮播功能&#xff0c;每个轮播项内部使用Scroll-View组件来展示可垂直滚动的长内容&#xff0c;如图片和文本。 代码 <!-- wxml --> <view class"swiper-container…

Spring Boot+Vue前后端分离项目如何部署到服务器

&#x1f31f; 前言 欢迎来到我的技术小宇宙&#xff01;&#x1f30c; 这里不仅是我记录技术点滴的后花园&#xff0c;也是我分享学习心得和项目经验的乐园。&#x1f4da; 无论你是技术小白还是资深大牛&#xff0c;这里总有一些内容能触动你的好奇心。&#x1f50d; &#x…

搜索引擎SEO策略介绍

baidu搜索&#xff1a;如何联系八爪鱼SEO baidu搜索&#xff1a;如何联系八爪鱼SEO baidu搜索&#xff1a;如何联系八爪鱼SEO 第一、 关键词的选择策略&#xff1a; 1、门户类的网站关键词选择策略&#xff1a; 网站每个页面本身基本都包含有关键词&#xff1a;网站拥有上百…

STM32-PWR电源控制

PWR(Power Control)电源控制 管理STM32内部的电源供电部分&#xff0c;可以实现可编程电压检测器和低功耗模式的功能。 电源管理器 上电复位&#xff08;POR&#xff09;和掉电复位&#xff08;PDR&#xff09; STM32内部有一个完整的上电复位(POR)和掉电复位(PDR)电路&…

免费搭建导航网站教程带免费空间域名源码

使用免费空间和免费域名免费搭建一个导航网站 手把手视频教程 https://pan.xunlei.com/s/VNsoMehs7RCjz3IClV6h2vNMA1?pwdq596#

【阿里云系列】-部署ACK集群的POD应用日志如何集成到日志服务(SLS)中

介绍 我们在实际部署应用到阿里云的ACK集群后&#xff0c;由于后期应用服务的持续维护诉求可能需要跟踪排查问题&#xff0c;此时就要具备将应用的历史日志存档便于后期排查问题 处理方式 为了解决以上的普遍需求&#xff0c;需要将ACK中的应用日志采集到SLS的Logstore中,然…

照片怎么调到100kb以下?图片压缩可以这样做

在需要通过网络传输或共享图片的场景中&#xff0c;限制文件大小对于提高传输速度和节省带宽非常重要&#xff0c;将图片压缩到100k的文件大小可以确保更快地上传、下载和共享图片&#xff0c;适用于电子邮件、社交媒体、在线相册等网络传输场景&#xff0c;那么如何快速的将图…

云原生之容器编排实践-ruoyi-cloud项目部署到K8S:Nginx1.25.3

背景 前面搭建好了 Kubernetes 集群与私有镜像仓库&#xff0c;终于要进入服务编排的实践环节了。本系列拿 ruoyi-cloud 项目进行练手&#xff0c;按照 MySQL &#xff0c; Nacos &#xff0c; Redis &#xff0c; Nginx &#xff0c; Gateway &#xff0c; Auth &#xff0c;…

C++开发基础——IO操作与文件流

一&#xff0c;基础概念 C的IO操作是基于字节流&#xff0c;并且IO操作与设备无关&#xff0c;同一种IO操作可以在不同类型的设备上使用。 C的流是指流入/流出程序的字节序列&#xff0c;在输入操作中数据从外部设备(键盘&#xff0c;文件&#xff0c;网络等)流入程序&#x…

Qt 使用RAW INPUT获取HID触摸屏,笔设备,鼠标的原始数据,最低受支持的客户端:Windows XP [仅限桌面应用]

在开发绘图应用程序时&#xff0c;经常会需要读取笔设备的数据&#xff0c;通过对笔数据的解析&#xff0c;来判断笔的坐标&#xff0c;粗细。如果仅仅只是读取鼠标的坐标&#xff0c;就需要人为在应用程序端去修改笔的粗细&#xff0c;并且使用体验不好&#xff0c;如果可以实…

SQL笔记——数据库系统导论(数据库的设计)

目录 数据依赖范式第一范式1NF第二范式2NF第三范式3NFBC范式 数据依赖的公理系统闭包最小依赖集候选码 设计需求分析概念结构设计E-R图的概念模型E-R模型转换为关系模型(指出转换结果中每个关系模式的主码和外码) 逻辑结构设计物理结构设计数据库实施数据库运行和维护 数据依赖…

安装配置MySQL

安装配置MySQL主要包括以下步骤&#xff1a; 1、检查并卸载旧版本的MySQL 2、如果不使用Mariadb&#xff0c;则也需要卸载 3、离线安装MySQL MySQL数据库安装在node3节点上&#xff0c;其他节点通过远程访问的方式使用MySQL数据库。 在node3检查并卸载老版本的MySQL 使用…

ARMv8架构特殊寄存器介绍-1

1&#xff0c;ELR寄存器&#xff08;Exception Link Register &#xff09; The Exception Link Register holds the exception return address。 异常链接寄存器保存异常返回地址。最常用也很重要。 2&#xff0c;SPSR&#xff08;Saved Process Status Register&#xff09;…

Linux环境下,QtCreator运行不起来

文章目录 一、qtcreator运行不起来二、错误信息三、下载libxcb-cursor四、安装 一、qtcreator运行不起来 直接点击qtcreator运行不起来 然后再命令行界面下&#xff0c; 进入到qtcreator所在的目录&#xff1a; cd /opt/Qt/Tools/QtCreator/bin 运行程序&#xff1a;./qtcr…

考研C语言复习进阶(1)

目录 1. 数据类型介绍 1.1 类型的基本归类&#xff1a; 2. 整形在内存中的存储 2.1 原码、反码、补码 2.2 大小端介绍 3. 浮点型在内存中的存储 ​编辑 1. 数据类型介绍 前面我们已经学习了基本的内置类型&#xff1a; char //字符数据类型 short //短整型 int /…

使用 Jenkins 和 Spinnaker 构建 Kubernetes CI/CD

无论您是新手还是持续集成和持续交付以及容器化领域的经验丰富&#xff0c;本文都将为您提供设置 Spinnaker 以满足您的软件应用程序交付需求的基本知识。 了解 Jenkins、Spinnaker 和 Kubernetes Kubernetes 和 Jenkins 是两个强大的工具&#xff0c;它们相互配合&#xff0…

图扑数字孪生楼宇智控可视化平台

从概念提出到风险评估再到跟踪实施&#xff0c;关于智慧园区规划与建设的探讨从未停止。传统楼宇控制系统的各子系统独立存在并不互通&#xff0c;所有信息交互都依赖于中央控制器&#xff0c;导致系统控制的实时性较差。 利用大数据、云计算等智能化技术&#xff0c;让人、物…

《计算机网络》考研:2024/3/11:2.1.6-习题精选(5、6题暂未完成)

2024/3/11 2.1.6 习题精选 一、单项选择题 我的答案&#xff1a;1.D 2.A 3.C 4.B 5.A 标准答案&#xff1a;1.D 2.A 3.B 4.B 5.A 3、【解】&#xff1a; 并行传输的特点&#xff1a;距离短、速度快。 串行传输的特点&#xff1a;距离长、速度慢。 在计算机内部通常为了保证速…

介绍kubernetes的功能与架构及其组件

一、功能简介 1、服务发现和负载均衡 Kubernetes 可以使用 DNS 名称或自己的 IP 地址公开容器, 如果进入容器的流量很大, Kubernetes 可以负载均衡并分配网络流量, 从而使部署稳定. 常用的DNS插件为coreDNS, 用作服务发现和集群中容器通讯; 负载均衡器常使用集群内的service资…