Оценка 21
[+1/-0]
Оценить ![]() ![]() ![]() ![]() ![]() ![]()
|
| Как прикрутить к моей программе преобразование из текста в голос? А как получить список установленных голосов? А если нужно не проговаривать, а создавать из текста аудиофайл? | ![]() |
Наиболее просто это сделать с использованием 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; } else return 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; else return 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;
}
else
return 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%.
Оценка 21
[+1/-0]
Оценить ![]() ![]() ![]() ![]() ![]() ![]()
|