Merge pull request #5912 from osmandapp/js_voice_routing

JS TTS refactoring
This commit is contained in:
vshcherb 2018-08-21 10:27:44 +02:00 committed by GitHub
commit 3dc93e3c15
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 41 additions and 469 deletions

View file

@ -588,11 +588,12 @@ public class AppInitializer implements IProgress {
boolean useJs = app.getSettings().USE_JS_VOICE_GUIDANCE.get();
if (useJs && JSTTSCommandPlayerImpl.isMyData(voiceDir)) {
return new JSTTSCommandPlayerImpl(ctx, applicationMode, osmandApplication.getRoutingHelper().getVoiceRouter(), voiceProvider);
} else if (MediaCommandPlayerImpl.isMyData(voiceDir)) {
return useJs && JSMediaCommandPlayerImpl.isMyData(voiceDir) ? new JSMediaCommandPlayerImpl(osmandApplication, applicationMode, osmandApplication.getRoutingHelper().getVoiceRouter(), voiceProvider)
: new MediaCommandPlayerImpl(osmandApplication, applicationMode, osmandApplication.getRoutingHelper().getVoiceRouter(), voiceProvider);
} else if (useJs && JSMediaCommandPlayerImpl.isMyData(voiceDir)) {
return new JSMediaCommandPlayerImpl(osmandApplication, applicationMode, osmandApplication.getRoutingHelper().getVoiceRouter(), voiceProvider);
} else if (TTSCommandPlayerImpl.isMyData(voiceDir)) {
return new TTSCommandPlayerImpl(ctx, applicationMode, osmandApplication.getRoutingHelper().getVoiceRouter(), voiceProvider);
} else if (MediaCommandPlayerImpl.isMyData((voiceDir))) {
return new MediaCommandPlayerImpl(osmandApplication, applicationMode, osmandApplication.getRoutingHelper().getVoiceRouter(), voiceProvider);
}
throw new CommandPlayerException(ctx.getString(R.string.voice_data_not_supported));
}

View file

@ -1,164 +0,0 @@
package net.osmand.plus.voice;
import android.content.Context;
import android.media.AudioManager;
import net.osmand.PlatformUtil;
import net.osmand.StateChangedListener;
import net.osmand.plus.ApplicationMode;
import net.osmand.plus.OsmandApplication;
import net.osmand.plus.api.AudioFocusHelper;
import org.apache.commons.logging.Log;
import java.util.List;
import alice.tuprolog.InvalidLibraryException;
import alice.tuprolog.Prolog;
import alice.tuprolog.Struct;
import alice.tuprolog.Term;
import alice.tuprolog.Var;
public abstract class AbstractJSCommandPlayer implements CommandPlayer, StateChangedListener<ApplicationMode> {
private static final Log log = PlatformUtil.getLog(AbstractJSCommandPlayer.class);
protected String language = "";
protected OsmandApplication ctx;
protected int streamType;
private ApplicationMode applicationMode;
public static boolean btScoStatus = false;
private static AudioFocusHelper mAudioFocusHelper;
public static String btScoInit = "";
protected AbstractJSCommandPlayer(OsmandApplication ctx, ApplicationMode applicationMode,
String voiceProvider) {
this.ctx = ctx;
this.applicationMode = applicationMode;
long time = System.currentTimeMillis();
this.ctx = ctx;
if (log.isInfoEnabled()) {
log.info("Initializing prolog system : " + (System.currentTimeMillis() - time)); //$NON-NLS-1$
}
this.streamType = ctx.getSettings().AUDIO_STREAM_GUIDANCE.getModeValue(applicationMode);
language = voiceProvider.substring(0, voiceProvider.indexOf("-tts"));
}
@Override
public String getCurrentVoice() {
return null;
}
@Override
public CommandBuilder newCommandBuilder() {
JSCommandBuilder commandBuilder = new JSCommandBuilder(this);
commandBuilder.setParameters("km-m", true);
return commandBuilder;
}
@Override
public void playCommands(CommandBuilder builder) {
}
@Override
public void clear() {
if(ctx != null && ctx.getSettings() != null) {
ctx.getSettings().APPLICATION_MODE.removeListener(this);
}
abandonAudioFocus();
ctx = null;
}
@Override
public List<String> execute(List<Struct> listStruct) {
return null;
}
@Override
public void updateAudioStream(int streamType) {
}
@Override
public String getLanguage() {
return "en";
}
@Override
public boolean supportsStructuredStreetNames() {
return true;
}
@Override
public void stop() {
}
protected synchronized void abandonAudioFocus() {
log.debug("abandonAudioFocus");
if ((ctx != null && ctx.getSettings().AUDIO_STREAM_GUIDANCE.getModeValue(applicationMode) == 0) || (btScoStatus == true)) {
toggleBtSco(false);
}
if (ctx != null && mAudioFocusHelper != null) {
mAudioFocusHelper.abandonFocus(ctx, applicationMode, streamType);
}
mAudioFocusHelper = null;
}
private synchronized boolean toggleBtSco(boolean on) {
// Hardy, 2016-07-03: Establish a low quality BT SCO (Synchronous Connection-Oriented) link to interrupt e.g. a car stereo FM radio
if (on) {
try {
AudioManager mAudioManager = (AudioManager) ctx.getSystemService(Context.AUDIO_SERVICE);
if (mAudioManager == null || !mAudioManager.isBluetoothScoAvailableOffCall()) {
btScoInit = "Reported not available.";
return false;
}
mAudioManager.setMode(0);
mAudioManager.startBluetoothSco();
mAudioManager.setBluetoothScoOn(true);
mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
btScoStatus = true;
} catch (Exception e) {
System.out.println("Exception starting BT SCO " + e.getMessage() );
btScoStatus = false;
btScoInit = "Available, but not initializad.\n(" + e.getMessage() + ")";
return false;
}
btScoInit = "Available, initialized OK.";
return true;
} else {
AudioManager mAudioManager = (AudioManager) ctx.getSystemService(Context.AUDIO_SERVICE);
if (mAudioManager == null) {
return false;
}
mAudioManager.setBluetoothScoOn(false);
mAudioManager.stopBluetoothSco();
mAudioManager.setMode(AudioManager.MODE_NORMAL);
btScoStatus = false;
return true;
}
}
public ApplicationMode getApplicationMode() {
return applicationMode;
}
@Override
public void stateChanged(ApplicationMode change) {
//
// if(prologSystem != null) {
// try {
// prologSystem.getTheoryManager().retract(new Struct("appMode", new Var()));
// } catch (Exception e) {
// log.error("Retract error: ", e);
// }
// prologSystem.getTheoryManager()
// .assertA(
// new Struct("appMode", new Struct(ctx.getSettings().APPLICATION_MODE.get().getStringKey()
// .toLowerCase())), true, "", true);
// }
}
}

View file

@ -75,21 +75,27 @@ public abstract class AbstractPrologCommandPlayer implements CommandPlayer, Stat
this.sortedVoiceVersions = sortedVoiceVersions;
this.applicationMode = applicationMode;
long time = System.currentTimeMillis();
try {
this.ctx = ctx;
prologSystem = new Prolog(getLibraries());
} catch (InvalidLibraryException e) {
log.error("Initializing error", e); //$NON-NLS-1$
throw new RuntimeException(e);
}
if (log.isInfoEnabled()) {
log.info("Initializing prolog system : " + (System.currentTimeMillis() - time)); //$NON-NLS-1$
}
this.ctx = ctx;
this.streamType = ctx.getSettings().AUDIO_STREAM_GUIDANCE.getModeValue(applicationMode);
init(voiceProvider, ctx.getSettings(), configFile);
final Term langVal = solveSimplePredicate("language");
if (langVal instanceof Struct) {
language = ((Struct) langVal).getName();
if (!ctx.getSettings().USE_JS_VOICE_GUIDANCE.get()) {
if (log.isInfoEnabled()) {
log.info("Initializing prolog system : " + (System.currentTimeMillis() - time)); //$NON-NLS-1$
}
try {
prologSystem = new Prolog(getLibraries());
} catch (InvalidLibraryException e) {
log.error("Initializing error", e); //$NON-NLS-1$
throw new RuntimeException(e);
}
init(voiceProvider, ctx.getSettings(), configFile);
final Term langVal = solveSimplePredicate("language");
if (langVal instanceof Struct) {
language = ((Struct) langVal).getName();
}
} else if (voiceProvider != null) {
initVoiceDir(voiceProvider);
language = voiceProvider.replace("-tts", "").replace("-formal", "");
}
}
@ -141,15 +147,7 @@ public abstract class AbstractPrologCommandPlayer implements CommandPlayer, Stat
private void init(String voiceProvider, OsmandSettings settings, String configFile) throws CommandPlayerException {
prologSystem.clearTheory();
voiceDir = null;
if (voiceProvider != null) {
File parent = ctx.getAppPath(IndexConstants.VOICE_INDEX_DIR);
voiceDir = new File(parent, voiceProvider);
if (!voiceDir.exists()) {
voiceDir = null;
throw new CommandPlayerException(
ctx.getString(R.string.voice_data_unavailable));
}
}
initVoiceDir(voiceProvider);
// see comments below why it is impossible to read from zip (don't know
// how to play file from zip)
@ -201,6 +199,18 @@ public abstract class AbstractPrologCommandPlayer implements CommandPlayer, Stat
}
}
private void initVoiceDir(String voiceProvider) throws CommandPlayerException {
if (voiceProvider != null) {
File parent = ctx.getAppPath(IndexConstants.VOICE_INDEX_DIR);
voiceDir = new File(parent, voiceProvider);
if (!voiceDir.exists()) {
voiceDir = null;
throw new CommandPlayerException(
ctx.getString(R.string.voice_data_unavailable));
}
}
}
protected Term solveSimplePredicate(String predicate) {
Term val = null;
Var v = new Var("MyVariable"); //$NON-NLS-1$

View file

@ -1,7 +1,5 @@
package net.osmand.plus.voice;
import android.media.MediaPlayer;
import android.system.Os;
import net.osmand.IndexConstants;
import net.osmand.PlatformUtil;
@ -9,7 +7,6 @@ import net.osmand.plus.ApplicationMode;
import net.osmand.plus.OsmandApplication;
import net.osmand.plus.routing.VoiceRouter;
import org.apache.commons.logging.Log;
import org.mozilla.javascript.ScriptableObject;
import java.io.BufferedReader;
@ -18,12 +15,8 @@ import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import static net.osmand.plus.voice.AbstractPrologCommandPlayer.DELAY_CONST;
public class JSMediaCommandPlayerImpl extends MediaCommandPlayerImpl {
private static final org.apache.commons.logging.Log log = PlatformUtil.getLog(JSMediaCommandPlayerImpl.class);
@ -34,7 +27,6 @@ public class JSMediaCommandPlayerImpl extends MediaCommandPlayerImpl {
public JSMediaCommandPlayerImpl(OsmandApplication ctx, ApplicationMode applicationMode, VoiceRouter vrt, String voiceProvider) throws CommandPlayerException {
super(ctx, applicationMode, vrt, voiceProvider);
app = ctx;
org.mozilla.javascript.Context context = org.mozilla.javascript.Context.enter();
context.setOptimizationLevel(-1);
jsScope = context.initSafeStandardObjects();

View file

@ -28,78 +28,22 @@ import java.util.HashMap;
import java.util.List;
import java.util.Locale;
public class JSTTSCommandPlayerImpl extends AbstractJSCommandPlayer {
public class JSTTSCommandPlayerImpl extends TTSCommandPlayerImpl {
private static final org.apache.commons.logging.Log log = PlatformUtil.getLog(JSTTSCommandPlayerImpl.class);
private static final String TAG = JSTTSCommandPlayerImpl.class.getSimpleName();
private static TextToSpeech mTts;
private static String ttsVoiceStatus = "";
private static String ttsVoiceUsed = "";
private boolean speechAllowed = false;
private Context mTtsContext;
private ScriptableObject jsScope;
private float cSpeechRate = 1;
private static final class IntentStarter implements
DialogInterface.OnClickListener {
private final Context ctx;
private final String intentAction;
private final Uri intentData;
private IntentStarter(Context ctx, String intentAction) {
this(ctx,intentAction, null);
}
private IntentStarter(Context ctx, String intentAction, Uri intentData) {
this.ctx = ctx;
this.intentAction = intentAction;
this.intentData = intentData;
}
@Override
public void onClick(DialogInterface dialog, int which) {
Intent installIntent = new Intent();
installIntent.setAction(intentAction);
if (intentData != null) {
installIntent.setData(intentData);
}
ctx.startActivity(installIntent);
}
}
private OsmandApplication app;
private ApplicationMode appMode;
private VoiceRouter vrt;
private HashMap<String, String> params = new HashMap<String, String>();
private static int ttsRequests = 0;
public JSTTSCommandPlayerImpl(Activity ctx, ApplicationMode applicationMode, VoiceRouter vrt, String voiceProvider) throws CommandPlayerException {
super((OsmandApplication) ctx.getApplication(), applicationMode, voiceProvider);
this.app = (OsmandApplication) ctx.getApplicationContext();
this.appMode = applicationMode;
this.vrt = vrt;
if (Algorithms.isEmpty(language)) {
throw new CommandPlayerException(
ctx.getString(R.string.voice_data_corrupted));
}
OsmandApplication app = (OsmandApplication) ctx.getApplicationContext();
if(app.accessibilityEnabled()) {
cSpeechRate = app.getSettings().SPEECH_RATE.get();
}
initializeEngine(app, ctx);
params.put(TextToSpeech.Engine.KEY_PARAM_STREAM, app.getSettings().AUDIO_STREAM_GUIDANCE
.getModeValue(getApplicationMode()).toString());
super(ctx, applicationMode, vrt, voiceProvider);
this.app = (OsmandApplication) ctx.getApplication();
org.mozilla.javascript.Context context = org.mozilla.javascript.Context.enter();
context.setOptimizationLevel(-1);
jsScope = context.initSafeStandardObjects();
try {
BufferedReader br = new BufferedReader(new FileReader(new File(
app.getAppPath(IndexConstants.VOICE_INDEX_DIR).getAbsolutePath() +
"/" + voiceProvider + "/" + language + "_tts.js")));
"/" + voiceProvider + "/" + voiceProvider.replace("-tts", "_tts") + ".js")));
context.evaluateReader(jsScope, br, "JS", 1, null);
br.close();
} catch (IOException e) {
@ -109,129 +53,6 @@ public class JSTTSCommandPlayerImpl extends AbstractJSCommandPlayer {
}
}
private void initializeEngine(final Context ctx, final Activity act) {
if (mTtsContext != ctx) {
internalClear();
}
if (mTts == null) {
mTtsContext = ctx;
ttsVoiceStatus = "";
ttsVoiceUsed = "";
ttsRequests = 0;
final float speechRate = cSpeechRate;
final String[] lsplit = (language.replaceAll("-formal", "") + "____.").split("[_\\-]");
// constructor supports lang_country_variant
Locale newLocale0 = new Locale(lsplit[0], lsplit[1], lsplit[2]);
// #3344: Try Locale builder instead of constructor (only available from API 21). Also supports script (for now supported as trailing x_x_x_Scrp)
if (android.os.Build.VERSION.SDK_INT >= 21) {
try {
newLocale0 = new Locale.Builder().setLanguage(lsplit[0]).setScript(lsplit[3]).setRegion(lsplit[1]).setVariant(lsplit[2]).build();
} catch (RuntimeException e) {
// Falls back to constructor
}
}
final Locale newLocale = newLocale0;
mTts = new TextToSpeech(ctx, new TextToSpeech.OnInitListener() {
@Override
public void onInit(int status) {
if (status != TextToSpeech.SUCCESS) {
ttsVoiceStatus = "NO INIT SUCCESS";
internalClear();
} else if (mTts != null) {
speechAllowed = true;
switch (mTts.isLanguageAvailable(newLocale)) {
case TextToSpeech.LANG_MISSING_DATA:
if (isSettingsActivity(act)) {
AlertDialog.Builder builder = createAlertDialog(
R.string.tts_missing_language_data_title,
R.string.tts_missing_language_data,
new JSTTSCommandPlayerImpl.IntentStarter(
act,
TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA),
act);
builder.show();
}
ttsVoiceStatus = newLocale.getDisplayName() + ": LANG_MISSING_DATA";
ttsVoiceUsed = getVoiceUsed();
break;
case TextToSpeech.LANG_AVAILABLE:
ttsVoiceStatus = newLocale.getDisplayName() + ": LANG_AVAILABLE";
case TextToSpeech.LANG_COUNTRY_AVAILABLE:
ttsVoiceStatus = "".equals(ttsVoiceStatus) ? newLocale.getDisplayName() + ": LANG_COUNTRY_AVAILABLE" : ttsVoiceStatus;
case TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE:
try {
mTts.setLanguage(newLocale);
} catch(Exception e) {
e.printStackTrace();
mTts.setLanguage(Locale.getDefault());
}
if(speechRate != 1) {
mTts.setSpeechRate(speechRate);
}
ttsVoiceStatus = "".equals(ttsVoiceStatus) ? newLocale.getDisplayName() + ": LANG_COUNTRY_VAR_AVAILABLE" : ttsVoiceStatus;
ttsVoiceUsed = getVoiceUsed();
break;
case TextToSpeech.LANG_NOT_SUPPORTED:
//maybe weird, but I didn't want to introduce parameter in around 5 methods just to do this if condition
if (isSettingsActivity(act)) {
AlertDialog.Builder builder = createAlertDialog(
R.string.tts_language_not_supported_title,
R.string.tts_language_not_supported,
new JSTTSCommandPlayerImpl.IntentStarter(
act,
Intent.ACTION_VIEW, Uri.parse("market://search?q=text to speech engine"
)),
act);
builder.show();
}
ttsVoiceStatus = newLocale.getDisplayName() + ": LANG_NOT_SUPPORTED";
ttsVoiceUsed = getVoiceUsed();
break;
}
}
}
private boolean isSettingsActivity(final Context ctx) {
return ctx instanceof SettingsActivity;
}
private String getVoiceUsed() {
try {
if (android.os.Build.VERSION.SDK_INT >= 21) {
if (mTts.getVoice() != null) {
return mTts.getVoice().toString() + " (API " + android.os.Build.VERSION.SDK_INT + ")";
}
} else {
return mTts.getLanguage() + " (API " + android.os.Build.VERSION.SDK_INT + " only reports language)";
}
} catch (RuntimeException e) {
// mTts.getVoice() might throw NPE
}
return "-";
}
});
mTts.setOnUtteranceCompletedListener(new TextToSpeech.OnUtteranceCompletedListener() {
// The call back is on a binder thread.
@Override
public synchronized void onUtteranceCompleted(String utteranceId) {
if (--ttsRequests <= 0)
abandonAudioFocus();
log.debug("ttsRequests="+ttsRequests);
if (ttsRequests < 0) {
ttsRequests = 0;
}
}
});
}
}
@Override
public String getCurrentVoice() {
return null;
}
@Override
public JSCommandBuilder newCommandBuilder() {
JSCommandBuilder commandBuilder = new JSCommandBuilder(this);
@ -240,88 +61,11 @@ public class JSTTSCommandPlayerImpl extends AbstractJSCommandPlayer {
return commandBuilder;
}
@Override
public void playCommands(CommandBuilder builder) {
final List<String> execute = builder.execute(); //list of strings, the speech text, play it
StringBuilder bld = new StringBuilder();
for (String s : execute) {
bld.append(s).append(' ');
}
if (mTts != null && !vrt.isMute() && speechAllowed) {
if (ttsRequests++ == 0) {
// Delay first prompt of each batch to allow BT SCO connection being established
if (app.getSettings().AUDIO_STREAM_GUIDANCE.getModeValue(appMode) == 0) {
ttsRequests++;
if (android.os.Build.VERSION.SDK_INT < 21) {
params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID,""+System.currentTimeMillis());
mTts.playSilence(app.getSettings().BT_SCO_DELAY.get(), TextToSpeech.QUEUE_ADD, params);
} else {
mTts.playSilentUtterance(app.getSettings().BT_SCO_DELAY.get(), TextToSpeech.QUEUE_ADD, ""+System.currentTimeMillis());
}
}
}
Log.d(TAG, "ttsRequests= "+ttsRequests);
params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID,""+System.currentTimeMillis());
mTts.speak(bld.toString(), TextToSpeech.QUEUE_ADD, params);
// Audio focus will be released when onUtteranceCompleted() completed is called by the TTS engine.
} else if (app != null && vrt.isMute()) {
// sendAlertToAndroidWear(ctx, bld.toString());
}
}
@Override
public void stop(){
ttsRequests = 0;
if (mTts != null){
mTts.stop();
}
abandonAudioFocus();
}
public static String getTtsVoiceStatus() {
return ttsVoiceStatus;
}
public static String getTtsVoiceUsed() {
return ttsVoiceUsed;
}
@Override
public void clear() {
super.clear();
internalClear();
}
@Override
public void updateAudioStream(int streamType) {
super.updateAudioStream(streamType);
params.put(TextToSpeech.Engine.KEY_PARAM_STREAM, streamType+"");
}
@Override
public String getLanguage() {
return language;
}
@Override
public boolean supportsStructuredStreetNames() {
return true;
}
private void internalClear() {
ttsRequests = 0;
speechAllowed = false;
if (mTts != null) {
mTts.shutdown();
mTts = null;
}
abandonAudioFocus();
mTtsContext = null;
ttsVoiceStatus = "";
ttsVoiceUsed = "";
}
public static boolean isMyData(File voiceDir) {
if (!voiceDir.getName().contains("tts")) {
return false;
@ -333,15 +77,4 @@ public class JSTTSCommandPlayerImpl extends AbstractJSCommandPlayer {
}
return false;
}
private AlertDialog.Builder createAlertDialog(int titleResID, int messageResID,
JSTTSCommandPlayerImpl.IntentStarter intentStarter, final Activity ctx) {
AlertDialog.Builder builder = new AlertDialog.Builder(ctx);
builder.setCancelable(true);
builder.setNegativeButton(R.string.shared_string_no, null);
builder.setPositiveButton(R.string.shared_string_yes, intentStarter);
builder.setTitle(titleResID);
builder.setMessage(messageResID);
return builder;
}
}