Web Audio API mit Claude Code: Wellenform, Aufnahme und Pegelmesser
Implementiere Web Audio API mit Claude Code: Wellenform, Aufnahme, Pegelmesser, Cleanup und Tests in React/TypeScript.
Mit der Audioerfahrung beginnen
Die Web Audio API ist die Browser-API für ein Audio-Graph-Modell. Du lädst Ton, verbindest ihn mit Nodes, änderst Lautstärke, analysierst Signale und sendest das Ergebnis an die Ausgabe. Für einfache Wiedergabe reicht oft das audio-Element. Web Audio API brauchst du, wenn ein Produkt einen Wellenform-Player, eine Aufnahmevorschau, einen Eingangspegelmesser, Benachrichtigungstöne, Effekte oder ein Sprachlernwerkzeug benötigt.
Claude Code ist dabei nützlich, weil das Problem nicht nur ein einzelnes Codebeispiel ist. Das Problem ist die Integration in eine bestehende React-App mit SSR, Autoplay-Regeln, Mikrofonberechtigungen, Cleanup, Accessibility und mobilem Verhalten. Wenn Claude Code Komponenten, Tests, Routen und Repository-Regeln lesen darf, kann es nicht nur Code erzeugen, sondern auch die typischen Audiofehler prüfen.
Dieser Leitfaden zeigt den Ablauf: Prompt für Claude Code, React/TypeScript-AudioContext-Hook, Wellenform mit AnalyserNode, Aufnahmevorschau, Pegelmesser, Benachrichtigungston, Cleanup, Playwright-Prüfung und manuelle Tests. Nutze parallel die offiziellen Quellen MDN Web Audio API, MDN Autoplay Guide und MediaRecorder. Für eine vollständige Player-Oberfläche passt auch der interne Artikel Audio Player mit Claude Code.
Fünf praktische Use Cases
Vor dem ersten Code solltest du den Use Case benennen. Unterschiedliche Audioerlebnisse brauchen unterschiedliche Nodes und Reviewpunkte.
| Use Case | Haupt-APIs | Was Claude Code bauen soll | Hauptrisiko |
|---|---|---|---|
| Wellenform-Player | AudioContext, AudioBufferSourceNode, AnalyserNode | Wiedergabe, Stop, Wellenform, Master-Lautstärke | source nach start() wiederverwenden |
| Aufnahmevorschau | getUserMedia, MediaRecorder, MediaStreamAudioSourceNode | Aufnehmen, Stoppen, lokale Blob URL, Anhören | Mikrofon-Tracks bleiben aktiv |
| Pegelmesser | AnalyserNode.getByteTimeDomainData | RMS-Berechnung und Live-Anzeige | requestAnimationFrame läuft weiter |
| Benachrichtigung und Effekte | OscillatorNode, GainNode | kurze Töne, Fade, Klickvermeidung | Ton vor User-Geste starten |
| Sprachlern-Tool | Playback-Hook, Recorder, Loop | Nachsprechen, Vergleich, Selbstkontrolle | unklare Einwilligung und Speicherung |
Diese Trennung verhindert überladene Implementierungen. Ein Wellenform-Player braucht kein Mikrofon. Eine Aufnahmevorschau sollte das Mikrofon nicht an die Lautsprecher weiterleiten. Ein Benachrichtigungston sollte kurz, leise und nach dem Ende getrennt sein.
Prompt für Claude Code
Der Prompt sollte Ziel, Grenzen und Prüfungen enthalten. Claude Code soll zuerst das Projekt lesen und erst dann editieren.
Implementiere eine Web Audio API Demo mit React + TypeScript.
Anforderungen:
- AudioContext nur nach einer User-Geste erstellen oder resume ausführen.
- useWebAudioEngine Hook für master gain, analyser, playback und cleanup hinzufügen.
- Wellenform-Player, Aufnahmevorschau, Eingangspegelmesser und Benachrichtigungston implementieren.
- Aufgenommenes Audio nur als lokale Blob URL verwenden; nicht hochladen.
- MediaStream tracks und AudioNodes beim Stoppen und Unmount trennen.
- requestAnimationFrame abbrechen und Blob URLs revoke ausführen.
- UI-Zustände für Autoplay-Blockierung, verweigerte Mikrofonberechtigung und nicht unterstützte Browser anzeigen.
Lies vor Änderungen die vorhandene Komponentenstruktur, Lint-Regeln und Tests.
Ergänze danach Playwright- oder manuelle Prüfnotizen.
Dieser Prompt ist absichtlich konkret. Web-Audio-Fehler treten oft erst nach dem ersten Erfolg auf: zweite Wiedergabe, Navigation, abgelehnte Berechtigung oder mobile Browser zeigen die Probleme.
React/TypeScript AudioContext Hook
Der Hook bündelt den Audio-Graphen. AudioContext ist der Einstieg, GainNode steuert Lautstärke und AnalyserNode liefert Daten für Wellenform und Pegel. Ein zentraler Hook erleichtert Cleanup und Review.
import { useCallback, useEffect, useRef, useState } from "react";
type WebKitWindow = Window & typeof globalThis & {
webkitAudioContext?: typeof AudioContext;
};
export type AudioEngine = {
context: AudioContext;
masterGain: GainNode;
analyser: AnalyserNode;
};
export function useWebAudioEngine() {
const engineRef = useRef<AudioEngine | null>(null);
const sourcesRef = useRef<Set<AudioBufferSourceNode>>(new Set());
const [state, setState] = useState<AudioContextState | "unsupported">("suspended");
const createEngine = useCallback(() => {
if (typeof window === "undefined") return null;
if (engineRef.current) return engineRef.current;
const AudioContextClass =
window.AudioContext ?? (window as WebKitWindow).webkitAudioContext;
if (!AudioContextClass) {
setState("unsupported");
return null;
}
const context = new AudioContextClass({ latencyHint: "interactive" });
const masterGain = context.createGain();
const analyser = context.createAnalyser();
analyser.fftSize = 2048;
analyser.smoothingTimeConstant = 0.82;
masterGain.gain.value = 0.8;
masterGain.connect(analyser);
analyser.connect(context.destination);
engineRef.current = { context, masterGain, analyser };
setState(context.state);
return engineRef.current;
}, []);
const resume = useCallback(async () => {
const engine = createEngine();
if (!engine) return null;
if (engine.context.state === "suspended") {
await engine.context.resume();
}
setState(engine.context.state);
return engine;
}, [createEngine]);
const playBuffer = useCallback(
async (url: string) => {
const engine = await resume();
if (!engine) return null;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to load audio: ${response.status}`);
}
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await engine.context.decodeAudioData(arrayBuffer.slice(0));
const source = engine.context.createBufferSource();
source.buffer = audioBuffer;
source.connect(engine.masterGain);
source.start();
sourcesRef.current.add(source);
source.onended = () => {
source.disconnect();
sourcesRef.current.delete(source);
};
return source;
},
[resume],
);
const setVolume = useCallback((volume: number) => {
const engine = createEngine();
if (!engine) return;
const safeVolume = Math.min(1, Math.max(0, volume));
engine.masterGain.gain.setTargetAtTime(
safeVolume,
engine.context.currentTime,
0.01,
);
}, [createEngine]);
const stopAll = useCallback(() => {
for (const source of sourcesRef.current) {
try {
source.stop();
} catch {
source.disconnect();
}
}
sourcesRef.current.clear();
}, []);
useEffect(() => {
return () => {
stopAll();
const engine = engineRef.current;
if (!engine) return;
engine.masterGain.disconnect();
engine.analyser.disconnect();
void engine.context.close();
engineRef.current = null;
};
}, [stopAll]);
return {
state,
resume,
playBuffer,
setVolume,
stopAll,
getEngine: () => engineRef.current,
};
}
Wichtig ist die neue AudioBufferSourceNode pro Wiedergabe. Nach start() darf sie nicht erneut gestartet werden. Genau diesen Punkt sollte Claude Code im Review prüfen.
Wellenform mit AnalyserNode zeichnen
Für den Einstieg reicht eine Oszilloskop-ähnliche Wellenform. Sie nutzt Zeitbereichsdaten statt Frequenzbalken und zeigt sofort, ob Audio durch den Graphen läuft.
import { useEffect, useRef } from "react";
type WaveformCanvasProps = {
analyser: AnalyserNode | null;
label?: string;
};
export function WaveformCanvas({
analyser,
label = "Audio waveform",
}: WaveformCanvasProps) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas || !analyser) return;
const canvasContext = canvas.getContext("2d");
if (!canvasContext) return;
const data = new Uint8Array(analyser.fftSize);
let frameId = 0;
const draw = () => {
const rect = canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
const width = Math.max(320, Math.floor(rect.width * dpr));
const height = Math.max(120, Math.floor(rect.height * dpr));
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
}
analyser.getByteTimeDomainData(data);
canvasContext.fillStyle = "#0f172a";
canvasContext.fillRect(0, 0, width, height);
canvasContext.lineWidth = Math.max(2, 2 * dpr);
canvasContext.strokeStyle = "#22c55e";
canvasContext.beginPath();
const sliceWidth = width / data.length;
for (let index = 0; index < data.length; index += 1) {
const value = data[index] / 128;
const y = (value * height) / 2;
const x = index * sliceWidth;
if (index === 0) canvasContext.moveTo(x, y);
else canvasContext.lineTo(x, y);
}
canvasContext.lineTo(width, height / 2);
canvasContext.stroke();
frameId = window.requestAnimationFrame(draw);
};
draw();
return () => window.cancelAnimationFrame(frameId);
}, [analyser]);
return (
<canvas
ref={canvasRef}
aria-label={label}
role="img"
style={{ width: "100%", height: "180px", display: "block" }}
/>
);
}
Die feste CSS-Höhe verhindert Layoutsprünge und horizontales Scrollen. Die interne Auflösung wird mit devicePixelRatio angepasst, damit die Linie auch auf Mobilgeräten scharf bleibt.
Aufnahmevorschau und Pegelmesser
Für die Aufnahmedatei ist MediaRecorder pragmatisch. Web Audio API übernimmt die Analyse des Eingangs. So bleibt der Code verständlich und der Mikrofoneingang wird nicht aus Versehen zur Ausgabe geleitet.
import { useCallback, useEffect, useRef, useState } from "react";
import type { AudioEngine } from "./useWebAudioEngine";
type RecorderPreviewProps = {
resume: () => Promise<AudioEngine | null>;
};
function chooseMimeType() {
const candidates = ["audio/webm;codecs=opus", "audio/webm", "audio/mp4"];
return candidates.find((type) => MediaRecorder.isTypeSupported(type)) ?? "";
}
export function RecorderPreview({ resume }: RecorderPreviewProps) {
const recorderRef = useRef<MediaRecorder | null>(null);
const streamRef = useRef<MediaStream | null>(null);
const sourceRef = useRef<MediaStreamAudioSourceNode | null>(null);
const analyserRef = useRef<AnalyserNode | null>(null);
const chunksRef = useRef<BlobPart[]>([]);
const frameRef = useRef<number | null>(null);
const [isRecording, setIsRecording] = useState(false);
const [level, setLevel] = useState(0);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const cleanupInput = useCallback(() => {
if (frameRef.current !== null) {
window.cancelAnimationFrame(frameRef.current);
frameRef.current = null;
}
sourceRef.current?.disconnect();
analyserRef.current?.disconnect();
streamRef.current?.getTracks().forEach((track) => track.stop());
sourceRef.current = null;
analyserRef.current = null;
streamRef.current = null;
recorderRef.current = null;
setLevel(0);
}, []);
const startMeter = useCallback((analyser: AnalyserNode) => {
const data = new Uint8Array(analyser.fftSize);
const tick = () => {
analyser.getByteTimeDomainData(data);
let sum = 0;
for (const value of data) {
const centered = (value - 128) / 128;
sum += centered * centered;
}
const rms = Math.sqrt(sum / data.length);
setLevel(Math.min(1, rms * 3));
frameRef.current = window.requestAnimationFrame(tick);
};
tick();
}, []);
const startRecording = useCallback(async () => {
try {
setError(null);
const engine = await resume();
if (!engine) throw new Error("AudioContext is not available.");
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const source = engine.context.createMediaStreamSource(stream);
const analyser = engine.context.createAnalyser();
analyser.fftSize = 1024;
source.connect(analyser);
const mimeType = chooseMimeType();
const recorder = new MediaRecorder(
stream,
mimeType ? { mimeType } : undefined,
);
chunksRef.current = [];
recorder.ondataavailable = (event) => {
if (event.data.size > 0) chunksRef.current.push(event.data);
};
recorder.onstop = () => {
const blob = new Blob(chunksRef.current, {
type: mimeType || "audio/webm",
});
const url = URL.createObjectURL(blob);
setPreviewUrl((oldUrl) => {
if (oldUrl) URL.revokeObjectURL(oldUrl);
return url;
});
chunksRef.current = [];
cleanupInput();
setIsRecording(false);
};
streamRef.current = stream;
sourceRef.current = source;
analyserRef.current = analyser;
recorderRef.current = recorder;
recorder.start();
startMeter(analyser);
setIsRecording(true);
} catch (recordingError) {
cleanupInput();
setIsRecording(false);
setError(recordingError instanceof Error ? recordingError.message : "Recording failed.");
}
}, [cleanupInput, resume, startMeter]);
const stopRecording = useCallback(() => {
const recorder = recorderRef.current;
if (recorder && recorder.state !== "inactive") recorder.stop();
else cleanupInput();
}, [cleanupInput]);
useEffect(() => {
return () => {
cleanupInput();
if (previewUrl) URL.revokeObjectURL(previewUrl);
};
}, [cleanupInput, previewUrl]);
return (
<section aria-label="Recorder preview">
<div>
<button type="button" onClick={startRecording} disabled={isRecording}>
Record
</button>
<button type="button" onClick={stopRecording} disabled={!isRecording}>
Stop
</button>
</div>
<meter min={0} max={1} value={level} aria-label="input level" />
{error && <p role="alert">{error}</p>}
{previewUrl && (
<audio controls src={previewUrl} aria-label="recording preview" />
)}
</section>
);
}
Dieses Beispiel hält die Aufnahme lokal. Wenn dein Produkt Audio hochlädt, brauchst du Einwilligung, Aufbewahrungsdauer, Löschverhalten und serverseitige Prüfung. Ein Sprachlernwerkzeug kann zunächst mit lokaler Vorschau starten.
Benachrichtigungston und Cleanup
Ein kurzer Ton kann mit OscillatorNode erzeugt werden. GainNode fügt einen kurzen Fade hinzu, damit kein harter Klick entsteht.
export async function playNotificationTone(context: AudioContext) {
if (context.state === "suspended") {
await context.resume();
}
const oscillator = context.createOscillator();
const gain = context.createGain();
const now = context.currentTime;
oscillator.type = "sine";
oscillator.frequency.setValueAtTime(880, now);
gain.gain.setValueAtTime(0.0001, now);
gain.gain.exponentialRampToValueAtTime(0.2, now + 0.02);
gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.18);
oscillator.connect(gain);
gain.connect(context.destination);
oscillator.start(now);
oscillator.stop(now + 0.2);
oscillator.onended = () => {
oscillator.disconnect();
gain.disconnect();
};
}
export function disconnectSafely(nodes: Array<AudioNode | null>) {
for (const node of nodes) {
try {
node?.disconnect();
} catch {
// The node may already be disconnected.
}
}
}
export function stopMediaStream(stream: MediaStream | null) {
stream?.getTracks().forEach((track) => track.stop());
}
Auch kleine Effekte brauchen Review. Sie sehen harmlos aus, erzeugen aber Nodes und müssen nach Ende oder Navigation getrennt werden.
Typische Fallstricke
Erstens: AudioContext vor einer User-Geste starten. Viele Browser blockieren Ton ohne Klick, Touch oder Tastaturaktion. Vermeide resume() im Mount-useEffect; nutze Play-, Record- oder Start-Buttons.
Zweitens: Autoplay-Blockierung nicht als UI-Zustand behandeln. Wenn context.state weiterhin suspended ist, muss die Oberfläche erklären, was der Nutzer tun soll. Stilles Scheitern wirkt wie ein Defekt.
Drittens: Ressourcen nicht freigeben. Prüfe AudioNode.disconnect(), MediaStreamTrack.stop(), cancelAnimationFrame(), URL.revokeObjectURL() und AudioContext.close(). Teste nach Navigation, nicht nur nach Stop.
Viertens: Aufnahme-Daten unterschätzen. Audio kann personenbezogen sein. Erkläre, ob es lokal bleibt, hochgeladen wird, wie lange es gespeichert wird und wie es gelöscht werden kann.
Fünftens: Mobile Latenz ignorieren. latencyHint: "interactive" hilft, aber Bluetooth, Energiesparmodus und Browserunterschiede bleiben. Für Rhythmusspiele oder Aussprachewertung muss die akzeptable Verzögerung vorher definiert werden.
Playwright und manuelle Prüfung
Playwright prüft UI-Fluss und DOM-Zustände. Echter Ton, Echo, Lautstärke und mobile Latenz brauchen echte Geräte.
import { chromium, expect, test } from "@playwright/test";
test("audio demo starts after a gesture and creates a recording preview", async () => {
const browser = await chromium.launch({
args: [
"--use-fake-device-for-media-stream",
"--use-fake-ui-for-media-stream",
],
});
const context = await browser.newContext({ permissions: ["microphone"] });
const page = await context.newPage();
await page.goto("http://localhost:5173/audio-demo");
await page.getByRole("button", { name: /start audio/i }).click();
await expect(page.getByTestId("audio-state")).toContainText(/running|ready/i);
await page.getByRole("button", { name: /record/i }).click();
await page.waitForTimeout(600);
await page.getByRole("button", { name: /stop/i }).click();
await expect(page.getByLabel("recording preview")).toBeVisible();
await browser.close();
});
Manuell solltest du Chrome, Safari, iOS Safari und Android Chrome prüfen. Wichtig sind erster Tap, Mikrofonablehnung, verschwindende Mikrofonanzeige nach Stop oder Navigation, keine horizontale Wellenform-Überbreite und sinnvolle Lautstärke mit Kopfhörern. Für Performance passt Claude Code Performance-Optimierung.
Review-Prompt für Claude Code
Nach der Implementierung sollte Claude Code als Reviewer arbeiten.
Reviewe diese React + TypeScript Web Audio API Implementierung.
Fokus:
- AudioContext wird nur nach User-Geste erstellt oder resumed.
- AudioBufferSourceNode wird nach start() nicht wiederverwendet.
- MediaStream tracks stoppen bei Aufnahmeende und Component unmount.
- AudioNodes, requestAnimationFrame und Blob URLs werden freigegeben.
- Mikrofoneingang ist nicht versehentlich mit destination verbunden.
- Autoplay, Mobile-Verhalten und verweigerte Berechtigungen haben UI-Zustände.
- Automatische und manuelle Prüfungen sind klar getrennt.
Gib Findings nach Schweregrad mit Datei, Zeile, Grund und Fix-Vorschlag zurück.
Die offiziellen Claude Code common workflows helfen, diese Reviewregeln im Team zu wiederholen. Lege sie in CLAUDE.md oder REVIEW.md ab.
ClaudeCodeLab CTA
Eine Web-Audio-Demo ist schnell gebaut, aber Produktion betrifft Datenschutz, Accessibility, Analytics, mobile Tests und Produktfluss. ClaudeCodeLab kann über Claude Code training and consultation bei Claude-Code-Regeln, Review-Prompts, Playwright-Prüfungen und Repository-spezifischen Audio-UI-Mustern helfen.
Für ein Einzelprojekt reichen zuerst Wellenform-Player und Aufnahmevorschau. Für ein Teamprojekt gehören Einwilligung, Datenaufbewahrung, Event-Tracking und Supportpfad ebenfalls in den Review.
Verifizierungsnotiz
Masa hat diese Struktur in einer kleinen React-Demo geprüft. AudioContext und Cleanup in useWebAudioEngine zu bündeln, machte spätere Erweiterungen wie Aufnahmevorschau, Pegelmesser und Benachrichtigungston deutlich leichter prüfbar. Die Version, in der jede Komponente selbst new AudioContext() aufrief, war fragiler: Nach Navigation blieb die Mikrofonanzeige manchmal aktiv, wiederholte Klicks machten Source-Zuständigkeit unklar und mobile Autoplay-Fehler waren schwer zu erklären. Die hilfreiche Claude-Code-Anweisung war nicht nur “baue Audio UI”, sondern “prüfe Verbindungs-Cleanup und Berechtigungsfehler”.
Kostenloses PDF: Claude-Code-Cheatsheet
E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.
Wir schützen Ihre Daten und senden keinen Spam.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude Code Permission Safety Ladder: Zugriff kontrolliert erweitern
Von read-only zu begrenzten Änderungen, Prüfbefehlen und Deploy-Checks mit klarer Kontrolle.
Claude Code Small PR Proof Pack: kleine Änderungen reviewbar machen
Ein Proof Pack für Claude-Code-PRs: Diff, Checks, öffentliche URL, CTA-Pfad und Rollback.
Claude-Code-Review-Gate vor dem Commit
Vor dem Commit mit Claude Code prüfen: Diff, Build, öffentliche URL, Gumroad-Links, Beratung-CTA, fehlende Tests und fremde Dateien.