/*
* Copyright (C) 2013 jonas.oreland@gmail.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.runnerup.workout.feedback;
import android.annotation.TargetApi;
import android.content.Context;
import android.media.AudioManager;
import android.os.Build;
import android.speech.tts.TextToSpeech;
import android.speech.tts.UtteranceProgressListener;
import android.util.Log;
import org.runnerup.util.Formatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;
@TargetApi(Build.VERSION_CODES.FROYO)
public class RUTextToSpeech {
private static final String UTTERANCE_ID = "RUTextTospeech";
boolean mute = false;
final boolean trace = true;
final TextToSpeech textToSpeech;
final AudioManager audioManager;
long id = (long) (System.nanoTime() + (1000 * Math.random()));
class Entry {
final String text;
final HashMap<String, String> params;
public Entry(String text, HashMap<String, String> params) {
this.text = text;
this.params = params;
}
}
final HashSet<String> cueSet = new HashSet<String>();
final ArrayList<Entry> cueList = new ArrayList<Entry>();
public RUTextToSpeech(TextToSpeech tts, boolean mute_, Context context) {
this.textToSpeech = tts;
this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
this.mute = mute_;
Locale locale = Formatter.getAudioLocale(context);
if (locale != null) {
int res;
switch((res = tts.isLanguageAvailable(locale))) {
case TextToSpeech.LANG_AVAILABLE:
case TextToSpeech.LANG_COUNTRY_AVAILABLE:
case TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE:
res = tts.setLanguage(locale);
Log.e(getClass().getName(), "setLanguage(" + locale.getDisplayLanguage() + ") => " + res);
break;
case TextToSpeech.LANG_MISSING_DATA:
case TextToSpeech.LANG_NOT_SUPPORTED:
Log.e(getClass().getName(), "setLanguage("+locale.getDisplayLanguage()+") => MISSING: " + res);
break;
}
}
if (this.mute) {
UtteranceCompletion.setUtteranceCompletedListener(tts, this);
}
}
private String getId(String text) {
long val;
synchronized (this) {
val = this.id;
this.id++;
}
return UTTERANCE_ID + Long.toString(val);
}
int speak(String text, int queueMode, HashMap<String, String> params) {
if (queueMode == TextToSpeech.QUEUE_FLUSH) {
if (trace) {
Log.e(getClass().getName(), "speak (mute: " + mute + "): " + text);
}
// speak directly
if (mute) {
return speakWithMute(text, queueMode, params);
} else {
return textToSpeech.speak(text, queueMode, params);
}
} else {
if (!cueSet.contains(text)) {
if (trace) {
Log.e(getClass().getName(), "buffer speak: " + text);
}
cueSet.add(text);
cueList.add(new Entry(text, params));
} else {
if (trace) {
Log.e(getClass().getName(), "skip buffer (duplicate) speak: " + text);
}
}
return 0;
}
}
/**
* Requests audio focus before speaking, if no focus is given nothing is
* said.
*
* @param text
* @param queueMode
* @param params
* @return
*/
private int speakWithMute(String text, int queueMode,
HashMap<String, String> params) {
if (requestFocus()) {
final String utId = getId(text);
outstanding.add(utId);
if (params == null) {
params = new HashMap<String, String>();
}
params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utId);
int res = textToSpeech.speak(text, queueMode, params);
if (res == TextToSpeech.ERROR) {
outstanding.remove(utId);
}
if (outstanding.isEmpty()) {
audioManager.abandonAudioFocus(null);
}
return res;
}
Log.e(getClass().getName(), "Could not get audio focus.");
return TextToSpeech.ERROR;
}
final HashSet<String> outstanding = new HashSet<String>();
void utteranceCompleted(String id) {
outstanding.remove(id);
if (outstanding.isEmpty()) {
audioManager.abandonAudioFocus(null);
}
}
private boolean requestFocus() {
final AudioManager am = audioManager;
int result = am.requestAudioFocus(
null,// afChangeListener,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
}
public void emit() {
if (cueSet.isEmpty()) {
return;
}
if (mute && requestFocus() == true) {
for (Entry e : cueList) {
final String utId = getId(e.text);
outstanding.add(utId);
HashMap<String, String> params = e.params;
if (params == null) {
params = new HashMap<String, String>();
}
params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utId);
int res = textToSpeech.speak(e.text, TextToSpeech.QUEUE_ADD, params);
if (res == TextToSpeech.ERROR) {
Log.e(getClass().getName(), "res == ERROR emit() text: " + e.text + ", utId: " + utId
+ ") outstanding.size(): " + outstanding.size());
outstanding.remove(utId);
}
}
if (outstanding.isEmpty()) {
audioManager.abandonAudioFocus(null);
}
} else {
for (Entry e : cueList) {
textToSpeech.speak(e.text, TextToSpeech.QUEUE_ADD, e.params);
}
}
cueSet.clear();
cueList.clear();
}
}
// separate class to handle FROYO/deprecation
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
class UtteranceCompletion {
@SuppressWarnings("deprecation")
public static void setUtteranceCompletedListener(
TextToSpeech tts, final RUTextToSpeech ruTextToSpeech) {
if (Build.VERSION.SDK_INT < 15) {
tts
.setOnUtteranceCompletedListener(new android.speech.tts.TextToSpeech.OnUtteranceCompletedListener() {
@Override
public void onUtteranceCompleted(String utteranceId) {
ruTextToSpeech.utteranceCompleted(utteranceId);
}
});
} else {
tts.setOnUtteranceProgressListener(new UtteranceProgressListener() {
@Override
public void onDone(String utteranceId) {
ruTextToSpeech.utteranceCompleted(utteranceId);
}
@Override
public void onError(String utteranceId) {
ruTextToSpeech.utteranceCompleted(utteranceId);
}
@Override
public void onStart(String utteranceId) {
}
});
}
}
}