尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
使用MediaStore.Audio获得手机中的音频文件:TaoToken统一Key通道下的Android音频读取实战
1. 从手机里把音频捞出来MediaStore.Audio 到底能做什么如果你正在做一款 Android 端的音乐播放器、语音备忘录整理工具或者想把手机里录的会议音频批量丢给 AI 做转写和分类第一个绕不开的问题就是怎么把手机里的音频文件找出来很多刚接触 Android 多媒体开发的朋友第一反应是遍历/sdcard目录用File.listFiles()递归扫描。这条路在 Android 10 之后基本走不通了——分区存储Scoped Storage把外部存储的访问权限收得很紧你直接读/storage/emulated/0/Music大概率拿到空列表或者直接抛SecurityException。就算在旧版本上能扫到文件速度也慢得离谱用户手机里几千个文件递归一遍ANR 就找上门了。正确的做法是用系统提供的多媒体数据库MediaStore。你可以把它理解成 Android 系统在每次开机时自动维护的一张「媒体索引表」所有被系统识别到的图片、音频、视频都会登记在册。你不需要自己去翻文件系统只要拿着ContentResolver去查这张表就行。查出来的每一条记录都带着标题、歌手、时长、MIME 类型、文件大小、路径等元数据拿来直接用。MediaStore.Audio就是这张表里专门管音频的那部分。它分两个 URIMediaStore.Audio.Media.EXTERNAL_CONTENT_URI外部存储用户自己拷进去的歌、录音MediaStore.Audio.Media.INTERNAL_CONTENT_URI内部存储系统铃声等实际开发里两个都要查因为用户可能把音频放在任意一个位置。查询的核心是Cursor它就像数据库查询返回的结果集游标你通过moveToNext()一行行往下读用getColumnIndexOrThrow()拿到列索引再取值。这篇文章我会带你走完整条链路从权限配置、封装查询帮助类到把音频元数据整理成列表再通过TaoToken 统一 Key 通道把数据上传给 AI 服务做分类和转写。TaoToken 在这里扮演的是一个「统一入口」的角色——你不用为每个模型单独申请 Key、记不同的 Base URL一个 Key 就能调通多家模型对做音频转写这种需要反复试不同模型的场景特别省事。适合谁看有基本 Android 开发经验、能看懂 Java/Kotlin、想快速把本地音频接进 AI 能力的开发者。下面所有代码都可以直接复制进项目跑。2. 前置准备权限清单与 TaoToken 统一 Key 通道配置在写查询代码之前先把两件事搞定权限和AI 通道。这两块没配好后面代码写得再漂亮也跑不起来。2.1 权限配置清单Android 的音频读取权限随着版本变化改过好几次你得按版本分别声明。打开AndroidManifest.xml加上这些manifest xmlns:androidhttp://schemas.android.com/apk/res/android !-- Android 12 及以下读外部存储 -- uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE android:maxSdkVersion32 / !-- Android 13 (API 33) 及以上细分媒体权限 -- uses-permission android:nameandroid.permission.READ_MEDIA_AUDIO / !-- 如果要读取录音文件可能还需要 -- uses-permission android:nameandroid.permission.RECORD_AUDIO / application ... ... /application /manifest注意READ_EXTERNAL_STORAGE上的android:maxSdkVersion32这是关键。Android 13 开始这个权限被拆成了READ_MEDIA_AUDIO、READ_MEDIA_IMAGES、READ_MEDIA_VIDEO三个细分权限如果你不加maxSdkVersion在 Android 13 上申请旧权限会被系统忽略查询直接返回空。运行时申请权限的代码Kotlin 写法Java 同理private fun requestAudioPermission() { val permission if (Build.VERSION.SDK_INT Build.VERSION_CODES.TIRAMISU) { Manifest.permission.READ_MEDIA_AUDIO } else { Manifest.permission.READ_EXTERNAL_STORAGE } if (ContextCompat.checkSelfPermission(this, permission) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, arrayOf(permission), REQ_CODE) } else { // 已有权限直接查询 loadAudioFiles() } }这里有个我踩过的坑Android 13 上如果你只申请了READ_MEDIA_AUDIO但用户手机里音频是通过某些 App 下载到私有目录的MediaStore 里可能查不到。这种情况只能通过 SAFStorage Access Framework让用户手动选目录不在本文范围内。2.2 TaoToken 统一 Key 通道音频元数据整理好之后我们要把它发给 AI 做分类比如按「会议录音/音乐/播客」打标签或者转写。这里用 TaoToken 的统一 Key 通道好处是你只需要维护一个 Key就能切换不同模型。先去官网注册并拿到 Keyhttps://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content登录后在控制台的 API Keys 页面创建一个 Keyhttps://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content拿到形如sk-xxxxxxxx的 Key 之后记下两个地址Base URLhttps://taotoken.net/api注意这个不加 UTM 参数是纯 API 端点模型对话入口https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content如果你打算长期做编码类、Agent 类的调用可以看下 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content接入文档在这里遇到参数问题先翻它https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content为什么用统一 Key 而不是直连各家我实测下来做音频转写时经常要在 Whisper 类模型和通用大模型之间切换——有的音频方言重通用模型识别不准有的音频是纯音乐需要模型判断类型而不是转写。如果每家都单独申请 Key、记不同的请求格式光配置就能耗掉半天。统一通道把鉴权和路由都收拢了你只改model字段就能换模型。3. 可复制配置ContentResolver 查询与元数据封装这一节是核心给你能直接跑的完整代码。先看查询逻辑再看数据封装。3.1 查询音频的完整代码MediaStore.Audio.Media提供了大量列常用的有这些列名含义类型DISPLAY_NAME文件名StringTITLE歌曲标题StringDURATION时长毫秒intARTIST歌手/艺术家StringMIME_TYPEMIME 类型StringSIZE文件大小字节longDATA文件绝对路径已废弃但可用StringALBUM专辑名StringIS_MUSIC是否为音乐int封装一个帮助类把外部存储和内部存储都查一遍public class MusicHelper { public static ListMusic getAllSongs(Context context) { ListMusic songs new ArrayList(); // 查询外部存储 Cursor external context.getContentResolver().query( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, null); parseCursor(external, songs); // 查询内部存储 Cursor internal context.getContentResolver().query( MediaStore.Audio.Media.INTERNAL_CONTENT_URI, null, null, null, null); parseCursor(internal, songs); return songs; } private static void parseCursor(Cursor cursor, ListMusic out) { if (cursor null) return; try { while (cursor.moveToNext()) { String filename cursor.getString(cursor.getColumnIndexOrThrow( MediaStore.Audio.Media.DISPLAY_NAME)); String title cursor.getString(cursor.getColumnIndexOrThrow( MediaStore.Audio.Media.TITLE)); int duration cursor.getInt(cursor.getColumnIndexOrThrow( MediaStore.Audio.Media.DURATION)); String artist cursor.getString(cursor.getColumnIndexOrThrow( MediaStore.Audio.Media.ARTIST)); String mimeType cursor.getString(cursor.getColumnIndexOrThrow( MediaStore.Audio.Media.MIME_TYPE)); long size cursor.getLong(cursor.getColumnIndexOrThrow( MediaStore.Audio.Media.SIZE)); String path cursor.getString(cursor.getColumnIndexOrThrow( MediaStore.Audio.Media.DATA)); out.add(new Music(filename, title, duration, artist, mimeType, size, path)); } } finally { cursor.close(); } } }几个关键点解释一下query()的五个参数分别是 URI、要返回的列null 表示全部、WHERE 条件、条件参数、排序。生产环境建议把第二个参数写成具体的列数组而不是 null这样能减少内存占用——MediaStore 的列有几十个全查出来很浪费。getColumnIndexOrThrow()比getColumnIndex()好前者列不存在会直接抛异常帮你早发现问题后者返回 -1你取值时可能拿到莫名其妙的数据。cursor.close()必须放在 finally 里Cursor 是系统资源不关会泄漏查几次之后可能就报CursorWindowAllocationException。3.2 数据类封装public class Music { private String filename; private String title; private int duration; private String artist; private String mimeType; private long size; private String filePath; public Music(String filename, String title, int duration, String artist, String mimeType, long size, String filePath) { this.filename filename; this.title title; this.duration duration; this.artist artist; this.mimeType mimeType; this.size size; this.filePath filePath; } // getter / setter 省略IDE 一键生成 public JSONObject toJson() throws JSONException { JSONObject obj new JSONObject(); obj.put(filename, filename); obj.put(title, title); obj.put(duration_ms, duration); obj.put(artist, artist); obj.put(mime_type, mimeType); obj.put(size_bytes, size); return obj; } }toJson()方法是为了后面上传给 AI 用的把元数据序列化成 JSON。3.3 调用方式在 Activity 或 Fragment 里ListMusic allSongs MusicHelper.getAllSongs(this); Log.d(MusicScan, 共扫描到 allSongs.size() 个音频文件); for (Music m : allSongs) { Log.d(MusicScan, m.getTitle() - m.getArtist() ( m.getDuration() / 1000 s)); }this就是 Activity 的 ContextgetContentResolver()从它拿。3.4 上传给 AI 做分类的请求体把元数据打包成 JSON通过 TaoToken 通道发给模型做分类。请求体长这样{ model: claude-sonnet-4-20250514, max_tokens: 1024, messages: [ { role: user, content: 下面是一批手机音频文件的元数据请按类型分类音乐/播客/会议录音/语音备忘/其他并给出判断理由。\n\n[{\filename\:\meeting_20240301.mp3\,\title\:\周会录音\,\duration_ms\:3600000,\artist\:\unknown\,\mime_type\:\audio/mpeg\,\size_bytes\:28800000}] } ] }注意model字段你可以换成任何 TaoToken 支持的模型 ID。做分类任务用轻量模型就够做转写任务要选支持音频输入的模型。4. 验证请求用 curl 打通 TaoToken 通道并判读返回代码写完了先别急着跑 App。先用 curl 验证通道是通的这样能把「网络问题」和「代码问题」分开排查。这一步很多人跳过结果 App 里报错时不知道是 Key 错了还是代码写错了。4.1 基础连通性测试打开终端执行curl -X POST https://taotoken.net/api/v1/messages \ -H Content-Type: application/json \ -H x-api-key: sk-你的Key \ -H anthropic-version: 2023-06-01 \ -d { model: claude-sonnet-4-20250514, max_tokens: 256, messages: [ {role: user, content: 回复两个字通了} ] }如果你用的是 OpenAI 兼容格式的模型端点换成/v1/chat/completionscurl -X POST https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer sk-你的Key \ -d { model: gpt-4o-mini, messages: [{role: user, content: 回复两个字通了}] }4.2 返回结果判读成功的情况你会看到类似这样的 JSON{ id: msg_01xxxx, type: message, role: assistant, content: [ {type: text, text: 通了} ], model: claude-sonnet-4-20250514, stop_reason: end_turn, usage: {input_tokens: 12, output_tokens: 5} }重点看content数组里的text字段这就是模型回复。usage里的 token 数可以用来估算成本。失败的情况常见几种{error: {type: authentication_error, message: invalid x-api-key}}这是 Key 错了或者没传。检查x-api-key头Anthropic 格式或Authorization: BearerOpenAI 格式别搞混。{error: {type: invalid_request_error, message: model not found}}模型 ID 写错了。去文档页查支持的模型列表。{error: {type: rate_limit_error, message: rate limit exceeded}}请求太频繁等几秒重试。4.3 在 Android 里发请求curl 通了之后Android 端用 OkHttp 发同样的请求val client OkHttpClient() val json JSONObject().apply { put(model, claude-sonnet-4-20250514) put(max_tokens, 1024) put(messages, JSONArray().put(JSONObject().apply { put(role, user) put(content, buildPrompt(allSongs)) })) } val request Request.Builder() .url(https://taotoken.net/api/v1/messages) .addHeader(Content-Type, application/json) .addHeader(x-api-key, BuildConfig.TAOTOKEN_KEY) .addHeader(anthropic-version, 2023-06-01) .post(json.toString().toRequestBody(application/json.toMediaType())) .build() client.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { Log.e(AI, 请求失败, e) } override fun onResponse(call: Call, response: Response) { val body response.body?.string() Log.d(AI, 返回: $body) } })Key 千万别硬编码在代码里用BuildConfig或者local.properties注入否则打包发出去就泄露了。5. 本篇常见错误排查401、Cursor 空、OAuth 报错逐个击破这一节把我在实际项目里遇到过的报错整理出来对照着查。5.1 401 Unauthorized / invalid x-api-key现象curl 或 App 请求返回 401提示invalid x-api-key或authentication_error。原因排查第一检查 Key 有没有复制完整。TaoToken 的 Key 形如sk-开头的一长串复制时容易漏掉尾部字符。重新去控制台复制一次https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content第二检查请求头格式。Anthropic 格式用x-api-key: sk-xxxOpenAI 格式用Authorization: Bearer sk-xxx。两者不能混用用 Anthropic 端点却传 Bearer 头就会 401。第三检查 Key 是否被禁用或额度耗尽。控制台里能看到 Key 的状态和余额。5.2 Cursor 返回 null 或空列表现象getAllSongs()返回空 List或者query()直接返回 null。原因排查第一权限没给。Android 13 必须申请READ_MEDIA_AUDIO只申请READ_EXTERNAL_STORAGE会被忽略。用checkSelfPermission确认一下。第二MediaStore 索引没刷新。系统在开机时刷新媒体库如果你刚用 adb push 了一个音频文件进去MediaStore 里可能还没登记。手动触发扫描adb shell am broadcast -a android.intent.action.MEDIA_SCANNER_SCAN_FILE -d file:///sdcard/Music/test.mp3或者在代码里用MediaScannerConnection.scanFile()。第三查询的 URI 不对。外部存储用EXTERNAL_CONTENT_URI内部用INTERNAL_CONTENT_URI别写反。5.3 local proxy failed / connection refused现象请求报local proxy failed或Connection refused。原因排查这类错误通常是网络层的问题。检查手机是否联网、是否开了飞行模式。如果你在模拟器里跑确认模拟器的网络配置正常。注意不要尝试用任何网络代理工具那既不合规也可能导致请求被拦截。TaoToken 的 API 地址https://taotoken.net/api在国内网络环境下可以直接访问不需要额外配置。5.4 reading choices / 返回体解析失败现象请求返回 200但解析 JSON 时报reading choices或JSONException。原因排查不同模型的返回结构不一样。Anthropic 格式的回复在content[0].textOpenAI 格式在choices[0].message.content。你解析时用错了路径就会报错。先打印完整返回体看看结构Log.d(AI, raw response: $body)确认结构后再写解析代码。5.5 OAuth / Codex auth.json 相关报错如果你在用 Codex 或类似工具配置里涉及auth.json需要确保三件套齐全Base URLhttps://taotoken.net/apiKey你的sk-xxxModel ID具体模型标识如claude-sonnet-4-20250514auth.json里缺任何一项都会报鉴权失败。格式参考{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model: claude-sonnet-4-20250514 }如果你用 Claude Code 做开发接入配置也是同样的三件套Base URL 填https://taotoken.net/apiKey 填你的模型选支持的 ID。配置完可以用一个简单请求验证curl -X POST https://taotoken.net/api/v1/messages \ -H x-api-key: sk-你的Key \ -H anthropic-version: 2023-06-01 \ -H Content-Type: application/json \ -d {model:claude-sonnet-4-20250514,max_tokens:64,messages:[{role:user,content:ping}]}返回里有content就说明通了。6. 把音频接进 AI从元数据到分类转写的完整链路前面把查询和通道都打通了最后说说怎么把这两块拼起来做成一个真正有用的功能。6.1 元数据分类的提示词设计拿到音频列表后构造提示词让模型分类。关键是给模型足够的上下文别只丢一个文件名。我试过只传文件名模型经常把track01.mp3猜成音乐其实那是会议录音。把时长、MIME 类型、文件大小都带上准确率明显提升fun buildPrompt(songs: ListMusic): String { val sb StringBuilder() sb.append(下面是一批手机音频文件的元数据请按类型分类) sb.append(音乐/播客/会议录音/语音备忘/其他输出 JSON 数组。\n\n) val arr JSONArray() songs.take(50).forEach { arr.put(it.toJson()) } // 一次别传太多 sb.append(arr.toString()) return sb.toString() }一次别传太多50 条左右比较合适。传几百条进去模型容易漏掉后面的而且 token 消耗大。6.2 音频转写的处理方式分类只是第一步。真正要做转写需要把音频文件本身传给模型。这里分两种情况情况一模型支持音频输入。直接把音频文件 base64 编码后放进请求体。注意文件大小限制一般单次请求不超过 25MB超过要分段。情况二模型只支持文本。那就得先在本地做语音识别比如用 Android 的SpeechRecognizer把识别结果文本发给模型做整理和摘要。我实测下来会议录音这种场景先用本地识别拿到粗文本再让模型做纠错和分段效果比直接端到端转写更可控成本也低。6.3 完整调用示例把分类请求封装成一个方法suspend fun classifyAudio(songs: ListMusic): String { val prompt buildPrompt(songs) val json JSONObject().apply { put(model, claude-sonnet-4-20250514) put(max_tokens, 2048) put(messages, JSONArray().put(JSONObject().apply { put(role, user) put(content, prompt) })) } val request Request.Builder() .url(https://taotoken.net/api/v1/messages) .addHeader(Content-Type, application/json) .addHeader(x-api-key, BuildConfig.TAOTOKEN_KEY) .addHeader(anthropic-version, 2023-06-01) .post(json.toString().toRequestBody(application/json.toMediaType())) .build() return withContext(Dispatchers.IO) { val response OkHttpClient().newCall(request).execute() val body response.body?.string() ?: // 解析 content[0].text JSONObject(body) .getJSONArray(content) .getJSONObject(0) .getString(text) } }调用lifecycleScope.launch { val songs withContext(Dispatchers.IO) { MusicHelper.getAllSongs(thisMainActivity) } if (songs.isEmpty()) { Toast.makeText(thisMainActivity, 没找到音频文件, Toast.LENGTH_SHORT).show() returnlaunch } val result classifyAudio(songs) Log.d(AI, 分类结果: $result) }6.4 几个实用建议第一查询放子线程。MediaStore 查询在音频多的时候可能耗时几百毫秒放主线程会卡顿。用Dispatchers.IO包起来。第二结果做缓存。音频列表不会频繁变化查一次存内存或数据库别每次进页面都查。第三上传前脱敏。音频元数据里的DATA字段包含完整文件路径可能带用户名等信息。上传给 AI 前把路径去掉只留文件名和元数据。第四控制请求频率。批量分类时加个间隔别一秒发几十个请求容易触发限流。第五错误要兜底。网络请求可能失败给用户一个重试按钮别让界面卡在 loading。到这里从 MediaStore.Audio 查询本地音频到通过 TaoToken 统一通道把元数据发给 AI 做分类转写整条链路就通了。核心就三块权限配对、Cursor 查询封装、通道请求验证。把这三块跑通剩下的就是业务逻辑的打磨了。
RELATED

相关推荐

本地大模型 vs 在线平台:开发者 AI 编程工具实测对比与选型指南(TaoToken 统一 Key 接入篇)

本地大模型 vs 在线平台:开发者 AI 编程工具实测对比与选型指南(TaoToken 统一 Key 接入篇)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/18 19:09:22
Gemini CLI 安装踩坑实录:Node.js 版本、认证与 TaoToken 统一 Key 配置

Gemini CLI 安装踩坑实录:Node.js 版本、认证与 TaoToken 统一 Key 配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/18 19:09:22
【小白轻松解决】OpenClaw 2.6.4 连接 DeepSeek 模型完整教程:把 Base URL 改到 TaoToken

【小白轻松解决】OpenClaw 2.6.4 连接 DeepSeek 模型完整教程:把 Base URL 改到 TaoToken

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/18 19:09:22
MORE NEWS

更多资讯

📰

Spring Boot编程训练系统实战:高并发判题与Docker沙箱隔离

简介:这是一套基于Spring Boot开发的编程训练系统完整源码,面向Java初学者、高校计算机专业学生及编程自学爱好者,旨在解决理论学习与动手实践脱节的问题,提供从课程学习、在线编码到社区交流的一站式编程能力提升环境。资源包共8…

📰

TongRDS边缘诊断中间件:轻量级远程诊断与离线数据同步方案

简介:TongRDS-2.2.1.4.Node.tar.gz 是东方通推出的分布式内存数据缓存中间件企业版服务节点安装包,面向Java后端开发、中间件运维及分布式系统架构师,解决高并发场景下内存数据共享效率低、弹性伸缩管理复杂等核心问题。资源共84个文件&#…

📰

STM32驱动RJGT105加密芯片的I²C时序与密钥锁存实战

简介:本资源是面向嵌入式安全开发者的STM32与RJGT105加密芯片通信实战Demo,适用于物联网设备安全认证、固件加密保护等场景,适合具备STM32基础开发能力的中级工程师快速验证硬件级安全方案。压缩包共292个文件,涵盖76个头文件&…

📰

XDMA PCIe DMA开发实战:从IP配置到Linux用户态稳定传输

简介:本资源是面向FPGA开发工程师与高速接口学习者的Xilinx PCIe与XDMA技术实践源码包,聚焦PCIe分层协议理解、XDMA IP核配置、AXI4/AXI4-Stream数据通路搭建及驱动协同开发等核心难点,助力解决FPGA与主机间高效DMA传输、中断响应与性能调优等…

📰

Qt串口模块qtserialport源码解析:跨平台实现与避坑指南

简介:这份资源是面向 Qt 开发者的 qtserialport 串口库源码包,主要解决 Qt4 环境缺少原生串口支持的问题。Qt4 本身不提供串口模块,进行串口通信往往需要依赖第三方库或手动集成 qtserialport,本包正好提供了完整的源码与工程文件…

📰

LTX 2.3本地部署实战:图生视频带音频,消费级显卡也能跑

一开始听到这个标题的人大概都会问:图生视频,还要带声音?本地显卡跑得动?其实我一开始也是这么想的。直到我真的把“LTX 2.3”这个模型装进自己的显卡里,从一张静态图一路推到动态短片,再配上和画面同步的音…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬