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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user