NAudio用法詳解(6)播放過程流程分析

幾個相關的類

WaveFormat

 public class WaveFormat
    {
        /// <summary>format type</summary>
        protected WaveFormatEncoding waveFormatTag;
        /// <summary>number of channels</summary>
        protected short channels;
        /// <summary>sample rate</summary>
        protected int sampleRate;
        /// <summary>for buffer estimation</summary>
        protected int averageBytesPerSecond;
        /// <summary>block size of data</summary>
        protected short blockAlign;
        /// <summary>number of bits per sample of mono data</summary>
        protected short bitsPerSample;
        /// <summary>number of following bytes</summary>
        protected short extraSize;
        //省略屬性及函數描述
    }

這個類非常關鍵,在waveInOpen和waveOutOpen函數中都需要用到這個類。
這個類對應的C++結構爲WAVEFORMATEX,定義如下。

C++
typedef struct {
  WORD  wFormatTag;
  WORD  nChannels;
  DWORD nSamplesPerSec;
  DWORD nAvgBytesPerSec;
  WORD  nBlockAlign;
  WORD  wBitsPerSample;
  WORD  cbSize;
} WAVEFORMATEX;
參數 C++數據類型 Net數據類型 說明
waveFormatTag WORD ushort 格式類型,例如pcm格式、adpcm等
channels WORD sort 通道數量,例如單聲道,雙聲道,多聲道
sampleRate DWORD int 採樣率,例如8000,16000,44100等。
averageBytesPerSecond DWORD int 平均數據傳輸率,每秒的字節數
blockAlign WORD short 數據塊的大小,特定聲音格式下的最小數據單元的大小(字節數),程序每次處理的數據必須是這個大小的倍數。
bitsPerSample WORD short 沒采樣的位數,這個參數和格式有關,8位、16位、20位或者24位等等。
extraSize WORD short 額外信息的字節數,和格式類型有關。

更詳細的參數說明,參考WAVEFORMATEX structure

IWavePlayer接口

這個接口,定義了播放相關的類要實現的功能,包括初始化(Init)、打開(Open)、關閉(Close)和暫停(Pause),還包括兩個屬性(Volume和PlaybackState)和一個播放停止事件(PlaybackStopped)。從下圖可以看出,WaveOut、WaveOutEvent都實現本接口。
在這裏插入圖片描述

IWavePosition接口

這個接口只有一個只讀屬性(WaveFormat OutputWaveFormat)和一個函數(long GetPosition())。GetPosition函數獲得已經播放的位置(字節數)。WaveOut和WaveOutEvent實現了本接口。

在這裏插入圖片描述

IWaveProvider接口

這個接口是所有WaveProvider的通用接口,例如WaveInProvider
在這裏插入圖片描述
在這裏插入圖片描述

ISampleProvider

ISampleProvider和IWaveProvider接口從結構上沒有任何區別。區別在於Read函數的參數類型。一個緩衝爲Int型,一個緩衝爲float型。

接口 Read函數
IWaveProvider int Read(byte[] buffer, int offset, int count);
ISampleProvider int Read(float[] buffer, int offset, int count);

在這裏插入圖片描述

WaveOutEvent播放文件過程分析

由於WaveOutEvent是WaveOut播放方式的默認方法,因此,首先分析基於本類的播放過程。掌握了本類的播放過程後,其它類的播放過程也就迎刃而解了。

準備聲音文件(AudioFileReader )

private AudioFileReader audioFileReader;
audioFileReader = new AudioFileReader(fileName);

AudioFileReader的繼承關係如下圖所示。
在這裏插入圖片描述
在分析播放過程時,再詳細說明本類的功能。

播放類(WaveOutEvent )初始化

只需要給waveOutEvent類在初始化時傳入AudioFileReader 對象即可。

private WaveOutEvent waveOutEvent = new WaveOutEvent();
waveOutEvent.Init(audioFileReader);

waveOutEvent.Init()函數說明

細心的讀者會看到WaveOutEvent的Init()函數的聲明如下。

public void Init(IWaveProvider waveProvider)

裏邊需要傳入IWaveProvider類型的對象,而上面的代碼調用中,傳入的是AudioFileReader對象,而AudioFileReader實現的是ISampleProvider接口。這有沒有問題?
其實NAudioInit中使用的是擴展方法。

public static class WaveExtensionMethods
{
......
        public static void Init(this IWavePlayer wavePlayer, ISampleProvider sampleProvider, bool convertTo16Bit = false)
        {
            IWaveProvider provider = convertTo16Bit ? (IWaveProvider)new SampleToWaveProvider16(sampleProvider) : new SampleToWaveProvider(sampleProvider);
            wavePlayer.Init(provider);
        }
......
}

從上面的代碼可以看出,首先對sampleProvider轉換成waveProvider,然後再調用WaveOutEvent的Init方法。

WaveOutEvent.Init()過程分析

爲了代碼註釋說明的方便,下面將代碼斷開。

        public void Init(IWaveProvider waveProvider)
        {
            if (playbackState != PlaybackState.Stopped)
            {
                throw new InvalidOperationException("Can't re-initialize during playback");
            }
            if (hWaveOut != IntPtr.Zero)
            {
                // normally we don't allow calling Init twice, but as experiment, see if we can clean up and go again
                // try to allow reuse of this waveOut device
                // n.b. risky if Playback thread has not exited
                DisposeBuffers();
                CloseWaveOut();
            }

相關初始化

            callbackEvent = new AutoResetEvent(false);

回調事件信號

            waveStream = waveProvider;
            int bufferSize = waveProvider.WaveFormat.ConvertLatencyToByteSize((DesiredLatency + NumberOfBuffers - 1) / NumberOfBuffers);

根據設定的延遲時間,計算緩衝區的大小。

            MmResult result;
            lock (waveOutLock)
            {
                result = WaveInterop.waveOutOpenWindow(out hWaveOut, (IntPtr)DeviceNumber, waveStream.WaveFormat, callbackEvent.SafeWaitHandle.DangerousGetHandle(), IntPtr.Zero, WaveInterop.WaveInOutOpenFlags.CallbackEvent);
            }

調用winmm.dllwaveOutOpen函數,fdwOpen參數傳入的是CallbackEvent標識,因此是事件機制。回調函數並非某個直接的函數,而是AutoResetEvent對象。
根據回調機制,播放中,播放緩衝區有變化時,會觸發回調函數,在回調處理過程中,需要給播放緩衝區讀入新的數據,但是AutoResetEvent對象顯然沒法實現讀入新數據的能力。那麼WaveOutEvent究竟是如何實現這個功能呢?
NAudiio是在播放線程中,根據AutoResetEvent對象的信號量狀態讀取新的數據

            MmException.Try(result, "waveOutOpen");

            buffers = new WaveOutBuffer[NumberOfBuffers];
            playbackState = PlaybackState.Stopped;
            for (var n = 0; n < NumberOfBuffers; n++)
            {
                buffers[n] = new WaveOutBuffer(hWaveOut, bufferSize, waveStream, waveOutLock);
            }
        }

初始化緩衝區

播放過程

播放過程看起來非常簡單:

            waveOutEvent.Play();
            waveOutEvent.Volume = 0.3f;
        public void Play()
        {
            if (buffers == null || waveStream == null)
            {
                throw new InvalidOperationException("Must call Init first");
            }
            if (playbackState == PlaybackState.Stopped)
            {
                playbackState = PlaybackState.Playing;
                callbackEvent.Set(); // give the thread a kick
               ThreadPool.QueueUserWorkItem(state => PlaybackThread(), null);
            }
            else if (playbackState == PlaybackState.Paused)
            {
                Resume();
                callbackEvent.Set(); // give the thread a kick
            }
        }

核心就是這句:ThreadPool.QueueUserWorkItem(state => PlaybackThread(), null);線程函數如下:

        private void PlaybackThread()
        {
            Exception exception = null;
            try
            {
                DoPlayback();
            }
            catch (Exception e)
            {
                exception = e;
            }
            finally
            {
                playbackState = PlaybackState.Stopped;
                // we're exiting our background thread
                RaisePlaybackStoppedEvent(exception);
            }
        }

真正的線程函數如下:

        private void DoPlayback()
        {
            while (playbackState != PlaybackState.Stopped)
            {
                if (!callbackEvent.WaitOne(DesiredLatency))
                {
                    if (playbackState == PlaybackState.Playing)
                    {
                        Debug.WriteLine("WARNING: WaveOutEvent callback event timeout");
                    }
                }
                    
                
                // requeue any buffers returned to us
                if (playbackState == PlaybackState.Playing)
                {
                    int queued = 0;
                    foreach (var buffer in buffers)
                    {
                        if (buffer.InQueue || buffer.OnDone())
                        {
                            queued++;
                        }
                    }
                    if (queued == 0)
                    {
                        // we got to the end
                        playbackState = PlaybackState.Stopped;
                        callbackEvent.Set();
                    }
                }
            }
        }

核心語句就是if (buffer.InQueue || buffer.OnDone())

先說buffer.OnDone()

        internal bool OnDone()
        {
            int bytes;
            lock (waveStream)
            {
                bytes = waveStream.Read(buffer, 0, buffer.Length);
            }
            if (bytes == 0)
            {
                return false;
            }
            for (int n = bytes; n < buffer.Length; n++)
            {
                buffer[n] = 0;
            }
            WriteToWaveOut();
            return true;
        }

從代碼可以看出來,從waveStream中讀取數據,waveStream實際指向audioFileReader,因此是執行audioFileReader.Read功能。
最後調用WriteToWaveOut發送到聲音設備。

        private void WriteToWaveOut()
        {
            MmResult result;

            lock (waveOutLock)
            {
                result = WaveInterop.waveOutWrite(hWaveOut, header, Marshal.SizeOf(header));
            }
            if (result != MmResult.NoError)
            {
                throw new MmException(result, "waveOutWrite");
            }

            GC.KeepAlive(this);
        }

最終調用winmm.dll庫的waveOutWrite函數。
WaveInterop.waveOutWrite(hWaveOut, header, Marshal.SizeOf(header));

播放結束

直接調用WaveOutEvent的Stop方法即可。

        public void Stop()
        {
            if (playbackState != PlaybackState.Stopped)
            {
                // in the call to waveOutReset with function callbacks
                // some drivers will block here until OnDone is called
                // for every buffer
                playbackState = PlaybackState.Stopped; // set this here to avoid a problem with some drivers whereby 
                MmResult result;
                lock (waveOutLock)
                {
                    result = WaveInterop.waveOutReset(hWaveOut);
                }
                if (result != MmResult.NoError)
                {
                    throw new MmException(result, "waveOutReset");
                }
                callbackEvent.Set(); // give the thread a kick, make sure we exit
            }
        }

最終調用 winmm.dll庫的waveOutReset(hWaveOut)函數。
以上就是播放過程的主體流程分析,讀者如果要分析播放緩衝的數據內容,可以再深入分析WaveOutBufferSampleChannelAudioFileReader等相關的類。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章