Patch Studio is a visual sound design GUI for the @web-kits/audio library. It provides a DAW-style timeline with layered waveform visualization, a parameter sidebar with interactive EQ and envelope graphs, real-time playback with live parameter tweaking, and patch import/export. The app deploys as a static site to GitHub Pages.
Repo: CameronFoxly/patch-studio
Live: https://cameronfoxly.github.io/patch-studio/
- Framework: Next.js 16 (App Router, Turbopack, static export via
output: "export") - UI: shadcn/ui v4.4.0, style
base-nova(uses@base-ui/react, NOT Radix) - Styling: Tailwind CSS v4 (uses
@theme inline {}blocks, NOTtailwind.config.js) - State: Zustand 5 with undo/redo via zundo 2
- Audio:
@web-kits/audio—defineSound()+play()pattern - Icons: Lucide React (not Nucleo/web-kits icons — those are paid)
- Panels:
react-resizable-panels(usesPanel,Group,Separator— NOTPanelGroup/PanelResizeHandle) - Deploy: GitHub Pages via
.github/workflows/deploy.yml
npm run dev # Start dev server (Turbopack)
npm run build # Production build (static export to out/)
npm run lint # ESLintAlways run npm run build after changes to verify no errors. The build must pass cleanly.
src/
├── app/ # Next.js App Router (page.tsx is the single page)
├── components/
│ ├── ui/ # shadcn/ui components + SliderInput
│ ├── toolbar/ # Top bar: transport, presets, toolbar
│ ├── timeline/ # Timeline: layers, waveform canvas, envelope overlay, ruler
│ ├── sidebar/ # Parameter panels: source, filter, envelope, effects, etc.
│ ├── sequence/ # Step sequencer (future)
│ └── shared/ # Theme toggle, shared components
├── hooks/
│ ├── use-audio-engine.ts # Playback engine (rAF loop, retrigger throttle)
│ └── use-keyboard-shortcuts.ts
├── lib/
│ ├── audio/ # engine.ts (playSound), patch-converter.ts
│ ├── presets/ # registry.ts (metadata), loader.ts (fetch from public/)
│ ├── store/ # Zustand store with slices
│ │ ├── index.ts # Store composition with temporal middleware
│ │ └── slices/ # layers.ts, timeline.ts, sequence.ts, ui.ts
│ ├── types/ # TypeScript types matching @web-kits/audio API
│ └── utils.ts # cn() utility
public/
└── presets/ # 10 JSON preset collection files (252 sounds)
- Uses
@base-ui/reactcomponents, NOT Radix primitives - Component APIs may use
renderprop pattern DropdownMenuSubTriggerdoes NOT reliably forward custom event handlers- Add new components with:
npx shadcn@latest add <component>
- CRITICAL: Raw
:root/.darkCSS blocks are stripped by Tailwind v4. Use@theme inline {}blocks instead. - Font is set via
@theme inline { --font-sans: ... } - Built-in neutral theme provides color variables automatically
- No
tailwind.config.jsfile — config is in CSS via@theme
useStore.temporalis aStoreApi, NOT callable as a hook- Must use
useZustandStore(useStore.temporal, selector)for reactive subscriptions - The
useTemporalState()hook insrc/lib/store/index.tswraps this correctly - Undo/redo only tracks
layersandsequence*state (not transient UI/timeline)
@web-kits/audiocreates entire audio graphs perdefineSound()+play()— no parameter patching on running sounds- Live updates work via throttled retrigger: stop current voice, create new one (80ms cooldown)
isPlayingRefguards against race conditions in asynctriggerSound()- Animation loop reads
useStore.getState()every frame for liveisLooping/regionStart/regionEnd - Spacebar always controls playback unless a text input is focused
- Uses min/max decimation (DAW-style) for consistent appearance across zoom levels
- Generates samples at 8kHz virtual sample rate, then decimates per pixel column
- Seeded PRNG (mulberry32) for stable noise rendering across redraws
- ResizeObserver triggers redraws when canvas size changes (zoom, window resize)
- Canvas clips to
roundRectfor rounded corners
- SVG path uses
preserveAspectRatio="none"for the envelope line (stretches with block) - Control points are HTML
<div>elements (NOT SVG circles) to avoid aspect ratio distortion - Both waveform and overlay use identical
totalDuration = attack + decay + release + 0.5
- Uses Audio EQ Cookbook biquad coefficient formulas for accurate frequency response curves
- Log-scale frequency axis (20Hz–20kHz), dB scale (-24 to +24)
- Shows source frequency spectrum as subtle background histogram
- Control points: X = frequency (log scale), Y = gain (peaking/shelf) or Q (other types)
- SVG hit areas render LAST (on top) so they always capture pointer events
next.config.tsconditionally setsbasePath/assetPrefixwhenDEPLOY_TARGET=gh-pagesNEXT_PUBLIC_BASE_PATHenv var used in client-side fetch URLs (presets, assets)- Preset fetches must use this prefix or they 404 on the
/patch-studio/subpath
- Add the field to the relevant type in
src/lib/types/ - Add store action in
src/lib/store/slices/layers.ts - Use
<SliderInput>component for numerical values (slider + editable text input) - For toggle groups with icons, follow the pattern in
source-panel.tsx
- Add the type to
src/lib/types/effects.ts - Update
effects-panel.tsxwith controls - The audio engine passes effects through to
@web-kits/audiovialayerToSoundDef()
- Add the JSON file to
public/presets/ - Add metadata entry in
src/lib/presets/registry.ts - The loader will auto-discover it
Docs: https://audio.raphaelsalaja.com/llms.txt
Key concepts:
defineSound(definition)returns a play function- Single layer:
{ source, filter?, envelope?, gain?, pan?, lfo?, effects? } - Multi-layer:
{ layers: [...], effects? } - Source types: sine, triangle, square, sawtooth, noise, wavetable
- Filter types: lowpass, highpass, bandpass, notch, allpass, peaking, lowshelf, highshelf
- Effects: reverb, delay, distortion, compressor, tremolo, chorus, bitcrusher, eq
- Envelope: { attack?, decay, sustain?, release? }
- LFO targets: frequency, detune, gain, pan, filter.frequency, filter.Q, etc.
- Neutral, clean aesthetic — no flashy colors
- All numerical inputs use
SliderInput(slider + editable text field) - Interactive graphs use consistent control point style: visible dot → highlight ring → invisible hit area (rendered last for pointer events)
- Use Lucide icons throughout
- Keep components focused — one file per component