feat(ncm): 网易云客户端 v1 UI+播放链路 (agent-205) — os.meow.ncm: 登录页(真实unikey二维码+轮询状态机)/我的歌单(listview)/歌单详情/播放页(封面+seek+滚动歌词+竖横屏切换)/设置(音质默认Hi-res+缓存上限默认10G); 播放线程(缓存LRU+minimp3→ALSA UAPI直开hw:0,0); 媒体会话MSG_META上报+PLAYCTL接收(首真实媒体源)

ncm 1e331964 (text 764KB); 0 warning; 宿主离屏6页截图目检+真实QR可扫+qemu-mipsel干净启动+解码446xRT
真机待联调: ALSA时序/803扫码闭环/媒体会话联动/gbk大字库渲染
This commit is contained in:
MeowOS committed 2026-08-31 07:24:59 +08:00
1 parent 32543ac616
commit 21519ce2e4
23 files changed
+3255

No files matched your search

+67
View File
@@ -0,0 +1,67 @@
/* test/test_decode.c — 播放解码链路验证(宿主):minimp3 解码 mp3 → audio_out(FILE sink) 写 WAV。
* 不依赖网络/登录(输入本地 mp3)。验证:解码无错、WAV 非空、时长合理、RT 因子。
* 运行:NCM_AUDIO_SINK=file NCM_AUDIO_OUT=/tmp/ncm_decode.wav ./test_decode <path.mp3>
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "../ncm_app_internal.h" /* 引 audio_out / minimp3 声明 */
#include "minimp3_ex.h"
ncm_app_state_t g_st;
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER };
volatile int g_app_quit = 0;
volatile int g_net_ready = 0;
int g_host_mock = 1;
static double now_s(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec + ts.tv_nsec / 1e9;
}
int main(int argc, char **argv)
{
const char *path = (argc > 1) ? argv[1] : "vendor/minimp3/vectors/performance/MIPSTest.mp3";
mp3dec_ex_t dec;
memset(&dec, 0, sizeof dec);
if (mp3dec_ex_open(&dec, path, MP3D_SEEK_TO_BYTE) != 0) { fprintf(stderr, "open failed\n"); return 1; }
static mp3d_sample_t pcm[8192 * 2];
/* 读首帧确定采样率/声道(read 会填 dec.info)*/
size_t got = mp3dec_ex_read(&dec, pcm, 8192 * 2);
int sr = dec.info.hz;
int ch = dec.info.channels;
if (sr <= 0 || ch <= 0) { fprintf(stderr, "bad stream\n"); return 1; }
printf("mp3: %s %dHz x%d detected_samples=%lld bitrate=%dkbps\n",
path, sr, ch, (long long)dec.samples, dec.info.bitrate_kbps);
if (audio_out_open(sr, ch, 16) != 0) { fprintf(stderr, "audio_out open failed\n"); return 1; }
long long frames = 0;
double t0 = now_s();
if (got > 0) {
int fr = (int)(got / (size_t)ch);
if (audio_out_write(pcm, fr) != fr) { fprintf(stderr, "write failed\n"); return 1; }
frames += fr;
}
for (;;) {
if (g_app_quit) break;
got = mp3dec_ex_read(&dec, pcm, 8192 * 2);
if (got == 0) break;
int fr = (int)(got / (size_t)ch);
if (audio_out_write(pcm, fr) != fr) { fprintf(stderr, "write failed\n"); break; }
frames += fr;
}
double dt = now_s() - t0;
audio_out_close();
mp3dec_ex_close(&dec);
double secs = (double)frames / (double)sr;
double rt = (dt > 0) ? secs / dt : 0;
printf("decoded %lld frames = %.2f s audio (%d Hz) in %.3f s => %.1fx RT\n",
frames, secs, sr, dt, rt);
printf("OK: %s\n", (frames > 0) ? "decode chain works" : "EMPTY");
return (frames > 0) ? 0 : 1;
}
+163
View File
@@ -0,0 +1,163 @@
/* test/test_ncm_app.c — 宿主验证:真实页面 view + 真实 SDK 绘制,MEOW_UI_DUMP 导 PPM 截图目检。
*
* 编译见 build_ncm_host.sh。运行:NCM_PAGE=login|home|playlist|player|lands|settings \
* MEOW_UI_DUMP=/tmp/ncm_<page>.ppm ./ncm_host
* 不依赖 /dev/fb0(SDK 宿主导出 480x720 离屏);meow_font 打桩(ASCII 5x7;中文不渲染,仅验布局)。
* 数据全 mock(g_host_mock=1,页面跳过网络)。
*/
#include "../ncm_app_internal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
ncm_app_state_t g_st;
ncm_play_run_t g_play = { .mu = PTHREAD_MUTEX_INITIALIZER,
.state = NCM_PC_STOPPED, .cur_song = -1 };
volatile int g_app_quit = 0;
volatile int g_net_ready = 0;
int g_host_mock = 1;
static void mock_playlists(void)
{
static const char *names[] = { "My Favorites", "Focus Mix", "Commute", "Workout", "Sleep" };
static const int counts[] = { 128, 56, 34, 22, 15 };
g_st.playlist_count = 5;
g_st.logged_in = 1;
ncm_strlcpy(g_st.nickname, "Tester", sizeof g_st.nickname);
for (int i = 0; i < 5; i++) {
snprintf(g_st.playlists[i].id, sizeof g_st.playlists[i].id, "pl%d", i);
snprintf(g_st.playlists[i].name, sizeof g_st.playlists[i].name, "%s", names[i]);
g_st.playlists[i].track_count = counts[i];
snprintf(g_st.playlists[i].cover, sizeof g_st.playlists[i].cover,
"http://p1.music.126.net/cover%d.jpg", i);
}
}
static void mock_songs(void)
{
static const char *sn[] = { "Song One", "Song Two", "Song Three",
"Song Four", "Song Five", "Song Six" };
static const char *ar[] = { "Artist A", "Artist B", "Artist C",
"Artist D", "Artist E", "Artist F" };
g_st.song_count = 6;
g_st.active_playlist = 0;
for (int i = 0; i < 6; i++) {
snprintf(g_st.songs[i].id, sizeof g_st.songs[i].id, "%ld", 1000L + i);
snprintf(g_st.songs[i].name, sizeof g_st.songs[i].name, "%s", sn[i]);
snprintf(g_st.songs[i].art_name, sizeof g_st.songs[i].art_name, "%s", ar[i]);
snprintf(g_st.songs[i].album_name, sizeof g_st.songs[i].album_name, "Album %d", i + 1);
g_st.songs[i].duration_ms = 180000 + i * 20000;
g_st.songs[i].fee = (i == 3) ? 1 : 0;
}
ncm_strlcpy(g_st.cover_url, "http://p1.music.126.net/cover0.jpg", sizeof g_st.cover_url);
g_st.playing_song = 0;
}
static void mock_lyric(void)
{
static const char *lrc =
"[00:00.00]Intro\n[00:05.00]First line of the song\n"
"[00:10.50]Second line here\n[00:15.20]Third line with translation\n"
"[00:20.80]Fourth line\n[00:26.10]Fifth\n[00:31.40]Sixth line\n";
static const char *tl =
"[00:10.50]Second translation\n[00:15.20]Third translation\n";
lyric_parse(lrc, tl);
}
static void mock_qr(void)
{
g_st.qr_size = 29;
for (int y = 0; y < 29; y++)
for (int x = 0; x < 29; x++) {
int border = (x < 3 || x >= 26 || y < 3 || y >= 26);
int finder = (x < 7 && y < 7) || (x >= 22 && y < 7) || (x < 7 && y >= 22);
g_st.qr_mods[y * 29 + x] = border || finder || ((x ^ y) & 1);
}
g_st.login_state = NCM_LOGIN_PENDING;
}
static int page_id(const char *name)
{
if (strcmp(name, "login") == 0) return ACT_LOGIN;
if (strcmp(name, "home") == 0) return ACT_HOME;
if (strcmp(name, "playlist") == 0) return ACT_PLAYLIST;
if (strcmp(name, "player") == 0) return ACT_PLAYER;
if (strcmp(name, "lands") == 0) return ACT_PLAYER; /* 横屏布局 */
if (strcmp(name, "settings") == 0) return ACT_SETTINGS;
return -1;
}
int main(int argc, char **argv)
{
const char *name = getenv("NCM_PAGE");
if (!name || !name[0]) name = "home";
char dump[512];
const char *dp = getenv("MEOW_UI_DUMP");
if (!dp || !dp[0]) snprintf(dump, sizeof dump, "/tmp/ncm_%s.ppm", name);
else snprintf(dump, sizeof dump, "%s", dp);
setenv("MEOW_UI_DUMP", dump, 1);
(void)argc; (void)argv;
meow_ui_opts o = { APP_W, APP_H };
if (meow_ui_init(&o) != 0) { fprintf(stderr, "init failed\n"); return 1; }
meow_theme_add_listener(NULL);
/* mock 状态 */
if (g_st.cache_dir[0] == 0) {
snprintf(g_st.cache_dir, sizeof g_st.cache_dir, "/tmp/ncm_host_cache");
}
if (strcmp(name, "login") == 0) mock_qr();
else if (strcmp(name, "loginlive") == 0) { /* 真实 unikey → 真实二维码 */
ncm_ctx_init(&g_st.ctx, "/tmp/ncm_host_login");
char unikey[64];
int rc = ncm_login_unikey(g_st.ctx.device_id, unikey, sizeof unikey);
if (rc != NCM_OK) { fprintf(stderr, "unikey fetch failed rc=%d\n", rc); return 1; }
ncm_login_qr_url(unikey, g_st.qr_url, sizeof g_st.qr_url);
int sz = 0;
if (ncm_login_qr_matrix(g_st.qr_url, g_st.qr_mods, 177, &sz) != NCM_OK) {
fprintf(stderr, "qr matrix failed\n"); return 1;
}
g_st.qr_size = sz;
g_st.login_state = NCM_LOGIN_PENDING;
printf("live unikey=%s qr_url=%s size=%d\n", unikey, g_st.qr_url, sz);
}
else if (strcmp(name, "playlist") == 0) { mock_playlists(); mock_songs(); }
else if (strcmp(name, "player") == 0 || strcmp(name, "lands") == 0) {
mock_playlists(); mock_songs(); mock_lyric();
pthread_mutex_lock(&g_play.mu);
g_play.state = NCM_PC_PLAYING;
g_play.cur_song = 0;
g_play.pos_ms = 94000;
g_play.dur_ms = 180000;
pthread_mutex_unlock(&g_play.mu);
} else if (strcmp(name, "settings") == 0) {
g_st.logged_in = 1;
ncm_strlcpy(g_st.nickname, "Tester", sizeof g_st.nickname);
g_st.level = NCM_LEVEL_HIRES;
g_st.cache_limit_gb = 10;
} else {
mock_playlists();
}
meow_view *v = NULL;
if (strcmp(name, "login") == 0 || strcmp(name, "loginlive") == 0) v = login_view_create();
else if (strcmp(name, "home") == 0) v = home_view_create();
else if (strcmp(name, "playlist") == 0) v = playlist_view_create();
else if (strcmp(name, "player") == 0 || strcmp(name, "lands") == 0) v = player_view_create();
else if (strcmp(name, "settings") == 0) v = settings_view_create();
if (!v) { fprintf(stderr, "bad page\n"); return 1; }
if (strcmp(name, "lands") == 0) player_set_screen(1);
int pid = (strcmp(name, "loginlive") == 0) ? ACT_LOGIN : page_id(name);
meow_view_page_push(pid);
/* player 页:其 on_tick 通常驱动进度;这里直接再画一次确保状态就绪 */
meow_ui_paint_full();
if (strcmp(name, "player") == 0 || strcmp(name, "lands") == 0) {
player_tick(meow_ui_time_ms());
}
meow_ui_shutdown();
printf("dumped %s -> %s\n", name, dump);
return 0;
}