如何刷新 ErrorDataReceived 以更快地处理?

How to refresh ErrorDataReceived to process faster?

我正在使用 sox.exe 播放一些音频文件。 我是这样称呼它的:

SoxPlayer = new Process
{
    StartInfo = new ProcessStartInfo
    {
        CreateNoWindow = true,
        RedirectStandardError = true,
        UseShellExecute = false,
        FileName = Play,
        Arguments = arg,
        WorkingDirectory = Application.StartupPath + "\bin\"
    }
};

这是解释 StandardError 输出的代码:

private void UpdatePlaybackTime(string output)
{
    if (string.IsNullOrEmpty(output)) return;
    if (!output.Contains("%") || !output.Contains("[")) return;
    var index1 = output.IndexOf("%", StringComparison.Ordinal) + 1;
    var index2 = output.IndexOf("[", StringComparison.Ordinal);
    var time = output.Substring(index1, index2 - index1).Trim();
    var times = time.Split(new[] { ":" }, StringSplitOptions.None);
    var seconds = Convert.ToDouble(times[0]) * 3600;
    seconds = seconds + (Convert.ToDouble(times[1]) * 60);
    seconds = seconds + (Convert.ToDouble(times[2]));
    if (seconds == 0 || seconds < PlaybackSeconds) return;
    PlaybackSeconds = seconds;
}

我的目标是尽可能准确地从 sox 输出中获取播放时间,而不是使用可能与 sox 自身失去同步的内部计时器(就像我之前所做的那样)。

我的第一次尝试是使用我在网上找到的推荐:

SoxPlayer.ErrorDataReceived += (sender, args) => UpdatePlaybackTime(args.Data);
SoxPlayer.Start();
SoxPlayer.BeginErrorReadLine();

当前代码 "works" 中我得到了我想要的信息,但似乎每 5 秒左右调用一次 UpdatePlaybackTime()。调用时,获取的信息是准确的,但显然我想每秒更新几次播放信息,而不是每 5 秒一次。

我的理解是,当 StandardError 缓冲区变满时,正在调用 UpdatePlaybackTime。我试过用我的播放器计时器调用 SoxPlayer.BeginErrorReadLine() 但它说它已经在异步运行。我试过 SoxPlayer.StandardError.DiscardBufferedData() 但由于正在进行的异步进程,它会抛出异常。

那么,我怎样才能捕捉到我需要的播放信息呢?提前致谢!

编辑:

在讨论了这段代码以及它如何因缓冲而无法工作之后,我还在单独的 BackgroundWorker 线程中尝试了以下操作,结果相同(即大约每 5 秒更新一次):

SoxPlayer.Start();
SoxTimer.RunWorkerAsync();

private void SoxTimer_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
{
    var sr = new StreamReader(SoxPlayer.StandardError.BaseStream);
    while (sr.Peek() > 0)
    {
        var line = sr.ReadLine();
        UpdatePlaybackTime(line);
    }
}

private void SoxTimer_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
    if (!SoxPlayer.HasExited)
    {
        SoxTimer.RunWorkerAsync();
    }
}

当此 BackgroundWorker 完成时,它会检查是否 SoxPlayer.HasExited,如果没有,它会再次运行。这与我的第一次尝试具有相同的效果。 PlaybackSeconds 大约每 5 秒更新一次,此时它会更新到正确的时间,然后基于 PlaybackSeconds 值的其余代码也可以正常工作。

我也尝试通过创建线程来读取 StandardError 输出来实现相同的目的。每个实例都会产生相同的结果,在调用 UpdatePlaybackTime() 之间会有 5 秒左右的延迟。当它这样做时,它会遍历自我们上次遍历它以来发送到 StandardError 的所有输出,因此它会以小的增量非常快速地更新 PlaybackSeconds 值,并将其保留在当时的当前值。但同样,就用户而言,每 5 秒更新一次。

Sox 的创造者坚持认为问题不在他们这一端。在控制台窗口中播放时,输出是恒定的。根据 sox 创作者的说法,每 0.1 秒一次。如果我告诉 sox 将 is standarderror 输出到文本文件,同样的情况也会发生。文本文件上的信息不断更新。然而阅读 StandardError 流本身,我现在花了两天的大部分时间都没有可接受的结果。

感谢您的帮助。

编辑 2:

按照彼得在下面的建议,这是一个全新的项目。甚至没有更改任何内容的默认名称。与目前描述的行为相同。所以我要回去责备(咳咳,讨论)SoX 窥视者。

using System;
using System.Diagnostics;
using System.Threading;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        private Process SoxPlayer;
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            var bin = Application.StartupPath + "\bin\";

            SoxPlayer = new Process
            {
                StartInfo = new ProcessStartInfo
                {
                    CreateNoWindow = true,
                    RedirectStandardError = true,
                    UseShellExecute = false,
                    FileName = bin + "sox.exe",
                    Arguments = "song.ogg -c 2 -d",
                    WorkingDirectory = bin
                }
            };

            SoxPlayer.Start();
            var thread = new Thread(() =>
            {
                int cch;
                var rgch = new char[1];

                while ((cch = SoxPlayer.StandardError.Read(rgch, 0, rgch.Length)) > 0)
                {
                    var cch1 = cch;
                    label1.Invoke(new MethodInvoker(() => label1.Text = new string(rgch, 0, cch1)));
                }
            });
            thread.Start();
        }

        private void button2_Click(object sender, EventArgs e)
        {
            SoxPlayer.Kill();
        }
    }
}

这是一个简单的代码示例,不会重现您描述的行为:

class Program
{
    static void Main(string[] args)
    {
        Process process = new Process();

        process.StartInfo.FileName = "SimpleStderrWriter.exe";
        process.StartInfo.UseShellExecute = false;
        process.StartInfo.RedirectStandardError = true;
        process.StartInfo.RedirectStandardInput = true;

        process.Start();

        Thread thread = new Thread(() =>
        {
            int cch;
            char[] rgch = new char[1];

            Console.WriteLine("Reading stderr from process");

            while ((cch = process.StandardError.Read(rgch, 0, rgch.Length)) > 0)
            {
                Console.Write(new string(rgch, 0, cch));
            }

            Console.WriteLine();
            Console.WriteLine("Process exited");
        });

        Console.WriteLine("Press Enter to terminate process");
        thread.Start();

        Console.ReadLine();
        process.StandardInput.WriteLine();
        thread.Join();
    }
}

这是 SimpleStderrWriter.exe 程序的代码:

class Program
{
    static void Main(string[] args)
    {
        bool exit = false;

        Thread thread = new Thread(() =>
        {
            while (!Volatile.Read(ref exit))
            {
                Console.Error.Write('.');
                Thread.Sleep(250);
            }
        });

        thread.Start();
        Console.ReadLine();
        Volatile.Write(ref exit, true);
        thread.Join();
    }
}

此代码示例通过在生成子进程的 stderr 输出时尽快接收和重新发出它,清楚地表明 .NET 中没有任何默认情况下会导致您遇到的延迟。显而易见的结论是,要么你的 SoX 顾问是错误的,它出于某种原因做了一些缓冲,要么你自己在代码中添加了一些东西,导致你遇到延迟。

如果您确定后者不是这种情况,那么您需要回到您的 SoX 顾问那里并向他们解释他们错了。如果您确定 SoX Advisor 是正确的,那么您需要 post 一个与上述类似的示例,但 确实 重现了您遇到的延迟。