Моё почтение, господа. Не так давно я написал статью, в которой рассказывал о том, как создать класс для проигрывания звука и видео. В течение последних несколько недель, несмотря на сессию, мне приходилось регулярно использовать этот класс. И, честно признаюсь, я нашел его не очень удобным. Спроектирован он был правильно, и если в программе задействовалось немного музыкальных файлов, проблем с их использованием не возникало, но при большом количестве ресурсов управлять ими становилось проблематично. Поэтому этот урок будет посвящен написанию надстройки (объектно-ориентированной, разумеется), упрощающей нам жизнь.

    Итак, что же мы хотим? Мы хотим класс, который инкапсулировал бы бОльшую часть рутинного кода (инициализация и управление ресурсами), предоставлял удобный интерфейс + некоторые специфические возможности по проигрыванию. Вот вроде бы и все!

    Инициализировать сотни объектов ручками как-то тоскливо, поэтому создадим функцию, принимающую в качестве параметра путь к директории в которой лежит файл со списком ресурсов для загрузки. Вот она:

void			cMediaManager::LoadMedias( HWND hWnd , char * path ){
//--------------1--------------
	char		rel_path[256];
	memcpy( rel_path , path , 256 );

	strcat( path , "media_enum.log" );

	FILE		*f_stream = fopen( path , "rt" );

	if( !f_stream ){
		char			error_message[1000];
		strcpy( error_message , "Невозможно загрузить описатель медиа файлов : " );
		strcat( error_message, path );
		MessageBox( 0 , error_message , "Ошибка" , 0 );
		exit( 0 );
	}

	char			directive[ 256 ];

	for( ; EOF != fscanf( f_stream , "%s" , directive ) ; ){
//--------------2--------------
		if( !strcmp( directive , "set_autoselection" ) ){
			fscanf( f_stream , "%s" , directive );
			RSelection[ GetCursor( directive ) ] = true;
		}
//--------------3--------------
		else{
			//название
			sMediaAlias			MediaAlias;
			memcpy( &MediaAlias , directive , 256 );
			MediaAliases.AddEnd( MediaAlias );

			//читаем название файла
			fscanf( f_stream , "%s" , directive );
			memcpy( path , rel_path , 256 );
			strcat( path , directive );

			//читаем файл
			Medias[ Medias.Cursor++ ].LoadFile( hWnd , path );

			Medias[ Medias.Cursor - 1 ].SetNotifyWindow( hWnd , WM_DSNOTIFICATION , Medias.Cursor - 1 );

			StartingTime.AddEnd( NULL );

			RSelection.AddEnd( false );
		}
	}
}

    А вот и класс (пока что его часть):

struct		sMediaAlias{char Alias[256];};
class		cMediaManager{
	cArray		MediaAliases;

	cArray		Medias;
public:
	void				LoadMedias( HWND , char * );
};

    Подробнее об этой функции: в первом блоке просто проверяем, существует ли файл (media_enum.log) со списком всех ресурсов или нет. Если существует, то в блоке 3 читаем и сохраняем в массиве MediaAliases название ресурса (иногда бывает удобно в качестве идентификатора использовать название ресурса, а не ID), затем читаем путь к ресурсу, грузим сам ресурс и указываем, какому окну будет посылаться сообщения от потоков воспроизведения. При этом в качестве lParam будет передаваться курсор на этот медиа файл, что бы мы знали, от какого ресурса мы получили это сообщение. А вот и сам обработчик событий:

void			cMediaManager::HandleEvent( long lParam ){
	if( Medias.Cursor ){
		LONG evCode, evParam1, evParam2;
		while( SUCCEEDED( Medias[ lParam ].MediaEvent->GetEvent( &evCode, 
				( LONG_PTR * ) &evParam1 , ( LONG_PTR *) &evParam2 , 0 ) ) ){
			Medias[ lParam ].MediaEvent->FreeEventParams( evCode , evParam1 , evParam2 );

			if( EC_COMPLETE == evCode ){

				Medias[ lParam ].StopFile();

				StartingTime[ lParam ] = NULL;
			}
		}
	}
}

    Как вы, наверное, заметили, ресурсы должны лежать в той же директории что и media_enum.log. О значении же блока 2 я расскажу немного позже.

    Теперь неплохо бы получить доступ к загруженным файлам:

cMediaPlayer	&cMediaManager::operator[]( char * m_name ){
	for( int i( 0 ) ; i < Medias.Cursor ; i++ ){
		if( !strcmp( MediaAliases[ i ].Alias , m_name ) )
			return( Medias[ i ] );
	}
	char			e_message[1000];
	strcpy( e_message , "Не найден медиа ресурс. " );
	strcat( e_message , m_name );
	MessageBox( 0 , e_message , "Ошибка." , 0 );
}

cMediaPlayer	&cMediaManager::operator[]( int i ){
	if( i < 0 || i >= Medias.Cursor ){
		MessageBox( 0 , "Некорректный курсор. cMediaPlayer&cMediaPlayer::operator[]( int i )" , "Ошибка." , 0 );
		exit( 0 );
	}
	return( Medias[ i ] );
}

    Тут вроде бы все просто – в первом случае обращаемся к файлу по имени (не самый быстрый способ), а во втором по курсору. Уже можно проигрывать музыку:

    MediaManager[“some_file”].PlayFile();

    К сожалению DirectShow не позволяет одновременно проигрывать разные участки одного буфера, хотя это иногда необходимо. Я пока не могу предложить Вам ничего кроме лобового решения: создать несколько буферов и при необходимости проигрывать их одновременно. Но сразу же возникает следующая проблема: допустим, вы делаете игру, в которой с какой-то периодичностью взрываются юниты. А если практически одновременно взорвется 10 юнитов? Неужели нам надо загружать 10 файлов? Выкрутимся из затруднения следующим образом: пусть число буферов на один эффект будет фиксировано (это число зависит от продолжительности файла и его востребованности). Изначально ни один из буферов не проигрывается. Возьмем предыдущий пример: как только один из юнитов взрывается, мы берём первый свободный (то есть который не проигрывается в данный момент) буфер и начинаем проигрывать его содержимое. Когда взрывается второй юнит, берём следующий буфер, затем ещё один, затем ещё и так далее. Когда все свободные буферы закончатся, останавливаем проигрывание буфера, который был запущен раньше всех, и запускаем его заново.

    Теперь сама реализация: в классе нам понадобится массив, который будет хранить время начала воспроизведения для каждого буфера – cArrayStartingTime и функция, выбирающая необходимый буфер:

void			cMediaManager::RunAppropriateMedia( int i ){
	char		m_name[256];
	memcpy( &m_name , &MediaAliases[ i ] , 256 );

	int			min_time( StartingTime[ i ] );
	int			media_cursor( i );

	for( int j( 0 ) ; j < Medias.Cursor ; j++ ){
		if( !strcmp( m_name , MediaAliases[ j ].Alias ) ){
			if( StartingTime[ j ] < min_time ){
				min_time = StartingTime[ j ];
				media_cursor = j;
			}
		}
	}
	char		Number[100];
	//Запускаем найденный файл
	Medias[ media_cursor ].StopFile();
	StartingTime[ media_cursor ] = clock();
	Medias[ media_cursor ].PlayFile();
}

    Алгоритм следующий:
    1)по курсору находим название файла
    2)в цикле ищем курсор на буфер, чье воспроизведение началось раньше остальных
    3)перезапускаем найденный буфер

    Создание нескольких буферов, можно осуществить без какой-либо доработки кода, просто в файле media_enum.log нужно несколько раз указать путь к файлу, например:

ameno			music/ameno.wav
ameno			music/ameno.wav
ameno			music/ameno.wav

money			music/money.mp3
money			music/money.mp3

oliver			music/oliver.mp3

click			sfx/click.wav
focus			sfx/focus.wav

    Таким образом, мы создадим 3 буфера для ресурса ameno, и 2 буфера для ресурса money.

    Иногда требуется проигрывать музыкальные композиции в произвольном порядке (режим автовыбора). Эту функциональность довольно просто реализовать. Возможно, мы захотим проигрывать не все буферы, находящиеся в массиве, а только некоторые, поэтому создадим массив флагов cArray RSelection, который будет инициализироваться при загрузке ресурсов (блок 2 в функции cMediaManager::LoadMedias( HWND hWnd , char * path ) ), следующая композиция для проигрывания будет выбираться из списка буферов Medias[i] у которых RSelection[i] == true.

int				cMediaManager::GetRandomSelCount( void ){
	int			ret( 0 );
	for( int i( 0 ) ; i < Medias.Cursor ; i++ ){
		if( RSelection[ i ] ){
			ret++;
		}
	}
	return( ret );
}

    Эта функция возвращает число буферов для которых возможен автовыбор. Сама функция, которая выбирает композицию, приведена ниже:

void			cMediaManager::RandomSelection( void ){
	int		rnd_count( GetRandomSelCount() );
	int		media_cursor( rnd_count * frand() );
	char		Number[100];
	for( int i( 0 ) ; i < Medias.Cursor ; i++ ){
		if( RSelection[ i ] ){
			if( media_cursor )
				media_cursor--;
			else{
				Medias[ i ].PlayFile();
				goto end;
			}
		}
	}
end:;
}

    frand() – возвращает псевдослучайное число из отрезка [0,1].

    Вот и все что я хотел Вам сегодня рассказать. Дополнительную функциональность Вы теперь сможете добавить сами, это не составит Вам особого труда. Я старался расписывать все как можно подробнее, если же не смотря на это, возникнут вопросы? пишите на Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.

    Исходные коды.