将文本转语音/语音转文本集成到 Android 应用中





4.00/5 (2投票s)
这是一个在 Android 应用中实现文本转语音和语音转文本的示例
引言
Android SDK 的文本转语音引擎是一个非常实用的工具,可以集成到您的 Android 应用中实现语音功能。在本文中,我们将探讨如何使用 TTS 引擎将文本转换为语音,以及将语音转换为文本。在此过程中,我们还将了解 TTS 如何在具有语音功能的记事本应用中得到实际应用。我将该应用命名为 TalkingNotePad
。这款应用具有标准的记事本功能,如打开和保存文本文件,以及额外的功能,如录音、朗读文件内容和通过语音命令执行操作。此外,我们还将简要介绍如何使用存储访问框架 (SAF) 执行文本文件的输入和输出操作。在此应用中,可以通过按钮或语音命令来执行操作。
通过直接点击按钮或使用语音命令可用的选项如下:
- 打开 - 打开文件
- 保存 - 保存文件
- 朗读 - 朗读文本
- 录音 - 录制语音
- 语音命令 - 使用语音执行命令
- 清空文本 - 清空文本
- 帮助 - 显示帮助屏幕
- 关于 - 显示关于屏幕
背景
要在任何应用中将文本转换为语音,需要 `TextToSpeech` 类的一个实例和 `TextToSpeech.OnInitListener` 接口。`TextToSpeech.OnInitListener` 包含 `onInit()` 方法,该方法在 `TextToSpeech` 引擎初始化完成后被调用。`onInit()` 方法有一个整数参数,表示 `TextToSpeech` 引擎初始化的状态。一旦 `TextToSpeech` 引擎初始化完成,我们就可以调用 `TextToSpeech` 类的 `speak()` 方法来播放文本作为语音。`speak()` 方法的第一个参数是要朗读的文本,第二个参数是队列模式。队列模式参数可以是 `QUEUE_ADD`,用于将新条目添加到播放队列末尾;也可以是 `QUEUE_FLUSH`,用于用新条目覆盖播放队列中的条目。
要将语音转换为文本,我们可以使用 `RecognizerIntent` 类,配合 `ACTION_RECOGNIZE_SPEECH` 操作和 `startActivityForResult()` 方法,并在 `onActivityResult()` 方法中处理结果。
`ACTION_RECOGNIZE_SPEECH` 操作会启动一个活动,提示用户进行语音输入,并将其通过语音识别器发送,如下所示:
识别结果存储在一个名为 `EXTRA_RESULTS` 的 `ArrayList` 中。
要打开或创建文件,我们可以使用存储访问框架。存储访问框架包含以下元素:
- 文档提供程序 (Document Provider),它允许访问存储设备中的文件。
- 客户端应用 (Client App),它调用 `ACTION_OPEN_DOCUMENT` 或 `ACTION_CREATE_DOCUMENT` Intent 来处理文档提供程序返回的文件。
- 选择器 (Picker),它提供用户界面,用于从满足客户端应用搜索条件的文档提供程序中访问文件。
在 SAF 中,我们可以分别使用 `ACTION_OPEN_DOCUMENT` 和 `ACTION_CREATE_DOCUMENT` Intent 来打开和创建文件。打开和创建文件的实际任务可以在 `onActivityResult()` 方法中实现。
打开和保存屏幕如下所示:
Using the Code
以下布局创建了记事本应用的界面:
<?xml version="1.0" encoding="utf-8"?>
<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">
<ScrollView
android:layout_width="600px"
android:layout_height="600px"
android:scrollbars="vertical"
android:background="@drawable/shape">
<EditText
android:id="@+id/txtFileContents"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</ScrollView>
<TableLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TableRow>
<Button
android:id="@+id/btnOpen"
android:text="Open"
android:drawableLeft="@drawable/open"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btnSave"
android:text="Save"
android:drawableLeft="@drawable/save"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</TableRow>
<TableRow>
<Button
android:id="@+id/btnSpeak"
android:text="Speak"
android:drawableLeft="@drawable/speak"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btnRecord"
android:text="Record"
android:drawableLeft="@drawable/record"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
</TableRow>
<TableRow>
<Button
android:id="@+id/btnVoiceCommand"
android:text="Voice Command"
android:drawableLeft="@drawable/command"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btnClear"
android:text="Clear Text"
android:drawableLeft="@drawable/clear"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
</TableRow>
<TableRow>
<Button
android:id="@+id/btnHelp"
android:text="Help"
android:drawableLeft="@drawable/help"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btnAbout"
android:text="About"
android:drawableLeft="@drawable/about"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</TableRow>
</TableLayout>
</LinearLayout>
EditText
的背景由 drawable 文件夹中的以下标记创建:
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"
android:width="300px"
android:height="600px">
<corners android:radius="50px" />
<solid android:color="#FFFF00" />
<stroke android:width="2px"
android:color="#FFFF00" />
</shape>
以下函数触发 `ACTION_OPEN_DOCUMENT` Intent:
public void open()
{
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
startActivityForResult(intent,OPEN_FILE);
}
上面的代码触发了 `onActivityResult()` 方法中以下代码的执行,该代码使用流类打开选定的文件,并将其内容显示在 `EditText` 控件上。
if (resultCode == RESULT_OK)
{
try
{
Uri uri = data.getData();
String filename=uri.toString().substring
(uri.toString().indexOf("%")).replace
("%2F","/").replace("%3A","/storage/emulated/0/");
//Here I have retrieved the filename by replacing characters in the uri.
//It works on my device. Not sure about other devices.
FileInputStream stream=new FileInputStream(new File(filename));
InputStreamReader reader=new InputStreamReader(stream);
BufferedReader br=new BufferedReader(reader);
StringBuffer buffer=new StringBuffer();
String s=br.readLine();
while(s!=null)
{
buffer.append(s+"\n");
s=br.readLine();
}
txtFileContents.setText(buffer.toString().trim());
br.close();
reader.close();
stream.close();
}
catch(Exception ex)
{
AlertDialog.Builder builder=new AlertDialog.Builder(this);
builder.setCancelable(true);
builder.setTitle("Error");
builder.setMessage(ex.getMessage());
builder.setIcon(R.drawable.error);
AlertDialog dialog=builder.create();
dialog.show();
}
}
同样,以下函数触发 `ACTION_CREATE_DOCUMENT` Intent:
public void save()
{
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TITLE,"newfile.txt");
startActivityForResult(intent,SAVE_FILE);
}
这将导致执行以下代码,将 `EditText` 控件的内容保存到文件中:
if(resultCode==RESULT_OK)
{
try
{
Uri uri = data.getData();
String filename=uri.toString().substring
(uri.toString().indexOf("%")).replace
("%2F","/").replace("%3A","/storage/emulated/0/");
FileOutputStream stream=new FileOutputStream(new File(filename));
OutputStreamWriter writer=new OutputStreamWriter(stream);
BufferedWriter bw=new BufferedWriter(writer);
bw.write(txtFileContents.getText().toString(),0,
txtFileContents.getText().toString().length());
bw.close();
writer.close();
stream.close();
}
catch(Exception ex)
{
AlertDialog.Builder builder=new AlertDialog.Builder(this);
builder.setCancelable(true);
builder.setTitle("Error");
builder.setMessage(ex.getMessage());
builder.setIcon(R.drawable.error);
AlertDialog dialog=builder.create();
dialog.show();
}
}
为了朗读 `EditText` 控件的内容,使用了以下用户定义函数:
public void speak()
{
if(txtFileContents.getText().toString().trim().length()==0)
{
AlertDialog.Builder builder=new AlertDialog.Builder(this);
builder.setCancelable(true);
builder.setTitle("Error");
builder.setMessage("Nothing to speak. Please type or record some text.");
builder.setIcon(R.drawable.error);
AlertDialog dialog=builder.create();
dialog.show();
}
else
{
tts=new TextToSpeech(getApplicationContext(),new TextToSpeech.OnInitListener()
{
public void onInit(int status)
{
if(status!=TextToSpeech.ERROR)
{
tts.setLanguage(Locale.US);
String str=txtFileContents.getText().toString();
tts.speak(str,TextToSpeech.QUEUE_ADD,null);
}
}
});
}
}
上面的代码初始化了 `TextToSpeech` 引擎,并将语言设置为 `Locale.US`。然后,它将 `EditText` 控件的内容检索到一个 `string` 变量中,最后调用 `speak()` 函数将文本转换为语音。
以下代码用于使用 `ACTION_RECOGNIZE_SPEECH` Intent 录制语音:
public void record()
{
Intent intent=new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE,Locale.getDefault());
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
if(voiceCommandMode && !recording)
{
intent.putExtra(RecognizerIntent.EXTRA_PROMPT,"Speak a command to be executed...");
}
else
{
intent.putExtra(RecognizerIntent.EXTRA_PROMPT,"Say something to record...");
}
startActivityForResult(intent,RECORD_VOICE);
}
上面的代码会检查我们是在执行语音命令还是在录制正常语音,并显示不同的提示。然后,它会触发 `onActivityResult()` 函数中以下代码的执行:
if(resultCode==RESULT_OK)
{
ArrayList result=data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
if(voiceCommandMode)
{
String command=result.get(0).toUpperCase();
if(command.equals("OPEN")||command.startsWith("OP")||command.startsWith("OB"))
{
Toast.makeText(getBaseContext(),"Executing Open Command",Toast.LENGTH_SHORT).show();
open();
}
else if(command.equals("SAVE")||command.startsWith("SA")||command.startsWith("SE"))
{
Toast.makeText(getBaseContext(),"Executing Save Command",Toast.LENGTH_SHORT).show();
save();
}
else if(command.equals("SPEAK")||command.startsWith("SPA")||
command.startsWith("SPE")||command.startsWith("SPI"))
{
Toast.makeText(getBaseContext(),"Executing Speak Command",Toast.LENGTH_SHORT).show();
speak();
}
else if(command.equals("RECORD")||command.startsWith("REC")||command.startsWith("RAC")||
command.startsWith("RAK")||command.startsWith("REK"))
{
Toast.makeText(getBaseContext(),"Executing Record Command",Toast.LENGTH_SHORT).show();
recording=true;
record();
}
else if(command.equals("CLEAR")||command.equals("KLEAR")||
command.startsWith("CLA")||command.startsWith("CLE")||
command.startsWith("CLI")||command.startsWith("KLA")||
command.startsWith("KLE")||command.startsWith("KLI"))
{
Toast.makeText(getBaseContext(),"Executing Clear Command",Toast.LENGTH_SHORT).show();
clear();
}
else if(command.equals("HELP")||command.startsWith("HAL")||
command.startsWith("HEL")||command.startsWith("HIL")||command.startsWith("HUL"))
{
Toast.makeText(getBaseContext(),"Executing Help Command",Toast.LENGTH_SHORT).show();
help();
}
else if(command.equals("ABOUT")||command.startsWith("ABA")||command.startsWith("ABO"))
{
Toast.makeText(getBaseContext(),"Executing About Command",Toast.LENGTH_SHORT).show();
about();
}
else
{
Toast.makeText(getBaseContext(),"Unrecognized command",Toast.LENGTH_SHORT).show();
}
voiceCommandMode=false;
}
else
{
txtFileContents.setText(result.get(0));
}
}
}
上面的代码会执行语音命令之一(如果点击了“语音命令”按钮)。否则,它只会将语音文本显示在 `EditText` 控件上。该代码使用 `EXTRA_RESULTS` 参数调用 `getStringArrayListExtra()` 方法来获取结果 `ArrayList`。然后,它使用 `get()` 方法提取作为第一个元素的语音文本。
注意:为了避免语音命令无法识别的问题,我将语音与听起来相似的词语进行了比较。我不确定这是最好的方法,但它似乎是一个快速的解决方案。
也可以通过点击按钮来执行命令。`onClick()` 方法中的以下代码根据点击的按钮发起操作:
voiceCommandMode=false;
recording=false;
Button b=(Button)v;
if(b.getId()==R.id.btnOpen)
{
open();
}
if(b.getId()==R.id.btnSave)
{
save();
}
if(b.getId()==R.id.btnSpeak)
{
speak();
}
if(b.getId()==R.id.btnRecord)
{
record();
}
if(b.getId()==R.id.btnVoiceCommand)
{
voiceCommandMode=true;
record();
}
if(b.getId()==R.id.btnClear)
{
clear();
}
if(b.getId()==R.id.btnHelp)
{
help();
}
if(b.getId()==R.id.btnAbout)
{
about();
}
为了读写外部存储,需要将以下权限添加到 androidmanifest.xml 文件中:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
关注点
这是使用 TextToSpeech
API 的一个基于语音的 Android 应用示例。使用 TextToSpeech
API 可以创建更多此类激动人心的应用。