import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { getSampleRate, playSequence, stopAll } from './audio' import { AnalysisPanel } from './components/AnalysisPanel' import { PlaybackPanel } from './components/PlaybackPanel' import { SourcePanel } from './components/SourcePanel' import { defaultPhaseMap, type PhaseMapSpec } from './phasemap' import { Renderer } from './renderer' import { makePreset, presets } from './sources' import type { RenderResult, Source } from './types' const RENDER_DEBOUNCE_MS = 120 const MAX_COLS = 600 const MAX_ROWS = 256 const CURVE_POINTS = 1024 export default function App() { const fs = useMemo(() => getSampleRate(), []) const fmax = Math.min(10000, fs / 2) const [source, setSource] = useState(() => makePreset(presets[0], fs)) const [sigmaMs, setSigmaMs] = useState(5) const [map, setMap] = useState(defaultPhaseMap) const [result, setResult] = useState(null) const [rendering, setRendering] = useState(false) const [error, setError] = useState(null) const [playing, setPlaying] = useState(null) const rendererRef = useRef(null) const resultSource = useRef(null) // The worker is created on demand and terminated on unmount. React StrictMode unmounts and // remounts in development, so a terminated worker must be replaced rather than kept. useEffect( () => () => { rendererRef.current?.terminate() rendererRef.current = null }, [], ) // Re-render B whenever the source, the window, or the map changes (debounced for slider drags). useEffect(() => { if (!source) return setRendering(true) const timer = setTimeout(() => { const renderer = rendererRef.current ?? (rendererRef.current = new Renderer()) renderer .request({ samples: source.samples, fs: source.fs, sigmaMs, map, fmax, maxCols: MAX_COLS, maxRows: MAX_ROWS, curvePoints: CURVE_POINTS }) .then((r) => { if (!r) return // superseded resultSource.current = source setResult(r) setError(null) setRendering(false) }) .catch((err: unknown) => { setError(err instanceof Error ? err.message : String(err)) setRendering(false) }) }, RENDER_DEBOUNCE_MS) return () => clearTimeout(timer) }, [source, sigmaMs, map, fmax]) const play = useCallback( async (label: string, which: ('A' | 'B')[]) => { const src = resultSource.current if (!src || !result) return const g = result.gain const scaled = (x: Float32Array) => x.map((v) => v * g) const buffers = which.map((w) => scaled(w === 'A' ? src.samples : result.y)) setPlaying(label) try { await playSequence(buffers) } finally { setPlaying(null) } }, [result], ) // Show plots for the source the current result belongs to, even while a new render is pending. const shownSource = resultSource.current return (

Phase Perception 2

Take a sound and keep how strong each frequency is at each moment, but scramble the timing (the phase) of the frequency components. Does it still sound the same? Sound A is the original and sound B is the scrambled version.

  1. Choose a sound.
  2. Choose how the phases are scrambled. The quick-start buttons cover the main cases.
  3. Play A and B and listen for a difference.
{error &&
Rendering failed: {error}
} {result && shownSource && ( void play(l, w)} stop={stopAll} /> )} {!result && !error &&
Rendering…
}
) }