Ogg Vorbis 播放器 .NET 实现






4.80/5 (28投票s)
一篇关于在 .NET 中解码 Ogg Vorbis 音频文件的文章。
引言
TgPlayOgg 项目是一个 .NET C# 库,允许您在托管代码中播放 Ogg Vorbis 文件。TgPlayOgg 通过调用非托管 C++ 项目 TGPlayOgg_vorbisfile 来解码给定的 Ogg Vorbis 文件到可用的声音数据。TgPlayOgg 还需要托管 DirectX 来输出声音。
背景
在 TrayGames,我们需要为提供给第三方开发者的多人在线游戏开发 SDK (TGSDK) 添加播放声音文件的支持。我们最初使用的是 MP3 音频格式,但我们担心许可问题(费用会在达到一定销售水平后开始收取)。在比较了替代方案后,我们选择了 Ogg Vorbis 格式。Ogg Vorbis 是一种完全开放、无专利、专业的音频编码和流媒体技术,具有开源的所有优势。
Using the Code
如果您下载了源代码,在“OggPlayer Sample”文件夹下有一个“OggPlayer.sln”解决方案文件,可以构建本文中提到的所有项目。在 TgPlayOgg 项目下的“Test App”文件夹中提供了一个示例测试应用程序。该应用程序演示了如何使用该库。步骤如下:
- 包含对 TgPlayOgg 项目的引用,并导入
TG.Sound
命名空间。 - 构造一个
OggPlay
类的实例(无论同时播放多少个 Ogg Vorbis 文件,您的应用程序只需要一个实例)。 - 将您的
PlayOggFile
事件处理程序添加到PlayOggFileResult
委托。现在您可以根据需要随时调用PlayOggFile
。请注意,PlayOggFile
在调用后立即返回,因为解码和播放是在单独的线程中进行的。文件播放完毕后会调用您的事件处理程序。 - 当您不再使用
OggPlay
类实例时,您应该调用Dispose
来确保 DirectSound 设备对象使用的非托管资源得到清理。
让我们看看测试应用程序的亮点。首先,我们看到它有一个执行初始化并处理 PlayOggFile
事件的方法。请注意,我们必须在 try
块中调用 OggPlay
构造函数,因为它会调用 DirectSound 并可能引发异常。然后,它有另一个方法允许用户选择一个 Ogg Vorbis 音频文件进行播放。
using TG.Sound;
private void InitTestOfOggPlayer()
{
try
{
oplay = new OggPlay(this, OggSampleSize.SixteenBits);
oplay.PlayOggFileResult += new PlayOggFileEventHandler(PlayOggFileResult);
textBox1.Text = "Initialization successful.\r\n";
}
catch(Exception e)
{
textBox1.Text = "Initialization failed: " + e.Message + "\r\n";
}
}
private void Button1Click(object sender, System.EventArgs e)
{
OggName = GetOggFileNameToOpen();
if (OggName != null)
{
oplay.PlayOggFile(OggName, ++PlayId);
textBox1.Text = "Playing " + OggName + " Id= " + PlayId.ToString() + "\r\n";
}
}
Ogg Vorbis 解码器在解码 Ogg Vorbis 数据时可能遇到错误。如果数据不足以流式传输到初始缓冲区(Ogg 太小)或者无法读取文件,Ogg Vorbis 文件可能无法播放。有两个错误计数仅供参考,因为如果成功,创建的波形数据已被播放,但如果这两个计数中的任何一个非零,声音可能不像预期那样。我们处理 PlayOggFile
事件的方式是显示一个状态消息,指示成功或错误(带有两个错误计数)。我们稍后将详细了解这些错误计数。
private static void PlayOggFileResult(object sender, PlayOggFileEventArgs e)
{
if (e.Success)
{
MainForm.textBox1.Text += "PlayOggFile(" + e.PlayId + ") succeeded ("
+ "ErrorHoleCount: " + e.ErrorHoleCount + ", ErrorBadLinkCount: "
+ e.ErrorBadLinkCount + ").\r\n";
}
else
{
MainForm.textBox1.Text += "PlayOggFile(" + e.PlayId + ") failed: '"
+ e.ReasonForFailure + "'\r\n";
}
PlayId--;
}
请注意,如果一个或多个 Ogg Vorbis 文件仍在播放,退出调用应用程序不会终止播放线程。这些播放线程将继续运行,尽管您再也听不到它们播放。线程将完成播放它们正在播放的 Ogg Vorbis 文件然后退出,除非线程被明确指示停止播放。因此,当您的应用程序退出时,它应该停止所有仍在播放的长 Ogg Vorbis 文件。这就是为什么测试应用程序会在 Form.Closing
事件中调用 OggPlay.StopOggFile
,我们稍后会详细介绍。
protected void Form1_Closing(object sender,
System.ComponentModel.CancelEventArgs e)
{
// Determine if any Ogg files are still playing by checking the PlayId member
if (PlayId > 0)
{
// Display a MsgBox asking the user to save changes or abort
if (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 playbacks
while (PlayId > 0)
oplay.StopOggFile(PlayId--);
}
}
}
其他您可能想要中断播放线程的情况是,如果您的应用程序具有暂停功能,或者当用户切换离开您的游戏时您想重置声音。
Ogg Vorbis 包装器
Ogg Vorbis 的高级 API Vorbisfile 只有两个输入选项:要么是 C 文件指针,要么是一组自定义回调函数,用于读取输入的 Ogg Vorbis 数据。这些选项中更好且更可移植的可能是自定义回调,但我不知道 .NET 1.1 是否对它的方法的调用约定有任何控制,并且它的标准调用约定是 StdCall
,而 Vorbisfile 动态链接库(DLL)是用 Cdecl
调用约定编译的。因此,考虑到 C# 和 .NET 1.1,我们决定编写一些 C/C++ 代码并将其编译成一个 DLL,这个 DLL 包含了 Vorbisfile 所需的回调。这就是我们创建 TGPlayOgg_vorbis 包装器项目的原因。
我后来了解到,您可以使用 DllImportAttribute
类来提供调用从非托管 DLL 导出的函数所需的信息。因此,您应该能够修改此库的源代码,以消除 TGPlayOgg_vorbis 包装器项目,并直接从 TGPlayOgg 项目调用 Vorbisfile API。 .NET Framework 基本类库 (BCL) 提供了 StdCall
、Cdecl
、ThisCall
和 WinApi
调用约定。WinApi
会根据平台(Windows 或 Windows CE)自动选择正确的类型。例如,要更改位于 SomeLibrary.dll 中的 SomeFunction
,您可以使用以下代码:
[DllImport("SomeLibrary.DLL", EntryPoint="SomeFunction", SetLastError=true,
CharSet=CharSet.Unicode, ExactSpelling=true,
CallingConvention=CallingConvention.Cdecl)]
public static extern bool SomeFunction(String param1, String param2);
但目前,TGPlayOgg_vorbis 项目为我们调用 Ogg Vorbis API。有三个包装函数:init_file_for_ogg_decode
、ogg_decode_one_vorbis_packet
和 ogg_final_cleanup
。您永远不会直接调用这些方法,C# 库会调用它们以便解码文件。如果您想将这些方法的定义添加到您自己的托管应用程序中,您可以定义一个 NativeMethods
类(使用任何类名),并将函数原型添加到其中。为非托管 DLL 函数设置一个单独的类是明智的,因为使用 DLL 函数容易出错。封装 DLL 声明会在调试时让您的工作更轻松。“Vorbisapi.cs”文件在 TGPlayOgg_vorbis 项目中已经有一个包含声明的类定义。
// External C functions in the TgPlayOgg_vorbisfile unmanaged DLL
[DllImport("TgPlayOgg_vorbisfile.dll", CharSet=CharSet.Unicode,
CallingConvention=CallingConvention.Cdecl)]
public unsafe static extern int init_for_ogg_decode(
string fileName, void **vf_out);
[DllImport("TgPlayOgg_vorbisfile.dll", CallingConvention=CallingConvention.Cdecl)]
public unsafe static extern int 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)]
public unsafe static extern int final_ogg_cleanup(void *vf_ptr);
允许我们进行这些调用的服务是平台调用 (PInvoke) 服务。PInvoke 将使我们的托管代码能够调用 DLL 中实现的非托管函数。它将定位并调用导出的函数,并在需要时将参数跨托管/非托管代码边界进行封送。请注意,PInvoke 会将非托管函数生成的异常抛给托管调用者。现在让我们看看我们的非托管函数。
init_file_for_ogg_decode
函数将打开并初始化给定的 Ogg Vorbis 文件以进行解码。它通过调用 op_open
API 函数来设置所有相关的解码结构。另外,您应该知道,ov_open
成功后,将完全接管文件资源。使用 ov_open
打开文件后,您必须使用 ov_clear
来关闭它,而不是 fclose
或任何其他函数。我们的包装函数负责所有这些,这是我们的初始化函数的样子:
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_ code return err_code; } // Copy the memory pointer to the caller *vf_out = vf_ptr; return 0; // success }
ogg_decode_one_vorbis_packet
函数会将 PCM(脉冲编码调制)数据写入给定的缓冲区,并返回写入该缓冲区的字节数。首先,它调用 ov_read
,它会根据指定的字节数、所需的字节序、符号和字大小返回解码后的 PCM 音频。如果音频是多通道的,则通道会交错在输出缓冲区中。此函数用于在循环中解码 Vorbis 文件。我们稍后将看到的 C# 应用程序将执行此操作。
接下来,它调用 ov_info
,它会返回指定比特流的 vorbis_info
结构。这使我们能够将比特流的通道数和采样率返回到我们的 C# 应用程序。可能会发生两种错误:OV_HOLE
,表示数据中断;OV_EBADLINK
,表示提供了无效的流段,或者请求的链接已损坏。
Ogg Vorbis 格式允许将多个逻辑比特流(有约束地)组合到单个物理比特流中。请注意,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; } else if (ov_ret < 0) { // An error occurred, bad ogg data of some kind if (ov_ret == OV_HOLE) ++(*err_ov_hole_cnt); else if (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 库
Microsoft .NET 1.1 框架没有播放声音的类,因此要播放从解码的 Ogg Vorbis 文件数据构建的波形数据,基本上有两种选择。第一个是将波形数据写出为 WAV 文件,然后使用 quartz.dll(在 Win98 及更高版本上)来播放该 WAV 文件。这种选择的缺点是 WAV 文件可能非常大(例如,测试了一个 5.5 MB 的 Ogg Vorbis 文件,结果生成了 67 MB 的 WAV 文件),并且播放要在整个 WAV 文件写出后才能开始(例如,在 1.6 GHz P4 PC 上解码那个 5.5 MB 的 Ogg Vorbis 文件并写出 WAV 文件需要超过 20 秒)。另一种选择是使用托管 DirectX 中的方法,这意味着无需写出任何 WAV 文件,我们可以在生成波形数据时进行播放,因此播放可以比第一种方法更快开始。TrayGames 客户端已确保目标计算机上安装了托管 DirectX API,所以这也不是问题,这也是我们选择的方案。
OggPlay
类是您的应用程序将使用的主要类。其构造函数创建一个新的 DirectX Sound 设备,设置 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
参数由 DirectSound SetCooperativeLevel
方法使用,该方法将其 owner
参数定义为“使用 Device 对象的应用程序的 System.Windows.Forms.Control
”。这应该是您应用程序的主窗口。wantedOggSampleSize
参数是 8 位或 16 位。8 位采样大小质量较低,但比 16 位采样大小更快且占用内存更少。如果您的应用程序的 Ogg Vorbis 文件是用 8 位采样大小编码的,则选择 8(您也可以选择 16,但这很浪费,并且如果 Ogg Vorbis 源只有 8 位,则毫无益处)。如果您的应用程序的 Ogg Vorbis 文件是用 16 位采样大小编码的,则选择 16 以在播放期间获得完整的音质,或者选择 8,或者为用户提供选择 8 的选项,如果您想最大限度地减少播放资源需求。如果您的应用程序的 Ogg Vorbis 文件是混合的(有些是用 8 位采样大小编码的,有些是用 16 位采样大小编码的),则选择您认为最好的(8 位或 16 位都可以播放所有 Ogg Vorbis 文件)。
TgPlayOgg 库声明了两个事件(带有委托)和一个事件参数类(定义了两个事件的数据),用于播放和停止 Ogg Vorbis 文件。PlayOggFileResult
事件(PlayOggFileEventHandler
委托)用于在 PlayOggFile
方法完成时进行事件通知,而 StopOggFileNow
事件(StopOggFileEventHandler
委托)用于在客户端希望提前中断播放时使用。这是事件参数类数据成员的概览:
public sealed class PlayOggFileEventArgs : EventArgs
{
private bool success;
// If !Success then this is the explanation for the failure
private string reasonForFailure;
// The value of the playID parameter when PlayOggFile() was called
private int playId;
public int 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
提供了两个简单的 PlayOggFile
和 StopOggFile
方法。PlayOggFile
播放由 fileName
参数指定的 Ogg Vorbis 文件。playId
参数是由用户决定的任意值,它会在引发的 PlayOggFileResult
事件中返回。此事件由 PlayOggFileThreadProc
引发。在您的事件处理程序代码中,您可以使用返回的 playID
来知道是哪个特定的 PlayOggFile
调用导致了该处理的事件。这就是为什么您的应用程序应该附加到 PlayOggFileEventHandler
委托的原因。
public void 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
当然需要。
public void 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
方法注册为处理 StopOggFileNow
。
int ErrorCode = NativeMethods.init_file_for_ogg_decode(FileName, &vf);
if (ErrorCode != 0)
{
// . . .
oplay.PlayOggFileResult(this, EventArgs);
return;
}
接下来 PlayOggFileThreadProc
创建 PCM 字节数组并将其传递给 ogg_decode_one_vorbis_packet
函数。此函数将返回解码后的 Ogg Vorbis 数据的第一个块及其大小。
// Get next chunk of PCM data, pin these so GC can't relocate them
fixed(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 WaveFormat
、BufferDescription
、SecondaryBuffer
和 Notify
对象。WaveFormat
用于保存解码后波形音频数据的格式。BufferDescription
将描述新缓冲区对象的特性,包括 WaveFormat
。SecondaryBuffer
具有用于管理声音缓冲区的属性和方法。Notify
允许我们在播放过程中的不同点设置通知触发器。
int HoldThisManySamples =
(int)(SamplingRate * SecBufHoldThisManySeconds);
// Set the format
MyWaveFormat.AverageBytesPerSecond = AverageBytesPerSecond;
MyWaveFormat.BitsPerSample = (short)BitsPerSample;
MyWaveFormat.BlockAlign = (short)BlockAlign;
MyWaveFormat.Channels = (short)ChannelsCount;
MyWaveFormat.SamplesPerSecond = SamplingRate;
MyWaveFormat.FormatTag = WaveFormatTag.Pcm;
// Set BufferDescription
MyDescription = new BufferDescription();
MyDescription.Format = MyWaveFormat;
MyDescription.BufferBytes =
SecBufByteSize = HoldThisManySamples * BlockAlign;
MyDescription.CanGetCurrentPosition = true;
MyDescription.ControlPositionNotify = true;
// Create the buffer
SecBuf = new SecondaryBuffer(MyDescription, DirectSoundDevice);
// Set 3 notification points, at 0, 1/3, and 2/3 SecBuf size
MyNotify = 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 stream
PcmStream.SetLength(0);
PcmStream.Write(PcmBuffer, 0, PcmBytes);
PcmStream.Position = 0;
PcmStreamNextConsumPcmPosition = 0;
// Initial load of secondary buffer
if (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
}
}
关注点
以上基本上就是 TgPlayOgg 和 TgPlayOgg_vorbisfile 项目的亮点。如果您想了解解码 Ogg Vorbis 音频文件的知识,或者作为如何从托管 .NET 环境调用非托管代码的示例,这些项目会很有趣。如果您有兴趣查看完整的 TGSDK 以制作您自己的多人在线游戏,您可以在 TrayGames 网站上获取。您可能还想查看 Ogg Vorbis 网站,以了解更多关于他们的编码格式以及处理它的许多工具。
修订历史
- 2007 年 4 月 2 日
更新了此库以支持 Visual Studio .NET 2005,并进行了多项 bug 修复。更新后的库也包含在 TGSDK 中,可从 TrayGames 开发者网站下载。
- 2006 年 3 月 7 日
更新了此库以支持 .NET 2.0,添加了一个
WaitForAllOggFiles
方法,该方法会阻塞直到所有待处理的 Ogg 文件播放完毕,并进行了多项 bug 修复。 - 2005 年 8 月 22 日
添加了更多关于此库调用的 Vorbisfile API 函数的详细信息,以解码 Ogg Vorbis 音频文件。
- 2005 年 8 月 11 日
修复了源代码中的一些小缺陷,更新了测试应用程序,并修正了文章中的一些语法错误。
- 2005 年 7 月 18 日
初始版本。根据一些受欢迎的反馈,我已更新了本文的 Ogg Vorbis 包装器部分,以讨论 .NET Framework 基本类库
DllImportAttribute
类,该类可用于调用从非托管 DLL 导出的函数。