/ proof-of-concept / phase-perception-2
Sign in
proof-of-concept / phase-perception-2
phase-perception-2 / src / components / PlaybackPanel.tsx
97 lines · 3.9 KBCodeBlameHistory
2import { formatSeconds } from '../plot'
3import type { RenderResult, Source } from '../types'
4import { SpectrogramPlot } from './SpectrogramPlot'
5import { WaveformPlot } from './WaveformPlot'
7type Props = {
8 source: Source
9 result: RenderResult
10 stale: boolean
11 playing: string | null
12 play: (label: string, which: ('A' | 'B')[]) => void
13 stop: () => void
16const COLOR_A = '#1f5fa8'
17const COLOR_B = '#d9641e'
18const SPANS: { label: string; sec: number | null }[] = [
19 { label: 'full clip', sec: null },
20 { label: '500 ms', sec: 0.5 },
21 { label: '100 ms', sec: 0.1 },
22 { label: '20 ms', sec: 0.02 },
23 { label: '5 ms', sec: 0.005 },
25const DB_RANGE = 70
27/** Play A and B, and compare their waveforms and spectrograms. */
28export function PlaybackPanel({ source, result, stale, playing, play, stop }: Props) {
29 const [spanIdx, setSpanIdx] = useState(0)
30 const [center, setCenter] = useState(0.5)
31 const fs = source.fs
32 const duration = source.samples.length / fs
33 const span = SPANS[spanIdx].sec ?? duration
34 const start = SPANS[spanIdx].sec ? Math.min(Math.max(0, center * duration - span / 2), Math.max(0, duration - span)) : 0
35 let yMax = 0
36 for (let i = 0; i < source.samples.length; i++) yMax = Math.max(yMax, Math.abs(source.samples[i]))
37 for (let i = 0; i < result.y.length; i++) yMax = Math.max(yMax, Math.abs(result.y[i]))
38 yMax = Number(yMax.toPrecision(2)) || 1
39 const busy = playing !== null
41 return (
42 <section className="card">
43 <h2>Listen and compare</h2>
44 <p className="hint">
45 A is the source; B has the phase map applied. Both are played with the same gain{stale ? ' (B is being re-rendered)' : ''}. The
46 waveforms differ by {Math.round(100 * result.relDiff)}% of A's RMS, while the spectrograms below, computed at the analysis
47 resolution, should look the same.
48 </p>
49 <div className="button-row">
50 <button className="primary" onClick={() => play('A', ['A'])} disabled={busy || stale}>
51 Play A
52 </button>
53 <button className="primary" onClick={() => play('B', ['B'])} disabled={busy || stale}>
54 Play B
55 </button>
56 <button className="primary" onClick={() => play('A then B', ['A', 'B'])} disabled={busy || stale}>
57 Play A then B
58 </button>
59 <button className="primary" onClick={() => play('B then A', ['B', 'A'])} disabled={busy || stale}>
60 Play B then A
61 </button>
62 <button onClick={stop} disabled={!busy}>
63 Stop
64 </button>
65 <span className="playing-indicator">{playing ? `Playing ${playing}…` : ''}</span>
66 </div>
68 <div className="zoom-controls">
69 <label>
70 <span>Waveform window</span>
71 <select value={spanIdx} onChange={(e) => setSpanIdx(Number(e.target.value))}>
72 {SPANS.map((s, i) => (
73 <option key={s.label} value={i}>
74 {s.label}
75 </option>
76 ))}
77 </select>
78 </label>
79 {SPANS[spanIdx].sec !== null && (
80 <label className="grow">
81 <span>Position</span>
82 <input type="range" min={0} max={1} step={0.0005} value={center} onChange={(e) => setCenter(Number(e.target.value))} />
83 <span className="slider-unit">{formatSeconds(start)}</span>
84 </label>
85 )}
86 </div>
87 <WaveformPlot samples={source.samples} fs={fs} startSec={start} spanSec={span} yMax={yMax} label="A" color={COLOR_A} />
88 <WaveformPlot samples={result.y} fs={fs} startSec={start} spanSec={span} yMax={yMax} label="B" color={COLOR_B} />
90 <div className="plot-label">Spectrograms at the analysis resolution, {DB_RANGE} dB range, shared scale</div>
91 <div className="spectrograms">
92 <SpectrogramPlot spec={result.specA} dbRange={DB_RANGE} label="A" color="#fff" />
93 <SpectrogramPlot spec={result.specB} dbRange={DB_RANGE} label="B" color="#fff" />
94 </div>
95 </section>
96 )
↑↓ move↵openescclose