← Back to Blog Apr 23, 2026 · 01:00 AM · 6 min read

Frogger Sound Effects: Hop, Splash, and Level Up Sounds

By Wuidi · Tutorial · Part 6

This is the final part of our Frogger series. In Part 5, we added level progression with faster traffic and shorter timers. Now we bring the game to life with sound effects. We will use the Web Audio API to generate all sounds programmatically — no audio files needed. Every hop, splash, and level-up gets its own distinct sound.

What You Will Learn

Step 1: AudioContext Setup

The Web Audio API requires an AudioContext. Browsers block audio until the user interacts with the page, so we create the context on the first click or keypress. This is the same pattern used in every browser game.

let audioCtx = null;
let muted = false;

function initAudio() {
  if (!audioCtx) {
    audioCtx = new (window.AudioContext
      || window.webkitAudioContext)();
  }
}

// Initialize on first user interaction
document.addEventListener('keydown', initAudio,
  { once: true });
document.addEventListener('click', initAudio,
  { once: true });
document.addEventListener('touchstart', initAudio,
  { once: true });
Tip: We listen for touchstart in addition to click and keydown. On mobile, the first touch needs to unlock audio. The { once: true } option ensures the listener removes itself after firing — no wasted event checks on every subsequent interaction.

Step 2: Hop Sound

Every time the frog moves one tile, play a short blip. This gives satisfying tactile feedback for each hop. The sound is a quick high-pitched square wave that decays rapidly.

function playHop() {
  if (!audioCtx || muted) return;

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

  osc.connect(gain);
  gain.connect(audioCtx.destination);

  osc.type = 'square';
  osc.frequency.value = 480;

  const t = audioCtx.currentTime;
  gain.gain.setValueAtTime(0.12, t);
  gain.gain.exponentialRampToValueAtTime(
    0.001, t + 0.06);

  osc.start(t);
  osc.stop(t + 0.06);
}

// Call in the keydown handler:
document.addEventListener('keydown', (e) => {
  if (gameOver || deathAnim || levelTransition)
    return;

  let moved = false;
  switch (e.key) {
    case 'ArrowUp':
      if (frog.row > 0) { frog.row--; moved = true; }
      break;
    case 'ArrowDown':
      if (frog.row < ROWS - 1) { frog.row++; moved = true; }
      break;
    case 'ArrowLeft':
      if (frog.col > 0) { frog.col--; moved = true; }
      break;
    case 'ArrowRight':
      if (frog.col < COLS - 1) { frog.col++; moved = true; }
      break;
  }
  if (moved) playHop();
});

The hop sound is only 60ms long — barely noticeable on its own, but it makes every movement feel responsive. The square wave gives it a retro arcade character that fits the Frogger aesthetic.

Step 3: Splash / Death Sound

When the frog drowns or gets hit by a car, play a descending tone. The pitch drops from high to low, creating a "falling" feeling that signals failure.

function playSplash() {
  if (!audioCtx || muted) return;

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

  osc.connect(gain);
  gain.connect(audioCtx.destination);

  osc.type = 'sine';
  const t = audioCtx.currentTime;

  // Descending pitch: 500Hz → 80Hz
  osc.frequency.setValueAtTime(500, t);
  osc.frequency.exponentialRampToValueAtTime(
    80, t + 0.5);

  gain.gain.setValueAtTime(0.2, t);
  gain.gain.exponentialRampToValueAtTime(
    0.001, t + 0.5);

  osc.start(t);
  osc.stop(t + 0.5);
}

// Add noise burst for car hit variant
function playCarHit() {
  if (!audioCtx || muted) return;

  // Impact thud
  const osc = audioCtx.createOscillator();
  const gain = audioCtx.createGain();

  osc.connect(gain);
  gain.connect(audioCtx.destination);

  osc.type = 'sawtooth';
  const t = audioCtx.currentTime;

  osc.frequency.setValueAtTime(150, t);
  osc.frequency.exponentialRampToValueAtTime(
    40, t + 0.3);

  gain.gain.setValueAtTime(0.25, t);
  gain.gain.exponentialRampToValueAtTime(
    0.001, t + 0.3);

  osc.start(t);
  osc.stop(t + 0.3);
}
Tip: Using different waveforms for different death types helps the player distinguish what happened. Sine wave for drowning (smooth, watery), sawtooth for car hit (harsh, crunchy). The player learns the sounds subconsciously and reacts faster.

Step 4: Goal Reached Sound

When the frog reaches a lily pad, play an ascending sweep that feels rewarding. The pitch rises from low to high, the opposite of the death sound — success feels like going up.

function playGoal() {
  if (!audioCtx || muted) return;

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

  osc.connect(gain);
  gain.connect(audioCtx.destination);

  osc.type = 'square';
  const t = audioCtx.currentTime;

  // Ascending pitch: 300Hz → 900Hz
  osc.frequency.setValueAtTime(300, t);
  osc.frequency.exponentialRampToValueAtTime(
    900, t + 0.25);

  gain.gain.setValueAtTime(0.15, t);
  gain.gain.exponentialRampToValueAtTime(
    0.001, t + 0.3);

  osc.start(t);
  osc.stop(t + 0.3);
}

The goal sound is 300ms — long enough to feel celebratory but short enough to not overlap with the next action. The ascending pitch creates a natural "success" feeling that players universally understand.

Step 5: Level Up Jingle

When all goal slots are filled and the player advances to the next level, play a three-note ascending melody. This is more elaborate than the single-goal sound, marking it as a bigger achievement.

function playLevelUp() {
  if (!audioCtx || muted) return;

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

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

    osc.connect(gain);
    gain.connect(audioCtx.destination);

    osc.type = 'square';
    osc.frequency.value = freq;

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

    osc.start(t);
    osc.stop(t + dur);
  });
}
Tip: The notes A4 (440Hz), C#5 (554Hz), E5 (659Hz) form an A major triad — a universally "happy" chord. Playing them as an ascending arpeggio creates a classic video game victory jingle. Three notes is the sweet spot: enough to feel melodic, short enough to not interrupt gameplay.

Step 6: Mute Toggle and Mobile Audio

Players need a way to mute sounds. We add a simple toggle with the M key. On mobile, we also need to handle the audio unlock pattern — the first touch must resume the AudioContext if it was suspended.

// Mute toggle with M key
document.addEventListener('keydown', (e) => {
  if (e.key === 'm' || e.key === 'M') {
    muted = !muted;
  }
});

// Mobile audio unlock
function unlockAudio() {
  if (audioCtx &&
      audioCtx.state === 'suspended') {
    audioCtx.resume();
  }
}

document.addEventListener('touchstart',
  unlockAudio, { once: true });

// Draw mute indicator on HUD
function drawMuteIndicator() {
  ctx.fillStyle = '#888';
  ctx.font = '12px "Segoe UI", sans-serif';
  ctx.textAlign = 'right';
  ctx.fillText(
    muted ? '🔇 M to unmute' : '🔊 M to mute',
    WIDTH - 8, HEIGHT - 8
  );
}

The mute indicator sits in the bottom-right corner of the canvas. It shows the current state and reminds the player which key toggles it. On mobile, the touchstart listener calls audioCtx.resume() to unlock audio that was suspended by the browser's autoplay policy.

Wiring Sounds to Game Events

Here is where each sound gets called in the game code:

// In keydown handler (after movement):
if (moved) playHop();

// In onFrogDeath():
function onFrogDeath(type) {
  if (type === 'hit') playCarHit();
  else playSplash();
  lives--;
  // ... rest of death logic
}

// In awardGoal():
function awardGoal() {
  playGoal();
  // ... scoring logic

  if (filledGoals.length >= goalSlots.length) {
    playLevelUp();
    startNextLevel();
  }
}

// In drawHUD() — add at the end:
drawMuteIndicator();

Series Complete!

Congratulations — you have built a complete Frogger game from scratch! Over 6 parts we covered:

From here, you can keep adding features: mobile swipe controls, animated sprites, turtles that dive underwater, bonus items on logs, or a two-player mode. The foundation is solid — have fun building on it.

What to build next? If you enjoyed this series, check out our other game tutorials: Each series builds a complete game from scratch in 6 parts.

More Articles

👾
Tutorial How to Make a Pac-Man Game for Beginners Tutorial · Apr 23, 2026 · 08:00 AM
🐍
Tutorial How to Make a Snake Game: Fundamentals for Beginners Tutorial · Apr 23, 2026 · 02:00 PM
📈
Tutorial Frogger Levels & Difficulty: Faster Traffic Each Level Tutorial · Apr 23, 2026 · 12:00 AM
← Back to Blog