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>({
|
const [settings, setSettings] = useState<AppSettings>({
|
||||||
difficulty: 'first_pos',
|
difficulty: 'first_pos',
|
||||||
showHint: false,
|
showHint: false,
|
||||||
tuningId: 'standard'
|
tuningId: 'standard',
|
||||||
|
keySignature: 'C'
|
||||||
});
|
});
|
||||||
|
|
||||||
const [streak, setStreak] = useState(0);
|
const [streak, setStreak] = useState(0);
|
||||||
@@ -122,6 +123,7 @@ function App() {
|
|||||||
<SheetMusic
|
<SheetMusic
|
||||||
targetMidi={targetMidi}
|
targetMidi={targetMidi}
|
||||||
playedMidi={pitchData?.midi}
|
playedMidi={pitchData?.midi}
|
||||||
|
keySignature={settings.keySignature}
|
||||||
width={Math.min(window.innerWidth - 40, 500)}
|
width={Math.min(window.innerWidth - 40, 500)}
|
||||||
height={250}
|
height={250}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface AppSettings {
|
|||||||
difficulty: Difficulty;
|
difficulty: Difficulty;
|
||||||
showHint: boolean;
|
showHint: boolean;
|
||||||
tuningId: string;
|
tuningId: string;
|
||||||
|
keySignature: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ControlsProps {
|
interface ControlsProps {
|
||||||
@@ -63,6 +64,34 @@ export const Controls: React.FC<ControlsProps> = ({ settings, onUpdateSettings }
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</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
|
<button
|
||||||
className={`control-button ${settings.showHint ? 'active' : ''}`}
|
className={`control-button ${settings.showHint ? 'active' : ''}`}
|
||||||
onClick={toggleHint}
|
onClick={toggleHint}
|
||||||
@@ -73,3 +102,4 @@ export const Controls: React.FC<ControlsProps> = ({ settings, onUpdateSettings }
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { Renderer, Stave, StaveNote, Accidental, Voice, Formatter } from 'vexflow';
|
import { Renderer, Stave, StaveNote, Accidental, Voice, Formatter } from 'vexflow';
|
||||||
import { getNoteDetails } from '../music/NoteUtils';
|
import { getNoteInKey } from '../music/NoteUtils';
|
||||||
|
|
||||||
|
|
||||||
interface SheetMusicProps {
|
interface SheetMusicProps {
|
||||||
targetMidi: number;
|
targetMidi: number;
|
||||||
@@ -9,6 +10,7 @@ interface SheetMusicProps {
|
|||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
transpose?: number; // Transposition in semitones for visualization (e.g., +12 for guitar)
|
transpose?: number; // Transposition in semitones for visualization (e.g., +12 for guitar)
|
||||||
|
keySignature?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SheetMusic: React.FC<SheetMusicProps> = ({
|
export const SheetMusic: React.FC<SheetMusicProps> = ({
|
||||||
@@ -17,7 +19,8 @@ export const SheetMusic: React.FC<SheetMusicProps> = ({
|
|||||||
clef = 'treble',
|
clef = 'treble',
|
||||||
width = 300,
|
width = 300,
|
||||||
height = 200,
|
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);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -35,16 +38,13 @@ export const SheetMusic: React.FC<SheetMusicProps> = ({
|
|||||||
// Create Stave
|
// Create Stave
|
||||||
const stave = new Stave(10, 40, width - 20);
|
const stave = new Stave(10, 40, width - 20);
|
||||||
stave.addClef(clef);
|
stave.addClef(clef);
|
||||||
|
stave.addKeySignature(keySignature);
|
||||||
stave.setContext(context).draw();
|
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 getVexFlowKey = (midi: number) => {
|
||||||
const visualMidi = midi + transpose;
|
const visualMidi = midi + transpose;
|
||||||
const details = getNoteDetails(visualMidi);
|
return getNoteInKey(visualMidi, keySignature);
|
||||||
return {
|
|
||||||
keys: [`${details.name.toLowerCase()}/${details.octave}`],
|
|
||||||
hasAccidental: details.name.includes("#")
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create Target Note
|
// Create Target Note
|
||||||
@@ -56,8 +56,8 @@ export const SheetMusic: React.FC<SheetMusicProps> = ({
|
|||||||
clef: clef
|
clef: clef
|
||||||
});
|
});
|
||||||
|
|
||||||
if (targetData.hasAccidental) {
|
if (targetData.accidental) {
|
||||||
targetStaveNote.addModifier(new Accidental("#"));
|
targetStaveNote.addModifier(new Accidental(targetData.accidental));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playedMidi) {
|
if (playedMidi) {
|
||||||
@@ -75,7 +75,7 @@ export const SheetMusic: React.FC<SheetMusicProps> = ({
|
|||||||
duration: "h",
|
duration: "h",
|
||||||
clef: clef
|
clef: clef
|
||||||
});
|
});
|
||||||
if (targetData.hasAccidental) targetNoteHalf.addModifier(new Accidental("#"));
|
if (targetData.accidental) targetNoteHalf.addModifier(new Accidental(targetData.accidental));
|
||||||
|
|
||||||
|
|
||||||
if (playedMidi === targetMidi) {
|
if (playedMidi === targetMidi) {
|
||||||
@@ -84,8 +84,8 @@ export const SheetMusic: React.FC<SheetMusicProps> = ({
|
|||||||
playedStaveNote.setStyle({ fillStyle: "var(--color-error)", strokeStyle: "var(--color-error)" });
|
playedStaveNote.setStyle({ fillStyle: "var(--color-error)", strokeStyle: "var(--color-error)" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playedData.hasAccidental) {
|
if (playedData.accidental) {
|
||||||
playedStaveNote.addModifier(new Accidental("#"));
|
playedStaveNote.addModifier(new Accidental(playedData.accidental));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use camelCase properties as fixed previously
|
// Use camelCase properties as fixed previously
|
||||||
@@ -102,7 +102,7 @@ export const SheetMusic: React.FC<SheetMusicProps> = ({
|
|||||||
voice.draw(context, stave);
|
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" />;
|
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;
|
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
|
export const KEY_SIGNATURES: { [key: string]: { name: string, accidentals: string[] } } = {
|
||||||
// But usually we work with MIDI.
|
'C': { name: 'C Major', accidentals: [] },
|
||||||
// This is just a note for logic: Guitar Sounding = Written - 12.
|
'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