概述
在上一篇博文音频变速变调原理及soundtouch代码分析中,介绍了音频变速变调的原理以及开源代码 soundtouch 的源码分析,本次将介绍另外一个音频变速变调开源代码-sonic 的源码分析。源码地址为:https://github.com/waywardgeek/sonic。sonic 与 soundtouch 在实现音频变速上的方法不同,soundtouch 采用 wsola 方法,并使用相关峰来寻找相似帧,而 sonic 则是采用基因周期的方法来寻找相似帧。sonic 的代码结构异常简单,有 java 版本和 c 版本,这里使用 c 版本构建一个 sonic 的 vs 工程如下:
关键文件只有一个 sonic.c。
sonic 的使用方法
使用 sonic 命令行时,使用方法如下:
"Usage: sonic [OPTION]... infile outfile\n"
" -c -- Modify pitch by emulating vocal chords vibrating\n faster or slower.\n"
" -n -- Enable nonlinear speedup\n"
" -o -- Override the sample rate of the output. -o 44200\n"
" on an input file at 22100 KHz will paly twice as fast\n"
" and have twice the pitch.\n"
" -q -- Disable speed-up heuristics. May increase quality.\n"
" -p pitch -- Set pitch scaling factor. 1.3 means 30%% higher.\n"
" -r rate -- Set playback rate. 2.0 means 2X faster, and 2X "
"pitch.\n"
" -s speed -- Set speed up factor. 2.0 means 2X faster.\n"
复制代码
其中的-p -r -s 就是变调变速,变调不变速,变速不变调处理。
从 main.c 中我们可以看出 sonic 的函数调用操作也较为简单。基本调用流程如下:
// 创建流
stream = sonicCreateStream(sampleRate, numChannels);
// 设置参数
sonicSetSpeed(stream, speed);
sonicSetPitch(stream, pitch);
sonicSetRate(stream, rate);
sonicSetVolume(stream, volume);
sonicSetChordPitch(stream, emulateChordPitch);
sonicSetQuality(stream, quality);
// 循环处理
do {
samplesRead = readFromWaveFile(inFile, inBuffer, BUFFER_SIZE / numChannels);
if (samplesRead == 0) {
//输出缓存中未处理的数据
sonicFlushStream(stream);
} else {
//写入数据 并进行处理
sonicWriteShortToStream(stream, inBuffer, samplesRead);
}
if (!computeSpectrogram) {
do {
//读取数据
samplesWritten = sonicReadShortFromStream(stream, outBuffer,
BUFFER_SIZE / numChannels);
if (samplesWritten > 0 && !computeSpectrogram) {
writeToWaveFile(outFile, outBuffer, samplesWritten);
}
} while (samplesWritten > 0);
}
} while (samplesRead > 0);
// destroy
sonicDestroyStream(stream);
复制代码
创建好 sonic 流后,调用者只需要 sonicWriteShortToStream 向 sonic 输入数据,再从 sonicReadShortFromStream 读取数据即可,就像操作文件流一样。在处理文件到末尾时,由于 sonic 流中处理的有缓存数据,需要刷新缓存 sonicFlushStream。
sonic 变速不变调原理
sonic 中使用变速不变调最重要的一步是寻找基因周期。人在发浊音时,气流通过声门使声带产生张驰振荡式振动,这种声带振动的频率称为基频,相应的周期就称为基音周期(Pitch)。从声音的时域波形和语谱图观察基因周期如下:
时域波形标红的部分可以近似的认为是一个基因周期,对应语谱图为标黑的部分。从图中能看出基因周期的两部分语音有很大的相似性。准确的寻找基因周期一直以来都是一个难题,sonic 中使用的是 AMDF(平均幅度差函数法)方法。该方法极其简单,就是计算 2 个对比帧每个采样点幅度差之和。sonic 计算基因周期的方法为 findPitchPeriodInRange,代码如下:
static int findPitchPeriodInRange(short* samples, int minPeriod, int maxPeriod,
int* retMinDiff, int* retMaxDiff) {
int period, bestPeriod = 0, worstPeriod = 255;
short* s;
short* p;
short sVal, pVal;
unsigned long diff, minDiff = 1, maxDiff = 0;
int i;
//minPeriod maxPeriod为定义好的经验值,不同的采用率下会不同
for (period = minPeriod; period <= maxPeriod; period++) {
diff = 0;
s = samples;
p = samples + period;
for (i = 0; i < period; i++) {
sVal = *s++;
pVal = *p++;
diff += sVal >= pVal ? (unsigned short)(sVal - pVal) // diff就是AMDF
: (unsigned short)(pVal - sVal);
}
if (bestPeriod == 0 || diff * bestPeriod < minDiff * period) {
minDiff = diff;
bestPeriod = period;
}
if (diff * worstPeriod > maxDiff * period) {
maxDiff = diff;
worstPeriod = period;
}
}
*retMinDiff = minDiff / bestPeriod;
*retMaxDiff = maxDiff / worstPeriod;
return bestPeriod;
}
复制代码
在 range 范围内遍历每个帧与起始帧的 AMDF 值,值最小的帧与起始帧的距离则是基因周期。为了加速计算,sonic 统一将数据的采用率降低到了 4000。
寻找到基因周期后,便进行变速变调。
加速原理:
以基因周期 period = 60、inputbuffer 点为 0、采样率为 16000、speed = 1.5 为例:
1.根据 inputbuffer 的首个帧 在 range 范围内寻找到基因周期 peroid = 60
2.根据 speed 计算当前 inputbuffer 需要保存的点 。为 60 * (2 - 1.5)/(1.5-1) = 60
保存的点计算方式如下:
if (speed >= 2.0f) {
newSamples = period / (speed - 1.0f); //大于 2 没有数据进行保留了
} else {
newSamples = period;
stream->remainingInputToCopy = period * (2.0f - speed) / (speed - 1.0f);
}
3. 将 inputbuffer 中连续的 2 个基因周期进行合并输出到 outputbuffer 中,及 inputbuffer 中的 0-60、60-120 进行合并输出到 outputbuffer 中的 0-60。 合并时按照 1: 1/60 : 0 和 0: 1/60 : 1 的比重进行叠加。
4. 将 inputbuffer 中保存的 60 个点的数据拷贝到 outputbuffer 中的 60-120。保存的 60 个点为 inputbuffer 中的 120-180
5. 此时 inpoutbuffer 的 60+60+60 = 180 点数据变成了 outputbuffer 中的 60+60 = 120 点数据 完成了 1.5 倍加速
6. go on
减速原理
和加速大致一样,不同的是减速需要插入数据。
以基因周期 period = 60, inputbuffer 起始点为 0, 采样率为 16000,speed = 0.8 为例
1.根据 inputbuffer 的首个帧 在 range 范围内寻找到基因周期 peroid = 60
2.根据 speed 计算当前需要保存的点 为 60 * (2 * 0.8- 1)/(1-0.8) = 180
if (speed < 0.5f) {
newSamples = period * speed / (1.0f - speed); //小鱼 0.5 不需要进行保存
} else {
newSamples = period;
stream->remainingInputToCopy =
period * (2.0f * speed - 1.0f) / (1.0f - speed);
}
3. 将 inputbuffer 中 0:60 的数据输出到 outputbuffer 中,并将 60-120 0-60 进行合并。 合并时按照 1:1/60:0 和 0:1/60:1 的比重进行叠加。
4. 将 inputbuffer 中保存的 180 个点的数据拷贝到 outputbuffer 中。
5. 此时 inpoutbuffer 的 60+60+180 = 300 点数据变成了 outputbuffer 中的 60+60+60+180 = 360 点数据 完成了 0.8 倍减速
6. go on
从 sonic 的实现来看,相比于 soundtouch,其需要缓存的数据应该会更小,因为控制加减速变化的帧就是当前基因周期的下一个周期,在时域上是相邻的。另外 sonic 使用 AMDF 寻找基因周期,其算法复杂度应该要小于 soundtouch。加减速的核心函数为 changespeed
static int changeSpeed(sonicStream stream, float speed) {
short* samples;
int numSamples = stream->numInputSamples;
int position = 0, period, newSamples;
int maxRequired = stream->maxRequired;
/* printf("Changing speed to %f\n", speed); */
if (stream->numInputSamples < maxRequired) {
return 1;
}
do {
if (stream->remainingInputToCopy > 0) {
newSamples = copyInputToOutput(stream, position);
position += newSamples;
} else {
samples = stream->inputBuffer + position * stream->numChannels;
period = findPitchPeriod(stream, samples, 1);
#ifdef SONIC_SPECTROGRAM
if (stream->spectrogram != NULL) {
sonicAddPitchPeriodToSpectrogram(stream->spectrogram, samples, period,
stream->numChannels);
newSamples = period;
position += period;
} else
#endif /* SONIC_SPECTROGRAM */
if (speed > 1.0) {
newSamples = skipPitchPeriod(stream, samples, speed, period);
position += period + newSamples;
} else {
newSamples = insertPitchPeriod(stream, samples, speed, period);
position += newSamples;
}
}
if (newSamples == 0) {
return 0; /* Failed to resize output buffer */
}
} while (position + maxRequired <= numSamples);
removeInputSamples(stream, position);
return 1;
}
复制代码
sonic 变调不变速原理
变调不变速可以通过先进行变速不变调再进行升降采样来完成。sonic 采用的就是这个思想。sonic 会首先将改变音调的 pitch 值转化为 speed 和 rate 值。
float speed = stream->speed / stream->pitch;
rate *= stream->pitch;
复制代码
另外,变调变速原理使用的是 changeSpeed 函数,不在赘述。
最后
变速变调的开源代码我现在只发现了 sonic 和 soundtouch,现在广泛使用的还是 soundtouch,在分析源代码的时候,只是大致探究了其实现原理,对于两种算法变速变调后音频的音质还没有去做评估。文章中有什么不对的地方,希望大家一起讨论。
参考
https://github.com/waywardgeek/sonic
picola的原理
评论