Моё почтение, господа. Не так давно я написал статью, в которой рассказывал о том, как создать класс для проигрывания звука и видео. В течение последних несколько недель, несмотря на сессию, мне приходилось регулярно использовать этот класс. И, честно признаюсь, я нашел его не очень удобным. Спроектирован он был правильно, и если в программе задействовалось немного музыкальных файлов, проблем с их использованием не возникало, но при большом количестве ресурсов управлять ими становилось проблематично. Поэтому этот урок будет посвящен написанию надстройки (объектно-ориентированной, разумеется), упрощающей нам жизнь.
Итак, что же мы хотим? Мы хотим класс, который инкапсулировал бы бОльшую часть рутинного кода (инициализация и управление ресурсами), предоставлял удобный интерфейс + некоторые специфические возможности по проигрыванию. Вот вроде бы и все!
Инициализировать сотни объектов ручками как-то тоскливо, поэтому создадим функцию, принимающую в качестве параметра путь к директории в которой лежит файл со списком ресурсов для загрузки. Вот она:
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{ cArrayMediaAliases; 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 файлов? Выкрутимся из затруднения следующим образом: пусть число буферов на один эффект будет фиксировано (это число зависит от продолжительности файла и его востребованности). Изначально ни один из буферов не проигрывается. Возьмем предыдущий пример: как только один из юнитов взрывается, мы берём первый свободный (то есть который не проигрывается в данный момент) буфер и начинаем проигрывать его содержимое. Когда взрывается второй юнит, берём следующий буфер, затем ещё один, затем ещё и так далее. Когда все свободные буферы закончатся, останавливаем проигрывание буфера, который был запущен раньше всех, и запускаем его заново.
Теперь сама реализация: в классе нам понадобится массив, который будет хранить время начала воспроизведения для каждого буфера – cArray Алгоритм следующий:
Создание нескольких буферов, можно осуществить без какой-либо доработки кода, просто в файле media_enum.log нужно несколько раз указать путь к файлу, например:
Таким образом, мы создадим 3 буфера для ресурса ameno, и 2 буфера для ресурса money.
Иногда требуется проигрывать музыкальные композиции в произвольном порядке (режим автовыбора). Эту функциональность довольно просто реализовать. Возможно, мы захотим проигрывать не все буферы, находящиеся в массиве, а только некоторые, поэтому создадим массив флагов cArray Эта функция возвращает число буферов для которых возможен автовыбор. Сама функция, которая выбирает композицию, приведена ниже:
frand() – возвращает псевдослучайное число из отрезка [0,1].
Вот и все что я хотел Вам сегодня рассказать. Дополнительную функциональность Вы теперь сможете добавить сами, это не составит Вам особого труда. Я старался расписывать все как можно подробнее, если же не смотря на это, возникнут вопросы? пишите на
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)перезапускаем найденный буфер
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
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:;
}