mirror of
https://github.com/9x/sheetmusictrainer.git
synced 2026-09-02 09:34:33 +02:00
Ear training mode
This commit is contained in:
@@ -23,6 +23,7 @@ export interface AppSettings {
|
||||
showTuningMeter: boolean;
|
||||
rhythm: RhythmSettings;
|
||||
zenMode: boolean;
|
||||
gameMode: 'sight_reading' | 'ear_training';
|
||||
}
|
||||
|
||||
interface ControlsProps {
|
||||
@@ -151,6 +152,28 @@ export const Controls: React.FC<ControlsProps> = ({ settings, onUpdateSettings }
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="control-group" style={{ borderTop: '1px solid rgba(255,255,255,0.1)', paddingTop: '12px', marginTop: '12px', width: '100%' }}>
|
||||
<label className="control-label" style={{ marginBottom: '8px' }}>
|
||||
<span>Game Mode</span>
|
||||
</label>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
className={`control-button ${settings.gameMode === 'sight_reading' ? 'active' : ''}`}
|
||||
onClick={() => onUpdateSettings({ ...settings, gameMode: 'sight_reading' })}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
Sight Reading
|
||||
</button>
|
||||
<button
|
||||
className={`control-button ${settings.gameMode === 'ear_training' ? 'active' : ''}`}
|
||||
onClick={() => onUpdateSettings({ ...settings, gameMode: 'ear_training' })}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
Ear Training
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rhythm Controls */}
|
||||
<div className="control-group rhythm-group" style={{ borderTop: '1px solid rgba(255,255,255,0.1)', paddingTop: '12px', marginTop: '12px', width: '100%' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '12px', marginBottom: '8px' }}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Renderer, Stave, StaveNote, Accidental, Voice, Formatter, StaveConnector } from 'vexflow';
|
||||
import { Renderer, Stave, StaveNote, Accidental, Voice, Formatter, StaveConnector, GhostNote, Annotation } from 'vexflow';
|
||||
import { getNoteInKey } from '../music/NoteUtils';
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ interface SheetMusicProps {
|
||||
height?: number;
|
||||
transpose?: number; // Transposition in semitones for visualization (e.g., +12 for guitar)
|
||||
keySignature?: string;
|
||||
hideTargetNote?: boolean;
|
||||
}
|
||||
|
||||
export const SheetMusic: React.FC<SheetMusicProps> = ({
|
||||
@@ -20,7 +21,8 @@ export const SheetMusic: React.FC<SheetMusicProps> = ({
|
||||
width = 300,
|
||||
height = 250, // Increased default height for Grand Staff
|
||||
transpose = 12, // Default to +1 octave (Guitar Notation)
|
||||
keySignature = 'C'
|
||||
keySignature = 'C',
|
||||
hideTargetNote = false
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -111,14 +113,38 @@ export const SheetMusic: React.FC<SheetMusicProps> = ({
|
||||
};
|
||||
|
||||
// --- Create Notes ---
|
||||
const targetObj = createStaveNote(targetMidi, "w", 'target');
|
||||
let targetObj: { note: StaveNote | GhostNote, clef: string };
|
||||
|
||||
if (hideTargetNote) {
|
||||
const visualMidi = targetMidi + transpose;
|
||||
let noteClef = clef;
|
||||
if (clef === 'grand') {
|
||||
noteClef = getGrandStaffClef(visualMidi);
|
||||
}
|
||||
|
||||
// Create a GhostNote (invisible) instead of a StaveNote
|
||||
// We use the same keys to ensure it takes up the right vertical space/clef logic
|
||||
const data = getNoteInKey(visualMidi, keySignature);
|
||||
const ghost = new GhostNote({
|
||||
keys: data.keys,
|
||||
duration: "w",
|
||||
clef: noteClef as 'treble' | 'bass'
|
||||
});
|
||||
|
||||
// Add a Question Mark Annotation
|
||||
ghost.addModifier(new Annotation("?").setVerticalJustification(Annotation.VerticalJustify.CENTER));
|
||||
|
||||
targetObj = { note: ghost, clef: noteClef };
|
||||
} else {
|
||||
targetObj = createStaveNote(targetMidi, "w", 'target');
|
||||
}
|
||||
|
||||
const voicesToDraw: { stave: Stave, voice: Voice }[] = [];
|
||||
|
||||
// Helper to push voice
|
||||
const addVoice = (stave: Stave, notes: StaveNote[]) => {
|
||||
const addVoice = (stave: Stave, notes: (StaveNote | GhostNote)[]) => {
|
||||
const voice = new Voice({ numBeats: 4, beatValue: 4 });
|
||||
voice.addTickables(notes);
|
||||
voice.addTickables(notes as any[]);
|
||||
new Formatter().joinVoices([voice]).format([voice], width - 60);
|
||||
voicesToDraw.push({ stave, voice });
|
||||
};
|
||||
@@ -126,7 +152,20 @@ export const SheetMusic: React.FC<SheetMusicProps> = ({
|
||||
|
||||
if (playedMidi) {
|
||||
const playedObj = createStaveNote(playedMidi, "h", 'played');
|
||||
const targetHalfObj = createStaveNote(targetMidi, "h", 'target');
|
||||
|
||||
let targetHalfObj: { note: StaveNote | GhostNote, clef: string };
|
||||
if (hideTargetNote) {
|
||||
const visualMidi = targetMidi + transpose;
|
||||
let noteClef = clef;
|
||||
if (clef === 'grand') noteClef = getGrandStaffClef(visualMidi);
|
||||
const data = getNoteInKey(visualMidi, keySignature);
|
||||
|
||||
const ghost = new GhostNote({ keys: data.keys, duration: "h", clef: noteClef as 'treble' | 'bass' });
|
||||
ghost.addModifier(new Annotation("?").setVerticalJustification(Annotation.VerticalJustify.CENTER));
|
||||
targetHalfObj = { note: ghost, clef: noteClef };
|
||||
} else {
|
||||
targetHalfObj = createStaveNote(targetMidi, "h", 'target');
|
||||
}
|
||||
|
||||
// If Grand Staff: Notes might be on DIFFERENT staves.
|
||||
// We need to group notes by Stave.
|
||||
|
||||
Reference in New Issue
Block a user