H264基本概念之 采样(sampling)、帧(frame)与场(field)

本文介绍了视频信号的采样过程,包括时间采样和空间采样,并阐述了帧与场的概念。帧是逐行扫描得到的图像,而场则是隔行扫描形成的。讨论了帧频、场频的选择及其历史背景,并对比了帧编码与场编码在不同场景下的适用性。

 一、视频信号的采样

(上图来自参考文献2)

 

       视频信号不同于音频信号,它是一个随着时间和空间时变的二维信号,通过采样、编码将模拟信号转换到数字信号,方便传输和存储。视频信号是由一系列的图像组成,时间上对视频模拟信号进行采样就是每个一段时间提取一个图像,而空间采样是指每一个图像由多少个像素点组成,像素点越多,图像画面越清晰。人的视觉系统决定了采样频率,当采样频率是10~20帧每秒,对快速运动的图像,人眼可以感觉到图像不流畅;当采样频率为20~30帧时,基本上看起来比较流畅。当采样频率更高时,其实图像的质量提升并不明显,受人眼的分辨率限制,但数据量却增加了很多。空间采样决定了图像的像素数,由于现在有三种彩色电视制式,为了互相可以视频通信,往往采用一种中间公共格式(CIF)。

 

二、帧与场的概念

      帧:当采样视频信号时,如果是通过逐行扫描,那么得到的信号就是一帧图像,通常帧频为25帧每秒(PAL制)、30帧每秒(NTSC制);

      场:当采样视频信号时,如果是通过隔行扫描(奇、偶数行),那么一帧图像就被分成了两场,通常场频为50Hz(PAL制)、60Hz(NTSC制);

 

      帧频、场频的由来:最早由于抗干扰和滤波技术的限制,电视图像的场频通常与电网频率(交流电)相一致,于是根据各地交流电频率不同就有了欧洲和中国等PAL制的50Hz和北美等NTSC制的60Hz,但是现在并没有这样的限制了,帧频可以和场频一样,或者场频可以更高。

<think>我们正在讨论的是如何为多个AAC裸数据添加ADTS头。根据之前的讨论,我们知道ADTS(Audio Data Transport Stream)是一种用于AAC音频的封装格式,它在每个AAC前面添加一个头信息,使得每个都可以独立解码。 从引用[1]中我们知道:ADTS在每个AAC前面添加一个ADTS头,而ADIF则在整个AAC数据的开始添加一个头。ADTS适用于流传输,因为它允许任意解码。 引用[3]提到,从MP4文件中提取的AAC数据包需要添加ADTS头,这样解码器才能正确解码。 引用[4]指出ADTS头中有一个字段“Number of Raw Data Blocks in Frame”,这个字段表示ADTS中AAC原始数据块的数量。通常,这个值设置为0,表示该ADTS包含一个AAC原始数据块(即一个AAC)。但是,这个字段也可以大于0,表示该ADTS包含多个AAC原始数据块,即多个AAC。 因此,给多个AAC数据添加ADTS头有两种方法: 1. 为每个AAC裸数据单独添加一个ADTS头(每个ADTS只包含一个AAC)。 2. 将多个AAC裸数据合并成一个大的数据块,然后在前面添加一个ADTS头,并在ADTS头中设置“Number of Raw Data Blocks in Frame”字段为AAC的个数(即大于1)。但是要注意,这种方式并不常见,因为大多数解码器可能只支持每个ADTS包含一个AAC。 然而,在实际应用中,为了兼容性简单性,通常采用第一种方法:每个AAC裸数据(即每个AAC)前面都添加一个ADTS头,形成一个独立的ADTS。 步骤: 1. 获取AAC裸数据(每个裸数据是一个AAC原始)。 2. 为每个AAC裸数据生成一个ADTS头(7字节或9字节,通常为7字节)。 3. 将ADTS头对应的AAC裸数据拼接起来,形成一个完整的ADTS。 ADTS头的结构(7字节版本): 字节1-2: 同步字(0xFFF),12位。 字节3: 版本(0表示MPEG-4,1表示MPEG-2)、层(总是00)、保护缺失位(1表示没有CRC,0则有CRC)、配置信息(profile)的2位。 字节4: 配置信息(profile)的2位(加上前一字节的2位,共4位,实际使用低4位的高2位?)、采样率索引(4位)。 字节5: 通道配置(高2位)、原始数据块数量(低1位,通常为0)、长度(高2位)【注意:这里需要进一步拆解】 实际上,ADTS头各字段的位分配如下(7字节,共56位): - 同步字(12位):0xFFF - 版本(1位):0 for MPEG-4, 1 for MPEG-2 - 层(2位):'00' - 保护缺失(1位):1表示没有CRC,0则有CRC(如果有CRC,则ADTS头变为9字节,后面加2字节CRC) - 配置信息(4位):等于AAC的profile(如LC为2)减1,即实际值为profile-1 - 采样率索引(4位):见下表 - 私有位(1位) - 通道配置(3位) - 原始数据块数量(1位):0表示该ADTS包含一个AAC,1表示包含多个(但具体多个需要后续字段?实际上,这个字段只有一个位,只能表示0或1,但标准中有一个字段叫做“number_of_raw_data_blocks_in_frame”,它实际上是在第5字节的低2位(如果只有一个数据块,则为0;如果有多个,则放在后面的扩展部分)。但标准中,当保护缺失位为1时(即没有CRC),在通道配置之后是一个长度字段(13位),然后是缓冲器满度(11位),最后是原始数据块数量(2位)。所以实际上,在7字节的ADTS头中,原始数据块数量占2位(在字节5的低2位字节6的高2位?)【这里需要澄清】 实际上,更标准的描述(参考ISO/IEC 13818-7): ADTS头固定部分(28位,即3.5字节): - syncword: 12 bits (0xFFF) - ID: 1 bit (0: MPEG-4, 1: MPEG-2) - layer: 2 bits (always '00') - protection_absent: 1 bit - profile: 2 bits (实际是配置信息,表示使用的AAC profile,值为profile枚举值减1) - sampling_frequency_index: 4 bits (采样率索引,对应一个表) - private_bit: 1 bit - channel_configuration: 3 bits (声道配置) - original/copy: 1 bit (0: original, 1: copy) - home: 1 bit 然后是可变部分(当protection_absent为0时,后面有16位CRC,否则没有): - copyright_identification_bit: 1 bit - copyright_identification_start: 1 bit - frame_length: 13 bits (包括ADTS头长度+AAC长度,所以是整个ADTS的长度) - adts_buffer_fullness: 11 bits (0x7FF表示可变码率) - number_of_raw_data_blocks_in_frame: 2 bits (值为0表示有1个AAC,1表示有2个,...,3表示有4个?实际上,这个值等于块数减1?) 但是,根据引用[4]:ADTS中有一个字段“Number of Raw Data Blocks in Frame”,它表示ADTS中AAC原始数据块的数量。这个字段在ADTS头中的位置是在第5第6字节中。 重新组织一下7字节ADTS头的结构(没有CRC的情况): 字节1: 0xFF (同步字高8位) 字节2: 0xF? (同步字低4位,后面4位开始其他字段) 具体位布局(从高位到低位,从左到右): 字节1: 8位:11111111 字节2: 高4位:1111(同步字低4位),低4位:版本(1位)、层(2位)、保护缺失(1位) -> 所以字节2:1111 + ID(1)+layer(2)+protection_absent(1) 字节3: 高2位:配置信息(profile)的高2位(实际上整个配置信息是2位?不对,前面引用说配置信息是4位) -> 这里需要修正。 实际上,标准中是: 同步字:12位(0xFFF) 然后紧接着是: ID: 1位 layer: 2位 protection_absent: 1位 profile: 4位(这里是4位,表示配置信息)?但是前面引用[4]说配置信息是2位?需要确认。 根据ISO/IEC 13818-7(ADTS)的定义: profile(配置信息)是2位(因为AAC profile只有4种:Main, LC, SSR, LTP),所以用2位表示,但实际上在ADTS头中,这个字段是2位,然后后面紧跟着4位的采样率索引。 因此,7字节ADTS头的位布局(56位): 位0-11: 同步字(0xFFF) 位12: ID (1 bit) 位13-14: layer (2 bits) (00) 位15: protection_absent (1 bit) 位16-17: profile (2 bits) // 例如AAC LC为1(因为profile枚举值:1=Main, 2=LC, 3=SSR, 4=LTP,所以LC对应二进制01,即1) 位18-21: 采样率索引 (4 bits) // 例如44.1kHz对应4 位22: 私有位 (1 bit) 位23-25: 通道配置 (3 bits) // 例如双声道为2 位26: 原始/复制 (1 bit) 位27: home (1 bit) 位28: 版权标识位 (1 bit) 位29: 版权标识开始 (1 bit) 位30-42: 长度 (13 bits) // 整个ADTS的长度,包括7字节头+AAC长度 位43-53: 缓冲器满度 (11 bits) // 0x7FF表示可变码率 位54-55: 原始数据块数量 (2 bits) // 0表示该ADTS包含1个AAC,1表示2个,2表示3个,3表示4个?但实际这个字段表示的是在ADTS中除了第一个数据块之外,后面还有几个数据块。因此,总的AAC数 = number_of_raw_data_blocks_in_frame + 1 因此,如果我们要在一个ADTS中包含多个AAC裸数据(比如2个),那么: - 将AAC裸数据1AAC裸数据2按顺序拼接起来(中间没有分隔)。 - 设置ADTS头中的number_of_raw_data_blocks_in_frame为1(因为有两个数据块,所以值为1,表示有一个额外的块)。 - 长度(13位)设置为ADTS头长度(7字节)加上两个AAC裸数据的长度之。 注意:每个AAC裸数据块之间没有额外的头或分隔,它们是连续存储的。 但是,这种方式并不常用,因为许多解码器可能只支持每个ADTS包含一个AAC(即number_of_raw_data_blocks_in_field=0)。因此,为了兼容性,我们通常为每个AAC裸数据生成一个ADTS(即每个AAC裸数据前面加一个ADTS头)。 因此,回答用户的问题: 有两种方法: 方法1(推荐):为每个AAC裸数据单独添加一个ADTS头,生成独立的ADTS。 方法2:将多个AAC裸数据拼接在一起,然后在最前面添加一个ADTS头,并在该ADTS头中设置“number_of_raw_data_blocks_in_frame”字段为(数据块数-1)。但是这种方法需要确保解码器支持。 由于用户要求“多个AAC数据添加ADTS头”,这里我们分别给出两种方法的步骤。 方法1:每个AAC单独添加ADTS头(常用方法) 步骤: 1. 对于每个AAC裸数据(假设为data_i): a. 计算整个ADTS的长度:frame_length = 7 + data_i.length (因为ADTS头7字节,如果使用CRC则9字节,这里假设无CRC) b. 构建ADTS头(7字节): - 同步字:0xFFF - ID: 0(表示MPEG-4) - layer: 0 - protection_absent: 1(无CRC) - profile: 根据AAC级别设置(例如AAC LC为1,因为profile枚举值2-1=1?注意:profile在ADTS头中存储的值是AAC profile减1,例如LC的profile为2,则存储为1) - 采样率索引:根据采样率查表(例如44100Hz为4) - 通道配置:根据声道数(例如双声道为2) - 长度:frame_length(13位) - 缓冲器满度:0x7FF(表示可变码率) - 原始数据块数量:0(表示只有一个AAC) c. 将构建好的7字节ADTS头data_i拼接起来,形成一个ADTS。 方法2:多个AAC共用一个ADTS头(较少用) 步骤: 1. 将N个AAC裸数据按顺序拼接成一个大的数据块(data_total)。 2. 计算整个ADTS的长度:frame_length = 7 + data_total.length 3. 构建ADTS头(7字节): - 同步字、ID、layer、protection_absent、profile、采样率索引、通道配置等字段的设置方法1相同。 - 长度:frame_length - 缓冲器满度:0x7FF - 原始数据块数量:N-1 (因为N个数据块,所以这个字段为N-1,注意这个字段只有2位,所以N最大为4?因为2位最大值为3,所以数据块数最大为4) 4. 将构建好的ADTS头data_total拼接起来,形成一个大的ADTS。 注意:方法2中,ADTS头中的原始数据块数量字段(2位)只能表示1到4个数据块(因为0表示1个,1表示2个,2表示3个,3表示4个)。所以最多只能4个AAC放在一个ADTS中。 实际应用中,推荐使用方法1,因为兼容性好,且每个ADTS独立,方便流传输。 代码示例(方法1): 假设我们有一个AAC裸数据的数组(aac_frames),每个元素是一个AAC的字节数组。 我们需要为每个AAC添加ADTS头,并将结果存储到adts_frames列表中。 以下是一个示例函数,用于生成ADTS头(7字节版本): ```python def create_adts_header(frame_length, profile, sample_rate_index, channels): # 参数说明: # frame_length: ADTS总长度(包括头7字节AAC数据长度) # profile: AAC配置,例如2表示LC(在ADTS头中存储为profile-1,所以这里传入2的话,头中存1) # sample_rate_index: 采样率索引,例如44100Hz对应4 # channels: 声道数,例如双声道为2 # 初始化7字节的头,全0 header = bytearray(7) # 同步字:0xFFF (12位),设置前两个字节 header[0] = 0xFF # 同步字高8位 header[1] = 0xF0 # 同步字低4位(11110000),后面4位将用于其他字段 # 设置第2个字节的低4位:ID(1位)、layer(2位)、protection_absent(1位) # 假设:ID=0(MPEG-4),layer=0(00),protection_absent=1(无CRC) header[1] |= 0x0C # 将第2字节设置为1111 1100? 不对,应该这样: # 实际上,第1字节为0xFF,第2字节高4位是0xF0(同步字的低4位,即1111),然后低4位需要设置:ID(0),layer(00),protection_absent(1)-> 二进制0001,即0x1 # 所以:header[1] = 0xF0 | 0x01 -> 11110001,即0xF1 header[1] |= 0x01 # 设置protection_absent=1,同时ID=0,layer=00,所以最后4位为0001 # 第3个字节:高2位为profile(注意profile是4位吗?前面我们说明是2位,但实际上profile字段只有2位) # 根据标准,profile字段占2位(第16-17位),所以放在第3个字节的高2位 header[2] = (profile - 1) << 6 # 减去1是因为profile在ADTS头中存储为实际profile值减1(例如LC为2-1=1) # 接下来4位是采样率索引 header[2] |= (sample_rate_index & 0x0F) << 2 # 第3个字节的最后1位是私有位(0) # 第4个字节:高1位是私有位(继续为0),然后3位是通道配置,然后1位是原始/复制(0),1位是home(0) header[3] = (channels & 0x07) << 6 # 通道配置放在第4字节的高3位(实际上移6位,因为一个字节8位,移6位后占位7、6、5) # 然后第4个字节还有2位(低位)没有设置,但注意,后面的字段(原始/复制、home、版权标识位等)我们通常设为0。 # 接下来是长度(13位),占据第4字节的低2位(位45)第5字节的全部8位以及第6字节的高5位?不对,按照位布局: # 长度(13位)应该从第4字节的第6位开始(因为第4字节的前6位已经被占用:通道配置3位(占位4的6、7、8?))——这里需要重新考虑位布局。 # 重新整理位布局(从0开始计数): # 位0-11: 同步字(字节0的0-7位,字节1的0-3位) # 位12: ID(字节1的4位) # 位13-14: layer(字节1的5-6位) # 位15: protection_absent(字节1的7位) # 位16-17: profile(字节2的0-1位) # 位18-21: 采样率索引(字节2的2-5位) # 位22: 私有位(字节2的6位) # 位23-25: 通道配置(字节2的7位字节3的0-1位?)不对,字节2只有8位,所以通道配置在字节3的高3位(位23、24、25)? # 实际上,字节2(第3个字节)的位: # 位0-1: profile # 位2-5: 采样率索引 # 位6: 私有位 # 位7: 通道配置的最高位?不对,通道配置有3位,所以通道配置实际上跨越了字节2字节3? # 标准中,通道配置在字节3的高3位?还是整个在字节3? # 为了避免混淆,我们按照位偏移来构建: # 重新构建(按位偏移,从左到右,每个字节从高位到低位?实际上,在二进制流中,字节0先传输,字节0的最高位最先传输) # 但构建字节数组时,我们按字节顺序,每个字节内从高位到低位。 # 标准位布局(从字节0开始,每个字节8位,高位在前): # 字节0: 8位(位0-7): 0xFF # 字节1: 位8-15: 高4位为0xF(同步字最后4位),低4位为ID(位12)、layer(位13-14)、protection_absent(位15) # 位8-11: 0xF(同步字) # 位12(字节1的位0): ID # 位13-14(字节1的位1-2): layer # 位15(字节1的位3): protection_absent # 字节2: 位16-23: # 位16-17(字节2的位0-1): profile # 位18-21(字节2的位2-5): 采样率索引 # 位22(字节2的位6): 私有位 # 位23(字节2的位7): 通道配置的最高位? # 字节3: 位24-31: # 位24-25(字节3的位0-1): 通道配置的剩余两位(如果通道配置有3位,则字节2的位7是最高位,字节3的位0-1是低两位) # 但是,标准中通道配置是3位,且在位布局中是连续的位23、24、25(共3位)? 如果是这样,那么: # 字节2的位7(位23)是通道配置的最高位(第1位),字节3的位0(位24)是第2位,字节3的位1(位25)是第3位。 # 然后,位26: 原始/复制(1位),位27: home(1位),位28: 版权标识位(1位),位29: 版权标识开始(1位),然后长度从位30开始。 # 因此,构建头时: # 我们设定: # syncword: 0xFFF -> 字节0=0xFF, 字节1的高4位=0xF # 字节1的低4位:ID(0)<< 3 | layer(0)<< 1 | protection_absent(1) -> 0x00 | 0x00 | 0x01 = 0x01,所以字节1=0xF1 # 字节2:profile(2位,存储值为实际profile-1)<<6 | 采样率索引<<2 | 0<<1 | (通道配置>>1) & 1 (这里通道配置需要拆开) # 但实际上,通道配置的3位是连续的,我们可以这样: # 通道配置值 = channels (例如2,二进制010) # 通道配置的最高位(第1位)在字节2的最后一位(位23),用channels的第2位(从0开始)?不对,我们按位布局: # 通道配置占据位23、24、25(连续3位),所以: # channel_bits = channels & 0x07 # 取低3位 # 位23(通道配置的最高位)对应channel_bits的第2位(因为3位:位2、1、0)?实际上,位23是通道配置的最高位,应该是channel_bits的bit2(最高位)。 # 位24是channel_bits的bit1,位25是channel_bits的bit0(最低位)?不对,应该是: # channel_bits = (channels >> 1) & 0x04 | (channels & 0x01)<<2 | ... 这样很麻烦。 # 另一种方法是使用位掩码,按照位偏移设置整个56位的值,然后分割成7个字节。 # 这里我们采用位运算组合: # 总共56位,用一个整数(大于56位)表示,然后分割成7个字节。 adts = 0 # 同步字:12位,0xFFF adts |= 0xFFF << (56-12) # 同步字在最高12位(位0-11对应整个头的最高位?)不对,我们构建时,adts的最高位(第0位)对应传输的第一个bit(即字节0的最高位)。 # 但为了方便,我们可以将adts看作一个56位的整数,从高位到低位(位0-55),位0是最高位(第一个bit),位55是最低位。 # 同步字12位:放在位0-11(最高12位) # 然后ID(1位)放在位12 # layer(2位)放在位13-14 # protection_absent(1位)放在位15 # profile(2位)放在位16-17 # 采样率索引(4位)放在位18-21 # 私有位(1位)放在位22 # 通道配置(3位)放在位23-25 # 原始/复制(1位)放在位26(0) # home(1 bit)放在位27(0) # 版权标识位(1位)放在位28(0) # 版权标识开始(1位)放在位29(0) # 长度(13位)放在位30-42 # 缓冲器满度(11位)放在位43-53(0x7FF) # 原始数据块数量(2位)放在位54-55(0) # 设置各个字段(注意:字段值需要放在对应的位上) adts = 0 # 同步字: 0xFFF (12 bits) at bit0-11 adts |= 0xFFF << 44 # 44 = 56-12,即从高位开始的12位(位44-55是低12位,但我们希望同步字在最高12位,所以移位44位,即从位44到位55?不对,应该是左移(56-12)=44位,这样同步字就占据了位[44:55](共12位),而最高位(位0-43)还没有设置。但我们的头一共56位,我们希望同步字在最前面(最高位),所以移位44位后,同步字在最高12位(位44到位55)?实际上,如果我们用一个大整数,最高位是位0(最左边),那么移位44位后,同步字就在位44-55(共12位),这是低12位?不对,这取决于端序。 # 为了避免混淆,我们直接构建字节数组: # 字节0: 0xFF # 字节1: 0xF0 | (ID << 3) | (layer<<1) | protection_absent -> 这里ID=0, layer=0, protection_absent=1 -> 0x01,所以字节1=0xF1 # 字节2: (profile-1)<<6 | (sample_rate_index<<2) | (0)<<1 | (channels>>2) # 这里私有位为0,通道配置的最高位(channels是3位,最高位是channels的第3位(从1开始计数)?不对,channels是一个3位的值(0-7),我们将其拆成:最高1位(放在字节2的最低位)低2位(放在字节3的高2位)?这样很麻烦。 # 因此,我们采用标准方法: # 长度包括头长度(7字节)AAC数据长度,设为x。 # 缓冲器满度设为0x7FF(二进制全1) # 原始数据块数量设为0(表示1个AAC) # 我们构建一个7字节的数组: header = [0] * 7 header[0] = 0xFF # 同步字高8位 header[1] = 0xF1 # 同步字低4位(1111) + ID(0) + layer(00) + protection_absent(1) -> 11110001 -> 0xF1 # 字节2: # profile-1 放在高2位(左移6位) # 采样率索引放在接下来的4位(左移2位) # 私有位(0)放在接下来1位(左移1位)-> 0 # 通道配置的最高1位放在最后1位(右移1位后取最低1位)-> channels>>1 & 0x01 # 所以:header[2] = ((profile-1) << 6) | (sample_rate_index << 2) | (0 << 1) | ((channels & 0x04) >> 2) # 注意:通道配置是3位,我们取最高位(对应0x04)右移2位(因为0x04是第3位(从1开始计数),右移2位后变成第1位) # 例如channels=2(二进制010),最高位是0,所以(2 & 0x04)为0,右移2位还是0。 header[2] = ((profile-1) << 6) | (sample_rate_index << 2) | ((channels & 0x04) >> 2) # 字节3: # 通道配置的低2位(放在高2位): (channels & 0x03) << 6 # 然后是原始/复制(0)、home(0)、版权标识位(0)、版权标识开始(0)-> 这些都为0 # 然后长度的高2位(13位长度)放在字节3的低2位(注意长度13位,其中高2位放在这里,低8位在字节4) # 所以:header[3] = ((channels & 0x03) << 6) | (frame_length >> 11) & 0x03 header[3] = ((channels & 0x03) << 6) | ((frame_length >> 11) & 0x03) # 字节4: 长度的中间8位(位5-12)-> 实际上是长度的位3-10(因为我们有13位,从高到低:位12-0)? # 长度 = frame_length (13 bits),所以: # bit12- bit5 放在字节4(即 frame_length >> 3 取低8位)? # 不对,应该这样: # 长度在ADTS头中占据13位(位30-42),其中: # 位30-31(最高两位)在字节3的低2位(我们刚刚设置了) # 位32-39(中间8位)在字节4(整个8位) # 位40-42(低3位)在字节5的高3位 # 所以: header[4] = (frame_length >> 3) & 0xFF # 取中间8位(即右移3位后取低8位) # 字节5: 长度的低3位(放在高3位)缓冲器满度的高3位(缓冲器满度11位) # 长度的低3位:frame_length & 0x07 -> 然后左移5位(放到字节5的高3位) # 缓冲器满度的高3位:0x7FF>>8 -> 0x7 # 所以:header[5] = ((frame_length & 0x07) << 5) | 0x1F & (0x7FF>>6) ??? 缓冲器满度11位,其中高3位放在字节5的低5位?不对,应该是放在字节5的低5位的高3位?即: # 缓冲器满度:位43-53(11位),其中高3位(位43-45)放在字节5的低5位中的高3位(即字节5的位0-2)?不对,应该整体考虑: # 字节5: 8位 # 高3位: 长度的最低3位(即frame_length & 0x0007,然后左移5位) # 低5位: 缓冲器满度的最高5位(0x7FF>>6) header[5] = ((frame_length & 0x07) << 5) | ((0x7FF >> 6) & 0x1F) # 字节6: 高5位: 缓冲器满度的低6位(0x7FF的低6位)<< 3? 不对,缓冲器满度的低6位应该放在字节6的高6位?不对,字节6有8位。 # 缓冲器满度共11位:我们已经有字节5放了高5位(上面我们放了最高的5位?),那么剩下的6位放在字节6的高6位。 # 所以:header[6] = ( (0x7FF & 0x3F) << 2 ) | (number_of_raw_data_blocks_in_frame) # 因为number_of_raw_data_blocks_in_frame在最后2位(位54-55),所以放在字节6的最低2位。 # 这里我们number_of_raw_data_blocks_in_frame设为0(表示1个AAC) header[6] = ((0x7FF & 0x3F) << 2) | 0 # 0x7FF & 0x3F 取低6位,然后左移2位(因为最后2位留给number_of_raw_data_blocks_in_frame) # 但是,注意:缓冲器满度0x7FF(11位): 二进制为11111111111 # 高5位:11111 # 低6位:111111 # 所以:字节5的低5位 = 11111(0x1F),字节6的高6位 = 111111<<2 = 11111100,然后最后2位为00(number_of_raw_data_blocks_in_frame=0) # 所以字节6 = 11111100 = 0xFC # 因此,整个头为:0xFF, 0xF1, (profile-1)<<6|采样率索引<<2|(channels高1位), (channels低2<<6)|长度高2位, 长度中间8位, (长度低3<<5)|0x1F, 0xFC # 最后,将header列表转换为bytearray return bytes(header) # 注意:以上代码中的位操作可能有误,因为位布局较为复杂,建议参考标准文档或成熟的实现。 # 由于ADTS头的构建较为复杂,这里给出一个常用的简化版本(假设单一AAC,且通道配置为双声道,采样率索引为4,profile为LC(2)): def create_adts_header_simple(data_length, profile=2, sample_rate_index=4, channels=2): # 总长度 = data_length + 7 frame_length = data_length + 7 # 创建7字节的头 header = bytearray(7) # 字节0-1: 同步字 header[0] = 0xFF header[1] = 0xF1 # 0xF1: 11110001 -> 同步字低4位1111 + ID=0, layer=00, protection_absent=1 # 字节2: profile采样率等 # profile-1 (LC:2-1=1) -> 二进制01,左移6位 -> 01000000 # 采样率索引4 -> 二进制0100,左移2位 -> 00010000 # 私有位0 -> 0 # 通道配置的高1位: channels=2 (二进制010) -> 取最高位0,所以|0 header[2] = ( (profile-1) << 6 ) | ( sample_rate_index << 2 ) | ( (channels & 0x04) >> 2 ) # 字节3: 通道配置的低2位 -> 10 (因为2的二进制010,低2位为10) 左移6位 -> 10000000 # 长度的高2位: frame_length共13位,取最高2位(>>11)-> 因为frame_length<=8191,所以>>11后最多3(因为13位最大8191,>>11是除以2048,所以0-7) # 这里我们取低2位(&0x03) header[3] = ( (channels & 0x03) << 6 ) | ( (frame_length >> 11) & 0x03 ) # 字节4: 长度中间8位: (frame_length >> 3) & 0xFF header[4] = ( frame_length >> 3 ) & 0xFF # 字节5: 长度的低3位(左移5位) -> (frame_length & 0x07)<<5 # 然后 | 0x1F (缓冲器满度的高5位,0x7FF>>6 = 0x1F) header[5] = ( (frame_length & 0x07) << 5 ) | 0x1F # 字节6: 缓冲器满度的低6位 (0x3F) 左移2位(因为最后2位用于number_of_raw_data_blocks_in_frame,设为0) header[6] = 0xFC # 0x3F<<2 = 0xFC,然后再0x00(number_of_raw_data_blocks_in_frame=0) -> 0xFC return header # 然后,对于每个AAC,调用这个函数生成ADTS头,然后拼接: adts_frames = [] for aac_frame in aac_frames: header = create_adts_header_simple(len(aac_frame)) adts_frame = header + aac_frame # 拼接 adts_frames.append(adts_frame) ``` 对于方法2,只需将多个AAC拼接后调用一次create_adts_header,并设置number_of_raw_data_blocks_in_frame=N-1,并将缓冲器满度之后的2位设置为N-1(在字节6的最低2位),同时注意长度的计算。 但是,由于字节6的最低2位在create_adts_header_simple中被设为0(0xFC就是11111100,最后两位为00),所以如果我们使用多个AAC,需要修改字节6: header[6] = (0x3F << 2) | ( (N-1) & 0x03 ) # 0x3F<<2 = 0xFC,然后| (N-1)(2位) 但注意,长度是13位,需要计算整个ADTS的长度(7 + 所有AAC长度之)。 由于方法2不常用,这里不展开。 总结:给多个AAC裸数据添加ADTS头,推荐为每个AAC单独添加一个ADTS头(方法1),这样每个ADTS都是独立的,兼容性好。 </think>### 多个AAC裸数据添加ADTS头的方法 根据ADTS格式特性[^1][^2],为多个AAC裸数据添加ADTS头有两种主要方法,具体选择取决于应用景: --- ### **方法1:独立封装(推荐)** **每个AAC单独添加ADTS头**,生成多个独立的ADTS。这是最兼容且最常用的方法[^2][^3]。 **步骤:** 1. **遍历每个AAC裸数据** 2. **计算ADTS长度**: $$ \text{frame\_length} = \text{ADTS\_HEADER\_SIZE} + \text{aac\_data\_length} $$ (ADTS头通常7字节,无CRC时) 3. **构建ADTS头**(7字节结构): | 字段 | 位宽 | 值/计算方式 | |---------------------|-------|--------------------------------| | 同步字 | 12位 | `0xFFF` | | ID (MPEG-4) | 1位 | `0` | | Layer | 2位 | `00` | | 保护缺失标志 | 1位 | `1` (无CRC) | | Profile (如AAC-LC) | 2位 | `profile-1` (LC为`2-1=1`) | | 采样率索引 | 4位 | 查表(如44.1kHz=4) | | 通道配置 | 3位 | 声道数(如立体声=2) | | 长度 | 13位 | 步骤2计算的`frame_length` | | 缓冲器满度 | 11位 | `0x7FF` (可变码率) | | 数据块数量 | 2位 | `0` (表示1个AAC) | 4. **拼接输出**:ADTS头 + AAC裸数据 **Python示例代码**: ```python def create_adts_header(data_length, profile=2, sample_rate_idx=4, channels=2): frame_length = data_length + 7 # ADTS头7字节 header = bytearray(7) # 字节1-2: 同步字 + 基础信息 header[0] = 0xFF header[1] = 0xF1 # 0xF1 = 11110001 (ID=0, layer=00, protection_absent=1) # 字节2: Profile采样率 header[2] = ((profile - 1) << 6) | (sample_rate_idx << 2) | (channels >> 2) # 字节3: 通道配置 + 长度高2位 header[3] = ((channels & 0x03) << 6) | ((frame_length >> 11) & 0x03) # 字节4: 长度中间8位 header[4] = (frame_length >> 3) & 0xFF # 字节5: 长度低3位 + 缓冲器满度高5位 header[5] = ((frame_length & 0x07) << 5) | 0x1F # 0x1F=缓冲器满度高5位 # 字节6: 缓冲器满度低6位 + 数据块数量(=0) header[6] = 0xFC # 0xFC=11111100 (缓冲器满度低6位 + 00) return bytes(header) # 为每个AAC添加ADTS头 adts_frames = [] for aac_data in aac_raw_data_list: adts_header = create_adts_header(len(aac_data)) adts_frames.append(adts_header + aac_data) ``` --- ### **方法2:合并封装** **多个AAC共享一个ADTS头**,通过`number_of_raw_data_blocks`字段声明包含的数(兼容性较差)[^4]。 **步骤:** 1. **拼接多个AAC裸数据**: ```python combined_data = b''.join(aac_raw_data_list) ``` 2. **计算总长度**: $$ \text{frame\_length} = \text{ADTS\_HEADER\_SIZE} + \sum{\text{aac\_data\_length}} $$ 3. **设置ADTS头关键字段**: - `number_of_raw_data_blocks` = `N-1`(N为AAC数量) - 其他字段同方法1 4. **修改头部的数据块数量字段**(字节6最低2位): ```python header[6] = (0xFC & 0xFC) | ( (num_frames-1) & 0x03 ) # 保留高6位,设置低2位 ``` **注意事项**: - 此方法在字节6的 **最低2位** 存储`N-1`值[^4] - 最多支持4个AAC(`2^2=4`种组合) - 部分解码器可能不支持此格式 --- ### **关键区别对比** | **特性** | 方法1 (独立封装) | 方法2 (合并封装) | |------------------------|-----------------------|-------------------------| | 兼容性 | 广泛支持[^2][^3] | 有限支持 | | 实时流传输 | 适用(每个可独立解码)| 不适用 | | 数据块数量字段 | 固定为`0` | 设置为`N-1` | | 适用景 | 直播/RTP/常见文件格式 | 特定嵌入式系统 | > 建议优先采用**方法1**,除非有明确的多封装需求[^2][^4]。 --- ### **相关问题** 1. 如何从MP4文件中提取AAC裸数据? 2. ADTS头中的采样率索引如何映射到实际采样率? 3. AAC-LCHE-AAC在ADTS封装中有何区别? 4. 为什么缓冲器满度通常设置为`0x7FF`?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值