Opus 从入门到精通 (五)OggOpus 封装器全解析
为什么要封装
前面《Opus 从入门到精通(四)Opus 解码程序实现》提到如果不封装会有两个问题:
无法从文件本身获取音频的元数据(采样率,声道数,码率等)
缺少帧分隔标识,无法从连续的文件流中分隔帧(尤其是 vbr 情况)
针对上面的问题我们可以自定义一种封装格式,增加类似于 WAV 的 Header,Header 中存储元数据,每一帧音频数据前面增加可以标识帧边界的头,但是又会引出其他问题:
播放器播放时 Seek 操作不方便,无法快速定位特定位置
如果有多路,设计到同步问题
文件切割,合并问题
我们引入 Opus 编码的标准封装格式 Ogg.
OGG 封装格式介绍
下面我们在对 Ogg 做一个简练的总结
OGG 简介
Ogg 是一个自由且开放标准的多媒体文件格式,由 Xiph.Org 基金会所维护。Ogg 格式并不受到软件专利的限制,并设计用于有效率地流媒体和处理高质量的数字多媒体。“Ogg”意指一种文件格式,可以纳入各式各样自由和开放源代码的编解码器,包含音效、视频、文字(像字幕)与元数据的处理。在 Ogg 的多媒体框架下,Theora 提供有损的影像层面,而通常用音乐导向的 Vorbis 编解码器作为音效层面。针对语音设计的压缩编解码器 Speex 和无损的音效压缩编解码器 FLAC 与 OggPCM 也可能作为音效层面使用。“Ogg”这个词汇通常意指 Ogg Vorbis 此一音频文件格式,也就是将 Vorbis 编码的音效包含在 Ogg 的容器中所成的格式。在以往,.ogg 此一扩展名曾经被用在任何 Ogg 支持格式下的内容;但在 2007 年,Xiph.Org 基金会为了向后兼容的考量,提出请求,将.ogg 只留给 Vorbis 格式来使用。Xiph.Org 基金会决定创造一些新的扩展名和媒体格式来描述不同类型的内容, 像是只包含音效所用的.oga、 包含或不含声音的影片(涵盖 Theora)所用的.ogv, 和可以包含任何比特流的.ogx。Xiph.Org 基金会对 Ogg 的参考实现,当前最新的版本是 2010 年 3 月 26 日发布的 libogg 1.2.0。这两个库都是在新 BSD 许可证下发布的自由软件。因为其格式自由,和其参考实现并非 Copyleft 形式,无论自由或专有、商业或非商业的媒体播放器,甚至部分制造商的便携式媒体播放器和全球定位系统接收器都采用了 Ogg 下的各种编解码器。当前 Android 系统所有的内置铃声也都使用 Ogg 文件。Ogg 只是容器格式。由编解码器编码的实际音频或视频存储在 Ogg 容器内。Ogg 容器可以包含用多个编解码器编码的流,例如,具有声音的视频文件包含由音频编解码器和视频编解码器编码的数据。 作为容器格式,Ogg 可以以各种格式(如 Dirac,MNG,CELT,MPEG-4,MP3 等)嵌入音频和视频,但是 Ogg 旨在和通常用于以下 Xiph.org 免费编解码器:音频
视频
文本
Ogg 格式封装结构
ogg 是以页(page)为单位将逻辑流组织链接起来,每个页都有 pageheader 和 pagedata。页头中有如下的定义:
capture_pattern 页标识:ASCII 字符,0x4f 'O' 0x67 'g' 0x67 'g' 0x53 'S',<font color=red>4 个字节</font>大小,它标识着一个页的开始。
stream_structure_version 版本 id:一般当前版本默认为 0,<font color=red>1 个字节</font>。
header_type_flag 类型标识:标识当前的页的类型,<font color=red>1 个字节</font>,- 0x01:本页媒体编码数据与前一页属于同一个逻辑流的同一个 packet,若此位没有设,表示本页是以一个新的 packet 开始的;- 0x02:表示该页为逻辑流的第一页,bos 标识,如果此位未设置,那表示不是第一页;- 0x04:表示该页位逻辑流的最后一页,eos 标识,如果此位未设置,那表示本页不是最后一页。
granule_position:媒体编码相关的参数信息,<font color=red>8 个字节</font>,对于音频流来说,它存储着到本页为止逻辑流在 PCM 输出中采样码的数目,可以由它来算得时间戳。对于视频流来说,它存储着到本页为止视频帧编码的数目。若此值为-1,那表示截止到本页,逻辑流的 packet 未结束。(小端)
serial_number:当前页中的流的 id,<font color=red>4 个字节</font>,它是区分本页所属逻辑流与其他逻辑流的序号,我们可以通过这个值来划分流。(小端)
page_seguence_number:本页在逻辑流的序号,<font color=red>4 个字节</font>。
CRC_cbecksum:循环冗余效验码效验,<font color=red>4 个字节</font>,用来效验每页的有效性。
number_page_segments:给定本页在 segment_table 域中出现的 segement 个数,<font color=red>1 个字节</font>。
segment_table:从字面看它就是一个表,表示着每个 segment 的长度,取值范围是 0~255。由 segment(1 个 segment 就是 1 个字节)可以得到 packet 的值,每个 packet 的大小是以最后一个不等于 255 的 segment 结束的,从页头中的 segment_table 可以得到每个 packet 长度,举例:如果一组 segment 依次顺序为 FF 45 FF FF FF 40FF 05FF FF FF 66(共 4 个 packet,含 12 个 segment,每个 packet 的长度是:FF 45【324】;FF FF FF 40【829】;FF 05【260】;FF FF FF 66【847】),那么第一个 packet 的长度为 255+69 = 324,第二个 packet 大小 829,同理。
页头基本上就是由上述的参数组成,由此我们可以得到页头的长度和整个页的长度:
页头部格式:
OGG 封装处理过程
音视频编码在提供给 Ogg 封装之前是以具有包边界的“Packets”形式呈现的,包边界依赖于具体的编码格式,如 Ogg 封装示意图。
将逻辑流的各个包进行分片 segmentation,每片大小固定为 255Byte,但包的最后一个 segment 通常小于 255 字节。因为 packet 的大小可以是任意长度,由具体的媒体编码器来决定。
进行页封装,每页都被加上页头,每页的长度可不等,由具体情况而确定。页头部 segment_table 域告知了“lacing_value”值的大小,即页中最后一个 segment 的长度(可以为 0,或小于 255)。一次处理一个 packet,此 packet 被封装成一个或多个 page 页(page 的长度设定了上限,一般为 4kB);下一个 packet 必须用新的 page 开始封装,由首部字段域 header_type_flag 的设置规定来表示。
多个已被页格式封装好的逻辑流(如语音、文本、图片、音频、视频等)按应用要求的时序关系合成物理流。
多个已被页格式封装好的逻辑流(如语音、文本、图片、音频、视频等)按应用要求的时序关系合成物理流。
<<The Ogg Encapsulation Format Version 0>>官方文档的图,我做了一个图片形式:
OGG 封装流程示意图
Ogg 文件的映射与逆映射
用 Ogg 文件格式封装好压缩编码媒体流可用于存储(磁盘文件)或直接传输(TCP 或管道),这是因为 Ogg 比特流格式提供了封装/同步、差错同步捕获、寻找标记以及其它足够的信息使得这种分散开的数据能够完全地还原为封装之前的具有包边界“packet”形式的压缩编码媒体流,恢复到这种原来媒体流就具有的包边界形式不需要依赖于针对压缩编码的解码器。也就是说 Ogg 映射与逆映射和媒体流的压缩编码、解码具有相对独立性。
Ogg 文件需要解封装的情况有两种:
播放器要对媒体流解码之前;
对媒体流进行 RTP/UDP 传输之前。解封装的过程就是 ogg 逆映射过程,即还原为具有包边界“packet”形式的媒体流,同时以预先填充好了的 RTP 首部字段与相应一段媒体数据捆绑,形成 RTP 封包。此过程便是媒体流从 Ogg 格式到 RTP 格式的转换过程。
将以 packet 为单元的媒体流映射为以 page 为单元的 Ogg 格式比特流,其中间经过了 segment 的划分和重组环节,但方便了对媒体流的存储与传输(TCP)。对源缓冲区媒体数据(packet)的操作,需建立几个中间环节的数据结构,只需将切割的媒体数据在内存移动一次,操作指向媒体数据的指针便能达到媒体数据迁移到目的缓冲区(page)的意图,其过程可用两个函数转换来表述:
OGG 实践之 Vorbis 比特流结构
Vorbis 比特流是以三个数据包头开始的。这些头数据包按顺序依次是:The identification header、The comment header 和设置数据包。这些都与解码 Vorbis 音频文件密切相关的。
数据包头结构
每个数据包都是以同样的头结构开始的:[packet_type] : 8 bit value0x76, 0x6f, 0x72, 0x62, 0x69, 0x73: the characters'v','o','r','b','i','s' as six octets
The identification header
The identificationheader identifies the bitstream as Vorbis, Vorbis version, and the simpleaudio characteristics of the stream such as sample rate and number of channels.
[vorbis_version] = read 32 bits as unsigned integer
[audio_channels] = read 8 bit integer as unsigned 必须大于 0
[audio_sample_rate] = read 32 bits as unsigned integer 必须大于 0
[bitrate_maximum] = read 32 bits as signed integer
[bitrate_nominal] = read 32 bits as signed integer
[bitrate_minimum] = read 32 bits as signed integer
[blocksize_0] = 2 exponent (read 4 bits as unsigned integer)必须小于等于[blocksize_1]
[blocksize_1] = 2 exponent (read 4 bits as unsigned integer)
[framing_flag] = read one bit 不能为 0
Thebitrate fields above are used only as hints. The nominal bitrate fieldespecially may be considerably of in purely VBR streams. The fields aremeaningful only when greater than zero.
All three fields set to thesame value implies a fixed rate, or tightly bounded, nearly fixed-ratebitstream
Only nominal set implies a VBRor ABR stream that averages the nominal bitrate
Maximum and or minimum setimplies a VBR bitstream that obeys the bitrate limits
None set indicates the encoderdoes not care to speculate.
The comment header
The comment header includes user text comments (\tags") and a vendor stringfor the application/library that produced the bitstream.
The comment header is logically a list of eight-bit-clean vectors; the number ofvectors is bounded to 232 .. 1 and the length of each vector is limited to 232.. 1 bytes. The vector length is encoded; the vector contents themselves arenot null terminated. In addition to the vector list, there is a single vectorfor vendor name (also 8 bit clean, length encoded in 32 bits). For example, the1.0 release of libvorbis set the vendor string to \Xiph.Org libVorbis I20020717".
The vector lengths and number of vectors are stored lsbfirst, according to the bit packing conventions of the vorbis codec. However,since data in the comment header is octetaligned,they can simply be read asunaligned 32 bit little endian unsigned integers
The comment vectors are structured similarlyto a UNIX environment variable. That is,comment fields consist of a field nameand a corresponding value and look like:
1 comment[0]="ARTIST=me";
2 comment[1]="TITLE=the sound of Vorbis";
The field name is case-insensitive and may consist of ASCII 0x20 through 0x7D, 0x3D ('=')excluded. ASCII 0x41 through 0x5A inclusive (characters A-Z) is to beconsidered equivalent to ASCII 0x61 through 0x7A inclusive (characters a-z).Thefield name is immediately followed by ASCII 0x3D ('=');
this equals sign is used to terminate the field name.0x3D is followed by 8 bit cleanUTF-8 encoded value of the field contents to the end of the field.Field namesBelow is a proposed, minimal list of standard field names with a description ofintended use. No single or group of field names is mandatory; a comment headermay contain one, all or none of the names in this list.
TITLE Track/Work name
VERSION The version field may be used to differentiate multipleversions of the same track title in a single collection. (e.g. remix info)
ALBUM The collection name to which this track belongs
TRACKNUMBER The track number of this piece if part of a specific largercollection or album
ARTIST The artist generally considered responsible for the work. Inpopular music this is usually the performing band or singer. For classicalmusic it would be the composer.For an audio book it would be the author of theoriginal text.
PERFORMER The artist(s) who performed the work. In classical musicthis would be the conductor, orchestra, soloists. In an audio book it would bethe actor who did the reading. In popular music this is typically the same asthe ARTIST and is omitted.
COPYRIGHT Copyright attribution.
LICENSE License information, eg, 'All Rights Reserved', 'Any UsePermitted'.
ORGANIZATION Name of the organization producing the track (i.e. the'record label')
DESCRIPTION A short text description of the contents
GENRE A short text indication of music genre
DATE Date the track was recorded
LOCATION Location where track was recorded
CONTACT Contact information for the creators or distributors of thetrack. This could be a URL, an email address, the physical address of the producinglabel.
SRC International Standard Recording Code for the track; see theISRC intro page for more information on ISRC numbers.
Hint: Field names are not required to beunique (occur once) within a comment header. As
an example, assume a track was recorded bythree well know artists; the following is permissible, and encouraged:
1 ARTIST=Dizzy Gillespie
2 ARTIST=Sonny Rollins
3 ARTIST=Sonny Stitt
4 Setup Header
The setupheader includes extensive CODEC setup information as well as the complete VQand Hu man codebooks needed for decode.
Thesetup header contains, in order, the lists of codebook configurations,time-domain transform configurations (placeholders in Vorbis I), floorconfigurations, residue configurations,channel mapping configurations and modeconfigurations. It finishes with a framing bit of '1'. 如下图:
OggOpus 封装格式介绍
终于介绍完 Ogg 的结构了,可以开始 Opus 的 Ogg 封装了,先以一个示例文件分析了解一下 Opus 封装的 Ogg 结构
一个 Opus 文件按 Ogg 页结构拆分
我们以我生成的这个链接: https://pan.baidu.com/s/1xRdehcOJSJILZ58b8KOt-A 提取码: nzq7 文件为例分析:
第一页
0x4F 0x67 0x67 0x53 : capture_pattern 页标识,OggS ASCII 字符
00 : stream_structure_version,版本 id
02 : header_type_flag,类型标识,表示该页为逻辑流的第一页
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 : granule_position:媒体编码相关的参数信息,表示到本页为止逻辑流有 0 个采样
0x92 0x5B 0xBF 0x32 serial_number:当前页中的流的 id
0x00 0x00 0x00 0x00 : page_seguence_number:本页在逻辑流的序号
0xD4 0x40 0x6A 0xC6 : CRC_cbecksum:循环冗余效验码效验
0x01 : number_page_segments:给定本页在 segment_table 域中出现的 segement 个数,本页只有一个
0x13 : segment_table,本页只有一个长度为 19 的 segment
0x4F-0x00 : page data,后面分析
第二页
...
0x4F 0x67 0x67 0x53 : capture_pattern 页标识,OggS ASCII 字符
00 : stream_structure_version,版本 id 0
0x00 : header_type_flag,类型标识,表示该页不是逻辑流的第一页,不是最后一页,也不是延续之前 packet
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 : granule_position:媒体编码相关的参数信息,表示到本页为止逻辑流有 0 个采样
0x92 0x5B 0xBF 0x32 : serial_number,当前页中的流的 id
0x01 0x00 0x00 0x00 : page_seguence_number:本页在逻辑流的序号
0x6F 0x8B 0x74 0xD4 : CRC_cbecksum:循环冗余效验码效验
0x03 : number_page_segments:给定本页在 segment_table 域中出现的 segement 个数,本页有 3 个
0xFF 0xFF 0xFF : segment_table,本页有三个 segment,三个 segment 组成一个 packet,大小为 255+255+254 = 764(0x2fc),计算下来最后一个字节的位置应该是 0000004Dh + 2fch = 349h
0x0000004D-0x00000349 : page data,后面分析
第三页
0x4F 0x67 0x67 0x53 : capture_pattern 页标识,OggS ASCII 字符
00 : stream_structure_version,版本 id 0
0x00 : header_type_flag,类型标识,表示该页不是逻辑流的第一页,不是最后一页,也不是延续之前 packet
0x80 0xBB 0x00 0x00 0x00 0x00 0x00 0x00 : granule_position:媒体编码相关的参数信息,表示到本页为止逻辑流有 48000 个采样(小端,0xBB80 = 48000
0x92 0x5B 0xBF 0x32 : serial_number,当前页中的流的 id
0x02 0x00 0x00 0x00 : page_seguence_number:本页在逻辑流的序号
0xF9 0x3C 0xFA 0x36 : CRC_cbecksum:循环冗余效验码效验
0x32 : number_page_segments:给定本页在 segment_table 域中出现的 segement 个数,本页有 50 个
0x0F 0x11 ... 0x2B : segment_table,本页有 50 个 segment,50 个 segment 组成 50 个 packet
0x00000364-0x00000bca : page data,后面分析 0x866=2150 个字节 本文件码率为 16kbits/s,帧大小为 20ms,一帧 16000/8/1000*20=40 字节,
第四页
0x4F 0x67 0x67 0x53 : capture_pattern 页标识,OggS ASCII 字符
00 : stream_structure_version,版本 id 0
0x00 : header_type_flag,类型标识,表示该页不是逻辑流的第一页,不是最后一页,也不是延续之前 packet
0x00 0x77 0x01 0x00 0x00 0x00 0x00 0x00 : granule_position:媒体编码相关的参数信息,表示到本页为止逻辑流有 48000 个采样(小端,0x017700 = 96000
0x92 0x5B 0xBF 0x32 : serial_number,当前页中的流的 id
0x03 0x00 0x00 0x00 : page_seguence_number:本页在逻辑流的序号
0x89 0xB5 0x54 0x5E : CRC_cbecksum:循环冗余效验码效验
0x32 : number_page_segments:给定本页在 segment_table 域中出现的 segement 个数,本页有 50 个
0x32 0x30 ... 0x35: segment_table,本页有 50 个 segment,50 个 segment 组成 50 个 packet
0x00000c18-0x0000146a : page data,后面分析 0x852=2130 个字节
第三页第四页已经开始很类似,是时候真正的解析 Page 中的 Opus,首先还是从官方文档看看 Opus 是怎么封装进 Ogg 容器的
Opus 音频编解码器的 Ogg 封装
参考Ogg Encapsulation for the opus Audio Codec,先来个简单介绍:
简介
IETF Opus 编解码器是针对两者均进行了优化的低延迟音频编解码器语音和通用音频。有关技术,请参见[RFC6716]细节。本文档定义了 Opus 封装在连续的逻辑 Ogg 位流[RFC3533]。 Ogg 封装为 Opus 提供长期存储格式,支持所有基本功能,包括元数据,快速准确的搜索,损坏检测,错误后重新捕获,低开销以及能够将 Opus 与其他编解码器(包括视频)复用最小缓冲。它还提供了实时流式格式通过可靠的面向流的运输进行交付,而无需需要所有数据(甚至是数据的总长度)以与磁盘存储格式相同的形式预先存储。这样解决了咱们最开始提到为什么要封装提出的问题.
<font color=red>Ogg Opus 推荐的 mime-type 是 audio/ogg,高特性的可以设置为'audio/ogg;codecs=opus',推荐的文件扩展名为.opus</font>
直接引用一段文档原文,继续我们的解析之旅:This encapsulation defines the contents of the packet data, including the necessary headers, the organization of those packets into a logical stream, and the interpretation of the codec-specific granule position field. It does not attempt to describe or specify the existing Ogg container format. Readers unfamiliar with the basic concepts mentioned above are encouraged to review the details in [RFC3533].
packet Organization
一个 Ogg Opus 流被组织成如下结构:
Figure 1: Example Packet Organization for a Logical Ogg Opus Stream 它有两个 mandator header packets:
ID header:在 ogg 逻辑流中的第一个包必须包含 ID header,它唯一的把一个流标识成一个 opus 音频.它单独的并且完整的放置在 ogg 逻辑流的第一页.这一页有 bos 标记
comment header:逻辑流中第二个包必须包含 comment header,它包含用户提供的元信息.它可能是跨多个页,从第二个逻辑页开始.无论 comment header 跨多少页,它必须结束在它完成的地方.随后的所有页面都是音频数据页面,它们的 Ogg 数据包包含音频数据包。对于 N 个不同的流中的每一个,每个音频数据包都包含一个 Opus 包,其中对于单声道或立体声,N 通常为 1,但对于多声道音频,N 可能大于 1。值 N 在 ID 标头中指定,并在逻辑 Ogg 比特流的整个长度上固定。
Identification Header
对应我们文件第一个 page data:
Magic Signature:OpusHead 8 字节
Version : 1 字节 unsigned,对应值 0x01
Output Channel Count 'C':1 个字节,unsigned,声道数,它可能和编码声道数不一致,它可能被修改成 packet-by-packet,对应值 0x01
Pre-skip:2 字节,unsigned,小端,这是要从开始播放时的解码器输出,从页面的颗粒位置减去以计算其 PCM 样本位置。裁剪现有 Ogg 的开头时 Opus 流是至少 3840 个样本(80 毫秒)的预跳过时间是建议确保解码器中完全收敛。对应值 0x0138=312 字节
Input Sample Rate:4 字节,unsigned, little endian,原始输入采样,对应值 0x3e80=16000
Output Gain:2 字节,signed,little endian,这是解码时要应用的增益。是 20 * log10 缩放解码器输出以实现所需的播放音量,以 16 位带符号二进制存储,对应值:0x0000
Channel Mapping Family:1 字节,unsigned,该字节指示输出渠道的顺序和语音含义.该八位位组的每个当前指定的值表示一个映射系列,它定义了一组允许的通道数,以及每个允许的通道数的通道名称的有序集合.当前值 0x0,表示声道数为 1 或者 2
Channel Mapping Table:可选,当 Channel Mapping Family 为 0 时被省略.
Comment Header
对应文件的第二个 page data:
Magic Signature:8 个字节,OpusTags
Vendor String Length:4 个字节,unsigned,little endian:当前值:0x00000032=50 字节
Vendor String:长度由 Vendor String Length 指定,utf-8 编码,当前值 0x6C 0x69 ....0x74 0x79,内容:"libopus unknown,libopusenc 0.2.1-2-g9cb17c6-dirty",因为我用的是 libopussenc 输出的
User Comment List Length:4 字节,unsigned, little endian,该字段指示用户提供的注释数。它可能表示用户提供的评论为零,在这种情况下数据包中没有其他字段。一定不要表示评论太多,以至于评论字符串长度将需要比其余的可用数据更多的数据数据包。当前值 0x00 00 00 02 = 2 个评论
User Comment #i String Length:4 字节,unsigned, little endian:该字段提供以下用户注释字符串的长度,以八位字节为单位。每个用户评论都有一个,由“用户评论列表长度”字段。它不得表示字符串比数据包的其余部分长。当前值:0x00 00 00 11 = 17
User Comment #i String:可变长度,UTF-8 载体,当前值 0x41 0x52 ... 0x69 0x0B,内容为"ARTIST=qingkouwei"
我们使用的这个文件有两个 User Comment,第二个为:
User Comment List Length: 0x00 00 00 0B = 11 个字节
User Comment: 0x54 0x49 ...0x6B 0x65,内容为"TITLE=beike"
总字节大小: 12 + 50 + 4 + 4 + 17 + 4 + 11 = 102 ,与前面计算的 764 字节差下的字节都是 padding
紧随用户评论列表之后,评论标头可以包含零填充或此处未指定的其他二进制数据。如果此数据的第一个字节的最低有效位是 1,然后编辑者应在更新时保留此数据的内容标签,但是如果该位为 0,则可以将所有此类数据视为填充,并根据需要截断或丢弃。可以为后面提供扩展预留空间.注释标题可以任意大,并且可以分布在大量的 Ogg 页面。实现必须避免尝试出现为过大的标题分配过大的内存。为了做到这一点,实现可以对待如果流的注释头大于 125,829,120 个八位位组(120 MB),视为无效,并且可以忽略未完全包含在注释的前 61,440 个八位字节中标头。用户注释字符串遵循由 NAME = value 所描述的格式[VORBIS-COMMENT]具有相同的推荐标签名称:ARTIST,TITLE,DATE,ALBUM 等。
OggOpus 先介绍到这,后面继续分析 OggOpus 封装库的实现与 API 等.
参考
版权声明: 本文为 InfoQ 作者【轻口味】的原创文章。
原文链接:【http://xie.infoq.cn/article/5150cc8497433a90738343190】。文章转载请联系作者。
评论