概述
在上一篇博文音频变速变调原理及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的原理
评论