# AI 꾸러미 — 엔진 소리 (속도 연동) — Procedural engine sound (RPM-driven synthesis)
> 엔진 회전수에서 기본 음(rpm/60 × 실린더 수 ÷ 2)을 계산해 톱니 · 네모 · 사인 배음과 잡음을 필터로 섞고, 속도가 오르면 음 높이와 밝기가 함께 올라가게 한다.  
> 견본: https://ai-techstudio.web.app/#t/i156

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

## 주문서

### 만들어 줘: 엔진 소리 (속도 연동) — Procedural engine sound (RPM-driven synthesis)

#### 1. 목표
주차장 탈출 장난감 차에 속도에 따라 바뀌는 엔진 소리를 합성해 줘 — 회전수에서 기본 음을 계산하고 배음 · 잡음 · 필터를 섞어, 빨라지면 높고 밝아지게. 느낌은 귀여운 장난감 차 부릉부릉.

#### 2. 핵심 기술 용어
- **Procedural engine sound (RPM-driven synthesis)** — 회전수로 만드는 엔진 소리
- **Firing frequency = rpm/60 × cylinders/2** — 4행정 엔진 폭발 횟수 = 기본 음
- **Amplitude modulation · waveshaper** — 크기 떨림(부르릉) · 찌그러뜨려 거칠게
- **AudioParam.setTargetAtTime** — 값을 부드럽게 따라가게 — 속도 바뀜

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

#### 4. 조건
- 속도 → 회전수 → 기본 음을 한 함수로 — 진동수를 손으로 따로따로 바꾸지 않는다
- 모든 값 바뀜은 setTargetAtTime 으로 (속도 손잡이를 빨리 돌려도 딸깍 없이)
- 시동 켜기 · 끄기는 크기를 0.15초 정도로 서서히 — 뚝 끊지 않는다
- 끌 때 발진기 · 잡음 소스를 모두 stop()

#### 5. 완성 기준 (이게 보이면 성공)
- 시동을 켜면 낮은 공회전 소리(약 57Hz)가 부르릉 떨며 난다
- 속도를 0 → 100km/h 로 올리면 음 높이와 밝기가 함께 매끄럽게 올라간다 (약 220Hz)
- 미리 보기에서 0 → 100 → 0 km/h 로 가속 · 감속하는 소리가 들린다

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

## 원리
- 회전수 rpm = 850(공회전) + 속도(km/h) × 50. 4기통 4행정은 두 바퀴에 한 번씩 실린더마다 터지므로 기본 음 f0 = rpm/60 × 4/2.
- 소리 재료: 톱니파 f0 · 네모파 f0/2(0.5) · 사인 2f0(0.3) · 대역 필터(3f0) 지난 잡음(0.6).
- 모두 저역 필터(300 + rpm × 0.35 Hz, Q 2)를 지나 웨이브셰이퍼(k = 3)로 살짝 찌그러뜨린 뒤, f0/2 사인으로 크기를 0.6 ± 0.4 떨게 해 「부르릉」 결을 만든다.
- 속도가 바뀌면 모든 진동수 · 필터를 setTargetAtTime(값, 지금, 0.08 ~ 0.12) 로 함께 따라가게 — 음 높이와 밝기가 같이 오른다.

## 핵심 코드 — 회전수로 움직이는 엔진
(발췌: demos/demosSfx.ts rpmOf · engine 을 정리 (osc · noise · filt · gainN · shaper · chain 은 견본 도우미))
```ts
const rpmOf = (speed: number) => 850 + speed * 50;      // km/h → rpm

function engine(c: AudioContext, out: AudioNode, t0: number) {
  const cyl = 4;
  const saw = osc(c, 'sawtooth', 30, t0, 0);
  const sub = osc(c, 'square', 15, t0, 0);
  const hi = osc(c, 'sine', 60, t0, 0);
  const am = osc(c, 'sine', 30, t0, 0);                     // 크기 떨림 (부르릉)
  const n = noise(c, t0, 0);
  const lp = filt(c, 'lowpass', 500, 2);
  const bp = filt(c, 'bandpass', 120, 2);
  const amG = gainN(c, 0.6), amD = gainN(c, 0.4);
  chain(am, amD);
  amD.connect(amG.gain);                                    // 0.6 ± 0.4
  saw.connect(lp);
  chain(sub, gainN(c, 0.5), lp);
  chain(hi, gainN(c, 0.3), lp);
  chain(n, bp, gainN(c, 0.6), lp);
  chain(lp, shaper(c, 3), amG, gainN(c, 0.5), out);         // 살짝 찌그러뜨려 거칠게
  const set = (speed: number, t: number, tc = 0.08) => {
    const f0 = (rpmOf(speed) / 60) * (cyl / 2);             // 4행정: 두 바퀴에 한 번씩 폭발
    saw.frequency.setTargetAtTime(f0, t, tc);
    sub.frequency.setTargetAtTime(f0 / 2, t, tc);
    hi.frequency.setTargetAtTime(f0 * 2, t, tc);
    am.frequency.setTargetAtTime(f0 / 2, t, tc);
    bp.frequency.setTargetAtTime(f0 * 3, t, tc);
    lp.frequency.setTargetAtTime(300 + rpmOf(speed) * 0.35, t, tc); // 빠를수록 밝게
  };
  set(0, t0, 0.001);
  return { set, srcs: [saw, sub, hi, am, n] };              // 끌 때 srcs 모두 stop()
}
```

## 흔한 실수 · 확인 목록
- [ ] **기본 음을 rpm/60 × 실린더 수로 두면 한 옥타브 높게 앵앵거린다** — 4행정 엔진은 두 바퀴에 한 번 터진다 — × 실린더 ÷ 2.
- [ ] **속도를 바꿀 때 .value 로 바로 넣으면 딸깍딸깍 계단 소리가 난다** — setTargetAtTime(값, 지금, 0.08 ~ 0.12) 으로 따라가게.
- [ ] **사인 · 톱니만으로는 전자음처럼 들린다** — 대역 필터 지난 잡음 + 웨이브셰이퍼 + 크기 떨림이 「기계」 결을 만든다.
- [ ] **시동을 끄면 뚝 끊긴다** — 앞에 둔 크기 게인을 setTargetAtTime(0, 지금, 0.15) 으로 줄인 뒤 stop.

## 완성 기준 체크리스트
- [ ] 시동을 켜면 낮은 공회전 소리(약 57Hz)가 부르릉 떨며 난다
- [ ] 속도를 0 → 100km/h 로 올리면 음 높이와 밝기가 함께 매끄럽게 올라간다 (약 220Hz)
- [ ] 미리 보기에서 0 → 100 → 0 km/h 로 가속 · 감속하는 소리가 들린다

## 이 기술 정보
- id: `i156` · 분류: 소리 › 효과음 (SFX) · 공통 · 난이도 보통 · 폰 부담 가벼움 (폰 OK) — 엔진 하나 = 발진기 4 + 잡음 1 + 필터 2 + 셰이퍼 1, 켜져 있는 동안 계속. 동시에 서너 대는 가볍다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i156
- 쓰면 좋을 때: 속도가 계속 바뀌는 탈것 소리 / 파일 없이 장난감 차 · 배 소리를 낼 때
- 쓰지 말 때: 진짜 자동차처럼 실감 나야 할 때 — 회전수별 녹음을 섞는 방식이 낫다 / 차가 수십 대 동시에 — 가까운 몇 대만 소리를 낸다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### engine — `src/demos/demosSfx.ts:3248`
```ts
function engine(c: BA, out: AudioNode, t0: number, r: Rnd): Engine {
  const cyl = 4;
  const saw = osc(c, 'sawtooth', 30, t0, 0);
  const sub = osc(c, 'square', 15, t0, 0);
  const hi = osc(c, 'sine', 60, t0, 0);
  const am = osc(c, 'sine', 30, t0, 0);
  const n = noise(c, t0, 0, r);
  const lp = filt(c, 'lowpass', 500, 2);
  const bp = filt(c, 'bandpass', 120, 2);
  const amG = gainN(c, 0.6);
  const amD = gainN(c, 0.4);
  chain(am, amD);
  amD.connect(amG.gain);
  saw.connect(lp);
  chain(sub, gainN(c, 0.5), lp);
  chain(hi, gainN(c, 0.3), lp);
  chain(n, bp, gainN(c, 0.6), lp);
  chain(lp, shaper(c, 3), amG, gainN(c, 0.5), out);
  const set = (speed: number, t: number, tc = 0.08): void => {
    const f0 = (rpmOf(speed) / 60) * (cyl / 2);
    saw.frequency.setTargetAtTime(f0, t, tc);
    sub.frequency.setTargetAtTime(f0 / 2, t, tc);
    hi.frequency.setTargetAtTime(f0 * 2, t, tc);
    am.frequency.setTargetAtTime(f0 / 2, t, tc);
    bp.frequency.setTargetAtTime(f0 * 3, t, tc);
    lp.frequency.setTargetAtTime(300 + rpmOf(speed) * 0.35, t, tc);
  };
  set(0, t0, 0.001);
  return { set, srcs: [saw, sub, hi, am, n] };
}
```

### rpmOf — `src/demos/demosSfx.ts:3243`
```ts
const rpmOf = (speed: number): number => 850 + speed * 50;
```

## 관련 기술
- 먼저 알면 좋은 기술: [코드로 효과음 합성 (견본판 70가지)](https://ai-techstudio.web.app/ai/t/i499.md) `i499`
- 다음에 해 볼 기술: [도플러 「니이이용」](https://ai-techstudio.web.app/ai/t/i157.md) `i157`
