
AI 보이스 커버를 「코드」로 만들기 — Demucs + RVC + Optuna로 음악 지식 제로에서 도전한 음성 변환 파이프라인
요약
음악 지식이 없는 상태에서 Claude Code를 활용해 Demucs와 RVC를 이용한 AI 보이스 커버 파이프라인을 구축한 사례입니다. 보컬 분리, 음성 변환, 그리고 Spotify의 pedalboard 라이브러리를 이용한 FX 체인 적용 과정을 다룹니다.
핵심 포인트
- Demucs를 활용한 보컬과 반주의 정교한 분리
- RVC v2를 이용한 목소리 재현도와 가사 전달력의 트레이드오프 조절
- Claude Code를 활용한 라이브러리 선정 및 파라미터 설계
- pedalboard 라이브러리를 통한 Python 기반 FX 체인 구현
서론
「이번 봄에 시작한 일」로서, 최근 빠져서 직접 손을 움직여 본 이야기를 해보려고 합니다!
YouTube에서 Freddie Mercury의 AI 커버 영상을 보는 것에 빠졌습니다. 일본어 노래를 Freddie가 커버한 듯한 음원·영상입니다 (YouTube에서 검색하면 나옵니다). 댓글창에는 「AI 대단하다」라는 반응이 달려 있었고, 구체적으로 어떤 모델을 사용해서 무엇을 하면 이런 음원을 만들 수 있는지 궁금해져서 조사해 보고 직접 해보기로 했습니다. 특히 저는 음악적인 지식이 전혀 없기 때문에, 그런 상태에서 어디까지 할 수 있는지 시험해 보았습니다.
코드는 Claude Code와 상담하며 작성하고 있습니다. 음향 처리 지식이 없는 부분은 「이런 것을 하고 싶다」라고 전달하여 라이브러리 선정이나 파라미터(Parameter) 설계를 맡겼습니다.
주제는 Chester를 개인적으로 좋아하고, Chester의 공개 모델이 있으며, 목소리 톤에 어울릴 법한 일본어 노래를 ChatGPT에게 제안받아, 「Chester Bennington이 요네즈 켄시의 Lemon을 부른다면」으로 정했습니다.
우선 심플하게 시도해 보기
첫 번째 접근 방식은 단순합니다. 악곡을 보컬과 반주로 분리하고, 보컬을 Chester의 목소리로 변환한 뒤, 반주와 다시 합치는 것입니다.
보컬 분리 (Demucs)
Meta가 개발한 Demucs로 악곡을 2개의 트랙으로 나눕니다.
def separate(input_path, output_dir, model_name="htdemucs_ft", device=None):
model = get_model(model_name)
model.to(device)
...
보컬 이외의 스템(Stem)을 모두 합쳐서 하나의 반주로 만들었습니다. 개별적으로 추출하면 스템 간의 잔차 노이즈(Residual Noise)가 누적될 수 있다고 Claude Code가 조언하여 이 방식을 선택했습니다.
음성 변환 (RVC)
분리된 보컬을 RVC v2로 Chester의 목소리로 변환합니다.
converter = RVCConverter(device="cuda:0", is_half=True)
result_path = converter.infer_audio(
voice_model="chester_bennington",
...
index_rate를 높일수록 Chester의 목소리에 가까워지지만, 일본어 자음이 뭉개지기 쉽습니다. protect를 낮추면 변환이 강해지지만, 가사 전달력이 떨어집니다. 파라미터의 의미는 Claude Code에게 물어가며 들어본 결과, index_rate=0.85, protect=0.25로 결정했습니다. 「목소리의 재현도」와 「가사의 전달력」 사이의 트레이드오프(Trade-off) 관계입니다.
합쳐본 결과
변환된 보컬과 반주를 그대로 섞어 보았습니다. 일단 커버 음원 형태는 됩니다. 하지만 들어보니 무언가 부족합니다. 목소리가 평탄하고 뒤로 물러나 있어서 현장감이 없습니다. YouTube에서 들었던 Freddie의 AI 커버와는 하늘과 땅 차이였습니다.
무엇이 부족한가
음악 제작 지식이 없어 감이 오지 않았지만, Claude Code에게 물어보니 「레코딩 스튜디오에서는 보컬에 EQ, 컴프레션 (Compression), 리버브 (Reverb) 등의 처리를 겹겹이 쌓는 것이 보통인데, RVC의 출력물에는 그것이 포함되어 있지 않다」라고 했습니다.
7단 FX 체인으로 마무리하기
Claude Code에게 「Python으로 보컬에 FX 체인을 걸고 싶다」라고 전달했더니, Spotify사가 OSS(Open Source Software)로 공개하고 있는 pedalboard를 추천해 주었습니다. VST 플러그인이 필요 없으며, Python에서 직접 파라미터를 전달할 수 있습니다. 이를 사용하여 7단의 이펙트 체인(Effect Chain)을 구성했습니다.
입력 (모노럴)
↓ ① 노이즈 게이트 (Noise Gate)
↓ ② EQ 셰이핑 (EQ Shaping)
...
각 스테이지의 구성과 주파수 대역 등의 설정은 Claude Code가 선정했습니다. 각각의 역할을 간단히 설명하겠습니다.
① 노이즈 게이트 (Noise Gate) — RVC 변환 시 발생하는 아티팩트 (Artifact)를 제거합니다. 일정 음량 이하의 신호를 컷(Cut)합니다.
② EQ 셰이핑 (EQ Shaping) — 4밴드로 주파수 특성을 조절합니다. 3.5kHz의 프레즌스 (Presence) 대역을 +3.5dB 부스트하면 목소리가 믹스 안에서 앞으로 나옵니다. 이 대역은 인간의 귀가 민감하게 반응하는 영역입니다.
plugins = [
HighpassFilter(cutoff_frequency_hz=100.0),
LowShelfFilter(cutoff_frequency_hz=250.0, gain_db=-2.0),
...
③ 컴프레션 (Compression) — 볼륨의 편차를 억제합니다. 록 보컬은 음량 차이가 크기 때문에, ratio=4.5로 강하게 압축하고 있습니다.
④ 디에서 (De-esser) — 치찰음(ㅅ, ㅊ 발음 시 발생하는 '쉿' 하는 소리)을 억제합니다. pedalboard에 전용 디에서가 없기 때문에, PeakFilter → Compressor → PeakFilter를 샌드위치 구조로 배치하여 대체했습니다.
def _apply_de_esser(audio, sr, p):
freq = p["frequency_hz"] # 6000Hz
board = Pedalboard([
...
+6dB로 치찰음 대역만 끌어올려 컴프레서의 임계값(Threshold)을 넘겨 압축한 뒤, -6dB로 다시 되돌립니다. 이는 '멀티밴드 컴프레서 (Multi-band Compressor)'나 '다이내믹 EQ (Dynamic EQ)'라고 불리는 기법과 유사하며, 범용 이펙트의 조합을 통해 특정 대역만을 선택적으로 압축하는 처리를 구현한 것입니다.
⑤ 새츄레이션 (Saturation) — 왜곡 성분을 2할 정도 섞어, 목소리에 록 특유의 거친 질감과 두께감을 더합니다.
⑥ 더블링 (Doubling) — RVC의 출력은 모노럴 (Mono)입니다. 좌우의 피치를 ±5센트 (cents)씩 틀어주고, 딜레이(Delay)도 비대칭(왼쪽 15ms / 오른쪽 20ms)으로 설정하여 스테레오감을 확장합니다. 피치 시프트 (Pitch Shift)를 통해 좌우 파형이 비상관화(Uncorrelated)되므로, 콤 필터링 (Comb Filtering) 현상이 일어나기 어렵습니다.
def _generate_doubles(audio_mono, sr, p):
detune = p["detune_cents"] / 100.0
left_board = Pedalboard([
...
⑦ 공간계 (Spatial Effects) — 리버브 (Reverb)와 딜레이 (Delay)로 공간의 확장감을 부여합니다.
파라미터를 Optuna로 자동 최적화하기
FX 체인의 구성은 Claude Code에 맡겼지만, 각 파라미터의 최적값은 어떻게 정해야 할까요? Claude Code에게 프리셋 값을 만들어 달라고 할 수도 있지만, 그것이 정말 최적인지는 알 수 없습니다. 게다가 '좋은 소리인가'는 주관적인 영역이기에 그대로 목적 함수 (Objective Function)로 사용할 수 없습니다.
Claude Code와 상담한 결과, "실제 Chester의 가창과 음향 특징량 사이의 거리를 목적 함수로 삼아 Optuna로 탐색한다"는 접근 방식을 제안받았습니다. 레퍼런스로 Chester의 "Numb", "In The End" 보컬을 준비하고, 처리 후의 음성과 거리(Distance)를 최소화하는 방향으로 자동 탐색을 수행합니다.
거리 측정 방법
librosa를 사용하여 5종류의 음향 특징량을 추출합니다.
| 특징량 | 포착하는 요소 | 거리 계산 방법 |
|---|---|---|
| MFCC | 음색 그 자체. 인간의 청각 특성에 기반한 멜 스케일 (Mel Scale)로 산출 | 프레임별 평균 벡터 간의 코사인 거리 (Cosine Distance) |
| ... |
MFCC나 스펙트럼 콘트라스트 (Spectral Contrast)와 같이 다차원 벡터가 되는 특징량에는 코사인 거리를 사용하고, 스칼라 (Scalar) 값인 특징량에는 상대 오차 (Relative Error)를 사용합니다. 이들을 가중 합산하여, 음색을 직접적으로 반영하는 MFCC의 가중치를 최대(45%)로 설정했습니다.
distance = (0.45 * mfcc_dist
+ 0.20 * contrast_dist
+ 0.15 * centroid_dist
...
Optuna로 탐색하기
Optuna의 TPE 샘플러 (TPE Sampler)를 사용하여 100회의 트라이얼 (Trial)을 수행합니다. 계산 비용을 절감하기 위해 곡 전체가 아닌, 25% 지점부터 15초간의 클립을 사용했습니다.
def objective(trial):
params = _create_trial_params(trial, base_params)
processed = apply_chain(source_clip.copy(), TARGET_SR, params)
...
믹싱 (Mixing)
인핸스 (Enhance)된 보컬과 반주를 합성합니다. 보컬의 볼륨은 반주의 RMS로부터 역산하여 자동으로 맞춥니다. M/S 처리를 통해 스테레오감을 약간 넓히고, 리미터 (Limiter)로 음 깨짐(Clipping)을 방지하여 완성합니다.
if vocal_gain_db is None:
acc_rms = np.sqrt(np.mean(accompaniment**2))
vocal_gain_db = compute_gain_db(vocals, target_rms=acc_rms * 1.2)
...
요약
비교적 간단하고 빠르게 음원 제작까지 마칠 수 있었습니다. 자동화 파이프라인을 구축한다면 YouTube에 정기적으로 게시하는 것도 가능할지 모릅니다 (AI 콘텐츠는 수익화가 불가능하지만...).
음악 지식이 있는 사람이라면 Optuna를 사용할 필요도 없이 수동으로 조절할 수 있을지도 모릅니다. 저에게는 그것이 불가능했기에, "레퍼런스(Reference)와의 거리를 최소화한다"라는 접근 방식이 필요했습니다. 실제로 YouTube에 게시된 음원들은 퀄리티가 높은 것이 많으며, 퀄리티 향상을 위해서는 음악 도메인 지식 (Domain Knowledge)이 필요할 것 같습니다.
또한, MFCC 기반의 거리는 음색의 유사도를 측정할 수 있지만, "자연스럽게 들리는가", "감정이 전달되는가"와 같은 최종적인 품질 판단은 귀로 직접 수행했습니다. 무언가 더 좋은 방법이 있을까요...
Discussion

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