HTML6 Games설치없는 무료 웹 게임 / devlog

설치 없이, 지금 바로 플레이

브라우저에서 즐기는 무료 미니게임 모음. 로그인하면 기록이 계정에 저장되어 어느 기기에서든 이어서 볼 수 있어요.

게임 목록
게임 목록
모든 개발 이야기

소리 파일 없이 Web Audio 로 엔진·바람·타이어 소리 만들기

2026년 11월 1일 · 그림·소리

이 사이트의 효과음은 처음부터 소리 파일이 없었다. 점수, 부딪힘, 끝 같은 짧은 소리는 모두 Web Audio API 로 그 자리에서 만든다. 오실레이터(정해진 파형을 내는 소리 발생기) 하나를 켜고, 크기를 빠르게 올렸다 내리고, 0.2초쯤 뒤에 끈다. 음 높이와 파형만 바꿔 소리 열세 가지를 만들었다.

고속도로 드라이버는 그것으로는 모자랐다. 레이싱 게임에는 달리는 내내 나는 소리가 필요하다. 엔진은 속도에 따라 높아져야 하고, 기어를 바꾸면 회전이 떨어져야 하고, 빨라질수록 바람 소리가 커져야 하고, 급하게 꺾으면 타이어가 끽 소리를 내야 한다. 녹음한 엔진 소리를 재생 속도만 바꿔 틀 수도 있지만, 짧은 소리처럼 합성으로 만들기로 했다.

Web Audio 에서 쓴 것들

Web Audio API 는 소리를 만드는 노드를 선으로 이어 그래프를 만드는 방식이다. 소리를 내는 노드에서 출발해 소리를 바꾸는 노드를 거쳐 스피커에 닿는다. 이 글에서 쓰는 것은 몇 개 되지 않는다.

노드는 connect() 로 잇는다. connect() 는 이어진 노드를 돌려주어서 한 줄로 쓸 수 있다.

const ctx = new AudioContext();
const osc = ctx.createOscillator();
const filter = ctx.createBiquadFilter();
const amp = ctx.createGain();
osc.type = 'sawtooth';
osc.frequency.value = 110;
filter.type = 'lowpass';
filter.frequency.value = 800;
amp.gain.value = 0.1;
osc.connect(filter).connect(amp).connect(ctx.destination);
osc.start();

// wobble the volume 8 times a second with an LFO
const lfo = ctx.createOscillator(), depth = ctx.createGain();
lfo.frequency.value = 8;
depth.gain.value = 0.05;
lfo.connect(depth).connect(amp.gain);
lfo.start();

이 정도면 톱니파를 필터로 깎은 '부우웅' 소리가 크기가 흔들리며 계속 난다. 아래의 엔진·바람·타이어 소리는 모두 이 조합에서 재료와 숫자만 다르다.

계속 나는 소리를 위한 공통 기능

게임 코드는 브라우저의 소리 기능을 직접 쓰지 않고 사이트가 주는 api 로만 쓴다. 소리를 끄거나 일시정지하거나 다른 탭으로 가면 사이트가 대신 조용히 하기 위해서다. 짧은 소리는 금방 끝나니 문제가 없었는데, 계속 나는 소리는 다르다. 일시정지 중에는 게임 루프가 멈춰 소리 값을 바꾸지 않으니, 사이트가 꺼 주지 않으면 엔진 소리가 멈춤 화면에서도 계속 난다.

그래서 공통 기능 api.hum 을 새로 만들었다. 소리 하나의 구성은 이렇다.

게임은 처음에 재료를 정해 소리를 만들고, 매 프레임 높이·필터 주파수·크기·떨림을 넘긴다.

const engine = api.hum({
  voices: [
    { type: 'sawtooth' },
    { type: 'sawtooth', mult: 1.007, gain: 0.8 },
    { type: 'square', mult: 0.5, gain: 0.5 },
  ],
  filter: 'lowpass', q: 4,
});

// every frame
engine.set({ freq, cutoff, gain, wob, wobFreq });

값을 바로 바꾸지 않고 setTargetAtTime 으로 목표를 향해 짧은 시간 동안 따라가게 한다. 프레임마다 값이 계단처럼 뛰면 딸깍거리는 잡음이 나기 때문이다. 사이트 쪽에서는 소리마다 마지막 크기를 기억해 두었다가, 소리를 켜거나 일시정지를 풀면 그 크기로 돌려놓는다. 다시 하기와 나가기에서는 노드를 모두 멈춘다.

set({ freq, cutoff, gain }, smooth = 0.03) {
  const t = audio.currentTime;
  if (freq != null) for (const v of voices) v.osc.frequency.setTargetAtTime(freq * v.mult, t, smooth);
  if (cutoff != null) filter.frequency.setTargetAtTime(cutoff, t, smooth);
  if (gain != null) { volume = gain; gate(); }
},
gate() { // called on mute, pause and tab switch too
  const open = soundOn && !paused && !document.hidden;
  out.gain.setTargetAtTime(open ? volume * 0.08 : 0, audio.currentTime, 0.04);
},

엔진: 톱니파 둘과 네모파 하나

엔진 소리의 재료는 오실레이터 셋이다. 톱니파 하나, 그보다 0.7% 높은 톱니파 하나, 반 옥타브 아래(0.5배)의 네모파 하나다. 거의 같은 높이의 두 소리를 겹치면 둘이 조금씩 어긋나며 맥놀이가 생겨 소리가 두꺼워진다. 네모파는 아래를 받쳐 준다. 톱니파와 네모파는 배음이 많아 그대로는 거칠어서, lowpass 필터로 높은 소리를 깎는다. 필터의 Q 를 4 로 높여 잘리는 주파수 근처를 살짝 강조했다.

엔진의 상태는 회전 rpm 하나로 나타낸다. 0 에서 1 사이 값이고, 이 값에 따라 세 가지가 함께 움직인다.

떨림은 크기를 0.15 만큼, 기본 높이의 절반 빠르기로 흔든다. 배기가 덜컹거리는 느낌이 여기서 난다.

기어를 올리면 회전이 떨어진다

속도를 그대로 높이로 바꾸면 사이렌처럼 계속 올라가기만 한다. 엔진 소리답게 하려면 기어가 있어야 한다. 기어 경계를 속도 비율로 정해 두고, 한 기어 안에서는 회전이 0.5 에서 1 까지 오르게 했다. 다음 기어로 넘어가면 회전이 다시 0.5 로 떨어진다. 1단만 공회전 0.18 에서 시작한다.

// gear boundaries as speed / 260 km/h (top speed rises to 420, so 6th and 7th exist)
const GEARS = [0, 0.16, 0.33, 0.52, 0.74, 1.0, 1.3, 1.62];

function rpmOf(v) {
  let g = 0;
  while (g < GEARS.length - 2 && v >= GEARS[g + 1]) g++;
  const lo = g === 0 ? 0.18 : 0.5; // idle only in first gear
  return Math.min(1.05, lo + ((1 - lo) * (v - GEARS[g])) / (GEARS[g + 1] - GEARS[g]));
}

이 게임은 체크포인트를 지날 때마다 최고 속도가 260km/h 에서 20km/h 씩 올라 420km/h 까지 간다. 그래서 260km/h 를 넘는 6단과 7단도 있다. 가속하면 소리가 오르다 떨어지기를 되풀이하며 '웅–웅–' 하고 올라간다. 높이 값을 기록해 보니 280Hz 쯤까지 오르다 183Hz 쯤으로 떨어지기를 기어마다 되풀이했다.

바람과 타이어는 흰 소음으로

바람은 오실레이터 없이 흰 소음만 쓴다. 흰 소음을 bandpass 필터에 통과시키면 한 대역만 남아 '쉬익' 하는 소리가 된다. Q 를 0.7 로 낮게 두어 대역을 넓게 잡았다. 속도가 오르면 통과 대역이 높아지고(500Hz + km/h × 3), 크기는 속도의 제곱으로 커진다(0.6 × (km/h ÷ 420)²). 느릴 때는 거의 안 들리다가 빨라질수록 확 커진다.

타이어 끽 소리는 흰 소음에 1300~1500Hz 의 세모파를 섞어, 2300~2800Hz 를 통과시키는 bandpass 에 넣는다. 높이와 필터 주파수를 매 프레임 그 범위 안에서 무작위로 조금씩 흔들고, 크기를 초당 17번 크게 떨게 했다. 이 떨림이 타이어가 미끄러지며 노면을 긁는 느낌을 낸다. 소리가 나는 조건은 바퀴 연기와 같다. 도로 위에서 빠르게 달리며 브레이크를 밟거나, 빠르게 0.2초 넘게 꺾을 때다.

처음 값으로 들어 보니 끽 소리가 엔진에 묻혀 거의 들리지 않았다. 처음에는 필터 Q 가 6 으로 통과하는 대역이 좁았고, 크기도 작았다. 브레이크를 밟을 때 실제 출력 크기를 기록해 보니 엔진이 0.045, 끽 소리가 0.028 이었다. Q 를 3 으로 낮춰 대역을 넓히고 크기를 4배쯤 키웠다. 그리고 끽 소리가 나는 동안에는 엔진을 0.55배로 낮췄다. 같은 상황에서 엔진 0.028, 끽 소리 0.118 이 되어 이제는 끽 소리가 엔진보다 크다. 끽 소리만 키우지 않고, 그동안 엔진이 잠깐 비켜 주게 한 것이다.

끝날 때와 처음 시작할 때

시간이 다 되면 차가 미끄러지듯 멈추는 연출이 1.8초 동안 이어지고, 그 뒤 결과 창이 뜬다. 소리도 1.6초에 걸쳐 0 으로 줄여, 결과 창에서는 조용하다.

브라우저는 페이지를 열자마자 소리를 내지 못하게 막는다. 자동 재생 정책 때문에 화면을 누르거나 키를 누른 뒤에야 소리를 낼 수 있다. 이 게임은 시작 전에 차가 출발선에서 공회전하는 연출이 있는데, 이 공회전 소리는 첫 입력 뒤부터 들린다.

소리를 듣지 않고 확인하기

화면은 글자로 읽어 확인할 수 있지만 소리는 그럴 수 없다. 그래서 브라우저에서 AudioParam.prototype.setTargetAtTime 을 감싸, 게임이 넘기는 높이와 크기를 배열에 쌓았다.

const log = [];
const orig = AudioParam.prototype.setTargetAtTime;
AudioParam.prototype.setTargetAtTime = function (value, time, k) {
  log.push({ value, time });
  return orig.call(this, value, time, k);
};

기어마다 높이가 오르다 떨어지는지, 일시정지하면 크기가 0 이 되고 계속하면 돌아오는지, 브레이크 때 엔진과 끽 소리의 크기가 얼마인지를 모두 이 기록으로 봤다. 앞에 적은 숫자들이 여기서 나왔다. 숫자가 맞는지는 기록으로 확인하고, 듣기 좋은지는 실제로 들어 보고 정했다.

소리 파일 대신 합성으로 해서 좋았던 점

물론 녹음한 진짜 엔진 소리만큼 실감 나지는 않는다. 파형 몇 개로 만든 소리라 어딘가 옛날 게임기 같은 느낌이 남는다. 실제 차 소리처럼 들려야 하는 게임이라면 녹음한 소리가 낫다.

참고

이 글과 관련된 게임

고속도로 드라이버굽이치는 고속도로를 달려 시간 안에 체크포인트에 닿으세요! 해 보기

다른 글

홈 화면에 추가하기

  1. 화면 아래(아이패드는 위)의 공유 버튼을 누르세요. 네모에서 화살표가 위로 나온 모양이에요.
  2. 목록을 내려 "홈 화면에 추가"를 누르세요.
  3. 오른쪽 위 "추가"를 누르면 홈 화면에 HTML6 Games 아이콘이 생겨요.

Safari 에서만 추가할 수 있어요. 홈 화면에서 열면 주소창 없이 앱처럼 보이고, 인터넷이 끊겨도 열립니다.