Use Cases (更新: 2026/6/2)

Claude Codeでオーディオプレーヤーを実装する

Claude Codeを使って、波形表示・プレイリスト・イコライザー対応のカスタムオーディオプレーヤーを構築する方法を紹介します。

Claude Codeでオーディオプレーヤーを実装する

オーディオプレーヤーをClaude Codeで構築する

音楽配信サービス、ポッドキャスト、教育コンテンツなど、カスタムオーディオプレーヤーが必要な場面は多くあります。Claude Codeを使えば、波形表示やプレイリスト機能を備えた本格的なプレーヤーを構築できます。

基本プレーヤーの実装

> カスタムオーディオプレーヤーを作って。
> 再生/一時停止、シークバー、音量、再生速度変更に対応。
> プレイリスト機能と波形表示も実装して。
// 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) {
      // 3秒以上なら先頭に戻る
      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" />

      {/* カバーアート */}
      <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>

      {/* トラック情報 */}
      <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>

        {/* シークバー */}
        <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>

        {/* コントロール */}
        <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>

        {/* ボリューム */}
        <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>

      {/* プレイリスト */}
      <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>
  );
}

Web Audio APIでの波形表示

より高度な機能として、Web Audio APIを使った波形のリアルタイム表示もClaude Codeに依頼できます。AudioContextとAnalyserNodeを組み合わせることで、再生中の音声波形をCanvasに描画できます。

関連記事

動画プレーヤーの実装は動画プレーヤー構築ガイド、レスポンシブ対応についてはレスポンシブデザインも参考にしてください。

Web Audio APIの詳細はMDN(developer.mozilla.org/Web/API/Web_Audio_API)をご覧ください。

2026年版: プロダクション品質へのアップグレード

オーディオプレーヤーとは、音声ファイルを再生するだけの部品ではありません。ユーザーが「どの教材を聞いているのか」「何分まで進んだのか」「今の速度で聞き続けてよいのか」を理解しながら操作するための、音声用のUIです。ブラウザ標準のaudio要素でも再生はできますが、学習サービス、ポッドキャスト、メディア商品のLPでは、進捗、章立て、CTA、アクセシビリティ、分析イベントまで含めて設計する必要があります。

このページのReact/TypeScriptコードは、まずHTMLAudioElementuseRefで保持し、再生、一時停止、シーク、音量、プレイリストを1つのコンポーネントにまとめる構成です。コピーして使う場合は、tracksに空配列を渡さず、srcにはpublic配下またはCDN上の実在する音声ファイルを指定してください。音声UIは見た目より状態管理の失敗が目立つため、ボタンを増やす前に「現在の曲」「現在時刻」「メタデータ読み込み済みか」の3つを安定させるのが実務では重要です。

HTMLAudioElementとWeb Audio APIの使い分け

HTMLAudioElementは、audio要素をJavaScriptから操作するためのブラウザ標準オブジェクトです。再生、停止、音量、再生位置、読み込み状態、メタデータ取得のような基本操作を担当します。通常の教材プレーヤー、ポッドキャスト、試聴プレーヤーなら、まずこちらを中心に作るのが堅実です。仕様とイベントはMDNのaudio要素で確認できます。

一方、Web Audio APIは、音声をノードのグラフとして扱うための低レベルAPIです。AudioContextAnalyserNodeGainNodeなどを組み合わせると、波形表示、スペクトラム表示、イコライザー、フェード、効果音ミキシングが作れます。ただし、再生ボタンやシークバーの代わりではありません。実務ではHTMLAudioElementで安全に再生し、必要なときだけWeb Audio APIを接続する形が保守しやすいです。React側ではDOM要素を直接握るため、useRefの使い方も合わせて確認してください。

レイヤー役割実装メモ
データ曲名、著者、URL、長さ、カバー画像CMSや商品ページから受け取る。空配列を許さない
再生エンジンHTMLAudioElementで再生、停止、シークloadedmetadatatimeupdateを購読する
解析エンジンWeb Audio APIで波形や音量を解析CORS設定とAudioContext.resume()に注意
UIボタン、スライダー、プレイリストaria-label、キーボード操作、フォーカス表示を入れる
計測再生開始、50%到達、完了、CTAクリック教材改善とメディア商品のCV改善に使う

実際のuse case: 音声UIをどこで使うか

1つ目のuse caseは、語学学習やプログラミング講座のレッスン音声です。速度変更、10秒戻し、章ごとのブックマーク、スクリプトとの同期があると、学習者は復習しやすくなります。単に「再生できる」だけではなく、聞き直しやすいことが継続率に直結します。

2つ目は、ポッドキャストやニュースメディアの商品ページです。無料サンプル、会員限定エピソード、プレイリスト、広告挿入位置、関連エピソードの導線を設計します。音声を聞いている途中でページを離脱しないよう、次に読む記事や登録フォームへのリンクを近くに置くと、メディアプロダクトとしての収益導線が作れます。

3つ目は、SaaSや社内教育のオンボーディングです。テキストの手順に音声ガイドを添えると、画面操作をしながら学べます。管理画面のチュートリアル、セールス台本、カスタマーサポートの練習など、短い音声を連続で扱う場面ではプレイリストと進捗保存が効きます。

4つ目は、クリエイターの試聴ページです。楽曲、ナレーション、効果音素材を売る場合、波形表示と短いプレビュー範囲を組み合わせると、購入前に品質を確認できます。Gumroadや自社決済に流す前の「納得してもらうUI」として、音声プレーヤーはかなり強い部品です。

pitfallと失敗例: 動くけれど壊れやすい場所

よくあるpitfallは、自動再生を前提にすることです。多くのブラウザでは、ユーザー操作なしのaudio.play()は拒否されます。play()はPromiseを返すため、本番ではcatchして「再生ボタンを押してください」と表示する設計が必要です。

次の失敗は、durationを最初から数値だと思い込むことです。メタデータが読み込まれる前はNaNになる場合があり、シークバーのmaxにそのまま入れるとUIが不安定になります。この記事のコードのようにloadedmetadataで更新し、表示側はduration || 0で守るのが基本です。

Web Audio APIを使う場合はCORSも落とし穴です。CDN上の音声をAnalyserNodeにつなぐなら、音声ファイルのレスポンスに適切なCORSヘッダーが必要です。これがないと、ローカルでは動いた波形表示が本番だけ無音またはエラーになります。さらに、iOSではAudioContextをユーザー操作後にresume()する必要があるケースもあります。

Reactの落とし穴としては、曲変更直後にsetTimeoutで再生する実装をそのまま本番採用することです。デモでは動きますが、ネットワークが遅いとまだsrcが準備できていない場合があります。堅牢にするなら、曲変更後にcanplayまたはloadedmetadataを待ってから再生する流れに寄せてください。

関連リンクと収益導線

React側の状態設計はClaude Code React開発、波形や解析の深掘りはClaude Code Web Audio API、ボタン名・フォーカス・キーボード対応はアクセシビリティ実装と合わせて読むと理解しやすくなります。

音声UIを教材やメディア商品に組み込むなら、実装だけでなく「どの再生イベントを計測し、どのCTAにつなげるか」まで決めておくべきです。ClaudeCodeLabでは、既存リポジトリに合わせたClaude Codeの導入、CLAUDE.md、レビュー観点、計測イベント設計をtraining / consultationで整理できます。個人開発なら無料試聴からGumroadの商品ページへ、法人向けなら講座完了率と問い合わせ導線へつなぐ設計が現実的です。

検証メモ

この記事のコードで確認すべき点は、コンポーネントがクライアント側で動くこと、tracksが必ず1件以上あること、音声URLが実在すること、再生ボタンがユーザー操作として扱われることです。試した結果として一番差が出るのは波形表示そのものではなく、loadedmetadata前の表示崩れ、再生拒否時のメッセージ、シークバー操作時の状態同期でした。つまり、音声プレーヤーは派手なエフェクトより先に、基本イベントを丁寧に扱うほど実用品になります。

#Claude Code #オーディオ #Web Audio API #React #プレーヤー
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。