按 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
574 lines
26 KiB
C
574 lines
26 KiB
C
/* 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;
|
||
}
|