在 .NET 中从内存加载 Ogg Vorbis 文件






4.64/5 (14投票s)
一篇关于从内存加载 Ogg Vorbis 音频数据的文章。
引言
本文介绍如何从内存流加载 Ogg Vorbis 文件,然后将该内存流解码为托管环境中使用。本文要求您阅读我上一篇描述如何在 .NET 中实现 Ogg Vorbis 播放器的文章,因为我将向该文章中使用的库添加代码,并且我假设您了解这些库的基本架构。此版本的 TgOggPlay 库包含了原始版本的所有功能。
背景
在 TrayGames,我们需要为提供给第三方开发者的多人在线游戏开发 SDK (TGSDK) 添加对播放 Ogg Vorbis 文件的支持。我们开发了一个库,允许您在托管的 .NET 环境中播放这些文件,但该库需要您要解码的文件的文件名。一些开发者要求该库也能通过接受包含编码的 Ogg Vorbis 数据的一块内存而不是文件名来工作。当您没有单独的音频文件,而是拥有一个包含许多嵌入式音频文件的单个资源文件时,这很有用。TrayGames 为其皮肤库创建的资源 PAK 文件就是这种情况。
使用代码
本文使用的代码库与我们在第一篇文章中使用的基本相同,但添加了一些代码来支持新的数据加载方式。TgPlayOgg 项目中添加了一个接受 `byte` 数组而不是文件名字符串作为参数的方法,但该项目中的其他类基本保持不变。在非托管的 TgPlayOgg_vorbisfile 包装器项目中添加了一个新的加载函数,但其他函数保持不变。在非托管包装器项目中添加了一个包含 Ogg Vorbis API 解码内存中 Ogg Vorbis 数据所需的调用函数的 C++ 文件,这些都是新的。
对测试应用程序进行了一些更改以演示新功能。“播放 Ogg 文件...”按钮现在调用使用内存流的新方法。测试应用程序中使用的方法将文件加载到内存中以进行演示,并保持测试应用程序的简单性。通常,包含数据的内存流将来自从 PAK 文件或类似文件提取的数据。“重复最后一个 Ogg”按钮使用文件名字符串来执行原始的 Ogg Vorbis 文件播放方式。总体而言,跨所有项目进行了一些其他更改,以清理源代码,提供更好的注释,并修复了自原始文章发布以来发现的一些错误。
如果您下载源代码,在“OggPlayer Sample”文件夹下有一个“OggPlayer.sln”解决方案文件,它将构建本文中提到的所有项目。在 TgOggOrg 项目下的“Test App”文件夹中提供了一个示例测试应用程序。该测试应用程序与我的第一篇文章中提供的应用程序相同,只有一些小的更改。让我们看一下对这个测试应用程序的更改。我们更改了 `Button1Click` 方法(处理“播放 Ogg 文件...”按钮)以将所选 Ogg Vorbis 文件的内容读取到扁平的 `byte` 数组中。然后,我们将 `byte` 数组传递给 TgPlayOgg 对象 `oplay` 中的一个新方法以开始播放文件。新方法如下:
private void Button1Click(object sender, System.EventArgs e)
{
OggName = GetOggFileNameToOpen();
if (null != OggName)
{
// Demonstrate using the library with a memory stream
using (FileStream fs = new FileStream(OggName,
FileMode.Open, FileAccess.Read))
{
byte[] OggData = new byte[fs.Length];
BinaryReader br = new BinaryReader(fs);
br.Read(OggData, 0, (int)fs.Length);
br.Close();
oplay.PlayOggFile(OggData, ++PlayId);
}
textBox1.Text +=
"Playing '" + OggName + "' Id= " + PlayId.ToString() + "\r\n";
StillPlaying++;
}
}
我们基本上仍然是从内存加载 Ogg Vorbis 文件(至少开始是这样),但这只是为了使演示测试应用程序保持简单。同样,通常您的包含数据的内存流将来自从 PAK 文件或类似文件提取的数据。
Ogg Vorbis 包装器
Ogg Vorbis API 不包含从内存加载文件的函数;相反,它们允许您提供四个回调函数,它们将使用这些函数来加载文件。这样做的优点是我们的 Ogg Vorbis 音频数据可以存储在任何地方,缺点是我们必须做一些工作。在我上一篇文章中,TgPlayOgg_vorbisfile 包装器项目提供了一个 `init_for_ogg_decode` 方法。此方法使用 Ogg Vorbis `ov_info` API 函数打开并初始化给定的 Ogg Vorbis 文件以进行解码。该 API 函数接受文件指针作为参数。由于我们要从内存加载数据而不是从文件加载,因此我们不能再使用 `ov_info` 函数。相反,我们现在将填写一个 `ov_callbacks` 结构并将其作为参数提供给 `ov_open_callbacks` API 函数。
为了填写 `ov_callbacks` 结构,我们需要创建四个新函数,它们将作为回调函数来读取、查找、定位和关闭内存中的数据。这些函数应以与标准 C 运行时 IO 函数相同的方式工作,具有相同的参数和相同的返回值。我们所有四个函数都将使用以下自定义结构,该结构包含指向我们内存中的 Ogg Vorbis 文件的指针以及一些额外的信息:
typedef struct _OggMemoryFile { unsigned char* dataPtr;// Pointer to the data in memory long dataSize; // Size of the data long dataRead; // Bytes read so far } OGG_MEMORY_FILE, *POGG_MEMORY_FILE;
我们的 `vorbis_read` 函数从输入流中读取最多 `sizeToRead` 个大小为 `byteSize` 的项,并将它们存储在输出缓冲区 `data_src` 中。它返回实际读取的完整项的数量,如果发生错误或在达到请求的项数之前遇到文件末尾,则该数量可能少于 `sizeToRead`。返回值为零表示我们已到达文件末尾,无法再读取更多数据。返回值为 -1 表示发生错误。
size_t vorbis_read(void* data_ptr,// A pointer to the data // that the vorbis files need size_t byteSize, // Byte size on this particular system size_t sizeToRead, // Maximum number of items to be read void* data_src) // A pointer to the data we passed into // ov_open_callbacks { POGG_MEMORY_FILE vorbisData = static_cast(data_src); if (NULL == vorbisData) return -1; // Calculate how much we need to read. // This can be sizeToRead*byteSize // or less depending on how near the // EOF marker we are. size_t actualSizeToRead, spaceToEOF = vorbisData->dataSize - vorbisData->dataRead; if ((sizeToRead*byteSize) < spaceToEOF) actualSizeToRead = (sizeToRead*byteSize); else actualSizeToRead = spaceToEOF; // A copy of the data from memory // to the datastruct that the // Vorbisfile API will use. if (actualSizeToRead) { // Copy the data from the start // of the file PLUS how much // we have already read in. memcpy(data_ptr, (char*)vorbisData->dataPtr + vorbisData->dataRead, actualSizeToRead); // Increase by how much we have read by vorbisData->dataRead += actualSizeToRead; } return actualSizeToRead; }
我们的 `vorbis_seek` 函数将与流关联的指针移动到距离 `origin`(`SEEK_SET`、`SEEK_CUR`、`SEEK_END`)`offset` 字节的新位置。您可以使用此函数重新定位流中的数据指针。`origin` 参数是定位的起点,数据指针将相应地移动,确保不超出数据边界。返回 -1 表示该文件不可查找,而返回 0 表示成功。
int vorbis_seek(void* data_src, // A pointer to the data we // passed to ov_open_callbacks ogg_int64_t offset, // Number of bytes from origin int origin) // Initial position { POGG_MEMORY_FILE vorbisData = static_cast(data_src); if (NULL == vorbisData) return -1; switch (origin) { case SEEK_SET: { // Seek to the start of the data // file, make sure we are not // going to the end of the file. ogg_int64_t actualOffset; if (vorbisData->dataSize >= offset) actualOffset = offset; else actualOffset = vorbisData->dataSize; // Set where we now are vorbisData->dataRead = static_cast(actualOffset); break; } case SEEK_CUR: { // Seek from where we are, make // sure we don't go past the end size_t spaceToEOF = vorbisData->dataSize - vorbisData->dataRead; ogg_int64_t actualOffset; if (offset < spaceToEOF) actualOffset = offset; else actualOffset = spaceToEOF; // Seek from our currrent location vorbisData->dataRead += static_cast<LONG>(actualOffset); break; } case SEEK_END: // Seek from the end of the file vorbisData->dataRead = vorbisData->dataSize+1; break; default: _ASSERT(false && "The 'origin' argument must be " + "one of the following constants, defined in STDIO.H!\n"); break; }; return 0; }
我们的 `vorbis_close` 函数关闭流并释放我们为 Ogg Vorbis 数据以及保存数据的结构分配的内存。此函数非常有用,因为它允许我们为 Ogg Vorbis 数据分配内存,将结构交给 `ov_open_callbacks` API 函数,然后不再担心它。此函数被调用的事实为我们提供了一个机会来释放该内存,而无需保留原始结构的副本。
int vorbis_close(void* data_src) { // Free the memory that we // created for the stream. POGG_MEMORY_FILE oggStream = static_cast(data_src); if (NULL != oggStream) { if (NULL != oggStream->dataPtr) { delete[] oggStream->dataPtr; oggStream->dataPtr = NULL; } delete oggStream; return 0; } _ASSERT(false && "The 'data_src' argument (set by " + "ov_open_callbacks) was NULL so memory was not cleaned up!\n"); return EOF; }
我们的 `vorbis_tell` 函数获取与流关联的指针的当前位置。该位置表示为相对于流开头的偏移量。
long vorbis_tell(void* data_src) { POGG_MEMORY_FILE vorbisData = static_cast(data_src); if (NULL == vorbisData) return -1L; // We just want to tell the Vorbisfile // API how much we have read so far return vorbisData->dataRead; }
一旦我们编写了回调函数,我们就可以从内存中打开文件了。我在原始包装器项目 `memory_stream_for_ogg_decode` 中添加了一个新方法,它完成了所有必要的工作。这是我们的 .NET 库将调用的方法,而不是 `init_for_ogg_decode` 方法。新方法为 `OggVorbis_File` 结构(这是 API 调用的输出缓冲区)分配内存。接下来,它将给定内存流中的数据保存在 `OGG_MEMORY_FILE` 结构中。一旦我们将文件加载到内存中,我们就需要让 Vorbis 库知道如何读取它。为此,我们提供启用读取的回调函数。最后,我们需要将指向我们数据的指针(`OGG_MEMORY_FILE` 结构)、指向我们的 `OggVorbis_File` 输出缓冲区(Vorbis 库将填充)的指针以及我们的回调结构传递给 `ov_open_callbacks`。
int memory_stream_for_ogg_decode(unsigned char* stream, int sizeOfStream, void** vf_out) { void *vf_ptr = malloc(sizeof(OggVorbis_File)); if (NULL == vf_ptr) return ifod_err_malloc_failed; POGG_MEMORY_FILE oggStream = new OGG_MEMORY_FILE; oggStream->dataRead = 0; oggStream->dataSize = sizeOfStream; oggStream->dataPtr = new unsigned char[sizeOfStream]; for (int i=0; i < sizeOfStream; i++, stream++) oggStream->dataPtr[i] = *stream; oggCallbacks.read_func = vorbis_read; oggCallbacks.close_func = vorbis_close; oggCallbacks.seek_func = vorbis_seek; oggCallbacks.tell_func = vorbis_tell; int ov_ret = ov_open_callbacks(oggStream, static_cast<OGGVORBIS_FILE *>(vf_ptr), NULL, 0, oggCallbacks); if (0 > ov_ret) { // There was an error . . . } // Copy the memory pointer to the caller *vf_out = vf_ptr; return 0; // Success! }
请注意,`oggCallbacks` 是一个类型为 `ov_callbacks` 的全局变量。这些是我们为从内存加载 Ogg Vorbis 文件而对包装器项目进行的所有更改。所有其他对 Ogg Vorbis API 的调用都与以前完全相同。我们快完成了;我们只需要对我们的 .NET 库进行一些调整。
.NET Ogg Vorbis 库
原始 TgPlayOgg 库提供了一个 `OggPlay` 类,其中有一个 `PlayOggFile` 方法。此方法播放 `fileName` 参数指定的 Ogg Vorbis 文件。`playId` 参数是用户确定的任意值,它会在引发的 `PlayOggFileResult` 事件中返回。我们重载了此方法以接受一个 `data` 参数。此参数是包含您要解码的 Ogg Vorbis 音频数据的扁平 `byte` 数组。除此之外,该方法与其他重载版本相同。
public void PlayOggFile(byte[] data, int playId)
{
// Create an event argument class
// identified by the playId
PlayOggFileEventArgs EventArgs =
new PlayOggFileEventArgs(playId);
// Decode the Ogg Vorbis memory
// stream in a separate thread
PlayOggFileThreadInfo pofInfo = new PlayOggFileThreadInfo(
EventArgs, null, data,
OggFileSampleSize == OggSampleSize.EightBits ? 8 : 16,
DirectSoundDevice, this);
Thread PlaybackThread = new Thread(
new ThreadStart(pofInfo.PlayOggFileThreadProc));
PlaybackThread.Start();
}
`OggPlay` 类包含 `PlayOggFileThreadInfo` 类,该类用作 `OggPlay` 类 `PlayOggFile` 方法中创建的播放线程的线程类。我们需要对此类进行一个小更改,以便 `PlayOggFileThreadProc` 方法可以确定是调用 `init_for_ogg_decode` 包装器方法(如果我们使用的是文件),还是调用新的 `memory_stream_for_ogg_decode` 方法(如果我们使用的是内存流)。方法的其余部分保持不变。我们还需要添加一个新的数据成员 `byte[] memFile`,以及一个新参数到类构造函数中以设置此成员。
public void PlayOggFileThreadProc()
{
// . . .
// Initialize the file for Ogg Vorbis decoding using
// data from either a file name or memory stream.
int ErrorCode = 0;
if (null != FileName)
ErrorCode =
NativeMethods.init_for_ogg_decode(FileName, &vf);
else if (null != MemFile)
ErrorCode =
NativeMethods.memory_stream_for_ogg_decode(
MemFile, MemFile.Length, &vf);
// . . .
}
最后,我们必须将新方法的原型添加到我们非托管包装器项目中的 `NativeMethods` 类中。
// Initialization for decoding the // given Ogg Vorbis memory stream. [DllImport("TgPlayOgg_vorbisfile.dll", CharSet=CharSet.Unicode, CallingConvention=CallingConvention.Cdecl)] public unsafe static extern int memory_stream_for_ogg_decode( byte[] stream, int sizeOfStream, void** vf_out);
这些是我们为从内存加载 Ogg Vorbis 音频文件而对 .NET 库项目进行的所有更改。该项目中的所有其他方法都与以前完全相同。
关注点
这些基本上是关于从内存流或其他任何您想要的地方播放 Ogg Vorbis 编码音频数据的要点。回调可以从任何地方加载文件,而不仅仅是内存,这为开发人员提供了对音频样本的很大控制和灵活性。如果您想了解加载 Ogg Vorbis 音频文件的不同方式,这些项目很有趣,或者可以作为创建简单效果而不更改原始音频样本源的起点。如果您有兴趣了解完整的 TGSDK 以制作自己的多人在线游戏,您可以从 TrayGames 网站上获取它。您可能还想访问 Ogg Vorbis 网站以了解有关其编码格式的更多信息。
修订历史
- 2006 年 3 月 7 日
更新了此库以支持 .NET 2.0,添加了一个 `WaitForAllOggFiles` 方法,该方法将阻塞直到所有未完成的 Ogg 文件播放完毕,并进行了多项错误修复。更新后的库仅在 TGSDK 中可用,可从 TrayGames 网站下载。
- 2005 年 9 月 21 日
初始版本。