# AI 꾸러미 — 코드로 효과음 합성 — Procedural SFX synthesis (Web Audio API)
> 소리 파일 없이 발진기 · 잡음 · 필터 · 소리 크기 봉투를 엮어 동전 · 점프 · 폭발 같은 효과음을 코드로 만든다.  
> 견본: https://ai-techstudio.web.app/#t/i499

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

## 주문서

### 만들어 줘: 코드로 효과음 합성 — Procedural SFX synthesis (Web Audio API)

#### 1. 목표
동전 줍기 「딩딩!」 효과음을 소리 파일 없이 Web Audio 로 합성해 줘. 느낌은 8비트 오락실.

#### 2. 핵심 기술 용어
- **Procedural SFX synthesis (Web Audio API)** — 코드로 효과음 합성
- **OscillatorNode · noise buffer** — 발진기(사인 · 네모 · 톱니) · 잡음
- **ADSR envelope (GainNode ramps)** — 소리 크기 봉투 — 어택 · 유지 · 감쇠
- **BiquadFilterNode · pitch sweep** — 필터 · 음 높이 미끄러짐

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

#### 4. 조건
- AudioContext 는 사용자가 처음 누를 때 만들거나 resume() — 그 전엔 소리가 안 난다 (특히 폰)
- 소리마다 매개변수(음 높이 · 꼬리 길이)를 받아 같은 함수로 여러 소리를 만들 수 있게
- 마지막에 주 GainNode 하나로 전체 크기를 제한해 겹쳐도 찢어지지 않게
- 소리 파일 · 외부 라이브러리 없이

#### 5. 완성 기준 (이게 보이면 성공)
- 단추를 누르면 바로(지연 없이) 동전 「딩딩!」 소리가 난다
- 음 높이 · 꼬리 길이 슬라이더를 바꾸면 소리가 그에 맞게 바뀐다
- 연타해도 소리가 찢어지거나 끊기지 않는다
- 폰에서 첫 터치 뒤 소리가 난다

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

## 원리
- 효과음은 「음원(발진기 · 잡음) → 필터 → 소리 크기 봉투 → 출력」 사슬이다.
- 동전: 네모파가 한 박자(0.075초) 뒤 4도 위로 뛰는 두 음. 점프: 음 높이가 위로 미끄러짐. 폭발: 저역 필터를 지난 잡음.
- 봉투는 GainNode 의 exponentialRamp 로 — 빠른 어택(2ms) · 짧은 유지 · 지수 감쇠가 「또렷한」 소리를 만든다.
- 모든 시간은 AudioContext.currentTime 기준으로 미리 예약하므로 화면 프레임과 상관없이 정확하다.

## 핵심 코드 — 동전 「딩딩!」 — 네모파 두 음 + 소리 크기 봉투
(발췌: demos/demosSfx.ts recCoin · envG 를 이어 붙임)
```ts
let ctx: AudioContext | null = null;
const st2 = (semi: number) => Math.pow(2, semi / 12); // 반음 → 배수

/** 0 → peak (a초) → 유지(hold) → 지수 감쇠(d초). exponentialRamp 는 0 으로 못 가서 0.0001 */
function envG(c: AudioContext, out: AudioNode, t0: number, a: number, hold: number, d: number, peak: number) {
  const g = c.createGain();
  g.gain.setValueAtTime(0.0001, t0);
  g.gain.exponentialRampToValueAtTime(peak, t0 + Math.max(0.0005, a));
  g.gain.setValueAtTime(peak, t0 + a + hold);
  g.gain.exponentialRampToValueAtTime(0.0001, t0 + a + hold + d);
  g.connect(out);
  return g;
}

function coin(c: AudioContext, out: AudioNode, t0: number, pitch = 0, tail = 0.35) {
  const f1 = 987.77 * st2(pitch);                       // B5
  const o = c.createOscillator();
  o.type = 'square';
  o.frequency.setValueAtTime(f1, t0);
  o.frequency.setValueAtTime(f1 * st2(5), t0 + 0.075);  // 한 박자 뒤 4도 위
  const lp = c.createBiquadFilter();
  lp.type = 'lowpass';
  lp.frequency.value = 7000;                            // 네모파의 날카로운 고역 다듬기
  o.connect(lp).connect(envG(c, out, t0, 0.002, 0.07, tail, 0.3));
  o.start(t0);
  o.stop(t0 + 0.08 + tail + 0.05);
}

button.onclick = async () => {
  ctx ??= new AudioContext();                           // 사용자가 누른 뒤에 만든다
  await ctx.resume();
  coin(ctx, ctx.destination, ctx.currentTime + 0.01);
};
```

## 흔한 실수 · 확인 목록
- [ ] **페이지가 열리자마자 소리를 내려 하면 아무 소리도 안 난다** — 브라우저 자동 재생 정책 — 첫 클릭 · 터치 안에서 AudioContext 를 만들거나 resume().
- [ ] **exponentialRampToValueAtTime(0) 은 오류** — 지수 곡선은 0 에 닿을 수 없다. 0.0001 로 줄이고 끝나면 stop().
- [ ] **여러 소리가 겹치면 찢어진다 (클리핑)** — 모든 소리를 주 GainNode(0.5 안팎) 하나로 모으고, 필요하면 DynamicsCompressorNode.
- [ ] **파형 그림만 보고 고르면 실제로 들으면 어색하다** — 이 사이트에서 이름 · 모양만 보고 골랐다가 지적받았다. 반드시 들어 보고, 가장 큰 소리를 타격 순간에 맞춘다.
- [ ] **같은 소리를 똑같이 반복하면 금방 거슬린다** — 음 높이를 ±1~2반음 · 크기를 조금씩 무작위로 (라운드 로빈).

## 완성 기준 체크리스트
- [ ] 단추를 누르면 바로(지연 없이) 동전 「딩딩!」 소리가 난다
- [ ] 음 높이 · 꼬리 길이 슬라이더를 바꾸면 소리가 그에 맞게 바뀐다
- [ ] 연타해도 소리가 찢어지거나 끊기지 않는다
- [ ] 폰에서 첫 터치 뒤 소리가 난다

## 이 기술 정보
- id: `i499` · 분류: 소리 › 효과음 (SFX) · 공통 · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 노드 몇 개를 만들고 버린다. 수십 개가 한꺼번에 울려도 가볍다. 잡음 버퍼는 한 번 만들어 돌려 쓴다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i499
- 쓰면 좋을 때: 소리 파일을 구하거나 만들기 어려울 때 · 용량을 줄일 때 / 같은 소리를 높이 · 길이만 바꿔 여러 번 쓸 때 (콤보 · 점수 올라가기)
- 쓰지 말 때: 목소리 · 악기처럼 복잡한 실제 소리 — 녹음 파일이 낫다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### recCoin — `src/demos/demosSfx.ts:1056`
```ts
const recCoin: Rec = (c, out, t0, p) => {
  const { pitch = 0, tail = 0.35 } = p;
  const f1 = 987.77 * st2(pitch);
  const o = osc(c, 'square', f1, t0, 0.08 + tail);
  o.frequency.setValueAtTime(f1 * st2(5), t0 + 0.075);
  chain(o, filt(c, 'lowpass', 7000), envG(c, out, t0, 0.002, 0.07, tail, 0.3));
};
```

### envG — `src/demos/demosSfx.ts:159`
```ts
function envG(c: BA, out: AudioNode, t0: number, a: number, hold: number, d: number, peak: number): GainNode {
  const g = c.createGain();
  g.gain.setValueAtTime(0.0001, t0);
  g.gain.exponentialRampToValueAtTime(peak, t0 + Math.max(0.0005, a));
  g.gain.setValueAtTime(peak, t0 + a + hold);
  g.gain.exponentialRampToValueAtTime(0.0001, t0 + a + hold + d);
  g.connect(out);
  return g;
}
```

### osc — `src/demos/demosSfx.ts:150`
```ts
function osc(c: BA, type: OscillatorType, f: number, t0: number, dur: number): OscillatorNode {
  const o = c.createOscillator();
  o.type = type;
  o.frequency.setValueAtTime(f, t0);
  o.start(t0);
  if (dur > 0) o.stop(t0 + dur + 0.05);
  return o;
}
```

## 관련 기술
- 먼저 알면 좋은 기술: [정답 아르페지오 (조성 맞춤)](https://ai-techstudio.web.app/ai/t/i141.md) `i141`
- 다음에 해 볼 기술: [층 쌓기 (타격 + 몸통 + 꼬리)](https://ai-techstudio.web.app/ai/t/i148.md) `i148` · [반복 피로 막기 (라운드 로빈)](https://ai-techstudio.web.app/ai/t/i147.md) `i147`
- 참고 문서: [MDN — Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API) · [MDN — OscillatorNode](https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode) · [sfxr (브라우저판 jsfxr)](https://sfxr.me/)
