65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.64/5 (14投票s)

2005年9月21日

CDDL

9分钟阅读

viewsIcon

83853

downloadIcon

741

一篇关于从内存加载 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 日

    初始版本。

© . All rights reserved.