Синтез речи в Android-приложении Не так давно пришлось прикручивать к нашему приложению озвучку с помощью Text-to-Speech (TTS). Об этом-то я и хочу сегодня рассказать. Quick Start 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 import import import import android.content.Context; android.os.Build; android.speech.tts.TextToSpeech; android.speech.tts.TextToSpeech.OnInitListener; 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 Вот так: У меня так и не получилось отловить эту ситуацию. По идее, setEngineByPackageName должен бы вернуть ERROR, и мы бы догадались, что что-то не так. Но он отрабатывает на ура, и приложение разговаривает, чем попало. Галочка "Использовать мои настройки" Это тоже достаточно вредная штука, и еѐ нужно учитывать. Дело в том, что пользователь может выставить собственные настройки TTS и эту галочку. И тогда вся ваша конфигурация не будет применяться. Отслеживать состояние этой настройки можно с помощью метода areDefaultsEnforced (в Android 2.2 и выше. Если версия меньше, нужен TTSE и метод areDefaultsEnforcedExtended) Заключение Собственно, вот и все, что накопилось за те две недели, что я занимаюсь озвучкой приложения. Субъективное ощущение от этого API — сыровато. Не хватает доступа ко всем настройкам TTS в системе. Для пользователя они слишком сложные и неочевидные ("Мои настройки" — яркий пример). Разнобой в опциях различных TTS-движков также печалит. В общем, использовать TTS не так сложно, а вот обрабатывать различные его состояния — целое дело. Ссылки An introduction to Text-To-Speech in Android Using Text to Speech in Android Пример Исходники к статье прилагаются. Там рассмотрены следующие ситуации: Простая инициализация TTS Проверка голосовых данных Pico Использование TextToSpeechBeta