Source code

Revision control

Copy as Markdown

// 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:critical reason:data-parser
#include "qtexttospeech_winrt_audiosource.h"
#include <QtCore/qdebug.h>
#include <QtCore/qspan.h>
#include <QtCore/qtimer.h>
#include <QtCore/private/qfunctions_winrt_p.h>
#include <QtCore/private/qsystemerror_p.h>
#include <robuffer.h>
#include <winrt/base.h>
#include <QtCore/private/qfactorycacheregistration_p.h>
#include <windows.foundation.h>
#include <windows.foundation.collections.h>
#include <windows.media.core.h>
#include <windows.media.speechsynthesis.h>
#include <windows.storage.streams.h>
#include <wrl.h>
using namespace ABI::Windows::Foundation;
using namespace ABI::Windows::Foundation::Collections;
using namespace ABI::Windows::Media::Core;
using namespace ABI::Windows::Media::SpeechSynthesis;
using namespace ABI::Windows::Storage::Streams;
using namespace Microsoft::WRL;
using namespace Microsoft::WRL::Wrappers;
QT_BEGIN_NAMESPACE
/*
AudioSource implements a sequential QIODevice for a stream of synthesized speech.
It also implements the handler interfaces for responding to the asynchronous
operations of the synthesized speech stream being avialable, and reading data from
the stream via a COM buffer.
Whenever the QIODevice has read all the data from the COM buffer, more data is
requested. When the data is available (the BytesReadyHandler's handler's Invoke
implementation is called), readyRead is emitted.
The AudioSource directly controls a QAudioSink. As soon as the data stream is
available, the source calls QAudioSink::start. Pause/resume are delegated to the
sink; closing the source stops the sink.
*/
AudioSource::AudioSource(ComPtr<IAsyncOperation<SpeechSynthesisStream*>> synthOperation)
: synthOperation(synthOperation)
{
synthOperation->put_Completed(this);
audioFormat.setSampleFormat(QAudioFormat::Int16);
audioFormat.setSampleRate(16000);
audioFormat.setChannelConfig(QAudioFormat::ChannelConfigMono);
}
/*
Calls close, which is virtual and called from ~QIODevice,
but that won't call our override.
*/
AudioSource::~AudioSource()
{
close();
}
/*
Cancel any incomplete asynchronous operations, and stop the
sink before closing the QIODevice.
*/
void AudioSource::close()
{
ComPtr<IAsyncInfo> asyncInfo;
AsyncStatus status = AsyncStatus::Completed;
if (synthOperation) {
if (HRESULT hr = synthOperation.As(&asyncInfo); SUCCEEDED(hr)) {
asyncInfo->get_Status(&status);
if (status != AsyncStatus::Completed)
asyncInfo->Cancel();
}
}
if (readOperation) {
if (HRESULT hr = readOperation.As(&asyncInfo); SUCCEEDED(hr)) {
asyncInfo->get_Status(&status);
if (status != AsyncStatus::Completed)
asyncInfo->Cancel();
}
}
QIODevice::close();
}
qint64 AudioSource::bytesAvailable() const
{
return m_bufferSpan.size_bytes() + QIODevice::bytesAvailable();
}
/*
Fills data with as many bytes from the COM buffer as possible. If this
empties the COM buffer, calls fetchMore to start a new asynchronous
read operation.
*/
qint64 AudioSource::readData(char *data, qint64 maxlen)
{
// this may happen as per the documentation
if (!maxlen)
return 0;
if (m_bufferSpan.isEmpty() && atEnd())
return -1;
auto take = [](auto span, qsizetype n) {
return span.first(std::min(span.size(), n));
};
auto drop = [](auto span, qsizetype n) {
return span.subspan(std::min(span.size(), n));
};
QSpan source = take(m_bufferSpan, maxlen);
switch (m_pause) {
case NoPause:
break;
case PauseRequested: {
Q_ASSERT(audioFormat.sampleFormat() == QAudioFormat::Int16);
if (m_pauseRequestedAt) {
if (m_pauseRequestedAt <= m_bytesRead) {
// we missed the window, pause immediately
source = {};
} else if (m_pauseRequestedAt <= m_bytesRead + quint64(source.size())) {
source = take(source, qsizetype(m_pauseRequestedAt - m_bytesRead));
} else {
break; // pause point is in a later chunk
}
m_pause = Paused;
m_pauseRequestedAt = 0;
break;
}
// If no byte to pause at is specified, look for silence in the current
// chunk. We are dealing with artificially created sound, so we don't have
// to find a large enough window with overall low energy; we can just
// look for a series (e.g. 1/50th of a second) of samples with low
// absolute values.
const int silenceDuration = audioFormat.sampleRate() / 50;
const auto samples = QSpan(reinterpret_cast<const short *>(source.data()),
source.size() / sizeof(short));
if (!m_pauseDetectionSilenceCount.has_value())
m_pauseDetectionSilenceCount = 0;
for (qsizetype i = 0; i < samples.size(); ++i) {
if (qAbs(samples[i]) < 10)
*m_pauseDetectionSilenceCount += 1;
else {
*m_pauseDetectionSilenceCount = 0;
continue;
}
if (*m_pauseDetectionSilenceCount > silenceDuration) {
// long enough silence found, only provide data until we are in the silence.
// we will still try to play at least half of the silent part
const qsizetype silentSamplesToPlay = silenceDuration / 2;
source = take(source,
(i > silentSamplesToPlay ? i - silentSamplesToPlay : i)
* sizeof(short));
m_pause = Paused;
m_pauseDetectionSilenceCount = std::nullopt;
break;
} else {
*m_pauseDetectionSilenceCount = 0;
}
}
// no silence found - stop after this chunk
if (m_pause != Paused) {
m_pauseDetectionSilenceCount = std::nullopt;
m_pause = Paused;
}
break;
}
case Paused:
// starve the sink so that it goes idle
return 0;
}
if (source.isEmpty())
return 0;
memcpy(data, source.data(), source.size());
m_bufferSpan = drop(m_bufferSpan, source.size());
if (m_bufferSpan.isEmpty())
fetchMore();
m_bytesRead += source.size();
return source.size();
}
bool AudioSource::atEnd() const
{
// not done as long as QIODevice's buffer is not empty
if (!QIODevice::atEnd() && QIODevice::bytesAvailable())
return false;
// If we get here, bytesAvailable() has returned 0, so our buffers are
// exhaused. Try to see if we are waiting for readOperation to finish.
AsyncStatus status = AsyncStatus::Completed;
if (readOperation) {
ComPtr<IAsyncInfo> asyncInfo;
if (HRESULT hr = readOperation.As(&asyncInfo); SUCCEEDED(hr))
asyncInfo->get_Status(&status);
}
if (status == AsyncStatus::Started)
return false;
// ... or if there is more in the stream
UINT64 ioPos = 0;
UINT64 ioSize = 0;
if (randomAccessStream) {
randomAccessStream->get_Size(&ioSize);
randomAccessStream->get_Position(&ioPos);
}
return ioPos >= ioSize;
}
/*
Completion handler for synthesising the stream.
Is called as soon as synthesized pcm data is available, at which point we can
open the QIODevice, fetch data, and start the audio sink.
*/
HRESULT AudioSource::Invoke(IAsyncOperation<SpeechSynthesisStream*> *operation, AsyncStatus status)
{
Q_ASSERT(operation == synthOperation.Get());
ComPtr<IAsyncInfo> asyncInfo;
synthOperation.As(&asyncInfo);
if (status == AsyncStatus::Error) {
QString errorString;
if (asyncInfo) {
HRESULT errorCode;
asyncInfo->get_ErrorCode(&errorCode);
if (errorCode == 0x80131537) // Windows gives us only an Unknown error
errorString = QStringLiteral("Error when synthesizing: Input format error");
else
errorString = QSystemError(errorCode, QSystemError::NativeError).toString();
} else {
errorString = QStringLiteral("Error when synthesizing: no information available");
}
setErrorString(errorString);
emit errorInStream();
}
if (status != AsyncStatus::Completed) {
if (asyncInfo)
asyncInfo->Close();
synthOperation.Reset();
return E_FAIL;
}
ComPtr<ISpeechSynthesisStream> speechStream;
HRESULT hr = operation->GetResults(&speechStream);
RETURN_HR_IF_FAILED("Could not access stream.");
hr = speechStream.As(&inputStream);
RETURN_HR_IF_FAILED("Could not cast to inputStream.");
inputStream.As(&randomAccessStream);
ComPtr<IBufferFactory> bufferFactory;
hr = RoGetActivationFactory(HString::MakeReference(RuntimeClass_Windows_Storage_Streams_Buffer).Get(),
IID_PPV_ARGS(&bufferFactory));
RETURN_HR_IF_FAILED("Could not create buffer factory.");
// use the same buffer size as default read chunk size
bufferFactory->Create(16384, m_buffer.GetAddressOf());
hr = m_buffer->QueryInterface(IID_PPV_ARGS(&bufferByteAccess));
RETURN_HR_IF_FAILED("Could not access buffer.");
populateBoundaries();
// release our reference to the speech stream operation
if (asyncInfo)
asyncInfo->Close();
synthOperation.Reset();
// we are buffered, but we don't want QIODevice to buffer as well
open(QIODevice::ReadOnly|QIODevice::Unbuffered);
emit streamReady(audioFormat);
fetchMore();
return S_OK;
}
/*
*/
void AudioSource::populateBoundaries()
{
ComPtr<ITimedMetadataTrackProvider> metaData;
if (!SUCCEEDED(inputStream.As(&metaData)))
return;
ComPtr<IVectorView<TimedMetadataTrack*>> metaDataTracks;
metaData->get_TimedMetadataTracks(&metaDataTracks);
quint32 trackCount = 0;
metaDataTracks->get_Size(&trackCount);
for (quint32 i = 0; i < trackCount; ++i) {
ComPtr<ITimedMetadataTrack> metaDataTrack;
HRESULT hr = metaDataTracks->GetAt(i, &metaDataTrack);
Q_ASSERT_SUCCEEDED(hr);
const auto boundaryType = [metaDataTrack]{
ComPtr<IMediaTrack> mediaTrack;
HRESULT hr = metaDataTrack.As(&mediaTrack);
Q_ASSERT_SUCCEEDED(hr);
if (HString hstr; SUCCEEDED(mediaTrack->get_Id(hstr.GetAddressOf()))) {
const QString trackName = QString::fromWCharArray(hstr.GetRawBuffer(0));
if (trackName == QStringLiteral("SpeechWord"))
return Boundary::Word;
if (trackName == QStringLiteral("SpeechSentence"))
return Boundary::Sentence;
}
return Boundary::Unknown;
}();
if (boundaryType == Boundary::Unknown)
continue;
ComPtr<IVectorView<IMediaCue*>> cues;
if (!SUCCEEDED(metaDataTrack->get_Cues(&cues)))
continue;
quint32 cueCount = 0;
cues->get_Size(&cueCount);
boundaries.reserve(boundaries.size() + cueCount);
for (quint32 j = 0; j < cueCount; ++j) {
ComPtr<IMediaCue> cue;
hr = cues->GetAt(j, &cue);
Q_ASSERT_SUCCEEDED(hr);
ComPtr<ISpeechCue> speechCue;
if (!SUCCEEDED(cue.As(&speechCue))) {
qWarning("Invalid cue");
break;
}
QString text;
if (HString hstr; SUCCEEDED(speechCue->get_Text(hstr.GetAddressOf())))
text = QString::fromWCharArray(hstr.GetRawBuffer(0));
int startIndex = -1;
if (IReference<int> *refInt; SUCCEEDED(speechCue->get_StartPositionInInput(&refInt)))
refInt->get_Value(&startIndex);
int endIndex = -1;
if (IReference<int> *refInt; SUCCEEDED(speechCue->get_EndPositionInInput(&refInt)))
refInt->get_Value(&endIndex);
// A time period expressed in 100-nanosecond units. the Duration property is always 0 for speech.
TimeSpan startTime = {};
cue->get_StartTime(&startTime);
const qint64 usec = startTime.Duration / 10; // QAudioSink APIs operate on microseconds
boundaries.append(Boundary{boundaryType, text, startIndex, endIndex, usec});
}
}
std::sort(boundaries.begin(), boundaries.end());
}
/*
Completion handler for reading from the stream.
Resets the COM buffer so that it points at the correct position in the
stream, and emits readyRead so that the sink pulls more data.
*/
HRESULT AudioSource::Invoke(IAsyncOperationWithProgress<IBuffer*, unsigned int> *read,
AsyncStatus status)
{
if (status != AsyncStatus::Completed)
return E_FAIL;
// there should never be multiple read operations
Q_ASSERT(readOperation.Get() == read);
HRESULT hr = read->GetResults(&m_buffer);
RETURN_HR_IF_FAILED("Could not access buffer.");
m_bufferSpan = [&] {
byte *ptr = nullptr;
bufferByteAccess->Buffer(&ptr);
UINT32 length = 0;
m_buffer->get_Length(&length);
return QSpan(reinterpret_cast<const char *>(ptr), qsizetype(length));
}();
// Skip the RIFF header in the first buffer to prevent an audible click.
constexpr qsizetype WaveHeaderLength = 44;
if (!m_headerConsumed) {
Q_ASSERT(m_bytesRead == 0);
Q_ASSERT(m_bufferSpan.size() >= WaveHeaderLength);
Q_ASSERT(QByteArrayView(m_bufferSpan).startsWith("RIFF"));
m_bufferSpan = m_bufferSpan.subspan(WaveHeaderLength);
m_headerConsumed = true;
}
ComPtr<IAsyncInfo> asyncInfo;
if (HRESULT hr = readOperation.As(&asyncInfo); SUCCEEDED(hr))
asyncInfo->Close();
readOperation.Reset();
// inform the sink that more data has arrived
if (m_pause == NoPause && !m_bufferSpan.isEmpty())
emit readyRead();
return S_OK;
}
/*
Starts an asynchronous read operation. There can only be one such
operation pending at any given time, so fetchMore must only be called
if the buffer provided by a previous operation is exhaused.
*/
bool AudioSource::fetchMore()
{
Q_ASSERT(m_buffer);
if (readOperation) {
qWarning () << "Fetching more while a read operation is already pending";
return false;
}
UINT32 capacity;
m_buffer->get_Capacity(&capacity);
InputStreamOptions streamOptions = {};
HRESULT hr = inputStream->ReadAsync(m_buffer.Get(), capacity, streamOptions, readOperation.GetAddressOf());
if (!SUCCEEDED(hr))
return false;
readOperation->put_Completed(this);
return true;
}
QT_END_NAMESPACE