- TypeScript 97.3%
- JavaScript 0.9%
- HTML 0.8%
- CSS 0.8%
- Shell 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
- Rename ScoreSync → MusicPDF inkl. IndexedDB-Migration alter Daten - Darkmode (System/Hell/Dunkel) für Library + Editor - Editor: Multi-Select via Shift-Klick + Lasso (Rechtsklick) + Strg+A, Bulk-Edit (Farbe / Löschen) für mehrere Markierungen - Editor: Fit-to-Width beim PDF-Laden, kein Flash mehr - Editor: Scroll-to-Cue beim Selektieren einer Markierung - Viewer: Annotation-Tool (Stift / Radierer / Farben / Stärken) mit SVG-Polylines in normalisierten Seitenkoords; persistent in App, ephemeral im Tafel-Widget - Viewer: Undo/Redo (Strg+Z / Strg+Shift+Z) für Annotationen - Viewer: Doppelklick auf Markierung springt im Playback dorthin - Viewer: Auto-Fokus mit Zoom beim Play-Klick - Viewer: Fokus-Modus bleibt beim Zoomen aktiv, Cue als Anker - Viewer: feinere Mausrad-Zoomstufen - Fix: YouTube-Player-Subscription lief zu früh — Time/Duration kamen nicht in der UI an, Timeline blieb leer |
||
| public | ||
| src | ||
| .gitignore | ||
| index.html | ||
| package-lock.json | ||
| package.json | ||
| postcss.config.js | ||
| README.md | ||
| start.sh | ||
| tafel.html | ||
| tailwind.config.js | ||
| tsconfig.app.json | ||
| tsconfig.json | ||
| tsconfig.node.json | ||
| vite.config.ts | ||
| vite.config.widget.ts | ||
MusicPDF
A local-first web app for syncing sheet music PDFs with audio playback — either a local audio file or a YouTube video. Mark regions on a page, attach a time and (optionally) a short note, then play back: the matching region pulses in sync with the recording.
Everything lives in the browser: PDFs and audio files are stored as Blobs in
IndexedDB, projects can be exported to a portable .zip, and there is no
backend at all.
Stack
- React + TypeScript + Vite
- Tailwind CSS
pdfjs-distfor PDF rendering- YouTube IFrame Player API + native
<audio>for playback idbfor IndexedDBjszipfor import/exportlucide-reactfor icons
Getting started
npm install
npm run dev # start the dev server (http://localhost:5173)
npm run build # produce a production build in dist/
npm run preview # serve the production build locally
Open the app in your browser — there is nothing else to configure.
How a project is structured
Each project consists of:
- A PDF (the score)
- A media source: either a YouTube URL/ID or an audio file
- A list of cues. Each cue has a timestamp, a target page, a highlight rectangle (stored in normalized 0–1 coordinates so it survives zooming), and an optional note.
ZIP format
Exportieren produces a .zip containing:
project.json # title, mediaType, mediaSource, cues, schemaVersion: 1
score.pdf # the PDF
audio.<ext> # only present when mediaType === "audioFile"
Importieren reads the same shape back, validates project.json, and adds
the project to the local library with a fresh ID.
Views
- Bibliothek — grid of saved projects with thumbnails; create, open, import, export, delete.
- Bearbeiten — two-pane editor. Drag a rectangle on the page to create a
cue at the player's current time. Selected cues expose resize handles and a
note field. Keyboard shortcuts:
Space— play / pause[/]— nudge the selected cue's time by ±0.1 sDelete/Backspace— remove the selected cue
- Spielen — distraction-free playback. The active cue is determined by a binary search over the time-sorted cues; the PDF auto-pages to the cue and the highlight pulses while the note (if any) appears in a tooltip.
Source layout
src/
components/ reusable UI (Modal, Toast, PdfPage, MediaPanel, NewProjectModal)
media/ MediaPlayer interface + YouTube and HTML audio implementations
pdf/ pdf.js worker setup, render + thumbnail helpers, useDoc hook
storage/ idb wrapper, project CRUD
util/ formatTime, binary search, file download helpers
views/ LibraryView, EditorView, PlayerView
types.ts ProjectRecord, Cue, ProjectJsonV1
App.tsx top-level routing
main.tsx entry point
The media player abstraction (src/media/player.ts) exposes a single
interface:
interface MediaPlayer {
play(): Promise<void> | void;
pause(): void;
seek(t: number): void;
getCurrentTime(): number;
getDuration(): number;
isPlaying(): boolean;
onTimeUpdate(cb: (t: number) => void): () => void;
onPlayStateChange(cb: (playing: boolean) => void): () => void;
destroy(): void;
}
The editor and player views only depend on this interface — they don't care
whether the backend is YouTube or a <audio> element.
Notes
- Object URLs created for audio playback are revoked when the player is destroyed.
- TypeScript strict mode is on (
tsconfig.app.json). localStorageis used only for the small last-route preference; everything project-related lives in IndexedDB.