Разработка для PlayStation
Часть 6. Звук
Эта статья является частью цикла «Разработка для PlayStation», который выходил в «FPS» в 2015-2017 годах. Предыдущую часть читайте тут.
Важной составляющей любой игры являются звук и музыка. На этом уроке я покажу, как воспроизводить на PSX короткие звуки, полностью помещающиеся в памяти – в будущем, надеюсь, доберемся до потоковой музыки и Audio CD.
У PSX имеется 16-битный звуковой процессор (SPU) с 24 ADPCM-каналами (или голосами – voice) и звуковой памятью в 512 кб. Каналы используются как источники звуков или как частотные модуляторы для других каналов. Каждый канал имеет собственную программируемую ADSR-огибающую (envelope filter), которая используется для описания изменения громкости звука – например, плавного нарастания и затухания. Аббревиатура ADSR означает Attack, Decay, Sustain, Release (атака, спад, поддержка, затухание). В этой статье я ADSR-эффекты разбирать не стал – вернемся к ним в другой раз.
Для того, чтобы воспроизвести звук, нужно передать его в звуковую память, привязать к одному из каналов и включить этот канал. Звучит просто, но это сложнее, чем кажется. PSX для хранения звука использует не привычное нам PCM-кодирование (то есть, амплитуды сигнала, оцифрованные через заданные промежутки времени), а ADPCM – «продвинутую» разновидность PCM, где используется переменная частота дискретизации, а сигнал, к тому же, кодируется в виде разности между текущим и предыдущим значениями амплитуды. Получается эдакий простейший вид сжатия, который на PSX поддерживается аппаратно. К счастью, чтобы использовать ADPCM, вникать в суть его работы нам необязательно. ADPCM-данные хранятся в формате VAG, и для конвертации звуков в этот формат из обычного WAV существует специальная утилита, написанная доблестными хакерами - wav2vag. Ее довольно легко найти в Интернете – например, исходник есть тут. С компиляцией, надеюсь, справитесь без моей помощи ;)
Убедитесь, что ваш WAV-файл сохранен с частотой дискретизации 44100 Гц. В Audacity, например, это задается параметром «Частота проекта» (Project frequency) в левом нижнем углу. Затем выполните следующую команду:
wav2vag input.wav SOUND.VAG -raw -sraw16 -freq=44100
Теперь программная часть. Для краткости я не буду приводить здесь общий код инициализации программы и главного цикла – вы найдете его в предыдущих уроках. Подключаем необходимые хедеры и объявляем глобальные переменные:
#include <libsnd.h>
#include <libspu.h>
// Максимальное количество выделений звуковой памяти
#define MALLOC_MAX 3
// Блок памяти, внутри которого можно создавать звуковые буферы
char spu_malloc_rec[SPU_MALLOC_RECSIZ * (MALLOC_MAX + 1)];
В функции main объявляем локальные переменные:
// Общие настройки для всех каналов
SpuCommonAttr commonVoiceAttr;
// Индивидуальные настройки канала
SpuVoiceAttr indivVoiceAttr;
// Данные из VAG-файла
Data vagData;
// Адрес данных в звуковой памяти
unsigned long soundAdr;
Инициализируем звук:
SpuInit();
SpuInitMalloc(MALLOC_MAX, spu_malloc_rec);
commonVoiceAttr.mask = (SPU_COMMON_MVOLL | SPU_COMMON_MVOLR);
// Громкость левой колонки
commonVoiceAttr.mvol.left = 0x3fff;
// Громкость правой колонки
commonVoiceAttr.mvol.right = 0x3fff;
SpuSetCommonAttr(&commonVoiceAttr);
Загружаем VAG-файл с диска, используя функцию readFile из предыдущих уроков:
readFile("\\SOUND.VAG;1", &vagData);
// Переключаемся на асинхронный доступ к звуковой памяти (DMA)
SpuSetTransferMode(SpuTransByDMA);
// Выделяем буфер в памяти под звук и получаем его адрес
soundAdr = SpuMalloc(vagData.length);
// Указываем адрес, по которому собираемся записывать данные
SpuSetTransferStartAddr(soundAdr);
// Копируем данные из оперативной памяти в звуковую
SpuWrite((u_char*)vagData.ptr, vagData.length);
// Ждем, пока копирование не завершится
SpuIsTransferCompleted(SPU_TRANSFER_WAIT);
Теперь осталось привязать звуковой буфер к каналу:
indivVoiceAttr.mask = (
SPU_VOICE_VOLL |
SPU_VOICE_VOLR |
SPU_VOICE_PITCH |
SPU_VOICE_WDSA |
SPU_VOICE_ADSR_AMODE |
SPU_VOICE_ADSR_SMODE |
SPU_VOICE_ADSR_RMODE |
SPU_VOICE_ADSR_AR |
SPU_VOICE_ADSR_DR |
SPU_VOICE_ADSR_SR |
SPU_VOICE_ADSR_RR |
SPU_VOICE_ADSR_SL
);
// Номер канала, от SPU_0CH до SPU_23CH
indivVoiceAttr.voice = (SPU_0CH);
// Адрес нашего буфера
indivVoiceAttr.addr = soundAdr;
// настройки громкости, частоты и ADSR
indivVoiceAttr.volume.left = 0x1fff;
indivVoiceAttr.volume.right = 0x1fff;
indivVoiceAttr.pitch = 0x1000;
indivVoiceAttr.a_mode = SPU_VOICE_LINEARIncN;
indivVoiceAttr.s_mode = SPU_VOICE_LINEARIncN;
indivVoiceAttr.r_mode = SPU_VOICE_LINEARDecN;
indivVoiceAttr.ar = 0x0;
indivVoiceAttr.dr = 0x0;
indivVoiceAttr.sr = 0x0;
indivVoiceAttr.rr = 0x0;
indivVoiceAttr.sl = 0xf;
SpuSetVoiceAttr(&indivVoiceAttr);
Теперь для воспроизведения канала можно вызвать следующую функцию:
SpuSetKey(SPU_ON, SPU_0CH);
Остановить воспроизведение можно так:
SpuSetKey(SPU_OFF, SPU_0CH);
Напоследок напомню, что существует такой сайт, как http://www.psxdev.net там очень много полезной актуальной информации по разработке под PSX, есть активное сообщество. Если возникли вопросы, не стесняйтесь, пишите мне на почту: gecko0307@gmail.com, постараюсь помочь по мере своих возможностей.