mirror of
https://github.com/9x/sheetmusictrainer.git
synced 2026-09-02 09:34:33 +02:00
Readme and technical documentation
This commit is contained in:
107
README.md
107
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
|
## 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
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);
|
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;
|
||||||
|
|||||||
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 { 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 >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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