项目进展2026-09-24

MD 阅读 Android 版 · 语音导出(MP3)与菜单/关于页

Android 版落地保存语音 / MP3 导出(未引入 NDK、零新增权限),新增设置弹窗,并把「关于」页与鸿蒙逐项对齐;导出产物经帧级扫描与 ffmpeg 解码双重复核,确认可播放。

版本:1.1.4 (versionCode 1001004) 包名:com.bzm.mdreader.android 验证机:华为 Mate 30 Pro (LIO-AN00) / Android 12 / SDK 31 / 1176×2400 验证日期:2026-09-24 验证方式:USB 真机 + adb 自动化操作 + 离线帧级/解码级复核


一、结论

本轮三项交付全部真机验收通过:

  1. 保存语音 / MP3 导出 —— 从零落地,未引入 NDK、未新增任何权限;产物经四路独立复核确认为合法可播放 MP3。
  2. 菜单「设置」 —— 新增入口与设置弹窗,置于分隔线以下、与「关于」并列。
  3. 「关于」页 —— 与鸿蒙逐项对齐(品牌区 / 定位语 / 可点击署名 / 版本历史 / 蓝色关闭)。

附带完成:把 Android 侧成型的图形渲染层(高度算法与 CSS)同步回鸿蒙版,并修掉同类隐患。


二、MP3 导出:技术路线与关键决策

2.1 原计划被推翻

初版设想是「synthesizeToFile → WAV 拼接 → LAME NDK 编码」。落地前实测发现该路线代价过高,改用更省的方案:

候选方案判定原因
LAME + NDK(原计划)❌ 弃需装 NDK 与 CMake,构建链变长、产物体积翻倍;本机此前未装
系统 MediaCodec❌ 弃只能输出 AAC/M4A,出不了 MP3
WebView + 与鸿蒙同一份 lamejs✅ 采用零新增依赖、零新增权限;编码器与鸿蒙完全同一份源码,音质与参数天然一致

2.2 链路

逐句 synthesizeToFile ──▶ WAV(16kHz/mono/16bit) ──▶ 解析 RIFF/WAVE 定位 data 区
        ▲                                                     │
        │ 在途句数上限 4(流控)                                ▼
        └──────────── 分块 PCM ──▶ WebView 内 lamejs ──▶ MP3 字节流 ──▶ SAF 流式落盘
  • 编码器运行在 WebView:assets/mp3enc.html 加载 assets/lamejs.js(由鸿蒙 libs/lamejs.esm.js 转换 export → window.* 得到),

Kotlin 侧通过 evaluateJavascript 下发 base64 PCM、取回 base64 MP3 分片。

  • 格式锁定在首句:以第一句真实 WAV 头为准确定采样率/声道,后续句格式不一致则跳过(防引擎中途换格式导致音调错乱)。
  • 取消即收尾:点「停止保存」不丢数据 —— 已编码部分 flush 尾部后写完整 MP3 帧流,产出的是可正常播放的短文件,不是损坏文件。

2.3 改动文件

文件说明
core/TtsPlayer.kt新增「仅合成不发声」模式(synthesizeToFile 流水线 + 在途句数流控)
core/Mp3EncoderBridge.kt新增。WebView 编码桥(主线程建 View、后台线程阻塞式调用)
core/AudioExporter.kt新增。WAV 解析 + 分块编码 + 流式落盘 + 取消收尾
assets/mp3enc.html新增。编码页面(PCM 进、MP3 出)
assets/lamejs.js新增。与鸿蒙同源的 LAME 编码器
ui/ReadCard.kt新增保存三态 UI(进度条 / 百分比 / 字节数 / 第 N/M 句)
MainActivity.kt接线 SAF 保存选择器、导出编排、进度回调;保存期间禁用播放
ui/SettingsDialog.kt新增。设置弹窗
ui/AboutDialog.kt新增。关于弹窗 + 版本历史抽取(与鸿蒙同算法)
ui/ReaderScreen.kt菜单补「设置」项与分组分隔线
assets/diag.html删除。图形白屏排查期的诊断页,已无引用,避免随包发布

三、真机验收清单(全部通过)

#验收项结果证据
1保存入口✅点「保存语音」拉起系统保存选择器(CREATE_DOCUMENT typ=audio/mpeg),默认文件名 demo.mp3
2保存态 UI✅按钮转「保存中」、进度条 + 59.43% + 2.7 MB + 「第 104/175 句」,位置行显「保存位置:1」
3保存期间禁播放✅UI dump 显示「开始」按钮 enabled="false"、透明度 0.4
4时间行✅保存中显示 00:00 / 11:45(预估总时长,与鸿蒙同一 4.5 字/秒公式)
5保存完成✅面板复位、弹「已保存为 MP3:<文件名>」
6取消保存✅点「停止保存」→ cancel requested → stopped (synth=true) → released;面板复位、可再次保存
7取消语义✅中断产出 1,562,688 字节部分文件,帧流完整、可播放(见第四节)
8重复导出确定性✅同一文档两次全量导出字节数完全一致(均 6,363,360 字节)
9零权限✅Manifest 仍只有 2 条普通级权限(前台服务×2,供朗读播放用),无 INTERNET、无存储权限
10菜单「设置」✅菜单六项:源码(预览) / 打开文件 / 分享 Markdown / 语音朗读 / ─── / 设置 / 关于
11设置弹窗✅默认项 4 条 + 「更多设置项将在后续版本开放」+「知道了」
12关于弹窗✅M 徽标 + v1.1.4 徽章 + 定位语 + 可点击署名 + 版本历史 + 蓝色关闭
13版本号运行时读取✅弹窗显示 v1.1.4 == versionName == 鸿蒙 app.json5(杜绝硬编码漂移)
14署名跳官网✅点击后由系统浏览器打开官网(com.huawei.browser),截图确认落地页为志睿源官网
15关于关闭✅关闭后 版本历史 节点消失(UI dump 命中数 0)
16回归:源码切换✅切源码态正常显示 Markdown 原文
17回归:分享✅拉起系统分享面板(HwChooserActivity)

四、MP3 产物独立复核(离线,四路交叉)

复核手段结果
逐帧扫描(自写解析器)22,095 帧、坏字节 0、解析终点 恰好等于文件长度(6,363,360)
参数MPEG2 Layer III / 16000 Hz / 64 kbps / 单声道(与请求一致)
ffmpeg 解码00:13:15.42,解出 12,726,720 采样点(= 795.42 s × 16000 Hz,完全吻合)
音量统计mean −21.5 dB / max −2.6 dB → 真实语音(静音会是约 −91 dB)
静音间隙检测全篇 无 > 2 s 断流(只有句间自然停顿)
Android MediaStore 自解析时长 795420 ms —— 与逐帧计算毫秒级完全一致
取消产物5,426 帧 / 0 坏字节 / 03:15.34 / mean −21.4 dB,同样完整可播

字节熵 7.335 / 8,40 个 20 秒分块全部均匀 → 全程真实语音、首尾无缺损。


五、平台差异与取舍(需知情)

项鸿蒙Android处置
导出母带采样率固定 16000 Hz跟随系统语音引擎(实测华为 hiAI = 16000 Hz)代码内置 9 档白名单(8000~48000),不在白名单则明确报错而非静默出错
保存中状态行显示「准备就绪」同保持与鸿蒙一致(未擅自改成「保存中」)
取消时提示「已取消保存」+「已停止,部分语音已保存」双 Toast同与鸿蒙行为一致
编码器lamejs(ArkTS)lamejs(WebView)同一份源码,参数与音质一致

六、图形渲染层同步回鸿蒙版

Android 侧本轮成型的通用改进已同步回鸿蒙 rawfile/graph.html:

  1. 确定性高度算法 —— h = min(自然宽, 可用宽) × vbH/vbW + padding,等价浏览器 max-width:100% + height:auto,与测量时机无关;
  2. viewBox 归一 + fitViewBoxToContent();
  3. body.fullscreen 限定 height:100% / overflow:hidden(原全局写法会导致普通流内容整块不绘制 —— 图形白屏的根因);
  4. 全屏容器 position:fixed 四边归零(百分比高度链在哑页面里已断,改 fixed 才居中)。

接纳下限同步放宽:MermaidView.ets 80 → 24、SvgView.ets 60 → 24(与 Android GRAPH_MIN_H 对齐)—— 这正是 Android 上「7.8 时间线真实高 79 被下限 80 拒掉 → 底部留白 181px」的同一隐患。

两版唯一保留差异:htmlLabels —— Android 必须 false(WebView 下 true 会让整个 SVG 不绘制),鸿蒙保持 true(ArkWeb 无此问题,保留富文本标签能力)。已在两版文件内互相注明「改一处勿误同步另一处」。

静态核对已完成(鸿蒙机本轮未接入,无法编译验收):getInlineHeight() / renderGraph(code, fitWidth) / renderSvg(code, fitWidth) / renderAny 接口均保留, 鸿蒙 MermaidView.ets 的轮询调用与 Index.ets 全屏调用签名逐一对齐,无破坏性风险。


七、权限与合规

  • 未新增任何权限;Manifest 仍为零 INTERNET、零存储、零电话等敏感权限。
  • 导出走 SAF(系统保存选择器),用户自选保存位置,应用不需要任何存储权限。
  • MP3 编码在本地 WebView 内完成,不联网;「零权限 · 零网络 · 零收集」卖点未被破坏。

八、下一步

  1. Release 签名打包 → 华为 AGC 上架(功能已齐,建议先做一轮长文档导出内存复核)
  2. 菜单「隐藏系统栏 / 显示系统栏」(对齐鸿蒙菜单第 3 项)
  3. Android 工程尚未纳管版本控制(C:\bzm\android\mdreader-lite 无 git)——建议尽快 git init 并首次提交,与鸿蒙版对齐
  4. 鸿蒙侧 graph.html 同步后需真机回归一次图形高度与全屏(本轮只做了静态核对)
← 返回文章列表