Win 10资源管理器几个不爽之处

Win 10我已经持续不断黑了几年,不过居然好像没在Blog里提过。姑且先让我罗列几个比较大的黑点:

输入法。这个我真的不想再赘述了,懂的都懂。一言以蔽之,Win 10默认做不到(以下是AND的关系):

  1. 同时有美式键盘,搜狗,日文输入法
  2. 默认是美式键盘
  3. 单一组合键(无论是什么,当然最好是ctrl+space)在美式键盘和搜狗之间切换(即:不会切换到日文)

Workaround就是通过注册表或者第三方的输入法管理器(例如搜狗带的)添加一个中文-美式键盘然后改用ctrl+shift切换两者(切换到日文还是和以前一样用alt+shift这点没区别)。这个解决方法不完美,有时候会卡住,但是也算在咬牙能用的范围内。

设置混乱,settings和控制面板共存,settings的界面远不如控制面板好用。

这个算是小问题,但是很烦:任务栏右键菜单巨卡无比——Win 7就已经比XP卡多了,没想到Win 10更卡,很多时候居然要右键之后5秒才能出菜单。虽然知道现在右键菜单的功能越来越丰富,但是好歹优化下啊喂。

当然Win 10也是有不少优点,其中大多数是看不见的内核、性能相关的,比如Win 10上用MTP比起Win 7就流畅许多(Win 7 MTP真的无力吐槽),奇怪的小BUG修了不少(上次提过的自动限制音量不知道还有人记得不)。而且Win 10的UI比起Win 8还是有比较明显的进步,比如开始菜单就make sense多了。所以,如果真的要让我改用Win 10,我也不至于无法忍受;但是因为这些蛋疼的改动让我暂时没有主动去换Win 10的欲望。

好了,这些都不是本文的重点,这次是专门来黑Win 10的资源管理器的。由于新笔记本预装Win 10,家里是Win 7,正好有机会可以仔细对比两者的差异。Win 10对资源管理器的改动不多,但是几乎每个改动都让我有几分蛋疼。先说点好的:Win 10在navigation panel左上角的Quick access添加了“自动显示最近用过的目录”功能,这个非常实用。Win 7年代只有一个“最近访问的位置”,结果点进去有几十个,还不能按照“最近使用”的顺序排序,非常难用。

好的说完,我们来说说其他改动是什么以及为什么我不喜欢。先来个Win 10资源管理器的样子:

2017-07-07

首先第一个黑点:Folders。Folders和之前Win 7微软力推的Library略有不同(后者我这里隐藏了,你可以在左侧navigation panel空白处右键调出来):就拿文档Library来说,其实他是link了诸如公用用户的Documents(XP时期叫My documents)、你当前账户下的Documents等在一起的一个虚拟文件夹,你自然也可以添加别的文件夹进去:

2017-07-07 (1)

其实Library本身就很鸡肋了:虽然MS的想法是好的,但是实际去用的人非常少,用起来也非常不方便,因为这个虚拟文件夹的统合度略差,不但看上去和一般的文件夹一点都不一样,操作起来也有区别(譬如,如果我想回到上级目录(C:\Users\[我的用户名]),在这里是做不到的)。

估计微软自己也意识到了Library的问题,搞出了这Folder出来。Folder一般对应的就是一个单独的、当前用户的文件夹,默认就是C:\Users\[你的用户名]\Documents之类的,不过如果你装了OneNote,Documents和照片会被改成C:\Users\[你的用户名]\OneDrive\[文档或照片文件夹]。当然,你也可以修改Location(右键属性即可)为其他文件夹。

可以看到,比起library那个非常难受的统合,这个好歹是单一文件夹,用起来没那么别扭。但是问题在于,对于我这种主力用Google Drive的,还是完全没用。而且,如果我真的要访问One Drive,左边导航窗格里已经有一个链接了,无非就是多点一下展开的问题。

一般而言,用不着不是问题,隐藏就好了。但是,这个Folders完全无法关闭:不但要在根目录下占据一大排(见图一),更无法接受的是要在导航窗格里居然不是单独的一个分支,而是全部堆在“Thic PC”节点下面占一大列,把本地磁盘都挤到下面。我在Win 7进行根目录跳转时,基本完全是靠左侧的导航窗格进行:有一个我的电脑的根节点下面全是盘符,非常清晰好找;结果加了这坨目录后,每次找C:\找半天。当年Library我不用,好歹可以折叠,而且不会和“我的电脑”根节点混在一起。

这里有一篇教程教你如何通过注册表移除这些Folders快捷方式(实际folder自然还在)。我没有用这个,因为我一会儿会用另外一款软件来实现。

第二个黑点:取消了右上角的View Layout选项。在Win 7时代,于右上角有一个切换View Layout的滑块,可以快速在诸如大图标、列表、详细等显示方式中切换:

heh

结果,到了Win 10中,该滑块移除,从而你只能使用以下四种方式进行切换:1:打开菜单栏View,然后从里面选;2. 右键->View->选;3. ctrl+滚轮切;4. 可以用ctrl+shift+数字的方法切换各个Layout。其中2和3是Win 7就有的。

你可能会说,明明还有高达四种的方式可以切,这也能喷?殊不知这个小细节正是我从用Win 10的第一天就觉得难受,坚持了一个月终于忍不住装第三方插件的导火索。

首先,这个功能用得非常多。尤其是在整理带略缩图的文件夹时(我装了各种shell插件,基本视频、图片、音频、乃至压缩包全部有略缩图,再加上常见的PDF等,几乎大部分文件都有略缩图),经常要在大图标乃至超大图标(便于浏览内容)、列表(便于浏览文件名)和详细(便于排序以及浏览文件大小)中来回切换。所以哪怕每次操作能节省一秒,也是非常重要的。

那回到上面那三种方式,都有什么缺点?第一种方式不用说了,那个View菜单默认是隐藏的(除非你Pin起来,蛋那就要占一大坨宝贵的纵向空间,更不现实),点开之后还要在那个横竖混杂排序的8个选项里找对应的找半天;第三种的问题在于一次只能切一格(而且还不止选项里那8种,略缩图又多了N种尺寸),所以用来从诸如略缩图到详细之间的切换根本不现实。方法四那快捷键……你试试就知道有多难按了。

第二种看似是最合理的,事实上确实也比其他三种方便许多,但是有两个因素决定了它用起来依然很难受:1. Windows右键菜单的动画效果(别忘了你还得Hover展开到第二级)。虽然这个效果绝对不算慢,但是用多了还是很抓狂;2. 这个更重要——在一个全是略缩图的、大图标Layout的文件夹里,找到空白处点出右键菜单其实非常痛苦——需要鼠标瞄准半天。

当然,我们黑也要客观:除了上面四种方式,Win 10确实还在右下角添加了(前提是你没隐藏状态栏)两个小按钮,可以在详细和略缩图两个比较常用的按钮中切换。但是对我来说只有两档真的不够用。

最后一个黑点,实话说是我最近才发现的,但是一旦发现就无法直视了。在Win 7中,最下方有一条可以调整高度的“details pane”的东西。这条能显示一些文件的基本信息,比如文件大小、修改日期之类。但是真正实用的地方是,它会显示一些metadata,比如视频文件的resolution和码率、图像的尺寸等等:

QQ截图20170708181142

在Win 10中,多增加了一个“状态栏”,会显示选中文件数量和大小——但是也就仅此而已了。远没有之前的实用。不过,“Details pane”并没有真的移除,只是(开启后)挪到了右边:

Untitled-1

不用多说,也能看出这个有多不方便。

还好,上述三个黑点全都可以用OldNewExplorer这个软件修复。具体过程不说了,有挺多选项的,下面是我“修复”之后的样子:

2017-07-07 (4).png

(把Details pane挪到下面之后其实状态栏就没多大用了,可以隐藏。)

哦还有最经典的问题:“复制过来的文件有重名文件时,保留两个文件”这个在Win 7 非常简单的操作在Win 10变得异常繁琐。

这是Win 10:

heh3

这是Win 7:

heh2

高下立判。

Pre-emphasis☆超·大·全·补完☆

更新:我感觉我搜了下中文圈谈pre-emphasis的不多,而且很多没提到EAC无法识别subcode的问题,我觉得有责任感(大雾)写个摘要提炼下重点。这样别人找起来方便也不用读我这私货奇多的裹脚布。

TL;DR

  1. 关于Pre-emphasis的介绍见前文,那个不长。但是简单来说,为了减少噪声和量化误差的影响,部分老CD(尤其是日本八十年代)会对CD进行预增益,加强高频,并在播放的时候靠CD Player进行de-emphasis来还原。抓碟时需要手动进行这一过程,否则抓出来的音频高频偏高。
  2. 存在只有部分track有预增益的CD
  3. Pre-em的Flag可以写在CDDA的TOC里,也可以写在subcode(正常应该两者皆有)。抓轨软件识别出之后,一般会在CUE里加上FLAGS PRE
  4. 主流rip软件EAC只能识别TOC里的Pre-em flag,不能识别subcode里的!很重要所以再说一遍,EAC的pre-em识别功能是个残疾!要正确地读取subcode里的pre-em flag,Windows推荐使用CUERipper,Mac推荐XLD。
  5. pre-em的音轨即使识别出来,也需要处理。可以用SOX等软件直接进行de-em转换,也可以用foobar2000配合pre-emphasis=1(需要手动添加,CUE里的不识别)的tag和foo_deemph.dll、foo_dsp_deemph.dll等插件进行回放时即时de-em(不修改文件)。
  6. foo_deemph vs foo_dsp_deemph: 前者为后处理滤镜,只支持无损音频(fb2k本身限制),但是可以参与ReplayGain计算(即:RG scan是在考虑了de-em之后计算),另外BUG:暂不支持无损+内嵌CUE+仅部分轨有pre-emphasis=1的特殊情况。后者为DSP(需要DSP中勾选),支持所有格式但是不参与RG计算,无法计算出准确的音量增益。
  7. 可以利用频谱分析(推荐使用周期图,最清晰)来对比音轨和识别pre-em过的音轨。

下面正文。


我发现一个趋势:每次我写点啥文章,绝对会在写完之后发现更多该topic相关的东西,然后……写出一个比本篇还长的续。这次大概也不例外了。

Pre-emphasis危机

上篇说到Pre-emphasis的事情,有提到我对我之前的收藏里有多少没发现的Pre-emphasis的担忧,这不果然说中了。因为知道了这玩意的存在,所以在抓うしろゆびさされ組的第二专《∞》的时候专门留了个心眼,发现和首专不同,这张抓出来并没有pre-emphasis的tag,听起来也一切正常。不过和手头其他版本对比的时候我想起来当年我就觉得《おニャン子クラブ大全集》(下称:大全集)里的「バナナの涙」不太对,但是当时手头其他版本都是MP3的只有这个是无损,所以以为是别的都有问题。这和原版专辑《∞》里的一比就听出问题了:《大全集》里的版本明显尖锐许多。

我赶紧去看《大全集》的CUE,发现并没有FLAG PRE。这下我彻底恐慌了:连官方出的精选集都能搞这幺蛾子(有pre-emphasis却没标明),这到底要怎么避免哦?我进而把这《大全集》的DISC 4 with おニャン子クラブ集又重头到尾听了一遍(同时不断和手头有的其他版本对比),果然发现有多首,包括うしろゆびさされ組的全部四轨都有pre-em的问题。其他有些虽然听着偏尖锐,但是你别说,这个东西单独听真的很难说清楚到底有没有pre-em,尤其是要知道这个pre-emph主要是对某些音色的乐器影响比较大(比如上次提到过的,架子鼓里的钹),对人声尤其是比较粗的人声其实变化不大(嗯新田恵利这种很尖锐的声线就明显多了w),某些歌曲甚至对比着听都不明显。而且和其他版本对比本身也有个问题——你怎么知道其他版本就是正确的呢?尤其是我手里的其他版本很多连无损都不是。

当然,这张DISC 4本身就是从小猫单飞的各个单曲/专辑里采集来的,音源不一,所以有这种问题也算在我的意料之内,不过接下来的事情就更蛋疼了。《大全集》大部分碟是小猫俱乐部八十年代的原创录音专辑+额外曲目的形式。所以如果不出意外,至少前面和原专辑一样的部分的pre-emphasis状态应该统一:要么都有要么都没有。还是通过对比的方式,我发现至少前三张碟的大部分都OK——并没有pre-emph,还顺便发现了反而是我最早下的一张的小猫的碟(网易云扒的MP3),算是最经典的二专《PANIC THE WORLD》(下称:PANIC),其实有pre-em(汗,这碟我感觉听了有快上百遍了,当时也没觉得有啥问题……现在再去听真是尖到不行)。

用Foobar2000回放有损的pre-em音频的方案

这里插播一个实践上的问题。上次提到de-emphasis要用foobar的两个插件,lvqcl开发的foo_deemph和foo_dsp_deemph。前者是后处理插件,也就是说无设置,会无脑应用到后处理里(包含回放和转换,如果勾了后处理选项)。我之前是用的这个。结果这个的问题在于他仅作用于无损音频——关于这点我问了作者,并不是他不想改,而是foobar对后处理插件的限制(或者说foobar的工作流程如此)。所以,我上面提到那个,有pre-emphasis的MP3专辑《PANIC》,自然只能用DSP版本才行。但是这就又引出一个问题:回放增益(ReplayGain)。在进行了deemph之后,音频改动还是蛮大的,回放增益扫描的结果也不会一样。事实上,一般一个音轨deemph之后,回放增益得加个2dB。之前用foo_deemph的时候倒是OK,因为回放增益在计算时会自动考虑进去所有后处理;但是现在换了DSP版,这个在计算回放增益的时候是不工作的。

解决办法倒不是没有,就是非常啰嗦:

  1. 把MP3文件转换成WAV;
  2. 现在因为是无损了,所以foo_deemph工作了,可以扫描RG了。
  3. 把RG用MP3tag复制RG到之前的MP3(这是一个傻逼点,因为foobar2000刻意不支持文件间复制RG信息,只能手动一个个输入…一般的标签倒是支持)。
  4. 然后回放的时候……你得换成foo_dsp_deemph,因为只有这个支持有损(别忘了pre-emphasis的tag还得有,DSP里得勾上)。虽然顺序变成了先apply RG再apply deemph,从数学原理上来讲两者等效。

另外,因为回放的时候你不能同时装foo_deemph和foo_dsp_deemph(否则无损的有pre-emphasis tag的音频就会被deemph两次了),所以整个流程你得重启foobar N次,动用两个软件(foobar2000,Mp3Tag)。

不过这个流程可以稍微简化一下:在转换MP3到WAV的时候,直接用DSP版的把deemph硬编码进去——别忘了之后删掉pre-emphasis的tag——然后和上面就一样了。这个方案的区别在于完全用不到后处理版的foo_deemph,不需要重启foobar,但是总体而言还是很麻烦。

而且更麻烦的一点是,如果你改用foo_dsp_deemph(凡是你要给有损音频de-em,这是唯一的选择),那么就连原来没问题的无损的音频的RG扫描都不能直接进行了。所以,果然我的第一要务是赶紧把这些垃圾MP3洗版买二手CD洗掉。

哦顺便说句,foo_dsp_deemph还有另外一个同名的,由开发者mudlord从他的那个巨型插件foo_dsp_effect里分离出来的独立组件。之前说的有误,这个插件虽然名字叫DSP,但是其实包含上述两个插件的的功能,即后处理+DSP:在安装该插件之后,自动含对有pre_emphasis =1 tag的文件进行de-em;与此同时,你还可以在DSP开启强制de-em。而且其提供了俩DSP,外一个是黑胶的RIAA Curve

至于de-em算法方面,我对比了lvqcl版、mudlord版以及sox,虽然均非bitwise identical但是基本频谱一致,应该不会有任何听感上的差别。(7/5更新)

「およしになってねTEACHER」之谜

实践方面的问题就插播完毕,回到之前的话题,既然我们知道三专《PANIC》有pre-emphasis,那前两专呢?一专《KICK OFF》我手头的是无损,打开CUE一看发现里面大大的FLAGS PRE,这就好说了,直接给wav文件加个tag完事儿。二专《夢カタログ》(下称:夢)我之前虽然下了个MP3,但一直都没解压,因为反正《大全集》里都有了;这不为了对比我又翻出来了。果然这二专一听,也是有pre-emphasis的,但是前面说到的“蛋疼”的事情来了:我发现在de-emphasis之后,二专的大部分曲目听感和《大全集》版一致,唯有第一轨「およしになってねTEACHER」(即第二单曲),不但两者不同,和3专《PANIC》里收录的版本也不同!我又找了多个版本,最后从“尖锐程度”从大到小排列大概是这么个样子(其实有的差别很小,我用了耳朵听之外的方法检测,不过这是下一章的重点。另外,听的时候因为一定要保持平均响度高度一致(否则怎么对比),由于上面提到过的RG+pre-em的配合问题,这里请暂时配合on the fly版的RG DSP:foo_r128norm使用,并放在foo_dsp_deemph的后面):

  1. 《夢》(pre-emph)
  2. 《大全集》 = Single版
  3. 《夢》(de-emph)
  4. 《PANIC》(de-emph) =《SUPER BEST》
  5. 《大全集》(de-emph) = Single版 (de-emph)

这里,4这两个版本听感完全一致,外加精选集《SUPER BEST》是我从实体碟抓的无损(不过有个暂不相关的问题后面讲),所以姑且称之为“标准版本”。那么《夢》和重制《夢》的《大全集》的问题在于:第一,两者莫名地不一致,虽然差别不大;第二,《夢》即使加了de-em(根据其他音轨推算应该加),也比4这个标准版要尖锐一些(但是不至于刺耳);至于《大全集》版,不加de-em处于《夢》加和不加之间,加了就反而比4这个标准版还要低沉。

如果再进一步从听感上选最合理的然后简化,有三个版本(还是按照高频强度从高到低排列):

  • 《大全集》 = Single版
  • 《夢》(de-emph)
  • 《PANIC》(de-emph) =《SUPER BEST》

几乎可以肯定《大全集》制作的时候,是使用单曲音源替换掉了第一轨——因为手头的单曲版和他的特征一致,而且否则也无法解释为啥其他轨《大全集》和《夢》de-em后听感完全一致,唯有这个不同。但是要注意的是「およしになってねTEACHER」这张单曲是只有黑胶版的,并没有发过CD;我手头这个“单曲版”的MP3到底怎么从黑胶Rip出来,规不规范,无从得知;不过对于制作方波利佳音,他肯定有母带所以倒不是问题。

至于《夢》的版本,在正确地加了de-emph之后其实和《大全集》版/单曲版区别并不大,这里这里我大胆猜测,两者的区别可能仅仅是黑胶和CD制作上客观导致的(毕竟一个analog一个digital)。又或者是《夢》里的版本是Album version(当年album version基本都是没任何标记)。

至于后面为什么《PANIC》以及《SUPER BEST》这两张精选集里收的版本互相一致、却和上面的单曲版和专辑版都不同就很难理解了。而且这个“不同”还是相当明显的——从最最开始的前奏就很明显可以听出。一般而言,这类精选集收的应该都是单曲版,其不同的原因现在也很难考证了。

利用频谱分析

上述「TEACHER」这场风波,让我了解到纯靠听来比较音轨,实在是不靠谱,尤其是差别小的时候。而且很多时候,我手头只有一个版本,也很难判断是否有pre-em。介于pre-em技术的原理就是加个EQ,所以第一时间想到的就是用频谱来分析了。

一般而言,直接FFT后的Amplitude spectrum就OK,用频谱密度(Spectral Density)也行,不过基本趋势都是一样的。具体实现上前者没什么好说的,注意用双侧转换成单侧(参见Matlab的fft说明文档里的范例)就是。谱密度的话,可以直接用PeriodogramMatlab函数),或者用Welch’s methodMatlab函数)可能更美观些。

不过,在分析前,得先做一些前处理。为了方便对比,首先需要注意的是音量均衡的问题。虽然我可以在foobar里跑RG,但是为了简单起见我做了个纯粹根据数学RMS(均方根)来均衡强度的函数:

这样,我就不用操心响度不一不便于对比的问题了。实际操作上,音轨通过audioread导入之后,要先downmix到单音道,然后跑这个来均衡强度:

 [wave,Fs]=audioread(file);
 x=(wave(:,1)+wave(:,2))/2; 
 x=normalizerms(x, -20);

这里我选了-20dB这个比较低的目标RMS,因为我不想导致clipping。

然后再把信号跑上面的提到的几种频谱法就行了,下面我主要以Periodogram为例。那么,就让我们先随便找一首歌,来对比pre-em和de-em的区别。用的曲目是上次提过的「偏差値BOY」,无损音源。

(左边:原始文件(pre-em),右边:de-em之后。点击查看幻灯片或者大图)

和想象的一样,两者的周期图的区别较为明显,后者高频方向向下倾斜的趋势更明显,也符合de-em的原理。基本上,如果看到曲线接近后者,可以认为是正常的歌曲;如果是前者这种很平的,那就是有pre-em。嗯,还是有点模糊…不过比纯靠听稍微强点。

接下来就看看上面提到过的「TEACHER」的部分版本。

左起:《SUPER》版,《夢》版(de-em后)和《大全集》版(de-em后)

其中,《夢》版(de-em)在16kHZ左右的狂跌是因为MP3格式所导致。不过除掉这部分不看,也可以看出和1的标准版相比,整体更平(即:高频更高)。而最后一个的《大全集》版(de-em后)则比1更斜(即:高频更低)一些。

这里有一点要注意,就是《大全集》版(de-em后)以及这里没贴出来的单曲版(两者基本一样)都在大约1.5kHZ的地方有一个凹槽,这是很不正常的(别的版本都没有)。同样的凹槽其实我在很多音轨上都见过,我强烈怀疑是早期母带制作时,在压制或者是analog转digital时什么过程的痕迹。也许这也能解释为什么《大全集》版(de-em后)比标准版要低,虽然两者听感几乎一致。

其他几种visualization的方法既然我都做了,就贴一下让大家感受一下。

Welch’s method估计PSD:参数我都瞎搞的,不过基本而言,x后面的第一个参数window越小分割次数越高,平滑度越高,也越慢。

左起:《SUPER》版,《夢》版(de-em后)和《大全集》版(de-em后)

另外,我一直想模拟一下类似foobar频谱的按band分开的柱状图:

QQ图片20170701184057

但是有太多的搞不清楚的问题:第一,band怎么分?Foobar明显不是用线性的,每个band的label分别是(以20个band为例):

[50,69,94,129,176,241,331,453,620,850,1200,1600,2200,3000,4100,5600,7700,11000,14000,20000]

看不出有什么规律,只知道肯定不是线性,手动回归了一下大概是类似y=10^(1.5633+0.1368x) (x=1:20)的样子。

第二,每个band里面到底应该怎么把里面的点“累积”起来?是直接相加(因为是离散的)?

总之,我瞎搞了一番,最后成图如下:

左起:《SUPER》版,《夢》版(de-em后)和《大全集》版(de-em后)。单位写错了应该是HZ,懒得改了。

完整代码(含所有的可视化方式,在上方切换):

CD rip:正确读取subcode的pre-em flags

之前说过,我最怕的是厂商把明明有pre-em的曲目压到CD里面却不处理也不标明——因为这样作为听众哪怕有实体碟那么听到的也是错误的。那么我上面提到的几个例子,尤其是我手头有无损的《大全集》真的一定就是这样吗?答案是否定的。没错,虽然我手里的大全集的CUE,并没有FLAGS PRE,但是这并不代表原碟就一定没有对pre-em进行标记。

这要先从pre-em的标记方式说起。在CDDA中,可以有两个地方标记各种flag:一个是在目录(Table of contents,TOC)中,一个是在“subcode”(又称subchannel data)中。其实CDDA实际的结构要更复杂一些,简单地说就是除了16位的音频数据的部分,其他有大量的类似元数据的subcode存在。其中,每一轨都可以有自己的subcode,而在整张光碟的头部、尾部又各有一个区域完全是subcode。其中,头部的subcode包括了一些诸如discid之类的元数据,另外也有TOC(所谓的TRACK 00):TOC其实就是对整张专辑各个轨道的简单描述,主要是每轨的开始时间(便于跳转),但是也可以包括诸如pre-em在内的tag。

然而与此同时,在每一轨的subcode中,同样可以包含这样的信息。这问题就来了:虽然理想情况下两者应该是一致的,但是大量实践证明,有许多CD,只在其中一个中包含了pre-em的flag(一般是subcode)。

这俩不一致本身其实不是什么大事儿,但是问题在于,Win平台下最流行的CDrip软件,大名名鼎鼎的的EAC,不支持subcode flag。呃,其实准确地说,也是有难言之隐:早在01年左右的0.9x版本的EAC,包含一个叫做“Detect TOC Manually”的功能,说白了就是用subcode里的信息手动重建TOC(而不依赖于TRACK 00的TOC),这个功能就能检测到subcode-only的pre-em tag。但是由于当时有些CD利用hack TOC的方式来防复制,所以EAC这种功能有违反欧洲法规的风险(等于你绕过了TOC的加密),作者就在后面移除了该功能。大概这也是为什么我在论坛有时候会看到有人强调要用旧版EAC的缘故?

无论如何,身为几乎在烧友圈(至少国内)被神化的EAC居然缺失这么明显一个功能,也是略显讽刺。所以,即使碰到有的碟,正确用EAC抓取、CUE没有pre-em的flag,听起来却尖锐,也就是上面说的《大全集》中的某些轨,也不一定就是厂商搞砸,而是subcode的flag没被抓出来。当然现实来说,《大全集》是21世纪的2005年发售的,感觉还在发行带pre-em的CD概率并不高……大概还是搞砸了罢,这个没有实体版实在是无从确认了。别忘了之后波利佳音又洗过一版,说不定正是为了擦屁股呢?(笑)

不过虽然那个手头没有实体无法验证,我之所以会深入研究这个正是因为我手里就有一张这样的碟:[1986-10-21] [D32P6003] おニャン子クラブ – スーパーベスト(即前文所说的《SUPER BEST》)。这碟我用EAC抓,是显示没有pre-em:

QQ截图20170701222211

抓出来的CUE自然也没有flag。但是实际上呢?用听的就知道,第一轨「お先に失礼」肯定有pre-em,后面的第二轨又明显没有。可见这碟应该是属于部分pre-em那种(考虑到是选集,也说得通)。

既然EAC不行,我们只能换软件了。hydrogenaudio提供了一个非常完善的ripper软件列表。不过排除一些收费软件和非Win平台的(比如很有名的Mac平台的XLD),外加上我从这贴了解到的其他一些,我大概筛选出这几个:

于是先来试试最简单的CUERipper。

QQ图片20170701234441

选项非常简单,要我说是EAC过分复杂了。虽然这里看不出来,但是抓出来的CUE确实有正确的FLAGS PRE没错。具体来说,是tr. 1/6/9/11有,也和试听听感完全一致。

再让我们试试其他俩软件。cdda2wav和cdrdao都是命令行工具;而且更不方便的是两者官方网站都不提供binary,只有source code。cdda2wav现在是软件包“cdrtools”(原名:cdrecord)的一部分,这里可以找到Win-32原生的编译版本(最新:v3.02a07),或者用第三方的GUI版本——cdrtfe,里面也有编译好的cdda2wav(不过是虚拟版,如果你要单独运行,得把cygwin目录下的cygwin1.dll复制到和cdda2wav.exe一起)。

但是这个cdda2wav……用起来有问题。还是上面这张碟,让我们跑个-J(仅输出信息)来看看:

C:\Users\Administrator\Desktop\schily-cdrtools-3.02a07\win32>cdda2wav -J
No target specified, trying to find one...
Using dev=7,0,0.
Type: ROM, Vendor 'HL-DT-ST' Model 'DVDRAM SP80NB60 ' Revision 'RA00' MMC+CDDA
261632 bytes buffer memory requested, transfer size 64512 bytes, 4 buffers, 27 s
ectors
#Cdda2wav version 3.02a07_mingw32_nt_1.0.17-0.48-3-2-_i686_i686, libparanoia sup
port
AUDIOtrack pre-emphasis copy-permitted tracktype channels
 1-15 no no audio 2
Table of Contents: total tracks:15, (total time 57:28.72)
 1.( 3:50.00), 2.( 4:16.50), 3.( 3:59.45), 4.( 3:28.52), 5.( 3:44.08),
 6.( 3:12.20), 7.( 3:52.20), 8.( 3:45.20), 9.( 4:08.45), 10.( 4:00.65),
 11.( 3:58.67), 12.( 3:50.70), 13.( 3:50.15), 14.( 3:08.60), 15.( 4:21.60),

Table of Contents: starting sectors
 1.( 0), 2.( 17250), 3.( 36500), 4.( 54470), 5.( 70122),
 6.( 86930), 7.( 101350), 8.( 118770), 9.( 135665), 10.( 154310),
 11.( 172375), 12.( 190292), 13.( 207612), 14.( 224877), 15.( 239037),
 lead-out( 258672)
CDINDEX discid: Bk20VmlSPt6WEen5bpehBUBc6no-
CDDB discid: 0xc20d780f
CD-Text: not detected
CD-Extra: not detected
No media catalog number present.
scanning for ISRCs: 15 ...
index scan: 1...difference: TOC:without, subchannel:with preemphasis
correcting TOC...
difference: TOC:with, subchannel:without preemphasis
correcting TOC...
index scan: 5...difference: TOC:without, subchannel:with preemphasis
correcting TOC...
difference: TOC:with, subchannel:without preemphasis
correcting TOC...
difference: TOC:without, subchannel:with preemphasis
correcting TOC...
index scan: 6...difference: TOC:without, subchannel:with preemphasis
correcting TOC...
difference: TOC:with, subchannel:without preemphasis
correcting TOC...
index scan: 8...difference: TOC:without, subchannel:with preemphasis
correcting TOC...
difference: TOC:with, subchannel:without preemphasis
correcting TOC...
difference: TOC:without, subchannel:with preemphasis
correcting TOC...
index scan: 9...difference: TOC:without, subchannel:with preemphasis
correcting TOC...
difference: TOC:with, subchannel:without preemphasis
correcting TOC...
index scan: 10...difference: TOC:without, subchannel:with preemphasis
correcting TOC...
difference: TOC:with, subchannel:without preemphasis
correcting TOC...
difference: TOC:without, subchannel:with preemphasis
correcting TOC...
index scan: 11...difference: TOC:without, subchannel:with preemphasis
correcting TOC...
difference: TOC:with, subchannel:without preemphasis
correcting TOC...
index scan: 15...

前面的不重要,不过也可以看到TOC里没扫到pre-em。但是从每个track的subcode scan开始:

index scan: 1...difference: TOC:without, subchannel:with preemphasis
correcting TOC...
difference: TOC:with, subchannel:without preemphasis
correcting TOC...
index scan: 5...difference: TOC:without, subchannel:with preemphasis
correcting TOC...
difference: TOC:with, subchannel:without preemphasis
correcting TOC...
difference: TOC:without, subchannel:with preemphasis
correcting TOC...

这就很诡异了:扫第1轨的时候,显示发现了TOC和subchannel不一致(subchannel=有,pre-em,TOC=无),于是纠正TOC,到这步没问题;但是紧接着,居然又来了一遍,不过这次是说TOC有(废话,你自己刚改的),subchannel没(??),于是又改了一次,把TOC给改回成无了!

同理第5轨,不过这次不同的是这么翻来覆去改了三次,即TOC为无->有->无->有,最后结论是有pre-em。

那么上面的全部总结下来,最后留在.inf文件里的居然是:tr. 5/8/10有pre-em。发现问题没有?正好和之前用CUERipper测得的向前偏移了一轨(1前面是0所以没有了)。为了保险起见,我又把这几轨全部找出来,和其他碟里收录的同一轨进行反复对比。虽然有的确实比别的版本稍微锐那么一点点(例如tr. 10那首「シンデレラたちへの伝言」我手头有俩无损版本的,有一个还是我自抓),但是如果真的去跑de-em,那又钝太多(靠频谱),所以基本可以90%肯定是没有pre-em的。

好家伙,Linux下最有名DAE(digital audio extraction)软件的cdda2wav居然有毛病,这事儿我得去hydrogenaudio反馈一下了。那让我们看看cdrdao好了。这里先说个笑点:EAC的目录里赫然有个cdrdao.exe(虽然是很老的1.1.9版),可见他也是调用了cdrdao来进行TOC提取的……然而阉割了cdrdao带的从track subcode里提取信息的功能。这软件的Windows binary也是相当的难找,最后在这个已经404的网页Archive里找到了一个编译好的1.2.3版(2009年最后版)和相应的dll文件(其实就是上面提过的cygwin1.dll)。

废话不多说,上log。

E:\sync\Software\cd\cdrdao-1.2.3-bin\cdrdao-1.2.3-win32>cdrdao read-toc --device
 8,0,0 test.toc
Cdrdao version 1.2.3 - (C) Andreas Mueller <andreas@daneb.de>

8,0,0: HL-DT-ST DVDRAM SP80NB60 Rev: RA00
Cannot read driver table from file "/usr/local/share/cdrdao/drivers" - using bui
lt-in table.
Using driver: Generic SCSI-3/MMC - Version 2.0 (options 0x0000)

ERROR: Unable to determine drive letter for device 8,0,0! No OS level locking.
Reading toc data...

Track Mode Flags Start Length
------------------------------------------------------------
 1 AUDIO 0 00:00:00( 0) 03:50:00( 17250)
 2 AUDIO 0 03:50:00( 17250) 04:16:50( 19250)
 3 AUDIO 0 08:06:50( 36500) 03:59:45( 17970)
 4 AUDIO 0 12:06:20( 54470) 03:28:52( 15652)
 5 AUDIO 0 15:34:72( 70122) 03:44:08( 16808)
 6 AUDIO 0 19:19:05( 86930) 03:12:20( 14420)
 7 AUDIO 0 22:31:25(101350) 03:52:20( 17420)
 8 AUDIO 0 26:23:45(118770) 03:45:20( 16895)
 9 AUDIO 0 30:08:65(135665) 04:08:45( 18645)
10 AUDIO 0 34:17:35(154310) 04:00:65( 18065)
11 AUDIO 0 38:18:25(172375) 03:58:67( 17917)
12 AUDIO 0 42:17:17(190292) 03:50:70( 17320)
13 AUDIO 0 46:08:12(207612) 03:50:15( 17265)
14 AUDIO 0 49:58:27(224877) 03:08:60( 14160)
15 AUDIO 0 53:07:12(239037) 04:21:60( 19635)
Leadout AUDIO 0 57:28:72(258672)

PQ sub-channel reading (audio track) is supported, data format is BCD.
Raw P-W sub-channel reading (audio track) is supported.
Cooked R-W sub-channel reading (audio track) is supported.
Analyzing track 01 (AUDIO): start 00:00:00, length 03:50:00...
Found 38 Q sub-channels with CRC errors.
WARNING: Pre-emphasis flag of track differs from TOC - toc file contains TOC set
ting.
Analyzing track 02 (AUDIO): start 03:50:00, length 04:16:50...
Found pre-gap: 00:02:10
Found 28 Q sub-channels with CRC errors.
Control nibbles of track match CD-TOC settings.
Analyzing track 03 (AUDIO): start 08:06:50, length 03:59:45...
Found pre-gap: 00:02:08
Found 21 Q sub-channels with CRC errors.
Control nibbles of track match CD-TOC settings.
Analyzing track 04 (AUDIO): start 12:06:20, length 03:28:52...
Found pre-gap: 00:02:00
Found 11 Q sub-channels with CRC errors.
Control nibbles of track match CD-TOC settings.
Analyzing track 05 (AUDIO): start 15:34:72, length 03:44:08...
Found pre-gap: 00:01:72
Found 9 Q sub-channels with CRC errors.
Control nibbles of track match CD-TOC settings.
Analyzing track 06 (AUDIO): start 19:19:05, length 03:12:20...
Found pre-gap: 00:02:03
Found 12 Q sub-channels with CRC errors.
WARNING: Pre-emphasis flag of track differs from TOC - toc file contains TOC set
ting.
Analyzing track 07 (AUDIO): start 22:31:25, length 03:52:20...
Found pre-gap: 00:02:03
Found 20 Q sub-channels with CRC errors.
Control nibbles of track match CD-TOC settings.
Analyzing track 08 (AUDIO): start 26:23:45, length 03:45:20...
Found pre-gap: 00:02:03
Found 17 Q sub-channels with CRC errors.
Control nibbles of track match CD-TOC settings.
Analyzing track 09 (AUDIO): start 30:08:65, length 04:08:45...
Found pre-gap: 00:02:03
Found 13 Q sub-channels with CRC errors.
WARNING: Pre-emphasis flag of track differs from TOC - toc file contains TOC set
ting.
Analyzing track 10 (AUDIO): start 34:17:35, length 04:00:65...
Found pre-gap: 00:02:05
Found 10 Q sub-channels with CRC errors.
Control nibbles of track match CD-TOC settings.
Analyzing track 11 (AUDIO): start 38:18:25, length 03:58:67...
Found pre-gap: 00:02:03
Found 15 Q sub-channels with CRC errors.
WARNING: Pre-emphasis flag of track differs from TOC - toc file contains TOC set
ting.
Analyzing track 12 (AUDIO): start 42:17:17, length 03:50:70...
Found pre-gap: 00:02:02
Found 11 Q sub-channels with CRC errors.
Control nibbles of track match CD-TOC settings.
Analyzing track 13 (AUDIO): start 46:08:12, length 03:50:15...
Found pre-gap: 00:02:00
Found 9 Q sub-channels with CRC errors.
Control nibbles of track match CD-TOC settings.
Analyzing track 14 (AUDIO): start 49:58:27, length 03:08:60...
Found pre-gap: 00:02:00
Found 7 Q sub-channels with CRC errors.
Control nibbles of track match CD-TOC settings.
Analyzing track 15 (AUDIO): start 53:07:12, length 04:21:60...
Found pre-gap: 00:02:00
Found 6 Q sub-channels with CRC errors.
Control nibbles of track match CD-TOC settings.

Reading of toc data finished successfully.

可以看到,找到且只在1/6/9/11轨中找到了pre-em信息,和CUERipper一致。不过有一点,他只是提醒你“WARNING: Pre-emphasis flag of track differs from TOC – toc file contains TOC setting.”,实际在提取出来的TOC文件里并自动没有修改成正确的、来自subcode的pre-em tag,这个还得自己手动来。

部分Pre-em的CD rip的存储和回放方式

碟算是抓完了,pre-em信息也有了,这就引出下一个问题:如何存储/回放这样的碟。WAV+CUE的方式是行不通的——因为上篇说过了,外置CUE里的FLAG PRE foobar2000不认,你也无法给CUE加pre_emphaiss的tag,加到音频文件里又会适用给整张碟。

保持整轨的前提下,唯一的解决方案就是FLAC+内嵌CUE了(其他无损格式应该也行)。要注意的是,FLAC+内嵌CUE,并不仅仅等价于“FLAC+CUE,只不过CUE内嵌”。在这里,内嵌的CUE主要起一个提供时间戳分轨的功能,但是实际上你对每一轨的metatag的自由度非常高,和单独的FLAC没有两样。也就是说,你完全可以做到给单个FLAC文件里每一轨加不同的pre_emphaiss的tag——即使CUE不支持。你甚至可以做到:每一轨的album不一样!不过如果你真的这么做,然后用foobar2000带的内嵌CUE查看器你会发现,CUE里的album还是老的值,可见这些元数据里并不是真的存在CUE的(不过,如果你进行一些CUE能支持的metatag操作,比如同时修改所有轨的albumalbum artist,或者单独轨的artisttitle,内嵌的CUE还是会跟着同步更新的)。

这个方案算是不错,但是和直接用分轨FLAC+pre-em tag一样都有一个问题前面提过:RG。因为我有一些lossy的需要加pre-em tag的文件,所以我被迫用了DSP版的de-em;但是DSP和RG不兼容(计算RG时不考虑DSP)。所以我现在俩抉择:1) 转成分轨FLAC,且同时直接将de-em硬计算进去;2) 换回后处理版的de-em,但是把那些有损音频强行transcode成无损一份来存储。实话说,两种方案都很蠢。现在我暂时还没想好到底要用哪种方案,姑且先去foobar论坛发了一贴问。

更新:今天又发现一个foo_deemph的bug:丫不支持FLAC+内嵌CUE的pre-emphasis tag,加了也不会de-em,orz 所以指望靠FLAC+InCue来保持最大程度的原Image是不现实了。我现在采用的方案是:使用foo_deemph插件,但是把所有 1) 只有部分轨有pre-em的CD 2) 有损带pre-em的音频 全部转一份分轨FLAC然后加pre_emphasis = 1 的tag。老的文件打包备份(防止被foobar20000的数据库读取;我在FB里filter掉了*.rar和*.zip)。这样的缺点是很占空间(有损转无损,还保留两份…)

OK,这个话题到此算是结束了。接下来我得强迫症般地把之前抓的碟都拉出来看一看,以保证没有subcode的pre-em flags……

收集老CD要注意Pre-emphasis的存在

Blog荒废了好几个月,本来正好前天有个题材(压片相关)要写,结果正巧碰到另外个事儿,觉得得先写写这个。无损音乐自己玩了这么多年,以为算是太阳下面没什么新鲜事,没想到上次一个Replay Gain一个Media server把我好好折腾了一番。这不,今天又接触了一个前所未闻的概念——emphasis。

这个问题其实要从几个月前说起。当时我在收集小猫俱乐部相关CD的时候,下到这么一盘——うしろゆびさされ組的首专「ふ・わ・ふ・ら」。

うしろゆびさされ組 - ふ・わ・ふ・ら

当然,这种80年代的老碟都是基本只能找到MP3或者网易云音乐(这里顺便提一句,网易云绝大多数老一些的、尤其是非华语音乐资源都是网上找的,那么多码率选项显然都是二次转换出来的。那伪320kbps实际一般看频谱都是128kps级别,还不如直接128呢,少一次有损压缩),我倒也没要求太高。

但是这张碟的奇怪之处在于,听起来非常“不舒服”。什么程度呢?我听完一张甚至会觉得耳朵疼(我听音乐一般声音很小)。我自认对这些东西比较敏感,觉得这碟肯定是哪里不对。我找遍网上所有的版本,都有这个问题。其中比较明显的是其中收录的单曲1、2,「うしろゆびさされ組」和「バナナの涙」,和单曲版一比就能发现明显区别。

不过,后来我在维基百科看到,说是其中收录的单曲曲目虽然未标记但是是Album version,所以这也算解释的通。不过,这事儿我一直放在心里没忘。

后来,在网上漫无目的地搜索下不到的「AN bALANCING TOY」时,发现骏河屋有人卖二手的「ふ・わ・ふ・ら」等其他几张指指点点组的专辑,外加写真集什么的。正好从来没在骏河屋里买过东西,也试试代购靠不靠谱,就准备把上面有的东西都一起扫了。国内的几个,2poi啊玛莎多拉啊啥的试了下,总有支付或者快递之类的种种问题,于是转投面向西方的服务。最开始试的就是在骏河屋下面打广告条的“Buy Smart Japan”,网站倒是非常简单好用,全自动和亚马逊差不多,不过快递只支持EMS,那4500円的运费把我吓到了(毕竟日亚直购运费便宜的一笔)。

于是我又找了另外一家,Proxyrabbit Japan。这个的界面就蠢多了,买骏河屋一次最多5件,而且你填完表格之后他是通过人工确认和你邮件交流……并且不能即时出运费价格,要先交定金(最低25%商品价格)然后他那边到货了才告诉你邮费。不过我当时抱着上当一次也无所谓的心理就上了。大概quote后过了2天我交了定金(PayPal),又过了一周多那边才收到(据说骏河屋就是挺慢?)货。运费这次倒是有几个档可以选,结果EMS还是4500,于是我选了两千多的Air mail。

废话不多说,总之就是昨天才到我家,还没投递成功我去邮局自取的,前前后后快一个月了。以后还有需求的话,估计会试试转运而不是代购的(看到推荐了两家,http://www.biginjap.com 和 http://www.tenso.com/en/)。二手卖相还可以,没侧标就是了。

IMG_20170621_220513

EAC的话我虽然用得很少,前个月倒也抓过几次,我的经验就是没必要看什么“教程”,按照软件自带的指引一步步点,把offset之类的硬件相关设好就OK。我当年第一张是用的安全模式抓的,速度慢到发指(1.几x),我于是直接改用爆发模式抓(速度7-8x),反正抓完之后有AccurateRipCTDB两个数据库比较CRC32,如果都一致,都是bitwise identical了还觉得只有安全模式才行那只能认为是没文化了。这里容我吐槽下,都CURRENT YEAR了EAC抓出来的CUE居然还只能是ANSI(非Unicode)的,蠢哭。

总之……回到正题。于是我自然先赶紧抓了这张碟来听。嗯?听感好像和之前的完全一致,看来这碟本身就这样?就在我放弃治疗去改CUE的时候,发现有个没见过的玩意:

REM DATE 1986
REM DISCID 9109350A
REM COMMENT ExactAudioCopy v1.3
PERFORMER "うしろゆびさされ組"
TITLE "ふ・わ・ふ・ら"
FILE "うしろゆびさされ組 - ふ・わ・ふ・ら.wav" WAVE
 TRACK 01 AUDIO
 TITLE "SE・KI・LA・LA"
 FLAGS PRE
 INDEX 01 00:00:00

FLAGS PRE是什么鬼?

结果一查不得了,这正是问题的关键。原来,通信中存在一个叫做“emphasis”的技术。说来也很弱智,就是对于多少频率的信号(一般是高频),会进行放大;在信号到达接收端之后,再用反函数把信号衰减回去。放大的过程叫“pre-emphasis”,衰减叫“de-emphasis”。不用说,其目的是减少传输过程中噪音的影响,因为中间引入的噪音会被同样衰减,从而达到更高的信噪比。

这个技术在黑胶唱片、磁带等Analog领域广泛应用,但是在CD这种数字媒体中的应用很少。准确地说,在早期CD(70-80年代)只有14bit的时候,这个应用在其中来减少量化误差(为什么?留作思考题了w)。后来CD的精度变成16bit之后,就基本很少用了,但是其依然存在于CD的标准中,想用的话也是可以的。

事实上,带pre-emphasis的CD恰恰就仅在日本的80年代最流行(注意,日本的CD从80年代后半才开始普及,之前基本发音乐只发黑胶、磁带),其他国家不多。另外需要说明的是“pre-emphasis”(即:特定高频的信号的增益)是直接压在CD里的,不过一般同年代的CD player都会识别metatag里的相关flag,从而自动进行进行de-emphasis来还原信号。

但是到了电脑光驱,这就取决于你的软件端怎么处理了。我们上面可以看到,对于EAC抓轨,他会识别出相关的tag,并标记在CUE中。接下来就全看你的播放器了。

对于绝大多数人,和绝大多数软件,他们是完全无视这个信息的,所以就导致了我最开始提到的那张碟“很怪”的声音——高频被增益了(具体算法根据hydrogenaudio,是“a first-order filter with a gain of 10 dB (at 20 dB/decade) and time constants 50 μs and 15 μs”,我虽然大概能理解但是就不硬着翻译了,毕竟不是科班出身),所以听着耳朵疼,架子鼓里的钹的声音也变了。另外因为这个东西的存在过于冷门,大多数人估计根本没处理过,这也是为什么网上流传的版本全是这个德行的原因。仔细想想可怕极了,到底有多少手里的80s音乐会有这种问题而没被发现呢?

既然知道了原因,解决方案就很简单了。用Foobar2000的话,有网友开发出两个插件来处理这个问题——一个是后处理,一个是DSP插件,前者会自动在播放、计算音量增益、转换(前提你勾选了后处理)时进行de-emphasis,后者顾名思义需要在DSP里作为一个滤镜勾选。需要注意两个默认都不工作,需要对音频文件(而且前者仅限无损)添加PRE_EMPHASIS的tag,值为“1”,“ON”或者“Yes”。

这里有几个细节我啰嗦几句。一个就是CUE里的FLAGS PREFoobar是完全无视的,所以对于无损+CUE的方案,你要把上述的Tag加到你的无损文件里去。另外一个就是转换时,如果勾选了后处理(你一般应该勾选)会自动把de-emphasis给硬编码进去,所以出来的output是已经进行过de-emphasis的了。但是因为转换器默认会把tag都复制到新文件里,所以新文件还有PRE_EMPHASIS的tag,需要手动移除,否则就相当于de-emphasis两次,声音就变钝了。另外,虽然可能性很小,万一有别的软件支持识别CUE的flag,那么播放CUE+有PRE_EMPHASIS tag的无损音频可能会导致多重de-emphasis?嘛估计应该没软件这么蠢就是……

其他的处理软件比较著名的有命令行工具sox,用sox input.wav output.wav deemph可以轻松de-emphasis。虽然和foobar插件两者的结果并不是波形级一致,但是听感无区别。

至于我这张CD的处理方法自然可以是两种,一个就是直接硬编码进去,另外一个就是播放的实时进行de-emphasis。我暂时选的是第二种,这样就不用保留一个单独的原版WAV了,毕竟是破坏性操作;因此,转WAV为FLAC(节省体积)的时候要特别注意去掉勾选“后处理”选项,或者先转换格式,后加pre-emphasis tag。

最后,以这张专辑中我最喜欢的一首歌曲,『偏差値BOY』作为结尾吧 😀

利用MATLAB来进行批量图片颜色匹配

Edit: FML,回到家才发现用MATLAB处理出来的颜色和死了妈似的,学校的垃圾TN显示器太坑了……下文已经尽量修正。

在图像处理尤其是对冻鳗扫图处理时,“颜色匹配”是一个经常需要进行的操作。因为印刷和扫描的技术和载体所限,扫描出来的结果经常和艺术家想呈现的有很大偏差。当然,最理想的纠正方式应该是从源头上去校准扫描仪,但是很多时候这是不现实的(例如,图片来自网络)。

在有官方提供的Sample的情况下(需要注意:在官网、亚马逊等地方获取样图时,诸如CMYK硬拉成RGB之类的错误屡见不鲜,一定要避免从源头上就选了个错误图片做reference),利用数字原档来校准、还原颜色就成为了可能。

Photoshop有个内建的匹配颜色(英文:Match color)功能:

2017-04-21

可以看到,功能还算丰富。除了上面的滑块选项以外,你可以选择用于计算的选取范围(包括源和目标的都能选),最后计算出来的映射/变换则可以应用到全图(最上面那个现在灰色的选项),而不是仅限于选区。

PS的颜色匹配效果还凑合,但是为了追求最佳效果,在匹配之前一定要尽量把图像对齐。如果两者的范围不一致,也要把多出的部分裁掉。还好,这个过程在PS里也很简单:

  1. 导入两张图片为两个不同的图层。你的扫图置于底层(底层是不会变形的),样图置于上层。注意,不能是“背景”,所以如果是的话,双击一下转换为图层先。
  2. 选中两个图层,然后点Edit->Auto align layers。
    • Projection选Auto就好,下面两个透镜扭曲修正的选项不用勾。确定之后过几秒,应该就对齐了。PS这个算法不是那么完美,尤其是在图像有缩放的时候做不到pixel级对齐,不过对于我们这里的应用足够了。
    • 如果样图太小,可能会无法自动对齐。这种情况下,预先用自由变换(Ctrl+T)调整一下样图的大小吧。记住拖拽时按住Shift可以保持宽高比。你不用刻意调整和你的扫图完全一样大,差不多随便拖一下就行,因为自动对齐时PS会自动帮你做。
  3. 把两者不一致的地方裁剪掉。也很简单:ctrl+点击第一个图层,会建立一个和该图层一样大的选区;然后Crop就好。如法炮制第二个图层。这样,剩下的部分就是两个图层都有的部分。如果只有一个图像上有水印、文字之类的,最好把那部分也裁掉,否则影响颜色匹配。

接下来,只要进行颜色匹配就行了,注意源和目标别选反就行。

但是这里一个问题:如果你有一组扫图,其色彩基本类似,理论上而言,你应该只需要找其中一张的sample匹配,然后把那个transofrmation存下来,适用到其他图即可。可惜,PS并不提供这样的功能。当然,有一个笨方法:先把所有图片拼到一起,然后只选中其中一张图的区域进行颜色匹配,但是应用时忽略选区。但是这个方法实在听上去太蠢了还不能自动化。

于是,我转而追求其他的办法。MATLAB的Image processing toolbox集成了许多现成的图像处理函数,当年学DIP的时候也用得不少,于是我第一个就想到它。随便找一下,这个Histogram matching(imhistmatch)似乎就符合我的需要。

虽然不知道PS的图像匹配是什么原理(毕竟是商业机密),但是我约莫着其实也八九不离十也就是histogram matching。说白了,就是找到一个映射,使得变换后的直方图累计分布函数(CDF)和reference相似。我先试了下MATLAB这函数的效果,嘿还不错,甚至可以说比PS还强一点:

compare

(点击大图,注意观察云彩的颜色)

修正!PS的效果其实秒杀MATLAB的…只是由于学校的垃圾屏幕太烂我才没看出来orz 不过后面的内容姑且保留了,从RGB改用YCbCr效果会好很多。

既然我们找到这个函数,那现在就只需要剖析一下他(选中函数ctrl+D可以看源代码),看看里面到底是怎么运行的,这样我们就能把那个transofrmation给导出来。

结果这个imhistmatch的代码相当简单,其核心部分居然是调用另外一个函数——histeq,即直方图均衡化(Histogram equalization)。其实也很好理解,直方图均匀化本来就是直方图匹配的一个特例——即匹配出来的目标CDF是一条直直的斜线。在MATLAB里,两者都集成到了histeq而不是imhistmatch里可能也是因为前者叫起来更顺口吧。histeq除了直接均衡化之外,还可以手动设一个作为目标/参照的直方图来match,所以这里就是把我们的目标/参照的直方图拿来作为input了。需要注意histeq只支持单通道(准确地说是imhist只支持单通道),所以需要手动写个矩阵运算或者循环来分别搞RGB。

那么,histeq里面的原理又是怎样呢?文档里可以看到,其实这个函数本身就可以返回一个变换T给你了。这个变换T的形式是1×256个double array,每个分别是一个0-1闭区间内的值。用法嘛其实不言而喻了,就是用每个位置对应的double去乘原图的[0, 255]的强度值,得出来的就是output,其实和PS里的Curve是一模一样的。不过,这里可以偷个懒,PS有个内部函数grayxformmex就是帮你算这个的,会自动处理所有支持的图像数据类型(从double,uint8到uint16),而且是用C/C++写的速度更快,推荐直接用它。不过这个内部函数不在path里所以不能直接用,你得先复制grayxformmex.mexw64到你得工作目录才行(是编译过的文件,源代码如果感兴趣的话,网上能找到一个老版本的)。

OK,有了这一切东西,我们就可以很简单地写一个script:

我尽量模仿了imhistmatch的格式,所以应该对多种数据类型都能处理。不过可能数据验证方面的鲁棒性会差一点。最后%%后的部分是批量处理其他文件的代码。

由于前面红字所说的原因,我紧急改用了YCbCr channel来进行处理,效果确实好很多,但是天空的颜色果然就不对了orz,而且还是不如PS的…HSV我也试了,结果会出现奇怪的色块,懒得深究了。上面的代码已经更新,可以选择用RGB(默认),YCbCr或是HSV(开关在最上面)。对比一下三种的效果:

compare2.png

不过需要注意,MATLAB的算出来的结果颜色有点离散,纯色区域多多少少有些banding。这点PS的祖传算法就略胜一筹了,出来的图像的直方图还是非常连续的,没有太多的banding的问题。不过后来发现其实只有RGB模式会有这个问题,换成YCbCr就基本无大碍了。无论如何,这里推荐俩解决办法:方法1,先跑色彩匹配,再进行其他去噪步骤,会很大地由于去噪中的模糊等因素缓解这个问题;方法2,加点白噪声先,至少观感会强很多。这两种方法可以结合使用。

另外,PS的明度控制明显要好很多,我用YCbCr跑出来的结果高光区域都变淡了(比如有几页的纯白背景)。

2017-05-21补充:

后来我又多次使用上文所述的方法进行校色。我发现这个方法其实还是相当可行的,在很多时候甚至比PS的结果要好。不过要注意的是,即使是用YCbCr,有时候(很少见)也会出现和HSV一样的色块问题,所以出了结果一定要检查一下。

关于JPEG的那点事儿 Part 2:JPEG原理

前言

本文其实于差不多正好1年前写成,是关于JPEG的那点事儿的补充。但是由于实战篇一直烂尾,拖到现在。前几天看到Google发了个JPEG新算法,说是可以将JPEG的体积同质量情况下再压缩35%,突然想起了这文了。为了说清楚Google为什么能在古老的JPEG上压榨出新的空间,我觉得还是有必要先讲清楚JPEG的原理。但是本文成文之后实在太长,所以我想了想还是把和Google算法相关的、以及一个TL;DR版的JPEG原理单独发文(大概明天8点发w)。另外,前面提到的“实战篇”也会分割放送,减少文章长度。

序言

有一位朋友看了上文后问到,为什么步进(progressive)JPEG可以提高压缩率?

严格来讲,步进(Progressive)和交错(Interlacing,虽然“交错”是最常用的翻译,但是我是在无法完全理解这两个字的汉字想表达啥…)并不是一个概念。要讲步进,得先讲讲交错。

交错指的是图像解码时(以及存储时)并不是按照某个逐个像素依顺序解码——而是采用跳跃的方法:例如将整个图像分割成九个区域,先解码出每个区域的大体形状,然后再逐步解码细节。一般而言,这样的层级解码会分不止2层,例如在PNG使用的Adam7算法中,一共有7个子图像会被存储起来。维基百科上这张图可能会更直观一些:

https://upload.wikimedia.org/wikipedia/commons/2/27/Adam7_passes.gif

(From Wikimedia Commons)

这种方式的好处是,在图像加载过程中,图像会由模糊(准确地说是马赛克状)逐渐变清晰,而不是从上到下一行行地显示。这样在图像加载中途读者就可以对图像大概有个概念,而不是只能看到上面完全看不到下面:观感上加载速度会变快,而且也更方便一些。

你肯定要问了,这么做不是相当于在原图上在集成几个不同尺寸的略缩图,体积不增加就不错了,怎么会减小呢?事实上,对于其他图片格式,例如PNG和GIF,如果开启“交错”选项,确实图像会变大。但是对于JPEG的具体实现,所谓的“ progressive”,情况又不太一样。用IJG官方的FAQ里的话说“Basically, progressive JPEG is just a rearrangement of the same data into a more complicated order.”。但是具体技术上的实现方式是什么?

要说清这事儿可能还真得从头说一下JPEG的压缩过程。既然要讲,那就讲的详细一点。接下来我将把JPEG压缩的每个细节步骤(除了DCT的数学原理,这个我真不行……)都讲清楚。如果只是想了解大概,维基百科就写的不错了:但是如果真想做到自己写一个解码编码器的程度,有些细节不厘清还真不行。

JPEG编码基本原理

在DCT之前

JPEG编码主要分成三步,DCT、量化以及无损压缩。不过,在DCT之前,还要先色彩空间转换和色度抽样。色彩空间转换干的就是将RGB转换成YCbCr——即将亮度(Luma)和色度(Chroma)分离开,其理念是人眼对亮度的变化远敏感于色度变化等一大套感知视觉理论,这里不再赘述。顺便一提,色彩空间转换并不是完全无损的——因为转换前后都是整数,自然不可避免会有舍入误差。

转换之后,既然我们知道亮度更敏感,那就有做文章的空间。所谓色度抽样,就是对色度的部分进行抽样/缩小。主流的抽样方法有4:2:2、4:2:0,在JPEG的语境下更多叫做2×1和2×2,前者指水平分辨率抽样一半,垂直不变,后者指水平垂直各抽样一半。完全无抽样的叫做4:4:4,或者1×1。至于具体抽样缩图的算法JPEG里好像没有定义,一般都是直接将相邻两个像素求个平均了事(这里可能会导致图像处理界另一个著名的历史遗留问题:线性vs非线性色彩空间,以后抽空再单独写一下)。

抽样完毕后,终于可以进行到真正的编码部分了。JPEG压缩时,先将原图分割成8×8的block进行编码,又叫“最小编码单元(Minimum Coded Unit,MCU)”。当然,如果你有用色度抽样,MCU的大小也会相应放大。例如,如果你用了4:2:0的抽样,那么MCU就会变成16×16——但是Cr和Cb的实质大小其实依然只有1个8×8,只是塞进去了4个8×8的Y罢了。在编码的时候,每个通道也是分开的,所以这样的MCU可以理解成6个不同的blocks就行(但是压缩完之后的数据顺序又有讲究,后叙)。下面,我用“block”来特指单个通道的8×8的单元,来和MCU区分。

DCT

接下来,我们要对每个block的像素值(8bit图像就是0-255了,接下来全部以最常见的8bit为例。JPEG标准额外支持12bit图像,但是主要用于医疗领域,普通情况极少有人用)进行偏置128之后(使其集中在0两侧,加快DCT运算)做2D DCT转换成频域。转换出的结果依然是一个8×8的矩阵,只不过每个数据点代表的是不同频率(准确地说是一组不同的pattern(见下图))的强度:左上是低频,越往右下越高频。

File:Dctjpeg.png

(From Wikimedia Commons)

所以DCT说白了就是把原图分拆成这些pattern的线性叠加。其中左上角的DC分量,可以近似理解为整个block的强度均值,剩下的则是高频(或称AC)分量。

量化

DCT这步数学上来讲(不考虑舍入误差)是可逆的,真正的有损编码的是下面的量化步骤。量化在这个语境下其实就是拿一个预设的系数矩阵(量化表)去逐一除之前得出的DCT矩阵——介于人类对低频比较敏感,细节可以适当丢失,这个表的原则是越往左上系数越小,越往右下系数越大。至于具体的数据,据说都是实验出来的,不同的软件可能不同。主流的libjpeg根据JPEG标准的推荐,提供了一套0(1?)-100质量分别对应的表,可能也是最常见的系数。当然,你也可以自定义系数,例如Photoshop内置的量化表就和别的软件大多不一样。ImpulseAdventure的作者提供了一份非常详尽的市面上常见软件、相机的内置系数表。表格里数据的总体大小决定了JPEG的质量——系数越大,质量越低。高质量的表可能系数都只有个位数(事实上,100%质量的量化表全是1),而低质量的,例如拿一个JPEG 50%质量的量化矩阵来说,左上角的DC分量的除数有16,而右下区的甚至高达100左右。想象一下去拿这个表去除DCT矩阵,除出来的结果再近似到整数,考虑到右下的高频AC分量本来强度就不高,除以100之后基本都肯定小于0.5了,也就是会被约成0。这么搞下来,整个表就会变成一个右下区几乎都是0,而其他区域数值也很小的矩阵。

Y和Cb/Cr会有不同的量化表(可以猜到,色度那张压缩更狠),这个表会被嵌到JPEG文件的头部中。通过表格的数据可以估算JPEG的当时编码时的质量。

无损压缩

之后就到了无损压缩的部分,也是整个JPEG编码中最麻烦的部分。首先又得学个新词儿——Interleaving。这个一般也翻译成“交错”……但是和上面提到过的Interlacing不是一回事。我们前面知道,每个MCU有少至3个、多至6个8×8的blocks。编码的时候,我们既可以按照MCU分类,一次编码完整个MCU的所有block再进行下一个、也可以采用别的方式,这里按下不表。不过,最常用的、baseline的方法是先按MCU归类,然后按照一个固定的顺序读取。如果是无色度抽样的3 blocks MCU,那就是YCbCr的顺序,如果有多个Y,那就是Y00/Y01/Y10/Y11(左上,右上,左下,右下)/Cb/Cr的顺序。

sequence_2x2

(From ImpulseAdventure)

这种“不停地在不同components交替取数据编码”的方式(components在这个语境下就是不同的通道,Y、Cb和Cr都分别是一个component)就叫做“interleaving”,这样的JPEG叫做interleaved JPEG。可以看到,绝大部分的baseline JPEG都是interleaved的。

让我们回到每个block里面。首先要将我们的64个分量1D化。我们这里并不是按行或者列的顺序排队,而是通过斜对角蛇形的方式,从左上逐渐跑到右下。

600px-jpeg_zigzag-svg

(From Wikimedia Commons)

其原因是:越靠近左上的频率越低,量化压缩之后也越可能不是0,这样排序之后便于下面的游程编码(RLE,run-length encoding)进行。

游程编码

所谓RLE,是一种无损压缩高重复率数据的算法,还是直接引用维基的例子好了:“举例来说,一组资料串”AAAABBBCCDEEEE”,由4个A、3个B、2个C、1个D、4个E组成,经过变动长度编码法可将资料压缩为4A3B2C1D4E(由14个单位转成10个单位)。”

至于JPEG中的实现说起来则比较麻烦。简单概括,对于AC分量,我们只描述非零的强度和他们的位置。具体来讲,就是把非零的coefficient转换成“前置0的数量+强度的比特数”的分类单元+具体强度的形式。而强度是0的AC,自然被包含在那“前置0”里面了。这样说可能太抽象了,举个例子。假设我们的AC1和AC3(分别是矩阵第一行第二排和第三行第一排)分别是-2和3,而AC2是零。那么,AC1就会被表示成

(0,2)(-2)

(0,2)乃是分类单元(categories),0表示前面有0个0(毕竟这是第一个),2表示后面跟的数据(-2)需要用两位(2bit)才能表示(下述)。同理,AC3前面有1个0(AC2),那么就会被表示成

(1,2)(3)

或者我们把前面俩改写成16进制,共同占用1个字节:因为后面的数据不会超过15位(F)(实际上不会超过14位,高位F的部分除了F0特殊定义之外,并用不到),零的数量虽然确实会超过15个,但是我们特别指定(15,0)(0xF0,有的地方称作ZLF,“zero run length”的意思)为用来表示16个连续的0。如果某个block最后部分全是零,可以提前输出(0x00,End-of-Block,EoB,无需跟强度值)来结束这个block。

DC分量处理

对于DC量,首先我们用和前一个block的DC分量的差分的方式来记录——这样做可以节省一部分体积,因为图像大多是连续的,相邻block的DC分量一般相差不大,这么操作可以大幅度降低DC分量(一般是最大的一个数,也需要最多比特)的比特长度。这种编码方式叫做Differential pulse-code modulation

另外,我们也要像AC一样,对每个强度量先指定其位长的分类单元(否则我们怎么知道读到哪里算完呢?):0表示该DC量是0、1表示该DC量的数据只有1位、2表示有两位、……直到15(或者F),然后再写我们的数值。例如,如果一个DC量是15,那么就会被表示成

(4)(15)

即(位长)(数据)的形式。因为15需要4位才能编码下(下述),所以是4。

编码为二进制及霍夫曼编码

接下来,我们要进行最后的终极编码,也就是将上面这一堆劳什子转换成二进制。

对于强度的部分,很简单,按照之前分配好的位数转换即可。但是别忘了,我们的数值是有符号整数,要转成无符号的0和1,所以要稍微偏移(shift)一下。如果数据是0,对于AC自然就直接跳过了,DC会被分配“0位”,也就是只有分类单元的部分,而并没有数据。分配1位时,0代表-1,1代表1;分配2位时,用00表示-3、01表示-2、10表示2、11表示3,以此类推。这个编码方式是JPEG标准指定的,所有JPEG都一样,不能自定义。其实,它是用补码(二补数)推算出来的:如果数据是正的,那么就取补码最后N位(位数前面分配了);如果数据是负的,就取补码最后N位再减一。举例子的话,15被分配4位,15的补码是0…00001111(具体有多少个前置零取决于你的比特数,但是这里不影响),取最后四位就是1111;如果是-15,补码是1..1110001,取最后4位再减一就是0000。到这里也应该看出来了,我们的位数也不是随便分配的,说白了就是对于绝对值处在[2^N, 2^(N+1))之间的数,我们分配N位来表示。具体可以参照这文中的Table 5。

不过对于前面的分类单元部分,则不是直接转换成二进制就算完了,我们要充分利用霍夫曼编码的方法进一步压缩。如果有不了解霍夫曼的,其实就是简单地重排数据编码方式,出现频率高的用更少的比特编码,频率低的用更多的来编码,并最终得到一个总比特数更短的二进制码。如果我们不用霍夫曼,可以看到上面的分类单元对于AC量每个有8位,对于DC也有4位。霍夫曼编码后,其长度变成2至十几位不等。

JPEG默认就是启用霍夫曼的,其标准中也有个推荐的霍夫曼码表。但是和量化表一样,你也可以自定义——事实上,有人就发现,Photoshop就有一套自己单独搞的霍夫曼码表,而且根据JPEG的质量不同还稍有不同,以求达到最佳的压缩效果。另外,DC和AC、Luma和Chroma都可以分别使用不同的霍夫曼表,也就是说一般会有四张霍夫曼表。该表显然也会和量化表一样存在header中,否则无法解码。

不过,最优的霍夫曼(在JPEG文件结构的限制内。有研究称JPEG的霍夫曼从设计上就不可达到最优)自然是对于每个JPEG单独统计每个分类单元出现频率然后构建霍夫曼表了:——这也就是一般软件保存JPEG时的“优化霍夫曼”或者“Optimize”的意思了。不过很显然,这样做会要求先对所有MCU进行一遍扫描,自然会降低编码速度(也需要更大的buffer,这点在设计encoder的时候需要注意)。

最后,这些纯二进制的数据会按照MCU1-Y-DC、MCU1-Y-AC、MCU1-Cb-DC、MCU1-Cb-AC、MCU1-Cr-DC、MCU1-Cr-AC、MCU2-Y-DC……的顺序拼在一起。整个数据块必须结束在整字节里,最后不足的部分补1。另外一个特殊之处在于,如果数据中某个字节出现了0xFF(1111 1111),为了防止和JPEG的marker(标示各个组成部分开始的标示)混淆(全部以x0FF+非0字节组成),会加入padding 0来改写成0xFF00。

讲到现在,终于把普通的baseline、sequential的JPEG编码原理讲完了。

Baseline?Sequential?

插播一段:关于“baseline”的定义,其实非常含糊。根据ITU的standard的术语表,baseline其实是和“extended”相对应的,而不是progressive——sequential才是。“extended”是指每次Scan(下述)可以有高达8张霍夫曼表、并且每个component的可以是12位等等的扩展格式(极为罕见)。但是,在统一标准中后文中又出现了baseline和progressive的相对应……总而言之,连ITU自己的文档里术语都不是很严谨。在民间使用时,baseline多用来和progressive相对(虽然sequential更合适一点),这点要搞明白。

步进(Progressive)JPEG

我们还完全没提到最重要的步进(progressive)到底是怎么回事。不过看到上一段我们应该能想到,由于DCT的特性,高低频的数据都已经分离出来了。如果我们在存储时并不是按照完全按照MCU的顺序,而是先把DC和一些序号较小的AC分量挑出来先存储,这样加载的时候不就可以做到从模糊到清晰的效果了吗?没错,这就是progressive JPEG的基本原理了。而且,这样做我们只是调整了同样数据的位置而已,理论上并不会增加体积。

在具体实现上,得先讲一下Scan的概念。JPEG中经过DCT和量化之后的那堆系数(就是那些8×8矩阵),可以分为多成多个Scan来保存——每个Scan中只分配、存储部分数据。具体的分配方式,可以分为三种:

  1. 按照component分开。还记得上面提过的interleaved的概念吗?一般的JPEG,是将三个components(Y、Cb、Cr)全部混在一起编码的。你也可以全部分开——每个Scan只处理一个component。
  2. 按照8×8 block中的序列号(依然是蛇形顺序)分开。
  3. 按照编码后(二进制)的强度量的比特位置分开。

其中,后两个又称作“progression”,采用这种方式来分scan的JPEG就是progressive scan了。

Spectral selection

采用第2种方式的,叫做“Spectral selection”。例如,我们可以单独压缩0(DC),然后是AC1-AC6,然后剩下的AC7-63再一起。这样在传输图像时,DC部分在第一个scan就会扫描到,后面的慢慢读取。不过具体分配方式并不是任意的,有以下规定(ITU T.81 G.1.1.1.1):1)DC和AC必须分开;2)只有DC的Scan可以是interleaved(包含多个components,也就是色度亮度一起编码),AC的Scan必须是只含有一个component。所以,更现实的分配方式可以是

# Interleaved DC scan for Y,Cb,Cr:
0,1,2: 0-0, 0, 0 ;
# AC scans:
0: 1-2, 0, 0 ; # First two Y AC coefficients
0: 3-5, 0, 0 ; # Three more
1: 1-63, 0, 0 ; # All AC coefficients for Cb
2: 1-63, 0, 0 ; # All AC coefficients for Cr
0: 6-9, 0, 0 ; # More Y coefficients
0: 10-63, 0, 0 ; # Remaining Y coefficients

这个范例引用自 libjpeg-turbo的wizard.txt,这也是cjpeg.exe支持的scan file的格式。其中的0,0部分,下面马上提到。

Successive approximation

采用第3种的,叫做“Successive approximation”(有的地方叫做Successive renement,而把两者共用叫做Successive approximation……没错就是这么混乱)——所谓按照比特位置,就是把每个分量系数(coefficient)的二进制强度(或者称值)按照高位低位分开。例如,假设我们的值都是8 bit,我们可以在第一次scan只传输前7位,最后一次scan再把最低一位(Least significant bit,LSB)传输——可想而知,其效果就是图片精度逐渐变高了。上图的0,0部分的意思就是没有successive approximation。

这么说可能还是觉得有点迷茫,ITU T.81标准中的这张图可能是最直观的了:

itu-t81 124

(From ITU T.81)

Spectral selection和Successive approximation可以结合起来一起用。例如,cjpeg默认的-progressive采用以下这样的scan file:

# Initial DC scan for Y,Cb,Cr (lowest bit not sent)
0,1,2: 0-0, 0, 1 ;
# First AC scan: send first 5 Y AC coefficients, minus 2 lowest bits:
0: 1-5, 0, 2 ;
# Send all Cr,Cb AC coefficients, minus lowest bit:
# (chroma data is usually too small to be worth subdividing further;
# but note we send Cr first since eye is least sensitive to Cb)
2: 1-63, 0, 1 ;
1: 1-63, 0, 1 ;
# Send remaining Y AC coefficients, minus 2 lowest bits:
0: 6-63, 0, 2 ;
# Send next-to-lowest bit of all Y AC coefficients:
0: 1-63, 2, 1 ;
# At this point we’ve sent all but the lowest bit of all coefficients.
# Send lowest bit of DC coefficients
0,1,2: 0-0, 1, 0 ;
# Send lowest bit of AC coefficients
2: 1-63, 1, 0 ;
1: 1-63, 1, 0 ;
# Y AC lowest bit scan is last; it’s usually the largest scan
0: 1-63, 1, 0 ;

可以看出,整个过程有高达10个Scan。第一个Scan输送所有component的DC分量(除了最后一个bit,即LSB);然后输送Y通道的前5个AC分量,不过不包含最后两个bit;接下来是Chrma的所有AC分量,但是不包含最后一个bit(如上所述,在progressive中除了DC分量其他的都不允许interleaving,所以分了两次scan;而且,这里选择了先scan了Cr,因为人眼对Cb最不敏感)。再接下来是Y通道的后面6-63个AC分量,依然不包含最后两个bit。再下来是Y通道所有AC分量(1-63)的倒数第二个bit。

最后4个scan就是重复上面的顺序将所有的通道、分量的最后一个bit给传送了。

其他细节

至于在具体实现方式上,Spectral selection倒是蛮好理解的,每处理到某个MCU只要只处理其中一部分分量就是了。但是由于每次Scan现在只含有少数几个分量,对于编号比较大的的高频(例如:6-63)可能会有大量block完全是0。为了进一步节省字节,在EOB的基础上,我们又重新定义了一组EOBn控制符,来表示之后n个block都是空(纯0)的。当然这些控制符也会被霍夫曼编码了,就像EOB和ZLF一样。

Successive approximation说起来就有点复杂了。如果上面的scan file有仔细看,就会发现每个部分(DC或者每个component的AC)第一次Scan发送的比特数不一,但是之后每次Scan都只输送一个bit。这也是JPEG的规定。对于DC,很容易理解:假设我们某个MCU的某个component是7好了,那么二进制就是111,三位。如果我们分成两次Scan,第一次只传送前两位——也就是11,那么很显然,其“分类单元”应该是0x02;在第二次Scan的时候,因为很显然只有一位,那么分类单元那部分就可以省略掉了,直接把最后的bit补齐即可。

结语

那么,回到最开头的问题,为什么用progressive模式,就会体积减小呢?这里我没有一个确定的答案(…),但是可以看到,和DC/AC全部interleave在一起的Sequential模式相比,其最大的优势是把各个MCU的相似的分量都排在了一起;可以想到,这么做绝对有利于含有预测性质的游程编码,乃至后面的霍夫曼编码。另外,单纯把数据分成好几份(好几个Scan)然后设定不同的霍夫曼表这件事本身可能就能提升不少效率。

 

魔力女管家音乐歌词数据库搭建完成,顺便说说Sphinx

就像上一篇文章许诺的,我费了三天时间,把魔力女管家歌词库给搭出来了!

先上地址:http://fireattack.github.io/mahoromaticdb/ 在Github因为侵权把我的网站日掉之前,暂时就挂在这里了。

包括了所有的CD的简要信息,以及所有歌曲信息歌词(大部分含翻译)。所有的CD我都用当年收藏的Booklet精心制作了1000 px的封面(除了少数几个找不到BK的),欢迎使用。哦里面在最后还随意地包含了两张同人CD(C60、C61发售)的信息,其中第一张网上应该是能找到的,意外地非常好听,强烈推荐。

搭建过程中才发现我之前的文档写的有多烂。有好多后来才找到歌词的歌没包括就算了,格式也是一团糟,错字、标点符号不统一的问题比比皆是。看来我当年的强迫症要轻得多啊!我尽量把标点符号统一为:跟日文用全角,跟英文用半角,但是连用符号(例如:!?)用半角,波浪号的副标题前空一格(但是魔力女管家第二季的标题则不空),括号统一用全角。艺术家名义统一用角色(声优)的格式,但是少数早期CD直接单用声优名字。

呃好吧我承认这并不是特别统一…因为我有个更严重的强迫症,叫做“名从主人”…我一般会尽量遵照BK上的写法,因此牺牲一些统一度。

接下来的地方讲讲搭建网站过程中的一些值得记录的东西吧。

这次用的技术是Sphinx——一个用Python写成的Doc建站软件。之所以没有用Github Pages支持的Jekyll,主要是那个是用Ruby写的,不想去接触。不过后来才发现使用过程中99%都只是在和reStructuredText这玩意打交道,和后端的语言一点关系都没有,早知道就用Jekyll了——毕竟那个是用更简单的Markdown的来生成HTML的。

果然还是先说reStructuredText这种标记语言吧,毕竟大部分时间都耗在和他打交道了。一言以蔽之,这玩意的语法非常的反直觉和不灵活。不过用了三天之后,也算是慢慢熟悉了。reStructuredText的设计思想就是非常注重可读性:基本上而言,源代码就已经在ascii的层面上“格式化”了。例如其最反人类的设定——表格,正常来讲你需要手动用各种线把框框画出来(见下)!

+------------------------+------------+----------+----------+
| Header row, column 1   | Header 2   | Header 3 | Header 4 |
| (header rows optional) |            |          |          |
+========================+============+==========+==========+
| body row 1, column 1   | column 2   | column 3 | column 4 |
+------------------------+------------+----------+----------+
| body row 2             | Cells may span columns.          |
+------------------------+------------+---------------------+
| body row 3             | Cells may  | - Table cells       |
+------------------------+ span rows. | - contain           |
| body row 4             |            | - body elements.    |
+------------------------+------------+---------------------+

还好,还有csv table可以用,否则这真的要死人的(不过这里有个table ganerator可以用)。

除了最基本的一些标记(例如,粗体、斜体啥的,不过注意和Markdown不同),reStructuredText核心元素是directive和role这俩东西。前者是一种特定格式/结构的元素,一般“成块”出现;一般格式是:

.. directivename:: argument ...
   :option: value

   Content of the directive.

在第一行调用directive的名字,然后在第二行起带缩进写设置,然后空一行带缩进(缩进必须和前面保持一致)写被格式化的内容(有些时候则没有内容,比如图片啥的)。这个东西可以用来实现插入目录、图片、目标(锚点)、给内容加class等功能。role和directive类似,但是一般是用于行内(inline)mark一些内容,例如上面提到的粗体、斜体啥的,本质上也是一种预定义好的role。你也可以自定义role。

那么就大致按照我写站的时间顺序来讲吧,没啥逻辑关系。

在建立了你的Sphinx网站之后(推荐使用官方带的Sphinx-quickstart),第一件事在index(链接是到rst源代码,下同)页面中把站点的目录放在上面。这个倒是蛮简单,用Sphinx自带的toctree这个directive就行。在目录里你可以输入想包括的页面名称,以及目录深度。我设想的结构是所有CD信息在一个页面(cdlist.rst),而歌曲因为歌词较长,则每首歌分割为一个单独的页面放到里子目录songs/里。

这里就遇到了第一个问题:如果你用songs/kaerimichi的方式添加页面到toctree中,他会和cdlist都是平级——而不是处于一个Songs的一级目录之下。稍微研究了一下发现要这么做:在songs/子目录下先建立一个index.rst,然后在该rst中再建立一个toctree(下称toctree2),包含所有songs/下的页面。因为toctree总是从当前目录开始查询的,所以对于toctree2,你只需要罗列所有歌曲页面的名字就行,无需加songs/前缀。当然因为歌曲太多了,我们利用:glob:这个参数,就可以用通配符*来匹配所有页面了。回到根目录index的toctree,我们也只需要包含songs就可以了,会自动把toctree2里面的项显示为二级项(结果)。

正式写页面,对于CD list,我需要在最上面附一个(本页面内的)目录。这个用带的contents directive就能轻松做到。但是默认会加一个很多余的根目录节点,用:local:参数去掉它。搞定后,这个目录就会列出所有的section了。加一个section很简单,只要在一行文字下面加一堆“-”、“=”之类的就行。如果要多级section则需要分别套用不同的符号,不过我这里只有一层所以无所谓。一般而言,每个section会自动生成锚点,上面加的目录就能跳转。

因为页面上某些文字需要是小字,而且这些文字混在正文中(inline),我们需要一个role。当你给内容指定了role之后,生成html会自动指定对应的class名,因此配合CSS就可以实现想要的样式。要定义一个role,要先在文档某处(一般是最开头,我不确定在别的地方可不可以)写:

.. role:: smallfont

之后(注意:每个用到:smallfont:的rst都必须重新写一遍这个…),你就能用:smallfont:`your content`来标记你的内容了。不过这里有个限制:这一段代码的前后必须是非“word”的东西,也就是说中间一般得有一个空格。如果你不想要你的普通内容和小字内容之间有空格?需要加反斜杠来消掉空格。即:

your normal content\ :smallfont:`your small font content without space inbetween`

OK,那现在在CSS里写:

.smallfont{
 font-size: 80%;
 color: grey;
}

就行了。不过,怎么把自定义的CSS包含在你生成的网站呢?这里有好几个办法

  1. conf.py中加上
    def setup(app):
        app.add_stylesheet('custom.css')  # may also be an URL

    (你的custom.css应该在_static/目录下)

  2. conf.py中加上(注意这里又变成从根目录起了…下同)
    html_context = {
     'css_files': ['_static/custom.css'],
    }
  3. 先把你模板里的layout.html拷贝到目录下的_templates里(当然,保证你没删掉conf.py里的templates_path = ['_templates']),然后找地方加一行
    {% set css_files = ['_static/custom.css'] %}

    (在SO看到的是

    {% set css_files = css_files + ['_static/custom.css'] %}

    但似乎使用上并没有区别。)

  4. 还是上面的说的layout.html,直接强行加
    {%- block extrahead %}
      		&lt;link rel="stylesheet" href="{{ pathto('_static/custom.css', 1) }}" type="text/css" /&gt;
    {% endblock %}
    
  5. 最暴力的方法,找到你模板的CSS文件,然后修改之;或者修改之后放在_static/里(原因见下面)。

对于我用的alabaster模板,默认的layout.html已经包含了上面的选择4,所以我只需要把custom.css放在_static/下即可。哦这里顺便说句,你放在_static/下的文件默认全部都会复制到你build出来的html里(不管用不用得到),你可以利用这个来覆盖模板里的东西(如上面的5所述)——不过这里注意是覆盖,不是添加。另外,图片之类的resource就别往里放了,否则会复制两遍(因为所有引用过的图片会自动被Sphinx复制在build目录的_images/目录里),占地方。

搞定了“小字体”这个样式之后,在每个歌曲的页面,我还需要引入两种新的样式:日文和中文,分别用来标记不同语种的歌词,从而实现更好的字体显示效果。

这里因为是成块的内容,我们就不用role了,用一个directive:class。如名字所示,其功能和role类似,也是给一块内容标记class。原始的reStructuredText直接用class就行,但是Sphinx是为Python文档开发的,默认把class给定义成一个role了,所以需要改用rst-class

.. rst-class:: ja

	| まなざし そっと ひとつ
	| 誰にもみつからぬように
	| ふんわり時間だけが
	| 流れては消えてく

这里可以看到,argument(双冒号+空格后面的)自然是想要的class名称。因为没有选项(options),所以内容就从第三行(空一行)开始。至于pipe符号(|)这里的目的是强制换行。另外注意,所有的内容必须有一致缩进(具体多少无所谓,反正不会显示成缩进)——缩进结束就退出了这个rst-class了。而相对地,在普通正文中的缩进就是正常的缩进,而且你加几个空格都会如实反映。当然,你还得去你的CSS文件里定义.ja,这里就不赘述。

在写歌曲页面的时候,我遇到一个非常蛋疼的锚点问题。一般而言,锚点在加section/heading时是自动生成的,但是如果你不想开新的section呢?方法是在文档中加入这么一行:

.. _targetname:

至于引用(指向)目标时,Sphinx推荐的用法是用:ref:这个role:即形如:ref:`targetname`这样来引用。但是注意!一般而言target是配合section来用的,所以会自动成section的名字。但是现在我们是在正文中随便添加的,我们必须显式指定他的名字::ref:`Display Name <targetname>`才行。

但是这里有个问题——在Sphinx里,所有的target和ref都是全局的,跨文件的。所以,我每个文件里的target还不能一样,比如如果我每个文件里都有个.. _ja:,用:ref:`Display Name <targetname>`会不知道飞到哪个文件的_ja锚点里去。

研究了半天,发现只能用reStructuredText自带的引用方式——`targetname`_因为这个只适用于本文件,这样即使我每个文件都有个重名的锚点,也只会正确跳转到本文件内的。不过这个有个缺点,不支持Display name和targetname不一样(就是上面带尖括号的用法),所以我的targetname必须就是我想显示的文字。还好,似乎支持中文字符和符号,我用了“[中文]”当锚点名称也没事儿。最终效果

那么最后需要的功能就是给CD曲目列表里的对应歌曲添加链接了。因为是跨文档引用,所以推荐的方法是给每个文档的标题加上个target,然后用:ref:;但是我嫌麻烦,直接用另外一个role,:doc:做。方法基本一样,直接输入:doc:`filename`就行了——显示的文字自动从对应文档的标题提取。不过这里我也并没有直接这么做,因为Sphinx带了个非常好用的role,叫:any::用它可以智能地自动寻找最接近的reference,可以是:ref:,可以是:doc:。在用:any:之前,我们更可以把它指定为“default role”——在conf.py里添加:

default_role = 'any'

这样,当你使用单个撇括起来时(例如:`songs/kaerimichi`),会自动调用:any:这个role。这里,因为这是文档名,又会进而自动调用:doc:

到这里,基本在reStructuredText里遇到的困难都说完了。在Sphinx这边,我对默认的alabaster模板也没怎么改,但是有一点要注意:如果要用这个模板的完整功能,要修改sidebar为

html_sidebars = {
 '**': [
 'about.html',
 'navigation.html',
 'relations.html',
 'searchbox.html',
 'donate.html',
 ]
}

才行,因为模板自带了一些sidebar并没有包括在默认的设置中。模板带的选择在conf.py里修改html_theme_options,基本很好理解,我就改了logo和字体。

Sphinx有一个自带的“basic”模板,还有一些JS和CSS,基本所有的模板都有inherit。但是那个JS(doctools.js)有个问题(Chrome only,我已经汇报到Sphinx dev team),重现方法如下:

  1. 点一个带锚点的地址
  2. 滚动一些(即,你已经不再在锚点的原始位置)
  3. 点一个(非本页内的)链接,跳转到了其他页面
  4. 点“后退键”

正常来讲,点了后退之后,会后退到之前页面的之前位置。但是bug就是,后退之后会强制再读取一次锚点位置,然后跳转过去,而非你之前的阅读位置。你可以在Python 3的官方文档页面重现此bug,因为他用的就是Sphinx。相反Sphinx自己的文档页面则不会,因为他用的是旧版的doctools.js文件。Firefox下无此bug。

我研究了半天,发现是该JS文件中的以下函数:

/**
* workaround a firefox stupidity
* see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075
*/
fixFirefoxAnchorBug : function() {
if (document.location.hash)
window.setTimeout(function() {
document.location.href += '';
}, 10);
},

导致的。讽刺地是,该函数的目的是为了workaround Firefox的一个bug——Firefox对于HTML5新增的<section>锚点tag支持不好——才加的。本来函数有一个判断是只对Firefox有效,但是由于JQuery移除了$.browser,这里被改成了对所有浏览器有效,从而导致了上面所述的副作用。因为那个Firefox的bug对一般应用并没有什么影响(一般应用的锚点都是靠id="xxx"来搞的,并不会用到<section>),所以我们这里直接删掉就好。删除的方式则是复制一份这个JS文件修改,然后放到_static/文件夹内。这样,每次build,会自动覆盖。注意复制的时候,别复制成了原始模板文件夹里那个doctools.js_t了——那个是个JS“模板”,中间有些参数还没生成的。正确的方法是先build一遍,然后从html的目录里复制个原始版的JS。

还有一点要注意,用sphinx-build来build HTML文件的时候,有的时候并不会刷新改刷新的文件(尤其是sidebar之类的),所以隔一段时间最好把_build/目录全删了然后重新build。当然你也可以用-E选项,不过一般还是别强制了,因为要慢许多。

往Github Host的时候,需要加个.nojekyll文件来禁用GitHub Pages自带的翻译引擎,否则会有问题。但是这个文件只有加在根目录才有效,而我是把整个网站放到mahoromaticdb/子目录下的(因为GitHub一个用户好像就支持一个站),所以一开始迷惑了一阵为什么不好使。

从“アニメソングの歌詞ならここにおまかせ?”关闭说起

警告:本文大量个人的无病呻吟。

无意间点开尘封多年的书签,震惊地发现,アニメソングの歌詞ならここにおまかせ?(网址:http://www.jtw.zaq.ne.jp/animesong/)这网站居然关闭了。

一番搜索,可以知道关闭的时间点是今年一月底,原因也没有多说,作者就是一句“感谢长久以来的支持”。

C4Eq6cuUoAA8Ssx.png large.png

(Via:Twitter

不过,从页面报错信息来看,直接原因是作者用来Host网站的服务(J:COM NET加入者向けホームページサービス)停止运营了。当然,如果作者真的想继续,换个服务商并不是不可能,但是果然有些时候事情就是欠个导火索。我喜欢吹日本人或者外国人的“根性”,能不辞辛苦、不计报酬(准确地说是倒贴钱)地维护这类爱好网站,但是果然一切都是有极限的。毕竟到后来大多都是惯性使然,突然弦可能就断掉了。

这件事我居然过了一个多月才发现,可以说是过去的我所不可想象的:没错,这网站应该是我正好10年前那会,不夸张地说上的最多的网站。说起这话题,似乎应该先从网站介绍起。如名字所示,该站是一个收集动画歌词的网站。作者按照作品分类,收集了大量动画歌曲的歌词,并按时(一般一周一两次)更新最新出的歌曲的歌词。另外值得一提的是作者是有JASRAC的牌照的,所以是完全合法运营。当然,这些歌词多数在无数日本其他歌词网站都能找到,但是作为一个只更新动画歌词的网站,有其独有的优越之处,这得从我当年的“工作”说起。

06年那会大概是我刚入宅的时候,相信很多人都知道了,契机就是一部《魔力女管家》。我当时着迷程度非常深,在网络上疯狂搜刮和魔力女管家有关的一切资源,其中自然也不会放过音乐。魔力女管家的BGM是非常优秀的(作曲:増田俊郎),歌曲也不错。主打的自然是麻幌的配音川澄绫子,唱了两部的OP以及一些插曲不说,更在各种相关CD里唱了大量歌曲,甚至单独出了一张专辑。几个配角也都有歌曲,其中男主的三个青梅竹马(?)还组成了一个组合叫“とりおまてぃっく”(很显然名字捏他作品标题「まほろまてぃっく」),包办了所有的ED,也出过张专辑。当年这些歌我翻来覆去也算是听到烂了,更是我Anison的入门。

既然有歌曲,自然少不了歌词。我在搜索魔力女管家的歌词的时候无意发现了一个叫“漫网”的论坛,上面就有一个区叫做动漫歌词区。当时这区还挺活跃的。里面的好多版主我几年后还经常在ASTOST见到。虽然不能记起全部,但是有位叫紫亚的妹子倒是蛮深刻,还玩翻唱来着。

魔力女管家的歌曲歌词慢慢收集齐了,但是在这过程中,我逐渐对动漫歌曲歌词这个东西本身感了兴趣。于是我开始经常在这网站上发帖。不过歌词从哪里来呢?这就不得不提到标题这个网站了。作为一个只更新动漫歌词的网站,自然比去那种什么都更的网站方便,而且最棒的地方在有这网站还有change log,每次更新了哪些一目了然,只要原样照搬就是了。虽然网站有防复制,但是实在是很弱一般开源代码复制就是了。当然还有一些别的网站,比如当年比较有名的月之舞歌词站(一个国产的日音歌词收藏站,早就挂掉了)等等,但是还数“アニメソングの歌詞ならここにおまかせ?”用得最多。

不过,虽然我最早知道的是漫网,也一直有在混,我真正驻扎下去的则是另外一个论坛:动漫花园之音乐花园。动漫花园本身的名气相信不用多说了,即使是到今天大部分人也知道,因为其BT站的缘故。不过最早的动漫花园是从字幕组起家(和PPG算是齐名),然后有论坛和tracker的。动漫花园论坛当年的有意思之处在于,其音乐分区是一个独立的网站,名字叫音乐花园,管理基本也独立。当时上面聚集了一些动漫音乐或者日音牛人,很多现在都还在AO混,比如牛叔(nalanchen,即现在“MGRT音花雪月”的组长,说来MGRT正是Music Garden RIP Team的意思),jedi_vs_sith等,不过我和这些大大都不熟,算是单方面认识。音乐花园当年还算是一个满活跃的网站,还经常搞些音乐精选集什么的:

Cover_Inside.jpg
当年的音乐精选集  vol.3的伪·封面

因为音乐花园人比起漫网(哦顺便说句,当年漫网主站是个卖动漫资源的网站)比较少的缘故,“抢发”歌词也不剧烈,我逐渐在这边的歌词区(名字叫词苑)活跃起来。后来也记不清是自己主动申请还是被邀请,总之稀里糊涂地当了歌词区的版主。

在音乐花园这边打交道比较多的是词苑的几个老版主,冰河的海,草上飞校长,以及最老资格的domonick。一些具体事情就不详细说了,我就记得当初年纪小,为了歌词翻译脸皮超级厚的疯狂追着屁股求助,而草上飞校长总是非常客气地帮我翻译,实在是感激不尽。dominick基本算是歌词区的奠基人了,早期的大量歌词、以及目录的整理都是他涵盖的,还请Ayumu(不知道是谁,可能是上古巨神)制作了两款相关的软件,转码用的Sapphire和注音用的Ruby:

QQ截图20170310005223

QQ截图20170310005218

其他的著名人士还有Shion(很多ID,其他的还有“水色”等),她在漫网和DMHY这边都有活跃,是个非常细心的人;Frank543,他后来制作了一个非常好用的跨站歌词搜索引擎Lyrics Get(自带破解防复制),现在还可以用哦。

至于我自己嘛,只做过一些微小的工作,多数就是搬运歌词,其中自己比较感兴趣的几部作品,比如当时迷的爱马仕系列整理的稍微用了些心。另外很多歌词尤其是很多角色歌,网上是没有的,需要自己从booklet里抄写下来。另外还用自己三脚猫的C#功夫写了一两个自动格式化发帖的辅助小工具之类的。

就这么过了两三年,国内早期的动漫论坛开始慢慢走向一个衰败期。音乐花园后来也被动漫花园吸收了回去,成为了里面的一个版块。接下来就是DMHY的分裂风波了。我不知道还有多少人今天还记得,总而言之就是站长“很闲”和一些会员的运营思路没走到一起。过程很复杂,形成了“站长派”和“元老派”,当年作为一个论坛的积极参与者,自然队站在了“元老”这边,好像还参与过一些撕逼,现在想想简直幼稚得可怕。经过一番周折之后(中间有段时间甚至有俩动漫花园论坛),最后的妥协是站长继续经营资源网(同时在这前后建立了u2)出走,其他论坛元老保留了论坛。后面的结局自然都知道了,资源网活得好好的,论坛死的比原来更快。讽刺地是,在几年之后的今天,论坛(的尸体)又回归了很闲的手下:https://bbs.dmhy.org/ ,我当年复出无数心血的词苑的时间也定格在了2010年。

也大概是因为这个事情,就像标题网站的站长一样,我逐渐地也倦怠了,词苑的工作慢慢就荒废了。哦其实中间还在ASTOST混过一段,当过那边的歌词区的见习版主,不过没坚持多久就是了(然而就几个月还很傻地和那边的小圈子对喷过一次,年轻真好)。

好了,莫名其妙的追忆就到此为止了。作为祭奠,我把最初的起点——我整理的魔力女管家歌词传到GitHub了。没错你没看错,所有的歌词页面的编码都是错的(Firefox的话手动选成GB2312就行。Chrome把编码选择阉割了我也没办法呀哈哈哈),毕竟这是我N年前用Word文档(原始文档/Google Doc版,更全一些,还有所有CD的曲目信息)转的……闲的话再搞个正常点的网页吧。

ReplayGain在UPnP media server中的应用和补遗

UPnP media server是个挺有用的东西。在PC端配合一个功能非常全面的foobar2000插件——foo_upnp,安卓端配合foobar2000 Mobile或是BubbleUPnP,即可轻松实现包括但不限:用手机播放电脑里的媒体库,用手机当电脑的音响用(呃延迟没测过,估计只够听歌),用手机控制电脑播放器,以及前面的全部反过来控制等功能。当然,最理想的情况是把media server设在诸如NAS之类的设备上,但是就我个人来说,最常用的应用场景是躺在床上耳机插手机听电脑的媒体库/播放列表。

既然前一段时间折腾了那么就RG,这也得用上不是。让我们先仔细研究一下foobar2000那边的server的profile设置。

qq%e5%9b%be%e7%89%8720170220192532

另外为了便于测试,将Basic Settings里设成始终使用默认profile和关闭增加兼容性的额外流:

qq%e6%88%aa%e5%9b%be20170220193855

可以看到大抵来说,有这么几种streaming的方案:源文件直出,转码为MP3,解码为WAV。

直出最好理解,就是在http上host对应的文件而已。直出可以保有原始文件的所有metatag,自然也就包括RG信息;这样,在移动端使用支持RG的客户端时(这里就是foobar2000 mobile了),就会读取到信息进行响度调整。

但是直出的局限性非常大。foobar2000 mobile支持的格式非常有限。TAK这种就不用说了,自然也不支持任何音频文件+cue的类型。不过倒也不会播不了,只是sever会自动无条件帮你转成WAV。最吊诡的是,像FLAC这种明明是支持的格式(即,你把歌曲拷贝到手机里是能放的),有时候会在foobar2k mobile那边出现播放列表里完全看不到的现象(不能稳定重现。与之相对,第三方的BubbleUPnP则很稳定不会有这个问题)。

既然直出这条路不行,那就只能转码或者解码了。介于局域网带宽对于音频来说完全不是问题,自然选择音质更好的WAV,也避免了二次压缩的问题。不过,转码/解码之后有一个问题,就是RG信息完全丢失。哪怕是转码成MP3也是一样。还好,在下面那个Audio processing里,和Converter(转码器)一样,可以设置转码/解码时的处理。注意这里既然叫processing那自然是硬编码进去,而不是靠RG信息。所以foobar2000 mobile端会识别成无RG信息,调整那边相应的RG设置时要注意(我则是完全关闭)。

哦从图中可以看到,你可以设置规则对不同文件类型设置不同的转码规则(例如有些转,有些直出等等)。但是没啥卵用,因为你一旦开启了processing,所有的都会被强制转一遍mp3/WAV,所以还是死了这条心。

这里又是和上一篇文章中提到的普通用电脑回放时一样,面对一个抉择:是选择全部降低到和RG目标响度一样的响度(即:较低的响度),还是全部增益到和“新歌”一致的一个比较高的响度。这里还是选择了前者,因为毕竟那样对音质的保存是最完整的,而且主要用耳机听歌的话也不用担心有输出不足的问题。具体来说,就是在Processing里开启apply RG,同时对无RG信息的歌曲(主要是响度大的新歌)加个-9.5 dB的pre-amp。如果还不放心可以再加个Advanced limiter(虽然99%的情况根本不会有任何区别),但是介于Advanced limiter目前有个会把1缩成0.9999的bug,还是别了罢。

当然这种选择下就会有一个问题,那就是和我手机里那些已经转换好的、响度和新歌一个水平的音频文件有冲突。而且因为两者都是无RG信息,也无法靠调整手机端的RG设置中pre-amp来弥补。不过还好我一般其他场合听歌是用Google Music而不是foobar2000 mobile,以后就用fb2k mobile专门听媒体库就是。

多说一句,客户端那边选用foobar2000 mobile而不是BubbleUPnP的原因倒不是fb2k mobile支持RG(毕竟在我的最终配置下也用不到),主要还是foobar2000 mobile支持手机端的last.fm统计插件,虽然BubbleUPnP的界面大概好一万倍(电脑的foobar我觉得还行了,手机那个真的是丑的惨绝人寰)。另外,BubbleUPnP有个很烦的地方就是每次我修改服务器端(我电脑)的UPnP设置那边就必须会自动切断连接,也就是退出所有的远程playlist啥的,我还得重新选,在测试的时候尤其烦。而foobar2000 mobile则是另外一个极端……不但不会断,他还会缓存一部分媒体的地址(其实就是个URL,可以在电脑端或手机端的console里看到),也就是说我切换了设置之后有时候得在那边强制退出一次来让他刷新URL(服务器端这边即使你修改了设置,老设置的URL实际上还是并没有禁用的,依然有效)和播放列表。

screenshot_20170220-193326
客户端(foobar2000 mobile)读取流媒体的log范例

UPnP媒体库播放的响度规格化这事儿到此也就告一段落了。不过,我又回头去想在移动设备播放歌曲的workflow能否有可优化的地方。

如前文所述,我目前在移动设备采用的是“新歌不变,老歌先RG再+9.5dB”的方案,原因是因为这样兼容性最好,对没有RG支持的播放器也能完美播放,而且不会有输出太低的问题(我手机插车上听,即使在这种设置下都几乎要开到最大音量才行了,如果全部都低9.5dB那可调整的余地就太小了)。当然最大的问题就是+9.5dB导致的超过full scale引起的削波问题。

解决方案上次也说过,就是在转换时再用DSP加一层Advanced limiter,把超出的部分动态调整到1以下。不过由于犹豫会对音乐的完美呈现造成影响(毕竟还是部分压缩了动态范围)我之前一直没用。不过最近遇到这么一首歌,一下子就听出削波的问题了(下载链接):

废话一下,歌曲是菊池桃子的「卒業 -GRADUATION-」,个人的五星歌曲。算是无数好听的叫“卒业”的歌曲之一(其他的还有尾崎豊的,斉藤由貴等等)。菊池桃子天使般的声线在这歌里得到完美的体现。咳跑题了。

这歌的问题在于动态范围极大,在我的传统转换设置下,自然会溢出full scale很多,事实上,其peak达到了1.40(+2.93 dBFS)之高。从1:00左右开始,就会疯狂爆音,用手机+耳机听时最明显。因此,Advanced limiter就是非常必要的了。我对比了下开启Advanced limiter的版本和原版,完全听不出区别(毕竟音量大于1的部分只是极小的spike,频段可能更不是人耳敏感的部分),可见这个limiter的效果比起一般的动态压缩要好得多,也解除了我之前的一大顾虑。

不过在研究过程中我发现一个有趣的事实:AAC(m4a)这个格式居然可以保存大于1的数值!也就是说,我上面发这个+过9.5dB的音频文件,其实并没有损失任何信息或者削波——超过FS的1.4的peak也完整地记录在了音频中。这说明了什么呢?如果你的播放流程在输送到DAC之前有足够的衰减处理(例如软件中的波形层面的音量降低,但包含音响上的Analog的音量旋钮),又或者你的DAC/Analog设备足够专业,留有headroom支持超过1的波形,其实并不会产生削波/爆音。对应到电脑,foobar2000里的volume就是个不错的例子(但是别忘了上文提到的Win7的自带的limiter适用过早的bug,所以避免);对应手机端,根据我的观察,至少安卓系统的音量是不行的(不过安卓的音频处理一直都臭名昭著):如果仅仅靠调整系统的媒体音量,该爆音的还是爆(不清楚安卓的系统音量具体原理是数字上衰减波形还是模拟级)。不过,foobar2000 mobile中有个单独的volume设置,通过那个,或者里面带的RG设置的pre-amp,可以做到同响度情况下(即先用foobar2000 mobile的音量降低几个dB,再在系统音量里稍微开大一点达到同样响度)完美无爆音播放上面那个m4a文件。

不过这个用起来其实也有颇多不便:第一个不便是foobar2000 mobile的音量设置……他有bug。每次切歌,音量就会回归到0dB,即使界面里显示的还是你的设置。另外还是上面说的问题了,如果你降了几个dB,那输出不够这老问题又来了。所以,我最终还是经过权衡选择了转换时选择高响度水平+Advanced limiter的方案。当然还有第三个方案:转换时不加Advanced limiter(利用m4a的特性保留超过1的数值),但在播放时靠foobar2000 mobile自带的DSP即时加。这样可以最大限度保存动态范围,以后这些mp4拷贝到电脑上听时还可以还原。我现在还在想要不要全面转成这样——因为如果这么做,就表示了我就锁死只能用foobar2000 mobile当手机播放器了,这方面有点犹豫。

前面说了AAC(m4a)可以保存高于full scale的数值,那相对地WAV、FLAC和APE就不行了,如果你转换过程中增益到超过1,最终出来的文件会被削波到1。当然FLAC/APE之类的是为了完美呈现WAV而故意这么设置的吧,大概。MP3则比较奇怪,还是上面那个歌,转换之后会出来个peak为1.075的玩意…大概和MP3算法有关吧。另外每个的平均响度也会细微的差别,理论上来讲很自然AAC会高一些(毕竟没削波),虽然差别很小就是了。

qq%e6%88%aa%e5%9b%be20170221233307
利用RG扫描来查看peak和平均响度。因为Gain是负值,所以绝对值越大说明响度越大。音频全部通过原始wav文件+硬编码进(RG+增益9.5dB)的方式逐一生成

所以很显然地,如果你是转成了FLAC之类的已经会直接削波的格式,即使上面那个很复杂的操作也没法拯救你这音频了,怎么放都会爆音,万万要避免。

嗯,最后附一个几种流程的图示吧。三脚猫ps功夫不要笑话(点击大图)。

blog.png

上面的是最终响度=RG目标响度的workflow,优点是基本无损,缺点是输出比较低。目前我在电脑和UPnP媒体库输送到手机两种回放方式时使用。可以看到新歌直接降低-9.5dB(图里写成9了…),老歌直接RG,两者会获得一个相对一致的响度,同时不破坏老歌的动态范围。注意这里的-9.5dB和RG都是纯粹回放时加的,原始音频是不经过修改的。

下面的是方法2,即最终响度=新歌平均响度的workflow。我在转换歌曲到单独m4a文件时使用此方法。优点是兼容性强,和大多数市面上现成的歌曲文件响度一致;缺点自然是对于高动态的老歌峰值会溢出。为了防止削波,如果是即时回放或转换成支持超过FS的格式(AAC/m4a)时,可以通过再加个digital音量调整拯救(这种情况下就依然无损动态范围),或者如图所示干脆直接最后加个可选的Advance limiter进去(可以是转换时的硬编码,可以是回放的DSP)。对于不支持超过FS的音频格式,这大概是唯一可以接受的转换方案)。

半场好戏——妖精的旋律【旧文】

10年的读后感,当年不知道为啥只发了Bangumi。


说来惭愧,身为一个ACG fan(自称),居然一直没有看过这部大名鼎鼎的作品。可以说在我刚入宅的05年,在学校vod看到这部介绍就觉得很重口,不过当时没见过市面,看了点寒蝉,以为日本动画这样也很通常,就一直撂那里没看。昨天整理硬盘突然看到几个月前下的这套漫画(台版翻译成「變異體少女」),再想想自己也算是个原作党,于是就开始看了。

故事讲述地球上最近突然出现一种长角的新人类——二觭人,他们命中注定要取代人类,而旧人类自然不会束手就擒,他们很早就开始将这种人类圈养起来研究对策,但是意外还是发生,一个实力最强的二觭人——露西,从实验室中逃了出来。故事因此开始。露西因为人格转换而变成了一个人畜无害的无知少女,被一家公寓「枫庄」的主人耕太收留。而他俩其实早就见过面——八年前,正是露西杀掉了耕太的全家,而露西由于人格转换、耕太由于在这样的冲击下失忆,两人却互不相识。随着时间的推进,研究所不断派来各种人员来捉拿露西,而男主人公的记忆似乎也随时可能恢复……

可以说,整个故事在前半段张力十足,前一秒还一副主角相的谁谁,下一秒就可能变成一堆肉块,作者用这种果断,这种「杀人不眨眼」的功夫,很好地塑造出了二觭人的残忍恐怖特性;另外一方面,虽然出场人物不算很多,但是每个人都个性分明,而且几位女主人公都背负着悲惨的过去,可以说打下了一个不错的基调。但对我来说感触最深的却在于,作者笔下的女孩们那略显空洞的眼神加上大量的绯红,给人一种随便触碰就会坏掉的感觉,非常非常让人想怜惜(吐槽下,露西和娜娜变身后那御姐形象完全不喜…)。尤其是娜娜,从她一出场我就被迷倒了,她那种对父爱的渴求的感觉,实在是… 当然还有真理子,可以说,藏间和他两个女儿之间的爱恨情仇这条线,就是整部漫画最精华的部分。当真理子抓着露西爆炸之后,故事已经达到了他的最高潮(但是很可惜,作者并不这么想,所以这里渲染并不够,而且这场戏排的也不好,否则感人程度肯定可以再上升)。至此,作者成功地导演了半场好戏,如果故事在这里戛然而止(当然,如果真要这么做,得来AfterStory或者之前把二觭人和研究所那档子事儿讲清楚),那对我来说可以封神;但是很可惜作者并没有这么做。

但也许作者根本没有做好长期抗战的准备,在故事的后半段剧情开始急转直下,可以说在真理子之后,作者墨迹了5、6卷,在剧情上也基本没有什么实质性的发展了,说难听了感觉就像在骗稿费。如果说你要把二觭人的来龙去脉说清楚也不是不可以,但是用整整5卷来讲……?作者你也辛苦了。在这样苍白的故事发展下,别的一些问题开始大量暴露,比如剧情的前后不一致——比如到后来,二觭人似乎变得不再那么不可战胜,甚至某些情况下苍白无力;没错,人类方的武器是有提升,但是拜托编的圆一点,靠那样一个人都杀不死的破弹弓打二觭人,我没法接受。当然相反的,二觭人也会莫名变强,前面铺垫了那么久的触手长度,后面也不重要了,反正实在不行二觭人还会爆种嘛。比如前面伏笔的浪费,真由前面被后爸性侵犯过,而之后看到男主(被Nyu强迫)摸Nyu胸时还露出了很异样的眼神,但是后面也就这样不了了之了;真理子用自己生命换来露西的角断掉,而后面台词是「啊露西已经死了,现在她只是人畜无害的Nyu了」,但是后面露西不是随随便便又出来了?!而且角又长的更大了。最让我无语的是那个荒川(角泽的女助手,最后研发出疫苗那位),前面以为她后来必然有大作为,结果这个人整个就是一说相声的,尤其在那个岛要崩坏的时候一边逃一边和那个队长说冷笑话,完全是无厘头。再比如最后露西在地下湖碰到了角泽和那个巨型怪物,这终极对话本来可以玩的更有意思点,结果就是露西三下两下刮死那怪物,然后再刮死角泽和他那儿子(说他他儿子我又要喷了,这娃出场一共不到一话,你是来打酱油的??)…其实角泽自己并不是二觭人而仅仅是头骨异化这么好、这么讽刺的点子,我可以说我自己完全就没有想到,作者你要是好好经营一下,比如哪怕描写一下角泽知道之后那种「整个世界掉入漩涡」的感觉,相信这结局的档次至少能上两个台阶,结果这样的好包袱就这么埋没了。而男主人公(其实我觉得他根本不算主人公)在过完了属于自己的家家之后,后面的作用也虚无化,感觉就是为了结局时和露西缠绵几下而已,而且还突然变得非常中二……实话说,前面其实也算是他的线,但是风头全部被某个女儿控的大叔抢走光啊…

我个人而言,是比较讨厌剧情中出现什么新的人物,大概是一种不愿意接受改变的心理作祟;但是很明显对于一部作品,应该不断有新鲜血液加入,作者逐渐维护一个更大的人物关系交织的网,故事的格局才会因此而变大,而不是只有三四个人自己的过家家,尤其是对于这部张口闭口毁灭全人类的漫画来说。但是很可惜,作者显然有点小家子气了,打来打去还都是那几号人,甚至发指到真理子死了之后作者为了不浪费人设(喂),又套用模型造人造人出来。而一些新人物的出现,要么是功能性极强,要么是根本不知道他的作用,而最多的情况是直接给人作者想表达「啊,我终于编出来个新人物」的感觉。比如那第二个研究所,突然搞出来这劳什子干嘛?

到了后来另外一个问题是,读者对那些重口味的东西已经麻木,而作者反而越来越「仁慈」了。且不说战斗场面的残酷给人带来的刺激会下降很多这点,作者后来不知道是不是改信佛了,死的人越来越少,几个主人公都能在受极重伤的程度下存活,实在是太狗血,你当你是海贼王吗!不是我觉得一定要死人才能煽情,而是作者你自己拉高了读者的口味。前面坂东(就是那个SAT的暴力男)被露西整的右手断掉、双目失明的时候,你也许会有所感触;但是后来你再看到他连肠子都炸出来的时候,反而没了什么感觉;而如果看到他在故事的结尾,又走了出来的时候,你是否会掀桌呢?读者早就习惯了一会儿掉个胳膊一会儿缺根腿什么的,残肢已经不能再当成卖点了啊,作者!其实坂东的安排还算好了,至少时间上拉开了,就当是一个番外的Happy Ending;而之前的藏间为了真理子之死而拔枪自尽,结果几话之后你发现他原来被坂东救了没死成;前面男主为了挡子弹刚中枪,没几话你发现他原来只是肺穿孔;这种的才更让人有很强烈的「被耍了」的感觉。我觉得如果让我安排,结局我会安排让由香抑或真由死掉。二觭人怎么也非我族类,而且本身就是杀人机器,如果作者敢于让主角5人组中这两位完完全全的普通女孩牺牲,哪怕只是失去几个胳膊腿的话,我相信给人的冲击会猛烈得多。当然了,可以看出作者最后根本就只是想要一个大团圆结局而已,那么牺牲的只能是露西。

接下来讲一下在本篇作品里出现的两个元素——人格转换和失忆。这两个元素,可以说在各种作品中早已被用滥,其原因很简单:非常好用!对于编不下去的地方——玩失忆,玩分裂!简直是居家旅行杀人越货必备良品。如果你不是写搞笑作品,对于这两项你应该非常谨慎的使用,用好了是增加戏剧性,用不好了——那就是都合主义了,怎么方便怎么编。对于本部作品,可以说先天底子不错,一个变种人,玩玩分裂也无可厚非;男主受到那么大冲击失忆了也可以理解嘛。而且作者一开始还是很谨慎的,有人来抓我了我就是露西,平时我就是Nyu陪男主过家家,不是没有违和,但是可以接受。但是后来呢?当你看到露西可以在子弹射到肉里的时候紧急变身,将子弹弹出的时候,我想不用我再多评论。关于失忆这点本来没什么好说的,不过我想说些别的地方的一些所谓「主动失忆」:比如真由看到露西大战娜娜,她却认为那都是看错了,来自我麻痹;而男主一直中二地叫「Nyu没有杀人」,甚至完全不会有一丝怀疑,我也没什么话好说了。

还有一个很有意思的命题是于二觭人定位的思考。对于这样注定要取代新人类的物种,我们应该赶尽杀绝,还是和平共处? 抑或,就干脆被他们征服?作者没有明确给出答案,但是我个人感觉作者倾向于赶尽杀绝——首先,在他的笔下,二觭人被塑造了一个无差别杀人的残忍性格,最大的表现还不在什么三岁就会杀人这些描述上,而在于经常发生的一种场景:前一秒二觭人还在和你交谈,似乎也没有话不投机,结果下一秒你的你已经身首分离。根本就搞不清你为啥就死了。这种杀人的随意性,实在是让人类很难以接受这样的物种。当然,作者也是有对他们的怜悯的,但多建立在几个特定的角色之上,对于整个群体而言,作者并未流露出太多的感情。

最后,还有些乱七八糟的感想,不单独成章了。作者的画风可以说在第一卷时惨不忍睹,尤其是人物的脖子——每个人都向前伸着脖子的诡异模样,太惊悚了… 幸亏后面有很大改善,从第二卷之后基本就不会关心这个问题了。作品名称叫作「妖精的旋律」,在最后时刻露西死前唱了望美交她的歌,也算是点了题。不过看到那时候我一是已经很困,而是已经被后半段白开水的剧情摧残的不行了,也没剩下多少感触就是了…作者之前还画过一个同名读切(一话完),而且是他的处女作,也有一并收录在单行本之中,不过那画风,那剧情,我觉得还是不提也罢…刚翻看百度百科,看到这么一条:「在漫画原作中,旺太的小狗姿态只是伪装,其实它是拥有不死之身的生命体」……我的妈呀居然还有这么一出,我居然一直没想到……难怪那狗N次都没死……不过这种设定有意义吗?!莫非作者在玩票?另外这狗和露西小时候那条是一条吗………?嘛不管了不管了。

总体而言,虽然只是半场好戏,但是这依然是部不错的作品,评分的话7星吧。有人总吐槽说我写的读后感多半是在骂,这是实话,主要是很多时候作品的好,实在是难以用语言来描述,难免词穷;但是缺点说起来却很容易就滔滔不绝……嗯,还是那句话,大家批判地看。看动画的一些介绍,动画对本作改动比较大,而最关键的在于剧情只到前七卷,就是我所谓的「高潮」那里,说不定能拍出更好看的故事,有空找来看看。

补充几句关于真由吧。很喜欢的角色,而且其人物塑造上比起来也是比较丰满的。

只有12岁,就因为被继父虐待而离家,走时甚至连裙子都没有穿(她是在继父准备再次故技重施时逃跑的),下身穿着内裤就跑了出来。后来她碰到了从主人那里跑掉的汪太,两个相依为命,她在那个海滩边上的棚子里和汪太一起吃面包边的场景,很是温馨。而在汪太被主人要走、卖面包的店要搬走这个对她来说简直是末日一样的那个生日,她一个人在那里哭的场景,更让人难忘。还记得她刚到枫庄的时候,看到食物流下口水的那个可爱的表情。

在后来,同样是在那个棚子里,他照顾了坂东和藏间两个重要人物,和娜娜关系最好最熟悉的也是她,可以说她串起了很多重要的剧情。她也在枫庄找到了属于自己的幸福,我前面有说过最好剧情把她写死会加强作品的感觉,但是果然即使让我来写,我也下不了这个手。

ReplayGain、音量规格化与实战应用

又一个兔子洞(笑)。

ReplayGain和响度

故事开始前,大概得先讲讲到底什么是ReplayGain,以及为什么我们需要它。

响度规格化

一言以蔽之,ReplayGain是用来规格化(Normalize)音乐,或者说数字音频文件,的响度(Loudness)的。但是这里要非常小心术语,因为规格化这个词有很多不同的应用。控制动态范围——即“最响”和“最安静”之间的差别,这个有时候也叫作“规格化”(例如在PotPlayer中),但是在音频世界里,更常用的说法叫“压缩”(Compress)。可以看到,一个音频在单纯经过“压缩”之后,其平均响度可能并不会变,但动态范围会变小。这可以解决诸如“电影声效太大,人声小到听不见”之类的问题(但是注意:这并不是一个“正确”的解决方案。至少对我个人来讲,多数动态压缩滤镜听感极差),不过在混音界 Compressor 更常用的用法是指控制输出的范围,便于后期处理。可以看到,一个音频如果经过压缩,其声音特征就改变了。比如艺术家的本意就是这里是悄悄话,那里是爆炸声,结果压缩之后两者的差距就缩小了。因此,在播放音乐时,一般是不应该引入压缩的。

响度规格化,包括ReplayGain,则不同,它要解决的是这么一个问题:不同音轨之间的平均响度差别很大,用户播放时需要不停地调整音量来获得一个相对舒适的响度。在经过响度规格化之后,一个音轨本身的动态范围理想情况下应该不变,但是会整体地提升或者降低,从而达到音轨于音轨之间相对一致的响度。

既然知道了目标,那实现原理其实就不难理解:只要找到一种测量平均响度的方法,然后分别测量每个音轨的响度,然后再设定一个标准(Reference),比较两者不同,补上差值就行了。

不过这里有俩问题:一个是如何测量响度,一个拿什么当标准。ReplayGain作为音量规格化的一种实现,其实就是解决了这两个问题。

响度的测量

我之前一直以为,ReplayGain的响度测量其实就是单纯测量了一下音频信号的平均强度。

这里稍微赘述一下,(解码后的)音频信号其实表示起来非常简单,就俩量:一个是采样率(每秒多少个点,一般音频是44100 Hz),一个就是按照该采样间隔一字排开的一组时序的数据,每个代表当时的信号强度(当然,存储的时候这系列数据是逐个量化成一定位长的二进制)。至于强度的的范围,有多种表示方式,不过最常用的是是[-1, 1]。绝对值越大,理论上声音也就越响。其中最大的绝对值,一般叫做“Full Scale”,缩写FS。一个点的值既可以写成单纯的一个数字,也可以用dB来表示,例如如果是0.5,就可以写成-6.0206 dB relative to full scale,一般简称dBFS。这里有个计算器可以用(注意这里针对场量和功率量中dB的算法不同,这里应该用和电压、电流一样的场量的算法)。

说回“平均强度”。学过信号与处理的应该都知道(虽然我并没学过w),在这里是测量信号的方均根(Root mean square,RMS),而不是平均值或者绝对值平均。RMS这个度量,可以更好地反映比较不同音轨之间的“能量”差别。例如,一个幅值为1的正弦波的RMS就是 0.7071,这个值也可以写成dbFS的形式,这里就是-3.0103 dBFS(这个值很重要,我们后面还会遇到)。它的能量,就应该和峰值为0.7071的方波一致。同理,音乐文件多是多个波形(多为正弦波)的叠加,最简单的方法度量其“能量”的方法就是对所有的点求RMS。

公平地讲,用音频文件的RMS(物理特性)来表示响度,不算是个太坏的方式。响度和能量,两者本身就是非常相关的。不过由于人的听觉感应曲线并不是直线一根,对于不同频率的声音,敏感程度不同,还是应该有更先进的模型。“Loudness”这个词本身的含义其实就包含了人的主观感应在里面的,如果单纯讲物理特性一般会有其他的术语,这个一会再说。

ReplayGain的实现

关于ReplayGain的具体实现方式,其实在当年(2001年?)的Proposal里面讲得很清楚(这是另外一个版本,排版稍微好一点)。总体而言,分为三步:

  1. 先利用等响度曲线,通过滤镜对不同的频率部分的进行修正,给与不同的权重;
  2. 再讲音频分为每个50ms的段落,测量每段的的RMS;
  3. 将所有段落的RMS进行排序,然后选取位于95%处的RMS,作为整个音频的“代表RMS”。在这步作者的理论是,人类的感知响度其实和其中比较响的部分有关,而不是整个音轨的RMS。例如对于对话类型的音频,其中大部分时间都是空白因此总体RMS会很低,但是人只对有声音的地方敏感,所以并不会这么觉得。

最后,只要把这个代表RMS的dBFS值和标准进行比较,然后增加/减少其中的差值就行了。可以看到,整个过程很好理解,而且其本质上还是基于RMS。另外从算法中显然可见,如果放大整个音轨几个dB,其“代表RMS”也会提升对应的dB,所以要调整某个音频的响度,也非常地简单。

能量、声压、响度、RMS的关系

这里再废言几句。我们前面已经说了RMS是对音轨“平均”能量的一种度量,于是这里顺便讲讲感知响度/声压/能量的关系。说到声音大小,最常听说的说法就是“分贝”。其实准确而言,这里的分贝是dB SPL,即声压级(Sound pressure level)。我们知道dB是一个比较量,这里就是指相对于一个标准化了的基准声压,20 μPa(有时称为听阈)的dB值。声压级是个场量,所以20 dB = 10倍。而声音的能量,一般可以用声音能量密度(Sound intensity)或声功率(Sound power)(这两者之间就差了一个面积)来表示。这是另外一个不错的度量声音强度的方法。其和声压的对应关系是:声音能量密度扩大100倍,声压扩大10倍。不过由于声音能量密度和声功率都是功率量,所以如果也表示成级(Level)的话,是每扩大10倍=10 dB,或者20 dB等于变大100倍。因此,假设一个基准声音为1 [单位]的声压和1 [单位]的声音能量密度,那么一个有20 dB声压级的声音也正好会有20 dB的声音能量密度级——不过绝对数值上,分别会是10 [单位] 声压和100 [单位] 声音能量密度。

回到RMS的话,可以观察到,RMS的量纲是和波形的点的数值一致的场量(因为又开方过了),而非功率量。因此,和RMS对应的其实应该是声压级——也就是说,两个RMS差了10倍的音频,其“声压级”会差10倍(假设你的音响完全无其他损耗),“能量密度”或“功率”则其实应该是差了100倍。不过和上面一样,如果都用dB表示,数值则都是一样的,20dB。也就是说如果你整体提升一个音轨所有点的值2 dB,其RMS也会提升2dB(自己算算就知道),声压级和声能密度级也都会提升2dB。

到此为止,都是纯粹的物理量。牵扯到(感知)响度就复杂起来。说到响度,其实搜了一下相关的文献以外地少,连维基百科都说的很模糊。网上比较常见的是sengpielaudio的一系列文章([1][2]),这里也以此为基准。我们知道,人的各种感知和对应的物理强度,一般都是呈指数级关系(Stevens’ power law)。其中比较有名的是视觉(对光的敏感程度),这个话题我原来在知乎谈过一次,什么时候也可以整理一下发个blog。听觉自然也不例外,但是“指数级”的指数到底是多少呢?

一般而言,长度不过短、频率适中的声音,可以经验地认为声压级(SPL)每扩大10倍(即20dB),感知响度扩大4倍。也就是说,响度L正比于声压SP^0.6。如果换成声音能量密度,那就是L正比于SI^0.5*0.6=SI^0.3(因为声压和声能密度是开方关系)。换句话说,就是每10dB,响度翻倍(别忘了对于SI和SP,dB数是一样的)。

这里无耻地盗一张图来说明。

Loudness - Sound Pressure - Sound Intensity
响度/声压/声音能量密度对应表

Source:http://www.sengpielaudio.com/calculator-levelchange.htm

OK,既然我们知道了响度至少和能量、声压、甚至常说的dB数都不是线性而是指数关系,那么为啥我们还用RMS?仔细想想就会发现,我们的目的只有一个:使音量保持在一个水平线上。所以,不论他们之间的关系是啥,线性还是不线性,只要都是正相关的就可以:如果俩文件的RMS(或者是ReplayGain算出来的“代表RMS”)经过加减dB之后相同,那至少我们可以说,他们的响度也类似。毕竟,我们并不需要准确知道“A比B响多少”。其实就像我们说RMS是音频“能量”的代表,但也不是RMS大10倍能量就大10倍(而是100倍)。这就好比我们用摄氏度表示物体的冷热程度,你也不能说10度比1度热10倍一样(即使你换算成K,这种说法也不一定成立)。但是如果仅用来比较两者孰大孰小,就没问题。

响度的基准

现在,我们解决了第一个问题——如何测量响度。第二个问题自然就是选取标准。在当时,音响行业并没有任何相应的规范,于是RG的作者从电影行业——电影电视工程师协会(SMPTE)那里借来了一个规范:RMS是-20 dBFS的粉噪音,应该(在听众的位置上)呈现为83dB SPL。仔细解读这句话的话,你会发现它其实讲的是信号强度和实际声压的对应关系。你也可以说成“-15 dBFS的粉噪音应该呈现为88dB SPL”,关系依然不变。不过,这两个数也不能认为是任意选取的——粉噪音用-20 dBFS,是因为这个RMS级别的声音在电影、电视中比较典型,所谓“Alignment level”。“平均”强度是-20 dBFS,意味着在单侧有着20 dB的动态空间(即“headroom”)。这个强度的声音会被呈现为83dB SPL,大概也是认为83dB SPL是一个比较舒适的数值(这里有一份解读)。不过这里要注意,这个标准的本意是电影院用的,而电影院的音量,观众是不可调的,所以有一个规定的声压大小(83 dB SPL)很有意义。但是换成家庭媒体,意义就不大了:用户无论是在软件还是硬件,都有额外的Gain或者Volume可以调,这个数字并无太大意义。真正有意义的,是前半部分:-20 dBFS。

但是搞笑之处在于,ReplayGain自己觉得这个数太小(即平均音量太小),而且一般音乐也用不着这么大的headroom,于是自行加了6dB,变成-14 dbFS RMS粉噪音——作为RG的目标。那么,如果假设对应关系不变,那自然这个声音在理想的电影院环境里也就会被呈现为89 dB SPL了。这也就是你为什么会在各种地方看到,RG的目标是89 dB这种说法。但是可以看到,“89 dB”这个数字本身已经并没有任何实际意义了:在RG处理音轨的时候,纯粹是根据上面三步走,算出一个音轨的代表RMS,然后和-14 dbFS RMS的粉噪音的代表RMS比较而已。

事实上,RG1.0的原始代码都可以在这里下到,是MATLAB写的。里面也包括了一个ref_pink.wav文件,不过这还个是-20 dBFS RMS的,没有修改成-14 dBFS RMS。由于代码非常陈旧(2001年的…),MATLAB的一些函数已经发生了变化。所以,我进行了一些修改,发了个能用的版本在GitHub。对ref_pink.wav进行RG,可以看到得出的reference vRMS(前面我叫作“代表RMS”的那个东西)是-31.5。也就是说,如果是一个-14 dBFS RMS的粉噪音,就应是-25.5了。在真正的RG的实现中,无论是89/83还是-14/-20,其实都不需要参与计算,只要有这个-31.5/-25.5在就可以了。用同样的函数处理随便一个音频,得到其对应的vRMS为-14.9,也就是说我们需要降低16.6 dB,来使得其vRMS和reference(粉噪音)一致(如果换成-14 dBFS,那就是降低10.6dB)。

dBFS RMS的定义

这里又㕛叒叕得插播一段。如果你好奇地计算一下那个ref_pink.wav的RMS:

[y, Fs] = audioread(‘ref_pink.wav’);
myrms=rms(y);
valueDBFS = 20*log10(abs(myrms))

会发现……他并不是-20 dBFS,而是-23.0103 dBFS。这又是怎么回事?原来,dbFS又有“传统定义”和“数学定义”之分。所谓数学定义,就是我们这里计算的。但是在音响业中,经常用另外一种传统定义:因为音频大多是正弦波的叠加,故所谓的Full scale并无法达到。真正能达到的(在不发生削波的前提下),是一个幅值为1的正弦波的RMS,也就是0.7071。是不是觉得眼熟?前面出现过。这个数字换算成相对于数学上的Full sacle(通过方波可以达到),就刚好是-3.0103 dB。所以,经常情况下,业内说的“Full scale”,或者dBFS,是以这个值作为基准(0 dB)的。因此,一个数学上是-23.0103 dBFS的音频,在传统定义下,就变成了-20 dBFS了。这个定义一般只用于讨论RMS,在讨论波形上某个点、或者峰值时,FS依然是以1为基准。

ReplayGain——实战篇

OK,在彻底厘清了ReplayGain的今生前世,和相关的一些容易混淆的概念,我们终于可以进入实际应用,以及其中会遇到的问题——这也正是我要写此文的初衷。

响度竞赛

前面说过RG的目的是音量规格化,那就么具体到音乐,其问题来自于从90年代开始的“响度竞赛”。简单来说,商业公司发现,如果一个音轨明显比别人音量大,观众会心理上觉得更好听。因此,混音业开始悄悄地渐渐提升音频的响度,以求和别人一起放时“更突出”。

要提升响度,第一步自然是“maximize”——也就是把增益整个音频直到峰值(peak)达到1(或者0 dBFS)。不过要知道,几乎所有的商业混音本身就已经这么做了,那自然就没有上升空间。要进一步提高响度,唯有压缩动态范围——从而可以得到更高的平均响度(RMS)。因此,响度竞赛最大的影响其实并不只是响度增加,而是歌曲的动态范围也减小,起伏变小了(这里有一篇蛮长的文章,认为响度竞赛并没有导致动态范围减少。我没细看,不过在RMS level和峰值的差值这个语境下,动态范围变小是不争的事实)。

举例而言,おニャン子クラブ在1985年发行的专辑《KICK OFF》,随便选取一轨“真赤な自転車”,其RMS分别是(左右声道)[-19.6993  -20.3342](此处为“数学定义的”RMS,下同);如果拿它去跑ReplayGain,结果是-3.7319 dB(-14 dBFS粉噪音为基准,下同)。至于其峰值,是0.918945(或-0.7342 dBFS),甚至都没有max’d out(不过整张专辑的峰值确实是1就是了),单边动态范围约是19dB左右。

而前年发售的“ときめきポポロン♪”,RMS是[-11.5458  -11.5820],足足比上面的大了快9dB!峰值是1,也就是说从RMS到峰值的单边动态范围,只有11dB。如果拿这个去跑RG,算出来需要降低9.7776 dB。

在foobar2000中使用ReplayGain

可以看到,如果要想平衡所有的音量,最简单的办法当然全部都跑一遍RG,那自然也就都均衡了。不过这有俩问题:

  1. 跑所有的音乐实在是太不现实了。我本地的音乐足足有8 week+长,全部都跑一边的话大概要累死,而且这个计算其实还是挺慢的。
  2. 从上面的计算可以看到,RG默认的reference,实在是响度太低了。连以今天的标准来说声音小到不行的“真赤な自転車”,居然计算出来的是-3.7 dB,也就是还要再小3.7 dB!虽然我们可以通过其他途径再对结果进行增益,例如音量滑块或者音响的音量旋钮,但是这样的话就很难掌握foobar2000和其他程序相互的音量差。

然而,foobar2000内置的增益的目标响度是不可调的[*]。因此,我想到一种折衷的方法,利用foobar2000带的pre-amp选项。Pre-amp是一个RG之外的额外选项,可以在RG之后再加减一个dB,可以分别给有RG信息的音轨和无RG信息的音轨的设置不同的数值。我先选取大量近年的音乐,也就是音量合适不会太小、不用调整的音乐,然后全部视为一个整体进行RG计算。算出来的结果,大约是-9.5 dB。也就是说,近年的音乐的vRMS比RG的基准,平均来说高了9.5 dB。我们这里记住这个数,但是取消不保存,因为这些音乐并不需要任何处理。相反,对于那些音量偏小的老CD,我们先去真的跑RG:跑完之后的结果自然是一个一致、但是较低的音量。不过,我们只需要在pre-amp里设为+9.5 dB,所有的音乐就一样响了。而且,因为老CD毕竟是少数,这样也不用跑太多次。

[*] Foobar2000的RG的目标(-18 LUFS)是不能调的。在选项的高级里,确实倒有个选项可以设target volume level (dB)(默认89,呃至于这个数字怎么来的前面说了,这里你就理解为你修改后的数值和89的差值加到那个-18 LUFS就是),但是那个只对文件转换有用,对播放器内播放没用。

Clipping 和 True peak

和所有的增益有关的东西都需要注意一个点,就是clipping(削波)。我们知道信号的极限就是0 dBFS,如果你通过上述的RG+pre-amp之后,峰值超过了0,那就会被削波——这是我们不愿意看到的。事实上,前面也说了,80年代的音乐虽然RMS很低,但是峰值依然也是接近1的(毕竟,几乎所有的商业CD,出厂前都会max’d out)。经过我们上面的RG+pre-amp的处理,很容易地就超过了1。例如,“真赤な自転車”的峰值是-0.7342 dB,加上RG的-3.7 dB,和pre-amp的+9.5 dB,峰值就变成了+5.1 dB。这也是为什么RG同时也会扫描并且在元数据里记录峰值,便于事后再降低(fb2k里有选项)来防止clipping的缘故。

虽说一般而言,对少数峰值的削波,人耳并不敏感,不过还是尽量要避免这种情况。但是别忘了,在音频输出到mixer之前,还要经过“音量”的计算。一般而言,我的音量都设在-10到-20 dB之间,因此理论上来讲,应该不会有任何削波的问题。

呃,其实说到峰值和削波,还得插播一段讲讲true peak的问题。如果把数字形式的音乐(即一串数字)还原成模拟信号,相当于从一堆离散的采样点还原出波形。根据采样定理,在遵守原始的频带限制(频率不超过采样频率的一半)下,每一组离散点只能还原出唯一的波形信号(注:非科班出身的弊端又体现了,我原来一直没能真正理解采样定理。直到今天听到这句把常见说法“2倍于信号的频率采样可以完美采样”反过来的说法才豁然开朗。推荐看Xiph.org的这个视频,说的非常清楚,是科普的典范)。但是,能完美还原波形,不代表波形中的峰值就在我们的采样点中会出现(所以我上面说信号的极限就是0 dBFS也不太准确):

modified signal with ripple
曲线是过采样点的唯一的波形,但是峰值高于采样点

Source: https://techblog.izotope.com/2015/08/24/true-peak-detection/

因此,如果我们仅仅测试采样点的数值,来选取peak,可能会相差甚远。因此,更高级的峰值检测,一般会把信号还原之后再过采样2x甚至4x,从而来找到更准确的峰值。foobar2000的RG也自带oversample factor的选项,在advanced里的tools里。至于这个峰值搞这么准确到底有什么用(毕竟,我们的采样点本身并没有溢出嘛),就要牵扯到DAC了,这里就不赘述。

Windows mixer 的隐藏 Limiter 问题

不过,理论和实践总是不一样的。在我用上述方案播放音乐时,每当放到那些经过RG+pre-amp的老歌的大动态区域,总有一种很奇怪的感觉——嗯对,就是像被动态压缩过一样,听起来非常不适。百思不得其解之下,我决定把某首歌的RG和pre-amp硬编码进去(也就是说,彻底地修改波形,合成进去RG和pre-amp。RG本身则只是通过元数据的方法,并不真的修改波形文件本身)。虽然这样自然也会产生削波了,不过播放时的最终输出响度应该和有RG tag的原始文件一致。那么,现在播放这俩文件(原始文件+RG tag+re-amp)和转码后的文件,我惊奇地发现,听上去响度居然不一致:原始文件+RG+pre-amp的组合,要明显的小很多。

在实在没有思路的情况下,我斗胆去Fb2k的论坛发了一贴(要知道这种论坛,一般大牛脾气都不好)。不过惊喜地是,居然得到了答案。

原来,Windows的默认音频输出(Direct Sound,DS)混音器,有一个内置的“Limiter”。当输出音频超过一定值(这个值具体是多少不清楚,一说是0 dBFS)时,Windows会自行降低响度来防止削波。至于降低的方式不明,不过从我的感觉上来讲,不是整体降低音量,应该是类似于动态压缩的方式,即在一定的buffer内遇到超过1的峰值就降低,算法不是很高级从而产生不适感。

前面说过,考虑了音量之后明明根本不会超过0 dBFS才对。但是Foobar2000现在是用Windows自带的mixer来处理音量的(你改fb2k的音量,会发现右下角音量控制里面的foobar程序的音量也会跟着改),结果这个Limiter作用的时间早于音量(会检测音量调整前的峰值),结果就导致了这个现象。至于我自己压制的那个对比的文件,因为溢出的部分已经削波削掉了,自然就不会被Windows限制。不过,这个(limiter作用于音量前的问题)似乎是个Windows的bug:只有Win 7或者Win 8.1会有这个问题,在Win 10中,已经得到修复。另外,如果使用WASAPI之类的输出绕过DS,就完全不会触发这个Limiter,无论你真的溢出了没。

既然知道了原理,那解决方案也很显然了:我把所有没有RG信息的pre-amp设成-9.5 dB,有RG的设为0 dB,就可以保证两者依然均衡;但是总体音量偏小的问题,则只能通过把foobar的音量提升到0到-10 dB,还好之前留的余量足。其实,我也试过有RG信息的设成+3 dB(另外一个对应设成-6.5 dB),好像也不会被限制(虽然明明有少许溢出0 dbFS的peak),这就搞不明白了。

不过,往移动设备上转换的时候,我依然用的是新音乐原封不动、老音乐硬编码进RG+9.5 dB的方式。否则用我的解决方案播放转换过的文件,音量反而会变小了。虽然这样做可能会导致削波(因为转换时自然不会考虑音量进去),不过移动设备听不太出来。更好的方案应该是再加一层DSP,用fb2k自带的Advanced Limiter,把超过的部分智能地调整到0 dBFS以内;虽然有点像动态压缩,但是算法更高级,完全没有一般动态压缩的不适感。

顺便一提,根据某个帖子的说法,一般动态压缩的不适感似乎是来源于压缩“释放”得不够快,或者开始得太早的缘故:例如,你有一个音轨一直是很小声突然一个巨响,然后又恢复到很小声。我们想要压缩的自然是这个巨响,但是一般的动态压缩在检测到巨响(会有一个buffer预先检测)之后,会释放的比较慢,从而导致后面紧接着的“小声”部分会有个从小变大的过程,听起来就很不舒适了。

ReplayGain 2.0

故事到此,似乎也就告一段落。但是别急,还有最后一个爆炸性消息:fb2k的“ReplayGain”,并不(再)是ReplayGain。或者准确地说,并不再是原始的、也就是上面提到的算法了。我也是无意发现这件事的:你把之前我提过的ref_pink.wav文件放进foobar2000跑RG,因为是个-20dBFS RMS的而后来的RG标准是-14(见前文),理论上应该跑出来+6dB对不?结果却是:+2.35 dB

这又是怎么回事?!原来在2011年,欧洲广播联盟(EBU)提出了一个新规范,EBU R128解读),其中对响度规格化进行了规范。其中提出,响度规格化的目标应为-23 LUFS。LUFS又称LKFS,是ITU-R BS.1770中详细规定的一种测量响度的算法。比起RG,这好歹是一个正儿八经的行业协会,经过多年研究得出的规范,所以很快,foobar2000就修改了RG的算法(又称RG2.0)为ITU-R BS.1770中的算法了。唯一的区别是,为了和原来的RG保持在相当的响度,又“擅自”把目标提升了一些,改成了-18 LUFS。注意这里的“相当的响度”,是基于对大量音乐分析得出的(另参见当年的各种对比),而并不是把原始的pink噪音拿来分析的;事实上,原始的pink噪音(-14 RMS dBFS版)算出来是 -15.36 LUFS,响于-18。(如果想自己测试要注意,我发现bs1770gain对单声道处理和fb2k不同,于是我手动复制了一份声道把它变成了双声道先)。民间也有很多对这个的实现,例如r128gain (后改名为 bs1770gain,毕竟讲道理,算法的部分是bs1770规定的,而不是r128)、libebur128(fb2k就是用这个实现修改而成)等。

另外,除了直接使用ReplayGain之外,fb2k论坛的版主之一kode54开发了一个DSP插件,foo_r129norm,可以即时地将音轨的音量规格化。也就是说,如果用了个这个插件,完全傻瓜化,根本不需要单独去给音轨跑RG啦。不过,考虑到转换到移动设备的需求,我暂时并没有用它。顺便,DSP的处理是在RG之后的,也就是说pre-amp的选项就没效了。你要想再在此基础上提升一个音量,得用EQ整体加个dB咯。

结语

本文断断续续写了2天,阅读了大量的资料。不过可以说是很满足的,基本中间遇到的问题都算是想通了。中间看过的帖,我尽量都用链接插入到文章中了,算是便于自己以后查找。