mirror of
https://github.com/Stone-Red-Code/BeatDetector.git
synced 2026-09-04 00:46:06 +02:00
Initial commit
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
using NAudio.Dsp;
|
||||
using NAudio.Wave;
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace BeatDetector.AudioProcessing;
|
||||
|
||||
internal class AudioBeatDetector
|
||||
{
|
||||
public event EventHandler<BeatDetectorEventArgs>? OnBeat;
|
||||
|
||||
public event EventHandler<BeatDetectorEventArgs>? OnNoBeat;
|
||||
|
||||
private readonly IWaveIn waveIn;
|
||||
private readonly SampleAggregator sampleAggregator;
|
||||
private readonly TimeSpan beatDetectionDelay;
|
||||
private readonly int fftLength;
|
||||
|
||||
private readonly int chunkCount;
|
||||
private readonly BeatChunk[] beatChunks;
|
||||
|
||||
public AudioBeatDetector(TimeSpan beatDetectionDelay, int fftLength, int chunkCount)
|
||||
{
|
||||
this.beatDetectionDelay = beatDetectionDelay;
|
||||
this.chunkCount = chunkCount;
|
||||
this.fftLength = fftLength;
|
||||
|
||||
beatChunks = new BeatChunk[chunkCount];
|
||||
sampleAggregator = new SampleAggregator(fftLength);
|
||||
|
||||
for (int i = 0; i < chunkCount; i++)
|
||||
{
|
||||
beatChunks[i] = new BeatChunk();
|
||||
beatChunks[i].StopWatch.Start();
|
||||
}
|
||||
|
||||
sampleAggregator.FftCalculated += new EventHandler<FftEventArgs>(FftCalculated);
|
||||
sampleAggregator.PerformFFT = true;
|
||||
|
||||
waveIn = new WasapiLoopbackCapture();
|
||||
waveIn.DataAvailable += OnDataAvailable;
|
||||
|
||||
waveIn.StartRecording();
|
||||
}
|
||||
|
||||
private void OnDataAvailable(object? sender, WaveInEventArgs e)
|
||||
{
|
||||
byte[] buffer = e.Buffer;
|
||||
int bytesRecorded = e.BytesRecorded;
|
||||
int bufferIncrement = waveIn.WaveFormat.BlockAlign;
|
||||
|
||||
for (int index = 0; index < bytesRecorded; index += bufferIncrement)
|
||||
{
|
||||
float sample32 = BitConverter.ToSingle(buffer, index);
|
||||
sampleAggregator.Add(sample32);
|
||||
}
|
||||
}
|
||||
|
||||
private void FftCalculated(object? sender, FftEventArgs e)
|
||||
{
|
||||
Complex[][] chunks = e.Result.Chunk(fftLength / chunkCount).ToArray();
|
||||
|
||||
for (int i = 0; i < chunkCount; i++)
|
||||
{
|
||||
CalculateBeat(chunks[i], beatChunks[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
private void CalculateBeat(Complex[] values, BeatChunk beatChunk, int chunkIndex)
|
||||
{
|
||||
double energyLevel = 0;
|
||||
foreach (Complex value in values)
|
||||
{
|
||||
energyLevel += Math.Pow(Math.Abs(value.Y), 2);
|
||||
}
|
||||
|
||||
double averageEnergyLevel = beatChunk.EnergyHistory.Count > 0 ? beatChunk.EnergyHistory.Average() : double.MaxValue;
|
||||
|
||||
if (beatChunk.StopWatch.Elapsed >= beatDetectionDelay)
|
||||
{
|
||||
double difference = energyLevel - averageEnergyLevel;
|
||||
|
||||
if (difference > beatChunk.AverageDifference / 2)
|
||||
{
|
||||
beatChunk.AverageDifference = (difference + beatChunk.AverageDifference) / 2d;
|
||||
OnBeat?.Invoke(this, new BeatDetectorEventArgs(chunkIndex, difference));
|
||||
}
|
||||
else
|
||||
{
|
||||
OnNoBeat?.Invoke(this, new BeatDetectorEventArgs(chunkIndex, 0));
|
||||
}
|
||||
beatChunk.StopWatch.Restart();
|
||||
}
|
||||
|
||||
beatChunk.EnergyHistory.Add(energyLevel);
|
||||
if (beatChunk.EnergyHistory.Count > 43)
|
||||
{
|
||||
beatChunk.EnergyHistory.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class BeatDetectorEventArgs : EventArgs
|
||||
{
|
||||
public int ChunkIndex { get; }
|
||||
public double DetectedValue { get; }
|
||||
|
||||
public BeatDetectorEventArgs(int chunkIndex, double detectedValue)
|
||||
{
|
||||
ChunkIndex = chunkIndex;
|
||||
DetectedValue = detectedValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace BeatDetector.AudioProcessing;
|
||||
|
||||
internal class BeatChunk
|
||||
{
|
||||
public Stopwatch StopWatch { get; } = new Stopwatch();
|
||||
public List<double> EnergyHistory { get; } = new List<double>();
|
||||
public double AverageDifference { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using NAudio.Dsp;
|
||||
|
||||
using System;
|
||||
|
||||
namespace BeatDetector.AudioProcessing;
|
||||
|
||||
internal class SampleAggregator
|
||||
{
|
||||
// FFT
|
||||
public event EventHandler<FftEventArgs>? FftCalculated;
|
||||
|
||||
public bool PerformFFT { get; set; }
|
||||
private readonly Complex[] fftBuffer;
|
||||
private readonly FftEventArgs fftArgs;
|
||||
private int fftPos;
|
||||
private readonly int fftLength;
|
||||
private readonly int m;
|
||||
|
||||
public SampleAggregator(int fftLength)
|
||||
{
|
||||
if (!IsPowerOfTwo(fftLength))
|
||||
{
|
||||
throw new ArgumentException("FFT Length must be a power of two");
|
||||
}
|
||||
m = (int)Math.Log(fftLength, 2.0);
|
||||
this.fftLength = fftLength;
|
||||
fftBuffer = new Complex[fftLength];
|
||||
fftArgs = new FftEventArgs(fftBuffer);
|
||||
}
|
||||
|
||||
private bool IsPowerOfTwo(int x)
|
||||
{
|
||||
return (x & (x - 1)) == 0;
|
||||
}
|
||||
|
||||
public void Add(float value)
|
||||
{
|
||||
if (PerformFFT && FftCalculated is not null)
|
||||
{
|
||||
fftBuffer[fftPos].X = (float)(value * FastFourierTransform.HammingWindow(fftPos, fftLength));
|
||||
fftBuffer[fftPos].Y = 0; // This is always zero with audio.
|
||||
fftPos++;
|
||||
if (fftPos >= fftLength)
|
||||
{
|
||||
fftPos = 0;
|
||||
FastFourierTransform.FFT(true, m, fftBuffer);
|
||||
FftCalculated(this, fftArgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class FftEventArgs : EventArgs
|
||||
{
|
||||
public FftEventArgs(Complex[] result)
|
||||
{
|
||||
Result = result;
|
||||
}
|
||||
|
||||
public Complex[] Result { get; private set; }
|
||||
}
|
||||
Reference in New Issue
Block a user