Project guidance for AI coding assistants working in this repository.
# Development
npm run dev # Vite dev server at http://localhost:4404 (browser build)
npm run electron:dev # Electron dev mode (renderer at port 4406)
# Build
npm run build # Build React library → dist/
npm run electron:build # Build Electron app → out/
npm run electron:dist # Package distributable (Mac)
npm run build-kits # Regenerate public/kits/ from MPC-Sample/Projects/
# Quality
npm run typecheck # tsc --noEmit across lib + node tsconfigs
npm run lint # Biome lint
npm run check # Biome lint + format (auto-fix)
# Tests
npm test # Run all tests once (Vitest, no watch)Run a single test file:
npx vitest run src/xpj/__tests__/codec.test.tsPackage manager: npm only — do not use pnpm.
This project is simultaneously a React component library (dist/) and a desktop Electron app (out/). The same React source (src/) powers both targets; electron/ contains only the main-process code.
| Target | Entry | Config | Output |
|---|---|---|---|
| Library (npm) | src/index.ts → App |
vite.config.ts |
dist/ |
| Browser dev | src/main.tsx |
vite.config.ts |
served |
| Electron main | electron/main.ts |
electron.vite.config.ts |
out/main/ |
| Electron renderer | index.html |
electron.vite.config.ts |
out/renderer/ |
package.json main field points to out/main/index.js (Electron entry); library consumers use the exports map which resolves to dist/index.js.
Single global store (useMPCStore) is the source of truth. Key state:
padMap: Record<GlobalPadIdx, SamplePad | null>— 128 pad slots (banks A–H). This is the live source of truth;activeKit.padsis only the initial snapshot.activeKit,activeKitId— loaded kit metadataengineRef: AudioEngineLike | null— registered audio engine referenceuiScale,uiOffset— pan/zoom transform, persisted to localStorage- Per-session settings persisted under
mpc.settingsandmpc.uiTransformlocalStorage keys
Implements AudioEngineLike interface using Tone.js (one Tone.Player per pad, lazy-loaded). Audio graph: Tone.Player → per-pad Tone.Gain → master Tone.Gain → Tone.Waveform + Tone.FFT → destination. Buffers are loaded on first trigger and inflight fetches are deduplicated.
Akai MPC project files are gzip-compressed with a 5-line ASCII header followed by JSON. Critical constraint: the firmware expects float-typed fields to have decimal points (1.0 not 1). jsonLossless.ts carries raw number lexemes through parse/stringify to preserve this. codec.ts exposes encodeXpj / decodeXpj and the convenience aliases floatTag / serializeJson.
public/kits/<id>/manifest.json— generated byscripts/build-kits.mjsfromMPC-Sample/Projects/public/kits/<id>/*.wav— preset sample files (gitignored, ~300 MB)SampleKit/SamplePadtypes insrc/kits/kit.types.tsare frozen contracts shared across all modulesGlobalPadIdx0..127:bankIdx = idx >> 4,localIdx = idx & 0xf
isDesktop() / desktop() guard renderer code from browser vs Electron divergence. window.mpcDesktop is injected by electron/preload.ts via contextBridge. Desktop-only features: open/save .xpj file dialogs, auto-open export folder, SD card eject.
Tailwind CSS v4 via @tailwindcss/vite plugin. CSS tokens in src/styles/tokens.css. No CSS modules — class names applied directly. Biome enforces formatting (2-space indent, double quotes, 100-char line width); CSS files are excluded from Biome.
- Pad addressing: Always use
GlobalPadIdx(0–127) internally. Local pad number (1–16) is display only. Bank A = indices 0–15, Bank B = 16–31, etc. padMapis the source of truth for exports — never readactiveKit.padswhen building the export kit; usebuildExportKit(activeKit, padMap)from the store.- Float serialisation: Any number written to
.xpjthat the firmware treats as a float must go throughfloatTag()beforeserializeJson(). - Engine interaction: Store actions call
engineRef?.method()directly; components never call the engine — they dispatch store actions. - Tests live in
__tests__/subdirectories alongside source; electron tests are underelectron/__tests__/with their ownvitest.config.ts.