用Ruby写了一个自动连接循环音频片段的脚本

从3月开始我接了一个给游戏作曲的项目。大家知道游戏的BGM一般是循环播放的,因此作曲时也要考虑到这一特性,最后输出的音频素材(wav文件)应该切成两个Part,其中Part-A是一个引子(不进入循环的部分),Part-B是主循环体,A到B、B到B应该都可以无缝衔接,在游戏中播放出来应该是“A-B-B-B-B-...”这样无限循环下去。

为什么要说这个呢?因为除了给游戏输出素材之外,我还需要输出一份原声碟用的音轨,按照游戏原声碟的惯例,一首曲子一般是输出“A-B-B”(即循环两次),最后的部分再做一个淡出(fade out)效果。在有很多首曲子的情况下,一首一首到DAW里面去输出太麻烦了,既然我已经输出好了A、B素材,那么如果我写一个脚本,自动按照“A-B-B-淡出”的形式合并起来不就好了吗?

于是我想到用Ruby来完成这个工作。首先,找一个可以处理wav文件的gem:wav-file;接下来我们来想想如何实现两个操作:连接和淡出。连接很简单,只要将两个wav文件的数据块连起来就可以了;淡出也不难,wav文件中每个采样的值代表该采样的振幅(-32767~32767,以16位采样为例),将每个采样的值按一定比例缩小,最后缩小到0,就实现淡出了。

于是,为了实现淡出的计算,我们需要一个函数,该函数要满足的条件是,当自变量x的值从0到1移动时,函数f(x)的值从1到0移动。要实现这样一个函数,最简单的就是线性函数,即:

f(x)=1-x

看图像比较直接一些(图像只考虑0~1区间,下同):

fx_fo_linear

不过,声音振幅和感知响度之间并非是简单的线性关系,而是更接近对数关系(参见韦伯-费希纳定律),因此线性淡出的实际效果显得并不是很自然,一般会用一个非线性函数来补偿这种感觉上的差异,比如给刚才的线性函数加上一个指数:

f(x)=1-x^k \quad (k>0)

这个函数中的指数变量k,可以调节函数曲线的形状,当取k=1时,该函数等于上面的线性函数;当取k=2时,我们可以得到一个近似的等响度(constant power)淡出曲线:

fo_fx_curve_k2

当取k=4k=0.3时,可以分别得到下面这样的曲线:

fo_fx_curve_k4fo_fx_curve_k0.3

核心问题解决了之后剩下的就好办了,代码贴出来:

require 'rubygems'
require 'wav-file'

# Fade out function: curve type(use parameter k to adjust curve shape)
def fade_out_curve(x, k=1)
	1 - x ** k
end

# Set fade out length and bits
FADE_OUT_LEN = 5
BITS = 's*'

# Load pre (part-A) and main loop (part-B) wave files
format, dataChunk = WavFile::read(open(ARGV.shift))
dataChunkLoop = WavFile::readDataChunk(open(ARGV.shift))

# Slice for fade out
fo_range =  FADE_OUT_LEN * format.hz * format.channel - 1
wavs_fo = dataChunkLoop.data.unpack(BITS)[0..fo_range]

# Apply fade out
(0..fo_range).each{|i| wavs_fo[i] *= fade_out_curve(i.to_f / fo_range, 2)}

# Join (A + B * 2 + fade out slice)
dataChunk.data += dataChunkLoop.data * 2 + wavs_fo.pack(BITS)

# Write to output file
open(ARGV.shift, "w"){|out|
	WavFile::write(out, format, [dataChunk])
}

稍微解释一下淡出时间的算法。wav文件中存放数据的单位是“采样”(sample),因此需要根据设定的淡出时间来计算实际需要处理的采样数,即:采样数 = 时间(秒)×采样频率(Hz)×声道数量。例如,采样率为44100Hz的立体声(声道数=2),当淡出时间为5秒时需要处理的采样数量为:5\times44100\times2=441000。话说,wav文件中的样本是左右声道交替存放的(即LRLRLR),如果要追求准确的话,在淡出过程中每一对左右声道两个采样应该乘以同一个衰减系数,我们为了简便把左右声道当成连续采样来处理了,实际听感应该没有区别。

顺便,如果要做淡入(fade in)的话,只要把函数取负值就可以了,即:f(x)=x^k

发表评论

电子邮件地址不会被公开。 必填项已用*标注