mirror of
https://github.com/9x/sheetmusictrainer.git
synced 2026-09-02 09:34:33 +02:00
key signatures working
This commit is contained in:
@@ -26,7 +26,8 @@ function App() {
|
||||
const [settings, setSettings] = useState<AppSettings>({
|
||||
difficulty: 'first_pos',
|
||||
showHint: false,
|
||||
tuningId: 'standard'
|
||||
tuningId: 'standard',
|
||||
keySignature: 'C'
|
||||
});
|
||||
|
||||
const [streak, setStreak] = useState(0);
|
||||
@@ -122,6 +123,7 @@ function App() {
|
||||
<SheetMusic
|
||||
targetMidi={targetMidi}
|
||||
playedMidi={pitchData?.midi}
|
||||
keySignature={settings.keySignature}
|
||||
width={Math.min(window.innerWidth - 40, 500)}
|
||||
height={250}
|
||||
/>
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface AppSettings {
|
||||
difficulty: Difficulty;
|
||||
showHint: boolean;
|
||||
tuningId: string;
|
||||
keySignature: string;
|
||||
}
|
||||
|
||||
interface ControlsProps {
|
||||
@@ -63,6 +64,34 @@ export const Controls: React.FC<ControlsProps> = ({ settings, onUpdateSettings }
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="control-group">
|
||||
<label className="control-label">
|
||||
<span>Key Signature</span>
|
||||
</label>
|
||||
<select
|
||||
value={settings.keySignature}
|
||||
onChange={(e) => onUpdateSettings({ ...settings, keySignature: e.target.value })}
|
||||
className="control-select"
|
||||
>
|
||||
<optgroup label="Major Keys">
|
||||
<option value="C">C Major</option>
|
||||
<option value="G">G Major</option>
|
||||
<option value="D">D Major</option>
|
||||
<option value="A">A Major</option>
|
||||
<option value="E">E Major</option>
|
||||
<option value="F">F Major</option>
|
||||
<option value="Bb">Bb Major</option>
|
||||
<option value="Eb">Eb Major</option>
|
||||
</optgroup>
|
||||
<optgroup label="Minor Keys">
|
||||
<option value="Am">A Minor</option>
|
||||
<option value="Em">E Minor</option>
|
||||
<option value="Dm">D Minor</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={`control-button ${settings.showHint ? 'active' : ''}`}
|
||||
onClick={toggleHint}
|
||||
@@ -73,3 +102,4 @@ export const Controls: React.FC<ControlsProps> = ({ settings, onUpdateSettings }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Renderer, Stave, StaveNote, Accidental, Voice, Formatter } from 'vexflow';
|
||||
import { getNoteDetails } from '../music/NoteUtils';
|
||||
import { getNoteInKey } from '../music/NoteUtils';
|
||||
|
||||
|
||||
interface SheetMusicProps {
|
||||
targetMidi: number;
|
||||
@@ -9,6 +10,7 @@ interface SheetMusicProps {
|
||||
width?: number;
|
||||
height?: number;
|
||||
transpose?: number; // Transposition in semitones for visualization (e.g., +12 for guitar)
|
||||
keySignature?: string;
|
||||
}
|
||||
|
||||
export const SheetMusic: React.FC<SheetMusicProps> = ({
|
||||
@@ -17,7 +19,8 @@ export const SheetMusic: React.FC<SheetMusicProps> = ({
|
||||
clef = 'treble',
|
||||
width = 300,
|
||||
height = 200,
|
||||
transpose = 12 // Default to +1 octave (Guitar Notation)
|
||||
transpose = 12, // Default to +1 octave (Guitar Notation)
|
||||
keySignature = 'C'
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -35,16 +38,13 @@ export const SheetMusic: React.FC<SheetMusicProps> = ({
|
||||
// Create Stave
|
||||
const stave = new Stave(10, 40, width - 20);
|
||||
stave.addClef(clef);
|
||||
stave.addKeySignature(keySignature);
|
||||
stave.setContext(context).draw();
|
||||
|
||||
// Helper to create keys for VexFlow
|
||||
// Helper to create keys for VexFlow using key signature logic
|
||||
const getVexFlowKey = (midi: number) => {
|
||||
const visualMidi = midi + transpose;
|
||||
const details = getNoteDetails(visualMidi);
|
||||
return {
|
||||
keys: [`${details.name.toLowerCase()}/${details.octave}`],
|
||||
hasAccidental: details.name.includes("#")
|
||||
};
|
||||
return getNoteInKey(visualMidi, keySignature);
|
||||
};
|
||||
|
||||
// Create Target Note
|
||||
@@ -56,8 +56,8 @@ export const SheetMusic: React.FC<SheetMusicProps> = ({
|
||||
clef: clef
|
||||
});
|
||||
|
||||
if (targetData.hasAccidental) {
|
||||
targetStaveNote.addModifier(new Accidental("#"));
|
||||
if (targetData.accidental) {
|
||||
targetStaveNote.addModifier(new Accidental(targetData.accidental));
|
||||
}
|
||||
|
||||
if (playedMidi) {
|
||||
@@ -75,7 +75,7 @@ export const SheetMusic: React.FC<SheetMusicProps> = ({
|
||||
duration: "h",
|
||||
clef: clef
|
||||
});
|
||||
if (targetData.hasAccidental) targetNoteHalf.addModifier(new Accidental("#"));
|
||||
if (targetData.accidental) targetNoteHalf.addModifier(new Accidental(targetData.accidental));
|
||||
|
||||
|
||||
if (playedMidi === targetMidi) {
|
||||
@@ -84,8 +84,8 @@ export const SheetMusic: React.FC<SheetMusicProps> = ({
|
||||
playedStaveNote.setStyle({ fillStyle: "var(--color-error)", strokeStyle: "var(--color-error)" });
|
||||
}
|
||||
|
||||
if (playedData.hasAccidental) {
|
||||
playedStaveNote.addModifier(new Accidental("#"));
|
||||
if (playedData.accidental) {
|
||||
playedStaveNote.addModifier(new Accidental(playedData.accidental));
|
||||
}
|
||||
|
||||
// Use camelCase properties as fixed previously
|
||||
@@ -102,7 +102,7 @@ export const SheetMusic: React.FC<SheetMusicProps> = ({
|
||||
voice.draw(context, stave);
|
||||
}
|
||||
|
||||
}, [targetMidi, playedMidi, clef, width, height, transpose]);
|
||||
}, [targetMidi, playedMidi, clef, width, height, transpose, keySignature]);
|
||||
|
||||
return <div ref={containerRef} className="sheet-music-container" />;
|
||||
};
|
||||
|
||||
@@ -50,7 +50,111 @@ export function getRandomNote(min: number, max: number, validNotes?: number[]):
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
// Helper to convert Written note to Sounding note for guitar
|
||||
// e.g. Input: "C4" (Written) -> Returns C3 (Sounding) MIDI
|
||||
// But usually we work with MIDI.
|
||||
// This is just a note for logic: Guitar Sounding = Written - 12.
|
||||
|
||||
export const KEY_SIGNATURES: { [key: string]: { name: string, accidentals: string[] } } = {
|
||||
'C': { name: 'C Major', accidentals: [] },
|
||||
'G': { name: 'G Major', accidentals: ['F#'] },
|
||||
'D': { name: 'D Major', accidentals: ['F#', 'C#'] },
|
||||
'A': { name: 'A Major', accidentals: ['F#', 'C#', 'G#'] },
|
||||
'E': { name: 'E Major', accidentals: ['F#', 'C#', 'G#', 'D#'] },
|
||||
'F': { name: 'F Major', accidentals: ['Bb'] },
|
||||
'Bb': { name: 'Bb Major', accidentals: ['Bb', 'Eb'] },
|
||||
'Eb': { name: 'Eb Major', accidentals: ['Bb', 'Eb', 'Ab'] },
|
||||
'Am': { name: 'A Minor', accidentals: [] },
|
||||
'Em': { name: 'E Minor', accidentals: ['F#'] },
|
||||
'Dm': { name: 'D Minor', accidentals: ['Bb'] },
|
||||
};
|
||||
|
||||
// Map of midi values to their "natural" note names (0=C, 1=C#/Db, etc.)
|
||||
// We use this to decide if we need a sharp, flat, or natural based on the key.
|
||||
|
||||
// 0=C, 1=C#, 2=D, 3=D#, 4=E, 5=F, 6=F#, 7=G, 8=G#, 9=A, 10=A#, 11=B
|
||||
// But wait, VexFlow needs specific spelling.
|
||||
// Let's rely on standard theory:
|
||||
// Key of G (F#): If we have F# (midi 6), it's consistent. If we have F (midi 5), it's a natural.
|
||||
|
||||
/**
|
||||
* Specs for a note to be rendered by VexFlow
|
||||
*/
|
||||
export interface VexFlowNoteSpec {
|
||||
keys: string[]; // e.g., ["c/4"]
|
||||
accidental?: string; // "#", "b", "n" (natural), etc.
|
||||
}
|
||||
|
||||
export function getNoteInKey(midi: number, keySignature: string): VexFlowNoteSpec {
|
||||
// 1. Get fundamental details
|
||||
// We default to Sharps for sharp keys, Flats for flat keys?
|
||||
// Simplified logic:
|
||||
// C Major: Sharps for chromatics? Usually yes.
|
||||
|
||||
// Let's determine the step and accidental.
|
||||
// 60 = C4.
|
||||
|
||||
const keySpec = KEY_SIGNATURES[keySignature] || KEY_SIGNATURES['C'];
|
||||
const accList = keySpec.accidentals;
|
||||
|
||||
// Determine if key is "sharp-y" or "flat-y"
|
||||
const isFlatKey = accList.some(a => a.includes('b')) || keySignature === 'F' || keySignature.includes('b');
|
||||
|
||||
// Basic chromatic map preference
|
||||
const NOTE_NAMES_SHARP = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
|
||||
const NOTE_NAMES_FLAT = ["C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"];
|
||||
|
||||
const preferredNames = isFlatKey ? NOTE_NAMES_FLAT : NOTE_NAMES_SHARP;
|
||||
|
||||
const semitone = midi % 12;
|
||||
const octave = Math.floor(midi / 12) - 1;
|
||||
const rawName = preferredNames[semitone]; // e.g., "F#" or "Gb" or "C"
|
||||
|
||||
// Check if this note is in the key signature
|
||||
// In G Major: F# is in key. F is NOT.
|
||||
// In start keys (C, Am), no accidentals in key.
|
||||
|
||||
// Parse the rawName: "F#" -> step "F", mod "#"
|
||||
let step = rawName.charAt(0);
|
||||
let mod = rawName.length > 1 ? rawName.charAt(1) : "";
|
||||
|
||||
// VexFlow Key Format: "c/4", "f/4", "fb/4"
|
||||
// We need to return the accidental to *display*.
|
||||
// Rule:
|
||||
// If note is in key signature (e.g. F# in G Major) -> Show nothing (implied).
|
||||
// If note is NOT in key signature:
|
||||
// - If it's natural but key has accidental (e.g. F natural in G Major) -> Show Natural.
|
||||
// - If it has accidental and matches key (e.g. F# in G Major) -> Show nothing.
|
||||
// - If it has accidental and doesn't match key (e.g. C# in G Major) -> Show #.
|
||||
// - If it's natural and key is natural (e.g. C in C Major) -> Show nothing.
|
||||
|
||||
// Let's find what the key signature expects for this step.
|
||||
// Iterate accidentals in key to find if this step is modified in key.
|
||||
// e.g. G Major has F#. So for step 'F', expected is '#'. For 'C', expected is ''.
|
||||
|
||||
let expectedMod = "";
|
||||
for (const acc of accList) {
|
||||
if (acc.startsWith(step)) {
|
||||
expectedMod = acc.substring(1); // "#" or "b"
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let displayAccidental = undefined;
|
||||
|
||||
if (mod === expectedMod) {
|
||||
// Matches key signature -> No symbol needed
|
||||
displayAccidental = undefined;
|
||||
} else {
|
||||
// Diverges from key signature
|
||||
if (mod === "") {
|
||||
// Note is natural, but key expects accidental (e.g. F in G Major)
|
||||
displayAccidental = "n";
|
||||
} else {
|
||||
// Note has accidental (e.g. F# in C Major, or F# in F Major)
|
||||
displayAccidental = mod;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
keys: [`${step.toLowerCase()}${mod}/${octave}`], // Vexflow key: "c/4", "c#/4", "db/4"
|
||||
accidental: displayAccidental
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user