mirror of
https://github.com/unclshura/ABStemPlayer.git
synced 2026-08-07 00:43:38 +00:00
Compare commits
No commits in common. "74057c2006a1f0091fba13b01bc6e76fc891da42" and "950e5f1dcc7cecf76f7f80a4f7bc7333282010e7" have entirely different histories.
74057c2006
...
950e5f1dcc
@ -1,88 +1,27 @@
|
|||||||
using System.Diagnostics;
|
namespace AudioCore.Impl;
|
||||||
|
|
||||||
namespace AudioCore.Impl;
|
public sealed class FfmpegAudioReader : IAudioReader
|
||||||
|
|
||||||
public sealed class FfmpegAudioReader : IAudioReader, IDisposable
|
|
||||||
{
|
{
|
||||||
private readonly string _path;
|
private readonly FfmpegPipe _pipe;
|
||||||
|
|
||||||
// Lazy process wrapper
|
public int SampleRate => _pipe.SampleRate;
|
||||||
private Lazy<FfmpegProcess> _process;
|
public int Channels => _pipe.Channels;
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
_path = path;
|
_pipe = new FfmpegPipe(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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
112
AudioCore/Impl/FfmpegPipe.cs
Normal file
112
AudioCore/Impl/FfmpegPipe.cs
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,104 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,108 +0,0 @@
|
|||||||
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);
|
private const int _segmentSamples = (int)(_sampleRate * _segmentSeconds); // 343,980
|
||||||
private const int _overlap = _segmentSamples / 4;
|
private const int _overlap = _segmentSamples / 4; // 85,995
|
||||||
private const int _stride = _segmentSamples - _overlap;
|
private const int _stride = _segmentSamples - _overlap; // 257,985
|
||||||
|
|
||||||
private static readonly string[] _stemNames = Enum.GetNames<StemType>();
|
private static readonly string[] _stemNames = Enum.GetNames<StemType>();
|
||||||
|
|
||||||
@ -26,12 +26,14 @@ 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;
|
||||||
@ -39,12 +41,14 @@ 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();
|
||||||
@ -53,10 +57,14 @@ public sealed class Htdemucs6sSeparator : IStemSeparator
|
|||||||
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++)
|
||||||
{
|
{
|
||||||
@ -65,31 +73,42 @@ 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();
|
||||||
|
|
||||||
var stemCnt = _stemNames.Length;
|
// Extract output tensor shape (1, 6, 2, N)
|
||||||
var chCnt = _channels;
|
var stemCnt = _stemNames.Length; // 6
|
||||||
var length = _segmentSamples;
|
var chCnt = _channels; // 2
|
||||||
|
var length = _segmentSamples; // 343980
|
||||||
|
|
||||||
var stemStride = chCnt * length;
|
// Compute strides for flattened buffer
|
||||||
var channelStride = length;
|
var stemStride = chCnt * length; // 2 * N
|
||||||
|
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++)
|
||||||
@ -100,6 +119,7 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -111,6 +131,7 @@ 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++)
|
||||||
@ -124,12 +145,12 @@ 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
|
||||||
@ -153,8 +174,7 @@ public sealed class Htdemucs6sSeparator : IStemSeparator
|
|||||||
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,
|
.Select(stemType => (stemType, Path.Combine(request.OutputDirectory, $"{Path.GetFileNameWithoutExtension(request.SourceFilePath)}_{stemType}.flac")))
|
||||||
$"{Path.GetFileNameWithoutExtension(request.SourceFilePath)}_{stemType}.flac")))
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var stems = new List<StemTrack>();
|
var stems = new List<StemTrack>();
|
||||||
@ -168,7 +188,7 @@ public sealed class Htdemucs6sSeparator : IStemSeparator
|
|||||||
{
|
{
|
||||||
using var reader = new AudioFileReader(f.Item2);
|
using var reader = new AudioFileReader(f.Item2);
|
||||||
|
|
||||||
stems.Add(new StemTrack
|
var stem =new StemTrack
|
||||||
{
|
{
|
||||||
Type = Enum.Parse<StemType>(f.Item1),
|
Type = Enum.Parse<StemType>(f.Item1),
|
||||||
Name = Path.GetFileName(f.Item2),
|
Name = Path.GetFileName(f.Item2),
|
||||||
@ -176,12 +196,18 @@ public sealed class Htdemucs6sSeparator : IStemSeparator
|
|||||||
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];
|
||||||
@ -219,38 +245,59 @@ 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 cmd =
|
var psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "ffmpeg",
|
||||||
|
Arguments =
|
||||||
"-y " +
|
"-y " +
|
||||||
"-f f32le " +
|
"-f f32le " + // raw float32 little-endian
|
||||||
"-ar 44100 " +
|
"-ar 44100 " + // sample rate
|
||||||
"-ac 2 " +
|
"-ac 2 " + // channels
|
||||||
"-i pipe:0 " +
|
"-i pipe:0 " + // read from stdin
|
||||||
"-compression_level 12 " +
|
"-compression_level 12 " + // max FLAC compression
|
||||||
$"\"{path}\"";
|
$"\"{path}\"",
|
||||||
|
RedirectStandardInput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
WindowStyle = ProcessWindowStyle.Hidden,
|
||||||
|
};
|
||||||
|
|
||||||
using var ff = new FfmpegProcess(
|
using var ff = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start FFmpeg process");
|
||||||
name: $"flac:{Path.GetFileName(path)}",
|
using var stdin = ff.StandardInput.BaseStream;
|
||||||
commandLine: cmd,
|
|
||||||
redirectOutput: true,
|
|
||||||
redirectInput: true);
|
|
||||||
|
|
||||||
ff.StartProcess();
|
// Write raw float32 PCM directly to FFmpeg
|
||||||
|
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(frame.AsSpan(0, 4), stems[stemIndex, 0, i]);
|
BitConverter.TryWriteBytes(buffer.AsSpan(0, 4), stems[stemIndex, 0, i]);
|
||||||
BitConverter.TryWriteBytes(frame.AsSpan(4, 4), stems[stemIndex, 1, i]);
|
BitConverter.TryWriteBytes(buffer.AsSpan(4, 4), stems[stemIndex, 1, i]);
|
||||||
stdin.Write(frame, 0, frame.Length);
|
stdin.Write(buffer, 0, buffer.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
stdin.Flush();
|
stdin.Flush();
|
||||||
stdin.Close();
|
stdin.Close();
|
||||||
|
|
||||||
ff.Proc!.WaitForExit();
|
ff.WaitForExit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl
|
|||||||
private readonly int _sampleRate;
|
private readonly int _sampleRate;
|
||||||
private readonly int _channels;
|
private readonly int _channels;
|
||||||
|
|
||||||
private FfmpegProcess? _ff;
|
private Process? _ff;
|
||||||
private Stream? _stdin;
|
private Stream? _stdin;
|
||||||
private Stream? _stdout;
|
private Stream? _stdout;
|
||||||
|
|
||||||
@ -29,8 +29,11 @@ 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)
|
||||||
@ -51,6 +54,7 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl
|
|||||||
{
|
{
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,6 +72,7 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl
|
|||||||
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();
|
||||||
@ -99,34 +104,38 @@ 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 cmd =
|
var psi = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "ffmpeg",
|
||||||
|
Arguments =
|
||||||
$"-hide_banner -loglevel error " +
|
$"-hide_banner -loglevel error " +
|
||||||
$"-f f32le -ar {_sampleRate} -ac {_channels} -i pipe:0 " +
|
$"-f f32le -ar {_sampleRate} -ac {_channels} -i pipe:0 " +
|
||||||
$"-af \"rubberband=tempo={_speed}\" " +
|
$"-af \"rubberband=tempo={_speed}\" " +
|
||||||
$"-f f32le -ar {_sampleRate} -ac {_channels} pipe:1";
|
$"-f f32le -ar {_sampleRate} -ac {_channels} pipe:1",
|
||||||
|
RedirectStandardInput = true,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
WindowStyle = ProcessWindowStyle.Hidden,
|
||||||
|
};
|
||||||
|
|
||||||
_ff = new FfmpegProcess(
|
_ff = System.Diagnostics.Process.Start(psi);
|
||||||
name: $"rubberband:{_speed:F3}",
|
_stdin = _ff!.StandardInput.BaseStream;
|
||||||
commandLine: cmd,
|
_stdout = _ff!.StandardOutput.BaseStream;
|
||||||
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();
|
||||||
@ -143,6 +152,7 @@ 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;
|
||||||
|
|
||||||
@ -161,7 +171,7 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch { }
|
catch { /* swallow for now */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
private int DrainRing(Span<byte> dest, int maxBytes)
|
private int DrainRing(Span<byte> dest, int maxBytes)
|
||||||
@ -193,6 +203,7 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void ResetRing()
|
private void ResetRing()
|
||||||
{
|
{
|
||||||
lock (_ringLock)
|
lock (_ringLock)
|
||||||
@ -205,9 +216,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)
|
||||||
@ -215,10 +226,6 @@ 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,11 +129,8 @@ public sealed class FfmpegAudioReader_Tests
|
|||||||
WindowStyle = ProcessWindowStyle.Hidden,
|
WindowStyle = ProcessWindowStyle.Hidden,
|
||||||
};
|
};
|
||||||
|
|
||||||
using (var p = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start FFmpeg process"))
|
using (var p = Process.Start(psi))
|
||||||
{
|
{
|
||||||
// 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");
|
||||||
}
|
}
|
||||||
@ -207,24 +204,4 @@ 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,8 +79,6 @@ 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;
|
||||||
|
|
||||||
@ -130,24 +128,4 @@ 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