【3D技术宅公社】XR数字艺术论坛  XR技术讨论 XR互动电影 定格动画

 找回密码
 立即注册

QQ登录

只需一步,快速开始

调查问卷
论坛即将给大家带来全新的技术服务,面向三围图形学、游戏、动画的全新服务论坛升级为UTF8版本后,中文用户名和用户密码中有中文的都无法登陆,请发邮件到324007255(at)QQ.com联系手动修改密码

3D技术论坛将以计算机图形学为核心,面向教育 推出国内的三维教育引擎该项目在持续研发当中,感谢大家的关注。

查看: 3346|回复: 7

游戏中的声音资源打包

[复制链接]
发表于 2009-8-26 11:04:31 | 显示全部楼层 |阅读模式
注:2009/6/11已经增加了更新,更新的内容主要是Bug修复和扩展,通过文件映射的方法来支持流媒体。Demo地址:http://ishare.iask.sina.com.cn/f/5263833.html
补充的内容在另外一个帖子里。

介绍
         在以前的游戏当中,几乎所有的游戏都有将资源打包,无论是图形资源、声音文件等等。现在,越来越多的游戏的声音资源都公开了,通常背景音乐等采用ogg,音效采用wav;但是图形资源绝大部分游戏还是都是打包了的。有的有加密,有的没有。
         在学习游戏编程的初期我就对这个东西很感兴趣,但是又没有人教,到网上找资料也少得可怜,所以就只好自己摸索了。记得去年也做过类似的工作,但是当时做的感觉很不好,采取了一些投机取巧的方法,虽然看起来是打包了,但是实际上只是一个躯壳而已。
         随着时间的推移,自己懂得比以前稍微多了一点点,就想做点什么,即使现在还是“菜青虫”水平,“菜鸟”都算不上,呵呵。马上要毕业了,论文也早Over了,所以时间挺多的,因此写点这样的东西,希望现在的“曾经的我”能够从这个文章里边得到一点灵感。
         这篇文章将介绍一种最简单的声音文件打包并播放的方法。最简单意味着它没有加密没有考虑太多的特殊情况,只实现了最简单的wav打包和播放,流媒体没考虑在内,因为它们的处理毕竟还是有很大的不同的。

注意
         本文中所提到的程序以及附件都使用Visual Studio 2008 编写,项目文件也只能用VS2008打开。另外依赖于boost.filesystem、boost.shared_ptr、boost.unoredered_set,说白了如果您想查看效果的话必须安装boost,boost的安装教程可以参考我写得这个:http://ishare.iask.sina.com.cn/f/4875657.html。另外声音播放我使用的Fmod引擎,所以您需要到http://www.fmod.org/去下载最新的fmod引擎sdk安装并配置好VS。我相信VS2005和VS2008在配置正确的情况下应该都能编译通过的。

文件打包
         文件打包有很多方式的,我采取了一种比较简单的方式,我将打包后的结果分为数据文件和索引文件两部分,这样的好处是什么呢?至少我们追加文件的时候很方便也很快。加入我们采用文件头的形式,即索引在数据文件的头部,那么当我们要添加的新文件的时候,则意味着所有文件的重写,因为操作系统不会提供一个执行“插入”操作的API。这样最直接的好处就是游戏工具执行“保存”动作的时候的快速性。
         另外,文件打包还可以选择是否加密,是否压缩等等。加密这块我不是很了解,我去年做过一个加了密的,但是后来反思的时候发现并没有什么意义,与其说文件打包是为了保护资源的话,还不如说是为了管理的方便和效率。因为我觉得在一个电脑高手面前,加不加密并没有什么意义,比如说传奇式的”mycrack”大大。压缩个人以为还是有点意义的,至少可以少占不少硬盘,当然,这是需要付出代价的,运行时必须解压。Ogre3D中就支持zip打包,应该也是使用zlib来做的,使用zlib来做这个事情是很容易的,呵呵。它的官方网站是:http://www.zlib.net/
因此我们可以有这样的一个类来做这个简单的操作:
namespace db
{
   class CFilePack
   {
   public:
       typedef detail::Node                     node_type;
       typedef detail::CFilePackImp::node_vec   node_vec;
   public:
       CFilePack(
         const std::wstring& strDataFileName,
         const std::wstring& strIndexFileName
         );

       CFilePack(
         const std::string& strDataFileName,
         const std::string& strIndexFileName
         );
      
       ~CFilePack();

   public:
       bool addFile( const std::string& strFileName );
       bool addFile( const std::wstring& strFileName );

       bool load();
       bool save() const;

       const node_vec& getNodes() const
       {
         return m_spImp->getNodes();
       }
   protected:
       boost::shared_ptr<detail::CFilePackImp>     m_spImp;
   };
}
       最终对CFilePack的调用将被转发到detail::CFilePackImp中。提供多种字符串的重载,最终将使用string_shim转换到char调用detail::CfilePackImp的对应的函数:
namespace db
{
   namespace detail
   {
       class Node
       {
       public:
         enum
         {
             SIZE = 20
         };
         char       szName[SIZE];
         unsigned   uOffset;
         unsigned   uSize;
       public:
         bool operator == ( const Node& node ) const
         {
             return _stricmp( szName, node.szName ) == 0;
         }

         bool operator == ( const char* strName ) const
         {
             return _stricmp( szName, strName ) == 0;
         }

         bool operator < ( const Node& node ) const
         {
             return _stricmp( szName, node.szName )<0;
         }
       };

       class CFilePackImp
       {
       public:
         typedef std::string           string_type;
         typedef unsigned               offset_type;
         typedef std::vector<Node>     node_vec;
       public:
        
       public:
         CFilePackImp(
             const string_type& strDataFileName,
             const string_type& strIndexFileName );
         ~CFilePackImp();
       public:
         bool   addFile( const string_type& strFileName );
   //     bool   deleteFile( const string_type& strFileName );
       public:
         bool   load();
         bool   save() const;

         inline const node_vec& getNodes() const
         {
             return m_vecNodes;
         }
       protected:
         string_type       m_strDataFileName;         // 数据文件名
         string_type       m_strIndexFileName;         // 索引文件名
         node_vec         m_vecNodes;

         static const size_t uMaxSaveBytesOnTime = 1024 * 1024;   // 1M
       };
   }
}
         有了这两个类我们就可以这样来生成打包后的数据文件和索引文件,当然下面的Demo代码忽略了错误检测等等:
#include "filepack.hpp"

int main()
{
   // 下面我们使用这个类把三首歌曲打包成一个数据文件和一个索引
   db::CFilePack pack( "music.data", "music.inx" );

   pack.addFile( "drumloop.wav" );
   pack.addFile( "jaguar.wav" );
   pack.addFile( L"swish.wav" );

   // pack.load();

   return 0;
}

         实现细节就不说了,大家可以参考附件。

声音加载与播放
         现在我们要做的就是从索引文件中读取出文件信息,然后去数据文件中找我们感兴趣的数据。在上面的代码中我们可以看到,每一个索引节点都由固定长度的文件名字符串、在数据文件中的偏移量和数据的长度组成,现在在管理中就应该先获取这些信息,由于索引文件通常都很小,所以我们在程序运行期间将这些信息保存在内存中,根据是否有boost分别选择散列表unordered_set或者红黑树实现的set来保存。
         由于在整个系统中通常都只有一个这样的声音文件包,因此我们实现一个简单的单件管理类,这是它的接口类:
#pragma once

#include <string>
#include "ISound.hpp"

namespace db
{
   class ISoundManager
   {
   public:
       typedef std::string                 string_type;      
   public:
       virtual bool         initialize(
                           const string_type& strDataFile,
                           const string_type& strIndexFile
                           ) = 0;
       virtual ISound_SP     createSound( const string_type& strFile ) = 0;
       virtual void         release()=0;
   };
}

         从它我们实现一个Fmod的管理类:
#include "..\\pack_base\config.hpp"
#include "ISoundManager.hpp"
#include "Singleton.hpp"
#include "..\\pack_base\filepack.hpp"

#include <fmod.hpp>

#ifdef __DB_WORK_WITH_BOOST__
#include <boost/unordered/unordered_set.hpp>
#define __SOUND_MANAGER_CONTAINER_IMP__ boost::unordered_set

struct ihash : public std::unary_function< const db::detail::Node&, size_t >
{
   size_t operator()( const db::detail::Node& data ) const
   {
       size_t uSeed = 0;
       size_t uLen = strlen( data.szName );
       for( size_t i=0; i<uLen; ++i )
       {
         boost::hash_combine( uSeed, toupper( data.szName[ i ]) );
       }

       return uSeed;
   }
};

#else

#include <set>
#define __SOUND_MANAGER_CONTAINER_IMP__ std::set

#endif // #ifdef __DB_WORK_WITH_BOOST__


namespace db
{
   class CFmodSoundManager : public ISoundManager, public db::Singleton<CFmodSoundManager>
   {
   public:
       typedef ISoundManager::string_type       string_type;
       typedef db::CFilePack::node_type         node_type;
#ifdef __DB_WORK_WITH_BOOST__
       typedef __SOUND_MANAGER_CONTAINER_IMP__<node_type, ihash>   container_type;
#else
       typedef __SOUND_MANAGER_CONTAINER_IMP__<node_type>   container_type;
#endif


       friend class db::Singleton<CFmodSoundManager>;

   protected:
       CFmodSoundManager();
       virtual ~CFmodSoundManager();
   public:
       bool         initialize(
         const string_type& strDataFile,
         const string_type& strIndexFile
         );
       ISound_SP                       createSound( const string_type& strFile );
       void                           release();

   protected:
       bool       initFmodSystem();
       bool       initData();
       bool       loadtomemory( void* pBuf, size_t uByteCounts, size_t uoffset );
   protected:
       string_type         m_strDataFile;
       string_type         m_strIndexFile;
       container_type       m_nodes;
       FMOD::System*       m_pSystem;
   };
}
 楼主| 发表于 2009-8-26 11:04:39 | 显示全部楼层

         这是一个单件。接下来需要实现一个Sound的接口,因为Sound有普通的wav这样的声音文件,也有像mp3这样的流媒体。因此需要区别对待的,比如说wav的处理思路通常都是直接加载到内存中而mp3等是不会的。我们这里只实现wav的。接口类:
#pragma once

#include <string>
#include <boost/smart_ptr/shared_ptr.hpp>

namespace db
{
   class ISound
   {
   public:
       typedef     std::string     string_type;
   public:
       virtual bool                 load()   = 0;
       virtual void                 unLoad() = 0;
       virtual void                 play()   = 0;
       virtual void                 changePauseState()   = 0;
       virtual const string_type&   getName() const = 0;
       virtual void                 release() = 0;
   };

   typedef boost::shared_ptr<ISound>   ISound_SP;
}

         然后实现我们的内存wav,这和普通的声音有所不同,因为我们需要直接从内存中播放而不是通过一个wav格式的文件的文件名或者路径来构造,因此其实现如下:
#include "ISound.hpp"
#include <fmod.hpp>

#if defined(_MSC_VER )
#pragma comment( lib, "fmodex_vc.lib" )
#endif

namespace db
{
   class CFmodSoundManager;
   class CMemSound : public db::ISound
   {
   public:
       typedef   db::ISound                   base_class;
       typedef   base_class::string_type     string_type;

       friend class CFmodSoundManager;
   public:
       CMemSound( FMOD::System* pSystem, const string_type& strFile );
       virtual ~CMemSound();
   public:
       bool               load();
       void               unLoad();
       void               play();
       void               changePauseState();
       void               release();
       const string_type& getName() const;

   protected:
       bool               create( void* pBuf, unsigned uBytesCount );
   protected:
       string_type         m_strName;
       FMOD::System*       m_pSystem;
       FMOD::Sound*         m_pSound;
       FMOD::Channel*       m_pChannel;
   };
}

         现在再看管理单件的实现,其中需要提的就这两个函数:
bool CFmodSoundManager::loadtomemory( void* pBuf, size_t uByteCounts, size_t uoffset )
{
       FILE* pData = NULL;
       if( 0 != fopen_s( &pData, m_strDataFile.c_str(), "rb") )
       {
         return false;
       }

       fseek( pData, uoffset, SEEK_SET );
      
       fread_s( pBuf, uByteCounts, uByteCounts, 1, pData );

       fclose(pData);

       return true;
   }

   ISound_SP CFmodSoundManager::createSound( const string_type& strFile )
   {
       try
       {
         CFilePack::node_type node;
#ifdef __DB_USE_SAFE_CRT_FUNC__
         strcpy_s( node.szName, strFile.c_str() );
#else
         strcpy( node.szName, strFile.c_str() );
#endif
         container_type::const_iterator pos = m_nodes.find( node );
         if( pos == m_nodes.end() )
         {
             // 没找到哈
             throw std::exception("没这个文件");
         }

         CMemSound* pSound = new CMemSound( m_pSystem, pos->szName );
         char* pBuf = new char[pos->uSize];
        
         if( !this->loadtomemory( pBuf, pos->uSize, pos->uOffset ) )
         {
             delete pSound;
             delete[] pBuf;

             throw std::exception( "加载失败!" );
         }

         pSound->create( pBuf, pos->uSize );
      
         delete[] pBuf;

         return ISound_SP( pSound, boost::mem_fn( &ISound::release ) );   // 这样就算是pSound由DLL创建也不会出错了
       }
       catch( std::exception& /*e*/ )
       {
         return ISound_SP();
       }
   }
         它从散列或者二叉树中查找我们需要的文件的信息,如果找到了则打开数据文件将那一块文件加载到内存中,然后创建内存Sound的智能指针。然后我们就可以使用这个对象进行声音的播放了。下面是主函数的实现:
// Test
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>

#include "FmodManager.hpp"
#include <iostream>
#include <conio.h>
#include <Windows.h>
using namespace std;

using namespace db;

void printDebug()
{
     _CrtDumpMemoryLeaks();   // 它比Manager的析构先调用,因此内存泄露的提示是错误的。
}

int main()
{
   _CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
   atexit( printDebug );

   if( !CFmodSoundManager::GetSingletonPtr()->initialize( "music.data", "music.inx" ) )
   {
       cout<< "系统初始化失败!"<< endl;
       return -1;
   }

   CFmodSoundManager* pManager = CFmodSoundManager::GetSingletonPtr();
   ISound_SP spSound1 = pManager->createSound( "jaguar.wav" );

   if( !spSound1 )
   {
       cout<<"jaguar.wav加载失败!"<< endl;
       return -1;
   }

   ISound_SP spSound2 = pManager->createSound( "swish.wav" );

   if( !spSound2 )
   {
       cout<<"swish.wav加载失败!"<< endl;
       return -1;
   }

   printf_s( "请按播放jaguar.wav\n请按播放swish.wav\n请按q退出\n");
  
   spSound1->play();
   spSound2->play();

   bool bExit = false;
   do
   {  
       if (_kbhit())
       {
         int key = _getch();
         switch( key )
         {
         case '1':
             spSound1->play();
             break;
         case '2':
             spSound2->play();
             break;
         case 'q':
             bExit = true;
             break;
         default:
             break;
         }
       }

       Sleep( 10 );

   } while (!bExit);

   system("pause");

   return 0;
}

总结
         由于时间关系这里只列出了部分代码,详细的工程我会放在附件中。另外就是Bug问题,也是因为时间关系,我没做大量详细的测试,但是我觉得重要的思想而不是具体的操作,只要明白了思想要改也是很容易的。另外这个东东也只是我自己的一些简单的思想,工程中是怎么样的其实我也不知道,呵呵,因为我还没有看过别人是怎么做的;时间仓促也没有做什么详细的构思和设计,敬请原谅,O(∩_∩)O哈哈~
欢迎各位朋友对文中的任何点提出您的意见和问题。
           我的QQ是84638372,Email:dbdongbo@vip.qq.com 。
           2009/6/10于东北大学秦皇岛分校。

           PS: Demo工程的下载地址:http://ishare.iask.sina.com.cn/f/5263833.html
         比较工整的版本在我的cppblog:  http://www.cppblog.com/db123/archive/2009/06/10/87371.html

发表于 2014-8-11 11:39:28 | 显示全部楼层
楼主不错,谢谢分享,学习不少
发表于 2014-9-30 22:02:33 | 显示全部楼层
虽然是2009年的帖子  但是作为07年建材学院的我来说    我还经常去你们学校门口理发店理发呢
发表于 2014-11-10 19:09:32 | 显示全部楼层
不错的样子,支持一下
发表于 2015-1-30 14:09:16 | 显示全部楼层
正在学习中。。谢谢分享
发表于 2015-1-31 13:58:53 | 显示全部楼层
谢谢分享,支持
发表于 2015-1-31 20:54:57 | 显示全部楼层
技术大牛啊!膜拜中。。。。。。。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|3D数字艺术论坛 ( 沪ICP备14023054号 )

GMT+8, 2025-2-6 00:59

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表