← All articles
February 14, 2026Music

Procedural Sound Design with Web Audio API

Each module in KM.DEV has a unique sonic signature — all synthesized in real-time with oscillators and filters.

audiowebaudiosynthesis
Share

The Idea

Every module should sound different. Security gets an alert tone. Game gets an 8-bit coin pickup. Music gets a major triad. All synthesized — no audio files needed for UI sounds.

Web Audio Pipeline

OscillatorNode → GainNode → AudioContext.destination

Each sound is a short-lived oscillator with an exponential gain ramp to zero. The envelope (attack/decay) gives each sound its character.

Module Sound Signatures

| Module | Type | Character | |--------|------|-----------| | Security | Sawtooth sweep down | Alert, urgent | | Dev Tools | Square pulse | Digital, mechanical | | Game | Square + high pitch | 8-bit coin pickup | | Manga | Sine + vibrato | Soft, warm | | Robotics | Sawtooth chord | Metallic, industrial | | Music | Sine triad (C-E-G) | Harmonic, pure | | Web | Triangle sweep up | Clean, ascending |

Implementation

function playTone(freq: number, type: OscillatorType, duration: number) {
  const ctx = getAudioContext()
  const osc = ctx.createOscillator()
  const gain = ctx.createGain()

  osc.type = type
  osc.frequency.value = freq
  gain.gain.setValueAtTime(0.15, ctx.currentTime)
  gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration)

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

Browser Gotchas

The AudioContext starts in a suspended state. You must call ctx.resume() after a user gesture (click, tap). Without this, nothing plays on mobile Safari.

Also: create oscillators fresh each time. Reusing stopped oscillators throws InvalidStateError.