# AI 꾸러미 — 정답 아르페지오 (조성 맞춤) — Arpeggio (major triad + octave)
> 배경음악과 같은 조의 1 · 3 · 5 · 8 음을 70ms 간격으로 차례로 울려, 정답 소리가 어느 곡 위에서도 늘 어울리게 한다.  
> 견본: https://ai-techstudio.web.app/#t/i141

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

## 주문서

### 만들어 줘: 정답 아르페지오 (조성 맞춤) — Arpeggio (major triad + octave)

#### 1. 목표
정답 · 성공 소리를 정답 아르페지오로 만들어 줘 — 배경음악과 같은 조의 1-3-5-8 음을 60~80ms 간격으로. 느낌은 맑은 실로폰 같은.

#### 2. 핵심 기술 용어
- **Arpeggio (major triad + octave)** — 화음을 한 음씩 차례로 — 도 미 솔 도
- **Key-matched SFX** — 배경음악과 같은 조로 맞춘 효과음
- **Semitone ratio 2^(n/12)** — 반음 n 개 위 = 진동수 × 2^(n/12)
- **Web Audio OscillatorNode (triangle)** — 세모파 발진기 — 부드럽고 맑은 소리

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

#### 4. 조건
- 배경음악의 조(으뜸음 반음 수)를 매개변수로 받는다 — 음악 쪽이 지금 조를 알려 주게
- 간격 60 ~ 80ms (기본 70)
- 음 높이는 반음 비 2^(n/12) 로만 계산 — Hz 를 손으로 적지 않는다
- AudioContext.currentTime 기준으로 네 음을 한 번에 예약

#### 5. 완성 기준 (이게 보이면 성공)
- 정답을 누르면 「도 미 솔 도」가 70ms 간격으로 올라간다
- 조 손잡이를 바꾸면 같은 모양으로 다른 조에서 울리고, 그 조의 음악 위에서 어긋나지 않는다
- 간격을 40 → 160ms 로 바꾸면 화음 → 가락처럼 들리는 차이가 난다

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

## 원리
- 장조 화음은 으뜸음에서 0 · 4 · 7 반음, 그리고 한 옥타브 위 12 반음 — 이 넷이 「도 미 솔 도」.
- 으뜸음 = 523.25Hz(높은 도) × 2^(조/12). 배경음악이 라 장조면 조 = 9 를 넣으면 「라 도# 미 라」가 된다.
- 음마다 세모파(0.42초 · 크기 0.22 · 3ms 어택)와 한 옥타브 위 사인(0.15초 · 0.05)을 겹쳐 맑게.
- 간격 70ms — 너무 촘촘하면 한 덩어리 화음, 너무 넓으면 느린 가락처럼 들린다.

## 핵심 코드 — 조 맞춤 1-3-5-8 아르페지오
(발췌: demos/demosSfx.ts recArp · tone · envG 를 정리)
```ts
const st2 = (n: number) => Math.pow(2, n / 12);           // 반음 → 배수

function envG(c: BaseAudioContext, 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 tone(c: BaseAudioContext, out: AudioNode, t0: number, f: number, d: number, peak: number, type: OscillatorType = 'sine', a = 0.003) {
  const o = c.createOscillator();
  o.type = type;
  o.frequency.setValueAtTime(Math.min(f, c.sampleRate * 0.45), t0);
  o.connect(envG(c, out, t0, a, 0, d, peak));
  o.start(t0);
  o.stop(t0 + a + d + 0.05);
}
/** key = 배경음악 조 (0 다장조 · 2 라장조 · 7 사장조 …), gap = ms */
function correctArp(c: BaseAudioContext, out: AudioNode, t0: number, key = 0, gap = 70) {
  const base = 523.25 * st2(key);                             // 높은 도에서 조만큼
  [0, 4, 7, 12].forEach((n, i) => {
    const t = t0 + (i * gap) / 1000;
    tone(c, out, t, base * st2(n), 0.42, 0.22, 'triangle', 0.003);
    tone(c, out, t, base * st2(n) * 2, 0.15, 0.05, 'sine', 0.002); // 옥타브 위 반짝임
  });
}
```

## 흔한 실수 · 확인 목록
- [ ] **정답 소리를 아무 음으로 만들면 배경음악과 부딪혀 「삑사리」처럼 들린다** — 음악의 조를 받아 그 조의 1 · 3 · 5 · 8 음으로 — 늘 화음 안의 음이라 안 부딪힌다.
- [ ] **네 음을 setTimeout 으로 치면 간격이 들쭉날쭉하다** — AudioContext 시각으로 t0 + i × gap 에 한 번에 예약한다.
- [ ] **높은 조에서 옥타브 위 사인이 너무 날카롭다** — 배음 사인은 크기 0.05 로 아주 작게, 표본률 절반 아래로 묶는다 (tone 의 min).

## 완성 기준 체크리스트
- [ ] 정답을 누르면 「도 미 솔 도」가 70ms 간격으로 올라간다
- [ ] 조 손잡이를 바꾸면 같은 모양으로 다른 조에서 울리고, 그 조의 음악 위에서 어긋나지 않는다
- [ ] 간격을 40 → 160ms 로 바꾸면 화음 → 가락처럼 들리는 차이가 난다

## 이 기술 정보
- id: `i141` · 분류: 소리 › 효과음 (SFX) · 공통 · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 음 4개 × 발진기 2 = 8개를 0.5초 쓰고 버린다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i141
- 쓰면 좋을 때: 배경음악이 있는 게임의 정답 · 성공 소리 / 같은 화면에서 조가 바뀌는 음악을 쓸 때 (조만 넘겨주면 됨)
- 쓰지 말 때: 틀림 · 실패 소리 — 내려가는 음이나 낮은 「부부」 소리가 맞다 / 음악이 없는 화면 — 조를 맞출 필요가 없으니 그냥 견본 소리로

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### recArp — `src/demos/demosSfx.ts:1198`
```ts
const recArp: Rec = (c, out, t0, p) => {
  const { key = 0, gap = 70 } = p;
  const base = 523.25 * st2(key);
  [0, 4, 7, 12].forEach((n, i) => {
    const t = t0 + (i * gap) / 1000;
    tone(c, out, t, base * st2(n), 0.42, 0.22, 'triangle', 0.003);
    tone(c, out, t, base * st2(n) * 2, 0.15, 0.05, 'sine', 0.002);
  });
};
```

### tone — `src/demos/demosSfx.ts:201`
```ts
function tone(c: BA, out: AudioNode, t0: number, f: number, d: number, peak: number, type: OscillatorType = 'sine', a = 0.003): OscillatorNode {
  const o = osc(c, type, Math.min(f, c.sampleRate * 0.45), t0, a + d);
  o.connect(envG(c, out, t0, a, 0, d, peak));
  return o;
}
```

## 관련 기술
- 같은 방법으로 만든 효과 예시: [연속 정답 음 높이 오름](https://ai-techstudio.web.app/ai/t/i142.md) `i142`
- 먼저 알면 좋은 기술: [음계 양자화 (값 → 음)](https://ai-techstudio.web.app/ai/t/i143.md) `i143`
- 다음에 해 볼 기술: [반복 피로 막기 (라운드 로빈)](https://ai-techstudio.web.app/ai/t/i147.md) `i147`
- 참고 문서: [MDN — OscillatorNode](https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode)
