A simple, accurate metronome with a built-in interval timer for practice rounds.
Pick a tempo (BPM), how long each practice round should last, and how long the break between rounds should be. Hit start.
adapter-staticsetInterval-based playback drift)This project uses pnpm. The required version is locked in the packageManager field of package.json. Use Corepack (corepack enable) or install pnpm globally if you don't have it.
pnpm install
pnpm dev
Open the URL the dev server prints. The first click on Start also resumes the AudioContext (required by browser autoplay policy).
pnpm build
The static site is emitted to build/. You can preview it with:
pnpm preview
A GitHub Actions workflow (.github/workflows/deploy.yml) automatically builds and deploys to GitHub Pages on every push to main.
One-time setup on the repo:
lautronome (the base path in svelte.config.js is hardcoded to /lautronome).main. The workflow will publish to https://<your-user>.github.io/lautronome/.If the repo name ever changes, update paths.base in svelte.config.js to match.
src/
├── app.html <-- root HTML; sets data-theme="pine"
├── lib/
│ └── metronome.svelte.ts <-- audio engine + reactive state ($state)
└── routes/
├── +layout.svelte
├── +layout.ts <-- prerender = true (required for static export)
├── +page.svelte <-- the metronome UI
└── layout.css <-- Tailwind + Skeleton imports
JavaScript timers (setInterval, setTimeout) drift, especially when the tab is backgrounded. So instead of triggering each click from a JS timer, this app uses the lookahead pattern:
AudioContext.currentTime and OscillatorNode.start(when).The phase boundaries (round → break → round) still use setTimeout, but that's fine — being a few milliseconds late on a 60-second round is invisible.