mirror of
https://github.com/unclshura/ABStemPlayer.git
synced 2026-08-07 00:43:38 +00:00
Separate class for ffprobe process. Removed FfmpegPipe class. Reader is lazy now.
This commit is contained in:
parent
c63c6da255
commit
74057c2006
@ -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,75 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
namespace AudioCore.Impl;
|
|
||||||
|
|
||||||
public sealed class FfmpegPipe : IDisposable
|
|
||||||
{
|
|
||||||
private readonly string _path;
|
|
||||||
private FfmpegProcess? _process;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
TotalSamples = ProbeTotalSamples(path, sampleRate);
|
|
||||||
|
|
||||||
StartProcess(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StartProcess(long startSample)
|
|
||||||
{
|
|
||||||
var startSeconds = (double)startSample / 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";
|
|
||||||
|
|
||||||
_process = new FfmpegProcess(
|
|
||||||
name: $"pipe:{_path}",
|
|
||||||
commandLine: cmd,
|
|
||||||
redirectOutput: true,
|
|
||||||
redirectInput: true);
|
|
||||||
|
|
||||||
_process.StartProcess();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Read(float[] buffer, int offset, int count)
|
|
||||||
{
|
|
||||||
if (_process?.Stdout is null)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
return _process.Read(buffer, offset, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Seek(long sampleIndex)
|
|
||||||
{
|
|
||||||
DisposeProcessOnly();
|
|
||||||
StartProcess(sampleIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Reset()
|
|
||||||
{
|
|
||||||
Seek(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long ProbeTotalSamples(string path, int sampleRate) => FfmpegProcess.ProbeTotalSamples(path, sampleRate);
|
|
||||||
private void DisposeProcessOnly()
|
|
||||||
{
|
|
||||||
try { _process?.Dispose(); } catch { }
|
|
||||||
_process = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
DisposeProcessOnly();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +1,5 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace AudioCore.Impl;
|
namespace AudioCore.Impl;
|
||||||
|
|
||||||
@ -23,6 +24,8 @@ public sealed class FfmpegProcess : IDisposable
|
|||||||
|
|
||||||
public void StartProcess()
|
public void StartProcess()
|
||||||
{
|
{
|
||||||
|
Debug.WriteLine($"{_name}: Starting ffmpeg process");
|
||||||
|
|
||||||
var psi = new ProcessStartInfo
|
var psi = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = "ffmpeg",
|
FileName = "ffmpeg",
|
||||||
@ -81,38 +84,17 @@ public sealed class FfmpegProcess : IDisposable
|
|||||||
return readBytes / sizeof(float);
|
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()
|
private void DisposeProcessOnly()
|
||||||
{
|
{
|
||||||
try { Stdout?.Dispose(); } catch { }
|
if (Proc != null)
|
||||||
try { Stdin?.Dispose(); } catch { }
|
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 { Proc?.StandardError.BaseStream?.Dispose(); } catch { }
|
||||||
try { if (Proc != null && !Proc.HasExited) Proc.Kill(); } catch { }
|
try { if (Proc != null && !Proc.HasExited) Proc.Kill(); } catch { }
|
||||||
try { Proc?.Dispose(); } catch { }
|
try { Proc?.Dispose(); Proc = null; } catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -31,8 +31,6 @@ public sealed class RubberBandTimeStretchEngine : ITimeStretchEngine, IDisposabl
|
|||||||
|
|
||||||
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)
|
||||||
|
|||||||
@ -129,7 +129,7 @@ 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
|
// Start draining stderr immediately
|
||||||
_ = Task.Run(() => DrainStderr(p));
|
_ = Task.Run(() => DrainStderr(p));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user