코딩 어드벤처: 가산 합성 (Additive Synthesis)
요약
본 글은 코딩 어드벤처를 통해 가산 합성(Additive Synthesis)의 원리를 탐구하고, 스펙트로그램을 활용하여 소리의 주파수 변화를 시각화하는 과정을 다룹니다. 오디오 데이터 배열 채우기 함수와 Note 클래스를 정의하고, 위상 계산 및 sin 함수를 이용해 실제 사운드 웨이브 값을 생성하는 코딩 방법을 설명합니다.
핵심 포인트
- 가산 합성은 여러 주파수의 강도를 합성하여 소리를 만듭니다.
- 스펙트로그램을 통해 시간 경과에 따른 주파수 변화를 시각화할 수 있습니다.
- 오디오 시스템에서 위상(phase) 계산은 사운드 웨이브의 현재 위치를 파악하는 핵심입니다.
- 부동 소수점 정밀도 손실이 오디오 신호에 미묘한 오류를 일으킬 수 있음을 발견했습니다.
여러분, 그리고 코딩 어드벤처(Coding Adventures)의 또 다른 에피소드에 오신 것을 환영합니다. 오늘은 몇 가지 악기의 소리를 합성해 보려고 합니다. 최근에는 사운드의 여러 주파수 강도를 시각화하고 시간이 지남에 따라 어떻게 변하는지 보여주는 작은 스펙트로그램 도구를 만들었습니다. 심지어 여기에서 우리가 직접 값을 그릴 수도 있습니다. 예를 들어, 증가했다가 감소하는 주파수의 선 같은 것이요. 그리고 그 결과를 듣는 거죠. 저는 이것을 가지고 노는 것이 꽤 재미있다고 생각합니다. 그냥 무작위로 낙서하고 어떤 결과물이 나오는지 보는 것처럼요.
지난번에는 음성 녹음 위를 대략적으로 따라 그리면서... <speech synthesizing> 하는 시도를 해보기도 했습니다. 미래에 스피치 합성(speech synthesis)을 확실히 탐구할 것이지만, 일단은 음악에 주의를 돌려봅시다. 그래서 저는 다양한 악기 녹음이 담긴 좋은 저장소(repository)를 찾았습니다. 여기서 피아노로 연주된 개별 음표 등을 다운로드하여 우리의 스펙트로그램에 넣고 살펴보는 거죠.
자, 여기에는 간격이 일정하게 떨어진 주파수들이 한 묶음으로 보이고, 위로 갈수록 대략적으로 약해지는 것 같습니다. 하지만 분명히 많은 것이 일어나고 있으니, 이 부분을 더 자세히 살펴보기 위해 자주 돌아올 것 같습니다. 일단은 이러한 여러 주파수를 생성하기 위한 간단한 시스템을 만드는 것으로 시작합시다. 기존에 가지고 있던 작은 그리기 도구를 사용할 수도 있겠지만... 이번에는 좀 더 정밀한 접근 방식이 좋을 것 같습니다.
좋습니다. 여기 코드에 몇 가지 설정을 해 두었습니다. 먼저 이 빈 오디오 데이터 배열을 채우는 역할을 하는 함수가 있습니다. 실제로는 할당된 오디오 소스에 그 작업을 위임할 뿐이지만, 혹시라도 너무 크게 터져 나오는 일이 생길 경우를 대비하여 최종 안전 단계를 추가해 값들을 합리적인 범위 내로 엄격하게 제한했습니다. 그리고 작은 Note 클래스도 만들었습니다. 아니면 제 구문 강조 표시기가 어떤 이유에서인지 'Not-e'라고 주장합니다.
이것은 음의 주파수와 얼마나 오랫동안 눌려 있었는지 같은 정보를 담고 있습니다. 그런 다음 실제로 소리를 내기 위해 이 synthesize 함수가 있습니다. 이것을 저희 오디오 시스템의 소스로 지정했습니다. 그리고 여기에서 만약 음이 눌리면 타이머를 증가시킬 수 있고, 이를 사용해서 위상(phase)을 계산할 수 있습니다. 즉, 현재 시간상 사운드 웨이브의 어느 지점에 있는지를 말합니다. 그래서 최종적으로 sin 함수를 통과시켜 실제 값을 얻습니다.
좋습니다. 여기 Note를 트리거하는 작은 버튼을 설정했습니다. 즉, 우리가 앉아서 분명히 만들어낸 즐거운 소리를 감상할 수 있습니다. 음, 사실 이건 좀 이상했어요. 코드의 버그인지 제 귀에 문제가 있는 건지 모르겠지만, 끝부분에서 소리가 미묘하게 변한 것 같다고 확신합니다. 그래서 여기 스펙트로그램을 뒤져봤는데, 정말 의심스럽습니다. 주파수의 수직적인 퍼짐은 우리가 예상하는 것이니 상관없지만, 2초와 4초 지점에서 갑자기 아주 약간 다르게 보이는 이 작은 점프들이 문제입니다.
이 작은 구간을 다시 듣고 싶네요. 제가 추측하기로는 2초와 4초 지점은 부동 소수점(floating point) 정밀도를 약간 잃게 되는 값들인 것 같습니다. 왜냐하면 이 값이 명백히 해당 값의 정수 부분(integer part)을 표현해야 하기 때문이죠. 저는 이런 종류의 문제들이 정말 큰 숫자, 예를 들어 우주선이 우주의 중심에서 멀리 떨어져 있을 때 발생하는 그래픽 또는 물리적 문제로 나타나는 데 익숙합니다. 그럴 때는 마치 우주가 우주선 주위를 움직이게 하는 것 같은 해결책(workarounds)이 필요하죠.
하지만 저는 정밀도 손실이 처음부터 우리의 귀에 문제를 일으킬 거라고는 예상하지 못했습니다. 하지만 청취의 즐거움을 위해, 다양한 시간 지점에서의 소리가 어떤지 들어보시죠. 혹시 잠들 때 들을 수 있는 전체 버전이 좋으면 알려주세요. 아무튼, 시간을 기준으로 하는 대신 파동의 위상(phase)을 음표 안에 저장할 수 있을 것 같습니다. 왜냐하면 우리가 정수 주파수(integer frequencies)를 고수한다면, 1을 한 번 지날 때마다 완벽하게 반복될 것이기 때문에, 값이 1을 넘길 때마다 0으로 되돌려 최대의 정밀도를 유지할 수 있기 때문입니다. 그리고 이 작은 변경만으로 우리의 스펙트로그램(spectrogram)이 훨씬 더 만족스러워 보입니다.
자, 이제 우리는 신뢰할 수 있는 개별 음색(individual tones)을 만들 수 있게 되었고, 이를 통해 흥미롭게 들리는 것들을 구축할 수 있기를 바랍니다. 하지만 시간이 지남에 따라 음색의 크기(loudness)를 제어할 수 있어야 합니다. 그렇지 않으면 그저 연속적으로 시끄럽게 울리는 '삐-' 소리일 뿐이라 그다지 듣기 좋지 않거든요. 마치 피아노 음표의 파형(waveform)을 보면, 진폭(amplitude)이 시작할 때 최소한 분수 초 동안 서서히 커지고(ramp up) — 그리고 점차 사라지는 것(fade away)을 볼 수 있잖아요.
그래서 저는 여기서 간단한 도구를 만들었는데, 이 도구는 초기에 서서히 커지는 시간(ramp up)인 '어택(attack)' 시간을 밀리초 단위로 지정할 수 있게 해주고, 그 후에 소리가 사라지는 데 걸리는 시간도 얼마나 되는지 지정할 수 있습니다. 예를 들어 피아노에서 건반을 아무 때나 떼면 감쇠기(dampers)가 세게 내려와 진동을 매우 빠르게 멈추게 합니다. 따라서 이 '릴리즈(release)' 매개변수를 사용해서 소리가 사라지는 지속 시간을 제어할 수 있습니다. 이것은 일반적으로 엔벨로프(envelope)라고 알려져 있어서, 여기에 그 세 가지 매개변수를 담고 있고, 음이 얼마나 오래 유지되었거나 떼어졌는지에 따라 현재 값을 계산하는 함수도 있는 작은 엔벨로프 클래스가 있습니다.
그래서 빠르게 테스트해 보겠습니다. 음을 누르고 있으면 진폭(amplitude)이 1까지 올라갔다가 점차 감쇠하는 것을 볼 수 있습니다. 물론, 만약 제가 음을 놓으면—아, 이게 제가 예상했던 것과는 좀 다릅니다. 아마도 여기에 있는 값들이 제대로 작동하려면 0과 1 사이로 제한되어야 할 것 같습니다... 좋습니다, 다시 시도해 보겠습니다. 그래서 음을 놓겠습니다. 그리고 이것이 이제 잘 작동하는 것 같습니다. 일부 악기는 어택(attack)이 상당히 느릴 수 있으니, 어택 단계 중에 음을 놓는 경우도 테스트해야 합니다—하지만 이것 역시 괜찮게 작동하는 것 같습니다.
지금 당장 버그가 있을 수 있는 한 가지는 제가 이 값을 상당히 느린 릴리즈(release) 지속 시간으로 설정했을 때, 음이 아직 페이드 아웃(fading out)되고 있는 동안 다시 트리거(retrigger)하는 경우입니다. 이런 경우에는 새로운 어택(attack) 단계의 시작 부분에서 진폭이 갑자기 0으로 떨어지는 현상이 나타나는데, 이는 상당히 부자연스럽게 들릴 것입니다. 그러니 나중에 고쳐야 할 부분이긴 하지만, 아직 어떻게 처리해야 할지 확실하지 않습니다. 아무튼, 이 진폭을 신디사이저(synthesizer)에 빠르게 적용하고 싶습니다. 이렇게 하면 음 타이머(note timers)가 여기서 증가하게 되고, 이것들이 설정되면 엔벨로프(envelope)에서 진폭을 찾아내고 물론 그 값에 따라 파형(wave)을 스케일링할 수 있습니다. 좋습니다, 한번 해보겠습니다.
우선 몇 가지 더 긴 지속 시간을 설정하겠습니다. 그리고 이 버튼을 누르고... 떼어 보겠습니다. 이것은 잘 작동하는 것 같습니다. 짧은 값으로도 들어봅시다. 좋습니다, 나쁘지 않지만, 저는 이렇게 직선 형태로는 섬세한 표현을 할 여지가 충분하지 않다고 느꼈습니다. 그래서 여기에 커브 에디터(curve editor)를 코딩했습니다. 어떤 종류의 곡선이 이런 종류의 것에 가장 적절한지 모르기 때문에, 제가 익숙한 베지에 곡선(Bezier)으로 임시로 사용해 보았습니다.
음, 아직 다듬어야 할 부분이 몇 가지 있을 것 같아요. 하지만 몇 가지 수정을 거친 후, 주어진 x 좌표에 해당하는 곡선의 y 좌표를 검색하는 함수를 만들었습니다. 그래야 저희가 실제로 이 값을 엔벨로프(envelope)에서 사용할 수 있습니다. 다만 이게 아주 빠르지는 않아서, 곡선이 편집될 때마다 균일하게 간격을 둔 x 구간에서 검색을 실행하고 그 결과를 이 이산화된(discretized) 커브 클래스에 저장합니다. 이렇게 하면 정확도를 약간 희생하는 대신, 훨씬 효율적으로 곡선을 평가할 수 있게 됩니다.
이제 저희 엔벨로프는 어택(attack), 디케이(decay), 릴리즈(release) 세 가지 값에 접근할 수 있게 되었고, 단순히 직선을 사용하는 것이 아니라 이 값들을 찾아 사용합니다. 좋습니다. 그럼 이걸로 재미있는 것을 만들 수 있는지 확인해 봅시다. 지금은 곡선이 직선이라서, 다시 한번 소리를 들어보겠습니다... 그리고 이제 어택 커브를 조금 조정해서 시작부터 더 빠르게 상승하도록 해볼게요. 그러면 사운드에 약간의 타격감(punch)을 더할 수 있기를 바랍니다.
좋아요, 이게 괜찮은 것 같네요. 그럼 디케이도 좀 더 빨리 떨어지도록 조정해 봅시다. 음, 이걸보다 더 빨리 시켜보겠습니다. 이런 느낌으로요. 그리고 변화를 주기 위해 피치(pitch)도 낮춰볼게요. 지금 세팅에 대해서는 꽤 만족스럽습니다. 우리가 곡선을 얼마나 날카롭게 만들 수 있는지에는 약간 제한이 있는 것 같지만, 대부분의 경우 충분한 제어력을 얻었다고 생각합니다. 다만 지금 짜증나는 부분은, 만약 저희가 멜로디를 연주하고 싶다면 이렇게 주파수(frequency)를 수동으로 변경해야 한다는 점입니다.
그래서 제가 준비한 혁신적인 새 기능인 다중 음표(multiple notes)를 소개하게 되어 매우 기쁩니다! 하지만 현재는 모두 같은 소리를 내고 있습니다... 따라서 우리가 실제로 이 것들에 할당하고 싶은 주파수(frequency)가 무엇인지 알아내야 합니다. 간단한 해결책은 250Hz와 같이 기본 주파수를 선택한 다음, 거기서부터 예를 들어 50Hz 간격으로 증가시키는 것입니다. 그러면 합성기(synthesizer)가 이 모든 음표를 반복하며 그 파형(wave)들을 단순히 합산하여 처리합니다.
좋습니다. 이제 실제로 음악을 만들 수 있다는 것이 정말 흥분됩니다. 이런 곡은 어떨까요... 아니면 여러 음표가 동시에 유지되는 것을 시도해 보겠습니다. 다만, 한 번에 너무 많은 음표를 연주하지 않도록 주의해야 합니다. 왜냐하면 그 파형들의 합이 너무 커지면 클리핑(clipping) 현상이 발생하게 되고, 이는 귀에 그리 좋지는 않습니다. 이것은 정말 최후의 수단으로만 일어나야 하므로, 저는 오디오를 먼저 통과시켜서 처리하는 작은 함수를 만져보았습니다. 이 함수는 입력 값이 지정된 임계값(threshold)을 얼마나 초과했는지 확인하고, 초과가 없다면 입력 값을 변경 없이 그대로 반환합니다. 하지만 그렇지 않다면 주어진 계수(factor)만큼 초과량을 줄여줍니다.
또한 임계값 주변에 어느 정도의 스무딩(smoothing) 옵션도 추가했습니다. 좋습니다. 여기에서 해당 임계값을 데시벨(decibels) 단위로 지정하고, 그 이상으로 결과를 얼마나 낮출지 알려줄 수 있습니다. 그리고 여기에 스무딩이 어떻게 보이는지 보여드리겠습니다. 아직 이것을 synthesize에 연결해야 하므로, 현재 생성하는 모든 오디오 데이터에 대해 작은 루프를 빠르게 추가하여 이를 데시벨로 변환함으로써 음량(loudness)의 근사치로 만들고, 그 값이 임계값을 초과하면 감소시킵니다. 마지막으로 결과는 다시 원본 진폭(raw amplitudes)으로 변환됩니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 YouTube Sebastian Lague (절차적 생성)의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기