Разработка для 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, постараюсь помочь по мере своих возможностей.


Copyright © 2008-2021 Тимур Гафаров и соавторы. Доступно по СС BY-NC-SA 3.0.