Win-specific: Add volume and rate tuning for TTS, fix some errors

This commit is contained in:
Abs62 2013-04-26 17:41:39 +04:00
parent 58654a7423
commit 0fb8eed553
12 changed files with 288 additions and 39 deletions

View file

@ -973,7 +973,7 @@ void ArticleView::openLink( QUrl const & url, QUrl const & ref,
if ( itemMd5Id == md5Id ) if ( itemMd5Id == md5Id )
{ {
SpeechClient * speechClient = new SpeechClient( i->id, this ); SpeechClient * speechClient = new SpeechClient( *i, this );
connect( speechClient, SIGNAL( finished() ), speechClient, SLOT( deleteLater() ) ); connect( speechClient, SIGNAL( finished() ), speechClient, SLOT( deleteLater() ) );
speechClient->tell( text ); speechClient->tell( text );
break; break;

View file

@ -604,7 +604,7 @@ Class load() throw( exError )
QDomNode ves = root.namedItem( "voiceEngines" ); QDomNode ves = root.namedItem( "voiceEngines" );
if ( !wss.isNull() ) if ( !ves.isNull() )
{ {
QDomNodeList nl = ves.toElement().elementsByTagName( "voiceEngine" ); QDomNodeList nl = ves.toElement().elementsByTagName( "voiceEngine" );
@ -617,6 +617,12 @@ Class load() throw( exError )
v.id = ve.attribute( "id" ); v.id = ve.attribute( "id" );
v.name = ve.attribute( "name" ); v.name = ve.attribute( "name" );
v.iconFilename = ve.attribute( "icon" ); v.iconFilename = ve.attribute( "icon" );
v.volume = ve.attribute( "volume", "50" ).toInt();
if( v.volume < 0 || v.volume > 100 )
v.volume = 50;
v.rate = ve.attribute( "rate", "50" ).toInt();
if( v.rate < 0 || v.rate > 100 )
v.rate = 50;
c.voiceEngines.push_back( v ); c.voiceEngines.push_back( v );
} }
} }
@ -1205,6 +1211,14 @@ void save( Class const & c ) throw( exError )
QDomAttr icon = dd.createAttribute( "icon" ); QDomAttr icon = dd.createAttribute( "icon" );
icon.setValue( i->iconFilename ); icon.setValue( i->iconFilename );
v.setAttributeNode( icon ); v.setAttributeNode( icon );
QDomAttr volume = dd.createAttribute( "volume" );
volume.setValue( QString::number( i->volume ) );
v.setAttributeNode( volume );
QDomAttr rate = dd.createAttribute( "rate" );
rate.setValue( QString::number( i->rate ) );
v.setAttributeNode( rate );
} }
} }

View file

@ -383,25 +383,37 @@ typedef QVector< Program > Programs;
struct VoiceEngine struct VoiceEngine
{ {
bool enabled; bool enabled;
QString id; QString id;
QString name; QString name;
QString iconFilename; QString iconFilename;
int volume; // 0-100 allowed
int rate; // 0-100 allowed
VoiceEngine(): enabled( false ) VoiceEngine(): enabled( false )
, volume( 50 )
, rate( 50 )
{}
VoiceEngine( QString id_, QString name_, int volume_, int rate_ ):
enabled( false )
, id( id_ )
, name( name_ )
, volume( volume_ )
, rate( rate_ )
{}
bool operator == ( VoiceEngine const & other ) const
{ {
} return enabled == other.enabled &&
bool operator == ( VoiceEngine const & other ) const
{
return enabled == other.enabled &&
id == other.id && id == other.id &&
name == other.name && name == other.name &&
iconFilename == other.iconFilename; iconFilename == other.iconFilename &&
} volume == other.volume &&
rate == other.rate;
}
bool operator != ( VoiceEngine const & other ) const bool operator != ( VoiceEngine const & other ) const
{ return ! operator == ( other ); } { return ! operator == ( other ); }
}; };
typedef QVector< VoiceEngine> VoiceEngines; typedef QVector< VoiceEngine> VoiceEngines;

View file

@ -348,7 +348,8 @@ win32 {
texttospeechsource.hh \ texttospeechsource.hh \
sapi.hh \ sapi.hh \
sphelper.hh \ sphelper.hh \
speechclient.hh speechclient.hh \
speechhlp.hh
} }
mac { mac {

View file

@ -2,6 +2,7 @@
#define __SPEECHCLIENT_HH_INCLUDED__ #define __SPEECHCLIENT_HH_INCLUDED__
#include <QObject> #include <QObject>
#include "config.hh"
class SpeechClient: public QObject class SpeechClient: public QObject
{ {
@ -13,18 +14,28 @@ public:
{ {
QString id; QString id;
QString name; QString name;
// Volume and rate may vary from 0 to 100
int volume;
int rate;
Engine( Config::VoiceEngine const & e ) :
id( e.id )
, name( e.name )
, volume( e.volume )
, rate( e.rate )
{}
}; };
typedef QList<Engine> Engines; typedef QList<Engine> Engines;
SpeechClient( QString const & engineId, QObject * parent = 0L ); SpeechClient( Config::VoiceEngine const & e, QObject * parent = 0L );
virtual ~SpeechClient(); virtual ~SpeechClient();
static Engines availableEngines(); static Engines availableEngines();
const Engine & engine() const; const Engine & engine() const;
bool tell( QString const & text );
bool say( QString const & text ); bool tell( QString const & text, int volume = -1, int rate = -1 );
bool say( QString const & text, int volume = -1, int rate = -1 );
signals: signals:
void started( bool ok ); void started( bool ok );

View file

@ -7,10 +7,13 @@
struct SpeechClient::InternalData struct SpeechClient::InternalData
{ {
InternalData( QString const & engineId ): InternalData( Config::VoiceEngine const & e ):
waitingFinish( false ) waitingFinish( false )
, engine( e )
, oldVolume( -1 )
, oldRate( -1 )
{ {
sp = speechCreate( engineId.toStdWString().c_str() ); sp = speechCreate( e.id.toStdWString().c_str() );
} }
~InternalData() ~InternalData()
@ -19,13 +22,15 @@ struct SpeechClient::InternalData
} }
SpeechHelper sp; SpeechHelper sp;
Engine engine;
bool waitingFinish; bool waitingFinish;
Engine engine;
int oldVolume;
int oldRate;
}; };
SpeechClient::SpeechClient( QString const & engineId, QObject * parent ): SpeechClient::SpeechClient( Config::VoiceEngine const & e, QObject * parent ):
QObject( parent ), QObject( parent ),
internalData( new InternalData( engineId ) ) internalData( new InternalData( e ) )
{ {
} }
@ -40,11 +45,10 @@ static bool enumEngines( void * /* token */,
void * userData ) void * userData )
{ {
SpeechClient::Engines * pEngines = ( SpeechClient::Engines * )userData; SpeechClient::Engines * pEngines = ( SpeechClient::Engines * )userData;
SpeechClient::Engine engine = SpeechClient::Engine engine( Config::VoiceEngine(
{
QString::fromWCharArray( id ), QString::fromWCharArray( id ),
QString::fromWCharArray( name ) QString::fromWCharArray( name ),
}; 50, 50 ) );
pEngines->push_back( engine ); pEngines->push_back( engine );
return true; return true;
} }
@ -61,7 +65,7 @@ const SpeechClient::Engine & SpeechClient::engine() const
return internalData->engine; return internalData->engine;
} }
bool SpeechClient::tell( QString const & text ) bool SpeechClient::tell( QString const & text, int volume, int rate )
{ {
if ( !speechAvailable( internalData->sp ) ) if ( !speechAvailable( internalData->sp ) )
return false; return false;
@ -69,7 +73,16 @@ bool SpeechClient::tell( QString const & text )
if ( internalData->waitingFinish ) if ( internalData->waitingFinish )
return false; return false;
if( volume < 0 )
volume = engine().volume;
if( rate < 0 )
rate = engine().rate;
internalData->oldVolume = setSpeechVolume( internalData->sp, volume );
internalData->oldRate = setSpeechRate( internalData->sp, rate );
bool ok = speechTell( internalData->sp, text.toStdWString().c_str() ); bool ok = speechTell( internalData->sp, text.toStdWString().c_str() );
emit started( ok ); emit started( ok );
if ( ok ) if ( ok )
@ -84,12 +97,27 @@ bool SpeechClient::tell( QString const & text )
return ok; return ok;
} }
bool SpeechClient::say( QString const & text ) bool SpeechClient::say( QString const & text, int volume, int rate )
{ {
if ( !speechAvailable( internalData->sp ) ) if ( !speechAvailable( internalData->sp ) )
return false; return false;
return speechSay( internalData->sp, text.toStdWString().c_str() ); if( volume < 0 )
volume = engine().volume;
if( rate < 0 )
rate = engine().rate;
int oldVolume = setSpeechVolume( internalData->sp, volume );
int oldRate = setSpeechRate( internalData->sp, rate );
bool ok = speechSay( internalData->sp, text.toStdWString().c_str() );
if( oldVolume >=0 )
setSpeechVolume( internalData->sp, oldVolume );
if( oldRate >=0 )
setSpeechRate( internalData->sp, oldRate );
return ok;
} }
void SpeechClient::timerEvent( QTimerEvent * evt ) void SpeechClient::timerEvent( QTimerEvent * evt )
@ -103,6 +131,14 @@ void SpeechClient::timerEvent( QTimerEvent * evt )
{ {
killTimer( evt->timerId() ) ; killTimer( evt->timerId() ) ;
internalData->waitingFinish = false; internalData->waitingFinish = false;
if( internalData->oldVolume >=0 )
setSpeechVolume( internalData->sp, internalData->oldVolume );
if( internalData->oldRate >=0 )
setSpeechRate( internalData->sp, internalData->oldRate );
internalData->oldVolume = -1;
internalData->oldRate = -1;
emit finished(); emit finished();
} }
} }

View file

@ -165,3 +165,27 @@ bool speechSay(SpeechHelper sp, const wchar_t *text)
HRESULT hr = sp->voice->Speak(text, SPF_IS_NOT_XML, 0); HRESULT hr = sp->voice->Speak(text, SPF_IS_NOT_XML, 0);
return !!SUCCEEDED(hr); return !!SUCCEEDED(hr);
} }
int setSpeechVolume( SpeechHelper sp, int newVolume )
{
if( !sp || !sp->voice || newVolume < 0 || newVolume > 100 )
return -1;
unsigned short oldVolume;
HRESULT hr = sp->voice->GetVolume( &oldVolume );
if( !SUCCEEDED( hr ) )
return -1;
sp->voice->SetVolume( (unsigned short) newVolume );
return oldVolume;
}
int setSpeechRate( SpeechHelper sp, int newRate )
{
if( !sp || !sp->voice || newRate < 0 || newRate > 100 )
return -1;
long oldRate;
HRESULT hr = sp->voice->GetRate( &oldRate );
if( !SUCCEEDED( hr ) )
return -1;
sp->voice->SetRate( ( newRate - 50 ) / 5 );
return oldRate * 5 + 50;
}

View file

@ -17,6 +17,8 @@ const wchar_t * speechEngineName(SpeechHelper sp);
bool speechTell(SpeechHelper sp, const wchar_t *text); bool speechTell(SpeechHelper sp, const wchar_t *text);
bool speechTellFinished(SpeechHelper sp); bool speechTellFinished(SpeechHelper sp);
bool speechSay(SpeechHelper sp, const wchar_t *text); bool speechSay(SpeechHelper sp, const wchar_t *text);
int setSpeechVolume( SpeechHelper sp, int newVolume );
int setSpeechRate( SpeechHelper sp, int newRate );
#ifdef __cplusplus #ifdef __cplusplus
} }

View file

@ -17,6 +17,11 @@
#define SPERR_NOT_FOUND MAKE_SAPI_ERROR(0x03a) #define SPERR_NOT_FOUND MAKE_SAPI_ERROR(0x03a)
#endif #endif
#ifdef _SAPI_VER
#undef _SAPI_VER
#endif
#define _SAPI_VER 0503
inline void SpHexFromUlong(WCHAR * psz, ULONG ul) inline void SpHexFromUlong(WCHAR * psz, ULONG ul)
{ {
// If for some reason we cannot convert a number, set it to 0 // If for some reason we cannot convert a number, set it to 0

View file

@ -25,6 +25,30 @@ TextToSpeechSource::TextToSpeechSource( QWidget * parent,
{ {
ui.availableVoiceEngines->addItem( engine.name, engine.id ); ui.availableVoiceEngines->addItem( engine.name, engine.id );
} }
if( voiceEngines.count() > 0 )
{
QModelIndex const &idx = ui.selectedVoiceEngines->model()->index( 0, 0 );
if( idx.isValid() )
ui.selectedVoiceEngines->setCurrentIndex( idx );
}
adjustSliders();
connect( ui.volumeSlider, SIGNAL( valueChanged( int ) ),
this, SLOT( slidersChanged() ) );
connect( ui.rateSlider, SIGNAL( valueChanged( int ) ),
this, SLOT( slidersChanged() ) );
connect( ui.selectedVoiceEngines->selectionModel(), SIGNAL( selectionChanged( QItemSelection, QItemSelection ) ),
this, SLOT( selectionChanged() ) );
}
void TextToSpeechSource::slidersChanged()
{
if( ui.selectedVoiceEngines->selectionModel()->hasSelection() )
voiceEnginesModel.setEngineParams( ui.selectedVoiceEngines->currentIndex(),
ui.volumeSlider->value(),
ui.rateSlider->value()) ;
} }
void TextToSpeechSource::on_addVoiceEngine_clicked() void TextToSpeechSource::on_addVoiceEngine_clicked()
@ -37,11 +61,14 @@ void TextToSpeechSource::on_addVoiceEngine_clicked()
return; return;
} }
// Fake id and name int idx = ui.availableVoiceEngines->currentIndex();
QString name = ui.availableVoiceEngines->itemText( 0 ); if( idx >= 0 )
QString id = ui.availableVoiceEngines->itemData( 0 ).toString(); {
voiceEnginesModel.addNewVoiceEngine( id, name ); QString name = ui.availableVoiceEngines->itemText( idx );
fitSelectedVoiceEnginesColumns(); QString id = ui.availableVoiceEngines->itemData( idx ).toString();
voiceEnginesModel.addNewVoiceEngine( id, name, ui.volumeSlider->value(), ui.rateSlider->value() );
fitSelectedVoiceEnginesColumns();
}
} }
void TextToSpeechSource::on_removeVoiceEngine_clicked() void TextToSpeechSource::on_removeVoiceEngine_clicked()
@ -66,8 +93,12 @@ void TextToSpeechSource::on_previewVoice_clicked()
return; return;
QString engineId = ui.availableVoiceEngines->itemData( idx ).toString(); QString engineId = ui.availableVoiceEngines->itemData( idx ).toString();
QString name = ui.availableVoiceEngines->itemText( idx );
QString text = ui.previewText->text(); QString text = ui.previewText->text();
SpeechClient * speechClient = new SpeechClient( engineId, this ); int volume = ui.volumeSlider->value();
int rate = ui.rateSlider->value();
SpeechClient * speechClient = new SpeechClient( Config::VoiceEngine( engineId, name, volume, rate ), this );
connect( speechClient, SIGNAL( started( bool ) ), ui.previewVoice, SLOT( setDisabled( bool ) ) ); connect( speechClient, SIGNAL( started( bool ) ), ui.previewVoice, SLOT( setDisabled( bool ) ) );
connect( speechClient, SIGNAL( finished() ), this, SLOT( previewVoiceFinished() ) ); connect( speechClient, SIGNAL( finished() ), this, SLOT( previewVoiceFinished() ) );
@ -87,6 +118,35 @@ void TextToSpeechSource::fitSelectedVoiceEnginesColumns()
ui.selectedVoiceEngines->resizeColumnToContents( VoiceEnginesModel::kColumnIcon ); ui.selectedVoiceEngines->resizeColumnToContents( VoiceEnginesModel::kColumnIcon );
} }
void TextToSpeechSource::adjustSliders()
{
QModelIndex const & index = ui.selectedVoiceEngines->currentIndex();
if ( index.isValid() )
{
Config::VoiceEngines const &engines = voiceEnginesModel.getCurrentVoiceEngines();
ui.volumeSlider->setValue( engines[ index.row() ].volume );
ui.rateSlider->setValue( engines[ index.row() ].rate );
return;
}
ui.volumeSlider->setValue( 50 );
ui.rateSlider->setValue( 50 );
}
void TextToSpeechSource::selectionChanged()
{
disconnect( ui.volumeSlider, SIGNAL( valueChanged( int ) ),
this, SLOT( slidersChanged() ) );
disconnect( ui.rateSlider, SIGNAL( valueChanged( int ) ),
this, SLOT( slidersChanged() ) );
adjustSliders();
connect( ui.volumeSlider, SIGNAL( valueChanged( int ) ),
this, SLOT( slidersChanged() ) );
connect( ui.rateSlider, SIGNAL( valueChanged( int ) ),
this, SLOT( slidersChanged() ) );
}
VoiceEnginesModel::VoiceEnginesModel( QWidget * parent, VoiceEnginesModel::VoiceEnginesModel( QWidget * parent,
Config::VoiceEngines const & voiceEngines ): Config::VoiceEngines const & voiceEngines ):
QAbstractItemModel( parent ), voiceEngines( voiceEngines ) QAbstractItemModel( parent ), voiceEngines( voiceEngines )
@ -100,7 +160,8 @@ void VoiceEnginesModel::removeVoiceEngine( int index )
endRemoveRows(); endRemoveRows();
} }
void VoiceEnginesModel::addNewVoiceEngine( QString const & id, QString const & name ) void VoiceEnginesModel::addNewVoiceEngine( QString const & id, QString const & name,
int volume, int rate )
{ {
if ( id.isEmpty() || name.isEmpty() ) if ( id.isEmpty() || name.isEmpty() )
return; return;
@ -109,6 +170,8 @@ void VoiceEnginesModel::addNewVoiceEngine( QString const & id, QString const & n
v.enabled = true; v.enabled = true;
v.id = id; v.id = id;
v.name = name; v.name = name;
v.volume = volume;
v.rate = rate;
beginInsertRows( QModelIndex(), voiceEngines.size(), voiceEngines.size() ); beginInsertRows( QModelIndex(), voiceEngines.size(), voiceEngines.size() );
voiceEngines.push_back( v ); voiceEngines.push_back( v );
@ -243,6 +306,15 @@ bool VoiceEnginesModel::setData( QModelIndex const & index, const QVariant & val
return false; return false;
} }
void VoiceEnginesModel::setEngineParams( QModelIndex idx, int volume, int rate )
{
if ( idx.isValid() )
{
voiceEngines[ idx.row() ].volume = volume;
voiceEngines[ idx.row() ].rate = rate;
}
}
VoiceEngineEditor::VoiceEngineEditor( SpeechClient::Engines const & engines, QWidget * parent ): VoiceEngineEditor::VoiceEngineEditor( SpeechClient::Engines const & engines, QWidget * parent ):
QComboBox( parent ) QComboBox( parent )
{ {

View file

@ -29,10 +29,12 @@ public:
VoiceEnginesModel( QWidget * parent, Config::VoiceEngines const & voiceEngines ); VoiceEnginesModel( QWidget * parent, Config::VoiceEngines const & voiceEngines );
void removeVoiceEngine( int index ); void removeVoiceEngine( int index );
void addNewVoiceEngine( QString const & id, QString const & name ); void addNewVoiceEngine( QString const & id, QString const & name,
int volume, int rate );
Config::VoiceEngines const & getCurrentVoiceEngines() const Config::VoiceEngines const & getCurrentVoiceEngines() const
{ return voiceEngines; } { return voiceEngines; }
void setEngineParams( QModelIndex idx, int volume, int rate );
QModelIndex index( int row, int column, QModelIndex const & parent ) const; QModelIndex index( int row, int column, QModelIndex const & parent ) const;
QModelIndex parent( QModelIndex const & parent ) const; QModelIndex parent( QModelIndex const & parent ) const;
@ -94,12 +96,15 @@ private slots:
void on_removeVoiceEngine_clicked(); void on_removeVoiceEngine_clicked();
void on_previewVoice_clicked(); void on_previewVoice_clicked();
void previewVoiceFinished(); void previewVoiceFinished();
void slidersChanged();
void selectionChanged();
private: private:
Ui::TextToSpeechSource ui; Ui::TextToSpeechSource ui;
VoiceEnginesModel voiceEnginesModel; VoiceEnginesModel voiceEnginesModel;
void fitSelectedVoiceEnginesColumns(); void fitSelectedVoiceEnginesColumns();
void adjustSliders();
}; };
#endif // __TEXTTOSPEECHSOURCE_HH_INCLUDED__ #endif // __TEXTTOSPEECHSOURCE_HH_INCLUDED__

View file

@ -78,6 +78,73 @@
</item> </item>
</layout> </layout>
</item> </item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Preferences</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Volume:</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="volumeSlider">
<property name="maximum">
<number>100</number>
</property>
<property name="sliderPosition">
<number>50</number>
</property>
<property name="tracking">
<bool>false</bool>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TicksAbove</enum>
</property>
<property name="tickInterval">
<number>10</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Rate:</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="rateSlider">
<property name="maximum">
<number>100</number>
</property>
<property name="sliderPosition">
<number>50</number>
</property>
<property name="tracking">
<bool>false</bool>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TicksAbove</enum>
</property>
<property name="tickInterval">
<number>10</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item> <item>
<widget class="QGroupBox" name="groupBox_2"> <widget class="QGroupBox" name="groupBox_2">
<property name="title"> <property name="title">