Как я и обещал ранее, сегодня мы будем учиться работать со звуком и, поскольку нам это понадобиться, немножко с ресурсами. Низкоуровневая работа со звуком (особенно кроссплатформенная) - дело не простое, поэтому, нам очень повезло, что разработчики Marmalade сделали ее за нас.
Для своих нужд мы используем SoundEngine, который, хотя и не является частью Marmalade, входит в комплект дистрибутива и легко может быть использован. У SoundEngine имеется ряд недостатков, о которых я скажу ниже, но его возможности, на текущий момент, нас вполне удовлетворяют.
Следует сразу сказать, что речь идет о двух принципиально различных подсистемах. s3eAudio - позволяет проигрывать фоновую музыку, поддерживает ряд кодеков (в том числе mp3). IwSound - позволяет воспроизводить несколько звуковых эффектов одновременно, но работает только с PCM-кодированными одноканальными wav-файлами. Подробное описание использования этих подсистем можно найти здесь, мы же, попытаемся встроить возможность воспроизведения звука в наш Framework. В файл настроек приложения app.icf
добавляем строку, управляющую количеством звуковых эффектов,
воспроизводить которые можно одновременно:
[SOUND]
MaxChannels=16
Файл проекта примет следующий вид:
[SOUND]
MaxChannels=16
Файл проекта примет следующий вид:
#!/usr/bin/env mkb
options
{
module_path="$MARMALADE_ROOT/examples"
}
subprojects
{
iw2d
iwresmanager
SoundEngine
}
includepath
{
./source/Main
./source/Common
./source/Scene
}
files
{
[Main]
(source/Main)
Main.cpp
Main.h
TouchPad.cpp
TouchPad.h
Desktop.cpp
Desktop.h
Locale.cpp
Locale.h
[Common]
(source/Common)
IObject.h
IScreenObject.h
ISprite.h
ISpriteOwner.h
AbstractScreenObject.h
AbstractScreenObject.cpp
AbstractSpriteOwner.h
AbstractSpriteOwner.cpp
[Scene]
(source/Scene)
Scene.cpp
Scene.h
Background.cpp
Background.h
Sprite.cpp
Sprite.h
[Data]
(data)
locale_en.group
locale_ru.group
sounds.group
}
assets
{
(data)
background.png
sprite.png
music.mp3
(data-ram/data-gles1, data)
locale_en.group.bin
locale_ru.group.bin
sounds.group.bin
}
Мы видим, что наш файл проекта сильно вырос. Во первых, добавилась опция module_path, указывающая путь к исходникам SoundEngine. В раздел subprojets добавлены менеджер ресурсов (iwresmanager) и, собственно, SoundEngine. В папке Data, появились три новых файла, с расширением .group, о которых мы поговорим позже, а также файл music.mp3, содержащий фоновую мелодию. Наконец, в исходники добавлено описание класса Locale, который будет содержать зачатки системы локализации наших приложений.
options
{
module_path="$MARMALADE_ROOT/examples"
}
subprojects
{
iw2d
iwresmanager
SoundEngine
}
includepath
{
./source/Main
./source/Common
./source/Scene
}
files
{
[Main]
(source/Main)
Main.cpp
Main.h
TouchPad.cpp
TouchPad.h
Desktop.cpp
Desktop.h
Locale.cpp
Locale.h
[Common]
(source/Common)
IObject.h
IScreenObject.h
ISprite.h
ISpriteOwner.h
AbstractScreenObject.h
AbstractScreenObject.cpp
AbstractSpriteOwner.h
AbstractSpriteOwner.cpp
[Scene]
(source/Scene)
Scene.cpp
Scene.h
Background.cpp
Background.h
Sprite.cpp
Sprite.h
[Data]
(data)
locale_en.group
locale_ru.group
sounds.group
}
assets
{
(data)
background.png
sprite.png
music.mp3
(data-ram/data-gles1, data)
locale_en.group.bin
locale_ru.group.bin
sounds.group.bin
}
Мы видим, что наш файл проекта сильно вырос. Во первых, добавилась опция module_path, указывающая путь к исходникам SoundEngine. В раздел subprojets добавлены менеджер ресурсов (iwresmanager) и, собственно, SoundEngine. В папке Data, появились три новых файла, с расширением .group, о которых мы поговорим позже, а также файл music.mp3, содержащий фоновую мелодию. Наконец, в исходники добавлено описание класса Locale, который будет содержать зачатки системы локализации наших приложений.
Рассмотрим, что изменилось в наших исходниках. В класс Desktop мы добавляем методы для старта и завершения воспроизведения фоновой музыки:
if (s3eAudioIsCodecSupported(S3E_AUDIO_CODEC_MP3) &&
s3eAudioIsCodecSupported(S3E_AUDIO_CODEC_PCM))
s3eAudioPlay(res, 0);
}
void Desktop::stopMusic() {
s3eAudioStop();
}
...
void Desktop::startMusic(const char* res) {if (s3eAudioIsCodecSupported(S3E_AUDIO_CODEC_MP3) &&
s3eAudioIsCodecSupported(S3E_AUDIO_CODEC_PCM))
s3eAudioPlay(res, 0);
}
void Desktop::stopMusic() {
s3eAudioStop();
}
Здесь все предельно просто. Мы проверяем наличие необходимых кодеков и запускаем воспроизведение, передавая имя файла (вместе с расширением). Второй параметр функции s3eAudioPlay предоставляет возможность циклически воспроизводить один и тот-же звук (значение по умолчанию - 1 (однократное воспроизведение), 0 - позволяет воспроизводить звук неограниченнное количество раз). Мы пока не будем оформлять подобным образом функцию воспроизведения звукового эффекта IwSound. В дальнейшем, эта возможность будет интегрирована со спрайтами (в то время как воспроизведение фоновой музыке останется в ведении Desktop). Пока добавим тестовый код непосредственно в функцию main:
#include "Main.h"
#include "s3e.h"
#include "Iw2D.h"
#include "IwGx.h"
#include "IwSound.h"
#include "TouchPad.h"
#include "Desktop.h"
#include "Scene.h"
#include "Background.h"
#include "Sprite.h"
void init() {
// Initialise Mamrlade graphics system and Iw2D module
IwGxInit();
Iw2DInit();
// Init IwSound
IwSoundInit();
// Set the default background clear colour
IwGxSetColClear(0x0, 0x0, 0x0, 0);
// Initialise the rsource manager
IwResManagerInit();
#ifdef IW_BUILD_RESOURCES
// Tell resource system how to convert WAV files
IwGetResManager()->AddHandler(new CIwResHandlerWAV);
#endif
IwGetResManager()->LoadGroup("sounds.group");
IwGetResManager()->LoadGroup("locale_en.group");
IwGetResManager()->LoadGroup("locale_ru.group");
touchPad.init();
desktop.init();
}
void release() {
desktop.release();
touchPad.release();
// Shut down the resource manager
IwResManagerTerminate();
// Shutdown IwSound
IwSoundTerminate();
Iw2DTerminate();
IwGxTerminate();
}
int main() {
init(); {
Scene scene;
new Background(&scene, "background.png", 1, elNothing);
new Sprite(&scene, "sprite.png", 122, 100, 2, elNothing);
desktop.setScene(&scene);
// DEBUG:
uint64 musicTimeout = 5000;
uint64 soundTimeout = 6000;
int32 duration = 1000 / 25;
// Main Game Loop
while (!desktop.isQuitMessageReceived()) {
// Update keyboard system
s3eKeyboardUpdate();
// Update Iw Sound Manager
IwGetSoundManager()->Update();
// Update
touchPad.update();
uint64 timestamp = s3eTimerGetMs();
desktop.update(timestamp);
// DEBUG:
if ((musicTimeout > 0)&&(timestamp > musicTimeout)) {
desktop.startMusic("music.mp3");
musicTimeout = 0;
}
if ((soundTimeout > 0)&&(timestamp > soundTimeout)) {
CIwResGroup* resGroup;
const char* groupName = Locale::getGroupName(elSound);
if (groupName != NULL) {
resGroup = IwGetResManager()->GetGroupNamed(groupName);
CIwSoundSpec* SoundSpec =
(CIwSoundSpec*)resGroup->GetResNamed("sound",
IW_SOUND_RESTYPE_SPEC);
CIwSoundInst* SoundInstance = SoundSpec->Play();
}
soundTimeout = 0;
}
// Clear the screen
IwGxClear(IW_GX_COLOUR_BUFFER_F | IW_GX_DEPTH_BUFFER_F);
touchPad.clear();
// Refresh
desktop.refresh();
// Show the surface
Iw2DSurfaceShow();
// Yield to the opearting system
s3eDeviceYield(duration);
}
}
release();
return 0;
}
Мы добавили инициализацию менеджера ресурсов и SoundEngine в функцию init, освобождение этих ресурсов в release, а также вызов IwGetSoundManager()->Update() в главном цикле приложения (рекомендую помнить о необходимости периодического вызова метода update используемых подсистем, поскольку если этого случайно не сделать, ошибки могут быть очень не тривиальными). Помимо сказанного выше, в init добавлена загрузка групп ресурсов (о которых мы сейчас поговорим), и появились два временных фрагмента кода (помеченных коментарием DEBUG), задачей которых является воспроизведение фоновой музыки и однократное воспроизведение звукового эффекта через 5 и 6 секунд, начиная с момента начала работы, соответсвенно. Посмотрим на содержимое файла sounds.group:
CIwResGroup
{
name "sounds"
"./sounds/menubutton.wav"
"./sounds/sound.wav"
CIwSoundSpec
{
name "menubutton"
data "menubutton"
vol 0.9
loop false
}
CIwSoundSpec
{
name "sound"
data "sound"
vol 0.9
loop false
}
CIwSoundGroup
{
name "sound_effects"
maxPolyphony 8
killOldest true
addSpec "menubutton"
addSpec "sound"
}
}
Здесь описаны два звуковых ресурса menubutton (звук нажатия кнопки, который мы будем использовать в следующих статьях) и sound. Указаны относительные имена использованных файлов (начиная от каталога data) и для каждого эффекта создано описание CIwSoundSpec, содержащее имя ресурса, а также данные о громкости воспроизведения и признак циклического воспроизведения звука. Также, все эффекты описаны в CIwSoundGroup, в которой также указано количество эффектов, которые можно воспроризводить одновременно.
Компиляция группы ресурсов будет выполняться при загрузке группы методом LoadGroup. Поэтому, если мы забыли положить какой-то файл в нужное место, при выполнении программы, мы увидим следующую ошибку:
Добавив необходимые файлы и выполнив программу снова, мы увидим, что в каталоге data-ram, появился подкаталог data-gles1 содержащий скомпилированные ресурсы. Пока мы отлаживаем приложение, мы можем удалять этот каталог (он будет пересоздаваться), но он должен присутствовать на момент deployment-а, в противном случае, последний будет завершен с ошибкой. Снова обратим внимание на код активации звукового эффекта:
resGroup = IwGetResManager()->GetGroupNamed(groupName);
CIwSoundSpec* SoundSpec = (CIwSoundSpec*)resGroup->GetResNamed("sound",
IW_SOUND_RESTYPE_SPEC);
CIwSoundInst* SoundInstance = SoundSpec->Play();
Мы ищем группу по имени и если ее нашли, получаем спецификацию звукового эффекта, используя который, можно воспроизвести звук. Метод Play возвращает SoundInstance, используя который можно управлять уже воспроизводимым звуком (например остановить воспроизведение).
#include "Main.h"
#include "s3e.h"
#include "Iw2D.h"
#include "IwGx.h"
#include "IwSound.h"
#include "TouchPad.h"
#include "Desktop.h"
#include "Scene.h"
#include "Background.h"
#include "Sprite.h"
void init() {
// Initialise Mamrlade graphics system and Iw2D module
IwGxInit();
Iw2DInit();
// Init IwSound
IwSoundInit();
// Set the default background clear colour
IwGxSetColClear(0x0, 0x0, 0x0, 0);
// Initialise the rsource manager
IwResManagerInit();
#ifdef IW_BUILD_RESOURCES
// Tell resource system how to convert WAV files
IwGetResManager()->AddHandler(new CIwResHandlerWAV);
#endif
IwGetResManager()->LoadGroup("sounds.group");
IwGetResManager()->LoadGroup("locale_en.group");
IwGetResManager()->LoadGroup("locale_ru.group");
touchPad.init();
desktop.init();
}
void release() {
desktop.release();
touchPad.release();
// Shut down the resource manager
IwResManagerTerminate();
// Shutdown IwSound
IwSoundTerminate();
Iw2DTerminate();
IwGxTerminate();
}
int main() {
init(); {
Scene scene;
new Background(&scene, "background.png", 1, elNothing);
new Sprite(&scene, "sprite.png", 122, 100, 2, elNothing);
desktop.setScene(&scene);
// DEBUG:
uint64 musicTimeout = 5000;
uint64 soundTimeout = 6000;
int32 duration = 1000 / 25;
// Main Game Loop
while (!desktop.isQuitMessageReceived()) {
// Update keyboard system
s3eKeyboardUpdate();
// Update Iw Sound Manager
IwGetSoundManager()->Update();
// Update
touchPad.update();
uint64 timestamp = s3eTimerGetMs();
desktop.update(timestamp);
// DEBUG:
if ((musicTimeout > 0)&&(timestamp > musicTimeout)) {
desktop.startMusic("music.mp3");
musicTimeout = 0;
}
if ((soundTimeout > 0)&&(timestamp > soundTimeout)) {
CIwResGroup* resGroup;
const char* groupName = Locale::getGroupName(elSound);
if (groupName != NULL) {
resGroup = IwGetResManager()->GetGroupNamed(groupName);
CIwSoundSpec* SoundSpec =
(CIwSoundSpec*)resGroup->GetResNamed("sound",
IW_SOUND_RESTYPE_SPEC);
CIwSoundInst* SoundInstance = SoundSpec->Play();
}
soundTimeout = 0;
}
// Clear the screen
IwGxClear(IW_GX_COLOUR_BUFFER_F | IW_GX_DEPTH_BUFFER_F);
touchPad.clear();
// Refresh
desktop.refresh();
// Show the surface
Iw2DSurfaceShow();
// Yield to the opearting system
s3eDeviceYield(duration);
}
}
release();
return 0;
}
Мы добавили инициализацию менеджера ресурсов и SoundEngine в функцию init, освобождение этих ресурсов в release, а также вызов IwGetSoundManager()->Update() в главном цикле приложения (рекомендую помнить о необходимости периодического вызова метода update используемых подсистем, поскольку если этого случайно не сделать, ошибки могут быть очень не тривиальными). Помимо сказанного выше, в init добавлена загрузка групп ресурсов (о которых мы сейчас поговорим), и появились два временных фрагмента кода (помеченных коментарием DEBUG), задачей которых является воспроизведение фоновой музыки и однократное воспроизведение звукового эффекта через 5 и 6 секунд, начиная с момента начала работы, соответсвенно. Посмотрим на содержимое файла sounds.group:
CIwResGroup
{
name "sounds"
"./sounds/menubutton.wav"
"./sounds/sound.wav"
CIwSoundSpec
{
name "menubutton"
data "menubutton"
vol 0.9
loop false
}
CIwSoundSpec
{
name "sound"
data "sound"
vol 0.9
loop false
}
CIwSoundGroup
{
name "sound_effects"
maxPolyphony 8
killOldest true
addSpec "menubutton"
addSpec "sound"
}
}
Здесь описаны два звуковых ресурса menubutton (звук нажатия кнопки, который мы будем использовать в следующих статьях) и sound. Указаны относительные имена использованных файлов (начиная от каталога data) и для каждого эффекта создано описание CIwSoundSpec, содержащее имя ресурса, а также данные о громкости воспроизведения и признак циклического воспроизведения звука. Также, все эффекты описаны в CIwSoundGroup, в которой также указано количество эффектов, которые можно воспроризводить одновременно.
Компиляция группы ресурсов будет выполняться при загрузке группы методом LoadGroup. Поэтому, если мы забыли положить какой-то файл в нужное место, при выполнении программы, мы увидим следующую ошибку:
Добавив необходимые файлы и выполнив программу снова, мы увидим, что в каталоге data-ram, появился подкаталог data-gles1 содержащий скомпилированные ресурсы. Пока мы отлаживаем приложение, мы можем удалять этот каталог (он будет пересоздаваться), но он должен присутствовать на момент deployment-а, в противном случае, последний будет завершен с ошибкой. Снова обратим внимание на код активации звукового эффекта:
resGroup = IwGetResManager()->GetGroupNamed(groupName);
CIwSoundSpec* SoundSpec = (CIwSoundSpec*)resGroup->GetResNamed("sound",
IW_SOUND_RESTYPE_SPEC);
CIwSoundInst* SoundInstance = SoundSpec->Play();
Мы ищем группу по имени и если ее нашли, получаем спецификацию звукового эффекта, используя который, можно воспроизвести звук. Метод Play возвращает SoundInstance, используя который можно управлять уже воспроизводимым звуком (например остановить воспроизведение).
Разумеется, звуки - не единственное, что можно поместить в группу ресурсов. Точно также, мы можем создать группу изображений, чем мы воспользуемся, чтобы локализовать графические ресурсы нашего приложения. Мы просто создаем две группы: locale_en и locale_ru, и помещаем в них рисунки с надписями на разных языках. Вот как будет выглядеть группа locale_ru, с ресурсами, которые понадобятся нам, когда мы будем разрабатывать пользовательский интерфейс:
CIwResGroup
{
name "locale_ru"
"./locale_ru/play.png"
"./locale_ru/setup.png"
"./locale_ru/musicoff.png"
"./locale_ru/musicon.png"
"./locale_ru/soundoff.png"
"./locale_ru/soundon.png"
}
{
name "locale_ru"
"./locale_ru/play.png"
"./locale_ru/setup.png"
"./locale_ru/musicoff.png"
"./locale_ru/musicon.png"
"./locale_ru/soundoff.png"
"./locale_ru/soundon.png"
}
В locale_en изменятся только пути к каталогу с картинками. Разумеется, мы рассчитываем на то, что картинки на разных языках одинаковы по размеру и дизайну и различаются только языком надписей. В класс Sprite (и его интерфейс) внесем небольшое изменение:
ISprite.h:
#ifndef _ISPRITE_H_
#define _ISPRITE_H_
#include "Locale.h"
#include "Iw2D.h"
#include "IwGx.h"
class ISprite {
public:
virtual void addImage(const char* res, int state = 0, int locale = 0) = 0;
virtual CIw2DImage* getImage(int state = 0) = 0;
};
#endif // _ISPRITE_H_
#define _ISPRITE_H_
#include "Locale.h"
#include "Iw2D.h"
#include "IwGx.h"
class ISprite {
public:
virtual void addImage(const char* res, int state = 0, int locale = 0) = 0;
virtual CIw2DImage* getImage(int state = 0) = 0;
};
#endif // _ISPRITE_H_
Sprite.cpp:
#include "Sprite.h"
#include "Locale.h"
#include "Desktop.h"
Sprite::Sprite(ISpriteOwner* owner, const char* res, int x, int y, int zOrder, int locale): AbstractScreenObject(x, y)
, owner(owner)
, capturedId(-1)
, img(NULL) {
addImage(res, 0, locale);
owner->addSprite((AbstractScreenObject*)this, zOrder);
}
#include "Locale.h"
#include "Desktop.h"
Sprite::Sprite(ISpriteOwner* owner, const char* res, int x, int y, int zOrder, int locale): AbstractScreenObject(x, y)
, owner(owner)
, capturedId(-1)
, img(NULL) {
addImage(res, 0, locale);
owner->addSprite((AbstractScreenObject*)this, zOrder);
}
...
void Sprite::addImage(const char*res, int state, int loc) {
CIwResGroup* resGroup;
const char* groupName = Locale::getGroupName(loc);
if (groupName != NULL) {
resGroup = IwGetResManager()->GetGroupNamed(groupName);
IwGetResManager()->SetCurrentGroup(resGroup);
img = Iw2DCreateImageResource(res);
} else {
img = Iw2DCreateImage(res);
}
}
CIwResGroup* resGroup;
const char* groupName = Locale::getGroupName(loc);
if (groupName != NULL) {
resGroup = IwGetResManager()->GetGroupNamed(groupName);
IwGetResManager()->SetCurrentGroup(resGroup);
img = Iw2DCreateImageResource(res);
} else {
img = Iw2DCreateImage(res);
}
}
Мы передаем в addImage код локали, по которому определяем имя группы и если такая группа существует, загружаем ресурс из нее по имени. В противном случае, мы, как и прежде, загружаем файл. Следует подчеркнуть, что когда м загружаем ресурс, мы используем имя ресурса, без расширения. Если мы загружаем файл, расширение обязательно должно быть указано.
Как легко догадаться из вышесказанного, кодами локали занимается класс Locale. В нашем случае, он имеет предельно простую реализацию:
Locale.h:
#ifndef _LOCALE_H_
#define _LOCALE_H_
enum ELocale {
elNothing = 0x0,
elImage = 0x1,
elSound = 0x2,
elEnImage = 0x5,
elRuImage = 0x9,
elEnSound = 0x6,
elRuSound = 0xA
};
class Locale {
public:
static int getCurrentImageLocale();
static int getCurrentSoundLocale();
static int getCommonImageLocale() {return elImage;}
static int getCommonSoundLocale() {return elSound;}
static const char* getGroupName(int locale);
};
#endif // _LOCALE_H_
#define _LOCALE_H_
enum ELocale {
elNothing = 0x0,
elImage = 0x1,
elSound = 0x2,
elEnImage = 0x5,
elRuImage = 0x9,
elEnSound = 0x6,
elRuSound = 0xA
};
class Locale {
public:
static int getCurrentImageLocale();
static int getCurrentSoundLocale();
static int getCommonImageLocale() {return elImage;}
static int getCommonSoundLocale() {return elSound;}
static const char* getGroupName(int locale);
};
#endif // _LOCALE_H_
Locale.cpp:
#include "Locale.h"
#include "s3e.h"
const char* Locale::getGroupName(int locale) {
switch (locale) {
case elImage: return "images";
case elEnSound:
case elRuSound:
case elSound: return "sounds";
case elEnImage: return "locale_en";
case elRuImage: return "locale_ru";
default: return NULL;
}
}
int Locale::getCurrentImageLocale() {
int32 lang = s3eDeviceGetInt(S3E_DEVICE_LANGUAGE);
switch (lang) {
case S3E_DEVICE_LANGUAGE_RUSSIAN: return elRuImage;
default: return elEnImage;
}
}
int Locale::getCurrentSoundLocale() {
int32 lang = s3eDeviceGetInt(S3E_DEVICE_LANGUAGE);
switch (lang) {
case S3E_DEVICE_LANGUAGE_RUSSIAN: return elRuSound;
default: return elEnSound;
}
}
#include "s3e.h"
const char* Locale::getGroupName(int locale) {
switch (locale) {
case elImage: return "images";
case elEnSound:
case elRuSound:
case elSound: return "sounds";
case elEnImage: return "locale_en";
case elRuImage: return "locale_ru";
default: return NULL;
}
}
int Locale::getCurrentImageLocale() {
int32 lang = s3eDeviceGetInt(S3E_DEVICE_LANGUAGE);
switch (lang) {
case S3E_DEVICE_LANGUAGE_RUSSIAN: return elRuImage;
default: return elEnImage;
}
}
int Locale::getCurrentSoundLocale() {
int32 lang = s3eDeviceGetInt(S3E_DEVICE_LANGUAGE);
switch (lang) {
case S3E_DEVICE_LANGUAGE_RUSSIAN: return elRuSound;
default: return elEnSound;
}
}
Функция s3eDeviceGetInt позволяет нам получить информацию о языковых настройках устройства (разумеется кроссплатформенно). Вызывая getCurrentImageLocale мы получаем код локали, передавая который в getGroupName, получаем имя группы. Разумеется, точно также можно локализовать и звуковые ресурсы (просто эту возможность мы сейчас не используем).
Существует большое искушение - поместить в языконезависимую группу все графические ресурсы. Делать этого не стоит, поскольку в скомпилированной группе, графические ресурсы храняться в распакованном виде (и именно в таком виде они попадут в дистрибутив приложения). Если мы поместим background-изображения в группу ресурсов, то скорее всего, мы получим apk-файл размером > 10M, против одномегабайтного, использующего картинки в png-формате.
В следующей статье, мы рассмотрим анимированные и композитные спрайты.
Исходный код проекта можно получить здесь.
Исходный код проекта можно получить здесь.
Комментариев нет:
Отправить комментарий