---
name: "chord-synthesizer"
description: "Polyphonic chord synthesizer using Web Audio API. Plays a I→vi→IV→V chord progression (C-Am-F-G) using triangle wave oscillators routed through a BiquadFilter and AnalyserNode. Tests complex multi-node audio graph capture."
metadata:
  version: "1.0.0"
  runtime:
    entrypoint: "index.html"
    timeout_seconds: 12
disable-model-invocation: true
---

# Chord Synthesizer

Polyphonic synthesizer demonstrating complex Web Audio API routing.

## Core Concepts
- Multiple simultaneous `OscillatorNode` voices
- `BiquadFilter` for tone shaping
- Gain envelopes for natural attack/release
- Frequency spectrum visualization

## Usage
Plays C major → A minor → F major → G major chord progression automatically.

---

## 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>Chord Synthesizer</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: #818cf8; letter-spacing: -0.5px; }
.subtitle { font-size: 13px; color: #6b7280; }

.progression {
  display: flex;
  gap: 12px;
}
.chord-card {
  background: #0d0d1a;
  border: 1px solid #1e1e3a;
  border-radius: 12px;
  padding: 14px 18px;
  text-align: center;
  min-width: 88px;
  transition: all 0.18s;
}
.chord-card.active {
  background: #1a1a3e;
  border-color: #818cf8;
  box-shadow: 0 0 22px rgba(129,140,248,0.4);
}
.chord-card .roman { font-size: 11px; color: #4b5563; margin-bottom: 4px; letter-spacing: 1px; }
.chord-card .name { font-size: 24px; font-weight: 700; color: #a5b4fc; }
.chord-card.active .name { color: #fff; }
.chord-card .notes { font-size: 10px; color: #4b5563; margin-top: 5px; line-height: 1.6; }
.chord-card.active .notes { color: #818cf8; }

canvas {
  border: 1px solid #1e1e3a;
  border-radius: 10px;
  background: #080812;
  display: block;
}

.status { font-size: 13px; color: #6b7280; height: 18px; }
.status.playing { color: #a5b4fc; }
.status.done { color: #6ee7b7; }
</style>
</head>
<body>
<h1>&#9836; Chord Synthesizer</h1>
<p class="subtitle">I → vi → IV → V progression · Triangle wave · BiquadFilter</p>

<div class="progression" id="progression"></div>

<canvas id="spectrum" width="560" height="130"></canvas>

<p class="status" id="status">Initializing audio graph…</p>

<script>
const CHORDS = [
  { roman: 'I',  name: 'C',  freqs: [261.63, 329.63, 392.00], noteNames: 'C4 · E4 · G4' },
  { roman: 'vi', name: 'Am', freqs: [220.00, 261.63, 329.63], noteNames: 'A3 · C4 · E4' },
  { roman: 'IV', name: 'F',  freqs: [174.61, 220.00, 261.63], noteNames: 'F3 · A3 · C4' },
  { roman: 'V',  name: 'G',  freqs: [196.00, 246.94, 293.66], noteNames: 'G3 · B3 · D4' },
];

const CHORD_DURATION = 700; // ms

// Build chord cards
const progression = document.getElementById('progression');
CHORDS.forEach((c, i) => {
  const card = document.createElement('div');
  card.className = 'chord-card';
  card.id = 'chord-' + i;
  card.innerHTML = `
    <div class="roman">${c.roman}</div>
    <div class="name">${c.name}</div>
    <div class="notes">${c.noteNames}</div>
  `;
  progression.appendChild(card);
});

// Canvas spectrum visualizer
const canvas = document.getElementById('spectrum');
const ctx2d = canvas.getContext('2d');
let analyser, freqData, animId;

function drawSpectrum() {
  animId = requestAnimationFrame(drawSpectrum);
  if (!analyser) return;

  analyser.getByteFrequencyData(freqData);

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

  // Draw bars
  const barCount = 56;
  const barW = Math.floor(W / barCount) - 2;
  const step = Math.floor(freqData.length / barCount);

  for (let i = 0; i < barCount; i++) {
    const value = freqData[i * step] / 255;
    const barH = Math.max(2, value * H);
    const hue = 220 + value * 60; // blue → purple
    const lightness = 40 + value * 30;
    ctx2d.fillStyle = `hsl(${hue}, 80%, ${lightness}%)`;
    const x = i * (barW + 2) + 4;
    ctx2d.beginPath();
    ctx2d.roundRect(x, H - barH, barW, barH, [3, 3, 0, 0]);
    ctx2d.fill();
  }
}

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

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

  const audioCtx = new AudioCtx();

  // Master gain
  const masterGain = audioCtx.createGain();
  masterGain.gain.setValueAtTime(0.18, audioCtx.currentTime); // quiet to avoid clipping 3 voices

  // BiquadFilter (lowpass for warmth)
  const filter = audioCtx.createBiquadFilter();
  filter.type = 'lowpass';
  filter.frequency.setValueAtTime(3200, audioCtx.currentTime);
  filter.Q.setValueAtTime(0.8, audioCtx.currentTime);

  // AnalyserNode for spectrum display
  analyser = audioCtx.createAnalyser();
  analyser.fftSize = 512;
  analyser.smoothingTimeConstant = 0.75;
  freqData = new Uint8Array(analyser.frequencyBinCount);

  // Routing: masterGain → filter → destination
  //          masterGain → analyser (parallel tap for visualization)
  masterGain.connect(filter);
  filter.connect(audioCtx.destination);
  masterGain.connect(analyser);

  drawSpectrum();

  statusEl.className = 'status playing';
  statusEl.textContent = 'Playing I → vi → IV → V…';

  const durationSec = CHORD_DURATION / 1000;

  for (let ci = 0; ci < CHORDS.length; ci++) {
    const chord = CHORDS[ci];

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

    const t = audioCtx.currentTime;
    const voiceGains = [];

    // Create one oscillator per note in the chord
    chord.freqs.forEach(freq => {
      const osc = audioCtx.createOscillator();
      const vGain = audioCtx.createGain();

      osc.type = 'triangle';
      osc.frequency.setValueAtTime(freq, t);

      // Attack/release envelope
      vGain.gain.setValueAtTime(0, t);
      vGain.gain.linearRampToValueAtTime(1.0, t + 0.03);
      vGain.gain.setValueAtTime(1.0, t + durationSec - 0.08);
      vGain.gain.linearRampToValueAtTime(0, t + durationSec);

      osc.connect(vGain);
      vGain.connect(masterGain);

      osc.start(t);
      osc.stop(t + durationSec + 0.01);
      voiceGains.push(vGain);
    });

    await new Promise(r => setTimeout(r, CHORD_DURATION + 80));
  }

  // Stop visualization after short tail
  await new Promise(r => setTimeout(r, 400));
  cancelAnimationFrame(animId);

  // Clear spectrum
  ctx2d.clearRect(0, 0, canvas.width, canvas.height);
  document.querySelectorAll('.chord-card').forEach(c => c.classList.remove('active'));

  statusEl.className = 'status done';
  statusEl.textContent = '✓ Progression complete — C · Am · F · G';
}

setTimeout(playProgression, 350);
</script>
</body>
</html>
```
