Java tutorial
/* Copyright (C) 2013 Olga Yakovleva <yakovleva.o.v@gmail.com> */ /* This program 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 */ /* (at your option) any later version. */ /* This program 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 this program. If not, see <http://www.gnu.org/licenses/>. */ package com.github.olga_yakovleva.rhvoice.android; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.media.AudioFormat; import android.os.Build; 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.speech.tts.Voice; import android.support.v4.content.LocalBroadcastManager; import android.text.TextUtils; import android.util.Log; import com.github.olga_yakovleva.rhvoice.*; import java.io.File; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; public final class RHVoiceService extends TextToSpeechService { private static final String TAG = "RHVoiceTTSService"; private static final int[] languageSupportConstants = { TextToSpeech.LANG_NOT_SUPPORTED, TextToSpeech.LANG_AVAILABLE, TextToSpeech.LANG_COUNTRY_AVAILABLE, TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE }; private final BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (BuildConfig.DEBUG) Log.v(TAG, "Data state changed"); initialize(); } }; private static class Tts { public TTSEngine engine = null; public List<AndroidVoiceInfo> voices; public Set<String> languages; public Map<String, AndroidVoiceInfo> voiceIndex; public Tts() { voices = new ArrayList<AndroidVoiceInfo>(); languages = new HashSet<String>(); voiceIndex = new HashMap<String, AndroidVoiceInfo>(); } public Tts(Tts other, boolean passEngine) { this.voices = other.voices; this.languages = other.languages; this.voiceIndex = other.voiceIndex; if (!passEngine) return; this.engine = other.engine; other.engine = null; } } private static class TtsManager { private Tts tts; private boolean done; public synchronized void reset(Tts newTts) { if (newTts == null) throw new IllegalArgumentException(); if (tts != null && tts.engine != null) tts.engine.shutdown(); tts = newTts; } public synchronized void destroy() { done = true; if (tts != null && tts.engine != null) tts.engine.shutdown(); tts = null; } public synchronized Tts acquireForSynthesis() { if (done) return null; if (tts == null) return null; if (tts.engine == null) return null; Tts result = new Tts(tts, true); return result; } public synchronized void release(Tts usedTts) { if (usedTts == null || usedTts.engine == null) throw new IllegalArgumentException(); if (done || tts.engine != null) usedTts.engine.shutdown(); else tts = new Tts(usedTts, true); } public synchronized Tts get() { if (tts == null) return null; return new Tts(tts, false); } } private final TtsManager ttsManager = new TtsManager(); private volatile AndroidVoiceInfo currentVoice; private volatile boolean speaking = false; private class Player implements TTSClient { private SynthesisCallback callback; public Player(SynthesisCallback callback) { this.callback = callback; } public boolean playSpeech(short[] samples) { if (!speaking) return false; final ByteBuffer buffer = ByteBuffer.allocate(samples.length * 2); buffer.order(ByteOrder.LITTLE_ENDIAN); buffer.asShortBuffer().put(samples); final byte[] bytes = buffer.array(); final int size = callback.getMaxBufferSize(); int offset = 0; int count; while (offset < bytes.length) { if (!speaking) return false; count = Math.min(size, bytes.length - offset); if (callback.audioAvailable(bytes, offset, count) != TextToSpeech.SUCCESS) return false; offset += count; } return true; } }; static private class Candidate { public final AndroidVoiceInfo voice; public final int score; public Candidate() { voice = null; score = 0; } public Candidate(AndroidVoiceInfo voice, String language, String country, String variant) { this.voice = voice; score = voice.getSupportLevel(language, country, variant); } } private static class LanguageSettings { public AndroidVoiceInfo voice; public boolean detect; } private void logLanguage(String language, String country, String variant) { Log.v(TAG, "Language: " + language); if (TextUtils.isEmpty(country)) return; Log.v(TAG, "Country: " + country); if (TextUtils.isEmpty(variant)) return; Log.v(TAG, "Variant: " + variant); } private Map<String, LanguageSettings> getLanguageSettings(Tts tts) { Map<String, LanguageSettings> result = new HashMap<String, LanguageSettings>(); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); for (String language : tts.languages) { LanguageSettings settings = new LanguageSettings(); String prefVoice = prefs.getString("language." + language + ".voice", null); for (AndroidVoiceInfo voice : tts.voices) { if (!voice.getSource().getLanguage().getAlpha3Code().equals(language)) continue; String voiceName = voice.getSource().getName(); if (settings.voice == null) { settings.voice = voice; if (prefVoice == null) break; } if (voiceName.equals(prefVoice)) { settings.voice = voice; break; } } settings.detect = prefs.getBoolean("language." + language + ".detect", true); result.put(language, settings); } return result; } private Candidate findBestVoice(Tts tts, String language, String country, String variant, String voiceName, Map<String, LanguageSettings> languageSettings) { if (!TextUtils.isEmpty(voiceName)) { AndroidVoiceInfo voice = tts.voiceIndex.get(voiceName); if (voice != null) return new Candidate(voice, language, country, ""); } LanguageSettings settings = null; if (languageSettings != null) settings = languageSettings.get(language); Candidate best = new Candidate(); Candidate bestPreferred = new Candidate(); for (AndroidVoiceInfo voice : tts.voices) { Candidate candidate = new Candidate(voice, language, country, variant); if (candidate.score > best.score) best = candidate; if ((settings != null) && settings.voice.equals(voice)) bestPreferred = candidate; } return bestPreferred.score >= best.score ? bestPreferred : best; } private void initialize() { if (BuildConfig.DEBUG) Log.i(TAG, "Initializing the engine"); DataManager dm = new DataManager(this); dm.checkFiles(); if (!dm.isUpToDate()) startService(new Intent(this, DataService.class)); List<String> paths = dm.getUpToDatePaths(); if (paths.isEmpty()) { Log.w(TAG, "No voice data"); return; } Tts tts = new Tts(); try { File configDir = Config.getDir(this); tts.engine = new TTSEngine("", configDir.getAbsolutePath(), paths, CoreLogger.instance); } catch (Exception e) { if (BuildConfig.DEBUG) Log.e(TAG, "Error during engine initialization", e); return; } List<VoiceInfo> engineVoices = tts.engine.getVoices(); if (engineVoices.isEmpty()) { if (BuildConfig.DEBUG) Log.w(TAG, "No voices"); tts.engine.shutdown(); return; } for (VoiceInfo engineVoice : engineVoices) { AndroidVoiceInfo nextVoice = new AndroidVoiceInfo(engineVoice); if (BuildConfig.DEBUG) Log.i(TAG, "Found voice " + nextVoice.toString()); tts.voices.add(nextVoice); tts.voiceIndex.put(nextVoice.getName(), nextVoice); tts.languages.add(engineVoice.getLanguage().getAlpha3Code()); } ttsManager.reset(tts); } @Override public void onCreate() { if (BuildConfig.DEBUG) Log.i(TAG, "Starting the service"); LocalBroadcastManager.getInstance(this).registerReceiver(receiver, new IntentFilter(DataService.ACTION_DATA_STATE_CHANGED)); initialize(); super.onCreate(); } @Override public void onDestroy() { super.onDestroy(); LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver); ttsManager.destroy(); } @Override protected String[] onGetLanguage() { String[] result = { "rus", "RUS", "" }; AndroidVoiceInfo voice = currentVoice; if (voice == null) { Tts tts = ttsManager.get(); if (tts != null) { Locale locale = Locale.getDefault(); Candidate bestMatch = findBestVoice(tts, locale.getISO3Language(), locale.getISO3Country(), "", "", getLanguageSettings(tts)); if (bestMatch.voice != null) voice = bestMatch.voice; } } if (voice == null) return result; result[0] = voice.getLanguage(); result[1] = voice.getCountry(); return result; } @Override protected int onIsLanguageAvailable(String language, String country, String variant) { if (BuildConfig.DEBUG) { Log.v(TAG, "onIsLanguageAvailable called"); logLanguage(language, country, variant); } Tts tts = ttsManager.get(); if (tts == null) { if (BuildConfig.DEBUG) Log.w(TAG, "Not initialized yet"); return TextToSpeech.LANG_NOT_SUPPORTED; } Candidate bestMatch = findBestVoice(tts, language, country, variant, "", null); int result = languageSupportConstants[bestMatch.score]; if (BuildConfig.DEBUG) Log.v(TAG, "Result: " + result); return result; } @Override protected int onLoadLanguage(String language, String country, String variant) { if (BuildConfig.DEBUG) Log.v(TAG, "onLoadLanguage called"); return onIsLanguageAvailable(language, country, variant); } @Override protected void onStop() { speaking = false; } @Override protected void onSynthesizeText(SynthesisRequest request, SynthesisCallback callback) { if (BuildConfig.DEBUG) { Log.v(TAG, "onSynthesize called"); logLanguage(request.getLanguage(), request.getCountry(), request.getVariant()); } Tts tts = ttsManager.acquireForSynthesis(); if (tts == null) { if (BuildConfig.DEBUG) Log.w(TAG, "Not initialized yet"); callback.error(); return; } try { speaking = true; Map<String, LanguageSettings> languageSettings = getLanguageSettings(tts); String voiceName = ""; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) voiceName = request.getVoiceName(); final Candidate bestMatch = findBestVoice(tts, request.getLanguage(), request.getCountry(), request.getVariant(), voiceName, languageSettings); if (bestMatch.voice == null) { if (BuildConfig.DEBUG) Log.e(TAG, "Unsupported language"); callback.error(); return; } if (BuildConfig.DEBUG) Log.v(TAG, "Selected voice: " + bestMatch.voice.getSource().getName()); currentVoice = bestMatch.voice; StringBuilder voiceProfileSpecBuilder = new StringBuilder(); voiceProfileSpecBuilder.append(bestMatch.voice.getSource().getName()); for (Map.Entry<String, LanguageSettings> entry : languageSettings.entrySet()) { if (entry.getKey().equals(bestMatch.voice.getLanguage())) continue; if (entry.getValue().detect) { String name = entry.getValue().voice.getSource().getName(); voiceProfileSpecBuilder.append("+").append(name); } } String profileSpec = voiceProfileSpecBuilder.toString(); if (BuildConfig.DEBUG) Log.v(TAG, "Synthesizing the following text: " + request.getText()); int rate = request.getSpeechRate(); if (BuildConfig.DEBUG) Log.v(TAG, "rate=" + rate); int pitch = request.getPitch(); if (BuildConfig.DEBUG) Log.v(TAG, "pitch=" + pitch); if (BuildConfig.DEBUG) Log.v(TAG, "Profile: " + profileSpec); final SynthesisParameters params = new SynthesisParameters(); params.setVoiceProfile(profileSpec); params.setRate(((double) rate) / 100.0); params.setPitch(((double) pitch) / 100.0); final Player player = new Player(callback); callback.start(24000, AudioFormat.ENCODING_PCM_16BIT, 1); tts.engine.speak(request.getText(), params, player); callback.done(); } catch (RHVoiceException e) { if (BuildConfig.DEBUG) Log.e(TAG, "Synthesis error", e); callback.error(); } finally { speaking = false; ttsManager.release(tts); } } @Override public String onGetDefaultVoiceNameFor(String language, String country, String variant) { if (BuildConfig.DEBUG) { Log.v(TAG, "onGetDefaultVoiceNameFor called"); logLanguage(language, country, variant); } Tts tts = ttsManager.get(); if (tts == null) return null; Candidate bestMatch = findBestVoice(tts, language, country, variant, "", getLanguageSettings(tts)); if (bestMatch == null || bestMatch.voice == null) return null; String name = bestMatch.voice.getName(); if (BuildConfig.DEBUG) Log.v(TAG, "Voice name: " + name); return name; } @Override public List<Voice> onGetVoices() { List<Voice> result = new ArrayList<Voice>(); Tts tts = ttsManager.get(); if (tts == null) return result; for (AndroidVoiceInfo voice : tts.voices) { result.add(voice.getAndroidVoice()); } return result; } @Override public int onIsValidVoiceName(String name) { if (BuildConfig.DEBUG) Log.v(TAG, "onIsValidVoiceName called for name " + name); Tts tts = ttsManager.get(); if (tts == null) return TextToSpeech.ERROR; if (tts.voiceIndex.containsKey(name)) { if (BuildConfig.DEBUG) Log.v(TAG, "Voice found"); return TextToSpeech.SUCCESS; } else { if (BuildConfig.DEBUG) Log.v(TAG, "Voice not found"); return TextToSpeech.ERROR; } } @Override public int onLoadVoice(String name) { return TextToSpeech.SUCCESS; } }