Воспроизводим mp3 в своей программе и что может этому помешать +13


Вступительная


Давным-давно, лет 100 10 тому назад, когда только прокрастинация начала захватывать мой разум, я решил, что пока я еще в состоянии им (разумом) пользоваться, надо срочно строить себе спасательную шлюпку, дабы не утонуть в пучине отложенных дел. Конечно, на тот момент уже существовало достаточно количество различных напоминалок, будильников, шедулеров и прочих разных «умных» часов, но, как водится, свое — оно всегда ближе и понятнее, чем чужое, даже с мегабайтами файлов справки. Да и зря, что ли, книжку по бейсику у друга одолжил?

Но, как водится, обо всем по порядку.


10-15 лет тому назад

Изучение бейсика началось еще со времен Subor (денди с клавиатурой) с картриджем, который содержал в себе различные виды этого замечательного ЯП. Затем на компьютере в DOS в старом добром синеэкранном qb. Ну а уж затем, чувствуя себя обалденным программистом, пересел на монструозный (хах! Для 486 процессора с 16МБ на борту...) VB5.
Когда уже лепка окон с кучей непонятных кнопок уже порядком надоела, решил написать что-нибудь полезное. Ну а тут и необходимость такая появилась как раз. Ко всем имеющимся в доме будильникам я уже давно привык и мог спокойно спать, даже если они гудели все одновременно.
— Ну, — думал я, — уж колонки-то мои (18-ваттные!) наверняка меня поднимут.
Начал с простого: кинул на форму таймер, вывел на нее пару текстбоксов и кнопку «Старт!». Задавал время, через которое меня можно начать поднимать, сколько раз меня нужно поднимать, ну и mp3-файлик, который надо запускать в винампе на полную громкость, дабы все-таки меня поднять.
Долго ли, коротко ли, но каждый раз размышлять о количестве минут, которые необходимо задавать до подъема, мне надоело. Было принято решение все же задавать время, начиная с которого начинать трезвонить во все колокола. Кроме того, надо было еще обеспечить свое время подъема для каждого дня недели. Еще хотелось видеть стикер на экране, который выведет расписание текущего дня, возможность выключения сразу группы событий (а ну как каникулы летние настали и сессии все сданы), да еще и блокировать по ночам компьютер. Да так, чтобы сам его не смог разблокировать. И еще…

mp3 играю сам


… и еще зачем мне запускать файл в винампе, если могу научить свой чудо-будильник делать это? В винампе могу громкость убавить, могу удалить, да много чего еще могу с ним сделать, и в итоге — проспать.
Первое же, предлагаемое MSDN решение, MCI, рассматривать не стал, как совсем неспортивное для такого «крутого» программиста, как я, а декодировать в бейсике mp3, напротив, посчитал слишком спортивным. В конце концов остановился на объектах COM, которые неплохо встраивались в среду vb и позволяли довольно гибко управлять воспроизведением и получать необходимую информацию о ходе воспроизведения. Настал час IGraphBuilder'а (в недрах vb он назывался FilgraphManager. Хоть и не совсем точно, но для упрощения пусть будет так).

Весь код сводился к банальщине:
Dim mpl As New FilgraphManager, mplinfo As IMediaPosition

Function OpenMP3(ByVal mp3file As String) As Boolean
On Error Resume Next
mpl.RenderFile mp3file

If Err.Number <> 0 Then
   Set mplinfo = mpl
   Timer1.Enabled = True
   OpenMP3 = True
   Exit Function
End If

OpenMP3 = False
End Function

Sub Timer1_Timer()
Label1.Caption = mplinfo.CurrentPosition
End Sub


Работало так, что я был счастлив, словно сдал все сессии в университете сразу наперед и даже уже его закончил.
Монстро-будильник на vb5
image


Ну а причем тут C++ ?!

Но вскорости, то ли компьютер стал слишком быстрым, то ли не такой уж я и крутой программист был, покуда писал этого монстра, но будильник мой стал меня частенько подводить. То на экзамен не разбудит, проскакивая время выполнения события, переходя к ожиданию следующего, то в наглую при мне показывает, что меня будит, но при этом не издает ни звука. Решил (как это часто бывает с «крутыми» программистами), что теперь-то уж я точно крутой, по сравнению со мной тогдашним, и перепишу его на C++! Зря, что ли, книжку за авторством Страуструпа покупал за бешеные деньжищи?

На тот момент я вполне сносно уже дергал winapi, и даже иногда вставлял классы, прочитав оглавление книжки за авторством Страуструпа. Спустя пару дней, накидав скелет будильника, решил, что ему тоже необходимо самостоятельно воспроизводить mp3, без всяких там медиа-плееров, винампов и иже с ними. MCI, как водится, тоже отверг.

Думал я, что, быть может, раньше хоть и не такой всесильным был, но все ж кой-чего да соображал. А возьму-ка я тот же FilterGraph, быстренько сделаю простой класс, и продолжу свои изыскания. Да и примерчик в MSDN подкупал своей простотой.

Быстренько состряпал класс, задал выполнение события, терпеливо дождался и…

«Программа выполнила недопустимую операцию и будет закрыта...»
Что за ерунда? Как это я в нескольких строчках кода копипасты мог допустить ошибку? Может файл не тот? Может где объект не инициализировался? Проверка по шагам давала стабильно выброс исключения на вызове метода RenderFile.

Старый дедовский метод гугла

Возиться было сильно лень, начал вместо кода сочинять поисковые запросы, которые сводили все к порушенному стеку, ошибках в коде задающего вопрос, много к чему еще, но не к тому, что видел я. Код, который работает у всех, не работает у меня. Переборов свое эго, начитавшись советов «бросай эти ваши непонятные графбилдеры, бери в руки MCI и вперед», переписал скрипя зубами на MCI.

Опять F5, mciSendString, и…
«Программа выполнила недопустимую операцию и будет закрыта...»

Начал грешить на монитор, неправильную клавиатуру, процессор, который вдруг разучился делать то, чему его учили на заводе. Нашел в недрах сайта MS утилиту GraphEdit, которая уж точно должна работать как нужно, запустил, и…

image

Высшие силы компьютерной техники явно работали против меня.

Если перепробовали все, но все еще ничего не работает — RTFM

На мое счастье, в IGraphBuilder есть метод SetLogFile. Очень хороший, надо сказать, метод. Без него бы я бы точно потерял покой и сон, углубившись в простыню встроенного дизассемблера.

Запустив в очередной раз отладку, погоревав над сообщением об исключении по адресу, весьма далекому от моего кода, без всякого стека и имени модуля, к которому оный адрес мог относится, полез читать собранный лог.

Не смотря на то, что он весьма нудный, его изучение, однако, принесло свои плоды:
Просмотр файла d:\437236.mp3
Файл имеет тип носителя 0xe436eb83… Подтип 0xe436eb87…
clsid фильтра источника 0xe436ebb5…

Render: ошибка метода QueryInternalStreams. Фильтр по адресу 2914b8c
Возврат с уровня 2
Возврат! Отключается контакт 2914fbc
Возврат! Отключается контакт 293a804
Render: больше нет контактов — не удается найти контакт, используемый фильтром 2914b8c
Возврат! Удаление фильтра 2914b8c
Render: попытка использования нового фильтра с выводимым именем device:sw:{083863F1-70DE-11D0-BD40-00A0C911CE86}\{138130AF-A79B-45D5-B4AA-87697457BA87}…

А дальше ничего.

Из этого немедленно (на самом деле, бессонные сутки спустя) был сделан вывод: сбоит некий фильтр с CLSID={138130AF-A79B-45D5-B4AA-87697457BA87}

Вредитель-прожигатель

Быстренько открыл реестр, нашел в нем этот CLSID, обнаружил, что это:
Nero 6 malware?

Ох, говорила мне многострадальная моя семерка, не ставь ты эту шестую неру, козлено... не виноватая я буду, если что не так пойдет. Не послушал я.
Деинсталлировал Nero 6. Заработало все.
Инсталлировал обратно. Перестало работать.

Но тут уж я, порядком разгоряченный, решил, что так просто проблему не буду решать. Нахмурив брови, заинсталлировал вновь Nero 6 и начал думать: Как же мне, не удаляя ее, заставить таки играть свои будильники?

Ларчик НЕ просто открывался

Итак, методом чтения документации MSDN разной глубины вложенности, пришел к выводу, что mp3 (да и любой медиа-файл) воспроизводится по следующему алгоритму:
1. В IBaseFilter загружается файл источник;
2. Выполняется поиск фильтра, ответственного за вывод звука в звуковое (или иное выходное) устройство;
3. Выполняется поиск промежуточных фильтров, которые преобразуют поток данных из источника в поток данных, понятный выходному устройству.

Примерный разбор mp3-файла выходит такой:
1. Загружаем в IBaseFilter содержимое входного файла;
2. Ищем фильтр-парсер файла;
3. Ищем фильтр-декодер в аудио-поток;
4. Ищем фильтр, который этот поток отправит в выходное устройство

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

Метод научного тыка

У нас есть набор фильтров: его мы можем получить через IFilterMapper
Для каждого из фильтров у нас есть набор пинов (INPUT/OUTPUT), список которых мы можем получить из метода IBaseFilter::EnumPins
Для каждого из пинов есть набор медиа-типов, список которых мы можем получить через IPin::EnumMediaTypes.

Пора перечислять.
Набросал классы для IFilterMapper, IBaseFilter, IPin (ниже приведу ссылку, где можно будет увидеть реализацию) и начал перечислять все известные системе фильтры, подыскивать подходящие пины, соединять их.

Длинная простыня кода, которая рождалась долго и в муках
// Несколько упрощенный код 
void CDSPlayer::PlayFile(LPCTSTR pszFile)
{
		HRESULT hr = 0;

		IBaseFilter * pSource = NULL;

		// загружаем исходный файл
		if ( FAILED(m_pGraph->AddSourceFilter(pszFile, pszFile, &pSource)) )
			return FALSE;

		// пытаемся отстроить свой граф фильтров
		if ( SUCCEEDED( TryConnectFilters( m_pGraph, pSource ) ) )
		{
			// воспроизводим
			return SUCCEEDED( m_pControl->Run() );
		}
		
		return FALSE;
}

// пытаемся состроить граф из фильтров, выстраивая его от pSource
// в pSource (там загружен входной файл) только один pin - OUTPUT
HRESULT CDSPlayer::TryConnectFilters(IGraphBuilder * pGraph, IBaseFilter * pSource)
{
	std::vector<CBaseFilter*> filters;
	m_fltEnum.FilterList(filters);

	return ConnectFilters(filters, pGraph, pSource);
}

// производим попытку найти подходящий фильтр для входного
HRESULT CDSPlayer::ConnectFilters(std::vector<CBaseFilter*> & vFltList, IGraphBuilder * pGraph, IBaseFilter * pSource)
{
	HRESULT hr = E_NOINTERFACE;

	CPin * pSourcePin = NULL;

	CBaseFilter fltSource(pSource);

	// ищем ВЫХОДНОЙ пин, чтобы...
	std::vector<CPin*> pl;
	fltSource.PinList( pl );
	for(std::vector<CPin*>::iterator vpl = pl.begin(); vpl < pl.end(); ++vpl)
	{
		CPin * pin = *vpl;

		if ( pin->Dir() == PINDIR_OUTPUT )
		{
			pSourcePin = pin;

			// ...соединить его с подходящим ВХОДНЫМ фильтром
			hr = ( SUCCEEDED( ConnectPins(vFltList, pGraph, pSource, pin) ) ? S_OK : hr );
			// Если найдем еще какой-нибудь OUTPUT, то попробуем и его соединить. Может быть и видео-файл, где кроме звука будет еще и видео
		}
	}

	return ( pSourcePin ? hr : S_OK );
}


// если Nero, то забудем его как страшный сон
BOOL CDSPlayer::IsFilterAllowed(CBaseFilter * pFilter)
{
	CString s = pFilter->Name();
	if ( s[0] == _T('N') &&
		 s[1] == _T('e') &&
		 s[2] == _T('r') &&
		 s[3] == _T('o') &&
		 s[4] == _T(' ')
		 )
		return FALSE;		// skip ugly nero filters

	return TRUE;

}

HRESULT CDSPlayer::ConnectPins(std::vector<CBaseFilter*> & vFltList, IGraphBuilder * pGraph, IBaseFilter * pSource, CPin * pSourcePin)
{
	std::vector<AM_MEDIA_TYPE> vAmtSource;

	// извлекаем все медиа-типы нашего пина...
	pSourcePin->MediaTypesList(vAmtSource);

        // проходим по всем фильтрам
	for(std::vector<CBaseFilter*>::iterator v = vFltList.begin(); v < vFltList.end(); ++v)
	{
		CBaseFilter * pFlt = *v;

		if ( !IsFilterAllowed( pFlt ) )
			continue;

		if ( pFlt->Init() )
		{
			std::vector<CPin*> vPinList;
			pFlt->PinList(vPinList);

			for(std::vector<CPin*>::iterator vp = vPinList.begin(); vp < vPinList.end(); ++vp)
			{
				CPin * pin = (*vp);
				// отбрасываем все OUTPUT пины, нужны только INPUT
				if ( pin->Dir() == PINDIR_OUTPUT )
				{
					continue;
				}

				std::vector<AM_MEDIA_TYPE> vamt;
				pin->MediaTypesList(vamt);
				for(std::vector<AM_MEDIA_TYPE>::iterator va = vamt.begin(); va < vamt.end(); ++va)
				{
					for(std::vector<AM_MEDIA_TYPE>::iterator vas = vAmtSource.begin(); vas < vAmtSource.end(); ++vas)
					{
						// если медиа-тип пина входного фильтра соответствует медиа-типу перебираемого фильтра, пробуем соединить
						if ( vas->majortype == va->majortype )
						{
							// сперва добавим фильтр в граф (на этом этапе загрузка фильтров от Nero6 выбросила бы EXCEPTION_ACCESS_VIOLATION)
							pGraph->AddFilter(pFlt->Object(), pFlt->Name());

							// ТОЛЬКО ConnectDirect! Метод Connect, если увидит несоответствующий пин, попытается самостоятельно пройтись по фильтрам и снова наткнется на сбойные
							HRESULT hrc = pGraph->ConnectDirect(pSourcePin->Object(), pin->Object(), NULL);
							if ( SUCCEEDED( hrc ) )
							{
								_tprintf(TEXT("Found suitlable filter '%s'!\n"), pFlt->Name());
								// Соединились? Отлично! Теперь тоже самое рекурсивно и для следующего (только что найденного) фильтра в цепочке
								return ConnectFilters(vFltList, pGraph, pFlt->Object());
							}
							// ну а если не соединились, то выбрасываем его из графа и идем дальше по списку
							pGraph->RemoveFilter(pFlt->Object());
						}
					}
				}
			}
		}
	}

	// не нашли ничего подходящего. Воспроизводить нечем :-(
	return E_NOINTERFACE;
}



Не устал ли читатель изучать длиннющую простыню кода выше? Устал. Точно устал. Так вот: этот код, который рождался долго и в муках, с первого раза не заработал. То ли пины плохие, то ли фильтры плохо фильтруют. Звука не было.

В поисках Атлантиды

Код инициализации фильтра прост (был) до безобразия:
Инициализация CBaseFilter

CBaseFilter::CBaseFilter(REGFILTER rf)
	:
	m_rf( rf ),
	m_sFilterName( rf.Name ),
	m_pBaseFilter( NULL ),
	m_fDontRelease( FALSE )
{

}


BOOL CBaseFilter::Init()
{
	if ( m_pBaseFilter ) // похоже, что уже инициализирован вторым конструктором (здесь его код не приведен)
		return TRUE;

	// создаем объект
	HRESULT hr = CoCreateInstance(m_rf.Clsid, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (void**) &m_pBaseFilter);

	// если создали, то пересчитаем его пины на будущее
	if ( SUCCEEDED( hr ) )
		hr = InitPins();

	return SUCCEEDED( hr );
}



Вот только половину фильтров (в том числе, столь желанный mp3-фильтр) он не загружал, ибо E_NOINTERFACE.
Снова судьба отправляла меня в документацию, которая четко и ясно давала понять, что не все золото, что...не всяк фильтр нуждается в такой сложности, чтобы быть фильтром DS. Иными словами, для многих фильтров не нужно делать сложных изысканий и методов, чтобы стать полноценным DS-фильтром. Придумали упрощенные DMO, а для них и обертку, которая сделает многие вещи сама. Пришлось дописать метод инициализации:
ПРОДВИНУТАЯ(!) инициализация CBaseFilter
BOOL CBaseFilter::Init()
{
	if ( m_pBaseFilter )
		return TRUE;

	HRESULT hr = CoCreateInstance(m_rf.Clsid, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (void**) &m_pBaseFilter);
	if ( E_NOINTERFACE == hr ) // похоже, что это объект DMO, и вместо него будем использовать DMOWrapper
	{
		hr = CoCreateInstance(CLSID_DMOWrapperFilter, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (void**) &m_pBaseFilter);
		if ( SUCCEEDED( hr ) )
		{
			IDMOWrapperFilter * pDMOFilter = NULL;
			hr = m_pBaseFilter->QueryInterface(IID_IDMOWrapperFilter, (void**) &pDMOFilter);
			if ( SUCCEEDED( hr ) )
			{
				// загрузим в обертку наш DMO-фильтр. Т.к. хочется только звука, то будем использовать категорию AUDIO
				pDMOFilter->Init(m_rf.Clsid, DMOCATEGORY_AUDIO_DECODER);
				// обертку инициализировали, теперь она сама со всем справится
				pDMOFilter->Release();
			}
		}
	}

	if ( SUCCEEDED( hr ) )
		hr = InitPins();

	return SUCCEEDED( hr );
}



И вот долгожданное чудо!
image


Ну и после этого, я уже спокойно продолжил натягивать всякие красивости на написанный скелет. Получилось даже слегка лучше, чем на vb.

Новый монструозный будильник
image


Ну а остальные истории, связанные с этим многострадальным будильником, уже не так интересны. Теперь уж точно встану вовремя! Ну, пока и к этому не привыкну…

PS:
Прикладываю полный код такого плеера.
Реализация в Visual Studio
Код, конечно, не блещет, но это, наверное, потому, что писался он с упорством локомотива и скоростью истребителя.

Ну а цель статьи: все-таки поделиться со всеми своими изысканиями по проблеме, решение которой я, каюсь, так и не нашел в свое время в сети. Если этого решения еще нет, то теперь оно уже точно есть.
-->


К сожалению, не доступен сервер mySQL