Как прикрутить к моей программе преобразование из текста в голос?
А как получить список установленных голосов? А если нужно не проговаривать, а создавать из текста аудиофайл? |
Наиболее просто это сделать с использованием Microsoft Speech API v5.1.
Для работы вам понадобится Speech SDK 5.1 for Windows® applications. Скачиваем, устанавливаем, добавляем пути к h- и lib-файлам SDK в настройки студии.
Подключаем к проекту необходимые файлы (здесь и далее под проектом подразумевается WTL-проект, созданный в Visual Studio 7.1):
#include <sapi.h> #pragma warning(disable: 4267) #include <sphelper.h> #pragma warning(default: 4267) |
я предпочитаю работать с COM-объектами через _com_ptr_t, поэтому добавляю еще макрос, определяющий обертку для интерфейса ISpVoice:
#include <comdef.h> _COM_SMARTPTR_TYPEDEF(ISpVoice, __uuidof(ISpVoice)); |
соответственно, код для генерации голоса по тексту будет выглядеть так:
ПРЕДУПРЕЖДЕНИЕ В прилагаемых демонстрационных проектах реальная обработка ошибок заменена на ASSERT-ы, поэтому рекомендуется тестировать Debug-версии, т.к. работа Release-версий с отключенной индикацией ошибок может вызвать недоумение. |
LRESULT CMainDlg::OnBnClickedBtnSpeech(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { if ( DoDataExchange( TRUE ) ) { ISpVoicePtr spVoice; // "поднимаем" компонент HRESULT hr = spVoice.CreateInstance( CLSID_SpVoice ); _ASSERTE( hr == S_OK ); if ( SUCCEEDED(hr) ) { // говорим// CString _text - собственно текст из поля ввода hr = spVoice->Speak( CA2W( _text ), SPF_DEFAULT, NULL ); _ASSERTE( hr == S_OK ); } } return 0; } |
Ложкой дегтя в этой бочке меда является то, что для Speech API 5.1 нет бесплатных или хотя бы "условно бесплатных" (см. ниже) движков для синтеза русской речи. Зато такие движки, даже в некотором ассортименте, есть для Speech API v4.0.
Опять же нужно будет скачать и установить Speech SDK 4.0. Условно-бесплатный движок (Lernout & Hauspie TTS3000 TTS Engine) для синтеза русской речи можно взять со страницы Microsoft Agent download page for end-users. Условно-бесплатный, потому что (цитата):
ПРЕДУПРЕЖДЕНИЕ Further note, that these text-to-speech engines are licensed only for use in Microsoft Agent enabled applications and Web pages with a visibly displayed Microsoft Agent character. |
Добавляем к проекту файлы, необходимые для работы со Speech API 4.0:
#include <initguid.h> #include <speech.h> #include <ComDef.h> _COM_SMARTPTR_TYPEDEF(ITTSEnum, IID_ITTSEnum); _COM_SMARTPTR_TYPEDEF(ITTSCentral, IID_ITTSCentral); _COM_SMARTPTR_TYPEDEF(IAudioMultiMediaDevice, IID_IAudioMultiMediaDevice); _COM_SMARTPTR_TYPEDEF(IAudio, IID_IAudio); |
Процесс преобразования текста в голос, в отличии от рассмотренного выше примера будет асинхронным, поэтому нам понадобятся несколько "долгоживущих" переменных. Можно сделать их членами класса тестового диалога:
// собственно интерфейс к движку TTS ITTSCentralPtr _spTTSCentral; // объекты для получения уведомлений о ходе процесса преобразования CTestBufNotify _TestBufNotify; CTestNotify _TestNotify; // cookies, получаемая при регистрации notify-интерфейсов, будет нужна при разрегистрации DWORD _dwRegKey; // интерфейс для выбора аудио-устройства IAudioMultiMediaDevicePtr _spIAudioMultiMediaDevice; |
и собственно запуск процесса преобразования:
LRESULT CMainDlg::OnBnClickedBtnSpeech(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { if ( DoDataExchange( TRUE ) && _text.GetLength() ) { // прежде чем регистрироваться с новыми параметрами, надо разрегистрироваться UnregTTSCentral(); // задаем аудио-устройство для вывода звукаif( !InitMMAudio() ) return 0; // поднимаем компонент - enumerator ITTSEnumPtr spTTSEnum; HRESULT hr = spTTSEnum.CreateInstance( CLSID_TTSEnumerator ); _ASSERTE( hr == S_OK ); if ( FAILED( hr ) ) return 0; // задаем используемый голос (его GUID)// _VoicesCombo - combobox для выбора голосовint i = _VoicesCombo.GetCurSel(); // _GUIDVoices[ i ] - GUID текущего голоса, сохраненный при заполнении ComboBox-а hr = spTTSEnum->Select( _GUIDVoices[ i ], &_spTTSCentral, _spIAudioMultiMediaDevice ); _ASSERTE( hr == S_OK ); if ( FAILED( hr ) ) return 0; // регистрируемся в движке hr = _spTTSCentral->Register( &_TestNotify, IID_ITTSNotifySink, &_dwRegKey ); _ASSERTE( hr == S_OK ); if ( FAILED( hr ) ) return 0; // объект для передачи текста в функцию TextData, он может быть локкальным, т.к.// TextData скопирует эти данные перед использованием SDATA TextToSpeech; TextToSpeech.dwSize = _text.GetLength() + 1; TextToSpeech.pData = _text.GetBuffer(0); // Функция TextData асинхронная, для воспроизведения создаст поток и тут же вернет управление// по окончании преобразования _TestNotify вызовет OnAudioStop(); hr = _spTTSCentral->TextData( CHARSET_TEXT, 0, TextToSpeech, &_TestBufNotify, IID_ITTSBufNotifySink ); _text.ReleaseBuffer(); _ASSERTE( hr == S_OK ); if ( FAILED( hr ) ) return 0; // запрещаем кнопку запуска процесса преобразования _BtnSpeech.EnableWindow( FALSE ); } return 0; } void CMainDlg::OnAudioStop() { // разрешаем кнопку запуска процесса преобразования _BtnSpeech.EnableWindow( TRUE ); } |
Функции UnregTTSCentral() и InitMMAudio() выглядят так:
void CMainDlg::UnregTTSCentral() { if ( _spTTSCentral ) { HRESULT hr = _spTTSCentral->UnRegister( _dwRegKey ); _spTTSCentral = NULL; _ASSERTE( hr == S_OK ); } } BOOL CMainDlg::InitMMAudio() { HRESULT hr; if ( _spIAudioMultiMediaDevice ) { hr = ( ( IAudioPtr ) _spIAudioMultiMediaDevice ) ->Flush(); _spIAudioMultiMediaDevice = NULL; _ASSERTE( hr == S_OK ); } hr = _spIAudioMultiMediaDevice.CreateInstance( CLSID_MMAudioDest ); _ASSERTE( hr == S_OK ); if ( SUCCEEDED( hr ) ) { hr = _spIAudioMultiMediaDevice->DeviceNumSet( WAVE_MAPPER ); // устройство по умолчанию _ASSERTE( hr == S_OK ); if ( SUCCEEDED( hr ) ) return TRUE; } elsereturn FALSE; return TRUE; } |
Реализация объектов CTestBufNotify и CTestNotify, используемых для уведомления о ходе процесса преобразования, в частности о его окончании, взята из примеров Speech 4.0 SDK.
Для только что рассмотренного примера использования SAPI 4 это происходит при заполнении комбобокса со списком голосов:
BOOL CMainDlg::FillComboVoices() { ITTSEnumPtr spTTSEnum; TTSMODEINFO TTSModeInfo; DWORD dwNumTimes = 0; HRESULT hr; // поднимаем компонент - enumerator hr = spTTSEnum.CreateInstance( CLSID_TTSEnumerator ); _ASSERTE( hr == S_OK ); if ( FAILED( hr ) ) return FALSE; // получаем общее количество голосов, и// если они есть, информацию по первому голосу hr = spTTSEnum->Next ( 1, &TTSModeInfo, &dwNumTimes ); _ASSERTE( SUCCEEDED( hr ) ); if ( FAILED( hr ) ) return FALSE; if ( dwNumTimes == 0 ) { ::MessageBox( m_hWnd, "Не обнаружено ни одного голоса", "FillComboVoices()", MB_OK | MB_ICONSTOP ); return FALSE; } // очищаем vector для GUID-ов _GUIDVoices.clear(); // и combobox для текстовых названий голосов _VoicesCombo.ResetContent(); while ( dwNumTimes ) { // запоминаем текстовое названиеif ( TTSModeInfo.dwFeatures & TTSFEATURE_ANYWORD ) _VoicesCombo.AddString( TTSModeInfo.szModeName ); else { CString sz; sz = TTSModeInfo.szModeName; sz += " (Может говорить только определеные слова!)"; _VoicesCombo.AddString( sz ); } // и GUID _GUIDVoices.push_back( TTSModeInfo.gModeID ); // и следующий hr = spTTSEnum->Next( 1, &TTSModeInfo, &dwNumTimes ); _ASSERTE( SUCCEEDED( hr ) ); if ( FAILED( hr ) ) return FALSE; } return TRUE; } |
Для SAPI 5.1 все сводится к вызову функции из SDK:
#include <spuihelp.h> // заполнение combobox-а голосами BOOL CMainDlg::FillComboVoices() { // CComboBox _VoicesCombo = GetDlgItem( IDC_COMBO_VOICES ); HRESULT hr = SpInitTokenComboBox( _VoicesCombo, SPCAT_VOICES ); _ASSERTE( hr == S_OK ); if( SUCCEEDED( hr ) ) return TRUE; elsereturn FALSE; } |
а пример преобразования с применением выбранного голоса станет таким:
LRESULT CMainDlg::OnBnClickedBtnSpeech(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { if ( DoDataExchange( TRUE ) ) { ISpVoicePtr spVoice; // "поднимаем" компонент HRESULT hr = spVoice.CreateInstance( CLSID_SpVoice ); _ASSERTE( hr == S_OK ); if ( SUCCEEDED(hr) ) { // получаем информацию о новом выбранном голосе ISpObjectToken* pToken = SpGetCurSelComboBoxToken( _VoicesCombo ); // текущий голос ISpObjectTokenPtr pOldToken; hr = spVoice->GetVoice( &pOldToken ); _ASSERTE( hr == S_OK ); if (SUCCEEDED(hr)) { // устананавливать новый голос имеет смысл только// если он действительно поменялсяif (pOldToken != pToken) { hr = spVoice->SetVoice( pToken ); _ASSERTE( hr == S_OK ); } } // говорим// CString _text - собственно текст из поля ввода hr = spVoice->Speak( CA2W( _text ), SPF_DEFAULT, NULL ); _ASSERTE( hr == S_OK ); } } return 0; } |
Процесс аналогичен простому воспроизведению, только при регистрации в TTS движке используется не IAudioMultiMediaDevice, а предварительно сконфигурированный указатель на интефейс IAudioFile:
// имя аудиофайла CString _AudioFile; // формат создаваемого аудиофайла, инициализируется в конструкторе CMainDlg WAVEFORMATFULL _fmt; // интерфейс для задания формата аудиофайла IAudioFilePtr _spAudioFile; |
задаем параметры:
BOOL CMainDlg::InitFileAudio() { if ( DoDataExchange( DDX_SAVE ) ) { HRESULT hr; // если уже используется, освобождаемif ( _spAudioFile ) { hr = _spAudioFile->Flush(); _spAudioFile = NULL; _ASSERTE( hr == S_OK ); } // поднимаем компонент hr = _spAudioFile.CreateInstance( CLSID_AudioDestFile ); _ASSERTE( hr == S_OK ); if ( FAILED( hr ) ) return FALSE; // устанавливаем скорость преобразования// 0x100 означает реальную скорость - 1сек речи преобразуется за 1сек// 0x200 - 1сек речи записывается за 0,5 сек и т.д. hr = _spAudioFile->RealTimeSet( 0x100 << _RateIdx ); _ASSERTE( hr == S_OK ); if ( FAILED( hr ) ) return FALSE; // устанавливаем формат записываемого файла// ВНИМАНИЕ: Используемый движок не обязательно поддерживает все возможные форматы// причем выяснится это не здесь, а при spTTSEnum->Select() SDATA WFEX; WFEX.pData = &_fmt; // предварительно сформированная структура с форматом аудиофайла WFEX.dwSize = sizeof(WAVEFORMATEX) + _fmt.cbSize; hr = ( ( IAudioPtr ) _spAudioFile ) ->WaveFormatSet( WFEX ); _ASSERTE( hr == S_OK ); if ( FAILED( hr ) ) return FALSE; return TRUE; } elsereturn FALSE; return TRUE; } |
и собственно преобразование текста в аудиофайл:
LRESULT CMainDlg::OnBnClickedBtnFile(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { if ( DoDataExchange( DDX_SAVE ) && _text.GetLength() ) { // прежде чем регистрироваться с новыми параметрами, надо разрегистрироваться UnregTTSCentral(); // заново задаем формат аудиофайлаif ( !InitFileAudio() ) return 0; // если есть такой файл, удаляемif ( _access( _AudioFile, 0 ) == 0 ) ::DeleteFile( _AudioFile ); _ASSERTE( _access( _AudioFile, 0 ) == -1 ); HRESULT hr; ITTSEnumPtr spTTSEnum; // поднимаем компонент - enumerator hr = spTTSEnum.CreateInstance( CLSID_TTSEnumerator ); _ASSERTE( hr == S_OK ); if ( FAILED( hr ) ) return 0; // устанавливаем имя аудиофайла hr = _spAudioFile->Set( CA2W( _AudioFile ), 1 ); _ASSERTE( hr == S_OK ); if ( FAILED( hr ) ) return 0; // задаем используемый голос (его GUID) и "приемник" голоса - аудиофайл// _VoicesCombo - combobox для выбора голосовint i = _VoicesCombo.GetCurSel(); // _GUIDVoices[ i ] - GUID текущего голоса, сохраненный при заполнении ComboBox-а hr = spTTSEnum->Select( _GUIDVoices[ i ], &_spTTSCentral, _spAudioFile ); _ASSERTE( hr == S_OK ); if ( FAILED( hr ) ) return 0; // регистрируемся в движке hr = _spTTSCentral->Register( &_TestNotify, IID_ITTSNotifySink, &_dwRegKey ); _ASSERTE( hr == S_OK ); if ( FAILED( hr ) ) return 0; // объект для передачи текста в функцию TextData, он может быть локальным, т.к.// TextData скопирует эти данные перед использованием SDATA TextToSpeech; TextToSpeech.dwSize = _text.GetLength() + 1; TextToSpeech.pData = _text.GetBuffer(0); // Функция TextData асинхронная, для воспроизведения создаст поток и тут же вернет управление// по окончании преобразования _TestNotify вызовет OnAudioStop(); hr = _spTTSCentral->TextData( CHARSET_TEXT, 0, TextToSpeech, &_TestBufNotify, IID_ITTSBufNotifySink ); _text.ReleaseBuffer(); _ASSERTE( hr == S_OK ); if ( FAILED( hr ) ) return 0; // запрещаем кнопку запуска процесса преобразования _BtnSpeech.EnableWindow( FALSE ); _Btn2File.EnableWindow( FALSE ); _BtnPlay.EnableWindow( FALSE ); } return 0; } |
соответственно действия по событию окончания преобразования приобретают следующий вид:
void CMainDlg::OnAudioStop() { // разрешаем кнопку запуска процесса преобразования _BtnSpeech.EnableWindow( TRUE ); _Btn2File.EnableWindow( TRUE ); HRESULT hr; if ( _spIAudioMultiMediaDevice ) { hr = ( ( IAudioPtr ) _spIAudioMultiMediaDevice ) ->Flush(); _ASSERTE( hr == S_OK ); } // отпускаем аудиофайл, иначе он остается в заблокированном состоянииif ( _spAudioFile ) { hr = _spAudioFile->Flush(); _ASSERTE( hr == S_OK ); } if ( _access( _AudioFile, 0 ) == 0 ) _BtnPlay.EnableWindow( TRUE ); } |
Идея таже самая - перенаправление вывода:
LRESULT CMainDlg::OnBnClickedBtnFile(WORD /*wNotifyCode*/, WORD /*wID*/, HWND /*hWndCtl*/, BOOL& /*bHandled*/) { if ( DoDataExchange( DDX_SAVE ) ) { CWaitCursor wc; // если есть такой файл, удаляемif ( _access( _AudioFile, 0 ) == 0 ) ::DeleteFile( _AudioFile ); _ASSERTE( _access( _AudioFile, 0 ) == -1 ); ISpVoicePtr spVoice; ISpStreamPtr spStream; CSpStreamFormat cAudioFmt; // "поднимаем" компонент HRESULT hr = spVoice.CreateInstance( CLSID_SpVoice ); _ASSERTE( hr == S_OK ); if( FAILED( hr ) ) return 0; // получаем информацию о новом выбранном голосе ISpObjectToken* pToken = SpGetCurSelComboBoxToken( _VoicesCombo ); // текущий голос ISpObjectTokenPtr pOldToken; hr = spVoice->GetVoice( &pOldToken ); _ASSERTE( hr == S_OK ); if (SUCCEEDED(hr)) { // устананавливать новый голос имеет смысл только// если он действительно поменялсяif (pOldToken != pToken) { hr = spVoice->SetVoice( pToken ); _ASSERTE( hr == S_OK ); } } // задаем формат файла, используя указатель на предварительно заполненную структуру WAVEFORMATEX hr = SPBindToFile( _AudioFile, SPFM_CREATE_ALWAYS, &spStream, &SPDFID_WaveFormatEx, &_fmt ); _ASSERTE( hr == S_OK ); if( FAILED( hr ) ) return 0; //// другой вариант - для задания формата используется перечисление SPSTREAMFORMAT//SPSTREAMFORMAT _FileFmt = SPSF_22kHz16BitMono;//hr = cAudioFmt.AssignFormat( _FileFmt );//_ASSERTE( hr == S_OK );//if( FAILED( hr ) )// return 0;//hr = SPBindToFile( _AudioFile, SPFM_CREATE_ALWAYS, &spStream, // &cAudioFmt.FormatId(), cAudioFmt.WaveFormatExPtr() );//_ASSERTE( hr == S_OK );//if( FAILED( hr ) )// return 0;// задаем вывод - в аудиофайл hr = spVoice->SetOutput( spStream, TRUE ); _ASSERTE( hr == S_OK ); if( FAILED( hr ) ) return 0; // SPF_DEFAULT - синхронное преобразование TTS, функция завершится, // когда преобразование будет завершено hr = spVoice->Speak( CA2W( _text ), SPF_DEFAULT, NULL ); // метод возвратил ошибку? возможно дело в неподдерживаемом формате файла _ASSERTE( hr == S_OK ); if( FAILED( hr ) ) return 0; hr = spStream->Close(); _ASSERTE( hr == S_OK ); } return 0; } |
Мне не удалось обнаружить в документации на SAPI 5.1 методов для регулировки скорости записи в файл, аналогичных используемым в SAPI 4, судя по тестам преобразование текста для записи в файл производится с максимально возможной скоростью, загружая процессор на 100%.