Реактивное введение в программирование Game Boy Advance (часть 7 из 8)

Звук (Direct Sound)…



Оглавление


Часть 1: Инструментарий, основы, кнопки, таймеры
Часть 2: Пиксельные видеорежимы
Часть 3: Тайловые видеорежимы
Часть 4: Спрайты
Часть 5: Вращающиеся фоны и прозрачность
Часть 6: Прерывания, DMA
Часть 7: Звук (Direct Sound)
Часть 8: Сохранения

10. Звук (Direct Sound)


Звуковая система в GBA состоит из двух независимых частей. Первая — доставшийся в наследство от Game Boy и Game Boy Color так называемый «чиптюн» — один из представителей обширного класса аналогичных микросхем 8-битных компьютеров и консолей. Они представляют из себя несколько (порядка трёх-четырёх) независимо программируемых звуковых осцилляторов, выдающих сравнительно простые сигналы — прямоугольный, треугольный, синусоидальный, случайный (шумовой) и тому подобное. Конкретно семейство Game Boy имело 4 таких звуковых канала: 2 прямоугольных, 1 шумовой и 1 для низкокачественного 4-битного семплированного звука (заполнив встроенный буфер в 32 семпла произвольной формой сигнала можно было использовать его в зацикленном режиме как еще один осциллятор). Звуковой драйвер таких систем как правило садился на VBlank и регулярно обновлял параметры звучания этим осцилляторам по заданной программе. В результате быстрых подмен звучания можно было даже имитировать большее число одновременно звучащих независимых каналов, чем в системе на самом деле было. Вообще программирование чиптюна — это отдельная и очень большая песня, но я её в этом цикле уроков не буду затрагивать.

Я сосредоточусь только на второй составляющей звуковой подсистемы GBA, появившейся именно в нём — так называемом Direct Sound (не путать с одноименной технологией из DirectX компании Microsoft). Он из себя представляет 2 независимых канала (их называют «канал A» и «канал B») 8-битного PCM-звука с помощью которых можно обеспечить стереозвучание направив их вывод (опционально) в разные колонки (в самом GBA стереозвук может быть прослушан только в наушниках, внешний звук в самой консоли выводится только через один динамик).
Основой взаимодействия с каналами являются 32-битные порты ввода-вывода FIFO_A и FIFO_B (в описаниях их часто разносят на две 16-битных половинки, например FIFO_A_L и FIFO_A_H, так что я сделал и такие синонимы в коде тоже). Запись 32-битного значения в эти порты приводит к добавлению четырёх звуковых семплов во внутренний FIFO-буфер соответствующего аудиоканала. Семплы являются байтами со знаком — таким образом каналы наши имеют разрядность в 8 бит при этом первыми в очереди воспроизведения оказываются младшие байты записанного слова. Всего же внутренние буферы каждого канала могут хранить до 32-ух семплов.
Частоту воспроизведения аудиоканала задаёт один из двух таймеров — или TIMER0 или TIMER1 (это настраивается в порте управления Direct Sound). При переполнении заданного таймера аудиоканал будет доставать очередной байт/семпл из буфера и выдавать его на вывод, буфер при этом будет опустошаться по принципу FIFO. Так как таймеры начинают отсчёт со значения записанного в REG_TMXD и переполняются по достижению предела 16-битного счётчика (65536), то если мы хотим иметь частоту звукового вывода в Freq герц, то формула для вычисления этого начального значения отсчёта выглядит так:
REG_TMXD = 65536 — ((1<<24) / Freq), где
1<<24 (или 2^24 = 16777216) — системная тактовая частота GBA, таким образом формула предполагает, что таймер работает на ней (без делителей частоты). При этом несмотря на то, что частота таймера может быть настроена как угодно, но звуковая подсистема GBA интерполирует звук на свою внутреннюю частоту — 32768 Гц. Поэтому во первых — не имеет смысла пытаться выжать из GBA популярные ныне 44кГц, во вторых желательно выставлять частоту таймеров на значения делящие 32768 без остатка для достижения минимальных искажений звука. Так, например, для воспроизведения на частоте 16384 Гц затравочное значение для таймера должно быть выставлено в 64512, а для частоты 8192 Гц — в 63488.

Когда аудиоканал исчерпает свой FIFO-буфер, то он начнёт воспроизводить тишину (нулевое значение семплов), так что наша задача в первую очередь состоит в том, чтобы вовремя подавать в FIFO-буфер свежие данные так, чтобы он не успевал заканчиваться. Есть 2 решения этой проблемы:
а) Настроить следующий таймер на инкремент по переполнению предыдущего, задать ему затравочное значение в 65536 минус количество семплов, через которые надо вызвать прерывание пополнение буфера и собственно в этом прерывании дополнять буфер этим количеством семплов. Заодно в прерывании мы можем вычислять значения семплов на лету — например смешивая звуки разных источников, то есть производя микширование. Это действительно работает, но заметно перегружает процессор. Например, если заполнять FIFO-буфера на все 32 семпла и через 32 отсчёта вспомогательного таймера снова пополнять опустевший буфер, то при частоте вывода в 16 кГц прерывание будет вызываться целых 512 раз в секунду. Проблема тут в том, что переключение процессора в режим обработки прерывания само по себе занимает приличное количество инструкций и повторять его столь часто — пустая трата процессорного времени.
б) Во втором же методе процессор практически не участвует — это режим вывода посредством DMA. Каналы DMA1 и DMA2 в режиме DMA_START_SPECIAL начинают подпитывать буфера звукового чипа, как это было описано в предыдущей главе. DMA-каналу выставляются режимы DMA_REPEAT и DMA_START_SPECIAL, адрес назначения выставляется в FIFO_A или FIFO_B, а адрес источника на начало массива звуковых данных в памяти. Если так сделать, то звуковой канал при срабатывании по таймеру проверяет свой FIFO-буфер и если там осталось только 16 семплов или меньше, то он будит соответствующий DMA-канал и тот пересылает четыре 32-битных значения в соответствующий FIFO-порт. Настройки битности и размера передачи при этом игнорируются и адрес назначения остаётся без изменений. Этот метод существенно разгружает процессор и обладает одним только, но существенным недостатком — воспроизведён может быть только один звук за раз (и всего два, если зайдействовать оба канала), микшировать аппаратно без участия процессора звуковая система GBA не умеет.
Кроме того надо всегда иметь ввиду, что DMA-каналы блокируют как процессор так и DMA-каналы с большими номерами на время своей работы, поэтому следует аккуратно относится к временным промежуткам на которые может быть заблокировано воспроизведение звука во избежание щелчков и прочей порчи аудиопотока.

Итак, два имеющихся метода обладают существенными недостатками, но если соединить их вместе, то можно организовать уже вменяемый звуковой драйвер.
Принцип работы этого звукового драйвера будет заключаться в следующем: DMA-контроллер будет подкармливать звуковой чип из одного звукового банка небольшого размера. Вспомогательный таймер будет настроен так, чтобы вызывать прерывание в момент когда достигается конец этого буфера. Код в прерывании будет переключать DMA-контроллер на воспроизведение второго банка, то есть будет производить переключение банков. После этого ставший неактивным банк заполняется свежими данными из источников звука — то есть производится микширование. Процесс этот зациклен бесконечно. Размеры банков выставляются так, чтобы программное прерывание вызывалось разумное число раз в секунду — не больше сотни, но и не сильно меньше — этого достаточно часто, чтобы без паузы начинать проигрывать нужные фрагменты звука при наступлении внутриигровых событий и досточно редко, чтобы доля затрат процессора на активацию прерываний стала незначительна. Микшер будет простенький — всё что он будет уметь делать — это воспроизводить несколько звуковых фрагментов с изменяемым параметром громкости у каждого и при этом иметь еще общий параметр громкости (master volume). Воспроизводит GBA 8-битные семплы, поэтому звуковые данные тоже будут массивами байт со знаком. Cмешивание нескольких звуков производится простым суммированием значений самплов по физическому принципу суперпозиции, при этом легко получить переполнение байт. Поэтому предварительно звуковые фрагменты будут смешиваться/складываться в 16-битном буфере и уже оттуда подаваться в 8-битный буфер вывода. Здесь нам и пригодится параметр громкости master volume — он по совместительству будет и «делителем» суммарной громкости звуков для их правильного баланса без переполнения за границы байта. Например воспроизводя (суммируя) два звука и выставив master volume в 0.5 (1/2) мы обеспечим невыход суммарной громкости за границы байта, по сути выводя среднее арифметическое. Однако, как мы помним из главы про вращающиеся спрайты, вещественные числа на GBA — слишком дорогое удовольствие, поэтому мы поступим с ними так же как поступали и там — сделаем их в формате с фиксированной точкой fixed(8:8). В этом формате будут задаваться все громкости для микшера.
Но прежде чем реализовать программу со звуковым драйвером опишем несколько новых техник, которые нам пригодятся.

10.1 VBlankIntrWait()


Теперь, когда мы вовсю используем прерывания, мы можем реализовать лучший метод дожидаться VBlank в каждом кадре, нежели крутить бесконечные циклы в ожидании нужных диапазонов значений REG_VCOUNT. Последний способ, как нетрудно догадаться, плохо подходит для портативной консоли, которая должна бережно относится к потребляемый энергии и не загружать ненужной работой процессор. Идея способа давно известна и проста — отправить процессор в спящий режим пока не наступит нужное прерывание. Код выполняющий такую функцию есть в консоли уже в готовом виде в прошивке BIOS, поэтому заодно нам надо научится вызывать процедуры из BIOS (как минимум без параметров в этом случае). Добавим следующий код в gba_defs.h:


// Макрос вызова процедуры из BIOS (системный вызов) без параметров
#if defined ( __thumb__ )
#define SystemCall(N) __asm("SWI "#N"\n" ::: "r0","r1","r2","r3")
#else
#define SystemCall(N) __asm("SWI "#N"<<16\n" ::: "r0","r1","r2","r3")
#endif

// Вызвать процедуру BIOS, которая останавливает процессор
// до наступления прерывания по VBlank. 
inline void VBlankIntrWait()    { SystemCall( 5 ); };

// Флаг BIOS для обеспечения работы VBlankIntrWait()
#define BIOS_WAIT_FLAG      (*((volatile u16 *) 0x3007FF8))

// "Барьер памяти" (memory barrier) для компилятора (см. ниже 10.2)
#define MEMORY_BARRIER      asm volatile("": : :"memory")


Итак, нужная нам функция в BIOS это функция номер 5. Дело в том, что функции из BIOS вызываются через механизм программных прерываний, которые имеют порядковые номера. Используется встроенный ассемблер и макрос разворачивается в зависимости от режима ARM/Thumb текущего модуля трансляциии. Чтобы функция VBlankIntrWait() заработала (а не завешивала систему) нужно выполнить ряд условий: прерывание VBlank должно и испускаться и перехватываться, а процедура обработки прерывания обязана на выходе при обновлении REG_IF наложить по битовому OR то же самое значение, что она в него записывает во флаг BIOS:
BIOS_WAIT_FLAG |= value;
Так процедура в BIOS сможет понять, что прерывание действительно вызывалось и вызывалось именно нужное прерывание. Использование VBlankIntrWait() для синхронизации основной программы с VBlank и реализации VSync предпочтительно.

10.2 Барьеры памяти (memory barriers)


Довольно значимой проблемой для взаимодействия кода программы с кодом обработчиком прерываний является оптимизирующий компилятор, который ничего про прерывания не знает и может сделать недопустимые для нас предположения. Функцию обработчика прерываний эта проблема касается в меньшей степени — так как мы не объявляем её как static, то сохраняется возможность вызова её откуда то из другого модуля трансляции, а значит компилятор вынужден честно считывать все переменные из памяти при входе в функцию и записывать их по выходу — а ничего другого тут и не требуется. Но всё становится сложнее с кодом основной программы, который крутится в бесконечном игровом цикле в main. Так как в нём даже упоминаний о вызове обработика прерываний нет, а цикл работы бесконечно зациклен, то оптимизирующий компилятор может предположить, что даже если что-то записывается в переменные в памяти, то оно не может быть нигде больше использовано, так как ничего другого больше и не выполняется, а значит всё это «тлен» и может быть беспощадно выкинуто из итогового образа программы. Первой линией защиты от излишних оптимизаций служит ключевое слово volatile. Причём если вдруг потребуются такие гарантии с не-volatile переменной, то можно ссылку на неё преобразовать в volatile-ссылку и добиться эффекта «на месте». Однако в GNU C/C++ существует еще один (compiler-specific) способ — создать так называемый «барьер памяти» (memory barrier) для компилятора. Как это делается показано в макросе MEMORY_BARRIER в (10.1).
Такой «барьер» заставляет компилятор сохранить в память значения переменных (если они кешированы в регистрах) до барьера и не делать предположений о содержимом памяти после барьера (то есть считать их кешированные значения в регистрах устаревшими). Это позволяет оптимизатору не ломать логику программы с активным использованием прерываний, выкидывая куски кода которые по его мнению не производят полезных действий. Такой барьер действует только на уровне компилятора, но не процессора. Существуют еще барьеры памяти для процессора, так как его конвеер может переставлять уже порядок исполнения независимых инструкций прямо во время выполнения программы, но так как GBA является однопроцессорной системой, то в нём это не имеет никакого значения.

10.3 Интерфейс взаимодействия с Direct Sound


Дополним описанием портов ввода-вывода Direct Sound и их флагами файл gba_defs.h:


// *** FIFO-буферы для двух каналов (A и B) DirectSound .
// 32-битные значения в портах FIFO_A и FIFO_B принимают четыре сампла
// (байты со знаком), при этом первыми проигрываются младшие байты.
// FIFO-буфера имеют размер 8 32-битных значений или 32 сампла (байта).
#define REG_FIFO_A_L        (*((volatile u16 *) 0x40000a0))
#define REG_FIFO_A_H        (*((volatile u16 *) 0x40000a2))
#define REG_FIFO_A      (*((volatile u32 *) 0x40000a0))
#define REG_FIFO_B_L        (*((volatile u16 *) 0x40000a4))
#define REG_FIFO_B_H        (*((volatile u16 *) 0x40000a6))
#define REG_FIFO_B      (*((volatile u32 *) 0x40000a4))

// *** Регистр общего управления PSG (осцилляторами)
// мной пока не используется и его флаги я не выписываю.
#define REG_SOUNDCNT_L      (*((volatile u16 *) 0x4000080))

// *** Регистр управления DirectSound и общей громкостью PSG.
#define REG_SOUNDCNT_H      (*((volatile u16 *) 0x4000082))
// Биты 0-1: громкость звуковых осцилляторов (PSG).
#define SOUND_PSG_VOL_25    0x0000
#define SOUND_PSG_VOL_50    0x0001
#define SOUND_PSG_VOL_100   0x0002
// Бит 2: громкость канала A (50% или 100%)
#define SOUND_A_VOL_50      0x0000
#define SOUND_A_VOL_100     0x0004
// Бит 3: громкость канала B (50% или 100%)
#define SOUND_B_VOL_50      0x0000
#define SOUND_B_VOL_100     0x0008
// Бит 8: вывод канала A в правый динамик
#define SOUND_A_RIGHT       0x0100
// Бит 9: вывод канала A в левый динамик
#define SOUND_A_LEFT        0x0200
// Бит 10: выбор подпитывающего таймера канала A (0-ой или 1-ый)
#define SOUND_A_TIMER0      0x0000
#define SOUND_A_TIMER1      0x0400
// Бит 11: при установке в 1 очищает FIFO-буфер
#define SOUND_A_RESET       0x0800
// Бит 12: вывод канала B в правый динамик
#define SOUND_B_RIGHT       0x1000
// Бит 13: вывод канала B в левый динамик
#define SOUND_B_LEFT        0x2000
// Бит 14: выбор подпитывающего таймера канала B (0-ой или 1-ый)
#define SOUND_B_TIMER0      0x0000
#define SOUND_B_TIMER1      0x4000
// Бит 15: при установке в 1 очищает FIFO-буфер
#define SOUND_B_RESET       0x8000


10.4 Программа конвертации WAV-файлов в исходники С/С++


Для воспроизведения звуков нам нужны данные с ними. Здесь мы продолжим использовать простейший трюк с подключением таких данных как просто массивов в исходном коде. Для этого надо написать программу конвертирующую звук в нужный нам формат. Сами PCM-звуки на GBA являются 8-битными, но так как порции передаваемые по DMA являются 4-байтными, то для простоты и гарантий выравнивания в памяти мы будем хранить их как массивы unsigned int (32-битные элементы). Напишем программу, которая переводит звук в формате WAVE, моно, 16-бит в исходный код на С/С++. Это программа не для GBA, а для ПК, то есть компилировать её надо компилятором для него же, что я оставляю за пределами этого руководства:


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <vector>
#include <algorithm>
#include <string>

void mksp( int sp )
{
	for ( int i = 0; i < sp; i++ )
	{
		putchar( ' ' );
	};
};

void dump_riff( FILE *f, char *name, unsigned int pos, unsigned int size, int level )
{
	mksp( 4 * level );
	if ( pos )
	{
		printf( "%s SIZE: %u\n", name, size );
	}
	else
	{
		printf( "BLOCK: %s SIZE: %u\n", name, size );
	};
};

typedef void (*riff_callback)( FILE *f, char *name, unsigned int pos, unsigned int size, int level );

unsigned int enum_riff_chunks( FILE *f, int level, unsigned int pos, riff_callback callback )
{
	char name[ 5 ], lname[ 5 ];
	unsigned int size, psize, subsize;

	fseek( f, pos, SEEK_SET );
	fread( &name, 4, 1, f );
	name[ 4 ] = 0;
	fread( &size, 4, 1, f );
	psize = (size & 1) ? size + 1 : size; // padded size

	if ( (strcmp( name, "RIFF" ) == 0) || (strcmp( name, "LIST" ) == 0) )
	{
		fread( &lname, 4, 1, f );
		lname[ 4 ] = 0;

		callback( f, lname, 0, size, level );

		subsize = 0;
		while ( subsize < psize - 4 )
		{
			subsize += enum_riff_chunks( f, level + 1, pos + 12 + subsize, callback );
		};
	}
	else
	{
		callback( f, name, pos + 8, size, level );
	};
	return psize + 8;
};

std::vector<short int> wavdata;

void read_wave( FILE *f, char *name, unsigned int pos, unsigned int size, int level )
{
	if ( pos && (strcmp( name, "data" ) == 0) )
	{
		printf( "FOUND WAVE DATA AT %u SIZE: %u\n", pos, size );
		wavdata.resize( size / 2 );
		fread( &wavdata[ 0 ], size, 1, f );
	}
	else
	{
		//printf( "Ignoring %s section at %u with size %u...\n", name, pos, size );
	};
};

int main( int argc, char *argv[] )
{
	if ( argc <= 2 )
	{
		printf( "Usage: wav2incl.exe input.wav [+]output wavname\n" );
		return 0;
	};
	FILE *f = fopen( argv[ 1 ], "rb" );
	if ( f )
	{
		enum_riff_chunks( f, 0, 0, dump_riff );
		enum_riff_chunks( f, 0, 0, read_wave );
		fclose( f );
		// write it to output file...
		
		std::string outname = argv[ 2 ];
		const char *mode = "wt";
		if ( outname[ 0 ] == '+' )
		{
			outname = argv[ 2 ] + 1;
			mode = "at";
		};

		f = fopen( outname.c_str(), mode );
		if ( f )
		{
			int xmax = 0, xmin = 0;
			const char *wavname = "wavname";
			if ( argc >= 4 )
			{
				wavname = argv[ 3 ];
			};
			fprintf( f, "const int %s_size = %i;\n", wavname, wavdata.size() );
			fprintf( f, "const unsigned int %s[] = {", wavname );
			for ( int i = 0; i < wavdata.size(); i += 4 )
			{
				if ( i ) fputs( ",", f );
				if ( (i % (4 * 8)) == 0 ) fputs( "\n", f );
				unsigned int data = 0;
				for ( int j = 0; j < 4; j++ )
				{
					int xr = wavdata[ (i + j < wavdata.size()) ? (i + j) : (wavdata.size() - 1) ];
					//printf( "wavdata[%i]=%i\n", i + j, xr );
					signed char x = xr / 256;
					xmax = std::max( (int)x, xmax );
					xmin = std::min( (int)x, xmin );
					data = (data >> 8) | (x << 24);
				};
				fprintf( f, "0x%x", data );
			};
			fputs( "};\n\n", f );
			fclose( f );
			printf( "Processed %u samples, min: %i max: %i.\n", wavdata.size(), xmin, xmax );
		}
		else
		{
			printf( "Cannot create file with name '%s'!\n", argv[ 2 ] );
		};
	}
	else
	{
		printf( "Cannot open file with name '%s'!\n", argv[ 1 ] );
	};
	return 0;
};


Входным файлом для этой программы должен быть wav-файл, моно, PCM 16 бит (без шифрования) с нужной нам частотой дискретизации. Сама программа для краткости не анализирует заголовки WAVE и не производит никаких преобразований форматов и частот — поэтому звук сразу должен быть правильным, иначе вместо результата получится шум. Создавать нужные звуки можно, например, с помощью бесплатной программы Audacity. Предварительно в ней надо выставить частоту дискретизации всего проекта в 16384, потом загрузив (и по необходимости отредактировав) звук активируем следующие пункты меню: «дорожки->преобразовать звук в монофонический», «дорожки->сменить частоту дискретизации...» (выставим значение 16384 для нашего теста), снова проверим чтобы настройки проекта тоже были 16384 (если нет — то поменяем их опять) и сделаем «файл->экспорт аудио...», выбрав там формат «Microsoft WAV / PCM 16».
Выполняя команды в командной строке:
wav2incl.exe sound_data1.wav 10_sound_data1.cpp soundData1
создадим 2 файла 10_sound_data1.cpp и 10_sound_data2.cpp со звуковыми данными, которые будут представлять из себя массивы с именами soundData1 и soundData2 соответственно.

10.4 Пример звукового драйвера

И вот наконец то мы готовы написать пример программы со звуковым драйвером.
Он будет работать на частоте 16 кГц (это настраивается) и выводить только 1 звуковой канал в оба стереодинамика одновременно.
Звуковые данные будут приниматься через однонаправленный связный список структур SoundSource.
10_sound.cpp:

#include "string.h"
#include "gba_defs.h"

#include "10_sound_data1.inc"
#include "10_sound_data2.inc"

// Частота воспроизведения звукового драйвера в герцах
const int sndFreqHz = 16384;
// Частота с которой будет вызваться прерывание для звука.
// Должно делить нацело SndFreqHz и желательно не превышать 100
const int sndIntrHz = 64;
// Необходимый размер звуковых банков
const int sndBankSize = sndFreqHz / sndIntrHz;

// Два звуковых банка, а так же 16-битный банк микшера
i8 sndBank0[ sndBankSize ];
i8 sndBank1[ sndBankSize ];
i16 sndMixBank[ sndBankSize ];

// Режимы воспроизведения источников звука
// Off - выкл, Once - однократно, Repeat - зацикленно (бесконечно)
enum SoundMode
{
	SoundModeOff, SoundModeOnce, SoundModeRepeat
};

// Источник звука для звукового драйвера
struct SoundSource
{
	int mode;		// Режим воспроизведения
	int volume;		// Громкость fixed(:8), 0x0100 как 1.0f
	const u32 *start;	// Начало аудиоданных
	const u32 *end;		// Конец аудиоданных (следующий-за-последним)
	const u32 *cur;		// Текущий к воспроизведению фрагмент
	SoundSource *next;	// Следующий источник звука в списке
};

// Указатель на первый источник звука (голова однонаправленного списка)
SoundSource *volatile sndFirstSource = NULL;
// Номер текущего воспроизводящегося банка
volatile u8 sndCurBank = 0;
// Основная громкость звуков (master volume), fixed(8:8), 0x0100 как 1.0f
volatile i16 sndVolume = 0x0080;

// Процедура таймера для драйвера звука.
// Должна вызываться по таймеру, когда DMA уже считал текущий буфер вывода,
// чтобы переключить его на другой и заполнить текущий свежими данными.
void soundDriverTimer()
{
	// Меняем рабочий банк (через временную переменную)
	u8 curBank = 1 - sndCurBank;
	sndCurBank = curBank;
	// Переключаем DMA-канал на другой звуковой банк
	REG_DMA1CNT_H = 0; // выключим канал, чтобы обновить его параметры
	// Переключим источник данных
	REG_DMA1SAD = curBank ? (u32) sndBank1 : (u32) sndBank0;
	// Включим DMA-канал и тем самым применим новые параметры
	REG_DMA1CNT_H = DMA_SRC_INCR | DMA_REPEAT | DMA_START_SPECIAL | DMA_ENABLE;
	// Далее работаем с банком, который начнёт воспроизводится со следующего прерывания
	i8 *workBank = curBank ? sndBank0 : sndBank1;
	// Заполняем звуковой банк тишиной...
	memset( (void *) sndMixBank, 0, sizeof( sndMixBank ) );

	// Смешиваем источники звука в 16-битный банк микшера
	i16 *mixPtr;	// указатель в буфер микшера
	i8 *chPtr;	// указатель на 8-битные данные
	// Обходим список источников звука
	SoundSource *workSource = sndFirstSource;
	while ( workSource )
	{
		if ( workSource->mode != SoundModeOff )
		{
			mixPtr = sndMixBank;
			chPtr = (signed char *) workSource->cur;
			for ( int i = 0; i < sndBankSize / 4; i++ )
			{
				if ( workSource->cur >= workSource->end )
			 	{	
			 		// При достижении конца данных источника...
			 		if ( workSource->mode == SoundModeOnce )
			 		{
			 			// ...у однократных заканчиваем воспроизведение
						workSource->mode = SoundModeOff;
						workSource->cur = workSource->start;
						break;
			 		}
			 		else
			 		{
			 			// ...у бесконечных возвращаемся в начало
			 			workSource->cur = workSource->start;
			 			chPtr = (signed char *) workSource->cur;
			 		};
				};
				// Так как volume это fixed(:8), то результат умножения надо
				// сдвинуть вправо на 8 бит, чтобы он остался целым числом
				*mixPtr++ += ((*chPtr++ * workSource->volume) >> 8);
				*mixPtr++ += ((*chPtr++ * workSource->volume) >> 8);
				*mixPtr++ += ((*chPtr++ * workSource->volume) >> 8);
				*mixPtr++ += ((*chPtr++ * workSource->volume) >> 8);

				workSource->cur++;
			};
		};
		workSource = workSource->next;	// переходим к следующему источнику
	};

	// Выводим намикшированное в 16-битном банке в банк вывода
	i16 vol = sndVolume; // закешируем volatile переменную
	mixPtr = sndMixBank;
	chPtr = workBank;
	for ( int i = 0; i < sndBankSize; i++ )
	{
		*chPtr++ = ((*mixPtr++ * vol) >> 8);
	};
};

// Обработчик прерываний.
// Атрибут GCC __target__ ("arm") переключает компилятор в
// создание кода функции в режиме Arm даже внутри модуля
// в котором режим компиляции по умолчанию - Thumb.
void __attribute__ ((__target__ ("arm"))) interruptHandler(void)
{
	// Выключим прерывания
	REG_IME = 0;
	// Запомним входной флаг
	u16 enter_IF = REG_IF;

	// Проверяем, что прерывание равно INT_TIMER1
	if ( enter_IF & INT_TIMER1 )
	{
		// Вызываем процедуру обработки драйвера звука
		// (заодно полезно этим переключиться в режим Thumb).
		soundDriverTimer();
	};
	// Сбросим входной флаг (записью 1!)
	REG_IF = enter_IF;
	// Обновим флаг ожидания в BIOS для работы VBlankIntrWait()
	BIOS_WAIT_FLAG |= enter_IF;
	// Включим прерывания
	REG_IME = 1;
};

// Инициализация звукового драйвера
void soundDriverInit()
{
	// Предварительно отключим всё касающееся звука
	// Выключаем реакцию на прерывание таймера 1
	REG_IE &= ~INT_TIMER1;
	// Выключаем звуковой чип
	REG_SOUNDCNT_X = 0;
	// Выключаем DMA1
	REG_DMA1CNT_H = 0;
	// Выключаем таймеры
	REG_TM0CNT = 0;
	REG_TM1CNT = 0;

	// Очищаем звуковые буфера
	memset( (void*) sndBank0, 0, sndBankSize );
	memset( (void*) sndBank1, 0, sndBankSize );

	// Сбрасываем первый источник звука и текущий банк
	sndFirstSource = NULL;
	sndCurBank = 0;

	// Канал A на 100% громкости и в оба стереоканала, сбрасываем 
	// ему внутренний буфер и привязываем воспроизведение к первому таймеру
	REG_SOUNDCNT_H =	SOUND_A_VOL_100 |
				SOUND_A_LEFT | SOUND_A_RIGHT |
				SOUND_A_TIMER0 | SOUND_A_RESET;
	// Включаем звуковой чип
	REG_SOUNDCNT_X =	SOUND_ENABLE;
    			
    	// Подготовимся к обработке прерываний по опустошению буферов
	REG_IE |= INT_TIMER1;			// Включим реакцию на INT_TIMER1

	// Настраиваем канал DMA1 на подпитывание звуковых данных в FIFO_A
	REG_DMA1DAD = (u32) & REG_FIFO_A;
	REG_DMA1SAD = (u32) sndBank0;
	REG_DMA1CNT_H = DMA_SRC_INCR | DMA_REPEAT | 
			DMA_START_SPECIAL | DMA_ENABLE;

	// Настраиваем служебный таймер на прерывание для 
	// переключения звуковых буферов
	REG_TM1D	= 65536 - sndBankSize;
	REG_TM1CNT	= TIMER_ENABLE | TIMER_OVERFLOW | TIMER_IRQ_ENABLE;

	// Запускаем таймер-драйвер звукового канала FIFO_A
	REG_TM0D	= 65536 - ((1 << 24) / sndFreqHz);
	REG_TM0CNT	= TIMER_ENABLE;

	// Восемь "затравочных" семплов для синхронизации 
	// всех очередей (под вопросом)
	REG_FIFO_A = 0;
	REG_FIFO_A = 0;
};

// Для реализации разных реакций на нажатие кнопок (событие) 
// и удерживание их (состояние) запомним предыдущее и текущее и состояния
int prevKeys, curKeys = 0;

// Обновление предыдущего и текущего состояния кнопок.
// Эту функцию следует вызывать 1 раз за VSync или наподобие того.
void readKeys()
{
	prevKeys = curKeys;
	curKeys = ~REG_KEYS;
};

// Зажата ли (состояние) кнопка
inline int keyIsDown( int key )
{
	return curKeys & key;
};

// Была ли кнопка нажата с момента последнего вызова read_keys.
// То есть для активации каждого события кнопку надо зажимать и отжимать.
inline int keyIsPressed( int key )
{
	return (curKeys & key) && !(prevKeys & key);
};

// Для точечного обновления или чтения не-volatile переменных, используемых
// или обновляемых в прерываниях можно "обернуть" их признаком volatile
// посредоством этой функции, например:
// vlt( a ) = vlt( b );
// Для массовых обновлений данных следует использовать MEMORY_BARRIER
template< class T >
inline volatile T &vlt( T &val )
{
	return val;
};

int main(void)
{
	// Инициализируем палитру так, чтобы 8-битный индекс цвета 
	// совпадал с цветовой маской BBGGGRRR
	for ( unsigned int i = 0; i < 256; i++ )
	{
		BGR_PALETTE[ i ] = RGB( (i & 7) << 2, (i & 56) >> 1, (i & 192) >> 3 );
		SPR_PALETTE[ i ] = RGB( (i & 7) << 2, (i & 56) >> 1, (i & 192) >> 3 );
	};

	// Очистим видеопамять
	memset( (void*) VID_RAM_START, 0, VID_RAM_SIZE );

	// Включаем MODE_0
	REG_DISPCNT = MODE_0;

	// Инициализируем звук
	soundDriverInit();

	REG_INTERRUPT = &interruptHandler;	// Установим обработчик

	// Создаём и настраиваем источники звука для драйвера
	SoundSource sndSrc[ 5 ];

	for ( int i = 0; i < 4; i++ )
	{
		sndSrc[ i ].mode = SoundModeOff;	// Изначально выключены
		sndSrc[ i ].volume = 0x0100;	// Громкость в 100%
		sndSrc[ i ].start = soundData1;	// Первый звуковой фрагмент
		// Как и у итераторов конец это элемент следующий за последним
		sndSrc[ i ].end = soundData1 + sizeof( soundData1 ) / sizeof( soundData1[ 0 ] );
		sndSrc[ i ].cur = sndSrc[ i ].start;	// Текущий семпл - первый
		sndSrc[ i ].next = &sndSrc[ i + 1 ];	// Связный список
	};

	// Громкость нулевого звука в 1/2
	sndSrc[ 0 ].volume = 0x0080;

	// Третий звук воспроизводит  второй звуковой фрагмент
	sndSrc[ 3 ].start = soundData2;
	sndSrc[ 3 ].end = soundData2 + sizeof( soundData2 ) / sizeof( soundData2[ 0 ] );
	sndSrc[ 3 ].cur = sndSrc[ 3 ].start;
    
	// Четвертый звук будет воспроизводить второй звуковой фрагмент
	sndSrc[ 4 ].mode = SoundModeOff;
	sndSrc[ 4 ].volume = 0x0100;
	sndSrc[ 4 ].start = soundData2;
	sndSrc[ 4 ].end = soundData2 + sizeof( soundData2 ) / sizeof( soundData2[ 0 ] );
	sndSrc[ 4 ].cur = sndSrc[ 4 ].start;
	sndSrc[ 4 ].next = NULL;	// Конец связного списка

	// Настраиваем голову связного списка источников звука
	sndFirstSource = sndSrc;

	// Включим генерацию прерываний по VBlank для работы VBlankIntrWait()
	REG_DISPSTAT |= VBLANK_IRQ_ENABLE;
	// Включим реакцию на прерывания INT_VBLANK для работы VBlankIntrWait()
	REG_IE |= INT_VBLANK;

	// Бесконечный цикл
	while ( true )
	{
		// Дождёмся выхода в VBLANK новым, энергосберегающим способом
		VBlankIntrWait();

		readKeys();	// Обновляем состояние кнопок

		// Входим в режим обновления данных для прерывания.
		// Обработка прерываний выключается, а компилятору
		// выставляется memory barrier, который заставляет его
		// сбросить значения измененых переменных в память до барьера,
		// а после барьера не предполагать в рамках оптимизации, что
		// их значения в памяти остались неизменными. Это устраняет необходимость
		// метить все данные как volatile при массовых их обновлениях 
		// для прерываний и предотвращает оптимизатор компилятора от
		// ложного мнения, что бесконечный цикл ниже не производит
		// никакой полезной работы и его инструкции можно выкидывать.
		// Такие обновления следует делать как можно быстрее и реже.
		REG_IME = 0;
		MEMORY_BARRIER;

		// Нажатия на D-Pad запускает первые четыре звука
		// на однократное воспроизведение
		if ( keyIsPressed( KEY_UP ) )
		{
			sndSrc[ 0 ].mode = SoundModeOnce;
		};

		if ( keyIsPressed( KEY_RIGHT ) )
		{
			sndSrc[ 1 ].mode = SoundModeOnce;
		};

		if ( keyIsPressed( KEY_DOWN ) )
		{
			sndSrc[ 2 ].mode = SoundModeOnce;
		};

		if ( keyIsPressed( KEY_LEFT ) )
		{
			sndSrc[ 3 ].mode = SoundModeOnce;
		};

		// START запускает музыкальный фрагмент
		// на бесконечное воспроизведение
		if ( keyIsPressed( KEY_START ) )
		{
			sndSrc[ 4 ].mode = SoundModeRepeat;
			sndSrc[ 4 ].cur = sndSrc[ 4 ].start;
		};

		// SELECT выключает музыкальный фрагмент
		if ( keyIsPressed( KEY_SELECT ) )
		{
			sndSrc[ 4 ].mode = SoundModeOff;
		};

		// A запускает 2 звука одновременно
		if ( keyIsPressed( KEY_A ) )
		{
			sndSrc[ 0 ].mode = SoundModeOnce;
			sndSrc[ 1 ].mode = SoundModeOnce;
		};

		// Заканчиваем обновление данных для прерывания.
		// Выставляется еще один memory barrier и включаются прерывания
		MEMORY_BARRIER;
		REG_IME = 1;

		// Общая громкость (master volume) выставляются в 1/2,
		// а зажатие каждой из кнопок L и/или R уменьшает
		// её в 2 раза (то есть до 1/4 суммарно)
		// Так как SndVolume объявлена как volatile нет 
		// необходимости выставлять memory barriers и т.д.
		sndVolume = 0x0080;
		if ( keyIsDown( KEY_R ) )
		{
			sndVolume /= 2;
		};
		if ( keyIsDown( KEY_L ) )
		{
			sndVolume /= 2;
		};
	};

	return 0;
}


Файл build_10_sound.bat будет выглядеть следующим образом:


wav2incl.exe sound_data1.wav 10_sound_data1.inc soundData1
wav2incl.exe sound_data2.wav 10_sound_data2.inc soundData2

@SET MODULES=
@set PROGNAME=10_sound

@call build_gba.bat


Звуковой драйвер — самое сложное с чем мы в этих уроках столкнулись.
Если вы возьмёте уже скомпилированный мной пример, то кнопки ВВЕРХ/ВПРАВО/ВНИЗ каждая запускает независимый голосовой отсчёт 1-2-3-4-5, кнопка «A» запускает сразу 2 звука (таким образом они будут звучать как один удвоенной громкости), START включает фоновый музыкальный фрагмент, SELECT выключает его, ВЛЕВО включает его же на однократное воспроизведение, а кнопки L и R приглушают общую громкость наполовину будучи зажатыми (то есть в 4 раза, если зажать их обе).
Звуковой драйвер немного куцый, но вы можете дополнять и дорабатывать его как пожелаете.
Итак, перед нами осталась последняя (и совсем несложная задача) — сохранять состояние игр в постоянную память в картридже и это руководство будет завершено.

Продолжение...

6 комментариев

avatar
может я невнимателен, но — почему просто не отрезать заголовок вавки и потом читать их в массив?
  • VBI
  • 0
avatar
Всмысле слить в одну статью? Ну я начинал делать на жж где есть ограничение на размер поста и мне кажется тут оно тоже должно быть.
avatar
заголовок Wav ;)
avatar
Ааа… У WAVE не заголовок — там RIFF-формат у которого замороченная древовидная структура может быть со всякими опциональными блоками типа названия авторов и тому подобное — сам код парсящий вынужден быть потому рекурсивным в обработке этих блоков.
avatar
я делал проще — отрезал голову, тело на сковороду в ковокс и вперёд :))
avatar
Ну такое может дать сбой на том или ином файле/редакторе. Тот же Audacity старается вставить блок с информацией об исполнителе и т.п., оно либо встанет перед и исказит заголовок, либо после и исказит данные.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.