2010年2月19日 星期五

Jitter Correction

CD-ROM 因為在設計上是 定線速,所以在讀內圈資料和讀外圈資料時的
碟片轉速會不同,讀內圈時會比外圈快得多,而為了要簡化設計,許多 CD-ROM
都只是用一個 FIFO(First In, First out)的緩衝區來控制轉速,當緩衝區快要
滿的時候就將轉速降低,反之則加快,故在讀取資料時的間距就會忽大忽小,
這就是 jitter。這在讀取資料軌時沒有什麼問題,因為每個資料區塊有起始碼
及第三層 ECC(error correcting code),但音樂軌時就有問題了,因為在
音樂軌的資料中沒有起始碼,無法準確的決定每一個 frame 的起始位置,所以
會有一種現象就是,一片音樂 CD 中的某一首歌,在兩台 CD-Player 中播放
出來的時間會有一點點差距。為了要解決這種音樂播放時的問題,各家廠商都在
CD device 裡加上一些線路來設法解決問題,這就是 de-jitter。

當我們要在電腦上利用可以抓音軌的 CD-ROM 來抓音軌時,一樣會發生這種
問題,但因為近來 CD device 的技術越來越進步,再加上(我猜測的)有些公司
的 CD-ROM 有用到額外通道中的一些資訊來確定每個 frame 的起始位置,因此
已經可以做到 100% 完整重現音樂軌的資料,因為無論如何,de-jitter 後所得到
的音樂軌資料,很有可能會和原始資料不同,既使在大部份的情形下可能聽不
出來,但是仔細聽還是有些變化比較快的地方是可以聽得出來的。

用軟體來做 de-jitter 的一種常用的方法是,將同一個 frame 讀出兩次加以
比較,如果完全相同就沒有問題,如果還是不同就要讀第三次,然後找出最相近的
兩次,然後再用一些方法來算出可以接受的資料,如果三次的差距都很大,那麼
de-jitter 就會失敗,這在使用 cdda 時是偶爾會出現的。有人問說為何 cdda
會比 cdgrabp 慢得多,而 cdda 抓出來的品質卻比 cdgrabp 好,這是因為 cdda
deafult 會有 de-jitter,而 cdgrabp 沒有罷了。當這種情形發生時,就表示
你抓到的資料已經是經過修飾的了,已經和原始資料不同了,既使你不太能夠聽
得出差異。

我為何敢如此斷定上面這件事呢?很簡單,請將一個音樂軌重覆抓兩次,
存成 ta.wav 及 tb.wav,然後用 DOS 的 FC 加以比較:fc/b ta.wav tb.wav,
你就會發現原來它們的差距是如此的大!附帶要提的一件事就是,在有 smartdrv
的情形下,很容易發生 jitter 的情形,既使那台 CD-ROM 本來應該是很好的,
此時可以試著將 smartdrv 關掉,同一軌再抓兩次比較看看,你或許就會驚訝的
發現,"no differences encountered"!

好啦,請各位有心抓音軌的人,將你的 PC 啟動到 DOS 模式下,將 smartdrv
關掉,然後將你最喜歡的那首歌抓下兩次比較看看,如果完全一模一樣,那個恭禧
你,如果有所不同,那就表示你以前抓下來的"沒有音爆"的音樂軌,其實是經過
修飾的,當然,如果你不介意,那還有誰能介意呢?

2010年2月10日 星期三

算术编码

现在通用的大多数数据压缩方法都属于两大阵营之一:基于字典的方案和统计方法。在小系统世界中,基于字典的数据压缩技术此时似乎更加流行。不过,通 过将算术编码与强大的模型技术结合在一起,数据压缩的统计方法可以真正达到更好的性能。这篇分成两部分的文章讨论了如何用几个不同的模型方法与算术编码组 合以达到一些重大的压缩率。本文的第一部分详细说明算术编码是如何工作的。第二部分说明如何开发一些可以使用算术编码的有效模型以生成高性能压缩程序。

钟爱的术语

  数据压缩通常通过从输入“文本”获取“符号”、处理它们,并将“代码”写入到压缩后的文件来进行运作。对于本文来说,符号通常是字节,但是他们 很可能只是像素、80位的浮点数或者EBCDIC字符。数据压缩方案需要能够将已压缩的文件转换回到与输入文本的一样的拷贝才是有效的。如果已压缩的文件 比输入文本更小,那么不必说,它也是有用的。

  基于字典的压缩系统通过用固定长度码来代替输入文本中的一组符号来进行运作。字典技术的一个众所周知的例子是LZW数据压缩。(请参见DDJ的 89年第10期中的“LZW 数据压缩”一文)。LZW通过通常从9到16位大小范围的码来取代本来无限长的字符串来进行运作。

  数据压缩的统计方法采取一种完全不同的方法。它们通过一次编码多个符号来运作。将符号编码到可变长的输出码中。输出码的长度根据符号的概率或者频率进行变化。低概率的符用较多的位进行编码,并且高概率符用较少的位进行编码。

  实践中,统计和字典方法之间的分界线并不总是那么清晰。一些方案并不能明显地归为某一个阵营或者另一个,并且总是有一些使用来自两种技术特性的混合方案。不过,在本文中讨论的方法使用算术编码来实现纯粹的统计压缩方案。

霍夫曼(Huffman)编码:退役的冠军

  在数据流中只是能够精确地计算字符的概率还不够,我们也需要能有效地利用这个知识的编码方法。基于概率统计的最著名的编码方法可能是霍夫曼编 码。D.A.霍夫曼在1952年发表了一篇论文说明为已给定其概率的一组符号创建码表的方法。当使用固定长度的编码时,霍夫曼编码表保证产生最低可能的输 出位来统计输入流中可能的符号。霍夫曼称这些“最小冗余编码”,但是这个方案现在统称为霍夫曼编码。其它固定长度编码系统,如香农-范诺 (Shannon-Fano)编码,通过霍夫曼证明不是最理想的。

  霍夫曼编码为每个符号指定一个输出码,输出码可以1位这么短,也可以比输入符号长得多,严格取决于它们的概率。用于每个符号的优化位数是以2为 底(1/p)的对数,这里p是指定字符的概率。因此,例如,如果可以在随机字节流中找到的字符概率是1/256,每个字符的最佳位数是以2为底的256的 对数,或者8。如果概率上升到1/2,编码字符所需要的最佳位数将降为1。

  这个方案放到现实中时的问题是霍夫曼编码的位长度必须是整数。例如,如果字符的概率是1/3,编码这个字符的最佳位数是大约1.6。霍夫曼编码方案必须要么指定1位,要么指定2位给编码,并且任何一种选择都导致可能比理论更长的压缩消息。

  当字符的概率变得很高时,这个非最佳编码也变成值得注意的问题。如果能开发一种可以为给定的字符指定90%的概率的统计方法的话,最佳编码码大小将是0.15个比特位。霍夫曼编码系统只可能给符号指定一个1位的码,这要比必需的码位长6倍。

自适应方案

  当尝试进行自适应数据压缩时产生的霍夫曼编码的第二个问题。当进行非自适应数据压缩时,压缩程序单程扫描一遍数据来收集统计信息。然后使用在整个编码过程中都不能改变的统计信息来编码数据。

  解码程序为了解码已压缩的数据流,它首先需要一个统计信息的副本。编码程序一般情况下将预先考虑的统计表放入已压缩的消息中,以使解码程序可以在开始之前读入统计表。这显然给消息增加了一定量的负担。

  当使用很简单的统计模型压缩数据时,编码表趋于更小。例如,每个字符的频率计数能以相当高的精确性存储在差不多256个字节大小的空间中。它并 不会给除真正最小的消息之外的其它任何消息增加明显的长度。不过,为了获得更好的压缩率,统计模型在大小上有必要的增加。如果消息的统计变得太大,在压缩 率中的任何改进被将需要预先放入已编码消息中的统计而增加的长度抵消了。

  为了回避这个问题,提出了自适应数据压缩。在自适应数据压缩中,编码程序和解码程序都以其处于相同状态的统计模型开始。两者中的每一个一次处理 单个字符,并在字符读入后更新统计模型。这非常类似于大多数像LZW编码这样基于字典方案的工作。以非最佳模型开始的消息会有少量的效率损失,但是它经常 通过不必随消息传送任何统计信息而得到更好的弥补。

  将自适应模型与霍夫曼编码结合起来的问题是重建霍夫曼树是一个非常昂贵的过程。为了让自适应方案高效,有必要在每一个字符出现之后调整霍夫曼编 码。直到霍夫曼编码第一次开发的20年之后,执行自适应霍夫曼编码的算法都没有发布。即使在现在,最好的自适应霍夫曼编码算法仍然相当耗时间和金钱。

算术编码(Arithmetic Coding):它如何工作

  一个替代霍夫曼编码的可观候选方案已在仅近十年得到证实:算术编码(Arithmetic Coding)。算术编码完全放弃了用指定的编码取代输入符号的主张。取而代之的是,它接受输入符号流并用单个浮点输出数来代替它。消息越长(并且越复 杂),在输出数中就需要越多的位。直到最近仍然没有发现在计算机上用固定大小的寄存器来实现这一思想的实践方法。

  算术编码过程产生的输出是单个小于1并大于等于0的数。这个单个的数可以唯一地解码以创建参与其构造的精确字符流。为了构造输出数,符号的编码必须有一组指定给它们的概率。例如,如果我要编码随机消息“BILL GATES”,我会得到看起来如下的概率分布:

 字符    概率
---------------------------
 SPACE   1/10
  A    1/10
  B    1/10
  E    1/10
  G    1/10
  I    1/10
  L    2/10
  S    1/10
  T    1/10

   一旦知道了字符的概率,需要按“概率线”为单个符号指定一个范围,这个“概率线”名义上是从0到1为哪个字符指定哪一段范围并不麻烦,只要编码程序和解码程序都采用相同的方式来指定范围。我们在这里使用的这一组九个字符符号将看起来如下:

 字符    概率    范围
--------------------------------------------
 SPACE   1/10   0.00 - 0.10
  A    1/10   0.10 - 0.20
  B    1/10   0.20 - 0.30
  E    1/10   0.30 - 0.40
  G    1/10   0.40 - 0.50
  I    1/10   0.50 - 0.60
  L    2/10   0.60 - 0.80
  S    1/10   0.80 - 0.90
  T    1/10   0.90 - 1.00

  在0~1范围内为每一个字符指定一个与其出现的概率对应的的片断。当编码消息“BILL GATES”时,第一个符号是“B”。要正确编码第一个字符,最终编码后的消息必须是大于或者等于0.20并小于0.30的一个数。要编码这个数,我们所 要做的是明了这个数会落入的范围。因此,在编码完第一个字符后,这个范围的最底端是0.20,并且范围的最顶端是0.30。

  在编码完第一个字符之后,我们知道对于我们的输出数来说我们的范围现在就以这个底数和这个顶数为边界。在剩余的编码过程期间发生的事情是每一个 要编码的新符号将更进一步地限制输出数的可能范围。下一个要编码的字符,“I”,拥有范围0.50到0.60。如果它是我们消息中的第一个数,我们可以直 接将我们的底和顶的范围设为这两个值。但是“I”是第二个数。因此我们不能直接设定,而是应该说“I”在0.2~0.3的子范围中拥有与 0.50~0.60对应的范围。这意味着新编码的数将必然落入当前已建立范围的第50至第60个百分点之间的位置。按照这个逻辑将我们的数进一步限制到 0.25至0.26之间的范围。

  对任意长度的消息按照这种思路完成编码的算法说明如下:

Set low to 0.0
Set high to 1.0
While there are still input symbols do
get an input symbol
code_range = high - low.
high = low + range*high_range(symbol)
low = low + range*low_range(symbol)
End of While
output low

  按照这个流程可以得出对我们所选择的消息的自然而简单的总结,看起来如下:

 输入字符  Low 值     High 值
-----------------------------------------------------
        0.0       1.0
  B     0.2       0.3
  I     0.25       0.26
  L     0.256      0.258
  L     0.2572      0.2576
 SPACE    0.25720     0.25724
  G     0.257216     0.257220
  A     0.2572164    0.2572168
  T     0.25721676    0.2572168
  E     0.257216772   0.257216776
  S     0.2572167752   0.2572167756

  因此最终的底值,0.2572167752将使用我们介绍的编码方案唯一编码消息“BILL GATES”。

  有了这个编码方案,明白解码过程如何运作相对比较容易。我们通过看哪一个符号拥有我们已编码消息所落在的码空间来查找到第一个符号。因为数 0.2572167752落在0.2和0.3之间,我们知道第一个字符必然是“B”。然后我们需要将“B”从已编码的数中移除。因为我们知道“B”的底和 顶范围,我们可以用与将它们放入过程相反的过程来消除它们的影响。首先,我们从数中减去“B”的底值,得到0.0572167752,然后我们除以“B” 的范围,即0.1。所得的值为0.572167752。然后我们可以计算出在哪里停止,这个停止的位置正是在下一个字母“I”的范围之内。

  解码输入数的算法看上去如下:

get encoded number
Do
find symbol whose range straddles the encoded number
output the symbol
range = symbol low value - symbol high value
subtract symbol low value from encoded number
divide encoded number by range
until no more symbols

  注意我为了方便起见忽略了判断什么时候才没有更多的符号需要解码的问题。可以通过将一个特殊EOF符号编码进消息,也可以随已编码的消息一同传送流的长度来解决这个问题。

  “BILL GATES”消息的解码算法的处理过程如下:

 编码数值     输出字符   Low  High  Range
-----------------------------------------------------------
 0.2572167752    B     0.2  0.3  0.1
 0.5721677752    I     0.5  0.6  0.1
 0.72167752     L     0.6  0.8  0.2
 0.6083876      L     0.6  0.8  0.2
 0.041938     SPACE    0.0  0.1  0.1
 0.41938       G     0.4  0.5  0.1
 0.1938       A     0.2  0.3  0.1
 0.938        T     0.9  1.0  0.1
 0.38        E     0.3  0.4  0.1
 0.8         S     0.8  0.9  0.1
 0.0

  概括来说,编码过程就是一个简单地使用每一个新的符号来收缩数的可能范围这样一个过程。新的范围与附加给这个符号的预先定义的概率成比例。解码是逆向过程,其中展开的范围与抽取出的每一个字符的概率成比例。

实践中的问题

  使用算术编码编/解码符号流不是太复杂。但是初看上去,它似乎完全无法实现。大多数计算机最多支持的80位或差不多的浮点数。这是不是就意味着你必须在每次完成10到15个符号以后重新开始?你需要浮点处理器?不同浮点格式的机器可以使用算术编码来通信吗?

  如它所展现的,算术编码最好使用标准16位和32位整数来完成。不必非要要求浮点数,也不必要求其帮助才能使用算术编码。代替浮点的方法是增量 转换方案,在方案中,声明为定点整数的变量接收在底端的新位,并将这些位转移到顶端,最终形成一个大的单数,这个数的位只取决于计算机的存储媒介,媒介能 存储多长,这个数就有多大。

  在前面的部分,我通过明了括起可能输出数的范围的顶端数和底端数,来说明算法如何工作。当算法第一次启动时,底端数设为0.0,并且顶端数设为1.0。使用整数首先进行的简化是以二进制的形式改变1.0至0.999......,或者.111......。

  为了在整数寄存器中存储这些数,我们首先调整它们,以让上面这个隐含的小数点是在词的左边。然后我们载入与适合我们要处理的单词所需要大小差不 多一样的初始顶值和底值。我的实现使用16位无符号数,因此初始的顶值是0xFFFF,并且底值是0。我们知道顶值从FF开始无限增长,并且底值从零开始 无限增长,因此我们可以在需要这些剩余位(也就是最后两位“在0x00与0xFF之间”)时我们无需再附加顶值和底值,而是直接将它们转移出来。

  如果你想像我们的“BILL GATES”例子在一个具有5个数的寄存器中,我们设置与上述相当的这个数看起来应当如下:

  顶值:99999
  底值:00000

  为了找到我们新的范围数,我们需要应用前面部分说明的编码算法。我们首先需要计算底值和顶值之间的范围。两个寄存器之间的差值将是 100000,而不是 99999。这是因为我们假设有无穷多个的9(实际上只受限于我们要处理的单词数量)被加到了存储顶值的寄存器上,因此我们需要增加计算出来的差值。那么 我们使用来自前面部分的方程来计算新的顶值:

  high = low + high_range(symbol)

  在这种情况下顶值范围是.30,这个范围给出了一个新的顶值30000。存储新的顶值之前,我们需要先将它减1,再次强调,因为有那个隐藏的数字加到了整数值上。因此,新的顶值是29999。

  底端数的计算遵循相同的方法,结果得到新值为20000。因此,现在顶值和底值看起来如下:

  顶端值: 29999 (999...)
  底端值: 20000 (000...)

  从这一点来说,顶端和底端的最高位的数字应该匹配。因为我们的算法本质,顶端和底端可以继续增长接近至另一个完全没有匹配过的数。这意味着一旦 它们在最高位的数字上匹配时,这个数字就不再改变。 因此,我们现在可以输出这个数字作为我们编码数的第一个数字。这可以通过将顶端和底端向左移动一个数字来完成,并且在顶端的最低位数字后移入一个9进来。 在这个算法的C实现中是以二进制的形式执行相当的操作。

  随着这个过程的进行,顶端和底端继续向着接近碰头的方向增长,然后将转移出的数字编码进单词中。我们的“BILL GATES”消息的过程看起来如下:

               High  Low   Range  累积输出
----------------------------------------------------------------------------------
 Initial state       99999  00000  100000
 Encode B (0.2 - 0.3)   29999  20000
 Shift out 2        99999  00000  100000  .2
 Encode I (0.5 - 0.6)   59999  50000       .2
 Shift out 5        99999  00000  100000  .25
 Encode L (0.6 - 0.8)   79999  60000  20000   .25
 Encode L (0.6 - 0.8)   75999  72000       .25
 Shift out 7        59999  20000  40000   .257
 Encode SPACE (0.0 - 0.1) 23999  20000       .257
 Shift out 2        39999  00000  40000   .2572
 Encode G (0.4 - 0.5)   19999  16000       .2572
 Shift out 1        99999  60000  40000   .25721
 Encode A (0.1 - 0.2)   67999  64000       .25721
 Shift out 6        79999  40000  40000   .257216
 Encode T (0.9 - 1.0)   79999  76000       .257216
 Shift out 7        99999  60000  40000   .2572167
 Encode E (0.3 - 0.4)   75999  72000       .2572167
 Shift out 7        59999  20000  40000   .25721677
 Encode S (0.8 - 0.9)   55999  52000       .25721677
 Shift out 5        59999  20000       .257216775
 Shift out 2                      .2572167752
 Shift out 0                      .25721677520

  注意,在所有的字母都解决完后,两个额外的数字需要转移出顶端值或者底端值来结束输出单词。

复杂性

  这个方案对于增量编码一条消息来说非常适合。在使用双精度整数计算以确保消息可以精确地编码的期间,精确度是有保证的。不过,在某些情形下,精确度会有一定潜在的损失。

  如果已编码的词其中有0个或9个字符串,顶端值和底端值将慢慢地会聚到一个值,但是可能不是立即看它们的最高位数字的匹配。例如,顶值和底值可能看起来如下:

  High: 700004
  Low: 699995

  此时,计算的范围的长度将只是一个单个数字,这意味我们会没有足够的精度来精确地编码这个输出词。甚至更糟,在几次更多的迭代后,顶端数和底端数可能会看起来如下:

  High: 70000
  Low: 69999

  此时,这两个值永远也粘不到一块了。顶端值和底端值变得如此小,以至于任何计算总是返回相同的值。但是,因为两个词的最高位的数字并不相等,算法并不能输出数字并进行转移。它似乎更像是一个僵局。

  战胜这个下溢问题的办法是防止事情变得这么糟。最初的算法说过一些像“如果顶值和底值的最高位的数字匹配的话,将它转移出来”这样的事情。如果 两个数字不匹配,而它们现在是很邻近的数,就需要应用第二次测试。如果顶端数和底端数还是分开的(也就是还没有碰上),那么我们测试看顶端中第二高位的数 字是否是 0,以及底端第二高位的数字是否是 9。如果是,它意味着我们正在接近下溢,并且需要采取行动。

  当下溢出现,我们采用一个些微转移操作来阻止它。不是将最高位的数字转移出这个词,而是从顶端值和底端值中删除第二高位的数字,并将剩余的数字 左移以填充空白。最高位的数字仍留在原位。然后我们需要调置一个下溢计数器来记住我们扔掉了一个数字,并且我们不能完全确信它是否就会是以0或者9结束。 操作看起来如下:

        Before  After
------------------------------------------------
High:     40344   43449
Low:     39810   38100
Underflow:  0     1

  在每一次重新计算操作之后,如果最高位的数字并不能匹配上,我们可以再次检查下溢数字。如果下溢仍然出现,我们将它们转移出来并增加计数器。

  当最高位的数字最终会聚到一个单数值时,我们首先输出这个值。然后,我们输出所有在前面丢弃的“下溢”数字。下溢数字将全部是 9 或者全部是 0,取决于顶端数和底端数是否会聚到一个较高的值还是一个较低的值。在这个算法的C实现中,下溢计数明了输出了多少个9或者0。

解码

  在“理想的”解码过程中,我们使用整个输入数进行工作。因此算法要求我们做一些像“通过符号概率划分已编码的数”。在实践中,我们不能在一个可能有十亿个字节那么长的数上执行操作。正如编码过程,解确程序通过将16和32位整数用于计算来运作。

   取代维护两个数,顶端数和底端数,解码程序必须维护3个整数。前两个是顶端数,严格对应到由编码程序维护的顶端数和底端数。第三个数,编码,包含正在从输 入位流中读入的当前的位。编码值将总是处于顶端值和底端值之间。像它们越来越接近一样,将发生新的转移操作,并且顶端数和底端数将从编码退出。

  在解码程序中,顶端值和底端值严格对应于编码程序所使用的顶端值和底端值。这两个值正如它们在编码程序中一样,在每一个符号之后都将被更新,并 且应该有精确的相同值。通过在顶端数和底端数的第一个的数字上执行相同的比较测试,解码程序知道何时是将一个新的数字转移到输入编码中的时间。也与编码程 序步调一致,执行相同的下溢测试。

  在理想的算法中,通过查找其概率接近编码的当前值的符号来判断当前已编码的符号是什么是可行的。在整数算法中,事情些微更复杂一些。在这种情况 下,概率缩放因子由顶端数和底端数之间的差来决定。因此将使用两个 16 位的正整数之间来计算范围,来代替 0.1 和 1.0 之间的范围计算。当前的概率由落入这个范围的当前编码所决定。如果你用(high-low+1)除(value-low),你会得到当前符号的实际概率。

哪儿有牛肉?

  到目前为止,这个编码过程可能看起来很有意思,但是为什么说它在霍夫曼编码的基础上有所改进,看起来并不显然。当我们测试一个概率有一点不同的 案例时,答案就变得很清晰了。让我们测试一个我们必须编码流“AAAAAAA”的案例,并且“A”的概率已知是0.90。这意味着任何输入字符有90%的 机会是字母“A”。我们建立起我们的概率表,以使字母“A”占有范围0.0到0.9,并且消息符号的结束占有0.9到1.0的范围。那么编码过程看起来如 下:

 输入字符  Low 值    High 值
----------------------------------------------
       0.0      1.0
  A    0.0      0.9
  A    0.0      0.81
  A    0.0      0.729
  A    0.0      0.6561
  A    0.0      0.59049
  A    0.0      0.531441
  A    0.0      0.4782969
  END   0.43046721   0.4782969

  现在我们知道底端值和顶端值是什么,剩下的所有事情就是拿一个数来编码这个消息。数“0.45”将可以使这条消息唯一解码成 “AAAAAAA”。这两个数字用了比7位还些微少点空间来定义,这意味着我们用少于8位的空间编码了8个符号。最佳的霍夫曼使用这个方案来编码上面的消 息最少也要9位。

  进一步延伸这个观点,我设置一个只定义了两个符号的测试。字节值“0”有16382/16383的概率,并且EOF符号有1/16383的出现 概率。然后我创建一个测试文件其中填上一百万(100,000)个“0”。使用这个模型压缩之后,输出文件只有3个字节长!使用霍夫曼编码的最小的长度也 有12,501个字节。这显然是一个人为的例子,但是它论证了当符号的概率正确的时候,算术编码能够以比每个字节只占1位还好的比率压缩数据。

编码

  一个用C编写的编码过程放在列表1和2中。它包含两个部分,一个初始化过程,和编码程序本身。用于编码程序以及解码程序的编码第一次由 IanH.Witten、Radford Neal和John Cleary发表在《Communication of the ACM》的1987年2月期的一篇名为《Arithmetic Coding for Data Compression》的论文中,并且经过作者的允许在此公开。我稍微修改了这个编码以使程序的建模部分和编码部分进一步地隔离开。

  先前说明的两个算法之间有两个主要的差异以及代码入在列表1-2。第一个不同是在转换概率的方法中。在上面说明的算法中,概率保持为一对范围为 0.0至1.0的浮点数。每一个符号有其自己的范围部分。在我们在这里说明的程序中,符号有稍微不同的定义。符号的范围定义为两个经过缩放因子计算得出的 整数而不是两个浮点数。这个缩放因子也作为符号定义的一部分包括其中。因此,对于“BILL GATES”的例子,字母“L”在前面定义为一对值为0.60和0.80的顶/底端数。在此处使用的编码中,用值为10的确良符号缩放因子,计算出底和顶 端数分别为6和8来定义“B”。采用这种方法来完成定义的原因是因为它在建模这种类型的数据时,能很好地与我们保持统计的办法相适应。这两种方法是相当 的,只是保持用整数进行运算减少了许多需要完成的工作。注意字符实际上一直拥有范围,但不包括顶端值。

  在这个算法中的第二个不同之处是所有比较和转移操作都是以二进制的形式完成的,而不是十进制。前面给出的说明是基于十进制数字进行的,以使算法 更好理解一点。算法在十进制下可以正常工作,但是在大多数计算机上进行十进制数的屏蔽数字和移位很困难。因此我们现在比较最开始的两个位,而不是比较最开 始的两个数字。

  我们丢失了为了使用编码和解码算法所需要的两件事情。第一件是一组面向位的输入和输出程序。这放在了列表3和4中,并且在代码中没有注释。建模 代码负责跟踪每一个字符的概率,并执行两个不同的转换。在编码过程期间,建模代码取将要被编码的字符并将其转换成概率范围。概率范围定义为一个底数、一个 顶数和一个总范围。在解码过程期间,建模代码必须取一个来自输入位流的计数并将其转换成一个字符进行输出。

测试代码

  一个短的测试程序放在列表3中。它实现一个使用前面讨论的“BILL GATES”模型的压缩/展开程序。在几个需要判断的地方加入了打印语句以让你准确地跟踪在这个程序中进行的处理。

  BILL.C有其自己非常简单的模型单元,这个模型对每一个消息中可能的字母都有一组因定的概率。我加入了一个新字符,即空字符串结束符,以知道何时停止解码这条消息。

  BILL.C编码一个随便定义的输入字符串,并将其写出到一个叫作TEST.CMP的文件中。然后它解码这条消息将其打印在屏幕上进行验证。在 BILL.C中有几个仍然没有讨论过的建模函数。在编码过程期间,调用了一个称为convert_int_to_symbol程序。这个程序获取一个给定 的输入字符,并将其转换成一个底值、顶值和用在此模型中的缩放因子。因为我们用于BILL.C的模型是一组固定的概率,这就意味着在这个表中查询概率。这 些一旦定义完毕,就可以调用编码程序。

  在解码过程期间,有两个与建模有关的函数。为了决定输入流中什么字符正在等待被解码,模型需要询问模型来判断当前的缩放因子是多少。在我们的例 子中,缩放因子(或者是计算范围)是固定在11,因为在我们的模型中总是有11次计算。这个数被传送给解码程序,并且它为使用这个缩放因子的给定符号返回 一个计数。然后,调用一个称为convert_symbol_to_int的建模函数调用。它获取给定的这个计数,并判断什么字符与这个计数相匹配。最 终,再次调用解码程序来解出输入流中的那个字符。

下一次

  一旦你成功地理解了如何使用算术编码来编码/解码符号,那么你可以开始尝试创建将具有超级压缩性能的模型。下一个月的总结文章讨论一些你可以尝试的自适应压缩方法。一个相当简单的统计模型程序能够提供超过备受尊敬的程序,如PKZIP或者COMPRESS的压缩。

参考资源

  如我在前面提到,在1987年7月的《Communication of the ACM》(ACM 通信)提供权威的算术编码的概论。这篇文章的大部分在由Timothy C.Bell、John G. Cleary和Ian H.Witten所著的《Text Compression》(文本压缩)一书中重印。这本书为基于统计和基于字典的压缩两种技术提供了出色的概论。另一本好书是由 James Storer所著的《Data Compression》(数据压缩)。

Bell, Timothy C., Cleary, John G., Witten, Ian H,(1990) "Text Compression",Prentice Hall, Englewood NJ

Nelson, Mark(1989) "LZW Data Compression", Doctor Dobb's Journal, October, pp 29-37

Storer, J.A.,(1988) "Data Compression", Computer Science Press, Rockville, MD

Witten, Ian H., Neal, Radford M., and Cleary, John G.(1987)"Arithmetic Coding for Data Compression", Communications of the ACM, June, pp 520-540.

列表 1 coder.h 使用算术编码程序所需要的变量、声明以及原型。这些声明用于需要与coder.c中的算术编码程序接口所需要的程序。
列表 2 coder.c 这个文件包含了完成符号的算术编码所需要的代码。在这个模块中的所有程序为了完成编码需要知道的是符号计算的概率和缩放因子是什么。这个信息通常入在SYMBOL结构中。
列表 3 bitio.h 这个文件包含了使用位流输入/输出程序所需要的函数原型。
列表 4 bitio.c 这个程序包含了一组用于算术数据压缩的面向位的输入输出程序。
列表 5 bill.c 这个小小的验证程序将使用算术数据压缩来编码然后解码只使用了一个由包含在句子“BILL GATES”中的字母组成的字符串。它使用固定、硬编码的概率表。

2010年2月3日 星期三

PCM(Pulse Code Modulation) vs PAM(Pulse Amplitude Modulation)

類比訊號要在電腦網路上傳輸,必須先轉換成數位訊號。一般把類比訊號轉換成數位訊號的過程稱為 A/D 轉換(Analog/Digital Conversion),而把數位訊號轉換為類比訊號的過程稱為 D/A 轉換(Digital/Analog Conversion)。 目前最廣泛採用的類比數位轉換技術為「博碼調變」(Pulse Code Modulation,簡稱 PCM),這是 1939 年由美國貝爾實驗室所研發出來的技術。PCM 的主要步驟有三:取樣(Sampling)、量化(Quantization)和編碼(Encoding),茲分述如下。 取樣取樣的基本原理可以用一個「定 時開關」來說明,如下圖所示。其中,x(t) 表示未取樣前的原始訊號,而 SW 為一定時開關,每隔T秒會自動開關一次。於是,原始訊號 x(t) 只有在 1T、2T、3T … 等時間間隔時,因開關 SW 為 ON 狀態,才會被導通輸出,其餘時間皆因開關 SW 呈 OFF 狀態而無法輸出。依此過程,最後便可得到取樣後的輸出訊號 y(t)。由於取樣後的訊號為脈衝訊號,其振幅與原始訊號在該取樣點時的振幅相同,所以取樣過程又稱為脈衝振幅調變(Pulse Amplitude Modulation,簡稱 PAM)。 圖 2-6 取樣的基本原理 取樣的時間間隔稱為「取樣週期」(Sampling Cycle),單位為「秒」,可記做T;將取樣週期取倒數可得每秒的取樣次數,稱為「取樣頻率」(Sampling Frequency),單位為 Hz(次/秒),可記做 fs。 例如:如果每隔 1/100 秒取樣一次,則取樣週期為 1/100 秒,也就是一秒會取樣 100 次,亦即取樣頻率為 100Hz。 取樣過程所造成的訊號失真現象稱為取樣誤差(Sampling Error),取樣誤差與取樣頻率息息相關。取樣頻率不可太低,否則原始訊號取樣後將嚴重失真,難以重建。那麼取樣頻率究竟應該多快才夠呢?可由 Nyquist所提出的取樣定理來說明。 取樣定理(Sampling Theory) 假設原始訊號 x(t) 的最高頻率為fc,則取樣頻率 fs 應大於或等於 fc 的兩倍, 亦即 fs≧2fc,將來才能由取樣後的訊號 y(t) 重建原始訊號 x(t)。 以電話網路的語音傳輸為例,取樣頻率為 8 KHz,這是為什麼呢?因為,一般電話線的頻率響應約為 300Hz~3.4 KHz,亦即最高頻率為 3.4 KHz,因此,根據取樣定理,電話語音的取樣頻率必須大於或等於 6.8 KHz 才不至於失真,所以電話語音的取樣頻率才會定為 8 KHz。 量化經過取樣後的脈衝訊號,其振幅 為原始訊號在該時間點的振幅,其大小值有無限種可能,無法直接編碼為二進位碼,因此需將其量化成階梯式的位階訊號,所以,量化過程實際上是一個將取樣訊號 取「近似值」的過程,每個近似值稱為量化等級或量化位階(Quantization Level),而量化等級間的級距間隔則視後續的編碼長度而定。 由於量化過程是將振幅取近似量,所以會產生所謂的「量化誤差」(Quantization Error),量化誤差的大小也是取決於編碼長度。 編碼量化後的訊號已經變成階梯式的 離散訊號,每個位階可以直接對應一個二進位碼,這便是編碼的過程。二進位碼的位元數稱為編碼長度(Encoding Length),編碼長度決定了量化等級的多寡及精密度。n 位元的編碼長度可以產生2的n次方個(2n)量化等級,例如,8 位元的編碼長度共有 28 = 256 個量化等級,16 位元的編碼長度則有 216 = 65536 個量化等級。換言之,編碼長度決定了訊號振幅的解析度,因此編碼長度與量化誤差的關係可歸納如下:

  • 編碼長度 n 愈大,則量化等級愈多,量化級距愈小(細),所以量化誤差也愈小。
  • 編碼長度 n 愈小,則量化等級愈少,量化級距愈大(大),所以量化誤差也愈大。