ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Android语音播报Demo从能响到能用:TTS、队列与音频焦点实战

Android语音播报Demo从能响到能用:TTS、队列与音频焦点实战 简介面向MFC开发者的语音播报Demo基于微软Speech APITTS实现文本到语音的转换适合需要在Windows桌面应用中集成语音提示、无障碍播报或自动化语音交互的C开发者参考。压缩包共54个文件约54.91MB包含头文件、源码文件、Visual Studio工程文件、编译生成的exe与调试文件以及界面图标等资源文件可直接打开工程查看完整实现。已有1031人学习下载。示例展示了如何初始化ISpVoice接口、加载并转换文本、控制语速与播放状态并附有Speech API相关头文件的引入方式工程中的SpeakDlg、TextSpeaker等模块分别负责界面交互与文本语音转换逻辑通过阅读工程代码与调试信息可快速掌握在MFC框架中搭建语音播报功能的完整流程。通过该Demo开发者还可以观察到MFC对话框程序的初始化流程以及如何将SAPI组件嵌入按钮事件中实现即时播报为后续扩展多语言支持或语音交互功能打下基础。 最近在项目里要预研一个语音播报Demo我第一反应是这东西不是随便找段代码就能跑起来吗把系统自带的TextToSpeech调起来合成一句播一句就完事了。等真正动手才发现网上能找到的Demo绝大多数都停留在“能响”这个层面——播完一句就结束来电话时声音压不住页面销毁了播放线程还在跑换个设备连引擎初始化都失败。这篇文章就把我从需求梳理、技术选型到跑通Demo、挨个填坑的完整过程整理出来给最近也在折腾语音播报无论是扫码提示、库存盘点、导航播报还是无障碍读屏的同行一个可参考的路线。1. 需求画像语音播报Demo到底要验证明白什么1.1 先分清你属于哪一类播报需求很多人接到“做个语音播报Demo”的需求就直接去搜TTS接口了这其实是本末倒置。我建议先花半天把需求方向理清楚因为不同场景对播报的诉求差异非常大。我做过的项目里语音播报大致可以分成三类。第一类是提示音类典型场景是扫码枪扫到条码后“滴”一声、仓库盘点时播报“数量正确”“请重试”这类需求往往用短音频文件MP3/WAV就能解决根本不需要TTS引擎选型时反而更简单。第二类是内容播报类比如把订单号、商品名、设备参数等动态文本转成语音念出来这才是TTS的主场文本内容不确定、可能很长需要合成引擎支持。第三类是交互类类似语音助手、多轮对话不仅要把机器的话念出来还要处理用户语音打断、上下文衔接这一层已经超出普通播报Demo的范围涉及ASR和对话管理工程复杂度完全是另一个量级。拿到需求如果说不清具体属于哪一类就拿“能响就行”当验收标准后面一定会返工。1.2 把“能响”和“能用”分开定义常见的误区是Demo 能出声。我自己的经验是验证一个语音播报Demo至少要分三层。第一层是能响也就是TTS初始化成功、调用speak能播放出一句话。第二层是能连续播多句文本按顺序播报不卡顿、不吞字、不重叠。第三层是能管控包括音频焦点是否抢占正常、App退到后台后播报是否继续或停止、来电时是否被打断、音量能否独立控制。在动手写代码之前最好先和需求方确认要验证到哪一层。如果只是给客户看效果第一层就够了但如果是给自己产品的技术预研至少要做到第三层再进入正式开发。我在实际项目里吃过亏Demo做到了“能响”就交差结果集成到业务里发现来电话时语音还在外放声音和通话混在一起被测试提了好几个bug这才回过头来补音频焦点处理。2. 技术路线本地TTS与云端合成Demo阶段别拍脑袋2.1 两条路线的核心差异语音播报的合成能力可以通过两条路线获取一是本地TTS引擎二是云端语音合成接口。本地引擎的做法是在设备端内置或加载合成模型把文本直接转成音频。优点是离线可用、延迟低首次初始化后基本在几十毫秒到几百毫秒不消耗流量数据不出设备隐私性好。缺点是音质上限有限尤其是在中低端硬件上合成出来的声音会比较机械而且本地引擎的资源包体积不小离线发音人往往要在首次启动时额外下载几百MB的语言包这个体验对用户来说并不友好。云端合成则是把文本上传到服务端由服务端合成音频返回。音质高、发音人选择多甚至能做情感合成、声音复刻。代价是必须有网络合成延迟受网络波动影响明显如果用来做扫码枪这类瞬时反馈的场景网络一慢体验会非常糟糕同时按调用量计费量产设备上是一笔持续的成本。2.2 容易被忽略的引擎适配问题本地TTS有个很隐蔽的坑不同Android设备的系统自带TTS引擎不一样。原生Android上通常是Google Text-to-Speech国内手机可能是讯飞、度秘、三星或其他定制引擎甚至同一家厂商的不同系统版本对TTS的支持程度都不一致。你在一台测试机上setLanguage好端端的换一台设备可能返回LANG_MISSING_DATA或者语音包没安装、播不出来的情况。所以选路线的核心判断标准是你的产品是面向固定设备还是用户随意安装的App。如果是工业手持机、仓储备货终端这类相对封闭的设备用本地TTS引擎或预置离线语音包更稳如果是通用App建议做成双引擎主用系统TTS同时允许用户在设置里切换云端合成代码层做好接口隔离。这样Demo能跑后续大规模部署也不至于被单一引擎卡脖子。2.3 有时候最佳方案不是TTS如果你的“语音播报Demo”其实只需要固定的几句提示语比如“操作成功”“请保持安静”我建议直接生成音频文件放资源里用MediaPlayer或SoundPool播放。好处是零初始化时间、零延迟、无兼容性问题音质还稳定。TTS适合文本动态变化的场景不是所有语音需求都需要合成引擎——这个判断做好能省后面一大半的适配麻烦。3. Android端把最简播报链路跑通从初始化到出声3.1 工程准备和关键配置这一节用Android原生TTSTextToSpeech来演示最简链路。工程上只需要在模块的build.gradle里确认minSdkVersion在21以上不需要额外依赖第三方库系统SDK里已经带了TTS API。权限方面有个容易误导人的点语音播报不需要麦克风权限也不一定需要网络权限这取决于你用的合成引擎。如果用系统TTS离线合成整个流程不申请任何运行时权限就能跑但如果要动态下载语音包在某些系统上会涉及存储和网络权限。另外从Android 13开始如果App要在后台播放音频或显示前台服务通知还得留意通知权限和前台服务类型声明这些在Demo阶段可以先不管但正式集成时会被测试问到。3.2 初始化、合成与播放的核心代码TTS使用上有一个铁律必须先等onInit回调返回成功才能调用speak。直接在构造函数后马上speak大概率会播不出来或者报错因为引擎可能还在加载。下面是一个能跑通的最简实现我用的是JavaKotlin思路完全一样public class SpeechHelper { private TextToSpeech tts; private boolean ready false; public SpeechHelper(Context context) { tts new TextToSpeech(context, status - { if (status TextToSpeech.SUCCESS) { int result tts.setLanguage(Locale.CHINESE); if (result TextToSpeech.LANG_MISSING_DATA || result TextToSpeech.LANG_NOT_SUPPORTED) { Log.e(TTS, 当前系统缺少中文语音包); } else { ready true; } } else { Log.e(TTS, TTS引擎初始化失败status status); } }); } public void speak(String text) { if (!ready) return; tts.speak(text, TextToSpeech.QUEUE_ADD, null, utteranceId_ System.currentTimeMillis()); } public void shutdown() { if (tts ! null) { tts.stop(); tts.shutdown(); ready false; } } }speak方法的第二个参数QUEUE_ADD表示把当前文本追加到播报队列上一句播完再播下一句。如果你希望新文本立刻打断当前正在播的内容就用QUEUE_FLUSH。这个“打断/追加”的选择是语音播报队列管理的核心后面会细说。3.3 队列为什么要自己管理不少初学的Demo会忽略一个现象连续调用speak多次时如果都用QUEUE_FLUSH永远只播最后一句如果都用QUEUE_ADD又无法实现“新消息到了立刻播报”。真实业务里往往是混合场景例如仓储播报扫描太快时应该允许新播报打断旧播报但有些关键提示又必须完整播完不能被后到的干扰。这就需要在业务层自己做队列管理。我给一个相对实用的做法维护一个消息队列给每条播报文本标注type和优先级默认给TTS设QUEUE_ADD每当来了一条高优先级消息先把当前队列里未播完的普通消息清掉再追加新消息。TTS的UtteranceProgressListener里有onStart和onDone回调可以借此感知当前哪条在播、队列是否清空从而精确控制播报节奏。这个逻辑看起来简单但就是在普通Demo和可交付代码之间的分水岭。4. 从“能响”到“不难用”集成阶段躲不开的四个坑4.1 音频焦点抢占为什么播报被电话打断后不恢复这也是最常见的线上问题。TTS默认的输出流是STREAM_MUSIC和音乐App走同一个音频流。如果用户正在听歌或有电话进来语音播报要么和音乐叠在一起要么直接被系统打断后不再恢复。正规做法是使用AudioManager请求音频焦点。在播报开始时requestAudioFocus指定AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK播报结束后立即abandonAudioFocus。如果你不希望语音播报把背景音乐压低而是彻底暂停考虑GAIN_TRANSIENT如果允许语音和音乐同时出声并且语音突出可以用MAY_DUCK但需要设置音量阈值。实测下来很多设备对DUCK的处理不太积极所以想要稳定的“压低音乐”效果最好在播报时手动调整一下媒体音量播完再恢复。音频焦点还有一个在电话通道里的场景要小心如果你用STREAM_VOICE_CALL或开启了免提模式系统可能把播报路由到听筒而不是扬声器。Demo阶段就用STREAM_MUSIC别乱改正式项目再根据硬件场景仔细选流。4.2 生命周期泄漏页面销毁了播报为什么还在TTS实例持有Context如果直接在Activity里new TextToSpeech(this, callback)而没有在onDestroy时释放Activity销毁后TTS还在后台持有引用轻则内存泄漏重则播报不停止。我见过一个现象页面退出后语音还在继续念订单号用户说“这App是不是卡死了”其实就是shutdown没调用。正确做法是把TTS实例放到单例或ApplicationContext级别避免和页面生命周期绑定。播报的启动和停止仍然可以由页面控制但引擎本身要活得比页面长。如果你按页面创建TTS务必在onDestroy里tts.stop()和tts.shutdown()并且把UI回调的引用及时置空防止回调打到已销毁的Activity上。4.3 声音“没听到”不代表播报没响我遇到过好几次用户反馈“没声音”查了一圈发现代码没问题是设备本身的问题。比如设备调成了静音模式STREAM_MUSIC会被静音蓝牙耳机连接时播报被路由到耳机而不是扬声器用户戴着耳机没感觉不戴耳机的人就听不到。所以在验收语音播报Demo时建议在代码里把音量控制单独设置播报时不直接依赖系统媒体音量可以调用AudioManager的setStreamVolume把STREAM_MUSIC临时调到一个合适的值播完再恢复必要时还可以检测当前音频路由判断是否有蓝牙A2DP设备连接。这类细节正式产品的播报模块必须处理得干干净净否则交付到客户手里会被一句“没声音”反复打回。4.4 引擎初始化异步与国产设备兼容TTS的所有操作都是异步的onInit回调不一定会很快来尤其是一些低端设备首次启动要加载引擎可能几秒钟。如果在readyfalse时调用speak目前代码是直接return这在功能上是对的但用户点击按钮毫无反馈体验会很差。更好的做法是把待播文本缓存到一个列表里等onInit成功后再自动播。国产设备上还有一类问题系统TTS引擎可能非常陈旧LANG_MISSING_DATA出现概率很高。处理方式是引导用户到系统设置里下载语音包或者提供一个“在Demo里切换引擎”的入口——调用Intent动作TextToSpeech.Engine.ACTION_CHECK_TTS_DATA检测可用引擎再弹出选择列表。语音播报Demo做到这一步基本算是在兼容性上能拿得出手了。5. 验收与进阶Demo做到什么程度才敢拿出去演示5.1 三张自检表把播报质量量化到演示前的最后一两天我会按功能、体验、适配三个维度清单验收。功能清单包括单句能播、队列能连续播、打断逻辑是否正确、播放完是否有回调。体验清单包括播报不重叠、不吞字播报时背景音乐会被压低或暂停电话进来时播报能停止挂断后如果业务需要还能继续静音模式下有明确提示或自动调音量。适配清单包括至少在一台原生系统手机、一台国产定制系统手机、一台平板上分别验证测过没装语音包的设备测过蓝牙音箱/耳机的路由。这三张表走完Demo才敢拿给客户或业务方按流程演示不然很容易出现“在你手机上响在我手机上哑”的尴尬。5.2 进阶功能优先级队列、动态读音和跨平台如果之后要继续深入这个Demo我建议优先做两件事。第一件是播报优先级队列前面提过是核心第二件是读音纠偏比如播报“XX型号2026”时默认TTS可能念成“二零二六”业务上要求念“两零二六”这需要对文本做预处理替换成自定义读音或带数字单位的分段表达。语音播报做到面向具体行业时这种文本规整比合成引擎本身更花时间。跨平台方面也要提前看一眼。iOS端本身有AVSpeechSynthesizer接口很简洁中文发音人可选但同样面临队列和焦点问题如果团队要用Flutter或React Native市面上插件很多但底层仍是调系统TTS接口Android端那些坑一个都不会少。所以真正靠谱的思路是把语音播报抽成一个内部组件先在企业自己的Android设备上打磨稳定再考虑对外分平台适配。我自己的习惯是每次做语音播报Demo都会在抽屉里备一台不同品牌的测试机。TTS这块“能说话”是及格线“说得对、说得顺、说得不吵人”才是真正要打磨的地方。一个语音播报Demo做到能连续播、能打断、不泄漏、不抢焦点、适配三台不同真机再往后接正式功能就是水到渠成的事。本文还有配套的精品资源点击获取
返回列表