/* bench/stream_sim.c — 边下边播全链离线验证(REPORT §21;NCM_STREAM_PLAY §8 阶段 0/1/2 矩阵)。 * * 链路:假网络线程(可调速率/抖动/断流/断续网)→ 真实 ncm_cache 写者(write(2) 直写 .tmp) * → 真实 stream_src(pread + condvar + fread 语义 read_cb)→ 真实 decoder * (mp3=open_cb SEEK_TO_BYTE|DO_NOT_SCAN / flac=drflac_open 回调)→ 真实 audio_out(SINK_NULL) * + PCM 捕获,与「同文件整首离线解码」逐字节比对(金标准;断流场景比到截断点)。 * * 矩阵:速率 {10MB/s, 400kbps, 160kbps} × {正常播完, seek 已下载区, seek 未下载区等待, * 40% 断流, 断续网};附加 flac 边下边播用例(dr_flac 回调路径)。 * 断言:跑完无死锁(看门狗)、PCM 逐字节一致(seek 用例除外,断流比前缀)、首开声延迟、 * underrun 统计(慢网/断续网 >0)、commit 后缓存文件与源逐字节一致。 * * 编译(宿主,ncm_app 目录下): * gcc -O2 -Wall -Wextra -DJSMN_HEADER -I. -Icore -I../sdk -I../meow_launcherd \ * -Ivendor/jsmn -Ivendor/minimp3 -Ivendor/dr_flac \ * bench/stream_sim.c app/play/stream_src.c app/play/decoder.c app/play/audio_out.c \ * core/ncm_cache.c -c # 逐个 -c 后链接,或整体: * gcc -O2 -Wall -Wextra ...同上... -o stream_sim ... app/play/minimp3_impl.o dr_flac_impl.o -lpthread -lm * (minimp3_impl/dr_flac_impl 为 vendor 豁免 TU,单独 -O2 无 -Wall 编译) * mipsel:换 $CC_MIPS -static,产物用 qemu-mipsel 跑。 * 运行:./stream_sim [mp3 路径] (0 网络;PASS/FAIL 退出码) */ #include "../app/ncm_app_internal.h" /* audio_out 声明 + 全局态(g_st/g_play/g_app_quit) */ #include "../app/play/decoder.h" #include "../app/play/stream_src.h" #include "../core/ncm_cache.h" #include "../core/ncm_common.h" #include #include #include #include #include #include #include #include #include /* ---- 全局态(audio_out.c / ncm_app_internal.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; /* ---- 桩:SDK/核心库其他 TU 符号(本 sim 不链) ---- */ void meow_log(meow_log_level lvl, const char *fmt, ...) { (void)lvl; (void)fmt; } 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; } /* audio_out.c 的 AUDIOD 后端符号(本 sim 恒 NCM_AUDIO_SINK=null,不会调到;仅链接用) */ int meow_audio_open(int rate, int ch, int fmt) { (void)rate; (void)ch; (void)fmt; return -1; } int meow_audio_write(const void *pcm, int frames) { (void)pcm; return frames; } int meow_audio_pause(int sid, int paused) { (void)sid; (void)paused; return 0; } int meow_audio_close(int sid) { (void)sid; return 0; } int meow_audio_query(meow_audio_state *out) { (void)out; return -1; } void meow_audio_on_preempt(meow_audio_preempt_fn cb, void *ctx) { (void)cb; (void)ctx; } /* ncm_cache.c 依赖(同 cache_evict_sim 口径内联) */ size_t ncm_strlcpy(char *dst, const char *src, size_t sz) { size_t n = 0; if (sz) { while (src[n] && n < sz - 1) { dst[n] = src[n]; n++; } dst[n] = 0; } while (src[n]) n++; return n; } int ncm_store_mkdir_p(const char *path) { if (!path || !*path) return -1; char tmp[512]; size_t len = strlen(path); if (len >= sizeof tmp) return -1; memcpy(tmp, path, len + 1); if (tmp[len - 1] == '/') tmp[len - 1] = '\0'; for (char *p = tmp + 1; *p; p++) { if (*p == '/') { *p = '\0'; mkdir(tmp, 0755); *p = '/'; } } return mkdir(tmp, 0755) == 0 || errno == EEXIST ? 0 : -1; } int ncm_store_read_cfg_int(const char *dir, const char *key, int dflt) { (void)dir; (void)key; return dflt; } /* ---- 小工具 ---- */ static double now_s(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (double)ts.tv_sec + ts.tv_nsec / 1e9; } typedef struct { unsigned char *b; size_t n, cap; } pcm_buf_t; static void pcm_put(pcm_buf_t *p, const void *data, size_t len) { if (p->n + len > p->cap) { size_t nc = p->cap ? p->cap * 2 : (1 << 20); while (nc < p->n + len) nc *= 2; p->b = (unsigned char *)realloc(p->b, nc); if (!p->b) { fprintf(stderr, "oom\n"); exit(2); } p->cap = nc; } memcpy(p->b + p->n, data, len); p->n += len; } static int g_fail = 0; #define CHECK(cond, fmt, ...) do { \ if (cond) printf(" PASS " fmt "\n", ##__VA_ARGS__); \ else { printf(" FAIL " fmt "\n", ##__VA_ARGS__); g_fail = 1; } \ } while (0) /* ---- 假网络线程:按速率/抖动/断流/断续网 把源文件写进真实 ncm_cache 写者 ---- */ typedef struct { const unsigned char *data; size_t size; uint64_t rate_Bps; /* 字节/秒 */ int jitter_ms; /* 每块延迟 ±jitter */ double cut_frac; /* >0: 写到该比例即断流(finish 带错误) */ int flaky; /* 断续网:每 ~6 块额外停 100-400ms */ stream_src_t *src; ncm_cache_writer_t *w; int rc; /* NCM_OK / 负 */ } net_t; static void *net_thread(void *arg) { net_t *nt = (net_t *)arg; const size_t CH = 4096; size_t off = 0; size_t cut_at = (nt->cut_frac > 0) ? (size_t)(nt->size * nt->cut_frac) : (size_t)-1; unsigned rng = 12345; int chunk_no = 0; while (off < nt->size && off < cut_at) { size_t n = nt->size - off; if (n > CH) n = CH; if (off + n > cut_at) n = cut_at - off; /* 节奏:块/速率 + 抖动 */ double base_ms = nt->rate_Bps ? (double)n * 1000.0 / (double)nt->rate_Bps : 0.0; rng = rng * 1103515245u + 12345u; double jit = nt->jitter_ms ? ((double)((rng >> 16) % 200) / 100.0 - 1.0) * nt->jitter_ms : 0.0; long sleep_ms = (long)(base_ms + jit); if (nt->flaky && chunk_no % 6 == 5) { rng = rng * 1103515245u + 12345u; sleep_ms += 100 + (long)((rng >> 16) % 300); /* 断续网:额外停滞 */ } if (sleep_ms > 0) usleep((useconds_t)sleep_ms * 1000); if (ncm_cache_write(nt->w, nt->data + off, n) != NCM_OK) { nt->rc = NCM_ERR_IO; goto out; } stream_src_note_written(nt->src, n); off += n; chunk_no++; } nt->rc = (off >= nt->size) ? NCM_OK : NCM_ERR_NET; /* cut=断流 */ out: stream_src_finish(nt->src, nt->rc == NCM_OK ? 0 : nt->rc); return NULL; } /* ---- 整文件离线解码(金标准 PCM) ---- */ static int decode_offline(const char *path, pcm_buf_t *pcm, int *rate, int *ch, int *bits, int64_t *frames) { ncm_decoder_t *dec = NULL; if (ncm_dec_open(&dec, path, rate, ch, bits, NULL) != 0) return -1; static unsigned char buf[4096 * 2 * 4]; int64_t fr, total = 0; while ((fr = ncm_dec_read(dec, buf, 4096)) > 0) { pcm_put(pcm, buf, (size_t)fr * (size_t)(*ch) * (size_t)(*bits) / 8); total += fr; } ncm_dec_close(dec); if (frames) *frames = total; return 0; } /* ---- 单条用例 ---- */ enum { SC_NORMAL = 0, SC_SEEK_DL, SC_SEEK_WAIT, SC_CUT, SC_FLAKY }; typedef struct { pcm_buf_t pcm; int64_t frames; /* 实际解码输出帧 */ int64_t frames_at_seek; /* seek 时已解码帧(非 seek 用例=0) */ int64_t seek_tgt_frames; /* seek 目标帧(非 seek 用例=0) */ int rate, ch, bits; double first_sound_s; /* 网线程启动 → 首块 PCM */ int underruns; uint64_t wait_ms; double wall_s; int open_ok; } case_result_t; static int run_case(const char *tag, const unsigned char *data, size_t size, const char *ext, uint64_t rate_Bps, int jitter_ms, double cut_frac, int flaky, int scenario, int64_t golden_total_frames, case_result_t *res) { memset(res, 0, sizeof *res); char dir[] = "/tmp/ncm_stream_sim_XXXXXX"; if (!mkdtemp(dir)) { printf(" FAIL mkdtemp\n"); return -1; } char cdir[600]; snprintf(cdir, sizeof cdir, "%s/cache", dir); ncm_cache_t cache; ncm_cache_init(&cache, cdir); char key[32]; static int case_no = 0; snprintf(key, sizeof key, "sim%d", case_no++); ncm_cache_writer_t *w = NULL; if (ncm_cache_open(&cache, key, ext, &w) != NCM_OK) { printf(" FAIL cache open\n"); return -1; } stream_src_t *src = NULL; if (stream_src_open_stream(&src, ncm_cache_writer_tmp_path(w)) != 0) { printf(" FAIL stream open\n"); return -1; } net_t nt = { data, size, rate_Bps, jitter_ms, cut_frac, flaky, src, w, NCM_OK }; pthread_t nth; double t0 = now_s(); if (pthread_create(&nth, NULL, net_thread, &nt) != 0) { printf(" FAIL pthread\n"); return -1; } /* 预缓冲 ≥128KB(同 player_manager 门槛) */ double wd_t0 = now_s(); int wr; while ((wr = stream_src_wait(src, 128 * 1024, 100)) == 0) { if (now_s() - wd_t0 > 120) { printf(" FAIL prebuffer watchdog (deadlock?)\n"); g_fail = 1; _exit(1); } } if (stream_src_failed(src) && stream_src_written(src) < 128 * 1024) { /* 首开声前失败(written < 预缓冲门槛)。已下过门槛的断流 *(如 cut40@10MB/s:finish 与预缓冲满足的竞态)照常放行,播到截断点(F5) */ printf(" FAIL download failed before prebuffer\n"); pthread_join(nth, NULL); ncm_cache_abort(w); stream_src_close(src); return -1; } ncm_decoder_t *dec = NULL; int rate = 0, ch = 0, bits = 0; if (ncm_dec_open_src(&dec, src, &rate, &ch, &bits, NULL) != 0) { printf(" FAIL dec open\n"); pthread_join(nth, NULL); ncm_cache_abort(w); stream_src_close(src); return -1; } res->open_ok = 1; res->rate = rate; res->ch = ch; res->bits = bits; if (audio_out_open(rate, ch, bits) != 0) { printf(" FAIL audio open\n"); g_fail = 1; _exit(1); } int64_t dur_ms = (golden_total_frames > 0 && rate > 0) ? golden_total_frames * 1000 / rate : 0; static unsigned char pcm[4096 * 2 * 4]; int64_t decoded = 0; /* 实际解码输出帧(seek 断言口径) */ int seek_done = 0; double first_sound = -1; for (;;) { if (now_s() - t0 > 150) { printf(" FAIL case watchdog (deadlock?)\n"); g_fail = 1; _exit(1); } int64_t fr = ncm_dec_read(dec, pcm, 4096); if (fr == 0) break; if (first_sound < 0) first_sound = now_s() - t0; if (audio_out_write(pcm, (int)fr) != (int)fr) { printf(" FAIL audio write\n"); g_fail = 1; _exit(1); } pcm_put(&res->pcm, pcm, (size_t)fr * (size_t)ch * (size_t)bits / 8); decoded += fr; /* 实时步调(模拟 audio_out 背压节拍器,慢网才会真 underrun) */ if (rate > 0) usleep((useconds_t)(fr * 1000000 / rate)); /* seek 用例:播 ~1s 后 seek */ if (!seek_done && dur_ms > 0 && rate > 0 && decoded * 1000 / rate >= 1000) { seek_done = 1; int64_t tgt = 0; if (scenario == SC_SEEK_DL) tgt = dur_ms / 4; /* 25%:预缓冲内,必已下载 */ else if (scenario == SC_SEEK_WAIT) tgt = dur_ms * 9 / 10; /* 90%:慢网未下载区,等待 */ if (tgt > 0) { res->frames_at_seek = decoded; res->seek_tgt_frames = tgt * rate / 1000; ncm_dec_seek_ms(dec, tgt, (int64_t)size, dur_ms); } } } res->first_sound_s = first_sound; res->frames = decoded; stream_src_stats(src, &res->underruns, &res->wait_ms); res->wall_s = now_s() - t0; audio_out_close(); ncm_dec_close(dec); pthread_join(nth, NULL); stream_src_abort(src); /* 已完成则无副作用 */ /* commit/abort + 完整下载的落盘校验(缓存文件与源逐字节一致) */ if (nt.rc == NCM_OK) { if (ncm_cache_commit(w) != 0) { printf(" FAIL commit\n"); g_fail = 1; } else { char fp[700]; if (ncm_cache_path(&cache, key, ext, fp, sizeof fp) == NCM_OK) { FILE *f = fopen(fp, "rb"); if (!f) { printf(" FAIL committed file missing\n"); g_fail = 1; } else { unsigned char *got = (unsigned char *)malloc(size); size_t rn = fread(got, 1, size, f); int extra = fgetc(f); fclose(f); if (rn != size || extra != EOF || memcmp(got, data, size) != 0) { printf(" FAIL committed bytes mismatch (rn=%zu size=%zu)\n", rn, size); g_fail = 1; } free(got); } } } } else { ncm_cache_abort(w); } stream_src_close(src); printf(" [%s] wall=%.2fs first_sound=%.2fs frames=%lld underruns=%d wait=%llums\n", tag, res->wall_s, res->first_sound_s, (long long)res->frames, res->underruns, (unsigned long long)res->wait_ms); return 0; } int main(int argc, char **argv) { (void)argc; const char *mp3 = (argc > 1) ? argv[1] : "vendor/minimp3/vectors/performance/MIPSTest.mp3"; setenv("NCM_AUDIO_SINK", "null", 1); /* 源文件入内存 */ FILE *f = fopen(mp3, "rb"); if (!f) { fprintf(stderr, "open %s failed\n", mp3); return 1; } fseek(f, 0, SEEK_END); long fsz = ftell(f); fseek(f, 0, SEEK_SET); unsigned char *data = (unsigned char *)malloc((size_t)fsz); if (!data || fread(data, 1, (size_t)fsz, f) != (size_t)fsz) { fprintf(stderr, "read failed\n"); return 1; } fclose(f); size_t size = (size_t)fsz; printf("src %s: %zu bytes\n", mp3, size); /* 金标准:整首离线解码(同样走 cb 路径——ncm_dec_open 内部也是静态 stream_src) */ pcm_buf_t golden = {0}; int grate, gch, gbits; int64_t gframes; if (decode_offline(mp3, &golden, &grate, &gch, &gbits, &gframes) != 0) { fprintf(stderr, "golden decode failed\n"); return 1; } printf("golden: %dHz x%d %dbit frames=%lld pcm=%zu bytes\n", grate, gch, gbits, (long long)gframes, golden.n); /* 40% 前缀金标准(断流用例比对到截断点) */ char prefix[] = "/tmp/ncm_stream_prefix_XXXXXX"; int pfd = mkstemp(prefix); if (pfd < 0) return 1; size_t cut_sz = size * 2 / 5; if (write(pfd, data, cut_sz) != (ssize_t)cut_sz) { fprintf(stderr, "prefix write\n"); return 1; } close(pfd); pcm_buf_t golden_cut = {0}; int t1, t2, t3; if (decode_offline(prefix, &golden_cut, &t1, &t2, &t3, NULL) != 0) { fprintf(stderr, "golden prefix decode failed\n"); return 1; } unlink(prefix); printf("golden-40%%: pcm=%zu bytes\n", golden_cut.n); struct { const char *name; uint64_t Bps; } rates[] = { { "10MB/s", 10485760u }, { "400kbps", 50000u }, { "160kbps", 20000u }, }; case_result_t r; int flaky_ur_total = 0; /* 断续网 underrun 跨速率聚合(见 flaky 用例注释) */ for (int ri = 0; ri < 3; ri++) { const char *rn = rates[ri].name; uint64_t Bps = rates[ri].Bps; char tag[64]; snprintf(tag, sizeof tag, "normal@%s", rn); printf("== CASE %s ==\n", tag); if (run_case(tag, data, size, "mp3", Bps, 0, 0, 0, SC_NORMAL, gframes, &r) == 0) { CHECK(r.pcm.n == golden.n && memcmp(r.pcm.b, golden.b, golden.n) == 0, "PCM 与整首离线解码逐字节一致 (%zu/%zu)", r.pcm.n, golden.n); if (Bps >= 1000000u) CHECK(r.first_sound_s >= 0 && r.first_sound_s < 2.0, "首开声 %.2fs < 2s", r.first_sound_s); else if (Bps >= 50000u) /* 物理下限:预缓冲 128KB / 50KB/s = 2.62s(F3 红线:MINIMP3_IO_SIZE 强制), * <2s@400kbps 与该红线冲突——按可达阈值断言并在 REPORT §21 记录。 */ CHECK(r.first_sound_s >= 0 && r.first_sound_s < 4.0, "首开声 %.2fs < 4s (128KB/50KBps=2.62s 下限)", r.first_sound_s); if (Bps <= 20000u) CHECK(r.underruns > 0, "慢网 underrun 统计 >0 (=%d)", r.underruns); } snprintf(tag, sizeof tag, "seek_dl@%s", rn); printf("== CASE %s ==\n", tag); if (run_case(tag, data, size, "mp3", Bps, 0, 0, 0, SC_SEEK_DL, gframes, &r) == 0) { /* 期望 = seek 前已解码 + 目标之后剩余(字节近似 seek,±10% 容差) */ int64_t expect = r.frames_at_seek + (gframes - r.seek_tgt_frames); double dev = expect > 0 ? (double)(r.frames - expect) / (double)expect : 1; if (dev < 0) dev = -dev; CHECK(dev < 0.10, "seek 已下载区后播完:decoded=%lld ≈ %lld (dev=%.1f%%)", (long long)r.frames, (long long)expect, dev * 100); } snprintf(tag, sizeof tag, "seek_wait@%s", rn); printf("== CASE %s ==\n", tag); if (run_case(tag, data, size, "mp3", Bps, 0, 0, 0, SC_SEEK_WAIT, gframes, &r) == 0) { int64_t expect = r.frames_at_seek + (gframes - r.seek_tgt_frames); double dev = expect > 0 ? (double)(r.frames - expect) / (double)expect : 1; if (dev < 0) dev = -dev; CHECK(dev < 0.15, "seek 未下载区等待后播完:decoded=%lld ≈ %lld (dev=%.1f%%)", (long long)r.frames, (long long)expect, dev * 100); } snprintf(tag, sizeof tag, "cut40@%s", rn); printf("== CASE %s ==\n", tag); if (run_case(tag, data, size, "mp3", Bps, 0, 0.4, 0, SC_CUT, gframes, &r) == 0) { CHECK(r.pcm.n == golden_cut.n && memcmp(r.pcm.b, golden_cut.b, golden_cut.n) == 0, "断流:PCM 与 40%% 前缀离线解码逐字节一致 (%zu/%zu)", r.pcm.n, golden_cut.n); } snprintf(tag, sizeof tag, "flaky@%s", rn); printf("== CASE %s ==\n", tag); if (run_case(tag, data, size, "mp3", Bps, 15, 0, 1, SC_FLAKY, gframes, &r) == 0) { CHECK(r.pcm.n == golden.n && memcmp(r.pcm.b, golden.b, golden.n) == 0, "断续网:PCM 逐字节一致 (%zu/%zu)", r.pcm.n, golden.n); /* 单次断续网是否打空缓冲取决于解码速度(qemu 远慢于宿主→缓冲消耗慢→可能 0 次), * 属时序非确定性——不做单用例硬断言,跨速率聚合见循环后。 */ flaky_ur_total += r.underruns; } } CHECK(flaky_ur_total > 0, "断续网跨速率 underrun 聚合 >0 (=%d)", flaky_ur_total); /* flac 边下边播兼容(dr_flac 回调路径接到 stream_src) */ const char *flac = "out/test-vectors/sweep_44k1_16.flac"; FILE *ff = fopen(flac, "rb"); if (!ff) { printf("== CASE flac_stream == SKIP(%s 不存在;§19.9 可一键重造)\n", flac); } else { fseek(ff, 0, SEEK_END); long fz = ftell(ff); fseek(ff, 0, SEEK_SET); unsigned char *fdata = (unsigned char *)malloc((size_t)fz); if (!fdata || fread(fdata, 1, (size_t)fz, ff) != (size_t)fz) { fprintf(stderr, "flac read\n"); return 1; } fclose(ff); pcm_buf_t fg = {0}; int64_t fgframes; if (decode_offline(flac, &fg, &t1, &t2, &t3, &fgframes) != 0) { fprintf(stderr, "flac golden\n"); return 1; } printf("== CASE flac_stream@400kbps ==\n"); if (run_case("flac_stream@400kbps", fdata, (size_t)fz, "flac", 50000u, 10, 0, 1, SC_NORMAL, fgframes, &r) == 0) { CHECK(r.rate == 44100 && r.bits == 16, "flac 参数 44.1k/16bit (=%d/%d)", r.rate, r.bits); CHECK(r.pcm.n == fg.n && memcmp(r.pcm.b, fg.b, fg.n) == 0, "flac 边下边播 PCM 逐字节一致 (%zu/%zu)", r.pcm.n, fg.n); } free(fdata); free(fg.b); } free(golden.b); free(golden_cut.b); free(data); printf(g_fail ? "== STREAM SIM: FAIL ==\n" : "== STREAM SIM: ALL PASS ==\n"); return g_fail; }