写点什么

音频变速变调 -sonic 源码分析

用户头像
floer rivor
关注
发布于: 2021 年 05 月 09 日

概述

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

发布于: 2021 年 05 月 09 日阅读数: 46
用户头像

floer rivor

关注

还未添加个人签名 2021.04.24 加入

还未添加个人简介

评论

发布
暂无评论
音频变速变调-sonic源码分析