コンテンツにスキップ

フレームワーク統合

React など framework との統合パターンや、Promise ベースの待機、UI アニメーション連携について説明します。

使用モジュール: adapters, play

React デモ

React で waa-play を使用する実動デモです。useSyncExternalStoresubscribeSnapshot / 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 カスタムフック

subscribeSnapshotgetSnapshot は React の useSyncExternalStore と直接互換性があります。

import { useSyncExternalStore, useRef, useCallback } from "react";
import { WaaPlayer } from "waa-play";
import type { Playback, PlaybackSnapshot } from "waa-play";
// Shared WaaPlayer instance
const 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 example
function 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>
);
}

解説: subscribeSnapshotstatechange, 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 example
function 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 complete
await waa.whenEnded(playback);
console.log("Playback completed");
// Wait for a specific position
const playback2 = waa.play(buffer);
await waa.whenPosition(playback2, 10); // 10 second mark
console.log("Reached 10 seconds");

解説: whenEndedended イベント(自然終了)で resolve する Promise を返します。手動 stop() では resolve されません。whenPositiontimeupdate イベントを監視し、指定ポジションに到達(または超過)したら 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 complete
await whenEnded(playback);
console.log("Playback completed");
// Wait for a specific position
const playback2 = play(ctx, buffer);
await whenPosition(playback2, 10); // 10 second mark
console.log("Reached 10 seconds");

3. スムーズ UI アニメーション

onFramerequestAnimationFrame ベースのスムーズな 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 onFrame
const 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")}`;
}
// Cleanup
playback.on("ended", () => stopFrame());

解説: onFramerequestAnimationFrame ループを内部で管理し、毎フレーム PlaybackSnapshot を callback に渡します。戻り値の関数を呼ぶとループが停止します。timeupdate(setInterval ベース、デフォルト 50ms)より滑らかな更新が可能です。

timeupdate イベントとの違い: timeupdatesetInterval ベースでバックグラウンドタブでも動作します。onFramerequestAnimationFrame ベースで視覚更新に最適ですがバックグラウンドでは停止します。

関数 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 onFrame
const 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")}`;
}
// Cleanup
playback.on("ended", () => stopFrame());

関連 API