欧加耳机音质助手

MelodyCodecTweaker 是一个面向 OPPO / OnePlus「无线耳机」App 的 LSPosed 模块。它不会替换系统文件,也不会修改「无线耳机」App 安装包,而是在运行时注入音质控制项,让部分原本藏在系统蓝牙栈里的编解码器、播放质量、采样率和 LE Audio 状态可以直接在耳机控制面板里操作。

模块主要服务于 ColorOS / OPlus 系设备上的 com.oplus.melody,同时配合 com.android.bluetoothcom.oplus.wirelesssettings 作用域完成更稳定的状态读取和写入。

主要功能

  • 在「无线耳机」主面板 DetailMainActivity 注入蓝牙音质区域。
  • 在 OneSpace 快捷面板 OneSpaceDetailActivity 注入同一套控制项。
  • 显示当前协议:SBC、AAC、LDAC、LHDC、LC3 等。
  • 支持播放质量切换,例如 LHDC 的连接优先、均衡、音质优先,以及 LDAC 的 330 / 660 / 990 kbps。
  • 支持采样率切换,根据当前耳机和协议动态显示 44.1 / 48 / 96 / 192 kHz 等可选项。
  • 播放质量和采样率会做联动修正,尽量避免写入蓝牙栈不接受的组合。
  • 支持按耳机记忆选择,重新连接后自动应用上次设置。
  • 支持 LE Audio 开关:开启后进入 LC3,并隐藏经典 A2DP 下的播放质量和采样率选项;关闭后恢复经典蓝牙音频状态。
  • 耳机未连接、当前协议不可控、耳机不支持 Hi-Res 或 LE Audio 时,会隐藏或置灰对应选项,尽量贴近官方控件表现。
  • 提供模块内置诊断页,可以查看作用域加载、页面 Hook、蓝牙桥接、无线设置桥接等状态。
  • 提供一键收集反馈包,便于排查不同手机、系统版本、耳机型号和「无线耳机」版本带来的差异。

使用要求

  • Android 12 及以上。
  • 支持 libxposed API 101 的框架,例如新版 LSPosed。
  • OPPO / OnePlus / ColorOS 系统上的「无线耳机」App:com.oplus.melody
  • 建议启用全部三个 LSPosed 作用域:
    • com.oplus.melody
    • com.android.bluetooth
    • com.oplus.wirelesssettings

com.oplus.melody 负责页面注入和用户交互,com.android.bluetooth 负责更稳定地读写 A2DP 编解码器状态,com.oplus.wirelesssettings 负责调用系统侧 LE Audio 能力。少开作用域可能仍能部分工作,但实时切换、状态回读和 LE Audio 会更容易失效。

安装与启用

  1. 安装模块 APK。
  2. 在 LSPosed 中启用模块。
  3. 勾选上面三个作用域。
  4. 强制停止「无线耳机」、蓝牙相关进程和无线设置,或者直接重启手机。
  5. 打开「无线耳机」App,进入耳机主面板或 OneSpace 面板查看注入项。

如果只想临时停用模块,可以打开桌面图标「欧加耳机音质助手」,关闭模块总开关。关闭后需要重启「无线耳机」进程,宿主页才会完全恢复原状。

内置诊断页

模块桌面入口会显示:

  • 模块总开关。
  • 模块版本、手机型号、Android 版本。
  • 「无线耳机」作用域是否加载。
  • 页面 Hook 是否安装成功。
  • 蓝牙作用域是否加载。
  • A2DP Bridge 是否收到事件。
  • 无线设置作用域是否加载。
  • LE Audio 无线设置桥是否收到事件。
  • 最近 Hook / 写入 / 状态同步日志。

如果出现「页面没有注入」「切换失败」「LE Audio 状态不刷新」这类问题,优先截一张诊断页图,基本可以先判断是作用域没生效、页面 Hook 丢了、蓝牙桥没收到,还是无线设置桥没工作。

一键反馈包

诊断页里的「一键收集反馈包」会生成:

OPlusHeadsetAudioHelper-feedback-YYYYMMDD-HHMMSS.zip

优先保存到:

/storage/emulated/0/

如果系统不允许直接写入根目录,会降级保存到:

/storage/emulated/0/Download/

反馈包包含设备信息、模块版本、相关包版本、诊断状态、最近模块事件、scope.listmodule.prop 和一份 logcat。它不会主动打包用户文件,但 logcat 里可能包含系统日志,请反馈前自行确认是否介意。

LE Audio 说明

LE Audio 开关只会在模块判断当前设备支持时显示。判断来源包括「无线耳机」自身状态、系统蓝牙 UUID、蓝牙侧桥接回传和无线设置侧状态,避免出现「手机支持 LE Audio,但当前耳机不支持」时误显示。

开启流程:

  1. 用户在「无线耳机」主面板或 OneSpace 面板点击 LE Audio。
  2. 模块在当前 Melody Activity 内弹出确认对话框。
  3. 用户确认后,模块再向系统侧作用域发送请求。
  4. com.oplus.wirelesssettings 或蓝牙侧桥接调用系统 LeAudioProfile.setEnabled(device, true)
  5. 蓝牙栈完成切换后回传状态,页面显示 蓝牙音质: LC3
  6. 经典 A2DP 的播放质量和采样率行隐藏。

关闭 LE Audio 时,耳机通常会短暂断开并重新连回经典蓝牙音频。模块会延迟刷新 A2DP 状态,期间页面可能短暂显示等待状态,这是蓝牙栈重新协商造成的。

播放质量与采样率

模块优先读取系统蓝牙栈中的实时能力,而不是硬编码所有耳机档位:

  • 当前协议来自 BluetoothA2dp.getCodecStatus()
  • 播放质量来自 codecSpecific1 能力。
  • 采样率来自 sampleRate bitmask。
  • 写入优先使用 setCodecConfigPreference() 并等待系统广播确认。

写入路径会按能力降级:

  1. Melody 进程内直接反射 A2DP 隐藏 API。
  2. 通过 com.android.bluetooth 中注册的 AIDL bridge 写入。
  3. 对 LDAC / 采样率尝试写入开发者选项 Settings.Global
  4. 最后尝试 root shell fallback。

LHDC 的实时切换更依赖厂商蓝牙栈。模块会先做一次 LHDC priming,再写入目标播放质量 / 采样率组合。如果蓝牙栈拒绝当前组合,模块会尽量自动选择兼容采样率,例如从「均衡 / 48 kHz」切换到「音质优先」时先提升到可用采样率。

兼容策略

「无线耳机」App 经常经过 R8 混淆,直接绑定单个类名非常容易在更新后失效。当前模块做了这些兜底:

  • 优先 Hook Manifest 中相对稳定的 Activity,例如 DetailMainActivityOneSpaceDetailActivity
  • 同时 Hook Melody / COUI / AndroidX 的 PreferenceFragment 形态。
  • 运行时扫描 FragmentManager,查找带有目标 PreferenceScreen 标记的页面。
  • 通过 Preference key、页面结构和可见分类兜底寻找注入点。
  • 从 Intent、Fragment / Activity 字段以及当前 active A2DP 设备解析当前耳机,兼容从系统设置跳转进 DetailMain 的路径。
  • 对没有 Hi-Res、没有 LE Audio、没有对应协议能力的设备做隐藏或禁用处理。
  • 系统侧蓝牙和无线设置也做了多点 Hook,降低系统更新后单点失效概率。

这些策略能覆盖同一大版本内较多小版本更新,但模块仍然依赖厂商私有页面结构和隐藏 API,不是公开 SDK。若「无线耳机」或系统更新后彻底改掉页面结构、资源 key、包名或蓝牙实现,仍可能部分失效甚至完全失效。

给普通用户的建议:

  • 非必要不要频繁更新「无线耳机」App。
  • 尽量固定在已验证可用的版本。
  • 更新前保留旧版 APK,方便回退。
  • 更新后如果模块失效,请提供新 APK、反馈包、手机型号、系统版本、耳机型号和截图。

已知边界

  • 模块只能控制当前耳机和系统已经协商出的 A2DP / LE Audio 能力,不能强行让耳机支持不存在的协议。
  • 不支持 LE Audio 的手机或耳机不会显示 LE Audio 开关。
  • 不支持 Hi-Res 或没有对应页面项的耳机会走备用注入点;如果页面结构完全不同,仍可能无法显示。
  • 系统冻结 com.oplus.wirelesssettings、蓝牙栈重启或耳机重连期间,状态回读可能延迟几秒。
  • 部分厂商蓝牙栈会拒绝特定播放质量 / 采样率组合,模块会尝试联动修正,但不能保证所有组合都能实时生效。

日志排查

调试时可以抓取:

adb logcat -s MelodyCodecLsp:V

常见关键字:

  • evt=scope.host.start / evt=scope.host.context.ready:无线耳机作用域是否加载。
  • evt=preference.fragment.hooked:PreferenceFragment Hook 是否安装。
  • evt=detailmain.activity.hooked:主面板 Activity Hook 是否安装。
  • evt=onespace.activity.hooked:OneSpace Activity Hook 是否安装。
  • evt=mac.resolved:当前耳机地址是否解析成功。
  • detailmain_fallback.injected / hires_anchored.injected / onespace.injected:页面注入是否成功。
  • evt=scope.system.context.ready:蓝牙作用域是否加载。
  • evt=codec.updated.hooks:蓝牙侧编解码器更新 Hook 是否安装。
  • evt=scope.wirelesssettings.context.ready:无线设置作用域是否加载。
  • le.melody.state.recv:LE Audio 状态是否回传到 Melody。
  • write.path:播放质量 / 采样率实际走了哪条写入路径。
  • 切换未生效:蓝牙栈拒绝本次写入或回读未确认。

构建

项目使用 Android Gradle Plugin,目标 Java 17。release 构建当前关闭 R8,方便保留清晰的 Hook 排查路径。

本地构建:

gradle wrapper --gradle-version 8.10.2
./gradlew :app:assembleRelease

输出位置:

app/build/outputs/apk/release/

GitHub Actions 分为两个入口:

  • Build APK:推送 main / master、PR 或手动触发时执行,用于日常开发构建,产物名带 dev 和提交号。
  • Release APK:仅手动触发。它会按 patch / minor / major 或指定版本号自动抬升 versionNameversionCode,构建签名 APK,提交版本号变更,创建符合 Xposed Modules Repo 规则的 versionCode-versionName tag(例如 4-1.2.0),并在 GitHub Release 中写入手填说明和自动生成的提交记录。发布工作流还会把源码、tag 和 APK Release 自动同步到 Xposed-Modules-Repo/xyz.melodylsp.codec,需要在源仓库配置 LSP_REPO_TOKEN secret。

项目结构

app/src/main/
├── AndroidManifest.xml
├── resources/META-INF/xposed/
│   ├── java_init.list
│   ├── module.prop
│   └── scope.list
├── aidl/xyz/melodylsp/codec/bridge/
└── java/xyz/melodylsp/codec/
    ├── MelodyCodecLspEntry.java
    ├── bt/          # A2DP 隐藏 API 反射
    ├── bridge/      # AIDL Parcelable 类型
    ├── diag/        # 诊断事件与反馈包
    ├── host/        # Melody 页面注入与 UI 控制
    ├── leaudio/     # LE Audio 状态、IPC 与无线设置桥接
    ├── storage/     # 按耳机保存记忆项
    ├── system/      # com.android.bluetooth 侧 bridge
    ├── ui/          # 模块内置诊断页和总开关
    └── util/        # 日志

许可

本项目使用 Apache-2.0 License。

Releases