ABStemPlayer/AudioCore/Impl/Htdemucs6sSeparator.cs

257 lines
8.3 KiB
C#

using System.Diagnostics;
using Microsoft.ML.OnnxRuntime;
using NAudio.Wave;
namespace AudioCore.Impl;
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);
private const int _overlap = _segmentSamples / 4;
private const int _stride = _segmentSamples - _overlap;
private static readonly string[] _stemNames = Enum.GetNames<StemType>();
public async Task<StemSet> SeparateAsync(
StemSeparationRequest request,
IProgressReporter<double> progress,
CancellationToken ct = default)
{
Directory.CreateDirectory(request.OutputDirectory);
var existingStems = CheckExistingStems(request);
if (existingStems != null)
return existingStems;
var mix = LoadStereoFloatWave(request.SourceFilePath, out var sr);
if (sr != _sampleRate)
throw new InvalidOperationException($"Input must be {_sampleRate} Hz");
var totalSamples = mix.GetLength(1);
var opts = new SessionOptions();
opts.AppendExecutionProvider_CPU();
opts.GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL;
var modelPath = Path.Combine(AppContext.BaseDirectory, "Data", "htdemucs_6s.onnx");
using var session = new InferenceSession(modelPath, opts);
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);
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 chunk = new float[_channels, _segmentSamples];
for (var ch = 0; ch < _channels; ch++)
Array.Copy(mix, ch * totalSamples + start, chunk, ch * _segmentSamples, clen);
var inputData = new float[_channels * _segmentSamples];
for (var ch = 0; ch < _channels; ch++)
{
var baseIndex = ch * _segmentSamples;
for (var s = 0; s < _segmentSamples; s++)
inputData[baseIndex + s] = chunk[ch, s];
}
using var inputOrtValue = OrtValue.CreateTensorValueFromMemory(
inputData,
new long[] { 1, _channels, _segmentSamples });
var outputData = new float[_stemNames.Length * _channels * _segmentSamples];
using var outputOrtValue = OrtValue.CreateTensorValueFromMemory(
outputData,
new long[] { 1, _stemNames.Length, _channels, _segmentSamples });
using var io = session.CreateIoBinding();
io.BindInput("mix", inputOrtValue);
io.BindOutput("stems", outputOrtValue);
session.RunWithBinding(new RunOptions(), io);
var buf = outputData.AsSpan();
var stemCnt = _stemNames.Length;
var chCnt = _channels;
var length = _segmentSamples;
var stemStride = chCnt * length;
var channelStride = length;
for (var stem = 0; stem < stemCnt; stem++)
{
for (var ch = 0; ch < chCnt; ch++)
{
var baseIndex = stem * stemStride + ch * channelStride;
for (var s = 0; s < clen; s++)
{
var w = window[s];
var v = buf[baseIndex + s];
outStems[stem, ch, start + s] += v * w;
}
}
}
for (var s = 0; s < clen; s++)
weight[start + s] += window[s];
await progress.ReportProgress((double)(i + 1) / nChunks, ct);
}
for (var stem = 0; stem < _stemNames.Length; stem++)
{
for (var ch = 0; ch < _channels; ch++)
{
for (var s = 0; s < totalSamples; s++)
{
var w = weight[s];
if (w > 1e-8f)
outStems[stem, ch, s] /= w;
}
}
}
var result = new List<StemTrack>();
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<StemType>(_stemNames[i]),
Name = name,
FilePath = path,
SampleRate = _sampleRate,
Channels = _channels,
Duration = TimeSpan.FromSeconds((double)totalSamples / _sampleRate)
});
}
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")))
.ToList();
var stems = new List<StemTrack>();
var set = new StemSet
{
OriginalFilePath = request.SourceFilePath,
Stems = stems
};
foreach (var f in filesToCheck.Where(f => File.Exists(f.Item2)))
{
using var reader = new AudioFileReader(f.Item2);
stems.Add(new StemTrack
{
Type = Enum.Parse<StemType>(f.Item1),
Name = Path.GetFileName(f.Item2),
FilePath = f.Item2,
SampleRate = reader.WaveFormat.SampleRate,
Channels = reader.WaveFormat.Channels,
Duration = reader.TotalTime
});
}
return stems.Count > 0 ? set : null;
}
private static float[] MakeWindow(int n, int overlap)
{
var w = new float[n];
for (var i = 0; i < n; i++) w[i] = 1f;
for (var i = 0; i < overlap; i++)
{
var fade = (float)i / overlap;
w[i] = fade;
w[n - 1 - i] = fade;
}
return w;
}
private static float[,] LoadStereoFloatWave(string path, out int sampleRate)
{
using var reader = new AudioFileReader(path);
sampleRate = reader.WaveFormat.SampleRate;
var samples = new List<float>();
var buffer = new float[reader.WaveFormat.SampleRate * 4];
int read;
while ((read = reader.Read(buffer, 0, buffer.Length)) > 0)
samples.AddRange(buffer.AsSpan(0, read));
var total = samples.Count / 2;
var result = new float[2, total];
for (var i = 0; i < total; i++)
{
result[0, i] = samples[2 * i];
result[1, i] = samples[2 * i + 1];
}
return result;
}
private static void WriteFlac(string path, float[,,] stems, int stemIndex, int totalSamples)
{
var cmd =
"-y " +
"-f f32le " +
"-ar 44100 " +
"-ac 2 " +
"-i pipe:0 " +
"-compression_level 12 " +
$"\"{path}\"";
using var ff = new FfmpegProcess(
name: $"flac:{Path.GetFileName(path)}",
commandLine: cmd,
redirectOutput: true,
redirectInput: true);
ff.StartProcess();
var stdin = ff.Stdin!;
var frame = new byte[sizeof(float) * 2];
for (var i = 0; i < totalSamples; i++)
{
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.Proc!.WaitForExit();
}
}