import { useEffect, useRef, useState } from 'react' import { decodeFile, recordMicrophone, type Clip } from '../capture' import { MAX_SECONDS, excerpt, makePreset, presets } from '../sources' import type { Source } from '../types' type Props = { fs: number source: Source | null setSource: (s: Source) => void busy: boolean } /** Choose the source sound: a synthesized preset, a file, or a microphone recording. */ export function SourcePanel({ fs, source, setSource, busy }: Props) { const [clip, setClip] = useState(null) const [startSec, setStartSec] = useState(0) const [recording, setRecording] = useState(null) const [error, setError] = useState(null) const abortRef = useRef(null) const fileRef = useRef(null) const clipSec = clip ? clip.samples.length / clip.fs : 0 const canScrub = clip !== null && clipSec > MAX_SECONDS // A loaded clip becomes the source through its excerpt. useEffect(() => { if (!clip) return setSource({ name: clip.name, fs: clip.fs, samples: excerpt(clip.samples, clip.fs, startSec) }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [clip, startSec]) const choosePreset = (i: number) => { setClip(null) setError(null) setSource(makePreset(presets[i], fs)) } const onFile = async (file: File | undefined) => { if (!file) return setError(null) try { const c = await decodeFile(file) setStartSec(0) setClip(c) } catch (err) { setError(`Could not decode ${file.name}: ${err instanceof Error ? err.message : String(err)}`) } } const record = async () => { setError(null) const ac = new AbortController() abortRef.current = ac setRecording(0) try { const c = await recordMicrophone(MAX_SECONDS, ac, (s) => setRecording(s)) setStartSec(0) setClip(c) } catch (err) { setError(`Recording failed: ${err instanceof Error ? err.message : String(err)}`) } finally { setRecording(null) abortRef.current = null } } return (

Source sound

Pick a synthesized test sound, load an audio file, or record from the microphone. Clips are mono and limited to {MAX_SECONDS} s. Transient sounds (clicks, bursts, plucks) are where phase changes are expected to be audible; steady sounds are where they are not.

{presets.map((p, i) => ( ))} void onFile(e.target.files?.[0])} /> {recording === null ? ( ) : ( )}
{canScrub && clip && ( )}
{source ? ( <> {source.name}: {(source.samples.length / source.fs).toFixed(2)} s at {source.fs} Hz ) : ( 'No source selected.' )}
{error &&
{error}
}
) }