Оценка 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;
}
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%.
Оценка 21
[+1/-0]
Оценить ![]() ![]() ![]() ![]() ![]() ![]()
|