Framework Integration
This guide covers integration patterns with frameworks like React, Promise-based waiting, and UI animation synchronization.
Modules used: adapters, play
React Demo
A working demo of waa-play with React. Uses useSyncExternalStore with subscribeSnapshot / getSnapshot to reactively manage playback state.
Sound Source
Generate a synthesized tone or load an audio file to get started.
Demo Source Code
The core logic of this demo. UI rendering code is omitted.
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> );}Code Examples
1. React Custom Hook
subscribeSnapshot and getSnapshot are directly compatible with React’s 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> );}Explanation: subscribeSnapshot subscribes to statechange, timeupdate, seek, and ended events, invoking the callback on changes. getSnapshot returns an immutable snapshot. These two functions map directly to the subscribe / getSnapshot parameters of useSyncExternalStore.
Function 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-Based Waiting
You can wait for playback completion or position arrival using Promises.
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");Explanation: whenEnded returns a Promise that resolves on the ended event (natural completion). It does not resolve on manual stop(). whenPosition monitors the timeupdate event and resolves when the specified position is reached (or exceeded).
Function 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. Smooth UI Animation
onFrame enables smooth progress bar and seeker updates based on requestAnimationFrame.
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());Explanation: onFrame manages a requestAnimationFrame loop internally, passing a PlaybackSnapshot to the callback every frame. Calling the returned function stops the loop. This provides smoother updates than timeupdate (setInterval-based, default 50ms).
Difference from timeupdate event: timeupdate is setInterval-based and works in background tabs. onFrame is requestAnimationFrame-based, ideal for visual updates but pauses in the background.
Function 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());