From c63c6da2550c5a15c314eb1885508d88c3b15343 Mon Sep 17 00:00:00 2001 From: Alexander Shabarshov Date: Tue, 7 Jul 2026 10:25:33 +0100 Subject: [PATCH] Separate class for Ffmpeg process handling --- AudioCore/Impl/FfmpegPipe.cs | 73 ++------- AudioCore/Impl/FfmpegProcess.cs | 122 ++++++++++++++ AudioCore/Impl/Htdemucs6sSeparator.cs | 155 ++++++------------ AudioCore/Impl/RubberBandTimeStretchEngine.cs | 67 ++++---- AudioCore_Tests/FfmpegAudioReader_Tests.cs | 23 +++ AudioCore_Tests/Pipeline_Integration_Tests.cs | 22 +++ 6 files changed, 270 insertions(+), 192 deletions(-) create mode 100644 AudioCore/Impl/FfmpegProcess.cs diff --git a/AudioCore/Impl/FfmpegPipe.cs b/AudioCore/Impl/FfmpegPipe.cs index 0546a5e..24ff8ef 100644 --- a/AudioCore/Impl/FfmpegPipe.cs +++ b/AudioCore/Impl/FfmpegPipe.cs @@ -5,8 +5,7 @@ namespace AudioCore.Impl; public sealed class FfmpegPipe : IDisposable { private readonly string _path; - private Process? _proc; - private Stream? _stdout; + private FfmpegProcess? _process; public int SampleRate { get; } public int Channels { get; } @@ -18,7 +17,6 @@ public sealed class FfmpegPipe : IDisposable SampleRate = sampleRate; Channels = channels; - // Optional: probe duration TotalSamples = ProbeTotalSamples(path, sampleRate); StartProcess(0); @@ -28,41 +26,28 @@ public sealed class FfmpegPipe : IDisposable { var startSeconds = (double)startSample / SampleRate; - var psi = new ProcessStartInfo - { - FileName = "ffmpeg", - Arguments = - $"-hide_banner -loglevel error " + - $"-nostdin " + // prevent console attach + var cmd = + "-hide_banner -loglevel error " + + "-nostdin " + $"-ss {startSeconds.ToString(System.Globalization.CultureInfo.InvariantCulture)} " + $"-i \"{_path}\" " + - $"-f f32le -ac {Channels} -ar {SampleRate} pipe:1", + $"-f f32le -ac {Channels} -ar {SampleRate} pipe:1"; - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - RedirectStandardInput = true, // prevents console window - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - ErrorDialog = false - }; + _process = new FfmpegProcess( + name: $"pipe:{_path}", + commandLine: cmd, + redirectOutput: true, + redirectInput: true); - _proc = Process.Start(psi); - _stdout = _proc!.StandardOutput.BaseStream; + _process.StartProcess(); } public int Read(float[] buffer, int offset, int count) { - var bytesNeeded = count * sizeof(float); - var tmp = new byte[bytesNeeded]; - - var readBytes = _stdout!.Read(tmp, 0, bytesNeeded); - if (readBytes <= 0) + if (_process?.Stdout is null) return 0; - Buffer.BlockCopy(tmp, 0, buffer, offset * sizeof(float), readBytes); - - return readBytes / sizeof(float); + return _process.Read(buffer, offset, count); } public void Seek(long sampleIndex) @@ -71,38 +56,16 @@ public sealed class FfmpegPipe : IDisposable StartProcess(sampleIndex); } - public void Reset() => Seek(0); - - private static long ProbeTotalSamples(string path, int sampleRate) + public void Reset() { - var psi = new ProcessStartInfo - { - FileName = "ffprobe", - Arguments = $"-v error -show_entries format=duration -of csv=p=0 \"{path}\"", - RedirectStandardOutput = true, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - UseShellExecute = false - }; - - using var p = Process.Start(psi); - var s = p!.StandardOutput.ReadToEnd(); - p.WaitForExit(); - - if (double.TryParse(s, System.Globalization.NumberStyles.Float, - System.Globalization.CultureInfo.InvariantCulture, out var seconds)) - { - return (long)(seconds * sampleRate); - } - - return 0; + Seek(0); } + private static long ProbeTotalSamples(string path, int sampleRate) => FfmpegProcess.ProbeTotalSamples(path, sampleRate); private void DisposeProcessOnly() { - try { _stdout?.Dispose(); } catch { } - try { if (_proc != null && !_proc.HasExited) _proc.Kill(); } catch { } - try { _proc?.Dispose(); } catch { } + try { _process?.Dispose(); } catch { } + _process = null; } public void Dispose() diff --git a/AudioCore/Impl/FfmpegProcess.cs b/AudioCore/Impl/FfmpegProcess.cs new file mode 100644 index 0000000..140a3b6 --- /dev/null +++ b/AudioCore/Impl/FfmpegProcess.cs @@ -0,0 +1,122 @@ +using System.Diagnostics; + +namespace AudioCore.Impl; + +public sealed class FfmpegProcess : IDisposable +{ + public Process? Proc { get; private set; } + public Stream? Stdout { get; private set; } + public Stream? Stdin { get; private set; } + + private string _name; + private string _commandLine; + private bool _redirectOutput; + private bool _redirectInput; + + public FfmpegProcess(string name, string commandLine, bool redirectOutput = true, bool redirectInput = true) + { + _name = name; + _commandLine = commandLine; + _redirectOutput = redirectOutput; + _redirectInput = redirectInput; + } + + public void StartProcess() + { + var psi = new ProcessStartInfo + { + FileName = "ffmpeg", + Arguments = _commandLine, + UseShellExecute = false, + RedirectStandardOutput = _redirectOutput, + RedirectStandardError = true, + RedirectStandardInput = _redirectInput, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + }; + + Proc = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start ffmpeg process"); + + if ( _redirectInput) + Stdin = Proc.StandardInput.BaseStream; + if (_redirectOutput) + Stdout = Proc.StandardOutput.BaseStream; + + // Start draining stderr immediately + _ = Task.Run(() => DrainStderr(Proc)); + } + + private void DrainStderr(Process proc) + { + try + { + var reader = proc.StandardError; + + // ffmpeg writes short lines, so ReadLine is fine + // If you want zero allocations, use ReadAsync into a rented buffer. + string? line; + while ((line = reader.ReadLine()) != null) + { + Debug.WriteLine($"{_name}: {line}"); + } + } + catch + { + // ignore exceptions during stderr drain, as the process may have exited + } + } + + public int Read(float[] buffer, int offset, int count) + { + var bytesNeeded = count * sizeof(float); + var tmp = new byte[bytesNeeded]; + + var readBytes = Stdout!.Read(tmp, 0, bytesNeeded); + if (readBytes <= 0) + return 0; + + Buffer.BlockCopy(tmp, 0, buffer, offset * sizeof(float), readBytes); + + return readBytes / sizeof(float); + } + + public static long ProbeTotalSamples(string path, int sampleRate) + { + var psi = new ProcessStartInfo + { + FileName = "ffprobe", + Arguments = $"-v error -show_entries format=duration -of csv=p=0 \"{path}\"", + RedirectStandardOutput = true, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + UseShellExecute = false + }; + + using var p = Process.Start(psi); + var s = p!.StandardOutput.ReadToEnd(); + p.WaitForExit(); + + if (double.TryParse(s, System.Globalization.NumberStyles.Float, + System.Globalization.CultureInfo.InvariantCulture, out var seconds)) + { + return (long)(seconds * sampleRate); + } + + return 0; + } + + private void DisposeProcessOnly() + { + try { Stdout?.Dispose(); } catch { } + try { Stdin?.Dispose(); } catch { } + try { Proc?.StandardError.BaseStream?.Dispose(); } catch { } + try { if (Proc != null && !Proc.HasExited) Proc.Kill(); } catch { } + try { Proc?.Dispose(); } catch { } + } + + public void Dispose() + { + DisposeProcessOnly(); + } +} diff --git a/AudioCore/Impl/Htdemucs6sSeparator.cs b/AudioCore/Impl/Htdemucs6sSeparator.cs index 6e809a5..a8920ed 100644 --- a/AudioCore/Impl/Htdemucs6sSeparator.cs +++ b/AudioCore/Impl/Htdemucs6sSeparator.cs @@ -9,9 +9,9 @@ public sealed class Htdemucs6sSeparator : IStemSeparator private const int _sampleRate = 44100; private const int _channels = 2; private const double _segmentSeconds = 7.8; - private const int _segmentSamples = (int)(_sampleRate * _segmentSeconds); // 343,980 - private const int _overlap = _segmentSamples / 4; // 85,995 - private const int _stride = _segmentSamples - _overlap; // 257,985 + private const int _segmentSamples = (int)(_sampleRate * _segmentSeconds); + private const int _overlap = _segmentSamples / 4; + private const int _stride = _segmentSamples - _overlap; private static readonly string[] _stemNames = Enum.GetNames(); @@ -26,14 +26,12 @@ public sealed class Htdemucs6sSeparator : IStemSeparator if (existingStems != null) return existingStems; - // 1. Load audio var mix = LoadStereoFloatWave(request.SourceFilePath, out var sr); if (sr != _sampleRate) throw new InvalidOperationException($"Input must be {_sampleRate} Hz"); var totalSamples = mix.GetLength(1); - // 2. Prepare ONNX session var opts = new SessionOptions(); opts.AppendExecutionProvider_CPU(); opts.GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL; @@ -41,30 +39,24 @@ public sealed class Htdemucs6sSeparator : IStemSeparator var modelPath = Path.Combine(AppContext.BaseDirectory, "Data", "htdemucs_6s.onnx"); using var session = new InferenceSession(modelPath, opts); - // 3. Prepare buffers var outStems = new float[_stemNames.Length, _channels, totalSamples]; var weight = new float[totalSamples]; var window = MakeWindow(_segmentSamples, _overlap); var nChunks = Math.Max(1, (totalSamples + _stride - 1) / _stride); - // 4. Sliding window inference for (var i = 0; i < nChunks; i++) { ct.ThrowIfCancellationRequested(); var start = i * _stride; - var end = Math.Min(start + _segmentSamples, totalSamples); - var clen = end - start; + var end = Math.Min(start + _segmentSamples, totalSamples); + var clen = end - start; - // Extract chunk into [2, N] var chunk = new float[_channels, _segmentSamples]; for (var ch = 0; ch < _channels; ch++) - { Array.Copy(mix, ch * totalSamples + start, chunk, ch * _segmentSamples, clen); - } - // Build flat input buffer (1,2,N) var inputData = new float[_channels * _segmentSamples]; for (var ch = 0; ch < _channels; ch++) { @@ -73,42 +65,31 @@ public sealed class Htdemucs6sSeparator : IStemSeparator inputData[baseIndex + s] = chunk[ch, s]; } - // Create OrtValue for input using var inputOrtValue = OrtValue.CreateTensorValueFromMemory( - inputData, - new long[] { 1, _channels, _segmentSamples } -); + inputData, + new long[] { 1, _channels, _segmentSamples }); - // Prepare output buffer (CPU) var outputData = new float[_stemNames.Length * _channels * _segmentSamples]; - // Create OrtValue for output using var outputOrtValue = OrtValue.CreateTensorValueFromMemory( - outputData, - new long[] { 1, _stemNames.Length, _channels, _segmentSamples } -); + outputData, + new long[] { 1, _stemNames.Length, _channels, _segmentSamples }); - // Bind using IOBinding using var io = session.CreateIoBinding(); io.BindInput("mix", inputOrtValue); io.BindOutput("stems", outputOrtValue); - // Execute on GPU → output goes directly to CPU buffer session.RunWithBinding(new RunOptions(), io); - // Now outputData contains (1,6,2,N) var buf = outputData.AsSpan(); - // Extract output tensor shape (1, 6, 2, N) - var stemCnt = _stemNames.Length; // 6 - var chCnt = _channels; // 2 - var length = _segmentSamples; // 343980 + var stemCnt = _stemNames.Length; + var chCnt = _channels; + var length = _segmentSamples; - // Compute strides for flattened buffer - var stemStride = chCnt * length; // 2 * N - var channelStride = length; // N + var stemStride = chCnt * length; + var channelStride = length; - // Overlap-add for (var stem = 0; stem < stemCnt; stem++) { for (var ch = 0; ch < chCnt; ch++) @@ -119,7 +100,6 @@ public sealed class Htdemucs6sSeparator : IStemSeparator { var w = window[s]; var v = buf[baseIndex + s]; - outStems[stem, ch, start + s] += v * w; } } @@ -131,7 +111,6 @@ public sealed class Htdemucs6sSeparator : IStemSeparator await progress.ReportProgress((double)(i + 1) / nChunks, ct); } - // 5. Normalize by weight for (var stem = 0; stem < _stemNames.Length; stem++) { for (var ch = 0; ch < _channels; ch++) @@ -145,36 +124,37 @@ public sealed class Htdemucs6sSeparator : IStemSeparator } } - // 6. Write stems var result = new List(); for (var i = 0; i < _stemNames.Length; i++) { var name = $"{Path.GetFileNameWithoutExtension(request.SourceFilePath)}_{_stemNames[i]}.flac"; var path = Path.Combine(request.OutputDirectory, name); + WriteFlac(path, outStems, i, totalSamples); result.Add(new StemTrack { - Type = Enum.Parse(_stemNames[i]), - Name = name, - FilePath = path, + Type = Enum.Parse(_stemNames[i]), + Name = name, + FilePath = path, SampleRate = _sampleRate, - Channels = _channels, - Duration = TimeSpan.FromSeconds((double)totalSamples / _sampleRate) + Channels = _channels, + Duration = TimeSpan.FromSeconds((double)totalSamples / _sampleRate) }); } - return new StemSet - { - OriginalFilePath = request.SourceFilePath, - Stems = result - }; + return new StemSet + { + OriginalFilePath = request.SourceFilePath, + Stems = result + }; } private StemSet? CheckExistingStems(StemSeparationRequest request) { var filesToCheck = Enum.GetNames(typeof(StemType)) - .Select(stemType => (stemType, Path.Combine(request.OutputDirectory, $"{Path.GetFileNameWithoutExtension(request.SourceFilePath)}_{stemType}.flac"))) + .Select(stemType => (stemType, Path.Combine(request.OutputDirectory, + $"{Path.GetFileNameWithoutExtension(request.SourceFilePath)}_{stemType}.flac"))) .ToList(); var stems = new List(); @@ -188,26 +168,20 @@ public sealed class Htdemucs6sSeparator : IStemSeparator { using var reader = new AudioFileReader(f.Item2); - var stem =new StemTrack + stems.Add(new StemTrack { - Type = Enum.Parse(f.Item1), - Name = Path.GetFileName(f.Item2), - FilePath = f.Item2, + Type = Enum.Parse(f.Item1), + Name = Path.GetFileName(f.Item2), + FilePath = f.Item2, SampleRate = reader.WaveFormat.SampleRate, - Channels = reader.WaveFormat.Channels, - Duration = reader.TotalTime - }; - - stems.Add(stem); + Channels = reader.WaveFormat.Channels, + Duration = reader.TotalTime + }); } return stems.Count > 0 ? set : null; } - // ------------------------------ - // Helpers - // ------------------------------ - private static float[] MakeWindow(int n, int overlap) { var w = new float[n]; @@ -245,59 +219,38 @@ public sealed class Htdemucs6sSeparator : IStemSeparator return result; } - //private static void WriteWave(string path, float[,,] stems, int stemIndex, int totalSamples) - //{ - // var format = WaveFormat.CreateIeeeFloatWaveFormat(_sampleRate, _channels); - - // using var writer = new WaveFileWriter(path, format); - - // for (int i = 0; i < totalSamples; i++) - // { - // writer.WriteSample(stems[stemIndex, 0, i]); - // writer.WriteSample(stems[stemIndex, 1, i]); - // } - //} - private static void WriteFlac(string path, float[,,] stems, int stemIndex, int totalSamples) { - var psi = new ProcessStartInfo - { - FileName = "ffmpeg", - Arguments = + var cmd = "-y " + - "-f f32le " + // raw float32 little-endian - "-ar 44100 " + // sample rate - "-ac 2 " + // channels - "-i pipe:0 " + // read from stdin - "-compression_level 12 " + // max FLAC compression - $"\"{path}\"", - RedirectStandardInput = true, - RedirectStandardError = true, - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - }; + "-f f32le " + + "-ar 44100 " + + "-ac 2 " + + "-i pipe:0 " + + "-compression_level 12 " + + $"\"{path}\""; - using var ff = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start FFmpeg process"); - using var stdin = ff.StandardInput.BaseStream; + using var ff = new FfmpegProcess( + name: $"flac:{Path.GetFileName(path)}", + commandLine: cmd, + redirectOutput: true, + redirectInput: true); - // Write raw float32 PCM directly to FFmpeg - var buffer = new byte[sizeof(float) * 2]; // stereo frame + ff.StartProcess(); + + var stdin = ff.Stdin!; + var frame = new byte[sizeof(float) * 2]; for (var i = 0; i < totalSamples; i++) { - BitConverter.TryWriteBytes(buffer.AsSpan(0, 4), stems[stemIndex, 0, i]); - BitConverter.TryWriteBytes(buffer.AsSpan(4, 4), stems[stemIndex, 1, i]); - stdin.Write(buffer, 0, buffer.Length); + BitConverter.TryWriteBytes(frame.AsSpan(0, 4), stems[stemIndex, 0, i]); + BitConverter.TryWriteBytes(frame.AsSpan(4, 4), stems[stemIndex, 1, i]); + stdin.Write(frame, 0, frame.Length); } stdin.Flush(); stdin.Close(); - ff.WaitForExit(); + ff.Proc!.WaitForExit(); } - - - } diff --git a/AudioCore/Impl/RubberBandTimeStretchEngine.cs b/AudioCore/Impl/RubberBandTimeStretchEngine.cs index a08c6b9..8adefbd 100644 --- a/AudioCore/Impl/RubberBandTimeStretchEngine.cs +++ b/AudioCore/Impl/RubberBandTimeStretchEngine.cs @@ -9,7 +9,7 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl private readonly int _sampleRate; private readonly int _channels; - private Process? _ff; + private FfmpegProcess? _ff; private Stream? _stdin; private Stream? _stdout; @@ -29,7 +29,6 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl _sampleRate = sampleRate; _channels = channels; - // Ring buffer: e.g. 1 second of audio var bytesPerSecond = sampleRate * channels * sizeof(float); _ring = new byte[bytesPerSecond]; @@ -48,17 +47,16 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl public TimeStretchedAudioBlock Process(MixedAudioBlock input) { var expectedFloats = input.Frames * _channels; - var expectedBytes = expectedFloats * sizeof(float); + var expectedBytes = expectedFloats * sizeof(float); if (Math.Abs(_speed - 1.0f) < 0.01f) { var buf = _pool.Rent(expectedFloats); Array.Copy(input.Buffer.Samples, buf.Samples, input.Buffer.Length); - return new TimeStretchedAudioBlock(buf, input.Frames, _channels, _sampleRate); } - var span = input.Buffer.Span; + var span = input.Buffer.Span; var bytes = MemoryMarshal.AsBytes(span); _stdin!.Write(bytes); @@ -68,11 +66,10 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl if (available <= 0) return default; - var outBuf = _pool.Rent(expectedFloats); + var outBuf = _pool.Rent(expectedFloats); var outBytes = MemoryMarshal.AsBytes(outBuf.Span); var readBytes = DrainRing(outBytes, expectedBytes); - if (readBytes <= 0) { outBuf.Dispose(); @@ -94,8 +91,8 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl lock (_ringLock) { var available = (_ringWrite >= _ringRead) - ? _ringWrite - _ringRead - : _ring.Length - _ringRead + _ringWrite; + ? _ringWrite - _ringRead + : _ring.Length - _ringRead + _ringWrite; if (available > 0) return available; @@ -104,38 +101,34 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl Thread.Sleep(2); } + Debug.WriteLine("Rubberband: Timeout waiting for output from ffmpeg"); return 0; } - - private void StartProcess() { - var psi = new ProcessStartInfo - { - FileName = "ffmpeg", - Arguments = - $"-hide_banner -loglevel error " + - $"-f f32le -ar {_sampleRate} -ac {_channels} -i pipe:0 " + - $"-af \"rubberband=tempo={_speed}\" " + - $"-f f32le -ar {_sampleRate} -ac {_channels} pipe:1", - RedirectStandardInput = true, - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - }; + var cmd = + $"-hide_banner -loglevel error " + + $"-f f32le -ar {_sampleRate} -ac {_channels} -i pipe:0 " + + $"-af \"rubberband=tempo={_speed}\" " + + $"-f f32le -ar {_sampleRate} -ac {_channels} pipe:1"; - _ff = System.Diagnostics.Process.Start(psi); - _stdin = _ff!.StandardInput.BaseStream; - _stdout = _ff!.StandardOutput.BaseStream; + _ff = new FfmpegProcess( + name: $"rubberband:{_speed:F3}", + commandLine: cmd, + redirectOutput: true, + redirectInput: true); + + _ff.StartProcess(); + + _stdin = _ff.Stdin!; + _stdout = _ff.Stdout!; _readerRunning = true; - _readerThread = new Thread(ReaderLoop) { IsBackground = true }; + _readerThread = new Thread(ReaderLoop) { IsBackground = true }; _readerThread.Start(); } - private void RestartProcess() { DisposeProcess(); @@ -152,7 +145,6 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl while (_readerRunning) { var read = _stdout!.Read(buf, 0, buf.Length); - if (read <= 0) break; @@ -171,7 +163,7 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl } } } - catch { /* swallow for now */ } + catch { } } private int DrainRing(Span dest, int maxBytes) @@ -179,8 +171,8 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl lock (_ringLock) { var available = (_ringWrite >= _ringRead) - ? _ringWrite - _ringRead - : _ring.Length - _ringRead + _ringWrite; + ? _ringWrite - _ringRead + : _ring.Length - _ringRead + _ringWrite; if (available <= 0) return 0; @@ -203,7 +195,6 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl } } - private void ResetRing() { lock (_ringLock) @@ -216,9 +207,9 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl private void DisposeProcess() { _readerRunning = false; + try { _stdout?.Close(); } catch { } try { _stdin?.Close(); } catch { } - try { _ff?.Kill(); } catch { } try { _ff?.Dispose(); } catch { } if (_readerThread != null) @@ -226,6 +217,10 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl try { _readerThread.Join(500); } catch { } _readerThread = null; } + + _ff = null; + _stdin = null; + _stdout = null; } public void Dispose() => DisposeProcess(); diff --git a/AudioCore_Tests/FfmpegAudioReader_Tests.cs b/AudioCore_Tests/FfmpegAudioReader_Tests.cs index c5f9b7a..7444bd7 100644 --- a/AudioCore_Tests/FfmpegAudioReader_Tests.cs +++ b/AudioCore_Tests/FfmpegAudioReader_Tests.cs @@ -131,6 +131,9 @@ public sealed class FfmpegAudioReader_Tests using (var p = Process.Start(psi)) { + // Start draining stderr immediately + _ = Task.Run(() => DrainStderr(p)); + p!.WaitForExit(); Assert.AreEqual(0, p.ExitCode, "FFmpeg failed to convert MP3 to FLAC"); } @@ -204,4 +207,24 @@ public sealed class FfmpegAudioReader_Tests } } + private static void DrainStderr(Process proc) + { + try + { + var reader = proc.StandardError; + + // ffmpeg writes short lines, so ReadLine is fine + // If you want zero allocations, use ReadAsync into a rented buffer. + string? line; + while ((line = reader.ReadLine()) != null) + { + Debug.WriteLine(line); + } + } + catch (Exception ex) + { + Debug.WriteLine(ex.ToString()); + } + } + } diff --git a/AudioCore_Tests/Pipeline_Integration_Tests.cs b/AudioCore_Tests/Pipeline_Integration_Tests.cs index 6409e95..d21302c 100644 --- a/AudioCore_Tests/Pipeline_Integration_Tests.cs +++ b/AudioCore_Tests/Pipeline_Integration_Tests.cs @@ -79,6 +79,8 @@ public sealed class Pipeline_Integration_Tests using var ff = Process.Start(psi); var stdin = ff!.StandardInput.BaseStream; + // Start draining stderr immediately + _ = Task.Run(() => DrainStderr(ff)); var running = true; @@ -128,4 +130,24 @@ public sealed class Pipeline_Integration_Tests Assert.IsGreaterThan(0, read, "FLAC output is not decodable"); } + + private static void DrainStderr(Process proc) + { + try + { + var reader = proc.StandardError; + + // ffmpeg writes short lines, so ReadLine is fine + // If you want zero allocations, use ReadAsync into a rented buffer. + string? line; + while ((line = reader.ReadLine()) != null) + { + Debug.WriteLine(line); + } + } + catch (Exception ex) + { + Debug.WriteLine(ex.ToString()); + } + } }