From ae4296776f30d0fb76e2f04a9c8cd5bc4ba3a63f Mon Sep 17 00:00:00 2001 From: Jakub Melka Date: Sat, 28 Aug 2021 16:11:44 +0200 Subject: [PATCH] AudioBook Plugin: create audio book stream --- Pdf4QtLib/sources/pdfdocumenttextflow.cpp | 21 +++ Pdf4QtLib/sources/pdfdocumenttextflow.h | 5 + Pdf4QtLib/sources/pdfplugin.h | 10 + Pdf4QtViewer/Pdf4QtViewer.pro | 3 +- Pdf4QtViewer/pdfprogramcontroller.cpp | 14 ++ Pdf4QtViewer/pdfprogramcontroller.h | 1 + .../AudioBookPlugin/AudioBookPlugin.pro | 2 + .../AudioBookPlugin/audiobookcreator.cpp | 176 ++++++++++++++++++ .../AudioBookPlugin/audiobookcreator.h | 57 ++++++ .../AudioBookPlugin/audiobookplugin.cpp | 32 +++- PdfTool/pdftoolaudiobook.cpp | 1 + 11 files changed, 320 insertions(+), 2 deletions(-) create mode 100644 Pdf4QtViewerPlugins/AudioBookPlugin/audiobookcreator.cpp create mode 100644 Pdf4QtViewerPlugins/AudioBookPlugin/audiobookcreator.h diff --git a/Pdf4QtLib/sources/pdfdocumenttextflow.cpp b/Pdf4QtLib/sources/pdfdocumenttextflow.cpp index 41f0025..01e2ce5 100644 --- a/Pdf4QtLib/sources/pdfdocumenttextflow.cpp +++ b/Pdf4QtLib/sources/pdfdocumenttextflow.cpp @@ -974,6 +974,27 @@ PDFDocumentTextFlowEditor::PageIndicesMappingRange PDFDocumentTextFlowEditor::ge return std::equal_range(m_pageIndicesMapping.cbegin(), m_pageIndicesMapping.cend(), std::make_pair(pageIndex, size_t(0)), comparator); } +PDFDocumentTextFlow PDFDocumentTextFlowEditor::createEditedTextFlow() const +{ + PDFDocumentTextFlow::Items items; + items.reserve(getItemCount()); + + const size_t size = getItemCount(); + for (size_t i = 0; i < size; ++i) + { + if (isRemoved(i)) + { + continue; + } + + PDFDocumentTextFlow::Item item = *getOriginalItem(i); + item.text = getText(i); + items.emplace_back(std::move(item)); + } + + return PDFDocumentTextFlow(std::move(items)); +} + void PDFDocumentTextFlowEditor::createPageMapping() { m_pageIndicesMapping.clear(); diff --git a/Pdf4QtLib/sources/pdfdocumenttextflow.h b/Pdf4QtLib/sources/pdfdocumenttextflow.h index b139a89..98be760 100644 --- a/Pdf4QtLib/sources/pdfdocumenttextflow.h +++ b/Pdf4QtLib/sources/pdfdocumenttextflow.h @@ -261,6 +261,11 @@ public: const EditedItem* getEditedItem(size_t index) const { return &m_editedTextFlow.at(index); } + /// Creates text flow from active edited items. If item is removed, + /// then it is not added into this text flow. User text modification + /// is applied to a text flow. + PDFDocumentTextFlow createEditedTextFlow() const; + private: void createPageMapping(); void createEditedFromOriginalTextFlow(); diff --git a/Pdf4QtLib/sources/pdfplugin.h b/Pdf4QtLib/sources/pdfplugin.h index a9f9cdc..2c1a8fb 100644 --- a/Pdf4QtLib/sources/pdfplugin.h +++ b/Pdf4QtLib/sources/pdfplugin.h @@ -54,9 +54,19 @@ public: explicit IPluginDataExchange() = default; virtual ~IPluginDataExchange() = default; + struct VoiceSettings + { + QString directory; + QString voiceName; + double volume = 1.0; + double rate = 0.0; + double pitch = 0.0; + }; + virtual QString getOriginalFileName() const = 0; virtual pdf::PDFTextSelection getSelectedText() const = 0; virtual QMainWindow* getMainWindow() const = 0; + virtual VoiceSettings getVoiceSettings() const = 0; }; class PDF4QTLIBSHARED_EXPORT PDFPlugin : public QObject diff --git a/Pdf4QtViewer/Pdf4QtViewer.pro b/Pdf4QtViewer/Pdf4QtViewer.pro index 23074f9..b10a346 100644 --- a/Pdf4QtViewer/Pdf4QtViewer.pro +++ b/Pdf4QtViewer/Pdf4QtViewer.pro @@ -99,7 +99,8 @@ plugins.files = $$DESTDIR/pdfplugins/ObjectInspectorPlugin.dll \ $$DESTDIR/pdfplugins/OutputPreviewPlugin.dll \ $$DESTDIR/pdfplugins/DimensionsPlugin.dll \ $$DESTDIR/pdfplugins/SoftProofingPlugin.dll \ - $$DESTDIR/pdfplugins/RedactPlugin.dll + $$DESTDIR/pdfplugins/RedactPlugin.dll \ + $$DESTDIR/pdfplugins/AudioBookPlugin.dll plugins.path = $$DESTDIR/install/pdfplugins plugins.CONFIG += no_check_exist diff --git a/Pdf4QtViewer/pdfprogramcontroller.cpp b/Pdf4QtViewer/pdfprogramcontroller.cpp index f4702ba..e4f6c2f 100644 --- a/Pdf4QtViewer/pdfprogramcontroller.cpp +++ b/Pdf4QtViewer/pdfprogramcontroller.cpp @@ -1078,6 +1078,20 @@ QMainWindow* PDFProgramController::getMainWindow() const return m_mainWindow; } +pdf::IPluginDataExchange::VoiceSettings PDFProgramController::getVoiceSettings() const +{ + VoiceSettings voiceSettings; + + const PDFViewerSettings::Settings& settings = m_settings->getSettings(); + voiceSettings.directory = m_settings->getDirectory(); + voiceSettings.voiceName = settings.m_speechVoice; + voiceSettings.pitch = settings.m_speechPitch; + voiceSettings.rate = settings.m_speechRate; + voiceSettings.volume = settings.m_speechVolume; + + return voiceSettings; +} + void PDFProgramController::onActionRotateRightTriggered() { m_pdfWidget->getDrawWidgetProxy()->performOperation(pdf::PDFDrawWidgetProxy::RotateRight); diff --git a/Pdf4QtViewer/pdfprogramcontroller.h b/Pdf4QtViewer/pdfprogramcontroller.h index eeebb17..bbd8e27 100644 --- a/Pdf4QtViewer/pdfprogramcontroller.h +++ b/Pdf4QtViewer/pdfprogramcontroller.h @@ -286,6 +286,7 @@ public: virtual QString getOriginalFileName() const override; virtual pdf::PDFTextSelection getSelectedText() const override; virtual QMainWindow* getMainWindow() const override; + virtual VoiceSettings getVoiceSettings() const override; signals: void queryPasswordRequest(QString* password, bool* ok); diff --git a/Pdf4QtViewerPlugins/AudioBookPlugin/AudioBookPlugin.pro b/Pdf4QtViewerPlugins/AudioBookPlugin/AudioBookPlugin.pro index 37cf0a6..536347f 100644 --- a/Pdf4QtViewerPlugins/AudioBookPlugin/AudioBookPlugin.pro +++ b/Pdf4QtViewerPlugins/AudioBookPlugin/AudioBookPlugin.pro @@ -33,10 +33,12 @@ DESTDIR = $$OUT_PWD/../../pdfplugins CONFIG += c++11 SOURCES += \ + audiobookcreator.cpp \ audiobookplugin.cpp \ audiotextstreameditordockwidget.cpp HEADERS += \ + audiobookcreator.h \ audiobookplugin.h \ audiotextstreameditordockwidget.h diff --git a/Pdf4QtViewerPlugins/AudioBookPlugin/audiobookcreator.cpp b/Pdf4QtViewerPlugins/AudioBookPlugin/audiobookcreator.cpp new file mode 100644 index 0000000..74e3fb8 --- /dev/null +++ b/Pdf4QtViewerPlugins/AudioBookPlugin/audiobookcreator.cpp @@ -0,0 +1,176 @@ +// Copyright (C) 2021 Jakub Melka +// +// This file is part of PDF4QT. +// +// PDF4QT is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// with the written consent of the copyright owner, any later version. +// +// PDF4QT is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with PDF4QT. If not, see . + +#include "audiobookcreator.h" + +#ifdef Q_OS_WIN +#include +#pragma comment(lib, "ole32") +#endif + +namespace pdfplugin +{ + +AudioBookCreator::AudioBookCreator() : + m_initialized(false) +{ +#ifdef Q_OS_WIN + auto comResult = ::CoInitialize(nullptr); + m_initialized = SUCCEEDED(comResult); +#endif +} + +AudioBookCreator::~AudioBookCreator() +{ +#ifdef Q_OS_WIN + if (m_initialized) + { + ::CoUninitialize(); + } +#endif +} + +pdf::PDFOperationResult AudioBookCreator::createAudioBook(const Settings& settings, pdf::PDFDocumentTextFlow& flow) +{ +#ifdef Q_OS_WIN + QString audioString; + QTextStream textStream(&audioString); + + for (const pdf::PDFDocumentTextFlow::Item& item : flow.getItems()) + { + QString trimmedText = item.text.trimmed(); + if (!trimmedText.isEmpty()) + { + textStream << trimmedText << endl; + } + } + + auto getVoiceToken = [](const Settings& settings) + { + ISpObjectToken* token = nullptr; + + QStringList voiceSelector; + if (!settings.voiceName.isEmpty()) + { + voiceSelector << QString("Name=%1").arg(settings.voiceName); + } + if (!settings.voiceGender.isEmpty()) + { + voiceSelector << QString("Gender=%1").arg(settings.voiceGender); + } + if (!settings.voiceAge.isEmpty()) + { + voiceSelector << QString("Age=%1").arg(settings.voiceAge); + } + if (!settings.voiceLangCode.isEmpty()) + { + voiceSelector << QString("Language=%1").arg(settings.voiceLangCode); + } + QString voiceSelectorString = voiceSelector.join(";"); + LPCWSTR requiredAttributes = !voiceSelectorString.isEmpty() ? (LPCWSTR)voiceSelectorString.utf16() : nullptr; + + ISpObjectTokenCategory* category = nullptr; + if (!SUCCEEDED(::CoCreateInstance(CLSID_SpObjectTokenCategory, NULL, CLSCTX_ALL, __uuidof(ISpObjectTokenCategory), (LPVOID*)&category))) + { + return token; + } + + if (!SUCCEEDED(category->SetId(SPCAT_VOICES, FALSE))) + { + category->Release(); + return token; + } + + IEnumSpObjectTokens* enumTokensObject = nullptr; + if (SUCCEEDED(category->EnumTokens(requiredAttributes, NULL, &enumTokensObject))) + { + enumTokensObject->Next(1, &token, NULL); + } + + if (enumTokensObject) + { + enumTokensObject->Release(); + } + + if (category) + { + category->Release(); + } + + return token; + }; + + ISpObjectToken* voiceToken = getVoiceToken(settings); + + // Do we have any voice? + if (!voiceToken) + { + return tr("No suitable voice found."); + } + + QString outputFile = settings.audioFileName; + BSTR outputFileName = (BSTR)outputFile.utf16(); + + ISpeechFileStream* stream = nullptr; + if (!SUCCEEDED(::CoCreateInstance(CLSID_SpFileStream, NULL, CLSCTX_ALL, __uuidof(ISpeechFileStream), (LPVOID*)&stream))) + { + voiceToken->Release(); + return tr("Cannot create output stream '%1'.").arg(outputFile); + } + + ISpVoice* voice = nullptr; + if (!SUCCEEDED(::CoCreateInstance(CLSID_SpVoice, NULL, CLSCTX_ALL, __uuidof(ISpVoice), (LPVOID*)&voice))) + { + voiceToken->Release(); + stream->Release(); + return tr("Cannot create voice."); + } + + if (!SUCCEEDED(stream->Open(outputFileName, SSFMCreateForWrite))) + { + voiceToken->Release(); + voice->Release(); + stream->Release(); + return tr("Cannot create output stream '%1'.").arg(outputFile); + } + + if (!SUCCEEDED(voice->SetVoice(voiceToken))) + { + voiceToken->Release(); + voice->Release(); + stream->Release(); + return tr("Failed to set requested voice."); + } + + LPCWSTR stringToSpeak = (LPCWSTR)audioString.utf16(); + + voice->SetOutput(stream, FALSE); + voice->SetRate(settings.rate * 10.0); + voice->SetVolume(settings.volume * 100.0); + voice->Speak(stringToSpeak, SPF_PURGEBEFORESPEAK | SPF_PARSE_SAPI, NULL); + + voice->Release(); + stream->Release(); + voiceToken->Release(); + + return true; +#else + return tr("Audio book plugin is unsupported on your system."); +#endif +} + +} // namespace pdfplugin diff --git a/Pdf4QtViewerPlugins/AudioBookPlugin/audiobookcreator.h b/Pdf4QtViewerPlugins/AudioBookPlugin/audiobookcreator.h new file mode 100644 index 0000000..4d35de7 --- /dev/null +++ b/Pdf4QtViewerPlugins/AudioBookPlugin/audiobookcreator.h @@ -0,0 +1,57 @@ +// Copyright (C) 2021 Jakub Melka +// +// This file is part of PDF4QT. +// +// PDF4QT is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// with the written consent of the copyright owner, any later version. +// +// PDF4QT is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with PDF4QT. If not, see . + +#ifndef AUDIOBOOKCREATOR_H +#define AUDIOBOOKCREATOR_H + +#include "pdfdocumenttextflow.h" + +#include + +namespace pdfplugin +{ + +class AudioBookCreator +{ + Q_DECLARE_TR_FUNCTIONS(pdfplugin::AudioBookCreator) + +public: + AudioBookCreator(); + ~AudioBookCreator(); + + struct Settings + { + QString audioFileName; + QString voiceName; + QString voiceGender; + QString voiceAge; + QString voiceLangCode; + double rate = 0.0; + double volume = 1.0; + }; + + bool isInitialized() const { return m_initialized; } + + pdf::PDFOperationResult createAudioBook(const Settings& settings, pdf::PDFDocumentTextFlow& flow); + +private: + bool m_initialized; +}; + +} // namespace pdfplugin + +#endif // AUDIOBOOKCREATOR_H diff --git a/Pdf4QtViewerPlugins/AudioBookPlugin/audiobookplugin.cpp b/Pdf4QtViewerPlugins/AudioBookPlugin/audiobookplugin.cpp index 68bbf60..8866b1d 100644 --- a/Pdf4QtViewerPlugins/AudioBookPlugin/audiobookplugin.cpp +++ b/Pdf4QtViewerPlugins/AudioBookPlugin/audiobookplugin.cpp @@ -20,6 +20,7 @@ #include "pdfwidgettool.h" #include "pdfutils.h" #include "pdfwidgetutils.h" +#include "audiobookcreator.h" #include #include @@ -27,6 +28,7 @@ #include #include #include +#include #include namespace pdfplugin @@ -117,7 +119,7 @@ void AudioBookPlugin::setWidget(pdf::PDFWidget* widget) m_actionCreateAudioBook = new QAction(QIcon(":/pdfplugins/audiobook/create-audio-book.svg"), tr("Create Audio Book"), this); m_actionCreateAudioBook->setObjectName("actionAudioBook_CreateAudioBook"); - connect(m_actionRestoreOriginalText, &QAction::triggered, this, &AudioBookPlugin::onCreateAudioBook); + connect(m_actionCreateAudioBook, &QAction::triggered, this, &AudioBookPlugin::onCreateAudioBook); m_actionClear = new QAction(QIcon(":/pdfplugins/audiobook/clear.svg"), tr("Clear Text Stream"), this); m_actionClear->setObjectName("actionAudioBook_Clear"); @@ -425,7 +427,35 @@ void AudioBookPlugin::onMoveSelectionDown() void AudioBookPlugin::onCreateAudioBook() { + pdf::IPluginDataExchange::VoiceSettings voiceSettings = m_dataExchangeInterface->getVoiceSettings(); + QString fileName = QFileDialog::getSaveFileName(m_widget, tr("Select Audio File"), voiceSettings.directory, tr("Audio stream (*.mp3)")); + if (fileName.isEmpty()) + { + return; + } + + pdf::PDFOperationResult result = true; + AudioBookCreator audioBookCreator; + if (audioBookCreator.isInitialized()) + { + AudioBookCreator::Settings settings; + settings.audioFileName = fileName; + settings.voiceName = voiceSettings.voiceName; + settings.rate = voiceSettings.rate; + settings.volume = voiceSettings.volume; + pdf::PDFDocumentTextFlow textFlow = m_textFlowEditor.createEditedTextFlow(); + result = audioBookCreator.createAudioBook(settings, textFlow); + } + else + { + result = tr("Audio book creator cannot be initialized."); + } + + if (!result) + { + QMessageBox::critical(m_widget, tr("Error"), result.getErrorMessage()); + } } void AudioBookPlugin::onRectanglePicked(pdf::PDFInteger pageIndex, QRectF rectangle) diff --git a/PdfTool/pdftoolaudiobook.cpp b/PdfTool/pdftoolaudiobook.cpp index 9543839..2ee10a6 100644 --- a/PdfTool/pdftoolaudiobook.cpp +++ b/PdfTool/pdftoolaudiobook.cpp @@ -132,6 +132,7 @@ int PDFToolAudioBookBase::fillVoices(const PDFToolOptions& options, PDFVoiceInfo if (!SUCCEEDED(category->SetId(SPCAT_VOICES, FALSE))) { PDFConsole::writeError(PDFToolTranslationContext::tr("SAPI Error: Cannot enumerate SAPI voices."), options.outputCodec); + category->Release(); return ErrorSAPI; }