Improve chord/note detection, add tuning persistence/UI
Enhanced AudioAnalyzer with bass chroma, integrated chroma, and chord frame accumulation for more robust and stable chord detection. Added spectral peak note estimation and note continuity logic. Refined chord scoring and introduced chord lock mechanism to prevent rapid switching. Added methods for tuning reset, set, and snapshot retrieval. Updated SensitivityProfile with chord lock parameters. In Game1, added keyboard shortcuts and JSON persistence for detection tuning profiles. Improved HUD with confidence bars and new indicators. These changes increase detection accuracy, stability, and user customization.
This commit is contained in:
@@ -20,6 +20,11 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
private readonly float[] _spectrumAccumulator;
|
||||
private readonly int[] _spectrumAccumulatorCounts;
|
||||
private readonly float[] _chroma = new float[12];
|
||||
private readonly float[] _bassChroma = new float[12];
|
||||
private readonly float[] _integratedChroma = new float[12];
|
||||
private readonly float[] _integratedBassChroma = new float[12];
|
||||
private readonly float[] _chordChromaSum = new float[12];
|
||||
private readonly float[] _chordBassSum = new float[12];
|
||||
private readonly float[] _noteVoteScores = new float[12];
|
||||
private readonly float[] _chordVoteScores = new float[12 * ChordTypeCount];
|
||||
private readonly float[][] _spectrumRefsByMode;
|
||||
@@ -48,7 +53,10 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
private readonly Queue<float> _beatIntervals = new();
|
||||
private readonly Queue<NoteVote> _noteVotes = new();
|
||||
private readonly Queue<ChordVote> _chordVotes = new();
|
||||
private readonly Queue<ChordFrame> _chordFrames = new();
|
||||
private const float TemporalVoteWindowSeconds = 0.30f;
|
||||
private const float ChordIntegrationWindowSeconds = 0.34f;
|
||||
private const int MaxNotePeakCandidates = 5;
|
||||
private const int MaxBeatIntervals = 8;
|
||||
private const int ChordTypeCount = 9;
|
||||
private readonly float[] _chordHarmonicityOffsetByMode = new float[2];
|
||||
@@ -58,9 +66,12 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
private int _lastChordRoot = -1;
|
||||
private int _lastChordType = -1;
|
||||
private float _lastChordChangeTimeSeconds;
|
||||
private float _chordLockUntilSeconds;
|
||||
private int _stableNoteIndex = -1;
|
||||
private float _stableNoteStrength;
|
||||
private float _lastNoteChangeTimeSeconds;
|
||||
private int _lastRawNoteIndex = -1;
|
||||
private float _lastRawNoteChangeTimeSeconds;
|
||||
private bool _noteIsActive;
|
||||
private int _activeNoteIndex = -1;
|
||||
private readonly float[] _pitchSamples;
|
||||
@@ -198,6 +209,31 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
AdjustTuning(mode, _chordVoteConsensusOffsetByMode, delta, -0.35f, 0.35f);
|
||||
}
|
||||
|
||||
public void ResetTuning(AudioInputMode mode)
|
||||
{
|
||||
int modeIndex = mode == AudioInputMode.Microphone ? 0 : 1;
|
||||
lock (_tuningSync)
|
||||
{
|
||||
_chordHarmonicityOffsetByMode[modeIndex] = 0f;
|
||||
_chordSeparationOffsetByMode[modeIndex] = 0f;
|
||||
_noteVoteConsensusOffsetByMode[modeIndex] = 0f;
|
||||
_chordVoteConsensusOffsetByMode[modeIndex] = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetTuning(AudioInputMode mode, DetectionTuningSnapshot snapshot)
|
||||
{
|
||||
int modeIndex = mode == AudioInputMode.Microphone ? 0 : 1;
|
||||
SensitivityProfile baseProfile = mode == AudioInputMode.Microphone ? SensitivityProfile.Microphone : SensitivityProfile.SystemOutput;
|
||||
lock (_tuningSync)
|
||||
{
|
||||
_chordHarmonicityOffsetByMode[modeIndex] = Clamp(snapshot.ChordHarmonicityThreshold - baseProfile.ChordHarmonicityThreshold, -0.35f, 0.35f);
|
||||
_chordSeparationOffsetByMode[modeIndex] = Clamp(snapshot.ChordSeparationThreshold - baseProfile.ChordSeparationThreshold, -0.10f, 0.20f);
|
||||
_noteVoteConsensusOffsetByMode[modeIndex] = Clamp(snapshot.NoteVoteConsensus - baseProfile.NoteVoteConsensus, -0.35f, 0.35f);
|
||||
_chordVoteConsensusOffsetByMode[modeIndex] = Clamp(snapshot.ChordVoteConsensus - baseProfile.ChordVoteConsensus, -0.35f, 0.35f);
|
||||
}
|
||||
}
|
||||
|
||||
private void AnalysisLoop(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
@@ -255,6 +291,7 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
float dominantMagnitude = 0f;
|
||||
float dominantFrequency = 0f;
|
||||
Array.Clear(_chroma);
|
||||
Array.Clear(_bassChroma);
|
||||
|
||||
for (int bin = 1; bin < half; bin++)
|
||||
{
|
||||
@@ -296,6 +333,10 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
if (pitchClass >= 0)
|
||||
{
|
||||
_chroma[pitchClass] += pitchMagnitude;
|
||||
if (frequency <= 260f)
|
||||
{
|
||||
_bassChroma[pitchClass] += pitchMagnitude;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -357,15 +398,34 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
float bpmInstant = CalculateBpm();
|
||||
_smoothedBpm = Smooth(_smoothedBpm, bpmInstant, 0.18f, 0.04f);
|
||||
|
||||
EstimateFundamentalFrequencyHps(sampleRate, _pitchFftBuffer, out float hpsFrequency, out float hpsMagnitude, out float harmonicity);
|
||||
float noteFrequency = hpsFrequency > 0f ? hpsFrequency : dominantFrequency;
|
||||
float noteMagnitude = hpsFrequency > 0f ? hpsMagnitude : dominantMagnitude;
|
||||
EstimateFundamentalFrequencyHps(sampleRate, _pitchFftBuffer, out float hpsFrequency, out float hpsMagnitude, out float hpsHarmonicity);
|
||||
EstimateNoteFromPeaks(sampleRate, _pitchFftBuffer, out float peakFrequency, out float peakStrength, out float peakHarmonicity);
|
||||
float noteFrequency;
|
||||
float noteMagnitude;
|
||||
float harmonicity;
|
||||
if (peakStrength >= hpsMagnitude * 0.82f && peakFrequency > 0f)
|
||||
{
|
||||
noteFrequency = peakFrequency;
|
||||
noteMagnitude = peakStrength;
|
||||
harmonicity = MathF.Max(peakHarmonicity, hpsHarmonicity * 0.85f);
|
||||
}
|
||||
else
|
||||
{
|
||||
noteFrequency = hpsFrequency > 0f ? hpsFrequency : dominantFrequency;
|
||||
noteMagnitude = hpsFrequency > 0f ? hpsMagnitude : dominantMagnitude;
|
||||
harmonicity = hpsHarmonicity;
|
||||
}
|
||||
|
||||
int noteIndex = GetNoteIndexFromFrequency(noteFrequency);
|
||||
float whistleBoost = GetWhistleBoost(noteFrequency);
|
||||
float dominantStrength = Clamp01(noteMagnitude * profile.NoteStrengthGain * (1f + whistleBoost * 0.65f) * (0.58f + (0.42f * harmonicity)));
|
||||
float dominantStrength = Clamp01(noteMagnitude * profile.NoteStrengthGain * (1f + whistleBoost * 0.65f) * (0.52f + (0.48f * harmonicity)));
|
||||
ApplyRawNoteContinuity(now, harmonicity, dominantStrength, ref noteIndex);
|
||||
|
||||
AccumulateChordFrames(now, _chroma, _bassChroma, MathF.Max(dominantStrength, _smoothedLevel * 0.75f));
|
||||
|
||||
DetectChord(
|
||||
_chroma,
|
||||
_integratedChroma,
|
||||
_integratedBassChroma,
|
||||
_smoothedLevel,
|
||||
profile,
|
||||
harmonicity,
|
||||
@@ -432,6 +492,18 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
_hpPrevInput = 0f;
|
||||
_hpPrevOutput = 0f;
|
||||
_lpPrevOutput = 0f;
|
||||
_lastRawNoteIndex = -1;
|
||||
_lastRawNoteChangeTimeSeconds = 0f;
|
||||
_chordLockUntilSeconds = 0f;
|
||||
while (_chordFrames.Count > 0)
|
||||
{
|
||||
_chordFrames.Dequeue();
|
||||
}
|
||||
|
||||
Array.Clear(_chordChromaSum);
|
||||
Array.Clear(_chordBassSum);
|
||||
Array.Clear(_integratedChroma);
|
||||
Array.Clear(_integratedBassChroma);
|
||||
}
|
||||
|
||||
private static float[] BuildHannWindow(int length)
|
||||
@@ -502,8 +574,46 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
return note;
|
||||
}
|
||||
|
||||
private void ApplyRawNoteContinuity(float now, float harmonicity, float noteStrength, ref int noteIndex)
|
||||
{
|
||||
if (noteIndex < 0)
|
||||
{
|
||||
if (noteStrength <= 0.04f)
|
||||
{
|
||||
_lastRawNoteIndex = -1;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_lastRawNoteIndex >= 0)
|
||||
{
|
||||
int dist = CircularSemitoneDistance(_lastRawNoteIndex, noteIndex);
|
||||
float sinceLast = now - _lastRawNoteChangeTimeSeconds;
|
||||
bool unstableJump = dist >= 5 && sinceLast < 0.20f && harmonicity < 0.44f && noteStrength < 0.28f;
|
||||
if (unstableJump)
|
||||
{
|
||||
noteIndex = _lastRawNoteIndex;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (_lastRawNoteIndex != noteIndex)
|
||||
{
|
||||
_lastRawNoteIndex = noteIndex;
|
||||
_lastRawNoteChangeTimeSeconds = now;
|
||||
}
|
||||
}
|
||||
|
||||
private static int CircularSemitoneDistance(int a, int b)
|
||||
{
|
||||
int d = Math.Abs(a - b) % 12;
|
||||
return Math.Min(d, 12 - d);
|
||||
}
|
||||
|
||||
private void DetectChord(
|
||||
float[] chroma,
|
||||
float[] bassChroma,
|
||||
float level,
|
||||
SensitivityProfile profile,
|
||||
float harmonicity,
|
||||
@@ -577,7 +687,7 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
{
|
||||
for (int chordType = 0; chordType < ChordTypeCount; chordType++)
|
||||
{
|
||||
float score = ScoreChord(chroma, r, chordType);
|
||||
float score = ScoreChord(chroma, bassChroma, r, chordType);
|
||||
if (score > bestScore)
|
||||
{
|
||||
secondBestScore = bestScore;
|
||||
@@ -604,7 +714,7 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
|
||||
if (_lastChordRoot >= 0 && _lastChordType >= 0)
|
||||
{
|
||||
float held = ScoreChord(chroma, _lastChordRoot, _lastChordType);
|
||||
float held = ScoreChord(chroma, bassChroma, _lastChordRoot, _lastChordType);
|
||||
if ((bestRoot != _lastChordRoot || bestType != _lastChordType) && held * 1.08f > bestScore)
|
||||
{
|
||||
bestRoot = _lastChordRoot;
|
||||
@@ -670,7 +780,186 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
harmonicity = Clamp01(((second + third + fourth) / 3f) / (fundamental + 1e-6f));
|
||||
}
|
||||
|
||||
private static float ScoreChord(float[] chroma, int root, int type)
|
||||
private void EstimateNoteFromPeaks(int sampleRate, Complex[] frequencyBuffer, out float frequencyHz, out float strength, out float harmonicity)
|
||||
{
|
||||
frequencyHz = 0f;
|
||||
strength = 0f;
|
||||
harmonicity = 0f;
|
||||
|
||||
int half = _fftSize / 2;
|
||||
int minBin = Math.Max(2, (int)(70f * _fftSize / sampleRate));
|
||||
int maxBin = Math.Min(half - 1, (int)(4200f * _fftSize / sampleRate));
|
||||
if (maxBin <= minBin + 3)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Span<int> candidateBins = stackalloc int[MaxNotePeakCandidates];
|
||||
Span<float> candidateScores = stackalloc float[MaxNotePeakCandidates];
|
||||
for (int i = 0; i < MaxNotePeakCandidates; i++)
|
||||
{
|
||||
candidateBins[i] = -1;
|
||||
candidateScores[i] = 0f;
|
||||
}
|
||||
|
||||
for (int bin = minBin + 1; bin < maxBin - 1; bin++)
|
||||
{
|
||||
float m = (float)frequencyBuffer[bin].Magnitude;
|
||||
if (m <= 1e-8f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float prev = (float)frequencyBuffer[bin - 1].Magnitude;
|
||||
float next = (float)frequencyBuffer[bin + 1].Magnitude;
|
||||
if (m < prev || m < next)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float h2 = (bin * 2 < half) ? (float)frequencyBuffer[bin * 2].Magnitude : 0f;
|
||||
float h3 = (bin * 3 < half) ? (float)frequencyBuffer[bin * 3].Magnitude : 0f;
|
||||
float localScore = m + (0.62f * h2) + (0.44f * h3);
|
||||
|
||||
int insert = -1;
|
||||
for (int i = 0; i < MaxNotePeakCandidates; i++)
|
||||
{
|
||||
if (localScore > candidateScores[i])
|
||||
{
|
||||
insert = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (insert < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int j = MaxNotePeakCandidates - 1; j > insert; j--)
|
||||
{
|
||||
candidateScores[j] = candidateScores[j - 1];
|
||||
candidateBins[j] = candidateBins[j - 1];
|
||||
}
|
||||
|
||||
candidateScores[insert] = localScore;
|
||||
candidateBins[insert] = bin;
|
||||
}
|
||||
|
||||
Span<float> classScores = stackalloc float[12];
|
||||
float bestCandidateScore = 0f;
|
||||
int bestBin = -1;
|
||||
float bestHarm = 0f;
|
||||
for (int i = 0; i < MaxNotePeakCandidates; i++)
|
||||
{
|
||||
int bin = candidateBins[i];
|
||||
if (bin < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float freq = (sampleRate * bin) / (float)_fftSize;
|
||||
int cls = GetNoteIndexFromFrequency(freq);
|
||||
if (cls < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float score = candidateScores[i];
|
||||
classScores[cls] += score;
|
||||
if (score > bestCandidateScore)
|
||||
{
|
||||
float m = (float)frequencyBuffer[bin].Magnitude;
|
||||
float h2 = (bin * 2 < half) ? (float)frequencyBuffer[bin * 2].Magnitude : 0f;
|
||||
float h3 = (bin * 3 < half) ? (float)frequencyBuffer[bin * 3].Magnitude : 0f;
|
||||
bestCandidateScore = score;
|
||||
bestBin = bin;
|
||||
bestHarm = Clamp01(((h2 + h3) * 0.5f) / (m + 1e-6f));
|
||||
}
|
||||
}
|
||||
|
||||
int bestClass = -1;
|
||||
float bestClassScore = 0f;
|
||||
for (int i = 0; i < classScores.Length; i++)
|
||||
{
|
||||
if (classScores[i] > bestClassScore)
|
||||
{
|
||||
bestClassScore = classScores[i];
|
||||
bestClass = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestBin < 0 || bestClass < 0 || bestClassScore <= 1e-8f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float bestClassFreq = (sampleRate * bestBin) / (float)_fftSize;
|
||||
frequencyHz = bestClassFreq;
|
||||
strength = bestClassScore;
|
||||
harmonicity = bestHarm;
|
||||
}
|
||||
|
||||
private void AccumulateChordFrames(float now, float[] chroma, float[] bassChroma, float weight)
|
||||
{
|
||||
float w = Clamp01(weight);
|
||||
float[] chromaFrame = new float[12];
|
||||
float[] bassFrame = new float[12];
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
chromaFrame[i] = chroma[i] * w;
|
||||
bassFrame[i] = bassChroma[i] * w;
|
||||
_chordChromaSum[i] += chromaFrame[i];
|
||||
_chordBassSum[i] += bassFrame[i];
|
||||
}
|
||||
|
||||
_chordFrames.Enqueue(new ChordFrame(now, chromaFrame, bassFrame));
|
||||
float cutoff = now - ChordIntegrationWindowSeconds;
|
||||
while (_chordFrames.Count > 0 && _chordFrames.Peek().Time < cutoff)
|
||||
{
|
||||
ChordFrame old = _chordFrames.Dequeue();
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
_chordChromaSum[i] -= old.Chroma[i];
|
||||
_chordBassSum[i] -= old.BassChroma[i];
|
||||
}
|
||||
}
|
||||
|
||||
float sum = 0f;
|
||||
float bassSum = 0f;
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
if (_chordChromaSum[i] < 0f)
|
||||
{
|
||||
_chordChromaSum[i] = 0f;
|
||||
}
|
||||
|
||||
if (_chordBassSum[i] < 0f)
|
||||
{
|
||||
_chordBassSum[i] = 0f;
|
||||
}
|
||||
|
||||
sum += _chordChromaSum[i];
|
||||
bassSum += _chordBassSum[i];
|
||||
}
|
||||
|
||||
if (sum <= 1e-8f || bassSum <= 1e-8f)
|
||||
{
|
||||
Array.Clear(_integratedChroma);
|
||||
Array.Clear(_integratedBassChroma);
|
||||
return;
|
||||
}
|
||||
|
||||
float inv = 1f / sum;
|
||||
float bassInv = 1f / bassSum;
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
_integratedChroma[i] = _chordChromaSum[i] * inv;
|
||||
_integratedBassChroma[i] = _chordBassSum[i] * bassInv;
|
||||
}
|
||||
}
|
||||
|
||||
private static float ScoreChord(float[] chroma, float[] bassChroma, int root, int type)
|
||||
{
|
||||
int i0 = root;
|
||||
int i1 = root;
|
||||
@@ -724,12 +1013,26 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
break;
|
||||
}
|
||||
|
||||
float score = chroma[i0] * 1.00f + chroma[i1] * 0.92f + chroma[i2] * 0.84f;
|
||||
float rootPresence = chroma[i0];
|
||||
float thirdPresence = chroma[i1];
|
||||
float fifthPresence = chroma[i2];
|
||||
float supportingPresence = MathF.Max(thirdPresence, fifthPresence);
|
||||
float primary = (rootPresence * 0.72f) + (supportingPresence * 0.28f);
|
||||
float triadBlend = (rootPresence * 0.45f) + (thirdPresence * 0.30f) + (fifthPresence * 0.25f);
|
||||
|
||||
// Lower root reliance slightly so first/second inversions still score well.
|
||||
float score = (primary * 0.82f) + (triadBlend * 1.05f);
|
||||
if (i3 >= 0)
|
||||
{
|
||||
score += chroma[i3] * 0.72f;
|
||||
}
|
||||
|
||||
// Bass chroma helps prefer plausible roots for piano voicings and slash chords.
|
||||
float bassRoot = bassChroma[i0];
|
||||
float bassThird = bassChroma[i1];
|
||||
float bassFifth = bassChroma[i2];
|
||||
score += (bassRoot * 0.26f) + (MathF.Max(bassThird, bassFifth) * 0.16f);
|
||||
|
||||
int b2 = (root + 1) % 12;
|
||||
int tritone = (root + 6) % 12;
|
||||
int clash = (root + 11) % 12;
|
||||
@@ -1079,14 +1382,20 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
if (chordRoot >= 0 && chordConfidence >= profile.ChordConfidenceThreshold)
|
||||
{
|
||||
bool changed = _lastChordRoot != chordRoot || _lastChordType != chordType;
|
||||
bool canSwitch = (now - _lastChordChangeTimeSeconds) >= profile.SwitchHoldSeconds;
|
||||
if (!changed || canSwitch || chordConfidence > stableChordConfidence * 1.2f)
|
||||
bool lockActive = now < _chordLockUntilSeconds;
|
||||
bool canSwitch = !lockActive && (now - _lastChordChangeTimeSeconds) >= profile.SwitchHoldSeconds;
|
||||
bool strongOverride = chordConfidence > stableChordConfidence * 1.35f;
|
||||
if (!changed || canSwitch || strongOverride)
|
||||
{
|
||||
_lastChordRoot = chordRoot;
|
||||
_lastChordType = chordType;
|
||||
if (changed)
|
||||
{
|
||||
_lastChordChangeTimeSeconds = now;
|
||||
if (noteStrength >= profile.ChordLockNoteThreshold)
|
||||
{
|
||||
_chordLockUntilSeconds = now + profile.ChordLockSeconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1101,6 +1410,7 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
{
|
||||
_lastChordRoot = -1;
|
||||
_lastChordType = -1;
|
||||
_chordLockUntilSeconds = 0f;
|
||||
stableChordRoot = -1;
|
||||
stableChordType = -1;
|
||||
stableChordConfidence = 0f;
|
||||
@@ -1146,6 +1456,8 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
float chordConfidenceThreshold,
|
||||
float noteOnsetThreshold,
|
||||
float noteReleaseThreshold,
|
||||
float chordLockNoteThreshold,
|
||||
float chordLockSeconds,
|
||||
float chordLevelThreshold,
|
||||
float chordHarmonicityThreshold,
|
||||
float chordPeakRatioThreshold,
|
||||
@@ -1172,6 +1484,8 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
public float ChordConfidenceThreshold { get; } = chordConfidenceThreshold;
|
||||
public float NoteOnsetThreshold { get; } = noteOnsetThreshold;
|
||||
public float NoteReleaseThreshold { get; } = noteReleaseThreshold;
|
||||
public float ChordLockNoteThreshold { get; } = chordLockNoteThreshold;
|
||||
public float ChordLockSeconds { get; } = chordLockSeconds;
|
||||
public float ChordLevelThreshold { get; } = chordLevelThreshold;
|
||||
public float ChordHarmonicityThreshold { get; } = chordHarmonicityThreshold;
|
||||
public float ChordPeakRatioThreshold { get; } = chordPeakRatioThreshold;
|
||||
@@ -1199,6 +1513,8 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
chordConfidenceThreshold: 0.18f,
|
||||
noteOnsetThreshold: 0.20f,
|
||||
noteReleaseThreshold: 0.08f,
|
||||
chordLockNoteThreshold: 0.19f,
|
||||
chordLockSeconds: 0.12f,
|
||||
chordLevelThreshold: 0.10f,
|
||||
chordHarmonicityThreshold: 0.24f,
|
||||
chordPeakRatioThreshold: 0.12f,
|
||||
@@ -1226,6 +1542,8 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
chordConfidenceThreshold: 0.14f,
|
||||
noteOnsetThreshold: 0.18f,
|
||||
noteReleaseThreshold: 0.07f,
|
||||
chordLockNoteThreshold: 0.16f,
|
||||
chordLockSeconds: 0.11f,
|
||||
chordLevelThreshold: 0.08f,
|
||||
chordHarmonicityThreshold: 0.18f,
|
||||
chordPeakRatioThreshold: 0.10f,
|
||||
@@ -1243,6 +1561,8 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
|
||||
private readonly record struct ChordVote(float Time, int Root, int Type, float Weight);
|
||||
|
||||
private readonly record struct ChordFrame(float Time, float[] Chroma, float[] BassChroma);
|
||||
|
||||
public readonly record struct DetectionTuningSnapshot(
|
||||
float ChordHarmonicityThreshold,
|
||||
float ChordSeparationThreshold,
|
||||
|
||||
+86
-10
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using FractalVisualizer.Audio;
|
||||
using FractalVisualizer.Models;
|
||||
using FractalVisualizer.Rendering;
|
||||
@@ -40,6 +42,7 @@ public class Game1 : Game
|
||||
(1920, 1080)
|
||||
];
|
||||
private int _resolutionIndex = 0;
|
||||
private const string TuningProfilePath = "detection-tuning.json";
|
||||
|
||||
public Game1()
|
||||
{
|
||||
@@ -144,6 +147,21 @@ public class Game1 : Game
|
||||
_showLegend = !_showLegend;
|
||||
}
|
||||
|
||||
if (IsPressed(keyboard, Keys.Back))
|
||||
{
|
||||
_audioAnalyzer.ResetTuning(_currentInputMode);
|
||||
}
|
||||
|
||||
if (IsPressed(keyboard, Keys.F5))
|
||||
{
|
||||
SaveTuningProfiles();
|
||||
}
|
||||
|
||||
if (IsPressed(keyboard, Keys.F6))
|
||||
{
|
||||
LoadTuningProfiles();
|
||||
}
|
||||
|
||||
if (IsPressed(keyboard, Keys.Left))
|
||||
{
|
||||
_fractalPresetIndex = Math.Max(0, _fractalPresetIndex - 1);
|
||||
@@ -243,19 +261,24 @@ public class Game1 : Game
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "BPM " + _latestFeatures.Bpm.ToString("0.0"), 12, 150, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "NOTE " + GetDetectedNoteName(_latestFeatures.DetectedNoteIndex), 12, 170, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "CHORD " + GetDetectedChordName(_latestFeatures.DetectedChordRootIndex, _latestFeatures.DetectedChordType, _latestFeatures.DetectedChordConfidence), 12, 190, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "PAUSED " + (_isPaused ? "YES" : "NO"), 12, 210, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "FPS " + _fps, 12, 230, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "QUALITY " + _renderQuality + " 1 2 3", 12, 250, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "F OR F11 FULLSCREEN R RESIZE H HELP", 12, 270, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "[ ] HARM " + tuning.ChordHarmonicityThreshold.ToString("0.000"), 12, 290, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "; ' SEP " + tuning.ChordSeparationThreshold.ToString("0.000"), 12, 310, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, ", . NCONS " + tuning.NoteVoteConsensus.ToString("0.000"), 12, 330, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "- + CCONS " + tuning.ChordVoteConsensus.ToString("0.000"), 12, 350, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "NOCHORD " + (_latestFeatures.DetectedChordRootIndex < 0 ? "YES" : "NO"), 12, 210, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "PAUSED " + (_isPaused ? "YES" : "NO"), 12, 230, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "FPS " + _fps, 12, 250, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "QUALITY " + _renderQuality + " 1 2 3", 12, 270, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "F OR F11 FULLSCREEN R RESIZE H HELP", 12, 290, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "[ ] HARM " + tuning.ChordHarmonicityThreshold.ToString("0.000"), 12, 310, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "; ' SEP " + tuning.ChordSeparationThreshold.ToString("0.000"), 12, 330, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, ", . NCONS " + tuning.NoteVoteConsensus.ToString("0.000"), 12, 350, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "- + CCONS " + tuning.ChordVoteConsensus.ToString("0.000"), 12, 370, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "BACK RESET MODE F5 SAVE F6 LOAD", 12, 390, 2, Color.White);
|
||||
if (_showLegend)
|
||||
{
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "LEGEND SPACE PAUSE TAB HUD M INPUT <- -> PRESET", 12, 370, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "TUNING [ ] HARM ; ' SEP , . NOTE CONS - + CHORD CONS", 12, 390, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "LEGEND SPACE PAUSE TAB HUD M INPUT <- -> PRESET", 12, 410, 2, Color.White);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "TUNING [ ] HARM ; ' SEP , . NOTE CONS - + CHORD CONS", 12, 430, 2, Color.White);
|
||||
}
|
||||
|
||||
DrawConfidenceBar(820, 14, 420, 12, _latestFeatures.DetectedNoteStrength, new Color(48, 72, 118), new Color(120, 220, 255), "NOTE CONF", 798, 12);
|
||||
DrawConfidenceBar(820, 40, 420, 12, _latestFeatures.DetectedChordConfidence, new Color(64, 52, 48), new Color(255, 182, 92), "CHORD CONF", 790, 38);
|
||||
_spriteBatch.End();
|
||||
}
|
||||
|
||||
@@ -385,4 +408,57 @@ public class Game1 : Game
|
||||
_graphics.PreferredBackBufferHeight = height;
|
||||
_graphics.ApplyChanges();
|
||||
}
|
||||
|
||||
private void DrawConfidenceBar(int x, int y, int width, int height, float value, Color bg, Color fg, string label, int labelX, int labelY)
|
||||
{
|
||||
if (_spriteBatch is null || _pixel is null || _hudFont is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float clamped = Math.Clamp(value, 0f, 1f);
|
||||
int fillWidth = (int)MathF.Round(width * clamped);
|
||||
_spriteBatch.Draw(_pixel, new Rectangle(x, y, width, height), bg);
|
||||
if (fillWidth > 0)
|
||||
{
|
||||
_spriteBatch.Draw(_pixel, new Rectangle(x, y, fillWidth, height), fg);
|
||||
}
|
||||
|
||||
_spriteBatch.Draw(_pixel, new Rectangle(x - 1, y - 1, width + 2, 1), Color.White * 0.45f);
|
||||
_spriteBatch.Draw(_pixel, new Rectangle(x - 1, y + height, width + 2, 1), Color.White * 0.45f);
|
||||
_spriteBatch.Draw(_pixel, new Rectangle(x - 1, y - 1, 1, height + 2), Color.White * 0.45f);
|
||||
_spriteBatch.Draw(_pixel, new Rectangle(x + width, y - 1, 1, height + 2), Color.White * 0.45f);
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, label + " " + clamped.ToString("0.000"), labelX, labelY, 2, Color.White);
|
||||
}
|
||||
|
||||
private void SaveTuningProfiles()
|
||||
{
|
||||
TuningProfiles profile = new(
|
||||
_audioAnalyzer.GetTuning(AudioInputMode.Microphone),
|
||||
_audioAnalyzer.GetTuning(AudioInputMode.SystemOutput));
|
||||
string json = JsonSerializer.Serialize(profile, new JsonSerializerOptions { WriteIndented = true });
|
||||
File.WriteAllText(TuningProfilePath, json);
|
||||
}
|
||||
|
||||
private void LoadTuningProfiles()
|
||||
{
|
||||
if (!File.Exists(TuningProfilePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string json = File.ReadAllText(TuningProfilePath);
|
||||
TuningProfiles? profile = JsonSerializer.Deserialize<TuningProfiles>(json);
|
||||
if (profile is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_audioAnalyzer.SetTuning(AudioInputMode.Microphone, profile.Microphone);
|
||||
_audioAnalyzer.SetTuning(AudioInputMode.SystemOutput, profile.SystemOutput);
|
||||
}
|
||||
|
||||
private readonly record struct TuningProfiles(
|
||||
AudioAnalyzer.DetectionTuningSnapshot Microphone,
|
||||
AudioAnalyzer.DetectionTuningSnapshot SystemOutput);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user