Files

87 lines
3.0 KiB
C#
Raw Permalink Normal View History

using FftSharp;
namespace Substation;
public static class MusicAnalyzer
{
2026-08-08 19:20:46 +00:00
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)
{
2026-08-08 19:20:46 +00:00
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);
}
2026-08-08 19:20:46 +00:00
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);
}
2026-08-08 19:20:46 +00:00
public class TickFeature
{
public double MelodyEnergy;
public double MelodyCount;
public readonly List<double> MelodyFreqSamples = new();
}
2026-08-08 19:20:46 +00:00
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);
}
}