0

DirectSoundC#を使用して、マイクからスピーカーにサウンド サンプルをストリーミングしようとしています。「マイクを聞く」に似ているはずですが、後でこれを別の用途に使用したいと考えています。私のアプローチをテストすることで、バックグラウンドで静かなくすぐり、クラッキング ノイズに気付きました。これは、バッファの書き込みと再生の間の遅延と関係があると思います。これは、チャンクを書き込むための待ち時間よりも大きくなければなりません。

録音と再生の間の遅延を 50ms 未満に設定した場合。ほとんどの場合は機能しますが、非常に大きなクラッキング ノイズが発生することがあります。そのため、少なくとも 50 ミリ秒の遅延を決定しました。これは私にとっては問題ありませんが、システムの「デバイスをリッスンする」の遅延ははるかに短いようです。約 15 ~ 30 ミリ秒で、ほとんど目立たないと思います。50 ミリ秒で、少なくとも少しのリバーブ効果が得られます。

以下に、私のマイクコードを (部分的に) 示します。 初期化は次のように行われます。

        capture = new Capture(device);

        // Creating the buffer
        // Determining the buffer size
        bufferSize = format.AverageBytesPerSecond * bufferLength / 1000;
        while (bufferSize % format.BlockAlign != 0) bufferSize += 1;
        chunkSize = Math.Max(bufferSize, 256); 
        bufferSize = chunkSize * BUFFER_CHUNKS;
        this.bufferLength = chunkSize * 1000 / format.AverageBytesPerSecond; // Redetermining the buffer Length that will be used.

        captureBufferDescription = new CaptureBufferDescription();
        captureBufferDescription.BufferBytes = bufferSize;
        captureBufferDescription.Format = format;
        captureBuffer = new CaptureBuffer(captureBufferDescription, capture);

        // Creating Buffer control           
        bufferARE = new AutoResetEvent(false);
        // Adding notifier to buffer.
        bufferNotify = new Notify(captureBuffer);
        BufferPositionNotify[] bpns = new BufferPositionNotify[BUFFER_CHUNKS];
        for(int i = 0 ; i < BUFFER_CHUNKS ; i ++) bpns[i] =    new BufferPositionNotify() { Offset = chunkSize * (i+1) - 1, EventNotifyHandle = bufferARE.SafeWaitHandle.DangerousGetHandle() };
        bufferNotify.SetNotificationPositions(bpns); 

キャプチャは、追加のスレッドで次のように実行されます。

        // Initializing
        MemoryStream tempBuffer = new MemoryStream();

        // Capturing
        while (isCapturing && captureBuffer.Capturing)
        {
            bufferARE.WaitOne();
            if (isCapturing && captureBuffer.Capturing)
            {
                captureBuffer.Read(currentBufferPart * chunkSize, tempBuffer, chunkSize, LockFlag.None);
                ReportChunk(applyVolume(tempBuffer.GetBuffer()));
                currentBufferPart = (currentBufferPart + 1) % BUFFER_CHUNKS;
                tempBuffer.Dispose();
                tempBuffer = new MemoryStream(); // Reset Buffer;
            }
        }

        // Finalizing
        isCapturing = false;
        tempBuffer.Dispose();
        captureBuffer.Stop();
        if (bufferARE.WaitOne(bufferLength + 1)) currentBufferPart = (currentBufferPart + 1) % BUFFER_CHUNKS; // That on next start the correct bufferpart will be read.
        stateControlARE.Set();

キャプチャ中ReportChunkは、データをサブスクライブ可能なイベントとしてスピーカーに送信します。スピーカー部分は次のように初期化されます。

        // Creating the dxdevice.
        dxdevice = new Device(device);
        dxdevice.SetCooperativeLevel(hWnd, CooperativeLevel.Normal);

        // Creating the buffer
        bufferDescription = new BufferDescription();
        bufferDescription.BufferBytes = bufferSize;
        bufferDescription.Format = input.Format;
        bufferDescription.ControlVolume = true;

        bufferDescription.GlobalFocus = true; // That sound doesn't stop if the hWnd looses focus.
        bufferDescription.StickyFocus = true; // - " -
        buffer = new SecondaryBuffer(bufferDescription, dxdevice);
        chunkQueue = new Queue<byte[]>();

        // Creating buffer control
        bufferARE = new AutoResetEvent(false);

        // Register at input device
        input.ChunkCaptured += new AInput.ReportBuffer(input_ChunkCaptured);

データは、次の単純な方法で、イベント メソッドによってキューに入れられます。

        chunkQueue.Enqueue(buffer);
        bufferARE.Set();

再生バッファの充填と再生バッファの開始/停止は、別のスレッドによって行われます。

        // Initializing
        int wp = 0;
        bufferARE.WaitOne(); // wait for first chunk

        // Playing / writing data to play buffer.
        while (isPlaying)
        {
            Thread.Sleep(1);
            bufferARE.WaitOne(BufferLength * 3); // If a chunk is played and there is no new chunk we try to continue and may stop playing, else may the buffer runs out. 
            // Note that this may fails if the sender was interrupted within one chunk
            if (isPlaying)
            {
                if (chunkQueue.Count > 0)
                {
                    while (chunkQueue.Count > 0) wp = writeToBuffer(chunkQueue.Dequeue(), wp);
                    if (buffer.PlayPosition > wp - chunkSize * 3 / 2) buffer.SetCurrentPosition(((wp - chunkSize * 2 + bufferSize) % bufferSize));
                    if (!buffer.Status.Playing)
                    {
                        buffer.SetCurrentPosition(((wp - chunkSize * 2 + bufferSize) % bufferSize));    // We have 2 chunks buffered so we step back 2 chunks and play them while getting new chunks.
                        buffer.Play(0, BufferPlayFlags.Looping);
                    }
                }
                else
                {
                    buffer.Stop();
                    bufferARE.WaitOne(); // wait for a filling chunk
                }
            }
        }

        // Finalizing
        isPlaying = false;
        buffer.Stop();
        stateControlARE.Set();

writeToBufferは、最後の書き込み位置を表すandをthis.buffer.Write(wp, data, LockFlag.None);考慮して、エンキューされたチャンクをバッファーに書き込むだけです。これが私のコードで重要なことのすべてだと思います。定義が欠落している可能性があり、少なくともスレッドを開始/停止=制御する別の方法があります。bufferSizechunkSizewp

バッファーの入力を間違えた場合や初期化が間違っている場合に備えて、このコードを投稿しました。しかし、C# バイトコードの実行が遅すぎるなどの理由で、この問題が発生すると思います。しかし、最終的に私の質問は未解決です。私の質問は、レイテンシーを減らす方法と、存在してはならないノイズを回避する方法です。

4

2 に答える 2

0

ずっと前に、この問題はThread.Sleep(1);高い CPU 使用率との組み合わせが原因であることがわかりました。Windows のタイマーの解像度はデフォルトで 15,6 ミリ秒であるため、このスリープは 1 ミリ秒のスリープを意味するのではなく、次のクロック割り込みに達するまでスリープします。(詳細については、このペーパーを参照してください) 高い CPU 使用率と組み合わせると、チャンクの長さまたはそれ以上までスタックする可能性があります。

例:チャンクサイズが 40 ミリ秒の場合、これは約 46.8 ミリ秒 (3 * 15.6 ミリ秒) になる可能性があり、これがくすぐりの原因になります。そのための 1 つの解決策は、解像度を 1 ミリ秒に設定することです。これは次の方法で実行できます。

[DllImport("winmm.dll", EntryPoint="timeBeginPeriod", SetLastError=true)]
private static extern uint timeBeginPeriod(uint uiPeriod);

[DllImport("winmm.dll", EntryPoint="timeEndPeriod", SetLastError=true)]
private static extern uint timeEndPeriod(uint uiPeriod);

void routine()
{
   Thead.Sleep(1); // May takes about 15,6ms or even longer.
   timeBeginPeriod(1); // Should be set at the startup of the application.
   Thead.Sleep(1); // May takes about 1, 2 or 3 ms depending on the CPU usage.

   // ... time depending routines goes here ...

   timeEndPeriod(1); // Should end at application shutdown.
}

私の知る限り、これはdirectxによってすでに行われているはずです。ただし、この設定はグローバル設定であるため、アプリケーションの他の部分または他のアプリケーションによって変更される可能性があります。アプリケーションが設定を一度設定して取り消す場合、これは発生しません。しかし、どういうわけか、汚れたプログラムされた部分または他の実行中のアプリケーションが原因で発生するようです。

監視する必要があるもう 1 つのことは、何らかの理由で 1 つのチャンクをスキップしている場合、directx バッファーの正しい位置をまだ使用しているかどうかです。この場合、再同期が必要です。

于 2012-08-08T10:06:26.630 に答える