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'
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 4import type { RenderResult, Source } from '../types'
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 5import { ImagePlot } from './ImagePlot'
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 6import { WaveformPlot } from './WaveformPlot'
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
15}
17const COLOR_A = '#1f5fa8'
18const COLOR_B = '#d9641e'
19const DB_RANGE = 70
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 20const specColor = dbColor(DB_RANGE)
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 21
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
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 26 const minSpan = 16 / 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), [])
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 33 const zoomBy = (factor: number) => {
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 }
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 54
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 55 let yMax = 0
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
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 60 const zoomed = view.span < duration
61 const highlight: [number, number] | undefined = zoomed ? [view.start, view.start + view.span] : undefined
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 62
63 return (
64 <section className="card">
5fe383dTechnical wording and labels, presets with stated predictions, phase map plots always visibleJeremy Magland 65 <h2>3. Listen</h2>
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 66 <p className="hint">
5fe383dTechnical wording and labels, presets with stated predictions, phase map plots always visibleJeremy Magland 67 A is the source, B is the phase-rotated version; both use the same gain{stale ? ' (B is being re-rendered)' : ''}. RMS of B minus A
68 is {Math.round(100 * result.relDiff)}% of the RMS of A. The spectrograms are computed at the analysis resolution and should match
69 {result.timeVarying ? ' when the map changes slowly compared with the window' : ''}.
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 70 </p>
71 <div className="button-row">
72 <button className="primary" onClick={() => play('A', ['A'])} disabled={busy || stale}>
73 Play A
74 </button>
75 <button className="primary" onClick={() => play('B', ['B'])} disabled={busy || stale}>
76 Play B
77 </button>
78 <button className="primary" onClick={() => play('A then B', ['A', 'B'])} disabled={busy || stale}>
79 Play A then B
80 </button>
81 <button className="primary" onClick={() => play('B then A', ['B', 'A'])} disabled={busy || stale}>
82 Play B then A
83 </button>
84 <button onClick={stop} disabled={!busy}>
85 Stop
86 </button>
87 <span className="playing-indicator">{playing ? `Playing ${playing}…` : ''}</span>
88 </div>
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 90 <div className="view-controls">
5fe383dTechnical wording and labels, presets with stated predictions, phase map plots always visibleJeremy Magland 91 <span className="info">Drag to zoom into a range; wheel zooms; the bar below pans. Both waveforms share the view.</span>
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 92 <span className="spacer" />
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 93 <span className="info">{zoomed ? `${formatSeconds(view.span)} from ${formatSeconds(view.start)}` : `full clip, ${formatSeconds(duration)}`}</span>
9cfa9d2Plucked string default, struck string and bell sounds, numbered steps, quick-start experiments, plots behind a disclosureJeremy Magland 94 <span className="button-group">
95 <button className="small" onClick={() => zoomBy(0.5)} disabled={view.span <= minSpan}>
96 Zoom in
97 </button>
98 <button className="small" onClick={() => zoomBy(2)} disabled={!zoomed}>
99 Zoom out
100 </button>
101 <button className="small" onClick={() => setView({ start: 0, span: duration })} disabled={!zoomed}>
102 Full clip
103 </button>
104 </span>
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 105 </div>
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 106 <WaveformPlot samples={source.samples} fs={fs} view={view} onView={onView} yMax={yMax} label="A" color={COLOR_A} />
107 <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 108 <div className="scrollbar" ref={scrollRef} onScroll={onScroll} aria-label="pan">
109 <div style={{ width: `${(100 * duration) / view.span}%` }} />
110 </div>
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 111
7886a79Waveforms: wheel zoom, drag pan, double-click reset, shared view marked on the spectrogramsJeremy Magland 112 <div className="plot-label">Spectrograms at the analysis resolution, {DB_RANGE} dB range, shared scale{zoomed ? '; the box marks the waveform view' : ''}</div>
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 113 <div className="spectrograms">
e9f92d1Time-varying random phase maps by decimated subband synthesis; drag-to-zoom, scrollbar pan and zoom buttons for the waveformsJeremy Magland 114 <ImagePlot image={result.specA} color={specColor} label="A" labelColor="#fff" highlight={highlight} />
115 <ImagePlot image={result.specB} color={specColor} label="B" labelColor="#fff" highlight={highlight} />
7d910acPhase perception 2: subband phase rotation of arbitrary sounds with filter bank, phase maps, plots, and playbackJeremy Magland 116 </div>
117 </section>
118 )
119}