/ proof-of-concept / phase-perception-2
Sign in
proof-of-concept / phase-perception-2
phase-perception-2 / src / components / PlaybackPanel.tsx
120 lines · 5.5 KBCodeBlameHistory
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 1import { useCallback, useEffect, useRef, useState } from 'react'
2import { clampView, type View } from '../hooks/useZoomPan'
3import { dbColor, formatSeconds } from '../plot'
8type Props = {
9 source: Source
10 result: RenderResult
11 stale: boolean
12 playing: string | null
13 play: (label: string, which: ('A' | 'B')[]) => void
14 stop: () => void
17const COLOR_A = '#1f5fa8'
18const COLOR_B = '#d9641e'
19const DB_RANGE = 70
22/** Play A and B, and compare their waveforms and spectrograms. */
23export function PlaybackPanel({ source, result, stale, playing, play, stop }: Props) {
24 const fs = source.fs
25 const duration = source.samples.length / fs
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 27 const [view, setView] = useState<View>({ start: 0, span: duration })
28 // A new clip resets the view; the same clip re-rendered with a new map keeps it.
29 useEffect(() => {
30 setView({ start: 0, span: source.samples.length / source.fs })
31 }, [source])
32 const onView = useCallback((v: View) => setView(v), [])
34 const c = view.start + view.span / 2
35 const span = view.span * factor
36 setView(clampView({ start: c - span / 2, span }, duration, minSpan))
37 }
39 // A native horizontal scrollbar pans the view: its content is as many times wider than the plot as the clip is longer than the view.
40 const scrollRef = useRef<HTMLDivElement>(null)
41 useEffect(() => {
42 const el = scrollRef.current
43 if (!el) return
44 const target = (view.start / duration) * el.scrollWidth
45 if (Math.abs(el.scrollLeft - target) > 1) el.scrollLeft = target
46 }, [view, duration])
47 const onScroll = () => {
48 const el = scrollRef.current
49 if (!el || el.scrollWidth <= el.clientWidth) return
50 const start = (el.scrollLeft / el.scrollWidth) * duration
51 if (Math.abs(start - view.start) <= duration / el.scrollWidth) return // our own programmatic scroll
52 setView(clampView({ start, span: view.span }, duration, minSpan))
53 }
56 for (let i = 0; i < source.samples.length; i++) yMax = Math.max(yMax, Math.abs(source.samples[i]))
57 for (let i = 0; i < result.y.length; i++) yMax = Math.max(yMax, Math.abs(result.y[i]))
58 yMax = Number(yMax.toPrecision(2)) || 1
59 const busy = playing !== null
61 const highlight: [number, number] | undefined = zoomed ? [view.start, view.start + view.span] : undefined
63 return (
64 <section className="card">
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 67 A is the original and B is the scrambled version, played at the same level{stale ? ' (B is being re-rendered)' : ''}. If they sound
68 the same, phase is inaudible for this sound and these settings. The waveforms differ by {Math.round(100 * result.relDiff)}% of A's
69 RMS; the spectrograms, which show how strong each frequency is over time, should look the same
70 {result.timeVarying ? ' as long as the scramble changes slowly compared with the time window' : ''}.
72 <div className="button-row">
73 <button className="primary" onClick={() => play('A', ['A'])} disabled={busy || stale}>
74 Play A
75 </button>
76 <button className="primary" onClick={() => play('B', ['B'])} disabled={busy || stale}>
77 Play B
78 </button>
79 <button className="primary" onClick={() => play('A then B', ['A', 'B'])} disabled={busy || stale}>
80 Play A then B
81 </button>
82 <button className="primary" onClick={() => play('B then A', ['B', 'A'])} disabled={busy || stale}>
83 Play B then A
84 </button>
85 <button onClick={stop} disabled={!busy}>
86 Stop
87 </button>
88 <span className="playing-indicator">{playing ? `Playing ${playing}…` : ''}</span>
89 </div>
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 92 <span className="info">Drag across a waveform to zoom into that range. The scroll wheel also zooms; the bar below the waveforms pans.</span>
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 94 <span className="info">{zoomed ? `${formatSeconds(view.span)} from ${formatSeconds(view.start)}` : `full clip, ${formatSeconds(duration)}`}</span>
96 <button className="small" onClick={() => zoomBy(0.5)} disabled={view.span <= minSpan}>
97 Zoom in
98 </button>
99 <button className="small" onClick={() => zoomBy(2)} disabled={!zoomed}>
100 Zoom out
101 </button>
102 <button className="small" onClick={() => setView({ start: 0, span: duration })} disabled={!zoomed}>
103 Full clip
104 </button>
105 </span>
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 107 <WaveformPlot samples={source.samples} fs={fs} view={view} onView={onView} yMax={yMax} label="A" color={COLOR_A} />
108 <WaveformPlot samples={result.y} fs={fs} view={view} onView={onView} yMax={yMax} label="B" color={COLOR_B} />
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 109 <div className="scrollbar" ref={scrollRef} onScroll={onScroll} aria-label="pan">
110 <div style={{ width: `${(100 * duration) / view.span}%` }} />
111 </div>
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 113 <div className="plot-label">Spectrograms at the analysis resolution, {DB_RANGE} dB range, shared scale{zoomed ? '; the box marks the waveform view' : ''}</div>
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 115 <ImagePlot image={result.specA} color={specColor} label="A" labelColor="#fff" highlight={highlight} />
116 <ImagePlot image={result.specB} color={specColor} label="B" labelColor="#fff" highlight={highlight} />
118 </section>
119 )
↑↓ move↵openescclose