news 2026/10/20 3:35:44

Android智能语音开发实战:从语音识别到语音合成的完整链路与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android智能语音开发实战:从语音识别到语音合成的完整链路与避坑指南

1. 从零到一:Android 智能语音开发到底在做什么

很多人第一次听到“Android 智能语音开发”这个词,脑子里浮现的可能是电影里那种跟手机自然对话的场景。但真到自己动手的时候,往往连第一步该干什么都不清楚。我刚开始接触这块的时候也一样,网上搜一圈,全是碎片化的东西,要么只讲语音识别,要么只讲语音合成,没有一个完整的脉络。所以这篇内容我打算把整个链路从头到尾捋一遍,把我在实际项目里踩过的坑、试过的方案、以及那些文档里不会写的细节都摊开来讲。

先说清楚这个领域到底包含什么。Android 上的智能语音开发,核心其实就三件事:语音转文字(识别)、文字转语音(合成)、以及围绕这两者构建的交互逻辑。你手机上那些语音助手、语音输入法、有声读物播放器、语音记账工具,底层都跑不出这个范围。听起来简单,但每一块往下挖都有不少门道。

这篇文章适合谁看?如果你是刚入门的 Android 开发者,想给自己的 App 加一个语音功能,或者你完全没接触过语音开发但想系统了解一下,那这篇内容就是为你准备的。我不会假设你懂信号处理或者深度学习,所有概念我都会用生活化的方式解释。但如果你已经做过语音项目,里面关于参数调优、延迟优化、异常处理的部分应该也能给你一些新的思路。

整个链路我打算按这样的顺序展开:先讲清楚技术选型的逻辑,为什么有人用系统自带的能力、有人用第三方服务、有人自己训练模型;然后拆解核心环节的实现细节,包括权限处理、音频采集、识别回调、合成播放这些;接着给出一套完整的实操流程,你可以直接照着做;最后是我在实际项目中遇到的各种奇葩问题和排查方法。每一块我都会尽量给出可以直接复现的代码和配置。

2. 技术选型:系统能力、第三方服务还是自研模型

2.1 三条路线的真实成本对比

做语音功能,第一个要做的决定就是:用谁的引擎?这个问题没有标准答案,完全取决于你的场景。我把常见的三条路线拉出来对比一下。

对比维度系统自带能力第三方云服务自研/本地模型
接入难度极低,几行代码低,需要注册和配置高,需要模型和推理框架
识别准确率中等,看设备高,持续优化取决于训练数据
离线可用部分支持通常不支持完全支持
成本免费按调用量计费前期投入大,后期边际成本低
隐私性数据在本地数据上传到服务端数据完全本地
定制化几乎没有有限定制完全可控
延迟低取决于网络低(本地推理)

系统自带的能力,比如 Android 原生的 SpeechRecognizer 和 TextToSpeech,最大的优势就是接入快、不用花钱、不用联网也能跑一部分。但问题也很明显:不同厂商的设备上表现差异巨大,有的手机识别率很高,有的就一言难尽。而且你几乎没法控制它的行为,比如你想调整识别灵敏度、想自定义词汇表,基本做不到。

第三方云服务的识别准确率确实高出一截,尤其是对中文方言、专业术语的处理。但代价是你要把用户的语音数据传到别人的服务器上,这在一些对隐私敏感的场景里是硬伤。另外网络抖动的时候延迟会很明显,用户说完一句话等两三秒才出结果,体验很差。

自研或者本地部署模型,听起来很酷,但实际门槛不低。你需要有足够的标注数据、需要选合适的推理框架、需要处理模型体积和手机性能的平衡。不过如果你的场景对隐私要求极高,或者需要在完全离线的环境下工作,那这条路是绕不开的。

2.2 我的选型建议:从场景倒推

我一般会问自己三个问题来决定用哪条路线。

第一个问题:用户能不能接受联网?如果这是一个记事本类的工具,用户可能希望在地铁里、飞机上也能用语音输入,那就必须考虑离线方案。如果是一个客服类的应用,本来就要联网,那用云服务完全没问题。

第二个问题:识别的内容有没有专业术语?比如医疗、法律、工程领域的词汇,通用引擎的识别率会明显下降。这时候要么用支持自定义词库的云服务,要么自己针对领域数据做微调。

第三个问题:预算和团队能力如何?如果只是做个 Demo 或者小工具,系统自带的能力足够了。如果是商业产品,云服务的按量计费在用户量大了之后是一笔不小的开支,这时候就要算一笔账:自研的前期投入多久能摊平。

我个人的经验是,大多数中小型项目用“系统能力打底 + 云服务兜底”的混合方案最划算。具体来说,优先尝试系统自带的识别,如果置信度低于某个阈值,再调用云服务重新识别。这样既保证了离线场景的可用性,又在网络好的时候拿到了更高的准确率。

2.3 那些选型时容易忽略的隐性成本

选型的时候大家容易只看识别率这一个指标,但实际上有几个隐性成本很容易被低估。

第一个是冷启动时间。系统自带的识别器在某些设备上首次调用要等一两秒才能就绪,云服务则要建立网络连接。如果你的 App 是那种用户点一下就要立刻说话的,这个等待时间会非常影响体验。解决办法是在页面加载的时候就预热引擎,而不是等用户点了按钮才开始初始化。

第二个是音频格式的兼容性。不同的引擎对采样率、位深、声道数的要求不一样。系统识别器通常要求 16kHz 单声道 PCM,而云服务可能接受多种格式但推荐特定的编码。如果你在采集端没有处理好,要么识别不出来,要么准确率大打折扣。这个坑我在第一个项目里就踩过,采集用的是 44.1kHz,直接丢给识别器,结果返回的全是乱码。

第三个是并发和资源占用。语音识别和语音合成如果同时运行,会争抢音频通道。Android 的音频系统对同时播放和录制有严格的限制,处理不好就是其中一个完全没声音。这个后面讲具体实现的时候我会详细说怎么规避。

3. 核心环节拆解:从麦克风到文字再到声音

3.1 权限申请:别等到用户点了按钮才要权限

Android 的录音权限是危险权限,必须动态申请。很多新手会犯的一个错误是:用户点了“开始录音”按钮,才弹出权限对话框。这时候用户已经表达了明确的意图,突然被一个系统弹窗打断,体验很割裂。

更好的做法是在页面初始化的时候就检查权限,如果没授权,用一个友好的引导页说明为什么需要这个权限,然后再触发系统弹窗。如果用户拒绝了,要给出明确的提示,告诉他去设置里怎么开。如果用户勾选了“不再询问”,那就只能引导他去系统设置页面手动开启。

// 检查录音权限 private fun checkAudioPermission(): Boolean { return ContextCompat.checkSelfPermission( this, Manifest.permission.RECORD_AUDIO ) == PackageManager.PERMISSION_GRANTED } // 申请权限 private val requestPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted -> if (isGranted) { startListening() } else { // 判断是否被永久拒绝 if (!shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) { showGoToSettingsDialog() } else { showPermissionDeniedTip() } } }

注意:从 Android 11 开始,如果应用在后台使用麦克风,还需要额外声明前台服务。如果你的语音功能需要在后台持续监听,必须创建一个前台服务并显示通知,否则系统会直接掐断你的录音。

3.2 音频采集:采样率和缓冲区大小的选择

音频采集这块,最核心的参数就两个:采样率和缓冲区大小。

采样率决定了你能捕捉到多高的频率。根据奈奎斯特采样定理,采样率至少要是目标频率的两倍。人声的主要频率范围在 300Hz 到 3400Hz 之间,所以电话系统的采样率是 8kHz。但语音识别引擎通常要求 16kHz,因为更高的采样率能保留更多辅音的高频信息,对识别准确率有帮助。44.1kHz 是音乐 CD 的标准,对语音识别来说完全没必要,反而会增加数据量和处理负担。

缓冲区大小决定了采集的延迟和稳定性。缓冲区太小,容易发生欠载,导致音频断断续续;缓冲区太大,延迟就高,用户说完话要等很久才有反应。Android 的 AudioRecord 提供了一个getMinBufferSize方法来获取设备支持的最小缓冲区,我一般会取这个值的两到四倍作为实际使用的缓冲区大小。

val sampleRate = 16000 val channelConfig = AudioFormat.CHANNEL_IN_MONO val audioFormat = AudioFormat.ENCODING_PCM_16BIT val minBufferSize = AudioRecord.getMinBufferSize( sampleRate, channelConfig, audioFormat ) val bufferSize = minBufferSize * 2 val audioRecord = AudioRecord( MediaRecorder.AudioSource.VOICE_RECOGNITION, sampleRate, channelConfig, audioFormat, bufferSize )

这里AudioSource的选择也有讲究。VOICE_RECOGNITION会启用一些针对语音的预处理,比如降噪和自动增益,适合识别场景。MIC是原始信号,不做任何处理,适合需要自己处理音频的场景。VOICE_COMMUNICATION针对通话优化,会启用回声消除,适合双向通话的场景。

3.3 语音识别:回调机制和中间结果的处理

系统自带的 SpeechRecognizer 使用起来很简单,但它是一个异步回调的模型,理解它的回调时序很重要。

val speechRecognizer = SpeechRecognizer.createSpeechRecognizer(context) val recognitionListener = object : RecognitionListener { override fun onReadyForSpeech(params: Bundle?) { // 引擎就绪,可以开始说话 updateUiState("请开始说话") } override fun onBeginningOfSpeech() { // 检测到用户开始说话 updateUiState("正在聆听...") } override fun onRmsChanged(rmsdB: Float) { // 音量变化,可以用来做音量条动画 updateVolumeIndicator(rmsdB) } override fun onBufferReceived(buffer: ByteArray?) { // 原始音频数据,一般不用 } override fun onEndOfSpeech() { // 用户停止说话 updateUiState("识别中...") } override fun onError(error: Int) { // 错误处理,后面详细讲 handleRecognitionError(error) } override fun onResults(results: Bundle?) { // 最终识别结果 val matches = results?.getStringArrayList( SpeechRecognizer.RESULTS_RECOGNITION ) val text = matches?.firstOrNull() ?: "" handleFinalResult(text) } override fun onPartialResults(partialResults: Bundle?) { // 中间结果,可以用来做实时字幕 val matches = partialResults?.getStringArrayList( SpeechRecognizer.RESULTS_RECOGNITION ) val partialText = matches?.firstOrNull() ?: "" updatePartialText(partialText) } override fun onEvent(eventType: Int, params: Bundle?) { // 一般不用 } } speechRecognizer.setRecognitionListener(recognitionListener)

onPartialResults这个回调非常有用。它会在用户说话的过程中不断返回中间结果,你可以用它来做实时字幕效果,让用户看到自己说的话正在被识别。但要注意,中间结果是不稳定的,后面的回调可能会修正前面的内容。所以如果你要做字幕,最好用一个单独的 TextView 来显示中间结果,等onResults回来之后再替换成最终结果。

3.4 语音合成:让机器说话的自然度调优

TextToSpeech 的接入也不复杂,但要让合成出来的声音听起来自然,有几个参数需要调。

val tts = TextToSpeech(context) { status -> if (status == TextToSpeech.SUCCESS) { // 设置语言 val result = tts.setLanguage(Locale.CHINA) if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) { // 语言不支持,需要提示用户 } // 设置语速,1.0 是正常速度 tts.setSpeechRate(1.0f) // 设置音调,1.0 是正常音调 tts.setPitch(1.0f) } } // 合成并播放 fun speak(text: String) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { val params = Bundle().apply { putInt(TextToSpeech.Engine.KEY_PARAM_STREAM, AudioManager.STREAM_MUSIC) } tts.speak(text, TextToSpeech.QUEUE_FLUSH, params, "utteranceId") } else { tts.speak(text, TextToSpeech.QUEUE_FLUSH, null) } }

语速和音调的调整要根据内容来。如果是播报新闻,语速可以稍快一点,1.1 到 1.2 左右;如果是给儿童讲故事,语速要慢,0.8 到 0.9,音调可以稍微高一点。但不要调得太极端,否则听起来会很奇怪。

还有一个容易被忽略的点是文本的预处理。TTS 引擎对数字、符号、英文的处理规则不一样。比如“2024年”有的引擎会读成“二零二四年”,有的会读成“两千零二十四年”。如果你对读音有特定要求,最好在传入之前把文本规范化。比如把“100”写成“一百”,把“km”写成“千米”。

4. 完整实操:搭一个能用的语音记账 Demo

4.1 项目结构和依赖配置

光讲原理不够,我带你搭一个完整的 Demo。这个 Demo 的功能是:用户按住按钮说话,比如“午餐花了三十五块”,松开后识别出文字,解析出金额和类别,然后语音播报确认。

先看项目结构:

app/ src/main/ java/com/example/voicebook/ MainActivity.kt AudioRecorderManager.kt SpeechRecognitionManager.kt TtsManager.kt ExpenseParser.kt res/ layout/activity_main.xml values/strings.xml

依赖方面,系统自带的 API 不需要额外引入库。但如果你要用协程来处理异步逻辑,需要加上:

dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3" }

4.2 音频录制模块的封装

我把录音相关的逻辑封装成一个独立的类,这样主界面不用关心底层细节。

class AudioRecorderManager { private var audioRecord: AudioRecord? = null private var isRecording = false private var recordingThread: Thread? = null companion object { const val SAMPLE_RATE = 16000 const val CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT } fun startRecording(onDataAvailable: (ByteArray) -> Unit) { val minBufferSize = AudioRecord.getMinBufferSize( SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT ) val bufferSize = minBufferSize * 2 audioRecord = AudioRecord( MediaRecorder.AudioSource.VOICE_RECOGNITION, SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT, bufferSize ) if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) { throw IllegalStateException("AudioRecord 初始化失败") } audioRecord?.startRecording() isRecording = true recordingThread = Thread { val buffer = ByteArray(bufferSize) while (isRecording) { val bytesRead = audioRecord?.read(buffer, 0, buffer.size) ?: 0 if (bytesRead > 0) { onDataAvailable(buffer.copyOf(bytesRead)) } } }.also { it.start() } } fun stopRecording() { isRecording = false recordingThread?.join(1000) audioRecord?.stop() audioRecord?.release() audioRecord = null recordingThread = null } }

注意:audioRecord.read是一个阻塞调用,必须放在子线程里执行。如果你在主线程调用,会直接 ANR。另外,stopRecording的时候要先设置isRecording = false,再 join 线程,最后释放资源。顺序反了的话,子线程可能还在读一个已经释放的 AudioRecord,导致崩溃。

4.3 识别与合成的协同调度

识别和合成如果同时进行,会出问题。因为 TTS 播放的时候会占用音频输出通道,而识别需要音频输入通道,两者本身不冲突。但如果你在 TTS 播放的时候启动识别,麦克风会把 TTS 的声音也录进去,导致识别出乱七八糟的内容。

我的做法是:TTS 播放期间暂停识别,播放结束后再恢复。用一个状态机来管理:

sealed class VoiceState { object Idle : VoiceState() object Listening : VoiceState() object Recognizing : VoiceState() object Speaking : VoiceState() } class VoiceController( private val recognizer: SpeechRecognitionManager, private val tts: TtsManager ) { private var currentState: VoiceState = VoiceState.Idle fun startListening() { if (currentState is VoiceState.Speaking) { // 正在播放,先停止 tts.stop() } currentState = VoiceState.Listening recognizer.startListening() } fun onRecognitionResult(text: String) { currentState = VoiceState.Recognizing // 解析文本 val expense = ExpenseParser.parse(text) if (expense != null) { // 合成确认语音 currentState = VoiceState.Speaking tts.speak("好的,${expense.category}${expense.amount}元,已记录") } else { currentState = VoiceState.Speaking tts.speak("抱歉,我没有听清楚,请再说一遍") } } fun onTtsComplete() { currentState = VoiceState.Idle } }

4.4 文本解析:从“午餐三十五块”到结构化数据

语音识别出来的是一串文字,你需要从中提取出金额和类别。这块我用的是规则匹配加关键词映射,对于 Demo 来说足够了。

object ExpenseParser { private val categoryKeywords = mapOf( "早餐" to "早餐", "午餐" to "午餐", "晚餐" to "晚餐", "打车" to "交通", "地铁" to "交通", "公交" to "交通", "买菜" to "食材", "超市" to "日用品", "电影" to "娱乐", "咖啡" to "饮品" ) private val numberPattern = Regex("(\\d+(\\.\\d+)?)") private val chineseNumberMap = mapOf( "零" to 0, "一" to 1, "二" to 2, "两" to 2, "三" to 3, "四" to 4, "五" to 5, "六" to 6, "七" to 7, "八" to 8, "九" to 9, "十" to 10, "百" to 100, "千" to 1000 ) fun parse(text: String): Expense? { val category = categoryKeywords.entries .firstOrNull { text.contains(it.key) } ?.value ?: "其他" val amount = extractAmount(text) ?: return null return Expense(category, amount) } private fun extractAmount(text: String): Double? { // 先尝试阿拉伯数字 numberPattern.find(text)?.let { return it.value.toDoubleOrNull() } // 再尝试中文数字 return parseChineseNumber(text) } private fun parseChineseNumber(text: String): Double? { // 简化版中文数字解析,处理“三十五”这类 var result = 0.0 var current = 0.0 for (char in text) { val value = chineseNumberMap[char.toString()] ?: continue when { value >= 10 -> { if (current == 0.0) current = 1.0 result += current * value current = 0.0 } else -> { current = current * 10 + value } } } result += current return if (result > 0) result else null } } data class Expense(val category: String, val amount: Double)

这个解析器很粗糙,但能覆盖大部分日常场景。实际项目中你可能需要更复杂的 NLP 处理,比如处理“一百二”这种省略说法,或者“三十五块五”这种带角分的表达。

4.5 界面交互:按住说话的状态反馈

界面这块,核心是给用户清晰的状态反馈。我用一个按钮加一个状态文本加一个音量指示器来实现。

<LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:gravity="center" android:padding="24dp"> <TextView android:id="@+id/tvStatus" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="按住下方按钮开始说话" android:textSize="18sp" /> <TextView android:id="@+id/tvPartialResult" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:gravity="center" android:textSize="16sp" android:textColor="#666666" /> <ProgressBar android:id="@+id/volumeIndicator" style="?android:attr/progressBarStyleHorizontal" android:layout_width="200dp" android:layout_height="8dp" android:layout_marginTop="24dp" android:max="100" android:progress="0" /> <Button android:id="@+id/btnSpeak" android:layout_width="120dp" android:layout_height="120dp" android:layout_marginTop="32dp" android:text="按住说话" android:textSize="16sp" /> </LinearLayout>

按钮的触摸事件处理:

btnSpeak.setOnTouchListener { _, event -> when (event.action) { MotionEvent.ACTION_DOWN -> { startListening() true } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { stopListening() true } else -> false } }

音量指示器的更新在onRmsChanged回调里:

override fun onRmsChanged(rmsdB: Float) { // rmsdB 的范围大概是 -2 到 10 val progress = ((rmsdB + 2) / 12 * 100).toInt().coerceIn(0, 100) runOnUiThread { volumeIndicator.progress = progress } }

5. 常见问题与排查技巧实录

5.1 识别没反应或者立刻返回错误

这是最常见的问题,原因可能有好几种。我整理了一个排查表:

现象可能原因排查方法
调用 startListening 后立刻 onError没有录音权限检查权限是否授予
调用后没有任何回调识别器没有正确初始化检查 createSpeechRecognizer 的返回值
onError 返回 ERROR_NO_MATCH没有说话或者环境太吵提示用户重新说
onError 返回 ERROR_RECOGNIZER_BUSY上一次识别还没结束确保每次 start 之前先 cancel
onError 返回 ERROR_INSUFFICIENT_PERMISSIONS权限被拒绝引导用户去设置开启
onError 返回 ERROR_NETWORK网络问题(云识别)检查网络连接

其中ERROR_RECOGNIZER_BUSY特别常见。如果你连续快速点击按钮,上一次识别还没结束就启动了新的,就会报这个错。解决办法是在startListening之前先调用cancel(),并且用一个标志位防止重复启动。

fun startListening() { if (isListening) return isListening = true speechRecognizer.cancel() // 先取消上一次 speechRecognizer.startListening(intent) }

5.2 识别结果不准确或者断句奇怪

识别准确率受很多因素影响。除了引擎本身的能力,你能控制的主要是这几点:

音频质量。确保采样率是 16kHz,位深是 16bit,单声道。如果设备支持,开启VOICE_RECOGNITION音频源。避免在嘈杂环境下使用,如果必须在嘈杂环境用,可以考虑加一个简单的降噪预处理。

说话方式。语音识别对语速和停顿很敏感。太快太连贯的说话,识别率会下降。可以在 UI 上引导用户“请慢慢说,说完停顿一下”。

自定义词汇。系统识别器支持通过EXTRA_BIASING_STRINGS传入一些偏置词,让引擎更倾向于识别这些词。比如你的应用是记账的,可以传入“早餐午餐晚餐打车地铁”这些词。

val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply { putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM) putExtra(RecognizerIntent.EXTRA_LANGUAGE, "zh-CN") putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true) putExtra(RecognizerIntent.EXTRA_BIASING_STRINGS, arrayListOf("早餐", "午餐", "晚餐", "打车")) }

5.3 TTS 没有声音或者声音异常

TTS 没声音,先检查这几个点:

  • 语言包是否已安装。有些设备出厂时没有预装中文语音包,需要用户去系统设置里下载。
  • 媒体音量是否为零。TTS 默认走的是媒体音量通道,如果用户把媒体音量调到了零,TTS 就没声音。
  • 是否被其他音频应用占用。如果有音乐播放器在后台播放,TTS 可能会被压制。

声音异常,比如语速太快太慢、音调奇怪,检查setSpeechRate和setPitch的值是否在合理范围内。另外,不同设备的 TTS 引擎实现不一样,同样的参数在不同手机上效果可能不同。如果对声音一致性要求高,可以考虑用云端的 TTS 服务,但那就需要联网了。

实操心得:在onInit回调里不要直接调用speak,因为这时候引擎可能还没完全就绪。我一般会设置一个标志位,等onInit返回 SUCCESS 之后再允许调用speak。如果用户在初始化完成之前就触发了播放,把文本存到一个队列里,等初始化完成后再播放。

5.4 内存泄漏和资源释放

语音相关的对象,比如 SpeechRecognizer、TextToSpeech、AudioRecord,都是系统资源,必须在使用完毕后释放。特别是 SpeechRecognizer,它内部持有 Context,如果不释放,会导致 Activity 泄漏。

override fun onDestroy() { super.onDestroy() speechRecognizer?.destroy() tts?.shutdown() audioRecorder?.stopRecording() }

另外,如果是在 Fragment 里使用,要在onDestroyView里释放,而不是onDestroy,因为 Fragment 的 View 可能比 Fragment 本身先销毁。

6. 进阶方向:让语音交互更自然

6.1 连续对话与打断处理

基础的语音交互是“按一下说一句”,但更自然的体验是连续对话。用户说完一句,系统自动开始听下一句,不需要再按按钮。这需要处理几个问题:怎么判断用户说完了?怎么处理用户打断系统说话的情况?

判断用户说完,可以用静音检测。当音频的能量低于某个阈值持续超过一定时间(比如 1.5 秒),就认为用户说完了。这个阈值需要根据环境噪音动态调整,安静环境下阈值低,嘈杂环境下阈值高。

打断处理,就是在 TTS 播放的时候持续监听麦克风,如果检测到用户开始说话,立刻停止 TTS 播放,切换到识别模式。这个逻辑听起来简单,但实现起来要小心音频通道的切换,处理不好会有回音或者爆音。

6.2 离线识别的可行性

如果你需要完全离线的识别,系统自带的识别器在部分设备上支持离线模式。可以通过SpeechRecognizer.isRecognitionAvailable和checkRecognitionSupport来查询设备是否支持离线识别。但离线识别的准确率通常比在线低不少,而且支持的词汇量有限。

另一个思路是用本地的轻量级模型,比如一些开源的语音识别引擎,可以集成到 Android 应用中。但模型体积通常不小,最小的也有几十兆,对 App 包大小有影响。而且推理速度取决于手机性能,低端机上可能很慢。

6.3 语音唤醒词的实现

语音唤醒是指设备在待机状态下持续监听,听到特定的词(比如“你好助手”)就激活。这个功能对功耗要求很高,因为要一直开着麦克风。系统级别的唤醒通常由硬件协处理器完成,第三方应用很难做到低功耗的持续监听。

如果你的应用需要唤醒词功能,比较现实的做法是:在应用处于前台时,用低采样率持续监听,检测到唤醒词后再启动完整的识别流程。但这样功耗依然不低,而且用户可能会担心隐私问题。所以除非是核心功能,否则不建议在普通应用里做唤醒词。

7. 我踩过的那些坑和最后的建议

做语音项目这几年,踩过的坑真的不少。有一个印象特别深:早期做一个语音输入功能,测试的时候一切正常,上线后收到用户反馈说在某些手机上完全没反应。排查了很久才发现,那些手机的系统里根本没有预装语音识别服务,SpeechRecognizer.createSpeechRecognizer返回了 null,而我没有做空判断,直接调用了方法就崩溃了。从那以后,我养成了一个习惯:所有系统服务相关的对象,创建之后第一件事就是判空。

还有一个坑是关于音频焦点的。如果你的应用在录音,这时候来了一个电话,系统会把音频通道抢走,你的录音就断了。如果不处理这个中断,用户会发现录到一半没声音了。正确的做法是注册AudioManager.OnAudioFocusChangeListener,在失去焦点的时候暂停录音,等重新获得焦点后再恢复。

最后分享一个我觉得很实用的技巧:在开发阶段,把识别和合成的中间结果都打日志。包括每次识别的原始音频长度、识别返回的置信度、TTS 的初始化状态、播放完成的时间戳。这些日志在排查问题的时候非常有用。我甚至会在 Debug 版本里做一个隐藏的调试面板,实时显示这些信息,测试的时候一目了然。

语音开发这个方向,入门不难,但要做好做精,需要关注的细节很多。从音频采集的参数调优,到识别引擎的选型,再到交互逻辑的设计,每一环都会影响最终的体验。希望这篇内容能帮你少走一些弯路,把精力花在真正创造价值的地方。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 3:33:59

计算机组成原理第六章:指令系统、寻址方式与RISC/CISC全解析

计算机组成原理学到第六章&#xff0c;很多人第一次被“指令”卡住。前面的运算器怎么加法、存储器怎么读写&#xff0c;都还能靠直觉跟上&#xff1b;一看到指令格式、寻址方式&#xff0c;就感觉开始背天书。其实这一章没那么玄&#xff1a;指令系统就是程序和CPU之间约定好的…

作者头像 李华
网站建设 2026/10/20 3:32:58

医疗AI大模型落地实战:UltraUnion超声诊断模型开发全解析

去年年底&#xff0c;院里抽调了一批人组了个"大模型进医院"的专项小组&#xff0c;任务不是写汇报材料&#xff0c;而是真正把医疗AI大模型落到业务流程里。说实话&#xff0c;刚被拉进去的时候我有点怀疑——这词听着热&#xff0c;但医疗行业的环境、数据、合规要…

作者头像 李华
网站建设 2026/10/20 3:32:07

智能监控系统AI架构实战:边缘预筛与事件链路设计

监控系统在AI落地这件事上&#xff0c;一直是看着热闹、真正做好不容易的典型场景。很多团队最初的想法就是“接一路视频流&#xff0c;跑个模型&#xff0c;有告警就推送”&#xff0c;可真到生产环境就会发现&#xff1a;视频流不稳定、GPU资源不够分、告警重复轰炸、模型一更…

作者头像 李华
网站建设 2026/10/20 3:32:05

Flutter for OpenHarmony数据导出实战:CSV/JSON生成与分享避坑指南

做移动数据使用监管助手App&#xff0c;功能做到后面&#xff0c;几乎每个用户都会提同一个需求&#xff1a;统计出来的数据&#xff0c;能不能导出来存成文件&#xff1f;在Flutter for OpenHarmony这套技术栈里&#xff0c;数据导出实现远没有想象中那么顺手&#xff0c;文件…

作者头像 李华
网站建设 2026/10/20 3:31:33

哥白尼30米DEM全国分省打包处理全流程解析

GIS圈里折腾地形数据的朋友&#xff0c;这两年基本绕不开一个名字&#xff1a;哥白尼30米DEM。先别急着划走&#xff0c;这次分享的不是那种随手甩个原始链接就完事的资源帖&#xff0c;而是我自己把数据从原始瓦片一直处理到“全国分省、解压即用”的全过程。这个30米分辨率的…

作者头像 李华