# AI 꾸러미 — 도플러 「니이이용」 — Doppler effect
> 지나가는 차의 거리 · 다가오는 속도를 미리 곡선으로 계산해 음 높이(f × c/(c + v)) · 크기 · 좌우를 함께 바꿔, 다가올 땐 높고 지나가면 낮아지는 「니이이용」을 만든다.  
> 견본: https://ai-techstudio.web.app/#t/i157

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

## 주문서

### 만들어 줘: 도플러 「니이이용」 — Doppler effect

#### 1. 목표
지나가는 차 「니이이용」에 도플러 효과를 넣어 줘 — 거리와 다가오는 속도로 음 높이 · 크기 · 좌우를 곡선으로 계산해서. 느낌은 만화처럼 과장된.

#### 2. 핵심 기술 용어
- **Doppler effect** — 도플러 효과 — 다가오면 높게, 멀어지면 낮게
- **Radial velocity** — 듣는 사람 쪽으로 다가오는(멀어지는) 빠르기
- **AudioParam.setValueCurveAtTime** — 미리 계산한 값 배열을 시간에 맞춰 재생
- **StereoPannerNode · distance attenuation** — 좌우 이동 · 멀수록 작게

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

#### 4. 조건
- 진동수 · 크기 · 좌우를 같은 위치 계산에서 함께 — 따로 계산하면 어긋난다
- 곡선은 setValueCurveAtTime 으로 (매 프레임 바꾸지 않는다)
- 소리 빠르기 343m/s, 듣는 사람과 길 사이 거리(6m)를 매개변수로
- 속도 손잡이 범위는 8 ~ 80m/s — 느리면 차이가 안 들린다

#### 5. 완성 기준 (이게 보이면 성공)
- 차가 왼쪽에서 다가올 때 높고 크다가, 앞을 지나는 순간 음이 뚝 떨어지며 오른쪽으로 사라진다
- 속도를 80m/s 로 올리면 음 떨어짐이 더 크고 빠르다
- 이어폰으로 들으면 왼쪽 → 오른쪽으로 옮겨 간다

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

## 원리
- 차는 x = −L + v·t 로 지나가고, 듣는 사람은 길에서 6m 떨어져 있다. 거리 r = √(x² + 6²).
- 멀어지는 속도 vr = v · x / r (다가오면 음수). 들리는 음 = 300Hz × 343 / (343 + vr) — 343 은 소리 빠르기(m/s).
- 크기 = 0.9 / (1 + r/6), 좌우 = x / r (−1 왼쪽 ~ 1 오른쪽).
- 2.6초를 128점으로 미리 계산해 setValueCurveAtTime 으로 진동수 · 크기 · 좌우에 한꺼번에 건다 — 셋이 정확히 같이 움직인다.
- 소리 재료는 톱니 + 반 높이 네모(0.4) → 1800Hz 저역 필터 — 엔진처럼 웅웅.

## 핵심 코드 — 도플러 곡선 미리 계산 + 한꺼번에 걸기
(발췌: demos/demosSfx.ts recDoppler 를 정리)
```ts
function passBy(c: AudioContext, out: AudioNode, t0: number, speed = 40) {
  const D = 2.6, N = 128, L = (speed * D) / 2;            // 2.6초 동안 −L → +L
  const fq = new Float32Array(N), gv = new Float32Array(N), pv = new Float32Array(N);
  for (let i = 0; i < N; i++) {
    const tt = (i / (N - 1)) * D;
    const x = -L + speed * tt;
    const r = Math.hypot(x, 6);                           // 길에서 6m 떨어져 듣는다
    const vr = (speed * x) / r;                           // + = 멀어짐
    fq[i] = 300 * (343 / (343 + vr));                     // 도플러
    gv[i] = 0.9 / (1 + r / 6);                            // 멀수록 작게
    pv[i] = Math.max(-1, Math.min(1, x / r));             // 왼쪽 → 오른쪽
  }
  const o = c.createOscillator(), o2 = c.createOscillator();
  o.type = 'sawtooth'; o2.type = 'square';
  o.frequency.setValueCurveAtTime(fq, t0, D);
  o2.frequency.setValueCurveAtTime(fq.map((f) => f / 2), t0, D);
  const g = c.createGain();
  g.gain.setValueAtTime(0, t0);
  g.gain.setValueCurveAtTime(gv, t0, D);
  const pn = c.createStereoPanner();
  pn.pan.setValueCurveAtTime(pv, t0, D);
  const lp = c.createBiquadFilter();
  lp.type = 'lowpass'; lp.frequency.value = 1800; lp.Q.value = 1;
  const g2 = c.createGain();
  g2.gain.value = 0.4;
  o.connect(lp); o2.connect(g2).connect(lp);
  lp.connect(g).connect(pn).connect(out);
  o.start(t0); o2.start(t0);
  o.stop(t0 + D + 0.05); o2.stop(t0 + D + 0.05);
}
```

## 흔한 실수 · 확인 목록
- [ ] **음 높이만 바꾸고 크기 · 좌우를 그대로 두면 「지나간다」는 느낌이 안 난다** — 세 값을 같은 위치 계산에서 함께 — 지나가는 순간 크기가 가장 크고 좌우가 가운데.
- [ ] **곡선을 건 AudioParam 에 또 setValueAtTime 을 겹치면 오류가 난다** — setValueCurveAtTime 구간과 겹치는 다른 예약은 허용되지 않는다. 곡선 시작 전에만 값을 둔다.
- [ ] **부호를 거꾸로 쓰면 다가올 때 낮아진다** — vr 은 멀어질 때 + — 343 / (343 + vr) 이 1 보다 작아져 낮아지는 쪽이 맞는지 들어서 확인.

## 완성 기준 체크리스트
- [ ] 차가 왼쪽에서 다가올 때 높고 크다가, 앞을 지나는 순간 음이 뚝 떨어지며 오른쪽으로 사라진다
- [ ] 속도를 80m/s 로 올리면 음 떨어짐이 더 크고 빠르다
- [ ] 이어폰으로 들으면 왼쪽 → 오른쪽으로 옮겨 간다

## 이 기술 정보
- id: `i157` · 분류: 소리 › 효과음 (SFX) · 공통 · 난이도 보통 · 폰 부담 가벼움 (폰 OK) — 128점 배열 셋을 한 번 계산하고 노드 몇 개 — 가볍다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i157
- 쓰면 좋을 때: 빠르게 스쳐 가는 물체가 있는 연출 / 「소리의 빠르기 · 진동수」 과학 체험
- 쓰지 말 때: 느린 물체 — 음 변화가 너무 작아 안 들린다 (몇 m/s 는 의미 없음) / 3D 엔진의 위치 소리를 이미 쓰고 있을 때 — 엔진의 도플러 설정을 켠다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### recDoppler — `src/demos/demosSfx.ts:1477`
```ts
const recDoppler: Rec = (c, out, t0, p) => {
  const { speed = 40 } = p;
  const D = 2.6;
  const N = 128;
  const L = (speed * D) / 2;
  const fq = new Float32Array(N);
  const gv = new Float32Array(N);
  const pv = new Float32Array(N);
  for (let i = 0; i < N; i++) {
    const tt = (i / (N - 1)) * D;
    const x = -L + speed * tt;
    const rr0 = Math.hypot(x, 6);
    const vr = (speed * x) / rr0; // + = 멀어짐
    fq[i] = 300 * (343 / (343 + vr));
    gv[i] = 0.9 / (1 + rr0 / 6);
    pv[i] = clamp(x / rr0, -1, 1);
  }
  const o = osc(c, 'sawtooth', 300, t0, D);
  o.frequency.setValueCurveAtTime(fq, t0, D);
  const o2 = osc(c, 'square', 150, t0, D);
  o2.frequency.setValueCurveAtTime(fq.map((f) => f / 2), t0, D);
  const g = c.createGain();
  g.gain.setValueAtTime(0, t0);
  g.gain.setValueCurveAtTime(gv, t0, D);
  const pn = c.createStereoPanner();
  pn.pan.setValueCurveAtTime(pv, t0, D);
  const lp = filt(c, 'lowpass', 1800, 1);
  o.connect(lp);
  chain(o2, gainN(c, 0.4), lp);
  chain(lp, g, pn, out);
};
```

## 관련 기술
- 먼저 알면 좋은 기술: [공간 소리 (PannerNode)](https://ai-techstudio.web.app/ai/t/i31.md) `i31` · [엔진 소리 (속도 연동)](https://ai-techstudio.web.app/ai/t/i156.md) `i156`
- 다음에 해 볼 기술: [가림 저역 필터 (벽 뒤 소리)](https://ai-techstudio.web.app/ai/t/i158.md) `i158`
- 참고 문서: [Wikipedia — Doppler effect](https://en.wikipedia.org/wiki/Doppler_effect) · [MDN — AudioParam.setValueCurveAtTime()](https://developer.mozilla.org/en-US/docs/Web/API/AudioParam/setValueCurveAtTime)
