본문으로 건너뛰기

© 2026 Molayo

YouTube요약2026. 06. 10. 13:03

코딩 어드벤처: 오디오 분석하기

요약

Sebastian Lague가 이산 푸리에 변환(DFT)을 사용하여 오디오 신호를 주파수, 위상, 진폭으로 분해하고 재구성하는 과정을 설명합니다. 특정 개수의 주파수만을 사용하여 신호를 다시 합성했을 때 발생하는 음질 변화와 푸리에 변환의 특성을 다룹니다.

핵심 포인트

  • 이산 푸리에 변환(DFT)을 통한 신호 분해 구현
  • 주파수, 위상, 진폭의 개념과 오디오 합성 원리
  • 사용된 주파수 개수에 따른 오디오 재구성 품질 차이
  • 시간에 따라 변하는 진폭을 처리하는 푸리에 변환의 한계

비디오: 코딩 어드벤처: 오디오 분석하기
채널: Sebastian Lague
길이: 21분 27초
출처: 자막 (수동, en-GB)

안녕하세요 여러분, Coding Adventures의 새로운 에피소드에 오신 것을 환영합니다. 오늘은 오디오를 분석하기 위한 작은 도구를 만들어보고자 합니다.

소리는 — 우리가 과거에 매우 조잡하게 시뮬레이션했던 것처럼 — 압력의 파동 (waves of pressure) 형태로 전달되며, 이 압력을 시간에 따라 그래프로 나타내면 이와 같은 일종의 신호 (signal)를 얻게 됩니다.

우리는 또한 이산 푸리에 변환 (Discrete Fourier Transform, DFT)을 구현했는데, 이를 통해 신호를 일정한 주파수 (frequency), 위상 (phase), 그리고 진폭 (amplitude)을 가진 개별 파동들로 분해할 수 있습니다.

여러분도 분명 알고 계시겠지만, 주파수 (frequency)는 단순히 파동이 1초 동안 위아래로 몇 번 움직이는지를 나타내며, 우리가 인지하는 음높이 (pitch)를 결정합니다. 예를 들어 여기 150 Hertz 파동이 있는데, 이는 상당히 낮은 웅웅거림을 들려주며, 여기는 350 Hertz입니다.

그다음 진폭 (amplitude)은 소리가 얼마나 크게 들리는지에 영향을 미치며, 이는 단순히 기준선으로부터 파동의 최대 높이로 측정됩니다.

마지막으로 위상 (phase)은 각도이며, 본질적으로 파동을 옆으로 이동시키는 역할을 합니다. 이것 자체만으로는 그리 흥미롭지 않지만, 여러 주파수가 합쳐질 경우 그들의 위상은 파동의 형태에 큰 영향을 미칠 수 있습니다.

어쨌든, 앞서 말했듯이 신호를 개별 파동으로 분해하는 것은 소리를 더 잘 이해하거나, 파동을 다시 합치기 전에 예를 들어 짜증 나는 고주파 (high frequency)를 제거하는 것과 같이 소리를 수정하는 데 매우 유용할 수 있습니다.

우리는 여기 있는 "Hello Everyone"라는 음성 녹음과 같이 훨씬 더 복잡한 신호들을 가지고 이런 작업을 시도해 보았습니다. 즉, 그 안에 포함된 다양한 주파수 (frequencies)의 진폭 (amplitudes)을 살펴보고, 가장 두드러진 주파수들의 아주 작은 부분 집합 (subset)만 사용하여 신호를 재구성해 보려고 시도한 것입니다.

이 코드는 입력 오디오를 우리가 만든 작은 푸리에 함수 (fourier function)에 통과시킨 다음, 주파수들을 진폭이 높은 순서대로 정렬하고, 그중 지정된 개수만큼을 가져오는 방식으로 수행됩니다. 이렇게 가져온 주파수들은 여기서 새로운 신호를 구축하는 데 사용되는데, 이는 주어진 주파수 (frequencies), 위상 (phases), 그리고 진폭 (amplitudes)을 가진 파동 (waves)들을 생성한 다음 그것들을 모두 합산함으로써 이루어집니다.

그럼 단 5개의 주파수만 포함되었을 때 오디오가 어떻게 들리는지 빠르게 들어봅시다. 기본적으로 그냥 이상한 저음의 떨림 소리처럼 들립니다. 그리고 여기 50개를 사용했을 때입니다.... 이것도 그다지 훨씬 낫지는 않네요. 그리고 여기 500개입니다. 이것은 마침내 최소한 말소리로 식별할 수 있는 수준이지만, 품질은 여전히 상당히 끔찍합니다.

하지만 여기까지 오기 위해 그렇게 많은 주파수가 필요했던 이유는, 개별 주파수 중 그 어느 것도 단독으로는 음성에서 발생하는 실제 소리에 대응하지 않기 때문입니다.

제가 의미하는 바를 조금 더 잘 설명하기 위해, 아마도 처음에는 무음으로 시작했다가 잠시 동안 8Hz로 진행된 후, 잠시 12Hz로 바뀌었다가 다시 무음이 되는 매우 단순한 신호를 생각해 봅시다.

우리는 단 2개의 주파수만으로 이것을 만들었지만, 그것들은 켜졌다 꺼졌다를 반복합니다. 즉, 시간의 흐름에 따라 진폭 (amplitudes)이 변하고 있다는 뜻인데, 이는 푸리에 변환 (fourier transform)이 이해하지 못하는 개념입니다. 따라서 전체 신호를 입력하면, 푸рье 변환은 가장 두드러진 주파수가 실제로는 10Hz라고 말할 것입니다. 그다음은 12Hz, 그다음은 11Hz, 그다음은 8Hz, 그리고 훨씬 더 많은 주파수들이 뒤따를 것입니다.

우리가 볼 수 있듯이, 우리의 녹음이 매우 단순해 보였을지라도, 그것이 시간에 따라 변한다는 사실은 신호 전체가 우리가 의도적으로 입력한 것보다 훨씬 더 많은 주파수 (frequencies)를 포함하고 있음을 의미합니다.

하지만 우리가 할 수 있는 일은, 전체 녹음 데이터를 푸리에 변환 (Fourier transform)에 통째로 넣는 대신, 이를 더 작은 세그먼트 (segments)로 잘게 나누어 개별적으로 정밀하게 조사하는 것입니다. 그렇게 하면 함수가 전체를 한 번에 표현하기 위해 필요한 추상적인 주파수들을 모두 찾는 대신, 각 시점에 실제로 존재했던 주파수들만을 이상적으로 감지할 수 있을 것입니다.

그러니 다시 음성 녹음으로 돌아가서, 이제 이것을 작은 세그먼트들로 슬라이싱 (slicing) 해보고, 각각을 개별적으로 푸리에 변환 (Fourier transform)에 통과시켜 보겠습니다.

참고로 이 과정은 "단시간 푸리에 변환 (Short-time Fourier transform)"이라고 알려져 있습니다. 저는 코드에서 두 가지 작은 구조체 (structures)로 시작했습니다. 하나는 단일 세그먼트의 주파수 데이터를 보유하고 해당 세그먼트가 신호의 어느 위치에 있는지 추적하기 위한 것이며, 다른 하나는 샘플링 레이트 (sample rate) 및 원본 신호의 샘플 수와 함께 이 모든 세그먼트들을 단순히 담아두기 위한 것입니다.

이 모든 것을 만들기 위해, 여기 신호를 입력받고 각 세그먼트가 포함해야 할 원하는 샘플 수를 받는 함수가 있습니다. 따라서 우리는 신호를 훑으며 해당 크기의 세그먼트들을 가져와 이산 푸리에 변환 (Discrete Fourier transform)을 통과시키고, 거기서 얻은 주파수 데이터를 우리의 세그먼트 구조체에 저장할 수 있습니다.

그러면 이 세그먼트들은 편의를 위해 여기 있는 우리의 작은 결과 구조체 (result structure)에 최종적으로 한데 모이게 됩니다.

이제 이것을 빠르게 테스트해 보기 위해 제가 하고 싶은 것은, 여기서 반환되는 결과 (result) 내의 각 세그먼트 (segment)를 루프 (loop)로 돌면서, 해당 세그먼트들의 주파수 데이터 (frequency data)로부터 원래 신호 (original signal)의 각 부분을 재구성 (reconstruct)하는 것입니다.

좋습니다. 우선 세그먼트당 48,000개의 샘플 (samples)로 실행해 보았는데, 이는 실제로 이 1초짜리 클립에 있는 모든 샘플입니다. 따라서 단조롭게도 단 하나의 세그먼트만 존재하게 됩니다. 참고로 하단에는 세그먼트를 구성하는 모든 주파수 (frequencies)의 진폭 (amplitudes)도 나와 있습니다.

여기 상단의 정보를 자세히 살펴보면, 예를 들어 세그먼트 지속 시간 (segment duration)이 1,000밀리초 (milliseconds)인 것을 볼 수 있습니다. 이는 우리가 신호 내에서 특정 주파수가 언제 존재하는지 파악하려고 할 때, 그보다 더 정밀하게 시간을 좁힐 수는 없음을 의미합니다.

마지막으로, 푸리에 변환 (Fourier transform)은 주어진 샘플 수의 절반만큼의 주파수를 분석할 수 있습니다. 이 경우에는 24,000개가 되겠네요. 이는 이 녹음이 포함할 수 있는 최대 주파수 (maximum frequency)이기도 하며, 따라서 우리가 돌려받는 주파수 정보는 여기에서 1 Hz 간격이 될 것입니다.

최대 주파수가 존재하는 이유는 아마 기억하시겠지만, 파동이 얼마나 빠르게 진동하는지 포착하기 위해서는 파동의 주기 (cycle)당 최소 두 개의 샘플이 필요하기 때문입니다. 그렇지 않으면 에일리어싱 (aliasing) 문제에 직면하게 되는데, 이는 고주파 (high frequency)가 녹음에서 저주파 (low frequency)로 잘못 나타나는 현상을 말합니다.

어쨌든, 여기서 입력 신호 (input signal)를 재구성한 것을 빠르게 들어보겠습니다. — 원본과 동일합니다.

하지만 이제 이것을 약 10개의 세그먼트로 나누어 보겠습니다. 그러면 세그먼트당 4,800개의 샘플이 되며, 각 세그먼트의 길이는 100밀리초가 됩니다.

이제 우리는 더 나은 '시간 해상도 (time resolution)'를 갖게 되었지만, 우리가 '주파수 해상도 (frequency resolution)'라고 부르고자 하는 것은 그에 상응하여 더 거칠어졌습니다.
그것이 품질에 어떤 영향을 미치는지 확인하기 위해 소리를 한번 들어보겠습니다...
HEHEHEHEHE
ELELELELELEL
LOLOLOLOLOLOLO
EHEHEHEHEHEHE
VRVRVRVRVRVRVR
RYRYRYRYRYRYRYRY
ONEONONONON
N-N-N-N-N-N-N
좋아요, 꽤 미묘하지만 무언가 약간 잘못된 것처럼 들리네요.
아, 이것은 전체가 아니라 세그먼트당 샘플 수여야 합니다. 그럼 이것을 수정해서 다시 시도해 보겠습니다 — 이제 우리의 재구성 (reconstruction) 결과는 다시 원본과 동일하게 들립니다.
계속해서 이번에는 100개의 세그먼트로 나누어 보겠습니다. 각 세그먼트의 길이를 단 10밀리초 (milliseconds)로 만들되, 주파수 사이의 간격은 100Hz로 설정하겠습니다. 이것을 들어보면 — 재구성은 여전히 완벽합니다.
하지만, 만약 우리가 극단까지 간다면 어떨까요 — 세그먼트당 단 하나의 샘플만 사용하는 경우 말입니다. 이 경우 우리는 오디오가 녹음된 속도와 일치하는 가능한 최상의 시간 해상도를 갖게 되지만, 모든 주파수 정보 (frequency information)를 맞바꾸어 잃게 됩니다.
여기서 세그먼트들을 살펴보면 — 이것이 위상 (phase)과 진폭 (amplitude)에 따라 단순히 위아래로 움직이는, 완전히 0 주파수 파동 (zero frequency waves)으로 구성되어 있음을 알 수 있습니다. 푸리에 변환 (Fourier transform)은 본질적으로 우리가 입력하는 각 샘플을 약간 다른 형식으로 그대로 뱉어내고 있는 것입니다.
따라서 이 소리를 재생하면 — 놀랍지도 않게, 결과는 다른 경우와 마찬가지로 원본과 동일합니다.
좋습니다, 우리가 어떻게 나누든 푸리에 변환에서 얻은 정보를 사용하여 원래의 신호 (signal)를 여전히 재구성할 수 있는 것 같습니다. 하지만 — 그 정보로부터 우리가 배울 수 있는 것에는 근본적인 트레이드오프 (tradeoff)가 존재합니다.

한쪽 극단에서는 우리가 시간상 어디에 있는지 정확히 알 수 있지만, 어떤 주파수 (frequencies)가 존재하는지에 대해서는 전혀 알 수 없습니다. 반대로 다른 쪽 극단에서는 주파수에 대해 최대한의 정밀도를 갖지만, 비극적으로 시간 감각을 잃어버리게 됩니다.

따라서 우리의 구체적인 목표에 따라, 이 두 가지 극단적인 불확실성 사이에서 적절한 균형을 맞춰야 합니다. 즉, 무엇이 일어나고 있는지에 대해 조금 배우면서, 그것이 언제 일어나고 있는지에 대해서도 조금은 알 수 있도록 말이죠.

그냥 재미 삼아, 우리가 여기서 이 세그먼트 (segments)들을 수동으로 스크러빙 (scrubbing) 하는 동안 실제로 오디오를 재생해 보겠습니다. 그래야 이 소리들이 고립되었을 때 어떻게 들리는지 감을 잡을 수 있으니까요.

좋아요, 이제 세그먼트당 단 하나의 가장 두드러진 주파수 (frequency)로만 제한한다면 어떤 소리가 날지 좀 궁금해지네요. 하지만 계속해서 "hello everyone"만 듣는 건 좀 질려서 — 형용사와 명사가 가득 담긴 거대한 리스트를 빠르게 다운로드했고, 무작위 쌍을 출력하는 아주 작은 Python 프로그램을 작성했습니다. 그래서 우리의 새로운 문구는... "Punctual thunderstorm (시간을 엄수하는 뇌우)"가 될 것입니다.

자 — 신호를 재구성할 때 진폭 (amplitude)이 가장 높은 주파수만을 취하도록 코드에 설정해 두었고, 각 세그먼트를 느리게 재생할 수 있도록 지속 시간 배수 (duration multiplier)도 추가했습니다. 이건 제가 초반에 실수로 "he-he-he-he-he-el-el-el-el"... 했던 것과 똑같이 작동합니다.

좋아요, 개별적인 톤 (tones)들을 들을 수 있도록 지속 시간 배수를 8로 설정했습니다. 그럼 한번 해보죠!

좋아요, 꽤 삐삐거리는 소리처럼 들리네요 — 하지만 일반 속도의 3배 정도로만 시도해 봅시다.. 오, 이제는 음성(speech)에 좀 더 가까운 소리가 나네요. 그럼 마지막으로 일반 속도로 시도해 보죠... 생각보다 놀라울 정도로 좋게 들리는 것 같습니다.

이 거의 무작위처럼 보이는 작은 삐 소리와 붑 소리들이, 단순히 적절한 속도로 재생되었을 때 갑자기 인식 가능한 단어들로 해소되는 방식이 정말 마음에 듭니다.

몇 가지 주파수(frequencies)를 조금 더 추가한다면, 즉 지금처럼 세그먼트당 5개 정도를 추가한다면 이 톤(tones)들이 어떻게 변하는지 빠르게 들어보고 싶습니다.

물론 추가된 주파수들로 인해 즉각적으로 약간의 복잡성이 더해지는 것을 들을 수 있는데, 그중 일부는 꽤 불쾌하게 들리기도 합니다. 어쨌든, 다시 처음으로 돌아가서 대략 실시간(real-time) 속도로 스크러빙(scrubbing)했을 때 어떻게 들리는지 확인해 봅시다.

좋습니다, 그 몇 개의 추가된 주파수들이 확실히 명료도(clarity)를 높여주었네요.

비교를 위해, 여기 모든 주파수가 포함된 현재 상태가 있습니다.

자, 이제 장난은 그만하고, 이 단시간 푸리에 변환 (Short-time Fourier Transform, STFT)으로부터 얻은 데이터를 실제로 시각화해 봅시다. 한 축에는 시간(time)이 흐르고, 다른 축에는 주파수(frequency)가 있으며, 해당 플롯의 각 지점의 진폭(amplitude)을 밝기(brightness)로 표현하는 방식처럼 말이죠.

다시 말해, 스펙트로그램 (spectrogram)입니다.

그래서 저는 데이터를 입력받아 2D 텍스처의 레드 채널(red channel)에 진폭을 기록하는 작은 함수를 작성했고, 이것이 여기 있는 셰이더 (shader)로 전달되도록 했습니다.

현재 이 작업이 하는 일은 우리가 주파수 범위를 지정할 수 있게 해주는 것뿐입니

AI 자동 생성 콘텐츠

본 콘텐츠는 YouTube Sebastian Lague (절차적 생성)의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0