From 291a2e8736cfee2722512ff455cad9893f412525 Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 7 Mar 2026 19:22:58 -0600 Subject: [PATCH] chore: remove diagnostic recording feature The diagnostic audio recording (WAV capture with sync metrics for Audacity analysis) was rarely used and added complexity. Removes the entire feature including UI, backend services, DI wiring, and configuration. --- .../Audio/DynamicResamplerSampleProvider.cs | 12 +- .../Audio/SoundTouchSampleProvider.cs | 11 +- .../Audio/WasapiAudioPlayer.cs | 13 +- .../Diagnostics/DiagnosticAudioRecorder.cs | 268 ------------------ .../Diagnostics/DiagnosticWavWriter.cs | 237 ---------------- .../Diagnostics/IDiagnosticAudioRecorder.cs | 91 ------ src/SendspinClient/App.xaml.cs | 15 +- src/SendspinClient/Configuration/AppPaths.cs | 14 - .../Configuration/DiagnosticsSettings.cs | 58 ---- .../ViewModels/MainViewModel.cs | 8 +- .../ViewModels/StatsViewModel.cs | 167 ----------- src/SendspinClient/Views/StatsWindow.xaml | 115 -------- src/SendspinClient/appsettings.json | 5 - 13 files changed, 8 insertions(+), 1006 deletions(-) delete mode 100644 src/SendspinClient.Services/Diagnostics/DiagnosticAudioRecorder.cs delete mode 100644 src/SendspinClient.Services/Diagnostics/DiagnosticWavWriter.cs delete mode 100644 src/SendspinClient.Services/Diagnostics/IDiagnosticAudioRecorder.cs delete mode 100644 src/SendspinClient/Configuration/DiagnosticsSettings.cs diff --git a/src/SendspinClient.Services/Audio/DynamicResamplerSampleProvider.cs b/src/SendspinClient.Services/Audio/DynamicResamplerSampleProvider.cs index a577200..4df04ab 100644 --- a/src/SendspinClient.Services/Audio/DynamicResamplerSampleProvider.cs +++ b/src/SendspinClient.Services/Audio/DynamicResamplerSampleProvider.cs @@ -6,8 +6,6 @@ using NAudio.Dsp; using NAudio.Wave; using Sendspin.SDK.Audio; -using SendspinClient.Services.Diagnostics; - namespace SendspinClient.Services.Audio; /// @@ -30,7 +28,6 @@ public sealed class DynamicResamplerSampleProvider : ISampleProvider, IDisposabl private readonly ISyncCorrectionProvider? _correctionProvider; private readonly WdlResampler _resampler; private readonly ILogger? _logger; - private readonly IDiagnosticAudioRecorder? _diagnosticRecorder; private readonly object _rateLock = new(); private readonly int _targetSampleRate; @@ -135,20 +132,17 @@ public double PlaybackRate /// Pass 0 or the source sample rate to perform only sync correction. /// /// Optional logger for debugging. - /// Optional diagnostic recorder for audio capture. public DynamicResamplerSampleProvider( ISampleProvider source, ISyncCorrectionProvider? correctionProvider = null, int targetSampleRate = 0, - ILogger? logger = null, - IDiagnosticAudioRecorder? diagnosticRecorder = null) + ILogger? logger = null) { ArgumentNullException.ThrowIfNull(source); _source = source; _correctionProvider = correctionProvider; _logger = logger; - _diagnosticRecorder = diagnosticRecorder; // Determine target sample rate - use source rate if not specified _targetSampleRate = targetSampleRate > 0 ? targetSampleRate : source.WaveFormat.SampleRate; @@ -262,10 +256,6 @@ public int Read(float[] buffer, int offset, int count) // Resample var outputGenerated = Resample(_sourceBuffer, inputRead, buffer, offset, count); - // Capture audio for diagnostic recording if enabled - // Zero overhead when disabled - null check is branch-predicted away - _diagnosticRecorder?.CaptureIfEnabled(buffer.AsSpan(offset, outputGenerated)); - // If we didn't generate enough output, pad with silence and track underrun if (outputGenerated < count) { diff --git a/src/SendspinClient.Services/Audio/SoundTouchSampleProvider.cs b/src/SendspinClient.Services/Audio/SoundTouchSampleProvider.cs index 3cd1cee..4d6c50d 100644 --- a/src/SendspinClient.Services/Audio/SoundTouchSampleProvider.cs +++ b/src/SendspinClient.Services/Audio/SoundTouchSampleProvider.cs @@ -6,8 +6,6 @@ using NAudio.Wave; using Sendspin.SDK.Audio; using SoundTouch; -using SendspinClient.Services.Diagnostics; - namespace SendspinClient.Services.Audio; /// @@ -31,7 +29,6 @@ public sealed class SoundTouchSampleProvider : ISampleProvider, IDisposable private readonly ISyncCorrectionProvider? _correctionProvider; private readonly SoundTouchProcessor _processor; private readonly ILogger? _logger; - private readonly IDiagnosticAudioRecorder? _diagnosticRecorder; private readonly object _rateLock = new(); private readonly int _channels; @@ -128,19 +125,16 @@ public double PlaybackRate /// The upstream sample provider to read from. /// Optional sync correction provider to subscribe to for rate change events. /// Optional logger for debugging. - /// Optional diagnostic recorder for audio capture. public SoundTouchSampleProvider( ISampleProvider source, ISyncCorrectionProvider? correctionProvider = null, - ILogger? logger = null, - IDiagnosticAudioRecorder? diagnosticRecorder = null) + ILogger? logger = null) { ArgumentNullException.ThrowIfNull(source); _source = source; _correctionProvider = correctionProvider; _logger = logger; - _diagnosticRecorder = diagnosticRecorder; _channels = source.WaveFormat.Channels; // Output format matches input (SoundTouch doesn't change sample rate, just playback rate) @@ -284,9 +278,6 @@ public int Read(float[] buffer, int offset, int count) Array.Fill(buffer, 0f, offset + totalSamplesOutput, count - totalSamplesOutput); } - // Capture audio for diagnostic recording if enabled - _diagnosticRecorder?.CaptureIfEnabled(buffer.AsSpan(offset, count)); - return count; } diff --git a/src/SendspinClient.Services/Audio/WasapiAudioPlayer.cs b/src/SendspinClient.Services/Audio/WasapiAudioPlayer.cs index 9c88840..a2c6567 100644 --- a/src/SendspinClient.Services/Audio/WasapiAudioPlayer.cs +++ b/src/SendspinClient.Services/Audio/WasapiAudioPlayer.cs @@ -9,7 +9,6 @@ using Sendspin.SDK.Models; using Sendspin.SDK.Synchronization; using SendspinClient.Services.Models; -using SendspinClient.Services.Diagnostics; namespace SendspinClient.Services.Audio; @@ -52,7 +51,6 @@ public sealed class WasapiAudioPlayer : IAudioPlayer private readonly ILogger _logger; private readonly SyncCorrectionStrategy _syncStrategy; private readonly ResamplerType _resamplerType; - private readonly IDiagnosticAudioRecorder? _diagnosticRecorder; private string? _deviceId; private WasapiOut? _wasapiOut; private AudioSampleProviderAdapter? _sampleProvider; @@ -166,19 +164,16 @@ public bool IsMuted /// WDL uses sinc interpolation, SoundTouch uses WSOLA algorithm. /// Ignored when strategy is DropInsertOnly. /// - /// Optional diagnostic recorder for audio capture. public WasapiAudioPlayer( ILogger logger, string? deviceId = null, SyncCorrectionStrategy syncStrategy = SyncCorrectionStrategy.Combined, - ResamplerType resamplerType = ResamplerType.Wdl, - IDiagnosticAudioRecorder? diagnosticRecorder = null) + ResamplerType resamplerType = ResamplerType.Wdl) { _logger = logger; _deviceId = deviceId; _syncStrategy = syncStrategy; _resamplerType = resamplerType; - _diagnosticRecorder = diagnosticRecorder; } /// @@ -382,8 +377,7 @@ private void CreateResampler(ISampleProvider sourceProvider) var soundTouch = new SoundTouchSampleProvider( sourceProvider, _correctionProvider, - _logger, - _diagnosticRecorder); + _logger); _resamplerProvider = soundTouch; _resamplerDisposable = soundTouch; break; @@ -394,8 +388,7 @@ private void CreateResampler(ISampleProvider sourceProvider) sourceProvider, _correctionProvider, _deviceNativeSampleRate, - _logger, - _diagnosticRecorder); + _logger); _resamplerProvider = wdl; _resamplerDisposable = wdl; break; diff --git a/src/SendspinClient.Services/Diagnostics/DiagnosticAudioRecorder.cs b/src/SendspinClient.Services/Diagnostics/DiagnosticAudioRecorder.cs deleted file mode 100644 index ce60d5e..0000000 --- a/src/SendspinClient.Services/Diagnostics/DiagnosticAudioRecorder.cs +++ /dev/null @@ -1,268 +0,0 @@ -// -// Licensed under the MIT License. See LICENSE file in the project root. -// - -using Microsoft.Extensions.Logging; -using Sendspin.SDK.Audio; -using Sendspin.SDK.Diagnostics; -using Sendspin.SDK.Synchronization; - -namespace SendspinClient.Services.Diagnostics; - -/// -/// Implements diagnostic audio recording with sync metric correlation. -/// -/// -/// -/// This class orchestrates audio capture and metric recording for diagnostic purposes. -/// When enabled, it maintains circular buffers for both audio samples and sync metrics, -/// allowing the user to save a snapshot of the last N seconds of audio with embedded -/// markers showing sync state at each moment. -/// -/// -/// Memory usage: ~17MB for 45 seconds of 48kHz stereo audio, plus ~40KB for metrics. -/// Memory is only allocated when enabled. -/// -/// -public sealed class DiagnosticAudioRecorder : IDiagnosticAudioRecorder -{ - private readonly ILogger _logger; - private readonly object _lock = new(); - - private DiagnosticAudioRingBuffer? _audioBuffer; - private SyncMetricRingBuffer? _metricBuffer; - private bool _isEnabled; - private bool _disposed; - - /// - public bool IsEnabled - { - get - { - lock (_lock) - { - return _isEnabled; - } - } - } - - /// - public double BufferedSeconds - { - get - { - lock (_lock) - { - return _audioBuffer?.BufferedSeconds ?? 0; - } - } - } - - /// - public int BufferDurationSeconds { get; } - - /// - public event EventHandler? RecordingStateChanged; - - /// - /// Initializes a new instance of the class. - /// - /// The logger instance. - /// The buffer duration in seconds (default: 45). - public DiagnosticAudioRecorder(ILogger logger, int bufferDurationSeconds = 45) - { - _logger = logger; - BufferDurationSeconds = bufferDurationSeconds; - } - - /// - public void Enable(int sampleRate, int channels) - { - lock (_lock) - { - if (_isEnabled) - { - _logger.LogDebug("Diagnostic recording already enabled"); - return; - } - - _audioBuffer = new DiagnosticAudioRingBuffer(sampleRate, channels, BufferDurationSeconds); - _metricBuffer = new SyncMetricRingBuffer(); - _isEnabled = true; - - _logger.LogInformation( - "Diagnostic recording enabled: {SampleRate}Hz, {Channels}ch, {Duration}s buffer (~{MemoryMB:F1}MB)", - sampleRate, - channels, - BufferDurationSeconds, - _audioBuffer.Capacity * sizeof(float) / 1024.0 / 1024.0); - } - - RecordingStateChanged?.Invoke(this, true); - } - - /// - public void Disable() - { - lock (_lock) - { - if (!_isEnabled) - { - return; - } - - _audioBuffer = null; - _metricBuffer = null; - _isEnabled = false; - - _logger.LogInformation("Diagnostic recording disabled, buffers freed"); - } - - RecordingStateChanged?.Invoke(this, false); - } - - /// - public void CaptureIfEnabled(ReadOnlySpan samples) - { - // Fast path when disabled - single volatile read - if (!_isEnabled) - { - return; - } - - // Capture samples (lock-free write to ring buffer) - _audioBuffer?.Write(samples); - } - - /// - public void RecordMetrics(AudioBufferStats stats) - { - if (!_isEnabled) - { - return; - } - - var audioBuffer = _audioBuffer; - var metricBuffer = _metricBuffer; - - if (audioBuffer == null || metricBuffer == null) - { - return; - } - - var snapshot = new SyncMetricSnapshot - { - TimestampMicroseconds = HighPrecisionTimer.Shared.GetCurrentTimeMicroseconds(), - SamplePosition = audioBuffer.TotalSamplesWritten, - RawSyncErrorMicroseconds = stats.SyncErrorMicroseconds, - SmoothedSyncErrorMicroseconds = stats.SyncErrorMicroseconds, // Same value (smoothed is what's exposed) - CorrectionMode = stats.CurrentCorrectionMode, - PlaybackRate = stats.TargetPlaybackRate, - BufferDepthMs = stats.BufferedMs, - }; - - metricBuffer.Record(snapshot); - } - - /// - public async Task SaveAsync(string directory) - { - DiagnosticAudioRingBuffer? audioBuffer; - SyncMetricRingBuffer? metricBuffer; - - lock (_lock) - { - if (!_isEnabled || _audioBuffer == null || _metricBuffer == null) - { - _logger.LogWarning("Cannot save: diagnostic recording is not enabled"); - return null; - } - - audioBuffer = _audioBuffer; - metricBuffer = _metricBuffer; - } - - // Capture snapshots (this allocates, but we're on a background thread) - var (samples, startIndex) = audioBuffer.CaptureSnapshot(); - var endIndex = startIndex + samples.Length; - var metrics = metricBuffer.GetSnapshotsInRange(startIndex, endIndex); - - if (samples.Length == 0) - { - _logger.LogWarning("Cannot save: no audio samples in buffer"); - return null; - } - - // Ensure directory exists - Directory.CreateDirectory(directory); - - // Generate filename with timestamp - var timestamp = DateTime.Now.ToString("yyyy-MM-dd_HHmmss"); - var baseName = $"diagnostic-{timestamp}"; - var wavPath = Path.Combine(directory, $"{baseName}.wav"); - var labelPath = Path.Combine(directory, $"{baseName}.txt"); - - _logger.LogInformation( - "Saving diagnostic recording: {Samples} samples ({Duration:F1}s), {Metrics} markers", - samples.Length, - (double)samples.Length / audioBuffer.SampleRate / audioBuffer.Channels, - metrics.Length); - - // Write files on background thread - await Task.Run(() => - { - // Write WAV file with cue markers - DiagnosticWavWriter.WriteWavWithMarkers( - wavPath, - samples, - audioBuffer.SampleRate, - audioBuffer.Channels, - metrics, - startIndex); - - // Write Audacity label file - WriteAudacityLabels(labelPath, metrics, audioBuffer.SampleRate, audioBuffer.Channels, startIndex); - }); - - _logger.LogInformation("Diagnostic recording saved: {WavPath}", wavPath); - - return wavPath; - } - - /// - /// Writes an Audacity-compatible label file. - /// - private static void WriteAudacityLabels( - string path, - SyncMetricSnapshot[] metrics, - int sampleRate, - int channels, - long startSamplePosition) - { - using var writer = new StreamWriter(path); - - foreach (var metric in metrics) - { - // Calculate time position relative to the start of the WAV - var sampleOffset = metric.SamplePosition - startSamplePosition; - var timeSeconds = (double)sampleOffset / sampleRate / channels; - - if (timeSeconds >= 0) - { - writer.WriteLine(metric.FormatAudacityLabel(timeSeconds)); - } - } - } - - /// - public void Dispose() - { - if (_disposed) - { - return; - } - - _disposed = true; - Disable(); - } -} diff --git a/src/SendspinClient.Services/Diagnostics/DiagnosticWavWriter.cs b/src/SendspinClient.Services/Diagnostics/DiagnosticWavWriter.cs deleted file mode 100644 index 4a94177..0000000 --- a/src/SendspinClient.Services/Diagnostics/DiagnosticWavWriter.cs +++ /dev/null @@ -1,237 +0,0 @@ -// -// Licensed under the MIT License. See LICENSE file in the project root. -// - -using System.Text; -using Sendspin.SDK.Diagnostics; - -namespace SendspinClient.Services.Diagnostics; - -/// -/// Writes WAV files with embedded cue markers for diagnostic audio analysis. -/// -/// -/// -/// Creates standard PCM WAV files with embedded cue points that can be viewed -/// in audio editors like Audacity. Each cue point includes a label showing -/// sync metrics at that moment. -/// -/// -/// WAV file structure: -/// -/// RIFF header -/// fmt chunk (audio format) -/// data chunk (PCM samples) -/// cue chunk (marker positions) -/// LIST adtl chunk (marker labels) -/// -/// -/// -public static class DiagnosticWavWriter -{ - /// - /// Writes a WAV file with embedded cue markers. - /// - /// The output file path. - /// The audio samples (32-bit float). - /// The sample rate in Hz. - /// The number of channels. - /// The metric snapshots to embed as markers. - /// The sample position of the first sample in the buffer. - public static void WriteWavWithMarkers( - string path, - float[] samples, - int sampleRate, - int channels, - SyncMetricSnapshot[] markers, - long startSamplePosition) - { - using var stream = new FileStream(path, FileMode.Create, FileAccess.Write); - using var writer = new BinaryWriter(stream); - - // Convert float samples to 16-bit PCM for broader compatibility - var pcmSamples = ConvertTo16BitPcm(samples); - - // Calculate chunk sizes - var fmtChunkSize = 16; // Standard PCM format chunk - var dataChunkSize = pcmSamples.Length; - var cueChunkSize = markers.Length > 0 ? 4 + (24 * markers.Length) : 0; - var adtlChunkSize = CalculateAdtlChunkSize(markers, startSamplePosition, sampleRate, channels); - - // RIFF header size = everything except "RIFF" and size field - var riffSize = 4 + // "WAVE" - 8 + fmtChunkSize + // fmt chunk - 8 + dataChunkSize + // data chunk - (cueChunkSize > 0 ? 8 + cueChunkSize : 0) + // cue chunk (optional) - (adtlChunkSize > 0 ? 8 + 4 + adtlChunkSize : 0); // LIST chunk (optional) - - // RIFF header - writer.Write(Encoding.ASCII.GetBytes("RIFF")); - writer.Write(riffSize); - writer.Write(Encoding.ASCII.GetBytes("WAVE")); - - // fmt chunk - WriteFmtChunk(writer, sampleRate, channels); - - // data chunk - WriteDataChunk(writer, pcmSamples); - - // cue chunk (if we have markers) - if (markers.Length > 0) - { - WriteCueChunk(writer, markers, startSamplePosition, sampleRate, channels); - WriteListAdtlChunk(writer, markers, startSamplePosition, sampleRate, channels); - } - } - - /// - /// Converts 32-bit float samples to 16-bit PCM. - /// - private static byte[] ConvertTo16BitPcm(float[] samples) - { - var result = new byte[samples.Length * 2]; - - for (var i = 0; i < samples.Length; i++) - { - // Clamp to [-1, 1] and convert to 16-bit - var clamped = Math.Clamp(samples[i], -1.0f, 1.0f); - var pcm = (short)(clamped * 32767); - result[i * 2] = (byte)(pcm & 0xFF); - result[i * 2 + 1] = (byte)((pcm >> 8) & 0xFF); - } - - return result; - } - - /// - /// Writes the fmt chunk (audio format). - /// - private static void WriteFmtChunk(BinaryWriter writer, int sampleRate, int channels) - { - const int bitsPerSample = 16; - var blockAlign = (short)(channels * bitsPerSample / 8); - var byteRate = sampleRate * blockAlign; - - writer.Write(Encoding.ASCII.GetBytes("fmt ")); - writer.Write(16); // Chunk size (16 for PCM) - writer.Write((short)1); // Audio format (1 = PCM) - writer.Write((short)channels); - writer.Write(sampleRate); - writer.Write(byteRate); - writer.Write(blockAlign); - writer.Write((short)bitsPerSample); - } - - /// - /// Writes the data chunk (audio samples). - /// - private static void WriteDataChunk(BinaryWriter writer, byte[] pcmSamples) - { - writer.Write(Encoding.ASCII.GetBytes("data")); - writer.Write(pcmSamples.Length); - writer.Write(pcmSamples); - } - - /// - /// Writes the cue chunk (marker positions). - /// - private static void WriteCueChunk( - BinaryWriter writer, - SyncMetricSnapshot[] markers, - long startSamplePosition, - int sampleRate, - int channels) - { - // Cue chunk header - writer.Write(Encoding.ASCII.GetBytes("cue ")); - writer.Write(4 + (24 * markers.Length)); // Chunk size - writer.Write(markers.Length); // Number of cue points - - for (var i = 0; i < markers.Length; i++) - { - var marker = markers[i]; - - // Calculate sample offset within the WAV file (in frames, not samples) - var sampleOffset = marker.SamplePosition - startSamplePosition; - var frameOffset = (int)(sampleOffset / channels); // Convert to frames - - if (frameOffset < 0) - { - frameOffset = 0; - } - - writer.Write(i + 1); // Cue point ID (1-indexed) - writer.Write(frameOffset); // Position (sample offset in playlist order) - writer.Write(Encoding.ASCII.GetBytes("data")); // Data chunk ID - writer.Write(0); // Chunk start (0 for data chunk) - writer.Write(0); // Block start - writer.Write(frameOffset); // Sample offset within data chunk - } - } - - /// - /// Calculates the size of all labl and note sub-chunks. - /// - private static int CalculateAdtlChunkSize( - SyncMetricSnapshot[] markers, - long startSamplePosition, - int sampleRate, - int channels) - { - if (markers.Length == 0) - { - return 0; - } - - var size = 0; - - foreach (var marker in markers) - { - // labl sub-chunk: 4 (chunk ID) + 4 (size) + 4 (cue ID) + label bytes + padding - var label = marker.FormatShortLabel(); - var labelBytes = Encoding.ASCII.GetByteCount(label) + 1; // +1 for null terminator - var labelPadded = (labelBytes + 1) & ~1; // Pad to even - size += 8 + 4 + labelPadded; - } - - return size; - } - - /// - /// Writes the LIST adtl chunk (marker labels). - /// - private static void WriteListAdtlChunk( - BinaryWriter writer, - SyncMetricSnapshot[] markers, - long startSamplePosition, - int sampleRate, - int channels) - { - var adtlSize = CalculateAdtlChunkSize(markers, startSamplePosition, sampleRate, channels); - - // LIST chunk header - writer.Write(Encoding.ASCII.GetBytes("LIST")); - writer.Write(4 + adtlSize); // Chunk size (type ID + sub-chunks) - writer.Write(Encoding.ASCII.GetBytes("adtl")); - - // Write labl sub-chunks - for (var i = 0; i < markers.Length; i++) - { - var marker = markers[i]; - var label = marker.FormatShortLabel(); - var labelBytes = Encoding.ASCII.GetBytes(label + '\0'); // Null-terminated - var paddedSize = (labelBytes.Length + 1) & ~1; // Pad to even boundary - - writer.Write(Encoding.ASCII.GetBytes("labl")); - writer.Write(4 + labelBytes.Length); // Size = cue ID + label - writer.Write(i + 1); // Cue point ID (1-indexed) - writer.Write(labelBytes); - - // Pad to even boundary if needed - if (labelBytes.Length % 2 != 0) - { - writer.Write((byte)0); - } - } - } -} diff --git a/src/SendspinClient.Services/Diagnostics/IDiagnosticAudioRecorder.cs b/src/SendspinClient.Services/Diagnostics/IDiagnosticAudioRecorder.cs deleted file mode 100644 index 063ef39..0000000 --- a/src/SendspinClient.Services/Diagnostics/IDiagnosticAudioRecorder.cs +++ /dev/null @@ -1,91 +0,0 @@ -// -// Licensed under the MIT License. See LICENSE file in the project root. -// - -using Sendspin.SDK.Audio; - -namespace SendspinClient.Services.Diagnostics; - -/// -/// Interface for diagnostic audio recording with sync metrics. -/// -/// -/// -/// The recorder captures audio samples and sync metrics for later analysis. -/// When enabled, it maintains a circular buffer of the last N seconds of audio -/// with embedded metric markers that can be saved to a WAV file on demand. -/// -/// -/// Usage pattern: -/// -/// Call to start capturing -/// Call from the audio thread to capture samples -/// Call periodically to capture sync state -/// Call to dump the buffer to a WAV file -/// Call to stop capturing and free memory -/// -/// -/// -public interface IDiagnosticAudioRecorder : IDisposable -{ - /// - /// Gets a value indicating whether recording is currently enabled. - /// - bool IsEnabled { get; } - - /// - /// Gets the duration of audio currently buffered in seconds. - /// Returns 0 if recording is disabled. - /// - double BufferedSeconds { get; } - - /// - /// Gets the configured buffer duration in seconds. - /// - int BufferDurationSeconds { get; } - - /// - /// Raised when the recording state changes (enabled/disabled). - /// - event EventHandler? RecordingStateChanged; - - /// - /// Enables recording and allocates the buffer. - /// - /// The audio sample rate (e.g., 48000). - /// The number of channels (e.g., 2 for stereo). - void Enable(int sampleRate, int channels); - - /// - /// Disables recording and frees the buffer. - /// - void Disable(); - - /// - /// Captures audio samples if recording is enabled. - /// - /// - /// - /// This method is called from the audio thread and MUST NOT BLOCK. - /// When disabled, this is effectively a no-op (single boolean check). - /// - /// - /// The audio samples to capture. - void CaptureIfEnabled(ReadOnlySpan samples); - - /// - /// Records current sync metrics for correlation with audio. - /// - /// - /// Call this periodically (e.g., every 100ms) from the stats update loop. - /// - /// Current audio buffer statistics. - void RecordMetrics(AudioBufferStats stats); - - /// - /// Saves the current buffer to a WAV file with embedded markers. - /// - /// The directory to save to. - /// The full path to the saved WAV file, or null if recording is disabled. - Task SaveAsync(string directory); -} diff --git a/src/SendspinClient/App.xaml.cs b/src/SendspinClient/App.xaml.cs index c79f1c6..fed4d00 100644 --- a/src/SendspinClient/App.xaml.cs +++ b/src/SendspinClient/App.xaml.cs @@ -12,7 +12,6 @@ using Sendspin.SDK.Models; using Sendspin.SDK.Synchronization; using SendspinClient.Services.Audio; -using SendspinClient.Services.Diagnostics; using SendspinClient.Services.Discord; using SendspinClient.Services.Models; using SendspinClient.Services.Notifications; @@ -277,22 +276,10 @@ private void ConfigureServices(IServiceCollection services) ? ResamplerType.SoundTouch : ResamplerType.Wdl; - // Read diagnostics configuration - var diagnosticsSettings = new DiagnosticsSettings(); - _configuration!.GetSection(DiagnosticsSettings.SectionName).Bind(diagnosticsSettings); - - // Diagnostic audio recorder for capturing audio with sync metrics - services.AddSingleton(sp => - { - var logger = sp.GetRequiredService>(); - return new DiagnosticAudioRecorder(logger, diagnosticsSettings.BufferSeconds); - }); - services.AddTransient(sp => { var logger = sp.GetRequiredService>(); - var diagnosticRecorder = sp.GetRequiredService(); - return new WasapiAudioPlayer(logger, audioDeviceId, syncStrategy, resamplerType, diagnosticRecorder); + return new WasapiAudioPlayer(logger, audioDeviceId, syncStrategy, resamplerType); }); // Audio pipeline - orchestrates decoder, buffer, and player diff --git a/src/SendspinClient/Configuration/AppPaths.cs b/src/SendspinClient/Configuration/AppPaths.cs index 670e66c..b905bb4 100644 --- a/src/SendspinClient/Configuration/AppPaths.cs +++ b/src/SendspinClient/Configuration/AppPaths.cs @@ -34,12 +34,6 @@ public static class AppPaths /// public static string LogDirectory { get; } = Path.Combine(UserDataDirectory, "logs"); - /// - /// Gets the diagnostics directory for storing diagnostic audio recordings. - /// Located at %LocalAppData%\WindowsSpin\diagnostics\. - /// - public static string DiagnosticsDirectory { get; } = Path.Combine(UserDataDirectory, "diagnostics"); - /// /// Gets the installation directory where the application executable is located. /// @@ -67,14 +61,6 @@ public static void EnsureLogDirectoryExists() Directory.CreateDirectory(LogDirectory); } - /// - /// Ensures the diagnostics directory exists. - /// - public static void EnsureDiagnosticsDirectoryExists() - { - Directory.CreateDirectory(DiagnosticsDirectory); - } - /// /// Copies the default settings to the user settings location if no user settings exist. /// This provides first-run initialization with factory defaults. diff --git a/src/SendspinClient/Configuration/DiagnosticsSettings.cs b/src/SendspinClient/Configuration/DiagnosticsSettings.cs deleted file mode 100644 index 52ae464..0000000 --- a/src/SendspinClient/Configuration/DiagnosticsSettings.cs +++ /dev/null @@ -1,58 +0,0 @@ -// -// Licensed under the MIT License. See LICENSE file in the project root. -// - -namespace SendspinClient.Configuration; - -/// -/// Configuration settings for diagnostic audio recording. -/// These settings can be modified in appsettings.json. -/// -public class DiagnosticsSettings -{ - /// - /// The configuration section name in appsettings.json. - /// - public const string SectionName = "Diagnostics"; - - /// - /// Gets or sets whether diagnostic recording is enabled by default. - /// When false (default), recording must be manually enabled via the Stats window. - /// Default: false. - /// - public bool EnableRecording { get; set; } - - /// - /// Gets or sets the duration of the circular audio buffer in seconds. - /// Determines how many seconds of audio can be captured when saving. - /// Higher values use more memory (~384KB per second for 48kHz stereo). - /// Default: 45 seconds (~17MB). - /// - public int BufferSeconds { get; set; } = 45; - - /// - /// Gets or sets the interval in milliseconds for recording sync metrics. - /// Lower values provide more detailed markers but use more memory. - /// Default: 100ms. - /// - public int MetricIntervalMs { get; set; } = 100; - - /// - /// Gets or sets the directory for diagnostic recordings. - /// If empty, defaults to %LocalAppData%\WindowsSpin\diagnostics\. - /// - public string OutputDirectory { get; set; } = string.Empty; - - /// - /// Gets the effective output directory, using the default if none specified. - /// - public string GetEffectiveOutputDirectory() - { - if (!string.IsNullOrWhiteSpace(OutputDirectory)) - { - return Environment.ExpandEnvironmentVariables(OutputDirectory); - } - - return AppPaths.DiagnosticsDirectory; - } -} diff --git a/src/SendspinClient/ViewModels/MainViewModel.cs b/src/SendspinClient/ViewModels/MainViewModel.cs index a985a02..c59acc4 100644 --- a/src/SendspinClient/ViewModels/MainViewModel.cs +++ b/src/SendspinClient/ViewModels/MainViewModel.cs @@ -20,7 +20,6 @@ using Sendspin.SDK.Models; using Sendspin.SDK.Protocol.Messages; using Sendspin.SDK.Synchronization; -using SendspinClient.Services.Diagnostics; using SendspinClient.Services.Discord; using SendspinClient.Services.Notifications; using SendspinClient.Views; @@ -81,7 +80,6 @@ public partial class MainViewModel : ViewModelBase private readonly IHttpClientFactory _httpClientFactory; private readonly ClientCapabilities _clientCapabilities; private readonly IUserSettingsService _settingsService; - private readonly IDiagnosticAudioRecorder _diagnosticRecorder; private SendspinClientService? _manualClient; private ISendspinConnection? _manualConnection; private readonly SemaphoreSlim _cleanupLock = new(1, 1); @@ -442,8 +440,7 @@ public MainViewModel( IDiscordRichPresenceService discordService, IHttpClientFactory httpClientFactory, ClientCapabilities clientCapabilities, - IUserSettingsService settingsService, - IDiagnosticAudioRecorder diagnosticRecorder) + IUserSettingsService settingsService) { _logger = logger; _loggerFactory = loggerFactory; @@ -457,7 +454,6 @@ public MainViewModel( _httpClientFactory = httpClientFactory; _clientCapabilities = clientCapabilities; _settingsService = settingsService; - _diagnosticRecorder = diagnosticRecorder; // Load current logging settings LoadLoggingSettings(); @@ -2340,7 +2336,7 @@ private void OpenStatsWindow() { try { - var statsViewModel = new StatsViewModel(_audioPipeline, _clockSynchronizer, _diagnosticRecorder, _clientCapabilities); + var statsViewModel = new StatsViewModel(_audioPipeline, _clockSynchronizer, _clientCapabilities); var statsWindow = new StatsWindow(statsViewModel) { Owner = App.Current.MainWindow, diff --git a/src/SendspinClient/ViewModels/StatsViewModel.cs b/src/SendspinClient/ViewModels/StatsViewModel.cs index 3066752..21a37cf 100644 --- a/src/SendspinClient/ViewModels/StatsViewModel.cs +++ b/src/SendspinClient/ViewModels/StatsViewModel.cs @@ -10,8 +10,6 @@ using Sendspin.SDK.Client; using Sendspin.SDK.Synchronization; using SendspinClient.Configuration; -using SendspinClient.Services.Diagnostics; - namespace SendspinClient.ViewModels; /// @@ -26,11 +24,9 @@ public partial class StatsViewModel : ViewModelBase { private readonly IAudioPipeline _audioPipeline; private readonly IClockSynchronizer _clockSynchronizer; - private readonly IDiagnosticAudioRecorder _diagnosticRecorder; private readonly ClientCapabilities _clientCapabilities; private readonly DispatcherTimer _updateTimer; private const int UpdateIntervalMs = 100; // 10 updates per second - private bool _isSaving; #region Sync Status Properties @@ -312,53 +308,6 @@ public partial class StatsViewModel : ViewModelBase #endregion - #region Diagnostic Recording Properties - - /// - /// Gets or sets whether diagnostic recording is enabled. - /// When enabled, allocates ~17MB for a 45-second circular buffer. - /// - [ObservableProperty] - private bool _isDiagnosticRecordingEnabled; - - /// - /// Gets the recording status display string. - /// - [ObservableProperty] - private string _recordingStatusDisplay = "Off"; - - /// - /// Gets the color for the recording status. - /// - [ObservableProperty] - private Brush _recordingStatusColor = Brushes.Gray; - - /// - /// Gets whether saving is available (recording is enabled and has data). - /// - [ObservableProperty] - private bool _canSaveRecording; - - /// - /// Gets the buffer duration in seconds for display. - /// - [ObservableProperty] - private string _bufferDurationDisplay = "45s"; - - /// - /// Gets whether a save operation is in progress. - /// - [ObservableProperty] - private bool _isSavingRecording; - - /// - /// Gets the path to the most recently saved recording. - /// - [ObservableProperty] - private string? _lastSavedRecordingPath; - - #endregion - /// /// Gets the update rate display string. /// @@ -370,17 +319,14 @@ public partial class StatsViewModel : ViewModelBase /// /// The audio pipeline to monitor. /// The clock synchronizer to monitor. - /// The diagnostic audio recorder. /// The client capabilities for advertised format display. public StatsViewModel( IAudioPipeline audioPipeline, IClockSynchronizer clockSynchronizer, - IDiagnosticAudioRecorder diagnosticRecorder, ClientCapabilities clientCapabilities) { _audioPipeline = audioPipeline; _clockSynchronizer = clockSynchronizer; - _diagnosticRecorder = diagnosticRecorder; _clientCapabilities = clientCapabilities; _updateTimer = new DispatcherTimer @@ -389,10 +335,6 @@ public StatsViewModel( }; _updateTimer.Tick += OnUpdateTimerTick; - // Initialize diagnostic recording display - BufferDurationDisplay = $"{_diagnosticRecorder.BufferDurationSeconds}s buffer"; - UpdateRecordingStatus(); - // Initialize advertised capabilities display UpdateAdvertisedCapabilities(); } @@ -424,14 +366,6 @@ private void UpdateStats() UpdateBufferStats(); UpdateClockSyncStats(); UpdateAudioFormatStats(); - UpdateRecordingStatus(); - - // Record sync metrics for diagnostic correlation if enabled - var stats = _audioPipeline.BufferStats; - if (stats != null) - { - _diagnosticRecorder.RecordMetrics(stats); - } } private void UpdateBufferStats() @@ -725,105 +659,4 @@ private static string FormatOutputFormat(Sendspin.SDK.Models.AudioFormat format) return $"PCM {format.SampleRate}Hz {format.Channels}ch 32-bit float"; } - #region Diagnostic Recording Methods - - /// - /// Updates the recording status display. - /// - private void UpdateRecordingStatus() - { - if (_isSaving) - { - RecordingStatusDisplay = "Saving..."; - RecordingStatusColor = new SolidColorBrush(Color.FromRgb(0x60, 0xa5, 0xfa)); // Blue - CanSaveRecording = false; - return; - } - - if (!_diagnosticRecorder.IsEnabled) - { - RecordingStatusDisplay = "Off"; - RecordingStatusColor = Brushes.Gray; - CanSaveRecording = false; - return; - } - - var bufferedSeconds = _diagnosticRecorder.BufferedSeconds; - RecordingStatusDisplay = $"Recording ({bufferedSeconds:F0}s)"; - RecordingStatusColor = new SolidColorBrush(Color.FromRgb(0xf8, 0x71, 0x71)); // Red (recording) - CanSaveRecording = bufferedSeconds > 0; - } - - /// - /// Called when IsDiagnosticRecordingEnabled changes. - /// - /// The new value. - partial void OnIsDiagnosticRecordingEnabledChanged(bool value) - { - if (value) - { - // Enable recording - need to get format from pipeline - var outputFormat = _audioPipeline.OutputFormat; - if (outputFormat != null) - { - _diagnosticRecorder.Enable(outputFormat.SampleRate, outputFormat.Channels); - } - else - { - // Default to 48kHz stereo if no format available yet - _diagnosticRecorder.Enable(48000, 2); - } - } - else - { - _diagnosticRecorder.Disable(); - } - - UpdateRecordingStatus(); - } - - /// - /// Saves the current diagnostic recording to a WAV file. - /// - [RelayCommand] - private async Task SaveDiagnosticRecordingAsync() - { - if (_isSaving || !_diagnosticRecorder.IsEnabled) - { - return; - } - - _isSaving = true; - IsSavingRecording = true; - UpdateRecordingStatus(); - - try - { - AppPaths.EnsureDiagnosticsDirectoryExists(); - var path = await _diagnosticRecorder.SaveAsync(AppPaths.DiagnosticsDirectory); - LastSavedRecordingPath = path; - } - finally - { - _isSaving = false; - IsSavingRecording = false; - UpdateRecordingStatus(); - } - } - - /// - /// Opens the diagnostics folder in Windows Explorer. - /// - [RelayCommand] - private void OpenDiagnosticsFolder() - { - AppPaths.EnsureDiagnosticsDirectoryExists(); - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo - { - FileName = AppPaths.DiagnosticsDirectory, - UseShellExecute = true, - }); - } - - #endregion } diff --git a/src/SendspinClient/Views/StatsWindow.xaml b/src/SendspinClient/Views/StatsWindow.xaml index 720f923..1bcdd28 100644 --- a/src/SendspinClient/Views/StatsWindow.xaml +++ b/src/SendspinClient/Views/StatsWindow.xaml @@ -301,121 +301,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/SendspinClient/appsettings.json b/src/SendspinClient/appsettings.json index a314b4c..8bcf925 100644 --- a/src/SendspinClient/appsettings.json +++ b/src/SendspinClient/appsettings.json @@ -8,11 +8,6 @@ "RetainedFileCount": 5, "OutputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.ffffff} [{Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}" }, - "Diagnostics": { - "EnableRecording": false, - "BufferSeconds": 45, - "MetricIntervalMs": 100 - }, "Audio": { "PreferredCodec": "flac", "StaticDelayMs": 200,