sonic音频变速不变调的原理分析

1.问题背景

        音频的原始pcm(Pulse Code Modulation)数据是由采样频率、通道数以及采样精度(位宽)而决定。人耳能听到的频率范围是[20H~20kHz],所以常见的音频采样率是44100HZ,即一秒内采样44100次,通道数一般为2, 代表双声道,而采样精度代表着每个“样本点”的大小,常用的大小为8bit16bit24bit位宽,一般是16bit 即2个字节。
        最简单的音频变速方法就是通过改变采样频率,比如音视频播放器中的 2 倍速,0.5 倍速播放。如果想要实现音频的2.0倍速播放,只需要每隔一个样本点丢一个点,即采样率降低一半。如果想要实现0.5倍速播放,只需要每隔一个样本点插入一个值为0的样本点。但是如果仅仅这样做,会造成声音的音调也发生变化,如一个大汉的粗犷之音变为一个娇柔少女的夹子音,这是我们无法接受的。

        目前成熟的商业化技术,一般想要实现变速不变调的功能都通过sonic或者sountTouch方法来处理pcm数据。

2.基础知识

2.1声音的三要素

        声音的三要素包括: 响度、音调、音色。在变速时,需要变的是音频的播放速度,同时要保持音调不变。

响度:

        响度代表声音的能量强弱,主要取决于振幅大小,声音的响度一般用声压来计量,声压的单位为帕(Pa),它与基准声压比值的对数值称为声压级,单位是分贝(db spl)。人耳对于响度的感知变化并不是线性的,且对低频和高频都不太敏感,对1000HZ-3000HZ的频率比较敏感。

音调:

        声波是有可以看作是有无数个不同频谱、振幅和相位的正弦波组成,音调的大小主要取决于声波基频的高低,不同乐器的基频不同,比如 bass的频很低,而军鼓的频率就比较高;钢琴键不同键的频率也不同,男生和女生的基频也不相同,女生声音的基频比男声要高。

音色:不同音色的声音,即使在相同响度和音调的情况下,也能让人区分开来。声音是由发声的物体的振动产生的。当发声物体的主体振动时会发出一个基音,同时其余各部分也有复合的振动,这些振动组合产生泛音。正是这些泛音决定了发生物体的音色,使人能辨别出不同的乐器甚至不同的人发出的声音。所以根据音色的不同可以划分出男音和女音;高音、中音和低音;弦乐和管乐等。所有泛音都比基音的频率高,但强度都相当弱。

2.2时域和频域

音频分析处理领域可以分为时域和频域
时域上表现为波形随着时间变化而变化。
频域分析则是首先对时域信号分帧、加窗、做stft(短时傅立叶变换)等处理,更方便的进行计算。比如把20ms-50ms的一个波形看作一个周期,进行分帧加窗处理,计算出该帧不同频率的响度值。

我们今天的主题变速不变调就是在频域上进行处理。

2.3浊音、基音周期的概念

        了解原理之前要先理解一下浊音的发音过程:来自肺部的气流冲击声门,造成声门的一张一合,形成一系列准周期的气流脉冲,经过声道(含口腔、鼻腔)的谐振及唇齿的辐射最终形成语音信号。故浊音波形呈现一定的准周期性。

        所谓基音周期,就是对这种准周期而言的,它反映了声门相邻两次开闭之间的时间间隔或开闭的频率

3.sonic原理分析

        Sonic变调不变速的主要原理通过使用波形相似叠加算法并采用AMDF(平均幅度差函数法)方法来寻找最相似的第三帧来实现变速不变调。

3.1 波形相似叠加(WSOLA)

核心算法思想如下:

  1. 图(a): 在原音频信号中取一帧(左边红色框),并加窗处理。

  2. 图(b): 在一个范围内(左边蓝色框)选取第二帧,这个帧的相位参数和第一帧(红色宽)的相位对齐。

  3. 图(c): 在另外一个范围(右上角蓝色大框)中查找和第二帧最相似的第三帧(右上角蓝色大框中的红色框)

  4. 图(d): 对第三帧进行加窗处理,然后和第一帧进行叠加。

那么如何寻找最相似的第三帧呐?
有两个波形相似叠加算法的实现,一个是Soundtouch,另外一个是Sonic,但它们在寻找最相似帧采用了不同的算法。其中Soundtouch采用了寻找相关峰算法来实现,而Sonic采用了AMDF(平均幅度差函数法)来实现。

3.2AMDF(平均幅度差函数法)

        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

保存的点计算方式如下:

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 倍加速

减速原理

和加速大致一样,不同的是减速需要插入数据0。

以基因周期 period = 60, inputbuffer 起始点为 0, 采样率为 16000,speed = 0.8 为例

1.根据 inputbuffer 的首个帧 在 range 范围内寻找到基因周期 peroid = 60

2.根据 speed 计算当前需要保存的点 为 60 * (2 * 0.8- 1)/(1-0.8) = 180

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 倍减速

加速、减速的核心函数代码如下:

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;
}

猜你喜欢

转载自blog.csdn.net/m0_67505927/article/details/131143953