본문 바로가기

서비스 제작

[RVC 코드 분석] 화자 목소리 모델과 가수 목소리 음원을 합성하여 화자 목소리로 부른 음원을 생성

화자의 목소리를 담은 노래 커버를 제작하는 ai 서비스를 개발중에 있다. 이를 위해 RVC(Retrieval-based Voice Conversion)라는 AI 음성 합성 기술을 사용한다. RVC에서 많이 사용하는 깃헙 레포지토리는 https://github.com/RVC-Project/Retrieval-based-Voice-Conversion-WebUI/tree/main 이다. 하지만 레포지토리 이름에서 보이는 대로 이는 그라디오로 만들어진 웹 상에서 작동하도록 설계된 레포지토리이다. 서비스 제작을 위해서는 코드를 분석하여 api로 만들 수 있도록 코드를 수정해야한다. 이 포스트에서는 서비스를 위한 AI 기능 구현을 위한 코드 분석 과정을 담는다.


서비스 상에서 구현되어야하는 AI 기능은 아래와 같다.


  • 노래에서 가수 목소리 음원과 MR 음원으로 분리
  • 화자의 목소리를 받아서 화자 목소리 모델을 생성
  • 화자 목소리 모델과 가수 목소리 음원을 합성하여 화자 목소리로 부른 음원을 생성

RVC 깃헙 코드상에서 이 기능들이 모두 구현되어 있다. 포스트 당 하나씩 살펴보자. 이번 포스팅에는 “화자 목소리 모델과 가수 목소리 음원을 합성하여 화자 목소리로 부른 음원을 생성”에 대해 알아본다.


화자 목소리 모델과 가수 목소리 음원을 합성하여 화자 목소리로 부른 음원을 생성



스크린샷 번호 의미
1 추론에 사용할 학습한 화자 목소리 모델
2 옥타브 변경
3 처리할 음원 오디오 파일 경로
4 학습한 인덱스 모델
5 음높이 추출 알고리즘
6 최종 샘플링레이트
7 입력 소스 볼륨 엔벨로프와 출력 볼륨 엔벨로프의 결합 비율 입력
8 청자음과 호흡 소리를 보호, 전자음 찢김 등의 아티팩트 방지, 0.5까지 올려서 비활성화, 낮추면 보호 강도 증가하지만 인덱스 효과 감소 가능성 있음
9 >=3인 경우 harvest 피치 인식 결과에 중간값 필터 적용, 필터 반경은 값으로 지정, 사용 시 무성음 감소 가능
10 검색 특징 비율
11 변환 버튼

이제 이를 코드에서 찾아보자. 위와 같이 WEB을 표현하는 gradio가 담겨있는 파일은 infer-web.py이다. 음원을 분리하는 부분의 코드는 아래와 같다. 가장 하단의 버튼 클릭하는 부분에 대해 설명해보면 but0 버튼을 클릭하면 vc.vc_single 함수에 spk_item와 같은 파라미터들이 들어가서 실행된다. 그 결과물이 [vc_output1, vc_output2] 변수명 안에 담기게된다.


with gr.Row():
    sid0 = gr.Dropdown(label=i18n("推理音色"), choices=sorted(names))
    with gr.Column():
        refresh_button = gr.Button(
            i18n("刷新音色列表和索引路径"), variant="primary"
        )
        clean_button = gr.Button(i18n("卸载音色省显存"), variant="primary")
    spk_item = gr.Slider(
        minimum=0,
        maximum=2333,
        step=1,
        label=i18n("请选择说话人id"),
        value=0,
        visible=False,
        interactive=True,
    )
    clean_button.click(
        fn=clean, inputs=[], outputs=[sid0], api_name="infer_clean"
    )
with gr.TabItem(i18n("单次推理")):
    with gr.Group():
        with gr.Row():
            with gr.Column():
                vc_transform0 = gr.Number(
                    label=i18n("变调(整数, 半音数量, 升八度12降八度-12)"),
                    value=0,
                )
                input_audio0 = gr.Textbox(
                    label=i18n(
                        "输入待处理音频文件路径(默认是正确格式示例)"
                    ),
                    placeholder="C:\\Users\\Desktop\\audio_example.wav",
                )
                file_index1 = gr.Textbox(
                    label=i18n(
                        "特征检索库文件路径,为空则使用下拉的选择结果"
                    ),
                    placeholder="C:\\Users\\Desktop\\model_example.index",
                    interactive=True,
                )
                file_index2 = gr.Dropdown(
                    label=i18n("自动检测index路径,下拉式选择(dropdown)"),
                    choices=sorted(index_paths),
                    interactive=True,
                )
                f0method0 = gr.Radio(
                    label=i18n(
                        "选择音高提取算法,输入歌声可用pm提速,harvest低音好但巨慢无比,crepe效果好但吃GPU,rmvpe效果最好且微吃GPU"
                    ),
                    choices=(
                        ["pm", "harvest", "crepe", "rmvpe"]
                        if config.dml == False
                        else ["pm", "harvest", "rmvpe"]
                    ),
                    value="rmvpe",
                    interactive=True,
                )

          with gr.Column():
              resample_sr0 = gr.Slider(
                  minimum=0,
                  maximum=48000,
                  label=i18n("后处理重采样至最终采样率,0为不进行重采样"),
                  value=0,
                  step=1,
                  interactive=True,
              )
              rms_mix_rate0 = gr.Slider(
                  minimum=0,
                  maximum=1,
                  label=i18n(
                      "输入源音量包络替换输出音量包络融合比例,越靠近1越使用输出包络"
                  ),
                  value=0.25,
                  interactive=True,
              )
              protect0 = gr.Slider(
                  minimum=0,
                  maximum=0.5,
                  label=i18n(
                      "保护清辅音和呼吸声,防止电音撕裂等artifact,拉满0.5不开启,调低加大保护力度但可能降低索引效果"
                  ),
                  value=0.33,
                  step=0.01,
                  interactive=True,
              )
              filter_radius0 = gr.Slider(
                  minimum=0,
                  maximum=7,
                  label=i18n(
                      ">=3则使用对harvest音高识别的结果使用中值滤波,数值为滤波半径,使用可以削弱哑音"
                  ),
                  value=3,
                  step=1,
                  interactive=True,
              )
              index_rate1 = gr.Slider(
                  minimum=0,
                  maximum=1,
                  label=i18n("检索特征占比"),
                  value=0.75,
                  interactive=True,
              )
              f0_file = gr.File(
                  label=i18n(
                      "F0曲线文件, 可选, 一行一个音高, 代替默认F0及升降调"
                  ),
                  visible=False,
              )

              refresh_button.click(
                  fn=change_choices,
                  inputs=[],
                  outputs=[sid0, file_index2],
                  api_name="infer_refresh",
              )
              # file_big_npy1 = gr.Textbox(
              #     label=i18n("特征文件路径"),
              #     value="E:\\codes\py39\\vits_vc_gpu_train\\logs\\mi-test-1key\\total_fea.npy",
              #     interactive=True,
              # )
              with gr.Group():
                  with gr.Column():
                      but0 = gr.Button(i18n("转换"), variant="primary")
                      with gr.Row():
                          vc_output1 = gr.Textbox(label=i18n("输出信息"))
                          vc_output2 = gr.Audio(
                              label=i18n("输出音频(右下角三个点,点了可以下载)")
                          )

                      but0.click(
                          vc.vc_single,
                          [
                              spk_item,
                              input_audio0,
                              vc_transform0,
                              f0_file,
                              f0method0,
                              file_index1,
                              file_index2,
                              # file_big_npy1,
                              index_rate1,
                              filter_radius0,
                              resample_sr0,
                              rms_mix_rate0,
                              protect0,
                          ],
                          [vc_output1, vc_output2],
                          api_name="infer_convert",
                      )

코드의 변수명과 웹 스크린샷의 번호를 연결해보자.


웹 스크린샷 번호 코드 변수명 의미
1 sid0 추론에 사용할 학습한 화자 목소리 모델
2 vc_transform0 옥타브 변경
3 input_audio0 처리할 음원 오디오 파일 경로
4 file_index1 학습한 인덱스 모델
5 f0method0 음높이 추출 알고리즘
6 resample_sr0 최종 샘플링레이트
7 rms_mix_rate0 입력 소스 볼륨 엔벨로프와 출력 볼륨 엔벨로프의 결합 비율 입력
8 protect0 청자음과 호흡 소리를 보호, 전자음 찢김 등의 아티팩트 방지, 0.5까지 올려서 비활성화, 낮추면 보호 강도 증가하지만 인덱스 효과 감소 가능성 있음
9 filter_radius0 >=3인 경우 harvest 피치 인식 결과에 중간값 필터 적용, 필터 반경은 값으로 지정, 사용 시 무성음 감소 가능
10 index_rate1 검색 특징 비율
11 but0 변환 버튼

더 깊게 들어갈 수 있지만 서비스 제작에 있어서는 이정도의 코드 분석이면 충분하기 때문에 여기까지 알아본다. 이제 코드를 기반으로 서비스에 사용할 수 있도록 아래와 같이 새로운 파일을 생성했다.


import argparse
import os
import sys

now_dir = os.getcwd()
sys.path.append(now_dir)
from dotenv import load_dotenv
from scipy.io import wavfile
import ffmpeg

from configs.config import Config
from infer.modules.vc.modules import VC

def arg_parse() -> tuple:
    parser = argparse.ArgumentParser()
    parser.add_argument("--f0up_key", type=int, default=0)
    parser.add_argument("--input_path", type=str, help="input path", default="/home/choi/desktop/rvc/ai/data/user1/output/music/vocal_origin_music.mp3_0.wav")
    parser.add_argument("--index_path", type=str, help="index path", default="/home/choi/desktop/rvc/ai/data/user1/output/trained_model/trained_index.index")
    parser.add_argument("--f0method", type=str, default="rmvpe", help="harvest or pm")
    parser.add_argument("--opt_path", type=str, help="opt path", default="/home/choi/desktop/rvc/ai/data/user1/output/cover/output.wav")
    parser.add_argument("--model_name", type=str, help="store in assets/weight_root", default="/home/choi/desktop/rvc/ai/data/user1/output/trained_model/trained_voice.pth")
    parser.add_argument("--index_rate", type=float, default=0.66, help="index rate")
    parser.add_argument("--device", type=str, help="device")
    parser.add_argument("--is_half", type=bool, help="use half -> True")
    parser.add_argument("--filter_radius", type=int, default=3, help="filter radius")
    parser.add_argument("--resample_sr", type=int, default=0, help="resample sr")
    parser.add_argument("--rms_mix_rate", type=float, default=1, help="rms mix rate")
    parser.add_argument("--protect", type=float, default=0.33, help="protect")

    args = parser.parse_args()
    sys.argv = sys.argv[:1]

    return args

def main():
    load_dotenv()
    args = arg_parse()
    config = Config()
    config.device = args.device if args.device else config.device
    config.is_half = args.is_half if args.is_half else config.is_half
    vc = VC(config)
    vc.get_vc(args.model_name)
    _, wav_opt = vc.vc_single(
        0,
        args.input_path,
        args.f0up_key,
        None,
        args.f0method,
        args.index_path,
        None,
        args.index_rate,
        args.filter_radius,
        args.resample_sr,
        args.rms_mix_rate,
        args.protect,
    )
    os.makedirs(os.path.dirname(args.opt_path), exist_ok=True)
    wavfile.write(args.opt_path, wav_opt[0], wav_opt[1])

if __name__ == "__main__":
    main()