嵌入式音频

本文详细介绍了声音的表示,包括声音的定义、特征及其数学描述,接着探讨了数字化音频的过程,重点讲解了PCM编码和AAC编码,包括背景、编码流程、编码规格和数据格式。此外,还提到了音频封装格式的重要性,以及无损和有损音频格式的区别。

一、声音的表示

1、声音的定义

        『声音』是振动产生的声波,通过介质(气体、固体、液体)传播并能被人或动物听觉器官所感知的波动现象。

2、声音有哪些特征?

        要提取声音的特征,首先要感知到它,人类的听觉感知系统是一个复杂的系统,如下图所示。它是怎么感知声音的呢?简单来讲,声音作为一种机械波,通过空气传播到人耳,在人耳中转变为神经动作电位,神经脉冲到达大脑,人从而感知到声音。

        声音的特征是我们在感知声音并不断对其现象进行研究的过程中逐步识别和提取出来的。声音的特征就是大家熟知的『声音三要素』:

  • 响度:表示声音的大小。

  • 音调:表示声音的高低。

  • 音色:表示声音的特色。

通过这个波形图,我们很难看出声音的有效信息,因为各个频率的波形都叠加在一起了。 借助频谱图来看,我们可以看看下图:

图片

波形可以由多个频率、不同振幅和相位的简单正弦波复合叠加得到的。波形图的横坐标是时间,纵坐标是振幅,表示的是所有频率叠加的正弦波振幅的总大小随时间的变化规律。

将该复合波形进行傅里叶变换,拆解还原成每个频率上单一的正弦波构成,相当于把二维的波形图往纸面方向拉伸,变成了三维的立体模型,而拉伸方向上的那根轴叫频率,现在从小到大每个频率点上都对应着一条不同幅值和相位的正弦波。

频谱图则是在这个立体模型的时间轴上进行切片,形成的以横坐标为频率,纵坐标为幅值的图形。它表示的是一个静态的时间点上,各频率正弦波的幅值大小的分布状况。

波形图可以帮助我们检查音乐整体音量的大小,在混音中常常可以看出动态和响度等问题,可以用来辅助调节压缩器和限制器。频谱图则可以帮助我们定位音乐细节在各频段上的分布问题,在混音中可以用来辅助调节滤波器和均衡器。

3、怎样对声音进行数学描述?

3.1、响度的数学描述

        声压级(Sound Pressure Level,SPL),是以 2×10-5 N/m² 为参考值,任一声压与其比值的对数乘以 20 记为声压级,单位也是『分贝(dB)』。 

        人耳对声音的感觉,与声压有关,但也不是只与声压有关,还和频率有关。声压级相同,频率不同的声音,听起来响度也不同。

        为了在数量上估计一个纯音的响度,可以把这个纯音和 1000 Hz 的某个声压级的纯音在响度上作比较。这两个声音在听觉上认为是相同的响度时,就可以把 1000 Hz 纯音的这个声压级规定为该频率纯音的响度级。响度级的单位为『方(Phon)』。

        举例来说,一个纯音的频率 1000 Hz,若希望其响度能达到 40 方,根据等响度曲线图,其声压级就必须达到 40 dB SPL。

        响度级既考虑了声音的物理效应,又考虑了人耳的听觉生理效应,表示人耳对声音的主观评价。我们日常所说的分贝指的是声压级。

3.2、音调的数学描述

        音调是人耳对声音高低的主观感受。音调对应的客观评价尺度是声波的『频率』。音调的高低是由振动频率决定的,两者成正相关关系。

        频率的计量我们比较熟悉,单位是『赫兹(Hz)』。那么音调是怎么计量呢?一种计量法是将音调的单位称为『美(mel)』,取频率 1000 Hz、声压级为 40 dB 的纯音的音调作标准,称为 1000 mel,另一些纯音,听起来调子高一倍的称为 2000 mel,调子低一倍的称为 500 mel,依此类推,可建立起整个可听频率内的音调标度。

3.3、音色的数学描述

        现实中声音的波形绝大多数都不是简单的正弦波,而是一种复杂的波。这种复杂的波形可以分解为一系列的正弦波,这些正弦波中有基频 f0,它对应声音的基音,还有与 f0 成整数倍关系的谐波:f1、f2、f3、f4 等,它们对应声音的泛音,它们的振幅有特定的比例。这种特定的比例,赋予每种声音特色,这就是音色。如果没有谐波成分,单纯的基频正弦信号是毫无音乐感的。因此,乐器乐音的频率范围包括基频和谐波。

所以,声音的音色决定于谐波频谱,也可以说是声音的波形所确定的。

二、数字化音频

1、怎样对声音进行数字化?

         对声音进行数字化,首先要使用特定的设备对声音进行采集,比如麦克风就是常见的声音采集设备。麦克风里面有一层碳膜,非常薄而且十分敏感。声音是一种纵波,会压缩空气也会压缩这层碳膜,碳膜在受到挤压时也会发出振动,在碳膜的下方就是一个电极,碳膜在振动的时候会接触电极,接触时间的长短和频率与声波的振动幅度和频率有关,这样就完成了声音信号到电信号的转换。之后再经过放大电路处理,就可以实施后面的采样、量化处理了。

        声音由波形组成,包含了不同频率、振幅的波的叠加。为了在数字媒体内表示这些波形,需要对波形进行采样,其采样率需要满足可以表示的声音的最高频率;同时还需要存储足够的位深,以表示声音样本中波形的适当振幅。

        声音由波形组成,包含了不同频率、振幅的波的叠加。为了在数字媒体内表示这些波形,需要对波形进行采样,其采样率需要满足可以表示的声音的最高频率;同时还需要存储足够的位深,以表示声音样本中波形的适当振幅。

        声音处理设备重建频率的能力称为其频率响应,创造适当响度和柔度的能力称为其动态范围,这些术语通常统称为声音设备的保真度。最简单的编码方式可以利用这两个基本元素重建声音,同时还能够高效地存储和传输数据。

        声音的数字化过程是将模拟信号(连续时间信号)转化为数字信号(离散时间信号)的过程,包括 3 个步骤:

  • 采样:以一定采样率在时域内获取离散信号。

  • 量化:每个采样点幅度的数字化表示。

  • 编码:以一定格式存储数据。

其过程如下图所示:

经过数字化处理后的数字音频包含如下三要素:

  • 采样率:奈奎斯特采样定理,采样频率应该不小于模拟信号频谱中最高频率的 2 倍

  • 量化位深:量化位深是对模拟音频信号的幅度轴进行数字化,它决定了模拟信号数字化以后的动态范围

  • 声道数:单声道(Mono)、立体声(Stereo)等

2、数字音频数据是什么?

        数字音频数据,其中最常见的格式是 PCM(Pulse Code Modulation),即脉冲编码调制格式。得到 PCM 数据的主要过程是将话音等模拟信号每隔一定时间进行取样,使其离散化,同时将抽样值按分层单位四舍五入取整量化,同时将抽样值按一组二进制码来表示抽样脉冲的幅值。也就是我们在上文中讲到的采样、量化、编码过程。要计算一个 PCM 音频流的码率需要数字音频的三要素信息即可:码率 = 采样率 × 量化位深 × 声道数。在处理 PCM 数据时,对于音频不同声道的数据,有两种不同的存储格式:

  • 交错格式:不同声道的数据交错排列。

  • 平坦格式:相同声道的数据聚集排列。

        由于 PCM 编码是无损编码,且广泛应用,所以我们通常可以认为音频的裸数据格式就是 PCM 的。但为了节省存储空间以及传输成本,通常我们会对音频 PCM 数据进行压缩,这也就是音频编码,比如 MP3、AAC、OPUS 都是我们常见的音频编码格式。

三、音频编码     

        对音频或视频进行编码最重要目的就是为了进行数据压缩,以此来降低数据传输和存储的成本

        拿音频来举例,一路采样率为 44100 Hz,量化位深为 16 bit,声道数为 2 的声音,如果不进行编码压缩,对应的码率是:441000 Hz * 16 bit * 2 = 1411200 bps = 1.346 Mbps。一分钟的时间所需要的数据量是:1.346 Mbps * 60 s = 80.75 Mb = 10.09 MB

        对于单单一路音频来说,这个数据量还是比较大的,在存储或传输时如果能进行压缩编码,可以一定程度上提高效率。

要对音频数据进行编码压缩,主要是寻找音频数据中的冗余信息对其进行压缩:

 

对音频进行编码常见的格式有:

  • PCM,无压缩。一种将模拟信号的数字化方法,无损编码。

  • WAV,无压缩。有多种实现方式,但是都不会进行压缩操作。其中一种实现就是在 PCM 数据格式的前面加上 44 字节,分别用来描述 PCM 的采样率、声道数、数据格式等信息。音质非常好,大量软件都支持。

  • MP3,有损压缩。音质在 128 Kbps 以上表现还不错,压缩比比较高,大量软件和硬件都支持,兼容性好。

  • AAC,有损压缩。在小于 128 Kbps 的码率下表现优异,并且多用于视频中的音频编码。

  • OPUS,有损压缩。可以用比 MP3 更小的码率实现比 MP3 更好的音质,高中低码率下均有良好的表现,兼容性不够好,流媒体特性不支持。适用于语音聊天的音频消息场景。

PCM 是音频原始数据的基础格式;AAC 则在短视频和直播场景广泛使用。

1、PCM 编码

 前文已经介绍

2、AAC 编码

2.1、背景介绍

        AAC,英文全称 Advanced Audio Coding,是由 Fraunhofer IIS、杜比实验室、AT&T、Sony 等公司共同开发,在 1997 年推出的基于 MPEG-2 的有损数字音频压缩的专利音频编码标准。2000 年,MPEG-4 标准在原 AAC 的基础上加上了 LTP(Long Term Prediction)、PNS(Perceptual Noise Substitution)、SBR(Spectral Band Replication)、PS(Parametric Stereo)等技术,并提供了多种扩展工具。

        AAC 作为 MP3 的后继者而被设计出来,综合了许多新的技术,有很多新的特性,它支持从 8k 到 96k 的各种采样率,支持多种声道配置方案。在相同的比特率之下,AAC 相较于 MP3 通常可以达到更好的声音质量。

2.2、编码工具及流程   

        AAC 属于感知音频编码。与所有感知音频编码类似,其原理是利用人耳听觉的掩蔽效应,对变换域中的谱线进行编码,去除将被掩蔽的信息,并控制编码时的量化噪声不被分辨。

        在编码过程中,时域信号先通过滤波器组(进行加窗 MDCT 变换)分解成频域谱线,同时时域信号经过心理声学模型获得信掩比,掩蔽阈值,M/S 立体声编码以及强度立体声编码需要的控制信息,还有滤波器组中应使用长短窗选择信息。瞬时噪声整形(TNS)模块将噪声整形为与能量谱包络形状类似,控制噪声的分布。强度立体声编码和预测以及 M/S 立体声编码都能有效降低编码所需比特数,随后的量化模块用两个嵌套循环进行了比特分配并控制量化噪声小于掩蔽阈值,之后就是改进了码本的哈夫曼编码。这样,与前面各模块得到的边带信息一起,就能构成 AAC 码流了。

        AAC 系统包含了增益控制、滤波器组、心理声学模型、量化与编码、预测、TNS、立体声处理等多种高效的编码工具。这些模块或过程的有机组合形成了 AAC 系统的基本编解码流程。

        在实际应用中,并不是所有的功能模块都是必需的,下表列出了 MPEG-2 AAC 各模块的可选性:

ISO/IEC 13818-7 标准中 MPEG-2 AAC 的编码流程如图:

图片

对应的 MPEG-2 AAC 的解码流程如图:

图片

2.3、编码规格   

为了能够适应于不同的应用场合,在 MPEG-2 AAC 标准中定义了三种不同编码规格:

1)MPEG-2 AAC LC(Low Complexity),低复杂度规格。用于要求在有限的存储空间和计算能力的条件下进行压缩场合。在这种框架中,没有预测和增益控制这两种工具,TNS 的阶数比较低。编码码率在 96kbps-192kbps 之间的可以用该规格。MP4 的音频部分常用该规格。

2)MPEG-2 AAC Main,主规格。具有最高的复杂度,可以用于存储量和计算能力都很充足的场合。在这种框架中,利用了除增益控制以外的所有编码工具来提高压缩效率。

3)MPEG-2 AAC SSR(Scalable Sample Rate),可变采样率规格。在这种框架中,使用了增益控制工具,但是预测和耦合工具是不被允许的,具有较低的带宽和 TNS 阶数。对于最低的一个 PQF 子带不使用增益控制工具。当带宽降低时,SSR 框架的复杂度也可降低,特别适应于网络带宽变化的场合。

Main 和 LC 框架是变化编码算法,采用 MDCT 作为其时/频分析模块,SSR 框架则采用混合滤波器组,先将信号等带宽地分成 4 个子带,再作 MDCT 变换。在三种方案里,通过选用不同模块在编码质量和编码算法复杂度之间进行折衷。

在 MPEG-4 AAC 标准中除了继承上面的三种规格进行改进外,还新增了三种编码规格:

1)MPEG-4 AAC LC(Low Complexity),低复杂度规格。

2)MPEG-4 AAC Main,主规格。

3)MPEG-4 AAC SSR(Scalable Sample Rate),可变采样率规格。

4)MPEG-4 AAC LD(Low Delay),低延迟规格。AAC 是感知型音频编解码器,可以在较低的比特率下提供很高质量的主观音质。但是这样的编解码器在低比特率下的算法延时往往超过 100ms,所以并不适合实时的双向通信。结合了感知音频编码和双向通信必须的低延时要求。可以保证最大 20ms 的算法延时和包括语音和音乐的信号的很好的音质。

5)MPEG-4 AAC LTP(Long Term Predicition),长时预测规格。在 Main Profile 的基础上增加了前向预测。

6)MPEG-4 AAC HE(High Efficiency),高效率规格。混合了 AAC 与 SBR(Spectral Band Replication,频段复制)技术。而新版本的 HE,即 HE v2 是 AAC 加上 SBR 和 PS(Parametric Stereo,参数立体声)技术。这样能在同样的效果上使用更低的码率。在编码码率为 32-96 Kbps 之间的音频文件时,建议首选这种规格。

2.4、AAC 格式

2.4.1、Audio Object Types

MPEG-4 标准包含了多种 AAC 的版本,像上面提到的 AAC-LC、HE-AAC、AAC-LD 等等,在标准中定义了编解码工具模块、音频对象类型(Audio Object Types)、编码规格(Profiles)来指定编码器。其中音频对象类型(Audio Object Types)是最主要的标记编码器的方式

2.4.2、Audio Specific Config

在传输和存储 MPEG-4 音频时,音频对象类型(Audio Object Types)和音频的基础信息(比如采样率、位深、声道)必须被编码,这些信息通常在 AudioSpecificConfig 数据结构中来指定。AudioSpecificConfig 的信息使得我们可以不用传输 AAC 比特流就能让解码器理解音频的相关信息,这对于编解码器协商期间的设置很有用,例如对于 SIP(Session Initiation Protocol,会话初始协议)或 SDP(Session Description Protocol,会话描述协议)的初始化设置。

2.4.3、数据格式

MPEG-4 的传输或存储格式一般都包含 Raw Data Blocks 或 Access Units,其中装载的是实际的音频编码数据的比特流。这些比特流又通过灵活的方式被分为代表不同声道的部分。取到这些数据后,我们就要进一步解析它们的数据格式了。

MPEG-2 AAC 的音频编码数据格式有以下两种:

1)ADIF(Audio Data Interchange Format),音频数据交换格式。这种格式的特征是可以确定的找到这个音频数据的开始,不需进行在音频数据流中间开始的解码,即它的解码必须在明确定义的开始处进行。这种格式常用在文件存储中。

2)ADTS(Audio Data Transport Stream),音频数据传输流。这种格式的特征是它是一个有同步字的比特流,解码可以在这个流中任何位置开始。这种格式适用于传输流。

MPEG-4 AAC 又增加了两种音频编码数据格式,新增的格式不仅针对传统的 AAC,还针对新的变体:AAC-LD、AAC-ELD。

1)LATM(Low-overhead MPEG-4 Audio Transport Multiplex)。这种格式有独立的比特流,允许使用 MPEG-4 中的错误恢复语法。

2)LOAS(Low Overhead Audio Stream)。这种格式是带有同步信息的 LATM,可以支持随机访问或跳过。

四、音频封装

        音频封装格式一般由:多媒体信息+音频流+封面流+歌词流组成。有些音乐会包含封面和歌词,则对应有封面流、歌词流。多媒体信息包括:标题、艺术家、专辑、作曲、音乐风格、日期、码率、时长、声道布局、采样率、音频编码器等。而音频封装包括:mp3、m4a、ogg、amr、wma、aac、wav、flac、ape等。前面两篇文章介绍过相关概念:走进音视频的世界——音视频基本概念、走进音视频世界——视频封装格式。音频格式如下图所示:

以下面问题为出发点,揭开音频封装格式的面纱:

① 音乐封面如何获取?

② 音乐歌词如何获取与显示?

③ 有损格式与无损格式有什么区别?

④ 不同封装格式有什么联系,又有什么区别?

先从FFprobe检测到的音频metadata说起,如下图1所示:

从上图可以看出,前半部分是title标题、artist艺术家、album专辑、album_artist艺术家专辑、composer作曲者、genre音乐风格式;中间部分是lyrics歌词,每句歌词前面有对应的时间戳;后半部分是两个流,第0编号的流是音频轨,包含:音频编码器、采样率、声道布局、码率,第1编号的流是封面,其实是一帧图像,包含:图像编码器、像素格式、分辨率。接下来根据上面提出的问题进行展开分析。

1、获取音乐的封面
音乐封面保存在视频图像流中,先解析出图像编码器、像素格式、分辨率等参数,然后根据编码器去寻找对应的解码器,并打开解码器,对图像编码压缩数据进行解码,最终解码出来的图像就是封面了。

2、音乐歌词的获取与显示
上面有提及,每句歌词前面有显示的时间戳,以音频时钟为基准,歌词时间戳同步于音频时间戳。也就是根据音频时间戳来同步解析歌词,然后把歌词回调到应用层渲染显示。另外一个问题,当前歌词什么时候该消失呢?歌词没有具体的显示时长,等待下一个歌词的到来,把当前歌词覆盖。

3、有损格式与无损格式的区别
(1) 无损音乐格式

无损音乐格式有ape、wav、flac三种,其中ape、flac都是基于wav进行压缩。而wav是微软专门为Windows开发的一种标准数字音频文件,文件扩展名wav,是WaveForm的缩写,文件大小计算公式:size=(采样率*量化位数*声道/8)/时间(秒)。一般采样率是44100Hz,量化位数为16位,声道数为2(即立体声道),1分钟的音频占用存储空间约为10M。

在Windows环境下,大多数媒体文件都是按照资源互换文件格式(Resource Interchange File Format)来储存信息的,简称为RIFF格式。构成RIFF的基本单位成为块(Chunk),每个RIFF文件由若干块组成,wav基本结构如下表所示:                                    

每个块由块标识、长度、数据组成,如下代码段:

struct chunk {
    u32 id; //块标识
    u32 size; //块大小
    u8 data[size]; //块内容
};


 其中fmt块由声道数、采样率、码率、块对齐、量化位数五个参数组成,如下表所示:

(2) 有损音乐格式

有损音乐格式包括:mp3、m4a、ogg、amr、wma、aac等。目前最为流行的是mp3(MPEG-1 audio layer3),有着mp3的下一代之称的是aac(Advance Audio Coding)。有损格式压缩率比无损的高,文件占用存储空间小,但是声音还原度不如无损格式。我们下载音乐时,碰到高品质或者无损音质的音乐,通常要VIP会员或按需收费,因为越高品质越接近原声。而无损音乐从理论上能够100%保留声音细节,100%还原原声。无损音质英文简称为SQ(Super Quality,超音质)。关于无损格式与有损格式对比如下表所示:

4、不同封装格式的联系与区别
封装格式共同点是基本结构是相同的,都是由多媒体信息+音频流+封面流+歌词流组成。区别是不同封装格式,采用编码方式不一样,压缩率不一样,音频流子结构不一样。下面是不同封装格式的多方位对比,如表4所示:

实现一个音频封装:

        我们要封装的格式是 M4A,属于 MPEG-4 标准,通常普通的 MPEG-4 文件扩展名是 .mp4,只包含音频的 MPEG-4 文件扩展名用 .m4a

我们来实现 KFMP4Muxer 模块:

public class KFMP4Muxer {
    public static final int KFMuxerErrorCreate = -2200;
    public static final int KFMuxerErrorAudioAddTrack = -2201;
    public static final int KFMuxerErrorVideoAddTrack = -2202;

    private static final String TAG = "KFMuxer";
    private KFMuxerConfig mConfig = null; ///< 封装配置
    private KFMuxerListener mListener = null; ///< 回调
    private MediaMuxer mMediaMuxer = null; ///< 封装实例
    private int mVideoTrackIndex = -1; ///< 视频 track 轨道下标
    private MediaFormat mVideoFormat = null; ///< 视频输入视频格式描述
    private List<KFBufferFrame> mVideoList = new ArrayList<>(); ///< 视频输入缓存
    private int mAudioTrackIndex = -1; ///< 音频 track 轨道下标
    private MediaFormat mAudioFormat = null; ///< 音频输入视频格式描述
    private List<KFBufferFrame> mAudioList = new ArrayList<>(); ///< 音频输入缓存
    private boolean mIsStart = false;
    private Handler mMainHandler = new Handler(Looper.getMainLooper()); ///< 主线程

    public KFMP4Muxer(KFMuxerConfig config, KFMuxerListener listener) {
        mConfig = config;
        mListener = listener;
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public void start() {
        _setupMuxer();
    }

    public void stop() {
        _stop();
    }

    public void setVideoMediaFormat(MediaFormat mediaFormat) {
        mVideoFormat = mediaFormat;
    }

    public void setAudioMediaFormat(MediaFormat mediaFormat) {
        mAudioFormat = mediaFormat;
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    ///< 写入音视频数据(编码后数据)。
    public void writeSampleData(boolean isVideo, ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo) {
        if ((bufferInfo.flags & BUFFER_FLAG_CODEC_CONFIG) != 0) {
            return;
        }

        if (buffer ==null || bufferInfo == null || mMediaMuxer == null || bufferInfo.size == 0) {
            return;
        }

        ///< 校验视频数据是否进入。
        if (!_hasAudioTrack() && !isVideo) {
            return;
        }

        ///< 校验视频数据是否进入。
        if (!_hasVideoTrack() && isVideo) {
            return;
        }

        ///< 数据转换结构体 KFBufferFrame。
        KFBufferFrame packet = new KFBufferFrame();
        ByteBuffer newBuffer = ByteBuffer.allocateDirect(bufferInfo.size);
        newBuffer.put(buffer).position(0);

        MediaCodec.BufferInfo newInfo = new MediaCodec.BufferInfo();
        newInfo.size = bufferInfo.size;
        newInfo.flags = bufferInfo.flags;
        newInfo.presentationTimeUs = bufferInfo.presentationTimeUs;

        packet.buffer = newBuffer;
        packet.bufferInfo = newInfo;
        if (isVideo) {
            ///< 初始化视频 Track。
            if (mVideoFormat != null && mVideoTrackIndex == -1) {
                _setupVideoTrack();
            }
            mVideoList.add(packet);
        } else {
            ///< 初始化音频Track
            if (mAudioFormat != null && mAudioTrackIndex == -1) {
                _setupAudioTrack();
            }
            mAudioList.add(packet);
        }

        ///< 校验音视频 Track 是否都初始化好。
        if ((_hasAudioTrack() && _hasVideoTrack() && mAudioTrackIndex >=0 && mVideoTrackIndex >= 0) ||
                (_hasAudioTrack() && !_hasVideoTrack() && mAudioTrackIndex >= 0) ||
                (!_hasAudioTrack() && _hasVideoTrack() && mVideoTrackIndex >= 0)) {
            if (!mIsStart) {
                _start();
                mIsStart = true;
            }

            ///< 音视频交错,目的音视频时间戳尽量不跳跃。
            if(mIsStart){
                _avInterleavedBuffers();
            }
        }
    }

    public void release() {
        _stop();
    }

    private void _start() {
        ///< 开启封装。
        try {
            if (mMediaMuxer != null) {
                mMediaMuxer.start();
            }
        } catch (Exception e) {
            Log.e(TAG, "start" + e);
        }
    }

    private void _stop() {
        ///< 关闭封装
        try {
            if (mMediaMuxer != null) {
                ///< 兜底一路没进来的 case,如果外层配置音视频一起封装但最终只进来一路也会处理。
                if (!mIsStart && (mVideoTrackIndex != 0 || mAudioTrackIndex != 0) && (mVideoList.size() > 0 || mAudioList.size() > 0)) {
                    mMediaMuxer.start();
                    mIsStart = true;
                }

                ///< 将缓冲中数据推入封装器。
                if (mIsStart) {
                    _appendAudioBuffers();
                    _appendVideoBuffers();
                    mMediaMuxer.stop();
                }

                ///< 释放封装器实例。
                mMediaMuxer.release();
                mMediaMuxer = null;
            }
        } catch (Exception e) {
            Log.e(TAG, "stop release" + e);
        }
        ///< 清空相关缓存与标记位。
        mVideoTrackIndex = -1;
        mAudioTrackIndex = -1;
        mIsStart = false;
        mVideoList.clear();
        mAudioList.clear();
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private boolean _hasAudioTrack() {
        return (mConfig.muxerType.value() & KFMediaBase.KFMediaType.KFMediaAudio.value()) != 0;
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private boolean _hasVideoTrack() {
        return (mConfig.muxerType.value() & KFMediaBase.KFMediaType.KFMediaVideo.value()) != 0;
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void _setupMuxer() {
        ///< 初始化封装器。
        if(mMediaMuxer == null){
            try {
                mMediaMuxer = new MediaMuxer(mConfig.outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
            } catch (IOException e) {
                Log.e(TAG, "new MediaMuxer" + e);
                _callBackError(KFMuxerErrorCreate,e.getMessage());
                return;
            }
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void _setupVideoTrack() {
        ///< 根据外层输入格式描述初始化视频 Track。
        if (mVideoFormat != null) {
            ///< 添加视频 Track。
            try {
                mVideoTrackIndex = mMediaMuxer.addTrack(mVideoFormat);
            } catch (Exception e) {
                Log.e(TAG, "addTrack" + e);
                _callBackError(KFMuxerErrorVideoAddTrack,e.getMessage());
            }
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void _setupAudioTrack() {
        ///< 根据外层输入格式描述初始化音频 Track。
        if(mAudioFormat != null){
            ///< 添加音频 Track。
            try {
                mAudioTrackIndex = mMediaMuxer.addTrack(mAudioFormat);
            } catch (Exception e) {
                Log.e(TAG, "addTrack" + e);
                _callBackError(KFMuxerErrorAudioAddTrack,e.getMessage());
            }
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void _avInterleavedBuffers() {
        ///< 音视频交错,通过对比时间戳大小交错进入。
        if (_hasVideoTrack() && _hasAudioTrack()) {
            while (mAudioList.size() > 0 && mVideoList.size() > 0) {
                KFBufferFrame audioPacket = mAudioList.get(0);
                KFBufferFrame videoPacket = mVideoList.get(0);

                if (audioPacket.bufferInfo.presentationTimeUs >= videoPacket.bufferInfo.presentationTimeUs) {
                    mMediaMuxer.writeSampleData(mVideoTrackIndex,videoPacket.buffer,videoPacket.bufferInfo);
                    mVideoList.remove(0);
                } else {
                    mMediaMuxer.writeSampleData(mAudioTrackIndex,audioPacket.buffer,audioPacket.bufferInfo);
                    mAudioList.remove(0);
                }
            }
        } else if (_hasVideoTrack()) {
            _appendVideoBuffers();
        } else if (_hasAudioTrack()) {
            _appendAudioBuffers();
        }
    }

    private void _appendAudioBuffers() {
        ///< 音频队列缓冲区推到封装器。
        while (mAudioList.size() > 0) {
            KFBufferFrame packet = mAudioList.get(0);
            mMediaMuxer.writeSampleData(mAudioTrackIndex,packet.buffer,packet.bufferInfo);
            mAudioList.remove(0);
        }
    }

    private void _appendVideoBuffers() {
        ///< 视频队列缓冲区推到封装器。
        while (mVideoList.size() > 0) {
            KFBufferFrame packet = mVideoList.get(0);
            mMediaMuxer.writeSampleData(mVideoTrackIndex,packet.buffer,packet.bufferInfo);
            mVideoList.remove(0);
        }
    }

    private void _callBackError(int error, String errorMsg) {
        ///< 错误回调。
        if (mListener != null) {
            mMainHandler.post(()->{
                mListener.muxerOnError(error,TAG + errorMsg);
            });
        }
    }
}
  • 1)创建封装器实例。调用 start

    • 在 _setupMuxer 方法中实现,通过输出路径与格式 2 个参数生成。

  • 2)创建音视频轨道及添加音频和视频数据。调用 writeSampleData: 检测音视频数据会创建对应的轨道。

    • 在 _setupVideoTrack 与 _setupAudioTrack 方法中实现。音频和视频的格式描述分别为mVideoFormatmAudioFormat

    • 当音频轨道与视频轨道都创建好后,会触发真正的开始 _start。这样设计的原因是外层可能优先输入音频或视频,但封装器开始前又需要创建音视频轨道,所以这里实现了等待逻辑。

  • 3)用两个队列作为缓冲区,分别管理音频和视频待封装数据。

    • 这两个队列分别是 mAudioList 和 mVideoList,存储数据类型为 KFBufferFrame

    • 每次当外部调用 writeSampleData: 方法送入待封装数据时,都是把数据放入两个队列中的一个,以便根据情况进行后续的音视频数据交织。

  • 4)同时封装音频和视频数据时,进行音视频数据交织。

    • 在 _avInterleavedBuffers 方法中实现音视频数据交织。当带封装的数据既有音频又有视频,就需要根据他们的时间戳信息进行交织,这样便于在播放该音视频时提升体验。

  • 5)音视频数据写入封装。

    • 同时封装音频和视频数据时,在做完音视频交织后,即分别将交织后的音视频数据写入封装器 mMediaMuxer writeSampleData。在 _avInterleavedBuffers 中实现。

    • 单独封装音频或视频数据时,则直接将数据写入封装器 mMediaMuxer writeSampleData。分别在 _appendAudioBuffers 和 _appendVideoBuffers 方法中实现。

  • 6)停止写入。

    • 在 stop → _stop 方法中实现。

    • 在停止前,还需要消费掉 mAudioList 和 mVideoList 的剩余数据,要调用 _appendAudioBuffers 与 _appendVideoBuffers

    • 封装器执行停止操作 mMediaMuxer stop

五、参考

https://mp.weixin.qq.com/s/b4XkNaZWnSLx8KxqV0Ul1A

https://mp.weixin.qq.com/s/LOUnbvYSNBONE9TgCxjLvw

https://mp.weixin.qq.com/s/lexAVx_O3Kz3-51OZ3TlLw

https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MTkxOTQyMQ==&action=getalbum&album_id=2277742064979214337&scene=173&from_msgid=2257485647&from_itemidx=1&count=3&nolastread=1#wechat_redirect

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值