Loading
2022. 8. 31. 01:45 - lazykuna

Audio Data의 Pitch를 변경해보자

예전에 음향 데이터를 열심히 가공하면서 찾아봤던 것들을 언젠가 이에 대해 제대로 다뤄보고 싶었으나 계속 미루고 미루다가, 이제서야 정리해서 글을 좀 써 둡니다.

먼저 이 글은 음원 데이터 분석 및 가공을 전문적으로 한 사람이 쓴 글이 아님을 미리 밝힙니다. 적힌 내용이 유효한지에 대해서는 책임질 수 없으며, 간혹 Outdated된 정보가 있을 수 있습니다. 제대로 된 정보를 얻으려면 관련 서적을 읽어보시는 것을 추천드립니다.

FFT를 왜함?

먼저 이야기에 앞서, 어떤 오디오 파일의 피치(음 높이)를 바꾼다고 가정해 봅시다. 대충 솔# 키로 부르던 노래를 미♭ 으로 내린다는 상황이 있다고 치죠. (맞는지 아닌지 모릅니다 걍 대충 씀)

Pitch를 바꾼다는 건, 오디오 데이터의 주파수를 조금씩 낮게 만들어서 다시 써야 한다는 이야기입니다. 즉 주파수의 진동수를 낮춰야 한다는 의미인데…

예를 들어, 20Hz+100Hz로 된 오디오의 피치를 2배 올리면, 40Hz+200Hz의 진동수를 가진 음파와 같은 결과를 만들게 됩니다

아주 대충 만든 그림…

그런데 생각해보면 PCM 데이터의 pitch만을 바꾸는 일은 간단합니다. 단순히 기존샘플의 데이터를 stretch 하면 되거든요. 데이터를 짧게 만들면 Pitch가 올라가고, 늘이면 내려갑니다. (품질을 위해서는 interpolate 등의 작업을 추가로 해야겠지만…)

하지만 이 방식의 문제는 음악의 전체 길이(속도)가 같이 바뀐다는 점입니다! 따라서 다른 방법을 사용하거나, 속도를 변경할 방법을 찾아야 합니다.

여기서 다른 방법은 FFT로 음원을 재생성하는 방법입니다.

1. FFT로 pitch 변경하기

누가 잘 정리해놔서 그대로 긁어다 둡니다.

  • Convert the audio data to the format required by FFT (e.g. int -> float, with separate L/R channels);
  • Apply suitable window function (e.g. Hann aka Hanning window)
  • Apply FFT (NB: if using typical complex-to-complex FFT then set all imaginary parts in the input array to zero);
  • Calculate the magnitude of the first N/2 FFT output bins (sqrt(re*re + im*im));
  • Optionally convert magnitude to dB (log) scale (20 * log10(magnitude) or 10 * log10(re*re + im*im));
  • Plot N/2 (log) magnitude values.

일단 오디오 파일을 디코딩 후 L/R 채널 구분하여 float 형태로 메모리에 읽어들입니다. 그 다음에 FFT를 수행할 window에 대해서 값들(샘플)을 가져오고, 이 샘플에 대해서 FFT 수행 후 음원특성을 고려하여 log 해 주면 분석이 됩니다.

  • Window size는 freq 대비 최소 2배는 되어야 함 — 허수 특성상 180도 대칭이기 때문에, Fs/2 이후의 Freq 값은 의미가 없는 값임

(비록 경시 알고리즘 설명이지만) FFT 자체는 이 블로그에 굉장히 세세하게 잘 설명되어 있다고 보는데, 간단히 말하면 FFT 수행을 위해서 필요한 input은…

  • Fs / N 개 만큼의 샘플 (이 값들에 대해서 FFT를 수행)

이고, output은 DFT, 즉 허수라는 점을 인지하고 있어야 한다. 그래서 FFT를 수행하면 실제로 얻는 것은 허수임.

그리고 이 허수를 freq로 변환하면 sample rate * Fs / N 주파수 변환이 완료됨.

example)

Sample 44100Hz, step 4
=> 11025 samples
=> Do FFT
=> 4 ~ 22050Hz FFT results with step 4
  • FFT의 상수 계수를 구하는 지점에서 실제 샘플 데이터를 사용할 수 있습니다. 이를 통해 계수를 알아낼 수 있음

    • TODO: 이 부분 세부 알고리즘은 나중에 더 찾아봐야 할 것 같습니다

    • istep이 2(의 배수)인 이유는 실수부/가수부 부분 때문입니다. DFT에서 사용되는 요소 그대로인 것을 확인할 수 있음.

    • istep을 2의 승수로 올리는 이유는 결국 w^2 구하려고…

    • tempr, tempi를 보면 어떤 ratio만큼 값을 돌려서 data[k]에 쌓아놓고 있는 것을 볼 수 있습니다. 그리고 잘 보면 k가 기준값의 2배(2 * istep) 해서 증가하고 있는 것을 볼 수 있습니다. 이로 보아 k가 짝수항, j가 홀수항이라는 것을 알 수 있고, 대충 아래 식을 2중 for loop로 돌리고 있다는 걸 볼 수 있겠네요.

    • 기본적으로 주파수 성분은 2의 승수 단위로만 측정이 되지만, 이를 m 번 반복하는 과정을 거쳐서 신호를 저장합니다. 예컨데, offset 0에서의 50Hz 성분 측정, Offset 에서의 50Hz 성분 측정 … 같이요.

    • 재미있는 점은, 컴퓨터에서의 FFT는 sin/cos를 직접 계산하지 않고 sinTable/cosTable을 이용합니다. 물론 반복되는 2pi * n / N 구하는 것보다야 이쪽이 훨씬 효율적인 건 맞으니…

    • c# FFT 예제 코드

    • java FFT 예제 코드

    • FFT 설명 포스트

  • 음원 파일 특성상 계수 계산에서 오차가 발생하게 되는데, 이때는 결과값이 아니라 계수 average를 취한다고 합니다. (결과값은 애당초 소리의 단위(베버-페히너의 법칙)로 인해서 Log 처리해야 하기 때문에…)

핵심 코드만 돚거해서 아래 정리해 둡니다.

// 가져온 코드입니다.
// https://stackoverflow.com/questions/17416112/apply-fft-on-pcm-data-and-convert-to-a-spectrogram

while (n > mmax) {
  var istep = 2 * mmax;
  for (var m = 0; m < istep; m += 2) {
    var wr = cosTable[tptr];
    var wi = sign * sinTable[tptr++];
    for (var k = m; k < 2 * n; k += 2 * istep) {
      var j = k + istep;
      var tempr = wr * data[j] - wi * data[j + 1];
      var tempi = wi * data[j] + wr * data[j + 1];
      data[j] = data[k] - tempr;
      data[j + 1] = data[k + 1] - tempi;
      data[k] = data[k] + tempr;
      data[k + 1] = data[k + 1] + tempi;
    }
  }
  mmax = istep;
}

2. 음원의 속도만 바꿔 보자

그런데 굳이 주파수 성분 분석을 통해서 다시 음원을 만드는 것보다, 더 간단하게 할 수 있지 않을까요? 위에서 말했듯이, “그냥 Stretch 해서 피치만 바꾼 후에, Window 크기(속도)를 어떻게든 바꾸면 되지 않을까?” 이라고 물어볼 수 있습니다.

되긴 됩니다! 물론 FFT 방식에 비해 소리가 깔끔하진 못해도, 훨씬 적은 프로세서 파워로 원하는 결과를 얻을 수 있다는 특징이 있습니다. 모 게임이 실시간 사운드 피치 변경할 때 이런 식으로 작업한 것으로 알고 있는데, 소리 자체는 gittering이 있는 좋은 품질이 아니었지만 그걸 감안했을 때 생각보다 괜찮았던 기억이 있습니다.

접근 방법이야 다양하겠지만, 대충 이런 느낌으로 Strectch(resample) 후 Window 패턴을 복제해서 길이를 맞추어 속도를 유지할 수 있습니다.

  • 이 방식도 세부적인 후처리 trick을 쓰면 (window overlapping사이에 Fade를 넣는다던가…) 훨씬 괜찮아지기는 합니다.
  • FFT로 Pitch 조정 + Stretch를 수행하는 방법도 있습니다.

TODO: 실제로 만들어보기.

이미 관련 라이브러리가 있기는 하다

FMOD라는 라이브러리에서는 Audio loading까지도 지원하고, OpenAL/libav와 같은 유명한 라이브러리에서도 지원하고 있습니다. 최근 찾아보니 libmixed 라는 mixing library에서도 기능을 제공하고 있습니다. scipy 에서도 오디오 FFT 분석 아주 쉽게 할 수 있고…

굳이 바퀴를 재발명할 필요는 없지만, 가끔 바닥부터 알고자 하는 변태들이 있다면 도움이 될 것 같네요.

'개발 > Algorithm' 카테고리의 다른 글

Parser에 대해서 알아보자  (0) 2022.09.04
자주 나오는 대회용 알고리즘 정리  (0) 2022.08.30
몇 가지 Greedy  (0) 2022.07.29
최소 공통 조상 구하기 (LCA)  (0) 2022.07.28
K개의 구간합 구하기  (0) 2022.07.28