
내 IDE 위에 떠 있는 리듬 게임을 만들었습니다
요약
Claude Code의 작업 대기 시간 동안 즐길 수 있는 Electron 기반의 투명 리듬 게임 개발 사례를 소개합니다. IDE 위에 항상 떠 있는 오버레이 방식을 통해 컨텍스트 스위칭 없이 드럼 연습이 가능하도록 설계되었습니다.
핵심 포인트
- Electron의 투명 창 및 항상 위(alwaysOnTop) 기능을 활용한 오버레이 구현
- 오디오 파일의 온셋 검출을 통한 자동 비트 분석 및 BPM 감지
- 개발 흐름을 방해하지 않는 비침습적 게임 플레이 환경 제공
1. 서론
매년 저는 새로운 취미를 갖습니다. 올해의 취미는 드럼입니다.
Claude Code가 정신없이 움직이는 것을 보고 있을 때, 저는 "내 리듬 기술을 연습할 수 있는 게임을 직접 만들어보면 어떨까?"라고 생각했습니다.
그래서 저는 IDE 위에 투명하게 떠 있는 Electron 기반의 리듬 게임을 만들었습니다. Claude Code가 생각 중일 때, 저는 F와 J 키를 눌러 로드된 곡에 맞춰 낮은 음(low hits)과 높은 음(high hits)을 연주합니다. Claude Code가 응답하면 다시 코딩으로 돌아갑니다. 컨텍스트 스위칭(Context switch)도, 별도의 창도 필요 없습니다. 게임은 그저 다른 모든 것들 위에 항상 그 자리에 있습니다.
2. 내가 만든 것
이 게임은 어떤 오디오 파일이든 불러올 수 있으며, 오프라인으로 분석하여 낮은 음과 높은 음이 어디에 위치하는지 찾아냅니다. 그 다음 곡의 재생 위치와 동기화된 두 개의 드럼 패드를 향해 스크롤되는 히트 타겟(hit targets)을 생성합니다. 낮은 음은 F를, 높은 음은 J를 누릅니다. 타이밍 정확도(Timing accuracy)에 따라 각 히트의 점수가 매겨집니다.
- 모든 오디오 파일 로드 가능: MP3, WAV, OGG, M4A, AAC, FLAC
- 오프라인 비트 분석: 재생이 시작되기 전 파일 전체에 대해 온셋 검출(onset detection)을 실행합니다.
- 다섯 가지 시각적 테마: Lime, Classic, Forest, Neon, Dusk
- BPM 자동 감지: 감지된 낮은 음 간격으로부터 템포를 추정합니다.
3. 실행 방법
git clone https://github.com/asakohayase/drum-overlay.git
cd drum-overlay
npm install
...
조작법:
F: 낮은 음 (low hits)J: 높은 음 (high hits)Space: 곡 재생 / 일시정지Cmd+Shift+Q: 종료
4. Electron 오버레이(Overlay)란 무엇인가?
브라우저 탭은 다른 앱 위에 떠 있을 수 없습니다. 브라우저 창 내부에서 작동하기 때문에, 이를 사용하려면 Alt-Tab을 눌러 창을 전환해야 하며, 이는 게임을 만드는 목적 자체를 무색하게 만듭니다. 운영체제(OS) 수준의 창 제어가 필요합니다.
Electron이 바로 그 기능을 제공합니다. 보통 독립형 데스크톱 앱을 빌드하는 데 사용됩니다. VS Code, Claude Desktop, Slack 모두 Electron으로 만들어졌습니다. 세 가지 창 플래그(window flags)가 오버레이를 가능하게 합니다:
**transparent: true**는 Electron이 추가하는 기본 흰색 창 배경을 제거합니다. 이것이 없다면 다음과 같이 보입니다:

**alwaysOnTop: true**는 시스템 전체에서 창을 다른 모든 창보다 위에 유지합니다. 다른 것을 클릭하더라도 창의 위치를 잃지 않습니다.
**setIgnoreMouseEvents(true, { forward: true })**가 없다면 IDE를 클릭할 수 없습니다. 창이 전체 화면을 덮고 있기 때문에 모든 클릭을 가로채게 됩니다. 이 플래그는 커서가 어디에 있는지 오버레이(overlay)에 알려주는 동시에, 클릭 이벤트를 아래에 있는 요소로 통과시킵니다. 커서가 패널에 진입하면 오버레이는 일시적으로 클릭 가능한 상태가 됩니다. 패널을 벗어나면 클릭이 다시 통과됩니다.
win = new BrowserWindow({
transparent: true,
frame: false,
...
렌더러(renderer)는 커서가 무엇 위에 있는지에 따라 상호작용성을 동적으로 전환합니다:
document.addEventListener('mousemove', (e) => {
const over = e.target.closest('.pad, .icon-btn, .play-btn, .progress-bar');
ipcRenderer.send('set-ignore-mouse', !over);
...
5. 아키텍처 (Architecture)
Web Audio API (내장):
OfflineAudioContext: 재생이 시작되기 전에 전체 분석 단계를 실행합니다. 일부 라이브러리는 실시간 분석(real-time analysis)만 제공하는데, 이는 노트 레인(note lane)을 미리 채우기에는 너무 늦습니다.createBiquadFilter: 저역 통과(lowpass)/대역 통과(bandpass) 주파수 필터를 적용합니다.decodeAudioData: MP3/WAV 등을 원시 샘플(raw samples)로 디코딩합니다.
그 위에 구축된 커스텀 코드:
detectOnsets: 필터링된 오디오에서 저음 타격(low-hit) 및 고음 타격(high-hit) 타임스탬프를 찾습니다.estimateBPM: 저음 타격 간격을 통해 템포(tempo)를 추정합니다.playKick/playSnare: 합성된 드럼 사운드입니다.drawFrame: 게임 루프(game loop), 노트 스크롤링, 히트 감지(hit detection), 점수 계산(scoring)을 담당합니다.
Audio file
│
▼
...
온셋 감지 (Onset detection)
DSP (디지털 신호 처리, Digital Signal Processing)는 오디오 신호에 적용되는 수학입니다: 주파수 필터링, 에너지 측정, 파형(waveform)에서의 패턴 찾기 등이 포함됩니다.
가장 단순한 접근 방식은 진폭(amplitude)에 임계값(threshold)을 설정하는 것입니다. 즉, 특정 음량 컷오프(loudness cutoff)를 넘는 프레임을 찾는 것이죠. 하지만 실제 곡에서는 전체적인 음량이 끊임없이 변하기 때문에 이 방식은 실패합니다. 조용한 절(verse)과 시끄러운 후렴구(chorus)는 진폭 범위가 완전히 다르기 때문입니다.
핵심 통찰: 드럼 히트(drum hits)는 단순히 큰 프레임이 아니라, 급격하고 갑작스러운 어택(attack)을 가진 과도 응답(transients)입니다. 베이스 히트는 0.5초 미만으로 감쇠하는 저역대 에너지의 갑작스러운 스파이크(spike)입니다. 이를 구분 짓는 것은 음량이 아니라, 에너지의 급격한 '증가'입니다. 따라서 에너지 자체에 임계값을 적용하는 대신, 에너지의 '1차 차분(first difference)'에 임계값을 적용합니다.
// 10ms 윈도우, 5ms 홉(hop) 단위의 RMS 에너지
const energy = new Float32Array(nFrames);
for (let i = 0; i < nFrames; i++) {
...
평균과 표준편차(std)를 기준으로 한 백분위수 임계값(Percentile threshold)을 사용합니다: "에너지 스파이크의 상위 3%를 온셋(onsets)으로 간주한다"는 방식입니다. 이 방식은 노이즈 플로어(noise floor)와 상관없이 각 노래에 자동으로 적응합니다.
const positives = [...strength].filter(v => v > 0).sort((a, b) => a - b);
const threshold = positives[Math.floor(positives.length * 0.97)];
최소 220ms의 간격을 두어 임계값 이상의 지역 최댓값(Local maxima)을 찾는 방식은 잔향(echoes)을 잡는 것을 방지합니다. 이 처리가 없다면, 히트의 링아웃(ring-out)이 2차 스파이크를 생성하여 두 번째 음표로 감지될 수 있습니다. 저음과 고음 히트는 주파수로 분리됩니다. 100Hz에서의 저역 통과 필터(lowpass)는 베이스 범위의 히트(킥, 탐, 베이스)를 포착하고, 2500Hz에서의 대역 통과 필터(bandpass)는 고음 범위의 히트(하이햇, 심벌즈, 스네어 크랙)를 포착합니다. OfflineAudioContext는 이러한 필터들을 적용하며 실시간보다 빠르게 렌더링합니다.
const [lowTimes, highTimes] = await Promise.all([
detectOnsets(buf, 'lowpass', 100, 1.0, 0.22, 0.98),
detectOnsets(buf, 'bandpass', 2500, 1.5, 0.22, 0.97),
...
사운드 합성 (Sound synthesis)
킥(kick)은 450ms 동안 160Hz에서 0에 가깝게 스윕(sweep)되는 사인파(sine wave, 바디 부분)와 900Hz에서의 20ms 사각파(square-wave) 버스트(클릭 어택 부분)의 조합입니다.
function playKick() {
const c = ctx();
const osc = c.createOscillator();
...
스네어(Snare)는 화이트 노이즈(Math.random()을 버퍼에 입력)를 2200Hz의 밴드패스(Bandpass) 필터로 통과시킨 후, 타격감을 위해 짧은 삼각파(Triangle-wave) 톤 스윕(Tone sweep)을 더한 것입니다.
6. 주요 학습 내용 (Key Learnings)
-
사전 분석을 위한
OfflineAudioContext사용. 노트 레인(Note lane)을 렌더링하려면 재생이 시작되기 전에 모든 히트 타임스탬프(Hit timestamps)를 알고 있어야 합니다.OfflineAudioContext는 전체 분석 과정을 사전에 실행합니다. 실시간 분석(Real-time analysis)을 사용하면 노래가 재생됨에 따라 히트 지점이 뒤늦게 나타나므로, 레인을 채우기에는 너무 늦습니다. -
평균+표준편차(Mean+std) 대신 백분위수 임계값(Percentile threshold) 사용. 심벌즈의 잔향(Cymbal wash)이 심한 트랙은 노이즈 플로어(Noise floor)를 높여 평균+표준편차 임계값을 무너뜨립니다. 백분위수 임계값은 트랙 내의 상대적인 스파이크 높이(Spike height)만을 고려합니다.
-
온셋 검출(Onset detection) 파라미터 튜닝 필요. 온셋 사이의 최소 간격과 백분위수 임계값 모두 적절한 느낌을 찾기 위해 몇 번의 반복 작업이 필요했습니다. 너무 관대하면 에코(Echo)까지 잡아내고, 너무 엄격하면 실제 히트(Real hits)를 놓치게 됩니다.
7. 결론 (Conclusion)
AI의 사고 정지(Thinking pauses)는 기본적으로 버려지는 시간입니다. 하지만 반드시 그럴 필요는 없습니다. 창의적인 무언가를 만드세요. 자신을 위해 만드세요. 그러면 예상했던 것보다 더 많은 것을 얻게 될지도 모릅니다.
8. 리소스 (Resources)
🚀 직접 시도해 보세요: github.com/asakohayase/drum-overlay
📚 더 알아보기:
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기