ARTICLE DETAIL

资讯详情

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

Rhythm韵:自制音乐app(1f)——用TaoToken统一Key打通MusicDB与MediaPlayer

Rhythm韵:自制音乐app(1f)——用TaoToken统一Key打通MusicDB与MediaPlayer 1. 从 MusicDB 到 MediaPlayerAndroid 自制音乐 app 本地播放链路怎么跑通做 Android 自制音乐 app绕不开一条最核心的链路MusicDB 扫描本地音频 → MainActivity 列表展示 → 点击条目启动 Service → MediaPlayer 后台播放。这条链路听起来简单但真正动手时很多人会卡在几个地方扫描不到歌曲、点击没反应、Service 启动了却没声音、MediaPlayer 状态报错。我自己在写「Rhythm韵」这个项目时前前后后踩了不少坑尤其是 Service 没在 Manifest 注册导致「播放没声音」那一次查了半天才定位到。这篇文章聚焦本地播放链路把 MusicDB 读取曲目、Service 后台播放、MainActivity 控制这三段串起来给出可直接复制的代码和配置片段。同时我会把 TaoToken 统一 Key 的配置方式也带进来——如果你后续想给 app 加 AI 能力比如智能推荐、语音控制这套 Key 管理方式能省掉很多重复配置的麻烦。适合正在做 Android 音乐播放器、想跑通本地播放流程的开发者也适合想把 AI 能力接进自有工程的人。核心检索词先明确Android 音乐播放器 MediaPlayer Service 本地播放链路。下面按「问题场景 → 前置配置 → 可复制代码 → 验证结果 → 报错排查 → 后续接入」的顺序展开每一步都能跟着做。2. TaoToken 统一 Key 前置配置给音乐 app 预留 AI 接入能力在正式写播放代码之前先把 TaoToken 的统一 Key 配置好。为什么放在前面因为「Rhythm韵」后续大概率会加 AI 功能比如根据听歌历史生成推荐歌单、用自然语言控制播放「播放周杰伦的快歌」。如果等到功能开发时才临时找 Key、配环境容易和播放链路的调试混在一起排查起来更乱。TaoToken 的作用是统一管理多个模型的 API Key你不需要为每个模型单独申请、单独配置。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。注册后在控制台创建 Key就能拿到一个统一的凭证。具体操作路径进入控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 在 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 生成 Key。这个 Key 就是后面所有 AI 请求的凭证。对于 Android 工程我建议把 Key 放在local.properties或BuildConfig里不要硬编码进 Java 代码。下面是一个可复制的local.properties片段# local.properties TAOTOKEN_API_KEYsk-你的统一Key TAOTOKEN_BASE_URLhttps://taotoken.net/api然后在build.gradleapp 模块里读取// app/build.gradle android { defaultConfig { buildConfigField String, TAOTOKEN_API_KEY, \${project.findProperty(TAOTOKEN_API_KEY) ?: }\ buildConfigField String, TAOTOKEN_BASE_URL, \https://taotoken.net/api\ } }这样在 Java 代码里就能通过BuildConfig.TAOTOKEN_API_KEY拿到 Key。注意local.properties默认在.gitignore里不会提交到仓库避免 Key 泄露。如果你用的是 Claude Code 做辅助开发可以配置settings.json指向 TaoToken 的接入点{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的统一Key } }模型 ID 按你实际使用的填比如claude-sonnet-4-20250514。这三件套——Base URL、Key、Model ID——是接入任何 AI 能力的基础先配好后面播放链路调试时就不会被环境问题干扰。提示Key 只创建一次就够多个模型共用。不要在每个功能模块里重复申请统一从BuildConfig读取。3. 可复制配置MusicDB 扫描 Service 播放 MainActivity 控制这一节是核心把三段代码完整串起来。先看 MusicDB 工具类负责从系统媒体库读取音频文件。// MusicDB.java public class MusicDB { public static ListMusic getMp3Infos(Context context) { Cursor cursor context.getContentResolver().query( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER); ListMusic musics new ArrayList(); if (cursor null) return musics; while (cursor.moveToNext()) { Music music new Music(); long id cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media._ID)); String title cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.TITLE)); String artist cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST)); long duration cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.DURATION)); long size cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.SIZE)); String url cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DATA)); int isMusic cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.Media.IS_MUSIC)); if (isMusic ! 0) { music.setId(id); music.setTitle(title); music.setArtist(artist); music.setDuration(duration); music.setSize(size); music.setUrl(url); musics.add(music); } } cursor.close(); return musics; } }注意几个细节cursor用完要close()否则会内存泄漏IS_MUSIC ! 0过滤掉铃声、通知音等非音乐文件DATA字段拿到的是文件绝对路径MediaPlayer 需要这个路径。接着是 MainActivity 里的列表适配器和点击事件// MainActivity 内部类 private class MusicListAdapter extends BaseAdapter { Override public View getView(int position, View convertView, ViewGroup parent) { convertView View.inflate(getApplicationContext(), R.layout.music_item, null); TextView title convertView.findViewById(R.id.tv_title); TextView artist convertView.findViewById(R.id.tv_artist); title.setText(musicList.get(position).getTitle()); artist.setText(musicList.get(position).getArtist()); return convertView; } // getCount / getItem / getItemId 省略 } private class MusicListItemClickListener implements AdapterView.OnItemClickListener { Override public void onItemClick(AdapterView? parent, View view, int position, long id) { if (musicList ! null) { Music music musicList.get(position); Intent intent new Intent(); intent.putExtra(url, music.getUrl()); intent.putExtra(MSG, AppConstant.PlayerMsg.PLAY_MSG); intent.setClass(MainActivity.this, PlayerService.class); startService(intent); } } }常量类AppConstant定义播放指令public class AppConstant { public class PlayerMsg { public static final int PLAY_MSG 1; public static final int PAUSE_MSG 2; public static final int STOP_MSG 3; public static final int CONTINUE_MSG 4; public static final int PRIVIOUS_MSG 5; public static final int NEXT_MSG 6; public static final int PROGRESS_CHANGE 7; public static final int PLAYING_MSG 8; } }Service 里用 MediaPlayer 播放public class PlayerService extends Service { private MediaPlayer mediaPlayer new MediaPlayer(); private String path; private boolean isPause; Override public IBinder onBind(Intent arg0) { return null; } Override public int onStartCommand(Intent intent, int flags, int startId) { if (mediaPlayer.isPlaying()) { stop(); } path intent.getStringExtra(url); int msg intent.getIntExtra(MSG, 0); if (msg AppConstant.PlayerMsg.PLAY_MSG) { play(0); } else if (msg AppConstant.PlayerMsg.PAUSE_MSG) { pause(); } else if (msg AppConstant.PlayerMsg.STOP_MSG) { stop(); } return super.onStartCommand(intent, flags, startId); } private void play(int position) { try { mediaPlayer.reset(); mediaPlayer.setDataSource(path); mediaPlayer.prepare(); mediaPlayer.setOnPreparedListener(new PreparedListener(position)); } catch (Exception e) { e.printStackTrace(); } } private void pause() { if (mediaPlayer ! null mediaPlayer.isPlaying()) { mediaPlayer.pause(); isPause true; } } private void stop() { if (mediaPlayer ! null) { mediaPlayer.stop(); try { mediaPlayer.prepare(); } catch (Exception e) { e.printStackTrace(); } } } Override public void onDestroy() { if (mediaPlayer ! null) { mediaPlayer.stop(); mediaPlayer.release(); } } private final class PreparedListener implements MediaPlayer.OnPreparedListener { private int positon; public PreparedListener(int positon) { this.positon positon; } Override public void onPrepared(MediaPlayer mp) { mediaPlayer.start(); if (positon 0) { mediaPlayer.seekTo(positon); } } } }最后Manifest 注册是必须的Service 不注册不会报错但播放没声音application activity android:name.MainActivity intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity service android:name.PlayerService / /application权限也要加uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE /Android 13 及以上还需要READ_MEDIA_AUDIO权限记得在运行时动态申请。4. 验证请求与成功结果确认播放链路真的跑通代码写完后怎么确认链路真的通了我一般分三步验证。第一步验证 MusicDB 扫描结果。在 MainActivity 的onCreate里加一行日志musicList MusicDB.getMp3Infos(this); Log.d(MusicDB, 扫描到歌曲数量: musicList.size()); for (Music m : musicList) { Log.d(MusicDB, m.getTitle() - m.getArtist() - m.getUrl()); }运行后看 Logcat如果数量为 0说明权限没给或者设备里确实没有音频文件。如果数量正常但url为空检查DATA字段是否被系统限制。第二步验证点击事件和 Service 启动。点击列表条目后Logcat 应该出现D/music--: Music{titlexxx, artistxxx, url/storage/emulated/0/Music/xxx.mp3}同时 Service 的onStartCommand会被调用。可以在play()方法里加日志Log.d(PlayerService, 开始播放: path);第三步验证 MediaPlayer 状态。onPrepared回调触发后mediaPlayer.start()执行此时应该能听到声音。如果没声音先检查设备媒体音量再检查path是否有效。成功的结果是点击列表条目 → Logcat 打印播放路径 → 听到音乐 → 再次点击其他条目能切换歌曲。如果这些都对上了说明本地播放链路已经跑通。注意mediaPlayer.prepare()是同步方法会阻塞主线程。如果音频文件较大建议用prepareAsync()配合OnPreparedListener避免 ANR。5. 本篇常见报错排查401、local proxy failed、reading choices、OAuth虽然这一篇聚焦本地播放但既然配了 TaoToken 统一 Key后续接 AI 能力时大概率会遇到几个典型报错。提前把排查思路写清楚省得你到时候抓瞎。401 UnauthorizedKey 无效或没带上。检查BuildConfig.TAOTOKEN_API_KEY是否为空请求头里是否带了Authorization: Bearer sk-xxx。如果 Key 是从控制台复制的注意不要多复制空格。local proxy failed本地代理配置问题。如果你在 Android 模拟器里请求https://taotoken.net/api模拟器默认走宿主机的网络一般不需要额外代理。如果报这个错检查settings.json或local.properties里的 Base URL 是否写成了http://而不是https://。reading choices 报错通常是响应体解析失败。比如你请求的是对话接口但返回的 JSON 结构里没有choices字段。检查 Model ID 是否填对比如claude-sonnet-4-20250514和gpt-4o的响应结构不同。用模型对话页面 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 先测一下模型是否正常返回。OAuth 相关报错如果你用 Claude Code 或 Codex 接入OAuth 流程可能因为回调地址不对失败。检查settings.json里的ANTHROPIC_BASE_URL是否指向https://taotoken.net/api以及 Key 是否有对应模型的权限。对于 Codex 的auth.json配置格式如下{ base_url: https://taotoken.net/api, api_key: sk-你的统一Key, model: claude-sonnet-4-20250514 }三件套——Base URL、Key、Model ID——缺一不可。如果用了 Cline MCP 或 CC Switch同样检查这三项是否一致。回到播放链路本身最常见的报错还是「Service 没注册」。症状是点击列表没反应Logcat 没有PlayerService的日志但也不报错。这时候去AndroidManifest.xml里补上service android:name.PlayerService /就能解决。我当初就是漏了这一行查了半天。另一个坑是MediaPlayer.setDataSource()传入的路径无效。如果url是content://开头的 URIsetDataSource(String)不支持需要用setDataSource(Context, Uri)。MusicDB 里拿的是DATA字段通常是文件路径但部分设备可能返回 URI建议加个判断。6. 后续接入与 CTA把统一 Key 用在长期编码和 Agent 场景本地播放链路跑通后「Rhythm韵」的基础功能就立住了。接下来如果要加 AI 能力比如根据歌曲特征生成推荐、用语音指令控制播放TaoToken 的统一 Key 就能直接复用。你不需要为每个模型单独配环境一套 Key 走天下。对于长期编码和 Agent 场景比如用 Claude Code 辅助开发整个音乐 app建议用 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 把编码助手的请求统一走 TaoToken方便管理额度和日志。如果只是想先验证模型能不能正常对话去模型对话页面 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 发一条消息试试确认 Key 和 Base URL 没问题。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里面有各语言的示例代码。API Keys 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content Key 丢了或者要换都在这里操作。最后说个实用技巧MediaPlayer 的reset()和release()一定要成对出现。reset()把播放器恢复到初始状态可以重新setDataSourcerelease()释放底层资源释放后不能再调用任何方法。Service 的onDestroy里先stop()再release()避免内存泄漏。播放链路本身不复杂把状态管理清楚后面加功能就顺了。
返回列表