# AI 꾸러미 — 방 크기 잔향 — Convolution reverb — ConvolverNode
> 지수로 사라지며 점점 먹먹해지는 잡음을 코드로 만들어 ConvolverNode 의 울림으로 쓰고, 꼬리 길이(방 크기)와 섞는 양으로 작은 방 ~ 큰 동굴 느낌을 낸다.  
> 견본: https://ai-techstudio.web.app/#t/i159

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

## 주문서

### 만들어 줘: 방 크기 잔향 — Convolution reverb — ConvolverNode

#### 1. 목표
동굴 · 보물 방에 방 크기 잔향을 넣어 줘 — 지수로 사라지는 잡음으로 울림을 코드로 만들어 ConvolverNode 에 쓰고, 방 크기 = 꼬리 길이, 섞는 양을 조절하게. 분위기는 깊고 신비한 동굴.

#### 2. 핵심 기술 용어
- **Convolution reverb — ConvolverNode** — 울림(임펄스 응답)을 소리에 덧씌우는 잔향
- **Synthetic impulse response (exponential decay noise)** — 코드로 만든 울림 — 지수로 사라지는 잡음
- **Dry / wet mix** — 원래 소리 · 울린 소리 섞는 비율
- **RT60 (reverb time)** — 잔향 시간 — 울림이 사라지는 길이

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

#### 4. 조건
- 울림 버퍼는 장소 · 크기마다 한 번만 만들고 돌려 쓴다 (소리마다 만들지 않는다)
- 두 채널을 다른 시드 잡음으로 — 같으면 넓이감이 없다
- 뒤로 갈수록 먹먹하게 (고음 먼저 사라짐) — 그냥 지수 감쇠 잡음은 쇳소리처럼 들린다
- 마른 소리 · 젖은 소리 둘 다 출력으로 — 섞는 양 손잡이

#### 5. 완성 기준 (이게 보이면 성공)
- 「쿵」 소리 뒤에 방 크기만큼 울림 꼬리가 남는다
- 방 크기를 0.2 → 4초로 바꾸면 작은 방 → 큰 동굴처럼 들린다
- 섞는 양 0 이면 마른 소리, 1 이면 멀리서 울리는 소리

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

## 원리
- 방에서 손뼉을 치면 「쾅 … 웅 …」 울림이 남는다. 그 울림 모양(임펄스 응답)을 소리에 덧씌우는 것이 컨볼루션 잔향이다.
- 울림을 녹음하지 않고 코드로 만든다: 길이 = 방 크기(0.2 ~ 4초), 스테레오 두 채널에 따로 만든 잡음.
- 잡음을 한 극 저역 필터에 통과시키되 뒤로 갈수록 더 먹먹하게(k = 0.15 + 0.8x), 크기는 (1 − x)^2.2 × e^(−3x) 로 사라지게 — 진짜 방처럼 고음이 먼저 사라진다.
- 원래 소리(마른 소리 0.8)와 ConvolverNode 를 지난 소리(젖은 소리 = 섞는 양 × 1.2)를 함께 내보낸다.

## 핵심 코드 — 코드로 만든 울림 + ConvolverNode
(발췌: demos/demosSfx.ts makeIR · recReverb 를 정리)
```ts
function rng(seed: number) {                              // 시드 난수 (견본과 같은 꼴)
  let a = seed >>> 0;
  return () => { a = (a + 0x6d2b79f5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
}
/** 길이 sec 초 울림 — 지수로 사라지고 뒤로 갈수록 먹먹해지는 잡음 */
function makeIR(c: BaseAudioContext, sec: number, seed = 3): AudioBuffer {
  const len = Math.max(1, Math.floor(c.sampleRate * sec));
  const b = c.createBuffer(2, len, c.sampleRate);
  for (let ch = 0; ch < 2; ch++) {
    const d = b.getChannelData(ch), r = rng(seed + ch * 17); // 채널마다 다른 잡음
    let lp = 0;
    for (let i = 0; i < len; i++) {
      const x = i / len;
      const k = 0.15 + 0.8 * x;                            // 뒤로 갈수록 고역이 먼저 사라짐
      lp = lp + (1 - k) * ((r() * 2 - 1) - lp);
      d[i] = lp * Math.pow(1 - x, 2.2) * Math.exp(-x * 3);
    }
  }
  return b;
}
/** 장소마다 하나: 소리는 dry 와 cv 둘 다에 잇는다 */
function room(c: AudioContext, out: AudioNode, size = 1.6, wet = 0.6) {
  const cv = c.createConvolver();
  cv.buffer = makeIR(c, size);
  const wg = c.createGain(); wg.gain.value = wet * 1.2;
  const dry = c.createGain(); dry.gain.value = 0.8;
  cv.connect(wg).connect(out);
  dry.connect(out);
  return { dry, cv };                                       // 예: env.connect(dry); env.connect(cv);
}
```

## 흔한 실수 · 확인 목록
- [ ] **그냥 지수 감쇠 백색 잡음으로 울림을 만들면 쇳소리 · 바람 소리처럼 들린다** — 진짜 방은 고음이 먼저 사라진다. 뒤로 갈수록 저역 필터를 세게 (k = 0.15 + 0.8x).
- [ ] **효과음마다 ConvolverNode 와 울림을 새로 만들면 폰에서 버벅인다** — 울림 버퍼 계산 · 컨볼루션 모두 무겁다. 장소마다 하나 만들어 두고 모든 소리가 그리로 보낸다.
- [ ] **ConvolverNode 의 기본 normalize 때문에 크기가 예상과 다르다** — 기본값 true 는 울림 크기를 자동으로 맞춘다. 섞는 양은 젖은 쪽 게인으로 조절한다.

## 완성 기준 체크리스트
- [ ] 「쿵」 소리 뒤에 방 크기만큼 울림 꼬리가 남는다
- [ ] 방 크기를 0.2 → 4초로 바꾸면 작은 방 → 큰 동굴처럼 들린다
- [ ] 섞는 양 0 이면 마른 소리, 1 이면 멀리서 울리는 소리

## 이 기술 정보
- id: `i159` · 분류: 소리 › 효과음 (SFX) · 공통 · 난이도 보통 · 폰 부담 보통 (폰 주의) — 컨볼루션은 울림 길이에 비례해 무겁다 — 4초 스테레오 = 약 38만 표본. 장소마다 하나만 두고 공유하면 폰도 괜찮다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i159
- 쓰면 좋을 때: 장소 분위기 (동굴 · 강당 · 작은 방)를 한 번에 바꿀 때 / 마른 합성 효과음에 공간감을 줄 때
- 쓰지 말 때: 모든 소리마다 컨볼버를 새로 만들기 — 장소마다 하나 만들어 여러 소리가 함께 쓴다 / 또렷해야 하는 안내 말소리 — 잔향을 아주 적게

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### makeIR — `src/demos/demosSfx.ts:207`
```ts
function makeIR(c: BA, sec: number, seed = 3): AudioBuffer {
  const len = Math.max(1, Math.floor(c.sampleRate * sec));
  const b = c.createBuffer(2, len, c.sampleRate);
  for (let ch = 0; ch < 2; ch++) {
    const d = b.getChannelData(ch);
    const r = rng(seed + ch * 17);
    let lp = 0;
    for (let i = 0; i < len; i++) {
      const x = i / len;
      const k = 0.15 + 0.8 * x; // 뒤로 갈수록 먹먹하게 (고역이 먼저 사라짐)
      lp = lp + (1 - k) * ((r() * 2 - 1) - lp);
      d[i] = lp * Math.pow(1 - x, 2.2) * Math.exp(-x * 3);
    }
  }
  return b;
}
```

### recReverb — `src/demos/demosSfx.ts:1529`
```ts
const recReverb: Rec = (c, out, t0, p, r) => {
  const { size = 1.6, wet = 0.6 } = p;
  const cv = c.createConvolver();
  cv.buffer = makeIR(c, size);
  const wg = gainN(c, wet * 1.2);
  chain(cv, wg, out);
  const dry = gainN(c, 0.8);
  dry.connect(out);
  const hit = (t: number, f: number): void => {
    const n = noise(c, t, 0.05, r);
    const e = envG(c, dry, t, 0.001, 0.005, 0.04, 0.8);
    chain(n, filt(c, 'bandpass', f, 0.9), e);
    e.connect(cv);
  };
  hit(t0, 1300);
  const pl = osc(c, 'triangle', 392, t0 + 0.25, 0.3);
  const e2 = envG(c, dry, t0 + 0.25, 0.003, 0, 0.25, 0.45);
  pl.connect(e2);
  e2.connect(cv);
};
```

## 관련 기술
- 먼저 알면 좋은 기술: [가림 저역 필터 (벽 뒤 소리)](https://ai-techstudio.web.app/ai/t/i158.md) `i158`
- 다음에 해 볼 기술: [메아리 · 딜레이 「야호~」](https://ai-techstudio.web.app/ai/t/i160.md) `i160`
- 참고 문서: [MDN — ConvolverNode](https://developer.mozilla.org/en-US/docs/Web/API/ConvolverNode)
