Files

213 lines
7.6 KiB
C#
Raw Permalink Normal View History

2026-08-08 19:20:46 +00:00
using FftSharp;
using NAudio.CoreAudioApi;
using NAudio.Wave;
using System.IO;
2026-08-08 19:20:46 +00:00
namespace Substation;
public class LiveCapture : IDisposable
{
readonly State _state;
readonly MMDevice _device;
WasapiLoopbackCapture? _capture;
Thread? _processThread;
volatile bool _running;
readonly Queue<double> _sampleBuffer = new();
readonly object _bufferLock = new();
double _liveMaxMelodyEnergy;
int _sampleRate;
int _fftsPerTick;
int _fftIndexInTick;
readonly DrumDetector _drums = new();
2026-08-08 19:20:46 +00:00
int _dataAvailableCount;
int _totalSamplesReceived;
int _fftCount;
int _tickCount;
static readonly string LogPath = Path.Combine(AppContext.BaseDirectory, "live.log");
static readonly object LogLock = new();
static void Log(string msg)
{
var line = $"{DateTime.Now:HH:mm:ss.fff} {msg}";
lock (LogLock)
{
try { File.AppendAllText(LogPath, line + Environment.NewLine); } catch { }
}
}
2026-08-08 19:20:46 +00:00
public event Action? Stopped;
public LiveCapture(State state, MMDevice device)
{
_state = state;
_device = device;
}
public void Start()
{
_running = true;
_capture = new WasapiLoopbackCapture(_device);
_sampleRate = _capture.WaveFormat.SampleRate;
_fftsPerTick = Math.Max(1, (int)Math.Round(MusicAnalyzer.TickDuration * _sampleRate / MusicAnalyzer.HopSize));
try { File.WriteAllText(LogPath, ""); } catch { }
Log($"[live] device: {_device.FriendlyName}");
Log($"[live] format: {_capture.WaveFormat} ({_capture.WaveFormat.BitsPerSample}bit, {_capture.WaveFormat.Channels}ch, {_sampleRate}Hz)");
Log($"[live] fftsPerTick: {_fftsPerTick}");
2026-08-08 19:20:46 +00:00
_capture.DataAvailable += OnDataAvailable;
_capture.RecordingStopped += OnRecordingStopped;
_processThread = new Thread(ProcessLoop) { IsBackground = true, Name = "LiveCapture-FFT" };
_processThread.Start();
_capture.StartRecording();
Log($"[live] capture started");
Log($"[live] log file: {LogPath}");
2026-08-08 19:20:46 +00:00
}
public void Stop()
{
_running = false;
try { _capture?.StopRecording(); } catch { }
}
void OnDataAvailable(object? sender, WaveInEventArgs e)
{
int channels = _capture!.WaveFormat.Channels;
int bytesPerSample = _capture.WaveFormat.BitsPerSample / 8;
int frameSize = channels * bytesPerSample;
int sampleCount = e.BytesRecorded / frameSize;
_dataAvailableCount++;
_totalSamplesReceived += sampleCount;
if (_dataAvailableCount % 100 == 1)
Log($"[live] dataAvailable #{_dataAvailableCount}: {e.BytesRecorded} bytes, {sampleCount} samples, total={_totalSamplesReceived}, buffer={_sampleBuffer.Count}");
2026-08-08 19:20:46 +00:00
lock (_bufferLock)
{
for (int i = 0; i < sampleCount; i++)
{
int offset = i * frameSize;
float left = BitConverter.ToSingle(e.Buffer, offset);
float right = channels >= 2
? BitConverter.ToSingle(e.Buffer, offset + bytesPerSample)
: left;
_sampleBuffer.Enqueue((left + right) * 0.5);
}
// Cap buffer size to prevent memory growth if processing falls behind
while (_sampleBuffer.Count > _sampleRate * 2)
_sampleBuffer.Dequeue();
}
}
void OnRecordingStopped(object? sender, StoppedEventArgs e)
{
_running = false;
Stopped?.Invoke();
Log($"[live] capture stopped. dataAvailable={_dataAvailableCount}, totalSamples={_totalSamplesReceived}, ffts={_fftCount}, ticks={_tickCount}");
if (e?.Exception != null)
Log($"[live] ERROR stop exception: {e.Exception.Message}");
2026-08-08 19:20:46 +00:00
}
void ProcessLoop()
{
var window = new FftSharp.Windows.Hanning();
var buffer = new double[MusicAnalyzer.WindowSize];
var tf = new MusicAnalyzer.TickFeature();
double[]? overlap = null; // last HopSize samples from previous window
2026-08-08 19:20:46 +00:00
Log("[live] process thread started");
2026-08-08 19:20:46 +00:00
while (_running)
{
double[]? windowData = null;
lock (_bufferLock)
{
int needed = overlap != null ? MusicAnalyzer.HopSize : MusicAnalyzer.WindowSize;
if (_sampleBuffer.Count >= needed)
2026-08-08 19:20:46 +00:00
{
if (overlap != null)
{
Array.Copy(overlap, 0, buffer, 0, MusicAnalyzer.HopSize);
for (int i = 0; i < MusicAnalyzer.HopSize; i++)
buffer[MusicAnalyzer.HopSize + i] = _sampleBuffer.Dequeue();
}
else
{
for (int i = 0; i < MusicAnalyzer.WindowSize; i++)
buffer[i] = _sampleBuffer.Dequeue();
}
// Save last HopSize samples for next window's overlap
overlap = new double[MusicAnalyzer.HopSize];
Array.Copy(buffer, MusicAnalyzer.HopSize, overlap, 0, MusicAnalyzer.HopSize);
2026-08-08 19:20:46 +00:00
windowData = buffer;
}
}
if (windowData == null)
{
if (_fftCount == 0 && _dataAvailableCount > 0 && _dataAvailableCount % 200 == 0)
Log($"[live] WARNING: data available ({_dataAvailableCount} callbacks, {_totalSamplesReceived} samples) but buffer has only {_sampleBuffer.Count} samples (need {MusicAnalyzer.WindowSize})");
2026-08-08 19:20:46 +00:00
Thread.Sleep(5);
continue;
}
window.ApplyInPlace(windowData);
var spectrum = FFT.Forward(windowData);
var mag = FFT.Magnitude(spectrum);
_fftCount++;
var (melodyEnergy, melodyFreq) = MusicAnalyzer.ExtractFeatures(mag, _sampleRate);
2026-08-08 19:20:46 +00:00
// Adaptive normalization: running max with slow decay
_liveMaxMelodyEnergy = Math.Max(melodyEnergy, _liveMaxMelodyEnergy * 0.999);
// Drum detection: map this FFT window to a sub-tick (0-3)
int subTick = _fftsPerTick > 0 ? _fftIndexInTick * 4 / _fftsPerTick : 0;
if (subTick > 3) subTick = 3;
_drums.ProcessWindow(mag, _sampleRate, MusicAnalyzer.WindowSize, subTick);
// Melody accumulation for chB
2026-08-08 19:20:46 +00:00
tf.MelodyEnergy += melodyEnergy;
tf.MelodyFreqSamples.Add(melodyFreq);
tf.MelodyCount++;
_fftIndexInTick++;
if (_fftIndexInTick >= _fftsPerTick)
{
var frameA = _drums.BuildFrame();
var frameB = MusicAnalyzer.BuildMelodyFrame(tf, _liveMaxMelodyEnergy);
2026-08-08 19:20:46 +00:00
_state.EnqueueStream('A', new[] { frameA });
_state.EnqueueStream('B', new[] { frameB });
_tickCount++;
if (_tickCount % 50 == 1)
{
int intA = (frameA.Intensity[0] + frameA.Intensity[1] + frameA.Intensity[2] + frameA.Intensity[3]) / 4;
int intB = (frameB.Intensity[0] + frameB.Intensity[1] + frameB.Intensity[2] + frameB.Intensity[3]) / 4;
Log($"[live] tick #{_tickCount}: ffts={_fftCount}, melodyEnergy={melodyEnergy:F4}, maxMelody={_liveMaxMelodyEnergy:F4}, freq={melodyFreq:F0}Hz, intA={intA}, intB={intB}, freqA={frameA.Freq[0]}, freqB={frameB.Freq[0]}");
}
2026-08-08 19:20:46 +00:00
tf = new MusicAnalyzer.TickFeature();
_fftIndexInTick = 0;
}
}
}
public void Dispose()
{
_running = false;
try { _capture?.Dispose(); } catch { }
}
}