Skip to content

Latest commit

 

History

27 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

android-claude-music 🎵

和你的 AI 一起听歌 —— 它自己会给你点歌

不用你开口。 你连着耳机、手头不忙、时间也对的时候,它自己挑一首放起来。 你收藏里那几十上百首,它一次读得完;整个曲库,它也点得到。

放歌全程在虚拟副屏上,不占你的屏;你没叫它的时候,还会挑一句歌词递过来,跟你聊那一句 (你耳机里唱到「不然怎么会挑中我呢」的那一刻,它手上就是这一句)。

它的技术底子在两头。 读"你在听什么",它不问平台 —— 直接读系统自己的播放状态 (adb 读 media_session),所以不管你在哪个 App 里放,它都知道放的是哪首、播到了第几秒。 点歌也不走平台接口,而是在自建的虚拟副屏上像人一样去点。

(歌单和歌词是另一条路 —— 走本机的接口服务,见准备工作。)

🎧 实测走 酷狗 · 免 Root 免电脑 · AI 跑在手机 Termux 本地 · 放歌走虚拟副屏不占屏

🔑 先决条件:你的 AI 得先住进手机、再摸到手机

  • ① 还没在手机上跑起 Claude Code?→ android-claude-wechat:一条命令装好 Claude Code + 运行环境,免电脑免 Root
  • ② 装好了但 AI 摸不到手机?→ android-claude-agent:免 Root 免电脑免 WiFi,给 AI 装上 adb 手脚
  • ③ 两个都好了,回到本仓库 —— 让 AI 坐到你旁边,一起听

三个仓库是一条线:住进来 → 摸得到 → 一起听

Gitee GitHub License


能干嘛

🎯 它自己会给你点歌 —— 不用你开口

你连着耳机、手头不忙、时间也对,它自己挑一首放起来 —— 不是定时任务在响,是它真在听。

怎么放的:平台侧的验证码拦着纯接口选歌,所以它走的是"像人一样去点"这条路 —— 在副屏上归位、开搜索、灌歌名、点中结果。结果行的坐标是当场从控件树里读的, 不写死一个像素,换台手机换套主题照样点得中。你收藏里那几十上百首它一次读得完; 整个曲库,它也点得到。你想点也行:"放《桃子》",一句话它就去点。

📻 而且不占你的屏

音乐 App 一启动就抢前台、你正看的被顶走 —— 这事它不干: 歌在虚拟副屏上放(am start --display 丢进自己建的那块屏),声音照常从你耳机出来, 你手里这块屏一动不动

虚拟副屏的根是别人的开源项目(agent-mobile-use)—— 我们在免 Root 的手机上把它跑通了,走过的弯路和改法写在 TUTORIAL 里,见鸣谢

💬 你听到哪一句,它知道

不是"知道歌名"那种,是知道你此刻唱到哪个字:把歌词时间轴跟你手机上的播放进度做对齐。

难在 dumpsys media_session 给的是快照 —— 实测跟真实进度差过 39 秒, 得按 /proc/uptime 把这段漂移补回去,它报的那句才跟你耳朵里的一致。 所以它开口,是在对的那一句上开口:

♪ 歌词: 《桃子》唱到「[0:19] 不然怎么会挑中我呢」
   [规则: 有意思就接一句聊,没感觉就放着不吭声;一首歌最多两句]

适合谁

  • 想让 AI 陪自己听歌的人 —— 既能点歌,也有旁边那个真在听的人
  • 想给 AI 装"主动说话"能力的人 —— 本仓库的轮询机制是通用的,听歌只是它的第一个应用(见第三层
  • 已经在用前两个仓库的人 —— 这是三部曲的第三棒,接得上

准备工作

需要 怎么来
Claude Code 跑在手机 Termux 里 android-claude-wechat
adb 回环(AI 的手脚) android-claude-agent
Node.js(跑本地音乐接口服务) pkg install nodejs,或前两个仓库已带
Python 3 Termux 自带
一个音乐 App + 你自己的账号 实测走酷狗。换平台要另找对应的本地接口服务(见 第一步

安装

bash <(curl -sL https://gitee.com/xvxv663/android-claude-music/raw/master/install.sh)

装完你会得到:

~/.claude/skills/kugou-music/      读收藏 · 自己挑歌放 · 报"此刻唱到哪句"
~/.claude/skills/listen-together/  陪听规则(写给 AI 看的)
~/.claude/skills/music-control/    什么时候该放(判断层)
~/.claude/scripts/listen-loop.sh   轮询:此刻唱到哪句 → 递给 AI
~/.claude/scripts/listen-detect.sh 轮询:这会儿该不该自己放一首
~/.claude/scripts/music_moment.sh  放一首(带 40 分钟冷却)
~/.claude/scripts/vd.sh            虚拟副屏(放歌不占屏)

装完还有一步得你亲手做:登录你自己的音乐账号

本仓库不内置任何账号。本地音乐接口服务要用你自己的账号登录一次,它才知道"我喜欢"里有什么。 登录态存在你自己手机里,不出手机、不上传。

手机号 + 验证码登录一次即可,步骤见 TUTORIAL.md


在你手机上要改的

每个人的手机都不一样 —— 概率、频率、时间窗、用哪个 App、屏幕上点哪儿, 全是给你自己调的。下面这些都在文件顶部的变量里(或者 music.env),打开就能改:

想改什么 改哪儿 默认
用哪个音乐 App music.envMUSIC_PKG com.kugou.android
你的歌单 music.envMUSIC_LIKE_ID 必填,见 TUTORIAL 第四步
它自己放歌的概率 listen-detect.shCHANCE 2%
什么时段才放 listen-detect.shHOUR_FROM / HOUR_TO 8 ~ 23 点
哪些 App 算"你在忙" listen-detect.shENT_APPS 抖音/快手/B站/爱奇艺…
歌词递话的频率 listen-loop.shGAP_MIN / GAP_MAX 30 ~ 80 秒(随机)
一首歌最多聊几句 listen-loop.shMAX_PER_SONG 2
放完一首多久不再放 music_moment.shMUSIC_COOLDOWN_MIN 40 分钟
副屏开多大 vd.sh dstart 宽 高 dpi 1280x720/240
副屏上那组坐标 kugou.sh 顶部 *_X / *_Y 7 个,装完先量一遍,见下一节
副屏上点哪儿 vd.sh tap X Y(每次现给) 每台机器都不一样

换成别的音乐 App

# 1. 拿包名(把关键词换成你要找的 App)
adb shell pm list packages | grep -i kugou
#    → package:com.kugou.android

# 2. 填进 music.env
MUSIC_PKG=com.kugou.android

⚠️包名只管一步,另两件事它管不了:

  • 歌词 —— 读歌词走的是本地接口服务(默认那个是酷狗的)。换平台,得找对应的本地接口服务; 没有的话,"一起听"就只剩一半:它知道你听什么,但跟不上唱到哪句。 (好消息是读播放状态和放歌都走系统媒体接口,跟 App 无关,换谁都能用。)
  • 坐标 —— 每个 App 副屏上的按钮位置都不一样。vd.sh tree 读控件树的坐标和文字, vd.sh shot 截图看着量。别抄别人博客里的坐标,那是别人手机的。

坐标要自己量一次

kugou.sh 顶部有一组坐标(7 个),是我按自己的手机量的。换台机器、换个分辨率, 甚至 App 升个小版,它们都可能漂。

漂了不报错——它只是点空:脚本照样报"放上了",实际放的是别的歌。所以装完先量一遍。

怎么量:在副屏上走到那个页面,读控件树,拿元素的框,取中心点。

bash scripts/kugou.sh play            # 1. 把 App 丢进副屏(跟平时一样)

bash scripts/vd.sh tree | grep '搜索'  # 2. 读树,找你要的那一行
# → 208 FrameLayout "搜索 ..." 478,136,666,186 ...
#                                  └─ 这四个数是 x1,y1,x2,y2

# 3. 中心点 = ((x1+x2)/2, (y1+y2)/2) = ((478+666)/2, (136+186)/2) = (572, 161)
#    填进 kugou.sh 顶部的 SEARCH_X / SEARCH_Y,其余六个同理

哪个坐标管哪一步,脚本里每个变量后面都写了注释。改完不用重启什么,下次跑就生效。

七个坐标分别在哪一屏(照着走到那页再量):

变量 在哪个页面
TAB_HOME_X/YTAB_MINE_X/Y 底部导航(任何主页面都有)
SEARCH_X/Y 首页顶部搜索框
IME_DONE_X/Y 键盘弹出来时,它右上角的「搜索」键(各输入法位置不同)
HIT_PLAYALL_X/Y 搜完出来的结果页,「单曲」那排的「全部播放」
LIKE_CARD_X/Y 「我的」页里「我喜欢」那张卡片(点上半部,下半跟播放条打架)
FAV_PLAYALL_X/Y 「我喜欢」歌单页顶部的「播放全部」

懒得一个个量kugou.sh 里有现成的量尺函数,按控件 id 或界面文字直接算中心点—— 坐标漂了(比如 App 换了版)用它重新量最快:

id2xy mrs            # 按 resource-id 找(搜索框是 mrs)
tree2xy '"我喜欢'     # 按界面上的文字找

日常放歌不会用它们:读一次控件树要十几秒,每一步都读等于白等。所以平时走上面那组 写死的坐标——快,只在量坐标和兜底的时候才读树。

频率类的东西乘起来看:轮询跑得越勤 × 概率越高 = 放得越频繁。 默认 listen-detect.sh 几分钟一轮 × 2%,一天大概也就放个一两次。


它是怎么做到的

三层。前两层是"听得见、听得懂",第三层才是这个仓库真正的骨头 —— 主动

第一层 · 听得见:adb 读播放状态

Android 系统里有个东西叫 media_session,所有正在播放的媒体状态都记在这儿:

adb -s 127.0.0.1:5555 shell dumpsys media_session
#   description=桃子 - 吴琳珂Moske
#   state=PlaybackState {state=PLAYING(3), position=149000, updated=..., ...}

歌名、歌手、播放状态、已经播到第几毫秒 —— 一行全有。这是全部的地基。

⚠️ 这一层要 adb 权限,也就是第二个仓库干的事。 没有它,AI 连"你在听歌"都不知道。

第二层 · 听得懂:把进度对齐到歌词

有了"播到第 149 秒",还得知道这 149 秒对应哪句词。三步:

① 拿歌词 —— 本地音乐接口服务按歌名配到这首歌,取回带时间轴的 LRC:

[00:13.00] 指尖触碰我的外壳
[00:19.00] 不然怎么会挑中我呢

② 对齐 —— 找 时间轴 ≤ 当前进度 的最后一句,就是"此刻"那句。

③ 补漂移 —— 这一步最容易翻车。dumpsys 给的是快照,不是实时的:

# 快照里的 updated(系统时钟,毫秒)跟"现在"差了多少,就往进度上补多少
now_ms = /proc/uptime × 1000
drift  = now_ms - updated
pos    = (position + drift) / 1000

实测这个差过 39 秒 —— 不补的话,你耳朵里早唱过去了,它还在报上一句。

再加一个提前量:它打完字递到你眼前,歌已经又走了几秒。默认往前顶 8 秒,报的才是"你会听到的那句"。

第三层 · 说得出:轮询(主动说话的骨架)

到这里它已经听得见、也听得懂了,但还是被动的:你不问,它不说。

要让它主动,先得认清一件事:AI 不是一直醒着的。

Claude Code 这类 Agent 一次只活在"一轮对话"里 —— 你发消息,它醒;回完,它睡。 所以"主动"不是让它一直盯着屏幕,而是给它装一个叫醒它的东西

这就是轮询:一个常驻的小脚本,隔一阵看一眼世界;有值得说的,就往会话里丢一行。 AI 被这一行叫醒,读到内容,自己决定开口还是闭嘴。

歌词这一路长这样:

# listen-loop.sh 的核心
GAP=$(( RANDOM % 50 + 30 ))                # 下次隔 30~80 秒
LP=$(python3 lyric_now.py --auto)          # 报"此刻正好唱到的那句"
[ -n "$LP" ] && echo "♪ 歌词: $LP [规则: 有意思就接一句聊,没感觉就放着不吭声]"

三个设计要点,全都是踩出来的:

① 时机随机,别用固定间隔。 固定每 60 秒问一次 → 同一首歌听十遍,十遍都在同一句开口,机械得像个闹钟。 改成随机 30~80 秒瞟一眼 → 随的是时机,所以同一首歌每次听,撞上的句子都不同。

② 一首歌最多开口两次。 脚本不知道什么叫"有意思",但它得管住频率 —— 四分钟一首歌,不设上限它能递四十次,把对话淹了。

③ 脚本只递话,判断权留给 AI。 规则跟着事件一起送到 AI 眼前([规则: 有意思就接一句聊…]),接不接是它自己的事。 "这句有没有意思"是主观的 —— 脚本挑不出来,AI 挑得出来。

💡 这套轮询是通用的,不只用来听歌。 你切到别的 App、电量掉到 20%、走了多少步、半夜屏还亮着 —— 任何"值得它主动开口"的事都能挂进同一条循环。歌词只是第一路。 想接进你自己的 AI:把 listen-loop.sh 抄进你那个常驻轮询里,或者单独挂一个。


放歌:不占你屏幕的那条路

"陪听"还有另一半:它放给你听。这里有个坑 —— 音乐 App 一启动就抢前台, 你的屏当场被顶走,正在聊的、正在看的全断。

解法是虚拟副屏:让系统多开一块看不见的屏,把音乐 App 丢进去跑。 声音照常从耳机出来,你的主屏一动不动。

vd.sh dstart                              # 开一块副屏
vd.sh open com.kugou.android              # 把 App 丢进副屏(不是主屏!)
adb shell input -d <副屏id> keyevent 126  # 播放 / 暂停

一句话原理am start --display 只对自己建的屏放行 —— 系统要看这块屏是不是你建的。 所以 settings put global overlay_display_devices 开出来的那种系统屏,shell 永远吃 SecurityException,得自己建一块。 两个 id 也别搞混:input -d 用逻辑 id,screencap -d 要 SurfaceFlinger 的 64 位 id,而且每次都变 —— 必须现取现用。

这条路是站在别人肩膀上走的,完整交待在鸣谢TUTORIAL 里。 本仓库只用到它"把 App 丢到别处"这一件事;副屏的开关、读树、点击都在 scripts/vd.sh 里,拿得走。


它自己会放歌

前面讲的都是"它在听"。这一节是"它主动放" —— 没人叫它,它自己挑时候给你放一首。

listen-detect.sh 每跑一轮判五件事:

判什么 怎么判 为什么
屏幕亮着吗 dumpsys powermWakefulness 黑着屏放歌没意义
在时间窗里吗 默认 8:00~23:00 半夜不吵你
蓝牙耳机在线吗 dumpsys audiobt_a2dp 没耳机等于外放,别放
正在娱乐 App 里吗 dumpsys activity 读前台包名 你正在刷视频看剧,别插一脚
概率命中吗 默认 2% 见下

前面五关判的是"这会儿能不能放";概率管的是另一件事 —— "放多了会不会烦"

条件全中的时刻,一天里其实很多(戴着耳机坐着的时候都算)。每次都放,就成了骚扰。 所以再掷一次骰子,大多数时候它按兵不动 —— 一天顶多冒出来一两次,那才叫惊喜。

命中之后它做两件事:放一首(转 music_moment.sh → 走副屏、不占屏,带 40 分钟冷却), 然后把这件事报给 AI —— 这才是"主动说话"的另一半:

♪ 放歌: 桃子 - 吴琳珂Moske [规则: 这一首是你自己放的,不是她点的 ——
  ①别问她"要不要听歌",想说什么就一句,像随手放了首歌
  ②她正忙着/刚被吵醒就当没这回事,别硬搭话
  ③歌名照上面这行念,读不到就别提歌名,别编]

规则跟着事件一起送到 AI 眼前 —— 这一条是整套东西的关键: 脚本不知道"这时候该说什么",AI 知道。所以脚本只把事实和边界递过去(谁放的、别问什么、别编什么), 开口说什么、开不开口,全是 AI 的。

(读法提示:上面那段里 AI 是"你"、你是"她"——因为那是脚本写给 AI 看的话,不是写给你的。)

挂进轮询(跟 listen-loop.sh 并列,隔几分钟一轮就够):

bash ~/.claude/scripts/listen-detect.sh

想改成"只报告、放不放由 AI 定":把脚本最后那段 music_moment.sh 去掉,只留 echo


出问题了?

读不到歌名 / 报"没在放歌"

  • 先确认 adb 在线:adb -s 127.0.0.1:5555 shell echo ok
  • 确认真的在放:adb shell dumpsys media_session | grep -m1 description=
  • 有些播放器不往 media_session 里写状态 —— 换系统自带的或主流音乐 App 试

报的句子总是慢半拍 / 快半拍

  • 提前量在 lyric_now.py 里,默认 8 秒:python3 lyric_now.py 12 可以临时改
  • 觉得一直对不上,先怀疑漂移没补:/proc/uptime 那条读不到就会退化成快照时间

歌词是空的

  • 这首歌本地接口没配到歌词。字段还不固定(有的给 lyrics、有的只给 base64 的 content),脚本三种都兜了
  • 小众歌、纯音乐、翻唱版本容易缺

说"放某首歌"没放成 / 放的不是那首

  • 脚本点结果行前会现读控件树找含歌名的那一行;找不到就是搜索没出结果 —— 先 vd.sh tree 看看树上有没有那首
  • 别走"歌单页右上角搜索"那条:输入法的全屏提取框会盖住上半屏,结果行点不动(点哪儿都被输入法吃掉)。走主界面顶部的搜索
  • 全流程约 30 秒(要归位、要搜索、要等页面),慢是正常的

接口服务连不上(4000 端口)

  • 服务挂了:进 ~/kugou-music-api 重新 PORT=4000 node index.js
  • 登录态掉了(隔太久没用):重新登录一次

副屏起不来 / 放歌还是切屏

  • vd.sh id 看两块屏在不在;不在就 vd.sh dstart
  • App 进程还活着的时候,启动请求会被复用到老 task(主屏) —— 所以 vd.sh open 内部先 force-stopam start,这一步不能省

Termux 被系统杀后台 —— 关掉 Termux 的电池优化,部分机型还要在最近任务里锁定。

更多问题 → Gitee Issues


技术原理与风险说明

  • 数据全在本地:播放状态从你自己手机上读,歌单/歌词走本机跑的服务,不上传任何第三方。全开源可审计。
  • 权限在你手里:装上之后 AI 能读你的屏幕状态、能操作手机 —— 这份权限随时可以断(关掉 adb 回环、停掉服务即可)。
  • 第三方接口:本仓库用到的 KuGouMusicApi第三方逆向项目,用于读取你自己账号的公开歌单信息。仅供学习交流,不要用于商业用途或传播版权内容,音乐版权归各平台与版权方所有。
  • 虚拟副屏吃性能:多开一块屏会让机器发热、耗电,不用的时候收掉(vd.sh dstop)。
  • 本项目仅供学习交流,使用者自行评估风险。

鸣谢

  • AcidGr/agent-mobile-use酸小明,MIT)—— 虚拟副屏的根。我们在没有 root 的手机上把它跑起来了(走 Shizuku + adb 这条路),过程中改的几处代码都在 src/ 里。本仓库 scripts/vd.shassets/*.dex 源自这个项目(含我们的修改),版权归原作者所有,MIT 许可全文见 assets/LICENSE-agent-mobile-use
    项目在持续更新,请以原仓库为准。
  • scrcpy(rom1v)—— --new-display 那条路,以及 FakeContext 那个关键解法(Context 上报的包名必须跟调用者 uid 对得上)都是从它那儿学的
  • KuGouMusicApi(MakcRe)—— 本机音乐接口服务,读歌单和歌词靠它
  • Termux —— Android 上的 Linux 终端
  • Shizuku —— 免 Root 系统权限
  • claude-code-android 与 Termux 部署 Claude Code 的先驱者们 —— 前两个仓库铺的路

License

MIT —— 拿走用,署名随意。第三方组件版权归各自作者(见鸣谢)。


🗺 下一步

  • 更多播放器适配:网易云 / QQ 音乐 / 本地播放器的 media_session 差异对照
  • 歌词源可换:现在绑本地接口,想接 LRCLIB 这类公开歌词库
  • 不止歌词:把"听到什么"扩展到播客、有声书、视频 —— 同一套时间轴对齐
  • 情绪感知:连着听三首慢歌,它该不该问一句"今天怎么了"
  • 选歌走 API 那条路:现在是副屏上搜 + 点(能用但慢,约 30 秒)。 平台侧的滑块验证码挡着纯 API 选歌 —— 等哪天绕得过去,就能一句话秒切

💡 有想法?去 Issues 提。

About

和你的 AI 一起听歌 —— 它自己会给你点歌,不用你开口。放歌走虚拟副屏不占你的屏;AI 跑在手机 Termux 本地,adb 直读播放状态,免 Root 免电脑,实测走酷狗。

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages