Tips & Tricks (अपडेट: 2/6/2026)

Claude Code से Web Audio API: waveform, recording और volume meter

Claude Code से Web Audio API लागू करें: waveform, recording, volume meter, cleanup और tests.

Claude Code से Web Audio API: waveform, recording और volume meter

पहले audio experience तय करें

Web Audio API browser में audio graph बनाने की API है। इससे आप sound load कर सकते हैं, उसे nodes से जोड़ सकते हैं, volume बदल सकते हैं, waveform निकाल सकते हैं और output तक भेज सकते हैं। साधारण playback के लिए audio element काफी है, लेकिन waveform player, recording preview, input volume meter, notification sound, sound effects या voice learning tool के लिए AudioContext, GainNode, AnalyserNode और MediaRecorder की जरूरत पड़ती है।

Claude Code इसलिए उपयोगी है क्योंकि production audio सिर्फ एक snippet नहीं है। React app में SSR, autoplay restriction, microphone permission, cleanup, accessibility और mobile latency साथ आते हैं। अगर Claude Code आपके components, routes, tests और repository rules पढ़ता है, तो वह implementation के साथ review भी बेहतर कर सकता है।

इस article में Claude Code prompt, React/TypeScript AudioContext hook, AnalyserNode waveform, recording preview, volume meter, notification tone, cleanup, Playwright test और manual verification शामिल हैं। Official reference के लिए MDN Web Audio API, MDN autoplay guide और MediaRecorder देखें। Complete player UI के लिए internal article Claude Code audio player guide भी पढ़ें।

पांच practical use cases

Implementation से पहले use case लिखें। हर use case में nodes और risks अलग होते हैं।

Use caseMain APIsClaude Code क्या बनाएमुख्य risk
Waveform playerAudioContext, AudioBufferSourceNode, AnalyserNodeplay, stop, waveform, master volumestart() के बाद source reuse करना
Recording previewgetUserMedia, MediaRecorder, MediaStreamAudioSourceNoderecord, stop, local Blob URL, previewmicrophone tracks active छोड़ना
Volume meterAnalyserNode.getByteTimeDomainDataRMS calculation और live meterrequestAnimationFrame बंद न करना
Notification soundOscillatorNode, GainNodeshort tone, fade, click preventionuser gesture से पहले sound बजाना
Voice learning toolplayback hook, recorder, loop controlsshadowing, pronunciation practiceconsent और retention unclear रहना

यह table Claude Code को सही boundary देता है। Waveform player को microphone permission नहीं चाहिए। Recording preview में microphone को speaker output से connect नहीं करना चाहिए। Notification tone छोटा, कम volume वाला और end के बाद disconnected होना चाहिए।

Claude Code को देने वाला prompt

Prompt में user gesture, local recording, cleanup और verification साफ लिखें। यह Claude Code को demo से आगे production habit की तरफ ले जाता है।

React + TypeScript में Web Audio API demo implement करें।

Requirements:
- AudioContext केवल user click या tap के बाद create या resume हो।
- useWebAudioEngine hook बनाएं जो master gain, analyser, playback और cleanup संभाले।
- waveform player, recording preview, input volume meter और notification tone implement करें।
- Recorded audio को local Blob URL तक रखें; server पर upload न करें।
- Stop और unmount पर MediaStream tracks रोकें और AudioNodes disconnect करें।
- requestAnimationFrame cancel करें और Blob URLs revoke करें।
- autoplay blocked, microphone denied और unsupported browser states UI में दिखाएं।

Edit करने से पहले existing components, lint rules और test setup पढ़ें।
Implementation के बाद Playwright या manual verification notes जोड़ें।

Web Audio API में problem अक्सर first click पर नहीं दिखती। दूसरी बार play करने पर source fail हो सकता है, route change के बाद mic indicator बच सकता है, या mobile Safari context को suspended रख सकता है।

React/TypeScript AudioContext hook

सबसे पहले audio graph को hook में रखें। AudioContext entry point है, GainNode volume control करता है और AnalyserNode waveform या meter data देता है।

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,
  };
}

मुख्य बात यह है कि हर playback के लिए नया AudioBufferSourceNode बनता है। start() के बाद इसे reuse नहीं किया जा सकता। Claude Code review में यह check जरूर रखें।

AnalyserNode से waveform बनाएं

सबसे सरल visualizer time-domain waveform है। यह frequency bars नहीं दिखाता, बल्कि audio amplitude की line बनाता है। इससे तुरंत दिखता है कि audio graph में signal आ रहा है या नहीं।

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" }}
    />
  );
}

Fixed height layout shift और horizontal scroll को रोकती है। devicePixelRatio से canvas sharp रहता है, खासकर mobile screens पर।

Recording preview और volume meter

Recording file के लिए MediaRecorder इस्तेमाल करें और live meter के लिए Web Audio API। इससे code साफ रहता है और microphone input गलती से speaker output में नहीं जाता।

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>
  );
}

यह example recording को local रखता है। अगर product upload करता है, तो consent text, retention, delete flow और server validation जरूर जोड़ें।

Notification sound और cleanup helpers

Short notification के लिए audio file जरूरी नहीं। OscillatorNode tone बनाता है और GainNode fade देता है ताकि click noise न आए।

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());
}

छोटे effects भी nodes बनाते हैं। Claude Code से पूछें कि onended में disconnect है या नहीं और navigation के बाद resources बचे हैं या नहीं।

Common pitfalls

पहला pitfall है user gesture से पहले AudioContext शुरू करना। कई browsers click, tap या key action के बिना sound block करते हैं। useEffect में page load पर resume() न करें।

दूसरा pitfall है autoplay block को UI state न मानना। अगर context.state अभी भी suspended है, तो user को बताएं कि sound शुरू करने के लिए tap करना होगा।

तीसरा pitfall है resource leak। AudioNode.disconnect(), MediaStreamTrack.stop(), cancelAnimationFrame(), URL.revokeObjectURL() और AudioContext.close() को stop और unmount पर check करें।

चौथा pitfall है recording privacy। Recording personal data हो सकती है। साफ बताएं कि audio local रहेगा या upload होगा, कितने समय तक रखा जाएगा और delete कैसे होगा।

पांचवां pitfall है mobile latency। latencyHint: "interactive" मदद करता है, लेकिन Bluetooth, battery saver और browser differences अभी भी असर डालते हैं। Rhythm game या pronunciation scoring में acceptable delay पहले तय करें।

Playwright और manual verification

Playwright UI flow verify कर सकता है। Real sound, echo, volume और mobile delay के लिए real devices चाहिए।

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();
});

Manual check में Chrome, Safari, iOS Safari और Android Chrome शामिल करें। देखें कि first tap से audio शुरू होता है या नहीं, mic deny पर useful message आता है या नहीं, stop या navigation के बाद mic indicator हटता है या नहीं, waveform horizontal scroll नहीं बनाता और headphones पर notification sound बहुत तेज नहीं है। Performance के लिए Claude Code performance optimization देखें।

Claude Code review prompt

Implementation के बाद Claude Code को reviewer की तरह चलाएं।

इस React + TypeScript Web Audio API implementation को review करें।

Focus:
- AudioContext केवल user gesture के बाद create या resume होता है।
- AudioBufferSourceNode को start() के बाद reuse नहीं किया गया।
- Recording stop और component unmount पर MediaStream tracks stop होते हैं।
- AudioNodes, requestAnimationFrame और Blob URLs release होते हैं।
- Microphone input गलती से destination से connect नहीं है।
- Autoplay, mobile और permission denied states UI में मौजूद हैं।
- Automated checks और manual checks अलग-अलग लिखे हैं।

Severity order में file, line, reason और fix suggestion लौटाएं।

Official Claude Code common workflows team workflow बनाने में मदद करते हैं। इन audio review rules को CLAUDE.md या REVIEW.md में रखना बेहतर है।

ClaudeCodeLab CTA

Web Audio API demo जल्दी बन जाता है, लेकिन production feature privacy, accessibility, analytics, mobile testing और product flow से जुड़ता है। ClaudeCodeLab Claude Code training and consultation में Claude Code rules, review prompts, Playwright checks और repository-specific audio UI patterns व्यवस्थित करने में मदद कर सकता है।

Solo project में पहले waveform player और recording preview बनाएं। Team project में consent copy, recording data policy, analytics events और support path भी review करें।

Verification memo

Masa ने इस structure को छोटे React demo में test किया। AudioContext और cleanup को useWebAudioEngine में centralize करने से recording preview, volume meter और notification tone जोड़ना आसान रहा। जिस version में हर component अपना new AudioContext() बना रहा था, उसमें route change के बाद mic indicator बच सकता था और repeated play clicks से source ownership unclear हो रही थी। सबसे useful Claude Code instruction था: “connection cleanup और permission errors review करो,” सिर्फ “audio UI बनाओ” नहीं।

#Claude Code #Web Audio API #audio processing #visualizer #TypeScript
मुफ़्त

मुफ़्त PDF: Claude Code cheatsheet

Email डालें और commands, review habits तथा safe workflow वाली एक-page PDF पाएँ.

हम आपका data सुरक्षित रखते हैं और spam नहीं भेजते.

Masa

लेखक के बारे में

Masa

Claude Code workflow और team adoption पर काम करने वाला engineer.