---
description: "Demonstrates Web Audio API tone generation using OscillatorNode. Plays a 5-note ascending scale (C4→C5) with waveform visualization on canvas. Tests the AudioNode→AudioDestinationNode audio capture path."
---

# Web Audio Oscillator

Demonstrates the Web Audio API by generating and playing a musical scale using OscillatorNode.

## Core Concepts
- `AudioContext` creation and lifecycle
- `OscillatorNode` frequency control and envelope
- `GainNode` for amplitude shaping
- `AnalyserNode` for real-time waveform display

## Usage
Open in any modern browser. The skill plays C4→C5 scale automatically and renders an oscilloscope.

---

## Bundled Files

### `index.html`

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Audio Oscillator</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
  background: #0a0a0f;
  color: #e2e8f0;
  font-family: system-ui, -apple-system, sans-serif;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 24px;
  padding: 24px;
}
h1 {
  font-size: 22px;
  font-weight: 700;
  color: #8b5cf6;
  letter-spacing: -0.5px;
}
.subtitle { font-size: 13px; color: #6b7280; }
canvas {
  border: 1px solid #1e1e3a;
  border-radius: 10px;
  background: #0d0d1a;
  display: block;
}
.note-display {
  display: flex;
  gap: 10px;
  align-items: center;
}
.note-badge {
  background: #1a1a2e;
  border: 1px solid #2d2d4e;
  border-radius: 8px;
  padding: 8px 18px;
  font-size: 20px;
  font-weight: 700;
  color: #a78bfa;
  min-width: 90px;
  text-align: center;
  transition: all 0.15s;
}
.note-badge.active {
  background: #2d1b69;
  border-color: #8b5cf6;
  color: #ede9fe;
  box-shadow: 0 0 18px rgba(139,92,246,0.4);
}
.freq-label {
  font-size: 12px;
  color: #6b7280;
  text-align: center;
  margin-top: 4px;
}
.status {
  font-size: 13px;
  color: #6b7280;
  height: 18px;
}
.status.playing { color: #a78bfa; }
.status.done { color: #6ee7b7; }
.notes-grid { display: flex; flex-direction: column; align-items: center; gap: 6px; }
</style>
</head>
<body>
<h1>&#9834; Web Audio Oscillator</h1>
<p class="subtitle">C4 → E4 → G4 → A4 → C5 scale via OscillatorNode</p>

<canvas id="waveform" width="560" height="160"></canvas>

<div class="notes-grid">
  <div class="note-display" id="noteDisplay"></div>
  <div class="freq-label" id="freqLabel">&nbsp;</div>
</div>

<p class="status" id="status">Initializing AudioContext…</p>

<script>
const NOTES = [
  { name: 'C4', freq: 261.63 },
  { name: 'E4', freq: 329.63 },
  { name: 'G4', freq: 392.00 },
  { name: 'A4', freq: 440.00 },
  { name: 'C5', freq: 523.25 },
];

const canvas = document.getElementById('waveform');
const ctx2d = canvas.getContext('2d');
const statusEl = document.getElementById('status');
const freqLabel = document.getElementById('freqLabel');
const noteDisplay = document.getElementById('noteDisplay');

// Build note badges
NOTES.forEach((n, i) => {
  const div = document.createElement('div');
  div.className = 'note-badge';
  div.id = 'note-' + i;
  div.textContent = n.name;
  noteDisplay.appendChild(div);
});

let analyser, dataArray, animId;

function drawWaveform() {
  animId = requestAnimationFrame(drawWaveform);
  if (!analyser) return;
  analyser.getByteTimeDomainData(dataArray);

  const W = canvas.width, H = canvas.height;
  ctx2d.clearRect(0, 0, W, H);

  // Grid lines
  ctx2d.strokeStyle = '#1a1a2e';
  ctx2d.lineWidth = 1;
  for (let y = 0; y <= H; y += H / 4) {
    ctx2d.beginPath(); ctx2d.moveTo(0, y); ctx2d.lineTo(W, y); ctx2d.stroke();
  }

  // Waveform
  ctx2d.strokeStyle = '#8b5cf6';
  ctx2d.lineWidth = 2;
  ctx2d.shadowColor = '#8b5cf6';
  ctx2d.shadowBlur = 6;
  ctx2d.beginPath();
  const sliceW = W / dataArray.length;
  let x = 0;
  for (let i = 0; i < dataArray.length; i++) {
    const v = dataArray[i] / 128.0;
    const y = (v * H) / 2;
    i === 0 ? ctx2d.moveTo(x, y) : ctx2d.lineTo(x, y);
    x += sliceW;
  }
  ctx2d.stroke();
  ctx2d.shadowBlur = 0;
}

async function playScale() {
  const AudioCtx = window.AudioContext || window.webkitAudioContext;
  if (!AudioCtx) {
    statusEl.textContent = 'Web Audio API not supported in this browser.';
    return;
  }

  const audioCtx = new AudioCtx();

  analyser = audioCtx.createAnalyser();
  analyser.fftSize = 2048;
  dataArray = new Uint8Array(analyser.frequencyBinCount);

  drawWaveform();

  statusEl.className = 'status playing';
  statusEl.textContent = 'Playing scale…';

  for (let i = 0; i < NOTES.length; i++) {
    const note = NOTES[i];

    // Highlight badge
    document.querySelectorAll('.note-badge').forEach(b => b.classList.remove('active'));
    document.getElementById('note-' + i).classList.add('active');
    freqLabel.textContent = note.freq + ' Hz';

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

    osc.type = 'sine';
    osc.frequency.setValueAtTime(note.freq, audioCtx.currentTime);

    // Envelope: fade in 20ms, sustain, fade out 60ms
    gain.gain.setValueAtTime(0, audioCtx.currentTime);
    gain.gain.linearRampToValueAtTime(0.4, audioCtx.currentTime + 0.02);
    gain.gain.setValueAtTime(0.4, audioCtx.currentTime + 0.38);
    gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.44);

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

    osc.start(audioCtx.currentTime);
    osc.stop(audioCtx.currentTime + 0.45);

    await new Promise(r => setTimeout(r, 500));
  }

  document.querySelectorAll('.note-badge').forEach(b => b.classList.remove('active'));
  statusEl.className = 'status done';
  statusEl.textContent = '✓ Scale complete — C4 to C5';
  freqLabel.textContent = '';
}

// Auto-play after brief delay
setTimeout(playScale, 400);
</script>
</body>
</html>
```
