フレームワーク統合
React など framework との統合パターンや、Promise ベースの待機、UI アニメーション連携について説明します。
使用モジュール: adapters, play
React デモ
React で waa-play を使用する実動デモです。useSyncExternalStore と subscribeSnapshot / getSnapshot を使って、再生状態をリアクティブに管理します。
Sound Source
Synth で音を生成するか、音声ファイルを読み込んで開始します。
デモのソースコード
このデモのコアロジックです。UI の描画コードは省略しています。
import { useCallback, useSyncExternalStore, useState, useMemo, useRef } from "react";import { WaaPlayer, getSnapshot, subscribeSnapshot } from "waa-play";import type { Playback, PlaybackSnapshot, PeakPair } from "waa-play";
// --- hooks ---
function usePlaybackSnapshot(playback: Playback | null): PlaybackSnapshot | null { const subscribe = useCallback( (cb: () => void) => (playback ? subscribeSnapshot(playback, cb) : () => {}), [playback], ); const snap = useCallback( () => (playback ? getSnapshot(playback) : null), [playback], ); return useSyncExternalStore(subscribe, snap, snap);}
function useReactPlayer() { const waaRef = useRef(new WaaPlayer()); const [buffer, setBuffer] = useState<AudioBuffer | null>(null); const [playback, setPlayback] = useState<Playback | null>(null); const [loop, setLoop] = useState(true); const snap = usePlaybackSnapshot(playback); const peaks = useMemo( () => (buffer ? waaRef.current.extractPeakPairs(buffer, { resolution: 200 }) : []), [buffer], );
async function handleGenerate() { const waa = waaRef.current; await waa.ensureRunning(); if (playback && playback.getState() !== "stopped") playback.stop(); setPlayback(null); setBuffer(waa.createSineBuffer(440, 3)); }
async function handleFile(e: React.ChangeEvent<HTMLInputElement>) { const file = e.target.files?.[0]; if (!file) return; const waa = waaRef.current; await waa.ensureRunning(); setPlayback(null); setBuffer(await waa.loadFromBlob(file)); }
function handleToggle() { if (!buffer) return; if (!playback || playback.getState() === "stopped") { if (playback) playback.dispose(); setPlayback(waaRef.current.play(buffer, { loop })); } else { playback.togglePlayPause(); } }
function handleStop() { playback?.stop(); }
function handleSeek(ratio: number) { if (playback && buffer) { playback.seek(ratio * buffer.duration); } }
function handleLoopToggle() { const next = !loop; setLoop(next); if (playback) playback.setLoop(next); }
return { buffer, snap, peaks, loop, handleGenerate, handleFile, handleToggle, handleStop, handleSeek, handleLoopToggle };}
// --- component ---
export default function ReactPlayerDemo() { const { buffer, snap, peaks, loop, handleGenerate, handleFile, handleToggle, handleStop, handleSeek, handleLoopToggle } = useReactPlayer(); const state = snap?.state ?? "stopped";
return ( <div> <button onClick={handleGenerate}>Generate Sine</button> <input type="file" accept="audio/*" onChange={handleFile} /> {buffer && ( <> <Waveform peaks={peaks} progress={snap?.progress ?? 0} onSeek={handleSeek} /> <span>{formatTime(snap?.position ?? 0)} / {formatTime(snap?.duration ?? 0)}</span> <button onClick={handleToggle}> {state === "playing" ? "Pause" : "Play"} </button> <button onClick={handleStop} disabled={state === "stopped"}> Stop </button> <label> <input type="checkbox" checked={loop} onChange={handleLoopToggle} /> Loop </label> </> )} </div> );}コード例
1. React カスタムフック
subscribeSnapshot と getSnapshot は React の useSyncExternalStore と直接互換性があります。
import { useSyncExternalStore, useRef, useCallback } from "react";import { WaaPlayer } from "waa-play";import type { Playback, PlaybackSnapshot } from "waa-play";
// Shared WaaPlayer instanceconst waa = new WaaPlayer();
function usePlaybackSnapshot(playback: Playback | null): PlaybackSnapshot | null { const subscribe = useCallback( (callback: () => void) => { if (!playback) return () => {}; return waa.subscribeSnapshot(playback, callback); }, [playback], ); const snap = useCallback( () => (playback ? waa.getSnapshot(playback) : null), [playback], ); return useSyncExternalStore(subscribe, snap, snap);}
// Usage examplefunction AudioPlayer({ url }: { url: string }) { const playbackRef = useRef<Playback | null>(null); const snapshot = usePlaybackSnapshot(playbackRef.current);
async function handlePlay() { await waa.ensureRunning(); const buffer = await waa.load(url); playbackRef.current = waa.play(buffer); }
return ( <div> <button onClick={handlePlay}>Play</button> {snapshot && ( <div> <p>State: {snapshot.state}</p> <p>Position: {snapshot.position.toFixed(1)}s / {snapshot.duration.toFixed(1)}s</p> <progress value={snapshot.progress} max={1} /> </div> )} </div> );}解説: subscribeSnapshot は statechange, timeupdate, seek, ended イベントを購読し、変更時に callback を呼びます。getSnapshot は immutable な snapshot を返します。この2つの関数は useSyncExternalStore の subscribe / getSnapshot パラメータにそのまま対応します。
関数 API 版
import { useSyncExternalStore, useRef, useCallback } from "react";import { createContext, ensureRunning } from "waa-play/context";import { loadBuffer } from "waa-play/buffer";import { play } from "waa-play/play";import { subscribeSnapshot, getSnapshot } from "waa-play/adapters";import type { Playback, PlaybackSnapshot } from "waa-play";
function usePlaybackSnapshot(playback: Playback | null): PlaybackSnapshot | null { const subscribe = useCallback( (callback: () => void) => { if (!playback) return () => {}; return subscribeSnapshot(playback, callback); }, [playback], ); const snap = useCallback( () => (playback ? getSnapshot(playback) : null), [playback], ); return useSyncExternalStore(subscribe, snap, snap);}
// Usage examplefunction AudioPlayer({ url }: { url: string }) { const ctxRef = useRef<AudioContext>(new AudioContext()); const playbackRef = useRef<Playback | null>(null); const snapshot = usePlaybackSnapshot(playbackRef.current);
async function handlePlay() { const ctx = ctxRef.current; await ensureRunning(ctx); const buffer = await loadBuffer(ctx, url); playbackRef.current = play(ctx, buffer); }
return ( <div> <button onClick={handlePlay}>Play</button> {snapshot && ( <div> <p>State: {snapshot.state}</p> <p>Position: {snapshot.position.toFixed(1)}s / {snapshot.duration.toFixed(1)}s</p> <progress value={snapshot.progress} max={1} /> </div> )} </div> );}2. Promise ベース待機
再生完了やポジション到達を Promise で待機できます。
import { WaaPlayer } from "waa-play";
const waa = new WaaPlayer();await waa.ensureRunning();const buffer = await waa.load("/audio/track.mp3");const playback = waa.play(buffer);
// Wait for playback to completeawait waa.whenEnded(playback);console.log("Playback completed");
// Wait for a specific positionconst playback2 = waa.play(buffer);await waa.whenPosition(playback2, 10); // 10 second markconsole.log("Reached 10 seconds");解説: whenEnded は ended イベント(自然終了)で resolve する Promise を返します。手動 stop() では resolve されません。whenPosition は timeupdate イベントを監視し、指定ポジションに到達(または超過)したら resolve します。
関数 API 版
import { createContext, ensureRunning, loadBuffer, play } from "waa-play";import { whenEnded, whenPosition } from "waa-play/adapters";
const ctx = createContext();await ensureRunning(ctx);const buffer = await loadBuffer(ctx, "/audio/track.mp3");const playback = play(ctx, buffer);
// Wait for playback to completeawait whenEnded(playback);console.log("Playback completed");
// Wait for a specific positionconst playback2 = play(ctx, buffer);await whenPosition(playback2, 10); // 10 second markconsole.log("Reached 10 seconds");3. スムーズ UI アニメーション
onFrame で requestAnimationFrame ベースのスムーズな progress bar・seeker 更新が可能です。
import { WaaPlayer } from "waa-play";
const waa = new WaaPlayer();await waa.ensureRunning();const buffer = await waa.load("/audio/track.mp3");
const progressBar = document.querySelector(".progress-bar") as HTMLElement;const timeDisplay = document.querySelector(".time") as HTMLElement;
const playback = waa.play(buffer);
// Update UI every frame with onFrameconst stopFrame = waa.onFrame(playback, (snapshot) => { // Progress bar progressBar.style.width = `${snapshot.progress * 100}%`;
// Time display const pos = snapshot.position; const dur = snapshot.duration; timeDisplay.textContent = `${formatTime(pos)} / ${formatTime(dur)}`;});
function formatTime(seconds: number): string { const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); return `${m}:${s.toString().padStart(2, "0")}`;}
// Cleanupplayback.on("ended", () => stopFrame());解説: onFrame は requestAnimationFrame ループを内部で管理し、毎フレーム PlaybackSnapshot を callback に渡します。戻り値の関数を呼ぶとループが停止します。timeupdate(setInterval ベース、デフォルト 50ms)より滑らかな更新が可能です。
timeupdate イベントとの違い: timeupdate は setInterval ベースでバックグラウンドタブでも動作します。onFrame は requestAnimationFrame ベースで視覚更新に最適ですがバックグラウンドでは停止します。
関数 API 版
import { createContext, ensureRunning, loadBuffer, play } from "waa-play";import { onFrame } from "waa-play/adapters";
const ctx = createContext();await ensureRunning(ctx);const buffer = await loadBuffer(ctx, "/audio/track.mp3");
const progressBar = document.querySelector(".progress-bar") as HTMLElement;const timeDisplay = document.querySelector(".time") as HTMLElement;
const playback = play(ctx, buffer);
// Update UI every frame with onFrameconst stopFrame = onFrame(playback, (snapshot) => { // Progress bar progressBar.style.width = `${snapshot.progress * 100}%`;
// Time display const pos = snapshot.position; const dur = snapshot.duration; timeDisplay.textContent = `${formatTime(pos)} / ${formatTime(dur)}`;});
function formatTime(seconds: number): string { const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); return `${m}:${s.toString().padStart(2, "0")}`;}
// Cleanupplayback.on("ended", () => stopFrame());