# AI 꾸러미 — 말소리 옹알이 (글자마다 짧은 음) — Gibberish voice / babble (Animal Crossing-like speech)
> 대사 글자마다 짧은 톱니파 블립을 내되 글자 코드로 음 높이를 · 한글 모음으로 두 공명 필터를 바꿔, 녹음 없이 캐릭터가 「옹알옹알」 말하는 소리를 만든다.  
> 견본: https://ai-techstudio.web.app/#t/i145

이 문서를 코드 도우미(Claude Code · Cursor · ChatGPT 등)에 그대로 주면 돼요. 「## 주문서」 가 할 일, 나머지는 참고 자료예요.

## 주문서

### 만들어 줘: 말소리 옹알이 (글자마다 짧은 음) — Gibberish voice / babble (Animal Crossing-like speech)

#### 1. 목표
안내 캐릭터 대사 (선배 · 마스코트)에 말소리 옹알이를 넣어 줘 — 대사 글자마다 짧은 소리를 내고, 글자 코드로 음 높이, 한글 모음으로 필터를 바꿔 말처럼 들리게. 목소리는 통통 튀는 귀여운 목소리.

#### 2. 핵심 기술 용어
- **Gibberish voice / babble (Animal Crossing-like speech)** — 옹알이 목소리 — 글자마다 짧은 소리
- **Formant filter (F1 · F2 bandpass)** — 모음을 만드는 두 공명 — 대역 필터 둘
- **Hangul syllable decomposition** — 한글 글자 코드에서 모음 번호 꺼내기
- **Sawtooth source · pitch glide** — 톱니파 목소리 · 끝이 살짝 내려감

#### 3. 환경
- 플랫폼: Web Audio API (TypeScript) — 소리 파일 없이 OscillatorNode · GainNode · BiquadFilterNode 로 합성
- 화면: 브라우저 — PC · 폰 모두 (폰은 첫 터치 뒤에만 소리가 난다)

#### 4. 조건
- 한글은 모음으로 필터, 한글이 아닌 글자는 소리 없이 쉬기만 (문장 부호 2.2칸)
- 같은 글자는 늘 같은 음 높이 — 무작위가 아니라 글자 코드로
- 목소리 높이 · 빠르기는 캐릭터마다 매개변수로
- 말풍선 글자 나타남과 같은 간격(0.078초)으로 맞춘다

#### 5. 완성 기준 (이게 보이면 성공)
- 「안녕! 나는 선배 윤정확이야.」가 글자마다 옹알옹알 들리고, ㅏ · ㅣ · ㅜ 가 서로 다르게 들린다
- 목소리 높이를 140Hz ↔ 600Hz 로 바꾸면 할아버지 ↔ 꼬마처럼 바뀐다
- 말풍선 글자가 소리와 같은 속도로 나타난다

#### 6. 진행 방식
- 핵심 코드 위주로, 설명은 짧게. 내 프로젝트에 끼워 넣기 쉬운 함수 · 클래스로 나눠 줘.
- 처음 화면에 바로 결과가 보이게, 그리고 켬/끔(또는 전/후) 비교를 할 수 있게 만들어 줘.
- 그림 · 소리 · 모델 파일이 필요하면 코드로 만든 임시 대체물로 먼저 돌아가게 하고, 진짜 파일로 바꿀 자리를 표시해 줘.
- 마지막에 「확인 방법」(무엇을 보면 성공인지)과 「조절할 값」 목록을 짧게 정리해 줘.
- 답변과 코드 주석은 한국어로 해 줘.

## 원리
- 한글 글자 코드 c 에서 모음 번호 = floor(((c − 0xAC00) % 588) / 28) — 21개 모음(ㅏ ~ ㅣ).
- 모음마다 두 공명 주파수(F1, F2)가 있다: ㅏ 800 · 1200, ㅣ 300 · 2300, ㅜ 350 · 800 … 톱니파를 이 두 대역 필터(Q 5 · 6)에 통과시키면 그 모음처럼 들린다.
- 음 높이 = 목소리 Hz(기본 320) × (1 + ((c % 7) − 3) × 0.045) — 글자마다 조금씩 달라 말의 억양처럼. 끝은 0.92 배로 살짝 내려간다.
- 한 글자 = 0.078초 ÷ 빠르기. 띄어쓰기는 0.6칸, 문장 부호는 2.2칸 쉰다.

## 핵심 코드 — 글자마다 모음 공명 블립
(발췌: demos/demosSfx.ts VOWEL_F · babble 을 정리 (osc · envG · filt 는 견본 도우미))
```ts
// 21 모음의 [F1, F2] — ㅏ ㅐ ㅑ ㅒ ㅓ ㅔ ㅕ ㅖ ㅗ ㅘ ㅙ ㅚ ㅛ ㅜ ㅝ ㅞ ㅟ ㅠ ㅡ ㅢ ㅣ
const VOWEL_F: [number, number][] = [[800, 1200], [700, 1700], [750, 1300], [700, 1750], [550, 1000], [500, 1800], [550, 1100],
  [500, 1800], [450, 850], [600, 1200], [600, 1600], [400, 1800], [450, 900], [350, 800], [500, 1000], [400, 1700],
  [350, 2000], [350, 850], [400, 1500], [350, 2000], [300, 2300]];

function babble(c: AudioContext, out: AudioNode, t0: number, text: string, voice = 320, speed = 1): number {
  const stepT = 0.078 / speed;
  let t = t0;
  for (const ch of text) {
    const code = ch.charCodeAt(0);
    if (code >= 0xac00 && code <= 0xd7a3) {
      const vi = Math.floor(((code - 0xac00) % 588) / 28);        // 모음 번호
      const [f1, f2] = VOWEL_F[vi] ?? [500, 1500];
      const f0 = voice * (1 + ((code % 7) - 3) * 0.045);           // 글자마다 억양
      const o = c.createOscillator();
      o.type = 'sawtooth';
      o.frequency.setValueAtTime(f0, t);
      o.frequency.exponentialRampToValueAtTime(f0 * 0.92, t + stepT * 0.9);
      const g = c.createGain();                                    // 6ms 어택 · 짧게 유지 · 감쇠
      g.gain.setValueAtTime(0.0001, t);
      g.gain.exponentialRampToValueAtTime(0.5, t + 0.006);
      g.gain.setValueAtTime(0.5, t + 0.006 + stepT * 0.35);
      g.gain.exponentialRampToValueAtTime(0.0001, t + 0.006 + stepT * 0.85);
      g.connect(out);
      const b1 = c.createBiquadFilter(), b2 = c.createBiquadFilter(), g2 = c.createGain();
      b1.type = b2.type = 'bandpass';
      b1.frequency.value = f1; b1.Q.value = 5;
      b2.frequency.value = f2; b2.Q.value = 6;
      g2.gain.value = 0.6;
      o.connect(b1).connect(g);
      o.connect(b2).connect(g2).connect(g);
      o.start(t); o.stop(t + stepT + 0.05);
      t += stepT;
    } else t += ch === ' ' ? stepT * 0.6 : stepT * 2.2;           // 띄어쓰기 · 문장 부호는 쉼
  }
  return t - t0;                                                   // 전체 길이 (말풍선 맞추기)
}
```

## 흔한 실수 · 확인 목록
- [ ] **글자마다 Math.random 음 높이를 쓰면 같은 대사가 매번 다르게 들려 어색하다** — 글자 코드(code % 7)로 정하면 같은 글자는 늘 같은 높이 — 말투처럼 들린다.
- [ ] **사인파로 만들면 모음 차이가 안 들린다** — 배음이 많은 톱니파를 공명 필터에 통과시켜야 F1 · F2 가 살아난다.
- [ ] **긴 설명을 모두 옹알이로 내면 금방 시끄럽다** — 짧은 대사 · 첫 문장에만, 소리 단추로 끌 수 있게.

## 완성 기준 체크리스트
- [ ] 「안녕! 나는 선배 윤정확이야.」가 글자마다 옹알옹알 들리고, ㅏ · ㅣ · ㅜ 가 서로 다르게 들린다
- [ ] 목소리 높이를 140Hz ↔ 600Hz 로 바꾸면 할아버지 ↔ 꼬마처럼 바뀐다
- [ ] 말풍선 글자가 소리와 같은 속도로 나타난다

## 이 기술 정보
- id: `i145` · 분류: 소리 › 효과음 (SFX) · 공통 · 난이도 보통 · 폰 부담 가벼움 (폰 OK) — 글자 하나 = 발진기 1 + 대역 필터 2 + 게인 2. 문장 하나 수십 개 — 가볍다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i145
- 쓰면 좋을 때: 대사가 많은 캐릭터에 목소리 느낌을 줄 때 (녹음 없이, 모든 언어) / 말풍선 글자가 하나씩 나타나는 연출과 함께
- 쓰지 말 때: 글을 실제로 읽어 줘야 할 때 — 음성 합성(u78) / 긴 설명문 전체 — 금방 귀가 피곤하다, 첫 몇 글자 · 짧은 대사에만

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### babble — `src/demos/demosSfx.ts:1277`
```ts
function babble(c: BA, out: AudioNode, t0: number, text: string, voice: number, speed: number): number {
  const stepT = 0.078 / speed;
  let t = t0;
  for (const ch of text) {
    const code = ch.charCodeAt(0);
    if (code >= 0xac00 && code <= 0xd7a3) {
      const vi = Math.floor(((code - 0xac00) % 588) / 28);
      const [f1, f2] = VOWEL_F[vi] ?? [500, 1500];
      const f0 = voice * (1 + ((code % 7) - 3) * 0.045);
      const o = osc(c, 'sawtooth', f0, t, stepT);
      o.frequency.exponentialRampToValueAtTime(f0 * 0.92, t + stepT * 0.9);
      const g = envG(c, out, t, 0.006, stepT * 0.35, stepT * 0.5, 0.5);
      const b1 = filt(c, 'bandpass', f1, 5);
      const b2 = filt(c, 'bandpass', f2, 6);
      o.connect(b1);
      o.connect(b2);
      b1.connect(g);
      chain(b2, gainN(c, 0.6), g);
      t += stepT;
    } else if (ch === ' ') t += stepT * 0.6;
    else t += stepT * 2.2;
  }
  return t - t0;
}
```

### VOWEL_F — `src/demos/demosSfx.ts:1253`
```ts
const VOWEL_F: [number, number][] = [
  [800, 1200], // ㅏ
  [700, 1700], // ㅐ
  [750, 1300], // ㅑ
  [700, 1750], // ㅒ
  [550, 1000], // ㅓ
  [500, 1800], // ㅔ
  [550, 1100], // ㅕ
  [500, 1800], // ㅖ
  [450, 850], // ㅗ
  [600, 1200], // ㅘ
  [600, 1600], // ㅙ
  [400, 1800], // ㅚ
  [450, 900], // ㅛ
  [350, 800], // ㅜ
  [500, 1000], // ㅝ
  [400, 1700], // ㅞ
  [350, 2000], // ㅟ
  [350, 850], // ㅠ
  [400, 1500], // ㅡ
  [350, 2000], // ㅢ
  [300, 2300], // ㅣ
];
```

## 관련 기술
- 먼저 알면 좋은 기술: [코드로 효과음 합성 (견본판 70가지)](https://ai-techstudio.web.app/ai/t/i499.md) `i499`
- 다음에 해 볼 기술: [읽어 주기 (음성 합성)](https://ai-techstudio.web.app/ai/t/u78.md) `u78`
- 참고 문서: [Wikipedia — Formant](https://en.wikipedia.org/wiki/Formant)
