Use Cases (Updated: 6/2/2026)

How to Build an Audio Player with Claude Code

Learn how to build an audio player using Claude Code. Includes practical code examples and step-by-step guidance.

How to Build an Audio Player with Claude Code

Building an Audio Player with Claude Code

There are many situations where you need a custom audio player: music streaming services, podcasts, educational content, and more. With Claude Code, you can build a full-featured player with waveform visualization and a playlist.

Implementing the Base Player

> Build a custom audio player.
> Support play/pause, a seek bar, volume, and playback speed.
> Also implement a playlist and waveform display.
// src/components/AudioPlayer.tsx
'use client';
import { useRef, useState, useEffect } from 'react';

interface Track {
  id: string;
  title: string;
  artist: string;
  src: string;
  duration: number;
  coverArt?: string;
}

interface AudioPlayerProps {
  tracks: Track[];
  initialTrackIndex?: number;
}

export function AudioPlayer({ tracks, initialTrackIndex = 0 }: AudioPlayerProps) {
  const audioRef = useRef<HTMLAudioElement>(null);
  const [currentTrackIndex, setCurrentTrackIndex] = useState(initialTrackIndex);
  const [isPlaying, setIsPlaying] = useState(false);
  const [currentTime, setCurrentTime] = useState(0);
  const [duration, setDuration] = useState(0);
  const [volume, setVolume] = useState(0.8);

  const currentTrack = tracks[currentTrackIndex];

  useEffect(() => {
    const audio = audioRef.current;
    if (!audio) return;

    const onTimeUpdate = () => setCurrentTime(audio.currentTime);
    const onLoadedMetadata = () => setDuration(audio.duration);
    const onEnded = () => playNext();

    audio.addEventListener('timeupdate', onTimeUpdate);
    audio.addEventListener('loadedmetadata', onLoadedMetadata);
    audio.addEventListener('ended', onEnded);

    return () => {
      audio.removeEventListener('timeupdate', onTimeUpdate);
      audio.removeEventListener('loadedmetadata', onLoadedMetadata);
      audio.removeEventListener('ended', onEnded);
    };
  }, [currentTrackIndex]);

  const togglePlay = () => {
    const audio = audioRef.current;
    if (!audio) return;
    if (audio.paused) {
      audio.play();
      setIsPlaying(true);
    } else {
      audio.pause();
      setIsPlaying(false);
    }
  };

  const playNext = () => {
    const nextIndex = (currentTrackIndex + 1) % tracks.length;
    setCurrentTrackIndex(nextIndex);
    setTimeout(() => {
      audioRef.current?.play();
      setIsPlaying(true);
    }, 100);
  };

  const playPrevious = () => {
    if (currentTime > 3) {
      // If more than 3 seconds have elapsed, restart the track
      audioRef.current!.currentTime = 0;
    } else {
      const prevIndex = (currentTrackIndex - 1 + tracks.length) % tracks.length;
      setCurrentTrackIndex(prevIndex);
      setTimeout(() => {
        audioRef.current?.play();
        setIsPlaying(true);
      }, 100);
    }
  };

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

  return (
    <div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg overflow-hidden max-w-md mx-auto">
      <audio ref={audioRef} src={currentTrack.src} preload="metadata" />

      {/* Cover art */}
      <div className="aspect-square bg-gray-200 dark:bg-gray-700 relative">
        {currentTrack.coverArt ? (
          <img src={currentTrack.coverArt} alt={currentTrack.title} className="w-full h-full object-cover" />
        ) : (
          <div className="w-full h-full flex items-center justify-center text-6xl text-gray-400">♪</div>
        )}
      </div>

      {/* Track info */}
      <div className="p-6">
        <h3 className="text-lg font-bold dark:text-white truncate">{currentTrack.title}</h3>
        <p className="text-gray-500 dark:text-gray-400 text-sm">{currentTrack.artist}</p>

        {/* Seek bar */}
        <div className="mt-4">
          <input
            type="range"
            min={0}
            max={duration || 0}
            value={currentTime}
            onChange={(e) => {
              const time = Number(e.target.value);
              audioRef.current!.currentTime = time;
              setCurrentTime(time);
            }}
            className="w-full h-1 accent-blue-600"
          />
          <div className="flex justify-between text-xs text-gray-400 mt-1">
            <span>{formatTime(currentTime)}</span>
            <span>{formatTime(duration)}</span>
          </div>
        </div>

        {/* Controls */}
        <div className="flex items-center justify-center gap-6 mt-4">
          <button onClick={playPrevious} className="text-2xl dark:text-white hover:text-blue-600">⏮</button>
          <button
            onClick={togglePlay}
            className="w-14 h-14 rounded-full bg-blue-600 text-white text-2xl flex items-center justify-center hover:bg-blue-700"
          >
            {isPlaying ? '⏸' : '▶'}
          </button>
          <button onClick={playNext} className="text-2xl dark:text-white hover:text-blue-600">⏭</button>
        </div>

        {/* Volume */}
        <div className="flex items-center gap-2 mt-4">
          <span className="text-sm dark:text-gray-400">🔊</span>
          <input
            type="range"
            min={0}
            max={1}
            step={0.05}
            value={volume}
            onChange={(e) => {
              const vol = Number(e.target.value);
              audioRef.current!.volume = vol;
              setVolume(vol);
            }}
            className="flex-1 h-1 accent-blue-600"
          />
        </div>
      </div>

      {/* Playlist */}
      <div className="border-t dark:border-gray-700 max-h-60 overflow-y-auto">
        {tracks.map((track, index) => (
          <button
            key={track.id}
            onClick={() => {
              setCurrentTrackIndex(index);
              setTimeout(() => { audioRef.current?.play(); setIsPlaying(true); }, 100);
            }}
            className={`w-full flex items-center gap-3 p-3 text-left hover:bg-gray-50 dark:hover:bg-gray-700 ${
              index === currentTrackIndex ? 'bg-blue-50 dark:bg-blue-900/30' : ''
            }`}
          >
            <span className="text-xs text-gray-400 w-6 text-right">
              {index === currentTrackIndex && isPlaying ? '♪' : index + 1}
            </span>
            <div className="flex-1 min-w-0">
              <p className="text-sm font-medium dark:text-white truncate">{track.title}</p>
              <p className="text-xs text-gray-500 truncate">{track.artist}</p>
            </div>
            <span className="text-xs text-gray-400">{formatTime(track.duration)}</span>
          </button>
        ))}
      </div>
    </div>
  );
}

Waveform Display with the Web Audio API

For a more advanced feature, you can also ask Claude Code to implement real-time waveform visualization using the Web Audio API. Combining AudioContext and AnalyserNode lets you render the playing audio’s waveform onto a canvas.

For video player implementation, see the video player build guide, and for responsive behavior, see responsive design.

For more on the Web Audio API, see MDN (developer.mozilla.org/Web/API/Web_Audio_API).

2026 production upgrade: from demo player to useful audio UI

An audio player is the interface that lets someone listen, pause, jump back, change volume, and understand where they are in a piece of sound. In a media product it is also a business surface: it can show the episode title, expose a free preview, remember progress, send analytics events, and point the listener to a course, membership, download, or consultation. The visible controls are only one part of the job. The player also has to manage browser playback rules, metadata loading, network errors, keyboard access, and state synchronization between React and the actual media element.

The TSX component above is a practical base because it keeps the real playback engine in an HTMLAudioElement referenced by useRef, while React owns the UI state. To copy it into a project, pass a non-empty tracks array, make sure every src points to a real audio file in public or a CDN, and keep the component on the client side. That is enough for a learning portal, a small podcast page, or a product preview player. The production work starts when you add analytics, accessibility labels, CORS-safe waveform analysis, and clear recovery states.

HTMLAudioElement vs Web Audio API

HTMLAudioElement is the browser object behind the audio element. It handles loading, decoding, buffering, playback, current time, volume, metadata, and media events. If the product requirement is “play this podcast episode and let the user seek,” start there. MDN’s audio element reference is the most useful checklist for attributes such as preload, controls, and source handling.

The Web Audio API is lower level. It lets you build an audio graph with nodes such as AudioContext, MediaElementAudioSourceNode, AnalyserNode, and GainNode. Use it when you need waveform rendering, spectrum visualization, EQ, fades, mixing, or audio-reactive UI. It should usually complement the media element rather than replace it. MDN’s Web Audio API guide explains the graph model, and React’s useRef reference explains why the media element should live in a ref instead of React state.

LayerResponsibilityProduction notes
Track dataTitle, artist, audio URL, duration, cover art, transcript IDValidate non-empty playlists before rendering
Playback engineHTMLAudioElement play, pause, seek, volume, metadataListen to loadedmetadata, timeupdate, ended, and error events
VisualizationWeb Audio API analyser for waveform or spectrumRequires CORS-safe audio and an AudioContext resumed after user action
UI controlsButtons, sliders, playlist, progress textAdd labels, focus states, keyboard behavior, and reduced-motion handling
Product layerAnalytics, CTA placement, saved progress, paywall rulesMeasure play, 25%, 50%, completion, replay, and CTA clicks

Real-world use case examples

The first use case is learning content. A language lesson, coding lecture, or onboarding module needs more than play and pause. Students want speed control, ten-second rewind, transcript sync, bookmarks, and a visible “resume from last time” state. For learning products, the player should reduce friction during review because repeated listening is where the value appears.

The second use case is a podcast or editorial media site. A custom player can show episode art, chapters, queue order, newsletter CTA, premium membership prompts, and related episodes. This is where audio UI becomes part of the conversion path. If a listener finishes a free episode, the next visible action should not be random; it should lead to the next episode, a subscription, a download, or a training page.

The third use case is a creator storefront. Musicians, voice actors, sound designers, and course creators can use a player to preview protected samples before purchase. A short waveform and a clear price or Gumroad link can make the page feel trustworthy because the buyer hears the product before paying.

The fourth use case is internal enablement. Sales scripts, customer support examples, pronunciation practice, and compliance training often live in private dashboards. In that setting, the player needs progress persistence, admin-visible completion data, and robust failure messages more than decorative animation.

Pitfalls, failures, and gotchas

The most common pitfall is assuming autoplay will work. Modern browsers often reject audio.play() until the user clicks or taps. Because play() returns a Promise, production code should handle rejection and show a clear prompt instead of silently leaving the button in a fake playing state.

Another failure is trusting duration too early. Until metadata loads, duration can be NaN or zero. Seek bars should use a guarded maximum, and the UI should not claim a final duration before loadedmetadata fires. The example already follows that pattern with duration || 0, but a real product should also show loading and error states.

Waveform features have their own risk. If the audio file comes from another domain, Web Audio analysis can fail unless the server sends the right CORS headers. A player that works with local files may fail after moving media to a CDN. Test the final domain, not only localhost.

React state can also drift from the media element. Track switches triggered with setTimeout are acceptable for a compact demo, but in a production playlist it is safer to wait for canplay or loadedmetadata before calling play(). That prevents race conditions on slow mobile networks.

Accessibility is not optional. Icon-only buttons need aria-label, keyboard focus must be visible, range sliders need clear labels, and screen readers should hear the current track and time. For the broader implementation checklist, pair this article with Claude Code accessibility.

For the React side, read Claude Code React development. For waveform analysis, continue with Claude Code Web Audio API. For inclusive controls, use Claude Code accessibility. These three topics cover the main split: state management, audio analysis, and usable interaction design.

If you are turning audio into a course, lead magnet, paid podcast, or media-product funnel, decide the CTA before you polish the waveform. A practical funnel is: free preview audio, email signup, paid download or membership, then a consultation offer for teams that need the workflow implemented in a real repository. ClaudeCodeLab can help map that through Claude Code setup, review prompts, analytics events, and implementation standards on the training and consultation page.

The useful production measure is not “does the waveform look nice?” It is “can the listener start, resume, understand progress, recover from failure, and take the next business action without confusion?” Build the player around that outcome and the UI will serve both learners and media-product readers.

#Claude Code #audio #Web Audio API #React #player
Free

Free PDF: Claude Code Cheatsheet

Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.

We handle your data with care and never send spam.

Level up your Claude Code workflow

Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.