пятница, 21 сентября 2012 г.

3. Работаем со звуком

Как я и обещал ранее, сегодня мы будем учиться работать со звуком и, поскольку нам это понадобиться, немножко с ресурсами. Низкоуровневая работа со звуком (особенно кроссплатформенная) - дело не простое, поэтому, нам очень повезло, что разработчики Marmalade сделали ее за нас.
Для своих нужд мы используем SoundEngine, который, хотя и не является частью Marmalade, входит в комплект дистрибутива и легко может быть использован. У SoundEngine имеется ряд недостатков, о которых я скажу ниже, но его возможности, на текущий момент, нас вполне удовлетворяют.
Следует сразу сказать, что речь идет о двух принципиально различных подсистемах. s3eAudio - позволяет проигрывать фоновую музыку, поддерживает ряд кодеков (в том числе mp3). IwSound - позволяет воспроизводить несколько звуковых эффектов одновременно, но работает только с PCM-кодированными одноканальными wav-файлами. Подробное описание использования этих подсистем можно найти здесь, мы же, попытаемся встроить возможность воспроизведения звука в наш Framework. В файл настроек приложения app.icf добавляем строку, управляющую количеством звуковых эффектов, воспроизводить которые можно одновременно:

[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, который будет содержать зачатки системы локализации наших приложений.
Рассмотрим, что изменилось в наших исходниках. В класс Desktop мы добавляем методы для старта и завершения воспроизведения фоновой музыки:

...
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, используя который можно управлять уже воспроизводимым звуком (например остановить воспроизведение).

Разумеется, звуки - не единственное, что можно поместить в группу ресурсов. Точно также, мы можем создать группу изображений, чем мы воспользуемся, чтобы локализовать графические ресурсы нашего приложения. Мы просто создаем две группы: 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"
}

В 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_
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);
}

...
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);
    }
}


Мы передаем в 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_
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;
    }
}
Функция s3eDeviceGetInt позволяет нам получить информацию о языковых настройках устройства (разумеется кроссплатформенно). Вызывая getCurrentImageLocale мы получаем код локали, передавая который в getGroupName, получаем имя группы. Разумеется, точно также можно локализовать и звуковые ресурсы (просто эту возможность мы сейчас не используем).
Существует большое искушение - поместить в языконезависимую группу все графические ресурсы. Делать этого не стоит, поскольку в скомпилированной группе, графические ресурсы храняться в распакованном виде (и именно в таком виде они попадут в дистрибутив приложения). Если мы поместим background-изображения в группу ресурсов, то скорее всего, мы получим apk-файл размером > 10M, против одномегабайтного, использующего картинки в png-формате.
В следующей статье, мы рассмотрим анимированные и композитные спрайты.

Исходный код проекта можно получить здесь.

Комментариев нет:

Отправить комментарий