454 lines
20 KiB
C
454 lines
20 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 */
|
||
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;
|
||
}
|