using System; using System.IO; using System.Linq; using System.Threading; using NAudio.CoreAudioApi; using NAudio.Wave; using TravelEar.Core; namespace TravelEar.Helper; /// /// Recording mode (T4a reference capture): records one WASAPI capture endpoint (the other /// machine's audio output arriving on a capture card or line-in) to a WAV until the window /// closes. The format is whatever the device delivers (usually 32-bit float, 2 ch, 48 kHz); the /// analysis script resamples and downmixes. Counters go to the status window and Helper.log. /// internal sealed class CaptureRecorder : IDisposable { private readonly HelperOptions _options; private readonly Action _report; private WasapiCapture? _capture; private WaveFileWriter? _writer; private MMDevice? _device; private long _bytes; private float _peak; private readonly System.Threading.Timer _timer; private readonly DateTime _started = DateTime.Now; public event Action? Exited; public string OutputPath { get; } public CaptureRecorder(HelperOptions options, Action report) { _options = options; _report = report; OutputPath = options.CaptureFile ?? Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "TravelEar", "calibration", $"peer-{DateTime.Now:yyyyMMdd-HHmmss}.wav"); _timer = new System.Threading.Timer(_ => Tick(), null, Timeout.Infinite, Timeout.Infinite); } public void Start() { try { using var enumerator = new MMDeviceEnumerator(); var devices = enumerator.EnumerateAudioEndPoints(DataFlow.Capture, DeviceState.Active).ToList(); var names = devices.Select(d => d.FriendlyName).ToList(); var wanted = _options.CaptureDevice ?? ""; var index = names.FindIndex(n => n.Contains(wanted, StringComparison.OrdinalIgnoreCase)); if (index < 0) { var listing = string.Join("\n ", names); HelperLog.Write($"Capture: no capture device matches '{wanted}'. Active capture devices:\n {listing}"); _report($"No capture device matches '{wanted}'.\nActive capture devices:\n {listing}"); foreach (var d in devices) d.Dispose(); Exited?.Invoke(Program.ExitNoDevice); return; } _device = devices[index]; for (var i = 0; i < devices.Count; i++) if (i != index) devices[i].Dispose(); Directory.CreateDirectory(Path.GetDirectoryName(OutputPath)!); _capture = new WasapiCapture(_device, false, 20); _writer = new WaveFileWriter(OutputPath, _capture.WaveFormat); _capture.DataAvailable += OnData; _capture.RecordingStopped += (_, e) => { if (e.Exception is not null) HelperLog.Write($"Capture: stopped with {e.Exception.Message}"); }; _capture.StartRecording(); HelperLog.Write($"Capture: recording '{_device.FriendlyName}' ({_capture.WaveFormat}) to {OutputPath}"); _timer.Change(1000, 1000); Tick(); } catch (Exception e) { HelperLog.Write($"Capture: could not start ({e.Message})."); _report($"Capture could not start:\n{e.Message}"); Exited?.Invoke(Program.ExitNoDevice); } } private void OnData(object? sender, WaveInEventArgs e) { var writer = _writer; if (writer is null) return; writer.Write(e.Buffer, 0, e.BytesRecorded); _bytes += e.BytesRecorded; if (_capture?.WaveFormat.Encoding == WaveFormatEncoding.IeeeFloat) { var peak = 0f; for (var i = 0; i + 4 <= e.BytesRecorded; i += 4) { var v = Math.Abs(BitConverter.ToSingle(e.Buffer, i)); if (v > peak) peak = v; } _peak = Math.Max(_peak * 0.9f, peak); } } private void Tick() { var format = _capture?.WaveFormat; var seconds = format is null ? 0 : _bytes / (double)format.AverageBytesPerSecond; var text = $"Device : {_device?.FriendlyName}\nMode : reference capture ({format})\n" + $"File : {OutputPath}\nRecorded : {seconds:F1} s, peak {_peak:F3}\nStarted : {_started:HH:mm:ss}\n\nClose this window to stop."; _report(text); if (((long)seconds) % 10 == 0) HelperLog.Write($"Capture: {seconds:F0} s recorded, peak {_peak:F3}"); } public void Dispose() { _timer.Dispose(); try { _capture?.StopRecording(); } catch (Exception) { /* closing */ } _capture?.Dispose(); _writer?.Dispose(); _device?.Dispose(); HelperLog.Write($"Capture: closed {OutputPath} ({_bytes} bytes)."); } }