Learning
레슨 7 / 8·25분

실전: 드럼 머신 만들기

드럼 머신 프로젝트

지금까지 배운 Tone.js의 기능을 활용하여 인터랙티브 드럼 머신을 만듭니다. 16스텝 시퀀서로 킥, 스네어, 하이햇 패턴을 프로그래밍하고, 실시간으로 BPM과 이펙트를 조절할 수 있는 프로젝트입니다.

javascript
import * as Tone from 'tone';

// 드럼 사운드 정의
const kick = new Tone.MembraneSynth({
  pitchDecay: 0.05,
  octaves: 6,
  oscillator: { type: 'sine' },
  envelope: { attack: 0.001, decay: 0.4, sustain: 0.01, release: 1.4 },
}).toDestination();

const snare = new Tone.NoiseSynth({
  noise: { type: 'white' },
  envelope: { attack: 0.005, decay: 0.15, sustain: 0 },
}).toDestination();

const hihat = new Tone.MetalSynth({
  frequency: 200,
  envelope: { attack: 0.001, decay: 0.05, release: 0.01 },
  harmonicity: 5.1,
  modulationIndex: 32,
  resonance: 4000,
  octaves: 1.5,
}).toDestination();
hihat.volume.value = -10; // 볼륨 낮추기

16스텝 시퀀서 구현

javascript
// 16스텝 패턴 정의 (1 = 활성, 0 = 비활성)
const patterns = {
  kick:  [1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0],
  snare: [0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0],
  hihat: [1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0],
};

let currentStep = 0;

// 메인 시퀀서 루프
const sequencer = new Tone.Sequence(
  (time, step) => {
    currentStep = step;

    if (patterns.kick[step]) {
      kick.triggerAttackRelease('C1', '8n', time);
    }
    if (patterns.snare[step]) {
      snare.triggerAttackRelease('16n', time);
    }
    if (patterns.hihat[step]) {
      hihat.triggerAttackRelease('32n', time, 0.3);
    }

    // UI 업데이트 (메인 스레드에서)
    Tone.Draw.schedule(() => {
      updateStepUI(step);
    }, time);
  },
  [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],
  '16n'
);

Tone.Transport.bpm.value = 120;
javascript
// 패턴 토글 함수
function toggleStep(instrument, step) {
  patterns[instrument][step] = patterns[instrument][step] ? 0 : 1;
}

// 재생/정지 제어
async function startDrumMachine() {
  await Tone.start(); // 오디오 컨텍스트 시작
  sequencer.start(0);
  Tone.Transport.start();
}

function stopDrumMachine() {
  sequencer.stop();
  Tone.Transport.stop();
  currentStep = 0;
}

// BPM 변경
function changeBPM(bpm) {
  Tone.Transport.bpm.rampTo(bpm, 0.5);
}

// 프리셋 패턴
const presets = {
  basic: {
    kick:  [1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0],
    snare: [0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0],
    hihat: [1,0,1,0, 1,0,1,0, 1,0,1,0, 1,0,1,0],
  },
  funky: {
    kick:  [1,0,0,1, 0,0,1,0, 1,0,0,1, 0,0,1,0],
    snare: [0,0,1,0, 1,0,0,1, 0,0,1,0, 1,0,0,0],
    hihat: [1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1],
  },
};

function loadPreset(name) {
  const preset = presets[name];
  Object.keys(preset).forEach(inst => {
    patterns[inst] = [...preset[inst]];
  });
}
javascript
// UI 업데이트 함수 (예시)
function updateStepUI(step) {
  // 모든 스텝의 활성 표시 제거
  document.querySelectorAll('.step').forEach(el => {
    el.classList.remove('active');
  });

  // 현재 스텝 활성 표시
  document.querySelectorAll('.step-' + step).forEach(el => {
    el.classList.add('active');
  });
}

// 이펙트 추가
const reverb = new Tone.Reverb({ decay: 2, wet: 0.2 }).toDestination();
const delay = new Tone.FeedbackDelay('8n.', 0.3);
delay.connect(reverb);

// 스네어에만 리버브 적용
snare.connect(reverb);

// 마스터 볼륨
const masterVol = new Tone.Volume(-6).toDestination();
kick.connect(masterVol);
snare.connect(masterVol);
hihat.connect(masterVol);
  • MembraneSynth — 막 진동을 시뮬레이션 (킥 드럼)
  • NoiseSynth — 노이즈 기반 타악기 (스네어)
  • MetalSynth — 금속 타악기 (하이햇, 심벌)
  • Tone.Draw.schedule — 오디오 타이밍에 맞춰 UI 업데이트
  • patterns 배열로 비트 패턴 프로그래밍
  • 프리셋으로 다양한 리듬 패턴 전환
💡

Tone.Draw.schedule()을 사용하면 오디오 타이밍에 정확하게 맞춘 UI 업데이트가 가능합니다. 직접 DOM을 업데이트하면 오디오와 시각적 피드백 사이에 타이밍 차이가 발생할 수 있습니다.