Readme and technical documentation

This commit is contained in:
2025-12-27 22:46:58 +01:00
parent d4415c516e
commit 5ce02498c1
6 changed files with 222 additions and 60 deletions

107
README.md
View File

@@ -1,73 +1,58 @@
# React + TypeScript + Vite # Sheet Music Trainer
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. A web-based application designed to help musicians practice sight-reading and ear training. It listens to your instrument (or voice) via the microphone and gives real-time feedback.
Currently, two official plugins are available: [Live Demo](https://9x.github.io/sheetmusictrainer/) (If available)
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh ## Features
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler - **Sight Reading Mode**: Read notes from the interactive staff and play them on your instrument. The app listens and confirms when you hit the correct note.
- **Ear Training Mode**: Listen to a reference note and try to reproduce it.
- **Real-time Pitch Detection**: Uses your device's microphone to detect notes instantly.
- **Instrument Support**: Optimized for Guitar and Piano, with configurable tunings for guitar.
- **Virtual Instruments**: On-screen interactive guitar fretboard and piano keys for visual reference or touch input.
- **Customizable**: Adjust difficulty (range, accidentals), key signatures, rhythm/metronome settings, and more.
- **Zen Mode**: Distraction-free practice interface.
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). ## Getting Started
## Expanding the ESLint configuration ### Prerequisites
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: - Node.js (v18 or higher recommended)
- npm
```js ### Installation
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this 1. Clone the repository:
tseslint.configs.recommendedTypeChecked, ```bash
// Alternatively, use this for stricter rules git clone https://github.com/9x/sheetmusictrainer.git
tseslint.configs.strictTypeChecked, cd sheetmusictrainer
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
``` ```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: 2. Install dependencies:
```bash
```js npm install
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
``` ```
3. Run the development server:
```bash
npm run dev
```
4. Open your browser and navigate to the local URL provided (usually `http://localhost:5173`).
## Technical Details
100% vibe coded using Google Antigravity and Gemini 3 pro.
This project is built with:
- React + TypeScript
- Vite
- VexFlow (for music notation)
- Pitchfinder (for audio detection)
For more details on the architecture, see [TECHNICAL_REALIZATION.md](./TECHNICAL_REALIZATION.md).
## License
MIT

48
TECHNICAL_REALIZATION.md Normal file
View File

@@ -0,0 +1,48 @@
# Technical Realization
## Architecture Overview
The **Sheet Music Trainer** is a client-side Single Page Application (SPA) built with **React** and **TypeScript**, powered by **Vite**. It is designed to run entirely in the browser without a backend server, allowing for low-latency audio processing and interactivity.
### Key Architectural Choices
1. **Component-Based UI**:
- The application is structured into reusable components (e.g., `SheetMusic`, `Fretboard`, `PianoKeys`, `Controls`) to maintain separation of concerns.
- State management is primarily handled via React's `useState` and `useReducer` at the `App` component level, with props drilling for simpler hierarchies.
2. **Audio Processing**:
- **Pitch Detection**: Utilizes the `pitchfinder` library (YIN algorithm) to detect pitch from the user's microphone in real-time. This processing happens in a dedicated hook/worker to keep the main thread responsive.
- **Audio Synthesis**: Uses standard Web Audio API for generating simple tones (sine/triangle waves) for playback and feedback.
3. **Music Rendering**:
- **VexFlow**: The standard library for rendering music notation on the web. It is used in the `SheetMusic` component to draw the staff, notes, clefs, and key signatures dynamically based on the current state.
4. **Responsiveness**:
- The application uses CSS variables and media queries to adapt to different screen sizes, with specific optimizations for mobile landscape mode to support instrument practice on tablets and phones.
## Frameworks and Libraries
### Core
* **[React](https://react.dev/)**: The library for web and native user interfaces.
* **[TypeScript](https://www.typescriptlang.org/)**: Strongly typed JavaScript for safer development.
* **[Vite](https://vitejs.dev/)**: Next Generation Frontend Tooling for fast development and building.
### Audio & Music
* **[VexFlow](https://www.vexflow.com/)**: A JavaScript library for rendering music notation and guitar tablature.
* **[Pitchfinder](https://github.com/peterkhayes/pitchfinder)**: A collection of pitch detection algorithms for Javascript. used for detecting the note played by the user.
### UI & Icons
* **[Lucide React](https://lucide.dev/)**: A clean and consistent icon library for the interface.
## Directory Structure
```
src/
├── components/ # Reusable UI components (SheetMusic, Fretboard, etc.)
├── hooks/ # Custom React hooks (usePitchDetector, useAudioPlayer, etc.)
├── music/ # Music logic, tuning definitions, and note utilities
├── styles/ # Global styles and component-specific CSS
├── assets/ # Static assets
├── App.tsx # Main application logic and layout
└── main.tsx # Entry point
```

View File

@@ -511,6 +511,28 @@ button {
color: var(--color-text-muted); color: var(--color-text-muted);
} }
.link-button {
background: none;
border: none;
padding: 0;
color: var(--color-primary);
cursor: pointer;
font-family: inherit;
font-size: inherit;
text-decoration: none;
transition: color 0.2s;
}
.link-button:hover {
color: var(--color-primary-dark);
text-decoration: underline;
}
.separator {
margin: 0 8px;
color: #666;
}
.app-subtitle a { .app-subtitle a {
color: var(--color-primary); color: var(--color-primary);
text-decoration: none; text-decoration: none;

View File

@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react';
import { SheetMusic } from './components/SheetMusic'; import { SheetMusic } from './components/SheetMusic';
import { Controls, type AppSettings } from './components/Controls'; import { Controls, type AppSettings } from './components/Controls';
import { SettingsModal } from './components/SettingsModal'; import { SettingsModal } from './components/SettingsModal';
import { OpenSourceModal } from './components/OpenSourceModal';
import { usePitchDetector } from './hooks/usePitchDetector'; import { usePitchDetector } from './hooks/usePitchDetector';
import { useMetronome } from './hooks/useMetronome'; import { useMetronome } from './hooks/useMetronome';
import { useAudioPlayer } from './hooks/useAudioPlayer'; import { useAudioPlayer } from './hooks/useAudioPlayer';
@@ -66,6 +67,7 @@ function App() {
const [revealed, setRevealed] = useState(false); const [revealed, setRevealed] = useState(false);
const [virtualNote, setVirtualNote] = useState<number | null>(null); const [virtualNote, setVirtualNote] = useState<number | null>(null);
const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [isOpenSourceModalOpen, setIsOpenSourceModalOpen] = useState(false);
const currentTuning = TUNINGS[settings.tuningId]; const currentTuning = TUNINGS[settings.tuningId];
const currentInstrumentDef = INSTRUMENT_DEFINITIONS[settings.instrument]; const currentInstrumentDef = INSTRUMENT_DEFINITIONS[settings.instrument];
@@ -575,6 +577,10 @@ function App() {
currentPitch={pitchData ? { note: pitchData.note, cents: pitchData.cents } : null} currentPitch={pitchData ? { note: pitchData.note, cents: pitchData.cents } : null}
/> />
<div className="app-subtitle"> <div className="app-subtitle">
<button className="link-button" onClick={() => setIsOpenSourceModalOpen(true)}>Open Source Libraries</button>
<span className="separator"></span>
<a href="https://github.com/9x/sheetmusictrainer" target="_blank" rel="noopener noreferrer">GitHub</a>
<span className="separator"></span>
<a href="http://jensmohrmann.de" target="_blank" rel="noopener noreferrer">jensmohrmann.de</a> <a href="http://jensmohrmann.de" target="_blank" rel="noopener noreferrer">jensmohrmann.de</a>
</div> </div>
</footer> </footer>
@@ -633,6 +639,11 @@ function App() {
settings={settings} settings={settings}
onUpdateSettings={setSettings} onUpdateSettings={setSettings}
/> />
<OpenSourceModal
isOpen={isOpenSourceModalOpen}
onClose={() => setIsOpenSourceModalOpen(false)}
/>
</div > </div >
); );
} }

View File

@@ -0,0 +1,48 @@
import React from 'react';
import { X } from 'lucide-react';
import '../styles/OpenSourceModal.css';
interface OpenSourceModalProps {
isOpen: boolean;
onClose: () => void;
}
const LIBRARIES = [
{ name: 'React', url: 'https://react.dev/', description: 'The library for web and native user interfaces' },
{ name: 'TypeScript', url: 'https://www.typescriptlang.org/', description: 'JavaScript with syntax for types' },
{ name: 'Vite', url: 'https://vitejs.dev/', description: 'Next Generation Frontend Tooling' },
{ name: 'VexFlow', url: 'https://www.vexflow.com/', description: 'Music notation rendering for the web' },
{ name: 'Pitchfinder', url: 'https://github.com/peterkhayes/pitchfinder', description: 'Pitch detection algorithms' },
{ name: 'Lucide React', url: 'https://lucide.dev/', description: 'Beautiful & consistent icon toolkit' },
];
export const OpenSourceModal: React.FC<OpenSourceModalProps> = ({ isOpen, onClose }) => {
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content open-source-modal" onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h2>Open Source Libraries</h2>
<button className="close-button" onClick={onClose} aria-label="Close">
<X size={24} />
</button>
</div>
<div className="modal-body">
<p>This project relies on these amazing open source libraries:</p>
<ul className="library-list">
{LIBRARIES.map((lib) => (
<li key={lib.name} className="library-item">
<a href={lib.url} target="_blank" rel="noopener noreferrer" className="library-name">
{lib.name}
</a>
<span className="library-desc">{lib.description}</span>
</li>
))}
</ul>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,48 @@
.open-source-modal {
max-width: 500px;
width: 90%;
}
.library-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 16px;
}
.library-item {
display: flex;
flex-direction: column;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
padding-bottom: 12px;
}
.library-item:last-child {
border-bottom: none;
}
.library-name {
font-weight: 600;
color: #646cff;
/* Vite purple or accent color */
text-decoration: none;
font-size: 1.1rem;
margin-bottom: 4px;
}
.library-name:hover {
text-decoration: underline;
}
.library-desc {
color: #ccc;
font-size: 0.9rem;
}
@media (prefers-color-scheme: light) {
.library-item {
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
}