Android 系统TTS(文字转语音)解析

一、TTS 简介

TTS(Text-to-Speech,文字转语音)是安卓系统内置的语音合成功能,可将文本转换为自然语音输出,广泛应用于语音播报、无障碍服务、语音助手等场景。安卓提供了 TextToSpeech 类来实现TTS功能,支持多语言、语速/音调调节、语音选择等核心能力。

二、核心知识点

1. 权限

安卓TTS基础功能无需额外权限(Android 6.0+),若需保存语音到本地,需添加存储权限:

xml 复制代码
<!-- 可选:保存TTS音频到本地 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" 
    android:maxSdkVersion="28" />

2. 核心类

  • TextToSpeech:TTS核心类,负责初始化、语音合成、参数配置。
  • TextToSpeech.OnInitListener:初始化回调接口,监听TTS引擎初始化结果。
  • Locale:配置语音的语言/地区(如中文、英文、日语)。

3. 关键方法

方法 作用
TextToSpeech(context, listener) 初始化TTS引擎
setLanguage(locale) 设置语音语言/地区
speak(text, queueMode, params, utteranceId) 播放语音
synthesizeToFile(text, params, file, utteranceId) 将语音合成到文件
setPitch(pitch) 设置音调(0.5-2.0,默认1.0)
setSpeechRate(rate) 设置语速(0.1-2.0,默认1.0)
stop() 停止播放
shutdown() 释放TTS资源(必须调用)

三、完整实现步骤

步骤1:添加依赖(无需额外依赖,系统内置)

安卓自带TTS引擎,无需在 build.gradle 中添加额外依赖。

步骤2:布局文件(activity_main.xml)

简单布局,包含输入框、播放按钮、语速/音调调节控件:

xml 复制代码
<?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:padding="16dp"
    android:orientation="vertical"
    android:gravity="center_horizontal">

    <EditText
        android:id="@+id/etText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入要朗读的文本"
        android:maxLines="5"/>

    <Button
        android:id="@+id/btnPlay"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="播放语音"
        android:layout_marginTop="16dp"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="16dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="语速:"
            android:gravity="center_vertical"/>

        <SeekBar
            android:id="@+id/sbRate"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:max="20"
            android:progress="10"/> <!-- 默认语速1.0 -->
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="8dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="音调:"
            android:gravity="center_vertical"/>

        <SeekBar
            android:id="@+id/sbPitch"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:max="20"
            android:progress="10"/> <!-- 默认音调1.0 -->
    </LinearLayout>

    <Button
        android:id="@+id/btnSave"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="保存语音到文件"
        android:layout_marginTop="16dp"/>

</LinearLayout>

步骤3:Activity逻辑实现(MainActivity.kt)

kotlin 复制代码
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
import android.util.Log
import android.widget.Button
import android.widget.EditText
import android.widget.SeekBar
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import java.io.File
import java.util.Locale

class MainActivity : AppCompatActivity(), TextToSpeech.OnInitListener {
    // 日志标签
    private val TAG = "TTS_DEMO"
    
    // TTS核心对象
    private lateinit var tts: TextToSpeech
    
    // 控件
    private lateinit var etText: EditText
    private lateinit var btnPlay: Button
    private lateinit var btnSave: Button
    private lateinit var sbRate: SeekBar
    private lateinit var sbPitch: SeekBar

    // 语音合成文件路径(Android 10+建议使用应用私有目录)
    private val ttsFilePath: String
        get() {
            return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                // Android 10+ 应用私有目录
                filesDir.absolutePath + File.separator + "tts_audio.wav"
            } else {
                // 外部存储(需权限)
                Environment.getExternalStorageDirectory().absolutePath + File.separator + "tts_audio.wav"
            }
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // 初始化控件
        initViews()
        
        // 初始化TTS引擎
        tts = TextToSpeech(this, this)
        
        // 设置进度条监听(语速/音调)
        setSeekBarListeners()
        
        // 设置按钮点击事件
        setButtonClickListeners()
        
        // 设置TTS播放进度监听
        setTTSProgressListener()
    }

    /**
     * 初始化控件
     */
    private fun initViews() {
        etText = findViewById(R.id.etText)
        btnPlay = findViewById(R.id.btnPlay)
        btnSave = findViewById(R.id.btnSave)
        sbRate = findViewById(R.id.sbRate)
        sbPitch = findViewById(R.id.sbPitch)
    }

    /**
     * 设置SeekBar监听(语速/音调)
     */
    private fun setSeekBarListeners() {
        // 语速调节(0.1-2.0)
        sbRate.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
            override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
                val rate = progress / 10.0f // 进度0-20 → 0.0-2.0(修正:0.1-2.0)
                val finalRate = if (rate < 0.1f) 0.1f else rate
                tts.setSpeechRate(finalRate)
                Log.d(TAG, "当前语速:$finalRate")
            }

            override fun onStartTrackingTouch(seekBar: SeekBar?) {}
            override fun onStopTrackingTouch(seekBar: SeekBar?) {}
        })

        // 音调调节(0.5-2.0)
        sbPitch.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
            override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
                val pitch = progress / 10.0f // 进度0-20 → 0.0-2.0(修正:0.5-2.0)
                val finalPitch = if (pitch < 0.5f) 0.5f else pitch
                tts.setPitch(finalPitch)
                Log.d(TAG, "当前音调:$finalPitch")
            }

            override fun onStartTrackingTouch(seekBar: SeekBar?) {}
            override fun onStopTrackingTouch(seekBar: SeekBar?) {}
        })
    }

    /**
     * 设置按钮点击事件
     */
    private fun setButtonClickListeners() {
        // 播放语音
        btnPlay.setOnClickListener {
            val text = etText.text.toString().trim()
            if (text.isEmpty()) {
                Toast.makeText(this, "请输入要朗读的文本", Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }
            playTTS(text)
        }

        // 保存语音到文件
        btnSave.setOnClickListener {
            val text = etText.text.toString().trim()
            if (text.isEmpty()) {
                Toast.makeText(this, "请输入要朗读的文本", Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }
            saveTTSToFile(text)
        }
    }

    /**
     * TTS初始化回调
     */
    override fun onInit(status: Int) {
        if (status == TextToSpeech.SUCCESS) {
            // 设置默认语言(中文)
            val result = tts.setLanguage(Locale.CHINA)
            
            // 检查语言是否支持
            if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
                Log.e(TAG, "中文语言包不支持,请安装TTS语言包")
                Toast.makeText(this, "中文语言包不支持,即将跳转到TTS设置", Toast.LENGTH_LONG).show()
                // 跳转到系统TTS设置页面
                val intent = Intent("com.android.settings.TTS_SETTINGS")
                startActivity(intent)
            } else {
                Log.d(TAG, "TTS初始化成功,语言设置为中文")
                // 设置默认语速和音调(1.0)
                tts.setSpeechRate(1.0f)
                tts.setPitch(1.0f)
                btnPlay.isEnabled = true
                btnSave.isEnabled = true
            }
        } else {
            Log.e(TAG, "TTS初始化失败,状态码:$status")
            Toast.makeText(this, "TTS初始化失败", Toast.LENGTH_SHORT).show()
            btnPlay.isEnabled = false
            btnSave.isEnabled = false
        }
    }

    /**
     * 播放TTS语音
     * @param text 要朗读的文本
     */
    private fun playTTS(text: String) {
        // 停止之前的播放(可选)
        tts.stop()
        
        // 设置播放参数
        val params = HashMap<String, String>()
        params[TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID] = "tts_play_utterance" // 唯一标识
        
        // 播放语音
        // queueMode:QUEUE_FLUSH(替换队列)/ QUEUE_ADD(添加到队列)
        val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            tts.speak(text, TextToSpeech.QUEUE_FLUSH, null, "tts_play_utterance")
        } else {
            @Suppress("DEPRECATION")
            tts.speak(text, TextToSpeech.QUEUE_FLUSH, params)
        }
        
        if (result == TextToSpeech.ERROR) {
            Log.e(TAG, "TTS播放失败")
            Toast.makeText(this, "语音播放失败", Toast.LENGTH_SHORT).show()
        }
    }

    /**
     * 将TTS语音保存到文件
     * @param text 要合成的文本
     */
    private fun saveTTSToFile(text: String) {
        val file = File(ttsFilePath)
        // 删除已存在的文件
        if (file.exists()) {
            file.delete()
        }

        // 设置合成参数
        val params = HashMap<String, String>()
        params[TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID] = "tts_save_utterance"

        // 合成到文件
        val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            tts.synthesizeToFile(text, null, file, "tts_save_utterance")
        } else {
            @Suppress("DEPRECATION")
            tts.synthesizeToFile(text, params, ttsFilePath)
        }

        if (result == TextToSpeech.SUCCESS) {
            Log.d(TAG, "TTS文件保存成功:$ttsFilePath")
            Toast.makeText(this, "语音已保存到:$ttsFilePath", Toast.LENGTH_LONG).show()
        } else {
            Log.e(TAG, "TTS文件保存失败")
            Toast.makeText(this, "语音保存失败", Toast.LENGTH_SHORT).show()
        }
    }

    /**
     * 设置TTS播放进度监听
     */
    private fun setTTSProgressListener() {
        tts.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
            override fun onStart(utteranceId: String?) {
                Log.d(TAG, "TTS开始播放:$utteranceId")
                runOnUiThread {
                    Toast.makeText(this@MainActivity, "开始播放语音", Toast.LENGTH_SHORT).show()
                }
            }

            override fun onDone(utteranceId: String?) {
                Log.d(TAG, "TTS播放完成:$utteranceId")
                runOnUiThread {
                    Toast.makeText(this@MainActivity, "语音播放完成", Toast.LENGTH_SHORT).show()
                }
            }

            override fun onError(utteranceId: String?) {
                Log.e(TAG, "TTS播放错误:$utteranceId")
                runOnUiThread {
                    Toast.makeText(this@MainActivity, "语音播放出错", Toast.LENGTH_SHORT).show()
                }
            }

            // Android 8.0+ 新增:播放暂停
            override fun onPause(utteranceId: String?) {
                super.onPause(utteranceId)
                Log.d(TAG, "TTS暂停播放:$utteranceId")
            }

            // Android 8.0+ 新增:播放继续
            override fun onResume(utteranceId: String?) {
                super.onResume(utteranceId)
                Log.d(TAG, "TTS恢复播放:$utteranceId")
            }

            // Android 11+ 新增:播放取消
            override fun onStop(utteranceId: String?, interrupted: Boolean) {
                super.onStop(utteranceId, interrupted)
                Log.d(TAG, "TTS停止播放:$utteranceId,是否中断:$interrupted")
            }
        })
    }

    /**
     * 释放TTS资源(必须调用,否则会内存泄漏)
     */
    override fun onDestroy() {
        super.onDestroy()
        // 停止播放
        tts.stop()
        // 释放资源
        tts.shutdown()
        Log.d(TAG, "TTS资源已释放")
    }
}

四、关键注意事项

1. 语言包支持

  • 若系统未安装对应语言的TTS包,setLanguage 会返回 LANG_MISSING_DATALANG_NOT_SUPPORTED,需引导用户跳转到系统TTS设置页面安装。
  • 常用Locale:
    • 中文(中国大陆):Locale.CHINA / Locale.SIMPLIFIED_CHINESE
    • 中文(中国台湾):Locale.TAIWAN
    • 英文(美国):Locale.US
    • 英文(英国):Locale.UK

2. 版本兼容

  • Android 5.0(Lollipop)前后,speaksynthesizeToFile 方法参数有差异,需做版本适配。
  • Android 10+ 限制外部存储访问,建议使用应用私有目录(filesDir/cacheDir)保存合成文件。

3. 资源释放

  • 必须在Activity/Fragment销毁时调用 tts.shutdown() 释放TTS资源,否则会导致内存泄漏。
  • 播放过程中可调用 tts.stop() 停止当前语音播放。

4. Utterance ID

  • KEY_PARAM_UTTERANCE_ID 是唯一标识,用于区分不同的语音合成请求,在 UtteranceProgressListener 中可监听对应ID的播放状态。

五、常见问题解决

1. TTS初始化失败(status=-1)

  • 检查系统是否安装TTS引擎(如Google文字转语音、系统自带TTS)。
  • 确认TTS引擎已启用:设置 → 系统 → 语言和输入法 → 文字转语音输出 → 选择默认引擎。

2. 语音播放无声音

  • 检查设备音量(媒体音量)是否开启。
  • 确认语言包已正确安装,且 setLanguage 返回成功。

3. 保存文件失败(Android 10+)

  • 避免使用外部存储根目录,改用应用私有目录(filesDir)。
  • 无需申请 WRITE_EXTERNAL_STORAGE 权限(Android 10+)。

4. 语速/音调调节无效

  • 确保在 onInit 成功后再设置语速/音调,且数值在合法范围(语速0.1-2.0,音调0.5-2.0)。

六、扩展功能

  1. 多语音选择 :通过 tts.voices 获取系统支持的语音列表,选择指定语音:

    kotlin 复制代码
    val voices = tts.voices
    for (voice in voices) {
        if (voice.name.contains("zh-CN")) {
            tts.voice = voice
            break
        }
    }
  2. 批量合成 :将文本分割为多个片段,通过 QUEUE_ADD 加入播放队列。

  3. 暂停/恢复播放 (Android 8.0+):

    kotlin 复制代码
    tts.pause() // 暂停
    tts.resume() // 恢复

总结

安卓TTS是轻量级、易集成的语音合成方案,核心是 TextToSpeech 类的初始化、参数配置和资源管理。开发时需注意版本兼容、语言包支持和资源释放,结合 UtteranceProgressListener 可实现更精细的播放状态监听。以上代码可直接集成到项目中,根据业务需求扩展功能即可。

相关推荐
2501_9462447821 分钟前
Flutter & OpenHarmony OA系统图片预览组件开发指南
android·javascript·flutter
极客小云26 分钟前
【IEEE Transactions系列期刊全览:计算机领域核心期刊深度解析】
android·论文阅读·python
wanghowie40 分钟前
02.01 Spring Boot|自动配置机制深度解析
android·spring boot·后端
一起搞IT吧1 小时前
三方相机问题分析十一:【手电筒回调异常】手电筒打开3档时,达到档位控制温度,手电筒二级界面中档位为0
android·图像处理·数码相机
2501_924064111 小时前
2025年移动应用渗透测试流程方案及iOS安卓测试方法对比
android·ios
千里马学框架1 小时前
安卓14-16车机手机仿小米su7三分屏实战项目专题
android·智能手机·framework·分屏·车载·小米汽车·三分屏
走在路上的菜鸟1 小时前
Android学Flutter学习笔记 第二节 Android视角认知Flutter(resource,生命周期,layout)
android·学习·flutter
zh_xuan2 小时前
kotlin的常见空检查
android·开发语言·kotlin
踏雪羽翼11 小时前
android TextView实现文字字符不同方向显示
android·自定义view·textview方向·文字方向·textview文字显示方向·文字旋转·textview文字旋转
lxysbly11 小时前
安卓玩MRP冒泡游戏:模拟器下载与使用方法
android·游戏