Files
Substation/LiveCapture.cs
T
mute ccc5093ee2 Add live audio capture via WASAPI loopback
- LiveCapture.cs: real-time WASAPI loopback capture with rolling buffer
  and continuous FFT analysis, enqueues WaveFrames at ~10Hz
- MusicAnalyzer.cs: refactored ExtractFeatures/MapPitchToPeriod/BuildWaveFrame
  as public reusable methods with sampleRate parameter
- MainForm: audio device selector (defaults to system default), Live button
  starts/stops capture, Stop All stops live too
- Adaptive normalization (running max with slow decay) for live volume changes
2026-08-08 19:20:46 +00:00

152 lines
4.6 KiB
C#

using System.Numerics;
using FftSharp;
using NAudio.CoreAudioApi;
using NAudio.Wave;
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[]? _prevRhythmMag;
double _liveMaxFlux;
double _liveMaxMelodyEnergy;
int _sampleRate;
int _fftsPerTick;
int _fftIndexInTick;
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));
_capture.DataAvailable += OnDataAvailable;
_capture.RecordingStopped += OnRecordingStopped;
_processThread = new Thread(ProcessLoop) { IsBackground = true, Name = "LiveCapture-FFT" };
_processThread.Start();
_capture.StartRecording();
Console.WriteLine($"[live] capture started: {_device.FriendlyName} ({_sampleRate}Hz, {_capture.WaveFormat.Channels}ch)");
}
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;
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();
Console.WriteLine("[live] capture stopped");
}
void ProcessLoop()
{
var window = new FftSharp.Windows.Hanning();
var buffer = new double[MusicAnalyzer.WindowSize];
var tf = new MusicAnalyzer.TickFeature();
while (_running)
{
double[]? windowData = null;
lock (_bufferLock)
{
if (_sampleBuffer.Count >= MusicAnalyzer.WindowSize)
{
for (int i = 0; i < MusicAnalyzer.WindowSize; i++)
buffer[i] = _sampleBuffer.Dequeue();
windowData = buffer;
}
}
if (windowData == null)
{
Thread.Sleep(5);
continue;
}
window.ApplyInPlace(windowData);
var spectrum = FFT.Forward(windowData);
var mag = FFT.Magnitude(spectrum);
var (rhythmEnergy, rhythmFlux, melodyEnergy, melodyFreq) =
MusicAnalyzer.ExtractFeatures(mag, _prevRhythmMag, _sampleRate);
_prevRhythmMag = mag;
// Adaptive normalization: running max with slow decay
_liveMaxFlux = Math.Max(rhythmFlux, _liveMaxFlux * 0.999);
_liveMaxMelodyEnergy = Math.Max(melodyEnergy, _liveMaxMelodyEnergy * 0.999);
tf.RhythmFlux = Math.Max(tf.RhythmFlux, rhythmFlux);
tf.MelodyEnergy += melodyEnergy;
tf.MelodyFreqSamples.Add(melodyFreq);
tf.MelodyCount++;
_fftIndexInTick++;
if (_fftIndexInTick >= _fftsPerTick)
{
var (frameA, frameB) = MusicAnalyzer.BuildWaveFrame(tf, _liveMaxFlux, _liveMaxMelodyEnergy);
_state.EnqueueStream('A', new[] { frameA });
_state.EnqueueStream('B', new[] { frameB });
tf = new MusicAnalyzer.TickFeature();
_fftIndexInTick = 0;
}
}
}
public void Dispose()
{
_running = false;
try { _capture?.Dispose(); } catch { }
}
}