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:
1 parent
32543ac616
commit
21519ce2e4
23 files changed
+3255
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user