C# 播放前延迟音频

C# 播放前延迟音频,c#,C#,我遇到了一个问题,接收到的音频数据播放时会出现口吃,所以我想出了一个主意,在开始播放之前稍微延迟一下,这样应用程序在开始播放之前有更多的时间收集音频部分。其想法是,它将通过收集的音频播放,其余的音频在需要之前进入应用程序 //A raised event from a udp class when data is received (from it's own thread) void udpClient_DataReceived(byte[] bytes) { audioQueue.

我遇到了一个问题,接收到的音频数据播放时会出现口吃,所以我想出了一个主意,在开始播放之前稍微延迟一下,这样应用程序在开始播放之前有更多的时间收集音频部分。其想法是,它将通过收集的音频播放,其余的音频在需要之前进入应用程序

//A raised event from a udp class when data is received (from it's own thread)
void udpClient_DataReceived(byte[] bytes) 
{
    audioQueue.Enqueue (bytes); //ConcurrentQueue

    if (audioQueue.Count > 10 && !playing) { //count > 10 is about a one second delay
        playing = true;
        PlayQueue ();
    }
}

private void PlayQueue()
{
    byte[] a;
    while (audioQueue.Count > 0) {
        audioQueue.TryDequeue (out a);
        audIn.PlayAudio (a);
    }
    playing = false;
}
然而,该准则有两个问题:

1) 如果音频长度短于设置的限制,则在收集更多音频之前不会播放。所以我需要某种延迟,它不需要最少的数据量来触发它

2) 有时,最后几节会错过,并留在队列中等待下一场比赛。传入的数据与PlayQueue函数的while循环之间存在竞争。有时,它在接收到所有数据之前完成


我不太确定如何在我的代码中解决这两个问题,如果您能给我任何帮助,我将不胜感激。

每当新数据包到达时,udpclient将引发datareceived事件。由于对共享状态(ConcurrentQueue和playing)的访问未序列化,因此最终可能会有两个或多个线程运行PlayQueue中的代码,或者当队列变空时,播放线程停止,而udpclient中可能会有静止数据。另一个问题是没有检查结果TryDequeue

首先,我将有一个且只有一个线程读取您的ConcurrentQueue,并使用派生类型执行跨线程信令

在这种情况下,您的代码如下所示:

var mre = new ManualResetEvent(false);
Thread audio;
bool play = true; // global keep running flag.

// Call this ONCE, at the start of your app.
void Init()
{
    audio  = new Thread(PlayQueue);
    audio.Start();
}

void udpClient_DataReceived(byte[] bytes) 
{
    audioQueue.Enqueue (bytes); //ConcurrentQueue

    mre.Set(); // this signals that we have data

    // disbale the timer if the stream is done
    // and/or set play to false
    // based on bytes received in your byte array
}

private void PlayQueue()
{
    var aTimer = new System.Timers.Timer(1000); // play after 1 second
    var timeEvent = new ManualResetEvent(false);
    aTimer.Elapsed += (s,e) => { timeEvent.Set(); };  // the time will start the play
    byte[] a;
    mre.WaitOne();  // wait until there is data
    atimer.Enabled = true; // it will start playimg after 1000 miliseconds
    timeEvent.WaitOne(); // wait for the timer

    while (play) {
        if (audioQueue.TryDequeue (out a))  // check if we succesfull got an array
        {
           audIn.PlayAudio (a);
        }
    }
}

将此视为最简单的可行解决方案。我并不是说这是生产级多线程代码所需的所有螺母和螺栓…

如果您不知道如何正确处理诸如ManualResetEvent或AutoResetEvent之类的事件,您可能会发现自己被多余的代码淹没,而只是尝试同步设置并等待它们,有时甚至添加额外的事件就是为了这样做

通常在消费者/生产者问题的情况下,我倾向于使用联锁类,它是惊人的原子函数。 它更干净,没有死锁的风险

此解决方案将在每次队列为空时缓冲1秒

    bool play = true; // global keep running flag.
    int m_numberOfSamples = 0;

    // Call this ONCE, at the start of your app.
    void Init()
    {
        // perform any initialization here... maybe allocate queue..
    }

    void EnqueueSample(byte[] bytes)
    {
        audioQueue.Enqueue(bytes); //ConcurrentQueue

        int numberOfSamples = Interlocked.Increment(ref m_numberOfSamples);
        if(numberOfSamples == 1)
        {
            // this is a case of first sample
            // Start a Task with a 1 sec delay
            Task.Factory.StartNew(() =>
                {
                    // Buffering...
                    // if you want to buffer x samples, use 1000*x/SampleRate instead of 1000 for 1 sec.
                    Task.Delay(1000).Wait();
                    PlayQueue();
                }, TaskCreationOptions.LongRunning);
        }
    }

    private void PlayQueue()
    {
        // if we are here, there is at least 1 sample already.
        byte[] a;
        int remainingSamples = 0;
        do
        {
            if (audioQueue.TryDequeue(out a))  // check if we succesfull got an array
            {
                audIn.PlayAudio(a);
                remainingSamples = Interlocked.Decrement(ref m_numberOfSamples);
            }
        }
        while (play && remainingSamples > 0);
        // we got out either by play = false or remainingSamples == 0
        // if remainingSamples == 0, we will get back here with a different **Task**
        // after a new sample has entered into the queue and again we buffer 1 sec using the Task.Delay
    }

请不要在问题标题中包含关于所用语言的信息,除非没有它就没有意义。标记可以达到这个目的。感谢线程提示,但这并不能解决第一个问题,因为音频样本可能比比较值短。降低该值可能会恢复口吃。我认为需要另一种方式来实现延迟(可能触发计时器或其他东西)。播放结束后,它也不会重置延迟。1秒需要多少字节?它没有重置是正确的,但是在您的问题中也没有信息,您如何能够检测到,换句话说,您如何知道流已经结束。通过在循环中引入mre.WaitOne()可以很容易地添加它。如果您知道流何时结束,可以调用mre.Reset。这将再次阻止PlayQueue方法。添加了一个计时器并将其移动到PlayQueue中,这是真的。目前还没有关于字节的任何规定,所以一旦我这样做了(这本身可能是它自己的provlen),就不难完成了。谢谢一旦这被编码和测试,如果这有效,我会接受你的答案。但是这段代码中的numberofSamples与TrydQueue返回false不一样吗?这段代码可能还会旋转第二个线程(但第一个线程几乎会在之后结束)。您对samplerate的建议非常关键。@rene使用Interlocked的外部int m_numberOfSamples可以为您带来额外的好处,如果不锁定队列并查询队列计数,仅使用ConcurrentQueue是无法实现的。按照我的实现方式,您甚至可以有多个生产者和一个消费者,只有将队列中的样本数从0增加到1的第一个生产者将使线程加速。我同意,这段代码可以旋转另一个线程,但只有当当前线程已经完成它的工作并且即将离开该方法时,才可以旋转。事实上,我认为我说得太早了。这看起来不像是任务。延迟是在做任何事情,我最终会创建很多线程。是
线程睡眠(1000)?对于是否应该使用
Thread.Sleep()
,我听到了不同的意见;对,很抱歉,忘记了Task.DelayThread.Sleep(1000)上的Wait();在我创建任务之前的if语句中,将仅阻止生产者1秒,而不会阻止使用者1秒。我们仍然希望生产者在消费者等待时向队列提供更多样本。