# AI 꾸러미 — 반복 피로 막기 (라운드 로빈) — Round-robin sample variation
> 자주 나는 소리를 변형 3개로 돌려 쓰고 매번 속도 1±0.05 · 크기 ±2dB 를 살짝 흔들어, 같은 「톡」이 기계처럼 똑같이 반복되지 않게 한다.  
> 견본: https://ai-techstudio.web.app/#t/i147

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

## 주문서

### 만들어 줘: 반복 피로 막기 (라운드 로빈) — Round-robin sample variation

#### 1. 목표
블록 놓기 · 단추 톡 소리에 반복 피로 막기를 넣어 줘 — 변형 2~4개를 차례로 돌려 쓰고, 매번 속도 1±0.05 · 크기 ±2dB 를 조금씩 흔들어서. 느낌은 티 안 나게 자연스러운.

#### 2. 핵심 기술 용어
- **Round-robin sample variation** — 라운드 로빈 — 변형 여러 개를 차례로 돌려 쓰기
- **Pitch / volume randomization** — 음 높이 · 크기를 조금씩 무작위로
- **Listener fatigue (machine-gun effect)** — 똑같은 소리 반복에서 오는 귀 피로
- **Decibel to gain 10^(dB/20)** — 데시벨 → 곱하는 배수

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

#### 4. 조건
- 변형은 2 ~ 4개를 차례로 (무작위 고르기는 같은 것이 연달아 나올 수 있다)
- 음 높이 흔들림 ±5% 안, 크기 ±2dB 안
- 켬/끔 비교 단추로 차이를 들려준다
- 신호음(정답 · 경고)에는 쓰지 않는다

#### 5. 완성 기준 (이게 보이면 성공)
- 「톡」 6번을 들으면 켬일 때 하나하나 조금씩 다르고, 끔일 때 기계처럼 똑같다
- 변형 표시 A · B · C 가 차례로 밝아진다
- 오래 눌러도 귀가 덜 피곤하다

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

## 원리
- 귀는 완전히 똑같은 소리가 반복되면 금방 「기계 같다」고 느낀다.
- 변형 A · B · C 를 순서대로 돌려 쓴다 (견본: 820 · 960 · 740Hz, 필터 Q 6 · 7 · 5, 크기 1 · 0.95 · 1.05).
- 그 위에 매번 속도(음 높이) = 1 + (난수 − 0.5) × 0.1 → ±5%, 크기 = 10^((난수 − 0.5) × 4 / 20) → ±2dB.
- 흔들림은 작게 — 너무 크면 다른 소리처럼 들린다. 「같은 소리인데 손으로 친 것 같다」가 목표.

## 핵심 코드 — 변형 3개 돌려 쓰기 + 작은 흔들림
(발췌: demos/demosSfx.ts VARS · recRobin 을 정리)
```ts
const VARS = [
  { f: 820, q: 6, b: 1 },
  { f: 960, q: 7, b: 0.95 },
  { f: 740, q: 5, b: 1.05 },
];
let turn = 0;

function tok(c: AudioContext, out: AudioNode, noiseBuf: AudioBuffer, t: number, vary = true) {
  const v = vary ? VARS[turn++ % VARS.length] : VARS[0];        // 차례로 돌려 쓰기
  const rate = vary ? 1 + (Math.random() - 0.5) * 0.1 : 1;       // 속도 ±5%
  const vol = vary ? Math.pow(10, ((Math.random() - 0.5) * 4) / 20) : 1; // 크기 ±2dB
  // 몸통: 짧게 내려가는 사인
  const o = c.createOscillator(), g = c.createGain();
  o.frequency.setValueAtTime(v.f * rate, t);
  o.frequency.exponentialRampToValueAtTime(v.f * rate * 0.8, t + 0.12);
  g.gain.setValueAtTime(0.0001, t);
  g.gain.exponentialRampToValueAtTime(0.45 * vol * v.b, t + 0.001);
  g.gain.exponentialRampToValueAtTime(0.0001, t + 0.121);
  o.connect(g).connect(out);
  o.start(t); o.stop(t + 0.2);
  // 딸깍: 대역 필터 지난 잡음 25ms
  const n = c.createBufferSource(), bp = c.createBiquadFilter(), ng = c.createGain();
  n.buffer = noiseBuf;
  bp.type = 'bandpass'; bp.frequency.value = v.f * 2.2 * rate; bp.Q.value = v.q;
  ng.gain.setValueAtTime(0.0001, t);
  ng.gain.exponentialRampToValueAtTime(0.7 * vol, t + 0.0008);
  ng.gain.exponentialRampToValueAtTime(0.0001, t + 0.026);
  n.connect(bp).connect(ng).connect(out);
  n.start(t, Math.random() * 1.5); n.stop(t + 0.08);             // 잡음도 매번 다른 자리부터
}
```

## 흔한 실수 · 확인 목록
- [ ] **변형을 무작위로 고르면 같은 것이 두세 번 연달아 나온다** — 차례로 돌리거나, 바로 앞 것만 빼고 고른다.
- [ ] **흔들림을 ±20% 로 크게 하면 다른 소리처럼 들린다** — 속도 ±5% · 크기 ±2dB 정도가 「같은 소리 다른 손」 느낌.
- [ ] **잡음 버퍼를 늘 처음부터 틀면 딸깍이 매번 똑같다** — 긴 잡음 버퍼(2초)를 만들어 두고 시작 자리를 무작위로 (start(t, offset)).

## 완성 기준 체크리스트
- [ ] 「톡」 6번을 들으면 켬일 때 하나하나 조금씩 다르고, 끔일 때 기계처럼 똑같다
- [ ] 변형 표시 A · B · C 가 차례로 밝아진다
- [ ] 오래 눌러도 귀가 덜 피곤하다

## 이 기술 정보
- id: `i147` · 분류: 소리 › 효과음 (SFX) · 공통 · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 난수 두 개와 배열 차례 하나 — 공짜.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i147
- 쓰면 좋을 때: 1초에 여러 번 나는 소리 (톡 · 발소리 · 블록) / 오래 하는 게임의 자주 나는 화면 소리
- 쓰지 말 때: 정답 · 경고처럼 「늘 같아야 알아듣는」 신호음 — 흔들면 헷갈린다 / 음악 음 — 음 높이를 흔들면 음정이 틀어진다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### recRobin — `src/demos/demosSfx.ts:1332`
```ts
const recRobin: Rec = (c, out, t0, p, r) => {
  const vary = p['vary'] ?? 1;
  for (let i = 0; i < 6; i++) {
    const t = t0 + i * 0.24;
    const v = vary ? VARS[i % 3]! : VARS[0]!;
    const rate = vary ? 1 + (r() - 0.5) * 0.1 : 1;
    const vol = vary ? Math.pow(10, ((r() - 0.5) * 4) / 20) : 1;
    const o = osc(c, 'sine', v.f * rate, t, 0.15);
    o.frequency.exponentialRampToValueAtTime(v.f * rate * 0.8, t + 0.12);
    o.connect(envG(c, out, t, 0.001, 0, 0.12, 0.45 * vol * v.b));
    const n = noise(c, t, 0.03, vary ? r : () => 0.3);
    chain(n, filt(c, 'bandpass', v.f * 2.2 * rate, v.q), envG(c, out, t, 0.0008, 0, 0.025, 0.7 * vol));
  }
};
```

### VARS — `src/demos/demosSfx.ts:1327`
```ts
const VARS = [
  { f: 820, q: 6, b: 1 },
  { f: 960, q: 7, b: 0.95 },
  { f: 740, q: 5, b: 1.05 },
];
```

## 관련 기술
- 먼저 알면 좋은 기술: [코드로 효과음 합성 (견본판 70가지)](https://ai-techstudio.web.app/ai/t/i499.md) `i499`
- 다음에 해 볼 기술: [층 쌓기 (타격 + 몸통 + 꼬리)](https://ai-techstudio.web.app/ai/t/i148.md) `i148`
