TTS 목소리 여섯 개를 겹쳐 떼창 만들기
랜선포차의 게임 콜 떼창을 TTS로 만든 방법. 목소리 6개를 0~80ms씩 어긋나게 겹치고, 길이를 중앙값에 맞추고, 캐릭터 56종의 조사까지 자동으로 붙여 클립 203개를 만든 과정을 실제 파형과 함께 정리했다.

랜선포차의 라운드는 떼창으로 시작한다. 주인공 캐릭터 이름을 넣어 “오징어가 좋아하는, 랜덤 게임~ 랜덤 게임~“을 다 같이 부르고, 그 사이 주인공이 3초 안에 게임을 고른다. 모임에서 이 구호를 외치는 그 박자가 게임의 시작 신호다. 여섯 명이 한꺼번에 외치는 소리를 녹음할 사람이 없어서, TTS 목소리 여섯 개를 겹쳐 만들기로 했다.
한 목소리로는 떼창이 안 된다
처음에는 TTS 한 목소리로 문장을 읽게 해 봤다. 또렷하게 들리는데 전혀 떼창 같지 않았다. 안내 방송이었다. 떼창이 떼창처럼 들리는 이유는 여러 사람이 완벽하게 맞추지 못하기 때문이다. 누구는 조금 빠르고, 누구는 목소리가 크고, 남녀와 나이가 섞여 있다.
그래서 한국어 TTS 목소리 여섯 개를 골랐다. 남자 청년 둘, 남자 중년 하나, 여자 청년 둘, 여자 중년 하나다. 목소리마다 시작 시점을 0, 20, 35, 45, 70, 80ms씩 늦추고, 볼륨도 1.0에서 0.75 사이로 조금씩 다르게 했다. 어긋나는 폭은 기본 80ms 안에 두고, 속삭이는 변형만 110ms까지 벌렸다.
| 목소리 | 시작 지연 | 볼륨 |
|---|---|---|
| 남 청년 | 0ms | 1.0 |
| 남 중년 | 20ms | 0.85 |
| 남 청년 | 35ms | 0.75 |
| 여 중년 | 45ms | 0.9 |
| 여 청년 | 70ms | 0.8 |
| 여 청년 | 80ms | 0.85 |
길이를 맞추는 게 제일 어려웠다
지연만 주고 겹치면 박자가 무너졌다. TTS는 같은 문장도 목소리마다 길이가 다르게 읽는다. 커버 그림이 “오징어” 콜의 실제 원본 파형이다. 목소리 여섯 개의 원본 길이가 2.60초에서 3.08초까지 벌어져 있다. 이대로 겹치면 뒤쪽 “랜덤 게임~“에서 한 사람이 혼자 늦게 끝난다.
그래서 생성 스크립트가 세 단계로 길이를 맞춘다.
- 앞뒤 무음 자르기. -45dB 아래 소리를 앞뒤에서 잘라 순수하게 말하는 부분만 남긴다.
- 너무 길면 빠르게 다시 받기. 목표 길이(게임 콜은 2.8초)보다 3% 넘게 길면, TTS의 말하기 속도 값을 0.05 단위로 올려 다시 요청한다. 최대 1.8배까지다. 그런데 TTS는 매번 조금씩 다르게 읽어서, 속도를 올렸는데 오히려 더 길게 나올 때가 있었다. 그래서 두 결과 중 짧은 쪽을 쓴다.
- 중앙값에 맞춰 늘이고 줄이기. 여섯 개 길이의 중앙값(목표 길이를 넘지 않게)을 기준으로, 각 목소리를 ffmpeg로 0.87배에서 1.3배 사이에서 늘이거나 줄인다. 범위를 넘으면 목소리가 기계처럼 변해서 그 안에서만 움직인다.
그다음 지연과 볼륨을 준 여섯 개를 합치고, 소리 크기를 일정하게 맞춘 뒤 피크를 눌러 모노 64kbps MP3로 저장한다. 오징어 콜의 최종 길이는 2.88초다. 클립 하나가 20KB대라 폰에서 받기에도 가볍다.
캐릭터 56종의 조사 붙이기
게임 콜에는 주인공 캐릭터 이름이 들어간다. 몸통이 56종이니 콜도 56개가 필요하다. 사람 캐릭터 목록은 JSON 파일 하나에 있어서, 스크립트가 그 파일을 읽어 콜 문장을 자동으로 만든다. 문제는 조사였다. “오징어가”와 “마늘이”처럼 이름 끝 글자에 받침이 있으면 “이”, 없으면 “가”다.
한글이면 글자 코드에서 받침 유무를 바로 계산할 수 있다. 그런데 이름에 숫자나 영문이 들어간 캐릭터가 있었다. “재벌3세”와 “행사MC”다. 숫자는 한국어로 읽었을 때의 받침(3은 “삼”이라 받침 있음)으로, 영문은 L·M·N·R처럼 읽으면 받침이 생기는 글자로 판단한다. TTS가 숫자와 영문을 엉뚱하게 읽지 않도록 이 두 이름은 “재벌 삼세”, “행사 엠씨”로 발음 표기를 바꿔 보낸다. 조사를 정할 수 없는 이름이 들어오면 스크립트가 멈추게 해서, 틀린 조사로 녹음되는 일을 막았다.
게임 콜 떼창이 나오는 순간. 자막과 함께 주인공의 3초 선택 시간이 시작된다
같은 떼창을 매번 다르게
라운드가 12번이니 한 판에 같은 떼창을 여러 번 듣는다. 똑같은 녹음이 반복되면 금방 질린다. 그래서 떼창마다 변형을 만들었다. 게임 콜은 기본 하나에 두 가지가 더 있다. 하나는 억지로 신난 버전으로, 톤을 올리고 속도 1.15배, 음높이 1.06배에 5~7명이 부른다. 다른 하나는 느리고 무덤덤한 버전으로, 속도 0.9배에 목소리를 가라앉히거나 속삭이게 하고 시작 지연을 110ms까지 벌린다. 게임의 캐릭터가 전부 무표정이라 무덤덤한 떼창이 의외로 잘 어울렸다.
변형용 목소리는 남녀 각 9개에서 뽑는다. 클립 이름으로 시드를 만든 난수를 써서, 스크립트를 다시 돌려도 같은 조합이 나오고 받아 둔 원본 캐시를 그대로 다시 쓴다. 게임 쪽은 기본과 변형을 한 묶음으로 보고 매번 무작위로 틀되, 직전에 튼 버전은 피한다. 커밋 6500dfc의 메시지에 이때 숫자가 남아 있다. “떼창·구호 변형: 묶음 65개 / 클립 203개, 클라이언트는 매번 무작위(직전 버전 회피)”. 게임 콜만 56종 × 3 = 168개이고, 나머지는 “무슨 게임?”, “게임, 스타트!”, 벌칙을 받는 사람에게 부르는 우유 구호, 게임을 못 고른 주인공에게 부르는 “언제까지 어깨춤을 추게 할 거야~” 같은 떼창과 판이 끝날 때의 “오늘의 우유왕 탄생!“이다.
| 종류 | 클립 수 |
|---|---|
| 게임 콜(캐릭터 56종 × 3버전) | 168 |
| 그 밖의 떼창·목소리 묶음 | 35 |
| 합계 | 203 |
크레딧이 떨어진 밤
떼창 하나에 목소리 여섯 개를 각각 요청하니 크레딧도 여섯 배로 든다. 첫날 밤 커밋 0e4ab37의 메시지에는 이렇게 적혀 있다. “사람 13종 콜은 크레딧 부족으로 미생성 → 박수 효과음으로 대체”. 중간에 크레딧이 떨어지는 경우를 대비해 스크립트는 시작과 끝에 남은 크레딧을 출력하고, 크레딧 부족 오류가 나면 새 요청을 멈춘 뒤 그때까지 만든 것만 목록에 반영한다. 못 만든 클립은 이름을 출력하고, 다시 실행하면 이어서 만든다. 그 덕에 게임에는 소리가 빈 캐릭터 대신 박수 소리가 났고, 한 시간쯤 뒤 충전하고 다시 돌린 커밋 38310a0에서 “몸통 56종 전부 콜 음성 완비”가 됐다.
요청 속도 제한도 있었다. 동시에 너무 많이 보내면 거절당해서, 동시 요청을 전역으로 4개까지만 허용하고 거절되면 점점 길게 기다렸다 다시 보낸다. 키는 스크립트만 읽고, 만들어진 음성 파일과 목록에는 들어가지 않는다.
떼창은 녹음한 사람 없이 만든 소리지만, 게임을 켜면 첫 라운드에서 가장 먼저 들리는 소리다. 캐릭터가 늘어나면 JSON에 한 줄을 넣고 스크립트를 한 번 돌리면 그 캐릭터의 떼창까지 생긴다. 이 떼창으로 시작하는 한 판은 랜선포차에서 들어 볼 수 있다. 떼창을 부르는 캐릭터 56종을 그린 이야기는 AI 이미지로 병맛 캐릭터 56종을 그린 과정에 있다.


