# AI 꾸러미 — 메아리 · 딜레이 「야호~」 — Feedback delay (echo) — DelayNode
> 소리를 DelayNode 로 늦춰 되먹임(0.6 아래)으로 돌리고 고리 안에 저역 필터를 넣어, 「야호~」 메아리가 점점 작고 먹먹하게 되돌아오게 한다.  
> 견본: https://ai-techstudio.web.app/#t/i160

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

## 주문서

### 만들어 줘: 메아리 · 딜레이 「야호~」 — Feedback delay (echo) — DelayNode

#### 1. 목표
강 골짜기 「야호~」 메아리 놀이에 메아리를 넣어 줘 — DelayNode 와 되먹임(0.6 넘지 않게), 고리 안에 저역 필터를 넣어 메아리가 점점 작고 먹먹하게. 느낌은 넓은 산골짜기.

#### 2. 핵심 기술 용어
- **Feedback delay (echo) — DelayNode** — 되먹임 딜레이 — 늦춘 소리를 다시 늦추기
- **Feedback gain < 1** — 돌 때마다 줄어드는 비율 — 1 이상이면 끝없이 커짐
- **Damping filter in the loop** — 고리 안 저역 필터 — 돌 때마다 먹먹하게
- **Formant vowel glide** — 모음 공명이 「아 → 오」로 미끄러짐

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

#### 4. 조건
- 되먹임 게인은 Math.min(0.6, 값) 처럼 반드시 위를 막는다
- 고리 = 딜레이 → 저역 필터 → 되먹임 게인 → 딜레이, 출력은 필터 뒤에서
- createDelay(최대 길이) 의 최대 길이를 손잡이 최대(0.7초)보다 크게
- 원래 목소리도 출력에 바로 — 메아리만 나오면 이상하다

#### 5. 완성 기준 (이게 보이면 성공)
- 「야호~」 뒤로 0.32초 간격 메아리가 여러 번, 점점 작고 먹먹하게 돌아온다
- 간격 손잡이로 골짜기 넓이(0.1 ~ 0.7초)가 바뀐다
- 되먹임을 0.6 까지 올려도 소리가 끝없이 커지지 않는다

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

## 원리
- 메아리 = 같은 소리가 조금 늦게(0.32초) 다시 들리는 것. DelayNode 로 늦춘다.
- 늦춘 소리를 다시 딜레이 입구로 돌려보내면(되먹임) 메아리가 여러 번 온다. 돌 때마다 되먹임 배수(0.5)만큼 작아진다.
- 되먹임은 반드시 1 보다 작게 — 견본은 0.6 으로 막는다. 1 이상이면 소리가 끝없이 커진다.
- 고리 안에 2200Hz 저역 필터를 두면 돌 때마다 고음이 깎여 먼 메아리일수록 먹먹하다 — 진짜 골짜기처럼.
- 「야호」 목소리: 톱니파 280 → 420 → 330Hz, 두 공명 필터가 800 → 480 · 1250 → 850 Hz 로 미끄러져 「아 → 오」.

## 핵심 코드 — 되먹임 딜레이 + 고리 안 저역 필터
(발췌: demos/demosSfx.ts recEcho 의 메아리 부분을 정리)
```ts
/** voice 에 이어 둔 소리를 메아리로 — time 간격(초), fb 되먹임 */
function echo(c: AudioContext, voice: AudioNode, out: AudioNode, time = 0.32, fb = 0.5) {
  const dl = c.createDelay(2);                       // 최대 2초
  dl.delayTime.value = time;
  const lp = c.createBiquadFilter();
  lp.type = 'lowpass';
  lp.frequency.value = 2200;                         // 돌 때마다 먹먹하게
  const fg = c.createGain();
  fg.gain.value = Math.min(0.6, fb);                 // 1 이상이면 끝없이 커진다 — 꼭 막기
  voice.connect(dl);
  dl.connect(lp);
  lp.connect(fg);
  fg.connect(dl);                                    // 고리
  lp.connect(out);                                   // 메아리 출력
}
// 「야호~」 목소리: 톱니 280 → 420 → 330Hz + 공명 800 → 480 · 1250 → 850 (아 → 오)
// const o = ctx.createOscillator(); o.type = 'sawtooth';
// o.frequency.setValueAtTime(280, t0); o.frequency.linearRampToValueAtTime(420, t0 + 0.2); o.frequency.linearRampToValueAtTime(330, t0 + 0.6);
// voice(봉투 GainNode) 를 out 에도 바로 잇고, echo(ctx, voice, out) 로 메아리를 덧붙인다
```

## 흔한 실수 · 확인 목록
- [ ] **되먹임을 1 이상으로 두면 소리가 끝없이 커져 귀가 아프다** — 손잡이 범위를 0 ~ 0.6 으로, 코드에서도 Math.min(0.6, fb) 로 이중으로 막는다.
- [ ] **DelayNode 없이 게인 · 필터끼리 고리를 만들면 소리가 안 난다** — Web Audio 는 DelayNode 가 없는 고리를 막아 버린다 — 딜레이 → 필터 → 게인 → 딜레이 처럼 고리 안에 꼭 딜레이를 둔다.
- [ ] **메아리가 원래 소리만큼 또렷해서 가짜 같다** — 고리 안 저역 필터(2200Hz)로 돌 때마다 고음을 깎는다.

## 완성 기준 체크리스트
- [ ] 「야호~」 뒤로 0.32초 간격 메아리가 여러 번, 점점 작고 먹먹하게 돌아온다
- [ ] 간격 손잡이로 골짜기 넓이(0.1 ~ 0.7초)가 바뀐다
- [ ] 되먹임을 0.6 까지 올려도 소리가 끝없이 커지지 않는다

## 이 기술 정보
- id: `i160` · 분류: 소리 › 효과음 (SFX) · 공통 · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 딜레이 1 + 필터 1 + 게인 1. 아주 가볍다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i160
- 쓰면 좋을 때: 골짜기 · 동굴 같은 넓은 장소 연출 / 말소리 놀이 (메아리 따라 하기)
- 쓰지 말 때: 방 울림처럼 「뭉친」 공간감 — 잔향(i159)이 맞다 / 빠른 효과음이 계속 나는 곳 — 메아리가 겹겹이 쌓여 지저분해진다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### recEcho — `src/demos/demosSfx.ts:1549`
```ts
const recEcho: Rec = (c, out, t0, p) => {
  const { time = 0.32, fb = 0.5 } = p;
  // 「야호~」: 톱니파 음 높이 미끄럼 + 모음 필터 (아 → 오)
  const o = osc(c, 'sawtooth', 300, t0, 0.62);
  o.frequency.setValueAtTime(280, t0);
  o.frequency.linearRampToValueAtTime(420, t0 + 0.2);
  o.frequency.linearRampToValueAtTime(330, t0 + 0.6);
  const v1 = filt(c, 'bandpass', 800, 5);
  v1.frequency.setValueAtTime(800, t0);
  v1.frequency.linearRampToValueAtTime(480, t0 + 0.45);
  const v2 = filt(c, 'bandpass', 1250, 6);
  v2.frequency.setValueAtTime(1250, t0);
  v2.frequency.linearRampToValueAtTime(850, t0 + 0.45);
  const voice = envG(c, out, t0, 0.04, 0.35, 0.2, 0.9);
  o.connect(v1);
  o.connect(v2);
  v1.connect(voice);
  chain(v2, gainN(c, 0.6), voice);
  const dl = c.createDelay(2);
  dl.delayTime.value = time;
  const lp = filt(c, 'lowpass', 2200);
  const fg = gainN(c, Math.min(0.6, fb));
  voice.connect(dl);
  chain(dl, lp, fg, dl);
  lp.connect(out);
};
```

## 관련 기술
- 먼저 알면 좋은 기술: [방 크기 잔향](https://ai-techstudio.web.app/ai/t/i159.md) `i159`
- 다음에 해 볼 기술: [그레인 합성](https://ai-techstudio.web.app/ai/t/i169.md) `i169`
- 참고 문서: [MDN — DelayNode](https://developer.mozilla.org/en-US/docs/Web/API/DelayNode)
