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_915909063 小时前
iOS 反编译防护工具全景解析 从底层符号到资源层的多维安全体系
android·安全·ios·小程序·uni-app·iphone·webview
Swizard3 小时前
速度与激情:Android Python + CameraX 零拷贝实时推理指南
android·python·ai·移动开发
summerkissyou19874 小时前
Android13-Audio-AudioTrack-播放流程
android·音视频
里纽斯4 小时前
RK平台Watchdog硬件看门狗验证
android·linux·rk3588·watchdog·看门狗·rk平台·wtd
三七吃山漆4 小时前
攻防世界——comment
android·python·web安全·网络安全·ctf
用户413079810615 小时前
终于懂了-ARouter原理初探
android
fatiaozhang95275 小时前
晶晨S905L3B芯片-2+8G-安卓9.0-ATV原生设置(深度精简优化)-通刷-线刷固件包
android·电视盒子·刷机固件·机顶盒刷机·晶晨s905l3b通刷包·e900v22c-s905l3
Railshiqian5 小时前
安卓如何查看settings是被哪个进程更新的
android
键来大师6 小时前
Android15 安装APK时监听且替换安装
android·framework·rk3588·android15