/ proof-of-concept / plate-dynamics-lab
Sign in
proof-of-concept / plate-dynamics-lab
plate-dynamics-lab / packages / components / src / audio / AudioPlayer.tsx
81 lines · 2.7 KBBlameHistoryRaw
1import { useEffect, useMemo, useRef, useState } from "react";
2import { encodeWav, prepareAudio, toAudioBuffer } from "@plate-dyn/core";
4export interface AudioPlayerProps {
5 /** Pressure signal [Pa] */
6 p: Float64Array;
7 fs: number;
8 label?: string;
9 /** Base name for the exported .wav file (no extension) */
10 filename?: string;
13/**
14 * Plays the radiated-pressure estimate once, exactly as long as it is, and
15 * offers the same clip as a 16-bit mono WAV download. Playback and export level
16 * are normalized; the displayed peak is not. A clip of a few milliseconds
17 * sounds like a click, and is labelled as such rather than repeated.
18 */
19export function AudioPlayer({ p, fs, label, filename = "plate-sound" }: AudioPlayerProps) {
20 const prep = useMemo(() => prepareAudio(p, fs), [p, fs]);
21 const ctxRef = useRef<AudioContext | null>(null);
22 const srcRef = useRef<AudioBufferSourceNode | null>(null);
23 const [playing, setPlaying] = useState(false);
24 useEffect(() => () => stop(), []);
26 const stop = () => {
27 srcRef.current?.stop();
28 srcRef.current = null;
29 setPlaying(false);
30 };
31 const play = () => {
32 if (!ctxRef.current) ctxRef.current = new AudioContext();
33 const ctx = ctxRef.current;
34 void ctx.resume();
35 stop();
36 const src = ctx.createBufferSource();
37 src.buffer = toAudioBuffer(ctx, prep);
38 src.connect(ctx.destination);
39 src.start();
40 src.onended = () => setPlaying(false);
41 srcRef.current = src;
42 setPlaying(true);
43 };
44 const download = () => {
45 // The exported file carries the same normalized signal that is played, at the
46 // simulation's own sample rate; the peak in Pa is reported alongside it.
47 const blob = new Blob([encodeWav(prep.samples, Math.round(prep.fs)) as BlobPart], {
48 type: "audio/wav",
49 });
50 const url = URL.createObjectURL(blob);
51 const a = document.createElement("a");
52 a.href = url;
53 a.download = `${filename}.wav`;
54 a.click();
55 setTimeout(() => URL.revokeObjectURL(url), 0);
56 };
58 const short = prep.durationS < 0.1;
59 return (
60 <div className="pl-row pl-small">
61 <button className="pl-btn" onClick={playing ? stop : play} disabled={p.length < 2}>
62 {playing ? "Stop" : `Play${label ? ` ${label}` : ""}`}
63 </button>
64 <button
65 className="pl-btn"
66 onClick={download}
67 disabled={p.length < 2}
68 title="Download as a 16-bit mono WAV file"
69 >
70 Save .wav
71 </button>
72 <span className="pl-muted">
73 {(prep.durationS * 1000).toFixed(1)} ms at {fs} Hz, peak {prep.peakPa.toExponential(2)} Pa;
74 normalized for playback, played once
75 {short
76 ? ". This excerpt is too short to hear as a note; lengthen the simulated window for a longer clip"
77 : ""}
78 </span>
79 </div>
80 );
↑↓ move↵openescclose