Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 0 additions & 22 deletions docs/adding-algorithms.md

This file was deleted.

231 changes: 105 additions & 126 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -1,128 +1,107 @@
# Architecture

Modules in `src/` use lowercase, hyphen-separated filenames. `components/ui/` contains
shadcn Base UI primitives; `components/layout/` contains the header and footer.
`components/playground/` contains the interactive screen and its controls;
`components/dialogs/` contains the algorithm editor and MIDI export dialogs.
`sorting/` contains the engine and request handling, and
`sorting/algorithms/` only algorithm implementations. Generators,
MIDI support, and visualizations each have their own directory. The worker lives in
`sorting/worker.mjs`; the Timbre adapter lives in `audio/timbre.mjs`. `@/` aliases `src/`.

`utilities/` contains small, stateless, domain-independent helpers: `cn.ts` combines
CSS classes; `random.ts`, `shuffle.ts`, and `swap.ts` handle numbers and arrays.
There is no separate `lib/` folder or catch-all `utils.ts` file. shadcn's
`utils` alias points to `@/utilities/cn` so newly generated components use the same helper.
Domain-specific code stays with its domain rather than accumulating in `utilities/`.

The sorting playground is the interactive data/sort screen, including its editor
and playback controls. React presentation lives under `components/`;
`controllers/` connects the sorting, audio, state, and visualization modules. Shared
settings and atoms remain in `state/`; component-local state stays in its component. A player
is one data or sort panel; the playground coordinates both. The older “workspace”
name did not describe a separate domain concept.

TypeScript is introduced incrementally alongside `.mjs` modules. Vite handles
bundling; `pnpm run typecheck` checks `.ts` and `.tsx` application modules separately.
Built-in algorithm source and the Ace editor remain JavaScript for now.

Generator implementations live in `generators/patterns/`. React charts live in
`components/visualizations/`; pure trajectory geometry and visualization names live
in `visualizations/`. Neither the registry nor geometry code touches the DOM.

## UI

TanStack Start routes in `src/routes/` contain Home, About, and API page content and use
the shared document in `src/components/layout/`. Public URLs are `/audio-sort/`, `/audio-sort/about`, and `/audio-sort/api`;
prerendering emits `index.html`, `about/index.html`, and `api/index.html`.
TanStack links provide client navigation and ordinary anchor fallbacks without
JavaScript. Audio and editor dependencies are dynamically imported by
Home's effect, never evaluated during server prerendering. About/API remain usable
without JavaScript. `src/client.tsx` hydrates the document.

[`browser-playground.tsx`](../src/components/playground/browser-playground.tsx) owns the browser playground lifecycle with an application-scoped
vanilla Jotai store. React owns settings, tabs, transport controls, counters,
sliders, and dialogs. Shared buttons, links, and option controls use Tailwind classes.
`styles/globals.css` holds the shadcn theme and document defaults;
Chart components use Tailwind fill/stroke classes. There is no separate visualization stylesheet.
The light theme uses sky accents and neutral surfaces. Playground layout uses one
threshold (`lg`, 1024px): stacked below it, side-by-side above it. Chart heights
are fluid, bounded with `clamp()`, without height-specific media queries.

[`create-playground.mjs`](../src/controllers/create-playground.mjs) coordinates workers,
data generation, settings subscriptions, soundfont preloading, and player lifetime.
React reads its playback snapshots through `useSyncExternalStore`. Settings and
custom algorithms are read directly from Jotai; there is no mirrored settings cache.
Audio clocks and nodes remain outside React and Jotai.

[`create-player.mjs`](../src/controllers/create-player.mjs) publishes recorded frames,
position, and renderer selection and delegates transport and synthesis to
`audio/create-transport.ts` and `audio/create-timbre-audio.mjs`.
React subscribes to player snapshots and renders all SVG children, including markers
and the envelope diagram. Trajectory geometry is memoized by frame data; playback
only changes active colors. D3 remains for pure array, scale, color, and path helpers,
not selections or DOM mutations.
The waveform preview canvas has the same explicit imperative ownership.
Pointer capture supports dragging input values across data updates.

Each waveform uses one shared envelope from `state/envelope.ts`. Defaults remain
attack 50 ms, decay 300 ms, sustain 50%, hold 200 ms, and release 300 ms.
Timbre's `adshr` holds at sustain level after decay; the envelope diagram uses that
ordering. The string preview is illustrative, not a sampled live waveform.

shadcn's Base UI dialogs provide modal focus containment and Escape handling.
Closing restores focus to the trigger. Ace loads on demand from its pnpm package;
closing a dialog invalidates pending initialization and destroys the editor/session.
The editor remains JavaScript with two-space soft tabs. Invalid edits leave the
catalog unchanged and show an error. Tab panels stay mounted to preserve the
editor and waveform canvas. Sliders use center-aligned thumbs so hidden-panel
initialization does not depend on measuring thumb widths. MIDI export uses shadcn native selects, `jsmidgen`,
`file-saver`, and Blob.

Cached-page suspension disconnects runtime effects, pauses audio, cancels workers
and pending resumes, and closes dialogs. Returning reconnects effects without
automatically playing. React continues to represent the same Jotai store.
Non-cached exits and Home effect cleanup unmount the playground, dispose owned resources, and
release chart pointer state. Fresh runtime instances can reuse the store.
The shared AudioContext stays library-owned.

All third-party JavaScript uses package imports. `audio/timbre.mjs` only re-exports the
pinned Timbre browser entry; there are no classic script tags or `public/js` files.
D3 imports remain scoped. Sample audio is fetched and decoded by first-party
modules; see [the audio boundary](audio-dependencies.md).

## Engine and workers

[`create-sort-engine.ts`](../src/sorting/create-sort-engine.ts) exports `createSortEngine()`. Each default sort request
gets a fresh engine so recorded frames, counters, and custom API changes do not
leak between requests. `engine.init()` resets recording state, but does not undo
changes to engine methods when deliberately reusing an instance.

[`sort-types.ts`](../src/sorting/sort-types.ts) defines items, frames, the
algorithm-facing API, and request/response contracts. Operations accept indices
or item references; callers remain responsible for valid indices. The recorder
preserves the legacy extra terminal frame for nonempty sorts.

[`sort-requests.ts`](../src/sorting/sort-requests.ts) handles two message types:

- Built-in: `{ key, type: "builtin", id, arr }` runs an imported algorithm.
- Custom: `{ key, type: "custom", source, arr }` compiles an editor body with
`Function("AS", source)`.

Replies contain `{ key, frames }` or `{ key, error }`. Without Worker support, the
UI uses the same request handler on the main thread. Custom code is arbitrary
JavaScript, not a security sandbox.

## Algorithms and editor source

[`algorithm-registry.mjs`](../src/sorting/algorithm-registry.mjs) holds immutable built-in functions and
metadata. [`state/algorithm-overrides.ts`](../src/state/algorithm-overrides.ts)
stores edits and additions per application store and derives a combined catalog.
Built-in identities stay intact until overridden; invalid source is compiled
before any state update. Duplicate IDs are rejected rather than replacing an
existing entry. Function metadata and editor source format are unchanged.
[`algorithm-sources.mjs`](../src/sorting/algorithm-sources.mjs) imports raw source separately so the
editor shows readable code without including those strings in the worker bundle.
Saving an edit creates a custom override and preserves its display metadata.

See [Adding algorithms](adding-algorithms.md) for registration and editor constraints.
See [development](development.md) for commands, publishing pages, and adding algorithms.

## Module map

Paths below are relative to `src/`; `@/` aliases that directory.

| Directory | Responsibility |
| ----------------------------------------------- | ------------------------------------------------------------------------ |
| `routes/` | TanStack Start pages and the legacy `index.html` redirect |
| `components/layout/` | Shared document, header, and footer |
| `components/playground/`, `components/dialogs/` | Interactive controls, editor, and MIDI export |
| `components/ui/` | Shared shadcn Base UI primitives |
| `components/visualizations/` | React SVG charts and envelope diagram |
| `controllers/` | Playground/player coordination and settings connections |
| `state/` | Jotai settings, envelopes, playback preferences, and algorithm overrides |
| `sorting/` | Recorder, worker protocol, registries, and algorithm implementations |
| `generators/`, `midi/` | Input patterns, musical data, and MIDI encoding |
| `audio/` | Playback transport, synthesis, and sample loading |
| `visualizations/` | Pure trajectory geometry and visualization types |
| `utilities/` | Stateless helpers for arrays, randomness, and CSS classes |

TypeScript and JavaScript coexist. Built-in algorithms and editable function bodies
remain JavaScript. Third-party JavaScript comes from package imports.

## UI and lifecycle

Home imports `components/playground/browser-playground.tsx` after hydration.
About/API import Markdown from `docs/` through `components/docs-page.tsx` and
remain readable without JavaScript. Browser audio and editor modules do not run
during prerendering.

`controllers/create-playground.mjs` coordinates data generation, workers, settings
connections, sample preloading, and two players. Each player publishes frames,
position, and visualization selection. React reads runtime snapshots through
`useSyncExternalStore` and settings through the application-scoped Jotai store.
Audio clocks, nodes, and sample caches stay outside React and Jotai.

React owns SVG children; D3 supplies array, scale, color, and path utilities.
Trajectory geometry is memoized by frame data. The waveform preview canvas is
drawn imperatively. Layout stacks below `lg` (1024px) and uses side-by-side panels
above it; chart heights use `clamp()`.

Base UI handles dialog focus and dismissal. Ace loads on demand, with pending
initialization cancelled on close. Tab panels stay mounted for editor/canvas
lifetime. Invalid source leaves the algorithm catalog unchanged.

Cached-page suspension disconnects effects, pauses audio, cancels workers and
pending resumes, and closes dialogs. Returning reconnects without automatically playing.
Leaving Home disposes the playground and its owned resources. The shared
AudioContext remains library-owned.

## Sorting and editor source

`sorting/create-sort-engine.ts` records item snapshots, markers, comparisons, and
swaps. Each default request gets a fresh engine. `init()` resets recording state,
but does not restore methods changed by custom code on a reused engine.
Operations accept indexes or item references; callers must supply valid indexes.
Nonempty recordings retain a legacy extra terminal frame.

`sorting/sort-requests.ts` handles built-in requests by registry ID and custom
requests by compiling a function body with `Function("AS", source)`. Replies
contain `{ key, frames }` or `{ key, error }`. Without Worker support, the same
handler runs on the main thread. Custom JavaScript has no security sandbox, and the
worker does not impose an execution or frame budget.

`sorting/algorithm-registry.mjs` holds frozen built-ins. Jotai stores custom
additions and overrides separately and derives the combined catalog. Source is
compiled before updating state; duplicate IDs are rejected.
`sorting/algorithm-sources.mjs` imports readable source separately for the editor,
so source strings are not included in the worker bundle.

## Audio

`audio/create-transport.ts` owns timing, position, direction, looping, and pending
resume invalidation. Its clock and effects are injected. Stop allows note tails
to finish; suspension/disposal silences owned nodes.
`audio/create-timbre-audio.mjs` connects transport events to synthesis and previews.
Waveforms share the envelope in `state/envelope.ts`; the string preview is
illustrative rather than a sampled live waveform.

`audio/create-soundfont.ts` shares a sample cache between both players, deduplicates
requests, allows retries, and aborts fetches after ten seconds. Cache misses load
without playing a late note. `audio/create-timbre-soundfont.mjs` decodes MP3s with
the existing AudioContext and feeds buffers to Timbre. Disposal aborts requests
and ignores late results. The GeneralUser GS sample bank and instrument/note
mapping were retained during modernization.

`audio/timbre.mjs` imports `timbre/timbre.dev.js`, not its Node entry.
`pnpm-workspace.yaml` excludes unused `speaker` and `readable-stream` dependencies;
Vite maps the bundle's CommonJS `global` to `globalThis`. Timbre publishes a global
as a side effect, but application code uses its import. `package.json` permits
compatible updates; the lockfile currently resolves Timbre to `14.11.25`.

The migration replaced deployed Timbre `14.06.23` (whose old development file was
`13.05.03`) and removed JSONP, JavaScript MP3 decoding, and the Flash fallback.
Regression tests preserve oscillator/envelope/pluck fingerprints and exercise
sample caching and native decoding. The original audit recorded successful CORS
fetches and stereo decoding for note 60, instruments 0, 42, and 127 on 2026-09-07;
that is historical evidence, not a current availability check.

For audio changes, audition waveform and soundfont modes, envelope edits,
instrument changes, tempo/volume, forward/reverse/loop playback, and suspension.
Automated tests do not establish audible equivalence, especially around decoder
padding. Changing the sample bank can change every instrument's sound. Remaining
listening and attribution work is tracked in [TODO](todo.md).
Loading