帮酷LOGO
0 0 评论
  • 显示原文与译文双语对照的内容
文章标签:播放器  play  Implementation  OGG  IMP  Vorbis  


Screenshot - OggPlayer.jpg

介紹

TgPlayOgg項目是一個. NET C# 庫,允許你從托管代碼播放 Ogg Vorbis文件。 通過TgPlayOgg將給定 Ogg Vorbis文件解碼為可用的聲音數據,這將調用非托管 C++ 項目 TGPlayOgg_vorbisfile。 TgPlayOgg還需要經過管理的DirectX來輸出聲音。

背景

為了讓我們能夠向第三方開發者提供播放聲音文件的支持,我們需要向第三方玩家在線遊戲開發SDK添加支持。 我們開始使用MP3音頻格式,但我們擔心的是( 當你達到一定的銷售水平后,費用就會上升)的許可問題。 經過比較之後,我們選擇使用 Ogg Vorbis格式。 Vorbis是一款完全開放。專利免費。專業音頻編碼和流技術,具有開放源碼的所有優點。

使用代碼

下載源代碼時," OggPlayer示例"文件夾下會有一個" OggPlayer.sln"解決方案文件,它將構建本文中提到的所有項目。 一個示例測試應用程序已經在TgPlayOgg項目下的"測試應用"文件夾中提供了。 這個應用程序演示了如何使用庫。 這些步驟如下所示:

  • 包含對TgPlayOgg項目的引用並導入 TG.Sound 命名空間。
  • 構造 OggPlay 類( 你的應用只需要一個實例,不管同時播放多少個 Ogg Vorbis文件)的實例。
  • PlayOggFile 事件處理程序添加到 PlayOggFileResult 委託。 現在可以隨時調用 PlayOggFile 了,只要你願意。 注意,PlayOggFile 在調用之後立即返回,因為解碼和回放是在單獨的線程中完成的。 當文件完成播放時,將調用你的事件處理程序。
  • 當你完成了 OggPlay 類的實例后,你將要調用 Dispose 來確保up對象使用的非托管資源。

讓我們來看看測試應用程序的亮點。 首先我們看到它有一個方法來執行初始化並處理 PlayOggFile 事件。 請注意,我們必須在 try 塊,因為它調用DirectSound可能引發異常。 然後它有另一個方法允許用戶選擇一個 Ogg Vorbis聲音文件來打開播放。

using TG.Sound;privatevoid InitTestOfOggPlayer()
{
 try {
 oplay = new OggPlay(this, OggSampleSize.SixteenBits);
 oplay.PlayOggFileResult += new PlayOggFileEventHandler(PlayOggFileResult);
 textBox1.Text = "Initialization successful.rn";
 }
 catch(Exception e)
 {
 textBox1.Text = "Initialization failed:" + e.Message + "rn";
 }
}privatevoid Button1Click(object sender, System.EventArgs e)
{
 OggName = GetOggFileNameToOpen();
 if (OggName!= null)
 {
 oplay.PlayOggFile(OggName, ++PlayId);
 textBox1.Text = "Playing" + OggName + " Id=" + PlayId.ToString() + "rn";
 } 
}

Ogg Vorbis解碼器在解碼 Ogg Vorbis數據時可能遇到錯誤。 如果沒有足夠的數據流通過初始緩衝區( Ogg太小了),則or文件可以能拒絕播放。 僅有兩個錯誤計數,因為成功創建的波形數據,但如果這兩個計數不是非零,則可以能沒有發音。 處理 PlayOggFile 事件的方法是顯示指示成功或者錯誤( 使用兩個錯誤計數)的狀態消息。 稍後我們將進一步了解這些錯誤計數意味著什麼。

privatestaticvoid PlayOggFileResult(object sender, PlayOggFileEventArgs e)
{
 if (e.Success)
 {
 MainForm.textBox1.Text += "PlayOggFile(" + e.PlayId + ") succeeded (" + "ErrorHoleCount:" + e.ErrorHoleCount + ", ErrorBadLinkCount:" 
 + e.ErrorBadLinkCount + ").rn";
 }
 else {
 MainForm.textBox1.Text += "PlayOggFile(" + e.PlayId + ") failed: '" 
 + e.ReasonForFailure + "'rn";
 }
 PlayId--;
}

注意,如果一個或者多個 Ogg Vorbis文件仍在播放,退出調用應用程序不會終止播放線程。 這些回放線程繼續運行,儘管你不能再播放它們。 線程將播放他們正在播放的,文件,然後退出,除非特別告訴線程停止播放。 因此,當應用程序退出時,它可能會殺死任何仍在播放的Ogg Vorbis文件。 這就是測試應用程序通過調用 OggPlay.StopOggFile 來處理 Form.Closing 事件的原因,稍後你將了解更多信息。

protectedvoid Form1_Closing(object sender, 
 System.ComponentModel.CancelEventArgs e)
{
 // Determine if any Ogg files are still playing by checking the PlayId memberif (PlayId >0)
 {
 // Display a MsgBox asking the user to save changes or abortif (MessageBox.Show("Ogg files are still playing," + 
 " are you sure you want to exit?", "TrayGames Ogg Player",
 MessageBoxButtons.YesNo) == DialogResult.No)
 {
 // Cancel the Closing event from closing the form e.Cancel = true;
 // Wait for files to finish playing... }
 else {
 // Kill all outstanding playbackswhile (PlayId >0)
 oplay.StopOggFile(PlayId--); 
 }
 }
}

它的他時候,如果應用程序有暫停功能,或者想在用戶離開遊戲時重置聲音。

Ogg Vorbis包裝

Ogg Vorbis'高級 API,Vorbisfile,只有兩個輸入選項: 一個C 文件指針或者一組自定義回調函數,用於讀取輸入of數據。 這兩種選擇更好且更具可移植性,可能是自定義回調,但是我不知道. NET 1.1的調用約定,它的標準調用約定是 StdCall,而Vorbisfile動態鏈接庫( DLLs ) 是用 Cdecl 調用約定編譯的。 因此,給定 C# 和. NET 1.1,我們決定編寫一些 C/C++ 代碼並將它的編譯成 DLL,這個DLL包含Vorbisfile需要的回調。 這就是我們創建TGPlayOgg_vorbis包裝器項目的原因。

從since了解到,可以使用 DllImportAttribute 類提供調用從非托管DLL導出的函數所需的信息。 因此你應該能夠修改這個庫的源代碼,以消除TGPlayOgg_vorbis包裝器項目,並從TGPlayOgg項目直接調用 Vorbisfile API調用。 .NET 基類庫( BCL ) 提供 StdCallCdeclThisCallWinApi 調用約定。 WinApi 根據平台( Windows 或者 Windows CE ) 自動選擇正確的類型。 例如要更改位於 SomeLibrary.dll 中的SomeFunction,可以使用下面的代碼:

[DllImport("SomeLibrary.DLL", EntryPoint="SomeFunction", SetLastError=true,
CharSet=CharSet.Unicode, ExactSpelling=true,
CallingConvention=CallingConvention.Cdecl)]publicstaticexternbool SomeFunction(String param1, String param2);

目前,TGPlayOgg_vorbis項目為我們提供了Ogg的調用。 有三個包裝函數: init_file_for_ogg_decodeogg_decode_one_vorbis_packetogg_final_cleanup。 你將永遠不會直接調用這些方法,C# 庫將使它們能夠解碼文件。 如果要為自己的托管應用程序添加這些方法的定義,你可以定義 NativeMethods 類( 使用任何類名稱),並將函數Prototype添加到它。 Having advise函數的單獨類,因為使用DLL函數可能會導致錯誤。 封裝DLL聲明使你在調試時更容易工作。 TGPlayOgg_vorbis項目中的" Vorbisapi.cs"文件已經具有包含聲明的類定義:

// External C functions in the TgPlayOgg_vorbisfile unmanaged DLL[DllImport("TgPlayOgg_vorbisfile.dll", CharSet=CharSet.Unicode,
 CallingConvention=CallingConvention.Cdecl)]publicunsafestaticexternint init_for_ogg_decode(string fileName, void **vf_out);
[DllImport("TgPlayOgg_vorbisfile.dll", CallingConvention=CallingConvention.Cdecl)]publicunsafestaticexternint ogg_decode_one_vorbis_packet(
 void *vf_ptr, void *buf_out, int buf_byte_size, int bits_per_sample, int *channels_cnt, int *sampling_rate, int *err_ov_hole_cnt, int *err_ov_ebadlink_cnt);
[DllImport("TgPlayOgg_vorbisfile.dll", CallingConvention=CallingConvention.Cdecl)]publicunsafestaticexternint final_ogg_cleanup(void *vf_ptr);

讓我們進行這些調用的是平台調用( PInvoke ) 服務。 PInvoke將使托管代碼調用在DLL中實現的非托管函數。 它將定位並調用導出的函數,並根據需要跨托管/非托管代碼邊界封送它們的參數。 注意,PInvoke拋出由托管調用方生成的非托管函數的異常。 現在讓我們看看非托管函數。

init_file_for_ogg_decode 函數將打開並初始化給定 Ogg Vorbis文件以進行解碼。 它通過調用 op_open API函數來建立所有相關的解碼結構。 另外,你應該注意到 ov_open 一旦成功,就擁有了文件資源的完全擁有。 使用 ov_open 打開文件后,必須使用,而不是 fclose 或者任何其他函數來將它的按 close。 我們的包裝函數處理所有這些,下面是初始化函數的外觀:

int init_file_for_ogg_decode(wchar_t *filename, void **vf_out)
{
 //.. . int ov_ret = ov_open(file_ptr, static_cast<OggVorbis_File*>(vf_ptr), NULL, 0);
 if (ov_ret <0)
 {
 // There was an error so cleanup now fclose(file_ptr);
 free(vf_ptr);
 // Return the ifod_err_ codereturn err_code;
 }
 // Copy the memory pointer to the caller *vf_out = vf_ptr;
 return0; // success}

ogg_decode_one_vorbis_packet 函數將 PCM ( 脈衝編碼調製) 數據寫入給定緩衝區,並返回寫入該緩衝區的位元組數。 首先它調用 ov_read,它返回所請求的位元組。signedness和單詞大小所解碼的PCM音頻的指定位元組數。 如果音頻是多聲道的,則通道在輸出緩衝區中交錯。 這裡函數用於在循環中解碼Vorbis文件。 我們的C# 應用程序,我們稍後將看到,它將在。

接下來它調用 ov_info,它返回指定流的vorbis_info 結構。 這允許我們返迴流量中的通道數,以及我們的C# 應用程序的取樣率。 有兩種可能發生的錯誤: OV_HOLE 指示數據中有中斷,OV_EBADLINK 指示提供了無效的流節,或者請求的鏈接已經損壞。

a 格式允許多個邏輯子目錄將( 帶限制) 組合到單個物理流程中。 請注意,Vorbisfile API可以在應用程序中隱藏多個邏輯流的本質,但是應用程序必須注意多個流段。 Ogg Vorbis文檔提供了更多關於Ogg邏輯比特流框架的信息。

int ogg_decode_one_vorbis_packet(void *vf_ptr, 
 void *buf_out, int buf_byte_size, 
 int ogg_sample_size, 
 int *channels_cnt, int *sampling_rate, 
 int *err_ov_hole_cnt, int *err_ov_ebadlink_cnt)
{
 //.. . for (bytes_put_in_buf = 0;;)
 {
 long ov_ret = ov_read(static_cast<OggVorbis_File*>(vf_ptr), 
 static_cast<char*>(buf_out), buf_byte_size, 0, 
 word_size, want_signed, &bitstream);
 if (ov_ret == 0) // at EOF {
 break;
 }
 elseif (ov_ret <0)
 {
 // An error occurred, bad ogg data of some kindif (ov_ret == OV_HOLE)
 ++(*err_ov_hole_cnt);
 elseif (ov_ret == OV_EBADLINK)
 ++(*err_ov_ebadlink_cnt);
 }
 else 
 {
 assert(ov_ret <= buf_byte_size);
 vorbis_info* vi_ptr = ov_info(static_cast<OggVorbis_File*>(vf_ptr), 
 bitstream);
 if (vi_ptr!= NULL)
 {
 // Number of channels in the bitstream *channels_cnt = vi_ptr->channels;
 // Sampling rate of the bitstream *sampling_rate = vi_ptr->rate;
 }
 bytes_put_in_buf = ov_ret;
 break;
 }
 }
 return bytes_put_in_buf;
}

使用 ov_open 打開比特流並完成解碼后,應用程序必須調用 ov_clear 來清除解碼器的緩衝區並關閉文件。 通過調用這個函數來實現 ogg_final_cleanup 函數,它還釋放了 vf_out 指向的內存。 你可以查看 Vorbisfile API文檔,了解關於這些函數的更多信息。

int ogg_final_cleanup(void *vf_ptr)
{
 int ret = 0;
 if (vf_ptr!= NULL)
 {
 ret = ov_clear(static_cast<OggVorbis_File*>(vf_ptr));
 // non-zero is failure free(vf_ptr);
 }
 return ret;
}

.NET Ogg Vorbis庫

為了播放來自解碼 Ogg Vorbis文件數據的波形數據,微軟. NET 1.1框架沒有聲音播放類,基本上有兩種選擇。 第一種方法是將波形數據輸出為WAV文件,然後使用 quartz.dll ( Win98和更高版本上) 播放WAV文件。 這裡選項的缺點是WAV文件可能非常大( 比如。 測試了 5.5 MB vmkernel文件,並導致了 67 MB WAV文件,並且在完成整個WAV文件之後回放( 比如 )。 在上解碼 5.5文件並寫出一個WAV文件需要超過10秒的。 另一種選擇是使用托管DirectX中的方法,這意味著無需編寫任何WAV文件。 TrayGames客戶機已經確保托管 DirectX api已經安裝在目標電腦上,這對我們來說並不是問題。

OggPlay 類是應用程序將使用的主類。 它的構造函數創建新的DirectX聲音設備,設置合作級別和 Ogg Vorbis文件的樣本大小。

public OggPlay(Control owner, OggSampleSize wantedOggSampleSize)
{
 // Set DirectSoundDevice DirectSoundDevice = new Device();
 // NOTE: The DirectSound documentation recommends// CooperativeLevel.Priority for games DirectSoundDevice.SetCooperativeLevel(owner, 
 CooperativeLevel.Priority);
 // Set OggSampleSize OggFileSampleSize = wantedOggSampleSize;
}

owner 參數使用參數,它定義它的參數作為"使用設備對象的應用程序的System.Windows.Forms.Control"。 這可能是你的應用程序窗口的主要內容。 wantedOggSampleSize 參數為 8位或者 16位。 8-bit 樣本大小較低,但比 16位 示例大小更快,佔用內存少。 如果你的Ogg應用程序的Vorbis文件編碼為 8-bit 樣本大小,那麼選擇 8 ( 你也可以選擇 16,但是如果are源只是 8 -bit,那麼它是浪費和獲得什麼的。)。 在你使用的Ogg應用程序中,你可以在播放時選擇,如果你希望最小化播放資源要求,請選擇 8來獲取完整音質,或者選擇 8. 如果Ogg應用程序的Vorbis文件是混合( 有些用 8-bit 樣本大小編碼,有些則用 16位 樣本大小編碼),那麼選擇你認為最好的( 設置,8或者 16位,將播放所有 Ogg Vorbis文件)。

TgPlayOgg庫聲明兩個帶有委託的事件和一個事件參數類( 它為兩個事件定義數據),用於播放和停止 Ogg Vorbis文件。 當 PlayOggFile 方法完成時,PlayOggFileResult 事件( PlayOggFileEventHandler 委託) 用於事件通知,而在客戶端希望提前中斷回放時使用 StopOggFileNow 事件( StopOggFileEventHandler 委託)。 下面是事件參數類的數據成員。

publicsealedclass PlayOggFileEventArgs : EventArgs
{
 privatebool success;
 // If!Success then this is the explanation for the failureprivatestring reasonForFailure;
 // The value of the playID parameter when PlayOggFile() was calledprivateint playId;
 publicint ErrorHoleCount,
 // Count of encountered OV_HOLE errors during decoding// indicates there was an interruption in the data. ErrorBadLinkCount;
 // Count of encountered OV_EBADLINK errors during decoding// indicates that an invalid stream// section was supplied to libvorbisfile, //.. . }

OggPlay 提供了兩個簡單的方法 PlayOggFileStopOggFilePlayOggFile 播放由 fileName 參數指定的Ogg Vorbis文件。 playId 參數是由用戶確定的任意值,並在引發的PlayOggFileResult 事件中返回。 此事件由 PlayOggFileThreadProc 引發。 在事件處理程序代碼中,可以使用返回的playId 知道哪個特定的PlayOggFile 調用導致該處理事件。 這就是你的應用程序應該附加到 PlayOggFileEventHandler 代理的原因。

publicvoid PlayOggFile(string fileName, int playId)
{
 PlayOggFileEventArgs EventArgs = new PlayOggFileEventArgs(playId);
 // Decode the ogg file in a separate thread PlayOggFileThreadInfo pofInfo = new PlayOggFileThreadInfo(
 EventArgs, fileName, 
 OggFileSampleSize == OggSampleSize.EightBits? 8 : 16,
 DirectSoundDevice, this);
 Thread PlaybackThread = new Thread(new 
 ThreadStart(pofInfo.PlayOggFileThreadProc));
 PlaybackThread.Start();
 Thread.Sleep(0);
}

StopOggFile 引發 StopOggFileNow 事件。 這裡事件將由 PlayOggFileThreadProc 方法處理。 應用程序不需要附加到 StopOggFileEventHandler 委託,但 PlayOggFileThreadProc 當然會。

publicvoid StopOggFile(int playId)
{
 PlayOggFileEventArgs EventArgs = new PlayOggFileEventArgs(playId);
 StopOggFileNow(this, EventArgs); 
}

OggPlay 類包含 PlayOggFileThreadInfo 類,它用作在 OggPlay 類的PlayOggFile 方法中創建的回放線程的線程類。 在某種程度上,此類位於托管和非托管環境之間。 它通過調用上面描述的托管 Ogg Vorbis包裝器來代表 OggPlay 工作。 這個類的主要方法是 PlayOggFileThreadProc,我們現在將查看該方法的一些部分。

PlayOggFileThreadProc 首先要做的是初始化 Ogg Vorbis文件,通過調用 Ogg Vorbis包裝器進行解碼。 如果在初始化期間遇到錯誤,則通過 PlayOggFileEventHandler ( 請參見下面) 返回。 注意,文件名。採樣率和DirectSound設備都通過它的構造函數傳遞給此類。 構造函數還註冊類'InterruptOggFilePlayback 方法to來處理 StopOggFileNow

int ErrorCode = NativeMethods.init_file_for_ogg_decode(FileName, &vf);if (ErrorCode!= 0)
{
 //.. .  oplay.PlayOggFileResult(this, EventArgs);
 return;
}

下一個 PlayOggFileThreadProc 創建PCM位元組 array 並將它傳遞給 ogg_decode_one_vorbis_packet 函數。 這個函數將返回解碼 Ogg Vorbis數據的第一個塊及其大小。

// Get next chunk of PCM data, pin these so GC can't relocate themfixed(byte *buf = &PcmBuffer[0])
{
 fixed(int *HoleCount = &EventArgs.ErrorHoleCount)
 {
 fixed(int *BadLinkCount = &EventArgs.ErrorBadLinkCount)
 {
 // NOTE: The sample size of the returned PCM data -- either 8-bit// or 16-bit samples -- is set by BitsPerSample PcmBytes = NativeMethods.ogg_decode_one_vorbis_packet(
 vf, buf, PcmBuffer.Length,
 BitsPerSample,
 &ChannelsCount, &SamplingRate,
 HoleCount, BadLinkCount);
 }
 }
}

第一次從 ogg_decode_one_vorbis_packet 函數返回時,我們創建 DirectSound WaveFormatBufferDescriptionSecondaryBufferNotify 對象。 WaveFormat 用於在解碼波形音頻數據后保存它的格式。 BufferDescription 將描述新緩衝對象的特性,包括 WaveFormatSecondaryBuffer 具有用於管理聲音緩衝區的方法和屬性。 Notify 允許我們在播放期間在不同的點設置通知觸發器。

int HoldThisManySamples =
 (int)(SamplingRate * SecBufHoldThisManySeconds);// Set the formatMyWaveFormat.AverageBytesPerSecond = AverageBytesPerSecond;
MyWaveFormat.BitsPerSample = (short)BitsPerSample;
MyWaveFormat.BlockAlign = (short)BlockAlign;
MyWaveFormat.Channels = (short)ChannelsCount;
MyWaveFormat.SamplesPerSecond = SamplingRate;
MyWaveFormat.FormatTag = WaveFormatTag.Pcm;// Set BufferDescriptionMyDescription = new BufferDescription();
MyDescription.Format = MyWaveFormat;
MyDescription.BufferBytes =
SecBufByteSize = HoldThisManySamples * BlockAlign;
MyDescription.CanGetCurrentPosition = true;
MyDescription.ControlPositionNotify = true;// Create the bufferSecBuf = new SecondaryBuffer(MyDescription, DirectSoundDevice);// Set 3 notification points, at 0, 1/3, and 2/3 SecBuf sizeMyNotify = new Notify(SecBuf);
BufferPositionNotify[] MyBufferPositions = new BufferPositionNotify[3];
MyBufferPositions[0].Offset = 0;
MyBufferPositions[0].EventNotifyHandle =
 SecBufNotifyAtBegin.Handle;
MyBufferPositions[1].Offset =
 (HoldThisManySamples/3) * BlockAlign;
MyBufferPositions[1].EventNotifyHandle =
 SecBufNotifyAtOneThird.Handle;
MyBufferPositions[2].Offset =
 ((HoldThisManySamples * 2)/3) * BlockAlign;
MyBufferPositions[2].EventNotifyHandle =
 SecBufNotifyAtTwoThirds.Handle;
MyNotify.SetNotificationPositions(MyBufferPositions);

在準備好這些對象之後,我們將解碼的PCM數據載入到 MemoryStream 對象中。 這裡流被寫入DirectSound緩衝區對象,然後使用非同步 Play 方法播放。 這個過程不斷重複直到我們到達 Ogg Vorbis文件的末尾。 必須知道,多個比特流段不一定使用相同數量的通道或者採樣速率( 我們把它稱為它的格式)。 我們可以在新的Ogg Vorbis文件開始時處理不同的格式,但是我們不能處理文件回放過程中的格式更改。 除了到達文件結尾或者錯誤之外,這是庫停止播放的另一個原因。

// Copy the new PCM data into PCM memory streamPcmStream.SetLength(0);
PcmStream.Write(PcmBuffer, 0, PcmBytes);
PcmStream.Position = 0;
PcmStreamNextConsumPcmPosition = 0;// Initial load of secondary bufferif (SecBufInitialLoad)
{
 int WriteCount = (int)Math.Min(
 PcmStream.Length,
 SecBufByteSize - SecBufNextWritePosition);
 if (WriteCount >0)
 {
 SecBuf.Write(
 SecBufNextWritePosition,
 PcmStream,
 WriteCount,
 LockFlag.None);
 SecBufNextWritePosition += WriteCount;
 PcmStreamNextConsumPcmPosition += WriteCount;
 }
 if (SecBufByteSize == SecBufNextWritePosition)
 {
 // Done filling the buffer SecBufInitialLoad = false;
 SecBufNextWritePosition = 0;
 // So start the playback// NOTE: Play does the playing in its own thread SecBuf.Play(0, BufferPlayFlags.Looping);
 Thread.Sleep(0);
 //yield rest of timeslice//so playback can start right away }
 else {
 continue; // Get more PCM data }
}

Points of Interest

這些都是示例。TgPlayOgg和TgPlayOgg_vorbisfile項目的亮點。 如果你想了解如何解碼from音頻文件或者如何調用托管. NET 環境中的非托管代碼,這些項目很有趣。 如果你對在TGSDK網路遊戲中進行多玩家在線遊戲感興趣,你可以在 TrayGames網站上找到。 你還可以查看的網站,了解更多關於它的編碼格式的信息,以及許多用於處理它的工具。

修訂歷史

  • 02 2007年04月

    更新了這裡庫,支持 Visual Studio. NET 2005並進行了幾個 Bug 修復。 更新后的庫也可以在TGSDK中使用,可以從TrayGames開發者網站下載。

  • 07 2006年03月

    為了支持. NET 2.0,更新了這個庫,添加了一個 WaitForAllOggFiles 方法,直到所有優秀的Ogg文件都完成播放,並進行了。

  • 22 2005年08月

    在 Vorbisfile API函數中添加了更多關於這個庫調用解碼 Ogg Vorbis聲音文件的細節。

  • 11 2005年08月

    修正了源代碼中一些小缺陷,更新了測試應用並修正了一些語法錯誤。

  • 18 2005年07月

    基於一些歡迎的反饋,我更新了本文的from包裝部分,討論了. NET 框架庫 DllImportAttribute 類。



文章标签:IMP  Implementation  play  播放器  OGG  Vorbis  

Copyright © 2011 HelpLib All rights reserved.    知识分享协议 京ICP备05059198号-3  |  如果智培  |  酷兔英语