Синтез речи в Android-приложении

Text-to-Speech (TTS) можно использовать двумя способами. Во-первых, можно завязываться на конкретный движок, покупать библиотеку и работать через неё. Про этот вариант ничего не могу сказать, знаю только теоретически. Второй, общеизвестный вариант — использовать стандартное API. Голоса в этом случае являются просто приложениями, установленными в системе.

Вообще-то заставить приложение говорить не так сложно, и мануалов по этому поводу полно. Но для полноты картины приведу начальные сведения.

Начиная с версии 1.6 в SDK есть стандартный класс TextToSpeech.

Подключение в приложение

Простейшая схема такова:

MainActivity.java

public class StartActivity extends Activity {
    private static final String enginePackageName = "com.svox.pico";
    
    private static final String SAMPLE_TEXT 
= "Synthesizes speech from text for 
immediate playback or to create a sound file.";
    TextToSpeech tts;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        setContentView(R.layout.main);
        
        tts = new TextToSpeech(this, new OnInitListener() {
            @Override
            public void onInit(int status) {
                if (status == TextToSpeech.SUCCESS) {
                    tts.setEngineByPackageName(enginePackageName);
                    tts.setLanguage(Locale.UK);

                    speak();                
                }
            }
        });
    }

    private void speak() {
        tts.speak(SAMPLE_TEXT, TextToSpeech.QUEUE_FLUSH, null);
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        tts.shutdown();
    }
}

Все вроде понятно. Создали экземпляр TextToSpeech, инициализировали в специальном листенере (задавать голос мы можем только в onInit), и с тех пор можем синтезировать и проигрывать речь с помощью метода speak. Обращу внимание, что это только схема, более приближенное к реальности приложение можно найти в примере к статье.

Метод speak

Рассмотрим подробнее сигнатуру метода speak:

speak(String text, int queueMode, HashMap params)

text
Текст, который нужно прочитать
queueMode
  • TextToSpeech.QUEUE_FLUSH, если хочется, чтобы предыдущая фраза прерывалась и сразу начиналась следующая
  • TextToSpeech.QUEUE_ADD, если хочется, чтобы предыдущая фраза договорилась до конца только после этого началась следующая
params
Массив дополнительных параметров. Возможные параметры:
  • TextToSpeech.Engine.KEY_PARAM_STREAM — поток, в котором будет воспроизводиться звук.
  • TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID — идентификатор фразы. Пригодится, если хочется обрабатывать событие окончания говорения, и при этом не запутаться в произносимых фразах.

Другие полезные методы

playSilence(long durationInMs, int queueMode, HashMap params)
Проигрывает тишину в течение заданного времени. Параметры те же, что у speak.
stop()
Останавливает воспроизведение
synthesizeToFile(String text, HashMap params, String filename)
Записывает синтезированную речь в файл. Параметры те же, что у speak.
addSpeech(String text, String filename), addSpeech(String text, String packagename, int resourceId)
Задает маппинг между фразой и существующим файлом/ресурсом. Если такой маппинг задан, то вместо синтезированной речи метод speak будет воспроизводить данный файл.
setOnUtteranceCompletedListener(TextToSpeech.OnUtteranceCompletedListener listener)
Задает слушателя для события окончания фразы.
areDefaultsEnforced()
Установлена ли в настройках TTS галочка «Мои настройки». О ней будет подробнее.
setPitch(float pitch)
Задает тембр голоса. 1 — обычное значение, чем меньше значение, тем ниже голос.
setSpeechRate(float speechRate)
Задает скорость речи. 1 — обычное значение, чем меньше значение, тем медленнее говорим.

TTS engines

Вкратце расскажу об известных TTS-движках. Как уже говорилось ранее, голоса — это просто сторонние приложения. Посмотрим, что у нас есть под Android.

Pico
Стандартный TTS-движок, знает 5 языков, поставляется бесплатно. Говорит неплохо, но русского не знает.
eSpeak
Свободный TTS-движок. Знает очень много языков. По-русски тоже говорит, но отвратительно.
SVOX
Довольно известный движок. Под Android распространяется следующим образом. Есть бесплатная программа-оболочка и платные голоса, которыми можно управлять из этой оболочки. Голосов очень много. Достаточно неплохо говорит по-русски, хотя есть проблемы с ударениями. В общем-то голос SVOX оказался единственным вариантом для русской озвучки приложения.
Loquendo
Также известный и качественный движок. К сожалению, в Android представлен мало. Для английского языка есть голос Susan, а вот для русского языка приложения нет, хотя вообще-то Loquendo говорить по-русски умеет.

А теперь немного о сложностях.

Проверка наличия голосовых данных

Pico TTS поставляется по умолчанию с системой. Но на некоторых моделях телефонов не установлены голосовые пакеты. Внешне это проявляется, например, в том, что в системных настройках синтеза речи всё задизаблено и предлагается скачать и установить некие ресурсы:

Отсутствуют голосовые данные

В официальном мануале описан способ обработки этой ситуации.

CheckVoiceActivity.java

public class CheckVoiceActivity extends Activity {
    TextToSpeech tts;

    private static final int REQUEST_CODE = 150;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        setContentView(R.layout.main);        

    }

    public void onPrepareSpeech(View view) {
        Intent checkIntent = new Intent();
        checkIntent.setAction(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
        checkIntent.setPackage(Consts.ENGINE);
        
        startActivityForResult(checkIntent, REQUEST_CODE);
    }

    protected void onActivityResult(int requestCode, int resultCode, Intent data)
    {
        if (requestCode == REQUEST_CODE)
        {
            if (resultCode == TextToSpeech.Engine.CHECK_VOICE_DATA_PASS)
            {
                // Голосовые данные установлены, можно создавать экземпляр TextToSpeech
                ...
            }
            else
            {
                // голосовые данные отсутствуют, предлагаем установить
                Intent installIntent = new Intent();
                installIntent.setAction(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA);
                installIntent.setPackage(Consts.ENGINE);
                startActivity(installIntent);
            }
        }
    }
    ...
}

Особенности работы под Android 2.1

Наше приложение должно было разговаривать не абы каким голосом, а исключительно красивым. Соответственно, была задача выбрать нужный нам TTS-движок из всех установленных у пользователя. В Android 2.2 у класса TextToSpeech есть метод setEngineByPackageName, но что делать в 2.1, где такого метода нет?

Существует известный обход этой проблемы, с использованием дополнительной программы и дополнительной библиотеки. В плане юзабилити, конечно, не ахти, ведь придется заставлять пользователя ставить какой-то сторонний софт. Зато работает. Итак:

  • Устанавливаем на телефон приложение Text-to-speech Extended (ссылка на маркет: market://details?id=com.google.tts)
  • Подключаем к нашему приложению библиотеку от eyes-free.
  • Вместо привычного TextToSpeech используем класс TextToSpeechBeta из этой библиотеки

Имеет смысл написать класс-оболочку такого примерно вида:

TextToSpeechWrapper

package com.demos.tts;

import java.util.HashMap;
import java.util.Locale;

import com.google.tts.TextToSpeechBeta;

import android.content.Context;
import android.os.Build;
import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.OnInitListener;
import android.speech.tts.TextToSpeech.OnUtteranceCompletedListener;

/**
 * Оболочка над TTS/TTSE
 * 
 * @author darja.ryazhskikh
 * 
 */
public class TextToSpeechWrapper {
    private TextToSpeech tts;
    private TextToSpeechBeta ttse;

    private Context context;

    public TextToSpeechWrapper(Context context) {
    this.context = context;
    }

    /**
     * Создаем стандартный TextToSpeech в случае версии Android от 2.2, и объект
     * TextToSpeechBeta для 2.1
     * 
     * @param context
     * @param listener
     */
    public void init(final OnInitListener listener) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {
        tts = new TextToSpeech(context, listener);
        ttse = null;
    } else {
        if (TextToSpeechBeta.isInstalled(context)) {
     ttse = new TextToSpeechBeta(context,
      new TextToSpeechBeta.OnInitListener() {
          @Override
          public void onInit(int status, int version) {
       listener.onInit(status);
          }
      });
        } else {
     ttse = null;
        }
        tts = null;
    }
    }

    /**
     * Проверяет, установлена ли в настройках TTS галочка
     * "Always use my settings"
     * 
     * @return
     */
    public Boolean areDefaultsEnforced() {
    if (tts != null)
        return tts.areDefaultsEnforced();
    else if (ttse != null)
        return ttse.areDefaultsEnforcedExtended();
    else
        return null;
    }

    public boolean setEngineByPackageName(String engine) {
    boolean success = false;
    try {
        if (tts != null)
     success = tts.setEngineByPackageName(engine) == TextToSpeech.SUCCESS;
        else if (ttse != null)
     success = ttse.setEngineByPackageNameExtended(engine) == TextToSpeechBeta.SUCCESS;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return success;
    }

    public void speak(String text, HashMap<String, String> params) {
    if (tts != null)
        tts.speak(text, TextToSpeech.QUEUE_FLUSH, params);
    else if (ttse != null)
        ttse.speak(text, TextToSpeech.QUEUE_FLUSH, params);
    }

    public void stop() {
    if (tts != null)
        tts.stop();
    else if (ttse != null)
        ttse.stop();
    }

    public boolean isLanguageAvailable(Locale loc) {
    if (tts != null) {
        int result = tts.isLanguageAvailable(loc);
        return result >= TextToSpeech.LANG_AVAILABLE;
    } else if (ttse != null)
        return ttse.isLanguageAvailable(loc) >= TextToSpeechBeta.LANG_AVAILABLE;

    return false;
    }

    public boolean setLanguage(Locale loc) {
    if (tts != null)
        return tts.setLanguage(loc) >= TextToSpeech.LANG_AVAILABLE;
    else if (ttse != null)
        return ttse.setLanguage(loc) >= TextToSpeechBeta.LANG_AVAILABLE;
    return false;
    }

    /**
     * Задает слушателя на окончание фразы
     * 
     * @param listener
     */
    public void setOnUtteranceCompletedListener(
        final OnUtteranceCompletedListener listener) {
    if (tts != null)
        tts.setOnUtteranceCompletedListener(listener);
    else if (ttse != null)
        ttse.setOnUtteranceCompletedListener(new TextToSpeechBeta.OnUtteranceCompletedListener() {
     @Override
     public void onUtteranceCompleted(String utteranceId) {
         listener.onUtteranceCompleted(utteranceId);
     }
        });
    }

    public void shutdown() {
    if (tts != null)
        tts.shutdown();
    if (ttse != null)
        ttse.shutdown();
    }
}

Конкретная реализация может быть и другой.

Конфигурируем TTS

Нам нужно сконфигурировать TTS определенным голосом. Голос, в свою очередь, определяется следующими параметрами:

  • Engine — задается функцией setEngineByPackageName.
  • Locale — задается функцией setLanguage.

Вариант 1, легкий, но редкий

Так работает Loquendo. Пишем:

tts.setEngineByPackageName("com.loquendo.tts.susan");

И всё начинает работать.

Вариант 2, сложный и частый

Так работают Pico и SVOX. У них есть оболочка (engine) и подключаемые модули (голоса). Рассмотрим на примере Pico

tts.setEngineByPackageName("com.svox.pico");
tts.setLanguage(Locale.US);

Тоже вроде все работает. Проблемы начинаются, когда у одной локали оказывается несколько голосов. Такое имеет место для SVOX. У одного языка может быть мужской, женский и детский голос. Это разные приложения, у них разные названия пакетов, но с точки зрения TTS все это одно и то же.

Если установлено несколько голосов для одной локали, выбран будет тот, который указан в настройках SVOX как дефолтный. Однако, мы это никак отследить не можем. Печально.

Общие проблемы для обоих вариантов

TTS-движок задизаблен в настройках TextToSpeech

Вот так:

SVOX disabled

У меня так и не получилось отловить эту ситуацию. По идее, setEngineByPackageName должен бы вернуть ERROR, и мы бы догадались, что что-то не так. Но он отрабатывает на ура, и приложение разговаривает, чем попало.

Галочка "Использовать мои настройки"

Это тоже достаточно вредная штука, и её нужно учитывать. Дело в том, что пользователь может выставить собственные настройки TTS и эту галочку.

Мои настройки

И тогда вся ваша конфигурация не будет применяться. Отслеживать состояние этой настройки можно с помощью метода areDefaultsEnforced (в Android 2.2 и выше. Если версия меньше, нужен TTSE и метод areDefaultsEnforcedExtended)

Заключение

Собственно, вот и все, что накопилось за те две недели, что я занимаюсь озвучкой приложения. Субъективное ощущение от этого API — сыровато. Не хватает доступа ко всем настройкам TTS в системе. Для пользователя они слишком сложные и неочевидные ("Мои настройки" — яркий пример). Разнобой в опциях различных TTS-движков также печалит. В общем, использовать TTS не так сложно, а вот обрабатывать различные его состояния — целое дело.

Исходный текст

Голосовать: 
0
Голосов пока нет