1ab397767a
Dead code removed: - MusicAnalyzer.BuildWaveFrame (replaced by DrumDetector + BuildMelodyFrame) - MusicAnalyzer.ExtractFeatures simplified to melody-only (removed rhythm band) - MusicAnalyzer.RhythmLow/RhythmHigh constants, TickFeature.RhythmFlux - LiveCapture._liveMaxFlux + _prevRhythmMag (computed, never read) - State.ActualA/ActualB, State.LastSentA/LastSentB (set, never read) - Program.cs StrengthChanged handler (only wrote to dead fields) - Command.Mode property (never referenced) - DrumDetector.FreqKick/Snare/Brass/Silent static arrays (unused) Bugs fixed: - DrumDetector.BuildFrame: freqBytes was byte[16], now byte[4] - CoyoteDevice fallback: use nameFilter param instead of hardcoded string Refactoring: - Server.DoStatus calls BuildStatusPush(null) instead of duplicating - HeatMap: removed redundant colW assignment - Removed unused System.Numerics imports from LiveCapture, MusicAnalyzer
87 lines
3.0 KiB
C#
87 lines
3.0 KiB
C#
using FftSharp;
|
|
|
|
namespace Substation;
|
|
|
|
public static class MusicAnalyzer
|
|
{
|
|
public const int WindowSize = 2048;
|
|
public const int HopSize = 1024;
|
|
public const double TickDuration = 0.1;
|
|
|
|
const double MelodyLow = 300, MelodyHigh = 4000;
|
|
|
|
public static (double melodyEnergy, double melodyFreq)
|
|
ExtractFeatures(double[] magnitude, int sampleRate)
|
|
{
|
|
double binWidth = (double)sampleRate / WindowSize;
|
|
int melodyLoBin = (int)(MelodyLow / binWidth);
|
|
int melodyHiBin = (int)(MelodyHigh / binWidth);
|
|
|
|
// Melody band: energy + dominant frequency (spectral peak)
|
|
double melodyEnergy = 0;
|
|
double peakMag = 0;
|
|
int peakBin = melodyLoBin;
|
|
for (int i = melodyLoBin; i <= melodyHiBin && i < magnitude.Length; i++)
|
|
{
|
|
double m = magnitude[i];
|
|
melodyEnergy += m * m;
|
|
if (m > peakMag)
|
|
{
|
|
peakMag = m;
|
|
peakBin = i;
|
|
}
|
|
}
|
|
melodyEnergy = Math.Sqrt(melodyEnergy / (melodyHiBin - melodyLoBin + 1));
|
|
double melodyFreq = peakBin * binWidth;
|
|
|
|
return (melodyEnergy, melodyFreq);
|
|
}
|
|
|
|
public static int MapPitchToPeriod(double hz)
|
|
{
|
|
// Map melody frequency (300-4000Hz) to e-stim period (10-1000ms)
|
|
// Logarithmic mapping: low notes → deep, high notes → buzzy
|
|
double logFreq = Math.Log(Math.Clamp(hz, MelodyLow, MelodyHigh));
|
|
double logMin = Math.Log(MelodyLow);
|
|
double logMax = Math.Log(MelodyHigh);
|
|
double t = (logFreq - logMin) / (logMax - logMin); // 0..1
|
|
// Invert: high freq → short period (buzzy), low freq → long period (deep)
|
|
int ms = (int)Math.Round(1000 - t * 990); // 1000ms..10ms
|
|
return Math.Clamp(ms, 10, 1000);
|
|
}
|
|
|
|
public class TickFeature
|
|
{
|
|
public double MelodyEnergy;
|
|
public double MelodyCount;
|
|
public readonly List<double> MelodyFreqSamples = new();
|
|
}
|
|
|
|
public static WaveFrame BuildMelodyFrame(TickFeature tf, double maxMelodyEnergy)
|
|
{
|
|
double avgEnergy = tf.MelodyCount > 0 ? tf.MelodyEnergy / tf.MelodyCount : 0;
|
|
double normalizedEnergy = maxMelodyEnergy > 0 ? avgEnergy / maxMelodyEnergy : 0;
|
|
int melodyIntensity = (int)Math.Round(normalizedEnergy * 80);
|
|
melodyIntensity = Math.Clamp(melodyIntensity, 0, 100);
|
|
|
|
double weightedFreq = 0;
|
|
double totalWeight = 0;
|
|
foreach (var f in tf.MelodyFreqSamples)
|
|
{
|
|
weightedFreq += f * f;
|
|
totalWeight += f;
|
|
}
|
|
double avgMelodyHz = totalWeight > 0 ? weightedFreq / totalWeight : 500;
|
|
int estimsMs = MapPitchToPeriod(avgMelodyHz);
|
|
|
|
var intB = new[]
|
|
{
|
|
(byte)melodyIntensity, (byte)melodyIntensity,
|
|
(byte)melodyIntensity, (byte)melodyIntensity
|
|
};
|
|
var freqB = Freq.Compress4(new[] { estimsMs, estimsMs, estimsMs, estimsMs });
|
|
|
|
return new WaveFrame(freqB, intB);
|
|
}
|
|
}
|