ncm A2: 音频输出迁移 meow_audiod(SINK_AUDIOD 默认后端)— 修无声根因
- audio_out.c: 新增 SINK_AUDIOD 默认(SDK meow_audio_* 连 audiod);NCM_AUDIO_SINK=file/null/alsa 与 MEOW_AUDIO_DIRECT=1 保留调试后端;ALSA 保底修两大坑(hw_params 全放行初始化 + mask 清零 再单点)——旧 memset0 interval=「只能取0」→ HW_PARAMS 恒 EINVAL 即播放无声根因 - 新增 audio_out_pause:PAUSED 沿向 audiod 发 pause(停时钟不丢流);resume 流死自动重开 - player_manager: 注册 meow_audio_on_preempt → g_play 命令通道暂停;播放事件全量 meow_log (open 参数/grant_*/write 错误节流/切歌/暂停/seek/done) - appstate/app: /tmp/ncm_autoplay 真机联调 hook(仅播已缓存曲目,绝不发 song_url) - bench/audiod_chain_sim.c: 真实 audiod_host 假 socket 全链离线回归 14 项全 PASS - 验证: host 0 warning / WAV 回归 / qemu core PASS / mipsel 0 warning;真机 OPEN→grant 已通, 持续写帧被 M2.90 设备 PCM 播放内核硬挂死阻断(REPORT §16.3,4/4 复现,留主会话) - ncm md5 db5b78ad9e8f40e6306484efe2a08eb3(已热更设备)
This commit is contained in:
1 parent
9bbc3a1467
commit
e0136213fe
10 files changed
+598
-41
No files matched your search
@@ -1170,3 +1170,126 @@ ed2f71c52563e812a04fb8a7cc05148d app/test/test_ncm_app.c # settings mock 初
|
||||
| 设置页行数 | 4 | 5 | 新增「清理缓存」行并在「缓存上限」接「自定义」数字输入 |
|
||||
| 音质 sheet 档数 | 3(Hi-Res/Higher/Standard) | 5(+Lossless/Exhigh) | 对齐 core `NCM_LEVEL_*` 全档 + 降级链起点 |
|
||||
| `ncm_cache_clear` | — | 新增 | 删除 `song_*` 全部(mp3/封面/.tmp),供「清理缓存」与测试 |
|
||||
|
||||
---
|
||||
|
||||
## 16. A2:音频输出迁移 meow_audiod(SINK_AUDIOD 默认后端)+ 无声根因修复(2026-09-01,agent-248)
|
||||
|
||||
> 范围:仅 `patch_v2/ncm_app/`(含 build 脚本、bench、REPORT 本文件)。未动 `sdk/`、`meow_audiod/`、
|
||||
> `meow_launcherd/`、`vendor/`。风控纪律:真实 NCM 请求 = autoplay 验证的 1 detail + 1 song_detail
|
||||
> (Docu 单首歌单,共 2 次 metadata,**0 次 song_url 下载**,曲目走已缓存 mp3)。
|
||||
|
||||
### 16.1 迁移内容
|
||||
|
||||
- **`app/play/audio_out.c` 新增 SINK_AUDIOD 后端并设为默认**(AUDIOD_DESIGN.md §7 落地):
|
||||
- 后端选择(env 驱动):缺省 → AUDIOD(SDK `meow_audio_*` 连 `/usr/data/meow_audio.sock`);
|
||||
`NCM_AUDIO_SINK=file/null/alsa` → FILE(WAV 回归)/NULL(解码 RT)/ALSA 直开;
|
||||
`MEOW_AUDIO_DIRECT=1` → ALSA 直开(调试保底,非正式能力)。audiod 不在 → open 返回 -1
|
||||
(不隐式回退直开),日志记 `audiod open failed rc=...`。
|
||||
- **ALSA 保底路径修复两大坑**(与 audiod/SDK 同款修法,注释标明勿回退):① hw_params 改
|
||||
「全放行」初始化(mask 全 1 + interval [0,UINT_MAX])——旧 memset 0 的 interval 等价
|
||||
「只能取 0」→ 内核 refine 必空 → HW_PARAMS 恒 EINVAL,这正是**网易云「播放无声」的根因**
|
||||
(真机 probe 实证);② access/format/subformat mask 先清零再单点(留全 1 会被 choose 成
|
||||
MMAP_INTERLEAVED → write() EINVAL)。
|
||||
- **被抢占处理**(`player_manager.c`):`player_manager_start` 注册 `meow_audio_on_preempt`
|
||||
→ 回调记 WARN 日志并经 `player_manager_pause(1)`(g_play 命令通道,持锁)暂停播放+更新 UI 态,
|
||||
不直接动锁外状态。
|
||||
- **暂停/恢复语义对齐**:`run_song` 进 NCM_PC_PAUSED 沿 → `audio_out_pause(1)` 向 audiod 发
|
||||
MSG_AUDIO_PAUSE(停 PCM 时钟不丢流);出暂停沿 → `audio_out_pause(0)` 恢复;**恢复时若流已死**
|
||||
(被抢占/断链,SDK 返回负值)→ 自动 close+reopen 重建输出流(解码位置不变)。
|
||||
- **播放路径全量日志**(meow_log,无声排查永久资产):open 结果/请求参数/audiod 授权
|
||||
grant_*(open 后 `meow_audio_query` 落 rate/ch/fmt/underrun)、write 错误(首次+每 100 次节流)、
|
||||
pause/resume/reopen/close(含累计写帧)、play start/done/switch/seek/stopped 事件。
|
||||
- **真机联调 autoplay hook**(`appstate.c` + `app.c`,非正式能力):`/tmp/ncm_autoplay` 触发文件
|
||||
存在且 home 歌单已载 → 后台线程找「已缓存」曲目自动播放(内容=数字则歌单下标起扫 ≤3 个、
|
||||
=文本则歌单名子串单点匹配如 "Docu")。**绝不发 song_url 下载**——无缓存命中只记日志退出。
|
||||
用途:无用户在场时验证「ncm→SDK→audiod→DAC」全链。
|
||||
- `build_ncm_host.sh`:SDK_SRC 补 `meow_ui_audio`(audio_out.c 新增 meow_audio_* 引用,宿主链接需要)。
|
||||
|
||||
### 16.2 验证记录(分层)
|
||||
|
||||
| 层 | 项 | 结果 |
|
||||
|---|---|---|
|
||||
| 宿主 | `build_ncm_host.sh` | **自有代码 0 warning**(仅 SDK meow_ui_vol.c 既有 system() 告警) |
|
||||
| 宿主 | WAV 回归:`NCM_AUDIO_SINK=file NCM_AUDIO_OUT=... test_decode MIPSTest.mp3` | **PASS**:8.21s/48k/立体声,WAV 头合法,字节数 44+393984×4 精确一致 |
|
||||
| 宿主 | 离屏渲染 home/playlist/player/settings/search | 全 rc=0 无回归 |
|
||||
| 宿主 | **`bench/audiod_chain_sim.c`(新增)**:起真实 `meow_audiod_host`(FILE sink 假 socket)驱动真实 audio_out.c+SDK 客户端 | **14 项全 PASS**:OPEN(grant=44100/2ch/S16)→WRITE 51200 帧→QUERY(active=1 written>0)→PAUSE(paused=1)→RESUME→**抢占**(第二客户端裸发 OPEN→on_preempt 回调触发→旧流 write 失败)→**流死自动重开**(reopen 可写)→CLOSE(active=0)→audiod WAV 落盘非空 |
|
||||
| qemu | `qemu-mipsel ./out/test_ncm_core_mipsel` | **PASS**(core 未动,回归) |
|
||||
| mipsel | `build_ncm.sh` | **0 warning**;ncm md5 `db5b78ad9e8f40e6306484efe2a08eb3` |
|
||||
| 真机 | 设备已刷 M2.90(0.7.85),/usr/bin/meow_audiod 在板运行 | ✓ |
|
||||
| 真机 | ncm 热更(.new+mv),`am start` 不可用(amsd DECLARE 转发未联调,「pkg not registered」)→ 手动 nohup 起 | ✓ 启动/home 歌单 120 正常 |
|
||||
| 真机 | autoplay hook:name 'Docu' → playlist[1](1 tracks)→ song[0] id=2048962657 缓存命中 → play | ✓ 日志链路正确 |
|
||||
| 真机 | **audiod OPEN→grant 链**:`meow_audio tone` 拿到 `sid=1 grant=44100Hz/2ch/S16` | ✓ IPC/协商链设备上正常 |
|
||||
| 真机 | **持续写帧/underrun 验收**:**被阻断**(见 §16.3 重大发现) | ✗ 未达成 |
|
||||
|
||||
### 16.3 重大发现:M2.90 设备 PCM 播放启动 → 内核硬挂死 → 看门狗重启(4/4 复现)
|
||||
|
||||
**现象**:设备(0.7.85,stock kernel 4.4.94+ #15 2026-01-16,未变)上一进入 PCM 播放启动序列,
|
||||
数秒内整机硬挂死并重启。复现 4/4:`meow_audio tone` ×3(含一次无 ncm 的纯 audiod 路径)+
|
||||
ncm 经 SDK 链播放 ×1。kmsg 流抓取到最后内核日志(死亡点,两次抓取一致):
|
||||
|
||||
```
|
||||
[ 412.104797] hbc3000_set_samplerate codec_type=4 samplerate=44100 spdif=0 ...
|
||||
[ 412.104819] hbc3000_stop
|
||||
[ 412.104865] hbc3000_start
|
||||
[ 412.168307] cs43198_dai_prepare pcm
|
||||
[ 412.371919] hbc3000_start
|
||||
<<< 日志到此中断,无任何 panic/oops 文本 → 硬挂死(疑似时钟/电源域死锁),随后重启 >>>
|
||||
```
|
||||
|
||||
**归因排查记录**:
|
||||
- **固件打包无误**:设备 /usr/bin/meow_audiod md5 `e07e9144…` == 仓库构建 == fw_content.conf 条目。
|
||||
- **ncm 无关**:对照组 ncm.bak(A2 前二进制 19d99ce4)同设备前台 idle **31 分钟稳定**(boot G,
|
||||
期间经历 suspend/resume);本会话早段(23:05-23:45)观察到的 4 次「idle ncm 在跑数分钟后重启」
|
||||
实为**用户 M2.90 刷机循环**(卡根 r3proii.upt 22:46 上卡,多次升级重启)同窗口巧合,对照组证伪。
|
||||
- **suspend 相关性未证伪也未证实**:设备每次 boot 后 ~40s 自动息屏进 `System suspend`(dmesg 实证),
|
||||
而 4 次致命播放全部发生在 resume 之后;A1 收尾(agent-246)同内核 tone 实测 written=played、
|
||||
underrun=0 **通过**——据信在未 suspend 的新鲜 boot 窗口。未 suspend 的播放验证需要
|
||||
「重启后 40s 内立刻 tone」,本会话无法在不主动重启的前提下获得该窗口(留给用户/主会话)。
|
||||
|
||||
**影响与建议(留主会话/用户决策)**:
|
||||
- A2 真机验收的「持续写帧+underrun=0」被此问题阻断,非 ncm 代码问题(SDK 链 OPEN→grant 已实证:
|
||||
tone 拿到 `sid=1 grant=44100Hz/2ch/S16`;ncm 日志链到「缓存命中 → play」)。
|
||||
- 建议:① 新鲜 boot 40s 内(未 suspend)测 `meow_audio tone` 验证 suspend 相关性;
|
||||
② 若为真,权宜=禁用自动息屏(`display.screen_off_timeout`=0)或 resume 后重置 codec 电源/时钟;
|
||||
③ 根治需看 hbc3000/cs43198 驱动 resume 路径(stock 内核源码不在本仓库);挂死点恒在
|
||||
`cs43198_dai_prepare` 后第二次 `hbc3000_start`,无任何内核日志 = 硬挂(时钟/i2c/电源域死锁特征)。
|
||||
- 本会话为定位该问题触发过多次设备重启(均自动恢复;未碰 recovery_all/未 killall hiby_player/
|
||||
未主动重启设备)。
|
||||
|
||||
### 16.4 md5(本批改动文件 + 产物)
|
||||
|
||||
```
|
||||
db5b78ad9e8f40e6306484efe2a08eb3 ncm # mipsel 静态 App(新;已部署设备)
|
||||
```
|
||||
(源文件 md5 以 git 为准;改动清单见 §16.5)
|
||||
|
||||
### 16.5 出入清单(只改 `patch_v2/ncm_app/`)
|
||||
|
||||
```
|
||||
修改:
|
||||
app/play/audio_out.c # SINK_AUDIOD 默认后端 + ALSA 保底两坑修复 + audio_out_pause + 全量日志
|
||||
app/play/player_manager.c # on_audio_preempt 注册/回调 + 暂停沿 audiod pause/resume + 播放事件日志
|
||||
app/logic/appstate.c # autoplay 真机联调 hook(/tmp/ncm_autoplay,缓存命中才播,绝不下载)
|
||||
app/app.c # app_on_tick 挂 autoplay_maybe
|
||||
app/ncm_app_internal.h # audio_out_pause/autoplay_maybe 声明 + audio_out 注释更新
|
||||
build_ncm_host.sh # SDK_SRC 补 meow_ui_audio
|
||||
ncm / ncm_host # 重编产物
|
||||
新增:
|
||||
bench/audiod_chain_sim.c # audiod 全链离线回归(真实 audiod_host 假 socket,14 项)
|
||||
REPORT.md # 本批记录(§16)
|
||||
(未动 sdk/、meow_audiod/、meow_launcherd/、vendor/;未动设备系统/恢复区)
|
||||
```
|
||||
|
||||
### 16.6 诚实出入 / 已知限制
|
||||
|
||||
- **真机验收未闭环**:§16.3 内核硬挂死阻断「持续写帧 + underrun=0 + meow_audio status 在播」三项。
|
||||
已实证到 OPEN→grant(CLI tone)与 ncm→autoplay→缓存命中→play 日志链;SDK 客户端全链
|
||||
(含抢占/重开)由宿主 sim 对真实 audiod_host 14 项覆盖。
|
||||
- **autoplay hook 默认 0 起扫前 3 个歌单首页**:若首个歌单是超大歌单(5191 首 detail ~35s 解析)
|
||||
会慢/重;文本名单点匹配(如 Docu)是推荐用法。hook 为调试资产保留,不影响正常 UI 路径。
|
||||
- **`am start dev.player.ncm` 报 pkg not registered**:amsd 二期 launcher→amsd DECLARE 转发未联调
|
||||
(WORK_STATE 已记残余),本次手动 nohup 起 ncm(fb 与 launcher 并存,测试场景可接受)。
|
||||
- 设备日志中 ipc_media META 每秒一对(state=0 空态也发)为既有行为,本批未动。
|
||||
- **未碰** /data/recovery_all、未 killall hiby_player、未主动重启设备(§16.3 的多次重启均为
|
||||
被测路径触发的设备自重启)。
|
||||
@@ -84,6 +84,7 @@ static void app_on_swipe_progress(int x_start, int dx)
|
||||
static void app_on_tick(void)
|
||||
{
|
||||
uint64_t now = meow_ui_time_ms();
|
||||
autoplay_maybe(); /* 真机联调 hook:/tmp/ncm_autoplay 存在则自动播已缓存曲目(一次性) */
|
||||
if (login_is_active()) { login_tick(now); return; }
|
||||
if (player_is_active()) { player_tick(now); return; }
|
||||
if (search_is_active()) { search_tick(now); return; }
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define NCM_DEFAULT_DATA "/usr/data/mnt/sd_0/MeowData/dev.player.ncm/data"
|
||||
|
||||
@@ -205,3 +206,89 @@ int appstate_current_playing(void)
|
||||
if (g_st.playing_song < 0 || g_st.playing_song >= g_st.song_count) return -1;
|
||||
return g_st.playing_song;
|
||||
}
|
||||
|
||||
/* ---- 真机联调 autoplay hook(A2 音频迁移验收用,非正式能力)----
|
||||
* 触发文件 /tmp/ncm_autoplay 存在且 home 歌单已载 → 后台线程找「已缓存」曲目并播放:
|
||||
* - 内容为纯数字 → 从该歌单下标起扫最多 AUTOPLAY_SCAN_PL 个歌单首页;
|
||||
* - 内容为其它文本 → 按歌单名子串匹配**单个**歌单(如 "Docu";避开超大歌单的
|
||||
* 全量 trackIds 解析,真机联调省时省内存);
|
||||
* - 空文件 → 从 0 起扫。
|
||||
* 风控纪律:**绝不发 song_url 下载**——无缓存命中只记日志退出;metadata 请求 = 每歌单
|
||||
* 1 detail + 1 song_detail(≤ 2×AUTOPLAY_SCAN_PL 次)。 */
|
||||
#define AUTOPLAY_TRIGGER "/tmp/ncm_autoplay"
|
||||
#define AUTOPLAY_SCAN_PL 3
|
||||
|
||||
/* 播放单个歌单首页中第一首已缓存曲目。命中播放返回 1,否则 0。 */
|
||||
static int autoplay_try_playlist(int pl)
|
||||
{
|
||||
int rc = appstate_load_playlist_songs_page(pl, 0, NCM_SONG_BATCH);
|
||||
if (rc != NCM_OK) {
|
||||
meow_log(MEOW_LOG_WARN, "autoplay: playlist[%d] load rc=%d", pl, rc);
|
||||
return 0;
|
||||
}
|
||||
char cpath[600];
|
||||
for (int i = 0; i < g_st.song_count; i++) {
|
||||
if (ncm_cache_lookup(&g_st.ctx.cache, g_st.songs[i].id, ".mp3",
|
||||
cpath, sizeof cpath) == NCM_OK) {
|
||||
g_st.active_playlist = pl;
|
||||
meow_log(MEOW_LOG_INFO, "autoplay: playlist[%d] song[%d] id=%s cached -> play",
|
||||
pl, i, g_st.songs[i].id);
|
||||
player_manager_play(i);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
meow_log(MEOW_LOG_INFO, "autoplay: playlist[%d] first page %d songs, no cached track",
|
||||
pl, g_st.song_count);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void *autoplay_worker(void *arg)
|
||||
{
|
||||
const char *sel = (const char *)arg; /* NULL=从 0 起扫;数字=起始下标;其它=歌单名子串 */
|
||||
if (sel && sel[0] && !(sel[0] >= '0' && sel[0] <= '9')) {
|
||||
for (int pl = 0; pl < g_st.playlist_count && !g_app_quit; pl++) {
|
||||
if (strstr(g_st.playlists[pl].name, sel)) {
|
||||
meow_log(MEOW_LOG_INFO, "autoplay: name '%s' -> playlist[%d] '%s' (%d tracks)",
|
||||
sel, pl, g_st.playlists[pl].name, g_st.playlists[pl].track_count);
|
||||
if (autoplay_try_playlist(pl)) { free((void *)sel); return NULL; }
|
||||
break;
|
||||
}
|
||||
}
|
||||
meow_log(MEOW_LOG_WARN, "autoplay: no playlist name match / no cached track ('%s')", sel);
|
||||
free((void *)sel);
|
||||
return NULL;
|
||||
}
|
||||
int start = (sel && sel[0]) ? atoi(sel) : 0;
|
||||
free((void *)sel);
|
||||
if (start < 0) start = 0;
|
||||
int end = start + AUTOPLAY_SCAN_PL;
|
||||
if (end > g_st.playlist_count) end = g_st.playlist_count;
|
||||
for (int pl = start; pl < end && !g_app_quit; pl++)
|
||||
if (autoplay_try_playlist(pl)) return NULL;
|
||||
meow_log(MEOW_LOG_WARN, "autoplay: no cached track found (no download, risk control)");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void autoplay_maybe(void)
|
||||
{
|
||||
static int s_fired = 0;
|
||||
if (s_fired || g_app_quit) return;
|
||||
if (access(AUTOPLAY_TRIGGER, F_OK) != 0) return;
|
||||
if (!g_st.logged_in || g_st.playlist_count <= 0) return; /* 等 home 歌单载完 */
|
||||
s_fired = 1;
|
||||
char buf[64] = "";
|
||||
FILE *f = fopen(AUTOPLAY_TRIGGER, "r"); /* 一次性:读完即删 */
|
||||
if (f) {
|
||||
if (fgets(buf, sizeof buf, f)) {
|
||||
size_t n = strlen(buf);
|
||||
while (n && (buf[n - 1] == '\n' || buf[n - 1] == '\r' || buf[n - 1] == ' ')) buf[--n] = 0;
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
unlink(AUTOPLAY_TRIGGER);
|
||||
meow_log(MEOW_LOG_INFO, "autoplay: triggered, sel='%s'", buf);
|
||||
char *sel = strdup(buf);
|
||||
pthread_t t;
|
||||
if (pthread_create(&t, NULL, autoplay_worker, sel) == 0) pthread_detach(t);
|
||||
else free(sel);
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
* - app/ui/<page>.c 每页一个 view 文件(login/home/playlist/player/settings)
|
||||
* - app/ui/common/chrome.c 共享 chrome(topbar / 内容区几何 / 空态 / 提示)
|
||||
* - app/logic/ appstate(全局 ctx/歌单/歌曲/播放态)+ lyric(LRC 解析) + cover(封面缓存)
|
||||
* - app/play/ player_manager(播放线程) + audio_out(ALSA/文件输出) + ipc_media(媒体会话 IPC)
|
||||
* - app/play/ player_manager(播放线程) + audio_out(audiod/ALSA/文件输出) + ipc_media(媒体会话 IPC)
|
||||
*
|
||||
* 铁律(APPS.md §3.4.1 / WORK_STATE.md):物理键禁映射——on_key 不处理任何键语义;
|
||||
* 返回=左缘左滑(on_swipe LEFT)/返回钮/meow_ui_request_exit;左缘返回:播放页→歌单→主页→退出。
|
||||
@@ -267,6 +267,7 @@ int appstate_load_playlist_songs_page(int idx, int offset, int limit);
|
||||
void appstate_apply_playlist_cover(int idx);
|
||||
void appstate_set_playing_song(int idx, const char *cover_url);
|
||||
int appstate_current_playing(void); /* 返回当前播放歌曲下标(UI) */
|
||||
void autoplay_maybe(void); /* 真机联调 hook(/tmp/ncm_autoplay 触发,自动播已缓存曲目,一次性) */
|
||||
|
||||
/* lyric */
|
||||
int lyric_parse(const char *lrc, const char *tlrc); /* 填充 g_lyric_* */
|
||||
@@ -286,9 +287,11 @@ const char *cover_current_path(void); /* 封面原始文件路径(供 la
|
||||
void cover_cache_reset(void);
|
||||
|
||||
/* ---- play(app/play/)---- */
|
||||
/* audio_out */
|
||||
/* audio_out(后端:默认 SINK_AUDIOD=meow_audiod 独占服务;env NCM_AUDIO_SINK=file/null/alsa
|
||||
* 或 MEOW_AUDIO_DIRECT=1 选调试后端,见 audio_out.c 文件头) */
|
||||
int audio_out_open(int sample_rate, int channels, int bits); /* 0=成功 */
|
||||
int audio_out_write(const short *samples, int frames); /* <0=错误/结束 */
|
||||
int audio_out_pause(int paused); /* 1=暂停(停 PCM 时钟不丢流) 0=恢复(流死自动重开);非 AUDIOD 后端恒 0 */
|
||||
void audio_out_close(void);
|
||||
void audio_out_set_target(int ms); /* 预留:缓冲目标 */
|
||||
int audio_out_buffered_ms(void);
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
/* play/audio_out.c — 音频输出抽象:ALSA 直开 hw:0,0(44.1k/16bit/立体声)或 FILE/NULL sink。
|
||||
/* play/audio_out.c — 音频输出抽象:默认 SINK_AUDIOD(meow_audiod 独占服务,经 SDK meow_audio_*),
|
||||
* 调试保底 SINK_ALSADEV 直开 hw:0,0,另有 FILE/NULL sink。
|
||||
*
|
||||
* 后端选择(audio_out_open,env 驱动):
|
||||
* - 缺省 → SINK_AUDIOD(SDK 客户端连 /usr/data/meow_audio.sock;audiod 独占声卡、
|
||||
* 格式归一/采样率跟随/抢占全在服务端,AUDIOD_DESIGN.md §7)。
|
||||
* - NCM_AUDIO_SINK=file/null/alsa → FILE(WAV,NCM_AUDIO_OUT 指定路径,缺省 /tmp/ncm_out.wav)/
|
||||
* NULL(丢弃,仅测解码 RT)/ ALSA 直开。
|
||||
* - MEOW_AUDIO_DIRECT=1 → ALSA 直开(调试保底,非正式能力,勿依赖)。
|
||||
* audiod 不在 → open 返回 -1(不隐式回退直开,AUDIOD_DESIGN.md §7)。
|
||||
*
|
||||
* ALSA 直开两大坑(真机定位,agent-246,与 audiod/SDK 同款修法,勿回退):
|
||||
* ① hw_params 须「全放行」初始化(params_any 语义:mask 全 1、interval [0,UINT_MAX])
|
||||
* ——memset 0 的 interval 等价「只能取 0」→ 内核 refine 必空 → HW_PARAMS 恒 EINVAL;
|
||||
* ② access/format/subformat mask 须先清零再单点(pmask 只 OR)——留全 1 会被 choose 成
|
||||
* MMAP_INTERLEAVED → write() 恒 EINVAL。
|
||||
* 注意 musl 的 sound/asound.h 中 masks/intervals 是**重索引**数组:
|
||||
* masks[k] ↔ hw_param (FIRST_MASK + k);intervals[k] ↔ hw_param (FIRST_INTERVAL + k)。
|
||||
*
|
||||
* 设备(mipsel musl 无 libasound):用 ALSA 内核 UAPI(sound/asound.h)直开 /dev/snd/pcmC0D0p,
|
||||
* 非 mmap RW 模式下 ioctl 设 hw_params + write() 写交织帧。布局/时序真机待验证(本层只保证跨编译 +
|
||||
* 宿主回退正确)。
|
||||
* 宿主/qemu 验证:无 /dev/snd/pcmC0D0p → 回退 FILE sink(写 WAV,env NCM_AUDIO_OUT 指定路径,
|
||||
* 缺省 /tmp/ncm_out.wav)或 NULL(env NCM_AUDIO_SINK=null,丢弃,仅测解码 RT 用)。
|
||||
* 线程模型:仅由 player_manager 播放线程调用(单写者);audio_out_close 可被 UI 线程调用打断阻塞写。
|
||||
* 日志:open 结果/参数、audiod 授权 grant_*、write 错误、pause/resume/close 全写 meow_log
|
||||
* (无声排查的永久资产,勿省)。
|
||||
*/
|
||||
#include "../ncm_app_internal.h"
|
||||
#include "meow_ipc.h" /* AUDIO_FMT_S16 */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -21,11 +36,14 @@
|
||||
#define NCM_HAVE_ALSA 1
|
||||
#endif
|
||||
|
||||
enum { SINK_ALSADEV = 0, SINK_FILE = 1, SINK_NULL = 2 };
|
||||
enum { SINK_AUDIOD = 0, SINK_ALSADEV = 1, SINK_FILE = 2, SINK_NULL = 3 };
|
||||
static int g_sink = -1;
|
||||
static int g_fd = -1; /* alsa or file fd */
|
||||
static int g_sid = 0; /* audiod stream id(>0 有效) */
|
||||
static int g_rate, g_ch, g_bits;
|
||||
static const char *g_path = NULL;
|
||||
static uint64_t g_wr_frames = 0; /* 本次 open 累计写帧(日志) */
|
||||
static int g_wr_errs = 0; /* 写错误计数(节流日志) */
|
||||
|
||||
#define NCM_DEFAULT_OUT "/tmp/ncm_out.wav"
|
||||
|
||||
@@ -33,10 +51,7 @@ void audio_out_set_target(int ms) { (void)ms; }
|
||||
int audio_out_buffered_ms(void) { return 0; }
|
||||
|
||||
#ifdef NCM_HAVE_ALSA
|
||||
/* ALSA 非 mmap 直装:开设备 → hw_params(access=rw_interleaved, S16_LE, rate, 2ch, s16)→ write。
|
||||
* 注意:musl 的 sound/asound.h 中 masks/intervals 是**重索引**数组:
|
||||
* masks[k] ↔ hw_param (FIRST_MASK + k); intervals[k] ↔ hw_param (FIRST_INTERVAL + k)。
|
||||
* 故访问时须减 FIRST_MASK/FIRST_INTERVAL,否则越界/错位。 */
|
||||
/* ALSA 非 mmap 直装:开设备 → hw_params(access=rw_interleaved, S16_LE, rate, ch)→ write。 */
|
||||
static void pmask(struct snd_mask *m, unsigned bit) { m->bits[bit / 32] |= 1u << (bit % 32); }
|
||||
static void pivset(struct snd_interval *iv, unsigned val)
|
||||
{
|
||||
@@ -49,27 +64,42 @@ static int alsa_open(int rate, int ch, int bits)
|
||||
{
|
||||
if (bits != 16) return -1;
|
||||
int fd = open("/dev/snd/pcmC0D0p", O_RDWR);
|
||||
if (fd < 0) return -1;
|
||||
if (fd < 0) {
|
||||
meow_log(MEOW_LOG_ERROR, "audio: alsa open /dev/snd/pcmC0D0p failed errno=%d", errno);
|
||||
return -1;
|
||||
}
|
||||
struct snd_pcm_hw_params p;
|
||||
/* 坑①:「全放行」初始化(snd_pcm_hw_params_any 语义):mask 全 1、interval [0,UINT_MAX]。
|
||||
* memset 0 的 interval 会被内核当成「只能取 0」约束 → refine 必空 → HW_PARAMS 恒 EINVAL。 */
|
||||
memset(&p, 0, sizeof p);
|
||||
unsigned nm = SNDRV_PCM_HW_PARAM_LAST_MASK - SNDRV_PCM_HW_PARAM_FIRST_MASK + 1;
|
||||
for (unsigned i = 0; i < nm; i++)
|
||||
for (unsigned b = 0; b < sizeof p.masks[i].bits / sizeof p.masks[i].bits[0]; b++)
|
||||
p.masks[i].bits[b] = ~0u;
|
||||
unsigned ni = SNDRV_PCM_HW_PARAM_LAST_INTERVAL - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1;
|
||||
for (unsigned i = 0; i < ni; i++) { p.intervals[i].min = 0; p.intervals[i].max = ~0u; }
|
||||
p.flags = 0;
|
||||
pmask(&p.masks[SNDRV_PCM_HW_PARAM_ACCESS - SNDRV_PCM_HW_PARAM_FIRST_MASK],
|
||||
SNDRV_PCM_ACCESS_RW_INTERLEAVED);
|
||||
pmask(&p.masks[SNDRV_PCM_HW_PARAM_FORMAT - SNDRV_PCM_HW_PARAM_FIRST_MASK],
|
||||
SNDRV_PCM_FORMAT_S16_LE);
|
||||
pmask(&p.masks[SNDRV_PCM_HW_PARAM_SUBFORMAT - SNDRV_PCM_HW_PARAM_FIRST_MASK],
|
||||
SNDRV_PCM_SUBFORMAT_STD);
|
||||
/* 坑②:mask 先清零再单点(pmask 只 OR):全 1 的 access 会被内核 choose 成
|
||||
* MMAP_INTERLEAVED → write() 恒 EINVAL(本机 RW_INTERLEAVED 可用,须显式约束)。 */
|
||||
struct snd_mask *am = &p.masks[SNDRV_PCM_HW_PARAM_ACCESS - SNDRV_PCM_HW_PARAM_FIRST_MASK];
|
||||
memset(am, 0, sizeof *am); pmask(am, SNDRV_PCM_ACCESS_RW_INTERLEAVED);
|
||||
struct snd_mask *fm = &p.masks[SNDRV_PCM_HW_PARAM_FORMAT - SNDRV_PCM_HW_PARAM_FIRST_MASK];
|
||||
memset(fm, 0, sizeof *fm); pmask(fm, SNDRV_PCM_FORMAT_S16_LE);
|
||||
struct snd_mask *sm = &p.masks[SNDRV_PCM_HW_PARAM_SUBFORMAT - SNDRV_PCM_HW_PARAM_FIRST_MASK];
|
||||
memset(sm, 0, sizeof *sm); pmask(sm, SNDRV_PCM_SUBFORMAT_STD);
|
||||
pivset(&p.intervals[SNDRV_PCM_HW_PARAM_CHANNELS - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL],
|
||||
(unsigned)ch);
|
||||
pivset(&p.intervals[SNDRV_PCM_HW_PARAM_RATE - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL],
|
||||
(unsigned)rate);
|
||||
p.rmask = ~0u;
|
||||
p.info = 0;
|
||||
p.msbits = 0;
|
||||
p.rate_num = 0;
|
||||
p.rate_den = 0;
|
||||
if (ioctl(fd, SNDRV_PCM_IOCTL_HW_PARAMS, &p) != 0) { close(fd); return -1; }
|
||||
p.rmask = ~0u; p.info = 0; p.msbits = 0; p.rate_num = 0; p.rate_den = 0;
|
||||
if (ioctl(fd, SNDRV_PCM_IOCTL_HW_PARAMS, &p) != 0) {
|
||||
meow_log(MEOW_LOG_ERROR, "audio: alsa HW_PARAMS failed errno=%d rate=%d ch=%d",
|
||||
errno, rate, ch);
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
ioctl(fd, SNDRV_PCM_IOCTL_PREPARE, 0);
|
||||
meow_log(MEOW_LOG_INFO, "audio: alsa open ok rate=%d ch=%d (direct debug)", rate, ch);
|
||||
return fd;
|
||||
}
|
||||
#endif
|
||||
@@ -96,29 +126,76 @@ static void file_write_header(int fd, int rate, int ch, int bits, uint32_t data_
|
||||
if (write(fd, h, 44) != 44) { /* 忽略 */ }
|
||||
}
|
||||
|
||||
static int file_open(int rate, int ch, int bits)
|
||||
{
|
||||
int fd = open(g_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (fd < 0) { /* 无路可写 → null */
|
||||
meow_log(MEOW_LOG_WARN, "audio: file open %s failed errno=%d -> null sink", g_path, errno);
|
||||
g_sink = SINK_NULL; g_fd = -1;
|
||||
return 0;
|
||||
}
|
||||
file_write_header(fd, rate, ch, bits, 0);
|
||||
g_sink = SINK_FILE;
|
||||
g_fd = fd;
|
||||
meow_log(MEOW_LOG_INFO, "audio: file sink %s rate=%d ch=%d bits=%d", g_path, rate, ch, bits);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int audio_out_open(int sample_rate, int channels, int bits)
|
||||
{
|
||||
g_rate = sample_rate; g_ch = channels; g_bits = bits;
|
||||
g_wr_frames = 0; g_wr_errs = 0;
|
||||
const char *env = getenv("NCM_AUDIO_SINK");
|
||||
g_path = getenv("NCM_AUDIO_OUT");
|
||||
if (!g_path || !g_path[0]) g_path = NCM_DEFAULT_OUT;
|
||||
|
||||
/* 显式 null(测 RT) */
|
||||
if (env && strcmp(env, "null") == 0) { g_sink = SINK_NULL; g_fd = -1; return 0; }
|
||||
/* 显式 null(测解码 RT) */
|
||||
if (env && strcmp(env, "null") == 0) {
|
||||
g_sink = SINK_NULL; g_fd = -1;
|
||||
meow_log(MEOW_LOG_INFO, "audio: null sink rate=%d ch=%d", sample_rate, channels);
|
||||
return 0;
|
||||
}
|
||||
/* 显式 file(宿主/回归) */
|
||||
if (env && strcmp(env, "file") == 0)
|
||||
return file_open(sample_rate, channels, bits);
|
||||
|
||||
/* 尝试 ALSA(仅未显式指定 file;宿主无设备自动回退) */
|
||||
if (!(env && strcmp(env, "file") == 0)) {
|
||||
/* 调试保底:ALSA 直开(仅显式指定;修法见文件头两大坑注释) */
|
||||
const char *de = getenv("MEOW_AUDIO_DIRECT");
|
||||
int want_alsa = (env && strcmp(env, "alsa") == 0) || (de && de[0] == '1');
|
||||
if (want_alsa) {
|
||||
#ifdef NCM_HAVE_ALSA
|
||||
int fd = alsa_open(sample_rate, channels, bits);
|
||||
if (fd >= 0) { g_sink = SINK_ALSADEV; g_fd = fd; return 0; }
|
||||
meow_log(MEOW_LOG_ERROR, "audio: alsa direct open failed");
|
||||
return -1;
|
||||
#else
|
||||
meow_log(MEOW_LOG_ERROR, "audio: alsa sink requested but no ALSA on this build");
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
/* FILE sink */
|
||||
int fd = open(g_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (fd < 0) { g_sink = SINK_NULL; g_fd = -1; return 0; } /* 无路可写 → null */
|
||||
file_write_header(fd, sample_rate, channels, bits, 0);
|
||||
g_sink = SINK_FILE;
|
||||
g_fd = fd;
|
||||
|
||||
/* 默认:SINK_AUDIOD(meow_audiod 独占服务)。v1 SDK 声源格式 S16(minimp3 输出即 S16_LE)。 */
|
||||
if (bits != 16) {
|
||||
meow_log(MEOW_LOG_ERROR, "audio: audiod open: bits=%d unsupported (v1 S16 only)", bits);
|
||||
return -1;
|
||||
}
|
||||
int sid = meow_audio_open(sample_rate, channels, AUDIO_FMT_S16);
|
||||
if (sid < 0) {
|
||||
meow_log(MEOW_LOG_ERROR, "audio: audiod open failed rc=%d rate=%d ch=%d (audiod 不在?)",
|
||||
sid, sample_rate, channels);
|
||||
return -1;
|
||||
}
|
||||
g_sink = SINK_AUDIOD;
|
||||
g_sid = sid;
|
||||
/* 授权参数(grant_*)落日志:无声排查关键资产 */
|
||||
meow_audio_state st;
|
||||
if (meow_audio_query(&st) == 0)
|
||||
meow_log(MEOW_LOG_INFO,
|
||||
"audio: audiod open ok sid=%d req=%dHz/%dch/%dbit grant=%uHz/%uch/fmt=%u underrun=%u",
|
||||
sid, sample_rate, channels, bits, st.rate, st.ch, st.fmt, st.underrun);
|
||||
else
|
||||
meow_log(MEOW_LOG_INFO, "audio: audiod open ok sid=%d req=%dHz/%dch/%dbit",
|
||||
sid, sample_rate, channels, bits);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -126,6 +203,18 @@ int audio_out_write(const short *samples, int frames)
|
||||
{
|
||||
if (g_app_quit) return -1;
|
||||
if (g_sink == SINK_NULL || g_sink < 0) return frames;
|
||||
if (g_sink == SINK_AUDIOD) {
|
||||
int w = meow_audio_write(samples, frames);
|
||||
if (w != frames) {
|
||||
g_wr_errs++;
|
||||
if (g_wr_errs == 1 || (g_wr_errs % 100) == 0)
|
||||
meow_log(MEOW_LOG_ERROR, "audio: audiod write failed rc=%d frames=%d (errs=%d)",
|
||||
w, frames, g_wr_errs);
|
||||
return -1;
|
||||
}
|
||||
g_wr_frames += (uint64_t)frames;
|
||||
return frames;
|
||||
}
|
||||
size_t n = (size_t)frames * (size_t)g_ch * (size_t)(g_bits / 8);
|
||||
if (g_sink == SINK_ALSADEV) {
|
||||
/* 阻塞尽力写满 - EINTR/EAGAIN 重试;错误返回 */
|
||||
@@ -135,19 +224,60 @@ int audio_out_write(const short *samples, int frames)
|
||||
ssize_t w = write(g_fd, p + off, n - off);
|
||||
if (w > 0) { off += (size_t)w; continue; }
|
||||
if (w < 0 && (errno == EINTR || errno == EAGAIN)) continue;
|
||||
meow_log(MEOW_LOG_ERROR, "audio: alsa write failed errno=%d", errno);
|
||||
return -1;
|
||||
}
|
||||
g_wr_frames += (uint64_t)frames;
|
||||
return frames;
|
||||
}
|
||||
if (g_sink == SINK_FILE) {
|
||||
if (write(g_fd, samples, n) != (ssize_t)n) return -1;
|
||||
g_wr_frames += (uint64_t)frames;
|
||||
return frames;
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
/* 暂停(1)/恢复(0) audiod 流(停 PCM 时钟但不丢流)。仅 AUDIOD 后端有效,其余后端恒 0。
|
||||
* 恢复时若流已死(被抢占/audiod 断链)→ 自动重开(解码位置不变,仅重建输出流)。 */
|
||||
int audio_out_pause(int paused)
|
||||
{
|
||||
if (g_sink != SINK_AUDIOD || g_sid <= 0) return 0;
|
||||
int rc = meow_audio_pause(g_sid, paused);
|
||||
if (rc >= 0) {
|
||||
meow_log(MEOW_LOG_INFO, "audio: audiod %s sid=%d ok", paused ? "pause" : "resume", g_sid);
|
||||
return 0;
|
||||
}
|
||||
if (paused) { /* 暂停失败(流多半已死):记日志即可,恢复时会走重开 */
|
||||
meow_log(MEOW_LOG_WARN, "audio: audiod pause sid=%d rc=%d (stream dead?)", g_sid, rc);
|
||||
return 0;
|
||||
}
|
||||
/* 恢复失败 = 流已死 → 重开 */
|
||||
meow_log(MEOW_LOG_WARN, "audio: audiod resume sid=%d rc=%d -> reopen", g_sid, rc);
|
||||
meow_audio_close(g_sid);
|
||||
g_sid = 0;
|
||||
int sid = meow_audio_open(g_rate, g_ch, AUDIO_FMT_S16);
|
||||
if (sid < 0) {
|
||||
meow_log(MEOW_LOG_ERROR, "audio: audiod reopen failed rc=%d rate=%d ch=%d", sid, g_rate, g_ch);
|
||||
return -1;
|
||||
}
|
||||
g_sid = sid;
|
||||
meow_log(MEOW_LOG_INFO, "audio: audiod reopen ok sid=%d", sid);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void audio_out_close(void)
|
||||
{
|
||||
if (g_sink == SINK_AUDIOD) {
|
||||
if (g_sid > 0) {
|
||||
meow_audio_close(g_sid);
|
||||
meow_log(MEOW_LOG_INFO, "audio: audiod close sid=%d written_frames=%llu wr_errs=%d",
|
||||
g_sid, (unsigned long long)g_wr_frames, g_wr_errs);
|
||||
g_sid = 0;
|
||||
}
|
||||
g_sink = -1;
|
||||
return;
|
||||
}
|
||||
if (g_sink == SINK_FILE && g_fd >= 0) {
|
||||
/* 回填:offset 4 = RIFF 总长-36;offset 40 = data 长度 */
|
||||
off_t end = lseek(g_fd, 0, SEEK_END);
|
||||
|
||||
@@ -26,6 +26,15 @@ static int64_t g_file_bytes;
|
||||
static int g_level; /* 音质档(来自设置) */
|
||||
static int g_cache_gb;
|
||||
|
||||
/* 被抢占回调(SDK 读线程分发;audiod 后开流赢):走 g_play 命令通道暂停(内部持锁),
|
||||
* 不直接动锁外状态。恢复时 audio_out_pause(0) 会检测流死并自动重开(见 audio_out.c)。 */
|
||||
static void on_audio_preempt(const char *by_app, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
meow_log(MEOW_LOG_WARN, "play: preempted by [%s] -> pause", (by_app && by_app[0]) ? by_app : "?");
|
||||
player_manager_pause(1);
|
||||
}
|
||||
|
||||
int player_manager_state(void) { int s; pthread_mutex_lock(&g_play.mu); s = g_play.state; pthread_mutex_unlock(&g_play.mu); return s; }
|
||||
int player_manager_playing_song(void) { int s; pthread_mutex_lock(&g_play.mu); s = g_play.cur_song; pthread_mutex_unlock(&g_play.mu); return s; }
|
||||
int64_t player_manager_pos_ms(void) { int64_t p; pthread_mutex_lock(&g_play.mu); p = g_play.pos_ms; pthread_mutex_unlock(&g_play.mu); return p; }
|
||||
@@ -196,10 +205,12 @@ static void run_song(int idx)
|
||||
g_play.dur_ms = dur;
|
||||
g_play.err = 0;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
meow_log(MEOW_LOG_INFO, "play: start idx=%d id=%s dur=%lldms", idx, s->id, (long long)dur);
|
||||
|
||||
mp3d_sample_t pcm[PCM_CHUNK];
|
||||
int64_t frames_done = 0; /* 已输出帧(每通道) */
|
||||
int need_seek_ms = -1;
|
||||
int paused_edge = 0; /* 暂停沿:进暂停已向 audiod 发 pause */
|
||||
|
||||
for (;;) {
|
||||
if (g_app_quit) break;
|
||||
@@ -212,15 +223,30 @@ static void run_song(int idx)
|
||||
int st = g_play.state;
|
||||
pthread_mutex_unlock(&g_play.mu);
|
||||
|
||||
if (cur != idx) break; /* 切歌 → 回外层 */
|
||||
if (st == NCM_PC_STOPPED) break;
|
||||
if (cur != idx) { meow_log(MEOW_LOG_INFO, "play: switch idx=%d -> %d", idx, cur); break; } /* 切歌 → 回外层 */
|
||||
if (st == NCM_PC_STOPPED) { meow_log(MEOW_LOG_INFO, "play: stopped idx=%d", idx); break; }
|
||||
|
||||
if (cmd == NCM_CTL_SEEK && seek_ms >= 0) { need_seek_ms = (int)seek_ms; }
|
||||
|
||||
if (st == NCM_PC_PAUSED) { usleep(40 * 1000); continue; } /* 冻结 */
|
||||
if (st == NCM_PC_PAUSED) {
|
||||
if (!paused_edge) { /* 进暂停:停 PCM 时钟但不丢流(audiod MSG_AUDIO_PAUSE) */
|
||||
paused_edge = 1;
|
||||
meow_log(MEOW_LOG_INFO, "play: paused idx=%d pos=%lldms", idx,
|
||||
(long long)(frames_done * 1000 / 44100));
|
||||
audio_out_pause(1);
|
||||
}
|
||||
usleep(40 * 1000);
|
||||
continue; /* 冻结 */
|
||||
}
|
||||
if (paused_edge) { /* 出暂停:恢复 audiod 流(流死则自动重开) */
|
||||
paused_edge = 0;
|
||||
meow_log(MEOW_LOG_INFO, "play: resumed idx=%d", idx);
|
||||
if (audio_out_pause(0) != 0) { set_err(NCM_ERR_IO, "audio resume failed"); break; }
|
||||
}
|
||||
|
||||
if (need_seek_ms >= 0) {
|
||||
/* 重开并 seek:关闭后重开(字节 offset 近似) */
|
||||
meow_log(MEOW_LOG_INFO, "play: seek idx=%d -> %dms", idx, need_seek_ms);
|
||||
audio_out_close();
|
||||
mp3dec_ex_close(&dec);
|
||||
memset(&dec, 0, sizeof dec);
|
||||
@@ -235,7 +261,10 @@ static void run_song(int idx)
|
||||
}
|
||||
|
||||
size_t got = mp3dec_ex_read(&dec, pcm, PCM_CHUNK);
|
||||
if (got == 0) break; /* 播完 */
|
||||
if (got == 0) { /* 播完 */
|
||||
meow_log(MEOW_LOG_INFO, "play: done idx=%d frames=%lld", idx, (long long)frames_done);
|
||||
break;
|
||||
}
|
||||
int frames = (int)(got / 2);
|
||||
if (audio_out_write(pcm, frames) != frames) break;
|
||||
frames_done += frames;
|
||||
@@ -377,6 +406,7 @@ int player_manager_start(void)
|
||||
g_level = g_st.level;
|
||||
g_cache_gb = g_st.cache_limit_gb;
|
||||
pthread_mutex_init(&g_play.mu, NULL);
|
||||
meow_audio_on_preempt(on_audio_preempt, NULL); /* 被抢占 → 暂停(SDK 读线程分发) */
|
||||
if (pthread_create(&g_thr, NULL, player_thread, NULL) != 0) return -1;
|
||||
g_thr_on = 1;
|
||||
return 0;
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
/* bench/audiod_chain_sim.c — ncm 音频输出迁移(A2,SINK_AUDIOD)离线全链验证(0 网络)。
|
||||
*
|
||||
* 起**真实** meow_audiod_host(FILE sink,假 socket=/tmp/ncm_audiod_sim.sock)作服务端,
|
||||
* 驱动**真实** app/play/audio_out.c(SINK_AUDIOD 默认后端)+ SDK 客户端 meow_ui_audio.c:
|
||||
* ① audio_out_open(44100,2,16) → audiod OPEN/OPEN_ACK(grant 授权)
|
||||
* ② 连续 WRITE(正弦帧)→ meow_audio_query 见 active=1 / written>0
|
||||
* ③ audio_out_pause(1)/(0) → QUERY paused=1/0(停 PCM 时钟不丢流)
|
||||
* ④ 第二客户端裸发 OPEN 抢占 → on_preempt 回调触发 + 旧流 write 失败
|
||||
* ⑤ audio_out_pause(0) 对流死自动重开 → 恢复可写
|
||||
* ⑥ audio_out_close → QUERY active=0(流回收);audiod 侧 WAV 落盘非空
|
||||
*
|
||||
* 编译(宿主;需先 ../meow_audiod/build_audiod.sh host 产出 meow_audiod_host):
|
||||
* cc -O2 -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd \
|
||||
* -Ivendor/mbedtls/include -Ivendor/jsmn -Ivendor/qrcodegen/c -Ivendor/minimp3 \
|
||||
* bench/audiod_chain_sim.c app/play/audio_out.c \
|
||||
* ../sdk/meow_ui_audio.c ../sdk/meow_ui_log.c -o audiod_chain_sim -lpthread -lm
|
||||
* 运行:./audiod_chain_sim (PASS/FAIL 计数,全过 exit 0)
|
||||
*/
|
||||
#include "../app/ncm_app_internal.h" /* audio_out_* + meow_ui.h */
|
||||
#include "meow_ipc.h" /* 抢占者裸帧:MSG_AUDIO_OPEN/m_audio_open_t */
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <math.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/un.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
/* ---- audio_out.c / meow_ui_log.c 依赖的外部符号(本 sim 提供最小实现) ---- */
|
||||
volatile int g_app_quit = 0;
|
||||
uint64_t meow_ui_time_ms(void)
|
||||
{
|
||||
struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (uint64_t)ts.tv_sec * 1000 + (uint64_t)ts.tv_nsec / 1000000;
|
||||
}
|
||||
|
||||
#define SIM_SOCK "/tmp/ncm_audiod_sim.sock"
|
||||
#define SIM_WAV "/tmp/ncm_audiod_sim.wav"
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
static void check(int cond, const char *name)
|
||||
{
|
||||
if (cond) { printf(" PASS %s\n", name); g_pass++; }
|
||||
else { printf(" FAIL %s\n", name); g_fail++; }
|
||||
}
|
||||
|
||||
/* 抢占回调标志(SDK 读线程置位) */
|
||||
static volatile int g_preempted = 0;
|
||||
static void sim_preempt_cb(const char *by_app, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
printf(" [cb] preempted by %s\n", by_app ? by_app : "?");
|
||||
g_preempted = 1;
|
||||
}
|
||||
|
||||
/* ---- 起 audiod(FILE sink 假声卡) ---- */
|
||||
static pid_t g_srv = -1;
|
||||
static int start_audiod(void)
|
||||
{
|
||||
unlink(SIM_SOCK); unlink(SIM_WAV);
|
||||
pid_t p = fork();
|
||||
if (p == 0) {
|
||||
setenv("MEOW_AUDIO_SOCK", SIM_SOCK, 1);
|
||||
setenv("MEOW_AUDIO_OUT", SIM_WAV, 1);
|
||||
setenv("MEOW_AUDIO_SINK", "file", 1);
|
||||
setenv("MEOW_AUDIO_FORCE", "1", 1);
|
||||
setenv("MEOW_AUDIO_AMIXER", "/bin/true", 1);
|
||||
int devnull = open("/dev/null", O_WRONLY);
|
||||
if (devnull >= 0) { dup2(devnull, 1); dup2(devnull, 2); }
|
||||
execl("../meow_audiod/meow_audiod_host", "meow_audiod_host", (char *)NULL);
|
||||
_exit(127);
|
||||
}
|
||||
if (p < 0) return -1;
|
||||
g_srv = p;
|
||||
for (int i = 0; i < 50; i++) { /* 等 socket 出现(≤5s) */
|
||||
if (access(SIM_SOCK, F_OK) == 0) return 0;
|
||||
usleep(100 * 1000);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* ---- 抢占者:裸 IPC 客户端发 OPEN ---- */
|
||||
static int wr_all(int fd, const void *p, size_t n)
|
||||
{
|
||||
const uint8_t *b = p; size_t o = 0;
|
||||
while (o < n) { ssize_t r = write(fd, b + o, n - o); if (r < 0) { if (errno == EINTR) continue; return -1; } o += (size_t)r; }
|
||||
return 0;
|
||||
}
|
||||
static int intruder_open(void)
|
||||
{
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0) return -1;
|
||||
struct sockaddr_un a;
|
||||
memset(&a, 0, sizeof a);
|
||||
a.sun_family = AF_UNIX;
|
||||
snprintf(a.sun_path, sizeof a.sun_path, "%s", SIM_SOCK);
|
||||
if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; }
|
||||
m_audio_open_t o;
|
||||
memset(&o, 0, sizeof o);
|
||||
snprintf(o.app_id, sizeof o.app_id, "intruder");
|
||||
o.rate = 44100; o.ch = 2; o.fmt = AUDIO_FMT_S16;
|
||||
uint8_t hdr[16];
|
||||
uint32_t magic = MEOW_IPC_MAGIC, len = sizeof o, req = 99;
|
||||
uint16_t ver = MEOW_IPC_VERSION, type = MSG_AUDIO_OPEN;
|
||||
memcpy(hdr + 0, &magic, 4); memcpy(hdr + 4, &ver, 2); memcpy(hdr + 6, &type, 2);
|
||||
memcpy(hdr + 8, &req, 4); memcpy(hdr + 12, &len, 4);
|
||||
if (wr_all(fd, hdr, 16) != 0 || wr_all(fd, &o, sizeof o) != 0) { close(fd); return -1; }
|
||||
return fd; /* 保持连接(断开会 EOF 回收 intruder 流) */
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setenv("MEOW_AUDIO_SOCK", SIM_SOCK, 1); /* SDK 客户端连假 socket */
|
||||
setenv("MEOW_LOG_APP", "dev.player.ncm", 1); /* audiod 状态里的 app_id */
|
||||
unsetenv("NCM_AUDIO_SINK"); /* 确保走默认 SINK_AUDIOD */
|
||||
unsetenv("MEOW_AUDIO_DIRECT");
|
||||
|
||||
if (start_audiod() != 0) { printf("FAIL: audiod_host start\n"); return 1; }
|
||||
printf("audiod up (sock=%s)\n", SIM_SOCK);
|
||||
|
||||
/* ① OPEN */
|
||||
meow_audio_on_preempt(sim_preempt_cb, NULL);
|
||||
check(audio_out_open(44100, 2, 16) == 0, "open: SINK_AUDIOD default ok");
|
||||
|
||||
/* ② WRITE 连续帧(~1s 正弦)+ QUERY 见活跃流 */
|
||||
static short pcm[2048 * 2];
|
||||
int wr_ok = 1;
|
||||
for (int c = 0; c < 25 && wr_ok; c++) {
|
||||
for (int i = 0; i < 2048; i++) {
|
||||
double t = (double)(c * 2048 + i) / 44100.0;
|
||||
short v = (short)(sin(2.0 * 3.14159265 * 440.0 * t) * 12000);
|
||||
pcm[i * 2] = v; pcm[i * 2 + 1] = v;
|
||||
}
|
||||
if (audio_out_write(pcm, 2048) != 2048) wr_ok = 0;
|
||||
usleep(10 * 1000);
|
||||
}
|
||||
check(wr_ok, "write: 25 chunks x 2048 frames ok");
|
||||
meow_audio_state st;
|
||||
check(meow_audio_query(&st) == 0 && st.active == 1 && st.written_frames > 0,
|
||||
"query: active=1 written>0");
|
||||
printf(" [state] app=%s sid=%u rate=%u ch=%u fmt=%u written=%llu played=%llu underrun=%u paused=%d\n",
|
||||
st.app_id, st.stream_id, st.rate, st.ch, st.fmt,
|
||||
(unsigned long long)st.written_frames, (unsigned long long)st.played_frames,
|
||||
st.underrun, st.paused);
|
||||
|
||||
/* ③ PAUSE / RESUME(停时钟不丢流) */
|
||||
check(audio_out_pause(1) == 0, "pause(1) ok");
|
||||
usleep(100 * 1000);
|
||||
check(meow_audio_query(&st) == 0 && st.paused == 1, "query: paused=1");
|
||||
check(audio_out_pause(0) == 0, "resume(0) ok");
|
||||
usleep(100 * 1000);
|
||||
check(meow_audio_query(&st) == 0 && st.paused == 0, "query: paused=0");
|
||||
|
||||
/* ④ 抢占:intruder OPEN → 回调触发 + 本流写失败 */
|
||||
int ifd = intruder_open();
|
||||
check(ifd >= 0, "intruder: open sent");
|
||||
for (int i = 0; i < 30 && !g_preempted; i++) usleep(100 * 1000);
|
||||
check(g_preempted == 1, "preempt: on_preempt callback fired");
|
||||
check(audio_out_write(pcm, 128) < 0, "preempt: old stream write fails");
|
||||
|
||||
/* ⑤ 流死后 resume(0) 自动重开(抢回流)→ 恢复可写 */
|
||||
check(audio_out_pause(0) == 0, "resume-after-preempt: reopen ok");
|
||||
check(audio_out_write(pcm, 2048) == 2048, "reopened stream writable");
|
||||
if (ifd >= 0) close(ifd);
|
||||
|
||||
/* ⑥ CLOSE → 流回收 */
|
||||
audio_out_close();
|
||||
usleep(200 * 1000);
|
||||
check(meow_audio_query(&st) == 0 && st.active == 0, "close: query active=0");
|
||||
|
||||
struct stat sb;
|
||||
check(stat(SIM_WAV, &sb) == 0 && sb.st_size > 44, "audiod WAV non-empty");
|
||||
|
||||
if (g_srv > 0) { kill(g_srv, SIGTERM); waitpid(g_srv, NULL, 0); }
|
||||
printf("== audiod_chain_sim: pass=%d fail=%d ==\n", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
@@ -19,7 +19,8 @@ INC="-I. -I../sdk -I../meow_launcherd -Icore \
|
||||
echo "==> host SDK objects"
|
||||
SDK_SRC="meow_ui_gfx meow_ui_sys meow_ui_theme meow_ui_vol meow_ui_log meow_ui_widget \
|
||||
meow_ui_input meow_ui_activity meow_ui_dialog meow_ui_ime meow_ui_draw meow_ui_view \
|
||||
meow_ui_img meow_ui_config meow_ui_nav meow_ui_media meow_notify_ipc meow_pinyin"
|
||||
meow_ui_img meow_ui_config meow_ui_nav meow_ui_media meow_notify_ipc meow_pinyin \
|
||||
meow_ui_audio"
|
||||
for f in $SDK_SRC; do
|
||||
$CC_HOST -O2 -c -DMEOW_UI_HOST_TEST -I"$SDK_DIR" -I"$ROOT/patch_v2/meow_launcherd" \
|
||||
"$SDK_DIR/$f.c" -o "$OUT/host/sdk_$f.o" || { echo "sdk $f FAILED"; exit 1; }
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user