Post date: Jul 25, 2011 5:05:38 PM
Trước tiên, xin nhấn mạnh rằng, âm thanh đóng vai trò rất lớn và không thể thiếu trong game. Đã qua rất lâu rồi thời game chỉ phát ra âm thanh pip pip. Việc quản lý âm thanh cũng là một công việc khá phức tạp và qui mô đến mức ta cần phải xây dựng một engine riêng cho nó (sound engine). Trong phạm vi của mình, bài viết chỉ có thể cung cấp những phương thức cơ bản nhất, đủ để có âm thanh trong game.
Trong phần này, ta sẽ tìm hiểu cách sử dụng âm thanh trong game. Thư viện được sử dụng bao gồm:
Bước đầu tiên, ta cần download gói cài đặt OpenAL ở đây. Với Win32, ta chỉ cần cài đặt, và dùng các lib được cung cấp đi kèm trong ví dụ (OpenAL32.lib). Với các thư viện vorbis, bạn có thể download ở đây.
Mục tiêu của phần này là ta có thể thiết lập một thư viện ân thanh đơn giản, có thể play được 2 dạng file là wav và ogg dùng thư viện OAL.
Reference:
Audio: Âm thanh, là các sóng dao động cơ học lan truyền trong không gian và truyền đến tai người nghe. Tai người có thể nghe được dãi tầng 20Hz đến 20kHz. Cả tiếng ồn và âm nhạc đều là âm thanh
Âm thanh được đặc trưng bởi: tần số (frequency), bước sóng (wave length), chu kỳ, và biên độ (Amplitude), vận tốc. Trong đó, amplitude quyết định độ to của âm thanh nghe được và tần số quyết định độ trầm/bổng của âm thanh.
Hiện nay, để lưu trữ âm thanh, người ta thường số hóa âm thanh (chuyển từ analog sang digital). Việc số hóa âm thanh giúp việc xử lý các hiệu ứng được dễ dàng hơn. Khi phát âm thanh, tín hiện digital lại được tái tạo thành analog và phát bởi các loa. Chất lượng việc số hóa âm thanh sẽ quyết định khả năng tái tạo âm thanh. Chất lượng số hóa càng cao, việc tái tạo âm thanh càng trung thực, nhưng sẽ đòi hỏi một không gian lưu trữ lớn. Phương pháp số hóa âm thanh phổ biến là PCM
Việc số hóa âm thanh thường được gọi là điều chế (modulation). Với PCM, modulation là sự kết hợp của 2 việc: lấy mẫu tín hiện (sampling) và lượng tử hóa (Quantization)
Sampling: Đều đăn sau một khoản thời gian nhất định, cường độ âm sẽ được ghi nhận, gọi làm mẫu (sample). Từ tín hiệu liên tục theo thời gian ban đầu, sau quá trình sampling, ta sẽ có các số liệu rời rạc hữu hạn về cường độ âm. Tần số lấy mẫu càng lớn (khoản cách thời gian 2 lần lấy mẫu càng nhỏ) thì tín chất lượng càng cao.
Quantization: Chuyển đổi giá trị liên tục của tín hiệu đầu vào (cường độ) thành các giá trị nguyên (làm tròn). Số lượng các giá trị nguyên được phân chia tùy thuộc vào bit depth (8 bits -> 256 giá trị). Bit depth càng lớn thì sai số càng nhỏ, chất lượng càng cao
Với PCM, bit depth thông thường là: 8, 16, 24, 32, 64. Các giá trị có thể được lưu theo dạng sign (có dấu) hoặc unsigned (không dấu). Ví dụ trong trường hợp 8bits, sign: từ -128 ... 127 và unsigned từ 0 ... 255. Về thứ tự bit, có 2 cách lưu trữ giá trị số (endian) LSB - least significant byte và MSB - most significant byte. Ngoài ra, ngày nay có một số hệ thống dùng số float thay cho số nguyên
Quá trình điều chế là sự tổng hợp của 2 quá trình sampling và quantization
Một số khái niệm về âm thanh số:
Đầu tiên, ta cần phải include 2 file header quan trọng của OAL.
#if CONFIG_PLATFORM==PLATFORM_WIN32_VS
# include <windows.h>
# include <time.h>
# include "../libs/ogl/gl.h"
# include "../libs/ogl/glext.h"
# pragma comment(lib, "opengl32.lib")
# include "../libs/oal/al.h"
# include "../libs/oal/alc.h"
# pragma comment(lib, "openal32.lib")
# pragma pack(1) //for struct
alignment
# include "../libs/vld/vld.h"
#endif
Tiếp theo, ta thiết kế mô hình các lớp quản lý âm thanh. Cũng giống với IO hay Video, ta sữ dụng cơ chế driver/manager cho Sound.
Người dùng chỉ cần sử dụng các phương thức được cung cấp bởi CAudioPlayer mà không cần quan tâm đến hạ tầng bên dưới. Để thuận tiện cho người dùng, phần cấu hình được bổ sung thêm tham số audio driver.
SGameConfig.h
struct SGameConfig
{
__UINT16 iWidth;
__UINT16 iHeight;
bool isUseFullScreen;
const char *strTitle;
IVideoDriver *pVideoDriver;
CAudioDriver *pAudioDriver;
CGame* pGame;
};
Là lớp cơ sở cho các AudioDriver, lớp này cung cấp một hàm duy nhất cho phép kiểm tra loại driver sử dụng: IsAudioDriver. Trong phạm vi trình bày, chỉ một driver duy nhất được sữ dụng là OpenAL
CAudioDriver.h
enum EAudioDriver
{
EAUDIO_DRIVER_OPENAL,
};
class CAudioDriver
{
public:
CAudioDriver(EAudioDriver driver);
bool IsAudioDriver(EAudioDriver driver);
virtual ~CAudioDriver(){}
protected:
EAudioDriver m_AudioDriver;
};
Lớp này tương tác với OpenAL và cung cấp các hàm cơ bản cho âm thanh:
Các bạn có thể tham khảo source code từ file CALAudiodriver.h và CALAudiodriver.cpp. Ta sẽ tìm hiểu kỹ hơn về OpenAL trong một chuyên mục khác.
Cơ chế hoạt động hiện tại của CALAudioDriver
void CALAudioDriver::Play (const char *name, bool isLoop)
{
SALData* data = 0;
if (this->m_SoundMapping->GetElement(name, data))
{
if(alGetError() != AL_NO_ERROR)
return;
alSourcei (data->Source, AL_LOOPING, isLoop );
if (!IsPlaying(data->Source))
{
alSourcei(data->Source, AL_SOURCE_RELATIVE, true);
alSourcePlay(data->Source);
}
}
}
Với cách này, người dùng không cần quản lý các ID trả về từ OpenAL mà có thể thao tác trực tiếp bằng tên file vốn thân thiện hơn trong quản lý và debug. Hiển nhiên, để tiện hơn, ta phải đánh đối bằng chi phí quản lý (tra cứu bảng), nhưng dễ thấy là lợi ích mà nó mang lại khá lớn.
Người dùng trực tiếp sử dụng lớp này cho âm thanh.
class CAudioPlayer: public CSingleton<CAudioPlayer>
{
friend class CSingleton<CAudioPlayer>;
protected:
CAudioPlayer();
public:
virtual ~CAudioPlayer();
void Play(const char *name, bool isLoop);
void Pause(const char *name);
void Stop(const char *name);
bool IsPlaying (const char *name);
void SetVolume(int vol);
__INT32 GetVolume();
void EnableSound(bool val){m_IsEnable = m_IsError?false:val;}
bool IsEnable() {return m_IsEnable;}
template <class IODriver> bool Load(const char* fileName, EAudioMinetype minetype = EAUDIOMINE_AUTO);
private:
bool Initialize ();
void Destroy();
CAudioDriver *m_pAudioDriver;
bool m_IsEnable;
bool m_IsError;
};
template <class IODriver> bool CAudioPlayer::Load(const char* fileName, EAudioMinetype minetype)
{
if (m_pAudioDriver->IsAudioDriver(EAUDIO_DRIVER_OPENAL))
{
return ((CALAudioDriver*)m_pAudioDriver)->Load<IODriver>(fileName, minetype);
}
return false;
}
CAudioDriver kiểm tra loại driver, từ đó gọi các hàm tương ứng được cung cấp bởi driver.
Với các phân lớp này mang lại cho ta một số lợi ích:
Lưu ý: cũng như các Singleton khác, CAudioDriver cần được hủy khi thoát game, tránh thất thoát memory
void FinalizeApp()
{
SAFE_DEL(Configuation.pGame);
SAFE_DEL(Configuation.pVideoDriver);
delete CFpsController::GetInstance();
delete CViewController<VIEWCLASS>::GetInstance();
delete CFileWin32Driver::GetInstance();
delete CDevice::GetInstance();
delete CStateManagement::GetInstance();
delete CGraphics2D::GetInstance();
delete CImageManager::GetInstance();
delete CSpriteDBManagement::GetInstance();
delete CControllerEventManager::GetInstance();
delete CAudioPlayer::GetInstance();
}
Download
Khi cần âm thanh với thời lượng dài, file wav tỏ ra khá nặng nề. Trong trường hợp này, một giải pháp thay thế khá hiệu quả là sử dụng file ogg. Vấn đề là làm sao tích hợp được thư viện play được file ogg vào thư viện ta đang xây dựng
Để có được thư viện cho ogg, ta có thể download thư viện 2 thư viện ogg và vorbis. Hai thư viện này hổ trợ build bởi nhiều platform và trình biên dịch khác nhau, trong đó ta chỉ cần visual studio 2010. Lưu ý, ta chỉ cần build các project libogg_static, libvorbis_static, và libvorbisfile_static. Để thuận tiện, bạn có thể download project đã được giản lượt ở đây.
Sau khi build, ta được 3 file thư viện: libogg_static.lib, libvorbis_static.lib, libvorbisfile_static.lib. Trong gameturto/lib, ta tạo thêm thư mục vorbis, và chép các lib này vào đây, đồng thời chép các file header từ project vorbis như codec.h, ogg.h, os_types.h, vorbisenc.h, vorbisfile.h. (các file header trong của vorbis trong gametutor đã được hiệu chỉnh đổi chút ở các dòng #include để phù hợp với project)
Đến lúc này, ta đã sẵn sàng các thư viện dùng trong việc đọc file ogg. Ta chỉ cần khai báo các header và lib trong Header.h (Đường dẫn đến file .lib cần được khai báo trong linker của project Demo)
#include "../libs/vorbis/codec.h"
#include "../libs/vorbis/ogg.h"
#include "../libs/vorbis/os_types.h"
#include "../libs/vorbis/vorbisenc.h"
#include "../libs/vorbis/vorbisfile.h"
#pragma comment(lib, "libogg_static.lib")
#pragma comment(lib, "libvorbis_static.lib")
#pragma comment(lib, "libvorbisfile_static.lib")
Vấn đề tiếp theo là làm sao để thư viện ogg-vorbis (gọi chung là vorbis) có thể tương thích với thư viện ta đang xây dựng. Thư viện vorbis có cung cấp hàm đọc file trực tiếp từ tên file thông qua hàm ov_open. Tuy nhiên, hàm này không tương thích với cơ chế quản lý file của ta đã định nghĩa (thông qua các IO driver). Giải pháp khả khi là dùng thư viện IO đã định nghĩa để đọc file vào buffer, sau đó dùng vorbis để load nội dung file âm thanh từ buffer. Tuy nhiên vorbis lại không hổ trợ load file từ buffer. Do đó, việc tiếp theo cần phải thực hiện là port cách load từ buffer cho vorbis.
Ta định nghĩa struct SOggFile đóng vai trò giả lập file, và lớp CVorbisStreamPorting gồm các thao tác cơ bản mô phỏng việc đọc file
struct SOggFile
{
char* dataPtr; // Pointer to the data in memoru
int dataSize; // Sizeo fo the data
int dataRead; // How much data we have read so far
};
class CVorbisStreamPorting
{
public:
static size_t VorbisRead(
void *ptr, // ptr to the data that the vorbis files need,
size_t byteSize, // how big a byte is
size_t sizeToRead, // How much we can read
void *datasource); // this is a pointer to the data we passed into ov_open_callbacks
static int VorbisSeek(
void *datasource, //this is a pointer to the data we passed into ov_open_callbacks
ogg_int64_t offset, //offset from the point we wish to seek to
int whence); //where we want to seek to
static int VorbisClose(void *datasource);
static long VorbisTell(void *datasource);
static void VorbisError(int code); //check error
};
Chi tiết về các implement các hàm này, bạn có thể tham khảo source code file CVorbisStreamPorting.cpp (khá đơn giản)
Để lớp CALAudioDriver có thể làm việc với ogg, lớp này cũng phải có các hàm của lớp CVorbisStreamPorting. Do đó, ta cho CALAudioDriver thừa kế private từ CVorbisStreamPorting:
class CALAudioDriver: public CAudioDriver, public CSingleton<CALAudioDriver>, private CVorbisStreamPorting
Tiếp theo, ta dựa vào hàm ov_open_callbacks, cấu trúc file Ogg ảo (SOggFile) và các hàm giả lập đọc ghi file trong CVorbisStreamPorting để load nội dung file ogg từ buffer (may mắn cơ chế này được hổ trợ bởi vorbis, thông qua hàm ov_open_callbacks)
Load ogg
bool CALAudioDriver::OpenOgg (char *buf, int buf_size,unsigned int buffer) { int result; OggVorbis_File oggStream; vorbis_info* vorbisInfo = 0; vorbis_comment* vorbisComment = 0; ALenum format; if(!buf) { Log("[CALAudioDriver::OpenOgg] Input buffer is null"); return 0; } SOggFile oggMemoryFile; oggMemoryFile.dataPtr = buf; oggMemoryFile.dataRead = 0; oggMemoryFile.dataSize = buf_size; ov_callbacks vorbisCallbacks = { VorbisRead, VorbisSeek, VorbisClose, VorbisTell }; result = ov_open_callbacks(&oggMemoryFile, &oggStream, NULL, 0, vorbisCallbacks); //return false; if(result < 0) { Log("[CALAudioDriver::OpenOgg] Could not open Ogg memory stream"); } vorbisInfo = ov_info(&oggStream, -1); vorbisComment = ov_comment(&oggStream, -1); if(vorbisInfo->channels == 1) { format = AL_FORMAT_MONO16; } else { format = AL_FORMAT_STEREO16; } // stream long long buffer_size = (long long)oggStream.pcmlengths; char *pcm = new char[buffer_size]; int size = 0; int section; while(size < buffer_size) { result = ov_read(&oggStream, pcm + size, buffer_size - size, 0, 2, 1, §ion); if(result > 0) size += result; else if(result < 0) { Log("[CALAudioDriver::OpenOgg] Error"); VorbisError(result); return false; } else break; } if(size == 0) { Log("[CALAudioDriver::OpenOgg] Error: Can not decode ogg file"); return false; } alBufferData(buffer, format, pcm, size, vorbisInfo->rate); ov_clear(&oggStream); delete pcm; return true; }
Tiếp theo, cung cấp hàm load Ogg từ file theo cơ chế: dùng IODriver load vào buffer, và load từ buffer
template <class IODriver> bool CALAudioDriver::OpenOgg(const char *nameFile,unsigned int buffer)
{
CReaderStream<IODriver> *F = new CReaderStream<IODriver>(nameFile);
if (F->GetStatus() == ESTREAM_OPEN)
{
int buf_size = F->GetLength();
char * buf = new char[buf_size];
F->Read((__UINT8*)buf, 0, buf_size);
SAFE_DEL(F);
bool re = OpenOgg (buf, buf_size, buffer);
SAFE_DEL(buf);
return re;
}
return false;
}
Cuối cùng là kết nối hàm OpenOgg với hàm Load của CALAudioDriver
Load
template <class IODriver> ALboolean CALAudioDriver::Load(const char* fileName, EAudioMinetype minetype) { SALData *Source = new SALData; if (this->m_SoundMapping->GetElement(fileName, Source)) { LogWarning("fileName %s has been already loaded. Ignore", fileName); return false; } // Load wav data into a buffer. alGenBuffers(1, &(Source->Buffer)); if(alGetError() != AL_NO_ERROR) return AL_FALSE; bool isWav = false; bool isOgg = false; if (minetype == EAUDIOMINE_AUTO) { //Check sound format char ext[3]; memcpy(ext, fileName + (strlen(fileName) - 3), 3); Str_ToLower(ext, 3); if (memcmp("wav", ext, 3) == 0) { isWav = true; } else if (memcmp("ogg", ext, 3) == 0) { isOgg = true; } } else if (minetype == EAUDIOMINE_WAV) { isWav = true; } else if (minetype == EAUDIOMINE_OGG) { isOgg = true; } //Wav format if (isWav) { if (!OpenWave<IODriver>(fileName, Source->Buffer)) { delete Source; LogError("[CALAudioDriver::Load] Can not load file %s", fileName); return AL_FALSE; } } else if (isOgg) { if (!OpenOgg<IODriver>(fileName, Source->Buffer)) { delete Source; LogError("[CALAudioDriver::Load] Can not load file %s", fileName); return AL_FALSE; } } else { LogError("[CALAudioDriver::Load] File type is not supported"); delete Source; return AL_FALSE; } // Bind the buffer with the source. alGenSources(1, &(Source->Source)); if(alGetError() != AL_NO_ERROR) { return AL_FALSE; } float tmp[] = {0.0, 0.0, 0.0}; alSourcei (Source->Source, AL_BUFFER, Source->Buffer ); alSourcef (Source->Source, AL_PITCH, 1.0 ); alSourcef (Source->Source, AL_GAIN, 1.0 ); alSourcefv(Source->Source, AL_POSITION, tmp); alSourcefv(Source->Source, AL_VELOCITY, tmp); alSourcei (Source->Source, AL_LOOPING, false ); m_SoundMapping->AddItem(fileName, Source); // Do another error check and return. if(alGetError() == AL_NO_ERROR) { return AL_TRUE; } else { return AL_FALSE; } }
Lưu ý
Một điều lưu ý quan trọng là cơ chế này của vorbis không thể thực hiện được nếu sử dụng structure alignment với #pragma pack(1). Do đó, #pragma pack(1) trong Header.h cần được gỡ bỏ, và hàm loadTGA của CImage cần được viết lại
CImage* CImage::LoadTGA(IReadableStream* stream)
{
TGA_Header header;
stream->ReadInt8(header.idlength);
stream->ReadInt8(header.colourmaptype);
stream->ReadInt8(header.datatypecode);
stream->ReadInt16(header.colourmaporigin);
stream->ReadInt16(header.colourmaplength);
stream->ReadInt8(header.colourmapdepth);
stream->ReadInt16(header.x_origin);
stream->ReadInt16(header.y_origin);
stream->ReadInt16(header.width);
stream->ReadInt16(header.height);
stream->ReadInt8(header.bitsperpixel);
stream->ReadInt8(header.imagedescriptor);
CImage *img = 0;
....
}
Việc demo khác đơn giản, được thực hiện trong hàm khởi tạo của state Poster như sau:
void CStatePoster::Init()
{
Log("State Poster: Init");
CGraphics2D::GetInstance()->Reset();
CSpriteDBManagement::GetInstance()->AddSpriteDBFromTextFile<CFileWin32Driver>("test.txt");
spr = new CSprite(CSpriteDBManagement::GetInstance()->Get("test.txt"));
spr->SetAnimation("XYZ", -1);
spr->SetPosition(SPosition2D<__INT32>(0, 0));
CAudioPlayer::GetInstance()->Load<CFileWin32Driver>("test.wav");
CAudioPlayer::GetInstance()->Load<CFileWin32Driver>("test2.wav");
CAudioPlayer::GetInstance()->Play("test.wav", false);
CAudioPlayer::GetInstance()->Play("test2.wav", true);
CAudioPlayer::GetInstance()->Load<CFileWin32Driver>("in_game.ogg");
CAudioPlayer::GetInstance()->Play("in_game.ogg", false);
}
Trong thư viện được thiết kế chỉ cung cấp hàm play file âm thanh nói chung, chứ chưa có sự phân loại âm thanh. Thông thường, âm thanh được chia làm 2 loại chính:
Trong một màn chơi có thể hoặc không có bgm. Nếu có sử dụng bgm, thì bgm sẽ được lặp đi lặp lại trong suốt màn chơi, hoặc đổi ngẫu nhiên. Đối với sfx, thường chỉ phát 1 lần tại một thời điểm. Do đó, cần thiết kế những hàm chuyên phục vụ cho từng loại sound để thuận tiện cho người dùng như: PlayBgm, PlaySfx ... (chưa có trong thư viện)
Bgm thường dài, do đó cần chọn file format có khả năng nén tốt thư mp3 hay ogg. Với các effect ngắn, wav có thể đảm nhận tốt.
Lưu ý với các file âm thanh, nhất là đối với sfx, không nên chọn file có bit-rate quá lớn (chất lượng quá cao). Nếu không sẽ tạo một khoảng trễ trước khi âm thanh được play. Điều này khiến người chơi game cảm thấy khó chịu, nhất là đối với sfx
Cũng giống như việc quản lý tương tác, âm thanh cũng cần 2 phần xử lý trực tiếp và gián tiếp. Thư viện hiện tại chỉ đáp ứng được việc xử lý trực tiếp (thực hiện phát/tắt âm thanh ngay khi gọi hàm). Cần một cơ chế tốt hơn trong việc quản lý các yêu cầu về âm thanh:
Download