目錄
幾個相關的類
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
接口。這有沒有問題?
其實NAudio
在Init
中使用的是擴展方法。
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.dll
的waveOutOpen函數
,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)函數。
以上就是播放過程的主體流程分析,讀者如果要分析播放緩衝的數據內容,可以再深入分析WaveOutBuffer
、SampleChannel
及AudioFileReader
等相關的類。