mirror of
https://github.com/9x/sheetmusictrainer.git
synced 2026-09-02 01:24:33 +02:00
Readme and technical documentation
This commit is contained in:
105
README.md
105
README.md
@@ -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
|
||||
- [@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
|
||||
## Features
|
||||
|
||||
## 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
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
### Installation
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/9x/sheetmusictrainer.git
|
||||
cd sheetmusictrainer
|
||||
```
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
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:
|
||||
3. Run the development server:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
4. Open your browser and navigate to the local URL provided (usually `http://localhost:5173`).
|
||||
|
||||
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...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
## 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
48
TECHNICAL_REALIZATION.md
Normal 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
|
||||
```
|
||||
22
src/App.css
22
src/App.css
@@ -511,6 +511,28 @@ button {
|
||||
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 {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
|
||||
11
src/App.tsx
11
src/App.tsx
@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { SheetMusic } from './components/SheetMusic';
|
||||
import { Controls, type AppSettings } from './components/Controls';
|
||||
import { SettingsModal } from './components/SettingsModal';
|
||||
import { OpenSourceModal } from './components/OpenSourceModal';
|
||||
import { usePitchDetector } from './hooks/usePitchDetector';
|
||||
import { useMetronome } from './hooks/useMetronome';
|
||||
import { useAudioPlayer } from './hooks/useAudioPlayer';
|
||||
@@ -66,6 +67,7 @@ function App() {
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
const [virtualNote, setVirtualNote] = useState<number | null>(null);
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
const [isOpenSourceModalOpen, setIsOpenSourceModalOpen] = useState(false);
|
||||
|
||||
const currentTuning = TUNINGS[settings.tuningId];
|
||||
const currentInstrumentDef = INSTRUMENT_DEFINITIONS[settings.instrument];
|
||||
@@ -575,6 +577,10 @@ function App() {
|
||||
currentPitch={pitchData ? { note: pitchData.note, cents: pitchData.cents } : null}
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -633,6 +639,11 @@ function App() {
|
||||
settings={settings}
|
||||
onUpdateSettings={setSettings}
|
||||
/>
|
||||
|
||||
<OpenSourceModal
|
||||
isOpen={isOpenSourceModalOpen}
|
||||
onClose={() => setIsOpenSourceModalOpen(false)}
|
||||
/>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
|
||||
48
src/components/OpenSourceModal.tsx
Normal file
48
src/components/OpenSourceModal.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
48
src/styles/OpenSourceModal.css
Normal file
48
src/styles/OpenSourceModal.css
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user