# SACD DST 最小接入方案 ## 1. 目标 只解决 TTMusic 当前的一个缺口: - `SACD ISO` 中 `DST` 压缩轨道无法提取 不做下面这些事: - 不重编 FFmpeg - 不改现有 `ijkplayer` 播放链路 - 不把 `audiodecoder.sacd` 整个 Kodi 插件搬进项目 - 不在第一版里做 SACD 元数据、封面、PCM 播放支持 第一版目标很单一: - 让 `LocalMusic` 的 ISO 提取在遇到 `DST` 轨道时,也能输出 `.dsf` ## 2. 现状 TTMusic 当前已经有完整的 ArkTS ISO 提取链路: - [`LocalMusic.ets`](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/view/LocalMusic.ets) - [`IsoArchiveUtil.ets`](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/common/util/IsoArchiveUtil.ets) - [`IsoArchiveTaskHelper.ets`](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/common/util/IsoArchiveTaskHelper.ets) 现有能力已经覆盖: - 解析 SACD ISO 轨道列表 - 按扇区读取 SACD 数据 - 拆出音频 packet - 组装非 DST 的 DSD frame - 直接写出 `.dsf` - `taskpool` 进度回传 当前卡住的唯一原因是这里主动拦截了 `DST`: - [`IsoArchiveTaskHelper.ets`](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/common/util/IsoArchiveTaskHelper.ets#L881) 并且在扇区处理逻辑里,`DST` 帧目前直接返回失败: - [`IsoArchiveTaskHelper.ets`](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/common/util/IsoArchiveTaskHelper.ets#L1088) ## 3. 结论 推荐的最小方案不是把 `audiodecoder.sacd` 整套搬进来,而是只抽它的 `libdstdec`。 原因: - TTMusic 已经自己实现了 SACD ISO 的轨道解析、扇区拆包和 DSF 写出 - `audiodecoder.sacd` 里真正解决 `DST` 的核心是 `lib/libdstdec` - `src/sacd/*`、`SACDAudio.cpp`、`Settings.cpp`、Kodi addon 外壳大多是播放器插件包装,不是你当前缺的能力 也就是说,当前最小缺口不是 “不会读 SACD ISO”,而是 “已经拿到完整 DST frame 后,不会解成 DSD frame”。 ## 4. 为什么不直接搬 `audiodecoder.sacd` 这个仓库确实有用,但直接整仓接入成本不合适。 强耦合点包括: - 顶层依赖 `Kodi` - `src/SACDAudio.cpp` 是 Kodi 音频解码器实例 - `src/sacd/sacd_media.*` 依赖 `kodi::vfs::CFile` - `src/sacd/sacd_reader.h` 依赖 `kodi::addon::AudioDecoderInfoTag` - `lib/libdsdpcm` 是 DSD 转 PCM 播放用,不是抽轨必需 对 TTMusic 当前任务来说,真正要用的是: - `lib/libdstdec` 而不是: - `Addon.cpp` - `SACDAudio.cpp` - `Settings.cpp` - `lib/libdsdpcm` - `lib/id3v2lib` - Kodi 资源和插件清单 ## 5. 推荐接入位置 推荐接到 `entry` 的 `libentry.so`,不要先塞进 `ijkplayer_napi`。 理由: - 这条能力本质是“本地文件提取”,不是播放器渲染能力 - `entry` 里已经有现成的 native bridge 模式 - ArkTS 侧已经直接通过 `libentry.so` 调原生方法 - 项目里已经存在 `@Concurrent + libentry.so` 的实际用例: - [`SmbFileCache.ets`](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/common/network/SmbFileCache.ets#L181) 所以推荐目录: - `entry/src/main/cpp/third_party/sacd/libdstdec/` - `entry/src/main/cpp/sacd_dst_decoder.h` - `entry/src/main/cpp/sacd_dst_decoder.cpp` - `entry/src/main/cpp/napi_init.cpp` - `entry/src/main/cpp/CMakeLists.txt` - `entry/src/main/cpp/types/libentry/Index.d.ts` ## 6. 最小源码集合 ### 6.1 第一版只拷这些 从 `audiodecoder.sacd` 拷贝: - `lib/libdstdec/binding/dst_decoder_mt.cpp` - `lib/libdstdec/binding/dst_decoder_mt.h` - `lib/libdstdec/binding/semaphore.h` - `lib/libdstdec/decoder/decoder.cpp` - `lib/libdstdec/decoder/decoder.h` - `lib/libdstdec/ac.h` - `lib/libdstdec/common.h` - `lib/libdstdec/consts.h` - `lib/libdstdec/ct.h` - `lib/libdstdec/fh.h` - `lib/libdstdec/fr.h` - `lib/libdstdec/segment.h` - `lib/libdstdec/stream.h` ### 6.2 第一版不要拷这些 - `src/SACDAudio.cpp` - `src/SACDAudio.h` - `src/Addon.cpp` - `src/Addon.h` - `src/Settings.cpp` - `src/Settings.h` - `src/sacd/sacd_core.*` - `src/sacd/id3_tagger.*` - `src/sacd/sacd_dsf.*` - `src/sacd/sacd_dsdiff.*` - `src/sacd/sacd_dsd.*` - `lib/libdsdpcm/*` - `lib/id3v2lib/*` ### 6.3 要改掉的唯一外部耦合 `libdstdec` 当前最主要的外部耦合是日志: - `common.h` 里包含了 `` 这一块建议改成 TTMusic 自己的日志适配: - 要么改成 `hilog` - 要么第一版直接做成 no-op 日志 换句话说,第一版不需要 Kodi 文件系统,不需要 Kodi 设置,不需要 Kodi AudioDecoder 类型。 ## 7. 最小架构 ### 7.1 保留现有 ArkTS 负责的部分 继续保留 ArkTS 现有能力: - SACD TOC 解析 - ISO 扇区读取 - packet/frame 组装 - `.dsf` 写出 - `taskpool` 进度更新 ### 7.2 只新增一个原生能力 新增一个 “DST frame -> DSD frame” 的 native 解码器会话。 推荐原生接口: ```ts export interface NativeModule { createSacdDstDecoder(channelCount: number, samplerate?: number, framerate?: number): number; decodeSacdDstFrame(decoderId: number, dstFrame: ArrayBuffer): ArrayBuffer; releaseSacdDstDecoder(decoderId: number): void; } ``` 默认参数建议: - `samplerate = 2822400` - `framerate = 75` 这里 `75` 不是拍脑袋,是 `audiodecoder.sacd` 的 `sacd_disc_t::get_framerate()` 直接返回的值。 ### 7.3 为什么要做成会话 不要做成每帧都重新初始化 decoder。 原因: - `dst_decoder_t::init()` 有初始化成本 - 同一轨道连续解码时复用实例更稳 - ArkTS 在提取单轨时天然就是顺序读帧 第一版直接用单线程 slot 即可: - `dst_decoder_t(1)` 这样每次 `decode()` 都能同步拿到当前帧输出,不需要额外做 flush 逻辑。 ## 8. ArkTS 侧改法 ### 8.1 只改 `IsoArchiveTaskHelper.ets` 第一版建议只改这个文件: - [`IsoArchiveTaskHelper.ets`](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/ets/common/util/IsoArchiveTaskHelper.ets) ### 8.2 改造点 当前 `extractSacdTrackEntry()` 里这段需要调整: - 遇到 `frameFormat === DST` 时,不再直接返回失败 建议改成: 1. 轨道开始前,按 `channelCount` 创建一个 `DST decoder session` 2. `processSacdSector()` 继续负责把 `DST` packet 组装成完整 frame 3. 一旦一个完整 `DST frame` 就绪,调用 native `decodeSacdDstFrame()` 4. 拿到返回的 `DSD frame` 后,继续复用当前 `writeSacdFrameToDsf()` 5. 轨道结束后释放 decoder ### 8.3 最小逻辑变化 当前逻辑里这段是阻断点: ```ts if (assembler.dstEncoded) { return false; } ``` 第一版应改成: - `assembler.dstEncoded === false`: - 走现有 `writeSacdFrameToDsf()` - `assembler.dstEncoded === true`: - 把 `assembler.buffer.subarray(0, assembler.size)` 送去 native 解码 - 解码结果再写入 `writeSacdFrameToDsf()` ### 8.4 进度逻辑可以完全复用 因为扇区读取和写文件仍在 ArkTS,当前这些逻辑都不用重写: - `advanceIsoExtractProgress()` - `reportIsoExtractProgress()` - `taskpool.Task.sendData()` 这也是这个方案比“整套搬 C++ 抽轨器”更适合当前仓库的原因。 ## 9. native 侧最小实现 ### 9.1 `sacd_dst_decoder.cpp` 建议内部结构: ```cpp struct SacdDstDecoderSession { int64_t id; uint32_t channelCount; uint32_t samplerate; uint32_t framerate; size_t dsdFrameSize; std::unique_ptr decoder; std::vector decodeBuffer; }; ``` ### 9.2 三个导出方法 #### `createSacdDstDecoder` 职责: - 校验 `channelCount` - 创建 `dst_decoder_t(1)` - 调 `init(channelCount, 2822400, 75)` - 分配 `decodeBuffer` - 返回 session id #### `decodeSacdDstFrame` 输入: - `decoderId` - `ArrayBuffer dstFrame` 职责: - 从 NAPI 取出 `ArrayBuffer` - 调 `decoder->decode(dstBytes, dstSize, &outPtr, &outSize)` - 把 `decodeBuffer` 打包成新的 `ArrayBuffer` 返回 输出: - 一帧完整 DSD 数据 #### `releaseSacdDstDecoder` 职责: - 释放 session ### 9.3 `CMakeLists.txt` 在 [`entry/src/main/cpp/CMakeLists.txt`](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/cpp/CMakeLists.txt) 中: - 给 `entry` 目标增加 `sacd_dst_decoder.cpp` - 把 `libdstdec` 源码一并编进 `entry` - 增加 `third_party/sacd/libdstdec` 相关头文件目录 - 继续链接 `libace_napi.z.so` 和 `hilog_ndk.z` 第一版不需要: - `WavPack` - `Iconv` - `Kodi` ## 10. TypeScript 类型声明 在 [`entry/src/main/cpp/types/libentry/Index.d.ts`](/mnt/d/harmony/2025/qimeng/TTMusic/entry/src/main/cpp/types/libentry/Index.d.ts) 里追加: ```ts createSacdDstDecoder(channelCount: number, samplerate?: number, framerate?: number): number; decodeSacdDstFrame(decoderId: number, dstFrame: ArrayBuffer): ArrayBuffer; releaseSacdDstDecoder(decoderId: number): void; ``` ArkTS 使用方式仍保持和 SMB bridge 一样: ```ts import nativeBridge from 'libentry.so'; ``` ## 11. 推荐落地顺序 ### Step 1 先把 `libdstdec` 编进 `libentry.so`,做一个本地自测接口: - 输入固定的 `DST frame` - 输出 `DSD frame` 先确认: - 编译过 - native 方法能从 ArkTS 调通 - `ArrayBuffer` 往返正常 ### Step 2 在 `IsoArchiveTaskHelper.ets` 里只改 `DST` 分支: - 非 `DST` 保持原样 - `DST` 走 native 解码 ### Step 3 跑 3 组真机测试: - 2ch DST SACD ISO - MCH DST SACD ISO - 普通非 DST SACD ISO 确保: - DST 能提取 - 非 DST 不回归 - 混合选择时部分成功逻辑正常 ### Step 4 最后再考虑是否补: - 提取后的 track title / album 元数据 - 封面 - 更细粒度的错误码 这些都不应该挡住第一版上线。 ## 12. Plan B 如果实际验证发现 TTMusic 当前 ArkTS 的 `DST frame` 组装和 `sacd_disc_t::read_frame()` 在某些碟片上不兼容,再退回到第二方案: - 再搬 `src/sacd/scarletbook.*` - 再搬 `src/sacd/sacd_disc.*` - 自己改造 `sacd_media.*` - 让 native 负责 “ISO -> DST/DSD frame” 但这应该是 `Plan B`,不是第一步。 理由很简单: - 接入量更大 - Kodi 耦合更多 - 调试成本更高 - 你当前仓库已经有一套可工作的 ArkTS 解析链路,没必要先推倒重来 ## 13. 风险与边界 ### 13.1 许可证 `audiodecoder.sacd` 顶层是 `GPL-2.0 / GPL-2.0-or-later` 路线,接入前必须先明确你项目能否接受对应分发约束。 `libdstdec/binding/dst_decoder_mt.*` 文件头本身写的是 `LGPL-2.1 or later`,但整仓来源仍然需要你按第三方合规去核。 这个问题不是技术问题,是分发问题。 ### 13.2 日志适配 `libdstdec/common.h` 当前带 `kodi::Log`,需要改成本地日志。 ### 13.3 首版不做元数据 第一版目标只保证: - 能提取 - 生成 `.dsf` - 不回归非 DST 不保证: - SACD 文本信息完整保留 - 专辑/曲目名从 SACD TOC 自动灌进 DSF tag 这些可以后续再做。 ## 14. 最终建议 对 TTMusic 来说,最小、最稳、最符合当前代码结构的方案是: 1. 不接 `audiodecoder.sacd` 整套 2. 只抽 `libdstdec` 3. 继续复用现有 ArkTS 的 SACD 扇区解析和 DSF writer 4. 把 `DST frame -> DSD frame` 这一步补到 `libentry.so` 如果你下一步要我继续做,实现顺序建议是: 1. 先把 `libdstdec` 接进 `entry/src/main/cpp` 2. 先把 `libentry.so` 的 3 个 native API 做出来 3. 再把 `IsoArchiveTaskHelper.ets` 的 `DST` 分支接上去