diff --git a/FractalVisualizer/Audio/AudioAnalyzer.cs b/FractalVisualizer/Audio/AudioAnalyzer.cs index 733efcc..1a8cec2 100644 --- a/FractalVisualizer/Audio/AudioAnalyzer.cs +++ b/FractalVisualizer/Audio/AudioAnalyzer.cs @@ -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 _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++) { - secondBestScore = bestScore; - bestScore = major; - bestRoot = r; - bestType = 0; - } - else if (major > secondBestScore) - { - secondBestScore = major; - } - - float minor = ScoreChord(chroma, r, 1); - if (minor > bestScore) - { - secondBestScore = bestScore; - bestScore = minor; - bestRoot = r; - bestType = 1; - } - else if (minor > secondBestScore) - { - secondBestScore = minor; + float score = ScoreChord(chroma, r, chordType); + if (score > bestScore) + { + secondBestScore = bestScore; + bestScore = score; + bestRoot = r; + bestType = chordType; + } + else if (score > secondBestScore) + { + secondBestScore = score; + } } } 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) diff --git a/FractalVisualizer/Game1.cs b/FractalVisualizer/Game1.cs index 37cb2f0..1b36427 100644 --- a/FractalVisualizer/Game1.cs +++ b/FractalVisualizer/Game1.cs @@ -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() diff --git a/FractalVisualizer/Rendering/FractalRenderer.cs b/FractalVisualizer/Rendering/FractalRenderer.cs index c066315..636662c 100644 --- a/FractalVisualizer/Rendering/FractalRenderer.cs +++ b/FractalVisualizer/Rendering/FractalRenderer.cs @@ -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)