Expanded chord detection, real-time tuning, improved FFT
Expanded chord detection to support 9 chord types (maj, min, sus2, sus4, dim, aug, 7, maj7, min7) with updated scoring and voting logic. Added pitch-filtered FFT path for improved tonal analysis. Introduced real-time, per-mode tuning of detection thresholds (harmonicity, separation, vote consensus) via keyboard shortcuts, with HUD display of current values. Refactored chord display and voting logic to handle expanded chord space. Added thread-safe tuning adjustment and helper methods. Overall, improved flexibility, accuracy, and user control for audio analysis.
This commit is contained in:
@@ -21,7 +21,7 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
private readonly int[] _spectrumAccumulatorCounts;
|
||||
private readonly float[] _chroma = new float[12];
|
||||
private readonly float[] _noteVoteScores = new float[12];
|
||||
private readonly float[] _chordVoteScores = new float[24];
|
||||
private readonly float[] _chordVoteScores = new float[12 * ChordTypeCount];
|
||||
private readonly float[][] _spectrumRefsByMode;
|
||||
private readonly float[] _levelRefsByMode = new float[2];
|
||||
private readonly float[] _bassRefsByMode = new float[2];
|
||||
@@ -30,6 +30,7 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
private readonly Stopwatch _clock = new();
|
||||
|
||||
private readonly object _lifecycleSync = new();
|
||||
private readonly object _tuningSync = new();
|
||||
|
||||
private CancellationTokenSource? _cts;
|
||||
private Task? _analysisTask;
|
||||
@@ -49,6 +50,11 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
private readonly Queue<ChordVote> _chordVotes = new();
|
||||
private const float TemporalVoteWindowSeconds = 0.30f;
|
||||
private const int MaxBeatIntervals = 8;
|
||||
private const int ChordTypeCount = 9;
|
||||
private readonly float[] _chordHarmonicityOffsetByMode = new float[2];
|
||||
private readonly float[] _chordSeparationOffsetByMode = new float[2];
|
||||
private readonly float[] _noteVoteConsensusOffsetByMode = new float[2];
|
||||
private readonly float[] _chordVoteConsensusOffsetByMode = new float[2];
|
||||
private int _lastChordRoot = -1;
|
||||
private int _lastChordType = -1;
|
||||
private float _lastChordChangeTimeSeconds;
|
||||
@@ -57,6 +63,11 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
private float _lastNoteChangeTimeSeconds;
|
||||
private bool _noteIsActive;
|
||||
private int _activeNoteIndex = -1;
|
||||
private readonly float[] _pitchSamples;
|
||||
private readonly Complex[] _pitchFftBuffer;
|
||||
private float _hpPrevInput;
|
||||
private float _hpPrevOutput;
|
||||
private float _lpPrevOutput;
|
||||
|
||||
private Snapshot _latestSnapshot;
|
||||
private bool _disposed;
|
||||
@@ -80,7 +91,9 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
|
||||
_hannWindow = BuildHannWindow(fftSize);
|
||||
_timeSamples = new float[fftSize];
|
||||
_pitchSamples = new float[fftSize];
|
||||
_fftBuffer = new Complex[fftSize];
|
||||
_pitchFftBuffer = new Complex[fftSize];
|
||||
_spectrumAccumulator = new float[spectrumBins];
|
||||
_spectrumAccumulatorCounts = new int[spectrumBins];
|
||||
_spectrumRefsByMode =
|
||||
@@ -151,6 +164,40 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
return Volatile.Read(ref _latestSnapshot).Features;
|
||||
}
|
||||
|
||||
public DetectionTuningSnapshot GetTuning(AudioInputMode mode)
|
||||
{
|
||||
int modeIndex = mode == AudioInputMode.Microphone ? 0 : 1;
|
||||
SensitivityProfile baseProfile = mode == AudioInputMode.Microphone ? SensitivityProfile.Microphone : SensitivityProfile.SystemOutput;
|
||||
lock (_tuningSync)
|
||||
{
|
||||
return new DetectionTuningSnapshot(
|
||||
Clamp(baseProfile.ChordHarmonicityThreshold + _chordHarmonicityOffsetByMode[modeIndex], 0.05f, 0.95f),
|
||||
Clamp(baseProfile.ChordSeparationThreshold + _chordSeparationOffsetByMode[modeIndex], 0.01f, 0.30f),
|
||||
Clamp(baseProfile.NoteVoteConsensus + _noteVoteConsensusOffsetByMode[modeIndex], 0.15f, 0.90f),
|
||||
Clamp(baseProfile.ChordVoteConsensus + _chordVoteConsensusOffsetByMode[modeIndex], 0.15f, 0.90f));
|
||||
}
|
||||
}
|
||||
|
||||
public void AdjustChordHarmonicity(AudioInputMode mode, float delta)
|
||||
{
|
||||
AdjustTuning(mode, _chordHarmonicityOffsetByMode, delta, -0.35f, 0.35f);
|
||||
}
|
||||
|
||||
public void AdjustChordSeparation(AudioInputMode mode, float delta)
|
||||
{
|
||||
AdjustTuning(mode, _chordSeparationOffsetByMode, delta, -0.10f, 0.20f);
|
||||
}
|
||||
|
||||
public void AdjustNoteVoteConsensus(AudioInputMode mode, float delta)
|
||||
{
|
||||
AdjustTuning(mode, _noteVoteConsensusOffsetByMode, delta, -0.35f, 0.35f);
|
||||
}
|
||||
|
||||
public void AdjustChordVoteConsensus(AudioInputMode mode, float delta)
|
||||
{
|
||||
AdjustTuning(mode, _chordVoteConsensusOffsetByMode, delta, -0.35f, 0.35f);
|
||||
}
|
||||
|
||||
private void AnalysisLoop(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
@@ -172,13 +219,23 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
int modeIndex = mode == AudioInputMode.Microphone ? 0 : 1;
|
||||
int sampleRate = Volatile.Read(ref _sampleRate);
|
||||
int half = _fftSize / 2;
|
||||
GetEffectiveThresholds(
|
||||
modeIndex,
|
||||
profile,
|
||||
out float chordHarmonicityThreshold,
|
||||
out float chordSeparationThreshold,
|
||||
out float noteVoteConsensusThreshold,
|
||||
out float chordVoteConsensusThreshold);
|
||||
|
||||
for (int i = 0; i < _fftSize; i++)
|
||||
{
|
||||
_fftBuffer[i] = new Complex(_timeSamples[i] * _hannWindow[i], 0.0);
|
||||
}
|
||||
|
||||
BuildPitchFilteredBuffer(sampleRate);
|
||||
|
||||
Fourier.Forward(_fftBuffer, FourierOptions.Matlab);
|
||||
Fourier.Forward(_pitchFftBuffer, FourierOptions.Matlab);
|
||||
|
||||
Array.Clear(_spectrumAccumulator);
|
||||
Array.Clear(_spectrumAccumulatorCounts);
|
||||
@@ -203,6 +260,7 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
{
|
||||
float frequency = (sampleRate * bin) / (float)_fftSize;
|
||||
float magnitude = (float)_fftBuffer[bin].Magnitude;
|
||||
float pitchMagnitude = (float)_pitchFftBuffer[bin].Magnitude;
|
||||
|
||||
int spectrumIndex = (int)((bin / (float)half) * _spectrumBins);
|
||||
if (spectrumIndex >= _spectrumBins)
|
||||
@@ -226,9 +284,9 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
highEnergy += magnitude;
|
||||
}
|
||||
|
||||
if (frequency >= 120f && frequency <= 6000f && magnitude > dominantMagnitude)
|
||||
if (frequency >= 120f && frequency <= 6000f && pitchMagnitude > dominantMagnitude)
|
||||
{
|
||||
dominantMagnitude = magnitude;
|
||||
dominantMagnitude = pitchMagnitude;
|
||||
dominantFrequency = frequency;
|
||||
}
|
||||
|
||||
@@ -237,7 +295,7 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
int pitchClass = GetNoteIndexFromFrequency(frequency);
|
||||
if (pitchClass >= 0)
|
||||
{
|
||||
_chroma[pitchClass] += magnitude;
|
||||
_chroma[pitchClass] += pitchMagnitude;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -299,17 +357,28 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
float bpmInstant = CalculateBpm();
|
||||
_smoothedBpm = Smooth(_smoothedBpm, bpmInstant, 0.18f, 0.04f);
|
||||
|
||||
EstimateFundamentalFrequencyHps(sampleRate, out float hpsFrequency, out float hpsMagnitude, out float harmonicity);
|
||||
EstimateFundamentalFrequencyHps(sampleRate, _pitchFftBuffer, out float hpsFrequency, out float hpsMagnitude, out float harmonicity);
|
||||
float noteFrequency = hpsFrequency > 0f ? hpsFrequency : dominantFrequency;
|
||||
float noteMagnitude = hpsFrequency > 0f ? hpsMagnitude : dominantMagnitude;
|
||||
int noteIndex = GetNoteIndexFromFrequency(noteFrequency);
|
||||
float whistleBoost = GetWhistleBoost(noteFrequency);
|
||||
float dominantStrength = Clamp01(noteMagnitude * profile.NoteStrengthGain * (1f + whistleBoost * 0.65f) * (0.58f + (0.42f * harmonicity)));
|
||||
|
||||
DetectChord(_chroma, _smoothedLevel, profile, harmonicity, out int chordRoot, out int chordType, out float chordConfidence);
|
||||
DetectChord(
|
||||
_chroma,
|
||||
_smoothedLevel,
|
||||
profile,
|
||||
harmonicity,
|
||||
chordHarmonicityThreshold,
|
||||
chordSeparationThreshold,
|
||||
out int chordRoot,
|
||||
out int chordType,
|
||||
out float chordConfidence);
|
||||
ApplyTemporalVoting(
|
||||
now,
|
||||
profile,
|
||||
noteVoteConsensusThreshold,
|
||||
chordVoteConsensusThreshold,
|
||||
noteIndex,
|
||||
dominantStrength,
|
||||
chordRoot,
|
||||
@@ -360,6 +429,9 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
private void OnSampleRateChanged(int sampleRate)
|
||||
{
|
||||
Volatile.Write(ref _sampleRate, sampleRate);
|
||||
_hpPrevInput = 0f;
|
||||
_hpPrevOutput = 0f;
|
||||
_lpPrevOutput = 0f;
|
||||
}
|
||||
|
||||
private static float[] BuildHannWindow(int length)
|
||||
@@ -430,9 +502,18 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
return note;
|
||||
}
|
||||
|
||||
private void DetectChord(float[] chroma, float level, SensitivityProfile profile, float harmonicity, out int root, out int type, out float confidence)
|
||||
private void DetectChord(
|
||||
float[] chroma,
|
||||
float level,
|
||||
SensitivityProfile profile,
|
||||
float harmonicity,
|
||||
float chordHarmonicityThreshold,
|
||||
float chordSeparationThreshold,
|
||||
out int root,
|
||||
out int type,
|
||||
out float confidence)
|
||||
{
|
||||
if (level < profile.ChordLevelThreshold || harmonicity < profile.ChordHarmonicityThreshold)
|
||||
if (level < profile.ChordLevelThreshold || harmonicity < chordHarmonicityThreshold)
|
||||
{
|
||||
root = -1;
|
||||
type = -1;
|
||||
@@ -471,7 +552,7 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
float geometricMean = MathF.Exp(logSum / chroma.Length);
|
||||
float flatness = geometricMean / (arithmeticMean + 1e-9f);
|
||||
float chromaHarmonicity = Clamp01(1f - (flatness * 1.25f));
|
||||
if (chromaHarmonicity < profile.ChordHarmonicityThreshold)
|
||||
if (chromaHarmonicity < chordHarmonicityThreshold)
|
||||
{
|
||||
root = -1;
|
||||
type = -1;
|
||||
@@ -494,36 +575,26 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
int bestType = -1;
|
||||
for (int r = 0; r < 12; r++)
|
||||
{
|
||||
float major = ScoreChord(chroma, r, 0);
|
||||
if (major > bestScore)
|
||||
for (int chordType = 0; chordType < ChordTypeCount; chordType++)
|
||||
{
|
||||
float score = ScoreChord(chroma, r, chordType);
|
||||
if (score > bestScore)
|
||||
{
|
||||
secondBestScore = bestScore;
|
||||
bestScore = major;
|
||||
bestScore = score;
|
||||
bestRoot = r;
|
||||
bestType = 0;
|
||||
bestType = chordType;
|
||||
}
|
||||
else if (major > secondBestScore)
|
||||
else if (score > secondBestScore)
|
||||
{
|
||||
secondBestScore = major;
|
||||
secondBestScore = score;
|
||||
}
|
||||
|
||||
float minor = ScoreChord(chroma, r, 1);
|
||||
if (minor > bestScore)
|
||||
{
|
||||
secondBestScore = bestScore;
|
||||
bestScore = minor;
|
||||
bestRoot = r;
|
||||
bestType = 1;
|
||||
}
|
||||
else if (minor > secondBestScore)
|
||||
{
|
||||
secondBestScore = minor;
|
||||
}
|
||||
}
|
||||
|
||||
float conf = Clamp01(bestScore / (total + 1e-6f));
|
||||
float separation = (bestScore - secondBestScore) / (total + 1e-6f);
|
||||
if (conf < 0.17f || separation < profile.ChordSeparationThreshold)
|
||||
if (conf < 0.17f || separation < chordSeparationThreshold)
|
||||
{
|
||||
root = -1;
|
||||
type = -1;
|
||||
@@ -548,7 +619,7 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
confidence = Clamp01(conf * (0.55f + (0.45f * harmonicity)));
|
||||
}
|
||||
|
||||
private void EstimateFundamentalFrequencyHps(int sampleRate, out float frequencyHz, out float magnitude, out float harmonicity)
|
||||
private void EstimateFundamentalFrequencyHps(int sampleRate, Complex[] frequencyBuffer, out float frequencyHz, out float magnitude, out float harmonicity)
|
||||
{
|
||||
frequencyHz = 0f;
|
||||
magnitude = 0f;
|
||||
@@ -568,10 +639,10 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
const float epsilon = 1e-12f;
|
||||
for (int bin = minBin; bin <= maxBin; bin++)
|
||||
{
|
||||
float m1 = (float)_fftBuffer[bin].Magnitude;
|
||||
float m2 = (float)_fftBuffer[bin * 2].Magnitude;
|
||||
float m3 = (float)_fftBuffer[bin * 3].Magnitude;
|
||||
float m4 = (float)_fftBuffer[bin * 4].Magnitude;
|
||||
float m1 = (float)frequencyBuffer[bin].Magnitude;
|
||||
float m2 = (float)frequencyBuffer[bin * 2].Magnitude;
|
||||
float m3 = (float)frequencyBuffer[bin * 3].Magnitude;
|
||||
float m4 = (float)frequencyBuffer[bin * 4].Magnitude;
|
||||
float logScore =
|
||||
MathF.Log(m1 + epsilon) +
|
||||
(0.90f * MathF.Log(m2 + epsilon)) +
|
||||
@@ -589,10 +660,10 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
float fundamental = (float)_fftBuffer[bestBin].Magnitude;
|
||||
float second = (float)_fftBuffer[bestBin * 2].Magnitude;
|
||||
float third = (float)_fftBuffer[bestBin * 3].Magnitude;
|
||||
float fourth = (float)_fftBuffer[bestBin * 4].Magnitude;
|
||||
float fundamental = (float)frequencyBuffer[bestBin].Magnitude;
|
||||
float second = (float)frequencyBuffer[bestBin * 2].Magnitude;
|
||||
float third = (float)frequencyBuffer[bestBin * 3].Magnitude;
|
||||
float fourth = (float)frequencyBuffer[bestBin * 4].Magnitude;
|
||||
|
||||
frequencyHz = (sampleRate * bestBin) / (float)_fftSize;
|
||||
magnitude = fundamental;
|
||||
@@ -601,12 +672,68 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
|
||||
private static float ScoreChord(float[] chroma, int root, int type)
|
||||
{
|
||||
int third = type == 0 ? (root + 4) % 12 : (root + 3) % 12;
|
||||
int fifth = (root + 7) % 12;
|
||||
int second = (root + 2) % 12;
|
||||
int fourth = (root + 5) % 12;
|
||||
float score = chroma[root] * 1.0f + chroma[third] * 0.9f + chroma[fifth] * 0.8f;
|
||||
score -= (chroma[second] + chroma[fourth]) * 0.15f;
|
||||
int i0 = root;
|
||||
int i1 = root;
|
||||
int i2 = root;
|
||||
int i3 = -1;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case 0: // maj
|
||||
i1 = (root + 4) % 12;
|
||||
i2 = (root + 7) % 12;
|
||||
break;
|
||||
case 1: // min
|
||||
i1 = (root + 3) % 12;
|
||||
i2 = (root + 7) % 12;
|
||||
break;
|
||||
case 2: // sus2
|
||||
i1 = (root + 2) % 12;
|
||||
i2 = (root + 7) % 12;
|
||||
break;
|
||||
case 3: // sus4
|
||||
i1 = (root + 5) % 12;
|
||||
i2 = (root + 7) % 12;
|
||||
break;
|
||||
case 4: // dim
|
||||
i1 = (root + 3) % 12;
|
||||
i2 = (root + 6) % 12;
|
||||
break;
|
||||
case 5: // aug
|
||||
i1 = (root + 4) % 12;
|
||||
i2 = (root + 8) % 12;
|
||||
break;
|
||||
case 6: // 7
|
||||
i1 = (root + 4) % 12;
|
||||
i2 = (root + 7) % 12;
|
||||
i3 = (root + 10) % 12;
|
||||
break;
|
||||
case 7: // maj7
|
||||
i1 = (root + 4) % 12;
|
||||
i2 = (root + 7) % 12;
|
||||
i3 = (root + 11) % 12;
|
||||
break;
|
||||
case 8: // min7
|
||||
i1 = (root + 3) % 12;
|
||||
i2 = (root + 7) % 12;
|
||||
i3 = (root + 10) % 12;
|
||||
break;
|
||||
default:
|
||||
i1 = (root + 4) % 12;
|
||||
i2 = (root + 7) % 12;
|
||||
break;
|
||||
}
|
||||
|
||||
float score = chroma[i0] * 1.00f + chroma[i1] * 0.92f + chroma[i2] * 0.84f;
|
||||
if (i3 >= 0)
|
||||
{
|
||||
score += chroma[i3] * 0.72f;
|
||||
}
|
||||
|
||||
int b2 = (root + 1) % 12;
|
||||
int tritone = (root + 6) % 12;
|
||||
int clash = (root + 11) % 12;
|
||||
score -= (chroma[b2] * 0.20f) + (chroma[tritone] * 0.14f) + (chroma[clash] * 0.08f);
|
||||
return Math.Max(0f, score);
|
||||
}
|
||||
|
||||
@@ -625,6 +752,21 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
return value;
|
||||
}
|
||||
|
||||
private static float Clamp(float value, float min, float max)
|
||||
{
|
||||
if (value < min)
|
||||
{
|
||||
return min;
|
||||
}
|
||||
|
||||
if (value > max)
|
||||
{
|
||||
return max;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static float GetWhistleBoost(float frequencyHz)
|
||||
{
|
||||
if (frequencyHz <= 0f)
|
||||
@@ -667,9 +809,61 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
return Clamp01((v / reference) * gain);
|
||||
}
|
||||
|
||||
private void BuildPitchFilteredBuffer(int sampleRate)
|
||||
{
|
||||
// Pitch/chord path: remove low rumble and very high hiss for clearer tonal detection.
|
||||
float dt = 1f / MathF.Max(1f, sampleRate);
|
||||
float hpCut = 70f;
|
||||
float lpCut = 5000f;
|
||||
float hpRc = 1f / (2f * MathF.PI * hpCut);
|
||||
float lpRc = 1f / (2f * MathF.PI * lpCut);
|
||||
float hpAlpha = hpRc / (hpRc + dt);
|
||||
float lpAlpha = dt / (lpRc + dt);
|
||||
|
||||
for (int i = 0; i < _fftSize; i++)
|
||||
{
|
||||
float x = _timeSamples[i];
|
||||
float hp = hpAlpha * (_hpPrevOutput + x - _hpPrevInput);
|
||||
_hpPrevInput = x;
|
||||
_hpPrevOutput = hp;
|
||||
float lp = _lpPrevOutput + (lpAlpha * (hp - _lpPrevOutput));
|
||||
_lpPrevOutput = lp;
|
||||
_pitchSamples[i] = lp;
|
||||
_pitchFftBuffer[i] = new Complex(lp * _hannWindow[i], 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
private void GetEffectiveThresholds(
|
||||
int modeIndex,
|
||||
SensitivityProfile baseProfile,
|
||||
out float chordHarmonicityThreshold,
|
||||
out float chordSeparationThreshold,
|
||||
out float noteVoteConsensusThreshold,
|
||||
out float chordVoteConsensusThreshold)
|
||||
{
|
||||
lock (_tuningSync)
|
||||
{
|
||||
chordHarmonicityThreshold = Clamp(baseProfile.ChordHarmonicityThreshold + _chordHarmonicityOffsetByMode[modeIndex], 0.05f, 0.95f);
|
||||
chordSeparationThreshold = Clamp(baseProfile.ChordSeparationThreshold + _chordSeparationOffsetByMode[modeIndex], 0.01f, 0.30f);
|
||||
noteVoteConsensusThreshold = Clamp(baseProfile.NoteVoteConsensus + _noteVoteConsensusOffsetByMode[modeIndex], 0.15f, 0.90f);
|
||||
chordVoteConsensusThreshold = Clamp(baseProfile.ChordVoteConsensus + _chordVoteConsensusOffsetByMode[modeIndex], 0.15f, 0.90f);
|
||||
}
|
||||
}
|
||||
|
||||
private void AdjustTuning(AudioInputMode mode, float[] offsets, float delta, float min, float max)
|
||||
{
|
||||
int modeIndex = mode == AudioInputMode.Microphone ? 0 : 1;
|
||||
lock (_tuningSync)
|
||||
{
|
||||
offsets[modeIndex] = Clamp(offsets[modeIndex] + delta, min, max);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyTemporalVoting(
|
||||
float now,
|
||||
SensitivityProfile profile,
|
||||
float noteVoteConsensusThreshold,
|
||||
float chordVoteConsensusThreshold,
|
||||
int noteIndex,
|
||||
float noteStrength,
|
||||
int chordRoot,
|
||||
@@ -684,16 +878,16 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
float noteWeight = noteIndex >= 0 ? MathF.Max(0f, noteStrength) : profile.SilenceVoteWeight;
|
||||
_noteVotes.Enqueue(new NoteVote(now, noteIndex, noteWeight));
|
||||
PruneOldVotes(_noteVotes, now);
|
||||
ResolveNoteVote(profile, out votedNoteIndex, out votedNoteStrength);
|
||||
ResolveNoteVote(profile, noteVoteConsensusThreshold, out votedNoteIndex, out votedNoteStrength);
|
||||
|
||||
bool hasChordCandidate = chordRoot >= 0 && chordConfidence >= (profile.ChordConfidenceThreshold * 0.55f);
|
||||
float chordWeight = hasChordCandidate ? chordConfidence : profile.SilenceVoteWeight;
|
||||
_chordVotes.Enqueue(new ChordVote(now, hasChordCandidate ? chordRoot : -1, hasChordCandidate ? chordType : -1, chordWeight));
|
||||
PruneOldVotes(_chordVotes, now);
|
||||
ResolveChordVote(profile, out votedChordRoot, out votedChordType, out votedChordConfidence);
|
||||
ResolveChordVote(profile, chordVoteConsensusThreshold, out votedChordRoot, out votedChordType, out votedChordConfidence);
|
||||
}
|
||||
|
||||
private void ResolveNoteVote(SensitivityProfile profile, out int noteIndex, out float noteStrength)
|
||||
private void ResolveNoteVote(SensitivityProfile profile, float noteVoteConsensusThreshold, out int noteIndex, out float noteStrength)
|
||||
{
|
||||
Array.Clear(_noteVoteScores);
|
||||
float totalWeight = 0f;
|
||||
@@ -742,7 +936,7 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
float separation = (best - second) / (totalWeight + 1e-6f);
|
||||
if (bestIndex < 0 ||
|
||||
best < profile.NoteVoteMinScore ||
|
||||
consensus < profile.NoteVoteConsensus ||
|
||||
consensus < noteVoteConsensusThreshold ||
|
||||
separation < profile.NoteVoteSeparation ||
|
||||
silenceWeight > best * 1.08f)
|
||||
{
|
||||
@@ -755,7 +949,7 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
noteStrength = Clamp01((consensus * 0.72f) + (separation * 0.28f));
|
||||
}
|
||||
|
||||
private void ResolveChordVote(SensitivityProfile profile, out int chordRoot, out int chordType, out float chordConfidence)
|
||||
private void ResolveChordVote(SensitivityProfile profile, float chordVoteConsensusThreshold, out int chordRoot, out int chordType, out float chordConfidence)
|
||||
{
|
||||
Array.Clear(_chordVoteScores);
|
||||
float totalWeight = 0f;
|
||||
@@ -767,7 +961,7 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
totalWeight += weight;
|
||||
if (vote.Root >= 0 && vote.Type >= 0)
|
||||
{
|
||||
int idx = (vote.Root * 2) + vote.Type;
|
||||
int idx = (vote.Root * ChordTypeCount) + vote.Type;
|
||||
_chordVoteScores[idx] += weight;
|
||||
}
|
||||
else
|
||||
@@ -806,7 +1000,7 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
float separation = (best - second) / (totalWeight + 1e-6f);
|
||||
if (bestIndex < 0 ||
|
||||
best < profile.ChordVoteMinScore ||
|
||||
consensus < profile.ChordVoteConsensus ||
|
||||
consensus < chordVoteConsensusThreshold ||
|
||||
separation < profile.ChordVoteSeparation ||
|
||||
silenceWeight > best * 1.10f)
|
||||
{
|
||||
@@ -816,8 +1010,8 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
chordRoot = bestIndex / 2;
|
||||
chordType = bestIndex % 2;
|
||||
chordRoot = bestIndex / ChordTypeCount;
|
||||
chordType = bestIndex % ChordTypeCount;
|
||||
chordConfidence = Clamp01((consensus * 0.75f) + (separation * 0.25f));
|
||||
}
|
||||
|
||||
@@ -1049,6 +1243,12 @@ public sealed class AudioAnalyzer : IDisposable
|
||||
|
||||
private readonly record struct ChordVote(float Time, int Root, int Type, float Weight);
|
||||
|
||||
public readonly record struct DetectionTuningSnapshot(
|
||||
float ChordHarmonicityThreshold,
|
||||
float ChordSeparationThreshold,
|
||||
float NoteVoteConsensus,
|
||||
float ChordVoteConsensus);
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
|
||||
@@ -26,6 +26,7 @@ public class Game1 : Game
|
||||
private AudioFeatures _latestFeatures;
|
||||
private bool _isPaused;
|
||||
private bool _showHud = true;
|
||||
private bool _showLegend = true;
|
||||
private int _fractalPresetIndex;
|
||||
private int _fps;
|
||||
private int _frameCounter;
|
||||
@@ -138,6 +139,11 @@ public class Game1 : Game
|
||||
_showHud = !_showHud;
|
||||
}
|
||||
|
||||
if (IsPressed(keyboard, Keys.H))
|
||||
{
|
||||
_showLegend = !_showLegend;
|
||||
}
|
||||
|
||||
if (IsPressed(keyboard, Keys.Left))
|
||||
{
|
||||
_fractalPresetIndex = Math.Max(0, _fractalPresetIndex - 1);
|
||||
@@ -156,6 +162,46 @@ public class Game1 : Game
|
||||
_audioCapture.SwitchMode(_currentInputMode);
|
||||
}
|
||||
|
||||
if (IsPressed(keyboard, Keys.OemOpenBrackets))
|
||||
{
|
||||
_audioAnalyzer.AdjustChordHarmonicity(_currentInputMode, -0.01f);
|
||||
}
|
||||
|
||||
if (IsPressed(keyboard, Keys.OemCloseBrackets))
|
||||
{
|
||||
_audioAnalyzer.AdjustChordHarmonicity(_currentInputMode, +0.01f);
|
||||
}
|
||||
|
||||
if (IsPressed(keyboard, Keys.OemSemicolon))
|
||||
{
|
||||
_audioAnalyzer.AdjustChordSeparation(_currentInputMode, -0.005f);
|
||||
}
|
||||
|
||||
if (IsPressed(keyboard, Keys.OemQuotes))
|
||||
{
|
||||
_audioAnalyzer.AdjustChordSeparation(_currentInputMode, +0.005f);
|
||||
}
|
||||
|
||||
if (IsPressed(keyboard, Keys.OemComma))
|
||||
{
|
||||
_audioAnalyzer.AdjustNoteVoteConsensus(_currentInputMode, -0.01f);
|
||||
}
|
||||
|
||||
if (IsPressed(keyboard, Keys.OemPeriod))
|
||||
{
|
||||
_audioAnalyzer.AdjustNoteVoteConsensus(_currentInputMode, +0.01f);
|
||||
}
|
||||
|
||||
if (IsPressed(keyboard, Keys.OemMinus))
|
||||
{
|
||||
_audioAnalyzer.AdjustChordVoteConsensus(_currentInputMode, -0.01f);
|
||||
}
|
||||
|
||||
if (IsPressed(keyboard, Keys.OemPlus))
|
||||
{
|
||||
_audioAnalyzer.AdjustChordVoteConsensus(_currentInputMode, +0.01f);
|
||||
}
|
||||
|
||||
_latestFeatures = _audioAnalyzer.GetLatestFeatures();
|
||||
_fpsElapsed += gameTime.ElapsedGameTime;
|
||||
_frameCounter++;
|
||||
@@ -184,6 +230,7 @@ public class Game1 : Game
|
||||
|
||||
if (_showHud && _spriteBatch is not null && _hudFont is not null && _pixel is not null)
|
||||
{
|
||||
var tuning = _audioAnalyzer.GetTuning(_currentInputMode);
|
||||
_spriteBatch.Begin(samplerState: SamplerState.PointClamp, blendState: BlendState.AlphaBlend);
|
||||
string modeText = _currentInputMode == AudioInputMode.Microphone ? "MIC" : "SYS";
|
||||
_hudFont.DrawText(_spriteBatch, _pixel, "MODE " + modeText + " M SWITCH", 12, 10, 2, Color.White);
|
||||
@@ -199,7 +246,16 @@ public class Game1 : Game
|
||||
_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", 12, 270, 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);
|
||||
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);
|
||||
}
|
||||
_spriteBatch.End();
|
||||
}
|
||||
|
||||
@@ -290,7 +346,19 @@ public class Game1 : Game
|
||||
return "-";
|
||||
}
|
||||
|
||||
return chordType == 1 ? root + " MIN" : root + " MAJ";
|
||||
return chordType switch
|
||||
{
|
||||
0 => root + " MAJ",
|
||||
1 => root + " MIN",
|
||||
2 => root + " SUS2",
|
||||
3 => root + " SUS4",
|
||||
4 => root + " DIM",
|
||||
5 => root + " AUG",
|
||||
6 => root + " 7",
|
||||
7 => root + " MAJ7",
|
||||
8 => root + " MIN7",
|
||||
_ => root + " MAJ"
|
||||
};
|
||||
}
|
||||
|
||||
private void ToggleFullScreen()
|
||||
|
||||
@@ -1253,7 +1253,9 @@ public sealed class FractalRenderer : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
int chordCode = features.DetectedChordRootIndex < 0 ? -1 : (features.DetectedChordRootIndex * 2 + (features.DetectedChordType == 1 ? 1 : 0));
|
||||
int chordCode = features.DetectedChordRootIndex < 0
|
||||
? -1
|
||||
: (features.DetectedChordRootIndex * 9 + Math.Clamp(features.DetectedChordType, 0, 8));
|
||||
if (chordCode != _lastChordCode)
|
||||
{
|
||||
_lastChordCode = chordCode;
|
||||
@@ -1609,7 +1611,19 @@ public sealed class FractalRenderer : IDisposable
|
||||
}
|
||||
|
||||
string root = NoteNames[features.DetectedChordRootIndex];
|
||||
return features.DetectedChordType == 1 ? root + " MIN" : root + " MAJ";
|
||||
return features.DetectedChordType switch
|
||||
{
|
||||
0 => root + " MAJ",
|
||||
1 => root + " MIN",
|
||||
2 => root + " SUS2",
|
||||
3 => root + " SUS4",
|
||||
4 => root + " DIM",
|
||||
5 => root + " AUG",
|
||||
6 => root + " 7",
|
||||
7 => root + " MAJ7",
|
||||
8 => root + " MIN7",
|
||||
_ => root + " MAJ"
|
||||
};
|
||||
}
|
||||
|
||||
private void SpawnFireworkRocket(float intensity)
|
||||
|
||||
Reference in New Issue
Block a user