
Web Audio Api
- 330 installs
- 45 repo stars
- Updated December 6, 2025
- martinholovsky/claude-skills-generator
web-audio-api is a Claude Code frontend skill that implements Web Audio API synthesis, effects chains, and microphone capture for developers who add interactive browser sound to web applications.
About
web-audio-api (version 1.0.0) in martinholovsky/claude-skills-generator teaches Web Audio API patterns for HUD feedback, voice input, spatial audio, and real-time visualization in browser apps. It documents AudioContext lifecycle rules—user-gesture initialization, suspended-state resume, and close-on-unmount cleanup—plus composables for oscillators, gain nodes, AnalyserNode visualization, PannerNode spatial audio, and getUserMedia microphone capture with permission handling. Browser support tables cover Chrome, Firefox, and Safari AudioContext and AudioWorklet minimum versions. Developers reach for web-audio-api when adding confirm, alert, and error beeps, voice-processing nodes, or Vitest-tested audio composables without memory leaks from per-sound AudioContext creation. The skill enforces TDD with Vitest mocks, volume clamping, buffer pooling, and accessibility fallbacks when microphone access is denied.
- AudioContext and node graph wiring
- Oscillators, filters, and gain staging
- Buffer loading and precise scheduling
- User-gesture unlock and latency pitfalls
- Effects chains for games and creative tools
Web Audio Api by the numbers
- 330 all-time installs (skills.sh)
- +10 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #720 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/martinholovsky/claude-skills-generator --skill web-audio-apiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 330 |
|---|---|
| repo stars | ★ 45 |
| Last updated | December 6, 2025 |
| Repository | martinholovsky/claude-skills-generator ↗ |
How do you add Web Audio API sound to web apps?
When adding browser sound synthesis, effects chains, playback controls, or interactive audio nodes to web apps and creative experiences.
Who is it for?
Frontend engineers adding HUD audio feedback, voice input, or interactive sound nodes to Vue or TypeScript web applications.
Skip if: Native mobile audio SDK work or server-side audio transcoding without browser AudioContext APIs.
When should I use this skill?
User asks for Web Audio API synthesis, browser sound effects, microphone capture, spatial audio, or audio visualization composables.
What you get
TypeScript audio composables, oscillator and gain-node patterns, spatial audio setup, microphone capture code, and Vitest test suites.
- Audio composable modules
- Vitest audio test suites
- HUD and microphone integration patterns
By the numbers
- Skill version 1.0.0 with browser support table for 3 major engines
- Documents 4 preset HUD sounds: confirm, alert, error, and click
- Limits concurrent active sound sources to 8 in SoundManager pattern
Files
Web Audio API Skill
1. Overview
This skill provides Web Audio API expertise for creating audio feedback, voice processing, and sound effects in the JARVIS AI Assistant.
Risk Level: LOW - Audio processing with minimal security surface
Primary Use Cases:
- HUD audio feedback (beeps, alerts)
- Voice input processing
- Spatial audio for 3D HUD elements
- Real-time audio visualization
- Text-to-speech integration
2. Core Responsibilities
2.1 Fundamental Principles
1. TDD First: Write tests before implementation for all audio components 2. Performance Aware: Optimize for 60fps with minimal audio latency 3. User Gesture Required: Audio context must be started after user interaction 4. Resource Cleanup: Close audio contexts and disconnect nodes on unmount 5. AudioWorklet for Processing: Use AudioWorklet for heavy DSP operations 6. Accessibility: Provide visual alternatives to audio feedback 7. Volume Control: Respect system and user volume preferences 8. Error Handling: Gracefully handle audio permission denials
3. Technology Stack & Versions
3.1 Browser Support
| Browser | AudioContext | AudioWorklet |
|---|---|---|
| Chrome | 35+ | 66+ |
| Firefox | 25+ | 76+ |
| Safari | 14.1+ | 14.1+ |
3.2 TypeScript Types
// types/audio.ts
interface AudioFeedbackOptions {
frequency: number
duration: number
type: OscillatorType
volume: number
}
interface SpatialAudioPosition {
x: number
y: number
z: number
}4. Implementation Patterns
4.1 Audio Context Management
// composables/useAudioContext.ts
export function useAudioContext() {
const audioContext = ref<AudioContext | null>(null)
const isInitialized = ref(false)
async function initialize() {
if (audioContext.value) return
audioContext.value = new AudioContext()
if (audioContext.value.state === 'suspended') await audioContext.value.resume()
isInitialized.value = true
}
onUnmounted(() => {
audioContext.value?.close()
audioContext.value = null
})
return { audioContext: readonly(audioContext), isInitialized: readonly(isInitialized), initialize }
}4.2 HUD Beep Feedback
// composables/useHUDSounds.ts
export function useHUDSounds() {
const { audioContext, initialize } = useAudioContext()
async function playBeep(options: Partial<AudioFeedbackOptions> = {}) {
await initialize()
const ctx = audioContext.value
if (!ctx) return
const { frequency = 440, duration = 0.1, type = 'sine', volume = 0.3 } = options
const safeVolume = Math.max(0, Math.min(1, volume))
const oscillator = ctx.createOscillator()
const gainNode = ctx.createGain()
oscillator.type = type
oscillator.frequency.value = frequency
gainNode.gain.value = safeVolume
gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration)
oscillator.connect(gainNode).connect(ctx.destination)
oscillator.start()
oscillator.stop(ctx.currentTime + duration)
}
const sounds = {
confirm: () => playBeep({ frequency: 880, duration: 0.1, volume: 0.2 }),
alert: () => playBeep({ frequency: 440, duration: 0.3, type: 'square', volume: 0.4 }),
error: () => playBeep({ frequency: 220, duration: 0.5, type: 'sawtooth', volume: 0.3 }),
click: () => playBeep({ frequency: 1000, duration: 0.05, volume: 0.1 })
}
return { playBeep, sounds }
}4.3 Audio Visualization
// composables/useAudioVisualization.ts
export function useAudioVisualization() {
const { audioContext, initialize } = useAudioContext()
let analyser: AnalyserNode | null = null
let dataArray: Uint8Array | null = null
async function setupAnalyser(source: AudioNode) {
await initialize()
const ctx = audioContext.value
if (!ctx) return
analyser = ctx.createAnalyser()
analyser.fftSize = 256
dataArray = new Uint8Array(analyser.frequencyBinCount)
source.connect(analyser)
}
function getFrequencyData(): Uint8Array | null {
if (!analyser || !dataArray) return null
analyser.getByteFrequencyData(dataArray)
return dataArray
}
return { setupAnalyser, getFrequencyData }
}4.4 Spatial Audio for 3D HUD
// composables/useSpatialAudio.ts
export function useSpatialAudio() {
const { audioContext, initialize } = useAudioContext()
let panner: PannerNode | null = null
async function createSpatialSource(position: SpatialAudioPosition) {
await initialize()
const ctx = audioContext.value
if (!ctx) return null
panner = ctx.createPanner()
panner.panningModel = 'HRTF'
panner.distanceModel = 'inverse'
setPosition(position)
return panner
}
function setPosition(pos: SpatialAudioPosition) {
if (!panner) return
panner.positionX.value = pos.x
panner.positionY.value = pos.y
panner.positionZ.value = pos.z
}
return { createSpatialSource, setPosition }
}4.5 Microphone Input
// composables/useMicrophone.ts
export function useMicrophone() {
const { audioContext, initialize } = useAudioContext()
const stream = ref<MediaStream | null>(null)
const isListening = ref(false)
const error = ref<string | null>(null)
async function startListening() {
try {
await initialize()
stream.value = await navigator.mediaDevices.getUserMedia({
audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true }
})
isListening.value = true
return stream.value
} catch (err) {
error.value = err instanceof Error ? err.message : 'Microphone access denied'
return null
}
}
function stopListening() {
stream.value?.getTracks().forEach(track => track.stop())
stream.value = null
isListening.value = false
}
onUnmounted(() => stopListening())
return { stream: readonly(stream), isListening: readonly(isListening), error: readonly(error), startListening, stopListening }
}5. Implementation Workflow (TDD)
Step 1: Write Failing Test First
// tests/composables/useHUDSounds.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useHUDSounds } from '~/composables/useHUDSounds'
// Mock AudioContext nodes
const mockOscillator = { connect: vi.fn(), start: vi.fn(), stop: vi.fn(), frequency: { value: 440 } }
const mockGainNode = { connect: vi.fn(), gain: { value: 1, exponentialRampToValueAtTime: vi.fn() } }
const mockAudioContext = {
state: 'running', currentTime: 0, destination: {},
createOscillator: vi.fn(() => mockOscillator),
createGain: vi.fn(() => mockGainNode),
resume: vi.fn(), close: vi.fn()
}
vi.stubGlobal('AudioContext', vi.fn(() => mockAudioContext))
describe('useHUDSounds', () => {
beforeEach(() => vi.clearAllMocks())
it('creates oscillator with correct frequency', async () => {
const { playBeep } = useHUDSounds()
await playBeep({ frequency: 880 })
expect(mockOscillator.frequency.value).toBe(880)
})
it('clamps volume to valid range', async () => {
const { playBeep } = useHUDSounds()
await playBeep({ volume: 2.5 })
expect(mockGainNode.gain.value).toBeLessThanOrEqual(1)
})
it('connects nodes in correct order', async () => {
const { playBeep } = useHUDSounds()
await playBeep()
expect(mockOscillator.connect).toHaveBeenCalledWith(mockGainNode)
expect(mockGainNode.connect).toHaveBeenCalledWith(mockAudioContext.destination)
})
})Step 2: Implement Minimum to Pass
// composables/useHUDSounds.ts
export function useHUDSounds() {
// Implementation from section 4.2
// Only add features that tests require
}Step 3: Refactor Following Patterns
After tests pass, refactor to:
- Extract shared audio context logic
- Add proper TypeScript types
- Implement cleanup on unmount
Step 4: Run Full Verification
# Run all audio-related tests
npm test -- --grep "audio|sound|HUD"
# Check types
npm run typecheck
# Verify no memory leaks in browser
npm run dev # Test manually with DevTools Memory tab6. Performance Patterns
6.1 AudioWorklet for Processing
// ✅ Good: Use AudioWorklet for DSP (runs on audio thread)
class NoiseGateProcessor extends AudioWorkletProcessor {
process(inputs: Float32Array[][], outputs: Float32Array[][]) {
for (let ch = 0; ch < inputs[0].length; ch++) {
for (let i = 0; i < inputs[0][ch].length; i++) {
outputs[0][ch][i] = Math.abs(inputs[0][ch][i]) > 0.01 ? inputs[0][ch][i] : 0
}
}
return true
}
}
registerProcessor('noise-gate', NoiseGateProcessor)
// ❌ Bad: ScriptProcessorNode (deprecated, blocks main thread)6.2 Buffer Pooling
// ✅ Good: Reuse audio buffers
class AudioBufferPool {
private pool: AudioBuffer[] = []
constructor(ctx: AudioContext, size: number, length: number) {
for (let i = 0; i < size; i++) {
this.pool.push(ctx.createBuffer(2, length, ctx.sampleRate))
}
}
acquire(): AudioBuffer | undefined { return this.pool.pop() }
release(buffer: AudioBuffer) {
for (let ch = 0; ch < buffer.numberOfChannels; ch++) {
buffer.getChannelData(ch).fill(0)
}
this.pool.push(buffer)
}
}
// ❌ Bad: Create new buffer each time
const buffer = ctx.createBuffer(2, 44100, 44100) // Allocates memory each call6.3 Offline Rendering
// ✅ Good: Pre-render complex sounds
async function prerenderSound(): Promise<AudioBuffer> {
const offlineCtx = new OfflineAudioContext(2, 44100, 44100)
const osc = offlineCtx.createOscillator()
const gain = offlineCtx.createGain()
osc.connect(gain).connect(offlineCtx.destination)
gain.gain.setValueAtTime(0, 0)
gain.gain.linearRampToValueAtTime(1, 0.01)
gain.gain.exponentialRampToValueAtTime(0.001, 1)
osc.start(); osc.stop(1)
return offlineCtx.startRendering()
}
// ❌ Bad: Generate complex sounds in real-time (multiple oscillators computed live)6.4 Node Graph Optimization
// ✅ Good: Reuse master gain node
const masterGain = ctx.createGain()
masterGain.connect(ctx.destination)
function playSound(buffer: AudioBuffer) {
const source = ctx.createBufferSource()
source.buffer = buffer
source.connect(masterGain)
source.start()
}
// ❌ Bad: Create full chain for each sound (gain + compressor per play)6.5 Memory Management
// ✅ Good: Disconnect and cleanup nodes
function playOneShot(buffer: AudioBuffer) {
const source = ctx.createBufferSource()
source.buffer = buffer
source.connect(masterGain)
source.onended = () => source.disconnect()
source.start()
}
// ✅ Good: Limit concurrent sounds (max 8)
class SoundManager {
private activeSources = new Set<AudioBufferSourceNode>()
play(buffer: AudioBuffer) {
if (this.activeSources.size >= 8) this.activeSources.values().next().value?.stop()
const source = ctx.createBufferSource()
source.buffer = buffer
source.connect(masterGain)
source.onended = () => { source.disconnect(); this.activeSources.delete(source) }
this.activeSources.add(source)
source.start()
}
}
// ❌ Bad: Never cleanup - nodes stay in memory after playback
const source = ctx.createBufferSource()
source.connect(ctx.destination)
source.start()7. Quality Standards
// ✅ Always require user gesture
button.addEventListener('click', async () => {
await audioContext.resume()
playSound()
})
// ✅ Respect user preferences
if (usePreferencesStore().preferences.soundEnabled) playBeep()
// ✅ Handle permission denial gracefully
try {
await navigator.mediaDevices.getUserMedia({ audio: true })
} catch (err) {
if (err.name === 'NotAllowedError') {
showVisualFeedback('Microphone access required')
}
}8. Testing & Quality
describe('HUD Sounds', () => {
it('validates volume bounds', async () => {
const { playBeep } = useHUDSounds()
await playBeep({ volume: 2 }) // Clamped to 1
await playBeep({ volume: -1 }) // Clamped to 0
})
})9. Common Mistakes & Anti-Patterns
9.1 Critical Anti-Patterns
// ❌ Auto-play without user gesture - BLOCKED
onMounted(() => playSound())
// ✅ After user interaction
const handleClick = async () => { await audioContext.resume(); playSound() }
// ❌ Memory leak - no cleanup
const audioContext = new AudioContext()
// ✅ Proper cleanup
onUnmounted(() => audioContext.close())
// ❌ New context per sound - performance killer
function playSound() { const ctx = new AudioContext() }
// ✅ Reuse context
const ctx = new AudioContext()
function playSound() { /* reuse ctx */ }10. Pre-Implementation Checklist
Phase 1: Before Writing Code
- [ ] Tests written for audio node creation and connections
- [ ] Tests written for volume clamping and validation
- [ ] Performance requirements identified (latency, concurrent sounds)
- [ ] AudioWorklet needed for DSP? Worklet file created
- [ ] Buffer pool size calculated for expected usage
Phase 2: During Implementation
- [ ] User gesture required for AudioContext initialization
- [ ] Audio context reused (not created per sound)
- [ ] Nodes disconnected in onended callbacks
- [ ] Volume bounds validated (0-1 range)
- [ ] Microphone permissions handled gracefully
- [ ] Error states provide visual feedback
Phase 3: Before Committing
- [ ] All audio tests pass:
npm test -- --grep "audio" - [ ] Type checking passes:
npm run typecheck - [ ] No memory leaks (tested in DevTools Memory tab)
- [ ] Audio context closed on component unmount
- [ ] Visual alternatives provided for accessibility
- [ ] Sound can be disabled via user preferences
- [ ] Volume respects system preferences
11. Summary
Web Audio API for JARVIS: Initialize after user gesture, cleanup on unmount, handle permission denials, provide visual alternatives. See references/advanced-patterns.md
Web Audio API Advanced Patterns
AudioWorklet Processing
Custom Audio Processor
// audioWorklets/noiseGate.js
class NoiseGateProcessor extends AudioWorkletProcessor {
static get parameterDescriptors() {
return [
{
name: 'threshold',
defaultValue: 0.01,
minValue: 0,
maxValue: 1
}
]
}
process(inputs, outputs, parameters) {
const input = inputs[0]
const output = outputs[0]
const threshold = parameters.threshold[0]
for (let channel = 0; channel < input.length; channel++) {
const inputChannel = input[channel]
const outputChannel = output[channel]
for (let i = 0; i < inputChannel.length; i++) {
outputChannel[i] = Math.abs(inputChannel[i]) > threshold
? inputChannel[i]
: 0
}
}
return true
}
}
registerProcessor('noise-gate', NoiseGateProcessor)Loading Worklet
async function setupNoiseGate(ctx: AudioContext) {
await ctx.audioWorklet.addModule('/audioWorklets/noiseGate.js')
const noiseGate = new AudioWorkletNode(ctx, 'noise-gate')
noiseGate.parameters.get('threshold').value = 0.05
return noiseGate
}Voice Processing Pipeline
export function createVoicePipeline(ctx: AudioContext) {
// High-pass filter to remove rumble
const highpass = ctx.createBiquadFilter()
highpass.type = 'highpass'
highpass.frequency.value = 80
// Low-pass to remove hiss
const lowpass = ctx.createBiquadFilter()
lowpass.type = 'lowpass'
lowpass.frequency.value = 8000
// Compressor for consistent levels
const compressor = ctx.createDynamicsCompressor()
compressor.threshold.value = -24
compressor.knee.value = 30
compressor.ratio.value = 12
compressor.attack.value = 0.003
compressor.release.value = 0.25
// Gain for final level
const gain = ctx.createGain()
gain.gain.value = 1.5
// Connect chain
highpass.connect(lowpass)
lowpass.connect(compressor)
compressor.connect(gain)
return {
input: highpass,
output: gain
}
}Convolution Reverb
export async function createReverb(
ctx: AudioContext,
impulseUrl: string
) {
const convolver = ctx.createConvolver()
const response = await fetch(impulseUrl)
const arrayBuffer = await response.arrayBuffer()
convolver.buffer = await ctx.decodeAudioData(arrayBuffer)
return convolver
}JARVIS Voice Synthesizer
export function createJARVISVoice(ctx: AudioContext) {
// Ring modulator for robotic effect
const oscillator = ctx.createOscillator()
oscillator.frequency.value = 50
oscillator.type = 'sine'
const ringMod = ctx.createGain()
oscillator.connect(ringMod.gain)
oscillator.start()
// Vocoder-like effect
const bandpass = ctx.createBiquadFilter()
bandpass.type = 'bandpass'
bandpass.frequency.value = 1000
bandpass.Q.value = 5
// Distortion for edge
const waveshaper = ctx.createWaveShaper()
const curve = new Float32Array(256)
for (let i = 0; i < 256; i++) {
const x = (i / 128) - 1
curve[i] = Math.tanh(x * 2)
}
waveshaper.curve = curve
// Connect
ringMod.connect(bandpass)
bandpass.connect(waveshaper)
return {
input: ringMod,
output: waveshaper
}
}Speech Recognition Integration
export function useSpeechRecognition() {
const recognition = ref<SpeechRecognition | null>(null)
const transcript = ref('')
const isListening = ref(false)
function start() {
const SpeechRecognition =
window.SpeechRecognition || window.webkitSpeechRecognition
if (!SpeechRecognition) {
throw new Error('Speech recognition not supported')
}
recognition.value = new SpeechRecognition()
recognition.value.continuous = true
recognition.value.interimResults = true
recognition.value.onresult = (event) => {
let final = ''
for (let i = 0; i < event.results.length; i++) {
if (event.results[i].isFinal) {
final += event.results[i][0].transcript
}
}
transcript.value = final
}
recognition.value.start()
isListening.value = true
}
function stop() {
recognition.value?.stop()
isListening.value = false
}
return { transcript, isListening, start, stop }
}Related skills
How it compares
Pick web-audio-api for in-browser synthesis and HUD feedback; pick WebRTC skills when the goal is real-time peer audio streaming.
FAQ
What browsers does web-audio-api support?
web-audio-api documents AudioContext support from Chrome 35+, Firefox 25+, and Safari 14.1+, with AudioWorklet available from Chrome 66+, Firefox 76+, and Safari 14.1+ per its compatibility table.
How does web-audio-api prevent audio memory leaks?
web-audio-api reuses a single AudioContext, disconnects nodes in onended callbacks, closes the context on component unmount, and limits concurrent AudioBufferSourceNode instances to avoid orphaned graph nodes.