Text to Speech (TTS) integration via Read Out! (TTS) on BlackBerry 10

Sometimes you like to listen to stuff instead of reading it yourself. In some situations this can became very handy. It’s the same for users of your app(s) as well. For that reason it could be a good fit to integrate a working text to speech (TTS) solution to your apps.

Read Out! (TTS) is probably one of the most popular and complete TTS apps available for BlackBerry 10. I’ve developed the app by myself back in July 2015 and since then provided some updates.

As I started the development of Read Out! (TTS), it wasn’t even been meant to be a separate app. At the beginning it was part of a ReadItNow! beta. In my previous blog post I already mentioned what ReadItNow! is, a native BlackBerry 10 client for the popular “read later” service Pocket. Because some users wanted to listen to articles, stored in ReadItNow!, rather than reading them, I’ve added a 3rd party app more than a year ago. Sadly that app had some limitations with large articles and the developer didn’t want to fix that issues. So I had a look at all available text to speech apps in the BlackBerry World. I couldn’t find anyone, that fitted with my needs. For that reason I decided to try it on my own… After some developing I was really happy with the result and I decided it was far better than everything else I’ve seen in the BlackBerry Word. So I extracted the working code from the ReadItNow! beta and put together a single app with only one purpose: bring TTS to a lot of apps! I simply don’t wanted to limit the usage to ReadItNow! as I thought other apps could benefit from this solution as well.

As a developer, you can invoke Read Out! (TTS) in three different ways:

 

  1. Send URL and let Read Out! (TTS) extract it’s content
  2. Send plain text
  3. Send plain text via JSON file

To make the integration easier, I’ve putten together a few sniplets to show you how to integrate it with C++ and QML. It’s really easy, you will see.

Let’s start with the header file of the class, where you want to add the invocation. You should add the following lines to your header file:

public:
   Q_INVOKABLE void invokeReadOutWithText(const QString &text);
   Q_INVOKABLE void invokeReadOutWithJSON(const QString &text, const int &position);
   Q_INVOKABLE void invokeReadOutWithURL(const QString &url);

signals:
   void readoutNotFound();

private slots:
   void onCheck3rdPartyAppsResponse();
   void childCardDone(const bb::system::CardDoneMessage &doneMessage);

private:
   void check3rdPartyApps();

   bool m_readoutAvailable;

Let’s start near the bottom with line 14. We define a new function called check3rdPartyApps(). This function allows us to detect if the user has Read Out! (TTS) already installed on his device. As this check will be asynchronously, we need a slot for handling the response as well. It’s defined in line 10 (onCheck3rdPartyAppsResponse()).

Now let’s see what both functions look like:

void ApplicationUI::check3rdPartyApps()
{
   qDebug() << "check3rdPartyApps()";
   m_readoutAvailable = false;
   InvokeQueryTargetsRequest request;
   request.setMimeType("text/plain");
   const InvokeReply *reply = m_invokeManager->queryTargets(request);
   // Listen for the results
   bool ok = QObject::connect(reply, SIGNAL(finished()), this, SLOT(onCheck3rdPartyAppsResponse()));
   Q_ASSERT(ok);
   Q_UNUSED(ok);
}

void ApplicationUI::onCheck3rdPartyAppsResponse()
{
   qDebug() << "onCheck3rdPartyAppsResponse()";

   // Get the reply from the sender object
   InvokeQueryTargetsReply *reply = qobject_cast<InvokeQueryTargetsReply*>(sender());
   if (reply) {
       if (reply->error() == InvokeReplyError::None) {
           QList<InvokeAction> targetsByAction = reply->actions();
           foreach (const InvokeAction &action, reply->actions()) {
               foreach (const InvokeTarget &target, action.targets()) {
                   //qDebug() << target.name();
                   if (target.name().indexOf("com.svzi.readout") != -1) {
                       m_readoutAvailable = true;
                   }
               }
           }
       } else if (reply->error() != InvokeReplyError::None) {
           qDebug() << "ERROR:" << reply->error();
       }

     // Clean up the results
     reply->deleteLater();
   }
   qDebug() << "m_readoutAvailable:" << m_readoutAvailable;
}

We’re using a InvokeQueryTargetsRequest in line 5 to search for apps that handle “text/plain” mime type (line 6). We query the targets in line 7 and connect the finished() signal of that request with our onCheck3rdPartyAppsResponse() slot (defined in line 10 of the header file).

In the slot we iterate over all actions (line 23) and the corresponding targets (line 24). If the target name is “com.svzi.readout”, we found the app installed on that device. We need to remember that for later, so we set our class member variable m_readoutAvailable to true (line 27).

I usually call the check3rdPartyApps() in the constructor of my class, as well as every time the app got foregrounded. This way the user can start the app without having Read Out! (TTS) installed, minimize your app, download Read Out! (TTS) from the BlackBerry World and can use it right away, because your apps checks for it when it get’s maximized.

Now let’s share some content to Read Out! (TTS)

As mentioned earlier there are three different ways to share content to Read Out! (TTS), let’s repeat them quickly:

  1. Send URL and let Read Out! (TTS) extract it’s content
  2. Send plain text
  3. Send plain text via JSON file

For that reason we have defined three different methods in our header file (lines 2 – 4). For those we will start looking at the implementation details.

1) Send URL and let Read Out! (TTS) extract it’s content

Sending an URL to Read Out! (TTS) is quite easy:

void ApplicationUI::invokeReadOutWithURL(const QString &url)
{
    if (m_readoutAvailable) {
    	bb:system:InvokeManager *invokeManager = new bb:system:InvokeManager(this);
        bb::system::InvokeRequest request;

        request.setTarget("com.svzi.readout");
        request.setAction("bb.action.SHARE");
        request.setUri(url);

        invokeManager->invoke(request);
    } else {
    	emit readoutNotFound();
    }

}

Before you start the invocation you should check if Read Out! (TTS) was found on the device (line 3). If the app isn’t installed you could emit a signal, as show in our example in line 13, to show a dialog or popup to the user and provide a link to the BlackBerry World to let him easily grab his copy of Read Out! (TTS) in order to use it with your app. This should be done for all available methods of invoking Read Out! (TTS).

Back to the function. As you can see the url param will be set as request URI. That’s all you need to listen to the content of an website. Read Out! (TTS) will analyze the URL and download the content. When the download is finished, it will start reading out the content aloud.

2) Send plain text

If you want to share some text to Read Out! (TTS), which is obviously the most common feature, you can do it this way:

void ApplicationUI::invokeReadOutWithText(const QString &text)
{
   if (m_readoutAvailable) {// strip HTML code from content!
       QTextDocument cont;
       cont.setHtml(text);

       bb:system:InvokeManager *invokeManager = new         bb:system:InvokeManager(this);
       bb::system::InvokeRequest request;

       request.setTarget("com.svzi.readout");
       request.setMimeType("text/plain");
       request.setAction("bb.action.SHARE");
       request.setData(cont.toPlainText());

       invokeManager->invoke(request);
   } else {
       emit readoutNotFound();
   }
}

Lines 10 – 12 are defining the invocation request. You should use this way of integration, if you don’t plan to send large texts to Read Out! (TTS), because there are some limitations, when it comes to the size of parameters in an invocation request. But that has nothing to do with Read Out! (TTS), as this limitation is valid for all BlackBerry 10 invocations. Sadly you won’t get any error message from the invoke, if your text message is too long. Only BlackBerry itself knows why they didn’t send an error when that happens… Instead your invoke request will do exactly nothing. 🙁

So if you want to share large portions of text (or if you want to make sure that you’re invocation request will always work, without worrying about the text size), you should have a look at the next way to share text.

3) Send plain text via JSON file

This is by far the most recommended way to share text data to Read Out! (TTS). As I pointed out in the previous paragraphs, there are some size restrictions regarding invocation parameter. To be sure that your invocation request will work the way you expect it, you should use this method.

Doing it this way, you also have some other advantages as well. You could define a starting point in milliseconds or percent, where the read out should start it. This is especially useful when you often share large texts to Read Out! (TTS), because Read Out! (TTS) will always let you know where it has stopped the playback. It always write the progress state to a separate file called readout_progress.json! More details on that later, first we should have a look on how to invoke it with JSON data:

void ApplicationUI::invokeReadOutWithJSON(const QString &content,
        const int &position)
{
    if (m_readoutAvailable) {
        // prepare invocation request
        bb::system::InvokeManager *invokeManager = new bb::system::InvokeManager(this);
        bb::system::InvokeRequest request;
        request.setTarget("com.svzi.readout");
        request.setAction("bb.action.OPEN_FILE");
        request.setMimeType("text/file");

        // strip HTML code from content!
        QTextDocument cont;
        cont.setHtml(content);

        // create json file
        QVariantMap map;
        map.insert("text", cont.toPlainText());
        map.insert("positionInMilliseconds", position * 1000); // optional, to set a starting position where the read out should start
        map.insert("positionInPercent", 0); // optional, to set a starting position where the read out should start
        map.insert("title", "Test"); // optional, used for export file name of mp3
        map.insert("url", ""); // optinal, not used right now
        map.insert("image", ""); // optional, not used right now

        // create data file
        QDir a;
        a.setPath("/accounts/1000/clipboard");
        QFile file(a.path() + "/readout.json");
        if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
            JsonDataAccess jda(&file);
            jda.save(map, &file);
            if (jda.hasError()) {
                const DataAccessError err = jda.error();
                const QString errorMsg = tr("Error writing data to JSON file: %1").arg(err.errorMessage());
                qDebug() << errorMsg;
                file.close();
                return;
            }
            file.close();
        } else {
            qDebug() << "Could not open file for create!";
            return;
        }

        bool result = QObject::connect(invokeManager, SIGNAL(childCardDone(const bb::system::CardDoneMessage&)), this, SLOT(childCardDone(const bb::system::CardDoneMessage&)));
        Q_ASSERT(result);

        invokeManager->invoke(request);
    } else {
    	emit readoutNotFound();
    }

}

At line 9 we can notice the first variance to our previous invocation requests. We set the the action to bb.action.OPEN_FILE and that is important because with this modification Read Out! (TTS) will look for a JSON file when it receives the invocation. In lines 17 – 23 you can see how an example JSON could look like and which params are possible.

Starting with line 26 it’s finally getting a little more fancy, as we now create a file (readout.json) inside the systems clipboard directory (/accounts/1000/clipboard/). We are using this directory to exchange data for one important reason: your app won’t need any additional permissions to access that directory! The rest of the source code should be obvious, as we only convert our QVariantMap to JSON in line 31 and start our invocation request in line 45. One more little change in that line 45: we finally connect a new method to the childCardDone() signal of the invokeManager (compare with the header file in line 11).

We’re connecting our slot to that signal, because we want to know at which position the playback has stopped. For that reason Read Out! (TTS) writes it’s own progress file, as already mentioned somewhere above. The file is called readout_progress.json and can be found in the same directory (/accounts/1000/clipboard/) as the invocation request JSON file. Here is an example of how it looks like:

{
   "durationInMilliseconds" : 42912,
   "positionInMilliseconds" : 15046,
   "positionInPercent" : 35.06245339299031
}

As soon as the following childCardDone() is triggered, you know that Read Out! (TTS) has been closed, for what ever reason, and the playback has stopped. With the available data you can check yourself if the user has listened to the whole text or not.

void ApplicationUI::childCardDone(const bb::system::CardDoneMessage &doneMessage) 
{
	// open the progress file
    QDir a;
    a.setPath("/accounts/1000/clipboard");
    QFile jsonFile(a.path() + "/readout_progress.json");

    if (!jsonFile.open(QFile::ReadOnly)) {
        qDebug() << "Could not find file for Read Out! (TTS)";
        return;
    } else {
        const QString doc = QString::fromUtf8(jsonFile.readAll());
        jsonFile.close();

        JsonDataAccess jda;
        QVariantMap progressMap = jda.loadFromBuffer(doc).toMap();
        if (jda.hasError()) {
            const DataAccessError err = jda.error();
            qDebug() << "Error converting data file for Read Out! (TTS):" << err.errorMessage());
            return;
        } else { 
            // do your stuff here
            bool ok;
            double durationInMilliseconds = progressMap.value("durationInMilliseconds").toDouble(&ok);
            qDebug() << "durationInMilliseconds:" << durationInMilliseconds;
            double positionInPercent = progressMap.value("positionInPercent").toDouble(&ok);
            qDebug() << "positionInPercent:" << positionInPercent;
            double positionInMilliseconds = progressMap.value("positionInMilliseconds").toDouble(&ok); 
            qDebug() << "positionInMilliseconds:" << positionInMilliseconds;

            if ((int) positionInPercent == 100) {
                // user has listened to everything
                qDebug() << "user heard 100%";
            } else {
                // user has stopped listened before it was over
                qDebug() << "user get bored or had another reason to stop listening";
            }
      	}
   }
}

 

What’s next?

Now it’s all said and done. You can can call either invokeReadOutWithURL(…), invokeReadOutWithText() or invokeReadItNowWithJSON(…). Whatever you want to listen to. You can call it from C++:

invokeReadOutWithText("This string is not as long as it could be, because I'm not that good in writing long texts, sorry. :-)");

And from QML as well:

_app.invokeReadOutWithURL("http://blog.sven-ziegler.com/2015/09/20/pocket-integration-via-readitnow-on-blackberry-10/");

You can find a working example app right here at my public Github profile: https://github.com/svzi/ShareToReadOut

Please let me know via my BBM Channel or Twitter if you have any issues with the integration, I’m happy to help.

Have you added Read Out! (TTS) to your app? Share your app below and I will review it for inclusion in my list of 3rd Party Apps.

That’s it. 🙂 Happy integrating!

About Sven Ziegler
Freelancing Mobile Developer, BlackBerry 10 Enthusiast.

About Sven Ziegler

Freelancing Mobile Developer, BlackBerry 10 Enthusiast.