Added full english JS tts support (needs performance optimization)

This commit is contained in:
PaulStets 2018-08-02 16:35:19 +03:00
parent 266aa817ec
commit 55232223a3
7 changed files with 515 additions and 51 deletions

View file

@ -2933,4 +2933,5 @@
<string name="how_to_open_wiki_title">How to open Wikipedia articles?</string>
<string name="use_js_voice_guidance">Use JS voice guidance</string>
<string name="use_js_voice_guidance_description">Use new voice guidance logic based on JavaScript</string>
<string name="test_voice_desrc">Tap a button and listen to the corresponding voice prompt to identify missing or faulty propmts.</string>
</resources>

View file

@ -579,7 +579,7 @@ public class AppInitializer implements IProgress {
throw new CommandPlayerException(ctx.getString(R.string.voice_data_unavailable));
}
if (app.getSettings().USE_JS_VOICE_GUIDANCE.get()) {
return new JSTTSCommandPlayerImpl(osmandApplication, applicationMode, osmandApplication.getRoutingHelper().getVoiceRouter(), voiceProvider);
return new JSTTSCommandPlayerImpl(ctx, applicationMode, osmandApplication.getRoutingHelper().getVoiceRouter(), voiceProvider);
}
if (MediaCommandPlayerImpl.isMyData(voiceDir)) {
return new MediaCommandPlayerImpl(osmandApplication, applicationMode, osmandApplication.getRoutingHelper().getVoiceRouter(), voiceProvider);

View file

@ -22,6 +22,8 @@ import net.osmand.plus.OsmandApplication;
import net.osmand.plus.R;
import net.osmand.plus.activities.OsmandActionBarActivity;
import net.osmand.plus.voice.AbstractPrologCommandPlayer;
import net.osmand.plus.voice.JSCommandBuilder;
import net.osmand.plus.voice.JSTTSCommandPlayerImpl;
import net.osmand.plus.voice.TTSCommandPlayerImpl;
import net.osmand.plus.voice.CommandBuilder;
import net.osmand.plus.voice.CommandPlayer;
@ -29,7 +31,9 @@ import net.osmand.plus.routing.VoiceRouter;
import net.osmand.util.Algorithms;
import java.io.File;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import alice.tuprolog.Struct;
@ -62,7 +66,7 @@ public class TestVoiceActivity extends OsmandActionBarActivity {
gl.setPadding(3, 3, 3, 3);
TextView tv = new TextView(this);
tv.setText("Tap a button and listen to the corresponding voice prompt to identify missing or faulty propmts.");
tv.setText(R.string.test_voice_desrc);
tv.setPadding(0, 5, 0, 7);
ScrollView sv = new ScrollView(this);
@ -75,7 +79,7 @@ public class TestVoiceActivity extends OsmandActionBarActivity {
// add buttons
setContentView(gl);
getSupportActionBar(). setTitle(R.string.test_voice_prompts);
getSupportActionBar().setTitle(R.string.test_voice_prompts);
selectVoice(ll);
}
@ -216,6 +220,10 @@ public class TestVoiceActivity extends OsmandActionBarActivity {
}
private void addButtons(final LinearLayout ll, CommandPlayer p) {
if (p instanceof JSTTSCommandPlayerImpl) {
addJSTTSPrompts(ll, (JSTTSCommandPlayerImpl) p);
return;
}
addButton(ll, "Route calculated and number tests:", builder(p));
addButton(ll, "\u25BA (1.1) New route calculated, 150m, 230sec (00:03:50)", builder(p).newRouteCalculated(150, 230));
addButton(ll, "\u25BA (1.2) New route calculated, 1350m, 3680sec (01:01:20)", builder(p).newRouteCalculated(1350, 3680));
@ -292,10 +300,108 @@ public class TestVoiceActivity extends OsmandActionBarActivity {
ll.forceLayout();
}
private Map<String, String> jsStreet(CommandPlayer p, String... args) {
Map<String, String> res = new HashMap<>();
if (!p.supportsStructuredStreetNames()) {
return res;
}
String[] streetNames = new String[]{"toRef", "toStreetName", "toDest", "fromRef", "fromStreetName", "fromDest"};
for (int i = 0; i < args.length; i++) {
res.put(streetNames[i], args[i]);
}
for (String streetName : streetNames) {
if (res.get(streetName) == null) {
res.put(streetName, "");
}
}
return res;
}
private void addJSTTSPrompts(LinearLayout ll, JSTTSCommandPlayerImpl p) {
addButton(ll, "Route calculated and number tests:", jsBuilder(p));
addButton(ll, "\u25BA (1.1) New route calculated, 150m, 230sec (00:03:50)", jsBuilder(p).newRouteCalculated(150, 230));
addButton(ll, "\u25BA (1.2) New route calculated, 1350m, 3680sec (01:01:20)", jsBuilder(p).newRouteCalculated(1350, 3680));
addButton(ll, "\u25BA (1.3) New route calculated 3700m, 7320sec (02:02)", jsBuilder(p).newRouteCalculated(3700, 7320));
addButton(ll, "\u25BA (1.4) New route calculated 9100m, 10980sec (03:03)", jsBuilder(p).newRouteCalculated(9100, 10980));
addButton(ll, "\u25BA (2.1) Route recalculated 11500m, 18600sec (05:10)", jsBuilder(p).routeRecalculated(11500, 18600));
addButton(ll, "\u25BA (2.2) Route recalculated 19633m, 26700sec (07:25)", jsBuilder(p).routeRecalculated(19633, 26700));
addButton(ll, "\u25BA (2.3) Route recalculated 89750m, 55800sec (15:30)", jsBuilder(p).routeRecalculated(89750, 55800));
addButton(ll, "\u25BA (2.4) Route recalculated 125900m, 92700sec (25:45)", jsBuilder(p).routeRecalculated(125900, 92700));
addButton(ll, "All turn types: prepareTurn, makeTurnIn, turn:", jsBuilder(p));
addButton(ll, "\u25BA (3.1) After 1520m turn slightly left", jsBuilder(p).prepareTurn(AbstractPrologCommandPlayer.A_LEFT_SL, 1520, jsStreet(p, "")));
addButton(ll, "\u25BA (3.2) In 450m turn sharply left onto 'Hauptstra"+"\u00df"+"e', then bear right", jsBuilder(p).turn(AbstractPrologCommandPlayer.A_LEFT_SH, 450, jsStreet(p, "Hauptstraße")).then().bearRight(jsStreet(p, "")));
addButton(ll, "\u25BA (3.3) Turn left, then in 100m turn slightly right", jsBuilder(p).turn(AbstractPrologCommandPlayer.A_LEFT, jsStreet(p, "")).then().turn(AbstractPrologCommandPlayer.A_RIGHT_SL, 100, jsStreet(p, "")));
addButton(ll, "\u25BA (3.4) After 3100m turn right onto 'SR 80' toward 'Rome'", jsBuilder(p).prepareTurn(AbstractPrologCommandPlayer.A_RIGHT, 3100, jsStreet(p, "", "SR 80", "Rome")));
addButton(ll, "\u25BA (3.5) In 370m turn slightly right onto 'Route 23' 'Main jsStreet', then bear left", jsBuilder(p).turn(AbstractPrologCommandPlayer.A_RIGHT_SL, 370, jsStreet(p, "Main jsStreet", "Route 23")).then().bearLeft(jsStreet(p, "")));
addButton(ll, "\u25BA (3.6) Turn sharply right onto 'Dr.-Quinn-Stra"+"\u00df"+"e'", jsBuilder(p).turn(AbstractPrologCommandPlayer.A_RIGHT_SH, jsStreet(p, "Dr.-Quinn-Straße")));
addButton(ll, "Keep left/right: prepareTurn, makeTurnIn, turn:", jsBuilder(p));
addButton(ll, "\u25BA (4.1) After 1810m keep left ' '", jsBuilder(p).prepareTurn(AbstractPrologCommandPlayer.A_LEFT_KEEP, 1810, jsStreet(p, "")));
addButton(ll, "\u25BA (4.2) In 400m keep left ' ' then in 80m keep right onto 'A1'", jsBuilder(p).turn(AbstractPrologCommandPlayer.A_LEFT_KEEP, 400, jsStreet(p, "")).then().turn(AbstractPrologCommandPlayer.A_RIGHT_KEEP, 80, jsStreet(p,"", "A1")));
addButton(ll, "\u25BA (4.3) Keep right on 'Highway 60'", jsBuilder(p).turn(AbstractPrologCommandPlayer.A_RIGHT_KEEP, jsStreet(p, "Highway 60", "", "", "Highway 60")));
addButton(ll, "\u25BA (4.4) Turn left onto 'Broadway', then in 100m keep right and arrive at your destination 'Town Hall'",
jsBuilder(p).turn(AbstractPrologCommandPlayer.A_LEFT, jsStreet(p, "Broadway"))
.then().turn(AbstractPrologCommandPlayer.A_RIGHT_KEEP, 100, jsStreet(p, "")).andArriveAtDestination("Town Hall"));
addButton(ll, "Roundabouts: prepareTurn, makeTurnIn, turn:", jsBuilder(p));
addButton(ll, "\u25BA (5.1) After 1250m enter a roundabout", jsBuilder(p).prepareRoundAbout(1250, 3, jsStreet(p,"", "I 15", "Los Angeles")));
addButton(ll, "\u25BA (5.2) In 450m enter the roundabout and take the 1st exit onto 'I 15' toward 'Los Angeles'", jsBuilder(p).roundAbout(450, 0, 1, jsStreet(p,"", "I 15", "Los Angeles")));
addButton(ll, "\u25BA (5.3) Roundabout: Take the 2nd exit onto 'Highway 60'", jsBuilder(p).roundAbout(0, 2, jsStreet(p, "Highway 60")));
addButton(ll, "U-turns: prepareTurn, makeTurnIn, turn, when possible:", jsBuilder(p));
addButton(ll, "\u25BA (6.1) After 640m make a U-turn", jsBuilder(p).prepareMakeUT(640, jsStreet(p, "")));
addButton(ll, "\u25BA (6.2) In 400m make a U-turn", jsBuilder(p).makeUT(400, jsStreet(p, "")));
addButton(ll, "\u25BA (6.3) Make a U-turn on 'Riviera'", jsBuilder(p).makeUT(jsStreet(p, "Riviera", "", "", "Riviera")));
addButton(ll, "\u25BA (6.4) When possible, make a U-turn", jsBuilder(p).makeUTwp());
addButton(ll, "Go straight, follow the road, approaching:", jsBuilder(p));
addButton(ll, "\u25BA (7.1) Straight ahead", jsBuilder(p).goAhead());
addButton(ll, "\u25BA (7.2) Continue for 2350m to ' '", jsBuilder(p).goAhead(2350, jsStreet(p, "")));
addButton(ll, "\u25BA (7.3) Continue for 360m to 'Broadway' and arrive at your intermediate destination ' '", jsBuilder(p).goAhead(360, jsStreet(p,"Broadway")).andArriveAtIntermediatePoint(""));
addButton(ll, "\u25BA (7.4) Continue for 800m to 'Dr Martin Luther King Jr Boulevard' and arrive at your destination ' '", jsBuilder(p).goAhead(800, jsStreet(p,"", "Dr Martin Luther King Jr Boulevard")).andArriveAtDestination(""));
addButton(ll, "\u25BA (7.5) Continue for 200m and pass GPX waypoint 'Trailhead'", jsBuilder(p).goAhead(200, new HashMap<String, String>()).andArriveAtWayPoint("Trailhead"));
addButton(ll, "\u25BA (7.6) Continue for 400m and pass favorite 'Brewery'", jsBuilder(p).goAhead(400, new HashMap<String, String>()).andArriveAtFavorite("Brewery"));
addButton(ll, "\u25BA (7.7) Continue for 600m and pass POI 'Museum'", jsBuilder(p).goAhead(600, new HashMap<String, String>()).andArriveAtPoi("Museum"));
addButton(ll, "Arriving and passing points:", jsBuilder(p));
addButton(ll, "\u25BA (8.1) Arrive at your destination 'Home'", jsBuilder(p).arrivedAtDestination("Home"));
addButton(ll, "\u25BA (8.2) Arrive at your intermediate destination 'Friend'", jsBuilder(p).arrivedAtIntermediatePoint("Friend"));
addButton(ll, "\u25BA (8.3) Passing GPX waypoint 'Trailhead'", jsBuilder(p).arrivedAtWayPoint("Trailhead"));
addButton(ll, "\u25BA (8.4) Passing favorite 'Brewery'", jsBuilder(p).arrivedAtFavorite("Brewery"));
addButton(ll, "\u25BA (8.5) Passing POI 'Museum'", jsBuilder(p).arrivedAtPoi("Museum"));
addButton(ll, "Attention prompts:", jsBuilder(p));
addButton(ll, "\u25BA (9.1) You are exceeding the speed limit '50' (18 m/s)", jsBuilder(p).speedAlarm(50, 18f));
addButton(ll, "\u25BA (9.2) Attention, speed camera", jsBuilder(p).attention("SPEED_CAMERA"));
addButton(ll, "\u25BA (9.3) Attention, border control", jsBuilder(p).attention("BORDER_CONTROL"));
addButton(ll, "\u25BA (9.4) Attention, railroad crossing", jsBuilder(p).attention("RAILWAY"));
addButton(ll, "\u25BA (9.5) Attention, traffic calming", jsBuilder(p).attention("TRAFFIC_CALMING"));
addButton(ll, "\u25BA (9.6) Attention, toll booth", jsBuilder(p).attention("TOLL_BOOTH"));
addButton(ll, "\u25BA (9.7) Attention, stop sign", jsBuilder(p).attention("STOP"));
addButton(ll, "\u25BA (9.8) Attention, pedestrian crosswalk", jsBuilder(p).attention("PEDESTRIAN"));
addButton(ll, "\u25BA (9.9) Attention, tunnel", jsBuilder(p).attention("TUNNEL"));
addButton(ll, "Other prompts:", jsBuilder(p));
addButton(ll, "\u25BA (10.1) GPS signal lost", jsBuilder(p).gpsLocationLost());
addButton(ll, "\u25BA (10.2) GPS signal recovered", jsBuilder(p).gpsLocationRecover());
addButton(ll, "\u25BA (10.3) You have been off the route for 1050m", jsBuilder(p).offRoute(1050));
addButton(ll, "\u25BA (10.4) You are back on the route", jsBuilder(p).backOnRoute());
addButton(ll, "Voice system info:", jsBuilder(p));
addButton(ll, "\u25BA (11.1) (Tap to refresh)\n" + getVoiceSystemInfo(), jsBuilder(p).attention(""));
addButton(ll, "\u25BA (11.2) Tap to change Phone call audio delay (if car stereo cuts off prompts). Default is 1500\u00A0ms.", jsBuilder(p).attention(""));
ll.forceLayout();
}
private CommandBuilder builder(CommandPlayer p){
return p.newCommandBuilder();
}
private JSCommandBuilder jsBuilder(JSTTSCommandPlayerImpl p) {
return p.newCommandBuilder();
}
public void addButton(ViewGroup layout, final String description, final CommandBuilder builder){
final Button button = new Button(this);
button.setGravity(Gravity.LEFT);

View file

@ -28,42 +28,46 @@ public class JSVoiceRouter extends VoiceRouter {
// Issue 2377: Play Dest here only if not already previously announced, to avoid repetition
if (includeDest == true) {
result.put("toRef", getSpeakablePointName(i.getRef()));
result.put("toStreetName", getSpeakablePointName(i.getStreetName()));
result.put("toDest", getSpeakablePointName(i.getDestinationName()));
result.put("toRef", getNonNullString(getSpeakablePointName(i.getRef())));
result.put("toStreetName", getNonNullString(getSpeakablePointName(i.getStreetName())));
result.put("toDest", getNonNullString(getSpeakablePointName(i.getDestinationName())));
} else {
result.put("toRef", getSpeakablePointName(i.getRef()));
result.put("toStreetName", getSpeakablePointName(i.getStreetName()));
result.put("toRef", getNonNullString(getSpeakablePointName(i.getRef())));
result.put("toStreetName", getNonNullString(getSpeakablePointName(i.getStreetName())));
result.put("toDest", "");
}
if (currentSegment != null) {
// Issue 2377: Play Dest here only if not already previously announced, to avoid repetition
if (includeDest == true) {
RouteDataObject obj = currentSegment.getObject();
result.put("fromRef", getSpeakablePointName(obj.getRef(settings.MAP_PREFERRED_LOCALE.get(),
settings.MAP_TRANSLITERATE_NAMES.get(), currentSegment.isForwardDirection())));
result.put("fromStreetName", getSpeakablePointName(obj.getName(settings.MAP_PREFERRED_LOCALE.get(),
settings.MAP_TRANSLITERATE_NAMES.get())));
result.put("fromDest", getSpeakablePointName(obj.getDestinationName(settings.MAP_PREFERRED_LOCALE.get(),
settings.MAP_TRANSLITERATE_NAMES.get(), currentSegment.isForwardDirection())));
result.put("fromRef", getNonNullString(getSpeakablePointName(obj.getRef(settings.MAP_PREFERRED_LOCALE.get(),
settings.MAP_TRANSLITERATE_NAMES.get(), currentSegment.isForwardDirection()))));
result.put("fromStreetName", getNonNullString(getSpeakablePointName(obj.getName(settings.MAP_PREFERRED_LOCALE.get(),
settings.MAP_TRANSLITERATE_NAMES.get()))));
result.put("fromDest", getNonNullString(getSpeakablePointName(obj.getDestinationName(settings.MAP_PREFERRED_LOCALE.get(),
settings.MAP_TRANSLITERATE_NAMES.get(), currentSegment.isForwardDirection()))));
} else {
RouteDataObject obj = currentSegment.getObject();
result.put("fromRef", getSpeakablePointName(obj.getRef(settings.MAP_PREFERRED_LOCALE.get(),
settings.MAP_TRANSLITERATE_NAMES.get(), currentSegment.isForwardDirection())));
result.put("fromStreetName", getSpeakablePointName(obj.getName(settings.MAP_PREFERRED_LOCALE.get(),
settings.MAP_TRANSLITERATE_NAMES.get())));
result.put("fromRef", getNonNullString(getSpeakablePointName(obj.getRef(settings.MAP_PREFERRED_LOCALE.get(),
settings.MAP_TRANSLITERATE_NAMES.get(), currentSegment.isForwardDirection()))));
result.put("fromStreetName", getNonNullString(getSpeakablePointName(obj.getName(settings.MAP_PREFERRED_LOCALE.get(),
settings.MAP_TRANSLITERATE_NAMES.get()))));
result.put("fromDest", "");
}
}
} else {
result.put("toRef", getSpeakablePointName(i.getRef()));
result.put("toStreetName", getSpeakablePointName(i.getStreetName()));
result.put("toRef", getNonNullString(getSpeakablePointName(i.getRef())));
result.put("toStreetName", getNonNullString(getSpeakablePointName(i.getStreetName())));
result.put("toDest", "");
}
return result;
}
private String getNonNullString(String speakablePointName) {
return speakablePointName == null ? "" : speakablePointName;
}
/**
* Updates status of voice guidance
* @param currentLocation
@ -235,10 +239,10 @@ public class JSVoiceRouter extends VoiceRouter {
}
if (t.getValue() == TurnType.TL || t.getValue() == TurnType.TSHL || t.getValue() == TurnType.TSLL
|| t.getValue() == TurnType.TU || t.getValue() == TurnType.KL ) {
play.then().bearLeft( getSpeakableJSStreetName(currentSegment, next, false));
play.then().bearLeft(getSpeakableJSStreetName(currentSegment, next, false));
} else if (t.getValue() == TurnType.TR || t.getValue() == TurnType.TSHR || t.getValue() == TurnType.TSLR
|| t.getValue() == TurnType.TRU || t.getValue() == TurnType.KR) {
play.then().bearRight( getSpeakableJSStreetName(currentSegment, next, false));
play.then().bearRight(getSpeakableJSStreetName(currentSegment, next, false));
}
}
if (isPlay) {

View file

@ -1,10 +1,50 @@
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("-"));
}
public abstract class AbstractJSCommandPlayer implements CommandPlayer {
@Override
public String getCurrentVoice() {
return null;
@ -24,7 +64,11 @@ public abstract class AbstractJSCommandPlayer implements CommandPlayer {
@Override
public void clear() {
if(ctx != null && ctx.getSettings() != null) {
ctx.getSettings().APPLICATION_MODE.removeListener(this);
}
abandonAudioFocus();
ctx = null;
}
@Override
@ -51,4 +95,70 @@ public abstract class AbstractJSCommandPlayer implements CommandPlayer {
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

@ -10,7 +10,6 @@ import org.mozilla.javascript.Function;
import org.mozilla.javascript.NativeJSON;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.WrapFactory;
import java.io.File;
import java.io.FileInputStream;
@ -23,12 +22,14 @@ import java.util.Map;
public class JSCommandBuilder extends CommandBuilder {
private static final Log log = PlatformUtil.getLog(JSCommandBuilder.class);
private static final String SET_MODE = "setMode";
private static final String SET_METRIC_CONST = "setMetricConst";
private Context jsContext;
private List<String> listStruct = new ArrayList<>();
ScriptableObject jsScope;
private ScriptableObject jsScope;
public JSCommandBuilder(CommandPlayer commandPlayer) {
JSCommandBuilder(CommandPlayer commandPlayer) {
super(commandPlayer);
}
@ -46,16 +47,16 @@ public class JSCommandBuilder extends CommandBuilder {
private String readFileContents(String path) {
FileInputStream fis = null;
StringBuilder fileContent = new StringBuilder("");
try {
fis = new FileInputStream(new File(path));
FileInputStream fis = new FileInputStream(new File(path));
byte[] buffer = new byte[1024];
int n;
while ((n = fis.read(buffer)) != -1)
{
fileContent.append(new String(buffer, 0, n));
}
fis.close();
} catch (IOException e) {
log.error(e.getMessage(), e);
}
@ -63,18 +64,20 @@ public class JSCommandBuilder extends CommandBuilder {
}
public void setParameters(String metricCons, boolean tts) {
// jsContext.property("setMode").toFunction().call(jsContext, tts);
Object obj = jsScope.get("setMode", jsScope);
Object mode = jsScope.get(SET_MODE, jsScope);
Object metrics = jsScope.get(SET_METRIC_CONST, jsScope);
callVoidJsFunction(mode, new Object[]{tts});
callVoidJsFunction(metrics, new Object[]{metricCons});
}
if (obj instanceof Function) {
Function jsFunction = (Function) obj;
jsFunction.call(jsContext, jsScope, jsScope, new Object[]{metricCons});
jsFunction.call(jsContext, jsScope, jsScope, new Object[]{tts});
private void callVoidJsFunction(Object function, Object[] params) {
if (function instanceof Function) {
Function jsFunction = (Function) function;
jsFunction.call(jsContext, jsScope, jsScope, params);
}
}
private JSCommandBuilder addCommand(String name, Object... args){
// TODO add JSCore
Object obj = jsScope.get(name);
if (obj instanceof Function) {
Function jsFunction = (Function) obj;
@ -97,7 +100,7 @@ public class JSCommandBuilder extends CommandBuilder {
}
public JSCommandBuilder makeUT(Map<String, String> streetName){
return addCommand(C_MAKE_UT, convertStreetName(streetName));
return makeUT(-1, streetName);
}
@Override
public JSCommandBuilder speedAlarm(int maxSpeed, float speed){
@ -126,10 +129,10 @@ public class JSCommandBuilder extends CommandBuilder {
public JSCommandBuilder turn(String param, Map<String, String> streetName) {
return addCommand(C_TURN, param, convertStreetName(streetName));
return turn(param, -1, streetName);
}
public JSCommandBuilder turn(String param, double dist, Map<String, String> streetName){
public JSCommandBuilder turn(String param, double dist, Map<String, String> streetName) {
return addCommand(C_TURN, param, dist, convertStreetName(streetName));
}

View file

@ -1,6 +1,8 @@
package net.osmand.plus.voice;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.speech.tts.TextToSpeech;
@ -8,21 +10,65 @@ import android.support.v7.app.AlertDialog;
import android.util.Log;
import net.osmand.IndexConstants;
import net.osmand.PlatformUtil;
import net.osmand.plus.ApplicationMode;
import net.osmand.plus.OsmandApplication;
import net.osmand.plus.R;
import net.osmand.plus.activities.SettingsActivity;
import net.osmand.plus.routing.VoiceRouter;
import net.osmand.util.Algorithms;
import org.mozilla.javascript.ScriptableObject;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
public class JSTTSCommandPlayerImpl extends AbstractJSCommandPlayer {
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 org.mozilla.javascript.Context jsContext;
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;
@ -32,12 +78,142 @@ public class JSTTSCommandPlayerImpl extends AbstractJSCommandPlayer {
private static int ttsRequests = 0;
public JSTTSCommandPlayerImpl(OsmandApplication ctx, ApplicationMode applicationMode, VoiceRouter vrt, String voiceProvider) {
this.app = ctx;
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;
this.voiceProvider = voiceProvider;
mTts = new TextToSpeech(ctx, null);
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());
setJSContext(app.getAppPath(IndexConstants.VOICE_INDEX_DIR).getAbsolutePath() + "/" + voiceProvider + "/" + language + "_tts.js");
}
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 + "____.").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
@ -48,11 +224,36 @@ public class JSTTSCommandPlayerImpl extends AbstractJSCommandPlayer {
@Override
public JSCommandBuilder newCommandBuilder() {
JSCommandBuilder commandBuilder = new JSCommandBuilder(this);
commandBuilder.setJSContext(app.getAppPath(IndexConstants.VOICE_INDEX_DIR).getAbsolutePath() + "/" + voiceProvider + "/en_tts.js");
commandBuilder.setParameters(app.getSettings().METRIC_SYSTEM.get().toHumanString(app), true);
commandBuilder.setJSContext(jsContext, jsScope);
commandBuilder.setParameters(app.getSettings().METRIC_SYSTEM.get().toTTSString(), true);
return commandBuilder;
}
private String readFileContents(String path) {
StringBuilder fileContent = new StringBuilder("");
try {
FileInputStream fis = new FileInputStream(new File(path));
byte[] buffer = new byte[1024];
int n;
while ((n = fis.read(buffer)) != -1)
{
fileContent.append(new String(buffer, 0, n));
}
fis.close();
} catch (IOException e) {
log.error(e.getMessage(), e);
}
return fileContent.toString();
}
private void setJSContext(String path) {
String script = readFileContents(path);
jsContext = org.mozilla.javascript.Context.enter();
jsContext.setOptimizationLevel(-1);
jsScope = jsContext.initStandardObjects();
jsContext.evaluateString(jsScope, script, "JS", 1, null);
}
@Override
public void playCommands(CommandBuilder builder) {
final List<String> execute = builder.execute(); //list of strings, the speech text, play it
@ -60,7 +261,7 @@ public class JSTTSCommandPlayerImpl extends AbstractJSCommandPlayer {
for (String s : execute) {
bld.append(s).append(' ');
}
if (mTts != null && !vrt.isMute()) {
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) {
@ -83,18 +284,37 @@ public class JSTTSCommandPlayerImpl extends AbstractJSCommandPlayer {
}
@Override
public void clear() {
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 "en";
return language;
}
@Override
@ -102,8 +322,28 @@ public class JSTTSCommandPlayerImpl extends AbstractJSCommandPlayer {
return true;
}
@Override
public void stop() {
private void internalClear() {
ttsRequests = 0;
speechAllowed = false;
if (mTts != null) {
mTts.shutdown();
mTts = null;
}
abandonAudioFocus();
mTtsContext = null;
ttsVoiceStatus = "";
ttsVoiceUsed = "";
}
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;
}
}