Files
MeowOS-For-R3P2/patch_v2/ncm_app/bench/stream_sim.c
T
MeowOS 19e8fc93ed feat(ncm §24): 流媒体客户端边下边播阶段3——.part断点续传(Range/206/200/416)+LRU防淘汰(.tmp/.part跳过)+失败重试1次退避
按 devdocs/plan/NCM_STREAM_PLAY.md §8 阶段3:
- .part 断续传:中断留 .part,续传带 Range,206 追加 / 200 丢头段 / 416 删残留整首重下;URL 每次新拉
- LRU 防淘汰:evict/total_bytes 跳过 .tmp/.part(在播文件不被动过)
- 失败重试:预缓冲前网络失败重试 1 次,退避 800ms,重试拿新签名 URL(风控:0 真实请求,未部署)
- 顺手修:gb_to_bytes 返回 int 溢出致默认 10GB 上限失效 -> uint64_t
- 验证:stream_sim(续传 PCM 金标准+全矩阵)/player_flow_sim(P9/P10/P11)/cache_evict_sim(case7/8)/test_ncm_core 宿主+qemu-mipsel 全 PASS;build_ncm.sh 0 warning;ncm_host 离屏正常

md5 ncm f8f316a1e6f3bdd8f6b5921fdbb3f3a2
2026-09-01 20:00:14 +08:00

574 lines
26 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <time.h>
#include <sys/stat.h>
/* ---- 全局态(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;
}