- 音视频
- 移动开发
【免费下载链接】SpotiFLAC-Mobile
Mobile music utility built with Flutter and Go. High-quality audio management for your personal library. - open source, no ads, no subscription.
本文围绕 SpotiFLAC-Mobile 中 android/app/src/androidTest/assets/README.md 这份测试资产文档展开,详解dsd-pattern.wv合成 DSD 数据与motion-probe-*.mp4运动封面(motion artwork)夹具的生成原理、位级格式约定,以及它们如何支撑 WavPack DSD 无损解码、DoP 打包与 FFmpeg 能力探针测试。读者将掌握这些合成夹具的构造公式、在 instrumentation 测试 APK 中的实际调用链,以及通过adb reverse与MotionArtworkProxy驱动加密 HLS 输入的可选 gate 流程。
一、为什么需要合成测试夹具:原生音频栈的“已知字节”验证策略
SpotiFLAC-Mobile 是使用 Flutter 与 Go/Rust 后端构建的开源高保真音乐工具,Android 侧同时依赖 WavPack 原生解码(C++,见 android/app/src/main/cpp/native_audio.cpp)、Oboe/AAudio 独占输出(见 android/app/src/main/kotlin/com/zarz/spotiflac/HiResPlayback.kt)以及通过ffmpeg-kit-audio内置的 FFmpeg 转码链路(见 android/app/src/main/kotlin/com/zarz/spotiflac/NativeDownloadFinalizer.kt)。
这些原生组件有两个共性:
- 它们不属于音乐录音,无法用真实版权素材做自动化断言;
- 它们的正确性必须逐字节验证——DSD 解码是否丢位、声道顺序是否错乱、seek 后位对齐是否保留、FFmpeg 依赖裁剪后编码器/解复用器是否仍然可用。
因此仓库在 instrumentation 测试 APK(androidTest)中打包了三类原始生成(original generated)的合成夹具:dsd-pattern.wv、motion-probe-h264.mp4与motion-probe-hevc.mp4。它们只随测试 APK 发布,不进入主应用包,见 android/app/src/androidTest/assets 目录。
二、dsd-pattern.wv:按公式生成的合成 DSD64 测试数据
2.1 生成参数与位级公式
dsd-pattern.wv并非录音,而是用WavPack 5.9.0编码的立体声DSD64 DSF测试文件,规格如下:
| 参数 | 值 |
|---|---|
| 采样格式 | DSD64(采样率 2,822,400 Hz,由 DSD64 的 64×44.1kHz 得来) |
| 声道数 | 2(立体声) |
| 每通道数据字节数 | 16,384 字节 |
| 通道块大小 | 4,096 字节 |
| 编码器 | WavPack 5.9.0 |
| 输入容器 | DSF |
其数据内容由确定性公式生成:规范化后的MSB-first字节i(第i个字节)在声道c上的值为
(i * 17 + c * 29) & 255这一线性同余式保证每个字节可离线重算,测试断言因此拥有“已知字节”(known bytes)基准。关键细节是:写入 DSF 输入时这些字节的位序被反转(LSB-first),这是 DSD 数据在 DSF 容器中常见的位序约定(关于 DSF 容器解析,见 android/app/src/main/kotlin/com/zarz/spotiflac/DsdFile.kt 中的readDsf():它读取fmt块的位序字段order,当order == 1时置reverseBits = true,用Integer.reverse(value) ushr 24逐字节还原为 MSB-first)。
2.2 为什么必须用合成数据:无损解码的逐字节断言
文档明确指出,该夹具用于测试四类能力,且都针对“已知字节”做精确匹配:
- 无损 WavPack DSD 解码:解码输出必须逐字节等于公式值,任何位翻转、丢字节都会导致断言失败;
- 声道顺序:
(i * 17 + c * 29) & 255中声道系数c * 29让左右声道的字节模式可区分,从而验证多声道交织与解交织; - seek 对齐:seek 到任意位置后,从该位置继续读取的字节仍符合公式,且位对齐不被破坏;
- 原生 LE/BE 打包与 DoP 打包:同一份 DSD 数据可被重打包为
dsd_be(MSB-first 原生)、dsd_le(LSB-first)与 DoP(DSD over PCM)三种编码输出,逐一校验。
三、从源码看 DSD 夹具的实际调用链
3.1 instrumentation 测试:HiResPlaybackTest
android/app/src/androidTest/kotlin/com/zarz/spotiflac/HiResPlaybackTest.kt 直接消费dsd-pattern.wv:
fixture()通过InstrumentationRegistry.getInstrumentation().context.assets.open("dsd-pattern.wv")把资产复制到 cacheDir;wavPackDsdDecodesOriginalBitsAndChannelOrderForNativeAndDop遍历("dsd_be" to 4)、("dsd_le" to 4)、("dop" to 3)、("dop" to 4)四种(encoding, subslot)组合,用DsdSource.readFrames(encoding, slot, 37)分块读取,逐帧断言expected = (position * 17 + channel * 29) and 255,并对 DoP 输出校验 0 填充字节与0x05标记字节(assertEquals(5, data[start + slot - 1].toInt())),最终断言游标精确停在16384——即每通道 16,384 字节被完整消费;wavPackSeekRetainsBitAlignmentAndSurvivesClosedSafLease通过/proc/self/fd/打开已关闭 SAF lease 的 fd,依次 seek 到20000µs / 0 / 40000µs,断言 seek 后首帧仍符合(cursor + index) * 17 + channel * 29;dsdCannotBeSentToAAudioAsPcm验证对HiResPlayback下发 prepare 后返回"reason": "dsd_unsupported"且"fatal": true——与 android/app/src/main/kotlin/com/zarz/spotiflac/HiResPlayback.kt 的注释“DSD never enters a PCM stream”(DSD 绝不进入 PCM 流)一致:prepare()先经DsdSource.isDsd(path)(检查DSD/FRM8/wvpk魔数)或DsdSource.open(path)探测,命中即拒绝。
3.2 原生解码层:WavPackDsdSource与 JNI
android/app/src/main/kotlin/com/zarz/spotiflac/DsdSource.kt 定义WavPackDsdSource:open()调用NativeAudio.openWavPack(android/app/src/main/kotlin/com/zarz/spotiflac/NativeAudio.kt),C++ 侧在 android/app/src/main/cpp/native_audio.cpp 中用WavpackOpenFileInput(path, error, OPEN_DSD_NATIVE | OPEN_ALT_TYPES, 0)打开,并以WavpackGetQualifyMode(context) & QMODE_DSD_AUDIO判定 DSD 内容;OPEN_DSD_NATIVE保证返回交织、MSB-first 的 DSD 字节(与夹具的规范化约定一致)。
读取路径上的格式转换逻辑(同样位于WavPackDsdSource.readFrames):
dsd_be:按 MSB-first 顺序输出 4 字节组;dsd_le:按 LSB-first 顺序(3 - index)输出;- DoP:每组 2 个 DSD 字节被打进 PCM 槽位,
subslot == 4时先写一个0填充字节,并在槽尾写标记字节5(0x05),Dart/原生传输层负责连续05/FA标记的分配(见DsdFile.readFrames中 DoP 分支注释)。
seek()将目标微秒按group(DoP 为 2、其余为 4)取整对齐,再调用NativeAudio.seekWavPack做样本级 seek,这正是“seek 保持位对齐”测试成立的关键。
3.3 Dart 侧播放编排:requiresDsd与allowDop
Dart 侧 lib/services/music_playback_deck.dart 的setSource()接收preferBitPerfect / directUsb / allowDop / requiresDsd等参数并通过 MethodChannel 下发到原生;若原生返回fatal: true(即 DSD 不支持),Dart 侧抛出UsbDsdUnavailable(“DSD requires a compatible direct USB output”)。相关设置项见 lib/models/settings.dart 中的usbDsdOverPcm,并在 lib/services/music_player_service.dart 中作为allowDop透传。由此,dsd-pattern.wv不仅验证解码正确性,还验证了“DSD 文件在无兼容输出时应被明确拒绝、而非静默降级”的产品行为。
四、运动封面夹具:motion-probe-h264.mp4与motion-probe-hevc.mp4
4.1 生成命令
两份夹具均为一秒 FFmpegtestsrc2测试图案,32×32 像素、2 帧每秒,分别用 H.264 与 HEVC 编码:
# H.264 ffmpeg -f lavfi -i testsrc2=size=32x32:rate=2 -t 1 -an -pix_fmt yuv420p \ -movflags +faststart -c:v libx264 # HEVC ffmpeg -f lavfi -i testsrc2=size=32x32:rate=2 -t 1 -an -pix_fmt yuv420p \ -movflags +faststart -c:v libx265 -x265-params log-level=error:pools=1 \ -tag:v hvc1要点:
- 去掉音频轨(
-an),因为夹具只验证视频解码与重封装; -movflags +faststart把 moov 移到文件头,便于流式读取;- HEVC 侧
-tag:v hvc1保证四字节编码器标签被多数播放/解复用链路正确识别。
4.2 用途:依赖裁剪后的解码与 stream-copy 回归
README 说明这两份夹具用于在缩减内置 FFmpeg 依赖之后,验证两件事:解码能力仍可用,以及MP4 stream-copy(-c:v copy重封装)仍可用。从 android/app/src/androidTest/kotlin/com/zarz/spotiflac/FFmpegCapabilitiesTest.kt 可看到其严格校验风格:
- 解码验证使用
-xerror -err_detect explode(把任何可检测错误提升为致命错误),-map 0:v:0 -an -sn -dn -f null -; - 重封装验证为
-map 0:v:0 -an -c:v copy -movflags +faststart,并断言输出文件非空。
之所以“只打包在 instrumentation APK”,是因为它们是纯测试资产:把 32×32 的视频塞进主 APK 只会无谓增大包体,而运动封面(motion artwork)的生产数据来自网络下载(lib/services/motion_artwork_store.dart 中持久化为motion_artwork/<sha256>.mp4,remux 为静音、自包含 MP4,输出受-t 30与-fs 25165824(24 MiB)限制)。
五、FFmpegCapabilitiesTest:能力探针的完整覆盖面
android/app/src/androidTest/kotlin/com/zarz/spotiflac/FFmpegCapabilitiesTest.kt 是夹具 README 的直接消费者,其目标注释明确为“Exercise shipped libraries rather than the host's unrelated FFmpeg build”(锻炼随包内置库,而非宿主上无关的 FFmpeg 构建)。它统一经NativeDownloadFinalizer.runFFmpegArguments(arrayOf("-v", "error", "-y", *arguments))执行,确保走的是应用内置 FFmpegKit 链路。覆盖四块:
- 音频编码器:
flac、m4a→alac、wav→pcm_s24le、aiff→pcm_s24be、mp3→libmp3lame、opus→libopus、aac→aac,用sine=frequency=997:sample_rate=48000生成 0.5 秒信号编码后回读解码; - 分析滤波器:
astats=metadata=1:reset=0,ebur128=peak=true:metadata=1,ametadata=print输出频谱统计文本,断言含lavfi.astats; - 频谱图与封面转换:
showspectrumpic生成 PNG,再scale=32:32转 JPEG 并断言非空; - 运动封面解码与重封装:从 assets 读取
motion-probe-h264.mp4/motion-probe-hevc.mp4,先严格解码、再 stream-copy 重封装,并断言 remux 产物可被再次严格解码。
六、加密 HLS gate:MotionArtworkProxy与adb reverse
6.1 可选 gate 的定位
README 特别说明:以上普通 fixture 测试不需要任何服务器或额外参数;只有需要验证加密 HLS(encrypted HLS)输入链路时,才启用“live gate”。这是因为真实运动封面源常常是带鉴权/加密的 HLS,而内置 FFmpegKit 的音频包“exposes HTTP but has no native HTTPS/TLS support”(详见 scripts/README_ios_ffmpeg_capabilities.md),TLS 与资源放行必须由 Dart 侧的代理接管。
6.2 启用步骤
# 1. 将测试机上的代理本地端口反向转发到宿主机同端口 adb reverse tcp:PORT tcp:PORT # 2. 运行带 gate 参数的 instrumentation 测试 adb shell am instrument \ -e motionProxyUrl http://127.0.0.1:PORT/TOKEN/0.m3u8 \ -w com.zarz.spotiflac.test/androidx.test.runner.AndroidJUnitRunner \ com.zarz.spotiflac.FFmpegCapabilitiesTestgate 的完整流程是:代理解码输入 → remux → 用随包原生库再次解码产物 MP4,因此一次运行同时覆盖 HLS 拉流、重封装与内置解码三段链路。TOKEN是代理的随机资源令牌(见下文)。
6.3MotionArtworkProxy的源码级设计
lib/services/motion_artwork_proxy.dart 实现了这一 gate:
- 架构:
MotionArtworkProxy.start()在 loopback 上绑定随机端口(HttpServer.bind(InternetAddress.loopbackIPv4, 0)),FFmpeg 只接触短命 loopback URL,上游 TLS 握手由 DartHttpClient完成——注释原话是“Dart handles upstream TLS; FFmpeg sees only a short-lived loopback URL”; - 资源白名单:
_register()把每个远端 URL 映射为/TOKEN/序号[.后缀]的本地路径,同时保留 HLS 分段的后缀名(“HLS validates segment extensions before opening them”),上限 4096 个资源、单 URL 长度上限 8192 字符;只有初始资源与 HLS playlist 中发现的 URL 才被允许请求; - 令牌安全:24 字节随机 token(
Random.secure())隐藏上游真实 URL 与查询串; - 传输预算:
maxBytes默认24 << 20(24 MiB),跨 playlist、密钥与分段共享累积预算,超限即close()并抛HttpException('Artwork transfer budget exceeded')——对应测试用例 “cumulative byte limit closes the proxy instead of retaining video”(见 test/motion_artwork_proxy_test.dart); - 生命周期:默认 30 秒
Timer自动关闭,杜绝代理泄漏; - HLS 重写:
_handle读取前 7 字节前缀与contentType/#EXTM3U特征判定 playlist;命中后解码 gzip/deflate、限制 512 KiB,逐行重写非注释行与URI="..."属性(含#EXT-X-KEY的 AES-128 密钥行)为本地令牌化 URL,再以application/vnd.apple.mpegurl返回。测试用例 “HLS rewrites master, redirected media, key, map and byte ranges” 完整覆盖了重定向 master、AES-128 key、key.bin?token=secret查询串与分段字节范围场景; - 协议卫生:仅允许 GET/HEAD,拒绝带查询串的本地请求(404),仅透传
contentType/contentRange/acceptRanges/contentEncoding头,自动跟随最多 5 次重定向。
在 android/app/src/androidTest/kotlin/com/zarz/spotiflac/FFmpegCapabilitiesTest.kt 的motionArtworkDecodingAndRemuxRemainAvailable()中,gate 分支读取InstrumentationRegistry.getArguments().getString("motionProxyUrl"),并以一组刻意的 FFmpeg 输入选项约束解复用行为:
-protocol_whitelist file,http,https,tcp,tls,crypto -rw_timeout 10000000 -http_multiple 0 -http_persistent 0 -extension_picky 0其中-http_multiple 0 -http_persistent 0的动机在 lib/services/motion_artwork_store.dart 中有注释说明:HLS 字节范围可共享同一个 MP4 URL,复用/预取 HTTP 连接会把下一个 range 拼进未完成的分段中;-extension_picky 0则允许不透明/无扩展名的分段路径(令牌化 URL 保留了常规后缀,但公开 HLS 源可能使用不透明路径)。同样的选项组在 scripts/README_ios_ffmpeg_capabilities.md 的 iOS 模拟器探针结果表中被验证为唯一可让“不透明分段 URL”通过的解复用配置。
七、如何运行与验证
普通 fixture 验证(无需服务器):
# 构建并安装 instrumentation 测试 APK 后,直接运行 adb shell am instrument \ -w com.zarz.spotiflac.test/androidx.test.runner.AndroidJUnitRunner \ com.zarz.spotiflac.HiResPlaybackTest \ com.zarz.spotiflac.FFmpegCapabilitiesTest启用加密 HLS gate 时,按第六节步骤增加-e motionProxyUrl ...参数。HiResPlaybackTest与FFmpegCapabilitiesTest均通过assets.open(...)读取夹具并复制到targetContext.cacheDir的独立临时目录(命名带System.nanoTime()随机后缀),并在finally中递归清理,避免测试间相互污染。
八、小结
android/app/src/androidTest/assets/下的三份合成夹具体现了一个可复用的原生栈验证方法论:
- 用确定性公式生成数据(
(i * 17 + c * 29) & 255),让“无损”与“位序正确”成为可机器断言的硬性事实,而非主观听感; - 把夹具与 instrumentation APK 绑定,保持主 APK 纯净,同时让真机/模拟器上的原生库回归随时可跑;
- 把不可控的外部输入(加密 HLS)隔离在可选 gate 之后,由
MotionArtworkProxy统一承担 TLS、资源白名单、传输预算与生命周期管理,测试默认路径零外部依赖。
若要继续深入,建议阅读:android/app/src/main/kotlin/com/zarz/spotiflac/DsdSource.kt、android/app/src/main/kotlin/com/zarz/spotiflac/DsdFile.kt、android/app/src/main/cpp/native_audio.cpp、lib/services/motion_artwork_proxy.dart 与 test/motion_artwork_proxy_test.dart。
- 音视频
- 移动开发
【免费下载链接】SpotiFLAC-Mobile
Mobile music utility built with Flutter and Go. High-quality audio management for your personal library. - open source, no ads, no subscription.
相关推荐
MediaGo 合成媒体夹具体系:基于 FFmpeg 的测试媒体生成、只读回环服务与版本化校验
MediaGo 合成媒体夹具体系:基于 FFmpeg 的测试媒体生成、只读回环服务与版本化校验 tests/media service/ 是 MediaGo 仓
音视频桌面应用后端BoltzGen vs AlphaFold:通用结合剂设计工具的革命性突破
BoltzGen vs AlphaFold:通用结合剂设计工具的革命性突破 在计算生物学领域,蛋白质结构预测和设计一直是科学家们关注的焦点。BoltzGen作为
Local Deep Research 测试夹具实战:SearchEngineMocks 与 Wikipedia URL 安全校验
Local Deep Research 测试夹具实战:SearchEngineMocks 与 Wikipedia URL 安全校验 本文以 Local Deep
AI应用人工智能大模型RAGAI Agent深度研究本地部署后端前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考