# `useAudioRecorder` Hook Documentation

**Version:** 1.0.0 (Based on the provided JavaScript code)

## Overview

The `useAudioRecorder` is a custom React hook designed to provide headless audio recording functionality. It encapsulates all the logic for interacting with the browser's MediaStream API (`getUserMedia`), `MediaRecorder` API, and `AudioContext` for processing and converting audio to WAV format.

By being "headless," this hook does not render any UI itself. Instead, it provides the state, data, and action functions necessary for you to build your own custom audio recorder UI components. This offers maximum flexibility in how the recorder looks and behaves within your application.

## Features

- **Microphone Access:** Handles requesting and managing microphone permissions.
- **Recording Control:** Provides functions to start, pause, resume, and stop recording.
- **State Management:** Exposes the current state of the recorder (e.g., 'idle', 'recording', 'paused', 'finished', 'permissionDenied').
- **Timer:** Tracks and exposes the duration of the current recording.
- **Audio Output:**
  - Provides the recorded audio as raw chunks (`Blob[]`).
  - Converts the recording to a WAV `Blob` (`finalWavBlob`).
  - Generates an `audioURL` (Object URL) for easy playback of the final recording (WAV or raw if WAV conversion fails).
- **Live Stream Access:** Exposes the live `MediaStream` and `AudioContext` via a callback (`onStreamReady`) for integrations like live waveform visualization (e.g., with WaveSurfer.js).
- **Error Handling:** Provides error messages for common issues.
- **Callbacks:** Offers various callbacks for state changes, timer updates, recording completion, errors, and stream readiness.
- **Configuration:** Allows basic configuration like MIME type for recording.

## Installation & Setup

1.  **Dependencies:**

    - `react`: Ensure you have React installed in your project.
    - `lucide-react` (Optional, for icons): The example `App` component uses `lucide-react` for icons. If you build your own UI, you can use any icon library or none.

2.  **Add the Hook to Your Project:**
    Copy the `useAudioRecorder` function and its helper functions (`audioBufferToWav`, `encodeWAV`, `floatTo16BitPCM`, `interleave`, `writeString`) into your React project (e.g., in a `hooks/useAudioRecorder.js` file).

## How to Use

Import the hook into your component and call it. It will return an object containing the recorder's state, data, and action functions.

```javascript
import React, { useEffect } from "react";
import { useAudioRecorder } from "./path/to/useAudioRecorder"; // Adjust path as needed
import {
  Mic,
  StopCircle,
  Play,
  Pause,
  Download,
  AlertTriangle,
  CheckCircle,
  RefreshCw,
} from "lucide-react"; // Example icons

function MyAudioRecorderComponent() {
  const {
    recordingState,
    duration,
    finalWavBlob,
    audioURL,
    errorMessage,
    isMicrophoneAvailable,
    actions, // Contains start, pause, resume, stop, requestPermission, reset
  } = useAudioRecorder({
    // Optional props/callbacks
    onStateChange: (payload) => console.log(`New state: ${payload.newState}`),
    onRecordingComplete: (payload) => {
      if (payload.wavBlob) {
        console.log("WAV recording finished:", payload.wavBlob);
      } else {
        console.log(
          "Raw recording finished (WAV conversion failed):",
          payload.audioChunks
        );
      }
    },
    onStreamReady: ({ stream, audioContext }) => {
      console.log(
        "Live stream and AudioContext are ready:",
        stream,
        audioContext
      );
      // Ideal place to integrate with WaveSurfer.js or other live visualizers
    },
    onError: (error) => console.error("Recorder error:", error.message),
  });

  // Cleanup for audioURL when component unmounts or audioURL changes
  useEffect(() => {
    return () => {
      if (audioURL) {
        URL.revokeObjectURL(audioURL);
      }
    };
  }, [audioURL]);

  const formatDurationForDisplay = (seconds) => {
    const minutes = Math.floor(seconds / 60);
    const remainingSeconds = seconds % 60;
    return `${String(minutes).padStart(2, "0")}:${String(
      remainingSeconds
    ).padStart(2, "0")}`;
  };

  // Build your UI using the returned state and actions
  return (
    <div className="my-recorder-ui p-4 border rounded-lg">
      <p>State: {recordingState}</p>
      <p>Duration: {formatDurationForDisplay(duration)}</p>

      {!isMicrophoneAvailable && (
        <p className="text-red-500">Microphone not available.</p>
      )}
      {errorMessage && <p className="text-red-500">Error: {errorMessage}</p>}

      {isMicrophoneAvailable && recordingState === "permissionNeeded" && (
        <button
          onClick={actions.requestPermission}
          className="bg-yellow-500 p-2 rounded"
        >
          <CheckCircle size={16} className="inline mr-1" /> Grant Permission
        </button>
      )}

      {recordingState === "permissionDenied" && (
        <div>
          <p className="text-red-500">
            Permission Denied. Please enable in browser settings.
          </p>
          <button
            onClick={actions.reset}
            className="bg-gray-500 p-2 rounded mt-1"
          >
            <RefreshCw size={16} className="inline mr-1" /> Reset
          </button>
        </div>
      )}

      {["ready", "recording", "paused", "finished"].includes(
        recordingState
      ) && (
        <div className="controls mt-2 space-x-2">
          {recordingState !== "recording" && recordingState !== "paused" && (
            <button
              onClick={actions.start}
              disabled={recordingState === "permissionDenied"}
              className="bg-blue-500 p-2 rounded"
            >
              <Mic size={16} className="inline mr-1" /> Start
            </button>
          )}
          {recordingState === "recording" && (
            <button
              onClick={actions.pause}
              className="bg-yellow-500 p-2 rounded"
            >
              <Pause size={16} className="inline mr-1" /> Pause
            </button>
          )}
          {recordingState === "paused" && (
            <button
              onClick={actions.resume}
              className="bg-green-500 p-2 rounded"
            >
              <Play size={16} className="inline mr-1" /> Resume
            </button>
          )}
          {(recordingState === "recording" || recordingState === "paused") && (
            <button onClick={actions.stop} className="bg-red-500 p-2 rounded">
              <StopCircle size={16} className="inline mr-1" /> Stop
            </button>
          )}
        </div>
      )}

      {(recordingState === "ready" || recordingState === "finished") &&
        recordingState !== "recording" &&
        recordingState !== "paused" && (
          <button
            onClick={actions.reset}
            className="bg-gray-500 p-2 rounded mt-2"
          >
            <RefreshCw size={16} className="inline mr-1" /> Reset Recorder
          </button>
        )}

      {audioURL && recordingState === "finished" && (
        <div className="playback mt-4">
          <h4>Playback:</h4>
          <audio controls src={audioURL}></audio>
          <a
            href={audioURL}
            download={`recording-${new Date().toISOString().slice(0, 10)}.${
              finalWavBlob ? "wav" : "webm"
            }`}
            className="block mt-2 text-blue-500 hover:underline"
          >
            <Download size={16} className="inline mr-1" /> Download{" "}
            {finalWavBlob ? "WAV" : "Raw Audio"}
          </a>
        </div>
      )}
    </div>
  );
}

export default MyAudioRecorderComponent;
```

## API Reference

### Hook Props (`props`)

The `useAudioRecorder` hook accepts an optional `props` object with the following properties:

- **`onStateChange`**: `(payload: { newState: string, oldState: string }) => void`
  - Callback function invoked when the `recordingState` changes.
    - `payload.newState`: The new state.
    - `payload.oldState`: The previous state.
- **`onTimerUpdate`**: `(currentTime: number) => void`
  - Callback function invoked every second while recording, providing the current recording duration in seconds.
- **`onRecordingComplete`**: `(payload: { wavBlob: Blob | null, audioChunks: Blob[], duration: number }) => void`
  - Callback function invoked when a recording is stopped and processed.
    - `payload.wavBlob`: The final recording as a WAV `Blob`. Can be `null` if WAV conversion fails.
    - `payload.audioChunks`: An array of raw `Blob` chunks as recorded by `MediaRecorder`.
    - `payload.duration`: The total duration of the recording in seconds.
- **`onError`**: `(error: { message: string, details?: any }) => void`
  - Callback function invoked when an error occurs within the hook.
    - `error.message`: A string describing the error.
    - `error.details`: Optional additional details about the error.
- **`onStreamReady`**: `(payload: { stream: MediaStream, audioContext: AudioContext }) => void`
  - Callback function invoked when the microphone `MediaStream` and `AudioContext` are successfully obtained and ready, just before `MediaRecorder` starts. This is useful for live processing or visualization.
- **`config`**: `object`
  - An optional configuration object for `MediaRecorder`.
    - `config.mimeType`: `string` (e.g., `'audio/webm;codecs=opus'`, `'audio/ogg;codecs=opus'`). Defaults to `'audio/webm'`.
    - `config.audioBitsPerSecond`: `number` (e.g., `128000`). Specifies the audio bitrate.

### Return Value

The `useAudioRecorder` hook returns an object with the following properties:

- **`recordingState`**: `string`
  - The current state of the recorder. Possible values:
    - `'idle'`: Initial state or after a full reset before permission checks.
    - `'permissionNeeded'`: Microphone permission has not been granted yet and is required.
    - `'requestingPermission'`: Actively requesting microphone permission.
    - `'permissionDenied'`: Microphone permission was denied by the user or browser.
    - `'ready'`: Permission granted, ready to start recording.
    - `'recording'`: Actively recording audio.
    - `'paused'`: Recording is paused.
    - `'stopping'`: Recording is in the process of stopping.
    - `'processing'`: Recorded audio is being processed (e.g., converting to WAV).
    - `'finished'`: Recording and processing are complete. The audio is available.
- **`duration`**: `number`
  - The current duration of the recording in seconds. Resets when a new recording starts.
- **`audioChunks`**: `Blob[]`
  - An array of raw `Blob` objects representing the chunks of the last completed recording.
- **`finalWavBlob`**: `Blob | null`
  - The `Blob` object for the final WAV audio of the last completed recording. `null` if WAV conversion failed or no recording is complete.
- **`audioURL`**: `string`
  - An object URL (`blob:...`) created from `finalWavBlob` (or the raw `recordedBlob` if WAV conversion fails). This URL can be used as the `src` for an `<audio>` element for playback.
  - **Important:** Remember to call `URL.revokeObjectURL(audioURL)` when the component unmounts or the `audioURL` is no longer needed to free up memory.
- **`errorMessage`**: `string | null`
  - A message describing the last error that occurred, or `null` if no error.
- **`isMicrophoneAvailable`**: `boolean`
  - Indicates whether the `navigator.mediaDevices.getUserMedia` API is available in the browser.
- **`actions`**: `object`
  - An object containing functions to control the recorder:
    - **`start()`**: `async () => Promise<void>`
      - Requests permission if needed, then starts the recording.
      - Triggers `onStreamReady` callback if provided.
    - **`pause()`**: `() => void`
      - Pauses the current recording if it is active.
    - **`resume()`**: `() => void`
      - Resumes a paused recording.
    - **`stop()`**: `async () => Promise<void>`
      - Stops the current recording and initiates processing (e.g., WAV conversion).
      - Triggers `onRecordingComplete` when done.
    - **`requestPermission()`**: `async () => Promise<boolean>`
      - Manually triggers the microphone permission request flow. Returns `true` if permission is granted, `false` otherwise.
    - **`reset()`**: `() => void`
      - Stops any active recording, clears all recorded data and states, releases the microphone stream, and re-evaluates permission status. Sets state to `'permissionNeeded'` or `'permissionDenied'` based on current browser permissions.

## WAV Conversion Internals

The hook includes utility functions (`audioBufferToWav`, `encodeWAV`, etc.) to convert the audio recorded by `MediaRecorder` (typically in a format like WebM with Opus codec) into a standard WAV (PCM) format. This is done client-side using the `AudioContext` to decode the initial recording and then re-encoding it as WAV.

If the `AudioContext` is unavailable or the decoding/encoding process fails, the `finalWavBlob` will be `null`, but the `audioURL` will still be populated with the raw recorded audio (e.g., the WebM file), and `audioChunks` will contain the raw data.

## Browser Compatibility

- **MediaRecorder API:** Check browser compatibility for `MediaRecorder`. Most modern browsers support it.
- **getUserMedia (Microphone):** Widely supported, but always requires user permission.
- **AudioContext:** Widely supported.
- **Permissions API (`navigator.permissions`):** Used for a better UX in checking permission status. If not available, the hook will gracefully degrade but might require more explicit user action to grant permission.

Ensure your application is served over **HTTPS** (or `localhost` during development), as `getUserMedia` typically requires a secure context.
