Hi-Res的意义(外一则)

(本文部分观点已经在在各种旧文中发表过,如果有既视感为正常现象。)

Hi-Res音频(相比CD)是否有意义?首先要客观分析HR到底改变了什么。

Remastering / remix

之前提过,很多时候Hi-Res最大的意义并不在于“High”本身,而是给了厂牌一次re-release的机会。这本质上和是否HR是没有任何关系的,唱片公司如果愿意,完全可以发行remastered的CD(事实上,很多精选集CD里的旧曲都进行了remaster)。注意这里的remastering是指狭义的、即混音有明显的变化的情况,从物理角度来说就是波形或频谱有明显的变化,不包括单纯的音量、动态范围变化(compress),那个下面专门叙述。

当然,重新混音并不是单纯的技术问题,而且还有艺术发挥的成分,所以这个主观性就很大了。很多时候可能重混后的版本不如原版(个人喜好)。随便举个例子,Lantis的HR版的『U・N・M・E・I ライブ』和CD版混音有较为明显的区别,HR版声场重新调整过,但是我个人觉得人声偏小,更喜好CD版。另外就是上次说过的「THE IDOLM@STER MILLION THE@TER GENERATION 11 UNION!!」这张碟也有类似区别。

对于新发行的音乐,一般CD版和HR版不会也不应该有混音(狭义)的区别——几乎不存在某波形HR能发挥、CD发挥不出来的情形。如果厂商这么做,那只能认为是为了商业考虑故意劣化CD版,而不是什么技术限制。

高采样率

一般HR都是从CD的44100Hz提升到96000Hz。由于采样定理和人耳的频率听觉极限客观摆在那里,可以自信地说,高采样率应该是HR最无用的提升没有之一。

硬要说的话,有个小问题就是现在音乐大多在制作过程中使用96000Hz(或者48000Hz?),而CD的采样率由于历史原因是44100这个不能整除的数字,所以resample可能会有时域上的aliasing等问题。不过,有大量高质量的resampling算法的存在,这个问题在主观听感上的影响可以忽略不计。

高量化精度

CD的量化精度(位深)是16bit,HR一般提升到24bit或者32bit。虽然从听感上来说这个到底能否能产生肉耳可辨的区别也很难讲,但是考虑到类比图像的话,8bit/channel这个在当年被普遍认为绰绰有余的位深现在肉眼可见的捉襟见肘(更不要提当年为了work around这个位深在深色感知灰度不足的问题,搞出的遗毒万年的gamma空间,这里按下不表),这里我们还是保守点说高位深还是大概有用的。而且高量化精度最大的优点就是给响度/振幅调整留下了足够的空间——这点在下面详述。

动态范围

前面铺垫了这么多,重头戏其实就是想说这个。CD明明是一个各种指标上都优越于黑胶的介质,为什么许多人却在追求70、80年代的老唱片,而且两者确实有明显的听感区别?一言以蔽之:万恶的响度战争

在追求“越来越响”的前提下,导致音乐的平均音量(我们用一个简单的客观指标:RMS)不断上升,而波形的振幅极限则是固定的0 dBFS,所以能做的只有不断压缩动态范围,使得音乐越来越平(再多不再展开,参见旧文)。

响度战争在进入21世纪之后已经基本进入一个平稳期,基本商业作品(指我一般听的日本ACG)稳定在单边动态范围(RMS->Peak)9-11dB这个数字上。

眼尖的可以发现,响度战争这个问题,和CD vs HR又是没有直接关系的:你完全可以塞高动态范围(因而平均音量也要小很多)的母带进CD(严谨地说,高动态范围配合高量化精度当然最好,但是CD的位深一般也绰绰有余了),只是厂商为了听起来够响不这么做而已。

幸好,HR一般是面向所谓audiophile的,所以比较重视这个问题,不会过度压缩动态范围。而这,也几乎是任何新发行音乐唯一值得追求HR的原因。

反过来讲,如果你发行的HR和CD完全一样的动态范围、更没有mastering上的区别,仅仅是数字96/24好看,那真的有意义么?请容我大言不惭地说一句,没有!

这里让我们有请Lantis选手:

TitleVersionRMSPeakDiff
Glow MapCD-8.84-0.18.74
Glow MapHi-Res-8.83-0.18.73
あの花のようにHi-Res-9.42-0.68.81
なんどでも笑おうHi-Res-16.8016.8
眠り姫 [ORT]Hi-Res-16.33-0.0916.24

不用多说什么了吧。下面两首是作为对比的哥伦比亚的最近发行的的HR。HR和CD完全一样的动态范围就不提了,更可怕的是烂铁的CD们的RMS已经拉到了-8.x这个水平,还剩多少动态范围可想而知了。

当然这个问题不是只有烂铁一家有,只不过一般没这么夸张。看看もちょ最近几张CD(这里同时列出单曲版和专辑版,因为部分有变化):

ArtistSgAlSongSourceVersionRMS (dB)Peak (dB)Diff (dB)
麻倉もも52365×LOVEAlbumCD-9.922-0.0289.894
麻倉もも52365×LOVESingleCD-10.847-0.00110.847
麻倉もも52365×LOVESingleHi-Res-12.595-0.19812.397
麻倉もも52365×LOVEAlbumHi-Res-11.323-0.20111.122
麻倉もも62シュークリームAlbumCD-10.388-0.00110.388
麻倉もも62シュークリームSingleCD-10.928-0.00110.927
麻倉もも62シュークリームSingleHi-Res-12.639-0.19812.442
麻倉もも62シュークリームAlbumHi-Res-11.805-0.20011.605
麻倉もも62スマッシュ・ドロップAlbumCD-9.808-0.0019.808
麻倉もも62スマッシュ・ドロップSingleCD-9.990-0.0019.990
麻倉もも62スマッシュ・ドロップSingleHi-Res-11.945-0.19811.747
麻倉もも62スマッシュ・ドロップAlbumHi-Res-11.247-0.20011.047
麻倉もも72“さよなら”聞いて。AlbumCD-10.814-0.00110.814
麻倉もも72“さよなら”聞いて。SingleCD-9.392-0.1139.279
麻倉もも72“さよなら”聞いて。SingleHi-Res-10.057-0.1169.942
麻倉もも72“さよなら”聞いて。AlbumHi-Res-12.201-0.20112.000
麻倉もも72ユメシンデレラAlbumCD-10.376-0.00910.367
麻倉もも72ユメシンデレラSingleCD-9.107-0.1748.934
麻倉もも72ユメシンデレラSingleHi-Res-9.709-0.1729.537
麻倉もも72ユメシンデレラAlbumHi-Res-11.783-0.20111.582

もちょ早期几张也存在CD版和HR版动态范围完全一样的问题,不过最近发行的至少能多出2dB左右。另外比较搞笑的是同样是音雨人,制作还有细微的差别,もちょ这里可以看到HR版一般有很多余(正确制作的音频有没有headroom,并没有任何区别)的-0.2dB的headroom,而ナンス的碟则基本全都是CD、HR无论RMS/Peak都完全一样的。手头的CD没抓只存了HR版,就不贴数据了。

不过,不同动态范围听起来区别大不大?这个和个人听力、设备、以及更重要的音乐类型还是关系比较大的(例如交响乐一般很需要大动态),我一般听听日呆流行也听不太出区别(事实上,为了车上放歌方便,我放手机的音乐全都是手动压缩过的动态范围均衡过音量的,我有罪)。但这至少是一个实打实、肉眼可见(笑)的客观的区别。

外一则:Python的音频库

在写DR计算的脚本时,发现Python的音频处理库中,似乎并没有一个明显的first choice。当然,数学计算部分都是用Numpy做的,我这里需要的仅仅是一个解码read的库而已。

搜刮了一阵,大概有这么一下几个选择,这里简单介绍下区别:

def read_wavefile(f): 
    from scipy.io import wavfile
    samplerate, data = wavfile.read(f)
    return data, samplerate

最简单的scipy里的wavfile,基本不支持wave/PCM之外的任何类型。输出就是原始整型,取决于位深,int16或者32。注意输出顺序是采样率/值,我颠倒了下便于和别的几个一致。

def read_sf(f):
    import soundfile as sf
    return sf.read(f) 

SoundFile库。输出似乎永远是float64。支持的格式比上面多,但是不支持m4a(好像支持raw AAC?)。

def read_librosa(f, resampling=None):
    import librosa
    data, samplerate = librosa.load(f, sr=resampling, mono=False) #mono by default, resampling super slow
    data = np.transpose(data)
    return data, samplerate

librosa。其本质是调用SF(上述)或者audioread,所以支持的格式也有限。另外参见注释:默认是mono输出很迷惑,自带的resampling巨慢。值的矩阵要转置下和别的统一。好像永远是float32。

def read_pydub(f, normalized=True):
    import pydub
    a = pydub.AudioSegment.from_file(f)
    # a = a.set_frame_rate(44100)
    y = np.array(a.get_array_of_samples())
    if a.channels == 2:
        y = y.reshape((-1, 2))
    if normalized:
        if y.dtype == np.int16:
            power = 15 
        elif y.dtype == np.int32:
            power = 31
        else:
            raise Exception
        return np.float32(y) / 2**power, a.frame_rate # convert to float32 should be more than enough
    else:
        return y, a.frame_rate # y is same as PCM (int16 or 32)

pydub是我比较推荐使用的库了,他是调用FFMPEG来解码的,所以基本支持任何格式。可以通过set_frame_rate来resampling。唯一的问题就是他默认输出的格式和别人比较与众不同,所以要通过上面这一坨子来输出和别人一样的东西。

这里的normalized是指把PCM的整型normalize到[-1,1]的float,并不会改变振幅啦。因为我后面发现做数学计算的时候整型会比较烦,所以我直接在这里面转了float32。

题外话,int16转float [-1,1]无论是除以2^15还是2^15-1都会有小问题(前者会导致32767无法变成1,后者会导致-32768溢出,虽然业界惯例是前者),不是左右对称的辣鸡有符号整型真的超纠结……

另外一个巨坑是如何判断np.array的dtype,参见这个SO,辅助阅读这个

最后前几天发现pydub有个bug:读取24bit的wav巨慢,比同样的FLAC慢几十倍,原因不明。

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的音频格式,这大概是唯一可以接受的转换方案)。

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