Web Audio API avec Claude Code : formes d'onde, enregistrement et volume
Implémentez Web Audio API avec Claude Code: onde, enregistrement, vumètre, cleanup et tests en React/TypeScript.
Commencer par l’expérience audio
La Web Audio API sert à construire un graphe audio dans le navigateur. On charge un son, on le connecte à des nœuds, on ajuste son volume, on l’analyse, puis on l’envoie vers la sortie. Pour une lecture simple, l’élément audio suffit souvent. La Web Audio API devient utile quand il faut un lecteur avec forme d’onde, un aperçu d’enregistrement, un vumètre d’entrée, un son de notification, des effets ou un outil d’apprentissage vocal.
Claude Code est utile parce que le vrai problème n’est pas de générer un extrait isolé. Le vrai problème est d’intégrer l’audio dans une application React existante sans casser le rendu côté serveur, les règles d’autoplay, les permissions micro, la libération des nœuds, l’accessibilité et le comportement mobile. Si Claude Code lit vos composants, routes, tests et règles de dépôt, il peut produire un code plus proche de la production.
Ce guide propose un flux complet : prompt pour Claude Code, hook AudioContext en React/TypeScript, forme d’onde avec AnalyserNode, aperçu d’enregistrement, vumètre, son de notification, nettoyage, test Playwright et vérification manuelle. Gardez sous la main les références officielles MDN Web Audio API, guide autoplay MDN et MediaRecorder. Pour l’interface de lecteur complète, consultez aussi construire un lecteur audio avec Claude Code.
Cinq cas d’usage concrets
Avant de coder, nommez le cas d’usage. Chaque cas demande un graphe différent et donc une revue différente.
| Cas d’usage | API principales | Ce que Claude Code doit produire | Risque principal |
|---|---|---|---|
| Lecteur avec onde | AudioContext, AudioBufferSourceNode, AnalyserNode | lecture, arrêt, onde, volume maître | réutiliser un source après start() |
| Aperçu d’enregistrement | getUserMedia, MediaRecorder, MediaStreamAudioSourceNode | enregistrer, arrêter, Blob URL local, écoute | laisser les tracks micro actives |
| Vumètre | AnalyserNode.getByteTimeDomainData | calcul RMS et affichage en direct | ne pas arrêter requestAnimationFrame |
| Notifications et effets | OscillatorNode, GainNode | sons courts, fade, prévention des clics | jouer avant un geste utilisateur |
| Outil d’apprentissage vocal | hook de lecture, enregistrement, boucle | répétition, comparaison, révision | règles de consentement et conservation floues |
Cette étape évite de demander une solution trop large. Un lecteur avec onde n’a pas besoin du micro. Un aperçu d’enregistrement ne doit pas renvoyer le micro vers les haut-parleurs. Un son de notification doit être court, discret par défaut et déconnecté à la fin.
Prompt pour Claude Code
Le prompt doit fixer les limites : geste utilisateur, portée de l’enregistrement, cleanup et vérification. Il doit aussi demander à Claude Code de lire le projet avant de modifier.
Implémente une démo Web Audio API en React + TypeScript.
Exigences:
- Créer ou reprendre AudioContext uniquement après un geste utilisateur.
- Ajouter un hook useWebAudioEngine pour master gain, analyser, playback et cleanup.
- Implémenter lecteur avec onde, aperçu d'enregistrement, vumètre d'entrée et son de notification.
- Garder l'audio enregistré en Blob URL local; ne pas l'envoyer au serveur.
- Arrêter les MediaStream tracks et déconnecter les AudioNodes à l'arrêt et au démontage.
- Annuler requestAnimationFrame et révoquer les Blob URLs.
- Afficher des états pour autoplay bloqué, refus micro et navigateur non supporté.
Avant d'éditer, inspecte les composants existants, les règles lint et les tests.
Après l'implémentation, ajoute des notes de vérification Playwright ou manuelles.
Ce niveau de détail change le résultat. Les bugs Web Audio API n’apparaissent pas toujours au premier clic. Ils apparaissent souvent au deuxième lancement, pendant la navigation, après un refus de permission, ou sur mobile quand le contexte reste suspended.
Hook React/TypeScript pour AudioContext
Commencez par isoler le graphe audio dans un hook. AudioContext est l’entrée, GainNode règle le volume et AnalyserNode fournit les données de visualisation. Un hook centralisé rend la libération des ressources beaucoup plus vérifiable.
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,
};
}
Le point essentiel est de créer un nouveau AudioBufferSourceNode à chaque lecture. Après start(), ce nœud n’est pas réutilisable. Demandez à Claude Code de vérifier ce point explicitement.
Dessiner une forme d’onde
Le visualiseur le plus simple utilise les données temporelles de AnalyserNode. Il ne montre pas les fréquences, mais l’amplitude dans le temps. C’est idéal pour un premier lecteur, car on voit immédiatement si le son traverse le graphe.
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" }}
/>
);
}
La hauteur fixe évite les sauts de mise en page et les débordements horizontaux. La résolution interne est recalculée avec devicePixelRatio, ce qui garde une ligne nette sur mobile.
Aperçu d’enregistrement et vumètre
Pour produire le fichier enregistré, utilisez MediaRecorder. Pour mesurer l’entrée, utilisez Web Audio API. Cette séparation évite aussi de connecter par erreur le micro vers la sortie audio.
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>
);
}
Cet exemple garde l’enregistrement local. Si votre produit l’envoie au serveur, ajoutez consentement, durée de conservation, suppression et validation serveur. Pour un outil de prononciation, l’aperçu local suffit souvent à la première version.
Son de notification et helpers de nettoyage
Un son court peut être généré avec OscillatorNode. GainNode permet un fade très bref pour éviter les clics.
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());
}
Demandez aussi une revue des petits effets sonores. Ils semblent secondaires, mais ils créent des nœuds et peuvent rester actifs après plusieurs navigations.
Pièges à éviter
Premier piège : démarrer AudioContext avant un geste utilisateur. Beaucoup de navigateurs bloquent l’audio lancé sans clic, toucher ou clavier. N’appelez pas resume() dans un useEffect de montage ; faites-le dans le bouton Play, Record ou Start.
Deuxième piège : ignorer l’état d’autoplay bloqué. Si le contexte reste suspended, l’interface doit expliquer l’action attendue. Un message clair vaut mieux qu’un silence qui ressemble à un bug.
Troisième piège : oublier les ressources. Vérifiez AudioNode.disconnect(), MediaStreamTrack.stop(), cancelAnimationFrame(), URL.revokeObjectURL() et AudioContext.close(). Testez après navigation, pas seulement après le bouton Stop.
Quatrième piège : traiter l’enregistrement comme un champ ordinaire. L’audio peut contenir des données personnelles. Dites s’il reste local, s’il est envoyé, combien de temps il est conservé et comment il est supprimé.
Cinquième piège : sous-estimer la latence mobile. latencyHint: "interactive" aide, mais les écouteurs Bluetooth, l’économie d’énergie et les différences de navigateur restent visibles. Pour un jeu rythmique ou un score de prononciation, définissez la latence acceptable avant de peaufiner l’onde.
Vérification Playwright et manuelle
Playwright vérifie le parcours et l’état DOM. Le vrai son, le volume, l’écho et la latence doivent être testés sur appareils réels.
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();
});
La vérification manuelle doit couvrir Chrome, Safari, iOS Safari et Android Chrome. Contrôlez le premier toucher, le refus du micro, la disparition de l’indicateur micro après arrêt ou navigation, l’absence de scroll horizontal dans l’onde et le volume du son de notification au casque. Pour la vitesse, reliez ce travail à l’optimisation des performances avec Claude Code.
Prompt de revue pour Claude Code
Après l’implémentation, utilisez Claude Code comme reviewer.
Revois cette implémentation Web Audio API en React + TypeScript.
Points à vérifier:
- AudioContext est créé ou repris seulement après un geste utilisateur.
- AudioBufferSourceNode n'est pas réutilisé après start().
- Les MediaStream tracks s'arrêtent à l'arrêt de l'enregistrement et au démontage.
- AudioNodes, requestAnimationFrame et Blob URLs sont libérés.
- L'entrée micro n'est pas connectée accidentellement à destination.
- Autoplay, mobile et refus de permission ont des états UI.
- Les contrôles automatiques et manuels sont séparés clairement.
Retourne les problèmes par gravité avec fichier, ligne, raison et correction proposée.
Les workflows communs de Claude Code aident à transformer cette revue en pratique d’équipe. Placez ces règles dans CLAUDE.md ou REVIEW.md pour les conserver.
CTA ClaudeCodeLab
Une démo Web Audio API se construit vite, mais une fonctionnalité de production touche la confidentialité, l’accessibilité, l’analytics, les tests mobiles et le parcours produit. ClaudeCodeLab peut aider à définir les règles Claude Code, les prompts de revue, les tests Playwright et les patterns audio propres à votre dépôt via Claude Code training and consultation.
Pour un projet personnel, commencez par le lecteur avec onde et l’aperçu d’enregistrement. Pour une équipe, revoyez aussi le consentement, la politique de conservation, les événements analytics et le support utilisateur.
Note de vérification
Masa a testé cette structure dans une petite démo React. Centraliser AudioContext et cleanup dans useWebAudioEngine a facilité l’ajout de l’aperçu d’enregistrement, du vumètre et du son de notification. La version où chaque composant faisait new AudioContext() était plus fragile : l’indicateur micro restait parfois actif après navigation, les clics répétés rendaient la propriété des sources confuse et les échecs d’autoplay mobile étaient difficiles à expliquer. La consigne utile pour Claude Code n’était pas seulement “crée une UI audio”, mais “revois la libération des connexions et les erreurs de permission”.
PDF gratuit: cheatsheet Claude Code
Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.
Nous protégeons vos données et n'envoyons pas de spam.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Échelle de sécurité des permissions Claude Code
Passer du read-only aux éditions limitées, preuves et checks de déploiement sans perdre le contrôle.
Claude Code Small PR Proof Pack : rendre les petits changements reviewables
Un pack de preuve pour PR Claude Code : diff, vérifications, URL publique, CTA et rollback.
Gate de review avant commit avec Claude Code
Review avant commit avec Claude Code : diff, build, URL publique, liens Gumroad, CTA consultation, tests manquants et fichiers hors scope.