mirror of
https://github.com/unclshura/ABStemPlayer.git
synced 2026-08-07 00:43:38 +00:00
Compare commits
2 Commits
950e5f1dcc
...
74057c2006
| Author | SHA1 | Date | |
|---|---|---|---|
| 74057c2006 | |||
| c63c6da255 |
@ -1,27 +1,88 @@
|
|||||||
namespace AudioCore.Impl;
|
using System.Diagnostics;
|
||||||
|
|
||||||
public sealed class FfmpegAudioReader : IAudioReader
|
namespace AudioCore.Impl;
|
||||||
|
|
||||||
|
public sealed class FfmpegAudioReader : IAudioReader, IDisposable
|
||||||
{
|
{
|
||||||
private readonly FfmpegPipe _pipe;
|
private readonly string _path;
|
||||||
|
|
||||||
public int SampleRate => _pipe.SampleRate;
|
// Lazy process wrapper
|
||||||
public int Channels => _pipe.Channels;
|
private Lazy<FfmpegProcess> _process;
|
||||||
public long TotalSamples => _pipe.TotalSamples;
|
|
||||||
|
// Remember last seek position
|
||||||
|
private long _pendingSeekSample = 0;
|
||||||
|
|
||||||
|
public int SampleRate { get; }
|
||||||
|
public int Channels { get; }
|
||||||
|
public long TotalSamples { get; }
|
||||||
|
public TimeSpan Duration { get; }
|
||||||
|
|
||||||
public FfmpegAudioReader(string path)
|
public FfmpegAudioReader(string path)
|
||||||
{
|
{
|
||||||
_pipe = new FfmpegPipe(path);
|
_path = path;
|
||||||
|
|
||||||
|
var probe = FfprobeProcess.ProbeAudio(path);
|
||||||
|
|
||||||
|
SampleRate = probe.SampleRate;
|
||||||
|
Channels = probe.Channels;
|
||||||
|
TotalSamples = probe.TotalSamples;
|
||||||
|
Duration = probe.Duration;
|
||||||
|
|
||||||
|
_process = CreateLazyProcess();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Lazy<FfmpegProcess> CreateLazyProcess() => new Lazy<FfmpegProcess>(() =>
|
||||||
|
{
|
||||||
|
var startSeconds = (double)_pendingSeekSample / SampleRate;
|
||||||
|
|
||||||
|
var cmd =
|
||||||
|
"-hide_banner -loglevel error " +
|
||||||
|
"-nostdin " +
|
||||||
|
$"-ss {startSeconds.ToString(System.Globalization.CultureInfo.InvariantCulture)} " +
|
||||||
|
$"-i \"{_path}\" " +
|
||||||
|
$"-f f32le -ac {Channels} -ar {SampleRate} pipe:1";
|
||||||
|
|
||||||
|
var p = new FfmpegProcess(
|
||||||
|
name: $"pipe:{_path}",
|
||||||
|
commandLine: cmd,
|
||||||
|
redirectOutput: true,
|
||||||
|
redirectInput: true);
|
||||||
|
|
||||||
|
p.StartProcess();
|
||||||
|
return p;
|
||||||
|
});
|
||||||
|
|
||||||
public int Read(float[] buffer, int offset, int count)
|
public int Read(float[] buffer, int offset, int count)
|
||||||
=> _pipe.Read(buffer, offset, count);
|
{
|
||||||
|
var proc = _process.Value; // starts process if not started
|
||||||
|
if (proc.Stdout is null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return proc.Read(buffer, offset, count);
|
||||||
|
}
|
||||||
|
|
||||||
public void Seek(long sampleIndex)
|
public void Seek(long sampleIndex)
|
||||||
=> _pipe.Seek(sampleIndex);
|
{
|
||||||
|
_pendingSeekSample = sampleIndex;
|
||||||
|
DisposeProcessOnly();
|
||||||
|
_process = CreateLazyProcess(); // new lazy instance
|
||||||
|
}
|
||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
=> Seek(0);
|
{
|
||||||
|
Seek(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DisposeProcessOnly()
|
||||||
|
{
|
||||||
|
if (_process.IsValueCreated)
|
||||||
|
{
|
||||||
|
try { _process.Value.Dispose(); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
=> _pipe.Dispose();
|
{
|
||||||
|
DisposeProcessOnly();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,112 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
namespace AudioCore.Impl;
|
|
||||||
|
|
||||||
public sealed class FfmpegPipe : IDisposable
|
|
||||||
{
|
|
||||||
private readonly string _path;
|
|
||||||
private Process? _proc;
|
|
||||||
private Stream? _stdout;
|
|
||||||
|
|
||||||
public int SampleRate { get; }
|
|
||||||
public int Channels { get; }
|
|
||||||
public long TotalSamples { get; }
|
|
||||||
|
|
||||||
public FfmpegPipe(string path, int sampleRate = 44100, int channels = 2)
|
|
||||||
{
|
|
||||||
_path = path;
|
|
||||||
SampleRate = sampleRate;
|
|
||||||
Channels = channels;
|
|
||||||
|
|
||||||
// Optional: probe duration
|
|
||||||
TotalSamples = ProbeTotalSamples(path, sampleRate);
|
|
||||||
|
|
||||||
StartProcess(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StartProcess(long startSample)
|
|
||||||
{
|
|
||||||
var startSeconds = (double)startSample / SampleRate;
|
|
||||||
|
|
||||||
var psi = new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = "ffmpeg",
|
|
||||||
Arguments =
|
|
||||||
$"-hide_banner -loglevel error " +
|
|
||||||
$"-nostdin " + // prevent console attach
|
|
||||||
$"-ss {startSeconds.ToString(System.Globalization.CultureInfo.InvariantCulture)} " +
|
|
||||||
$"-i \"{_path}\" " +
|
|
||||||
$"-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
|
|
||||||
};
|
|
||||||
|
|
||||||
_proc = Process.Start(psi);
|
|
||||||
_stdout = _proc!.StandardOutput.BaseStream;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 void Seek(long sampleIndex)
|
|
||||||
{
|
|
||||||
DisposeProcessOnly();
|
|
||||||
StartProcess(sampleIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Reset() => Seek(0);
|
|
||||||
|
|
||||||
private 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 { if (_proc != null && !_proc.HasExited) _proc.Kill(); } catch { }
|
|
||||||
try { _proc?.Dispose(); } catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
DisposeProcessOnly();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
104
AudioCore/Impl/FfmpegProcess.cs
Normal file
104
AudioCore/Impl/FfmpegProcess.cs
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"{_name}: Starting ffmpeg process");
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void DisposeProcessOnly()
|
||||||
|
{
|
||||||
|
if (Proc != null)
|
||||||
|
Debug.WriteLine($"{_name}: Disposing ffmpeg process");
|
||||||
|
|
||||||
|
try { Stdout?.Dispose(); Stdout = null; } catch { }
|
||||||
|
try { Stdin?.Dispose(); Stdin = null; } catch { }
|
||||||
|
try { Proc?.StandardError.BaseStream?.Dispose(); } catch { }
|
||||||
|
try { if (Proc != null && !Proc.HasExited) Proc.Kill(); } catch { }
|
||||||
|
try { Proc?.Dispose(); Proc = null; } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
DisposeProcessOnly();
|
||||||
|
}
|
||||||
|
}
|
||||||
108
AudioCore/Impl/FfprobeProcess.cs
Normal file
108
AudioCore/Impl/FfprobeProcess.cs
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace AudioCore.Impl;
|
||||||
|
|
||||||
|
public class AudioProbe
|
||||||
|
{
|
||||||
|
public int SampleRate { get; init; }
|
||||||
|
public int Channels { get; init; }
|
||||||
|
public long TotalSamples { get; init; }
|
||||||
|
public TimeSpan Duration { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class FfprobeProcess
|
||||||
|
{
|
||||||
|
private sealed class FfprobeJson
|
||||||
|
{
|
||||||
|
public FfprobeFormat? Format { get; set; }
|
||||||
|
public FfprobeStream[]? Streams { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FfprobeFormat
|
||||||
|
{
|
||||||
|
public string? Duration { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FfprobeStream
|
||||||
|
{
|
||||||
|
[JsonConverter(typeof(IntFlexibleConverter))]
|
||||||
|
public int Sample_Rate { get; set; }
|
||||||
|
[JsonConverter(typeof(IntFlexibleConverter))]
|
||||||
|
public int Channels { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class IntFlexibleConverter : JsonConverter<int>
|
||||||
|
{
|
||||||
|
public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
return reader.TokenType switch
|
||||||
|
{
|
||||||
|
JsonTokenType.Number => reader.GetInt32(),
|
||||||
|
JsonTokenType.String => int.Parse(reader.GetString()!, System.Globalization.CultureInfo.InvariantCulture),
|
||||||
|
_ => throw new JsonException($"Invalid token for int: {reader.TokenType}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteNumberValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AudioProbe ProbeAudio(string path)
|
||||||
|
{
|
||||||
|
var psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "ffprobe",
|
||||||
|
Arguments =
|
||||||
|
"-v error " +
|
||||||
|
"-select_streams a:0 " +
|
||||||
|
"-show_entries format=duration " +
|
||||||
|
"-show_entries stream=sample_rate,channels " +
|
||||||
|
"-print_format json " +
|
||||||
|
$"\"{path}\"",
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
WindowStyle = ProcessWindowStyle.Hidden
|
||||||
|
};
|
||||||
|
|
||||||
|
using var p = Process.Start(psi) ?? throw new InvalidOperationException("ffprobe failed");
|
||||||
|
var json = p.StandardOutput.ReadToEnd();
|
||||||
|
p.WaitForExit();
|
||||||
|
|
||||||
|
var opts = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var probe = JsonSerializer.Deserialize<FfprobeJson>(json, opts)
|
||||||
|
?? throw new InvalidOperationException("Invalid ffprobe JSON");
|
||||||
|
|
||||||
|
if (probe.Format?.Duration is null)
|
||||||
|
throw new InvalidOperationException("ffprobe missing duration");
|
||||||
|
|
||||||
|
if (probe.Streams is null || probe.Streams.Length == 0)
|
||||||
|
throw new InvalidOperationException("ffprobe missing audio stream");
|
||||||
|
|
||||||
|
var stream = probe.Streams[0];
|
||||||
|
|
||||||
|
var durationSeconds = double.Parse(probe.Format.Duration,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
var sampleRate = stream.Sample_Rate;
|
||||||
|
var channels = stream.Channels;
|
||||||
|
|
||||||
|
var totalSamples = (long)(durationSeconds * sampleRate);
|
||||||
|
|
||||||
|
return new AudioProbe
|
||||||
|
{
|
||||||
|
SampleRate = sampleRate,
|
||||||
|
Channels = channels,
|
||||||
|
Duration = TimeSpan.FromSeconds(durationSeconds),
|
||||||
|
TotalSamples = totalSamples
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,9 +9,9 @@ public sealed class Htdemucs6sSeparator : IStemSeparator
|
|||||||
private const int _sampleRate = 44100;
|
private const int _sampleRate = 44100;
|
||||||
private const int _channels = 2;
|
private const int _channels = 2;
|
||||||
private const double _segmentSeconds = 7.8;
|
private const double _segmentSeconds = 7.8;
|
||||||
private const int _segmentSamples = (int)(_sampleRate * _segmentSeconds); // 343,980
|
private const int _segmentSamples = (int)(_sampleRate * _segmentSeconds);
|
||||||
private const int _overlap = _segmentSamples / 4; // 85,995
|
private const int _overlap = _segmentSamples / 4;
|
||||||
private const int _stride = _segmentSamples - _overlap; // 257,985
|
private const int _stride = _segmentSamples - _overlap;
|
||||||
|
|
||||||
private static readonly string[] _stemNames = Enum.GetNames<StemType>();
|
private static readonly string[] _stemNames = Enum.GetNames<StemType>();
|
||||||
|
|
||||||
@ -26,14 +26,12 @@ public sealed class Htdemucs6sSeparator : IStemSeparator
|
|||||||
if (existingStems != null)
|
if (existingStems != null)
|
||||||
return existingStems;
|
return existingStems;
|
||||||
|
|
||||||
// 1. Load audio
|
|
||||||
var mix = LoadStereoFloatWave(request.SourceFilePath, out var sr);
|
var mix = LoadStereoFloatWave(request.SourceFilePath, out var sr);
|
||||||
if (sr != _sampleRate)
|
if (sr != _sampleRate)
|
||||||
throw new InvalidOperationException($"Input must be {_sampleRate} Hz");
|
throw new InvalidOperationException($"Input must be {_sampleRate} Hz");
|
||||||
|
|
||||||
var totalSamples = mix.GetLength(1);
|
var totalSamples = mix.GetLength(1);
|
||||||
|
|
||||||
// 2. Prepare ONNX session
|
|
||||||
var opts = new SessionOptions();
|
var opts = new SessionOptions();
|
||||||
opts.AppendExecutionProvider_CPU();
|
opts.AppendExecutionProvider_CPU();
|
||||||
opts.GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL;
|
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");
|
var modelPath = Path.Combine(AppContext.BaseDirectory, "Data", "htdemucs_6s.onnx");
|
||||||
using var session = new InferenceSession(modelPath, opts);
|
using var session = new InferenceSession(modelPath, opts);
|
||||||
|
|
||||||
// 3. Prepare buffers
|
|
||||||
var outStems = new float[_stemNames.Length, _channels, totalSamples];
|
var outStems = new float[_stemNames.Length, _channels, totalSamples];
|
||||||
var weight = new float[totalSamples];
|
var weight = new float[totalSamples];
|
||||||
var window = MakeWindow(_segmentSamples, _overlap);
|
var window = MakeWindow(_segmentSamples, _overlap);
|
||||||
|
|
||||||
var nChunks = Math.Max(1, (totalSamples + _stride - 1) / _stride);
|
var nChunks = Math.Max(1, (totalSamples + _stride - 1) / _stride);
|
||||||
|
|
||||||
// 4. Sliding window inference
|
|
||||||
for (var i = 0; i < nChunks; i++)
|
for (var i = 0; i < nChunks; i++)
|
||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var start = i * _stride;
|
var start = i * _stride;
|
||||||
var end = Math.Min(start + _segmentSamples, totalSamples);
|
var end = Math.Min(start + _segmentSamples, totalSamples);
|
||||||
var clen = end - start;
|
var clen = end - start;
|
||||||
|
|
||||||
// Extract chunk into [2, N]
|
|
||||||
var chunk = new float[_channels, _segmentSamples];
|
var chunk = new float[_channels, _segmentSamples];
|
||||||
for (var ch = 0; ch < _channels; ch++)
|
for (var ch = 0; ch < _channels; ch++)
|
||||||
{
|
|
||||||
Array.Copy(mix, ch * totalSamples + start, chunk, ch * _segmentSamples, clen);
|
Array.Copy(mix, ch * totalSamples + start, chunk, ch * _segmentSamples, clen);
|
||||||
}
|
|
||||||
|
|
||||||
// Build flat input buffer (1,2,N)
|
|
||||||
var inputData = new float[_channels * _segmentSamples];
|
var inputData = new float[_channels * _segmentSamples];
|
||||||
for (var ch = 0; ch < _channels; ch++)
|
for (var ch = 0; ch < _channels; ch++)
|
||||||
{
|
{
|
||||||
@ -73,42 +65,31 @@ public sealed class Htdemucs6sSeparator : IStemSeparator
|
|||||||
inputData[baseIndex + s] = chunk[ch, s];
|
inputData[baseIndex + s] = chunk[ch, s];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create OrtValue for input
|
|
||||||
using var inputOrtValue = OrtValue.CreateTensorValueFromMemory(
|
using var inputOrtValue = OrtValue.CreateTensorValueFromMemory(
|
||||||
inputData,
|
inputData,
|
||||||
new long[] { 1, _channels, _segmentSamples }
|
new long[] { 1, _channels, _segmentSamples });
|
||||||
);
|
|
||||||
|
|
||||||
// Prepare output buffer (CPU)
|
|
||||||
var outputData = new float[_stemNames.Length * _channels * _segmentSamples];
|
var outputData = new float[_stemNames.Length * _channels * _segmentSamples];
|
||||||
|
|
||||||
// Create OrtValue for output
|
|
||||||
using var outputOrtValue = OrtValue.CreateTensorValueFromMemory(
|
using var outputOrtValue = OrtValue.CreateTensorValueFromMemory(
|
||||||
outputData,
|
outputData,
|
||||||
new long[] { 1, _stemNames.Length, _channels, _segmentSamples }
|
new long[] { 1, _stemNames.Length, _channels, _segmentSamples });
|
||||||
);
|
|
||||||
|
|
||||||
// Bind using IOBinding
|
|
||||||
using var io = session.CreateIoBinding();
|
using var io = session.CreateIoBinding();
|
||||||
io.BindInput("mix", inputOrtValue);
|
io.BindInput("mix", inputOrtValue);
|
||||||
io.BindOutput("stems", outputOrtValue);
|
io.BindOutput("stems", outputOrtValue);
|
||||||
|
|
||||||
// Execute on GPU → output goes directly to CPU buffer
|
|
||||||
session.RunWithBinding(new RunOptions(), io);
|
session.RunWithBinding(new RunOptions(), io);
|
||||||
|
|
||||||
// Now outputData contains (1,6,2,N)
|
|
||||||
var buf = outputData.AsSpan();
|
var buf = outputData.AsSpan();
|
||||||
|
|
||||||
// Extract output tensor shape (1, 6, 2, N)
|
var stemCnt = _stemNames.Length;
|
||||||
var stemCnt = _stemNames.Length; // 6
|
var chCnt = _channels;
|
||||||
var chCnt = _channels; // 2
|
var length = _segmentSamples;
|
||||||
var length = _segmentSamples; // 343980
|
|
||||||
|
|
||||||
// Compute strides for flattened buffer
|
var stemStride = chCnt * length;
|
||||||
var stemStride = chCnt * length; // 2 * N
|
var channelStride = length;
|
||||||
var channelStride = length; // N
|
|
||||||
|
|
||||||
// Overlap-add
|
|
||||||
for (var stem = 0; stem < stemCnt; stem++)
|
for (var stem = 0; stem < stemCnt; stem++)
|
||||||
{
|
{
|
||||||
for (var ch = 0; ch < chCnt; ch++)
|
for (var ch = 0; ch < chCnt; ch++)
|
||||||
@ -119,7 +100,6 @@ public sealed class Htdemucs6sSeparator : IStemSeparator
|
|||||||
{
|
{
|
||||||
var w = window[s];
|
var w = window[s];
|
||||||
var v = buf[baseIndex + s];
|
var v = buf[baseIndex + s];
|
||||||
|
|
||||||
outStems[stem, ch, start + s] += v * w;
|
outStems[stem, ch, start + s] += v * w;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -131,7 +111,6 @@ public sealed class Htdemucs6sSeparator : IStemSeparator
|
|||||||
await progress.ReportProgress((double)(i + 1) / nChunks, ct);
|
await progress.ReportProgress((double)(i + 1) / nChunks, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Normalize by weight
|
|
||||||
for (var stem = 0; stem < _stemNames.Length; stem++)
|
for (var stem = 0; stem < _stemNames.Length; stem++)
|
||||||
{
|
{
|
||||||
for (var ch = 0; ch < _channels; ch++)
|
for (var ch = 0; ch < _channels; ch++)
|
||||||
@ -145,36 +124,37 @@ public sealed class Htdemucs6sSeparator : IStemSeparator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Write stems
|
|
||||||
var result = new List<StemTrack>();
|
var result = new List<StemTrack>();
|
||||||
for (var i = 0; i < _stemNames.Length; i++)
|
for (var i = 0; i < _stemNames.Length; i++)
|
||||||
{
|
{
|
||||||
var name = $"{Path.GetFileNameWithoutExtension(request.SourceFilePath)}_{_stemNames[i]}.flac";
|
var name = $"{Path.GetFileNameWithoutExtension(request.SourceFilePath)}_{_stemNames[i]}.flac";
|
||||||
var path = Path.Combine(request.OutputDirectory, name);
|
var path = Path.Combine(request.OutputDirectory, name);
|
||||||
|
|
||||||
WriteFlac(path, outStems, i, totalSamples);
|
WriteFlac(path, outStems, i, totalSamples);
|
||||||
|
|
||||||
result.Add(new StemTrack
|
result.Add(new StemTrack
|
||||||
{
|
{
|
||||||
Type = Enum.Parse<StemType>(_stemNames[i]),
|
Type = Enum.Parse<StemType>(_stemNames[i]),
|
||||||
Name = name,
|
Name = name,
|
||||||
FilePath = path,
|
FilePath = path,
|
||||||
SampleRate = _sampleRate,
|
SampleRate = _sampleRate,
|
||||||
Channels = _channels,
|
Channels = _channels,
|
||||||
Duration = TimeSpan.FromSeconds((double)totalSamples / _sampleRate)
|
Duration = TimeSpan.FromSeconds((double)totalSamples / _sampleRate)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return new StemSet
|
return new StemSet
|
||||||
{
|
{
|
||||||
OriginalFilePath = request.SourceFilePath,
|
OriginalFilePath = request.SourceFilePath,
|
||||||
Stems = result
|
Stems = result
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private StemSet? CheckExistingStems(StemSeparationRequest request)
|
private StemSet? CheckExistingStems(StemSeparationRequest request)
|
||||||
{
|
{
|
||||||
var filesToCheck = Enum.GetNames(typeof(StemType))
|
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();
|
.ToList();
|
||||||
|
|
||||||
var stems = new List<StemTrack>();
|
var stems = new List<StemTrack>();
|
||||||
@ -188,26 +168,20 @@ public sealed class Htdemucs6sSeparator : IStemSeparator
|
|||||||
{
|
{
|
||||||
using var reader = new AudioFileReader(f.Item2);
|
using var reader = new AudioFileReader(f.Item2);
|
||||||
|
|
||||||
var stem =new StemTrack
|
stems.Add(new StemTrack
|
||||||
{
|
{
|
||||||
Type = Enum.Parse<StemType>(f.Item1),
|
Type = Enum.Parse<StemType>(f.Item1),
|
||||||
Name = Path.GetFileName(f.Item2),
|
Name = Path.GetFileName(f.Item2),
|
||||||
FilePath = f.Item2,
|
FilePath = f.Item2,
|
||||||
SampleRate = reader.WaveFormat.SampleRate,
|
SampleRate = reader.WaveFormat.SampleRate,
|
||||||
Channels = reader.WaveFormat.Channels,
|
Channels = reader.WaveFormat.Channels,
|
||||||
Duration = reader.TotalTime
|
Duration = reader.TotalTime
|
||||||
};
|
});
|
||||||
|
|
||||||
stems.Add(stem);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return stems.Count > 0 ? set : null;
|
return stems.Count > 0 ? set : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------
|
|
||||||
// Helpers
|
|
||||||
// ------------------------------
|
|
||||||
|
|
||||||
private static float[] MakeWindow(int n, int overlap)
|
private static float[] MakeWindow(int n, int overlap)
|
||||||
{
|
{
|
||||||
var w = new float[n];
|
var w = new float[n];
|
||||||
@ -245,59 +219,38 @@ public sealed class Htdemucs6sSeparator : IStemSeparator
|
|||||||
return result;
|
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)
|
private static void WriteFlac(string path, float[,,] stems, int stemIndex, int totalSamples)
|
||||||
{
|
{
|
||||||
var psi = new ProcessStartInfo
|
var cmd =
|
||||||
{
|
|
||||||
FileName = "ffmpeg",
|
|
||||||
Arguments =
|
|
||||||
"-y " +
|
"-y " +
|
||||||
"-f f32le " + // raw float32 little-endian
|
"-f f32le " +
|
||||||
"-ar 44100 " + // sample rate
|
"-ar 44100 " +
|
||||||
"-ac 2 " + // channels
|
"-ac 2 " +
|
||||||
"-i pipe:0 " + // read from stdin
|
"-i pipe:0 " +
|
||||||
"-compression_level 12 " + // max FLAC compression
|
"-compression_level 12 " +
|
||||||
$"\"{path}\"",
|
$"\"{path}\"";
|
||||||
RedirectStandardInput = true,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
UseShellExecute = false,
|
|
||||||
CreateNoWindow = true,
|
|
||||||
WindowStyle = ProcessWindowStyle.Hidden,
|
|
||||||
};
|
|
||||||
|
|
||||||
using var ff = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start FFmpeg process");
|
using var ff = new FfmpegProcess(
|
||||||
using var stdin = ff.StandardInput.BaseStream;
|
name: $"flac:{Path.GetFileName(path)}",
|
||||||
|
commandLine: cmd,
|
||||||
|
redirectOutput: true,
|
||||||
|
redirectInput: true);
|
||||||
|
|
||||||
// Write raw float32 PCM directly to FFmpeg
|
ff.StartProcess();
|
||||||
var buffer = new byte[sizeof(float) * 2]; // stereo frame
|
|
||||||
|
var stdin = ff.Stdin!;
|
||||||
|
var frame = new byte[sizeof(float) * 2];
|
||||||
|
|
||||||
for (var i = 0; i < totalSamples; i++)
|
for (var i = 0; i < totalSamples; i++)
|
||||||
{
|
{
|
||||||
BitConverter.TryWriteBytes(buffer.AsSpan(0, 4), stems[stemIndex, 0, i]);
|
BitConverter.TryWriteBytes(frame.AsSpan(0, 4), stems[stemIndex, 0, i]);
|
||||||
BitConverter.TryWriteBytes(buffer.AsSpan(4, 4), stems[stemIndex, 1, i]);
|
BitConverter.TryWriteBytes(frame.AsSpan(4, 4), stems[stemIndex, 1, i]);
|
||||||
stdin.Write(buffer, 0, buffer.Length);
|
stdin.Write(frame, 0, frame.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
stdin.Flush();
|
stdin.Flush();
|
||||||
stdin.Close();
|
stdin.Close();
|
||||||
|
|
||||||
ff.WaitForExit();
|
ff.Proc!.WaitForExit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,22 +6,22 @@ namespace AudioCore.Impl;
|
|||||||
public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposable
|
public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposable
|
||||||
{
|
{
|
||||||
private readonly AudioBufferPool _pool;
|
private readonly AudioBufferPool _pool;
|
||||||
private readonly int _sampleRate;
|
private readonly int _sampleRate;
|
||||||
private readonly int _channels;
|
private readonly int _channels;
|
||||||
|
|
||||||
private Process? _ff;
|
private FfmpegProcess? _ff;
|
||||||
private Stream? _stdin;
|
private Stream? _stdin;
|
||||||
private Stream? _stdout;
|
private Stream? _stdout;
|
||||||
|
|
||||||
private float _speed = 1.0f;
|
private float _speed = 1.0f;
|
||||||
|
|
||||||
private readonly byte[] _ring;
|
private readonly byte[] _ring;
|
||||||
private int _ringWrite;
|
private int _ringWrite;
|
||||||
private int _ringRead;
|
private int _ringRead;
|
||||||
private readonly object _ringLock = new();
|
private readonly object _ringLock = new();
|
||||||
|
|
||||||
private Thread? _readerThread;
|
private Thread? _readerThread;
|
||||||
private bool _readerRunning;
|
private bool _readerRunning;
|
||||||
|
|
||||||
public RubberBandTimeStretchEngine(AudioBufferPool pool, int sampleRate = 44100, int channels = 2)
|
public RubberBandTimeStretchEngine(AudioBufferPool pool, int sampleRate = 44100, int channels = 2)
|
||||||
{
|
{
|
||||||
@ -29,11 +29,8 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl
|
|||||||
_sampleRate = sampleRate;
|
_sampleRate = sampleRate;
|
||||||
_channels = channels;
|
_channels = channels;
|
||||||
|
|
||||||
// Ring buffer: e.g. 1 second of audio
|
|
||||||
var bytesPerSecond = sampleRate * channels * sizeof(float);
|
var bytesPerSecond = sampleRate * channels * sizeof(float);
|
||||||
_ring = new byte[bytesPerSecond];
|
_ring = new byte[bytesPerSecond];
|
||||||
|
|
||||||
StartProcess();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Configure(PlaybackSpeedSettings settings)
|
public void Configure(PlaybackSpeedSettings settings)
|
||||||
@ -48,17 +45,16 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl
|
|||||||
public TimeStretchedAudioBlock Process(MixedAudioBlock input)
|
public TimeStretchedAudioBlock Process(MixedAudioBlock input)
|
||||||
{
|
{
|
||||||
var expectedFloats = input.Frames * _channels;
|
var expectedFloats = input.Frames * _channels;
|
||||||
var expectedBytes = expectedFloats * sizeof(float);
|
var expectedBytes = expectedFloats * sizeof(float);
|
||||||
|
|
||||||
if (Math.Abs(_speed - 1.0f) < 0.01f)
|
if (Math.Abs(_speed - 1.0f) < 0.01f)
|
||||||
{
|
{
|
||||||
var buf = _pool.Rent(expectedFloats);
|
var buf = _pool.Rent(expectedFloats);
|
||||||
Array.Copy(input.Buffer.Samples, buf.Samples, input.Buffer.Length);
|
Array.Copy(input.Buffer.Samples, buf.Samples, input.Buffer.Length);
|
||||||
|
|
||||||
return new TimeStretchedAudioBlock(buf, input.Frames, _channels, _sampleRate);
|
return new TimeStretchedAudioBlock(buf, input.Frames, _channels, _sampleRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
var span = input.Buffer.Span;
|
var span = input.Buffer.Span;
|
||||||
var bytes = MemoryMarshal.AsBytes(span);
|
var bytes = MemoryMarshal.AsBytes(span);
|
||||||
|
|
||||||
_stdin!.Write(bytes);
|
_stdin!.Write(bytes);
|
||||||
@ -68,11 +64,10 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl
|
|||||||
if (available <= 0)
|
if (available <= 0)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
var outBuf = _pool.Rent(expectedFloats);
|
var outBuf = _pool.Rent(expectedFloats);
|
||||||
var outBytes = MemoryMarshal.AsBytes(outBuf.Span);
|
var outBytes = MemoryMarshal.AsBytes(outBuf.Span);
|
||||||
|
|
||||||
var readBytes = DrainRing(outBytes, expectedBytes);
|
var readBytes = DrainRing(outBytes, expectedBytes);
|
||||||
|
|
||||||
if (readBytes <= 0)
|
if (readBytes <= 0)
|
||||||
{
|
{
|
||||||
outBuf.Dispose();
|
outBuf.Dispose();
|
||||||
@ -94,8 +89,8 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl
|
|||||||
lock (_ringLock)
|
lock (_ringLock)
|
||||||
{
|
{
|
||||||
var available = (_ringWrite >= _ringRead)
|
var available = (_ringWrite >= _ringRead)
|
||||||
? _ringWrite - _ringRead
|
? _ringWrite - _ringRead
|
||||||
: _ring.Length - _ringRead + _ringWrite;
|
: _ring.Length - _ringRead + _ringWrite;
|
||||||
|
|
||||||
if (available > 0)
|
if (available > 0)
|
||||||
return available;
|
return available;
|
||||||
@ -104,38 +99,34 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl
|
|||||||
Thread.Sleep(2);
|
Thread.Sleep(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine("Rubberband: Timeout waiting for output from ffmpeg");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void StartProcess()
|
private void StartProcess()
|
||||||
{
|
{
|
||||||
var psi = new ProcessStartInfo
|
var cmd =
|
||||||
{
|
$"-hide_banner -loglevel error " +
|
||||||
FileName = "ffmpeg",
|
$"-f f32le -ar {_sampleRate} -ac {_channels} -i pipe:0 " +
|
||||||
Arguments =
|
$"-af \"rubberband=tempo={_speed}\" " +
|
||||||
$"-hide_banner -loglevel error " +
|
$"-f f32le -ar {_sampleRate} -ac {_channels} pipe:1";
|
||||||
$"-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,
|
|
||||||
};
|
|
||||||
|
|
||||||
_ff = System.Diagnostics.Process.Start(psi);
|
_ff = new FfmpegProcess(
|
||||||
_stdin = _ff!.StandardInput.BaseStream;
|
name: $"rubberband:{_speed:F3}",
|
||||||
_stdout = _ff!.StandardOutput.BaseStream;
|
commandLine: cmd,
|
||||||
|
redirectOutput: true,
|
||||||
|
redirectInput: true);
|
||||||
|
|
||||||
|
_ff.StartProcess();
|
||||||
|
|
||||||
|
_stdin = _ff.Stdin!;
|
||||||
|
_stdout = _ff.Stdout!;
|
||||||
|
|
||||||
_readerRunning = true;
|
_readerRunning = true;
|
||||||
_readerThread = new Thread(ReaderLoop) { IsBackground = true };
|
_readerThread = new Thread(ReaderLoop) { IsBackground = true };
|
||||||
_readerThread.Start();
|
_readerThread.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void RestartProcess()
|
private void RestartProcess()
|
||||||
{
|
{
|
||||||
DisposeProcess();
|
DisposeProcess();
|
||||||
@ -152,7 +143,6 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl
|
|||||||
while (_readerRunning)
|
while (_readerRunning)
|
||||||
{
|
{
|
||||||
var read = _stdout!.Read(buf, 0, buf.Length);
|
var read = _stdout!.Read(buf, 0, buf.Length);
|
||||||
|
|
||||||
if (read <= 0)
|
if (read <= 0)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -171,7 +161,7 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch { /* swallow for now */ }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
private int DrainRing(Span<byte> dest, int maxBytes)
|
private int DrainRing(Span<byte> dest, int maxBytes)
|
||||||
@ -179,8 +169,8 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl
|
|||||||
lock (_ringLock)
|
lock (_ringLock)
|
||||||
{
|
{
|
||||||
var available = (_ringWrite >= _ringRead)
|
var available = (_ringWrite >= _ringRead)
|
||||||
? _ringWrite - _ringRead
|
? _ringWrite - _ringRead
|
||||||
: _ring.Length - _ringRead + _ringWrite;
|
: _ring.Length - _ringRead + _ringWrite;
|
||||||
|
|
||||||
if (available <= 0)
|
if (available <= 0)
|
||||||
return 0;
|
return 0;
|
||||||
@ -203,7 +193,6 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void ResetRing()
|
private void ResetRing()
|
||||||
{
|
{
|
||||||
lock (_ringLock)
|
lock (_ringLock)
|
||||||
@ -216,9 +205,9 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl
|
|||||||
private void DisposeProcess()
|
private void DisposeProcess()
|
||||||
{
|
{
|
||||||
_readerRunning = false;
|
_readerRunning = false;
|
||||||
|
|
||||||
try { _stdout?.Close(); } catch { }
|
try { _stdout?.Close(); } catch { }
|
||||||
try { _stdin?.Close(); } catch { }
|
try { _stdin?.Close(); } catch { }
|
||||||
try { _ff?.Kill(); } catch { }
|
|
||||||
try { _ff?.Dispose(); } catch { }
|
try { _ff?.Dispose(); } catch { }
|
||||||
|
|
||||||
if (_readerThread != null)
|
if (_readerThread != null)
|
||||||
@ -226,6 +215,10 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl
|
|||||||
try { _readerThread.Join(500); } catch { }
|
try { _readerThread.Join(500); } catch { }
|
||||||
_readerThread = null;
|
_readerThread = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ff = null;
|
||||||
|
_stdin = null;
|
||||||
|
_stdout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() => DisposeProcess();
|
public void Dispose() => DisposeProcess();
|
||||||
|
|||||||
@ -129,8 +129,11 @@ public sealed class FfmpegAudioReader_Tests
|
|||||||
WindowStyle = ProcessWindowStyle.Hidden,
|
WindowStyle = ProcessWindowStyle.Hidden,
|
||||||
};
|
};
|
||||||
|
|
||||||
using (var p = Process.Start(psi))
|
using (var p = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start FFmpeg process"))
|
||||||
{
|
{
|
||||||
|
// Start draining stderr immediately
|
||||||
|
_ = Task.Run(() => DrainStderr(p));
|
||||||
|
|
||||||
p!.WaitForExit();
|
p!.WaitForExit();
|
||||||
Assert.AreEqual(0, p.ExitCode, "FFmpeg failed to convert MP3 to FLAC");
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -79,6 +79,8 @@ public sealed class Pipeline_Integration_Tests
|
|||||||
|
|
||||||
using var ff = Process.Start(psi);
|
using var ff = Process.Start(psi);
|
||||||
var stdin = ff!.StandardInput.BaseStream;
|
var stdin = ff!.StandardInput.BaseStream;
|
||||||
|
// Start draining stderr immediately
|
||||||
|
_ = Task.Run(() => DrainStderr(ff));
|
||||||
|
|
||||||
var running = true;
|
var running = true;
|
||||||
|
|
||||||
@ -128,4 +130,24 @@ public sealed class Pipeline_Integration_Tests
|
|||||||
|
|
||||||
Assert.IsGreaterThan(0, read, "FLAC output is not decodable");
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user