Revision control

++ // Copyright (C) 2022 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++ #include "qtexttospeech_winrt.h"
++ #include "qtexttospeech_winrt_audiosource.h"
++
++ #include <QtMultimedia/qaudiodevice.h>
++ #include <QtMultimedia/qaudiosink.h>
++ #include <QtMultimedia/qmediadevices.h>
++ #include <QtMultimedia/private/qaudiosystem_p.h>
++
++ #include <QtCore/QBasicTimer>
++ #include <QtCore/QCoreApplication>
++ #include <QtCore/QElapsedTimer>
++ #include <QtCore/qset.h>
++ #include <QtCore/private/qfunctions_winrt_p.h>
++
++ #include <winrt/base.h>
++ #include <QtCore/private/qfactorycacheregistration_p.h>
++ #include <windows.foundation.h>
++ #include <windows.foundation.collections.h>
++ #include <windows.media.speechsynthesis.h>
++ #include <windows.storage.streams.h>
++
++ #include <wrl.h>
++
++ using namespace Qt::StringLiterals;
++
++ using namespace ABI::Windows::Foundation;
++ using namespace ABI::Windows::Foundation::Collections;
++ using namespace ABI::Windows::Media::SpeechSynthesis;
++ using namespace ABI::Windows::Storage::Streams;
++ using namespace Microsoft::WRL;
++ using namespace Microsoft::WRL::Wrappers;
++
++ QT_BEGIN_NAMESPACE
++
++ class QTextToSpeechEngineWinRTPrivate
++ {
++ Q_DECLARE_PUBLIC(QTextToSpeechEngineWinRT);
++ public:
++ QTextToSpeechEngineWinRTPrivate(QTextToSpeechEngineWinRT *q);
++ ~QTextToSpeechEngineWinRTPrivate();
++
++ void setError(QTextToSpeech::ErrorReason reason, const QString &string);
++ QTextToSpeech::State state = QTextToSpeech::Error;
++ QTextToSpeech::ErrorReason errorReason = QTextToSpeech::ErrorReason::Initialization;
++ QString errorString;
++
++ // interfaces used to access the speech synthesizer
++ ComPtr<ISpeechSynthesizer> synth;
++ ComPtr<ISpeechSynthesizerOptions2> options;
++
++ // data streaming - AudioSource implements COM interfaces as well as
++ // QIODevice, so we store it in a ComPtr instead of a std::unique_ptr
++ ComPtr<AudioSource> audioSource;
++ QList<AudioSource::Boundary> boundaries;
++ QList<AudioSource::Boundary>::const_iterator currentBoundary;
++ QBasicTimer boundaryTimer;
++ QElapsedTimer elapsedTimer;
++ qint64 playedTime = 0;
++ // the sink is connected to the source
++ std::unique_ptr<QAudioSink> audioSink;
++
++ template <typename Fn> void forEachVoice(Fn &&lambda) const;
++ void updateVoices();
++ QVoice createVoiceForInformation(const ComPtr<IVoiceInformation> &info) const;
++ void initializeAudioSink(const QAudioFormat &format);
++ void sinkStateChanged(QAudio::State sinkState);
++
++ private:
++ QAudioDevice audioDevice;
++ QTextToSpeechEngineWinRT *q_ptr;
++ };
++
++
++ QTextToSpeechEngineWinRTPrivate::QTextToSpeechEngineWinRTPrivate(QTextToSpeechEngineWinRT *q)
++ : q_ptr(q)
++ {
++ }
++
++ QTextToSpeechEngineWinRTPrivate::~QTextToSpeechEngineWinRTPrivate()
++ {
++ // Close and free the source explicitly and in the right order so that the buffer's
++ // aboutToClose signal gets emitted before this private object is destroyed.
++ if (audioSource) {
++ audioSource->close();
++ audioSource.Reset();
++ }
++ }
++
++ void QTextToSpeechEngineWinRTPrivate::setError(QTextToSpeech::ErrorReason reason, const QString &string)
++ {
++ Q_Q(QTextToSpeechEngineWinRT);
++ errorReason = reason;
++ errorString = string;
++ if (reason != QTextToSpeech::ErrorReason::NoError)
++ return;
++ if (state != QTextToSpeech::Error) {
++ state = QTextToSpeech::Error;
++ emit q->stateChanged(state);
++ }
++ emit q->errorOccurred(errorReason, errorString);
++ }
++
++ QTextToSpeechEngineWinRT::QTextToSpeechEngineWinRT(const QVariantMap &params, QObject *parent)
++ : QTextToSpeechEngine(parent)
++ , d_ptr(new QTextToSpeechEngineWinRTPrivate(this))
++ {
++ Q_D(QTextToSpeechEngineWinRT);
++
++ if (const auto it = params.find("audioDevice"_L1); it != params.end())
++ d->audioDevice = (*it).value<QAudioDevice>();
++ else
++ d->audioDevice = QMediaDevices::defaultAudioOutput();
++
++ if (d->audioDevice.isNull())
++ d->setError(QTextToSpeech::ErrorReason::Playback,
++ QCoreApplication::translate("QTextToSpeech", "No audio device available."));
++
++ HRESULT hr = CoInitialize(nullptr);
++ Q_ASSERT_SUCCEEDED(hr);
++
++ hr = RoActivateInstance(HString::MakeReference(RuntimeClass_Windows_Media_SpeechSynthesis_SpeechSynthesizer).Get(),
++ &d->synth);
++ if (!SUCCEEDED(hr)) {
++ d->setError(QTextToSpeech::ErrorReason::Initialization,
++ QCoreApplication::translate("QTextToSpeech", "Could not initialize text-to-speech engine."));
++ return;
++ } else if (voice() == QVoice()) {
++ d->setError(QTextToSpeech::ErrorReason::Configuration,
++ QCoreApplication::translate("QTextToSpeech", "Could not set default voice."));
++ } else {
++ d->state = QTextToSpeech::Ready;
++ d->errorReason = QTextToSpeech::ErrorReason::NoError;
++ }
++
++ // the rest is optional, we might not support these features
++
++ ComPtr<ISpeechSynthesizer2> synth2;
++ hr = d->synth->QueryInterface(__uuidof(ISpeechSynthesizer2), &synth2);
++ RETURN_VOID_IF_FAILED("ISpeechSynthesizer2 not implemented.");
++
++ ComPtr<ISpeechSynthesizerOptions> options1;
++ hr = synth2->get_Options(&options1);
++ Q_ASSERT_SUCCEEDED(hr);
++
++ // ask for boundary data, we might not get it
++ options1->put_IncludeSentenceBoundaryMetadata(true);
++ options1->put_IncludeWordBoundaryMetadata(true);
++
++ hr = options1->QueryInterface(__uuidof(ISpeechSynthesizerOptions2), &d->options);
++ RETURN_VOID_IF_FAILED("ISpeechSynthesizerOptions2 not implemented.");
++
++ d->options->put_AudioPitch(1.0);
++ d->options->put_AudioVolume(1.0);
++ d->options->put_SpeakingRate(1.0);
++ }
++
++ QTextToSpeechEngineWinRT::~QTextToSpeechEngineWinRT()
++ {
++ d_ptr.reset();
++ CoUninitialize();
++ }
++
++ /* Voice and language/locale management */
++
++ QVoice QTextToSpeechEngineWinRTPrivate::createVoiceForInformation(const ComPtr<IVoiceInformation> &info) const
++ {
++ Q_Q(const QTextToSpeechEngineWinRT);
++ HRESULT hr;
++
++ HString nativeName;
++ hr = info->get_DisplayName(nativeName.GetAddressOf());
++ Q_ASSERT_SUCCEEDED(hr);
++
++ VoiceGender gender;
++ hr = info->get_Gender(&gender);
++ Q_ASSERT_SUCCEEDED(hr);
++
++ HString voiceLanguage;
++ hr = info->get_Language(voiceLanguage.GetAddressOf());
++ Q_ASSERT_SUCCEEDED(hr);
++
++ HString voiceId;
++ hr = info->get_Id(voiceId.GetAddressOf());
++ Q_ASSERT_SUCCEEDED(hr);
++
++ return q->createVoice(QString::fromWCharArray(nativeName.GetRawBuffer(0)),
++ QLocale(QString::fromWCharArray(voiceLanguage.GetRawBuffer(0))),
++ gender == VoiceGender_Male ? QVoice::Male : QVoice::Female,
++ QVoice::Other,
++ QString::fromWCharArray(voiceId.GetRawBuffer(0)));
++ }
++
++ /*
++ Iterates over all available voices and calls \a lambda with the
++ IVoiceInformation for each. If the lambda returns true, the iteration
++ ends.
++
++ This helper is used in all voice and locale related engine implementations.
++ While not particular fast, none of these functions are performance critical,
++ and always operating on the official list of all voices avoids that we need
++ to maintain our own mappings, or keep all voice information instances in
++ memory.
++ */
++ template <typename Fn>
++ void QTextToSpeechEngineWinRTPrivate::forEachVoice(Fn &&lambda) const
++ {
++ HRESULT hr;
++
++ ComPtr<IInstalledVoicesStatic> stat;
++ hr = RoGetActivationFactory(HString::MakeReference(RuntimeClass_Windows_Media_SpeechSynthesis_SpeechSynthesizer).Get(),
++ IID_PPV_ARGS(&stat));
++ Q_ASSERT_SUCCEEDED(hr);
++
++ ComPtr<IVectorView<VoiceInformation*>> voiceInformations;
++ hr = stat->get_AllVoices(&voiceInformations);
++ RETURN_VOID_IF_FAILED("Could not get voice information.");
++
++ quint32 voiceSize;
++ hr = voiceInformations->get_Size(&voiceSize);
++ RETURN_VOID_IF_FAILED("Could not access size of voice information.");
++
++ for (quint32 i = 0; i < voiceSize; ++i) {
++ ComPtr<IVoiceInformation> voiceInfo;
++ hr = voiceInformations->GetAt(i, &voiceInfo);
++ Q_ASSERT_SUCCEEDED(hr);
++
++ if (lambda(voiceInfo))
++ break;
++ }
++ }
++
++ QList<QLocale> QTextToSpeechEngineWinRT::availableLocales() const
++ {
++ Q_D(const QTextToSpeechEngineWinRT);
++ if (!d->synth)
++ return QList<QLocale>();
++ QSet<QLocale> uniqueLocales;
++ d->forEachVoice([&uniqueLocales](const ComPtr<IVoiceInformation> &voiceInfo) {
++ HString voiceLanguage;
++ HRESULT hr = voiceInfo->get_Language(voiceLanguage.GetAddressOf());
++ Q_ASSERT_SUCCEEDED(hr);
++
++ uniqueLocales.insert(QLocale(QString::fromWCharArray(voiceLanguage.GetRawBuffer(0))));
++ return false;
++ });
++ return uniqueLocales.values();
++ }
++
++ QList<QVoice> QTextToSpeechEngineWinRT::availableVoices() const
++ {
++ Q_D(const QTextToSpeechEngineWinRT);
++ if (!d->synth)
++ return QList<QVoice>();
++ QList<QVoice> voices;
++ const QLocale currentLocale = locale();
++ d->forEachVoice([&](const ComPtr<IVoiceInformation> &voiceInfo) {
++ const QVoice voice = d->createVoiceForInformation(voiceInfo);
++ if (currentLocale == voice.locale())
++ voices.append(voice);
++ return false;
++ });
++ return voices;
++ }
++
++ QLocale QTextToSpeechEngineWinRT::locale() const
++ {
++ Q_D(const QTextToSpeechEngineWinRT);
++ if (!d->synth)
++ return QLocale(QLocale::C, QLocale::AnyTerritory);
++
++ ComPtr<IVoiceInformation> voiceInfo;
++ HRESULT hr = d->synth->get_Voice(&voiceInfo);
++ if (FAILED(hr) || !voiceInfo)
++ return QLocale(QLocale::C, QLocale::AnyTerritory);
++
++ HString language;
++ hr = voiceInfo->get_Language(language.GetAddressOf());
++ if (FAILED(hr))
++ return QLocale(QLocale::C, QLocale::AnyTerritory);
++
++ return QLocale(QString::fromWCharArray(language.GetRawBuffer(0)));
++ }
++
++ bool QTextToSpeechEngineWinRT::setLocale(const QLocale &locale)
++ {
++ Q_D(QTextToSpeechEngineWinRT);
++ if (!d->synth)
++ return false;
++
++ ComPtr<IVoiceInformation> foundVoice;
++ d->forEachVoice([&locale, &foundVoice](const ComPtr<IVoiceInformation> &voiceInfo) {
++ HString voiceLanguage;
++ HRESULT hr = voiceInfo->get_Language(voiceLanguage.GetAddressOf());
++ Q_ASSERT_SUCCEEDED(hr);
++
++ if (locale == QLocale(QString::fromWCharArray(voiceLanguage.GetRawBuffer(0)))) {
++ foundVoice = voiceInfo;
++ return true;
++ }
++ return false;
++ });
++
++ if (!foundVoice) {
++ d->setError(QTextToSpeech::ErrorReason::Configuration,
++ QCoreApplication::translate("QTextToSpeech", "No voice available for locale %1.")
++ .arg(locale.bcp47Name()));
++ return false;
++ }
++
++ return SUCCEEDED(d->synth->put_Voice(foundVoice.Get()));
++ }
++
++ QVoice QTextToSpeechEngineWinRT::voice() const
++ {
++ Q_D(const QTextToSpeechEngineWinRT);
++ if (!d->synth)
++ return QVoice();
++
++ ComPtr<IVoiceInformation> voiceInfo;
++ d->synth->get_Voice(&voiceInfo);
++
++ return d->createVoiceForInformation(voiceInfo);
++ }
++
++ bool QTextToSpeechEngineWinRT::setVoice(const QVoice &voice)
++ {
++ Q_D(QTextToSpeechEngineWinRT);
++ if (!d->synth)
++ return false;
++
++ const QString data = QTextToSpeechEngine::voiceData(voice).toString();
++ if (data.isEmpty())
++ d->setError(QTextToSpeech::ErrorReason::Configuration,
++ QCoreApplication::translate("QTextToSpeech", "Invalid voice."));
++
++ ComPtr<IVoiceInformation> foundVoice;
++ d->forEachVoice([&data, &foundVoice](const ComPtr<IVoiceInformation> &voiceInfo) {
++ HString voiceId;
++ HRESULT hr = voiceInfo->get_Id(voiceId.GetAddressOf());
++ if (FAILED(hr))
++ return false;
++
++ if (data == QString::fromWCharArray(voiceId.GetRawBuffer(0))) {
++ foundVoice = voiceInfo;
++ return true;
++ }
++ return false;
++ });
++
++ if (!foundVoice) {
++ d->setError(QTextToSpeech::ErrorReason::Configuration,
++ QCoreApplication::translate("QTextToSpeech", "Invalid voice."));
++ return false;
++ }
++
++ return SUCCEEDED(d->synth->put_Voice(foundVoice.Get()));
++ }
++
++ void QTextToSpeechEngineWinRT::timerEvent(QTimerEvent *e)
++ {
++ Q_D(QTextToSpeechEngineWinRT);
++ if (e->timerId() == d->boundaryTimer.timerId()) {
++ const qint64 elapsed = d->elapsedTimer.nsecsElapsed() / 1000 + d->playedTime;
++ if (d->currentBoundary->type == AudioSource::Boundary::Word)
++ emit sayingWord(d->currentBoundary->text, d->currentBoundary->beginIndex,
++ d->currentBoundary->endIndex - d->currentBoundary->beginIndex + 1);
++ ++d->currentBoundary;
++
++ if (d->audioSource && d->currentBoundary != d->boundaries.constEnd()) {
++ const qint64 msecsToNext = qMax((d->currentBoundary->startTime - elapsed) / 1000, 0);
++ d->boundaryTimer.start(msecsToNext, Qt::PreciseTimer, this);
++ } else {
++ d->boundaryTimer.stop();
++ }
++ }
++ QObject::timerEvent(e);
++ }
++
++ /* State and speech control */
++
++ QTextToSpeech::State QTextToSpeechEngineWinRT::state() const
++ {
++ Q_D(const QTextToSpeechEngineWinRT);
++ return d->state;
++ }
++
++ QTextToSpeech::ErrorReason QTextToSpeechEngineWinRT::errorReason() const
++ {
++ Q_D(const QTextToSpeechEngineWinRT);
++ return d->errorReason;
++ }
++
++ QString QTextToSpeechEngineWinRT::errorString() const
++ {
++ Q_D(const QTextToSpeechEngineWinRT);
++ return d->errorString;
++ }
++
++ void QTextToSpeechEngineWinRTPrivate::initializeAudioSink(const QAudioFormat &format)
++ {
++ Q_Q(const QTextToSpeechEngineWinRT);
++
++ // cancelled or another call to say() while waiting for the synthesizer
++ if (!audioSource)
++ return;
++
++ boundaries = audioSource->boundaryData();
++ currentBoundary = boundaries.constBegin();
++
++ audioSink.reset(new QAudioSink(audioDevice, format));
++ // LATER: use public API (compare QTBUG-138378)
++ QPlatformAudioSink *platformAudioSink = QPlatformAudioSink::get(*audioSink);
++ if (platformAudioSink)
++ platformAudioSink->setRole(QPlatformAudioSink::AudioEndpointRole::Accessibility);
++
++ QObject::connect(audioSink.get(), &QAudioSink::stateChanged,
++ q, [this](QAudio::State sinkState) {
++ sinkStateChanged(sinkState);
++ });
++ audioSink->start(audioSource.Get());
++ }
++
++ void QTextToSpeechEngineWinRTPrivate::sinkStateChanged(QAudio::State sinkState)
++ {
++ Q_Q(QTextToSpeechEngineWinRT);
++
++ const auto oldState = state;
++ switch (sinkState) {
++ case QAudio::IdleState:
++ if (audioSource) {
++ if (audioSource->atEnd()) {
++ state = QTextToSpeech::Ready;
++ playedTime = 0;
++ elapsedTimer.invalidate();
++ } else {
++ boundaryTimer.stop();
++ if (elapsedTimer.isValid())
++ playedTime += elapsedTimer.nsecsElapsed() / 1000;
++ elapsedTimer.invalidate();
++ audioSink->suspend();
++ }
++ }
++ break;
++ case QAudio::StoppedState:
++ state = QTextToSpeech::Ready;
++ playedTime = 0;
++ elapsedTimer.invalidate();
++ break;
++ case QAudio::ActiveState:
++ // boundaries are in micro-seconds, timers have msec precision
++ if (currentBoundary != boundaries.constEnd())
++ boundaryTimer.start(qMax(currentBoundary->startTime - playedTime, 0) / 1000, Qt::PreciseTimer, q);
++ elapsedTimer.start();
++ state = QTextToSpeech::Speaking;
++ break;
++ case QAudio::SuspendedState:
++ if (audioSource->m_pause != AudioSource::NoPause)
++ state = QTextToSpeech::Paused;
++ break;
++ }
++ if (state != oldState)
++ emit q->stateChanged(state);
++ }
++
++ void QTextToSpeechEngineWinRT::say(const QString &text)
++ {
++ Q_D(QTextToSpeechEngineWinRT);
++ if (!d->synth)
++ return;
++
++ // stop ongoing speech
++ stop(QTextToSpeech::BoundaryHint::Default);
++
++ HRESULT hr = S_OK;
++
++ HStringReference nativeText(reinterpret_cast<LPCWSTR>(text.utf16()), text.length());
++
++ ComPtr<IAsyncOperation<SpeechSynthesisStream*>> synthOperation;
++ hr = d->synth->SynthesizeTextToStreamAsync(nativeText.Get(), &synthOperation);
++ if (!SUCCEEDED(hr)) {
++ d->setError(QTextToSpeech::ErrorReason::Input,
++ QCoreApplication::translate("QTextToSpeech", "Speech synthesizing failure."));
++ return;
++ }
++
++ // The source will wait for the the data resulting out of the synthOperation, and emits
++ // streamReady when data is available. This starts a QAudioSink, which pulls the data.
++ d->audioSource.Attach(new AudioSource(synthOperation));
++
++ connect(d->audioSource.Get(), &AudioSource::streamReady, this, [d](const QAudioFormat &format){
++ d->initializeAudioSink(format);
++ });
++ connect(d->audioSource.Get(), &AudioSource::errorInStream, this, [this]{
++ Q_D(QTextToSpeechEngineWinRT);
++ d->setError(QTextToSpeech::ErrorReason::Playback,
++ QCoreApplication::translate("QTextToSpeech", "Error in audio stream."));
++ });
++ connect(d->audioSource.Get(), &QIODevice::aboutToClose, this, [d]{
++ d->boundaryTimer.stop();
++ d->audioSink.reset();
++ });
++ }
++
++ void QTextToSpeechEngineWinRT::synthesize(const QString &text)
++ {
++ Q_D(QTextToSpeechEngineWinRT);
++
++ HRESULT hr = S_OK;
++
++ HStringReference nativeText(reinterpret_cast<LPCWSTR>(text.utf16()), text.length());
++
++ ComPtr<IAsyncOperation<SpeechSynthesisStream*>> synthOperation;
++ hr = d->synth->SynthesizeTextToStreamAsync(nativeText.Get(), &synthOperation);
++ if (!SUCCEEDED(hr)) {
++ d->setError(QTextToSpeech::ErrorReason::Input,
++ QCoreApplication::translate("QTextToSpeech", "Speech synthesizing failure."));
++ return;
++ }
++
++ // The source will wait for the the data resulting out of the synthOperation, and emits
++ // streamReady when data is available. This starts a QAudioSink, which pulls the data.
++ d->audioSource.Attach(new AudioSource(synthOperation));
++
++ connect(d->audioSource.Get(), &AudioSource::streamReady, this, [d, this](const QAudioFormat &format){
++ if (d->state != QTextToSpeech::Synthesizing) {
++ d->state = QTextToSpeech::Synthesizing;
++ emit stateChanged(d->state);
++ }
++ });
++ connect(d->audioSource.Get(), &AudioSource::readyRead, this, [d, this](){
++ Q_ASSERT(d->state == QTextToSpeech::Synthesizing);
++ const QByteArray data = d->audioSource->read(d->audioSource->bytesAvailable());
++ emit synthesized(d->audioSource->format(), data);
++ if (d->audioSource->atEnd())
++ d->audioSource->close();
++ });
++ connect(d->audioSource.Get(), &AudioSource::aboutToClose, this, [d, this]{
++ if (d->state != QTextToSpeech::Ready) {
++ d->state = QTextToSpeech::Ready;
++ emit stateChanged(d->state);
++ }
++ });
++ connect(d->audioSource.Get(), &AudioSource::errorInStream, this, [d]{
++ d->setError(QTextToSpeech::ErrorReason::Input,
++ QCoreApplication::translate("QTextToSpeech", "Error synthesizing audio stream."));
++ });
++ }
++
++ void QTextToSpeechEngineWinRT::stop(QTextToSpeech::BoundaryHint boundaryHint)
++ {
++ Q_UNUSED(boundaryHint);
++ Q_D(QTextToSpeechEngineWinRT);
++
++ if (d->audioSource) {
++ d->audioSource->close();
++ d->audioSource.Reset();
++ }
++ }
++
++ void QTextToSpeechEngineWinRT::pause(QTextToSpeech::BoundaryHint boundaryHint)
++ {
++ Q_D(QTextToSpeechEngineWinRT);
++
++ if (!d->audioSource)
++ return;
++
++ auto pauseBoundaryType = AudioSource::Boundary::Unknown;
++ switch (boundaryHint) {
++ case QTextToSpeech::BoundaryHint::Default:
++ d->audioSource->pause(0);
++ return;
++ case QTextToSpeech::BoundaryHint::Immediate:
++ d->audioSource->pause(0);
++ if (d->audioSink)
++ d->audioSink->suspend();
++ return;
++ case QTextToSpeech::BoundaryHint::Word:
++ pauseBoundaryType = AudioSource::Boundary::Word;
++ break;
++ case QTextToSpeech::BoundaryHint::Sentence:
++ pauseBoundaryType = AudioSource::Boundary::Sentence;
++ break;
++ case QTextToSpeech::BoundaryHint::Utterance:
++ // taken care off by engine-independent implementation
++ return;
++ }
++
++ // find the next boundary of the matching type
++ const auto nextBoundary = std::find_if(d->currentBoundary + 1, d->boundaries.constEnd(),
++ [pauseBoundaryType](auto &&it){
++ return it.type == pauseBoundaryType;
++ });
++ if (nextBoundary != d->boundaries.constEnd()) {
++ d->audioSource->pause(d->audioSource->format()
++ .bytesForDuration(nextBoundary->startTime));
++ }
++ }
++
++ void QTextToSpeechEngineWinRT::resume()
++ {
++ Q_D(QTextToSpeechEngineWinRT);
++
++ if (d->audioSource)
++ d->audioSource->resume();
++ if (d->audioSink)
++ d->audioSink->resume();
++ }
++
++ /* Properties */
++
++ /*
++ The native value can range from 0.5 (half the default rate) to 6.0 (6x the default rate), inclusive.
++ The default value is 1.0 (the "normal" speaking rate for the current voice).
++
++ QTextToSpeech rate is from -1.0 to 1.0.
++ */
++ double QTextToSpeechEngineWinRT::rate() const
++ {
++ Q_D(const QTextToSpeechEngineWinRT);
++
++ if (!d->options) {
++ Q_UNIMPLEMENTED();
++ return 0.0;
++ }
++
++ double value;
++ d->options->get_SpeakingRate(&value);
++ if (value < 1.0) // 0.5 to 1.0 maps to -1.0 to 0.0
++ value = (value - 1.0) * 2.0;
++ else // 1.0 to 6.0 maps to 0.0 to 1.0
++ value = (value - 1.0) / 5.0;
++ return value;
++ }
++
++ bool QTextToSpeechEngineWinRT::setRate(double rate)
++ {
++ Q_D(QTextToSpeechEngineWinRT);
++
++ if (!d->options) {
++ Q_UNIMPLEMENTED();
++ return false;
++ }
++
++ if (rate < 0.0) // -1.0 to 0.0 maps to 0.5 to 1.0
++ rate = 0.5 * rate + 1.0;
++ else // 0.0 to 1.0 maps to 1.0 to 6.0
++ rate = rate * 5.0 + 1.0;
++ HRESULT hr = d->options->put_SpeakingRate(rate);
++ return SUCCEEDED(hr);
++ }
++
++ /*
++ The native value can range from 0.0 (lowest pitch) to 2.0 (highest pitch), inclusive.
++ The default value is 1.0.
++
++ The QTextToSpeech value is from -1.0 to 1.0.
++ */
++ double QTextToSpeechEngineWinRT::pitch() const
++ {
++ Q_D(const QTextToSpeechEngineWinRT);
++
++ if (!d->options) {
++ Q_UNIMPLEMENTED();
++ return 0.0;
++ }
++
++ double value;
++ d->options->get_AudioPitch(&value);
++ return value - 1.0;
++ }
++
++ bool QTextToSpeechEngineWinRT::setPitch(double pitch)
++ {
++ Q_D(QTextToSpeechEngineWinRT);
++
++ if (!d->options) {
++ Q_UNIMPLEMENTED();
++ return false;
++ }
++
++ HRESULT hr = d->options->put_AudioPitch(pitch + 1.0);
++ return SUCCEEDED(hr);
++ }
++
++ /*
++ The native value can range from 0.0 (lowest volume) to 1.0 (highest volume), inclusive.
++ The default value is 1.0.
++
++ This maps directly to the QTextToSpeech volume range.
++ */
++ double QTextToSpeechEngineWinRT::volume() const
++ {
++ Q_D(const QTextToSpeechEngineWinRT);
++
++ if (!d->options) {
++ Q_UNIMPLEMENTED();
++ return 0.0;
++ }
++
++ double value;
++ d->options->get_AudioVolume(&value);
++ return value;
++ }
++
++ bool QTextToSpeechEngineWinRT::setVolume(double volume)
++ {
++ Q_D(QTextToSpeechEngineWinRT);
++
++ if (!d->options) {
++ Q_UNIMPLEMENTED();
++ return false;
++ }
++
++ HRESULT hr = d->options->put_AudioVolume(volume);
++ return SUCCEEDED(hr);
++ }
++
++ QT_END_NAMESPACE