← Back to Blog Apr 23, 2026 · 07:00 PM · 6 min read

Snake Sound Effects: Eat, Die, and Level Up Sounds

In Part 5, we added mobile swipe controls. Now for the finishing touch — sound. A short rising tone when eating food, a descending buzz on death, a cheerful jingle when leveling up — these tiny audio cues make the game feel alive. In this final part of the Snake tutorial series, we use the Web Audio API to generate all sounds from code. No audio files needed.

By Wuidi · Tutorial · Part 6

What You Will Learn

Step 1: AudioContext Setup

The Web Audio API starts with an AudioContext. This is the central hub that creates and connects audio nodes. We create one context and reuse it for all sounds.

let audioCtx = null;
let isMuted = false;

function initAudio() {
  if (!audioCtx) {
    audioCtx = new (window.AudioContext
      || window.webkitAudioContext)();
  }
  // Resume if suspended (mobile browsers)
  if (audioCtx.state === 'suspended') {
    audioCtx.resume();
  }
}

function playTone(freq, duration, type, ramp) {
  if (!audioCtx || isMuted) return;

  const osc = audioCtx.createOscillator();
  const gain = audioCtx.createGain();

  osc.type = type || 'square';
  osc.frequency.setValueAtTime(freq, audioCtx.currentTime);

  if (ramp) {
    osc.frequency.linearRampToValueAtTime(
      ramp, audioCtx.currentTime + duration
    );
  }

  gain.gain.setValueAtTime(0.15, audioCtx.currentTime);
  gain.gain.exponentialRampToValueAtTime(
    0.001, audioCtx.currentTime + duration
  );

  osc.connect(gain);
  gain.connect(audioCtx.destination);
  osc.start();
  osc.stop(audioCtx.currentTime + duration);
}

The playTone() helper creates a single oscillator with optional frequency ramping. The gain node fades the sound out so it does not click when it stops. This one function powers all our sound effects.

Tip: Always use exponentialRampToValueAtTime for the gain fade-out. Stopping an oscillator abruptly causes an audible click. The ramp smooths the ending.

Step 2: Eat Sound

The eat sound is a short rising tone — it starts at a low frequency and ramps up. This creates a satisfying "boop" that feels rewarding.

function playEatSound() {
  playTone(300, 0.1, 'square', 600);
}

// Call in checkFood() when snake eats:
function checkFood() {
  const head = snake[0];
  if (head.x === food.x && head.y === food.y) {
    snake.push({ ...snake[snake.length - 1] });
    food = spawnFood();
    score += 10;
    addFloatingText(food.x, food.y, '+10');
    playEatSound();
  }
}

The tone starts at 300Hz and ramps to 600Hz over 0.1 seconds. The square wave gives it a retro, 8-bit character that fits the pixel-style Snake game. Experiment with the frequencies — 200→500 sounds deeper, 400→800 sounds brighter.

Step 3: Death Sound

The death sound is the opposite of the eat sound — a descending tone that feels like failure. We use a longer duration and a lower target frequency.

function playDeathSound() {
  playTone(400, 0.4, 'sawtooth', 80);
}

// Call in the game loop when collision is detected:
if (checkCollision()) {
  gameOver = true;
  playDeathSound();
  drawGameOver();
  return;
}

The sawtooth wave sounds harsher than square, which fits the "you died" mood. Starting at 400Hz and dropping to 80Hz over 0.4 seconds creates a descending buzz. The longer duration (0.4s vs 0.1s for eat) gives it more weight — death should feel significant.

Tip: Keep sound effects short. The eat sound is 100ms, the death sound is 400ms. Anything longer than 500ms starts to feel like it is interrupting gameplay rather than enhancing it.

Step 4: Level Up Jingle

When the snake reaches a new speed tier (from Part 3), play a quick ascending jingle — three notes that go up in pitch. This signals progress and feels rewarding.

function playLevelUpSound() {
  if (!audioCtx || isMuted) return;

  const notes = [440, 554, 659]; // A4, C#5, E5
  const noteDuration = 0.12;

  notes.forEach((freq, i) => {
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();

    osc.type = 'square';
    osc.frequency.setValueAtTime(
      freq, audioCtx.currentTime + i * noteDuration
    );

    const startTime = audioCtx.currentTime + i * noteDuration;
    gain.gain.setValueAtTime(0.15, startTime);
    gain.gain.exponentialRampToValueAtTime(
      0.001, startTime + noteDuration
    );

    osc.connect(gain);
    gain.connect(audioCtx.destination);
    osc.start(startTime);
    osc.stop(startTime + noteDuration);
  });
}

// Call when speed tier changes:
function checkFood() {
  const head = snake[0];
  if (head.x === food.x && head.y === food.y) {
    const oldTier = getSpeedTier();
    snake.push({ ...snake[snake.length - 1] });
    food = spawnFood();
    score += 10;
    playEatSound();

    const newTier = getSpeedTier();
    if (newTier !== oldTier) {
      playLevelUpSound();
    }
  }
}

The three notes (A4, C#5, E5) form a major triad — a universally "happy" chord. Each note plays for 120ms, so the entire jingle is 360ms. It is short enough to not interrupt gameplay but distinct enough to notice.

Step 5: Background Music

Optional background music adds atmosphere. We create a simple looping pattern using oscillators — a bass note that alternates between two pitches.

let bgmOsc = null;
let bgmGain = null;

function startBGM() {
  if (!audioCtx || isMuted || bgmOsc) return;

  bgmOsc = audioCtx.createOscillator();
  bgmGain = audioCtx.createGain();

  bgmOsc.type = 'triangle';
  bgmOsc.frequency.setValueAtTime(110, audioCtx.currentTime);
  bgmGain.gain.setValueAtTime(0.04, audioCtx.currentTime);

  bgmOsc.connect(bgmGain);
  bgmGain.connect(audioCtx.destination);
  bgmOsc.start();

  // Alternate between two notes
  scheduleBGMPattern();
}

function scheduleBGMPattern() {
  if (!bgmOsc) return;
  const now = audioCtx.currentTime;
  const beatLen = 0.5;

  for (let i = 0; i < 16; i++) {
    const freq = i % 2 === 0 ? 110 : 82.41; // A2, E2
    bgmOsc.frequency.setValueAtTime(
      freq, now + i * beatLen
    );
  }

  // Schedule next batch
  setTimeout(scheduleBGMPattern, 16 * beatLen * 1000);
}

function stopBGM() {
  if (bgmOsc) {
    bgmOsc.stop();
    bgmOsc = null;
    bgmGain = null;
  }
}
Tip: Keep background music very quiet (gain 0.04). It should be felt, not heard. If the player notices the music, it is too loud. Sound effects should always be louder than the background.

Step 6: Mute Toggle

Always give players a way to mute. Some people play at work, on public transport, or just prefer silence. A simple button in the corner toggles all audio.

// HTML button
// <button id="mute-btn" class="mute-btn">🔊</button>

const muteBtn = document.getElementById('mute-btn');

muteBtn.addEventListener('click', () => {
  isMuted = !isMuted;
  muteBtn.textContent = isMuted ? '🔇' : '🔊';

  if (isMuted) {
    stopBGM();
  } else {
    startBGM();
  }
});

// Also draw mute state on canvas as fallback
function drawMuteIndicator() {
  ctx.fillStyle = '#666';
  ctx.font = '12px "Segoe UI", sans-serif';
  ctx.textAlign = 'right';
  ctx.fillText(
    isMuted ? '🔇 Muted' : '🔊 Sound On',
    WIDTH - 8, HEIGHT - 6
  );
}

The button uses emoji for the icon — no image assets needed. When muted, all playTone() calls return early (we check isMuted at the top), and the background music stops. Unmuting restarts the BGM.

Step 7: Mobile Audio Unlock

Mobile browsers block audio until the user interacts with the page (tap, click, or keypress). This is a security feature to prevent auto-playing ads. We handle it by initializing audio on the first user interaction.

let audioUnlocked = false;

function unlockAudio() {
  if (audioUnlocked) return;

  initAudio();

  // Play a silent buffer to unlock
  const buffer = audioCtx.createBuffer(1, 1, 22050);
  const source = audioCtx.createBufferSource();
  source.buffer = buffer;
  source.connect(audioCtx.destination);
  source.start(0);

  audioUnlocked = true;
}

// Unlock on first touch or click
document.addEventListener('touchstart', unlockAudio,
  { once: true });
document.addEventListener('click', unlockAudio,
  { once: true });
document.addEventListener('keydown', unlockAudio,
  { once: true });

The trick is playing a silent buffer on the first interaction. This "unlocks" the AudioContext, and all subsequent sounds play normally. The { once: true } option removes the listener after it fires, so we do not waste resources on future events.

Tip: Many games show a "Tap to Start" overlay that serves double duty: it gives the player a clear start point and it provides the user interaction needed to unlock audio. This is the cleanest pattern for mobile games.

Series Complete

Congratulations — you have built a complete Snake game from scratch across six tutorials:

  1. Part 1: Fundamentals — Grid, movement, food, collision, game loop
  2. Part 2: High Score — localStorage, score display, floating text, game over screen
  3. Part 3: Speed Increase — Dynamic speed, tiers, requestAnimationFrame, difficulty curve
  4. Part 4: Wall Wrapping — Modulo arithmetic, wrap mode, kids mode
  5. Part 5: Mobile Controls — Touch events, swipe detection, responsive canvas
  6. Part 6: Sound Effects — Web Audio API, oscillators, mute toggle, mobile unlock

Every concept you learned here — game loops, input handling, collision detection, state persistence, responsive design, audio — applies to any browser game you build next. The Snake game is simple, but the skills are universal.

What to build next: Try our Pac-Man tutorial series — it covers maze generation, ghost AI, power pellets, and more advanced game mechanics. Same step-by-step approach, bigger challenge.

More Articles

👾
Tutorial How to Make a Pac-Man Game for Beginners Tutorial · Apr 23, 2026 · 08:00 AM
🔊
Tutorial Pac-Man Sound Effects: Waka, Ghost, and Power Pellet Sounds Tutorial · Apr 23, 2026 · 01:00 PM
🔄
Tutorial Snake Wall Wrapping: Appear on the Other Side Tutorial · Apr 23, 2026 · 05:00 PM
← Back to Blog