/* 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 */ uint64_t start; /* 起点偏移(续传:从 .part 大小续写;0=从头) */ 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 = (size_t)nt->start; 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, 0, 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; } /* 续传用例(阶段3 §5.2):会话1 中断下载存 .part,会话2 从 .part 续传 → 完整 PCM 金标准比对。 * 不含 http/Range(那是 player_flow_sim 的职责)——本用例验证「.part 前缀已在盘上、续写不被重复、 * 读端从头跟播、commit 后完整等于源文件」的写穿缓存续传契约。 */ static int run_resume_case(const char *tag, const unsigned char *data, size_t size, uint64_t rate_Bps, int jitter_ms, int flaky, double resume_frac, const pcm_buf_t *golden) { case_result_t rr; memset(&rr, 0, sizeof rr); char dir[] = "/tmp/ncm_stream_resume_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 rcase_no = 0; snprintf(key, sizeof key, "rs%d", rcase_no++); /* ---- 会话1:中断下载,写 resume_frac 比例 → abort_part 存 .part ---- */ ncm_cache_writer_t *w1 = NULL; if (ncm_cache_open(&cache, key, "mp3", &w1) != NCM_OK) { printf(" FAIL s1 open\n"); return -1; } stream_src_t *src1 = NULL; if (stream_src_open_stream(&src1, ncm_cache_writer_tmp_path(w1)) != 0) { printf(" FAIL s1 stream\n"); return -1; } net_t nt1 = { data, size, rate_Bps, jitter_ms, resume_frac, flaky, 0, src1, w1, NCM_OK }; pthread_t n1; pthread_create(&n1, NULL, net_thread, &nt1); pthread_join(n1, NULL); stream_src_close(src1); ncm_cache_abort_part(w1); /* .tmp → .part(保留前缀字节) */ uint64_t part_size = 0; if (ncm_cache_part_size(&cache, key, "mp3", &part_size) != NCM_OK || part_size == 0) { printf(" FAIL .part not parked\n"); return -1; } uint64_t exp_part = (uint64_t)((double)size * resume_frac); CHECK(part_size == exp_part, ".part size=%llu (expect %llu 前缀)", (unsigned long long)part_size, (unsigned long long)exp_part); /* ---- 会话2:续传(.part 追加)+ 读端从头跟播 → 完整 PCM ---- */ ncm_cache_writer_t *w2 = NULL; if (ncm_cache_open_resume(&cache, key, "mp3", &w2) != NCM_OK) { printf(" FAIL s2 resume open\n"); return -1; } stream_src_t *src2 = NULL; if (stream_src_open_stream_at(&src2, ncm_cache_writer_tmp_path(w2), part_size) != 0) { printf(" FAIL s2 stream\n"); return -1; } net_t nt2 = { data, size, rate_Bps, jitter_ms, 0, flaky, part_size, src2, w2, NCM_OK }; pthread_t n2; double t0 = now_s(); pthread_create(&n2, NULL, net_thread, &nt2); /* 预缓冲(前缀 ≥128KB 则立即满足;不足则等续写下足) */ double wd_t0 = now_s(); int wr; while ((wr = stream_src_wait(src2, 128 * 1024, 100)) == 0) if (now_s() - wd_t0 > 120) { printf(" FAIL resume prebuffer watchdog\n"); g_fail = 1; _exit(1); } ncm_decoder_t *dec = NULL; int rate = 0, ch = 0, bits = 0; if (ncm_dec_open_src(&dec, src2, &rate, &ch, &bits, NULL) != 0) { printf(" FAIL resume dec open\n"); return -1; } if (audio_out_open(rate, ch, bits) != 0) { printf(" FAIL audio open\n"); g_fail = 1; _exit(1); } static unsigned char pcm[4096 * 2 * 4]; int64_t decoded = 0; for (;;) { if (now_s() - t0 > 150) { printf(" FAIL resume watchdog\n"); g_fail = 1; _exit(1); } int64_t fr = ncm_dec_read(dec, pcm, 4096); if (fr == 0) break; if (rr.first_sound_s < 0) rr.first_sound_s = now_s() - t0; if (audio_out_write(pcm, (int)fr) != (int)fr) { printf(" FAIL audio write\n"); g_fail = 1; _exit(1); } pcm_put(&rr.pcm, pcm, (size_t)fr * (size_t)ch * (size_t)bits / 8); decoded += fr; if (rate > 0) usleep((useconds_t)(fr * 1000000 / rate)); } rr.frames = decoded; rr.wall_s = now_s() - t0; stream_src_stats(src2, &rr.underruns, &rr.wait_ms); audio_out_close(); ncm_dec_close(dec); pthread_join(n2, NULL); stream_src_abort(src2); /* 已完成则无副作用 */ /* 续传玩法:读端从头跟播,产出 PCM 应与整首离线解码逐字节一致(金标准) */ CHECK(rr.pcm.n == golden->n && memcmp(rr.pcm.b, golden->b, golden->n) == 0, "续传 PCM 与整首离线解码逐字节一致 (%zu/%zu)", rr.pcm.n, golden->n); free(rr.pcm.b); /* commit 转正 + 校验最终文件与源逐字节一致 */ int ck = ncm_cache_commit(w2); CHECK(ck == 0, "resume commit rc=%d", ck); if (ck == 0) { char fp[700]; if (ncm_cache_path(&cache, key, "mp3", fp, sizeof fp) == NCM_OK) { FILE *f = fopen(fp, "rb"); if (!f) { printf(" FAIL committed 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); CHECK(rn == size && extra == EOF && memcmp(got, data, size) == 0, "committed == source (%zu/%zu bytes)", rn, size); free(got); } } } stream_src_close(src2); printf(" [%s] resume .part=%llu wall=%.2fs first_sound=%.2fs frames=%lld underruns=%d\n", tag, (unsigned long long)part_size, rr.wall_s, rr.first_sound_s, (long long)rr.frames, rr.underruns); 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); } /* 阶段3:.part 续传——中断 40% → 存 .part → 续传(略过已下前缀)→ 完整 PCM 金标准比对 */ printf("== CASE resume@10MB/s (40%% cut) ==\n"); run_resume_case("resume@10MB/s", data, size, 10485760u, 0, 0, 0.4, &golden); printf("== CASE resume@400kbps (40%% cut, 慢网续传) ==\n"); run_resume_case("resume@400kbps", data, size, 50000u, 10, 1, 0.4, &golden); free(golden.b); free(golden_cut.b); free(data); printf(g_fail ? "== STREAM SIM: FAIL ==\n" : "== STREAM SIM: ALL PASS ==\n"); return g_fail; }