# Audio Player

> A feature-rich audio player component with waveform visualization, playback controls, and seeking capabilities.

**BNA UI** — a React Native / Expo component library.
These components render through `react-native`, not the DOM: there are no HTML
elements, no Tailwind classes and no Radix primitives. Source is copied into your
project and imported through `@/components/ui/*`, `@/components/charts/*`,
`@/hooks/*` and `@/theme/*`. Colours come from the `useColor` hook rather than
hardcoded hex; sizing tokens (`HEIGHT`, `FONT_SIZE`, `BORDER_RADIUS`, `CORNERS`)
come from `@/theme/globals`.

- Docs: https://ui.ahmedbna.com/docs/components/audio-player
- Markdown: https://ui.ahmedbna.com/docs/components/audio-player.md
- Structured JSON (props, usage, source, examples): https://ui.ahmedbna.com/r/ai/audio-player.json
- Install payload (source plus every file it imports): https://ui.ahmedbna.com/r/audio-player.json
- Install: `npx bna-ui add audio-player`
- npm dependencies: `expo-asset`, `expo-audio`, `expo-haptics`, `lucide-react-native`, `react-native-gesture-handler`, `react-native-reanimated`, `react-native-svg`, `react-native-worklets`
- Registry dependencies: `mode-provider`, `useColorScheme`, `colors`, `useColor`, `globals`, `useHaptics`, `text`, `view`, `icon`, `spinner`, `button`, `progress`, `audio-waveform`
- Preview recording: https://demo.ahmedbna.com/0024-audio-player-demo.MP4

---

**Example:** A basic audio player with all features enabled

```tsx
// components/demo/audio-player/audio-player-demo.tsx
import { AudioPlayer } from '@/components/ui/audio-player';

export function AudioPlayerDemo() {
  // Sample audio URL - replace with your actual audio source
  const sampleAudioUrl =
    'https://www.thesoundarchive.com/ringtones/old-phone-ringing.wav';

  return (
    <AudioPlayer
      source={{ uri: sampleAudioUrl }}
      showControls={true}
      showWaveform={true}
      showTimer={true}
      showProgressBar={true}
      autoPlay={false}
      onPlaybackStatusUpdate={(status) => {
        console.log('Playback status:', status);
      }}
    />
  );
}
```

## Installation

### CLI

```bash
npx bna-ui add audio-player
```

### Manual

**1.** Install the following dependencies:

```bash
npx expo install expo-audio lucide-react-native
```

**2.** Copy and paste the following code into your project.

```tsx
// components/ui/audio-player.tsx
import { AudioWaveform } from '@/components/ui/audio-waveform';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import { Text } from '@/components/ui/text';
import { useColor } from '@/hooks/useColor';
import { BORDER_RADIUS } from '@/theme/globals';
import { AudioSource, useAudioPlayer } from 'expo-audio';
import { Pause, Play, RotateCcw, Square } from 'lucide-react-native';
import { useCallback, useEffect, useState } from 'react';
import { StyleSheet, View, ViewStyle } from 'react-native';

export interface AudioPlayerProps {
  source: AudioSource;
  style?: ViewStyle;
  showControls?: boolean;
  showWaveform?: boolean;
  showTimer?: boolean;
  showProgressBar?: boolean;
  autoPlay?: boolean;
  onPlaybackStatusUpdate?: (status: any) => void;
}

export function AudioPlayer({
  source,
  style,
  showControls = true,
  showWaveform = true,
  showTimer = true,
  showProgressBar = true,
  autoPlay = false,
  onPlaybackStatusUpdate,
}: AudioPlayerProps) {
  const player = useAudioPlayer(source);
  const [duration, setDuration] = useState(0);
  const [position, setPosition] = useState(0);
  const [isSeeking, setIsSeeking] = useState(false);

  // Enhanced waveform data - more bars for smoother visualization
  const [waveformData] = useState<number[]>(
    Array.from({ length: 60 }, (_, i) => {
      // Create more varied and realistic waveform pattern
      const base1 = Math.sin((i / 60) * Math.PI * 6) * 0.4 + 0.5;
      const base2 = Math.sin((i / 60) * Math.PI * 2.5) * 0.3 + 0.4;
      const noise = (Math.random() - 0.5) * 0.25;
      const peak = Math.random() < 0.15 ? Math.random() * 0.4 : 0; // Occasional peaks
      return Math.max(0.15, Math.min(0.95, (base1 + base2) / 2 + noise + peak));
    })
  );

  // Theme colors
  const redColor = useColor('destructive');
  const secondaryColor = useColor('secondary');
  const textColor = useColor('text');
  const mutedColor = useColor('textMuted');

  useEffect(() => {
    if (autoPlay && player.isLoaded && !player.playing) {
      player.play();
    }
  }, [autoPlay, player.isLoaded]);

  useEffect(() => {
    const interval = setInterval(() => {
      if (player.isLoaded && !isSeeking) {
        const currentTime = player.currentTime || 0;
        const totalDuration = player.duration || 0;

        setDuration(totalDuration);
        setPosition(currentTime);

        // Check if the audio finished
        if (currentTime >= totalDuration && totalDuration > 0) {
          player.seekTo(0);
          player.pause(); // Ensure it's paused
        }

        if (onPlaybackStatusUpdate) {
          onPlaybackStatusUpdate({
            isLoaded: player.isLoaded,
            playing: player.playing,
            duration: totalDuration,
            position: currentTime,
          });
        }
      }
    }, 100);

    return () => clearInterval(interval);
  }, [player, onPlaybackStatusUpdate, isSeeking]);

  const handlePlayPause = () => {
    if (player.playing) {
      player.pause();
    } else {
      player.play();
    }
  };

  const handleBackFiveSeconds = () => {
    const newPosition = Math.max(0, position - 5);
    seekToPosition(newPosition);
  };

  const handleRestart = () => {
    seekToPosition(0);
  };

  // Unified seeking function
  const seekToPosition = useCallback(
    (newPosition: number) => {
      if (player.isLoaded && duration > 0) {
        const clampedPosition = Math.max(0, Math.min(duration, newPosition));
        player.seekTo(clampedPosition);
        setPosition(clampedPosition);
      }
    },
    [player, duration]
  );

  // Handle waveform seeking
  const handleWaveformSeek = useCallback(
    (seekPercentage: number) => {
      if (duration > 0) {
        const newPosition = (seekPercentage / 100) * duration;
        seekToPosition(newPosition);
      }
    },
    [duration, seekToPosition]
  );

  // Handle progress bar seeking
  const handleProgressSeek = useCallback(
    (progressValue: number) => {
      if (duration > 0) {
        const newPosition = (progressValue / 100) * duration;
        seekToPosition(newPosition);
      }
    },
    [duration, seekToPosition]
  );

  // Handle seeking start/end for smooth updates
  const handleSeekStart = useCallback(() => {
    setIsSeeking(true);
  }, []);

  const handleSeekEnd = useCallback(() => {
    setIsSeeking(false);
  }, []);

  const formatTime = (seconds: number) => {
    const mins = Math.floor(seconds / 60);
    const secs = Math.floor(seconds % 60);
    return `${mins}:${secs.toString().padStart(2, '0')}`;
  };

  const progressPercentage = duration > 0 ? (position / duration) * 100 : 0;

  return (
    <View
      style={[styles.container, { backgroundColor: secondaryColor }, style]}
    >
      {/* Waveform Visualization with seeking capability */}
      {showWaveform && (
        <View style={styles.waveformContainer}>
          <AudioWaveform
            data={waveformData}
            isPlaying={player.playing}
            progress={progressPercentage}
            onSeek={handleWaveformSeek}
            onSeekStart={handleSeekStart}
            onSeekEnd={handleSeekEnd}
            height={80}
            barCount={60}
            barWidth={4}
            barGap={1.5}
            activeColor={redColor}
            inactiveColor={mutedColor}
            animated={true}
            showProgress={true}
            interactive={true} // Enable seeking
          />
        </View>
      )}

      {/* Interactive Progress Bar */}
      {showProgressBar && (
        <View style={styles.progressContainer}>
          <Progress
            value={progressPercentage}
            onValueChange={handleProgressSeek}
            onSeekStart={handleSeekStart}
            onSeekEnd={handleSeekEnd}
            interactive={true}
            height={6}
            style={styles.progressBar}
          />
        </View>
      )}

      {/* Controls */}
      {showControls && (
        <View style={styles.controlsContainer}>
          <Button
            variant='ghost'
            size='icon'
            onPress={handleBackFiveSeconds}
            style={styles.controlButton}
            disabled={!player.isLoaded}
            accessibilityLabel='Rewind 5 seconds'
          >
            <RotateCcw size={18} color={textColor} />
          </Button>

          <Button
            size='icon'
            variant='destructive'
            onPress={handlePlayPause}
            disabled={!player.isLoaded}
            style={styles.playButton}
            accessibilityLabel={player.playing ? 'Pause' : 'Play'}
          >
            {player.playing ? (
              <Pause size={24} color='white' />
            ) : (
              <Play size={24} color='white' />
            )}
          </Button>

          <Button
            variant='ghost'
            size='icon'
            onPress={handleRestart}
            style={styles.controlButton}
            disabled={!player.isLoaded}
            accessibilityLabel='Restart'
          >
            <Square fill={textColor} size={18} color={textColor} />
          </Button>
        </View>
      )}

      {/* Timer */}
      {showTimer && (
        <View style={styles.timerContainer}>
          <Text variant='caption' style={{ color: mutedColor }}>
            {formatTime(position)} / {formatTime(duration)}
          </Text>
        </View>
      )}

      {/* Loading State */}
      {!player.isLoaded && (
        <View style={styles.loadingContainer}>
          <Text variant='caption' style={{ color: mutedColor }}>
            Loading audio...
          </Text>
        </View>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    borderRadius: BORDER_RADIUS,
    padding: 16,
    margin: 8,
  },
  waveformContainer: {
    alignItems: 'center',
    marginBottom: 12,
  },
  progressContainer: {
    marginBottom: 12,
    paddingHorizontal: 4,
  },
  progressBar: {
    // Additional styling if needed
  },
  controlsContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    gap: 12,
    marginBottom: 8,
  },
  controlButton: {
    width: 40,
    height: 40,
  },
  playButton: {
    width: 56,
    height: 56,
  },
  timerContainer: {
    alignItems: 'center',
  },
  loadingContainer: {
    alignItems: 'center',
    padding: 8,
  },
});
```

**3.** Update the import paths to match your project setup.

## Usage

```tsx
import { AudioPlayer } from '@/components/ui/audio-player';
```

### Basic Usage

```tsx
function MyComponent() {
  return (
    <AudioPlayer
      source={{ uri: 'https://example.com/audio.mp3' }}
      showControls={true}
      showWaveform={true}
      showTimer={true}
      autoPlay={false}
    />
  );
}
```

### With Local File

```tsx
<AudioPlayer
  source={require('./assets/audio/sample.mp3')}
  showControls={true}
  showWaveform={true}
  showTimer={true}
  showProgressBar={true}
  onPlaybackStatusUpdate={(status) => {
    console.log('Playback status:', status);
  }}
/>
```

### Minimal Player

```tsx
<AudioPlayer
  source={{ uri: 'https://example.com/audio.mp3' }}
  showControls={true}
  showWaveform={false}
  showTimer={false}
  showProgressBar={true}
/>
```

## Examples

#### Default

**Example:** A complete audio player with all features enabled

```tsx
// components/demo/audio-player/audio-player-demo.tsx
import { AudioPlayer } from '@/components/ui/audio-player';

export function AudioPlayerDemo() {
  // Sample audio URL - replace with your actual audio source
  const sampleAudioUrl =
    'https://www.thesoundarchive.com/ringtones/old-phone-ringing.wav';

  return (
    <AudioPlayer
      source={{ uri: sampleAudioUrl }}
      showControls={true}
      showWaveform={true}
      showTimer={true}
      showProgressBar={true}
      autoPlay={false}
      onPlaybackStatusUpdate={(status) => {
        console.log('Playback status:', status);
      }}
    />
  );
}
```

#### Minimal

**Example:** A minimal audio player with only essential controls

```tsx
// components/demo/audio-player/audio-player-minimal.tsx
import { AudioPlayer } from '@/components/ui/audio-player';

export function AudioPlayerMinimal() {
  const sampleAudioUrl =
    'https://www.thesoundarchive.com/ringtones/old-phone-ringing.wav';

  return (
    <AudioPlayer
      source={{ uri: sampleAudioUrl }}
      showControls={true}
      showWaveform={false}
      showTimer={false}
      showProgressBar={true}
      autoPlay={false}
    />
  );
}
```

#### Waveform Only

**Example:** Audio player focused on waveform visualization

```tsx
// components/demo/audio-player/audio-player-waveform.tsx
import { AudioPlayer } from '@/components/ui/audio-player';

export function AudioPlayerWaveform() {
  const sampleAudioUrl =
    'https://www.thesoundarchive.com/ringtones/old-phone-ringing.wav';

  return (
    <AudioPlayer
      source={{ uri: sampleAudioUrl }}
      showControls={true}
      showWaveform={true}
      showTimer={true}
      showProgressBar={false}
      autoPlay={false}
    />
  );
}
```

#### Custom Styling

**Example:** An audio player with custom styling and theming

```tsx
// components/demo/audio-player/audio-player-styled.tsx
import { AudioPlayer } from '@/components/ui/audio-player';
import { useColor } from '@/hooks/useColor';

export function AudioPlayerStyled() {
  const blue = useColor('indigo');

  const sampleAudioUrl =
    'https://www.thesoundarchive.com/ringtones/old-phone-ringing.wav';

  return (
    <AudioPlayer
      source={{ uri: sampleAudioUrl }}
      showControls={true}
      showWaveform={true}
      showTimer={true}
      showProgressBar={true}
      autoPlay={false}
      style={{
        borderRadius: 20,
        shadowColor: '#000',
        shadowOffset: {
          width: 0,
          height: 4,
        },
        shadowOpacity: 0.1,
        shadowRadius: 8,
        elevation: 5,
        backgroundColor: blue,
      }}
    />
  );
}
```

#### Auto Play

**Example:** Audio player that starts playing automatically when loaded

```tsx
// components/demo/audio-player/audio-player-autoplay.tsx
import { AudioPlayer } from '@/components/ui/audio-player';

export function AudioPlayerAutoplay() {
  const sampleAudioUrl =
    'https://www.thesoundarchive.com/ringtones/old-phone-ringing.wav';

  return (
    <AudioPlayer
      source={{ uri: sampleAudioUrl }}
      showControls={true}
      showWaveform={true}
      showTimer={true}
      showProgressBar={true}
      autoPlay={true}
      onPlaybackStatusUpdate={(status) => {
        if (status.isLoaded && status.playing) {
          console.log('Auto-playing audio');
        }
      }}
    />
  );
}
```

#### Progress Bar Only

**Example:** Audio player using only a progress bar for seeking

```tsx
// components/demo/audio-player/audio-player-progress.tsx
import { AudioPlayer } from '@/components/ui/audio-player';

export function AudioPlayerProgress() {
  const sampleAudioUrl =
    'https://www.thesoundarchive.com/ringtones/old-phone-ringing.wav';

  return (
    <AudioPlayer
      source={{ uri: sampleAudioUrl }}
      showControls={true}
      showWaveform={false}
      showTimer={true}
      showProgressBar={true}
      autoPlay={false}
    />
  );
}
```

#### Audio player Music

**Example:** Audio player with music-focused UI including album art and track info

```tsx
// components/demo/audio-player/audio-player-music.tsx
import { AudioPlayer } from '@/components/ui/audio-player';
import { Button } from '@/components/ui/button';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import {
  Heart,
  MoreHorizontal,
  Shuffle,
  SkipBack,
  SkipForward,
} from 'lucide-react-native';
import React, { useState } from 'react';
import { Image, StyleSheet } from 'react-native';

export function AudioPlayerMusic() {
  const [isLiked, setIsLiked] = useState(false);
  const sampleAudioUrl =
    'https://www.thesoundarchive.com/ringtones/old-phone-ringing.wav';

  return (
    <View style={styles.musicPlayer}>
      {/* Album Art */}
      <View style={styles.albumArtContainer}>
        <Image
          source={{
            uri: 'https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=200&h=200&fit=crop&crop=center',
          }}
          style={styles.albumArt}
        />
      </View>

      {/* Track Information */}
      <View style={styles.trackInfo}>
        <Text variant='body' style={styles.trackTitle}>
          Midnight Waves
        </Text>
        <Text variant='caption' style={styles.artistName}>
          Ocean Sounds Orchestra
        </Text>
      </View>

      {/* Action Buttons */}
      <View style={styles.actionButtons}>
        <Button
          variant='ghost'
          size='icon'
          onPress={() => setIsLiked(!isLiked)}
          style={styles.actionButton}
        >
          <Heart
            size={20}
            color={isLiked ? '#ff6b6b' : '#666'}
            fill={isLiked ? '#ff6b6b' : 'transparent'}
          />
        </Button>
        <Button variant='ghost' size='icon' style={styles.actionButton}>
          <MoreHorizontal size={20} color='#666' />
        </Button>
      </View>

      {/* Audio Player */}
      <AudioPlayer
        source={{ uri: sampleAudioUrl }}
        showControls={true}
        showWaveform={true}
        showTimer={true}
        showProgressBar={false}
        autoPlay={false}
        style={styles.playerContainer}
      />

      {/* Additional Controls */}
      <View style={styles.additionalControls}>
        <Button variant='ghost' size='icon' style={styles.controlButton}>
          <Shuffle size={18} color='#666' />
        </Button>
        <Button variant='ghost' size='icon' style={styles.controlButton}>
          <SkipBack size={18} color='#666' />
        </Button>
        <View style={styles.spacer} />
        <Button variant='ghost' size='icon' style={styles.controlButton}>
          <SkipForward size={18} color='#666' />
        </Button>
        <Button variant='ghost' size='icon' style={styles.controlButton}>
          <Shuffle size={18} color='#666' />
        </Button>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  musicPlayer: {
    backgroundColor: '#F2F2F7',
    borderRadius: 16,
    padding: 20,
    shadowColor: '#000',
    shadowOffset: {
      width: 0,
      height: 4,
    },
    shadowOpacity: 0.1,
    shadowRadius: 12,
    elevation: 8,
  },
  albumArtContainer: {
    alignItems: 'center',
    marginBottom: 16,
  },
  albumArt: {
    width: 120,
    height: 120,
    borderRadius: 12,
  },
  trackInfo: {
    alignItems: 'center',
    marginBottom: 12,
  },
  trackTitle: {
    fontSize: 18,
    fontWeight: '600',
    marginBottom: 4,
    color: '#1a1a1a',
  },
  artistName: {
    fontSize: 14,
    color: '#666',
  },
  actionButtons: {
    flexDirection: 'row',
    justifyContent: 'center',
    gap: 8,
    marginBottom: 16,
  },
  actionButton: {
    width: 36,
    height: 36,
  },
  playerContainer: {
    backgroundColor: 'transparent',
    margin: 0,
    padding: 0,
    marginBottom: 16,
  },
  additionalControls: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    gap: 8,
  },
  controlButton: {
    width: 36,
    height: 36,
  },
  spacer: {
    width: 40,
  },
});
```

## API Reference

### AudioPlayer

The main AudioPlayer component.

| Prop                     | Type                    | Default | Description                                         |
| ------------------------ | ----------------------- | ------- | --------------------------------------------------- |
| `source`                 | `AudioSource`           | -       | The audio source to play (URI or local file).       |
| `style`                  | `ViewStyle`             | -       | Additional styles for the player container.         |
| `showControls`           | `boolean`               | `true`  | Whether to show playback controls.                  |
| `showWaveform`           | `boolean`               | `true`  | Whether to show the waveform visualization.         |
| `showTimer`              | `boolean`               | `true`  | Whether to show the current time and duration.      |
| `showProgressBar`        | `boolean`               | `true`  | Whether to show the progress bar.                   |
| `autoPlay`               | `boolean`               | `false` | Whether to start playing automatically when loaded. |
| `onPlaybackStatusUpdate` | `(status: any) => void` | -       | Callback fired when playback status changes.        |

### AudioSource

The audio source configuration.

```tsx
type AudioSource =
  | {
      uri: string;
    }
  | number; // For local files using require()
```

### Playback Status

The status object passed to `onPlaybackStatusUpdate`:

```tsx
type PlaybackStatus = {
  isLoaded: boolean;
  playing: boolean;
  duration: number;
  position: number;
};
```

## Features

### Waveform Visualization

The audio player includes an interactive waveform that:

- Shows audio amplitude visualization with 60 bars
- Displays playback progress with visual feedback
- Supports seeking by tapping/clicking on the waveform
- Animates smoothly during playback
- Uses theme colors for active/inactive states

### Playback Controls

Standard playback controls include:

- **Play/Pause**: Toggle audio playback
- **Back 5 seconds**: Skip backward 5 seconds
- **Restart**: Return to the beginning of the track

### Interactive Progress Bar

An alternative to waveform seeking:

- Shows current playback position
- Allows seeking by dragging or tapping
- Smooth visual feedback during interaction
- Respects theme colors

### Timer Display

Shows current position and total duration:

- Format: `MM:SS / MM:SS`
- Updates in real-time during playback
- Uses muted text color from theme

## Platform Support

The AudioPlayer works across all platforms supported by Expo:

- **iOS**: Native audio playback with hardware control integration
- **Android**: Optimized audio engine with proper lifecycle management
- **Web**: HTML5 audio with fallback support

## Accessibility

The AudioPlayer component follows accessibility best practices:

- Screen reader announcements for control actions
- Proper button labeling and roles
- Keyboard navigation support (web)
- Respects system accessibility settings
- High contrast support for visually impaired users

## Performance

The component is optimized for performance:

- Efficient waveform rendering with limited update frequency
- Smooth animations using native drivers where possible
- Memory-efficient audio loading and cleanup
- Minimal re-renders during playback

## Theming

The AudioPlayer automatically adapts to your app's theme:

- Uses theme colors for backgrounds, text, and accents
- Supports both light and dark modes
- Destructive color for the main play button
- Muted colors for inactive states
- Customizable through theme configuration

## Advanced Usage

### Custom Playback Status Handling

```tsx
<AudioPlayer
  source={{ uri: 'https://example.com/podcast.mp3' }}
  onPlaybackStatusUpdate={(status) => {
    if (status.isLoaded && status.playing) {
      // Track listening analytics
      analytics.track('audio_playing', {
        position: status.position,
        duration: status.duration,
      });
    }
  }}
/>
```

### Responsive Design

```tsx
<AudioPlayer
  source={{ uri: 'https://example.com/music.mp3' }}
  style={{
    maxWidth: 400,
    alignSelf: 'center',
  }}
  showWaveform={Platform.OS !== 'web'} // Hide on web for better performance
/>
```

## Troubleshooting

### Audio Not Loading

- Ensure the audio source URL is accessible
- Check network connectivity
- Verify audio format is supported (MP3, AAC, WAV)
- Check for CORS issues on web platform

### Performance Issues

- Reduce waveform bar count for lower-end devices
- Disable animations on older devices
- Use lower quality audio files for better loading times

### Seeking Issues

- Ensure audio file supports seeking (not all streaming formats do)
- Check if the audio source provides duration metadata
- Verify the audio file is not corrupted
