帮酷LOGO
  • 显示原文与译文双语对照的内容
文章标签:播放器  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  |  如果智培  |  酷兔英语