FFT(Fast Fourier Transformer) 를 이용해 주파수 추출하기
음성 스트림으로부터 주파수를 추출하는 방법을 알아보자. FastFourierTransformer 라이브러리를 이용하려면 몇가지 기본 개념이 필요하다. 이해하기 어려운 용어나 수학적인 부분은 설명하지않는다.
PCM (Pulse Code Modulation)은 아날로그 오디오 신호를 디지털 형태로 변환하는 가장 일반적인 방법 중 하나다. 샘플링 방법에는 여러 가지 유형이 있지만 사실상 PCM 이 표준이며, 오디오 CD, DVD, 그리고 대부분의 디지털 오디오 시스템에서 사용된다.
PCM 은 샘플링(Sampling) - 양자화(Quantization) - 인코딩(Coding) 순서로 진행된다.
1. Sample Rate
이는 오디오 스트림을 사용해 개발하려고 할때 자주보게 되는 값이다. 오디오를 처리하기 위해 오디오 아날로그 신호를 디지털 신호 형태로 변환해야 한다. 이때, 아날로그 신호에서 일정 시간 간격으로 값을 측정하고 기록하는 과정을 거치는데 이 측정 시간 간격(샘플링 포인트)을 의미하는 것이 샘플 레이트(Sample Rate)다.
Sample Rate: 초당 샘플의 개수 | 단위: 헤르츠(Hz) 또는 샘플/초(Samples per Second)
샘플 레이트가 높을수록 더 많은 샘플을 취하게 되므로, 변환된 디지털 신호가 원본 아날로그 신호를 더 정확히 재현할 수 있다. 이러한 변환 과정을 샘플링(Sampling)이라고 하며, 오디오 신호가 얼마나 자주 샘플링되는지를 나타내는 측정치를 샘플 레이트라고 한다.
더 쉽게 말하자면, 아날로그 정보를 쪼개서 디지털 정보로 변환하는데 이 과정을 샘플링이라 하고, 이때 초당 샘플 레이트 개수만큼 쪼개서 디지털 정보로 변환한다. 초당 많은 개수로 오디오 샘플을 나눌 수록 더 많은 샘플을 취하게 되므로 더 원본에 가까워 진다.
일반적인 샘플레이트 값은 44.1kHz(오디오 CD), 48kHz(프로 오디오), 96kHz 및 192kHz(고해상도 오디오)다.
왜 보통 44.1kHz를 쓰는가?
Nyquist-Shannon 샘플링 정리: 샘플링할 때 정보 손실을 방지하려면 예상되는 가장 높은 신호 주파수의 최소 두 배 속도로 샘플링해야한다. 인간 최대 가청 주파수인 22kHz 의 두배인 44 kHz를 사용해야 정보 손실 없이 정확히 샘플링할 수 있다. 만약 최대 진동수 두배보다 작게 샘플링하면 기존에 없던 신호가 생기는 앨리어싱(Aliasing)이 발생할 수 있다. 여기서 샘플링 속도(샘플레이트) 절반을 나이퀴스트(Nyquist) 주파수라고 한다. 44 kHz에서 나이퀴스트 주파수는 22 kHz다.
2. Sample Size
샘플 사이즈(Sample Size)는 비트 깊이(Bit Depth)라고도 한다.
Sample Size(Bit Depth): 디지털 오디오에서 한 샘플을 표현하는 데 사용되는 비트 수
저장할 수 있는 데이터 양이라고 생각해도 된다.
- 16-bit: 65,536 values
- 24-bit: 16,777,216 values
- 32-bit: 4,294,967,296 values
비트 깊이는 샘플링 과정 중 샘플의 아날로그 값을 얼마나 세밀하게 디지털 값으로 변환할 수 있는지를 결정한다. 비트 깊이가 클수록 더 많은 디지털 값을 표현할 수 있기 때문에, 더 정밀한 오디오 정보를 포착할 수 있다. 때문에 샘플 사이즉 커질수록 더 많은 정보를 담아 오디오 해상도가 높아지고, 파일 크기가 커진다.
오디오 비트 깊이를 늘리고 오디오 샘플 레이트(속도)를 높이면 아날로그 파동을 재구성하기 위한 총 지점이 더 많이 생성되어 아날로그에 더 가까워질 수 있다.
동적 범위(dynamic range)
비트 깊이는 신호의 동적 범위(dynamic range) 에도 영향을 준다. 16비트 디지털 오디오의 최대 동적 범위는 96dB인 반면, 24비트 깊이는 최대 144dB를 제공한다. 44.1kHz의 샘플링 속도에 대한 16비트의 비트 깊이는 일반인의 가청 주파수와동적 범위를 재현하기에 충분하므로 표준 CD 형식이 되었다.
비트 깊이와 양자화의 관계
아날로그 신호는 연속적인 값을 가지고 있으며, 이를 디지털로 변환하기 위해 샘플링 지점에서의 신호 강도를 가장 가까운 디지털 값으로 "양자화"한다. 이 과정에서 선택할 수 있는 디지털 값의 수는 비트 깊이에 의해 결정된다. 예를 들어, 16비트 오디오는 각 샘플링 포인트에서 65,536(2^16)가지의 다른 값을 가질 수 있다.
양자화 과정에서 아날로그 값은 가장 가까운 디지털 값으로 반올림되기 때문에 완벽하게 일치하는 디지털 값이 항상 존재하지는 않을 수 있다. 이로 인해 발생하는 오류를 "양자화 오류" (noise floor) 라고 하며, 이는 신호에 추가적인 노이즈를 생성할 수 있다.
이때, 비트 깊이가 높을수록 양자화 과정에서 선택할 수 있는 값의 범위가 넓어지므로, 더 정확한 값으로 아날로그 신호를 표현할 수 있다.
이로인해 비트 깊이가 높을수록 양자화 오류는 줄어들고, 노이즈의 영향도 감소하며 필요한 데이터량도 증가하여 파일 크기가 커진다.
인코딩 단계에서는 양자화된 값을 디지털 데이터 스트림으로 인코딩하여 저장하거나 전송한다.
그럼 이 Sample Size 와 Sample Rate(Bit 크기) 는 몇으로 해야하는 것일까?
어떤 오디오 녹음을 쓰느냐에 달렸다.
- 자바의 AudioFormat 을 이용해 오디오 녹음을 진행하면, 노트북 마이크를 이용한다. 나는 M1 맥 OS를 사용하는데, 나의 노트북의 경우 sample size 16 비트로만 녹음이 되고, 32 비트부터는 녹음이 되지 않는다. 샘플링 속도는 32000, 48000, 16000 전부 가능하다.
- Web Audio API 를 이용해 오디오 녹음을 진행하는 경우, 아래와 같이 출력해보면 브라우저에서 지원하는 sample size와 sample rate를 알 수 있다. 크롬에서 출력해보면 48000 과 16비트가 나온다.
this.#stream = await navigator.mediaDevices.getUserMedia({ audio: true });
let track = this.#stream.getAudioTracks()[0];
console.log(track.getCapabilities());
- AudioWorkletNode를 사용하면 오디오 스트림이 Float32Array 로 나온다. 이 경우에는 데이터 손실이 없도록 sample size를 32비트로 한다.
정리하자면, 정해진 값은 없고 본인이 오디오를 사용하는 환경이 제공하는 값에 따라 적절한 값을 넣어준다.
윈도우 크기와 FFT 사이즈에 대한 원본 내용은 여기 에서 확인할 수 있다.
3. Window Size (시간 도메인)
FFT를 이용해 시간 도메인 <-> 주파수 도메인으로 변환할 수 있다.
FFT 에서 주파수 분석을 수행할 때 신호를 작은 시간 간격(윈도우)으로 나누어 각 간격에 대한 주파수 정보를 얻는다. 이 각 간격은 특정 지속 시간을 가지며, 이 시간 동안의 신호를 분석하여 해당 기간 동안의 주파수 성분을 알아낸다.
TR(Temporal Resolution) or 지속시간(T) = Window Size / SR(Sample Rate)
예를 들어 44.1 kHz 의 샘플 레이트(샘플링 속도) 에 1024 윈도우 사이즈를 사용하면
- T = 1024/44100 ≈ 0.023, 23 ms 지속시간이 된다.
같은 샘플 레이트에 4096 크기의 윈도우 사이즈를 사용하면
- T = 4096/44100 ≈ 0.093, 93 ms 지속시간이 된다.
이는 FFT 에서 신호를 분석할 때 각각 23 ms, 93 ms 지속 기간만큼씩 잘라서 각 조각에 대해 FFT를 수행한다는 것이며, 따라서 윈도우 사이즈가 4096일때 시간 해상도가 덜 정밀하다고 할 수 있다. 이 시간 해상도는 뒤에 설명할 주파수 해상도에 반비례한다.
탐지 가능한 최저 주파수(F0)는 윈도우의 크기(지속 시간)에 의해 결정된다.
F0 = 5 × ( SR / Window Size)
Window Size = 5 × SR / F(Signal)
샘플 레이트(샘플링 속도)가 44.1kHz 일때 최저 주파수에 따른 윈도우 사이즈는 다음과 같다.
- 최저 주파수가 440Hz 일때, WS = 5 × 44100 / 440 ≈ 501
- 최저 주파수가 300Hz 일때, WS = 5 × 44100 / 300 ≈ 735
- 최저 주파수가 100Hz 일때, WS = 5 × 44100 / 100 ≈ 2205
윈도우 크기와 주파수 해상도(Frequency Resolution)
주파수 해상도(Frequency Resolution)는 분석 윈도우 내의 "빈(bin)"의 수에 의해 결정된다. 빈의 수는 실제로 FFT 크기다.
N(Bins) = Window Size / 2
예를 들어, 1024 샘플 윈도우는 512개의 빈을 갖게 된다.
- 윈도우의 샘플 수는 빈의 수로 나뉜다. 즉, 빈이 많을수록 주파수 범위의 슬라이스가 더 많아져 더 정밀해진다. 이에 빈의 수는 주파수 분석의 해상도를 결정한다.
- 이때 빈의 수는 512, 1024, 2048, 4096 등 2의 거듭제곱이어야 한다.
- 윈도우 크기 역시 일반적으로 2의 거듭제곱으로 정의되지만, 필수사항은 아니다.
주파수 해상도는 두가지 방법으로 구할 수 있다.
FR = Fmax(or Nyquist Frequency) / N(Bins) = SR / Window Size
샘플 레이트(샘플링 속도)가 44.1kHz 일때 SR=44100 Hz, F(max) = 22050 Hz, 1024 윈도우 크기 (512 빈)
- FR = 44100 / 1024≈43.066
- FR = 22050 / 512≈43.066
이는 FFT를 사용하여 주파수 분석을 할 때, 전체 주파수 범위를 43.066 Hz씩 나눈 512개의 구간으로 분석한다는 의미한다.
- 첫 번째 빈: 0 Hz ~ 43.066 Hz
- 두 번째 빈: 43.066 Hz ~ 86.132 Hz
- ...
- 512번째 빈: 22006.934 Hz ~ 22050 Hz
4096 윈도우 크기(2048 빈)를 선택하면,
- FR = 44100 / 4096 ≈ 10.76
- FR = 22050 / 2048 ≈ 10.76
FFT를 사용하여 주파수 분석을 할 때, 전체 주파수 범위를 10.76 Hz씩 나눈 2048개의 구간으로 분석한다는 의미로 주파수 해상도가 더 정밀해진다.
- 첫 번째 빈: 0 Hz ~ 10.76 Hz
- 두 번째 빈: 10.76 Hz ~ 21.52 Hz
- ...
- 2048번째 빈: 22039.24 Hz ~ 22050 Hz
최대 주파수 해상도
윈도우 내 빈의 수는 16384를 초과해서는 안 된다. 이는 1.35 Hz 주파수 해상도에 해당하며, 이는 매우 높은 해상도이다.
정리하자면, 윈도우 크기가 커지면 주파수 해상도는 더 정밀해지지만 시간 해상도는 덜 정밀해진다. 반대로, 윈도우 크기가 작아지면 시간 해상도는 더 정밀해지지만, 주파수 해상도는 덜 정밀해진다. 두 해상도는 상호 간의 트레이드오프 관계에 있다.
4. FFT Size (주파수 도메인)
N(Bins)= FFT Size / 2
이는 윈도우 사이즈와 같은 개념이나 사용되는 도메인이 다르다.
FFT Size VS Window Size
FFT 는 시간 도메인 <-> 주파수 도메인으로 변환하는 것이다. 현재 나는 음성 데이터로부터 주파수를 얻는 것이기 때문에 시간 도메인 -> 주파수 도메인으로 가게된다. 따라서 입력 데이터 쪽에서 사용되는 것이 Window Size, 주파수에서 사용되는 것이 FFT Size 다.
이 때문에 FFT 사이즈는 Window 사이즈보다 크거나 같을 수 있다. 가장 일반적인 사이즈는 1024, 2048 및 4096 다 . 너무 크면 계산이 오래 걸린다.
윈도우 크기와 FFT 크기가 같은 경우
신호 (시간 도메인) -> [윈도우 크기: 1024] -> FFT 수행 -> [주파수 도메인: 1024 포인트]
윈도우 크기와 FFT 크기가 다른 경우
신호 (시간 도메인) -> [윈도우 크기: 1024] -> 제로 패딩 -> [FFT 크기: 4096] -> FFT 수행 -> [주파수 도메인: 4096 포인트]
윈도우 크기가 FFT 크기보다 작으면, 0으로 채워서 가장 가까운 2의 거듭제곱으로 만든다. 제로 패딩은 입력 신호의 정보를 증가시키지 않지만, 계산된 샘플 수를 증가시킨다.
시간 해상도 vs. 주파수 해상도
- 시간 해상도 (Temporal Resolution, TR): 신호를 시간 축에서 얼마나 잘 분해할 수 있는지를 나타낸다. 윈도우의 지속 시간에 의해 결정된다.
- 주파수 해상도 (Frequency Resolution, FR): 주파수 축에서 얼마나 잘 분해할 수 있는지를 나타낸다. FFT 크기와 샘플링 속도에 의해 결정된다.
이제 FFT 라이브러리를 사용해보자
실시간으로 오디오 스트림을 받아 처리하려고 한다.
FastFourierTransformer fft = new FastFourierTransformer(DftNormalization.STANDARD);
int streamLength = this.stream.length;
for (int start = 0; start <= streamLength; start += this.fftSize) {
ArrayList<Double> row = new ArrayList<>();
int end = (int)Math.min(start + this.fftSize, streamLength);
int size = (end != streamLength)? this.fftSize: this.fftSize + streamLength - (start + this.fftSize);
double[] segment = new double[size];
System.arraycopy(this.stream, start, segment, 0, size);
if (end == streamLength) segment = AudioStreamFormatter.padArrayToNextPowerOfTwo(segment);
Complex[] complexResult = fft.transform(segment, TransformType.FORWARD);
double maxMagnitude = 0;
double maxFrequency = 0;
for (int i = 0; i < complexResult.length / 2; i++) {
double magnitude = complexResult[i].abs();
if (magnitude > maxMagnitude) {
maxMagnitude = magnitude;
maxFrequency = i * this.sampleRate / this.fftSize;
}
}
}
실시간으로 오디오 스트림은을 받아와 처리하기 때문에 길이가 1024/2048.. 등으로 정해져 있지 않다. fft 사이즈로 스트림을 쪼개서 fft.transform() 을 호출하고, 마지막 부분은 제로패딩을 주어 채워준다. fft 사이즈는 1024 로 했다. 위에서 설명한 것을 기반으로 하면, 마지막 부분에서는 윈도우 사이즈가 FFT 사이즈보다 작아서 제로패딩을 채워주는 것이라고 말할 수 있다.
FFT 사이즈 만큼 쪼갠 후 해당 구간에서의 주파수는 강도(magnitude) 가 제일 쎈 빈 에서의 주파수가 된다.
bin spacing = sampling rate / FFT frame size
bin frequency = bin index * bin spacing
빈 구간 별 주파수 추출 자세한 내용은 다음을 참고하면 된다.
https://stackoverflow.com/questions/7674877/how-to-get-frequency-from-fft-result
How to get frequency from fft result?
I have recorded an array[1024] of data from my mic on my Android phone, passed it through a 1D forward DFT of the real data (setting a further 1024 bits to 0). I saved the array to a text file, and
stackoverflow.com
https://dobrian.github.io/cmp/topics/fourier-transform/1.getting-to-the-frequency-domain-theory.html