/* * Copyright (C) 2012-2013 Reece H. Dunn * Copyright (C) 2011 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * This file implements the Android Text-to-Speech engine for eSpeak. * * Android Version: 4.0 (Ice Cream Sandwich) * API Version: 14 */ package com.reecedunn.espeak; import android.annotation.SuppressLint; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioTrack; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.speech.tts.SynthesisCallback; import android.speech.tts.SynthesisRequest; import android.speech.tts.TextToSpeech; import android.speech.tts.TextToSpeechService; import android.util.Log; import android.util.Pair; import com.reecedunn.espeak.SpeechSynthesis.SynthReadyCallback; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; /** * Implements the eSpeak engine as a {@link TextToSpeechService}. * * @author msclrhd@gmail.com (Reece H. Dunn) * @author alanv@google.com (Alan Viverette) */ @SuppressLint("NewApi") public class TtsService extends TextToSpeechService { public static final String ESPEAK_INITIALIZED = "com.reecedunn.espeak.ESPEAK_INITIALIZED"; private static final String TAG = TtsService.class.getSimpleName(); private static final boolean DEBUG = false; private SpeechSynthesis mEngine; private SynthesisCallback mCallback; private final Map mAvailableVoices = new HashMap(); protected Voice mMatchingVoice = null; private BroadcastReceiver mOnLanguagesDownloaded = null; @Override public void onCreate() { initializeTtsEngine(); super.onCreate(); } @Override public void onDestroy() { super.onDestroy(); if (mOnLanguagesDownloaded != null) { unregisterReceiver(mOnLanguagesDownloaded); } } /** * Sets up the native eSpeak engine. */ private void initializeTtsEngine() { if (mEngine != null) { mEngine.stop(); mEngine = null; } mEngine = new SpeechSynthesis(this, mSynthCallback); mAvailableVoices.clear(); for (Voice voice : mEngine.getAvailableVoices()) { mAvailableVoices.put(voice.name, voice); } final Intent intent = new Intent(ESPEAK_INITIALIZED); sendBroadcast(intent); } @Override protected String[] onGetLanguage() { // This is used to specify the language requested from GetSampleText. if (mMatchingVoice == null) { return new String[] { "eng", "GBR", "" }; } return new String[] { mMatchingVoice.locale.getISO3Language(), mMatchingVoice.locale.getISO3Country(), mMatchingVoice.locale.getVariant() }; } private Pair findVoice(String language, String country, String variant) { if (!CheckVoiceData.hasBaseResources(this) || CheckVoiceData.canUpgradeResources(this)) { if (mOnLanguagesDownloaded == null) { mOnLanguagesDownloaded = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { initializeTtsEngine(); } }; final IntentFilter filter = new IntentFilter(DownloadVoiceData.BROADCAST_LANGUAGES_UPDATED); registerReceiver(mOnLanguagesDownloaded, filter); } final Intent intent = new Intent(this, DownloadVoiceData.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); return new Pair<>(null, TextToSpeech.LANG_MISSING_DATA); } final Locale query = new Locale(language, country, variant); Voice languageVoice = null; Voice countryVoice = null; synchronized (mAvailableVoices) { for (Voice voice : mAvailableVoices.values()) { switch (voice.match(query)) { case TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE: return new Pair<>(voice, TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE); case TextToSpeech.LANG_COUNTRY_AVAILABLE: countryVoice = voice; case TextToSpeech.LANG_AVAILABLE: languageVoice = voice; break; } } } if (languageVoice == null) { return new Pair<>(null, TextToSpeech.LANG_NOT_SUPPORTED); } else if (countryVoice == null) { return new Pair<>(languageVoice, TextToSpeech.LANG_AVAILABLE); } else { return new Pair<>(countryVoice, TextToSpeech.LANG_COUNTRY_AVAILABLE); } } private Pair getDefaultVoiceFor(String language, String country, String variant) { final Pair match = findVoice(language, country, variant); switch (match.second) { case TextToSpeech.LANG_AVAILABLE: return new Pair<>(findVoice(language, "", "").first, match.second); case TextToSpeech.LANG_COUNTRY_AVAILABLE: return new Pair<>(findVoice(language, country, "").first, match.second); default: return match; } } @Override protected int onIsLanguageAvailable(String language, String country, String variant) { return findVoice(language, country, variant).second; } @Override protected int onLoadLanguage(String language, String country, String variant) { final Pair match = getDefaultVoiceFor(language, country, variant); if (match.first != null) { mMatchingVoice = match.first; } return match.second; } @Override public String onGetDefaultVoiceNameFor(String language, String country, String variant) { final Voice match = getDefaultVoiceFor(language, country, variant).first; return (match == null) ? null : match.name; } @Override public List onGetVoices() { List voices = new ArrayList(); for (Voice voice : mAvailableVoices.values()) { int quality = android.speech.tts.Voice.QUALITY_NORMAL; int latency = android.speech.tts.Voice.LATENCY_VERY_LOW; voices.add(new android.speech.tts.Voice(voice.name, new Locale(voice.locale.getISO3Language(), voice.locale.getISO3Country(), voice.locale.getVariant()), quality, latency, false, null)); } return voices; } @Override public int onIsValidVoiceName(String name) { Voice voice = mAvailableVoices.get(name); return (voice == null) ? TextToSpeech.ERROR : TextToSpeech.SUCCESS; } @Override public int onLoadVoice(String name) { Voice voice = mAvailableVoices.get(name); if (voice == null) { return TextToSpeech.ERROR; } mMatchingVoice = voice; return TextToSpeech.SUCCESS; } @Override protected void onStop() { Log.i(TAG, "Received stop request."); mEngine.stop(); } @SuppressWarnings("deprecation") private String getRequestString(SynthesisRequest request) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { return request.getCharSequenceText().toString(); } else { return request.getText(); } } private int selectVoice(SynthesisRequest request) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { final String name = request.getVoiceName(); if (name != null && !name.isEmpty()) { return onLoadVoice(name); } } final int result = onLoadLanguage(request.getLanguage(), request.getCountry(), request.getVariant()); switch (result) { case TextToSpeech.LANG_MISSING_DATA: case TextToSpeech.LANG_NOT_SUPPORTED: return TextToSpeech.ERROR; } return TextToSpeech.SUCCESS; } @Override protected synchronized void onSynthesizeText(SynthesisRequest request, SynthesisCallback callback) { if (selectVoice(request) == TextToSpeech.ERROR) return; String text = getRequestString(request); if (text == null) return; if (DEBUG) { Log.i(TAG, "Received synthesis request: {language=\"" + mMatchingVoice.name + "\"}"); final Bundle params = request.getParams(); for (String key : params.keySet()) { Log.v(TAG, "Synthesis request contained param {" + key + ", " + params.get(key) + "}"); } } if (text.startsWith("" preprocessing tags, // so need to remove these before passing to synthesize. text = text.substring(text.indexOf("?>") + 2).trim(); } mCallback = callback; mCallback.start(mEngine.getSampleRate(), mEngine.getAudioFormat(), mEngine.getChannelCount()); final VoiceSettings settings = new VoiceSettings(PreferenceManager.getDefaultSharedPreferences(this), mEngine); mEngine.setVoice(mMatchingVoice, settings.getVoiceVariant()); mEngine.Rate.setValue(settings.getRate(), request.getSpeechRate()); mEngine.Pitch.setValue(settings.getPitch(), request.getPitch()); mEngine.PitchRange.setValue(settings.getPitchRange()); mEngine.Volume.setValue(settings.getVolume()); mEngine.Punctuation.setValue(settings.getPunctuationLevel()); mEngine.setPunctuationCharacters(settings.getPunctuationCharacters()); mEngine.synthesize(text, text.startsWith("