Skip to content
RuiquanQiaoPublic

About

Murmur — 安卓本地梦话与鼾声检测器。YAMNet on-device,录音不出设备。Material 3,中英双语。

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Murmur · 梦话检测器

一个安卓夜间录音应用:整晚监听,只把说梦话的那几十秒和打鼾的时段存下来,其余时间不留任何音频。 识别用的是 YAMNet(AudioSet 521 类声音分类模型),完全跑在手机本地,录音不出设备。

界面遵循 Material 3 / Material You,中英双语,跟随系统浅色/深色。

它怎么工作

两级流水线,为的是"整夜开着但不烧电":

  1. 能量闸门(每 100 ms,几乎零开销) 持续跟踪房间的本底噪声(向下快、向上慢,所以人声不会把本底抬上去)。 只有当声音比本底高出「触发灵敏度」设定的分贝数时,才唤醒第二级。
  2. YAMNet 判定(闸门打开时每 300 ms 一次) 把 0.975 秒的音频送进模型,得到 521 类分数,再归并成三组:
    • 说话 — Speech / Conversation / Whispering / Babbling / Shout…
    • 呓语 — Laughter / Groan / Sigh / Wail, moan / Humming…
    • 鼾声 — Snoring / Snort / Breathing / Wheeze…

模型加载失败时不会变聋 —— 自动退回纯音量检测(片段标为「声响」)。

说话和鼾声是分开录的

两者的时间尺度差了两个数量级,所以捕获策略不同:

说话 / 呓语 鼾声
判定阈值 人声判定阈值(默认 0.35) 鼾声判定阈值(默认 0.30)
多久安静算结束 2.5 秒 6 秒(呼吸之间本来就有间隔)
单段上限 60 秒 30 分钟
保留的音频 整段 前 10 秒采样

一夜鼾声如果全录下来是几百 MB(16 kHz 单声道 ≈ 115 MB/小时),所以鼾声只留开头一小段做样本, 时间轴仍然记录完整时长——记录页显示的是 鼾声 · 76% · 样本 10.0 秒 配右侧真实时长 28:00, 夜晚卡片顶部还会汇总「打鼾 43 分 · 占全夜 10%」。

每段片段会把触发之前的几秒(环形缓冲里的预录)一起存进去,所以不会切掉第一个字。

界面

三个页面,Material 3 组件(NavigationBar / LargeTopAppBar / Card / ListItem / Switch / Slider):

  • 今夜 — 呼吸光环(外环按 8 小时走一圈,刻度是当晚每次检出,鼾声用第三色)、实时音量条与触发线、说话/打鼾/时长三格统计
  • 记录 — 按夜晚分组的时间线,鼾声汇总,每段可播放 / 分享 / 删除
  • 设置 — 检测、鼾声、录音、外观、存储、系统六组

配色走 M3 色彩角色:说话 = primary,鼾声 = tertiary。 开启「跟随壁纸取色」时两者都来自 Material You 动态取色;关掉则用内置的 Midnight 配色(靛蓝 + 琥珀)。

语言在「设置 → 外观 → 语言」里切,跟随系统 / 简体中文 / English,不用重启应用。

安装

去 Releases 下最新的包,手机上直接点开安装:

文件 说明
murmur-<版本>-arm64.apk 16 MB,2017 年后的手机基本都是 arm64,优先选它
murmur-<版本>-armeabi.apk 15 MB,更老的 32 位机型
murmur-<版本>-universal.apk 28 MB,认不准架构就装这个,任何机型都能装

**只有第一次需要这样装。**装好之后的更新都在应用里完成,见下一节。

装好后第一次点「开始聆听」会要麦克风 + 通知权限,两个都要给 (通知是常驻前台服务的凭据,也是从锁屏直接结束录制的入口)。

务必去「设置 → 系统 → 后台省电限制」把 Murmur 设成不受限制, 否则某些国产 ROM 会在熄屏后掐掉录音。

更新

更新在应用里完成,不用再往手机传文件:设置 → 系统 → 检查更新。 默认也会自己查——每隔几小时最多一次,而且录制期间绝不打扰,可以在同一处关掉。

有新版时弹一个框,写着版本号、更新说明和安装包大小,三个选择:立即更新 / 以后再说 / 跳过此版本。 点更新就下载,下完调系统安装器,确认一下即可。录音和设置原样保留。

第一次更新时系统会多问一句「是否允许 Murmur 安装应用」——这是 Android 对所有自更新应用的要求, 点「去设置」打开开关再退回来,安装会自己接着走,以后不再问。 另外 Play Protect 可能提示「未扫描过此应用」,展开「更多详情」选「不扫描直接安装」即可。

它是怎么知道有新版的

每个 Release 带一个 version.json,GitHub 同时把它挂在一个固定地址上:

https://github.com/RuiquanQiao/murmur/releases/latest/download/version.json

应用只 GET 这一个地址(几百字节),比对里面的 versionCode,再按 Build.SUPPORTED_ABIS 挑出最合身的那个包下载——arm64 手机拿 16 MB 的,认不出来的退回 28 MB 的 universal。 除了这个清单和安装包本身,应用不发任何其他网络请求,录音一如既往不出设备。

新 Release 发布后,latest/download 这个别名大约要 1–2 分钟才指向新标签,属于正常延迟。

⚠️ keystore 丢了就再也没法覆盖安装了。 keystore/murmur-release.jks 和 keystore.properties 都在 .gitignore 里,只存在于本机。 把这两个文件一起复制一份到你放重要资料的地方——网盘、U 盘、密码管理器附件都行。 丢了只能换 applicationId 重新发一个「新应用」,老版本的录音救不回来。

存储

一切都在应用私有目录里,卸载即清空:

files/nights/<nightId>/night.json       会话元数据 + 片段索引
files/nights/<nightId>/clips/<id>.wav   16 kHz 单声道 16-bit PCM

没有数据库 —— 一夜就几条记录,文件夹比半迁移的 schema 更抗崩溃。 默认保留 14 天,可在设置里改成 3/7/30 天或永久。

开发

./gradlew assembleDebug          # 调试包
./gradlew assembleRelease        # 发布包(release key 签名)
./gradlew connectedAndroidTest   # 设备测试,需要已连接的模拟器或真机

签名走 keystore.properties(见 keystore.properties.sample)。 文件不存在时 release 构建会回落到 debug key —— 仍然装得上,但没法覆盖安装正式版。

发布新版本

  1. app/build.gradle.kts 里把 versionCode +1、versionName 改掉。 versionCode 忘了加,谁都不会提示更新——应用和 Android 都只认它。
  2. 把中英文更新说明各写进一个 UTF-8 文本文件,然后:
bash tools/release.sh notes-zh.txt notes-en.txt

脚本会构建、按 ABI 改好名、生成 version.json、一并推上 GitHub Releases。 标签已存在时它会拒绝执行——因为同名资产被覆盖后 CDN 会继续吐旧副本好几分钟, 所以永远发新标签,不要复用。

技术栈:Kotlin + Jetpack Compose,Material 3 1.4.0(当前 stable; Expressive 组件还在 1.5.0-alpha,不适合出正式包),TensorFlow Lite 2.17,minSdk 26 / targetSdk 36。

设备测试

app/src/androidTest 里的 8 个测试直接把音频喂进 DetectionEngine,绕过麦克风, 覆盖整条「闸门 → YAMNet → 存片段」的链路:

  • 模型能从 assets 加载,输出 521 类
  • 真实语音片段(speech_16k_mono.wav)人声分数 > 0.5
  • 完整管线对语音产出一条 TALK 片段,且 wav 文件非空
  • 开启鼾声检测后,真实语音仍归入 TALK 而不是 SNORE
  • 说话片段不被截断:audioMs == durationMs
  • 白噪声不误判成说话
  • 440 Hz 纯音不误判成说话
  • 整夜安静不产出任何片段

三个踩过的坑

  • MediaPipe tasks-audio 只发 ARM 原生库(没有 x86/x86_64), 在 x86_64 模拟器上必然 UnsatisfiedLinkError。所以这里直接用 org.tensorflow:tensorflow-lite, 四个 ABI 都有,也更小。
  • XNNPACK(TFLite 2.16 起默认开启)在这张图上会 SIGSEGV, 所以 Interpreter.Options 里显式 setUseXNNPACK(false)。 参考内核跑一个 0.975 s patch 只要几十毫秒,而检测器最多 3 Hz,完全够用。
  • Kotlin 的块注释会嵌套:KDoc 里写 clips/*.wav,那个 /* 会被当成嵌套注释开头, 整个文件从此解析失败,报错却指向文件末尾。

模型来源

app/src/main/assets/yamnet.tflite 来自 https://storage.googleapis.com/mediapipe-models/audio_classifier/yamnet/float32/1/yamnet.tflite, yamnet_labels.txt 是从该文件内嵌的 metadata 里解出来的 yamnet_label_list.txt。

About

Murmur — 安卓本地梦话与鼾声检测器。YAMNet on-device,录音不出设备。Material 3,中英双语。

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages