mirror of
https://github.com/9x/sheetmusictrainer.git
synced 2026-09-02 09:34:33 +02:00
tuner (buggy?)
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
} from './music/Tunings';
|
||||
import { INSTRUMENT_DEFINITIONS } from './music/InstrumentConfigs';
|
||||
import { FretboardHint } from './components/FretboardHint';
|
||||
import { TuningMeter } from './components/TuningMeter';
|
||||
import { Mic, MicOff, SkipForward, HelpCircle } from 'lucide-react';
|
||||
import './App.css';
|
||||
import './styles/skip-button.css';
|
||||
@@ -27,6 +28,7 @@ function App() {
|
||||
const [settings, setSettings] = useState<AppSettings>({
|
||||
difficulty: 'first_pos',
|
||||
showHint: false,
|
||||
showTuningMeter: false,
|
||||
tuningId: 'standard',
|
||||
keySignature: 'C',
|
||||
instrument: 'guitar',
|
||||
@@ -233,6 +235,9 @@ function App() {
|
||||
{listening ? <Mic size={28} /> : <MicOff size={28} />}
|
||||
</button>
|
||||
|
||||
{settings.showTuningMeter && pitchData ? (
|
||||
<TuningMeter cents={pitchData.cents} noteName={pitchData.note} />
|
||||
) : (
|
||||
<div className={`pitch-readout ${pitchData ? 'active' : ''}`}>
|
||||
{pitchData ? (
|
||||
<>
|
||||
@@ -245,6 +250,7 @@ function App() {
|
||||
<span className="placeholder">{listening ? "Listening..." : "Mic Off"}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Settings, Guitar, Music } from 'lucide-react';
|
||||
import { Settings, Guitar, Music, Gauge } from 'lucide-react';
|
||||
import { TUNINGS, INSTRUMENT_TUNINGS } from '../music/Tunings';
|
||||
import { INSTRUMENT_DEFINITIONS } from '../music/InstrumentConfigs';
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface AppSettings {
|
||||
tuningId: string;
|
||||
keySignature: string;
|
||||
instrument: string;
|
||||
showTuningMeter: boolean;
|
||||
rhythm: RhythmSettings;
|
||||
}
|
||||
|
||||
@@ -227,7 +228,19 @@ export const Controls: React.FC<ControlsProps> = ({ settings, onUpdateSettings }
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="control-group" style={{ borderTop: '1px solid rgba(255,255,255,0.1)', paddingTop: '12px', marginTop: '12px', width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<label className="control-label" style={{ marginBottom: 0 }}>
|
||||
<Gauge size={18} />
|
||||
<span>Tuning Meter</span>
|
||||
</label>
|
||||
<button
|
||||
className={`switch-button ${settings.showTuningMeter ? 'active' : ''}`}
|
||||
onClick={() => onUpdateSettings({ ...settings, showTuningMeter: !settings.showTuningMeter })}
|
||||
title={settings.showTuningMeter ? "Hide Tuner" : "Show Tuner"}
|
||||
>
|
||||
<div className="switch-thumb" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
118
src/components/TuningMeter.css
Normal file
118
src/components/TuningMeter.css
Normal file
@@ -0,0 +1,118 @@
|
||||
.tuning-meter-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 260px;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(4px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.meter-scale {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tick {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
font-size: 10px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.tick-left {
|
||||
left: 10%;
|
||||
}
|
||||
|
||||
.tick-center {
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.tick-right {
|
||||
left: 90%;
|
||||
}
|
||||
|
||||
.center-line {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translateX(-50%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.meter-needle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
background: white;
|
||||
transform: translateX(-50%);
|
||||
transition: left 0.1s ease-out;
|
||||
z-index: 2;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 8px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.meter-needle.perfect {
|
||||
background-color: #4ade80;
|
||||
box-shadow: 0 0 10px #4ade80;
|
||||
}
|
||||
|
||||
/* Tailwind Green-400 */
|
||||
.meter-needle.good {
|
||||
background-color: #86efac;
|
||||
}
|
||||
|
||||
/* Tailwind Green-300 */
|
||||
.meter-needle.okay {
|
||||
background-color: #facc15;
|
||||
}
|
||||
|
||||
/* Tailwind Yellow-400 */
|
||||
.meter-needle.poor {
|
||||
background-color: #f87171;
|
||||
}
|
||||
|
||||
/* Tailwind Red-400 */
|
||||
|
||||
.tuning-readout {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.tuning-readout .note-name {
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tuning-readout .cents-value {
|
||||
font-size: 0.9rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.tuning-readout .cents-value.perfect {
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.tuning-readout .cents-value.okay {
|
||||
color: #facc15;
|
||||
}
|
||||
|
||||
.tuning-readout .cents-value.poor {
|
||||
color: #f87171;
|
||||
}
|
||||
58
src/components/TuningMeter.tsx
Normal file
58
src/components/TuningMeter.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
|
||||
import React from 'react';
|
||||
import './TuningMeter.css';
|
||||
|
||||
interface TuningMeterProps {
|
||||
cents: number; // Deviation in cents from nearest note (-50 to +50 usually)
|
||||
noteName: string; // The detected note name (e.g. "A4")
|
||||
}
|
||||
|
||||
export const TuningMeter: React.FC<TuningMeterProps> = ({ cents, noteName }) => {
|
||||
// Clamp cents for display (-50 to 50)
|
||||
const clampedCents = Math.max(-50, Math.min(50, cents));
|
||||
|
||||
// Map -50..50 to 0..100% for position
|
||||
// -50 cents -> 0%
|
||||
// 0 cents -> 50%
|
||||
// 50 cents -> 100%
|
||||
const positionPercent = ((clampedCents + 50) / 100) * 100;
|
||||
|
||||
// Color determination
|
||||
let statusClass = 'neutral';
|
||||
if (Math.abs(cents) < 5) {
|
||||
statusClass = 'perfect'; // Green
|
||||
} else if (Math.abs(cents) < 15) {
|
||||
statusClass = 'good'; // Green-ish
|
||||
} else if (Math.abs(cents) < 30) {
|
||||
statusClass = 'okay'; // Yellow
|
||||
} else {
|
||||
statusClass = 'poor'; // Red
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tuning-meter-container">
|
||||
<div className="meter-scale">
|
||||
<div className="tick tick-left">-50</div>
|
||||
<div className="tick tick-center">0</div>
|
||||
<div className="tick tick-right">+50</div>
|
||||
|
||||
{/* Center Marker Line */}
|
||||
<div className="center-line"></div>
|
||||
|
||||
{/* The Needle/Indicator */}
|
||||
<div
|
||||
className={`meter-needle ${statusClass}`}
|
||||
style={{ left: `${positionPercent}%` }}
|
||||
>
|
||||
<div className="needle-head"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="tuning-readout">
|
||||
<span className="note-name">{noteName}</span>
|
||||
<span className={`cents-value ${statusClass}`}>
|
||||
{cents > 0 ? '+' : ''}{Math.round(cents)} ct
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { PitchAnalyzer } from '../audio/PitchAnalyzer';
|
||||
import { frequencyToMidi, getNoteDetails } from '../music/NoteUtils';
|
||||
import { frequencyToMidi, getNoteDetails, getCentDifference } from '../music/NoteUtils';
|
||||
|
||||
interface PitchData {
|
||||
frequency: number;
|
||||
@@ -28,11 +28,13 @@ export function usePitchDetector(active: boolean) {
|
||||
// Calculate cents off purely for display if needed,
|
||||
// but for training we usually care if midi matches.
|
||||
|
||||
const cents = getCentDifference(freq, midi);
|
||||
|
||||
setPitchData({
|
||||
frequency: freq,
|
||||
midi,
|
||||
note: scientific,
|
||||
cents: 0,
|
||||
cents,
|
||||
clarity: 1
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user