From ecb078c20592ae0c5245fa17716ed69a057d8457 Mon Sep 17 00:00:00 2001 From: Peter Osterlund Date: Mon, 26 Dec 2016 00:41:06 +0100 Subject: [PATCH] DroidFish: German text to speech support. --- DroidFish/res/values/strings.xml | 6 +- .../src/org/petero/droidfish/DroidFish.java | 4 +- .../src/org/petero/droidfish/Speech.java | 228 ++++++++++++++---- .../droidfish/gamelogic/SpeechTest.java | 129 +++++++--- 4 files changed, 280 insertions(+), 87 deletions(-) diff --git a/DroidFish/res/values/strings.xml b/DroidFish/res/values/strings.xml index e7ef65f..c5eb2bc 100644 --- a/DroidFish/res/values/strings.xml +++ b/DroidFish/res/values/strings.xml @@ -704,10 +704,12 @@ you are not actively using the program.\ off sound speech_en - + speech_de + Off Play sound English Speech - + German Speech + diff --git a/DroidFish/src/org/petero/droidfish/DroidFish.java b/DroidFish/src/org/petero/droidfish/DroidFish.java index 9892a59..4627fa2 100644 --- a/DroidFish/src/org/petero/droidfish/DroidFish.java +++ b/DroidFish/src/org/petero/droidfish/DroidFish.java @@ -3776,7 +3776,7 @@ public class DroidFish extends Activity /** Initialize text to speech if enabled in settings. */ private void initSpeech() { if (moveAnnounceType.startsWith("speech_")) - speech.initialize(this); + speech.initialize(this, moveAnnounceType.substring(7)); } @Override @@ -3793,7 +3793,7 @@ public class DroidFish extends Activity } } } else if (moveAnnounceType.startsWith("speech_")) { - speech.say(pos, move, moveAnnounceType.substring(7)); + speech.say(pos, move); } if (vibrateEnabled) { Vibrator v = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); diff --git a/DroidFish/src/org/petero/droidfish/Speech.java b/DroidFish/src/org/petero/droidfish/Speech.java index 5ec901a..87507ff 100644 --- a/DroidFish/src/org/petero/droidfish/Speech.java +++ b/DroidFish/src/org/petero/droidfish/Speech.java @@ -33,11 +33,33 @@ import android.widget.Toast; /** Handles text to speech translation. */ public class Speech { private TextToSpeech tts; - boolean initialized = false; - boolean supported = false; - String toSpeak = null; + private boolean initialized = false; + private String toSpeak = null; - public void initialize(final Context context) { + public enum Language { + EN, // English + DE, // German + NONE; // Not supported + + public static Language fromString(String langStr) { + if ("en".equals(langStr)) + return EN; + if ("de".equals(langStr)) + return DE; + return NONE; + } + } + private Language lang; + + + /** Initialize the text to speech engine for a given language. */ + public void initialize(final Context context, final String langStr) { + Language newLang = Language.fromString(langStr); + if (newLang != lang) + shutdown(); + final Locale loc = getLocale(newLang); + if (loc == null) + initialized = true; if (initialized) return; tts = new TextToSpeech(context, new OnInitListener() { @@ -46,12 +68,12 @@ public class Speech { initialized = true; int toast = -1; if (status == TextToSpeech.SUCCESS) { - int code = tts.setLanguage(Locale.US); + int code = tts.setLanguage(loc); switch (code) { case TextToSpeech.LANG_AVAILABLE: case TextToSpeech.LANG_COUNTRY_AVAILABLE: case TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE: - supported = true; + lang = Language.fromString(langStr); say(toSpeak); break; case TextToSpeech.LANG_MISSING_DATA: @@ -75,7 +97,7 @@ public class Speech { @SuppressWarnings("deprecation") public void say(String text) { if (initialized) { - if (supported && text != null) + if (lang != Language.NONE && text != null) tts.speak(text, TextToSpeech.QUEUE_FLUSH, null); toSpeak = null; } else { @@ -95,22 +117,22 @@ public class Speech { if (tts != null) { tts.shutdown(); tts = null; + lang = Language.NONE; initialized = false; - supported = false; } } /** Convert move "move" in position "pos" to a sentence and speak it. */ - public void say(Position pos, Move move, String langStr) { - String s = moveToText(pos, move, langStr); + public void say(Position pos, Move move) { + String s = moveToText(pos, move, lang); // System.out.printf("%.3f Speech.say(): %s\n", System.currentTimeMillis() * 1e-3, s); if (!s.isEmpty()) say(s); } /** Convert move "move" in position "pos" to a sentence that can be spoken. */ - public static String moveToText(Position pos, Move move, String langStr) { - if (move == null || !langStr.equals("en")) + public static String moveToText(Position pos, Move move, Language lang) { + if (move == null) return ""; String moveStr = TextIO.moveToString(pos, move, false, false); @@ -120,12 +142,15 @@ public class Speech { boolean check = moveStr.endsWith("+"); boolean checkMate = moveStr.endsWith("#"); boolean castle = false; + boolean enPassant = false; if (piece == Piece.WPAWN && !capture) { int fx = Position.getX(move.from); int tx = Position.getX(move.to); - if (fx != tx) + if (fx != tx) { capture = true; // En passant + enPassant = true; + } } StringBuilder sentence = new StringBuilder(); @@ -134,10 +159,10 @@ public class Speech { int fx = Position.getX(move.from); int tx = Position.getX(move.to); if (fx == 4 && tx == 6) { - sentence.append("Short castle"); + addWord(sentence, castleToString(true, lang)); castle = true; } else if (fx == 4 && (tx == 2)) { - sentence.append("Long castle"); + addWord(sentence, castleToString(false, lang)); castle = true; } } @@ -145,65 +170,178 @@ public class Speech { if (!castle) { boolean pawnMove = piece == Piece.WPAWN; if (!pawnMove) - sentence.append(pieceName(piece)).append(' '); + addWord(sentence, pieceName(piece, lang)); if (capture) { int i = moveStr.indexOf("x"); String from = moveStr.substring(pawnMove ? 0 : 1, i); if (!from.isEmpty()) - sentence.append(getFromWord(from)).append(' '); + addWord(sentence, fromToString(from, lang)); String to = moveStr.substring(i + 1, i + 3); - sentence.append(to.startsWith("e") ? "take " : "takes "); - sentence.append(to).append(' '); + addWord(sentence, captureToString(to, lang)); + addWord(sentence, toToString(to, lang)); + if (enPassant) + addWord(sentence, epToString(lang)); } else { int nSkip = (promotion ? 1 : 0) + ((check | checkMate) ? 1 : 0); int i = moveStr.length() - nSkip; String from = moveStr.substring(pawnMove ? 0 : 1, i - 2); if (!from.isEmpty()) - sentence.append(from).append(' '); + addWord(sentence, fromToString(from, lang)); String to = moveStr.substring(i - 2, i); - sentence.append(to).append(' '); + addWord(sentence, toToString(to, lang)); } if (promotion) - sentence.append(pieceName(move.promoteTo)).append(' '); + addWord(sentence, promToString(move.promoteTo, lang)); } if (checkMate) { - removeLastSpace(sentence); - sentence.append(". Check mate!"); + addWord(sentence, checkMateToString(lang)); } else if (check) { - removeLastSpace(sentence); - sentence.append(". Check!"); + addWord(sentence, checkToString(lang)); } return sentence.toString().trim(); } + /** Return the locale corresponding to a language string, + * or null if language not supported. */ + private static Locale getLocale(Language lang) { + switch (lang) { + case EN: + return Locale.US; + case DE: + return Locale.GERMAN; + case NONE: + return null; + } + throw new IllegalArgumentException(); + } + + /** Add zero or more words to the string builder. + * If anything was added, an extra space is also added at the end. */ + private static void addWord(StringBuilder sb, String words) { + if (!words.isEmpty()) + sb.append(words).append(' '); + } + /** Get the name of a non-pawn piece. Return empty string if no such piece. */ - private static String pieceName(int piece) { + private static String pieceName(int piece, Language lang) { piece = Piece.makeWhite(piece); - switch (piece) { - case Piece.WKING: return "King"; - case Piece.WQUEEN: return "Queen"; - case Piece.WROOK: return "Rook"; - case Piece.WBISHOP: return "Bishop"; - case Piece.WKNIGHT: return "Knight"; - default: return ""; - } + switch (lang) { + case EN: + switch (piece) { + case Piece.WKING: return "King"; + case Piece.WQUEEN: return "Queen"; + case Piece.WROOK: return "Rook"; + case Piece.WBISHOP: return "Bishop"; + case Piece.WKNIGHT: return "Knight"; + default: return ""; + } + case DE: + switch (piece) { + case Piece.WKING: return "König"; + case Piece.WQUEEN: return "Dame"; + case Piece.WROOK: return "Turm"; + case Piece.WBISHOP: return "Läufer"; + case Piece.WKNIGHT: return "Springer"; + default: return ""; + } + case NONE: + return ""; + } + throw new IllegalArgumentException(); } - /** Transform a "from" file or file+rank to a word. */ - private static String getFromWord(String from) { - if ("a".equals(from)) - return "ae"; - return from; + private static String fromToString(String from, Language lang) { + switch (lang) { + case EN: + if ("a".equals(from)) + return "ae"; + return from; + case DE: + return from; + case NONE: + return ""; + } + throw new IllegalArgumentException(); } - /** If the last character in the StringBuilder is a space, remove it. */ - private static void removeLastSpace(StringBuilder sb) { - int len = sb.length(); - if (len > 0 && sb.charAt(len - 1) == ' ') - sb.setLength(len - 1); + private static String toToString(String to, Language lang) { + return to; + } + + private static String captureToString(String to, Language lang) { + switch (lang) { + case EN: + return to.startsWith("e") ? "take" : "takes"; + case DE: + return "schlägt"; + case NONE: + return ""; + } + throw new IllegalArgumentException(); + } + + private static String castleToString(boolean kingSide, Language lang) { + switch (lang) { + case EN: + return kingSide ? "Short castle" : "Long castle"; + case DE: + return kingSide ? "Kleine Rochade" : "Große Rochade"; + case NONE: + return ""; + } + throw new IllegalArgumentException(); + } + + private static String epToString(Language lang) { + switch (lang) { + case EN: + return ""; + case DE: + return "en passant"; + case NONE: + return ""; + } + throw new IllegalArgumentException(); + } + + private static String promToString(int piece, Language lang) { + String pn = pieceName(piece, lang); + switch (lang) { + case EN: + return pn; + case DE: + return "Umwandlung zu " + pn; + case NONE: + return ""; + } + throw new IllegalArgumentException(); + } + + private static String checkToString(Language lang) { + switch (lang) { + case EN: + return "check!"; + case DE: + return "Schach!"; + case NONE: + return ""; + } + throw new IllegalArgumentException(); + } + + private static String checkMateToString(Language lang) { + switch (lang) { + case EN: + return "check mate!"; + case DE: + return "Schach matt!"; + case NONE: + return ""; + } + throw new IllegalArgumentException(); } } diff --git a/DroidFishTest/src/org/petero/droidfish/gamelogic/SpeechTest.java b/DroidFishTest/src/org/petero/droidfish/gamelogic/SpeechTest.java index 24c3c0a..c4303c5 100644 --- a/DroidFishTest/src/org/petero/droidfish/gamelogic/SpeechTest.java +++ b/DroidFishTest/src/org/petero/droidfish/gamelogic/SpeechTest.java @@ -26,130 +26,183 @@ public class SpeechTest extends TestCase { public SpeechTest() { } + private String[] moveToText(Position pos, Move move, String[] langStrs) { + String[] ret = new String[langStrs.length]; + for (int i = 0; i < langStrs.length; i++) { + Speech.Language lang = Speech.Language.fromString(langStrs[i]); + assertTrue(lang != null); + ret[i] = Speech.moveToText(pos, move, lang); + } + return ret; + } + + private static void assertEquals(String[] expected, String[] actual) { + assertEquals(expected.length, actual.length); + for (int i = 0; i < expected.length; i++) + assertEquals(expected[i], actual[i]); + } + public void testEnglish() { - String lang = "en"; + String[] lang = {"en", "de"}; { Game game = new Game(null, new TimeControlData()); Pair res = game.processString("e4"); - assertEquals("e4", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"e4", "e4"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("d5"); - assertEquals("d5", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"d5", "d5"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("exd5"); - assertEquals("e takes d5", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"e takes d5", "e schlägt d5"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("Qxd5"); - assertEquals("Queen takes d5", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"Queen takes d5", "Dame schlägt d5"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("Ne2"); - assertEquals("Knight e2", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"Knight e2", "Springer e2"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("Nf6"); - assertEquals("Knight f6", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"Knight f6", "Springer f6"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("Nbc3"); - assertEquals("Knight b c3", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"Knight b c3", "Springer b c3"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("e5"); - assertEquals("e5", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"e5", "e5"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("b4"); - assertEquals("b4", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"b4", "b4"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("a5"); - assertEquals("a5", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"a5", "a5"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("a3"); - assertEquals("a3", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"a3", "a3"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("axb4"); - assertEquals("ae takes b4", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"ae takes b4", "a schlägt b4"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("axb4"); - assertEquals("ae takes b4", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"ae takes b4", "a schlägt b4"}, + moveToText(game.prevPos(), res.second, lang)); } { Game game = new Game(null, new TimeControlData()); Pair res = game.processString("d4"); - assertEquals("d4", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"d4", "d4"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("e5"); - assertEquals("e5", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"e5", "e5"}, moveToText(game.prevPos(), res.second, lang)); res = game.processString("dxe5"); - assertEquals("d take e5", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"d take e5", "d schlägt e5"}, moveToText(game.prevPos(), res.second, lang)); res = game.processString("f6"); - assertEquals("f6", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"f6", "f6"}, moveToText(game.prevPos(), res.second, lang)); res = game.processString("exf6"); - assertEquals("e takes f6", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"e takes f6", "e schlägt f6"}, moveToText(game.prevPos(), res.second, lang)); res = game.processString("Bb4"); - assertEquals("Bishop b4. Check!", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"Bishop b4 check!", "Läufer b4 Schach!"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("c3"); - assertEquals("c3", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"c3", "c3"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("Ne7"); - assertEquals("Knight e7", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"Knight e7", "Springer e7"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("cxb4"); - assertEquals("c takes b4", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"c takes b4", "c schlägt b4"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("O-O"); - assertEquals("Short castle", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"Short castle", "Kleine Rochade"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("fxg7"); - assertEquals("f takes g7", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"f takes g7", "f schlägt g7"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("h6"); - assertEquals("h6", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"h6", "h6"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("gxf8Q+"); - assertEquals("g takes f8 Queen. Check!", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"g takes f8 Queen check!", "g schlägt f8 Umwandlung zu Dame Schach!"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("Kxf8"); - assertEquals("King takes f8", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"King takes f8", "König schlägt f8"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("b5"); - assertEquals("b5", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"b5", "b5"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("a5"); - assertEquals("a5", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"a5", "a5"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("bxa6"); - assertEquals("b takes a6", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"b takes a6", "b schlägt a6 en passant"}, + moveToText(game.prevPos(), res.second, lang)); } { Game game = new Game(null, new TimeControlData()); Pair res = game.processString("f4"); - assertEquals("f4", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"f4", "f4"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("e5"); - assertEquals("e5", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"e5", "e5"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("g4"); - assertEquals("g4", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"g4", "g4"}, + moveToText(game.prevPos(), res.second, lang)); res = game.processString("Qh4"); - assertEquals("Queen h4. Check mate!", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"Queen h4 check mate!", "Dame h4 Schach matt!"}, + moveToText(game.prevPos(), res.second, lang)); } { Game game = new Game(null, new TimeControlData()); playMoves(game, "d4 d5 Nc3 Nc6 Bf4 Bf5 Qd2 Qd7"); Pair res = game.processString("O-O-O"); - assertEquals("Long castle", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"Long castle", "Große Rochade"}, + moveToText(game.prevPos(), res.second, lang)); playMoves(game, "Nxd4 Nxd5 Qxd5 Qxd4 Qxd4 Nf3 Qxd1 Kxd1"); res = game.processString("O-O-O"); - assertEquals("Long castle. Check!", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"Long castle check!", "Große Rochade Schach!"}, + moveToText(game.prevPos(), res.second, lang)); + playMoves(game, "Kc1"); + res = game.processString("Rd7"); + assertEquals(new String[]{"Rook d7", "Turm d7"}, + moveToText(game.prevPos(), res.second, lang)); } { Game game = new Game(null, new TimeControlData()); playMoves(game, "e4 e5 h3 Bb4 Ne2 Bc3"); Pair res = game.processString("Nexc3"); - assertEquals("Knight e takes c3", Speech.moveToText(game.prevPos(), res.second, lang)); + assertEquals(new String[]{"Knight e takes c3", "Springer e schlägt c3"}, + moveToText(game.prevPos(), res.second, lang)); } }