---
description: "Demonstrates Web Audio API buffer playback by generating float32 PCM samples in JavaScript and playing them via AudioBufferSourceNode. Plays a 4-note arpeggio (A4, C5, E5, G5) through an AnalyserNode for real-time EQ visualization."
---

# Dynamic Audio Element

Generates PCM audio samples in JavaScript and plays them via AudioBufferSourceNode.

## Core Concepts
- Float32 PCM sample generation (sine wave math with fade envelope)
- `AudioBuffer` creation and population
- `AudioBufferSourceNode` for sample-accurate playback
- `AnalyserNode` for real-time EQ bar visualization

## Usage
Automatic playback of a 4-note arpeggio (A4, C5, E5, G5). Each note is a freshly generated 450ms audio buffer played sequentially.

---

## 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>Dynamic Audio Element</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: 28px;
  padding: 24px;
}
h1 { font-size: 22px; font-weight: 700; color: #06b6d4; letter-spacing: -0.5px; }
.subtitle { font-size: 13px; color: #6b7280; }
.notes-row {
  display: flex;
  gap: 14px;
}
.note-card {
  background: #0d1117;
  border: 1px solid #1e293b;
  border-radius: 12px;
  padding: 14px 20px;
  text-align: center;
  min-width: 80px;
  transition: all 0.15s;
}
.note-card.active {
  background: #0c2a33;
  border-color: #06b6d4;
  box-shadow: 0 0 20px rgba(6,182,212,0.35);
}
.note-card .name {
  font-size: 20px;
  font-weight: 700;
  color: #67e8f9;
}
.note-card.active .name { color: #fff; }
.note-card .hz {
  font-size: 11px;
  color: #4b5563;
  margin-top: 4px;
}
.eq-bars {
  display: flex;
  gap: 5px;
  align-items: flex-end;
  height: 60px;
}
.bar {
  width: 14px;
  border-radius: 3px 3px 0 0;
  background: #06b6d4;
  transition: height 0.08s ease;
  height: 4px;
  opacity: 0.5;
}
.bar.active { opacity: 1; }
.status {
  font-size: 13px;
  color: #6b7280;
  height: 18px;
}
.status.playing { color: #67e8f9; }
.status.done { color: #6ee7b7; }
.tech-row {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  justify-content: center;
}
.chip {
  font-size: 11px;
  padding: 3px 10px;
  border-radius: 20px;
  background: #0d1117;
  border: 1px solid #1e293b;
  color: #6b7280;
}
</style>
</head>
<body>
<h1>&#128267; Dynamic Audio Element</h1>
<p class="subtitle">Float32 PCM → AudioBuffer → AudioBufferSourceNode arpeggio</p>

<div class="notes-row" id="notesRow"></div>

<div class="eq-bars" id="eqBars"></div>

<p class="status" id="status">Building PCM audio…</p>

<div class="tech-row">
  <span class="chip">AudioContext</span>
  <span class="chip">AudioBuffer</span>
  <span class="chip">AudioBufferSourceNode</span>
  <span class="chip">AnalyserNode</span>
</div>

<script>
const NOTES = [
  { name: 'A4', freq: 440.00 },
  { name: 'C5', freq: 523.25 },
  { name: 'E5', freq: 659.26 },
  { name: 'G5', freq: 783.99 },
];

const SAMPLE_RATE = 44100;
const DURATION = 0.45; // seconds per note

// Build note cards
const notesRow = document.getElementById('notesRow');
NOTES.forEach((n, i) => {
  const card = document.createElement('div');
  card.className = 'note-card';
  card.id = 'nc-' + i;
  card.innerHTML = `<div class="name">${n.name}</div><div class="hz">${n.freq} Hz</div>`;
  notesRow.appendChild(card);
});

// Build EQ bars
const eqBars = document.getElementById('eqBars');
const BAR_COUNT = 12;
const bars = [];
for (let i = 0; i < BAR_COUNT; i++) {
  const b = document.createElement('div');
  b.className = 'bar';
  eqBars.appendChild(b);
  bars.push(b);
}

let analyser, dataArray, eqAnimId = null;

function animateEq(active) {
  if (!active) {
    if (eqAnimId) cancelAnimationFrame(eqAnimId);
    bars.forEach(b => { b.style.height = '4px'; b.classList.remove('active'); });
    return;
  }
  bars.forEach(b => b.classList.add('active'));
  function frame() {
    if (analyser) {
      analyser.getByteFrequencyData(dataArray);
      const step = Math.floor(dataArray.length / BAR_COUNT);
      bars.forEach((b, i) => {
        const v = dataArray[i * step] / 255;
        b.style.height = (4 + v * 52) + 'px';
      });
    } else {
      bars.forEach(b => { b.style.height = (6 + Math.random() * 50) + 'px'; });
    }
    eqAnimId = requestAnimationFrame(frame);
  }
  frame();
}

const statusEl = document.getElementById('status');

async function playArpeggio() {
  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 = 256;
  dataArray = new Uint8Array(analyser.frequencyBinCount);
  analyser.connect(audioCtx.destination);

  statusEl.className = 'status playing';
  statusEl.textContent = 'Playing arpeggio…';
  animateEq(true);

  const numSamples = Math.floor(SAMPLE_RATE * DURATION);
  const fadeLen = Math.floor(SAMPLE_RATE * 0.03); // 30ms fade

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

    // Highlight card
    document.querySelectorAll('.note-card').forEach(c => c.classList.remove('active'));
    document.getElementById('nc-' + i).classList.add('active');

    // Generate float32 PCM samples
    const audioBuffer = audioCtx.createBuffer(1, numSamples, SAMPLE_RATE);
    const data = audioBuffer.getChannelData(0);
    for (let j = 0; j < numSamples; j++) {
      let amp = 0.6;
      if (j < fadeLen) amp *= j / fadeLen;
      else if (j > numSamples - fadeLen) amp *= (numSamples - j) / fadeLen;
      data[j] = amp * Math.sin(2 * Math.PI * note.freq * j / SAMPLE_RATE);
    }

    // Play via AudioBufferSourceNode → analyser → destination
    const source = audioCtx.createBufferSource();
    source.buffer = audioBuffer;
    source.connect(analyser);
    source.start();

    await new Promise(r => setTimeout(r, DURATION * 1000 + 80));
  }

  animateEq(false);
  document.querySelectorAll('.note-card').forEach(c => c.classList.remove('active'));
  statusEl.className = 'status done';
  statusEl.textContent = '✓ Arpeggio complete — A4 C5 E5 G5';
}

setTimeout(playArpeggio, 300);
</script>
</body>
</html>
```
