十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

uni-app中集成Android原生TTS插件:离线与在线语音合成实践

uni-app中集成Android原生TTS插件:离线与在线语音合成实践 简介基于uni-app跨平台框架打造的Android原生TTS语音合成插件面向需要在App中快速接入语音播报能力的移动端开发者适用于智能语音助手、有声读物、语音导航等场景。插件支持离线与在线两种合成模式提供中文、英文等多语言输出开发者可按用户网络条件灵活切换兼顾流畅体验与成本控制。资源包共139个文件整体约32.55MB其中包含可集成的aar库、可直接安装的示例APK、Java/JS源码、gradle构建脚本、Vue页面示例以及docx说明文档和txt配套说明结构清晰便于二次定制与快速验证。目前已有133人学习下载。通过解压即可获得完整插件工程、调用示例与接口文档可帮助开发者避免从零编写TTS逻辑快速理解uni-app原生插件封装流程并借助跨平台特性将语音能力同步延伸至iOS与Web端。1. 为什么uni-app项目需要一个Android原生TTS插件一个语音导航页面里前端只需要把「前方三百米右转」交给TTS剩下的播报时长、音频焦点、断句停顿都该由底层处理。但uni-app编译到App端后H5的speechSynthesis不可用小程序端能力也回不到原生层唯一可靠的口子就是走原生插件。标题里这个Android原生TTS插件本质是把系统的、离线的、在线的三套合成路径封装成统一接口让uni-app侧用一个JS方法就能拿到「开始播报、播完、出错」三个关键事件。离线合成保证弱网下的导航应答在线合成提供更自然的神经网络音色中英文切换则覆盖了语音助手和有声读物这类场景里最常见的混读诉求。适合谁手头有uni-app项目、正准备接播报能力但不想把Android层代码摊到业务里的开发者。先理解插件要做的事后面参数和坑才好对齐。2. uni-app原生插件如何把TTS能力桥接给前端2.1 模块形态为什么用 Module 扩展而不是 Componentuni-app原生插件有Module与Component两种形态。Module扩展的是API能力不涉及UI渲染Component扩展的是自定义View适合地图、播放器这类需要原生控件的场景。TTS只产生音频不产生界面所以选Module。一个Module类继承UniModule内部方法用UniJSMethod注解暴露给前端通过uni.requireNativePlugin(模块名)就能拿到实例。常见做法是用一个TTSServiceModule包住所有合成逻辑下面是Android侧最基础的骨架public class TTSServiceModule extends UniModule { private TextToSpeech textToSpeech; private volatile boolean ready false; UniJSMethod(uiThread false) public void init(JSONObject options, UniJSCallback callback) { mUniSDKInstance.getContext().runOnUiThread(() - { textToSpeech new TextToSpeech(mUniSDKInstance.getContext(), status - { if (status TextToSpeech.SUCCESS) { ready true; callback.invoke(new HashMapString, Object() {{ put(code, 0); }}); } else { callback.invoke(new HashMapString, Object() {{ put(code, -1); put(message, tts init failed); }}); } }); }); } }逻辑说明TextToSpeech的初始化必须在主线程完成所以即使方法本身声明为uiThread false内部仍然要runOnUiThread包一层。ready用volatile修饰避免初始化异步完成后其他线程读到过期状态。回调invoke返回的code字段是约定好的结果码前端统一按code 0判断成功。2.2 Android 侧 TTS 引擎的接口边界系统TTS打开的是一条完整链路TextToSpeech.setLanguage()选语言、setSpeechRate()调语速、speak()进队列。语音数据由系统服务统一管理离线时能合成但音色受限于本地安装的语音包。第三方离线SDK则是一套独立实现自带Voice资源或需要单独下载音库接口不同但暴露给上层的能力一致输入文本、输出音频流。两组引擎共存时模块内建议做一个EngineAdapter把「初始化、合成、停止、释放」统一抽象成四个方法。前端不感知底层是系统引擎还是某个离线SDK切换引擎只发生在插件的配置层。这样做的收益是后续如果要换发音人不需要动业务代码。speak方法的Android侧实现关键参数在QMParams上UniJSMethod(uiThread false) public void speak(String text, JSONObject options, UniJSCallback callback) { if (!ready) { callback.invoke(new HashMapString, Object() {{ put(code, -1); put(message, engine not ready); }}); return; } float speed (float) options.optDouble(speed, 1.0); float pitch (float) options.optDouble(pitch, 1.0); boolean interrupt options.optBoolean(interrupt, false); textToSpeech.setSpeechRate(speed); textToSpeech.setPitch(pitch); textToSpeech.speak(text, interrupt ? TextToSpeech.QUEUE_FLUSH : TextToSpeech.QUEUE_ADD, null, tts- System.currentTimeMillis()); }参数说明interrupt为true时用QUEUE_FLUSH会清空当前队列适合导航中高频打断重播为false时用QUEUE_ADD排队适合有声书章节连播。utteranceId是回调的唯一标识播放完成、出错都会带它回来前端可以用它决定下一个动作。2.3 离线与在线的技术分界离线合成是「本地算」在线合成是「远端算完传音频」。离线路径的声音由本地引擎和音库决定无网络依赖响应在几十毫秒内在线路径通常走云端HTTP/WebSocket接口服务端返回完整音频或流式音频块客户端负责缓存和播放。在线合的成往往涉及两种播放策略一种是一次拉完整MP3再交给MediaPlayer适合有声书整段缓存另一种是边收PCM流边用AudioTrack播放首句延迟更低适合导航逐条播报。标题里的插件把这两种策略都纳入engineMode参数控制离线走系统引擎在线走网络请求。离线与在线的核心差异适合在选型时直接对照维度离线合成在线合成首句响应本地完成毫秒级依赖网络RTT和合成排队音色丰富度受本地音库限制可支持更高自然度的云端模型多语言切换需下载对应语言包云端动态返回覆盖广弱网表现不感知网络需处理超时、断流重试流量消耗无音频数据走网络在线路线的神经网络TTS方案例如coqui tts这类开源模型集群部署后音质稳定性和发音人风格都明显优于老式参数合成。但对一个uni-app插件来说在线引擎不是自己训练模型而是封装好与云端服务的交互让前端一个方法完成合成和播放。3. 在uni-app里接入TTS插件最小可运行示例3.1 插件声明与权限配置本地打包时插件工程编译出的AAR或源码目录放进nativeplugins目录云打包则在HBuilderX的「App原生插件配置」里勾选模块。模块名必须与AndroidManifest中注册的Service或Module类名对应。典型配置是先在manifest.json的App模块配置里声明插件名称TTSSpeech随后在页面中直接引用。在线合成依赖网络离线合成不需要额外权限但语音包下载和在线请求都需要在AndroidManifest中声明uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE /权限说明INTERNET是网络请求基础权限在线合成拿音频字节流必须声明ACCESS_NETWORK_STATE用来判断当前网络类型弱网时插件的降级逻辑依赖它。3.2 JS侧调用与参数映射前端最关心的就是「一行代码能播」。插件暴露的方法建议收敛为三个init初始化、speak合成播报、stop停止再加setParam做运行时参数热更新。所有方法统一回传code和message前端不需要关心Android层细节。const tts uni.requireNativePlugin(TTSSpeech) tts.init({ engineMode: offline, // 离线优先 language: zh-CN, speed: 1.0, pitch: 1.0 }, (res) { if (res.code 0) { tts.speak(下载完成开始导航, { interrupt: true, volume: 0.9, utteranceId: nav-001 }, (playback) { switch (playback.event) { case start: console.log(开始播报, playback.utteranceId) break case done: console.log(播报完成, playback.utteranceId) break case error: console.log(播报失败, playback.message) break } }) } })逻辑说明init完成前speak会触发engine not ready错误所以用回调嵌套保证初始化完成后再合成。interrupt: true会在新指令到来时清空旧播报导航场景就是靠它实现「当前播报被打断立即播新指令」的。回调的event字段是Android层UtteranceProgressListener映射出来的四个状态start、done、error和stop。3.3 生命周期与资源释放页面卸载时必须显式释放引擎否则会出现页面关了、声音还在响的问题。在uni-app页面销毁钩子里统一处理onUnload() { tts.stop({ utteranceId: nav-001 }, (res) { tts.release(() { console.log(TTS资源已释放) }) }) }释放逻辑说明stop先打断当前合成release再销毁系统TTS实例。如果不调release下次进入页面重新init时引擎状态可能残留导致回调收不到。Android侧还应在UniModule的onDestroy方法里兜底释放防止前端漏调。4. 离线合成与在线合成的切换策略与参数调优4.1 离线语音包检测与兜底切换离线TTS最隐蔽的问题是语音包缺失。系统TTS在setLanguage返回LANG_MISSING_DATA时合成不会报错但会静音。插件做好语音包检测前端就能在初始化阶段拿到状态int langResult textToSpeech.setLanguage(new Locale(zh, CN)); if (langResult TextToSpeech.LANG_MISSING_DATA) { // 返回给前端离线不可用建议切在线 callback.invoke(new HashMapString, Object() {{ put(code, -2); put(message, offline voice not installed); }}); } else if (langResult TextToSpeech.LANG_NOT_SUPPORTED) { // 当前引擎不支持该语言 callback.invoke(new HashMapString, Object() {{ put(code, -3); put(message, language not supported); }}); }说明LANG_MISSING_DATA与LANG_NOT_SUPPORTED是两种状态。前者语言包缺失但引擎支持引导用户下载语音包即可后者是引擎根本不认这个语言只能切在线或换引擎。插件层把这两个状态区分开前端才能给用户不同提示而不是笼统报失败。4.2 在线合成的音频缓存与流式播放在线合成每次请求都拉音频会浪费流量常见做法是插件内部做一层音频缓存。以文本hash为key第一次请求后把音频文件存到本地二次播报直接命中缓存响应速度接近离线。缓存目录建议放在_doc/tts_cache/下避免占用应用包体空间。function speakWithCache(text, opts) { const key hashCode(text) tts.synthesize({ text: text, engineMode: online, cacheKey: key, audioFormat: mp3 }, (res) { if (res.code 0) { // 返回的 res.path 是本地缓存文件 // 之后用 MediaPlayer 或插件内置播放器放 tts.playFile(res.path, opts) } }) }参数说明audioFormat决定在线接口返回的编码格式MP3通用但解码延迟略高PCM延迟低但文件大。语音导航首字延迟敏感建议PCM流式有声读物对首个字延迟不敏感MP3整段缓存更省事。cacheKey由文本内容决定同一文案反复播报只请求一次网络。4.3 多语言输出与语速音调参数门控中英文混合文本是智能语音助手的高频场景。系统TTS一次setLanguage只锁定一个语言混合文本需要插件做分句切分按空格和中英文边界把文本拆成若干片段每个片段设置对应语言后逐段合成。三个参数的归一化也容易踩坑。系统TTS的speed是0.5~2.0的浮点第三方离线SDK的语速可能是-10~10的档位在线接口可能是百分数。插件层要把这些归一化成统一语义参数范围默认值说明speed0.5 ~ 2.01.01.0为正常语速pitch0.5 ~ 2.01.0音调越高越尖锐volume0.0 ~ 1.01.0合成音频的播放音量languagezh-CN / en-USzh-CN决定语言包和发音人engineModeoffline / online / autoautoauto会先检测语音包再决定auto模式是推荐的默认值插件先做语音包检测离线可用就用离线缺少语音包或语言不支持时自动切在线。对用户来说听感是连续无感切换的。语速参数门控的目的是避免前端把无效值传下去。比如speed传了10系统TTS会直接按最大值处理显得插件不可控所以插件层对超范围参数要做钳制。5. 高频踩坑与落地验证TTS插件进入生产环境前的检查5.1 静音、断音与长文本截断三个坑最常在生产环境暴露。静音问题多数不是插件坏了而是音频焦点被其他App抢占。接入时插件内部要主动请求AudioManager.AUDIOFOCUS_GAIN_TRANSIENT播报结束后释放焦点导航场景用AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK允许音乐降低音量而不是完全中断。断音问题常出现在连续打断重播时前一次播报还没完全stop后一次speak就开始底层引擎会丢掉事件回调。插件里要做防抖收到stop后等onStop回调完成再放行下一条播报。长文本截断则是所有TTS引擎的公共边界。单次合成超过一定长度底层会截断或直接失败。插件应该内置按标点分句的逻辑遇到句号、逗号、问号自动切开逐句合成并排队。这个方法对有声读物章节、长新闻稿播报都能直接生效。5.2 场景参数组合与事件状态机三个典型场景的参数组合可以沉淀成模板场景engineModeinterruptspeed缓存策略智能语音助手autotrue1.0高频指令缓存有声读物offlinefalse0.9整段缓存语音导航onlinetrue1.2流式不缓存落地验证时至少覆盖四组检查关闭网络后播报离线内容开启网络后播报在线内容同一段中英文混合文本连续播报10次快速连发5条interrupt指令观察最后一条是否完整播完。生产环境最终要做的是把start、done、error、stop四种事件收敛成一个state字段前端页面只监听这个state驱动UI。插件的稳定性判断不是看单次播报成功而是看在连续打断、弱网、长文本混读的状态下回调事件是否不丢失、不重复。本文还有配套的精品资源点击获取
返回列表