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,