{"manifest":{"name":"Web Audio Oscillator","version":"1.0.0","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.","tags":["audio","web-audio","oscillator","waveform"],"standard":"agentskills.io","standard_version":"1.0","content_checksum":"4eb1314689b84c3daf701447ef338d0a2e1acbbc32fafaa845b64b7b72fd0354","bundle_checksum":"4f98942222e32285c5e9b92e33d8b7c795c722a9645dd705b1995aa6762c5dd6","metadata":{"runtime":{"entrypoint":"index.html","timeout_seconds":10}},"files":[{"path":"index.html","name":"index.html","mime_type":"text/html","checksum":"a3fc0dd4ae4528607d0e83b9d5344aa284d8517ffc1cfa1527c7aab6eef20ebb"}]},"files":{"SKILL.md":"# Web Audio Oscillator\n\nDemonstrates the Web Audio API by generating and playing a musical scale using OscillatorNode.\n\n## Core Concepts\n- `AudioContext` creation and lifecycle\n- `OscillatorNode` frequency control and envelope\n- `GainNode` for amplitude shaping\n- `AnalyserNode` for real-time waveform display\n\n## Usage\nOpen in any modern browser. The skill plays C4→C5 scale automatically and renders an oscilloscope.","index.html":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Web Audio Oscillator</title>\n<style>\n* { box-sizing: border-box; margin: 0; padding: 0; }\nbody {\n  background: #0a0a0f;\n  color: #e2e8f0;\n  font-family: system-ui, -apple-system, sans-serif;\n  min-height: 100vh;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  gap: 24px;\n  padding: 24px;\n}\nh1 {\n  font-size: 22px;\n  font-weight: 700;\n  color: #8b5cf6;\n  letter-spacing: -0.5px;\n}\n.subtitle { font-size: 13px; color: #6b7280; }\ncanvas {\n  border: 1px solid #1e1e3a;\n  border-radius: 10px;\n  background: #0d0d1a;\n  display: block;\n}\n.note-display {\n  display: flex;\n  gap: 10px;\n  align-items: center;\n}\n.note-badge {\n  background: #1a1a2e;\n  border: 1px solid #2d2d4e;\n  border-radius: 8px;\n  padding: 8px 18px;\n  font-size: 20px;\n  font-weight: 700;\n  color: #a78bfa;\n  min-width: 90px;\n  text-align: center;\n  transition: all 0.15s;\n}\n.note-badge.active {\n  background: #2d1b69;\n  border-color: #8b5cf6;\n  color: #ede9fe;\n  box-shadow: 0 0 18px rgba(139,92,246,0.4);\n}\n.freq-label {\n  font-size: 12px;\n  color: #6b7280;\n  text-align: center;\n  margin-top: 4px;\n}\n.status {\n  font-size: 13px;\n  color: #6b7280;\n  height: 18px;\n}\n.status.playing { color: #a78bfa; }\n.status.done { color: #6ee7b7; }\n.notes-grid { display: flex; flex-direction: column; align-items: center; gap: 6px; }\n</style>\n</head>\n<body>\n<h1>&#9834; Web Audio Oscillator</h1>\n<p class=\"subtitle\">C4 → E4 → G4 → A4 → C5 scale via OscillatorNode</p>\n\n<canvas id=\"waveform\" width=\"560\" height=\"160\"></canvas>\n\n<div class=\"notes-grid\">\n  <div class=\"note-display\" id=\"noteDisplay\"></div>\n  <div class=\"freq-label\" id=\"freqLabel\">&nbsp;</div>\n</div>\n\n<p class=\"status\" id=\"status\">Initializing AudioContext…</p>\n\n<script>\nconst NOTES = [\n  { name: 'C4', freq: 261.63 },\n  { name: 'E4', freq: 329.63 },\n  { name: 'G4', freq: 392.00 },\n  { name: 'A4', freq: 440.00 },\n  { name: 'C5', freq: 523.25 },\n];\n\nconst canvas = document.getElementById('waveform');\nconst ctx2d = canvas.getContext('2d');\nconst statusEl = document.getElementById('status');\nconst freqLabel = document.getElementById('freqLabel');\nconst noteDisplay = document.getElementById('noteDisplay');\n\n// Build note badges\nNOTES.forEach((n, i) => {\n  const div = document.createElement('div');\n  div.className = 'note-badge';\n  div.id = 'note-' + i;\n  div.textContent = n.name;\n  noteDisplay.appendChild(div);\n});\n\nlet analyser, dataArray, animId;\n\nfunction drawWaveform() {\n  animId = requestAnimationFrame(drawWaveform);\n  if (!analyser) return;\n  analyser.getByteTimeDomainData(dataArray);\n\n  const W = canvas.width, H = canvas.height;\n  ctx2d.clearRect(0, 0, W, H);\n\n  // Grid lines\n  ctx2d.strokeStyle = '#1a1a2e';\n  ctx2d.lineWidth = 1;\n  for (let y = 0; y <= H; y += H / 4) {\n    ctx2d.beginPath(); ctx2d.moveTo(0, y); ctx2d.lineTo(W, y); ctx2d.stroke();\n  }\n\n  // Waveform\n  ctx2d.strokeStyle = '#8b5cf6';\n  ctx2d.lineWidth = 2;\n  ctx2d.shadowColor = '#8b5cf6';\n  ctx2d.shadowBlur = 6;\n  ctx2d.beginPath();\n  const sliceW = W / dataArray.length;\n  let x = 0;\n  for (let i = 0; i < dataArray.length; i++) {\n    const v = dataArray[i] / 128.0;\n    const y = (v * H) / 2;\n    i === 0 ? ctx2d.moveTo(x, y) : ctx2d.lineTo(x, y);\n    x += sliceW;\n  }\n  ctx2d.stroke();\n  ctx2d.shadowBlur = 0;\n}\n\nasync function playScale() {\n  const AudioCtx = window.AudioContext || window.webkitAudioContext;\n  if (!AudioCtx) {\n    statusEl.textContent = 'Web Audio API not supported in this browser.';\n    return;\n  }\n\n  const audioCtx = new AudioCtx();\n\n  analyser = audioCtx.createAnalyser();\n  analyser.fftSize = 2048;\n  dataArray = new Uint8Array(analyser.frequencyBinCount);\n\n  drawWaveform();\n\n  statusEl.className = 'status playing';\n  statusEl.textContent = 'Playing scale…';\n\n  for (let i = 0; i < NOTES.length; i++) {\n    const note = NOTES[i];\n\n    // Highlight badge\n    document.querySelectorAll('.note-badge').forEach(b => b.classList.remove('active'));\n    document.getElementById('note-' + i).classList.add('active');\n    freqLabel.textContent = note.freq + ' Hz';\n\n    const osc = audioCtx.createOscillator();\n    const gain = audioCtx.createGain();\n\n    osc.type = 'sine';\n    osc.frequency.setValueAtTime(note.freq, audioCtx.currentTime);\n\n    // Envelope: fade in 20ms, sustain, fade out 60ms\n    gain.gain.setValueAtTime(0, audioCtx.currentTime);\n    gain.gain.linearRampToValueAtTime(0.4, audioCtx.currentTime + 0.02);\n    gain.gain.setValueAtTime(0.4, audioCtx.currentTime + 0.38);\n    gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.44);\n\n    osc.connect(gain);\n    gain.connect(analyser);\n    analyser.connect(audioCtx.destination);\n\n    osc.start(audioCtx.currentTime);\n    osc.stop(audioCtx.currentTime + 0.45);\n\n    await new Promise(r => setTimeout(r, 500));\n  }\n\n  document.querySelectorAll('.note-badge').forEach(b => b.classList.remove('active'));\n  statusEl.className = 'status done';\n  statusEl.textContent = '✓ Scale complete — C4 to C5';\n  freqLabel.textContent = '';\n}\n\n// Auto-play after brief delay\nsetTimeout(playScale, 400);\n</script>\n</body>\n</html>"}}