# AI 꾸러미 — 층 쌓기 (타격 + 몸통 + 꼬리) — Layered sound design (transient + body + tail)
> 효과음 하나를 짧은 잡음 타격 · 내려가는 몸통 · 잔향 꼬리 세 층으로 나눠 층마다 따로 사라지게 해서, 도장 「쾅」이 또렷하면서도 묵직하게 들린다.  
> 견본: https://ai-techstudio.web.app/#t/i148

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

## 주문서

### 만들어 줘: 층 쌓기 (타격 + 몸통 + 꼬리) — Layered sound design (transient + body + tail)

#### 1. 목표
도장 「쾅」 효과음을 세 층(짧은 잡음 타격 + 내려가는 몸통 + 잔향 꼬리)으로 합성해 줘 — 층마다 따로 감쇠하고 따로 끄고 켤 수 있게. 느낌은 묵직하고 또렷한.

#### 2. 핵심 기술 용어
- **Layered sound design (transient + body + tail)** — 층 쌓기 — 타격 · 몸통 · 꼬리
- **Transient** — 맨 앞 아주 짧은 「딱」 — 또렷함을 만든다
- **Pitch drop (exponential sweep)** — 음 높이가 빠르게 떨어지는 몸통 — 무게감
- **Filtered noise tail** — 닫히는 저역 필터를 지난 잡음 꼬리 — 울림

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

#### 4. 조건
- 층마다 따로 봉투(감쇠) — 한 봉투에 다 묶지 않는다
- 가장 큰 순간(타격 · 몸통 시작)을 화면의 「쾅」 순간과 같은 시각에
- 층마다 켬/끔 토글로 층의 역할을 들려준다
- 잡음 버퍼는 한 번 만들어 돌려 쓴다

#### 5. 완성 기준 (이게 보이면 성공)
- 도장이 내려오는 순간 「딱 + 쿵 + 웅~」이 한 소리로 들린다
- 타격만 끄면 뭉툭해지고, 몸통만 끄면 가벼워지고, 꼬리만 끄면 메마르게 들린다
- 소리 그림 아래 세 줄에 층마다 다른 길이가 보인다

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

## 원리
- 타격: 고역 통과(2000Hz) 잡음을 0.5ms 어택 · 18ms 감쇠로 — 귀가 「언제」를 알아듣는 딱 소리.
- 몸통: 사인 190Hz → 58Hz 로 0.26초에 떨어지고(크기 0.85), 세모파 380 → 120Hz 를 0.12초 살짝 겹친다 — 「무게」.
- 꼬리: 저역 필터가 1400 → 300Hz 로 0.9초에 닫히는 잡음, 30ms 어택 · 0.85초 감쇠 · 작게(0.22) — 방의 「울림」.
- 층마다 길이 · 감쇠가 달라서 처음엔 또렷하고 끝은 부드럽게 사라진다. 층을 하나씩 끄면 각 층이 맡은 일이 들린다.

## 핵심 코드 — 세 층 「쾅」
(발췌: demos/demosSfx.ts layHit · layBody · layTail · recLayers 를 정리 (noise · osc · envG · filt 는 견본 도우미))
```ts
// 도우미 (견본과 같음): noise(c, t0, dur) 잡음 소스 · osc(c, type, f, t0, dur) · envG(c, out, t0, 어택, 유지, 감쇠, 최고)
// filt(c, type, f, q?) 필터 · chain(a, b, c…) 차례로 잇기
function stamp(c: AudioContext, out: AudioNode, t0: number, on = { hit: true, body: true, tail: true }) {
  if (on.hit) {                                                    // 타격 — 또렷함
    chain(noise(c, t0, 0.03), filt(c, 'highpass', 2000), envG(c, out, t0, 0.0005, 0.002, 0.018, 0.9));
  }
  if (on.body) {                                                   // 몸통 — 무게
    const o = osc(c, 'sine', 190, t0, 0.32);
    o.frequency.exponentialRampToValueAtTime(58, t0 + 0.26);
    o.connect(envG(c, out, t0, 0.002, 0.02, 0.26, 0.85));
    const tr = osc(c, 'triangle', 380, t0, 0.15);
    tr.frequency.exponentialRampToValueAtTime(120, t0 + 0.12);
    tr.connect(envG(c, out, t0, 0.002, 0, 0.12, 0.25));
  }
  if (on.tail) {                                                   // 꼬리 — 울림
    const lp = filt(c, 'lowpass', 1400);
    lp.frequency.setValueAtTime(1400, t0);
    lp.frequency.exponentialRampToValueAtTime(300, t0 + 0.9);
    chain(noise(c, t0, 1), lp, envG(c, out, t0 + 0.01, 0.03, 0, 0.85, 0.22));
  }
}
```

## 흔한 실수 · 확인 목록
- [ ] **한 봉투로 세 층을 함께 줄이면 꼬리가 타격과 같이 잘린다** — 층마다 따로 envG — 타격 18ms, 몸통 0.26초, 꼬리 0.85초.
- [ ] **폰 스피커에서는 「쿵」이 거의 안 들린다** — 58Hz 몸통은 작은 스피커가 못 낸다. 타격 · 세모파(380Hz) 층이 그 몫을 한다 — 빼지 않는다.
- [ ] **소리의 최고점이 화면 「쾅」보다 늦으면 굼떠 보인다** — 타격 층 어택을 0.5ms 로 두고, 도장이 닿는 프레임에 t0 를 맞춘다. 들어 보고 맞출 것.

## 완성 기준 체크리스트
- [ ] 도장이 내려오는 순간 「딱 + 쿵 + 웅~」이 한 소리로 들린다
- [ ] 타격만 끄면 뭉툭해지고, 몸통만 끄면 가벼워지고, 꼬리만 끄면 메마르게 들린다
- [ ] 소리 그림 아래 세 줄에 층마다 다른 길이가 보인다

## 이 기술 정보
- id: `i148` · 분류: 소리 › 효과음 (SFX) · 공통 · 난이도 보통 · 폰 부담 가벼움 (폰 OK) — 층 셋 = 노드 10개 남짓, 1초 쓰고 버린다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i148
- 쓰면 좋을 때: 단순한 소리 하나로는 밋밋한 중요한 순간 (판정 도장 · 큰 착지) / 같은 틀로 크기 · 재질만 바꿔 여러 소리를 만들 때
- 쓰지 말 때: 아주 자주 나는 작은 소리 — 타격 + 몸통 두 층이면 충분 / 폰 스피커에서 몸통(58Hz)만 믿기 — 낮은 소리는 안 들린다, 타격 · 위쪽 배음을 꼭 함께

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### recLayers — `src/demos/demosSfx.ts:1368`
```ts
const recLayers: Rec = (c, out, t0, p, r) => {
  layHit(c, out, t0, p, r);
  layBody(c, out, t0, p, r);
  layTail(c, out, t0, p, r);
};
```

### layHit — `src/demos/demosSfx.ts:1346`
```ts
const layHit: Rec = (c, out, t0, p, r) => {
  if (!(p['l0'] ?? 1)) return;
  const n = noise(c, t0, 0.03, r);
  chain(n, filt(c, 'highpass', 2000), envG(c, out, t0, 0.0005, 0.002, 0.018, 0.9));
};
```

### layBody — `src/demos/demosSfx.ts:1351`
```ts
const layBody: Rec = (c, out, t0, p) => {
  if (!(p['l1'] ?? 1)) return;
  const o = osc(c, 'sine', 190, t0, 0.32);
  o.frequency.exponentialRampToValueAtTime(58, t0 + 0.26);
  o.connect(envG(c, out, t0, 0.002, 0.02, 0.26, 0.85));
  const tr = osc(c, 'triangle', 380, t0, 0.15);
  tr.frequency.exponentialRampToValueAtTime(120, t0 + 0.12);
  tr.connect(envG(c, out, t0, 0.002, 0, 0.12, 0.25));
};
```

### layTail — `src/demos/demosSfx.ts:1360`
```ts
const layTail: Rec = (c, out, t0, p, r) => {
  if (!(p['l2'] ?? 1)) return;
  const n = noise(c, t0, 1, r);
  const lp = filt(c, 'lowpass', 1400);
  lp.frequency.setValueAtTime(1400, t0);
  lp.frequency.exponentialRampToValueAtTime(300, t0 + 0.9);
  chain(n, lp, envG(c, out, t0 + 0.01, 0.03, 0, 0.85, 0.22));
};
```

## 관련 기술
- 먼저 알면 좋은 기술: [코드로 효과음 합성 (견본판 70가지)](https://ai-techstudio.web.app/ai/t/i499.md) `i499`
- 다음에 해 볼 기술: [방 크기 잔향](https://ai-techstudio.web.app/ai/t/i159.md) `i159`
