UNPKG

@audiowave/react

Version:

React audio visualization component

720 lines (552 loc) 22.3 kB
# @audiowave/react **Lightweight React components for real-time audio visualization** ## Inspiration and Key Differences This project is inspired by [react-audio-visualizer](https://github.com/Wolfy64/react-audio-visualize) but takes a different, more focused approach: **What makes us different:** - **Visualization only** - No built-in playback or recording functionality - **No control buttons** - Pure visualization components without UI controls - **Lightweight** - Minimal bundle size with focused feature set - **Easy integration** - Drop into existing audio applications without conflicts - **Flexible** - Works with any audio source you provide **Perfect for these scenarios:** - **Existing audio apps** - Add visualization to apps that already handle audio - **Recording software** - Visualize microphone input without audio conflicts - **Music players** - Add waveforms to audio/video playback - **Custom audio workflows** - Integrate with any audio source or processing pipeline - **Lightweight projects** - Minimal bundle size when you only need visualization ## Installation ```bash npm install @audiowave/react ``` ## Quick Start **Basic usage with microphone input:** ```tsx import { AudioWave, useMediaAudio } from '@audiowave/react'; import { useRef, useState, useMemo, useCallback } from 'react'; export default function App() { const [mediaStream, setMediaStream] = useState<MediaStream | null>(null); const [isRecording, setIsRecording] = useState(false); // AudioWave handles visualization, you control the audio source // Note: For dynamic audio sources, memoize the options object (see Important Usage Notes) const handleError = useCallback((error: Error) => { console.error('Audio error:', error); }, []); const audioOptions = useMemo(() => ({ source: mediaStream, onError: handleError, }), [mediaStream, handleError]); const { source, error } = useMediaAudio(audioOptions); const audioWaveRef = useRef<AudioWaveController>(null); const startRecording = async () => { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); setMediaStream(stream); setIsRecording(true); }; const stopRecording = () => { mediaStream?.getTracks().forEach(track => track.stop()); setMediaStream(null); setIsRecording(false); }; return ( <div> {error && <div>Error: {error.message}</div>} <AudioWave ref={audioWaveRef} source={source} height={100} /> {/* You provide the controls - AudioWave just visualizes */} <button onClick={isRecording ? stopRecording : startRecording}> {isRecording ? 'Stop Recording' : 'Start Recording'} </button> {/* Optional: Control visualization display */} <button onClick={() => audioWaveRef.current?.pause()}>Pause Waveform</button> <button onClick={() => audioWaveRef.current?.resume()}>Resume Waveform</button> <button onClick={() => audioWaveRef.current?.clear()}>Clear Waveform</button> </div> ); } ``` **Works with any audio source:** ```tsx // With audio file const audioElement = useRef<HTMLAudioElement>(null); const { source } = useAudioSource({ source: audioElement.current }); // With video file const videoElement = useRef<HTMLVideoElement>(null); const { source } = useAudioSource({ source: videoElement.current }); // With Web Audio API nodes const { source } = useAudioSource({ source: audioNode }); // With custom audio data (Electron, Node.js, etc.) const { source } = useCustomAudio(); ``` ## API Reference ### AudioWave Component The main visualization component - pure display, no audio control. ```tsx <AudioWave // === AUDIO SOURCE === source={source} // AudioSource from useAudioSource // === DIMENSIONS === width="100%" // Component width (string | number) height={200} // Component height in pixels // === VISUAL STYLING === backgroundColor="transparent" // Background color barColor="#ffffff" // Primary bar color secondaryBarColor="#5e5e5e" // Secondary/inactive bar color barWidth={2} // Width of each frequency bar gap={1} // Gap between bars in pixels rounded={0} // Border radius for rounded bars // === BORDER STYLING === showBorder={false} // Show border around visualization borderColor="#333333" // Border color borderWidth={1} // Border width in pixels borderRadius={0} // Border radius for rounded corners // === AMPLITUDE CALCULATION === amplitudeMode="peak" // Amplitude calculation: 'peak' | 'rms' | 'adaptive' // === ANIMATION & RENDERING === speed={3} // Animation speed (1-6, higher = slower) animateCurrentPick={true} // Enable smooth bar transitions fullscreen={false} // Fill entire parent container onlyActive={false} // Show visualization only when active gain={1.0} // Audio gain multiplier (0.1-10.0, default: 1.0) // === STATE CONTROL === isPaused={false} // Pause visualization (freeze display) // === ADVANCED CUSTOMIZATION === customRenderer={(context) => { // Custom rendering function for advanced visualizations // context: { canvas, audioData, width, height, ... } }} // === PLACEHOLDER CONTENT === placeholder={<div>No audio source</div>} // Custom placeholder content showPlaceholderBackground={true} // Show background in placeholder state // === CSS CLASSES === className="my-waveform" // CSS class for main container canvasClassName="my-canvas" // CSS class for canvas element // === CALLBACKS === onStateChange={(state) => console.log('State:', state)} // State change callback onRenderStart={() => console.log('Rendering started')} // Render start callback onRenderStop={() => console.log('Rendering stopped')} // Render stop callback onError={(error) => console.error('Error:', error)} // Error callback /> ``` #### Essential Props **Audio Source:** - `source` - AudioSource from `useMediaAudio` or `useCustomAudio` hook (required for visualization) **Dimensions:** - `width` - Component width, supports CSS units and numbers (default: `"100%"`) - `height` - Component height in pixels (default: `200`) **Visual Styling:** - `backgroundColor` - Background color (default: `"transparent"`) - `barColor` - Primary color for active audio bars (default: `"#ffffff"`) - `secondaryBarColor` - Color for inactive/past bars (default: `"#5e5e5e"`) - `barWidth` - Width of each frequency bar in pixels (default: `2`) - `gap` - Gap between bars in pixels (default: `1`) - `rounded` - Border radius for rounded bars (default: `0`) #### Advanced Props **Border Styling:** - `showBorder` - Show border around visualization area (default: `false`) - `borderColor` - Border color (default: `"#333333"`) - `borderWidth` - Border width in pixels (default: `1`) - `borderRadius` - Border radius for rounded corners (default: `0`) **Animation & Rendering:** - `speed` - Animation speed from 1-6, higher numbers are slower (default: `3`) - `animateCurrentPick` - Enable smooth bar transitions (default: `true`) - `fullscreen` - Fill entire parent container (default: `false`) - `onlyActive` - Show visualization only when audio is active (default: `false`) **State Control:** - `isPaused` - Pause visualization display without affecting audio (default: `false`) **Advanced Customization:** - `customRenderer` - Custom rendering function for advanced visualizations - `placeholder` - Custom React component to show when no audio source - `showPlaceholderBackground` - Whether to show background in placeholder state **CSS Classes:** - `className` - CSS class for the main container - `canvasClassName` - CSS class for the canvas element **Event Callbacks:** - `onStateChange` - Called when visualization state changes - `onRenderStart` - Called when rendering starts - `onRenderStop` - Called when rendering stops - `onError` - Called on render errors ### useMediaAudio Hook (Recommended) Converts media sources into visualization data. This is the main hook for most use cases. ```tsx const { source, error } = useMediaAudio({ source: mediaStream // MediaStream | HTMLAudioElement | HTMLVideoElement | AudioNode }); ``` > **⚠️ Important:** When using lazy-loaded audio sources (starting as `null`), you must memoize the options object to prevent flickering. See [Important Usage Notes](#important-usage-notes) for details. **Supported Sources:** - `MediaStream` - Microphone, recording software - `HTMLAudioElement` - Audio files - `HTMLVideoElement` - Video files - `AudioNode` - Web Audio API nodes **Returns:** - `source` - AudioSource instance for AudioWave component - `error` - Any processing errors (Error | null) ### useAudioSource Hook (Legacy) > **Note:** `useAudioSource` is an alias for `useMediaAudio` maintained for backward compatibility. New projects should use `useMediaAudio`. ### Specialized Hooks For better type safety and convenience, you can use specialized hooks: ```tsx // For microphone or recording software const { source, error } = useMediaStreamSource(mediaStream); // For audio/video files const { source, error } = useMediaElementSource(audioElement); // For Web Audio API nodes const { source, error } = useAudioNodeSource(audioNode); // For custom audio data (Electron, Node.js, etc.) const { source } = useCustomAudio({ provider: myProvider }); ``` ### useCustomAudio Hook For advanced use cases where you need to provide custom audio data (Electron apps, Node.js audio processing, custom audio pipelines), use the `useCustomAudio` hook with a custom provider: ```tsx import { useCustomAudio } from '@audiowave/react'; import { useMemo } from 'react'; function CustomAudioApp() { // Create audio data provider const audioProvider = useMemo(() => ({ onAudioData: (callback: (data: Uint8Array) => void) => { // Your audio data subscription logic const unsubscribe = yourAudioSource.subscribe(callback); return unsubscribe; // Return cleanup function }, onAudioError: (callback: (error: string) => void) => { // Optional error handling const unsubscribe = yourAudioSource.onError(callback); return unsubscribe; } }), []); const { source, error } = useCustomAudio({ provider: audioProvider, deviceId: 'default' }); return ( <div> {error && <div>Error: {error}</div>} <AudioWave source={source} height={120} /> </div> ); } ``` **Parameters:** - `provider` - AudioDataProvider implementation (required) - `deviceId` - Device identifier (optional, default: 'default') **Returns:** - `source` - AudioSource instance for AudioWave component - `status` - Current status: 'idle' | 'active' | 'paused' - `error` - Error message if any - `isActive` - Boolean indicating if audio is active - `clearError` - Function to clear error state **Audio Data Format:** - `Uint8Array` with values in range [0, 255] - 128 represents silence (center value) - Values above 128 represent positive amplitude - Values below 128 represent negative amplitude - Array length should match your desired visualization resolution ## Electron Integration Guide For Electron applications, use `useCustomAudio` with a custom provider for native audio processing: ### Best Practice: Main Process Audio Processing Process audio in the main process for optimal performance: **Main Process (`main.js`):** ```typescript import { AudioProcessor } from '@audiowave/core'; import { ipcMain } from 'electron'; class ElectronAudioCapture { private audioProcessor: AudioProcessor; constructor() { this.audioProcessor = new AudioProcessor({ bufferSize: 1024, skipInitialFrames: 2, inputBitsPerSample: 32, // naudiodon uses 32-bit inputChannels: 2, // stereo input }); this.setupAudioCapture(); } private setupAudioCapture() { // Setup your audio capture (naudiodon, etc.) audioCapture.on('data', (buffer: Buffer) => { // Process audio in main process const result = this.audioProcessor.process(buffer); if (result) { // Send processed data to renderer mainWindow.webContents.send('audio-data', result.timeDomainData); } }); } } // IPC handlers ipcMain.handle('start-audio-capture', async () => { // Start audio capture logic }); ipcMain.handle('stop-audio-capture', async () => { // Stop audio capture logic }); ``` **Renderer Process (`renderer.tsx`):** ```tsx import { useCustomAudio } from '@audiowave/react'; import { useMemo } from 'react'; function ElectronAudioApp() { // Create provider for Electron IPC communication const electronProvider = useMemo(() => ({ onAudioData: (callback: (data: Uint8Array) => void) => { const handleAudioData = (_event: any, audioData: Uint8Array) => { callback(audioData); }; window.electronAPI.onAudioData(handleAudioData); return () => { window.electronAPI.removeAudioDataListener?.(handleAudioData); }; }, onAudioError: (callback: (error: string) => void) => { const handleError = (_event: any, error: string) => { callback(error); }; window.electronAPI.onAudioError?.(handleError); return () => { window.electronAPI.removeAudioErrorListener?.(handleError); }; } }), []); const { source, error } = useCustomAudio({ provider: electronProvider, deviceId: 'default' }); const startCapture = () => { window.electronAPI.startAudioCapture(); }; const stopCapture = () => { window.electronAPI.stopAudioCapture(); }; return ( <div> {error && <div>Error: {error}</div>} <AudioWave source={source} height={120} /> <button onClick={startCapture}>Start</button> <button onClick={stopCapture}>Stop</button> </div> ); } ``` **Preload Script (`preload.js`):** ```typescript import { contextBridge, ipcRenderer } from 'electron'; contextBridge.exposeInMainWorld('electronAPI', { startAudioCapture: () => ipcRenderer.invoke('start-audio-capture'), stopAudioCapture: () => ipcRenderer.invoke('stop-audio-capture'), onAudioData: (callback: (event: any, data: Uint8Array) => void) => { ipcRenderer.on('audio-data', callback); }, onAudioError: (callback: (event: any, error: string) => void) => { ipcRenderer.on('audio-error', callback); }, removeAudioDataListener: (callback: Function) => { ipcRenderer.removeListener('audio-data', callback); }, removeAudioErrorListener: (callback: Function) => { ipcRenderer.removeListener('audio-error', callback); } }); ``` **Benefits of this approach:** - Optimal performance with minimal data transfer - UI responsiveness maintained - Single processing instance for multiple windows ## Visualization Control AudioWave provides simple controls for the visualization display (not the audio itself): ### Using Ref (Imperative) ```tsx const audioWaveRef = useRef<AudioWaveController>(null); // Basic visualization controls audioWaveRef.current?.pause(); // Freeze display audioWaveRef.current?.resume(); // Resume display audioWaveRef.current?.clear(); // Clear waveform data // State inspection methods const isPaused = audioWaveRef.current?.isPaused(); // Check if paused const state = audioWaveRef.current?.getState(); // Get current state: 'idle' | 'visualizing' | 'paused' const audioData = audioWaveRef.current?.getAudioData(); // Get current audio data (Uint8Array) ``` #### AudioWaveController Methods **Control Methods:** - `pause()` - Pause visualization (freeze waveform display) - `resume()` - Resume visualization from paused state - `clear()` - Clear all waveform data and reset display **State Methods:** - `isPaused()` - Returns boolean indicating if visualization is paused - `getState()` - Returns current state: `'idle'` | `'visualizing'` | `'paused'` - `getAudioData()` - Returns current audio data as Uint8Array ### Using Props (Declarative) ```tsx const [isPaused, setIsPaused] = useState(false); <AudioWave source={source} isPaused={isPaused} // Control via props /> ``` **Important:** These controls only affect the visualization display, not your audio source. ## Amplitude Calculation Modes AudioWave supports three different amplitude calculation methods for different visualization needs: ### Peak Mode (Default) ```tsx <AudioWave source={source} amplitudeMode="peak" /> ``` - **Best for**: General-purpose visualization, music, dynamic content - **Behavior**: Uses peak amplitude values from frequency data - **Characteristics**: High responsiveness, shows all audio peaks clearly - **Backward compatible**: Default mode, maintains existing behavior ### RMS Mode (Perceptual Loudness) ```tsx <AudioWave source={source} amplitudeMode="rms" /> ``` - **Best for**: Voice analysis, broadcast audio, perceptual loudness matching - **Behavior**: Root Mean Square calculation represents how humans perceive loudness - **Characteristics**: Smoother visualization, better represents perceived volume - **Quiet environments**: Enhanced with smooth noise floor transition - Range 1-9: Quiet signals with exponential scaling - Range 10+: Audible signals with logarithmic scaling - Natural baseline (1) represents environmental noise floor ### Adaptive Mode (Dynamic Scaling) ```tsx <AudioWave source={source} amplitudeMode="adaptive" /> ``` - **Best for**: Varying audio levels, automatic gain adjustment, mixed content - **Behavior**: Dynamically adjusts scaling based on recent audio levels - **Characteristics**: Automatically compensates for quiet or loud audio sources - **Use case**: When audio levels vary significantly or are unpredictable ### Mode Switching Example ```tsx import { useState } from 'react'; import { AudioWave, useAudioSource } from '@audiowave/react'; function AmplitudeModeDemo() { const [mediaStream, setMediaStream] = useState<MediaStream | null>(null); const [amplitudeMode, setAmplitudeMode] = useState<'peak' | 'rms' | 'adaptive'>('peak'); const { source } = useAudioSource({ source: mediaStream }); return ( <div> <AudioWave source={source} amplitudeMode={amplitudeMode} height={120} barWidth={2} gap={1} /> <div> <label>Amplitude Mode:</label> <select value={amplitudeMode} onChange={(e) => setAmplitudeMode(e.target.value as any)} > <option value="peak">Peak (Default)</option> <option value="rms">RMS (Perceptual)</option> <option value="adaptive">Adaptive (Auto-scaling)</option> </select> </div> </div> ); } ``` ## Best Practices ### Error Handling ```tsx const { source, error } = useAudioSource({ source: mediaStream }); if (error) { return <div>Visualization error: {error.message}</div>; } ``` ### Important Usage Notes #### Lazy Loading Audio Sources (Required for Proper Functionality) When your audio source starts as `null` and is set later (common in user-initiated scenarios), you **must** memoize the `useAudioSource` options. Without this, the visualization will flicker and restart continuously, making it unusable: **❌ WRONG - This will cause flickering:** ```tsx function MyAudioApp() { const [mediaStream, setMediaStream] = useState<MediaStream | null>(null); // This creates a new options object on every render! const { source } = useMediaAudio({ source: mediaStream, onError: (error) => console.error('Audio error:', error) }); // ... rest of component } ``` **✅ CORRECT - Memoize the options object:** ```tsx function MyAudioApp() { const [mediaStream, setMediaStream] = useState<MediaStream | null>(null); // Step 1: Memoize the onError callback const handleAudioError = useCallback((error: Error) => { console.error('Audio error:', error); }, []); // Step 2: Memoize the entire options object const audioSourceOptions = useMemo(() => ({ source: mediaStream, onError: handleAudioError, }), [mediaStream, handleAudioError]); // Step 3: Use the memoized options const { source } = useMediaAudio(audioSourceOptions); const startRecording = async () => { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); setMediaStream(stream); // This triggers re-render with new source }; return ( <div> <button onClick={startRecording}>Start Recording</button> <AudioWave source={source} /> </div> ); } ``` **Why is this required?** - Each render creates a new options object `{ source: mediaStream, onError: ... }` - `useMediaAudio` sees this as a "change" and reinitializes the audio processing - This causes the visualization to flicker and restart repeatedly, making it unusable - This is not a performance issue - it's a functional requirement for proper operation **Quick Reference - Copy this pattern:** ```tsx // Always use this pattern for lazy-loaded audio sources const handleError = useCallback((error: Error) => { console.error('Audio error:', error); }, []); const options = useMemo(() => ({ source: yourAudioSource, // MediaStream | HTMLAudioElement | etc. onError: handleError, }), [yourAudioSource, handleError]); const { source } = useMediaAudio(options); ``` ### Performance Optimization #### Memoizing Component Props ```tsx // Memoize AudioWave props for better performance const audioWaveProps = useMemo(() => ({ source, height: 200, barWidth: 2, gap: 1, backgroundColor: 'transparent', barColor: '#ffffff' }), [source]); return <AudioWave {...audioWaveProps} />; ``` ### Responsive Design ```tsx <AudioWave source={source} width="100%" height={window.innerWidth < 768 ? 80 : 120} style={{ maxWidth: '100%' }} /> ``` ## TypeScript Support Full TypeScript support with comprehensive types: ```tsx import type { AudioWaveProps, AudioWaveController, AudioSource, UseAudioSourceOptions } from '@audiowave/react'; ``` ## Browser Support - Chrome 66+ (March 2018) - Firefox 60+ (May 2018) - Safari 14+ (September 2020) - Edge 79+ (January 2020) Requires Web Audio API and MediaStream API support. ## License MIT © [teomyth](https://github.com/teomyth)