key signatures working

This commit is contained in:
2025-12-21 17:53:26 +01:00
parent f00a3669b4
commit 8caf775fe7
4 changed files with 155 additions and 19 deletions

View File

@@ -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}
/>

View File

@@ -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>
);
};

View File

@@ -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" />;
};

View File

@@ -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
};
}