Integrating Text To Speech/Speech To Text In An Android App

Introduction

 
Android SDK text to speech engine is a very useful tool to integrate voice in your Android apps. In this article, we will look at converting text to speech as well as speech to text by using the TTS engine. In the process, we will also see how TTS can be practically used in a Notepad app which has a voice feature. I have named the app TalkingNotePad. This app has the standard Notepad features like opening and saving text files plus additional features like voice recording, speaking the file contents and performing actions using voice commands.
 
Also, we will briefly look at performing text file input and output operations using the Storage Access Framework (SAF). In this app, commands can be executed by using buttons or using voice.
 
The following options are available by directly using buttons or by using Voice Command,
  • Open - To open a file
  • Save - To save a file
  • Speak - To speak text
  • Record - To record voice
  • Voice Command - To execute commands using voice
  • Clear Text - To clear Text
  • Help - To show the Help screen
  • About - To show About screen

Background

 
To convert text to speech in any app, an instance of the TextToSpeech class and the TextToSpeech.OnInitListener interface is required. The TextToSpeech.OnInitListener contains the onInit() method which is called when the TextToSpeech engine initialization is complete. The onInit()method has an integer parameter which represents the status of TextToSpeech engine initialization. Once the TextToSpeech engine initialization is complete, we can call the speak() method of the TextToSpeech class to play the text as speech. The first parameter of the speak() method is the text to be spoken and the second parameter is the queue mode. The queue mode parameter can be QUEUE_ADD to add the new entry at the end of the playback queue or QUEUE_FLUSH to overwrite the entries in the playback queue with the new entry.
 
To convert speech to text, we can use the RecognizerIntent class with the ACTION_RECOGNIZE_SPEECHaction and startActivityForResult() method and handle the result in the onActivityResult() method.
 
The ACTION_RECOGNIZE_SPEECH action starts an activity that prompts the user for speech and sends it through a speech recognizer as follows,
 
Android SDK text to speech engine
 
The results of recognition are stored in an ArrayList called EXTRA_RESULTS.
 
To open or create files, we can use the Storage Access Framework. The Storage Access Framework consists of the following elements,
  • Document Provider, which allows accessing files from a storage device.
  • Client App, which invokes the ACTION_OPEN_DOCUMENT or ACTION_CREATE_DOCUMENT intents to work with files returned by document providers.
  • Picker, which provides the UI to access files from document providers that satisfy the client app's search criteria.
In SAF, we can use the ACTION_OPEN_DOCUMENT and ACTION_CREATE_DOCUMENT intents for opening and creating files respectively. The actual tasks of opening and creating files can be implemented in the onActivityResult() method.
 
The open and save screens are as follows,
 
Android SDK text to speech engine
 
Using the Code
 
The following layout creates the interface for the Notepad app,
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center">  
  3.     <ScrollView android:layout_width="600px" android:layout_height="600px" android:scrollbars="vertical" android:background="@drawable/shape">  
  4.         <EditText android:id="@+id/txtFileContents" android:layout_width="match_parent" android:layout_height="match_parent" />  
  5.     </ScrollView>  
  6.     <TableLayout android:layout_width="wrap_content" android:layout_height="wrap_content">  
  7.         <TableRow>  
  8.             <Button android:id="@+id/btnOpen" android:text="Open" android:drawableLeft="@drawable/open" android:layout_width="wrap_content" android:layout_height="wrap_content" />  
  9.             <Button android:id="@+id/btnSave" android:text="Save" android:drawableLeft="@drawable/save" android:layout_width="wrap_content" android:layout_height="wrap_content" />  
  10.         </TableRow>  
  11.         <TableRow>  
  12.             <Button android:id="@+id/btnSpeak" android:text="Speak" android:drawableLeft="@drawable/speak" android:layout_width="wrap_content" android:layout_height="wrap_content" />  
  13.             <Button android:id="@+id/btnRecord" android:text="Record" android:drawableLeft="@drawable/record" android:layout_width="fill_parent" android:layout_height="wrap_content" />  
  14.         </TableRow>  
  15.         <TableRow>  
  16.             <Button android:id="@+id/btnVoiceCommand" android:text="Voice Command" android:drawableLeft="@drawable/command" android:layout_width="wrap_content" android:layout_height="wrap_content" />  
  17.             <Button android:id="@+id/btnClear" android:text="Clear Text" android:drawableLeft="@drawable/clear" android:layout_width="fill_parent" android:layout_height="wrap_content" />  
  18.         </TableRow>  
  19.         <TableRow>  
  20.             <Button android:id="@+id/btnHelp" android:text="Help" android:drawableLeft="@drawable/help" android:layout_width="wrap_content" android:layout_height="wrap_content" />  
  21.             <Button android:id="@+id/btnAbout" android:text="About" android:drawableLeft="@drawable/about" android:layout_width="wrap_content" android:layout_height="wrap_content" />  
  22.         </TableRow>  
  23.     </TableLayout>  
  24. </LinearLayout>  
The background for the EditText is created by the following markup in the drawable folder,
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" android:width="300px" android:height="600px">  
  3.     <corners android:radius="50px" />  
  4.     <solid android:color="#FFFF00" />  
  5.     <stroke android:width="2px" android:color="#FFFF00" />  
  6. </shape>  
The following function fires the ACTION_OPEN_DOCUMENT intent,
  1. public void open() {  
  2.     Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);  
  3.     intent.addCategory(Intent.CATEGORY_OPENABLE);  
  4.     intent.setType("*/*");  
  5.     startActivityForResult(intent, OPEN_FILE);  
  6. }  
The above code triggers the execution of the following code in the onActivityResult() method which opens the selected file using stream classes and displays its contents on an EditText control,
  1. if (resultCode == RESULT_OK) {  
  2.     try {  
  3.         Uri uri = data.getData();  
  4.         String filename = uri.toString().substring(uri.toString().indexOf("%")).replace("%2F""/").replace("%3A""/storage/emulated/0/");  
  5.         //Here I have retrieved the filename by replacing characters in the uri.  
  6.         //It works on my device. Not sure about other devices.  
  7.         FileInputStream stream = new FileInputStream(new File(filename));  
  8.         InputStreamReader reader = new InputStreamReader(stream);  
  9.         BufferedReader br = new BufferedReader(reader);  
  10.         StringBuffer buffer = new StringBuffer();  
  11.         String s = br.readLine();  
  12.         while (s != null) {  
  13.             buffer.append(s + "\n");  
  14.             s = br.readLine();  
  15.         }  
  16.         txtFileContents.setText(buffer.toString().trim());  
  17.         br.close();  
  18.         reader.close();  
  19.         stream.  
  20.     } catch (Exception ex) {  
  21.         AlertDialog.Builder builder = new AlertDialog.Builder(this);  
  22.         builder.setCancelable(true);  
  23.         builder.setTitle("Error");  
  24.         builder.setMessage(ex.getMessage());  
  25.         builder.setIcon(R.drawable.error);  
  26.         AlertDialog dialog = builder.create();  
  27.         dialog.show();  
  28.     }  
  29. }  
Similarly, the following function fires the ACTION_CREATE_DOCUMENT intent,
  1. public void save() {  
  2.     Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);  
  3.     intent.addCategory(Intent.CATEGORY_OPENABLE);  
  4.     intent.setType("text/plain");  
  5.     intent.putExtra(Intent.EXTRA_TITLE, "newfile.txt");  
  6.     startActivityForResult(intent, SAVE_FILE);  
  7. }  
And this results in the execution of the following code to save the contents of the EditText control to a file,
  1. if (resultCode == RESULT_OK) {  
  2.     try {  
  3.         Uri uri = data.getData();  
  4.         String filename = uri.toString().substring(uri.toString().indexOf("%")).replace("%2F""/").replace("%3A""/storage/emulated/0/");  
  5.         FileOutputStream stream = new FileOutputStream(new File(filename));  
  6.         OutputStreamWriter writer = new OutputStreamWriter(stream);  
  7.         BufferedWriter bw = new BufferedWriter(writer);  
  8.         bw.write(txtFileContents.getText().toString(), 0,  
  9.             txtFileContents.getText().toString().length());  
  10.         bw.close();  
  11.         writer.close();  
  12.         stream.close();  
  13.     } catch (Exception ex) {  
  14.         AlertDialog.Builder builder = new AlertDialog.Builder(this);  
  15.         builder.setCancelable(true);  
  16.         builder.setTitle("Error");  
  17.         builder.setMessage(ex.getMessage());  
  18.         builder.setIcon(R.drawable.error);  
  19.         AlertDialog dialog = builder.create();  
  20.         dialog.show();  
  21.     }  
  22. }  
In order to speak the contents of the EditText control, the following user defined function is used,
  1. public void speak() {  
  2.     if (txtFileContents.getText().toString().trim().length() == 0) {  
  3.         AlertDialog.Builder builder = new AlertDialog.Builder(this);  
  4.         builder.setCancelable(true);  
  5.         builder.setTitle("Error");  
  6.         builder.setMessage("Nothing to speak. Please type or record some text.");  
  7.         builder.setIcon(R.drawable.error);  
  8.         AlertDialog dialog = builder.create();  
  9.         dialog.show();  
  10.     } else {  
  11.         tts = new TextToSpeech(getApplicationContext(), new TextToSpeech.OnInitListener() {  
  12.             public void onInit(int status) {  
  13.                 if (status != TextToSpeech.ERROR) {  
  14.                     tts.setLanguage(Locale.US);  
  15.                     String str = txtFileContents.getText().toString();  
  16.                     tts.speak(str, TextToSpeech.QUEUE_ADD, null);  
  17.                 }  
  18.             }  
  19.         });  
  20.     }  
  21. }  
The above code initializes the TextToSpeech engine and sets the language to Locale.US. Then it retrieves the contents of the EditText control into a string variable and finally calls the speak() function to convert the text to speech.
 
The following code is used to record speech using the ACTION_RECOGNIZE_SPEECH intent,
  1. public void record() {  
  2.     Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);  
  3.     intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.getDefault());  
  4.     intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);  
  5.     if (voiceCommandMode && !recording) {  
  6.         intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "Speak a command to be executed...");  
  7.     } else {  
  8.         intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "Say something to record...");  
  9.     }  
  10.     startActivityForResult(intent, RECORD_VOICE);  
  11. }  
The above code checks whether we are executing a voice command or recording normal speech and displays a different prompt depending upon that. It then triggers the execution of the following code in the onActivityResult() function,
  1. if (resultCode == RESULT_OK) {  
  2.     ArrayList result = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);  
  3.     if (voiceCommandMode) {  
  4.         String command = result.get(0);  
  5.         if (command.toUpperCase().equals("OPEN") || command.toUpperCase().startsWith("OP") ||  
  6.             command.toUpperCase().startsWith("OB")) {  
  7.             Toast.makeText(getBaseContext(), "Executing Open Command", Toast.LENGTH_SHORT).show();  
  8.             open();  
  9.         } else if (command.toUpperCase().equals("SAVE") || command.toUpperCase().startsWith("SA") ||  
  10.             command.toUpperCase().startsWith("SE")) {  
  11.             Toast.makeText(getBaseContext(), "Executing Save Command", Toast.LENGTH_SHORT).show();  
  12.             save();  
  13.         } else if (command.toUpperCase().equals("SPEAK") || command.toUpperCase().startsWith("SPA") ||  
  14.             command.toUpperCase().startsWith("SPE") || command.toUpperCase().startsWith("SPI")) {  
  15.             Toast.makeText(getBaseContext(), "Executing Speak Command", Toast.LENGTH_SHORT).show();  
  16.             speak();  
  17.         } else if (command.toUpperCase().equals("RECORD") || command.toUpperCase().startsWith("REC") ||  
  18.             command.toUpperCase().startsWith("RAC") || command.toUpperCase().startsWith("RAK") ||  
  19.             command.toUpperCase().startsWith("REK")) {  
  20.             Toast.makeText(getBaseContext(), "Executing Record Command", Toast.LENGTH_SHORT).show();  
  21.             recording = true;  
  22.             record();  
  23.         } else if (command.toUpperCase().equals("CLEAR") || command.toUpperCase().equals("KLEAR") ||  
  24.             command.toUpperCase().startsWith("CLA") || command.toUpperCase().startsWith("CLE") ||  
  25.             command.toUpperCase().startsWith("CLI") || command.toUpperCase().startsWith("KLA") ||  
  26.             command.toUpperCase().startsWith("KLE") || command.toUpperCase().startsWith("KLI")) {  
  27.             Toast.makeText(getBaseContext(), "Executing Clear Command", Toast.LENGTH_SHORT).show();  
  28.             clear();  
  29.         } else if (command.toUpperCase().equals("HELP") || command.toUpperCase().startsWith("HAL") ||  
  30.             command.toUpperCase().startsWith("HEL") || command.toUpperCase().startsWith("HIL") ||  
  31.             command.toUpperCase().startsWith("HUL")) {  
  32.             Toast.makeText(getBaseContext(), "Executing Help Command", Toast.LENGTH_SHORT).show();  
  33.             help();  
  34.         } else if (command.toUpperCase().equals("ABOUT") || command.toUpperCase().startsWith("ABA") ||  
  35.             command.toUpperCase().startsWith("ABO")) {  
  36.             Toast.makeText(getBaseContext(), "Executing About Command", Toast.LENGTH_SHORT).show();  
  37.             about();  
  38.         } else {  
  39.             Toast.makeText(getBaseContext(), "Unrecognized command", Toast.LENGTH_SHORT).show();  
  40.         }  
  41.         voiceCommandMode = false;  
  42.     } else {  
  43.         txtFileContents.setText(result.get(0));  
  44.     }  
  45. }  
The above code executes one of the voice commands if we had clicked on the "Voice Command" button. Otherwise, it simply displays the spoken text on the EditText control. The code uses the getStringArrayListExtra() method with the EXTRA_RESULTS parameter to get the result ArrayList. Then it extracts the spoken text as the first element using the get() method.
 
Note
 
To avoid the problems of voice commands not getting recognized, I have compared the speech with similar sounding words. I am not sure if it is the best way out but it seemed to be a quick solution.
 
Commands can also be executed by clicking on buttons. The following code in the onClick() method initiates the actions depending on the button clicked,
  1. voiceCommandMode = false;  
  2. recording = false;  
  3. Button b = (Button) v;  
  4. if (b.getId() == R.id.btnOpen) {  
  5.     open();  
  6. }  
  7. if (b.getId() == R.id.btnSave) {  
  8.     save();  
  9. }  
  10. if (b.getId() == R.id.btnSpeak) {  
  11.     speak();  
  12. }  
  13. if (b.getId() == R.id.btnRecord) {  
  14.     record();  
  15. }  
  16. if (b.getId() == R.id.btnVoiceCommand) {  
  17.     voiceCommandMode = true;  
  18.     record();  
  19. }  
  20. if (b.getId() == R.id.btnClear) {  
  21.     clear();  
  22. }  
  23. if (b.getId() == R.id.btnHelp) {  
  24.     help();  
  25. }  
  26. if (b.getId() == R.id.btnAbout) {  
  27.     about();  
  28. }  
The following permissions need to be added to the androidmanifest.xml file in order to read from and write to external storage,
  1. <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>  
  2. <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>  

Conclusion

 
This is one example of a voice based Android app using TextToSpeech API. Many more such exciting apps can be created using TextToSpeech API.


Similar Articles