65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (2投票s)

2018年8月31日

CPOL

4分钟阅读

viewsIcon

16838

downloadIcon

991

这是一个在 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 可以创建更多此类激动人心的应用。

© . All rights reserved.